@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 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": "danger-full-access",
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 叠层):`danger-full-access` / `workspace-write` / `read-only`。收紧后产生的审批会在微信里问你 | danger-full-access |
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`,默认 `danger-full-access`)**:默认仍是最宽——手机任务常要跑 `mvn`、写
131
- `~/.m2`、动工作区之外的文件,收紧到 `workspace-write` 会直接把这些操作拦掉。想要更安全就把 `acp.permPreset`
132
- 改成 `workspace-write` / `read-only`:**1.1.0 起审批会在微信里问你**(回「批准 / 拒绝」),不再"无应答直接失败",
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
- > 什么时候会真的看到审批卡?**当通道权限预设被收紧时**(`acp.permPreset=workspace-write`/`read-only`)。
170
- > 默认的 `danger-full-access` 不产生审批——想用这个能力,就把它改窄。
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,默认 danger-full-access):
12
- # 收紧到 workspace-write / read-only 后,需要审批的操作会把审批卡推到微信——
13
- # 回「批准 / 拒绝」(或 1 / 2)即可,120 秒未回按拒绝处理(fail-closed)。
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: danger-full-access
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,默认 danger-full-access):
12
- # 收紧到 workspace-write / read-only 后,需要审批的操作会把审批卡推到微信——
13
- # 回「批准 / 拒绝」(或 1 / 2)即可,120 秒未回按拒绝处理(fail-closed)。
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: danger-full-access
16
+ defaultPreset: workspace-write
18
17
  #
19
18
  # 备注(2026-09-21 实测,别重复踩):**模型目录不随行配置走**。
20
19
  # 本文件曾试在两处放 llm-deepseek 目录(改行 config;停用 base 行 + 换行 id 插副本),
@@ -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)。默认仍是最宽的 danger-full-access:
240
- // 收紧到 workspace-write 会让 ~/.m2 之类的写操作被拦(maven 编译直接失败),所以交用户显式选择。
241
- // 但**审批现在是可应答的**:一旦收紧,审批卡会推到微信,回「批准 / 拒绝」即可。
242
- const ACP_PERM_PRESET = String(ACP_CFG.permPreset || 'danger-full-access')
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: workspace-write' + NL)
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: workspace-write' + NL)
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*)defaultPreset:\s*danger-full-access\s*$/.test(l) ? l.replace('danger-full-access', ACP_PERM_PRESET) : l))
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')
@@ -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),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zmainer/dsh-wx-bridge",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "type": "module",
5
5
  "main": "./lib/index.js",
6
6
  "dsh": {