dsh-remote-wecom 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE.md ADDED
@@ -0,0 +1,11 @@
1
+ # Source notices
2
+
3
+ DSH Remote WeCom is distributed under the [MIT License](LICENSE), copyright 2026 Luke.
4
+
5
+ ## Included source
6
+
7
+ - `lib/core/` (session engine, commands, cards, WeCom transport, configuration, directory aliases and artifact filtering) was adapted from [pi-remote-wecom](https://github.com/lukeinx/pi-remote-wecom) `0.3.17` (npm `pi-remote-wecom@0.3.17`), also copyright 2026 Luke under MIT. Its own included-source notices (pi-remote, pi-tab-title) apply to the parts carried over; the tab-title module itself is not included here.
8
+
9
+ ## Dependencies
10
+
11
+ The WeCom transport uses the official [@wecom/aibot-node-sdk](https://github.com/WecomTeam/aibot-node-sdk), installed as a dependency. DeepSeek Harness modules are supplied by the host at runtime; `@deepseek-ai/dsh` is a development dependency used only by the end-to-end test. Dependency packages retain their own licenses; see their installed package contents and the lockfile metadata.
package/README.md CHANGED
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ # dsh-remote-wecom
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/dsh-remote-wecom)](https://www.npmjs.com/package/dsh-remote-wecom)
4
+
5
+ > Control DeepSeek Harness Desktop sessions from WeCom (企业微信) on your phone. Docs and commands are in Chinese.
6
+
7
+ 在手机企业微信里遥控 DeepSeek Harness Desktop:发消息、收结果、切会话和模型、审批与回答提问、取回产物。插件跑在 Desktop 里,自己持有企微智能机器人的长连接。
8
+
9
+ ## 要求
10
+
11
+ - DeepSeek Harness Desktop(DSH `>=0.2.0-rc.2`)
12
+ - Node.js `>=22.19.0`(命令行用)
13
+ - 一个专用的企业微信智能机器人。同一个机器人只能有一条长连接,不要与其他程序共用
14
+
15
+ ## 安装
16
+
17
+ Desktop 运行时会锁住 `desktop` profile,`dsh plugin add` 用不了,需要手动加:
18
+
19
+ 1. 退出 Desktop。
20
+ 2. 在 `~/.dsh/profiles/desktop/package.json` 里追加(保留已有条目):
21
+
22
+ ```json
23
+ {
24
+ "dependencies": { "dsh-remote-wecom": "^0.1.0" },
25
+ "dsh": { "profile": { "bundles": ["…", "dsh-remote-wecom"] } }
26
+ }
27
+ ```
28
+
29
+ 3. 安装并启动:
30
+
31
+ ```sh
32
+ cd ~/.dsh/profiles/desktop
33
+ node "/Applications/DeepSeek Harness.app/Contents/Resources/runtime/primary-runtime/dependencies/pnpm/bin/pnpm.mjs" install
34
+ open -a "DeepSeek Harness"
35
+ ```
36
+
37
+ 升级时改版本号,再重复第 3 步。
38
+
39
+ ## 绑定
40
+
41
+ 打开 Desktop「设置 → 企微远程」:
42
+
43
+ 1. 用企业微信扫码,自动创建机器人并连接。
44
+ 2. 10 分钟内给机器人发一条单聊消息,发送人即成为主人;过了窗口就发页面上的 6 位绑定码。之后只接主人的单聊。
45
+
46
+ 同一页可以换机器人、解绑、选新会话位置、设置手机审批超时。扫不了码时可以手动填 BotID / Secret。
47
+
48
+ ## 手机上用
49
+
50
+ 选中会话后直接发消息;引用某条推送回复,消息会发给该推送所属的会话。图片和文件可以先发,5 分钟内再补说明。任务完成后自动推送结果;审批和提问同时出现在手机和 Desktop,谁先答用谁的。
51
+
52
+ | 命令 | 作用 |
53
+ |---|---|
54
+ | `活跃会话` / `ls` | 切换到运行中、置顶或最近活跃的会话 |
55
+ | `历史会话 [关键词]` / `h` | 查找并继续历史会话 |
56
+ | `创建会话 [工作区] [消息]` / `n` | 新建会话并发送首条消息 |
57
+ | `工作区 [名称]` / `cd` | 选择新会话的默认位置 |
58
+ | `插话 内容` / `steer` | 插进正在运行的这一轮 |
59
+ | `目标 [描述]` / `goal` | 设置长期目标,自动续跑到完成 |
60
+ | `模型 [关键词]` / `model` | 切换模型 |
61
+ | `思考强度 [强度]` / `think` | 切换思考强度 |
62
+ | `权限模式 [模式]` / `perm` | 切换当前会话的权限模式 |
63
+ | `压缩` / `compact` | 压缩当前会话上下文 |
64
+ | `产物` / `art` | 取回当前会话生成的文件 |
65
+ | `关注` / `取关` | 电脑上发起的任务也推到手机 / 只推手机发起的 |
66
+ | `状态` / `停止` / `重启` / `帮助` | 查看状态、中断任务、重连、查看全部命令 |
67
+
68
+ 中文命令和参数之间要有空格:`模型 high` 是命令,`模型high` 会当普通消息发出。
69
+
70
+ ## 离线看门狗(可选,macOS)
71
+
72
+ 插件随 Desktop 启停,Desktop 退出后就没人接企微了。看门狗是一个独立的 launchd 进程:发现插件不在线时暂存消息、在后台拉起 Desktop,插件上线后自动处理。
73
+
74
+ ```sh
75
+ alias dsh-remote-wecom='node ~/.dsh/profiles/desktop/node_modules/dsh-remote-wecom/bin/dsh-remote.mjs'
76
+ dsh-remote-wecom watchdog install # 升级插件或换 Node 后要重新执行
77
+ dsh-remote-wecom watchdog status
78
+ dsh-remote-wecom watchdog uninstall
79
+ ```
80
+
81
+ ## 命令行
82
+
83
+ 设置页之外的备用入口(`dsh-remote-wecom` 即上面的 alias):
84
+
85
+ | 命令 | 作用 |
86
+ |---|---|
87
+ | `setup [--bot-id ID --secret S]` | 写入机器人凭证并生成绑定码 |
88
+ | `bind` | 解除当前主人,生成新绑定码 |
89
+ | `status` | 查看配置、绑定状态和最近日志 |
90
+ | `alias [名称 目录 \| rm 名称]` | 管理目录别名,供「创建会话」「工作区」使用 |
91
+ | `watchdog install\|uninstall\|status` | 离线看门狗 |
92
+
93
+ 数据在 `~/.dsh/integrations/dsh-remote-wecom/`,`config.json` 权限为 600。可选配置和实现细节见 [docs/DESIGN.md](docs/DESIGN.md)。
94
+
95
+ ## 让 Agent 代装
96
+
97
+ - 改 profile 前先退出 Desktop,只追加条目,不动已有的 `dependencies` 和 `bundles`。
98
+ - 扫码和绑定由用户在手机上完成;不读取、不打印 Secret。
99
+ - 用 `dsh-remote-wecom status` 验证:出现 `Secret:已配置` 和 `主人:已绑定` 即完成。
100
+ - 改本仓库代码前先读 [AGENTS.md](AGENTS.md)。
101
+
102
+ ## 开发
103
+
104
+ ```sh
105
+ git clone https://github.com/lukeinx/dsh-remote-wecom.git && cd dsh-remote-wecom
106
+ npm install
107
+ npm run check # 打包设置页 + 语法检查 + 单元测试
108
+ npm run test:e2e # 隔离的 DSH + 假企微,约 30 秒
109
+ ```
110
+
111
+ 本地调试时把 profile 里的依赖改成 `"dsh-remote-wecom": "link:<仓库绝对路径>"`,改完代码重启 Desktop。
112
+
113
+ ## 许可
114
+
115
+ MIT,见 [LICENSE](LICENSE) 与 [NOTICE.md](NOTICE.md)。
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ // 电脑端配置工具:只读写数据目录,运行中的插件约 5 秒内发现配置变化并重连。
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import readline from 'node:readline/promises';
6
+ import {parseArgs} from 'node:util';
7
+ import {BIND_FILE, BIND_TTL_MS, CONFIG_FILE, VERSION, applyCredentials, defaultDataDir, hasCredentials, loadConfig, readUserConfig, resetOwner} from '../lib/core/config.mjs';
8
+ import {checkName, expandDir, loadAliases, missingDirs, saveAliases, tildeDir} from '../lib/core/aliases.mjs';
9
+ import {execFileSync} from 'node:child_process';
10
+ import {fileURLToPath} from 'node:url';
11
+ import {createWatchdog, heartbeatAge, heartbeatFresh, spoolCount, watchdogState} from '../lib/core/watchdog.mjs';
12
+
13
+ const USAGE = `dsh-remote-wecom ${VERSION}
14
+
15
+ 用法:
16
+ dsh-remote-wecom setup [--bot-id ID --secret SECRET] 写入机器人凭证并生成绑定码(不带参数则交互输入)
17
+ dsh-remote-wecom bind 重新生成绑定码(解除当前主人)
18
+ dsh-remote-wecom status 查看配置与最近日志
19
+ dsh-remote-wecom alias [名称 目录 | rm 名称] 查看或修改目录别名
20
+ dsh-remote-wecom watchdog install|uninstall|status Desktop 离线看门狗(launchd):暂存消息并后台拉起 Desktop
21
+
22
+ 选项:
23
+ --dir 目录 数据目录,默认 $DSH_HOME/integrations/dsh-remote-wecom(DSH_HOME 默认 ~/.dsh)`;
24
+
25
+ const LOG_TAIL = 20;
26
+
27
+ function printCode(code) {
28
+ console.log(`\n绑定码:${code}(${BIND_TTL_MS / 60000} 分钟内有效)`);
29
+ console.log('在企业微信里打开与该机器人的单聊,发送这 6 位数字完成绑定。');
30
+ }
31
+ async function prompt(question, {secret = false} = {}) {
32
+ const rl = readline.createInterface({input: process.stdin, output: process.stdout, terminal: true});
33
+ if (secret) rl._writeToOutput = s => { if (/\n|\r/.test(s) || s.startsWith(question)) process.stdout.write(s.startsWith(question) ? question : s); };
34
+ try { return (await rl.question(question)).trim(); } finally { rl.close(); }
35
+ }
36
+
37
+ async function setup(dir, values) {
38
+ const user = readUserConfig(dir);
39
+ let botId = values['bot-id'];
40
+ let secret = values.secret;
41
+ if (!botId && !secret) {
42
+ if (!process.stdin.isTTY) throw new Error('非交互环境请用 setup --bot-id ID --secret SECRET');
43
+ botId = await prompt(`BotID${user.botId ? `(回车沿用 ${user.botId})` : ''}:`) || user.botId;
44
+ secret = await prompt('Secret:', {secret: true});
45
+ process.stdout.write('\n');
46
+ }
47
+ if (!botId || !secret) throw new Error('BotID 和 Secret 都需要提供');
48
+ const {bound, code} = applyCredentials(dir, {botId, secret});
49
+ console.log(`配置已保存:${path.join(dir, CONFIG_FILE)}`);
50
+ if (bound) {
51
+ console.log('已绑定主人,保持不变;需要换绑时执行 dsh-remote-wecom bind。');
52
+ return;
53
+ }
54
+ printCode(code);
55
+ console.log('运行中的 DeepSeek Harness 会在约 5 秒内自动连接;若未安装插件,请先按 README 安装并重启 Desktop。');
56
+ }
57
+
58
+ function bind(dir) {
59
+ const {unbound, code} = resetOwner(dir);
60
+ if (unbound) console.log('已解除原主人的绑定。');
61
+ printCode(code);
62
+ }
63
+
64
+ function status(dir) {
65
+ console.log(`dsh-remote-wecom ${VERSION}`);
66
+ console.log(`数据目录:${dir}`);
67
+ let cfg;
68
+ try { cfg = loadConfig(dir); }
69
+ catch (e) { console.log(`配置:无效(${e.message})`); return; }
70
+ if (!hasCredentials(cfg)) console.log('配置:未配置机器人,运行 dsh-remote-wecom setup');
71
+ else {
72
+ console.log(`BotID:${cfg.botId}`);
73
+ console.log('Secret:已配置');
74
+ console.log(`主人:${cfg.ownerUserId ? '已绑定' : '未绑定'}`);
75
+ }
76
+ try {
77
+ const b = JSON.parse(fs.readFileSync(path.join(dir, BIND_FILE), 'utf8'));
78
+ const left = b.expiresAt - Date.now();
79
+ console.log(`绑定码:${left > 0 ? `有效,剩余 ${Math.ceil(left / 60000)} 分钟` : '已过期,运行 dsh-remote-wecom bind 重新生成'}`);
80
+ } catch {}
81
+ console.log(`审批超时:${Math.round(cfg.remoteConfirmTimeoutMs / 1000)} 秒后${cfg.remoteConfirm === 'ask' ? '拒绝' : '允许'}${cfg.remoteConfirm === 'allow' ? '(remoteConfirm=allow,不询问)' : ''}`);
82
+ let pending = 0;
83
+ try { pending = fs.readdirSync(path.join(dir, 'outbox')).filter(f => f.endsWith('.json')).length; } catch {}
84
+ if (pending) console.log(`待发消息:${pending} 条`);
85
+ let lines = [];
86
+ try { lines = fs.readFileSync(path.join(dir, 'bridge.log'), 'utf8').trimEnd().split('\n').slice(-LOG_TAIL); } catch {}
87
+ console.log(lines.length ? `\n最近日志:\n${lines.join('\n')}` : '\n还没有日志:插件未运行过,或 Desktop 未重启。');
88
+ }
89
+
90
+ function alias(dir, args) {
91
+ let table = loadAliases(dir);
92
+ if (args[0] === 'rm' && args[1]) {
93
+ const name = checkName(args[1]);
94
+ if (!(name in table)) throw new Error(`没有别名「${name}」`);
95
+ delete table[name];
96
+ table = saveAliases(dir, table);
97
+ } else if (args.length === 2) {
98
+ const name = checkName(args[0]);
99
+ table = saveAliases(dir, {...table, [name]: tildeDir(expandDir(args[1]))});
100
+ } else if (args.length) throw new Error('用法:dsh-remote-wecom alias 名称 目录 | alias rm 名称');
101
+ const rows = Object.entries(table);
102
+ const missing = new Set(missingDirs(table));
103
+ console.log(rows.length ? rows.map(([n, d]) => `${n} → ${d}${missing.has(n) ? '(目录不存在)' : ''}`).join('\n') : '还没有目录别名。');
104
+ }
105
+
106
+ const WATCHDOG_LABEL = 'com.dsh-remote-wecom.watchdog';
107
+ const WATCHDOG_TICK_MS = 2000;
108
+ const WATCHDOG_LOG = 'watchdog.log';
109
+ const WATCHDOG_LOG_MAX = 1024 * 1024;
110
+ const plistFile = () => path.join(process.env.HOME, 'Library', 'LaunchAgents', `${WATCHDOG_LABEL}.plist`);
111
+ const domain = () => `gui/${process.getuid()}`;
112
+ const xml = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
113
+
114
+ function watchdogRun(dir) {
115
+ const file = path.join(dir, WATCHDOG_LOG);
116
+ const log = line => {
117
+ try {
118
+ if (fs.statSync(file, {throwIfNoEntry: false})?.size > WATCHDOG_LOG_MAX) fs.renameSync(file, `${file}.1`);
119
+ fs.appendFileSync(file, `${new Date().toISOString()} ${line}\n`, {mode: 0o600});
120
+ } catch {}
121
+ };
122
+ const dog = createWatchdog({dir, log});
123
+ log('看门狗启动');
124
+ const tick = () => { try { dog.tick(); } catch (e) { log(`tick 失败:${e?.message || e}`); } };
125
+ tick();
126
+ const timer = setInterval(tick, WATCHDOG_TICK_MS);
127
+ const quit = () => { clearInterval(timer); dog.stop(); setTimeout(() => process.exit(0), 300); };
128
+ process.on('SIGTERM', quit);
129
+ process.on('SIGINT', quit);
130
+ }
131
+
132
+ function desktopRunning() {
133
+ try { execFileSync('pgrep', ['-x', 'DeepSeek Harness'], {stdio: 'ignore'}); return true; } catch { return false; }
134
+ }
135
+
136
+ function watchdogInstall(dir, force) {
137
+ // 旧版插件不写心跳:此时装上看门狗会把正在运行的插件踢下线。
138
+ if (!force && desktopRunning() && !heartbeatFresh(dir))
139
+ throw new Error('DSH Desktop 正在运行,但插件没有心跳(可能是旧版本插件或插件未启用)。请先重启 Desktop 加载新版插件再安装;确认无误可加 --force。');
140
+ const args = [process.execPath, fileURLToPath(import.meta.url), 'watchdog', 'run', '--dir', dir];
141
+ const env = {PATH: '/usr/bin:/bin:/usr/sbin:/sbin', ...(process.env.DSH_HOME ? {DSH_HOME: process.env.DSH_HOME} : {})};
142
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
143
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
144
+ <plist version="1.0">
145
+ <dict>
146
+ <key>Label</key><string>${WATCHDOG_LABEL}</string>
147
+ <key>ProgramArguments</key>
148
+ <array>
149
+ ${args.map(a => ` <string>${xml(a)}</string>`).join('\n')}
150
+ </array>
151
+ <key>EnvironmentVariables</key>
152
+ <dict>
153
+ ${Object.entries(env).map(([k, v]) => ` <key>${k}</key><string>${xml(v)}</string>`).join('\n')}
154
+ </dict>
155
+ <key>RunAtLoad</key><true/>
156
+ <key>KeepAlive</key><true/>
157
+ <key>ProcessType</key><string>Background</string>
158
+ <key>StandardErrorPath</key><string>${xml(path.join(dir, 'watchdog.err'))}</string>
159
+ </dict>
160
+ </plist>
161
+ `;
162
+ fs.mkdirSync(path.dirname(plistFile()), {recursive: true});
163
+ fs.mkdirSync(dir, {recursive: true, mode: 0o700});
164
+ try { execFileSync('launchctl', ['bootout', `${domain()}/${WATCHDOG_LABEL}`], {stdio: 'ignore'}); } catch {}
165
+ fs.writeFileSync(plistFile(), plist);
166
+ execFileSync('launchctl', ['bootstrap', domain(), plistFile()]);
167
+ console.log(`看门狗已安装并启动:${plistFile()}`);
168
+ console.log(`Node:${process.execPath}(升级或切换 Node 后请重新执行 watchdog install)`);
169
+ }
170
+
171
+ function watchdogUninstall() {
172
+ try { execFileSync('launchctl', ['bootout', `${domain()}/${WATCHDOG_LABEL}`], {stdio: 'ignore'}); } catch {}
173
+ fs.rmSync(plistFile(), {force: true});
174
+ console.log('看门狗已卸载。');
175
+ }
176
+
177
+ function watchdogStatus(dir) {
178
+ let loaded = false;
179
+ try { execFileSync('launchctl', ['print', `${domain()}/${WATCHDOG_LABEL}`], {stdio: 'ignore'}); loaded = true; } catch {}
180
+ console.log(`看门狗:${loaded ? '已安装并运行' : fs.existsSync(plistFile()) ? '已安装但未加载' : '未安装'}`);
181
+ const age = heartbeatAge(dir);
182
+ console.log(`插件心跳:${Number.isFinite(age) ? `${Math.round(age / 1000)} 秒前` : '无'}`);
183
+ const wd = watchdogState(dir);
184
+ if (wd) console.log(`企微连接:${wd.connected ? '看门狗持有' : '看门狗未连接'}(${new Date(wd.at).toLocaleString()})`);
185
+ const n = spoolCount(dir);
186
+ if (n) console.log(`暂存消息:${n} 条`);
187
+ let lines = [];
188
+ try { lines = fs.readFileSync(path.join(dir, WATCHDOG_LOG), 'utf8').trimEnd().split('\n').slice(-LOG_TAIL); } catch {}
189
+ if (lines.length) console.log(`\n最近日志:\n${lines.join('\n')}`);
190
+ }
191
+
192
+ function watchdog(dir, [sub = 'status'], force) {
193
+ if (process.platform !== 'darwin' && sub !== 'run') throw new Error('看门狗仅支持 macOS(launchd)');
194
+ if (sub === 'run') watchdogRun(dir);
195
+ else if (sub === 'install') watchdogInstall(dir, force);
196
+ else if (sub === 'uninstall') watchdogUninstall();
197
+ else if (sub === 'status') watchdogStatus(dir);
198
+ else throw new Error('用法:dsh-remote-wecom watchdog install|uninstall|status');
199
+ }
200
+
201
+ try {
202
+ const {values, positionals} = parseArgs({allowPositionals: true, options: {
203
+ 'bot-id': {type: 'string'}, secret: {type: 'string'}, dir: {type: 'string'}, force: {type: 'boolean'}, help: {type: 'boolean', short: 'h'},
204
+ }});
205
+ const [command = 'help', ...rest] = positionals;
206
+ const dir = values.dir ? path.resolve(expandDir(values.dir)) : defaultDataDir();
207
+ if (values.help || command === 'help') console.log(USAGE);
208
+ else if (command === 'setup') await setup(dir, values);
209
+ else if (command === 'bind') bind(dir);
210
+ else if (command === 'status') status(dir);
211
+ else if (command === 'alias') alias(dir, rest);
212
+ else if (command === 'watchdog') watchdog(dir, rest, values.force);
213
+ else { console.log(USAGE); process.exitCode = 2; }
214
+ } catch (e) {
215
+ console.error(e.message);
216
+ process.exitCode = 1;
217
+ }
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-remote-wecom
3
+ name: dsh-remote-wecom