dsh-schedule-ilife 0.3.7 → 0.3.9

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
@@ -33,7 +33,7 @@ window.__ModuleLoader__.load({
33
33
  const PLUGIN = "dsh-schedule-ilife";
34
34
  //#endregion
35
35
  //#region src/settings.ts
36
- /** 常用项:改了就影响「库与产物落在哪」,放页面上。 */
36
+ /** 常用项:#764 起是「可改 1 + 只读 2」(高级组已空,见下)。 */
37
37
  const COMMON = [
38
38
  {
39
39
  key: "db.dir",
@@ -48,30 +48,23 @@ window.__ModuleLoader__.load({
48
48
  title: "库文件名",
49
49
  tier: "common",
50
50
  control: "text",
51
- hint: "数据目录下的数据库文件名。改名=换库,旧记录不会被读入。"
51
+ readonly: true,
52
+ resolveFrom: "dbFile",
53
+ hint: "数据目录下的库文件。只读,要改请编辑配置文件。"
52
54
  },
53
55
  {
54
56
  key: "html.dir",
55
- title: "HTML 产物目录名",
57
+ title: "HELP 产物目录",
56
58
  tier: "common",
57
59
  control: "text",
58
- hint: "数据目录下存放 HELP 页面的子目录名。"
60
+ readonly: true,
61
+ resolveFrom: "htmlDir",
62
+ hint: "HELP 页面的存放目录。只读,要改请编辑配置文件。"
59
63
  }
60
64
  ];
61
- /** 高级项:默认收起。改错了多半只是自己撞上麻烦,不确定就别动。 */
62
- const ADVANCED = [{
63
- key: "files.help",
64
- title: "产物文件名主体 · HELP",
65
- tier: "advanced",
66
- control: "text",
67
- hint: "HELP 文件名前缀,后面自动加时间戳。"
68
- }, {
69
- key: "lark.cliPath",
70
- title: "飞书 CLI 路径",
71
- tier: "advanced",
72
- control: "text",
73
- hint: "飞书 CLI 的完整路径。留空=自动探测。"
74
- }];
65
+ /** 高级项:#764 起已空(`files.help`/`lark.cliPath` 出配置表)——组还在,行没有。
66
+ * 空组不渲染(见 client.ts),行表长度即 3。 */
67
+ const ADVANCED = [];
75
68
  /** 设置页的行表,顺序即页面顺序。 */
76
69
  const CONFIG_ITEMS = [...COMMON, ...ADVANCED];
77
70
  /** 常用项行数(页面上直接画出来的那些)。 */
@@ -116,6 +109,11 @@ window.__ModuleLoader__.load({
116
109
  const RPC_ENDPOINT_CONFIG_GET = "config.get";
117
110
  const RPC_ENDPOINT_CONFIG_SAVE = "config.save";
118
111
  const RPC_ENDPOINT_CONFIG_RESET = "config.reset";
112
+ /** #706 配置体检端点:只读一次,回一份报告(判据由技能侧出,本包只透传,不重写一个字)。
113
+ *
114
+ * 端点名与技能侧那条只读命令同名(`<技能>.config.check`);报告形状的唯一真相在技能侧
115
+ * `packages/skill-<技能>/src/health.ts`,面板侧镜像在 `packages/plugin-manager/src/health-contract.ts`。 */
116
+ const RPC_ENDPOINT_CONFIG_CHECK = "config.check";
119
117
  /** 信封守卫:供 client 拆包前校验(port 拥有信封定义,守卫住 port)。 */
120
118
  function isRpcResult(raw) {
121
119
  if (typeof raw !== "object" || raw === null) return false;
@@ -300,15 +298,6 @@ window.__ModuleLoader__.load({
300
298
  if (needle === "") return entries;
301
299
  return entries.filter((entry) => entry.name.toLowerCase().includes(needle));
302
300
  }
303
- /** 给界面用的一句话位置说明:优先显示相对 home 的说法,省得整条绝对路径占满一行。 */
304
- function locationLabel(listing) {
305
- const home = listing.home.replace(/[\\/]+$/, "");
306
- if (home !== "" && listing.path !== home && listing.path.startsWith(home)) {
307
- const tail = listing.path.slice(home.length).replace(/^[\\/]+/, "");
308
- if (tail !== "") return "~" + (listing.path.includes("\\") ? "\\" : "/") + tail;
309
- }
310
- return listing.path;
311
- }
312
301
  /** 新建文件夹的名字合不合法(单个路径段:不许分隔符、不许空、不许 `.`/`..`)。
313
302
  *
314
303
  * 与宿主 browse 后端的校验同口径——这里先拦一道,是为了在图上给得出人话,不是替代宿主校验。 */
@@ -687,14 +676,6 @@ window.__ModuleLoader__.load({
687
676
  fontSize: 14,
688
677
  fontWeight: 700
689
678
  },
690
- headTail: {
691
- marginLeft: "auto",
692
- fontSize: 12,
693
- opacity: .72,
694
- overflow: "hidden",
695
- textOverflow: "ellipsis",
696
- whiteSpace: "nowrap"
697
- },
698
679
  crumbs: {
699
680
  display: "flex",
700
681
  flexWrap: "wrap",
@@ -724,10 +705,6 @@ window.__ModuleLoader__.load({
724
705
  padding: "0 12px 8px",
725
706
  fontSize: 12
726
707
  },
727
- rootsLabel: {
728
- opacity: .72,
729
- marginRight: 2
730
- },
731
708
  rootButton: {
732
709
  border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35))",
733
710
  borderRadius: 6,
@@ -836,14 +813,11 @@ window.__ModuleLoader__.load({
836
813
  });
837
814
  return react.createElement("div", { style: S$1.crumbs }, parts);
838
815
  }
839
- /** 可跳转的根那一行(Windows 上的盘符等)。**少于两个根就不画**:一个根没有「跳到别处」可言。 */
840
- function rootsRow(state, labels, onEnter) {
816
+ /** 可跳转的根那一行(Windows 上的盘符等):**就是本机全部磁盘**,故不加行首说明。
817
+ * **少于两个根就不画**:一个根没有「跳到别处」可言。 */
818
+ function rootsRow(state, onEnter) {
841
819
  if (state.roots.length < 2) return null;
842
- const parts = [react.createElement("span", {
843
- key: "roots-label",
844
- style: S$1.rootsLabel
845
- }, labels.roots)];
846
- for (const root of state.roots) parts.push(react.createElement("button", {
820
+ const parts = state.roots.map((root) => react.createElement("button", {
847
821
  key: root.path,
848
822
  type: "button",
849
823
  style: {
@@ -853,11 +827,7 @@ window.__ModuleLoader__.load({
853
827
  title: ROOT_KIND_TEXT[root.kind] ?? ROOT_KIND_TEXT.other,
854
828
  onClick: () => onEnter(root.path)
855
829
  }, root.path));
856
- return react.createElement("div", {
857
- style: S$1.roots,
858
- role: "group",
859
- "aria-label": labels.roots
860
- }, parts);
830
+ return react.createElement("div", { style: S$1.roots }, parts);
861
831
  }
862
832
  /** 一行子目录:点名字进它,点「选」把它当目标(「选」再点一次取消)。 */
863
833
  function entryRow(entry, selected, labels, onToggleSelect, onEnter) {
@@ -935,11 +905,10 @@ window.__ModuleLoader__.load({
935
905
  if (!open) return null;
936
906
  const rows = rowsOf(state);
937
907
  const target = targetOf(state);
938
- const listing = state.listing ?? EMPTY_LISTING;
939
- const hidden = hiddenCount(listing);
908
+ const hidden = hiddenCount(state.listing ?? EMPTY_LISTING);
940
909
  const body = [];
941
910
  body.push(crumbRow(state, props.onEnter));
942
- body.push(rootsRow(state, labels, props.onEnter));
911
+ body.push(rootsRow(state, props.onEnter));
943
912
  body.push(react.createElement("div", { style: S$1.pathRow }, react.createElement("input", {
944
913
  style: S$1.input,
945
914
  value: state.draft,
@@ -1006,12 +975,12 @@ window.__ModuleLoader__.load({
1006
975
  role: "dialog",
1007
976
  "aria-modal": true,
1008
977
  "aria-label": labels.title
1009
- }, react.createElement("div", { style: S$1.head }, react.createElement("span", { style: S$1.title }, labels.title), react.createElement("span", {
1010
- style: S$1.headTail,
1011
- title: listing.path
1012
- }, state.listing === null ? "" : locationLabel(listing)), react.createElement("button", {
978
+ }, react.createElement("div", { style: S$1.head }, react.createElement("span", { style: S$1.title }, labels.title), react.createElement("button", {
1013
979
  type: "button",
1014
- style: S$1.button,
980
+ style: {
981
+ ...S$1.button,
982
+ marginLeft: "auto"
983
+ },
1015
984
  "aria-label": labels.close,
1016
985
  title: labels.close,
1017
986
  onClick: props.onClose
@@ -1214,24 +1183,121 @@ window.__ModuleLoader__.load({
1214
1183
  function resetConfigSurface(call) {
1215
1184
  return configRpc(call, RPC_ENDPOINT_CONFIG_RESET, {});
1216
1185
  }
1217
- /** 把配置取值铺成「行键 → 输入框文本」(页面表单态;值缺项即空串,不返空留白)。
1218
- *
1219
- * `prefill` 是**落点回执**(配置面那几格解析出来的绝对路径):标了 `prefillFrom` 的行在取值空着时
1220
- * 直接显示那条绝对路径——用户不必自己拼路径(#743),显示的就是技能真会用的那个目录(逐字相同)。 */
1186
+ /** 读一次配置体检(只读;判据由技能侧出,本包只透传,不重写一个字)。 */
1187
+ function fetchHealthSurface(call) {
1188
+ return configRpc(call, RPC_ENDPOINT_CONFIG_CHECK, {});
1189
+ }
1190
+ /** 从体检报告里挑出 `lark.cli` 那一项;形状不对回 null(不抛)。 */
1191
+ function larkItemOf(report) {
1192
+ if (typeof report !== "object" || report === null) return null;
1193
+ const items = report.items;
1194
+ if (!Array.isArray(items)) return null;
1195
+ for (const it of items) {
1196
+ if (typeof it !== "object" || it === null) continue;
1197
+ const rec = it;
1198
+ if (rec["id"] !== "lark.cli") continue;
1199
+ if (typeof rec["id"] !== "string" || typeof rec["status"] !== "string" || typeof rec["message"] !== "string") return null;
1200
+ return {
1201
+ id: rec["id"],
1202
+ status: rec["status"],
1203
+ message: rec["message"],
1204
+ action: typeof rec["action"] === "string" ? rec["action"] : ""
1205
+ };
1206
+ }
1207
+ return null;
1208
+ }
1209
+ /** 飞书 CLI 官网(面板状态行与安装 prompt 里的同一地址,唯一定义地是这里)。 */
1210
+ const LARK_OFFICIAL_URL = "https://www.feishu.cn/feishu-cli";
1211
+ /** 状态行上逐字显示的那一行(三档都在、显示成文字、可点击跳转,不改写、不加话)。 */
1212
+ const LARK_OFFICIAL_LINE = "飞书CLI官网为:https://www.feishu.cn/feishu-cli";
1213
+ /** 「复制 prompt」按钮复制的内容(定稿住 #759 最新一条评论 v5 的逐字全文,两家的实施票都引它,
1214
+ * 不在本票里另写一份;官网指南 shape 变化时改 #759 那一条,这里跟着换)。 */
1215
+ const LARK_PROMPT_V5 = `在这台机器上安装并配置飞书 CLI(lark-cli)。你来推进,做完为止。
1216
+
1217
+ 必须满足的终态(每条都要真跑,把命令原文和输出贴回来):
1218
+ 1) lark-cli --version >= 1.0.82(更低的版本没有我们要用的 --no-wait)
1219
+ 2) where lark-cli(Windows)或 which lark-cli(macOS/Linux)能命中;Windows 上还要确认 %APPDATA%\\npm\\lark-cli.cmd 这个文件存在
1220
+ 3) lark-cli auth status 的输出里有 identities.user.openId
1221
+ 4) lark-cli auth check --scope task 退出码 0(连退出码一起贴)—— 备忘录要用任务域
1222
+ 5) lark-cli calendar +agenda 退出码 0(连退出码一起贴)—— 作息管家要读你的主日历
1223
+ 6) 技能侧也能用:跑一次作息管家的「飞书探测」(=一条不落地的同步探测),不报「缺 CLI/未登录/日历不可达」
1224
+
1225
+ 怎么做:
1226
+ 7) Node.js/npm 不可用就先装。
1227
+ 8) 安装:npm install -g @larksuite/cli。只装这个官方包(bin 名 lark-cli);不要装 npm 上的 lark-cli。
1228
+ 9) 官方指南里的第二件事也照做:npx -y skills add https://open.feishu.cn --skill -y
1229
+ 10) 配置与登录照这两条走(都是 CLI 自己在 --help 里写给 AI agent 的写法,别改写成别的):
1230
+ 先初始化:lark-cli config init --new
1231
+ 它会阻塞等你完成浏览器授权,所以起在后台、从它的输出里把验证链接捞出来
1232
+ (CLI 原文:Run it in the background and retrieve the verification URL from its output);
1233
+ 链接给我,我批准后你继续。
1234
+ 如果它报 Inside an Agent context 之类被拒,照它的提示改用 lark-cli config bind。
1235
+ 再授权:lark-cli auth login --no-wait --json --domain calendar,task
1236
+ 它会立刻返回一个验证链接(或二维码);把链接给我,我批准后你再用
1237
+ lark-cli auth login --device-code <拿到的 device code> 收尾。
1238
+ 官方指南:https://open.feishu.cn/document/no_class/mcp-archive/feishu-cli-installation-guide.md
1239
+ 子命令以 lark-cli auth --help / config --help 和官方文档为准,不要凭猜;做完要能过第 3、4、5 条。
1240
+ 11) npm 全局目录如果不在系统 PATH 上,把它加进系统 PATH(不是只在当前终端 export)。
1241
+
1242
+ 怎么推进:
1243
+ 12) 除第 13 条那三类,其余你自己一口气做完,不要逐步问我确认:安装、改 PATH、重试、修报错都自己做完。
1244
+ 13) 只有这三类必须我介入;遇到就用 /wizard 生成脚本带我走,走完你接着自动往下做:
1245
+ ① 浏览器里的初始化授权 / 登录(把验证链接或二维码给我);
1246
+ ② 系统权限提示(管理员/sudo);
1247
+ ③ Node.js 需要我先定怎么装。
1248
+ 14) 任何一步失败:自己按报错修;修不动再一次性告诉我「卡在哪、报错原文、需要我做什么」。
1249
+
1250
+ 参考官网:https://www.feishu.cn/feishu-cli`;
1251
+ /** 复制一段文本:先剪贴板,不成再走兜底(永不抛,回是否成功)。 */
1252
+ async function copyText(text, deps = {}) {
1253
+ if (deps.clipboard !== void 0) try {
1254
+ await deps.clipboard.writeText(text);
1255
+ return true;
1256
+ } catch {}
1257
+ if (deps.execCopy !== void 0) try {
1258
+ return deps.execCopy(text);
1259
+ } catch {
1260
+ return false;
1261
+ }
1262
+ return false;
1263
+ }
1264
+ /** 当刻环境的剪贴板(经 `globalThis` 懒取:顶层不碰 DOM,loader 沙箱与 node 单测都不炸;拿不到回 null)。 */
1265
+ function clipboardOf() {
1266
+ try {
1267
+ return globalThis.navigator?.clipboard ?? null;
1268
+ } catch {
1269
+ return null;
1270
+ }
1271
+ }
1272
+ /** 只读行显示什么:标了 `resolveFrom` ⇒ 回执 `resolved` 组那一格;没标 ⇒ 配置文件里那个值本身
1273
+ * (数字类只读项走这一档:显示的是生效数字,见 #749 补注二的甲档)。 */
1274
+ function readonlyTextOf(item, raw, prefill) {
1275
+ if (item.resolveFrom === void 0) return raw;
1276
+ const shown = prefill.resolved?.[item.resolveFrom];
1277
+ return typeof shown === "string" ? shown : "";
1278
+ }
1221
1279
  function toDraft(values, prefill = {}) {
1222
1280
  const draft = {};
1223
1281
  for (const item of CONFIG_ITEMS) {
1224
1282
  const v = readPath(values, item.key);
1225
1283
  const raw = v === void 0 || v === null ? "" : String(v);
1284
+ if (item.readonly === true) {
1285
+ draft[item.key] = readonlyTextOf(item, raw, prefill);
1286
+ continue;
1287
+ }
1226
1288
  const fallback = item.prefillFrom === void 0 ? void 0 : prefill[item.prefillFrom];
1227
1289
  draft[item.key] = raw === "" && typeof fallback === "string" && fallback !== "" ? fallback : raw;
1228
1290
  }
1229
1291
  return draft;
1230
1292
  }
1231
- /** 表单态 → 配置取值(按控件种类还原类型;空串对文本项照收,语义由「未配」承担)。 */
1293
+ /** 表单态 → 配置取值(按控件种类还原类型;空串对文本项照收,语义由「按默认落点」承担)。
1294
+ *
1295
+ * **只读行不收**(#764,照 #749 样板):它们显示的是技能算好的绝对路径,写回配置就是把「显示的路径」
1296
+ * 当成「配置值」——保存只提交真能改的那些行(本家=`db.dir` 一行),其余键由技能侧做组内合并保留现值。 */
1232
1297
  function fromDraft(draft) {
1233
1298
  const values = {};
1234
1299
  for (const item of CONFIG_ITEMS) {
1300
+ if (item.readonly === true) continue;
1235
1301
  const raw = draft[item.key] ?? "";
1236
1302
  const value = item.control === "number" ? Number(raw) : item.control === "switch" ? raw === "true" : raw;
1237
1303
  writePath(values, item.key, item.control === "number" && !Number.isFinite(value) ? 0 : value);
@@ -1290,31 +1356,38 @@ window.__ModuleLoader__.load({
1290
1356
  /** 一行输入(四种控件对齐受限 YAML 子集:文本/数字/布尔/目录)。
1291
1357
  *
1292
1358
  * 目录档=文本框 + 一枚唤起系统文件夹选择器的按钮;`onBrowse` 缺席(命名空间拿不到、
1293
- * 或这条路已被拒)时不画按钮,文本框照旧——那是该缝自己的契约(供不了就收起入口,不是失败)。 */
1359
+ * 或这条路已被拒)时不画按钮,文本框照旧——那是该缝自己的契约(供不了就收起入口,不是失败)。
1360
+ *
1361
+ * **只读行(#764,照 #749 样板)**:`item.readonly` 为真时控件一律 `disabled`(值只经技能侧解析后显示、
1362
+ * 不给改),目录行的浏览按钮**保留但不可点击**(#747 定稿:入口不作废,只是不能改)。控件文案**不出现省略号**
1363
+ * (#746 处置 9)——按钮就写「选择文件夹」/「浏览」两个字面。 */
1294
1364
  function Row(props) {
1295
1365
  const { item } = props;
1296
- const onChange = (e) => props.onChange(item.key, e.target.value);
1366
+ const readonly = item.readonly === true;
1367
+ const disabled = props.disabled || readonly;
1368
+ const onChange = readonly ? void 0 : (e) => props.onChange(item.key, e.target.value);
1297
1369
  const control = item.control === "switch" ? react.createElement("input", {
1298
1370
  type: "checkbox",
1299
1371
  checked: props.value === "true",
1300
- disabled: props.disabled,
1301
- onChange: (e) => props.onChange(item.key, e.target.checked ? "true" : "false")
1372
+ disabled,
1373
+ onChange: readonly ? void 0 : (e) => props.onChange(item.key, e.target.checked ? "true" : "false")
1302
1374
  }) : react.createElement("input", {
1303
1375
  style: S.input,
1304
1376
  type: item.control === "number" ? "number" : "text",
1305
1377
  value: props.value,
1306
- disabled: props.disabled,
1378
+ disabled,
1307
1379
  spellCheck: false,
1308
1380
  onChange
1309
1381
  });
1310
- /** 三态入口:供不了(`none`/缺席)就不画,不摆一个点了没反应的死按钮。 */
1382
+ /** 三态入口:供不了(`none`/缺席)就不画,不摆一个点了没反应的死按钮。
1383
+ * 只读行照画(按钮保留),但 `disabled` ⇒ 点不动。 */
1311
1384
  const entry = props.browser ?? null;
1312
1385
  const browse = item.control === "directory" && entry !== null && entry.mode !== "none" ? react.createElement("button", {
1313
1386
  style: S.btnPick,
1314
1387
  type: "button",
1315
- disabled: props.disabled,
1388
+ disabled,
1316
1389
  onClick: () => entry.onOpen(item.key)
1317
- }, entry.mode === "native" ? "选择文件夹…" : "浏览…") : null;
1390
+ }, entry.mode === "native" ? "选择文件夹" : "浏览") : null;
1318
1391
  return react.createElement("div", { style: S.row }, react.createElement("div", { style: S.label }, item.title), react.createElement("div", { style: S.hint }, item.hint), browse === null ? control : react.createElement("div", { style: S.pickRow }, control, browse));
1319
1392
  }
1320
1393
  /** 技能设置页:承载作息管家自己的全部可配置项(只配置,不干活)。 */
@@ -1326,6 +1399,10 @@ window.__ModuleLoader__.load({
1326
1399
  const [busy, setBusy] = react.useState(false);
1327
1400
  const [notice, setNotice] = react.useState(null);
1328
1401
  const [error, setError] = react.useState(null);
1402
+ /** 飞书 CLI 状态行(三档读数由技能侧体检出,面板只显示;`null` 以外的三态见下)。 */
1403
+ const [lark, setLark] = react.useState({ kind: "loading" });
1404
+ /** 复制 prompt 那枚按钮的回执(idle=没点过)。 */
1405
+ const [copied, setCopied] = react.useState("idle");
1329
1406
  const apply = react.useCallback((surface) => {
1330
1407
  setState({
1331
1408
  kind: "ready",
@@ -1333,6 +1410,25 @@ window.__ModuleLoader__.load({
1333
1410
  });
1334
1411
  setDraft(toDraft(surface.values, surface));
1335
1412
  }, []);
1413
+ const loadHealth = react.useCallback(async () => {
1414
+ setLark({ kind: "loading" });
1415
+ const r = await fetchHealthSurface(props.getCall());
1416
+ if (!r.ok) {
1417
+ setLark({
1418
+ kind: "failed",
1419
+ message: r.message
1420
+ });
1421
+ return;
1422
+ }
1423
+ const item = larkItemOf(r.surface);
1424
+ setLark(item === null ? {
1425
+ kind: "failed",
1426
+ message: "体检回执里没有飞书 CLI 那一项"
1427
+ } : {
1428
+ kind: "ready",
1429
+ item
1430
+ });
1431
+ }, [props.getCall]);
1336
1432
  const load = react.useCallback(async () => {
1337
1433
  setNotice(null);
1338
1434
  setError(null);
@@ -1342,7 +1438,12 @@ window.__ModuleLoader__.load({
1342
1438
  kind: "failed",
1343
1439
  message: r.message
1344
1440
  });
1345
- }, [apply, props.getCall]);
1441
+ await loadHealth();
1442
+ }, [
1443
+ apply,
1444
+ loadHealth,
1445
+ props.getCall
1446
+ ]);
1346
1447
  react.useEffect(() => {
1347
1448
  let alive = true;
1348
1449
  (async () => {
@@ -1353,6 +1454,22 @@ window.__ModuleLoader__.load({
1353
1454
  kind: "failed",
1354
1455
  message: r.message
1355
1456
  });
1457
+ const h = await fetchHealthSurface(props.getCall());
1458
+ if (!alive) return;
1459
+ if (!h.ok) setLark({
1460
+ kind: "failed",
1461
+ message: h.message
1462
+ });
1463
+ else {
1464
+ const item = larkItemOf(h.surface);
1465
+ setLark(item === null ? {
1466
+ kind: "failed",
1467
+ message: "体检回执里没有飞书 CLI 那一项"
1468
+ } : {
1469
+ kind: "ready",
1470
+ item
1471
+ });
1472
+ }
1356
1473
  })();
1357
1474
  return () => {
1358
1475
  alive = false;
@@ -1417,7 +1534,10 @@ window.__ModuleLoader__.load({
1417
1534
  onOpen: onOpenRow
1418
1535
  };
1419
1536
  const surface = state.kind === "ready" ? state.surface : null;
1420
- const dirty = surface !== null && CONFIG_ITEMS.some((i) => (draft[i.key] ?? "") !== (toDraft(surface.values, surface)[i.key] ?? ""));
1537
+ /** 脏值只看**可改行**(#764,照 #749 样板):只读行显示的是技能算好的绝对路径,
1538
+ * 拿它当「改动」会让打开面板就变「未保存」。 */
1539
+ const editableItems = CONFIG_ITEMS.filter((i) => i.readonly !== true);
1540
+ const dirty = surface !== null && editableItems.some((i) => (draft[i.key] ?? "") !== (toDraft(surface.values, surface)[i.key] ?? ""));
1421
1541
  /** 写完之后重新读一份:写回执只有 {path, values},头部那两行要的是读整面。 */
1422
1542
  const writeThenReload = react.useCallback(async (done) => {
1423
1543
  setBusy(true);
@@ -1455,8 +1575,8 @@ window.__ModuleLoader__.load({
1455
1575
  if (r.ok) await writeThenReload("已重置为默认(原配置已另存一份 .bak)");
1456
1576
  else setError(r.message);
1457
1577
  }, [props.getCall, writeThenReload]);
1458
- const head = react.createElement("div", null, react.createElement("div", { style: S.title }, `${SLOT_TITLE} · 配置`), surface !== null ? react.createElement("div", null, react.createElement("div", { style: S.info }, `配置文件 ${surface.path}`), react.createElement("div", { style: S.info }, `数据目录 ${surface.dataDir}`), surface.created ? react.createElement("div", { style: S.muted }, "(配置文件刚按默认值生成)") : null) : null);
1459
- if (state.kind === "loading") return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.muted }, "配置读取中…"));
1578
+ const head = react.createElement("div", null, react.createElement("div", { style: S.title }, `${SLOT_TITLE} · 配置`), surface !== null ? react.createElement("div", null, react.createElement("div", { style: S.info }, `配置文件 ${surface.path}`), react.createElement("div", { style: S.info }, `数据目录 ${surface.resolved?.dbDir ?? surface.dataDir}`), surface.created ? react.createElement("div", { style: S.muted }, "(配置文件刚按默认值生成)") : null) : null);
1579
+ if (state.kind === "loading") return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.muted }, "配置读取中"));
1460
1580
  if (state.kind === "failed") return react.createElement("div", { style: S.card }, react.createElement("div", { style: S.title }, `${SLOT_TITLE} · 配置`), react.createElement("div", { style: S.error }, state.message), react.createElement("div", { style: S.bar }, react.createElement("button", {
1461
1581
  style: S.btn,
1462
1582
  type: "button",
@@ -1476,12 +1596,42 @@ window.__ModuleLoader__.load({
1476
1596
  onChange,
1477
1597
  browser: rowEntry
1478
1598
  });
1479
- return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.rows }, common.map(renderRow)), react.createElement("details", { style: S.advanced }, react.createElement("summary", { style: S.summary }, ADVANCED_GROUP_TITLE), react.createElement("div", { style: S.muted }, ADVANCED_GROUP_NOTE), advanced.map(renderRow)), react.createElement("div", { style: S.bar }, react.createElement("button", {
1599
+ /** 「复制 prompt」:剪贴板不在就报失败,不抛(按钮三档都在,见 #761 定稿)。 */
1600
+ const onCopyPrompt = react.useCallback(async () => {
1601
+ const ok = await copyText(LARK_PROMPT_V5, { clipboard: clipboardOf() ?? void 0 });
1602
+ setCopied(ok ? "done" : "failed");
1603
+ }, []);
1604
+ /** 「飞书 CLI」状态行(#764,定稿 #761):不是配置项——三档读数由技能侧体检出,面板只显示;
1605
+ * 三档都有「复制 prompt」按钮与官网那一行(逐字、可点击)。 */
1606
+ const renderLark = () => {
1607
+ if (lark.kind === "loading") return react.createElement("div", { style: S.rows }, react.createElement("div", { style: S.label }, "飞书 CLI"), react.createElement("div", { style: S.muted }, "飞书 CLI 检测中"));
1608
+ if (lark.kind === "failed") return react.createElement("div", { style: S.rows }, react.createElement("div", { style: S.label }, "飞书 CLI"), react.createElement("div", { style: S.error }, lark.message), react.createElement("div", { style: S.bar }, react.createElement("button", {
1609
+ style: S.btn,
1610
+ type: "button",
1611
+ onClick: () => void loadHealth()
1612
+ }, "重新检测")));
1613
+ const item = lark.item;
1614
+ const tone = item.status === "green" ? S.okText : item.status === "red" ? S.error : S.muted;
1615
+ return react.createElement("div", { style: S.rows }, react.createElement("div", { style: S.label }, "飞书 CLI"), react.createElement("div", { style: tone }, item.message), item.action === "" ? null : react.createElement("div", { style: S.hint }, item.action), react.createElement("div", { style: S.bar }, react.createElement("button", {
1616
+ style: S.btn,
1617
+ type: "button",
1618
+ onClick: () => void onCopyPrompt()
1619
+ }, "复制 prompt"), react.createElement("button", {
1620
+ style: S.btn,
1621
+ type: "button",
1622
+ onClick: () => void loadHealth()
1623
+ }, "重新检测")), copied === "done" ? react.createElement("div", { style: S.okText }, "已复制安装指引") : copied === "failed" ? react.createElement("div", { style: S.error }, "复制失败:剪贴板不可用,请手动复制对话框里的安装指引") : null, react.createElement("div", { style: S.info }, react.createElement("a", {
1624
+ href: LARK_OFFICIAL_URL,
1625
+ target: "_blank",
1626
+ rel: "noreferrer"
1627
+ }, LARK_OFFICIAL_LINE)));
1628
+ };
1629
+ return react.createElement("div", { style: S.card }, head, react.createElement("div", { style: S.rows }, common.map(renderRow)), advanced.length === 0 ? null : react.createElement("details", { style: S.advanced }, react.createElement("summary", { style: S.summary }, ADVANCED_GROUP_TITLE), react.createElement("div", { style: S.muted }, ADVANCED_GROUP_NOTE), advanced.map(renderRow)), renderLark(), react.createElement("div", { style: S.bar }, react.createElement("button", {
1480
1630
  style: dirty ? S.btnPrimary : S.btn,
1481
1631
  type: "button",
1482
1632
  disabled: busy || !dirty,
1483
1633
  onClick: () => void onSave()
1484
- }, busy ? "处理中…" : "保存"), react.createElement("button", {
1634
+ }, busy ? "处理中" : "保存"), react.createElement("button", {
1485
1635
  style: S.btn,
1486
1636
  type: "button",
1487
1637
  disabled: busy,
@@ -1502,7 +1652,7 @@ window.__ModuleLoader__.load({
1502
1652
  go: "转到",
1503
1653
  showHidden: (n) => "显示隐藏目录(" + n + ")",
1504
1654
  empty: "这个目录里没有子目录。",
1505
- loading: "正在读取…",
1655
+ loading: "正在读取",
1506
1656
  newFolder: "新建文件夹",
1507
1657
  createConfirm: "创建",
1508
1658
  createCancel: "取消",
@@ -1510,8 +1660,7 @@ window.__ModuleLoader__.load({
1510
1660
  selected: "已选",
1511
1661
  open: "选定这个目录",
1512
1662
  cancel: "取消",
1513
- willPick: "将选定:",
1514
- roots: "其他磁盘:"
1663
+ willPick: "将选定:"
1515
1664
  }
1516
1665
  }) : null, notice !== null ? react.createElement("div", { style: S.okText }, notice) : null, error !== null ? react.createElement("div", { style: S.error }, error) : null);
1517
1666
  }
@@ -1536,16 +1685,23 @@ window.__ModuleLoader__.load({
1536
1685
  })));
1537
1686
  }
1538
1687
  //#endregion
1688
+ exports.LARK_OFFICIAL_LINE = LARK_OFFICIAL_LINE;
1689
+ exports.LARK_OFFICIAL_URL = LARK_OFFICIAL_URL;
1690
+ exports.LARK_PROMPT_V5 = LARK_PROMPT_V5;
1539
1691
  exports.READ_TIMEOUT_MS = READ_TIMEOUT_MS;
1540
1692
  exports.Row = Row;
1541
1693
  exports.apply = apply;
1694
+ exports.clipboardOf = clipboardOf;
1695
+ exports.copyText = copyText;
1542
1696
  exports.createBrowseHandler = createBrowseHandler;
1543
1697
  exports.directoryEntryMode = directoryEntryMode;
1544
1698
  exports.fetchConfigSurface = fetchConfigSurface;
1699
+ exports.fetchHealthSurface = fetchHealthSurface;
1545
1700
  exports.fromDraft = fromDraft;
1546
1701
  exports.humanizeConfigFailure = humanizeConfigFailure;
1547
1702
  exports.inject = inject;
1548
1703
  exports.isDirectoryPicker = isDirectoryPicker;
1704
+ exports.larkItemOf = larkItemOf;
1549
1705
  exports.openRowBrowser = openRowBrowser;
1550
1706
  exports.pickDirectory = pickDirectory;
1551
1707
  exports.pickerModeOf = pickerModeOf;