@rezti/dsh-rez-wechat 0.1.1 → 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
@@ -2,24 +2,11 @@
2
2
 
3
3
  由 `@rezti/dsh-rez-suite` 一次装入。不要单独 `dsh plugin add`。
4
4
 
5
- 通道接入复用现成 MCP,不自写微信长连接。
6
-
7
5
  | 能力 | 做法 |
8
6
  |---|---|
7
+ | 个人微信(QClaw / ClawBot) | 打开 dsh Web → Rez 面板 → **微信** → 扫码。之后直接在微信里说话。 |
9
8
  | 企业微信发到群 | `wecom-mcp`(`mcp__wechat__*`)+ `REZ_WECHAT_WEBHOOK` |
10
- | 个人微信登录 / 收发 | `weixin-mcp`(`mcp__weixin__*`),QClaw / ClawBot 官方扫码 |
11
-
12
- ## 个人微信(QClaw 那套)
13
-
14
- 1. 微信 → 发现 → 小程序 → 搜索 **ClawBot**,创建一个机器人
15
- 2. 本机扫码一次:
16
-
17
- ```bash
18
- npx weixin-mcp login
19
- ```
20
-
21
- 3. 装过 suite 后,dsh 会自动起 `npx -y weixin-mcp`。缺登录时该 MCP 空转,不把 dsh 打挂。
22
9
 
23
- 凭证目录:`$WEIXIN_MCP_DIR` `~/.openclaw/openclaw-weixin/` `~/.weixin-mcp/`。
10
+ 个人微信复用现成 `dsh-wechat-bridge`(腾讯官方 iLink),不自写 WebSocket,也不用员工再跑 `npx weixin-mcp login`。
24
11
 
25
- 这是 ClawBot 客服会话,不是个人微信号管好友/群。
12
+ 这是 ClawBot 客服会话,不是用个人微信号管好友/群。
@@ -0,0 +1,32 @@
1
+ /**
2
+ * QClaw-style WeChat channel: reuse dsh-wechat-bridge (official iLink / ClawBot).
3
+ * Scan QR in the Rez panel, then talk in WeChat. Do not expose MCP tools.
4
+ */
5
+ export type WeixinPhase = 'idle' | 'login' | 'need_verify' | 'listening' | 'error';
6
+ export interface WeixinStatus {
7
+ loggedIn: boolean;
8
+ listening: boolean;
9
+ phase: WeixinPhase;
10
+ qrContent?: string;
11
+ qrDataUrl?: string;
12
+ hint: string;
13
+ }
14
+ export declare class WeixinChannel {
15
+ private loginChild;
16
+ private runChild;
17
+ private phase;
18
+ private qrContent;
19
+ private qrDataUrl;
20
+ private hint;
21
+ private logBuf;
22
+ status(): WeixinStatus;
23
+ startIfLoggedIn(): void;
24
+ startLogin(): Promise<WeixinStatus>;
25
+ sendVerify(code: string): WeixinStatus;
26
+ logout(): Promise<WeixinStatus>;
27
+ dispose(): void;
28
+ private startRun;
29
+ private stopLogin;
30
+ private stopRun;
31
+ }
32
+ export declare const weixinDataDir: string;
package/lib/channel.js ADDED
@@ -0,0 +1,170 @@
1
+ /**
2
+ * QClaw-style WeChat channel: reuse dsh-wechat-bridge (official iLink / ClawBot).
3
+ * Scan QR in the Rez panel, then talk in WeChat. Do not expose MCP tools.
4
+ */
5
+ import { spawn } from 'node:child_process';
6
+ import { existsSync } from 'node:fs';
7
+ import { homedir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ const DATA_DIR = join(homedir(), '.dsh', 'dsh-rez-weixin');
10
+ const AUTH_FILE = join(DATA_DIR, 'weixin-auth.json');
11
+ function extractQrContent(chunk) {
12
+ const labeled = chunk.match(/二维码链接[^\n]*\n\s*(\S+)/);
13
+ if (labeled?.[1] !== undefined && labeled[1].length > 8)
14
+ return labeled[1];
15
+ const probe = chunk.match(/📱 链接:\s*(\S+)/);
16
+ if (probe?.[1] !== undefined)
17
+ return probe[1];
18
+ const https = chunk.match(/https?:\/\/[^\s]+/);
19
+ return https?.[0];
20
+ }
21
+ async function toDataUrl(content) {
22
+ try {
23
+ const qrcode = await import('qrcode');
24
+ const fn = qrcode.toDataURL ?? qrcode.default?.toDataURL;
25
+ if (typeof fn !== 'function')
26
+ return undefined;
27
+ return await fn(content, { margin: 1, width: 240 });
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ export class WeixinChannel {
34
+ loginChild;
35
+ runChild;
36
+ phase = 'idle';
37
+ qrContent;
38
+ qrDataUrl;
39
+ hint = '打开 Rez 面板的「微信」页扫码,然后直接在微信里说话。';
40
+ logBuf = '';
41
+ status() {
42
+ const loggedIn = existsSync(AUTH_FILE);
43
+ const status = {
44
+ loggedIn,
45
+ listening: this.runChild !== undefined && this.runChild.exitCode === null,
46
+ phase: this.phase,
47
+ hint: this.hint,
48
+ };
49
+ if (this.qrContent !== undefined)
50
+ status.qrContent = this.qrContent;
51
+ if (this.qrDataUrl !== undefined)
52
+ status.qrDataUrl = this.qrDataUrl;
53
+ return status;
54
+ }
55
+ startIfLoggedIn() {
56
+ if (existsSync(AUTH_FILE))
57
+ this.startRun();
58
+ }
59
+ async startLogin() {
60
+ this.stopLogin();
61
+ this.phase = 'login';
62
+ this.qrContent = undefined;
63
+ this.qrDataUrl = undefined;
64
+ this.logBuf = '';
65
+ this.hint = '正在向微信申请二维码…';
66
+ const child = spawn('npx', ['-y', 'dsh-wechat-bridge', 'login', '--data-dir', DATA_DIR], {
67
+ env: process.env,
68
+ stdio: ['pipe', 'pipe', 'pipe'],
69
+ });
70
+ this.loginChild = child;
71
+ const onChunk = (buf) => {
72
+ const text = buf.toString('utf8');
73
+ this.logBuf += text;
74
+ if (this.logBuf.includes('输入手机微信显示的数字') || this.logBuf.includes('请重新输入手机上显示的数字')) {
75
+ this.phase = 'need_verify';
76
+ this.hint = '手机上出现了配对数字,填到下面确认。';
77
+ }
78
+ if (this.logBuf.includes('已扫码'))
79
+ this.hint = '已扫码,请在手机上确认。';
80
+ const qr = extractQrContent(text) ?? extractQrContent(this.logBuf);
81
+ if (qr !== undefined && qr !== this.qrContent) {
82
+ this.qrContent = qr;
83
+ this.hint = '用微信扫一扫这个二维码(或在手机微信打开链接)。';
84
+ void toDataUrl(qr).then(url => {
85
+ if (url !== undefined && this.qrContent === qr)
86
+ this.qrDataUrl = url;
87
+ });
88
+ }
89
+ };
90
+ child.stdout?.on('data', onChunk);
91
+ child.stderr?.on('data', onChunk);
92
+ child.on('exit', code => {
93
+ if (this.loginChild === child)
94
+ this.loginChild = undefined;
95
+ if (code === 0 && existsSync(AUTH_FILE)) {
96
+ this.phase = 'listening';
97
+ this.hint = '已登录。直接在微信里给 ClawBot 发消息即可,不必再跑命令。';
98
+ this.startRun();
99
+ return;
100
+ }
101
+ if (this.phase === 'login' || this.phase === 'need_verify') {
102
+ this.phase = existsSync(AUTH_FILE) ? 'idle' : 'error';
103
+ this.hint = code === 0 ? '登录结束。' : '登录未完成,请再扫一次。';
104
+ }
105
+ });
106
+ return this.status();
107
+ }
108
+ sendVerify(code) {
109
+ const trimmed = code.trim();
110
+ if (trimmed.length === 0)
111
+ return this.status();
112
+ this.loginChild?.stdin?.write(trimmed + '\n');
113
+ this.phase = 'login';
114
+ this.hint = '已提交配对码,等待手机确认…';
115
+ return this.status();
116
+ }
117
+ async logout() {
118
+ this.stopRun();
119
+ this.stopLogin();
120
+ await new Promise(resolve => {
121
+ const child = spawn('npx', ['-y', 'dsh-wechat-bridge', 'logout', '--data-dir', DATA_DIR], {
122
+ env: process.env,
123
+ stdio: 'ignore',
124
+ });
125
+ child.on('exit', () => resolve());
126
+ child.on('error', () => resolve());
127
+ });
128
+ this.phase = 'idle';
129
+ this.qrContent = undefined;
130
+ this.qrDataUrl = undefined;
131
+ this.hint = '已退出。再点「扫码登录」即可。';
132
+ return this.status();
133
+ }
134
+ dispose() {
135
+ this.stopLogin();
136
+ this.stopRun();
137
+ }
138
+ startRun() {
139
+ if (this.runChild !== undefined && this.runChild.exitCode === null)
140
+ return;
141
+ this.phase = 'listening';
142
+ this.hint = '微信通道已在后台听着。在微信里发消息就会进本机 dsh。';
143
+ const child = spawn('npx', ['-y', 'dsh-wechat-bridge', 'run', '--data-dir', DATA_DIR], {
144
+ env: process.env,
145
+ stdio: ['ignore', 'pipe', 'pipe'],
146
+ });
147
+ this.runChild = child;
148
+ child.on('exit', code => {
149
+ if (this.runChild === child)
150
+ this.runChild = undefined;
151
+ if (this.phase === 'listening') {
152
+ this.phase = existsSync(AUTH_FILE) ? 'idle' : 'error';
153
+ this.hint = code === 0 ? '微信监听已停。' : '微信监听退出,可再点扫码或等自动重连。';
154
+ }
155
+ });
156
+ }
157
+ stopLogin() {
158
+ if (this.loginChild === undefined)
159
+ return;
160
+ this.loginChild.kill('SIGTERM');
161
+ this.loginChild = undefined;
162
+ }
163
+ stopRun() {
164
+ if (this.runChild === undefined)
165
+ return;
166
+ this.runChild.kill('SIGTERM');
167
+ this.runChild = undefined;
168
+ }
169
+ }
170
+ export const weixinDataDir = DATA_DIR;
package/lib/index.js CHANGED
@@ -1,15 +1,115 @@
1
+ import { WeixinChannel } from './channel.js';
1
2
  export const name = 'rez-wechat';
2
3
  export const inject = ['systemPrompt'];
3
4
  const GUIDANCE = [
4
5
  '企业微信群发送走官方 dsh-mcp-client + wecom-mcp(mcp__wechat__*),密钥 REZ_WECHAT_WEBHOOK。',
5
- '个人微信登录/收发走官方 ClawBot API,复用现成 weixin-mcp(mcp__weixin__*:weixin_send / weixin_poll / weixin_contacts)。这是 QClaw 同款扫码登录,不要自写 WebSocket。',
6
- '员工先在微信小程序创建 ClawBot,本机执行一次:npx weixin-mcp login。凭证默认在 ~/.weixin-mcp/,可用 WEIXIN_MCP_DIR 改目录。',
6
+ '个人微信是 QClaw 同款 ClawBot 通道:在 Rez 面板「微信」页扫码一次,然后直接在微信里说话,dsh 会自动回。不要再跑 npx weixin-mcp login,也不要调用 mcp__weixin__*。',
7
7
  '这是 ClawBot 客服会话,不是用个人微信号管好友/群。对外发消息必须先征得用户批准。',
8
8
  ].join('');
9
+ function isLoopbackRequest(request) {
10
+ const address = request.socket.remoteAddress;
11
+ if (address !== '127.0.0.1' && address !== '::1' && address !== '::ffff:127.0.0.1')
12
+ return false;
13
+ const host = request.headers.host;
14
+ if (typeof host !== 'string')
15
+ return false;
16
+ try {
17
+ const hostUrl = new URL('http://' + host);
18
+ return hostUrl.hostname === '127.0.0.1' || hostUrl.hostname === 'localhost' || hostUrl.hostname === '[::1]';
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function writeJson(res, status, body) {
25
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'referrer-policy': 'no-referrer' });
26
+ res.end(JSON.stringify(body));
27
+ }
28
+ async function readJsonBody(req) {
29
+ const chunks = [];
30
+ let size = 0;
31
+ for await (const chunk of req) {
32
+ size += chunk.length;
33
+ if (size > 16 * 1024)
34
+ return undefined;
35
+ chunks.push(chunk);
36
+ }
37
+ try {
38
+ const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8'));
39
+ return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ function weixinRoutes(channel) {
46
+ const path = '/api/dsh-rez-suite/weixin';
47
+ const guard = (req, res, method) => {
48
+ if (!isLoopbackRequest(req)) {
49
+ writeJson(res, 403, { error: 'forbidden: loopback-only' });
50
+ return false;
51
+ }
52
+ if (req.method !== method) {
53
+ writeJson(res, 405, { error: 'method not allowed' });
54
+ return false;
55
+ }
56
+ return true;
57
+ };
58
+ return [
59
+ {
60
+ kind: 'exact',
61
+ path,
62
+ handler: async (req, res) => {
63
+ if (req.method === 'GET') {
64
+ if (!guard(req, res, 'GET'))
65
+ return;
66
+ writeJson(res, 200, channel.status());
67
+ return;
68
+ }
69
+ if (req.method === 'POST') {
70
+ if (!guard(req, res, 'POST'))
71
+ return;
72
+ writeJson(res, 200, await channel.startLogin());
73
+ return;
74
+ }
75
+ if (req.method === 'DELETE') {
76
+ if (!guard(req, res, 'DELETE'))
77
+ return;
78
+ writeJson(res, 200, await channel.logout());
79
+ return;
80
+ }
81
+ writeJson(res, 405, { error: 'method not allowed' });
82
+ },
83
+ },
84
+ {
85
+ kind: 'exact',
86
+ path: path + '/verify',
87
+ handler: async (req, res) => {
88
+ if (!guard(req, res, 'POST'))
89
+ return;
90
+ const body = await readJsonBody(req);
91
+ const code = typeof body?.code === 'string' ? body.code : '';
92
+ writeJson(res, 200, channel.sendVerify(code));
93
+ },
94
+ },
95
+ ];
96
+ }
9
97
  export function apply(ctx) {
98
+ const channel = new WeixinChannel();
99
+ channel.startIfLoggedIn();
10
100
  ctx.effect(() => ctx.systemPrompt.section({
11
101
  name: 'plugin:dsh-rez-wechat',
12
102
  order: 162,
13
103
  text: GUIDANCE,
14
104
  }), 'dsh-rez-wechat: prompt');
105
+ const bag = ctx;
106
+ if (bag.webServer !== undefined) {
107
+ const routes = weixinRoutes(channel);
108
+ ctx.effect(() => {
109
+ const disposers = routes.map(route => bag.webServer.register(route));
110
+ return () => { for (const dispose of disposers)
111
+ dispose(); };
112
+ }, 'dsh-rez-wechat: weixin routes');
113
+ }
114
+ ctx.effect(() => () => { channel.dispose(); }, 'dsh-rez-wechat: weixin channel');
15
115
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rezti/dsh-rez-wechat",
3
- "version": "0.1.1",
4
- "description": "ReZ-TI WeChat/WeCom bridges for DeepSeek Harness. WeCom send uses wecom-mcp; personal WeChat login/chat reuses weixin-mcp (QClaw/ClawBot).",
3
+ "version": "0.1.2",
4
+ "description": "ReZ-TI WeChat/WeCom bridges. Personal WeChat is QClaw/ClawBot scan-and-chat via dsh-wechat-bridge; WeCom group send uses wecom-mcp.",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -20,10 +20,15 @@
20
20
  "README.md"
21
21
  ],
22
22
  "license": "Apache-2.0",
23
+ "dependencies": {
24
+ "qrcode": "^1.5.4"
25
+ },
23
26
  "devDependencies": {
24
27
  "@deepseek-ai/cordis": "^4.0.1",
28
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
25
29
  "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
26
30
  "@types/node": "^22.20.0",
31
+ "@types/qrcode": "^1.5.5",
27
32
  "typescript": "~5.7.2"
28
33
  },
29
34
  "publishConfig": {