@yancyyu/openhermit 1.5.8 → 1.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/dist-renderer/assets/{ProjectEditorOverlay-BNoDw9T1.js → ProjectEditorOverlay-C5D83Zxv.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-CfGRKQIu.js → TeamGraphOverlay-ajzuM1-u.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-Ct8Hm5_h.js → _basePickBy-C9H2zmVj.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-BofrAFBx.js → _baseUniq-CpGZGemc.js} +1 -1
- package/dist-renderer/assets/{arc-AbJgatzR.js → arc-CbGBDw-m.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-gpniCJVk.js → architectureDiagram-VXUJARFQ-nuKXUIpb.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-aBbbmONC.js → blockDiagram-VD42YOAC-DHUUE7Jc.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DJio1IsU.js → c4Diagram-YG6GDRKO-OILHhqLM.js} +1 -1
- package/dist-renderer/assets/channel-DpUKLLrj.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-D1_HKao2.js → chunk-4BX2VUAB-dqNpZaQ8.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-NAmVxF4k.js → chunk-55IACEB6-BCoSJQM-.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-Ce829laz.js → chunk-B4BG7PRW-BLbg8yVR.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-Ct2Le12y.js → chunk-DI55MBZ5-CUUWOs1Q.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-Cie3DzKk.js → chunk-FMBD7UC4-D9geTN5P.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-4f5Yb50e.js → chunk-QN33PNHL-BGT8-BRX.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-D9ranl9c.js → chunk-QZHKN3VN-CC8ebGaM.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-bdGZWlEy.js → chunk-TZMSLE5B-CajekcT6.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-LL85aSlz.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-LL85aSlz.js +1 -0
- package/dist-renderer/assets/clone-BHWsFzFA.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-C6tvfcVi.js → cose-bilkent-S5V4N54A-C_x7hSy3.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-B-4qcZam.js → dagre-6UL2VRFP-C4Y1k4DZ.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-CwT3TLjx.js → diagram-PSM6KHXK-oRIeULoh.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-BWH6-ZFd.js → diagram-QEK2KX5R-DwSqw5HF.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DfpPnfi1.js → diagram-S2PKOQOG-DqjGYje2.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BFbEFR4x.js → erDiagram-Q2GNP2WA-CEV5bBgg.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-Dg3cf5hW.js → flowDiagram-NV44I4VS-BQQkrRyu.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-B21y55W5.js → ganttDiagram-JELNMOA3-CLy4WR1G.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-BDV3BJzn.js → gitGraphDiagram-V2S2FVAM-6W3ioQu_.js} +1 -1
- package/dist-renderer/assets/{graph-BfaZ4hZt.js → graph-BnLKQvhH.js} +1 -1
- package/dist-renderer/assets/{index-CCqtDawH.js → index-B4aiRxoU.js} +1 -1
- package/dist-renderer/assets/{index-pMg_LlsS.js → index-B8lKqPVq.js} +1 -1
- package/dist-renderer/assets/{index-CZltVMDP.js → index-BRuhNKyU.js} +12 -12
- package/dist-renderer/assets/{index-BMXHMpkG.js → index-BufvLVIl.js} +1 -1
- package/dist-renderer/assets/{index-Ct0-y9TF.js → index-C1xShqKH.js} +1 -1
- package/dist-renderer/assets/{index-CVMSpK8C.js → index-zIOLLI7O.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DvMlS0CL.js → infoDiagram-HS3SLOUP-BoBweEEY.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DIyMluRv.js → journeyDiagram-XKPGCS4Q-DLL0V5oP.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CVOx8f-7.js → kanban-definition-3W4ZIXB7-HsFtEDG3.js} +1 -1
- package/dist-renderer/assets/{layout-BPKIXUf4.js → layout-ClIooAAq.js} +1 -1
- package/dist-renderer/assets/{linear-CScZGLr2.js → linear-r3RJcj8y.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-CmDQ7Wo6.js → mindmap-definition-VGOIOE7T-BA_P1U4V.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-DbVClin-.js → pieDiagram-ADFJNKIX-CzPAfkTB.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CAB0MYcW.js → quadrantDiagram-AYHSOK5B-PvdPWzFJ.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-w2Lfpg3T.js → requirementDiagram-UZGBJVZJ-CHqIL_Od.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-kvG1QoKY.js → sankeyDiagram-TZEHDZUN-ConzpACM.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-DCVBQ23J.js → sequenceDiagram-WL72ISMW-Zryq4oxP.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-ItZ0JBvq.js → stateDiagram-FKZM4ZOC-BA9V7NHF.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-Hpmw4dMm.js → stateDiagram-v2-4FDKWEC3-CGnaujD-.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BzSFaAjV.js → timeline-definition-IT6M3QCI-DPs2ZjMm.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-fSz4hQn0.js → treemap-GDKQZRPO-B0lzrLxb.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CT1kaGlv.js → xychartDiagram-PRI3JC2R-CINGmMxX.js} +1 -1
- package/dist-renderer/index.html +1 -1
- package/package.json +2 -1
- package/src/main/server.ts +993 -764
- package/src/main/services/UpdateService.ts +4 -1
- package/src/main/services/ccConnect/CcConnectBridge.ts +1 -8
- package/src/main/services/ccConnect/CcConnectClient.ts +7 -2
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +14 -4
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +11 -6
- package/src/renderer/App.tsx +18 -7
- package/src/renderer/api/httpClient.ts +136 -42
- package/src/renderer/components/chat/ChatHistory.tsx +11 -8
- package/src/renderer/components/dashboard/DashboardView.tsx +4 -2
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +2 -7
- package/src/renderer/components/layout/Sidebar.tsx +3 -1
- package/src/renderer/components/schedules/SchedulesView.tsx +15 -13
- package/src/renderer/components/settings/SettingsTabs.tsx +2 -1
- package/src/renderer/components/settings/hooks/useSettingsHandlers.ts +4 -5
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +19 -4
- package/src/renderer/components/settings/sections/CliStatusSection.tsx +63 -59
- package/src/renderer/components/settings/sections/GeneralSection.tsx +5 -11
- package/src/renderer/components/settings/sections/HarnessSection.tsx +30 -15
- package/src/renderer/components/settings/sections/PlatformsSection.tsx +110 -51
- package/src/renderer/components/sidebar/SidebarSessions.tsx +100 -67
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +26 -43
- package/src/renderer/components/team/CcSessionsSection.tsx +34 -14
- package/src/renderer/components/team/TeamDetailView.tsx +150 -148
- package/src/renderer/components/team/TeamEmptyState.tsx +27 -16
- package/src/renderer/components/team/TeamListView.tsx +4 -2
- package/src/renderer/components/team/activity/ActivityItem.tsx +6 -1
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +282 -75
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +2 -1
- package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +64 -21
- package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +68 -19
- package/src/renderer/components/team/dialogs/ProjectPathSelector.tsx +20 -16
- package/src/renderer/components/team/dialogs/platformMeta.ts +66 -11
- package/src/renderer/components/team/editor/EditorFileTree.tsx +9 -7
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +7 -10
- package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +1 -3
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +1 -5
- package/src/renderer/components/team/messages/MessageComposer.tsx +3 -1
- package/src/renderer/components/team/messages/MessagesPanel.tsx +34 -26
- package/src/renderer/components/team/schedule/CcCronScheduleDialog.tsx +1 -3
- package/src/renderer/components/team/schedule/ScheduleSection.tsx +9 -10
- package/src/renderer/store/slices/scheduleSlice.ts +4 -1
- package/src/renderer/store/slices/teamSlice.ts +3 -1
- package/src/shared/types/api.ts +70 -21
- package/src/shared/utils/leadDetection.ts +5 -1
- package/tsconfig.json +26 -0
- package/dist-renderer/assets/channel-CZ8sd5Xf.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-CMcfSKj5.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CMcfSKj5.js +0 -1
- package/dist-renderer/assets/clone-CMuwA8RV.js +0 -1
package/src/main/server.ts
CHANGED
|
@@ -108,7 +108,10 @@ function loadConfig(): HermitConfig {
|
|
|
108
108
|
const tomlBridgeToken = readCcConnectTomlToken('bridge');
|
|
109
109
|
const defaults: HermitConfig = {
|
|
110
110
|
ccBaseUrl: process.env.CC_CONNECT_BASE_URL ?? 'http://127.0.0.1:9820',
|
|
111
|
-
ccToken:
|
|
111
|
+
ccToken:
|
|
112
|
+
process.env.CC_CONNECT_TOKEN ||
|
|
113
|
+
process.env.CC_CONNECT_MANAGEMENT_TOKEN ||
|
|
114
|
+
tomlManagementToken,
|
|
112
115
|
ccBridgeUrl: process.env.CC_CONNECT_BRIDGE_URL ?? 'ws://127.0.0.1:9810/bridge/ws',
|
|
113
116
|
ccBridgeToken:
|
|
114
117
|
process.env.CC_CONNECT_BRIDGE_TOKEN ||
|
|
@@ -123,7 +126,9 @@ function loadConfig(): HermitConfig {
|
|
|
123
126
|
const raw = JSON.parse(readFileSync(HERMIT_CONFIG_FILE, 'utf-8')) as Partial<HermitConfig>;
|
|
124
127
|
merged = { ...defaults, ...raw };
|
|
125
128
|
}
|
|
126
|
-
} catch {
|
|
129
|
+
} catch {
|
|
130
|
+
/* ignore parse errors */
|
|
131
|
+
}
|
|
127
132
|
if (!merged.ccBridgeToken.trim()) {
|
|
128
133
|
merged = { ...merged, ccBridgeToken: tomlBridgeToken || merged.ccToken };
|
|
129
134
|
}
|
|
@@ -190,10 +195,10 @@ function normalizePlatformAllowFrom(value: unknown): Record<string, string> {
|
|
|
190
195
|
return {};
|
|
191
196
|
}
|
|
192
197
|
const entries = Object.entries(value as Record<string, unknown>)
|
|
193
|
-
.map(
|
|
194
|
-
platform
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
.map(
|
|
199
|
+
([platform, allowFrom]) =>
|
|
200
|
+
[platform.trim(), typeof allowFrom === 'string' ? allowFrom.trim() : ''] as const
|
|
201
|
+
)
|
|
197
202
|
.filter(([platform, allowFrom]) => platform.length > 0 && allowFrom.length > 0);
|
|
198
203
|
return Object.fromEntries(entries);
|
|
199
204
|
}
|
|
@@ -224,13 +229,15 @@ bridge.on('reply', (msg) => {
|
|
|
224
229
|
const teamName = resolveTeamFromSessionKey(sessionKey) ?? sessionKey;
|
|
225
230
|
|
|
226
231
|
// 存储 agent 回复到本地
|
|
227
|
-
svc
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
232
|
+
svc
|
|
233
|
+
.appendMessage(teamName, {
|
|
234
|
+
from: teamName,
|
|
235
|
+
to: 'user',
|
|
236
|
+
role: 'agent',
|
|
237
|
+
content: (msg as { content?: string }).content ?? '',
|
|
238
|
+
meta: { sessionKey },
|
|
239
|
+
})
|
|
240
|
+
.catch(() => {});
|
|
234
241
|
|
|
235
242
|
// 广播 inbox 事件 — 前端收到后会调 scheduleTrackedTeamMessageRefresh 重拉消息
|
|
236
243
|
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
@@ -245,13 +252,15 @@ bridge.on('reply_stream', (msg) => {
|
|
|
245
252
|
// 流式结束,存储完整回复
|
|
246
253
|
const fullText = (msg as { full_text?: string }).full_text ?? '';
|
|
247
254
|
if (fullText) {
|
|
248
|
-
svc
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
svc
|
|
256
|
+
.appendMessage(teamName, {
|
|
257
|
+
from: teamName,
|
|
258
|
+
to: 'user',
|
|
259
|
+
role: 'agent',
|
|
260
|
+
content: fullText,
|
|
261
|
+
meta: { sessionKey },
|
|
262
|
+
})
|
|
263
|
+
.catch(() => {});
|
|
255
264
|
}
|
|
256
265
|
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
257
266
|
} else {
|
|
@@ -306,7 +315,11 @@ await app.register(cors, {
|
|
|
306
315
|
// /api/v1/* → cc-connect /api/v1/* (兼容旧 renderer 直接打 /api/v1 的代码)
|
|
307
316
|
// ===========================================================================
|
|
308
317
|
|
|
309
|
-
async function proxyToCcConnect(
|
|
318
|
+
async function proxyToCcConnect(
|
|
319
|
+
request: import('fastify').FastifyRequest,
|
|
320
|
+
reply: import('fastify').FastifyReply,
|
|
321
|
+
stripPrefix: string
|
|
322
|
+
) {
|
|
310
323
|
const baseUrl = runtimeConfig.ccBaseUrl.replace(/\/+$/, '');
|
|
311
324
|
const token = runtimeConfig.ccToken;
|
|
312
325
|
|
|
@@ -338,7 +351,10 @@ async function proxyToCcConnect(request: import('fastify').FastifyRequest, reply
|
|
|
338
351
|
const body = Buffer.from(await upstream.arrayBuffer());
|
|
339
352
|
return reply
|
|
340
353
|
.code(upstream.status)
|
|
341
|
-
.header(
|
|
354
|
+
.header(
|
|
355
|
+
'Content-Type',
|
|
356
|
+
upstream.headers.get('content-type') ?? 'application/json; charset=utf-8'
|
|
357
|
+
)
|
|
342
358
|
.send(body);
|
|
343
359
|
}
|
|
344
360
|
|
|
@@ -500,13 +516,22 @@ function writeCcConnectConfig(updates: Record<string, unknown>): void {
|
|
|
500
516
|
// Update [management] section
|
|
501
517
|
if (updates.management_enabled !== undefined) {
|
|
502
518
|
const val = updates.management_enabled ? 'true' : 'false';
|
|
503
|
-
raw = raw.replace(
|
|
519
|
+
raw = raw.replace(
|
|
520
|
+
/(\[management\][^\n]*\n(?:[^\[]*)?)(enabled\s*=\s*)(true|false)/s,
|
|
521
|
+
(match, prefix, key) => `${prefix}${key}${val}`
|
|
522
|
+
);
|
|
504
523
|
}
|
|
505
524
|
if (updates.management_port !== undefined) {
|
|
506
|
-
raw = raw.replace(
|
|
525
|
+
raw = raw.replace(
|
|
526
|
+
/(\[management\][^\n]*\n(?:[^\[]*)?)(port\s*=\s*)\d+/s,
|
|
527
|
+
`$1$2${updates.management_port}`
|
|
528
|
+
);
|
|
507
529
|
}
|
|
508
530
|
if (updates.management_token !== undefined) {
|
|
509
|
-
raw = raw.replace(
|
|
531
|
+
raw = raw.replace(
|
|
532
|
+
/(\[management\][^\n]*\n(?:[^\[]*)?)(token\s*=\s*)"[^"]*"/s,
|
|
533
|
+
`$1$2"${updates.management_token}"`
|
|
534
|
+
);
|
|
510
535
|
}
|
|
511
536
|
|
|
512
537
|
// Update [bridge] section
|
|
@@ -515,25 +540,40 @@ function writeCcConnectConfig(updates: Record<string, unknown>): void {
|
|
|
515
540
|
raw = raw.replace(/(\[bridge\][^\n]*\n(?:[^\[]*)?)(enabled\s*=\s*)(true|false)/s, `$1$2${val}`);
|
|
516
541
|
}
|
|
517
542
|
if (updates.bridge_port !== undefined) {
|
|
518
|
-
raw = raw.replace(
|
|
543
|
+
raw = raw.replace(
|
|
544
|
+
/(\[bridge\][^\n]*\n(?:[^\[]*)?)(port\s*=\s*)\d+/s,
|
|
545
|
+
`$1$2${updates.bridge_port}`
|
|
546
|
+
);
|
|
519
547
|
}
|
|
520
548
|
if (updates.bridge_token !== undefined) {
|
|
521
|
-
raw = raw.replace(
|
|
549
|
+
raw = raw.replace(
|
|
550
|
+
/(\[bridge\][^\n]*\n(?:[^\[]*)?)(token\s*=\s*)"[^"]*"/s,
|
|
551
|
+
`$1$2"${updates.bridge_token}"`
|
|
552
|
+
);
|
|
522
553
|
}
|
|
523
554
|
|
|
524
555
|
// Update [log] section
|
|
525
556
|
if (updates.log_level !== undefined) {
|
|
526
|
-
raw = raw.replace(
|
|
557
|
+
raw = raw.replace(
|
|
558
|
+
/(\[log\][^\n]*\n(?:[^\[]*)?)(level\s*=\s*)"[^"]*"/s,
|
|
559
|
+
`$1$2"${updates.log_level}"`
|
|
560
|
+
);
|
|
527
561
|
}
|
|
528
562
|
|
|
529
563
|
// Update [display] section
|
|
530
564
|
if (updates.display_thinking !== undefined) {
|
|
531
565
|
const val = updates.display_thinking ? 'true' : 'false';
|
|
532
|
-
raw = raw.replace(
|
|
566
|
+
raw = raw.replace(
|
|
567
|
+
/(\[display\][^\n]*\n(?:[^\[]*)?)(thinking_messages\s*=\s*)(true|false)/s,
|
|
568
|
+
`$1$2${val}`
|
|
569
|
+
);
|
|
533
570
|
}
|
|
534
571
|
if (updates.display_tool !== undefined) {
|
|
535
572
|
const val = updates.display_tool ? 'true' : 'false';
|
|
536
|
-
raw = raw.replace(
|
|
573
|
+
raw = raw.replace(
|
|
574
|
+
/(\[display\][^\n]*\n(?:[^\[]*)?)(tool_messages\s*=\s*)(true|false)/s,
|
|
575
|
+
`$1$2${val}`
|
|
576
|
+
);
|
|
537
577
|
}
|
|
538
578
|
|
|
539
579
|
writeFileSync(configFile, raw, 'utf-8');
|
|
@@ -559,7 +599,11 @@ app.post<{ Body: Record<string, unknown> }>('/api/cc-config', async (request, re
|
|
|
559
599
|
writeCcConnectConfig(updates);
|
|
560
600
|
|
|
561
601
|
// If management port/token changed, notify user to restart cc-connect
|
|
562
|
-
const needsRestart =
|
|
602
|
+
const needsRestart =
|
|
603
|
+
'management_port' in updates ||
|
|
604
|
+
'management_token' in updates ||
|
|
605
|
+
'bridge_port' in updates ||
|
|
606
|
+
'bridge_token' in updates;
|
|
563
607
|
|
|
564
608
|
return {
|
|
565
609
|
ok: true,
|
|
@@ -654,62 +698,68 @@ app.post('/api/cc-reload', async () => {
|
|
|
654
698
|
app.get('/api/teams', async () => {
|
|
655
699
|
try {
|
|
656
700
|
const projects = await cc.listProjects();
|
|
657
|
-
return await Promise.all(
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const meta = await svc.readTeamManifest(p.name);
|
|
670
|
-
if (meta.displayName) displayName = meta.displayName;
|
|
671
|
-
if (meta.color) color = meta.color;
|
|
672
|
-
if (meta.description) description = meta.description;
|
|
673
|
-
pendingDelete = meta.pendingDelete === true;
|
|
674
|
-
restartRequired = meta.restartRequired === true;
|
|
675
|
-
if (typeof meta.workDir === 'string') {
|
|
676
|
-
workDir = meta.workDir.trim();
|
|
677
|
-
}
|
|
678
|
-
} catch { /* no local manifest, use defaults */ }
|
|
679
|
-
|
|
680
|
-
// 兼容仅存在于 cc-connect 的团队:回退读取 project 详情拿 work_dir。
|
|
681
|
-
if (!workDir) {
|
|
701
|
+
return await Promise.all(
|
|
702
|
+
projects.map(async (p) => {
|
|
703
|
+
// platforms 从 listProjects 返回的是 string[],有 platform 即认为在线
|
|
704
|
+
const isOnline = Array.isArray(p.platforms) && p.platforms.length > 0;
|
|
705
|
+
|
|
706
|
+
// 尝试从本地元数据读取 displayName 等信息
|
|
707
|
+
let displayName = p.name;
|
|
708
|
+
let color = 'blue';
|
|
709
|
+
let description = `${p.agent_type} · ${p.platforms?.join(', ') ?? ''}`;
|
|
710
|
+
let workDir = '';
|
|
711
|
+
let pendingDelete = false;
|
|
712
|
+
let restartRequired = false;
|
|
682
713
|
try {
|
|
683
|
-
const
|
|
684
|
-
if (
|
|
685
|
-
|
|
714
|
+
const meta = await svc.readTeamManifest(p.name);
|
|
715
|
+
if (meta.displayName) displayName = meta.displayName;
|
|
716
|
+
if (meta.color) color = meta.color;
|
|
717
|
+
if (meta.description) description = meta.description;
|
|
718
|
+
pendingDelete = meta.pendingDelete === true;
|
|
719
|
+
restartRequired = meta.restartRequired === true;
|
|
720
|
+
if (typeof meta.workDir === 'string') {
|
|
721
|
+
workDir = meta.workDir.trim();
|
|
686
722
|
}
|
|
687
723
|
} catch {
|
|
688
|
-
|
|
724
|
+
/* no local manifest, use defaults */
|
|
689
725
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
726
|
+
|
|
727
|
+
// 兼容仅存在于 cc-connect 的团队:回退读取 project 详情拿 work_dir。
|
|
728
|
+
if (!workDir) {
|
|
729
|
+
try {
|
|
730
|
+
const detail = await cc.getProject(p.name);
|
|
731
|
+
if (typeof detail.work_dir === 'string') {
|
|
732
|
+
workDir = detail.work_dir.trim();
|
|
733
|
+
}
|
|
734
|
+
} catch {
|
|
735
|
+
// ignore detail read failure, keep empty path
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
teamName: p.name,
|
|
741
|
+
displayName,
|
|
742
|
+
description,
|
|
743
|
+
color,
|
|
744
|
+
memberCount: 1,
|
|
745
|
+
members: [{ name: p.name, role: 'agent', agentId: p.agent_type, color }],
|
|
746
|
+
taskCount: 0,
|
|
747
|
+
lastActivity: null,
|
|
748
|
+
isAlive: isOnline,
|
|
749
|
+
harness: p.agent_type,
|
|
750
|
+
bindProject: p.name,
|
|
751
|
+
workDir,
|
|
752
|
+
projectPath: workDir || undefined,
|
|
753
|
+
sessionsCount: p.sessions_count,
|
|
754
|
+
heartbeatEnabled: p.heartbeat_enabled,
|
|
755
|
+
pendingDelete,
|
|
756
|
+
restartRequired,
|
|
757
|
+
};
|
|
758
|
+
})
|
|
759
|
+
);
|
|
760
|
+
} catch {
|
|
761
|
+
return [];
|
|
762
|
+
}
|
|
713
763
|
});
|
|
714
764
|
|
|
715
765
|
// POST /api/teams/create → 直接在 cc-connect 创建 project
|
|
@@ -757,7 +807,7 @@ app.post('/api/teams/create', async (request, reply) => {
|
|
|
757
807
|
// GET /api/teams/:name/data → TeamViewSnapshot (cc-connect project 为主,本地 tasks 为辅)
|
|
758
808
|
app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, reply) => {
|
|
759
809
|
const { name } = request.params;
|
|
760
|
-
|
|
810
|
+
|
|
761
811
|
// 本地元数据(始终尝试读取)
|
|
762
812
|
let displayName = name; // 默认使用 team ID
|
|
763
813
|
let color = 'blue';
|
|
@@ -799,7 +849,9 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
799
849
|
if (meta.platformAllowFrom) {
|
|
800
850
|
platformAllowFrom = normalizePlatformAllowFrom(meta.platformAllowFrom);
|
|
801
851
|
}
|
|
802
|
-
} catch {
|
|
852
|
+
} catch {
|
|
853
|
+
/* no local manifest */
|
|
854
|
+
}
|
|
803
855
|
|
|
804
856
|
// 本地任务
|
|
805
857
|
const rawTasks = activeTasks(await svc.readTasks(name).catch(() => []));
|
|
@@ -865,15 +917,17 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
865
917
|
members: [{ name: displayName, role: 'lead' }],
|
|
866
918
|
},
|
|
867
919
|
tasks: teamTasks,
|
|
868
|
-
members: [
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
920
|
+
members: [
|
|
921
|
+
{
|
|
922
|
+
name: displayName,
|
|
923
|
+
agentId: p.agent_type,
|
|
924
|
+
agentType: p.agent_type,
|
|
925
|
+
role: 'lead',
|
|
926
|
+
color,
|
|
927
|
+
currentTaskId: null,
|
|
928
|
+
taskCount: teamTasks.length,
|
|
929
|
+
},
|
|
930
|
+
],
|
|
877
931
|
kanbanState: { teamName: name, reviewers: [], tasks: {} },
|
|
878
932
|
processes: [],
|
|
879
933
|
isAlive: isOnline,
|
|
@@ -917,15 +971,17 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
917
971
|
members: [{ name: displayName, role: 'lead' }],
|
|
918
972
|
},
|
|
919
973
|
tasks: teamTasks,
|
|
920
|
-
members: [
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
974
|
+
members: [
|
|
975
|
+
{
|
|
976
|
+
name: displayName,
|
|
977
|
+
agentId: harness,
|
|
978
|
+
agentType: harness,
|
|
979
|
+
role: 'lead',
|
|
980
|
+
color,
|
|
981
|
+
currentTaskId: null,
|
|
982
|
+
taskCount: teamTasks.length,
|
|
983
|
+
},
|
|
984
|
+
],
|
|
929
985
|
kanbanState: { teamName: name, reviewers: [], tasks: {} },
|
|
930
986
|
processes: [],
|
|
931
987
|
isAlive: false,
|
|
@@ -951,20 +1007,22 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, r
|
|
|
951
1007
|
});
|
|
952
1008
|
|
|
953
1009
|
// PATCH /api/teams/:name — 更新团队元数据
|
|
954
|
-
app.patch<{
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1010
|
+
app.patch<{
|
|
1011
|
+
Params: { name: string };
|
|
1012
|
+
Body: { displayName?: string; color?: string; description?: string };
|
|
1013
|
+
}>('/api/teams/:name', async (request, reply) => {
|
|
1014
|
+
try {
|
|
1015
|
+
const updated = await svc.updateTeam(request.params.name, request.body ?? {});
|
|
1016
|
+
return { ok: true, data: updated };
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
return reply.code(404).send(reply500(err));
|
|
962
1019
|
}
|
|
963
|
-
);
|
|
1020
|
+
});
|
|
964
1021
|
|
|
965
1022
|
// DELETE /api/teams/:name
|
|
966
1023
|
app.delete<{ Params: { name: string }; Querystring: { deleteFiles?: string } }>(
|
|
967
|
-
'/api/teams/:name',
|
|
1024
|
+
'/api/teams/:name',
|
|
1025
|
+
async (request, reply) => {
|
|
968
1026
|
const teamName = request.params.name;
|
|
969
1027
|
try {
|
|
970
1028
|
try {
|
|
@@ -973,7 +1031,7 @@ app.delete<{ Params: { name: string }; Querystring: { deleteFiles?: string } }>(
|
|
|
973
1031
|
pendingDelete: true,
|
|
974
1032
|
restartRequired: result.restart_required === true,
|
|
975
1033
|
});
|
|
976
|
-
|
|
1034
|
+
} catch (err) {
|
|
977
1035
|
request.log.warn({ err, teamName }, 'delete cc-connect project failed or project missing');
|
|
978
1036
|
}
|
|
979
1037
|
if (request.query.deleteFiles === 'true') {
|
|
@@ -1000,8 +1058,24 @@ function toTaskStatus(s: string): 'todo' | 'doing' | 'done' {
|
|
|
1000
1058
|
}
|
|
1001
1059
|
|
|
1002
1060
|
/** internal Task → TeamTask shape (for UI consumption) */
|
|
1003
|
-
function toTeamTask(task: {
|
|
1004
|
-
|
|
1061
|
+
function toTeamTask(task: {
|
|
1062
|
+
id: string;
|
|
1063
|
+
title?: string;
|
|
1064
|
+
subject?: string;
|
|
1065
|
+
description?: string;
|
|
1066
|
+
status: string;
|
|
1067
|
+
assignee?: string | null;
|
|
1068
|
+
result?: string | null;
|
|
1069
|
+
createdAt: string;
|
|
1070
|
+
updatedAt: string;
|
|
1071
|
+
order: number;
|
|
1072
|
+
teamSlug: string;
|
|
1073
|
+
}) {
|
|
1074
|
+
const statusMap: Record<string, string> = {
|
|
1075
|
+
todo: 'pending',
|
|
1076
|
+
doing: 'in_progress',
|
|
1077
|
+
done: 'completed',
|
|
1078
|
+
};
|
|
1005
1079
|
return {
|
|
1006
1080
|
id: task.id,
|
|
1007
1081
|
displayId: task.id.slice(0, 8),
|
|
@@ -1057,11 +1131,14 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/tasks', async (request)
|
|
|
1057
1131
|
try {
|
|
1058
1132
|
const tasks = activeTasks(await svc.readTasks(request.params.name));
|
|
1059
1133
|
return tasks.map(toTeamTask);
|
|
1060
|
-
} catch {
|
|
1134
|
+
} catch {
|
|
1135
|
+
return [];
|
|
1136
|
+
}
|
|
1061
1137
|
});
|
|
1062
1138
|
|
|
1063
1139
|
app.post<{ Params: { name: string }; Body: Record<string, unknown> }>(
|
|
1064
|
-
'/api/teams/:name/tasks',
|
|
1140
|
+
'/api/teams/:name/tasks',
|
|
1141
|
+
async (request, reply) => {
|
|
1065
1142
|
const body = request.body ?? {};
|
|
1066
1143
|
// 支持 subject(TeamTask)或 title(内部)
|
|
1067
1144
|
const title = (body.subject ?? body.title) as string | undefined;
|
|
@@ -1073,16 +1150,17 @@ app.post<{ Params: { name: string }; Body: Record<string, unknown> }>(
|
|
|
1073
1150
|
status: body.status ? toTaskStatus(body.status as string) : 'todo',
|
|
1074
1151
|
});
|
|
1075
1152
|
if (task.assignee) {
|
|
1076
|
-
svc
|
|
1077
|
-
request.
|
|
1078
|
-
|
|
1153
|
+
svc
|
|
1154
|
+
.dispatchTask(request.params.name, task)
|
|
1155
|
+
.catch((err) => request.log.warn({ err }, 'dispatchTask failed'));
|
|
1079
1156
|
}
|
|
1080
1157
|
return toTeamTask(task);
|
|
1081
1158
|
}
|
|
1082
1159
|
);
|
|
1083
1160
|
|
|
1084
1161
|
app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
|
|
1085
|
-
'/api/teams/:name/tasks/:id',
|
|
1162
|
+
'/api/teams/:name/tasks/:id',
|
|
1163
|
+
async (request) => {
|
|
1086
1164
|
const body = request.body ?? {};
|
|
1087
1165
|
const patch: Record<string, unknown> = {};
|
|
1088
1166
|
if (body.subject !== undefined) patch.title = body.subject;
|
|
@@ -1094,22 +1172,23 @@ app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown>
|
|
|
1094
1172
|
if (body.result !== undefined) patch.result = body.result;
|
|
1095
1173
|
const task = await svc.patchTask(request.params.name, request.params.id, patch);
|
|
1096
1174
|
if (patch.assignee && task.assignee) {
|
|
1097
|
-
svc
|
|
1098
|
-
request.
|
|
1099
|
-
|
|
1175
|
+
svc
|
|
1176
|
+
.dispatchTask(request.params.name, task)
|
|
1177
|
+
.catch((err) => request.log.warn({ err }, 'dispatchTask failed'));
|
|
1100
1178
|
}
|
|
1101
1179
|
return toTeamTask(task);
|
|
1102
1180
|
}
|
|
1103
1181
|
);
|
|
1104
1182
|
|
|
1105
1183
|
app.delete<{ Params: { name: string; id: string } }>(
|
|
1106
|
-
'/api/teams/:name/tasks/:id',
|
|
1184
|
+
'/api/teams/:name/tasks/:id',
|
|
1185
|
+
async (request, reply) => {
|
|
1107
1186
|
try {
|
|
1108
1187
|
await svc.patchTask(request.params.name, request.params.id, {
|
|
1109
1188
|
status: 'done',
|
|
1110
1189
|
result: '__deleted__',
|
|
1111
1190
|
});
|
|
1112
|
-
|
|
1191
|
+
return { ok: true };
|
|
1113
1192
|
} catch {
|
|
1114
1193
|
return reply.code(404).send({ error: 'not found' });
|
|
1115
1194
|
}
|
|
@@ -1121,7 +1200,8 @@ app.delete<{ Params: { name: string; id: string } }>(
|
|
|
1121
1200
|
// ===========================================================================
|
|
1122
1201
|
|
|
1123
1202
|
app.patch<{ Params: { name: string }; Body: { collaboration: boolean } }>(
|
|
1124
|
-
'/api/teams/:name/collaboration',
|
|
1203
|
+
'/api/teams/:name/collaboration',
|
|
1204
|
+
async (request, reply) => {
|
|
1125
1205
|
const { collaboration } = request.body ?? {};
|
|
1126
1206
|
if (typeof collaboration !== 'boolean') {
|
|
1127
1207
|
return reply.code(400).send({ error: 'collaboration must be boolean' });
|
|
@@ -1149,55 +1229,90 @@ app.get<{ Params: { name: string } }>('/api/teams/:name/heartbeat', async (reque
|
|
|
1149
1229
|
try {
|
|
1150
1230
|
const data = await cc.getHeartbeat(request.params.name);
|
|
1151
1231
|
return { ok: true, data };
|
|
1152
|
-
} catch (err) {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
app.post<{ Params: { name: string } }>('/api/teams/:name/heartbeat/enable', async (request, reply) => {
|
|
1156
|
-
try {
|
|
1157
|
-
await cc.resumeHeartbeat(request.params.name);
|
|
1158
|
-
return { ok: true };
|
|
1159
|
-
} catch (err) { return reply.code(500).send(reply500(err)); }
|
|
1232
|
+
} catch (err) {
|
|
1233
|
+
return reply.code(404).send(reply500(err));
|
|
1234
|
+
}
|
|
1160
1235
|
});
|
|
1161
1236
|
|
|
1162
|
-
app.post<{ Params: { name: string } }>(
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
}
|
|
1237
|
+
app.post<{ Params: { name: string } }>(
|
|
1238
|
+
'/api/teams/:name/heartbeat/enable',
|
|
1239
|
+
async (request, reply) => {
|
|
1240
|
+
try {
|
|
1241
|
+
await cc.resumeHeartbeat(request.params.name);
|
|
1242
|
+
return { ok: true };
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
return reply.code(500).send(reply500(err));
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
);
|
|
1168
1248
|
|
|
1169
|
-
app.post<{ Params: { name: string } }>(
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
}
|
|
1249
|
+
app.post<{ Params: { name: string } }>(
|
|
1250
|
+
'/api/teams/:name/heartbeat/disable',
|
|
1251
|
+
async (request, reply) => {
|
|
1252
|
+
try {
|
|
1253
|
+
await cc.pauseHeartbeat(request.params.name);
|
|
1254
|
+
return { ok: true };
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
return reply.code(500).send(reply500(err));
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
);
|
|
1175
1260
|
|
|
1176
|
-
app.post<{ Params: { name: string } }>(
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1261
|
+
app.post<{ Params: { name: string } }>(
|
|
1262
|
+
'/api/teams/:name/heartbeat/pause',
|
|
1263
|
+
async (request, reply) => {
|
|
1264
|
+
try {
|
|
1265
|
+
await cc.pauseHeartbeat(request.params.name);
|
|
1266
|
+
return { ok: true };
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
return reply.code(500).send(reply500(err));
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
);
|
|
1182
1272
|
|
|
1183
|
-
app.
|
|
1184
|
-
'/api/teams/:name/heartbeat',
|
|
1273
|
+
app.post<{ Params: { name: string } }>(
|
|
1274
|
+
'/api/teams/:name/heartbeat/resume',
|
|
1275
|
+
async (request, reply) => {
|
|
1185
1276
|
try {
|
|
1186
|
-
await cc.
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1277
|
+
await cc.resumeHeartbeat(request.params.name);
|
|
1278
|
+
return { ok: true };
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
return reply.code(500).send(reply500(err));
|
|
1281
|
+
}
|
|
1190
1282
|
}
|
|
1191
1283
|
);
|
|
1192
1284
|
|
|
1285
|
+
app.patch<{
|
|
1286
|
+
Params: { name: string };
|
|
1287
|
+
Body: { interval_mins?: number; only_when_idle?: boolean; silent?: boolean };
|
|
1288
|
+
}>('/api/teams/:name/heartbeat', async (request, reply) => {
|
|
1289
|
+
try {
|
|
1290
|
+
await cc.updateProject(request.params.name, request.body as Record<string, unknown>);
|
|
1291
|
+
const data = await cc.getHeartbeat(request.params.name);
|
|
1292
|
+
return { ok: true, data };
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
return reply.code(500).send(reply500(err));
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1193
1298
|
// ===========================================================================
|
|
1194
1299
|
// Harness 列表 — 从 cc-connect projects 提取已用 agent_type,合并固定枚举
|
|
1195
1300
|
// GET /api/harnesses
|
|
1196
1301
|
// ===========================================================================
|
|
1197
1302
|
|
|
1198
1303
|
const CC_AGENT_TYPES = [
|
|
1199
|
-
'claudecode',
|
|
1200
|
-
'
|
|
1304
|
+
'claudecode',
|
|
1305
|
+
'codex',
|
|
1306
|
+
'cursor',
|
|
1307
|
+
'gemini',
|
|
1308
|
+
'iflow',
|
|
1309
|
+
'kimi',
|
|
1310
|
+
'devin',
|
|
1311
|
+
'opencode',
|
|
1312
|
+
'qoder',
|
|
1313
|
+
'pi',
|
|
1314
|
+
'acp',
|
|
1315
|
+
'tmux',
|
|
1201
1316
|
] as const;
|
|
1202
1317
|
|
|
1203
1318
|
app.get('/api/harnesses', async () => {
|
|
@@ -1229,7 +1344,9 @@ app.post<{ Params: { name: string } }>('/api/teams/:name/launch', async (request
|
|
|
1229
1344
|
const p = await cc.getProject(name);
|
|
1230
1345
|
projectExists = true;
|
|
1231
1346
|
isOnline = Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
|
|
1232
|
-
} catch {
|
|
1347
|
+
} catch {
|
|
1348
|
+
/* project 不存在 */
|
|
1349
|
+
}
|
|
1233
1350
|
|
|
1234
1351
|
return {
|
|
1235
1352
|
ok: true,
|
|
@@ -1239,11 +1356,15 @@ app.post<{ Params: { name: string } }>('/api/teams/:name/launch', async (request
|
|
|
1239
1356
|
projectExists,
|
|
1240
1357
|
isOnline,
|
|
1241
1358
|
message: projectExists
|
|
1242
|
-
? isOnline
|
|
1359
|
+
? isOnline
|
|
1360
|
+
? '团队在线'
|
|
1361
|
+
: '团队 project 存在但无活跃连接'
|
|
1243
1362
|
: `cc-connect 中不存在 project "${name}"`,
|
|
1244
1363
|
},
|
|
1245
1364
|
};
|
|
1246
|
-
} catch (err) {
|
|
1365
|
+
} catch (err) {
|
|
1366
|
+
return reply.code(404).send(reply500(err));
|
|
1367
|
+
}
|
|
1247
1368
|
});
|
|
1248
1369
|
|
|
1249
1370
|
app.post<{ Params: { name: string } }>('/api/teams/:name/stop', async (request) => {
|
|
@@ -1263,104 +1384,131 @@ app.post<{ Params: { name: string } }>('/api/teams/:name/stop', async (request)
|
|
|
1263
1384
|
// Feishu/Lark setup
|
|
1264
1385
|
app.post('/api/setup/feishu/begin', async (request, reply) => {
|
|
1265
1386
|
try {
|
|
1266
|
-
const result = await (
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1387
|
+
const result = await (
|
|
1388
|
+
await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/begin`, {
|
|
1389
|
+
method: 'POST',
|
|
1390
|
+
headers: {
|
|
1391
|
+
'Content-Type': 'application/json',
|
|
1392
|
+
...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
|
|
1393
|
+
},
|
|
1394
|
+
body: JSON.stringify(request.body ?? {}),
|
|
1395
|
+
})
|
|
1396
|
+
).json();
|
|
1274
1397
|
return result;
|
|
1275
|
-
} catch (err) {
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
return reply500(err);
|
|
1400
|
+
}
|
|
1276
1401
|
});
|
|
1277
1402
|
|
|
1278
1403
|
app.post('/api/setup/feishu/poll', async (request, reply) => {
|
|
1279
1404
|
try {
|
|
1280
|
-
const result = await (
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1405
|
+
const result = await (
|
|
1406
|
+
await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/poll`, {
|
|
1407
|
+
method: 'POST',
|
|
1408
|
+
headers: {
|
|
1409
|
+
'Content-Type': 'application/json',
|
|
1410
|
+
...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
|
|
1411
|
+
},
|
|
1412
|
+
body: JSON.stringify(request.body ?? {}),
|
|
1413
|
+
})
|
|
1414
|
+
).json();
|
|
1288
1415
|
return result;
|
|
1289
|
-
} catch (err) {
|
|
1416
|
+
} catch (err) {
|
|
1417
|
+
return reply500(err);
|
|
1418
|
+
}
|
|
1290
1419
|
});
|
|
1291
1420
|
|
|
1292
1421
|
app.post('/api/setup/feishu/save', async (request, reply) => {
|
|
1293
1422
|
try {
|
|
1294
|
-
const result = await (
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1423
|
+
const result = await (
|
|
1424
|
+
await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/save`, {
|
|
1425
|
+
method: 'POST',
|
|
1426
|
+
headers: {
|
|
1427
|
+
'Content-Type': 'application/json',
|
|
1428
|
+
...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
|
|
1429
|
+
},
|
|
1430
|
+
body: JSON.stringify(request.body ?? {}),
|
|
1431
|
+
})
|
|
1432
|
+
).json();
|
|
1302
1433
|
return result;
|
|
1303
|
-
} catch (err) {
|
|
1434
|
+
} catch (err) {
|
|
1435
|
+
return reply500(err);
|
|
1436
|
+
}
|
|
1304
1437
|
});
|
|
1305
1438
|
|
|
1306
1439
|
// Weixin setup
|
|
1307
1440
|
app.post('/api/setup/weixin/begin', async (request, reply) => {
|
|
1308
1441
|
try {
|
|
1309
|
-
const result = await (
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1442
|
+
const result = await (
|
|
1443
|
+
await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/begin`, {
|
|
1444
|
+
method: 'POST',
|
|
1445
|
+
headers: {
|
|
1446
|
+
'Content-Type': 'application/json',
|
|
1447
|
+
...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
|
|
1448
|
+
},
|
|
1449
|
+
body: JSON.stringify(request.body ?? {}),
|
|
1450
|
+
})
|
|
1451
|
+
).json();
|
|
1317
1452
|
return result;
|
|
1318
|
-
} catch (err) {
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
return reply500(err);
|
|
1455
|
+
}
|
|
1319
1456
|
});
|
|
1320
1457
|
|
|
1321
1458
|
app.post('/api/setup/weixin/poll', async (request, reply) => {
|
|
1322
1459
|
try {
|
|
1323
|
-
const result = await (
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1460
|
+
const result = await (
|
|
1461
|
+
await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/poll`, {
|
|
1462
|
+
method: 'POST',
|
|
1463
|
+
headers: {
|
|
1464
|
+
'Content-Type': 'application/json',
|
|
1465
|
+
...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
|
|
1466
|
+
},
|
|
1467
|
+
body: JSON.stringify(request.body ?? {}),
|
|
1468
|
+
})
|
|
1469
|
+
).json();
|
|
1331
1470
|
return result;
|
|
1332
|
-
} catch (err) {
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
return reply500(err);
|
|
1473
|
+
}
|
|
1333
1474
|
});
|
|
1334
1475
|
|
|
1335
1476
|
app.post('/api/setup/weixin/save', async (request, reply) => {
|
|
1336
1477
|
try {
|
|
1337
|
-
const result = await (
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1478
|
+
const result = await (
|
|
1479
|
+
await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/save`, {
|
|
1480
|
+
method: 'POST',
|
|
1481
|
+
headers: {
|
|
1482
|
+
'Content-Type': 'application/json',
|
|
1483
|
+
...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
|
|
1484
|
+
},
|
|
1485
|
+
body: JSON.stringify(request.body ?? {}),
|
|
1486
|
+
})
|
|
1487
|
+
).json();
|
|
1345
1488
|
return result;
|
|
1346
|
-
} catch (err) {
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
return reply500(err);
|
|
1491
|
+
}
|
|
1347
1492
|
});
|
|
1348
1493
|
|
|
1349
1494
|
// Generic platform add (manual credential form)
|
|
1350
|
-
app.post<{
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1495
|
+
app.post<{
|
|
1496
|
+
Params: { name: string };
|
|
1497
|
+
Body: { type: string; options?: Record<string, unknown>; work_dir?: string; agent_type?: string };
|
|
1498
|
+
}>('/api/projects/:name/add-platform', async (request, reply) => {
|
|
1499
|
+
try {
|
|
1500
|
+
const result = await cc.createProject(
|
|
1501
|
+
request.params.name,
|
|
1502
|
+
request.body.agent_type ?? 'claudecode',
|
|
1503
|
+
request.body.work_dir ?? '',
|
|
1504
|
+
request.body.type,
|
|
1505
|
+
(request.body.options ?? {}) as Record<string, string>
|
|
1506
|
+
);
|
|
1507
|
+
return result;
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
return reply500(err);
|
|
1362
1510
|
}
|
|
1363
|
-
);
|
|
1511
|
+
});
|
|
1364
1512
|
|
|
1365
1513
|
// ===========================================================================
|
|
1366
1514
|
// 组织图 API — GET /api/graph
|
|
@@ -1393,11 +1541,15 @@ app.get('/api/graph', async () => {
|
|
|
1393
1541
|
});
|
|
1394
1542
|
}
|
|
1395
1543
|
}
|
|
1396
|
-
} catch {
|
|
1544
|
+
} catch {
|
|
1545
|
+
/* skip */
|
|
1546
|
+
}
|
|
1397
1547
|
}
|
|
1398
1548
|
|
|
1399
1549
|
return { ok: true, data: { nodes, edges } };
|
|
1400
|
-
} catch (err) {
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
return reply500(err);
|
|
1552
|
+
}
|
|
1401
1553
|
});
|
|
1402
1554
|
|
|
1403
1555
|
// ===========================================================================
|
|
@@ -1497,7 +1649,9 @@ async function executeMcpTool(
|
|
|
1497
1649
|
assignee: args.assignee ?? null,
|
|
1498
1650
|
});
|
|
1499
1651
|
if (task.assignee) {
|
|
1500
|
-
svc.dispatchTask(args.team_slug, task).catch(() => {
|
|
1652
|
+
svc.dispatchTask(args.team_slug, task).catch(() => {
|
|
1653
|
+
/* best-effort */
|
|
1654
|
+
});
|
|
1501
1655
|
}
|
|
1502
1656
|
return text(task);
|
|
1503
1657
|
}
|
|
@@ -1516,12 +1670,14 @@ app.get('/mcp', (request, reply) => {
|
|
|
1516
1670
|
|
|
1517
1671
|
// MCP initialize 握手
|
|
1518
1672
|
const endpoint = `http://${request.hostname}/mcp`;
|
|
1519
|
-
reply.raw.write(
|
|
1520
|
-
`event: endpoint\ndata: ${JSON.stringify({ endpoint })}\n\n`
|
|
1521
|
-
);
|
|
1673
|
+
reply.raw.write(`event: endpoint\ndata: ${JSON.stringify({ endpoint })}\n\n`);
|
|
1522
1674
|
|
|
1523
1675
|
const ka = setInterval(() => {
|
|
1524
|
-
try {
|
|
1676
|
+
try {
|
|
1677
|
+
reply.raw.write(': keep-alive\n\n');
|
|
1678
|
+
} catch {
|
|
1679
|
+
clearInterval(ka);
|
|
1680
|
+
}
|
|
1525
1681
|
}, 15000);
|
|
1526
1682
|
|
|
1527
1683
|
request.raw.on('close', () => clearInterval(ka));
|
|
@@ -1529,56 +1685,59 @@ app.get('/mcp', (request, reply) => {
|
|
|
1529
1685
|
});
|
|
1530
1686
|
|
|
1531
1687
|
// POST /mcp — JSON-RPC 请求处理
|
|
1532
|
-
app.post<{
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1688
|
+
app.post<{
|
|
1689
|
+
Body: { jsonrpc?: string; id?: unknown; method?: string; params?: Record<string, unknown> };
|
|
1690
|
+
}>('/mcp', async (request, reply) => {
|
|
1691
|
+
const { id, method, params = {} } = request.body ?? {};
|
|
1692
|
+
|
|
1693
|
+
// MCP initialize
|
|
1694
|
+
if (method === 'initialize') {
|
|
1695
|
+
return {
|
|
1696
|
+
jsonrpc: '2.0',
|
|
1697
|
+
id,
|
|
1698
|
+
result: {
|
|
1699
|
+
protocolVersion: '2024-11-05',
|
|
1700
|
+
capabilities: { tools: {} },
|
|
1701
|
+
serverInfo: { name: 'hermit-tasks', version: '1.0.0' },
|
|
1702
|
+
},
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1536
1705
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1706
|
+
// MCP tools/list
|
|
1707
|
+
if (method === 'tools/list') {
|
|
1708
|
+
return { jsonrpc: '2.0', id, result: { tools: MCP_TOOLS } };
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// MCP tools/call
|
|
1712
|
+
if (method === 'tools/call') {
|
|
1713
|
+
const toolName = params.name as string;
|
|
1714
|
+
const toolArgs = (params.arguments ?? {}) as Record<string, string>;
|
|
1715
|
+
try {
|
|
1716
|
+
const content = await executeMcpTool(toolName, toolArgs);
|
|
1717
|
+
return { jsonrpc: '2.0', id, result: { content } };
|
|
1718
|
+
} catch (err) {
|
|
1539
1719
|
return {
|
|
1540
1720
|
jsonrpc: '2.0',
|
|
1541
1721
|
id,
|
|
1542
1722
|
result: {
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1723
|
+
content: [
|
|
1724
|
+
{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` },
|
|
1725
|
+
],
|
|
1726
|
+
isError: true,
|
|
1546
1727
|
},
|
|
1547
1728
|
};
|
|
1548
1729
|
}
|
|
1730
|
+
}
|
|
1549
1731
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
// MCP tools/call
|
|
1556
|
-
if (method === 'tools/call') {
|
|
1557
|
-
const toolName = params.name as string;
|
|
1558
|
-
const toolArgs = (params.arguments ?? {}) as Record<string, string>;
|
|
1559
|
-
try {
|
|
1560
|
-
const content = await executeMcpTool(toolName, toolArgs);
|
|
1561
|
-
return { jsonrpc: '2.0', id, result: { content } };
|
|
1562
|
-
} catch (err) {
|
|
1563
|
-
return {
|
|
1564
|
-
jsonrpc: '2.0',
|
|
1565
|
-
id,
|
|
1566
|
-
result: {
|
|
1567
|
-
content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
1568
|
-
isError: true,
|
|
1569
|
-
},
|
|
1570
|
-
};
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
// notifications/initialized — 无需响应
|
|
1575
|
-
if (method === 'notifications/initialized') {
|
|
1576
|
-
return reply.code(204).send();
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
return reply.code(400).send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
|
|
1732
|
+
// notifications/initialized — 无需响应
|
|
1733
|
+
if (method === 'notifications/initialized') {
|
|
1734
|
+
return reply.code(204).send();
|
|
1580
1735
|
}
|
|
1581
|
-
|
|
1736
|
+
|
|
1737
|
+
return reply
|
|
1738
|
+
.code(400)
|
|
1739
|
+
.send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
|
|
1740
|
+
});
|
|
1582
1741
|
|
|
1583
1742
|
// ===========================================================================
|
|
1584
1743
|
// Hermit 主仓 UI 首屏强依赖的几个 stub(占位实现)
|
|
@@ -1646,7 +1805,7 @@ const DEFAULT_APP_CONFIG = {
|
|
|
1646
1805
|
enabled: true,
|
|
1647
1806
|
soundEnabled: true,
|
|
1648
1807
|
ignoredRegex: [] as string[],
|
|
1649
|
-
|
|
1808
|
+
ignoredRepositories: [] as string[],
|
|
1650
1809
|
snoozedUntil: null as number | null,
|
|
1651
1810
|
snoozeMinutes: 30,
|
|
1652
1811
|
includeSubagentErrors: false,
|
|
@@ -1758,7 +1917,7 @@ app.post<{ Body: { section?: unknown; data?: unknown } }>('/api/config/update',
|
|
|
1758
1917
|
})
|
|
1759
1918
|
: current;
|
|
1760
1919
|
return {
|
|
1761
|
-
|
|
1920
|
+
success: true,
|
|
1762
1921
|
data: writeAppConfig(next),
|
|
1763
1922
|
};
|
|
1764
1923
|
});
|
|
@@ -1776,7 +1935,15 @@ type InMemoryScheduleRun = {
|
|
|
1776
1935
|
id: string;
|
|
1777
1936
|
scheduleId: string;
|
|
1778
1937
|
teamName: string;
|
|
1779
|
-
status:
|
|
1938
|
+
status:
|
|
1939
|
+
| 'pending'
|
|
1940
|
+
| 'warming_up'
|
|
1941
|
+
| 'warm'
|
|
1942
|
+
| 'running'
|
|
1943
|
+
| 'completed'
|
|
1944
|
+
| 'failed'
|
|
1945
|
+
| 'failed_interrupted'
|
|
1946
|
+
| 'cancelled';
|
|
1780
1947
|
scheduledFor: string;
|
|
1781
1948
|
startedAt: string;
|
|
1782
1949
|
warmUpCompletedAt?: string;
|
|
@@ -1810,7 +1977,9 @@ function buildFallbackSessionKey(teamName: string): string {
|
|
|
1810
1977
|
return `hermit:${teamName}:session`;
|
|
1811
1978
|
}
|
|
1812
1979
|
|
|
1813
|
-
async function waitForHarnessBridgeConnected(
|
|
1980
|
+
async function waitForHarnessBridgeConnected(
|
|
1981
|
+
timeoutMs = HARNESS_BRIDGE_CONNECT_TIMEOUT_MS
|
|
1982
|
+
): Promise<void> {
|
|
1814
1983
|
if (bridge.connected) return;
|
|
1815
1984
|
bridge.start();
|
|
1816
1985
|
if (bridge.connected) return;
|
|
@@ -1859,30 +2028,32 @@ async function resolveTeamWorkDirs(teamNames: string[]): Promise<Map<string, str
|
|
|
1859
2028
|
const uniqueTeamNames = [...new Set(teamNames.filter((name) => name.trim().length > 0))];
|
|
1860
2029
|
const results = new Map<string, string>();
|
|
1861
2030
|
|
|
1862
|
-
await Promise.all(
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
const meta = await svc.readTeamManifest(teamName);
|
|
1866
|
-
if (typeof meta.workDir === 'string') {
|
|
1867
|
-
cwd = meta.workDir.trim();
|
|
1868
|
-
}
|
|
1869
|
-
} catch {
|
|
1870
|
-
// ignore
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
if (!cwd) {
|
|
2031
|
+
await Promise.all(
|
|
2032
|
+
uniqueTeamNames.map(async (teamName) => {
|
|
2033
|
+
let cwd = '';
|
|
1874
2034
|
try {
|
|
1875
|
-
const
|
|
1876
|
-
if (typeof
|
|
1877
|
-
cwd =
|
|
2035
|
+
const meta = await svc.readTeamManifest(teamName);
|
|
2036
|
+
if (typeof meta.workDir === 'string') {
|
|
2037
|
+
cwd = meta.workDir.trim();
|
|
1878
2038
|
}
|
|
1879
2039
|
} catch {
|
|
1880
2040
|
// ignore
|
|
1881
2041
|
}
|
|
1882
|
-
}
|
|
1883
2042
|
|
|
1884
|
-
|
|
1885
|
-
|
|
2043
|
+
if (!cwd) {
|
|
2044
|
+
try {
|
|
2045
|
+
const detail = await cc.getProject(teamName);
|
|
2046
|
+
if (typeof detail.work_dir === 'string') {
|
|
2047
|
+
cwd = detail.work_dir.trim();
|
|
2048
|
+
}
|
|
2049
|
+
} catch {
|
|
2050
|
+
// ignore
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
results.set(teamName, cwd);
|
|
2055
|
+
})
|
|
2056
|
+
);
|
|
1886
2057
|
|
|
1887
2058
|
return results;
|
|
1888
2059
|
}
|
|
@@ -2010,14 +2181,17 @@ app.post<{ Body: Record<string, unknown> }>('/api/schedules', async (request, re
|
|
|
2010
2181
|
: DEFAULT_SCHEDULE_MAX_TURNS;
|
|
2011
2182
|
|
|
2012
2183
|
const launchConfig =
|
|
2013
|
-
body.launchConfig &&
|
|
2184
|
+
body.launchConfig &&
|
|
2185
|
+
typeof body.launchConfig === 'object' &&
|
|
2186
|
+
!Array.isArray(body.launchConfig)
|
|
2014
2187
|
? (body.launchConfig as Record<string, unknown>)
|
|
2015
2188
|
: {};
|
|
2016
2189
|
const prompt = typeof launchConfig.prompt === 'string' ? launchConfig.prompt.trim() : '';
|
|
2017
2190
|
const cwd = typeof launchConfig.cwd === 'string' ? launchConfig.cwd.trim() : '';
|
|
2018
|
-
const sessionKey =
|
|
2019
|
-
|
|
2020
|
-
|
|
2191
|
+
const sessionKey =
|
|
2192
|
+
typeof launchConfig.session_key === 'string' && launchConfig.session_key.trim().length > 0
|
|
2193
|
+
? launchConfig.session_key.trim()
|
|
2194
|
+
: buildFallbackSessionKey(teamName);
|
|
2021
2195
|
|
|
2022
2196
|
if (!teamName || !cronExpression || !prompt) {
|
|
2023
2197
|
return reply
|
|
@@ -2111,12 +2285,16 @@ app.delete<{ Params: { id: string } }>('/api/schedules/:id', async (request, rep
|
|
|
2111
2285
|
jobs = await cc.listCronJobs();
|
|
2112
2286
|
listedJobs = true;
|
|
2113
2287
|
} catch (listErr) {
|
|
2114
|
-
request.log.warn(
|
|
2288
|
+
request.log.warn(
|
|
2289
|
+
{ err: listErr, scheduleId: requestedId },
|
|
2290
|
+
'list cron jobs before delete failed'
|
|
2291
|
+
);
|
|
2115
2292
|
}
|
|
2116
2293
|
const target = findCronJobByRouteId(jobs, requestedId);
|
|
2117
2294
|
if (target) {
|
|
2118
2295
|
resolvedId = target.id;
|
|
2119
|
-
resolvedTeamName =
|
|
2296
|
+
resolvedTeamName =
|
|
2297
|
+
'project' in target && typeof target.project === 'string' ? target.project : '';
|
|
2120
2298
|
} else if (
|
|
2121
2299
|
listedJobs &&
|
|
2122
2300
|
!jobs.some((job) => job.id === normalizedId || job.id.startsWith(normalizedId))
|
|
@@ -2216,7 +2394,11 @@ app.post<{ Params: { id: string } }>('/api/schedules/:id/trigger', async (reques
|
|
|
2216
2394
|
let run: InMemoryScheduleRun;
|
|
2217
2395
|
|
|
2218
2396
|
try {
|
|
2219
|
-
await cc.sendMessage(
|
|
2397
|
+
await cc.sendMessage(
|
|
2398
|
+
job.project,
|
|
2399
|
+
job.session_key || buildFallbackSessionKey(job.project),
|
|
2400
|
+
job.prompt
|
|
2401
|
+
);
|
|
2220
2402
|
run = {
|
|
2221
2403
|
id: runId,
|
|
2222
2404
|
scheduleId: job.id,
|
|
@@ -2280,20 +2462,22 @@ app.get<{ Params: { id: string } }>('/api/schedules/:id/runs', async (request) =
|
|
|
2280
2462
|
const job = jobs.find((item) => item.id === scheduleId);
|
|
2281
2463
|
const lastRunAt = normalizeCronLastRun(job?.last_run);
|
|
2282
2464
|
if (!job || !lastRunAt) return [];
|
|
2283
|
-
return [
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2465
|
+
return [
|
|
2466
|
+
{
|
|
2467
|
+
id: `last-${scheduleId}`,
|
|
2468
|
+
scheduleId,
|
|
2469
|
+
teamName: job.project,
|
|
2470
|
+
status: 'completed',
|
|
2471
|
+
scheduledFor: lastRunAt,
|
|
2472
|
+
startedAt: lastRunAt,
|
|
2473
|
+
executionStartedAt: lastRunAt,
|
|
2474
|
+
completedAt: lastRunAt,
|
|
2475
|
+
durationMs: 0,
|
|
2476
|
+
exitCode: 0,
|
|
2477
|
+
retryCount: 0,
|
|
2478
|
+
summary: 'Last run from cc-connect',
|
|
2479
|
+
},
|
|
2480
|
+
];
|
|
2297
2481
|
} catch {
|
|
2298
2482
|
return [];
|
|
2299
2483
|
}
|
|
@@ -2312,23 +2496,29 @@ app.get<{ Params: { id: string; runId: string } }>(
|
|
|
2312
2496
|
);
|
|
2313
2497
|
|
|
2314
2498
|
// Browse directories — returns subdirectories at the given path
|
|
2315
|
-
app.post<{ Body: { path?: string; limit?: number } }>(
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
const entries = readdirSync(target, { withFileTypes: true });
|
|
2321
|
-
const dirs = entries
|
|
2322
|
-
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
2323
|
-
.slice(0, limit)
|
|
2324
|
-
.map((e) => path.join(target, e.name));
|
|
2325
|
-
return { success: true, data: { path: target, dirs, hasParent: target !== path.dirname(target) } };
|
|
2326
|
-
} catch {
|
|
2327
|
-
return { success: false, error: `无法访问目录: ${target}` };
|
|
2328
|
-
}
|
|
2329
|
-
});
|
|
2499
|
+
app.post<{ Body: { path?: string; limit?: number } }>(
|
|
2500
|
+
'/api/config/browse-folders',
|
|
2501
|
+
async (request) => {
|
|
2502
|
+
const { path: dirPath, limit = 200 } = request.body ?? {};
|
|
2503
|
+
const target = dirPath && dirPath.trim() ? dirPath.trim() : os.homedir();
|
|
2330
2504
|
|
|
2331
|
-
|
|
2505
|
+
try {
|
|
2506
|
+
const entries = readdirSync(target, { withFileTypes: true });
|
|
2507
|
+
const dirs = entries
|
|
2508
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.'))
|
|
2509
|
+
.slice(0, limit)
|
|
2510
|
+
.map((e) => path.join(target, e.name));
|
|
2511
|
+
return {
|
|
2512
|
+
success: true,
|
|
2513
|
+
data: { path: target, dirs, hasParent: target !== path.dirname(target) },
|
|
2514
|
+
};
|
|
2515
|
+
} catch {
|
|
2516
|
+
return { success: false, error: `无法访问目录: ${target}` };
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
);
|
|
2520
|
+
|
|
2521
|
+
// POST /api/workspace/list — 文件目录浏览
|
|
2332
2522
|
app.post<{ Body: { dirPath?: string } }>('/api/workspace/list', async (request) => {
|
|
2333
2523
|
const { dirPath } = request.body ?? {};
|
|
2334
2524
|
const target = dirPath && dirPath.trim() ? dirPath.trim() : os.homedir();
|
|
@@ -2345,7 +2535,9 @@ app.post<{ Body: { dirPath?: string } }>('/api/workspace/list', async (request)
|
|
|
2345
2535
|
try {
|
|
2346
2536
|
const stat = statSync(fullPath);
|
|
2347
2537
|
size = stat.size;
|
|
2348
|
-
} catch {
|
|
2538
|
+
} catch {
|
|
2539
|
+
/* ignore */
|
|
2540
|
+
}
|
|
2349
2541
|
return {
|
|
2350
2542
|
name: e.name,
|
|
2351
2543
|
isDirectory,
|
|
@@ -2391,7 +2583,9 @@ function resolveEditorPath(root: string, rawPath: unknown): string {
|
|
|
2391
2583
|
throw new Error('filePath/dirPath 参数不能为空');
|
|
2392
2584
|
}
|
|
2393
2585
|
const requested = rawPath.trim();
|
|
2394
|
-
const resolved = path.resolve(
|
|
2586
|
+
const resolved = path.resolve(
|
|
2587
|
+
path.isAbsolute(requested) ? requested : path.join(root, requested)
|
|
2588
|
+
);
|
|
2395
2589
|
if (!isPathInsideRoot(root, resolved)) {
|
|
2396
2590
|
throw new Error('路径超出项目根目录');
|
|
2397
2591
|
}
|
|
@@ -2432,9 +2626,7 @@ app.get<{ Querystring: { root?: unknown; dirPath?: unknown; maxEntries?: string
|
|
|
2432
2626
|
async (request, reply) => {
|
|
2433
2627
|
try {
|
|
2434
2628
|
const root = resolveEditorRoot(request.query.root);
|
|
2435
|
-
const dirPath = request.query.dirPath
|
|
2436
|
-
? resolveEditorPath(root, request.query.dirPath)
|
|
2437
|
-
: root;
|
|
2629
|
+
const dirPath = request.query.dirPath ? resolveEditorPath(root, request.query.dirPath) : root;
|
|
2438
2630
|
const maxEntriesRaw = Number.parseInt(request.query.maxEntries ?? '', 10);
|
|
2439
2631
|
const maxEntries = Number.isFinite(maxEntriesRaw)
|
|
2440
2632
|
? Math.min(Math.max(maxEntriesRaw, 1), MAX_EDITOR_DIR_ENTRIES)
|
|
@@ -2500,31 +2692,30 @@ app.get<{ Querystring: { root?: unknown; filePath?: unknown } }>(
|
|
|
2500
2692
|
}
|
|
2501
2693
|
);
|
|
2502
2694
|
|
|
2503
|
-
app.post<{
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
}
|
|
2695
|
+
app.post<{
|
|
2696
|
+
Body: { root?: unknown; filePath?: unknown; content?: unknown; baselineMtimeMs?: unknown };
|
|
2697
|
+
}>('/api/editor/writeFile', async (request, reply) => {
|
|
2698
|
+
try {
|
|
2699
|
+
const root = resolveEditorRoot(request.body?.root);
|
|
2700
|
+
const filePath = resolveEditorPath(root, request.body?.filePath);
|
|
2701
|
+
const content = request.body?.content;
|
|
2702
|
+
if (typeof content !== 'string') {
|
|
2703
|
+
throw new Error('content 必须是字符串');
|
|
2704
|
+
}
|
|
2705
|
+
const baselineRaw = request.body?.baselineMtimeMs;
|
|
2706
|
+
if (typeof baselineRaw === 'number' && Number.isFinite(baselineRaw)) {
|
|
2707
|
+
const currentMtime = statSync(filePath).mtimeMs;
|
|
2708
|
+
if (Math.abs(currentMtime - baselineRaw) > 1) {
|
|
2709
|
+
throw new Error('CONFLICT: file changed on disk');
|
|
2519
2710
|
}
|
|
2520
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
2521
|
-
const st = statSync(filePath);
|
|
2522
|
-
return { mtimeMs: st.mtimeMs, size: st.size };
|
|
2523
|
-
} catch (err) {
|
|
2524
|
-
return sendEditorError(reply, err);
|
|
2525
2711
|
}
|
|
2712
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
2713
|
+
const st = statSync(filePath);
|
|
2714
|
+
return { mtimeMs: st.mtimeMs, size: st.size };
|
|
2715
|
+
} catch (err) {
|
|
2716
|
+
return sendEditorError(reply, err);
|
|
2526
2717
|
}
|
|
2527
|
-
);
|
|
2718
|
+
});
|
|
2528
2719
|
|
|
2529
2720
|
app.post<{ Body: { root?: unknown; parentDir?: unknown; fileName?: unknown } }>(
|
|
2530
2721
|
'/api/editor/createFile',
|
|
@@ -2532,7 +2723,8 @@ app.post<{ Body: { root?: unknown; parentDir?: unknown; fileName?: unknown } }>(
|
|
|
2532
2723
|
try {
|
|
2533
2724
|
const root = resolveEditorRoot(request.body?.root);
|
|
2534
2725
|
const parentDir = resolveEditorPath(root, request.body?.parentDir);
|
|
2535
|
-
const fileName =
|
|
2726
|
+
const fileName =
|
|
2727
|
+
typeof request.body?.fileName === 'string' ? request.body.fileName.trim() : '';
|
|
2536
2728
|
if (!fileName) {
|
|
2537
2729
|
throw new Error('fileName 不能为空');
|
|
2538
2730
|
}
|
|
@@ -2565,16 +2757,19 @@ app.post<{ Body: { root?: unknown; parentDir?: unknown; dirName?: unknown } }>(
|
|
|
2565
2757
|
}
|
|
2566
2758
|
);
|
|
2567
2759
|
|
|
2568
|
-
app.post<{ Body: { root?: unknown; filePath?: unknown } }>(
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2760
|
+
app.post<{ Body: { root?: unknown; filePath?: unknown } }>(
|
|
2761
|
+
'/api/editor/deleteFile',
|
|
2762
|
+
async (request, reply) => {
|
|
2763
|
+
try {
|
|
2764
|
+
const root = resolveEditorRoot(request.body?.root);
|
|
2765
|
+
const filePath = resolveEditorPath(root, request.body?.filePath);
|
|
2766
|
+
rmSync(filePath, { recursive: true, force: false });
|
|
2767
|
+
return { deletedPath: filePath };
|
|
2768
|
+
} catch (err) {
|
|
2769
|
+
return sendEditorError(reply, err);
|
|
2770
|
+
}
|
|
2576
2771
|
}
|
|
2577
|
-
|
|
2772
|
+
);
|
|
2578
2773
|
|
|
2579
2774
|
app.post<{ Body: { root?: unknown; sourcePath?: unknown; destDir?: unknown } }>(
|
|
2580
2775
|
'/api/editor/moveFile',
|
|
@@ -2680,7 +2875,8 @@ app.get('/api/editor/search', async () => ({ results: [], totalMatches: 0, trunc
|
|
|
2680
2875
|
|
|
2681
2876
|
// 消息分页 — store 期望 MessagesPage 结构
|
|
2682
2877
|
app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: string } }>(
|
|
2683
|
-
'/api/teams/:name/messages',
|
|
2878
|
+
'/api/teams/:name/messages',
|
|
2879
|
+
async (request) => {
|
|
2684
2880
|
const { name } = request.params;
|
|
2685
2881
|
const requestedLimit = Number(request.query.limit ?? 50);
|
|
2686
2882
|
const limit = Math.min(
|
|
@@ -2708,13 +2904,13 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
|
|
|
2708
2904
|
: undefined;
|
|
2709
2905
|
const session = sessionKey ? sessionByKey.get(sessionKey) : undefined;
|
|
2710
2906
|
return {
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2907
|
+
messageId: m.id,
|
|
2908
|
+
from: m.from,
|
|
2909
|
+
to: m.to,
|
|
2910
|
+
text: m.content,
|
|
2911
|
+
timestamp: m.ts,
|
|
2912
|
+
read: true,
|
|
2913
|
+
source: (m.role === 'user' ? 'user_sent' : 'inbox') as string,
|
|
2718
2914
|
session: sessionKey
|
|
2719
2915
|
? {
|
|
2720
2916
|
id: session?.id,
|
|
@@ -2747,101 +2943,94 @@ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: stri
|
|
|
2747
2943
|
|
|
2748
2944
|
// 消息 head(messages-head 不是标准路由,storeok调 getMessagesPage 的同路由带 limit)
|
|
2749
2945
|
// member-activity-meta
|
|
2750
|
-
app.get<{ Params: { name: string } }>(
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
}
|
|
2760
|
-
);
|
|
2946
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/member-activity-meta', async (request) => {
|
|
2947
|
+
const { name } = request.params;
|
|
2948
|
+
return {
|
|
2949
|
+
teamName: name,
|
|
2950
|
+
computedAt: new Date().toISOString(),
|
|
2951
|
+
members: {},
|
|
2952
|
+
feedRevision: '0',
|
|
2953
|
+
};
|
|
2954
|
+
});
|
|
2761
2955
|
|
|
2762
2956
|
// member-activity — GET /api/teams/:name/member-activity
|
|
2763
|
-
app.get<{ Params: { name: string } }>(
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
}
|
|
2773
|
-
);
|
|
2957
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/member-activity', async (request) => {
|
|
2958
|
+
const { name } = request.params;
|
|
2959
|
+
return {
|
|
2960
|
+
teamName: name,
|
|
2961
|
+
computedAt: new Date().toISOString(),
|
|
2962
|
+
members: {},
|
|
2963
|
+
feedRevision: '0',
|
|
2964
|
+
};
|
|
2965
|
+
});
|
|
2774
2966
|
|
|
2775
2967
|
// member-spawn-statuses — GET /api/teams/:name/member-spawn-statuses
|
|
2776
|
-
app.get<{ Params: { name: string } }>(
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
}
|
|
2784
|
-
);
|
|
2968
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/member-spawn-statuses', async (request) => {
|
|
2969
|
+
const { name } = request.params;
|
|
2970
|
+
return {
|
|
2971
|
+
statuses: {},
|
|
2972
|
+
runId: null,
|
|
2973
|
+
};
|
|
2974
|
+
});
|
|
2785
2975
|
|
|
2786
2976
|
// agent-runtime — GET /api/teams/:name/agent-runtime
|
|
2787
|
-
app.get<{ Params: { name: string } }>(
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
}
|
|
2797
|
-
);
|
|
2977
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/agent-runtime', async (request) => {
|
|
2978
|
+
const { name } = request.params;
|
|
2979
|
+
return {
|
|
2980
|
+
teamName: name,
|
|
2981
|
+
updatedAt: new Date().toISOString(),
|
|
2982
|
+
runId: null,
|
|
2983
|
+
members: {},
|
|
2984
|
+
};
|
|
2985
|
+
});
|
|
2798
2986
|
|
|
2799
2987
|
// lead-activity — GET /api/teams/:name/lead-activity
|
|
2800
|
-
app.get<{ Params: { name: string } }>(
|
|
2801
|
-
'
|
|
2802
|
-
|
|
2803
|
-
}
|
|
2804
|
-
);
|
|
2988
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/lead-activity', async () => {
|
|
2989
|
+
return { state: 'offline', updatedAt: new Date().toISOString() };
|
|
2990
|
+
});
|
|
2805
2991
|
|
|
2806
2992
|
// lead-context — GET /api/teams/:name/lead-context
|
|
2807
|
-
app.get<{ Params: { name: string } }>(
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
}
|
|
2811
|
-
);
|
|
2993
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/lead-context', async () => {
|
|
2994
|
+
return { usage: null };
|
|
2995
|
+
});
|
|
2812
2996
|
|
|
2813
2997
|
// sessions — 从 cc-connect project sessions 获取,转换为前端 Session 格式
|
|
2814
|
-
app.get<{ Params: { name: string } }>(
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
}
|
|
2998
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/sessions', async (request) => {
|
|
2999
|
+
try {
|
|
3000
|
+
const sessions = await cc.listSessions(request.params.name);
|
|
3001
|
+
return sessions.map((s) => ({
|
|
3002
|
+
id: s.id,
|
|
3003
|
+
title: s.user_name || s.chat_name || s.name || s.session_key,
|
|
3004
|
+
projectId: request.params.name,
|
|
3005
|
+
sessionKey: s.session_key,
|
|
3006
|
+
platform: s.platform,
|
|
3007
|
+
userName: s.user_name ?? null,
|
|
3008
|
+
chatName: s.chat_name ?? null,
|
|
3009
|
+
active: s.active,
|
|
3010
|
+
live: s.live,
|
|
3011
|
+
historyCount: s.history_count,
|
|
3012
|
+
createdAt: s.created_at,
|
|
3013
|
+
updatedAt: s.updated_at,
|
|
3014
|
+
lastMessage: s.last_message
|
|
3015
|
+
? {
|
|
3016
|
+
role: s.last_message.role,
|
|
3017
|
+
content: s.last_message.content,
|
|
3018
|
+
timestamp: s.last_message.timestamp,
|
|
3019
|
+
}
|
|
3020
|
+
: null,
|
|
3021
|
+
}));
|
|
3022
|
+
} catch {
|
|
3023
|
+
return [];
|
|
2838
3024
|
}
|
|
2839
|
-
);
|
|
3025
|
+
});
|
|
2840
3026
|
|
|
2841
3027
|
// GET session detail — 通过 cc-connect API 获取会话详情(含历史消息)
|
|
2842
3028
|
app.get<{ Params: { name: string; sessionId: string }; Querystring: { history_limit?: string } }>(
|
|
2843
|
-
'/api/teams/:name/sessions/:sessionId',
|
|
2844
|
-
|
|
3029
|
+
'/api/teams/:name/sessions/:sessionId',
|
|
3030
|
+
async (request) => {
|
|
3031
|
+
const historyLimit = request.query.history_limit
|
|
3032
|
+
? parseInt(request.query.history_limit, 10)
|
|
3033
|
+
: 500;
|
|
2845
3034
|
const detail = await cc.getSession(request.params.name, request.params.sessionId, historyLimit);
|
|
2846
3035
|
return mapCcSessionDetail(detail);
|
|
2847
3036
|
}
|
|
@@ -2849,12 +3038,15 @@ app.get<{ Params: { name: string; sessionId: string }; Querystring: { history_li
|
|
|
2849
3038
|
|
|
2850
3039
|
// DELETE session — 通过 cc-connect API 删除指定 session
|
|
2851
3040
|
app.delete<{ Params: { name: string; sessionId: string } }>(
|
|
2852
|
-
'/api/teams/:name/sessions/:sessionId',
|
|
3041
|
+
'/api/teams/:name/sessions/:sessionId',
|
|
3042
|
+
async (request, reply) => {
|
|
2853
3043
|
try {
|
|
2854
3044
|
await cc.deleteSession(request.params.name, request.params.sessionId);
|
|
2855
3045
|
return { ok: true };
|
|
2856
3046
|
} catch (err) {
|
|
2857
|
-
return reply
|
|
3047
|
+
return reply
|
|
3048
|
+
.code(500)
|
|
3049
|
+
.send({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
2858
3050
|
}
|
|
2859
3051
|
}
|
|
2860
3052
|
);
|
|
@@ -2863,30 +3055,37 @@ app.delete<{ Params: { name: string; sessionId: string } }>(
|
|
|
2863
3055
|
app.get('/api/teams/runtime/alive', async () => {
|
|
2864
3056
|
try {
|
|
2865
3057
|
const projects = await cc.listProjects();
|
|
2866
|
-
return await Promise.all(
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
3058
|
+
return await Promise.all(
|
|
3059
|
+
projects.map(async (p) => {
|
|
3060
|
+
let isAlive = false;
|
|
3061
|
+
try {
|
|
3062
|
+
const detail = await cc.getProject(p.name);
|
|
3063
|
+
isAlive = Array.isArray(detail.platforms) && detail.platforms.some((pl) => pl.connected);
|
|
3064
|
+
} catch {
|
|
3065
|
+
/* degraded */
|
|
3066
|
+
}
|
|
3067
|
+
return { teamName: p.name, isAlive, runId: p.name };
|
|
3068
|
+
})
|
|
3069
|
+
);
|
|
3070
|
+
} catch {
|
|
3071
|
+
return [];
|
|
3072
|
+
}
|
|
2875
3073
|
});
|
|
2876
3074
|
|
|
2877
3075
|
// process-alive — 查询 cc-connect project 在线状态
|
|
2878
|
-
app.get<{ Params: { name: string } }>(
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
3076
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/process-alive', async (request) => {
|
|
3077
|
+
try {
|
|
3078
|
+
const p = await cc.getProject(request.params.name);
|
|
3079
|
+
return Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
|
|
3080
|
+
} catch {
|
|
3081
|
+
return false;
|
|
2884
3082
|
}
|
|
2885
|
-
);
|
|
3083
|
+
});
|
|
2886
3084
|
|
|
2887
3085
|
// process-send — 从 Hermit UI 注入到 harness,不回发到 IM 平台。
|
|
2888
3086
|
app.post<{ Params: { name: string }; Body: { text?: string; message?: string } }>(
|
|
2889
|
-
'/api/teams/:name/process-send',
|
|
3087
|
+
'/api/teams/:name/process-send',
|
|
3088
|
+
async (request, reply) => {
|
|
2890
3089
|
try {
|
|
2891
3090
|
const text = request.body?.text ?? request.body?.message ?? '';
|
|
2892
3091
|
if (text) {
|
|
@@ -2906,28 +3105,22 @@ app.post<{ Params: { name: string }; Body: { text?: string; message?: string } }
|
|
|
2906
3105
|
);
|
|
2907
3106
|
|
|
2908
3107
|
// saved-request — 新版无此概念
|
|
2909
|
-
app.get<{ Params: { name: string } }>(
|
|
2910
|
-
'/api/teams/:name/saved-request', async () => null
|
|
2911
|
-
);
|
|
3108
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/saved-request', async () => null);
|
|
2912
3109
|
|
|
2913
3110
|
// kanban state — 返回空看板状态
|
|
2914
|
-
app.get<{ Params: { name: string } }>(
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
})
|
|
2920
|
-
);
|
|
3111
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/kanban', async (request) => ({
|
|
3112
|
+
teamName: request.params.name,
|
|
3113
|
+
reviewers: [],
|
|
3114
|
+
tasks: {},
|
|
3115
|
+
}));
|
|
2921
3116
|
|
|
2922
3117
|
// task-change-presence — 返回 {}
|
|
2923
|
-
app.get<{ Params: { name: string } }>(
|
|
2924
|
-
'/api/teams/:name/task-change-presence', async () => ({})
|
|
2925
|
-
);
|
|
3118
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/task-change-presence', async () => ({}));
|
|
2926
3119
|
|
|
2927
3120
|
// kanban column order — no-op
|
|
2928
|
-
app.post<{ Params: { name: string } }>(
|
|
2929
|
-
|
|
2930
|
-
);
|
|
3121
|
+
app.post<{ Params: { name: string } }>('/api/teams/:name/kanban-column-order', async () => ({
|
|
3122
|
+
ok: true,
|
|
3123
|
+
}));
|
|
2931
3124
|
|
|
2932
3125
|
// teams/tasks (全局任务列表 — 跨所有团队)
|
|
2933
3126
|
app.get('/api/teams/tasks', async () => {
|
|
@@ -2938,52 +3131,69 @@ app.get('/api/teams/tasks', async () => {
|
|
|
2938
3131
|
try {
|
|
2939
3132
|
const tasks = activeTasks(await svc.readTasks(p.name));
|
|
2940
3133
|
allTasks.push(...tasks.map(toTeamTask));
|
|
2941
|
-
} catch {
|
|
3134
|
+
} catch {
|
|
3135
|
+
/* skip */
|
|
3136
|
+
}
|
|
2942
3137
|
}
|
|
2943
3138
|
return allTasks;
|
|
2944
|
-
} catch {
|
|
3139
|
+
} catch {
|
|
3140
|
+
return [];
|
|
3141
|
+
}
|
|
2945
3142
|
});
|
|
2946
3143
|
|
|
2947
3144
|
// 团队任务子操作 — 全部委托给 svc.patchTask
|
|
2948
3145
|
app.post<{ Params: { name: string; id: string } }>(
|
|
2949
|
-
'/api/teams/:name/tasks/:id/request-review',
|
|
3146
|
+
'/api/teams/:name/tasks/:id/request-review',
|
|
3147
|
+
async (request) => {
|
|
2950
3148
|
try {
|
|
2951
3149
|
const task = await svc.patchTask(request.params.name, request.params.id, { status: 'done' });
|
|
2952
3150
|
return { ok: true, data: toTeamTask(task) };
|
|
2953
|
-
} catch {
|
|
3151
|
+
} catch {
|
|
3152
|
+
return { ok: true };
|
|
3153
|
+
}
|
|
2954
3154
|
}
|
|
2955
3155
|
);
|
|
2956
3156
|
app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
|
|
2957
|
-
'/api/teams/:name/tasks/:id/kanban',
|
|
3157
|
+
'/api/teams/:name/tasks/:id/kanban',
|
|
3158
|
+
async (request) => {
|
|
2958
3159
|
// kanban metadata — stored in board.json via patchTask (no-op for now, column tracked client-side)
|
|
2959
3160
|
return { ok: true };
|
|
2960
3161
|
}
|
|
2961
3162
|
);
|
|
2962
3163
|
app.patch<{ Params: { name: string; id: string }; Body: { status?: string } }>(
|
|
2963
|
-
'/api/teams/:name/tasks/:id/status',
|
|
3164
|
+
'/api/teams/:name/tasks/:id/status',
|
|
3165
|
+
async (request) => {
|
|
2964
3166
|
try {
|
|
2965
3167
|
const { status } = request.body ?? {};
|
|
2966
3168
|
const task = await svc.patchTask(request.params.name, request.params.id, {
|
|
2967
3169
|
status: status ? toTaskStatus(status) : undefined,
|
|
2968
3170
|
});
|
|
2969
3171
|
return toTeamTask(task);
|
|
2970
|
-
} catch {
|
|
3172
|
+
} catch {
|
|
3173
|
+
return { ok: true };
|
|
3174
|
+
}
|
|
2971
3175
|
}
|
|
2972
3176
|
);
|
|
2973
3177
|
app.patch<{ Params: { name: string; id: string }; Body: { owner?: string } }>(
|
|
2974
|
-
'/api/teams/:name/tasks/:id/owner',
|
|
3178
|
+
'/api/teams/:name/tasks/:id/owner',
|
|
3179
|
+
async (request) => {
|
|
2975
3180
|
try {
|
|
2976
3181
|
const body = request.body ?? {};
|
|
2977
|
-
const task = await svc.patchTask(request.params.name, request.params.id, {
|
|
3182
|
+
const task = await svc.patchTask(request.params.name, request.params.id, {
|
|
3183
|
+
assignee: body.owner ?? null,
|
|
3184
|
+
});
|
|
2978
3185
|
if (task.assignee) {
|
|
2979
3186
|
svc.dispatchTask(request.params.name, task).catch(() => {});
|
|
2980
3187
|
}
|
|
2981
3188
|
return toTeamTask(task);
|
|
2982
|
-
} catch {
|
|
3189
|
+
} catch {
|
|
3190
|
+
return { ok: true };
|
|
3191
|
+
}
|
|
2983
3192
|
}
|
|
2984
3193
|
);
|
|
2985
3194
|
app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
|
|
2986
|
-
'/api/teams/:name/tasks/:id/fields',
|
|
3195
|
+
'/api/teams/:name/tasks/:id/fields',
|
|
3196
|
+
async (request) => {
|
|
2987
3197
|
try {
|
|
2988
3198
|
const body = request.body ?? {};
|
|
2989
3199
|
const patch: Record<string, unknown> = {};
|
|
@@ -2991,11 +3201,14 @@ app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown>
|
|
|
2991
3201
|
if (body.description !== undefined) patch.description = body.description;
|
|
2992
3202
|
const task = await svc.patchTask(request.params.name, request.params.id, patch);
|
|
2993
3203
|
return toTeamTask(task);
|
|
2994
|
-
} catch {
|
|
3204
|
+
} catch {
|
|
3205
|
+
return { ok: true };
|
|
3206
|
+
}
|
|
2995
3207
|
}
|
|
2996
3208
|
);
|
|
2997
3209
|
app.post<{ Params: { name: string; id: string } }>(
|
|
2998
|
-
'/api/teams/:name/tasks/:id/start',
|
|
3210
|
+
'/api/teams/:name/tasks/:id/start',
|
|
3211
|
+
async (request) => {
|
|
2999
3212
|
try {
|
|
3000
3213
|
const task = await svc.patchTask(request.params.name, request.params.id, { status: 'doing' });
|
|
3001
3214
|
if (task.assignee) {
|
|
@@ -3003,11 +3216,14 @@ app.post<{ Params: { name: string; id: string } }>(
|
|
|
3003
3216
|
return { notifiedOwner: true };
|
|
3004
3217
|
}
|
|
3005
3218
|
return { notifiedOwner: false };
|
|
3006
|
-
} catch {
|
|
3219
|
+
} catch {
|
|
3220
|
+
return { notifiedOwner: false };
|
|
3221
|
+
}
|
|
3007
3222
|
}
|
|
3008
3223
|
);
|
|
3009
3224
|
app.post<{ Params: { name: string; id: string } }>(
|
|
3010
|
-
'/api/teams/:name/tasks/:id/start-by-user',
|
|
3225
|
+
'/api/teams/:name/tasks/:id/start-by-user',
|
|
3226
|
+
async (request) => {
|
|
3011
3227
|
try {
|
|
3012
3228
|
const task = await svc.patchTask(request.params.name, request.params.id, { status: 'doing' });
|
|
3013
3229
|
if (task.assignee) {
|
|
@@ -3015,13 +3231,19 @@ app.post<{ Params: { name: string; id: string } }>(
|
|
|
3015
3231
|
return { notifiedOwner: true };
|
|
3016
3232
|
}
|
|
3017
3233
|
return { notifiedOwner: false };
|
|
3018
|
-
} catch {
|
|
3234
|
+
} catch {
|
|
3235
|
+
return { notifiedOwner: false };
|
|
3236
|
+
}
|
|
3019
3237
|
}
|
|
3020
3238
|
);
|
|
3021
3239
|
app.post<{ Params: { name: string; id: string } }>(
|
|
3022
|
-
'/api/teams/:name/tasks/:id/soft-delete',
|
|
3240
|
+
'/api/teams/:name/tasks/:id/soft-delete',
|
|
3241
|
+
async (request, reply) => {
|
|
3023
3242
|
try {
|
|
3024
|
-
await svc.patchTask(request.params.name, request.params.id, {
|
|
3243
|
+
await svc.patchTask(request.params.name, request.params.id, {
|
|
3244
|
+
status: 'done',
|
|
3245
|
+
result: '__deleted__',
|
|
3246
|
+
});
|
|
3025
3247
|
return { ok: true };
|
|
3026
3248
|
} catch (err) {
|
|
3027
3249
|
return reply.code(404).send(reply500(err));
|
|
@@ -3029,7 +3251,8 @@ app.post<{ Params: { name: string; id: string } }>(
|
|
|
3029
3251
|
}
|
|
3030
3252
|
);
|
|
3031
3253
|
app.post<{ Params: { name: string; id: string } }>(
|
|
3032
|
-
'/api/teams/:name/tasks/:id/restore',
|
|
3254
|
+
'/api/teams/:name/tasks/:id/restore',
|
|
3255
|
+
async (request, reply) => {
|
|
3033
3256
|
try {
|
|
3034
3257
|
await svc.patchTask(request.params.name, request.params.id, { status: 'todo', result: null });
|
|
3035
3258
|
return { ok: true };
|
|
@@ -3038,54 +3261,55 @@ app.post<{ Params: { name: string; id: string } }>(
|
|
|
3038
3261
|
}
|
|
3039
3262
|
}
|
|
3040
3263
|
);
|
|
3041
|
-
app.get<{ Params: { name: string } }>(
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3264
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/deleted-tasks', async (request) => {
|
|
3265
|
+
try {
|
|
3266
|
+
const tasks = await svc.readTasks(request.params.name);
|
|
3267
|
+
return tasks.filter((t) => t.result === '__deleted__').map(toTeamTask);
|
|
3268
|
+
} catch {
|
|
3269
|
+
return [];
|
|
3047
3270
|
}
|
|
3048
|
-
);
|
|
3271
|
+
});
|
|
3049
3272
|
app.post<{ Params: { name: string; id: string }; Body: { text?: string } }>(
|
|
3050
|
-
'/api/teams/:name/tasks/:id/comments',
|
|
3273
|
+
'/api/teams/:name/tasks/:id/comments',
|
|
3274
|
+
async () => ({ ok: true })
|
|
3051
3275
|
);
|
|
3052
3276
|
app.post<{ Params: { name: string; id: string } }>(
|
|
3053
|
-
'/api/teams/:name/tasks/:id/clarification',
|
|
3277
|
+
'/api/teams/:name/tasks/:id/clarification',
|
|
3278
|
+
async () => ({ ok: true })
|
|
3054
3279
|
);
|
|
3055
3280
|
app.post<{ Params: { name: string; id: string } }>(
|
|
3056
|
-
'/api/teams/:name/tasks/:id/relationships',
|
|
3281
|
+
'/api/teams/:name/tasks/:id/relationships',
|
|
3282
|
+
async () => ({ ok: true })
|
|
3057
3283
|
);
|
|
3058
3284
|
|
|
3059
|
-
|
|
3060
3285
|
// 成员相关 stubs
|
|
3061
|
-
app.post<{ Params: { name: string } }>(
|
|
3062
|
-
'/api/teams/:name/members', async () => ({ ok: true })
|
|
3063
|
-
);
|
|
3286
|
+
app.post<{ Params: { name: string } }>('/api/teams/:name/members', async () => ({ ok: true }));
|
|
3064
3287
|
app.delete<{ Params: { name: string; memberName: string } }>(
|
|
3065
|
-
'/api/teams/:name/members/:memberName',
|
|
3288
|
+
'/api/teams/:name/members/:memberName',
|
|
3289
|
+
async () => ({ ok: true })
|
|
3066
3290
|
);
|
|
3067
3291
|
app.patch<{ Params: { name: string; memberName: string } }>(
|
|
3068
|
-
'/api/teams/:name/members/:memberName/role',
|
|
3292
|
+
'/api/teams/:name/members/:memberName/role',
|
|
3293
|
+
async () => ({ ok: true })
|
|
3069
3294
|
);
|
|
3070
3295
|
app.post<{ Params: { name: string; memberName: string } }>(
|
|
3071
|
-
'/api/teams/:name/members/:memberName/restart',
|
|
3296
|
+
'/api/teams/:name/members/:memberName/restart',
|
|
3297
|
+
async () => ({ ok: true })
|
|
3072
3298
|
);
|
|
3073
3299
|
app.post<{ Params: { name: string; memberName: string } }>(
|
|
3074
|
-
'/api/teams/:name/members/:memberName/skip-launch',
|
|
3300
|
+
'/api/teams/:name/members/:memberName/skip-launch',
|
|
3301
|
+
async () => ({ ok: true })
|
|
3075
3302
|
);
|
|
3076
3303
|
|
|
3077
3304
|
// claude logs
|
|
3078
|
-
app.get<{ Params: { name: string } }>(
|
|
3079
|
-
|
|
3080
|
-
|
|
3305
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/claude-logs', async () => ({
|
|
3306
|
+
logs: [],
|
|
3307
|
+
total: 0,
|
|
3308
|
+
}));
|
|
3081
3309
|
|
|
3082
3310
|
// restore / permanent delete
|
|
3083
|
-
app.post<{ Params: { name: string } }>(
|
|
3084
|
-
|
|
3085
|
-
);
|
|
3086
|
-
app.delete<{ Params: { name: string } }>(
|
|
3087
|
-
'/api/teams/:name/permanent', async () => ({ ok: true })
|
|
3088
|
-
);
|
|
3311
|
+
app.post<{ Params: { name: string } }>('/api/teams/:name/restore', async () => ({ ok: true }));
|
|
3312
|
+
app.delete<{ Params: { name: string } }>('/api/teams/:name/permanent', async () => ({ ok: true }));
|
|
3089
3313
|
|
|
3090
3314
|
// config operations
|
|
3091
3315
|
async function applyTeamConfigUpdate(
|
|
@@ -3097,11 +3321,9 @@ async function applyTeamConfigUpdate(
|
|
|
3097
3321
|
const color = typeof body.color === 'string' ? body.color.trim() : '';
|
|
3098
3322
|
const agentType = typeof body.agentType === 'string' ? body.agentType.trim() : '';
|
|
3099
3323
|
const workDir = typeof body.workDir === 'string' ? body.workDir.trim() : '';
|
|
3100
|
-
const permissionMode =
|
|
3101
|
-
typeof body.permissionMode === 'string' ? body.permissionMode.trim() : '';
|
|
3324
|
+
const permissionMode = typeof body.permissionMode === 'string' ? body.permissionMode.trim() : '';
|
|
3102
3325
|
const language = typeof body.language === 'string' ? body.language.trim() : '';
|
|
3103
|
-
const managedSources =
|
|
3104
|
-
typeof body.managedSources === 'string' ? body.managedSources.trim() : '';
|
|
3326
|
+
const managedSources = typeof body.managedSources === 'string' ? body.managedSources.trim() : '';
|
|
3105
3327
|
const showContextIndicator =
|
|
3106
3328
|
typeof body.showContextIndicator === 'boolean' ? body.showContextIndicator : undefined;
|
|
3107
3329
|
const replyFooter = typeof body.replyFooter === 'boolean' ? body.replyFooter : undefined;
|
|
@@ -3193,133 +3415,131 @@ async function applyTeamConfigUpdate(
|
|
|
3193
3415
|
};
|
|
3194
3416
|
}
|
|
3195
3417
|
|
|
3196
|
-
app.get<{ Params: { name: string } }>(
|
|
3197
|
-
|
|
3418
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/config', async (request, reply) => {
|
|
3419
|
+
try {
|
|
3420
|
+
const name = request.params.name;
|
|
3421
|
+
const p = await cc.getProject(name);
|
|
3422
|
+
// local metadata overlay
|
|
3423
|
+
let color = 'blue';
|
|
3424
|
+
let description = '';
|
|
3425
|
+
let language = '';
|
|
3426
|
+
let managedSources = '*';
|
|
3427
|
+
let disabledCommands: string[] = [];
|
|
3428
|
+
let showContextIndicator = false;
|
|
3429
|
+
let replyFooter = false;
|
|
3430
|
+
let injectSender = false;
|
|
3431
|
+
let permissionMode = 'default';
|
|
3432
|
+
let platformAllowFrom: Record<string, string> = {};
|
|
3198
3433
|
try {
|
|
3199
|
-
const
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
agentType: p.agent_type,
|
|
3268
|
-
workDir: p.work_dir ?? '',
|
|
3434
|
+
const meta = await svc.readTeamManifest(name);
|
|
3435
|
+
color = meta.color ?? color;
|
|
3436
|
+
description = meta.description ?? description;
|
|
3437
|
+
language = meta.language ?? language;
|
|
3438
|
+
managedSources = meta.managedSources ?? managedSources;
|
|
3439
|
+
disabledCommands = normalizeStringArray(meta.disabledCommands);
|
|
3440
|
+
showContextIndicator = meta.showContextIndicator ?? showContextIndicator;
|
|
3441
|
+
replyFooter = meta.replyFooter ?? replyFooter;
|
|
3442
|
+
injectSender = meta.injectSender ?? injectSender;
|
|
3443
|
+
permissionMode = meta.permissionMode ?? permissionMode;
|
|
3444
|
+
platformAllowFrom = normalizePlatformAllowFrom(meta.platformAllowFrom);
|
|
3445
|
+
} catch {
|
|
3446
|
+
/* ok */
|
|
3447
|
+
}
|
|
3448
|
+
const projectSettings = (p.settings ?? {}) as Record<string, unknown>;
|
|
3449
|
+
const resolvedLanguage =
|
|
3450
|
+
typeof projectSettings.language === 'string' && projectSettings.language.trim().length > 0
|
|
3451
|
+
? projectSettings.language.trim()
|
|
3452
|
+
: language;
|
|
3453
|
+
const resolvedManagedSources =
|
|
3454
|
+
typeof projectSettings.admin_from === 'string' && projectSettings.admin_from.trim().length > 0
|
|
3455
|
+
? projectSettings.admin_from.trim()
|
|
3456
|
+
: managedSources;
|
|
3457
|
+
const resolvedDisabledCommands =
|
|
3458
|
+
Array.isArray(projectSettings.disabled_commands) &&
|
|
3459
|
+
normalizeStringArray(projectSettings.disabled_commands).length > 0
|
|
3460
|
+
? normalizeStringArray(projectSettings.disabled_commands)
|
|
3461
|
+
: disabledCommands;
|
|
3462
|
+
const resolvedShowContextIndicator =
|
|
3463
|
+
typeof projectSettings.show_context_indicator === 'boolean'
|
|
3464
|
+
? projectSettings.show_context_indicator
|
|
3465
|
+
: showContextIndicator;
|
|
3466
|
+
const resolvedReplyFooter =
|
|
3467
|
+
typeof projectSettings.reply_footer === 'boolean'
|
|
3468
|
+
? projectSettings.reply_footer
|
|
3469
|
+
: replyFooter;
|
|
3470
|
+
const resolvedInjectSender =
|
|
3471
|
+
typeof projectSettings.inject_sender === 'boolean'
|
|
3472
|
+
? projectSettings.inject_sender
|
|
3473
|
+
: injectSender;
|
|
3474
|
+
const resolvedPlatformAllowFrom = (() => {
|
|
3475
|
+
const normalized = normalizePlatformAllowFrom(projectSettings.platform_allow_from);
|
|
3476
|
+
if (Object.keys(normalized).length > 0) {
|
|
3477
|
+
return normalized;
|
|
3478
|
+
}
|
|
3479
|
+
return platformAllowFrom;
|
|
3480
|
+
})();
|
|
3481
|
+
const resolvedPermissionMode =
|
|
3482
|
+
typeof p.agent_mode === 'string' && p.agent_mode.trim().length > 0
|
|
3483
|
+
? p.agent_mode.trim()
|
|
3484
|
+
: permissionMode;
|
|
3485
|
+
return {
|
|
3486
|
+
name,
|
|
3487
|
+
color,
|
|
3488
|
+
projectPath: p.work_dir ?? '',
|
|
3489
|
+
description,
|
|
3490
|
+
agentType: p.agent_type,
|
|
3491
|
+
workDir: p.work_dir ?? '',
|
|
3492
|
+
language: resolvedLanguage,
|
|
3493
|
+
managedSources: resolvedManagedSources,
|
|
3494
|
+
disabledCommands: resolvedDisabledCommands,
|
|
3495
|
+
showContextIndicator: resolvedShowContextIndicator,
|
|
3496
|
+
replyFooter: resolvedReplyFooter,
|
|
3497
|
+
injectSender: resolvedInjectSender,
|
|
3498
|
+
permissionMode: resolvedPermissionMode,
|
|
3499
|
+
platformAllowFrom: resolvedPlatformAllowFrom,
|
|
3500
|
+
settings: {
|
|
3501
|
+
...projectSettings,
|
|
3269
3502
|
language: resolvedLanguage,
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
admin_from: resolvedManagedSources,
|
|
3281
|
-
disabled_commands: resolvedDisabledCommands,
|
|
3282
|
-
show_context_indicator: resolvedShowContextIndicator,
|
|
3283
|
-
reply_footer: resolvedReplyFooter,
|
|
3284
|
-
inject_sender: resolvedInjectSender,
|
|
3285
|
-
platform_allow_from: resolvedPlatformAllowFrom,
|
|
3286
|
-
},
|
|
3287
|
-
};
|
|
3288
|
-
} catch { return reply.code(404).send({ error: 'not found' }); }
|
|
3503
|
+
admin_from: resolvedManagedSources,
|
|
3504
|
+
disabled_commands: resolvedDisabledCommands,
|
|
3505
|
+
show_context_indicator: resolvedShowContextIndicator,
|
|
3506
|
+
reply_footer: resolvedReplyFooter,
|
|
3507
|
+
inject_sender: resolvedInjectSender,
|
|
3508
|
+
platform_allow_from: resolvedPlatformAllowFrom,
|
|
3509
|
+
},
|
|
3510
|
+
};
|
|
3511
|
+
} catch {
|
|
3512
|
+
return reply.code(404).send({ error: 'not found' });
|
|
3289
3513
|
}
|
|
3290
|
-
);
|
|
3291
|
-
app.patch<{ Params: { name: string } }>(
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
return reply.code(400).send(reply500(err));
|
|
3301
|
-
}
|
|
3514
|
+
});
|
|
3515
|
+
app.patch<{ Params: { name: string } }>('/api/teams/:name/config', async (request, reply) => {
|
|
3516
|
+
try {
|
|
3517
|
+
const data = await applyTeamConfigUpdate(
|
|
3518
|
+
request.params.name,
|
|
3519
|
+
(request.body as Record<string, unknown>) ?? {}
|
|
3520
|
+
);
|
|
3521
|
+
return data;
|
|
3522
|
+
} catch (err) {
|
|
3523
|
+
return reply.code(400).send(reply500(err));
|
|
3302
3524
|
}
|
|
3303
|
-
);
|
|
3525
|
+
});
|
|
3304
3526
|
|
|
3305
3527
|
// provisioning stubs (新版无 provisioning 概念)
|
|
3306
3528
|
app.post('/api/teams/provisioning/prepare', async () => ({
|
|
3307
3529
|
runId: null,
|
|
3308
3530
|
warnings: [],
|
|
3309
3531
|
}));
|
|
3310
|
-
app.get<{ Params: { runId: string } }>(
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
'/api/teams/provisioning/:runId/cancel', async () => ({ ok: true })
|
|
3322
|
-
);
|
|
3532
|
+
app.get<{ Params: { runId: string } }>('/api/teams/provisioning/:runId', async () => ({
|
|
3533
|
+
runId: '',
|
|
3534
|
+
phase: 'done',
|
|
3535
|
+
progress: 100,
|
|
3536
|
+
message: '',
|
|
3537
|
+
done: true,
|
|
3538
|
+
error: null,
|
|
3539
|
+
}));
|
|
3540
|
+
app.post<{ Params: { runId: string } }>('/api/teams/provisioning/:runId/cancel', async () => ({
|
|
3541
|
+
ok: true,
|
|
3542
|
+
}));
|
|
3323
3543
|
|
|
3324
3544
|
// 团队创建已由上方 /api/teams/create 处理(cc-connect 直接调用)
|
|
3325
3545
|
|
|
@@ -3329,68 +3549,65 @@ app.post('/api/teams/templates/save', async () => ({ sources: [], templates: []
|
|
|
3329
3549
|
app.post('/api/teams/templates/refresh', async () => ({ sources: [], templates: [] }));
|
|
3330
3550
|
|
|
3331
3551
|
// replace members
|
|
3332
|
-
app.put<{ Params: { name: string } }>(
|
|
3333
|
-
'/api/teams/:name/members', async () => ({ ok: true })
|
|
3334
|
-
);
|
|
3552
|
+
app.put<{ Params: { name: string } }>('/api/teams/:name/members', async () => ({ ok: true }));
|
|
3335
3553
|
|
|
3336
3554
|
// draft
|
|
3337
|
-
app.delete<{ Params: { name: string } }>(
|
|
3338
|
-
'/api/teams/:name/draft', async () => ({ ok: true })
|
|
3339
|
-
);
|
|
3555
|
+
app.delete<{ Params: { name: string } }>('/api/teams/:name/draft', async () => ({ ok: true }));
|
|
3340
3556
|
|
|
3341
3557
|
// send-message — 从 Hermit 会话面板注入到 harness,不使用 Management /send(那会回发到 IM)。
|
|
3342
|
-
app.post<{
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3558
|
+
app.post<{
|
|
3559
|
+
Params: { name: string };
|
|
3560
|
+
Body: { member?: string; text?: string; content?: string; summary?: string; sessionKey?: string };
|
|
3561
|
+
}>('/api/teams/:name/send-message', async (request, reply) => {
|
|
3562
|
+
const teamName = request.params.name;
|
|
3563
|
+
const text = request.body?.text ?? request.body?.content ?? '';
|
|
3564
|
+
if (!text.trim()) return { ok: true, messageId: null };
|
|
3347
3565
|
|
|
3348
|
-
|
|
3566
|
+
const msgId = `hermit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3349
3567
|
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3568
|
+
// 使用固定格式 session key,保证 reply 事件能正确映射回 teamName
|
|
3569
|
+
const requestedSessionKey =
|
|
3570
|
+
typeof request.body?.sessionKey === 'string' ? request.body.sessionKey.trim() : '';
|
|
3571
|
+
let sessionKey = requestedSessionKey;
|
|
3354
3572
|
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3573
|
+
try {
|
|
3574
|
+
sessionKey = await sendHarnessMessageViaBridge({
|
|
3575
|
+
teamName,
|
|
3576
|
+
text,
|
|
3577
|
+
sessionKey,
|
|
3578
|
+
msgId,
|
|
3579
|
+
});
|
|
3580
|
+
} catch (err) {
|
|
3581
|
+
return reply.code(502).send({
|
|
3582
|
+
ok: false,
|
|
3583
|
+
error: err instanceof Error ? err.message : '发送到 harness 失败',
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3368
3586
|
|
|
3369
|
-
|
|
3370
|
-
|
|
3587
|
+
// 本地存储用户消息
|
|
3588
|
+
const userMsg = await svc
|
|
3589
|
+
.appendMessage(teamName, {
|
|
3371
3590
|
from: 'user',
|
|
3372
3591
|
to: teamName,
|
|
3373
3592
|
role: 'user',
|
|
3374
3593
|
content: text,
|
|
3375
3594
|
meta: { sessionKey },
|
|
3376
|
-
})
|
|
3377
|
-
|
|
3378
|
-
// 广播 SSE 让前端触发消息刷新
|
|
3379
|
-
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
3380
|
-
|
|
3381
|
-
return {
|
|
3382
|
-
ok: true,
|
|
3383
|
-
deliveredToInbox: true,
|
|
3384
|
-
messageId: userMsg?.id ?? msgId,
|
|
3385
|
-
runtimeDelivery: {
|
|
3386
|
-
attempted: true,
|
|
3387
|
-
delivered: true,
|
|
3388
|
-
},
|
|
3389
|
-
};
|
|
3390
|
-
}
|
|
3391
|
-
);
|
|
3595
|
+
})
|
|
3596
|
+
.catch(() => null);
|
|
3392
3597
|
|
|
3598
|
+
// 广播 SSE 让前端触发消息刷新
|
|
3599
|
+
broadcastSse('team-change', { type: 'inbox', teamName });
|
|
3393
3600
|
|
|
3601
|
+
return {
|
|
3602
|
+
ok: true,
|
|
3603
|
+
deliveredToInbox: true,
|
|
3604
|
+
messageId: userMsg?.id ?? msgId,
|
|
3605
|
+
runtimeDelivery: {
|
|
3606
|
+
attempted: true,
|
|
3607
|
+
delivered: true,
|
|
3608
|
+
},
|
|
3609
|
+
};
|
|
3610
|
+
});
|
|
3394
3611
|
|
|
3395
3612
|
// ===========================================================================
|
|
3396
3613
|
// 路由别名 — 修正前端调用路径与服务端路径的不匹配
|
|
@@ -3398,52 +3615,57 @@ app.post<{ Params: { name: string }; Body: { member?: string; text?: string; con
|
|
|
3398
3615
|
|
|
3399
3616
|
// requestReview: 前端调用 /tasks/:id/review,服务端原路由是 /tasks/:id/request-review
|
|
3400
3617
|
app.post<{ Params: { name: string; id: string } }>(
|
|
3401
|
-
'/api/teams/:name/tasks/:id/review',
|
|
3618
|
+
'/api/teams/:name/tasks/:id/review',
|
|
3619
|
+
async (request) => {
|
|
3402
3620
|
try {
|
|
3403
3621
|
const task = await svc.patchTask(request.params.name, request.params.id, { status: 'done' });
|
|
3404
3622
|
return { ok: true, data: toTeamTask(task) };
|
|
3405
|
-
} catch {
|
|
3623
|
+
} catch {
|
|
3624
|
+
return { ok: true };
|
|
3625
|
+
}
|
|
3406
3626
|
}
|
|
3407
3627
|
);
|
|
3408
3628
|
|
|
3409
3629
|
// updateKanban: 前端调用 PATCH /kanban/:taskId
|
|
3410
3630
|
app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
|
|
3411
|
-
'/api/teams/:name/kanban/:id',
|
|
3631
|
+
'/api/teams/:name/kanban/:id',
|
|
3632
|
+
async () => ({ ok: true })
|
|
3412
3633
|
);
|
|
3413
3634
|
|
|
3414
3635
|
// updateKanbanColumnOrder: 前端调用 PUT /kanban/column-order
|
|
3415
|
-
app.put<{ Params: { name: string } }>(
|
|
3416
|
-
|
|
3417
|
-
);
|
|
3636
|
+
app.put<{ Params: { name: string } }>('/api/teams/:name/kanban/column-order', async () => ({
|
|
3637
|
+
ok: true,
|
|
3638
|
+
}));
|
|
3418
3639
|
|
|
3419
3640
|
// updateConfig: 前端调用 PUT /config(服务端原有 PATCH,补充 PUT 别名)
|
|
3420
|
-
app.put<{ Params: { name: string } }>(
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
return reply.code(400).send(reply500(err));
|
|
3430
|
-
}
|
|
3641
|
+
app.put<{ Params: { name: string } }>('/api/teams/:name/config', async (request, reply) => {
|
|
3642
|
+
try {
|
|
3643
|
+
const data = await applyTeamConfigUpdate(
|
|
3644
|
+
request.params.name,
|
|
3645
|
+
(request.body as Record<string, unknown>) ?? {}
|
|
3646
|
+
);
|
|
3647
|
+
return data;
|
|
3648
|
+
} catch (err) {
|
|
3649
|
+
return reply.code(400).send(reply500(err));
|
|
3431
3650
|
}
|
|
3432
|
-
);
|
|
3651
|
+
});
|
|
3433
3652
|
|
|
3434
3653
|
// skipMemberForLaunch: 前端调用 /members/:memberName/skip
|
|
3435
3654
|
app.post<{ Params: { name: string; memberName: string } }>(
|
|
3436
|
-
'/api/teams/:name/members/:memberName/skip',
|
|
3655
|
+
'/api/teams/:name/members/:memberName/skip',
|
|
3656
|
+
async () => ({ ok: true })
|
|
3437
3657
|
);
|
|
3438
3658
|
|
|
3439
3659
|
// setTaskClarification: 前端调用 POST /task-clarification/:taskId
|
|
3440
3660
|
app.post<{ Params: { name: string; taskId: string } }>(
|
|
3441
|
-
'/api/teams/:name/task-clarification/:taskId',
|
|
3661
|
+
'/api/teams/:name/task-clarification/:taskId',
|
|
3662
|
+
async () => ({ ok: true })
|
|
3442
3663
|
);
|
|
3443
3664
|
|
|
3444
3665
|
// removeTaskRelationship: 前端调用 DELETE /tasks/:id/relationships
|
|
3445
3666
|
app.delete<{ Params: { name: string; id: string } }>(
|
|
3446
|
-
'/api/teams/:name/tasks/:id/relationships',
|
|
3667
|
+
'/api/teams/:name/tasks/:id/relationships',
|
|
3668
|
+
async () => ({ ok: true })
|
|
3447
3669
|
);
|
|
3448
3670
|
|
|
3449
3671
|
// ===========================================================================
|
|
@@ -3455,52 +3677,58 @@ app.post('/api/teams/config', async () => ({ ok: true }));
|
|
|
3455
3677
|
|
|
3456
3678
|
// kill-process
|
|
3457
3679
|
app.post<{ Params: { name: string }; Body: { pid?: number } }>(
|
|
3458
|
-
'/api/teams/:name/kill-process',
|
|
3680
|
+
'/api/teams/:name/kill-process',
|
|
3681
|
+
async () => ({ ok: true })
|
|
3459
3682
|
);
|
|
3460
3683
|
|
|
3461
3684
|
// member-logs
|
|
3462
3685
|
app.get<{ Params: { name: string; memberName: string } }>(
|
|
3463
|
-
'/api/teams/:name/member-logs/:memberName',
|
|
3686
|
+
'/api/teams/:name/member-logs/:memberName',
|
|
3687
|
+
async () => []
|
|
3464
3688
|
);
|
|
3465
3689
|
|
|
3466
3690
|
// task-logs
|
|
3467
3691
|
app.get<{ Params: { name: string; taskId: string } }>(
|
|
3468
|
-
'/api/teams/:name/task-logs/:taskId',
|
|
3692
|
+
'/api/teams/:name/task-logs/:taskId',
|
|
3693
|
+
async () => []
|
|
3469
3694
|
);
|
|
3470
3695
|
|
|
3471
3696
|
// activity
|
|
3472
|
-
app.get<{ Params: { name: string } }>(
|
|
3473
|
-
'/api/teams/:name/activity', async () => []
|
|
3474
|
-
);
|
|
3697
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/activity', async () => []);
|
|
3475
3698
|
|
|
3476
3699
|
// task-activity-detail
|
|
3477
|
-
app.get<{ Params: { name: string } }>(
|
|
3478
|
-
|
|
3479
|
-
);
|
|
3700
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/task-activity-detail', async () => ({
|
|
3701
|
+
entries: [],
|
|
3702
|
+
}));
|
|
3480
3703
|
|
|
3481
3704
|
// task-log-stream-summary
|
|
3482
3705
|
app.get<{ Params: { name: string; taskId: string } }>(
|
|
3483
|
-
'/api/teams/:name/task-log-stream-summary/:taskId',
|
|
3706
|
+
'/api/teams/:name/task-log-stream-summary/:taskId',
|
|
3707
|
+
async () => ({ chunks: [] })
|
|
3484
3708
|
);
|
|
3485
3709
|
|
|
3486
3710
|
// task-log-stream
|
|
3487
3711
|
app.get<{ Params: { name: string; taskId: string } }>(
|
|
3488
|
-
'/api/teams/:name/task-log-stream/:taskId',
|
|
3712
|
+
'/api/teams/:name/task-log-stream/:taskId',
|
|
3713
|
+
async () => ({ chunks: [] })
|
|
3489
3714
|
);
|
|
3490
3715
|
|
|
3491
3716
|
// exact-log-summaries
|
|
3492
3717
|
app.get<{ Params: { name: string; taskId: string } }>(
|
|
3493
|
-
'/api/teams/:name/exact-log-summaries/:taskId',
|
|
3718
|
+
'/api/teams/:name/exact-log-summaries/:taskId',
|
|
3719
|
+
async () => ({ logs: [] })
|
|
3494
3720
|
);
|
|
3495
3721
|
|
|
3496
3722
|
// exact-log-detail
|
|
3497
3723
|
app.get<{ Params: { name: string; taskId: string } }>(
|
|
3498
|
-
'/api/teams/:name/exact-log-detail/:taskId',
|
|
3724
|
+
'/api/teams/:name/exact-log-detail/:taskId',
|
|
3725
|
+
async () => ({ lines: [] })
|
|
3499
3726
|
);
|
|
3500
3727
|
|
|
3501
3728
|
// member-stats
|
|
3502
3729
|
app.get<{ Params: { name: string; memberName: string } }>(
|
|
3503
|
-
'/api/teams/:name/member-stats/:memberName',
|
|
3730
|
+
'/api/teams/:name/member-stats/:memberName',
|
|
3731
|
+
async () => ({
|
|
3504
3732
|
linesAdded: 0,
|
|
3505
3733
|
linesRemoved: 0,
|
|
3506
3734
|
filesTouched: [],
|
|
@@ -3519,12 +3747,12 @@ app.get<{ Params: { name: string; memberName: string } }>(
|
|
|
3519
3747
|
);
|
|
3520
3748
|
|
|
3521
3749
|
// tool-approval stubs
|
|
3522
|
-
app.post<{ Params: { name: string } }>(
|
|
3523
|
-
|
|
3524
|
-
);
|
|
3525
|
-
app.post<{ Params: { name: string } }>(
|
|
3526
|
-
|
|
3527
|
-
);
|
|
3750
|
+
app.post<{ Params: { name: string } }>('/api/teams/:name/tool-approval/respond', async () => ({
|
|
3751
|
+
ok: true,
|
|
3752
|
+
}));
|
|
3753
|
+
app.post<{ Params: { name: string } }>('/api/teams/:name/tool-approval/settings', async () => ({
|
|
3754
|
+
ok: true,
|
|
3755
|
+
}));
|
|
3528
3756
|
app.post('/api/teams/tool-approval/read-file', async () => ({ content: '' }));
|
|
3529
3757
|
|
|
3530
3758
|
// validate-cli-args
|
|
@@ -3533,13 +3761,12 @@ app.post('/api/teams/validate-cli-args', async () => ({ valid: true, args: [], e
|
|
|
3533
3761
|
// cross-team stubs
|
|
3534
3762
|
app.post('/api/cross-team/send', async () => ({ ok: true }));
|
|
3535
3763
|
app.get('/api/cross-team/targets', async () => []);
|
|
3536
|
-
app.get<{ Params: { name: string } }>(
|
|
3537
|
-
'/api/cross-team/outbox/:name', async () => []
|
|
3538
|
-
);
|
|
3764
|
+
app.get<{ Params: { name: string } }>('/api/cross-team/outbox/:name', async () => []);
|
|
3539
3765
|
|
|
3540
3766
|
// review stubs
|
|
3541
3767
|
app.get<{ Params: { name: string; memberName: string } }>(
|
|
3542
|
-
'/api/teams/:name/review/agent-changes/:memberName',
|
|
3768
|
+
'/api/teams/:name/review/agent-changes/:memberName',
|
|
3769
|
+
async (request) => ({
|
|
3543
3770
|
teamName: request.params.name,
|
|
3544
3771
|
memberName: request.params.memberName,
|
|
3545
3772
|
files: [],
|
|
@@ -3550,17 +3777,19 @@ app.get<{ Params: { name: string; memberName: string } }>(
|
|
|
3550
3777
|
})
|
|
3551
3778
|
);
|
|
3552
3779
|
app.get<{ Params: { name: string; taskId: string } }>(
|
|
3553
|
-
'/api/teams/:name/review/task-changes/:taskId',
|
|
3780
|
+
'/api/teams/:name/review/task-changes/:taskId',
|
|
3781
|
+
async () => ({ changes: [] })
|
|
3554
3782
|
);
|
|
3555
3783
|
app.get<{ Params: { name: string; memberName: string } }>(
|
|
3556
|
-
'/api/teams/:name/review/change-stats/:memberName',
|
|
3557
|
-
)
|
|
3558
|
-
app.get<{ Params: { name: string } }>(
|
|
3559
|
-
'/api/teams/:name/review/file-content', async () => ({ content: '' })
|
|
3560
|
-
);
|
|
3561
|
-
app.post<{ Params: { name: string } }>(
|
|
3562
|
-
'/api/teams/:name/review/apply-decisions', async () => ({ ok: true })
|
|
3784
|
+
'/api/teams/:name/review/change-stats/:memberName',
|
|
3785
|
+
async () => ({ stats: {} })
|
|
3563
3786
|
);
|
|
3787
|
+
app.get<{ Params: { name: string } }>('/api/teams/:name/review/file-content', async () => ({
|
|
3788
|
+
content: '',
|
|
3789
|
+
}));
|
|
3790
|
+
app.post<{ Params: { name: string } }>('/api/teams/:name/review/apply-decisions', async () => ({
|
|
3791
|
+
ok: true,
|
|
3792
|
+
}));
|
|
3564
3793
|
app.post('/api/teams/review/check-conflict', async () => ({ conflict: false }));
|
|
3565
3794
|
app.post('/api/teams/review/preview-reject', async () => ({ preview: '' }));
|
|
3566
3795
|
app.post('/api/teams/review/save-edited-file', async () => ({ ok: true }));
|