dsh-mobile-plugin 0.5.1 → 0.5.4
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/lib/host-guard.cjs +11 -13
- package/lib/impl.js +64 -43
- package/lib/index.js +21 -22
- package/lib/mobile/agent-presets.js +67 -1
- package/lib/mobile/debug-trace.js +180 -0
- package/lib/mobile/deepseek-account.js +3 -0
- package/lib/mobile/entry-window.js +6 -1
- package/lib/mobile/history-derive.js +148 -9
- package/lib/mobile/local-images.js +200 -47
- package/lib/mobile/session-local-image.js +45 -0
- package/lib/mobile/session-mirror.js +2 -1
- package/lib/mobile/session-send.js +13 -1
- package/lib/mobile/session-snapshot.js +37 -54
- package/lib/mobile/session-stats.js +51 -0
- package/lib/mobile/sessions.js +3 -2
- package/lib/mobile/tool-result-cache.js +75 -0
- package/lib/panel/index.html +6 -6
- package/lib/relay/firewall-probe.js +11 -5
- package/lib/routes/mobile-api.js +11 -0
- package/lib/shell/runtime.js +3 -2
- package/lib/sidecar-lifecycle.cjs +18 -1
- package/lib/update-install.cjs +2 -2
- package/package.json +2 -2
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - **Windows**:默认入站全拒,所以「有允许规则」才算放行,没有就是被挡;还要看那条
|
|
20
20
|
* 规则管的是哪个网络类别 —— 笔记本换个 WiFi、系统把那个网判成「公用」时,只放行
|
|
21
21
|
* 「专用」的老规则就不再生效。这就是「出去一趟就连不上、看一眼防火墙又好了」的
|
|
22
|
-
*
|
|
22
|
+
* 真身,所以当前接入的网络类别必须一起查。开关与规则都按**生效策略**(`ActiveStore`)
|
|
23
|
+
* 查,不按本地静态库 —— 见 `WINDOWS_SCRIPT` 上面那条。
|
|
23
24
|
* - **Linux**:只能读到「防火墙开着没」(`firewall-cmd --state` 不要 root),
|
|
24
25
|
* 「放行了没」要 root 才读得到 —— 那条我们不碰,一律回 `null`。
|
|
25
26
|
*
|
|
@@ -139,19 +140,24 @@ function decideWindowsAllows(report) {
|
|
|
139
140
|
/**
|
|
140
141
|
* Windows 那份 PowerShell。
|
|
141
142
|
*
|
|
142
|
-
*
|
|
143
|
+
* 三个要点:
|
|
143
144
|
* 1. `Get-NetFirewallApplicationFilter -Program <path>` 是**服务端过滤**,只回这个程序
|
|
144
145
|
* 的过滤条件;换成枚举全部规则再逐条 join 会有几百次 CIM 查询,第一次跑要好几秒。
|
|
145
146
|
* 2. 输出必须是 JSON 且全 ASCII —— `netsh` 的文字输出会跟着系统语言变,中文 Windows
|
|
146
147
|
* 上没有办法稳定匹配。
|
|
148
|
+
* 3. 每一条查询都写死 `-PolicyStore ActiveStore`。不带这个参数时 PowerShell 读的是
|
|
149
|
+
* 本地静态库(PersistentStore),而它**不含组策略下发的开关与规则**:组策略管着的
|
|
150
|
+
* 机器上,系统界面显示的防火墙开着,按本地库读出来三个 profile 可能全是「关」
|
|
151
|
+
* —— 用户看到的「实际防火墙开着、自检说关着」就是这一条;反过来,组策略下发的
|
|
152
|
+
* 放行规则也会被漏掉、报成「挡住」。
|
|
147
153
|
*/
|
|
148
154
|
const WINDOWS_SCRIPT = [
|
|
149
155
|
'$path = $env:DSH_FIREWALL_PROGRAM;',
|
|
150
|
-
'$profiles = @(Get-NetFirewallProfile | ForEach-Object { @{ name = "$($_.Name)"; enabled = [bool]$_.Enabled } });',
|
|
156
|
+
'$profiles = @(Get-NetFirewallProfile -PolicyStore ActiveStore | ForEach-Object { @{ name = "$($_.Name)"; enabled = [bool]$_.Enabled } });',
|
|
151
157
|
'$active = @(Get-NetConnectionProfile | ForEach-Object { $c = "$($_.NetworkCategory)"; if ($c -eq "DomainAuthenticated") { "Domain" } else { $c } });',
|
|
152
158
|
'$apps = @();',
|
|
153
|
-
'foreach ($f in @(Get-NetFirewallApplicationFilter -Program $path)) {',
|
|
154
|
-
' $r = Get-NetFirewallRule -Name $f.Name;',
|
|
159
|
+
'foreach ($f in @(Get-NetFirewallApplicationFilter -PolicyStore ActiveStore -Program $path)) {',
|
|
160
|
+
' $r = Get-NetFirewallRule -Name $f.Name -PolicyStore ActiveStore;',
|
|
155
161
|
' if ($r) { $apps += @{ program = "$($f.Program)"; action = "$($r.Action)"; enabled = ("$($r.Enabled)" -eq "True"); profile = "$($r.Profile)" } }',
|
|
156
162
|
'}',
|
|
157
163
|
'@{ profiles = $profiles; apps = $apps; activeProfiles = $active } | ConvertTo-Json -Depth 6 -Compress',
|
package/lib/routes/mobile-api.js
CHANGED
|
@@ -17,6 +17,7 @@ const { orderCandidates, withManualCandidate } = require('../relay/lan-network.c
|
|
|
17
17
|
const sessions = require('../mobile/sessions.js');
|
|
18
18
|
const sessionModelOptions = require('../mobile/session-model-options.js');
|
|
19
19
|
const sessionFile = require('../mobile/session-file.js');
|
|
20
|
+
const sessionLocalImage = require('../mobile/session-local-image.js');
|
|
20
21
|
const sessionStep = require('../mobile/session-step.js');
|
|
21
22
|
const fileRead = require('../mobile/file-read.js');
|
|
22
23
|
const stats = require('../mobile/stats.js');
|
|
@@ -29,6 +30,7 @@ const workspaces = require('../mobile/workspaces.js');
|
|
|
29
30
|
const directoryListing = require('../mobile/directory-listing.js');
|
|
30
31
|
const archiveClean = require('../mobile/archive-clean.js');
|
|
31
32
|
const sessionLog = require('../mobile/session-log.js');
|
|
33
|
+
const debugTrace = require('../mobile/debug-trace.js');
|
|
32
34
|
const hostSettings = require('../mobile/host-settings.js');
|
|
33
35
|
const channels = require('../mobile/channels.js');
|
|
34
36
|
const deepSeekAccount = require('../mobile/deepseek-account.js');
|
|
@@ -229,6 +231,8 @@ function registerMobileRoutes(deps) {
|
|
|
229
231
|
// 一步的过程详情:清单只带一行摘要,点开某一步才来这里取参数 / 输出 / diff / 思考全文。
|
|
230
232
|
route('/api/mobile/v1/session/step', sessionStep.handleSessionStep)
|
|
231
233
|
route('/api/mobile/v1/session/attachment', sessions.handleSessionAttachment)
|
|
234
|
+
// 正文图的字节:按登记过的编号取,不是按路径读文件(见 mobile/local-images.js 的文件头)。
|
|
235
|
+
route('/api/mobile/v1/session/local-image', sessionLocalImage.handleSessionLocalImage)
|
|
232
236
|
// 会话文件:允许本会话 present 声明的路径及会话工作区内的文件(见 mobile/session-file.js)。
|
|
233
237
|
route('/api/mobile/v1/session/file', sessionFile.handleSessionFile)
|
|
234
238
|
// 文件浏览器按绝对路径取字节,范围是宿主已登记的工作区及当前会话的项目目录
|
|
@@ -267,6 +271,9 @@ function registerMobileRoutes(deps) {
|
|
|
267
271
|
route('/api/mobile/v1/agent-presets/read', agentPresets.handleAgentPresetRead)
|
|
268
272
|
route('/api/mobile/v1/agent-presets/copy', agentPresets.handleAgentPresetCopy)
|
|
269
273
|
route('/api/mobile/v1/agent-presets/remove', agentPresets.handleAgentPresetRemove)
|
|
274
|
+
// 把某一档设成「新会话默认吃的那一档」:写宿主 settings 的 agent-preset-registry,
|
|
275
|
+
// 与桌面端预设页那颗「设为默认」同一格(不是预设自己的属性)。
|
|
276
|
+
route('/api/mobile/v1/agent-presets/default', agentPresets.handleAgentPresetDefault)
|
|
270
277
|
route('/api/mobile/v1/session/new', sessions.handleNew)
|
|
271
278
|
route('/api/mobile/v1/session/stream', stream.handleStream)
|
|
272
279
|
route('/api/mobile/v1/events/stream', events.handleDeviceStream)
|
|
@@ -326,6 +333,10 @@ function registerMobileRoutes(deps) {
|
|
|
326
333
|
// 的 127.0.0.1 上手机直连不到 —— 所以由插件代取(mobile/session-log.js),App 侧
|
|
327
334
|
// 再落盘。
|
|
328
335
|
route('/api/mobile/v1/session-log', sessionLog.handleSessionLog)
|
|
336
|
+
// 手机端调试轨迹(开发包专用,见 mobile/debug-trace.js):App 录滑动过程,边录边把 JSONL
|
|
337
|
+
// 打到这里,插件写进 <DSH_HOME>/mobile-debug/<capture>.jsonl,维护者在电脑上直接读。
|
|
338
|
+
// 只追加、轨迹名走白名单、单份 8MB 封顶 —— 不是「任意文件写入」。
|
|
339
|
+
route('/api/mobile/v1/debug/trace', debugTrace.handleDebugTrace)
|
|
329
340
|
// 宿主设置与模型提供方(宿主 settings.describe / llm.providers 的移动端只读代理)。
|
|
330
341
|
// 桌面端「设置」里有默认模型、提供方配置、凭据是否已设;手机端此前完全没有这一块,
|
|
331
342
|
// 只能看到 App 自己的偏好项。两个宿主接口本身已脱敏(secrets 只给 path 与 set),
|
package/lib/shell/runtime.js
CHANGED
|
@@ -45,9 +45,10 @@ const START_RETRY_MS = Number(process.env.DSH_MOBILE_START_RETRY_MS) > 0
|
|
|
45
45
|
* 代没有声明 `inject` 时的兜底(只有测试里的假代会走这里)。
|
|
46
46
|
*
|
|
47
47
|
* 真实实现 `lib/impl.js` 一定声明 inject,而且是按运行时二选一的,所以这里**不能**
|
|
48
|
-
* 拿它去覆盖代自己的声明 —— 那会把旧运行时要用的 `apiProxy`
|
|
48
|
+
* 拿它去覆盖代自己的声明 —— 那会把旧运行时要用的 `apiProxy` 抹掉。兜底值同样只留
|
|
49
|
+
* `webServer`:多一个服务就多一种「整行 pending」的理由。
|
|
49
50
|
*/
|
|
50
|
-
const DEFAULT_INJECT = ['webServer'
|
|
51
|
+
const DEFAULT_INJECT = ['webServer']
|
|
51
52
|
|
|
52
53
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
53
54
|
|
|
@@ -43,6 +43,11 @@ function createSidecarLifecycle(deps) {
|
|
|
43
43
|
// 那样重启就一定有一半调用点还握着旧实例。
|
|
44
44
|
// The child-process bridge is used by the sidecar transport; Cordis services remain injected read-only.
|
|
45
45
|
let sidecarState = 'starting'
|
|
46
|
+
// 「这个 error 态是不是健康轮询自己写上去的」。轮询失败只是这一次回环请求被掐了
|
|
47
|
+
// (worker 正在被换掉、socket 抖一下),下一次轮询通了就必须自己抹回 healthy;
|
|
48
|
+
// 不下发配置、不重启,那一行就会一直挂着旧错误等人来点重启。反过来,下发配置 /
|
|
49
|
+
// 重启失败留下的 error **不许**被轮询抹掉 —— worker 活着但配置没生效,那还是坏的。
|
|
50
|
+
let stateFromPoll = false
|
|
46
51
|
let sidecarSnapshot = { tunnel: { state: 'off', error: null }, lan: { state: 'off', error: null } }
|
|
47
52
|
// worker 意外退出时的唯一出口:把状态打回「不健康」,而不是让它冻在最后一帧。
|
|
48
53
|
// 冻住的后果是 /api/relay/status 继续报 tunnel: connected / lan: listening,
|
|
@@ -53,6 +58,7 @@ function createSidecarLifecycle(deps) {
|
|
|
53
58
|
// 时要把 markSidecarDown 交进去,箭头函数还没初始化就被引用会直接 TDZ 报错。
|
|
54
59
|
const markSidecarDown = (error) => {
|
|
55
60
|
const message = error instanceof Error ? error.message : String(error)
|
|
61
|
+
stateFromPoll = true
|
|
56
62
|
sidecarState = `error: ${message}`
|
|
57
63
|
sidecarSnapshot = { tunnel: { state: 'off', error: message }, lan: { state: 'off', error: message } }
|
|
58
64
|
}
|
|
@@ -119,12 +125,14 @@ function createSidecarLifecycle(deps) {
|
|
|
119
125
|
}
|
|
120
126
|
applyLedger.recordSuccess()
|
|
121
127
|
sidecarState = 'healthy'
|
|
128
|
+
stateFromPoll = false
|
|
122
129
|
return sidecarSnapshot
|
|
123
130
|
}
|
|
124
131
|
const syncSidecar = () => {
|
|
125
132
|
if (syncInFlight) { syncQueued = true; return syncInFlight }
|
|
126
133
|
syncInFlight = applySidecarConfig()
|
|
127
134
|
.catch(error => {
|
|
135
|
+
stateFromPoll = false
|
|
128
136
|
sidecarState = `error: ${error instanceof Error ? error.message : String(error)}`
|
|
129
137
|
throw error
|
|
130
138
|
})
|
|
@@ -176,12 +184,14 @@ function createSidecarLifecycle(deps) {
|
|
|
176
184
|
await sidecar.start()
|
|
177
185
|
await syncSidecar()
|
|
178
186
|
sidecarState = 'healthy'
|
|
187
|
+
stateFromPoll = false
|
|
179
188
|
sidecarRestarts += 1
|
|
180
189
|
sidecarRestartedAt = Date.now()
|
|
181
190
|
sidecarRestartError = null
|
|
182
191
|
return { ok: true, before, after: { tunnel: sidecarSnapshot.tunnel, lan: sidecarSnapshot.lan, state: sidecarState }, purgedModules: purged }
|
|
183
192
|
} catch (error) {
|
|
184
193
|
sidecarRestartError = error instanceof Error ? error.message : String(error)
|
|
194
|
+
stateFromPoll = false
|
|
185
195
|
sidecarState = `error: ${sidecarRestartError}`
|
|
186
196
|
// 起不来就把状态如实留在 error,而不是假装成功。宿主侧后续的 /tunnel/send
|
|
187
197
|
// 会因为没有 worker 而明确报 'sidecar facade is not running'。
|
|
@@ -203,7 +213,11 @@ function createSidecarLifecycle(deps) {
|
|
|
203
213
|
if (disposed) return
|
|
204
214
|
await syncSidecar()
|
|
205
215
|
sidecarState = 'healthy'
|
|
206
|
-
|
|
216
|
+
stateFromPoll = false
|
|
217
|
+
}).catch(error => {
|
|
218
|
+
stateFromPoll = false
|
|
219
|
+
sidecarState = `error: ${error instanceof Error ? error.message : String(error)}`
|
|
220
|
+
})
|
|
207
221
|
const healthTimer = setInterval(async () => {
|
|
208
222
|
try {
|
|
209
223
|
const st = await sidecar.request('/status')
|
|
@@ -212,6 +226,9 @@ function createSidecarLifecycle(deps) {
|
|
|
212
226
|
// 'connecting'。原来这次轮询只用来判断隧道连接跳变,结果被直接丢弃,于是
|
|
213
227
|
// /api/relay/status 永远谎报局域网"没起来",尽管 0.0.0.0:lanPort 早在监听。
|
|
214
228
|
if (st) sidecarSnapshot = st
|
|
229
|
+
// 轮询通了就等于「worker 就在那儿、这一下请求也成了」:若上一格的 error 是轮询自己
|
|
230
|
+
// 写上去的,这里必须抹回 healthy —— 抖一下不该永久挂一行红字,等用户来点重启。
|
|
231
|
+
if (stateFromPoll) { sidecarState = 'healthy'; stateFromPoll = false }
|
|
215
232
|
// 边沿判定与即时上报共用(noteTunnelConnected):这里只是权威兜底,
|
|
216
233
|
// 隧道在两次轮询之间断又连时补上那一次全量补推。
|
|
217
234
|
noteTunnelConnected(st?.tunnel?.state === 'connected')
|
package/lib/update-install.cjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 不自己实现包管理,只驱动它:profile 的 pnpm-workspace.yaml 已经写好 node-linker、peer 等
|
|
10
10
|
* 关键设置,这里只「cd 到 profile 跑一句 pnpm add <包名>@<版本>」,不传布局类开关。必要的
|
|
11
11
|
* 开关有两层:auto-install-peers=false(peer 是运行时注入的,npm 上没有)、minimum-release-age=0
|
|
12
|
-
*
|
|
12
|
+
* (一检测到新版就立刻装,不等「新发布先晾一会儿」;那道策略查的是整份
|
|
13
13
|
* lockfile,别的插件刚发也会连累我们),再加上 CI=1(无终端)。命令行带一份、环境变量再带一份:
|
|
14
14
|
* pnpm 12 的原生 CLI 会静默忽略这两个 --config 参数,环境变量那一份还认。
|
|
15
15
|
*
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* 包管理器优先用宿主发布的那个(profile 是宿主自带 pnpm 装的,PATH 上版本不同会
|
|
21
21
|
* ERR_PNPM_UNEXPECTED_STORE),找不到再退回 PATH。装之前先看有没有 agent 在跑;force 与 auto
|
|
22
|
-
*
|
|
22
|
+
* 都跳过这道闸(自动更新一检测到新版就装,与面板上那颗「强制更新」
|
|
23
23
|
* 同一个口径),两者的区别只剩「没新版时装不装」。顺序上先答「有没有新版」,没东西可装时
|
|
24
24
|
* 不该拿「有对话在跑」挡在前面。
|
|
25
25
|
*
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile-plugin",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (dsh 0.2.x, peers resolved by the host)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"icon": "./icon.png",
|
|
7
7
|
"engines": {
|
|
8
|
-
"dsh": ">=0.2.0
|
|
8
|
+
"dsh": ">=0.2.0"
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
11
|
"main": "lib/index.js",
|