dsh-paoding 0.4.5 → 0.4.7
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 +5 -4
- package/README_en.md +5 -4
- package/bin/dsh-paoding.mjs +22 -10
- package/docs/configuration.md +23 -0
- package/docs/configuration_en.md +23 -0
- package/docs/installation.md +23 -9
- package/docs/installation_en.md +20 -8
- package/package.json +1 -1
- package/plugins/paoding-config-ui/api-core.mjs +229 -8
- package/plugins/paoding-config-ui/index.mjs +81 -4
- package/plugins/paoding-config-ui/lib/base.css +253 -0
- package/plugins/paoding-config-ui/lib/client.js +510 -43
- package/plugins/paoding-config-ui/package.json +1 -1
- package/plugins/paoding-config-ui/stats-core.mjs +421 -0
- package/tools/lib/cli.mjs +298 -8
- package/tools/lib/config.mjs +140 -1
- package/tools/lib/doctor.mjs +82 -32
- package/tools/lib/lifecycle.mjs +333 -0
- package/tools/lib/preset-system.mjs +254 -38
- package/tools/lib/state.mjs +24 -9
|
@@ -93,6 +93,7 @@ window.__ModuleLoader__.load({
|
|
|
93
93
|
// ver.*(版本更新卡)/ models.* / tpl.*(场景模板)/ ws.*(工作区)/
|
|
94
94
|
// panel.*(配置面板正文)/ role.*(角色卡与委派行标题)/ warn.*(体检
|
|
95
95
|
// 告警卡)/ nav.*(侧栏入口与页头壳)/ menu.*(页头「更多」菜单与升级)/
|
|
96
|
+
// slim.*(工具面瘦身统计)/ diff.*(预览「本次变更」结构化差分)/
|
|
96
97
|
// common.*(跨片段复用的短词)。
|
|
97
98
|
// - t(key, params):按当前语言取文案,params 做 {name} 插值(split/join
|
|
98
99
|
// 而非 replace,避免替换串里的 $ 模式被二次解释)。缺 key 原样返回 key
|
|
@@ -455,6 +456,63 @@ window.__ModuleLoader__.load({
|
|
|
455
456
|
"menu.upgradedHot": { zh: "✅ 已升级到 {v},刷新页面即生效(preset 会自动按新版重生成)", en: "✅ Upgraded to {v} — refresh the page to apply (the preset regenerates itself against it)" },
|
|
456
457
|
"menu.errTail": { zh: ":{tail}", en: ": {tail}" },
|
|
457
458
|
"menu.langTip": { zh: "切换界面语言", en: "Switch UI language" },
|
|
459
|
+
|
|
460
|
+
// ── 工具筛选(角色卡工具网格超过 8 项时,网格上方的行内筛选框)─────────
|
|
461
|
+
"panel.toolFilter": { zh: "筛选工具…", en: "Filter tools…" },
|
|
462
|
+
|
|
463
|
+
// ── 工具面瘦身统计(state.slim / preview.slim 的一行统计)──────────────
|
|
464
|
+
"slim.line": { zh: "主 agent 工具面 {main}/{total} 个(瘦身 {pct}%)", en: "Main agent tool surface: {main}/{total} tools (down {pct}%)" },
|
|
465
|
+
// 预览弹窗里的第二口径小注:preview.slim 按本次提交(未保存)配置计算,
|
|
466
|
+
// 与页头按已保存配置的一行区分开。
|
|
467
|
+
"slim.pendingNote": { zh: "(按当前未保存配置)", en: " (per the current unsaved config)" },
|
|
468
|
+
|
|
469
|
+
// ── 委派效果卡(GET stats 的只读紧凑表,页头瘦身行之下)───────────────
|
|
470
|
+
"del.cardTitle": { zh: "委派效果", en: "Delegation stats" },
|
|
471
|
+
"del.colRole": { zh: "角色", en: "Role" },
|
|
472
|
+
"del.colCalls": { zh: "调用", en: "Calls" },
|
|
473
|
+
"del.colAvg": { zh: "均耗时", en: "Avg" },
|
|
474
|
+
"del.colFailed": { zh: "失败", en: "Failed" },
|
|
475
|
+
"del.colLastUsed": { zh: "最近使用", en: "Last used" },
|
|
476
|
+
"del.totalRoles": { zh: "共 {n} 个角色", en: "{n} roles in total" },
|
|
477
|
+
"del.empty": { zh: "暂无委派记录", en: "No delegation records yet" },
|
|
478
|
+
// 标题角标:响应 project='all'(缺省项目零记录,服务端回落全项目聚合)时点明统计口径
|
|
479
|
+
"del.scopeAll": { zh: "全部项目", en: "all projects" },
|
|
480
|
+
|
|
481
|
+
// ── 预览「本次变更」区块(preview.diff 结构化差分的渲染文案)───────────
|
|
482
|
+
// 服务端 diff 是纯结构零文案(tools/lib/config.mjs diffAssignments),
|
|
483
|
+
// 全部措辞在这里收口;mainAgent.modelChanged 恒 false,不设键不渲染。
|
|
484
|
+
"diff.heading": { zh: "本次变更", en: "Changes this time" },
|
|
485
|
+
"diff.unchanged": { zh: "与上次保存的配置一致", en: "Identical to the last saved config." },
|
|
486
|
+
"diff.mainToolsAdded": { zh: "主 agent 新带 {n} 个工具:{list}", en: "Main agent now carries {n} more tools: {list}" },
|
|
487
|
+
"diff.mainToolsRemoved": { zh: "主 agent 移除 {n} 个工具:{list}", en: "Main agent drops {n} tools: {list}" },
|
|
488
|
+
"diff.mainPersona": { zh: "主 agent 人设追加有变化", en: "Main agent persona addendum changed" },
|
|
489
|
+
"diff.mainSkills": { zh: "主 agent 技能分配有变化", en: "Main agent skill assignment changed" },
|
|
490
|
+
"diff.roleAdded": { zh: "新增角色「{name}」", en: "Role \"{name}\" added" },
|
|
491
|
+
"diff.roleRemoved": { zh: "移除角色「{name}」", en: "Role \"{name}\" removed" },
|
|
492
|
+
"diff.roleChanged": { zh: "角色「{name}」变更:{details}", en: "Role \"{name}\" changed: {details}" },
|
|
493
|
+
"diff.roleTools": { zh: "工具 +{added} -{removed}", en: "tools +{added} -{removed}" },
|
|
494
|
+
"diff.rolePersona": { zh: "persona 变化", en: "persona changed" },
|
|
495
|
+
"diff.roleModel": { zh: "专用模型变化", en: "dedicated model changed" },
|
|
496
|
+
"diff.roleStartMode": { zh: "启动方式变化", en: "start mode changed" },
|
|
497
|
+
"diff.roleBgMode": { zh: "后台续跑变化", en: "background session mode changed" },
|
|
498
|
+
"diff.rolesRemoveAdded": { zh: "移除内置角色 {n} 个:{list}", en: "Removed {n} built-in roles: {list}" },
|
|
499
|
+
"diff.rolesRemoveRemoved": { zh: "恢复内置角色 {n} 个:{list}", en: "Restored {n} built-in roles: {list}" },
|
|
500
|
+
// main_agent_extra 通道明细(主 agent 合并视图之外的通道拆分,一并列出)
|
|
501
|
+
"diff.mainExtraAdded": { zh: "主 agent 附加工具新增 {n} 个:{list}", en: "Main agent extra tools added ({n}): {list}" },
|
|
502
|
+
"diff.mainExtraRemoved": { zh: "主 agent 附加工具移除 {n} 个:{list}", en: "Main agent extra tools removed ({n}): {list}" },
|
|
503
|
+
|
|
504
|
+
// ── 恢复备份(页头「更多」菜单 → 备份列表弹层 → 二次确认 → POST restore)
|
|
505
|
+
"menu.restoreItem": { zh: "恢复备份…", en: "Restore backup…" },
|
|
506
|
+
"menu.backupTitle": { zh: "恢复备份", en: "Restore backup" },
|
|
507
|
+
"menu.backupLoading": { zh: "加载备份列表…", en: "Loading backups…" },
|
|
508
|
+
"menu.backupEmpty": { zh: "暂无备份", en: "No backups yet" },
|
|
509
|
+
"menu.backupBadResponse": { zh: "备份列表响应格式异常", en: "Unexpected backup list response" },
|
|
510
|
+
"menu.backupListFail": { zh: "备份列表拉取失败:{msg}", en: "Failed to load backups: {msg}" },
|
|
511
|
+
"menu.restoreTitle": { zh: "确认恢复此备份", en: "Restore this backup" },
|
|
512
|
+
"menu.restoreBody": { zh: "将用备份「{name}」覆盖当前配置,覆盖前当前配置会先自动备份。", en: "Backup \"{name}\" will overwrite the current config; the current config is backed up automatically first." },
|
|
513
|
+
"menu.restoreConfirm": { zh: "覆盖恢复", en: "Overwrite & restore" },
|
|
514
|
+
"menu.restored": { zh: "已恢复备份 {name},恢复前的配置已自动存为 bak-1", en: "Restored backup {name} — the previous config was saved as bak-1 automatically" },
|
|
515
|
+
"menu.restoreFail": { zh: "恢复失败:{msg}", en: "Restore failed: {msg}" },
|
|
458
516
|
};
|
|
459
517
|
// ── 样式注入(幂等)─────────────────────────────────────────────────
|
|
460
518
|
//
|
|
@@ -498,8 +556,10 @@ window.__ModuleLoader__.load({
|
|
|
498
556
|
|
|
499
557
|
// projBadge(可选):项目级技能角标文案(如「项目」),仅技能项在
|
|
500
558
|
// 工作区视角下、该技能来自工作区项目根时传入;其余网格不传不渲染。
|
|
559
|
+
// data-pd-tool:工具显示名打点——供 applyToolFilter 做筛选匹配(q 透传
|
|
560
|
+
// getAttribute,React 不感知),与 title 悬浮提示内容一致。
|
|
501
561
|
function toolItem(tool, tag, checked, disabled, onChange, key, projBadge) {
|
|
502
|
-
return h("label", { key: key, className: "omd-toolItem", title: tool },
|
|
562
|
+
return h("label", { key: key, className: "omd-toolItem", title: tool, "data-pd-tool": tool },
|
|
503
563
|
h("input", {
|
|
504
564
|
type: "checkbox",
|
|
505
565
|
className: "omd-check",
|
|
@@ -513,6 +573,135 @@ window.__ModuleLoader__.load({
|
|
|
513
573
|
projBadge ? h("span", { className: "omd-skillProj" }, projBadge) : null);
|
|
514
574
|
}
|
|
515
575
|
|
|
576
|
+
// ── 工具网格筛选(超过 8 项的分组在网格上方给一个行内筛选框)────────────
|
|
577
|
+
// 触发阈值:分组超过 8 项才出现筛选框(短网格不必多一个输入框)。
|
|
578
|
+
var TOOL_FILTER_MIN = 8;
|
|
579
|
+
|
|
580
|
+
// 工具网格(可选筛选框 + 网格本体):items 超过阈值时在外面包一层
|
|
581
|
+
// .omd-toolFilterWrap(输入框 + 网格),否则原样返回网格节点。不引入任何
|
|
582
|
+
// 状态管道——筛选 oninput 直接改 DOM 显隐(见 applyToolFilter),整卡重
|
|
583
|
+
// 渲染会吃掉输入框焦点,这个交互必须绕开 React。
|
|
584
|
+
// renderItem:单工具 → 节点(toolItem 调用点原样搬进来);emptyNode:
|
|
585
|
+
// 空列表占位(原「暂无 …」节点),空组照旧渲染、无筛选框。空占位作直接
|
|
586
|
+
// 子节点传入(不进数组),与改造前调用点的 children 形状一致,不触发
|
|
587
|
+
// React 的数组 key 警告。
|
|
588
|
+
function toolGridOf(tools, renderItem, emptyNode) {
|
|
589
|
+
var grid;
|
|
590
|
+
if (tools.length) {
|
|
591
|
+
grid = h.apply(null, ["div", { className: "omd-toolGrid" }].concat(tools.map(renderItem)));
|
|
592
|
+
} else {
|
|
593
|
+
grid = h("div", { className: "omd-toolGrid" }, emptyNode || null);
|
|
594
|
+
}
|
|
595
|
+
if (tools.length <= TOOL_FILTER_MIN) return grid;
|
|
596
|
+
return h("div", { className: "omd-toolFilterWrap" },
|
|
597
|
+
h("input", {
|
|
598
|
+
type: "text",
|
|
599
|
+
className: "omd-toolFilter",
|
|
600
|
+
placeholder: t("panel.toolFilter"),
|
|
601
|
+
spellCheck: false,
|
|
602
|
+
onInput: function (e) { applyToolFilter(e.target); },
|
|
603
|
+
}),
|
|
604
|
+
grid);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 筛选 oninput:从输入框向上取包裹层,按 data-pd-tool(工具显示名)做
|
|
608
|
+
// 大小写不敏感子串匹配,逐项切 display——纯 DOM 操作不触发重渲染,输入框
|
|
609
|
+
// 焦点不丢;清空输入恢复全部显示。
|
|
610
|
+
function applyToolFilter(input) {
|
|
611
|
+
var wrap = input && input.parentNode;
|
|
612
|
+
if (!wrap || !wrap.querySelectorAll) return;
|
|
613
|
+
var q = String(input.value || "").toLowerCase();
|
|
614
|
+
var items = wrap.querySelectorAll("label[data-pd-tool]");
|
|
615
|
+
for (var i = 0; i < items.length; i++) {
|
|
616
|
+
var name = items[i].getAttribute("data-pd-tool") || "";
|
|
617
|
+
items[i].style.display = q !== "" && name.toLowerCase().indexOf(q) === -1 ? "none" : "";
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ── 工具面瘦身统计(state.slim / preview.slim → 一行文案)───────────────
|
|
622
|
+
// 瘦身百分比:pct = total>0 ? round((1-main/total)*100) : 0(total=0 防除零)。
|
|
623
|
+
function slimPctOf(slim) {
|
|
624
|
+
var total = slim && typeof slim.total === "number" ? slim.total : 0;
|
|
625
|
+
var main = slim && typeof slim.main === "number" ? slim.main : 0;
|
|
626
|
+
return total > 0 ? Math.round((1 - main / total) * 100) : 0;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 一行统计文案(页头与预览弹窗共用同一拼法,防漂移)。
|
|
630
|
+
function slimLineText(slim) {
|
|
631
|
+
return t("slim.line", {
|
|
632
|
+
main: slim && typeof slim.main === "number" ? slim.main : 0,
|
|
633
|
+
total: slim && typeof slim.total === "number" ? slim.total : 0,
|
|
634
|
+
pct: slimPctOf(slim),
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ── 委派效果卡(GET stats → 页头瘦身行之下的只读紧凑表)────────────────
|
|
639
|
+
// 数据由 86 PageShell 首屏渲染完后懒加载(不阻塞开面板);服务缺失 / 拉取
|
|
640
|
+
// 失败 / 响应异常时整卡不渲染(delegationCardOf 返回 null),不打扰。
|
|
641
|
+
|
|
642
|
+
// 均耗时人话格式:1 分内「1.2s」(两位数以上取整秒),1 小时内「3m05s」,
|
|
643
|
+
// 更长「2h07m」。非正数一律 0s。
|
|
644
|
+
function delegDurationText(ms) {
|
|
645
|
+
if (!(ms > 0)) return "0s";
|
|
646
|
+
var s = ms / 1000;
|
|
647
|
+
if (s < 60) return (s >= 10 ? String(Math.round(s)) : s.toFixed(1)) + "s";
|
|
648
|
+
var m = Math.floor(s / 60);
|
|
649
|
+
if (m < 60) return m + "m" + String(Math.round(s % 60)).padStart(2, "0") + "s";
|
|
650
|
+
return Math.floor(m / 60) + "h" + String(m % 60).padStart(2, "0") + "m";
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 最近使用时间:当年「MM-DD HH:mm」,跨年「YYYY-MM-DD」——纯数字中性格式,
|
|
654
|
+
// 不进字典也不吃 i18n。
|
|
655
|
+
function delegWhenText(ms) {
|
|
656
|
+
var d = new Date(ms);
|
|
657
|
+
if (isNaN(d.getTime())) return "—";
|
|
658
|
+
var two = function (n) { return String(n).padStart(2, "0"); };
|
|
659
|
+
var now = new Date();
|
|
660
|
+
if (d.getFullYear() !== now.getFullYear()) {
|
|
661
|
+
return d.getFullYear() + "-" + two(d.getMonth() + 1) + "-" + two(d.getDate());
|
|
662
|
+
}
|
|
663
|
+
return two(d.getMonth() + 1) + "-" + two(d.getDate()) + " " + two(d.getHours()) + ":" + two(d.getMinutes());
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 卡片渲染:stats 缺失 / ok!==true / roles 非数组 → null(整卡零占位)。
|
|
667
|
+
// delegatedSessions=0 渲染标题 + 一行空文案;有数据渲染紧凑表(表头 +
|
|
668
|
+
// 最多 8 行,超出给「共 N 个角色」尾行),失败数 >0 标红提醒。
|
|
669
|
+
function delegationCardOf(stats) {
|
|
670
|
+
if (!stats || stats.ok !== true || !Array.isArray(stats.roles)) return null;
|
|
671
|
+
// project==='all' = 缺省项目零记录后回落的全项目聚合,标题尾加弱化角标
|
|
672
|
+
// 点明口径,免得误读成当前项目自己的数据(服务端见 stats-core.mjs 回落)。
|
|
673
|
+
var head = h("div", { className: "pd-delegTitle" }, t("del.cardTitle"),
|
|
674
|
+
stats.project === "all" ? h("span", { className: "pd-delegScope" }, t("del.scopeAll")) : null);
|
|
675
|
+
if (!stats.delegatedSessions) {
|
|
676
|
+
return h("div", { className: "pd-delegCard" }, head,
|
|
677
|
+
h("div", { className: "pd-delegEmpty" }, t("del.empty")));
|
|
678
|
+
}
|
|
679
|
+
var cols = [
|
|
680
|
+
["pd-delegRole", t("del.colRole")],
|
|
681
|
+
["pd-delegNum", t("del.colCalls")],
|
|
682
|
+
["pd-delegNum", t("del.colAvg")],
|
|
683
|
+
["pd-delegNum", t("del.colFailed")],
|
|
684
|
+
["pd-delegWhen", t("del.colLastUsed")],
|
|
685
|
+
];
|
|
686
|
+
var headerRow = h("div", { className: "pd-delegRow pd-delegHeaderRow" }, cols.map(function (c, i) {
|
|
687
|
+
return h("span", { key: "h" + i, className: c[0] }, c[1]);
|
|
688
|
+
}));
|
|
689
|
+
var shown = stats.roles.slice(0, 8);
|
|
690
|
+
var rows = shown.map(function (r) {
|
|
691
|
+
return h("div", { className: "pd-delegRow", key: r.role },
|
|
692
|
+
h("span", { className: "pd-delegRole", title: r.role }, r.role),
|
|
693
|
+
h("span", { className: "pd-delegNum" }, String(r.calls)),
|
|
694
|
+
h("span", { className: "pd-delegNum" }, delegDurationText(r.avgMs)),
|
|
695
|
+
h("span", { className: "pd-delegNum" + (r.failures > 0 ? " pd-delegFail" : "") }, String(r.failures)),
|
|
696
|
+
h("span", { className: "pd-delegWhen" }, delegWhenText(r.lastUsedAt)));
|
|
697
|
+
});
|
|
698
|
+
var tail = stats.roles.length > shown.length
|
|
699
|
+
? h("div", { className: "pd-delegMore" }, t("del.totalRoles", { n: stats.roles.length }))
|
|
700
|
+
: null;
|
|
701
|
+
return h("div", { className: "pd-delegCard" }, head,
|
|
702
|
+
h.apply(null, ["div", { className: "pd-delegTable" }, headerRow].concat(rows)), tail);
|
|
703
|
+
}
|
|
704
|
+
|
|
516
705
|
var DEFAULT_ROLES = ["search_external", "design", "implement"];
|
|
517
706
|
// 内置角色默认 persona 首行「身份主语」(仅用于 UI 占位与提示文案;实际生效值
|
|
518
707
|
// 由后端 SRC 模板 persona 首行决定)。「显示名」= 替换该主语(其余不动)。
|
|
@@ -1172,6 +1361,85 @@ window.__ModuleLoader__.load({
|
|
|
1172
1361
|
h("div", { className: "omd-section" }, rows));
|
|
1173
1362
|
}
|
|
1174
1363
|
|
|
1364
|
+
// ── 预览「本次变更」区块(preview.diff 结构化差分的渲染)─────────────────
|
|
1365
|
+
// 服务端 preview 响应的 diff 是纯结构零文案(tools/lib/config.mjs
|
|
1366
|
+
// diffAssignments),客户端在此按块渲染成可读行,全部措辞走 t() 字典:
|
|
1367
|
+
// - changed=false → 一行「与上次保存的配置一致」;
|
|
1368
|
+
// - changed=true → 主 agent(新带/移除工具 + persona/技能变化;modelChanged
|
|
1369
|
+
// 恒 false 不渲染)→ 各角色(新增/移除/变更,变更细分 +N -N 工具、
|
|
1370
|
+
// persona、专用模型、启动方式、后台续跑)→ roles_remove(删/恢复内置
|
|
1371
|
+
// 角色)→ main_extra(附加工具通道增删明细)逐块完整列出(预览区本就
|
|
1372
|
+
// 在整页滚动流里,行多不怕)。
|
|
1373
|
+
// apply 确认流程刻意不加本区块:预览才是「看一眼」的时刻,确认弹窗保持
|
|
1374
|
+
// 一屏说清。行按语义着色:净新增(绿)/ 净移除(红)/ 变更明细(中性)。
|
|
1375
|
+
function diffLineOf(text, tone) {
|
|
1376
|
+
return h("div", {
|
|
1377
|
+
className: "omd-diffLine" +
|
|
1378
|
+
(tone === "add" ? " omd-diffLineAdd" : tone === "del" ? " omd-diffLineDel" : ""),
|
|
1379
|
+
}, text);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// 工具/角色名列表串(按既有 listSep 口径连接);调用方先判空再取。
|
|
1383
|
+
function diffListStr(list) {
|
|
1384
|
+
return (list || []).join(t("common.listSep"));
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function diffBlockOf(diff) {
|
|
1388
|
+
if (!diff || typeof diff !== "object") return null; // 老服务端无 diff 字段:整块零占位
|
|
1389
|
+
var lines = [];
|
|
1390
|
+
if (diff.changed) {
|
|
1391
|
+
// 主 agent:extra 增删与 remove 增删已由服务端合并成净增减视图
|
|
1392
|
+
//(toolsAdded / toolsRemoved),直接按净变化渲染。
|
|
1393
|
+
var ma = diff.mainAgent || {};
|
|
1394
|
+
if (ma.toolsAdded && ma.toolsAdded.length) {
|
|
1395
|
+
lines.push(diffLineOf(t("diff.mainToolsAdded", { n: ma.toolsAdded.length, list: diffListStr(ma.toolsAdded) }), "add"));
|
|
1396
|
+
}
|
|
1397
|
+
if (ma.toolsRemoved && ma.toolsRemoved.length) {
|
|
1398
|
+
lines.push(diffLineOf(t("diff.mainToolsRemoved", { n: ma.toolsRemoved.length, list: diffListStr(ma.toolsRemoved) }), "del"));
|
|
1399
|
+
}
|
|
1400
|
+
if (ma.personaChanged) lines.push(diffLineOf(t("diff.mainPersona")));
|
|
1401
|
+
if (ma.skillsChanged) lines.push(diffLineOf(t("diff.mainSkills")));
|
|
1402
|
+
// 角色:新增 / 移除 / 变更(变更行细分到维度,逐维度本地化)。
|
|
1403
|
+
(diff.roles || []).forEach(function (r) {
|
|
1404
|
+
if (!r || !r.name) return;
|
|
1405
|
+
if (r.op === "added") { lines.push(diffLineOf(t("diff.roleAdded", { name: r.name }), "add")); return; }
|
|
1406
|
+
if (r.op === "removed") { lines.push(diffLineOf(t("diff.roleRemoved", { name: r.name }), "del")); return; }
|
|
1407
|
+
var details = [];
|
|
1408
|
+
if ((r.toolsAdded && r.toolsAdded.length) || (r.toolsRemoved && r.toolsRemoved.length)) {
|
|
1409
|
+
details.push(t("diff.roleTools", { added: (r.toolsAdded || []).length, removed: (r.toolsRemoved || []).length }));
|
|
1410
|
+
}
|
|
1411
|
+
if (r.personaChanged) details.push(t("diff.rolePersona"));
|
|
1412
|
+
if (r.modelChanged) details.push(t("diff.roleModel"));
|
|
1413
|
+
if (r.startModeChanged) details.push(t("diff.roleStartMode"));
|
|
1414
|
+
if (r.backgroundModeChanged) details.push(t("diff.roleBgMode"));
|
|
1415
|
+
if (details.length) {
|
|
1416
|
+
lines.push(diffLineOf(t("diff.roleChanged", { name: r.name, details: details.join(t("common.listSep")) })));
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
// roles_remove:新增 = 删除的内置角色(净移除);移除 = 恢复(净新增)。
|
|
1420
|
+
var rr = diff.rolesRemove || {};
|
|
1421
|
+
if (rr.added && rr.added.length) {
|
|
1422
|
+
lines.push(diffLineOf(t("diff.rolesRemoveAdded", { n: rr.added.length, list: diffListStr(rr.added) }), "del"));
|
|
1423
|
+
}
|
|
1424
|
+
if (rr.removed && rr.removed.length) {
|
|
1425
|
+
lines.push(diffLineOf(t("diff.rolesRemoveRemoved", { n: rr.removed.length, list: diffListStr(rr.removed) }), "add"));
|
|
1426
|
+
}
|
|
1427
|
+
// main_extra 通道明细(主 agent 合并视图之外的通道拆分,按任务要求一并列出)。
|
|
1428
|
+
var me = diff.mainExtra || {};
|
|
1429
|
+
if (me.added && me.added.length) {
|
|
1430
|
+
lines.push(diffLineOf(t("diff.mainExtraAdded", { n: me.added.length, list: diffListStr(me.added) }), "add"));
|
|
1431
|
+
}
|
|
1432
|
+
if (me.removed && me.removed.length) {
|
|
1433
|
+
lines.push(diffLineOf(t("diff.mainExtraRemoved", { n: me.removed.length, list: diffListStr(me.removed) }), "del"));
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
return h("div", { className: "omd-diffBlock" },
|
|
1437
|
+
h("div", { className: "omd-diffHeading" }, t("diff.heading")),
|
|
1438
|
+
diff.changed && lines.length
|
|
1439
|
+
? h("div", { className: "omd-diffLines" }, lines)
|
|
1440
|
+
: h("div", { className: "omd-diffNone" }, t("diff.unchanged")));
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1175
1443
|
// ── 主面板 ─────────────────────────────────────────────────────────────
|
|
1176
1444
|
|
|
1177
1445
|
function ConfigPanel(props) {
|
|
@@ -1199,6 +1467,11 @@ window.__ModuleLoader__.load({
|
|
|
1199
1467
|
// 状态挂在页头 PageShell(版本号展示在标题旁),经 props 下发:
|
|
1200
1468
|
// setVerInfo 供 loadVersion 回写页头(版本号与更新卡都在 PageShell)。
|
|
1201
1469
|
var setVerInfo = props && props.setVerInfo;
|
|
1470
|
+
// 工具面瘦身统计(state.slim = 按已保存配置算的 {total, main}):呈现区
|
|
1471
|
+
// 同样挂在页头 PageShell(副标题下一行,版本号/更新卡同区),经 props
|
|
1472
|
+
// 下发 setSlimInfo 供 load / doRescan / doApply 回写。null = 老服务端
|
|
1473
|
+
// 无该字段 / 尚未取到,页头一行零占位。
|
|
1474
|
+
var setSlimInfo = props && props.setSlimInfo;
|
|
1202
1475
|
// 导入配置后的整体刷新入口:ConfigPanel 把 load 注册给页头(PageShell 的
|
|
1203
1476
|
// 「更多 → 导入配置」成功后调用)。面板与页头分属两棵子树,props 只能
|
|
1204
1477
|
// 单向传「注册函数」,刷新动作反过来经 ref 回调触达。
|
|
@@ -1346,6 +1619,9 @@ window.__ModuleLoader__.load({
|
|
|
1346
1619
|
api("state").then(function (d) {
|
|
1347
1620
|
setData(d);
|
|
1348
1621
|
setAssign(buildTargetAssignments(d, activeWsRef.current));
|
|
1622
|
+
// 页头瘦身统计跟着 state 走(state.slim 按已保存的全局配置计算,与
|
|
1623
|
+
// 工作区视角无关);老服务端无 slim 字段时置 null 隐藏该行。
|
|
1624
|
+
if (typeof setSlimInfo === "function") setSlimInfo(d && d.slim ? d.slim : null);
|
|
1349
1625
|
}).catch(function (err) { setError(err.message); });
|
|
1350
1626
|
}, [api, buildTargetAssignments]);
|
|
1351
1627
|
|
|
@@ -1990,6 +2266,10 @@ window.__ModuleLoader__.load({
|
|
|
1990
2266
|
return w && (w.level === "error" || w.level === "warn");
|
|
1991
2267
|
}).length;
|
|
1992
2268
|
var warnTail = t("panel.warnTail", { n: warnItems.length });
|
|
2269
|
+
// 全局应用成功:页头瘦身统计改按本次提交配置(apply 响应自带
|
|
2270
|
+
// slim,与落盘内容同口径);工作区应用不动全局配置,下方 state
|
|
2271
|
+
// 回拉自带 slim,不在额外设值。
|
|
2272
|
+
if (!wsPath && r && r.slim && typeof setSlimInfo === "function") setSlimInfo(r.slim);
|
|
1993
2273
|
if (wsPath) {
|
|
1994
2274
|
// 工作区应用:点明生成的 preset id,提示去预设选择器选用(工作区
|
|
1995
2275
|
// 预设不会自动成为默认,须在新建会话时显式选择)。体检 bad 时
|
|
@@ -2003,6 +2283,7 @@ window.__ModuleLoader__.load({
|
|
|
2003
2283
|
api("state").then(function (d) {
|
|
2004
2284
|
setData(d);
|
|
2005
2285
|
setAssign(buildTargetAssignments(d, activeWsRef.current));
|
|
2286
|
+
if (typeof setSlimInfo === "function") setSlimInfo(d && d.slim ? d.slim : null);
|
|
2006
2287
|
}).catch(function () { /* 静默:落盘已成功,仅展示层暂旧 */ });
|
|
2007
2288
|
} else {
|
|
2008
2289
|
var gMsg = t("panel.appliedGlobal", { summary: badCount > 0 ? warnTail : summary })
|
|
@@ -2034,6 +2315,8 @@ window.__ModuleLoader__.load({
|
|
|
2034
2315
|
api("rescan", {}).then(function (d) {
|
|
2035
2316
|
setData(d);
|
|
2036
2317
|
setAssign(buildTargetAssignments(d, activeWsRef.current));
|
|
2318
|
+
// 重扫后的 state 自带瘦身统计,页头一行同步刷新(字段缺失则隐藏)。
|
|
2319
|
+
if (typeof setSlimInfo === "function") setSlimInfo(d && d.slim ? d.slim : null);
|
|
2037
2320
|
// 回调参数 d 即重扫后的新 state,直接报数字,补回原徽标条的信息量
|
|
2038
2321
|
toast.ok(t("panel.rescanned", { n: (d.inventory || []).length, m: (d.skills || []).length }));
|
|
2039
2322
|
}).catch(function (err) { toast.err(err.message); }).finally(function () {
|
|
@@ -2929,11 +3212,12 @@ window.__ModuleLoader__.load({
|
|
|
2929
3212
|
if (isMain) {
|
|
2930
3213
|
body.push(mainNameBlock());
|
|
2931
3214
|
body.push(h("div", { className: "omd-glabel" }, t("panel.baseToolsMain")));
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3215
|
+
// 三组工具网格都走 toolGridOf:超过 8 项的组自动带行内筛选框
|
|
3216
|
+
//(oninput 纯 DOM 切显隐,见 20 片段 toolGridOf / applyToolFilter)。
|
|
3217
|
+
body.push(toolGridOf(data.restrictBase || [], function (t) {
|
|
3218
|
+
return toolItem(t, "base", (a.main_agent_remove || []).indexOf(t) === -1, busy,
|
|
3219
|
+
function (e) { setMainRemove(t, e.target.checked); }, t);
|
|
3220
|
+
}));
|
|
2937
3221
|
// host 检测库存按角色卡同口径拆两组:mcp__ 前缀 → 「MCP 工具」,
|
|
2938
3222
|
// 其余(mnemon_* 等插件直注册工具)→ 「插件工具」。勾选逻辑不变,
|
|
2939
3223
|
// 都写入 main_agent_extra。
|
|
@@ -2941,21 +3225,15 @@ window.__ModuleLoader__.load({
|
|
|
2941
3225
|
var mainMcpTools = mainInv.filter(function (t) { return hostTool(t); });
|
|
2942
3226
|
var mainPluginTools = mainInv.filter(function (t) { return !hostTool(t); });
|
|
2943
3227
|
body.push(h("div", { className: "omd-glabel" }, t("panel.mcpToolsMain")));
|
|
2944
|
-
body.push(
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
function (e) { setMainExtra(t, e.target.checked); }, t);
|
|
2949
|
-
})
|
|
2950
|
-
: h("div", { className: "omd-empty" }, t("panel.noMcp"))));
|
|
3228
|
+
body.push(toolGridOf(mainMcpTools, function (t) {
|
|
3229
|
+
return toolItem(t, "mcp", a.main_agent_extra.indexOf(t) !== -1, busy,
|
|
3230
|
+
function (e) { setMainExtra(t, e.target.checked); }, t);
|
|
3231
|
+
}, h("div", { className: "omd-empty" }, t("panel.noMcp"))));
|
|
2951
3232
|
body.push(h("div", { className: "omd-glabel" }, t("panel.pluginToolsMain")));
|
|
2952
|
-
body.push(
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
function (e) { setMainExtra(t, e.target.checked); }, t);
|
|
2957
|
-
})
|
|
2958
|
-
: h("div", { className: "omd-empty" }, t("panel.noPlugin"))));
|
|
3233
|
+
body.push(toolGridOf(mainPluginTools, function (t) {
|
|
3234
|
+
return toolItem(t, "plugin", a.main_agent_extra.indexOf(t) !== -1, busy,
|
|
3235
|
+
function (e) { setMainExtra(t, e.target.checked); }, t);
|
|
3236
|
+
}, h("div", { className: "omd-empty" }, t("panel.noPlugin"))));
|
|
2959
3237
|
body.push(h("div", { className: "omd-glabel" }, t("panel.skillsMain")));
|
|
2960
3238
|
body.push(gridSkills.length
|
|
2961
3239
|
? h("div", null,
|
|
@@ -2997,30 +3275,22 @@ window.__ModuleLoader__.load({
|
|
|
2997
3275
|
var pluginTools = candidates.filter(function (t) { return !hostTool(t) && inv.indexOf(t) !== -1; });
|
|
2998
3276
|
var baseTools = candidates.filter(function (t) { return !hostTool(t) && inv.indexOf(t) === -1; });
|
|
2999
3277
|
var checked = a.roles[name].tools;
|
|
3278
|
+
// 三组网格同走 toolGridOf(超 8 项自动带筛选框,与主 agent 卡同款)。
|
|
3000
3279
|
body.push(h("div", { className: "omd-glabel" }, t("panel.baseTools")));
|
|
3001
|
-
body.push(
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
3006
|
-
})
|
|
3007
|
-
: h("div", { className: "omd-empty" }, t("panel.none"))));
|
|
3280
|
+
body.push(toolGridOf(baseTools, function (t) {
|
|
3281
|
+
return toolItem(t, "base", checked.indexOf(t) !== -1, busy,
|
|
3282
|
+
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
3283
|
+
}, h("div", { className: "omd-empty" }, t("panel.none"))));
|
|
3008
3284
|
body.push(h("div", { className: "omd-glabel" }, t("panel.mcpTools")));
|
|
3009
|
-
body.push(
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
3014
|
-
})
|
|
3015
|
-
: h("div", { className: "omd-empty" }, t("panel.noMcp"))));
|
|
3285
|
+
body.push(toolGridOf(mcpTools, function (t) {
|
|
3286
|
+
return toolItem(t, "mcp", checked.indexOf(t) !== -1, busy,
|
|
3287
|
+
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
3288
|
+
}, h("div", { className: "omd-empty" }, t("panel.noMcp"))));
|
|
3016
3289
|
body.push(h("div", { className: "omd-glabel" }, t("panel.pluginTools")));
|
|
3017
|
-
body.push(
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
3022
|
-
})
|
|
3023
|
-
: h("div", { className: "omd-empty" }, t("panel.noPlugin"))));
|
|
3290
|
+
body.push(toolGridOf(pluginTools, function (t) {
|
|
3291
|
+
return toolItem(t, "plugin", checked.indexOf(t) !== -1, busy,
|
|
3292
|
+
function (e) { setRoleTools(name, t, e.target.checked); }, t);
|
|
3293
|
+
}, h("div", { className: "omd-empty" }, t("panel.noPlugin"))));
|
|
3024
3294
|
body.push(personaBlock(name));
|
|
3025
3295
|
}
|
|
3026
3296
|
|
|
@@ -3203,6 +3473,14 @@ window.__ModuleLoader__.load({
|
|
|
3203
3473
|
(preview.roleResults || []).map(function (r) {
|
|
3204
3474
|
return " · " + r.role + ": " + r.intent.length + " → " + r.kept.length;
|
|
3205
3475
|
}).join("")),
|
|
3476
|
+
// 「本次变更」区块:服务端 diff 到场才渲染(老服务端零占位);
|
|
3477
|
+
// unchanged 一行、changed 完整列出(见 70 片段 diffBlockOf)。
|
|
3478
|
+
diffBlockOf(preview.diff),
|
|
3479
|
+
// 瘦身统计(按本次提交配置算的预览口径):与页头「按已保存配置」的
|
|
3480
|
+
// 一行区分,随行标注「按当前未保存配置」。apply 确认流程不加区块。
|
|
3481
|
+
preview.slim ? h("div", { className: "omd-slimLine" },
|
|
3482
|
+
slimLineText(preview.slim),
|
|
3483
|
+
h("span", { className: "omd-slimNote" }, t("slim.pendingNote"))) : null,
|
|
3206
3484
|
h("pre", { className: "omd-pre" }, preview.text)) : null,
|
|
3207
3485
|
|
|
3208
3486
|
// 悬浮操作坞(sticky bottom:按钮常驻滚动容器底部,改完配置无需滚回顶部再操作)
|
|
@@ -3729,6 +4007,41 @@ window.__ModuleLoader__.load({
|
|
|
3729
4007
|
});
|
|
3730
4008
|
}
|
|
3731
4009
|
|
|
4010
|
+
// ── 恢复备份(页头「更多」菜单 → 备份列表弹层 → 二次确认 → POST restore)──
|
|
4011
|
+
// 服务端契约(api-core.mjs / index.mjs 已落地):
|
|
4012
|
+
// GET config/backups → { ok, backups: [{file, mtimeMs, size}] }(新→旧,可空)
|
|
4013
|
+
// POST config/restore { file } → { ok };422 文件名非法/备份坏、404 不存在、
|
|
4014
|
+
// 409 有 apply 在跑({ error } 信封,客户端按既有错误 toast 惯例呈现)。
|
|
4015
|
+
// 行三段:bak-N 徽标(从文件名 .bak-N 尾缀解析,解析不出原样显示文件名)+
|
|
4016
|
+
// 本地时间(mtimeMs 按 UI 语言格式化)+ 大小(B/KB/MB 三档)。
|
|
4017
|
+
|
|
4018
|
+
// 备份徽标:file 恒为 basename(服务端保证),形如
|
|
4019
|
+
// 「<配置文件名>.bak-1」→ 「bak-1」;形状不对原样透出(不猜)。
|
|
4020
|
+
function backupTagOf(file) {
|
|
4021
|
+
var m = String(file || "").match(/\.bak-(\d+)$/);
|
|
4022
|
+
return m ? "bak-" + m[1] : String(file || "");
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
// 本地时间:toLocaleString 按当前 UI 语言取 locale(zh → zh-CN / en → en-US
|
|
4026
|
+
// ——Electron 外壳 navigator 语言被钉成应用 locale,不可作格式化依据),
|
|
4027
|
+
// 24 小时制免「上午/下午」占宽;Intl 缺失的老内核逐层回落实现缺省格式。
|
|
4028
|
+
function backupTimeText(ms) {
|
|
4029
|
+
var d = new Date(typeof ms === "number" ? ms : 0);
|
|
4030
|
+
try {
|
|
4031
|
+
return d.toLocaleString(pdLang() === "en" ? "en-US" : "zh-CN", { hour12: false });
|
|
4032
|
+
} catch (err) {
|
|
4033
|
+
try { return d.toLocaleString(); } catch (err2) { return String(d); }
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
// 大小:B / KB / MB 三档(配置文件量级远用不上 GB),KB 以上留一位小数。
|
|
4038
|
+
function backupSizeText(bytes) {
|
|
4039
|
+
var n = typeof bytes === "number" && isFinite(bytes) && bytes >= 0 ? bytes : 0;
|
|
4040
|
+
if (n < 1024) return n + " B";
|
|
4041
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
|
4042
|
+
return (n / (1024 * 1024)).toFixed(1) + " MB";
|
|
4043
|
+
}
|
|
4044
|
+
|
|
3732
4045
|
// 确认弹窗层:订阅 70 片段的 confirmCtrl 单例(照 makeFooterEntry 的
|
|
3733
4046
|
// useState + subscribe 写法),渲染 confirmModalOf。挂在 PageShell 根数组
|
|
3734
4047
|
// 末尾——fixed 全屏遮罩覆盖整页,面板(删除角色 / 保存并应用)与页头
|
|
@@ -3772,6 +4085,22 @@ window.__ModuleLoader__.load({
|
|
|
3772
4085
|
// 版本检测重试进行中(页头失败行的「重试」按钮禁用态):与升级三态分开,
|
|
3773
4086
|
// 检测失败重试不该锁升级(升级预检在服务端另做一次,不依赖这份 verInfo)。
|
|
3774
4087
|
var _vb = useState(false), verRetryBusy = _vb[0], setVerRetryBusy = _vb[1];
|
|
4088
|
+
// 工具面瘦身统计(state.slim = 按已保存配置算的 {total, main},ConfigPanel
|
|
4089
|
+
// 的 load / doRescan / doApply 经 props.setSlimInfo 回写):副标题下一行,
|
|
4090
|
+
// 版本号 / 更新卡同区呈现。null = 未取到 / 老服务端无字段,整行零占位。
|
|
4091
|
+
var _sl = useState(null), slimInfo = _sl[0], setSlimInfo = _sl[1];
|
|
4092
|
+
// 委派效果统计(GET stats,页头瘦身行之下):懒加载——本 effect 在首屏
|
|
4093
|
+
// 渲染完成后才发请求,不阻塞开面板(state 全量检测照旧先行)。只读展示
|
|
4094
|
+
// 拉一次不重试;服务缺失 / 拉取失败 / 响应异常一律维持 null 整卡不渲染,
|
|
4095
|
+
// 不为锦上添花的统计打扰使用。卸载后丢弃在途响应防置迟数据。
|
|
4096
|
+
var _st = useState(null), delStats = _st[0], setDelStats = _st[1];
|
|
4097
|
+
useEffect(function () {
|
|
4098
|
+
var alive = true;
|
|
4099
|
+
paodingApi("stats").then(function (d) {
|
|
4100
|
+
if (alive && d && d.ok === true) setDelStats(d);
|
|
4101
|
+
}).catch(function () { /* 静默降级:整卡不渲染 */ });
|
|
4102
|
+
return function () { alive = false; };
|
|
4103
|
+
}, []);
|
|
3775
4104
|
// 语言切换 tick:页头文案(标题 / 副标题 / 菜单 / 语言项)走 t(),切换后
|
|
3776
4105
|
// 需整体重渲染一次;ConfigPanel 内部另有 registerReload 整页重拉。
|
|
3777
4106
|
var _lg = useState(0), langTick = _lg[0], setLangTick = _lg[1];
|
|
@@ -3862,6 +4191,127 @@ window.__ModuleLoader__.load({
|
|
|
3862
4191
|
});
|
|
3863
4192
|
}
|
|
3864
4193
|
|
|
4194
|
+
// ── 恢复备份:备份列表弹层 ────────────────────────────────────────────
|
|
4195
|
+
// 打开即拉 GET config/backups(服务端已按新→旧排好,客户端不再排序);
|
|
4196
|
+
// 行点击走 confirmCtrl 二次确认(覆盖前当前配置会先自动备份,danger 红钮
|
|
4197
|
+
// 与导入同款门槛),确认后 POST config/restore——成功 toast + 面板既有
|
|
4198
|
+
// 刷新路径(reloadRef 重拉 state 重建 assign,预览 / 提示卡一并作废,
|
|
4199
|
+
// 委派行显示名跟着重注册);409 / 422 / 404 按既有错误 toast 惯例呈现
|
|
4200
|
+
//(本地前缀 + 服务端 message 原文),弹层留着方便换一个备份再试。
|
|
4201
|
+
var _bko = useState(false), bkOpen = _bko[0], setBkOpen = _bko[1];
|
|
4202
|
+
var _bkl = useState([]), bkList = _bkl[0], setBkList = _bkl[1];
|
|
4203
|
+
// bkBusy 双语义:列表拉取中(弹层显示加载行)/ 恢复请求在途(行全禁用,
|
|
4204
|
+
// 防双击重复回滚)。
|
|
4205
|
+
var _bkb = useState(false), bkBusy = _bkb[0], setBkBusy = _bkb[1];
|
|
4206
|
+
var _bke = useState(""), bkErr = _bke[0], setBkErr = _bke[1];
|
|
4207
|
+
|
|
4208
|
+
function openBackupModal() {
|
|
4209
|
+
setMoreOpen(false);
|
|
4210
|
+
setBkOpen(true);
|
|
4211
|
+
setBkList([]);
|
|
4212
|
+
setBkErr("");
|
|
4213
|
+
setBkBusy(true);
|
|
4214
|
+
paodingApi("config/backups").then(function (d) {
|
|
4215
|
+
if (!d || d.ok !== true || !Array.isArray(d.backups)) throw new Error(t("menu.backupBadResponse"));
|
|
4216
|
+
setBkList(d.backups);
|
|
4217
|
+
}).catch(function (err) {
|
|
4218
|
+
setBkErr(t("menu.backupListFail", { msg: err.message }));
|
|
4219
|
+
}).finally(function () {
|
|
4220
|
+
setBkBusy(false);
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
// 关弹层:恢复请求在途时不收(请求自见分晓,中途关层容易误以为没生效)。
|
|
4225
|
+
function closeBackupModal() {
|
|
4226
|
+
if (bkBusy) return;
|
|
4227
|
+
setBkOpen(false);
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
function pickBackup(item) {
|
|
4231
|
+
if (!item || typeof item.file !== "string" || item.file === "" || bkBusy) return;
|
|
4232
|
+
confirmCtrl.ask({
|
|
4233
|
+
title: t("menu.restoreTitle"),
|
|
4234
|
+
body: t("menu.restoreBody", { name: backupTagOf(item.file) }),
|
|
4235
|
+
danger: true,
|
|
4236
|
+
confirmText: t("menu.restoreConfirm"),
|
|
4237
|
+
onConfirm: function () {
|
|
4238
|
+
setBkBusy(true);
|
|
4239
|
+
paodingApi("config/restore", { file: item.file }).then(function () {
|
|
4240
|
+
toast.ok(t("menu.restored", { name: backupTagOf(item.file) }));
|
|
4241
|
+
setBkOpen(false);
|
|
4242
|
+
// 面板既有刷新路径(与「导入配置」同款):重拉 state 重建 assign;
|
|
4243
|
+
// 显示名 / 角色名单可能被回滚改变,事件流委派行键控注册跟着刷新。
|
|
4244
|
+
if (typeof reloadRef.current === "function") reloadRef.current();
|
|
4245
|
+
refreshDelegateRows();
|
|
4246
|
+
}).catch(function (err) {
|
|
4247
|
+
toast.err(t("menu.restoreFail", { msg: err.message }));
|
|
4248
|
+
}).finally(function () {
|
|
4249
|
+
setBkBusy(false);
|
|
4250
|
+
});
|
|
4251
|
+
},
|
|
4252
|
+
});
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
// 备份弹层开着时的 Esc 分层(捕获级,与 ConfirmLayer 同款思路):只收
|
|
4256
|
+
// 弹层本身;二次确认框开着时放行——ConfirmLayer 的监听(注册在前)先
|
|
4257
|
+
// stopPropagation 并只关确认框,这里不重复收弹层。
|
|
4258
|
+
useEffect(function () {
|
|
4259
|
+
if (!bkOpen) return;
|
|
4260
|
+
var onKey = function (e) {
|
|
4261
|
+
if (!e || e.key !== "Escape") return;
|
|
4262
|
+
if (confirmCtrl.getSnapshot().req) return; // 确认框优先:让它自己的监听去收
|
|
4263
|
+
e.stopPropagation();
|
|
4264
|
+
closeBackupModal();
|
|
4265
|
+
};
|
|
4266
|
+
document.addEventListener("keydown", onKey, true);
|
|
4267
|
+
return function () { document.removeEventListener("keydown", onKey, true); };
|
|
4268
|
+
}, [bkOpen, bkBusy]);
|
|
4269
|
+
|
|
4270
|
+
// 备份列表弹层(结构对齐 confirmModalOf 的 pd-modal* 家族):整行可点
|
|
4271
|
+
// 按钮列表(bak 徽标 + 本地时间 + 大小,服务端已按新→旧排好);拉取中
|
|
4272
|
+
// 一行加载文案、空列表「暂无备份」、失败一行原因(不弹 toast,弹层里
|
|
4273
|
+
// 就地交代)。body 高度封顶出滚,备份链再长也不顶破视口。
|
|
4274
|
+
function backupModalOf() {
|
|
4275
|
+
if (!bkOpen) return null;
|
|
4276
|
+
var rows = bkList.map(function (it, i) {
|
|
4277
|
+
return h("button", {
|
|
4278
|
+
type: "button",
|
|
4279
|
+
key: it && typeof it.file === "string" && it.file ? it.file : "bak-" + i,
|
|
4280
|
+
className: "pd-backupItem",
|
|
4281
|
+
disabled: bkBusy,
|
|
4282
|
+
title: it && typeof it.file === "string" ? it.file : undefined,
|
|
4283
|
+
onClick: function () { pickBackup(it); },
|
|
4284
|
+
},
|
|
4285
|
+
h("span", { className: "pd-backupTag" }, backupTagOf(it && it.file)),
|
|
4286
|
+
h("span", { className: "pd-backupTime" }, backupTimeText(it && it.mtimeMs)),
|
|
4287
|
+
h("span", { className: "pd-backupSize" }, backupSizeText(it && it.size)));
|
|
4288
|
+
});
|
|
4289
|
+
return h("div", {
|
|
4290
|
+
className: "pd-modalMask",
|
|
4291
|
+
// 只点遮罩空白处才收(与 confirmModalOf 同款 mousedown 判据)
|
|
4292
|
+
onMouseDown: function (e) { if (e.target === e.currentTarget) closeBackupModal(); },
|
|
4293
|
+
},
|
|
4294
|
+
h("div", { className: "pd-modalCard", role: "dialog", "aria-modal": "true", "aria-label": t("menu.backupTitle") },
|
|
4295
|
+
h("div", { className: "pd-modalHeader" },
|
|
4296
|
+
h("h2", { className: "pd-modalTitle" }, t("menu.backupTitle")),
|
|
4297
|
+
h("button", {
|
|
4298
|
+
type: "button", className: "pd-modalClose", "aria-label": t("common.close"),
|
|
4299
|
+
onClick: closeBackupModal,
|
|
4300
|
+
}, h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
|
|
4301
|
+
h("div", { className: "pd-modalBody" },
|
|
4302
|
+
bkErr
|
|
4303
|
+
? h("div", { className: "omd-status omd-statusError" },
|
|
4304
|
+
h("span", { className: "omd-statusIcon" }, "⛔"),
|
|
4305
|
+
h("span", { className: "omd-statusText" }, bkErr))
|
|
4306
|
+
: bkBusy && !bkList.length
|
|
4307
|
+
? h("div", { className: "pd-backupEmpty" }, t("menu.backupLoading"))
|
|
4308
|
+
: bkList.length
|
|
4309
|
+
? h("div", { className: "pd-backupList" }, rows)
|
|
4310
|
+
: h("div", { className: "pd-backupEmpty" }, t("menu.backupEmpty"))),
|
|
4311
|
+
h("div", { className: "pd-modalFooter" },
|
|
4312
|
+
h(P.Button, { variant: "outline", size: "md", disabled: bkBusy, onClick: closeBackupModal }, t("common.close")))));
|
|
4313
|
+
}
|
|
4314
|
+
|
|
3865
4315
|
// 菜单开着时 Esc 只收菜单(捕获级先于 PageShell 的关页监听,与
|
|
3866
4316
|
// ConfigPanel 弹窗的 Esc 分层同款);点菜单外空白处收菜单。
|
|
3867
4317
|
useEffect(function () {
|
|
@@ -3924,7 +4374,13 @@ window.__ModuleLoader__.load({
|
|
|
3924
4374
|
: null),
|
|
3925
4375
|
versionRowOf(verInfo, { busy: upgBusy, onUpgrade: doUpgrade, note: upgNote, error: upgErr, onRetry: doRetryVersion, retryBusy: verRetryBusy })),
|
|
3926
4376
|
h("p", { className: "pd-pageSubtitle" },
|
|
3927
|
-
t("menu.subtitle"))
|
|
4377
|
+
t("menu.subtitle")),
|
|
4378
|
+
// 工具面瘦身统计一行:按已保存的全局配置(state.slim,见 setSlimInfo
|
|
4379
|
+
// 回写点);数据未就绪 / 老服务端无该字段时零占位。
|
|
4380
|
+
slimInfo ? h("p", { className: "pd-slimLine" }, slimLineText(slimInfo)) : null,
|
|
4381
|
+
// 委派效果卡:紧贴瘦身行之下(delegationCardOf 自判,统计没拉到 /
|
|
4382
|
+
// 服务缺失时返回 null 整卡不渲染)。
|
|
4383
|
+
delegationCardOf(delStats)),
|
|
3928
4384
|
// 页头「更多」菜单:整份配置文件的导出 / 导入(低频操作收进来,页头
|
|
3929
4385
|
// 不添第二排按钮)。定位锚点用相对定位的薄包裹层,下拉复用工作区
|
|
3930
4386
|
// 「更多」的 omd-tabMenu / omd-tabMenuItem 既有样式;靠右对齐(inline
|
|
@@ -3956,6 +4412,13 @@ window.__ModuleLoader__.load({
|
|
|
3956
4412
|
disabled: expBusy || impBusy,
|
|
3957
4413
|
onClick: pickImportFile,
|
|
3958
4414
|
}, h("span", { className: "omd-tabText" }, impBusy ? t("menu.importing") : t("menu.importItem"))),
|
|
4415
|
+
// 恢复备份:弹备份列表(行点选 → confirmCtrl 二次确认 → POST
|
|
4416
|
+
// restore,见 openBackupModal / pickBackup / backupModalOf)。
|
|
4417
|
+
h("button", {
|
|
4418
|
+
type: "button", role: "menuitem", className: "omd-tabMenuItem",
|
|
4419
|
+
disabled: expBusy || impBusy,
|
|
4420
|
+
onClick: openBackupModal,
|
|
4421
|
+
}, h("span", { className: "omd-tabText" }, t("menu.restoreItem"))),
|
|
3959
4422
|
// 语言切换项:label 刻意不进字典——按当前语言显示「另一端」的叫法,
|
|
3960
4423
|
// 中文界面显示 English,英文界面显示中文(语言名用母语写才认得出)。
|
|
3961
4424
|
h("button", {
|
|
@@ -3983,7 +4446,11 @@ window.__ModuleLoader__.load({
|
|
|
3983
4446
|
title: t("nav.closeTip"),
|
|
3984
4447
|
onClick: function () { if (typeof onClose === "function") onClose(); },
|
|
3985
4448
|
}, h("span", { className: "pd-closeGlyph", "aria-hidden": "true" }, "×"))),
|
|
3986
|
-
h(ConfigPanel, { setVerInfo: setVerInfo, registerReload: registerReload }),
|
|
4449
|
+
h(ConfigPanel, { setVerInfo: setVerInfo, setSlimInfo: setSlimInfo, registerReload: registerReload }),
|
|
4450
|
+
// 恢复备份弹层:fixed 遮罩不占布局流;置于 ConfirmLayer 之前——两者
|
|
4451
|
+
// 遮罩同为 z-60,按 DOM 顺序叠放,行点选后的二次确认框必须盖在列表
|
|
4452
|
+
// 弹层之上。
|
|
4453
|
+
backupModalOf(),
|
|
3987
4454
|
// 反馈 toast 宿主:订阅模块级 toast 控制器,悬浮于操作坞上方
|
|
3988
4455
|
h(ToastHost),
|
|
3989
4456
|
// 通用确认弹窗层:订阅 confirmCtrl(发起方见各 confirmCtrl.ask 调用点),
|