@rezti/dsh-rez-suite 0.1.47 → 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 +10 -2
- package/README.md +4 -4
- package/README.zh.md +1 -1
- package/cordis.patch.yml +1 -1
- package/lib/client.d.ts +7 -1
- package/lib/client.js +294 -74
- package/lib/index.js +6 -4
- package/lib/style.css +11 -0
- package/package.json +4 -4
- package/src/changelog.ts +30 -0
- package/src/client/api.ts +15 -0
- package/src/client/locales.ts +14 -2
- package/src/client/panel/ConfigTab.tsx +137 -2
- package/src/client/panel/panel.module.css +11 -0
- package/src/esphome-lab.ts +4 -3
- package/src/observe-mcp.ts +1 -1
- package/src/protocol.ts +23 -0
- package/templates/staff/hr/.agents/skills/staff-onboard-keys/SKILL.md +8 -6
- package/templates/staff/lab/AGENTS.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,12 +5,20 @@
|
|
|
5
5
|
升级:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.
|
|
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
|
-
发版顺序:`
|
|
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 都没绑时兼容。
|
|
18
|
+
|
|
19
|
+
## 0.1.48 — 2026-08-19
|
|
20
|
+
|
|
21
|
+
- **启动修复**:0.1.47 网页 `Failed to load plugins`,因为客户端还按 `dsh-web-file-uploader` 注册,DSH 要的是 `@rezti/dsh-web-file-uploader`。回形针升到 `0.2.3`。
|
|
14
22
|
|
|
15
23
|
## 0.1.47 — 2026-08-19
|
|
16
24
|
|
package/README.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
雷泽插件的**唯一安装入口**。Odoo / 企微 / HA / TAPD / GSC / 企查查(仅法务助理房间) / 天眼查(仅老板) / 工作区(老板三室 + 全员助理)都由这一包拉齐,不要再逐个 `plugin add`,**不要**再装 `@rezti/dsh-rez-boss-desk`。
|
|
4
4
|
|
|
5
|
-
发版顺序(必做):先发 `@rezti/dsh-rez-
|
|
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
|
|
|
9
|
-
回形针走 npm 包 `@rezti/dsh-web-file-uploader@0.2.
|
|
9
|
+
回形针走 npm 包 `@rezti/dsh-web-file-uploader@0.2.3`(不要 GitHub:DSH 会拦子依赖 git,0.1.46 因此装不上)。
|
|
10
10
|
|
|
11
|
-
员工机不要 `SUITE_VERSION=latest`:新 profile 默认 `minimumReleaseAge: 1440`,当天发的包可能装不到。钉死 `0.1.
|
|
11
|
+
员工机不要 `SUITE_VERSION=latest`:新 profile 默认 `minimumReleaseAge: 1440`,当天发的包可能装不到。钉死 `0.1.48`,或把 profile 的 `minimumReleaseAge` 改成 `0`。
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# 第一次 / 升级(钉死版本,避开 24h 发布冷却)
|
|
15
|
-
dsh plugin --profile web add @rezti/dsh-rez-suite@0.1.
|
|
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.
|
|
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.
|
|
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)
|
|
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
|
|
@@ -718,81 +742,112 @@ function AuditTab({ api }) {
|
|
|
718
742
|
}
|
|
719
743
|
//#endregion
|
|
720
744
|
//#region src/changelog.ts
|
|
721
|
-
const SUITE_CHANGELOG = [
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
items: [{
|
|
730
|
-
zh: "0.1.46 装不上:DSH 禁止插件子依赖走 GitHub。回形针已改成 npm 包 @rezti/dsh-web-file-uploader@0.2.2。请装 0.1.47,不要再装 0.1.46。",
|
|
731
|
-
en: "0.1.46 cannot install: DSH blocks GitHub git deps as plugin subdependencies. The paperclip plugin is now @rezti/dsh-web-file-uploader@0.2.2 on npm. Install 0.1.47, not 0.1.46."
|
|
732
|
-
}, {
|
|
733
|
-
zh: "设置 → 插件 → ReZ-TI → 更新日志:能看到本页说明。",
|
|
734
|
-
en: "Settings → Plugins → ReZ-TI → Changelog shows these notes."
|
|
735
|
-
}]
|
|
736
|
-
}]
|
|
737
|
-
}, {
|
|
738
|
-
version: "0.1.46",
|
|
739
|
-
date: "2026-08-19",
|
|
740
|
-
sections: [{
|
|
741
|
-
title: {
|
|
742
|
-
zh: "员工会碰到的",
|
|
743
|
-
en: "What you will see"
|
|
744
|
-
},
|
|
745
|
-
items: [
|
|
746
|
-
{
|
|
747
|
-
zh: "回形针上传 PDF:说明书 / 规格书走输入栏回形针,先入 Marketing/Inbox,对应 TAPD 改成测试中并转凯文;凯文说审批了哪份,再进 Marketing/Publish。不要用微信发 PDF。",
|
|
748
|
-
en: "Paperclip PDF uploads: manuals go to Marketing/Inbox first. TAPD moves to 测试中 and Kelvin; after he names the file, it lands in Marketing/Publish. Do not send PDFs over WeChat."
|
|
749
|
-
},
|
|
750
|
-
{
|
|
751
|
-
zh: "运营多了「设计」房间:拍摄原片 Comfy 只修一次,shot / clean 先入 Inbox;凯文终审后同时写入 Marketing/RAW。",
|
|
752
|
-
en: "Operations gets a Design room. Retouch shots once in Comfy; shot/clean stay in Inbox until Kelvin dual-saves them to Marketing/RAW."
|
|
753
|
-
},
|
|
754
|
-
{
|
|
755
|
-
zh: "品牌 / VI 走 Marketing/Inbox/Brand/,终审后写入 Marketing/RAW/Brand/,不要写进某个 SKU。",
|
|
756
|
-
en: "Brand / VI files go Marketing/Inbox/Brand/, then Marketing/RAW/Brand/ after approval — not into a SKU folder."
|
|
757
|
-
},
|
|
758
|
-
{
|
|
759
|
-
zh: "工程师多了「实验室」:对话里给 YAML,立刻拿到下载链接。还在编是说明文件;成功是 firmware.bin;失败是带日志的 README。不要在对话里等 bin,不要默认 OTA。",
|
|
760
|
-
en: "Engineers get a Lab room. Submit YAML and get a download link immediately. Compiling shows a README; success is firmware.bin; failure includes the log. Do not wait for the bin in chat. Do not OTA by default."
|
|
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"
|
|
761
753
|
},
|
|
762
|
-
{
|
|
763
|
-
zh: "
|
|
764
|
-
en: "
|
|
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
|
+
},
|
|
760
|
+
{
|
|
761
|
+
version: "0.1.48",
|
|
762
|
+
date: "2026-08-19",
|
|
763
|
+
sections: [{
|
|
764
|
+
title: {
|
|
765
|
+
zh: "启动修复",
|
|
766
|
+
en: "Startup fix"
|
|
765
767
|
},
|
|
766
|
-
{
|
|
767
|
-
zh: "
|
|
768
|
-
en: "
|
|
768
|
+
items: [{
|
|
769
|
+
zh: "0.1.47 网页报 Failed to load plugins:回形针客户端仍按旧名注册。已发 @rezti/dsh-web-file-uploader@0.2.3,模块 id 与包名一致。请装 0.1.48。",
|
|
770
|
+
en: "0.1.47 failed to load plugins because the paperclip client still registered under the old package name. @rezti/dsh-web-file-uploader@0.2.3 matches the scoped name. Install 0.1.48."
|
|
771
|
+
}]
|
|
772
|
+
}]
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
version: "0.1.47",
|
|
776
|
+
date: "2026-08-19",
|
|
777
|
+
sections: [{
|
|
778
|
+
title: {
|
|
779
|
+
zh: "安装修复",
|
|
780
|
+
en: "Install fix"
|
|
769
781
|
},
|
|
770
|
-
{
|
|
771
|
-
zh: "
|
|
772
|
-
en: "
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
782
|
+
items: [{
|
|
783
|
+
zh: "0.1.46 装不上:DSH 禁止插件子依赖走 GitHub。回形针已改成 npm 包 @rezti/dsh-web-file-uploader@0.2.2。请装 0.1.47,不要再装 0.1.46。",
|
|
784
|
+
en: "0.1.46 cannot install: DSH blocks GitHub git deps as plugin subdependencies. The paperclip plugin is now @rezti/dsh-web-file-uploader@0.2.2 on npm. Install 0.1.47, not 0.1.46."
|
|
785
|
+
}, {
|
|
786
|
+
zh: "设置 → 插件 → ReZ-TI → 更新日志:能看到本页说明。",
|
|
787
|
+
en: "Settings → Plugins → ReZ-TI → Changelog shows these notes."
|
|
788
|
+
}]
|
|
789
|
+
}]
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
version: "0.1.46",
|
|
793
|
+
date: "2026-08-19",
|
|
794
|
+
sections: [{
|
|
795
|
+
title: {
|
|
796
|
+
zh: "员工会碰到的",
|
|
797
|
+
en: "What you will see"
|
|
784
798
|
},
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
799
|
+
items: [
|
|
800
|
+
{
|
|
801
|
+
zh: "回形针上传 PDF:说明书 / 规格书走输入栏回形针,先入 Marketing/Inbox,对应 TAPD 改成测试中并转凯文;凯文说审批了哪份,再进 Marketing/Publish。不要用微信发 PDF。",
|
|
802
|
+
en: "Paperclip PDF uploads: manuals go to Marketing/Inbox first. TAPD moves to 测试中 and Kelvin; after he names the file, it lands in Marketing/Publish. Do not send PDFs over WeChat."
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
zh: "运营多了「设计」房间:拍摄原片 Comfy 只修一次,shot / clean 先入 Inbox;凯文终审后同时写入 Marketing/RAW。",
|
|
806
|
+
en: "Operations gets a Design room. Retouch shots once in Comfy; shot/clean stay in Inbox until Kelvin dual-saves them to Marketing/RAW."
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
zh: "品牌 / VI 走 Marketing/Inbox/Brand/,终审后写入 Marketing/RAW/Brand/,不要写进某个 SKU。",
|
|
810
|
+
en: "Brand / VI files go Marketing/Inbox/Brand/, then Marketing/RAW/Brand/ after approval — not into a SKU folder."
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
zh: "工程师多了「实验室」:对话里给 YAML,立刻拿到下载链接。还在编是说明文件;成功是 firmware.bin;失败是带日志的 README。不要在对话里等 bin,不要默认 OTA。",
|
|
814
|
+
en: "Engineers get a Lab room. Submit YAML and get a download link immediately. Compiling shows a README; success is firmware.bin; failure includes the log. Do not wait for the bin in chat. Do not OTA by default."
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
zh: "人事助理可引导新人领本机安装包、自己申请 API Key。",
|
|
818
|
+
en: "HR assistant can walk new staff through the desktop installer and API key request."
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
zh: "设置页暂时不显示企业微信配置栏(已填过的 webhook 不会被擦掉)。",
|
|
822
|
+
en: "WeCom settings are hidden for now. An existing webhook is left as-is."
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
zh: "运营仍可改 TAPD 状态、写评论、往 Inbox 传文件;不能删 TAPD / Nextcloud。",
|
|
826
|
+
en: "Operations can still update TAPD and write Inbox files, but cannot delete TAPD or Nextcloud items."
|
|
827
|
+
}
|
|
828
|
+
]
|
|
829
|
+
}, {
|
|
830
|
+
title: {
|
|
831
|
+
zh: "安装与审批机",
|
|
832
|
+
en: "Installers and approver machines"
|
|
788
833
|
},
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
834
|
+
items: [
|
|
835
|
+
{
|
|
836
|
+
zh: "员工 Mac zip / Windows exe、审批人 Docker:每次启动拉钉死的 0.1.46,不要 latest。",
|
|
837
|
+
en: "Staff Mac zip / Windows exe and approver Docker pin suite 0.1.46. Do not use latest."
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
zh: "安装包只是启动壳,第一次会从外网拉 Node 和 dsh。",
|
|
841
|
+
en: "The zip/exe is a launcher. The first run downloads Node and dsh."
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
zh: "回形针依赖还在 GitHub。装 suite 要能访问 GitHub;公司机禁外网会失败。",
|
|
845
|
+
en: "The paperclip plugin still comes from GitHub. Installing suite needs GitHub access."
|
|
846
|
+
}
|
|
847
|
+
]
|
|
848
|
+
}]
|
|
849
|
+
}
|
|
850
|
+
];
|
|
796
851
|
function latestSuiteChangelog() {
|
|
797
852
|
const latest = SUITE_CHANGELOG[0];
|
|
798
853
|
if (latest === void 0) throw new Error("SUITE_CHANGELOG is empty");
|
|
@@ -893,6 +948,128 @@ const EMPTY_SECRETS = {
|
|
|
893
948
|
tianyancha: "",
|
|
894
949
|
esphome: ""
|
|
895
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
|
+
}
|
|
896
1073
|
function Field({ label, children }) {
|
|
897
1074
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
898
1075
|
className: panel_module_default.field,
|
|
@@ -986,9 +1163,38 @@ function ConfigTab({ api }) {
|
|
|
986
1163
|
const [message, setMessage] = (0, react.useState)("");
|
|
987
1164
|
const [error, setError] = (0, react.useState)("");
|
|
988
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)([]);
|
|
989
1169
|
const load = async () => {
|
|
990
1170
|
try {
|
|
991
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
|
+
}
|
|
992
1198
|
} catch (err) {
|
|
993
1199
|
setError(errorMessage(err));
|
|
994
1200
|
} finally {
|
|
@@ -1075,6 +1281,14 @@ function ConfigTab({ api }) {
|
|
|
1075
1281
|
const saved = await api.saveConfig(payload);
|
|
1076
1282
|
setConfig(saved);
|
|
1077
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);
|
|
1078
1292
|
setMessage(t("config.saved"));
|
|
1079
1293
|
} catch (err) {
|
|
1080
1294
|
setError(errorMessage(err));
|
|
@@ -1188,6 +1402,12 @@ function ConfigTab({ api }) {
|
|
|
1188
1402
|
usernameLabel: t("config.nextcloud.username"),
|
|
1189
1403
|
secretLabel: t("config.nextcloud.appPassword")
|
|
1190
1404
|
}),
|
|
1405
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WecomSection, {
|
|
1406
|
+
rooms: wecomRooms,
|
|
1407
|
+
drafts: wecomDrafts,
|
|
1408
|
+
status: wecomStatus,
|
|
1409
|
+
onChange: setWecomDrafts
|
|
1410
|
+
}),
|
|
1191
1411
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(McpSection, {
|
|
1192
1412
|
title: t("config.ha"),
|
|
1193
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
|
|
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
|
|
7723
|
-
*
|
|
7724
|
-
*
|
|
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.
|
|
4
|
-
"version": "0.1.
|
|
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,8 +45,8 @@
|
|
|
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.
|
|
49
|
-
"@rezti/dsh-web-file-uploader": "0.2.
|
|
48
|
+
"@rezti/dsh-rez-wechat": "^0.1.15",
|
|
49
|
+
"@rezti/dsh-web-file-uploader": "0.2.3",
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
51
51
|
"zod": "^4.4.3"
|
|
52
52
|
},
|
package/src/changelog.ts
CHANGED
|
@@ -20,6 +20,36 @@ 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
|
+
},
|
|
38
|
+
{
|
|
39
|
+
version: '0.1.48',
|
|
40
|
+
date: '2026-08-19',
|
|
41
|
+
sections: [
|
|
42
|
+
{
|
|
43
|
+
title: { zh: '启动修复', en: 'Startup fix' },
|
|
44
|
+
items: [
|
|
45
|
+
{
|
|
46
|
+
zh: '0.1.47 网页报 Failed to load plugins:回形针客户端仍按旧名注册。已发 @rezti/dsh-web-file-uploader@0.2.3,模块 id 与包名一致。请装 0.1.48。',
|
|
47
|
+
en: '0.1.47 failed to load plugins because the paperclip client still registered under the old package name. @rezti/dsh-web-file-uploader@0.2.3 matches the scoped name. Install 0.1.48.',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
23
53
|
{
|
|
24
54
|
version: '0.1.47',
|
|
25
55
|
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)
|
package/src/client/locales.ts
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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;
|
package/src/esphome-lab.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ESPHome lab: official dsh-mcp-client → lab MCP
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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'
|
package/src/observe-mcp.ts
CHANGED
|
@@ -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
|
|
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 只是启动壳,第一次会从外网下 Node
|
|
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.
|
|
33
|
-
3.
|
|
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.
|
|
39
|
-
3.
|
|
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
|
-
|
|
|
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/
|
|
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
|
|