dsh-remote-wecom 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
@@ -1,45 +1,41 @@
1
1
  # dsh-remote-wecom
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/dsh-remote-wecom)](https://www.npmjs.com/package/dsh-remote-wecom)
4
+ [![license](https://img.shields.io/npm/l/dsh-remote-wecom)](LICENSE)
4
5
 
5
- > Control DeepSeek Harness Desktop sessions from WeCom (企业微信) on your phone: send messages, receive results, switch sessions and models, answer approvals and questions, fetch artifacts. The docs are in Chinese; commands and cards are Chinese-first.
6
+ > Control DeepSeek Harness Desktop sessions from WeCom (企业微信) on your phone: send prompts, get results and files back, approve tool calls, switch sessions and models. Docs and commands are in Chinese.
6
7
 
7
- 在手机企业微信里遥控 DeepSeek Harness Desktop 的会话:发消息、看结果、切会话、切模型、审批与回答提问、取回产物。命令、卡片和推送策略与 [pi-remote-wecom](https://github.com/lukeinx/pi-remote-wecom) 一致,宿主换成 DSH。
8
+ 在手机企业微信里遥控 DeepSeek Harness Desktop:发消息、收结果、切会话和模型、审批与回答提问、取回产物。插件跑在 Desktop 里,自己持有企微智能机器人的长连接。
8
9
 
9
- 它是一个 DSH bundle 插件,跑在 Desktop 的 host 进程里,自己持有企微智能机器人长连接;Desktop 关闭即离线。设计与宿主接口见 [docs/DESIGN.md](docs/DESIGN.md)。
10
+ ![手机企业微信里切换会话、审批、插话、收结果和文件、切换模型](docs/images/chat.png)
10
11
 
11
- ## 与 pi-remote-wecom、dsh-im 的关系
12
-
13
- - [pi-remote-wecom](https://github.com/lukeinx/pi-remote-wecom):遥控 pi 终端 / 后台会话的 launchd daemon + pi 扩展。本项目搬来它的宿主无关部分(企微传输、命令与中文别名、卡片、推送与引用路由、关注/取关、目录别名、产物、outbox、附件),把 TUI / RPC 会话换成 Desktop 会话,HTTP 协议换成插件内直接调用 DSH 会话控制器。两者互不依赖,可以同时运行,但要用不同的机器人。
14
- - `@xmanrui/dsh-im`:DSH 官方生态里的多 IM 插件,企微交互是斜杠命令 + 单聊绑定单会话。本项目只参考了它的插件写法和审批 / 提问抢答方式,不依赖它;如果它的企微通道和本插件用同一个 BotID,两边会互相顶掉长连接。
12
+ - 任务跑完自动推结果,生成的文件一并发到手机
13
+ - 审批和提问同时发到手机和 Desktop,哪边先答用哪边
14
+ - 运行中可以插话、停止,也能设长期目标让它自己续跑
15
+ - 引用某条推送回复,消息就发回那个会话,多个任务并行不串
16
+ - Desktop 没开也能发:消息先暂存,后台拉起 Desktop 后自动处理(macOS)
15
17
 
16
18
  ## 要求
17
19
 
18
- - DeepSeek Harness Desktop(DSH `>=0.2.0-rc.2`,在 `0.2.1-rc.2` 上验证)
19
- - Node.js `>=22.19.0`(运行配置命令用)
20
- - 一个专用的企业微信智能机器人(长连接模式,拿到 BotID 和 Secret)。同一个 BotID 只能有一条长连接,不能与 pi-remote-wecom、dsh-im 企微通道共用。
20
+ - DeepSeek Harness Desktop(DSH `>=0.2.0-rc.2`)
21
+ - Node.js `>=22.19.0`(命令行用)
22
+ - 一个专用的企业微信智能机器人。同一个机器人只能有一条长连接,不要与其他程序共用
21
23
 
22
24
  ## 安装
23
25
 
24
- `desktop` profile 只归 Desktop 管,`dsh plugin --profile desktop add` 会报 `managed exclusively by the Electron application`。所以要先退出 Desktop,手动改 profile 的 `package.json`,再用 Desktop 自带的 pnpm 安装。
26
+ Desktop 运行时会锁住 `desktop` profile,`dsh plugin add` 用不了,需要手动加:
25
27
 
26
- 1. 完全退出 DeepSeek Harness Desktop。
27
- 2. 编辑 `~/.dsh/profiles/desktop/package.json`,保留已有条目,只加两处:
28
+ 1. 退出 Desktop。
29
+ 2. 在 `~/.dsh/profiles/desktop/package.json` 里追加(保留已有条目):
28
30
 
29
31
  ```json
30
32
  {
31
- "dependencies": {
32
- "dsh-remote-wecom": "^0.1.0"
33
- },
34
- "dsh": {
35
- "profile": {
36
- "bundles": ["…已有条目…", "dsh-remote-wecom"]
37
- }
38
- }
33
+ "dependencies": { "dsh-remote-wecom": "^0.1.2" },
34
+ "dsh": { "profile": { "bundles": ["…", "dsh-remote-wecom"] } }
39
35
  }
40
36
  ```
41
37
 
42
- 3. 安装并启动 Desktop:
38
+ 3. 安装并启动:
43
39
 
44
40
  ```sh
45
41
  cd ~/.dsh/profiles/desktop
@@ -47,157 +43,85 @@
47
43
  open -a "DeepSeek Harness"
48
44
  ```
49
45
 
50
- 4. 打开「设置 → 企微远程」,能看到二维码即安装成功,接着按下面「配置与绑定」操作。
51
-
52
- 升级:改 `dependencies` 里的版本号后重复第 3 步。
53
-
54
- 从源码安装(开发用):先 `git clone https://github.com/lukeinx/dsh-remote-wecom.git && cd dsh-remote-wecom && npm install && npm run build`(`build` 生成设置页前端 `dist/client.js`,它不进 git,缺了 Desktop 会加载失败),第 2 步的依赖写成 `"dsh-remote-wecom": "link:<仓库绝对路径>"`。改了插件代码(含重新 `npm run build` 的设置页)要重启 Desktop 才生效,配置变化不用。
46
+ 升级时改版本号,再重复第 3 步。
55
47
 
56
- ### 命令行
48
+ 也可以把下面这段发给 Agent(Claude Code、Cursor、pi 等)代装:
57
49
 
58
- 配置、绑定和看门狗还有命令行入口。下文的 `dsh-remote-wecom` 命令指向 profile 里装好的那份:
59
-
60
- ```sh
61
- alias dsh-remote-wecom='node ~/.dsh/profiles/desktop/node_modules/dsh-remote-wecom/bin/dsh-remote.mjs'
50
+ ```text
51
+ 按 https://github.com/lukeinx/dsh-remote-wecom 的 README 给我的 DeepSeek Harness Desktop 安装 dsh-remote-wecom,
52
+ 遵守「让 Agent 代装」一节的规则;装好后提醒我到「设置 → 企微远程」扫码绑定。
62
53
  ```
63
54
 
64
- 源码安装时直接用仓库里的 `node bin/dsh-remote.mjs`。不要用 `npx dsh-remote-wecom` 安装看门狗,`npx` 的缓存目录随时会被清掉。
55
+ ## 绑定
65
56
 
66
- ### 让 Agent 代装
57
+ 打开 Desktop「设置 → 企微远程」:
67
58
 
68
- 把本 README 交给 coding agent 时,请它遵守:
59
+ <img src="docs/images/settings.png" alt="Desktop 设置页「企微远程」" width="560">
69
60
 
70
- - 改 `~/.dsh/profiles/desktop/package.json` 前先退出 Desktop,只追加条目,不删除、不重排已有的 `dependencies` 和 `bundles`。
71
- - 不用 `dsh plugin --profile desktop add`(会被拒绝),用上面的 pnpm 命令。
72
- - 不读取、不打印 `config.json` 里的 Secret;扫码和发绑定码需要用户本人在手机上完成,Agent 只负责打开设置页或运行 `dsh-remote-wecom setup`。
73
- - 验证用 `dsh-remote-wecom status`:出现 `Secret:已配置` 和 `主人:已绑定` 即完成;末尾日志可以看连接情况。
74
- - 在本仓库里改代码前先读 [AGENTS.md](AGENTS.md)。
75
-
76
- ### Desktop 离线看门狗(可选,macOS)
77
-
78
- 插件跑在 Desktop 进程里,Desktop 退出后企微就没人接了。看门狗是一个 launchd 常驻进程,补上这段:
79
-
80
- ```sh
81
- dsh-remote-wecom watchdog install # 写入 ~/Library/LaunchAgents 并启动,登录后自动运行
82
- dsh-remote-wecom watchdog status # 是否运行、插件心跳、暂存消息数、最近日志
83
- dsh-remote-wecom watchdog uninstall
84
- ```
61
+ 1. 用企业微信扫码,自动创建机器人并连接。
62
+ 2. 10 分钟内给机器人发一条单聊消息,发送人即成为主人;过了窗口就发页面上的 6 位绑定码。之后只接主人的单聊。
85
63
 
86
- - 插件运行时每 10 秒写一次心跳(`alive.json`),看门狗只在心跳超过 30 秒没更新时才连企微,平时不占连接。
87
- - Desktop 离线时发来的消息先暂存到 `spool/`(附件已下载到 `inbox/`),看门狗用 `open -g -j` 在后台拉起 Desktop。插件上线后连上企微(同一机器人只能有一个连接,看门狗随即被挤下线),按顺序处理暂存的消息;超过 30 分钟的只提示不执行。拉起是预期路径,中间不发「已启动 / 已重启 / 开始处理」之类的通知,手机直接收到结果;只有拉起失败或超时才提醒。
88
- - 卡片点击引用的是插件内存里的状态,离线期间点了只提示重新操作,Desktop 拉起上线后再提醒一次。
89
- - 拉起 2 分钟后插件仍未上线(比如插件被停用),手机会收到提醒。
90
- - 安装时如果 Desktop 正在运行却没有心跳(还是旧版本插件),会拒绝安装:先重启 Desktop 加载新插件再装,否则看门狗会把正在运行的插件挤下线。
91
- - 安装时记下当前 Node 和插件所在路径,换 Node 版本、升级插件或移动仓库后要重新 `watchdog install`。
64
+ 同一页可以换机器人、解绑、选新会话位置、设置手机审批超时。扫不了码时可以手动填 BotID / Secret。
92
65
 
93
- ## 配置与绑定
66
+ 打开「桌面端退出后仍可拉起」(macOS):Desktop 没开时,手机发来的消息先暂存,并在后台启动 Desktop,启动后自动处理。移除插件前先关掉它。
94
67
 
95
- 推荐在 Desktop 里完成:**设置 → 企微远程**。
68
+ ## 手机上用
96
69
 
97
- 1. 页面显示二维码,用企业微信扫码并确认,自动创建智能机器人并写入 BotID / Secret(`config.json`,权限 600),插件随即连接。
98
- 2. 扫码成功后 10 分钟内,在企业微信里给这个机器人发任意一条单聊消息,发送人即成为主人。页面同时给出 6 位备用绑定码,自动绑定窗口过了也可以发绑定码完成绑定。之后只接主人的单聊消息。
99
- 3. 绑定后在同一页可以:更换机器人(重新扫码)、解绑账号或换绑定码、选「新会话位置」(Desktop 工作区或未分组,与手机上的 `工作区` 命令是同一项;宿主不支持工作区时不显示)、改「手机审批」的超时策略与秒数、开关「新建会话时打开 Desktop」、重新连接;目录别名只能用命令行 `alias` 管理。改动即时保存、立即生效。
70
+ 选中会话后直接发消息;引用某条推送回复,消息会发给该推送所属的会话。图片和文件可以先发,5 分钟内再补说明。任务完成后自动推送结果;审批和提问同时出现在手机和 Desktop,谁先答用谁的。
100
71
 
101
- 扫不了码时,页面上的「手动填写 BotID」可以直接输入已有机器人的 BotID / Secret。也可以用命令行(结果相同):
72
+ | 命令 | 作用 |
73
+ |---|---|
74
+ | `活跃会话` / `ls` | 切换到运行中、置顶或最近活跃的会话 |
75
+ | `历史会话 [关键词]` / `h` | 查找并继续历史会话 |
76
+ | `创建会话 [工作区] [消息]` / `n` | 新建会话并发送首条消息 |
77
+ | `工作区 [名称]` / `cd` | 选择新会话的默认位置 |
78
+ | `插话 内容` / `steer` | 插进正在运行的这一轮 |
79
+ | `目标 [描述]` / `goal` | 设置长期目标,自动续跑到完成 |
80
+ | `模型 [关键词]` / `model` | 切换模型 |
81
+ | `思考强度 [强度]` / `think` | 切换思考强度 |
82
+ | `权限模式 [模式]` / `perm` | 切换当前会话的权限模式 |
83
+ | `压缩` / `compact` | 压缩当前会话上下文 |
84
+ | `产物` / `art` | 取回当前会话生成的文件 |
85
+ | `关注` / `取关` | 电脑上发起的任务也推到手机 / 只推手机发起的 |
86
+ | `状态` / `停止` / `重启` / `帮助` | 查看状态、中断任务、重连、查看全部命令 |
87
+
88
+ 中文命令和参数之间要有空格:`模型 high` 是命令,`模型high` 会当普通消息发出。
89
+
90
+ ## 命令行
91
+
92
+ 设置页之外的备用入口:
102
93
 
103
94
  ```sh
104
- dsh-remote-wecom setup # 交互输入 BotID / Secret
105
- dsh-remote-wecom setup --bot-id ID --secret S # 非交互
95
+ alias dsh-remote-wecom='node ~/.dsh/profiles/desktop/node_modules/dsh-remote-wecom/bin/dsh-remote.mjs'
106
96
  ```
107
97
 
108
- `setup` 写入 `config.json` 并打印 6 位绑定码,在机器人单聊里发送绑定码,第一个发对的人成为主人。运行中的插件每 5 秒检查一次 `config.json` / `bind.json`,命令行改动后自动重连,不必重启 Desktop。
109
-
110
98
  | 命令 | 作用 |
111
99
  |---|---|
112
- | `setup [--bot-id ID --secret S]` | 写入凭证;未绑定时生成绑定码(换机器人会清掉原主人) |
113
- | `bind` | 解除当前主人,生成新绑定码(10 分钟有效,错 5 次作废) |
114
- | `status` | 配置、绑定状态、待发消息和最近日志(不显示 Secret) |
115
- | `alias [名称 目录 \| rm 名称]` | 查看或修改目录别名(「创建会话 别名 消息」「工作区 别名」用;别名目录是 Desktop 工作区时归入该工作区,否则进「未分组」) |
116
- | `watchdog install\|uninstall\|status` | Desktop 离线看门狗,见上面「安装」 |
117
-
118
- 所有命令都可加 `--dir 目录` 指定数据目录。
119
-
120
- 数据目录默认 `$DSH_HOME/integrations/dsh-remote-wecom/`(`DSH_HOME` 默认 `~/.dsh`),里面有 `config.json`、`bind.json`、`aliases.json`、`.default-place`(新建会话的默认工作区)、`phone-sessions.json`(最近一次用户消息来自手机的会话)、`outbox/`(断网待发)、`inbox/`(手机发来的附件)、`alive.json`(插件心跳)、`spool/` 与 `watchdog.log`(看门狗暂存的消息和日志)、`bridge.log` 等。
121
-
122
- `config.json` 可选项:
123
-
124
- | 字段 | 默认 | 说明 |
125
- |---|---|---|
126
- | `remoteConfirm` | `ask-then-allow` | 手机发起的任务里,审批转企微卡片;超时后 `ask-then-allow` 允许、`ask` 拒绝;`allow` 不问直接允许 |
127
- | `remoteConfirmTimeoutMs` | `600000` | 审批与提问的等待上限(1 秒到 1 天) |
128
- | `openDesktop` | `false` | 「创建会话」后用 `open dsh://open` 把 Desktop 窗口拉到前台(锁屏时跳过);Desktop 对这个链接只做聚焦窗口,不会切到新会话 |
129
- | `nameWaitMs` | `10000` | 创建会话后等模型起标题的时间,超时用首条消息截断的临时标题 |
130
- | `inboxDir` | `<数据目录>/inbox` | 附件落地目录 |
131
- | `dirAliases` | `{"home": "~"}` | 在没有 `aliases.json` 时使用的初始目录别名 |
132
-
133
- ## 常用命令
134
-
135
- **手机企微**:选中会话后,直接发消息即可。引用某条推送回复,消息会发给推送所属的会话,并把它切为当前会话。有其他会话推送过、你还没回过消息时,不引用直接发的消息会先暂存,弹卡片让你在当前会话和推送过的会话里选,选完直接投递。
136
-
137
- 命令与 pi-remote-wecom 相同;「DSH 下」一栏列出行为差异,空着的表示一致。
138
-
139
- | 缩写 | 命令 | 用途 | DSH 下 |
140
- |---|---|---|---|
141
- | `ls` | `活跃会话` / `切换会话` | 选择活跃会话 | 活跃 = 运行中、当前选中、Desktop 置顶的会话,加上最近 24 小时有活动的(最多 10 个);不含 subagent 和已归档,Desktop 重启不影响;置顶在前(📌),其余按 Desktop 侧栏的工作区顺序分组、未分组最后,每项标出工作区 |
142
- | `h` | `历史会话 [关键词]` | 查找并继续历史会话;默认隐藏 subagent 会话,加 `全部` 才显示(如 `历史会话 全部 关键词`) | 只检索会话名和每轮用户消息摘要,不检索回复;Desktop 里已归档的会话也要加 `全部` 才显示 |
143
- | `n` | `创建会话 [工作区] [消息]` / `新建会话` | 新建会话并投递首条消息 | 第一个词是工作区名、`未分组`、目录别名或 `~/路径` 时作为位置,否则用 `工作区` 选的默认位置;`openDesktop` 打开时新建后 `open dsh://open` 拉起窗口(没有跳到指定会话的入口,需在侧栏点开),默认不拉起 |
144
- | `nb` | `创建后台会话 [工作区] [消息]` / `新建后台会话` | 新建会话,不拉起窗口 | 同样是 Desktop 会话,不管 `openDesktop` 都不拉起 |
145
- | `steer` | `插话 内容` | 把内容插进正在运行的这一轮,结果随这一轮推回;空闲时等同普通消息 | DSH 独有。直接发消息仍是排到下一轮 |
146
- | `perm` | `权限模式 [模式名]` / `切换权限模式 [模式名]`(也可 `切换权限`;单发 `权限` 只弹卡片) | 切换当前会话的权限模式(如 read-only / workspace-write) | DSH 独有,走 `/permission`,只影响这个会话 |
147
- | `goal` | `目标 [描述]` / `目标 修改 描述` / `目标 暂停` / `目标 继续` / `目标 清除` | 给当前会话设一个长期目标,模型在同一会话里自动续跑,直到完成、受阻、暂停或到达轮数上限;不带参数弹卡片看状态、点选操作 | DSH 独有,走 `/goal`。中间各轮不推,结束时推一条并附最后一轮结果;Desktop 重启后执行中的目标不会自己接着跑,发 `目标 继续` |
148
- | `select N [消息]` | `选择会话 N [消息]` | 按列表序号切换,可顺带消息 | |
149
- | `model` | `切换模型 [关键词]` / `模型 [关键词]` | 切换当前会话的模型 | 列出 DSH 模型目录里可路由的全部模型,没有 scope 之分 |
150
- | `think` | `切换思考强度 [强度]` / `思考强度 [强度]` | 切换思考强度;只列当前模型支持的级别 | 级别取模型目录的 `reasoning.efforts` |
151
- | `compact` | `压缩会话 [说明]` / `压缩 [说明]` | 手动压缩当前会话上下文;运行中不可压缩 | 不支持指定压缩重点,带说明时照常压缩并在卡片提示 |
152
- | `restart` | `重启 [force]` / `重启服务` | 重启遥控服务;有任务运行时默认拒绝并列出占用会话,`重启 force` 强制 | 只重建插件内的遥控实例(重读配置、重连企微),不重启 Desktop,任务继续跑 |
153
- | `cd` | `工作区 [名称]` / `切换工作区`(也可 `目录`、`切换目录`) | 选择新建会话的默认位置 | pi 里是选目录;DSH 里列出 Desktop 的工作区和「未分组」(默认),不是工作区的别名目录新会话进未分组 |
154
- | `art` | `产物` / `查看产物` | 列出当前会话生成的 html/md/pdf/docx/xlsx/pptx/png/svg 文件(不含 git 跟踪的文件、隐藏目录和临时目录):不超过 3 个直接发送,更多时卡片多选 | 来源是 `write` / `edit` / `present` 工具和回复里的文件链接 |
155
- | `remote` / `mute` | `关注` / `取关`(也可 `取消关注`、`静音`) | 出门时 `关注`:电脑上发起的任务完成后也推送;回到电脑前 `取关`:只推手机发起的 | `关注` 后电脑发起的任务的审批也发到手机(Desktop 关窗后任务还在跑),`取关` 后留在 Desktop |
156
- | `status` / `stop` / `help` | `状态` / `停止` / `帮助` | 查看状态、中断任务、查看全部命令 | 状态卡片显示工作区(分支)、模型·思考强度、上下文、权限模式与 token 用量,没有花费和扩展状态项 |
157
-
158
- 图片或文件可先发送,再在 5 分钟内补充说明;每次最多 10 件,每件不超过 100MiB。任务完成后自动回传结果,长结果以文件发送。
159
-
160
- 中文别名需要与参数用空白字符分隔:`模型 high` 是命令,`模型high` 会当作普通消息发出。
161
-
162
- **电脑**:在「设置 → 企微远程」里管理,见上面的「配置与绑定」;没有 pi 里的 `/remote`、`/tabmodel`、`/tabname`(会话名由 DSH 自动生成)。
163
-
164
- 其他行为:
165
-
166
- - 手机发的消息 2 秒内完成就直接回结果,否则回「运行中」,完成后推送;超长结果转成 Markdown 文件。
167
- - 手机发起的任务里,审批和 `ask_user_question` 提问会同时出现在手机和 Desktop,谁先答用谁的;当前没选中的会话的弹窗先挂起,切过去时补推。多题逐题推送,可点选也可自己写。审批无人处理时按 `remoteConfirm` 决定。电脑发起的任务:提问也推手机,审批只在 `关注` 后推。
168
- - 定时提醒(`schedule_*` 工具设的)和目标续跑(goal)自动跑的轮次:会话最近一次用户消息来自手机(或目标是在手机上设定 / 继续的)时推回手机,最近一次来自电脑时按电脑发起处理。定时任务每次都推(「⏰ 定时任务完成」);目标续跑只在结束时推一条(「🎯 目标已完成 / 受阻 / 已暂停 / 到达轮数上限 / 已清除」),正文是最后一轮的结果,中途被中断或出错的一轮照常推。
169
- - 往 Desktop 里已归档的会话发消息(引用旧推送、「历史会话 全部」选中)会先取消归档并提示;取消失败时 DSH 会拦下这一轮,手机收到「任务失败」。
170
-
171
- ## 限制
172
-
173
- - 不装看门狗时,Desktop 未运行就离线;装了也只是暂存消息、拉起 Desktop,任务仍在 Desktop 里跑。
174
- - `compact` 不支持指定压缩重点(DSH `0.2.1-rc.2` 的 `/compact` 不接受参数),带说明时照常压缩并提示。
175
- - 历史检索只搜会话名和每轮用户消息的摘要(`turnOutline`),不搜回复。
176
- - 产物来自插件运行期间的事件,打开会话时再用最近 1000 条消息范围的快照补扫;临时目录、隐藏目录和 git 跟踪的文件不算产物。
177
- - 不能在手机上添加、移除或重命名工作区,也不能改会话名、分叉会话。
178
- - 插件随 Desktop 启停:Desktop 退出时,运行中的手机任务回一条「任务中断」(结果不再送回,任务本身可能已在电脑上完成),下次启动后收到「已重启」通知。「已重启」只在手机端用得上时发:手机发了 `重启`、重启时有推送中的任务被中断,或手机 30 分钟内用过;在电脑上随手重启 Desktop 不提醒;被看门狗拉起时也不发。看门狗暂存的消息是「至多一次」:插件取出即删,处理到一半时 Desktop 崩溃,剩下的不会重放。
179
- - 「关注」「取关」只在内存里生效:插件重启后恢复推送绑定会话的电脑任务,电脑发起的审批不再转手机。
100
+ | `setup [--bot-id ID --secret S]` | 写入机器人凭证并生成绑定码 |
101
+ | `bind` | 解除当前主人,生成新绑定码 |
102
+ | `status` | 查看配置、绑定状态和最近日志 |
103
+ | `alias [名称 目录 \| rm 名称]` | 管理目录别名,供「创建会话」「工作区」使用 |
104
+ | `watchdog install\|uninstall\|status` | 「桌面端退出后仍可拉起」的开关与状态 |
180
105
 
181
- ## 开发
106
+ 数据在 `~/.dsh/integrations/dsh-remote-wecom/`,`config.json` 权限为 600。可选配置和实现细节见 [docs/DESIGN.md](docs/DESIGN.md)。
182
107
 
183
- ```sh
184
- npm install
185
- npm run build # 打包设置页前端 client/ → dist/client.js
186
- npm test # 单元测试
187
- npm run check # build + 全部源文件语法检查 + 单元测试
188
- npm run check:public # 发布前检查:不含内网链接、本机路径,package.json 指向公开仓库
189
- npm run test:e2e # build 后用临时 DSH_HOME 起隔离的 dsh --profile web + 假 LLM + 假企微 + 假扫码接口,约 30 秒
190
- ```
108
+ ## 让 Agent 代装
191
109
 
192
- 端到端测试不碰 `~/.dsh`;会在 `$HOME` 下建一个临时工作目录(产物过滤会排除系统临时目录),结束时删除。
110
+ - 改 profile 前先退出 Desktop,只追加条目,不动已有的 `dependencies` 和 `bundles`。
111
+ - 扫码和绑定由用户在手机上完成;不读取、不打印 Secret。
112
+ - 用 `dsh-remote-wecom status` 验证:出现 `Secret:已配置` 和 `主人:已绑定` 即完成。
113
+ - 改本仓库代码前先读 [AGENTS.md](AGENTS.md)。
193
114
 
194
- 预览设置页(不碰 Desktop 和 `~/.dsh`,不请求真实扫码接口):
115
+ ## 开发
195
116
 
196
117
  ```sh
197
- node scripts/preview.mjs --state unconfigured # 或 bound / unbound;--port 指定端口,默认随机
118
+ git clone https://github.com/lukeinx/dsh-remote-wecom.git && cd dsh-remote-wecom
119
+ npm install
120
+ npm run check # 打包设置页 + 语法检查 + 单元测试
121
+ npm run test:e2e # 隔离的 DSH + 假企微,约 30 秒
198
122
  ```
199
123
 
200
- 脚本用临时 `DSH_HOME` 起 web 版 DSH,接假企微和假扫码接口(第 3 次轮询返回成功),打印带 `?token=` 的地址,第一次必须用这个地址打开,之后浏览器靠 cookie 免 token。打开后点「继续」关掉预览版说明,再点「更多 → 设置 → 企微远程」。在终端输入一行文字,等于用户 alice 在企微单聊里发给机器人,用来走扫码后的自动绑定。Ctrl-C 退出并删除临时目录。
124
+ 本地调试时把 profile 里的依赖改成 `"dsh-remote-wecom": "link:<仓库绝对路径>"`,改完代码重启 Desktop。
201
125
 
202
126
  ## 许可
203
127
 
@@ -7,8 +7,8 @@ import {parseArgs} from 'node:util';
7
7
  import {BIND_FILE, BIND_TTL_MS, CONFIG_FILE, VERSION, applyCredentials, defaultDataDir, hasCredentials, loadConfig, readUserConfig, resetOwner} from '../lib/core/config.mjs';
8
8
  import {checkName, expandDir, loadAliases, missingDirs, saveAliases, tildeDir} from '../lib/core/aliases.mjs';
9
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';
10
+ import {DESKTOP_BUNDLE_ID, createWatchdog, heartbeatAge, heartbeatFresh, spoolCount, watchdogState} from '../lib/core/watchdog.mjs';
11
+ import {createLaunchd, launchdSupported} from '../lib/core/launchd.mjs';
12
12
 
13
13
  const USAGE = `dsh-remote-wecom ${VERSION}
14
14
 
@@ -103,13 +103,9 @@ function alias(dir, args) {
103
103
  console.log(rows.length ? rows.map(([n, d]) => `${n} → ${d}${missing.has(n) ? '(目录不存在)' : ''}`).join('\n') : '还没有目录别名。');
104
104
  }
105
105
 
106
- const WATCHDOG_LABEL = 'com.dsh-remote-wecom.watchdog';
107
106
  const WATCHDOG_TICK_MS = 2000;
108
107
  const WATCHDOG_LOG = 'watchdog.log';
109
108
  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
109
 
114
110
  function watchdogRun(dir) {
115
111
  const file = path.join(dir, WATCHDOG_LOG);
@@ -129,55 +125,29 @@ function watchdogRun(dir) {
129
125
  process.on('SIGINT', quit);
130
126
  }
131
127
 
128
+ // 看门狗进程用的也是 Desktop 的可执行文件,按进程名判断会误报;只认登记到 LaunchServices 的 App。
132
129
  function desktopRunning() {
133
- try { execFileSync('pgrep', ['-x', 'DeepSeek Harness'], {stdio: 'ignore'}); return true; } catch { return false; }
130
+ try { return execFileSync('lsappinfo', ['find', `bundleid=${DESKTOP_BUNDLE_ID}`], {encoding: 'utf8'}).trim() !== ''; } catch { return false; }
134
131
  }
135
132
 
136
133
  function watchdogInstall(dir, force) {
137
134
  // 旧版插件不写心跳:此时装上看门狗会把正在运行的插件踢下线。
138
135
  if (!force && desktopRunning() && !heartbeatFresh(dir))
139
136
  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)`);
137
+ const launchd = createLaunchd({dir});
138
+ launchd.install();
139
+ console.log(`看门狗已安装并启动:${launchd.file}`);
140
+ console.log('也可以在 Desktop「设置 → 企微远程」里开关。');
169
141
  }
170
142
 
171
- function watchdogUninstall() {
172
- try { execFileSync('launchctl', ['bootout', `${domain()}/${WATCHDOG_LABEL}`], {stdio: 'ignore'}); } catch {}
173
- fs.rmSync(plistFile(), {force: true});
143
+ function watchdogUninstall(dir) {
144
+ createLaunchd({dir}).uninstall();
174
145
  console.log('看门狗已卸载。');
175
146
  }
176
147
 
177
148
  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()) ? '已安装但未加载' : '未安装'}`);
149
+ const launchd = createLaunchd({dir});
150
+ console.log(`看门狗:${launchd.loaded() ? '已安装并运行' : fs.existsSync(launchd.file) ? '已安装但未加载' : '未安装'}`);
181
151
  const age = heartbeatAge(dir);
182
152
  console.log(`插件心跳:${Number.isFinite(age) ? `${Math.round(age / 1000)} 秒前` : '无'}`);
183
153
  const wd = watchdogState(dir);
@@ -190,10 +160,10 @@ function watchdogStatus(dir) {
190
160
  }
191
161
 
192
162
  function watchdog(dir, [sub = 'status'], force) {
193
- if (process.platform !== 'darwin' && sub !== 'run') throw new Error('看门狗仅支持 macOS(launchd)');
163
+ if (!launchdSupported() && sub !== 'run') throw new Error('看门狗仅支持 macOS(launchd)');
194
164
  if (sub === 'run') watchdogRun(dir);
195
165
  else if (sub === 'install') watchdogInstall(dir, force);
196
- else if (sub === 'uninstall') watchdogUninstall();
166
+ else if (sub === 'uninstall') watchdogUninstall(dir);
197
167
  else if (sub === 'status') watchdogStatus(dir);
198
168
  else throw new Error('用法:dsh-remote-wecom watchdog install|uninstall|status');
199
169
  }
package/dist/client.js CHANGED
@@ -174,7 +174,8 @@ var zh = {
174
174
  "row.owner": "\u7ED1\u5B9A\u8D26\u53F7",
175
175
  "row.place": "\u65B0\u4F1A\u8BDD\u4F4D\u7F6E",
176
176
  "row.confirm": "\u624B\u673A\u5BA1\u6279",
177
- "row.openDesktop": "\u65B0\u5EFA\u4F1A\u8BDD\u65F6\u6253\u5F00 Desktop",
177
+ "row.openDesktop": "\u65B0\u5EFA\u4F1A\u8BDD\u65F6\u6253\u5F00\u684C\u9762\u7AEF",
178
+ "row.watchdog": "\u684C\u9762\u7AEF\u9000\u51FA\u540E\u4ECD\u53EF\u62C9\u8D77",
178
179
  "bot.rescan": "\u66F4\u6362",
179
180
  "bot.rescanConfirm": "\u5C06\u66FF\u6362\u5F53\u524D\u673A\u5668\u4EBA",
180
181
  "owner.unbind": "\u89E3\u7ED1",
@@ -220,6 +221,7 @@ var en = {
220
221
  "row.place": "New sessions in",
221
222
  "row.confirm": "Phone approvals",
222
223
  "row.openDesktop": "Open Desktop for new sessions",
224
+ "row.watchdog": "Stay reachable after Desktop quits",
223
225
  "bot.rescan": "Change",
224
226
  "bot.rescanConfirm": "Replaces the current bot",
225
227
  "owner.unbind": "Unlink",
@@ -432,7 +434,15 @@ function Configured({ s, now, act, busy, errors, setError, t }) {
432
434
  disabled: busy.open,
433
435
  onChange: (v) => act("open", "setPrefs", { openDesktop: v })
434
436
  }
435
- ))), /* @__PURE__ */ h("div", { className: "drw-foot" }, /* @__PURE__ */ h(Button, { variant: "outline", disabled: busy.restart, onClick: () => act("restart", "restart") }, busy.restart ? t("restarting") : t("restart")), errors.restart ? /* @__PURE__ */ h("span", { className: "drw-error", role: "alert" }, errors.restart) : null));
437
+ )), s.watchdog ? /* @__PURE__ */ h(Row, { label: t("row.watchdog"), error: errors.watchdog }, /* @__PURE__ */ h(
438
+ Switch,
439
+ {
440
+ checked: s.watchdog.enabled,
441
+ label: t("row.watchdog"),
442
+ disabled: busy.watchdog,
443
+ onChange: (v) => act("watchdog", "setWatchdog", { enabled: v })
444
+ }
445
+ )) : null), /* @__PURE__ */ h("div", { className: "drw-foot" }, /* @__PURE__ */ h(Button, { variant: "outline", disabled: busy.restart, onClick: () => act("restart", "restart") }, busy.restart ? t("restarting") : t("restart")), errors.restart ? /* @__PURE__ */ h("span", { className: "drw-error", role: "alert" }, errors.restart) : null));
436
446
  }
437
447
  function Settings({ call, t }) {
438
448
  const [s, setS] = useState(null);
package/docs/DESIGN.md CHANGED
@@ -104,6 +104,16 @@ DeepSeek Harness.app ── host 进程
104
104
  ## 配置与绑定
105
105
 
106
106
  - 数据目录 `$DSH_HOME/integrations/dsh-remote-wecom/`:`config.json`(`botId`、`secret`、`ownerUserId` 等)、`bind.json`、`cards.json`、`seen.json`、`aliases.json`、`.default-place`、`phone-sessions.json`、`outbox/`、`inbox/`、`alive.json`、`watchdog.json`、`spool/`、`bridge.log`、`watchdog.log`。
107
+ - `config.json` 可选项:
108
+
109
+ | 字段 | 默认 | 说明 |
110
+ |---|---|---|
111
+ | `remoteConfirm` | `ask-then-allow` | 手机发起的任务里,审批转企微卡片;超时后 `ask-then-allow` 允许、`ask` 拒绝;`allow` 不问直接允许 |
112
+ | `remoteConfirmTimeoutMs` | `600000` | 审批与提问的等待上限(1 秒到 1 天) |
113
+ | `openDesktop` | `false` | 「创建会话」后用 `open dsh://open` 把 Desktop 窗口拉到前台 |
114
+ | `nameWaitMs` | `10000` | 创建会话后等模型起标题的时间 |
115
+ | `inboxDir` | `<数据目录>/inbox` | 附件落地目录 |
116
+ | `dirAliases` | `{"home": "~"}` | 没有 `aliases.json` 时使用的初始目录别名 |
107
117
  - 首选 Desktop「设置 → 企微远程」扫码(见下节);命令行是后备:`node bin/dsh-remote.mjs setup` 写入 BotID / Secret 并生成 6 位绑定码,`bind` 解除原主人并重新生成绑定码,`status` 看状态,`alias` 管理目录别名。插件约每 5 秒检查 `config.json` / `bind.json` 的 mtime,变化后停掉并重建遥控实例(插件自己绑定主人时的写入不算),不必重启 Desktop。
108
118
  - 同一 BotID 只能一条长连接:不能与 pi remote、dsh-im 企微通道共用机器人。
109
119
 
@@ -134,7 +144,10 @@ DeepSeek Harness.app ── host 进程
134
144
 
135
145
  ## Desktop 离线看门狗
136
146
 
137
- 插件随 Desktop 启停,macOS 上 Cmd+Q 后企微无人接。`dsh-remote-wecom watchdog install` 装一个 launchd LaunchAgent(`KeepAlive`)跑 `watchdog run`,每 2 秒 tick 一次(`lib/core/watchdog.mjs`)。
147
+ 插件随 Desktop 启停,macOS 上 Cmd+Q 后企微无人接。设置页「桌面端退出后仍可拉起」(`setWatchdog`)或 `dsh-remote-wecom watchdog install` 装一个 launchd LaunchAgent(`KeepAlive`)跑 `watchdog run`,每 2 秒 tick 一次(`lib/core/watchdog.mjs`)。
148
+
149
+ - 登记在 `lib/core/launchd.mjs`。可执行文件优先用 Desktop 自带的 Electron(`ELECTRON_RUN_AS_NODE=1`,与 dsh 命令行相同),不依赖用户的 Node。这样的进程不向 LaunchServices 登记,`open -b` 照常拉起 Desktop;但进程名同为 `DeepSeek Harness`,判断 Desktop 是否在运行要用 `lsappinfo find bundleid=...`,不能用 `pgrep`。
150
+ - 插件启动时 `sync()`:登记指向本数据目录且内容与当前插件路径不一致(pnpm 升级后路径会变)或未加载时重装;指向其他数据目录的登记(如测试)不动。插件配置 `watchdog: false` 隐藏开关,`watchdogDryRun: true`(预览、e2e)把 plist 写进数据目录且不调 launchctl。
138
151
 
139
152
  - 交接靠心跳:插件 `apply` 时先写 `alive.json`(先于连企微),此后每 10 秒刷新,插件停止且遥控实例收尾后删除(只删自己 pid 写的)。心跳 30 秒内新鲜时看门狗不连、已连则断开;未配置或未绑定主人也不连。配置重载只重建遥控实例,心跳不断。
140
153
  - 企微服务端在新连接建立时向旧连接发 `disconnected_event` 并断开,SDK 不重连被踢的一方。插件上线时挤掉看门狗是预期路径;看门狗被其他连接踢掉时退避 5 分钟,避免与重复部署互踢。反向竞争(看门狗刚好在插件连上后才连,把插件踢掉):传输层记 `kickedAt`,看门狗下一个 tick 看到心跳会断开并把 `watchdog.json` 写成 `connected:false`,插件心跳 tick 发现「被踢之后看门狗已让出」就重建遥控实例。
@@ -0,0 +1,98 @@
1
+ // 看门狗的 launchd 登记:设置页开关与命令行共用。
2
+ // 优先用 Desktop 自带的 Electron 以 Node 模式运行(dsh 命令行也是这样跑的),不依赖用户自己装的 Node。
3
+ // 这样的进程不向 LaunchServices 登记,`open -b` 仍会正常拉起 Desktop。
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import {execFileSync} from 'node:child_process';
8
+ import {fileURLToPath} from 'node:url';
9
+
10
+ export const WATCHDOG_LABEL = 'com.dsh-remote-wecom.watchdog';
11
+ export const DESKTOP_EXECUTABLE = '/Applications/DeepSeek Harness.app/Contents/MacOS/DeepSeek Harness';
12
+ const BIN = fileURLToPath(new URL('../../bin/dsh-remote.mjs', import.meta.url));
13
+ const BOOTSTRAP_TRIES = 5;
14
+
15
+ export const launchdSupported = (platform = process.platform) => platform === 'darwin';
16
+
17
+ const xml = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
18
+ const sleep = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
19
+
20
+ function runtime() {
21
+ if (process.versions.electron) return {execPath: process.execPath, electron: true};
22
+ if (fs.existsSync(DESKTOP_EXECUTABLE)) return {execPath: DESKTOP_EXECUTABLE, electron: true};
23
+ return {execPath: process.execPath, electron: false};
24
+ }
25
+
26
+ export function renderPlist({dir, execPath, electron, bin = BIN, dshHome = process.env.DSH_HOME}) {
27
+ const args = [execPath, bin, 'watchdog', 'run', '--dir', dir];
28
+ const env = {
29
+ PATH: '/usr/bin:/bin:/usr/sbin:/sbin',
30
+ ...(electron ? {ELECTRON_RUN_AS_NODE: '1'} : {}),
31
+ ...(dshHome ? {DSH_HOME: dshHome} : {}),
32
+ };
33
+ return `<?xml version="1.0" encoding="UTF-8"?>
34
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
35
+ <plist version="1.0">
36
+ <dict>
37
+ <key>Label</key><string>${WATCHDOG_LABEL}</string>
38
+ <key>ProgramArguments</key>
39
+ <array>
40
+ ${args.map(a => ` <string>${xml(a)}</string>`).join('\n')}
41
+ </array>
42
+ <key>EnvironmentVariables</key>
43
+ <dict>
44
+ ${Object.entries(env).map(([k, v]) => ` <key>${k}</key><string>${xml(v)}</string>`).join('\n')}
45
+ </dict>
46
+ <key>RunAtLoad</key><true/>
47
+ <key>KeepAlive</key><true/>
48
+ <key>ProcessType</key><string>Background</string>
49
+ <key>StandardErrorPath</key><string>${xml(path.join(dir, 'watchdog.err'))}</string>
50
+ </dict>
51
+ </plist>
52
+ `;
53
+ }
54
+
55
+ /**
56
+ * 一台机器只登记一个看门狗:只认指向 dir 的那份,别的数据目录(如测试)不会改写它。
57
+ * @param {{dir: string, home?: string, uid?: number, run?: (cmd: string, args: string[]) => void, execPath?: string, electron?: boolean, bin?: string}} opts
58
+ */
59
+ export function createLaunchd({dir, home = os.homedir(), uid = process.getuid?.(), run = null, ...spec}) {
60
+ const exec = run || ((cmd, args) => execFileSync(cmd, args, {stdio: 'ignore'}));
61
+ const file = path.join(home, 'Library', 'LaunchAgents', `${WATCHDOG_LABEL}.plist`);
62
+ const target = `gui/${uid}/${WATCHDOG_LABEL}`;
63
+ const render = () => renderPlist({dir, ...runtime(), ...spec});
64
+ const read = () => { try { return fs.readFileSync(file, 'utf8'); } catch { return ''; } };
65
+ const bootout = () => { try { exec('launchctl', ['bootout', target]); } catch {} };
66
+ const loaded = () => { try { exec('launchctl', ['print', target]); return true; } catch { return false; } };
67
+
68
+ function install() {
69
+ fs.mkdirSync(path.dirname(file), {recursive: true});
70
+ fs.mkdirSync(dir, {recursive: true, mode: 0o700});
71
+ bootout();
72
+ fs.writeFileSync(file, render());
73
+ // bootout 是异步卸载,紧接着 bootstrap 可能还在卸载中。
74
+ for (let i = 1; ; i++) {
75
+ try { exec('launchctl', ['bootstrap', `gui/${uid}`, file]); return; }
76
+ catch (e) { if (i >= BOOTSTRAP_TRIES) throw e; sleep(200 * i); }
77
+ }
78
+ }
79
+ return {
80
+ file,
81
+ loaded,
82
+ install,
83
+ /** 当前登记的是不是这个数据目录的看门狗。 */
84
+ enabled: () => read().includes(`<string>${xml(dir)}</string>`),
85
+ uninstall() {
86
+ bootout();
87
+ fs.rmSync(file, {force: true});
88
+ },
89
+ /** 已开启时把登记对齐到当前插件(升级后安装路径会变);返回是否改写。 */
90
+ sync() {
91
+ const current = read();
92
+ if (!current.includes(`<string>${xml(dir)}</string>`)) return false;
93
+ if (current === render() && loaded()) return false;
94
+ install();
95
+ return true;
96
+ },
97
+ };
98
+ }
@@ -9,7 +9,7 @@ import path from 'node:path';
9
9
  import {tildeDir} from '../core/aliases.mjs';
10
10
 
11
11
  export const SETTINGS_ENDPOINT = 'dsh-remote-wecom/settings';
12
- export const SETTINGS_METHODS = ['state', 'qrStart', 'qrCancel', 'saveCredentials', 'newBindCode', 'unbind', 'setPrefs', 'setPlace', 'restart'];
12
+ export const SETTINGS_METHODS = ['state', 'qrStart', 'qrCancel', 'saveCredentials', 'newBindCode', 'unbind', 'setPrefs', 'setPlace', 'setWatchdog', 'restart'];
13
13
  // 与遥控实例共用:手机「工作区」命令和设置页写同一个文件,内容 {workspaceId} 或 {cwd},没有文件 = 未分组。
14
14
  export const DEFAULT_PLACE_FILE = '.default-place';
15
15
  const UNGROUPED = 'none';
@@ -89,11 +89,12 @@ const RELOAD_WAIT_MS = 2500;
89
89
  const QR_READY_WAIT_MS = 3000;
90
90
 
91
91
  /**
92
- * getWorkspaces 缺省时(宿主不支持工作区)不显示默认位置。
92
+ * getWorkspaces 缺省时(宿主不支持工作区)不显示默认位置;watchdog 缺省时(非 macOS)不显示看门狗开关。
93
93
  * @param {{dir: string, getRemote: () => any, getBootError: () => string, reload: (why: string, opts?: {notify?: boolean}) => Promise<void>, qr: any,
94
- * getWorkspaces?: (() => Promise<{items: {id: string, path: string, title: string}[]}>) | null}} deps
94
+ * getWorkspaces?: (() => Promise<{items: {id: string, path: string, title: string}[]}>) | null,
95
+ * watchdog?: {enabled: () => boolean, install: () => void, uninstall: () => void} | null}} deps
95
96
  */
96
- export function createSettingsHandler({dir, getRemote, getBootError, reload, qr, getWorkspaces = null}) {
97
+ export function createSettingsHandler({dir, getRemote, getBootError, reload, qr, getWorkspaces = null, watchdog = null}) {
97
98
  const settle = p => Promise.race([Promise.resolve(p).catch(() => {}), delay(RELOAD_WAIT_MS)]);
98
99
  const placeFile = path.join(dir, DEFAULT_PLACE_FILE);
99
100
 
@@ -156,6 +157,7 @@ export function createSettingsHandler({dir, getRemote, getBootError, reload, qr,
156
157
  ? {remoteConfirm: cfg.remoteConfirm, remoteConfirmTimeoutMs: cfg.remoteConfirmTimeoutMs, openDesktop: cfg.openDesktop}
157
158
  : {remoteConfirm: 'ask-then-allow', remoteConfirmTimeoutMs: 600000, openDesktop: false},
158
159
  place: placeState(items),
160
+ watchdog: watchdog ? {enabled: watchdog.enabled()} : null,
159
161
  version: VERSION,
160
162
  };
161
163
  }
@@ -212,6 +214,12 @@ export function createSettingsHandler({dir, getRemote, getBootError, reload, qr,
212
214
  fs.mkdirSync(dir, {recursive: true, mode: 0o700});
213
215
  fs.writeFileSync(placeFile, JSON.stringify({workspaceId: id}) + '\n', {mode: 0o600});
214
216
  },
217
+ async setWatchdog(payload) {
218
+ if (!watchdog) throw new Error('当前系统不支持');
219
+ if (typeof payload?.enabled !== 'boolean') throw new Error('参数无效');
220
+ try { payload.enabled ? watchdog.install() : watchdog.uninstall(); }
221
+ catch { throw new Error(payload.enabled ? '开启失败,请稍后重试' : '关闭失败,请稍后重试'); }
222
+ },
215
223
  async restart() {
216
224
  requireConfigured();
217
225
  await settle(reload('设置页重启服务'));
package/lib/index.mjs CHANGED
@@ -8,6 +8,7 @@ import {createQrAuth, createQrFlow} from './core/qr-auth.mjs';
8
8
  import {createDshHost} from './host/dsh-host.mjs';
9
9
  import {HEARTBEAT_MS, clearHeartbeat, watchdogState, writeHeartbeat} from './core/watchdog.mjs';
10
10
  import {createSettingsHandler, qrSuccessWriter, registerSettingsRoute} from './host/settings-rpc.mjs';
11
+ import {createLaunchd, launchdSupported} from './core/launchd.mjs';
11
12
 
12
13
  export const name = 'dsh-remote-wecom';
13
14
  export const inject = ['typertGateway', 'connection'];
@@ -95,9 +96,16 @@ export function apply(ctx, options = {}) {
95
96
  intervalMs: Number(options?.qrPollMs) > 0 ? Number(options.qrPollMs) : undefined,
96
97
  log: debug,
97
98
  });
99
+ // watchdogDryRun:预览和测试用,plist 写进数据目录、不调 launchctl。
100
+ const dryRun = options?.watchdogDryRun === true;
101
+ const launchd = options?.watchdog === false || !(dryRun || launchdSupported()) ? null
102
+ : createLaunchd({dir, ...(dryRun ? {home: dir, run: () => {}} : {})});
103
+ try { if (launchd?.sync()) debug('看门狗已对齐到当前插件'); }
104
+ catch (e) { warn(`看门狗更新失败:${e?.message || e}`); }
98
105
  const handler = createSettingsHandler({
99
106
  dir, getRemote: () => remote, getBootError: () => bootError, reload, qr,
100
107
  getWorkspaces: typeof host.workspaces === 'function' ? () => host.workspaces() : null,
108
+ watchdog: launchd,
101
109
  });
102
110
  ctx.effect(() => registerSettingsRoute(ctx, handler), 'dsh-remote-wecom: settings route');
103
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-wecom",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "DeepSeek Harness plugin: control Desktop sessions from WeCom, with session cards, attachments, approvals and task results.",
5
5
  "author": "lukeinx",
6
6
  "repository": {
@@ -19,7 +19,13 @@
19
19
  "deepseek-desktop",
20
20
  "wecom",
21
21
  "wechat-work",
22
- "remote-control"
22
+ "remote-control",
23
+ "deepseek",
24
+ "dsh",
25
+ "ai-agent",
26
+ "chatops",
27
+ "mobile",
28
+ "企业微信"
23
29
  ],
24
30
  "main": "lib/index.mjs",
25
31
  "exports": {
@@ -38,7 +44,7 @@
38
44
  "README.md",
39
45
  "LICENSE",
40
46
  "NOTICE.md",
41
- "docs"
47
+ "docs/DESIGN.md"
42
48
  ],
43
49
  "dsh": {
44
50
  "bundle": {