@adversity/coding-tool-x 2.2.0

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 (125) hide show
  1. package/CHANGELOG.md +333 -0
  2. package/LICENSE +21 -0
  3. package/README.md +404 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
  6. package/dist/web/assets/index-aL3cKxSK.css +41 -0
  7. package/dist/web/favicon.ico +0 -0
  8. package/dist/web/index.html +14 -0
  9. package/dist/web/logo.png +0 -0
  10. package/docs/CHANGELOG.md +582 -0
  11. package/docs/DIRECTORY_MIGRATION.md +112 -0
  12. package/docs/PROJECT_STRUCTURE.md +396 -0
  13. package/docs/bannel.png +0 -0
  14. package/docs/home.png +0 -0
  15. package/docs/logo.png +0 -0
  16. package/docs/multi-channel-load-balancing.md +249 -0
  17. package/package.json +73 -0
  18. package/src/commands/channels.js +504 -0
  19. package/src/commands/cli-type.js +99 -0
  20. package/src/commands/daemon.js +286 -0
  21. package/src/commands/doctor.js +332 -0
  22. package/src/commands/list.js +222 -0
  23. package/src/commands/logs.js +259 -0
  24. package/src/commands/port-config.js +115 -0
  25. package/src/commands/proxy-control.js +258 -0
  26. package/src/commands/proxy.js +152 -0
  27. package/src/commands/resume.js +137 -0
  28. package/src/commands/search.js +190 -0
  29. package/src/commands/stats.js +224 -0
  30. package/src/commands/switch.js +48 -0
  31. package/src/commands/toggle-proxy.js +222 -0
  32. package/src/commands/ui.js +92 -0
  33. package/src/commands/workspace.js +454 -0
  34. package/src/config/default.js +40 -0
  35. package/src/config/loader.js +75 -0
  36. package/src/config/paths.js +121 -0
  37. package/src/index.js +373 -0
  38. package/src/reset-config.js +92 -0
  39. package/src/server/api/agents.js +248 -0
  40. package/src/server/api/aliases.js +36 -0
  41. package/src/server/api/channels.js +258 -0
  42. package/src/server/api/claude-hooks.js +480 -0
  43. package/src/server/api/codex-channels.js +312 -0
  44. package/src/server/api/codex-projects.js +91 -0
  45. package/src/server/api/codex-proxy.js +182 -0
  46. package/src/server/api/codex-sessions.js +491 -0
  47. package/src/server/api/codex-statistics.js +57 -0
  48. package/src/server/api/commands.js +245 -0
  49. package/src/server/api/config-templates.js +182 -0
  50. package/src/server/api/config.js +147 -0
  51. package/src/server/api/convert.js +127 -0
  52. package/src/server/api/dashboard.js +125 -0
  53. package/src/server/api/env.js +144 -0
  54. package/src/server/api/favorites.js +77 -0
  55. package/src/server/api/gemini-channels.js +261 -0
  56. package/src/server/api/gemini-projects.js +91 -0
  57. package/src/server/api/gemini-proxy.js +160 -0
  58. package/src/server/api/gemini-sessions.js +397 -0
  59. package/src/server/api/gemini-statistics.js +57 -0
  60. package/src/server/api/health-check.js +118 -0
  61. package/src/server/api/mcp.js +336 -0
  62. package/src/server/api/pm2-autostart.js +269 -0
  63. package/src/server/api/projects.js +124 -0
  64. package/src/server/api/prompts.js +279 -0
  65. package/src/server/api/proxy.js +235 -0
  66. package/src/server/api/rules.js +271 -0
  67. package/src/server/api/sessions.js +595 -0
  68. package/src/server/api/settings.js +61 -0
  69. package/src/server/api/skills.js +305 -0
  70. package/src/server/api/statistics.js +91 -0
  71. package/src/server/api/terminal.js +202 -0
  72. package/src/server/api/ui-config.js +64 -0
  73. package/src/server/api/workspaces.js +407 -0
  74. package/src/server/codex-proxy-server.js +538 -0
  75. package/src/server/dev-server.js +26 -0
  76. package/src/server/gemini-proxy-server.js +518 -0
  77. package/src/server/index.js +305 -0
  78. package/src/server/proxy-server.js +469 -0
  79. package/src/server/services/agents-service.js +354 -0
  80. package/src/server/services/alias.js +71 -0
  81. package/src/server/services/channel-health.js +234 -0
  82. package/src/server/services/channel-scheduler.js +234 -0
  83. package/src/server/services/channels.js +347 -0
  84. package/src/server/services/codex-channels.js +625 -0
  85. package/src/server/services/codex-config.js +90 -0
  86. package/src/server/services/codex-parser.js +322 -0
  87. package/src/server/services/codex-sessions.js +665 -0
  88. package/src/server/services/codex-settings-manager.js +397 -0
  89. package/src/server/services/codex-speed-test-template.json +24 -0
  90. package/src/server/services/codex-statistics-service.js +255 -0
  91. package/src/server/services/commands-service.js +360 -0
  92. package/src/server/services/config-templates-service.js +732 -0
  93. package/src/server/services/env-checker.js +307 -0
  94. package/src/server/services/env-manager.js +300 -0
  95. package/src/server/services/favorites.js +163 -0
  96. package/src/server/services/gemini-channels.js +333 -0
  97. package/src/server/services/gemini-config.js +73 -0
  98. package/src/server/services/gemini-sessions.js +689 -0
  99. package/src/server/services/gemini-settings-manager.js +263 -0
  100. package/src/server/services/gemini-statistics-service.js +253 -0
  101. package/src/server/services/health-check.js +399 -0
  102. package/src/server/services/mcp-service.js +1188 -0
  103. package/src/server/services/prompts-service.js +492 -0
  104. package/src/server/services/proxy-runtime.js +79 -0
  105. package/src/server/services/pty-manager.js +435 -0
  106. package/src/server/services/rules-service.js +401 -0
  107. package/src/server/services/session-cache.js +127 -0
  108. package/src/server/services/session-converter.js +577 -0
  109. package/src/server/services/sessions.js +757 -0
  110. package/src/server/services/settings-manager.js +163 -0
  111. package/src/server/services/skill-service.js +965 -0
  112. package/src/server/services/speed-test.js +545 -0
  113. package/src/server/services/statistics-service.js +386 -0
  114. package/src/server/services/terminal-commands.js +155 -0
  115. package/src/server/services/terminal-config.js +140 -0
  116. package/src/server/services/terminal-detector.js +306 -0
  117. package/src/server/services/ui-config.js +130 -0
  118. package/src/server/services/workspace-service.js +662 -0
  119. package/src/server/utils/pricing.js +41 -0
  120. package/src/server/websocket-server.js +557 -0
  121. package/src/ui/menu.js +129 -0
  122. package/src/ui/prompts.js +100 -0
  123. package/src/utils/format.js +43 -0
  124. package/src/utils/port-helper.js +94 -0
  125. package/src/utils/session.js +239 -0
@@ -0,0 +1,662 @@
1
+ // 多项目工作区服务
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { execSync } = require('child_process');
5
+ const { PATHS } = require('../../config/paths');
6
+ const configTemplatesService = require('./config-templates-service');
7
+
8
+ // 工作区配置文件路径
9
+ const WORKSPACES_CONFIG = path.join(PATHS.base, 'workspaces.json');
10
+
11
+ /**
12
+ * 生成唯一工作区 ID
13
+ */
14
+ function generateWorkspaceId() {
15
+ return `workspace_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
16
+ }
17
+
18
+ /**
19
+ * 加载工作区配置
20
+ */
21
+ function loadWorkspaces() {
22
+ try {
23
+ if (fs.existsSync(WORKSPACES_CONFIG)) {
24
+ const content = fs.readFileSync(WORKSPACES_CONFIG, 'utf8');
25
+ // 处理空文件或无效内容
26
+ if (!content || content.trim() === '') {
27
+ return { workspaces: [] };
28
+ }
29
+ return JSON.parse(content);
30
+ }
31
+ } catch (error) {
32
+ console.error('加载工作区配置失败:', error.message);
33
+ }
34
+ return { workspaces: [] };
35
+ }
36
+
37
+ /**
38
+ * 保存工作区配置
39
+ */
40
+ function saveWorkspaces(data) {
41
+ try {
42
+ const dir = path.dirname(WORKSPACES_CONFIG);
43
+ if (!fs.existsSync(dir)) {
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ }
46
+ fs.writeFileSync(WORKSPACES_CONFIG, JSON.stringify(data, null, 2), 'utf8');
47
+ return true;
48
+ } catch (error) {
49
+ console.error('保存工作区配置失败:', error.message);
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 获取所有工作区列表
56
+ */
57
+ function listWorkspaces() {
58
+ const data = loadWorkspaces();
59
+ return data.workspaces.map(ws => ({
60
+ ...ws,
61
+ exists: fs.existsSync(ws.path),
62
+ projectCount: ws.projects.length
63
+ }));
64
+ }
65
+
66
+ /**
67
+ * 获取单个工作区详情
68
+ */
69
+ function getWorkspace(id) {
70
+ const data = loadWorkspaces();
71
+ const workspace = data.workspaces.find(ws => ws.id === id);
72
+ if (!workspace) {
73
+ return null;
74
+ }
75
+
76
+ // 检查工作区目录是否存在
77
+ workspace.exists = fs.existsSync(workspace.path);
78
+
79
+ // 检查每个项目链接是否有效
80
+ workspace.projects = workspace.projects.map(proj => ({
81
+ ...proj,
82
+ linkExists: fs.existsSync(path.join(workspace.path, proj.name)),
83
+ sourceExists: fs.existsSync(proj.sourcePath)
84
+ }));
85
+
86
+ return workspace;
87
+ }
88
+
89
+ /**
90
+ * 检查目录是否是 git 仓库
91
+ */
92
+ function isGitRepo(dirPath) {
93
+ try {
94
+ if (!fs.existsSync(dirPath)) {
95
+ return false;
96
+ }
97
+ const gitDir = path.join(dirPath, '.git');
98
+ return fs.existsSync(gitDir);
99
+ } catch (error) {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * 获取 git 仓库的所有 worktree
106
+ */
107
+ function getGitWorktrees(repoPath) {
108
+ try {
109
+ if (!isGitRepo(repoPath)) {
110
+ return [];
111
+ }
112
+ const output = execSync('git worktree list --porcelain', {
113
+ cwd: repoPath,
114
+ encoding: 'utf8'
115
+ });
116
+
117
+ const worktrees = [];
118
+ const lines = output.split('\n');
119
+ let current = {};
120
+
121
+ for (const line of lines) {
122
+ if (line.startsWith('worktree ')) {
123
+ if (current.path) {
124
+ worktrees.push(current);
125
+ }
126
+ current = { path: line.substring(9) };
127
+ } else if (line.startsWith('branch ')) {
128
+ current.branch = line.substring(7);
129
+ } else if (line.startsWith('HEAD ')) {
130
+ current.head = line.substring(5);
131
+ }
132
+ }
133
+
134
+ if (current.path) {
135
+ worktrees.push(current);
136
+ }
137
+
138
+ return worktrees.filter(wt => wt.path !== repoPath); // 排除主仓库
139
+ } catch (error) {
140
+ console.error('获取 worktree 列表失败:', error.message);
141
+ return [];
142
+ }
143
+ }
144
+
145
+ /**
146
+ * 创建多项目工作区
147
+ * @param {Object} options - 创建选项
148
+ * @param {string} options.name - 工作区名称
149
+ * @param {string} options.description - 工作区描述
150
+ * @param {string} options.baseDir - 基础目录(可选)
151
+ * @param {Array} options.projects - 项目列表 [{sourcePath, name, createWorktree, branch}]
152
+ */
153
+ function createWorkspace(options) {
154
+ const { name, description = '', baseDir, projects = [], configTemplateId } = options;
155
+
156
+ if (!name || name.trim() === '') {
157
+ throw new Error('工作区名称不能为空');
158
+ }
159
+
160
+ if (projects.length === 0) {
161
+ throw new Error('至少需要选择一个项目');
162
+ }
163
+
164
+ // 确定工作区路径
165
+ let workspacePath;
166
+ if (baseDir && baseDir.trim() !== '') {
167
+ workspacePath = path.resolve(baseDir, name);
168
+ } else {
169
+ // 默认:第一个项目的父目录
170
+ const firstProject = projects[0];
171
+ const firstProjectParent = path.dirname(firstProject.sourcePath);
172
+ workspacePath = path.join(firstProjectParent, name);
173
+ }
174
+
175
+ // 检查工作区目录是否已存在
176
+ if (fs.existsSync(workspacePath)) {
177
+ throw new Error(`目录已存在: ${workspacePath}`);
178
+ }
179
+
180
+ // 创建工作区目录
181
+ fs.mkdirSync(workspacePath, { recursive: true });
182
+
183
+ const workspaceProjects = [];
184
+
185
+ try {
186
+ // 处理每个项目
187
+ for (const proj of projects) {
188
+ const { sourcePath, name: linkName, branch } = proj;
189
+ // useWorktree: 未指定时,Git 仓库默认 true,非 Git 仓库默认 false
190
+ const isGit = isGitRepo(sourcePath);
191
+ const useWorktree = proj.createWorktree !== undefined ? proj.createWorktree : isGit;
192
+
193
+ // 检查源项目是否存在
194
+ if (!fs.existsSync(sourcePath)) {
195
+ throw new Error(`源项目不存在: ${sourcePath}`);
196
+ }
197
+
198
+ // 生成链接名(默认使用源目录名)
199
+ const symlinkName = linkName || path.basename(sourcePath);
200
+ const symlinkPath = path.join(workspacePath, symlinkName);
201
+
202
+ let targetPath = sourcePath;
203
+ let worktrees = [];
204
+
205
+ // Git 仓库且需要创建 worktree
206
+ if (isGit && useWorktree) {
207
+ // 获取当前分支作为默认分支
208
+ let targetBranch = branch;
209
+ if (!targetBranch) {
210
+ try {
211
+ targetBranch = execSync('git rev-parse --abbrev-ref HEAD', {
212
+ cwd: sourcePath,
213
+ encoding: 'utf8'
214
+ }).trim();
215
+ } catch (e) {
216
+ targetBranch = 'main';
217
+ }
218
+ }
219
+
220
+ // worktree 路径:源目录同级,名称为 "项目名-workspace-分支名"
221
+ const worktreePath = path.join(
222
+ path.dirname(sourcePath),
223
+ `${path.basename(sourcePath)}-ws-${targetBranch.replace(/\//g, '-')}`
224
+ );
225
+
226
+ // 检查 worktree 是否已存在
227
+ if (fs.existsSync(worktreePath)) {
228
+ // 已存在则直接使用
229
+ targetPath = worktreePath;
230
+ worktrees.push({
231
+ branch: targetBranch,
232
+ path: worktreePath
233
+ });
234
+ } else {
235
+ try {
236
+ // 尝试检出已有分支
237
+ execSync(`git worktree add "${worktreePath}" "${targetBranch}"`, {
238
+ cwd: sourcePath,
239
+ stdio: 'pipe'
240
+ });
241
+
242
+ targetPath = worktreePath;
243
+ worktrees.push({
244
+ branch: targetBranch,
245
+ path: worktreePath
246
+ });
247
+ } catch (error) {
248
+ // 如果分支不存在,尝试创建新分支
249
+ try {
250
+ execSync(`git worktree add "${worktreePath}" -b "${targetBranch}"`, {
251
+ cwd: sourcePath,
252
+ stdio: 'pipe'
253
+ });
254
+ targetPath = worktreePath;
255
+ worktrees.push({
256
+ branch: targetBranch,
257
+ path: worktreePath
258
+ });
259
+ } catch (err) {
260
+ // worktree 创建失败,回退到软链接模式
261
+ console.warn(`创建 worktree 失败,使用软链接: ${err.message}`);
262
+ targetPath = sourcePath;
263
+ worktrees = getGitWorktrees(sourcePath);
264
+ }
265
+ }
266
+ }
267
+ } else if (isGit) {
268
+ // Git 仓库但不创建 worktree,获取已有的 worktrees 信息
269
+ worktrees = getGitWorktrees(sourcePath);
270
+ }
271
+ // 非 Git 仓库:targetPath 保持为 sourcePath,直接软链接
272
+
273
+ // 创建软链接
274
+ fs.symlinkSync(targetPath, symlinkPath, 'dir');
275
+
276
+ workspaceProjects.push({
277
+ name: symlinkName,
278
+ sourcePath,
279
+ targetPath,
280
+ isGitRepo: isGit,
281
+ useWorktree: isGit && useWorktree,
282
+ worktrees
283
+ });
284
+ }
285
+
286
+ // 应用配置模板(如果指定)
287
+ let templateInfo = null;
288
+ if (configTemplateId) {
289
+ try {
290
+ const result = configTemplatesService.applyTemplate(workspacePath, configTemplateId);
291
+ templateInfo = {
292
+ templateId: configTemplateId,
293
+ templateName: result.template,
294
+ appliedAt: new Date().toISOString()
295
+ };
296
+ } catch (templateError) {
297
+ console.warn('应用配置模板失败:', templateError.message);
298
+ // 不中断工作区创建流程
299
+ }
300
+ }
301
+
302
+ // 保存工作区配置
303
+ const workspaceId = generateWorkspaceId();
304
+ const workspace = {
305
+ id: workspaceId,
306
+ name,
307
+ description,
308
+ path: workspacePath,
309
+ projects: workspaceProjects,
310
+ configTemplate: templateInfo,
311
+ createdAt: new Date().toISOString(),
312
+ lastUsed: new Date().toISOString()
313
+ };
314
+
315
+ const data = loadWorkspaces();
316
+ data.workspaces.push(workspace);
317
+ saveWorkspaces(data);
318
+
319
+ return workspace;
320
+ } catch (error) {
321
+ // 清理已创建的目录
322
+ try {
323
+ if (fs.existsSync(workspacePath)) {
324
+ fs.rmSync(workspacePath, { recursive: true, force: true });
325
+ }
326
+ } catch (cleanupError) {
327
+ console.error('清理失败:', cleanupError.message);
328
+ }
329
+ throw error;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * 删除工作区
335
+ * @param {string} id - 工作区 ID
336
+ * @param {boolean} removeFiles - 是否删除物理文件
337
+ */
338
+ function deleteWorkspace(id, removeFiles = false) {
339
+ const data = loadWorkspaces();
340
+ const index = data.workspaces.findIndex(ws => ws.id === id);
341
+
342
+ if (index === -1) {
343
+ throw new Error('工作区不存在');
344
+ }
345
+
346
+ const workspace = data.workspaces[index];
347
+
348
+ // 如果需要删除物理文件
349
+ if (removeFiles && fs.existsSync(workspace.path)) {
350
+ try {
351
+ // 清理 worktrees
352
+ for (const proj of workspace.projects) {
353
+ if (proj.worktrees && proj.worktrees.length > 0) {
354
+ for (const wt of proj.worktrees) {
355
+ if (fs.existsSync(wt.path)) {
356
+ try {
357
+ execSync(`git worktree remove "${wt.path}" --force`, {
358
+ cwd: proj.sourcePath,
359
+ stdio: 'pipe'
360
+ });
361
+ } catch (error) {
362
+ console.error(`删除 worktree 失败: ${wt.path}`, error.message);
363
+ }
364
+ }
365
+ }
366
+ }
367
+ }
368
+
369
+ // 删除工作区目录
370
+ fs.rmSync(workspace.path, { recursive: true, force: true });
371
+ } catch (error) {
372
+ throw new Error(`删除工作区文件失败: ${error.message}`);
373
+ }
374
+ }
375
+
376
+ // 从配置中移除
377
+ data.workspaces.splice(index, 1);
378
+ saveWorkspaces(data);
379
+
380
+ return true;
381
+ }
382
+
383
+ /**
384
+ * 更新工作区最后使用时间
385
+ */
386
+ function updateWorkspaceLastUsed(id) {
387
+ const data = loadWorkspaces();
388
+ const workspace = data.workspaces.find(ws => ws.id === id);
389
+
390
+ if (workspace) {
391
+ workspace.lastUsed = new Date().toISOString();
392
+ saveWorkspaces(data);
393
+ }
394
+ }
395
+
396
+ /**
397
+ * 为工作区添加项目
398
+ */
399
+ function addProjectToWorkspace(workspaceId, projectConfig) {
400
+ const data = loadWorkspaces();
401
+ const workspace = data.workspaces.find(ws => ws.id === workspaceId);
402
+
403
+ if (!workspace) {
404
+ throw new Error('工作区不存在');
405
+ }
406
+
407
+ const { sourcePath, name: linkName, branch } = projectConfig;
408
+ // useWorktree: 未指定时,Git 仓库默认 true,非 Git 仓库默认 false
409
+ const isGit = isGitRepo(sourcePath);
410
+ const useWorktree = projectConfig.createWorktree !== undefined ? projectConfig.createWorktree : isGit;
411
+
412
+ if (!fs.existsSync(sourcePath)) {
413
+ throw new Error(`源项目不存在: ${sourcePath}`);
414
+ }
415
+
416
+ const symlinkName = linkName || path.basename(sourcePath);
417
+ const symlinkPath = path.join(workspace.path, symlinkName);
418
+
419
+ // 检查链接是否已存在
420
+ if (fs.existsSync(symlinkPath)) {
421
+ throw new Error(`项目已存在: ${symlinkName}`);
422
+ }
423
+
424
+ let targetPath = sourcePath;
425
+ let worktrees = [];
426
+
427
+ // Git 仓库且需要创建 worktree
428
+ if (isGit && useWorktree) {
429
+ // 获取当前分支作为默认分支
430
+ let targetBranch = branch;
431
+ if (!targetBranch) {
432
+ try {
433
+ targetBranch = execSync('git rev-parse --abbrev-ref HEAD', {
434
+ cwd: sourcePath,
435
+ encoding: 'utf8'
436
+ }).trim();
437
+ } catch (e) {
438
+ targetBranch = 'main';
439
+ }
440
+ }
441
+
442
+ const worktreePath = path.join(
443
+ path.dirname(sourcePath),
444
+ `${path.basename(sourcePath)}-ws-${targetBranch.replace(/\//g, '-')}`
445
+ );
446
+
447
+ // 检查 worktree 是否已存在
448
+ if (fs.existsSync(worktreePath)) {
449
+ targetPath = worktreePath;
450
+ worktrees.push({ branch: targetBranch, path: worktreePath });
451
+ } else {
452
+ try {
453
+ execSync(`git worktree add "${worktreePath}" "${targetBranch}"`, {
454
+ cwd: sourcePath,
455
+ stdio: 'pipe'
456
+ });
457
+ targetPath = worktreePath;
458
+ worktrees.push({ branch: targetBranch, path: worktreePath });
459
+ } catch (error) {
460
+ try {
461
+ execSync(`git worktree add "${worktreePath}" -b "${targetBranch}"`, {
462
+ cwd: sourcePath,
463
+ stdio: 'pipe'
464
+ });
465
+ targetPath = worktreePath;
466
+ worktrees.push({ branch: targetBranch, path: worktreePath });
467
+ } catch (err) {
468
+ console.warn(`创建 worktree 失败,使用软链接: ${err.message}`);
469
+ targetPath = sourcePath;
470
+ worktrees = getGitWorktrees(sourcePath);
471
+ }
472
+ }
473
+ }
474
+ } else if (isGit) {
475
+ worktrees = getGitWorktrees(sourcePath);
476
+ }
477
+
478
+ // 创建软链接
479
+ fs.symlinkSync(targetPath, symlinkPath, 'dir');
480
+
481
+ // 更新配置
482
+ workspace.projects.push({
483
+ name: symlinkName,
484
+ sourcePath,
485
+ targetPath,
486
+ isGitRepo: isGit,
487
+ useWorktree: isGit && useWorktree,
488
+ worktrees
489
+ });
490
+
491
+ saveWorkspaces(data);
492
+ return workspace;
493
+ }
494
+
495
+ /**
496
+ * 从工作区移除项目
497
+ */
498
+ function removeProjectFromWorkspace(workspaceId, projectName, removeWorktrees = false) {
499
+ const data = loadWorkspaces();
500
+ const workspace = data.workspaces.find(ws => ws.id === workspaceId);
501
+
502
+ if (!workspace) {
503
+ throw new Error('工作区不存在');
504
+ }
505
+
506
+ const projectIndex = workspace.projects.findIndex(p => p.name === projectName);
507
+
508
+ if (projectIndex === -1) {
509
+ throw new Error('项目不存在');
510
+ }
511
+
512
+ const project = workspace.projects[projectIndex];
513
+ const symlinkPath = path.join(workspace.path, projectName);
514
+
515
+ // 删除软链接
516
+ if (fs.existsSync(symlinkPath)) {
517
+ fs.unlinkSync(symlinkPath);
518
+ }
519
+
520
+ // 清理 worktrees
521
+ if (removeWorktrees && project.worktrees && project.worktrees.length > 0) {
522
+ for (const wt of project.worktrees) {
523
+ if (fs.existsSync(wt.path)) {
524
+ try {
525
+ execSync(`git worktree remove "${wt.path}" --force`, {
526
+ cwd: project.sourcePath,
527
+ stdio: 'pipe'
528
+ });
529
+ } catch (error) {
530
+ console.error(`删除 worktree 失败: ${wt.path}`, error.message);
531
+ }
532
+ }
533
+ }
534
+ }
535
+
536
+ // 从配置中移除
537
+ workspace.projects.splice(projectIndex, 1);
538
+ saveWorkspaces(data);
539
+
540
+ return workspace;
541
+ }
542
+
543
+
544
+ /**
545
+ * 获取所有渠道(Claude/Codex/Gemini)的项目并集
546
+ * @returns {Array} 去重后的项目列表
547
+ */
548
+ function getAllAvailableProjects() {
549
+ const { NATIVE_PATHS } = require('../../config/paths');
550
+ const sessionsService = require('./sessions');
551
+
552
+ const allProjects = [];
553
+ const seenPaths = new Set();
554
+
555
+ // 定义渠道配置
556
+ const channels = [
557
+ { name: 'claude', projectsDir: NATIVE_PATHS.claude.projects },
558
+ { name: 'codex', projectsDir: NATIVE_PATHS.codex.sessions },
559
+ { name: 'gemini', projectsDir: NATIVE_PATHS.gemini.tmp }
560
+ ];
561
+
562
+ for (const channel of channels) {
563
+ try {
564
+ if (!fs.existsSync(channel.projectsDir)) {
565
+ continue;
566
+ }
567
+
568
+ const config = { projectsDir: channel.projectsDir };
569
+ const projects = sessionsService.getProjectsWithStats(config, { force: true });
570
+
571
+ for (const proj of projects) {
572
+ // 使用 fullPath 去重
573
+ const projectPath = proj.fullPath;
574
+ if (projectPath && !seenPaths.has(projectPath)) {
575
+ seenPaths.add(projectPath);
576
+ allProjects.push({
577
+ name: proj.name,
578
+ displayName: proj.displayName,
579
+ fullPath: projectPath,
580
+ channel: channel.name,
581
+ sessionCount: proj.sessionCount || 0,
582
+ lastUsed: proj.lastUsed,
583
+ isGitRepo: isGitRepo(projectPath)
584
+ });
585
+ }
586
+ }
587
+ } catch (error) {
588
+ console.error(`获取 ${channel.name} 项目失败:`, error.message);
589
+ }
590
+ }
591
+
592
+ // 按最后使用时间排序
593
+ allProjects.sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0));
594
+
595
+ return allProjects;
596
+ }
597
+
598
+ /**
599
+ * 在工作区中启动 CLI 工具
600
+ * @param {string} workspaceId 工作区 ID
601
+ * @param {string} tool 工具名称 (claude/codex/gemini)
602
+ * @param {string} projectName 可选,工作区内的项目名
603
+ * @returns {object} 启动信息
604
+ */
605
+ function getLaunchCommand(workspaceId, tool, projectName = null) {
606
+ const workspace = getWorkspace(workspaceId);
607
+ if (!workspace) {
608
+ throw new Error('工作区不存在');
609
+ }
610
+
611
+ if (!workspace.exists) {
612
+ throw new Error('工作区目录不存在');
613
+ }
614
+
615
+ // 确定工作目录
616
+ let workDir = workspace.path;
617
+ if (projectName) {
618
+ const project = workspace.projects.find(p => p.name === projectName);
619
+ if (!project) {
620
+ throw new Error(`项目不存在: ${projectName}`);
621
+ }
622
+ workDir = path.join(workspace.path, projectName);
623
+ if (!fs.existsSync(workDir)) {
624
+ throw new Error(`项目目录不存在: ${workDir}`);
625
+ }
626
+ }
627
+
628
+ // 根据工具类型生成启动命令
629
+ const commands = {
630
+ claude: 'claude',
631
+ codex: 'codex',
632
+ gemini: 'gemini'
633
+ };
634
+
635
+ const cmd = commands[tool];
636
+ if (!cmd) {
637
+ throw new Error(`不支持的工具: ${tool}`);
638
+ }
639
+
640
+ return {
641
+ command: cmd,
642
+ cwd: workDir,
643
+ workspaceName: workspace.name,
644
+ projectName: projectName
645
+ };
646
+ }
647
+
648
+ module.exports = {
649
+ loadWorkspaces,
650
+ saveWorkspaces,
651
+ listWorkspaces,
652
+ getWorkspace,
653
+ createWorkspace,
654
+ deleteWorkspace,
655
+ updateWorkspaceLastUsed,
656
+ addProjectToWorkspace,
657
+ removeProjectFromWorkspace,
658
+ isGitRepo,
659
+ getGitWorktrees,
660
+ getAllAvailableProjects,
661
+ getLaunchCommand
662
+ };
@@ -0,0 +1,41 @@
1
+ const { loadConfig } = require('../../config/loader');
2
+ const DEFAULT_CONFIG = require('../../config/default');
3
+
4
+ const RATE_KEYS = ['input', 'output', 'cacheCreation', 'cacheRead', 'cached', 'reasoning'];
5
+
6
+ function getPricingConfig(toolKey) {
7
+ try {
8
+ const config = loadConfig();
9
+ if (config.pricing && config.pricing[toolKey]) {
10
+ return config.pricing[toolKey];
11
+ }
12
+ } catch (err) {
13
+ console.error('[Pricing] Failed to load pricing config:', err);
14
+ }
15
+ return DEFAULT_CONFIG.pricing[toolKey];
16
+ }
17
+
18
+ function resolvePricing(toolKey, modelPricing = {}, defaultPricing = {}) {
19
+ const base = { ...defaultPricing, ...(modelPricing || {}) };
20
+ const pricingConfig = getPricingConfig(toolKey);
21
+
22
+ if (!pricingConfig) {
23
+ return base;
24
+ }
25
+
26
+ if (pricingConfig.mode === 'custom') {
27
+ const result = { ...base };
28
+ RATE_KEYS.forEach((key) => {
29
+ if (typeof pricingConfig[key] === 'number' && Number.isFinite(pricingConfig[key])) {
30
+ result[key] = pricingConfig[key];
31
+ }
32
+ });
33
+ return result;
34
+ }
35
+
36
+ return base;
37
+ }
38
+
39
+ module.exports = {
40
+ resolvePricing
41
+ };