From feefae16c6fb0d2c7caaf3194f5683f05f3db1ec Mon Sep 17 00:00:00 2001 From: Hunter B Date: Sat, 30 May 2026 23:55:00 -0700 Subject: [PATCH] fix(feishu): preserve per-chat model state --- integrations/feishu-bridge/src/index.mjs | 30 +++++++++++++------- integrations/feishu-bridge/src/lib.mjs | 12 ++++++++ integrations/feishu-bridge/test/lib.test.mjs | 26 +++++++++++++++++ 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/integrations/feishu-bridge/src/index.mjs b/integrations/feishu-bridge/src/index.mjs index 0a485b5c..69b1b968 100644 --- a/integrations/feishu-bridge/src/index.mjs +++ b/integrations/feishu-bridge/src/index.mjs @@ -16,6 +16,7 @@ import { parseList, parseApprovalDecisionArgs, parseTextContent, + preservedChatStateFields, splitMessage, stripGroupPrefix } from "./lib.mjs"; @@ -266,6 +267,7 @@ async function ensureThread(chatId, { forceNew = false } = {}) { }); const state = { + ...preservedChatStateFields(existing), threadId: thread.id, lastSeq: 0, activeTurnId: null, @@ -505,7 +507,9 @@ async function resumeThread(chatId, args) { return; } const detail = await runtimeJson(`/v1/threads/${encodeURIComponent(threadId)}`); + const existing = await threadStore.getChat(chatId); await threadStore.setChat(chatId, { + ...preservedChatStateFields(existing), threadId, lastSeq: Number(detail.latest_seq || 0), activeTurnId: null, @@ -601,22 +605,28 @@ async function sendText(chatId, text) { throw new Error("Lark SDK client does not expose im message create API"); } + let canReply = Boolean(replyMessage); for (const chunk of splitMessage(text, config.maxReplyChars)) { const body = { msg_type: "text", content: JSON.stringify({ text: chunk }) }; - if (replyMessage) { - await replyMessage({ - path: { message_id: replyToMessageId }, - data: body - }); - } else { - await createMessage({ - params: { receive_id_type: "chat_id" }, - data: { ...body, receive_id: chatId } - }); + if (canReply) { + try { + await replyMessage({ + path: { message_id: replyToMessageId }, + data: body + }); + continue; + } catch (error) { + canReply = false; + console.warn("Feishu reply API failed; falling back to message create", error); + } } + await createMessage({ + params: { receive_id_type: "chat_id" }, + data: { ...body, receive_id: chatId } + }); } } diff --git a/integrations/feishu-bridge/src/lib.mjs b/integrations/feishu-bridge/src/lib.mjs index 2408fe81..217b6beb 100644 --- a/integrations/feishu-bridge/src/lib.mjs +++ b/integrations/feishu-bridge/src/lib.mjs @@ -166,6 +166,17 @@ export function commandAction(command) { } } +export function preservedChatStateFields(state = {}) { + const preserved = {}; + if (Object.prototype.hasOwnProperty.call(state || {}, "model")) { + preserved.model = state.model || null; + } + if (state?.replyToMessageId) { + preserved.replyToMessageId = state.replyToMessageId; + } + return preserved; +} + export function splitMessage(text, maxChars = 3500) { const value = String(text || ""); const chars = Array.from(value); @@ -346,6 +357,7 @@ export function helpText() { "/threads - recent runtime threads", "/new - create a new thread for this chat", "/resume - bind this chat to an existing thread", + "/model - set or reset this chat's model", "/interrupt - interrupt the active turn", "/compact - compact the current thread", "/allow [remember] - approve a pending tool call", diff --git a/integrations/feishu-bridge/test/lib.test.mjs b/integrations/feishu-bridge/test/lib.test.mjs index ca242035..40e264d2 100644 --- a/integrations/feishu-bridge/test/lib.test.mjs +++ b/integrations/feishu-bridge/test/lib.test.mjs @@ -12,8 +12,10 @@ import { parseCommand, parseList, parseTextContent, + preservedChatStateFields, splitMessage, stripGroupPrefix, + helpText, validateBridgeConfig } from "../src/lib.mjs"; @@ -89,12 +91,36 @@ test("commandAction maps bridge commands and falls back to prompts", () => { kind: "resume", threadId: "thread-1" }); + assert.deepEqual(commandAction(parseCommand("/model deepseek-v4-pro")), { + kind: "set_model", + modelName: "deepseek-v4-pro" + }); assert.deepEqual(commandAction(parseCommand("/unknown value")), { kind: "prompt", prompt: "/unknown value" }); }); +test("helpText documents per-chat model switching", () => { + assert.match(helpText(), /\/model /); +}); + +test("preservedChatStateFields carries model across state replacement", () => { + assert.deepEqual( + preservedChatStateFields({ + threadId: "old-thread", + model: "deepseek-v4-flash", + replyToMessageId: "om_123", + activeTurnId: "turn-1" + }), + { + model: "deepseek-v4-flash", + replyToMessageId: "om_123" + } + ); + assert.deepEqual(preservedChatStateFields({ model: null }), { model: null }); +}); + test("parseApprovalDecisionArgs extracts remember flag", () => { assert.deepEqual(parseApprovalDecisionArgs("ap_123 remember"), { approvalId: "ap_123",