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/client.js CHANGED
@@ -113,17 +113,22 @@ function makePanel(React) {
113
113
  const [note, setNote] = useState(null);
114
114
  const [midDraft, setMidDraft] = useState('');
115
115
  const [scan, setScan] = useState(null);
116
+ // 归属判定摘要(哪些技能归本插件管、哪些是市场第三方)
117
+ const [scopeInfo, setScopeInfo] = useState(null);
116
118
  // 特征文件夹采纳:面板上待人工决定的那些(路径数组)
117
119
  const [featurePicks, setFeaturePicks] = useState([]);
118
120
  const [patrolTime, setPatrolTime] = useState('');
119
121
  const [skillScan, setSkillScan] = useState(null);
120
- const [skillPicks, setSkillPicks] = useState([]);
121
- const [skillMasterDraft, setSkillMasterDraft] = useState('');
122
122
  const [skillIv, setSkillIv] = useState('');
123
- const [skillLog, setSkillLog] = useState(null);
124
- const [skillList, setSkillList] = useState(null);
125
- const [listPicks, setListPicks] = useState([]);
126
- // 插件清单共享:明细单独取一次(要扫盘),根目录草稿跟着输入框走
123
+ // 1.3.0:技能共享与优化。母版那套输入框与勾选表都没了,换成这几样——
124
+ // inspectResult 体检结果(纯机械,不调模型)
125
+ // learningList 待批草稿(学习 + 优化共用一份台账)
126
+ // draftPreview 草稿正文预览
127
+ // optRunning 后台优化是否在跑(面板靠它禁用按钮并显示进度)
128
+ const [inspectResult, setInspectResult] = useState(null);
129
+ const [learningList, setLearningList] = useState(null);
130
+ const [draftPreview, setDraftPreview] = useState(null);
131
+ const [optRunning, setOptRunning] = useState(false);
127
132
  // 插件清单共享:明细单独取一次(要扫盘),根目录草稿跟着输入框走
128
133
  const [pluginDetail, setPluginDetail] = useState(null);
129
134
  const [pluginRootDraft, setPluginRootDraft] = useState('');
@@ -403,31 +408,33 @@ function makePanel(React) {
403
408
  }
404
409
  };
405
410
 
411
+ /* ---------------- 技能共享与优化(1.3.0,母版已取消) ----------------
412
+ *
413
+ * 面板上就这几个动作,全部对应 HTTP 路由,没有本地状态机:
414
+ * 查看现状 /skills/scan 只读,扫本机 + 别人的格子
415
+ * 立即共享 /skills/share 备份本机 → 拉缺失 → 差异进学习评估
416
+ * 体检 /skills/inspect 纯机械,不调模型、不写盘
417
+ * 优化 /skills/optimize/run 后台跑,出草稿
418
+ * 待批草稿 /skills/learning/list 批准 / 驳回 / 看正文
419
+ */
420
+
406
421
  const doSkillScan = async () => {
407
422
  setBusy('skillscan');
408
423
  setNote(null);
409
424
  try {
410
425
  const r = await call('/skills/scan', {});
411
426
  setSkillScan(r);
412
- // 只有判不出来的才需要你勾选
413
- setSkillPicks((r.push || []).map((x) => x.name));
414
- const bits = [];
415
- const autoPull = (r.pull || []).filter((x) => x.done).length;
416
- const autoPush = (r.autoPush || []).filter((x) => x.done).length;
417
- if (autoPull) bits.push(`已自动拉取 ${autoPull}`);
418
- else if (r.pull && r.pull.length) bits.push(`待拉 ${r.pull.length}`);
419
- if (autoPush) bits.push(`已自动推送 ${autoPush}`);
420
- else if (r.autoPush && r.autoPush.length) bits.push(`待推 ${r.autoPush.length}`);
421
- const mn = r.mnemon || {};
422
- if (mn.applied && mn.applied.length) bits.push(`记忆插件草稿落地 ${mn.applied.length}`);
423
- if (mn.skipped && mn.skipped.length) bits.push(`草稿跳过 ${mn.skipped.length}`);
424
- if (r.push.length) bits.push(`待你裁决 ${r.push.length}`);
425
- if (r.same.length) bits.push(`一致 ${r.same.length}`);
426
- if (r.localOnly.length) bits.push(`本地独有 ${r.localOnly.length}`);
427
- setNote({
428
- tone: r.push.length ? 'warn' : 'ok',
429
- text: bits.length ? `技能比对:${bits.join('、')}` : '技能比对:母版为空',
430
- });
427
+ if (r && r.scope) setScopeInfo(r.scope);
428
+ const c = (r && r.counts) || {};
429
+ const bits = [
430
+ `本机自研 ${c.local ?? 0}`,
431
+ `他机特征本 ${c.peers ?? 0}`,
432
+ `缺失 ${c.missing ?? 0}`,
433
+ `差异 ${c.differ ?? 0}`,
434
+ `一致 ${c.same ?? 0}`,
435
+ ];
436
+ if ((r.warnings || []).length) bits.push(`${r.warnings.length} 条提示`);
437
+ setNote({ tone: 'ok', text: `技能现状:${bits.join('、')}` });
431
438
  } catch (e) {
432
439
  setNote({ tone: 'err', text: String((e && e.message) || e) });
433
440
  } finally {
@@ -435,18 +442,48 @@ function makePanel(React) {
435
442
  }
436
443
  };
437
444
 
438
- const doSkillPull = async () => {
439
- setBusy('skillpull');
445
+ const doSkillShare = async () => {
446
+ setBusy('skillshare');
447
+ setNote({ tone: 'warn', text: '正在共享:备份本机技能 → 扫别人的格子 → 拉缺失 → 差异进学习评估(可能要几十秒)' });
448
+ try {
449
+ const r = await call('/skills/share', {});
450
+ const rep = (r && r.report) || {};
451
+ if (rep.skipped) {
452
+ setNote({ tone: 'warn', text: `共享未执行:${rep.skipped}` });
453
+ } else {
454
+ const bits = [
455
+ `备份 ${rep.backup ? rep.backup.counts.total : 0}`,
456
+ `他机 ${(rep.peers || []).length} 台`,
457
+ `缺失 ${(rep.missing && rep.missing.found) || 0}(已拉 ${(rep.missing && rep.missing.pulled) || 0})`,
458
+ `差异 ${(rep.differ && rep.differ.found) || 0}(评估 ${(rep.differ && rep.differ.evaluated) || 0})`,
459
+ `待批 ${rep.pending || 0}`,
460
+ ];
461
+ const errs = (rep.errors || []).slice(0, 2);
462
+ setNote({
463
+ tone: errs.length ? 'warn' : 'ok',
464
+ text: `共享完成:${bits.join('、')}` + (errs.length ? `(${errs.join(';')})` : ''),
465
+ });
466
+ }
467
+ await refresh();
468
+ await doSkillLearning();
469
+ } catch (e) {
470
+ setNote({ tone: 'err', text: String((e && e.message) || e) });
471
+ } finally {
472
+ setBusy('');
473
+ }
474
+ };
475
+
476
+ const doSkillInspect = async () => {
477
+ setBusy('skillinspect');
440
478
  setNote(null);
441
479
  try {
442
- const r = await call('/skills/pull', {});
480
+ const r = await call('/skills/inspect', {});
481
+ setInspectResult(r);
482
+ if (r && r.scope) setScopeInfo((cur) => cur || { counts: r.scope, thirdParty: [] });
443
483
  setNote({
444
- tone: r.errors.length ? 'warn' : 'ok',
445
- text: `拉取完成:更新 ${r.pulled.length} 个技能${
446
- r.pendingPush.length ? `,${r.pendingPush.length} 个待推送` : ''
447
- }${r.errors.length ? `,${r.errors.length} 个失败` : ''}`,
484
+ tone: (r.items || []).length ? 'warn' : 'ok',
485
+ text: `体检完成:${r.scanned} 个(健康 ${r.healthy},第三方跳过 ${r.thirdParty}),有问题的 ${(r.items || []).length} 个`,
448
486
  });
449
- await doSkillScan();
450
487
  } catch (e) {
451
488
  setNote({ tone: 'err', text: String((e && e.message) || e) });
452
489
  } finally {
@@ -454,19 +491,66 @@ function makePanel(React) {
454
491
  }
455
492
  };
456
493
 
457
- const doSkillPush = async () => {
458
- if (!skillPicks.length) {
459
- setNote({ tone: 'err', text: '请先勾选要推送到母版的技能' });
460
- return;
494
+ /**
495
+ * 跑一轮优化。
496
+ *
497
+ * 后台跑,所以这里只管两件事:起跑、然后定时问进度。最多盯 5 分钟——
498
+ * 一轮通常 3 条 × 每条 20-60 秒;超过还没完就交回用户,让他稍后点「待批草稿」。
499
+ */
500
+ const doSkillOptimize = async () => {
501
+ setBusy('skillopt');
502
+ setOptRunning(true);
503
+ setNote({ tone: 'warn', text: '优化已转后台:体检 → 出草稿(每条一次模型调用,约 20-60 秒)' });
504
+ try {
505
+ const r = await call('/skills/optimize/run', {});
506
+ if (r && r.started === false) {
507
+ setNote({ tone: 'warn', text: '上一轮还在跑,本次没有重复启动' });
508
+ return;
509
+ }
510
+ for (let i = 0; i < 60; i += 1) {
511
+ await new Promise((ok) => setTimeout(ok, 5000));
512
+ const st = await call('/skills/optimize/state', {});
513
+ if (st.running) {
514
+ setNote({ tone: 'warn', text: `优化进行中…(已 ${(i + 1) * 5} 秒)` });
515
+ continue;
516
+ }
517
+ const lr = st.lastReport;
518
+ setOptRunning(false);
519
+ if (!lr) {
520
+ setNote({ tone: 'warn', text: st.lastError ? `优化失败:${st.lastError}` : '优化结束,但没有拿到本轮报告(看宿主日志)' });
521
+ return;
522
+ }
523
+ const bits = [
524
+ `体检 ${lr.scanned || 0}`,
525
+ `健康 ${lr.healthy || 0}`,
526
+ `第三方跳过 ${lr.skippedThirdParty || 0}`,
527
+ `出草稿 ${lr.drafted || 0}`,
528
+ `审核未过 ${lr.rejected || 0}`,
529
+ ];
530
+ if ((lr.merged || 0) > 0) bits.push(`合并 ${lr.merged}`);
531
+ setNote({
532
+ tone: (lr.errors || []).length ? 'warn' : 'ok',
533
+ text: `优化完成:${bits.join('、')}` + ((lr.errors || []).length ? `(${String(lr.errors[0]).slice(0, 80)})` : ''),
534
+ });
535
+ await refresh();
536
+ await doSkillLearning();
537
+ return;
538
+ }
539
+ setOptRunning(false);
540
+ setNote({ tone: 'warn', text: '优化仍在后台跑,稍后点「待批草稿」看结果' });
541
+ } catch (e) {
542
+ setOptRunning(false);
543
+ setNote({ tone: 'err', text: String((e && e.message) || e) });
544
+ } finally {
545
+ setBusy('');
461
546
  }
462
- setBusy('skillpush');
547
+ };
548
+
549
+ const doSkillLearning = async () => {
550
+ setBusy('skilllearn');
463
551
  try {
464
- const r = await call('/skills/push', { names: skillPicks });
465
- setNote({
466
- tone: r.errors.length ? 'warn' : 'ok',
467
- text: `已推送 ${r.pushed.length} 个技能到母版(旧版备份在 _backup/${r.stamp})`,
468
- });
469
- await doSkillScan();
552
+ const r = await call('/skills/learning/list', {});
553
+ setLearningList(r.items || []);
470
554
  } catch (e) {
471
555
  setNote({ tone: 'err', text: String((e && e.message) || e) });
472
556
  } finally {
@@ -474,11 +558,15 @@ function makePanel(React) {
474
558
  }
475
559
  };
476
560
 
477
- const saveSkills = async (patch) => {
478
- setBusy('skillcfg');
561
+ const viewDraft = async (name) => {
562
+ setBusy(`draft:${name}`);
479
563
  try {
480
- await call('/skills/config', patch);
481
- await load();
564
+ const r = await call('/skills/learning/draft', { name });
565
+ if (!r.ok) {
566
+ setNote({ tone: 'warn', text: `读不到草稿:${r.message || name}` });
567
+ return;
568
+ }
569
+ setDraftPreview({ name, text: r.text });
482
570
  } catch (e) {
483
571
  setNote({ tone: 'err', text: String((e && e.message) || e) });
484
572
  } finally {
@@ -486,11 +574,18 @@ function makePanel(React) {
486
574
  }
487
575
  };
488
576
 
489
- const loadSkillLog = async () => {
490
- setBusy('skilllog');
577
+ const approveDraft = async (name) => {
578
+ setBusy(`approve:${name}`);
491
579
  try {
492
- const r = await call('/skills/changelog', { limit: 30 });
493
- setSkillLog(r);
580
+ const r = await call('/skills/learning/approve', { name });
581
+ if (r.ok) {
582
+ setNote({ tone: 'ok', text: `${name} 已落地${r.backup ? `(旧版备份在 ${r.backup})` : ''}` });
583
+ setDraftPreview(null);
584
+ } else {
585
+ setNote({ tone: 'warn', text: r.message || '落地失败' });
586
+ }
587
+ await doSkillLearning();
588
+ await refresh();
494
589
  } catch (e) {
495
590
  setNote({ tone: 'err', text: String((e && e.message) || e) });
496
591
  } finally {
@@ -498,13 +593,49 @@ function makePanel(React) {
498
593
  }
499
594
  };
500
595
 
501
- const loadSkillList = async () => {
502
- setBusy('skilllist');
503
- setNote(null);
596
+ const rejectDraft = async (name) => {
597
+ setBusy(`reject:${name}`);
504
598
  try {
505
- const r = await call('/skills/list', {});
506
- setSkillList(r);
507
- setListPicks([]);
599
+ const r = await call('/skills/learning/reject', { name, reason: '面板驳回' });
600
+ setNote({
601
+ tone: r.ok ? 'ok' : 'warn',
602
+ text: r.ok ? `${name} 已驳回(草稿留着,可复核)` : r.message || '驳回失败',
603
+ });
604
+ await doSkillLearning();
605
+ } catch (e) {
606
+ setNote({ tone: 'err', text: String((e && e.message) || e) });
607
+ } finally {
608
+ setBusy('');
609
+ }
610
+ };
611
+
612
+ /** 人工改归属:市场第三方 ↔ 自己编写。改完立刻重算,不用等下一轮。 */
613
+ const setScope = async (name, scope) => {
614
+ setBusy(`scope:${name}`);
615
+ try {
616
+ const r = await call('/skills/scope/set', { name, scope });
617
+ if (r.ok) {
618
+ if (r.scopeSnapshot) setScopeInfo(r.scopeSnapshot);
619
+ setNote({
620
+ tone: 'ok',
621
+ text: `${name} 已改为「${scope === 'own' ? '自己编写' : '第三方'}」,下一轮同步与优化按新归属处理`,
622
+ });
623
+ } else {
624
+ setNote({ tone: 'warn', text: r.message || '改归属失败' });
625
+ }
626
+ } catch (e) {
627
+ setNote({ tone: 'err', text: String((e && e.message) || e) });
628
+ } finally {
629
+ setBusy('');
630
+ }
631
+ };
632
+
633
+ const saveSkills = async (patch) => {
634
+ setBusy('skillcfg');
635
+ try {
636
+ await call('/skills/config', patch);
637
+ await refresh();
638
+ setNote({ tone: 'ok', text: '技能共享设置已保存' });
508
639
  } catch (e) {
509
640
  setNote({ tone: 'err', text: String((e && e.message) || e) });
510
641
  } finally {
@@ -576,26 +707,6 @@ function makePanel(React) {
576
707
  }
577
708
  };
578
709
 
579
- const changeMode = async (mode) => {
580
- if (!listPicks.length) {
581
- setNote({ tone: 'err', text: '请先勾选技能' });
582
- return;
583
- }
584
- setBusy('mode');
585
- try {
586
- const r = await call('/skills/mode', { names: listPicks, mode });
587
- const names = { sync: '同步', local: '本机独属', master: '母版有·本机不拉取' };
588
- const bits = [`${r.applied} 个改为「${names[mode]}」`];
589
- if (r.failed) bits.push(`${r.failed} 个失败`);
590
- if (r.audit && r.audit.written) bits.push(`已记录 ${r.audit.written} 条母版改动`);
591
- setNote({ tone: r.failed ? 'warn' : 'ok', text: bits.join(',') });
592
- await loadSkillList();
593
- } catch (e) {
594
- setNote({ tone: 'err', text: String((e && e.message) || e) });
595
- } finally {
596
- setBusy('');
597
- }
598
- };
599
710
 
600
711
  if (!state) {
601
712
  return h(
@@ -612,6 +723,12 @@ function makePanel(React) {
612
723
  const allSelected = ws.length > 0 && (selected || []).length === ws.length;
613
724
  const folders = state.folders || [];
614
725
 
726
+ // 技能共享派生量:面板顶部那行摘要与按钮上的计数
727
+ const shareState = (state.skills && state.skills.share) || {};
728
+ const shareRoot = (shareState && shareState.root) || (state.skills && state.skills.shareRoot) || '';
729
+ const optLast = shareState.lastOptimize || null;
730
+ const pendingCount = (learningList || []).length;
731
+
615
732
  const bannerClass =
616
733
  note && note.tone === 'ok'
617
734
  ? 'dsb-banner dsb-banner-ok'
@@ -1062,6 +1179,47 @@ function makePanel(React) {
1062
1179
  ),
1063
1180
  )
1064
1181
  : null,
1182
+
1183
+ /**
1184
+ * 你点过「跳过」的目录。
1185
+ *
1186
+ * 跳过的语义就是「从待决定清单里消失」,所以它必须在这里有个落点,
1187
+ * 否则改不了主意只能翻文件。点「改为建特征文件夹」走 /features/adopt,
1188
+ * 会顺手把跳过记录清掉。
1189
+ */
1190
+ (state.features && (state.features.declined || []).length)
1191
+ ? h(
1192
+ 'div',
1193
+ { style: { marginTop: 14 } },
1194
+ h(
1195
+ 'div',
1196
+ { className: 'dsb-row-t', style: { marginBottom: 6 } },
1197
+ `已跳过:${state.features.declined.length} 个(不再询问,可改主意)`,
1198
+ ),
1199
+ (state.features.declined || []).map((w) =>
1200
+ h(
1201
+ 'div',
1202
+ { className: 'dsb-row', key: w.path },
1203
+ h(
1204
+ 'div',
1205
+ { className: 'dsb-row-main' },
1206
+ h('div', { className: 'dsb-row-t' }, w.title),
1207
+ h('div', { className: 'dsb-row-d' }, '你选了不在这个目录建本机特征文件夹'),
1208
+ h('div', { className: 'dsb-row-d' }, w.path),
1209
+ ),
1210
+ h(
1211
+ 'button',
1212
+ {
1213
+ className: 'dsb-btn dsb-btn-sm',
1214
+ disabled: !!busy,
1215
+ onClick: () => doAdopt([w.path]),
1216
+ },
1217
+ '改为建特征文件夹',
1218
+ ),
1219
+ ),
1220
+ ),
1221
+ )
1222
+ : null,
1065
1223
  ),
1066
1224
 
1067
1225
  // 插件清单同步(1.0.0 从自动更新器迁入)
@@ -1269,75 +1427,63 @@ function makePanel(React) {
1269
1427
  })(),
1270
1428
  ),
1271
1429
 
1272
- // 技能同步
1430
+ // 技能共享 + 本机优化(1.3.0)
1431
+ //
1432
+ // 母版取消了:技能的身体在特征文件夹里,一人一格。所以这一栏只有三件事——
1433
+ // ① 共享:把本机自研技能备份出去、把别人有的拉回来
1434
+ // ② 优化:点一下后台体检 + 精炼 + 合并 + 补全,出草稿等人批
1435
+ // ③ 归属:谁归本插件管;市场装的第三方技能不在这里面
1273
1436
  h(
1274
1437
  'div',
1275
1438
  { className: 'dsb-card' },
1276
1439
  h(
1277
1440
  'div',
1278
1441
  { className: 'dsb-card-h' },
1279
- h('h3', { className: 'dsb-card-t' }, '技能同步'),
1280
- state.skills && state.skills.lastRunAt
1281
- ? h('span', { className: 'dsb-tag' }, `上次 ${fmtAgo(state.skills.lastRunAt)}`)
1442
+ h('h3', { className: 'dsb-card-t' }, '技能共享与优化'),
1443
+ state.skills && state.skills.share && state.skills.share.lastRunAt
1444
+ ? h('span', { className: 'dsb-tag' }, `上次共享 ${fmtAgo(state.skills.share.lastRunAt)}`)
1282
1445
  : null,
1283
1446
  ),
1284
1447
  h(
1285
1448
  'p',
1286
1449
  { className: 'dsb-sub', style: { marginBottom: 12 } },
1287
- '把母版文件夹里的技能拉到本地部署。母版更新则覆盖本地;本地更新只报告,确认后才推回母版;本地独有默认跳过。',
1450
+ '技能不再走母版:本机自研技能备份进自己的特征文件夹(含说明、来源、更新时间),别的机器的备份只读拉取。' +
1451
+ '同名但内容不同的技能不做覆盖,交给学习评估出合并稿。市场装的第三方技能不管理、不同步、不优化。',
1288
1452
  ),
1289
1453
 
1290
- // 母版路径
1454
+ // 操作行
1291
1455
  h(
1292
1456
  'div',
1293
- { className: 'dsb-actions', style: { marginBottom: 10 } },
1294
- h('input', {
1295
- className: 'dsb-input',
1296
- style: { flex: 1, minWidth: 240 },
1297
- value: skillMasterDraft !== '' ? skillMasterDraft : (state.skills && state.skills.masterRoot) || '',
1298
- placeholder: '母版文件夹,例如 W:\\DSH同步工作空间\\系统文件\\Skills',
1299
- disabled: busy === 'skillcfg',
1300
- onChange: (e) => setSkillMasterDraft(e.target.value),
1301
- onKeyDown: (e) => {
1302
- if (e.key === 'Enter') {
1303
- saveSkills({ masterRoot: e.target.value });
1304
- setSkillMasterDraft('');
1305
- }
1306
- },
1307
- }),
1457
+ { className: 'dsb-actions', style: { marginBottom: 10, flexWrap: 'wrap' } },
1308
1458
  h(
1309
1459
  'button',
1310
- {
1311
- className: 'dsb-btn dsb-btn-primary dsb-btn-sm',
1312
- disabled: busy === 'skillcfg',
1313
- onClick: () => {
1314
- const v = skillMasterDraft !== '' ? skillMasterDraft : (state.skills && state.skills.masterRoot) || '';
1315
- if (v.trim()) {
1316
- saveSkills({ masterRoot: v });
1317
- setSkillMasterDraft('');
1318
- }
1319
- },
1320
- },
1321
- '保存路径',
1460
+ { className: 'dsb-btn dsb-btn-primary dsb-btn-sm', onClick: doSkillScan, disabled: !!busy },
1461
+ busy === 'skillscan' ? '读取中…' : '查看现状',
1322
1462
  ),
1323
- ),
1324
- state.skills && state.skills.masterRoot
1325
- ? h('div', { className: 'dsb-row-d', style: { marginBottom: 10 } }, `本地部署位置:${state.skills.localRoot || '~/.agents/skills'}`)
1326
- : null,
1327
-
1328
- // 操作
1329
- h(
1330
- 'div',
1331
- { className: 'dsb-actions' },
1332
1463
  h(
1333
1464
  'button',
1334
- { className: 'dsb-btn dsb-btn-primary dsb-btn-sm', onClick: doSkillScan, disabled: !!busy },
1335
- busy === 'skillscan' ? '比对中…' : '比对',
1465
+ { className: 'dsb-btn dsb-btn-primary dsb-btn-sm', onClick: doSkillShare, disabled: !!busy },
1466
+ busy === 'skillshare' ? '共享中…' : '立即共享',
1336
1467
  ),
1337
1468
  h(
1338
1469
  'button',
1339
- { className: 'dsb-btn dsb-btn-sm', onClick: doSkillPull, disabled: !!busy },
1340
- busy === 'skillpull' ? '拉取中…' : '立即拉取',
1470
+ { className: 'dsb-btn dsb-btn-sm', onClick: doSkillInspect, disabled: !!busy },
1471
+ busy === 'skillinspect' ? '体检中…' : '体检(不调模型)',
1472
+ ),
1473
+ h(
1474
+ 'button',
1475
+ {
1476
+ className: 'dsb-btn dsb-btn-sm',
1477
+ onClick: doSkillOptimize,
1478
+ disabled: !!busy || optRunning,
1479
+ title: '后台跑一轮:精炼 / 精简 / 合并重复 / 补工况 / 补踩坑 / 修死引用;出草稿,批准才落本机',
1480
+ },
1481
+ optRunning ? '优化中…' : '优化本机技能',
1482
+ ),
1483
+ h(
1484
+ 'button',
1485
+ { className: 'dsb-btn dsb-btn-sm', onClick: doSkillLearning, disabled: !!busy },
1486
+ busy === 'skilllearn' ? '读取中…' : `待批草稿(${pendingCount})`,
1341
1487
  ),
1342
1488
  h(
1343
1489
  'label',
@@ -1345,39 +1491,35 @@ function makePanel(React) {
1345
1491
  h('input', {
1346
1492
  type: 'checkbox',
1347
1493
  className: 'dsb-chk',
1348
- checked: !!(state.skills && state.skills.enabled),
1349
- disabled: busy === 'skillcfg' || !(state.skills && state.skills.masterRoot),
1494
+ checked: !!(state.skills && state.skills.enabled !== false),
1495
+ disabled: busy === 'skillcfg',
1350
1496
  onChange: (e) => saveSkills({ enabled: e.target.checked }),
1351
1497
  }),
1352
1498
  h('span', { className: 'dsb-row-t' }, '自动同步'),
1353
1499
  ),
1354
1500
  h(
1355
1501
  'label',
1356
- { className: 'dsb-actions', style: { gap: 6 }, title: '每轮同步前先把记忆插件写好的技能草稿落地并推回母版' },
1502
+ { className: 'dsb-actions', style: { gap: 6 }, title: '同名不同内容时调模型评估怎么合并;关掉只记录差异' },
1357
1503
  h('input', {
1358
1504
  type: 'checkbox',
1359
1505
  className: 'dsb-chk',
1360
- checked: !!(state.skills && state.skills.mnemonAuto !== false),
1361
- disabled: busy === 'skillcfg' || !(state.skills && state.skills.masterRoot),
1362
- onChange: (e) => saveSkills({ mnemonAuto: e.target.checked }),
1506
+ checked: !!(state.skills && state.skills.share && state.skills.share.learn !== false),
1507
+ disabled: busy === 'skillcfg',
1508
+ onChange: (e) => saveSkills({ learn: e.target.checked }),
1363
1509
  }),
1364
- h('span', { className: 'dsb-row-t' }, '记忆插件优化自动处理'),
1510
+ h('span', { className: 'dsb-row-t' }, '差异学习'),
1365
1511
  ),
1366
1512
  h(
1367
1513
  'label',
1368
- {
1369
- className: 'dsb-actions',
1370
- style: { gap: 6 },
1371
- title: '同步前先发现记忆插件的新优化:判断有没有必要 → 有必要就生成草稿 → 审核草稿 → 合格才通过',
1372
- },
1514
+ { className: 'dsb-actions', style: { gap: 6 }, title: '评估结论直接落到本机技能根(默认关:先出草稿,人批了再落)' },
1373
1515
  h('input', {
1374
1516
  type: 'checkbox',
1375
1517
  className: 'dsb-chk',
1376
- checked: !!(state.skills && state.skills.mnemonOptimize !== false),
1377
- disabled: busy === 'skillcfg' || !(state.skills && state.skills.masterRoot),
1378
- onChange: (e) => saveSkills({ mnemonOptimize: e.target.checked }),
1518
+ checked: !!(state.skills && state.skills.share && state.skills.share.autoApply === true),
1519
+ disabled: busy === 'skillcfg',
1520
+ onChange: (e) => saveSkills({ autoApply: e.target.checked }),
1379
1521
  }),
1380
- h('span', { className: 'dsb-row-t' }, '优化闭环(发现→审核→通过)'),
1522
+ h('span', { className: 'dsb-row-t' }, '评估通过直接落地'),
1381
1523
  ),
1382
1524
  h('span', { className: 'dsb-row-t' }, '每'),
1383
1525
  h('input', {
@@ -1388,7 +1530,7 @@ function makePanel(React) {
1388
1530
  max: 10080,
1389
1531
  step: 1,
1390
1532
  value: skillIv || String((state.skills && state.skills.intervalMinutes) ?? 120),
1391
- disabled: !(state.skills && state.skills.enabled) || busy === 'skillcfg',
1533
+ disabled: !(state.skills && state.skills.enabled !== false) || busy === 'skillcfg',
1392
1534
  onChange: (e) => setSkillIv(e.target.value),
1393
1535
  onBlur: (e) => {
1394
1536
  const n = Number(e.target.value);
@@ -1398,58 +1540,95 @@ function makePanel(React) {
1398
1540
  }),
1399
1541
  h('span', { className: 'dsb-row-t' }, '分钟(默认 120)'),
1400
1542
  ),
1543
+ h(
1544
+ 'div',
1545
+ { className: 'dsb-row-d', style: { marginBottom: 10 } },
1546
+ `本机技能部署位置:${(state.skills && state.skills.localRoot) || '~/.agents/skills'}` +
1547
+ (shareRoot ? ` · 特征备份目录:${shareRoot}` : ''),
1548
+ ),
1401
1549
 
1402
- // 比对结果
1403
- skillScan
1550
+ // 归属概览
1551
+ scopeInfo
1404
1552
  ? h(
1405
1553
  'div',
1406
- { style: { marginTop: 14 } },
1407
-
1408
- // 待拉取(自动执行了)
1409
- skillScan.pull.length
1554
+ { style: { marginTop: 4, marginBottom: 10 } },
1555
+ h(
1556
+ 'div',
1557
+ { className: 'dsb-row-t', style: { marginBottom: 4 } },
1558
+ `归属:本机自研 ${(scopeInfo.counts && scopeInfo.counts.own) || 0} 个 · 市场第三方 ${(scopeInfo.counts && scopeInfo.counts.thirdParty) || 0} 个(第三方不同步、不优化)`,
1559
+ ),
1560
+ (scopeInfo.thirdParty || []).length
1410
1561
  ? h(
1411
1562
  'div',
1412
- null,
1413
- h(
1414
- 'div',
1415
- { className: 'dsb-row-t', style: { marginBottom: 6 } },
1416
- `母版更新:${skillScan.pull.length} 个${skillScan.auto === false ? '(仅比对,未执行)' : '(已自动拉取)'}`,
1417
- ),
1418
- skillScan.pull.map((p) =>
1563
+ { className: 'dsb-hist', style: { maxHeight: 110, overflowY: 'auto' } },
1564
+ (scopeInfo.thirdParty || []).map((x, i) =>
1419
1565
  h(
1420
1566
  'div',
1421
- { className: 'dsb-row', key: p.name },
1567
+ { key: `tp${i}`, style: { display: 'flex', gap: 8, alignItems: 'center' } },
1568
+ h('span', { style: { flex: 1 } }, `${x.name}(${x.by})`),
1422
1569
  h(
1423
- 'div',
1424
- { className: 'dsb-row-main' },
1425
- h(
1426
- 'div',
1427
- { className: 'dsb-row-t' },
1428
- p.name,
1429
- h('span', { className: 'dsb-tag dsb-tag-ok' }, p.action === 'new' ? '新增' : '更新'),
1430
- p.done ? h('span', { className: 'dsb-tag dsb-tag-ok' }, '已完成') : null,
1431
- ),
1432
- h('div', { className: 'dsb-row-d' }, p.reason || ''),
1570
+ 'button',
1571
+ {
1572
+ className: 'dsb-btn dsb-btn-sm',
1573
+ disabled: !!busy,
1574
+ title: '误判成第三方?改成「自己编写」,之后就会参与共享与优化',
1575
+ onClick: () => setScope(x.name, 'own'),
1576
+ },
1577
+ '改为自己编写',
1433
1578
  ),
1434
1579
  ),
1435
1580
  ),
1436
1581
  )
1437
- : null,
1582
+ : h('div', { className: 'dsb-row-d' }, '没有识别到市场第三方技能'),
1583
+ )
1584
+ : null,
1585
+
1586
+ // 上次共享结果
1587
+ state.skills && state.skills.share && state.skills.share.lastResult
1588
+ ? h(
1589
+ 'div',
1590
+ { className: 'dsb-row-d', style: { marginBottom: 8 } },
1591
+ (() => {
1592
+ const lr = state.skills.share.lastResult;
1593
+ return (
1594
+ `上次共享(${lr.reason || '定时'}):备份 ${lr.backup || 0} 个、他机 ${lr.peers || 0} 台、` +
1595
+ `缺失 ${lr.missing || 0}(已拉 ${lr.pulled || 0})、差异 ${lr.differ || 0}(评估 ${lr.evaluated || 0})、待批 ${lr.pending || 0}`
1596
+ );
1597
+ })(),
1598
+ )
1599
+ : null,
1438
1600
 
1439
- // 本机更新(同步属性已自动推送)
1440
- skillScan.autoPush && skillScan.autoPush.length
1601
+ // 上次优化结果
1602
+ optLast
1603
+ ? h(
1604
+ 'div',
1605
+ { className: 'dsb-row-d', style: { marginBottom: 8 } },
1606
+ `上次优化:体检 ${optLast.scanned || 0} 个(健康 ${optLast.healthy || 0},第三方跳过 ${optLast.skippedThirdParty || 0})、` +
1607
+ `出草稿 ${optLast.drafted || 0}、审核未过 ${optLast.rejected || 0}` +
1608
+ ((optLast.draftedNames || []).length ? ` · ${optLast.draftedNames.join('、')}` : ''),
1609
+ )
1610
+ : null,
1611
+
1612
+ // 现状
1613
+ skillScan
1614
+ ? h(
1615
+ 'div',
1616
+ { style: { marginTop: 12 } },
1617
+
1618
+ // 缺失(已拉 / 待拉)
1619
+ (skillScan.missing || []).length
1441
1620
  ? h(
1442
1621
  'div',
1443
- { style: { marginTop: 14 } },
1622
+ null,
1444
1623
  h(
1445
1624
  'div',
1446
1625
  { className: 'dsb-row-t', style: { marginBottom: 6 } },
1447
- `本机更新:${skillScan.autoPush.length} 个(已自动推送到母版)`,
1626
+ `本机没有、别人有:${skillScan.missing.length} 个`,
1448
1627
  ),
1449
- skillScan.autoPush.map((p) =>
1628
+ skillScan.missing.map((p) =>
1450
1629
  h(
1451
1630
  'div',
1452
- { className: 'dsb-row', key: p.name },
1631
+ { className: 'dsb-row', key: `miss-${p.name}` },
1453
1632
  h(
1454
1633
  'div',
1455
1634
  { className: 'dsb-row-main' },
@@ -1457,439 +1636,181 @@ function makePanel(React) {
1457
1636
  'div',
1458
1637
  { className: 'dsb-row-t' },
1459
1638
  p.name,
1460
- h('span', { className: 'dsb-tag dsb-tag-ok' }, p.done ? '已推送' : '待推送'),
1461
- ),
1462
- h(
1463
- 'div',
1464
- { className: 'dsb-row-d' },
1465
- `${p.reason}${BASIS_LABEL[p.basis] ? ` · ${BASIS_LABEL[p.basis]}` : ''}`,
1639
+ h('span', { className: 'dsb-tag dsb-tag-ok' }, '待拉取'),
1640
+ p.version ? h('span', { className: 'dsb-tag' }, `v${p.version}`) : null,
1466
1641
  ),
1642
+ h('div', { className: 'dsb-row-d' }, `来自 ${p.from || '未知特征本'}`),
1467
1643
  ),
1468
1644
  ),
1469
1645
  ),
1470
1646
  )
1471
1647
  : null,
1472
1648
 
1473
- // 待推送(需确认):只剩判不出来的那批
1474
- skillScan.push.length
1649
+ // 差异(交给学习评估)
1650
+ (skillScan.differ || []).length
1475
1651
  ? h(
1476
1652
  'div',
1477
1653
  { style: { marginTop: 14 } },
1478
1654
  h(
1479
1655
  'div',
1480
1656
  { className: 'dsb-row-t', style: { marginBottom: 6 } },
1481
- `判不出谁新,需要你决定:${skillScan.push.length} 个`,
1657
+ `同名但内容不同:${skillScan.differ.length} 个(不覆盖,进学习评估)`,
1482
1658
  ),
1483
- skillScan.push.map((p) =>
1659
+ skillScan.differ.map((p) =>
1484
1660
  h(
1485
1661
  'div',
1486
- { className: 'dsb-ws', key: p.name },
1487
- h('input', {
1488
- type: 'checkbox',
1489
- className: 'dsb-chk',
1490
- checked: skillPicks.includes(p.name),
1491
- onChange: () =>
1492
- setSkillPicks((cur) =>
1493
- cur.includes(p.name) ? cur.filter((x) => x !== p.name) : [...cur, p.name],
1494
- ),
1495
- }),
1662
+ { className: 'dsb-row', key: `diff-${p.name}` },
1496
1663
  h(
1497
1664
  'div',
1498
- { className: 'dsb-ws-body' },
1665
+ { className: 'dsb-row-main' },
1499
1666
  h(
1500
1667
  'div',
1501
1668
  { className: 'dsb-row-t' },
1502
1669
  p.name,
1503
- h(
1504
- 'span',
1505
- { className: `dsb-tag ${p.action === 'overwrite' ? 'dsb-tag-warn' : ''}` },
1506
- p.action === 'overwrite' ? '本地更新' : '内容不同',
1507
- ),
1670
+ h('span', { className: 'dsb-tag dsb-tag-warn' }, '内容不同'),
1508
1671
  ),
1509
1672
  h(
1510
1673
  'div',
1511
1674
  { className: 'dsb-row-d' },
1512
- `${p.reason}${p.localVersion ? `(本地 v${p.localVersion}` : ''}${
1513
- p.masterVersion ? ` / 母版 v${p.masterVersion})` : p.localVersion ? ')' : ''
1514
- }${BASIS_LABEL[p.basis] ? ` · ${BASIS_LABEL[p.basis]}` : ''}`,
1675
+ `来源 ${(p.peers || []).join('、')} · 不同指纹 ${p.distinctHashes || 1} 种`,
1515
1676
  ),
1516
- p.diff && p.diff.changed
1517
- ? h(
1518
- 'div',
1519
- { className: 'dsb-row-d' },
1520
- `母版独有 ${p.diff.masterOnly} 行、本机独有 ${p.diff.localOnly} 行(${p.diff.masterBytes} vs ${p.diff.localBytes} 字节)`,
1521
- )
1522
- : null,
1523
- p.diff && p.diff.samples && p.diff.samples.length
1524
- ? h(
1525
- 'div',
1526
- {
1527
- className: 'dsb-row-d',
1528
- style: { fontFamily: 'monospace', fontSize: 11, whiteSpace: 'pre-wrap' },
1529
- },
1530
- p.diff.samples.join('\n'),
1531
- )
1532
- : null,
1533
1677
  ),
1534
1678
  ),
1535
1679
  ),
1536
- h(
1537
- 'div',
1538
- { className: 'dsb-actions', style: { marginTop: 10 } },
1539
- h(
1540
- 'button',
1541
- {
1542
- className: 'dsb-btn dsb-btn-primary dsb-btn-sm',
1543
- disabled: !!busy || !skillPicks.length,
1544
- onClick: doSkillPush,
1545
- },
1546
- busy === 'skillpush' ? '推送中…' : `推送到母版(${skillPicks.length})`,
1547
- ),
1548
- h(
1549
- 'button',
1550
- {
1551
- className: 'dsb-btn dsb-btn-sm',
1552
- onClick: () => setSkillPicks(skillScan.push.map((x) => x.name)),
1553
- },
1554
- '全选',
1555
- ),
1556
- ),
1557
1680
  )
1558
1681
  : null,
1559
1682
 
1560
- // 记录入口
1683
+ // 计数
1561
1684
  h(
1562
1685
  'div',
1563
- { className: 'dsb-actions', style: { marginTop: 12 } },
1564
- h(
1565
- 'button',
1566
- { className: 'dsb-btn dsb-btn-sm', onClick: loadSkillLog, disabled: !!busy },
1567
- busy === 'skilllog' ? '读取中…' : '查看母版修改记录',
1568
- ),
1569
- h(
1570
- 'button',
1571
- {
1572
- className: 'dsb-btn dsb-btn-sm',
1573
- onClick: loadSkillList,
1574
- disabled: !!busy || !(state.skills && state.skills.masterRoot),
1575
- },
1576
- busy === 'skilllist' ? '读取中…' : '技能列表 / 设置属性',
1577
- ),
1578
- skillLog
1579
- ? h(
1580
- 'span',
1581
- { className: 'dsb-row-d' },
1582
- skillLog.exists
1583
- ? `${skillLog.rows.length} 条 · ${state.skills.masterRoot}\\_技能修改记录.md`
1584
- : '记录文件尚未创建',
1585
- )
1586
- : null,
1686
+ { className: 'dsb-row-d', style: { marginTop: 12 } },
1687
+ `本机自研 ${(skillScan.counts && skillScan.counts.local) || 0} 个 · ` +
1688
+ `一致 ${(skillScan.counts && skillScan.counts.same) || 0} 个 · ` +
1689
+ `他机特征本 ${(skillScan.counts && skillScan.counts.peers) || 0} 个`,
1587
1690
  ),
1588
- skillLog && skillLog.rows.length
1589
- ? h(
1590
- 'div',
1591
- { className: 'dsb-hist', style: { marginTop: 8, maxHeight: 180, overflowY: 'auto' } },
1592
- skillLog.rows.map((r, i) =>
1593
- h(
1594
- 'div',
1595
- { key: i },
1596
- `${r.time} · ${r.operation} · ${r.skill} · ${r.actor} · ${r.trigger}${
1597
- r.detail ? ` · ${r.detail}` : ''
1598
- }`,
1599
- ),
1600
- ),
1601
- )
1602
- : null,
1691
+ )
1692
+ : null,
1603
1693
 
1604
- // 记忆插件草稿:这一轮落地了什么、跳过了什么
1605
- skillScan.mnemon && (skillScan.mnemon.applied.length || skillScan.mnemon.skipped.length)
1606
- ? h(
1694
+ // 体检结果
1695
+ inspectResult
1696
+ ? h(
1697
+ 'div',
1698
+ { style: { marginTop: 14 } },
1699
+ h(
1700
+ 'div',
1701
+ { className: 'dsb-row-t', style: { marginBottom: 6 } },
1702
+ `体检:${inspectResult.scanned} 个(健康 ${inspectResult.healthy},第三方跳过 ${inspectResult.thirdParty})`,
1703
+ ),
1704
+ (inspectResult.items || []).slice(0, 12).map((p) =>
1705
+ h(
1706
+ 'div',
1707
+ { className: 'dsb-row', key: `ins-${p.name}` },
1708
+ h(
1607
1709
  'div',
1608
- { style: { marginTop: 10 } },
1710
+ { className: 'dsb-row-main' },
1609
1711
  h(
1610
1712
  'div',
1611
- { className: 'dsb-row-t', style: { marginBottom: 6 } },
1612
- `记忆插件技能优化:落地 ${skillScan.mnemon.applied.length} 个` +
1613
- (skillScan.mnemon.pushed.length ? `(已推回母版 ${skillScan.mnemon.pushed.length})` : '') +
1614
- (skillScan.mnemon.skipped.length ? `、跳过 ${skillScan.mnemon.skipped.length} 个` : ''),
1713
+ { className: 'dsb-row-t' },
1714
+ p.name,
1715
+ h('span', { className: 'dsb-tag dsb-tag-warn' }, `分 ${p.score}`),
1716
+ h('span', { className: 'dsb-tag' }, p.actionLabel || p.action),
1615
1717
  ),
1616
- skillScan.mnemon.applied.length
1617
- ? h(
1618
- 'div',
1619
- { className: 'dsb-hist', style: { maxHeight: 120, overflowY: 'auto' } },
1620
- skillScan.mnemon.applied.map((a, i) =>
1621
- h('div', { key: `ma${i}` }, `+ ${a.name}(${a.action === 'create' ? '新建' : '更新'})`),
1622
- ),
1623
- )
1624
- : null,
1625
- skillScan.mnemon.skipped.length
1626
- ? h(
1627
- 'div',
1628
- { className: 'dsb-hist', style: { maxHeight: 140, overflowY: 'auto' } },
1629
- skillScan.mnemon.skipped.map((s, i) =>
1630
- h('div', { key: `ms${i}` }, `- ${s.name}:${s.detail || s.reason}`),
1631
- ),
1632
- )
1633
- : null,
1634
- )
1635
- : null,
1636
-
1637
- // 优化闭环:发现了什么、入队了什么、审核通过什么
1638
- skillScan.optimize && skillScan.optimize.discovered
1639
- ? h(
1640
- 'div',
1641
- { style: { marginTop: 10 } },
1642
1718
  h(
1643
1719
  'div',
1644
- { className: 'dsb-row-t', style: { marginBottom: 6 } },
1645
- `优化闭环:发现 ${skillScan.optimize.discovered.suggestions} 条建议 / ` +
1646
- `${skillScan.optimize.discovered.drafts} 份草稿 · 要做 ${(skillScan.optimize.todo || []).length} · ` +
1647
- `不做 ${(skillScan.optimize.dropped || []).length} · 起草 ${(skillScan.optimize.pipeline?.stage?.drafted || []).length} · ` +
1648
- `落地 ${(skillScan.optimize.accepted || []).length} · 结案 ${(skillScan.optimize.closed || []).length}`,
1649
- ),
1650
- (skillScan.optimize.audited || []).length
1651
- ? h(
1652
- 'div',
1653
- { className: 'dsb-hist', style: { maxHeight: 140, overflowY: 'auto' } },
1654
- skillScan.optimize.audited.map((a, i) =>
1655
- h(
1656
- 'div',
1657
- { key: `oa${i}` },
1658
- `${a.ok ? '✓' : '✗'} ${a.skillName}${a.source === 'mnemon-draft' ? '(记忆插件草稿)' : ''}${
1659
- a.problems && a.problems.length ? ':' + a.problems.slice(0, 2).join(';') : ''
1660
- }`,
1661
- ),
1662
- ),
1663
- )
1664
- : null,
1665
- (skillScan.optimize.dropped || []).length
1666
- ? h(
1667
- 'div',
1668
- { className: 'dsb-hist', style: { maxHeight: 120, overflowY: 'auto' } },
1669
- skillScan.optimize.dropped.map((d, i) =>
1670
- h('div', { key: `od${i}` }, `- ${d.skillName}:${d.reason}`),
1671
- ),
1672
- )
1673
- : null,
1674
- (skillScan.optimize.pipeline?.stage?.failed || []).length
1675
- ? h(
1676
- 'div',
1677
- { className: 'dsb-hist', style: { maxHeight: 120, overflowY: 'auto' } },
1678
- skillScan.optimize.pipeline.stage.failed.map((f, i) =>
1679
- h('div', { key: `of${i}` }, `✗ ${f.skillName}(${f.phase}):${f.reason}`),
1680
- ),
1681
- )
1682
- : null,
1683
- h(
1684
- 'div',
1685
- { className: 'dsb-actions', style: { marginTop: 8, gap: 8 } },
1686
- h(
1687
- 'button',
1688
- {
1689
- className: 'dsb-btn',
1690
- disabled: busy === 'optrun',
1691
- title: '立刻跑一轮:发现 → 起草 → 审核 → 落地 → 结案',
1692
- onClick: async () => {
1693
- setBusy('optrun');
1694
- setNote({ tone: 'ok', text: '优化闭环:已提交,起草在后台跑(每条一次模型调用,约 20-60 秒)' });
1695
- try {
1696
- const r = await call('/skills/optimize/run', {});
1697
- if (r.started === false) {
1698
- setNote({ tone: 'warn', text: '上一轮还在跑,本次没有重复启动' });
1699
- return;
1700
- }
1701
- // 后台跑,轮询看结果:最多盯 3 分钟,每 5 秒一次
1702
- const startedAt = r.startedAt;
1703
- for (let i = 0; i < 36; i++) {
1704
- await new Promise((ok) => setTimeout(ok, 5000));
1705
- const st = await call('/skills/optimize/state', {});
1706
- if (st.running) {
1707
- setNote({ tone: 'ok', text: `优化闭环起草中…(已 ${(i + 1) * 5} 秒)` });
1708
- continue;
1709
- }
1710
- const lr = st.lastReport;
1711
- if (!lr || (startedAt && lr.at && lr.at < startedAt)) {
1712
- setNote({ tone: 'warn', text: '优化闭环已结束,但没有拿到本轮报告(看宿主日志)' });
1713
- return;
1714
- }
1715
- setNote({
1716
- tone: (lr.failed || []).length || (lr.errors || []).length ? 'warn' : 'ok',
1717
- text:
1718
- `优化闭环:起草 ${(lr.drafted || []).length}、落地 ${(lr.landed || []).length}、` +
1719
- `失败 ${(lr.failed || []).length}、剩余 ${lr.remaining ?? '?'}` +
1720
- ((lr.errors || []).length ? `(首个错误:${String(lr.errors[0]).slice(0, 80)})` : ''),
1721
- });
1722
- return;
1723
- }
1724
- setNote({ tone: 'warn', text: '优化闭环仍在后台跑,稍后点「只看状态」看结果' });
1725
- } catch (e) {
1726
- setNote({ tone: 'warn', text: '优化闭环提交失败:' + (e && e.message ? e.message : String(e)) });
1727
- } finally {
1728
- setBusy(null);
1729
- }
1730
- },
1731
- },
1732
- busy === 'optrun' ? '后台起草中…' : '跑一轮优化闭环',
1733
- ),
1734
- h(
1735
- 'button',
1736
- {
1737
- className: 'dsb-btn',
1738
- disabled: busy === 'optdry',
1739
- title: '读账本与运行态,不改任何文件',
1740
- onClick: async () => {
1741
- setBusy('optdry');
1742
- try {
1743
- const st = await call('/skills/optimize/state', {});
1744
- const lv = st.ledgerView || {};
1745
- const lr = st.lastReport;
1746
- setNote({
1747
- tone: 'ok',
1748
- text:
1749
- `账本:待处理 ${lv.pending ?? '?'}(已有草稿 ${lv.withDraft ?? 0})、` +
1750
- `已结案 ${lv.adopted ?? '?'}、generation ${lv.generation ?? '?'}` +
1751
- (st.running ? ' · 后台正在起草' : '') +
1752
- (lr
1753
- ? ` · 上次:起草 ${(lr.drafted || []).length}、落地 ${(lr.landed || []).length}、剩余 ${lr.remaining}`
1754
- : ' · 还没有手动跑过'),
1755
- });
1756
- } catch (e) {
1757
- setNote({ tone: 'warn', text: '读状态失败:' + (e && e.message ? e.message : String(e)) });
1758
- } finally {
1759
- setBusy(null);
1760
- }
1761
- },
1762
- },
1763
- '只看状态',
1764
- ),
1720
+ { className: 'dsb-row-d' },
1721
+ (p.issues || []).map((x) => `[${x.code}] ${x.why}`).join(';').slice(0, 260),
1765
1722
  ),
1766
- )
1767
- : null,
1768
-
1769
- // 统计
1770
- h(
1771
- 'div',
1772
- { className: 'dsb-row-d', style: { marginTop: 12 } },
1773
- `一致 ${skillScan.same.length} 个 · 本地独有 ${skillScan.localOnly.length} 个(跳过,不推母版)`,
1723
+ ),
1724
+ ),
1774
1725
  ),
1726
+ (inspectResult.items || []).length > 12
1727
+ ? h('div', { className: 'dsb-row-d' }, `…还有 ${inspectResult.items.length - 12} 个,出草稿时按分数从高往低处理`)
1728
+ : null,
1775
1729
  )
1776
1730
  : null,
1777
1731
 
1778
- // 技能列表 + 属性
1779
- skillList
1732
+ // 待批草稿
1733
+ learningList
1780
1734
  ? h(
1781
1735
  'div',
1782
1736
  { style: { marginTop: 16 } },
1783
1737
  h(
1784
1738
  'div',
1785
- { className: 'dsb-card-h' },
1786
- h('h4', { className: 'dsb-card-t', style: { fontSize: 13 } }, '技能与单机属性'),
1787
- h(
1788
- 'button',
1789
- {
1790
- className: 'dsb-btn dsb-btn-sm',
1791
- onClick: () =>
1792
- setListPicks(
1793
- skillList.items.length === listPicks.length ? [] : skillList.items.map((i) => i.name),
1794
- ),
1795
- },
1796
- skillList.items.length === listPicks.length ? '全不选' : '全选',
1797
- ),
1798
- ),
1799
- h(
1800
- 'p',
1801
- { className: 'dsb-sub', style: { marginBottom: 10 } },
1802
- '属性只存本机。改属性会执行实质动作:设「本机独属」删母版、设「本机不拉取」删本机、设「同步」推送到母版——对母版的改动都会写进修改记录。',
1803
- ),
1804
- h(
1805
- 'div',
1806
- { className: 'dsb-actions', style: { marginBottom: 10 } },
1807
- h(
1808
- 'button',
1809
- {
1810
- className: 'dsb-btn dsb-btn-sm',
1811
- disabled: !!busy || !listPicks.length,
1812
- onClick: () => changeMode('sync'),
1813
- },
1814
- busy === 'mode' ? '处理中…' : `设为同步(${listPicks.length})`,
1815
- ),
1816
- h(
1817
- 'button',
1818
- {
1819
- className: 'dsb-btn dsb-btn-sm',
1820
- disabled: !!busy || !listPicks.length,
1821
- onClick: () => changeMode('local'),
1822
- },
1823
- '设为本机独属',
1824
- ),
1825
- h(
1826
- 'button',
1827
- {
1828
- className: 'dsb-btn dsb-btn-sm',
1829
- disabled: !!busy || !listPicks.length,
1830
- onClick: () => changeMode('master'),
1831
- },
1832
- '设为本机不拉取',
1833
- ),
1739
+ { className: 'dsb-row-t', style: { marginBottom: 6 } },
1740
+ `待批草稿:${learningList.length} 份(批准才改本机技能,批准前自动备份)`,
1834
1741
  ),
1835
- h(
1836
- 'div',
1837
- { style: { maxHeight: 300, overflowY: 'auto' } },
1838
- skillList.items.map((it) => {
1839
- const modeLabel =
1840
- it.mode === 'sync' ? '同步' : it.mode === 'local' ? '本机独属' : '本机不拉取';
1841
- const tagCls =
1842
- it.mode === 'sync' ? 'dsb-tag-ok' : it.mode === 'local' ? '' : 'dsb-tag-warn';
1843
- const statusText =
1844
- it.status === 'same'
1845
- ? '一致'
1846
- : it.status === 'diff'
1847
- ? `内容不同${it.masterVersion && it.localVersion ? `(母版 v${it.masterVersion} / 本机 v${it.localVersion})` : ''}${
1848
- it.verdict === 'local'
1849
- ? ' · 本机新'
1850
- : it.verdict === 'master'
1851
- ? ' · 母版新'
1852
- : it.verdict === 'unknown'
1853
- ? ' · 待确认'
1854
- : ''
1855
- }`
1856
- : it.status === 'local-only'
1857
- ? '仅本机有'
1858
- : it.status === 'missing-local'
1859
- ? '仅母版有'
1860
- : '母版有·本机不拉取';
1861
- const statusHint =
1862
- it.status === 'diff' && it.hint
1863
- ? `${it.hint}${BASIS_LABEL[it.basis] ? ` · ${BASIS_LABEL[it.basis]}` : ''}`
1864
- : '';
1865
- return h(
1866
- 'div',
1867
- { className: 'dsb-ws', key: it.name },
1868
- h('input', {
1869
- type: 'checkbox',
1870
- className: 'dsb-chk',
1871
- checked: listPicks.includes(it.name),
1872
- onChange: () =>
1873
- setListPicks((cur) =>
1874
- cur.includes(it.name) ? cur.filter((x) => x !== it.name) : [...cur, it.name],
1875
- ),
1876
- }),
1742
+ learningList.length
1743
+ ? learningList.slice(0, 20).map((it) =>
1877
1744
  h(
1878
1745
  'div',
1879
- { className: 'dsb-ws-body' },
1746
+ { className: 'dsb-ws', key: `ln-${it.skill}` },
1880
1747
  h(
1881
1748
  'div',
1882
- { className: 'dsb-row-t' },
1883
- it.name,
1884
- h('span', { className: `dsb-tag ${tagCls}` }, modeLabel),
1885
- it.explicit ? h('span', { className: 'dsb-tag' }, '已设') : null,
1749
+ { className: 'dsb-ws-body' },
1750
+ h(
1751
+ 'div',
1752
+ { className: 'dsb-row-t' },
1753
+ it.skill,
1754
+ h('span', { className: 'dsb-tag dsb-tag-ok' }, it.label || it.action),
1755
+ it.basis ? h('span', { className: 'dsb-tag' }, it.basis) : null,
1756
+ it.confidence ? h('span', { className: 'dsb-tag' }, `置信 ${it.confidence}`) : null,
1757
+ it.peerFeature ? h('span', { className: 'dsb-tag' }, `来自 ${it.peerFeature}`) : null,
1758
+ ),
1759
+ h('div', { className: 'dsb-row-d' }, String(it.reason || '').slice(0, 300)),
1760
+ it.audit && it.audit.problems && it.audit.problems.length
1761
+ ? h('div', { className: 'dsb-row-d' }, `审核问题:${it.audit.problems.join(';')}`)
1762
+ : null,
1763
+ h(
1764
+ 'div',
1765
+ { className: 'dsb-actions', style: { marginTop: 6, gap: 6 } },
1766
+ h(
1767
+ 'button',
1768
+ { className: 'dsb-btn dsb-btn-sm', onClick: () => viewDraft(it.skill), disabled: !!busy },
1769
+ '看草稿',
1770
+ ),
1771
+ h(
1772
+ 'button',
1773
+ {
1774
+ className: 'dsb-btn dsb-btn-primary dsb-btn-sm',
1775
+ onClick: () => approveDraft(it.skill),
1776
+ disabled: !!busy,
1777
+ },
1778
+ busy === `approve:${it.skill}` ? '落地中…' : '批准落地',
1779
+ ),
1780
+ h(
1781
+ 'button',
1782
+ { className: 'dsb-btn dsb-btn-sm', onClick: () => rejectDraft(it.skill), disabled: !!busy },
1783
+ '驳回',
1784
+ ),
1785
+ ),
1886
1786
  ),
1887
- h('div', { className: 'dsb-row-d' }, statusText),
1888
- statusHint ? h('div', { className: 'dsb-row-d' }, statusHint) : null,
1889
1787
  ),
1890
- );
1891
- }),
1892
- ),
1788
+ )
1789
+ : h('div', { className: 'dsb-row-d' }, '没有待批草稿:点「优化本机技能」出一批'),
1790
+ draftPreview
1791
+ ? h(
1792
+ 'div',
1793
+ null,
1794
+ h(
1795
+ 'div',
1796
+ { className: 'dsb-actions', style: { marginTop: 8, marginBottom: 4 } },
1797
+ h('span', { className: 'dsb-row-t' }, `草稿预览:${draftPreview.name}`),
1798
+ h(
1799
+ 'button',
1800
+ { className: 'dsb-btn dsb-btn-sm', onClick: () => setDraftPreview(null) },
1801
+ '收起',
1802
+ ),
1803
+ ),
1804
+ h(
1805
+ 'pre',
1806
+ {
1807
+ className: 'dsb-hist',
1808
+ style: { maxHeight: 320, overflow: 'auto', whiteSpace: 'pre-wrap', fontSize: 11 },
1809
+ },
1810
+ draftPreview.text || '(空)',
1811
+ ),
1812
+ )
1813
+ : null,
1893
1814
  )
1894
1815
  : null,
1895
1816
  ),