dsh-helper 0.16.7 → 0.17.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/CHANGELOG.md +109 -0
- package/docs/DEVELOPMENT.md +1 -1
- package/lib/client.js +2 -0
- package/lib/index.js +86 -11
- package/package.json +1 -1
- package/src/host/restart.js +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,115 @@
|
|
|
8
8
|
> (`package-lock.json` 根节点跟齐)与本文件顶部段落,两者连同提交一起走;发布只补 annotated tag,
|
|
9
9
|
> 不再用 `npm version` 升版。
|
|
10
10
|
|
|
11
|
+
## [0.17.1] — 未发布
|
|
12
|
+
|
|
13
|
+
**修复 `lib/client.js` 的构建污染**:仓库里那份产物被打进了错误的 zod。
|
|
14
|
+
|
|
15
|
+
### 问题
|
|
16
|
+
|
|
17
|
+
本地 `node_modules` 从未 `npm ci` 过(只有一个 `.cache` 目录),esbuild 解析 `zod`
|
|
18
|
+
时向上层目录搜,命中了 **`../dsh-expert/node_modules/zod`(4.6.2)**,于是产物变成
|
|
19
|
+
994300 字节、内嵌 4.6.2;而干净环境(npm ci 后有 `node_modules/zod` 4.6.5)应当产出
|
|
20
|
+
**994948 字节、内嵌 4.6.5**。表现为 `verify:ci` 报「产物与仓库里的 lib/client.js 不一致」,
|
|
21
|
+
但 `src/client` 一行都没改 —— 是构建环境的问题,不是代码的问题。
|
|
22
|
+
|
|
23
|
+
### 修复
|
|
24
|
+
|
|
25
|
+
`npm ci` 补齐依赖后 `npm run build` 重出产物:994948 字节,md5 `a68f5e62…`,
|
|
26
|
+
内嵌 `node_modules/zod` 4.6.5。typecheck / client smoke(81) / host smoke(118) 全绿。
|
|
27
|
+
|
|
28
|
+
> 提醒:以后在干净 clone 上改客户端代码前,先 `npm ci`,否则产物会被隔壁仓库的依赖污染。
|
|
29
|
+
|
|
30
|
+
## [0.17.0] — 2026-11-11
|
|
31
|
+
|
|
32
|
+
**`send_file_to_im` 默认跟随当前会话,不再要求每个人手配 botId / toUserId。**
|
|
33
|
+
|
|
34
|
+
### 为什么改
|
|
35
|
+
|
|
36
|
+
0.16.9 及以前,「发给谁」只能靠 `config.json` 里的 `botId` + `toUserId` 或者工具参数指定。
|
|
37
|
+
这对单人环境够用,但这个插件是公开装的:**每个用户都得先去微信机器人插件里把
|
|
38
|
+
账号 id 和收件人 id 抄一遍**,抄错就静默失败(跨机器人投递在 iLink 侧是「已受理但
|
|
39
|
+
对方收不到」)。而这两个值本来就在微信机器人插件的 `config.json` 里,且**当前会话是
|
|
40
|
+
谁发起的**这件事,会话本身就知道。
|
|
41
|
+
|
|
42
|
+
### 现在怎么做
|
|
43
|
+
|
|
44
|
+
新增会话推断:从最近的 `user/message` 里解析 IM 层注入的
|
|
45
|
+
`<dsh_im_source>{"senderId":"…"}</dsh_im_source>`,用 senderId 去 match 账号表的
|
|
46
|
+
`ownerUserId`,反查出该用哪个机器人、回给谁。
|
|
47
|
+
|
|
48
|
+
取值优先级(高 → 低):
|
|
49
|
+
|
|
50
|
+
| 层 | 账号 | 收件人 |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| ① 工具参数 | `botId` | `toUserId` |
|
|
53
|
+
| ② 当前会话推断 | 匹配到的机器人 | 该 senderId |
|
|
54
|
+
| ③ config.json | `botId` | `toUserId` |
|
|
55
|
+
| ④ 兜底 | 第一个账号 | 该账号 `ownerUserId` |
|
|
56
|
+
|
|
57
|
+
`config.json` 从「必填」降级为「非 IM 会话时的默认值」——对话框、CLI、定时任务
|
|
58
|
+
这些没有 IM 来源的会话,仍然按它发。
|
|
59
|
+
|
|
60
|
+
### 实测
|
|
61
|
+
|
|
62
|
+
用真实会话事件验证(妻子账号的会话):
|
|
63
|
+
|
|
64
|
+
- 会话推断 → `{botId: wx_97dda581…, toUserId: o9cq8008sm…}` ✅
|
|
65
|
+
- 无参数无配置 → 跟随会话 ✅
|
|
66
|
+
- 配置指向 A、会话是 B → **按会话走 B** ✅
|
|
67
|
+
- 显式传参 → 压过一切 ✅
|
|
68
|
+
- 非 IM 会话 / 账号表查无此人 → 返回 null,正常回落到配置 ✅
|
|
69
|
+
|
|
70
|
+
## [0.16.9] — 2026-11-11
|
|
71
|
+
|
|
72
|
+
**`send_file_to_im` 支持指定用哪个微信机器人发送** —— 修掉「在 A 账号的会话里发文件,永远从第一个账号发出」的问题。
|
|
73
|
+
|
|
74
|
+
### 新增
|
|
75
|
+
|
|
76
|
+
- 工具参数 `botId`:显式指定用哪个微信机器人账号发送文件。
|
|
77
|
+
省略时仍按 config.json 的 `botId` → 首个账号的顺序回落,行为与旧版一致。
|
|
78
|
+
- 为什么必须有它:跨机器人投递在 iLink 侧是静默失败的——用 A 机器人的 token 给 B 机器人
|
|
79
|
+
的用户发文件,接口会回「受理」,但对方永远收不到(各 bot 的用户 id 体系互相不认)。
|
|
80
|
+
之前实现里 `account = accountStore.list()[0]`,与当前会话所属账号无关,于是
|
|
81
|
+
「在谁的会话说、就发给谁」这个直觉落空了。
|
|
82
|
+
|
|
83
|
+
### 用法
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
send_file_to_im({ path: '/绝对/路径.xlsx', botId: 'wx_97dda58120f44f44ffa50e8e' })
|
|
87
|
+
// 收件人省略时会自动用该账号的 ownerUserId,即「发给这个机器人绑定的人」
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## [0.16.8] — 2026-11-11
|
|
91
|
+
|
|
92
|
+
**桌面端的重启按钮不再骗人** —— 原先在 DSH Desktop 里它显示为可点,一点就报「重启失败:untrusted」。
|
|
93
|
+
|
|
94
|
+
### 修复
|
|
95
|
+
|
|
96
|
+
- **根因**:桌面窗口跑在 `dsh-app://app`,请求由 Electron 主进程的 `forwardWebRequest` 转发给宿主,
|
|
97
|
+
而它转发前会删掉 `origin` / `host` / `cookie` / `sec-fetch-site`。重启闸 `trustedRestartRequest`
|
|
98
|
+
要求 `Origin` 存在且与 `Host` 同源,于是桌面端恒为 false;而状态接口走的是松一档的
|
|
99
|
+
`trustedLocalRequest`(放行无 Origin),按钮因此显示成"可点" —— 两者不一致,成了
|
|
100
|
+
「看起来能用、一点就 403」。
|
|
101
|
+
- 新增 `detectedDesktopHost()`(判据 `DSH_CLIENT_VERSION`,Electron 主进程只发给桌面端宿主;
|
|
102
|
+
刻意不吃 `ELECTRON_RUN_AS_NODE` —— 那是「Electron 当 Node 用」的通用标记,会误伤别的 Electron 应用):
|
|
103
|
+
桌面端 `/restart/status` 返回 `allowed: false, reason: 'desktop'`,按钮置灰并提示
|
|
104
|
+
「桌面端宿主由 DeepSeek Harness App 管理:退出 App 后重新打开即可」;`POST /restart` 补上同一档 403,
|
|
105
|
+
且**排在来源闸之前**,免得用户继续看到没头没尾的 untrusted。
|
|
106
|
+
- 与上游 dshmarket 口径一致:它在桌面端把重启标成 `supported: false` / `managedBy: 'desktop-host'`。
|
|
107
|
+
桌面端也确实不该自己重启 —— 宿主一退出,Electron 主进程立刻 `fail("dsh desktop host stopped")`
|
|
108
|
+
进 fatal 恢复;插件另起的替代进程还拿不到主进程持有的 hostCookie(窗口会 401)。
|
|
109
|
+
- 冒烟:新增 4 条桌面端断言(含「只认 `DSH_CLIENT_VERSION`」与「非桌面端的无 Origin 请求仍被来源闸拒掉」
|
|
110
|
+
两条边界,后者保证这一档没把来源闸整体顶掉),host-smoke 114 → 118 项;`docs/DEVELOPMENT.md`
|
|
111
|
+
自检表的项数同步。
|
|
112
|
+
|
|
113
|
+
### 备注
|
|
114
|
+
|
|
115
|
+
- **桌面端其余功能不受影响**:`/config`、`/send-file`、`/restart/status` 走的都是放行无 Origin 的那道闸,
|
|
116
|
+
一直是正常的;桌面端只有「重启」这一项走不通。
|
|
117
|
+
- 桌面端要重启宿主,用 App 自己的方式:退出 App 后重新打开(开发模式下 App 菜单里另有
|
|
118
|
+
「重启应用与 Host」,正式版没有这一项)。
|
|
119
|
+
|
|
11
120
|
## [0.16.7] — 未发布
|
|
12
121
|
|
|
13
122
|
**README 精简** —— 357 行 → 55 行。
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -53,7 +53,7 @@ npm run build # tools/build-client.mjs → lib/client.js(esbuild)
|
|
|
53
53
|
| --- | --- | --- |
|
|
54
54
|
| `npm run typecheck` | `tsc -p tsconfig.json`:只检查标了 `// @ts-check` 的自研 Host 文件(`lib/schedule*.js`),不碰构建产物 | 见下 |
|
|
55
55
|
| `npm run smoke` | 模拟 `__ModuleLoader__` + 假 React/DOM 跑客户端逻辑:打标、未读判定、cron 模型往返、样式表结构、布局度量、remote 方法集合、提示音、界面外观覆盖规则、设置卡片分组与投递加固两项的呈现、apply 分阶段降级 | 81 项 |
|
|
56
|
-
| `npm run host-smoke` | host 侧纯函数与真实引擎:图片嗅探、投递方式、错误翻译、投递加固(口令 / 目录白名单)、重启安全闸、平台适配 spawn、文案表、来源闸,以及用真实调度引擎跑的定时任务用例(执行窗口并发变更、迁移诊断自动合并 / 仍搁浅、合并幂等性 = 删任务后提示消失 / migration 字段穿过 persist、损坏文件留存、改名迁移与幂等) |
|
|
56
|
+
| `npm run host-smoke` | host 侧纯函数与真实引擎:图片嗅探、投递方式、错误翻译、投递加固(口令 / 目录白名单)、重启安全闸、平台适配 spawn、文案表、来源闸,以及用真实调度引擎跑的定时任务用例(执行窗口并发变更、迁移诊断自动合并 / 仍搁浅、合并幂等性 = 删任务后提示消失 / migration 字段穿过 persist、损坏文件留存、改名迁移与幂等) | 118 项 |
|
|
57
57
|
| `npm run render-smoke` | 客户端组件真实渲染断言(配置卡片、定时任务面板、界面外观组、标题栏刷新按钮) | 10 项 |
|
|
58
58
|
| `npm run preview` | 把呼吸灯样式抽成预览页,调动画参数用 | — |
|
|
59
59
|
| `npm run verify` | `typecheck` + `build` + 上面三个冒烟 | — |
|
package/lib/client.js
CHANGED
|
@@ -23481,6 +23481,8 @@ function restartDisabledTitle(reason) {
|
|
|
23481
23481
|
return "\u5BBF\u4E3B\u6B63\u88AB\u8C03\u8BD5\u5668\u9644\u7740\uFF0C\u4E0D\u80FD\u91CD\u542F";
|
|
23482
23482
|
case "supervisor":
|
|
23483
23483
|
return "\u5BBF\u4E3B\u7531 systemd \u7B49\u76D1\u7763\u5668\u7BA1\u7406\uFF0C\u91CD\u542F\u529F\u80FD\u5DF2\u7981\u7528";
|
|
23484
|
+
case "desktop":
|
|
23485
|
+
return "\u684C\u9762\u7AEF\u5BBF\u4E3B\u7531 DeepSeek Harness App \u7BA1\u7406\uFF1A\u9000\u51FA App \u540E\u91CD\u65B0\u6253\u5F00\u5373\u53EF";
|
|
23484
23486
|
case "untrusted":
|
|
23485
23487
|
return "\u8BF7\u6C42\u6765\u6E90\u4E0D\u662F\u540C\u6E90\u73AF\u56DE";
|
|
23486
23488
|
default:
|
package/lib/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { IMAGE_EXTENSIONS, sniffImageKind } from '../src/host/image-sniff.js';
|
|
34
34
|
import {
|
|
35
35
|
detectedDebugger,
|
|
36
|
+
detectedDesktopHost,
|
|
36
37
|
detectedSupervisor,
|
|
37
38
|
scheduleRestart,
|
|
38
39
|
servingPort,
|
|
@@ -713,8 +714,13 @@ async function handleRestartStatusRequest(request, response) {
|
|
|
713
714
|
const restartEnabled = settings.restartEnabled !== false;
|
|
714
715
|
const supervisor = detectedSupervisor();
|
|
715
716
|
const debuggerAttached = detectedDebugger();
|
|
717
|
+
const desktopHost = detectedDesktopHost();
|
|
716
718
|
let reason = null;
|
|
719
|
+
// 桌面端排在 debugger / supervisor 之前:它是"这一档从根本上不该点",
|
|
720
|
+
// 而调试器/监督器是"这台机器上的偶然状态"。桌面端打开宿主调试端口时两者会同时命中,
|
|
721
|
+
// 那时该告诉用户的是前者。
|
|
717
722
|
if (!restartEnabled) reason = 'disabled-in-config';
|
|
723
|
+
else if (desktopHost !== null) reason = 'desktop';
|
|
718
724
|
else if (debuggerAttached !== null) reason = 'debugger';
|
|
719
725
|
else if (supervisor !== null) reason = 'supervisor';
|
|
720
726
|
json(response, 200, {
|
|
@@ -722,14 +728,15 @@ async function handleRestartStatusRequest(request, response) {
|
|
|
722
728
|
reason,
|
|
723
729
|
supervisor,
|
|
724
730
|
debugger: debuggerAttached,
|
|
731
|
+
desktopHost,
|
|
725
732
|
restartEnabled,
|
|
726
733
|
});
|
|
727
734
|
}
|
|
728
735
|
|
|
729
736
|
/**
|
|
730
737
|
* POST /api/dsh-helper/restart
|
|
731
|
-
*
|
|
732
|
-
*
|
|
738
|
+
* 过五道闸:restartEnabled / detectedDesktopHost / trustedRestartRequest /
|
|
739
|
+
* detectedDebugger / detectedSupervisor,通过则 scheduleRestart(port),本进程 500ms 后自杀。
|
|
733
740
|
*/
|
|
734
741
|
async function handleRestartRequest(request, response) {
|
|
735
742
|
if (request.method !== 'POST') {
|
|
@@ -742,6 +749,13 @@ async function handleRestartRequest(request, response) {
|
|
|
742
749
|
json(response, 403, { error: 'disabled-in-config', reason: 'disabled-in-config' });
|
|
743
750
|
return;
|
|
744
751
|
}
|
|
752
|
+
// 桌面端必须排在 trustedRestartRequest **之前**:Electron 的 forwardWebRequest 转发时
|
|
753
|
+
// 会剥掉 Origin,这一档在桌面端永远过不了来源闸,先判桌面端才能给出说得通的理由
|
|
754
|
+
// (否则用户看到的是没头没尾的 untrusted,而按钮又是亮的)。
|
|
755
|
+
if (detectedDesktopHost() !== null) {
|
|
756
|
+
json(response, 403, { error: 'desktop', reason: 'desktop' });
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
745
759
|
if (!trustedRestartRequest(request)) {
|
|
746
760
|
json(response, 403, { error: 'untrusted', reason: 'untrusted' });
|
|
747
761
|
return;
|
|
@@ -1071,6 +1085,50 @@ export function describeDeliveryFailure(error, { tokenAgeMinutes } = {}) {
|
|
|
1071
1085
|
return wrapped;
|
|
1072
1086
|
}
|
|
1073
1087
|
|
|
1088
|
+
/**
|
|
1089
|
+
* 从当前会话最近的入站消息里推断「该用哪个机器人、发给谁」。
|
|
1090
|
+
*
|
|
1091
|
+
* 入站消息带一段由 IM 层注入的 `<dsh_im_source>{"senderId":"…"}</dsh_im_source>`,
|
|
1092
|
+
* 其中的 senderId 就是这次对话的主人。拿它去 match 账号表的 ownerUserId,就能反查出
|
|
1093
|
+
* 该用哪个机器人回他——**不需要用户在插件配置里再抄一遍 botId / toUserId**。
|
|
1094
|
+
*
|
|
1095
|
+
* 推断失败(不是 IM 会话、或账号表里没有这个人)返回 null,调用方按老逻辑回落。
|
|
1096
|
+
*/
|
|
1097
|
+
function inferImTargetFromSession(session, accounts) {
|
|
1098
|
+
let events;
|
|
1099
|
+
try {
|
|
1100
|
+
events = typeof session?.snapshotEvents === 'function'
|
|
1101
|
+
? session.snapshotEvents()
|
|
1102
|
+
: session?.events;
|
|
1103
|
+
} catch {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
if (!Array.isArray(events) || events.length === 0) return null;
|
|
1107
|
+
|
|
1108
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
1109
|
+
const ev = events[i];
|
|
1110
|
+
if (ev?.type !== 'user/message') continue;
|
|
1111
|
+
const content = ev?.data?.content ?? ev?.data?.message?.content;
|
|
1112
|
+
const text = Array.isArray(content)
|
|
1113
|
+
? content.map((part) => (part?.type === 'text' && typeof part.text === 'string' ? part.text : '')).join('')
|
|
1114
|
+
: '';
|
|
1115
|
+
if (!text || !text.includes('dsh_im_source')) continue;
|
|
1116
|
+
|
|
1117
|
+
const matched = /<dsh_im_source>\s*(\{[\s\S]*?\})\s*<\/dsh_im_source>/.exec(text);
|
|
1118
|
+
if (!matched) continue;
|
|
1119
|
+
let senderId;
|
|
1120
|
+
try {
|
|
1121
|
+
senderId = JSON.parse(matched[1])?.senderId;
|
|
1122
|
+
} catch {
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (typeof senderId !== 'string' || !senderId.trim()) continue;
|
|
1126
|
+
const owner = accounts.find((a) => a?.ownerUserId === senderId.trim());
|
|
1127
|
+
if (owner?.botId) return { botId: owner.botId, toUserId: senderId.trim() };
|
|
1128
|
+
}
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1074
1132
|
/**
|
|
1075
1133
|
* dsh-im 后端:把文件投递到微信。
|
|
1076
1134
|
*
|
|
@@ -1078,14 +1136,21 @@ export function describeDeliveryFailure(error, { tokenAgeMinutes } = {}) {
|
|
|
1078
1136
|
* 文件已由 deliverFile 读完并校验过大小时才进来。
|
|
1079
1137
|
*/
|
|
1080
1138
|
async function deliverViaDshIm({
|
|
1081
|
-
credentials, settings, absolutePath, bytes, signal, logger,
|
|
1139
|
+
credentials, settings, absolutePath, bytes, signal, logger, session,
|
|
1082
1140
|
}) {
|
|
1083
1141
|
const dshImDir = resolveDshImDir(settings.dshImDir);
|
|
1084
1142
|
const { api, state, config } = await loadWeixinModules(dshImDir);
|
|
1085
1143
|
|
|
1086
1144
|
const dir = weixinDir();
|
|
1087
1145
|
const accountStore = await new config.WeixinConfigStore(join(dir, 'config.json')).load();
|
|
1088
|
-
|
|
1146
|
+
// 优先级:显式参数 → 当前会话推断 → 配置默认值 → 第一个账号。
|
|
1147
|
+
// 会话推断排在配置之前,是因为「谁在跟我说话」比「设置里填过谁」更精确:
|
|
1148
|
+
// 在有 IM 来源的会话里,默认就该回给那个发消息的人。
|
|
1149
|
+
const inferred = inferImTargetFromSession(session, accountStore.list());
|
|
1150
|
+
const wantedBotId = (typeof settings.explicitBotId === 'string' ? settings.explicitBotId.trim() : '')
|
|
1151
|
+
|| inferred?.botId
|
|
1152
|
+
|| (typeof settings.botId === 'string' ? settings.botId.trim() : '')
|
|
1153
|
+
|| '';
|
|
1089
1154
|
const account = (wantedBotId ? accountStore.get(wantedBotId) : null) ?? accountStore.list()[0];
|
|
1090
1155
|
|
|
1091
1156
|
if (!account?.botId) {
|
|
@@ -1098,7 +1163,9 @@ async function deliverViaDshIm({
|
|
|
1098
1163
|
throw new Error('微信机器人凭据缺失,请在「设置 → IM机器人」里移除后重新接入。');
|
|
1099
1164
|
}
|
|
1100
1165
|
|
|
1101
|
-
const toUserId = (typeof settings.
|
|
1166
|
+
const toUserId = (typeof settings.explicitToUserId === 'string' && settings.explicitToUserId.trim())
|
|
1167
|
+
|| (inferred && inferred.botId === account.botId ? inferred.toUserId : '')
|
|
1168
|
+
|| (typeof settings.toUserId === 'string' && settings.toUserId.trim())
|
|
1102
1169
|
|| account.ownerUserId;
|
|
1103
1170
|
if (!toUserId) throw new Error('没有可用的收件人(账号里缺少 ownerUserId),请在配置里指定 toUserId。');
|
|
1104
1171
|
|
|
@@ -1225,7 +1292,7 @@ export function detectDeliveryBackend(settings = {}) {
|
|
|
1225
1292
|
|
|
1226
1293
|
/** 核心:把文件投递到 IM。后端选好后各自投递,本函数只负责「读文件 + 分派」。 */
|
|
1227
1294
|
async function deliverFile({
|
|
1228
|
-
credentials, settings, filePath, workspace, signal, logger,
|
|
1295
|
+
credentials, settings, filePath, workspace, signal, logger, session,
|
|
1229
1296
|
}) {
|
|
1230
1297
|
// 白名单在这里归一化(一次 realpath),再交给唯一的读文件收口点。
|
|
1231
1298
|
const { absolutePath, bytes } = await readDeliveryFile(
|
|
@@ -1239,7 +1306,7 @@ async function deliverFile({
|
|
|
1239
1306
|
});
|
|
1240
1307
|
}
|
|
1241
1308
|
return deliverViaDshIm({
|
|
1242
|
-
credentials, settings, absolutePath, bytes, signal, logger,
|
|
1309
|
+
credentials, settings, absolutePath, bytes, signal, logger, session,
|
|
1243
1310
|
});
|
|
1244
1311
|
}
|
|
1245
1312
|
|
|
@@ -1761,6 +1828,13 @@ export function apply(ctx) {
|
|
|
1761
1828
|
type: 'string',
|
|
1762
1829
|
description: '可选:收件人 id。省略时用配置里的默认收件人,再退回该账号绑定的用户。',
|
|
1763
1830
|
},
|
|
1831
|
+
botId: {
|
|
1832
|
+
type: 'string',
|
|
1833
|
+
description: '可选:用哪个微信机器人账号发送(如 wx_97dda58120f44f44ffa50e8e)。'
|
|
1834
|
+
+ '省略时用 config.json 里的 botId,再退回第一个账号。'
|
|
1835
|
+
+ '**要投递给另一个微信绑定(另一个机器人账号的用户)时必须指定**——'
|
|
1836
|
+
+ '否则会从第一个机器人发出,跨机器人投递会被平台静默丢弃。',
|
|
1837
|
+
},
|
|
1764
1838
|
},
|
|
1765
1839
|
required: ['path'],
|
|
1766
1840
|
},
|
|
@@ -1787,20 +1861,21 @@ export function apply(ctx) {
|
|
|
1787
1861
|
+ '在「插件列表 → 辅助补丁」的设置卡片里打开「允许把文件投递到 IM」即可恢复。');
|
|
1788
1862
|
}
|
|
1789
1863
|
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1864
|
+
// 显式参数单独带下去:它必须压过「会话推断」和「配置默认值」。
|
|
1865
|
+
const explicitToUserId = typeof args?.toUserId === 'string' ? args.toUserId.trim() : '';
|
|
1866
|
+
const explicitBotId = typeof args?.botId === 'string' ? args.botId.trim() : '';
|
|
1793
1867
|
|
|
1794
1868
|
const workspace = exec?.agent?.session?.header?.cwd;
|
|
1795
1869
|
return deliverFile({
|
|
1796
1870
|
credentials: imCtx.credentials,
|
|
1797
1871
|
// 后端目录(dshImDir / imConnectDir)已经随 ...settings 展开(来自 config.json),
|
|
1798
1872
|
// 不需要再从 apply 的第二参数里拼一次——那个参数以前根本没被 DSH 传进来。
|
|
1799
|
-
settings: { ...settings,
|
|
1873
|
+
settings: { ...settings, explicitToUserId, explicitBotId },
|
|
1800
1874
|
filePath: args?.path,
|
|
1801
1875
|
workspace: typeof workspace === 'string' && workspace ? workspace : undefined,
|
|
1802
1876
|
signal: exec?.signal,
|
|
1803
1877
|
logger: imCtx.logger,
|
|
1878
|
+
session: exec?.agent?.session,
|
|
1804
1879
|
});
|
|
1805
1880
|
},
|
|
1806
1881
|
});
|
package/package.json
CHANGED
package/src/host/restart.js
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* - scheduleRestart 不接受 recovery 参数(不写 .recovery.json)
|
|
8
8
|
* - restartHelperSource 不带 recovery 移交分支
|
|
9
9
|
* - 删除 recoveryScriptPath(依赖 lib/recovery.js 产物,本插件无产物)
|
|
10
|
+
* - 新增 detectedDesktopHost:dshmarket 靠配置里的 desktopHost 标记达成同一件事,
|
|
11
|
+
* 本插件没有那份配置,改成从宿主环境自己认
|
|
10
12
|
*
|
|
11
13
|
* 安全闸逻辑一行不漏照搬:
|
|
12
14
|
* - supervisor: INVOCATION_ID||JOURNAL_STREAM 且 (ppid===1 || comm==='systemd')
|
|
13
15
|
* - debugger: inspector.url() || execArgv/NODE_OPTIONS 按 token 前缀匹配
|
|
16
|
+
* - desktop: DSH_CLIENT_VERSION 非空(Electron 主进程只发给桌面端宿主)
|
|
14
17
|
* - 请求来源: trustedRestartRequest(独立文件)
|
|
15
18
|
*/
|
|
16
19
|
|
|
@@ -81,6 +84,35 @@ export function detectedSupervisor(
|
|
|
81
84
|
return null;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/**
|
|
88
|
+
* 桌面端宿主检测(DSH Desktop)。
|
|
89
|
+
*
|
|
90
|
+
* 判据取 `DSH_CLIENT_VERSION`:Electron 主进程在 spawn 宿主进程时注入它
|
|
91
|
+
* (app.asar/lib/main.js 里构造 DesktopHostProcess 的那处 env),`dsh web`、
|
|
92
|
+
* 内置 CLI、headless 都不会有。比 `ELECTRON_RUN_AS_NODE` 精确 —— 后者是所有
|
|
93
|
+
* "把 Electron 当 Node 用"的通用标记,别的 Electron 应用里跑 dsh 也会命中。
|
|
94
|
+
*
|
|
95
|
+
* 为什么桌面端要单独判一档(2026-10-06 实测定案):桌面窗口跑在 `dsh-app://app`,
|
|
96
|
+
* 请求由 Electron 主进程的 forwardWebRequest 转发给宿主,而它**转发前会删掉
|
|
97
|
+
* origin / host / cookie / sec-fetch-site**。于是重启闸(trustedRestartRequest
|
|
98
|
+
* 要求 Origin 存在且与 Host 同源)在桌面端恒为 false,用户点按钮只会拿到
|
|
99
|
+
* `403 untrusted`;而 status 走的是松一档的 trustedLocalRequest(放行无 Origin),
|
|
100
|
+
* 按钮反倒显示成"可点"——两者不一致,就成了"看起来能用、一点就报错"。
|
|
101
|
+
* 上游 dshmarket 在 routes.ts 里把桌面端的 restart 直接标成
|
|
102
|
+
* `supported: false` / `managedBy: 'desktop-host'`,本插件补上同一档。
|
|
103
|
+
*
|
|
104
|
+
* 桌面端也确实不该自己重启:宿主一退出,Electron 主进程立刻
|
|
105
|
+
* `fail("dsh desktop host stopped")` 进 fatal 恢复;插件另起的替代进程还拿不到
|
|
106
|
+
* 主进程持有的 hostCookie(窗口会 401)。正确做法是让用户从 App 侧退出后重开。
|
|
107
|
+
*
|
|
108
|
+
* @returns {'desktop' | null}
|
|
109
|
+
*/
|
|
110
|
+
export function detectedDesktopHost(env = process.env) {
|
|
111
|
+
const version = env.DSH_CLIENT_VERSION;
|
|
112
|
+
if (typeof version !== 'string' || version.trim() === '') return null;
|
|
113
|
+
return 'desktop';
|
|
114
|
+
}
|
|
115
|
+
|
|
84
116
|
/**
|
|
85
117
|
* 检测器附着检测。按以下优先级:
|
|
86
118
|
* 1. inspector.url() 已设置(覆盖 --inspect 启动 / inspector.open() / SIGUSR1 attach)
|