@kafca/agentdock 0.1.46 → 0.1.48

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 (103) hide show
  1. package/README.md +11 -0
  2. package/dist/renderer/assets/{Badge-BIesaDYG.js → Badge-C92lMj9Q.js} +1 -1
  3. package/dist/renderer/assets/{Chat-CpataoDh.js → Chat-DqoKCPwz.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-_Mdmltgv.js → ChatMarkdown-C4psQvmt.js} +1 -1
  5. package/dist/renderer/assets/{Config-Pe5-zKZk.js → Config-1uk8FQL7.js} +2 -2
  6. package/dist/renderer/assets/{CronList-BnaOSPT8.js → CronList-DnKCiYWC.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-D05l8GKE.js → Dashboard-rOAQcJA0.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-BDBavDll.js → EmptyState-bF6Sc61O.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-DvR798l_.js → KnowledgeDetail-C5-Dp95R.js} +2 -2
  10. package/dist/renderer/assets/{KnowledgeHome-r2ZuvU8-.js → KnowledgeHome-DGa1EWXD.js} +2 -2
  11. package/dist/renderer/assets/{Logs-BaljtmAV.js → Logs-CKKVW2k8.js} +1 -1
  12. package/dist/renderer/assets/{Modal-CXhD_OWR.js → Modal-DIc9HxD5.js} +3 -3
  13. package/dist/renderer/assets/{MonitorList-D7tyipbx.js → MonitorList-D2YOJRIX.js} +1 -1
  14. package/dist/renderer/assets/{Page-KUuFYgjg.js → Page-vZ-_l6Kn.js} +1 -1
  15. package/dist/renderer/assets/ProjectDetail-CqTa6kAh.js +11 -0
  16. package/dist/renderer/assets/{ProjectList-CbdHd5Ev.js → ProjectList-LgDQVVyZ.js} +1 -1
  17. package/dist/renderer/assets/{Select-Djc8BXMC.js → Select-BN2HMOmY.js} +1 -1
  18. package/dist/renderer/assets/SessionChat-Beco1N1T.js +1 -0
  19. package/dist/renderer/assets/{SessionList-DHgl1s1D.js → SessionList-3NkskCJJ.js} +1 -1
  20. package/dist/renderer/assets/ThreadChat-Ji8pOm5p.js +28 -0
  21. package/dist/renderer/assets/{Workspace-DTocqY-F.js → Workspace-yarrGItB.js} +1 -1
  22. package/dist/renderer/assets/{arrow-left-CXyya3hP.js → arrow-left-DNovvfPp.js} +1 -1
  23. package/dist/renderer/assets/{book-open-9n4Kxc7E.js → book-open-CarZFsXv.js} +1 -1
  24. package/dist/renderer/assets/{bot-C9HBk5XH.js → bot-g16rGMp6.js} +1 -1
  25. package/dist/renderer/assets/{check-CutSg9a5.js → check-DjFX73Zi.js} +1 -1
  26. package/dist/renderer/assets/{chevron-down-DrydR4i3.js → chevron-down-B02YlQS_.js} +1 -1
  27. package/dist/renderer/assets/{circle-BEjtahw6.js → circle-DPfWdG1F.js} +1 -1
  28. package/dist/renderer/assets/{desktop-DATgk_ka.js → desktop-DMkue1T6.js} +1 -1
  29. package/dist/renderer/assets/{funnel-BhqZpS4f.js → funnel-Dy1VL-D9.js} +1 -1
  30. package/dist/renderer/assets/{heart-Dp8Vs-7_.js → heart-YjvGwq5-.js} +1 -1
  31. package/dist/renderer/assets/{index-BKpv0P0s.js → index-CZN1STMc.js} +2 -2
  32. package/dist/renderer/assets/index-CjCuR6DI.css +11 -0
  33. package/dist/renderer/assets/{index-cI3pWBnN.js → index-mvjDN9uV.js} +9 -9
  34. package/dist/renderer/assets/{pencil-ywcJzeOo.js → pencil-qpMjLIFd.js} +1 -1
  35. package/dist/renderer/assets/{play-AtN492Cx.js → play-CPCcNQcH.js} +1 -1
  36. package/dist/renderer/assets/{plus-BEj14BBV.js → plus-B76JWGKd.js} +1 -1
  37. package/dist/renderer/assets/projects-C1nFINjh.js +1 -0
  38. package/dist/renderer/assets/{save-D6ov4CNv.js → save-DtvrD3W1.js} +1 -1
  39. package/dist/renderer/assets/{search-C3bvw7AR.js → search-Ddth-YHH.js} +1 -1
  40. package/dist/renderer/assets/{server-C1Uyedyx.js → server-D0zmghb3.js} +1 -1
  41. package/dist/renderer/assets/{session-utils-DpRwJ6Nh.js → session-utils-C9oe-21F.js} +1 -1
  42. package/dist/renderer/assets/{sessions-BWmAppwK.js → sessions-BXOnLLah.js} +1 -1
  43. package/dist/renderer/assets/{shield-check-CVx1mEs2.js → shield-check-Cv5d_blH.js} +1 -1
  44. package/dist/renderer/assets/{trash-2-o-heXOgs.js → trash-2-C2mimX2b.js} +1 -1
  45. package/dist/renderer/assets/{user-dPOzUqIc.js → user-BrLJ4elW.js} +1 -1
  46. package/dist/renderer/assets/{wifi-off-Fz5c3_x_.js → wifi-off-DvRJYLbG.js} +1 -1
  47. package/dist/renderer/index.html +2 -2
  48. package/dist-electron/packages/core-sdk/src/index.js +4 -0
  49. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +53 -7
  50. package/dist-electron/services/local-ai-core/src/acp/store/local-core-acp-store.js +3 -0
  51. package/dist-electron/services/local-ai-core/src/acp/store/platform-store.js +13 -5
  52. package/dist-electron/services/local-ai-core/src/acp/store/schema.js +1 -0
  53. package/dist-electron/services/local-ai-core/src/acp/store/thread-store.js +3 -0
  54. package/dist-electron/services/local-ai-core/src/agents/pi/launch.js +1 -1
  55. package/dist-electron/services/local-ai-core/src/agents/shared/launch-utils.js +22 -0
  56. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +132 -348
  57. package/dist-electron/services/local-ai-core/src/channel/shared/base-channel-gateway.js +306 -0
  58. package/dist-electron/services/local-ai-core/src/channel/shared/session-command-runtime.js +14 -3
  59. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +131 -382
  60. package/dist-electron/services/local-ai-core/src/router/workspace-router.js +8 -1
  61. package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +2 -1
  62. package/dist-electron/services/local-ai-core/src/runtime/channel-service.js +150 -0
  63. package/dist-electron/services/local-ai-core/src/runtime/env-utils.js +12 -0
  64. package/dist-electron/services/local-ai-core/src/runtime/external-service.js +234 -0
  65. package/dist-electron/services/local-ai-core/src/runtime/handlers/automation-handler.js +34 -0
  66. package/dist-electron/services/local-ai-core/src/runtime/handlers/capabilities-handler.js +12 -0
  67. package/dist-electron/services/local-ai-core/src/runtime/handlers/channel-handler.js +65 -0
  68. package/dist-electron/services/local-ai-core/src/runtime/handlers/external-handler.js +17 -0
  69. package/dist-electron/services/local-ai-core/src/runtime/handlers/knowledge-handler.js +65 -0
  70. package/dist-electron/services/local-ai-core/src/runtime/handlers/openai-handler.js +496 -0
  71. package/dist-electron/services/local-ai-core/src/runtime/handlers/provider-handler.js +24 -0
  72. package/dist-electron/services/local-ai-core/src/runtime/handlers/runtime-handler.js +68 -0
  73. package/dist-electron/services/local-ai-core/src/runtime/handlers/runtimes-handler.js +37 -0
  74. package/dist-electron/services/local-ai-core/src/runtime/handlers/scheduler-handler.js +34 -0
  75. package/dist-electron/services/local-ai-core/src/runtime/handlers/security-handler.js +49 -0
  76. package/dist-electron/services/local-ai-core/src/runtime/handlers/task-handler.js +27 -0
  77. package/dist-electron/services/local-ai-core/src/runtime/handlers/thread-handler.js +46 -0
  78. package/dist-electron/services/local-ai-core/src/runtime/handlers/workspace-handler.js +29 -0
  79. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +40 -592
  80. package/dist-electron/services/local-ai-core/src/runtime/server-helpers.js +96 -0
  81. package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +3 -0
  82. package/dist-electron/services/local-ai-core/src/runtime/server.js +107 -1084
  83. package/dist-electron/services/local-ai-core/src/runtime/standalone.js +13 -1
  84. package/dist-electron/services/local-ai-core/src/thread/thread-command-service.js +23 -2
  85. package/dist-electron/services/local-ai-core/src/thread/thread-slash-command-dispatcher.js +1 -0
  86. package/dist-electron/src/pages/Projects/project-provider-config.js +75 -0
  87. package/dist-electron/src/pages/Threads/thread-chat-model.js +2 -0
  88. package/dist-electron/tests/contracts/architecture-docs.test.js +1 -0
  89. package/dist-electron/tests/electron/agent-runtime-detector.test.js +18 -0
  90. package/dist-electron/tests/electron/knowledge-skill-script.test.js +15 -5
  91. package/dist-electron/tests/electron/lac-cli.test.js +591 -688
  92. package/dist-electron/tests/electron/plugin-kernel.test.js +47 -31
  93. package/dist-electron/tests/electron/project-provider-config.test.js +71 -0
  94. package/dist-electron/tests/electron/workspace-task-store.test.js +40 -11
  95. package/dist-electron/tests/integration/local-core-cors.test.js +22 -0
  96. package/dist-electron/tests/integration/local-core-refactor.test.js +177 -0
  97. package/dist-electron/tests/integration/local-core-routes.test.js +45 -0
  98. package/package.json +2 -2
  99. package/dist/renderer/assets/ProjectDetail-Ddq94-yu.js +0 -11
  100. package/dist/renderer/assets/SessionChat-DOy0iKQR.js +0 -1
  101. package/dist/renderer/assets/ThreadChat-I7dYibDc.js +0 -23
  102. package/dist/renderer/assets/index-CO4fGqmv.css +0 -11
  103. package/dist/renderer/assets/projects-Clar8lgb.js +0 -1
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const node_test_1 = __importDefault(require("node:test"));
7
7
  const strict_1 = __importDefault(require("node:assert/strict"));
8
- const node_http_1 = require("node:http");
9
8
  const lac_js_1 = require("../../services/local-ai-core/src/cli/lac.js");
10
9
  function createIo() {
11
10
  let stdout = '';
@@ -28,738 +27,642 @@ function createIo() {
28
27
  read: () => ({ stdout, stderr }),
29
28
  };
30
29
  }
30
+ function withFetchMock(mock) {
31
+ const originalFetch = globalThis.fetch;
32
+ globalThis.fetch = mock;
33
+ return { restore: () => { globalThis.fetch = originalFetch; } };
34
+ }
31
35
  (0, node_test_1.default)('lac scheduler add posts thread context and lets local core resolve platform binding', async () => {
32
36
  let capturedBody = null;
33
- const server = (0, node_http_1.createServer)(async (req, res) => {
34
- if (req.method === 'POST' && req.url === '/api/local/v1/scheduler/jobs') {
35
- const chunks = [];
36
- for await (const chunk of req) {
37
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
38
- }
39
- capturedBody = Buffer.concat(chunks).toString('utf8');
40
- res.setHeader('content-type', 'application/json');
41
- res.end(JSON.stringify({
42
- ok: true,
43
- data: {
44
- id: 'job:826aff79-570b-4308-822e-18318e2c96ba',
45
- workspaceId: '知识库',
46
- platform: 'lark',
47
- route: {
48
- type: 'channel.chat',
49
- channelId: 'chat-1',
50
- participantId: 'user-1',
51
- threadId: 'thread-1',
52
- },
53
- executionMode: 'same-thread',
54
- triggerType: 'cron',
55
- cronExpr: '*/2 * * * *',
56
- promptTemplate: 'ping',
57
- description: 'two-minute ping',
58
- enabled: true,
59
- concurrencyPolicy: 'skip_if_running',
60
- createdAt: '2026-04-22T06:00:00.000Z',
61
- updatedAt: '2026-04-22T06:00:00.000Z',
62
- },
63
- }));
64
- return;
65
- }
66
- res.statusCode = 404;
67
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
68
- });
69
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
70
- const address = server.address();
71
- (0, strict_1.default)(address && typeof address === 'object');
72
- const { io, read } = createIo();
73
- const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'add', '--cron', '*/2 * * * *', '--message', 'ping', '--desc', 'two-minute ping'], {
74
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
75
- LOCAL_AI_WORKSPACE_ID: '知识库',
76
- LOCAL_AI_THREAD_ID: 'thread-1',
77
- LOCAL_AI_PLATFORM: 'lark',
78
- LOCAL_AI_CHAT_ID: 'chat-1',
79
- LOCAL_AI_PLATFORM_USER_ID: 'user-1',
80
- }, io);
81
- server.close();
82
- strict_1.default.equal(exitCode, 0);
83
- (0, strict_1.default)(capturedBody);
84
- strict_1.default.deepEqual(JSON.parse(capturedBody), {
85
- workspaceId: '知识库',
86
- threadId: 'thread-1',
87
- executionMode: 'same-thread',
88
- triggerType: 'cron',
89
- cronExpr: '*/2 * * * *',
90
- promptTemplate: 'ping',
91
- description: 'two-minute ping',
92
- enabled: true,
37
+ const { restore } = withFetchMock(async (input, init) => {
38
+ if (init?.body)
39
+ capturedBody = init.body;
40
+ return new Response(JSON.stringify({
41
+ ok: true,
42
+ data: {
43
+ id: 'job:826aff79-570b-4308-822e-18318e2c96ba',
44
+ workspaceId: '知识库',
45
+ platform: 'lark',
46
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' },
47
+ executionMode: 'same-thread',
48
+ triggerType: 'cron',
49
+ cronExpr: '*/2 * * * *',
50
+ promptTemplate: 'ping',
51
+ description: 'two-minute ping',
52
+ enabled: true,
53
+ concurrencyPolicy: 'skip_if_running',
54
+ createdAt: '2026-04-22T06:00:00.000Z',
55
+ updatedAt: '2026-04-22T06:00:00.000Z',
56
+ },
57
+ }), { headers: { 'content-type': 'application/json' } });
93
58
  });
94
- strict_1.default.match(read().stdout, /Created scheduler job 826aff79/);
59
+ try {
60
+ const { io, read } = createIo();
61
+ const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'add', '--cron', '*/2 * * * *', '--message', 'ping', '--desc', 'two-minute ping'], {
62
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1',
63
+ LOCAL_AI_WORKSPACE_ID: '知识库',
64
+ LOCAL_AI_THREAD_ID: 'thread-1',
65
+ LOCAL_AI_PLATFORM: 'lark',
66
+ LOCAL_AI_CHAT_ID: 'chat-1',
67
+ LOCAL_AI_PLATFORM_USER_ID: 'user-1',
68
+ }, io);
69
+ strict_1.default.equal(exitCode, 0);
70
+ (0, strict_1.default)(capturedBody);
71
+ strict_1.default.deepEqual(JSON.parse(capturedBody), {
72
+ workspaceId: '知识库',
73
+ threadId: 'thread-1',
74
+ executionMode: 'same-thread',
75
+ triggerType: 'cron',
76
+ cronExpr: '*/2 * * * *',
77
+ promptTemplate: 'ping',
78
+ description: 'two-minute ping',
79
+ enabled: true,
80
+ });
81
+ strict_1.default.match(read().stdout, /Created scheduler job 826aff79/);
82
+ }
83
+ finally {
84
+ restore();
85
+ }
95
86
  });
96
87
  (0, node_test_1.default)('lac scheduler add posts a local job without IM context', async () => {
97
88
  let capturedBody = null;
98
- const server = (0, node_http_1.createServer)(async (req, res) => {
99
- if (req.method === 'POST' && req.url === '/api/local/v1/scheduler/jobs') {
100
- const chunks = [];
101
- for await (const chunk of req) {
102
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
103
- }
104
- capturedBody = Buffer.concat(chunks).toString('utf8');
105
- res.setHeader('content-type', 'application/json');
106
- res.end(JSON.stringify({
107
- ok: true,
108
- data: {
109
- id: 'job-local-1',
110
- workspaceId: '知识库',
111
- platform: 'local',
112
- route: {
113
- type: 'local.thread',
114
- channelId: '知识库',
115
- },
116
- executionMode: 'same-thread',
117
- triggerType: 'cron',
118
- cronExpr: '*/5 * * * *',
119
- promptTemplate: 'ping local',
120
- description: 'local ping',
121
- enabled: true,
122
- concurrencyPolicy: 'skip_if_running',
123
- createdAt: '2026-04-22T06:00:00.000Z',
124
- updatedAt: '2026-04-22T06:00:00.000Z',
125
- },
126
- }));
127
- return;
128
- }
129
- res.statusCode = 404;
130
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
131
- });
132
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
133
- const address = server.address();
134
- (0, strict_1.default)(address && typeof address === 'object');
135
- const { io, read } = createIo();
136
- const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'add', '--cron', '*/5 * * * *', '--message', 'ping local', '--desc', 'local ping'], {
137
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
138
- LOCAL_AI_WORKSPACE_ID: '知识库',
139
- }, io);
140
- server.close();
141
- strict_1.default.equal(exitCode, 0);
142
- (0, strict_1.default)(capturedBody);
143
- strict_1.default.deepEqual(JSON.parse(capturedBody), {
144
- workspaceId: '知识库',
145
- executionMode: 'same-thread',
146
- triggerType: 'cron',
147
- cronExpr: '*/5 * * * *',
148
- promptTemplate: 'ping local',
149
- description: 'local ping',
150
- enabled: true,
89
+ const { restore } = withFetchMock(async (input, init) => {
90
+ if (init?.body)
91
+ capturedBody = init.body;
92
+ return new Response(JSON.stringify({
93
+ ok: true,
94
+ data: {
95
+ id: 'job-local-1',
96
+ workspaceId: '知识库',
97
+ platform: 'local',
98
+ route: { type: 'local.thread', channelId: '知识库' },
99
+ executionMode: 'same-thread',
100
+ triggerType: 'cron',
101
+ cronExpr: '*/5 * * * *',
102
+ promptTemplate: 'ping local',
103
+ description: 'local ping',
104
+ enabled: true,
105
+ concurrencyPolicy: 'skip_if_running',
106
+ createdAt: '2026-04-22T06:00:00.000Z',
107
+ updatedAt: '2026-04-22T06:00:00.000Z',
108
+ },
109
+ }), { headers: { 'content-type': 'application/json' } });
151
110
  });
152
- strict_1.default.match(read().stdout, /Created scheduler job job-local-1/);
111
+ try {
112
+ const { io, read } = createIo();
113
+ const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'add', '--cron', '*/5 * * * *', '--message', 'ping local', '--desc', 'local ping'], {
114
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1',
115
+ LOCAL_AI_WORKSPACE_ID: '知识库',
116
+ }, io);
117
+ strict_1.default.equal(exitCode, 0);
118
+ (0, strict_1.default)(capturedBody);
119
+ strict_1.default.deepEqual(JSON.parse(capturedBody), {
120
+ workspaceId: '知识库',
121
+ executionMode: 'same-thread',
122
+ triggerType: 'cron',
123
+ cronExpr: '*/5 * * * *',
124
+ promptTemplate: 'ping local',
125
+ description: 'local ping',
126
+ enabled: true,
127
+ });
128
+ strict_1.default.match(read().stdout, /Created scheduler job job-local-1/);
129
+ }
130
+ finally {
131
+ restore();
132
+ }
153
133
  });
154
134
  (0, node_test_1.default)('lac channel send-file posts a workspace-relative file through outbound message parts', async () => {
155
135
  let capturedBody = null;
156
- const server = (0, node_http_1.createServer)(async (req, res) => {
157
- if (req.method === 'POST' && req.url === '/api/local/v1/platforms/lark/%E7%9F%A5%E8%AF%86%E5%BA%93/messages') {
158
- const chunks = [];
159
- for await (const chunk of req) {
160
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
161
- }
162
- capturedBody = Buffer.concat(chunks).toString('utf8');
163
- res.setHeader('content-type', 'application/json');
164
- res.end(JSON.stringify({
165
- ok: true,
166
- data: {
167
- platform: 'lark',
168
- workspaceId: '知识库',
169
- channelId: 'chat-1',
170
- messageIds: ['msg-file-1'],
171
- attachments: [{
172
- kind: 'file',
173
- attachmentId: 'file-key-1',
174
- fileName: 'out.pdf',
175
- fileSize: 123,
176
- metadata: { fileKey: 'file-key-1' },
177
- }],
178
- },
179
- }));
180
- return;
181
- }
182
- res.statusCode = 404;
183
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
184
- });
185
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
186
- const address = server.address();
187
- (0, strict_1.default)(address && typeof address === 'object');
188
- const { io, read } = createIo();
189
- const exitCode = await (0, lac_js_1.runCli)(['channel', 'send-file', '--path', 'reports/out.pdf', '--name', 'out.pdf'], {
190
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
191
- LOCAL_AI_WORKSPACE_ID: '知识库',
192
- LOCAL_AI_WORKSPACE_PATH: '/workspace/project',
193
- LOCAL_AI_PLATFORM: 'lark',
194
- LOCAL_AI_CHAT_ID: 'chat-1',
195
- LOCAL_AI_PLATFORM_USER_ID: 'user-1',
196
- }, io);
197
- server.close();
198
- strict_1.default.equal(exitCode, 0);
199
- (0, strict_1.default)(capturedBody);
200
- strict_1.default.deepEqual(JSON.parse(capturedBody), {
201
- route: {
202
- type: 'channel.chat',
203
- channelId: 'chat-1',
204
- participantId: 'user-1',
205
- },
206
- parts: [{
207
- type: 'file',
208
- path: 'reports/out.pdf',
209
- fileName: 'out.pdf',
210
- metadata: {
211
- workspacePath: '/workspace/project',
212
- },
213
- }],
136
+ const { restore } = withFetchMock(async (input, init) => {
137
+ if (init?.body)
138
+ capturedBody = init.body;
139
+ return new Response(JSON.stringify({
140
+ ok: true,
141
+ data: {
142
+ platform: 'lark',
143
+ workspaceId: '知识库',
144
+ channelId: 'chat-1',
145
+ messageIds: ['msg-file-1'],
146
+ attachments: [{
147
+ kind: 'file',
148
+ attachmentId: 'file-key-1',
149
+ fileName: 'out.pdf',
150
+ fileSize: 123,
151
+ metadata: { fileKey: 'file-key-1' },
152
+ }],
153
+ },
154
+ }), { headers: { 'content-type': 'application/json' } });
214
155
  });
215
- strict_1.default.match(read().stdout, /Sent file out\.pdf to chat-1: msg-file-1/);
156
+ try {
157
+ const { io, read } = createIo();
158
+ const exitCode = await (0, lac_js_1.runCli)(['channel', 'send-file', '--path', 'reports/out.pdf', '--name', 'out.pdf'], {
159
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1',
160
+ LOCAL_AI_WORKSPACE_ID: '知识库',
161
+ LOCAL_AI_WORKSPACE_PATH: '/workspace/project',
162
+ LOCAL_AI_PLATFORM: 'lark',
163
+ LOCAL_AI_CHAT_ID: 'chat-1',
164
+ LOCAL_AI_PLATFORM_USER_ID: 'user-1',
165
+ }, io);
166
+ strict_1.default.equal(exitCode, 0);
167
+ (0, strict_1.default)(capturedBody);
168
+ strict_1.default.deepEqual(JSON.parse(capturedBody), {
169
+ route: {
170
+ type: 'channel.chat',
171
+ channelId: 'chat-1',
172
+ participantId: 'user-1',
173
+ },
174
+ parts: [{
175
+ type: 'file',
176
+ path: 'reports/out.pdf',
177
+ fileName: 'out.pdf',
178
+ metadata: {
179
+ workspacePath: '/workspace/project',
180
+ },
181
+ }],
182
+ });
183
+ strict_1.default.match(read().stdout, /Sent file out\.pdf to chat-1: msg-file-1/);
184
+ }
185
+ finally {
186
+ restore();
187
+ }
216
188
  });
217
189
  (0, node_test_1.default)('lac channel send-file normalizes instance-qualified platform env into route instance', async () => {
218
190
  let capturedUrl = '';
219
191
  let capturedBody = null;
220
- const server = (0, node_http_1.createServer)(async (req, res) => {
221
- if (req.method === 'POST') {
222
- capturedUrl = req.url || '';
223
- const chunks = [];
224
- for await (const chunk of req) {
225
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
226
- }
227
- capturedBody = Buffer.concat(chunks).toString('utf8');
228
- res.setHeader('content-type', 'application/json');
229
- res.end(JSON.stringify({
230
- ok: true,
231
- data: {
232
- platform: 'lark',
233
- workspaceId: '知识库',
234
- channelId: 'chat-1',
235
- messageIds: ['msg-file-1'],
236
- attachments: [{ kind: 'file', fileName: 'out.pdf', fileSize: 123 }],
237
- },
238
- }));
239
- return;
240
- }
241
- res.statusCode = 404;
242
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
243
- });
244
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
245
- const address = server.address();
246
- (0, strict_1.default)(address && typeof address === 'object');
247
- const { io } = createIo();
248
- const exitCode = await (0, lac_js_1.runCli)(['channel', 'send-file', '--path', 'reports/out.pdf'], {
249
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
250
- LOCAL_AI_WORKSPACE_ID: '知识库',
251
- LOCAL_AI_WORKSPACE_PATH: '/workspace/project',
252
- LOCAL_AI_PLATFORM: 'lark:lark-1',
253
- LOCAL_AI_CHAT_ID: 'chat-1',
254
- LOCAL_AI_PLATFORM_USER_ID: 'user-1',
255
- }, io);
256
- server.close();
257
- strict_1.default.equal(exitCode, 0);
258
- strict_1.default.equal(capturedUrl, '/api/local/v1/platforms/lark/%E7%9F%A5%E8%AF%86%E5%BA%93/messages');
259
- (0, strict_1.default)(capturedBody);
260
- strict_1.default.deepEqual(JSON.parse(capturedBody).route, {
261
- type: 'channel.chat',
262
- channelId: 'chat-1',
263
- instanceId: 'lark-1',
264
- participantId: 'user-1',
265
- });
266
- });
267
- (0, node_test_1.default)('lac channel send-file posts allowed absolute paths through the same outbound message path', async () => {
268
- let capturedBody = null;
269
- const server = (0, node_http_1.createServer)(async (req, res) => {
270
- if (req.method === 'POST' && req.url === '/api/local/v1/platforms/lark/%E7%9F%A5%E8%AF%86%E5%BA%93/messages') {
271
- const chunks = [];
272
- for await (const chunk of req) {
273
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
274
- }
275
- capturedBody = Buffer.concat(chunks).toString('utf8');
276
- res.setHeader('content-type', 'application/json');
277
- res.end(JSON.stringify({
278
- ok: true,
279
- data: {
280
- platform: 'lark',
281
- workspaceId: '知识库',
282
- channelId: 'chat-1',
283
- messageIds: ['msg-file-2'],
284
- attachments: [{
285
- kind: 'file',
286
- attachmentId: 'file-key-2',
287
- fileName: 'absolute.pdf',
288
- fileSize: 456,
289
- metadata: { fileKey: 'file-key-2' },
290
- }],
291
- },
292
- }));
293
- return;
294
- }
295
- res.statusCode = 404;
296
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
192
+ const { restore } = withFetchMock(async (input, init) => {
193
+ capturedUrl = typeof input === 'string' ? input : input instanceof URL ? input.href : String(input);
194
+ if (init?.body)
195
+ capturedBody = init.body;
196
+ return new Response(JSON.stringify({
197
+ ok: true,
198
+ data: {
199
+ platform: 'lark',
200
+ workspaceId: '知识库',
201
+ channelId: 'chat-1',
202
+ messageIds: ['msg-file-1'],
203
+ attachments: [{ kind: 'file', fileName: 'out.pdf', fileSize: 123 }],
204
+ },
205
+ }), { headers: { 'content-type': 'application/json' } });
297
206
  });
298
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
299
- const address = server.address();
300
- (0, strict_1.default)(address && typeof address === 'object');
301
- const { io, read } = createIo();
302
- const exitCode = await (0, lac_js_1.runCli)(['channel', 'send-file', '--path', '/tmp/absolute.pdf'], {
303
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
304
- LOCAL_AI_WORKSPACE_ID: '知识库',
305
- LOCAL_AI_WORKSPACE_PATH: '/workspace/project',
306
- LOCAL_AI_PLATFORM: 'lark',
307
- LOCAL_AI_CHAT_ID: 'chat-1',
308
- LOCAL_AI_PLATFORM_USER_ID: 'user-1',
309
- }, io);
310
- server.close();
311
- strict_1.default.equal(exitCode, 0);
312
- (0, strict_1.default)(capturedBody);
313
- strict_1.default.deepEqual(JSON.parse(capturedBody), {
314
- route: {
207
+ try {
208
+ const { io } = createIo();
209
+ const exitCode = await (0, lac_js_1.runCli)(['channel', 'send-file', '--path', 'reports/out.pdf'], {
210
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1',
211
+ LOCAL_AI_WORKSPACE_ID: '知识库',
212
+ LOCAL_AI_WORKSPACE_PATH: '/workspace/project',
213
+ LOCAL_AI_PLATFORM: 'lark:lark-1',
214
+ LOCAL_AI_CHAT_ID: 'chat-1',
215
+ LOCAL_AI_PLATFORM_USER_ID: 'user-1',
216
+ }, io);
217
+ strict_1.default.equal(exitCode, 0);
218
+ strict_1.default.match(capturedUrl, /\/platforms\/lark\/%E7%9F%A5%E8%AF%86%E5%BA%93\/messages/);
219
+ (0, strict_1.default)(capturedBody);
220
+ strict_1.default.deepEqual(JSON.parse(capturedBody).route, {
315
221
  type: 'channel.chat',
316
222
  channelId: 'chat-1',
223
+ instanceId: 'lark-1',
317
224
  participantId: 'user-1',
318
- },
319
- parts: [{
320
- type: 'file',
321
- path: '/tmp/absolute.pdf',
322
- metadata: {
323
- workspacePath: '/workspace/project',
324
- },
325
- }],
225
+ });
226
+ }
227
+ finally {
228
+ restore();
229
+ }
230
+ });
231
+ (0, node_test_1.default)('lac channel send-file posts allowed absolute paths through the same outbound message path', async () => {
232
+ let capturedBody = null;
233
+ const { restore } = withFetchMock(async (input, init) => {
234
+ if (init?.body)
235
+ capturedBody = init.body;
236
+ return new Response(JSON.stringify({
237
+ ok: true,
238
+ data: {
239
+ platform: 'lark',
240
+ workspaceId: '知识库',
241
+ channelId: 'chat-1',
242
+ messageIds: ['msg-file-2'],
243
+ attachments: [{
244
+ kind: 'file',
245
+ attachmentId: 'file-key-2',
246
+ fileName: 'absolute.pdf',
247
+ fileSize: 456,
248
+ metadata: { fileKey: 'file-key-2' },
249
+ }],
250
+ },
251
+ }), { headers: { 'content-type': 'application/json' } });
326
252
  });
327
- strict_1.default.match(read().stdout, /Sent file absolute\.pdf to chat-1: msg-file-2/);
253
+ try {
254
+ const { io, read } = createIo();
255
+ const exitCode = await (0, lac_js_1.runCli)(['channel', 'send-file', '--path', '/tmp/absolute.pdf'], {
256
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1',
257
+ LOCAL_AI_WORKSPACE_ID: '知识库',
258
+ LOCAL_AI_WORKSPACE_PATH: '/workspace/project',
259
+ LOCAL_AI_PLATFORM: 'lark',
260
+ LOCAL_AI_CHAT_ID: 'chat-1',
261
+ LOCAL_AI_PLATFORM_USER_ID: 'user-1',
262
+ }, io);
263
+ strict_1.default.equal(exitCode, 0);
264
+ (0, strict_1.default)(capturedBody);
265
+ strict_1.default.deepEqual(JSON.parse(capturedBody), {
266
+ route: {
267
+ type: 'channel.chat',
268
+ channelId: 'chat-1',
269
+ participantId: 'user-1',
270
+ },
271
+ parts: [{
272
+ type: 'file',
273
+ path: '/tmp/absolute.pdf',
274
+ metadata: {
275
+ workspacePath: '/workspace/project',
276
+ },
277
+ }],
278
+ });
279
+ strict_1.default.match(read().stdout, /Sent file absolute\.pdf to chat-1: msg-file-2/);
280
+ }
281
+ finally {
282
+ restore();
283
+ }
328
284
  });
329
285
  (0, node_test_1.default)('lac monitor add posts thread context and stock source config', async () => {
330
286
  let capturedBody = null;
331
- const server = (0, node_http_1.createServer)(async (req, res) => {
332
- if (req.method === 'POST' && req.url === '/api/local/v1/automation/monitors') {
333
- const chunks = [];
334
- for await (const chunk of req) {
335
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
336
- }
337
- capturedBody = Buffer.concat(chunks).toString('utf8');
338
- res.setHeader('content-type', 'application/json');
339
- res.end(JSON.stringify({
340
- ok: true,
341
- data: {
342
- id: 'monitor:826aff79-570b-4308-822e-18318e2c96ba',
343
- workspaceId: '知识库',
344
- title: 'AAPL swing',
345
- sourceType: 'stock.quote',
346
- sourceConfig: { symbol: 'AAPL', price: 188.5 },
347
- condition: { metric: 'abs_change_percent', operator: '>=', value: 3 },
348
- promptTemplate: 'analyze AAPL',
349
- platform: 'lark',
350
- route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1' },
351
- executionMode: 'side-thread',
352
- enabled: true,
353
- cooldownMs: 900000,
354
- concurrencyPolicy: 'skip_if_running',
355
- createdAt: '2026-04-22T06:00:00.000Z',
356
- updatedAt: '2026-04-22T06:00:00.000Z',
357
- },
358
- }));
359
- return;
360
- }
361
- res.statusCode = 404;
362
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
363
- });
364
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
365
- const address = server.address();
366
- (0, strict_1.default)(address && typeof address === 'object');
367
- const { io, read } = createIo();
368
- const exitCode = await (0, lac_js_1.runCli)([
369
- 'monitor',
370
- 'add',
371
- '--title',
372
- 'AAPL swing',
373
- '--source',
374
- 'stock.quote',
375
- '--symbol',
376
- 'aapl',
377
- '--price',
378
- '188.5',
379
- '--condition',
380
- 'abs_change_percent >= 3',
381
- '--message',
382
- 'analyze AAPL',
383
- ], {
384
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
385
- LOCAL_AI_WORKSPACE_ID: '知识库',
386
- LOCAL_AI_THREAD_ID: 'thread-1',
387
- LOCAL_AI_PLATFORM: 'lark',
388
- LOCAL_AI_CHAT_ID: 'chat-1',
389
- LOCAL_AI_PLATFORM_USER_ID: 'user-1',
390
- }, io);
391
- server.close();
392
- strict_1.default.equal(exitCode, 0);
393
- (0, strict_1.default)(capturedBody);
394
- strict_1.default.deepEqual(JSON.parse(capturedBody), {
395
- workspaceId: '知识库',
396
- threadId: 'thread-1',
397
- title: 'AAPL swing',
398
- sourceType: 'stock.quote',
399
- sourceConfig: { symbol: 'AAPL', price: 188.5 },
400
- condition: { metric: 'abs_change_percent', operator: '>=', value: 3 },
401
- promptTemplate: 'analyze AAPL',
402
- executionMode: 'side-thread',
403
- cooldownMs: 900000,
404
- enabled: true,
287
+ const { restore } = withFetchMock(async (input, init) => {
288
+ if (init?.body)
289
+ capturedBody = init.body;
290
+ return new Response(JSON.stringify({
291
+ ok: true,
292
+ data: {
293
+ id: 'monitor:826aff79-570b-4308-822e-18318e2c96ba',
294
+ workspaceId: '知识库',
295
+ title: 'AAPL swing',
296
+ sourceType: 'stock.quote',
297
+ sourceConfig: { symbol: 'AAPL', price: 188.5 },
298
+ condition: { metric: 'abs_change_percent', operator: '>=', value: 3 },
299
+ promptTemplate: 'analyze AAPL',
300
+ platform: 'lark',
301
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1' },
302
+ executionMode: 'side-thread',
303
+ enabled: true,
304
+ cooldownMs: 900000,
305
+ concurrencyPolicy: 'skip_if_running',
306
+ createdAt: '2026-04-22T06:00:00.000Z',
307
+ updatedAt: '2026-04-22T06:00:00.000Z',
308
+ },
309
+ }), { headers: { 'content-type': 'application/json' } });
405
310
  });
406
- strict_1.default.match(read().stdout, /Created monitor 826aff79/);
311
+ try {
312
+ const { io, read } = createIo();
313
+ const exitCode = await (0, lac_js_1.runCli)([
314
+ 'monitor',
315
+ 'add',
316
+ '--title',
317
+ 'AAPL swing',
318
+ '--source',
319
+ 'stock.quote',
320
+ '--symbol',
321
+ 'aapl',
322
+ '--price',
323
+ '188.5',
324
+ '--condition',
325
+ 'abs_change_percent >= 3',
326
+ '--message',
327
+ 'analyze AAPL',
328
+ ], {
329
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1',
330
+ LOCAL_AI_WORKSPACE_ID: '知识库',
331
+ LOCAL_AI_THREAD_ID: 'thread-1',
332
+ LOCAL_AI_PLATFORM: 'lark',
333
+ LOCAL_AI_CHAT_ID: 'chat-1',
334
+ LOCAL_AI_PLATFORM_USER_ID: 'user-1',
335
+ }, io);
336
+ strict_1.default.equal(exitCode, 0);
337
+ (0, strict_1.default)(capturedBody);
338
+ strict_1.default.deepEqual(JSON.parse(capturedBody), {
339
+ workspaceId: '知识库',
340
+ threadId: 'thread-1',
341
+ title: 'AAPL swing',
342
+ sourceType: 'stock.quote',
343
+ sourceConfig: { symbol: 'AAPL', price: 188.5 },
344
+ condition: { metric: 'abs_change_percent', operator: '>=', value: 3 },
345
+ promptTemplate: 'analyze AAPL',
346
+ executionMode: 'side-thread',
347
+ cooldownMs: 900000,
348
+ enabled: true,
349
+ });
350
+ strict_1.default.match(read().stdout, /Created monitor 826aff79/);
351
+ }
352
+ finally {
353
+ restore();
354
+ }
407
355
  });
408
356
  (0, node_test_1.default)('lac monitor list filters by current thread context', async () => {
409
- const server = (0, node_http_1.createServer)((req, res) => {
410
- if (req.method === 'GET' && req.url === '/api/local/v1/automation/monitors?workspace_id=%E7%9F%A5%E8%AF%86%E5%BA%93') {
411
- res.setHeader('content-type', 'application/json');
412
- res.end(JSON.stringify({
413
- ok: true,
414
- data: {
415
- monitors: [
416
- {
417
- id: 'monitor-1',
418
- workspaceId: '知识库',
419
- title: 'current',
420
- sourceType: 'stock.quote',
421
- sourceConfig: { symbol: 'AAPL' },
422
- condition: { metric: 'abs_change_percent', operator: '>=', value: 3 },
423
- promptTemplate: 'ping',
424
- platform: 'lark',
425
- route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1' },
426
- executionMode: 'side-thread',
427
- enabled: true,
428
- cooldownMs: 900000,
429
- concurrencyPolicy: 'skip_if_running',
430
- createdAt: '2026-04-22T06:00:00.000Z',
431
- updatedAt: '2026-04-22T06:00:00.000Z',
432
- },
433
- {
434
- id: 'monitor-2',
435
- workspaceId: '知识库',
436
- title: 'other',
437
- sourceType: 'stock.quote',
438
- sourceConfig: { symbol: 'MSFT' },
439
- condition: { metric: 'abs_change_percent', operator: '>=', value: 3 },
440
- promptTemplate: 'pong',
441
- platform: 'lark',
442
- route: { type: 'channel.chat', channelId: 'chat-2', participantId: 'user-2' },
443
- executionMode: 'side-thread',
444
- enabled: true,
445
- cooldownMs: 900000,
446
- concurrencyPolicy: 'skip_if_running',
447
- createdAt: '2026-04-22T06:00:00.000Z',
448
- updatedAt: '2026-04-22T06:00:00.000Z',
449
- },
450
- ],
451
- },
452
- }));
453
- return;
454
- }
455
- res.statusCode = 404;
456
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
357
+ const { restore } = withFetchMock(async () => {
358
+ return new Response(JSON.stringify({
359
+ ok: true,
360
+ data: {
361
+ monitors: [
362
+ {
363
+ id: 'monitor-1',
364
+ workspaceId: '知识库',
365
+ title: 'current',
366
+ sourceType: 'stock.quote',
367
+ sourceConfig: { symbol: 'AAPL' },
368
+ condition: { metric: 'abs_change_percent', operator: '>=', value: 3 },
369
+ promptTemplate: 'ping',
370
+ platform: 'lark',
371
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1' },
372
+ executionMode: 'side-thread',
373
+ enabled: true,
374
+ cooldownMs: 900000,
375
+ concurrencyPolicy: 'skip_if_running',
376
+ createdAt: '2026-04-22T06:00:00.000Z',
377
+ updatedAt: '2026-04-22T06:00:00.000Z',
378
+ },
379
+ {
380
+ id: 'monitor-2',
381
+ workspaceId: '知识库',
382
+ title: 'other',
383
+ sourceType: 'stock.quote',
384
+ sourceConfig: { symbol: 'MSFT' },
385
+ condition: { metric: 'abs_change_percent', operator: '>=', value: 3 },
386
+ promptTemplate: 'pong',
387
+ platform: 'lark',
388
+ route: { type: 'channel.chat', channelId: 'chat-2', participantId: 'user-2' },
389
+ executionMode: 'side-thread',
390
+ enabled: true,
391
+ cooldownMs: 900000,
392
+ concurrencyPolicy: 'skip_if_running',
393
+ createdAt: '2026-04-22T06:00:00.000Z',
394
+ updatedAt: '2026-04-22T06:00:00.000Z',
395
+ },
396
+ ],
397
+ },
398
+ }), { headers: { 'content-type': 'application/json' } });
457
399
  });
458
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
459
- const address = server.address();
460
- (0, strict_1.default)(address && typeof address === 'object');
461
- const { io, read } = createIo();
462
- const exitCode = await (0, lac_js_1.runCli)(['monitor', 'list', '--thread'], {
463
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
464
- LOCAL_AI_WORKSPACE_ID: '知识库',
465
- LOCAL_AI_THREAD_ID: 'thread-1',
466
- LOCAL_AI_PLATFORM: 'lark',
467
- LOCAL_AI_CHAT_ID: 'chat-1',
468
- LOCAL_AI_PLATFORM_USER_ID: 'user-1',
469
- }, io);
470
- server.close();
471
- strict_1.default.equal(exitCode, 0);
472
- strict_1.default.match(read().stdout, /monitor-1/);
473
- strict_1.default.doesNotMatch(read().stdout, /monitor-2/);
400
+ try {
401
+ const { io, read } = createIo();
402
+ const exitCode = await (0, lac_js_1.runCli)(['monitor', 'list', '--thread'], {
403
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1',
404
+ LOCAL_AI_WORKSPACE_ID: '知识库',
405
+ LOCAL_AI_THREAD_ID: 'thread-1',
406
+ LOCAL_AI_PLATFORM: 'lark',
407
+ LOCAL_AI_CHAT_ID: 'chat-1',
408
+ LOCAL_AI_PLATFORM_USER_ID: 'user-1',
409
+ }, io);
410
+ strict_1.default.equal(exitCode, 0);
411
+ strict_1.default.match(read().stdout, /monitor-1/);
412
+ strict_1.default.doesNotMatch(read().stdout, /monitor-2/);
413
+ }
414
+ finally {
415
+ restore();
416
+ }
474
417
  });
475
418
  (0, node_test_1.default)('lac scheduler list shows workspace jobs by default', async () => {
476
- const server = (0, node_http_1.createServer)((req, res) => {
477
- if (req.method === 'GET' && req.url === '/api/local/v1/scheduler/jobs?workspace_id=%E7%9F%A5%E8%AF%86%E5%BA%93') {
478
- res.setHeader('content-type', 'application/json');
479
- res.end(JSON.stringify({
480
- ok: true,
481
- data: {
482
- jobs: [
483
- {
484
- id: 'job:826aff79-570b-4308-822e-18318e2c96ba',
485
- workspaceId: '知识库',
486
- platform: 'lark',
487
- route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' },
488
- executionMode: 'same-thread',
489
- triggerType: 'cron',
490
- cronExpr: '*/2 * * * *',
491
- promptTemplate: 'ping',
492
- description: 'current',
493
- enabled: true,
494
- concurrencyPolicy: 'skip_if_running',
495
- createdAt: '2026-04-22T06:00:00.000Z',
496
- updatedAt: '2026-04-22T06:00:00.000Z',
497
- },
498
- {
499
- id: 'job-2',
500
- workspaceId: '知识库',
501
- platform: 'lark',
502
- route: { type: 'channel.chat', channelId: 'chat-2', participantId: 'user-2', threadId: 'thread-2' },
503
- executionMode: 'side-thread',
504
- triggerType: 'cron',
505
- cronExpr: '0 9 * * *',
506
- promptTemplate: 'pong',
507
- description: 'other',
508
- enabled: true,
509
- concurrencyPolicy: 'skip_if_running',
510
- createdAt: '2026-04-22T06:00:00.000Z',
511
- updatedAt: '2026-04-22T06:00:00.000Z',
512
- },
513
- ],
514
- },
515
- }));
516
- return;
517
- }
518
- res.statusCode = 404;
519
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
419
+ const { restore } = withFetchMock(async () => {
420
+ return new Response(JSON.stringify({
421
+ ok: true,
422
+ data: {
423
+ jobs: [
424
+ {
425
+ id: 'job:826aff79-570b-4308-822e-18318e2c96ba',
426
+ workspaceId: '知识库',
427
+ platform: 'lark',
428
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' },
429
+ executionMode: 'same-thread',
430
+ triggerType: 'cron',
431
+ cronExpr: '*/2 * * * *',
432
+ promptTemplate: 'ping',
433
+ description: 'current',
434
+ enabled: true,
435
+ concurrencyPolicy: 'skip_if_running',
436
+ createdAt: '2026-04-22T06:00:00.000Z',
437
+ updatedAt: '2026-04-22T06:00:00.000Z',
438
+ },
439
+ {
440
+ id: 'job-2',
441
+ workspaceId: '知识库',
442
+ platform: 'lark',
443
+ route: { type: 'channel.chat', channelId: 'chat-2', participantId: 'user-2', threadId: 'thread-2' },
444
+ executionMode: 'side-thread',
445
+ triggerType: 'cron',
446
+ cronExpr: '0 9 * * *',
447
+ promptTemplate: 'pong',
448
+ description: 'other',
449
+ enabled: true,
450
+ concurrencyPolicy: 'skip_if_running',
451
+ createdAt: '2026-04-22T06:00:00.000Z',
452
+ updatedAt: '2026-04-22T06:00:00.000Z',
453
+ },
454
+ ],
455
+ },
456
+ }), { headers: { 'content-type': 'application/json' } });
520
457
  });
521
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
522
- const address = server.address();
523
- (0, strict_1.default)(address && typeof address === 'object');
524
- const { io, read } = createIo();
525
- const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'list'], {
526
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
527
- LOCAL_AI_WORKSPACE_ID: '知识库',
528
- LOCAL_AI_THREAD_ID: 'thread-1',
529
- }, io);
530
- server.close();
531
- strict_1.default.equal(exitCode, 0);
532
- strict_1.default.match(read().stdout, /826aff79/);
533
- strict_1.default.doesNotMatch(read().stdout, /job:826aff79/);
534
- strict_1.default.match(read().stdout, /job-2/);
458
+ try {
459
+ const { io, read } = createIo();
460
+ const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'list'], {
461
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1',
462
+ LOCAL_AI_WORKSPACE_ID: '知识库',
463
+ LOCAL_AI_THREAD_ID: 'thread-1',
464
+ }, io);
465
+ strict_1.default.equal(exitCode, 0);
466
+ strict_1.default.match(read().stdout, /826aff79/);
467
+ strict_1.default.doesNotMatch(read().stdout, /job:826aff79/);
468
+ strict_1.default.match(read().stdout, /job-2/);
469
+ }
470
+ finally {
471
+ restore();
472
+ }
535
473
  });
536
474
  (0, node_test_1.default)('lac scheduler info prints the short job id', async () => {
537
- const server = (0, node_http_1.createServer)((req, res) => {
538
- if (req.method === 'GET' && req.url === '/api/local/v1/scheduler/jobs/826aff79') {
539
- res.setHeader('content-type', 'application/json');
540
- res.end(JSON.stringify({
541
- ok: true,
542
- data: {
543
- id: 'job:826aff79-570b-4308-822e-18318e2c96ba',
544
- workspaceId: '知识库',
545
- platform: 'lark',
546
- route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' },
547
- executionMode: 'side-thread',
548
- triggerType: 'cron',
549
- cronExpr: '30 18 * * *',
550
- promptTemplate: 'ping',
551
- description: 'daily ping',
552
- enabled: true,
553
- concurrencyPolicy: 'skip_if_running',
554
- createdAt: '2026-04-22T06:00:00.000Z',
555
- updatedAt: '2026-04-22T06:00:00.000Z',
556
- },
557
- }));
558
- return;
559
- }
560
- if (req.method === 'GET' && req.url === '/api/local/v1/scheduler/jobs/826aff79/runs') {
561
- res.setHeader('content-type', 'application/json');
562
- res.end(JSON.stringify({ ok: true, data: { runs: [] } }));
563
- return;
475
+ let requestCount = 0;
476
+ const { restore } = withFetchMock(async (input) => {
477
+ requestCount++;
478
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : String(input);
479
+ if (url.includes('/runs')) {
480
+ return new Response(JSON.stringify({ ok: true, data: { runs: [] } }), { headers: { 'content-type': 'application/json' } });
564
481
  }
565
- res.statusCode = 404;
566
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
482
+ return new Response(JSON.stringify({
483
+ ok: true,
484
+ data: {
485
+ id: 'job:826aff79-570b-4308-822e-18318e2c96ba',
486
+ workspaceId: '知识库',
487
+ platform: 'lark',
488
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' },
489
+ executionMode: 'side-thread',
490
+ triggerType: 'cron',
491
+ cronExpr: '30 18 * * *',
492
+ promptTemplate: 'ping',
493
+ description: 'daily ping',
494
+ enabled: true,
495
+ concurrencyPolicy: 'skip_if_running',
496
+ createdAt: '2026-04-22T06:00:00.000Z',
497
+ updatedAt: '2026-04-22T06:00:00.000Z',
498
+ },
499
+ }), { headers: { 'content-type': 'application/json' } });
567
500
  });
568
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
569
- const address = server.address();
570
- (0, strict_1.default)(address && typeof address === 'object');
571
- const { io, read } = createIo();
572
- const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'info', '826aff79'], {
573
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
574
- }, io);
575
- server.close();
576
- strict_1.default.equal(exitCode, 0);
577
- strict_1.default.match(read().stdout, /Job: 826aff79/);
578
- strict_1.default.doesNotMatch(read().stdout, /job:826aff79/);
501
+ try {
502
+ const { io, read } = createIo();
503
+ const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'info', '826aff79'], { LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1' }, io);
504
+ strict_1.default.equal(exitCode, 0);
505
+ strict_1.default.match(read().stdout, /Job: 826aff79/);
506
+ strict_1.default.doesNotMatch(read().stdout, /job:826aff79/);
507
+ }
508
+ finally {
509
+ restore();
510
+ }
579
511
  });
580
512
  (0, node_test_1.default)('lac scheduler edit patches a short job id and normalizes execution mode', async () => {
581
513
  let capturedBody = null;
582
- const server = (0, node_http_1.createServer)(async (req, res) => {
583
- if (req.method === 'PATCH' && req.url === '/api/local/v1/scheduler/jobs/826aff79') {
584
- const chunks = [];
585
- for await (const chunk of req) {
586
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
587
- }
588
- capturedBody = Buffer.concat(chunks).toString('utf8');
589
- res.setHeader('content-type', 'application/json');
590
- res.end(JSON.stringify({
591
- ok: true,
592
- data: {
593
- id: 'job:826aff79-570b-4308-822e-18318e2c96ba',
594
- workspaceId: '知识库',
595
- platform: 'lark',
596
- route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' },
597
- executionMode: 'side-thread',
598
- triggerType: 'cron',
599
- cronExpr: '0 10 * * *',
600
- promptTemplate: 'updated ping',
601
- description: 'daily updated ping',
602
- enabled: false,
603
- concurrencyPolicy: 'skip_if_running',
604
- createdAt: '2026-04-22T06:00:00.000Z',
605
- updatedAt: '2026-04-22T07:00:00.000Z',
606
- },
607
- }));
608
- return;
609
- }
610
- res.statusCode = 404;
611
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
612
- });
613
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
614
- const address = server.address();
615
- (0, strict_1.default)(address && typeof address === 'object');
616
- const { io, read } = createIo();
617
- const exitCode = await (0, lac_js_1.runCli)([
618
- 'scheduler',
619
- 'edit',
620
- '826aff79',
621
- '--cron',
622
- '0 10 * * *',
623
- '--message',
624
- 'updated ping',
625
- '--desc',
626
- 'daily updated ping',
627
- '--enabled',
628
- 'false',
629
- '--execution-mode',
630
- 'side_thread',
631
- ], {
632
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
633
- }, io);
634
- server.close();
635
- strict_1.default.equal(exitCode, 0);
636
- (0, strict_1.default)(capturedBody);
637
- strict_1.default.deepEqual(JSON.parse(capturedBody), {
638
- cronExpr: '0 10 * * *',
639
- promptTemplate: 'updated ping',
640
- description: 'daily updated ping',
641
- enabled: false,
642
- executionMode: 'side-thread',
514
+ const { restore } = withFetchMock(async (input, init) => {
515
+ if (init?.body)
516
+ capturedBody = init.body;
517
+ return new Response(JSON.stringify({
518
+ ok: true,
519
+ data: {
520
+ id: 'job:826aff79-570b-4308-822e-18318e2c96ba',
521
+ workspaceId: '知识库',
522
+ platform: 'lark',
523
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' },
524
+ executionMode: 'side-thread',
525
+ triggerType: 'cron',
526
+ cronExpr: '0 10 * * *',
527
+ promptTemplate: 'updated ping',
528
+ description: 'daily updated ping',
529
+ enabled: false,
530
+ concurrencyPolicy: 'skip_if_running',
531
+ createdAt: '2026-04-22T06:00:00.000Z',
532
+ updatedAt: '2026-04-22T07:00:00.000Z',
533
+ },
534
+ }), { headers: { 'content-type': 'application/json' } });
643
535
  });
644
- strict_1.default.match(read().stdout, /Updated scheduler job 826aff79/);
645
- strict_1.default.doesNotMatch(read().stdout, /job:826aff79/);
536
+ try {
537
+ const { io, read } = createIo();
538
+ const exitCode = await (0, lac_js_1.runCli)([
539
+ 'scheduler',
540
+ 'edit',
541
+ '826aff79',
542
+ '--cron',
543
+ '0 10 * * *',
544
+ '--message',
545
+ 'updated ping',
546
+ '--desc',
547
+ 'daily updated ping',
548
+ '--enabled',
549
+ 'false',
550
+ '--execution-mode',
551
+ 'side_thread',
552
+ ], { LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1' }, io);
553
+ strict_1.default.equal(exitCode, 0);
554
+ (0, strict_1.default)(capturedBody);
555
+ strict_1.default.deepEqual(JSON.parse(capturedBody), {
556
+ cronExpr: '0 10 * * *',
557
+ promptTemplate: 'updated ping',
558
+ description: 'daily updated ping',
559
+ enabled: false,
560
+ executionMode: 'side-thread',
561
+ });
562
+ strict_1.default.match(read().stdout, /Updated scheduler job 826aff79/);
563
+ strict_1.default.doesNotMatch(read().stdout, /job:826aff79/);
564
+ }
565
+ finally {
566
+ restore();
567
+ }
646
568
  });
647
569
  (0, node_test_1.default)('lac scheduler del deletes a short job id', async () => {
648
- const server = (0, node_http_1.createServer)((req, res) => {
649
- if (req.method === 'DELETE' && req.url === '/api/local/v1/scheduler/jobs/826aff79') {
650
- res.setHeader('content-type', 'application/json');
651
- res.end(JSON.stringify({ ok: true, data: { deleted: true } }));
652
- return;
653
- }
654
- res.statusCode = 404;
655
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
570
+ const { restore } = withFetchMock(async () => {
571
+ return new Response(JSON.stringify({ ok: true, data: { deleted: true } }), { headers: { 'content-type': 'application/json' } });
656
572
  });
657
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
658
- const address = server.address();
659
- (0, strict_1.default)(address && typeof address === 'object');
660
- const { io, read } = createIo();
661
- const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'del', '826aff79'], {
662
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
663
- }, io);
664
- server.close();
665
- strict_1.default.equal(exitCode, 0);
666
- strict_1.default.match(read().stdout, /Deleted scheduler job 826aff79/);
667
- strict_1.default.doesNotMatch(read().stdout, /job:826aff79/);
573
+ try {
574
+ const { io, read } = createIo();
575
+ const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'del', '826aff79'], { LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1' }, io);
576
+ strict_1.default.equal(exitCode, 0);
577
+ strict_1.default.match(read().stdout, /Deleted scheduler job 826aff79/);
578
+ strict_1.default.doesNotMatch(read().stdout, /job:826aff79/);
579
+ }
580
+ finally {
581
+ restore();
582
+ }
668
583
  });
669
584
  (0, node_test_1.default)('lac scheduler run triggers a short job id', async () => {
670
- const server = (0, node_http_1.createServer)((req, res) => {
671
- if (req.method === 'POST' && req.url === '/api/local/v1/scheduler/jobs/826aff79/run') {
672
- res.setHeader('content-type', 'application/json');
673
- res.end(JSON.stringify({
674
- ok: true,
675
- data: {
676
- id: 'run-1',
677
- jobId: 'job:826aff79-570b-4308-822e-18318e2c96ba',
678
- status: 'queued',
679
- triggeredAt: '2026-04-22T07:00:00.000Z',
680
- startedAt: '2026-04-22T07:00:00.000Z',
681
- completedAt: '',
682
- output: '',
683
- error: '',
684
- },
685
- }));
686
- return;
687
- }
688
- res.statusCode = 404;
689
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
585
+ const { restore } = withFetchMock(async () => {
586
+ return new Response(JSON.stringify({
587
+ ok: true,
588
+ data: {
589
+ id: 'run-1',
590
+ jobId: 'job:826aff79-570b-4308-822e-18318e2c96ba',
591
+ status: 'queued',
592
+ triggeredAt: '2026-04-22T07:00:00.000Z',
593
+ startedAt: '2026-04-22T07:00:00.000Z',
594
+ completedAt: '',
595
+ output: '',
596
+ error: '',
597
+ },
598
+ }), { headers: { 'content-type': 'application/json' } });
690
599
  });
691
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
692
- const address = server.address();
693
- (0, strict_1.default)(address && typeof address === 'object');
694
- const { io, read } = createIo();
695
- const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'run', '826aff79'], {
696
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
697
- }, io);
698
- server.close();
699
- strict_1.default.equal(exitCode, 0);
700
- strict_1.default.match(read().stdout, /Triggered scheduler job 826aff79: queued/);
701
- strict_1.default.doesNotMatch(read().stdout, /job:826aff79/);
600
+ try {
601
+ const { io, read } = createIo();
602
+ const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'run', '826aff79'], { LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1' }, io);
603
+ strict_1.default.equal(exitCode, 0);
604
+ strict_1.default.match(read().stdout, /Triggered scheduler job 826aff79: queued/);
605
+ strict_1.default.doesNotMatch(read().stdout, /job:826aff79/);
606
+ }
607
+ finally {
608
+ restore();
609
+ }
702
610
  });
703
611
  (0, node_test_1.default)('lac scheduler list --thread filters by current thread context', async () => {
704
- const server = (0, node_http_1.createServer)((req, res) => {
705
- if (req.method === 'GET' && req.url === '/api/local/v1/scheduler/jobs?workspace_id=%E7%9F%A5%E8%AF%86%E5%BA%93') {
706
- res.setHeader('content-type', 'application/json');
707
- res.end(JSON.stringify({
708
- ok: true,
709
- data: {
710
- jobs: [
711
- {
712
- id: 'job-1',
713
- workspaceId: '知识库',
714
- platform: 'lark',
715
- route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1' },
716
- executionMode: 'same-thread',
717
- triggerType: 'cron',
718
- cronExpr: '*/2 * * * *',
719
- promptTemplate: 'ping',
720
- description: 'current',
721
- enabled: true,
722
- concurrencyPolicy: 'skip_if_running',
723
- createdAt: '2026-04-22T06:00:00.000Z',
724
- updatedAt: '2026-04-22T06:00:00.000Z',
725
- },
726
- {
727
- id: 'job-2',
728
- workspaceId: '知识库',
729
- platform: 'lark',
730
- route: { type: 'channel.chat', channelId: 'chat-2', participantId: 'user-2' },
731
- executionMode: 'side-thread',
732
- triggerType: 'cron',
733
- cronExpr: '0 9 * * *',
734
- promptTemplate: 'pong',
735
- description: 'other',
736
- enabled: true,
737
- concurrencyPolicy: 'skip_if_running',
738
- createdAt: '2026-04-22T06:00:00.000Z',
739
- updatedAt: '2026-04-22T06:00:00.000Z',
740
- },
741
- ],
742
- },
743
- }));
744
- return;
745
- }
746
- res.statusCode = 404;
747
- res.end(JSON.stringify({ ok: false, error: 'not found' }));
612
+ const { restore } = withFetchMock(async () => {
613
+ return new Response(JSON.stringify({
614
+ ok: true,
615
+ data: {
616
+ jobs: [
617
+ {
618
+ id: 'job-1',
619
+ workspaceId: '知识库',
620
+ platform: 'lark',
621
+ route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1' },
622
+ executionMode: 'same-thread',
623
+ triggerType: 'cron',
624
+ cronExpr: '*/2 * * * *',
625
+ promptTemplate: 'ping',
626
+ description: 'current',
627
+ enabled: true,
628
+ concurrencyPolicy: 'skip_if_running',
629
+ createdAt: '2026-04-22T06:00:00.000Z',
630
+ updatedAt: '2026-04-22T06:00:00.000Z',
631
+ },
632
+ {
633
+ id: 'job-2',
634
+ workspaceId: '知识库',
635
+ platform: 'lark',
636
+ route: { type: 'channel.chat', channelId: 'chat-2', participantId: 'user-2' },
637
+ executionMode: 'side-thread',
638
+ triggerType: 'cron',
639
+ cronExpr: '0 9 * * *',
640
+ promptTemplate: 'pong',
641
+ description: 'other',
642
+ enabled: true,
643
+ concurrencyPolicy: 'skip_if_running',
644
+ createdAt: '2026-04-22T06:00:00.000Z',
645
+ updatedAt: '2026-04-22T06:00:00.000Z',
646
+ },
647
+ ],
648
+ },
649
+ }), { headers: { 'content-type': 'application/json' } });
748
650
  });
749
- await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
750
- const address = server.address();
751
- (0, strict_1.default)(address && typeof address === 'object');
752
- const { io, read } = createIo();
753
- const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'list', '--thread'], {
754
- LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
755
- LOCAL_AI_WORKSPACE_ID: '知识库',
756
- LOCAL_AI_THREAD_ID: 'thread-1',
757
- LOCAL_AI_PLATFORM: 'lark',
758
- LOCAL_AI_CHAT_ID: 'chat-1',
759
- LOCAL_AI_PLATFORM_USER_ID: 'user-1',
760
- }, io);
761
- server.close();
762
- strict_1.default.equal(exitCode, 0);
763
- strict_1.default.match(read().stdout, /job-1/);
764
- strict_1.default.doesNotMatch(read().stdout, /job-2/);
651
+ try {
652
+ const { io, read } = createIo();
653
+ const exitCode = await (0, lac_js_1.runCli)(['scheduler', 'list', '--thread'], {
654
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1',
655
+ LOCAL_AI_WORKSPACE_ID: '知识库',
656
+ LOCAL_AI_THREAD_ID: 'thread-1',
657
+ LOCAL_AI_PLATFORM: 'lark',
658
+ LOCAL_AI_CHAT_ID: 'chat-1',
659
+ LOCAL_AI_PLATFORM_USER_ID: 'user-1',
660
+ }, io);
661
+ strict_1.default.equal(exitCode, 0);
662
+ strict_1.default.match(read().stdout, /job-1/);
663
+ strict_1.default.doesNotMatch(read().stdout, /job-2/);
664
+ }
665
+ finally {
666
+ restore();
667
+ }
765
668
  });