@shgroup/dsh-serenity-hooks 1.84.0 → 1.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.84.0",
3
+ "version": "1.86.0",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -227,7 +227,7 @@ async function listCccs(ctx, opts = {}) {
227
227
  if (typeof defaultRoot === "string" && defaultRoot !== "" && !c.roots.includes(defaultRoot)) c.roots.unshift(defaultRoot);
228
228
  let rolesOf = null;
229
229
  if (opts.withRoles === true) {
230
- const { listTrajectoryConstraints } = await import("./profile-CiRWh2a2.js").then((n) => n.i);
230
+ const { listTrajectoryConstraints } = await import("./profile-vHheCJiU.js").then((n) => n.a);
231
231
  rolesOf = (root) => {
232
232
  try {
233
233
  return listTrajectoryConstraints(root).map((p) => p.code);
package/lib/client.js CHANGED
@@ -3810,6 +3810,25 @@ Instruction-following style:
3810
3810
  const [feishuName, setFeishuName] = (0, react.useState)("");
3811
3811
  const [feishuBusy, setFeishuBusy] = (0, react.useState)(false);
3812
3812
  const [feishuNotice, setFeishuNotice] = (0, react.useState)(null);
3813
+ /**
3814
+ * 添加账号的**渠道二选一**(owner 2026-10-21:「不对称,交互上要体现二选一」)。
3815
+ *
3816
+ * ## 改之前为什么"不对称"(这是真问题,不是审美)
3817
+ *
3818
+ * 微信与飞书各是**一张常驻卡片**,并且飞书那张的**三格表单一直展开**:
3819
+ * · 视觉上像是"**两件事都要填**",而实际是二选一;
3820
+ * · 微信只有一个按钮,飞书有一整块输入区 ⇒ 两边**分量不等**,读起来像"微信是次要的";
3821
+ * · 空着两格 App ID/Secret 长期占版面,把真正的动作(扫码)挤到上面去。
3822
+ *
3823
+ * ## 改成什么
3824
+ *
3825
+ * **一个「添加账号」入口 + 渠道切换**:先选渠道,再只显示该渠道的一条路径。
3826
+ * `null` = 还没选(**初始收起**,不默认展开任何一家 —— 默认展开就等于替用户做了选择)。
3827
+ *
3828
+ * ⚠️ **为什么仍是同一个块的两种形态,而不是两个并列卡片**:渠道是**互斥**的
3829
+ * (一个账号只属于一家),并列会把"互斥"读成"并列"。
3830
+ */
3831
+ const [addChannel, setAddChannel] = (0, react.useState)(null);
3813
3832
  (0, react.useEffect)(() => {
3814
3833
  let alive = true;
3815
3834
  (async () => {
@@ -4208,49 +4227,147 @@ Instruction-following style:
4208
4227
  })
4209
4228
  })]
4210
4229
  }) }, a.accountId)),
4211
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4212
- className: "ss-rowCard",
4213
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4214
- className: "ss-rowText",
4215
- children: [
4216
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4217
- className: "ss-rowName",
4218
- children: "扫码绑定微信"
4219
- }),
4220
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
4231
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4232
+ className: "ss-rowCard",
4233
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4234
+ className: "ss-rowText",
4235
+ children: [
4236
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4237
+ className: "ss-rowName",
4238
+ children: "添加账号"
4239
+ }),
4240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4241
+ className: "ss-rowDesc",
4242
+ children: [
4243
+ addChannel === null && "选一个渠道,再按它的方式绑定(一个账号只属于一家)",
4244
+ addChannel === "weixin" && "微信:手机「扫一扫」确认——每扫一次绑定一个新账号",
4245
+ addChannel === "feishu" && "飞书:填企业自建应用的 App ID 与 App Secret——每提交一次新增一个账号"
4246
+ ]
4247
+ }),
4248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4249
+ className: "ss-help",
4250
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4251
+ type: "button",
4252
+ className: "ss-helpMark",
4253
+ "aria-label": "添加账号帮助",
4254
+ children: "?"
4255
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4256
+ className: "ss-helpTip",
4257
+ children: "两个渠道二选一,绑定方式不同:\n· 微信:走腾讯 iLink 开放协议(个人微信 Bot API),扫码确认,不需要平台审核\n· 飞书:走企业自建应用,需要 App ID 与 App Secret(开放平台 → 开发者后台 → 凭证与基础信息)\n· 两者可以同时存在,但**每个账号只属于一家**;账号、路由、开关互不影响\n· 每绑定一次新增一个账号,各自独立连接"
4258
+ })]
4259
+ })
4260
+ ]
4261
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4262
+ className: "ss-rowControl",
4263
+ children: addChannel !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4264
+ type: "button",
4265
+ className: "ss-removeBtn",
4266
+ onClick: () => {
4267
+ setAddChannel(null);
4268
+ setFeishuNotice(null);
4269
+ },
4270
+ children: "收起"
4271
+ })
4272
+ })]
4273
+ }),
4274
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4275
+ className: "ss-channelPicker",
4276
+ role: "radiogroup",
4277
+ "aria-label": "选择添加渠道",
4278
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4279
+ type: "button",
4280
+ role: "radio",
4281
+ "aria-checked": addChannel === "weixin",
4282
+ className: `ss-channelOption${addChannel === "weixin" ? " ss-channelOptionOn" : ""}`,
4283
+ onClick: () => {
4284
+ setAddChannel("weixin");
4285
+ setFeishuNotice(null);
4286
+ },
4287
+ children: "微信"
4288
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4289
+ type: "button",
4290
+ role: "radio",
4291
+ "aria-checked": addChannel === "feishu",
4292
+ className: `ss-channelOption${addChannel === "feishu" ? " ss-channelOptionOn" : ""}`,
4293
+ onClick: () => {
4294
+ setAddChannel("feishu");
4295
+ setFeishuNotice(null);
4296
+ },
4297
+ children: "飞书"
4298
+ })]
4299
+ }),
4300
+ addChannel === "weixin" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4301
+ className: "ss-channelBody",
4302
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4303
+ className: "ss-rowText",
4304
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4221
4305
  className: "ss-rowDesc",
4222
4306
  children: [
4223
- loginPhase === "idle" && "微信「扫一扫」确认——每扫一次绑定一个新账号",
4307
+ loginPhase === "idle" && "点击后用手机微信「扫一扫」确认",
4224
4308
  loginPhase === "waiting" && "二维码已生成(5 分钟有效)——微信扫一扫 → 手机确认",
4225
4309
  loginPhase === "confirmed" && (loginNotice ?? "绑定成功"),
4226
4310
  loginPhase === "error" && (loginError ?? "绑定失败"),
4227
4311
  loginPhase === "expired" && "二维码已过期,请重新获取"
4228
4312
  ]
4313
+ })
4314
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4315
+ className: "ss-rowControl",
4316
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4317
+ type: "button",
4318
+ className: "ss-loginBtn",
4319
+ disabled: loginPhase === "waiting",
4320
+ onClick: () => void startLogin(),
4321
+ children: loginPhase === "waiting" ? "等待扫码…" : "扫码绑定"
4322
+ })
4323
+ })]
4324
+ }),
4325
+ addChannel === "feishu" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4326
+ className: "ss-channelBody ss-channelBodyCol",
4327
+ children: [
4328
+ feishuNotice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4329
+ className: "ss-rowDesc",
4330
+ children: feishuNotice
4229
4331
  }),
4230
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4231
- className: "ss-help",
4232
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4332
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4333
+ className: "ss-feishuForm",
4334
+ children: [
4335
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4336
+ className: "ss-input",
4337
+ type: "text",
4338
+ placeholder: "App ID(cli_xxx)",
4339
+ value: feishuAppId,
4340
+ onChange: (e) => setFeishuAppId(e.target.value)
4341
+ }),
4342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4343
+ className: "ss-input",
4344
+ type: "password",
4345
+ placeholder: "App Secret",
4346
+ value: feishuAppSecret,
4347
+ onChange: (e) => setFeishuAppSecret(e.target.value)
4348
+ }),
4349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4350
+ className: "ss-input",
4351
+ type: "text",
4352
+ placeholder: "账号名(可选,如「家庭群机器人」)",
4353
+ value: feishuName,
4354
+ onChange: (e) => setFeishuName(e.target.value)
4355
+ })
4356
+ ]
4357
+ }),
4358
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4359
+ className: "ss-channelActions",
4360
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4233
4361
  type: "button",
4234
- className: "ss-helpMark",
4235
- "aria-label": "扫码绑定帮助",
4236
- children: "?"
4237
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4238
- className: "ss-helpTip",
4239
- children: "扫码绑定微信账号。\n· 走腾讯 iLink 开放协议(个人微信 Bot API)\n· 步骤:生成二维码 → 手机微信「扫一扫」→ 手机上确认\n· 每扫一次绑定一个新账号,可多账号,各自独立轮询\n· 二维码 5 分钟有效;不需要平台审核"
4240
- })]
4362
+ className: "ss-loginBtn",
4363
+ disabled: feishuBusy || feishuAppId.trim() === "" || feishuAppSecret.trim() === "",
4364
+ onClick: () => void addFeishuAccount(),
4365
+ children: feishuBusy ? "添加中…" : "添加飞书账号"
4366
+ })
4241
4367
  })
4242
4368
  ]
4243
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4244
- className: "ss-rowControl",
4245
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4246
- type: "button",
4247
- className: "ss-loginBtn",
4248
- disabled: loginPhase === "waiting",
4249
- onClick: () => void startLogin(),
4250
- children: loginPhase === "waiting" ? "等待扫码…" : "扫码绑定"
4251
- })
4252
- })]
4253
- }) }),
4369
+ })
4370
+ ] }),
4254
4371
  loginPhase === "waiting" && loginQrSvg !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4255
4372
  className: "ss-rowCard ss-qrRow",
4256
4373
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -4264,68 +4381,6 @@ Instruction-following style:
4264
4381
  })
4265
4382
  })]
4266
4383
  }) }),
4267
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4268
- className: "ss-rowCard",
4269
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4270
- className: "ss-rowText",
4271
- children: [
4272
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4273
- className: "ss-rowName",
4274
- children: "添加飞书账号"
4275
- }),
4276
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4277
- className: "ss-rowDesc",
4278
- children: feishuNotice ?? "填企业自建应用的 App ID 与 App Secret——每提交一次新增一个账号"
4279
- }),
4280
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4281
- className: "ss-help",
4282
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4283
- type: "button",
4284
- className: "ss-helpMark",
4285
- "aria-label": "飞书绑定帮助",
4286
- children: "?"
4287
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4288
- className: "ss-helpTip",
4289
- children: "添加飞书账号(企业自建应用)。\n· 飞书开放平台 → 开发者后台 → 你的应用 → 「凭证与基础信息」\n· 复制 App ID 与 App Secret 填在下面\n· 事件订阅选「长连接」并订阅 im.message.receive_v1\n· 每提交一次新增一个账号,各自独立连接\n· 与微信并列:账号、路由、开关互不影响"
4290
- })]
4291
- })
4292
- ]
4293
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4294
- className: "ss-rowControl",
4295
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4296
- type: "button",
4297
- className: "ss-loginBtn",
4298
- disabled: feishuBusy || feishuAppId.trim() === "" || feishuAppSecret.trim() === "",
4299
- onClick: () => void addFeishuAccount(),
4300
- children: feishuBusy ? "添加中…" : "添加飞书账号"
4301
- })
4302
- })]
4303
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4304
- className: "ss-feishuForm",
4305
- children: [
4306
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4307
- className: "ss-input",
4308
- type: "text",
4309
- placeholder: "App ID(cli_xxx)",
4310
- value: feishuAppId,
4311
- onChange: (e) => setFeishuAppId(e.target.value)
4312
- }),
4313
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4314
- className: "ss-input",
4315
- type: "password",
4316
- placeholder: "App Secret",
4317
- value: feishuAppSecret,
4318
- onChange: (e) => setFeishuAppSecret(e.target.value)
4319
- }),
4320
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4321
- className: "ss-input",
4322
- type: "text",
4323
- placeholder: "账号名(可选,如「家庭群机器人」)",
4324
- value: feishuName,
4325
- onChange: (e) => setFeishuName(e.target.value)
4326
- })
4327
- ]
4328
- })] }),
4329
4384
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4330
4385
  className: "ss-rowCard",
4331
4386
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -4386,7 +4441,7 @@ Instruction-following style:
4386
4441
  }
4387
4442
  //#endregion
4388
4443
  //#region \0sp-css:src/client/SettingsSection
4389
- const css$2 = "/* SettingsSection — dsh 原生设置面板 serenity-hooks 简单配置页\n v1.21.1 看齐 dsh 自身:官方 settings 设计语言——\n section > title + intro + group(groupTitle) + rowCard 行卡;\n 全部 --dsw-alias-* token(与 ui-settings-models 同词汇表)。 */\n\n/* 页面容器(官方 .section:flex column gap 12) */\n.ss-section {\n display: flex;\n flex-direction: column;\n gap: 14px;\n max-width: 720px;\n color: var(--dsw-alias-label-primary);\n}\n\n/* 页面标题(官方 .title:16px/24 500) */\n.ss-title {\n margin: 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n/* 页面说明(官方 .intro:14px/22 tertiary) */\n.ss-intro {\n margin: 0;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n/* 分组(多级标题:分组标题 + 行卡列表) */\n.ss-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n/* 分组标题(二级:12px/18 500 secondary——官方 fieldLabel 风格) */\n.ss-groupTitle {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* 行卡列表(官方 .rows:无 marker,gap 8) */\n.ss-rows {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n/* 行卡(官方 .rowCard:border-l2 r12 padding 12/14) */\n.ss-rowCard {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n padding: 12px 14px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n}\n\n/* ── v1.29 需求②:可展开行(整行点击 → 行内详设)── */\n.ss-expandable {\n flex-wrap: wrap; /* detail 换行占满整行 */\n cursor: default;\n}\n.ss-expandable .ss-rowText {\n cursor: pointer;\n}\n.ss-expandable .ss-rowText:hover .ss-rowName {\n color: var(--dsw-alias-state-success-primary, #22c55e);\n}\n.ss-expanded {\n border-color: var(--dsw-alias-state-success-primary, #22c55e);\n}\n.ss-expandMark {\n display: inline-block;\n margin-left: 6px;\n font-size: 10px;\n color: var(--dsw-alias-label-tertiary);\n transition: transform 0.15s ease;\n}\n.ss-expanded .ss-expandMark {\n transform: rotate(90deg);\n}\n.ss-rowDetail {\n flex-basis: 100%;\n width: 100%;\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin-top: 10px;\n padding-top: 12px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n min-width: 0;\n}\n.ss-detailStack {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.ss-detailIntro {\n margin: 0;\n font-size: 12px;\n line-height: 19px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-line;\n word-break: break-word;\n overflow-wrap: anywhere;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.06));\n}\n\n/* 行卡文本(标题 + 说明 左列) */\n.ss-rowText {\n display: flex;\n flex-direction: column;\n gap: 3px;\n min-width: 0;\n flex: 1;\n}\n\n/* 行卡标题(官方 .rowName:14px/22 500) */\n.ss-rowName {\n font-size: 14px;\n line-height: 22px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n/* IM 桥:账号行的**渠道标签**(2026-10-19 泛化后新增)。\n 为什么是标签而不是\"把渠道拼进 id\":渠道是账号的属性,不是名字的一部分;\n 拼进去会让「同一个账号换了渠道」看起来像另一个账号。\n 🔴 不得定宽(v1.72.0 那次 `sp-label` 写死 76px 压住数值的教训)⇒ inline-block + 自适应。 */\n.ss-channelTag {\n display: inline-block;\n padding: 0 6px;\n margin-right: 6px;\n border-radius: 4px;\n font-size: 11px;\n line-height: 18px;\n font-weight: 600;\n vertical-align: 1px;\n background: rgba(11, 168, 117, .12);\n color: #0ba875;\n white-space: nowrap;\n}\n\n@media (prefers-color-scheme: dark) {\n .ss-channelTag { color: #3ddc9a; }\n}\n\n/* 行卡说明(12px/18 tertiary;v1.27.5 紧凑化:默认单行截断,hover title 看全文) */\n.ss-rowDesc {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* 行卡控件(右置) */\n.ss-rowControl {\n flex: none;\n display: flex;\n align-items: center;\n}\n\n/* 开关(官方 token 的自绘 toggle:track 22px,thumb 16px) */\n.ss-switch {\n position: relative;\n flex: none;\n width: 40px;\n height: 22px;\n cursor: pointer;\n}\n\n.ss-switch input {\n position: absolute;\n opacity: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n cursor: pointer;\n}\n\n.ss-switchTrack {\n position: absolute;\n inset: 0;\n border-radius: 11px;\n background: var(--dsw-alias-border-l3);\n transition: background var(--ds-transition-duration-fast, 0.15s) var(--ds-ease-in-out, ease);\n}\n\n.ss-switchTrack::after {\n content: '';\n position: absolute;\n top: 3px;\n left: 3px;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform var(--ds-transition-duration-fast, 0.15s) var(--ds-ease-in-out, ease);\n}\n\n.ss-switch input:checked + .ss-switchTrack {\n background: var(--dsw-alias-brand-primary);\n}\n\n.ss-switch input:checked + .ss-switchTrack::after {\n transform: translateX(18px);\n}\n\n.ss-switch input:disabled + .ss-switchTrack {\n opacity: 0.4;\n cursor: default;\n}\n\n/* Skiff 调试端口输入(number;关闭时跟随 disabled 半透明) */\n.ss-portInput {\n width: 96px;\n padding: 6px 8px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n\n.ss-portInput:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n/* 自主轨迹「立即唤起」按钮(v1.26.14 调试语义;官方 button 语言:primary 弱化) */\n.ss-wakeBtn {\n flex: none;\n padding: 5px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.12));\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity var(--ds-transition-duration-fast, 0.15s) ease;\n}\n\n.ss-wakeBtn:hover:not(:disabled) {\n border-color: var(--dsw-alias-state-success-primary, #22c55e);\n color: var(--dsw-alias-state-success-primary, #22c55e);\n}\n\n.ss-wakeBtn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n/* 错误提示(会话清理等) */\n.ss-error {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-error-primary, #ef4444);\n background: rgba(239, 68, 68, 0.08);\n border-radius: 8px;\n padding: 8px 10px;\n white-space: pre-line;\n word-break: break-word;\n}\n\n/* ── v1.27.5 紧凑化:折叠组(details/summary)── */\n.ss-collapse {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n overflow: hidden;\n}\n\n.ss-collapseHead {\n display: flex;\n align-items: baseline;\n gap: 10px;\n padding: 10px 14px;\n cursor: pointer;\n list-style: none;\n user-select: none;\n}\n\n.ss-collapseHead::-webkit-details-marker {\n display: none;\n}\n\n.ss-collapseHead::before {\n content: '▸';\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n transition: transform 0.15s ease;\n}\n\n.ss-collapse[open] > .ss-collapseHead::before {\n transform: rotate(90deg);\n}\n\n.ss-collapseHead:hover {\n background: var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.06));\n}\n\n.ss-collapseTitle {\n font-size: 13px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n.ss-collapseDesc {\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n}\n\n.ss-collapseBody {\n padding: 10px 14px 14px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n}\n\n/* ── 开关 + 端口内联行 ── */\n.ss-switchRow {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n/* ── v1.27.6 「?」帮助浮层(hover/焦点显示三行以上严谨说明)── */\n.ss-help {\n position: relative;\n display: inline-flex;\n align-items: center;\n margin-top: 2px;\n}\n\n.ss-helpMark {\n flex: none;\n width: 16px;\n height: 16px;\n padding: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 50%;\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.12));\n color: var(--dsw-alias-label-tertiary);\n font-size: 10px;\n line-height: 1;\n font-weight: 600;\n cursor: help;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n transition: border-color 0.15s ease, color 0.15s ease;\n}\n\n.ss-helpMark:hover,\n.ss-helpMark:focus-visible {\n border-color: var(--dsw-alias-state-success-primary, #22c55e);\n color: var(--dsw-alias-state-success-primary, #22c55e);\n outline: none;\n}\n\n.ss-helpTip {\n position: absolute;\n left: 0;\n top: calc(100% + 6px);\n z-index: 30;\n width: 300px;\n max-width: 60vw;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-overlay);\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.28);\n font-size: 12px;\n line-height: 19px;\n font-weight: 400;\n color: var(--dsw-alias-label-primary);\n white-space: pre-line;\n word-break: break-word;\n opacity: 0;\n pointer-events: none;\n transform: translateY(-4px);\n transition: opacity 0.15s ease, transform 0.15s ease;\n}\n\n.ss-help:hover .ss-helpTip,\n.ss-helpMark:focus-visible + .ss-helpTip {\n opacity: 1;\n pointer-events: auto;\n transform: translateY(0);\n}\n\n/* 折叠组内帮助浮层定位修正(左侧行卡不会溢出面板) */\n.ss-rowCard .ss-helpTip {\n left: 0;\n}\n\n/* ── 微信桥(F4c-3,v1.27.0 实验性)── */\n\n/* CCC 选择器(下拉) */\n.ss-select {\n max-width: 280px;\n padding: 6px 8px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* 扫码绑定按钮(复用 wakeBtn 语言) */\n.ss-loginBtn {\n flex: none;\n padding: 5px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.12));\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity var(--ds-transition-duration-fast, 0.15s) ease;\n}\n\n.ss-loginBtn:hover:not(:disabled) {\n border-color: var(--dsw-alias-state-success-primary, #22c55e);\n color: var(--dsw-alias-state-success-primary, #22c55e);\n}\n\n.ss-loginBtn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n/* 移除账号按钮(危险语义:红边框) */\n.ss-removeBtn {\n flex: none;\n padding: 5px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.12));\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity var(--ds-transition-duration-fast, 0.15s) ease;\n}\n\n.ss-removeBtn:hover:not(:disabled) {\n border-color: var(--dsw-alias-state-error-primary, #ef4444);\n color: var(--dsw-alias-state-error-primary, #ef4444);\n}\n\n/* 二维码展示行 */\n.ss-qrRow {\n align-items: center;\n}\n\n.ss-qrSvg {\n flex: none;\n width: 120px; /* v1.27.5 紧凑化:160 → 120 */\n height: 120px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: #fff; /* 二维码需要白底才能被微信扫出 */\n padding: 6px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.ss-qrSvg svg {\n width: 100%;\n height: 100%;\n}\n\n/* 飞书凭据表单(P4):三格竖排——appId / appSecret / 账号名。\n * 🔴 不做成一行三格:App Secret 通常 32 位,窄屏下会被压到看不见自己输了什么\n * (本文件 D152 记过同族:定宽 + 无断点会把值压到标签上)。竖排在小屏也保持可读。 */\n.ss-feishuForm {\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 6px;\n}\n\n.ss-feishuForm .ss-input {\n width: 100%;\n box-sizing: border-box;\n padding: 7px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.ss-feishuForm .ss-input::placeholder {\n color: var(--dsw-alias-label-secondary);\n}\n\n/* 路由表 JSON 编辑框 */\n.ss-routesEditor {\n width: 100%;\n box-sizing: border-box;\n margin-top: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n font-family: var(--ds-font-family-code);\n resize: vertical;\n}\n\n/* ── 窄屏(面板被塞进窄栏或手机浏览器)────────────────────────────────────\n * 排版问题只有一个根因:`.ss-rowCard` 是「左文本 + 右控件」的两列 flex,\n * 而控件里有端口输入(96px)/开关(40px)/端口+开关(150px)三档。\n * 窄到放不下时,flex 会先压文字(`min-width: 0` 已开)⇒ 标题被挤成一列竖排字。\n * 处理:窄屏把控件落到第二行(左对齐),文字保住整行宽。 */\n@media (max-width: 560px) {\n .ss-rowCard {\n flex-wrap: wrap;\n gap: 10px;\n }\n\n .ss-rowControl {\n flex-basis: 100%;\n justify-content: flex-start;\n }\n\n /* 行内说明在窄屏不必截断:整行宽足够,截断反而藏信息 */\n .ss-rowDesc {\n -webkit-line-clamp: unset;\n display: block;\n }\n\n /* 详情区与折叠头在窄屏收紧内边距(横向像素更值钱) */\n .ss-rowDetail,\n .ss-collapseBody {\n padding-left: 0;\n padding-right: 0;\n }\n}\n";
4444
+ const css$2 = "/* SettingsSection — dsh 原生设置面板 serenity-hooks 简单配置页\n v1.21.1 看齐 dsh 自身:官方 settings 设计语言——\n section > title + intro + group(groupTitle) + rowCard 行卡;\n 全部 --dsw-alias-* token(与 ui-settings-models 同词汇表)。 */\n\n/* 页面容器(官方 .section:flex column gap 12) */\n.ss-section {\n display: flex;\n flex-direction: column;\n gap: 14px;\n max-width: 720px;\n color: var(--dsw-alias-label-primary);\n}\n\n/* 页面标题(官方 .title:16px/24 500) */\n.ss-title {\n margin: 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n/* 页面说明(官方 .intro:14px/22 tertiary) */\n.ss-intro {\n margin: 0;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n}\n\n/* 分组(多级标题:分组标题 + 行卡列表) */\n.ss-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n/* 分组标题(二级:12px/18 500 secondary——官方 fieldLabel 风格) */\n.ss-groupTitle {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* 行卡列表(官方 .rows:无 marker,gap 8) */\n.ss-rows {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n/* 行卡(官方 .rowCard:border-l2 r12 padding 12/14) */\n.ss-rowCard {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n padding: 12px 14px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n}\n\n/* ── v1.29 需求②:可展开行(整行点击 → 行内详设)── */\n.ss-expandable {\n flex-wrap: wrap; /* detail 换行占满整行 */\n cursor: default;\n}\n.ss-expandable .ss-rowText {\n cursor: pointer;\n}\n.ss-expandable .ss-rowText:hover .ss-rowName {\n color: var(--dsw-alias-state-success-primary, #22c55e);\n}\n.ss-expanded {\n border-color: var(--dsw-alias-state-success-primary, #22c55e);\n}\n.ss-expandMark {\n display: inline-block;\n margin-left: 6px;\n font-size: 10px;\n color: var(--dsw-alias-label-tertiary);\n transition: transform 0.15s ease;\n}\n.ss-expanded .ss-expandMark {\n transform: rotate(90deg);\n}\n.ss-rowDetail {\n flex-basis: 100%;\n width: 100%;\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin-top: 10px;\n padding-top: 12px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n min-width: 0;\n}\n.ss-detailStack {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n.ss-detailIntro {\n margin: 0;\n font-size: 12px;\n line-height: 19px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-line;\n word-break: break-word;\n overflow-wrap: anywhere;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.06));\n}\n\n/* 行卡文本(标题 + 说明 左列) */\n.ss-rowText {\n display: flex;\n flex-direction: column;\n gap: 3px;\n min-width: 0;\n flex: 1;\n}\n\n/* 行卡标题(官方 .rowName:14px/22 500) */\n.ss-rowName {\n font-size: 14px;\n line-height: 22px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n/* IM 桥:账号行的**渠道标签**(2026-10-19 泛化后新增)。\n 为什么是标签而不是\"把渠道拼进 id\":渠道是账号的属性,不是名字的一部分;\n 拼进去会让「同一个账号换了渠道」看起来像另一个账号。\n 🔴 不得定宽(v1.72.0 那次 `sp-label` 写死 76px 压住数值的教训)⇒ inline-block + 自适应。 */\n.ss-channelTag {\n display: inline-block;\n padding: 0 6px;\n margin-right: 6px;\n border-radius: 4px;\n font-size: 11px;\n line-height: 18px;\n font-weight: 600;\n vertical-align: 1px;\n background: rgba(11, 168, 117, .12);\n color: #0ba875;\n white-space: nowrap;\n}\n\n@media (prefers-color-scheme: dark) {\n .ss-channelTag { color: #3ddc9a; }\n}\n\n/* 行卡说明(12px/18 tertiary;v1.27.5 紧凑化:默认单行截断,hover title 看全文) */\n.ss-rowDesc {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* 行卡控件(右置) */\n.ss-rowControl {\n flex: none;\n display: flex;\n align-items: center;\n}\n\n/* 开关(官方 token 的自绘 toggle:track 22px,thumb 16px) */\n.ss-switch {\n position: relative;\n flex: none;\n width: 40px;\n height: 22px;\n cursor: pointer;\n}\n\n.ss-switch input {\n position: absolute;\n opacity: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n cursor: pointer;\n}\n\n.ss-switchTrack {\n position: absolute;\n inset: 0;\n border-radius: 11px;\n background: var(--dsw-alias-border-l3);\n transition: background var(--ds-transition-duration-fast, 0.15s) var(--ds-ease-in-out, ease);\n}\n\n.ss-switchTrack::after {\n content: '';\n position: absolute;\n top: 3px;\n left: 3px;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform var(--ds-transition-duration-fast, 0.15s) var(--ds-ease-in-out, ease);\n}\n\n.ss-switch input:checked + .ss-switchTrack {\n background: var(--dsw-alias-brand-primary);\n}\n\n.ss-switch input:checked + .ss-switchTrack::after {\n transform: translateX(18px);\n}\n\n.ss-switch input:disabled + .ss-switchTrack {\n opacity: 0.4;\n cursor: default;\n}\n\n/* Skiff 调试端口输入(number;关闭时跟随 disabled 半透明) */\n.ss-portInput {\n width: 96px;\n padding: 6px 8px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n\n.ss-portInput:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n/* 自主轨迹「立即唤起」按钮(v1.26.14 调试语义;官方 button 语言:primary 弱化) */\n.ss-wakeBtn {\n flex: none;\n padding: 5px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.12));\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity var(--ds-transition-duration-fast, 0.15s) ease;\n}\n\n.ss-wakeBtn:hover:not(:disabled) {\n border-color: var(--dsw-alias-state-success-primary, #22c55e);\n color: var(--dsw-alias-state-success-primary, #22c55e);\n}\n\n.ss-wakeBtn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n/* 错误提示(会话清理等) */\n.ss-error {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-state-error-primary, #ef4444);\n background: rgba(239, 68, 68, 0.08);\n border-radius: 8px;\n padding: 8px 10px;\n white-space: pre-line;\n word-break: break-word;\n}\n\n/* ── v1.27.5 紧凑化:折叠组(details/summary)── */\n.ss-collapse {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n overflow: hidden;\n}\n\n.ss-collapseHead {\n display: flex;\n align-items: baseline;\n gap: 10px;\n padding: 10px 14px;\n cursor: pointer;\n list-style: none;\n user-select: none;\n}\n\n.ss-collapseHead::-webkit-details-marker {\n display: none;\n}\n\n.ss-collapseHead::before {\n content: '▸';\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary);\n transition: transform 0.15s ease;\n}\n\n.ss-collapse[open] > .ss-collapseHead::before {\n transform: rotate(90deg);\n}\n\n.ss-collapseHead:hover {\n background: var(--dsw-alias-interactive-bg-hover, rgba(128, 128, 128, 0.06));\n}\n\n.ss-collapseTitle {\n font-size: 13px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n.ss-collapseDesc {\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n min-width: 0;\n}\n\n.ss-collapseBody {\n padding: 10px 14px 14px;\n border-top: 1px solid var(--dsw-alias-border-l1);\n}\n\n/* ── 开关 + 端口内联行 ── */\n.ss-switchRow {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n/* ── v1.27.6 「?」帮助浮层(hover/焦点显示三行以上严谨说明)── */\n.ss-help {\n position: relative;\n display: inline-flex;\n align-items: center;\n margin-top: 2px;\n}\n\n.ss-helpMark {\n flex: none;\n width: 16px;\n height: 16px;\n padding: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 50%;\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.12));\n color: var(--dsw-alias-label-tertiary);\n font-size: 10px;\n line-height: 1;\n font-weight: 600;\n cursor: help;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n transition: border-color 0.15s ease, color 0.15s ease;\n}\n\n.ss-helpMark:hover,\n.ss-helpMark:focus-visible {\n border-color: var(--dsw-alias-state-success-primary, #22c55e);\n color: var(--dsw-alias-state-success-primary, #22c55e);\n outline: none;\n}\n\n.ss-helpTip {\n position: absolute;\n left: 0;\n top: calc(100% + 6px);\n z-index: 30;\n width: 300px;\n max-width: 60vw;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-alias-bg-overlay);\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.28);\n font-size: 12px;\n line-height: 19px;\n font-weight: 400;\n color: var(--dsw-alias-label-primary);\n white-space: pre-line;\n word-break: break-word;\n opacity: 0;\n pointer-events: none;\n transform: translateY(-4px);\n transition: opacity 0.15s ease, transform 0.15s ease;\n}\n\n.ss-help:hover .ss-helpTip,\n.ss-helpMark:focus-visible + .ss-helpTip {\n opacity: 1;\n pointer-events: auto;\n transform: translateY(0);\n}\n\n/* 折叠组内帮助浮层定位修正(左侧行卡不会溢出面板) */\n.ss-rowCard .ss-helpTip {\n left: 0;\n}\n\n/* ── 微信桥(F4c-3,v1.27.0 实验性)── */\n\n/* CCC 选择器(下拉) */\n.ss-select {\n max-width: 280px;\n padding: 6px 8px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* 扫码绑定按钮(复用 wakeBtn 语言) */\n.ss-loginBtn {\n flex: none;\n padding: 5px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.12));\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity var(--ds-transition-duration-fast, 0.15s) ease;\n}\n\n.ss-loginBtn:hover:not(:disabled) {\n border-color: var(--dsw-alias-state-success-primary, #22c55e);\n color: var(--dsw-alias-state-success-primary, #22c55e);\n}\n\n.ss-loginBtn:disabled {\n opacity: 0.4;\n cursor: default;\n}\n\n/* 移除账号按钮(危险语义:红边框) */\n.ss-removeBtn {\n flex: none;\n padding: 5px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.12));\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity var(--ds-transition-duration-fast, 0.15s) ease;\n}\n\n.ss-removeBtn:hover:not(:disabled) {\n border-color: var(--dsw-alias-state-error-primary, #ef4444);\n color: var(--dsw-alias-state-error-primary, #ef4444);\n}\n\n/* 二维码展示行 */\n.ss-qrRow {\n align-items: center;\n}\n\n.ss-qrSvg {\n flex: none;\n width: 120px; /* v1.27.5 紧凑化:160 → 120 */\n height: 120px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: #fff; /* 二维码需要白底才能被微信扫出 */\n padding: 6px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.ss-qrSvg svg {\n width: 100%;\n height: 100%;\n}\n\n/* 飞书凭据表单(P4):三格竖排——appId / appSecret / 账号名。\n * 🔴 不做成一行三格:App Secret 通常 32 位,窄屏下会被压到看不见自己输了什么\n * (本文件 D152 记过同族:定宽 + 无断点会把值压到标签上)。竖排在小屏也保持可读。 */\n.ss-feishuForm {\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 6px;\n}\n\n.ss-feishuForm .ss-input {\n width: 100%;\n box-sizing: border-box;\n padding: 7px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.ss-feishuForm .ss-input::placeholder {\n color: var(--dsw-alias-label-secondary);\n}\n\n/* 渠道二选一(owner 2026-10-21:「不对称,交互上要体现二选一」)。\n * 判据:**选中态必须一眼可见**(描边 + 文字色),因为这两个按钮的\"当前只有一个成立\"\n * 是这条交互的全部语义;没有选中态就退化成两个普通按钮,等于没做二选一。 */\n.ss-channelPicker {\n display: flex;\n gap: 6px;\n margin: 8px 0 0;\n}\n\n.ss-channelOption {\n flex: none;\n padding: 5px 14px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n cursor: pointer;\n transition: opacity var(--ds-transition-duration-fast, 0.15s) ease;\n}\n\n.ss-channelOption:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.ss-channelOptionOn {\n border-color: var(--dsw-alias-state-success-primary, #22c55e);\n color: var(--dsw-alias-state-success-primary, #22c55e);\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.12));\n}\n\n/* 渠道展开区:微信是「一行说明 + 按钮」,飞书是「竖排表单 + 按钮」 */\n.ss-channelBody {\n display: flex;\n align-items: center;\n gap: 12px;\n justify-content: space-between;\n margin-top: 8px;\n padding: 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.ss-channelBodyCol {\n flex-direction: column;\n align-items: stretch;\n}\n\n.ss-channelActions {\n display: flex;\n justify-content: flex-end;\n}\n\n/* 路由表 JSON 编辑框 */\n.ss-routesEditor {\n width: 100%;\n box-sizing: border-box;\n margin-top: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n font-family: var(--ds-font-family-code);\n resize: vertical;\n}\n\n/* ── 窄屏(面板被塞进窄栏或手机浏览器)────────────────────────────────────\n * 排版问题只有一个根因:`.ss-rowCard` 是「左文本 + 右控件」的两列 flex,\n * 而控件里有端口输入(96px)/开关(40px)/端口+开关(150px)三档。\n * 窄到放不下时,flex 会先压文字(`min-width: 0` 已开)⇒ 标题被挤成一列竖排字。\n * 处理:窄屏把控件落到第二行(左对齐),文字保住整行宽。 */\n@media (max-width: 560px) {\n .ss-rowCard {\n flex-wrap: wrap;\n gap: 10px;\n }\n\n .ss-rowControl {\n flex-basis: 100%;\n justify-content: flex-start;\n }\n\n /* 行内说明在窄屏不必截断:整行宽足够,截断反而藏信息 */\n .ss-rowDesc {\n -webkit-line-clamp: unset;\n display: block;\n }\n\n /* 详情区与折叠头在窄屏收紧内边距(横向像素更值钱) */\n .ss-rowDetail,\n .ss-collapseBody {\n padding-left: 0;\n padding-right: 0;\n }\n}\n";
4390
4445
  if (typeof document !== "undefined" && document.querySelector("style[data-sp-css=\"SettingsSection\"]") === null) {
4391
4446
  const tag = document.createElement("style");
4392
4447
  tag.setAttribute("data-sp-css", "SettingsSection");
@@ -87,6 +87,39 @@ export interface ImChannel {
87
87
  }>;
88
88
  /** 通道健康快照(`status` 动作)——必须可 JSON 化(工具返回值契约) */
89
89
  status(root: string): ImJson;
90
+ /**
91
+ * 列出**与这条 CCC 有过往来的对端用户**(`contacts` 动作,2026-10-22 新增)。
92
+ *
93
+ * 🔴 **它存在的理由**(owner 原话:「配路由需要的是账户到轨迹,轨迹 CCC 知道,
94
+ * 账户 CCC 不知道,需要有个能力让 CCC 查询用户配好的账户」):
95
+ * 路由要写 `{ user: "<通道用户 id>", trajectory: "<轨迹编码>" }`。轨迹编码 CCC 知道,
96
+ * **但"对方是谁"它不知道** —— 那个 id 只存在于聊天记录里。
97
+ * ⇒ 没有本动作,CCC 配路由时只能去翻日志或猜。
98
+ *
99
+ * ⚠️ **可选**:某通道若没有可扫的记录来源(例如未实现记录落盘),不实现即可,
100
+ * 工具会如实返回 `ACTION_UNSUPPORTED` —— 比返回空数组好(空数组会被读成"确实没有人")。
101
+ */
102
+ listContacts?(root: string): ImContact[];
103
+ }
104
+ /**
105
+ * 一个"有过往来的对端"(`contacts` 动作的条目)。
106
+ *
107
+ * 字段刻意**全部可 JSON 化**(工具返回值契约),且不引用内部类型。
108
+ */
109
+ export interface ImContact {
110
+ /** 该对端属于哪个渠道(`weixin` / `feishu`) */
111
+ channel: string;
112
+ /** 对端在该通道内的稳定 id(微信 `xxx@im.wechat` / 飞书 `ou_…`) */
113
+ userId: string;
114
+ /** 收发过它的账号(本地唯一键,如 `wechat-1`)—— 多账号 CCC 靠它区分 */
115
+ accountIds: string[];
116
+ /** 当前路由到的轨迹编码;未配路由 ⇒ `null` */
117
+ route: string | null;
118
+ /** 首次/最近出现时间(ISO8601;取不到为 `null`) */
119
+ firstSeen: string | null;
120
+ lastSeen: string | null;
121
+ /** 见过的消息条数 */
122
+ messages: number;
90
123
  }
91
124
  /** 注册通道(index.ts apply 调用;重复 id 覆盖——测试与热重载友好) */
92
125
  export declare function registerImChannel(channel: ImChannel): void;
package/lib/im/hook.d.ts CHANGED
@@ -27,6 +27,19 @@ interface WeixinHookEventBase {
27
27
  ts: number;
28
28
  /** 触发 CCC 根(多 CCC 一进程可区分) */
29
29
  cccRoot: string;
30
+ /**
31
+ * 渠道 id(`weixin` / `feishu`)。
32
+ *
33
+ * 🔴 2026-10-22 新增(owner:「一个 CCC 绑定多个账户很常见,聊天记录落盘要完善」)。
34
+ * 此前**没有这个字段** ⇒ 三个后果:① 飞书事件落进同一份 jsonl 后**无法区分渠道**
35
+ * (只能靠 `ou_` / `@im.wechat` 的形状猜 = D167 违反:认族不认名);② 一个 CCC 配多个
36
+ * 账号时**不知是哪个渠道**收发的(`accountId` 是本地命名习惯,不携带渠道信息);
37
+ * ③ 按渠道分组统计在字段层就做不到。
38
+ *
39
+ * ⚠️ **向后兼容**:存量事件没有它 ⇒ 读侧按 `accountId → im.accounts[].channel`
40
+ * 回填,仍取不到则按 `weixin`(那是本字段存在之前唯一的渠道)。
41
+ */
42
+ channel: string;
30
43
  /** 接收账号(incoming)/ 发送账号(outgoing) */
31
44
  accountId: string;
32
45
  /** 对端用户(from_user_id,形如 userA@im.wechat) */
@@ -71,6 +84,8 @@ type WeixinHookEvent = WeixinHookIncomingEvent | WeixinHookOutgoingEvent;
71
84
  /** incoming 事件构造输入(bridge 提供实参) */
72
85
  interface IncomingHookInput {
73
86
  cccRoot: string;
87
+ /** 渠道 id(`weixin` / `feishu`);缺省 `weixin`(存量调用点零改动) */
88
+ channel?: string;
74
89
  accountId: string;
75
90
  userId: string;
76
91
  sessionId: string;
@@ -81,6 +96,8 @@ interface IncomingHookInput {
81
96
  /** outgoing 事件构造输入(bridge 提供实参) */
82
97
  interface OutgoingHookInput {
83
98
  cccRoot: string;
99
+ /** 渠道 id(`weixin` / `feishu`);缺省 `weixin` */
100
+ channel?: string;
84
101
  accountId: string;
85
102
  userId: string;
86
103
  sessionId: string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * im/log-scan.ts — 读聊天记录落盘,汇总出「与谁有过往来」(`contacts` 的数据源)
3
+ *
4
+ * ## 这个文件解决什么
5
+ *
6
+ * owner 2026-10-22:「配路由需要的是**账户到轨迹**,轨迹 CCC 知道,**账户 CCC 不知道**,
7
+ * 需要有个能力让 CCC 查询用户配好的账户」。这里的"账户"= **通道用户**(对方是谁),
8
+ * 不是 IM 账号(我们自己登录的身份,`status` 早就能查)。
9
+ *
10
+ * 那个 id(微信 `xxx@im.wechat` / 飞书 `ou_…`)不在任何配置文件里 ——
11
+ * 它**只在聊天记录里出现过**。⇒ 本文件把记录读成联系人表。
12
+ *
13
+ * ## 🔴 两条硬边界
14
+ *
15
+ * 1. **只读**:本文件不写任何东西。真相源仍是那些 jsonl(append-only 事件流),
16
+ * 本文件只是它的一次投影 —— ⛔ 不产生第二真相源。
17
+ * 2. **不猜渠道**:事件的 `channel` 字段为准;缺它(存量事件)⇒ 按
18
+ * `accountId → im.accounts[].channel` 回填,仍取不到才落 `weixin`
19
+ * (那是该字段存在之前唯一的渠道)。⛔ **绝不按 userId 的形状猜**
20
+ * (`ou_` 前缀 / `@im.wechat` 后缀都是命名习惯,不是判据 —— D167 认族不认名)。
21
+ */
22
+ /** 日志根目录族(**新旧两代并存**:`_im-logs/<channel>/` 与存量 `_weixin-logs/`) */
23
+ export declare const IM_LOG_DIR_NEW = "_im-logs";
24
+ export declare const IM_LOG_DIR_LEGACY = "_weixin-logs";
25
+ /** 汇总结果(可 JSON 化) */
26
+ export interface ScannedContact {
27
+ channel: string;
28
+ userId: string;
29
+ accountIds: string[];
30
+ /** 事件里记录的路由目标(`role` 字段;它是事件发生当时的轨迹编码,未必等于**当前**路由) */
31
+ roleAtEvent: string | null;
32
+ firstSeen: number | null;
33
+ lastSeen: number | null;
34
+ messages: number;
35
+ }
36
+ /** 扫描结果(含**扫描域**——否定性结论必须附它) */
37
+ export interface ScanResult {
38
+ contacts: ScannedContact[];
39
+ /** 实际读过的文件(相对 CCC 根) */
40
+ sources: string[];
41
+ /** 读失败的文件及其原因(不静默) */
42
+ errors: string[];
43
+ }
44
+ /** 某 CCC 下**存在**的日志目录(相对根),新代在前 */
45
+ export declare function imLogRoots(root: string): string[];
46
+ /**
47
+ * 扫描本 CCC 的全部聊天记录,按 `(channel, userId)` 汇总。
48
+ *
49
+ * @param root CCC 根
50
+ * @param accountChannel 存量事件缺 `channel` 时的回填表(`accountId → channel`)
51
+ * @returns 汇总结果 + **扫描域** + 读取错误(三者都不可省)
52
+ */
53
+ export declare function scanImContacts(root: string, accountChannel?: Record<string, string>): ScanResult;
package/lib/im/route.d.ts CHANGED
@@ -64,8 +64,15 @@ export declare function readImSettings(root: string, paths?: string[]): ImSettin
64
64
  * ⚠️ 2026-10-19 起它是 {@link readImSettings} 的**薄别名**(签名与返回形状不变):
65
65
  * 旧调用点无需改动,但**配置来源已经统一**——`im` 段优先、`weixin` 段回退。
66
66
  * 保留名字是为了让既有调用点与测试**逐字不动**(P1 是纯重构,读数必须不变)。
67
+ *
68
+ * 🔴 2026-10-22:**返回类型由 `WeixinSettings` 改为 `ImSettings`**。
69
+ * 此前实现返回的就是 `readImSettings(...)`(已是归一形态:`accounts[].channel` 必填),
70
+ * 却把它断言成旧类型 —— 于是类型层"看不见"`channel` 字段,
71
+ * 调用方写 `a.channel` 会报 TS2339,而**运行时它一直在那儿**。
72
+ * ⇒ 那是**类型声明在撒谎**,不是调用方写错。改成真实类型后,多账户/多渠道的读侧才敢用它。
73
+ * ⚠️ 附带效应:`WeixinSettings` 的旧形状调用方若用到旧字段,会在此处暴露(好事,早暴露)。
67
74
  */
68
- export declare function readWeixinSettings(root: string, paths?: string[]): WeixinSettings;
75
+ export declare function readWeixinSettings(root: string, paths?: string[]): ImSettings;
69
76
  /** 账号凭据(localstore credential scope;baseUrl 缺省 = 官方默认) */
70
77
  export interface WeixinAccountCredential {
71
78
  token: string;