dsh-composer-ux 0.16.5 → 0.17.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/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.17.0] — 2026-10-01(新增:单向微信通知)
6
+
7
+ 用户 2026-10-01 明确:**只要通知,不做双向**、不走 OpenClaw,用现成的微信推送服务。界面里固定标注
8
+ **「只能通知,不能回」** —— 不暗示一个不存在的能力。
9
+
10
+ - **两条渠道**:PushPlus token(推到你关注的公众号对话)/ 企业微信群机器人 webhook;总开关**默认关**。
11
+ - **三类事件**(照 dsh-messager 那套):需要你回应(等审批 / 等你回答)· 任务完成 · 任务出错。
12
+ 挂在官方事件上:、(**waterfall 透传**,只观察不代答)、
13
+ (只有 running→idle 才算完成)、。
14
+ - **防刷屏**:30 秒内同会话同类合并(按会话分桶,别的会话完成不会被吞)。
15
+ - **测试推送**:绕过防刷屏与分类开关(但仍要求总开关开着、凭据填了),且**不写记账**
16
+ —— 否则一次测试会压掉后面真实的通知。
17
+ - **凭据安全**:只存本机;界面与日志只显示打码形式(头 4 尾 2);响应体与错误信息里绝不回显 token。
18
+ - **投递在宿主侧**:标签页关着也能推。宿主没有事件总线时**只告警不挂**(通知挂不上绝不连累宿主启动)。
19
+
20
+ ## 验证
21
+
22
+ - `npm test`:**28 个套件 2473 passed / 0 failed**(新增 4 个套件:微信通知核心 / 事件适配 /
23
+ 发送器 / 测试推送路由)。
24
+ - 变异:**167/167 全部咬住**(新增 FP 审批不再透传、FQ 去掉防刷屏、FR 凭据不打码;并把 0.16.4
25
+ 滚动改动后失效的 EC/FC 两条重新对准当前结构)。
26
+
27
+ ## 待用户验收(不是已验证)
28
+
29
+ **「消息真的到你手机」这一步我无法验证** —— 这台机器上没有 PushPlus token、也没有群机器人地址。
30
+ 请你在设置页打开总开关、填好凭据、点一次**「发送测试」**:界面上会显示渠道给的原话(成功或失败原因)。
31
+ 在这条完成之前,请不要把本功能当成已验证。
32
+
5
33
  ## [0.16.5] — 2026-10-01(设置页抬头加「官网 ↗」)
6
34
 
7
35
  起因(用户原话):**「https://www.deepseek.com/harness/ 把这个 DSH 的官网链接放到我的插件里」**;
package/README.md CHANGED
@@ -518,7 +518,7 @@ Windows 上 DSH 给模型的终端工具是 **PowerShell**(工具名 `pwsh`)
518
518
  | 工具循环的消息形状 / 轮次封顶 / 异常必须降级 | 同上第 6 节(假 llm 驱动整条循环,逐条检查 `role:'tool'` 与 `toolCallId`) |
519
519
  | 工具路径:默认关、开了才派、**查完吐散文必须回落**、拿不到工作目录就不派 | `test/quick-commands.mjs` 5g 节(13 条,真路由 + 假 llm) |
520
520
  | 工具读到的内容不进台账 | 同 5g 节:断言台账里只有工具名与次数、没有读到的文件内容 |
521
- | 变异守卫真的会咬人,且**必须是指定的那条测试变红** | `npm run test:mutations`:**164 条全部咬住**(判据是 `red.some(line => line.includes(item.expect))`,不是"有红就行") |
521
+ | 变异守卫真的会咬人,且**必须是指定的那条测试变红** | `npm run test:mutations`:**167 条全部咬住**(判据是 `red.some(line => line.includes(item.expect))`,不是"有红就行") |
522
522
  | 发布门禁三条判据 | `npm run gates` 全过;`check:tag` 实测 `v0.12.0` 的 tag 名与所指提交里的版本一致 |
523
523
  | 三平台 CI | GitHub Actions:ubuntu / windows / macos × Node 20(`npm ci → typecheck → build → test → check:pack → check:docs`) |
524
524
 
@@ -652,9 +652,9 @@ node build.mjs # 产出 lib/index.js + lib/client.js
652
652
  # ⚠️ 宿主半会**内联** schemastery / cosmokit:
653
653
  # 优先用 DSH 检出里的 vendor 副本,检出不在时退到 node_modules
654
654
  # 里同版本的 npm 包(两者逐字节相同)——CI 上走的就是退路
655
- npm test # 24 个套件;当前 2385 passed, 0 failed(2026-09-29 实测;CI 三平台同样全绿)
655
+ npm test # 28 个套件;当前 2473 passed, 0 failed(2026-09-29 实测;CI 三平台同样全绿)
656
656
  # 走 scripts/run-tests.mjs:顺带把"多少套件/多少条"记进 test/.last-run.json
657
- npm run test:mutations # 手动跑:变异测试,证明那套护栏真的在咬人(164 条,须单独跑)
657
+ npm run test:mutations # 手动跑:变异测试,证明那套护栏真的在咬人(167 条,须单独跑)
658
658
  # 同样记录结果,供下面的文档门禁核对
659
659
  npm run gates # 发版门禁三条一起跑:tag 指向 / 包内容 / 文档数字
660
660
  npm run check:tag # ① tag 名里的版本 == 该 tag 所指提交里的 package.json 版本
package/lib/client.js CHANGED
@@ -1011,6 +1011,12 @@ var RESTART_CSRF_HEADER = "x-composer-ux-restart";
1011
1011
  var RESTART_CSRF_VALUE = "1";
1012
1012
  var SYNC_API_PATH = "/composer-ux/sync-prices";
1013
1013
  var ENABLED_FIELD = "enabled";
1014
+ var NOTIFY_ENABLED_FIELD = "notifyEnabled";
1015
+ var NOTIFY_CHANNEL_FIELD = "notifyChannel";
1016
+ var NOTIFY_PUSHPLUS_FIELD = "notifyPushplusToken";
1017
+ var NOTIFY_WECOM_FIELD = "notifyWecomWebhook";
1018
+ var NOTIFY_KINDS_FIELD = "notifyKinds";
1019
+ var NOTIFY_TEST_API_PATH = "/composer-ux/notify/test";
1014
1020
  var KEYS_ENABLED_FIELD = "keysEnabled";
1015
1021
  var MENU_ENABLED_FIELD = "menuEnabled";
1016
1022
  var QUICK_ENABLED_FIELD = "quickEnabled";
@@ -1278,6 +1284,12 @@ var DEFAULT_HEADER_NAME = "x-opencode-session";
1278
1284
  var HEADER_VALUE_MAX = 200;
1279
1285
  var HEADER_NAME_MAX = 64;
1280
1286
  var DEFAULT_SETTINGS = {
1287
+ // 微信通知:默认全关全空(0.17.0)。
1288
+ notifyEnabled: false,
1289
+ notifyChannel: "pushplus",
1290
+ notifyPushplusToken: "",
1291
+ notifyWecomWebhook: "",
1292
+ notifyKinds: { "needs-input": true, done: true, error: true },
1281
1293
  // 总开关默认开:它只是总闸,真正"要不要用这一栏"由下面五个开关决定(默认关)。
1282
1294
  enabled: true,
1283
1295
  // 五栏默认关(老文档由 sectionEnabledOf 迁移成"碰过就开")。
@@ -1489,7 +1501,26 @@ function sanitizeSettings(value) {
1489
1501
  return out;
1490
1502
  };
1491
1503
  const asTier = () => normalizeOptimizerTier(source[OPTIMIZER_TIER_FIELD]);
1504
+ const asNotifyChannel = () => {
1505
+ const raw = source[NOTIFY_CHANNEL_FIELD];
1506
+ return raw === "wecom" ? "wecom" : "pushplus";
1507
+ };
1508
+ const asNotifyKinds = () => {
1509
+ const raw = source[NOTIFY_KINDS_FIELD];
1510
+ const row2 = typeof raw === "object" && raw !== null ? raw : {};
1511
+ return {
1512
+ "needs-input": row2["needs-input"] !== false,
1513
+ done: row2.done !== false,
1514
+ error: row2.error !== false
1515
+ };
1516
+ };
1492
1517
  return {
1518
+ // 微信通知(0.17.0,单向):默认全关/全空。
1519
+ notifyEnabled: asBool(NOTIFY_ENABLED_FIELD),
1520
+ notifyChannel: asNotifyChannel(),
1521
+ notifyPushplusToken: asString(NOTIFY_PUSHPLUS_FIELD),
1522
+ notifyWecomWebhook: asString(NOTIFY_WECOM_FIELD),
1523
+ notifyKinds: asNotifyKinds(),
1493
1524
  enabled: asBool(ENABLED_FIELD),
1494
1525
  // 五栏开关:显式写过听它的,否则按"用户碰过没"迁移(见 sectionEnabledOf)。
1495
1526
  keysEnabled: sectionEnabledOf(KEYS_ENABLED_FIELD, source),
@@ -5833,6 +5864,141 @@ function useRestart() {
5833
5864
  };
5834
5865
  return { stage, detail: detail2, blocked, ask, confirm, cancel };
5835
5866
  }
5867
+ function maskSecret(value) {
5868
+ if (value === "") return "";
5869
+ return value.length < 8 ? "****" : `${value.slice(0, 4)}\u2026${value.slice(-2)}`;
5870
+ }
5871
+ function NotifyCard({ settings, setField }) {
5872
+ const on = settings.notifyEnabled;
5873
+ const channel = settings.notifyChannel;
5874
+ const kinds = settings.notifyKinds;
5875
+ const [busy, setBusy] = (0, import_react7.useState)(false);
5876
+ const [note2, setNote] = (0, import_react7.useState)("");
5877
+ const send = () => {
5878
+ setBusy(true);
5879
+ setNote("");
5880
+ void fetch(NOTIFY_TEST_API_PATH, {
5881
+ method: "POST",
5882
+ headers: { "content-type": "application/json", [RESTART_CSRF_HEADER]: RESTART_CSRF_VALUE },
5883
+ body: "{}"
5884
+ }).then(async (r) => await r.json()).then((v) => {
5885
+ setNote(v.ok === true ? "\u5DF2\u53D1\u51FA \u2014\u2014 \u53BB\u5FAE\u4FE1\u770B\u4E00\u773C\uFF08\u8FD9\u91CC\u4E0D\u5199\u201C\u5DF2\u9A8C\u8BC1\u201D\uFF0C\u53EA\u6709\u4F60\u80FD\u786E\u8BA4\uFF09" : `\u6CA1\u53D1\u51FA\u53BB\uFF1A${v.reason ?? "\u672A\u77E5\u539F\u56E0"}`);
5886
+ }).catch((e) => {
5887
+ setNote(`\u8BF7\u6C42\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`);
5888
+ }).finally(() => {
5889
+ setBusy(false);
5890
+ });
5891
+ };
5892
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
5893
+ FoldCard,
5894
+ {
5895
+ name: "\u5FAE\u4FE1\u901A\u77E5",
5896
+ summary: on ? `\u5DF2\u5F00 \xB7 ${channel === "pushplus" ? "PushPlus" : "\u4F01\u4E1A\u5FAE\u4FE1\u673A\u5668\u4EBA"} \xB7 \u53EA\u80FD\u901A\u77E5\uFF0C\u4E0D\u80FD\u56DE` : "\u672A\u542F\u7528 \xB7 \u53EA\u80FD\u901A\u77E5\uFF0C\u4E0D\u80FD\u56DE",
5897
+ toggle: { checked: on, onChange: (next) => {
5898
+ setField(NOTIFY_ENABLED_FIELD, next);
5899
+ } },
5900
+ children: [
5901
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { margin: "10px 0", fontSize: 12, lineHeight: 1.6, opacity: 0.85 }, children: [
5902
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: "\u53EA\u80FD\u901A\u77E5\uFF0C\u4E0D\u80FD\u56DE\u3002" }),
5903
+ "\u9700\u8981\u4F60\u56DE\u5E94 / \u5B8C\u6210 / \u51FA\u9519\u65F6\u63A8\u4E00\u6761\u5230\u4F60\u624B\u673A\uFF1B\u8981\u5904\u7406\u4ECD\u56DE DSH \u754C\u9762\u70B9\u3002"
5904
+ ] }),
5905
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
5906
+ ToggleRow,
5907
+ {
5908
+ label: "\u7528 PushPlus\uFF08\u5230\u4F60\u5173\u6CE8\u7684\u516C\u4F17\u53F7\uFF09",
5909
+ desc: "pushplus.plus \u7528\u5FAE\u4FE1\u767B\u5F55\u62FF token\uFF0C\u586B\u5728\u4E0B\u9762\u3002",
5910
+ checked: channel === "pushplus",
5911
+ onChange: () => {
5912
+ setField(NOTIFY_CHANNEL_FIELD, "pushplus");
5913
+ },
5914
+ first: true
5915
+ }
5916
+ ),
5917
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
5918
+ TextFieldRow,
5919
+ {
5920
+ title: "PushPlus token",
5921
+ desc: settings.notifyPushplusToken === "" ? "\u8FD8\u6CA1\u586B\uFF08\u586B\u4E86\u624D\u80FD\u63A8\u9001\uFF09" : `\u5F53\u524D\uFF1A${maskSecret(settings.notifyPushplusToken)}\uFF08\u8981\u770B\u5168\u6587\u8BF7\u91CD\u65B0\u7C98\u8D34\uFF09`,
5922
+ value: settings.notifyPushplusToken,
5923
+ placeholder: "\u7C98\u8D34 token",
5924
+ maxLength: 200,
5925
+ onChange: (next) => {
5926
+ setField(NOTIFY_PUSHPLUS_FIELD, next);
5927
+ }
5928
+ }
5929
+ ),
5930
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
5931
+ ToggleRow,
5932
+ {
5933
+ label: "\u7528\u4F01\u4E1A\u5FAE\u4FE1\u7FA4\u673A\u5668\u4EBA",
5934
+ desc: "\u53EA\u6709\u4F60\u7684\u4F01\u4E1A\u5FAE\u4FE1\u7FA4\u91CC\u52A0\u4E2A\u673A\u5668\u4EBA\uFF0C\u628A webhook \u586B\u4E0B\u9762\u3002",
5935
+ checked: channel === "wecom",
5936
+ onChange: () => {
5937
+ setField(NOTIFY_CHANNEL_FIELD, "wecom");
5938
+ }
5939
+ }
5940
+ ),
5941
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
5942
+ TextFieldRow,
5943
+ {
5944
+ title: "\u4F01\u4E1A\u5FAE\u4FE1 webhook",
5945
+ desc: settings.notifyWecomWebhook === "" ? "\u8FD8\u6CA1\u586B" : `\u5F53\u524D\uFF1A${maskSecret(settings.notifyWecomWebhook)}`,
5946
+ value: settings.notifyWecomWebhook,
5947
+ placeholder: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=\u2026",
5948
+ maxLength: 500,
5949
+ onChange: (next) => {
5950
+ setField(NOTIFY_WECOM_FIELD, next);
5951
+ }
5952
+ }
5953
+ ),
5954
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
5955
+ ToggleRow,
5956
+ {
5957
+ label: "\u9700\u8981\u4F60\u56DE\u5E94\u65F6\u901A\u77E5",
5958
+ desc: "\u7B49\u5BA1\u6279\u3001\u7B49\u4F60\u56DE\u7B54\u95EE\u9898\u65F6\u63A8\u4E00\u6761\u3002",
5959
+ checked: kinds["needs-input"],
5960
+ onChange: (next) => {
5961
+ setField(NOTIFY_KINDS_FIELD, { ...kinds, "needs-input": next });
5962
+ }
5963
+ }
5964
+ ),
5965
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
5966
+ ToggleRow,
5967
+ {
5968
+ label: "\u4EFB\u52A1\u5B8C\u6210\u65F6\u901A\u77E5",
5969
+ desc: "\u4E00\u8F6E\u8DD1\u5B8C\uFF08\u8FD0\u884C\u4E2D\u2192\u7A7A\u95F2\uFF09\u63A8\u4E00\u6761\u3002",
5970
+ checked: kinds.done,
5971
+ onChange: (next) => {
5972
+ setField(NOTIFY_KINDS_FIELD, { ...kinds, done: next });
5973
+ }
5974
+ }
5975
+ ),
5976
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
5977
+ ToggleRow,
5978
+ {
5979
+ label: "\u4EFB\u52A1\u51FA\u9519\u65F6\u901A\u77E5",
5980
+ desc: "\u67D0\u6B65\u6216\u67D0\u8F6E\u62A5\u9519\u65F6\u63A8\u4E00\u6761\u3002",
5981
+ checked: kinds.error,
5982
+ onChange: (next) => {
5983
+ setField(NOTIFY_KINDS_FIELD, { ...kinds, error: next });
5984
+ }
5985
+ }
5986
+ ),
5987
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
5988
+ ToggleRow,
5989
+ {
5990
+ label: "\u6D4B\u8BD5\u63A8\u9001",
5991
+ desc: note2 === "" ? "\u70B9\u53F3\u8FB9\u6309\u94AE\uFF0C\u7ACB\u523B\u53D1\u4E00\u6761\u5230\u4F60\u5FAE\u4FE1\u9A8C\u8BC1\u914D\u7F6E\u3002" : note2,
5992
+ checked: false,
5993
+ onChange: () => {
5994
+ },
5995
+ action: { label: busy ? "\u53D1\u9001\u4E2D\u2026" : "\u53D1\u9001\u6D4B\u8BD5", onClick: send }
5996
+ }
5997
+ )
5998
+ ]
5999
+ }
6000
+ );
6001
+ }
5836
6002
  function RestartButton({ restart }) {
5837
6003
  const busy = restart.stage === "restarting" || restart.stage === "asking";
5838
6004
  const blocked = restart.blocked;
@@ -5883,7 +6049,7 @@ function RestartBanner({ restart }) {
5883
6049
  }, children: "\u77E5\u9053\u4E86" }) })
5884
6050
  ] });
5885
6051
  }
5886
- function SettingsSection({ useLive, useBook, useBookStatus, useWriteNotice, actions }) {
6052
+ function SettingsSection({ useLive, useBook, useBookStatus, useWriteNotice, actions, setField }) {
5887
6053
  const settings = useLive((item) => item);
5888
6054
  const book = useBook((item) => item);
5889
6055
  const bookStatus = useBookStatus((item) => item);
@@ -5961,6 +6127,7 @@ function SettingsSection({ useLive, useBook, useBookStatus, useWriteNotice, acti
5961
6127
  )
5962
6128
  ] })
5963
6129
  ] }),
6130
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(NotifyCard, { settings, setField }),
5964
6131
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RestartBanner, { restart }),
5965
6132
  writeNotice !== "" && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-ux-restartBanner dsh-ux-restartBannerFailed", role: "status", children: [
5966
6133
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "dsh-ux-restartBannerText", children: [