coding-tool-x 3.3.9 → 3.4.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.
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>CC-TOOL - ClaudeCode增强工作助手</title>
8
- <script type="module" crossorigin src="/assets/index-CL-qpoJ_.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-D_WItvHE.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-DyTJGI4N.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-3bf-fPGP.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendors-CKPV1OAU.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-Bdxp09n2.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/icons-B5Pl4lrD.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-D_5dRFOL.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-Dz7v9OM0.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.3.9",
3
+ "version": "3.4.0",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -151,8 +151,8 @@ router.get('/trend', async (req, res) => {
151
151
  return res.status(400).json({ error: 'Hour granularity is limited to 7 days' });
152
152
  }
153
153
 
154
- if (diffDays > 90) {
155
- return res.status(400).json({ error: 'Date range cannot exceed 90 days' });
154
+ if (diffDays > 365) {
155
+ return res.status(400).json({ error: 'Date range cannot exceed 365 days' });
156
156
  }
157
157
 
158
158
  const filters = {
@@ -204,8 +204,8 @@ router.get('/trend/export', async (req, res) => {
204
204
  return res.status(400).json({ error: 'Hour granularity is limited to 7 days' });
205
205
  }
206
206
 
207
- if (diffDays > 90) {
208
- return res.status(400).json({ error: 'Date range cannot exceed 90 days' });
207
+ if (diffDays > 365) {
208
+ return res.status(400).json({ error: 'Date range cannot exceed 365 days' });
209
209
  }
210
210
 
211
211
  const result = await getTrendStatistics({ startDate, endDate, granularity, step, groupBy, metric });
@@ -402,12 +402,10 @@ router.post('/:id/projects', (req, res) => {
402
402
  router.delete('/:id/projects/:projectName', (req, res) => {
403
403
  try {
404
404
  const { id, projectName } = req.params;
405
- const removeWorktrees = req.query.removeWorktrees === 'true';
406
405
 
407
406
  const workspace = workspaceService.removeProjectFromWorkspace(
408
407
  id,
409
- projectName,
410
- removeWorktrees
408
+ projectName
411
409
  );
412
410
 
413
411
  res.json({
@@ -34,18 +34,15 @@ function normalizeGatewaySourceType(value, fallback = 'codex') {
34
34
  }
35
35
 
36
36
  function buildManagedCodexEnvMap(channels = [], { includeProxyKey = false } = {}) {
37
- const envMap = {};
38
-
39
- for (const channel of channels) {
40
- if (!channel?.envKey || !channel?.apiKey) continue;
41
- envMap[channel.envKey] = channel.apiKey;
42
- }
43
-
37
+ // 代理模式:只写代理 Key,不暴露真实 API Key
44
38
  if (includeProxyKey) {
45
- envMap[CODEX_PROXY_ENV_KEY] = CODEX_PROXY_ENV_VALUE;
39
+ return { [CODEX_PROXY_ENV_KEY]: CODEX_PROXY_ENV_VALUE };
46
40
  }
47
41
 
48
- return envMap;
42
+ // 直连模式:只写当前激活渠道的 Key,环境中有且只有一条记录
43
+ const activeChannel = channels.find(ch => ch.enabled && ch.envKey && ch.apiKey);
44
+ if (!activeChannel) return {};
45
+ return { [activeChannel.envKey]: activeChannel.apiKey };
49
46
  }
50
47
 
51
48
  // 获取渠道存储文件路径
@@ -181,81 +181,51 @@ function syncPosixEnvironment(nextValues, previousState, options) {
181
181
  const {
182
182
  runtime,
183
183
  homeDir,
184
- envFilePath,
185
184
  stateFilePath,
186
185
  shellEnv,
187
186
  execSync
188
187
  } = options;
189
- const { preferred, candidates } = getPosixProfileCandidates(homeDir, shellEnv);
190
188
  const nextKeys = Object.keys(nextValues).sort();
191
- const previousProfiles = Array.isArray(previousState.profiles) ? previousState.profiles : [];
192
- const managedProfiles = new Set(previousProfiles);
193
- const sourceSnippet = buildSourceSnippet(envFilePath, homeDir);
194
189
  let changed = false;
195
190
 
196
- if (nextKeys.length > 0) {
197
- const envContent = [
198
- '# Managed by Coding-Tool for Codex env_key providers',
199
- ...nextKeys.map(key => `export ${key}=${shellQuote(nextValues[key])}`),
200
- ''
201
- ].join('\n');
202
- changed = writeTextFileIfChanged(envFilePath, envContent) || changed;
203
-
204
- const existingProfiles = candidates.filter(filePath => fs.existsSync(filePath));
205
- if (managedProfiles.size === 0) {
206
- if (existingProfiles.length > 0) {
207
- for (const profilePath of existingProfiles) {
208
- managedProfiles.add(profilePath);
209
- }
210
- } else {
211
- managedProfiles.add(preferred);
212
- }
213
- }
214
-
215
- for (const profilePath of managedProfiles) {
216
- const currentContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
217
- const nextContent = upsertManagedBlock(currentContent, sourceSnippet);
218
- changed = writeTextFileIfChanged(profilePath, nextContent) || changed;
219
- }
220
- } else {
221
- if (fs.existsSync(envFilePath)) {
222
- fs.unlinkSync(envFilePath);
223
- changed = true;
224
- }
225
-
191
+ // 清理旧版本遗留的 shell profile 注入(迁移兼容)
192
+ const previousProfiles = Array.isArray(previousState.profiles) ? previousState.profiles : [];
193
+ if (previousProfiles.length > 0) {
194
+ const { candidates } = getPosixProfileCandidates(homeDir, shellEnv);
226
195
  const cleanupTargets = new Set([
227
196
  ...previousProfiles,
228
197
  ...candidates.filter(filePath => fs.existsSync(filePath))
229
198
  ]);
230
-
231
199
  for (const profilePath of cleanupTargets) {
232
200
  if (!fs.existsSync(profilePath)) continue;
233
201
  const currentContent = fs.readFileSync(profilePath, 'utf8');
202
+ if (!currentContent.includes(PROFILE_MARKER_START)) continue;
234
203
  const nextContent = stripManagedBlock(currentContent);
235
204
  const finalContent = nextContent ? `${nextContent}\n` : '';
236
205
  changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
237
206
  }
238
- managedProfiles.clear();
239
- }
240
-
241
- for (const key of Object.keys(previousState.values || {})) {
242
- if (!Object.prototype.hasOwnProperty.call(nextValues, key)) {
243
- delete process.env[key];
244
- }
245
- }
246
- for (const [key, value] of Object.entries(nextValues)) {
247
- process.env[key] = value;
248
207
  }
249
208
 
209
+ // macOS:用 launchctl 写入全局环境变量,新开终端/进程即生效
250
210
  if (runtime === 'darwin') {
251
211
  applyLaunchctlEnvironment(previousState.values || {}, nextValues, execSync);
212
+ const prevValues = previousState.values || {};
213
+ const keysChanged =
214
+ Object.keys(nextValues).length !== Object.keys(prevValues).length ||
215
+ Object.entries(nextValues).some(([k, v]) => prevValues[k] !== v);
216
+ changed = changed || keysChanged;
217
+ }
218
+
219
+ // Linux:写 ~/.config/environment.d/(桌面/systemd)+ ~/.profile(SSH/登录shell)
220
+ if (runtime === 'linux') {
221
+ changed = applyLinuxEnvironment(nextValues, homeDir) || changed;
252
222
  }
253
223
 
254
224
  if (nextKeys.length > 0) {
255
225
  writeJsonFile(stateFilePath, {
256
226
  version: 1,
257
227
  values: nextValues,
258
- profiles: Array.from(managedProfiles)
228
+ profiles: []
259
229
  });
260
230
  } else if (fs.existsSync(stateFilePath)) {
261
231
  fs.unlinkSync(stateFilePath);
@@ -265,14 +235,54 @@ function syncPosixEnvironment(nextValues, previousState, options) {
265
235
  changed,
266
236
  reloadRequired: changed,
267
237
  isFirstTime: Object.keys(previousState.values || {}).length === 0 && nextKeys.length > 0,
268
- sourceCommand: nextKeys.length > 0 ? `source "${buildHomeRelativeShellPath(envFilePath, homeDir)}"` : null,
269
- shellConfigPath: managedProfiles.size > 0 ? Array.from(managedProfiles)[0] : null,
270
- shellConfigPaths: Array.from(managedProfiles),
271
- envFilePath: nextKeys.length > 0 ? envFilePath : null,
238
+ sourceCommand: null,
239
+ shellConfigPath: null,
240
+ shellConfigPaths: [],
241
+ envFilePath: null,
272
242
  managedKeys: nextKeys
273
243
  };
274
244
  }
275
245
 
246
+ function applyLinuxEnvironment(nextValues, homeDir) {
247
+ let changed = false;
248
+
249
+ // 1. ~/.config/environment.d/codex-env.conf(systemd 用户环境,桌面终端生效)
250
+ const envdDir = path.join(homeDir, '.config', 'environment.d');
251
+ const envdFile = path.join(envdDir, 'codex-env.conf');
252
+ const nextKeys = Object.keys(nextValues).sort();
253
+
254
+ if (nextKeys.length > 0) {
255
+ const envdContent = [
256
+ '# Managed by Coding-Tool',
257
+ ...nextKeys.map(key => `${key}=${nextValues[key]}`),
258
+ ''
259
+ ].join('\n');
260
+ changed = writeTextFileIfChanged(envdFile, envdContent) || changed;
261
+ } else if (fs.existsSync(envdFile)) {
262
+ fs.unlinkSync(envdFile);
263
+ changed = true;
264
+ }
265
+
266
+ // 2. ~/.profile(登录 shell,SSH 和新终端均生效)
267
+ const profilePath = path.join(homeDir, '.profile');
268
+ if (nextKeys.length > 0) {
269
+ const exportLines = nextKeys.map(key => `export ${key}=${shellQuote(nextValues[key])}`).join('\n');
270
+ const snippet = [PROFILE_MARKER_START, exportLines, PROFILE_MARKER_END].join('\n');
271
+ const currentContent = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
272
+ const nextContent = upsertManagedBlock(currentContent, snippet);
273
+ changed = writeTextFileIfChanged(profilePath, nextContent) || changed;
274
+ } else if (fs.existsSync(profilePath)) {
275
+ const currentContent = fs.readFileSync(profilePath, 'utf8');
276
+ if (currentContent.includes(PROFILE_MARKER_START)) {
277
+ const nextContent = stripManagedBlock(currentContent);
278
+ const finalContent = nextContent ? `${nextContent}\n` : '';
279
+ changed = writeTextFileIfChanged(profilePath, finalContent) || changed;
280
+ }
281
+ }
282
+
283
+ return changed;
284
+ }
285
+
276
286
  function applyLaunchctlEnvironment(previousValues, nextValues, execSync) {
277
287
  const previousKeys = new Set(Object.keys(previousValues || {}));
278
288
  for (const [key, value] of Object.entries(nextValues || {})) {
@@ -306,21 +316,14 @@ function syncWindowsEnvironment(nextValues, previousState, options) {
306
316
  let changed = false;
307
317
 
308
318
  for (const [key, value] of Object.entries(nextValues)) {
309
- if (previousValues[key] === value) {
310
- process.env[key] = value;
311
- continue;
312
- }
319
+ if (previousValues[key] === value) continue;
313
320
  setWindowsUserEnv(key, value, execSync);
314
- process.env[key] = value;
315
321
  changed = true;
316
322
  }
317
323
 
318
324
  for (const key of Object.keys(previousValues)) {
319
- if (Object.prototype.hasOwnProperty.call(nextValues, key)) {
320
- continue;
321
- }
325
+ if (Object.prototype.hasOwnProperty.call(nextValues, key)) continue;
322
326
  removeWindowsUserEnv(key, execSync);
323
- delete process.env[key];
324
327
  changed = true;
325
328
  }
326
329
 
@@ -920,9 +920,13 @@ async function getTrendStatistics({ startDate, endDate, granularity = 'day', ste
920
920
 
921
921
  if (granularity === 'day') {
922
922
  labels.push(dateStr);
923
- const byDimension = activeFilters
923
+ let byDimension = activeFilters
924
924
  ? readJsonlForDay(year, month, day, groupBy, activeFilters)
925
925
  : mergeAllToolsDailyStats(dateStr, groupBy);
926
+ if (!activeFilters && Object.keys(byDimension).length === 0) {
927
+ // Fallback: if daily stats are missing, derive from JSONL logs
928
+ byDimension = readJsonlForDay(year, month, day, groupBy);
929
+ }
926
930
 
927
931
  // Accumulate dimensions seen so far with 0 for this label position
928
932
  const labelIdx = labels.length - 1;
@@ -8,6 +8,24 @@ const configTemplatesService = require('./config-templates-service');
8
8
  // 工作区配置文件路径
9
9
  const WORKSPACES_CONFIG = PATHS.workspaces;
10
10
 
11
+ function createSymlink(target, linkPath) {
12
+ try {
13
+ fs.symlinkSync(target, linkPath, 'dir');
14
+ } catch (err) {
15
+ if (err.code === 'EPERM' && process.platform === 'win32') {
16
+ throw new Error(
17
+ `创建软链接失败:Windows 需要管理员权限或开启开发者模式。\n` +
18
+ `解决方案:\n` +
19
+ `1. 以管理员身份运行终端后重试\n` +
20
+ `2. 或开启开发者模式:设置 → 系统 → 开发者选项 → 开发者模式\n` +
21
+ `3. 对于 Git 项目,建议保持默认的 worktree 模式(无需软链接)\n` +
22
+ `原始错误: ${err.message}`
23
+ );
24
+ }
25
+ throw err;
26
+ }
27
+ }
28
+
11
29
  function runGitCommand(args, options = {}) {
12
30
  const execOptions = {
13
31
  encoding: 'utf8',
@@ -287,7 +305,7 @@ function createWorkspace(options) {
287
305
  let targetPath = sourcePath;
288
306
  let worktrees = [];
289
307
 
290
- // Git 仓库且需要创建 worktree
308
+ // Git 仓库且需要创建 worktree:直接在工作区目录内创建,无需额外 symlink
291
309
  if (isGit && useWorktree) {
292
310
  // 获取当前分支作为默认分支
293
311
  let targetBranch = branch;
@@ -299,72 +317,46 @@ function createWorkspace(options) {
299
317
  }
300
318
  }
301
319
 
302
- // worktree 路径:源目录同级,名称为 "项目名-workspace-分支名"
303
- const worktreePath = path.join(
304
- path.dirname(sourcePath),
305
- `${path.basename(sourcePath)}-ws-${targetBranch.replace(/\//g, '-')}`
306
- );
307
-
308
- // 检查 worktree 是否已存在
309
- if (fs.existsSync(worktreePath)) {
310
- // 已存在则直接使用
311
- targetPath = worktreePath;
312
- worktrees.push({
313
- branch: targetBranch,
314
- path: worktreePath
320
+ // worktree 直接创建到工作区目录内的项目路径
321
+ const worktreePath = symlinkPath;
322
+
323
+ try {
324
+ // 尝试检出已有分支
325
+ runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
326
+ cwd: sourcePath
315
327
  });
316
- } else {
328
+ } catch (error) {
329
+ // 如果分支不存在,尝试创建新分支
317
330
  try {
318
- // 尝试检出已有分支
319
- runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
331
+ const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
332
+ if (baseBranch && baseBranch.trim()) {
333
+ worktreeArgs.push(baseBranch.trim());
334
+ }
335
+ runGitCommand(worktreeArgs, {
320
336
  cwd: sourcePath
321
337
  });
322
-
323
- targetPath = worktreePath;
324
- worktrees.push({
325
- branch: targetBranch,
326
- path: worktreePath
327
- });
328
- } catch (error) {
329
- // 如果分支不存在,尝试创建新分支
330
- try {
331
- const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
332
- if (baseBranch && baseBranch.trim()) {
333
- worktreeArgs.push(baseBranch.trim());
334
- }
335
- runGitCommand(worktreeArgs, {
336
- cwd: sourcePath
337
- });
338
- targetPath = worktreePath;
339
- worktrees.push({
340
- branch: targetBranch,
341
- path: worktreePath
342
- });
343
- } catch (err) {
344
- // Check if it's a "branch already checked out" error
345
- if (err.message.includes('already checked out')) {
346
- throw new Error(
347
- `无法创建 worktree:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
348
- `错误详情: ${err.message}\n\n` +
349
- `提示:请为此项目指定不同的分支名,或禁用 worktree 模式。`
350
- );
351
- }
352
-
353
- // For other errors, provide clear message but allow fallback
354
- console.warn(`创建 worktree 失败,使用软链接: ${err.message}`);
355
- targetPath = sourcePath;
356
- worktrees = getGitWorktrees(sourcePath);
338
+ } catch (err) {
339
+ if (err.message.includes('already checked out')) {
340
+ throw new Error(
341
+ `无法创建 worktree:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
342
+ `错误详情: ${err.message}\n\n` +
343
+ `提示:请为此项目指定不同的分支名,或禁用 worktree 模式。`
344
+ );
357
345
  }
346
+ throw new Error(`创建 worktree 失败: ${err.message}`);
358
347
  }
359
348
  }
349
+
350
+ targetPath = worktreePath;
351
+ worktrees.push({ branch: targetBranch, path: worktreePath });
360
352
  } else if (isGit) {
361
- // Git 仓库但不创建 worktree,获取已有的 worktrees 信息
353
+ // Git 仓库但不创建 worktree:symlink 指向源路径,记录已有 worktrees
362
354
  worktrees = getGitWorktrees(sourcePath);
355
+ createSymlink(targetPath, symlinkPath);
356
+ } else {
357
+ // 非 Git 仓库:symlink 指向源路径
358
+ createSymlink(targetPath, symlinkPath);
363
359
  }
364
- // 非 Git 仓库:targetPath 保持为 sourcePath,直接软链接
365
-
366
- // 创建软链接
367
- fs.symlinkSync(targetPath, symlinkPath, 'dir');
368
360
 
369
361
  workspaceProjects.push({
370
362
  name: symlinkName,
@@ -438,36 +430,17 @@ function deleteWorkspace(id, removeFiles = false) {
438
430
 
439
431
  const workspace = data.workspaces[index];
440
432
 
441
- // 清理 worktrees (无论是否删除工作区目录,都应该清理 worktree)
433
+ // 注销 worktrees(worktree 目录在工作区内,git 引用需先注销)
442
434
  for (const proj of workspace.projects) {
443
- if (proj.isGitRepo && proj.sourcePath && fs.existsSync(proj.sourcePath)) {
444
- try {
445
- // 重新扫描实际的 worktrees,确保获取最新状态
446
- const actualWorktrees = getGitWorktrees(proj.sourcePath);
447
- for (const wt of actualWorktrees) {
448
- // 只删除属于这个工作区的 worktree (通过 -ws- 标识符识别)
449
- if (wt.path && wt.path.includes('-ws-')) {
450
- try {
451
- console.log(`清理 worktree: ${wt.path}`);
452
- runGitCommand(['worktree', 'remove', wt.path, '--force'], {
453
- cwd: proj.sourcePath
454
- });
455
- } catch (error) {
456
- console.error(`删除 worktree 失败: ${wt.path}`, error.message);
457
- // 如果 git worktree remove 失败,尝试手动删除目录
458
- if (fs.existsSync(wt.path)) {
459
- try {
460
- fs.rmSync(wt.path, { recursive: true, force: true });
461
- console.log(`手动删除 worktree 目录: ${wt.path}`);
462
- } catch (rmError) {
463
- console.error(`手动删除 worktree 目录失败: ${wt.path}`, rmError.message);
464
- }
465
- }
466
- }
467
- }
435
+ if (proj.useWorktree && proj.sourcePath && proj.targetPath) {
436
+ if (fs.existsSync(proj.sourcePath)) {
437
+ try {
438
+ runGitCommand(['worktree', 'remove', proj.targetPath, '--force'], {
439
+ cwd: proj.sourcePath
440
+ });
441
+ } catch (error) {
442
+ console.error(`注销 worktree 失败: ${proj.targetPath}`, error.message);
468
443
  }
469
- } catch (error) {
470
- console.error(`扫描 worktree 失败: ${proj.sourcePath}`, error.message);
471
444
  }
472
445
  }
473
446
  }
@@ -533,9 +506,8 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
533
506
  let targetPath = sourcePath;
534
507
  let worktrees = [];
535
508
 
536
- // Git 仓库且需要创建 worktree
509
+ // Git 仓库且需要创建 worktree:直接在工作区目录内创建,无需额外 symlink
537
510
  if (isGit && useWorktree) {
538
- // 获取当前分支作为默认分支
539
511
  let targetBranch = branch;
540
512
  if (!targetBranch) {
541
513
  try {
@@ -545,73 +517,44 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
545
517
  }
546
518
  }
547
519
 
548
- const worktreePath = path.join(
549
- path.dirname(sourcePath),
550
- `${path.basename(sourcePath)}-ws-${targetBranch.replace(/\//g, '-')}`
551
- );
520
+ const worktreePath = symlinkPath;
552
521
 
553
- // 检查 worktree 是否已存在
554
- if (fs.existsSync(worktreePath)) {
555
- targetPath = worktreePath;
556
- worktrees.push({ branch: targetBranch, path: worktreePath });
557
- } else {
522
+ try {
523
+ runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
524
+ cwd: sourcePath
525
+ });
526
+ } catch (error) {
558
527
  try {
559
- runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
560
- cwd: sourcePath
561
- });
562
- targetPath = worktreePath;
563
- worktrees.push({ branch: targetBranch, path: worktreePath });
564
- } catch (error) {
565
- // Check if branch is already checked out elsewhere
566
- if (error.message && error.message.includes('already checked out')) {
528
+ const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
529
+ if (baseBranch && baseBranch.trim()) {
530
+ worktreeArgs.push(baseBranch.trim());
531
+ }
532
+ runGitCommand(worktreeArgs, { cwd: sourcePath });
533
+ } catch (err) {
534
+ if (err.message && err.message.includes('already checked out')) {
567
535
  throw new Error(
568
536
  `无法添加项目:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
569
537
  `仓库路径: ${sourcePath}\n\n` +
570
- `Git worktree 不允许在同一仓库的多个工作树中检出相同的分支。\n\n` +
571
538
  `解决方案:\n` +
572
539
  `1. 指定不同的分支名\n` +
573
540
  `2. 或者禁用 worktree 模式(设置 createWorktree: false)`
574
541
  );
575
542
  }
576
-
577
- // Branch doesn't exist, try creating it
578
- try {
579
- const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
580
- if (baseBranch && baseBranch.trim()) {
581
- worktreeArgs.push(baseBranch.trim());
582
- }
583
- runGitCommand(worktreeArgs, {
584
- cwd: sourcePath
585
- });
586
- targetPath = worktreePath;
587
- worktrees.push({ branch: targetBranch, path: worktreePath });
588
- } catch (err) {
589
- // Check for "already checked out" error in create branch attempt
590
- if (err.message && err.message.includes('already checked out')) {
591
- throw new Error(
592
- `无法添加项目:分支 '${targetBranch}' 已在其他工作树中检出。\n` +
593
- `仓库路径: ${sourcePath}\n\n` +
594
- `Git worktree 不允许在同一仓库的多个工作树中检出相同的分支。\n\n` +
595
- `解决方案:\n` +
596
- `1. 指定不同的分支名\n` +
597
- `2. 或者禁用 worktree 模式(设置 createWorktree: false)`
598
- );
599
- }
600
-
601
- // Other errors: fall back to symlink mode
602
- console.warn(`创建 worktree 失败,使用软链接: ${err.message}`);
603
- targetPath = sourcePath;
604
- worktrees = getGitWorktrees(sourcePath);
605
- }
543
+ throw new Error(`创建 worktree 失败: ${err.message}`);
606
544
  }
607
545
  }
546
+
547
+ targetPath = worktreePath;
548
+ worktrees.push({ branch: targetBranch, path: worktreePath });
608
549
  } else if (isGit) {
550
+ // Git 仓库但不创建 worktree:symlink 指向源路径
609
551
  worktrees = getGitWorktrees(sourcePath);
552
+ createSymlink(targetPath, symlinkPath);
553
+ } else {
554
+ // 非 Git 仓库:symlink 指向源路径
555
+ createSymlink(targetPath, symlinkPath);
610
556
  }
611
557
 
612
- // 创建软链接
613
- fs.symlinkSync(targetPath, symlinkPath, 'dir');
614
-
615
558
  // 更新配置
616
559
  workspace.projects.push({
617
560
  name: symlinkName,
@@ -629,7 +572,7 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
629
572
  /**
630
573
  * 从工作区移除项目
631
574
  */
632
- function removeProjectFromWorkspace(workspaceId, projectName, removeWorktrees = false) {
575
+ function removeProjectFromWorkspace(workspaceId, projectName) {
633
576
  const data = loadWorkspaces();
634
577
  const workspace = data.workspaces.find(ws => ws.id === workspaceId);
635
578
 
@@ -644,26 +587,28 @@ function removeProjectFromWorkspace(workspaceId, projectName, removeWorktrees =
644
587
  }
645
588
 
646
589
  const project = workspace.projects[projectIndex];
647
- const symlinkPath = path.join(workspace.path, projectName);
590
+ const projectPath = path.join(workspace.path, projectName);
648
591
 
649
- // 删除软链接
650
- if (fs.existsSync(symlinkPath)) {
651
- fs.unlinkSync(symlinkPath);
652
- }
653
-
654
- // 清理 worktrees
655
- if (removeWorktrees && project.worktrees && project.worktrees.length > 0) {
656
- for (const wt of project.worktrees) {
657
- if (fs.existsSync(wt.path)) {
658
- try {
659
- runGitCommand(['worktree', 'remove', wt.path, '--force'], {
660
- cwd: project.sourcePath
661
- });
662
- } catch (error) {
663
- console.error(`删除 worktree 失败: ${wt.path}`, error.message);
592
+ if (project.useWorktree) {
593
+ // worktree 项目:用 git worktree remove 注销引用(目录在工作区内)
594
+ if (fs.existsSync(projectPath) && fs.existsSync(project.sourcePath)) {
595
+ try {
596
+ runGitCommand(['worktree', 'remove', projectPath, '--force'], {
597
+ cwd: project.sourcePath
598
+ });
599
+ } catch (error) {
600
+ console.error(`注销 worktree 失败: ${projectPath}`, error.message);
601
+ // git 注销失败时手动删除目录
602
+ if (fs.existsSync(projectPath)) {
603
+ fs.rmSync(projectPath, { recursive: true, force: true });
664
604
  }
665
605
  }
666
606
  }
607
+ } else {
608
+ // symlink 项目:直接删除软链接
609
+ if (fs.existsSync(projectPath)) {
610
+ fs.unlinkSync(projectPath);
611
+ }
667
612
  }
668
613
 
669
614
  // 从配置中移除
@@ -1 +0,0 @@
1
- .analytics-page[data-v-7683ab57]{height:100%;background:var(--bg-primary);overflow-y:auto;padding:16px 20px;box-sizing:border-box;display:flex;flex-direction:column;gap:14px}.analytics-header[data-v-7683ab57]{display:flex;align-items:center;justify-content:space-between;flex-shrink:0;flex-wrap:wrap;gap:10px}.page-title[data-v-7683ab57]{margin:0;font-size:18px;font-weight:700;color:var(--text-primary)}.toolbar[data-v-7683ab57]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.range-buttons[data-v-7683ab57]{display:flex;align-items:center;gap:4px}.range-btn[data-v-7683ab57]{padding:4px 10px;font-size:12px;border:1px solid var(--border-color, #e0e0e6);background:transparent;color:var(--text-secondary);border-radius:4px;cursor:pointer;transition:all .15s}.range-btn[data-v-7683ab57]:hover{border-color:var(--primary-color, #18a058);color:var(--primary-color, #18a058)}.range-btn.active[data-v-7683ab57]{background:var(--primary-color, #18a058);border-color:var(--primary-color, #18a058);color:#fff}.custom-range-wrapper[data-v-7683ab57]{margin-left:4px}.loading-overlay[data-v-7683ab57]{display:flex;justify-content:center;padding:20px 0}.summary-cards[data-v-7683ab57]{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;flex-shrink:0}.summary-card[data-v-7683ab57]{background:var(--bg-secondary);border:1px solid var(--border-color, #e0e0e6);border-radius:8px;padding:14px 16px}.summary-label[data-v-7683ab57]{font-size:11px;color:var(--text-secondary);margin-bottom:6px;text-transform:uppercase;letter-spacing:.05em}.summary-value[data-v-7683ab57]{font-size:22px;font-weight:700;color:var(--text-primary);line-height:1.2}.chart-section[data-v-7683ab57]{background:var(--bg-secondary);border:1px solid var(--border-color, #e0e0e6);border-radius:8px;padding:14px 16px;flex-shrink:0}.chart-section.fullscreen[data-v-7683ab57]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;border-radius:0;padding:20px;background:var(--bg-secondary);overflow:auto}.cumulative-section[data-v-7683ab57]{margin-bottom:8px}.chart-section-header[data-v-7683ab57]{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.chart-title[data-v-7683ab57]{font-size:13px;font-weight:600;color:var(--text-primary)}.chart-controls[data-v-7683ab57]{display:flex;align-items:center;gap:8px}.chart-type-toggle[data-v-7683ab57]{display:flex;border:1px solid var(--border-color, #e0e0e6);border-radius:4px;overflow:hidden}.toggle-btn[data-v-7683ab57]{padding:3px 10px;font-size:12px;border:none;background:transparent;color:var(--text-secondary);cursor:pointer;transition:all .15s}.toggle-btn.active[data-v-7683ab57]{background:var(--primary-color, #18a058);color:#fff}.icon-btn[data-v-7683ab57]{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid var(--border-color, #e0e0e6);border-radius:4px;background:transparent;cursor:pointer;color:var(--text-secondary);transition:all .15s}.icon-btn[data-v-7683ab57]:hover{border-color:var(--primary-color, #18a058);color:var(--primary-color, #18a058)}.main-chart[data-v-7683ab57]{height:560px;width:100%}.chart-section.fullscreen .main-chart[data-v-7683ab57]{height:calc(100vh - 100px)}.cumulative-chart[data-v-7683ab57]{height:200px;width:100%}.empty-state[data-v-7683ab57]{height:120px;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);font-size:13px}@media (max-width: 768px){.analytics-page[data-v-7683ab57]{padding:10px 12px}.summary-cards[data-v-7683ab57]{grid-template-columns:repeat(2,1fr)}.analytics-header[data-v-7683ab57]{flex-direction:column;align-items:flex-start}.toolbar[data-v-7683ab57]{width:100%}}@media (max-width: 480px){.summary-cards[data-v-7683ab57]{grid-template-columns:1fr 1fr}.summary-value[data-v-7683ab57]{font-size:18px}}