zen-gitsync 2.12.8 → 2.13.1

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.
@@ -20,7 +20,7 @@ import fs from 'fs';
20
20
  import fsp from 'fs/promises';
21
21
  import path from 'path';
22
22
  import os from 'os';
23
- import { spawn, execFileSync } from 'child_process';
23
+ import { spawn, execFileSync, execFile } from 'child_process';
24
24
  import { EventEmitter } from 'events';
25
25
  import express from 'express';
26
26
 
@@ -28,6 +28,35 @@ const DATA_DIR = path.join(os.homedir(), '.zen-gitsync');
28
28
  const PROMPTS_FILE = path.join(DATA_DIR, 'prompts.json');
29
29
  const TASKS_FILE = path.join(DATA_DIR, 'tasks.json');
30
30
  const IMAGES_DIR = path.join(DATA_DIR, 'workbench-images');
31
+ const INSTRUCTION_FILE = path.join(DATA_DIR, 'ai-instruction.json');
32
+
33
+ // 子项目识别 / 文件扫描时需要跳过的目录
34
+ const SKIP_DIRS = new Set([
35
+ 'node_modules', 'dist', 'build', '.next', '.nuxt', '__pycache__',
36
+ 'target', 'out', 'coverage', 'vendor', '.git', '.svn', '.hg',
37
+ '.idea', '.vscode', '.gradle', '.terraform', '.cache', '.parcel-cache',
38
+ '.turbo', '.svelte-kit', 'storybook-static'
39
+ ]);
40
+
41
+ // 默认生成指令:用户首次使用时作为可编辑指令的初始值
42
+ const DEFAULT_INSTRUCTION = `你是一名资深软件架构师。
43
+
44
+ 【探索步骤】
45
+ 1. 先识别项目结构:扫描根目录是否包含 .git 目录,以及 package.json / pyproject.toml / go.mod / Cargo.toml / pom.xml / build.gradle{,.kts} / composer.json / Gemfile / pubspec.yaml 这 9 种 manifest。
46
+ 2. 如果根目录含 manifest,就把整个根目录视为一个子项目。
47
+ 3. 如果根目录不含 manifest、但子目录(含一层 .git 或上述 manifest)形成多个子项目,对每个子项目分别探索。
48
+ 4. 对每个子项目,重点读取:
49
+ - 所有识别到的 manifest(限制单文件 20KB)
50
+ - README.md(限制 8KB)
51
+ - 入口文件:package.json 的 main / scripts / workspaces 字段;pyproject.toml 的 [project.scripts];go.mod 的 module;Cargo.toml 的 [[bin]];pom.xml 的 <modules>
52
+ - 2 层目录树(最多 200 行)
53
+
54
+ 【输出要求】
55
+ 1. 给出一段 400-800 字的中文「项目架构说明」,覆盖:项目整体定位、技术栈、模块划分、核心流程、关键设计决策。
56
+ 2. 必须引用子项目里实际存在的文件路径、目录名、依赖名,不要编造。
57
+ 3. 多个子项目时:先逐个说明,最后输出一段「整体架构」总结它们之间的关系。
58
+ 4. 语气专业、具体、面向接手这个项目的开发者。
59
+ 5. 只返回 JSON:{ "name": "项目名(10-20字)", "summary": "架构说明正文" }。`;
31
60
 
32
61
  // 单个附件最大 5MB;与 Anthropic Messages API 文档约束一致
33
62
  const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
@@ -151,7 +180,8 @@ async function listDirTree(projectPath, maxDepth = 2, maxEntries = 400) {
151
180
  return lines.join('\n');
152
181
  }
153
182
 
154
- async function callLlmJson(model, prompt) {
183
+ async function callLlmJson(model, prompt, opts = {}) {
184
+ const { maxTokens = 1500, timeoutMs = 60000 } = opts;
155
185
  const { default: fetch } = await import('node-fetch').catch(() => ({ default: globalThis.fetch }));
156
186
  const url = `${String(model.baseURL || '').replace(/\/$/, '')}/chat/completions`;
157
187
  const headers = { 'Content-Type': 'application/json' };
@@ -160,14 +190,14 @@ async function callLlmJson(model, prompt) {
160
190
  const body = JSON.stringify({
161
191
  model: model.model,
162
192
  messages: [{ role: 'user', content: prompt }],
163
- max_tokens: 1500,
193
+ max_tokens: maxTokens,
164
194
  temperature: 0.4,
165
195
  response_format: { type: 'json_object' },
166
196
  stream: false,
167
197
  });
168
198
 
169
199
  const controller = new AbortController();
170
- const timer = setTimeout(() => controller.abort(), 60000);
200
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
171
201
  try {
172
202
  const resp = await fetch(url, { method: 'POST', headers, body, signal: controller.signal });
173
203
  const data = await resp.json().catch(() => ({}));
@@ -230,6 +260,113 @@ function interpolate(template, ctx) {
230
260
  // ── 进程表:记录每个子任务的运行状态 ──────────────────────────────────────
231
261
  const bus = new EventEmitter();
232
262
  const jobs = new Map(); // jobId -> { id, taskId, subId, status, pid, startedAt, endedAt, exitCode, error, prompt }
263
+ // 被用户主动取消的 jobId 集合——runTaskQueue 在 waitProcessExit 之后检查这个集合
264
+ // 来决定把 job 标为 'cancelled' 还是 'done'。
265
+ // 用 Set 而不是 job.cancelled 标志,是为了在 SIGTERM 发出后到 child 真正退出之间
266
+ // 有一个简洁的"待回收"窗口。
267
+ const cancelledJobs = new Set();
268
+
269
+ // ── 生成指令持久化(~/.zen-gitsync/ai-instruction.json) ────────────────────
270
+ async function readInstruction() {
271
+ try {
272
+ const buf = await fsp.readFile(INSTRUCTION_FILE, 'utf-8');
273
+ const obj = JSON.parse(buf);
274
+ if (obj && typeof obj.instruction === 'string' && obj.instruction.trim()) {
275
+ return obj.instruction;
276
+ }
277
+ } catch { /* 文件不存在或解析失败 */ }
278
+ return DEFAULT_INSTRUCTION;
279
+ }
280
+
281
+ async function writeInstruction(instruction) {
282
+ await ensureDataDir();
283
+ const text = String(instruction || '').trim() || DEFAULT_INSTRUCTION;
284
+ const tmp = `${INSTRUCTION_FILE}.tmp`;
285
+ await fsp.writeFile(tmp, JSON.stringify({ instruction: text, updatedAt: nowIso() }, null, 2), 'utf-8');
286
+ await fsp.rename(tmp, INSTRUCTION_FILE);
287
+ }
288
+
289
+ // ── 子项目识别:递归找 .git / manifest;A 是 B 的祖先时只保留 B ─────────────
290
+ async function findSubProjects(projectPath, opts = {}) {
291
+ const { maxDepth = 4 } = opts;
292
+ const candidates = [];
293
+
294
+ async function walk(dir, depth) {
295
+ if (depth > maxDepth) return;
296
+ let entries;
297
+ try { entries = await fsp.readdir(dir, { withFileTypes: true }); }
298
+ catch { return; }
299
+
300
+ let hasManifest = false;
301
+ let hasGit = false;
302
+ const subDirs = [];
303
+ for (const e of entries) {
304
+ if (!e.isDirectory() && !e.isFile()) continue;
305
+ if (e.name.startsWith('.')) {
306
+ if (e.name === '.git' && e.isDirectory()) hasGit = true;
307
+ continue;
308
+ }
309
+ if (e.isDirectory()) {
310
+ if (SKIP_DIRS.has(e.name)) continue;
311
+ subDirs.push(path.join(dir, e.name));
312
+ } else if (e.isFile() && MANIFEST_FILES.includes(e.name)) {
313
+ hasManifest = true;
314
+ }
315
+ }
316
+ if (hasManifest || hasGit) {
317
+ candidates.push(dir);
318
+ return; // 子目录里若还有 manifest,会被自己发现;这里不再下钻避免冗余
319
+ }
320
+ if (depth >= maxDepth) return;
321
+ for (const sub of subDirs) {
322
+ await walk(sub, depth + 1);
323
+ }
324
+ }
325
+
326
+ await walk(projectPath, 0);
327
+
328
+ // 去重:若 candidates 里 A 是 B 的祖先,只保留更深一级的 B
329
+ candidates.sort((a, b) => a.length - b.length);
330
+ const kept = [];
331
+ for (const c of candidates) {
332
+ let dominated = false;
333
+ for (const k of kept) {
334
+ if (c === k || c.startsWith(k + path.sep)) { dominated = true; break; }
335
+ }
336
+ if (!dominated) kept.push(c);
337
+ }
338
+
339
+ // 收集每个子项目的关键文件
340
+ const result = [];
341
+ for (const root of kept) {
342
+ const manifests = {};
343
+ for (const m of MANIFEST_FILES) {
344
+ const p = path.join(root, m);
345
+ try {
346
+ const stat = await fsp.stat(p);
347
+ if (stat.isFile()) {
348
+ manifests[m] = stat.size > 20000
349
+ ? await safeReadFile(p, 20000)
350
+ : await fsp.readFile(p, 'utf8');
351
+ }
352
+ } catch { /* 不存在就跳过 */ }
353
+ }
354
+ let readme = '';
355
+ try {
356
+ const stat = await fsp.stat(path.join(root, 'README.md'));
357
+ if (stat.isFile()) readme = await safeReadFile(path.join(root, 'README.md'), 8000);
358
+ } catch { /* 不存在就跳过 */ }
359
+ const dirTree = await listDirTree(root, 2, 200);
360
+ result.push({
361
+ root,
362
+ name: path.basename(root) || path.basename(projectPath),
363
+ manifests,
364
+ readme,
365
+ dirTree
366
+ });
367
+ }
368
+ return result;
369
+ }
233
370
 
234
371
  function publish(event, payload) {
235
372
  bus.emit('event', { event, payload, ts: nowIso() });
@@ -260,7 +397,9 @@ function launchClaudeInNewWindow(cwd, promptText) {
260
397
  return new Promise((resolve, reject) => {
261
398
  const args = [
262
399
  '-p', promptText,
263
- '--output-format', 'text',
400
+ '--input-format', 'text',
401
+ '--output-format', 'stream-json',
402
+ '--verbose',
264
403
  '--permission-mode', 'bypassPermissions',
265
404
  '--dangerously-skip-permissions'
266
405
  ];
@@ -356,31 +495,92 @@ async function runTaskQueue(task, repoPath, branch) {
356
495
  try {
357
496
  const { pid, child } = await launchClaudeInNewWindow(repoPath || process.cwd(), prompt);
358
497
  job.pid = pid;
498
+ // 保存 child 引用,供 cancel 接口调用 kill
499
+ job.child = child;
359
500
  job.startedAt = nowIso();
360
501
  job.status = 'running';
361
502
  publish('job:update', job);
362
503
 
363
- // 累积子进程输出到 job.output,定期推送给前端;过长时截断尾部避免内存膨胀。
504
+ // 流式 NDJSON 解析:把 stdout 当作 stream-json 协议处理
505
+ // assistant.text → job.output (用户主要关心的内容)
506
+ // assistant.thinking → job.thinking (折叠展示,让用户知道 Claude 在想)
507
+ // 其他事件(init / tool_use / result 等)忽略,避免噪声
508
+ // 解析失败的行原样进 output,便于排查协议异常。
509
+ // 过长时(>256KB)只截断尾部 256KB,避免内存膨胀。
364
510
  const MAX_OUTPUT = 256 * 1024;
365
- const onChunk = (buf) => {
366
- const text = buf.toString('utf8');
367
- job.output = (job.output + text).slice(-MAX_OUTPUT);
511
+ const MAX_THINKING = 64 * 1024;
512
+ job.output = '';
513
+ job.thinking = '';
514
+ const lineBuf = { stdout: '', stderr: '' };
515
+
516
+ const parseLines = (channel, buf) => {
517
+ const chunk = buf.toString('utf8');
518
+ lineBuf[channel] += chunk;
519
+ const lines = lineBuf[channel].split('\n');
520
+ lineBuf[channel] = lines.pop() ?? ''; // 最后一段可能不完整,留给下次
521
+ for (const line of lines) {
522
+ const trimmed = line.trim();
523
+ if (!trimmed) continue;
524
+ if (channel === 'stderr' || !trimmed.startsWith('{')) {
525
+ // 非 stream-json 行:原样塞进 output(兼容老版本 claude / 错误信息)
526
+ job.output = (job.output + trimmed + '\n').slice(-MAX_OUTPUT);
527
+ continue;
528
+ }
529
+ let evt;
530
+ try { evt = JSON.parse(trimmed) } catch { continue }
531
+ if (evt.type !== 'assistant') continue;
532
+ const blocks = evt.message?.content;
533
+ if (!Array.isArray(blocks)) continue;
534
+ for (const b of blocks) {
535
+ if (b.type === 'text' && typeof b.text === 'string') {
536
+ job.output = (job.output + b.text).slice(-MAX_OUTPUT);
537
+ } else if (b.type === 'thinking' && typeof b.thinking === 'string') {
538
+ job.thinking = (job.thinking + b.thinking).slice(-MAX_THINKING);
539
+ }
540
+ }
541
+ }
368
542
  publish('job:update', job);
369
543
  };
370
- if (child.stdout) child.stdout.on('data', onChunk);
371
- if (child.stderr) child.stderr.on('data', onChunk);
544
+ if (child.stdout) child.stdout.on('data', (buf) => parseLines('stdout', buf));
545
+ if (child.stderr) child.stderr.on('data', (buf) => parseLines('stderr', buf));
372
546
 
373
547
  // 等待进程退出(detached 不阻塞主进程,用 polling /proc 兜底)
374
548
  await waitProcessExit(pid);
549
+ const wasCancelled = cancelledJobs.has(jobId)
550
+ if (wasCancelled) cancelledJobs.delete(jobId)
551
+ // 进程退出时 stdout 可能残留最后一段未换行的 NDJSON,flush 一次
552
+ if (lineBuf.stdout.trim()) {
553
+ try {
554
+ const evt = JSON.parse(lineBuf.stdout.trim())
555
+ if (evt.type === 'assistant' && Array.isArray(evt.message?.content)) {
556
+ for (const b of evt.message.content) {
557
+ if (b.type === 'text' && typeof b.text === 'string') {
558
+ job.output = (job.output + b.text).slice(-MAX_OUTPUT)
559
+ } else if (b.type === 'thinking' && typeof b.thinking === 'string') {
560
+ job.thinking = (job.thinking + b.thinking).slice(-MAX_THINKING)
561
+ }
562
+ }
563
+ }
564
+ } catch { /* 不是 JSON,忽略 */ }
565
+ }
375
566
  job.endedAt = nowIso();
376
- job.exitCode = 0;
377
- job.status = 'done';
378
- sub.status = 'done';
567
+ if (wasCancelled) {
568
+ job.exitCode = 130; // 128 + SIGINT(2),约定俗成的"用户取消"退出码
569
+ job.status = 'cancelled';
570
+ job.error = '用户已停止执行';
571
+ // sub 不改状态——cancelled 是 job 维度,同 task 后续 sub 仍可继续执行
572
+ } else {
573
+ job.exitCode = 0;
574
+ job.status = 'done';
575
+ sub.status = 'done';
576
+ }
379
577
  } catch (err) {
380
578
  job.error = err && err.message ? err.message : String(err);
381
579
  job.status = 'error';
382
580
  sub.status = 'error';
383
581
  } finally {
582
+ // 移除 child 引用——避免后续被 SSE 序列化到前端
583
+ delete job.child
384
584
  publish('job:update', job);
385
585
  publish('sub:update', { taskId: task.id, sub });
386
586
  }
@@ -449,112 +649,175 @@ export function registerWorkbenchRoutes({ app, getCurrentProjectPath, getProject
449
649
  return res.status(400).json({ success: false, error: '未配置 AI 模型,请先在通用设置中添加模型' });
450
650
  }
451
651
 
452
- // 收集项目上下文
453
- const dirTree = await listDirTree(projectPath, 2, 400);
454
- const manifest = await readProjectManifest(projectPath);
455
- const readme = await safeReadFile(path.join(projectPath, 'README.md'), 8000);
456
-
457
- const manifestBlock = Object.entries(manifest)
458
- .map(([name, content]) => `\n--- ${name} ---\n${content}`)
459
- .join('\n');
652
+ // 读取用户可编辑的生成指令;没存就用默认
653
+ const userInstruction = await readInstruction();
654
+
655
+ // 递归识别多子项目
656
+ const subProjects = await findSubProjects(projectPath);
657
+ if (subProjects.length === 0) {
658
+ // 没识别到任何子项目:回退到根目录本身
659
+ const fallbackTree = await listDirTree(projectPath, 2, 400);
660
+ const fallbackManifest = await readProjectManifest(projectPath);
661
+ const fallbackReadme = await safeReadFile(path.join(projectPath, 'README.md'), 8000);
662
+ subProjects.push({
663
+ root: projectPath,
664
+ name: path.basename(projectPath),
665
+ manifests: fallbackManifest,
666
+ readme: fallbackReadme,
667
+ dirTree: fallbackTree
668
+ });
669
+ }
460
670
 
461
671
  const projectName = path.basename(projectPath);
672
+ const LLM_OPTS = { maxTokens: 4000, timeoutMs: 1200000 };
462
673
 
463
- const userPayload = `项目根目录:${projectPath}
464
- 项目名称:${projectName}
674
+ // ── 第一阶段:基于可编辑指令 + 根目录概览,生成「可复用的提示词模板」 ──
675
+ const overviewBlock = subProjects.map(sp =>
676
+ `### 子项目 ${sp.name} (${sp.root})\n目录:\n${sp.dirTree || '(无)'}`
677
+ ).join('\n\n');
465
678
 
466
- ## 目录结构(前 2 层,截断)
467
- ${dirTree || '(无)'}
679
+ const firstPrompt = `${userInstruction}
468
680
 
469
- ## README.md
470
- ${readme || '(无)'}
681
+ ---
682
+
683
+ 以下是你需要分析的项目(请先生成「可复用的提示词模板」,不要直接给总结):
471
684
 
472
- ## 关键 manifest
473
- ${manifestBlock || '(无)'}`;
685
+ 项目根目录:${projectPath}
686
+ 项目名称:${projectName}
687
+ 子项目数:${subProjects.length}
474
688
 
475
- // 第一阶段:让 LLM 写一段「可复用的提示词模板」
476
- const templateSystemPrompt = `你是一名资深软件架构师。任务:根据用户提供的项目目录结构、README、manifest 文件,输出一段**可复用的提示词模板**。
477
- 这段模板将作为指令注入到大模型的 system prompt 中,用来指导大模型对**当前项目**做「项目架构总结」。
689
+ ## 子项目概览
690
+ ${overviewBlock || '(无)'}
478
691
 
479
- 要求:
480
- 1. 模板主体使用中文,语气专业、具体
481
- 2. 模板中必须明确使用 4 个变量占位符:
482
- - {{task.title}} - 任务标题
483
- - {{task.desc}} - 任务详细描述
484
- - {{sub.title}} - 子任务标题
485
- - {{sub.desc}} - 子任务详细描述
486
- 并向大模型说明:项目根目录是 {{repo.path}},当前 git 分支是 {{branch}}
487
- 3. 模板应指导大模型:阅读项目目录、识别语言与框架、找出入口文件、画出主要模块依赖关系、输出 200-400 字的中文总结
488
- 4. 模板长度控制在 300-600 字之间
489
- 5. 只返回 JSON,不要任何额外解释
692
+ ## 各子项目 manifest 与 README
693
+ ${subProjects.map(sp => {
694
+ const manifestBlock = Object.entries(sp.manifests)
695
+ .map(([n, c]) => `\n--- ${n} ---\n${c}`)
696
+ .join('\n');
697
+ return `\n### ${sp.name}\n${manifestBlock || '(无 manifest)'}\n\nREADME(前 8KB):\n${sp.readme || '(无)'}`;
698
+ }).join('\n')}
490
699
 
491
- 返回 JSON:
700
+ 只返回 JSON:
492
701
  {
493
- "name": "模板名称(10-20字)",
494
- "template": "模板正文"
702
+ "name": "项目名(10-20字)",
703
+ "template": "可复用的提示词模板(300-600字),应明确使用 {{task.title}} / {{task.desc}} / {{sub.title}} / {{sub.desc}} / {{repo.path}} / {{branch}} 这 6 个变量"
495
704
  }`;
496
705
 
497
- const first = await callLlmJson(model, `${templateSystemPrompt}\n\n${userPayload}`);
498
- const templateName = String(first.name || '').trim() || '项目架构总结';
706
+ const first = await callLlmJson(model, firstPrompt, LLM_OPTS);
707
+ const templateName = String(first.name || '').trim() || projectName || '项目架构说明';
499
708
  const template = String(first.template || '').trim();
500
- if (!template) {
501
- return res.status(500).json({ success: false, error: 'AI 未返回有效模板' });
502
- }
503
709
 
504
- // 第二阶段:以模板为指令,喂入项目上下文,跑一次实际生成
505
- const execPrompt = `${template}
710
+ // ── 第二阶段:为每个子项目分别生成总结(单子项目 = 现在的行为) ──
711
+ async function summarizeOneSub(sp) {
712
+ const manifestBlock = Object.entries(sp.manifests)
713
+ .map(([n, c]) => `\n--- ${n} ---\n${c}`)
714
+ .join('\n');
715
+ const subPrompt = `${template}
506
716
 
507
717
  ---
508
718
 
509
- 以下是你需要分析的项目实际数据(请直接基于这些数据输出最终结果):
719
+ 以下是你需要分析的一个子项目(请直接基于这些数据输出该子项目的架构说明):
510
720
 
511
- 项目根目录:${projectPath}
512
- 项目名称:${projectName}
721
+ 子项目根目录:${sp.root}
722
+ 子项目名称:${sp.name}
513
723
 
514
724
  ## 目录结构(前 2 层)
515
- ${dirTree || '(无)'}
516
-
517
- ## README.md
518
- ${readme || '(无)'}
725
+ ${sp.dirTree || '(无)'}
519
726
 
520
- ## 关键 manifest
727
+ ## manifest
521
728
  ${manifestBlock || '(无)'}
522
729
 
523
- 请输出一份 200-400 字的中文架构总结,包含:项目整体定位、技术栈、模块划分、核心流程、关键设计决策。只返回 JSON:
730
+ ## README
731
+ ${sp.readme || '(无)'}
524
732
 
733
+ 只返回 JSON:
525
734
  {
526
- "summary": "架构总结正文"
735
+ "summary": "该子项目的架构说明(300-600字)"
527
736
  }`;
737
+ const r = await callLlmJson(model, subPrompt, LLM_OPTS);
738
+ return { name: sp.name, root: sp.root, summary: String(r.summary || '').trim() };
739
+ }
740
+
741
+ const subSummaries = await Promise.all(subProjects.map(summarizeOneSub));
742
+
743
+ // ── 第三阶段:仅多子项目时合并(单子项目直接拿它的 summary) ──
744
+ let finalSummary = '';
745
+ let finalName = templateName;
528
746
 
529
- const second = await callLlmJson(model, execPrompt);
530
- const summary = String(second.summary || '').trim();
747
+ if (subSummaries.length === 1) {
748
+ finalSummary = subSummaries[0].summary;
749
+ } else {
750
+ const mergePrompt = `你是项目架构师。下列是同一仓库下 N 个子项目的架构说明,请合并输出**单一**的「项目架构说明」(800-1500字),覆盖:项目整体定位、技术栈、模块划分、子项目间关系、核心流程、关键设计决策。
751
+ 子项目之间用清晰的小标题或编号分隔。最后输出一段「整体架构」总结它们如何协同。
752
+ 只引用实际出现的子项目名 / 文件路径 / 依赖名,不要编造。只返回 JSON:
531
753
 
532
- if (!summary) {
533
- // 兜底:仅返回模板,结果留空
754
+ {
755
+ "name": "项目名(10-20字)",
756
+ "summary": "合并后的架构说明"
757
+ }
758
+
759
+ ## 子项目说明
760
+ ${subSummaries.map((s, i) => `\n### [${i + 1}] ${s.name} (${s.root})\n${s.summary || '(空)'}`).join('\n')}`;
761
+
762
+ const merged = await callLlmJson(model, mergePrompt, LLM_OPTS);
763
+ finalSummary = String(merged.summary || '').trim()
764
+ || subSummaries.map(s => `### ${s.name}\n${s.summary}`).join('\n\n');
765
+ finalName = String(merged.name || '').trim() || templateName;
766
+ }
767
+
768
+ if (!finalSummary) {
769
+ // 兜底:仅返回模板
534
770
  return res.json({
535
771
  success: true,
536
- name: templateName,
772
+ name: finalName,
537
773
  template,
538
774
  result: '',
539
775
  content: template
540
776
  });
541
777
  }
542
778
 
543
- // 把模板和实际结果拼到一起,作为预置提示词存进 prompts.json
544
- const content = `${template}\n\n## 当前项目架构总结(已生成于 ${nowIso()})\n\n${summary}`;
545
-
779
+ // 顶层 request 已经自带 20 分钟(1200s)超时;
780
+ // 这里在 express 处理器内部不再额外加整体超时。
546
781
  res.json({
547
782
  success: true,
548
- name: templateName,
783
+ name: finalName,
549
784
  template,
550
- result: summary,
551
- content
785
+ result: finalSummary,
786
+ content: finalSummary
552
787
  });
553
788
  } catch (err) {
554
789
  res.status(500).json({ success: false, error: err.message });
555
790
  }
556
791
  });
557
792
 
793
+ // ── 生成指令:读 / 写(用户可在弹窗里自定义) ───────────────────────
794
+ app.get('/api/workbench/prompts/ai-instruction', async (_req, res) => {
795
+ try {
796
+ const instruction = await readInstruction();
797
+ res.json({ success: true, instruction, isDefault: instruction === DEFAULT_INSTRUCTION });
798
+ } catch (err) {
799
+ res.status(500).json({ success: false, error: err.message });
800
+ }
801
+ });
802
+
803
+ app.put('/api/workbench/prompts/ai-instruction', async (req, res) => {
804
+ try {
805
+ const text = req.body && typeof req.body.instruction === 'string'
806
+ ? req.body.instruction.trim()
807
+ : '';
808
+ if (!text) {
809
+ return res.status(400).json({ success: false, error: '指令不能为空' });
810
+ }
811
+ if (text.length > 50000) {
812
+ return res.status(413).json({ success: false, error: '指令过长(最多 50000 字符)' });
813
+ }
814
+ await writeInstruction(text);
815
+ res.json({ success: true });
816
+ } catch (err) {
817
+ res.status(500).json({ success: false, error: err.message });
818
+ }
819
+ });
820
+
558
821
  // SSE 事件流
559
822
  app.get('/api/workbench/events', (req, res) => {
560
823
  res.set({
@@ -733,6 +996,49 @@ ${manifestBlock || '(无)'}
733
996
  res.json({ success: true, jobs: snapshotJobs() });
734
997
  });
735
998
 
999
+ // ── 取消正在执行的 job ───────────────────────────────────────────
1000
+ // POST /api/workbench/jobs/:id/cancel
1001
+ // 行为:
1002
+ // - 找到正在运行的 job,调 child.kill() 终止 claude 进程
1003
+ // - Windows 下用 taskkill /T /F 杀进程树(claude 进程可能 fork 出子进程)
1004
+ // - 加入 cancelledJobs 集合,runTaskQueue 退出循环后会把 job 标为 'cancelled'
1005
+ // - 只影响这一个 sub;同 task 后续 sub 仍按队列顺序继续执行
1006
+ app.post('/api/workbench/jobs/:id/cancel', (req, res) => {
1007
+ const job = jobs.get(req.params.id)
1008
+ if (!job) {
1009
+ return res.status(404).json({ success: false, error: 'job 不存在' })
1010
+ }
1011
+ if (job.status !== 'running' && job.status !== 'pending') {
1012
+ return res.status(400).json({ success: false, error: `当前状态 ${job.status} 不可取消` })
1013
+ }
1014
+ cancelledJobs.add(job.id)
1015
+ // 立即给前端一个状态反馈(不等 child 真正退出)
1016
+ job.status = 'cancelled'
1017
+ job.error = '用户已停止执行'
1018
+ job.endedAt = nowIso()
1019
+ publish('job:update', { ...job }) // 用浅拷贝避免序列化 child 引用
1020
+ const child = job.child
1021
+ if (!child) {
1022
+ return res.json({ success: true, message: '已标记取消,进程将尽快结束' })
1023
+ }
1024
+ try {
1025
+ if (process.platform === 'win32') {
1026
+ // Windows: child.kill(SIGTERM) 经常无效,用 taskkill 杀进程树
1027
+ execFile('taskkill', ['/PID', String(child.pid), '/T', '/F'], (err) => {
1028
+ if (err) {
1029
+ console.warn(`[workbench] taskkill ${child.pid} 失败:`, err.message)
1030
+ }
1031
+ })
1032
+ } else {
1033
+ child.kill('SIGTERM')
1034
+ }
1035
+ res.json({ success: true, message: '已发送停止信号' })
1036
+ } catch (err) {
1037
+ cancelledJobs.delete(job.id)
1038
+ res.status(500).json({ success: false, error: '发送停止信号失败: ' + err.message })
1039
+ }
1040
+ });
1041
+
736
1042
  // ── 子任务附件:上传 / 删除 / 列表 ───────────────────────────────
737
1043
  // 上传:POST /api/workbench/subtasks/:subId/attachments
738
1044
  // header: X-Original-Name, X-Mime-Type
@@ -1,3 +0,0 @@
1
- import{o as e}from"./rolldown-runtime-CMxvf4Kt.js";import{$n as t,An as n,Cn as r,En as i,Fn as a,Hn as o,In as s,Jn as c,Kn as l,Ln as u,Mn as d,Nn as f,P as p,Qn as m,Sn as ee,Un as te,Wn as ne,_ as re,ar as h,c as g,cr as _,d as v,er as y,f as ie,g as ae,h as oe,ht as se,ir as ce,kn as b,m as le,or as x,p as ue,rr as S,sr as C,t as de,v as fe,wn as pe,y as me,zn as he}from"./vendor-A4IPqbyo.js";import{n as w,t as T}from"./_plugin-vue_export-helper-opDPZuCO.js";import{a as ge,r as _e}from"./index-DPihtJfT.js";var ve=e(v(),1),ye={class:`source-map-view`},be={class:`sm-toolbar`},xe={class:`sm-toolbar-left`},Se={class:`sm-title`},Ce={class:`sm-toolbar-center`},we=[`placeholder`,`disabled`],Te=[`disabled`],Ee={class:`sm-toolbar-right`},De={class:`sm-body`},Oe={class:`sm-panel-header sm-panel-header--tabs`},ke={key:0,class:`sm-badge`},Ae={class:`sm-panel-body sm-file-tree`},je=[`onClick`],Me={key:1,class:`sm-tree-arrow-spacer`},Ne={key:2,class:`sm-tree-icon mit-icon`,"aria-hidden":`true`},Pe=[`xlink:href`],Fe={key:3,class:`sm-tree-icon mit-icon`,"aria-hidden":`true`},Ie=[`xlink:href`],Le=[`title`],Re={key:1,class:`sm-tree-empty`},ze={class:`sm-panel-body sm-outline-body`},Be={class:`sm-badge`,style:{"margin-left":`auto`}},Ve=[`onClick`],He=[`title`],Ue={key:0,class:`sm-outline-desc`},We={key:1,class:`sm-tree-empty`},Ge={class:`sm-panel sm-panel-graph`},Ke={key:0,class:`sm-project-info`},qe={class:`sm-lang-badge`},Je={class:`sm-summary-text`},Ye={class:`sm-graph-container`},Xe={class:`sm-layout-btn-wrap`},Ze=[`disabled`],Qe=[`title`],$e={class:`sm-fn-label`},et={key:0,class:`sm-fn-desc`},tt={key:0,class:`sm-graph-empty`},nt={viewBox:`0 0 24 24`,width:`48`,height:`48`,fill:`none`,stroke:`currentColor`,"stroke-width":`1`,style:{opacity:`0.3`}},rt={key:1,class:`sm-graph-loading`},it={class:`sm-log-header`},at={key:0,class:`sm-log-indicator`},ot={class:`sm-log-body`,ref:`logBodyRef`},st={key:0,class:`sm-empty`},ct={class:`sm-panel-header`},lt={key:0,class:`sm-badge sm-badge-amber`},ut={key:0,class:`sm-node-detail`},dt={class:`sm-node-name`},ft={class:`sm-node-file`},pt={key:0},mt={key:0,class:`sm-node-desc`},ht={class:`sm-panel-body sm-source-body`},gt={key:0,class:`sm-source-overlay`},_t={key:1,class:`sm-source-overlay sm-source-placeholder`},E=T(he({__name:`SourceMapView`,setup(e){let v=S(document.documentElement.getAttribute(`data-theme`)===`dark`?`dark`:`light`);function he(){v.value=document.documentElement.getAttribute(`data-theme`)===`dark`?`dark`:`light`}let T=null,E=ge(),D=S(E.currentDirectory||``),O=S(`idle`),k=S([]),vt=0,A=S(null),j=S(null),M=S(`files`),N=S(``),P=S(``),F=S(!1),I=S([]),L=S(new Set),R=S({files:!0,graph:!0,source:!0}),z=S(!1),B=S(220),V=S(360),H=S(140),U=null,W=0,yt=0,G=0;function K(e,t){U=e,W=t.clientX,yt=t.clientY,G=e===`files`?B.value:e===`source`?V.value:H.value,document.addEventListener(`mousemove`,bt),document.addEventListener(`mouseup`,q),t.preventDefault()}function bt(e){U&&(U===`files`?B.value=Math.max(120,Math.min(480,G+e.clientX-W)):U===`source`?V.value=Math.max(200,Math.min(700,G+W-e.clientX)):H.value=Math.max(60,Math.min(500,G+yt-e.clientY)))}function q(){U=null,document.removeEventListener(`mousemove`,bt),document.removeEventListener(`mouseup`,q)}let J=S(null),Y=ce(null),{fitView:xt,setNodes:X,setEdges:St,getNodes:Ct,getEdges:wt,updateNodeInternals:Tt}=me(),Z=b(()=>O.value===`scanning`||O.value===`analyzing`),Et=b(()=>v.value===`dark`?`#334155`:`#cbd5e1`),Q=b(()=>A.value?.nodes.find(e=>e.id===j.value)??null),Dt=b(()=>{if(!A.value)return[];let e=new Map;for(let t of A.value.nodes){let n=t.subsystem??`__default__`;if(!e.has(n)){let r=A.value.subsystems?.find(e=>e.name===n);e.set(n,{displayName:r?.displayName||t.subsystem||w(`@SRCMAP:默认`),color:t.subsystemColor||kt[0],nodes:[]})}e.get(n).nodes.push(t)}return[...e.entries()].map(([,e])=>e)});function $(e,t=`info`){k.value.push({id:++vt,message:e,type:t,timestamp:Date.now()}),k.value.length>200&&k.value.splice(0,k.value.length-200)}function Ot(e){let t={name:``,path:``,kind:`dir`,children:[],childMap:new Map};for(let n of e){let e=n.trim().split(`/`).filter(Boolean),r=t,i=``;for(let t=0;t<e.length;t++){let n=e[t];i=i?`${i}/${n}`:n;let a=t===e.length-1,o=r.childMap.get(n);o||(o={name:n,path:i,kind:a?`file`:`dir`,children:[],childMap:new Map},r.childMap.set(n,o),r.children.push(o)),r=o}}let n=e=>{e.sort((e,t)=>e.kind===t.kind?e.name.localeCompare(t.name):e.kind===`dir`?-1:1),e.forEach(e=>{e.kind===`dir`&&n(e.children)})};return n(t.children),t.children}let kt=[`#f59e0b`,`#3b82f6`,`#10b981`,`#8b5cf6`];function At(e){return e.subsystemColor?e.subsystemColor:e.subsystemIndex===void 0?e.importance===`high`?`#f59e0b`:e.importance===`low`?`#94a3b8`:`#3b82f6`:kt[e.subsystemIndex%kt.length]}function jt(e){return{ts:`typescript`,tsx:`typescript`,js:`javascript`,mjs:`javascript`,cjs:`javascript`,jsx:`javascript`,vue:`html`,svelte:`html`,html:`html`,py:`python`,java:`java`,go:`go`,rs:`rust`,cpp:`cpp`,cc:`cpp`,cxx:`cpp`,c:`c`,h:`c`,hpp:`cpp`,cs:`csharp`,rb:`ruby`,php:`php`,swift:`swift`,kt:`kotlin`,sh:`shell`,bash:`shell`,json:`json`,yaml:`yaml`,yml:`yaml`,md:`markdown`,css:`css`,scss:`scss`,less:`less`,sql:`sql`}[e.split(`.`).pop()?.toLowerCase()||``]||`plaintext`}function Mt(){!J.value||Y.value||(Y.value=g.create(J.value,{value:``,language:`plaintext`,theme:v.value===`dark`?`vs-dark`:`vs`,readOnly:!0,fontSize:12,lineHeight:19,fontFamily:`'JetBrains Mono', 'Fira Code', Consolas, monospace`,minimap:{enabled:!1},scrollBeyondLastLine:!1,automaticLayout:!0,wordWrap:`off`,padding:{top:8,bottom:8},scrollbar:{verticalScrollbarSize:6,horizontalScrollbarSize:6}}))}m(v,e=>{Y.value&&g.setTheme(e===`dark`?`vs-dark`:`vs`)});function Nt(e,t){let n={};t.forEach(e=>{n[e.source]||(n[e.source]=[]),n[e.source].push(e.target)});let r=new Map;e.forEach(e=>{let t=e.subsystem??`__default`;r.has(t)||r.set(t,[]),r.get(t).push(e)});let i=[],a=0;for(let[,e]of r){let t=new Set(e.map(e=>e.id)),r={},o=e.length?[e[0].id]:[];for(o[0]&&(r[o[0]]=0);o.length;){let e=o.shift();(n[e]||[]).filter(e=>t.has(e)).forEach(t=>{r[t]===void 0&&(r[t]=(r[e]??0)+1,o.push(t))})}let s={};e.forEach(e=>{let t=r[e.id]??0;s[t]=(s[t]||0)+1});let c={};e.forEach(e=>{let t=r[e.id]??0;c[t]=(c[t]??-1)+1;let n=c[t],o=s[t]??1,l=a*600+n*220-(o-1)*220/2,u=t*110,d=At(e);i.push({id:e.id,type:`default`,position:{x:l,y:u},label:e.label,class:`sm-fn-node`,data:{...e,_accentColor:d},style:{"--node-accent":d}})}),a++}return{flowNodes:i,flowEdges:t.map((e,t)=>({id:`e_${t}_${e.source}_${e.target}`,source:e.source,target:e.target,type:`smoothstep`,animated:!1,class:`sm-fn-edge`,markerEnd:{type:oe.ArrowClosed}}))}}async function Pt(){let e=Ct.value,t=wt.value;if(e.length!==0){z.value=!0;try{await o(),await new Promise(e=>requestAnimationFrame(()=>e())),Tt(e.map(e=>e.id)),await new Promise(e=>requestAnimationFrame(()=>e()));let n=new Map;e.forEach(e=>{let t=e.data?.subsystem??`__default__`;n.has(t)||n.set(t,[]),n.get(t).push(e)});let r=new Map,i=0;for(let[,e]of n){let n=new ve.default.graphlib.Graph;n.setDefaultEdgeLabel(()=>({})),n.setGraph({rankdir:`TB`,nodesep:55,ranksep:75,marginx:40,marginy:40});let a=new Set(e.map(e=>e.id));e.forEach(e=>{let t=190,r=e.data?.description?68:48,i=document.querySelector(`.vue-flow__node[data-id="${e.id}"]`);i&&i.offsetWidth>0&&(t=i.offsetWidth,r=i.offsetHeight),n.setNode(e.id,{width:t,height:r})}),t.forEach(e=>{a.has(e.source)&&a.has(e.target)&&n.setEdge(e.source,e.target)}),ve.default.layout(n);let o=0;e.forEach(e=>{let t=n.node(e.id);t&&(r.set(e.id,{x:i+t.x-t.width/2,y:t.y-t.height/2}),o=Math.max(o,t.x+t.width/2))}),i+=o+120}X(e.map(e=>({...e,position:r.get(e.id)??e.position}))),await o(),xt({padding:.18})}finally{z.value=!1}}}async function Ft(){if(!D.value.trim()){p.warning(w(`@SRCMAP:请先输入项目路径`));return}if(!Z.value){k.value=[],A.value=null,j.value=null,N.value=``,P.value=``,X([]),St([]),O.value=`scanning`,$(w(`@SRCMAP:开始分析项目...`),`info`);try{let e=await fetch(`/api/code-analysis/analyze`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({path:D.value})});if(!e.ok){let t=await e.json().catch(()=>({error:`请求失败`}));throw Error(t.error||`HTTP ${e.status}`)}let t=e.body.getReader(),n=new TextDecoder,r=``;O.value=`analyzing`;let i=``,a=[];for(;;){let{done:e,value:o}=await t.read();if(e)break;r+=n.decode(o,{stream:!0});let s=r.split(`
2
- `);r=s.pop()??``;for(let e of s)if(e.startsWith(`event:`))i=e.slice(6).trim(),a=[];else if(e.startsWith(`data:`))a.push(e.slice(5).trim());else if(e===``){if(i&&a.length){let e=a.join(``);try{let t=JSON.parse(e);It(i,t)}catch{}}i=``,a=[]}}}catch(e){$(`${w(`@SRCMAP:分析失败`)}: ${e.message}`,`error`),O.value=`error`}}}function It(e,t){if(e===`log`)$(t.message,t.type||`info`);else if(e===`files`)I.value=Ot(t.files||[]),I.value.forEach(e=>{e.kind===`dir`&&L.value.add(e.path)});else if(e===`result`){A.value={language:t.language||``,entryFile:t.entryFile||``,entryFunction:t.entryFunction||``,nodes:Array.isArray(t.nodes)?t.nodes:[],edges:Array.isArray(t.edges)?t.edges:[],techStack:Array.isArray(t.techStack)?t.techStack:[],summary:t.summary||``,allFiles:Array.isArray(t.allFiles)?t.allFiles:[],codeFiles:Array.isArray(t.codeFiles)?t.codeFiles:[],subsystems:Array.isArray(t.subsystems)?t.subsystems:void 0};let{flowNodes:e,flowEdges:n}=Nt(A.value.nodes,A.value.edges);X(e),St(n),o(()=>Pt())}else e===`done`&&(t.error?($(`${w(`@SRCMAP:分析失败`)}: ${t.error}`,`error`),O.value=`error`):O.value=`done`)}async function Lt(e){if(!(!e||F.value)&&P.value!==e){F.value=!0,P.value=e,N.value=``;try{let t=await(await fetch(`/api/code-analysis/file-content?path=${encodeURIComponent(D.value)}&file=${encodeURIComponent(e)}`)).json();if(t.error)throw Error(t.error);N.value=t.content||``}catch(e){N.value=`// ${w(`@SRCMAP:加载失败`)}: ${e.message}`}finally{F.value=!1}}}function Rt(e){let t=e.node.data;j.value=t.id,t.file&&Lt(t.file)}function zt(e){L.value.has(e)?L.value.delete(e):L.value.add(e)}let Bt=b(()=>{let e=[];function t(n,r){for(let i of n){let n=L.value.has(i.path);e.push({name:i.name,path:i.path,kind:i.kind,depth:r,expanded:n}),i.kind===`dir`&&n&&t(i.children,r+1)}}return t(I.value,0),e});return m(()=>E.currentDirectory,e=>{e&&!D.value&&(D.value=e)}),m([N,P],([e,t])=>{let n=Y.value;if(!n)return;let r=jt(t||``),i=n.getModel(),a=g.createModel(e||``,r);n.setModel(a),i?.dispose(),n.setScrollPosition({scrollTop:0,scrollLeft:0})}),ne(()=>{Mt(),!D.value&&E.currentDirectory&&(D.value=E.currentDirectory),T=new MutationObserver(()=>he()),T.observe(document.documentElement,{attributes:!0,attributeFilter:[`data-theme`]})}),te(()=>{Y.value?.getModel()?.dispose(),Y.value?.dispose(),q(),T?.disconnect(),T=null}),(e,o)=>{let p=se;return l(),f(`div`,ye,[n(`div`,be,[n(`div`,xe,[o[9]||=n(`svg`,{viewBox:`0 0 24 24`,width:`18`,height:`18`,fill:`none`,stroke:`currentColor`,"stroke-width":`1.8`,class:`sm-icon-map`},[n(`polygon`,{points:`3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21`}),n(`line`,{x1:`9`,y1:`3`,x2:`9`,y2:`18`}),n(`line`,{x1:`15`,y1:`6`,x2:`15`,y2:`21`})],-1),n(`span`,Se,_(h(w)(`@SRCMAP:源码地图`)),1)]),n(`div`,Ce,[y(n(`input`,{"onUpdate:modelValue":o[0]||=e=>D.value=e,class:`sm-path-input`,placeholder:h(w)(`@SRCMAP:输入项目目录路径`),disabled:Z.value,onKeydown:pe(Ft,[`enter`])},null,40,we),[[ee,D.value]]),n(`button`,{class:`sm-btn sm-btn-primary`,disabled:Z.value,onClick:Ft},[Z.value?(l(),f(i,{key:0},[o[10]||=n(`span`,{class:`sm-spinner`},null,-1),s(` `+_(h(w)(`@SRCMAP:分析中...`)),1)],64)):(l(),f(i,{key:1},[s(_(O.value===`done`?h(w)(`@SRCMAP:重新分析`):h(w)(`@SRCMAP:开始分析`)),1)],64))],8,Te)]),n(`div`,Ee,[u(p,{content:h(w)(`@SRCMAP:文件列表`),placement:`bottom`},{default:t(()=>[n(`button`,{class:x([`sm-panel-btn`,{active:R.value.files}]),onClick:o[1]||=e=>R.value.files=!R.value.files},[...o[11]||=[n(`svg`,{viewBox:`0 0 24 24`,width:`16`,height:`16`,fill:`none`,stroke:`currentColor`,"stroke-width":`1.8`},[n(`path`,{d:`M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z`})],-1)]],2)]),_:1},8,[`content`]),u(p,{content:h(w)(`@SRCMAP:调用图`),placement:`bottom`},{default:t(()=>[n(`button`,{class:x([`sm-panel-btn`,{active:R.value.graph}]),onClick:o[2]||=e=>R.value.graph=!R.value.graph},[...o[12]||=[n(`svg`,{viewBox:`0 0 24 24`,width:`16`,height:`16`,fill:`none`,stroke:`currentColor`,"stroke-width":`1.8`},[n(`circle`,{cx:`12`,cy:`12`,r:`3`}),n(`circle`,{cx:`3`,cy:`6`,r:`2`}),n(`circle`,{cx:`21`,cy:`6`,r:`2`}),n(`circle`,{cx:`3`,cy:`18`,r:`2`}),n(`circle`,{cx:`21`,cy:`18`,r:`2`}),n(`line`,{x1:`5`,y1:`6`,x2:`9`,y2:`11`}),n(`line`,{x1:`19`,y1:`6`,x2:`15`,y2:`11`}),n(`line`,{x1:`5`,y1:`18`,x2:`9`,y2:`13`}),n(`line`,{x1:`19`,y1:`18`,x2:`15`,y2:`13`})],-1)]],2)]),_:1},8,[`content`]),u(p,{content:h(w)(`@SRCMAP:源码面板`),placement:`bottom`},{default:t(()=>[n(`button`,{class:x([`sm-panel-btn`,{active:R.value.source}]),onClick:o[3]||=e=>R.value.source=!R.value.source},[...o[13]||=[n(`svg`,{viewBox:`0 0 24 24`,width:`16`,height:`16`,fill:`none`,stroke:`currentColor`,"stroke-width":`1.8`},[n(`polyline`,{points:`16 18 22 12 16 6`}),n(`polyline`,{points:`8 6 2 12 8 18`})],-1)]],2)]),_:1},8,[`content`])])]),n(`div`,De,[y(n(`div`,{class:`sm-panel sm-panel-files`,style:C({width:B.value+`px`})},[n(`div`,Oe,[n(`button`,{class:x([`sm-tab-btn`,{active:M.value===`files`}]),onClick:o[4]||=e=>M.value=`files`},[o[14]||=n(`svg`,{viewBox:`0 0 24 24`,width:`12`,height:`12`,fill:`none`,stroke:`currentColor`,"stroke-width":`1.8`},[n(`path`,{d:`M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z`})],-1),s(` `+_(h(w)(`@SRCMAP:文件列表`))+` `,1),A.value?(l(),f(`span`,ke,_(A.value.allFiles.length),1)):d(``,!0)],2),n(`button`,{class:x([`sm-tab-btn`,{active:M.value===`outline`}]),onClick:o[5]||=e=>M.value=`outline`},[o[15]||=a(`<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.8" data-v-71d3569e><line x1="8" y1="6" x2="21" y2="6" data-v-71d3569e></line><line x1="8" y1="12" x2="21" y2="12" data-v-71d3569e></line><line x1="8" y1="18" x2="21" y2="18" data-v-71d3569e></line><line x1="3" y1="6" x2="3.01" y2="6" data-v-71d3569e></line><line x1="3" y1="12" x2="3.01" y2="12" data-v-71d3569e></line><line x1="3" y1="18" x2="3.01" y2="18" data-v-71d3569e></line></svg>`,1),s(` `+_(h(w)(`@SRCMAP:大纲`)),1)],2)]),y(n(`div`,Ae,[Bt.value.length>0?(l(!0),f(i,{key:0},c(Bt.value,e=>(l(),f(`div`,{key:e.path,class:x([`sm-tree-node`,{"sm-tree-node--dir":e.kind===`dir`,"sm-tree-node--active":e.kind===`file`&&P.value===e.path}]),style:C({paddingLeft:10+e.depth*14+`px`}),onClick:t=>e.kind===`dir`?zt(e.path):Lt(e.path)},[e.kind===`dir`?(l(),f(`span`,{key:0,class:x([`sm-tree-arrow`,{expanded:e.expanded}])},[...o[16]||=[n(`svg`,{viewBox:`0 0 24 24`,width:`10`,height:`10`,fill:`none`,stroke:`currentColor`,"stroke-width":`2.5`,"stroke-linecap":`round`,"stroke-linejoin":`round`},[n(`polyline`,{points:`9 18 15 12 9 6`})],-1)]],2)):(l(),f(`span`,Me)),e.kind===`dir`?(l(),f(`svg`,Ne,[n(`use`,{"xlink:href":`#${h(_e)(e.name)||`icon-folder`}`},null,8,Pe)])):(l(),f(`svg`,Fe,[n(`use`,{"xlink:href":`#${h(_e)(e.name)}`},null,8,Ie)])),n(`span`,{class:`sm-tree-name`,title:e.path},_(e.name),9,Le)],14,je))),128)):(l(),f(`div`,Re,_(h(w)(`@SRCMAP:暂无文件,请先开始分析`)),1))],512),[[r,M.value===`files`]]),y(n(`div`,ze,[Dt.value.length>0?(l(!0),f(i,{key:0},c(Dt.value,e=>(l(),f(`div`,{key:e.displayName,class:`sm-outline-group`},[n(`div`,{class:`sm-outline-group-header`,style:C({color:e.color})},[o[17]||=n(`svg`,{viewBox:`0 0 24 24`,width:`8`,height:`8`,fill:`currentColor`},[n(`circle`,{cx:`12`,cy:`12`,r:`6`})],-1),s(` `+_(e.displayName)+` `,1),n(`span`,Be,_(e.nodes.length),1)],4),(l(!0),f(i,null,c(e.nodes,t=>(l(),f(`div`,{key:t.id,class:x([`sm-outline-node`,{"sm-outline-node--active":j.value===t.id}]),onClick:e=>{j.value=t.id,t.file&&Lt(t.file)}},[n(`span`,{class:`sm-outline-dot`,style:C({background:e.color})},null,4),n(`span`,{class:`sm-outline-label`,title:t.file||t.label},_(t.label),9,He),t.description?(l(),f(`span`,Ue,_(t.description.length>18?t.description.slice(0,18)+`…`:t.description),1)):d(``,!0)],10,Ve))),128))]))),128)):(l(),f(`div`,We,_(h(w)(`@SRCMAP:暂无分析结果`)),1))],512),[[r,M.value===`outline`]])],4),[[r,R.value.files]]),y(n(`div`,{class:`sm-resizer sm-resizer-v`,onMousedown:o[6]||=e=>K(`files`,e)},null,544),[[r,R.value.files&&R.value.graph]]),y(n(`div`,Ge,[A.value?(l(),f(`div`,Ke,[n(`span`,qe,_(A.value.language),1),A.value.subsystems&&A.value.subsystems.length>1?(l(!0),f(i,{key:0},c(A.value.subsystems,e=>(l(),f(`span`,{key:e.name,class:`sm-subsystem-tag`,style:C({borderColor:e.color,color:e.color})},`● `+_(e.displayName||e.name),5))),128)):(l(!0),f(i,{key:1},c(A.value.techStack.slice(0,4),e=>(l(),f(`span`,{key:e,class:`sm-tech-tag`},_(e),1))),128)),n(`span`,Je,_(A.value.summary),1)])):d(``,!0),n(`div`,Ye,[n(`div`,Xe,[n(`button`,{class:`sm-layout-btn`,disabled:z.value||!A.value,onClick:Pt},[o[18]||=a(`<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.8" data-v-71d3569e><rect x="3" y="3" width="7" height="7" rx="1" data-v-71d3569e></rect><rect x="14" y="3" width="7" height="7" rx="1" data-v-71d3569e></rect><rect x="3" y="14" width="7" height="7" rx="1" data-v-71d3569e></rect><rect x="14" y="14" width="7" height="7" rx="1" data-v-71d3569e></rect></svg>`,1),s(` `+_(z.value?h(w)(`@SRCMAP:布局中...`):h(w)(`@SRCMAP:优化布局`)),1)],8,Ze)]),u(h(re),{class:`sm-vue-flow`,"default-viewport":{zoom:1},"min-zoom":.2,"max-zoom":4,"fit-view-on-init":``,onNodeClick:Rt},{"node-default":t(({data:e,label:t})=>[u(h(fe),{type:`target`,position:h(ae).Top},null,8,[`position`]),n(`div`,{class:`sm-fn-inner`,title:`${t}${e?.description?`
3
- `+e.description:``}`},[n(`div`,$e,_(t),1),e?.description?(l(),f(`div`,et,_(e.description.length>22?e.description.slice(0,22)+`…`:e.description),1)):d(``,!0)],8,Qe),u(h(fe),{type:`source`,position:h(ae).Bottom},null,8,[`position`])]),default:t(()=>[u(h(le),{variant:h(ue).Dots,gap:20,size:1,"pattern-color":Et.value},null,8,[`variant`,`pattern-color`]),u(h(ie)),u(h(de),{"node-color":`#3b82f6`,"mask-color":v.value===`dark`?`rgba(0,0,0,0.55)`:`rgba(15,23,42,0.06)`},null,8,[`mask-color`])]),_:1}),!A.value&&!Z.value?(l(),f(`div`,tt,[(l(),f(`svg`,nt,[...o[19]||=[n(`polygon`,{points:`3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21`},null,-1),n(`line`,{x1:`9`,y1:`3`,x2:`9`,y2:`18`},null,-1),n(`line`,{x1:`15`,y1:`6`,x2:`15`,y2:`21`},null,-1)]])),n(`p`,null,_(h(w)(`@SRCMAP:输入项目路径后点击开始分析`)),1)])):d(``,!0),Z.value&&!A.value?(l(),f(`div`,rt,[o[20]||=n(`span`,{class:`sm-spinner sm-spinner-lg`},null,-1),n(`p`,null,_(h(w)(`@SRCMAP:AI 正在分析项目结构...`)),1)])):d(``,!0)]),n(`div`,{class:`sm-resizer sm-resizer-h`,onMousedown:o[7]||=e=>K(`log`,e)},null,32),n(`div`,{class:`sm-log-panel`,style:C({height:H.value+`px`})},[n(`div`,it,[o[21]||=n(`svg`,{viewBox:`0 0 24 24`,width:`12`,height:`12`,fill:`none`,stroke:`currentColor`,"stroke-width":`1.8`},[n(`polyline`,{points:`4 17 10 11 4 5`}),n(`line`,{x1:`12`,y1:`19`,x2:`20`,y2:`19`})],-1),s(` `+_(h(w)(`@SRCMAP:Agent 日志`))+` `,1),Z.value?(l(),f(`span`,at)):d(``,!0)]),n(`div`,ot,[(l(!0),f(i,null,c(k.value.slice(-50),e=>(l(),f(`div`,{key:e.id,class:x([`sm-log-entry`,`sm-log-entry--${e.type}`])},_(e.message),3))),128)),k.value.length===0?(l(),f(`div`,st,_(h(w)(`@SRCMAP:等待开始分析`)),1)):d(``,!0)],512)],4)],512),[[r,R.value.graph]]),y(n(`div`,{class:`sm-resizer sm-resizer-v`,onMousedown:o[8]||=e=>K(`source`,e)},null,544),[[r,R.value.graph&&R.value.source]]),y(n(`div`,{class:`sm-panel sm-panel-source`,style:C({width:V.value+`px`})},[n(`div`,ct,[o[22]||=n(`svg`,{viewBox:`0 0 24 24`,width:`13`,height:`13`,fill:`none`,stroke:`currentColor`,"stroke-width":`1.8`},[n(`polyline`,{points:`16 18 22 12 16 6`}),n(`polyline`,{points:`8 6 2 12 8 18`})],-1),s(` `+_(P.value||h(w)(`@SRCMAP:源码面板`))+` `,1),Q.value?(l(),f(`span`,lt,_(Q.value.label),1)):d(``,!0)]),Q.value?(l(),f(`div`,ut,[n(`div`,dt,_(Q.value.label),1),n(`div`,ft,[s(_(Q.value.file),1),Q.value.line?(l(),f(`span`,pt,` :`+_(Q.value.line),1)):d(``,!0)]),Q.value.description?(l(),f(`div`,mt,_(Q.value.description),1)):d(``,!0)])):d(``,!0),n(`div`,ht,[F.value?(l(),f(`div`,gt,[...o[23]||=[n(`span`,{class:`sm-spinner`},null,-1)]])):d(``,!0),!F.value&&!N.value?(l(),f(`div`,_t,_(h(w)(`@SRCMAP:点击调用图中的节点查看源码`)),1)):d(``,!0),n(`div`,{ref_key:`monacoContainerRef`,ref:J,class:`sm-monaco-container`},null,512)])],4),[[r,R.value.source]])])])}}}),[[`__scopeId`,`data-v-71d3569e`]]);export{E as default};
@@ -1 +0,0 @@
1
- .workbench[data-v-3d63d0a0]{background:var(--bg-container);height:100%;color:var(--text-primary);display:flex}.wb-sidebar[data-v-3d63d0a0]{border-right:1px solid var(--border-color);background:var(--bg-surface);flex-shrink:0;width:260px;padding:12px;overflow-y:auto}.wb-sidebar__header[data-v-3d63d0a0]{justify-content:space-between;align-items:center;margin-bottom:8px;display:flex}.wb-sidebar__header h3[data-v-3d63d0a0]{color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;margin:0;font-size:13px;font-weight:600}.wb-sidebar__divider[data-v-3d63d0a0]{background:var(--border-color);height:1px;margin:16px 0}.wb-task-list[data-v-3d63d0a0],.wb-prompt-list[data-v-3d63d0a0]{margin:0;padding:0;list-style:none}.wb-task-item[data-v-3d63d0a0]{border-radius:var(--radius-md);cursor:pointer;border:1px solid #0000;margin-bottom:4px;padding:10px 12px;transition:background .15s,border-color .15s}.wb-task-item[data-v-3d63d0a0]:hover{background:var(--bg-hover)}.wb-task-item.active[data-v-3d63d0a0]{border-color:var(--color-primary);background:#3b82f61a}.wb-task-item__title[data-v-3d63d0a0]{margin-bottom:2px;font-size:13px;font-weight:500}.wb-task-item__meta[data-v-3d63d0a0]{color:var(--text-tertiary);justify-content:space-between;align-items:center;font-size:11px;display:flex}.wb-task-item__del[data-v-3d63d0a0],.wb-prompt-item__del[data-v-3d63d0a0],.wb-sub-item__del[data-v-3d63d0a0]{color:var(--text-tertiary);cursor:pointer;background:0 0;border:none;padding:0 4px;font-size:16px;line-height:1}.wb-task-item__del[data-v-3d63d0a0]:hover,.wb-prompt-item__del[data-v-3d63d0a0]:hover,.wb-sub-item__del[data-v-3d63d0a0]:hover{color:#ef4444}.wb-prompt-item[data-v-3d63d0a0]{border-radius:var(--radius-sm);justify-content:space-between;align-items:center;padding:6px 8px;font-size:12px;display:flex}.wb-prompt-item[data-v-3d63d0a0]:hover{background:var(--bg-hover)}.wb-prompt-item__name[data-v-3d63d0a0]{cursor:pointer;white-space:nowrap;text-overflow:ellipsis;flex:1;overflow:hidden}.wb-empty[data-v-3d63d0a0]{text-align:center;color:var(--text-tertiary);padding:16px 8px;font-size:12px}.wb-split[data-v-3d63d0a0]{flex-direction:column;flex:1;gap:12px;padding:16px 20px;display:flex;overflow-y:auto}.wb-placeholder[data-v-3d63d0a0]{color:var(--text-tertiary);flex:1;justify-content:center;align-items:center;font-size:13px;display:flex}.wb-split__header[data-v-3d63d0a0]{align-items:center;gap:8px;display:flex}.wb-input[data-v-3d63d0a0]{background:var(--bg-base);border:1px solid var(--border-color);color:var(--text-primary);border-radius:var(--radius-md);outline:none;padding:6px 10px;font-size:13px}.wb-input[data-v-3d63d0a0]:focus{border-color:var(--color-primary)}.wb-input--title[data-v-3d63d0a0]{flex:1;font-size:16px;font-weight:600}.wb-select[data-v-3d63d0a0]{background:var(--bg-base);border:1px solid var(--border-color);color:var(--text-primary);border-radius:var(--radius-md);padding:6px 8px;font-size:13px}.wb-textarea[data-v-3d63d0a0]{background:var(--bg-base);border:1px solid var(--border-color);color:var(--text-primary);border-radius:var(--radius-md);resize:vertical;box-sizing:border-box;outline:none;width:100%;padding:8px 10px;font-family:inherit;font-size:13px}.wb-textarea[data-v-3d63d0a0]:focus{border-color:var(--color-primary)}.wb-textarea--sm[data-v-3d63d0a0]{min-height:40px}.wb-split__sub-header[data-v-3d63d0a0]{justify-content:space-between;align-items:center;margin-top:8px;display:flex}.wb-split__sub-header h4[data-v-3d63d0a0]{color:var(--text-secondary);margin:0;font-size:13px;font-weight:600}.wb-sub-list[data-v-3d63d0a0]{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.wb-sub-item[data-v-3d63d0a0]{background:var(--bg-surface);border:1px solid var(--border-color);border-radius:var(--radius-md);padding:10px}.wb-sub-item__row[data-v-3d63d0a0]{align-items:center;gap:8px;margin-bottom:6px;display:flex}.wb-sub-item__status[data-v-3d63d0a0]{color:#fff;border-radius:10px;flex-shrink:0;padding:2px 8px;font-size:11px}.wb-sub-item__pid[data-v-3d63d0a0]{color:var(--text-tertiary);flex-shrink:0;font-family:ui-monospace,monospace;font-size:11px}.wb-sub-item__row .wb-input[data-v-3d63d0a0]{flex:1}.wb-log-details[data-v-3d63d0a0]{border:1px solid var(--border-color);border-radius:var(--radius-sm,4px);background:var(--bg-code);margin-top:6px;overflow:hidden}.wb-log-summary[data-v-3d63d0a0]{cursor:pointer;color:var(--text-secondary);-webkit-user-select:none;user-select:none;justify-content:space-between;align-items:center;gap:8px;padding:6px 10px;font-size:12px;list-style:none;display:flex}.wb-log-summary[data-v-3d63d0a0]::-webkit-details-marker{display:none}.wb-log-summary[data-v-3d63d0a0]:hover{background:#3b82f60f}.wb-log-summary__meta[data-v-3d63d0a0]{color:var(--text-tertiary);font-variant-numeric:tabular-nums;font-size:11px}.wb-log-pre[data-v-3d63d0a0]{max-height:240px;font-family:var(--font-mono,ui-monospace, monospace);color:var(--text-primary);background:var(--bg-code);white-space:pre-wrap;word-break:break-word;border-top:1px solid var(--border-color);margin:0;padding:8px 10px;font-size:12px;line-height:1.55;overflow:auto}.wb-attachments[data-v-3d63d0a0]{border:1px solid var(--border-color);border-radius:var(--radius-sm,4px);background:var(--bg-subtle,var(--bg-container));margin-top:6px;transition:border-color .15s,background .15s;overflow:hidden}.wb-attachments.is-paste-hover[data-v-3d63d0a0]{border-color:var(--color-primary);box-shadow:inset 0 0 0 1px var(--color-primary);background:#3b82f60f}.wb-attachments__paste-hint[data-v-3d63d0a0]{background:var(--color-primary);color:#fff;text-align:center;border-top:1px solid var(--border-color);padding:4px 10px;font-size:12px}.wb-attachments__head[data-v-3d63d0a0]{color:var(--text-secondary);border-bottom:1px solid var(--border-color);justify-content:space-between;align-items:center;padding:6px 10px;font-size:12px;display:flex}.wb-attachments__label[data-v-3d63d0a0]{align-items:center;gap:6px;display:inline-flex}.wb-attachments__count[data-v-3d63d0a0]{font-variant-numeric:tabular-nums;color:var(--text-tertiary);font-size:11px}.wb-attachments__add[data-v-3d63d0a0]{border:1px solid var(--border-color);background:var(--bg-container);color:var(--text-primary);border-radius:var(--radius-sm,4px);cursor:pointer;padding:3px 10px;font-size:12px;transition:background .15s}.wb-attachments__add[data-v-3d63d0a0]:hover:not(:disabled){border-color:var(--color-primary);color:var(--color-primary);background:#3b82f614}.wb-attachments__add[data-v-3d63d0a0]:disabled{opacity:.5;cursor:not-allowed}.wb-attachments__list[data-v-3d63d0a0]{flex-wrap:wrap;gap:4px;margin:0;padding:4px;list-style:none;display:flex}.wb-attachment[data-v-3d63d0a0]{border:1px solid var(--border-color);border-radius:var(--radius-sm,4px);background:var(--bg-container);align-items:center;gap:6px;min-width:0;max-width:240px;padding:4px 6px 4px 4px;display:flex}.wb-attachment__icon[data-v-3d63d0a0]{background:var(--bg-code);width:32px;height:32px;color:var(--text-tertiary);letter-spacing:.5px;border-radius:3px;flex-shrink:0;justify-content:center;align-items:center;font-size:10px;font-weight:600;display:flex;overflow:hidden}.wb-attachment__icon--img[data-v-3d63d0a0]{background:var(--bg-code)}.wb-attachment__icon img[data-v-3d63d0a0]{object-fit:cover;width:100%;height:100%}.wb-attachment__meta[data-v-3d63d0a0]{flex:1;min-width:0}.wb-attachment__name[data-v-3d63d0a0]{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}.wb-attachment__sub[data-v-3d63d0a0]{color:var(--text-tertiary);white-space:nowrap;text-overflow:ellipsis;font-size:10px;overflow:hidden}.wb-attachment__del[data-v-3d63d0a0]{color:var(--text-tertiary);cursor:pointer;background:0 0;border:none;border-radius:3px;flex-shrink:0;width:20px;height:20px;font-size:16px;line-height:1}.wb-attachment__del[data-v-3d63d0a0]:hover{color:#ef4444;background:#ef444414}