ygc-dsh-sync 1.2.1 → 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 +466 -545
- package/index.js +311 -223
- package/lib/api.js +164 -747
- package/lib/core.js +73 -43
- package/lib/features.js +19 -4
- 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
package/lib/api.js
CHANGED
|
@@ -35,41 +35,11 @@ import {
|
|
|
35
35
|
planFeatures,
|
|
36
36
|
rememberDecision,
|
|
37
37
|
} from './features.js';
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
applyPush,
|
|
44
|
-
buildHistoryIndex,
|
|
45
|
-
diffSummary,
|
|
46
|
-
judgeUpdate,
|
|
47
|
-
loadLineage,
|
|
48
|
-
makeStamp,
|
|
49
|
-
planSync,
|
|
50
|
-
readMnemonLedger,
|
|
51
|
-
recordLineage,
|
|
52
|
-
resolveMode,
|
|
53
|
-
saveLineage,
|
|
54
|
-
scanSkills,
|
|
55
|
-
} from './skills.js';
|
|
56
|
-
import {
|
|
57
|
-
appendMasterChanges,
|
|
58
|
-
changelogPath,
|
|
59
|
-
ensureChangelog,
|
|
60
|
-
readChangelog,
|
|
61
|
-
readChangelogTimes,
|
|
62
|
-
} from './changelog.js';
|
|
63
|
-
import { runMnemonPrepass } from './mnemon.js';
|
|
64
|
-
import {
|
|
65
|
-
runOptimizationCycle,
|
|
66
|
-
readQueue,
|
|
67
|
-
triage,
|
|
68
|
-
discoverOptimizations,
|
|
69
|
-
auditDraft,
|
|
70
|
-
MNEMON_LEDGER,
|
|
71
|
-
} from './mnemon-optimize.js';
|
|
72
|
-
import { createHostDrafter } from './mnemon-drafter.js';
|
|
38
|
+
// 1.3.0 起母版功能取消:技能只有「本机特征文件夹里的备份 + 别人格子里的备份」,
|
|
39
|
+
// 所以 skills.js 里那套「母版 vs 本机」的比对/拉取/推送,以及记忆插件那套优化闭环,
|
|
40
|
+
// 都不再从这里调用。模块本身留着(记忆插件的草稿落地走它自己的入口),
|
|
41
|
+
// 但 api.js 不再 import 那些用不上的符号。
|
|
42
|
+
import { LOCAL_SKILLS_ROOT } from './skills.js';
|
|
73
43
|
import {
|
|
74
44
|
SHARE_POLICY,
|
|
75
45
|
installSharedPlugin,
|
|
@@ -149,77 +119,6 @@ export function createApi(deps) {
|
|
|
149
119
|
/** 本机某工作区路径对应的 projectKey 集合。 */
|
|
150
120
|
const projectKeysOf = (p) => projectKeysFor(p);
|
|
151
121
|
|
|
152
|
-
/**
|
|
153
|
-
* 判新旧要的三份只读证据。
|
|
154
|
-
*
|
|
155
|
-
* lineage :母版根目录的 `.skill-lineage.json`,同步插件自己的观测记录——
|
|
156
|
-
* 每侧内容何时变成现在这样、上次一致时的内容树是哪份。
|
|
157
|
-
* mnemonLedger:`~/.mnemon/skills/ledger.json`,记忆插件的技能账本,
|
|
158
|
-
* 里面每条 hash 就是 SKILL.md 的 sha256,reason='evolution'
|
|
159
|
-
* 就是它改的。只读,不写。
|
|
160
|
-
* history :母版 `_backup/`、本机 `.pre-pull-*`、记忆插件 `versions/`
|
|
161
|
-
* 里的历史全文,按内容哈希索引出「这份内容何时出现过」。
|
|
162
|
-
*/
|
|
163
|
-
const judgeEvidence = (masterRoot) => ({
|
|
164
|
-
lineage: masterRoot ? loadLineage(path.join(masterRoot, LINEAGE_FILE)) : {},
|
|
165
|
-
mnemonLedger: readMnemonLedger(),
|
|
166
|
-
history: buildHistoryIndex({
|
|
167
|
-
masterRoot,
|
|
168
|
-
localRoot: LOCAL_SKILLS_ROOT,
|
|
169
|
-
changelogTimes: masterRoot ? readChangelogTimes(masterRoot) : null,
|
|
170
|
-
}),
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* 记录一次比对观测:两侧各自当前是什么内容、各自何时变成这样。
|
|
175
|
-
*
|
|
176
|
-
* 这是「内容修改时间」的唯一来源——不是 mtime(同步/NAS 搬运都会刷),
|
|
177
|
-
* 而是同步插件真正看到内容变化的那一刻。内容没变则时间保持不动。
|
|
178
|
-
*
|
|
179
|
-
* 两侧内容一致时顺手把 baseline 更新成这一份:基线就是「上次两侧一致的
|
|
180
|
-
* 内容树」,只有一致的时候才有意义。
|
|
181
|
-
*/
|
|
182
|
-
const observeLineage = (masterRoot, plan, by) => {
|
|
183
|
-
if (!masterRoot) return 0;
|
|
184
|
-
const file = path.join(masterRoot, LINEAGE_FILE);
|
|
185
|
-
let map = loadLineage(file);
|
|
186
|
-
const now = Date.now();
|
|
187
|
-
let touched = 0;
|
|
188
|
-
for (const [name, m] of plan.master) {
|
|
189
|
-
const l = plan.local.get(name);
|
|
190
|
-
if (!l) continue; // 只有一侧有,没有可比性
|
|
191
|
-
const same = m.treeHash === l.treeHash;
|
|
192
|
-
map = recordLineage(map, {
|
|
193
|
-
name,
|
|
194
|
-
...(same ? { baseline: m.treeHash } : {}),
|
|
195
|
-
master: m,
|
|
196
|
-
local: l,
|
|
197
|
-
by: by || '',
|
|
198
|
-
at: now,
|
|
199
|
-
});
|
|
200
|
-
touched++;
|
|
201
|
-
}
|
|
202
|
-
return saveLineage(file, map) ? touched : 0;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
/** 同步真正落地之后,把两侧一致的内容登记成新基线。 */
|
|
206
|
-
const stampBaseline = (masterRoot, entries, by) => {
|
|
207
|
-
if (!masterRoot || !entries.length) return 0;
|
|
208
|
-
const file = path.join(masterRoot, LINEAGE_FILE);
|
|
209
|
-
let map = loadLineage(file);
|
|
210
|
-
for (const e of entries) {
|
|
211
|
-
if (!e) continue;
|
|
212
|
-
map = recordLineage(map, {
|
|
213
|
-
name: e.name,
|
|
214
|
-
baseline: e.treeHash,
|
|
215
|
-
master: e,
|
|
216
|
-
local: e,
|
|
217
|
-
by: by || '',
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
return saveLineage(file, map) ? entries.length : 0;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
122
|
const routes = {
|
|
224
123
|
/** 全量状态:配置、匹配工作区、备份统计、其他机器。 */
|
|
225
124
|
'GET /state': async (req, res) => {
|
|
@@ -292,13 +191,30 @@ export function createApi(deps) {
|
|
|
292
191
|
initialized: state.features.initialized === true,
|
|
293
192
|
pending: plan.ask,
|
|
294
193
|
pendingCount: plan.ask.length,
|
|
194
|
+
// 你点过「跳过」的目录:不在 pending 里,但要能在面板上找回来(可改主意)
|
|
195
|
+
declined: plan.declined,
|
|
196
|
+
declinedCount: plan.declined.length,
|
|
295
197
|
activeCount: active.length,
|
|
296
198
|
inactiveCount: inactive.length,
|
|
297
199
|
// 巡检会自动处理的(面板上只报个数)
|
|
298
200
|
autoCreateFolder: plan.createFolder.length,
|
|
299
201
|
autoRestoreWorkspace: plan.restoreWorkspace.length,
|
|
300
202
|
},
|
|
301
|
-
skills: {
|
|
203
|
+
skills: {
|
|
204
|
+
...state.skills,
|
|
205
|
+
localRoot: LOCAL_SKILLS_ROOT,
|
|
206
|
+
// 面板要显示「备份到哪」:这里直接给出实际路径,别让前端自己拼
|
|
207
|
+
...(typeof deps.draftsRoot === 'function'
|
|
208
|
+
? { shareRoot: (() => {
|
|
209
|
+
try {
|
|
210
|
+
const d = deps.draftsRoot();
|
|
211
|
+
return d ? String(d).replace(/[\\/]learn-drafts.*$/, '') : '';
|
|
212
|
+
} catch {
|
|
213
|
+
return '';
|
|
214
|
+
}
|
|
215
|
+
})() }
|
|
216
|
+
: {}),
|
|
217
|
+
},
|
|
302
218
|
// 插件清单共享(1.0.0 从自动更新器迁来):配置 + 运行态 + 口径声明
|
|
303
219
|
plugins: {
|
|
304
220
|
...state.plugins,
|
|
@@ -933,721 +849,222 @@ export function createApi(deps) {
|
|
|
933
849
|
|
|
934
850
|
/* ---------------- 技能同步 ---------------- */
|
|
935
851
|
|
|
936
|
-
|
|
937
|
-
* 比对 + 自动同步。
|
|
852
|
+
/* ---------------- 技能共享与优化(1.3.0:母版功能已取消) ----------------
|
|
938
853
|
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
854
|
+
* 技能不再走「母版 → 本机」这一套。取而代之的是两份东西:
|
|
855
|
+
* 特征文件夹里的技能备份:`插件库/<本机标识>/skills/`(整棵目录 + 名片)
|
|
856
|
+
* 别人格子里的技能备份:只读,本机缺的拉过来,同名不同内容进学习评估
|
|
941
857
|
*
|
|
942
|
-
*
|
|
858
|
+
* 归属规矩(用户 2026-11-15):技能市场装的第三方技能不管理、不同步、不优化;
|
|
859
|
+
* 只对本机自己总结 / 编写的技能做共享与优化。判定在 lib/skill-scope.js。
|
|
860
|
+
*/
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* 扫描:本机 vs 别人的技能格,不写任何文件(拉取与评估要人点「立即共享」)。
|
|
864
|
+
*
|
|
865
|
+
* 走 runShare 的 dryRun 分支,而不是另写一份只读扫描:两处各写一份必然走形,
|
|
866
|
+
* 而且 dryRun 本来就已经是「只算不写」。
|
|
943
867
|
*/
|
|
944
868
|
'POST /skills/scan': async (req, res) => {
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const dryRunFlag = body?.dryRun === true;
|
|
948
|
-
const state = loadState();
|
|
949
|
-
const master = state.skills.masterRoot;
|
|
950
|
-
if (!master) {
|
|
951
|
-
json(res, 400, { ok: false, error: '尚未设置技能母版文件夹' });
|
|
869
|
+
if (typeof deps.runShare !== 'function') {
|
|
870
|
+
json(res, 500, { ok: false, error: '宿主未提供技能共享能力' });
|
|
952
871
|
return;
|
|
953
872
|
}
|
|
954
|
-
|
|
955
|
-
|
|
873
|
+
const report = await deps.runShare({ reason: '扫描', dryRun: true });
|
|
874
|
+
if (!report || report.ok === false) {
|
|
875
|
+
json(res, 500, { ok: false, error: (report && report.error) || '扫描失败' });
|
|
956
876
|
return;
|
|
957
877
|
}
|
|
958
|
-
|
|
959
|
-
// ── 阶段 0:记忆插件技能优化闭环(发现 → 起草 → 审核 → 落地 → 结案)
|
|
960
|
-
// 0.8.0 起这一步会写账本(结案)并调模型起草;dryRun 时只报告。
|
|
961
|
-
const optimize = auto
|
|
962
|
-
? await runOptimizationCycle({
|
|
963
|
-
localRoot: LOCAL_SKILLS_ROOT,
|
|
964
|
-
masterRoot: master,
|
|
965
|
-
stageDir: process.env.DSH_SYNC_STAGE_DIR || 'C:/DSH-Work/mnemon-drafts/stage',
|
|
966
|
-
minConfidence: Number(state.skills.mnemonMinConfidence) || 0.7,
|
|
967
|
-
enabled: state.skills.mnemonOptimize !== false,
|
|
968
|
-
// 比对/巡检必须自己把「生成草案」这一步走完。
|
|
969
|
-
//
|
|
970
|
-
// 历史:这里曾经写死 `draft: null`,理由是「比对是同步语义,起草要调模型会卡住」。
|
|
971
|
-
// 结果是任何走比对路径的巡检(面板点「比对」、开机首拉、每 120 分钟的定时轮)
|
|
972
|
-
// 都只发现建议、从不起草,建议永远停在 pending;只有单独点「立即优化」
|
|
973
|
-
// 或走定时器那条后台链路才会起草。用户要的是「巡检发现待处理就自己处理完」,
|
|
974
|
-
// 所以把起草器接回来,由 body 控制:
|
|
975
|
-
// body.draft === false → 只比对(显式要求纯比对时才这样)
|
|
976
|
-
// body.dryRun === true → 不调模型(预演)
|
|
977
|
-
// 其余情况一律带起草器,让「发现 → 起草 → 审核 → 落地 → 结案」在一条路径里跑完。
|
|
978
|
-
draft: dryRunFlag || body?.draft === false ? null : drafter(),
|
|
979
|
-
maxDrafts: Number(state.skills.mnemonMaxDrafts) || 6,
|
|
980
|
-
onEvent: (kind, payload) => log(`优化闭环 ${kind}: ${JSON.stringify(payload)}`),
|
|
981
|
-
})
|
|
982
|
-
: { enabled: false, skipped: 'auto=false', discovered: { suggestions: 0, drafts: 0 }, todo: [], dropped: [], enqueued: [], audited: [], accepted: [], errors: [] };
|
|
983
|
-
|
|
984
|
-
// 记忆插件草稿预处理:auto 时先落地再比对,比对看到的就是落地后的内容
|
|
985
|
-
const prepass = auto
|
|
986
|
-
? runMnemonPrepass({
|
|
987
|
-
masterRoot: master,
|
|
988
|
-
localRoot: LOCAL_SKILLS_ROOT,
|
|
989
|
-
modes: state.skills.modes || {},
|
|
990
|
-
machineId: state.machineId,
|
|
991
|
-
machineName: state.machineName,
|
|
992
|
-
enabled: state.skills.mnemonAuto !== false,
|
|
993
|
-
})
|
|
994
|
-
: { enabled: false, drafts: 0, apply: [], skipped: [], applied: [], pushed: [], errors: [] };
|
|
995
|
-
|
|
996
|
-
const plan = planSync({
|
|
997
|
-
masterRoot: master,
|
|
998
|
-
localRoot: LOCAL_SKILLS_ROOT,
|
|
999
|
-
modes: state.skills.modes || {},
|
|
1000
|
-
...judgeEvidence(master),
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
// 比对本身就是一次观测:记下两侧各自现在是什么内容、何时变成这样
|
|
1004
|
-
const next = loadState();
|
|
1005
|
-
const observed = observeLineage(master, plan, next.machineId);
|
|
1006
|
-
|
|
1007
|
-
const stamp = makeStamp();
|
|
1008
|
-
let pulled = { pulled: [], errors: [] };
|
|
1009
|
-
let pushed = { pushed: [], errors: [] };
|
|
1010
|
-
|
|
1011
|
-
if (auto && (plan.pull.length || plan.autoPush.length)) {
|
|
1012
|
-
// 母版更新 → 拉到本机
|
|
1013
|
-
if (plan.pull.length) {
|
|
1014
|
-
pulled = applyPull({ items: plan.pull, localRoot: LOCAL_SKILLS_ROOT, stamp });
|
|
1015
|
-
}
|
|
1016
|
-
// 本机更新(同步属性)→ 推回母版,旧版先存档
|
|
1017
|
-
if (plan.autoPush.length) {
|
|
1018
|
-
const backRoot = path.join(master, '_backup');
|
|
1019
|
-
pushed = applyPush({ items: plan.autoPush, masterRoot: master, backRoot, stamp });
|
|
1020
|
-
const auditItems = pushed.pushed.map((p) => ({
|
|
1021
|
-
skill: p.name,
|
|
1022
|
-
operation: p.operation,
|
|
1023
|
-
detail: `${p.operation === '覆盖' ? `旧版备份于 _backup/${p.name}.${stamp}` : '母版此前无此技能'}(自动:内容更新)`,
|
|
1024
|
-
}));
|
|
1025
|
-
if (auditItems.length) {
|
|
1026
|
-
try {
|
|
1027
|
-
appendMasterChanges(auditItems, {
|
|
1028
|
-
masterRoot: master,
|
|
1029
|
-
machineId: next.machineId,
|
|
1030
|
-
machineName: next.machineName,
|
|
1031
|
-
trigger: '自动同步',
|
|
1032
|
-
});
|
|
1033
|
-
} catch {
|
|
1034
|
-
/* 记录写不进去不影响同步本身 */
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
// 同步过的内容现在两侧一致,登记成新基线
|
|
1039
|
-
const aligned = [
|
|
1040
|
-
...plan.pull.map((x) => x.master),
|
|
1041
|
-
...plan.autoPush.map((x) => x.local),
|
|
1042
|
-
...plan.same.map((x) => x.master),
|
|
1043
|
-
].filter(Boolean);
|
|
1044
|
-
if (aligned.length) {
|
|
1045
|
-
let map = loadLineage(path.join(master, LINEAGE_FILE));
|
|
1046
|
-
for (const e of aligned) {
|
|
1047
|
-
map = recordLineage(map, { name: e.name, baseline: e.treeHash, master: e, local: e, by: next.machineId });
|
|
1048
|
-
}
|
|
1049
|
-
saveLineage(path.join(master, LINEAGE_FILE), map);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
next.skills.lastRunAt = Date.now();
|
|
1054
|
-
next.skills.lastResult = {
|
|
1055
|
-
pull: plan.pull.length,
|
|
1056
|
-
autoPush: plan.autoPush.length,
|
|
1057
|
-
push: plan.push.length,
|
|
1058
|
-
same: plan.same.length,
|
|
1059
|
-
localOnly: plan.localOnly.length,
|
|
1060
|
-
pulled: pulled.pulled.length,
|
|
1061
|
-
pushed: pushed.pushed.length,
|
|
1062
|
-
observed,
|
|
1063
|
-
auto,
|
|
1064
|
-
scannedAt: plan.scannedAt,
|
|
1065
|
-
mnemon: {
|
|
1066
|
-
drafts: prepass.drafts,
|
|
1067
|
-
applied: prepass.applied.length,
|
|
1068
|
-
pushed: prepass.pushed.length,
|
|
1069
|
-
skipped: prepass.skipped.length,
|
|
1070
|
-
},
|
|
1071
|
-
};
|
|
1072
|
-
if (auto && (pulled.pulled.length || pushed.pushed.length || prepass.applied.length)) {
|
|
1073
|
-
pushHistory(
|
|
1074
|
-
next,
|
|
1075
|
-
'skills',
|
|
1076
|
-
`技能自动同步:拉取 ${pulled.pulled.length}、推送 ${pushed.pushed.length}、待裁决 ${plan.push.length}${
|
|
1077
|
-
prepass.applied.length ? `、记忆插件草稿落地 ${prepass.applied.length}` : ''
|
|
1078
|
-
}`,
|
|
1079
|
-
);
|
|
1080
|
-
}
|
|
1081
|
-
saveState(next);
|
|
1082
|
-
|
|
1083
|
-
const brief = (x) => ({
|
|
1084
|
-
name: x.name,
|
|
1085
|
-
action: x.action,
|
|
1086
|
-
reason: x.reason || (x.action === 'new' ? '母版新增' : ''),
|
|
1087
|
-
basis: x.basis || null,
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
const pulledNames = new Set(pulled.pulled.map((p) => p.name));
|
|
1091
|
-
const pushedNames = new Set(pushed.pushed.map((p) => p.name));
|
|
1092
|
-
|
|
1093
878
|
json(res, 200, {
|
|
1094
879
|
ok: true,
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
})),
|
|
1108
|
-
// 判不出来的:只报告,等用户裁决(附差异摘要,方便一眼决定)
|
|
1109
|
-
push: plan.push.map((x) => ({
|
|
1110
|
-
...brief(x),
|
|
1111
|
-
masterVersion: x.master.version,
|
|
1112
|
-
localVersion: x.local.version,
|
|
1113
|
-
diff: x.diff || null,
|
|
1114
|
-
})),
|
|
1115
|
-
same: plan.same.map((x) => x.name),
|
|
1116
|
-
localOnly: plan.localOnly.map((x) => x.name),
|
|
1117
|
-
// 优化闭环:发现了什么、判断结果、入队、审核、通过
|
|
1118
|
-
optimize,
|
|
1119
|
-
// 记忆插件草稿:这一轮落地了哪些、跳过了哪些(跳过都带原因)
|
|
1120
|
-
mnemon: {
|
|
1121
|
-
enabled: prepass.enabled,
|
|
1122
|
-
drafts: prepass.drafts,
|
|
1123
|
-
apply: prepass.apply,
|
|
1124
|
-
applied: prepass.applied.map((a) => ({
|
|
1125
|
-
name: a.name,
|
|
1126
|
-
action: a.action,
|
|
1127
|
-
draftId: a.draftId,
|
|
1128
|
-
targetRoot: a.targetRoot,
|
|
1129
|
-
backup: a.backup,
|
|
1130
|
-
})),
|
|
1131
|
-
pushed: prepass.pushed.map((p) => ({ name: p.name, operation: p.operation })),
|
|
1132
|
-
skipped: prepass.skipped,
|
|
880
|
+
scope: report.scope || null,
|
|
881
|
+
peers: report.peers || [],
|
|
882
|
+
missing: report.missingPreview || [],
|
|
883
|
+
differ: report.differPreview || [],
|
|
884
|
+
thirdParty: report.thirdPartyPreview || [],
|
|
885
|
+
counts: {
|
|
886
|
+
local: report.scope ? report.scope.own : null,
|
|
887
|
+
peers: (report.peers || []).length,
|
|
888
|
+
missing: (report.missing && report.missing.found) || 0,
|
|
889
|
+
same: report.sameCount || 0,
|
|
890
|
+
differ: (report.differ && report.differ.found) || 0,
|
|
891
|
+
thirdParty: report.thirdPartyFound || 0,
|
|
1133
892
|
},
|
|
1134
|
-
|
|
1135
|
-
observed,
|
|
893
|
+
warnings: (report.errors || []).slice(0, 8),
|
|
1136
894
|
});
|
|
1137
895
|
},
|
|
1138
896
|
|
|
897
|
+
/** 立即共享一轮:备份本机 → 扫别人 → 拉缺失 → 差异进学习评估。 */
|
|
898
|
+
'POST /skills/share': async (req, res) => {
|
|
899
|
+
if (typeof deps.runShare !== 'function') {
|
|
900
|
+
json(res, 500, { ok: false, error: '宿主未提供技能共享能力' });
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const body = await readJson(req).catch(() => ({}));
|
|
904
|
+
const report = await deps.runShare({ reason: '手动', dryRun: body?.dryRun === true });
|
|
905
|
+
json(res, 200, { ok: report?.ok !== false, report });
|
|
906
|
+
},
|
|
907
|
+
|
|
1139
908
|
/**
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
* 不改任何文件。面板用它显示「现在积压了多少」。
|
|
1143
|
-
*/
|
|
1144
|
-
/**
|
|
1145
|
-
* 手动跑一轮完整流程(发现 → 起草 → 审核 → 落地 → 结案)。
|
|
1146
|
-
*
|
|
1147
|
-
* body: { dryRun?: boolean, maxDrafts?: number, wait?: boolean }
|
|
909
|
+
* 本机技能优化:精炼 / 精简 / 合并重复 / 补工况 / 补踩坑 / 修死引用 / 停用作废。
|
|
1148
910
|
*
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
* 浏览器/代理还可能先超时。要同步结果就传 `wait:true`(只建议 dryRun 时用)。
|
|
1152
|
-
* 进度用 `POST /skills/optimize/state` 的 running/nextRun 两个字段轮询。
|
|
911
|
+
* 一律后台跑:一轮要调好几次模型、每次几十秒,同步等会让人以为界面卡死。
|
|
912
|
+
* 进度用 `POST /skills/optimize/state` 轮询。
|
|
1153
913
|
*/
|
|
1154
914
|
'POST /skills/optimize/run': async (req, res) => {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
// 母版不是必需品:没有母版照样能落地并结案(结案不依赖母版是既有约定),
|
|
1158
|
-
// 只是跳过「推母版」这一步。所以这里不拦,只在报告里说明。
|
|
1159
|
-
const master = state.skills.masterRoot || undefined;
|
|
1160
|
-
const enabled = state.skills.mnemonOptimize !== false;
|
|
1161
|
-
const maxDrafts = Number(body.maxDrafts) || Number(state.skills.mnemonMaxDrafts) || 6;
|
|
1162
|
-
const dryRun = body.dryRun === true;
|
|
1163
|
-
|
|
1164
|
-
const runOnce = () =>
|
|
1165
|
-
runOptimizationCycle({
|
|
1166
|
-
localRoot: LOCAL_SKILLS_ROOT,
|
|
1167
|
-
masterRoot: master,
|
|
1168
|
-
stageDir: process.env.DSH_SYNC_STAGE_DIR || 'C:/DSH-Work/mnemon-drafts/stage',
|
|
1169
|
-
minConfidence: Number(state.skills.mnemonMinConfidence) || 0.7,
|
|
1170
|
-
maxDrafts,
|
|
1171
|
-
dryRun,
|
|
1172
|
-
// dryRun 不调模型:那样「预演」也要两分钟,且没有意义(没落地可预演)
|
|
1173
|
-
draft: dryRun ? null : drafter(),
|
|
1174
|
-
enabled,
|
|
1175
|
-
onEvent: (kind, payload) => log(`优化闭环 ${kind}: ${JSON.stringify(payload)}`),
|
|
1176
|
-
});
|
|
1177
|
-
|
|
1178
|
-
// 同步模式:只给 dryRun / 明确要 wait 的调用方
|
|
1179
|
-
if (body.wait === true || dryRun) {
|
|
1180
|
-
try {
|
|
1181
|
-
const report = await runOnce();
|
|
1182
|
-
json(res, 200, { ok: true, report });
|
|
1183
|
-
} catch (err) {
|
|
1184
|
-
json(res, 500, { ok: false, error: String((err && err.message) || err) });
|
|
1185
|
-
}
|
|
915
|
+
if (typeof deps.runOptimize !== 'function') {
|
|
916
|
+
json(res, 500, { ok: false, error: '宿主未提供技能优化能力' });
|
|
1186
917
|
return;
|
|
1187
918
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
started: false,
|
|
1194
|
-
running: true,
|
|
1195
|
-
startedAt: optimizeRun.startedAt,
|
|
1196
|
-
note: '上一轮还在跑,本次不重复启动',
|
|
1197
|
-
});
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
optimizeRun.running = true;
|
|
1201
|
-
optimizeRun.startedAt = new Date().toISOString();
|
|
1202
|
-
optimizeRun.lastReport = null;
|
|
1203
|
-
optimizeRun.lastError = null;
|
|
1204
|
-
runOnce()
|
|
1205
|
-
.then((report) => {
|
|
1206
|
-
optimizeRun.lastReport = {
|
|
1207
|
-
at: new Date().toISOString(),
|
|
1208
|
-
remaining: report.remaining,
|
|
1209
|
-
closed: report.closed,
|
|
1210
|
-
landed: (report.pipeline && report.pipeline.stage && report.pipeline.stage.landed) || [],
|
|
1211
|
-
drafted: (report.pipeline && report.pipeline.stage && report.pipeline.stage.drafted) || [],
|
|
1212
|
-
failed: (report.pipeline && report.pipeline.stage && report.pipeline.stage.failed) || [],
|
|
1213
|
-
errors: report.errors,
|
|
1214
|
-
};
|
|
1215
|
-
log(
|
|
1216
|
-
`优化闭环(手动):起草 ${optimizeRun.lastReport.drafted.length}、` +
|
|
1217
|
-
`落地 ${optimizeRun.lastReport.landed.length}、失败 ${optimizeRun.lastReport.failed.length}、` +
|
|
1218
|
-
`剩余 ${report.remaining}`,
|
|
1219
|
-
);
|
|
1220
|
-
})
|
|
1221
|
-
.catch((err) => {
|
|
1222
|
-
optimizeRun.lastError = String((err && err.message) || err);
|
|
1223
|
-
log(`优化闭环(手动)失败:${optimizeRun.lastError}`);
|
|
1224
|
-
})
|
|
1225
|
-
.finally(() => {
|
|
1226
|
-
optimizeRun.running = false;
|
|
1227
|
-
optimizeRun.finishedAt = new Date().toISOString();
|
|
1228
|
-
});
|
|
1229
|
-
json(res, 202, {
|
|
1230
|
-
ok: true,
|
|
1231
|
-
started: true,
|
|
1232
|
-
running: true,
|
|
1233
|
-
startedAt: optimizeRun.startedAt,
|
|
1234
|
-
maxDrafts,
|
|
1235
|
-
note: '起草已转后台,用 POST /skills/optimize/state 轮询进度',
|
|
1236
|
-
});
|
|
1237
|
-
},
|
|
1238
|
-
|
|
1239
|
-
'POST /skills/optimize/state': async (req, res) => {
|
|
1240
|
-
const state = loadState();
|
|
1241
|
-
const queue = readQueue();
|
|
1242
|
-
const found = discoverOptimizations({ localRoot: LOCAL_SKILLS_ROOT });
|
|
1243
|
-
const { todo, dropped } = triage({
|
|
1244
|
-
suggestions: found.suggestions,
|
|
1245
|
-
minConfidence: Number(state.skills.mnemonMinConfidence) || 0.7,
|
|
1246
|
-
seen: queue.seen,
|
|
919
|
+
const body = await readJson(req).catch(() => ({}));
|
|
920
|
+
const maxPerRound = Number(body?.maxPerRound);
|
|
921
|
+
const started = deps.runOptimize({
|
|
922
|
+
maxPerRound: Number.isFinite(maxPerRound) && maxPerRound > 0 ? maxPerRound : undefined,
|
|
923
|
+
includeMerge: body?.includeMerge !== false,
|
|
1247
924
|
});
|
|
1248
|
-
// 账本视角的三段计数:没有草稿的 / 有 ready 草稿的 / 已经在磁盘上的
|
|
1249
|
-
let ledgerView = { pending: 0, withDraft: 0, readyDrafts: 0, adopted: 0, generation: null };
|
|
1250
|
-
try {
|
|
1251
|
-
const raw = JSON.parse(fs.readFileSync(MNEMON_LEDGER, 'utf8'));
|
|
1252
|
-
const sug = Array.isArray(raw.suggestions) ? raw.suggestions : [];
|
|
1253
|
-
const drf = Array.isArray(raw.drafts) ? raw.drafts : [];
|
|
1254
|
-
const readyBySuggestion = new Set(drf.filter((d) => d && d.status === 'ready' && d.suggestionId).map((d) => d.suggestionId));
|
|
1255
|
-
const pending = sug.filter((s) => s && s.status === 'pending');
|
|
1256
|
-
ledgerView = {
|
|
1257
|
-
pending: pending.length,
|
|
1258
|
-
withDraft: pending.filter((s) => readyBySuggestion.has(s.id)).length,
|
|
1259
|
-
readyDrafts: drf.filter((d) => d && d.status === 'ready').length,
|
|
1260
|
-
adopted: sug.filter((s) => s && s.status === 'adopted').length,
|
|
1261
|
-
generation: typeof raw.generation === 'number' ? raw.generation : null,
|
|
1262
|
-
};
|
|
1263
|
-
} catch {
|
|
1264
|
-
/* 账本读不到就返回零值,面板不报错 */
|
|
1265
|
-
}
|
|
1266
925
|
json(res, 200, {
|
|
1267
926
|
ok: true,
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
startedAt: optimizeRun.startedAt,
|
|
1272
|
-
finishedAt: optimizeRun.finishedAt,
|
|
1273
|
-
lastReport: optimizeRun.lastReport,
|
|
1274
|
-
lastError: optimizeRun.lastError,
|
|
1275
|
-
enabled: state.skills.mnemonOptimize !== false,
|
|
1276
|
-
minConfidence: Number(state.skills.mnemonMinConfidence) || 0.7,
|
|
1277
|
-
queue: {
|
|
1278
|
-
file: '~/.mnemon/skills/external-drafts.json',
|
|
1279
|
-
generation: queue.generation,
|
|
1280
|
-
waiting: queue.items.filter((x) => x.status === 'waiting-draft').length,
|
|
1281
|
-
done: queue.items.filter((x) => x.status === 'done').length,
|
|
1282
|
-
seen: queue.seen.length,
|
|
1283
|
-
items: queue.items.slice(-40),
|
|
1284
|
-
},
|
|
1285
|
-
ledger: { suggestions: found.suggestions.length, readyDrafts: found.drafts.length },
|
|
1286
|
-
todo: todo.map((x) => ({ skillName: x.skillName, action: x.action, priority: x.priority, reason: x.reason })),
|
|
1287
|
-
dropped,
|
|
927
|
+
started: true,
|
|
928
|
+
note: '优化已转后台:用 POST /skills/optimize/state 轮询进度;出的稿子是待批草稿,批准才落本机',
|
|
929
|
+
state: started,
|
|
1288
930
|
});
|
|
1289
931
|
},
|
|
1290
932
|
|
|
1291
|
-
/**
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
const body = await readJson(req).catch(() => ({}));
|
|
1297
|
-
const stageDir = body.stageDir || process.env.DSH_SYNC_STAGE_DIR || 'C:/DSH-Work/mnemon-drafts/stage';
|
|
1298
|
-
const state = loadState();
|
|
1299
|
-
const queue = readQueue();
|
|
1300
|
-
const found = discoverOptimizations({ localRoot: LOCAL_SKILLS_ROOT });
|
|
1301
|
-
const { todo } = triage({
|
|
1302
|
-
suggestions: found.suggestions,
|
|
1303
|
-
minConfidence: Number(state.skills.mnemonMinConfidence) || 0.7,
|
|
1304
|
-
seen: queue.seen,
|
|
1305
|
-
});
|
|
1306
|
-
|
|
1307
|
-
const rows = [];
|
|
1308
|
-
for (const item of todo) {
|
|
1309
|
-
const file = `${stageDir}/${item.skillName}/SKILL.md`;
|
|
1310
|
-
if (!fs.existsSync(file)) {
|
|
1311
|
-
rows.push({ skillName: item.skillName, action: item.action, staged: false, ok: null, problems: [] });
|
|
1312
|
-
continue;
|
|
1313
|
-
}
|
|
1314
|
-
const text = fs.readFileSync(file, 'utf8');
|
|
1315
|
-
const audit = auditDraft({ text, name: item.skillName, isUpdate: item.action === 'update', localRoot: LOCAL_SKILLS_ROOT });
|
|
1316
|
-
rows.push({ skillName: item.skillName, action: item.action, staged: true, ok: audit.ok, problems: audit.problems, info: audit.info });
|
|
1317
|
-
}
|
|
1318
|
-
json(res, 200, { ok: true, stageDir, rows });
|
|
933
|
+
/** 优化进度:running / 上一轮结果 / 待批数量。 */
|
|
934
|
+
'POST /skills/optimize/state': async (req, res) => {
|
|
935
|
+
const state = typeof deps.optimizeState === 'function' ? deps.optimizeState() : { running: false };
|
|
936
|
+
const pending = typeof deps.learningList === 'function' ? deps.learningList() : [];
|
|
937
|
+
json(res, 200, { ok: true, ...state, pendingCount: pending.length });
|
|
1319
938
|
},
|
|
1320
939
|
|
|
1321
|
-
/**
|
|
1322
|
-
'POST /skills/
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
if (!master) {
|
|
1326
|
-
json(res, 400, { ok: false, error: '尚未设置技能母版文件夹' });
|
|
940
|
+
/** 整库体检:纯机械,不调模型、不写文件,秒回。 */
|
|
941
|
+
'POST /skills/inspect': async (req, res) => {
|
|
942
|
+
if (typeof deps.inspectSkills !== 'function') {
|
|
943
|
+
json(res, 500, { ok: false, error: '宿主未提供体检能力' });
|
|
1327
944
|
return;
|
|
1328
945
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
return;
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
const plan = planSync({
|
|
1335
|
-
masterRoot: master,
|
|
1336
|
-
localRoot: LOCAL_SKILLS_ROOT,
|
|
1337
|
-
modes: state.skills.modes || {},
|
|
1338
|
-
...judgeEvidence(master),
|
|
1339
|
-
});
|
|
1340
|
-
const stamp = makeStamp();
|
|
1341
|
-
|
|
1342
|
-
const pulled = applyPull({ items: plan.pull, localRoot: LOCAL_SKILLS_ROOT, stamp });
|
|
1343
|
-
|
|
1344
|
-
// 拉取落地后,被拉的和本来就一致的两侧都对齐了,一起登记成基线
|
|
1345
|
-
const next = loadState();
|
|
1346
|
-
const stamped = stampBaseline(
|
|
1347
|
-
master,
|
|
1348
|
-
[...plan.pull.map((x) => x.master), ...plan.same.map((x) => x.master)].filter(Boolean),
|
|
1349
|
-
next.machineId,
|
|
1350
|
-
);
|
|
1351
|
-
|
|
1352
|
-
next.skills.lastRunAt = Date.now();
|
|
1353
|
-
next.skills.lastResult = {
|
|
1354
|
-
pulled: pulled.pulled.length,
|
|
1355
|
-
pendingPush: plan.push.length,
|
|
1356
|
-
errors: pulled.errors.length,
|
|
1357
|
-
lineage: stamped,
|
|
1358
|
-
};
|
|
1359
|
-
pushHistory(
|
|
1360
|
-
next,
|
|
1361
|
-
'skills',
|
|
1362
|
-
`技能拉取:更新 ${pulled.pulled.length}、待推 ${plan.push.length}`,
|
|
1363
|
-
);
|
|
1364
|
-
saveState(next);
|
|
946
|
+
json(res, 200, { ok: true, ...(await deps.inspectSkills()) });
|
|
947
|
+
},
|
|
1365
948
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
pendingPush: plan.push.map((x) => ({ name: x.name, reason: x.reason, basis: x.basis || null })),
|
|
1371
|
-
lineage: stamped,
|
|
1372
|
-
stamp,
|
|
1373
|
-
});
|
|
949
|
+
/** 待批的学习 / 优化草稿列表。 */
|
|
950
|
+
'POST /skills/learning/list': async (req, res) => {
|
|
951
|
+
const items = typeof deps.learningList === 'function' ? await deps.learningList() : [];
|
|
952
|
+
json(res, 200, { ok: true, items });
|
|
1374
953
|
},
|
|
1375
954
|
|
|
1376
|
-
/**
|
|
1377
|
-
'POST /skills/
|
|
955
|
+
/** 读一份草稿正文(面板预览)。 */
|
|
956
|
+
'POST /skills/learning/draft': async (req, res) => {
|
|
1378
957
|
const body = await readJson(req);
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
json(res, 400, { ok: false, error: '尚未设置技能母版文件夹' });
|
|
958
|
+
const name = String(body?.name || '').trim();
|
|
959
|
+
if (!name) {
|
|
960
|
+
json(res, 400, { ok: false, error: '缺少技能名' });
|
|
1383
961
|
return;
|
|
1384
962
|
}
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
963
|
+
const r = typeof deps.learningDraft === 'function' ? await deps.learningDraft(name) : { ok: false, text: '' };
|
|
964
|
+
json(res, r.ok ? 200 : 404, r);
|
|
965
|
+
},
|
|
966
|
+
|
|
967
|
+
/** 批准一份草稿:写进本机技能根,旧版先备份。 */
|
|
968
|
+
'POST /skills/learning/approve': async (req, res) => {
|
|
969
|
+
const body = await readJson(req);
|
|
970
|
+
const name = String(body?.name || '').trim();
|
|
971
|
+
if (!name) {
|
|
972
|
+
json(res, 400, { ok: false, error: '缺少技能名' });
|
|
1388
973
|
return;
|
|
1389
974
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
masterRoot: master,
|
|
1393
|
-
localRoot: LOCAL_SKILLS_ROOT,
|
|
1394
|
-
modes: state.skills.modes || {},
|
|
1395
|
-
...judgeEvidence(master),
|
|
1396
|
-
});
|
|
1397
|
-
const stamp = makeStamp();
|
|
1398
|
-
|
|
1399
|
-
// 覆盖项:本机更新的(同步属性,在 autoPush 里)+ 待裁决里被指名的
|
|
1400
|
-
// 新增项:localOnly(用户显式指名才推)
|
|
1401
|
-
// 注意 autoPush 也要算进来——否则「本机更新」这条在面板上点「推送」
|
|
1402
|
-
// 会报「没有可推送的变更」。
|
|
1403
|
-
const overwrites = [...plan.autoPush, ...plan.push].filter((x) => names.includes(x.name));
|
|
1404
|
-
const creates = plan.localOnly
|
|
1405
|
-
.filter((x) => names.includes(x.name))
|
|
1406
|
-
.map((x) => ({ name: x.name, action: 'create', local: x.local, master: null }));
|
|
1407
|
-
const items = [...overwrites, ...creates];
|
|
1408
|
-
|
|
1409
|
-
if (!items.length) {
|
|
1410
|
-
json(res, 400, { ok: false, error: '选中的技能当前没有可推送的变更' });
|
|
975
|
+
if (typeof deps.learningApprove !== 'function') {
|
|
976
|
+
json(res, 500, { ok: false, error: '宿主未提供落地能力' });
|
|
1411
977
|
return;
|
|
1412
978
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
const out = applyPush({ items, masterRoot: master, backRoot, stamp });
|
|
1416
|
-
|
|
1417
|
-
// 每一次实际写入都留记录
|
|
1418
|
-
const audit = appendMasterChanges(
|
|
1419
|
-
out.pushed.map((p) => ({
|
|
1420
|
-
skill: p.name,
|
|
1421
|
-
operation: p.operation,
|
|
1422
|
-
detail: p.operation === '覆盖' ? `旧版备份于 _backup/${p.name}.${stamp}` : '母版此前无此技能',
|
|
1423
|
-
})),
|
|
1424
|
-
{
|
|
1425
|
-
masterRoot: master,
|
|
1426
|
-
machineId: state.machineId,
|
|
1427
|
-
machineName: state.machineName,
|
|
1428
|
-
trigger: '用户授权',
|
|
1429
|
-
},
|
|
1430
|
-
);
|
|
1431
|
-
|
|
1432
|
-
const next = loadState();
|
|
1433
|
-
// 推送落地后母版就是本机这一份,登记成新基线
|
|
1434
|
-
const stamped = stampBaseline(
|
|
1435
|
-
master,
|
|
1436
|
-
out.pushed.map((p) => items.find((i) => i.name === p.name)).filter(Boolean).map((i) => i.local).filter(Boolean),
|
|
1437
|
-
next.machineId,
|
|
1438
|
-
);
|
|
1439
|
-
pushHistory(
|
|
1440
|
-
next,
|
|
1441
|
-
'skills',
|
|
1442
|
-
`技能推送:覆盖 ${out.pushed.filter((p) => p.operation === '覆盖').length}、新增 ${
|
|
1443
|
-
out.pushed.filter((p) => p.operation === '新增').length
|
|
1444
|
-
}`,
|
|
1445
|
-
);
|
|
1446
|
-
saveState(next);
|
|
1447
|
-
|
|
1448
|
-
json(res, 200, {
|
|
1449
|
-
ok: true,
|
|
1450
|
-
pushed: out.pushed,
|
|
1451
|
-
errors: out.errors,
|
|
1452
|
-
backupRoot: backRoot,
|
|
1453
|
-
stamp,
|
|
1454
|
-
lineage: stamped,
|
|
1455
|
-
changelog: {
|
|
1456
|
-
path: audit.written[0]?.path || changelogPath(master),
|
|
1457
|
-
written: audit.written.length,
|
|
1458
|
-
failed: audit.failed,
|
|
1459
|
-
},
|
|
1460
|
-
});
|
|
979
|
+
const r = await deps.learningApprove({ name, force: body?.force === true });
|
|
980
|
+
json(res, r.ok ? 200 : 409, r);
|
|
1461
981
|
},
|
|
1462
982
|
|
|
1463
|
-
/**
|
|
1464
|
-
'POST /skills/
|
|
983
|
+
/** 驳回:只标状态与理由,草稿文件留着复核。 */
|
|
984
|
+
'POST /skills/learning/reject': async (req, res) => {
|
|
1465
985
|
const body = await readJson(req);
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
json(res, 400, { ok: false, error: '尚未设置技能母版文件夹' });
|
|
986
|
+
const name = String(body?.name || '').trim();
|
|
987
|
+
if (!name) {
|
|
988
|
+
json(res, 400, { ok: false, error: '缺少技能名' });
|
|
1470
989
|
return;
|
|
1471
990
|
}
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
991
|
+
const r = typeof deps.learningReject === 'function'
|
|
992
|
+
? await deps.learningReject({ name, reason: body?.reason })
|
|
993
|
+
: { ok: false, message: '宿主未提供驳回能力' };
|
|
994
|
+
json(res, r.ok ? 200 : 404, r);
|
|
1475
995
|
},
|
|
1476
996
|
|
|
1477
|
-
/**
|
|
1478
|
-
'POST /skills/
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
if (!master) {
|
|
1482
|
-
json(res, 400, { ok: false, error: '尚未设置技能母版文件夹' });
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
if (!fs.existsSync(master)) {
|
|
1486
|
-
json(res, 400, { ok: false, error: `母版文件夹不存在或不可访问:${master}` });
|
|
997
|
+
/** 技能归属:谁归本插件管、谁是市场装的第三方。 */
|
|
998
|
+
'POST /skills/scope': async (req, res) => {
|
|
999
|
+
if (typeof deps.skillScope !== 'function') {
|
|
1000
|
+
json(res, 500, { ok: false, error: '宿主未提供归属判定' });
|
|
1487
1001
|
return;
|
|
1488
1002
|
}
|
|
1489
|
-
|
|
1490
|
-
const modes = state.skills.modes || {};
|
|
1491
|
-
const m = scanSkills(master);
|
|
1492
|
-
const l = scanSkills(LOCAL_SKILLS_ROOT);
|
|
1493
|
-
const names = new Set([...m.keys(), ...l.keys()]);
|
|
1494
|
-
const { lineage, mnemonLedger, history } = judgeEvidence(master);
|
|
1495
|
-
|
|
1496
|
-
const items = [...names].sort().map((name) => {
|
|
1497
|
-
const mm = m.get(name);
|
|
1498
|
-
const ll = l.get(name);
|
|
1499
|
-
const mode = resolveMode(name, modes, m, l);
|
|
1500
|
-
let status;
|
|
1501
|
-
if (mm && !ll) status = mode === 'master' ? 'master-only' : 'missing-local';
|
|
1502
|
-
else if (!mm && ll) status = 'local-only';
|
|
1503
|
-
else status = mm.treeHash === ll.treeHash ? 'same' : 'diff';
|
|
1504
|
-
|
|
1505
|
-
// 内容不同时顺便给出「谁更新」的结论,前端不用再自己算
|
|
1506
|
-
let verdict = null;
|
|
1507
|
-
let basis = null;
|
|
1508
|
-
let hint = '';
|
|
1509
|
-
let diff = null;
|
|
1510
|
-
if (status === 'diff') {
|
|
1511
|
-
const j = judgeUpdate({ name, master: mm, local: ll, record: lineage[name], ledger: mnemonLedger, history });
|
|
1512
|
-
verdict = j.verdict;
|
|
1513
|
-
basis = j.basis;
|
|
1514
|
-
hint = j.detail;
|
|
1515
|
-
diff = diffSummary(mm.text, ll.text);
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
return {
|
|
1519
|
-
name,
|
|
1520
|
-
mode,
|
|
1521
|
-
explicit: Object.prototype.hasOwnProperty.call(modes, name),
|
|
1522
|
-
status,
|
|
1523
|
-
verdict,
|
|
1524
|
-
basis,
|
|
1525
|
-
hint,
|
|
1526
|
-
diff,
|
|
1527
|
-
masterVersion: (mm && mm.version) || '',
|
|
1528
|
-
localVersion: (ll && ll.version) || '',
|
|
1529
|
-
inMaster: !!mm,
|
|
1530
|
-
inLocal: !!ll,
|
|
1531
|
-
};
|
|
1532
|
-
});
|
|
1533
|
-
|
|
1534
|
-
json(res, 200, { ok: true, masterRoot: master, localRoot: LOCAL_SKILLS_ROOT, items });
|
|
1003
|
+
json(res, 200, { ok: true, ...(await deps.skillScope()) });
|
|
1535
1004
|
},
|
|
1536
1005
|
|
|
1537
|
-
/**
|
|
1538
|
-
'POST /skills/
|
|
1006
|
+
/** 人工改一个技能的归属(自己编写 / 第三方)。 */
|
|
1007
|
+
'POST /skills/scope/set': async (req, res) => {
|
|
1539
1008
|
const body = await readJson(req);
|
|
1540
|
-
const
|
|
1541
|
-
const
|
|
1542
|
-
if (!
|
|
1543
|
-
json(res, 400, { ok: false, error: '
|
|
1009
|
+
const name = String(body?.name || '').trim();
|
|
1010
|
+
const scope = String(body?.scope || '').trim();
|
|
1011
|
+
if (!name || (scope !== 'own' && scope !== 'third-party')) {
|
|
1012
|
+
json(res, 400, { ok: false, error: '需要 name 与 scope(own | third-party)' });
|
|
1544
1013
|
return;
|
|
1545
1014
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
json(res, 400, { ok: false, error: '属性只能是 sync / local / master' });
|
|
1015
|
+
if (typeof deps.setSkillScope !== 'function') {
|
|
1016
|
+
json(res, 500, { ok: false, error: '宿主未提供归属写入' });
|
|
1549
1017
|
return;
|
|
1550
1018
|
}
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
json(res, 400, { ok: false, error: '未选择任何技能' });
|
|
1554
|
-
return;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
const stamp = makeStamp();
|
|
1558
|
-
const backRoot = path.join(master, '_backup');
|
|
1559
|
-
const modes = state.skills.modes || {};
|
|
1560
|
-
const m0 = scanSkills(master);
|
|
1561
|
-
const l0 = scanSkills(LOCAL_SKILLS_ROOT);
|
|
1562
|
-
|
|
1563
|
-
const results = [];
|
|
1564
|
-
const applied = [];
|
|
1565
|
-
|
|
1566
|
-
for (const name of names) {
|
|
1567
|
-
const from = resolveMode(name, modes, m0, l0);
|
|
1568
|
-
const out = applyModeChange({
|
|
1569
|
-
name,
|
|
1570
|
-
from,
|
|
1571
|
-
to: mode,
|
|
1572
|
-
masterRoot: master,
|
|
1573
|
-
localRoot: LOCAL_SKILLS_ROOT,
|
|
1574
|
-
backRoot,
|
|
1575
|
-
stamp,
|
|
1576
|
-
});
|
|
1577
|
-
results.push(out);
|
|
1578
|
-
if (out.ok) applied.push({ name, mode });
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// 只有成功的才落属性
|
|
1582
|
-
const next = loadState();
|
|
1583
|
-
for (const a of applied) next.skills.modes[a.name] = a.mode;
|
|
1584
|
-
const auditItems = results.flatMap((r) => (r.ok && r.audit ? r.audit : []));
|
|
1585
|
-
if (applied.length) {
|
|
1586
|
-
pushHistory(next, 'skills', `技能属性:${applied.length} 个改为「${MODE_LABEL[mode]}」`);
|
|
1587
|
-
}
|
|
1588
|
-
saveState(next);
|
|
1589
|
-
|
|
1590
|
-
// 对母版的实际改动写审计
|
|
1591
|
-
let audit = { written: 0, failed: [] };
|
|
1592
|
-
if (auditItems.length) {
|
|
1593
|
-
const w = appendMasterChanges(auditItems, {
|
|
1594
|
-
masterRoot: master,
|
|
1595
|
-
machineId: next.machineId,
|
|
1596
|
-
machineName: next.machineName,
|
|
1597
|
-
trigger: '用户授权',
|
|
1598
|
-
});
|
|
1599
|
-
audit = { written: w.written.length, failed: w.failed, path: w.written[0] && w.written[0].path };
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
json(res, 200, {
|
|
1603
|
-
ok: true,
|
|
1604
|
-
mode,
|
|
1605
|
-
results,
|
|
1606
|
-
applied: applied.length,
|
|
1607
|
-
failed: results.filter((r) => !r.ok).length,
|
|
1608
|
-
audit,
|
|
1609
|
-
});
|
|
1019
|
+
const r = await deps.setSkillScope({ name, scope });
|
|
1020
|
+
json(res, r.ok ? 200 : 400, r);
|
|
1610
1021
|
},
|
|
1611
1022
|
|
|
1612
|
-
/**
|
|
1023
|
+
/**
|
|
1024
|
+
* 技能共享 + 优化的配置。
|
|
1025
|
+
*
|
|
1026
|
+
* 注意:**没有 masterRoot**。母版功能 1.3.0 起取消,技能来源就是各台机器
|
|
1027
|
+
* 特征文件夹里的备份;老 state 里残留的 masterRoot 在这里被显式清掉。
|
|
1028
|
+
*/
|
|
1613
1029
|
'POST /skills/config': async (req, res) => {
|
|
1614
1030
|
const body = await readJson(req);
|
|
1615
1031
|
const state = loadState();
|
|
1616
1032
|
const skills = { ...state.skills };
|
|
1033
|
+
const share = { ...(skills.share || {}) };
|
|
1617
1034
|
|
|
1618
|
-
if (typeof body.
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
}
|
|
1035
|
+
if (typeof body.enabled === 'boolean') skills.enabled = body.enabled;
|
|
1036
|
+
if (typeof body.shareEnabled === 'boolean') share.enabled = body.shareEnabled;
|
|
1037
|
+
if (typeof body.learn === 'boolean') share.learn = body.learn;
|
|
1038
|
+
if (typeof body.autoApply === 'boolean') share.autoApply = body.autoApply;
|
|
1039
|
+
if (body.learnPerRound !== undefined) {
|
|
1040
|
+
const n = Number(body.learnPerRound);
|
|
1041
|
+
if (!Number.isFinite(n) || n < 0 || n > 24) {
|
|
1042
|
+
json(res, 400, { ok: false, error: '每轮评估条数应为 0 到 24' });
|
|
1043
|
+
return;
|
|
1628
1044
|
}
|
|
1045
|
+
share.learnPerRound = Math.floor(n);
|
|
1629
1046
|
}
|
|
1630
|
-
if (
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
if (typeof body.mnemonOptimize === 'boolean') skills.mnemonOptimize = body.mnemonOptimize;
|
|
1635
|
-
if (body.mnemonMinConfidence !== undefined) {
|
|
1636
|
-
const n = Number(body.mnemonMinConfidence);
|
|
1637
|
-
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
1638
|
-
json(res, 400, { ok: false, error: '置信度门槛应在 0 到 1 之间' });
|
|
1047
|
+
if (body.optimizePerRound !== undefined) {
|
|
1048
|
+
const n = Number(body.optimizePerRound);
|
|
1049
|
+
if (!Number.isFinite(n) || n < 1 || n > 12) {
|
|
1050
|
+
json(res, 400, { ok: false, error: '每轮优化条数应为 1 到 12' });
|
|
1639
1051
|
return;
|
|
1640
1052
|
}
|
|
1641
|
-
|
|
1053
|
+
share.optimizePerRound = Math.floor(n);
|
|
1642
1054
|
}
|
|
1643
1055
|
if (body.intervalMinutes !== undefined) {
|
|
1644
1056
|
const n = Number(body.intervalMinutes);
|
|
1645
1057
|
if (!Number.isFinite(n) || n < 1 || n > 10080) {
|
|
1646
|
-
json(res, 400, { ok: false, error: '间隔应为 1
|
|
1058
|
+
json(res, 400, { ok: false, error: '间隔应为 1 到 10080 之间的整数(分钟)' });
|
|
1647
1059
|
return;
|
|
1648
1060
|
}
|
|
1649
1061
|
skills.intervalMinutes = Math.floor(n);
|
|
1650
1062
|
}
|
|
1063
|
+
// 母版已取消:显式清掉残留字段,避免老配置继续影响任何判定
|
|
1064
|
+
delete skills.masterRoot;
|
|
1065
|
+
delete skills.mnemonAuto;
|
|
1066
|
+
delete skills.mnemonOptimize;
|
|
1067
|
+
skills.share = share;
|
|
1651
1068
|
state.skills = skills;
|
|
1652
1069
|
saveState(state);
|
|
1653
1070
|
json(res, 200, { ok: true, skills: state.skills, localRoot: LOCAL_SKILLS_ROOT });
|