feat(feishu): add /model command for per-chat model switching
The bridge only supported a single global model (DEEPSEEK_MODEL env /
default_text_model in config.toml). Users who wanted a different model
for a particular Feishu group had to restart the bridge with a different
env var — impractical and disruptive.
This commit adds per-chat model switching so users in a group chat can
run "/model <name>" to switch the model for all future threads and
turns in that chat, without affecting other chats.
Changes:
- **lib.mjs — commandAction()**: handle "model" command → { kind:
"set_model", modelName }.
- **index.mjs**:
- setChatModel(chatId, modelName): store/clear per-chat model in the
thread store. "/model default" resets to the bridge-level default.
- ensureThread(): read per-chat model from store when creating a
new runtime thread; fall back to config.model.
- runPrompt(): read per-chat model for each turn submission
(independent of the thread-creation model), so a /model change
takes effect on the very next message.
Usage:
/model deepseek-v4-flash — switch this chat to Flash
/model deepseek-v4-pro — switch this chat to Pro
/model default — reset to bridge default
Resolution priority (per-chat): global default < per-chat /model.
/ 新增 /model 命令,支持按飞书群设置独立模型。
/ 群内输入 /model <name> 切换,/model default 恢复全局默认。
This commit is contained in:
@@ -231,6 +231,9 @@ async function handleCommand(chatId, command) {
|
||||
case "approval":
|
||||
await decideApproval(chatId, action);
|
||||
return;
|
||||
case "set_model":
|
||||
await setChatModel(chatId, action.modelName);
|
||||
return;
|
||||
case "prompt":
|
||||
await runPrompt(chatId, action.prompt);
|
||||
return;
|
||||
@@ -243,10 +246,14 @@ async function ensureThread(chatId, { forceNew = false } = {}) {
|
||||
const existing = await threadStore.getChat(chatId);
|
||||
if (existing?.threadId && !forceNew) return existing;
|
||||
|
||||
// Use per-chat model if set, fall back to bridge-level default.
|
||||
// / 优先使用 per-chat 模型(/model 命令设置),否则用桥接级别的默认模型。
|
||||
const effectiveModel = existing?.model || config.model;
|
||||
|
||||
const thread = await runtimeJson("/v1/threads", {
|
||||
method: "POST",
|
||||
body: {
|
||||
model: config.model,
|
||||
model: effectiveModel,
|
||||
workspace: config.workspace,
|
||||
mode: config.mode,
|
||||
allow_shell: config.allowShell,
|
||||
@@ -274,6 +281,10 @@ async function runPrompt(chatId, prompt) {
|
||||
return;
|
||||
}
|
||||
const state = await ensureThread(chatId);
|
||||
// Use per-chat model for this turn (may differ from the thread's
|
||||
// creation model if the user ran /model after the thread was created).
|
||||
// / 使用 per-chat 模型执行本轮对话(如果用户在创建线程后切换过模型)。
|
||||
const effectiveModel = state?.model || config.model;
|
||||
const detail = await runtimeJson(`/v1/threads/${encodeURIComponent(state.threadId)}`);
|
||||
const activeBlock = activeTurnBlock(detail, state);
|
||||
if (activeBlock) {
|
||||
@@ -296,7 +307,7 @@ async function runPrompt(chatId, prompt) {
|
||||
body: {
|
||||
prompt,
|
||||
input_summary: prompt.slice(0, 200),
|
||||
model: config.model,
|
||||
model: effectiveModel,
|
||||
mode: config.mode,
|
||||
allow_shell: config.allowShell,
|
||||
trust_mode: config.trustMode,
|
||||
@@ -553,6 +564,24 @@ async function decideApproval(chatId, action) {
|
||||
await sendText(chatId, `Approval ${approvalId}: ${decision}${remember ? " and remember" : ""}`);
|
||||
}
|
||||
|
||||
async function setChatModel(chatId, modelName) {
|
||||
// /model <name> — set per-chat model; "default" or empty resets to bridge default.
|
||||
// / /model "default" 或空参数 — 恢复桥接级别的默认模型。
|
||||
if (!modelName || modelName === "default") {
|
||||
await threadStore.patchChat(chatId, {
|
||||
model: null,
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
await sendText(chatId, `Reset per-chat model. Using bridge default: ${config.model}`);
|
||||
return;
|
||||
}
|
||||
await threadStore.patchChat(chatId, {
|
||||
model: modelName,
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
await sendText(chatId, `Per-chat model set to: ${modelName}`);
|
||||
}
|
||||
|
||||
async function sendText(chatId, text) {
|
||||
// Try reply API first — keeps bot responses inside the same Feishu
|
||||
// thread/topic instead of spawning new standalone topics.
|
||||
|
||||
@@ -147,6 +147,11 @@ export function commandAction(command) {
|
||||
return { kind: "interrupt" };
|
||||
case "compact":
|
||||
return { kind: "compact" };
|
||||
case "model":
|
||||
// /model <model_name> — switch per-chat default model.
|
||||
// Stored in thread store and used for future threads/turns.
|
||||
// Pass "default" to reset to the bridge-level default.
|
||||
return { kind: "set_model", modelName: command.args };
|
||||
case "allow":
|
||||
return { kind: "approval", decision: "allow", ...parseApprovalDecisionArgs(command.args) };
|
||||
case "deny":
|
||||
|
||||
Reference in New Issue
Block a user