@kafca/agentdock 0.1.43 → 0.1.44

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 (59) hide show
  1. package/README.md +6 -0
  2. package/dist/renderer/assets/{Badge-CbG6yRSm.js → Badge-Qk9qHovH.js} +1 -1
  3. package/dist/renderer/assets/{Chat-BvNNWGGX.js → Chat-BK-fm0Xr.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-Ct-PvJB8.js → ChatMarkdown-BKdW8OON.js} +1 -1
  5. package/dist/renderer/assets/{Config-lQX2qVVV.js → Config-B93CQGvY.js} +1 -1
  6. package/dist/renderer/assets/{CronList-Dj_3muVG.js → CronList-Z3TvSRKP.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-eh_RhGYb.js → Dashboard-Ch3QkWwf.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-Bn2Qh14U.js → EmptyState-kD9yAwtw.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-Bsz6Ou41.js → KnowledgeDetail-BAMCLrjz.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeHome-Cgj4XFXn.js → KnowledgeHome-ChvjOfrc.js} +1 -1
  11. package/dist/renderer/assets/{Logs-Dp-NF6GN.js → Logs-Cv0nth3j.js} +1 -1
  12. package/dist/renderer/assets/{Modal-B6YPPBN6.js → Modal-DwY4lecS.js} +1 -1
  13. package/dist/renderer/assets/{MonitorList-xEZWg460.js → MonitorList-JEvMoWUm.js} +1 -1
  14. package/dist/renderer/assets/{Page-Bn7RjGOS.js → Page-CnB9ePsb.js} +1 -1
  15. package/dist/renderer/assets/{ProjectDetail-Cv_jIE61.js → ProjectDetail-DJa3sMmU.js} +1 -1
  16. package/dist/renderer/assets/{ProjectList-BDk5_gqY.js → ProjectList-D6CXumR_.js} +1 -1
  17. package/dist/renderer/assets/{Select-xshfMIEQ.js → Select-zfoksLGB.js} +1 -1
  18. package/dist/renderer/assets/{SessionChat-BkHy90z3.js → SessionChat-DUTh4WoM.js} +1 -1
  19. package/dist/renderer/assets/{SessionList-BZyXT92L.js → SessionList-D9o-lUst.js} +1 -1
  20. package/dist/renderer/assets/{ThreadChat-DUOIyXrS.js → ThreadChat-BYXlT-Kc.js} +1 -1
  21. package/dist/renderer/assets/{Workspace-Dl80fNEC.js → Workspace-BrhSwRQ-.js} +1 -1
  22. package/dist/renderer/assets/{arrow-left-C54ZI-AS.js → arrow-left-CNEAuElg.js} +1 -1
  23. package/dist/renderer/assets/{book-open-Dl7GO0zn.js → book-open-BNJMhNI-.js} +1 -1
  24. package/dist/renderer/assets/{check-CiqgtrOL.js → check-BnclClRU.js} +1 -1
  25. package/dist/renderer/assets/{chevron-down-DTL2OuVh.js → chevron-down-Bh53e0O-.js} +1 -1
  26. package/dist/renderer/assets/{circle-DaVKfa3P.js → circle-ClZTWCWO.js} +1 -1
  27. package/dist/renderer/assets/{desktop-jaAiayNB.js → desktop-C5vBT0yM.js} +1 -1
  28. package/dist/renderer/assets/{funnel-CrmnLMJI.js → funnel-_5hsz0xI.js} +1 -1
  29. package/dist/renderer/assets/{heart-BMfQI7y_.js → heart-u4Q1aOqj.js} +1 -1
  30. package/dist/renderer/assets/{index-HPNb1zfx.js → index-B36eowj-.js} +2 -2
  31. package/dist/renderer/assets/{index-nreiKY1f.js → index-BNFx4uYv.js} +1 -1
  32. package/dist/renderer/assets/{pencil-wTQ4l465.js → pencil-DLh9QNmT.js} +1 -1
  33. package/dist/renderer/assets/{play-BK--MXmw.js → play-BDTk9Scm.js} +1 -1
  34. package/dist/renderer/assets/{plug-BIx9fjip.js → plug-BG0JHW6B.js} +1 -1
  35. package/dist/renderer/assets/{plus-Dn7JFWQ8.js → plus-BSc8POsz.js} +1 -1
  36. package/dist/renderer/assets/projects-DwfZwxDW.js +1 -0
  37. package/dist/renderer/assets/{save-CC3zOSWb.js → save-D5AO_G6a.js} +1 -1
  38. package/dist/renderer/assets/{search-CDTy9Czu.js → search-CniPk7Ey.js} +1 -1
  39. package/dist/renderer/assets/{server-DtQk3t2a.js → server-GsXyQAZm.js} +1 -1
  40. package/dist/renderer/assets/{session-utils-4ep5D66T.js → session-utils-BDxzgTBC.js} +1 -1
  41. package/dist/renderer/assets/{sessions-Dor1LteH.js → sessions-CLcKA1Tt.js} +1 -1
  42. package/dist/renderer/assets/{shield-check-Cz2LpU6r.js → shield-check-4i0E3hmf.js} +1 -1
  43. package/dist/renderer/assets/{trash-2-C2EmWFZV.js → trash-2-DhtOCsO5.js} +1 -1
  44. package/dist/renderer/assets/{user-I8B-cs5A.js → user-BtC_pVXV.js} +1 -1
  45. package/dist/renderer/assets/{wifi-off-CPevTQJn.js → wifi-off-BHPzPY_B.js} +1 -1
  46. package/dist/renderer/index.html +1 -1
  47. package/dist-electron/services/local-ai-core/src/channel/lark/config.js +2 -0
  48. package/dist-electron/services/local-ai-core/src/channel/lark/inbound.js +114 -0
  49. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +52 -65
  50. package/dist-electron/services/local-ai-core/src/channel/shared/thread-routing.js +34 -0
  51. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +12 -18
  52. package/dist-electron/tests/integration/channel-gateways.test.js +32 -0
  53. package/dist-electron/tests/integration/lark-gateway.test.js +1918 -0
  54. package/dist-electron/tests/integration/local-core-acp-progress.test.js +1772 -0
  55. package/dist-electron/tests/integration/local-core-refactor.test.js +0 -4544
  56. package/dist-electron/tests/integration/local-core-routes.test.js +204 -0
  57. package/dist-electron/tests/integration/weixin-gateway.test.js +927 -0
  58. package/package.json +1 -1
  59. package/dist/renderer/assets/projects-brJZgwEJ.js +0 -1
@@ -8,3674 +8,17 @@ const strict_1 = __importDefault(require("node:assert/strict"));
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_os_1 = require("node:os");
10
10
  const node_path_1 = require("node:path");
11
- const node_stream_1 = require("node:stream");
12
11
  const local_core_acp_response_processor_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-response-processor.js");
13
12
  const local_core_slash_commands_js_1 = require("../../services/local-ai-core/src/acp/local-core-slash-commands.js");
14
13
  const scheduled_conversation_executor_js_1 = require("../../services/local-ai-core/src/scheduler/scheduled-conversation-executor.js");
15
14
  const scheduler_run_lifecycle_js_1 = require("../../services/local-ai-core/src/scheduler/scheduler-run-lifecycle.js");
16
15
  const lark_execution_policies_js_1 = require("../../services/local-ai-core/src/scheduler/lark-execution-policies.js");
17
16
  const local_schedule_adapter_js_1 = require("../../services/local-ai-core/src/scheduler/local-schedule-adapter.js");
18
- const local_core_weixin_gateway_js_1 = require("../../services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js");
19
- const local_core_lark_gateway_js_1 = require("../../services/local-ai-core/src/channel/lark/local-core-lark-gateway.js");
20
17
  const cards_js_1 = require("../../services/local-ai-core/src/channel/lark/cards.js");
21
- const runtime_state_js_1 = require("../../services/local-ai-core/src/channel/lark/runtime-state.js");
22
18
  const local_core_acp_backend_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-backend.js");
23
- const local_core_acp_turn_coordinator_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js");
24
19
  const local_core_acp_store_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-store.js");
25
20
  const session_command_service_js_1 = require("../../services/local-ai-core/src/thread/session-command-service.js");
26
21
  const thread_command_service_js_1 = require("../../services/local-ai-core/src/thread/thread-command-service.js");
27
- const local_core_acp_progress_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-progress.js");
28
- const local_core_acp_permission_lifecycle_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-permission-lifecycle.js");
29
- const workspace_acp_permissions_js_1 = require("../../services/local-ai-core/src/acp/workspace-acp-permissions.js");
30
- const server_routes_js_1 = require("../../services/local-ai-core/src/runtime/server-routes.js");
31
- function extractLarkCreatedMessage(request) {
32
- const msgType = String(request.data?.msg_type || '');
33
- const content = JSON.parse(String(request.data?.content || '{}'));
34
- return {
35
- msgType,
36
- content,
37
- text: extractLarkCreatedMessageText(msgType, content),
38
- };
39
- }
40
- function extractLarkCreatedMessageText(msgType, content) {
41
- if (msgType === 'interactive') {
42
- return String(content.elements?.[0]?.content || content.body?.elements?.[0]?.content || '');
43
- }
44
- if (msgType === 'post') {
45
- return (content.zh_cn?.content || [])
46
- .map((line) => (Array.isArray(line) ? line : [])
47
- .map((item) => {
48
- return String(item?.text || item?.href || '');
49
- })
50
- .join(''))
51
- .join('\n');
52
- }
53
- return String(content.text || '');
54
- }
55
- function findLarkPostMdText(content) {
56
- for (const line of content.zh_cn?.content || []) {
57
- for (const item of Array.isArray(line) ? line : []) {
58
- if (item?.tag === 'md') {
59
- return String(item.text || '');
60
- }
61
- }
62
- }
63
- return '';
64
- }
65
- (0, node_test_1.default)('local core route parser separates runtime refresh and runtime detail routes', () => {
66
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/logs'), { name: 'logs.list' });
67
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/runtimes'), { name: 'runtimes.list' });
68
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/runtimes/refresh'), { name: 'runtimes.refresh' });
69
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/runtimes/codex'), {
70
- name: 'runtimes.detail',
71
- runtimeId: 'codex',
72
- });
73
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/runtimes/codex/refresh'), {
74
- name: 'runtimes.refresh-one',
75
- runtimeId: 'codex',
76
- });
77
- });
78
- (0, node_test_1.default)('local core route parser keeps scheduler job get, runs, and run distinct', () => {
79
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/scheduler/jobs/job-abc'), {
80
- name: 'scheduler.job.get',
81
- jobId: 'job-abc',
82
- });
83
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/scheduler/jobs/job-abc/runs'), {
84
- name: 'scheduler.job.runs',
85
- jobId: 'job-abc',
86
- });
87
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/scheduler/jobs/job-abc/run'), {
88
- name: 'scheduler.job.run',
89
- jobId: 'job-abc',
90
- });
91
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/scheduler/jobs/job-abc/run'), null);
92
- });
93
- (0, node_test_1.default)('local core route parser keeps thread actions separate from generic thread routes', () => {
94
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/threads'), { name: 'threads.list' });
95
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/threads'), { name: 'threads.create' });
96
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/threads/workspace%2Fa%3A%3Athread%2F1'), {
97
- name: 'thread.get',
98
- threadId: 'workspace/a::thread/1',
99
- });
100
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('PATCH', '/api/local/v1/threads/thread-1/knowledge-bases'), {
101
- name: 'thread.update-knowledge-bases',
102
- threadId: 'thread-1',
103
- });
104
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/threads/thread-1/messages'), {
105
- name: 'thread.messages.send',
106
- threadId: 'thread-1',
107
- });
108
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/threads/thread-1/actions'), {
109
- name: 'thread.actions.send',
110
- threadId: 'thread-1',
111
- });
112
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/threads/thread-1/messages'), null);
113
- });
114
- (0, node_test_1.default)('local core route parser only accepts run interrupt action with POST', () => {
115
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/runs/run-1/interrupt'), {
116
- name: 'run.interrupt',
117
- runId: 'run-1',
118
- });
119
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/runs/run-1/interrupt'), null);
120
- });
121
- (0, node_test_1.default)('local core route parser keeps workspace state routes bounded to one id segment', () => {
122
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/workspaces'), { name: 'workspaces.list' });
123
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/workspaces/workspace%2Fone/streaming-probe'), {
124
- name: 'workspace.streaming-probe',
125
- workspaceId: 'workspace/one',
126
- });
127
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/workspace-registry'), { name: 'workspace-registry.create' });
128
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('PATCH', '/api/local/v1/workspace-registry/workspace%2Fone'), {
129
- name: 'workspace-registry.update',
130
- workspaceId: 'workspace/one',
131
- });
132
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/workspace-security/workspace%2Fone'), {
133
- name: 'workspace-security.get',
134
- workspaceId: 'workspace/one',
135
- });
136
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/workspace-registry/workspace-1/extra'), null);
137
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/workspace-security/workspace-1'), null);
138
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/workspaces/workspace-1/streaming-probe'), null);
139
- });
140
- (0, node_test_1.default)('local core route parser keeps approval resolution separate from approval detail', () => {
141
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/approvals'), { name: 'approvals.list' });
142
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/approvals'), { name: 'approvals.create' });
143
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/approvals/approval-1'), {
144
- name: 'approval.get',
145
- approvalId: 'approval-1',
146
- });
147
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/approvals/approval-1/resolve'), {
148
- name: 'approval.resolve',
149
- approvalId: 'approval-1',
150
- });
151
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/approvals/approval-1/resolve'), null);
152
- });
153
- (0, node_test_1.default)('local core route parser keeps task collection and task detail routes distinct', () => {
154
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/audit-events'), { name: 'audit-events.list' });
155
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/security/command-risk'), { name: 'security.command-risk.classify' });
156
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/tasks'), { name: 'tasks.list' });
157
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/tasks'), { name: 'tasks.create' });
158
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('PATCH', '/api/local/v1/tasks/task%2Fone'), {
159
- name: 'task.update',
160
- taskId: 'task/one',
161
- });
162
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('DELETE', '/api/local/v1/tasks/task-1'), null);
163
- });
164
- (0, node_test_1.default)('local core route parser keeps knowledge collection and folder routes distinct', () => {
165
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/knowledge/sources'), { name: 'knowledge.sources.list' });
166
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/knowledge/config'), { name: 'knowledge.config.read' });
167
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/knowledge/config'), { name: 'knowledge.config.update' });
168
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/knowledge/folders'), { name: 'knowledge.folders.list' });
169
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/knowledge/folders'), { name: 'knowledge.folders.create' });
170
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('PATCH', '/api/local/v1/knowledge/folders/folder%2Fone'), {
171
- name: 'knowledge.folder.update',
172
- folderId: 'folder/one',
173
- });
174
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/knowledge/folders/folder-1'), null);
175
- });
176
- (0, node_test_1.default)('local core route parser keeps knowledge base files and search routes distinct', () => {
177
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/knowledge/bases'), { name: 'knowledge.bases.list' });
178
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/knowledge/bases'), { name: 'knowledge.bases.create' });
179
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/knowledge/bases/base%2Fone'), {
180
- name: 'knowledge.base.get',
181
- knowledgeBaseId: 'base/one',
182
- });
183
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/knowledge/bases/base%2Fone/files'), {
184
- name: 'knowledge.base.files.list',
185
- knowledgeBaseId: 'base/one',
186
- });
187
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/knowledge/bases/base%2Fone/search'), {
188
- name: 'knowledge.base.search',
189
- knowledgeBaseId: 'base/one',
190
- });
191
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('DELETE', '/api/local/v1/knowledge/bases/base%2Fone/files/file%2Fone'), {
192
- name: 'knowledge.base.file.delete',
193
- knowledgeBaseId: 'base/one',
194
- fileId: 'file/one',
195
- });
196
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('PATCH', '/api/local/v1/knowledge/bases/base-1/files'), null);
197
- });
198
- (0, node_test_1.default)('local core route parser recognizes capability, plugin, and event routes', () => {
199
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/capabilities'), { name: 'capabilities.read' });
200
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/capabilities/snapshot'), { name: 'capabilities.snapshot' });
201
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/plugins/diagnostics'), { name: 'plugins.diagnostics' });
202
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/events'), { name: 'events.stream' });
203
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/events'), null);
204
- });
205
- (0, node_test_1.default)('local core route parser keeps platform read routes distinct', () => {
206
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/platforms/lark'), {
207
- name: 'platform.gateways.list',
208
- platform: 'lark',
209
- });
210
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/platforms/lark/pairings'), {
211
- name: 'platform.pairings.list',
212
- platform: 'lark',
213
- });
214
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/platforms/lark/users'), {
215
- name: 'platform.users.list',
216
- platform: 'lark',
217
- });
218
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/platforms/lark/workspace%2Fone'), {
219
- name: 'platform.gateway.get',
220
- platform: 'lark',
221
- workspaceId: 'workspace/one',
222
- });
223
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/platforms/lark/workspace%2Fone/qrcode/status'), {
224
- name: 'platform.qrcode.status',
225
- platform: 'lark',
226
- workspaceId: 'workspace/one',
227
- });
228
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/platforms/lark/workspace-1/qrcode'), null);
229
- });
230
- (0, node_test_1.default)('local core route parser keeps platform write routes distinct', () => {
231
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/platforms/lark/pairings/approve'), {
232
- name: 'platform.pairing.approve',
233
- platform: 'lark',
234
- });
235
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/platforms/lark/pairings/reject'), {
236
- name: 'platform.pairing.reject',
237
- platform: 'lark',
238
- });
239
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/platforms/lark/workspace%2Fone/test'), {
240
- name: 'platform.gateway.test',
241
- platform: 'lark',
242
- workspaceId: 'workspace/one',
243
- });
244
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/platforms/lark/workspace%2Fone/files'), {
245
- name: 'platform.file.send',
246
- platform: 'lark',
247
- workspaceId: 'workspace/one',
248
- });
249
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/platforms/lark/workspace%2Fone/messages'), {
250
- name: 'platform.message.send',
251
- platform: 'lark',
252
- workspaceId: 'workspace/one',
253
- });
254
- strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/platforms/lark/workspace%2Fone/qrcode'), {
255
- name: 'platform.qrcode.create',
256
- platform: 'lark',
257
- workspaceId: 'workspace/one',
258
- });
259
- strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/platforms/lark/pairings/qrcode'), null);
260
- });
261
- (0, node_test_1.default)('ACP tool call update is emitted with its pending tool name', () => {
262
- const appended = [];
263
- const emitted = [];
264
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
265
- appendMessage: (_threadId, _role, content, kind) => appended.push({ content, kind }),
266
- emitBridge: (event) => emitted.push(event),
267
- updateRunStatus: () => { },
268
- sendRaw: () => true,
269
- });
270
- const session = {
271
- threadId: 'thread-1',
272
- bridgeSessionKey: 'session:thread-1',
273
- currentRunId: 'run-1',
274
- currentTurn: {
275
- runId: 'run-1',
276
- replyCtx: 'run-1',
277
- previewHandle: 'preview-1',
278
- assistantText: '',
279
- typingStarted: true,
280
- previewStarted: false,
281
- permission: null,
282
- },
283
- loadReplayMode: false,
284
- schedulerJobCreatedByRun: new Map(),
285
- };
286
- coordinator.handleAgentNotification(session, {
287
- method: 'session/update',
288
- params: {
289
- update: {
290
- sessionUpdate: 'tool_call',
291
- title: 'Terminal',
292
- },
293
- },
294
- });
295
- coordinator.handleAgentNotification(session, {
296
- method: 'session/update',
297
- params: {
298
- update: {
299
- sessionUpdate: 'tool_call_update',
300
- title: "ls ~/Desktop - List files on the user's desktop",
301
- status: 'running',
302
- },
303
- },
304
- });
305
- strict_1.default.deepEqual(appended, [
306
- {
307
- content: "🔧 Terminal: ls ~/Desktop - List files on the user's desktop - running",
308
- kind: 'progress',
309
- },
310
- ]);
311
- strict_1.default.equal(emitted.length, 1);
312
- strict_1.default.equal(emitted[0]?.content, appended[0]?.content);
313
- });
314
- (0, node_test_1.default)('ACP permission normalization treats allow_all as allow all', () => {
315
- strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionAction)('allow_all'), 'allow all');
316
- strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionAction)('allow_always'), 'allow all');
317
- strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionAction)('always'), 'allow all');
318
- strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionAction)('allow_once'), 'allow');
319
- });
320
- (0, node_test_1.default)('ACP permission normalization preserves always-allow option semantics', () => {
321
- strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionOptionAction)({
322
- optionId: 'approve',
323
- name: 'Always allow',
324
- kind: 'allow',
325
- }), 'allow all');
326
- strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionOptionAction)({
327
- optionId: 'allow-all-tools',
328
- name: '始终允许',
329
- kind: 'allow',
330
- }), 'allow all');
331
- strict_1.default.equal((0, workspace_acp_permissions_js_1.normalizePermissionOptionAction)({
332
- optionId: 'allow_once',
333
- name: 'Allow once',
334
- kind: 'allow',
335
- }), 'allow');
336
- });
337
- (0, node_test_1.default)('ACP permission lifecycle parses actionable options and fallback prompt content', () => {
338
- const options = (0, local_core_acp_permission_lifecycle_js_1.parsePermissionOptions)([
339
- { optionId: 'approve-once', name: 'Allow once', kind: 'allow' },
340
- { optionId: 'approve-always', name: 'Always allow', kind: 'allow' },
341
- { optionId: '', name: 'missing id', kind: 'reject' },
342
- ]);
343
- strict_1.default.deepEqual(options, [
344
- {
345
- optionId: 'approve-once',
346
- name: 'Allow once',
347
- kind: 'allow',
348
- normalizedAction: 'allow',
349
- },
350
- {
351
- optionId: 'approve-always',
352
- name: 'Always allow',
353
- kind: 'allow',
354
- normalizedAction: 'allow all',
355
- },
356
- ]);
357
- strict_1.default.deepEqual((0, local_core_acp_permission_lifecycle_js_1.createRunningPermissionRequest)({
358
- requestId: 42,
359
- toolTitle: 'Terminal: lac scheduler add --cron "* * * * *"',
360
- options,
361
- approvalId: 'approval-1',
362
- }), {
363
- requestId: 42,
364
- approvalId: 'approval-1',
365
- toolTitle: 'Terminal: lac scheduler add --cron "* * * * *"',
366
- isSchedulerAdd: true,
367
- options,
368
- });
369
- strict_1.default.deepEqual((0, local_core_acp_permission_lifecycle_js_1.createPermissionApprovalInput)({
370
- threadId: 'thread-1',
371
- runId: 'run-1',
372
- toolTitle: 'Terminal: npm test',
373
- options,
374
- }), {
375
- threadId: 'thread-1',
376
- runId: 'run-1',
377
- title: 'Approve Terminal: npm test',
378
- description: 'Terminal: npm test',
379
- command: 'Terminal: npm test',
380
- options,
381
- });
382
- strict_1.default.equal((0, local_core_acp_permission_lifecycle_js_1.isSchedulerAddCommand)('Terminal: lac scheduler add --cron "* * * * *"'), true);
383
- strict_1.default.match((0, local_core_acp_permission_lifecycle_js_1.createPermissionPrompt)('Terminal: npm test'), /Terminal: npm test/);
384
- strict_1.default.match((0, local_core_acp_permission_lifecycle_js_1.createPermissionPrompt)('Terminal: npm test'), /allow all \/ allow \/ deny/);
385
- });
386
- (0, node_test_1.default)('ACP permission lifecycle writes pending permission state and tool detail together', () => {
387
- const permissionRequest = (0, local_core_acp_permission_lifecycle_js_1.createRunningPermissionRequest)({
388
- requestId: 42,
389
- toolTitle: 'Terminal: npm test',
390
- options: [],
391
- approvalId: 'approval-1',
392
- });
393
- const toolCall = {
394
- key: 'call-1',
395
- title: 'Terminal',
396
- messageId: 'run-1-tool-1',
397
- sequence: 1,
398
- emitted: false,
399
- };
400
- const currentTurn = {
401
- runId: 'run-1',
402
- replyCtx: 'run-1',
403
- previewHandle: 'preview-1',
404
- thoughtPreviewHandle: 'thought-preview-1',
405
- thoughtMessageId: 'run-1-thought',
406
- assistantText: '',
407
- thoughtText: '',
408
- typingStarted: true,
409
- previewStarted: false,
410
- thoughtPreviewStarted: false,
411
- permission: null,
412
- pendingToolCalls: { 'call-1': toolCall },
413
- pendingToolCallOrder: ['call-1'],
414
- activeToolCallKey: 'call-1',
415
- };
416
- const session = {
417
- currentTurn,
418
- pendingPermissionByRun: new Map(),
419
- };
420
- const synced = [];
421
- (0, local_core_acp_permission_lifecycle_js_1.applyPendingPermissionRequest)({
422
- session,
423
- runId: 'run-1',
424
- permissionRequest,
425
- resolveFallbackToolCall: () => toolCall,
426
- syncLegacyPendingToolCall: (_turn, nextToolCall) => synced.push(nextToolCall?.detail || ''),
427
- });
428
- strict_1.default.equal(session.pendingPermissionByRun.get('run-1'), permissionRequest);
429
- strict_1.default.equal(currentTurn.permission, permissionRequest);
430
- strict_1.default.equal(currentTurn.pendingToolCallDetail, 'Terminal: npm test');
431
- strict_1.default.equal(toolCall.detail, 'Terminal: npm test');
432
- strict_1.default.deepEqual(synced, ['Terminal: npm test']);
433
- });
434
- (0, node_test_1.default)('ACP pending permission is projected into refreshed thread detail payloads', () => {
435
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
436
- emitBridge: () => { },
437
- appendMessage: () => { },
438
- updateRunStatus: () => { },
439
- sendRaw: () => true,
440
- });
441
- const permissionRequest = (0, local_core_acp_permission_lifecycle_js_1.createRunningPermissionRequest)({
442
- requestId: 42,
443
- toolTitle: 'Terminal: npm test',
444
- options: (0, local_core_acp_permission_lifecycle_js_1.parsePermissionOptions)([
445
- { optionId: 'approve-once', name: 'Allow once', kind: 'allow' },
446
- { optionId: 'reject', name: 'Deny', kind: 'reject' },
447
- ]),
448
- approvalId: 'approval-1',
449
- });
450
- const session = {
451
- currentRunId: 'run-1',
452
- pendingPermissionByRun: new Map([['run-1', permissionRequest]]),
453
- };
454
- const detail = {
455
- messages: [
456
- {
457
- id: 'permission-message',
458
- role: 'assistant',
459
- content: '等待工具确认',
460
- },
461
- ],
462
- };
463
- const pending = coordinator.getPendingPermissionRequest(session, detail);
464
- strict_1.default.ok(pending);
465
- strict_1.default.equal(pending.id, 'permission-message');
466
- strict_1.default.equal(pending.content, '等待工具确认');
467
- strict_1.default.equal(pending.actionReplyCtx, 'run-1');
468
- strict_1.default.equal(pending.actionMode, 'permission');
469
- strict_1.default.equal(pending.actionInteractive, true);
470
- strict_1.default.deepEqual(pending.actions.flat().map((action) => action.data), ['allow', 'deny']);
471
- });
472
- (0, node_test_1.default)('ACP progress projection extracts tool output and formats durable progress content', () => {
473
- strict_1.default.equal((0, local_core_acp_progress_js_1.extractToolCallKey)({ tool_call_id: ' call-a ' }), 'call-a');
474
- strict_1.default.equal((0, local_core_acp_progress_js_1.extractToolCallKey)({ invocationId: 42 }), '42');
475
- strict_1.default.equal((0, local_core_acp_progress_js_1.extractToolCallKey)({ id: ' ' }), '');
476
- strict_1.default.equal((0, local_core_acp_progress_js_1.extractToolUpdateContent)([
477
- { type: 'content', content: { type: 'text', text: 'first line' } },
478
- { type: 'content', content: { type: 'image', text: 'ignored' } },
479
- { type: 'content', content: { type: 'text', text: 'second line' } },
480
- ]), 'first line\nsecond line');
481
- strict_1.default.equal((0, local_core_acp_progress_js_1.formatToolProgressMessage)({
482
- toolName: 'Terminal',
483
- title: 'npm test',
484
- status: 'completed',
485
- content: 'ok',
486
- }), '🔧 Terminal: npm test - completed - ok');
487
- strict_1.default.equal((0, local_core_acp_progress_js_1.resolveToolUpdateDisplayTitle)({
488
- title: 'Tool update',
489
- status: 'completed',
490
- priorDetail: 'npm test',
491
- }), 'npm test');
492
- strict_1.default.equal((0, local_core_acp_progress_js_1.isEmptyRunningToolUpdate)({
493
- title: 'Tool update',
494
- status: 'running',
495
- content: '',
496
- }), true);
497
- });
498
- (0, node_test_1.default)('ACP progress projection applies assistant and thought chunks with bridge metadata', () => {
499
- const currentTurn = {
500
- runId: 'run-1',
501
- previewHandle: 'preview-1',
502
- thoughtPreviewHandle: 'thought-preview-1',
503
- thoughtMessageId: 'run-1-thought',
504
- assistantText: '',
505
- thoughtText: '',
506
- previewStarted: false,
507
- thoughtPreviewStarted: false,
508
- };
509
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.applyAssistantMessageChunk)(currentTurn, 'hello'), {
510
- bridgeType: 'preview_start',
511
- previewHandle: 'preview-1',
512
- content: 'hello',
513
- bridgeKind: 'assistant',
514
- });
515
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.applyAssistantMessageChunk)(currentTurn, ' world'), {
516
- bridgeType: 'update_message',
517
- previewHandle: 'preview-1',
518
- content: 'hello world',
519
- bridgeKind: 'assistant',
520
- });
521
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.applyThoughtChunk)(currentTurn, '先理解'), {
522
- bridgeType: 'preview_start',
523
- previewHandle: 'thought-preview-1',
524
- messageId: 'run-1-thought',
525
- content: '先理解',
526
- bridgeKind: 'thought',
527
- });
528
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.applyThoughtChunk)(currentTurn, ',再修改'), {
529
- bridgeType: 'update_message',
530
- previewHandle: 'thought-preview-1',
531
- messageId: 'run-1-thought',
532
- content: '先理解,再修改',
533
- bridgeKind: 'thought',
534
- });
535
- strict_1.default.equal(currentTurn.thoughtText, '先理解,再修改');
536
- });
537
- (0, node_test_1.default)('ACP thought chunks merge provider snapshots without duplicating text', () => {
538
- const currentTurn = {
539
- runId: 'run-1',
540
- thoughtPreviewHandle: 'thought-preview-1',
541
- thoughtMessageId: 'run-1-thought',
542
- thoughtText: '',
543
- thoughtPreviewStarted: false,
544
- };
545
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.applyThoughtChunk)(currentTurn, 'The user wants to see their desktop files.'), {
546
- bridgeType: 'preview_start',
547
- previewHandle: 'thought-preview-1',
548
- messageId: 'run-1-thought',
549
- content: 'The user wants to see their desktop files.',
550
- bridgeKind: 'thought',
551
- });
552
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.applyThoughtChunk)(currentTurn, 'The user wants to see their desktop files. Let me show them.'), {
553
- bridgeType: 'update_message',
554
- previewHandle: 'thought-preview-1',
555
- messageId: 'run-1-thought',
556
- content: 'The user wants to see their desktop files. Let me show them.',
557
- bridgeKind: 'thought',
558
- });
559
- strict_1.default.equal(currentTurn.thoughtText, 'The user wants to see their desktop files. Let me show them.');
560
- });
561
- (0, node_test_1.default)('ACP thought segment close starts a new streaming preview for web and app', () => {
562
- const currentTurn = {
563
- runId: 'run-1',
564
- thoughtPreviewHandle: 'thought-preview-1',
565
- thoughtMessageId: 'run-1-thought-1',
566
- thoughtText: '',
567
- thoughtSequence: 1,
568
- thoughtPreviewStarted: false,
569
- };
570
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.applyThoughtChunk)(currentTurn, 'first thought'), {
571
- bridgeType: 'preview_start',
572
- previewHandle: 'thought-preview-1',
573
- messageId: 'run-1-thought-1',
574
- content: 'first thought',
575
- bridgeKind: 'thought',
576
- });
577
- (0, local_core_acp_progress_js_1.closeThoughtSegment)(currentTurn);
578
- strict_1.default.equal(currentTurn.thoughtText, '');
579
- strict_1.default.equal(currentTurn.thoughtPreviewStarted, false);
580
- strict_1.default.equal(currentTurn.thoughtMessageId, 'run-1-thought-2');
581
- strict_1.default.equal(currentTurn.thoughtPreviewHandle, 'run-1-thought-preview-2');
582
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.applyThoughtChunk)(currentTurn, 'second thought'), {
583
- bridgeType: 'preview_start',
584
- previewHandle: 'run-1-thought-preview-2',
585
- messageId: 'run-1-thought-2',
586
- content: 'second thought',
587
- bridgeKind: 'thought',
588
- });
589
- });
590
- (0, node_test_1.default)('ACP progress projection registers pending tool calls in order', () => {
591
- const currentTurn = {
592
- toolCallSequence: 0,
593
- pendingToolCalls: {},
594
- pendingToolCallOrder: [],
595
- };
596
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.registerPendingToolCall)({
597
- currentTurn,
598
- runId: 'run-1',
599
- update: { id: 'call-a', title: 'Terminal', rawInput: { command: 'npm test' } },
600
- }), {
601
- key: 'call-a',
602
- title: 'Terminal',
603
- messageId: 'run-1-tool-1',
604
- input: { command: 'npm test' },
605
- sequence: 1,
606
- emitted: false,
607
- });
608
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.registerPendingToolCall)({
609
- currentTurn,
610
- runId: 'run-1',
611
- update: { title: 'Read' },
612
- }), {
613
- key: 'sequence:2',
614
- title: 'Read',
615
- messageId: 'run-1-tool-2',
616
- sequence: 2,
617
- emitted: false,
618
- });
619
- strict_1.default.deepEqual(currentTurn.pendingToolCallOrder, ['call-a', 'sequence:2']);
620
- strict_1.default.equal(currentTurn.activeToolCallKey, 'sequence:2');
621
- strict_1.default.deepEqual((0, local_core_acp_progress_js_1.getToolCallsInOrder)(currentTurn).map((toolCall) => toolCall.key), ['call-a', 'sequence:2']);
622
- strict_1.default.equal((0, local_core_acp_progress_js_1.resolveFallbackToolCall)(currentTurn)?.key, 'sequence:2');
623
- strict_1.default.equal((0, local_core_acp_progress_js_1.resolveToolCallForUpdate)(currentTurn, { id: 'call-a' })?.key, 'call-a');
624
- strict_1.default.equal(currentTurn.activeToolCallKey, 'call-a');
625
- (0, local_core_acp_progress_js_1.syncLegacyPendingToolCall)(currentTurn, currentTurn.pendingToolCalls['call-a']);
626
- strict_1.default.equal(currentTurn.pendingToolCallTitle, 'Terminal');
627
- strict_1.default.equal(currentTurn.pendingToolCallId, 'run-1-tool-1');
628
- (0, local_core_acp_progress_js_1.deletePendingToolCall)(currentTurn, 'call-a');
629
- strict_1.default.deepEqual(currentTurn.pendingToolCallOrder, ['sequence:2']);
630
- strict_1.default.equal(currentTurn.activeToolCallKey, undefined);
631
- });
632
- (0, node_test_1.default)('ACP progress projection ignores empty plan entries', () => {
633
- strict_1.default.equal((0, local_core_acp_progress_js_1.formatPlanProgress)([
634
- { content: '检查消息流' },
635
- { content: ' ' },
636
- { content: '修复持久化' },
637
- ]), '检查消息流 | 修复持久化');
638
- strict_1.default.equal((0, local_core_acp_progress_js_1.formatPlanProgress)([{ content: '' }]), '');
639
- });
640
- (0, node_test_1.default)('ACP tool call running and completed updates share one message id', () => {
641
- const upserted = [];
642
- const emitted = [];
643
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
644
- appendMessage: () => strict_1.default.fail('tool updates should be upserted'),
645
- upsertMessage: (_threadId, id, _role, content, kind) => upserted.push({ id, content, kind }),
646
- emitBridge: (event) => emitted.push(event),
647
- updateRunStatus: () => { },
648
- sendRaw: () => true,
649
- });
650
- const session = {
651
- threadId: 'thread-1',
652
- bridgeSessionKey: 'session:thread-1',
653
- currentRunId: 'run-1',
654
- currentTurn: {
655
- runId: 'run-1',
656
- replyCtx: 'run-1',
657
- previewHandle: 'preview-1',
658
- assistantText: '',
659
- typingStarted: true,
660
- previewStarted: false,
661
- permission: null,
662
- },
663
- loadReplayMode: false,
664
- schedulerJobCreatedByRun: new Map(),
665
- };
666
- coordinator.handleAgentNotification(session, {
667
- method: 'session/update',
668
- params: {
669
- update: {
670
- sessionUpdate: 'tool_call',
671
- title: 'Find',
672
- },
673
- },
674
- });
675
- coordinator.handleAgentNotification(session, {
676
- method: 'session/update',
677
- params: {
678
- update: {
679
- sessionUpdate: 'tool_call_update',
680
- title: "Find `src/pages/Threads/**/*`",
681
- status: 'running',
682
- },
683
- },
684
- });
685
- coordinator.handleAgentNotification(session, {
686
- method: 'session/update',
687
- params: {
688
- update: {
689
- sessionUpdate: 'tool_call_update',
690
- title: 'Tool update',
691
- status: 'completed',
692
- content: [
693
- {
694
- type: 'content',
695
- content: {
696
- type: 'text',
697
- text: 'src/pages/Threads/ThreadChat.tsx',
698
- },
699
- },
700
- ],
701
- },
702
- },
703
- });
704
- strict_1.default.equal(upserted.length, 2);
705
- strict_1.default.equal(upserted[0]?.id, upserted[1]?.id);
706
- strict_1.default.equal(upserted[0]?.id, 'run-1-tool-1');
707
- strict_1.default.equal(upserted[0]?.content, "🔧 Find: Find `src/pages/Threads/**/*` - running");
708
- strict_1.default.equal(upserted[1]?.content, '🔧 Find: Find `src/pages/Threads/**/*` - completed - src/pages/Threads/ThreadChat.tsx');
709
- strict_1.default.equal(emitted.length, 2);
710
- strict_1.default.deepEqual(emitted.map((event) => event.messageId), ['run-1-tool-1', 'run-1-tool-1']);
711
- });
712
- (0, node_test_1.default)('ACP concurrent tool call updates are matched by call id', () => {
713
- const upserted = [];
714
- const emitted = [];
715
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
716
- appendMessage: () => strict_1.default.fail('tool updates should be upserted'),
717
- upsertMessage: (_threadId, id, _role, content, kind, toolCall) => upserted.push({ id, content, kind, toolCall }),
718
- emitBridge: (event) => emitted.push(event),
719
- updateRunStatus: () => { },
720
- sendRaw: () => true,
721
- });
722
- const session = {
723
- threadId: 'thread-1',
724
- bridgeSessionKey: 'session:thread-1',
725
- currentRunId: 'run-1',
726
- currentTurn: {
727
- runId: 'run-1',
728
- replyCtx: 'run-1',
729
- previewHandle: 'preview-1',
730
- assistantText: '',
731
- typingStarted: true,
732
- previewStarted: false,
733
- permission: null,
734
- },
735
- loadReplayMode: false,
736
- schedulerJobCreatedByRun: new Map(),
737
- };
738
- coordinator.handleAgentNotification(session, {
739
- method: 'session/update',
740
- params: {
741
- update: {
742
- sessionUpdate: 'tool_call',
743
- id: 'call-a',
744
- title: 'Terminal',
745
- rawInput: { command: 'npm test' },
746
- },
747
- },
748
- });
749
- coordinator.handleAgentNotification(session, {
750
- method: 'session/update',
751
- params: {
752
- update: {
753
- sessionUpdate: 'tool_call',
754
- id: 'call-b',
755
- title: 'Read',
756
- },
757
- },
758
- });
759
- coordinator.handleAgentNotification(session, {
760
- method: 'session/update',
761
- params: {
762
- update: {
763
- sessionUpdate: 'tool_call_update',
764
- id: 'call-b',
765
- title: 'Read package.json',
766
- status: 'running',
767
- },
768
- },
769
- });
770
- coordinator.handleAgentNotification(session, {
771
- method: 'session/update',
772
- params: {
773
- update: {
774
- sessionUpdate: 'tool_call_update',
775
- id: 'call-a',
776
- title: 'Tool update',
777
- status: 'completed',
778
- content: [
779
- {
780
- type: 'content',
781
- content: {
782
- type: 'text',
783
- text: 'terminal output',
784
- },
785
- },
786
- ],
787
- },
788
- },
789
- });
790
- strict_1.default.equal(upserted.length, 2);
791
- strict_1.default.equal(upserted[0]?.id, 'run-1-tool-2');
792
- strict_1.default.equal(upserted[0]?.content, '🔧 Read: Read package.json - running');
793
- strict_1.default.equal(upserted[1]?.id, 'run-1-tool-1');
794
- strict_1.default.equal(upserted[1]?.content, '🔧 Terminal: completed - terminal output');
795
- strict_1.default.deepEqual(emitted.map((event) => event.messageId), ['run-1-tool-2', 'run-1-tool-1']);
796
- strict_1.default.deepEqual(upserted[1]?.toolCall, {
797
- id: 'call-a',
798
- name: 'Terminal',
799
- status: 'completed',
800
- input: { command: 'npm test' },
801
- output: 'terminal output',
802
- detail: undefined,
803
- label: '工具结果',
804
- });
805
- strict_1.default.deepEqual(emitted[1]?.toolCall, upserted[1]?.toolCall);
806
- });
807
- (0, node_test_1.default)('ACP tool call update backfills rawInput from pi ACP updates', () => {
808
- const upserted = [];
809
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
810
- appendMessage: () => strict_1.default.fail('tool updates should be upserted'),
811
- upsertMessage: (_threadId, id, _role, content, _kind, toolCall) => upserted.push({ id, content, toolCall }),
812
- emitBridge: () => { },
813
- updateRunStatus: () => { },
814
- sendRaw: () => true,
815
- });
816
- const session = {
817
- threadId: 'thread-1',
818
- bridgeSessionKey: 'session:thread-1',
819
- currentRunId: 'run-1',
820
- currentTurn: {
821
- runId: 'run-1',
822
- replyCtx: 'run-1',
823
- previewHandle: 'preview-1',
824
- assistantText: '',
825
- typingStarted: true,
826
- previewStarted: false,
827
- permission: null,
828
- },
829
- loadReplayMode: false,
830
- schedulerJobCreatedByRun: new Map(),
831
- };
832
- coordinator.handleAgentNotification(session, {
833
- method: 'session/update',
834
- params: {
835
- update: {
836
- sessionUpdate: 'tool_call',
837
- toolCallId: 'call-a',
838
- title: 'bash',
839
- },
840
- },
841
- });
842
- coordinator.handleAgentNotification(session, {
843
- method: 'session/update',
844
- params: {
845
- update: {
846
- sessionUpdate: 'tool_call_update',
847
- toolCallId: 'call-a',
848
- status: 'completed',
849
- rawInput: { command: 'ls -la ~/Desktop' },
850
- content: [
851
- {
852
- type: 'content',
853
- content: {
854
- type: 'text',
855
- text: 'total 32',
856
- },
857
- },
858
- ],
859
- },
860
- },
861
- });
862
- strict_1.default.equal(upserted[0]?.toolCall?.name, 'bash');
863
- strict_1.default.deepEqual(upserted[0]?.toolCall?.input, { command: 'ls -la ~/Desktop' });
864
- });
865
- (0, node_test_1.default)('ACP permission tool parameters are preserved in completed tool cards', () => {
866
- const upserted = [];
867
- const emitted = [];
868
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
869
- appendMessage: () => { },
870
- upsertMessage: (_threadId, id, _role, content, kind) => upserted.push({ id, content, kind }),
871
- emitBridge: (event) => emitted.push(event),
872
- updateRunStatus: () => { },
873
- sendRaw: () => true,
874
- });
875
- const session = {
876
- threadId: 'thread-1',
877
- bridgeSessionKey: 'session:thread-1',
878
- currentRunId: 'run-1',
879
- currentTurn: {
880
- runId: 'run-1',
881
- replyCtx: 'run-1',
882
- previewHandle: 'preview-1',
883
- assistantText: '',
884
- typingStarted: true,
885
- previewStarted: false,
886
- permission: null,
887
- },
888
- pendingPermissionByRun: new Map(),
889
- loadReplayMode: false,
890
- schedulerJobCreatedByRun: new Map(),
891
- };
892
- coordinator.handleAgentNotification(session, {
893
- method: 'session/update',
894
- params: {
895
- update: {
896
- sessionUpdate: 'tool_call',
897
- title: 'Terminal',
898
- },
899
- },
900
- });
901
- coordinator.handleAgentRequest(session, {
902
- method: 'session/request_permission',
903
- id: 42,
904
- params: {
905
- toolCall: {
906
- title: 'Terminal',
907
- parameters: {
908
- command: 'ls -la ~/Desktop',
909
- cwd: '/Users/mochuxian',
910
- },
911
- },
912
- options: [
913
- { optionId: 'allow-once', name: 'Allow', kind: 'allow_once' },
914
- ],
915
- },
916
- });
917
- coordinator.handleAgentNotification(session, {
918
- method: 'session/update',
919
- params: {
920
- update: {
921
- sessionUpdate: 'tool_call_update',
922
- title: 'Tool update',
923
- status: 'completed',
924
- content: [
925
- {
926
- type: 'content',
927
- content: {
928
- type: 'text',
929
- text: 'Desktop file list',
930
- },
931
- },
932
- ],
933
- },
934
- },
935
- });
936
- strict_1.default.equal(upserted.length, 1);
937
- strict_1.default.equal(upserted[0]?.id, 'run-1-tool-1');
938
- strict_1.default.match(upserted[0]?.content || '', /Terminal/);
939
- strict_1.default.match(upserted[0]?.content || '', /parameters:/);
940
- strict_1.default.match(upserted[0]?.content || '', /ls -la ~\/Desktop/);
941
- strict_1.default.match(upserted[0]?.content || '', /completed - Desktop file list/);
942
- });
943
- (0, node_test_1.default)('ACP permission button rows preserve always allow actions with structured status', () => {
944
- const appended = [];
945
- const emitted = [];
946
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
947
- appendMessage: (_threadId, _role, content, kind, _toolCall, bridgeKind, bridgeStatus) => appended.push({ content, kind, bridgeKind, bridgeStatus }),
948
- emitBridge: (event) => emitted.push(event),
949
- updateRunStatus: () => { },
950
- sendRaw: () => true,
951
- });
952
- const session = {
953
- threadId: 'thread-1',
954
- bridgeSessionKey: 'session:thread-1',
955
- currentRunId: 'run-1',
956
- currentTurn: {
957
- runId: 'run-1',
958
- replyCtx: 'run-1',
959
- previewHandle: 'preview-1',
960
- assistantText: '',
961
- typingStarted: true,
962
- previewStarted: false,
963
- permission: null,
964
- },
965
- pendingPermissionByRun: new Map(),
966
- loadReplayMode: false,
967
- schedulerJobCreatedByRun: new Map(),
968
- };
969
- coordinator.handleAgentRequest(session, {
970
- method: 'session/request_permission',
971
- id: 42,
972
- params: {
973
- toolCall: {
974
- title: 'Terminal',
975
- parameters: {
976
- command: 'system_profiler SPHardwareDataType',
977
- },
978
- },
979
- options: [
980
- { optionId: 'approve-once', name: 'Allow once', kind: 'allow' },
981
- { optionId: 'approve-always', name: 'Always allow', kind: 'allow' },
982
- { optionId: 'reject', name: 'Reject', kind: 'reject' },
983
- ],
984
- },
985
- });
986
- strict_1.default.equal(appended[0]?.bridgeKind, 'permission');
987
- strict_1.default.equal(appended[0]?.bridgeStatus, 'awaiting_input');
988
- strict_1.default.equal(emitted[0]?.bridgeKind, 'permission');
989
- strict_1.default.equal(emitted[0]?.bridgeStatus, 'awaiting_input');
990
- strict_1.default.deepEqual(emitted[0]?.buttonRows, [[
991
- { text: 'allow', data: 'allow' },
992
- { text: 'allow all', data: 'allow all' },
993
- { text: 'deny', data: 'deny' },
994
- ]]);
995
- strict_1.default.equal(session.pendingPermissionByRun.get('run-1')?.options[1]?.optionId, 'approve-always');
996
- });
997
- (0, node_test_1.default)('Hermes ACP permission options respect allow_permanent false', () => {
998
- const emitted = [];
999
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1000
- appendMessage: () => { },
1001
- emitBridge: (event) => emitted.push(event),
1002
- updateRunStatus: () => { },
1003
- sendRaw: () => true,
1004
- });
1005
- const session = {
1006
- threadId: 'thread-1',
1007
- bridgeSessionKey: 'session:thread-1',
1008
- currentRunId: 'run-1',
1009
- currentTurn: {
1010
- runId: 'run-1',
1011
- replyCtx: 'run-1',
1012
- previewHandle: 'preview-1',
1013
- assistantText: '',
1014
- typingStarted: true,
1015
- previewStarted: false,
1016
- permission: null,
1017
- agentType: 'hermes',
1018
- },
1019
- pendingPermissionByRun: new Map(),
1020
- loadReplayMode: false,
1021
- schedulerJobCreatedByRun: new Map(),
1022
- };
1023
- coordinator.handleAgentRequest(session, {
1024
- method: 'session/request_permission',
1025
- id: 42,
1026
- params: {
1027
- allow_permanent: false,
1028
- toolCall: {
1029
- title: 'Terminal',
1030
- parameters: {
1031
- command: 'system_profiler SPHardwareDataType',
1032
- },
1033
- },
1034
- options: [
1035
- { optionId: 'approve-once', name: 'Allow once', kind: 'allow' },
1036
- { optionId: 'approve-always', name: 'Always allow', kind: 'allow' },
1037
- { optionId: 'reject', name: 'Reject', kind: 'reject' },
1038
- ],
1039
- },
1040
- });
1041
- strict_1.default.deepEqual(emitted[0]?.buttonRows, [[
1042
- { text: 'allow', data: 'allow' },
1043
- { text: 'deny', data: 'deny' },
1044
- ]]);
1045
- strict_1.default.deepEqual(session.pendingPermissionByRun.get('run-1')?.options.map((option) => option.optionId), ['approve-once', 'reject']);
1046
- });
1047
- (0, node_test_1.default)('ACP yolo mode auto-selects permission requests without rendering cards', () => {
1048
- const emitted = [];
1049
- const appended = [];
1050
- const rawPayloads = [];
1051
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1052
- appendMessage: (_threadId, _role, content) => appended.push(content),
1053
- emitBridge: (event) => emitted.push(event),
1054
- updateRunStatus: () => { },
1055
- getThreadAgentMode: () => 'bypassPermissions',
1056
- sendRaw: (_session, payload) => {
1057
- rawPayloads.push(payload);
1058
- return true;
1059
- },
1060
- });
1061
- const session = {
1062
- threadId: 'thread-1',
1063
- bridgeSessionKey: 'session:thread-1',
1064
- currentRunId: 'run-1',
1065
- currentTurn: {
1066
- runId: 'run-1',
1067
- replyCtx: 'run-1',
1068
- previewHandle: 'preview-1',
1069
- assistantText: '',
1070
- typingStarted: true,
1071
- previewStarted: false,
1072
- permission: null,
1073
- },
1074
- pendingPermissionByRun: new Map(),
1075
- loadReplayMode: false,
1076
- schedulerJobCreatedByRun: new Map(),
1077
- };
1078
- coordinator.handleAgentRequest(session, {
1079
- method: 'session/request_permission',
1080
- id: 42,
1081
- params: {
1082
- toolCall: {
1083
- title: 'Terminal',
1084
- parameters: {
1085
- command: 'system_profiler SPHardwareDataType',
1086
- },
1087
- },
1088
- options: [
1089
- { optionId: 'approve-once', name: 'Allow once', kind: 'allow' },
1090
- { optionId: 'approve-always', name: 'Always allow', kind: 'allow_all' },
1091
- { optionId: 'reject', name: 'Reject', kind: 'reject' },
1092
- ],
1093
- },
1094
- });
1095
- strict_1.default.equal(rawPayloads[0]?.id, 42);
1096
- strict_1.default.equal(rawPayloads[0]?.result?.outcome?.outcome, 'selected');
1097
- strict_1.default.equal(rawPayloads[0]?.result?.outcome?.optionId, 'approve-always');
1098
- strict_1.default.equal(session.pendingPermissionByRun.size, 0);
1099
- strict_1.default.deepEqual(appended, []);
1100
- strict_1.default.deepEqual(emitted, []);
1101
- });
1102
- (0, node_test_1.default)('ACP bare tool call is flushed before assistant text', () => {
1103
- const appended = [];
1104
- const emitted = [];
1105
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1106
- appendMessage: (_threadId, _role, content) => appended.push(content),
1107
- emitBridge: (event) => emitted.push(event),
1108
- updateRunStatus: () => { },
1109
- sendRaw: () => true,
1110
- });
1111
- const session = {
1112
- threadId: 'thread-1',
1113
- bridgeSessionKey: 'session:thread-1',
1114
- currentRunId: 'run-1',
1115
- currentTurn: {
1116
- runId: 'run-1',
1117
- replyCtx: 'run-1',
1118
- previewHandle: 'preview-1',
1119
- assistantText: '',
1120
- typingStarted: true,
1121
- previewStarted: false,
1122
- permission: null,
1123
- },
1124
- loadReplayMode: false,
1125
- schedulerJobCreatedByRun: new Map(),
1126
- };
1127
- coordinator.handleAgentNotification(session, {
1128
- method: 'session/update',
1129
- params: {
1130
- update: {
1131
- sessionUpdate: 'tool_call',
1132
- title: 'Terminal',
1133
- },
1134
- },
1135
- });
1136
- coordinator.handleAgentNotification(session, {
1137
- method: 'session/update',
1138
- params: {
1139
- update: {
1140
- sessionUpdate: 'agent_message_chunk',
1141
- content: { type: 'text', text: 'done' },
1142
- },
1143
- },
1144
- });
1145
- strict_1.default.deepEqual(appended, ['🔧 Terminal']);
1146
- strict_1.default.equal(emitted[0]?.content, '🔧 Terminal');
1147
- strict_1.default.equal(session.currentTurn.assistantText, 'done');
1148
- });
1149
- (0, node_test_1.default)('ACP closes assistant message segments at event boundaries without folding them into final', () => {
1150
- const upserted = [];
1151
- const emitted = [];
1152
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1153
- appendMessage: () => { },
1154
- upsertMessage: (_threadId, id, _role, content, _kind, _toolCall, bridgeKind) => {
1155
- upserted.push({ id, content, bridgeKind });
1156
- },
1157
- emitBridge: (event) => emitted.push(event),
1158
- updateRunStatus: () => { },
1159
- sendRaw: () => true,
1160
- });
1161
- const session = {
1162
- threadId: 'thread-1',
1163
- bridgeSessionKey: 'session:thread-1',
1164
- currentRunId: 'run-1',
1165
- currentTurn: {
1166
- runId: 'run-1',
1167
- replyCtx: 'run-1',
1168
- previewHandle: 'run-1-assistant-preview-1',
1169
- assistantMessageId: 'run-1-assistant-1',
1170
- assistantSequence: 1,
1171
- thoughtPreviewHandle: 'thought-preview-1',
1172
- thoughtMessageId: 'run-1-thought-1',
1173
- assistantText: '',
1174
- rawAssistantText: '',
1175
- thoughtText: '',
1176
- typingStarted: true,
1177
- previewStarted: false,
1178
- thoughtPreviewStarted: false,
1179
- pendingToolCalls: {},
1180
- pendingToolCallOrder: [],
1181
- toolCallSequence: 0,
1182
- toolObservations: [],
1183
- permission: null,
1184
- },
1185
- loadReplayMode: false,
1186
- schedulerJobCreatedByRun: new Map(),
1187
- };
1188
- coordinator.handleAgentNotification(session, {
1189
- method: 'session/update',
1190
- params: {
1191
- update: {
1192
- sessionUpdate: 'agent_message_chunk',
1193
- content: { type: 'text', text: '我先检查一下' },
1194
- },
1195
- },
1196
- });
1197
- strict_1.default.deepEqual(emitted, []);
1198
- strict_1.default.deepEqual(upserted, []);
1199
- coordinator.handleAgentNotification(session, {
1200
- method: 'session/update',
1201
- params: {
1202
- update: {
1203
- sessionUpdate: 'tool_call',
1204
- title: 'Terminal',
1205
- },
1206
- },
1207
- });
1208
- strict_1.default.deepEqual(upserted, [{
1209
- id: 'run-1-assistant-1',
1210
- content: '我先检查一下',
1211
- bridgeKind: 'assistant',
1212
- }]);
1213
- strict_1.default.equal(emitted.length, 1);
1214
- strict_1.default.equal(emitted[0]?.type, 'reply');
1215
- strict_1.default.equal(emitted[0]?.content, '我先检查一下');
1216
- strict_1.default.equal(emitted[0]?.messageId, 'run-1-assistant-1');
1217
- strict_1.default.equal(emitted[0]?.bridgeKind, 'assistant');
1218
- strict_1.default.equal(session.currentTurn.assistantText, '');
1219
- strict_1.default.equal(session.currentTurn.rawAssistantText, '');
1220
- coordinator.handleAgentNotification(session, {
1221
- method: 'session/update',
1222
- params: {
1223
- update: {
1224
- sessionUpdate: 'agent_message_chunk',
1225
- content: { type: 'text', text: '最终回答' },
1226
- },
1227
- },
1228
- });
1229
- strict_1.default.equal(session.currentTurn.assistantText, '最终回答');
1230
- strict_1.default.equal(session.currentTurn.rawAssistantText, '最终回答');
1231
- strict_1.default.equal(upserted.filter((entry) => entry.bridgeKind === 'assistant').length, 1);
1232
- strict_1.default.equal(emitted.length, 2);
1233
- strict_1.default.equal(emitted[1]?.bridgeKind, 'tool');
1234
- });
1235
- (0, node_test_1.default)('ACP tool-scoped assistant chunks stay out of final assistant buffer', () => {
1236
- const appended = [];
1237
- const emitted = [];
1238
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1239
- appendMessage: (_threadId, _role, content, _kind, _toolCall, bridgeKind) => appended.push({ content, bridgeKind }),
1240
- emitBridge: (event) => emitted.push(event),
1241
- updateRunStatus: () => { },
1242
- sendRaw: () => true,
1243
- });
1244
- const session = {
1245
- threadId: 'thread-1',
1246
- bridgeSessionKey: 'session:thread-1',
1247
- currentRunId: 'run-1',
1248
- currentTurn: {
1249
- runId: 'run-1',
1250
- replyCtx: 'run-1',
1251
- previewHandle: 'preview-1',
1252
- assistantText: '',
1253
- typingStarted: true,
1254
- previewStarted: false,
1255
- permission: null,
1256
- },
1257
- loadReplayMode: false,
1258
- schedulerJobCreatedByRun: new Map(),
1259
- };
1260
- coordinator.handleAgentNotification(session, {
1261
- method: 'session/update',
1262
- params: {
1263
- update: {
1264
- sessionUpdate: 'agent_message_chunk',
1265
- _meta: {
1266
- claudeCode: {
1267
- parentToolUseId: 'tool-1',
1268
- },
1269
- },
1270
- content: {
1271
- type: 'text',
1272
- text: 'tool scoped transcript',
1273
- },
1274
- },
1275
- },
1276
- });
1277
- coordinator.handleAgentNotification(session, {
1278
- method: 'session/update',
1279
- params: {
1280
- update: {
1281
- sessionUpdate: 'agent_message_chunk',
1282
- content: { type: 'text', text: 'final answer' },
1283
- },
1284
- },
1285
- });
1286
- strict_1.default.deepEqual(appended, [{ content: 'tool scoped transcript', bridgeKind: 'tool' }]);
1287
- strict_1.default.equal(emitted[0]?.bridgeKind, 'tool');
1288
- strict_1.default.equal(emitted[0]?.content, 'tool scoped transcript');
1289
- strict_1.default.equal(session.currentTurn.assistantText, 'final answer');
1290
- });
1291
- (0, node_test_1.default)('Hermes ACP assistant chunks strip restored history replay inside its own behavior', () => {
1292
- const emitted = [];
1293
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1294
- appendMessage: () => { },
1295
- emitBridge: (event) => emitted.push(event),
1296
- updateRunStatus: () => { },
1297
- sendRaw: () => true,
1298
- });
1299
- const session = {
1300
- threadId: 'thread-1',
1301
- bridgeSessionKey: 'session:thread-1',
1302
- currentRunId: 'run-1',
1303
- currentTurn: {
1304
- runId: 'run-1',
1305
- replyCtx: 'run-1',
1306
- previewHandle: 'preview-1',
1307
- agentType: 'hermes',
1308
- assistantText: '',
1309
- rawAssistantText: '',
1310
- priorAssistantFinalMessages: ['Hi! 😊 How can I help you today?', '我是 Hermes Agent。'],
1311
- typingStarted: true,
1312
- previewStarted: false,
1313
- permission: null,
1314
- },
1315
- loadReplayMode: false,
1316
- schedulerJobCreatedByRun: new Map(),
1317
- };
1318
- for (const text of ['Hi! 😊 How can I help you today?', '我是 Hermes Agent。', '确认删除前需要你确认。']) {
1319
- coordinator.handleAgentNotification(session, {
1320
- method: 'session/update',
1321
- params: {
1322
- update: {
1323
- sessionUpdate: 'agent_message_chunk',
1324
- content: { type: 'text', text },
1325
- },
1326
- },
1327
- });
1328
- }
1329
- strict_1.default.equal(session.currentTurn.assistantText, '确认删除前需要你确认。');
1330
- strict_1.default.deepEqual(emitted.map((event) => event.content), []);
1331
- });
1332
- (0, node_test_1.default)('Hermes ACP assistant chunks strip replay even when stored prior final is already polluted', () => {
1333
- const emitted = [];
1334
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1335
- appendMessage: () => { },
1336
- emitBridge: (event) => emitted.push(event),
1337
- updateRunStatus: () => { },
1338
- sendRaw: () => true,
1339
- });
1340
- const previousCleanFinal = '你确定要删除 **Sisyphus_介绍.txt** 这个文件吗?删除后无法恢复。确认的话我马上执行。';
1341
- const pollutedStoredFinal = [
1342
- 'Hi! How can I help you today?',
1343
- '我是 Hermes Agent,你的 AI 助手。',
1344
- '好的,让我看看你的桌面文件。',
1345
- previousCleanFinal,
1346
- ].join('');
1347
- const session = {
1348
- threadId: 'thread-1',
1349
- bridgeSessionKey: 'session:thread-1',
1350
- currentRunId: 'run-1',
1351
- currentTurn: {
1352
- runId: 'run-1',
1353
- replyCtx: 'run-1',
1354
- previewHandle: 'preview-1',
1355
- agentType: 'hermes',
1356
- assistantText: '',
1357
- rawAssistantText: '',
1358
- priorAssistantFinalMessages: [pollutedStoredFinal],
1359
- typingStarted: true,
1360
- previewStarted: false,
1361
- permission: null,
1362
- },
1363
- loadReplayMode: false,
1364
- schedulerJobCreatedByRun: new Map(),
1365
- };
1366
- for (const text of [
1367
- 'Hi! How can I help you today?',
1368
- '我是 Hermes Agent,你的 AI 助手。',
1369
- '好的,让我看看你的桌面文件。',
1370
- previousCleanFinal,
1371
- '已删除 **Sisyphus_介绍.txt**,现在 Text 文件夹是空的了。',
1372
- ]) {
1373
- coordinator.handleAgentNotification(session, {
1374
- method: 'session/update',
1375
- params: {
1376
- update: {
1377
- sessionUpdate: 'agent_message_chunk',
1378
- content: { type: 'text', text },
1379
- },
1380
- },
1381
- });
1382
- }
1383
- strict_1.default.equal(session.currentTurn.assistantText, '已删除 **Sisyphus_介绍.txt**,现在 Text 文件夹是空的了。');
1384
- strict_1.default.deepEqual(emitted.map((event) => event.content), []);
1385
- });
1386
- (0, node_test_1.default)('Hermes ACP assistant chunks keep a fresh answer when no replay anchor is present', () => {
1387
- const emitted = [];
1388
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1389
- appendMessage: () => { },
1390
- emitBridge: (event) => emitted.push(event),
1391
- updateRunStatus: () => { },
1392
- sendRaw: () => true,
1393
- });
1394
- const session = {
1395
- threadId: 'thread-1',
1396
- bridgeSessionKey: 'session:thread-1',
1397
- currentRunId: 'run-1',
1398
- currentTurn: {
1399
- runId: 'run-1',
1400
- replyCtx: 'run-1',
1401
- previewHandle: 'preview-1',
1402
- agentType: 'hermes',
1403
- assistantText: '',
1404
- rawAssistantText: '',
1405
- priorAssistantFinalMessages: ['上一轮已经污染的历史,但是它不会出现在这次新回复里。'],
1406
- typingStarted: true,
1407
- previewStarted: false,
1408
- permission: null,
1409
- },
1410
- loadReplayMode: false,
1411
- schedulerJobCreatedByRun: new Map(),
1412
- };
1413
- coordinator.handleAgentNotification(session, {
1414
- method: 'session/update',
1415
- params: {
1416
- update: {
1417
- sessionUpdate: 'agent_message_chunk',
1418
- content: { type: 'text', text: '已删除文件。' },
1419
- },
1420
- },
1421
- });
1422
- strict_1.default.equal(session.currentTurn.assistantText, '已删除文件。');
1423
- strict_1.default.deepEqual(emitted.map((event) => event.content), []);
1424
- });
1425
- (0, node_test_1.default)('Hermes ACP progress updates suppress restored tool and thought replay only', () => {
1426
- const upserted = [];
1427
- const emitted = [];
1428
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1429
- appendMessage: () => strict_1.default.fail('tool updates should be upserted'),
1430
- upsertMessage: (_threadId, id, _role, content, _kind, _toolCall, bridgeKind) => {
1431
- upserted.push({ id, content, bridgeKind });
1432
- },
1433
- emitBridge: (event) => emitted.push(event),
1434
- updateRunStatus: () => { },
1435
- sendRaw: () => true,
1436
- });
1437
- const session = {
1438
- threadId: 'thread-1',
1439
- bridgeSessionKey: 'session:thread-1',
1440
- currentRunId: 'run-1',
1441
- currentTurn: {
1442
- runId: 'run-1',
1443
- replyCtx: 'run-1',
1444
- previewHandle: 'preview-1',
1445
- thoughtPreviewHandle: 'thought-preview-1',
1446
- thoughtMessageId: 'run-1-thought-1',
1447
- agentType: 'hermes',
1448
- assistantText: '',
1449
- thoughtText: '',
1450
- thoughtSequence: 1,
1451
- typingStarted: true,
1452
- previewStarted: false,
1453
- thoughtPreviewStarted: false,
1454
- priorAssistantProgressMessages: [
1455
- {
1456
- kind: 'tool',
1457
- content: '🔧 terminal: rm ~/Desktop/Text/Sisyphus_介绍.txt: completed - terminal result\n- **exit_code:** 0',
1458
- },
1459
- {
1460
- kind: 'thought',
1461
- content: 'The user confirmed they want to delete the file. Let me delete it.',
1462
- },
1463
- ],
1464
- permission: null,
1465
- },
1466
- loadReplayMode: false,
1467
- schedulerJobCreatedByRun: new Map(),
1468
- };
1469
- coordinator.handleAgentNotification(session, {
1470
- method: 'session/update',
1471
- params: {
1472
- update: {
1473
- sessionUpdate: 'tool_call',
1474
- id: 'old-tool',
1475
- title: 'terminal',
1476
- rawInput: { command: 'rm ~/Desktop/Text/Sisyphus_介绍.txt' },
1477
- },
1478
- },
1479
- });
1480
- coordinator.handleAgentNotification(session, {
1481
- method: 'session/update',
1482
- params: {
1483
- update: {
1484
- sessionUpdate: 'tool_call_update',
1485
- id: 'old-tool',
1486
- title: 'tool update',
1487
- status: 'completed',
1488
- content: [
1489
- {
1490
- type: 'content',
1491
- content: {
1492
- type: 'text',
1493
- text: 'terminal result\n- **exit_code:** 0',
1494
- },
1495
- },
1496
- ],
1497
- },
1498
- },
1499
- });
1500
- coordinator.handleAgentNotification(session, {
1501
- method: 'session/update',
1502
- params: {
1503
- update: {
1504
- sessionUpdate: 'agent_thought_chunk',
1505
- content: {
1506
- type: 'text',
1507
- text: 'The user confirmed they want to delete the file. Let me delete it.',
1508
- },
1509
- },
1510
- },
1511
- });
1512
- coordinator.handleAgentNotification(session, {
1513
- method: 'session/update',
1514
- params: {
1515
- update: {
1516
- sessionUpdate: 'tool_call',
1517
- id: 'new-tool',
1518
- title: 'terminal',
1519
- rawInput: { command: 'ls -la ~/Desktop/PDF/' },
1520
- },
1521
- },
1522
- });
1523
- coordinator.handleAgentNotification(session, {
1524
- method: 'session/update',
1525
- params: {
1526
- update: {
1527
- sessionUpdate: 'tool_call_update',
1528
- id: 'new-tool',
1529
- title: 'tool update',
1530
- status: 'completed',
1531
- content: [
1532
- {
1533
- type: 'content',
1534
- content: {
1535
- type: 'text',
1536
- text: 'terminal result\n- **output:** pdf files\n- **exit_code:** 0',
1537
- },
1538
- },
1539
- ],
1540
- },
1541
- },
1542
- });
1543
- coordinator.handleAgentNotification(session, {
1544
- method: 'session/update',
1545
- params: {
1546
- update: {
1547
- sessionUpdate: 'agent_thought_chunk',
1548
- content: {
1549
- type: 'text',
1550
- text: 'Let me present the PDF files to the user.',
1551
- },
1552
- },
1553
- },
1554
- });
1555
- strict_1.default.deepEqual(upserted.map((entry) => entry.content), [
1556
- '🔧 terminal: completed - terminal result\n- **output:** pdf files\n- **exit_code:** 0',
1557
- 'Let me present the PDF files to the user.',
1558
- ]);
1559
- strict_1.default.deepEqual(emitted.map((event) => event.content), [
1560
- '🔧 terminal: completed - terminal result\n- **output:** pdf files\n- **exit_code:** 0',
1561
- 'Let me present the PDF files to the user.',
1562
- ]);
1563
- });
1564
- (0, node_test_1.default)('Hermes ACP progress updates suppress restored plan and tool-scoped assistant chunks', () => {
1565
- const appended = [];
1566
- const emitted = [];
1567
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1568
- appendMessage: (_threadId, _role, content, _kind, _toolCall, bridgeKind) => appended.push({ content, bridgeKind }),
1569
- emitBridge: (event) => emitted.push(event),
1570
- updateRunStatus: () => { },
1571
- sendRaw: () => true,
1572
- });
1573
- const session = {
1574
- threadId: 'thread-1',
1575
- bridgeSessionKey: 'session:thread-1',
1576
- currentRunId: 'run-1',
1577
- currentTurn: {
1578
- runId: 'run-1',
1579
- replyCtx: 'run-1',
1580
- previewHandle: 'preview-1',
1581
- agentType: 'hermes',
1582
- assistantText: '',
1583
- priorAssistantProgressMessages: [
1584
- { kind: 'plan', content: '旧计划 | 已完成' },
1585
- { kind: 'tool', content: 'raw restored command output' },
1586
- ],
1587
- typingStarted: true,
1588
- previewStarted: false,
1589
- permission: null,
1590
- },
1591
- loadReplayMode: false,
1592
- schedulerJobCreatedByRun: new Map(),
1593
- pendingRawAssistantProgressChunks: ['raw restored command output', 'fresh command output'],
1594
- };
1595
- coordinator.handleAgentNotification(session, {
1596
- method: 'session/update',
1597
- params: {
1598
- update: {
1599
- sessionUpdate: 'plan',
1600
- entries: [{ content: '旧计划' }, { content: '已完成' }],
1601
- },
1602
- },
1603
- });
1604
- coordinator.handleAgentNotification(session, {
1605
- method: 'session/update',
1606
- params: {
1607
- update: {
1608
- sessionUpdate: 'agent_message_chunk',
1609
- content: { type: 'text', text: 'raw restored command output' },
1610
- },
1611
- },
1612
- });
1613
- coordinator.handleAgentNotification(session, {
1614
- method: 'session/update',
1615
- params: {
1616
- update: {
1617
- sessionUpdate: 'plan',
1618
- entries: [{ content: '新计划' }, { content: '进行中' }],
1619
- },
1620
- },
1621
- });
1622
- coordinator.handleAgentNotification(session, {
1623
- method: 'session/update',
1624
- params: {
1625
- update: {
1626
- sessionUpdate: 'agent_message_chunk',
1627
- content: { type: 'text', text: 'fresh command output' },
1628
- },
1629
- },
1630
- });
1631
- strict_1.default.deepEqual(appended, [
1632
- { content: '新计划 | 进行中', bridgeKind: 'plan' },
1633
- { content: 'fresh command output', bridgeKind: 'tool' },
1634
- ]);
1635
- strict_1.default.deepEqual(emitted.map((event) => ({ content: event.content, bridgeKind: event.bridgeKind })), [
1636
- { content: '新计划 | 进行中', bridgeKind: 'plan' },
1637
- { content: 'fresh command output', bridgeKind: 'tool' },
1638
- ]);
1639
- });
1640
- (0, node_test_1.default)('ACP raw local command output assistant chunks stay out of final assistant buffer', () => {
1641
- const appended = [];
1642
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1643
- appendMessage: (_threadId, _role, content, _kind, _toolCall, bridgeKind) => appended.push({ content, bridgeKind }),
1644
- emitBridge: () => { },
1645
- updateRunStatus: () => { },
1646
- sendRaw: () => true,
1647
- });
1648
- const session = {
1649
- threadId: 'thread-1',
1650
- bridgeSessionKey: 'session:thread-1',
1651
- currentRunId: 'run-1',
1652
- currentTurn: {
1653
- runId: 'run-1',
1654
- replyCtx: 'run-1',
1655
- previewHandle: 'preview-1',
1656
- assistantText: '',
1657
- typingStarted: true,
1658
- previewStarted: false,
1659
- permission: null,
1660
- },
1661
- loadReplayMode: false,
1662
- schedulerJobCreatedByRun: new Map(),
1663
- pendingRawAssistantProgressChunks: [],
1664
- };
1665
- coordinator.handleAgentNotification(session, {
1666
- method: '_claude/sdkMessage',
1667
- params: {
1668
- message: {
1669
- type: 'system',
1670
- subtype: 'local_command_output',
1671
- content: 'raw command output',
1672
- },
1673
- },
1674
- });
1675
- coordinator.handleAgentNotification(session, {
1676
- method: 'session/update',
1677
- params: {
1678
- update: {
1679
- sessionUpdate: 'agent_message_chunk',
1680
- content: { type: 'text', text: 'raw command output' },
1681
- },
1682
- },
1683
- });
1684
- coordinator.handleAgentNotification(session, {
1685
- method: 'session/update',
1686
- params: {
1687
- update: {
1688
- sessionUpdate: 'agent_message_chunk',
1689
- content: { type: 'text', text: 'final answer' },
1690
- },
1691
- },
1692
- });
1693
- strict_1.default.deepEqual(appended, [{ content: 'raw command output', bridgeKind: 'tool' }]);
1694
- strict_1.default.equal(session.currentTurn.assistantText, 'final answer');
1695
- });
1696
- (0, node_test_1.default)('ACP final text strips observed provider tool transcript prefix', () => {
1697
- const currentTurn = {
1698
- runId: 'run-1',
1699
- replyCtx: 'run-1',
1700
- previewHandle: 'preview-1',
1701
- assistantText: '',
1702
- thoughtText: '',
1703
- typingStarted: true,
1704
- previewStarted: false,
1705
- thoughtPreviewStarted: false,
1706
- permission: null,
1707
- toolObservations: [],
1708
- };
1709
- (0, local_core_acp_progress_js_1.recordToolObservation)(currentTurn, {
1710
- name: 'webReader',
1711
- title: 'webReader',
1712
- input: {
1713
- url: 'https://github.com/Thysrael/Horizon',
1714
- return_format: 'markdown',
1715
- retain_images: false,
1716
- },
1717
- status: 'completed',
1718
- outputText: JSON.stringify([{
1719
- title: 'GitHub - Thysrael/Horizon: Your own AI-powered news radar',
1720
- description: 'Your own AI-powered news radar. Generates daily briefings in English & Chinese.',
1721
- url: 'https://github.com/Thysrael/Horizon',
1722
- }]),
1723
- });
1724
- const polluted = [
1725
- '**🌐 Z.ai Built-in Tool: webReader**',
1726
- '',
1727
- '**Input:**',
1728
- '```json',
1729
- '{"url":"https://github.com/Thysrael/Horizon","return_format":"markdown","retain_images":false}',
1730
- '```',
1731
- '',
1732
- '*Executing on server...*',
1733
- ' **Output:**',
1734
- '**webReader_result_summary:** [{"text": {"title": "GitHub - Thysrael/Horizon: Your own AI-powered news radar", "description": "Your own AI-powered news radar. Generates daily briefings in English & Chinese.", "url": "https...',
1735
- ' 已放入 `00-Inbox/Horizon.md`。这个项目和咱们的 AI 早报 skill 功能高度重叠,架构可以作为参考。',
1736
- ].join('\n');
1737
- strict_1.default.equal((0, local_core_acp_progress_js_1.stripObservedToolTranscriptsFromAssistantText)(polluted, currentTurn.toolObservations), '已放入 `00-Inbox/Horizon.md`。这个项目和咱们的 AI 早报 skill 功能高度重叠,架构可以作为参考。');
1738
- strict_1.default.equal((0, local_core_acp_progress_js_1.stripObservedToolTranscriptsFromAssistantText)(polluted, []), polluted.trim());
1739
- });
1740
- (0, node_test_1.default)('ACP final text keeps normal answers that mention observed tool evidence', () => {
1741
- const observations = [{
1742
- name: 'webReader',
1743
- input: { url: 'https://github.com/Thysrael/Horizon' },
1744
- outputText: JSON.stringify([{
1745
- title: 'GitHub - Thysrael/Horizon: Your own AI-powered news radar',
1746
- url: 'https://github.com/Thysrael/Horizon',
1747
- }]),
1748
- }];
1749
- const answer = [
1750
- '我看了 https://github.com/Thysrael/Horizon,它是一个 AI 新闻雷达项目。',
1751
- '结论:可以作为早报 skill 的参考,但不需要直接照搬。',
1752
- ].join('\n');
1753
- strict_1.default.equal((0, local_core_acp_progress_js_1.stripObservedToolTranscriptsFromAssistantText)(answer, observations), answer);
1754
- });
1755
- (0, node_test_1.default)('ACP plan updates are persisted and emitted as thinking progress', () => {
1756
- const appended = [];
1757
- const emitted = [];
1758
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1759
- appendMessage: (_threadId, _role, content, kind) => appended.push({ content, kind }),
1760
- emitBridge: (event) => emitted.push(event),
1761
- updateRunStatus: () => { },
1762
- sendRaw: () => true,
1763
- });
1764
- const session = {
1765
- threadId: 'thread-1',
1766
- bridgeSessionKey: 'session:thread-1',
1767
- currentRunId: 'run-1',
1768
- currentTurn: {
1769
- runId: 'run-1',
1770
- replyCtx: 'run-1',
1771
- previewHandle: 'preview-1',
1772
- assistantText: '',
1773
- typingStarted: true,
1774
- previewStarted: false,
1775
- permission: null,
1776
- },
1777
- loadReplayMode: false,
1778
- schedulerJobCreatedByRun: new Map(),
1779
- };
1780
- coordinator.handleAgentNotification(session, {
1781
- method: 'session/update',
1782
- params: {
1783
- update: {
1784
- sessionUpdate: 'plan',
1785
- entries: [
1786
- { content: '检查消息流' },
1787
- { content: '修复持久化' },
1788
- ],
1789
- },
1790
- },
1791
- });
1792
- strict_1.default.deepEqual(appended, [
1793
- {
1794
- content: '检查消息流 | 修复持久化',
1795
- kind: 'progress',
1796
- },
1797
- ]);
1798
- strict_1.default.equal(emitted.length, 1);
1799
- strict_1.default.equal(emitted[0]?.type, 'reply');
1800
- strict_1.default.equal(emitted[0]?.bridgeKind, 'plan');
1801
- strict_1.default.equal(emitted[0]?.content, appended[0]?.content);
1802
- });
1803
- (0, node_test_1.default)('ACP thought chunks stream for web/app and start a fresh segment at tool boundaries', () => {
1804
- const appended = [];
1805
- const upserted = [];
1806
- const emitted = [];
1807
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
1808
- appendMessage: (_threadId, _role, content, kind) => appended.push({ content, kind }),
1809
- upsertMessage: (_threadId, id, _role, content, kind) => upserted.push({ id, content, kind }),
1810
- emitBridge: (event) => emitted.push(event),
1811
- updateRunStatus: () => { },
1812
- sendRaw: () => true,
1813
- });
1814
- const session = {
1815
- threadId: 'thread-1',
1816
- bridgeSessionKey: 'session:thread-1',
1817
- currentRunId: 'run-1',
1818
- currentTurn: {
1819
- runId: 'run-1',
1820
- replyCtx: 'run-1',
1821
- previewHandle: 'preview-1',
1822
- thoughtPreviewHandle: 'thought-preview-1',
1823
- thoughtMessageId: 'run-1-thought-1',
1824
- assistantText: '',
1825
- thoughtText: '',
1826
- thoughtSequence: 1,
1827
- typingStarted: true,
1828
- previewStarted: false,
1829
- thoughtPreviewStarted: false,
1830
- permission: null,
1831
- },
1832
- loadReplayMode: false,
1833
- schedulerJobCreatedByRun: new Map(),
1834
- };
1835
- coordinator.handleAgentNotification(session, {
1836
- method: 'session/update',
1837
- params: {
1838
- update: {
1839
- sessionUpdate: 'agent_thought_chunk',
1840
- content: { type: 'text', text: '先理解问题' },
1841
- },
1842
- },
1843
- });
1844
- strict_1.default.deepEqual(appended, []);
1845
- strict_1.default.deepEqual(upserted, [
1846
- {
1847
- id: 'run-1-thought-1',
1848
- content: '先理解问题',
1849
- kind: 'progress',
1850
- },
1851
- ]);
1852
- strict_1.default.deepEqual(emitted.map((event) => event.type), ['preview_start']);
1853
- strict_1.default.equal(emitted[0]?.content, '先理解问题');
1854
- coordinator.handleAgentNotification(session, {
1855
- method: 'session/update',
1856
- params: {
1857
- update: {
1858
- sessionUpdate: 'agent_thought_chunk',
1859
- content: { type: 'text', text: ',再检查代码' },
1860
- },
1861
- },
1862
- });
1863
- strict_1.default.deepEqual(appended, []);
1864
- strict_1.default.deepEqual(upserted.map((entry) => entry.content), ['先理解问题', '先理解问题,再检查代码']);
1865
- strict_1.default.deepEqual(emitted.map((event) => event.type), ['preview_start', 'update_message']);
1866
- strict_1.default.equal(emitted[1]?.content, '先理解问题,再检查代码');
1867
- coordinator.handleAgentNotification(session, {
1868
- method: 'session/update',
1869
- params: {
1870
- update: {
1871
- sessionUpdate: 'tool_call',
1872
- title: 'webReader',
1873
- },
1874
- },
1875
- });
1876
- strict_1.default.deepEqual(appended, []);
1877
- strict_1.default.equal(session.currentTurn.thoughtText, '');
1878
- strict_1.default.equal(session.currentTurn.thoughtMessageId, 'run-1-thought-2');
1879
- strict_1.default.equal(session.currentTurn.thoughtPreviewHandle, 'run-1-thought-preview-2');
1880
- });
1881
- (0, node_test_1.default)('ACP store preserves structured progress metadata', () => {
1882
- const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentdock-thought-store-'));
1883
- const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
1884
- try {
1885
- const thread = store.createThread('project-1', 'Thread');
1886
- store.upsertMessage(thread.id, 'run-1-thought', 'assistant', '先理解问题', 'progress', undefined, 'thought');
1887
- store.upsertMessage(thread.id, 'run-1-thought', 'assistant', '先理解问题,再检查代码', 'progress', undefined, 'thought');
1888
- store.appendMessage(thread.id, 'assistant', '等待确认', 'progress', undefined, 'permission', 'awaiting_input');
1889
- const detail = store.getThread(thread.id, []);
1890
- strict_1.default.equal(detail.messages.length, 2);
1891
- strict_1.default.equal(detail.messages[0]?.id, 'run-1-thought');
1892
- strict_1.default.equal(detail.messages[0]?.kind, 'progress');
1893
- strict_1.default.equal(detail.messages[0]?.bridgeKind, 'thought');
1894
- strict_1.default.equal(detail.messages[0]?.content, '先理解问题,再检查代码');
1895
- strict_1.default.equal(detail.messages[1]?.bridgeKind, 'permission');
1896
- strict_1.default.equal(detail.messages[1]?.bridgeStatus, 'awaiting_input');
1897
- strict_1.default.equal(detail.historyCount, 2);
1898
- }
1899
- finally {
1900
- store.close();
1901
- (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
1902
- }
1903
- });
1904
- (0, node_test_1.default)('ACP store preserves structured tool call progress metadata', () => {
1905
- const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentdock-tool-store-'));
1906
- const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
1907
- try {
1908
- const thread = store.createThread('project-1', 'Thread');
1909
- store.upsertMessage(thread.id, 'run-1-tool-1', 'assistant', '🔧 bash: completed - total 32', 'progress', {
1910
- id: 'call-1',
1911
- name: 'bash',
1912
- status: 'completed',
1913
- input: { command: 'ls -la ~/Desktop' },
1914
- detail: 'ls -la ~/Desktop',
1915
- output: 'total 32',
1916
- label: '工具结果',
1917
- });
1918
- const detail = store.getThread(thread.id, []);
1919
- strict_1.default.equal(detail.messages.length, 1);
1920
- strict_1.default.equal(detail.messages[0]?.id, 'run-1-tool-1');
1921
- strict_1.default.equal(detail.messages[0]?.toolCall?.name, 'bash');
1922
- strict_1.default.equal(detail.messages[0]?.toolCall?.status, 'completed');
1923
- strict_1.default.deepEqual(detail.messages[0]?.toolCall?.input, { command: 'ls -la ~/Desktop' });
1924
- strict_1.default.equal(detail.messages[0]?.toolCall?.detail, 'ls -la ~/Desktop');
1925
- strict_1.default.equal(detail.messages[0]?.toolCall?.output, 'total 32');
1926
- }
1927
- finally {
1928
- store.close();
1929
- (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
1930
- }
1931
- });
1932
- (0, node_test_1.default)('channel gateways ignore unowned bridge events without route miss log noise', async () => {
1933
- const logs = [];
1934
- const commonOptions = {
1935
- store: {},
1936
- readConfig: async () => null,
1937
- getWorkspaceRouter: () => ({}),
1938
- eventBus: { emit: () => { }, on: () => () => { } },
1939
- log: (line) => logs.push(line),
1940
- };
1941
- const larkGateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway(commonOptions);
1942
- const weixinGateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway(commonOptions);
1943
- await larkGateway.onBridgeEvent({
1944
- type: 'update_message',
1945
- sessionKey: 'localcore-acp:project-1:thread-1',
1946
- content: 'stream chunk',
1947
- });
1948
- await weixinGateway.onBridgeEvent({
1949
- type: 'update_message',
1950
- sessionKey: 'localcore-acp:project-1:thread-1',
1951
- content: 'stream chunk',
1952
- });
1953
- strict_1.default.deepEqual(logs, []);
1954
- });
1955
- (0, node_test_1.default)('lark bridge sends completed thought once before final answer', async () => {
1956
- const createdMessages = [];
1957
- const patchedCards = [];
1958
- const storedMessageIds = [];
1959
- const client = {
1960
- im: {
1961
- message: {
1962
- create: async (request) => {
1963
- const messageId = `lark-msg-${createdMessages.length + 1}`;
1964
- const message = extractLarkCreatedMessage(request);
1965
- createdMessages.push({
1966
- messageId,
1967
- msgType: message.msgType,
1968
- text: message.text,
1969
- content: message.content,
1970
- });
1971
- return { data: { message_id: messageId } };
1972
- },
1973
- patch: async (request) => {
1974
- const card = JSON.parse(String(request.data.content || '{}'));
1975
- patchedCards.push({
1976
- messageId: String(request.path.message_id || ''),
1977
- text: String(card.elements?.[0]?.content || ''),
1978
- });
1979
- },
1980
- },
1981
- },
1982
- };
1983
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
1984
- store: {
1985
- getPlatformThreadBinding: () => ({
1986
- workspace_id: 'default',
1987
- platform: 'lark',
1988
- chat_id: 'chat-1',
1989
- platform_user_id: 'user-1',
1990
- thread_id: 'thread-1',
1991
- last_platform_message_id: null,
1992
- }),
1993
- updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
1994
- storedMessageIds.push(messageId);
1995
- },
1996
- },
1997
- readConfig: async () => null,
1998
- getWorkspaceRouter: () => ({}),
1999
- eventBus: { emit: () => { }, on: () => () => { } },
2000
- });
2001
- const internals = gateway;
2002
- internals.runtime.set('default', {
2003
- workspaceId: 'default',
2004
- enabled: true,
2005
- status: 'running',
2006
- connected: true,
2007
- appId: 'app-1',
2008
- client,
2009
- });
2010
- internals.threadRouting.set('session:thread-1', {
2011
- workspaceId: 'default',
2012
- platformUserId: 'user-1',
2013
- chatId: 'chat-1',
2014
- threadId: 'thread-1',
2015
- });
2016
- await gateway.onBridgeEvent({
2017
- type: 'preview_start',
2018
- sessionKey: 'session:thread-1',
2019
- replyCtx: 'run-1',
2020
- previewHandle: 'thought-preview-1',
2021
- bridgeKind: 'thought',
2022
- content: '先理解问题',
2023
- });
2024
- await gateway.onBridgeEvent({
2025
- type: 'update_message',
2026
- sessionKey: 'session:thread-1',
2027
- replyCtx: 'run-1',
2028
- previewHandle: 'thought-preview-1',
2029
- bridgeKind: 'thought',
2030
- content: '先理解问题,再检查代码',
2031
- });
2032
- await gateway.onBridgeEvent({
2033
- type: 'reply',
2034
- sessionKey: 'session:thread-1',
2035
- replyCtx: 'run-1',
2036
- content: '最终回答',
2037
- });
2038
- strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['interactive', 'post']);
2039
- strict_1.default.equal(createdMessages[0]?.text, '先理解问题,再检查代码');
2040
- strict_1.default.equal(createdMessages[1]?.text, '最终回答');
2041
- strict_1.default.equal(patchedCards.length, 0);
2042
- strict_1.default.deepEqual(storedMessageIds, ['lark-msg-2']);
2043
- });
2044
- (0, node_test_1.default)('lark bridge sends final reply as its own post message instead of streaming draft', async () => {
2045
- const createdMessages = [];
2046
- const patchedCards = [];
2047
- const storedMessageIds = [];
2048
- const client = {
2049
- im: {
2050
- message: {
2051
- create: async (request) => {
2052
- const messageId = `lark-msg-${createdMessages.length + 1}`;
2053
- const message = extractLarkCreatedMessage(request);
2054
- createdMessages.push({
2055
- messageId,
2056
- msgType: message.msgType,
2057
- text: message.text,
2058
- content: message.content,
2059
- });
2060
- return { data: { message_id: messageId } };
2061
- },
2062
- patch: async (request) => {
2063
- const card = JSON.parse(String(request.data.content || '{}'));
2064
- patchedCards.push({
2065
- messageId: String(request.path.message_id || ''),
2066
- text: String(card.elements?.[0]?.content || ''),
2067
- });
2068
- },
2069
- },
2070
- },
2071
- };
2072
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2073
- store: {
2074
- getPlatformThreadBinding: () => ({
2075
- workspace_id: 'default',
2076
- platform: 'lark',
2077
- chat_id: 'chat-1',
2078
- platform_user_id: 'user-1',
2079
- thread_id: 'thread-1',
2080
- last_platform_message_id: null,
2081
- }),
2082
- updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
2083
- storedMessageIds.push(messageId);
2084
- },
2085
- },
2086
- readConfig: async () => null,
2087
- getWorkspaceRouter: () => ({}),
2088
- eventBus: { emit: () => { }, on: () => () => { } },
2089
- });
2090
- const internals = gateway;
2091
- internals.runtime.set('default', {
2092
- workspaceId: 'default',
2093
- enabled: true,
2094
- status: 'running',
2095
- connected: true,
2096
- appId: 'app-1',
2097
- client,
2098
- });
2099
- internals.threadRouting.set('session:thread-1', {
2100
- workspaceId: 'default',
2101
- platformUserId: 'user-1',
2102
- chatId: 'chat-1',
2103
- threadId: 'thread-1',
2104
- });
2105
- await gateway.onBridgeEvent({
2106
- type: 'update_message',
2107
- sessionKey: 'session:thread-1',
2108
- replyCtx: 'run-1',
2109
- content: '流式中的最终回答草稿',
2110
- });
2111
- await gateway.onBridgeEvent({
2112
- type: 'reply',
2113
- sessionKey: 'session:thread-1',
2114
- replyCtx: 'run-1',
2115
- content: '真正最终回答',
2116
- });
2117
- strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['post']);
2118
- strict_1.default.deepEqual(createdMessages.map((message) => message.text), ['真正最终回答']);
2119
- strict_1.default.deepEqual(patchedCards, []);
2120
- strict_1.default.deepEqual(storedMessageIds, ['lark-msg-1']);
2121
- });
2122
- (0, node_test_1.default)('lark bridge sends completed assistant segments as separate non-final messages', async () => {
2123
- const createdMessages = [];
2124
- const patchedCards = [];
2125
- const storedMessageIds = [];
2126
- const client = {
2127
- im: {
2128
- message: {
2129
- create: async (request) => {
2130
- const messageId = `lark-msg-${createdMessages.length + 1}`;
2131
- const message = extractLarkCreatedMessage(request);
2132
- createdMessages.push({
2133
- messageId,
2134
- msgType: message.msgType,
2135
- text: message.text,
2136
- content: message.content,
2137
- });
2138
- return { data: { message_id: messageId } };
2139
- },
2140
- patch: async (request) => {
2141
- const card = JSON.parse(String(request.data.content || '{}'));
2142
- patchedCards.push({
2143
- messageId: String(request.path.message_id || ''),
2144
- text: String(card.elements?.[0]?.content || ''),
2145
- });
2146
- },
2147
- },
2148
- },
2149
- };
2150
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2151
- store: {
2152
- getPlatformThreadBinding: () => ({
2153
- workspace_id: 'default',
2154
- platform: 'lark',
2155
- chat_id: 'chat-1',
2156
- platform_user_id: 'user-1',
2157
- thread_id: 'thread-1',
2158
- last_platform_message_id: null,
2159
- }),
2160
- updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
2161
- storedMessageIds.push(messageId);
2162
- },
2163
- },
2164
- readConfig: async () => null,
2165
- getWorkspaceRouter: () => ({}),
2166
- eventBus: { emit: () => { }, on: () => () => { } },
2167
- });
2168
- const internals = gateway;
2169
- internals.runtime.set('default', {
2170
- workspaceId: 'default',
2171
- enabled: true,
2172
- status: 'running',
2173
- connected: true,
2174
- appId: 'app-1',
2175
- client,
2176
- });
2177
- internals.threadRouting.set('session:thread-1', {
2178
- workspaceId: 'default',
2179
- platformUserId: 'user-1',
2180
- chatId: 'chat-1',
2181
- threadId: 'thread-1',
2182
- });
2183
- await gateway.onBridgeEvent({
2184
- type: 'reply',
2185
- sessionKey: 'session:thread-1',
2186
- replyCtx: 'run-1',
2187
- messageId: 'run-1-assistant-1',
2188
- bridgeKind: 'assistant',
2189
- content: '我先检查一下',
2190
- });
2191
- await gateway.onBridgeEvent({
2192
- type: 'reply',
2193
- sessionKey: 'session:thread-1',
2194
- replyCtx: 'run-1',
2195
- content: '最终回答',
2196
- });
2197
- strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['post', 'post']);
2198
- strict_1.default.deepEqual(createdMessages.map((message) => message.text), ['我先检查一下', '最终回答']);
2199
- strict_1.default.deepEqual(patchedCards, []);
2200
- strict_1.default.deepEqual(storedMessageIds, ['lark-msg-2']);
2201
- });
2202
- (0, node_test_1.default)('lark bridge creates final replies without patching prior final messages', async () => {
2203
- const createdMessages = [];
2204
- const patchedCards = [];
2205
- const client = {
2206
- im: {
2207
- message: {
2208
- create: async (request) => {
2209
- const messageId = `lark-msg-${createdMessages.length + 1}`;
2210
- const message = extractLarkCreatedMessage(request);
2211
- createdMessages.push({
2212
- messageId,
2213
- msgType: message.msgType,
2214
- text: message.text,
2215
- content: message.content,
2216
- });
2217
- return { data: { message_id: messageId } };
2218
- },
2219
- patch: async (request) => {
2220
- const card = JSON.parse(String(request.data.content || '{}'));
2221
- patchedCards.push({
2222
- messageId: String(request.path.message_id || ''),
2223
- text: String(card.elements?.[0]?.content || ''),
2224
- });
2225
- },
2226
- },
2227
- },
2228
- };
2229
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2230
- store: {
2231
- getPlatformThreadBinding: () => ({
2232
- workspace_id: 'default',
2233
- platform: 'lark',
2234
- chat_id: 'chat-1',
2235
- platform_user_id: 'user-1',
2236
- thread_id: 'thread-1',
2237
- last_platform_message_id: null,
2238
- }),
2239
- updatePlatformThreadMessageId: () => { },
2240
- },
2241
- readConfig: async () => null,
2242
- getWorkspaceRouter: () => ({}),
2243
- eventBus: { emit: () => { }, on: () => () => { } },
2244
- });
2245
- const internals = gateway;
2246
- internals.runtime.set('default', {
2247
- workspaceId: 'default',
2248
- enabled: true,
2249
- status: 'running',
2250
- connected: true,
2251
- appId: 'app-1',
2252
- client,
2253
- });
2254
- internals.threadRouting.set('session:thread-1', {
2255
- workspaceId: 'default',
2256
- platformUserId: 'user-1',
2257
- chatId: 'chat-1',
2258
- threadId: 'thread-1',
2259
- });
2260
- await gateway.onBridgeEvent({
2261
- type: 'reply',
2262
- sessionKey: 'session:thread-1',
2263
- replyCtx: 'run-1',
2264
- content: 'first final',
2265
- });
2266
- await gateway.onBridgeEvent({
2267
- type: 'reply',
2268
- sessionKey: 'session:thread-1',
2269
- replyCtx: 'run-2',
2270
- content: 'second final',
2271
- });
2272
- strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['post', 'post']);
2273
- strict_1.default.deepEqual(createdMessages.map((message) => message.text), ['first final', 'second final']);
2274
- strict_1.default.deepEqual(patchedCards, []);
2275
- });
2276
- (0, node_test_1.default)('lark bridge sends tool and final as separate post messages without patching tool progress', async () => {
2277
- const createdMessages = [];
2278
- const patchedCards = [];
2279
- const storedMessageIds = [];
2280
- const client = {
2281
- im: {
2282
- message: {
2283
- create: async (request) => {
2284
- const messageId = `lark-msg-${createdMessages.length + 1}`;
2285
- const message = extractLarkCreatedMessage(request);
2286
- createdMessages.push({
2287
- messageId,
2288
- msgType: message.msgType,
2289
- text: message.text,
2290
- content: message.content,
2291
- });
2292
- return { data: { message_id: messageId } };
2293
- },
2294
- patch: async (request) => {
2295
- const card = JSON.parse(String(request.data.content || '{}'));
2296
- patchedCards.push({
2297
- messageId: String(request.path.message_id || ''),
2298
- text: String(card.elements?.[0]?.content || ''),
2299
- });
2300
- },
2301
- },
2302
- },
2303
- };
2304
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2305
- store: {
2306
- getPlatformThreadBinding: () => ({
2307
- workspace_id: 'default',
2308
- platform: 'lark',
2309
- chat_id: 'chat-1',
2310
- platform_user_id: 'user-1',
2311
- thread_id: 'thread-1',
2312
- last_platform_message_id: null,
2313
- }),
2314
- updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
2315
- storedMessageIds.push(messageId);
2316
- },
2317
- },
2318
- readConfig: async () => null,
2319
- getWorkspaceRouter: () => ({}),
2320
- eventBus: { emit: () => { }, on: () => () => { } },
2321
- });
2322
- const internals = gateway;
2323
- internals.runtime.set('default', {
2324
- workspaceId: 'default',
2325
- enabled: true,
2326
- status: 'running',
2327
- connected: true,
2328
- appId: 'app-1',
2329
- client,
2330
- });
2331
- internals.threadRouting.set('session:thread-1', {
2332
- workspaceId: 'default',
2333
- platformUserId: 'user-1',
2334
- chatId: 'chat-1',
2335
- threadId: 'thread-1',
2336
- });
2337
- await gateway.onBridgeEvent({
2338
- type: 'update_message',
2339
- sessionKey: 'session:thread-1',
2340
- replyCtx: 'run-1',
2341
- content: '好的,文件存在,现在发送给你:已',
2342
- });
2343
- await gateway.onBridgeEvent({
2344
- type: 'reply',
2345
- sessionKey: 'session:thread-1',
2346
- replyCtx: 'run-1',
2347
- messageId: 'tool-1',
2348
- bridgeKind: 'tool',
2349
- content: 'bash completed',
2350
- toolCall: {
2351
- id: 'tool-1',
2352
- name: 'bash',
2353
- status: 'completed',
2354
- output: 'Sent file CLAUDE.md to chat-1: msg-file-1',
2355
- },
2356
- });
2357
- await gateway.onBridgeEvent({
2358
- type: 'reply',
2359
- sessionKey: 'session:thread-1',
2360
- replyCtx: 'run-1',
2361
- content: '好的,文件存在,现在发送给你:已发送!`CLAUDE.md` 文件已经发出去了,请查收',
2362
- });
2363
- strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['post', 'post']);
2364
- strict_1.default.deepEqual(createdMessages.map((message) => message.text), [
2365
- '🔧 bash',
2366
- '好的,文件存在,现在发送给你:已发送!`CLAUDE.md` 文件已经发出去了,请查收',
2367
- ]);
2368
- strict_1.default.deepEqual(patchedCards, []);
2369
- strict_1.default.deepEqual(storedMessageIds, ['lark-msg-2']);
2370
- });
2371
- (0, node_test_1.default)('lark bridge flushes interleaved thought segments before tools and final', async () => {
2372
- const createdMessages = [];
2373
- const patchedCards = [];
2374
- const storedMessageIds = [];
2375
- const client = {
2376
- im: {
2377
- message: {
2378
- create: async (request) => {
2379
- const messageId = `lark-msg-${createdMessages.length + 1}`;
2380
- const message = extractLarkCreatedMessage(request);
2381
- createdMessages.push({
2382
- messageId,
2383
- msgType: message.msgType,
2384
- text: message.text,
2385
- content: message.content,
2386
- });
2387
- return { data: { message_id: messageId } };
2388
- },
2389
- patch: async (request) => {
2390
- const card = JSON.parse(String(request.data.content || '{}'));
2391
- patchedCards.push({
2392
- messageId: String(request.path.message_id || ''),
2393
- text: String(card.elements?.[0]?.content || ''),
2394
- });
2395
- },
2396
- },
2397
- },
2398
- };
2399
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2400
- store: {
2401
- getPlatformThreadBinding: () => ({
2402
- workspace_id: 'default',
2403
- platform: 'lark',
2404
- chat_id: 'chat-1',
2405
- platform_user_id: 'user-1',
2406
- thread_id: 'thread-1',
2407
- last_platform_message_id: 'old-message',
2408
- }),
2409
- updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
2410
- storedMessageIds.push(messageId);
2411
- },
2412
- },
2413
- readConfig: async () => null,
2414
- getWorkspaceRouter: () => ({}),
2415
- eventBus: { emit: () => { }, on: () => () => { } },
2416
- });
2417
- const internals = gateway;
2418
- internals.runtime.set('default', {
2419
- workspaceId: 'default',
2420
- enabled: true,
2421
- status: 'running',
2422
- connected: true,
2423
- appId: 'app-1',
2424
- client,
2425
- });
2426
- internals.threadRouting.set('session:thread-1', {
2427
- workspaceId: 'default',
2428
- platformUserId: 'user-1',
2429
- chatId: 'chat-1',
2430
- threadId: 'thread-1',
2431
- });
2432
- await gateway.onBridgeEvent({
2433
- type: 'preview_start',
2434
- sessionKey: 'session:thread-1',
2435
- replyCtx: 'run-1',
2436
- previewHandle: 'thought-preview-1',
2437
- bridgeKind: 'thought',
2438
- content: '先理解',
2439
- });
2440
- await gateway.onBridgeEvent({
2441
- type: 'update_message',
2442
- sessionKey: 'session:thread-1',
2443
- replyCtx: 'run-1',
2444
- previewHandle: 'thought-preview-1',
2445
- bridgeKind: 'thought',
2446
- content: '先理解用户需求',
2447
- });
2448
- await gateway.onBridgeEvent({
2449
- type: 'reply',
2450
- sessionKey: 'session:thread-1',
2451
- replyCtx: 'run-1',
2452
- messageId: 'run-1-tool-1',
2453
- bridgeKind: 'tool',
2454
- content: 'Terminal running',
2455
- toolCall: {
2456
- id: 'call-1',
2457
- name: 'Terminal',
2458
- status: 'running',
2459
- input: { command: 'uname -a', description: 'Get system info' },
2460
- output: '',
2461
- },
2462
- });
2463
- await gateway.onBridgeEvent({
2464
- type: 'reply',
2465
- sessionKey: 'session:thread-1',
2466
- replyCtx: 'run-1',
2467
- messageId: 'run-1-tool-1',
2468
- bridgeKind: 'tool',
2469
- content: 'Terminal completed',
2470
- toolCall: {
2471
- id: 'call-1',
2472
- name: 'Terminal',
2473
- status: 'completed',
2474
- input: { command: 'uname -a', description: 'Get system info' },
2475
- output: 'Linux',
2476
- },
2477
- });
2478
- await gateway.onBridgeEvent({
2479
- type: 'update_message',
2480
- sessionKey: 'session:thread-1',
2481
- replyCtx: 'run-1',
2482
- previewHandle: 'thought-preview-1',
2483
- bridgeKind: 'thought',
2484
- content: '看到了 Linux',
2485
- });
2486
- await gateway.onBridgeEvent({
2487
- type: 'reply',
2488
- sessionKey: 'session:thread-1',
2489
- replyCtx: 'run-1',
2490
- content: '最终回答',
2491
- });
2492
- strict_1.default.deepEqual(createdMessages.map((message) => message.msgType), ['interactive', 'post', 'interactive', 'post']);
2493
- strict_1.default.deepEqual(createdMessages.map((message) => message.text), [
2494
- '先理解用户需求',
2495
- '🔧 Terminal\n\n```json\n{\n "command": "uname -a",\n "description": "Get system info"\n}\n```',
2496
- '看到了 Linux',
2497
- '最终回答',
2498
- ]);
2499
- strict_1.default.equal(findLarkPostMdText(createdMessages[1]?.content), '🔧 Terminal\n\n```json\n{\n "command": "uname -a",\n "description": "Get system info"\n}\n```');
2500
- strict_1.default.deepEqual(patchedCards, []);
2501
- strict_1.default.deepEqual(storedMessageIds, ['lark-msg-4']);
2502
- });
2503
- (0, node_test_1.default)('lark bridge does not stream thought updates before completion', async () => {
2504
- const createdCards = [];
2505
- const patchedCards = [];
2506
- const client = {
2507
- im: {
2508
- message: {
2509
- create: async (request) => {
2510
- const messageId = `lark-msg-${createdCards.length + 1}`;
2511
- const card = JSON.parse(String(request.data.content || '{}'));
2512
- createdCards.push({
2513
- messageId,
2514
- text: String(card.elements?.[0]?.content || ''),
2515
- });
2516
- return { data: { message_id: messageId } };
2517
- },
2518
- patch: async (request) => {
2519
- const card = JSON.parse(String(request.data.content || '{}'));
2520
- patchedCards.push({
2521
- messageId: String(request.path.message_id || ''),
2522
- text: String(card.elements?.[0]?.content || ''),
2523
- });
2524
- },
2525
- },
2526
- },
2527
- };
2528
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2529
- store: {
2530
- getPlatformThreadBinding: () => ({
2531
- workspace_id: 'default',
2532
- platform: 'lark',
2533
- chat_id: 'chat-1',
2534
- platform_user_id: 'user-1',
2535
- thread_id: 'thread-1',
2536
- last_platform_message_id: null,
2537
- }),
2538
- updatePlatformThreadMessageId: () => { },
2539
- },
2540
- readConfig: async () => null,
2541
- getWorkspaceRouter: () => ({}),
2542
- eventBus: { emit: () => { }, on: () => () => { } },
2543
- });
2544
- const internals = gateway;
2545
- internals.runtime.set('default', {
2546
- workspaceId: 'default',
2547
- enabled: true,
2548
- status: 'running',
2549
- connected: true,
2550
- appId: 'app-1',
2551
- client,
2552
- });
2553
- internals.threadRouting.set('session:thread-1', {
2554
- workspaceId: 'default',
2555
- platformUserId: 'user-1',
2556
- chatId: 'chat-1',
2557
- threadId: 'thread-1',
2558
- });
2559
- await gateway.onBridgeEvent({
2560
- type: 'typing_start',
2561
- sessionKey: 'session:thread-1',
2562
- replyCtx: 'run-1',
2563
- });
2564
- await gateway.onBridgeEvent({
2565
- type: 'preview_start',
2566
- sessionKey: 'session:thread-1',
2567
- replyCtx: 'run-1',
2568
- previewHandle: 'thought-preview-1',
2569
- bridgeKind: 'thought',
2570
- content: 'The user',
2571
- });
2572
- await gateway.onBridgeEvent({
2573
- type: 'update_message',
2574
- sessionKey: 'session:thread-1',
2575
- replyCtx: 'run-1',
2576
- previewHandle: 'thought-preview-1',
2577
- bridgeKind: 'thought',
2578
- content: 'The user sent a short casual message.',
2579
- });
2580
- await gateway.onBridgeEvent({
2581
- type: 'update_message',
2582
- sessionKey: 'session:thread-1',
2583
- replyCtx: 'run-1',
2584
- previewHandle: 'thought-preview-1',
2585
- bridgeKind: 'thought',
2586
- content: 'The user sent a short casual message. I should reply briefly.',
2587
- });
2588
- await gateway.onBridgeEvent({
2589
- type: 'typing_stop',
2590
- sessionKey: 'session:thread-1',
2591
- replyCtx: 'run-1',
2592
- });
2593
- strict_1.default.deepEqual(createdCards.map((card) => card.text), ['The user sent a short casual message. I should reply briefly.']);
2594
- strict_1.default.deepEqual(patchedCards.map((card) => card.text), []);
2595
- strict_1.default.ok(!createdCards.some((card) => /处理中|正在思考/.test(card.text)));
2596
- });
2597
- (0, node_test_1.default)('lark permission requests render as clickable card buttons', async () => {
2598
- const createdCards = [];
2599
- const patchedCards = [];
2600
- const threadActions = [];
2601
- const client = {
2602
- im: {
2603
- message: {
2604
- create: async (request) => {
2605
- createdCards.push(JSON.parse(String(request.data.content || '{}')));
2606
- return { data: { message_id: 'permission-msg-1' } };
2607
- },
2608
- patch: async (request) => {
2609
- patchedCards.push({
2610
- messageId: request.path?.message_id,
2611
- card: JSON.parse(String(request.data.content || '{}')),
2612
- });
2613
- },
2614
- },
2615
- },
2616
- };
2617
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2618
- store: {
2619
- getPlatformThreadBinding: () => ({
2620
- workspace_id: 'default',
2621
- platform: 'lark',
2622
- chat_id: 'chat-1',
2623
- platform_user_id: 'user-1',
2624
- thread_id: 'thread-1',
2625
- last_platform_message_id: null,
2626
- }),
2627
- },
2628
- readConfig: async () => null,
2629
- getWorkspaceRouter: () => ({
2630
- sendThreadAction: async (threadId, action) => {
2631
- threadActions.push({ threadId, action });
2632
- return { runId: 'run-1' };
2633
- },
2634
- }),
2635
- eventBus: { emit: () => { }, on: () => () => { } },
2636
- });
2637
- const internals = gateway;
2638
- internals.runtime.set('default', {
2639
- workspaceId: 'default',
2640
- enabled: true,
2641
- status: 'running',
2642
- connected: true,
2643
- appId: 'app-1',
2644
- cardActionsEnabled: true,
2645
- client,
2646
- });
2647
- internals.threadRouting.set('session:thread-1', {
2648
- workspaceId: 'default',
2649
- platformUserId: 'user-1',
2650
- chatId: 'chat-1',
2651
- threadId: 'thread-1',
2652
- });
2653
- await gateway.onBridgeEvent({
2654
- type: 'buttons',
2655
- sessionKey: 'session:thread-1',
2656
- replyCtx: 'run-1',
2657
- content: [
2658
- '等待工具确认',
2659
- '',
2660
- 'Terminal',
2661
- '',
2662
- 'parameters:',
2663
- '{"command":"ls"}',
2664
- '',
2665
- '请选择一个选项继续执行。',
2666
- ].join('\n'),
2667
- buttonRows: [[
2668
- { text: 'allow', data: 'allow' },
2669
- { text: 'allow all', data: 'allow all' },
2670
- { text: 'deny', data: 'deny' },
2671
- ]],
2672
- });
2673
- const actionElement = createdCards[0]?.elements?.find((element) => element.tag === 'action');
2674
- strict_1.default.equal(createdCards.length, 1);
2675
- strict_1.default.match(createdCards[0]?.elements?.[0]?.content || '', /需要工具确认/);
2676
- strict_1.default.deepEqual(actionElement?.actions?.map((action) => ({
2677
- label: action.text?.content,
2678
- type: action.type,
2679
- response: action.value?.response,
2680
- threadId: action.value?.thread_id,
2681
- })), [
2682
- { label: '允许一次', type: 'primary', response: 'allow', threadId: 'thread-1' },
2683
- { label: '始终允许', type: 'default', response: 'allow all', threadId: 'thread-1' },
2684
- { label: '拒绝', type: 'danger', response: 'deny', threadId: 'thread-1' },
2685
- ]);
2686
- await internals.handleCardActionEvent('default', {
2687
- event: {
2688
- action: {
2689
- value: {
2690
- action: 'permission_response',
2691
- response: 'allow all',
2692
- thread_id: 'thread-1',
2693
- session_key: 'session:thread-1',
2694
- },
2695
- },
2696
- context: {
2697
- open_message_id: 'permission-msg-1',
2698
- },
2699
- },
2700
- });
2701
- strict_1.default.deepEqual(threadActions, [
2702
- { threadId: 'thread-1', action: 'allow all' },
2703
- ]);
2704
- strict_1.default.equal(patchedCards.length, 1);
2705
- strict_1.default.equal(patchedCards[0]?.messageId, 'permission-msg-1');
2706
- strict_1.default.match(patchedCards[0]?.card?.elements?.[0]?.content || '', /工具确认已处理/);
2707
- strict_1.default.equal(patchedCards[0]?.card?.elements?.some((element) => element.tag === 'action'), false);
2708
- });
2709
- (0, node_test_1.default)('lark allow all card action preserves the final reply after tool execution', async () => {
2710
- const createdMessages = [];
2711
- const patchedCards = [];
2712
- let nextMessageId = 1;
2713
- let gateway;
2714
- const client = {
2715
- im: {
2716
- message: {
2717
- create: async (request) => {
2718
- const messageId = `card-${nextMessageId++}`;
2719
- const message = extractLarkCreatedMessage(request);
2720
- createdMessages.push({
2721
- messageId,
2722
- msgType: message.msgType,
2723
- text: message.text,
2724
- content: message.content,
2725
- });
2726
- return { data: { message_id: messageId } };
2727
- },
2728
- patch: async (request) => {
2729
- patchedCards.push({
2730
- messageId: request.path?.message_id,
2731
- card: JSON.parse(String(request.data.content || '{}')),
2732
- });
2733
- },
2734
- },
2735
- },
2736
- };
2737
- gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2738
- store: {
2739
- getPlatformThreadBinding: () => ({
2740
- workspace_id: 'default',
2741
- platform: 'lark',
2742
- chat_id: 'chat-1',
2743
- platform_user_id: 'user-1',
2744
- thread_id: 'thread-1',
2745
- last_platform_message_id: null,
2746
- }),
2747
- clearPlatformThreadMessageId: () => { },
2748
- updatePlatformThreadMessageId: () => { },
2749
- },
2750
- readConfig: async () => null,
2751
- getWorkspaceRouter: () => ({
2752
- sendThreadAction: async (threadId, action) => {
2753
- strict_1.default.equal(threadId, 'thread-1');
2754
- strict_1.default.equal(action, 'allow all');
2755
- await gateway.onBridgeEvent({
2756
- type: 'typing_start',
2757
- sessionKey: 'session:thread-1',
2758
- replyCtx: 'run-1',
2759
- });
2760
- await gateway.onBridgeEvent({
2761
- type: 'reply',
2762
- sessionKey: 'session:thread-1',
2763
- replyCtx: 'run-1',
2764
- content: '桌面文件列表:AI进展报告_2026年4月.md',
2765
- });
2766
- return { runId: 'run-1' };
2767
- },
2768
- }),
2769
- eventBus: { emit: () => { }, on: () => () => { } },
2770
- });
2771
- const internals = gateway;
2772
- internals.runtime.set('default', {
2773
- workspaceId: 'default',
2774
- enabled: true,
2775
- status: 'running',
2776
- connected: true,
2777
- appId: 'app-1',
2778
- cardActionsEnabled: true,
2779
- client,
2780
- });
2781
- internals.threadRouting.set('session:thread-1', {
2782
- workspaceId: 'default',
2783
- platformUserId: 'user-1',
2784
- chatId: 'chat-1',
2785
- threadId: 'thread-1',
2786
- });
2787
- internals.outboundTurns.set('session:thread-1', {
2788
- sessionKey: 'session:thread-1',
2789
- progressMessageIds: {},
2790
- permissionMessageId: 'permission-msg-1',
2791
- awaitingPermission: true,
2792
- processing: true,
2793
- previewText: '',
2794
- finalText: '',
2795
- thinkingSteps: [],
2796
- thoughtSegmentSequence: 0,
2797
- toolCalls: [],
2798
- statusLines: [],
2799
- buttonRows: [[{ text: '始终允许', data: 'allow all' }]],
2800
- lastPatchedAt: 0,
2801
- lastPatchedAtByMessageId: {},
2802
- });
2803
- await internals.handleCardActionEvent('default', {
2804
- event: {
2805
- action: {
2806
- value: {
2807
- action: 'permission_response',
2808
- response: 'allow all',
2809
- thread_id: 'thread-1',
2810
- session_key: 'session:thread-1',
2811
- },
2812
- },
2813
- context: {
2814
- open_message_id: 'permission-msg-1',
2815
- },
2816
- },
2817
- });
2818
- strict_1.default.equal(patchedCards[0]?.messageId, 'permission-msg-1');
2819
- strict_1.default.match(patchedCards[0]?.card?.elements?.[0]?.content || '', /工具确认已处理/);
2820
- strict_1.default.equal(createdMessages.length, 1);
2821
- strict_1.default.equal(createdMessages[0]?.msgType, 'post');
2822
- strict_1.default.match(createdMessages[0]?.text || '', /桌面文件列表/);
2823
- });
2824
- (0, node_test_1.default)('lark channel sends tool name and parameters once without streaming output', async () => {
2825
- const createdMessages = [];
2826
- const patchedCards = [];
2827
- const client = {
2828
- im: {
2829
- message: {
2830
- create: async (request) => {
2831
- createdMessages.push(extractLarkCreatedMessage(request));
2832
- return { data: { message_id: 'tool-msg-1' } };
2833
- },
2834
- patch: async (request) => {
2835
- patchedCards.push(JSON.parse(String(request.data.content || '{}')));
2836
- },
2837
- },
2838
- },
2839
- };
2840
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2841
- store: {
2842
- getPlatformThreadBinding: () => ({
2843
- workspace_id: 'default',
2844
- platform: 'lark',
2845
- chat_id: 'chat-1',
2846
- platform_user_id: 'user-1',
2847
- thread_id: 'thread-1',
2848
- last_platform_message_id: null,
2849
- }),
2850
- },
2851
- readConfig: async () => null,
2852
- getWorkspaceRouter: () => ({}),
2853
- eventBus: { emit: () => { }, on: () => () => { } },
2854
- });
2855
- const internals = gateway;
2856
- internals.runtime.set('default', {
2857
- workspaceId: 'default',
2858
- enabled: true,
2859
- status: 'running',
2860
- connected: true,
2861
- appId: 'app-1',
2862
- client,
2863
- });
2864
- internals.threadRouting.set('session:thread-1', {
2865
- workspaceId: 'default',
2866
- platformUserId: 'user-1',
2867
- chatId: 'chat-1',
2868
- threadId: 'thread-1',
2869
- });
2870
- await gateway.onBridgeEvent({
2871
- type: 'reply',
2872
- sessionKey: 'session:thread-1',
2873
- replyCtx: 'run-1',
2874
- messageId: 'tool-1',
2875
- bridgeKind: 'tool',
2876
- content: 'Terminal running',
2877
- toolCall: {
2878
- id: 'tool-1',
2879
- name: 'Terminal',
2880
- status: 'running',
2881
- input: { command: 'ls ~/Desktop' },
2882
- output: '',
2883
- },
2884
- });
2885
- await gateway.onBridgeEvent({
2886
- type: 'reply',
2887
- sessionKey: 'session:thread-1',
2888
- replyCtx: 'run-1',
2889
- messageId: 'tool-1',
2890
- bridgeKind: 'tool',
2891
- content: 'Terminal completed',
2892
- toolCall: {
2893
- id: 'tool-1',
2894
- name: 'Terminal',
2895
- status: 'completed',
2896
- input: { command: 'ls ~/Desktop' },
2897
- output: 'secret terminal output',
2898
- },
2899
- });
2900
- const text = createdMessages[0]?.text || '';
2901
- strict_1.default.equal(createdMessages[0]?.msgType, 'post');
2902
- strict_1.default.match(text, /🔧 Terminal/);
2903
- strict_1.default.match(text, /```json\n{\n "command": "ls ~\/Desktop"\n}\n```/);
2904
- strict_1.default.doesNotMatch(text, /参数/);
2905
- strict_1.default.equal(findLarkPostMdText(createdMessages[0]?.content), '🔧 Terminal\n\n```json\n{\n "command": "ls ~/Desktop"\n}\n```');
2906
- strict_1.default.equal(patchedCards.length, 0);
2907
- strict_1.default.doesNotMatch(text, /completed/);
2908
- strict_1.default.doesNotMatch(text, /secret terminal output/);
2909
- });
2910
- (0, node_test_1.default)('lark card action message id can be extracted from full callback payload', async () => {
2911
- const patchedCards = [];
2912
- const threadActions = [];
2913
- const client = {
2914
- im: {
2915
- message: {
2916
- patch: async (request) => {
2917
- patchedCards.push({
2918
- messageId: request.path?.message_id,
2919
- card: JSON.parse(String(request.data.content || '{}')),
2920
- });
2921
- },
2922
- },
2923
- },
2924
- };
2925
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2926
- store: {},
2927
- readConfig: async () => null,
2928
- getWorkspaceRouter: () => ({
2929
- sendThreadAction: async (threadId, action) => {
2930
- threadActions.push({ threadId, action });
2931
- return { runId: 'run-1' };
2932
- },
2933
- }),
2934
- eventBus: { emit: () => { }, on: () => () => { } },
2935
- });
2936
- const internals = gateway;
2937
- internals.runtime.set('default', {
2938
- workspaceId: 'default',
2939
- enabled: true,
2940
- status: 'running',
2941
- connected: true,
2942
- appId: 'app-1',
2943
- client,
2944
- });
2945
- await internals.handleCardActionEvent('default', {
2946
- schema: '2.0',
2947
- event: {
2948
- action: {
2949
- value: {
2950
- action: 'permission_response',
2951
- response: 'allow',
2952
- thread_id: 'thread-1',
2953
- },
2954
- },
2955
- },
2956
- event_context: {
2957
- open_message_id: 'permission-msg-nested',
2958
- },
2959
- });
2960
- strict_1.default.deepEqual(threadActions, [
2961
- { threadId: 'thread-1', action: 'allow' },
2962
- ]);
2963
- strict_1.default.equal(patchedCards[0]?.messageId, 'permission-msg-nested');
2964
- strict_1.default.equal(patchedCards[0]?.card?.elements?.some((element) => element.tag === 'action'), false);
2965
- });
2966
- (0, node_test_1.default)('lark permission requests fall back to text commands when card actions are disabled', async () => {
2967
- const createdCards = [];
2968
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
2969
- store: {
2970
- getPlatformThreadBinding: () => ({
2971
- workspace_id: 'default',
2972
- platform: 'lark',
2973
- chat_id: 'chat-1',
2974
- platform_user_id: 'user-1',
2975
- thread_id: 'thread-1',
2976
- last_platform_message_id: null,
2977
- }),
2978
- },
2979
- readConfig: async () => null,
2980
- getWorkspaceRouter: () => ({}),
2981
- eventBus: { emit: () => { }, on: () => () => { } },
2982
- });
2983
- const internals = gateway;
2984
- internals.runtime.set('default', {
2985
- workspaceId: 'default',
2986
- enabled: true,
2987
- status: 'running',
2988
- connected: true,
2989
- appId: 'app-1',
2990
- cardActionsEnabled: false,
2991
- client: {
2992
- im: {
2993
- message: {
2994
- create: async (request) => {
2995
- createdCards.push(JSON.parse(String(request.data.content || '{}')));
2996
- return { data: { message_id: 'permission-msg-1' } };
2997
- },
2998
- patch: async () => { },
2999
- },
3000
- },
3001
- },
3002
- });
3003
- internals.threadRouting.set('session:thread-1', {
3004
- workspaceId: 'default',
3005
- platformUserId: 'user-1',
3006
- chatId: 'chat-1',
3007
- threadId: 'thread-1',
3008
- });
3009
- await gateway.onBridgeEvent({
3010
- type: 'buttons',
3011
- sessionKey: 'session:thread-1',
3012
- replyCtx: 'run-1',
3013
- content: '等待工具确认\n\nTerminal\n\n请选择一个选项继续执行。',
3014
- buttonRows: [[
3015
- { text: 'allow', data: 'allow' },
3016
- { text: 'deny', data: 'deny' },
3017
- ]],
3018
- });
3019
- strict_1.default.equal(createdCards.length, 1);
3020
- strict_1.default.match(createdCards[0]?.elements?.[0]?.content || '', /请直接回复/);
3021
- strict_1.default.equal(createdCards[0]?.elements?.some((element) => element.tag === 'action'), false);
3022
- });
3023
- (0, node_test_1.default)('lark image messages are downloaded and forwarded as generic channel image parts', async () => {
3024
- const sentMessages = [];
3025
- const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
3026
- const client = {
3027
- im: {
3028
- messageResource: {
3029
- get: async (request) => {
3030
- strict_1.default.equal(request.path.message_id, 'msg-image-1');
3031
- strict_1.default.equal(request.path.file_key, 'img-key-1');
3032
- strict_1.default.equal(request.params.type, 'image');
3033
- return {
3034
- headers: { 'content-type': 'image/png' },
3035
- getReadableStream: () => node_stream_1.Readable.from([pngBytes]),
3036
- };
3037
- },
3038
- },
3039
- messageReaction: {
3040
- create: async () => ({ data: { reaction_id: 'reaction-1' } }),
3041
- },
3042
- },
3043
- };
3044
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
3045
- store: {
3046
- expirePendingPairings: () => { },
3047
- getAuthorizedUser: () => ({
3048
- id: 'auth-1',
3049
- workspace_id: 'default',
3050
- platform: 'lark',
3051
- platform_user_id: 'user-1',
3052
- chat_id: 'chat-1',
3053
- display_name: 'User',
3054
- thread_id: 'thread-1',
3055
- }),
3056
- getPlatformThreadBinding: () => ({
3057
- workspace_id: 'default',
3058
- platform: 'lark',
3059
- chat_id: 'chat-1',
3060
- platform_user_id: 'user-1',
3061
- thread_id: 'thread-1',
3062
- last_platform_message_id: null,
3063
- }),
3064
- getLatestRunForThread: () => null,
3065
- clearPlatformThreadMessageId: () => { },
3066
- },
3067
- readConfig: async () => ({
3068
- projects: [{
3069
- name: 'default',
3070
- root: '/tmp/project',
3071
- platforms: [{
3072
- type: 'lark',
3073
- options: {
3074
- app_id: 'app-1',
3075
- app_secret: 'secret-1',
3076
- },
3077
- }],
3078
- }],
3079
- }),
3080
- getWorkspaceRouter: () => ({
3081
- getThreadSessionKey: (threadId) => `session:${threadId}`,
3082
- sendThreadMessage: async (threadId, content) => {
3083
- sentMessages.push({ threadId, content });
3084
- return { runId: 'run-1' };
3085
- },
3086
- }),
3087
- eventBus: { emit: () => { }, on: () => () => { } },
3088
- });
3089
- const internals = gateway;
3090
- internals.runtime.set('default', {
3091
- workspaceId: 'default',
3092
- enabled: true,
3093
- status: 'running',
3094
- connected: true,
3095
- appId: 'app-1',
3096
- client,
3097
- });
3098
- await internals.handleMessageEvent('default', {
3099
- event: {
3100
- sender: {
3101
- sender_id: { user_id: 'user-1' },
3102
- },
3103
- message: {
3104
- message_id: 'msg-image-1',
3105
- message_type: 'image',
3106
- chat_id: 'chat-1',
3107
- content: JSON.stringify({ image_key: 'img-key-1' }),
3108
- },
3109
- },
3110
- });
3111
- strict_1.default.equal(sentMessages.length, 1);
3112
- strict_1.default.equal(sentMessages[0]?.threadId, 'thread-1');
3113
- strict_1.default.match(sentMessages[0]?.content?.displayText, /\[User Message\]\n\[Image\]\n\[\/User Message\]/);
3114
- strict_1.default.deepEqual(sentMessages[0]?.content?.contentParts?.map((part) => part.type), ['text', 'image']);
3115
- strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.mimeType, 'image/png');
3116
- strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.data, pngBytes.toString('base64'));
3117
- });
3118
- (0, node_test_1.default)('lark file messages are downloaded and forwarded as generic channel file parts', async () => {
3119
- const sentMessages = [];
3120
- const fileBytes = Buffer.from('file content');
3121
- const client = {
3122
- im: {
3123
- messageResource: {
3124
- get: async (request) => {
3125
- strict_1.default.equal(request.path.message_id, 'msg-file-in-1');
3126
- strict_1.default.equal(request.path.file_key, 'file-key-in-1');
3127
- strict_1.default.equal(request.params.type, 'file');
3128
- return {
3129
- headers: { 'content-type': 'application/pdf' },
3130
- getReadableStream: () => node_stream_1.Readable.from([fileBytes]),
3131
- };
3132
- },
3133
- },
3134
- messageReaction: {
3135
- create: async () => ({ data: { reaction_id: 'reaction-1' } }),
3136
- },
3137
- },
3138
- };
3139
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
3140
- store: {
3141
- expirePendingPairings: () => { },
3142
- getAuthorizedUser: () => ({
3143
- id: 'auth-1',
3144
- workspace_id: 'default',
3145
- platform: 'lark',
3146
- platform_user_id: 'user-1',
3147
- chat_id: 'chat-1',
3148
- display_name: 'User',
3149
- thread_id: 'thread-1',
3150
- }),
3151
- getPlatformThreadBinding: () => ({
3152
- workspace_id: 'default',
3153
- platform: 'lark',
3154
- chat_id: 'chat-1',
3155
- platform_user_id: 'user-1',
3156
- thread_id: 'thread-1',
3157
- last_platform_message_id: null,
3158
- }),
3159
- getLatestRunForThread: () => null,
3160
- clearPlatformThreadMessageId: () => { },
3161
- },
3162
- readConfig: async () => ({
3163
- projects: [{
3164
- name: 'default',
3165
- root: '/tmp/project',
3166
- platforms: [{
3167
- type: 'lark',
3168
- options: {
3169
- app_id: 'app-1',
3170
- app_secret: 'secret-1',
3171
- },
3172
- }],
3173
- }],
3174
- }),
3175
- getWorkspaceRouter: () => ({
3176
- getThreadSessionKey: (threadId) => `session:${threadId}`,
3177
- sendThreadMessage: async (threadId, content) => {
3178
- sentMessages.push({ threadId, content });
3179
- return { runId: 'run-1' };
3180
- },
3181
- }),
3182
- eventBus: { emit: () => { }, on: () => () => { } },
3183
- });
3184
- const internals = gateway;
3185
- internals.runtime.set('default', {
3186
- workspaceId: 'default',
3187
- enabled: true,
3188
- status: 'running',
3189
- connected: true,
3190
- appId: 'app-1',
3191
- client,
3192
- });
3193
- await internals.handleMessageEvent('default', {
3194
- event: {
3195
- sender: {
3196
- sender_id: { user_id: 'user-1' },
3197
- },
3198
- message: {
3199
- message_id: 'msg-file-in-1',
3200
- message_type: 'file',
3201
- chat_id: 'chat-1',
3202
- content: JSON.stringify({ file_key: 'file-key-in-1', file_name: 'report.pdf' }),
3203
- },
3204
- },
3205
- });
3206
- strict_1.default.equal(sentMessages.length, 1);
3207
- strict_1.default.equal(sentMessages[0]?.threadId, 'thread-1');
3208
- strict_1.default.match(sentMessages[0]?.content?.displayText, /\[User Message\]\n\[File: report\.pdf\]\n\[\/User Message\]/);
3209
- strict_1.default.deepEqual(sentMessages[0]?.content?.contentParts?.map((part) => part.type), ['text', 'file']);
3210
- strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.mimeType, 'application/pdf');
3211
- strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.data, fileBytes.toString('base64'));
3212
- strict_1.default.equal(sentMessages[0]?.content?.contentParts?.[1]?.fileName, 'report.pdf');
3213
- });
3214
- (0, node_test_1.default)('lark channel can upload and send a local file', async () => {
3215
- const tempDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'lark-file-send-'));
3216
- try {
3217
- const filePath = (0, node_path_1.join)(tempDir, 'report.pdf');
3218
- (0, node_fs_1.writeFileSync)(filePath, 'pdf content');
3219
- const uploads = [];
3220
- const messages = [];
3221
- const client = {
3222
- im: {
3223
- file: {
3224
- create: async (request) => {
3225
- uploads.push(request);
3226
- await new Promise((resolve, reject) => {
3227
- request.data.file.on('data', () => { });
3228
- request.data.file.on('error', reject);
3229
- request.data.file.on('end', resolve);
3230
- });
3231
- return { file_key: 'file-key-1' };
3232
- },
3233
- },
3234
- message: {
3235
- create: async (request) => {
3236
- messages.push(request);
3237
- return { data: { message_id: 'msg-file-1' } };
3238
- },
3239
- },
3240
- },
3241
- };
3242
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
3243
- store: {},
3244
- readConfig: async () => null,
3245
- getWorkspaceRouter: () => ({}),
3246
- eventBus: { emit: () => { }, on: () => () => { } },
3247
- });
3248
- const internals = gateway;
3249
- internals.runtime.set('default', {
3250
- workspaceId: 'default',
3251
- enabled: true,
3252
- status: 'running',
3253
- connected: true,
3254
- appId: 'app-1',
3255
- client,
3256
- });
3257
- const result = await gateway.sendFile('default', {
3258
- path: filePath,
3259
- channelId: 'oc_chat_1',
3260
- });
3261
- strict_1.default.equal(uploads.length, 1);
3262
- strict_1.default.equal(uploads[0]?.data?.file_type, 'pdf');
3263
- strict_1.default.equal(uploads[0]?.data?.file_name, 'report.pdf');
3264
- strict_1.default.equal(messages.length, 1);
3265
- strict_1.default.equal(messages[0]?.params?.receive_id_type, 'chat_id');
3266
- strict_1.default.equal(messages[0]?.data?.receive_id, 'oc_chat_1');
3267
- strict_1.default.equal(messages[0]?.data?.msg_type, 'file');
3268
- strict_1.default.deepEqual(JSON.parse(messages[0]?.data?.content), { file_key: 'file-key-1' });
3269
- strict_1.default.deepEqual(result, {
3270
- platform: 'lark',
3271
- workspaceId: 'default',
3272
- channelId: 'oc_chat_1',
3273
- messageId: 'msg-file-1',
3274
- fileKey: 'file-key-1',
3275
- fileName: 'report.pdf',
3276
- fileSize: Buffer.byteLength('pdf content'),
3277
- });
3278
- }
3279
- finally {
3280
- (0, node_fs_1.rmSync)(tempDir, { recursive: true, force: true });
3281
- }
3282
- });
3283
- (0, node_test_1.default)('weixin channel can encrypt, upload, and send a local file', async () => {
3284
- const tempDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'weixin-file-send-'));
3285
- const originalFetch = globalThis.fetch;
3286
- try {
3287
- const filePath = (0, node_path_1.join)(tempDir, 'report.txt');
3288
- (0, node_fs_1.writeFileSync)(filePath, 'hello weixin');
3289
- const uploadUrlRequests = [];
3290
- const cdnUploads = [];
3291
- const sentMessages = [];
3292
- globalThis.fetch = (async (url, init) => {
3293
- const target = String(url);
3294
- if (target.endsWith('/ilink/bot/getuploadurl')) {
3295
- uploadUrlRequests.push(JSON.parse(String(init?.body || '{}')));
3296
- return {
3297
- ok: true,
3298
- json: async () => ({ ret: 0, upload_param: 'upload-param-1' }),
3299
- };
3300
- }
3301
- if (target.includes('/upload?')) {
3302
- const body = init?.body instanceof Uint8Array ? init.body : new Uint8Array();
3303
- cdnUploads.push({ url: target, size: body.byteLength });
3304
- return {
3305
- ok: true,
3306
- headers: {
3307
- get: (name) => name.toLowerCase() === 'x-encrypted-param' ? 'download-param-1' : null,
3308
- },
3309
- };
3310
- }
3311
- if (target.endsWith('/ilink/bot/sendmessage')) {
3312
- sentMessages.push(JSON.parse(String(init?.body || '{}')));
3313
- return {
3314
- ok: true,
3315
- json: async () => ({ ret: 0 }),
3316
- };
3317
- }
3318
- throw new Error(`Unexpected fetch ${target}`);
3319
- });
3320
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
3321
- store: {
3322
- getPlatformThreadBinding: () => ({
3323
- workspace_id: 'default',
3324
- platform: 'weixin',
3325
- chat_id: 'user-1',
3326
- platform_user_id: 'user-1',
3327
- thread_id: 'thread-1',
3328
- last_platform_message_id: 'ctx-1',
3329
- }),
3330
- listAuthorizedUsers: () => [],
3331
- },
3332
- readConfig: async () => ({
3333
- projects: [{
3334
- name: 'default',
3335
- root: '/tmp/project',
3336
- platforms: [{
3337
- type: 'weixin',
3338
- options: {
3339
- token: 'token-1',
3340
- account_id: 'account-1',
3341
- base_url: 'https://weixin.example',
3342
- cdn_base_url: 'https://cdn.example/c2c',
3343
- },
3344
- }],
3345
- }],
3346
- }),
3347
- getWorkspaceRouter: () => ({}),
3348
- eventBus: { emit: () => { }, on: () => () => { } },
3349
- });
3350
- const internals = gateway;
3351
- internals.runtime.set('default', {
3352
- workspaceId: 'default',
3353
- enabled: true,
3354
- status: 'running',
3355
- connected: true,
3356
- accountId: 'account-1',
3357
- });
3358
- const result = await gateway.sendFile('default', {
3359
- path: filePath,
3360
- channelId: 'user-1',
3361
- participantId: 'user-1',
3362
- });
3363
- strict_1.default.equal(uploadUrlRequests.length, 1);
3364
- strict_1.default.equal(uploadUrlRequests[0]?.media_type, 3);
3365
- strict_1.default.equal(uploadUrlRequests[0]?.to_user_id, 'user-1');
3366
- strict_1.default.equal(uploadUrlRequests[0]?.rawsize, Buffer.byteLength('hello weixin'));
3367
- strict_1.default.equal(uploadUrlRequests[0]?.filesize, 16);
3368
- strict_1.default.equal(cdnUploads.length, 1);
3369
- strict_1.default.match(cdnUploads[0]?.url || '', /encrypted_query_param=upload-param-1/);
3370
- strict_1.default.equal(cdnUploads[0]?.size, 16);
3371
- strict_1.default.equal(sentMessages.length, 1);
3372
- const message = sentMessages[0]?.msg;
3373
- strict_1.default.equal(message?.to_user_id, 'user-1');
3374
- strict_1.default.equal(message?.context_token, 'ctx-1');
3375
- strict_1.default.equal(message?.item_list?.[0]?.type, 4);
3376
- strict_1.default.equal(message?.item_list?.[0]?.file_item?.file_name, 'report.txt');
3377
- strict_1.default.equal(message?.item_list?.[0]?.file_item?.len, String(Buffer.byteLength('hello weixin')));
3378
- strict_1.default.equal(message?.item_list?.[0]?.file_item?.media?.encrypt_query_param, 'download-param-1');
3379
- strict_1.default.equal(result.platform, 'weixin');
3380
- strict_1.default.equal(result.channelId, 'user-1');
3381
- strict_1.default.equal(result.fileName, 'report.txt');
3382
- strict_1.default.equal(result.fileSize, Buffer.byteLength('hello weixin'));
3383
- strict_1.default.match(result.messageId, /^openclaw-weixin-/);
3384
- strict_1.default.ok(result.fileKey);
3385
- }
3386
- finally {
3387
- globalThis.fetch = originalFetch;
3388
- (0, node_fs_1.rmSync)(tempDir, { recursive: true, force: true });
3389
- }
3390
- });
3391
- (0, node_test_1.default)('lark message callbacks acknowledge before long thread runs finish', async () => {
3392
- let registeredHandlers = {};
3393
- let sentMessages = 0;
3394
- const logs = [];
3395
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
3396
- store: {
3397
- expirePendingPairings: () => { },
3398
- getAuthorizedUser: () => ({
3399
- id: 'auth-1',
3400
- workspace_id: 'default',
3401
- platform_user_id: 'user-1',
3402
- chat_id: 'chat-1',
3403
- display_name: 'User',
3404
- thread_id: 'thread-1',
3405
- }),
3406
- getPlatformThreadBinding: () => ({
3407
- workspace_id: 'default',
3408
- platform: 'lark',
3409
- chat_id: 'chat-1',
3410
- platform_user_id: 'user-1',
3411
- thread_id: 'thread-1',
3412
- last_platform_message_id: null,
3413
- }),
3414
- getLatestRunForThread: () => null,
3415
- clearPlatformThreadMessageId: () => { },
3416
- listPendingPairings: () => [],
3417
- listAuthorizedUsers: () => [],
3418
- },
3419
- readConfig: async () => ({
3420
- projects: [
3421
- {
3422
- name: 'default',
3423
- agent: { type: 'claudecode', providers: [] },
3424
- platforms: [{ type: 'lark', options: { app_id: 'app-1', app_secret: 'secret-1', auto_approve: true } }],
3425
- },
3426
- ],
3427
- }),
3428
- getWorkspaceRouter: () => ({
3429
- getThreadSessionKey: (threadId) => `session:${threadId}`,
3430
- sendThreadMessage: async () => {
3431
- sentMessages++;
3432
- return new Promise(() => { });
3433
- },
3434
- }),
3435
- eventBus: { emit: () => { }, on: () => () => { } },
3436
- log: (message) => logs.push(message),
3437
- });
3438
- gateway.larkModulePromise = Promise.resolve({
3439
- AppType: { SelfBuild: 'self-build' },
3440
- Domain: { Feishu: 'feishu' },
3441
- LoggerLevel: { info: 'info' },
3442
- Client: class {
3443
- },
3444
- EventDispatcher: class {
3445
- register(handlers) {
3446
- registeredHandlers = handlers;
3447
- }
3448
- },
3449
- WSClient: class {
3450
- async start() { }
3451
- },
3452
- });
3453
- await gateway.enable('default');
3454
- const handler = registeredHandlers['im.message.receive_v1'];
3455
- strict_1.default.equal(typeof handler, 'function');
3456
- const result = await Promise.race([
3457
- handler({
3458
- event: {
3459
- sender: { sender_id: { user_id: 'user-1' } },
3460
- message: {
3461
- message_id: 'msg-1',
3462
- chat_id: 'chat-1',
3463
- message_type: 'text',
3464
- content: JSON.stringify({ text: 'hi' }),
3465
- },
3466
- },
3467
- }),
3468
- new Promise((resolve) => setTimeout(() => resolve('timed-out'), 25)),
3469
- ]);
3470
- strict_1.default.notEqual(result, 'timed-out');
3471
- await new Promise((resolve) => setTimeout(resolve, 0));
3472
- strict_1.default.equal(sentMessages, 1);
3473
- strict_1.default.ok(logs.some((line) => line.includes('received im.message.receive_v1') &&
3474
- line.includes('message=msg-1') &&
3475
- line.includes('type=text') &&
3476
- line.includes('chat=chat-1') &&
3477
- line.includes('sender=user-1')));
3478
- strict_1.default.ok(logs.some((line) => line.includes('handling message event') && line.includes('contentBytes=')));
3479
- });
3480
- (0, node_test_1.default)('lark inbound messages use active runtime binding before config refresh catches up', async () => {
3481
- const users = new Map();
3482
- const threadBindings = new Map();
3483
- const createdThreads = [];
3484
- const updatedModes = [];
3485
- const sentCards = [];
3486
- const platformKey = 'lark:lark-hot';
3487
- const bindingKey = `${platformKey}:chat-1:user-1`;
3488
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
3489
- store: {
3490
- expirePendingPairings: () => { },
3491
- listPendingPairings: () => [],
3492
- listAuthorizedUsers: () => [...users.values()],
3493
- getAuthorizedUser: (_workspaceId, platformUserId, requestedPlatform) => users.get(`${requestedPlatform}:${platformUserId}`),
3494
- createAuthorizedUser: (user) => users.set(`${user.platform}:${user.platform_user_id}`, user),
3495
- updateAuthorizedUserThread: (_workspaceId, platformUserId, threadId, requestedPlatform) => {
3496
- users.set(`${requestedPlatform}:${platformUserId}`, { ...users.get(`${requestedPlatform}:${platformUserId}`), thread_id: threadId });
3497
- },
3498
- getPlatformThreadBinding: () => threadBindings.get(bindingKey),
3499
- upsertPlatformThreadBinding: (binding) => threadBindings.set(bindingKey, binding),
3500
- getThreadRow: (threadId) => ({ id: threadId, agent_mode: threadId === 'thread-1' ? 'bypassPermissions' : 'default' }),
3501
- updateThreadAgentMode: (threadId, mode) => updatedModes.push({ threadId, mode }),
3502
- getLatestRunForThread: () => null,
3503
- clearPlatformThreadMessageId: () => { },
3504
- },
3505
- readConfig: async () => ({ projects: [] }),
3506
- getWorkspaceRouter: () => ({
3507
- createThread: async (_workspaceId, title) => {
3508
- const id = `thread-${createdThreads.length + 1}`;
3509
- createdThreads.push(`${id}:${title}`);
3510
- return { id };
3511
- },
3512
- getThreadSessionKey: (threadId) => `session:${threadId}`,
3513
- sendThreadMessage: async () => ({ runId: 'run-1' }),
3514
- }),
3515
- eventBus: { emit: () => { }, on: () => () => { } },
3516
- });
3517
- const internals = gateway;
3518
- internals.runtime.set('project-1::lark-hot', {
3519
- workspaceId: 'project-1',
3520
- instanceId: 'lark-hot',
3521
- displayName: 'Lark Hot',
3522
- platformKey,
3523
- enabled: true,
3524
- status: 'running',
3525
- connected: true,
3526
- appId: 'app-1',
3527
- autoApprove: true,
3528
- cardActionsEnabled: true,
3529
- client: {
3530
- im: {
3531
- message: {
3532
- create: async (request) => {
3533
- sentCards.push(JSON.parse(String(request.data.content || '{}')));
3534
- return { data: { message_id: 'card-1' } };
3535
- },
3536
- },
3537
- messageReaction: {
3538
- create: async () => ({ data: { reaction_id: 'reaction-1' } }),
3539
- },
3540
- },
3541
- },
3542
- });
3543
- await gateway.handleInboundMessage({
3544
- workspaceId: 'project-1',
3545
- instanceId: 'lark-hot',
3546
- platformKey,
3547
- platformUserId: 'user-1',
3548
- chatId: 'chat-1',
3549
- displayName: 'User',
3550
- text: '/new',
3551
- messageId: 'msg-1',
3552
- });
3553
- strict_1.default.equal(users.get(`${platformKey}:user-1`)?.thread_id, 'thread-2');
3554
- strict_1.default.deepEqual(createdThreads.map((item) => item.split(':')[0]), ['thread-1', 'thread-2']);
3555
- strict_1.default.deepEqual(updatedModes, [{ threadId: 'thread-2', mode: 'bypassPermissions' }]);
3556
- strict_1.default.match(sentCards[0]?.elements?.[0]?.content || '', /已开始新会话/);
3557
- });
3558
- (0, node_test_1.default)('lark rendering suppresses noisy pending tool progress cards', () => {
3559
- const turn = (0, runtime_state_js_1.createLarkTurnState)('session-1');
3560
- const rendered = (0, runtime_state_js_1.renderLarkBridgeEventMessage)(turn, {
3561
- type: 'update_message',
3562
- sessionKey: 'session-1',
3563
- replyCtx: 'run-1',
3564
- previewHandle: 'tool-1',
3565
- bridgeKind: 'tool',
3566
- content: 'bash pending',
3567
- toolCall: {
3568
- id: 'tool-1',
3569
- name: 'bash',
3570
- status: 'pending',
3571
- output: '',
3572
- },
3573
- });
3574
- strict_1.default.equal(rendered.text, '');
3575
- strict_1.default.equal(rendered.key, 'noop');
3576
- const completed = (0, runtime_state_js_1.renderLarkBridgeEventMessage)(turn, {
3577
- type: 'update_message',
3578
- sessionKey: 'session-1',
3579
- replyCtx: 'run-1',
3580
- previewHandle: 'tool-1',
3581
- bridgeKind: 'tool',
3582
- content: 'bash completed',
3583
- toolCall: {
3584
- id: 'tool-1',
3585
- name: 'bash: Tool update',
3586
- status: 'completed',
3587
- output: 'verbose output',
3588
- },
3589
- });
3590
- strict_1.default.equal(completed.text, '🔧 bash: Tool update');
3591
- strict_1.default.equal(completed.delivery, 'message');
3592
- });
3593
- (0, node_test_1.default)('ACP skips empty generic running tool updates', () => {
3594
- const appended = [];
3595
- const emitted = [];
3596
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
3597
- appendMessage: (_threadId, _role, content) => appended.push(content),
3598
- emitBridge: (event) => emitted.push(event),
3599
- updateRunStatus: () => { },
3600
- sendRaw: () => true,
3601
- });
3602
- const session = {
3603
- threadId: 'thread-1',
3604
- bridgeSessionKey: 'session:thread-1',
3605
- currentRunId: 'run-1',
3606
- currentTurn: {
3607
- runId: 'run-1',
3608
- replyCtx: 'run-1',
3609
- previewHandle: 'preview-1',
3610
- assistantText: '',
3611
- typingStarted: true,
3612
- previewStarted: false,
3613
- permission: null,
3614
- },
3615
- loadReplayMode: false,
3616
- schedulerJobCreatedByRun: new Map(),
3617
- };
3618
- coordinator.handleAgentNotification(session, {
3619
- method: 'session/update',
3620
- params: {
3621
- update: {
3622
- sessionUpdate: 'tool_call_update',
3623
- title: 'Tool update',
3624
- status: 'running',
3625
- },
3626
- },
3627
- });
3628
- strict_1.default.deepEqual(appended, []);
3629
- strict_1.default.deepEqual(emitted, []);
3630
- });
3631
- (0, node_test_1.default)('ACP skips empty generic running updates even after a tool name', () => {
3632
- const appended = [];
3633
- const emitted = [];
3634
- const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
3635
- appendMessage: (_threadId, _role, content) => appended.push(content),
3636
- emitBridge: (event) => emitted.push(event),
3637
- updateRunStatus: () => { },
3638
- sendRaw: () => true,
3639
- });
3640
- const session = {
3641
- threadId: 'thread-1',
3642
- bridgeSessionKey: 'session:thread-1',
3643
- currentRunId: 'run-1',
3644
- currentTurn: {
3645
- runId: 'run-1',
3646
- replyCtx: 'run-1',
3647
- previewHandle: 'preview-1',
3648
- assistantText: '',
3649
- typingStarted: true,
3650
- previewStarted: false,
3651
- permission: null,
3652
- },
3653
- loadReplayMode: false,
3654
- schedulerJobCreatedByRun: new Map(),
3655
- };
3656
- coordinator.handleAgentNotification(session, {
3657
- method: 'session/update',
3658
- params: {
3659
- update: {
3660
- sessionUpdate: 'tool_call',
3661
- title: 'Terminal',
3662
- },
3663
- },
3664
- });
3665
- coordinator.handleAgentNotification(session, {
3666
- method: 'session/update',
3667
- params: {
3668
- update: {
3669
- sessionUpdate: 'tool_call_update',
3670
- title: 'Tool update',
3671
- status: 'running',
3672
- },
3673
- },
3674
- });
3675
- strict_1.default.deepEqual(appended, []);
3676
- strict_1.default.deepEqual(emitted, []);
3677
- strict_1.default.equal(session.currentTurn.pendingToolCallTitle, undefined);
3678
- });
3679
22
  (0, node_test_1.default)('response processor derives slash fallback replies and cron system responses', async () => {
3680
23
  const processor = new local_core_acp_response_processor_js_1.LocalCoreAcpResponseProcessor({
3681
24
  getScheduledDeliveryBinding: (threadId) => threadId === 'thread-1'
@@ -4388,890 +731,3 @@ function findLarkPostMdText(content) {
4388
731
  sessionKey: 'session:thread-origin',
4389
732
  });
4390
733
  });
4391
- (0, node_test_1.default)('weixin channel can request a QR code without platform options', async () => {
4392
- const originalFetch = globalThis.fetch;
4393
- const stateDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'weixin-channel-qr-'));
4394
- const requests = [];
4395
- globalThis.fetch = (async (input, init) => {
4396
- requests.push({
4397
- url: String(input),
4398
- headers: new Headers(init?.headers),
4399
- });
4400
- return new Response(JSON.stringify({
4401
- qrcode: 'ticket-1',
4402
- qrcode_img_content: 'https://liteapp.weixin.qq.com/q/test?qrcode=ticket-1&bot_type=3',
4403
- expired: 180,
4404
- }), {
4405
- status: 200,
4406
- headers: { 'Content-Type': 'application/json' },
4407
- });
4408
- });
4409
- try {
4410
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
4411
- store: {},
4412
- readConfig: async () => ({
4413
- projects: [
4414
- {
4415
- name: 'default',
4416
- agent: { type: 'localcore-acp', providers: [] },
4417
- platforms: [{ type: 'weixin', options: { state_dir: stateDir } }],
4418
- },
4419
- ],
4420
- }),
4421
- getWorkspaceRouter: () => ({}),
4422
- eventBus: { emit: () => { }, on: () => () => { } },
4423
- });
4424
- const result = await gateway.getQrCode('default');
4425
- strict_1.default.deepEqual(result, {
4426
- ticket: 'ticket-1',
4427
- expiresIn: 180,
4428
- qrCodeUrl: 'https://liteapp.weixin.qq.com/q/test?qrcode=ticket-1&bot_type=3',
4429
- instanceId: 'default',
4430
- displayName: 'WeChat 1',
4431
- });
4432
- strict_1.default.equal(requests[0]?.url, 'https://ilinkai.weixin.qq.com/ilink/bot/get_bot_qrcode?bot_type=3');
4433
- strict_1.default.equal(requests[0]?.headers.has('Authorization'), false);
4434
- strict_1.default.equal(requests[0]?.headers.has('AuthorizationType'), false);
4435
- }
4436
- finally {
4437
- globalThis.fetch = originalFetch;
4438
- (0, node_fs_1.rmSync)(stateDir, { recursive: true, force: true });
4439
- }
4440
- });
4441
- (0, node_test_1.default)('weixin QR confirmation persists credentials and starts authenticated polling', async () => {
4442
- const originalFetch = globalThis.fetch;
4443
- const stateDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'weixin-channel-'));
4444
- const requests = [];
4445
- globalThis.fetch = (async (input, init) => {
4446
- const url = String(input);
4447
- requests.push({
4448
- url,
4449
- headers: new Headers(init?.headers),
4450
- });
4451
- if (url.includes('/get_qrcode_status')) {
4452
- return new Response(JSON.stringify({
4453
- status: 'confirmed',
4454
- bot_token: 'bot-token-1',
4455
- baseurl: 'https://ilinkai.weixin.qq.com',
4456
- ilink_bot_id: 'bot-1',
4457
- ilink_user_id: 'user-1',
4458
- }), {
4459
- status: 200,
4460
- headers: { 'Content-Type': 'application/json' },
4461
- });
4462
- }
4463
- return new Promise((_resolve, reject) => {
4464
- init?.signal?.addEventListener('abort', () => reject(new Error('aborted')), { once: true });
4465
- });
4466
- });
4467
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
4468
- store: {
4469
- expirePendingPairings: () => { },
4470
- listPendingPairings: () => [],
4471
- listAuthorizedUsers: () => [],
4472
- },
4473
- readConfig: async () => ({
4474
- projects: [
4475
- {
4476
- name: 'default',
4477
- agent: { type: 'localcore-acp', providers: [] },
4478
- platforms: [{ type: 'weixin', options: { state_dir: stateDir } }],
4479
- },
4480
- ],
4481
- }),
4482
- getWorkspaceRouter: () => ({}),
4483
- eventBus: { emit: () => { }, on: () => () => { } },
4484
- });
4485
- try {
4486
- const result = await gateway.checkQrCodeStatus('default', 'ticket-1');
4487
- await new Promise((resolve) => setTimeout(resolve, 0));
4488
- strict_1.default.equal(result.status, 'confirmed');
4489
- const pollingRequest = requests.find((request) => request.url.endsWith('/ilink/bot/getupdates'));
4490
- strict_1.default.equal(pollingRequest?.headers.get('Authorization'), 'Bearer bot-token-1');
4491
- strict_1.default.equal(pollingRequest?.headers.get('AuthorizationType'), 'ilink_bot_token');
4492
- }
4493
- finally {
4494
- await gateway.stop();
4495
- globalThis.fetch = originalFetch;
4496
- (0, node_fs_1.rmSync)(stateDir, { recursive: true, force: true });
4497
- }
4498
- });
4499
- (0, node_test_1.default)('lark channel can request an official app registration QR code without extra setup', async () => {
4500
- const originalFetch = globalThis.fetch;
4501
- const requests = [];
4502
- globalThis.fetch = (async (input, init) => {
4503
- requests.push({
4504
- url: String(input),
4505
- method: init?.method,
4506
- body: String(init?.body || ''),
4507
- headers: new Headers(init?.headers),
4508
- });
4509
- return new Response(JSON.stringify({
4510
- device_code: 'device-code-1',
4511
- user_code: 'ABCD-EFGH',
4512
- expires_in: 300,
4513
- interval: 5,
4514
- }), {
4515
- status: 200,
4516
- headers: { 'Content-Type': 'application/json' },
4517
- });
4518
- });
4519
- try {
4520
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
4521
- store: {},
4522
- readConfig: async () => ({
4523
- projects: [
4524
- {
4525
- name: 'default',
4526
- agent: { type: 'localcore-acp', providers: [] },
4527
- platforms: [{ type: 'lark', options: {} }],
4528
- },
4529
- ],
4530
- }),
4531
- getWorkspaceRouter: () => ({}),
4532
- eventBus: { emit: () => { }, on: () => () => { } },
4533
- });
4534
- const result = await gateway.getQrCode('default');
4535
- strict_1.default.deepEqual(result, {
4536
- ticket: 'device-code-1',
4537
- expiresIn: 300,
4538
- interval: 5,
4539
- qrCodeUrl: 'https://open.feishu.cn/page/openclaw?user_code=ABCD-EFGH&from=openclaw',
4540
- instanceId: 'default',
4541
- displayName: 'Lark 1',
4542
- });
4543
- strict_1.default.equal(requests[0]?.url, 'https://accounts.feishu.cn/oauth/v1/app/registration');
4544
- strict_1.default.equal(requests[0]?.method, 'POST');
4545
- strict_1.default.equal(requests[0]?.headers.get('Content-Type'), 'application/x-www-form-urlencoded');
4546
- strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('action'), 'begin');
4547
- strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('archetype'), 'PersonalAgent');
4548
- strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('request_callbacks'), null);
4549
- }
4550
- finally {
4551
- globalThis.fetch = originalFetch;
4552
- }
4553
- });
4554
- (0, node_test_1.default)('lark app registration QR confirmation returns app credentials', async () => {
4555
- const originalFetch = globalThis.fetch;
4556
- const requests = [];
4557
- globalThis.fetch = (async (input, init) => {
4558
- const url = String(input);
4559
- requests.push({ url, body: String(init?.body || '') });
4560
- return new Response(JSON.stringify({
4561
- client_id: 'cli_lark_1',
4562
- client_secret: 'secret-1',
4563
- }), {
4564
- status: 200,
4565
- headers: { 'Content-Type': 'application/json' },
4566
- });
4567
- });
4568
- try {
4569
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
4570
- store: {},
4571
- readConfig: async () => ({
4572
- projects: [
4573
- {
4574
- name: 'default',
4575
- agent: { type: 'localcore-acp', providers: [] },
4576
- platforms: [{ type: 'lark', options: {} }],
4577
- },
4578
- ],
4579
- }),
4580
- getWorkspaceRouter: () => ({}),
4581
- eventBus: { emit: () => { }, on: () => () => { } },
4582
- });
4583
- const result = await gateway.checkQrCodeStatus('default', 'lark-ticket-1');
4584
- strict_1.default.equal(requests[0]?.url, 'https://accounts.feishu.cn/oauth/v1/app/registration');
4585
- strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('action'), 'poll');
4586
- strict_1.default.equal(new URLSearchParams(requests[0]?.body).get('device_code'), 'lark-ticket-1');
4587
- strict_1.default.deepEqual(result, {
4588
- status: 'confirmed',
4589
- credentials: {
4590
- appId: 'cli_lark_1',
4591
- appSecret: 'secret-1',
4592
- },
4593
- });
4594
- }
4595
- finally {
4596
- globalThis.fetch = originalFetch;
4597
- }
4598
- });
4599
- (0, node_test_1.default)('lark channel keeps multiple bot instances in one workspace isolated', async () => {
4600
- const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
4601
- store: {
4602
- expirePendingPairings: () => { },
4603
- listPendingPairings: () => [],
4604
- listAuthorizedUsers: () => [],
4605
- },
4606
- readConfig: async () => ({
4607
- projects: [
4608
- {
4609
- name: 'default',
4610
- agent: { type: 'localcore-acp', providers: [] },
4611
- platforms: [
4612
- { type: 'lark', options: { instance_id: 'bot-a', app_id: 'cli_a', app_secret: 'secret-a' } },
4613
- { type: 'lark', options: { instance_id: 'bot-b', app_id: 'cli_b', app_secret: 'secret-b' } },
4614
- ],
4615
- },
4616
- ],
4617
- }),
4618
- getWorkspaceRouter: () => ({}),
4619
- eventBus: { emit: () => { }, on: () => () => { } },
4620
- });
4621
- gateway.larkModulePromise = Promise.resolve({
4622
- AppType: { SelfBuild: 'self-build' },
4623
- Domain: { Feishu: 'feishu' },
4624
- LoggerLevel: { info: 'info' },
4625
- Client: class {
4626
- },
4627
- EventDispatcher: class {
4628
- register() { }
4629
- },
4630
- WSClient: class {
4631
- async start() { }
4632
- },
4633
- });
4634
- await gateway.refreshBindings();
4635
- const statuses = gateway.listStatuses();
4636
- strict_1.default.deepEqual(statuses.map((status) => [status.workspaceId, status.instanceId, status.appId, status.status]), [
4637
- ['default', 'bot-a', 'cli_a', 'running'],
4638
- ['default', 'bot-b', 'cli_b', 'running'],
4639
- ]);
4640
- });
4641
- (0, node_test_1.default)('weixin channel keeps multiple bot instances in one workspace isolated', async () => {
4642
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
4643
- store: {
4644
- expirePendingPairings: () => { },
4645
- listPendingPairings: () => [],
4646
- listAuthorizedUsers: () => [],
4647
- },
4648
- readConfig: async () => ({
4649
- projects: [
4650
- {
4651
- name: 'default',
4652
- agent: { type: 'localcore-acp', providers: [] },
4653
- platforms: [
4654
- { type: 'weixin', options: { instance_id: 'wx-a', account_id: 'account-a' } },
4655
- { type: 'weixin', options: { instance_id: 'wx-b', account_id: 'account-b' } },
4656
- ],
4657
- },
4658
- ],
4659
- }),
4660
- getWorkspaceRouter: () => ({}),
4661
- eventBus: { emit: () => { }, on: () => () => { } },
4662
- });
4663
- await gateway.refreshBindings();
4664
- const statuses = gateway.listStatuses();
4665
- strict_1.default.deepEqual(statuses.map((status) => [status.workspaceId, status.instanceId, status.appId, status.status]), [
4666
- ['default', 'wx-a', 'account-a', 'stopped'],
4667
- ['default', 'wx-b', 'account-b', 'stopped'],
4668
- ]);
4669
- });
4670
- (0, node_test_1.default)('weixin inbound message handling is idempotent by message identity', async () => {
4671
- const sentThreadMessages = [];
4672
- const users = new Map();
4673
- const threadBindings = new Map();
4674
- const bindingKey = 'default:chat-1:user-1';
4675
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
4676
- store: {
4677
- expirePendingPairings: () => { },
4678
- listPendingPairings: () => [],
4679
- listPairingRequests: () => [],
4680
- listAuthorizedUsers: () => [...users.values()],
4681
- getAuthorizedUser: (_workspaceId, platformUserId) => users.get(platformUserId),
4682
- createAuthorizedUser: (user) => users.set(user.platform_user_id, user),
4683
- updateAuthorizedUserThread: (_workspaceId, platformUserId, threadId) => {
4684
- users.set(platformUserId, { ...users.get(platformUserId), thread_id: threadId });
4685
- },
4686
- getPlatformThreadBinding: () => threadBindings.get(bindingKey),
4687
- upsertPlatformThreadBinding: (binding) => threadBindings.set(bindingKey, binding),
4688
- updatePlatformThreadMessageId: (_workspaceId, _chatId, _platformUserId, messageId) => {
4689
- threadBindings.set(bindingKey, { ...threadBindings.get(bindingKey), last_platform_message_id: messageId });
4690
- },
4691
- getLatestRunForThread: () => null,
4692
- },
4693
- readConfig: async () => ({
4694
- projects: [
4695
- {
4696
- name: 'default',
4697
- agent: { type: 'localcore-acp', providers: [] },
4698
- platforms: [{ type: 'weixin', options: {} }],
4699
- },
4700
- ],
4701
- }),
4702
- getWorkspaceRouter: () => ({
4703
- createThread: async () => ({ id: 'thread-1' }),
4704
- getThreadSessionKey: (threadId) => `session:${threadId}`,
4705
- sendThreadMessage: async (_threadId, text) => {
4706
- sentThreadMessages.push(text);
4707
- return { runId: 'run-1' };
4708
- },
4709
- }),
4710
- eventBus: { emit: () => { }, on: () => () => { } },
4711
- });
4712
- const input = {
4713
- workspaceId: 'default',
4714
- platformUserId: 'user-1',
4715
- chatId: 'chat-1',
4716
- displayName: 'User',
4717
- text: 'hello',
4718
- messageId: 'msg-1',
4719
- contextToken: 'ctx-1',
4720
- };
4721
- await gateway.handleInboundMessage(input);
4722
- await gateway.handleInboundMessage(input);
4723
- strict_1.default.equal(sentThreadMessages.length, 1);
4724
- strict_1.default.match(sentThreadMessages[0] || '', /hello/);
4725
- });
4726
- (0, node_test_1.default)('weixin downloaded file attachment becomes a structured file content part', () => {
4727
- const part = (0, local_core_weixin_gateway_js_1.createWeixinAttachmentContentPart)({
4728
- path: '/tmp/report.pdf',
4729
- kind: 'file',
4730
- name: 'report.pdf',
4731
- });
4732
- strict_1.default.deepEqual(part, {
4733
- type: 'file',
4734
- path: '/tmp/report.pdf',
4735
- fileName: 'report.pdf',
4736
- });
4737
- });
4738
- (0, node_test_1.default)('weixin downloaded image attachment keeps image data content part', () => {
4739
- const part = (0, local_core_weixin_gateway_js_1.createWeixinAttachmentContentPart)({
4740
- path: '/tmp/image.png',
4741
- kind: 'image',
4742
- name: 'image.png',
4743
- data: 'aW1n',
4744
- mimeType: 'image/png',
4745
- });
4746
- strict_1.default.deepEqual(part, {
4747
- type: 'image',
4748
- data: 'aW1n',
4749
- mimeType: 'image/png',
4750
- fileName: 'image.png',
4751
- });
4752
- });
4753
- (0, node_test_1.default)('weixin bridge skips duplicate rendered replies', async () => {
4754
- const originalFetch = globalThis.fetch;
4755
- const sentBodies = [];
4756
- globalThis.fetch = (async (_input, init) => {
4757
- sentBodies.push(JSON.parse(String(init?.body || '{}')));
4758
- return new Response(JSON.stringify({ errcode: 0 }), {
4759
- status: 200,
4760
- headers: { 'Content-Type': 'application/json' },
4761
- });
4762
- });
4763
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
4764
- store: {
4765
- getPlatformThreadBinding: () => ({
4766
- workspace_id: 'default',
4767
- platform: 'weixin',
4768
- chat_id: 'chat-1',
4769
- platform_user_id: 'user-1',
4770
- thread_id: 'thread-1',
4771
- last_platform_message_id: 'ctx-1',
4772
- }),
4773
- },
4774
- readConfig: async () => ({
4775
- projects: [
4776
- {
4777
- name: 'default',
4778
- agent: { type: 'localcore-acp', providers: [] },
4779
- platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
4780
- },
4781
- ],
4782
- }),
4783
- getWorkspaceRouter: () => ({}),
4784
- eventBus: { emit: () => { }, on: () => () => { } },
4785
- });
4786
- const internals = gateway;
4787
- internals.runtime.set('default', {
4788
- workspaceId: 'default',
4789
- enabled: true,
4790
- status: 'running',
4791
- connected: true,
4792
- accountId: 'bot-1',
4793
- });
4794
- internals.threadRouting.set('session:thread-1', {
4795
- workspaceId: 'default',
4796
- platformUserId: 'user-1',
4797
- chatId: 'chat-1',
4798
- threadId: 'thread-1',
4799
- });
4800
- try {
4801
- await gateway.onBridgeEvent({ type: 'update_message', sessionKey: 'session:thread-1', content: 'same reply' });
4802
- await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'same reply' });
4803
- await gateway.onBridgeEvent({ type: 'typing_stop', sessionKey: 'session:thread-1' });
4804
- await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'same reply' });
4805
- strict_1.default.equal(sentBodies.length, 1);
4806
- strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, 'same reply');
4807
- }
4808
- finally {
4809
- globalThis.fetch = originalFetch;
4810
- }
4811
- });
4812
- (0, node_test_1.default)('weixin bridge keeps context replies to one truncated text message', async () => {
4813
- const originalFetch = globalThis.fetch;
4814
- const sentBodies = [];
4815
- globalThis.fetch = (async (_input, init) => {
4816
- sentBodies.push(JSON.parse(String(init?.body || '{}')));
4817
- return new Response(JSON.stringify({ errcode: 0 }), {
4818
- status: 200,
4819
- headers: { 'Content-Type': 'application/json' },
4820
- });
4821
- });
4822
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
4823
- store: {
4824
- getPlatformThreadBinding: () => ({
4825
- workspace_id: 'default',
4826
- platform: 'weixin',
4827
- chat_id: 'chat-1',
4828
- platform_user_id: 'user-1',
4829
- thread_id: 'thread-1',
4830
- last_platform_message_id: 'ctx-1',
4831
- }),
4832
- },
4833
- readConfig: async () => ({
4834
- projects: [
4835
- {
4836
- name: 'default',
4837
- agent: { type: 'localcore-acp', providers: [] },
4838
- platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
4839
- },
4840
- ],
4841
- }),
4842
- getWorkspaceRouter: () => ({}),
4843
- eventBus: { emit: () => { }, on: () => () => { } },
4844
- });
4845
- const internals = gateway;
4846
- internals.runtime.set('default', {
4847
- workspaceId: 'default',
4848
- enabled: true,
4849
- status: 'running',
4850
- connected: true,
4851
- accountId: 'bot-1',
4852
- });
4853
- internals.threadRouting.set('session:thread-1', {
4854
- workspaceId: 'default',
4855
- platformUserId: 'user-1',
4856
- chatId: 'chat-1',
4857
- threadId: 'thread-1',
4858
- });
4859
- try {
4860
- await gateway.onBridgeEvent({
4861
- type: 'reply',
4862
- sessionKey: 'session:thread-1',
4863
- content: Array.from({ length: 80 }, (_, index) => `第 ${index + 1} 行:这是一段用于测试微信长文本切分的内容。`).join('\n\n'),
4864
- });
4865
- strict_1.default.equal(sentBodies.length, 1);
4866
- strict_1.default.equal(sentBodies[0]?.msg?.context_token, 'ctx-1');
4867
- for (const body of sentBodies) {
4868
- const text = body?.msg?.item_list?.[0]?.text_item?.text || '';
4869
- strict_1.default.ok(Buffer.byteLength(text, 'utf-8') <= 3500);
4870
- strict_1.default.match(text, /内容过长,已截断以保证微信送达/);
4871
- strict_1.default.equal(body?.base_info?.channel_version, '2.1.7');
4872
- strict_1.default.equal(body?.msg?.from_user_id, '');
4873
- strict_1.default.match(body?.msg?.client_id || '', /^openclaw-weixin-/);
4874
- }
4875
- }
4876
- finally {
4877
- globalThis.fetch = originalFetch;
4878
- }
4879
- });
4880
- (0, node_test_1.default)('weixin bridge sends protocol-compatible final reply payload', async () => {
4881
- const originalFetch = globalThis.fetch;
4882
- const sentRequests = [];
4883
- globalThis.fetch = (async (_input, init) => {
4884
- const body = JSON.parse(String(init?.body || '{}'));
4885
- sentRequests.push({ body, headers: new Headers(init?.headers) });
4886
- return new Response(JSON.stringify({ ret: 0 }), {
4887
- status: 200,
4888
- headers: { 'Content-Type': 'application/json' },
4889
- });
4890
- });
4891
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
4892
- store: {
4893
- getPlatformThreadBinding: () => ({
4894
- workspace_id: 'default',
4895
- platform: 'weixin',
4896
- chat_id: 'chat-1',
4897
- platform_user_id: 'user-1',
4898
- thread_id: 'thread-1',
4899
- last_platform_message_id: 'ctx-1',
4900
- }),
4901
- },
4902
- readConfig: async () => ({
4903
- projects: [
4904
- {
4905
- name: 'default',
4906
- agent: { type: 'localcore-acp', providers: [] },
4907
- platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
4908
- },
4909
- ],
4910
- }),
4911
- getWorkspaceRouter: () => ({}),
4912
- eventBus: { emit: () => { }, on: () => () => { } },
4913
- });
4914
- const internals = gateway;
4915
- internals.runtime.set('default', {
4916
- workspaceId: 'default',
4917
- enabled: true,
4918
- status: 'running',
4919
- connected: true,
4920
- accountId: 'bot-1',
4921
- });
4922
- internals.threadRouting.set('session:thread-1', {
4923
- workspaceId: 'default',
4924
- platformUserId: 'user-1',
4925
- chatId: 'chat-1',
4926
- threadId: 'thread-1',
4927
- });
4928
- try {
4929
- await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'final reply' });
4930
- strict_1.default.equal(sentRequests.length, 1);
4931
- strict_1.default.equal(sentRequests[0]?.body?.msg?.context_token, 'ctx-1');
4932
- strict_1.default.equal(sentRequests[0]?.body?.msg?.from_user_id, '');
4933
- strict_1.default.equal(sentRequests[0]?.body?.msg?.message_state, 2);
4934
- strict_1.default.equal(sentRequests[0]?.body?.base_info?.channel_version, '2.1.7');
4935
- strict_1.default.match(sentRequests[0]?.body?.msg?.client_id || '', /^openclaw-weixin-/);
4936
- strict_1.default.equal(sentRequests[0]?.body?.msg?.item_list?.[0]?.text_item?.text, 'final reply');
4937
- strict_1.default.equal(sentRequests[0]?.headers.get('iLink-App-Id'), 'bot');
4938
- strict_1.default.equal(sentRequests[0]?.headers.get('iLink-App-ClientVersion'), '131335');
4939
- strict_1.default.equal(sentRequests[0]?.headers.get('AuthorizationType'), 'ilink_bot_token');
4940
- strict_1.default.equal(sentRequests[0]?.headers.get('Authorization'), 'Bearer bot-token-1');
4941
- }
4942
- finally {
4943
- globalThis.fetch = originalFetch;
4944
- }
4945
- });
4946
- (0, node_test_1.default)('weixin bridge sends status events in real time', async () => {
4947
- const originalFetch = globalThis.fetch;
4948
- const sentBodies = [];
4949
- globalThis.fetch = (async (_input, init) => {
4950
- sentBodies.push(JSON.parse(String(init?.body || '{}')));
4951
- return new Response(JSON.stringify({ ret: 0 }), {
4952
- status: 200,
4953
- headers: { 'Content-Type': 'application/json' },
4954
- });
4955
- });
4956
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
4957
- store: {
4958
- getPlatformThreadBinding: () => ({
4959
- workspace_id: 'default',
4960
- platform: 'weixin',
4961
- chat_id: 'chat-1',
4962
- platform_user_id: 'user-1',
4963
- thread_id: 'thread-1',
4964
- last_platform_message_id: 'ctx-1',
4965
- }),
4966
- },
4967
- readConfig: async () => ({
4968
- projects: [
4969
- {
4970
- name: 'default',
4971
- agent: { type: 'localcore-acp', providers: [] },
4972
- platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
4973
- },
4974
- ],
4975
- }),
4976
- getWorkspaceRouter: () => ({}),
4977
- eventBus: { emit: () => { }, on: () => () => { } },
4978
- });
4979
- const internals = gateway;
4980
- internals.runtime.set('default', {
4981
- workspaceId: 'default',
4982
- enabled: true,
4983
- status: 'running',
4984
- connected: true,
4985
- accountId: 'bot-1',
4986
- });
4987
- internals.threadRouting.set('session:thread-1', {
4988
- workspaceId: 'default',
4989
- platformUserId: 'user-1',
4990
- chatId: 'chat-1',
4991
- threadId: 'thread-1',
4992
- });
4993
- try {
4994
- await gateway.onBridgeEvent({ type: 'status', sessionKey: 'session:thread-1', content: '正在检查桌面文件' });
4995
- strict_1.default.equal(sentBodies.length, 1);
4996
- strict_1.default.equal(sentBodies[0]?.msg?.context_token, 'ctx-1');
4997
- strict_1.default.equal(sentBodies[0]?.msg?.message_state, 2);
4998
- strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, '正在检查桌面文件');
4999
- }
5000
- finally {
5001
- globalThis.fetch = originalFetch;
5002
- }
5003
- });
5004
- (0, node_test_1.default)('weixin bridge sends tool progress in real time before final reply', async () => {
5005
- const originalFetch = globalThis.fetch;
5006
- const sentBodies = [];
5007
- globalThis.fetch = (async (_input, init) => {
5008
- sentBodies.push(JSON.parse(String(init?.body || '{}')));
5009
- return new Response(JSON.stringify({ ret: 0 }), {
5010
- status: 200,
5011
- headers: { 'Content-Type': 'application/json' },
5012
- });
5013
- });
5014
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
5015
- store: {
5016
- getPlatformThreadBinding: () => ({
5017
- workspace_id: 'default',
5018
- platform: 'weixin',
5019
- chat_id: 'chat-1',
5020
- platform_user_id: 'user-1',
5021
- thread_id: 'thread-1',
5022
- last_platform_message_id: 'ctx-1',
5023
- }),
5024
- },
5025
- readConfig: async () => ({
5026
- projects: [
5027
- {
5028
- name: 'default',
5029
- agent: { type: 'localcore-acp', providers: [] },
5030
- platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
5031
- },
5032
- ],
5033
- }),
5034
- getWorkspaceRouter: () => ({}),
5035
- eventBus: { emit: () => { }, on: () => () => { } },
5036
- });
5037
- const internals = gateway;
5038
- internals.runtime.set('default', {
5039
- workspaceId: 'default',
5040
- enabled: true,
5041
- status: 'running',
5042
- connected: true,
5043
- accountId: 'bot-1',
5044
- });
5045
- internals.threadRouting.set('session:thread-1', {
5046
- workspaceId: 'default',
5047
- platformUserId: 'user-1',
5048
- chatId: 'chat-1',
5049
- threadId: 'thread-1',
5050
- });
5051
- try {
5052
- await gateway.onBridgeEvent({
5053
- type: 'reply',
5054
- sessionKey: 'session:thread-1',
5055
- bridgeKind: 'tool',
5056
- content: 'list desktop',
5057
- toolCall: {
5058
- name: 'list desktop',
5059
- status: 'running',
5060
- output: '',
5061
- },
5062
- });
5063
- await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'final reply' });
5064
- strict_1.default.equal(sentBodies.length, 2);
5065
- strict_1.default.equal(sentBodies[0]?.msg?.message_state, 2);
5066
- strict_1.default.equal(sentBodies[1]?.msg?.message_state, 2);
5067
- strict_1.default.equal(sentBodies[0]?.msg?.context_token, 'ctx-1');
5068
- strict_1.default.equal(sentBodies[1]?.msg?.context_token, 'ctx-1');
5069
- strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, '**处理中**\n• list desktop - running');
5070
- strict_1.default.equal(sentBodies[1]?.msg?.item_list?.[0]?.text_item?.text, 'final reply');
5071
- }
5072
- finally {
5073
- globalThis.fetch = originalFetch;
5074
- }
5075
- });
5076
- (0, node_test_1.default)('weixin bridge skips completed tool result updates but keeps final reply', async () => {
5077
- const originalFetch = globalThis.fetch;
5078
- const sentBodies = [];
5079
- globalThis.fetch = (async (_input, init) => {
5080
- sentBodies.push(JSON.parse(String(init?.body || '{}')));
5081
- return new Response(JSON.stringify({ errcode: 0 }), {
5082
- status: 200,
5083
- headers: { 'Content-Type': 'application/json' },
5084
- });
5085
- });
5086
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
5087
- store: {
5088
- getPlatformThreadBinding: () => ({
5089
- workspace_id: 'default',
5090
- platform: 'weixin',
5091
- chat_id: 'chat-1',
5092
- platform_user_id: 'user-1',
5093
- thread_id: 'thread-1',
5094
- last_platform_message_id: 'ctx-1',
5095
- }),
5096
- },
5097
- readConfig: async () => ({
5098
- projects: [
5099
- {
5100
- name: 'default',
5101
- agent: { type: 'localcore-acp', providers: [] },
5102
- platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
5103
- },
5104
- ],
5105
- }),
5106
- getWorkspaceRouter: () => ({}),
5107
- eventBus: { emit: () => { }, on: () => () => { } },
5108
- });
5109
- const internals = gateway;
5110
- internals.runtime.set('default', {
5111
- workspaceId: 'default',
5112
- enabled: true,
5113
- status: 'running',
5114
- connected: true,
5115
- accountId: 'bot-1',
5116
- });
5117
- internals.threadRouting.set('session:thread-1', {
5118
- workspaceId: 'default',
5119
- platformUserId: 'user-1',
5120
- chatId: 'chat-1',
5121
- threadId: 'thread-1',
5122
- });
5123
- try {
5124
- await gateway.onBridgeEvent({
5125
- type: 'reply',
5126
- sessionKey: 'session:thread-1',
5127
- bridgeKind: 'tool',
5128
- content: 'Tool update completed',
5129
- toolCall: {
5130
- name: 'Tool update',
5131
- status: 'completed',
5132
- output: '/Users/mochuxian/Desktop has many files and this result should not be sent',
5133
- },
5134
- });
5135
- await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'final reply' });
5136
- strict_1.default.equal(sentBodies.length, 1);
5137
- strict_1.default.equal(sentBodies[0]?.msg?.message_state, 2);
5138
- strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, 'final reply');
5139
- }
5140
- finally {
5141
- globalThis.fetch = originalFetch;
5142
- }
5143
- });
5144
- (0, node_test_1.default)('weixin bridge keeps failed tool update status without execution details', async () => {
5145
- const originalFetch = globalThis.fetch;
5146
- const sentBodies = [];
5147
- globalThis.fetch = (async (_input, init) => {
5148
- sentBodies.push(JSON.parse(String(init?.body || '{}')));
5149
- return new Response(JSON.stringify({ errcode: 0 }), {
5150
- status: 200,
5151
- headers: { 'Content-Type': 'application/json' },
5152
- });
5153
- });
5154
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
5155
- store: {
5156
- getPlatformThreadBinding: () => ({
5157
- workspace_id: 'default',
5158
- platform: 'weixin',
5159
- chat_id: 'chat-1',
5160
- platform_user_id: 'user-1',
5161
- thread_id: 'thread-1',
5162
- last_platform_message_id: 'ctx-1',
5163
- }),
5164
- },
5165
- readConfig: async () => ({
5166
- projects: [
5167
- {
5168
- name: 'default',
5169
- agent: { type: 'localcore-acp', providers: [] },
5170
- platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
5171
- },
5172
- ],
5173
- }),
5174
- getWorkspaceRouter: () => ({}),
5175
- eventBus: { emit: () => { }, on: () => () => { } },
5176
- });
5177
- const internals = gateway;
5178
- internals.runtime.set('default', {
5179
- workspaceId: 'default',
5180
- enabled: true,
5181
- status: 'running',
5182
- connected: true,
5183
- accountId: 'bot-1',
5184
- });
5185
- internals.threadRouting.set('session:thread-1', {
5186
- workspaceId: 'default',
5187
- platformUserId: 'user-1',
5188
- chatId: 'chat-1',
5189
- threadId: 'thread-1',
5190
- });
5191
- try {
5192
- await gateway.onBridgeEvent({
5193
- type: 'reply',
5194
- sessionKey: 'session:thread-1',
5195
- bridgeKind: 'tool',
5196
- content: 'Tool update failed',
5197
- toolCall: {
5198
- name: 'Tool update',
5199
- status: 'failed',
5200
- output: 'stack trace and command output should not be sent',
5201
- },
5202
- });
5203
- strict_1.default.equal(sentBodies.length, 1);
5204
- strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, '**处理中**\n• Tool update - failed');
5205
- }
5206
- finally {
5207
- globalThis.fetch = originalFetch;
5208
- }
5209
- });
5210
- (0, node_test_1.default)('weixin bridge folds progress after nine context sends and preserves final reply', async () => {
5211
- const originalFetch = globalThis.fetch;
5212
- const sentBodies = [];
5213
- globalThis.fetch = (async (_input, init) => {
5214
- sentBodies.push(JSON.parse(String(init?.body || '{}')));
5215
- return new Response(JSON.stringify({ ret: 0 }), {
5216
- status: 200,
5217
- headers: { 'Content-Type': 'application/json' },
5218
- });
5219
- });
5220
- const gateway = new local_core_weixin_gateway_js_1.LocalCoreWeixinGateway({
5221
- store: {
5222
- getPlatformThreadBinding: () => ({
5223
- workspace_id: 'default',
5224
- platform: 'weixin',
5225
- chat_id: 'chat-1',
5226
- platform_user_id: 'user-1',
5227
- thread_id: 'thread-1',
5228
- last_platform_message_id: 'ctx-1',
5229
- }),
5230
- },
5231
- readConfig: async () => ({
5232
- projects: [
5233
- {
5234
- name: 'default',
5235
- agent: { type: 'localcore-acp', providers: [] },
5236
- platforms: [{ type: 'weixin', options: { token: 'bot-token-1' } }],
5237
- },
5238
- ],
5239
- }),
5240
- getWorkspaceRouter: () => ({}),
5241
- eventBus: { emit: () => { }, on: () => () => { } },
5242
- });
5243
- const internals = gateway;
5244
- internals.runtime.set('default', {
5245
- workspaceId: 'default',
5246
- enabled: true,
5247
- status: 'running',
5248
- connected: true,
5249
- accountId: 'bot-1',
5250
- });
5251
- internals.threadRouting.set('session:thread-1', {
5252
- workspaceId: 'default',
5253
- platformUserId: 'user-1',
5254
- chatId: 'chat-1',
5255
- threadId: 'thread-1',
5256
- });
5257
- try {
5258
- for (let index = 1; index <= 12; index += 1) {
5259
- await gateway.onBridgeEvent({
5260
- type: 'reply',
5261
- sessionKey: 'session:thread-1',
5262
- bridgeKind: 'status',
5263
- content: `🔧 tool ${index}`,
5264
- });
5265
- }
5266
- await gateway.onBridgeEvent({ type: 'reply', sessionKey: 'session:thread-1', content: 'final reply' });
5267
- strict_1.default.equal(sentBodies.length, 10);
5268
- strict_1.default.equal(sentBodies[0]?.msg?.item_list?.[0]?.text_item?.text, '**处理中**\n• 🔧 tool 1');
5269
- strict_1.default.match(sentBodies[8]?.msg?.item_list?.[0]?.text_item?.text, /🔧 tool 9/);
5270
- strict_1.default.doesNotMatch(sentBodies.map((body) => body?.msg?.item_list?.[0]?.text_item?.text || '').join('\n'), /🔧 tool 10|🔧 tool 11|🔧 tool 12/);
5271
- strict_1.default.match(sentBodies[9]?.msg?.item_list?.[0]?.text_item?.text, /已省略 3 条过程消息/);
5272
- strict_1.default.match(sentBodies[9]?.msg?.item_list?.[0]?.text_item?.text, /final reply/);
5273
- }
5274
- finally {
5275
- globalThis.fetch = originalFetch;
5276
- }
5277
- });