fix(feishu): preserve per-chat model state

This commit is contained in:
Hunter B
2026-05-30 23:55:00 -07:00
parent 30db74bcdb
commit feefae16c6
3 changed files with 58 additions and 10 deletions
+20 -10
View File
@@ -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 }
});
}
}
+12
View File
@@ -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 <thread_id> - bind this chat to an existing thread",
"/model <name|default> - set or reset this chat's model",
"/interrupt - interrupt the active turn",
"/compact - compact the current thread",
"/allow <approval_id> [remember] - approve a pending tool call",
@@ -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 <name\|default>/);
});
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",