dsh-github-flow 0.4.0 → 0.6.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 +91 -12
- package/README.md +91 -12
- package/docs/UPGRADE-NOTES.md +9 -12
- package/docs/architecture.md +63 -29
- package/docs/images/1.png +0 -0
- package/lib/api/routes-actions.js +268 -5
- package/lib/api/routes-overview.js +147 -29
- package/lib/cache.d.ts +77 -4
- package/lib/cache.js +114 -7
- package/lib/client.d.ts +9 -4
- package/lib/client.js +5293 -942
- package/lib/commands/gh.d.ts +6 -1
- package/lib/commands/gh.js +144 -71
- package/lib/executor/error-classifier.d.ts +55 -0
- package/lib/executor/error-classifier.js +199 -0
- package/lib/executor/git-parser.d.ts +157 -1
- package/lib/executor/git-parser.js +513 -4
- package/lib/executor.d.ts +147 -4
- package/lib/executor.js +866 -77
- 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 +129 -14
- package/lib/tools/image-utils.d.ts +27 -0
- package/lib/tools/image-utils.js +73 -3
- 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 +336 -0
- package/lib/ui/commit-modal.js +144 -14
- package/lib/ui/help-modal.d.ts +21 -0
- package/lib/ui/help-modal.js +300 -0
- package/lib/ui/issue-modal.d.ts +14 -0
- package/lib/ui/issue-modal.js +366 -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 +516 -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/storage.d.ts +56 -0
- package/lib/ui/storage.js +163 -0
- package/lib/ui/styles.d.ts +1 -1
- package/lib/ui/styles.js +1219 -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 +172 -0
- package/lib/ui/workspace-panel.js +198 -31
- 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/renderers/skill.js
CHANGED
|
@@ -14,6 +14,11 @@ export function renderSkillMarkdown(args, value) {
|
|
|
14
14
|
}
|
|
15
15
|
if (typeof value.result === 'string')
|
|
16
16
|
return value.result;
|
|
17
|
+
// 6.0 特化 action 处理(update)
|
|
18
|
+
if (args?.action === 'update' || value?.action === 'update') {
|
|
19
|
+
const extra = value.output || value.detail;
|
|
20
|
+
return `### 🔄 ${value.message || 'Agent Skills 更新结果'}\n` + (extra ? `\`\`\`text\n${String(extra).trim()}\n\`\`\`` : '');
|
|
21
|
+
}
|
|
17
22
|
if (typeof value.message === 'string' && !value.items) {
|
|
18
23
|
const extra = value.detail || value.output;
|
|
19
24
|
return `**${value.message}**` + (extra ? `\n\`\`\`\n${extra}\n\`\`\`` : '');
|
|
@@ -56,6 +61,181 @@ export function renderSkillMarkdown(args, value) {
|
|
|
56
61
|
...rows,
|
|
57
62
|
].join('\n');
|
|
58
63
|
}
|
|
64
|
+
// 6.3 Skill 校验与发布报告
|
|
65
|
+
if (args?.action === 'publish' || value?.mode === 'dry-run' || value?.mode === 'fix' || value?.mode === 'publish') {
|
|
66
|
+
return renderSkillPublishMarkdown(value);
|
|
67
|
+
}
|
|
59
68
|
// 兜底降级
|
|
60
69
|
return JSON.stringify(value, null, 2);
|
|
61
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* 解析 gh skill publish 命令输出的 TSV 诊断行与状态汇总
|
|
73
|
+
*
|
|
74
|
+
* 核心设计理由与约束:
|
|
75
|
+
* 1. gh skill publish 命令无论成功还是失败,均采用制表符 `\t` 分隔的 TSV 纯文本行输出;
|
|
76
|
+
* 2. 诊断行格式:`error\t<skill>\t<msg>`、`warning\t<skill>\t<msg>`、`fixed\t<skill>\t<msg>` 或 `warning\t\t<repo_rule>`;
|
|
77
|
+
* 3. 校验失败时 exitCode 为 1,但其 stdout 中携带精准的错误项列表,需提取后结构化呈现,绝不能当作盲目异常吞没;
|
|
78
|
+
* 4. 若未匹配到有效技能或遇到无技能目录,从 stderr 提取根因提示。
|
|
79
|
+
*/
|
|
80
|
+
export function parseSkillPublishOutput(rawStdout, rawStderr, exitOk, mode, directory, tag) {
|
|
81
|
+
const items = [];
|
|
82
|
+
const repoWarnings = [];
|
|
83
|
+
let summary = '';
|
|
84
|
+
const combined = `${rawStdout || ''}\n${rawStderr || ''}`;
|
|
85
|
+
const lines = combined.split(/\r?\n/).map(l => l.trimEnd()).filter(Boolean);
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
// 匹配 TSV 诊断行:error/warning/fixed + \t + skillName + \t + message
|
|
88
|
+
const tsvMatch = line.match(/^(error|warning|fixed)\t([^\t]*)\t(.*)$/i);
|
|
89
|
+
if (tsvMatch) {
|
|
90
|
+
const type = tsvMatch[1].toLowerCase();
|
|
91
|
+
const skillName = tsvMatch[2].trim();
|
|
92
|
+
const message = tsvMatch[3].trim();
|
|
93
|
+
if (!skillName) {
|
|
94
|
+
// 技能名为空通常是仓库级规则警告(如 ruleset 缺少 tag 保护)
|
|
95
|
+
repoWarnings.push(message);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
items.push({ type, skillName, message });
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// 汇总与特征行识别
|
|
103
|
+
if (/validation failed with \d+ error\(s\)/i.test(line)) {
|
|
104
|
+
summary = line.trim();
|
|
105
|
+
}
|
|
106
|
+
else if (/Dry run complete/i.test(line)) {
|
|
107
|
+
if (!summary)
|
|
108
|
+
summary = line.trim();
|
|
109
|
+
}
|
|
110
|
+
else if (/Fixed \d+ file\(s\)/i.test(line)) {
|
|
111
|
+
summary = line.trim();
|
|
112
|
+
}
|
|
113
|
+
else if (/no skills found in/i.test(line)) {
|
|
114
|
+
summary = line.trim();
|
|
115
|
+
}
|
|
116
|
+
else if (/Released/i.test(line) && mode === 'publish') {
|
|
117
|
+
if (!summary)
|
|
118
|
+
summary = line.trim();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// 判定最终业务成功态:
|
|
122
|
+
// 1. 若 items 中存在任一 error,则判定为失败;
|
|
123
|
+
// 2. 若 exitOk 为 false,即使没有 TSV 行也判定为失败;
|
|
124
|
+
// 3. dry-run 模式下无 error 且正常跑完判定为成功;
|
|
125
|
+
// 4. 若没有找到 skill(summary 包含 no skills found),视作失败并在 items 中追加说明项。
|
|
126
|
+
const hasErrors = items.some(i => i.type === 'error');
|
|
127
|
+
const noSkillsFound = summary.includes('no skills found in');
|
|
128
|
+
let success = exitOk && !hasErrors && !noSkillsFound;
|
|
129
|
+
if (noSkillsFound && items.length === 0) {
|
|
130
|
+
items.push({
|
|
131
|
+
type: 'error',
|
|
132
|
+
skillName: '-',
|
|
133
|
+
message: summary || '在目标目录下未检索到符合规范的 Agent Skill(需满足 skills/*/SKILL.md 等路径约束)',
|
|
134
|
+
});
|
|
135
|
+
success = false;
|
|
136
|
+
}
|
|
137
|
+
else if (!exitOk && items.length === 0 && rawStderr) {
|
|
138
|
+
// 其它非零退出且无 TSV 诊断行的情况,将 stderr 作为通用错误提示
|
|
139
|
+
items.push({
|
|
140
|
+
type: 'error',
|
|
141
|
+
skillName: '-',
|
|
142
|
+
message: rawStderr.trim(),
|
|
143
|
+
});
|
|
144
|
+
success = false;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
mode,
|
|
148
|
+
success,
|
|
149
|
+
directory,
|
|
150
|
+
items,
|
|
151
|
+
repoWarnings,
|
|
152
|
+
summary,
|
|
153
|
+
tag,
|
|
154
|
+
rawText: rawStdout.trim() || rawStderr.trim(),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Skill Publish 结构化 Markdown 渲染器
|
|
159
|
+
* 将体检报告、自动修复明细与正式发布结果转化为紧凑、清晰的表格与指引
|
|
160
|
+
*/
|
|
161
|
+
export function renderSkillPublishMarkdown(report) {
|
|
162
|
+
if (!report)
|
|
163
|
+
return '无返回内容';
|
|
164
|
+
if (report.status === 'AWAITING_CONFIRMATION') {
|
|
165
|
+
return [
|
|
166
|
+
`> ### ${report.warning || '⚠️ 操作确认拦截'}`,
|
|
167
|
+
`> - **影响评估**: ${report.impact || ''}`,
|
|
168
|
+
`> - **确认指引**: ${report.requiredAction || ''}`,
|
|
169
|
+
].join('\n');
|
|
170
|
+
}
|
|
171
|
+
const sections = [];
|
|
172
|
+
const dirHint = report.directory ? ` (目录: \`${report.directory}\`)` : '';
|
|
173
|
+
if (report.mode === 'dry-run') {
|
|
174
|
+
const errorCount = report.items.filter(i => i.type === 'error').length;
|
|
175
|
+
const warningCount = report.items.filter(i => i.type === 'warning').length;
|
|
176
|
+
if (report.success && errorCount === 0) {
|
|
177
|
+
sections.push(`### ✅ Agent Skills 合规校验通过!${dirHint}\n`);
|
|
178
|
+
sections.push('受检 Skill 均符合 agentskills.io 规范(命名规范、目录匹配、必需 frontmatter 字段均通过)。');
|
|
179
|
+
if (warningCount > 0) {
|
|
180
|
+
sections.push(`\n**建议优化项 (${warningCount} 项):**`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
sections.push(`### ⚠️ Agent Skills 校验发现问题 (${errorCount} 项错误${warningCount > 0 ? `,${warningCount} 项警告` : ''})${dirHint}\n`);
|
|
185
|
+
}
|
|
186
|
+
if (report.items.length > 0) {
|
|
187
|
+
const rows = report.items.map(item => {
|
|
188
|
+
let badge = 'ℹ️ 提示';
|
|
189
|
+
if (item.type === 'error')
|
|
190
|
+
badge = '❌ 错误';
|
|
191
|
+
else if (item.type === 'warning')
|
|
192
|
+
badge = '⚠️ 建议';
|
|
193
|
+
else if (item.type === 'fixed')
|
|
194
|
+
badge = '🛠️ 已修复';
|
|
195
|
+
return `| ${badge} | \`${sanitizeCell(item.skillName, 30)}\` | ${sanitizeCell(item.message, 80)} |`;
|
|
196
|
+
});
|
|
197
|
+
sections.push('| 级别 | Skill 名称 | 问题详情与指引 |\n| :--- | :--- | :--- |\n' + rows.join('\n'));
|
|
198
|
+
}
|
|
199
|
+
if (report.repoWarnings && report.repoWarnings.length > 0) {
|
|
200
|
+
sections.push('\n**仓库策略提醒:**\n' + report.repoWarnings.map(w => `- 💡 ${w}`).join('\n'));
|
|
201
|
+
}
|
|
202
|
+
if (report.success && errorCount === 0) {
|
|
203
|
+
sections.push('\n> 💡 **正式发版指引**: 若需创建 GitHub Release 正式发布技能,请指定版本 Tag 并附带 `confirm: true`:\n> `github_skill(action: "publish", tag: "v1.0.0", dry_run: false, confirm: true)`');
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
sections.push('\n> 🔍 **修复提示**: 若存在安装元数据残留,可先调用 `github_skill(action: "publish", fix: true)` 自动剥离清洗。');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (report.mode === 'fix') {
|
|
210
|
+
const fixedItems = report.items.filter(i => i.type === 'fixed');
|
|
211
|
+
sections.push(`### 🛠️ Agent Skills 元数据自动修复${report.success ? '完成' : '结果'}${dirHint}\n`);
|
|
212
|
+
if (fixedItems.length > 0) {
|
|
213
|
+
sections.push(`已自动清洗 **${fixedItems.length}** 处技能文件中的安装残留元数据(如 \`metadata.github-*\`):\n`);
|
|
214
|
+
const rows = fixedItems.map(item => `| \`${sanitizeCell(item.skillName, 30)}\` | ${sanitizeCell(item.message, 80)} |`);
|
|
215
|
+
sections.push('| Skill 名称 | 修复操作 |\n| :--- | :--- |\n' + rows.join('\n'));
|
|
216
|
+
sections.push('\n> 📌 **后续步骤**: 请使用 `git diff` 审查本地文件变更,提交 Git 后再重新运行合规体检或发布。');
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
sections.push(report.summary || '未检测到需要修复的元数据残留。');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (report.mode === 'publish') {
|
|
223
|
+
if (report.success) {
|
|
224
|
+
sections.push(`### 🚀 Agent Skill 发布成功!${dirHint}\n`);
|
|
225
|
+
sections.push(`- **版本 Tag**: \`${report.tag || '-'}\``);
|
|
226
|
+
sections.push('- **发布状态**: 已自动附加 `agent-skills` 仓库 Topic,并创建了关联 GitHub Release。');
|
|
227
|
+
if (report.summary)
|
|
228
|
+
sections.push(`\n${report.summary}`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
sections.push(`### ❌ Agent Skill 发布失败${dirHint}\n`);
|
|
232
|
+
if (report.summary)
|
|
233
|
+
sections.push(`- **原因**: ${report.summary}`);
|
|
234
|
+
if (report.items.length > 0) {
|
|
235
|
+
const rows = report.items.map(item => `| ${item.type} | \`${sanitizeCell(item.skillName, 30)}\` | ${sanitizeCell(item.message, 80)} |`);
|
|
236
|
+
sections.push('\n| 级别 | Skill 名称 | 详情 |\n| :--- | :--- | :--- |\n' + rows.join('\n'));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return sections.join('\n');
|
|
241
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TelemetrySummary } from '../telemetry.js';
|
|
1
2
|
export interface StatusSectionItem {
|
|
2
3
|
repo?: string;
|
|
3
4
|
number?: string;
|
|
@@ -21,3 +22,7 @@ export declare function parseStatusOutput(output: string): ParsedStatusReport;
|
|
|
21
22
|
* Status 领域渲染器:负责用户跨仓待办大盘的 Markdown 格式化
|
|
22
23
|
*/
|
|
23
24
|
export declare function renderStatusMarkdown(args: any, value: any): string;
|
|
25
|
+
/**
|
|
26
|
+
* 渲染 DSH GitHub 工具链健康度与降级率审计报表
|
|
27
|
+
*/
|
|
28
|
+
export declare function renderHealthAuditMarkdown(summary: TelemetrySummary): string;
|
package/lib/renderers/status.js
CHANGED
|
@@ -90,6 +90,9 @@ export function parseStatusOutput(output) {
|
|
|
90
90
|
export function renderStatusMarkdown(args, value) {
|
|
91
91
|
if (!value)
|
|
92
92
|
return '无返回内容';
|
|
93
|
+
if (value?.healthScore !== undefined || value?.degradationRate !== undefined || args?.action === 'health') {
|
|
94
|
+
return renderHealthAuditMarkdown(value);
|
|
95
|
+
}
|
|
93
96
|
const report = value.parsed || parseStatusOutput(value.rawText || value.output || '');
|
|
94
97
|
const renderSection = (title, icon, items, emptyText) => {
|
|
95
98
|
if (!items || items.length === 0) {
|
|
@@ -122,3 +125,52 @@ export function renderStatusMarkdown(args, value) {
|
|
|
122
125
|
'> 💡 **待办操作建议**: 可通过 `github_issue(action: \'view\')` 或 `github_pr(action: \'checkout\')` 快速检视对应议题并展开工作。',
|
|
123
126
|
].join('\n');
|
|
124
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* 渲染 DSH GitHub 工具链健康度与降级率审计报表
|
|
130
|
+
*/
|
|
131
|
+
export function renderHealthAuditMarkdown(summary) {
|
|
132
|
+
if (!summary)
|
|
133
|
+
return '无遥测健康度数据';
|
|
134
|
+
let badge = '🟢 极佳';
|
|
135
|
+
if (summary.statusLevel === 'NEEDS_ATTENTION') {
|
|
136
|
+
badge = '🔴 需关注';
|
|
137
|
+
}
|
|
138
|
+
else if (summary.statusLevel === 'GOOD') {
|
|
139
|
+
badge = '🟡 良好';
|
|
140
|
+
}
|
|
141
|
+
const successRate = summary.totalCalls > 0
|
|
142
|
+
? `${((summary.successCount / summary.totalCalls) * 100).toFixed(1)}%`
|
|
143
|
+
: '100%';
|
|
144
|
+
const lines = [
|
|
145
|
+
'### 🩺 DSH GitHub 工具链健康度与降级率审计报表\n',
|
|
146
|
+
`- **综合健康分**: ${badge} (${summary.healthScore} / 100 分)`,
|
|
147
|
+
`- **统计样本量**: 最近 ${summary.totalCalls} 次 Agent 工具调用`,
|
|
148
|
+
`- **API 降级率**: \`${summary.degradationRate}%\` (${summary.degradedCount} 次触发降级链路)`,
|
|
149
|
+
`- **安全防御拦截**: ${summary.confirmBlockedCount + summary.scopeBlockedCount} 次 (含 ${summary.scopeBlockedCount} 次权限缺失,${summary.confirmBlockedCount} 次确认防线)`,
|
|
150
|
+
`- **执行成功率**: \`${successRate}\` (${summary.successCount} 成功, ${summary.failureCount} 失败)`,
|
|
151
|
+
];
|
|
152
|
+
// 工具调用分布表
|
|
153
|
+
const toolNames = Object.keys(summary.toolStats || {});
|
|
154
|
+
if (toolNames.length > 0) {
|
|
155
|
+
const sorted = toolNames.sort((a, b) => summary.toolStats[b].total - summary.toolStats[a].total);
|
|
156
|
+
const rows = sorted.map((name) => {
|
|
157
|
+
const s = summary.toolStats[name];
|
|
158
|
+
const rate = s.total > 0 ? `${((s.success / s.total) * 100).toFixed(0)}%` : '100%';
|
|
159
|
+
let toolBadge = '🟢 正常';
|
|
160
|
+
if (s.degraded > 0)
|
|
161
|
+
toolBadge = '🟡 存在降级';
|
|
162
|
+
if (s.failed > 0 && s.success === 0)
|
|
163
|
+
toolBadge = '🔴 异常';
|
|
164
|
+
return `| \`${name}\` | ${s.total} | ${rate} | ${s.degraded} | ${toolBadge} |`;
|
|
165
|
+
});
|
|
166
|
+
lines.push('\n**核心工具调用分布:**', '| 工具名称 | 调用次数 | 成功率 | 降级次数 | 状态 |', '| :--- | :---: | :---: | :---: | :--- |', ...rows);
|
|
167
|
+
}
|
|
168
|
+
// 自愈优化建议
|
|
169
|
+
if (Array.isArray(summary.recommendations) && summary.recommendations.length > 0) {
|
|
170
|
+
lines.push('\n> 💡 **优化与自愈建议:**');
|
|
171
|
+
summary.recommendations.forEach((rec) => {
|
|
172
|
+
lines.push(`> - ${rec}`);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH GitHub Flow - 运行时遥测审计与降级率收集器
|
|
3
|
+
*
|
|
4
|
+
* 遵循 /dsh-plugin-dev-skill 规范,利用 Cordis 的 tools/result 事件总线
|
|
5
|
+
* 建立内存中固定容量的轻量无锁环形队列,实现零外部数据库依赖、常驻内存严格 <200KB
|
|
6
|
+
* 的工业级运行时可观测性,量化分析 18 大工具矩阵的 API 降级率与健康度指标。
|
|
7
|
+
*/
|
|
8
|
+
export interface TelemetryRecord {
|
|
9
|
+
id: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
toolName: string;
|
|
12
|
+
action?: string;
|
|
13
|
+
durationMs?: number;
|
|
14
|
+
success: boolean;
|
|
15
|
+
degraded: boolean;
|
|
16
|
+
scopeBlocked: boolean;
|
|
17
|
+
confirmBlocked: boolean;
|
|
18
|
+
errorType?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ToolUsageStat {
|
|
21
|
+
total: number;
|
|
22
|
+
success: number;
|
|
23
|
+
degraded: number;
|
|
24
|
+
failed: number;
|
|
25
|
+
blocked: number;
|
|
26
|
+
}
|
|
27
|
+
export interface TelemetrySummary {
|
|
28
|
+
totalCalls: number;
|
|
29
|
+
successCount: number;
|
|
30
|
+
failureCount: number;
|
|
31
|
+
degradedCount: number;
|
|
32
|
+
degradationRate: number;
|
|
33
|
+
scopeBlockedCount: number;
|
|
34
|
+
confirmBlockedCount: number;
|
|
35
|
+
healthScore: number;
|
|
36
|
+
statusLevel: 'EXCELLENT' | 'GOOD' | 'NEEDS_ATTENTION';
|
|
37
|
+
toolStats: Record<string, ToolUsageStat>;
|
|
38
|
+
recommendations: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 内存固定容量环形队列(无锁、确定性内存上限)
|
|
42
|
+
*/
|
|
43
|
+
export declare class TelemetryRingBuffer {
|
|
44
|
+
private buffer;
|
|
45
|
+
private readonly capacity;
|
|
46
|
+
constructor(capacity?: number);
|
|
47
|
+
push(record: TelemetryRecord): void;
|
|
48
|
+
getAll(): TelemetryRecord[];
|
|
49
|
+
size(): number;
|
|
50
|
+
clear(): void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 遥测收集与健康分析引擎
|
|
54
|
+
*/
|
|
55
|
+
export declare class TelemetryCollector {
|
|
56
|
+
private ringBuffer;
|
|
57
|
+
private callCounter;
|
|
58
|
+
constructor(capacity?: number);
|
|
59
|
+
/**
|
|
60
|
+
* 记录一次工具调用结果(Fail-Open 保护,绝不阻滞主流程)
|
|
61
|
+
*/
|
|
62
|
+
recordCall(name: string, args: any, result: any, durationMs?: number): void;
|
|
63
|
+
/**
|
|
64
|
+
* 生成工具链健康度与降级率统计大盘
|
|
65
|
+
*/
|
|
66
|
+
getSummary(): TelemetrySummary;
|
|
67
|
+
clear(): void;
|
|
68
|
+
}
|
|
69
|
+
/** 全局单例收集器实例 */
|
|
70
|
+
export declare const defaultTelemetryCollector: TelemetryCollector;
|
package/lib/telemetry.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH GitHub Flow - 运行时遥测审计与降级率收集器
|
|
3
|
+
*
|
|
4
|
+
* 遵循 /dsh-plugin-dev-skill 规范,利用 Cordis 的 tools/result 事件总线
|
|
5
|
+
* 建立内存中固定容量的轻量无锁环形队列,实现零外部数据库依赖、常驻内存严格 <200KB
|
|
6
|
+
* 的工业级运行时可观测性,量化分析 18 大工具矩阵的 API 降级率与健康度指标。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 内存固定容量环形队列(无锁、确定性内存上限)
|
|
10
|
+
*/
|
|
11
|
+
export class TelemetryRingBuffer {
|
|
12
|
+
buffer = [];
|
|
13
|
+
capacity;
|
|
14
|
+
constructor(capacity = 200) {
|
|
15
|
+
this.capacity = Math.max(1, capacity);
|
|
16
|
+
}
|
|
17
|
+
push(record) {
|
|
18
|
+
if (this.buffer.length >= this.capacity) {
|
|
19
|
+
this.buffer.shift(); // 先进先出丢弃最旧的一条
|
|
20
|
+
}
|
|
21
|
+
this.buffer.push(record);
|
|
22
|
+
}
|
|
23
|
+
getAll() {
|
|
24
|
+
return [...this.buffer];
|
|
25
|
+
}
|
|
26
|
+
size() {
|
|
27
|
+
return this.buffer.length;
|
|
28
|
+
}
|
|
29
|
+
clear() {
|
|
30
|
+
this.buffer = [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 遥测收集与健康分析引擎
|
|
35
|
+
*/
|
|
36
|
+
export class TelemetryCollector {
|
|
37
|
+
ringBuffer;
|
|
38
|
+
callCounter = 0;
|
|
39
|
+
constructor(capacity = 200) {
|
|
40
|
+
this.ringBuffer = new TelemetryRingBuffer(capacity);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 记录一次工具调用结果(Fail-Open 保护,绝不阻滞主流程)
|
|
44
|
+
*/
|
|
45
|
+
recordCall(name, args, result, durationMs) {
|
|
46
|
+
try {
|
|
47
|
+
if (!name || typeof name !== 'string')
|
|
48
|
+
return;
|
|
49
|
+
this.callCounter++;
|
|
50
|
+
const action = typeof args?.action === 'string' ? args.action : undefined;
|
|
51
|
+
const isScopeBlocked = result?.status === 'SCOPE_REQUIRED';
|
|
52
|
+
const isConfirmBlocked = result?.status === 'AWAITING_CONFIRMATION';
|
|
53
|
+
const isBlocked = isScopeBlocked || isConfirmBlocked;
|
|
54
|
+
const isDegraded = result?.degraded === true || result?.data?.degraded === true;
|
|
55
|
+
const isFailed = !isBlocked && (result?.ok === false || result?.success === false || !!result?.error);
|
|
56
|
+
const record = {
|
|
57
|
+
id: `t_${Date.now()}_${this.callCounter}`,
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
toolName: name,
|
|
60
|
+
action,
|
|
61
|
+
durationMs: typeof durationMs === 'number' && durationMs >= 0 ? durationMs : undefined,
|
|
62
|
+
success: !isFailed && !isBlocked,
|
|
63
|
+
degraded: isDegraded,
|
|
64
|
+
scopeBlocked: isScopeBlocked,
|
|
65
|
+
confirmBlocked: isConfirmBlocked,
|
|
66
|
+
errorType: isFailed ? String(result?.error || 'UNKNOWN_ERROR').slice(0, 50) : undefined,
|
|
67
|
+
};
|
|
68
|
+
this.ringBuffer.push(record);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// 遥测打点非阻塞隔离:严禁将任何收集器内部异常外溢给调用方
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 生成工具链健康度与降级率统计大盘
|
|
76
|
+
*/
|
|
77
|
+
getSummary() {
|
|
78
|
+
const records = this.ringBuffer.getAll();
|
|
79
|
+
const totalCalls = records.length;
|
|
80
|
+
let successCount = 0;
|
|
81
|
+
let failureCount = 0;
|
|
82
|
+
let degradedCount = 0;
|
|
83
|
+
let scopeBlockedCount = 0;
|
|
84
|
+
let confirmBlockedCount = 0;
|
|
85
|
+
const toolStats = {};
|
|
86
|
+
for (const r of records) {
|
|
87
|
+
if (!toolStats[r.toolName]) {
|
|
88
|
+
toolStats[r.toolName] = { total: 0, success: 0, degraded: 0, failed: 0, blocked: 0 };
|
|
89
|
+
}
|
|
90
|
+
const stat = toolStats[r.toolName];
|
|
91
|
+
stat.total++;
|
|
92
|
+
if (r.degraded) {
|
|
93
|
+
degradedCount++;
|
|
94
|
+
stat.degraded++;
|
|
95
|
+
}
|
|
96
|
+
if (r.scopeBlocked) {
|
|
97
|
+
scopeBlockedCount++;
|
|
98
|
+
stat.blocked++;
|
|
99
|
+
}
|
|
100
|
+
if (r.confirmBlocked) {
|
|
101
|
+
confirmBlockedCount++;
|
|
102
|
+
stat.blocked++;
|
|
103
|
+
}
|
|
104
|
+
if (r.success) {
|
|
105
|
+
successCount++;
|
|
106
|
+
stat.success++;
|
|
107
|
+
}
|
|
108
|
+
else if (!r.scopeBlocked && !r.confirmBlocked) {
|
|
109
|
+
failureCount++;
|
|
110
|
+
stat.failed++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const degradationRate = totalCalls > 0 ? Number(((degradedCount / totalCalls) * 100).toFixed(1)) : 0;
|
|
114
|
+
const failureRate = totalCalls > 0 ? (failureCount / totalCalls) * 100 : 0;
|
|
115
|
+
// 健康分计算算法:基准 100 分,按降级率(权重 0.4)与失败率(权重 0.5)综合惩罚
|
|
116
|
+
let healthScore = 100;
|
|
117
|
+
if (totalCalls > 0) {
|
|
118
|
+
const penalty = degradationRate * 0.4 + failureRate * 0.5;
|
|
119
|
+
healthScore = Math.max(0, Math.round(100 - penalty));
|
|
120
|
+
}
|
|
121
|
+
let statusLevel = 'EXCELLENT';
|
|
122
|
+
if (healthScore < 70) {
|
|
123
|
+
statusLevel = 'NEEDS_ATTENTION';
|
|
124
|
+
}
|
|
125
|
+
else if (healthScore < 90) {
|
|
126
|
+
statusLevel = 'GOOD';
|
|
127
|
+
}
|
|
128
|
+
// 智能自愈建议推导
|
|
129
|
+
const recommendations = [];
|
|
130
|
+
if (toolStats['github_repo'] && toolStats['github_repo'].degraded > 0) {
|
|
131
|
+
recommendations.push('发现 github_repo 文件读取触发了 API 降级链路,建议将 GitHub CLI 升级至 2.95.0+ 以启用原生免克隆极速读取。');
|
|
132
|
+
}
|
|
133
|
+
if (scopeBlockedCount > 0) {
|
|
134
|
+
recommendations.push('发现因缺少 OAuth Scope 导致调用拦截,请在终端执行相应的 `gh auth refresh -s <scope>` 补全授权。');
|
|
135
|
+
}
|
|
136
|
+
if (degradationRate > 20) {
|
|
137
|
+
recommendations.push('当前工具链整体 API 降级率较高,可能当前宿主 CLI 版本过低,建议运行 `/gh upgrade` 检查升级。');
|
|
138
|
+
}
|
|
139
|
+
if (recommendations.length === 0) {
|
|
140
|
+
recommendations.push('工具链运行状态极佳,所有调用均走原生高速通道,无降级与权限阻断。');
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
totalCalls,
|
|
144
|
+
successCount,
|
|
145
|
+
failureCount,
|
|
146
|
+
degradedCount,
|
|
147
|
+
degradationRate,
|
|
148
|
+
scopeBlockedCount,
|
|
149
|
+
confirmBlockedCount,
|
|
150
|
+
healthScore,
|
|
151
|
+
statusLevel,
|
|
152
|
+
toolStats,
|
|
153
|
+
recommendations,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
clear() {
|
|
157
|
+
this.ringBuffer.clear();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** 全局单例收集器实例 */
|
|
161
|
+
export const defaultTelemetryCollector = new TelemetryCollector(200);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { GhExecutor } from '../executor.js';
|
|
2
|
+
export declare function createAgentTaskTool(executor: GhExecutor): {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
parameters: {
|
|
6
|
+
type: string;
|
|
7
|
+
properties: {
|
|
8
|
+
action: {
|
|
9
|
+
type: string;
|
|
10
|
+
enum: string[];
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
repo: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
task_id: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
prompt: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
base_branch: {
|
|
26
|
+
type: string;
|
|
27
|
+
description: string;
|
|
28
|
+
};
|
|
29
|
+
custom_agent: {
|
|
30
|
+
type: string;
|
|
31
|
+
description: string;
|
|
32
|
+
};
|
|
33
|
+
limit: {
|
|
34
|
+
type: string;
|
|
35
|
+
description: string;
|
|
36
|
+
};
|
|
37
|
+
log: {
|
|
38
|
+
type: string;
|
|
39
|
+
description: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
required: string[];
|
|
43
|
+
};
|
|
44
|
+
output: {
|
|
45
|
+
schema: {
|
|
46
|
+
type: string;
|
|
47
|
+
};
|
|
48
|
+
render(args: any, value: any): {
|
|
49
|
+
type: string;
|
|
50
|
+
text: string;
|
|
51
|
+
}[];
|
|
52
|
+
};
|
|
53
|
+
execute(args: any, execContext: any): Promise<any>;
|
|
54
|
+
};
|