chatccc 0.2.215 → 0.2.216

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.215",
3
+ "version": "0.2.216",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,138 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import {
8
+ FeishuMessageLedger,
9
+ createAckFirstEventHandler,
10
+ } from "../feishu-message-ingress.ts";
11
+
12
+ const tempDirs: string[] = [];
13
+
14
+ async function createLedger(maxEntries = 5): Promise<FeishuMessageLedger> {
15
+ const dir = await mkdtemp(join(tmpdir(), "chatccc-feishu-ingress-"));
16
+ tempDirs.push(dir);
17
+ return new FeishuMessageLedger(join(dir, "messages.json"), maxEntries);
18
+ }
19
+
20
+ afterEach(async () => {
21
+ await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
22
+ });
23
+
24
+ describe("createAckFirstEventHandler", () => {
25
+ it("returns before starting the asynchronous event worker", async () => {
26
+ let scheduled: (() => void) | undefined;
27
+ const schedule = vi.fn((task: () => void) => {
28
+ scheduled = task;
29
+ });
30
+ const worker = vi.fn(async () => {});
31
+ const onError = vi.fn();
32
+ const handler = createAckFirstEventHandler(worker, onError, schedule);
33
+
34
+ await expect(handler({ message: "test" })).resolves.toBeUndefined();
35
+ expect(schedule).toHaveBeenCalledOnce();
36
+ expect(worker).not.toHaveBeenCalled();
37
+
38
+ scheduled?.();
39
+ await vi.waitFor(() => expect(worker).toHaveBeenCalledWith({ message: "test" }));
40
+ expect(onError).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it("reports detached worker failures without rejecting the SDK callback", async () => {
44
+ const error = new Error("worker failed");
45
+ const onError = vi.fn();
46
+ const handler = createAckFirstEventHandler(
47
+ async () => {
48
+ throw error;
49
+ },
50
+ onError,
51
+ (task) => task(),
52
+ );
53
+
54
+ await expect(handler({})).resolves.toBeUndefined();
55
+ await vi.waitFor(() => expect(onError).toHaveBeenCalledWith(error));
56
+ });
57
+ });
58
+
59
+ describe("FeishuMessageLedger", () => {
60
+ it("rejects an already accepted message after a process restart", async () => {
61
+ const first = await createLedger();
62
+ await first.load();
63
+
64
+ expect(await first.accept({
65
+ messageId: "om_001",
66
+ chatId: "oc_chat",
67
+ createTime: 100,
68
+ })).toBe("accepted");
69
+
70
+ const restarted = new FeishuMessageLedger(first.filePath, 5);
71
+ await restarted.load();
72
+
73
+ expect(await restarted.accept({
74
+ messageId: "om_001",
75
+ chatId: "oc_chat",
76
+ createTime: 100,
77
+ })).toBe("duplicate");
78
+ });
79
+
80
+ it("rejects a unique old event after restoring the chat high-water mark", async () => {
81
+ const first = await createLedger();
82
+ await first.load();
83
+
84
+ expect(await first.accept({
85
+ messageId: "om_new",
86
+ chatId: "oc_chat",
87
+ createTime: 200,
88
+ })).toBe("accepted");
89
+
90
+ const restarted = new FeishuMessageLedger(first.filePath, 5);
91
+ await restarted.load();
92
+
93
+ expect(await restarted.accept({
94
+ messageId: "om_old",
95
+ chatId: "oc_chat",
96
+ createTime: 100,
97
+ })).toBe("stale");
98
+ });
99
+
100
+ it("accepts distinct messages with the same create timestamp", async () => {
101
+ const ledger = await createLedger();
102
+ await ledger.load();
103
+
104
+ expect(await ledger.accept({
105
+ messageId: "om_001",
106
+ chatId: "oc_chat",
107
+ createTime: 100,
108
+ })).toBe("accepted");
109
+ expect(await ledger.accept({
110
+ messageId: "om_002",
111
+ chatId: "oc_chat",
112
+ createTime: 100,
113
+ })).toBe("accepted");
114
+ });
115
+
116
+ it("keeps only the configured number of recent message IDs", async () => {
117
+ const ledger = await createLedger(2);
118
+ await ledger.load();
119
+
120
+ await ledger.accept({ messageId: "om_001", chatId: "oc_chat", createTime: 100 });
121
+ await ledger.accept({ messageId: "om_002", chatId: "oc_chat", createTime: 200 });
122
+ await ledger.accept({ messageId: "om_003", chatId: "oc_chat", createTime: 300 });
123
+
124
+ const restarted = new FeishuMessageLedger(ledger.filePath, 2);
125
+ await restarted.load();
126
+
127
+ expect(await restarted.accept({
128
+ messageId: "om_001",
129
+ chatId: "oc_other",
130
+ createTime: 100,
131
+ })).toBe("accepted");
132
+ expect(await restarted.accept({
133
+ messageId: "om_003",
134
+ chatId: "oc_chat",
135
+ createTime: 300,
136
+ })).toBe("duplicate");
137
+ });
138
+ });
@@ -73,9 +73,10 @@ describe("SimulatedPlatform", () => {
73
73
  });
74
74
 
75
75
  it("纯函数 formatDelayNotice 正常工作", () => {
76
- const notice = SimulatedPlatform.formatDelayNotice(Date.now() - 20 * 60 * 1000, "测试消息");
77
- expect(notice).toBeDefined();
78
- expect(notice).toContain("延迟送达");
76
+ const notice = SimulatedPlatform.formatDelayNotice(Date.now() - 20 * 60 * 1000, "测试消息");
77
+ expect(notice).toBeDefined();
78
+ expect(notice).toContain("延迟送达");
79
+ expect(notice).not.toContain("因服务离线");
79
80
  // 近期消息不触发
80
81
  expect(SimulatedPlatform.formatDelayNotice(Date.now(), "test")).toBeNull();
81
82
  });
package/src/feishu-api.ts CHANGED
@@ -1023,7 +1023,7 @@ export function formatDelayNotice(createTimeMs: number, messageText?: string, no
1023
1023
  }
1024
1024
 
1025
1025
  const contentLine = messageText ? `\n> 原始内容:${messageText.slice(0, 200)}` : "";
1026
- return `> ⚠️ 延迟送达提醒:此消息于 ${sendTimeStr} 发送,因服务离线,延迟约 ${delayStr}后送达${contentLine}`;
1026
+ return `> ⚠️ 延迟送达提醒:此消息于 ${sendTimeStr} 发送,现延迟约 ${delayStr}后送达${contentLine}`;
1027
1027
  }
1028
1028
 
1029
1029
  /**
@@ -0,0 +1,195 @@
1
+ import { homedir } from "node:os";
2
+ import { dirname, join } from "node:path";
3
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
+
5
+ export const MAX_PROCESSED = 5000;
6
+
7
+ export type FeishuMessageDisposition = "accepted" | "duplicate" | "stale";
8
+
9
+ export interface FeishuMessageIdentity {
10
+ messageId?: string;
11
+ chatId: string;
12
+ createTime: number;
13
+ }
14
+
15
+ interface PersistedMessageEntry {
16
+ messageId: string;
17
+ chatId: string;
18
+ createTime: number;
19
+ }
20
+
21
+ interface PersistedMessageLedger {
22
+ version: 1;
23
+ entries: PersistedMessageEntry[];
24
+ }
25
+
26
+ type ScheduleTask = (task: () => void) => void;
27
+
28
+ const defaultSchedule: ScheduleTask = (task) => {
29
+ setImmediate(task);
30
+ };
31
+
32
+ /**
33
+ * The Feishu SDK waits for an event handler's return value before sending its
34
+ * WebSocket response. Schedule the real work for the next event-loop turn so
35
+ * the SDK callback can return and acknowledge the event immediately.
36
+ */
37
+ export function createAckFirstEventHandler<T>(
38
+ worker: (data: T) => Promise<void>,
39
+ onError: (error: unknown) => void,
40
+ schedule: ScheduleTask = defaultSchedule,
41
+ ): (data: T) => Promise<void> {
42
+ return async (data) => {
43
+ schedule(() => {
44
+ void worker(data).catch(onError);
45
+ });
46
+ };
47
+ }
48
+
49
+ function isPersistedEntry(value: unknown): value is PersistedMessageEntry {
50
+ if (!value || typeof value !== "object") return false;
51
+ const entry = value as Record<string, unknown>;
52
+ return typeof entry.messageId === "string"
53
+ && entry.messageId.length > 0
54
+ && typeof entry.chatId === "string"
55
+ && typeof entry.createTime === "number"
56
+ && Number.isFinite(entry.createTime);
57
+ }
58
+
59
+ export class FeishuMessageLedger {
60
+ private readonly messageIds: Set<string>;
61
+ private entries: PersistedMessageEntry[] = [];
62
+ private latestCreateTimeByChat = new Map<string, number>();
63
+ private persistTail: Promise<void> = Promise.resolve();
64
+
65
+ constructor(
66
+ public readonly filePath: string,
67
+ private readonly maxEntries = MAX_PROCESSED,
68
+ messageIds?: Set<string>,
69
+ ) {
70
+ this.messageIds = messageIds ?? new Set<string>();
71
+ }
72
+
73
+ async load(): Promise<void> {
74
+ this.clearMemory();
75
+
76
+ try {
77
+ const raw = await readFile(this.filePath, "utf-8");
78
+ const parsed = JSON.parse(raw) as Partial<PersistedMessageLedger>;
79
+ const entries = Array.isArray(parsed.entries)
80
+ ? parsed.entries.filter(isPersistedEntry)
81
+ : [];
82
+ this.entries = entries.slice(-this.maxEntries);
83
+ this.rebuildIndexes();
84
+
85
+ if (entries.length > this.maxEntries) {
86
+ await this.persist();
87
+ }
88
+ } catch (error) {
89
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
90
+ console.error(
91
+ `[FEISHU-DEDUP] Failed to load ${this.filePath}: ${(error as Error).message}`,
92
+ );
93
+ }
94
+ }
95
+ }
96
+
97
+ async accept(identity: FeishuMessageIdentity): Promise<FeishuMessageDisposition> {
98
+ const { messageId, chatId, createTime } = identity;
99
+
100
+ if (messageId && this.messageIds.has(messageId)) {
101
+ return "duplicate";
102
+ }
103
+
104
+ const latestCreateTime = this.latestCreateTimeByChat.get(chatId);
105
+ if (latestCreateTime !== undefined && createTime < latestCreateTime) {
106
+ return "stale";
107
+ }
108
+
109
+ if (!messageId) {
110
+ this.recordLatestCreateTime(chatId, createTime);
111
+ return "accepted";
112
+ }
113
+
114
+ this.entries.push({ messageId, chatId, createTime });
115
+ this.messageIds.add(messageId);
116
+ this.recordLatestCreateTime(chatId, createTime);
117
+
118
+ if (this.entries.length > this.maxEntries) {
119
+ this.entries = this.entries.slice(-this.maxEntries);
120
+ this.rebuildIndexes();
121
+ }
122
+
123
+ try {
124
+ await this.persist();
125
+ } catch (error) {
126
+ console.error(
127
+ `[FEISHU-DEDUP] Failed to persist ${this.filePath}: ${(error as Error).message}`,
128
+ );
129
+ }
130
+ return "accepted";
131
+ }
132
+
133
+ clearMemory(): void {
134
+ this.entries = [];
135
+ this.messageIds.clear();
136
+ this.latestCreateTimeByChat.clear();
137
+ this.persistTail = Promise.resolve();
138
+ }
139
+
140
+ private recordLatestCreateTime(chatId: string, createTime: number): void {
141
+ const current = this.latestCreateTimeByChat.get(chatId);
142
+ if (current === undefined || createTime > current) {
143
+ this.latestCreateTimeByChat.set(chatId, createTime);
144
+ }
145
+ }
146
+
147
+ private rebuildIndexes(): void {
148
+ this.messageIds.clear();
149
+ this.latestCreateTimeByChat.clear();
150
+ for (const entry of this.entries) {
151
+ this.messageIds.add(entry.messageId);
152
+ this.recordLatestCreateTime(entry.chatId, entry.createTime);
153
+ }
154
+ }
155
+
156
+ private persist(): Promise<void> {
157
+ const snapshot: PersistedMessageLedger = {
158
+ version: 1,
159
+ entries: this.entries.map((entry) => ({ ...entry })),
160
+ };
161
+
162
+ const nextPersist = this.persistTail
163
+ .catch(() => {})
164
+ .then(async () => {
165
+ await mkdir(dirname(this.filePath), { recursive: true });
166
+ const tempPath = `${this.filePath}.${process.pid}.tmp`;
167
+ try {
168
+ await writeFile(tempPath, JSON.stringify(snapshot), "utf-8");
169
+ await rename(tempPath, this.filePath);
170
+ } finally {
171
+ await rm(tempPath, { force: true }).catch(() => {});
172
+ }
173
+ });
174
+ this.persistTail = nextPersist;
175
+ return nextPersist;
176
+ }
177
+ }
178
+
179
+ const defaultLedgerPath = join(
180
+ homedir(),
181
+ ".chatccc",
182
+ "state",
183
+ "feishu-message-ledger.json",
184
+ );
185
+
186
+ export const processedMessages = new Set<string>();
187
+ export const feishuMessageLedger = new FeishuMessageLedger(
188
+ defaultLedgerPath,
189
+ MAX_PROCESSED,
190
+ processedMessages,
191
+ );
192
+
193
+ export function clearFeishuMessageLedgerMemory(): void {
194
+ feishuMessageLedger.clearMemory();
195
+ }
package/src/index.ts CHANGED
@@ -96,11 +96,9 @@ import {
96
96
  sendCardKitMessage,
97
97
  updateCardKitCard,
98
98
  } from "./cardkit.ts";
99
- import {
100
- MAX_PROCESSED,
101
- loadSessionRegistryForBinding,
102
- processedMessages,
103
- rebuildBindingsFromRegistry,
99
+ import {
100
+ loadSessionRegistryForBinding,
101
+ rebuildBindingsFromRegistry,
104
102
  resetState,
105
103
  setSessionPlatform,
106
104
  startUnifiedDisplayLoop,
@@ -115,7 +113,11 @@ import { handleCommand, type PlatformAdapter } from "./orchestrator.ts";
115
113
  import { createWechatAdapter, startWechatPlatform } from "./wechat-platform.ts";
116
114
  import { handleCodexResetCardAction } from "./codex-reset-actions.ts";
117
115
  import { resolveFeishuCardActionChatType } from "./card-action-routing.ts";
118
- import { reloadRuntimeConfig } from "./runtime-reload.ts";
116
+ import { reloadRuntimeConfig } from "./runtime-reload.ts";
117
+ import {
118
+ createAckFirstEventHandler,
119
+ feishuMessageLedger,
120
+ } from "./feishu-message-ingress.ts";
119
121
 
120
122
  // ---------------------------------------------------------------------------
121
123
  // Feishu 平台适配器
@@ -262,12 +264,105 @@ function parseCardAction(data: unknown): CardActionResult | null {
262
264
  // WebSocket relay broadcast
263
265
  // ---------------------------------------------------------------------------
264
266
 
265
- let broadcastToRelay: (data: unknown) => void = () => {};
266
- const wechatSignal = { stopped: false };
267
-
268
- // ---------------------------------------------------------------------------
269
- // Simulate mode: inject message via HTTP
270
- // ---------------------------------------------------------------------------
267
+ let broadcastToRelay: (data: unknown) => void = () => {};
268
+ const wechatSignal = { stopped: false };
269
+
270
+ async function processFeishuMessageEvent(data: Evt): Promise<void> {
271
+ const traceId = makeTraceId();
272
+ try {
273
+ broadcastToRelay(data);
274
+
275
+ const event = getInnerEvent(data);
276
+ const message = event.message;
277
+ if (!message) return;
278
+
279
+ const messageId = message.message_id;
280
+ const chatId = message.chat_id ?? "";
281
+ const chatType = message.chat_type ?? "group";
282
+ const msgTimestamp = parseInt(message.create_time ?? "0", 10) || Date.now();
283
+ const disposition = await feishuMessageLedger.accept({
284
+ messageId,
285
+ chatId,
286
+ createTime: msgTimestamp,
287
+ });
288
+
289
+ if (disposition === "duplicate") {
290
+ console.log(`[MSG] Duplicate message ignored: ${messageId ?? "(missing id)"}`);
291
+ return;
292
+ }
293
+ if (disposition === "stale") {
294
+ logTrace(traceId, "DONE", {
295
+ outcome: "skip_stale_feishu_message",
296
+ messageId,
297
+ chatId,
298
+ msgTimestamp,
299
+ });
300
+ console.log(
301
+ `[${ts()}] [SKIP] Stale Feishu message ignored: messageId=${messageId ?? "(missing id)"} chatId=${chatId} createTime=${msgTimestamp}`,
302
+ );
303
+ return;
304
+ }
305
+
306
+ const text = await formatMessageContent(message);
307
+ const openId = event.sender?.sender_id?.open_id ?? "";
308
+
309
+ console.log(
310
+ `[MSG] id=${messageId ?? "(missing)"} sender=${openId} chat=${chatId} type=${chatType} text="${text}"`,
311
+ );
312
+ appendChatLog(chatId, openId, text);
313
+
314
+ if (messageId) {
315
+ getTenantAccessToken().then((freshToken) =>
316
+ addReaction(freshToken, messageId).catch((err) =>
317
+ console.error(`[${ts()}] Reaction failed: ${(err as Error).message}`)
318
+ )
319
+ ).catch((err) =>
320
+ console.error(`[${ts()}] Reaction token failed: ${(err as Error).message}`)
321
+ );
322
+ }
323
+
324
+ if (!text) return;
325
+ logTrace(traceId, "RECV", {
326
+ messageId,
327
+ chatId,
328
+ chatType,
329
+ text: text.slice(0, 100),
330
+ });
331
+
332
+ const delayNotice = formatDelayNotice(msgTimestamp, text);
333
+ if (delayNotice) {
334
+ const delayToken = await getTenantAccessToken();
335
+ await sendCardReply(delayToken, chatId, "延迟送达", delayNotice, "yellow").catch(() => {});
336
+ }
337
+
338
+ await handleCommand(
339
+ feishuPlatform,
340
+ text,
341
+ chatId,
342
+ openId,
343
+ msgTimestamp,
344
+ chatType,
345
+ traceId,
346
+ buildUpdateCommandId("message", messageId),
347
+ );
348
+ } catch (err) {
349
+ logTrace(traceId, "ERROR", { message: (err as Error).message });
350
+ console.error(
351
+ `[${ts()}] [FATAL] im.message.receive_v1 worker crashed: ${(err as Error).message}`,
352
+ );
353
+ }
354
+ }
355
+
356
+ const handleFeishuMessageEventAckFirst = createAckFirstEventHandler(
357
+ processFeishuMessageEvent,
358
+ (err) => console.error(
359
+ `[${ts()}] [FATAL] Detached Feishu event worker failed: ${(err as Error).message}`,
360
+ ),
361
+ );
362
+
363
+ // ---------------------------------------------------------------------------
364
+ // Simulate mode: inject message via HTTP
365
+ // ---------------------------------------------------------------------------
271
366
 
272
367
  async function handleSimInjectMessage(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
273
368
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -399,74 +494,9 @@ async function startBotServiceCore(): Promise<void> {
399
494
 
400
495
  console.log(`[${ts()}] [AUTH] Token obtained`);
401
496
 
402
- const eventDispatcher = new EventDispatcher({});
403
- eventDispatcher.register({
404
- "im.message.receive_v1": async (data: Evt) => {
405
- const traceId = makeTraceId();
406
- try {
407
- broadcastToRelay(data);
408
-
409
- const event = getInnerEvent(data);
410
- const message = event.message;
411
- if (!message) return;
412
-
413
- const messageId = message.message_id;
414
- if (messageId) {
415
- if (processedMessages.has(messageId)) {
416
- console.log(`[MSG] Duplicate message ignored: ${messageId}`);
417
- return;
418
- }
419
- processedMessages.add(messageId);
420
- if (processedMessages.size > MAX_PROCESSED) {
421
- const it = processedMessages.values();
422
- for (let i = 0; i < 1000; i++) processedMessages.delete(it.next().value as string);
423
- }
424
- }
425
-
426
- const text = await formatMessageContent(message);
427
- const sender = event.sender;
428
- const openId = sender?.sender_id?.open_id ?? "";
429
- const chatId = message.chat_id ?? "";
430
- const chatType = message.chat_type ?? "group";
431
-
432
- console.log(`[MSG] sender=${openId} chat=${chatId} type=${chatType} text="${text}"`);
433
- appendChatLog(chatId, openId, text);
434
-
435
- if (messageId) {
436
- getTenantAccessToken().then((freshToken) =>
437
- addReaction(freshToken, messageId).catch((err) =>
438
- console.error(`[${ts()}] Reaction failed: ${(err as Error).message}`)
439
- )
440
- ).catch((err) =>
441
- console.error(`[${ts()}] Reaction token failed: ${(err as Error).message}`)
442
- );
443
- }
444
-
445
- if (!text) return;
446
- const msgTimestamp = parseInt(message.create_time ?? "0", 10) || Date.now();
447
- logTrace(traceId, "RECV", { chatId, chatType, text: text.slice(0, 100) });
448
- const delayNotice = formatDelayNotice(msgTimestamp, text);
449
- if (delayNotice) {
450
- const delayToken = await getTenantAccessToken();
451
- await sendCardReply(delayToken, chatId, "延迟送达", delayNotice, "yellow").catch(() => {});
452
- }
453
- // 仅 `/update` 会使用这个稳定 ID 做跨重启幂等;其他命令仍沿用
454
- // processedMessages 的进程内去重。
455
- await handleCommand(
456
- feishuPlatform,
457
- text,
458
- chatId,
459
- openId,
460
- msgTimestamp,
461
- chatType,
462
- traceId,
463
- buildUpdateCommandId("message", messageId),
464
- );
465
- } catch (err) {
466
- logTrace(traceId, "ERROR", { message: (err as Error).message });
467
- console.error(`[${ts()}] [FATAL] im.message.receive_v1 handler crashed: ${(err as Error).message}`);
468
- }
469
- },
497
+ const eventDispatcher = new EventDispatcher({});
498
+ eventDispatcher.register({
499
+ "im.message.receive_v1": handleFeishuMessageEventAckFirst,
470
500
 
471
501
  "card.action.trigger": async (data: Evt) => {
472
502
  try {
@@ -593,7 +623,8 @@ async function startBotServiceCore(): Promise<void> {
593
623
  } else {
594
624
  // 进程首次启动:此时所有 Map 都是空的,resetState 主要是打个 LOG 标识"开始
595
625
  // 干净状态"。修正残留的 running stream-state 并重建 session→chat 映射。
596
- resetState();
626
+ resetState();
627
+ await feishuMessageLedger.load();
597
628
  startUnifiedDisplayLoop();
598
629
  fixStaleStreamStates().then(async () => {
599
630
  const registry = await loadSessionRegistryForBinding();
package/src/session.ts CHANGED
@@ -42,6 +42,13 @@ import { resourceMonitor, registerProcess, unregisterProcess } from "./adapters/
42
42
  import { buildImSkillsPromptCached, exportSkillSubDocs, clearImSkillsPromptCache } from "./im-skills.ts";
43
43
  import type { PlatformAdapter } from "./platform-adapter.ts";
44
44
  import { hasResponseStalled, observeResponseProgress } from "./response-stall.ts";
45
+ import {
46
+ MAX_PROCESSED,
47
+ clearFeishuMessageLedgerMemory,
48
+ processedMessages,
49
+ } from "./feishu-message-ingress.ts";
50
+
51
+ export { MAX_PROCESSED, processedMessages };
45
52
 
46
53
  // 微信显示循环压缩:头5 + ... + 尾5,避免在最后一步 sendText 中压缩指令回复
47
54
  function compressWechatDisplayText(text: string): string {
@@ -131,9 +138,6 @@ async function createVisibleProgressCard(
131
138
  // Shared state (imported by index.ts)
132
139
  // ---------------------------------------------------------------------------
133
140
 
134
- export const processedMessages = new Set<string>();
135
- export const MAX_PROCESSED = 5000;
136
-
137
141
  /** 每个 chatId 上一次已处理消息的时间戳,用于拦截延迟送达的旧消息 */
138
142
  export const lastMsgTimestamps = new Map<string, number>();
139
143
 
@@ -463,7 +467,7 @@ export function resetState(): void {
463
467
  }
464
468
  chatSessionMap.clear();
465
469
  sessionInfoMap.clear();
466
- processedMessages.clear();
470
+ clearFeishuMessageLedgerMemory();
467
471
  lastMsgTimestamps.clear();
468
472
  chatPlatformMap.clear();
469
473
  for (const prompt of activePrompts.values()) {