dsh-github-flow 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +75 -8
- package/README.md +75 -8
- package/docs/UPGRADE-NOTES.md +9 -12
- package/docs/architecture.md +39 -21
- package/lib/api/routes-actions.js +132 -0
- package/lib/client.d.ts +4 -3
- package/lib/client.js +1436 -80
- package/lib/commands/gh.d.ts +6 -1
- package/lib/commands/gh.js +144 -71
- package/lib/executor/git-parser.d.ts +33 -1
- package/lib/executor/git-parser.js +116 -4
- package/lib/executor.d.ts +27 -2
- package/lib/executor.js +189 -5
- package/lib/main.js +28 -4
- package/lib/renderers/agent-task.d.ts +16 -0
- package/lib/renderers/agent-task.js +107 -0
- package/lib/renderers/codespace.d.ts +16 -0
- package/lib/renderers/codespace.js +103 -0
- package/lib/renderers/discussion.d.ts +16 -0
- package/lib/renderers/discussion.js +101 -0
- package/lib/renderers/markdown.d.ts +4 -0
- package/lib/renderers/markdown.js +4 -0
- package/lib/renderers/project.d.ts +20 -0
- package/lib/renderers/project.js +128 -0
- package/lib/renderers/skill.d.ts +30 -0
- package/lib/renderers/skill.js +180 -0
- package/lib/renderers/status.d.ts +5 -0
- package/lib/renderers/status.js +52 -0
- package/lib/telemetry.d.ts +70 -0
- package/lib/telemetry.js +161 -0
- package/lib/tools/agent-task.d.ts +54 -0
- package/lib/tools/agent-task.js +135 -0
- package/lib/tools/codespace.d.ts +51 -0
- package/lib/tools/codespace.js +154 -0
- package/lib/tools/discussion.d.ts +70 -0
- package/lib/tools/discussion.js +195 -0
- package/lib/tools/help.js +109 -9
- package/lib/tools/project.d.ts +80 -0
- package/lib/tools/project.js +241 -0
- package/lib/tools/repo.js +13 -1
- package/lib/tools/skill.d.ts +24 -1
- package/lib/tools/skill.js +115 -5
- package/lib/tools/status.d.ts +3 -2
- package/lib/tools/status.js +9 -4
- package/lib/types.d.ts +5 -0
- package/lib/ui/commit-modal.js +144 -14
- package/lib/ui/help-modal.d.ts +15 -0
- package/lib/ui/help-modal.js +202 -0
- package/lib/ui/storage.d.ts +9 -0
- package/lib/ui/storage.js +24 -0
- package/lib/ui/styles.d.ts +1 -1
- package/lib/ui/styles.js +259 -0
- package/lib/ui/sync-settings-modal.js +1 -1
- package/lib/ui/upgrade-card.js +39 -14
- package/lib/ui/utils.d.ts +35 -0
- package/lib/ui/utils.js +160 -0
- package/lib/ui/workspace-panel.js +30 -5
- package/lib/upgrade-manager.d.ts +5 -1
- package/lib/upgrade-manager.js +24 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +2 -2
package/lib/executor.js
CHANGED
|
@@ -7,9 +7,9 @@ import { findMostRecentWorkspacePath, findWorkspacePathBySession, hasGitDir } fr
|
|
|
7
7
|
// 子模块纯函数解耦导入并对外部 Re-export 保证 100% 向后兼容
|
|
8
8
|
import { parseGhVersion, compareGhVersions } from './executor/version-utils.js';
|
|
9
9
|
import { spawnWithStdin, safeSlice, cutByUtf8Bytes, truncateOutput } from './executor/process-utils.js';
|
|
10
|
-
import { parseGitStatusOutput, parseGitLogLines, parseGitBranchLines } from './executor/git-parser.js';
|
|
10
|
+
import { parseGitStatusOutput, parseGitLogLines, parseGitBranchLines, parseUncommittedDiff, parseGitStatText } from './executor/git-parser.js';
|
|
11
11
|
import { getPlatformInstallGuide, getGhUpgradeHint } from './executor/platform-guide.js';
|
|
12
|
-
export { parseGhVersion, compareGhVersions, spawnWithStdin, safeSlice, cutByUtf8Bytes, truncateOutput, parseGitStatusOutput, parseGitLogLines, parseGitBranchLines, getPlatformInstallGuide, getGhUpgradeHint, };
|
|
12
|
+
export { parseGhVersion, compareGhVersions, spawnWithStdin, safeSlice, cutByUtf8Bytes, truncateOutput, parseGitStatusOutput, parseGitLogLines, parseGitBranchLines, parseUncommittedDiff, parseGitStatText, getPlatformInstallGuide, getGhUpgradeHint, };
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
14
14
|
/** 认证状态缓存时长。用户手动 `gh auth login/logout` 后最多 30 秒内面板才会反映,属可接受代价 */
|
|
15
15
|
const AUTH_CACHE_TTL_MS = 30_000;
|
|
@@ -205,9 +205,19 @@ export class GhExecutor {
|
|
|
205
205
|
friendlyError = stderrText;
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
+
// 失败时若子进程输出了 stdout(如 gh skill publish 校验未通过时的 TSV 诊断行),
|
|
209
|
+
// 予以保留并应用同样的字符/字节截断预算,以便上层渲染器结构化解析诊断信息,避免丢失关键现场
|
|
210
|
+
let rawOutput;
|
|
211
|
+
if (err.stdout) {
|
|
212
|
+
const outText = Buffer.isBuffer(err.stdout) ? err.stdout.toString('utf8') : String(err.stdout);
|
|
213
|
+
if (outText.trim()) {
|
|
214
|
+
rawOutput = truncateOutput(outText.trim(), this.maxOutputChars, this.maxOutputBytes).text;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
208
217
|
return {
|
|
209
218
|
ok: false,
|
|
210
219
|
error: friendlyError,
|
|
220
|
+
rawOutput,
|
|
211
221
|
};
|
|
212
222
|
}
|
|
213
223
|
}
|
|
@@ -333,13 +343,183 @@ export class GhExecutor {
|
|
|
333
343
|
timeout: 3_000,
|
|
334
344
|
windowsHide: true,
|
|
335
345
|
});
|
|
336
|
-
return parseGitStatusOutput(String(stdout));
|
|
346
|
+
return parseGitStatusOutput(String(stdout), 2000);
|
|
337
347
|
}
|
|
338
348
|
catch {
|
|
339
349
|
// 非 Git 目录 / git 未安装 / 超时:静默降级为 null(调用方不渲染该行)
|
|
340
350
|
return null;
|
|
341
351
|
}
|
|
342
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* 分页获取本地未提交文件列表(无网络开销,纯本地毫秒级响应)
|
|
355
|
+
*/
|
|
356
|
+
async getUncommittedFiles(cwd, limit = 15, skip = 0) {
|
|
357
|
+
const targetCwd = this.resolveWorkspaceCwd(cwd);
|
|
358
|
+
try {
|
|
359
|
+
const { stdout } = await execFileAsync('git', ['status', '-sb', '--porcelain'], {
|
|
360
|
+
cwd: targetCwd || undefined,
|
|
361
|
+
timeout: 3_000,
|
|
362
|
+
windowsHide: true,
|
|
363
|
+
});
|
|
364
|
+
// 允许解析上限 2000 个文件
|
|
365
|
+
const parsed = parseGitStatusOutput(String(stdout), 2000);
|
|
366
|
+
if (!parsed) {
|
|
367
|
+
return { items: [], totalCount: 0, hasMore: false };
|
|
368
|
+
}
|
|
369
|
+
const allFiles = parsed.dirtyFiles || [];
|
|
370
|
+
const totalCount = parsed.dirtyCount;
|
|
371
|
+
const safeSkip = Math.max(0, skip);
|
|
372
|
+
const safeLimit = Math.max(1, limit);
|
|
373
|
+
const items = allFiles.slice(safeSkip, safeSkip + safeLimit);
|
|
374
|
+
const hasMore = safeSkip + items.length < totalCount;
|
|
375
|
+
return { items, totalCount, hasMore };
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return { items: [], totalCount: 0, hasMore: false };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 获取指定文件的未提交 Git Diff 对比(Unified Diff 格式)。
|
|
383
|
+
* 严格实施路径穿越安全校验;支持已跟踪修改文件、暂存文件与未跟踪新增文件(??)。
|
|
384
|
+
*/
|
|
385
|
+
async getFileDiff(cwd, relativePath) {
|
|
386
|
+
const targetCwd = this.resolveWorkspaceCwd(cwd);
|
|
387
|
+
if (!relativePath || typeof relativePath !== 'string') {
|
|
388
|
+
return { ok: false, filePath: '', error: '缺少目标文件路径' };
|
|
389
|
+
}
|
|
390
|
+
// 路径安全防线:严格防止路径穿越
|
|
391
|
+
const cleanRel = relativePath.trim().replace(/\\/g, '/');
|
|
392
|
+
const resolvedTarget = path.resolve(targetCwd);
|
|
393
|
+
const resolvedFile = path.resolve(targetCwd, cleanRel);
|
|
394
|
+
if (!resolvedFile.startsWith(resolvedTarget + path.sep) && resolvedFile !== resolvedTarget) {
|
|
395
|
+
return { ok: false, filePath: cleanRel, error: '非法路径,禁止访问工作区外部文件' };
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
// 1. 先用 porcelain status 获取该文件的状态码
|
|
399
|
+
let fileStatus = 'M';
|
|
400
|
+
try {
|
|
401
|
+
const { stdout: statusOut } = await execFileAsync('git', ['status', '--porcelain', '--', cleanRel], {
|
|
402
|
+
cwd: targetCwd || undefined,
|
|
403
|
+
timeout: 2_000,
|
|
404
|
+
windowsHide: true,
|
|
405
|
+
});
|
|
406
|
+
const trimmed = String(statusOut).trim();
|
|
407
|
+
if (trimmed) {
|
|
408
|
+
fileStatus = trimmed.slice(0, 2).trim() || 'M';
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch { }
|
|
412
|
+
// 2. 如果是未跟踪新增文件 (??)
|
|
413
|
+
if (fileStatus === '??') {
|
|
414
|
+
if (!fs.existsSync(resolvedFile)) {
|
|
415
|
+
return { ok: false, filePath: cleanRel, error: '目标文件不存在' };
|
|
416
|
+
}
|
|
417
|
+
const stat = fs.statSync(resolvedFile);
|
|
418
|
+
if (stat.isDirectory()) {
|
|
419
|
+
return { ok: false, filePath: cleanRel, error: '目标路径为目录而非文件' };
|
|
420
|
+
}
|
|
421
|
+
// 读取文件前 8KB 探测是否为二进制
|
|
422
|
+
const fd = fs.openSync(resolvedFile, 'r');
|
|
423
|
+
const probeBuf = Buffer.alloc(Math.min(stat.size, 8192));
|
|
424
|
+
fs.readSync(fd, probeBuf, 0, probeBuf.length, 0);
|
|
425
|
+
fs.closeSync(fd);
|
|
426
|
+
if (probeBuf.includes(0)) {
|
|
427
|
+
return {
|
|
428
|
+
ok: true,
|
|
429
|
+
filePath: cleanRel,
|
|
430
|
+
status: '??',
|
|
431
|
+
isBinary: true,
|
|
432
|
+
diff: 'Binary file (untracked)',
|
|
433
|
+
additions: 0,
|
|
434
|
+
deletions: 0,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const content = fs.readFileSync(resolvedFile, 'utf8');
|
|
438
|
+
const lines = content.split('\n');
|
|
439
|
+
// 截断保护:最多展示 1500 行防卡死
|
|
440
|
+
const maxLines = 1500;
|
|
441
|
+
const truncated = lines.length > maxLines;
|
|
442
|
+
const displayLines = truncated ? lines.slice(0, maxLines) : lines;
|
|
443
|
+
const diffBody = displayLines.map((l) => '+' + l).join('\n') + (truncated ? '\n... [差异过大,已截断展示前 1500 行]' : '');
|
|
444
|
+
const fullDiff = `diff --git a/${cleanRel} b/${cleanRel}\nnew file mode 100644\n--- /dev/null\n+++ b/${cleanRel}\n@@ -0,0 +1,${lines.length} @@\n${diffBody}`;
|
|
445
|
+
return {
|
|
446
|
+
ok: true,
|
|
447
|
+
filePath: cleanRel,
|
|
448
|
+
status: '??',
|
|
449
|
+
diff: fullDiff,
|
|
450
|
+
additions: lines.length,
|
|
451
|
+
deletions: 0,
|
|
452
|
+
isBinary: false,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
// 3. 已跟踪文件(M, D, A 等):执行 git diff HEAD -- <cleanRel>
|
|
456
|
+
let rawDiff = '';
|
|
457
|
+
try {
|
|
458
|
+
const { stdout } = await execFileAsync('git', ['diff', 'HEAD', '--', cleanRel], {
|
|
459
|
+
cwd: targetCwd || undefined,
|
|
460
|
+
timeout: 4_000,
|
|
461
|
+
windowsHide: true,
|
|
462
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
463
|
+
});
|
|
464
|
+
rawDiff = String(stdout);
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// 无 HEAD 场景(空仓库初次提交前),优雅降级为 git diff -- <cleanRel>
|
|
468
|
+
try {
|
|
469
|
+
const { stdout } = await execFileAsync('git', ['diff', '--', cleanRel], {
|
|
470
|
+
cwd: targetCwd || undefined,
|
|
471
|
+
timeout: 4_000,
|
|
472
|
+
windowsHide: true,
|
|
473
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
474
|
+
});
|
|
475
|
+
rawDiff = String(stdout);
|
|
476
|
+
}
|
|
477
|
+
catch { }
|
|
478
|
+
}
|
|
479
|
+
// 如果 diff 为空但处于已删除状态(D),或者在 staged 中
|
|
480
|
+
if (!rawDiff) {
|
|
481
|
+
try {
|
|
482
|
+
const { stdout: stagedDiff } = await execFileAsync('git', ['diff', '--cached', '--', cleanRel], {
|
|
483
|
+
cwd: targetCwd || undefined,
|
|
484
|
+
timeout: 3_000,
|
|
485
|
+
windowsHide: true,
|
|
486
|
+
});
|
|
487
|
+
rawDiff = String(stagedDiff);
|
|
488
|
+
}
|
|
489
|
+
catch { }
|
|
490
|
+
}
|
|
491
|
+
if (rawDiff.includes('Binary files differ') || rawDiff.includes('GIT binary patch')) {
|
|
492
|
+
return {
|
|
493
|
+
ok: true,
|
|
494
|
+
filePath: cleanRel,
|
|
495
|
+
status: fileStatus,
|
|
496
|
+
isBinary: true,
|
|
497
|
+
diff: 'Binary files differ',
|
|
498
|
+
additions: 0,
|
|
499
|
+
deletions: 0,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const diffStats = parseUncommittedDiff(rawDiff);
|
|
503
|
+
// 截断保护:行数超过 1500 行时截断
|
|
504
|
+
const diffLines = rawDiff.split('\n');
|
|
505
|
+
let finalDiff = rawDiff;
|
|
506
|
+
if (diffLines.length > 1500) {
|
|
507
|
+
finalDiff = diffLines.slice(0, 1500).join('\n') + '\n... [差异过大,已截断展示前 1500 行]';
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
ok: true,
|
|
511
|
+
filePath: cleanRel,
|
|
512
|
+
status: fileStatus,
|
|
513
|
+
diff: finalDiff || '没有检测到文本差异',
|
|
514
|
+
additions: diffStats.additions,
|
|
515
|
+
deletions: diffStats.deletions,
|
|
516
|
+
isBinary: false,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
return { ok: false, filePath: cleanRel, error: err?.message || '获取文件差异失败' };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
343
523
|
/**
|
|
344
524
|
* 本地 Git 提交记录(三段一次返回:本地领先 / 远程领先 / 最近提交)。
|
|
345
525
|
*
|
|
@@ -695,7 +875,7 @@ export class GhExecutor {
|
|
|
695
875
|
return null;
|
|
696
876
|
}
|
|
697
877
|
// 1. 优先尝试由 gh 直接根据当前上下文识别
|
|
698
|
-
const res = await this.run(['repo', 'view', '--json', 'nameWithOwner,name,owner,defaultBranchRef,description,isPrivate,url'], { cwd: targetCwd, timeoutMs: 10_000 });
|
|
878
|
+
const res = await this.run(['repo', 'view', '--json', 'nameWithOwner,name,owner,defaultBranchRef,description,isPrivate,url,stargazerCount,forkCount'], { cwd: targetCwd, timeoutMs: 10_000 });
|
|
699
879
|
if (res.ok && res.data && res.data.nameWithOwner) {
|
|
700
880
|
const d = res.data;
|
|
701
881
|
return {
|
|
@@ -706,6 +886,8 @@ export class GhExecutor {
|
|
|
706
886
|
description: d.description || '',
|
|
707
887
|
isPrivate: Boolean(d.isPrivate),
|
|
708
888
|
url: d.url || `https://github.com/${d.nameWithOwner}`,
|
|
889
|
+
stargazerCount: typeof d.stargazerCount === 'number' ? d.stargazerCount : 0,
|
|
890
|
+
forkCount: typeof d.forkCount === 'number' ? d.forkCount : 0,
|
|
709
891
|
};
|
|
710
892
|
}
|
|
711
893
|
// 2. 原生文件解析:直接读取本地 .git/config 文件(0 子进程开销、免疫沙箱 EPERM)
|
|
@@ -719,7 +901,7 @@ export class GhExecutor {
|
|
|
719
901
|
const repoName = match[2];
|
|
720
902
|
const nameWithOwner = `${owner}/${repoName}`;
|
|
721
903
|
// 使用精确的 owner/repo 向 gh 获取元数据(显式传参,不依赖无上下文的探测)
|
|
722
|
-
const directRes = await this.run(['repo', 'view', nameWithOwner, '--json', 'nameWithOwner,name,owner,defaultBranchRef,description,isPrivate,url'], { cwd: targetCwd, timeoutMs: 10_000 });
|
|
904
|
+
const directRes = await this.run(['repo', 'view', nameWithOwner, '--json', 'nameWithOwner,name,owner,defaultBranchRef,description,isPrivate,url,stargazerCount,forkCount'], { cwd: targetCwd, timeoutMs: 10_000 });
|
|
723
905
|
if (directRes.ok && directRes.data && directRes.data.nameWithOwner) {
|
|
724
906
|
const d = directRes.data;
|
|
725
907
|
return {
|
|
@@ -730,6 +912,8 @@ export class GhExecutor {
|
|
|
730
912
|
description: d.description || '',
|
|
731
913
|
isPrivate: Boolean(d.isPrivate),
|
|
732
914
|
url: d.url || `https://github.com/${nameWithOwner}`,
|
|
915
|
+
stargazerCount: typeof d.stargazerCount === 'number' ? d.stargazerCount : 0,
|
|
916
|
+
forkCount: typeof d.forkCount === 'number' ? d.forkCount : 0,
|
|
733
917
|
};
|
|
734
918
|
}
|
|
735
919
|
// 若离线或网络受限,由本地 git remote 配置兜底返回
|
package/lib/main.js
CHANGED
|
@@ -12,7 +12,12 @@ import { createRulesetTool } from './tools/ruleset.js';
|
|
|
12
12
|
import { createCacheTool } from './tools/cache.js';
|
|
13
13
|
import { createGistTool } from './tools/gist.js';
|
|
14
14
|
import { createStatusTool } from './tools/status.js';
|
|
15
|
+
import { createAgentTaskTool } from './tools/agent-task.js';
|
|
16
|
+
import { createDiscussionTool } from './tools/discussion.js';
|
|
17
|
+
import { createProjectTool } from './tools/project.js';
|
|
18
|
+
import { createCodespaceTool } from './tools/codespace.js';
|
|
15
19
|
import { createHelpTool } from './tools/help.js';
|
|
20
|
+
import { defaultTelemetryCollector } from './telemetry.js';
|
|
16
21
|
import { registerGhCommand } from './commands/gh.js';
|
|
17
22
|
import { registerApiRoutes } from './api/routes.js';
|
|
18
23
|
import { UpdateChecker } from './updater.js';
|
|
@@ -88,11 +93,26 @@ export function apply(ctx, config = {}) {
|
|
|
88
93
|
else {
|
|
89
94
|
ctx.github = executor;
|
|
90
95
|
}
|
|
91
|
-
// 2.
|
|
96
|
+
// 2. 注册 Cordis tools/result 事件切面(无侵入全量工具审计与降级率打点)
|
|
97
|
+
if (ctx.on && typeof ctx.on === 'function') {
|
|
98
|
+
ctx.on('tools/result', (exec, result) => {
|
|
99
|
+
try {
|
|
100
|
+
const toolName = exec?.name || exec?.tool?.name || '';
|
|
101
|
+
if (typeof toolName === 'string' && toolName.startsWith('github_')) {
|
|
102
|
+
const args = exec?.arguments || exec?.args || {};
|
|
103
|
+
defaultTelemetryCollector.recordCall(toolName, args, result);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Fail-Open 非阻塞防线:严禁打点逻辑异常影响主工具调用
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// 3. 注册人类 Slash 命令 (/gh status, /gh upgrade, /gh help)
|
|
92
112
|
if (ctx.commands) {
|
|
93
|
-
registerGhCommand(ctx.commands, executor, updateChecker);
|
|
113
|
+
registerGhCommand(ctx.commands, executor, updateChecker, defaultTelemetryCollector);
|
|
94
114
|
}
|
|
95
|
-
//
|
|
115
|
+
// 4. 注册 Agent 模型工具集(核心领域工具 + 元指南)
|
|
96
116
|
if (ctx.tools) {
|
|
97
117
|
ctx.tools.register(createPrTool(executor));
|
|
98
118
|
ctx.tools.register(createIssueTool(executor));
|
|
@@ -106,7 +126,11 @@ export function apply(ctx, config = {}) {
|
|
|
106
126
|
ctx.tools.register(createRulesetTool(executor));
|
|
107
127
|
ctx.tools.register(createCacheTool(executor));
|
|
108
128
|
ctx.tools.register(createGistTool(executor));
|
|
109
|
-
ctx.tools.register(createStatusTool(executor));
|
|
129
|
+
ctx.tools.register(createStatusTool(executor, defaultTelemetryCollector));
|
|
130
|
+
ctx.tools.register(createAgentTaskTool(executor));
|
|
131
|
+
ctx.tools.register(createDiscussionTool(executor));
|
|
132
|
+
ctx.tools.register(createProjectTool(executor));
|
|
133
|
+
ctx.tools.register(createCodespaceTool(executor));
|
|
110
134
|
ctx.tools.register(createHelpTool(executor));
|
|
111
135
|
}
|
|
112
136
|
// 4. 注册面向 Client 端的 WebServer HTTP 路由(如果当前环境启用了 Web UI 服务)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 格式化 Agent Task 状态徽标
|
|
3
|
+
*/
|
|
4
|
+
export declare function formatAgentTaskStateBadge(state?: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* 渲染 Agent Tasks 列表表格
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderAgentTaskList(items: any[]): string;
|
|
9
|
+
/**
|
|
10
|
+
* 渲染单个 Agent Task 详情卡片
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderAgentTaskView(task: any): string;
|
|
13
|
+
/**
|
|
14
|
+
* Agent Task 领域综合渲染入口
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderAgentTaskMarkdown(args: any, value: any): string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { sanitizeCell } from './common.js';
|
|
2
|
+
/**
|
|
3
|
+
* 格式化 Agent Task 状态徽标
|
|
4
|
+
*/
|
|
5
|
+
export function formatAgentTaskStateBadge(state) {
|
|
6
|
+
if (!state)
|
|
7
|
+
return '⚪ UNKNOWN';
|
|
8
|
+
const upper = state.toUpperCase();
|
|
9
|
+
switch (upper) {
|
|
10
|
+
case 'COMPLETED':
|
|
11
|
+
case 'SUCCESS':
|
|
12
|
+
return '🟢 COMPLETED';
|
|
13
|
+
case 'IN_PROGRESS':
|
|
14
|
+
case 'RUNNING':
|
|
15
|
+
return '🟡 RUNNING';
|
|
16
|
+
case 'FAILED':
|
|
17
|
+
case 'FAILURE':
|
|
18
|
+
return '🔴 FAILED';
|
|
19
|
+
case 'CANCELLED':
|
|
20
|
+
case 'CANCELED':
|
|
21
|
+
return '⚪ CANCELLED';
|
|
22
|
+
default:
|
|
23
|
+
return `⚪ ${upper}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 渲染 Agent Tasks 列表表格
|
|
28
|
+
*/
|
|
29
|
+
export function renderAgentTaskList(items) {
|
|
30
|
+
const list = items.filter(Boolean);
|
|
31
|
+
if (list.length === 0) {
|
|
32
|
+
return '📭 暂无智能体任务 (Agent Tasks)。';
|
|
33
|
+
}
|
|
34
|
+
const rows = list.map((t) => {
|
|
35
|
+
const rawId = String(t.id || '-');
|
|
36
|
+
const shortId = rawId.length > 8 ? `\`${rawId.slice(0, 8)}...\`` : `\`${rawId}\``;
|
|
37
|
+
const name = sanitizeCell(t.name || '(无描述)', 40);
|
|
38
|
+
const stateBadge = formatAgentTaskStateBadge(t.state);
|
|
39
|
+
let prCell = '-';
|
|
40
|
+
if (t.pullRequestNumber) {
|
|
41
|
+
const prText = `#${t.pullRequestNumber}`;
|
|
42
|
+
prCell = t.pullRequestUrl ? `[${prText}](${t.pullRequestUrl})` : prText;
|
|
43
|
+
}
|
|
44
|
+
const created = t.createdAt ? t.createdAt.split('T')[0] : '-';
|
|
45
|
+
return `| ${shortId} | ${name} | ${stateBadge} | ${prCell} | ${created} |`;
|
|
46
|
+
});
|
|
47
|
+
return [
|
|
48
|
+
`**智能体任务列表 (${list.length} 条):**\n`,
|
|
49
|
+
'| 任务 ID | 描述 | 状态 | 关联 PR | 创建日期 |',
|
|
50
|
+
'| :--- | :--- | :--- | :--- | :--- |',
|
|
51
|
+
...rows,
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 渲染单个 Agent Task 详情卡片
|
|
56
|
+
*/
|
|
57
|
+
export function renderAgentTaskView(task) {
|
|
58
|
+
if (!task || typeof task !== 'object') {
|
|
59
|
+
return '无任务详情数据';
|
|
60
|
+
}
|
|
61
|
+
const name = task.name || '(未命名任务)';
|
|
62
|
+
const stateBadge = formatAgentTaskStateBadge(task.state);
|
|
63
|
+
const repo = task.repository ? `[${task.repository}](https://github.com/${task.repository})` : '-';
|
|
64
|
+
const user = task.user?.login || task.user || '-';
|
|
65
|
+
const created = task.createdAt || '-';
|
|
66
|
+
const completed = task.completedAt || '-';
|
|
67
|
+
const lines = [
|
|
68
|
+
`### 🤖 Agent Task: ${name}\n`,
|
|
69
|
+
`- **任务 ID**: \`${task.id || '-'}\``,
|
|
70
|
+
`- **状态**: ${stateBadge}`,
|
|
71
|
+
`- **目标仓库**: ${repo}`,
|
|
72
|
+
`- **分派用户**: @${user}`,
|
|
73
|
+
`- **创建时间**: ${created}`,
|
|
74
|
+
`- **完成时间**: ${completed}`,
|
|
75
|
+
];
|
|
76
|
+
if (task.pullRequestNumber) {
|
|
77
|
+
const prTitle = task.pullRequestTitle ? ` - ${task.pullRequestTitle}` : '';
|
|
78
|
+
const prLink = task.pullRequestUrl
|
|
79
|
+
? `[#${task.pullRequestNumber}${prTitle}](${task.pullRequestUrl})`
|
|
80
|
+
: `#${task.pullRequestNumber}${prTitle}`;
|
|
81
|
+
lines.push(`- **关联 PR**: ${prLink} (${task.pullRequestState || 'OPEN'})`);
|
|
82
|
+
}
|
|
83
|
+
if (task.logs || task.log) {
|
|
84
|
+
lines.push('\n**会话日志记录:**', '```text', String(task.logs || task.log).trim(), '```');
|
|
85
|
+
}
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Agent Task 领域综合渲染入口
|
|
90
|
+
*/
|
|
91
|
+
export function renderAgentTaskMarkdown(args, value) {
|
|
92
|
+
if (!value)
|
|
93
|
+
return '无返回内容';
|
|
94
|
+
if (typeof value.result === 'string')
|
|
95
|
+
return value.result;
|
|
96
|
+
if (typeof value.message === 'string' && !value.items) {
|
|
97
|
+
const extra = value.detail || value.output || value.url;
|
|
98
|
+
return `**${value.message}**` + (extra ? `\n\`\`\`\n${extra}\n\`\`\`` : '');
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value.items) && args?.action === 'list') {
|
|
101
|
+
return renderAgentTaskList(value.items);
|
|
102
|
+
}
|
|
103
|
+
if (args?.action === 'view' || value.id) {
|
|
104
|
+
return renderAgentTaskView(value);
|
|
105
|
+
}
|
|
106
|
+
return JSON.stringify(value, null, 2);
|
|
107
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 格式化 Codespace 状态徽标
|
|
3
|
+
*/
|
|
4
|
+
export declare function formatCodespaceStateBadge(state?: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* 渲染 Codespaces 列表表格
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderCodespaceList(items: any[]): string;
|
|
9
|
+
/**
|
|
10
|
+
* 渲染单个 Codespace 详情卡片
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderCodespaceView(cs: any): string;
|
|
13
|
+
/**
|
|
14
|
+
* Codespace 领域综合渲染入口
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderCodespaceMarkdown(args: any, value: any): string;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { sanitizeCell } from './common.js';
|
|
2
|
+
import { renderScopeRequiredMarkdown } from './project.js';
|
|
3
|
+
/**
|
|
4
|
+
* 格式化 Codespace 状态徽标
|
|
5
|
+
*/
|
|
6
|
+
export function formatCodespaceStateBadge(state) {
|
|
7
|
+
if (!state)
|
|
8
|
+
return '⚪ UNKNOWN';
|
|
9
|
+
const lower = state.toLowerCase();
|
|
10
|
+
if (lower === 'available')
|
|
11
|
+
return '🟢 AVAILABLE';
|
|
12
|
+
if (lower === 'shutdown')
|
|
13
|
+
return '⚪ SHUTDOWN';
|
|
14
|
+
if (lower.includes('start') || lower.includes('rebuild'))
|
|
15
|
+
return '🟡 STARTING';
|
|
16
|
+
if (lower.includes('updat') || lower.includes('export'))
|
|
17
|
+
return '🟡 BUSY';
|
|
18
|
+
return `⚪ ${state.toUpperCase()}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 渲染 Codespaces 列表表格
|
|
22
|
+
*/
|
|
23
|
+
export function renderCodespaceList(items) {
|
|
24
|
+
const list = items.filter(Boolean);
|
|
25
|
+
if (list.length === 0) {
|
|
26
|
+
return '📭 暂无云端开发环境 (Codespaces)。';
|
|
27
|
+
}
|
|
28
|
+
const rows = list.map((cs) => {
|
|
29
|
+
const name = `\`${sanitizeCell(cs.name || '-', 25)}\``;
|
|
30
|
+
const displayName = sanitizeCell(cs.displayName || cs.name || '-', 25);
|
|
31
|
+
const repo = cs.repository ? `[${cs.repository}](https://github.com/${cs.repository})` : '-';
|
|
32
|
+
const stateBadge = formatCodespaceStateBadge(cs.state);
|
|
33
|
+
const machine = sanitizeCell(cs.machineDisplayName || cs.machineName || '-', 20);
|
|
34
|
+
const lastUsed = cs.lastUsedAt ? cs.lastUsedAt.split('T')[0] : '-';
|
|
35
|
+
return `| ${name} | ${displayName} | ${repo} | ${stateBadge} | ${machine} | ${lastUsed} |`;
|
|
36
|
+
});
|
|
37
|
+
return [
|
|
38
|
+
`**云端开发环境列表 (${list.length} 台):**\n`,
|
|
39
|
+
'| 实例标识 | 显示名称 | 关联仓库 | 运行状态 | 机器规格 | 上次使用 |',
|
|
40
|
+
'| :--- | :--- | :--- | :--- | :--- | :--- |',
|
|
41
|
+
...rows,
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 渲染单个 Codespace 详情卡片
|
|
46
|
+
*/
|
|
47
|
+
export function renderCodespaceView(cs) {
|
|
48
|
+
if (!cs || typeof cs !== 'object') {
|
|
49
|
+
return '无云端环境详情数据';
|
|
50
|
+
}
|
|
51
|
+
const name = cs.displayName || cs.name || '未命名实例';
|
|
52
|
+
const stateBadge = formatCodespaceStateBadge(cs.state);
|
|
53
|
+
const repo = cs.repository ? `[${cs.repository}](https://github.com/${cs.repository})` : '-';
|
|
54
|
+
const owner = cs.owner || cs.billableOwner || '-';
|
|
55
|
+
const machine = cs.machineDisplayName || cs.machineName || '-';
|
|
56
|
+
const created = cs.createdAt || '-';
|
|
57
|
+
const lastUsed = cs.lastUsedAt || '-';
|
|
58
|
+
const lines = [
|
|
59
|
+
`### ☁️ Codespace: ${name}\n`,
|
|
60
|
+
`- **实例标识**: \`${cs.name || '-'}\``,
|
|
61
|
+
`- **运行状态**: ${stateBadge}`,
|
|
62
|
+
`- **关联仓库**: ${repo}`,
|
|
63
|
+
`- **所有者**: @${owner}`,
|
|
64
|
+
`- **机器规格**: ${machine}`,
|
|
65
|
+
`- **空闲停机**: ${cs.idleTimeoutMinutes ? `${cs.idleTimeoutMinutes} 分钟` : '默认'}`,
|
|
66
|
+
`- **创建时间**: ${created}`,
|
|
67
|
+
`- **上次活跃**: ${lastUsed}`,
|
|
68
|
+
];
|
|
69
|
+
if (cs.devcontainerPath) {
|
|
70
|
+
lines.push(`- **容器配置**: \`${cs.devcontainerPath}\``);
|
|
71
|
+
}
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Codespace 领域综合渲染入口
|
|
76
|
+
*/
|
|
77
|
+
export function renderCodespaceMarkdown(args, value) {
|
|
78
|
+
if (!value)
|
|
79
|
+
return '无返回内容';
|
|
80
|
+
if (value.status === 'SCOPE_REQUIRED') {
|
|
81
|
+
return renderScopeRequiredMarkdown(value);
|
|
82
|
+
}
|
|
83
|
+
if (value.status === 'AWAITING_CONFIRMATION') {
|
|
84
|
+
return [
|
|
85
|
+
`> ### ${value.warning || '⚠️ 操作确认拦截'}`,
|
|
86
|
+
`> - **影响评估**: ${value.impact || ''}`,
|
|
87
|
+
`> - **确认指引**: ${value.requiredAction || '如确认继续,请在参数中附加 `confirm: true` 重新调用。'}`,
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
90
|
+
if (typeof value.result === 'string')
|
|
91
|
+
return value.result;
|
|
92
|
+
if (typeof value.message === 'string' && !value.items) {
|
|
93
|
+
const extra = value.detail || value.output;
|
|
94
|
+
return `**${value.message}**` + (extra ? `\n\`\`\`\n${extra}\n\`\`\`` : '');
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(value.items) && args?.action === 'list') {
|
|
97
|
+
return renderCodespaceList(value.items);
|
|
98
|
+
}
|
|
99
|
+
if (args?.action === 'view' || value.name) {
|
|
100
|
+
return renderCodespaceView(value);
|
|
101
|
+
}
|
|
102
|
+
return JSON.stringify(value, null, 2);
|
|
103
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 格式化 Discussion 状态徽标
|
|
3
|
+
*/
|
|
4
|
+
export declare function formatDiscussionStateBadge(state?: string, answered?: boolean, closed?: boolean): string;
|
|
5
|
+
/**
|
|
6
|
+
* 渲染 Discussion 列表表格
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderDiscussionList(items: any[]): string;
|
|
9
|
+
/**
|
|
10
|
+
* 渲染单个 Discussion 详情卡片
|
|
11
|
+
*/
|
|
12
|
+
export declare function renderDiscussionView(d: any): string;
|
|
13
|
+
/**
|
|
14
|
+
* Discussion 领域综合渲染入口
|
|
15
|
+
*/
|
|
16
|
+
export declare function renderDiscussionMarkdown(args: any, value: any): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { sanitizeCell } from './common.js';
|
|
2
|
+
/**
|
|
3
|
+
* 格式化 Discussion 状态徽标
|
|
4
|
+
*/
|
|
5
|
+
export function formatDiscussionStateBadge(state, answered, closed) {
|
|
6
|
+
if (answered)
|
|
7
|
+
return '🟢 ANSWERED';
|
|
8
|
+
const effective = state ? String(state).toUpperCase() : (closed ? 'CLOSED' : 'OPEN');
|
|
9
|
+
if (effective === 'OPEN')
|
|
10
|
+
return '🔵 OPEN';
|
|
11
|
+
if (effective === 'CLOSED')
|
|
12
|
+
return '🟣 CLOSED';
|
|
13
|
+
return `⚪ ${effective}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 渲染 Discussion 列表表格
|
|
17
|
+
*/
|
|
18
|
+
export function renderDiscussionList(items) {
|
|
19
|
+
const list = items.filter(Boolean);
|
|
20
|
+
if (list.length === 0) {
|
|
21
|
+
return '📭 暂无讨论主题 (Discussions)。';
|
|
22
|
+
}
|
|
23
|
+
const rows = list.map((d) => {
|
|
24
|
+
const num = d.number ? `#${d.number}` : '-';
|
|
25
|
+
const link = d.url ? `[${num}](${d.url})` : num;
|
|
26
|
+
const title = sanitizeCell(d.title || '(无标题)', 45);
|
|
27
|
+
const category = sanitizeCell(d.category?.name || d.category || 'General', 15);
|
|
28
|
+
const stateBadge = formatDiscussionStateBadge(d.state, d.answered, d.closed);
|
|
29
|
+
const author = d.author?.login || d.author || '-';
|
|
30
|
+
const updated = d.updatedAt ? d.updatedAt.split('T')[0] : (d.createdAt ? d.createdAt.split('T')[0] : '-');
|
|
31
|
+
return `| ${link} | ${title} | \`${category}\` | ${stateBadge} | @${author} | ${updated} |`;
|
|
32
|
+
});
|
|
33
|
+
return [
|
|
34
|
+
`**讨论主题列表 (${list.length} 条):**\n`,
|
|
35
|
+
'| 编号 | 标题 | 分类 | 状态 | 作者 | 更新日期 |',
|
|
36
|
+
'| :--- | :--- | :--- | :--- | :--- | :--- |',
|
|
37
|
+
...rows,
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 渲染单个 Discussion 详情卡片
|
|
42
|
+
*/
|
|
43
|
+
export function renderDiscussionView(d) {
|
|
44
|
+
if (!d || typeof d !== 'object') {
|
|
45
|
+
return '无讨论详情数据';
|
|
46
|
+
}
|
|
47
|
+
const num = d.number ? `#${d.number}` : '';
|
|
48
|
+
const title = d.title || '(无标题)';
|
|
49
|
+
const stateBadge = formatDiscussionStateBadge(d.state, d.answered, d.closed);
|
|
50
|
+
const category = d.category?.name || d.category || 'General';
|
|
51
|
+
const author = d.author?.login || d.author || '-';
|
|
52
|
+
const created = d.createdAt || '-';
|
|
53
|
+
const link = d.url ? `[查看原始讨论](${d.url})` : '';
|
|
54
|
+
const lines = [
|
|
55
|
+
`### 💬 Discussion ${num}: ${title}\n`,
|
|
56
|
+
`- **状态**: ${stateBadge}`,
|
|
57
|
+
`- **分类**: \`${category}\``,
|
|
58
|
+
`- **发起人**: @${author}`,
|
|
59
|
+
`- **创建时间**: ${created}`,
|
|
60
|
+
];
|
|
61
|
+
if (link)
|
|
62
|
+
lines.push(`- **链接**: ${link}`);
|
|
63
|
+
if (Array.isArray(d.labels) && d.labels.length > 0) {
|
|
64
|
+
const labelBadges = d.labels.map((l) => `\`${l.name || l}\``).join(' ');
|
|
65
|
+
lines.push(`- **标签**: ${labelBadges}`);
|
|
66
|
+
}
|
|
67
|
+
if (d.body) {
|
|
68
|
+
lines.push('\n**正文内容:**', '---', d.body.trim(), '---');
|
|
69
|
+
}
|
|
70
|
+
// 渲染关联评论列表
|
|
71
|
+
if (Array.isArray(d.comments) && d.comments.length > 0) {
|
|
72
|
+
lines.push(`\n**讨论回复 (${d.comments.length} 条):**`);
|
|
73
|
+
d.comments.forEach((c, index) => {
|
|
74
|
+
const cAuthor = c.author?.login || c.author || '匿名';
|
|
75
|
+
const cTime = c.createdAt ? c.createdAt.replace('T', ' ').slice(0, 19) : '';
|
|
76
|
+
const cBody = (c.body || '').trim();
|
|
77
|
+
lines.push(`\n> **#${index + 1} 楼 @${cAuthor}** (${cTime}):\n> ${cBody.replace(/\n/g, '\n> ')}`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Discussion 领域综合渲染入口
|
|
84
|
+
*/
|
|
85
|
+
export function renderDiscussionMarkdown(args, value) {
|
|
86
|
+
if (!value)
|
|
87
|
+
return '无返回内容';
|
|
88
|
+
if (typeof value.result === 'string')
|
|
89
|
+
return value.result;
|
|
90
|
+
if (typeof value.message === 'string' && !value.items) {
|
|
91
|
+
const extra = value.detail || value.output || value.url;
|
|
92
|
+
return `**${value.message}**` + (extra ? `\n\`\`\`\n${extra}\n\`\`\`` : '');
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(value.items) && args?.action === 'list') {
|
|
95
|
+
return renderDiscussionList(value.items);
|
|
96
|
+
}
|
|
97
|
+
if (args?.action === 'view' || value.number) {
|
|
98
|
+
return renderDiscussionView(value);
|
|
99
|
+
}
|
|
100
|
+
return JSON.stringify(value, null, 2);
|
|
101
|
+
}
|