@kafca/agentdock 0.1.7

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.
Files changed (79) hide show
  1. package/README.md +70 -0
  2. package/bin/agentdock.mjs +275 -0
  3. package/build/icon.png +0 -0
  4. package/dist/renderer/assets/index-BaNpzC6d.js +482 -0
  5. package/dist/renderer/assets/index-CLWJyO0E.css +11 -0
  6. package/dist/renderer/favicon.svg +19 -0
  7. package/dist/renderer/index.html +14 -0
  8. package/dist/renderer/logo-options.svg +153 -0
  9. package/dist-electron/electron/knowledge-skill-script.test.js +125 -0
  10. package/dist-electron/electron/lac-cli.test.js +221 -0
  11. package/dist-electron/electron/local-core-refactor.test.js +1075 -0
  12. package/dist-electron/electron/main.js +163 -0
  13. package/dist-electron/electron/plugin-kernel.test.js +534 -0
  14. package/dist-electron/electron/preload.js +2 -0
  15. package/dist-electron/package.json +3 -0
  16. package/dist-electron/packages/contracts/src/index.js +18 -0
  17. package/dist-electron/packages/contracts/src/local-core.js +2 -0
  18. package/dist-electron/packages/core-sdk/src/index.js +406 -0
  19. package/dist-electron/packages/knowledge-api/src/ai-vector-provider.js +388 -0
  20. package/dist-electron/packages/knowledge-api/src/index.js +146 -0
  21. package/dist-electron/packages/knowledge-api/src/sqlite-store.js +467 -0
  22. package/dist-electron/packages/knowledge-api/src/thread-knowledge-store.js +21 -0
  23. package/dist-electron/packages/knowledge-api/test/ai-vector-provider.test.js +412 -0
  24. package/dist-electron/packages/plugin-sdk/src/index.js +2 -0
  25. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +369 -0
  26. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-response-processor.js +110 -0
  27. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +171 -0
  28. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +635 -0
  29. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-transport.js +190 -0
  30. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +244 -0
  31. package/dist-electron/services/local-ai-core/src/acp/workspace-acp-permissions.js +52 -0
  32. package/dist-electron/services/local-ai-core/src/cli/lac.js +290 -0
  33. package/dist-electron/services/local-ai-core/src/gateway/local-core-lark-gateway.js +976 -0
  34. package/dist-electron/services/local-ai-core/src/gateway/local-core-weixin-gateway.js +1143 -0
  35. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +291 -0
  36. package/dist-electron/services/local-ai-core/src/kernel/capability-registry.js +67 -0
  37. package/dist-electron/services/local-ai-core/src/kernel/diagnostics.js +27 -0
  38. package/dist-electron/services/local-ai-core/src/kernel/event-bus.js +27 -0
  39. package/dist-electron/services/local-ai-core/src/kernel/lifecycle-manager.js +85 -0
  40. package/dist-electron/services/local-ai-core/src/kernel/plugin-registry.js +60 -0
  41. package/dist-electron/services/local-ai-core/src/plugins/builtin/agent-localcore-acp-plugin.js +114 -0
  42. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-lark-plugin.js +59 -0
  43. package/dist-electron/services/local-ai-core/src/plugins/builtin/channel-weixin-plugin.js +56 -0
  44. package/dist-electron/services/local-ai-core/src/plugins/builtin/knowledge-ai-vector-plugin.js +7 -0
  45. package/dist-electron/services/local-ai-core/src/plugins/builtin/knowledge-noop-plugin.js +7 -0
  46. package/dist-electron/services/local-ai-core/src/plugins/builtin/runtime-capabilities-plugin.js +62 -0
  47. package/dist-electron/services/local-ai-core/src/plugins/builtin/scheduler-cron-plugin.js +49 -0
  48. package/dist-electron/services/local-ai-core/src/plugins/builtin/scheduler-lark-plugin.js +38 -0
  49. package/dist-electron/services/local-ai-core/src/plugins/builtin/scheduler-weixin-plugin.js +38 -0
  50. package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +230 -0
  51. package/dist-electron/services/local-ai-core/src/router/workspace-router-types.js +2 -0
  52. package/dist-electron/services/local-ai-core/src/router/workspace-router.js +389 -0
  53. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +315 -0
  54. package/dist-electron/services/local-ai-core/src/runtime/local-core-runtime-state.js +282 -0
  55. package/dist-electron/services/local-ai-core/src/runtime/server.js +465 -0
  56. package/dist-electron/services/local-ai-core/src/runtime/standalone.js +38 -0
  57. package/dist-electron/services/local-ai-core/src/scheduler/adapters.js +2 -0
  58. package/dist-electron/services/local-ai-core/src/scheduler/cron-command-detector.js +70 -0
  59. package/dist-electron/services/local-ai-core/src/scheduler/cron.js +53 -0
  60. package/dist-electron/services/local-ai-core/src/scheduler/execution-policy.js +2 -0
  61. package/dist-electron/services/local-ai-core/src/scheduler/lark-execution-policies.js +66 -0
  62. package/dist-electron/services/local-ai-core/src/scheduler/lark-schedule-adapter.js +77 -0
  63. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +46 -0
  64. package/dist-electron/services/local-ai-core/src/scheduler/scheduler-run-lifecycle.js +64 -0
  65. package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +133 -0
  66. package/dist-electron/services/local-ai-core/src/scheduler/weixin-execution-policies.js +66 -0
  67. package/dist-electron/services/local-ai-core/src/scheduler/weixin-schedule-adapter.js +78 -0
  68. package/dist-electron/services/local-ai-core/src/thread/workspace-thread-id.js +17 -0
  69. package/dist-electron/services/local-ai-core/src/thread/workspace-thread-mappers.js +43 -0
  70. package/dist-electron/shared/desktop.js +195 -0
  71. package/dist-electron/src/api/client.js +73 -0
  72. package/dist-electron/src/api/sessions.js +21 -0
  73. package/dist-electron/src/lib/session-utils.js +51 -0
  74. package/dist-electron/src/pages/Threads/thread-chat-model.js +246 -0
  75. package/dist-electron/src/pages/Threads/thread-chat-model.test.js +76 -0
  76. package/dist-electron/src/pages/Threads/thread-chat-permission.js +26 -0
  77. package/dist-electron/src/pages/Threads/thread-chat-permission.test.js +102 -0
  78. package/dist-electron/src/pages/Threads/thread-chat-task-state.js +65 -0
  79. package/package.json +146 -0
@@ -0,0 +1,1075 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_fs_1 = require("node:fs");
9
+ const node_os_1 = require("node:os");
10
+ const node_path_1 = require("node:path");
11
+ const local_core_acp_response_processor_js_1 = require("../services/local-ai-core/src/acp/local-core-acp-response-processor.js");
12
+ const scheduled_conversation_executor_js_1 = require("../services/local-ai-core/src/scheduler/scheduled-conversation-executor.js");
13
+ const scheduler_run_lifecycle_js_1 = require("../services/local-ai-core/src/scheduler/scheduler-run-lifecycle.js");
14
+ const lark_execution_policies_js_1 = require("../services/local-ai-core/src/scheduler/lark-execution-policies.js");
15
+ const local_core_weixin_gateway_js_1 = require("../services/local-ai-core/src/gateway/local-core-weixin-gateway.js");
16
+ const local_core_acp_turn_coordinator_js_1 = require("../services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js");
17
+ (0, node_test_1.default)('ACP tool call update is emitted with its pending tool name', () => {
18
+ const appended = [];
19
+ const emitted = [];
20
+ const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
21
+ appendMessage: (_threadId, _role, content, kind) => appended.push({ content, kind }),
22
+ emitBridge: (event) => emitted.push(event),
23
+ updateRunStatus: () => { },
24
+ sendRaw: () => true,
25
+ });
26
+ const session = {
27
+ threadId: 'thread-1',
28
+ bridgeSessionKey: 'session:thread-1',
29
+ currentRunId: 'run-1',
30
+ currentTurn: {
31
+ runId: 'run-1',
32
+ replyCtx: 'run-1',
33
+ previewHandle: 'preview-1',
34
+ assistantText: '',
35
+ typingStarted: true,
36
+ previewStarted: false,
37
+ permission: null,
38
+ },
39
+ loadReplayMode: false,
40
+ schedulerJobCreatedByRun: new Map(),
41
+ };
42
+ coordinator.handleAgentNotification(session, {
43
+ method: 'session/update',
44
+ params: {
45
+ update: {
46
+ sessionUpdate: 'tool_call',
47
+ title: 'Terminal',
48
+ },
49
+ },
50
+ });
51
+ coordinator.handleAgentNotification(session, {
52
+ method: 'session/update',
53
+ params: {
54
+ update: {
55
+ sessionUpdate: 'tool_call_update',
56
+ title: "ls ~/Desktop - List files on the user's desktop",
57
+ status: 'running',
58
+ },
59
+ },
60
+ });
61
+ strict_1.default.deepEqual(appended, [
62
+ {
63
+ content: "🔧 Terminal: ls ~/Desktop - List files on the user's desktop - running",
64
+ kind: 'progress',
65
+ },
66
+ ]);
67
+ strict_1.default.equal(emitted.length, 1);
68
+ strict_1.default.equal(emitted[0]?.content, appended[0]?.content);
69
+ });
70
+ (0, node_test_1.default)('ACP bare tool call is flushed before assistant text', () => {
71
+ const appended = [];
72
+ const emitted = [];
73
+ const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
74
+ appendMessage: (_threadId, _role, content) => appended.push(content),
75
+ emitBridge: (event) => emitted.push(event),
76
+ updateRunStatus: () => { },
77
+ sendRaw: () => true,
78
+ });
79
+ const session = {
80
+ threadId: 'thread-1',
81
+ bridgeSessionKey: 'session:thread-1',
82
+ currentRunId: 'run-1',
83
+ currentTurn: {
84
+ runId: 'run-1',
85
+ replyCtx: 'run-1',
86
+ previewHandle: 'preview-1',
87
+ assistantText: '',
88
+ typingStarted: true,
89
+ previewStarted: false,
90
+ permission: null,
91
+ },
92
+ loadReplayMode: false,
93
+ schedulerJobCreatedByRun: new Map(),
94
+ };
95
+ coordinator.handleAgentNotification(session, {
96
+ method: 'session/update',
97
+ params: {
98
+ update: {
99
+ sessionUpdate: 'tool_call',
100
+ title: 'Terminal',
101
+ },
102
+ },
103
+ });
104
+ coordinator.handleAgentNotification(session, {
105
+ method: 'session/update',
106
+ params: {
107
+ update: {
108
+ sessionUpdate: 'agent_message_chunk',
109
+ content: { type: 'text', text: 'done' },
110
+ },
111
+ },
112
+ });
113
+ strict_1.default.deepEqual(appended, ['🔧 Terminal']);
114
+ strict_1.default.equal(emitted[0]?.content, '🔧 Terminal');
115
+ strict_1.default.equal(session.currentTurn.assistantText, 'done');
116
+ });
117
+ (0, node_test_1.default)('ACP skips empty generic running tool updates', () => {
118
+ const appended = [];
119
+ const emitted = [];
120
+ const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
121
+ appendMessage: (_threadId, _role, content) => appended.push(content),
122
+ emitBridge: (event) => emitted.push(event),
123
+ updateRunStatus: () => { },
124
+ sendRaw: () => true,
125
+ });
126
+ const session = {
127
+ threadId: 'thread-1',
128
+ bridgeSessionKey: 'session:thread-1',
129
+ currentRunId: 'run-1',
130
+ currentTurn: {
131
+ runId: 'run-1',
132
+ replyCtx: 'run-1',
133
+ previewHandle: 'preview-1',
134
+ assistantText: '',
135
+ typingStarted: true,
136
+ previewStarted: false,
137
+ permission: null,
138
+ },
139
+ loadReplayMode: false,
140
+ schedulerJobCreatedByRun: new Map(),
141
+ };
142
+ coordinator.handleAgentNotification(session, {
143
+ method: 'session/update',
144
+ params: {
145
+ update: {
146
+ sessionUpdate: 'tool_call_update',
147
+ title: 'Tool update',
148
+ status: 'running',
149
+ },
150
+ },
151
+ });
152
+ strict_1.default.deepEqual(appended, []);
153
+ strict_1.default.deepEqual(emitted, []);
154
+ });
155
+ (0, node_test_1.default)('ACP skips empty generic running updates even after a tool name', () => {
156
+ const appended = [];
157
+ const emitted = [];
158
+ const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
159
+ appendMessage: (_threadId, _role, content) => appended.push(content),
160
+ emitBridge: (event) => emitted.push(event),
161
+ updateRunStatus: () => { },
162
+ sendRaw: () => true,
163
+ });
164
+ const session = {
165
+ threadId: 'thread-1',
166
+ bridgeSessionKey: 'session:thread-1',
167
+ currentRunId: 'run-1',
168
+ currentTurn: {
169
+ runId: 'run-1',
170
+ replyCtx: 'run-1',
171
+ previewHandle: 'preview-1',
172
+ assistantText: '',
173
+ typingStarted: true,
174
+ previewStarted: false,
175
+ permission: null,
176
+ },
177
+ loadReplayMode: false,
178
+ schedulerJobCreatedByRun: new Map(),
179
+ };
180
+ coordinator.handleAgentNotification(session, {
181
+ method: 'session/update',
182
+ params: {
183
+ update: {
184
+ sessionUpdate: 'tool_call',
185
+ title: 'Terminal',
186
+ },
187
+ },
188
+ });
189
+ coordinator.handleAgentNotification(session, {
190
+ method: 'session/update',
191
+ params: {
192
+ update: {
193
+ sessionUpdate: 'tool_call_update',
194
+ title: 'Tool update',
195
+ status: 'running',
196
+ },
197
+ },
198
+ });
199
+ strict_1.default.deepEqual(appended, []);
200
+ strict_1.default.deepEqual(emitted, []);
201
+ strict_1.default.equal(session.currentTurn.pendingToolCallTitle, undefined);
202
+ });
203
+ (0, node_test_1.default)('response processor derives slash fallback replies and cron system responses', async () => {
204
+ const processor = new local_core_acp_response_processor_js_1.LocalCoreAcpResponseProcessor({
205
+ getScheduledDeliveryBinding: (threadId) => threadId === 'thread-1'
206
+ ? {
207
+ workspaceId: '知识库',
208
+ platform: 'lark',
209
+ route: {
210
+ type: 'channel.chat',
211
+ channelId: 'chat-1',
212
+ participantId: 'user-1',
213
+ threadId,
214
+ },
215
+ }
216
+ : null,
217
+ scheduler: {
218
+ createJob: async () => ({
219
+ id: 'job-1',
220
+ workspaceId: '知识库',
221
+ platform: 'lark',
222
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' },
223
+ executionMode: 'same-thread',
224
+ triggerType: 'cron',
225
+ cronExpr: '*/2 * * * *',
226
+ promptTemplate: 'ping',
227
+ description: 'two-minute ping',
228
+ enabled: true,
229
+ concurrencyPolicy: 'skip_if_running',
230
+ createdAt: '2026-04-22T06:00:00.000Z',
231
+ updatedAt: '2026-04-22T06:00:00.000Z',
232
+ }),
233
+ listJobsForThread: async () => [],
234
+ deleteJob: async () => { },
235
+ },
236
+ });
237
+ strict_1.default.equal(processor.deriveSlashCommandReply('/mode', {}), '模式命令已执行,但当前 ACP 运行时没有返回可显示的模式菜单。请直接使用 `/mode <name>`。');
238
+ const processed = await processor.processAssistantResponse('thread-1', '已为你创建。\n[CRON_CREATE]\nname: test\nschedule: */2 * * * *\nschedule_description: 每 2 分钟\nmessage: ping\n[/CRON_CREATE]');
239
+ strict_1.default.equal(processed.displayContent.trim(), '已为你创建。');
240
+ strict_1.default.match(processed.systemResponses[0] || '', /已创建定时任务/);
241
+ });
242
+ (0, node_test_1.default)('scheduled conversation executor uses execution policy hooks around a thread run', async () => {
243
+ const calls = [];
244
+ const job = {
245
+ id: 'job-1',
246
+ workspaceId: '知识库',
247
+ platform: 'lark',
248
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' },
249
+ executionMode: 'same-thread',
250
+ triggerType: 'cron',
251
+ cronExpr: '*/2 * * * *',
252
+ promptTemplate: 'ping',
253
+ description: 'two-minute ping',
254
+ enabled: true,
255
+ concurrencyPolicy: 'skip_if_running',
256
+ createdAt: '2026-04-22T06:00:00.000Z',
257
+ updatedAt: '2026-04-22T06:00:00.000Z',
258
+ };
259
+ const executor = new scheduled_conversation_executor_js_1.ScheduledConversationExecutor({
260
+ store: {
261
+ getRun: () => ({ status: 'completed' }),
262
+ },
263
+ workspaceRouter: {
264
+ sendThreadMessage: async (threadId, prompt) => {
265
+ calls.push(`send:${threadId}:${prompt}`);
266
+ return { runId: 'run-1' };
267
+ },
268
+ getThread: async (threadId) => ({
269
+ id: threadId,
270
+ messages: [
271
+ { role: 'assistant', kind: 'final', content: 'done' },
272
+ ],
273
+ }),
274
+ },
275
+ });
276
+ const result = await executor.execute(job, 'ping', {
277
+ resolveTarget: async () => ({
278
+ kind: 'thread',
279
+ threadId: 'thread-1',
280
+ workspaceId: '知识库',
281
+ platform: 'lark',
282
+ route: job.route,
283
+ }),
284
+ beforeExecute: (target) => {
285
+ calls.push(`before:${target.threadId}`);
286
+ },
287
+ afterExecute: (target) => {
288
+ calls.push(`after:${target.threadId}`);
289
+ },
290
+ }, 1000);
291
+ strict_1.default.deepEqual(calls, [
292
+ 'before:thread-1',
293
+ 'send:thread-1:ping',
294
+ 'after:thread-1',
295
+ ]);
296
+ strict_1.default.equal(result.replyText, 'done');
297
+ });
298
+ (0, node_test_1.default)('scheduler run lifecycle updates run and job state through explicit transitions', () => {
299
+ const emittedRuns = [];
300
+ const emittedJobs = [];
301
+ const job = {
302
+ id: 'job-1',
303
+ workspaceId: '知识库',
304
+ platform: 'lark',
305
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' },
306
+ executionMode: 'same-thread',
307
+ triggerType: 'cron',
308
+ cronExpr: '*/2 * * * *',
309
+ promptTemplate: 'ping',
310
+ description: 'two-minute ping',
311
+ enabled: true,
312
+ concurrencyPolicy: 'skip_if_running',
313
+ createdAt: '2026-04-22T06:00:00.000Z',
314
+ updatedAt: '2026-04-22T06:00:00.000Z',
315
+ };
316
+ const jobs = new Map([
317
+ ['job-1', job],
318
+ ]);
319
+ const runs = new Map();
320
+ let seq = 0;
321
+ const lifecycle = new scheduler_run_lifecycle_js_1.SchedulerRunLifecycle({
322
+ store: {
323
+ createScheduledJobRun: (jobId, status, input) => {
324
+ const run = { id: `run-${++seq}`, jobId, status, ...input };
325
+ runs.set(run.id, run);
326
+ return run;
327
+ },
328
+ updateScheduledJobRun: (runId, input) => {
329
+ const next = { ...runs.get(runId), ...input };
330
+ runs.set(runId, next);
331
+ return next;
332
+ },
333
+ updateScheduledJobStatus: (jobId, input) => {
334
+ jobs.set(jobId, { ...(jobs.get(jobId) || job), ...input });
335
+ },
336
+ getScheduledJob: (jobId) => jobs.get(jobId),
337
+ },
338
+ emitRun: (run) => emittedRuns.push(`${run.id}:${run.status}`),
339
+ emitJob: (job) => emittedJobs.push(`${job.id}:${job.enabled}`),
340
+ });
341
+ const queued = lifecycle.markQueued(job, '2026-04-22T06:00:00.000Z');
342
+ lifecycle.markRunning(queued.id);
343
+ lifecycle.markSucceeded(job, queued.id, {
344
+ threadId: 'thread-1',
345
+ runId: 'run-1',
346
+ platformMessageId: 'msg-1',
347
+ }, true);
348
+ strict_1.default.deepEqual(emittedRuns, [
349
+ 'run-1:queued',
350
+ 'run-1:running',
351
+ 'run-1:succeeded',
352
+ ]);
353
+ strict_1.default.deepEqual(emittedJobs, ['job-1:false']);
354
+ });
355
+ (0, node_test_1.default)('lark side-thread execution policy reuses a dedicated scheduled thread', async () => {
356
+ const job = {
357
+ id: 'job-1',
358
+ workspaceId: '知识库',
359
+ platform: 'lark',
360
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-origin' },
361
+ executionMode: 'side-thread',
362
+ triggerType: 'cron',
363
+ cronExpr: '*/2 * * * *',
364
+ promptTemplate: 'ping',
365
+ description: 'two-minute ping',
366
+ enabled: true,
367
+ concurrencyPolicy: 'skip_if_running',
368
+ createdAt: '2026-04-22T06:00:00.000Z',
369
+ updatedAt: '2026-04-22T06:00:00.000Z',
370
+ };
371
+ const policy = (0, lark_execution_policies_js_1.createLarkExecutionPolicy)(job, {
372
+ store: {},
373
+ workspaceRouter: {
374
+ listThreads: async () => [{ id: 'thread-scheduled', title: '[Scheduled] two-minute ping' }],
375
+ createThread: async () => ({ id: 'thread-new' }),
376
+ },
377
+ getChannelRuntime: () => ({
378
+ muteThreadBridge: () => { },
379
+ unmuteThreadBridge: () => { },
380
+ }),
381
+ }, async () => 'thread-origin');
382
+ const target = await policy.resolveTarget(job);
383
+ strict_1.default.equal(target.threadId, 'thread-scheduled');
384
+ });
385
+ (0, node_test_1.default)('lark same-thread execution policy keeps the original thread target', async () => {
386
+ const job = {
387
+ id: 'job-1',
388
+ workspaceId: '知识库',
389
+ platform: 'lark',
390
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-origin' },
391
+ executionMode: 'same-thread',
392
+ triggerType: 'cron',
393
+ cronExpr: '*/2 * * * *',
394
+ promptTemplate: 'ping',
395
+ description: 'two-minute ping',
396
+ enabled: true,
397
+ concurrencyPolicy: 'skip_if_running',
398
+ createdAt: '2026-04-22T06:00:00.000Z',
399
+ updatedAt: '2026-04-22T06:00:00.000Z',
400
+ };
401
+ const policy = (0, lark_execution_policies_js_1.createLarkExecutionPolicy)(job, {
402
+ store: {},
403
+ workspaceRouter: {},
404
+ getChannelRuntime: () => ({
405
+ muteThreadBridge: () => { },
406
+ unmuteThreadBridge: () => { },
407
+ }),
408
+ }, async () => 'thread-origin');
409
+ const target = await policy.resolveTarget(job);
410
+ strict_1.default.equal(target.threadId, 'thread-origin');
411
+ });
412
+ (0, node_test_1.default)('weixin channel can request a QR code without platform options', async () => {
413
+ const originalFetch = globalThis.fetch;
414
+ const stateDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'weixin-channel-qr-'));
415
+ const requests = [];
416
+ globalThis.fetch = (async (input, init) => {
417
+ requests.push({
418
+ url: String(input),
419
+ headers: new Headers(init?.headers),
420
+ });
421
+ return new Response(JSON.stringify({
422
+ qrcode: 'ticket-1',
423
+ qrcode_img_content: 'https://liteapp.weixin.qq.com/q/test?qrcode=ticket-1&bot_type=3',
424
+ expired: 180,
425
+ }), {
426
+ status: 200,
427
+ headers: { 'Content-Type': 'application/json' },
428
+ });
429
+ });
430
+ try {
431
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
432
+ store: {},
433
+ readConfig: async () => ({
434
+ projects: [
435
+ {
436
+ name: 'default',
437
+ agent: { type: 'localcore-acp', providers: [] },
438
+ platforms: [{ type: 'weixin', options: { state_dir: stateDir } }],
439
+ },
440
+ ],
441
+ }),
442
+ getWorkspaceRouter: () => ({}),
443
+ eventBus: { emit: () => { }, on: () => () => { } },
444
+ });
445
+ const result = await gateway.getQrCode('default');
446
+ strict_1.default.deepEqual(result, {
447
+ ticket: 'ticket-1',
448
+ expiresIn: 180,
449
+ qrCodeUrl: 'https://liteapp.weixin.qq.com/q/test?qrcode=ticket-1&bot_type=3',
450
+ });
451
+ strict_1.default.equal(requests[0]?.url, 'https://ilinkai.weixin.qq.com/ilink/bot/get_bot_qrcode?bot_type=3');
452
+ strict_1.default.equal(requests[0]?.headers.has('Authorization'), false);
453
+ strict_1.default.equal(requests[0]?.headers.has('AuthorizationType'), false);
454
+ }
455
+ finally {
456
+ globalThis.fetch = originalFetch;
457
+ (0, node_fs_1.rmSync)(stateDir, { recursive: true, force: true });
458
+ }
459
+ });
460
+ (0, node_test_1.default)('weixin QR confirmation persists credentials and starts authenticated polling', async () => {
461
+ const originalFetch = globalThis.fetch;
462
+ const stateDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'weixin-channel-'));
463
+ const requests = [];
464
+ globalThis.fetch = (async (input, init) => {
465
+ const url = String(input);
466
+ requests.push({
467
+ url,
468
+ headers: new Headers(init?.headers),
469
+ });
470
+ if (url.includes('/get_qrcode_status')) {
471
+ return new Response(JSON.stringify({
472
+ status: 'confirmed',
473
+ bot_token: 'bot-token-1',
474
+ baseurl: 'https://ilinkai.weixin.qq.com',
475
+ ilink_bot_id: 'bot-1',
476
+ ilink_user_id: 'user-1',
477
+ }), {
478
+ status: 200,
479
+ headers: { 'Content-Type': 'application/json' },
480
+ });
481
+ }
482
+ return new Promise((_resolve, reject) => {
483
+ init?.signal?.addEventListener('abort', () => reject(new Error('aborted')), { once: true });
484
+ });
485
+ });
486
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
487
+ store: {
488
+ expirePendingPairings: () => { },
489
+ listPendingPairings: () => [],
490
+ listAuthorizedUsers: () => [],
491
+ },
492
+ readConfig: async () => ({
493
+ projects: [
494
+ {
495
+ name: 'default',
496
+ agent: { type: 'localcore-acp', providers: [] },
497
+ platforms: [{ type: 'weixin', options: { state_dir: stateDir } }],
498
+ },
499
+ ],
500
+ }),
501
+ getWorkspaceRouter: () => ({}),
502
+ eventBus: { emit: () => { }, on: () => () => { } },
503
+ });
504
+ try {
505
+ const result = await gateway.checkQrCodeStatus('default', 'ticket-1');
506
+ await new Promise((resolve) => setTimeout(resolve, 0));
507
+ strict_1.default.equal(result.status, 'confirmed');
508
+ const pollingRequest = requests.find((request) => request.url.endsWith('/ilink/bot/getupdates'));
509
+ strict_1.default.equal(pollingRequest?.headers.get('Authorization'), 'Bearer bot-token-1');
510
+ strict_1.default.equal(pollingRequest?.headers.get('AuthorizationType'), 'ilink_bot_token');
511
+ }
512
+ finally {
513
+ await gateway.stop();
514
+ globalThis.fetch = originalFetch;
515
+ (0, node_fs_1.rmSync)(stateDir, { recursive: true, force: true });
516
+ }
517
+ });
518
+ (0, node_test_1.default)('weixin inbound message handling is idempotent by message identity', async () => {
519
+ const sentThreadMessages = [];
520
+ const users = new Map();
521
+ const threadBindings = new Map();
522
+ const bindingKey = 'default:chat-1:user-1';
523
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
524
+ store: {
525
+ expirePendingPairings: () => { },
526
+ listPendingPairings: () => [],
527
+ listPairingRequests: () => [],
528
+ listAuthorizedUsers: () => [...users.values()],
529
+ getAuthorizedUser: (_workspaceId, platformUserId) => users.get(platformUserId),
530
+ createAuthorizedUser: (user) => users.set(user.platform_user_id, user),
531
+ updateAuthorizedUserThread: (_workspaceId, platformUserId, threadId) => {
532
+ users.set(platformUserId, { ...users.get(platformUserId), thread_id: threadId });
533
+ },
534
+ getPlatformThreadBinding: () => threadBindings.get(bindingKey),
535
+ upsertPlatformThreadBinding: (binding) => threadBindings.set(bindingKey, binding),
536
+ updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
537
+ threadBindings.set(bindingKey, { ...threadBindings.get(bindingKey), last_platform_message_id: messageId });
538
+ },
539
+ getLatestRunForThread: () => null,
540
+ },
541
+ readConfig: async () => ({
542
+ projects: [
543
+ {
544
+ name: 'default',
545
+ agent: { type: 'localcore-acp', providers: [] },
546
+ platforms: [{ type: 'weixin', options: {} }],
547
+ },
548
+ ],
549
+ }),
550
+ getWorkspaceRouter: () => ({
551
+ createThread: async () => ({ id: 'thread-1' }),
552
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
553
+ sendThreadMessage: async (_threadId, text) => {
554
+ sentThreadMessages.push(text);
555
+ return { runId: 'run-1' };
556
+ },
557
+ }),
558
+ eventBus: { emit: () => { }, on: () => () => { } },
559
+ });
560
+ const input = {
561
+ workspaceId: 'default',
562
+ platformUserId: 'user-1',
563
+ chatId: 'chat-1',
564
+ displayName: 'User',
565
+ text: 'hello',
566
+ messageId: 'msg-1',
567
+ contextToken: 'ctx-1',
568
+ };
569
+ await gateway.handleInboundMessage(input);
570
+ await gateway.handleInboundMessage(input);
571
+ strict_1.default.equal(sentThreadMessages.length, 1);
572
+ strict_1.default.match(sentThreadMessages[0] || '', /hello/);
573
+ });
574
+ (0, node_test_1.default)('weixin bridge skips duplicate rendered replies', async () => {
575
+ const originalFetch = globalThis.fetch;
576
+ const sentBodies = [];
577
+ globalThis.fetch = (async (_input, init) => {
578
+ sentBodies.push(JSON.parse(String(init?.body || '{}')));
579
+ return new Response(JSON.stringify({ errcode: 0 }), {
580
+ status: 200,
581
+ headers: { 'Content-Type': 'application/json' },
582
+ });
583
+ });
584
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
585
+ store: {
586
+ getPlatformThreadBinding: () => ({
587
+ workspace_id: 'default',
588
+ platform: 'weixin',
589
+ chat_id: 'chat-1',
590
+ platform_user_id: 'user-1',
591
+ thread_id: 'thread-1',
592
+ last_platform_message_id: 'ctx-1',
593
+ }),
594
+ },
595
+ readConfig: async () => ({
596
+ projects: [
597
+ {
598
+ name: 'default',
599
+ agent: { type: 'localcore-acp', providers: [] },
600
+ platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
601
+ },
602
+ ],
603
+ }),
604
+ getWorkspaceRouter: () => ({}),
605
+ eventBus: { emit: () => { }, on: () => () => { } },
606
+ });
607
+ const internals = gateway;
608
+ internals.runtime.set('default', {
609
+ workspaceId: 'default',
610
+ enabled: true,
611
+ status: 'running',
612
+ connected: true,
613
+ accountId: 'bot-1',
614
+ });
615
+ internals.threadRouting.set('session:thread-1', {
616
+ workspaceId: 'default',
617
+ platformUserId: 'user-1',
618
+ chatId: 'chat-1',
619
+ threadId: 'thread-1',
620
+ });
621
+ try {
622
+ await gateway.onBridgeEvent({ type: 'update_message', sessionKey: 'session:thread-1', content: 'same reply' });
623
+ await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'same reply' });
624
+ await gateway.onBridgeEvent({ type: 'typing_stop', sessionKey: 'session:thread-1' });
625
+ await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'same reply' });
626
+ strict_1.default.equal(sentBodies.length, 1);
627
+ strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, 'same reply');
628
+ }
629
+ finally {
630
+ globalThis.fetch = originalFetch;
631
+ }
632
+ });
633
+ (0, node_test_1.default)('weixin bridge keeps context replies to one truncated text message', async () => {
634
+ const originalFetch = globalThis.fetch;
635
+ const sentBodies = [];
636
+ globalThis.fetch = (async (_input, init) => {
637
+ sentBodies.push(JSON.parse(String(init?.body || '{}')));
638
+ return new Response(JSON.stringify({ errcode: 0 }), {
639
+ status: 200,
640
+ headers: { 'Content-Type': 'application/json' },
641
+ });
642
+ });
643
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
644
+ store: {
645
+ getPlatformThreadBinding: () => ({
646
+ workspace_id: 'default',
647
+ platform: 'weixin',
648
+ chat_id: 'chat-1',
649
+ platform_user_id: 'user-1',
650
+ thread_id: 'thread-1',
651
+ last_platform_message_id: 'ctx-1',
652
+ }),
653
+ },
654
+ readConfig: async () => ({
655
+ projects: [
656
+ {
657
+ name: 'default',
658
+ agent: { type: 'localcore-acp', providers: [] },
659
+ platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
660
+ },
661
+ ],
662
+ }),
663
+ getWorkspaceRouter: () => ({}),
664
+ eventBus: { emit: () => { }, on: () => () => { } },
665
+ });
666
+ const internals = gateway;
667
+ internals.runtime.set('default', {
668
+ workspaceId: 'default',
669
+ enabled: true,
670
+ status: 'running',
671
+ connected: true,
672
+ accountId: 'bot-1',
673
+ });
674
+ internals.threadRouting.set('session:thread-1', {
675
+ workspaceId: 'default',
676
+ platformUserId: 'user-1',
677
+ chatId: 'chat-1',
678
+ threadId: 'thread-1',
679
+ });
680
+ try {
681
+ await gateway.onBridgeEvent({
682
+ type: 'reply',
683
+ sessionKey: 'session:thread-1',
684
+ content: Array.from({ length: 80 }, (_, index) => `第 ${index + 1} 行:这是一段用于测试微信长文本切分的内容。`).join('\n\n'),
685
+ });
686
+ strict_1.default.equal(sentBodies.length, 1);
687
+ strict_1.default.equal(sentBodies[0]?.msg?.context_token, 'ctx-1');
688
+ for (const body of sentBodies) {
689
+ const text = body?.msg?.item_list?.[0]?.text_item?.text || '';
690
+ strict_1.default.ok(Buffer.byteLength(text, 'utf-8') <= 3500);
691
+ strict_1.default.match(text, /内容过长,已截断以保证微信送达/);
692
+ strict_1.default.equal(body?.base_info?.channel_version, '2.1.7');
693
+ strict_1.default.equal(body?.msg?.from_user_id, '');
694
+ strict_1.default.match(body?.msg?.client_id || '', /^openclaw-weixin-/);
695
+ }
696
+ }
697
+ finally {
698
+ globalThis.fetch = originalFetch;
699
+ }
700
+ });
701
+ (0, node_test_1.default)('weixin bridge sends protocol-compatible final reply payload', async () => {
702
+ const originalFetch = globalThis.fetch;
703
+ const sentRequests = [];
704
+ globalThis.fetch = (async (_input, init) => {
705
+ const body = JSON.parse(String(init?.body || '{}'));
706
+ sentRequests.push({ body, headers: new Headers(init?.headers) });
707
+ return new Response(JSON.stringify({ ret: 0 }), {
708
+ status: 200,
709
+ headers: { 'Content-Type': 'application/json' },
710
+ });
711
+ });
712
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
713
+ store: {
714
+ getPlatformThreadBinding: () => ({
715
+ workspace_id: 'default',
716
+ platform: 'weixin',
717
+ chat_id: 'chat-1',
718
+ platform_user_id: 'user-1',
719
+ thread_id: 'thread-1',
720
+ last_platform_message_id: 'ctx-1',
721
+ }),
722
+ },
723
+ readConfig: async () => ({
724
+ projects: [
725
+ {
726
+ name: 'default',
727
+ agent: { type: 'localcore-acp', providers: [] },
728
+ platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
729
+ },
730
+ ],
731
+ }),
732
+ getWorkspaceRouter: () => ({}),
733
+ eventBus: { emit: () => { }, on: () => () => { } },
734
+ });
735
+ const internals = gateway;
736
+ internals.runtime.set('default', {
737
+ workspaceId: 'default',
738
+ enabled: true,
739
+ status: 'running',
740
+ connected: true,
741
+ accountId: 'bot-1',
742
+ });
743
+ internals.threadRouting.set('session:thread-1', {
744
+ workspaceId: 'default',
745
+ platformUserId: 'user-1',
746
+ chatId: 'chat-1',
747
+ threadId: 'thread-1',
748
+ });
749
+ try {
750
+ await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'final reply' });
751
+ strict_1.default.equal(sentRequests.length, 1);
752
+ strict_1.default.equal(sentRequests[0]?.body?.msg?.context_token, 'ctx-1');
753
+ strict_1.default.equal(sentRequests[0]?.body?.msg?.from_user_id, '');
754
+ strict_1.default.equal(sentRequests[0]?.body?.msg?.message_state, 2);
755
+ strict_1.default.equal(sentRequests[0]?.body?.base_info?.channel_version, '2.1.7');
756
+ strict_1.default.match(sentRequests[0]?.body?.msg?.client_id || '', /^openclaw-weixin-/);
757
+ strict_1.default.equal(sentRequests[0]?.body?.msg?.item_list?.[0]?.text_item?.text, 'final reply');
758
+ strict_1.default.equal(sentRequests[0]?.headers.get('iLink-App-Id'), 'bot');
759
+ strict_1.default.equal(sentRequests[0]?.headers.get('iLink-App-ClientVersion'), '131335');
760
+ strict_1.default.equal(sentRequests[0]?.headers.get('AuthorizationType'), 'ilink_bot_token');
761
+ strict_1.default.equal(sentRequests[0]?.headers.get('Authorization'), 'Bearer bot-token-1');
762
+ }
763
+ finally {
764
+ globalThis.fetch = originalFetch;
765
+ }
766
+ });
767
+ (0, node_test_1.default)('weixin bridge sends status events in real time', async () => {
768
+ const originalFetch = globalThis.fetch;
769
+ const sentBodies = [];
770
+ globalThis.fetch = (async (_input, init) => {
771
+ sentBodies.push(JSON.parse(String(init?.body || '{}')));
772
+ return new Response(JSON.stringify({ ret: 0 }), {
773
+ status: 200,
774
+ headers: { 'Content-Type': 'application/json' },
775
+ });
776
+ });
777
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
778
+ store: {
779
+ getPlatformThreadBinding: () => ({
780
+ workspace_id: 'default',
781
+ platform: 'weixin',
782
+ chat_id: 'chat-1',
783
+ platform_user_id: 'user-1',
784
+ thread_id: 'thread-1',
785
+ last_platform_message_id: 'ctx-1',
786
+ }),
787
+ },
788
+ readConfig: async () => ({
789
+ projects: [
790
+ {
791
+ name: 'default',
792
+ agent: { type: 'localcore-acp', providers: [] },
793
+ platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
794
+ },
795
+ ],
796
+ }),
797
+ getWorkspaceRouter: () => ({}),
798
+ eventBus: { emit: () => { }, on: () => () => { } },
799
+ });
800
+ const internals = gateway;
801
+ internals.runtime.set('default', {
802
+ workspaceId: 'default',
803
+ enabled: true,
804
+ status: 'running',
805
+ connected: true,
806
+ accountId: 'bot-1',
807
+ });
808
+ internals.threadRouting.set('session:thread-1', {
809
+ workspaceId: 'default',
810
+ platformUserId: 'user-1',
811
+ chatId: 'chat-1',
812
+ threadId: 'thread-1',
813
+ });
814
+ try {
815
+ await gateway.onBridgeEvent({ type: 'status', sessionKey: 'session:thread-1', content: '正在检查桌面文件' });
816
+ strict_1.default.equal(sentBodies.length, 1);
817
+ strict_1.default.equal(sentBodies[0]?.msg?.context_token, 'ctx-1');
818
+ strict_1.default.equal(sentBodies[0]?.msg?.message_state, 2);
819
+ strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, '正在检查桌面文件');
820
+ }
821
+ finally {
822
+ globalThis.fetch = originalFetch;
823
+ }
824
+ });
825
+ (0, node_test_1.default)('weixin bridge sends tool progress in real time before final reply', async () => {
826
+ const originalFetch = globalThis.fetch;
827
+ const sentBodies = [];
828
+ globalThis.fetch = (async (_input, init) => {
829
+ sentBodies.push(JSON.parse(String(init?.body || '{}')));
830
+ return new Response(JSON.stringify({ ret: 0 }), {
831
+ status: 200,
832
+ headers: { 'Content-Type': 'application/json' },
833
+ });
834
+ });
835
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
836
+ store: {
837
+ getPlatformThreadBinding: () => ({
838
+ workspace_id: 'default',
839
+ platform: 'weixin',
840
+ chat_id: 'chat-1',
841
+ platform_user_id: 'user-1',
842
+ thread_id: 'thread-1',
843
+ last_platform_message_id: 'ctx-1',
844
+ }),
845
+ },
846
+ readConfig: async () => ({
847
+ projects: [
848
+ {
849
+ name: 'default',
850
+ agent: { type: 'localcore-acp', providers: [] },
851
+ platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
852
+ },
853
+ ],
854
+ }),
855
+ getWorkspaceRouter: () => ({}),
856
+ eventBus: { emit: () => { }, on: () => () => { } },
857
+ });
858
+ const internals = gateway;
859
+ internals.runtime.set('default', {
860
+ workspaceId: 'default',
861
+ enabled: true,
862
+ status: 'running',
863
+ connected: true,
864
+ accountId: 'bot-1',
865
+ });
866
+ internals.threadRouting.set('session:thread-1', {
867
+ workspaceId: 'default',
868
+ platformUserId: 'user-1',
869
+ chatId: 'chat-1',
870
+ threadId: 'thread-1',
871
+ });
872
+ try {
873
+ await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: '🔧 list desktop' });
874
+ await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'final reply' });
875
+ strict_1.default.equal(sentBodies.length, 2);
876
+ strict_1.default.equal(sentBodies[0]?.msg?.message_state, 2);
877
+ strict_1.default.equal(sentBodies[1]?.msg?.message_state, 2);
878
+ strict_1.default.equal(sentBodies[0]?.msg?.context_token, 'ctx-1');
879
+ strict_1.default.equal(sentBodies[1]?.msg?.context_token, 'ctx-1');
880
+ strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, '🔧 list desktop');
881
+ strict_1.default.equal(sentBodies[1]?.msg?.item_list?.[0]?.text_item?.text, 'final reply');
882
+ }
883
+ finally {
884
+ globalThis.fetch = originalFetch;
885
+ }
886
+ });
887
+ (0, node_test_1.default)('weixin bridge skips completed tool result updates but keeps final reply', async () => {
888
+ const originalFetch = globalThis.fetch;
889
+ const sentBodies = [];
890
+ globalThis.fetch = (async (_input, init) => {
891
+ sentBodies.push(JSON.parse(String(init?.body || '{}')));
892
+ return new Response(JSON.stringify({ errcode: 0 }), {
893
+ status: 200,
894
+ headers: { 'Content-Type': 'application/json' },
895
+ });
896
+ });
897
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
898
+ store: {
899
+ getPlatformThreadBinding: () => ({
900
+ workspace_id: 'default',
901
+ platform: 'weixin',
902
+ chat_id: 'chat-1',
903
+ platform_user_id: 'user-1',
904
+ thread_id: 'thread-1',
905
+ last_platform_message_id: 'ctx-1',
906
+ }),
907
+ },
908
+ readConfig: async () => ({
909
+ projects: [
910
+ {
911
+ name: 'default',
912
+ agent: { type: 'localcore-acp', providers: [] },
913
+ platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
914
+ },
915
+ ],
916
+ }),
917
+ getWorkspaceRouter: () => ({}),
918
+ eventBus: { emit: () => { }, on: () => () => { } },
919
+ });
920
+ const internals = gateway;
921
+ internals.runtime.set('default', {
922
+ workspaceId: 'default',
923
+ enabled: true,
924
+ status: 'running',
925
+ connected: true,
926
+ accountId: 'bot-1',
927
+ });
928
+ internals.threadRouting.set('session:thread-1', {
929
+ workspaceId: 'default',
930
+ platformUserId: 'user-1',
931
+ chatId: 'chat-1',
932
+ threadId: 'thread-1',
933
+ });
934
+ try {
935
+ await gateway.onBridgeEvent({
936
+ type: 'reply',
937
+ sessionKey: 'session:thread-1',
938
+ content: '🔧 Tool update - completed - /Users/mochuxian/Desktop has many files and this result should not be sent',
939
+ });
940
+ await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'final reply' });
941
+ strict_1.default.equal(sentBodies.length, 1);
942
+ strict_1.default.equal(sentBodies[0]?.msg?.message_state, 2);
943
+ strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, 'final reply');
944
+ }
945
+ finally {
946
+ globalThis.fetch = originalFetch;
947
+ }
948
+ });
949
+ (0, node_test_1.default)('weixin bridge keeps failed tool update status without execution details', async () => {
950
+ const originalFetch = globalThis.fetch;
951
+ const sentBodies = [];
952
+ globalThis.fetch = (async (_input, init) => {
953
+ sentBodies.push(JSON.parse(String(init?.body || '{}')));
954
+ return new Response(JSON.stringify({ errcode: 0 }), {
955
+ status: 200,
956
+ headers: { 'Content-Type': 'application/json' },
957
+ });
958
+ });
959
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
960
+ store: {
961
+ getPlatformThreadBinding: () => ({
962
+ workspace_id: 'default',
963
+ platform: 'weixin',
964
+ chat_id: 'chat-1',
965
+ platform_user_id: 'user-1',
966
+ thread_id: 'thread-1',
967
+ last_platform_message_id: 'ctx-1',
968
+ }),
969
+ },
970
+ readConfig: async () => ({
971
+ projects: [
972
+ {
973
+ name: 'default',
974
+ agent: { type: 'localcore-acp', providers: [] },
975
+ platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
976
+ },
977
+ ],
978
+ }),
979
+ getWorkspaceRouter: () => ({}),
980
+ eventBus: { emit: () => { }, on: () => () => { } },
981
+ });
982
+ const internals = gateway;
983
+ internals.runtime.set('default', {
984
+ workspaceId: 'default',
985
+ enabled: true,
986
+ status: 'running',
987
+ connected: true,
988
+ accountId: 'bot-1',
989
+ });
990
+ internals.threadRouting.set('session:thread-1', {
991
+ workspaceId: 'default',
992
+ platformUserId: 'user-1',
993
+ chatId: 'chat-1',
994
+ threadId: 'thread-1',
995
+ });
996
+ try {
997
+ await gateway.onBridgeEvent({
998
+ type: 'reply',
999
+ sessionKey: 'session:thread-1',
1000
+ content: '🔧 Tool update - failed - stack trace and command output should not be sent',
1001
+ });
1002
+ strict_1.default.equal(sentBodies.length, 1);
1003
+ strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, '🔧 Tool update - failed');
1004
+ }
1005
+ finally {
1006
+ globalThis.fetch = originalFetch;
1007
+ }
1008
+ });
1009
+ (0, node_test_1.default)('weixin bridge folds progress after nine context sends and preserves final reply', async () => {
1010
+ const originalFetch = globalThis.fetch;
1011
+ const sentBodies = [];
1012
+ globalThis.fetch = (async (_input, init) => {
1013
+ sentBodies.push(JSON.parse(String(init?.body || '{}')));
1014
+ return new Response(JSON.stringify({ ret: 0 }), {
1015
+ status: 200,
1016
+ headers: { 'Content-Type': 'application/json' },
1017
+ });
1018
+ });
1019
+ const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
1020
+ store: {
1021
+ getPlatformThreadBinding: () => ({
1022
+ workspace_id: 'default',
1023
+ platform: 'weixin',
1024
+ chat_id: 'chat-1',
1025
+ platform_user_id: 'user-1',
1026
+ thread_id: 'thread-1',
1027
+ last_platform_message_id: 'ctx-1',
1028
+ }),
1029
+ },
1030
+ readConfig: async () => ({
1031
+ projects: [
1032
+ {
1033
+ name: 'default',
1034
+ agent: { type: 'localcore-acp', providers: [] },
1035
+ platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
1036
+ },
1037
+ ],
1038
+ }),
1039
+ getWorkspaceRouter: () => ({}),
1040
+ eventBus: { emit: () => { }, on: () => () => { } },
1041
+ });
1042
+ const internals = gateway;
1043
+ internals.runtime.set('default', {
1044
+ workspaceId: 'default',
1045
+ enabled: true,
1046
+ status: 'running',
1047
+ connected: true,
1048
+ accountId: 'bot-1',
1049
+ });
1050
+ internals.threadRouting.set('session:thread-1', {
1051
+ workspaceId: 'default',
1052
+ platformUserId: 'user-1',
1053
+ chatId: 'chat-1',
1054
+ threadId: 'thread-1',
1055
+ });
1056
+ try {
1057
+ for (let index = 1; index <= 12; index += 1) {
1058
+ await gateway.onBridgeEvent({
1059
+ type: 'reply',
1060
+ sessionKey: 'session:thread-1',
1061
+ content: `🔧 tool ${index}`,
1062
+ });
1063
+ }
1064
+ await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'final reply' });
1065
+ strict_1.default.equal(sentBodies.length, 10);
1066
+ strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, '🔧 tool 1');
1067
+ strict_1.default.equal(sentBodies[8]?.msg?.item_list?.[0]?.text_item?.text, '🔧 tool 9');
1068
+ strict_1.default.doesNotMatch(sentBodies.map((body) => body?.msg?.item_list?.[0]?.text_item?.text || '').join('\n'), /🔧 tool 10|🔧 tool 11|🔧 tool 12/);
1069
+ strict_1.default.match(sentBodies[9]?.msg?.item_list?.[0]?.text_item?.text, /已省略 3 条过程消息/);
1070
+ strict_1.default.match(sentBodies[9]?.msg?.item_list?.[0]?.text_item?.text, /final reply/);
1071
+ }
1072
+ finally {
1073
+ globalThis.fetch = originalFetch;
1074
+ }
1075
+ });