@rezti/dsh-rez-suite 0.1.0 → 0.1.2

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
@@ -7,3 +7,5 @@ dsh plugin --profile web add @rezti/dsh-rez-suite
7
7
  ```
8
8
 
9
9
  组成见仓库根 README。子包不要单独 `dsh plugin add`。
10
+
11
+ 个人微信(QClaw/ClawBot)在 Rez 面板「微信」页扫码,然后直接在微信里说话。
package/README.zh.md CHANGED
@@ -8,4 +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)在 Rez 面板「微信」页扫码,然后直接在微信里说话。
12
+
11
13
  子包不要再单独 `dsh plugin add`,否则会重复注册 MCP。
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
@@ -7555,7 +7557,8 @@ const COMPOSED_SERVERS = [
7555
7557
  const REZ_GUIDANCE = [
7556
7558
  "本机已安装 ReZ-TI 套件(一次安装):工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。",
7557
7559
  "工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。",
7558
- "密钥走官方 dsh-credentials / 环境变量:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。",
7560
+ "密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。",
7561
+ "个人微信(QClaw/ClawBot):打开 Rez 面板「微信」页扫码,然后直接在微信里说话,不必再跑命令、也不要调用微信 MCP 工具。",
7559
7562
  "意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。",
7560
7563
  "涉及资金、对外发送、设备控制、生产写入时先征得用户批准。"
7561
7564
  ].join("");
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
- "description": "ReZ-TI employee plugin for DeepSeek Harness: one install enables company MCP (Odoo / Nextcloud / WeCom / Home Assistant) and work identity.",
4
- "version": "0.1.0",
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.2",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -36,13 +36,13 @@
36
36
  "dependencies": {
37
37
  "@modelcontextprotocol/sdk": "^1.30.0",
38
38
  "zod": "^4.4.3",
39
- "@rezti/dsh-rez-intent": "0.1.0",
40
- "@rezti/dsh-rez-ha-bridge": "0.1.0",
41
39
  "@rezti/dsh-rez-nextcloud": "0.1.0",
42
40
  "@rezti/dsh-rez-odoo": "0.1.0",
43
- "@rezti/dsh-rez-sso": "0.1.0",
41
+ "@rezti/dsh-rez-ha-bridge": "0.1.0",
42
+ "@rezti/dsh-rez-intent": "0.1.2",
44
43
  "@rezti/dsh-rez-token-manager": "0.1.0",
45
- "@rezti/dsh-rez-wechat": "0.1.0"
44
+ "@rezti/dsh-rez-wechat": "0.1.2",
45
+ "@rezti/dsh-rez-sso": "0.1.2"
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
@@ -59,7 +59,8 @@ const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant'] as con
59
59
  export const REZ_GUIDANCE = [
60
60
  '本机已安装 ReZ-TI 套件(一次安装):工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。',
61
61
  '工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。',
62
- '密钥走官方 dsh-credentials / 环境变量:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。',
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('')
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