@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.
Files changed (65) hide show
  1. package/dist-renderer/assets/{ProjectEditorOverlay-Br0X83Jf.js → ProjectEditorOverlay-DlFQ6mai.js} +1 -1
  2. package/dist-renderer/assets/{TeamGraphOverlay-DHMTbZPZ.js → TeamGraphOverlay-D2TPMPGR.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-DzIiX7yH.js → _basePickBy-Cmd0RHLQ.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-6hZuzTLU.js → _baseUniq-BI_iy8ea.js} +1 -1
  5. package/dist-renderer/assets/{arc-CXgO6fx_.js → arc-NzW2mjTP.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DKWgtDHr.js → architectureDiagram-VXUJARFQ-Bzq85AYv.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-DOMUcC40.js → blockDiagram-VD42YOAC-D1PvYS-b.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B_k2L7qX.js → c4Diagram-YG6GDRKO-D49RKzPC.js} +1 -1
  9. package/dist-renderer/assets/channel-Ch7JrfUu.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-BeD_ccFy.js → chunk-4BX2VUAB-fmI_MQmQ.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-ClZfkA5w.js → chunk-55IACEB6-Xsv9RCXZ.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-5XluxXsn.js → chunk-B4BG7PRW-BE1KO8Um.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-BzIjjNVm.js → chunk-DI55MBZ5-tqJ7Mv7f.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-HgH3MK_H.js → chunk-FMBD7UC4-DMD45MVJ.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-WeC5T3Ba.js → chunk-QN33PNHL-DOhGrz-q.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-Cu1ApHfW.js → chunk-QZHKN3VN-D8yDgJdD.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-BOhlynJM.js → chunk-TZMSLE5B-BcsEDu7A.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-z9I4AnFy.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-z9I4AnFy.js +1 -0
  20. package/dist-renderer/assets/clone-Dfi1Jx6l.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DGZSihDQ.js → cose-bilkent-S5V4N54A-DlSqGHMX.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-CnxwCbku.js → dagre-6UL2VRFP-BTT9tSAx.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-DsIhoxdI.js → diagram-PSM6KHXK-Du-U-mK2.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-Cmh9KUF5.js → diagram-QEK2KX5R-jFdHeKas.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CKxV456A.js → diagram-S2PKOQOG-DKLNK2bu.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-EnvYjOjc.js → erDiagram-Q2GNP2WA-CZxHgIIo.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-BmNeWY_A.js → flowDiagram-NV44I4VS-v4XStCD0.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D30fyK-u.js → ganttDiagram-JELNMOA3-DJjD_BEL.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-CrUNiYg1.js → gitGraphDiagram-V2S2FVAM-BNy-jr03.js} +1 -1
  30. package/dist-renderer/assets/{graph-CY1gTfTb.js → graph-DDTrn6je.js} +1 -1
  31. package/dist-renderer/assets/{index-CaEbzwAU.js → index-BBp78BAu.js} +1 -1
  32. package/dist-renderer/assets/{index-59r209c1.js → index-BQrwHZ-k.js} +84 -84
  33. package/dist-renderer/assets/{index-DMR9B1UP.js → index-CRKQSG9S.js} +1 -1
  34. package/dist-renderer/assets/{index-9_hO4N1e.js → index-D8_B-cfs.js} +1 -1
  35. package/dist-renderer/assets/{index-BC2hXmg_.js → index-DR6Wz52b.js} +1 -1
  36. package/dist-renderer/assets/{index-D5K-SjBG.js → index-eotrJaYy.js} +1 -1
  37. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-By_XUlcD.js → infoDiagram-HS3SLOUP-DqnOsuza.js} +1 -1
  38. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BM1LJE9m.js → journeyDiagram-XKPGCS4Q-DTobaO1d.js} +1 -1
  39. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DHIW3aTA.js → kanban-definition-3W4ZIXB7-HbwVOvWc.js} +1 -1
  40. package/dist-renderer/assets/{layout-DAKiL_Mo.js → layout--VYmTcw2.js} +1 -1
  41. package/dist-renderer/assets/{linear-DwOaRYea.js → linear-BsJh89Mr.js} +1 -1
  42. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-b7bJ2cha.js → mindmap-definition-VGOIOE7T-BZqUZePd.js} +1 -1
  43. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-DxyL9Zr2.js → pieDiagram-ADFJNKIX-B1q_nH6P.js} +1 -1
  44. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CR33pHlF.js → quadrantDiagram-AYHSOK5B-UD8QhSEu.js} +1 -1
  45. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-BAiSRSlh.js → requirementDiagram-UZGBJVZJ-BA_i7Nw8.js} +1 -1
  46. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8JmDjoa.js → sankeyDiagram-TZEHDZUN-CMTnX-2d.js} +1 -1
  47. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-c1d0Wi1m.js → sequenceDiagram-WL72ISMW-BQXDB615.js} +1 -1
  48. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-nT8BiH2O.js → stateDiagram-FKZM4ZOC-BAsPXy6X.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-Dmibmlso.js → stateDiagram-v2-4FDKWEC3-DTUIBfce.js} +1 -1
  50. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-DpoRepUA.js → timeline-definition-IT6M3QCI-BdasmVkC.js} +1 -1
  51. package/dist-renderer/assets/{treemap-GDKQZRPO-C41UJeIH.js → treemap-GDKQZRPO-BkKQqIui.js} +1 -1
  52. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-KMjGARKN.js → xychartDiagram-PRI3JC2R-EAlPHOdx.js} +1 -1
  53. package/dist-renderer/index.html +1 -1
  54. package/package.json +1 -1
  55. package/src/main/server.ts +29 -12
  56. package/src/main/services/system-manager/BuiltinWorkflowSeeder.ts +139 -0
  57. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +1 -1
  58. package/src/renderer/api/httpClient.ts +17 -15
  59. package/src/renderer/components/team/TeamListView.tsx +1 -0
  60. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +88 -29
  61. package/src/shared/types/team.ts +4 -0
  62. package/dist-renderer/assets/channel-D0XS_akr.js +0 -1
  63. package/dist-renderer/assets/classDiagram-2ON5EDUG-D13Ffs0U.js +0 -1
  64. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D13Ffs0U.js +0 -1
  65. package/dist-renderer/assets/clone-B1ZrxI1D.js +0 -1
@@ -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
- return await systemManagerConfig.getConfig();
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 name = String(body.teamName ?? body.displayName ?? '').trim();
1268
- const displayName = String(body.displayName ?? body.teamName ?? '').trim() || name;
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 (!name) return reply.code(400).send({ error: 'name required' });
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
- // Check for duplicate displayName to prevent creating multiple teams with the same Chinese name
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 normalizedName = displayName.toLowerCase();
1278
- const duplicate = existingTeams.find(
1279
- (t) => t.displayName?.toLowerCase() === normalizedName
1294
+ const duplicateProject = existingTeams.find(
1295
+ (t) => t.bindProject?.toLowerCase() === bindProject.toLowerCase()
1280
1296
  );
1281
- if (duplicate) {
1297
+ if (duplicateProject) {
1282
1298
  return reply.code(409).send({
1283
- error: `数字员工"${displayName}"已存在(ID: ${duplicate.slug})。请使用不同的名称。`,
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: name,
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:${name}:${Date.now()}` };
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.displayName);
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(10_000);
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(10_000);
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(10_000);
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(10_000);
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(10_000);
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(10_000);
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 allTakenTeamNames = useMemo(
156
- () => [...new Set([...existingTeamNames, ...provisioningTeamNames])],
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 && sanitizedTeamName) clearProvisioningError?.(sanitizedTeamName);
271
- }, [open, clearProvisioningError, sanitizedTeamName]);
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: sanitizedTeamName,
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 (allTakenTeamNames.includes(sanitizedTeamName)) {
299
- setLocalError(isNameProvisioning ? '数字员工正在启动中' : '数字员工名称已存在');
329
+ if (!teamName.trim()) {
330
+ setLocalError('请输入数字员工名称');
300
331
  return false;
301
332
  }
302
- if (!sanitizedTeamName) {
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[sanitizedTeamName] && (
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[sanitizedTeamName]}
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(sanitizedTeamName, effectiveCwd || undefined, {
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={!sanitizedTeamName || !effectiveCwd || isSubmitting} onClick={handleCreate}>
650
+ <Button size="sm" disabled={!teamName.trim() || !bindProject || !effectiveCwd || isSubmitting} onClick={handleCreate}>
592
651
  {isSubmitting ? '创建中...' : '创建数字员工'}
593
652
  </Button>
594
653
  </>
@@ -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};