@kafca/agentdock 0.1.44 → 0.1.45

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 (113) hide show
  1. package/README.md +71 -11
  2. package/bin/agentdock.mjs +5 -5
  3. package/dist/renderer/assets/{Badge-Qk9qHovH.js → Badge-BIojwD6s.js} +1 -1
  4. package/dist/renderer/assets/{Chat-BK-fm0Xr.js → Chat-wjBR3XEX.js} +1 -1
  5. package/dist/renderer/assets/{ChatMarkdown-BKdW8OON.js → ChatMarkdown-DqszsPB_.js} +1 -1
  6. package/dist/renderer/assets/Config-BDCHs5A2.js +21 -0
  7. package/dist/renderer/assets/{CronList-Z3TvSRKP.js → CronList-CEWQYTov.js} +1 -1
  8. package/dist/renderer/assets/Dashboard-DrWftfk3.js +26 -0
  9. package/dist/renderer/assets/{EmptyState-kD9yAwtw.js → EmptyState-BqCx1oWr.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeDetail-BAMCLrjz.js → KnowledgeDetail-BwK8Gpfa.js} +2 -2
  11. package/dist/renderer/assets/{KnowledgeHome-ChvjOfrc.js → KnowledgeHome-hhDTZMDO.js} +3 -3
  12. package/dist/renderer/assets/{Logs-Cv0nth3j.js → Logs-C9xPEhFT.js} +1 -1
  13. package/dist/renderer/assets/{Modal-DwY4lecS.js → Modal-D2U4r2V0.js} +1 -1
  14. package/dist/renderer/assets/{MonitorList-JEvMoWUm.js → MonitorList-CayKbfwJ.js} +1 -1
  15. package/dist/renderer/assets/{Page-CnB9ePsb.js → Page-urf5GcT_.js} +1 -1
  16. package/dist/renderer/assets/ProjectDetail-B1GyAr7a.js +11 -0
  17. package/dist/renderer/assets/{ProjectList-D6CXumR_.js → ProjectList-BoipQypE.js} +1 -1
  18. package/dist/renderer/assets/{Select-zfoksLGB.js → Select-CAscWpy5.js} +1 -1
  19. package/dist/renderer/assets/{SessionChat-DUTh4WoM.js → SessionChat-BAVy5221.js} +1 -1
  20. package/dist/renderer/assets/{SessionList-D9o-lUst.js → SessionList-BZpuMvh0.js} +1 -1
  21. package/dist/renderer/assets/{ThreadChat-BYXlT-Kc.js → ThreadChat-JX7m9biG.js} +1 -1
  22. package/dist/renderer/assets/Workspace-CFW8otjU.js +14 -0
  23. package/dist/renderer/assets/{arrow-left-CNEAuElg.js → arrow-left-7HrNuS-V.js} +1 -1
  24. package/dist/renderer/assets/{book-open-BNJMhNI-.js → book-open-BDoAu82s.js} +1 -1
  25. package/dist/renderer/assets/{check-BnclClRU.js → check-DGlyq4xX.js} +1 -1
  26. package/dist/renderer/assets/{chevron-down-Bh53e0O-.js → chevron-down-Bb_3EHq9.js} +1 -1
  27. package/dist/renderer/assets/{circle-ClZTWCWO.js → circle-DIpqZnYw.js} +1 -1
  28. package/dist/renderer/assets/desktop-Ci1fnRDR.js +1 -0
  29. package/dist/renderer/assets/{desktop-C6q6wBae.js → desktop-Do_UrC-o.js} +5 -5
  30. package/dist/renderer/assets/en-CmAr_0e6.js +1 -0
  31. package/dist/renderer/assets/es-Cs9x52ca.js +1 -0
  32. package/dist/renderer/assets/{funnel-_5hsz0xI.js → funnel-Bbf0ERLP.js} +1 -1
  33. package/dist/renderer/assets/{heart-u4Q1aOqj.js → heart-CGqU-qfw.js} +1 -1
  34. package/dist/renderer/assets/{index-BNFx4uYv.js → index-Df-Trovt.js} +2 -2
  35. package/dist/renderer/assets/{index-B36eowj-.js → index-DwhTjLA1.js} +36 -36
  36. package/dist/renderer/assets/index-RaTga2BC.css +11 -0
  37. package/dist/renderer/assets/ja-DtvtBRTW.js +1 -0
  38. package/dist/renderer/assets/{pencil-DLh9QNmT.js → pencil-CHiy1MvK.js} +1 -1
  39. package/dist/renderer/assets/{play-BDTk9Scm.js → play-CGYnmC4Q.js} +1 -1
  40. package/dist/renderer/assets/{plug-BG0JHW6B.js → plug-Mexu4gyV.js} +1 -1
  41. package/dist/renderer/assets/{plus-BSc8POsz.js → plus-B_Q5-Oj7.js} +1 -1
  42. package/dist/renderer/assets/projects-C6bbkC-O.js +1 -0
  43. package/dist/renderer/assets/{save-D5AO_G6a.js → save-CJ_QFPw0.js} +1 -1
  44. package/dist/renderer/assets/{search-CniPk7Ey.js → search-D8Dkhsyq.js} +1 -1
  45. package/dist/renderer/assets/{server-GsXyQAZm.js → server-Cd96R7WL.js} +1 -1
  46. package/dist/renderer/assets/{session-utils-BDxzgTBC.js → session-utils-BNUbQBfF.js} +1 -1
  47. package/dist/renderer/assets/{sessions-CLcKA1Tt.js → sessions-BACfV8s1.js} +1 -1
  48. package/dist/renderer/assets/{shield-check-4i0E3hmf.js → shield-check-Cnn-yQUA.js} +1 -1
  49. package/dist/renderer/assets/{trash-2-DhtOCsO5.js → trash-2-oWCxwQ9D.js} +1 -1
  50. package/dist/renderer/assets/{user-BtC_pVXV.js → user-BzE9BwsY.js} +1 -1
  51. package/dist/renderer/assets/{wifi-off-BHPzPY_B.js → wifi-off-DHxnNkdY.js} +1 -1
  52. package/dist/renderer/assets/zh-CcJ9vBZj.js +1 -0
  53. package/dist/renderer/assets/zh-TW-elI72AC1.js +1 -0
  54. package/dist/renderer/index.html +2 -2
  55. package/dist-electron/electron/main.js +26 -1
  56. package/dist-electron/packages/core-sdk/src/index.js +28 -0
  57. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +111 -62
  58. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +80 -7
  59. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-transport.js +78 -5
  60. package/dist-electron/services/local-ai-core/src/acp/store/external-store.js +98 -0
  61. package/dist-electron/services/local-ai-core/src/acp/store/local-core-acp-store.js +33 -0
  62. package/dist-electron/services/local-ai-core/src/acp/store/model-provider-store.js +88 -0
  63. package/dist-electron/services/local-ai-core/src/acp/store/schema.js +43 -0
  64. package/dist-electron/services/local-ai-core/src/agents/pi/launch.js +2 -119
  65. package/dist-electron/services/local-ai-core/src/agents/pi/provider-materializer.js +148 -0
  66. package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +65 -10
  67. package/dist-electron/services/local-ai-core/src/channel/shared/session-command-runtime.js +4 -2
  68. package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +139 -29
  69. package/dist-electron/services/local-ai-core/src/execution/agent-execution-backend.js +56 -0
  70. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +18 -57
  71. package/dist-electron/services/local-ai-core/src/kernel/local-core-errors.js +255 -0
  72. package/dist-electron/services/local-ai-core/src/plugins/builtin/catalog.js +78 -0
  73. package/dist-electron/services/local-ai-core/src/router/workspace-route-config.js +3 -1
  74. package/dist-electron/services/local-ai-core/src/router/workspace-router.js +64 -9
  75. package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +5 -2
  76. package/dist-electron/services/local-ai-core/src/runtime/config-migration.js +152 -0
  77. package/dist-electron/services/local-ai-core/src/runtime/deployment-diagnostics.js +175 -0
  78. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +330 -3
  79. package/dist-electron/services/local-ai-core/src/runtime/local-core-runtime-state.js +11 -2
  80. package/dist-electron/services/local-ai-core/src/runtime/provider-config-migration.js +32 -0
  81. package/dist-electron/services/local-ai-core/src/runtime/runtime-detection-service.js +76 -8
  82. package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +62 -0
  83. package/dist-electron/services/local-ai-core/src/runtime/server.js +708 -4
  84. package/dist-electron/services/local-ai-core/src/runtime/standalone.js +13 -3
  85. package/dist-electron/services/local-ai-core/src/sandbox/opensandbox-client.js +113 -0
  86. package/dist-electron/services/local-ai-core/src/sandbox/sandbox-config.js +273 -0
  87. package/dist-electron/services/local-ai-core/src/sandbox/sandbox-manager.js +215 -0
  88. package/dist-electron/services/local-ai-core/src/sandbox/sandbox-stdio-proxy.js +188 -0
  89. package/dist-electron/services/local-ai-core/src/thread/slash-command-registry.js +3 -0
  90. package/dist-electron/services/local-ai-core/src/thread/thread-command-service.js +32 -0
  91. package/dist-electron/services/local-ai-core/src/thread/thread-slash-command-dispatcher.js +71 -0
  92. package/dist-electron/shared/desktop.js +85 -1
  93. package/dist-electron/tests/contracts/architecture-docs.test.js +83 -0
  94. package/dist-electron/tests/electron/plugin-kernel.test.js +225 -18
  95. package/dist-electron/tests/electron/runtime-detection-service.test.js +57 -5
  96. package/dist-electron/tests/electron/sandbox-config.test.js +541 -0
  97. package/dist-electron/tests/electron/workspace-task-store.test.js +257 -0
  98. package/dist-electron/tests/integration/lark-gateway.test.js +59 -0
  99. package/dist-electron/tests/integration/local-core-refactor.test.js +183 -20
  100. package/dist-electron/tests/integration/local-core-routes.test.js +34 -0
  101. package/package.json +2 -3
  102. package/dist/renderer/assets/Config-B93CQGvY.js +0 -16
  103. package/dist/renderer/assets/Dashboard-Ch3QkWwf.js +0 -26
  104. package/dist/renderer/assets/ProjectDetail-DJa3sMmU.js +0 -11
  105. package/dist/renderer/assets/Workspace-BrhSwRQ-.js +0 -14
  106. package/dist/renderer/assets/desktop-C5vBT0yM.js +0 -1
  107. package/dist/renderer/assets/en-D2SkCrhG.js +0 -1
  108. package/dist/renderer/assets/es-C8UBPhxM.js +0 -1
  109. package/dist/renderer/assets/index-CaQZSRbg.css +0 -11
  110. package/dist/renderer/assets/ja-ChF4vbBL.js +0 -1
  111. package/dist/renderer/assets/projects-DwfZwxDW.js +0 -1
  112. package/dist/renderer/assets/zh-DIcdHycM.js +0 -1
  113. package/dist/renderer/assets/zh-TW-BtRwreY_.js +0 -1
@@ -10,6 +10,7 @@ const node_path_1 = require("node:path");
10
10
  const node_os_1 = require("node:os");
11
11
  const local_core_acp_store_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-store.js");
12
12
  const local_core_acp_session_coordinator_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-session-coordinator.js");
13
+ const local_core_acp_transport_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-transport.js");
13
14
  const local_core_acp_turn_coordinator_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js");
14
15
  const scheduler_service_js_1 = require("../../services/local-ai-core/src/scheduler/scheduler-service.js");
15
16
  const lark_schedule_adapter_js_1 = require("../../services/local-ai-core/src/scheduler/lark-schedule-adapter.js");
@@ -40,6 +41,141 @@ const local_core_controller_js_1 = require("../../services/local-ai-core/src/run
40
41
  (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
41
42
  }
42
43
  });
44
+ (0, node_test_1.default)('model providers persist independently from workspace config', () => {
45
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'model-provider-store-'));
46
+ try {
47
+ const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
48
+ const provider = store.upsertModelProvider({
49
+ name: 'deepseek',
50
+ api_key: 'secret',
51
+ base_url: 'https://api.deepseek.com',
52
+ model: 'deepseek-v4-flash',
53
+ env: { CUSTOM_PROVIDER_ENV: '1' },
54
+ });
55
+ strict_1.default.equal(provider.id, 'deepseek');
56
+ strict_1.default.equal(provider.api_key, 'secret');
57
+ strict_1.default.equal(store.listModelProviders().length, 1);
58
+ const reopened = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
59
+ strict_1.default.equal(reopened.getModelProvider('deepseek')?.model, 'deepseek-v4-flash');
60
+ reopened.close();
61
+ store.close();
62
+ }
63
+ finally {
64
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
65
+ }
66
+ });
67
+ (0, node_test_1.default)('external project and thread mappings persist with isolated workspace paths', () => {
68
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'external-store-'));
69
+ try {
70
+ const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
71
+ const project = store.upsertExternalProject({
72
+ userId: 'user-a',
73
+ externalProjectId: 'project-a',
74
+ workspaceId: 'external-user-a-project-a',
75
+ workspacePath: '/data/users/user-a/projects/project-a',
76
+ displayName: 'Project A',
77
+ agentType: 'pi',
78
+ providerId: 'deepseek',
79
+ metadata: { source: 'external' },
80
+ createdAt: '2026-05-15T00:00:00.000Z',
81
+ updatedAt: '2026-05-15T00:00:00.000Z',
82
+ });
83
+ const thread = store.createThread(project.workspaceId, 'Thread A', 'pi');
84
+ store.upsertExternalThread({
85
+ userId: project.userId,
86
+ externalProjectId: project.externalProjectId,
87
+ externalThreadId: 'thread-a',
88
+ workspaceId: project.workspaceId,
89
+ threadId: thread.id,
90
+ workspacePath: '/data/users/user-a/projects/project-a/threads/thread-a/workspace',
91
+ metadata: { channel: 'api' },
92
+ createdAt: '2026-05-15T00:00:00.000Z',
93
+ updatedAt: '2026-05-15T00:00:00.000Z',
94
+ });
95
+ store.close();
96
+ const reopened = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
97
+ strict_1.default.equal(reopened.getExternalProject('user-a', 'project-a')?.workspacePath, '/data/users/user-a/projects/project-a');
98
+ strict_1.default.equal(reopened.getExternalThreadByThreadId(thread.id)?.workspacePath, '/data/users/user-a/projects/project-a/threads/thread-a/workspace');
99
+ reopened.close();
100
+ }
101
+ finally {
102
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
103
+ }
104
+ });
105
+ (0, node_test_1.default)('controller migrates embedded project providers into shared provider store', async () => {
106
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'model-provider-migration-'));
107
+ try {
108
+ const runtime = (0, bootstrap_js_1.bootstrapLocalCoreRuntime)({
109
+ userDataPath,
110
+ enableKnowledge: false,
111
+ log: () => { },
112
+ });
113
+ const controller = new local_core_controller_js_1.LocalCoreController(userDataPath, runtime);
114
+ await controller.saveStructuredConfigFile({
115
+ projects: [{
116
+ name: 'workspace-a',
117
+ agent: {
118
+ type: 'pi',
119
+ options: { work_dir: '/tmp/workspace-a' },
120
+ providers: [{
121
+ name: 'deepseek-v4-flash',
122
+ api_key: 'secret',
123
+ base_url: 'https://api.deepseek.com',
124
+ model: 'deepseek-v4-flash',
125
+ }],
126
+ },
127
+ platforms: [],
128
+ }],
129
+ });
130
+ const config = await controller.readConfigFile();
131
+ strict_1.default.equal(config.parsed?.projects?.[0]?.agent.options?.provider_id, 'deepseek');
132
+ strict_1.default.equal(config.parsed?.projects?.[0]?.agent.providers, undefined);
133
+ const providers = await controller.listModelProviders();
134
+ strict_1.default.equal(providers.providers[0]?.id, 'deepseek');
135
+ strict_1.default.equal(providers.providers[0]?.name, 'deepseek');
136
+ await controller.close();
137
+ }
138
+ finally {
139
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
140
+ }
141
+ });
142
+ (0, node_test_1.default)('workspace router resolves projects that select a shared provider', async () => {
143
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'workspace-provider-ref-'));
144
+ try {
145
+ const runtime = (0, bootstrap_js_1.bootstrapLocalCoreRuntime)({
146
+ userDataPath,
147
+ enableKnowledge: false,
148
+ log: () => { },
149
+ });
150
+ const controller = new local_core_controller_js_1.LocalCoreController(userDataPath, runtime);
151
+ const provider = await controller.createModelProvider({
152
+ name: 'deepseek',
153
+ api_key: 'secret',
154
+ base_url: 'https://api.deepseek.com',
155
+ model: 'deepseek-v4-flash',
156
+ });
157
+ await controller.saveStructuredConfigFile({
158
+ projects: [{
159
+ name: 'workspace-a',
160
+ agent: {
161
+ type: 'pi',
162
+ options: {
163
+ work_dir: '/tmp/workspace-a',
164
+ provider_id: provider.id,
165
+ },
166
+ },
167
+ platforms: [],
168
+ }],
169
+ });
170
+ const workspaces = await controller.listWorkspaces();
171
+ strict_1.default.equal(workspaces[0]?.id, 'workspace-a');
172
+ strict_1.default.equal(workspaces[0]?.agentType, 'pi');
173
+ await controller.close();
174
+ }
175
+ finally {
176
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
177
+ }
178
+ });
43
179
  (0, node_test_1.default)('scheduler create resolves a Lark delivery route without binding the job to a thread', async () => {
44
180
  const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-binding-route-'));
45
181
  try {
@@ -443,6 +579,50 @@ const local_core_controller_js_1 = require("../../services/local-ai-core/src/run
443
579
  (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
444
580
  }
445
581
  });
582
+ (0, node_test_1.default)('ACP runtime PATH includes service-safe user bin directories', () => {
583
+ const previousHome = process.env.HOME;
584
+ process.env.HOME = '/home/agentdock-test';
585
+ try {
586
+ const path = (0, local_core_acp_session_coordinator_js_1.buildAgentPath)('/usr/bin:/bin', '/opt/agentdock/bin');
587
+ strict_1.default.equal(path, '/opt/agentdock/bin:/home/agentdock-test/.local/bin:/home/agentdock-test/bin:/usr/bin:/bin');
588
+ }
589
+ finally {
590
+ if (previousHome === undefined) {
591
+ delete process.env.HOME;
592
+ }
593
+ else {
594
+ process.env.HOME = previousHome;
595
+ }
596
+ }
597
+ });
598
+ (0, node_test_1.default)('ACP transport reports missing agent commands without an unhandled process error', async () => {
599
+ const closed = new Promise((resolve) => {
600
+ const transport = new local_core_acp_transport_js_1.LocalCoreAcpTransport({
601
+ log: () => { },
602
+ onAgentRequest: () => { },
603
+ onAgentNotification: () => { },
604
+ onSessionClosed: (_session, error) => {
605
+ resolve(error);
606
+ },
607
+ });
608
+ transport.spawnSession({
609
+ threadId: 'thread-missing-command',
610
+ bridgeSessionKey: 'session:missing-command',
611
+ config: {
612
+ workspaceId: 'workspace-a',
613
+ agentType: 'hermes',
614
+ command: 'agentdock-command-that-does-not-exist',
615
+ args: ['acp'],
616
+ env: {},
617
+ workDir: (0, node_os_1.tmpdir)(),
618
+ model: '',
619
+ },
620
+ runtimeEnv: {},
621
+ });
622
+ });
623
+ const error = await closed;
624
+ strict_1.default.equal(error.message, 'ACP agent command not found: agentdock-command-that-does-not-exist');
625
+ });
446
626
  (0, node_test_1.default)('ACP scheduled session can override permission mode without changing thread mode', async () => {
447
627
  const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-permission-mode-'));
448
628
  try {
@@ -515,6 +695,83 @@ const local_core_controller_js_1 = require("../../services/local-ai-core/src/run
515
695
  (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
516
696
  }
517
697
  });
698
+ (0, node_test_1.default)('ACP sandbox sessions use container workspace cwd', async () => {
699
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'sandbox-session-cwd-'));
700
+ try {
701
+ const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
702
+ const thread = store.createThread('workspace-a', 'Thread');
703
+ let sessionNewParams;
704
+ const coordinator = new local_core_acp_session_coordinator_js_1.LocalCoreAcpSessionCoordinator({
705
+ store,
706
+ transport: {
707
+ spawnSession(input) {
708
+ return {
709
+ threadId: input.threadId,
710
+ bridgeSessionKey: input.bridgeSessionKey,
711
+ closed: false,
712
+ sessionId: '',
713
+ supportsLoad: false,
714
+ pendingPermissionByRun: new Map(),
715
+ schedulerJobCreatedByRun: new Map(),
716
+ launchPermissionMode: '',
717
+ };
718
+ },
719
+ initializeSession: async () => { },
720
+ request: async (_session, method, params) => {
721
+ if (method === 'session/new') {
722
+ sessionNewParams = params;
723
+ return { sessionId: 'session-1' };
724
+ }
725
+ return {};
726
+ },
727
+ closeSession: () => { },
728
+ closeSessionWithError: () => { },
729
+ sendRaw: () => true,
730
+ },
731
+ runThreadMap: new Map(),
732
+ emitBridge: () => { },
733
+ });
734
+ await coordinator.ensureSession(thread.id, 'session:thread-1', {
735
+ workspaceId: 'workspace-a',
736
+ agentType: 'pi',
737
+ command: process.execPath,
738
+ args: ['/host/sandbox-stdio-proxy.js'],
739
+ env: {},
740
+ workDir: '/host/workspace-a',
741
+ model: '',
742
+ sandbox: {
743
+ enabled: true,
744
+ provider: 'opensandbox',
745
+ transport: 'http-ndjson',
746
+ serverUrl: 'http://127.0.0.1:8080',
747
+ apiKeyEnv: 'OPEN_SANDBOX_API_KEY',
748
+ image: 'agentdock/pi-acp:local',
749
+ acpPort: 8080,
750
+ entrypoint: ['node', '/opt/agentdock/acp-bridge.mjs'],
751
+ timeoutSeconds: 600,
752
+ lifecycle: 'per_thread',
753
+ idleSeconds: 900,
754
+ warmPoolSize: 0,
755
+ cpu: '1000m',
756
+ memory: '1Gi',
757
+ userId: 'local',
758
+ projectId: 'workspace-a',
759
+ stateScope: 'project',
760
+ workspaceHostPath: '/host/workspace-a',
761
+ workspaceMountPath: '/workspace',
762
+ stateHostPath: '/host/state',
763
+ stateMountPath: '/agent-state',
764
+ runtimeCommand: '/usr/local/bin/pi-acp',
765
+ runtimeArgs: [],
766
+ runtimeEnv: {},
767
+ },
768
+ });
769
+ strict_1.default.equal(sessionNewParams?.cwd, '/workspace');
770
+ }
771
+ finally {
772
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
773
+ }
774
+ });
518
775
  (0, node_test_1.default)('ACP permission requests honor scheduled session permission override', () => {
519
776
  const sentPayloads = [];
520
777
  let approvalCreated = false;
@@ -687,6 +687,65 @@ function findLarkPostMdText(content) {
687
687
  strict_1.default.deepEqual(patchedCards.map((card) => card.text), []);
688
688
  strict_1.default.ok(!createdCards.some((card) => /处理中|正在思考/.test(card.text)));
689
689
  });
690
+ (0, node_test_1.default)('lark gateway records structured error state when startup fails', async () => {
691
+ const emittedEvents = [];
692
+ const gateway = new local_core_lark_gateway_js_1.LocalCoreLarkGateway({
693
+ store: {
694
+ expirePendingPairings: () => { },
695
+ listPendingPairings: () => [],
696
+ listAuthorizedUsers: () => [],
697
+ },
698
+ readConfig: async () => null,
699
+ getWorkspaceRouter: () => ({}),
700
+ eventBus: {
701
+ emit: (event) => emittedEvents.push(event),
702
+ on: () => () => { },
703
+ },
704
+ });
705
+ gateway.larkModulePromise = Promise.resolve({
706
+ AppType: { SelfBuild: 'SelfBuild' },
707
+ Domain: { Feishu: 'Feishu' },
708
+ LoggerLevel: { info: 'info' },
709
+ Client: class {
710
+ request() {
711
+ return Promise.resolve({ bot: { open_id: 'bot-open-id' } });
712
+ }
713
+ },
714
+ EventDispatcher: class {
715
+ register() { }
716
+ },
717
+ WSClient: class {
718
+ start() {
719
+ throw new Error('invalid app credentials');
720
+ }
721
+ on() { }
722
+ },
723
+ });
724
+ await gateway.startWorkspace({
725
+ workspaceId: 'workspace-1',
726
+ instanceId: 'default',
727
+ displayName: 'Workspace 1',
728
+ platformKey: 'lark',
729
+ appId: 'cli_a1',
730
+ appSecret: 'secret',
731
+ encryptKey: '',
732
+ verificationToken: '',
733
+ autoApprove: false,
734
+ cardActionsEnabled: true,
735
+ groupReplyAll: false,
736
+ enabled: true,
737
+ brand: 'lark',
738
+ project: { name: 'workspace-1' },
739
+ });
740
+ const status = gateway.getStatus('workspace-1');
741
+ strict_1.default.equal(status.status, 'error');
742
+ strict_1.default.equal(status.connected, false);
743
+ strict_1.default.equal(status.lastErrorInfo?.code, 'channel_auth_failed');
744
+ strict_1.default.equal(status.lastErrorInfo?.suggestedAction, 'Check app credentials and restart the Lark gateway.');
745
+ strict_1.default.match(status.lastError || '', /invalid app credentials/);
746
+ strict_1.default.ok(status.lastErrorAt);
747
+ strict_1.default.equal(emittedEvents.some((event) => event.type === 'localcore.error' && event.payload?.scope === 'channel.lark'), true);
748
+ });
690
749
  (0, node_test_1.default)('lark permission requests render as clickable card buttons', async () => {
691
750
  const createdCards = [];
692
751
  const patchedCards = [];
@@ -15,10 +15,11 @@ const scheduler_run_lifecycle_js_1 = require("../../services/local-ai-core/src/s
15
15
  const lark_execution_policies_js_1 = require("../../services/local-ai-core/src/scheduler/lark-execution-policies.js");
16
16
  const local_schedule_adapter_js_1 = require("../../services/local-ai-core/src/scheduler/local-schedule-adapter.js");
17
17
  const cards_js_1 = require("../../services/local-ai-core/src/channel/lark/cards.js");
18
+ const session_command_runtime_js_1 = require("../../services/local-ai-core/src/channel/shared/session-command-runtime.js");
18
19
  const local_core_acp_backend_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-backend.js");
19
20
  const local_core_acp_store_js_1 = require("../../services/local-ai-core/src/acp/local-core-acp-store.js");
20
21
  const session_command_service_js_1 = require("../../services/local-ai-core/src/thread/session-command-service.js");
21
- const thread_command_service_js_1 = require("../../services/local-ai-core/src/thread/thread-command-service.js");
22
+ const thread_slash_command_dispatcher_js_1 = require("../../services/local-ai-core/src/thread/thread-slash-command-dispatcher.js");
22
23
  (0, node_test_1.default)('response processor derives slash fallback replies and cron system responses', async () => {
23
24
  const processor = new local_core_acp_response_processor_js_1.LocalCoreAcpResponseProcessor({
24
25
  getScheduledDeliveryBinding: (threadId) => threadId === 'thread-1'
@@ -76,7 +77,15 @@ const thread_command_service_js_1 = require("../../services/local-ai-core/src/th
76
77
  availableAgents: ['codex', 'pi', 'hermes'],
77
78
  }), /当前线程 Agent:pi/);
78
79
  });
79
- (0, node_test_1.default)('thread command service handles local slash commands without ACP transport', async () => {
80
+ (0, node_test_1.default)('slash help command exposes global local command help', () => {
81
+ const help = (0, thread_slash_command_dispatcher_js_1.slashHelpText)();
82
+ strict_1.default.match(help, /`\/help`/);
83
+ strict_1.default.match(help, /`\/stop`/);
84
+ strict_1.default.match(help, /`\/mode /);
85
+ strict_1.default.match(help, /`\/agent /);
86
+ strict_1.default.match(help, /`\/list`/);
87
+ });
88
+ (0, node_test_1.default)('thread slash command dispatcher handles local slash commands without ACP transport', async () => {
80
89
  const row = {
81
90
  id: 'thread-1',
82
91
  workspace_id: 'workspace-a',
@@ -95,24 +104,39 @@ const thread_command_service_js_1 = require("../../services/local-ai-core/src/th
95
104
  const audits = [];
96
105
  const syncedModes = [];
97
106
  const closedThreads = [];
98
- const service = new thread_command_service_js_1.ThreadCommandService({
99
- getThreadRow: () => row,
100
- updateThreadAgentMode: (_threadId, mode) => {
101
- row.agent_mode = mode;
102
- },
103
- updateThreadAgentType: (_threadId, agentType) => {
104
- row.agent_type = agentType;
105
- },
106
- getLatestRunForThread: () => undefined,
107
- createAuditEvent: (input) => {
108
- audits.push(input);
109
- },
110
- getAgentTypes: () => ['codex', 'pi', 'hermes', 'claudecode'],
111
- setThreadMode: async (_threadId, mode) => {
112
- syncedModes.push(mode);
107
+ const interruptedRuns = [];
108
+ let latestRun;
109
+ const service = new thread_slash_command_dispatcher_js_1.ThreadSlashCommandDispatcher({
110
+ session: {
111
+ listThreads: () => [],
112
+ getThread: () => ({ id: row.id, title: row.title, messages: [] }),
113
+ createThread: () => ({ id: 'created-thread', title: 'Created', messages: [] }),
114
+ renameThread: () => ({ id: row.id, title: 'Renamed', messages: [] }),
115
+ deleteThread: () => ({ deleted: true }),
113
116
  },
114
- closeThreadSession: (threadId) => {
115
- closedThreads.push(threadId);
117
+ thread: {
118
+ getThreadRow: () => row,
119
+ updateThreadAgentMode: (_threadId, mode) => {
120
+ row.agent_mode = mode;
121
+ },
122
+ updateThreadAgentType: (_threadId, agentType) => {
123
+ row.agent_type = agentType;
124
+ },
125
+ getLatestRunForThread: () => latestRun,
126
+ createAuditEvent: (input) => {
127
+ audits.push(input);
128
+ },
129
+ getAgentTypes: () => ['codex', 'pi', 'hermes', 'claudecode'],
130
+ setThreadMode: async (_threadId, mode) => {
131
+ syncedModes.push(mode);
132
+ },
133
+ closeThreadSession: (threadId) => {
134
+ closedThreads.push(threadId);
135
+ },
136
+ interruptRun: async (runId) => {
137
+ interruptedRuns.push(runId);
138
+ return { interrupted: true };
139
+ },
116
140
  },
117
141
  });
118
142
  strict_1.default.deepEqual(await service.execute({
@@ -121,6 +145,32 @@ const thread_command_service_js_1 = require("../../services/local-ai-core/src/th
121
145
  content: '/not-local',
122
146
  defaultAgentType: 'codex',
123
147
  }), { handled: false, displayText: '' });
148
+ const inactiveStopResult = await service.execute({
149
+ threadId: row.id,
150
+ workspaceId: row.workspace_id,
151
+ content: '/stop',
152
+ defaultAgentType: 'codex',
153
+ });
154
+ strict_1.default.equal(inactiveStopResult.handled, true);
155
+ strict_1.default.match(inactiveStopResult.displayText, /没有正在运行的任务/);
156
+ latestRun = {
157
+ id: 'run-active',
158
+ thread_id: row.id,
159
+ status: 'running',
160
+ started_at: '2026-05-11T00:00:00.000Z',
161
+ updated_at: '2026-05-11T00:00:00.000Z',
162
+ };
163
+ const stopResult = await service.execute({
164
+ threadId: row.id,
165
+ workspaceId: row.workspace_id,
166
+ content: '/stop',
167
+ defaultAgentType: 'codex',
168
+ });
169
+ strict_1.default.equal(stopResult.handled, true);
170
+ strict_1.default.match(stopResult.displayText, /已请求停止当前任务/);
171
+ strict_1.default.deepEqual(interruptedRuns, ['run-active']);
172
+ strict_1.default.equal(audits.at(-1)?.type, 'task.updated');
173
+ latestRun = undefined;
124
174
  const modeResult = await service.execute({
125
175
  threadId: row.id,
126
176
  workspaceId: row.workspace_id,
@@ -153,6 +203,111 @@ const thread_command_service_js_1 = require("../../services/local-ai-core/src/th
153
203
  strict_1.default.match(resetResult.displayText, /回到默认 Agent:codex/);
154
204
  strict_1.default.deepEqual(closedThreads, ['thread-1', 'thread-1']);
155
205
  });
206
+ (0, node_test_1.default)('channel session command runtime uses unified slash dispatcher for thread commands', async () => {
207
+ const row = {
208
+ id: 'thread-1',
209
+ workspace_id: 'workspace-a',
210
+ session_id: 'session-1',
211
+ bridge_session_key: 'bridge-1',
212
+ title: 'Channel command',
213
+ agent_type: 'codex',
214
+ created_at: '2026-05-11T00:00:00.000Z',
215
+ updated_at: '2026-05-11T00:00:00.000Z',
216
+ history_count: 0,
217
+ excerpt: '',
218
+ acp_session_id: null,
219
+ acp_supports_load: 0,
220
+ agent_mode: 'default',
221
+ };
222
+ const sentResults = [];
223
+ const interruptedRuns = [];
224
+ const routes = new Map();
225
+ const dispatcher = new thread_slash_command_dispatcher_js_1.ThreadSlashCommandDispatcher({
226
+ session: {
227
+ listThreads: () => [],
228
+ getThread: (threadId) => ({ id: threadId, title: row.title, messages: [] }),
229
+ createThread: () => ({ id: 'created-thread', title: 'Created channel thread', messages: [] }),
230
+ renameThread: (threadId, title) => ({ id: threadId, title, messages: [] }),
231
+ deleteThread: () => ({ deleted: true }),
232
+ },
233
+ thread: {
234
+ getThreadRow: () => row,
235
+ updateThreadAgentMode: (_threadId, mode) => {
236
+ row.agent_mode = mode;
237
+ },
238
+ updateThreadAgentType: (_threadId, agentType) => {
239
+ row.agent_type = agentType;
240
+ },
241
+ getLatestRunForThread: () => ({
242
+ id: 'run-channel',
243
+ thread_id: row.id,
244
+ status: 'running',
245
+ started_at: '2026-05-11T00:00:00.000Z',
246
+ updated_at: '2026-05-11T00:00:00.000Z',
247
+ }),
248
+ createAuditEvent: () => { },
249
+ getAgentTypes: () => ['codex', 'pi'],
250
+ interruptRun: async (runId) => {
251
+ interruptedRuns.push(runId);
252
+ return { interrupted: true };
253
+ },
254
+ },
255
+ });
256
+ const runtime = new session_command_runtime_js_1.ChannelSessionCommandRuntime({
257
+ dispatcher,
258
+ store: {
259
+ getThreadRow: () => row,
260
+ updateThreadAgentMode: (_threadId, mode) => {
261
+ row.agent_mode = mode;
262
+ },
263
+ updateAuthorizedUserThread: () => { },
264
+ upsertPlatformThreadBinding: () => { },
265
+ },
266
+ getThreadSessionKey: (threadId) => `session:${threadId}`,
267
+ setThreadRoute: (sessionKey, route) => {
268
+ routes.set(sessionKey, route);
269
+ },
270
+ createRoute: (input, threadId) => ({
271
+ workspaceId: input.workspaceId,
272
+ instanceId: input.instanceId,
273
+ platformKey: input.platformKey,
274
+ platformUserId: input.platformUserId,
275
+ chatId: input.chatId,
276
+ threadId,
277
+ }),
278
+ sendResult: async (_input, result) => {
279
+ sentResults.push(result);
280
+ },
281
+ });
282
+ const stopResult = await runtime.execute({
283
+ workspaceId: 'workspace-a',
284
+ currentThreadId: row.id,
285
+ text: '/stop',
286
+ defaultTitle: 'Channel thread',
287
+ defaultAgentType: 'codex',
288
+ chatId: 'chat-1',
289
+ platformUserId: 'user-1',
290
+ platformKey: 'lark',
291
+ instanceId: 'default',
292
+ });
293
+ strict_1.default.equal(stopResult.handled, true);
294
+ strict_1.default.deepEqual(interruptedRuns, ['run-channel']);
295
+ strict_1.default.match(sentResults.at(-1)?.displayText || '', /已请求停止当前任务/);
296
+ const newResult = await runtime.execute({
297
+ workspaceId: 'workspace-a',
298
+ currentThreadId: row.id,
299
+ text: '/new Follow up',
300
+ defaultTitle: 'Channel thread',
301
+ defaultAgentType: 'codex',
302
+ chatId: 'chat-1',
303
+ platformUserId: 'user-1',
304
+ platformKey: 'lark',
305
+ instanceId: 'default',
306
+ });
307
+ strict_1.default.equal(newResult.handled, true);
308
+ strict_1.default.equal(newResult.threadId, 'created-thread');
309
+ strict_1.default.equal(routes.get('session:created-thread')?.threadId, 'created-thread');
310
+ });
156
311
  (0, node_test_1.default)('thread agent mode persists with thread state', () => {
157
312
  const dir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentdock-mode-'));
158
313
  try {
@@ -193,9 +348,10 @@ const thread_command_service_js_1 = require("../../services/local-ai-core/src/th
193
348
  const thread = store.createThread('workspace-a', 'Agent command', 'codex');
194
349
  const bridgeEvents = [];
195
350
  const acceptedMessages = [];
351
+ const runThreadMap = new Map();
196
352
  const backend = new local_core_acp_backend_js_1.LocalCoreAcpBackend({
197
353
  store,
198
- runThreadMap: new Map(),
354
+ runThreadMap,
199
355
  emitBridge: (event) => bridgeEvents.push(event),
200
356
  eventBus: {
201
357
  emit: (event) => acceptedMessages.push(event),
@@ -236,6 +392,13 @@ const thread_command_service_js_1 = require("../../services/local-ai-core/src/th
236
392
  await backend.sendThreadMessage(thread.id, '/agent use hermes', config);
237
393
  strict_1.default.equal(store.getThreadRow(thread.id)?.agent_type, 'hermes');
238
394
  strict_1.default.match(store.getThread(thread.id, []).messages.at(-1)?.content || '', /下一轮开始生效/);
395
+ store.updateRun('run-stop', thread.id, 'running');
396
+ runThreadMap.set('run-stop', thread.id);
397
+ await backend.sendThreadMessage(thread.id, '/stop', config);
398
+ strict_1.default.equal(store.getRun('run-stop')?.status, 'interrupted');
399
+ strict_1.default.match(store.getThread(thread.id, []).messages.at(-1)?.content || '', /已将当前任务标记为停止|已请求停止当前任务/);
400
+ await backend.sendThreadMessage(thread.id, '/help', config);
401
+ strict_1.default.match(store.getThread(thread.id, []).messages.at(-1)?.content || '', /`\/stop`/);
239
402
  strict_1.default.ok(acceptedMessages.length >= 8);
240
403
  backend.close();
241
404
  store.close();
@@ -81,6 +81,40 @@ const server_routes_js_1 = require("../../services/local-ai-core/src/runtime/ser
81
81
  strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/workspace-security/workspace-1'), null);
82
82
  strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/workspaces/workspace-1/streaming-probe'), null);
83
83
  });
84
+ (0, node_test_1.default)('local core route parser handles shared model provider routes', () => {
85
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/providers'), { name: 'providers.list' });
86
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/providers'), { name: 'providers.create' });
87
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('PUT', '/api/local/v1/providers/deepseek'), {
88
+ name: 'provider.update',
89
+ providerId: 'deepseek',
90
+ });
91
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('DELETE', '/api/local/v1/providers/deepseek'), {
92
+ name: 'provider.delete',
93
+ providerId: 'deepseek',
94
+ });
95
+ strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/providers/deepseek/extra'), null);
96
+ });
97
+ (0, node_test_1.default)('local core route parser handles external project and run routes', () => {
98
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/external/projects'), {
99
+ name: 'external.project.ensure',
100
+ });
101
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/external/runs'), {
102
+ name: 'external.run.create',
103
+ });
104
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/external/runs/run%2Fone/events'), {
105
+ name: 'external.run.events',
106
+ runId: 'run/one',
107
+ });
108
+ strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/external/projects'), null);
109
+ strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/external/runs/run-1/events'), null);
110
+ });
111
+ (0, node_test_1.default)('local core route parser handles OpenAI-compatible chat completions route', () => {
112
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/openai/chat/completions'), {
113
+ name: 'openai.chat.completions',
114
+ });
115
+ strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/openai/chat/completions'), null);
116
+ strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/openai/completions'), null);
117
+ });
84
118
  (0, node_test_1.default)('local core route parser keeps approval resolution separate from approval detail', () => {
85
119
  strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/approvals'), { name: 'approvals.list' });
86
120
  strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/approvals'), { name: 'approvals.create' });