dsh-github-flow 0.5.0 → 0.7.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 +112 -17
- package/README.md +115 -17
- package/docs/UPGRADE-NOTES.md +12 -11
- package/docs/architecture.md +81 -22
- package/docs/images/1.png +0 -0
- package/lib/api/route-helpers.d.ts +19 -0
- package/lib/api/route-helpers.js +54 -0
- package/lib/api/routes-actions.js +214 -9
- package/lib/api/routes-lifecycle.js +8 -1
- package/lib/api/routes-overview.d.ts +26 -0
- package/lib/api/routes-overview.js +396 -36
- package/lib/api/routes-proxy.js +8 -1
- package/lib/api/routes.d.ts +11 -1
- package/lib/api/routes.js +15 -2
- package/lib/cache.d.ts +56 -4
- package/lib/cache.js +77 -7
- package/lib/client.d.ts +8 -3
- package/lib/client.js +5542 -1391
- package/lib/destructive.d.ts +59 -0
- package/lib/destructive.js +285 -0
- package/lib/executor/args-guard.d.ts +17 -0
- package/lib/executor/args-guard.js +23 -0
- package/lib/executor/error-classifier.d.ts +55 -0
- package/lib/executor/error-classifier.js +199 -0
- package/lib/executor/failure-anchors.d.ts +52 -0
- package/lib/executor/failure-anchors.js +116 -0
- package/lib/executor/git-parser.d.ts +124 -0
- package/lib/executor/git-parser.js +397 -0
- package/lib/executor/pr-review.d.ts +95 -0
- package/lib/executor/pr-review.js +175 -0
- package/lib/executor/run-log.d.ts +36 -0
- package/lib/executor/run-log.js +78 -0
- package/lib/executor.d.ts +277 -6
- package/lib/executor.js +1265 -94
- package/lib/github-service.d.ts +1 -1
- package/lib/main.d.ts +92 -18
- package/lib/main.js +186 -95
- package/lib/renderers/common.d.ts +0 -3
- package/lib/renderers/common.js +8 -44
- package/lib/renderers/markdown.d.ts +1 -0
- package/lib/renderers/markdown.js +1 -0
- package/lib/renderers/pr.js +26 -1
- package/lib/renderers/task.d.ts +9 -0
- package/lib/renderers/task.js +120 -0
- package/lib/review-rules.d.ts +60 -0
- package/lib/review-rules.js +204 -0
- package/lib/shared/state-badges.d.ts +42 -0
- package/lib/shared/state-badges.js +102 -0
- package/lib/task-primitives.d.ts +109 -0
- package/lib/task-primitives.js +194 -0
- package/lib/tool-tiers.d.ts +57 -0
- package/lib/tool-tiers.js +168 -0
- package/lib/tools/agent-task.js +4 -4
- package/lib/tools/api.js +18 -6
- package/lib/tools/cache.js +42 -40
- package/lib/tools/codespace.js +4 -12
- package/lib/tools/discussion.js +4 -4
- package/lib/tools/gist.d.ts +1 -39
- package/lib/tools/gist.js +4 -14
- package/lib/tools/help.js +72 -18
- package/lib/tools/image-utils.d.ts +27 -0
- package/lib/tools/image-utils.js +73 -3
- package/lib/tools/issue.js +16 -28
- package/lib/tools/label.js +4 -15
- package/lib/tools/pr.d.ts +47 -0
- package/lib/tools/pr.js +170 -33
- package/lib/tools/project.js +4 -13
- package/lib/tools/release.js +11 -22
- package/lib/tools/repo.js +4 -4
- package/lib/tools/ruleset.js +9 -6
- package/lib/tools/run.js +76 -24
- package/lib/tools/secret.js +11 -17
- package/lib/tools/skill.js +18 -22
- package/lib/tools/status.js +4 -4
- package/lib/tools/task.d.ts +219 -0
- package/lib/tools/task.js +457 -0
- package/lib/types.d.ts +514 -1
- package/lib/ui/help-modal.d.ts +6 -0
- package/lib/ui/help-modal.js +124 -16
- package/lib/ui/issue-modal.d.ts +14 -0
- package/lib/ui/issue-modal.js +370 -0
- package/lib/ui/markdown-lite.d.ts +66 -0
- package/lib/ui/markdown-lite.js +312 -0
- package/lib/ui/network-banner.d.ts +12 -0
- package/lib/ui/network-banner.js +35 -0
- package/lib/ui/pr-modal.d.ts +17 -0
- package/lib/ui/pr-modal.js +555 -0
- package/lib/ui/repo-badges.d.ts +43 -0
- package/lib/ui/repo-badges.js +82 -0
- package/lib/ui/repo-selector.d.ts +14 -0
- package/lib/ui/repo-selector.js +112 -0
- package/lib/ui/run-modal.d.ts +26 -0
- package/lib/ui/run-modal.js +486 -0
- package/lib/ui/storage.d.ts +47 -0
- package/lib/ui/storage.js +139 -0
- package/lib/ui/styles.d.ts +1 -1
- package/lib/ui/styles.js +1330 -44
- package/lib/ui/sync-settings-modal.js +1 -1
- package/lib/ui/utils.js +13 -1
- package/lib/ui/workspace-panel.js +226 -29
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +9 -2
package/lib/tools/codespace.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapWithGate } from '../destructive.js';
|
|
1
2
|
import { renderCodespaceMarkdown } from '../renderers/markdown.js';
|
|
2
3
|
import { checkScopeRequired } from './project.js';
|
|
3
4
|
function normalizeOutput(data) {
|
|
@@ -12,7 +13,7 @@ function normalizeOutput(data) {
|
|
|
12
13
|
const CODESPACE_LIST_FIELDS = 'name,displayName,repository,owner,state,gitStatus,lastUsedAt,createdAt,machineName';
|
|
13
14
|
const CODESPACE_VIEW_FIELDS = 'name,displayName,repository,owner,state,gitStatus,lastUsedAt,createdAt,machineName,devcontainerPath,recentFolders,idleTimeoutMinutes';
|
|
14
15
|
export function createCodespaceTool(executor) {
|
|
15
|
-
return {
|
|
16
|
+
return wrapWithGate({
|
|
16
17
|
name: 'github_codespace',
|
|
17
18
|
description: '管理 GitHub Codespaces 云端远程开发环境:查阅实例列表与运行状态、检视实例配置详情、停止运行中的环境防计费、安全销毁实例。未授权时提供平台感知自愈命令。',
|
|
18
19
|
parameters: {
|
|
@@ -53,8 +54,7 @@ export function createCodespaceTool(executor) {
|
|
|
53
54
|
},
|
|
54
55
|
},
|
|
55
56
|
async execute(args, execContext) {
|
|
56
|
-
const cwd
|
|
57
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
57
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
58
58
|
const runOpts = { cwd, sessionId };
|
|
59
59
|
switch (args.action) {
|
|
60
60
|
case 'list': {
|
|
@@ -123,14 +123,6 @@ export function createCodespaceTool(executor) {
|
|
|
123
123
|
throw new Error('delete 操作必须提供 codespace 名称');
|
|
124
124
|
}
|
|
125
125
|
const cleanName = args.codespace.trim();
|
|
126
|
-
if (args.confirm !== true) {
|
|
127
|
-
return {
|
|
128
|
-
status: 'AWAITING_CONFIRMATION',
|
|
129
|
-
warning: `⚠️ 销毁确认拦截:你即将永久删除云端开发环境「${cleanName}」。`,
|
|
130
|
-
impact: '物理销毁 Codespace 将永久释放云端计算资源,环境中任何未提交并推送到远端 Git 的临时修改都将无法找回。',
|
|
131
|
-
requiredAction: '如确认销毁该实例,请核对环境名称并在参数中附加 `confirm: true` 重新调用。',
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
126
|
const deleteCmd = ['codespace', 'delete', '-c', cleanName, '-f'];
|
|
135
127
|
if (args.org)
|
|
136
128
|
deleteCmd.push('-o', String(args.org).trim());
|
|
@@ -150,5 +142,5 @@ export function createCodespaceTool(executor) {
|
|
|
150
142
|
throw new Error(`未知的 Codespace 操作: ${args.action}`);
|
|
151
143
|
}
|
|
152
144
|
},
|
|
153
|
-
};
|
|
145
|
+
});
|
|
154
146
|
}
|
package/lib/tools/discussion.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapWithGate } from '../destructive.js';
|
|
1
2
|
import { renderDiscussionMarkdown } from '../renderers/markdown.js';
|
|
2
3
|
function normalizeOutput(data) {
|
|
3
4
|
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
@@ -11,7 +12,7 @@ function normalizeOutput(data) {
|
|
|
11
12
|
const DISCUSSION_LIST_FIELDS = 'number,title,body,category,author,closed,createdAt,updatedAt,url,labels,answered,stateReason';
|
|
12
13
|
const DISCUSSION_VIEW_FIELDS = 'number,title,body,category,author,state,createdAt,updatedAt,url,labels,answered,comments';
|
|
13
14
|
export function createDiscussionTool(executor) {
|
|
14
|
-
return {
|
|
15
|
+
return wrapWithGate({
|
|
15
16
|
name: 'github_discussion',
|
|
16
17
|
description: '管理 GitHub Discussions 讨论区:浏览社区讨论、检视讨论详情与回复楼层、发起新讨论议题、编辑现有讨论以及追加评论回复。基于 gh CLI preview 特性。',
|
|
17
18
|
parameters: {
|
|
@@ -74,8 +75,7 @@ export function createDiscussionTool(executor) {
|
|
|
74
75
|
},
|
|
75
76
|
},
|
|
76
77
|
async execute(args, execContext) {
|
|
77
|
-
const cwd
|
|
78
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
78
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
79
79
|
const repoArgs = args.repo ? ['-R', args.repo] : [];
|
|
80
80
|
const runOpts = { cwd, sessionId };
|
|
81
81
|
switch (args.action) {
|
|
@@ -191,5 +191,5 @@ export function createDiscussionTool(executor) {
|
|
|
191
191
|
throw new Error(`未知的 Discussion 操作: ${args.action}`);
|
|
192
192
|
}
|
|
193
193
|
},
|
|
194
|
-
};
|
|
194
|
+
});
|
|
195
195
|
}
|
package/lib/tools/gist.d.ts
CHANGED
|
@@ -71,14 +71,9 @@ export declare function createGistTool(executor: GhExecutor): {
|
|
|
71
71
|
public?: undefined;
|
|
72
72
|
description?: undefined;
|
|
73
73
|
success?: undefined;
|
|
74
|
+
gist_id?: undefined;
|
|
74
75
|
message?: undefined;
|
|
75
76
|
detail?: undefined;
|
|
76
|
-
status?: undefined;
|
|
77
|
-
action?: undefined;
|
|
78
|
-
gist_id?: undefined;
|
|
79
|
-
warning?: undefined;
|
|
80
|
-
impact?: undefined;
|
|
81
|
-
requiredAction?: undefined;
|
|
82
77
|
} | {
|
|
83
78
|
items?: undefined;
|
|
84
79
|
count?: undefined;
|
|
@@ -92,11 +87,6 @@ export declare function createGistTool(executor: GhExecutor): {
|
|
|
92
87
|
success?: undefined;
|
|
93
88
|
message?: undefined;
|
|
94
89
|
detail?: undefined;
|
|
95
|
-
status?: undefined;
|
|
96
|
-
action?: undefined;
|
|
97
|
-
warning?: undefined;
|
|
98
|
-
impact?: undefined;
|
|
99
|
-
requiredAction?: undefined;
|
|
100
90
|
} | {
|
|
101
91
|
items?: undefined;
|
|
102
92
|
count?: undefined;
|
|
@@ -110,11 +100,6 @@ export declare function createGistTool(executor: GhExecutor): {
|
|
|
110
100
|
description: any;
|
|
111
101
|
message: string;
|
|
112
102
|
detail?: undefined;
|
|
113
|
-
status?: undefined;
|
|
114
|
-
action?: undefined;
|
|
115
|
-
warning?: undefined;
|
|
116
|
-
impact?: undefined;
|
|
117
|
-
requiredAction?: undefined;
|
|
118
103
|
} | {
|
|
119
104
|
items?: undefined;
|
|
120
105
|
count?: undefined;
|
|
@@ -128,28 +113,5 @@ export declare function createGistTool(executor: GhExecutor): {
|
|
|
128
113
|
gist_id: string;
|
|
129
114
|
message: string;
|
|
130
115
|
detail: string;
|
|
131
|
-
status?: undefined;
|
|
132
|
-
action?: undefined;
|
|
133
|
-
warning?: undefined;
|
|
134
|
-
impact?: undefined;
|
|
135
|
-
requiredAction?: undefined;
|
|
136
|
-
} | {
|
|
137
|
-
items?: undefined;
|
|
138
|
-
count?: undefined;
|
|
139
|
-
rawText?: undefined;
|
|
140
|
-
filename?: undefined;
|
|
141
|
-
content?: undefined;
|
|
142
|
-
url?: undefined;
|
|
143
|
-
public?: undefined;
|
|
144
|
-
description?: undefined;
|
|
145
|
-
success?: undefined;
|
|
146
|
-
message?: undefined;
|
|
147
|
-
detail?: undefined;
|
|
148
|
-
status: string;
|
|
149
|
-
action: string;
|
|
150
|
-
gist_id: string;
|
|
151
|
-
warning: string;
|
|
152
|
-
impact: string;
|
|
153
|
-
requiredAction: string;
|
|
154
116
|
}>;
|
|
155
117
|
};
|
package/lib/tools/gist.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { wrapWithGate } from '../destructive.js';
|
|
1
2
|
import { renderGistMarkdown, parseGistListOutput } from '../renderers/markdown.js';
|
|
2
3
|
export function createGistTool(executor) {
|
|
3
|
-
return {
|
|
4
|
+
return wrapWithGate({
|
|
4
5
|
name: 'github_gist',
|
|
5
6
|
description: '管理 GitHub Gist 代码片段与诊断留痕:查看 Gist 列表、检视代码正文 (view)、通过文本内容或本地文件创建新 Gist (create,默认安全 secret 模式)、编辑以及安全删除。删除操作不可逆,需 confirm: true。',
|
|
6
7
|
parameters: {
|
|
@@ -58,8 +59,7 @@ export function createGistTool(executor) {
|
|
|
58
59
|
},
|
|
59
60
|
},
|
|
60
61
|
async execute(args, execContext) {
|
|
61
|
-
const cwd
|
|
62
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
62
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
63
63
|
const runOpts = { cwd, sessionId, rawText: true };
|
|
64
64
|
switch (args.action) {
|
|
65
65
|
case 'list': {
|
|
@@ -161,16 +161,6 @@ export function createGistTool(executor) {
|
|
|
161
161
|
if (!args.gist_id) {
|
|
162
162
|
throw new Error('删除 Gist 时 `gist_id` 参数为必填项。');
|
|
163
163
|
}
|
|
164
|
-
if (args.confirm !== true) {
|
|
165
|
-
return {
|
|
166
|
-
status: 'AWAITING_CONFIRMATION',
|
|
167
|
-
action: 'delete',
|
|
168
|
-
gist_id: String(args.gist_id),
|
|
169
|
-
warning: `⚠️ 高危破坏性操作:即将永久删除 Gist 代码片段 "${args.gist_id}"`,
|
|
170
|
-
impact: `删除 Gist 为物理删除,无法通过 GitHub API 恢复,所有引用该 Gist URL 的外部链接将失效并返回 404。`,
|
|
171
|
-
requiredAction: `如确认彻底删除,请在参数中附加 \`confirm: true\` 重新调用。示例:\`github_gist(action: 'delete', gist_id: '${args.gist_id}', confirm: true)\``,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
164
|
const delCmd = ['gist', 'delete', String(args.gist_id), '--yes'];
|
|
175
165
|
const res = await executor.run(delCmd, runOpts);
|
|
176
166
|
if (!res.ok)
|
|
@@ -186,5 +176,5 @@ export function createGistTool(executor) {
|
|
|
186
176
|
throw new Error(`不支持的 gist 操作: "${args.action}"。可选值: list, view, create, edit, delete。`);
|
|
187
177
|
}
|
|
188
178
|
},
|
|
189
|
-
};
|
|
179
|
+
});
|
|
190
180
|
}
|
package/lib/tools/help.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapWithGate } from '../destructive.js';
|
|
1
2
|
const KNOWLEDGE_BASE = {
|
|
2
3
|
overview: `### 🧭 DSH GitHub Flow 工具箱全景导航
|
|
3
4
|
|
|
@@ -12,20 +13,23 @@ const KNOWLEDGE_BASE = {
|
|
|
12
13
|
| \`github_api\` | 原生 GitHub REST/GraphQL 逃生门 | 调用任意端点、获取原始文件字节 (raw)、支持自动翻页 (paginate) |
|
|
13
14
|
| \`github_release\` | 版本发布与构建资产交付 | 查看发版历史、自动生成 Release Notes、上传 .tgz 等构建产物 |
|
|
14
15
|
| \`github_secret\` | CI/CD 密钥与变量管理 | 查看 NPM_TOKEN 等密钥清单、经 stdin 安全写入密文、删除密钥;kind:'variable' 管理明文变量(set/get/delete) |
|
|
15
|
-
| \`github_skill\` | 跨 Agent 开源 Skills
|
|
16
|
+
| \`github_skill\` | 跨 Agent 开源 Skills 生态发现与维护 | 检索/预览/安装 GitHub 上的 Agent Skills(gh preview 特性);\`publish\` 规范自检与发布、\`update\` 检查并同步更新已安装技能 |
|
|
16
17
|
| \`github_label\` | 标签体系管理与跨仓库克隆 | 查看标签列表、创建/编辑标签、从源仓库一键 clone 标签体系、删除标签(需 confirm) |
|
|
17
18
|
| \`github_ruleset\` | 分支保护规则与策略诊断 | 检查分支适用的合并策略规则 (check)、查看规则集列表与详情,权威回答“PR 为何无法合并” |
|
|
18
|
-
| \`github_cache\` | Actions 构建缓存盘点与安全清理 | 查看缓存列表与体积占用、按 ID/Key
|
|
19
|
+
| \`github_cache\` | Actions 构建缓存盘点与安全清理 | 查看缓存列表与体积占用、按 ID/Key 删除指定缓存、删除缓存(三态均需 confirm) |
|
|
19
20
|
| \`github_gist\` | 代码片段与调试日志分享治理 | 查看 Gist 列表与代码正文、创建 Secret/Public 代码片段、编辑与物理删除(需 confirm) |
|
|
20
21
|
| \`github_status\` | 跨仓库待办与动态聚合看板 | 账号维度汇总指派给我的 Issue/PR、等待评审、被 @提及 以及订阅仓库近期动态 |
|
|
21
22
|
| \`github_agent_task\` | 云端智能体编码任务会话治理 | 查阅已分派任务列表、检视任务状态与会话日志、向云端 Agent 发起新编码重构任务 |
|
|
22
23
|
| \`github_discussion\` | 团队技术讨论与开源社区 RFC 闭环 | 浏览讨论列表、检视正文与评论楼层、发起新讨论、编辑正文与追加回复 |
|
|
23
24
|
| \`github_project\` | Projects v2 项目看板与任务规划 | 查阅项目列表/详情、浏览看板条目 (Items)、添加 Issue/PR 到看板、创建草稿任务与删除条目 |
|
|
24
25
|
| \`github_codespace\` | 云端远程开发环境管控 (MVP) | 查阅运行中实例、检视机器规格配置、停机防计费 (stop)、安全销毁环境 (delete,需 confirm) |
|
|
26
|
+
| \`github_task\` | **任务级原语**(一次调用完成一件完整的事) | 审一个 PR(元数据 + diff + CI + 仓库规则,可选写回行级评注)、诊断一次 CI 失败(定位 job/step + 日志锚点)、准备一次发版(Conventional Commits 归类 + SemVer + notes 草稿);默认只读 |
|
|
25
27
|
| \`github_help\` | 路由导航、官方语法速查与故障自愈 | 遇到不确定的操作、搜索语法或接口报错时查询用法与诊断配方 |
|
|
26
28
|
|
|
27
29
|
💡 **使用准则**:优先调用高层语义工具(pr / issue / run / repo / release / secret);当高层工具无法满足特殊接口需求时,再降级使用 \`github_api\`。
|
|
30
|
+
📊 **规模口径**:19 个工具 / 106 个业务 action(不含 \`github_help\` 的 22 个查询主题与 \`github_api\` 的 4 个 HTTP method——它们不是业务动作)。
|
|
28
31
|
💬 **人类指令**:聊天框支持 \`/gh status\`(大盘看板)、\`/gh upgrade\`(一键升级插件)、\`/gh help\`(快捷帮助)。
|
|
32
|
+
🖥️ **可视化工作台**:最左侧 Octocat 图标打开全景驾驶舱(4 标签:本地工作区 / 我的仓库 / Pull Requests / 待办 Issues);每个会话右侧栏有 5 个 Tab(Git / 未提交 / PR / Issues / Actions)。PR、Issue、Actions 三类卡片点击均可在线查看详情(Actions 为 job / step 树 + 按需失败日志),无需跳浏览器。
|
|
29
33
|
🗺️ **命令对照**:不确定某 gh 命令对应哪个工具?\`github_help(topic: 'mapping')\` 查询完整映射表(含未覆盖域的 API 引导)。`,
|
|
30
34
|
pr: `### 🔀 github_pr 详细操作指南
|
|
31
35
|
|
|
@@ -47,7 +51,9 @@ const KNOWLEDGE_BASE = {
|
|
|
47
51
|
- **分支落后自愈**: \`github_pr(action: 'update_branch', pr_number: 123)\`
|
|
48
52
|
- 将基准分支最新提交同步进 PR 分支,解决「分支落后需更新」的 CI 门禁失败。
|
|
49
53
|
- **上传本地附件/图片**: \`github_pr(action: 'create', title: 'UI 重构', body: '对比图如下', attach_images: ['./before.png', './after.png'])\`
|
|
50
|
-
- 需 gh ≥ 2.99;支持 create/edit/comment 动作,单批上限 50 个文件,禁止网络 URL,与 view_images
|
|
54
|
+
- 需 gh ≥ 2.99;支持 create/edit/comment 动作,单批上限 50 个文件,禁止网络 URL,与 view_images 对称闭环。
|
|
55
|
+
- **追加评论与编辑**: \`github_pr(action: 'comment', pr_number: 123, body: 'LGTM'); github_pr(action: 'edit', pr_number: 123, body: '更新后的描述')\`
|
|
56
|
+
- 两者同样支持 \`attach_images\` 附件通道。`,
|
|
51
57
|
issue: `### 🎯 github_issue 详细操作指南
|
|
52
58
|
|
|
53
59
|
- **筛选 Issue**: \`github_issue(action: 'list', state: 'open' | 'closed' | 'all', labels: ['bug'], assignees: ['octocat'], limit: 20)\`
|
|
@@ -88,7 +94,11 @@ const KNOWLEDGE_BASE = {
|
|
|
88
94
|
- **启停工作流**: \`github_run(action: 'workflow_enable'/'workflow_disable', workflow: '…')\`
|
|
89
95
|
- disable 后 trigger 将静默失败,直至重新 enable——输出会显式警告这一点。
|
|
90
96
|
- **下载构建产物**: \`github_run(action: 'download', run_id: '12345678', name: '构建产物名'?)\`
|
|
91
|
-
- 落盘目录由插件固定管理(\`~/.dsh/data/dsh-github-flow/artifacts/\` 下按仓库与 run
|
|
97
|
+
- 落盘目录由插件固定管理(\`~/.dsh/data/dsh-github-flow/artifacts/\` 下按仓库与 run 隔离),返回文件清单。
|
|
98
|
+
- **看单次运行的 job / step 结构**: \`github_run(action: 'view', run_id: '12345678')\`
|
|
99
|
+
- 返回 run 元数据 + jobs 树(每个 job 的 conclusion、起止时间、url,以及 steps 的 number/name/conclusion);这是「挂在第几步」最快的入口。
|
|
100
|
+
- **人类侧在线查看(不是工具调用)**: 右侧栏 Actions Tab 点击任一卡片 → job / step 树 + 失败步骤高亮 + 按需日志锚点(一次最多 3 个失败 job、服务端批次预算 90 秒)。
|
|
101
|
+
- Agent 侧的等价快捷方式是 \`github_task(action: 'diagnose_ci', run_id: '12345678')\`:定位失败 job/step → 萃取日志锚点 → 汇总疑似相关文件,一次返回结论包。`,
|
|
92
102
|
repo: `### 📦 github_repo 详细操作指南
|
|
93
103
|
|
|
94
104
|
- **查看元数据与标签**: \`github_repo(action: 'view')\`
|
|
@@ -164,7 +174,13 @@ const KNOWLEDGE_BASE = {
|
|
|
164
174
|
- \`github_skill(action: 'publish')\` —— 默认 \`--dry-run\` 模式,对本地技能目录进行 agentskills.io 合规性检查(命名规范、目录匹配、必需 frontmatter 字段等),零副作用安全自检。
|
|
165
175
|
- \`github_skill(action: 'publish', fix: true)\` —— 自动清洗并剥离残留的安装元数据(\`metadata.github-*\`),修改本地文件但不发布。
|
|
166
176
|
- \`github_skill(action: 'publish', tag: 'v1.0.0', dry_run: false, confirm: true)\` —— 正式发布,强制要求指定 \`tag\` 并附带 \`confirm: true\`(防无头挂起与误发布),自动创建 GitHub Release。
|
|
167
|
-
- ⚠️ 若报「unknown command」,说明宿主机 gh 版本过低,需升级到 2.9x
|
|
177
|
+
- ⚠️ 若报「unknown command」,说明宿主机 gh 版本过低,需升级到 2.9x 以上。
|
|
178
|
+
|
|
179
|
+
- **检查并同步更新已安装技能**:
|
|
180
|
+
- \`github_skill(action: 'update')\` —— **默认 dry-run**,只检查哪些技能有新版本,零副作用。
|
|
181
|
+
- \`github_skill(action: 'update', dry_run: false)\` —— 真正执行同步更新。
|
|
182
|
+
- \`github_skill(action: 'update', all: true)\` —— 更新全部已安装技能(否则只处理 \`skill_names\` 指定项)。
|
|
183
|
+
- \`github_skill(action: 'update', skill_names: ['code-review', 'devops'])\` —— 定向更新指定技能。`,
|
|
168
184
|
label: `### 🏷️ github_label 详细操作指南
|
|
169
185
|
|
|
170
186
|
- **查看标签列表**: \`github_label(action: 'list', limit: 30, search: 'bug')\`
|
|
@@ -193,8 +209,8 @@ const KNOWLEDGE_BASE = {
|
|
|
193
209
|
- 返回仓库 Actions 构建缓存清单、占用体积(自动换算为 KB/MB/GB)、关联 Git Ref 以及最后访问时间。
|
|
194
210
|
- 可附加 \`key: 'npm-cache'\` 过滤特定前缀,或通过 \`ref: 'refs/heads/main'\` 限定分支。
|
|
195
211
|
- **精准删除单条缓存**:
|
|
196
|
-
- 按数字 ID 删除: \`github_cache(action: 'delete', cache_id: '12345678')\`
|
|
197
|
-
- 按 Key 键名删除: \`github_cache(action: 'delete', key: 'Linux-npm-18-abcd1234')\`
|
|
212
|
+
- 按数字 ID 删除: \`github_cache(action: 'delete', cache_id: '12345678', confirm: true)\`
|
|
213
|
+
- 按 Key 键名删除: \`github_cache(action: 'delete', key: 'Linux-npm-18-abcd1234', confirm: true)\`
|
|
198
214
|
- **全量清空仓库缓存**:
|
|
199
215
|
\`github_cache(action: 'delete', all: true, confirm: true)\`
|
|
200
216
|
- 批量破坏性操作:会导致后续流水线构建重新拉取依赖,必须显式附加 \`confirm: true\`;底层自动注入 \`--succeed-on-no-caches\` 保证幂等执行。`,
|
|
@@ -221,7 +237,7 @@ const KNOWLEDGE_BASE = {
|
|
|
221
237
|
- **排除噪音仓库**: \`github_status(exclude: ['owner/noisy-repo'])\`
|
|
222
238
|
- 过滤高频刷屏但无需关注的仓库活动。
|
|
223
239
|
- **查看工具链运行时健康度**: \`github_status(action: 'health')\`
|
|
224
|
-
- 审计当前会话
|
|
240
|
+
- 审计当前会话 19 大工具矩阵的综合健康分、API 降级率、安全防线拦截频次与针对性自愈建议。`,
|
|
225
241
|
agent_task: `### 🤖 github_agent_task 智能体任务指南
|
|
226
242
|
|
|
227
243
|
管理与调度 GitHub Copilot Coding Agent 云端智能体会话:
|
|
@@ -265,14 +281,33 @@ const KNOWLEDGE_BASE = {
|
|
|
265
281
|
- **安全销毁实例**: \`github_codespace(action: 'delete', codespace: 'fuzzy-spoon-69vj', confirm: true)\`
|
|
266
282
|
- 物理销毁不可逆,必须附加 \`confirm: true\`。
|
|
267
283
|
- 🔑 若报缺少权限,在终端执行单行自愈授权命令:\`gh auth refresh -h github.com -s codespace\``,
|
|
284
|
+
task: `### 🎯 github_task 任务级原语指南
|
|
285
|
+
|
|
286
|
+
\`github_task\` 把「一件完整的事」封装成一次调用,返回**已裁剪、可直接执行**的结论包:
|
|
287
|
+
\`summary\`(结论)+ \`facts\`(结构化事实)+ \`nextActions\`(下一步)+ \`cost\`(字符数 / gh 调用数 / 耗时)。
|
|
288
|
+
|
|
289
|
+
- **审一个 PR**: \`github_task(action: 'review_pr', pr_number: 123)\`
|
|
290
|
+
- 一次拿齐:PR 元数据 + head SHA + 变动文件 diff(按 \`budget_chars\` 裁剪)+ CI 结论 + 仓库 \`AGENTS.md\` 审查规则 + 既有行级评论数。
|
|
291
|
+
- \`focus: 'security' | 'correctness' | 'perf' | 'style'\` 只影响 summary 的提示语,不改变取数。
|
|
292
|
+
- **写回**(默认关闭): \`github_task(action: 'review_pr', pr_number: 123, write: true, confirm: true, body: '…', comments: [{path, line, body}])\`,字段语义与 \`github_pr(action: 'review')\` 完全一致。
|
|
293
|
+
- **诊断一次 CI 失败**: \`github_task(action: 'diagnose_ci', run_id: 456)\` 或 \`github_task(action: 'diagnose_ci', pr_number: 123)\`
|
|
294
|
+
- 定位失败 job/step → 萃取失败日志锚点(pytest / node / Go / tsc / npm)→ 汇总疑似相关文件。
|
|
295
|
+
- 日志被截断时 \`truncated.totalUnknown = true\`:**不要据此判断「就这些失败」**。
|
|
296
|
+
- **准备一次发版**: \`github_task(action: 'prepare_release', bump: 'auto')\`
|
|
297
|
+
- 读最近 tag 以来的提交 → 按 Conventional Commits 归类 → 推导 SemVer(breaking→major / feat→minor / 其余→patch)→ 生成 notes 草稿 + 发版体检(工作区干净 / 同步远端 / CI 绿)。
|
|
298
|
+
- **本原语不会打 tag、不会 push、不会创建 Release**:发版是人的决定,它只把「该发什么版本、notes 长什么样」准备好,并在 \`nextActions\` 里给出可直接执行的命令。
|
|
299
|
+
|
|
300
|
+
> 需要细粒度控制时,用对应的原子工具:\`github_pr\` / \`github_run\` / \`github_release\`。原语是快捷方式,不是唯一入口。
|
|
301
|
+
`,
|
|
268
302
|
mapping: `### 🗺️ gh 命令 ↔ 插件工具映射总表
|
|
269
303
|
|
|
270
304
|
#### 一、已覆盖域映射(语义化 action 精确对照)
|
|
271
305
|
|
|
272
|
-
**PR(github_pr,
|
|
306
|
+
**PR(github_pr,15 action)**
|
|
273
307
|
| gh 命令 | 插件调用 | 备注 |
|
|
274
308
|
| :--- | :--- | :--- |
|
|
275
|
-
| \`gh pr list/view/create/diff/edit/comment\` | \`github_pr(action:'list'/'view'/'create'/'diff'/'edit'/'comment')\` |
|
|
309
|
+
| \`gh pr list/view/create/diff/edit/comment\` | \`github_pr(action:'list'/'view'/'create'/'diff'/'edit'/'comment')\` | 高频基础操作;edit/comment 支持 attach_images |
|
|
310
|
+
| (PR 图片查看,gh 无对应命令) | \`github_pr(action:'view_images', pr_number:n)\` | 插件独有:正文+评论图片下载并转为可读图像块 |
|
|
276
311
|
| \`gh pr close\` | 暂无专用 action——用 \`github_pr(action:'edit')\` 或 \`github_api(endpoint:'repos/{o}/{r}/pulls/{n}', method:'PATCH', fields:{state:'closed'})\` | 插件侧未覆盖 |
|
|
277
312
|
| \`gh pr checks\` | \`github_pr(action:'checks', pr_number:n)\` | CI 状态药丸 |
|
|
278
313
|
| \`gh pr review\` | \`github_pr(action:'review', review_decision:'APPROVE'…)\` | |
|
|
@@ -287,6 +322,7 @@ const KNOWLEDGE_BASE = {
|
|
|
287
322
|
| gh 命令 | 插件调用 | 备注 |
|
|
288
323
|
| :--- | :--- | :--- |
|
|
289
324
|
| \`gh issue list/view/create/comment/close/reopen/edit\` | \`github_issue(action:…)\` | 基础操作 |
|
|
325
|
+
| (Issue 图片查看,gh 无对应命令) | \`github_issue(action:'view_images', issue_number:n)\` | 插件独有:报障截图转图像块 |
|
|
290
326
|
| \`gh issue develop\` | \`github_issue(action:'develop', branch_name:…)\` | 关联开发分支 |
|
|
291
327
|
| \`gh issue delete\` | \`github_issue(action:'delete', confirm:true)\` | 物理删除(非 close) |
|
|
292
328
|
| \`gh issue transfer\` | \`github_issue(action:'transfer', target_repo:…, confirm:true)\` | 跨仓库迁移 |
|
|
@@ -325,8 +361,11 @@ const KNOWLEDGE_BASE = {
|
|
|
325
361
|
| \`gh variable list/set/get/delete\` | \`github_secret(action:…, kind:'variable')\` | CI 明文:-b 直传、get 可回显 |
|
|
326
362
|
| (gh secret get 不存在) | — | GitHub 侧加密存储,物理不可读取 |
|
|
327
363
|
|
|
328
|
-
**Skill(github_skill,
|
|
329
|
-
|
|
364
|
+
**Skill(github_skill,6 action)**
|
|
365
|
+
| gh 命令 | 插件调用 | 备注 |
|
|
366
|
+
| :--- | :--- | :--- |
|
|
367
|
+
| \`gh skill search/preview/install/list/publish\` | \`github_skill(action:…)\` | install 需 confirm(写磁盘);publish 默认 dry-run 安全自检,正式发布需 tag 与 confirm |
|
|
368
|
+
| \`gh skill update\` | \`github_skill(action:'update')\` | 默认 dry-run 检查;\`all:true\` 或 \`skill_names\` 定向;\`dry_run:false\` 才真正更新 |
|
|
330
369
|
|
|
331
370
|
**Label(github_label,5 action)**
|
|
332
371
|
| gh 命令 | 插件调用 | 备注 |
|
|
@@ -348,7 +387,7 @@ const KNOWLEDGE_BASE = {
|
|
|
348
387
|
| gh 命令 | 插件调用 | 备注 |
|
|
349
388
|
| :--- | :--- | :--- |
|
|
350
389
|
| \`gh cache list\` | \`github_cache(action:'list')\` | 缓存盘点与过滤 |
|
|
351
|
-
| \`gh cache delete\` | \`github_cache(action:'delete', cache_id
|
|
390
|
+
| \`gh cache delete\` | \`github_cache(action:'delete', cache_id:…, confirm:true)\` | 三态均需 confirm |
|
|
352
391
|
|
|
353
392
|
**Gist(github_gist,5 action)**
|
|
354
393
|
| gh 命令 | 插件调用 | 备注 |
|
|
@@ -358,6 +397,14 @@ const KNOWLEDGE_BASE = {
|
|
|
358
397
|
| \`gh gist edit\` | \`github_gist(action:'edit', gist_id:…)\` | 描述与文件修改 |
|
|
359
398
|
| \`gh gist delete\` | \`github_gist(action:'delete', gist_id:…, confirm:true)\` | 物理删除,需 confirm |
|
|
360
399
|
|
|
400
|
+
**Task(github_task,3 action)**
|
|
401
|
+
|
|
402
|
+
| gh 命令 | 插件调用 | 说明 |
|
|
403
|
+
| :--- | :--- | :--- |
|
|
404
|
+
| \`gh pr view\` + \`gh pr diff\` + \`gh pr checks\` | \`github_task(action:'review_pr')\` | 一次拿齐审阅上下文(可选写回行级评注) |
|
|
405
|
+
| \`gh run view --json jobs\` + \`--log-failed\` | \`github_task(action:'diagnose_ci')\` | 定位失败 job/step 并萃取日志锚点 |
|
|
406
|
+
| \`git describe\` + \`git log\` | \`github_task(action:'prepare_release')\` | 推导版本 + 生成 notes 草稿(不发版) |
|
|
407
|
+
|
|
361
408
|
**Status(github_status,2 action)**
|
|
362
409
|
| gh 命令 | 插件调用 | 备注 |
|
|
363
410
|
| :--- | :--- | :--- |
|
|
@@ -402,6 +449,7 @@ const KNOWLEDGE_BASE = {
|
|
|
402
449
|
|
|
403
450
|
- \`gh workflow …\` / \`gh search …\` / \`gh variable …\` 分别归 **github_run / github_repo / github_secret**(不是独立工具)
|
|
404
451
|
- \`gh api\` 的等价物是 \`github_api\`(全端点逃生门,支持 raw/paginate/jq)
|
|
452
|
+
- 人类侧在线查看(PR / Issue / Actions Run 详情弹窗)由插件 HTTP 路由提供(\`/api/github/pr-detail\`、\`/api/github/issue-detail\`、\`/api/github/run-detail\`、\`/api/github/run-logs\`),**不是工具 action**;Agent 侧对应能力分别是 \`github_pr(action:'view')\`、\`github_issue(action:'view')\`、\`github_run(action:'view')\`
|
|
405
453
|
- 图片查看:\`github_issue\`/\`github_pr\` 的 \`view_images\`、\`github_repo\` 的 \`readme\` + image_urls(插件独有能力,gh 无对应命令)
|
|
406
454
|
- 附件上传:\`github_issue\`/\`github_pr\` 的 \`attach_images\` 映射 gh ≥2.99 原生可重复 \`--attach\` flag(支持 create/edit/comment)
|
|
407
455
|
|
|
@@ -457,10 +505,16 @@ const KNOWLEDGE_BASE = {
|
|
|
457
505
|
- *解法*:需管理员前往 GitHub 网页端 \`Settings -> General -> Features\` 勾选开启 \`Discussions\`。
|
|
458
506
|
8. **操作被拦截提示 \`Awaiting confirmation\`**:
|
|
459
507
|
- *原因*:对于高危破坏性操作(如合并 PR、删除分支、物理删除 Issue、清空构建缓存、删除密钥、销毁 Codespace),插件设置了强制安全确认防线,未确认时绝对不触发底层 CLI。
|
|
460
|
-
- *解法*:审阅影响评估报告后,在参数中显式传入 \`confirm: true\`
|
|
508
|
+
- *解法*:审阅影响评估报告后,在参数中显式传入 \`confirm: true\` 即可放行。
|
|
509
|
+
9. **\`log_failed\` 拉取很慢 / 报 \`unexpected EOF\` / 连接被重置**:
|
|
510
|
+
- *原因*:GitHub 日志接口是「先打包再下载」,实测单次 4–40 秒,走代理时可能被中途掐断;矩阵构建下日志体量随 job 数线性增长。
|
|
511
|
+
- *解法*:① 先 \`github_run(action:'view')\` 确认到底哪个 job 挂了,只对失败 job 重试;② 换 \`github_task(action:'diagnose_ci')\` 走一次性的结论包(内部同样是锚点萃取,但会顺带汇总相关文件);③ 仍失败时改用 \`github_api\` 直接拉 job 日志端点,或让用户到 Actions 页面查看完整日志。
|
|
512
|
+
10. **人类侧「提取失败日志」为何只给一小段 / 为什么最多 3 个 job**:
|
|
513
|
+
- *原因*:弹窗内展示的是**锚点萃取后的片段**(失败块 + 上下文,无锚点时回退尾部 200 行),不是完整日志;一次点击最多串行拉 3 个失败 job,服务端总预算 90 秒——这是为了不让一次点击把面板卡住几分钟。
|
|
514
|
+
- *解法*:需要完整日志或更多 job 时,让用户点弹窗右上角「GitHub ↗」,或由 Agent 用 \`github_run(action:'log_failed')\` / \`github_task(action:'diagnose_ci')\` 分批处理。`
|
|
461
515
|
};
|
|
462
516
|
export function createHelpTool(executor) {
|
|
463
|
-
return {
|
|
517
|
+
return wrapWithGate({
|
|
464
518
|
name: 'github_help',
|
|
465
519
|
description: 'GitHub 工具箱使用指南与官方命令手册。当你不确定该用哪个工具、需要特定操作的高级语法与示例、或排查 GitHub 报错时调用。',
|
|
466
520
|
parameters: {
|
|
@@ -487,6 +541,7 @@ export function createHelpTool(executor) {
|
|
|
487
541
|
'discussion',
|
|
488
542
|
'project',
|
|
489
543
|
'codespace',
|
|
544
|
+
'task',
|
|
490
545
|
'mapping',
|
|
491
546
|
'search_syntax',
|
|
492
547
|
'troubleshooting',
|
|
@@ -511,8 +566,7 @@ export function createHelpTool(executor) {
|
|
|
511
566
|
},
|
|
512
567
|
},
|
|
513
568
|
async execute(args, execContext) {
|
|
514
|
-
const cwd
|
|
515
|
-
const sessionId = execContext?.sessionId || execContext?.session?.id || process.env.DSH_SESSION_ID;
|
|
569
|
+
const { cwd, sessionId } = executor.resolveExecScope(execContext);
|
|
516
570
|
// 1. 若显式指定了 gh_command,直接透传查询官方离线手册
|
|
517
571
|
if (args.gh_command && typeof args.gh_command === 'string') {
|
|
518
572
|
let parts = args.gh_command.trim().split(/\s+/);
|
|
@@ -542,5 +596,5 @@ export function createHelpTool(executor) {
|
|
|
542
596
|
result: `未找到主题「${rawTopic}」的指南。可用主题:${Object.keys(KNOWLEDGE_BASE).join(', ')}。`,
|
|
543
597
|
};
|
|
544
598
|
},
|
|
545
|
-
};
|
|
599
|
+
});
|
|
546
600
|
}
|
|
@@ -37,6 +37,33 @@ export declare function sniffImageMime(buf: Buffer): string | null;
|
|
|
37
37
|
/** 图片下载护栏:按真实资源消耗计算——单图字节与目录总量,无张数截断 */
|
|
38
38
|
export declare const IMG_MAX_BYTES: number;
|
|
39
39
|
export declare const IMG_DIR_MAX_BYTES: number;
|
|
40
|
+
export declare const IMG_DIR_LOW_WATERMARK: number;
|
|
41
|
+
/**
|
|
42
|
+
* 图片落盘目录(按内容寻址命名,重复下载天然去重)。
|
|
43
|
+
*
|
|
44
|
+
* 为什么支持 DSH_IMAGE_DIR 覆盖:目录里有 LRU 淘汰,按 mtime 删最旧文件。
|
|
45
|
+
* 若测试把 13MB 假数据写进真实用户目录,淘汰就会优先命中用户更早下载的
|
|
46
|
+
* 真图(用户图 mtime 更老,排序更靠前),造成静默数据丢失。
|
|
47
|
+
* 测试必须能把目录重定向到临时区——这与 upgrade-manager 的 DSH_CLI_BIN /
|
|
48
|
+
* DSH_PROFILE_DIR 覆盖是同一套思路。
|
|
49
|
+
*/
|
|
50
|
+
export declare function imageStoreDir(): string;
|
|
51
|
+
/** 粗略统计图片目录现有总量(遍历一次,文件数有护栏上限不会失控) */
|
|
52
|
+
export declare function imageDirSize(dir: string): number;
|
|
53
|
+
export interface LruEvictionResult {
|
|
54
|
+
freedBytes: number;
|
|
55
|
+
evictedCount: number;
|
|
56
|
+
evictedFiles: string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* LRU 图片缓存淘汰:当目录空间不足以容纳新图片时,按最后修改时间(mtime)淘汰最旧的历史文件。
|
|
60
|
+
*
|
|
61
|
+
* 护栏纪律:
|
|
62
|
+
* 1. 保护当前批次已写入的文件(protectFiles):避免同一批次内前一张图被后一张图立即顶替;
|
|
63
|
+
* 2. 仅淘汰历史文件:按 mtime 升序排列(最旧优先),逐一清理直到目录大小降至目标低水位;
|
|
64
|
+
* 3. 容错防御:单文件删除失败(如文件被锁定)静默跳过继续尝试下一个,绝不崩溃中断。
|
|
65
|
+
*/
|
|
66
|
+
export declare function evictLruImages(dir: string, neededBytes: number, protectFiles?: Set<string>, maxDirBytes?: number, targetBytes?: number): LruEvictionResult;
|
|
40
67
|
export interface DownloadedImage {
|
|
41
68
|
file: string;
|
|
42
69
|
mimeType: string;
|
package/lib/tools/image-utils.js
CHANGED
|
@@ -132,12 +132,24 @@ export function sniffImageMime(buf) {
|
|
|
132
132
|
/** 图片下载护栏:按真实资源消耗计算——单图字节与目录总量,无张数截断 */
|
|
133
133
|
export const IMG_MAX_BYTES = 5 * 1024 * 1024; // 单图 5MB
|
|
134
134
|
export const IMG_DIR_MAX_BYTES = 15 * 1024 * 1024; // 图片缓存目录总量 15MB
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
export const IMG_DIR_LOW_WATERMARK = 10 * 1024 * 1024; // LRU 淘汰低水位目标 10MB
|
|
136
|
+
/**
|
|
137
|
+
* 图片落盘目录(按内容寻址命名,重复下载天然去重)。
|
|
138
|
+
*
|
|
139
|
+
* 为什么支持 DSH_IMAGE_DIR 覆盖:目录里有 LRU 淘汰,按 mtime 删最旧文件。
|
|
140
|
+
* 若测试把 13MB 假数据写进真实用户目录,淘汰就会优先命中用户更早下载的
|
|
141
|
+
* 真图(用户图 mtime 更老,排序更靠前),造成静默数据丢失。
|
|
142
|
+
* 测试必须能把目录重定向到临时区——这与 upgrade-manager 的 DSH_CLI_BIN /
|
|
143
|
+
* DSH_PROFILE_DIR 覆盖是同一套思路。
|
|
144
|
+
*/
|
|
145
|
+
export function imageStoreDir() {
|
|
146
|
+
const override = (process.env.DSH_IMAGE_DIR || '').trim();
|
|
147
|
+
if (override)
|
|
148
|
+
return path.resolve(override);
|
|
137
149
|
return path.join(os.homedir(), '.dsh', 'data', 'dsh-github-flow', 'images');
|
|
138
150
|
}
|
|
139
151
|
/** 粗略统计图片目录现有总量(遍历一次,文件数有护栏上限不会失控) */
|
|
140
|
-
function imageDirSize(dir) {
|
|
152
|
+
export function imageDirSize(dir) {
|
|
141
153
|
let total = 0;
|
|
142
154
|
try {
|
|
143
155
|
for (const f of fs.readdirSync(dir)) {
|
|
@@ -150,6 +162,57 @@ function imageDirSize(dir) {
|
|
|
150
162
|
catch { }
|
|
151
163
|
return total;
|
|
152
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* LRU 图片缓存淘汰:当目录空间不足以容纳新图片时,按最后修改时间(mtime)淘汰最旧的历史文件。
|
|
167
|
+
*
|
|
168
|
+
* 护栏纪律:
|
|
169
|
+
* 1. 保护当前批次已写入的文件(protectFiles):避免同一批次内前一张图被后一张图立即顶替;
|
|
170
|
+
* 2. 仅淘汰历史文件:按 mtime 升序排列(最旧优先),逐一清理直到目录大小降至目标低水位;
|
|
171
|
+
* 3. 容错防御:单文件删除失败(如文件被锁定)静默跳过继续尝试下一个,绝不崩溃中断。
|
|
172
|
+
*/
|
|
173
|
+
export function evictLruImages(dir, neededBytes, protectFiles = new Set(), maxDirBytes = IMG_DIR_MAX_BYTES, targetBytes = IMG_DIR_LOW_WATERMARK) {
|
|
174
|
+
const result = { freedBytes: 0, evictedCount: 0, evictedFiles: [] };
|
|
175
|
+
if (!fs.existsSync(dir))
|
|
176
|
+
return result;
|
|
177
|
+
let currentSize = imageDirSize(dir);
|
|
178
|
+
if (currentSize + neededBytes <= maxDirBytes) {
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
const candidates = [];
|
|
182
|
+
try {
|
|
183
|
+
const entries = fs.readdirSync(dir);
|
|
184
|
+
for (const name of entries) {
|
|
185
|
+
const fullPath = path.join(dir, name);
|
|
186
|
+
if (protectFiles.has(fullPath))
|
|
187
|
+
continue;
|
|
188
|
+
try {
|
|
189
|
+
const stat = fs.statSync(fullPath);
|
|
190
|
+
if (stat.isFile()) {
|
|
191
|
+
candidates.push({ path: fullPath, size: stat.size, mtime: stat.mtimeMs });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch { }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
candidates.sort((a, b) => a.mtime - b.mtime);
|
|
201
|
+
for (const item of candidates) {
|
|
202
|
+
if (currentSize + neededBytes <= maxDirBytes && currentSize <= targetBytes) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
fs.unlinkSync(item.path);
|
|
207
|
+
currentSize -= item.size;
|
|
208
|
+
result.freedBytes += item.size;
|
|
209
|
+
result.evictedCount++;
|
|
210
|
+
result.evictedFiles.push(item.path);
|
|
211
|
+
}
|
|
212
|
+
catch { }
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
153
216
|
/**
|
|
154
217
|
* 交集过滤:把调用方请求的 URL 硬约束在正文实际提取的合法清单内。
|
|
155
218
|
* 为什么必须交集而非直接信任传入:image_urls 来自模型生成参数,存在幻觉或
|
|
@@ -190,6 +253,7 @@ export async function downloadImages(urls, run) {
|
|
|
190
253
|
const images = [];
|
|
191
254
|
const failures = [];
|
|
192
255
|
const remaining = [];
|
|
256
|
+
const currentBatchFiles = new Set();
|
|
193
257
|
let budgetExhausted = false;
|
|
194
258
|
// 无张数截断:字节护栏按真实资源消耗逐张判定。触顶后仍继续遍历——
|
|
195
259
|
// 剩余 URL 全部收进 remaining 供 Agent 分批续传,而非静默丢弃。
|
|
@@ -213,6 +277,11 @@ export async function downloadImages(urls, run) {
|
|
|
213
277
|
failures.push(`${url.slice(-24)}: 内容不是图片(魔数校验失败)`);
|
|
214
278
|
continue;
|
|
215
279
|
}
|
|
280
|
+
// 空间护栏检查:若超出目录上限,优先触发 LRU 淘汰历史文件以腾出预算
|
|
281
|
+
if (dirSize + buf.length > IMG_DIR_MAX_BYTES) {
|
|
282
|
+
const { freedBytes } = evictLruImages(dir, buf.length, currentBatchFiles);
|
|
283
|
+
dirSize = Math.max(0, dirSize - freedBytes);
|
|
284
|
+
}
|
|
216
285
|
if (dirSize + buf.length > IMG_DIR_MAX_BYTES) {
|
|
217
286
|
budgetExhausted = true;
|
|
218
287
|
remaining.push(url);
|
|
@@ -222,6 +291,7 @@ export async function downloadImages(urls, run) {
|
|
|
222
291
|
const ext = mime.includes('svg') ? 'svg' : mime.split('/')[1].replace('jpeg', 'jpg');
|
|
223
292
|
const file = path.join(dir, `${crypto.createHash('sha256').update(buf).digest('hex').slice(0, 16)}.${ext}`);
|
|
224
293
|
fs.writeFileSync(file, buf);
|
|
294
|
+
currentBatchFiles.add(file);
|
|
225
295
|
// 累计已写入量:下张图的总量护栏判定必须基于最新值(否则多图可突破上限)
|
|
226
296
|
dirSize += buf.length;
|
|
227
297
|
images.push({ file, mimeType: mime, url, bytes: buf.length });
|