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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/ui/public/assets/EditorView-BbyNq-0-.js +0 -0
- package/src/ui/public/assets/SourceMapView-CEDJofhr.js +3 -0
- package/src/ui/public/assets/WorkbenchView-DEbOy4sA.js +1 -0
- package/src/ui/public/assets/WorkbenchView-DRh-K9ZG.css +1 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-opDPZuCO.js → _plugin-vue_export-helper-CIzOKVnN.js} +3 -3
- package/src/ui/public/assets/{index-tejX3YgJ.css → index-DEEcURiV.css} +1 -1
- package/src/ui/public/assets/index-DNP2rwmI.js +66 -0
- package/src/ui/public/assets/{vendor-A4IPqbyo.js → vendor-BfQR1Ql2.js} +1 -1
- package/src/ui/public/index.html +4 -4
- package/src/ui/server/routes/npm.js +170 -1
- package/src/ui/server/routes/workbench.js +381 -75
- package/src/ui/public/assets/EditorView-DkH7PwMR.js +0 -0
- package/src/ui/public/assets/SourceMapView-BG6MIju7.js +0 -3
- package/src/ui/public/assets/WorkbenchView-CDKIBwuO.css +0 -1
- package/src/ui/public/assets/WorkbenchView-DgwZOvhW.js +0 -1
- package/src/ui/public/assets/index-DPihtJfT.js +0 -65
|
@@ -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:
|
|
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(),
|
|
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
|
-
'--
|
|
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
|
-
//
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
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',
|
|
371
|
-
if (child.stderr) child.stderr.on('data',
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
464
|
-
|
|
674
|
+
// ── 第一阶段:基于可编辑指令 + 根目录概览,生成「可复用的提示词模板」 ──
|
|
675
|
+
const overviewBlock = subProjects.map(sp =>
|
|
676
|
+
`### 子项目 ${sp.name} (${sp.root})\n目录:\n${sp.dirTree || '(无)'}`
|
|
677
|
+
).join('\n\n');
|
|
465
678
|
|
|
466
|
-
|
|
467
|
-
${dirTree || '(无)'}
|
|
679
|
+
const firstPrompt = `${userInstruction}
|
|
468
680
|
|
|
469
|
-
|
|
470
|
-
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
以下是你需要分析的项目(请先生成「可复用的提示词模板」,不要直接给总结):
|
|
471
684
|
|
|
472
|
-
|
|
473
|
-
|
|
685
|
+
项目根目录:${projectPath}
|
|
686
|
+
项目名称:${projectName}
|
|
687
|
+
子项目数:${subProjects.length}
|
|
474
688
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
这段模板将作为指令注入到大模型的 system prompt 中,用来指导大模型对**当前项目**做「项目架构总结」。
|
|
689
|
+
## 子项目概览
|
|
690
|
+
${overviewBlock || '(无)'}
|
|
478
691
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
700
|
+
只返回 JSON:
|
|
492
701
|
{
|
|
493
|
-
"name": "
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
512
|
-
|
|
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
|
-
##
|
|
727
|
+
## manifest
|
|
521
728
|
${manifestBlock || '(无)'}
|
|
522
729
|
|
|
523
|
-
|
|
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
|
-
|
|
530
|
-
|
|
747
|
+
if (subSummaries.length === 1) {
|
|
748
|
+
finalSummary = subSummaries[0].summary;
|
|
749
|
+
} else {
|
|
750
|
+
const mergePrompt = `你是项目架构师。下列是同一仓库下 N 个子项目的架构说明,请合并输出**单一**的「项目架构说明」(800-1500字),覆盖:项目整体定位、技术栈、模块划分、子项目间关系、核心流程、关键设计决策。
|
|
751
|
+
子项目之间用清晰的小标题或编号分隔。最后输出一段「整体架构」总结它们如何协同。
|
|
752
|
+
只引用实际出现的子项目名 / 文件路径 / 依赖名,不要编造。只返回 JSON:
|
|
531
753
|
|
|
532
|
-
|
|
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:
|
|
772
|
+
name: finalName,
|
|
537
773
|
template,
|
|
538
774
|
result: '',
|
|
539
775
|
content: template
|
|
540
776
|
});
|
|
541
777
|
}
|
|
542
778
|
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
779
|
+
// 顶层 request 已经自带 20 分钟(1200s)超时;
|
|
780
|
+
// 这里在 express 处理器内部不再额外加整体超时。
|
|
546
781
|
res.json({
|
|
547
782
|
success: true,
|
|
548
|
-
name:
|
|
783
|
+
name: finalName,
|
|
549
784
|
template,
|
|
550
|
-
result:
|
|
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
|
|
Binary file
|
|
@@ -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}
|