dsh-chef 0.3.36 → 0.3.38

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/dist/client.js CHANGED
@@ -28,17 +28,7 @@ window.__ModuleLoader__.load({
28
28
  //#endregion
29
29
  let react = require("react");
30
30
  react = __toESM(react, 1);
31
- //#region ../../node_modules/.pnpm/dsh-life-pack@0.3.34/node_modules/dsh-life-pack/dist/config-panel-contract.js
32
- /** 共用配置面板 · 契约:行表形状、配置面回执、几条两侧共用的常量。
33
- *
34
- * 为什么住这里:六个单品插件的设置页各有一份逐字相同的那套代码(卡片骨架/行渲染/样式表/
35
- * 状态机/配置面三通电话约 3105 行)。本件是抽件之后的**唯一一处定义地**。
36
- *
37
- * 本件不认识任何一家能力:行表由调用方给,分支只能由行表数据与配置面回执决定,
38
- * 里面不出现任何一个能力的名字(结构纪律「依赖方向」)。
39
- *
40
- * 对外只经 `config-panel-api.ts` 那一道门;本件自己不出门。
41
- */
31
+ //#region ../plugin-manager/src/config-panel-contract.ts
42
32
  /** 面板对配置面的三通电话:读整面/保存一份取值/重置为默认。三个名字各家逐字相同,收在这里一处。 */
43
33
  const RPC_ENDPOINT_CONFIG_GET = "config.get";
44
34
  const RPC_ENDPOINT_CONFIG_SAVE = "config.save";
@@ -51,6 +41,14 @@ window.__ModuleLoader__.load({
51
41
  const ADVANCED_GROUP_TITLE = "高级";
52
42
  /** 请求超时毫秒:与宿主侧 SPAWN_TIMEOUT_MS 同级,界面永不无限转圈。 */
53
43
  const READ_TIMEOUT_MS = 2e4;
44
+ /** 快路径首帧有界重试表(#1142,照 #982 更新目标表同值:瞬时未就绪不再整面失败)。
45
+ *
46
+ * 有界、调用方负责卸载清理(见 `config-panel.ts` 的取数),不是无界轮询;仅读整面用,保存/重置永不自动重试。 */
47
+ const CONFIG_READ_RETRY_MS = [
48
+ 1e3,
49
+ 3e3,
50
+ 8e3
51
+ ];
54
52
  /** 复制那一枚按钮就地变「已复制」之后,多久变回「复制」(定稿 v3 第三条:1.5 秒,不弹提示)。 */
55
53
  const COPY_FEEDBACK_MS = 1500;
56
54
  /** 宿主给「这是哪一种目录选择能力」时用的那两格原语的名字(软依赖:按名字现取,缺席即没有入口)。 */
@@ -63,7 +61,7 @@ window.__ModuleLoader__.load({
63
61
  return raw.ok === true || raw.ok === false;
64
62
  }
65
63
  //#endregion
66
- //#region ../../node_modules/.pnpm/dsh-life-pack@0.3.34/node_modules/dsh-life-pack/dist/config-panel-value.js
64
+ //#region ../plugin-manager/src/config-panel-value.ts
67
65
  /** 共用配置面板 · 取值与填值半:一份配置取值 ↔ 一张表单态,外加脏基线、跟随键、人话报错与三通电话。
68
66
  *
69
67
  * 本件**不认识 React**,也不认识宿主:喂一份配置面回执与一张行表,它把「页面上每一行该显示什么」
@@ -189,11 +187,20 @@ window.__ModuleLoader__.load({
189
187
  });
190
188
  return Promise.race([promise, limit]);
191
189
  }
192
- /** 一次配置电话(走调用方给的那条通道,靠端点名分发;永不抛,失败落字)。 */
193
- async function configRpc(call, channel, method, payload) {
190
+ /** 有界等待的 sleep(settle 即清 timer;调用方负责卸载清理)。 */
191
+ function retrySleep(ms) {
192
+ return new Promise((resolve) => {
193
+ const timer = setTimeout(() => {
194
+ clearTimeout(timer);
195
+ resolve();
196
+ }, ms);
197
+ });
198
+ }
199
+ /** 一次配置电话(走调用方给的那条通道,靠端点名分发;永不抛,失败落字,文案自带通道与下一步)。 */
200
+ async function configRpcOnce(call, channel, method, payload) {
194
201
  if (typeof call !== "function") return {
195
202
  ok: false,
196
- message: "宿主连接缺席:connection.rpc.call 不可用"
203
+ message: `宿主连接缺席:connection.rpc.call 不可用(通道 ${channel}),稍后重试`
197
204
  };
198
205
  try {
199
206
  const raw = await withDeadline(call("/api", channel.replace(/^\//, ""), {
@@ -202,7 +209,7 @@ window.__ModuleLoader__.load({
202
209
  }, AbortSignal.timeout(READ_TIMEOUT_MS)), READ_TIMEOUT_MS);
203
210
  if (!isRpcResult(raw)) return {
204
211
  ok: false,
205
- message: "回执信封异常(非 ok 信封)"
212
+ message: `回执信封异常(通道 ${channel}),点重试重新读取`
206
213
  };
207
214
  if (!raw.ok) return {
208
215
  ok: false,
@@ -215,42 +222,39 @@ window.__ModuleLoader__.load({
215
222
  } catch (e) {
216
223
  if (e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError")) return {
217
224
  ok: false,
218
- message: `配置请求超时(${Math.round(READ_TIMEOUT_MS / 1e3)}s):宿主未回`
225
+ message: `配置请求超时(${Math.round(READ_TIMEOUT_MS / 1e3)}s,通道 ${channel}):宿主未回,点重试重新读取`
219
226
  };
220
227
  return {
221
228
  ok: false,
222
- message: `配置失败:${e instanceof Error ? e.message : String(e)}`
229
+ message: `配置失败(通道 ${channel}):${e instanceof Error ? e.message : String(e)},点重试重新读取`
223
230
  };
224
231
  }
225
232
  }
226
- /** 读设置页整面。 */
227
- function fetchConfigSurface(call, channel) {
228
- return configRpc(call, channel, RPC_ENDPOINT_CONFIG_GET, {});
233
+ /** 读设置页整面(缺省单次;首帧按退避表传 `delays` 即自动重试)。 */
234
+ async function fetchConfigSurface(call, channel, retry) {
235
+ const delays = retry?.delays ?? [];
236
+ const sleepFn = retry?.sleep ?? retrySleep;
237
+ const alive = retry?.isAlive ?? (() => true);
238
+ let last = await configRpcOnce(call, channel, RPC_ENDPOINT_CONFIG_GET, {});
239
+ for (let i = 0; i < delays.length && !last.ok; i += 1) {
240
+ if (!alive()) return last;
241
+ const wait = delays[i] ?? 0;
242
+ if (wait > 0) await sleepFn(wait);
243
+ if (!alive()) return last;
244
+ last = await configRpcOnce(call, channel, RPC_ENDPOINT_CONFIG_GET, {});
245
+ }
246
+ return last;
229
247
  }
230
- /** 保存一份取值(回执只有 `{path, values}`,**不是**整面:写完要重新读一份整面)。 */
248
+ /** 保存一份取值(回执只有 `{path, values}`,**不是**整面:写完要重新读一份整面;永不自动重试)。 */
231
249
  function saveConfigSurface(call, channel, values) {
232
- return configRpc(call, channel, RPC_ENDPOINT_CONFIG_SAVE, { values });
250
+ return configRpcOnce(call, channel, RPC_ENDPOINT_CONFIG_SAVE, { values });
233
251
  }
234
- /** 重置为默认(技能侧先落一份备份;回执同样不是整面,写完要重新读一份整面)。 */
252
+ /** 重置为默认(技能侧先落一份备份;回执同样不是整面,写完要重新读一份整面;永不自动重试)。 */
235
253
  function resetConfigSurface(call, channel) {
236
- return configRpc(call, channel, RPC_ENDPOINT_CONFIG_RESET, {});
254
+ return configRpcOnce(call, channel, RPC_ENDPOINT_CONFIG_RESET, {});
237
255
  }
238
256
  //#endregion
239
- //#region ../../node_modules/.pnpm/dsh-life-pack@0.3.34/node_modules/dsh-life-pack/dist/directory-browser-contract.js
240
- /** 目录浏览器的纯逻辑半(**不认识宿主**:只认两格原语)。
241
- *
242
- * 为什么在这里:六个单品插件的设置页各有一批「值是一个目录」的行,它们要的界面是同一个东西。
243
- * 界面的**取数**来自宿主给的两个原语(`list`/`createDirectory`),而这两个原语的形状是**调用方喂进来的**
244
- * ——本件不 import 宿主的任何实现,也不认识 `remote.directoryPicker` 这个名字(那两件住在各家的
245
- * `dsh-ctx.ts` 镜像里)。于是本件在 Windows 桌面、远程浏览器、SSH 三种部署下是同一份代码。
246
- *
247
- * 装什么:本次要浏览的那一层怎么走(进子目录/回上一级/跳面包屑)、路径怎么切出「目录部分」与
248
- * 「筛选词」、回执怎么认。**不装**任何 IO、不装任何持久化——`list`/`createDirectory` 由调用方执行。
249
- *
250
- * 与平台自带浏览器件的关系:`@deepseek-ai/dsh-client-ui-directory-picker-browse` 的 `DirectoryBrowser`
251
- * 是包内私有件(只经 `ui-workspace` 的流程空位暴露),插件侧取不到,所以这里是一份独立实现,
252
- * 只对齐它的**交互结果**(进/选/新建),不对齐它的 DOM。
253
- */
257
+ //#region ../plugin-manager/src/directory-browser-contract.ts
254
258
  /** 认一认某个值是不是可用的目录取数面(软依赖守卫用:认不出就当没有,绝不把页面带下来)。 */
255
259
  function isBrowseFace(raw) {
256
260
  if (typeof raw !== "object" || raw === null) return false;
@@ -446,15 +450,7 @@ window.__ModuleLoader__.load({
446
450
  return { ok: true };
447
451
  }
448
452
  //#endregion
449
- //#region ../../node_modules/.pnpm/dsh-life-pack@0.3.34/node_modules/dsh-life-pack/dist/directory-browser-state.js
450
- /** 文件浏览器的操作半:把「进哪一层、这一层显示什么、选到了什么」算成状态,**IO 全部由调用方注入**。
451
- *
452
- * 分两层的原因:本件(操作)在 Node 里就能直测——喂一个假的取数面,就能把「进目录/回上一级/
453
- * 跳面包屑/筛选/新建文件夹/失败出人话」全跑一遍;视图那半(`directory-browser-ui.ts`)
454
- * 只负责把这份状态画成 DOM。两边都不认识宿主。
455
- *
456
- * 数据来源只有两格:`list(path?)` 与 `createDirectory(path, name)`(调用方注入的 `DirectoryBrowseFace`)。
457
- */
453
+ //#region ../plugin-manager/src/directory-browser-state.ts
458
454
  /** 建一个浏览器控制器。取数、回调、初值全部由调用方给。 */
459
455
  function createBrowseController(deps) {
460
456
  const label = deps.failureLabel ?? "浏览失败";
@@ -710,7 +706,7 @@ window.__ModuleLoader__.load({
710
706
  });
711
707
  }
712
708
  //#endregion
713
- //#region ../../node_modules/.pnpm/dsh-life-pack@0.3.34/node_modules/dsh-life-pack/dist/directory-browser-parts.js
709
+ //#region ../plugin-manager/src/directory-browser-parts.ts
714
710
  /** 文件浏览器视图半的**零件**:样式表与四行控件(面包屑/根/一行子目录/新建行)。
715
711
  *
716
712
  * 为什么拆出来:整张对话框的组版与这些零件的变化频率不同——零件跟着「图上多一行/少一行」动,
@@ -727,7 +723,7 @@ window.__ModuleLoader__.load({
727
723
  optical: "光驱",
728
724
  other: "磁盘"
729
725
  };
730
- const S$2 = {
726
+ const S$3 = {
731
727
  scrim: {
732
728
  position: "fixed",
733
729
  inset: 0,
@@ -883,21 +879,21 @@ window.__ModuleLoader__.load({
883
879
  listing.crumbs.forEach((crumb, index) => {
884
880
  if (index > 0) parts.push(react.createElement("span", {
885
881
  key: "sep-" + crumb.path,
886
- style: S$2.crumbSep
882
+ style: S$3.crumbSep
887
883
  }, "›"));
888
884
  const isCurrent = crumb.path === listing.path;
889
885
  parts.push(react.createElement("button", {
890
886
  key: crumb.path,
891
887
  type: "button",
892
888
  style: {
893
- ...S$2.chromeButton,
889
+ ...S$3.chromeButton,
894
890
  fontWeight: isCurrent ? 700 : 400
895
891
  },
896
892
  title: crumb.path,
897
893
  onClick: () => onEnter(crumb.path)
898
894
  }, crumb.name));
899
895
  });
900
- return react.createElement("div", { style: S$2.crumbs }, parts);
896
+ return react.createElement("div", { style: S$3.crumbs }, parts);
901
897
  }
902
898
  /** 可跳转的根那一行(Windows 上的盘符等):**就是本机全部磁盘**,故不加行首说明。
903
899
  * **少于两个根就不画**:一个根没有「跳到别处」可言。 */
@@ -907,27 +903,27 @@ window.__ModuleLoader__.load({
907
903
  key: root.path,
908
904
  type: "button",
909
905
  style: {
910
- ...S$2.chromeButton,
911
- ...S$2.rootButton
906
+ ...S$3.chromeButton,
907
+ ...S$3.rootButton
912
908
  },
913
909
  title: ROOT_KIND_TEXT[root.kind] ?? ROOT_KIND_TEXT.other,
914
910
  onClick: () => onEnter(root.path)
915
911
  }, root.path));
916
- return react.createElement("div", { style: S$2.roots }, parts);
912
+ return react.createElement("div", { style: S$3.roots }, parts);
917
913
  }
918
914
  /** 一行子目录:点名字进它,点「选」把它当目标(「选」再点一次取消)。 */
919
915
  function entryRow(entry, selected, labels, onToggleSelect, onEnter) {
920
916
  return react.createElement("div", {
921
917
  key: entry.path,
922
918
  style: {
923
- ...S$2.row,
919
+ ...S$3.row,
924
920
  padding: 0,
925
- ...selected ? S$2.rowSelected : {}
921
+ ...selected ? S$3.rowSelected : {}
926
922
  }
927
923
  }, react.createElement("button", {
928
924
  type: "button",
929
925
  style: {
930
- ...S$2.row,
926
+ ...S$3.row,
931
927
  flex: 1,
932
928
  border: "none"
933
929
  },
@@ -935,7 +931,7 @@ window.__ModuleLoader__.load({
935
931
  }, "📁 " + entry.name), react.createElement("button", {
936
932
  type: "button",
937
933
  style: {
938
- ...S$2.button,
934
+ ...S$3.button,
939
935
  marginRight: 8,
940
936
  padding: "2px 8px"
941
937
  },
@@ -948,10 +944,10 @@ window.__ModuleLoader__.load({
948
944
  function createRow(labels, onName, onCancel, onCreate, creatingName) {
949
945
  if (creatingName === null) return null;
950
946
  return react.createElement("div", { style: {
951
- ...S$2.pathRow,
947
+ ...S$3.pathRow,
952
948
  borderTop: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))"
953
949
  } }, react.createElement("input", {
954
- style: S$2.input,
950
+ style: S$3.input,
955
951
  value: creatingName,
956
952
  placeholder: labels.newFolder,
957
953
  "aria-label": labels.newFolder,
@@ -962,16 +958,16 @@ window.__ModuleLoader__.load({
962
958
  }
963
959
  }), react.createElement("button", {
964
960
  type: "button",
965
- style: S$2.button,
961
+ style: S$3.button,
966
962
  onClick: () => onCreate(creatingName)
967
963
  }, labels.createConfirm), react.createElement("button", {
968
964
  type: "button",
969
- style: S$2.button,
965
+ style: S$3.button,
970
966
  onClick: onCancel
971
967
  }, labels.createCancel));
972
968
  }
973
969
  //#endregion
974
- //#region ../../node_modules/.pnpm/dsh-life-pack@0.3.34/node_modules/dsh-life-pack/dist/directory-browser-ui.js
970
+ //#region ../plugin-manager/src/directory-browser-ui.ts
975
971
  /** 文件浏览器的视图半:把 `BrowseState` 画成一张对话框。**不认识宿主**,也不自己做 IO。
976
972
  *
977
973
  * 手写 `React.createElement`(本包 client 束禁 JSX,见 `tsdown.config.ts` 的既成口径)。
@@ -995,8 +991,8 @@ window.__ModuleLoader__.load({
995
991
  const body = [];
996
992
  body.push(crumbRow(state, props.onEnter));
997
993
  body.push(rootsRow(state, props.onEnter));
998
- body.push(react.createElement("div", { style: S$2.pathRow }, react.createElement("input", {
999
- style: S$2.input,
994
+ body.push(react.createElement("div", { style: S$3.pathRow }, react.createElement("input", {
995
+ style: S$3.input,
1000
996
  value: state.draft,
1001
997
  placeholder: labels.pathPlaceholder,
1002
998
  "aria-label": labels.pathPlaceholder,
@@ -1006,20 +1002,20 @@ window.__ModuleLoader__.load({
1006
1002
  }
1007
1003
  }), react.createElement("button", {
1008
1004
  type: "button",
1009
- style: S$2.button,
1005
+ style: S$3.button,
1010
1006
  onClick: props.onUp
1011
1007
  }, labels.up), react.createElement("button", {
1012
1008
  type: "button",
1013
- style: S$2.button,
1009
+ style: S$3.button,
1014
1010
  onClick: props.onCommitDraft
1015
1011
  }, labels.go)));
1016
1012
  body.push(react.createElement("div", {
1017
- style: S$2.list,
1013
+ style: S$3.list,
1018
1014
  role: "listbox",
1019
1015
  "aria-label": labels.title
1020
- }, rows.length === 0 ? react.createElement("div", { style: S$2.empty }, state.phase === "loading" ? labels.loading : labels.empty) : rows.map((entry) => entryRow(entry, state.selected === entry.path, labels, props.onSelect, props.onEnter))));
1016
+ }, rows.length === 0 ? react.createElement("div", { style: S$3.empty }, state.phase === "loading" ? labels.loading : labels.empty) : rows.map((entry) => entryRow(entry, state.selected === entry.path, labels, props.onSelect, props.onEnter))));
1021
1017
  body.push(createRow(labels, props.onDraft, () => props.onCreatingChange(null), props.onCreate, state.creating));
1022
- body.push(react.createElement("div", { style: S$2.footNote }, hidden > 0 ? react.createElement("label", { style: {
1018
+ body.push(react.createElement("div", { style: S$3.footNote }, hidden > 0 ? react.createElement("label", { style: {
1023
1019
  fontSize: 12,
1024
1020
  cursor: "pointer",
1025
1021
  marginRight: 10
@@ -1030,41 +1026,41 @@ window.__ModuleLoader__.load({
1030
1026
  }), " " + labels.showHidden(hidden)) : null, state.creating === null ? react.createElement("button", {
1031
1027
  type: "button",
1032
1028
  style: {
1033
- ...S$2.chromeButton,
1029
+ ...S$3.chromeButton,
1034
1030
  textDecoration: "underline"
1035
1031
  },
1036
1032
  onClick: () => props.onCreatingChange("")
1037
- }, labels.newFolder) : null, state.notice !== null ? react.createElement("div", { style: S$2.error }, state.notice) : null, state.phase === "failed" && state.failure !== null ? react.createElement("div", { style: S$2.error }, state.failure.message) : null));
1038
- body.push(react.createElement("div", { style: S$2.foot }, react.createElement("span", {
1039
- style: S$2.target,
1033
+ }, labels.newFolder) : null, state.notice !== null ? react.createElement("div", { style: S$3.error }, state.notice) : null, state.phase === "failed" && state.failure !== null ? react.createElement("div", { style: S$3.error }, state.failure.message) : null));
1034
+ body.push(react.createElement("div", { style: S$3.foot }, react.createElement("span", {
1035
+ style: S$3.target,
1040
1036
  title: target ?? ""
1041
1037
  }, target === null ? "" : labels.willPick + target), react.createElement("button", {
1042
1038
  type: "button",
1043
1039
  style: {
1044
- ...S$2.button,
1045
- ...target === null ? S$2.buttonOff : {}
1040
+ ...S$3.button,
1041
+ ...target === null ? S$3.buttonOff : {}
1046
1042
  },
1047
1043
  onClick: props.onPick
1048
1044
  }, labels.open), react.createElement("button", {
1049
1045
  type: "button",
1050
- style: S$2.button,
1046
+ style: S$3.button,
1051
1047
  onClick: props.onClose
1052
1048
  }, labels.cancel)));
1053
1049
  return react.createElement("div", {
1054
- style: S$2.scrim,
1050
+ style: S$3.scrim,
1055
1051
  role: "presentation",
1056
1052
  onMouseDown: (event) => {
1057
1053
  if (event.target === event.currentTarget) props.onClose();
1058
1054
  }
1059
1055
  }, react.createElement("div", {
1060
- style: S$2.dialog,
1056
+ style: S$3.dialog,
1061
1057
  role: "dialog",
1062
1058
  "aria-modal": true,
1063
1059
  "aria-label": labels.title
1064
- }, react.createElement("div", { style: S$2.head }, react.createElement("span", { style: S$2.title }, labels.title), react.createElement("button", {
1060
+ }, react.createElement("div", { style: S$3.head }, react.createElement("span", { style: S$3.title }, labels.title), react.createElement("button", {
1065
1061
  type: "button",
1066
1062
  style: {
1067
- ...S$2.button,
1063
+ ...S$3.button,
1068
1064
  marginLeft: "auto"
1069
1065
  },
1070
1066
  "aria-label": labels.close,
@@ -1089,7 +1085,7 @@ window.__ModuleLoader__.load({
1089
1085
  });
1090
1086
  }
1091
1087
  //#endregion
1092
- //#region ../../node_modules/.pnpm/dsh-life-pack@0.3.34/node_modules/dsh-life-pack/dist/title-seal.js
1088
+ //#region ../plugin-manager/src/title-seal.ts
1093
1089
  /** 共用标题 + 印章书签 · 纯组件(#996,不认任何一家)。
1094
1090
  *
1095
1091
  * 来源:原型 #995 变体 A(行尾悬签,人裁已定形)——标题左、签贴同行行尾向右探出。
@@ -1113,7 +1109,7 @@ window.__ModuleLoader__.load({
1113
1109
  *
1114
1110
  * 本件是纯函数:吃 props 回元素树,不留状态、不取数。手写 `React.createElement`(本包 client 束禁 JSX)。
1115
1111
  */
1116
- const S$1 = {
1112
+ const S$2 = {
1117
1113
  /** 整块 `h2`:同行基线双样式容器(原型 `.titleblock`;`flex:1 1 auto` 把签顶到行尾,390 宽可换行不挤)。 */
1118
1114
  titleBlock: {
1119
1115
  display: "flex",
@@ -1156,20 +1152,569 @@ window.__ModuleLoader__.load({
1156
1152
  function TitleBlock(props) {
1157
1153
  const label = (props.product + " " + props.purpose).trim();
1158
1154
  return react.createElement("h2", {
1159
- style: S$1.titleBlock,
1155
+ style: S$2.titleBlock,
1160
1156
  "aria-label": label
1161
- }, react.createElement("span", { style: S$1.product }, props.product), react.createElement("span", { style: S$1.purpose }, props.purpose));
1157
+ }, react.createElement("span", { style: S$2.product }, props.product), react.createElement("span", { style: S$2.purpose }, props.purpose));
1158
+ }
1159
+ //#endregion
1160
+ //#region ../plugin-manager/src/seal-scroll.ts
1161
+ /** 印章卷轴 · 纯组件(`title-seal.ts` 的配套件,不认任何一家)。
1162
+ *
1163
+ * 来源:原型定稿 2026-10-05(记录见 `docs/plugins/plugin-manager/印章原型-定稿.md` 第九节)。
1164
+ * 点印后弹出的那块:**外框逐档照抄对应那枚印的材质**(底色+边线都照抄,不许简化成"一圈金属色"),
1165
+ * **中间是纯纸**,内容固定三段——进展/状态/计划。
1166
+ *
1167
+ * 三条形状(逐值照原型锁死):
1168
+ * 外框:厚 1.125em(16px 下=18px,这是下限;只给 10px 整张卡会读成"白底加一圈边"),
1169
+ * 材质随 `role`×`tier` 九档之一,挂 `roughFrame` 糙边滤镜。
1170
+ * 纸:`bg-layer-2` 底+`border-l1` 描边,压在材质上(外框那层在纸下面,只露一圈)。
1171
+ * 字:标题居中 1em/字距 .16em;档位字 .66em/字距 .42em;三段标签 .66em/字距 .34em;
1172
+ * 正文 .84em/行高 1.9;段间 1px 细线+.94em 上距。
1173
+ *
1174
+ * 颜色分治(照 `title-seal.ts` 的先例):
1175
+ * 印那一份(外框材质、档位字色)**固定色**——纸变印不变,印不管纸面派生;
1176
+ * 纸那一份(纸底、纸边、标题、正文、段间线)走 DSH 主题别名。
1177
+ *
1178
+ * 糙边滤镜两条分开:印是小块,`scale 2.2` 就够;外框有几百像素宽,同一条会被稀释到看不出,
1179
+ * 故外框单用 `scale 5` 的 `roughFrame`。滤镜只挂外框那层,纸不挂(否则纸的直边也会被抖)。
1180
+ *
1181
+ * 本件是纯函数:吃 props 回元素树,不留状态、不取数。手写 `React.createElement`(本包 client 束禁 JSX)。
1182
+ */
1183
+ /** 糙边滤镜 id(文档作用域,故每页只许挂一次 `SealFilterDefs`)。 */
1184
+ const ROUGH_EDGE = "dshLifeSealRoughEdge";
1185
+ const ROUGH_FRAME = "dshLifeSealRoughFrame";
1186
+ const PAPER_BG = "var(--dsw-alias-bg-layer-2, #fdfaf2)";
1187
+ const PAPER_EDGE = "var(--dsw-alias-border-l1, #e6d9bd)";
1188
+ const LABEL_MAIN = "var(--dsw-alias-label-primary, #42506b)";
1189
+ const LABEL_SUB = "var(--dsw-alias-label-secondary, #4b4335)";
1190
+ /** 九档材质(3 角色 × 3 档)。键就是原型里的 `role`/`tier`。 */
1191
+ const MATERIALS = {
1192
+ skill: {
1193
+ copper: {
1194
+ frame: {
1195
+ background: "linear-gradient(180deg,#8a4a28,#6e3418)",
1196
+ border: "3px solid #a5652f",
1197
+ boxShadow: "inset 0 0 0 1px #f8f1e2aa, inset 0 2px 0 #ffffff26, inset 0 -3px 6px #0000004d"
1198
+ },
1199
+ accent: "#9a5f2a",
1200
+ ruler: "#c08145b3"
1201
+ },
1202
+ silver: {
1203
+ frame: {
1204
+ background: "linear-gradient(180deg,#b73124,#8e1f14)",
1205
+ border: "3px double #eef3f6",
1206
+ boxShadow: "inset 0 0 0 1px #fff, inset 0 2px 0 #ffffff2e, inset 0 -3px 6px #0000004d"
1207
+ },
1208
+ accent: "#6f767c",
1209
+ ruler: "#9aa0a6b3"
1210
+ },
1211
+ gold: {
1212
+ frame: {
1213
+ background: "linear-gradient(180deg,#d34a35,#a32216)",
1214
+ border: "3px double #f5d97a",
1215
+ boxShadow: "inset 0 0 0 1px #fff8, inset 0 2px 0 #ffffff33, inset 0 -3px 6px #0000004d"
1216
+ },
1217
+ accent: "#9c7a16",
1218
+ ruler: "#d8b338b3"
1219
+ }
1220
+ },
1221
+ help: {
1222
+ copper: {
1223
+ frame: {
1224
+ background: "linear-gradient(180deg,#b06a3a,#7e3f1d)",
1225
+ border: "3px solid #a5652f",
1226
+ boxShadow: "inset 0 2px 0 #ffffff26, inset 0 -3px 6px #0000004d"
1227
+ },
1228
+ accent: "#9a5f2a",
1229
+ ruler: "#c08145b3"
1230
+ },
1231
+ silver: {
1232
+ frame: {
1233
+ background: "linear-gradient(180deg,#8f979e,#5f666d)",
1234
+ border: "3px double #e8eef2",
1235
+ boxShadow: "inset 0 2px 0 #ffffff2e, inset 0 -3px 6px #0000004d"
1236
+ },
1237
+ accent: "#6f767c",
1238
+ ruler: "#9aa0a6b3"
1239
+ },
1240
+ gold: {
1241
+ frame: {
1242
+ background: "linear-gradient(180deg,#c63d2a,#a32216)",
1243
+ border: "3px double #f5d97a",
1244
+ boxShadow: "inset 0 2px 0 #ffffff33, inset 0 -3px 6px #0000004d"
1245
+ },
1246
+ accent: "#9c7a16",
1247
+ ruler: "#d8b338b3"
1248
+ }
1249
+ },
1250
+ plugin: {
1251
+ copper: {
1252
+ frame: {
1253
+ background: "linear-gradient(180deg,#b4773c,#7e3f1d 60%,#66300f)",
1254
+ boxShadow: "inset 0 1px 0 #d99a5e88, inset 0 -2px 5px #0006, 0 0 0 1px #5e2c12, inset 0 2px 0 #ffffff26"
1255
+ },
1256
+ accent: "#9a5f2a",
1257
+ ruler: "#c08145b3",
1258
+ rivet: {
1259
+ background: "radial-gradient(circle at 34% 28%,#f0c088,#a5652f 55%,#6a3216)",
1260
+ boxShadow: "inset 0 -1px 1px #00000055,0 1px 2px #00000088"
1261
+ }
1262
+ },
1263
+ silver: {
1264
+ frame: {
1265
+ background: "linear-gradient(180deg,#d8dee3,#a7aeb4 55%,#818990)",
1266
+ boxShadow: "inset 0 1px 0 #ffffffdd, inset 0 -2px 5px #0005, 0 0 0 1px #6f767c, inset 0 2px 0 #ffffff33"
1267
+ },
1268
+ accent: "#6f767c",
1269
+ ruler: "#9aa0a6b3",
1270
+ rivet: {
1271
+ background: "radial-gradient(circle at 34% 28%,#ffffff,#c3cad0 50%,#7d848b)",
1272
+ boxShadow: "inset 0 -1px 1px #00000044,0 1px 2px #00000077"
1273
+ }
1274
+ },
1275
+ gold: {
1276
+ frame: {
1277
+ background: "linear-gradient(180deg,#f2dc86,#c9a227 55%,#9c7d16)",
1278
+ boxShadow: "inset 0 1px 0 #fff8d8dd, inset 0 -2px 5px #0005, 0 0 0 1px #8a6a15"
1279
+ },
1280
+ accent: "#9c7a16",
1281
+ ruler: "#d8b338b3",
1282
+ rivet: {
1283
+ background: "radial-gradient(circle at 34% 28%,#fffbe0,#e3c565 50%,#a5841d)",
1284
+ boxShadow: "inset 0 -1px 1px #00000044,0 1px 2px #00000077,0 0 6px #c9a22788"
1285
+ }
1286
+ }
1287
+ }
1288
+ };
1289
+ const S$1 = {
1290
+ /** 外框(`.frame`):绝对铺满,挂糙边滤镜;纸压在它上面,只露一圈。 */
1291
+ frame: {
1292
+ position: "absolute",
1293
+ inset: 0,
1294
+ borderRadius: "1em",
1295
+ zIndex: 1,
1296
+ filter: "url(#" + ROUGH_FRAME + ")"
1297
+ },
1298
+ /** 四角铆钉(只有插件章的整面金属带有)。 */
1299
+ rivet: {
1300
+ position: "absolute",
1301
+ width: "0.69em",
1302
+ height: "0.69em",
1303
+ borderRadius: "50%"
1304
+ },
1305
+ /** 卷轴根:padding 就是外框厚度(.frame 铺满、纸在 padding 内)。 */
1306
+ scroll: {
1307
+ position: "relative",
1308
+ fontSize: "1em",
1309
+ lineHeight: 1.7,
1310
+ borderRadius: "0.875em",
1311
+ padding: "1.125em"
1312
+ },
1313
+ /** 纸(`.paper`):纯纸,无纹理。 */
1314
+ paper: {
1315
+ position: "relative",
1316
+ zIndex: 2,
1317
+ borderRadius: "0.25em",
1318
+ background: PAPER_BG,
1319
+ boxShadow: "0 1px 3px #3d241052, 0 0 0 1px " + PAPER_EDGE + ", inset 0 1px 0 #fffefb"
1320
+ },
1321
+ inner: {
1322
+ position: "relative",
1323
+ padding: "1.75em 2em 1.875em"
1324
+ },
1325
+ head: { textAlign: "center" },
1326
+ title: {
1327
+ margin: 0,
1328
+ fontSize: "1em",
1329
+ fontWeight: 600,
1330
+ letterSpacing: "0.16em",
1331
+ color: LABEL_MAIN
1332
+ },
1333
+ tier: {
1334
+ marginTop: "0.4375em",
1335
+ fontSize: "0.66em",
1336
+ letterSpacing: "0.42em",
1337
+ textIndent: "0.42em",
1338
+ fontWeight: 600
1339
+ },
1340
+ orn: {
1341
+ display: "flex",
1342
+ alignItems: "center",
1343
+ gap: "0.5625em",
1344
+ margin: "1em 0 1.25em"
1345
+ },
1346
+ ornLine: {
1347
+ flex: "1 1 auto",
1348
+ height: 1
1349
+ },
1350
+ ornDot: {
1351
+ width: "0.3125em",
1352
+ height: "0.3125em",
1353
+ transform: "rotate(45deg)",
1354
+ opacity: .75,
1355
+ flex: "0 0 auto"
1356
+ },
1357
+ block: { margin: "0 0 0.9375em" },
1358
+ blockDivided: {
1359
+ margin: "0 0 0.9375em",
1360
+ paddingTop: "0.9375em",
1361
+ borderTop: "1px solid " + PAPER_EDGE
1362
+ },
1363
+ blockLast: { margin: 0 },
1364
+ label: {
1365
+ fontSize: "0.66em",
1366
+ letterSpacing: "0.34em",
1367
+ textIndent: "0.34em",
1368
+ fontWeight: 600,
1369
+ marginBottom: "0.375em"
1370
+ },
1371
+ text: {
1372
+ margin: 0,
1373
+ fontSize: "0.84em",
1374
+ lineHeight: 1.9,
1375
+ letterSpacing: "0.02em",
1376
+ color: LABEL_SUB
1377
+ },
1378
+ /** 弹层遮罩。 */
1379
+ mask: {
1380
+ position: "fixed",
1381
+ inset: 0,
1382
+ background: "#241f1966",
1383
+ zIndex: 40
1384
+ },
1385
+ /** 弹层本体:居中,宽度按 em 给(16px 下≈25em)。 */
1386
+ dialog: {
1387
+ position: "fixed",
1388
+ left: "50%",
1389
+ top: "50%",
1390
+ transform: "translate(-50%,-50%)",
1391
+ width: "25em",
1392
+ zIndex: 50
1393
+ },
1394
+ close: {
1395
+ position: "absolute",
1396
+ right: "-0.375em",
1397
+ top: "-2.25em",
1398
+ font: "inherit",
1399
+ fontSize: "0.75em",
1400
+ color: "#fff",
1401
+ background: "#241f19b3",
1402
+ border: "none",
1403
+ borderRadius: "0.875em",
1404
+ padding: "0.25em 0.875em",
1405
+ cursor: "pointer"
1406
+ }
1407
+ };
1408
+ function materialOf(role, tier) {
1409
+ const found = MATERIALS[role]?.[tier];
1410
+ if (!found) throw new Error("未知的印档:" + role + "/" + tier);
1411
+ return found;
1412
+ }
1413
+ /** 糙边滤镜定义:两条分开(印用 edge,卷轴外框用 frame),每页只挂一次。
1414
+ *
1415
+ * 挂在文档任意位置都行(`width/height=0` 不占位),同页重复挂会撞 id。 */
1416
+ function SealFilterDefs() {
1417
+ return react.createElement("svg", {
1418
+ width: 0,
1419
+ height: 0,
1420
+ "aria-hidden": true,
1421
+ focusable: false,
1422
+ style: { position: "absolute" }
1423
+ }, react.createElement("filter", {
1424
+ id: ROUGH_EDGE,
1425
+ key: ROUGH_EDGE
1426
+ }, react.createElement("feTurbulence", {
1427
+ baseFrequency: .9,
1428
+ numOctaves: 2,
1429
+ result: "n"
1430
+ }), react.createElement("feDisplacementMap", {
1431
+ in: "SourceGraphic",
1432
+ in2: "n",
1433
+ scale: 2.2
1434
+ })), react.createElement("filter", {
1435
+ id: ROUGH_FRAME,
1436
+ key: ROUGH_FRAME,
1437
+ x: "-6%",
1438
+ y: "-6%",
1439
+ width: "112%",
1440
+ height: "112%"
1441
+ }, react.createElement("feTurbulence", {
1442
+ baseFrequency: .9,
1443
+ numOctaves: 2,
1444
+ result: "n"
1445
+ }), react.createElement("feDisplacementMap", {
1446
+ in: "SourceGraphic",
1447
+ in2: "n",
1448
+ scale: 5
1449
+ })));
1162
1450
  }
1163
- /** 印章书签(横向固定印,纯展示)。 */
1164
- function Seal(props) {
1165
- return react.createElement("span", {
1166
- style: S$1.seal,
1167
- role: "note",
1168
- "aria-label": props.sealText
1169
- }, props.sealText);
1451
+ /** 一卷:外框材质 + 纯纸 + 进展/状态/计划三段。 */
1452
+ function SealScroll(props) {
1453
+ const material = materialOf(props.role, props.tier);
1454
+ const frameChildren = props.role === "plugin" && material.rivet ? [
1455
+ react.createElement("span", {
1456
+ key: "tl",
1457
+ style: {
1458
+ ...S$1.rivet,
1459
+ ...material.rivet,
1460
+ left: "0.4375em",
1461
+ top: "0.4375em"
1462
+ }
1463
+ }),
1464
+ react.createElement("span", {
1465
+ key: "tr",
1466
+ style: {
1467
+ ...S$1.rivet,
1468
+ ...material.rivet,
1469
+ right: "0.4375em",
1470
+ top: "0.4375em"
1471
+ }
1472
+ }),
1473
+ react.createElement("span", {
1474
+ key: "bl",
1475
+ style: {
1476
+ ...S$1.rivet,
1477
+ ...material.rivet,
1478
+ left: "0.4375em",
1479
+ bottom: "0.4375em"
1480
+ }
1481
+ }),
1482
+ react.createElement("span", {
1483
+ key: "br",
1484
+ style: {
1485
+ ...S$1.rivet,
1486
+ ...material.rivet,
1487
+ right: "0.4375em",
1488
+ bottom: "0.4375em"
1489
+ }
1490
+ })
1491
+ ] : [];
1492
+ const line = (key, dir) => react.createElement("span", {
1493
+ key,
1494
+ style: {
1495
+ ...S$1.ornLine,
1496
+ background: "linear-gradient(" + dir + "deg,#0000," + material.ruler + ")"
1497
+ }
1498
+ });
1499
+ const block = (key, name, text, divided, last = false) => react.createElement("div", {
1500
+ key,
1501
+ style: last ? divided ? {
1502
+ ...S$1.blockLast,
1503
+ paddingTop: "0.9375em",
1504
+ borderTop: "1px solid " + PAPER_EDGE
1505
+ } : S$1.blockLast : divided ? S$1.blockDivided : S$1.block
1506
+ }, react.createElement("div", { style: {
1507
+ ...S$1.label,
1508
+ color: material.accent
1509
+ } }, name), react.createElement("p", { style: S$1.text }, text));
1510
+ return react.createElement("div", { style: S$1.scroll }, react.createElement("div", { style: {
1511
+ ...S$1.frame,
1512
+ ...material.frame
1513
+ } }, ...frameChildren), react.createElement("div", { style: S$1.paper }, react.createElement("div", { style: S$1.inner }, react.createElement("div", { style: S$1.head }, react.createElement("h4", { style: S$1.title }, props.title), react.createElement("div", { style: {
1514
+ ...S$1.tier,
1515
+ color: material.accent
1516
+ } }, props.tier === "copper" ? "铜" : props.tier === "silver" ? "银" : "金")), react.createElement("div", { style: S$1.orn }, line("l", "90"), react.createElement("i", { style: {
1517
+ ...S$1.ornDot,
1518
+ background: material.accent
1519
+ } }), line("r", "270")), block("progress", "进展", props.progress, false), block("status", "状态", props.status, true), block("plan", "计划", props.plan, true, true))));
1520
+ }
1521
+ /** 弹层:遮罩 + 居中卷轴 + 关闭。`open` 为假时不渲染(纯函数,不收自己的状态)。 */
1522
+ function SealScrollDialog(props) {
1523
+ if (!props.open) return null;
1524
+ const scroll = {
1525
+ role: props.role,
1526
+ tier: props.tier,
1527
+ title: props.title,
1528
+ progress: props.progress,
1529
+ status: props.status,
1530
+ plan: props.plan
1531
+ };
1532
+ return react.createElement(react.Fragment, null, react.createElement("div", {
1533
+ style: S$1.mask,
1534
+ onClick: props.onClose,
1535
+ role: "presentation"
1536
+ }), react.createElement("div", {
1537
+ style: S$1.dialog,
1538
+ role: "dialog",
1539
+ "aria-modal": true,
1540
+ "aria-label": props.title
1541
+ }, react.createElement("button", {
1542
+ type: "button",
1543
+ style: S$1.close,
1544
+ onClick: props.onClose
1545
+ }, "关闭"), react.createElement(SealScroll, scroll)));
1546
+ }
1547
+ //#endregion
1548
+ //#region ../plugin-manager/src/seal-stamp.ts
1549
+ /** 九枚印章 · 纯组件(`title-seal.ts` 的配套件,不认任何一家)。
1550
+ *
1551
+ * 来源:验收页 `.scratch/seal-9-restore-approve-v1.html` v2(逐值照 `.scratch/seal-9-roles-v6.html`:
1552
+ * 技能横条 1/HELP 旧版 2b/插件双铆 3,渐变、边线、铆钉、糙边、字号 padding 全锁)。
1553
+ * 印文:技能签统一“技能”、插件签统一“插件”(印面不带档位字,档位只看颜色边线);
1554
+ * HELP 签印产品名加空格加大写 HELP。点击弹卷(`seal-scroll.ts` 的 `SealScrollDialog`,调用方持 open),
1555
+ * 故印是 `button`(`aria-haspopup="dialog"`),hover 不出东西。
1556
+ *
1557
+ * 字号一律 em(16px 下与原型 px 逐值对齐:1.3125em=21px、1.0625em=17px);
1558
+ * 糙边走印那条(`SEAL_ROUGH_EDGE_ID`,定义只在 `seal-scroll.ts`,此处引用)。
1559
+ *
1560
+ * 本件是纯函数:吃 props 回元素树,不留状态、不取数。手写 `React.createElement`(本包 client 束禁 JSX)。
1561
+ */
1562
+ /** 三角色底座(字号 padding 随角色,材质随档位)。 */
1563
+ const ROLE_BASE = {
1564
+ skill: {
1565
+ fontSize: "1.0625em",
1566
+ padding: "0.5em 1.75em",
1567
+ letterSpacing: ".14em",
1568
+ borderRadius: "3px"
1569
+ },
1570
+ help: {
1571
+ fontSize: "1.0625em",
1572
+ padding: "0.5em 1em",
1573
+ letterSpacing: ".14em",
1574
+ borderRadius: "3px",
1575
+ color: "#f8f1e2"
1576
+ },
1577
+ plugin: {
1578
+ position: "relative",
1579
+ justifyContent: "center",
1580
+ fontSize: "1.0625em",
1581
+ padding: "0.5em 2em",
1582
+ letterSpacing: ".18em",
1583
+ borderRadius: "4px",
1584
+ color: "#f8f1e2"
1585
+ }
1586
+ };
1587
+ /** 九档印面(底色加边线都照抄对应那枚印,不许简化)。 */
1588
+ const FACE = {
1589
+ skill: {
1590
+ copper: {
1591
+ background: "linear-gradient(180deg,#8a4a28,#6e3418)",
1592
+ color: "#f8f1e2",
1593
+ border: "3px solid #a5652f",
1594
+ boxShadow: "inset 0 0 0 1px #f8f1e2aa"
1595
+ },
1596
+ silver: {
1597
+ background: "linear-gradient(180deg,#b73124,#8e1f14)",
1598
+ color: "#f8f1e2",
1599
+ border: "3px double #eef3f6",
1600
+ boxShadow: "inset 0 0 0 1px #fff"
1601
+ },
1602
+ gold: {
1603
+ background: "linear-gradient(180deg,#d34a35,#a32216)",
1604
+ color: "#fff8e0",
1605
+ border: "3px double #f5d97a",
1606
+ boxShadow: "inset 0 0 0 1px #fff8,0 4px 16px #c9a22755"
1607
+ }
1608
+ },
1609
+ help: {
1610
+ copper: {
1611
+ background: "linear-gradient(180deg,#b06a3a,#7e3f1d)",
1612
+ border: "3px solid #a5652f"
1613
+ },
1614
+ silver: {
1615
+ background: "linear-gradient(180deg,#8f979e,#5f666d)",
1616
+ border: "3px double #e8eef2"
1617
+ },
1618
+ gold: {
1619
+ background: "linear-gradient(180deg,#c63d2a,#a32216)",
1620
+ border: "3px double #f5d97a"
1621
+ }
1622
+ },
1623
+ plugin: {
1624
+ copper: { background: "linear-gradient(180deg,#b4773c,#7e3f1d 60%,#66300f)" },
1625
+ silver: { background: "linear-gradient(180deg,#d8dee3,#a7aeb4 55%,#818990)" },
1626
+ gold: { background: "linear-gradient(180deg,#f2dc86,#c9a227 55%,#9c7d16)" }
1627
+ }
1628
+ };
1629
+ /** 插件整面金属带的描边(随档)。 */
1630
+ const BAND_EDGE = {
1631
+ copper: { boxShadow: "inset 0 1px 0 #d99a5e88,inset 0 -2px 5px #0006,0 0 0 1px #5e2c12" },
1632
+ silver: { boxShadow: "inset 0 1px 0 #ffffffdd,inset 0 -2px 5px #0005,0 0 0 1px #6f767c" },
1633
+ gold: { boxShadow: "inset 0 1px 0 #fff8d8dd,inset 0 -2px 5px #0005,0 0 0 1px #8a6a15,0 3px 14px #c9a22766" }
1634
+ };
1635
+ /** 插件左右铆钉(随档,两颗)。 */
1636
+ const RIVET = {
1637
+ copper: {
1638
+ background: "radial-gradient(circle at 34% 28%,#f0c088,#a5652f 55%,#6a3216)",
1639
+ boxShadow: "inset 0 -1px 1px #00000055,0 1px 2px #00000088"
1640
+ },
1641
+ silver: {
1642
+ background: "radial-gradient(circle at 34% 28%,#ffffff,#c3cad0 50%,#7d848b)",
1643
+ boxShadow: "inset 0 -1px 1px #00000044,0 1px 2px #00000077"
1644
+ },
1645
+ gold: {
1646
+ background: "radial-gradient(circle at 34% 28%,#fffbe0,#e3c565 50%,#a5841d)",
1647
+ boxShadow: "inset 0 -1px 1px #00000044,0 1px 2px #00000077,0 0 6px #c9a22788"
1648
+ }
1649
+ };
1650
+ /** 一枚印章(button,点章弹卷)。未知组合直接抛错,不静默退化。 */
1651
+ function SealStamp(props) {
1652
+ const face = FACE[props.role][props.tier];
1653
+ if (face === void 0) throw new Error("未知印章组合:" + String(props.role) + "/" + String(props.tier));
1654
+ const buttonProps = {
1655
+ type: "button",
1656
+ style: {
1657
+ display: "inline-flex",
1658
+ alignItems: "center",
1659
+ cursor: "pointer",
1660
+ fontWeight: 800,
1661
+ lineHeight: 1.2,
1662
+ whiteSpace: "nowrap",
1663
+ filter: "url(#dshLifeSealRoughEdge)",
1664
+ ...ROLE_BASE[props.role],
1665
+ ...face
1666
+ },
1667
+ "aria-label": props.label,
1668
+ "aria-haspopup": "dialog",
1669
+ onClick: props.onSelect
1670
+ };
1671
+ if (props.role !== "plugin") return react.createElement("button", buttonProps, props.label);
1672
+ const band = {
1673
+ position: "absolute",
1674
+ top: "-1px",
1675
+ bottom: "-1px",
1676
+ left: "-1px",
1677
+ right: "-1px",
1678
+ zIndex: 1,
1679
+ borderRadius: "4px",
1680
+ ...BAND_EDGE[props.tier]
1681
+ };
1682
+ const rivet = {
1683
+ position: "absolute",
1684
+ zIndex: 3,
1685
+ width: "0.5em",
1686
+ height: "0.5em",
1687
+ borderRadius: "50%",
1688
+ top: "50%",
1689
+ transform: "translateY(-50%)",
1690
+ ...RIVET[props.tier]
1691
+ };
1692
+ return react.createElement("button", buttonProps, react.createElement("span", {
1693
+ style: {
1694
+ ...band,
1695
+ background: face.background
1696
+ },
1697
+ "data-seal-band": "1"
1698
+ }), react.createElement("span", {
1699
+ style: {
1700
+ ...rivet,
1701
+ left: "0.5em"
1702
+ },
1703
+ "data-seal-rivet": "left"
1704
+ }), react.createElement("span", {
1705
+ style: {
1706
+ ...rivet,
1707
+ right: "0.5em"
1708
+ },
1709
+ "data-seal-rivet": "right"
1710
+ }), react.createElement("span", { style: {
1711
+ position: "relative",
1712
+ zIndex: 2,
1713
+ textShadow: "0 1px 2px #00000088"
1714
+ } }, props.label));
1170
1715
  }
1171
1716
  //#endregion
1172
- //#region ../../node_modules/.pnpm/dsh-life-pack@0.3.34/node_modules/dsh-life-pack/dist/config-panel-view.js
1717
+ //#region ../plugin-manager/src/config-panel-view.ts
1173
1718
  /** 共用配置面板 · 视图半:一行怎么画(`Row`)、整面怎么画(`PanelBody`)。
1174
1719
  *
1175
1720
  * 本件是**纯函数**:吃一份状态与一组回调,回一棵 React 元素树,自己不留任何状态、不取任何数。
@@ -1230,6 +1775,7 @@ window.__ModuleLoader__.load({
1230
1775
  * +`border-radius:12px`+`padding:14px`+`box-shadow:inset 0 1px 0 rgba(255,255,255,.06)`。
1231
1776
  * 基准字号**不钉在卡片上**(#739 ⑤:跟着宿主走)。 */
1232
1777
  card: {
1778
+ position: "relative",
1233
1779
  padding: 14,
1234
1780
  borderRadius: 12,
1235
1781
  border: "1px solid " + PANEL_CARD_EDGE,
@@ -1247,6 +1793,16 @@ window.__ModuleLoader__.load({
1247
1793
  gap: 10,
1248
1794
  minHeight: 22
1249
1795
  },
1796
+ /** 印覆盖层(1158 红框区):卡片右上绝对区,只罩标题+头两行(底栏永远罩不到);本身镂空穿透,印各自可点、缝隙漏过。 */
1797
+ sealOverlay: {
1798
+ position: "absolute",
1799
+ top: 0,
1800
+ right: 0,
1801
+ width: "38%",
1802
+ height: "9em",
1803
+ pointerEvents: "none",
1804
+ zIndex: 5
1805
+ },
1250
1806
  /** `.ic-title{font-size:13.5px;font-weight:640}`——字号按 60 行那条锚点换算成 `1.08em`。
1251
1807
  * #996:卡片标题行改用 `TitleBlock`(见 `title-seal.ts`,原型 A 锁 1.15em/700+0.85em/400),
1252
1808
  * 这一格保留供附加块(`ConfigStyles` 键名不许改),头部本身不再用它。 */
@@ -1813,7 +2369,106 @@ window.__ModuleLoader__.load({
1813
2369
  const { state, title } = props;
1814
2370
  const surface = state.kind === "ready" ? state.surface : null;
1815
2371
  const product = title ?? "";
1816
- const seal = props.sealText === void 0 || props.sealText === "" ? null : react.createElement(Seal, { sealText: props.sealText });
2372
+ const seals = props.seals ?? [];
2373
+ /** 18 枚倾角各不同(1158):家下标×3+位序,步进错开。 */
2374
+ const TILT18 = [
2375
+ -4,
2376
+ -2.5,
2377
+ -1,
2378
+ -3,
2379
+ 1.5,
2380
+ 3,
2381
+ -1.5,
2382
+ 2,
2383
+ 4,
2384
+ -3.5,
2385
+ .5,
2386
+ 2.5,
2387
+ -4.5,
2388
+ 1,
2389
+ -2,
2390
+ 3.5,
2391
+ -.5,
2392
+ 4.5
2393
+ ];
2394
+ /** 三枚落位(相对覆盖层右上:HELP 右上/技能左中/插件右下,互不遮挡)。 */
2395
+ const SPOT = [
2396
+ {
2397
+ left: "45%",
2398
+ top: "0.3em"
2399
+ },
2400
+ {
2401
+ left: "5%",
2402
+ top: "2.8em"
2403
+ },
2404
+ {
2405
+ left: "50%",
2406
+ top: "5em"
2407
+ }
2408
+ ];
2409
+ const seed = props.sealSeed ?? 0;
2410
+ const order = [
2411
+ [
2412
+ 0,
2413
+ 1,
2414
+ 2
2415
+ ],
2416
+ [
2417
+ 1,
2418
+ 2,
2419
+ 0
2420
+ ],
2421
+ [
2422
+ 2,
2423
+ 0,
2424
+ 1
2425
+ ],
2426
+ [
2427
+ 0,
2428
+ 2,
2429
+ 1
2430
+ ],
2431
+ [
2432
+ 1,
2433
+ 0,
2434
+ 2
2435
+ ],
2436
+ [
2437
+ 2,
2438
+ 1,
2439
+ 0
2440
+ ]
2441
+ ][seed % 6];
2442
+ const tiltOfSlot = (slot) => String(TILT18[(seed * 3 + slot) % 18]);
2443
+ const zOfRole = (role) => role === "help" ? 4 : role === "skill" ? 3 : 2;
2444
+ const overlay = seals.length === 0 ? null : react.createElement("div", { style: S.sealOverlay }, react.createElement(SealFilterDefs, {}), ...order.map((dataIndex, slot) => react.createElement("span", {
2445
+ key: seals[dataIndex].role + ":" + seals[dataIndex].tier,
2446
+ style: {
2447
+ position: "absolute",
2448
+ left: SPOT[slot].left,
2449
+ top: SPOT[slot].top,
2450
+ zIndex: zOfRole(seals[dataIndex].role),
2451
+ pointerEvents: "auto",
2452
+ display: "inline-flex",
2453
+ transform: "rotate(" + tiltOfSlot(slot) + "deg)"
2454
+ }
2455
+ }, react.createElement(SealStamp, {
2456
+ role: seals[dataIndex].role,
2457
+ tier: seals[dataIndex].tier,
2458
+ label: seals[dataIndex].label,
2459
+ onSelect: props.onSealSelect === void 0 ? void 0 : props.onSealSelect.bind(null, dataIndex)
2460
+ }))));
2461
+ const openSeal = props.selectedSeal === null || props.selectedSeal === void 0 ? null : seals[props.selectedSeal] ?? null;
2462
+ const dialog = openSeal === null ? null : react.createElement(SealScrollDialog, {
2463
+ open: true,
2464
+ onClose: props.onSealClose ?? (() => void 0),
2465
+ role: openSeal.role,
2466
+ tier: openSeal.tier,
2467
+ title: openSeal.label,
2468
+ progress: openSeal.progress,
2469
+ status: openSeal.status,
2470
+ plan: openSeal.plan
2471
+ });
1817
2472
  const headDataDir = surface === null ? "" : surface.resolved?.["dbDir"] ?? surface.dataDir;
1818
2473
  const ready = state.kind === "ready";
1819
2474
  const dirtyCount = ready ? props.dirtyKeys.length : 0;
@@ -1828,10 +2483,10 @@ window.__ModuleLoader__.load({
1828
2483
  const head = react.createElement("div", { style: S.head }, react.createElement(TitleBlock, {
1829
2484
  product,
1830
2485
  purpose: "配置"
1831
- }), seal, react.createElement(PanelBadgeView, {
2486
+ }), react.createElement(PanelBadgeView, {
1832
2487
  kind: badge,
1833
2488
  dirtyCount
1834
- }));
2489
+ }), dialog);
1835
2490
  /** #939:这一块的高度由**客户端常量**决定、不由回执时序决定——加载期就把两行连标签一起画出来,
1836
2491
  * 值位画骨架条(`.ic-sk`),回执到了**原地填值** ⇒ 回执零位移(原先加载期只画一行小灰字、
1837
2492
  * 回执到了才长出两行,整块下移 21px)。
@@ -1859,7 +2514,7 @@ window.__ModuleLoader__.load({
1859
2514
  reply: surface
1860
2515
  };
1861
2516
  const extra = props.extra === void 0 ? null : props.extra(parts);
1862
- if (state.kind === "failed") return react.createElement("div", { style: S.card }, head, fileLines, react.createElement("div", { style: S.error }, state.message), extra, react.createElement("div", { style: S.bar }, react.createElement("button", {
2517
+ if (state.kind === "failed") return react.createElement("div", { style: S.card }, head, fileLines, react.createElement("div", { style: S.error }, state.message), extra, overlay, react.createElement("div", { style: S.bar }, react.createElement("button", {
1863
2518
  style: S.btn,
1864
2519
  type: "button",
1865
2520
  onClick: props.onRetry
@@ -1919,16 +2574,10 @@ window.__ModuleLoader__.load({
1919
2574
  }) : null, props.writeError !== null ? react.createElement("div", {
1920
2575
  style: S.error,
1921
2576
  role: "alert"
1922
- }, props.writeError) : null, props.error !== null ? react.createElement("div", { style: S.error }, props.error) : null, props.notice !== null ? react.createElement("div", { style: S.okText }, props.notice) : null);
2577
+ }, props.writeError) : null, props.error !== null ? react.createElement("div", { style: S.error }, props.error) : null, props.notice !== null ? react.createElement("div", { style: S.okText }, props.notice) : null, overlay);
1923
2578
  }
1924
2579
  //#endregion
1925
- //#region ../../node_modules/.pnpm/dsh-life-pack@0.3.34/node_modules/dsh-life-pack/dist/directory-browser-roots.js
1926
- /** 「本机根清单」的取数接线(客户端半):把一通电话变成图上那一行可跳转的根。
1927
- *
1928
- * **不认识宿主**:载体基段、通道名(第二段)、电话名全由调用方传进来——各家从自己的宿主镜像里拿
1929
- * 那几个字面量(和 `openRowBrowser` 的 `refusalCode` 同一条规矩)。本件只做两件事:
1930
- * 发一次调用、把回执信封拆成根清单;**任何失败都当空清单**(图上少画一行,不报错、不挡路)。
1931
- */
2580
+ //#region ../plugin-manager/src/directory-browser-roots.ts
1932
2581
  /** 认一认某个值像不像一个根(认不出的行丢掉:图的干净比多画一行重要)。 */
1933
2582
  function isRootRow(raw) {
1934
2583
  if (typeof raw !== "object" || raw === null) return false;
@@ -1994,7 +2643,7 @@ window.__ModuleLoader__.load({
1994
2643
  roots: "ilife-manager.roots"
1995
2644
  };
1996
2645
  //#endregion
1997
- //#region ../../node_modules/.pnpm/dsh-life-pack@0.3.34/node_modules/dsh-life-pack/dist/config-panel.js
2646
+ //#region ../plugin-manager/src/config-panel.ts
1998
2647
  /** 共用配置面板 · 组件本体:一台状态机 + 一圈副作用,渲染交给 `config-panel-view.ts`。
1999
2648
  *
2000
2649
  * 这一件就是六个技能设置页的那张卡片:六家只交「连哪条通道」与「有哪些配置项」(行表),
@@ -2076,6 +2725,8 @@ window.__ModuleLoader__.load({
2076
2725
  const [error, setError] = react.useState(null);
2077
2726
  const [browseRow, setBrowseRow] = react.useState(null);
2078
2727
  const [copy, setCopy] = react.useState(null);
2728
+ /** 弹卷开合:点章置下标、关卷置空(纯 UI 状态,不进脏基线)。 */
2729
+ const [selectedSeal, setSelectedSeal] = react.useState(null);
2079
2730
  /** 复制回执的复位定时器:**一次性 UI 回执**,不是数据轮询——点一次起一个,到点自我复位,
2080
2731
  * 下一次点击先清掉旧的,卸载时也清掉(下面那个清扫 effect 就是那一半)。 */
2081
2732
  const copyTimer = react.useRef(null);
@@ -2085,11 +2736,33 @@ window.__ModuleLoader__.load({
2085
2736
  copyTimer.current = null;
2086
2737
  }
2087
2738
  }, []);
2088
- /** 挂载期读一次整面:卸载之后回来的回执丢掉(不许往已经没了的页面上写)。 */
2739
+ /** 取数退避的计时器登记(#1142,照 #982 同形):卸载即清,不留挂起的等待;仅读整面用,保存/重置永不自动重试。 */
2740
+ const retryTimers = react.useRef([]);
2741
+ react.useEffect(() => () => {
2742
+ for (const id of retryTimers.current) clearTimeout(id);
2743
+ retryTimers.current = [];
2744
+ }, []);
2745
+ /** 在途去重:快速连点只跑一次,迟到回执不覆盖(#1142)。 */
2746
+ const inflight = react.useRef(false);
2747
+ /** 挂载期读一次整面(#1142 首帧有界重试):瞬时未就绪不再整面失败;卸载之后回来的回执丢掉。 */
2089
2748
  react.useEffect(() => {
2090
2749
  let alive = true;
2750
+ const sleep = (ms) => new Promise((resolve) => {
2751
+ const timer = setTimeout(() => {
2752
+ clearTimeout(timer);
2753
+ resolve();
2754
+ }, ms);
2755
+ retryTimers.current.push(timer);
2756
+ });
2091
2757
  (async () => {
2092
- const r = await fetchConfigSurface(props.getCall(), props.channel);
2758
+ const t0 = Date.now();
2759
+ const r = await fetchConfigSurface(props.getCall(), props.channel, {
2760
+ delays: CONFIG_READ_RETRY_MS,
2761
+ sleep,
2762
+ isAlive: () => alive
2763
+ });
2764
+ for (const id of retryTimers.current) clearTimeout(id);
2765
+ retryTimers.current = [];
2093
2766
  if (!alive) return;
2094
2767
  if (r.ok) {
2095
2768
  setState({
@@ -2097,10 +2770,13 @@ window.__ModuleLoader__.load({
2097
2770
  surface: r.surface
2098
2771
  });
2099
2772
  setDraft(toDraft(props.items, r.surface.values, r.surface));
2100
- } else setState({
2101
- kind: "failed",
2102
- message: r.message
2103
- });
2773
+ } else {
2774
+ setState({
2775
+ kind: "failed",
2776
+ message: r.message
2777
+ });
2778
+ console.warn("[dsh-life-pack] config surface load failed: " + props.channel + " elapsedMs=" + String(Date.now() - t0) + " " + r.message);
2779
+ }
2104
2780
  })();
2105
2781
  return () => {
2106
2782
  alive = false;
@@ -2115,15 +2791,25 @@ window.__ModuleLoader__.load({
2115
2791
  setDraft(toDraft(props.items, surface.values, surface));
2116
2792
  };
2117
2793
  const load = async () => {
2794
+ if (inflight.current) return;
2795
+ inflight.current = true;
2118
2796
  setNotice(null);
2119
2797
  setWriteError(null);
2120
2798
  setError(null);
2121
- const r = await fetchConfigSurface(props.getCall(), props.channel);
2122
- if (r.ok) apply(r.surface);
2123
- else setState({
2124
- kind: "failed",
2125
- message: r.message
2126
- });
2799
+ try {
2800
+ const t0 = Date.now();
2801
+ const r = await fetchConfigSurface(props.getCall(), props.channel);
2802
+ if (r.ok) apply(r.surface);
2803
+ else {
2804
+ setState({
2805
+ kind: "failed",
2806
+ message: r.message
2807
+ });
2808
+ console.warn("[dsh-life-pack] config surface reload failed: " + props.channel + " elapsedMs=" + String(Date.now() - t0) + " " + r.message);
2809
+ }
2810
+ } finally {
2811
+ inflight.current = false;
2812
+ }
2127
2813
  };
2128
2814
  /** 写完(保存/重置)之后重新读一份整面:写回执只有 `{path, values}`、重置回执只有
2129
2815
  * `{path, backupPath}`,都不是整面——拿它当整面用,页头那行「数据目录」会显示成空。 */
@@ -2233,7 +2919,11 @@ window.__ModuleLoader__.load({
2233
2919
  };
2234
2920
  return react.createElement(PanelBody, {
2235
2921
  title: props.title,
2236
- sealText: props.sealText,
2922
+ seals: props.seals,
2923
+ sealSeed: props.sealSeed,
2924
+ selectedSeal,
2925
+ onSealSelect: (index) => setSelectedSeal(index),
2926
+ onSealClose: () => setSelectedSeal(null),
2237
2927
  items: props.items,
2238
2928
  state,
2239
2929
  draft,