dsh-mobile-plugin 0.1.62 → 0.1.64
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/LICENSE +21 -0
- package/README.md +21 -1
- package/lib/http/routing.js +8 -1
- package/lib/impl.js +12 -15
- package/lib/mobile/activity.cjs +28 -0
- package/lib/panel/index.html +8 -8
- package/lib/relay/lan-network.cjs +97 -7
- package/lib/routes/control-plane.js +46 -1
- package/lib/update-install.cjs +56 -23
- package/package.json +2 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WNLUO
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -10,9 +10,28 @@ Electron 运行时**,`node host.js` 即可独立运行。
|
|
|
10
10
|
|
|
11
11
|
设备级实时兼容出口:`GET /api/mobile/v1/events/stream`(Bearer token)以 SSE 转发宿主 `events.mux` 的所有设备事件。每帧包装为 `{ type: "device/event", version: 1, eventId, sessionId?, rpcId?, payload }`;`payload` 保留原 mux 负载,旧的 `/session/stream` 与 relay 协议不变。`session/event` 的 `eventId` 为 `${sessionId}:${event.seq}`,其他帧使用连接内序号。工作区变更是否可见取决于宿主 mux 是否发布对应帧;当前插件没有额外伪造工作区事件。
|
|
12
12
|
|
|
13
|
+
## 仓库与发布
|
|
14
|
+
|
|
15
|
+
本仓库是插件的**公开发布仓库**,仓库根就是包根:别人从这里读代码,也从这里核对这份代码的说法。
|
|
16
|
+
|
|
17
|
+
开发和 App 客户端、中转服务器放在一起进行(一个私有 monorepo),所以这里的提交历史从拆分那天开始,
|
|
18
|
+
不是项目历史 —— 每次发版,把同一次改动一起推到这里。
|
|
19
|
+
|
|
20
|
+
- **仓库地址要指回本仓库**:`package.json` 的 `repository` 字段是 npm 包与插件列表条目之间的唯一关联
|
|
21
|
+
(列表侧从注册表自动采集,条目里不许手写 `npm:`)。改了它要等下一次 `npm publish` 才会体现在注册表上。
|
|
22
|
+
- **要被插件列表(awesome-dsh-plugin)收录**:往那个仓库提一个数据文件
|
|
23
|
+
`data/plugins/<owner>__<repo>.yml` 就行,**一个文件即全部投稿**;`description.en` 必填,分类用 `remote`。
|
|
24
|
+
硬门槛是 `package.json` 里声明 `dsh.bundle`(只声明 `dsh.client` 会被 CI 拒)、仓库创建满 1 天、
|
|
25
|
+
以及本仓库打了 `dsh-plugin` topic。那条「满 1 天」的检查**会自己转绿**(列表侧每 6 小时重跑一次),
|
|
26
|
+
当天看到红不用重提、也不用关了重开。
|
|
27
|
+
- **截图**:市场详情页要图,就在 `package.json` 旁边放一份 `screenshots.json`,列 1-8 张**本仓库内**的图片路径
|
|
28
|
+
(相对路径不能跳出仓库;绝对地址只收 GitHub 托管的 https)。不声明也行,市场会自己从本 README 里找图。
|
|
29
|
+
- **描述会被对着代码核**:条目里写的每个数字和 API 名字都要在代码里找得到。夸大是唯一会被打回的原因 ——
|
|
30
|
+
分类选得不准不会,维护者会直接改。
|
|
31
|
+
|
|
13
32
|
## 无头部署
|
|
14
33
|
|
|
15
|
-
部署到 Linux
|
|
34
|
+
部署到 Linux 服务器(无桌面环境)时的要点如下。
|
|
16
35
|
|
|
17
36
|
**目录布局不能漏掉凭据库。** 设备凭据库 `dsh-mobile-credentials` 就在本包目录内部(发布时它仍是独立的一个包,装进来后嵌在插件目录里),代码用包内相对路径引用它:
|
|
18
37
|
|
|
@@ -117,6 +136,7 @@ DSH_HOME=/opt/dsh-mobile/.dsh DSH_MOBILE_PROFILE=mobile node host.js
|
|
|
117
136
|
(页面写「访问暂时受限」并带上你的 IP)。换个节点或直连再发。
|
|
118
137
|
- npm 已经关掉了「命令行建号」和「验证器 App(TOTP)」两条路:建号只能在网页上做,2FA 只能挂
|
|
119
138
|
安全密钥。这两件事上试命令是白费时间,认它就行。
|
|
139
|
+
- **别指望用「绕过 2FA」的令牌跳掉上面这一下**(2026-10-11 实测):换上这把令牌之后 `npm whoami` 能过,但写操作照样要求交互式验证(拿 `npm dist-tag add` 试的),而 npm 2026-07-31 的公告说这类令牌 2027-01 起连直接发布也会失去。要自动化发布得迁到 trusted publishing(OIDC)或 staged publishing,本项目的发布是人在开发机上发,这两条都不顺手。
|
|
120
140
|
- 想少点一次确认,可以建一把「允许绕过 2FA」的细粒度令牌少输几次验证:
|
|
121
141
|
`npm token create --bypass-2fa --packages dsh-mobile-plugin --packages-and-scopes-permission read-write --expires 90`
|
|
122
142
|
(**最长 90 天**)。代价:这类令牌 **2027-01 起不能再直接发布**,到期或到点都得回到上面
|
package/lib/http/routing.js
CHANGED
|
@@ -63,10 +63,17 @@ const readJson = (req) => readJsonBody(req, 256 * 1024).then(body => body || {})
|
|
|
63
63
|
*
|
|
64
64
|
* @param {object} ctx 插件 ctx,透传给 handler
|
|
65
65
|
* @param {(req: object) => boolean} authorized 移动 token 校验,由调用方注入
|
|
66
|
+
* @param {() => void} [note] 通过 token 校验之后叫一声:宿主用它记「手机来过」的时刻,
|
|
67
|
+
* 引导最后一页据此说「手机已经连上了」。**只在校验通过之后叫**,401 那条不算——
|
|
68
|
+
* 否则随便谁打一下也能把面板骗成「连上了」。
|
|
66
69
|
* @returns {(handler: (ctx: object, req: object, res: object) => Promise<unknown>) => (req: object, res: object) => Promise<unknown>}
|
|
67
70
|
*/
|
|
68
|
-
const createGuarded = (ctx, authorized) => (handler) => async (req, res) => {
|
|
71
|
+
const createGuarded = (ctx, authorized, note = null) => (handler) => async (req, res) => {
|
|
69
72
|
if (!authorized(req)) return sendJson(res, 401, { ok: false, error: { code: 'unauthorized', message: '缺少或无效的移动 token' } })
|
|
73
|
+
if (note) {
|
|
74
|
+
// 记账本身绝不能影响请求结果:它坏了也只是面板少一个确认。
|
|
75
|
+
try { note() } catch { /* 忽略 */ }
|
|
76
|
+
}
|
|
70
77
|
try { await handler(ctx, req, res) } catch (error) {
|
|
71
78
|
sendJson(res, 500, { ok: false, error: { code: 'internal', message: String(error?.message || error) } })
|
|
72
79
|
}
|
package/lib/impl.js
CHANGED
|
@@ -24,7 +24,7 @@ const { authorized } = require('./mobile/auth.js')
|
|
|
24
24
|
const { createSidecarLifecycle } = require('./sidecar-lifecycle.cjs')
|
|
25
25
|
const { createMirrorPusher } = require('./mirror/pusher.cjs')
|
|
26
26
|
const { randomBytes } = require('node:crypto')
|
|
27
|
-
const { lanUrls,
|
|
27
|
+
const { lanUrls, advertisedCandidates } = require('./relay/lan-network.cjs')
|
|
28
28
|
const { resolvePort } = require('./relay/port-allocator.cjs')
|
|
29
29
|
const { createPreviewService } = require('./relay/preview.js')
|
|
30
30
|
// 本机设备凭据库:/api/relay/revoke-all 必须同时吊销局域网直连的 dshc_ 凭据,
|
|
@@ -32,6 +32,8 @@ const { createPreviewService } = require('./relay/preview.js')
|
|
|
32
32
|
// 入口按两条盘上布局分别找(嵌在插件包内 / 与插件包同级),见 shared/credentials-entry.js。
|
|
33
33
|
const credentials = require('./shared/credentials-entry.js').credentialsEntry()
|
|
34
34
|
const { createApiProxy } = require('./mobile/host-bridge.js')
|
|
35
|
+
// 「手机上最近来过没有」:守卫记、控制面状态读。单独一个模块,装配层只转交。
|
|
36
|
+
const activity = require('./mobile/activity.cjs')
|
|
35
37
|
const { installAutoArchive } = require('./auto-archive.cjs')
|
|
36
38
|
const { registerSessionCleanupTool } = require('./session-cleanup.cjs')
|
|
37
39
|
const { activeGuard } = require('./host-guard.cjs')
|
|
@@ -147,7 +149,7 @@ function createConfigStore(state) {
|
|
|
147
149
|
|
|
148
150
|
function pairingPayload(state, lanPort) {
|
|
149
151
|
const port = Number.isInteger(lanPort) ? lanPort : state.lanPort
|
|
150
|
-
const candidates = state.lanEnabled ?
|
|
152
|
+
const candidates = state.lanEnabled ? advertisedCandidates(port) : []
|
|
151
153
|
const urls = candidates.map(candidate => candidate.url).filter(Boolean)
|
|
152
154
|
const relayUrl = state.relayUrl || ''
|
|
153
155
|
const payload = {
|
|
@@ -168,17 +170,9 @@ function pairingPayload(state, lanPort) {
|
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
172
|
|
|
171
|
-
// 上报给 App
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
function safeLanCandidates(port) {
|
|
175
|
-
try {
|
|
176
|
-
const list = lanCandidates(port)
|
|
177
|
-
return Array.isArray(list) ? list : []
|
|
178
|
-
} catch {
|
|
179
|
-
return []
|
|
180
|
-
}
|
|
181
|
-
}
|
|
173
|
+
// 上报给 App 的候选地址(私网优先、没有私网才退公网)住在 `./relay/lan-network.cjs`
|
|
174
|
+
// 的 `advertisedCandidates`:那是地址策略,不是装配。这里只把它转交给三处消费点。
|
|
175
|
+
// 「手机上最近来过没有」同理,住在 `./mobile/activity.cjs`(守卫记、控制面状态读)。
|
|
182
176
|
|
|
183
177
|
/**
|
|
184
178
|
* 运行时是否自带 `@deepseek-ai/dsh-host-apiproxy`(即 `api-gateway` 那个入口)。
|
|
@@ -340,13 +334,15 @@ function apply(ctx) {
|
|
|
340
334
|
getLanPortAdjusted: () => lanPortAdjusted,
|
|
341
335
|
pairingPayload,
|
|
342
336
|
restartSidecar,
|
|
337
|
+
// 「手机上最近来过没有」——控制面状态里补给面板用(见 ./mobile/activity.cjs)。
|
|
338
|
+
mobileActivity: activity.snapshot,
|
|
343
339
|
// 桌面端「我读过了」那条路由(/api/relay/desktop-read)要清掉宿主侧的完成提醒,
|
|
344
340
|
// 需要插件 ctx 才能拿到按设备隔离的状态表。
|
|
345
341
|
ctx,
|
|
346
342
|
})
|
|
347
343
|
|
|
348
344
|
|
|
349
|
-
const guarded = createGuarded(ctx, authorized)
|
|
345
|
+
const guarded = createGuarded(ctx, authorized, activity.note)
|
|
350
346
|
|
|
351
347
|
// 内置浏览器预览的宿主端:手机按端口在它自己的环回上监听,把内置浏览器写下的原始
|
|
352
348
|
// 字节通过 WebSocket 送到这里,这里与 `127.0.0.1:<port>` 对搬。
|
|
@@ -387,7 +383,8 @@ function apply(ctx) {
|
|
|
387
383
|
// 一进来就 ReferenceError。同一个 apply() 里给 createStatusView 传的是
|
|
388
384
|
// `sidecarLifecycle.getSnapshot`,两处不一致才让这个笔误活了下来。
|
|
389
385
|
getSidecarSnapshot: sidecarLifecycle.getSnapshot,
|
|
390
|
-
|
|
386
|
+
// 移动端路由表那边叫它「局域网候选」,取的是同一份私网优先的清单。
|
|
387
|
+
safeLanCandidates: advertisedCandidates,
|
|
391
388
|
lanPortInUse: advertisedLanPort, // LAN 候选用实绑端口
|
|
392
389
|
})
|
|
393
390
|
ctx.effect(() => {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 「手机上最近来过没有」—— 一个时间戳,别的什么都没有。
|
|
5
|
+
*
|
|
6
|
+
* 移动端守卫每放行一个带有效 token 的请求就叫一次 `note()`(见 `../http/routing.js`
|
|
7
|
+
* 的 `createGuarded`,**401 那条不算**),控制面把 `snapshot()` 补进本机状态给面板读:
|
|
8
|
+
* 引导最后一页靠它把「等手机扫码」变成真结论,而不是一个永远转下去的圈。
|
|
9
|
+
*
|
|
10
|
+
* 为什么单独一个文件:装配层 `lib/impl.js` 有行数上限(`test/assembly-layer.test.cjs`
|
|
11
|
+
* 钉着),而这份状态既不属于装配、也不属于路由表。放在模块作用域就够 —— 一个进程就是
|
|
12
|
+
* 一个宿主,插件的代被换掉时这个数跟着重置,换代之后的第一部手机再来一次就会重新记上,
|
|
13
|
+
* 正是我们要的语义。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
let lastMobileAt = 0
|
|
17
|
+
|
|
18
|
+
/** 记一次「手机来过」。守卫里调用;异常一律吞掉——记账坏了也只是面板少一个确认。 */
|
|
19
|
+
function note() {
|
|
20
|
+
lastMobileAt = Date.now()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 面板读的快照。`0` = 从来没来过。 */
|
|
24
|
+
function snapshot() {
|
|
25
|
+
return { lastMobileAt }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { note, snapshot }
|