@zmainer/dsh-wx-bridge 1.1.0 → 1.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 +19 -8
- package/lib/kernel/acp-overlay-chat.yml +4 -5
- package/lib/kernel/acp-overlay.yml +4 -5
- package/lib/kernel/bridge.mjs +8 -7
- package/lib/kernel/wxpush.mjs +30 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ dsh plugin --profile <profile> add @zmainer/dsh-wx-bridge
|
|
|
74
74
|
"preset": "<预设 id,留空跟随 DSH 默认预设>",
|
|
75
75
|
"enabled": true,
|
|
76
76
|
"permPolicy": "ask",
|
|
77
|
-
"permPreset": "
|
|
77
|
+
"permPreset": "workspace-write",
|
|
78
78
|
"approvalTimeoutMs": 120000
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -95,7 +95,7 @@ dsh plugin --profile <profile> add @zmainer/dsh-wx-bridge
|
|
|
95
95
|
| `acp.dshBin` | ACP 用的 dsh 入口(默认跟随宿主运行时,schema 才一致) | 自动 |
|
|
96
96
|
| `acp.patch` | 覆盖叠层文件路径(预设档自动生成) | 插件内置 |
|
|
97
97
|
| `acp.permPolicy` | 审批策略:`ask`(默认,推到微信等回复)/ `allow`(自动批准)/ `reject`(一律拒绝) | ask |
|
|
98
|
-
| `acp.permPreset` | 手机通道权限预设(写进 ACP 叠层):`
|
|
98
|
+
| `acp.permPreset` | 手机通道权限预设(写进 ACP 叠层):`workspace-write`(默认)/ `read-only` / `danger-full-access`。越界的写与提权会在微信里弹审批卡 | workspace-write |
|
|
99
99
|
| `acp.approvalTimeoutMs` | 审批卡等待时长(超时按拒绝) | 120000 |
|
|
100
100
|
| `acp.patchAcp` | 是否允许给已装 `dsh-acp` 打预设补丁 | true |
|
|
101
101
|
|
|
@@ -127,10 +127,10 @@ dsh plugin --profile <profile> add @zmainer/dsh-wx-bridge
|
|
|
127
127
|
|
|
128
128
|
## 安全基线
|
|
129
129
|
|
|
130
|
-
- **权限预设(`acp.permPreset`,默认 `
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
- **权限预设(`acp.permPreset`,默认 `workspace-write`)**:手机任务默认只能写工作区;**越界的写与提权会变成
|
|
131
|
+
审批卡推到微信**,回「批准 / 拒绝」即可(1.1.0 起)。这正是默认从 `danger-full-access` 收紧的原因——
|
|
132
|
+
手机可应答审批后,不必再用"最宽权限"来绕开"无人可答"。要更严:`read-only`;要旧行为(免审批全权,
|
|
133
|
+
适合 `mvn` 写 `~/.m2` 这类高频越界任务):`danger-full-access`。
|
|
134
134
|
- **审批默认 `ask`**:需要审批的操作会把卡片推到微信等回复;**120 秒未回按拒绝**(fail-closed),
|
|
135
135
|
也可发 `/审批 自动` 恢复"自动批准"、`/审批 拒绝` 变成一律拒绝。找不到归属联系人的审批一律拒绝。
|
|
136
136
|
- **准入默认 `strict`**(TOFU 白名单 + 一次性登记 token):未登记的发送者**不执行**,但只回一次可操作提示
|
|
@@ -166,8 +166,8 @@ ACP 的 `session/request_permission` 请求**必须被应答**。早先版本只
|
|
|
166
166
|
- 同一时刻每个人只留一张卡:新卡到达会作废旧卡(避免"批准"批到过期的那张)。
|
|
167
167
|
- 开关:`/审批` 看策略、`/审批 询问|自动|拒绝` 改;面板/HTTP 侧用 `POST /wxbridge/approval`。
|
|
168
168
|
|
|
169
|
-
>
|
|
170
|
-
>
|
|
169
|
+
> 什么时候会真的看到审批卡?**当通道权限预设被收紧时**——默认的 `workspace-write` 就意味着
|
|
170
|
+
> "工作区之外的写 / 提权"会先问你;若不想要这种打扰,把 `acp.permPreset` 改成 `danger-full-access`。
|
|
171
171
|
|
|
172
172
|
## 主动推送(1.1.0)
|
|
173
173
|
|
|
@@ -266,6 +266,17 @@ ode_modules\@zmainer\dsh-wx-bridge` → 改名成 `...dsh-wx-bridge.bak` 更稳
|
|
|
266
266
|
|
|
267
267
|
## 最近变更
|
|
268
268
|
|
|
269
|
+
- **1.1.2**:修 `wxpush.mjs` 的**默认数据目录解析**——原来只按 `$DSH_HOME/wxbridge` 兜底,桌面端
|
|
270
|
+
(Electron)里 `DSH_HOME` 常为空或指向另一个 home,于是消息被排进 `~/.dsh/wxbridge/outbox`,
|
|
271
|
+
而桥在 `%APPDATA%\dsh-desktop\harness\wxbridge` 取件 ⇒ **排了队没人发**(本机实测)。
|
|
272
|
+
现在顺序是:`--data-dir`/`WXBRIDGE_DATA`/`BRIDGE_DATA` → `WXBRIDGE_CONFIG` 里的 `dataDir` →
|
|
273
|
+
**本文件自己所在的目录**(桥每次启动都把 CLI 刷到 `<dataDir>/wxpush.mjs`,跟着文件走永远对)。
|
|
274
|
+
另加 `--print-dir` 便于排查「消息推到哪去了」。
|
|
275
|
+
- **1.1.1**:**默认权限预设从 `danger-full-access` 收紧为 `workspace-write`**——审批能在微信里答了以后,
|
|
276
|
+
"手机端无人可答审批"就不再是把通道放到最宽的理由。行为变化:工作区之外的写与提权会先在微信里弹审批卡
|
|
277
|
+
(回「批准 / 拒绝」),要旧行为就把 `acp.permPreset` 设回 `danger-full-access`,要更严设 `read-only`。
|
|
278
|
+
同时把子进程 home 的 `settings.yaml` / headless profile 补丁里写死的 `workspace-write` 接到同一个配置项,
|
|
279
|
+
并修好「对话模式」生成叠层的 `defaultPreset` 替换(此前只在模板写死旧值时才替换)。
|
|
269
280
|
- **1.1.0**:**微信内审批应答 + 主动推送**(两条都是用户要求)——
|
|
270
281
|
① **审批**:ACP 的 `session/request_permission` 不再由桥按固定策略静默回答,而是把卡片推到微信
|
|
271
282
|
(选项带编号),回「批准 / 拒绝 / 1 / 2」即映射成 ACP 的 `optionId` 回去;**超时 120 秒按拒绝**
|
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
- id: dsh-computer-use
|
|
9
9
|
disabled: true
|
|
10
10
|
|
|
11
|
-
# 通道权限预设(config.acp.permPreset,默认
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# 默认留在最宽,是因为 ~/.m2 之类的写操作会被 workspace-write 拦掉(maven 编译直接失败)。
|
|
11
|
+
# 通道权限预设(config.acp.permPreset,默认 workspace-write):
|
|
12
|
+
# 越界(工作区之外)的写与提权会把审批卡推到微信——回「批准 / 拒绝」(或 1 / 2)即可,
|
|
13
|
+
# 120 秒未回按拒绝处理(fail-closed)。要更严:read-only;要旧行为(免审批全权):danger-full-access。
|
|
15
14
|
- id: permission
|
|
16
15
|
config:
|
|
17
|
-
defaultPreset:
|
|
16
|
+
defaultPreset: workspace-write
|
|
18
17
|
- id: skill-filesystem
|
|
19
18
|
disabled: true
|
|
20
19
|
- id: tool-skill
|
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
- id: dsh-computer-use
|
|
9
9
|
disabled: true
|
|
10
10
|
|
|
11
|
-
# 通道权限预设(config.acp.permPreset,默认
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# 默认留在最宽,是因为 ~/.m2 之类的写操作会被 workspace-write 拦掉(maven 编译直接失败)。
|
|
11
|
+
# 通道权限预设(config.acp.permPreset,默认 workspace-write):
|
|
12
|
+
# 越界(工作区之外)的写与提权会把审批卡推到微信——回「批准 / 拒绝」(或 1 / 2)即可,
|
|
13
|
+
# 120 秒未回按拒绝处理(fail-closed)。要更严:read-only;要旧行为(免审批全权):danger-full-access。
|
|
15
14
|
- id: permission
|
|
16
15
|
config:
|
|
17
|
-
defaultPreset:
|
|
16
|
+
defaultPreset: workspace-write
|
|
18
17
|
#
|
|
19
18
|
# 备注(2026-09-21 实测,别重复踩):**模型目录不随行配置走**。
|
|
20
19
|
# 本文件曾试在两处放 llm-deepseek 目录(改行 config;停用 base 行 + 换行 id 插副本),
|
package/lib/kernel/bridge.mjs
CHANGED
|
@@ -236,10 +236,11 @@ const PERM_MODES = ['ask', 'allow', 'reject']
|
|
|
236
236
|
function normPermMode(v, d = 'ask') { const s = String(v ?? '').toLowerCase(); return PERM_MODES.includes(s) ? s : d }
|
|
237
237
|
let ACP_PERM = normPermMode(ACP_CFG.permPolicy, 'ask')
|
|
238
238
|
const APPROVAL_TIMEOUT_MS = Number(process.env.WXBRIDGE_APPROVAL_TIMEOUT_MS || ACP_CFG.approvalTimeoutMs || 120000)
|
|
239
|
-
// 手机通道的权限预设(ACP 叠层里 permission.defaultPreset
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
239
|
+
// 手机通道的权限预设(ACP 叠层里 permission.defaultPreset)。默认 workspace-write:
|
|
240
|
+
// 越界(工作区之外)的写与提权会变成审批卡推到微信,回「批准 / 拒绝」即可 —— 手机可应答后,
|
|
241
|
+
// 不必再用"最宽权限"绕开审批。要回到旧行为:config.json 的 acp.permPreset 设 danger-full-access;
|
|
242
|
+
// 要更严:read-only(连写工作区都要批,maven 写 ~/.m2 也会拦)。
|
|
243
|
+
const ACP_PERM_PRESET = String(ACP_CFG.permPreset || 'workspace-write')
|
|
243
244
|
// 主动推送:外部往 outbox 丢 JSON,桥轮询代发(见 outbox.mjs / wxpush.mjs)
|
|
244
245
|
const OUTBOX_DIR = join(STATE_DIR, 'outbox')
|
|
245
246
|
const OUTBOX_SENT_DIR = join(OUTBOX_DIR, 'sent')
|
|
@@ -407,7 +408,7 @@ function prepareChildHome() {
|
|
|
407
408
|
writeFileSync(join(prof, 'cordis.yml'), '# dsh profile root (managed by wxbridge)' + NL + '[]' + NL)
|
|
408
409
|
writeFileSync(join(prof, 'cordis.patch.yml'),
|
|
409
410
|
'# wxbridge: keep headless permissions narrow without touching the host settings.yaml' + NL
|
|
410
|
-
+ '- id: permission' + NL + ' config:' + NL + ' defaultPreset:
|
|
411
|
+
+ '- id: permission' + NL + ' config:' + NL + ' defaultPreset: ' + ACP_PERM_PRESET + NL)
|
|
411
412
|
return { refs: Object.keys(refs), home: HOST_HOME, shared: true }
|
|
412
413
|
}
|
|
413
414
|
|
|
@@ -415,7 +416,7 @@ function prepareChildHome() {
|
|
|
415
416
|
writeFileSync(target, ['version: 1', 'refs:'].concat(Object.keys(refs).map((k) => ' ' + k + ': ' + refs[k])).join(NL) + NL)
|
|
416
417
|
// 默认预设**不写死**:预设由用户自己的 DSH 决定(ACP 会话本就不加入预设;写死某个人的预设
|
|
417
418
|
// 等于把插件产品绑到某个人的环境上)。
|
|
418
|
-
writeFileSync(join(CHILD_HOME, 'settings.yaml'), 'permission:' + NL + ' defaultPreset:
|
|
419
|
+
writeFileSync(join(CHILD_HOME, 'settings.yaml'), 'permission:' + NL + ' defaultPreset: ' + ACP_PERM_PRESET + NL)
|
|
419
420
|
try { spawn('cmd.exe', ['/c', 'icacls "' + target + '" /inheritance:r /grant:r "' + process.env.USERNAME + ':R"'], { windowsHide: true, stdio: 'ignore' }).unref() } catch {}
|
|
420
421
|
return { refs: Object.keys(refs), home: CHILD_HOME, shared: false }
|
|
421
422
|
}
|
|
@@ -871,7 +872,7 @@ function ensureChatOverlay() {
|
|
|
871
872
|
const lines = text.split(NL)
|
|
872
873
|
while (lines.length && !String(lines[lines.length - 1]).trim()) lines.pop()
|
|
873
874
|
const indent = (String(lines[lines.length - 1] || '').match(/^\s*/) || [''])[0]
|
|
874
|
-
const body = lines.map((l) => (/^(\s*
|
|
875
|
+
const body = lines.map((l) => (/^(\s*defaultPreset:\s*)\S+\s*$/.test(l) ? l.replace(/^(\s*defaultPreset:\s*)\S+\s*$/, '$1' + ACP_PERM_PRESET) : l))
|
|
875
876
|
const out = body.concat(pushHintLines().map((h) => indent + h)).join(NL) + NL
|
|
876
877
|
try {
|
|
877
878
|
const f = join(STATE_DIR, 'acp-overlay-chat.gen.yml')
|
package/lib/kernel/wxpush.mjs
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* 它只做一件事:把一条 JSON 丢进 <dataDir>/outbox/,桥轮询时替它发出去。
|
|
12
12
|
* 零依赖、零网络:没有微信凭据 / 桥没在跑时也只是排队,不会报错给你。
|
|
13
13
|
*/
|
|
14
|
-
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
15
|
-
import { join, resolve } from 'node:path'
|
|
14
|
+
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs'
|
|
15
|
+
import { join, resolve, dirname } from 'node:path'
|
|
16
16
|
import { homedir } from 'node:os'
|
|
17
17
|
import { randomUUID } from 'node:crypto'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
18
19
|
|
|
19
20
|
const args = process.argv.slice(2)
|
|
20
21
|
const argOf = (name, def = '') => { const i = args.indexOf(name); return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : def }
|
|
@@ -37,14 +38,39 @@ async function readStdin() {
|
|
|
37
38
|
return out.trim()
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* 数据目录解析(顺序很重要,2026-09-24 实测踩过):
|
|
43
|
+
* ① --data-dir / WXBRIDGE_DATA / BRIDGE_DATA —— 显式指定,永远最大
|
|
44
|
+
* ② WXBRIDGE_CONFIG 指向的配置里的 dataDir —— 同样的显式配置链
|
|
45
|
+
* ③ **本文件自己所在的目录** —— 桥每次启动都会把本文件刷到 <dataDir>/wxpush.mjs 一份,
|
|
46
|
+
* 所以"跟着这个文件走"永远等于桥真正在用的目录,不依赖任何环境变量
|
|
47
|
+
* 刻意**不读** $DSH_HOME/wxbridge/config.json 之类:桌面端(Electron)里 DSH_HOME 常为空或指向
|
|
48
|
+
* 另一个 home,读到的可能是旧目录里的旧 dataDir;实测症状就是"推送排了队,桥那头没人取件"。
|
|
49
|
+
*/
|
|
50
|
+
const SELF_DIR = dirname(fileURLToPath(import.meta.url))
|
|
51
|
+
|
|
52
|
+
function explicitConfigDataDir() {
|
|
53
|
+
const f = process.env.WXBRIDGE_CONFIG
|
|
54
|
+
if (!f) return ''
|
|
55
|
+
try {
|
|
56
|
+
const d = JSON.parse(readFileSync(f, 'utf8')).dataDir
|
|
57
|
+
return d ? resolve(String(d)) : ''
|
|
58
|
+
} catch { return '' }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const dataDir = resolve(argOf('--data-dir') || process.env.WXBRIDGE_DATA || process.env.BRIDGE_DATA
|
|
62
|
+
|| explicitConfigDataDir() || SELF_DIR)
|
|
63
|
+
|
|
40
64
|
const text = (argOf('--text') || (args.includes('--stdin') ? await readStdin() : '') || positionalText()).trim()
|
|
65
|
+
if (args.includes('--print-dir')) {
|
|
66
|
+
console.log('[wxpush] dataDir=' + dataDir + (text ? '' : '(未给内容,仅打印目录)'))
|
|
67
|
+
process.exit(0)
|
|
68
|
+
}
|
|
41
69
|
if (!text) {
|
|
42
70
|
console.error('[wxpush] 没有内容:用法 node wxpush.mjs "要推送的话"(或 --text / --stdin)')
|
|
43
71
|
process.exit(2)
|
|
44
72
|
}
|
|
45
73
|
|
|
46
|
-
const dataDir = resolve(argOf('--data-dir') || process.env.WXBRIDGE_DATA || process.env.BRIDGE_DATA
|
|
47
|
-
|| join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'wxbridge'))
|
|
48
74
|
const outbox = join(dataDir, 'outbox')
|
|
49
75
|
const entry = {
|
|
50
76
|
text: text.slice(0, 4000),
|