@yancyyu/openhermit 1.6.41 → 1.6.42
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/dist-renderer/assets/{ProjectEditorOverlay-Br0X83Jf.js → ProjectEditorOverlay-DlFQ6mai.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-DHMTbZPZ.js → TeamGraphOverlay-D2TPMPGR.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-DzIiX7yH.js → _basePickBy-Cmd0RHLQ.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-6hZuzTLU.js → _baseUniq-BI_iy8ea.js} +1 -1
- package/dist-renderer/assets/{arc-CXgO6fx_.js → arc-NzW2mjTP.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DKWgtDHr.js → architectureDiagram-VXUJARFQ-Bzq85AYv.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-DOMUcC40.js → blockDiagram-VD42YOAC-D1PvYS-b.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B_k2L7qX.js → c4Diagram-YG6GDRKO-D49RKzPC.js} +1 -1
- package/dist-renderer/assets/channel-Ch7JrfUu.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-BeD_ccFy.js → chunk-4BX2VUAB-fmI_MQmQ.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-ClZfkA5w.js → chunk-55IACEB6-Xsv9RCXZ.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-5XluxXsn.js → chunk-B4BG7PRW-BE1KO8Um.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-BzIjjNVm.js → chunk-DI55MBZ5-tqJ7Mv7f.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-HgH3MK_H.js → chunk-FMBD7UC4-DMD45MVJ.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-WeC5T3Ba.js → chunk-QN33PNHL-DOhGrz-q.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-Cu1ApHfW.js → chunk-QZHKN3VN-D8yDgJdD.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BOhlynJM.js → chunk-TZMSLE5B-BcsEDu7A.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-z9I4AnFy.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-z9I4AnFy.js +1 -0
- package/dist-renderer/assets/clone-Dfi1Jx6l.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DGZSihDQ.js → cose-bilkent-S5V4N54A-DlSqGHMX.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-CnxwCbku.js → dagre-6UL2VRFP-BTT9tSAx.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-DsIhoxdI.js → diagram-PSM6KHXK-Du-U-mK2.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-Cmh9KUF5.js → diagram-QEK2KX5R-jFdHeKas.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-CKxV456A.js → diagram-S2PKOQOG-DKLNK2bu.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-EnvYjOjc.js → erDiagram-Q2GNP2WA-CZxHgIIo.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-BmNeWY_A.js → flowDiagram-NV44I4VS-v4XStCD0.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D30fyK-u.js → ganttDiagram-JELNMOA3-DJjD_BEL.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-CrUNiYg1.js → gitGraphDiagram-V2S2FVAM-BNy-jr03.js} +1 -1
- package/dist-renderer/assets/{graph-CY1gTfTb.js → graph-DDTrn6je.js} +1 -1
- package/dist-renderer/assets/{index-CaEbzwAU.js → index-BBp78BAu.js} +1 -1
- package/dist-renderer/assets/{index-59r209c1.js → index-BQrwHZ-k.js} +84 -84
- package/dist-renderer/assets/{index-DMR9B1UP.js → index-CRKQSG9S.js} +1 -1
- package/dist-renderer/assets/{index-9_hO4N1e.js → index-D8_B-cfs.js} +1 -1
- package/dist-renderer/assets/{index-BC2hXmg_.js → index-DR6Wz52b.js} +1 -1
- package/dist-renderer/assets/{index-D5K-SjBG.js → index-eotrJaYy.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-By_XUlcD.js → infoDiagram-HS3SLOUP-DqnOsuza.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BM1LJE9m.js → journeyDiagram-XKPGCS4Q-DTobaO1d.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DHIW3aTA.js → kanban-definition-3W4ZIXB7-HbwVOvWc.js} +1 -1
- package/dist-renderer/assets/{layout-DAKiL_Mo.js → layout--VYmTcw2.js} +1 -1
- package/dist-renderer/assets/{linear-DwOaRYea.js → linear-BsJh89Mr.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-b7bJ2cha.js → mindmap-definition-VGOIOE7T-BZqUZePd.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-DxyL9Zr2.js → pieDiagram-ADFJNKIX-B1q_nH6P.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CR33pHlF.js → quadrantDiagram-AYHSOK5B-UD8QhSEu.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-BAiSRSlh.js → requirementDiagram-UZGBJVZJ-BA_i7Nw8.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8JmDjoa.js → sankeyDiagram-TZEHDZUN-CMTnX-2d.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-c1d0Wi1m.js → sequenceDiagram-WL72ISMW-BQXDB615.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-nT8BiH2O.js → stateDiagram-FKZM4ZOC-BAsPXy6X.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-Dmibmlso.js → stateDiagram-v2-4FDKWEC3-DTUIBfce.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-DpoRepUA.js → timeline-definition-IT6M3QCI-BdasmVkC.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-C41UJeIH.js → treemap-GDKQZRPO-BkKQqIui.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-KMjGARKN.js → xychartDiagram-PRI3JC2R-EAlPHOdx.js} +1 -1
- package/dist-renderer/index.html +1 -1
- package/package.json +1 -1
- package/src/main/server.ts +29 -12
- package/src/main/services/system-manager/BuiltinWorkflowSeeder.ts +139 -0
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +1 -1
- package/src/renderer/api/httpClient.ts +17 -15
- package/src/renderer/components/team/TeamListView.tsx +1 -0
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +88 -29
- package/src/shared/types/team.ts +4 -0
- package/dist-renderer/assets/channel-D0XS_akr.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-D13Ffs0U.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D13Ffs0U.js +0 -1
- package/dist-renderer/assets/clone-B1ZrxI1D.js +0 -1
package/src/main/server.ts
CHANGED
|
@@ -59,6 +59,7 @@ import { CollaborationBoardService } from './services/teams-mvp/CollaborationBoa
|
|
|
59
59
|
import { SystemManagerConfigService } from './services/system-manager/SystemManagerConfigService';
|
|
60
60
|
import { SystemManagerPtyService } from './services/system-manager/SystemManagerPtyService';
|
|
61
61
|
import { WorkflowPromptService } from './services/system-manager/WorkflowPromptService';
|
|
62
|
+
import { seedBuiltinWorkflows, ensureGlobalWorkflows } from './services/system-manager/BuiltinWorkflowSeeder';
|
|
62
63
|
import {
|
|
63
64
|
SYSTEM_MANAGER_BIND_PROJECT,
|
|
64
65
|
SYSTEM_MANAGER_DISPLAY_NAME,
|
|
@@ -1039,7 +1040,15 @@ app.get('/api/system-manager/status', async (_request, reply) => {
|
|
|
1039
1040
|
|
|
1040
1041
|
app.get('/api/system-manager/config', async (_request, reply) => {
|
|
1041
1042
|
try {
|
|
1042
|
-
|
|
1043
|
+
const config = await systemManagerConfig.getConfig();
|
|
1044
|
+
// Seed builtin workflows into workspace if missing
|
|
1045
|
+
const workspaceWorkflows = config.selectedWorkDir
|
|
1046
|
+
? path.join(config.selectedWorkDir, 'workflows')
|
|
1047
|
+
: undefined;
|
|
1048
|
+
if (workspaceWorkflows) {
|
|
1049
|
+
void seedBuiltinWorkflows(workspaceWorkflows);
|
|
1050
|
+
}
|
|
1051
|
+
return config;
|
|
1043
1052
|
} catch (err) {
|
|
1044
1053
|
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1045
1054
|
}
|
|
@@ -1264,23 +1273,30 @@ app.get('/api/teams', async () => {
|
|
|
1264
1273
|
app.post('/api/teams/create', async (request, reply) => {
|
|
1265
1274
|
try {
|
|
1266
1275
|
const body = (request.body ?? {}) as Record<string, unknown>;
|
|
1267
|
-
const
|
|
1268
|
-
const displayName = String(body.displayName ?? body.teamName ?? '').trim()
|
|
1276
|
+
const bindProject = String(body.bindProject ?? '').trim();
|
|
1277
|
+
const displayName = String(body.displayName ?? body.teamName ?? '').trim();
|
|
1269
1278
|
const harness = String(body.harness ?? 'claudecode');
|
|
1270
1279
|
let workDir = String(body.workDir ?? body.cwd ?? '');
|
|
1271
1280
|
|
|
1272
|
-
if (!
|
|
1281
|
+
if (!bindProject) return reply.code(400).send({ error: 'bindProject required' });
|
|
1282
|
+
if (!displayName) return reply.code(400).send({ error: 'displayName required' });
|
|
1273
1283
|
if (!workDir) return reply.code(400).send({ error: 'workDir required' });
|
|
1274
1284
|
|
|
1275
|
-
//
|
|
1285
|
+
// Validate bindProject is ASCII-safe (for URL routing and cc-connect project name)
|
|
1286
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(bindProject)) {
|
|
1287
|
+
return reply.code(400).send({
|
|
1288
|
+
error: '项目标识只能包含小写英文字母、数字、连字符和下划线,且必须以字母或数字开头',
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Check for duplicate bindProject (unique identifier, replaces displayName duplicate check)
|
|
1276
1293
|
const existingTeams = await svc.listTeams().catch(() => []);
|
|
1277
|
-
const
|
|
1278
|
-
|
|
1279
|
-
(t) => t.displayName?.toLowerCase() === normalizedName
|
|
1294
|
+
const duplicateProject = existingTeams.find(
|
|
1295
|
+
(t) => t.bindProject?.toLowerCase() === bindProject.toLowerCase()
|
|
1280
1296
|
);
|
|
1281
|
-
if (
|
|
1297
|
+
if (duplicateProject) {
|
|
1282
1298
|
return reply.code(409).send({
|
|
1283
|
-
error:
|
|
1299
|
+
error: `项目标识"${bindProject}"已被"${duplicateProject.displayName}"使用,请换一个。`,
|
|
1284
1300
|
});
|
|
1285
1301
|
}
|
|
1286
1302
|
|
|
@@ -1293,7 +1309,7 @@ app.post('/api/teams/create', async (request, reply) => {
|
|
|
1293
1309
|
// 本地创建只落 Hermit 团队目录;飞书/微信等外部平台在团队内按需绑定。
|
|
1294
1310
|
await svc.createTeam({
|
|
1295
1311
|
displayName,
|
|
1296
|
-
bindProject
|
|
1312
|
+
bindProject,
|
|
1297
1313
|
harness,
|
|
1298
1314
|
workDir,
|
|
1299
1315
|
color: typeof body.color === 'string' ? body.color : undefined,
|
|
@@ -1301,7 +1317,7 @@ app.post('/api/teams/create', async (request, reply) => {
|
|
|
1301
1317
|
createCcProject: false,
|
|
1302
1318
|
});
|
|
1303
1319
|
|
|
1304
|
-
return { runId: `local:${
|
|
1320
|
+
return { runId: `local:${bindProject}:${Date.now()}` };
|
|
1305
1321
|
} catch (err) {
|
|
1306
1322
|
return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
1307
1323
|
}
|
|
@@ -5653,6 +5669,7 @@ function reply500(err: unknown) {
|
|
|
5653
5669
|
// 启动 cc-connect Bridge WebSocket 连接(注册 platform=hermit adapter)
|
|
5654
5670
|
bridge.start();
|
|
5655
5671
|
await initializeTaskBusFromSettings();
|
|
5672
|
+
await ensureGlobalWorkflows();
|
|
5656
5673
|
|
|
5657
5674
|
try {
|
|
5658
5675
|
await app.listen({ host: HOST, port: PORT });
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BuiltinWorkflowSeeder — 将内置 workflow 作为 Claude Code 自定义命令
|
|
3
|
+
* 复制到控制台工作空间的 `.claude/commands/` 目录。
|
|
4
|
+
*
|
|
5
|
+
* 内置 workflow 以代码常量形式内嵌,在控制台打开工作空间时自动复制到
|
|
6
|
+
* <workspace>/.claude/commands/ 目录,成为原生 `/doctor` 等斜杠命令。
|
|
7
|
+
*/
|
|
8
|
+
import { mkdir, writeFile, stat } from 'node:fs/promises';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { createLogger } from '@shared/utils/logger';
|
|
13
|
+
|
|
14
|
+
const logger = createLogger('BuiltinWorkflowSeeder');
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Builtin workflow definitions
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const BUILTIN_WORKFLOWS: Record<string, string> = {
|
|
21
|
+
'doctor.md': `# Hermit Doctor — 环境诊断
|
|
22
|
+
|
|
23
|
+
你是一个 Hermit 运维诊断助手。请按以下步骤逐一检查当前环境,报告每项状态(✅ 正常 / ⚠️ 警告 / ❌ 异常),并在最后给出总结和建议。
|
|
24
|
+
|
|
25
|
+
## 1. 基础环境
|
|
26
|
+
|
|
27
|
+
- 检查操作系统和版本
|
|
28
|
+
- 检查可用内存(\`free -h\` 或 \`vm_stat\`)
|
|
29
|
+
- 检查磁盘空间(\`df -h .\`)
|
|
30
|
+
- 检查文件描述符限制(\`ulimit -n\`)
|
|
31
|
+
|
|
32
|
+
## 2. Claude Code CLI
|
|
33
|
+
|
|
34
|
+
- 检查 \`claude\` 是否在 PATH 中(\`which claude\`)
|
|
35
|
+
- 检查 Claude Code 版本(\`claude --version\`)
|
|
36
|
+
- 检查登录状态(\`claude auth status\` 或检查 \`~/.claude\` 目录)
|
|
37
|
+
|
|
38
|
+
## 3. cc-connect 连通性
|
|
39
|
+
|
|
40
|
+
- 检查 cc-connect 进程是否运行(\`ps aux | grep cc-connect\`)
|
|
41
|
+
- 检查 cc-connect API 是否可达(\`curl -s http://127.0.0.1:6300/api/v1/status\`)
|
|
42
|
+
- 检查 cc-connect 配置文件是否存在(\`~/.cc-connect/config.toml\`)
|
|
43
|
+
- 列出已配置的项目数量
|
|
44
|
+
|
|
45
|
+
## 4. Hermit 数据目录
|
|
46
|
+
|
|
47
|
+
- 检查 \`~/.hermit/\` 目录结构是否完整
|
|
48
|
+
- 检查 \`~/.hermit/teams/\` 下的团队数量和状态
|
|
49
|
+
- 检查是否有 pendingDelete 或 restartRequired 标记的团队
|
|
50
|
+
- 检查团队 team.json 是否有效(schemaVersion 字段)
|
|
51
|
+
|
|
52
|
+
## 5. 会话健康
|
|
53
|
+
|
|
54
|
+
- 统计当前运行的 Claude 会话数量(\`ps aux | grep claude | grep -v grep\`)
|
|
55
|
+
- 检查是否有僵尸进程
|
|
56
|
+
- 检查内存占用是否异常(单个会话 > 200MB 为警告)
|
|
57
|
+
|
|
58
|
+
## 6. 网络和端口
|
|
59
|
+
|
|
60
|
+
- 检查 Hermit 服务端口是否正常监听
|
|
61
|
+
- 检查 cc-connect 端口是否正常监听
|
|
62
|
+
- 检查 DNS 解析是否正常(\`nslookup api.anthropic.com\`)
|
|
63
|
+
|
|
64
|
+
## 输出格式
|
|
65
|
+
|
|
66
|
+
完成后请输出:
|
|
67
|
+
|
|
68
|
+
\`\`\`
|
|
69
|
+
🔍 Hermit Doctor 诊断报告
|
|
70
|
+
=========================
|
|
71
|
+
操作系统: ...
|
|
72
|
+
内存: ... (可用 / 总计)
|
|
73
|
+
磁盘: ... (可用 / 总计)
|
|
74
|
+
|
|
75
|
+
✅/⚠️/❌ Claude Code CLI: ...
|
|
76
|
+
✅/⚠️/❌ cc-connect: ...
|
|
77
|
+
✅/⚠️/❌ Hermit 数据: ...
|
|
78
|
+
✅/⚠️/❌ 会话健康: ...
|
|
79
|
+
✅/⚠️/❌ 网络: ...
|
|
80
|
+
|
|
81
|
+
📊 总结:
|
|
82
|
+
- 正常项: N
|
|
83
|
+
- 警告项: N
|
|
84
|
+
- 异常项: N
|
|
85
|
+
|
|
86
|
+
💡 建议:
|
|
87
|
+
1. ...
|
|
88
|
+
2. ...
|
|
89
|
+
\`\`\`
|
|
90
|
+
`,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Public API
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
function hermitHome(): string {
|
|
98
|
+
return process.env.HERMIT_HOME || path.join(os.homedir(), '.hermit');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 将内置命令文件复制到工作空间的 .claude/commands/ 目录。
|
|
103
|
+
* 如果已存在同名文件则跳过(尊重用户自定义)。
|
|
104
|
+
*
|
|
105
|
+
* @param workspaceDir 工作空间根目录
|
|
106
|
+
* @returns 实际复制的文件数量
|
|
107
|
+
*/
|
|
108
|
+
export async function seedBuiltinWorkflows(workspaceDir: string): Promise<number> {
|
|
109
|
+
let copied = 0;
|
|
110
|
+
try {
|
|
111
|
+
const targetDir = path.join(workspaceDir, '.claude', 'commands');
|
|
112
|
+
await mkdir(targetDir, { recursive: true });
|
|
113
|
+
|
|
114
|
+
for (const [filename, content] of Object.entries(BUILTIN_WORKFLOWS)) {
|
|
115
|
+
const targetPath = path.join(targetDir, filename);
|
|
116
|
+
const exists = await stat(targetPath).then(() => true).catch(() => false);
|
|
117
|
+
if (exists) continue;
|
|
118
|
+
|
|
119
|
+
await writeFile(targetPath, content, 'utf-8');
|
|
120
|
+
copied++;
|
|
121
|
+
logger.info(`seeded builtin workflow: ${filename} → ${targetPath}`);
|
|
122
|
+
}
|
|
123
|
+
} catch (err) {
|
|
124
|
+
logger.warn('failed to seed builtin workflows:', err instanceof Error ? err.message : err);
|
|
125
|
+
}
|
|
126
|
+
return copied;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Ensure ~/.hermit/.claude/commands/ has the builtin commands.
|
|
131
|
+
* Called once at app startup as fallback.
|
|
132
|
+
*/
|
|
133
|
+
export async function ensureGlobalWorkflows(): Promise<void> {
|
|
134
|
+
const globalWorkspace = hermitHome();
|
|
135
|
+
const copied = await seedBuiltinWorkflows(globalWorkspace);
|
|
136
|
+
if (copied > 0) {
|
|
137
|
+
logger.info(`seeded ${copied} builtin command(s) to ${path.join(globalWorkspace, '.claude', 'commands')}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -205,7 +205,7 @@ export class TeamWorkspaceService {
|
|
|
205
205
|
if (!input.bindProject) throw new Error('bindProject is required');
|
|
206
206
|
if (!input.workDir) throw new Error('workDir is required');
|
|
207
207
|
|
|
208
|
-
const slug = await this.createUniqueStorageSlug(input.
|
|
208
|
+
const slug = await this.createUniqueStorageSlug(input.bindProject);
|
|
209
209
|
const root = teamRoot(slug);
|
|
210
210
|
|
|
211
211
|
await fs.promises.mkdir(root, { recursive: true });
|
|
@@ -240,8 +240,8 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
240
240
|
return JSON.parse(text, (key, value) => HttpAPIClient.reviveDates(key, value)) as T;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
private async get<T>(path: string): Promise<T> {
|
|
244
|
-
const { controller, timeout } = this.createTimeoutController(
|
|
243
|
+
private async get<T>(path: string, timeoutMs = 10_000): Promise<T> {
|
|
244
|
+
const { controller, timeout } = this.createTimeoutController(timeoutMs);
|
|
245
245
|
try {
|
|
246
246
|
const res = await fetch(`${this.baseUrl}${path}`, { signal: controller.signal });
|
|
247
247
|
return this.parseJson<T>(res);
|
|
@@ -250,8 +250,8 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
private async post<T>(path: string, body?: unknown): Promise<T> {
|
|
254
|
-
const { controller, timeout } = this.createTimeoutController(
|
|
253
|
+
private async post<T>(path: string, body?: unknown, timeoutMs = 10_000): Promise<T> {
|
|
254
|
+
const { controller, timeout } = this.createTimeoutController(timeoutMs);
|
|
255
255
|
try {
|
|
256
256
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
257
257
|
method: 'POST',
|
|
@@ -280,8 +280,8 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
private async del<T>(path: string, body?: unknown): Promise<T> {
|
|
284
|
-
const { controller, timeout } = this.createTimeoutController(
|
|
283
|
+
private async del<T>(path: string, body?: unknown, timeoutMs = 10_000): Promise<T> {
|
|
284
|
+
const { controller, timeout } = this.createTimeoutController(timeoutMs);
|
|
285
285
|
try {
|
|
286
286
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
287
287
|
method: 'DELETE',
|
|
@@ -295,8 +295,8 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
private async put<T>(path: string, body?: unknown): Promise<T> {
|
|
299
|
-
const { controller, timeout } = this.createTimeoutController(
|
|
298
|
+
private async put<T>(path: string, body?: unknown, timeoutMs = 10_000): Promise<T> {
|
|
299
|
+
const { controller, timeout } = this.createTimeoutController(timeoutMs);
|
|
300
300
|
try {
|
|
301
301
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
302
302
|
method: 'PUT',
|
|
@@ -310,8 +310,8 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
private async patch<T>(path: string, body?: unknown): Promise<T> {
|
|
314
|
-
const { controller, timeout } = this.createTimeoutController(
|
|
313
|
+
private async patch<T>(path: string, body?: unknown, timeoutMs = 10_000): Promise<T> {
|
|
314
|
+
const { controller, timeout } = this.createTimeoutController(timeoutMs);
|
|
315
315
|
try {
|
|
316
316
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
317
317
|
method: 'PATCH',
|
|
@@ -325,8 +325,8 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
private async delete<T>(path: string): Promise<T> {
|
|
329
|
-
const { controller, timeout } = this.createTimeoutController(
|
|
328
|
+
private async delete<T>(path: string, timeoutMs = 10_000): Promise<T> {
|
|
329
|
+
const { controller, timeout } = this.createTimeoutController(timeoutMs);
|
|
330
330
|
try {
|
|
331
331
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
332
332
|
method: 'DELETE',
|
|
@@ -1093,11 +1093,11 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
1093
1093
|
};
|
|
1094
1094
|
|
|
1095
1095
|
teams: TeamsAPI = {
|
|
1096
|
-
list: async (): Promise<TeamSummary[]> => this.get<TeamSummary[]>('/api/teams'),
|
|
1096
|
+
list: async (): Promise<TeamSummary[]> => this.get<TeamSummary[]>('/api/teams', 30_000),
|
|
1097
1097
|
ensureSystemManager: async (): Promise<SystemManagerSummary> =>
|
|
1098
1098
|
this.post<SystemManagerSummary>('/api/system-manager/ensure'),
|
|
1099
1099
|
getData: async (teamName: string): Promise<TeamViewSnapshot> =>
|
|
1100
|
-
this.get<TeamViewSnapshot>(`/api/teams/${encodeURIComponent(teamName)}/data
|
|
1100
|
+
this.get<TeamViewSnapshot>(`/api/teams/${encodeURIComponent(teamName)}/data`, 30_000),
|
|
1101
1101
|
getTaskChangePresence: async (): Promise<
|
|
1102
1102
|
Record<string, 'has_changes' | 'no_changes' | 'unknown'>
|
|
1103
1103
|
> => {
|
|
@@ -1477,7 +1477,9 @@ export class HttpAPIClient implements ElectronAPI {
|
|
|
1477
1477
|
},
|
|
1478
1478
|
restartMember: async (teamName: string, memberName: string): Promise<void> => {
|
|
1479
1479
|
await this.post(
|
|
1480
|
-
`/api/teams/${encodeURIComponent(teamName)}/members/${encodeURIComponent(memberName)}/restart
|
|
1480
|
+
`/api/teams/${encodeURIComponent(teamName)}/members/${encodeURIComponent(memberName)}/restart`,
|
|
1481
|
+
undefined,
|
|
1482
|
+
30_000
|
|
1481
1483
|
);
|
|
1482
1484
|
},
|
|
1483
1485
|
skipMemberForLaunch: async (teamName: string, memberName: string): Promise<void> => {
|
|
@@ -840,6 +840,7 @@ export const TeamListView = (): React.JSX.Element => {
|
|
|
840
840
|
provisioningErrorsByTeam={provisioningErrorByTeam}
|
|
841
841
|
clearProvisioningError={clearProvisioningError}
|
|
842
842
|
existingTeamNames={teams.map((t) => t.teamName)}
|
|
843
|
+
existingBindProjects={teams.map((t) => t.bindProject).filter(Boolean) as string[]}
|
|
843
844
|
provisioningTeamNames={provisioningTeamNames}
|
|
844
845
|
activeTeams={activeTeams}
|
|
845
846
|
initialData={copyData ?? undefined}
|
|
@@ -76,6 +76,32 @@ function sanitizeTeamName(name: string): string {
|
|
|
76
76
|
return result;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Generate a unique ASCII project identifier from a display name.
|
|
81
|
+
* For Chinese names: produces "team-xxxx" (4-char random suffix).
|
|
82
|
+
* For ASCII names: produces a slugified version.
|
|
83
|
+
*/
|
|
84
|
+
function generateBindProject(displayName: string): string {
|
|
85
|
+
const trimmed = displayName.trim();
|
|
86
|
+
if (!trimmed) return '';
|
|
87
|
+
// Try to extract ASCII parts from the name
|
|
88
|
+
const asciiParts = trimmed
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.normalize('NFKD')
|
|
91
|
+
.replace(/[̀-ͯ]/g, '')
|
|
92
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
93
|
+
.replace(/-+/g, '-')
|
|
94
|
+
.replace(/^-|-$/g, '');
|
|
95
|
+
const base = asciiParts || 'team';
|
|
96
|
+
const suffix = Math.random().toString(36).slice(2, 6);
|
|
97
|
+
return `${base}-${suffix}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Validate bindProject: ASCII lowercase alphanumeric, hyphens, underscores. */
|
|
101
|
+
function isValidBindProject(value: string): boolean {
|
|
102
|
+
return /^[a-z0-9][a-z0-9_-]{1,}$/.test(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
// ---------------------------------------------------------------------------
|
|
80
106
|
// Wizard step types
|
|
81
107
|
// ---------------------------------------------------------------------------
|
|
@@ -92,6 +118,7 @@ interface CreateTeamDialogProps {
|
|
|
92
118
|
provisioningErrorsByTeam: Record<string, string | null>;
|
|
93
119
|
clearProvisioningError?: (teamName?: string) => void;
|
|
94
120
|
existingTeamNames: string[];
|
|
121
|
+
existingBindProjects?: string[];
|
|
95
122
|
provisioningTeamNames?: string[];
|
|
96
123
|
activeTeams?: ActiveTeamRef[];
|
|
97
124
|
initialData?: unknown;
|
|
@@ -107,6 +134,7 @@ export const CreateTeamDialog = ({
|
|
|
107
134
|
provisioningErrorsByTeam,
|
|
108
135
|
clearProvisioningError,
|
|
109
136
|
existingTeamNames,
|
|
137
|
+
existingBindProjects = [],
|
|
110
138
|
provisioningTeamNames = [],
|
|
111
139
|
activeTeams,
|
|
112
140
|
defaultProjectPath,
|
|
@@ -137,6 +165,17 @@ export const CreateTeamDialog = ({
|
|
|
137
165
|
const [selectedHarness, setSelectedHarness] = useState<CcAgentType>('claudecode');
|
|
138
166
|
const [description, setDescription] = useState('');
|
|
139
167
|
|
|
168
|
+
// ── bindProject (ASCII unique identifier) ────────────────────────────
|
|
169
|
+
const [bindProject, setBindProject] = useState('');
|
|
170
|
+
const [bindProjectManuallyEdited, setBindProjectManuallyEdited] = useState(false);
|
|
171
|
+
|
|
172
|
+
// Auto-generate bindProject from displayName when not manually edited
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (bindProjectManuallyEdited) return;
|
|
175
|
+
const auto = generateBindProject(teamName);
|
|
176
|
+
setBindProject(auto);
|
|
177
|
+
}, [teamName, bindProjectManuallyEdited]);
|
|
178
|
+
|
|
140
179
|
// ── Projects (for path selector) ─────────────────────────────────────
|
|
141
180
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
142
181
|
const [projectsLoading, setProjectsLoading] = useState(false);
|
|
@@ -152,19 +191,8 @@ export const CreateTeamDialog = ({
|
|
|
152
191
|
const [fieldErrors, setFieldErrors] = useState<{ teamName?: string; cwd?: string }>({});
|
|
153
192
|
|
|
154
193
|
// ── Name conflict detection ──────────────────────────────────────────
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
[existingTeamNames, provisioningTeamNames]
|
|
158
|
-
);
|
|
159
|
-
const sanitizedTeamName = sanitizeTeamName(teamName);
|
|
160
|
-
const trimmedInputName = teamName.trim().toLowerCase();
|
|
161
|
-
// Check both slug (for ASCII names) and display name (for Chinese/Unicode names)
|
|
162
|
-
// This prevents creating "产品经理团队" multiple times with different slugs (team-3, team-4...)
|
|
163
|
-
const isNameTaken =
|
|
164
|
-
existingTeamNames.includes(sanitizedTeamName) ||
|
|
165
|
-
activeTeams?.some((t) => t.displayName.toLowerCase() === trimmedInputName) === true;
|
|
166
|
-
const isNameProvisioning =
|
|
167
|
-
(provisioningTeamNames.includes(sanitizedTeamName) && !isNameTaken);
|
|
194
|
+
const isBindProjectTaken = existingBindProjects.includes(bindProject);
|
|
195
|
+
const isNameProvisioning = provisioningTeamNames.includes(bindProject) && !isBindProjectTaken;
|
|
168
196
|
|
|
169
197
|
const effectiveCwd =
|
|
170
198
|
cwdMode === 'project'
|
|
@@ -267,8 +295,8 @@ export const CreateTeamDialog = ({
|
|
|
267
295
|
|
|
268
296
|
// ── Clear provisioning error on open ─────────────────────────────────
|
|
269
297
|
useEffect(() => {
|
|
270
|
-
if (open &&
|
|
271
|
-
}, [open, clearProvisioningError,
|
|
298
|
+
if (open && bindProject) clearProvisioningError?.(bindProject);
|
|
299
|
+
}, [open, clearProvisioningError, bindProject]);
|
|
272
300
|
|
|
273
301
|
// ── Reset state on close ─────────────────────────────────────────────
|
|
274
302
|
const resetState = () => {
|
|
@@ -277,11 +305,14 @@ export const CreateTeamDialog = ({
|
|
|
277
305
|
setIsSubmitting(false);
|
|
278
306
|
setConflictDismissed(false);
|
|
279
307
|
setSelectedProviderRef(null);
|
|
308
|
+
setBindProject('');
|
|
309
|
+
setBindProjectManuallyEdited(false);
|
|
280
310
|
setStep('name');
|
|
281
311
|
};
|
|
282
312
|
|
|
283
313
|
const buildCreateRequest = (): TeamCreateRequest => ({
|
|
284
|
-
teamName:
|
|
314
|
+
teamName: bindProject,
|
|
315
|
+
bindProject,
|
|
285
316
|
displayName: teamName.trim() || undefined,
|
|
286
317
|
description: description.trim() || undefined,
|
|
287
318
|
color: teamColor || undefined,
|
|
@@ -295,12 +326,20 @@ export const CreateTeamDialog = ({
|
|
|
295
326
|
});
|
|
296
327
|
|
|
297
328
|
const validateCreateFields = (): boolean => {
|
|
298
|
-
if (
|
|
299
|
-
setLocalError(
|
|
329
|
+
if (!teamName.trim()) {
|
|
330
|
+
setLocalError('请输入数字员工名称');
|
|
300
331
|
return false;
|
|
301
332
|
}
|
|
302
|
-
if (!
|
|
303
|
-
setLocalError('
|
|
333
|
+
if (!bindProject) {
|
|
334
|
+
setLocalError('请输入项目标识');
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
if (!isValidBindProject(bindProject)) {
|
|
338
|
+
setLocalError('项目标识只能包含小写英文字母、数字、连字符和下划线(至少2个字符,以字母或数字开头)');
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
if (isBindProjectTaken) {
|
|
342
|
+
setLocalError(isNameProvisioning ? '数字员工正在启动中' : `项目标识"${bindProject}"已存在,请换一个`);
|
|
304
343
|
return false;
|
|
305
344
|
}
|
|
306
345
|
if (!effectiveCwd) {
|
|
@@ -399,11 +438,6 @@ export const CreateTeamDialog = ({
|
|
|
399
438
|
placeholder="例如:产品助手 / 前端工程师"
|
|
400
439
|
autoFocus
|
|
401
440
|
/>
|
|
402
|
-
{isNameTaken && (
|
|
403
|
-
<p className="text-[11px]" style={{ color: 'var(--field-error-text)' }}>
|
|
404
|
-
数字员工名称已存在
|
|
405
|
-
</p>
|
|
406
|
-
)}
|
|
407
441
|
{isNameProvisioning && (
|
|
408
442
|
<p className="text-[11px]" style={{ color: 'var(--warning-text)' }}>
|
|
409
443
|
同名数字员工正在启动中
|
|
@@ -411,6 +445,31 @@ export const CreateTeamDialog = ({
|
|
|
411
445
|
)}
|
|
412
446
|
</div>
|
|
413
447
|
|
|
448
|
+
<div className="space-y-1.5">
|
|
449
|
+
<Label htmlFor="team-bind-project">项目标识</Label>
|
|
450
|
+
<Input
|
|
451
|
+
id="team-bind-project"
|
|
452
|
+
className={cn(
|
|
453
|
+
'h-8 text-xs font-mono',
|
|
454
|
+
isBindProjectTaken && 'border-[var(--field-error-border)]'
|
|
455
|
+
)}
|
|
456
|
+
value={bindProject}
|
|
457
|
+
onChange={(e) => {
|
|
458
|
+
setBindProject(e.target.value.toLowerCase().replace(/[^a-z0-9_-]/g, ''));
|
|
459
|
+
setBindProjectManuallyEdited(true);
|
|
460
|
+
}}
|
|
461
|
+
placeholder="auto-generated-id"
|
|
462
|
+
/>
|
|
463
|
+
{isBindProjectTaken && (
|
|
464
|
+
<p className="text-[11px]" style={{ color: 'var(--field-error-text)' }}>
|
|
465
|
+
该项目标识已存在
|
|
466
|
+
</p>
|
|
467
|
+
)}
|
|
468
|
+
<p className="text-[11px] text-[var(--color-text-muted)]">
|
|
469
|
+
用于 URL 路由和 cc-connect 项目绑定,仅限小写英文/数字/连字符
|
|
470
|
+
</p>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
414
473
|
<div className="space-y-1.5">
|
|
415
474
|
<Label htmlFor="team-harness">Agent 类型</Label>
|
|
416
475
|
<HarnessSelect
|
|
@@ -541,7 +600,7 @@ export const CreateTeamDialog = ({
|
|
|
541
600
|
{localError}
|
|
542
601
|
</p>
|
|
543
602
|
)}
|
|
544
|
-
{provisioningErrorsByTeam[
|
|
603
|
+
{provisioningErrorsByTeam[bindProject] && (
|
|
545
604
|
<p
|
|
546
605
|
className="mt-1 rounded border p-2 text-xs"
|
|
547
606
|
style={{
|
|
@@ -550,7 +609,7 @@ export const CreateTeamDialog = ({
|
|
|
550
609
|
backgroundColor: 'var(--field-error-bg)',
|
|
551
610
|
}}
|
|
552
611
|
>
|
|
553
|
-
{provisioningErrorsByTeam[
|
|
612
|
+
{provisioningErrorsByTeam[bindProject]}
|
|
554
613
|
</p>
|
|
555
614
|
)}
|
|
556
615
|
</div>
|
|
@@ -572,7 +631,7 @@ export const CreateTeamDialog = ({
|
|
|
572
631
|
<Button
|
|
573
632
|
size="sm"
|
|
574
633
|
onClick={() => {
|
|
575
|
-
onOpenTeam(
|
|
634
|
+
onOpenTeam(bindProject, effectiveCwd || undefined, {
|
|
576
635
|
displayName: teamName.trim() || undefined,
|
|
577
636
|
});
|
|
578
637
|
clearDraft();
|
|
@@ -588,7 +647,7 @@ export const CreateTeamDialog = ({
|
|
|
588
647
|
<Button variant="outline" size="sm" onClick={onClose}>
|
|
589
648
|
取消
|
|
590
649
|
</Button>
|
|
591
|
-
<Button size="sm" disabled={!
|
|
650
|
+
<Button size="sm" disabled={!teamName.trim() || !bindProject || !effectiveCwd || isSubmitting} onClick={handleCreate}>
|
|
592
651
|
{isSubmitting ? '创建中...' : '创建数字员工'}
|
|
593
652
|
</Button>
|
|
594
653
|
</>
|
package/src/shared/types/team.ts
CHANGED
|
@@ -99,6 +99,8 @@ export interface TeamSummaryMember {
|
|
|
99
99
|
export interface TeamSummary {
|
|
100
100
|
teamName: string;
|
|
101
101
|
displayName: string;
|
|
102
|
+
/** cc-connect project name — ASCII, unique identifier for routing */
|
|
103
|
+
bindProject?: string;
|
|
102
104
|
description: string;
|
|
103
105
|
color?: string;
|
|
104
106
|
memberCount: number;
|
|
@@ -1500,6 +1502,8 @@ export interface TeamProvisioningMemberInput {
|
|
|
1500
1502
|
|
|
1501
1503
|
export interface TeamCreateRequest {
|
|
1502
1504
|
teamName: string;
|
|
1505
|
+
/** cc-connect project name — ASCII, unique. Auto-generated if not provided. */
|
|
1506
|
+
bindProject?: string;
|
|
1503
1507
|
displayName?: string;
|
|
1504
1508
|
description?: string;
|
|
1505
1509
|
color?: string;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a6 as o,a7 as n}from"./index-59r209c1.js";const t=(a,r)=>o.lang.round(n.parse(a)[r]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-5XluxXsn.js";import{_ as i}from"./index-59r209c1.js";import"./chunk-FMBD7UC4-HgH3MK_H.js";import"./chunk-55IACEB6-ClZfkA5w.js";import"./chunk-QN33PNHL-WeC5T3Ba.js";import"./splashScene-D0YB9uxm.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-B4BG7PRW-5XluxXsn.js";import{_ as i}from"./index-59r209c1.js";import"./chunk-FMBD7UC4-HgH3MK_H.js";import"./chunk-55IACEB6-ClZfkA5w.js";import"./chunk-QN33PNHL-WeC5T3Ba.js";import"./splashScene-D0YB9uxm.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as r}from"./_baseUniq-6hZuzTLU.js";var e=4;function a(o){return r(o,e)}export{a as c};
|