ygc-dsh-sync 1.2.2 → 1.3.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.md +107 -283
- package/client.js +425 -545
- package/index.js +311 -223
- package/lib/api.js +161 -747
- package/lib/core.js +73 -43
- package/lib/skill-learn.js +1115 -0
- package/lib/skill-optimize.js +888 -0
- package/lib/skill-scope.js +288 -0
- package/lib/skill-share-run.js +388 -0
- package/lib/skillshare.js +781 -0
- package/package.json +14 -3
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本机技能优化:点一下,后台自己把本机技能精炼、合并、优化。
|
|
3
|
+
*
|
|
4
|
+
* 与 skill-learn.js 的分工:
|
|
5
|
+
* skill-learn 管「我和别的机器同名技能不一样」→ 跨机学习
|
|
6
|
+
* skill-optimize 管「我自己这一堆技能」→ 本机体检 + 精炼 + 合并
|
|
7
|
+
* 两者的产出同构(都是待批草稿 + 台账记录),所以落地、批准、驳回共用一套。
|
|
8
|
+
*
|
|
9
|
+
* 只处理**归本插件管**的技能:第三方(技能市场装的)由 skill-scope.js 先剔掉,
|
|
10
|
+
* 这里拿到的 ownSet 已经不含它们;就算调用方传错,下面每个入口还会再查一次。
|
|
11
|
+
*
|
|
12
|
+
* 一轮的固定顺序(便宜的先跑,模型调用最后):
|
|
13
|
+
* 1. 机械体检(不花钱):缺章节、死指针、围栏、破折号、篇幅异常
|
|
14
|
+
* 2. 冗余配对(不花钱):描述词重合度 → 合并精炼候选
|
|
15
|
+
* 3. 建模评估(花钱):按严重度排序,取前 N 条出草稿
|
|
16
|
+
* 4. 落草稿 + 记台账(写自己那一格)
|
|
17
|
+
*
|
|
18
|
+
* 为什么要「后台 + 待批」而不是「点完就改」:
|
|
19
|
+
* 优化是不可逆动作,而且模型会犯错。草稿 + 机械审核 + 原文差异一起摆在
|
|
20
|
+
* 共享盘上,人(或另一个模型)能复核;批准时才替换本机副本,且替换前留备份。
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import crypto from 'node:crypto';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
DRAFTS_DIR,
|
|
28
|
+
LEARNINGS_INDEX,
|
|
29
|
+
LEARNINGS_INDEX_MD,
|
|
30
|
+
mySkillsRoot,
|
|
31
|
+
readSkillMeta,
|
|
32
|
+
} from './skillshare.js';
|
|
33
|
+
import { assertOwnFeaturePath, featuresDir } from './plugins.js';
|
|
34
|
+
import { parseFrontMatter, walkFiles } from './skills.js';
|
|
35
|
+
import {
|
|
36
|
+
extractSkillMarkdown,
|
|
37
|
+
findRedundantPairs,
|
|
38
|
+
landLearningDraft,
|
|
39
|
+
recordLearning,
|
|
40
|
+
writeLearningsMd,
|
|
41
|
+
} from './skill-learn.js';
|
|
42
|
+
|
|
43
|
+
/** 一次优化一轮最多出几份草稿(模型调用要时间,默认 3 条)。 */
|
|
44
|
+
export const DEFAULT_MAX_PER_ROUND = 3;
|
|
45
|
+
/** 一轮里模型调用总上限(含重试)。 */
|
|
46
|
+
const MAX_MODEL_CALLS = 8;
|
|
47
|
+
/** 单份技能送进模型的正文上限。 */
|
|
48
|
+
const MAX_SIDE_CHARS = 26000;
|
|
49
|
+
/** 优化后正文的硬上限行数。 */
|
|
50
|
+
const MAX_OPTIMIZED_LINES = 900;
|
|
51
|
+
|
|
52
|
+
export const OPT = Object.freeze({
|
|
53
|
+
REFINE: 'refine',
|
|
54
|
+
CONDENSE: 'condense',
|
|
55
|
+
MERGE: 'merge',
|
|
56
|
+
ENRICH: 'enrich',
|
|
57
|
+
PITFALLS: 'pitfalls',
|
|
58
|
+
FIXPATH: 'fixpath',
|
|
59
|
+
DEPRECATE: 'deprecate',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const OPT_LABEL = {
|
|
63
|
+
refine: '精炼(结构与可执行层)',
|
|
64
|
+
condense: '精简冗余(压长度不削判据)',
|
|
65
|
+
merge: '合并重复技能',
|
|
66
|
+
enrich: '补全工况与触发条件',
|
|
67
|
+
pitfalls: '补踩坑经验',
|
|
68
|
+
fixpath: '修掉不存在的引用',
|
|
69
|
+
deprecate: '停用(作废)',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/* -------------------------------------------------------------- 小工具 */
|
|
73
|
+
|
|
74
|
+
function readText(file) {
|
|
75
|
+
try {
|
|
76
|
+
return fs.readFileSync(file, 'utf8');
|
|
77
|
+
} catch {
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readJson(file) {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sha256(text) {
|
|
91
|
+
return crypto.createHash('sha256').update(String(text), 'utf8').digest('hex');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** SKILL.md 里被引用的本地相对路径(用于抓「引用了不存在的文件」)。 */
|
|
95
|
+
export function citedPaths(text) {
|
|
96
|
+
const out = new Set();
|
|
97
|
+
const t = String(text || '');
|
|
98
|
+
// 形如 `scripts/foo.mjs`、`references/bar.md`、`./assets/x.png`
|
|
99
|
+
for (const m of t.matchAll(/(?:^|[\s(`'"])((?:\.\/)?(?:scripts|references|assets|evals|templates)\/[A-Za-z0-9._\-/]+)/g)) {
|
|
100
|
+
out.add(m[1].replace(/^\.\//, ''));
|
|
101
|
+
}
|
|
102
|
+
return [...out];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 引用里哪些在本技能目录中真的不存在。 */
|
|
106
|
+
export function missingCitations(dir, text) {
|
|
107
|
+
const missing = [];
|
|
108
|
+
for (const rel of citedPaths(text)) {
|
|
109
|
+
if (!fs.existsSync(path.join(dir, rel))) missing.push(rel);
|
|
110
|
+
}
|
|
111
|
+
return missing;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 单技能机械体检。
|
|
116
|
+
*
|
|
117
|
+
* 每条都写成「问题 + 为什么错 + 该怎么做」,因为模型拿着这份清单去改,
|
|
118
|
+
* 比给它一句「优化一下」有用得多。
|
|
119
|
+
*/
|
|
120
|
+
export function inspectSkill({ name, dir, maxLines = 600, minLines = 40 }) {
|
|
121
|
+
const file = path.join(dir, 'SKILL.md');
|
|
122
|
+
const text = readText(file);
|
|
123
|
+
const fm = parseFrontMatter(file);
|
|
124
|
+
const issues = [];
|
|
125
|
+
const lines = text ? text.split(/\r?\n/).length : 0;
|
|
126
|
+
const fences = (text.match(/^```/gm) || []).length;
|
|
127
|
+
|
|
128
|
+
if (!text.trim()) {
|
|
129
|
+
issues.push({ code: 'empty', severity: 3, why: 'SKILL.md 是空的或读不到', fix: '补正文,或停用这个技能' });
|
|
130
|
+
}
|
|
131
|
+
if (!fm.name) issues.push({ code: 'no-name', severity: 2, why: 'frontmatter 缺 name', fix: '补 name,值与目录名一致' });
|
|
132
|
+
else if (fm.name !== name) {
|
|
133
|
+
issues.push({
|
|
134
|
+
code: 'name-mismatch',
|
|
135
|
+
severity: 2,
|
|
136
|
+
why: `frontmatter name=${fm.name} 与目录名 ${name} 不一致`,
|
|
137
|
+
fix: `把 name 改成 ${name}`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const desc = fm.description || '';
|
|
141
|
+
if (!desc) issues.push({ code: 'no-desc', severity: 3, why: '缺 description(技能永远不会被触发)', fix: '写 60 到 400 字,含用户会说的原话' });
|
|
142
|
+
else if (desc.length < 60) {
|
|
143
|
+
issues.push({
|
|
144
|
+
code: 'short-desc',
|
|
145
|
+
severity: 2,
|
|
146
|
+
why: `description 只有 ${desc.length} 字,触发不准`,
|
|
147
|
+
fix: '扩到 60 到 400 字,补 4 到 6 个用户真实会说的场景',
|
|
148
|
+
});
|
|
149
|
+
} else if (desc.length > 400) {
|
|
150
|
+
issues.push({
|
|
151
|
+
code: 'long-desc',
|
|
152
|
+
severity: 1,
|
|
153
|
+
why: `description ${desc.length} 字,超过 400(常驻上下文挤占)`,
|
|
154
|
+
fix: '压到 400 字以内,保留场景关键词',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const hasPit = /^##+\s*(Pitfalls|常见错误|踩坑)/m.test(text);
|
|
159
|
+
const hasVer = /^##+\s*(Verification|验收|自查|交付前)/m.test(text);
|
|
160
|
+
if (!hasPit) issues.push({ code: 'no-pitfalls', severity: 2, why: '缺踩坑小节(同一坑会反复踩)', fix: '加 `## Pitfalls`,每条写成 症状 / 为什么错 / 正确做法' });
|
|
161
|
+
if (!hasVer) issues.push({ code: 'no-verification', severity: 2, why: '缺验收小节(交出来什么算什么)', fix: '加 `## Verification`,写成可勾选的自查项' });
|
|
162
|
+
|
|
163
|
+
if (fences % 2 !== 0) {
|
|
164
|
+
issues.push({ code: 'fence', severity: 3, why: `代码围栏 ${fences} 个,不成对(正文已损坏)`, fix: '补齐闭合围栏,别把正文吃掉' });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const missing = missingCitations(dir, text);
|
|
168
|
+
if (missing.length) {
|
|
169
|
+
issues.push({
|
|
170
|
+
code: 'dead-citation',
|
|
171
|
+
severity: 3,
|
|
172
|
+
why: `引用了不存在的文件:${missing.slice(0, 4).join('、')}(模型会照着去读,读不到就开始编)`,
|
|
173
|
+
fix: '能删就删这条引用;确实需要就补上文件,或改成「自行命名一个脚本」的说法',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (lines && lines < minLines) {
|
|
178
|
+
issues.push({
|
|
179
|
+
code: 'too-short',
|
|
180
|
+
severity: 2,
|
|
181
|
+
why: `只有 ${lines} 行,缺可执行层(命令 / 数值 / 输出模板)`,
|
|
182
|
+
fix: '补具体命令、阈值与输出模板;确实不需要就考虑与其他技能合并',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (lines > maxLines) {
|
|
186
|
+
issues.push({
|
|
187
|
+
code: 'too-long',
|
|
188
|
+
severity: 2,
|
|
189
|
+
why: `${lines} 行,超过 ${maxLines} 行(该拆 references 或精简)`,
|
|
190
|
+
fix: '把细节挪进 references/,SKILL.md 只留判断与入口',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const dashes = (text.match(/[—–]/g) || []).length;
|
|
195
|
+
if (dashes > 6) {
|
|
196
|
+
issues.push({ code: 'dashes', severity: 1, why: `长破折号 ${dashes} 处(模型文体习惯,读起来累)`, fix: '换成逗号或冒号' });
|
|
197
|
+
}
|
|
198
|
+
const assets = {
|
|
199
|
+
scripts: fs.existsSync(path.join(dir, 'scripts')),
|
|
200
|
+
references: fs.existsSync(path.join(dir, 'references')),
|
|
201
|
+
evals: fs.existsSync(path.join(dir, 'evals')) || fs.existsSync(path.join(dir, 'evals.json')),
|
|
202
|
+
};
|
|
203
|
+
// 有资源目录但正文零指针 = 死文件(D4 的一类硬伤)
|
|
204
|
+
if ((assets.scripts || assets.references) && citedPaths(text).length === 0) {
|
|
205
|
+
issues.push({
|
|
206
|
+
code: 'unreferenced-assets',
|
|
207
|
+
severity: 2,
|
|
208
|
+
why: '目录里有 scripts/references,但正文一条加载指针都没有(等于死文件)',
|
|
209
|
+
fix: '在正文里写清什么时候读哪个文件(相对路径)',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const severity = issues.reduce((n, x) => Math.max(n, x.severity), 0);
|
|
214
|
+
return {
|
|
215
|
+
name,
|
|
216
|
+
dir,
|
|
217
|
+
lines,
|
|
218
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
219
|
+
files: walkFiles(dir).length,
|
|
220
|
+
descLength: desc.length,
|
|
221
|
+
assets,
|
|
222
|
+
hasPitfalls: hasPit,
|
|
223
|
+
hasVerification: hasVer,
|
|
224
|
+
missingCitations: missing,
|
|
225
|
+
issues,
|
|
226
|
+
severity,
|
|
227
|
+
score: issues.reduce((n, x) => n + x.severity, 0),
|
|
228
|
+
text,
|
|
229
|
+
mdHash: sha256(text),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* ------------------------------------------------------------ 提示词 */
|
|
234
|
+
|
|
235
|
+
export const OPT_SYSTEM = [
|
|
236
|
+
'你是技能(SKILL.md)的维护者。任务是把一份本机自研技能改得更好,不是重写它。',
|
|
237
|
+
'',
|
|
238
|
+
'输出必须分成两块,顺序固定:',
|
|
239
|
+
'第一块:一个 ```json 围栏,只含:',
|
|
240
|
+
'{',
|
|
241
|
+
' "action": "refine|condense|merge|enrich|pitfalls|fixpath|deprecate",',
|
|
242
|
+
' "confidence": 0.0 到 1.0,',
|
|
243
|
+
' "reason": "不超过 200 字:改了哪几处、为什么这样改",',
|
|
244
|
+
' "changes": ["具体改动一", "..."],',
|
|
245
|
+
' "dropped": ["删掉的内容及理由", "..."]',
|
|
246
|
+
'}',
|
|
247
|
+
'第二块:action 不是 deprecate 时,再给一个 ```markdown 围栏,里面是改完的 SKILL.md 全文;',
|
|
248
|
+
'action 是 deprecate 时不输出第二块。',
|
|
249
|
+
'',
|
|
250
|
+
'硬规则(不满足会被机械审核拒掉):',
|
|
251
|
+
'1. 保留 frontmatter,`name:` 与给定技能名逐字相同;description 60 到 400 字。',
|
|
252
|
+
'2. 必须有 `## Pitfalls`(或 `## 常见错误`)与 `## Verification`(或 `## 验收`)两节。',
|
|
253
|
+
'3. **不许删掉原有的可执行层**:命令、参数、阈值、输出模板、判定表。要精简就精简解释性文字。',
|
|
254
|
+
'4. 不许新增对不存在的文件的引用;原本指向不存在文件的引用要删掉或改成「自行命名」。',
|
|
255
|
+
'5. 不许出现长破折号「—」「–」。代码围栏成对闭合。',
|
|
256
|
+
'6. 不许比原文长出 40% 以上;允许比原文短,但删的必须是冗余而不是判据。',
|
|
257
|
+
'7. 每一条原有 Pitfalls 都要留下(可以合并措辞,但坑本身不许消失)。',
|
|
258
|
+
'',
|
|
259
|
+
'动作怎么选:',
|
|
260
|
+
'- 结构乱、缺章节、缺验收 → refine',
|
|
261
|
+
'- 解释性文字太多、判据被埋住 → condense',
|
|
262
|
+
'- 缺「什么情况下用 / 不用」→ enrich',
|
|
263
|
+
'- 只有正面规则没有踩坑 → pitfalls',
|
|
264
|
+
'- 引用了不存在的文件 → fixpath',
|
|
265
|
+
'- 两个技能该合成一个 → merge(会额外给另一份正文)',
|
|
266
|
+
'- 整条技能是错的、改也没意义 → deprecate',
|
|
267
|
+
].join('\n');
|
|
268
|
+
|
|
269
|
+
/** 组一条单技能优化提示词。 */
|
|
270
|
+
export function buildOptimizePrompt({ name, text, profile, issues, action, peer = null, redundant = [] }) {
|
|
271
|
+
const tr = (s, cap = MAX_SIDE_CHARS) => {
|
|
272
|
+
const t = String(s || '');
|
|
273
|
+
return t.length <= cap ? t : `${t.slice(0, cap)}\n\n[以上被截断:原文 ${t.length} 字,只给了前 ${cap} 字]`;
|
|
274
|
+
};
|
|
275
|
+
const L = [];
|
|
276
|
+
L.push(`技能名(frontmatter 的 name 必须逐字写成这个):${name}`);
|
|
277
|
+
L.push(`建议动作:${OPT_LABEL[action] || action}(可以改成更合适的动作,但要在 reason 里说清)`);
|
|
278
|
+
L.push('');
|
|
279
|
+
L.push('## 机械体检结果(真实现算,不要推翻,逐条处理)');
|
|
280
|
+
L.push(`行 ${profile.lines} · 字 ${profile.bytes} 字节 · 资产 scripts=${profile.assets.scripts} references=${profile.assets.references} evals=${profile.assets.evals}`);
|
|
281
|
+
if (!issues.length) L.push('- (没有机械问题,按动作自由优化)');
|
|
282
|
+
for (const it of issues) {
|
|
283
|
+
L.push(`- [${it.severity}] ${it.code}:${it.why}`);
|
|
284
|
+
L.push(` 处理方式:${it.fix}`);
|
|
285
|
+
}
|
|
286
|
+
if (peer) {
|
|
287
|
+
L.push('');
|
|
288
|
+
L.push(`## 待合并的另一份技能:${peer.name}`);
|
|
289
|
+
L.push(`行 ${peer.lines} · 说明:${String(peer.desc || '').slice(0, 200)}`);
|
|
290
|
+
L.push('合并要求:两份的可执行层都要保住;合成后 name 用上面给定的技能名;');
|
|
291
|
+
L.push('另一份里独有的坑与工况必须留在合并稿里,合并后在 JSON 的 dropped 里列出被移走的内容。');
|
|
292
|
+
L.push('```markdown');
|
|
293
|
+
L.push(tr(peer.text));
|
|
294
|
+
L.push('```');
|
|
295
|
+
}
|
|
296
|
+
if (Array.isArray(redundant) && redundant.length) {
|
|
297
|
+
L.push('');
|
|
298
|
+
L.push('## 与本机其他技能的重合(供判断是否该 merge)');
|
|
299
|
+
for (const r of redundant.slice(0, 4)) L.push(`- \`${r.a}\` 与 \`${r.b}\` 重合理度 ${r.similarity}:${r.reason}`);
|
|
300
|
+
}
|
|
301
|
+
L.push('');
|
|
302
|
+
L.push('## 当前 SKILL.md 全文');
|
|
303
|
+
L.push('```markdown');
|
|
304
|
+
L.push(tr(text));
|
|
305
|
+
L.push('```');
|
|
306
|
+
L.push('');
|
|
307
|
+
L.push('现在给出改完的版本。先 json 围栏,需要正文时再跟一个 markdown 围栏。');
|
|
308
|
+
return L.join('\n');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* ------------------------------------------------------------ 解析与审核 */
|
|
312
|
+
|
|
313
|
+
/** 从模型回复里取结论与正文(与 skill-learn 的解析同构,但动作集不同)。 */
|
|
314
|
+
export function parseOptimizeReply(text) {
|
|
315
|
+
const raw = String(text || '');
|
|
316
|
+
const out = { action: '', confidence: 0, reason: '', changes: [], dropped: [], content: '' };
|
|
317
|
+
const ACTIONS = new Set(Object.values(OPT));
|
|
318
|
+
const blocks = [...raw.matchAll(/```(?:json|JSON)\s*\r?\n([\s\S]*?)```/g)].map((m) => m[1].trim());
|
|
319
|
+
const whole = raw.trim();
|
|
320
|
+
if (!blocks.length && whole.startsWith('{')) blocks.push(whole);
|
|
321
|
+
for (const b of blocks) {
|
|
322
|
+
try {
|
|
323
|
+
const v = JSON.parse(b);
|
|
324
|
+
if (!v || typeof v !== 'object') continue;
|
|
325
|
+
const a = typeof v.action === 'string' ? v.action.trim() : '';
|
|
326
|
+
if (ACTIONS.has(a)) out.action = a;
|
|
327
|
+
const c = Number(v.confidence);
|
|
328
|
+
if (Number.isFinite(c)) out.confidence = Math.max(0, Math.min(1, c));
|
|
329
|
+
if (typeof v.reason === 'string') out.reason = v.reason.trim().slice(0, 1200);
|
|
330
|
+
if (Array.isArray(v.changes)) out.changes = v.changes.filter((x) => typeof x === 'string').slice(0, 20);
|
|
331
|
+
if (Array.isArray(v.dropped)) out.dropped = v.dropped.filter((x) => typeof x === 'string').slice(0, 20);
|
|
332
|
+
break;
|
|
333
|
+
} catch {
|
|
334
|
+
/* 坏 JSON 继续 */
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// 正文切分走共用实现:SKILL.md 内部自带代码块,非贪婪正则会把它截断
|
|
338
|
+
out.content = extractSkillMarkdown(raw);
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 审核优化稿。
|
|
344
|
+
*
|
|
345
|
+
* 与本机原有的 mnemon 审核口径一致的地方:frontmatter / 两节必备 / 围栏 / 破折号 / 章节不许丢。
|
|
346
|
+
* 刻意放宽的地方:**允许变短**(精炼是合法动作),但可执行层的命令行数量不许
|
|
347
|
+
* 掉到原文的 70% 以下——这是「精简」和「削判据」的分界线。
|
|
348
|
+
*/
|
|
349
|
+
export function auditOptimized({ text, name, originalText, action }) {
|
|
350
|
+
const problems = [];
|
|
351
|
+
const info = {};
|
|
352
|
+
if (!text || !text.trim()) return { ok: false, problems: ['优化稿为空'], info };
|
|
353
|
+
let body = text;
|
|
354
|
+
if (body.charCodeAt(0) === 0xfeff) body = body.slice(1);
|
|
355
|
+
const fm = body.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
356
|
+
if (!fm) problems.push('没有 frontmatter');
|
|
357
|
+
else {
|
|
358
|
+
const kv = {};
|
|
359
|
+
for (const l of fm[1].split(/\r?\n/)) {
|
|
360
|
+
const m = l.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
|
|
361
|
+
if (!m) continue;
|
|
362
|
+
let v = m[2].trim();
|
|
363
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
364
|
+
kv[m[1]] = v;
|
|
365
|
+
}
|
|
366
|
+
if (kv.name !== name) problems.push(`frontmatter name=${kv.name || '(空)'} 与技能名 ${name} 不一致`);
|
|
367
|
+
const desc = kv.description || '';
|
|
368
|
+
info.descLength = desc.length;
|
|
369
|
+
if (!desc) problems.push('缺 description');
|
|
370
|
+
else if (desc.length < 60) problems.push(`description 只有 ${desc.length} 字`);
|
|
371
|
+
else if (desc.length > 400) problems.push(`description ${desc.length} 字,超过 400`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
info.lines = text.split(/\r?\n/).length;
|
|
375
|
+
if (info.lines > MAX_OPTIMIZED_LINES) problems.push(`${info.lines} 行,超过上限 ${MAX_OPTIMIZED_LINES}`);
|
|
376
|
+
|
|
377
|
+
const srcLines = (originalText || '').split(/\r?\n/).length;
|
|
378
|
+
if (srcLines && info.lines > Math.ceil(srcLines * 1.4) + 40) {
|
|
379
|
+
problems.push(`比原文长出太多(${srcLines} → ${info.lines} 行),优化不该膨胀`);
|
|
380
|
+
}
|
|
381
|
+
if (!/^##+\s*(Pitfalls|常见错误|踩坑)/m.test(text)) problems.push('缺 Pitfalls / 常见错误 一节');
|
|
382
|
+
if (!/^##+\s*(Verification|验收|自查)/m.test(text)) problems.push('缺 Verification / 验收 一节');
|
|
383
|
+
|
|
384
|
+
const fences = (text.match(/^```/gm) || []).length;
|
|
385
|
+
if (fences % 2 !== 0) problems.push(`代码围栏 ${fences} 个,不成对`);
|
|
386
|
+
|
|
387
|
+
const dashes = (text.match(/[—–]/g) || []).length;
|
|
388
|
+
const srcDashes = ((originalText || '').match(/[—–]/g) || []).length;
|
|
389
|
+
if (dashes > Math.max(srcDashes, 6)) problems.push(`新增长破折号(${srcDashes} → ${dashes} 处)`);
|
|
390
|
+
|
|
391
|
+
// 可执行层不许被削
|
|
392
|
+
const countCmd = (t) => (String(t || '').match(/^\s*(\$\s|\w+\s+--|node |python |pnpm |npm |git |powershell |curl )/gm) || []).length;
|
|
393
|
+
const countNum = (t) => (String(t || '').match(/\b\d+(\.\d+)?\s*(ms|s|秒|分钟|行|字|%|MB|GB|pt|px)\b/g) || []).length;
|
|
394
|
+
info.cmdBefore = countCmd(originalText);
|
|
395
|
+
info.cmdAfter = countCmd(text);
|
|
396
|
+
info.numBefore = countNum(originalText);
|
|
397
|
+
info.numAfter = countNum(text);
|
|
398
|
+
if (info.cmdBefore >= 4 && info.cmdAfter < Math.floor(info.cmdBefore * 0.7)) {
|
|
399
|
+
problems.push(`命令行从 ${info.cmdBefore} 掉到 ${info.cmdAfter}(低于 70%),判据被削`);
|
|
400
|
+
}
|
|
401
|
+
if (info.numBefore >= 6 && info.numAfter < Math.floor(info.numBefore * 0.7)) {
|
|
402
|
+
problems.push(`具体数值从 ${info.numBefore} 掉到 ${info.numAfter}(低于 70%);收紧口子不行,现在是放宽`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 原有章节标题不许丢(允许改写同义,但标题集合不能少太多)
|
|
406
|
+
const heads = (t) => new Set((String(t || '').match(/^#{2,3}\s+(.+)$/gm) || []).map((x) => x.replace(/^#+\s*/, '').trim()));
|
|
407
|
+
const before = heads(originalText);
|
|
408
|
+
const after = heads(text);
|
|
409
|
+
const lost = [...before].filter((h) => !after.has(h));
|
|
410
|
+
info.lostHeadings = lost.slice(0, 6);
|
|
411
|
+
if (lost.length > Math.max(2, Math.floor(before.size * 0.4))) {
|
|
412
|
+
problems.push(`丢了 ${lost.length} 个原有章节(原有 ${before.size} 个):${lost.slice(0, 4).join(' / ')}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 原有 Pitfalls 条目:合并措辞可以,坑本身不许消失
|
|
416
|
+
const pitItems = (t) => {
|
|
417
|
+
const out = [];
|
|
418
|
+
let inPit = false;
|
|
419
|
+
for (const l of String(t || '').split(/\r?\n/)) {
|
|
420
|
+
const h = l.match(/^##+\s+(.+)$/);
|
|
421
|
+
if (h) {
|
|
422
|
+
inPit = /^(Pitfalls|常见错误|踩坑)/i.test(h[1].trim());
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (!inPit) continue;
|
|
426
|
+
const item = l.match(/^\s*(?:[-*+]|\d+\.)\s+(.{6,})$/);
|
|
427
|
+
if (item) out.push(item[1].trim().toLowerCase().replace(/\s+/g, ' ').slice(0, 50));
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
};
|
|
431
|
+
const srcPit = pitItems(originalText);
|
|
432
|
+
const nowPit = pitItems(text);
|
|
433
|
+
const lostPit = srcPit.filter((k) => !nowPit.some((x) => x.includes(k.slice(0, 16))));
|
|
434
|
+
info.lostPitfalls = lostPit.slice(0, 6);
|
|
435
|
+
if (srcPit.length >= 3 && lostPit.length > Math.max(1, Math.floor(srcPit.length * 0.3))) {
|
|
436
|
+
problems.push(`丢了 ${lostPit.length} / ${srcPit.length} 条原有踩坑条目`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (action && !Object.values(OPT).includes(action)) problems.push(`未知动作 ${action}`);
|
|
440
|
+
return { ok: problems.length === 0, problems, info };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/* ------------------------------------------------------------ 主流程 */
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* 跑一轮本机优化。
|
|
447
|
+
*
|
|
448
|
+
* @param {object} input
|
|
449
|
+
* @param {object} input.config { libraryRoot }
|
|
450
|
+
* @param {string} input.machineId
|
|
451
|
+
* @param {string} input.machineName
|
|
452
|
+
* @param {string} input.localRoot 本机技能根
|
|
453
|
+
* @param {Set<string>} input.ownSet 归本插件管的技能名(skill-scope 给的)
|
|
454
|
+
* @param {Function|null} input.generate 宿主模型
|
|
455
|
+
* @param {number} [input.maxPerRound=3]
|
|
456
|
+
* @param {boolean} [input.dryRun=false]
|
|
457
|
+
* @param {boolean} [input.includeMerge=true] 是否把冗余合并算进来
|
|
458
|
+
* @param {Function} [input.log]
|
|
459
|
+
*/
|
|
460
|
+
export async function runOptimizeRound({
|
|
461
|
+
config,
|
|
462
|
+
machineId,
|
|
463
|
+
machineName = '',
|
|
464
|
+
localRoot,
|
|
465
|
+
ownSet,
|
|
466
|
+
generate = null,
|
|
467
|
+
maxPerRound = DEFAULT_MAX_PER_ROUND,
|
|
468
|
+
dryRun = false,
|
|
469
|
+
includeMerge = true,
|
|
470
|
+
log,
|
|
471
|
+
}) {
|
|
472
|
+
const started = Date.now();
|
|
473
|
+
const feature = String(machineId || '').trim();
|
|
474
|
+
if (!feature) return { ok: false, error: '本机标识还没生成' };
|
|
475
|
+
|
|
476
|
+
const report = {
|
|
477
|
+
ok: true,
|
|
478
|
+
feature,
|
|
479
|
+
ms: 0,
|
|
480
|
+
scanned: 0,
|
|
481
|
+
skippedThirdParty: 0,
|
|
482
|
+
healthy: 0,
|
|
483
|
+
queued: [],
|
|
484
|
+
queueTotal: 0,
|
|
485
|
+
drafted: [],
|
|
486
|
+
rejected: [],
|
|
487
|
+
onlyRecorded: [],
|
|
488
|
+
merged: [],
|
|
489
|
+
errors: [],
|
|
490
|
+
dryRun,
|
|
491
|
+
modelCalls: 0,
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// ── 1. 体检(只查 ownSet 里的)
|
|
495
|
+
const dirs = [];
|
|
496
|
+
let entries = [];
|
|
497
|
+
try {
|
|
498
|
+
entries = fs.readdirSync(localRoot, { withFileTypes: true });
|
|
499
|
+
} catch {
|
|
500
|
+
return { ok: false, error: `读不到技能根:${localRoot}` };
|
|
501
|
+
}
|
|
502
|
+
for (const e of entries) {
|
|
503
|
+
if (!e.isDirectory() || e.name.startsWith('.') || e.name.startsWith('_')) continue;
|
|
504
|
+
const dir = path.join(localRoot, e.name);
|
|
505
|
+
if (!fs.existsSync(path.join(dir, 'SKILL.md'))) continue;
|
|
506
|
+
if (ownSet && !ownSet.has(e.name)) {
|
|
507
|
+
report.skippedThirdParty += 1;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
dirs.push({ name: e.name, dir });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const profiles = [];
|
|
514
|
+
for (const { name, dir } of dirs) {
|
|
515
|
+
try {
|
|
516
|
+
const p = inspectSkill({ name, dir });
|
|
517
|
+
profiles.push(p);
|
|
518
|
+
report.scanned += 1;
|
|
519
|
+
} catch (err) {
|
|
520
|
+
report.errors.push(`${name}:体检失败 ${String((err && err.message) || err)}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const byName = new Map(profiles.map((p) => [p.name, p]));
|
|
525
|
+
for (const p of profiles) if (!p.issues.length) report.healthy += 1;
|
|
526
|
+
|
|
527
|
+
// ── 2. 冗余配对(只保留两份都归自己管的)
|
|
528
|
+
let pairs = [];
|
|
529
|
+
if (includeMerge) {
|
|
530
|
+
try {
|
|
531
|
+
const asMap = new Map();
|
|
532
|
+
for (const p of profiles) {
|
|
533
|
+
asMap.set(p.name, {
|
|
534
|
+
name: p.name,
|
|
535
|
+
dirName: p.name,
|
|
536
|
+
description: (parseFrontMatter(path.join(p.dir, 'SKILL.md')).description || ''),
|
|
537
|
+
text: p.text,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
pairs = findRedundantPairs(asMap).filter((x) => ownSet.has(x.a) && ownSet.has(x.b));
|
|
541
|
+
} catch (err) {
|
|
542
|
+
report.errors.push(`冗余配对失败:${String((err && err.message) || err)}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── 3. 排优先级:严重度高的先处理
|
|
547
|
+
const cand = [];
|
|
548
|
+
for (const p of profiles) {
|
|
549
|
+
if (!p.issues.length) continue;
|
|
550
|
+
const action = decideAction(p);
|
|
551
|
+
cand.push({ name: p.name, profile: p, action, weight: p.score });
|
|
552
|
+
}
|
|
553
|
+
// 合并候选排在前面(一次处理两条技能,性价比最高)
|
|
554
|
+
for (const pair of pairs.slice(0, 4)) {
|
|
555
|
+
const a = byName.get(pair.a);
|
|
556
|
+
if (!a) continue;
|
|
557
|
+
cand.push({
|
|
558
|
+
name: pair.a,
|
|
559
|
+
profile: a,
|
|
560
|
+
action: OPT.MERGE,
|
|
561
|
+
weight: a.score + 6 + Math.round(pair.similarity * 10),
|
|
562
|
+
peerName: pair.b,
|
|
563
|
+
peerProfile: byName.get(pair.b) || null,
|
|
564
|
+
pair,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
cand.sort((x, y) => y.weight - x.weight);
|
|
568
|
+
|
|
569
|
+
// 同名去重:同一个技能只排一次,保留权重最高的那条
|
|
570
|
+
const seen = new Set();
|
|
571
|
+
const queue = [];
|
|
572
|
+
for (const c of cand) {
|
|
573
|
+
if (seen.has(c.name)) continue;
|
|
574
|
+
seen.add(c.name);
|
|
575
|
+
queue.push(c);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const limit = Math.max(0, Math.min(Number(maxPerRound) || 0, 12));
|
|
579
|
+
const picked = queue.slice(0, limit);
|
|
580
|
+
// queued 只记「本轮真的会处理」的那些:把整条队列塞进去会让面板显示
|
|
581
|
+
// 「排队 N 个」而实际只做 1 个,看起来像报错了。
|
|
582
|
+
report.queued = picked.map((c) => ({ name: c.name, action: c.action, label: OPT_LABEL[c.action], weight: c.weight }));
|
|
583
|
+
report.queueTotal = queue.length;
|
|
584
|
+
|
|
585
|
+
// ── 4. 出草稿
|
|
586
|
+
if (typeof generate !== 'function') {
|
|
587
|
+
report.skipped = '宿主没有可用默认模型:只做体检与排队,不出草稿';
|
|
588
|
+
} else {
|
|
589
|
+
for (const item of picked) {
|
|
590
|
+
if (report.modelCalls >= MAX_MODEL_CALLS) {
|
|
591
|
+
report.errors.push(`模型调用已达上限 ${MAX_MODEL_CALLS},剩下的下一轮再处理`);
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
const rec = await optimizeOne({
|
|
595
|
+
item,
|
|
596
|
+
config,
|
|
597
|
+
machineId: feature,
|
|
598
|
+
machineName,
|
|
599
|
+
localRoot,
|
|
600
|
+
generate,
|
|
601
|
+
dryRun,
|
|
602
|
+
redundant: pairs.filter((p) => p.a === item.name || p.b === item.name),
|
|
603
|
+
onModelCall: () => {
|
|
604
|
+
report.modelCalls += 1;
|
|
605
|
+
},
|
|
606
|
+
log,
|
|
607
|
+
});
|
|
608
|
+
// 字段名是 draft(草稿路径),不是 drafted。写错了会把「成功出稿」记成「审核未过」。
|
|
609
|
+
if (rec.draft) report.drafted.push({ name: rec.skill, action: rec.action, label: OPT_LABEL[rec.action] });
|
|
610
|
+
else if (rec.action === 'manual' || rec.action === OPT.DEPRECATE) report.onlyRecorded.push({ name: rec.skill, action: rec.action, note: '仅记录' });
|
|
611
|
+
else report.rejected.push({ name: rec.skill, reason: rec.reason });
|
|
612
|
+
if (rec.action === OPT.MERGE && rec.peerName) report.merged.push({ into: rec.skill, from: rec.peerName });
|
|
613
|
+
if (rec.error) report.errors.push(`${rec.skill}:${rec.error}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ── 5. 健康态也记一条(不调模型),让面板知道「体检跑过了」
|
|
618
|
+
if (!dryRun) {
|
|
619
|
+
try {
|
|
620
|
+
const doc = readJson(path.join(mySkillsRoot(config, feature), LEARNINGS_INDEX)) || { items: [] };
|
|
621
|
+
const items = Array.isArray(doc.items) ? doc.items : [];
|
|
622
|
+
const inspectAt = new Date().toISOString();
|
|
623
|
+
const summary = {
|
|
624
|
+
schemaVersion: 1,
|
|
625
|
+
feature,
|
|
626
|
+
machine: machineName,
|
|
627
|
+
generatedAt: inspectAt,
|
|
628
|
+
kind: 'inspect',
|
|
629
|
+
counts: {
|
|
630
|
+
scanned: report.scanned,
|
|
631
|
+
healthy: report.healthy,
|
|
632
|
+
queued: report.queued.length,
|
|
633
|
+
drafted: report.drafted.length,
|
|
634
|
+
thirdPartySkipped: report.skippedThirdParty,
|
|
635
|
+
},
|
|
636
|
+
// 只留最严重的 30 条,避免台账变成另一份全量体检报告
|
|
637
|
+
worst: profiles
|
|
638
|
+
.filter((p) => p.issues.length)
|
|
639
|
+
.sort((a, b) => b.score - a.score)
|
|
640
|
+
.slice(0, 30)
|
|
641
|
+
.map((p) => ({
|
|
642
|
+
skill: p.name,
|
|
643
|
+
score: p.score,
|
|
644
|
+
lines: p.lines,
|
|
645
|
+
codes: [...new Set(p.issues.map((x) => x.code))],
|
|
646
|
+
})),
|
|
647
|
+
at: inspectAt,
|
|
648
|
+
};
|
|
649
|
+
const rest = items.filter((x) => x.kind !== 'inspect');
|
|
650
|
+
writeLedger(config, feature, { ...doc, items: [...rest, summary] });
|
|
651
|
+
writeLearningsMd({ config, machineId: feature, log });
|
|
652
|
+
} catch (err) {
|
|
653
|
+
report.errors.push(`写体检摘要失败:${String((err && err.message) || err)}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
report.ms = Date.now() - started;
|
|
658
|
+
log?.(
|
|
659
|
+
`本机技能优化:体检 ${report.scanned} 个(健康 ${report.healthy},第三方跳过 ${report.skippedThirdParty})、` +
|
|
660
|
+
`排队 ${report.queued.length}、出草稿 ${report.drafted.length}` +
|
|
661
|
+
(report.rejected.length ? `、审核未过 ${report.rejected.length}` : '') +
|
|
662
|
+
(report.errors.length ? `、错误 ${report.errors.length}` : ''),
|
|
663
|
+
);
|
|
664
|
+
return report;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** 按体检问题选一个默认动作。 */
|
|
668
|
+
export function decideAction(profile) {
|
|
669
|
+
const codes = new Set(profile.issues.map((x) => x.code));
|
|
670
|
+
if (codes.has('empty')) return OPT.DEPRECATE;
|
|
671
|
+
if (codes.has('dead-citation')) return OPT.FIXPATH;
|
|
672
|
+
if (codes.has('fence')) return OPT.REFINE;
|
|
673
|
+
if (codes.has('too-long')) return OPT.CONDENSE;
|
|
674
|
+
if (codes.has('no-pitfalls')) return OPT.PITFALLS;
|
|
675
|
+
if (codes.has('no-verification')) return OPT.REFINE;
|
|
676
|
+
if (codes.has('too-short')) return OPT.ENRICH;
|
|
677
|
+
if (codes.has('unreferenced-assets')) return OPT.REFINE;
|
|
678
|
+
if (/desc/.test([...codes].join(' '))) return OPT.REFINE;
|
|
679
|
+
return OPT.REFINE;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/** 处理一条:调模型 → 解析 → 审核 → 落草稿 → 记台账。 */
|
|
683
|
+
async function optimizeOne({
|
|
684
|
+
item,
|
|
685
|
+
config,
|
|
686
|
+
machineId,
|
|
687
|
+
machineName,
|
|
688
|
+
localRoot,
|
|
689
|
+
generate,
|
|
690
|
+
dryRun,
|
|
691
|
+
redundant,
|
|
692
|
+
onModelCall,
|
|
693
|
+
log,
|
|
694
|
+
}) {
|
|
695
|
+
const { name, profile, action, peerName, peerProfile } = item;
|
|
696
|
+
const rec = {
|
|
697
|
+
skill: name,
|
|
698
|
+
at: new Date().toISOString(),
|
|
699
|
+
mineHash: profile.mdHash,
|
|
700
|
+
peerHash: '',
|
|
701
|
+
peerFeature: '',
|
|
702
|
+
peerMachine: '',
|
|
703
|
+
action,
|
|
704
|
+
label: OPT_LABEL[action],
|
|
705
|
+
basis: 'model',
|
|
706
|
+
confidence: 0,
|
|
707
|
+
reason: '',
|
|
708
|
+
evidence: profile.issues.map((x) => `[${x.code}] ${x.why}`).slice(0, 8),
|
|
709
|
+
dropped: [],
|
|
710
|
+
draft: null,
|
|
711
|
+
audit: null,
|
|
712
|
+
modelCalls: 0,
|
|
713
|
+
error: '',
|
|
714
|
+
kind: 'optimize',
|
|
715
|
+
inspect: { score: profile.score, lines: profile.lines, codes: [...new Set(profile.issues.map((x) => x.code))] },
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
const prompt = buildOptimizePrompt({
|
|
719
|
+
name,
|
|
720
|
+
text: profile.text,
|
|
721
|
+
profile,
|
|
722
|
+
issues: profile.issues,
|
|
723
|
+
action,
|
|
724
|
+
peer: peerProfile
|
|
725
|
+
? { name: peerName, text: peerProfile.text, lines: peerProfile.lines, desc: parseFrontMatter(path.join(peerProfile.dir, 'SKILL.md')).description || '' }
|
|
726
|
+
: null,
|
|
727
|
+
redundant,
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
let parsed = null;
|
|
731
|
+
let lastErr = '';
|
|
732
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
733
|
+
try {
|
|
734
|
+
onModelCall();
|
|
735
|
+
rec.modelCalls += 1;
|
|
736
|
+
const reply = await generate(prompt);
|
|
737
|
+
const p = parseOptimizeReply(reply);
|
|
738
|
+
if (!p.action) {
|
|
739
|
+
lastErr = '回复里没有可解析的 action';
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
parsed = p;
|
|
743
|
+
break;
|
|
744
|
+
} catch (err) {
|
|
745
|
+
lastErr = String((err && err.message) || err);
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (!parsed) {
|
|
750
|
+
rec.action = 'manual';
|
|
751
|
+
rec.label = '等人工处理';
|
|
752
|
+
rec.basis = 'manual';
|
|
753
|
+
rec.reason = `模型优化没跑通:${lastErr}`;
|
|
754
|
+
if (!dryRun) safeRecord(config, machineId, rec, machineName);
|
|
755
|
+
return rec;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
rec.action = parsed.action;
|
|
759
|
+
rec.label = OPT_LABEL[parsed.action] || parsed.action;
|
|
760
|
+
rec.confidence = parsed.confidence;
|
|
761
|
+
rec.reason = parsed.reason;
|
|
762
|
+
rec.dropped = parsed.dropped;
|
|
763
|
+
rec.changes = parsed.changes;
|
|
764
|
+
if (peerName) rec.mergedFrom = peerName;
|
|
765
|
+
|
|
766
|
+
if (parsed.action === OPT.DEPRECATE) {
|
|
767
|
+
if (parsed.confidence < 0.6) {
|
|
768
|
+
rec.action = 'manual';
|
|
769
|
+
rec.label = '等人工处理';
|
|
770
|
+
rec.reason = `模型建议停用但置信度只有 ${parsed.confidence}:${parsed.reason}`;
|
|
771
|
+
}
|
|
772
|
+
if (!dryRun) safeRecord(config, machineId, rec, machineName);
|
|
773
|
+
return rec;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (!parsed.content) {
|
|
777
|
+
rec.action = 'manual';
|
|
778
|
+
rec.label = '等人工处理';
|
|
779
|
+
rec.reason = `模型选了 ${parsed.action} 但没给正文`;
|
|
780
|
+
if (!dryRun) safeRecord(config, machineId, rec, machineName);
|
|
781
|
+
return rec;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const audit = auditOptimized({
|
|
785
|
+
text: parsed.content,
|
|
786
|
+
name,
|
|
787
|
+
originalText: profile.text,
|
|
788
|
+
action: parsed.action,
|
|
789
|
+
});
|
|
790
|
+
rec.audit = { ok: audit.ok, problems: audit.problems, info: audit.info };
|
|
791
|
+
if (!audit.ok) {
|
|
792
|
+
rec.action = 'manual';
|
|
793
|
+
rec.label = '等人工处理';
|
|
794
|
+
rec.reason = `优化稿没过机械审核(${audit.problems.slice(0, 3).join(';')})`;
|
|
795
|
+
if (!dryRun) safeRecord(config, machineId, rec, machineName);
|
|
796
|
+
return rec;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (!dryRun) {
|
|
800
|
+
try {
|
|
801
|
+
const d = landLearningDraft({
|
|
802
|
+
config,
|
|
803
|
+
machineId,
|
|
804
|
+
name,
|
|
805
|
+
content: parsed.content,
|
|
806
|
+
meta: {
|
|
807
|
+
action: parsed.action,
|
|
808
|
+
kind: 'optimize',
|
|
809
|
+
base: 'mine',
|
|
810
|
+
confidence: parsed.confidence,
|
|
811
|
+
reason: parsed.reason,
|
|
812
|
+
changes: parsed.changes,
|
|
813
|
+
dropped: parsed.dropped,
|
|
814
|
+
audit: audit.info,
|
|
815
|
+
mergedFrom: peerName || '',
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
rec.draft = d.file;
|
|
819
|
+
log?.(`优化草稿已出:${name}(${OPT_LABEL[parsed.action] || parsed.action})`);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
rec.error = String((err && err.message) || err);
|
|
822
|
+
}
|
|
823
|
+
safeRecord(config, machineId, rec, machineName);
|
|
824
|
+
}
|
|
825
|
+
return rec;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function safeRecord(config, machineId, rec, machineName) {
|
|
829
|
+
try {
|
|
830
|
+
recordLearning({ config, machineId, record: rec, machineName });
|
|
831
|
+
} catch {
|
|
832
|
+
/* 台账写失败不影响草稿本身 */
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function writeLedger(config, machineId, doc) {
|
|
837
|
+
const file = path.join(mySkillsRoot(config, machineId), LEARNINGS_INDEX);
|
|
838
|
+
assertOwnFeaturePath(file, { featuresRoot: featuresDir(config), machineId });
|
|
839
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
840
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
841
|
+
fs.writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
|
|
842
|
+
fs.renameSync(tmp, file);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** 整库体检摘要(不调模型,面板「体检」按钮用)。 */
|
|
846
|
+
export function inspectAll({ localRoot, ownSet }) {
|
|
847
|
+
const out = { scanned: 0, healthy: 0, thirdParty: 0, items: [], codes: {} };
|
|
848
|
+
let entries = [];
|
|
849
|
+
try {
|
|
850
|
+
entries = fs.readdirSync(localRoot, { withFileTypes: true });
|
|
851
|
+
} catch {
|
|
852
|
+
return out;
|
|
853
|
+
}
|
|
854
|
+
for (const e of entries) {
|
|
855
|
+
if (!e.isDirectory() || e.name.startsWith('.') || e.name.startsWith('_')) continue;
|
|
856
|
+
const dir = path.join(localRoot, e.name);
|
|
857
|
+
if (!fs.existsSync(path.join(dir, 'SKILL.md'))) continue;
|
|
858
|
+
if (ownSet && !ownSet.has(e.name)) {
|
|
859
|
+
out.thirdParty += 1;
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
const p = inspectSkill({ name: e.name, dir });
|
|
864
|
+
out.scanned += 1;
|
|
865
|
+
if (!p.issues.length) out.healthy += 1;
|
|
866
|
+
for (const it of p.issues) out.codes[it.code] = (out.codes[it.code] || 0) + 1;
|
|
867
|
+
if (p.issues.length) {
|
|
868
|
+
out.items.push({
|
|
869
|
+
name: p.name,
|
|
870
|
+
score: p.score,
|
|
871
|
+
lines: p.lines,
|
|
872
|
+
action: decideAction(p),
|
|
873
|
+
actionLabel: OPT_LABEL[decideAction(p)],
|
|
874
|
+
issues: p.issues.map((x) => ({ code: x.code, severity: x.severity, why: x.why })),
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
} catch {
|
|
878
|
+
/* 单个技能体检失败不影响整库 */
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
out.items.sort((a, b) => b.score - a.score);
|
|
882
|
+
return out;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** 草稿目录(面板用)。 */
|
|
886
|
+
export function draftsDir(config, machineId) {
|
|
887
|
+
return path.join(mySkillsRoot(config, machineId), DRAFTS_DIR);
|
|
888
|
+
}
|