@rezti/dsh-rez-suite 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -8,4 +8,4 @@ dsh plugin --profile web add @rezti/dsh-rez-suite
8
8
 
9
9
  组成见仓库根 README。子包不要单独 `dsh plugin add`。
10
10
 
11
- 个人微信(QClaw/ClawBot)需本机扫码一次:`npx weixin-mcp login`。
11
+ 个人微信(QClaw/ClawBot)在 Rez 面板「微信」页扫码,然后直接在微信里说话。
package/README.zh.md CHANGED
@@ -8,6 +8,6 @@ dsh plugin --profile web add @rezti/dsh-rez-suite
8
8
 
9
9
  密钥用环境变量或 dsh-credentials:`REZ_ODOO_TOKEN`、`REZ_NEXTCLOUD_PASSWORD`、`REZ_WECHAT_WEBHOOK`、`REZ_HA_TOKEN`。
10
10
 
11
- 个人微信(QClaw/ClawBot)另外扫码一次:`npx weixin-mcp login`。工具名 `mcp__weixin__*`。
11
+ 个人微信(QClaw/ClawBot)在 Rez 面板「微信」页扫码,然后直接在微信里说话。
12
12
 
13
13
  子包不要再单独 `dsh plugin add`,否则会重复注册 MCP。
package/cordis.patch.yml CHANGED
@@ -35,6 +35,9 @@
35
35
  - --transport
36
36
  - stdio
37
37
  env:
38
+ # App-password Basic Auth. Without this, newer nextcloud-mcp-server
39
+ # auto-detects login_flow and rejects NEXTCLOUD_USERNAME.
40
+ MCP_DEPLOYMENT_MODE: single_user_basic
38
41
  NEXTCLOUD_HOST: https://cloud.rez-ti.com
39
42
  NEXTCLOUD_USERNAME: rez-ti-2
40
43
  NEXTCLOUD_PASSWORD: !!js process.env.REZ_NEXTCLOUD_PASSWORD ?? ''
@@ -53,18 +56,6 @@
53
56
  env:
54
57
  WECOM_WEBHOOK_URL: !!js process.env.REZ_WECHAT_WEBHOOK ?? ''
55
58
  failOnStartupError: false
56
- # Personal WeChat (QClaw / ClawBot): reuse weixin-mcp, do not write a WebSocket client.
57
- # Employee login once: npx weixin-mcp login (微信 → 发现 → 小程序 → ClawBot)
58
- - id: rez-mcp-weixin
59
- name: '@deepseek-ai/dsh-mcp-client'
60
- config:
61
- serverName: weixin
62
- transport: stdio
63
- command: npx
64
- args:
65
- - -y
66
- - weixin-mcp
67
- failOnStartupError: false
68
59
  - id: rez-ha-bridge
69
60
  name: '@rezti/dsh-rez-ha-bridge'
70
61
  - id: rez-mcp-homeassistant
package/lib/client.d.ts CHANGED
@@ -7256,6 +7256,7 @@ declare const zh: {
7256
7256
  readonly 'entry.tooltip': "Rez Suite 配置与审计面板";
7257
7257
  readonly 'panel.title': "Rez Suite";
7258
7258
  readonly 'tab.config': "配置";
7259
+ readonly 'tab.weixin': "微信";
7259
7260
  readonly 'tab.status': "状态";
7260
7261
  readonly 'tab.audit': "用量";
7261
7262
  readonly 'config.role': "角色";
@@ -7327,6 +7328,15 @@ declare const zh: {
7327
7328
  readonly 'common.loading': "加载中…";
7328
7329
  readonly 'common.error': "出错:{error}";
7329
7330
  readonly 'common.refresh': "刷新";
7331
+ readonly 'weixin.intro': "和 QClaw 一样:扫一次码,然后直接在微信里跟本机 dsh 说话。不用再跑命令,也不用记工具名。";
7332
+ readonly 'weixin.login': "扫码登录";
7333
+ readonly 'weixin.logout': "退出微信";
7334
+ readonly 'weixin.verify': "提交配对码";
7335
+ readonly 'weixin.verifyPlaceholder': "手机上显示的数字";
7336
+ readonly 'weixin.qrAlt': "微信登录二维码";
7337
+ readonly 'weixin.listening': "通道在听:微信发消息 → 本机 dsh → 回微信。";
7338
+ readonly 'weixin.loggedIn': "已登录,通道未在听。再点扫码登录即可拉起。";
7339
+ readonly 'weixin.loggedOut': "尚未登录。";
7330
7340
  };
7331
7341
  type RezKey = keyof typeof zh;
7332
7342
  //#endregion
@@ -7421,6 +7431,16 @@ interface RezStatusResponse {
7421
7431
  servers: RezServerStatus[];
7422
7432
  totalRegisteredTools: number;
7423
7433
  }
7434
+ /** QClaw-style WeChat channel status (scan QR, then talk in WeChat). */
7435
+ type RezWeixinPhase = 'idle' | 'login' | 'need_verify' | 'listening' | 'error';
7436
+ interface RezWeixinStatus {
7437
+ loggedIn: boolean;
7438
+ listening: boolean;
7439
+ phase: RezWeixinPhase;
7440
+ qrContent?: string;
7441
+ qrDataUrl?: string;
7442
+ hint: string;
7443
+ }
7424
7444
  /** One audit ledger row. */
7425
7445
  interface RezAuditRecord {
7426
7446
  id: number;
@@ -7459,6 +7479,10 @@ declare class RezApi {
7459
7479
  status(): Promise<RezStatusResponse>;
7460
7480
  audit(): Promise<RezAuditResponse>;
7461
7481
  resetAudit(): Promise<void>;
7482
+ weixinStatus(): Promise<RezWeixinStatus>;
7483
+ weixinLogin(): Promise<RezWeixinStatus>;
7484
+ weixinVerify(code: string): Promise<RezWeixinStatus>;
7485
+ weixinLogout(): Promise<RezWeixinStatus>;
7462
7486
  }
7463
7487
  //#endregion
7464
7488
  //#region src/client/panel/RezPanel.d.ts
package/lib/client.js CHANGED
@@ -14,7 +14,9 @@ const REZ_API = {
14
14
  test: "/api/dsh-rez-suite/test",
15
15
  status: "/api/dsh-rez-suite/status",
16
16
  audit: "/api/dsh-rez-suite/audit",
17
- auditReset: "/api/dsh-rez-suite/audit/reset"
17
+ auditReset: "/api/dsh-rez-suite/audit/reset",
18
+ weixin: "/api/dsh-rez-suite/weixin",
19
+ weixinVerify: "/api/dsh-rez-suite/weixin/verify"
18
20
  };
19
21
  //#endregion
20
22
  //#region src/client/api.ts
@@ -60,6 +62,22 @@ var RezApi = class {
60
62
  async resetAudit() {
61
63
  await readJson(await fetch(REZ_API.auditReset, { method: "POST" }));
62
64
  }
65
+ async weixinStatus() {
66
+ return readJson(await fetch(REZ_API.weixin));
67
+ }
68
+ async weixinLogin() {
69
+ return readJson(await fetch(REZ_API.weixin, { method: "POST" }));
70
+ }
71
+ async weixinVerify(code) {
72
+ return readJson(await fetch(REZ_API.weixinVerify, {
73
+ method: "POST",
74
+ headers: { "content-type": "application/json" },
75
+ body: JSON.stringify({ code })
76
+ }));
77
+ }
78
+ async weixinLogout() {
79
+ return readJson(await fetch(REZ_API.weixin, { method: "DELETE" }));
80
+ }
63
81
  };
64
82
  //#endregion
65
83
  //#region src/client/locales.ts
@@ -71,6 +89,7 @@ const zh = {
71
89
  "entry.tooltip": "Rez Suite 配置与审计面板",
72
90
  "panel.title": "Rez Suite",
73
91
  "tab.config": "配置",
92
+ "tab.weixin": "微信",
74
93
  "tab.status": "状态",
75
94
  "tab.audit": "用量",
76
95
  "config.role": "角色",
@@ -141,13 +160,23 @@ const zh = {
141
160
  "common.close": "关闭",
142
161
  "common.loading": "加载中…",
143
162
  "common.error": "出错:{error}",
144
- "common.refresh": "刷新"
163
+ "common.refresh": "刷新",
164
+ "weixin.intro": "和 QClaw 一样:扫一次码,然后直接在微信里跟本机 dsh 说话。不用再跑命令,也不用记工具名。",
165
+ "weixin.login": "扫码登录",
166
+ "weixin.logout": "退出微信",
167
+ "weixin.verify": "提交配对码",
168
+ "weixin.verifyPlaceholder": "手机上显示的数字",
169
+ "weixin.qrAlt": "微信登录二维码",
170
+ "weixin.listening": "通道在听:微信发消息 → 本机 dsh → 回微信。",
171
+ "weixin.loggedIn": "已登录,通道未在听。再点扫码登录即可拉起。",
172
+ "weixin.loggedOut": "尚未登录。"
145
173
  };
146
174
  const en = {
147
175
  "entry.label": "Rez",
148
176
  "entry.tooltip": "Rez Suite configuration and audit panel",
149
177
  "panel.title": "Rez Suite",
150
178
  "tab.config": "Config",
179
+ "tab.weixin": "WeChat",
151
180
  "tab.status": "Status",
152
181
  "tab.audit": "Usage",
153
182
  "config.role": "Role",
@@ -218,7 +247,16 @@ const en = {
218
247
  "common.close": "Close",
219
248
  "common.loading": "Loading…",
220
249
  "common.error": "Error: {error}",
221
- "common.refresh": "Refresh"
250
+ "common.refresh": "Refresh",
251
+ "weixin.intro": "Same as QClaw: scan once, then talk in WeChat. No extra CLI, no MCP tool names.",
252
+ "weixin.login": "Scan to log in",
253
+ "weixin.logout": "Log out",
254
+ "weixin.verify": "Submit pairing code",
255
+ "weixin.verifyPlaceholder": "Digits shown on the phone",
256
+ "weixin.qrAlt": "WeChat login QR",
257
+ "weixin.listening": "Channel is live: WeChat → this dsh → WeChat.",
258
+ "weixin.loggedIn": "Logged in, listener idle. Scan again to start it.",
259
+ "weixin.loggedOut": "Not logged in."
222
260
  };
223
261
  /** Minimal {name} interpolator shared by the panel helpers. */
224
262
  function t(dict, key, values) {
@@ -268,6 +306,7 @@ var panel_module_default = {
268
306
  "panelHeader": "WRGRRW_panelHeader",
269
307
  "panelTitle": "WRGRRW_panelTitle",
270
308
  "primaryButton": "WRGRRW_primaryButton",
309
+ "qr": "WRGRRW_qr",
271
310
  "section": "WRGRRW_section",
272
311
  "sectionTitle": "WRGRRW_sectionTitle",
273
312
  "select": "WRGRRW_select",
@@ -915,6 +954,142 @@ function StatusTab({ api }) {
915
954
  });
916
955
  }
917
956
  //#endregion
957
+ //#region src/client/panel/WeixinTab.tsx
958
+ /**
959
+ * WeChat / QClaw tab: scan QR once, then talk in WeChat. No CLI, no MCP tools.
960
+ */
961
+ function WeixinTab({ api }) {
962
+ const [status, setStatus] = (0, react.useState)(null);
963
+ const [error, setError] = (0, react.useState)("");
964
+ const [code, setCode] = (0, react.useState)("");
965
+ const [busy, setBusy] = (0, react.useState)(false);
966
+ const load = async () => {
967
+ try {
968
+ setStatus(await api.weixinStatus());
969
+ setError("");
970
+ } catch (err) {
971
+ setError(errorMessage(err));
972
+ }
973
+ };
974
+ (0, react.useEffect)(() => {
975
+ load();
976
+ const timer = setInterval(() => {
977
+ load();
978
+ }, 1500);
979
+ return () => {
980
+ clearInterval(timer);
981
+ };
982
+ }, []);
983
+ const login = async () => {
984
+ setBusy(true);
985
+ try {
986
+ setStatus(await api.weixinLogin());
987
+ setError("");
988
+ } catch (err) {
989
+ setError(errorMessage(err));
990
+ } finally {
991
+ setBusy(false);
992
+ }
993
+ };
994
+ const verify = async () => {
995
+ setBusy(true);
996
+ try {
997
+ setStatus(await api.weixinVerify(code));
998
+ setCode("");
999
+ setError("");
1000
+ } catch (err) {
1001
+ setError(errorMessage(err));
1002
+ } finally {
1003
+ setBusy(false);
1004
+ }
1005
+ };
1006
+ const logout = async () => {
1007
+ setBusy(true);
1008
+ try {
1009
+ setStatus(await api.weixinLogout());
1010
+ setError("");
1011
+ } catch (err) {
1012
+ setError(errorMessage(err));
1013
+ } finally {
1014
+ setBusy(false);
1015
+ }
1016
+ };
1017
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1018
+ className: panel_module_default.tabBody,
1019
+ children: [
1020
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1021
+ className: panel_module_default.message,
1022
+ children: tt("weixin.intro")
1023
+ }),
1024
+ error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1025
+ className: panel_module_default.error,
1026
+ children: error
1027
+ }),
1028
+ status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1029
+ className: panel_module_default.message,
1030
+ children: status.hint
1031
+ }),
1032
+ status?.qrDataUrl !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1033
+ className: panel_module_default.qr,
1034
+ src: status.qrDataUrl,
1035
+ alt: tt("weixin.qrAlt")
1036
+ }),
1037
+ status?.qrContent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1038
+ className: panel_module_default.message,
1039
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1040
+ href: status.qrContent,
1041
+ target: "_blank",
1042
+ rel: "noreferrer",
1043
+ children: status.qrContent
1044
+ })
1045
+ }),
1046
+ status?.phase === "need_verify" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1047
+ className: panel_module_default.toolbar,
1048
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1049
+ className: panel_module_default.input,
1050
+ value: code,
1051
+ onChange: (event) => {
1052
+ setCode(event.target.value);
1053
+ },
1054
+ placeholder: tt("weixin.verifyPlaceholder")
1055
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1056
+ type: "button",
1057
+ className: panel_module_default.button,
1058
+ disabled: busy,
1059
+ onClick: () => {
1060
+ verify();
1061
+ },
1062
+ children: tt("weixin.verify")
1063
+ })]
1064
+ }),
1065
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1066
+ className: panel_module_default.toolbar,
1067
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1068
+ type: "button",
1069
+ className: panel_module_default.button,
1070
+ disabled: busy,
1071
+ onClick: () => {
1072
+ login();
1073
+ },
1074
+ children: tt("weixin.login")
1075
+ }), status?.loggedIn === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1076
+ type: "button",
1077
+ className: panel_module_default.button,
1078
+ disabled: busy,
1079
+ onClick: () => {
1080
+ logout();
1081
+ },
1082
+ children: tt("weixin.logout")
1083
+ })]
1084
+ }),
1085
+ status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1086
+ className: panel_module_default.message,
1087
+ children: status.listening ? tt("weixin.listening") : status.loggedIn ? tt("weixin.loggedIn") : tt("weixin.loggedOut")
1088
+ })
1089
+ ]
1090
+ });
1091
+ }
1092
+ //#endregion
918
1093
  //#region src/client/panel/RezPanel.tsx
919
1094
  /**
920
1095
  * Rez Suite panel shell: header + three tabs (config/status/audit).
@@ -924,6 +1099,10 @@ const TABS = [
924
1099
  id: "config",
925
1100
  label: () => tt("tab.config")
926
1101
  },
1102
+ {
1103
+ id: "weixin",
1104
+ label: () => tt("tab.weixin")
1105
+ },
927
1106
  {
928
1107
  id: "status",
929
1108
  label: () => tt("tab.status")
@@ -973,6 +1152,7 @@ function RezPanel({ controller, api }) {
973
1152
  className: panel_module_default.panelContent,
974
1153
  children: [
975
1154
  activeTab === "config" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigTab, { api }),
1155
+ activeTab === "weixin" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WeixinTab, { api }),
976
1156
  activeTab === "status" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusTab, { api }),
977
1157
  activeTab === "audit" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuditTab, { api })
978
1158
  ]
package/lib/index.js CHANGED
@@ -2847,7 +2847,9 @@ const REZ_API = {
2847
2847
  test: "/api/dsh-rez-suite/test",
2848
2848
  status: "/api/dsh-rez-suite/status",
2849
2849
  audit: "/api/dsh-rez-suite/audit",
2850
- auditReset: "/api/dsh-rez-suite/audit/reset"
2850
+ auditReset: "/api/dsh-rez-suite/audit/reset",
2851
+ weixin: "/api/dsh-rez-suite/weixin",
2852
+ weixinVerify: "/api/dsh-rez-suite/weixin/verify"
2851
2853
  };
2852
2854
  //#endregion
2853
2855
  //#region src/store.ts
@@ -7550,13 +7552,13 @@ const COMPOSED_SERVERS = [
7550
7552
  "odoo",
7551
7553
  "nextcloud",
7552
7554
  "wechat",
7553
- "weixin",
7554
7555
  "homeassistant"
7555
7556
  ];
7556
7557
  const REZ_GUIDANCE = [
7557
- "本机已安装 ReZ-TI 套件(一次安装):工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / 个人微信(QClaw/ClawBot) / Home Assistant。",
7558
- "工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__weixin__*、mcp__homeassistant__*。",
7559
- "密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。个人微信先 npx weixin-mcp login(ClawBot 扫码),不要自写 WebSocket。",
7558
+ "本机已安装 ReZ-TI 套件(一次安装):工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。",
7559
+ "工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。",
7560
+ "密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。",
7561
+ "个人微信(QClaw/ClawBot):打开 Rez 面板「微信」页扫码,然后直接在微信里说话,不必再跑命令、也不要调用微信 MCP 工具。",
7560
7562
  "意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。",
7561
7563
  "涉及资金、对外发送、设备控制、生产写入时先征得用户批准。"
7562
7564
  ].join("");
@@ -7599,7 +7601,8 @@ function apply(ctx, config) {
7599
7601
  saveConfig(value);
7600
7602
  current = () => value;
7601
7603
  try {
7602
- await ctx.settings?.update(REZ_SETTINGS_NAMESPACE, value);
7604
+ const settings = ctx.get("settings");
7605
+ if (settings?.update !== void 0) await settings.update(REZ_SETTINGS_NAMESPACE, value);
7603
7606
  } catch (error) {
7604
7607
  console.warn("[dsh-rez-suite] settings update failed, using local store:", error);
7605
7608
  }
package/lib/style.css CHANGED
@@ -219,6 +219,15 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
219
219
  background: var(--dsw-alias-interactive-bg-hover);
220
220
  }
221
221
 
222
+ .WRGRRW_qr {
223
+ background: #fff;
224
+ border-radius: 8px;
225
+ width: 240px;
226
+ height: 240px;
227
+ margin: 8px 0;
228
+ display: block;
229
+ }
230
+
222
231
  .WRGRRW_primaryButton {
223
232
  background: var(--dsw-alias-state-business-primary);
224
233
  border-color: var(--dsw-alias-state-business-primary);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rezti/dsh-rez-suite",
3
3
  "description": "ReZ-TI employee plugin for DeepSeek Harness: one install enables company MCP (Odoo / Nextcloud / WeCom / Weixin-ClawBot / Home Assistant) and work identity.",
4
- "version": "0.1.1",
4
+ "version": "0.1.3",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -37,12 +37,12 @@
37
37
  "@modelcontextprotocol/sdk": "^1.30.0",
38
38
  "zod": "^4.4.3",
39
39
  "@rezti/dsh-rez-ha-bridge": "0.1.0",
40
- "@rezti/dsh-rez-sso": "0.1.1",
41
- "@rezti/dsh-rez-token-manager": "0.1.0",
42
- "@rezti/dsh-rez-wechat": "0.1.1",
43
- "@rezti/dsh-rez-intent": "0.1.1",
40
+ "@rezti/dsh-rez-intent": "0.1.3",
44
41
  "@rezti/dsh-rez-nextcloud": "0.1.0",
45
- "@rezti/dsh-rez-odoo": "0.1.0"
42
+ "@rezti/dsh-rez-sso": "0.1.3",
43
+ "@rezti/dsh-rez-odoo": "0.1.0",
44
+ "@rezti/dsh-rez-wechat": "0.1.3",
45
+ "@rezti/dsh-rez-token-manager": "0.1.3"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "react": "^18.2.0",
package/src/client/api.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  type RezServerStatus,
10
10
  type RezStatusResponse,
11
11
  type RezTestResult,
12
+ type RezWeixinStatus,
12
13
  } from '../protocol.ts'
13
14
 
14
15
  export class RezApiError extends Error {
@@ -72,4 +73,28 @@ export class RezApi {
72
73
  const response = await fetch(REZ_API.auditReset, { method: 'POST' })
73
74
  await readJson<{ ok: boolean }>(response)
74
75
  }
76
+
77
+ async weixinStatus(): Promise<RezWeixinStatus> {
78
+ const response = await fetch(REZ_API.weixin)
79
+ return readJson<RezWeixinStatus>(response)
80
+ }
81
+
82
+ async weixinLogin(): Promise<RezWeixinStatus> {
83
+ const response = await fetch(REZ_API.weixin, { method: 'POST' })
84
+ return readJson<RezWeixinStatus>(response)
85
+ }
86
+
87
+ async weixinVerify(code: string): Promise<RezWeixinStatus> {
88
+ const response = await fetch(REZ_API.weixinVerify, {
89
+ method: 'POST',
90
+ headers: { 'content-type': 'application/json' },
91
+ body: JSON.stringify({ code }),
92
+ })
93
+ return readJson<RezWeixinStatus>(response)
94
+ }
95
+
96
+ async weixinLogout(): Promise<RezWeixinStatus> {
97
+ const response = await fetch(REZ_API.weixin, { method: 'DELETE' })
98
+ return readJson<RezWeixinStatus>(response)
99
+ }
75
100
  }
@@ -7,6 +7,7 @@ export const zh = {
7
7
  'entry.tooltip': 'Rez Suite 配置与审计面板',
8
8
  'panel.title': 'Rez Suite',
9
9
  'tab.config': '配置',
10
+ 'tab.weixin': '微信',
10
11
  'tab.status': '状态',
11
12
  'tab.audit': '用量',
12
13
  // config
@@ -82,6 +83,15 @@ export const zh = {
82
83
  'common.loading': '加载中…',
83
84
  'common.error': '出错:{error}',
84
85
  'common.refresh': '刷新',
86
+ 'weixin.intro': '和 QClaw 一样:扫一次码,然后直接在微信里跟本机 dsh 说话。不用再跑命令,也不用记工具名。',
87
+ 'weixin.login': '扫码登录',
88
+ 'weixin.logout': '退出微信',
89
+ 'weixin.verify': '提交配对码',
90
+ 'weixin.verifyPlaceholder': '手机上显示的数字',
91
+ 'weixin.qrAlt': '微信登录二维码',
92
+ 'weixin.listening': '通道在听:微信发消息 → 本机 dsh → 回微信。',
93
+ 'weixin.loggedIn': '已登录,通道未在听。再点扫码登录即可拉起。',
94
+ 'weixin.loggedOut': '尚未登录。',
85
95
  } as const
86
96
 
87
97
  export type RezKey = keyof typeof zh
@@ -91,6 +101,7 @@ export const en: Record<RezKey, string> = {
91
101
  'entry.tooltip': 'Rez Suite configuration and audit panel',
92
102
  'panel.title': 'Rez Suite',
93
103
  'tab.config': 'Config',
104
+ 'tab.weixin': 'WeChat',
94
105
  'tab.status': 'Status',
95
106
  'tab.audit': 'Usage',
96
107
  'config.role': 'Role',
@@ -162,6 +173,15 @@ export const en: Record<RezKey, string> = {
162
173
  'common.loading': 'Loading…',
163
174
  'common.error': 'Error: {error}',
164
175
  'common.refresh': 'Refresh',
176
+ 'weixin.intro': 'Same as QClaw: scan once, then talk in WeChat. No extra CLI, no MCP tool names.',
177
+ 'weixin.login': 'Scan to log in',
178
+ 'weixin.logout': 'Log out',
179
+ 'weixin.verify': 'Submit pairing code',
180
+ 'weixin.verifyPlaceholder': 'Digits shown on the phone',
181
+ 'weixin.qrAlt': 'WeChat login QR',
182
+ 'weixin.listening': 'Channel is live: WeChat → this dsh → WeChat.',
183
+ 'weixin.loggedIn': 'Logged in, listener idle. Scan again to start it.',
184
+ 'weixin.loggedOut': 'Not logged in.',
165
185
  }
166
186
 
167
187
  /** Minimal {name} interpolator shared by the panel helpers. */
@@ -9,9 +9,10 @@ import { tt } from './helpers.ts'
9
9
  import { AuditTab } from './AuditTab.tsx'
10
10
  import { ConfigTab } from './ConfigTab.tsx'
11
11
  import { StatusTab } from './StatusTab.tsx'
12
+ import { WeixinTab } from './WeixinTab.tsx'
12
13
  import css from './panel.module.css'
13
14
 
14
- export type RezTab = 'config' | 'status' | 'audit'
15
+ export type RezTab = 'config' | 'weixin' | 'status' | 'audit'
15
16
 
16
17
  export interface RezPanelProps {
17
18
  controller: PanelController
@@ -20,6 +21,7 @@ export interface RezPanelProps {
20
21
 
21
22
  const TABS: ReadonlyArray<{ id: RezTab; label: () => string }> = [
22
23
  { id: 'config', label: () => tt('tab.config') },
24
+ { id: 'weixin', label: () => tt('tab.weixin') },
23
25
  { id: 'status', label: () => tt('tab.status') },
24
26
  { id: 'audit', label: () => tt('tab.audit') },
25
27
  ]
@@ -42,6 +44,7 @@ export function RezPanel({ controller, api }: RezPanelProps) {
42
44
  </div>
43
45
  <div className={css.panelContent}>
44
46
  {activeTab === 'config' && <ConfigTab api={api} />}
47
+ {activeTab === 'weixin' && <WeixinTab api={api} />}
45
48
  {activeTab === 'status' && <StatusTab api={api} />}
46
49
  {activeTab === 'audit' && <AuditTab api={api} />}
47
50
  </div>
@@ -0,0 +1,99 @@
1
+ /**
2
+ * WeChat / QClaw tab: scan QR once, then talk in WeChat. No CLI, no MCP tools.
3
+ */
4
+
5
+ import { useEffect, useState } from 'react'
6
+ import type { RezApi } from '../api.ts'
7
+ import type { RezWeixinStatus } from '../../protocol.ts'
8
+ import { errorMessage, tt } from './helpers.ts'
9
+ import css from './panel.module.css'
10
+
11
+ export function WeixinTab({ api }: { api: RezApi }) {
12
+ const [status, setStatus] = useState<RezWeixinStatus | null>(null)
13
+ const [error, setError] = useState('')
14
+ const [code, setCode] = useState('')
15
+ const [busy, setBusy] = useState(false)
16
+
17
+ const load = async (): Promise<void> => {
18
+ try {
19
+ setStatus(await api.weixinStatus())
20
+ setError('')
21
+ } catch (err) {
22
+ setError(errorMessage(err))
23
+ }
24
+ }
25
+
26
+ useEffect(() => {
27
+ void load()
28
+ const timer = setInterval(() => { void load() }, 1500)
29
+ return () => { clearInterval(timer) }
30
+ }, [])
31
+
32
+ const login = async (): Promise<void> => {
33
+ setBusy(true)
34
+ try {
35
+ setStatus(await api.weixinLogin())
36
+ setError('')
37
+ } catch (err) {
38
+ setError(errorMessage(err))
39
+ } finally {
40
+ setBusy(false)
41
+ }
42
+ }
43
+
44
+ const verify = async (): Promise<void> => {
45
+ setBusy(true)
46
+ try {
47
+ setStatus(await api.weixinVerify(code))
48
+ setCode('')
49
+ setError('')
50
+ } catch (err) {
51
+ setError(errorMessage(err))
52
+ } finally {
53
+ setBusy(false)
54
+ }
55
+ }
56
+
57
+ const logout = async (): Promise<void> => {
58
+ setBusy(true)
59
+ try {
60
+ setStatus(await api.weixinLogout())
61
+ setError('')
62
+ } catch (err) {
63
+ setError(errorMessage(err))
64
+ } finally {
65
+ setBusy(false)
66
+ }
67
+ }
68
+
69
+ return (
70
+ <div className={css.tabBody}>
71
+ <p className={css.message}>{tt('weixin.intro')}</p>
72
+ {error !== '' && <div className={css.error}>{error}</div>}
73
+ {status !== null && <p className={css.message}>{status.hint}</p>}
74
+ {status?.qrDataUrl !== undefined && (
75
+ <img className={css.qr} src={status.qrDataUrl} alt={tt('weixin.qrAlt')} />
76
+ )}
77
+ {status?.qrContent !== undefined && (
78
+ <p className={css.message}><a href={status.qrContent} target="_blank" rel="noreferrer">{status.qrContent}</a></p>
79
+ )}
80
+ {status?.phase === 'need_verify' && (
81
+ <div className={css.toolbar}>
82
+ <input className={css.input} value={code} onChange={event => { setCode(event.target.value) }} placeholder={tt('weixin.verifyPlaceholder')} />
83
+ <button type="button" className={css.button} disabled={busy} onClick={() => { void verify() }}>{tt('weixin.verify')}</button>
84
+ </div>
85
+ )}
86
+ <div className={css.toolbar}>
87
+ <button type="button" className={css.button} disabled={busy} onClick={() => { void login() }}>{tt('weixin.login')}</button>
88
+ {status?.loggedIn === true && (
89
+ <button type="button" className={css.button} disabled={busy} onClick={() => { void logout() }}>{tt('weixin.logout')}</button>
90
+ )}
91
+ </div>
92
+ {status !== null && (
93
+ <p className={css.message}>
94
+ {status.listening ? tt('weixin.listening') : status.loggedIn ? tt('weixin.loggedIn') : tt('weixin.loggedOut')}
95
+ </p>
96
+ )}
97
+ </div>
98
+ )
99
+ }
@@ -226,6 +226,15 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
226
226
  background: var(--dsw-alias-interactive-bg-hover);
227
227
  }
228
228
 
229
+ .qr {
230
+ display: block;
231
+ width: 240px;
232
+ height: 240px;
233
+ margin: 8px 0;
234
+ background: #fff;
235
+ border-radius: 8px;
236
+ }
237
+
229
238
  .primaryButton {
230
239
  background: var(--dsw-alias-state-business-primary);
231
240
  border-color: var(--dsw-alias-state-business-primary);
package/src/index.ts CHANGED
@@ -54,12 +54,13 @@ export const Config: z<RezConfig> = z.object({
54
54
 
55
55
  const SECTION_ORDER = 150
56
56
 
57
- const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'weixin', 'homeassistant'] as const
57
+ const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant'] as const
58
58
 
59
59
  export const REZ_GUIDANCE = [
60
- '本机已安装 ReZ-TI 套件(一次安装):工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / 个人微信(QClaw/ClawBot) / Home Assistant。',
61
- '工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__weixin__*、mcp__homeassistant__*。',
62
- '密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。个人微信先 npx weixin-mcp login(ClawBot 扫码),不要自写 WebSocket。',
60
+ '本机已安装 ReZ-TI 套件(一次安装):工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。',
61
+ '工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。',
62
+ '密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。',
63
+ '个人微信(QClaw/ClawBot):打开 Rez 面板「微信」页扫码,然后直接在微信里说话,不必再跑命令、也不要调用微信 MCP 工具。',
63
64
  '意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
64
65
  '涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
65
66
  ].join('')
@@ -109,7 +110,8 @@ export function apply(ctx: Context, config?: RezConfig): void {
109
110
  saveConfig(value)
110
111
  current = () => value
111
112
  try {
112
- await ctx.settings?.update(REZ_SETTINGS_NAMESPACE, value)
113
+ const settings = ctx.get('settings') as { update?: (namespace: unknown, value: unknown) => Promise<void> } | undefined
114
+ if (settings?.update !== undefined) await settings.update(REZ_SETTINGS_NAMESPACE, value)
113
115
  } catch (error) {
114
116
  console.warn('[dsh-rez-suite] settings update failed, using local store:', error)
115
117
  }
package/src/presets.ts CHANGED
@@ -60,9 +60,6 @@ const RULES: Record<Exclude<RezRoleId, 'all'>, RoleRule[]> = {
60
60
  // WeCom one-to-one / group sends.
61
61
  { server: 'wecom', pattern: '*send*' },
62
62
  { server: 'wecom', pattern: '*message*' },
63
- { server: 'wechat', pattern: '*send*' },
64
- { server: 'weixin', pattern: '*send*' },
65
- { server: 'weixin', pattern: '*poll*' },
66
63
  ],
67
64
  operations: [
68
65
  // Odoo reporting and fulfilment.
@@ -92,8 +89,6 @@ const RULES: Record<Exclude<RezRoleId, 'all'>, RoleRule[]> = {
92
89
  // WeCom broadcasts.
93
90
  { server: 'wecom', pattern: '*broadcast*' },
94
91
  { server: 'wecom', pattern: '*send*' },
95
- { server: 'wechat', pattern: '*send*' },
96
- { server: 'weixin', pattern: '*send*' },
97
92
  // Local audit cache.
98
93
  { server: 'sqlite', pattern: '*query*' },
99
94
  { server: 'sqlite', pattern: '*maintenance*' },
package/src/protocol.ts CHANGED
@@ -89,6 +89,18 @@ export interface RezStatusResponse {
89
89
  totalRegisteredTools: number
90
90
  }
91
91
 
92
+ /** QClaw-style WeChat channel status (scan QR, then talk in WeChat). */
93
+ export type RezWeixinPhase = 'idle' | 'login' | 'need_verify' | 'listening' | 'error'
94
+
95
+ export interface RezWeixinStatus {
96
+ loggedIn: boolean
97
+ listening: boolean
98
+ phase: RezWeixinPhase
99
+ qrContent?: string
100
+ qrDataUrl?: string
101
+ hint: string
102
+ }
103
+
92
104
  /** One audit ledger row. */
93
105
  export interface RezAuditRecord {
94
106
  id: number
@@ -130,4 +142,6 @@ export const REZ_API = {
130
142
  status: REZ_API_BASE + '/status',
131
143
  audit: REZ_API_BASE + '/audit',
132
144
  auditReset: REZ_API_BASE + '/audit/reset',
145
+ weixin: REZ_API_BASE + '/weixin',
146
+ weixinVerify: REZ_API_BASE + '/weixin/verify',
133
147
  } as const