@rezti/dsh-rez-suite 0.1.48 → 0.1.49

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
@@ -5,12 +5,16 @@
5
5
  升级:
6
6
 
7
7
  ```bash
8
- dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.48
8
+ dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.49
9
9
  ```
10
10
 
11
11
  不要用裸 `plugin update`,不要写 `latest`(新 profile 有 24 小时发布冷却,当天可能装不上)。不要装 **0.1.46**(GitHub 子依赖会被 DSH 拦住)。
12
12
 
13
- 发版顺序:`sso@0.1.14` → `nextcloud@0.1.11` → `@rezti/dsh-web-file-uploader@0.2.3` → `suite@0.1.48`。
13
+ 发版顺序:`wechat@0.1.15` → `suite@0.1.49`。
14
+
15
+ ## 0.1.49 — 2026-08-19
16
+
17
+ - **企业微信智能机器人**:设置页按房间填 BotID + Secret。同事在企微跟哪个机器人说话,消息进网页左侧那个房间。密钥只存本机 `~/.dsh/wecom-channels.json`,不进 compose。旧群 Webhook 仅在一个 bot 都没绑时兼容。
14
18
 
15
19
  ## 0.1.48 — 2026-08-19
16
20
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  雷泽插件的**唯一安装入口**。Odoo / 企微 / HA / TAPD / GSC / 企查查(仅法务助理房间) / 天眼查(仅老板) / 工作区(老板三室 + 全员助理)都由这一包拉齐,不要再逐个 `plugin add`,**不要**再装 `@rezti/dsh-rez-boss-desk`。
4
4
 
5
- 发版顺序(必做):先发 `@rezti/dsh-rez-sso@0.1.14` → `@rezti/dsh-rez-nextcloud@0.1.11` → `@rezti/dsh-web-file-uploader@0.2.3` → `@rezti/dsh-rez-suite@0.1.48`。只发 suite 没有 SSO 0.1.14,实验室连接字段不会上 npm。suite 读取 SSO 的 `REZ_DEFAULT_CONNECTIONS.qcc`;缺字段时用官方 URL 兜底,避免插件加载失败把微信面板一起拖垮。
5
+ 发版顺序(必做):先发 `@rezti/dsh-rez-wechat@0.1.15` → `@rezti/dsh-rez-suite@0.1.49`。员工机安装包仍钉已发布的 suite,不要顺手改审批镜像 `SUITE_VERSION`。
6
6
 
7
7
  设置里选身份后,左侧自己出现对应房间(全员三助理;工程师另加实验室;运营另加设计;老板另加搞钱/产品/运营),不必手动添加工作区。实验室:大模型写 YAML、调 MCP、给下载链接。
8
8
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  ```bash
14
14
  # 第一次 / 升级(钉死版本,避开 24h 发布冷却)
15
- dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.48
15
+ dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.49
16
16
  ```
17
17
 
18
18
  本版改了什么:见 [CHANGELOG.md](./CHANGELOG.md)。
package/README.zh.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-rez-suite
2
2
 
3
- **用户只装这一包。** 升级只记:`dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.48`(不要用裸 `update`;不要 latest;不要 0.1.46 / 0.1.47)。本版改了什么见 [CHANGELOG.md](./CHANGELOG.md)。拆分包给开发改代码;不要对同一 profile 再单独 add 子包。
3
+ **用户只装这一包。** 升级只记:`dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.49`(不要用裸 `update`;不要 latest;不要 0.1.46 / 0.1.47)。本版改了什么见 [CHANGELOG.md](./CHANGELOG.md)。拆分包给开发改代码;不要对同一 profile 再单独 add 子包。
4
4
 
5
5
  说明书 / 规格书用输入栏**回形针**上传原 PDF(官方对话只收图,不要拖文件;不要用微信发 PDF)。先入 `Marketing/Inbox` + TAPD 测试中,凯文跟 Harness 说审批了哪份后,再写入 `Marketing/Publish/…`。
6
6
 
package/cordis.patch.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  # 用户只装这一包:`dsh plugin --profile web add @rezti/dsh-rez-suite`
2
- # 升级只记这一条:`dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.48`
2
+ # 升级只记这一条:`dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.49`
3
3
  # 不要再单独 add 子包,会重复注册。
4
4
  #
5
5
  # MCP rows are started by the domain plugins after rezSso resolves secrets.
package/lib/client.d.ts CHANGED
@@ -7255,7 +7255,7 @@ declare const zh: {
7255
7255
  readonly 'entry.label': "Rez";
7256
7256
  readonly 'entry.tooltip': "Rez Suite 配置与审计面板";
7257
7257
  readonly 'settings.title': "ReZ-TI";
7258
- readonly 'settings.description': "公司 MCP(Odoo / Nextcloud / HA / TAPD / GSC)。企业微信栏暂隐。个人微信在「微信」页签扫码。";
7258
+ readonly 'settings.description': "公司 MCP(Odoo / Nextcloud / HA / TAPD / GSC)和企业微信智能机器人。个人微信在「微信」页签扫码。";
7259
7259
  readonly 'panel.title': "Rez Suite";
7260
7260
  readonly 'tab.config': "配置";
7261
7261
  readonly 'tab.weixin': "微信";
@@ -7291,6 +7291,12 @@ declare const zh: {
7291
7291
  readonly 'config.wecom.botId': "Bot ID";
7292
7292
  readonly 'config.wecom.secret': "Secret";
7293
7293
  readonly 'config.wecom.webhook': "群机器人 Webhook";
7294
+ readonly 'config.wecom.hint': "Accio 同款:一个房间绑一个智能机器人。同事在企微跟哪个 bot 说话,就进网页左侧那个房间。密钥只存本机 ~/.dsh/wecom-channels.json,不要写进 compose。BotID / Secret 在企业微信管理后台 → 智能机器人。";
7295
+ readonly 'config.wecom.room': "房间";
7296
+ readonly 'config.wecom.add': "添加绑定";
7297
+ readonly 'config.wecom.remove': "删除";
7298
+ readonly 'config.wecom.empty': "还没有绑定。给人事/财务/法务等房间各建一个企微智能机器人,就能变成不同角色。";
7299
+ readonly 'config.wecom.status': "通道";
7294
7300
  readonly 'config.ha': "Home Assistant";
7295
7301
  readonly 'config.ha.url': "HA MCP URL";
7296
7302
  readonly 'config.ha.token': "长期访问令牌";
package/lib/client.js CHANGED
@@ -4,7 +4,7 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- (function(){var id="dsh-rez-suite-css";if(typeof document==="undefined"||document.getElementById(id))return;var s=document.createElement("style");s.id=id;s.textContent=".r9dlUa_wordmark {\n min-width: 0;\n color: inherit;\n align-items: center;\n gap: 8px;\n display: inline-flex;\n overflow: hidden;\n}\n\n.r9dlUa_mark {\n object-fit: contain;\n flex: none;\n width: 28px;\n height: 28px;\n display: block;\n}\n\n.r9dlUa_markOnly {\n object-fit: contain;\n width: 100%;\n max-width: 28px;\n height: 100%;\n max-height: 28px;\n display: block;\n}\n\n.r9dlUa_name {\n letter-spacing: .02em;\n white-space: nowrap;\n text-overflow: ellipsis;\n font-size: 15px;\n font-weight: 650;\n overflow: hidden;\n}\n\n.r9dlUa_badge {\n letter-spacing: .06em;\n opacity: .72;\n white-space: nowrap;\n border: 1px solid;\n border-radius: 4px;\n flex: none;\n padding: 2px 6px;\n font-size: 10px;\n font-weight: 700;\n line-height: 1.2;\n}\n.LOjPTW_shell {\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n display: flex;\n}\n\n.LOjPTW_card {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-3);\n border-radius: 12px;\n list-style: none;\n transition: border-color .16s, background .16s;\n}\n\n.LOjPTW_card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n.LOjPTW_cardOpen {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n.LOjPTW_header {\n appearance: none;\n width: 100%;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 12px;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n display: flex;\n}\n\n.LOjPTW_header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n.LOjPTW_headText {\n flex-direction: column;\n flex: 1;\n gap: 4px;\n min-width: 0;\n display: flex;\n}\n\n.LOjPTW_name {\n color: var(--dsw-alias-label-primary);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.LOjPTW_description {\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n line-height: 1.5;\n}\n\n.LOjPTW_chevron {\n color: var(--dsw-alias-label-tertiary);\n flex: none;\n transition: transform .16s;\n}\n\n.LOjPTW_chevronOpen {\n transform: rotate(180deg);\n}\n\n.LOjPTW_body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px 12px;\n padding-top: 8px;\n}\n\n.LOjPTW_tabBar {\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n flex: none;\n gap: 2px;\n display: flex;\n}\n\n.LOjPTW_tab {\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n background: none;\n border: none;\n border-bottom: 2px solid #0000;\n border-radius: 6px 6px 0 0;\n padding: 7px 14px;\n font-size: 13px;\n}\n\n.LOjPTW_tab:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_tab[data-active] {\n color: var(--dsw-alias-label-primary);\n border-bottom-color: var(--dsw-alias-state-business-primary);\n font-weight: 600;\n}\n\n.LOjPTW_tabBody {\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n padding: 12px 0 4px;\n display: flex;\n}\n\n.LOjPTW_section {\n border: 1px solid var(--dsw-alias-border-l1);\n background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-base));\n border-radius: 10px;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n display: flex;\n}\n\n.LOjPTW_sectionTitle {\n color: var(--dsw-alias-label-primary);\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n}\n\n.LOjPTW_field {\n flex-direction: column;\n align-items: stretch;\n gap: 4px;\n display: flex;\n}\n\n.LOjPTW_label {\n color: var(--dsw-alias-label-secondary);\n font-size: 12.5px;\n}\n\n.LOjPTW_input, .LOjPTW_select {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n outline: none;\n padding: 6px 8px;\n font-size: 13px;\n}\n\n.LOjPTW_checkbox {\n justify-self: start;\n width: 15px;\n height: 15px;\n}\n\n.LOjPTW_toolbar {\n flex-wrap: wrap;\n flex: none;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.LOjPTW_button {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n border-radius: 8px;\n padding: 7px 12px;\n font-size: 13px;\n}\n\n.LOjPTW_button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_button:disabled {\n opacity: .4;\n cursor: default;\n}\n\n.LOjPTW_primaryButton {\n background: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n\n.LOjPTW_dangerButton {\n background: var(--dsw-alias-state-danger, #c62828);\n border-color: var(--dsw-alias-state-danger, #c62828);\n color: #fff;\n}\n\n.LOjPTW_qr {\n background: #fff;\n border-radius: 8px;\n width: 200px;\n height: 200px;\n margin: 8px 0;\n display: block;\n}\n\n.LOjPTW_message {\n color: var(--dsw-alias-label-secondary);\n margin: 0;\n font-size: 12.5px;\n line-height: 1.5;\n}\n\n.LOjPTW_error {\n color: var(--dsw-alias-state-danger, #c62828);\n}\n\n.LOjPTW_tableWrap {\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n overflow: auto;\n}\n\n.LOjPTW_table {\n border-collapse: collapse;\n width: 100%;\n font-size: 12.5px;\n}\n\n.LOjPTW_table th {\n z-index: 1;\n text-align: left;\n background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));\n padding: 8px 10px;\n position: sticky;\n top: 0;\n}\n\n.LOjPTW_table td {\n border-top: 1px solid var(--dsw-alias-border-l1);\n vertical-align: top;\n padding: 8px 10px;\n}\n\n.LOjPTW_badge {\n border-radius: 999px;\n padding: 2px 8px;\n font-size: 11px;\n display: inline-block;\n}\n\n.LOjPTW_badgeOk {\n color: #229a55;\n background: #229a5524;\n}\n\n.LOjPTW_badgeFail {\n color: #c62828;\n background: #c6282824;\n}\n\n.LOjPTW_cards {\n grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));\n gap: 8px;\n display: grid;\n}\n\n.LOjPTW_stat {\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n padding: 10px;\n}\n\n.LOjPTW_cardLabel {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n}\n\n.LOjPTW_cardValue {\n color: var(--dsw-alias-label-primary);\n font-size: 15px;\n font-weight: 600;\n}\n\n.LOjPTW_upgradeLayer {\n flex: none;\n align-items: center;\n width: 100%;\n height: 42px;\n margin: 8px 0 0;\n display: flex;\n position: relative;\n}\n\n.LOjPTW_upgradeLayerRail {\n width: 36px;\n height: 36px;\n margin: 0;\n}\n\n.LOjPTW_upgradeButton {\n width: calc(100% + 4px);\n height: 42px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: none;\n border: none;\n border-radius: 12px;\n align-items: center;\n gap: 8px;\n margin: 0 -2px;\n padding: 0 10px 0 8px;\n font-family: inherit;\n font-size: 14px;\n display: inline-flex;\n overflow: hidden;\n}\n\n.LOjPTW_upgradeButton:hover, .LOjPTW_upgradeButton[data-outdated] {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_upgradeButton:disabled {\n cursor: progress;\n opacity: .72;\n}\n\n.LOjPTW_upgradeButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n.LOjPTW_upgradeLayerRail .LOjPTW_upgradeButton {\n border-radius: 50%;\n justify-content: center;\n gap: 0;\n width: 36px;\n height: 36px;\n padding: 0;\n}\n\n.LOjPTW_upgradeLabel {\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n overflow: hidden;\n}\n\n.LOjPTW_upgradeMeta {\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n flex: none;\n margin-left: auto;\n font-size: 12px;\n line-height: 16px;\n}\n\n.LOjPTW_upgradeDot {\n background: var(--dsw-alias-brand-primary);\n border-radius: 50%;\n flex: none;\n width: 7px;\n height: 7px;\n}\n\n.LOjPTW_changelogDate {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n margin-left: 8px;\n font-weight: 500;\n}\n\n.LOjPTW_changelogBlock {\n flex-direction: column;\n gap: 6px;\n display: flex;\n}\n\n.LOjPTW_changelogHeading {\n color: var(--dsw-alias-label-secondary);\n font-size: 12.5px;\n font-weight: 600;\n}\n\n.LOjPTW_changelogList {\n color: var(--dsw-alias-label-primary);\n flex-direction: column;\n gap: 6px;\n margin: 0;\n padding-left: 18px;\n font-size: 13px;\n line-height: 1.55;\n display: flex;\n}\n\n.LOjPTW_changelogList li + li {\n margin: 0;\n}\n\n.LOjPTW_changelogCode {\n background: var(--dsw-alias-bg-layer-3);\n border: 1px solid var(--dsw-alias-border-l2);\n word-break: break-all;\n border-radius: 6px;\n padding: 1px 6px;\n font-size: 12px;\n}\n";document.head.appendChild(s);})();
7
+ (function(){var id="dsh-rez-suite-css";if(typeof document==="undefined"||document.getElementById(id))return;var s=document.createElement("style");s.id=id;s.textContent=".r9dlUa_wordmark {\n min-width: 0;\n color: inherit;\n align-items: center;\n gap: 8px;\n display: inline-flex;\n overflow: hidden;\n}\n\n.r9dlUa_mark {\n object-fit: contain;\n flex: none;\n width: 28px;\n height: 28px;\n display: block;\n}\n\n.r9dlUa_markOnly {\n object-fit: contain;\n width: 100%;\n max-width: 28px;\n height: 100%;\n max-height: 28px;\n display: block;\n}\n\n.r9dlUa_name {\n letter-spacing: .02em;\n white-space: nowrap;\n text-overflow: ellipsis;\n font-size: 15px;\n font-weight: 650;\n overflow: hidden;\n}\n\n.r9dlUa_badge {\n letter-spacing: .06em;\n opacity: .72;\n white-space: nowrap;\n border: 1px solid;\n border-radius: 4px;\n flex: none;\n padding: 2px 6px;\n font-size: 10px;\n font-weight: 700;\n line-height: 1.2;\n}\n.LOjPTW_shell {\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n display: flex;\n}\n\n.LOjPTW_card {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-3);\n border-radius: 12px;\n list-style: none;\n transition: border-color .16s, background .16s;\n}\n\n.LOjPTW_card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n.LOjPTW_cardOpen {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n\n.LOjPTW_header {\n appearance: none;\n width: 100%;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 12px;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n display: flex;\n}\n\n.LOjPTW_header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n.LOjPTW_headText {\n flex-direction: column;\n flex: 1;\n gap: 4px;\n min-width: 0;\n display: flex;\n}\n\n.LOjPTW_name {\n color: var(--dsw-alias-label-primary);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.LOjPTW_description {\n color: var(--dsw-alias-label-tertiary);\n font-size: 13px;\n line-height: 1.5;\n}\n\n.LOjPTW_chevron {\n color: var(--dsw-alias-label-tertiary);\n flex: none;\n transition: transform .16s;\n}\n\n.LOjPTW_chevronOpen {\n transform: rotate(180deg);\n}\n\n.LOjPTW_body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px 12px;\n padding-top: 8px;\n}\n\n.LOjPTW_tabBar {\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n flex: none;\n gap: 2px;\n display: flex;\n}\n\n.LOjPTW_tab {\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n background: none;\n border: none;\n border-bottom: 2px solid #0000;\n border-radius: 6px 6px 0 0;\n padding: 7px 14px;\n font-size: 13px;\n}\n\n.LOjPTW_tab:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_tab[data-active] {\n color: var(--dsw-alias-label-primary);\n border-bottom-color: var(--dsw-alias-state-business-primary);\n font-weight: 600;\n}\n\n.LOjPTW_tabBody {\n flex-direction: column;\n gap: 12px;\n min-width: 0;\n padding: 12px 0 4px;\n display: flex;\n}\n\n.LOjPTW_section {\n border: 1px solid var(--dsw-alias-border-l1);\n background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-base));\n border-radius: 10px;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n display: flex;\n}\n\n.LOjPTW_sectionTitle {\n color: var(--dsw-alias-label-primary);\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n}\n\n.LOjPTW_field {\n flex-direction: column;\n align-items: stretch;\n gap: 4px;\n display: flex;\n}\n\n.LOjPTW_label {\n color: var(--dsw-alias-label-secondary);\n font-size: 12.5px;\n}\n\n.LOjPTW_input, .LOjPTW_select {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n outline: none;\n padding: 6px 8px;\n font-size: 13px;\n}\n\n.LOjPTW_checkbox {\n justify-self: start;\n width: 15px;\n height: 15px;\n}\n\n.LOjPTW_wecomRow {\n grid-template-columns: minmax(7rem, .8fr) minmax(8rem, 1fr) minmax(8rem, 1fr) auto;\n align-items: end;\n gap: 8px;\n display: grid;\n}\n\n.LOjPTW_wecomRow .LOjPTW_message {\n grid-column: 1 / -1;\n}\n\n.LOjPTW_toolbar {\n flex-wrap: wrap;\n flex: none;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.LOjPTW_button {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n border-radius: 8px;\n padding: 7px 12px;\n font-size: 13px;\n}\n\n.LOjPTW_button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_button:disabled {\n opacity: .4;\n cursor: default;\n}\n\n.LOjPTW_primaryButton {\n background: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n\n.LOjPTW_dangerButton {\n background: var(--dsw-alias-state-danger, #c62828);\n border-color: var(--dsw-alias-state-danger, #c62828);\n color: #fff;\n}\n\n.LOjPTW_qr {\n background: #fff;\n border-radius: 8px;\n width: 200px;\n height: 200px;\n margin: 8px 0;\n display: block;\n}\n\n.LOjPTW_message {\n color: var(--dsw-alias-label-secondary);\n margin: 0;\n font-size: 12.5px;\n line-height: 1.5;\n}\n\n.LOjPTW_error {\n color: var(--dsw-alias-state-danger, #c62828);\n}\n\n.LOjPTW_tableWrap {\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n overflow: auto;\n}\n\n.LOjPTW_table {\n border-collapse: collapse;\n width: 100%;\n font-size: 12.5px;\n}\n\n.LOjPTW_table th {\n z-index: 1;\n text-align: left;\n background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));\n padding: 8px 10px;\n position: sticky;\n top: 0;\n}\n\n.LOjPTW_table td {\n border-top: 1px solid var(--dsw-alias-border-l1);\n vertical-align: top;\n padding: 8px 10px;\n}\n\n.LOjPTW_badge {\n border-radius: 999px;\n padding: 2px 8px;\n font-size: 11px;\n display: inline-block;\n}\n\n.LOjPTW_badgeOk {\n color: #229a55;\n background: #229a5524;\n}\n\n.LOjPTW_badgeFail {\n color: #c62828;\n background: #c6282824;\n}\n\n.LOjPTW_cards {\n grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));\n gap: 8px;\n display: grid;\n}\n\n.LOjPTW_stat {\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n padding: 10px;\n}\n\n.LOjPTW_cardLabel {\n color: var(--dsw-alias-label-tertiary);\n font-size: 11px;\n}\n\n.LOjPTW_cardValue {\n color: var(--dsw-alias-label-primary);\n font-size: 15px;\n font-weight: 600;\n}\n\n.LOjPTW_upgradeLayer {\n flex: none;\n align-items: center;\n width: 100%;\n height: 42px;\n margin: 8px 0 0;\n display: flex;\n position: relative;\n}\n\n.LOjPTW_upgradeLayerRail {\n width: 36px;\n height: 36px;\n margin: 0;\n}\n\n.LOjPTW_upgradeButton {\n width: calc(100% + 4px);\n height: 42px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: none;\n border: none;\n border-radius: 12px;\n align-items: center;\n gap: 8px;\n margin: 0 -2px;\n padding: 0 10px 0 8px;\n font-family: inherit;\n font-size: 14px;\n display: inline-flex;\n overflow: hidden;\n}\n\n.LOjPTW_upgradeButton:hover, .LOjPTW_upgradeButton[data-outdated] {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.LOjPTW_upgradeButton:disabled {\n cursor: progress;\n opacity: .72;\n}\n\n.LOjPTW_upgradeButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n.LOjPTW_upgradeLayerRail .LOjPTW_upgradeButton {\n border-radius: 50%;\n justify-content: center;\n gap: 0;\n width: 36px;\n height: 36px;\n padding: 0;\n}\n\n.LOjPTW_upgradeLabel {\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n overflow: hidden;\n}\n\n.LOjPTW_upgradeMeta {\n color: var(--dsw-alias-label-tertiary);\n font-variant-numeric: tabular-nums;\n flex: none;\n margin-left: auto;\n font-size: 12px;\n line-height: 16px;\n}\n\n.LOjPTW_upgradeDot {\n background: var(--dsw-alias-brand-primary);\n border-radius: 50%;\n flex: none;\n width: 7px;\n height: 7px;\n}\n\n.LOjPTW_changelogDate {\n font-variant-numeric: tabular-nums;\n color: var(--dsw-alias-label-tertiary);\n margin-left: 8px;\n font-weight: 500;\n}\n\n.LOjPTW_changelogBlock {\n flex-direction: column;\n gap: 6px;\n display: flex;\n}\n\n.LOjPTW_changelogHeading {\n color: var(--dsw-alias-label-secondary);\n font-size: 12.5px;\n font-weight: 600;\n}\n\n.LOjPTW_changelogList {\n color: var(--dsw-alias-label-primary);\n flex-direction: column;\n gap: 6px;\n margin: 0;\n padding-left: 18px;\n font-size: 13px;\n line-height: 1.55;\n display: flex;\n}\n\n.LOjPTW_changelogList li + li {\n margin: 0;\n}\n\n.LOjPTW_changelogCode {\n background: var(--dsw-alias-bg-layer-3);\n border: 1px solid var(--dsw-alias-border-l2);\n word-break: break-all;\n border-radius: 6px;\n padding: 1px 6px;\n font-size: 12px;\n}\n";document.head.appendChild(s);})();
8
8
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
9
9
  let react = require("react");
10
10
  let react_jsx_runtime = require("react/jsx-runtime");
@@ -139,7 +139,7 @@ const zh = {
139
139
  "entry.label": "Rez",
140
140
  "entry.tooltip": "Rez Suite 配置与审计面板",
141
141
  "settings.title": "ReZ-TI",
142
- "settings.description": "公司 MCP(Odoo / Nextcloud / HA / TAPD / GSC)。企业微信栏暂隐。个人微信在「微信」页签扫码。",
142
+ "settings.description": "公司 MCP(Odoo / Nextcloud / HA / TAPD / GSC)和企业微信智能机器人。个人微信在「微信」页签扫码。",
143
143
  "panel.title": "Rez Suite",
144
144
  "tab.config": "配置",
145
145
  "tab.weixin": "微信",
@@ -175,6 +175,12 @@ const zh = {
175
175
  "config.wecom.botId": "Bot ID",
176
176
  "config.wecom.secret": "Secret",
177
177
  "config.wecom.webhook": "群机器人 Webhook",
178
+ "config.wecom.hint": "Accio 同款:一个房间绑一个智能机器人。同事在企微跟哪个 bot 说话,就进网页左侧那个房间。密钥只存本机 ~/.dsh/wecom-channels.json,不要写进 compose。BotID / Secret 在企业微信管理后台 → 智能机器人。",
179
+ "config.wecom.room": "房间",
180
+ "config.wecom.add": "添加绑定",
181
+ "config.wecom.remove": "删除",
182
+ "config.wecom.empty": "还没有绑定。给人事/财务/法务等房间各建一个企微智能机器人,就能变成不同角色。",
183
+ "config.wecom.status": "通道",
178
184
  "config.ha": "Home Assistant",
179
185
  "config.ha.url": "HA MCP URL",
180
186
  "config.ha.token": "长期访问令牌",
@@ -268,7 +274,7 @@ const en = {
268
274
  "entry.label": "Rez",
269
275
  "entry.tooltip": "Rez Suite configuration and audit panel",
270
276
  "settings.title": "ReZ-TI",
271
- "settings.description": "Company MCP (Odoo / Nextcloud / HA / TAPD / GSC). WeCom settings are hidden for now. Personal WeChat is the WeChat tab.",
277
+ "settings.description": "Company MCP (Odoo / Nextcloud / HA / TAPD / GSC) and WeCom AI bots. Personal WeChat is the WeChat tab.",
272
278
  "panel.title": "Rez Suite",
273
279
  "tab.config": "Config",
274
280
  "tab.weixin": "WeChat",
@@ -304,6 +310,12 @@ const en = {
304
310
  "config.wecom.botId": "Bot ID",
305
311
  "config.wecom.secret": "Secret",
306
312
  "config.wecom.webhook": "Group bot webhook",
313
+ "config.wecom.hint": "Accio-style: one WeCom AI bot per Harness room. Whoever talks to that bot lands in that sidebar folder. Secrets stay in ~/.dsh/wecom-channels.json, not compose. BotID / Secret come from WeCom admin → AI bot.",
314
+ "config.wecom.room": "Room",
315
+ "config.wecom.add": "Add binding",
316
+ "config.wecom.remove": "Remove",
317
+ "config.wecom.empty": "No bots yet. Bind a WeCom AI bot to HR / finance / legal / … so each room is a different role.",
318
+ "config.wecom.status": "Channel",
307
319
  "config.ha": "Home Assistant",
308
320
  "config.ha.url": "HA MCP URL",
309
321
  "config.ha.token": "Long-lived access token",
@@ -409,6 +421,7 @@ const REZ_API = {
409
421
  auditReset: "/api/dsh-rez-suite/audit/reset",
410
422
  weixin: "/api/dsh-rez-suite/weixin",
411
423
  weixinVerify: "/api/dsh-rez-suite/weixin/verify",
424
+ wecomChannels: "/api/dsh-rez-suite/wecom/channels",
412
425
  update: "/api/dsh-rez-suite/update"
413
426
  };
414
427
  //#endregion
@@ -471,6 +484,16 @@ var RezApi = class {
471
484
  async weixinLogout() {
472
485
  return readJson(await fetch(REZ_API.weixin, { method: "DELETE" }));
473
486
  }
487
+ async wecomChannels() {
488
+ return readJson(await fetch(REZ_API.wecomChannels));
489
+ }
490
+ async saveWecomChannels(bots) {
491
+ return readJson(await fetch(REZ_API.wecomChannels, {
492
+ method: "PUT",
493
+ headers: { "content-type": "application/json" },
494
+ body: JSON.stringify({ bots })
495
+ }));
496
+ }
474
497
  async updateStatus() {
475
498
  return readJson(await fetch(REZ_API.update));
476
499
  }
@@ -550,7 +573,8 @@ var panel_module_default = {
550
573
  "upgradeLabel": "LOjPTW_upgradeLabel",
551
574
  "upgradeLayer": "LOjPTW_upgradeLayer",
552
575
  "upgradeLayerRail": "LOjPTW_upgradeLayerRail",
553
- "upgradeMeta": "LOjPTW_upgradeMeta"
576
+ "upgradeMeta": "LOjPTW_upgradeMeta",
577
+ "wecomRow": "LOjPTW_wecomRow"
554
578
  };
555
579
  //#endregion
556
580
  //#region src/client/panel/AuditTab.tsx
@@ -719,6 +743,20 @@ function AuditTab({ api }) {
719
743
  //#endregion
720
744
  //#region src/changelog.ts
721
745
  const SUITE_CHANGELOG = [
746
+ {
747
+ version: "0.1.49",
748
+ date: "2026-08-19",
749
+ sections: [{
750
+ title: {
751
+ zh: "企业微信智能机器人",
752
+ en: "WeCom AI bots"
753
+ },
754
+ items: [{
755
+ zh: "设置页按房间绑定企微智能机器人(BotID + Secret)。同事在企微跟哪个 bot 说话,就进网页左侧那个房间。密钥只存本机,不进 compose。",
756
+ en: "Bind a WeCom AI bot (BotID + Secret) per Harness room. Talk to that bot in WeCom and the thread lands in that sidebar folder. Secrets stay on disk, not in compose."
757
+ }]
758
+ }]
759
+ },
722
760
  {
723
761
  version: "0.1.48",
724
762
  date: "2026-08-19",
@@ -910,6 +948,128 @@ const EMPTY_SECRETS = {
910
948
  tianyancha: "",
911
949
  esphome: ""
912
950
  };
951
+ const WECOM_SECRET_MASK = "********";
952
+ function WecomSection({ rooms, drafts, status, onChange }) {
953
+ const t = useT();
954
+ const used = new Set(drafts.map((row) => row.room));
955
+ const unused = rooms.filter((room) => !used.has(room));
956
+ const hintFor = (room) => status.find((row) => row.room === room)?.hint ?? "";
957
+ const add = () => {
958
+ const room = unused[0];
959
+ if (room === void 0) return;
960
+ onChange([...drafts, {
961
+ key: `new-${room}-${Date.now()}`,
962
+ room,
963
+ botId: "",
964
+ secret: "",
965
+ secretConfigured: false
966
+ }]);
967
+ };
968
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
969
+ title: t("config.wecom"),
970
+ children: [
971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
972
+ className: panel_module_default.message,
973
+ children: t("config.wecom.hint")
974
+ }),
975
+ drafts.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
976
+ className: panel_module_default.message,
977
+ children: t("config.wecom.empty")
978
+ }),
979
+ drafts.map((row, index) => {
980
+ const options = rooms.filter((room) => room === row.room || !used.has(room));
981
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
982
+ className: panel_module_default.wecomRow,
983
+ children: [
984
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
985
+ label: t("config.wecom.room"),
986
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
987
+ className: panel_module_default.select,
988
+ value: row.room,
989
+ onChange: (event) => {
990
+ const next = drafts.slice();
991
+ next[index] = {
992
+ ...row,
993
+ room: event.target.value
994
+ };
995
+ onChange(next);
996
+ },
997
+ children: options.map((room) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
998
+ value: room,
999
+ children: room
1000
+ }, room))
1001
+ })
1002
+ }),
1003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1004
+ label: t("config.wecom.botId"),
1005
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1006
+ className: panel_module_default.input,
1007
+ autoComplete: "off",
1008
+ value: row.botId,
1009
+ onChange: (event) => {
1010
+ const next = drafts.slice();
1011
+ next[index] = {
1012
+ ...row,
1013
+ botId: event.target.value
1014
+ };
1015
+ onChange(next);
1016
+ }
1017
+ })
1018
+ }),
1019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1020
+ label: t("config.wecom.secret"),
1021
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1022
+ className: panel_module_default.input,
1023
+ type: "password",
1024
+ autoComplete: "off",
1025
+ value: row.secret,
1026
+ placeholder: row.secretConfigured ? t("config.secretConfigured") : t("config.secretMissing"),
1027
+ onChange: (event) => {
1028
+ const next = drafts.slice();
1029
+ next[index] = {
1030
+ ...row,
1031
+ secret: event.target.value
1032
+ };
1033
+ onChange(next);
1034
+ }
1035
+ })
1036
+ }),
1037
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1038
+ type: "button",
1039
+ className: panel_module_default.button,
1040
+ onClick: () => {
1041
+ onChange(drafts.filter((_, i) => i !== index));
1042
+ },
1043
+ children: t("config.wecom.remove")
1044
+ }),
1045
+ hintFor(row.room) !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1046
+ className: panel_module_default.message,
1047
+ children: hintFor(row.room)
1048
+ })
1049
+ ]
1050
+ }, row.key);
1051
+ }),
1052
+ unused.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1053
+ className: panel_module_default.toolbar,
1054
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1055
+ type: "button",
1056
+ className: panel_module_default.button,
1057
+ onClick: add,
1058
+ children: t("config.wecom.add")
1059
+ })
1060
+ })
1061
+ ]
1062
+ });
1063
+ }
1064
+ function toWecomDrafts(bots) {
1065
+ return bots.map((bot, index) => ({
1066
+ key: `${bot.room}-${index}`,
1067
+ room: bot.room,
1068
+ botId: bot.botId,
1069
+ secret: "",
1070
+ secretConfigured: bot.secretConfigured
1071
+ }));
1072
+ }
913
1073
  function Field({ label, children }) {
914
1074
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
915
1075
  className: panel_module_default.field,
@@ -1003,9 +1163,38 @@ function ConfigTab({ api }) {
1003
1163
  const [message, setMessage] = (0, react.useState)("");
1004
1164
  const [error, setError] = (0, react.useState)("");
1005
1165
  const [results, setResults] = (0, react.useState)([]);
1166
+ const [wecomRooms, setWecomRooms] = (0, react.useState)([]);
1167
+ const [wecomDrafts, setWecomDrafts] = (0, react.useState)([]);
1168
+ const [wecomStatus, setWecomStatus] = (0, react.useState)([]);
1006
1169
  const load = async () => {
1007
1170
  try {
1008
1171
  setConfig(await api.getConfig());
1172
+ try {
1173
+ const wecom = await api.wecomChannels();
1174
+ setWecomRooms(wecom.rooms.length > 0 ? wecom.rooms : [
1175
+ "搞钱",
1176
+ "产品",
1177
+ "运营",
1178
+ "人事助理",
1179
+ "财务助理",
1180
+ "法务助理",
1181
+ "设计",
1182
+ "实验室"
1183
+ ]);
1184
+ setWecomDrafts(toWecomDrafts(wecom.bots));
1185
+ setWecomStatus(wecom.status);
1186
+ } catch {
1187
+ setWecomRooms([
1188
+ "搞钱",
1189
+ "产品",
1190
+ "运营",
1191
+ "人事助理",
1192
+ "财务助理",
1193
+ "法务助理",
1194
+ "设计",
1195
+ "实验室"
1196
+ ]);
1197
+ }
1009
1198
  } catch (err) {
1010
1199
  setError(errorMessage(err));
1011
1200
  } finally {
@@ -1092,6 +1281,14 @@ function ConfigTab({ api }) {
1092
1281
  const saved = await api.saveConfig(payload);
1093
1282
  setConfig(saved);
1094
1283
  setSecrets(EMPTY_SECRETS);
1284
+ const wecomSaved = await api.saveWecomChannels(wecomDrafts.map((row) => ({
1285
+ room: row.room,
1286
+ botId: row.botId.trim(),
1287
+ secret: row.secret.trim().length > 0 ? row.secret.trim() : row.secretConfigured ? WECOM_SECRET_MASK : ""
1288
+ })));
1289
+ setWecomRooms(wecomSaved.rooms);
1290
+ setWecomDrafts(toWecomDrafts(wecomSaved.bots));
1291
+ setWecomStatus(wecomSaved.status);
1095
1292
  setMessage(t("config.saved"));
1096
1293
  } catch (err) {
1097
1294
  setError(errorMessage(err));
@@ -1205,6 +1402,12 @@ function ConfigTab({ api }) {
1205
1402
  usernameLabel: t("config.nextcloud.username"),
1206
1403
  secretLabel: t("config.nextcloud.appPassword")
1207
1404
  }),
1405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WecomSection, {
1406
+ rooms: wecomRooms,
1407
+ drafts: wecomDrafts,
1408
+ status: wecomStatus,
1409
+ onChange: setWecomDrafts
1410
+ }),
1208
1411
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(McpSection, {
1209
1412
  title: t("config.ha"),
1210
1413
  row: config.connections.homeassistant,
package/lib/index.js CHANGED
@@ -2935,7 +2935,7 @@ const COMPOSED_SERVERS = [
2935
2935
  "tianyancha",
2936
2936
  "esphome"
2937
2937
  ];
2938
- /** Front panel hides these until install/docs are ready. Backend MCP can still start if a secret exists. TAPD 1154488930001001224. */
2938
+ /** Front panel hides webhook MCP wechat status; AI bots live in the Config WeCom bindings. TAPD 1154488930001001231. */
2939
2939
  const FRONT_HIDDEN_SERVERS = /* @__PURE__ */ new Set(["wechat"]);
2940
2940
  function mcpToolPrefix(server) {
2941
2941
  if (server === "qcc") return "mcp__qcc-";
@@ -3062,6 +3062,7 @@ const REZ_API = {
3062
3062
  auditReset: "/api/dsh-rez-suite/audit/reset",
3063
3063
  weixin: "/api/dsh-rez-suite/weixin",
3064
3064
  weixinVerify: "/api/dsh-rez-suite/weixin/verify",
3065
+ wecomChannels: "/api/dsh-rez-suite/wecom/channels",
3065
3066
  update: "/api/dsh-rez-suite/update"
3066
3067
  };
3067
3068
  //#endregion
@@ -7719,9 +7720,10 @@ function toolAbortedBeforeDispatchResult(prior) {
7719
7720
  //#endregion
7720
7721
  //#region src/esphome-lab.ts
7721
7722
  /**
7722
- * ESPHome lab: official dsh-mcp-client → lab MCP, plus HTTP fallback.
7723
- * Lab server runs official `esphome compile`. Never puts firmware bytes
7724
- * into the model context; the tool returns a click-later download link.
7723
+ * ESPHome lab client in Harness: official dsh-mcp-client → rezti-lab MCP.
7724
+ * The compiler lives in Mac Mini Docker (`deploy/lab`, compose name rezti-lab).
7725
+ * Missing lab / missing token must not block `dsh web` (failOnStartupError: false).
7726
+ * Never puts firmware bytes into the model context; the tool returns a click-later download link.
7725
7727
  */
7726
7728
  const ESPHOME_MCP_URL = "http://127.0.0.1:8100/mcp";
7727
7729
  /** POST /v1/compile 只交 YAML、立刻回链接。真机 `esphome compile` 在后台,最长见实验室 `ESPHOME_COMPILE_TIMEOUT`(默认 20 分钟)。 */
package/lib/style.css CHANGED
@@ -209,6 +209,17 @@
209
209
  height: 15px;
210
210
  }
211
211
 
212
+ .LOjPTW_wecomRow {
213
+ grid-template-columns: minmax(7rem, .8fr) minmax(8rem, 1fr) minmax(8rem, 1fr) auto;
214
+ align-items: end;
215
+ gap: 8px;
216
+ display: grid;
217
+ }
218
+
219
+ .LOjPTW_wecomRow .LOjPTW_message {
220
+ grid-column: 1 / -1;
221
+ }
222
+
212
223
  .LOjPTW_toolbar {
213
224
  flex-wrap: wrap;
214
225
  flex: none;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rezti/dsh-rez-suite",
3
- "description": "ReZ-TI one-package install for DeepSeek Harness. Update with: dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.48",
4
- "version": "0.1.48",
3
+ "description": "ReZ-TI one-package install for DeepSeek Harness. Update with: dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.49",
4
+ "version": "0.1.49",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -45,7 +45,7 @@
45
45
  "@rezti/dsh-rez-sso": "^0.1.14",
46
46
  "@rezti/dsh-rez-tapd": "^0.1.1",
47
47
  "@rezti/dsh-rez-token-manager": "^0.1.4",
48
- "@rezti/dsh-rez-wechat": "^0.1.14",
48
+ "@rezti/dsh-rez-wechat": "^0.1.15",
49
49
  "@rezti/dsh-web-file-uploader": "0.2.3",
50
50
  "@modelcontextprotocol/sdk": "^1.30.0",
51
51
  "zod": "^4.4.3"
package/src/changelog.ts CHANGED
@@ -20,6 +20,21 @@ export interface ChangelogRelease {
20
20
  }
21
21
 
22
22
  export const SUITE_CHANGELOG: readonly ChangelogRelease[] = [
23
+ {
24
+ version: '0.1.49',
25
+ date: '2026-08-19',
26
+ sections: [
27
+ {
28
+ title: { zh: '企业微信智能机器人', en: 'WeCom AI bots' },
29
+ items: [
30
+ {
31
+ zh: '设置页按房间绑定企微智能机器人(BotID + Secret)。同事在企微跟哪个 bot 说话,就进网页左侧那个房间。密钥只存本机,不进 compose。',
32
+ en: 'Bind a WeCom AI bot (BotID + Secret) per Harness room. Talk to that bot in WeCom and the thread lands in that sidebar folder. Secrets stay on disk, not in compose.',
33
+ },
34
+ ],
35
+ },
36
+ ],
37
+ },
23
38
  {
24
39
  version: '0.1.48',
25
40
  date: '2026-08-19',
package/src/client/api.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  type RezUpdateApplyResult,
13
13
  type RezUpdateStatus,
14
14
  type RezWeixinStatus,
15
+ type RezWecomChannelsResponse,
15
16
  } from '../protocol.ts'
16
17
 
17
18
  export class RezApiError extends Error {
@@ -100,6 +101,20 @@ export class RezApi {
100
101
  return readJson<RezWeixinStatus>(response)
101
102
  }
102
103
 
104
+ async wecomChannels(): Promise<RezWecomChannelsResponse> {
105
+ const response = await fetch(REZ_API.wecomChannels)
106
+ return readJson<RezWecomChannelsResponse>(response)
107
+ }
108
+
109
+ async saveWecomChannels(bots: Array<{ room: string; botId: string; secret?: string }>): Promise<RezWecomChannelsResponse> {
110
+ const response = await fetch(REZ_API.wecomChannels, {
111
+ method: 'PUT',
112
+ headers: { 'content-type': 'application/json' },
113
+ body: JSON.stringify({ bots }),
114
+ })
115
+ return readJson<RezWecomChannelsResponse>(response)
116
+ }
117
+
103
118
  async updateStatus(): Promise<RezUpdateStatus> {
104
119
  const response = await fetch(REZ_API.update)
105
120
  return readJson<RezUpdateStatus>(response)
@@ -6,7 +6,7 @@ export const zh = {
6
6
  'entry.label': 'Rez',
7
7
  'entry.tooltip': 'Rez Suite 配置与审计面板',
8
8
  'settings.title': 'ReZ-TI',
9
- 'settings.description': '公司 MCP(Odoo / Nextcloud / HA / TAPD / GSC)。企业微信栏暂隐。个人微信在「微信」页签扫码。',
9
+ 'settings.description': '公司 MCP(Odoo / Nextcloud / HA / TAPD / GSC)和企业微信智能机器人。个人微信在「微信」页签扫码。',
10
10
  'panel.title': 'Rez Suite',
11
11
  'tab.config': '配置',
12
12
  'tab.weixin': '微信',
@@ -43,6 +43,12 @@ export const zh = {
43
43
  'config.wecom.botId': 'Bot ID',
44
44
  'config.wecom.secret': 'Secret',
45
45
  'config.wecom.webhook': '群机器人 Webhook',
46
+ 'config.wecom.hint': 'Accio 同款:一个房间绑一个智能机器人。同事在企微跟哪个 bot 说话,就进网页左侧那个房间。密钥只存本机 ~/.dsh/wecom-channels.json,不要写进 compose。BotID / Secret 在企业微信管理后台 → 智能机器人。',
47
+ 'config.wecom.room': '房间',
48
+ 'config.wecom.add': '添加绑定',
49
+ 'config.wecom.remove': '删除',
50
+ 'config.wecom.empty': '还没有绑定。给人事/财务/法务等房间各建一个企微智能机器人,就能变成不同角色。',
51
+ 'config.wecom.status': '通道',
46
52
  'config.ha': 'Home Assistant',
47
53
  'config.ha.url': 'HA MCP URL',
48
54
  'config.ha.token': '长期访问令牌',
@@ -142,7 +148,7 @@ export const en: Record<RezKey, string> = {
142
148
  'entry.label': 'Rez',
143
149
  'entry.tooltip': 'Rez Suite configuration and audit panel',
144
150
  'settings.title': 'ReZ-TI',
145
- 'settings.description': 'Company MCP (Odoo / Nextcloud / HA / TAPD / GSC). WeCom settings are hidden for now. Personal WeChat is the WeChat tab.',
151
+ 'settings.description': 'Company MCP (Odoo / Nextcloud / HA / TAPD / GSC) and WeCom AI bots. Personal WeChat is the WeChat tab.',
146
152
  'panel.title': 'Rez Suite',
147
153
  'tab.config': 'Config',
148
154
  'tab.weixin': 'WeChat',
@@ -178,6 +184,12 @@ export const en: Record<RezKey, string> = {
178
184
  'config.wecom.botId': 'Bot ID',
179
185
  'config.wecom.secret': 'Secret',
180
186
  'config.wecom.webhook': 'Group bot webhook',
187
+ 'config.wecom.hint': 'Accio-style: one WeCom AI bot per Harness room. Whoever talks to that bot lands in that sidebar folder. Secrets stay in ~/.dsh/wecom-channels.json, not compose. BotID / Secret come from WeCom admin → AI bot.',
188
+ 'config.wecom.room': 'Room',
189
+ 'config.wecom.add': 'Add binding',
190
+ 'config.wecom.remove': 'Remove',
191
+ 'config.wecom.empty': 'No bots yet. Bind a WeCom AI bot to HR / finance / legal / … so each room is a different role.',
192
+ 'config.wecom.status': 'Channel',
181
193
  'config.ha': 'Home Assistant',
182
194
  'config.ha.url': 'HA MCP URL',
183
195
  'config.ha.token': 'Long-lived access token',
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { useEffect, useState, type ReactNode } from 'react'
7
7
  import type { RezApi } from '../api.ts'
8
- import type { RezPublicConfig, RezPublicConnection, RezRoleId, RezTestResult } from '../../protocol.ts'
8
+ import type { RezPublicConfig, RezPublicConnection, RezRoleId, RezTestResult, RezWecomBindingStatus, RezWecomBotPublic } from '../../protocol.ts'
9
9
  import { errorMessage, useT } from './helpers.ts'
10
10
  import css from './panel.module.css'
11
11
 
@@ -31,6 +31,115 @@ type SecretDrafts = {
31
31
 
32
32
  const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '', qcc: '', tianyancha: '', esphome: '' }
33
33
 
34
+ const WECOM_SECRET_MASK = '********'
35
+
36
+ type WecomDraft = {
37
+ key: string
38
+ room: string
39
+ botId: string
40
+ secret: string
41
+ secretConfigured: boolean
42
+ }
43
+
44
+ function WecomSection({
45
+ rooms,
46
+ drafts,
47
+ status,
48
+ onChange,
49
+ }: {
50
+ rooms: string[]
51
+ drafts: WecomDraft[]
52
+ status: RezWecomBindingStatus[]
53
+ onChange: (next: WecomDraft[]) => void
54
+ }) {
55
+ const t = useT()
56
+ const used = new Set(drafts.map(row => row.room))
57
+ const unused = rooms.filter(room => !used.has(room))
58
+ const hintFor = (room: string): string => status.find(row => row.room === room)?.hint ?? ''
59
+
60
+ const add = (): void => {
61
+ const room = unused[0]
62
+ if (room === undefined) return
63
+ onChange([...drafts, { key: `new-${room}-${Date.now()}`, room, botId: '', secret: '', secretConfigured: false }])
64
+ }
65
+
66
+ return (
67
+ <Section title={t('config.wecom')}>
68
+ <p className={css.message}>{t('config.wecom.hint')}</p>
69
+ {drafts.length === 0 && <p className={css.message}>{t('config.wecom.empty')}</p>}
70
+ {drafts.map((row, index) => {
71
+ const options = rooms.filter(room => room === row.room || !used.has(room))
72
+ return (
73
+ <div key={row.key} className={css.wecomRow}>
74
+ <Field label={t('config.wecom.room')}>
75
+ <select
76
+ className={css.select}
77
+ value={row.room}
78
+ onChange={event => {
79
+ const next = drafts.slice()
80
+ next[index] = { ...row, room: event.target.value }
81
+ onChange(next)
82
+ }}
83
+ >
84
+ {options.map(room => <option key={room} value={room}>{room}</option>)}
85
+ </select>
86
+ </Field>
87
+ <Field label={t('config.wecom.botId')}>
88
+ <input
89
+ className={css.input}
90
+ autoComplete="off"
91
+ value={row.botId}
92
+ onChange={event => {
93
+ const next = drafts.slice()
94
+ next[index] = { ...row, botId: event.target.value }
95
+ onChange(next)
96
+ }}
97
+ />
98
+ </Field>
99
+ <Field label={t('config.wecom.secret')}>
100
+ <input
101
+ className={css.input}
102
+ type="password"
103
+ autoComplete="off"
104
+ value={row.secret}
105
+ placeholder={row.secretConfigured ? t('config.secretConfigured') : t('config.secretMissing')}
106
+ onChange={event => {
107
+ const next = drafts.slice()
108
+ next[index] = { ...row, secret: event.target.value }
109
+ onChange(next)
110
+ }}
111
+ />
112
+ </Field>
113
+ <button
114
+ type="button"
115
+ className={css.button}
116
+ onClick={() => { onChange(drafts.filter((_, i) => i !== index)) }}
117
+ >
118
+ {t('config.wecom.remove')}
119
+ </button>
120
+ {hintFor(row.room) !== '' && <p className={css.message}>{hintFor(row.room)}</p>}
121
+ </div>
122
+ )
123
+ })}
124
+ {unused.length > 0 && (
125
+ <div className={css.toolbar}>
126
+ <button type="button" className={css.button} onClick={add}>{t('config.wecom.add')}</button>
127
+ </div>
128
+ )}
129
+ </Section>
130
+ )
131
+ }
132
+
133
+ function toWecomDrafts(bots: RezWecomBotPublic[]): WecomDraft[] {
134
+ return bots.map((bot, index) => ({
135
+ key: `${bot.room}-${index}`,
136
+ room: bot.room,
137
+ botId: bot.botId,
138
+ secret: '',
139
+ secretConfigured: bot.secretConfigured,
140
+ }))
141
+ }
142
+
34
143
  function Field({ label, children }: { label: string; children: ReactNode }) {
35
144
  return (
36
145
  <div className={css.field}>
@@ -133,10 +242,23 @@ export function ConfigTab({ api }: { api: RezApi }) {
133
242
  const [message, setMessage] = useState('')
134
243
  const [error, setError] = useState('')
135
244
  const [results, setResults] = useState<RezTestResult[]>([])
245
+ const [wecomRooms, setWecomRooms] = useState<string[]>([])
246
+ const [wecomDrafts, setWecomDrafts] = useState<WecomDraft[]>([])
247
+ const [wecomStatus, setWecomStatus] = useState<RezWecomBindingStatus[]>([])
136
248
 
137
249
  const load = async (): Promise<void> => {
138
250
  try {
139
251
  setConfig(await api.getConfig())
252
+ try {
253
+ const wecom = await api.wecomChannels()
254
+ setWecomRooms(wecom.rooms.length > 0 ? wecom.rooms : [
255
+ '搞钱', '产品', '运营', '人事助理', '财务助理', '法务助理', '设计', '实验室',
256
+ ])
257
+ setWecomDrafts(toWecomDrafts(wecom.bots))
258
+ setWecomStatus(wecom.status)
259
+ } catch {
260
+ setWecomRooms(['搞钱', '产品', '运营', '人事助理', '财务助理', '法务助理', '设计', '实验室'])
261
+ }
140
262
  } catch (err) {
141
263
  setError(errorMessage(err))
142
264
  } finally {
@@ -184,6 +306,14 @@ export function ConfigTab({ api }: { api: RezApi }) {
184
306
  const saved = await api.saveConfig(payload)
185
307
  setConfig(saved)
186
308
  setSecrets(EMPTY_SECRETS)
309
+ const wecomSaved = await api.saveWecomChannels(wecomDrafts.map(row => ({
310
+ room: row.room,
311
+ botId: row.botId.trim(),
312
+ secret: row.secret.trim().length > 0 ? row.secret.trim() : (row.secretConfigured ? WECOM_SECRET_MASK : ''),
313
+ })))
314
+ setWecomRooms(wecomSaved.rooms)
315
+ setWecomDrafts(toWecomDrafts(wecomSaved.bots))
316
+ setWecomStatus(wecomSaved.status)
187
317
  setMessage(t('config.saved'))
188
318
  } catch (err) {
189
319
  setError(errorMessage(err))
@@ -251,7 +381,12 @@ export function ConfigTab({ api }: { api: RezApi }) {
251
381
  usernameLabel={t('config.nextcloud.username')}
252
382
  secretLabel={t('config.nextcloud.appPassword')}
253
383
  />
254
- {/* WeCom (企业微信) 设置栏暂隐:安装用法未定,TAPD 1154488930001001224。保存时仍回写已有 wechat 配置,不擦掉。 */}
384
+ <WecomSection
385
+ rooms={wecomRooms}
386
+ drafts={wecomDrafts}
387
+ status={wecomStatus}
388
+ onChange={setWecomDrafts}
389
+ />
255
390
  <McpSection
256
391
  title={t('config.ha')}
257
392
  row={config.connections.homeassistant}
@@ -169,6 +169,17 @@
169
169
  height: 15px;
170
170
  }
171
171
 
172
+ .wecomRow {
173
+ display: grid;
174
+ grid-template-columns: minmax(7rem, 0.8fr) minmax(8rem, 1fr) minmax(8rem, 1fr) auto;
175
+ gap: 8px;
176
+ align-items: end;
177
+ }
178
+
179
+ .wecomRow .message {
180
+ grid-column: 1 / -1;
181
+ }
182
+
172
183
  .toolbar {
173
184
  display: flex;
174
185
  align-items: center;
@@ -1,7 +1,8 @@
1
1
  /**
2
- * ESPHome lab: official dsh-mcp-client → lab MCP, plus HTTP fallback.
3
- * Lab server runs official `esphome compile`. Never puts firmware bytes
4
- * into the model context; the tool returns a click-later download link.
2
+ * ESPHome lab client in Harness: official dsh-mcp-client → rezti-lab MCP.
3
+ * The compiler lives in Mac Mini Docker (`deploy/lab`, compose name rezti-lab).
4
+ * Missing lab / missing token must not block `dsh web` (failOnStartupError: false).
5
+ * Never puts firmware bytes into the model context; the tool returns a click-later download link.
5
6
  */
6
7
 
7
8
  import { loadRezConnections, readManagedCredential, REZ_CREDENTIAL_REFS, startMcpWhenSecret } from '@rezti/dsh-rez-sso'
@@ -14,7 +14,7 @@ export const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant',
14
14
 
15
15
  export type ComposedServer = (typeof COMPOSED_SERVERS)[number]
16
16
 
17
- /** Front panel hides these until install/docs are ready. Backend MCP can still start if a secret exists. TAPD 1154488930001001224. */
17
+ /** Front panel hides webhook MCP wechat status; AI bots live in the Config WeCom bindings. TAPD 1154488930001001231. */
18
18
  export const FRONT_HIDDEN_SERVERS: ReadonlySet<ComposedServer> = new Set(['wechat'])
19
19
 
20
20
  export function mcpToolPrefix(server: string): string {
package/src/protocol.ts CHANGED
@@ -142,6 +142,28 @@ export interface RezWeixinStatus {
142
142
  hint: string
143
143
  }
144
144
 
145
+ /** Accio-style WeCom AI bot: one BotID+Secret per Harness room. */
146
+ export type RezWecomBindingPhase = 'idle' | 'connecting' | 'authenticated' | 'error'
147
+
148
+ export interface RezWecomBotPublic {
149
+ room: string
150
+ botId: string
151
+ secretConfigured: boolean
152
+ }
153
+
154
+ export interface RezWecomBindingStatus {
155
+ room: string
156
+ botId: string
157
+ phase: RezWecomBindingPhase
158
+ hint: string
159
+ }
160
+
161
+ export interface RezWecomChannelsResponse {
162
+ rooms: string[]
163
+ bots: RezWecomBotPublic[]
164
+ status: RezWecomBindingStatus[]
165
+ }
166
+
145
167
  /** One audit ledger row. */
146
168
  export interface RezAuditRecord {
147
169
  id: number
@@ -197,5 +219,6 @@ export const REZ_API = {
197
219
  auditReset: REZ_API_BASE + '/audit/reset',
198
220
  weixin: REZ_API_BASE + '/weixin',
199
221
  weixinVerify: REZ_API_BASE + '/weixin/verify',
222
+ wecomChannels: REZ_API_BASE + '/wecom/channels',
200
223
  update: REZ_API_BASE + '/update',
201
224
  } as const
@@ -22,21 +22,23 @@ description: 人事助理。引导员工自己领取本机安装包、申请各
22
22
 
23
23
  没有 GitHub 权限:找人事要包,不要下未知来源的安装程序。
24
24
 
25
- **发包装时说清楚:** zip/exe 只是启动壳,第一次会从外网下 Nodedsh,再装钉死的 suite(现在 `0.1.48`)。不是装完就能离线用。长期分发走 CI artifact,不要把仓里 `deploy/dist/` 当主渠道。
25
+ **发包装时说清楚:** zip/exe 只是启动壳,第一次会从外网下 Node、**pnpm**、dsh,再装钉死的 suite(现在 `0.1.48`)。不是装完就能离线用。长期分发走 CI artifact,不要把仓里 `deploy/dist/` 当主渠道。
26
26
 
27
27
  回形针上传走 npm `@rezti/dsh-web-file-uploader`,装进 suite 即可。不要再单独从 GitHub add。
28
28
 
29
29
  ### Mac
30
30
 
31
31
  1. 解压 zip,把 **ReZ-TI Harness** 拖进「应用程序」。
32
- 2. 第一次:**右键 打开**(未签名)。会下载 Node、钉稳定 dsh、拉 **0.1.48** suite(不是 latest:当天发的 latest 可能被 24 小时冷却拦住)。
33
- 3. 浏览器打开 `http://127.0.0.1:3080/`。
32
+ 2. 若提示无法打开 / 已损坏:让对方双击包里的 **首次打开.command**,或在终端执行 `xattr -cr "/Applications/ReZ-TI Harness.app"`,再**右键 打开**。
33
+ 3. 第一次会弹出终端:下载 Node、安装 pnpm 和稳定 dsh、拉 **0.1.48** suite。不是装完就能离线用。
34
+ 4. 浏览器打开 `http://127.0.0.1:3080/`。失败看 `~/Library/Application Support/ReZ-TI/harness/logs/harness.log`。
34
35
 
35
36
  ### Windows
36
37
 
37
38
  1. 运行 exe(装到 `%LOCALAPPDATA%\ReZ-TI\Harness`,一般不需要管理员)。
38
- 2. 桌面 / 开始菜单点 **ReZ-TI Harness**。第一次同样拉 Node / dsh / 钉死 suite。
39
- 3. 打开 `http://127.0.0.1:3080/`。
39
+ 2. 若提示「Windows 已保护你的电脑」:让对方点 **更多信息 仍要运行**。未签名,不是病毒。
40
+ 3. 桌面 / 开始菜单点 **ReZ-TI Harness**。会弹出黑色命令窗口,不要立刻关掉。第一次拉 Node、**pnpm**、dsh、钉死 suite。
41
+ 4. 打开 `http://127.0.0.1:3080/`。窗口一闪而过:看 `%LOCALAPPDATA%\ReZ-TI\harness\logs\harness.log`。ARM 电脑暂不支持。
40
42
 
41
43
  密钥写在本机 `~/.dsh`(Windows `%USERPROFILE%\.dsh`)。不要删这份目录。
42
44
 
@@ -79,7 +81,7 @@ description: 人事助理。引导员工自己领取本机安装包、申请各
79
81
  | 用途 | 面板 | 谁要 | 怎么拿 |
80
82
  |------|------|------|--------|
81
83
  | TAPD Access Token | TAPD → Access Token;项目 ID 默认 `54488930`;**昵称改成自己的 TAPD 显示名**(不要留「凯文智能家居」) | 跟需求/缺陷的岗 | TAPD 个人设置生成 token。昵称必须是 TAPD 里的中文名 |
82
- | 企微群机器人 Webhook | 企业微信 → 群机器人 Webhook | 需要往指定群推送的岗 | **设置页暂不展示**(安装用法未定)。不要引导员工去申请或填写 Webhook |
84
+ | 企微智能机器人 BotID + Secret | 企业微信 → 智能机器人 | 要在企微里当某个房间角色说话的岗 | 设置页「企业微信」:选房间、填 BotID 和 Secret。一个房间一个 bot。密钥只存本机,不要发群、不要写进 compose。不要再申请群 Webhook |
83
85
  | HA 长期访问令牌 | Home Assistant → 长期访问令牌 | 只调试 HA 的岗 | HA 个人资料 → 长期访问令牌。默认 URL `http://homeassistant.local:8123/api/mcp`,现场不同再改 |
84
86
  | GSC 服务账号 JSON | GSC → 服务账号 JSON 或密钥文件路径 | 运营/SEO | 向凯文要公司服务账号文件路径,不要个人 Google 帐号硬接 |
85
87
  | 企查查 API Key | 企查查(法务助理) | **仅法务助理房间** | 未开通就说法务房间未挂企查查。其他房间不要引导申请 |
@@ -7,7 +7,7 @@
7
7
  ## 每次开场
8
8
 
9
9
  1. YAML 来源:对方说板型/功能(ESP32-H2 / C3 等)你就写完整 YAML;或回形针 `.yaml` / `.yml`;或把全文贴进对话。缺 `esphome.name` 就补。密钥不要写进 MEMORY,也不要放到公开下载目录。
10
- 2. 调 `mcp__esphome__compile`。没有 MCP 再用 `rez_esphome_compile`。未配置实验室 URL / Token 就说「实验室编译未开通」,指向 `deploy/esphome-lab/README.md`。不要自写编译器,不要自写 Device Builder WebSocket
10
+ 2. 调 `mcp__esphome__compile`。没有 MCP 再用 `rez_esphome_compile`。未配置实验室 URL / Token 就说「实验室编译未开通」,指向 `deploy/lab/README.md`。不要自写编译器,不要自写 Device Builder WebSocket。实验室 Docker 没起时 Harness 仍可用,不要假装已经编完。
11
11
  3. 工具立刻返回 **download 链接**。把链接发给对方去点。编译要几分钟:先点开是 `README.md`(编译中或失败日志);成功后再点/刷新是 `firmware.bin`。有 Nextcloud 时链接是公开目录。**不要把 bin 贴进对话。**
12
12
  4. 默认不 OTA、不替对方刷机。对方明确要求刷机再另说,并先征得批准。
13
13