@wenbin_wb/dsh-bridge 2.10.4 → 2.10.6
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 +26 -0
- package/README.en.md +410 -410
- package/README.md +432 -432
- package/client/client.js +368 -66
- package/client/index.js +320 -66
- package/docs/CODE_REVIEW.md +220 -0
- package/docs/telegram-usage.md +1 -1
- package/lib/auth/manager.js +5 -4
- package/lib/bridge-rpc.js +19 -0
- package/lib/cloudflared-manager.mjs +513 -361
- package/lib/compat.js +129 -129
- package/lib/feishu/index.js +225 -225
- package/lib/feishu/node.js +439 -439
- package/lib/index.js +43 -8
- package/lib/platform/base.js +147 -147
- package/lib/platform/commands.js +221 -221
- package/lib/platform/conversation-bridge.js +821 -821
- package/lib/platform/dsh-storage.js +117 -117
- package/lib/platform/index.js +10 -10
- package/lib/platform/message-split.js +229 -229
- package/lib/platform/session-catalog.js +372 -372
- package/lib/platform/stream-slices.js +21 -21
- package/lib/qq/index.js +312 -312
- package/lib/telegram/index.js +216 -216
- package/lib/telegram/node.js +322 -322
- package/lib/wechat/gateway.js +986 -986
- package/lib/wechat/index.js +244 -244
- package/lib/wechat/media.js +285 -285
- package/lib/wechat/node.js +352 -352
- package/package.json +2 -2
- package/docs/fix-plan-202608.md +0 -108
- package/docs/fix-tunnel-sse-and-session-list.md +0 -134
- package/docs/platform-abstraction-design.md +0 -473
- package/docs/wechat-bot-plan.md +0 -294
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# dsh-bridge 代码审查报告(含误报复审)
|
|
2
|
+
|
|
3
|
+
> 审查人:首席架构师视角(重度代码洁癖模式)
|
|
4
|
+
> 范围:`lib/`、`client/`、`test/`、构建/发布脚本与配置
|
|
5
|
+
> 性质:静态全量通读 + 关键路径推理;标注「需实测」的项请发布前验证
|
|
6
|
+
> 版本历史:v2 在 v1 基础上增加「误报复审」——对初版每条结论回源码复核,剔除误报、修正方向性错误、降级不必要事项,并新增复核中发现的真问题。
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 0. 总览
|
|
11
|
+
|
|
12
|
+
项目工程质量高(编码规范、CI ubuntu+windows×node22/24、覆盖广的单测、凭据分离、双语文档)。按严重程度分级:P0 安全、P1 功能 BUG、P2 质量、P3 风格。
|
|
13
|
+
|
|
14
|
+
**初版(v1)共 5 P0 + 9 P1 + 12 P2 + 4 架构项;经误报复审后修正为 3 P0 + 6 P1 + 11 P2 + 4 架构项**,其中:
|
|
15
|
+
- 剔除误报 3 项(v1 P1-5 Telegram 按钮审批、v1 P1-8 客户端 VersionBanner 轮询、v1 P2-8 upgradePlugin 分级)
|
|
16
|
+
- 修正方向 1 项(v1 P1-6 飞书群聊按钮:不是"他人可代批",而是"发起者本人被误拦")
|
|
17
|
+
- 降级 3 项(v1 P0-D hop-by-hop 降 P1;v1 P1-7 getDshVersion 反复 spawn 实为误读、降 P2;v1 P1-8 服务端 checkVersion 无 TTL 部分降 P2)
|
|
18
|
+
- 补实锤并重新定性 1 项(P0-C 隧道浏览器 HTTP/WS 无自身认证,依赖本地认证兜底)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. P0 安全缺陷(复核后 3 项)
|
|
23
|
+
|
|
24
|
+
### P0-A. `AuthManager.verifyRequest` 的 loopback 免认证对「同宿主任意进程」过度信任
|
|
25
|
+
|
|
26
|
+
**位置**:`lib/auth/manager.js:479-494`
|
|
27
|
+
|
|
28
|
+
**现状**:`allowLoopback && isLoopback && !isPublicTunnel` 分支只检查 `host.startsWith('127.0.0.1') || 'localhost' || ''`。
|
|
29
|
+
|
|
30
|
+
**威胁**:`allowLoopback` 默认 true。若 DSH 启用访问认证(`enabled=true`)但尚未设密码/Token,**任何能连到 3082 端口的同宿主进程**(浏览器扩展、恶意软件、同机容器若端口可达)设 `Host: 127.0.0.1:3082` 即以回环身份免认证获得完整访问权,还能调 `/__dsh_bridge__/loopback-token` 直接领 adminToken。
|
|
31
|
+
|
|
32
|
+
**复核结论**:成立。TCP 回环只能证明"来自本机",不能证明"来自本机浏览器/用户";HTTP 层无更强手段。产品若坚持"本机免密",至少应在 UI/文档显著提示;更稳妥是未配置任何凭据时不给 loopback 特殊放行。
|
|
33
|
+
|
|
34
|
+
**对应**:R1。
|
|
35
|
+
|
|
36
|
+
### P0-B. 启用认证但未设密码时,登录等于空密码放行
|
|
37
|
+
|
|
38
|
+
**位置**:`lib/auth/manager.js:367-373`(`verifyPassword` 在 `!hasPassword && mode!=='password_only'` 时返回 success:true)+ 登录 API。
|
|
39
|
+
|
|
40
|
+
**现状**:远程访客只要知道面板地址,POST `/__dsh_bridge__/login` 空密码即签发会话。这是产品"先开放后设密码"设计,但叠加 P0-A/公网隧道时等于无认证。
|
|
41
|
+
|
|
42
|
+
**复核结论**:成立(产品决策需确认),与 P0-A 合并建议。
|
|
43
|
+
|
|
44
|
+
**对应**:并入 R1 讨论。
|
|
45
|
+
|
|
46
|
+
### P0-C. 自建隧道服务端(HTTP 与 WS)均无自身认证,仅依赖本地认证兜底
|
|
47
|
+
|
|
48
|
+
**位置**:`scripts/install-tunnel-server.sh`(内嵌 server.mjs)`:221-302`。
|
|
49
|
+
|
|
50
|
+
**现状**:
|
|
51
|
+
- `/connect`(tunnel client 控制通道):`:306` 校验 `?token=`,**有鉴权** ✓
|
|
52
|
+
- **浏览器 HTTP 转发**(`:221-257`)与 **浏览器 WebSocket 代理**(`:267-302`):只要存在任一在线 tunnel client,**任何公网访客无需 token** 即可把 HTTP/WS 请求转发到本地 DSH(WS 只靠随机 wsId + 10s 超时,HTTP 靠 PATH_PREFIX 隐藏路径)。
|
|
53
|
+
|
|
54
|
+
**威胁评估(关键)**:隧道转发的流量在本地 ProxyServer 会被识别为隧道流量(`x-dsh-internal-tunnel` 头,不享受 loopback 免认证),**真正认证防线在本地 `AuthManager`**。因此:
|
|
55
|
+
- 若用户**启用了访问认证并设密码/Token** → 匿名隧道流量在本地被 401 拦截,P0-C 影响有限(仅暴露登录页);
|
|
56
|
+
- 若用户**未启用认证 / 未设密码**(配合 P0-B)→ 公网任意访客可直达完整 DSH。这与 P0-A/P0-B 同源,属「纵深防御缺口」而非独立可利用漏洞。
|
|
57
|
+
|
|
58
|
+
**复核结论**:成立,定性为**纵深防御缺陷(P0/P1 取决于本地认证配置)**——隧道公网入口无自身认证,把安全完全押注在本地认证开关上;建议服务端加独立访问凭据(或至少 WS 升级校验)。同时主页 `forwardRequest` 转发时透传了浏览器原始 Cookie/头到本地,若本地认证关闭则风险如上。
|
|
59
|
+
|
|
60
|
+
**对应**:R2(服务端加 token/会话校验,禁止匿名浏览器直连;与本地认证形成双保险)。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 2. P1 功能 BUG / 明确缺陷(复核后 6 项)
|
|
65
|
+
|
|
66
|
+
### P1-1. 本地 `ProxyServer` 非缓冲转发与 WS 升级未净化 hop-by-hop 头(由 v1 P0-D 降级)
|
|
67
|
+
|
|
68
|
+
**位置**:`lib/index.js:468`(非缓冲直通 `res.writeHead(status, proxyRes.headers)`)、`:496-512`(WS upgrade 手拼响应头)。
|
|
69
|
+
|
|
70
|
+
**现状**:HTML 缓冲路径已删 `content-length/transfer-encoding`(`:459-461`),但非缓冲直通与 WS 101 分支把上游头(含 `connection/keep-alive/transfer-encoding` 等 hop-by-hop)原样透传。多数客户端能容忍,但严格客户端/中间层可能解析异常;WS 101 分支还允许上游注入任意响应头。
|
|
71
|
+
|
|
72
|
+
**复核结论**:成立,属健壮性/协议合规缺陷(非主动利用漏洞,风险中等)。**降为 P1**。
|
|
73
|
+
|
|
74
|
+
**对应**:R3。
|
|
75
|
+
|
|
76
|
+
### P1-2. QQ 流式失败回退在无 `stream_msg_id` 时可能重复发整条消息
|
|
77
|
+
|
|
78
|
+
**位置**:`lib/qq/node.js:296-314`。
|
|
79
|
+
|
|
80
|
+
**现状**:流式中途失败 → catch 用 `streamMsgId`(首片失败时为 null)+ `index: slices.length` + `inputState: 10` 补发完整 md;`streamMsgId` 为 null 时这是"全新流式消息",随后若再走 `_sendMarkdown` 兜底则重复。
|
|
81
|
+
|
|
82
|
+
**复核结论**:成立。首片失败场景 `streamMsgId=null`,补发其实开启了一条**新的** replace 流,随后 catch 内 `_sendMarkdown` 再发一条 → 用户可能收到 2 条完整内容。建议:`streamMsgId` 为空时直接 `_sendMarkdown`,不发 stream 补发。
|
|
83
|
+
|
|
84
|
+
**对应**:R4。
|
|
85
|
+
|
|
86
|
+
### P1-3. QQ 引用回复自动开新会话的固定 100ms 等待是竞态
|
|
87
|
+
|
|
88
|
+
**位置**:`lib/qq/node.js:410-417`。
|
|
89
|
+
|
|
90
|
+
**现状**:无活动会话且用户引用机器人消息时,先 `handleInbound('/new')` 再 `sleep(100ms)` 再处理正文。`/new` 创建会话是异步重活(等 DSH 建 agent),100ms 未必够 → 竞态下正文可能仍落在"无活动会话"路径。
|
|
91
|
+
|
|
92
|
+
**复核结论**:成立但影响较轻(最坏情况用户看到提示语而不是对话)。因 `handleInbound` 自带串行队列,直接 `await` `/new` 完成即可,去掉固定 sleep。
|
|
93
|
+
|
|
94
|
+
**对应**:R5。
|
|
95
|
+
|
|
96
|
+
### P1-4. `[SEND_FILE]` 可外发任意本地绝对路径文件
|
|
97
|
+
|
|
98
|
+
**位置**:`lib/platform/message-split.js:134-150` + `conversation-bridge.js` 自动发送。
|
|
99
|
+
|
|
100
|
+
**现状**:模型输出中 `[SEND_FILE: <绝对路径>]` 只要 statSync 是文件即发送给 IM。模型输出不可信(提示注入/越狱可能),可借此把 `.ssh`、`.credentials`、`.env` 等发给白名单聊天。
|
|
101
|
+
|
|
102
|
+
**复核结论**:成立。当前缓解仅"指令必须显式出现",但模型可被诱导输出。建议发送前校验目标在会话 cwd / 已注册工作区范围内,并拦截敏感路径(可复用 `security/path-validator.js`)。
|
|
103
|
+
|
|
104
|
+
**对应**:R6。
|
|
105
|
+
|
|
106
|
+
### P1-5. 微信 gateway `stop()`/`restart()` 轮询竞态
|
|
107
|
+
|
|
108
|
+
**位置**:`lib/wechat/gateway.js:389-418`、`769-791`。
|
|
109
|
+
|
|
110
|
+
**现状**:`stop()` 只置 `stopPollingLocal=true`,不中断 in-flight `getUpdates`(最长 35s);`restart()` `await previous` 后启动新循环,但旧任务真正退出前新循环可能已开始(软停止),且 `setCredentials()` 触发 restart 时若旧轮询未退出 → 新旧 poller 并存 → iLink 单 token 单 poller 可能 403。
|
|
111
|
+
|
|
112
|
+
**复核结论**:成立。`await previous` 确实等待旧循环退出(`previous` 是 `runPollLoop()` promise,`stopPollingLocal` 置位后旧循环在本次迭代结束时退出),所以**旧 poller 并存窗口有限**;但 35s 长轮询 in-flight 期间 restart 会等满整个长轮询才切换,用户体验与"快速换 token"场景确有竞态窗口。属真实健壮性缺陷(P1 偏低 / P2 偏高),建议给长轮询加 AbortController。
|
|
113
|
+
|
|
114
|
+
**对应**:R7。
|
|
115
|
+
|
|
116
|
+
### P1-6. 飞书群聊卡片按钮:**发起者本人会被误拦**(复核修正方向)
|
|
117
|
+
|
|
118
|
+
**位置**:`lib/feishu/node.js:176-194`(`_handleAction`)+ `_handleInbound`(`:141` `senderId: isGroup ? peerId : senderId`)+ 审批桥 `initiator = turn.senderId`(`:274`)。
|
|
119
|
+
|
|
120
|
+
**现状**:
|
|
121
|
+
- 群聊中发起审批:`pending.peerId` = 群 peerId(`oc_...`,因为 `turn.senderId` 在群聊被设成了群 peerId);
|
|
122
|
+
- 群成员点击卡片按钮:`operatorId` = 成员 open_id(`ou_...`);
|
|
123
|
+
- `_handleAction` 比对 `pending.peerId !== operatorId` → **恒不等 → 群聊按钮审批永远被拦**(`return` 无任何提示),群内只能走文本 `/yes`。
|
|
124
|
+
|
|
125
|
+
**复核结论**:成立且**方向与 v1 报告相反**——不是"他人可代批",而是"发起者本人按钮不可用"。单聊正常(operatorId=发起者 open_id)。若产品把"群"整体作为授权主体(首个 @ 即授权整群,成员都可发消息使唤 agent),则"群成员按钮可决议"本身不算越权;问题只是**群聊按钮路径被误杀**。
|
|
126
|
+
|
|
127
|
+
**对应**:R8(群聊按钮决议的 peer 口径:要么允许群内成员按钮决议,要么明确只允许发起成员、需记录成员级发起者并实测)。
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 3. P2 代码质量 / 健壮性(复核后 11 项)
|
|
132
|
+
|
|
133
|
+
1. **cloudflared 启停状态机**(`cloudflared-manager`/`lib/index.js`):`startCloudflared` 非阻塞、90s 超时路径 `stop()` 后 reject、`.tmp` 残留(已有清理)。建议补状态机单测。
|
|
134
|
+
2. **tunnel-client SSE 截断**(`lib/tunnel-client.mjs:185`):收到第 2 个 chunk 即截断并 destroy 上游;500ms 定时器也截断。SSE 聊天流用户看到的是被腰斩的流。建议升级协议支持流式或文档明示"SSE 仅首屏"。
|
|
135
|
+
3. **微信 context token 明文落盘**(`wechat/gateway.js`):防抖合并写但 sync fs + 明文无权限收紧。建议文件权限 600 / 明确风险。
|
|
136
|
+
4. **`createConnectProxyAgent` 只支持 HTTP CONNECT**:`https://` 代理协议未特判,文档应说明。
|
|
137
|
+
5. **client 命令式 DOM 侵入 + 依赖宿主 class 名**(`client/index.js` 移动增强):对 DSH 版本升级脆弱,MutationObserver 常驻。建议收敛为官方 slot/注入点并加特性开关。
|
|
138
|
+
6. **`session-strip` 对 gzip 压缩响应不生效**:只在未压缩时剥离;压缩响应直接透传大字段。建议解压→剥离→重压缩,或上游请求 `accept-encoding: identity`。
|
|
139
|
+
7. **`checkVersion`/VersionBanner 无服务端 TTL 缓存**(`lib/index.js:931` + `client/index.js:2191`):**复核修正**:客户端 VersionBanner 仅在 mount 时 `check()` 一次(无 3s 轮询),因此"客户端 3s 轮询打 registry"**不成立**;剩余问题仅是服务端每次调用都发外网请求、无 TTL 缓存。降级为 P2 低优。
|
|
140
|
+
8. **`getDshVersion` 失败后不再重试**(`lib/index.js:598-643`,由 v1 P1-7 降级):函数入口即置 `_dshVersionLoaded=true`,失败后进程生命周期内不再探测(用户修复 PATH 需重启)。**v1 称"反复 spawn"是误读**,实际只探测一次;可议点仅为无重试。建议长退避(如 10 分钟后再探测)。
|
|
141
|
+
9. **`restartDsh` 直接 `process.exit`**:依赖外部守护,文档需明确使用前提。(保留)
|
|
142
|
+
10. **重复代码 / 命名不一致**:`getDshVersion` 与 `upgradePlugin` PATH 拼接重复;四处审批卡片渲染各写一遍(Telegram 有死代码 `sendApprovalCard`,见误报复审);平台 capability 键不一致(`supportsGroup` vs `group`)。建议统一。
|
|
143
|
+
11. **登录页/注入页面无 CSP**(P3 建议);大量空 `catch {}` 吞异常,建议至少 `logger.debug`。
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 4. 架构层面观察(复核后 4 项,均成立)
|
|
148
|
+
|
|
149
|
+
1. **context 注入方式不一致**:`ctx.qq/telegram/feishu = ...` 直接属性赋值(部分 try/catch);测试用 `reflect.provide`。cordis 对未注入属性读取会抛错,建议统一 `reflect.provide`/服务注册。
|
|
150
|
+
2. **平台 capability 字段命名不一致**:`supportsGroup` vs `group`、`supportsMedia` vs `media`,消费方需各自适配;`status` getter 覆盖方式不一。建议统一 schema + JSDoc。
|
|
151
|
+
3. **错误处理粗糙**:大量空 catch;RPC 层统一 `fail('bad-request')` 无法区分参数错与内部错误。建议细分错误码。
|
|
152
|
+
4. **测试覆盖缺口**:`security-vulnerabilities.test.mjs` P0-7 是半断言(未真正调 verifyRequest/签发端点);P0-6 未单独覆盖伪造 `x-dsh-internal-tunnel` 需 secret 路径(verifyRequest 用 secret 比较,伪造不可行,测试隐式覆盖)。建议补 P0-A/P0-C/R6 的回归测试。
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 5. 误报复审结论(v1 → v2 变更明细)
|
|
157
|
+
|
|
158
|
+
| 原编号 | 原结论 | 复审结论 | 处置 |
|
|
159
|
+
|---|---|---|---|
|
|
160
|
+
| P0-C | 隧道服务端鉴权"待补审" | 控制通道有 token 鉴权;但浏览器 HTTP/WS 转发路径**无自身认证**,仅靠本地 ProxyServer 认证兜底(纵深防御缺口) | 改写为 P0-C 实锤(定性随本地认证配置) |
|
|
161
|
+
| P1-5 | Telegram 按钮审批不校验发起者 | **误报**:Telegram 未重写审批桥,按钮分支是**死代码**(`sendApprovalCard` 无调用点),真实审批走基类文本卡 + `/yes` | 剔除,移入 P2 死代码清理建议 |
|
|
162
|
+
| P1-6 | 飞书群聊"他人可代批" | 方向反了:实际是**群聊发起者本人按钮被误拦**(peerId=群 vs operatorId=成员) | 改写为 P1-5(修正方向) |
|
|
163
|
+
| P1-7(v1) | getDshVersion 失败后反复 spawn | **误报**:函数入口已置 `_dshVersionLoaded=true`,只会尝试一次 | 降级为 P2 建议 |
|
|
164
|
+
| P1-8(v1) | VersionBanner 3s 轮询打 registry | **误报**:VersionBanner 仅 mount 时 check 一次,无轮询 | 剔除客户端轮询部分,保留服务端无 TTL 缓存(降 P2-7) |
|
|
165
|
+
| P2-8(v1) | upgradePlugin 分级提示 | 保留但影响轻微(`dsh` 失败转 npx/npm 是合理降级,误报"已是最新"概率低) | 降级合并至 P2-9 |
|
|
166
|
+
|
|
167
|
+
**复核中确认误报的共性根因**:v1 部分条目凭函数印象/推断而非逐行核对调用链,导致对"可达性"与"触发频率"高估。v2 已逐条回源码验证调用链与守卫条件。
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 6. 修复清单与实施状态(按复核后优先级)
|
|
172
|
+
|
|
173
|
+
> 已按用户确认范围实施全部 P1 与低成本 P2;R1/R2 按用户选择「维持现状 + 仅文档告警」未改服务端逻辑,改为 UI 高危告警与 README 双语安全须知。
|
|
174
|
+
|
|
175
|
+
| 编号 | 文件 | 修复内容 | 优先级 | 状态 |
|
|
176
|
+
|---|---|---|---|---|
|
|
177
|
+
| R1 | `lib/auth/manager.js` | loopback 免认证收紧(未设凭据时不无条件放行本机进程) | P0 | **按用户决策未改**:UI 增加「已开启认证但未设任何密码」高危告警(client/index.js);README 双语安全须知 |
|
|
178
|
+
| R2 | `scripts/install-tunnel-server.sh` | 隧道入口加独立认证 | P0 | **按用户决策未改**:README 双语明确「隧道转发无独立认证、依赖本地访问认证」安全须知 |
|
|
179
|
+
| R3 | `lib/index.js` | 代理响应净化 hop-by-hop 头(直通 + WS 101 白名单 + 非 101 净化) | P1 | ✅ 已实施 |
|
|
180
|
+
| R4 | `lib/qq/node.js` | 流式失败回退:`streamMsgId` 为空时直接降级单条 Markdown | P1 | ✅ 已实施 |
|
|
181
|
+
| R5 | `lib/qq/node.js` | 引用回复去掉固定 100ms,await `/new` 完成 | P1 | ✅ 已实施 |
|
|
182
|
+
| R6 | `lib/platform/message-split.js` + `conversation-bridge.js` | SEND_FILE 发送前校验:必须位于会话 cwd 内 + 敏感路径黑名单(.ssh/.credentials/.env 等) | P1 | ✅ 已实施(`isPathAllowedForSend`) |
|
|
183
|
+
| R7 | `lib/wechat/gateway.js` | stop/restart 竞态:长轮询加 AbortController 中断 in-flight | P1 | ✅ 已实施 |
|
|
184
|
+
| R8 | `lib/feishu/node.js` | 群聊卡片按钮决议口径:群整体授权(群内成员可按钮决议),单聊仍校验发起者 | P1 | ✅ 已实施(按用户选择「群内成员可按钮决议」) |
|
|
185
|
+
| R9 | `lib/telegram/node.js` | 死代码清理:删除无调用点的 `sendApprovalCard` | P2 | ✅ 已实施 |
|
|
186
|
+
| R10 | `lib/index.js` | `getDshVersion` 失败长退避重试(10 分钟后再探测) | P2 | ✅ 已实施 |
|
|
187
|
+
| R11 | `lib/index.js` | `checkVersion` 服务端结果 TTL 缓存(10 分钟) | P2 | ✅ 已实施 |
|
|
188
|
+
|
|
189
|
+
### 实施验证结果
|
|
190
|
+
|
|
191
|
+
- ✅ 全量单测:**169 passed / 0 failed**(`npm test`)
|
|
192
|
+
- ✅ lint:**0 errors**(28 条既存 warning,均为历史代码 no-unused-vars 降级,非本次引入)
|
|
193
|
+
- ✅ 构建:`npm run build:client` 成功产出 `client/client.js`
|
|
194
|
+
- ✅ 语法检查:全部改动文件 `node --check` 通过
|
|
195
|
+
|
|
196
|
+
### 仍需真机验证(不在本次单测覆盖内)
|
|
197
|
+
|
|
198
|
+
- 飞书群聊卡片按钮:修复后群内任意成员可按钮决议(与 QQ 群模型一致),需真机验证按钮事件链路;
|
|
199
|
+
- 微信 stop/restart 竞态:AbortController 中断长轮询的行为需在真实 iLink 轮询下确认;
|
|
200
|
+
- QQ 流式失败回退 / 引用回复:需真实 QQ Bot 环境验证。
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 8. Issue #28 修复记录(DSH 原生端口 3080 直连被误判远程)
|
|
206
|
+
|
|
207
|
+
**issue**:[#28](https://github.com/wenbin-wb/dsh-bridge/issues/28) — dsh 0.1.2-alpha.5 下 127.0.0.1:3080 被判定为远程 + 本机文件夹选择走远程抽屉(0 回复,经代码核查确认为真问题)。
|
|
208
|
+
|
|
209
|
+
**根因(现象 1,已修复)**:
|
|
210
|
+
- `/__dsh_bridge__/loopback-token`(本机领 adminToken 端点)只注册在代理端口 3082(lib/index.js ProxyServer),DSH 原生端口 3080 无此端点;
|
|
211
|
+
- 3080 页面相对路径 fetch 404 后,兜底跨域请求 3082 时,Origin `http://127.0.0.1:3080` 不在 CORS 白名单 → 被浏览器拦截 → 拿不到 adminToken;
|
|
212
|
+
- `adminPolicy=local_only` 下 `checkAdminAuth` 要求有效 adminToken → 面板锁定。
|
|
213
|
+
|
|
214
|
+
**修复**:`BridgeService.startProxy()` 的 `allowedOrigins` 加入 DSH 原生端口 origin(`http://127.0.0.1:<dshPort>` / `http://localhost:<dshPort>`),使直连原生端口时页面可跨域回读代理端点的 loopback-token。CORS 仍收敛(仅回显白名单 origin,非白名单不回显 ACAO 头,浏览器跨域仍被拦)。
|
|
215
|
+
|
|
216
|
+
**回归测试**:`test/proxy-auth.test.mjs` 新增「issue #28 regression」用例——白名单 dshPort origin 回环 POST 得 200 + ACAO 回显 + 有效 adminToken;非白名单 origin 不回显 ACAO。
|
|
217
|
+
|
|
218
|
+
**现象 2(文件夹选择走远程抽屉,未修)**:纯 `dsh web` 模式无原生 pickDirectory(无 Electron/原生宿主桥),`pick()` 抛 "needs the native capability" 后刻意降级远程抽屉。是否可修取决于 DSH 0.1.2 是否提供替代目录选择服务,需真机/DSH 侧确认。
|
|
219
|
+
|
|
220
|
+
> 本报告为 CODE_REVIEW 文档,不随 npm 包发布。修复均未 bump 版本号;如发布请自行决定版本与 CHANGELOG。
|
package/docs/telegram-usage.md
CHANGED
|
@@ -90,4 +90,4 @@
|
|
|
90
90
|
2. **安全白名单拦截**:
|
|
91
91
|
- 仅白名单内的用户消息会被放行给 Agent;非白名单用户发送的消息会被直接忽略,绝不消耗 Token 或喂给模型。
|
|
92
92
|
3. **敏感操作审批**:
|
|
93
|
-
- 当 Agent 尝试执行系统命令或敏感文件读写时,Telegram 会自动下发 `[✓ 批准执行]` / `[✕ 拒绝执行]` 交互按键,10 分钟未处理自动超时拒绝。
|
|
93
|
+
- 当 Agent 尝试执行系统命令或敏感文件读写时,Telegram 会自动下发 `[✓ 批准执行]` / `[✕ 拒绝执行]` 交互按键,10 分钟未处理自动超时拒绝。
|
package/lib/auth/manager.js
CHANGED
|
@@ -365,10 +365,11 @@ export class AuthManager {
|
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
if (!this.hasPassword) {
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
368
|
+
// 管理员尚未设置任何访问密码:无哈希可校验,放行(与 token_and_password 一致)。
|
|
369
|
+
// 历史实现曾在此拒绝 password_only 模式的空白登录,但那会形成死锁:
|
|
370
|
+
// 无密码 → 登录墙永远 401 → 进不去面板 → 看不到设密码表单 → 永远无法设密码。
|
|
371
|
+
// 未设密码 = 没有受保护秘密,应先让管理员进入完成密码初始化;
|
|
372
|
+
// 设置密码后,墙才会真正生效(此时任意/错误密码均被拒绝)。
|
|
372
373
|
return { success: true }
|
|
373
374
|
}
|
|
374
375
|
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -119,6 +119,25 @@ export function installBridgeRpc(ctx, { service, authManager, platformManager, l
|
|
|
119
119
|
const adminErr = checkAdminAuth(authManager, payload);
|
|
120
120
|
if (adminErr) return adminErr;
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
// 防自我锁死守卫(v2.10.5):
|
|
124
|
+
// password_only(仅密码)模式下若从未设置任何密码,开启防护或维持该模式会把
|
|
125
|
+
// 管理员锁在登录墙外(无哈希可校验、密码登录被拒 → 进不去面板设密码 → 死锁)。
|
|
126
|
+
// 因此:无任何密码时禁止单独开启 enabled(除非本次同请求携带 password),
|
|
127
|
+
// 也禁止单独切换到 password_only(除非已设密码或本次带 password)。
|
|
128
|
+
const willHaveNoPassword = !authManager.hasPassword && !authManager.hasAdminPassword
|
|
129
|
+
&& (password === undefined || !password);
|
|
130
|
+
const nextMode = mode ?? authManager.mode;
|
|
131
|
+
const nextEnabled = enabled ?? authManager.enabled;
|
|
132
|
+
if (willHaveNoPassword && nextMode === 'password_only') {
|
|
133
|
+
if (nextEnabled) {
|
|
134
|
+
return fail('bad-request', '仅密码模式需要先设置访问密码:请先在下方「设置外部访客访问密码」处设置密码,再开启安全防护');
|
|
135
|
+
}
|
|
136
|
+
if (mode !== undefined) {
|
|
137
|
+
return fail('bad-request', '仅密码模式需要先设置访问密码:请先在下方「设置外部访客访问密码」处设置密码后再切换');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
122
141
|
if (enabled != null) await authManager.setEnabled(enabled);
|
|
123
142
|
if (mode != null) await authManager.setMode(mode);
|
|
124
143
|
if (scope != null) await authManager.setScope(scope);
|