dsh-mobile-plugin 0.1.95 → 0.1.97
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 +73 -50
- package/config/agent-presets/standard/agent.cordis.yml +13 -0
- package/dsh-mobile-credentials/lib/credentials.js +24 -1
- package/lib/git/exec.js +129 -0
- package/lib/git/package.json +3 -0
- package/lib/git/parse.js +247 -0
- package/lib/git/read.js +617 -0
- package/lib/impl.js +2 -2
- package/lib/mirror/pusher.cjs +25 -2
- package/lib/mobile/archive-clean.js +31 -1
- package/lib/mobile/channels.js +583 -0
- package/lib/mobile/events.js +10 -8
- package/lib/mobile/git-repos.js +210 -0
- package/lib/mobile/git.js +603 -0
- package/lib/mobile/history-derive.js +79 -1
- package/lib/mobile/host-bridge/endpoints.js +3 -0
- package/lib/mobile/host-bridge/event-synthesis.js +109 -1
- package/lib/mobile/host-bridge/shims.js +9 -2
- package/lib/mobile/host-bridge/transport.js +30 -1
- package/lib/mobile/host-settings.js +41 -0
- package/lib/mobile/inbox-clean.js +337 -0
- package/lib/mobile/pairing.js +18 -1
- package/lib/mobile/questions.js +31 -9
- package/lib/mobile/schedule.js +440 -0
- package/lib/mobile/session-cache.js +18 -0
- package/lib/mobile/session-file.js +192 -0
- package/lib/mobile/session-model-options.js +184 -0
- package/lib/mobile/session-models.js +24 -2
- package/lib/mobile/session-schedule-mark.js +70 -0
- package/lib/mobile/session-snapshot.js +12 -1
- package/lib/mobile/sessions.js +8 -0
- package/lib/mobile/stats-collect.cjs +257 -0
- package/lib/mobile/stats-ledger.cjs +407 -0
- package/lib/mobile/stats.js +38 -0
- package/lib/mobile/stream.js +18 -10
- package/lib/mobile/uploads.js +24 -6
- package/lib/mobile/workspaces.js +30 -6
- package/lib/panel/index.html +4 -4
- package/lib/relay/lan-proxy.js +10 -1
- package/lib/routes/control-plane.js +38 -0
- package/lib/routes/mobile-api.js +70 -1
- package/lib/session-cleanup.cjs +8 -0
- package/lib/shared/sse-write.js +17 -6
- package/lib/shell/updater.js +3 -3
- package/package.json +3 -2
package/lib/relay/lan-proxy.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
const http = require('node:http');
|
|
15
15
|
const { WebSocketServer, WebSocket } = require('ws');
|
|
16
16
|
const core = require('./core.js');
|
|
17
|
-
const { isGlobalUnicastV6 } = require('./lan-network.cjs');
|
|
17
|
+
const { advertisedCandidates, ipv6Candidates, isGlobalUnicastV6 } = require('./lan-network.cjs');
|
|
18
18
|
const { safeEqual } = require('../shared/dshtoken');
|
|
19
19
|
const { isFreshRequest } = require('../shared/fresh-request');
|
|
20
20
|
const { encodeJsonBody } = require('../shared/http-util');
|
|
@@ -519,6 +519,13 @@ function createLanProxy(opts) {
|
|
|
519
519
|
const issued = credentials.issue(instanceId, device, token);
|
|
520
520
|
const cookieValue = core.signLanToken(lanSecret, instanceId, core.MOBILE_TOKEN_TTL_SEC);
|
|
521
521
|
setCookie(res);
|
|
522
|
+
// 宿主自己那批直连候选一起回给手机:手输公网地址(域名 / 公网 IP)配对时,
|
|
523
|
+
// 二维码那条路没有、也就只有这一处能拿到内网地址。回家后如果那个公网地址
|
|
524
|
+
// 打不通(路由器不支持回环 / 域名只在外网解析),手机还能靠这批候选找到这台
|
|
525
|
+
// 机器,否则只能删设备重配。App 的 `doPair` 本来就合并响应里的 `lanUrls`
|
|
526
|
+
// 与 `ipv6Candidates`,这里不回它就只能记住手输的那一个地址。
|
|
527
|
+
const directCandidates = advertisedCandidates(actualPort).map((candidate) => candidate.url).filter(Boolean);
|
|
528
|
+
const v6Candidates = ipv6Candidates(actualPort).map((candidate) => candidate.url).filter(Boolean);
|
|
522
529
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
523
530
|
// cookie 同时放进 body:RN fetch 读不到 Set-Cookie 响应头,App 需自行携带
|
|
524
531
|
res.end(JSON.stringify({
|
|
@@ -526,6 +533,8 @@ function createLanProxy(opts) {
|
|
|
526
533
|
instanceId,
|
|
527
534
|
token: token || '',
|
|
528
535
|
cookie: `dsh_lan=${cookieValue}`,
|
|
536
|
+
...(directCandidates.length ? { lanUrls: directCandidates } : {}),
|
|
537
|
+
...(v6Candidates.length ? { ipv6Candidates: v6Candidates } : {}),
|
|
529
538
|
deviceId: issued.deviceId,
|
|
530
539
|
deviceCredential: issued.credential,
|
|
531
540
|
credentialExpiresAt: issued.expiresAt,
|
|
@@ -49,6 +49,8 @@ const { checkForUpdate } = require('../update-registry.cjs');
|
|
|
49
49
|
const { declaredSource } = require('../update-install.cjs');
|
|
50
50
|
const { clearCompletionForAllDevices, invalidateSessionList } = require('../mobile/session-cache.js');
|
|
51
51
|
const { recordSessionRead } = require('../mobile/session-last-turn.js');
|
|
52
|
+
// 收件目录的占用与清理:和移动端那条路由是同一份实现(`lib/mobile/inbox-clean.js`)。
|
|
53
|
+
const { scanInbox, clearInbox, usageView, setAutoClear } = require('../mobile/inbox-clean.js');
|
|
52
54
|
|
|
53
55
|
/** 一次 desktop-read 上报最多带多少个会话 id(防灌爆;客户端半边一次最多也就几个)。 */
|
|
54
56
|
const MAX_DESKTOP_READ_IDS = 200;
|
|
@@ -770,6 +772,38 @@ function registerControlPlaneRoutes(deps) {
|
|
|
770
772
|
})
|
|
771
773
|
}
|
|
772
774
|
}) })
|
|
775
|
+
// ── 收件目录(手机分享进来的文件)──
|
|
776
|
+
//
|
|
777
|
+
// 同一份实现在移动端也有两条(`/api/mobile/v1/inbox/{status,clear}`),这里只是把它
|
|
778
|
+
// 开到网页面板上:面板与 App 读同一份扫描结果,两边不各算一套口径。
|
|
779
|
+
//
|
|
780
|
+
// 走 localOnly:清的是本机上那个固定收件目录,属于本机运维动作,和其余控制面一样。
|
|
781
|
+
// 面板侧也因此在网页管理窗口打不开时仍可由本机操作。
|
|
782
|
+
registerRoute({ kind: 'exact', path: '/api/relay/inbox/status', handler: localOnly('GET', async (_req, res) => {
|
|
783
|
+
sendJson(res, 200, usageView(await scanInbox()))
|
|
784
|
+
}) })
|
|
785
|
+
// 自动清理开关:默认关。打开后宿主每次启动顺手清一次(见 mobile/inbox-clean.js 的
|
|
786
|
+
// clearInboxOnce)。手动那颗「清理收件文件」不受它管 —— 那个是用户当场按的。
|
|
787
|
+
registerRoute({ kind: 'exact', path: '/api/relay/inbox/auto', handler: localOnly('POST', async (req, res) => {
|
|
788
|
+
let body = {}
|
|
789
|
+
try {
|
|
790
|
+
body = (await readJson(req)) || {}
|
|
791
|
+
} catch {
|
|
792
|
+
body = {}
|
|
793
|
+
}
|
|
794
|
+
if (typeof body.enabled !== 'boolean') return sendJson(res, 400, { ok: false, error: { code: 'bad-request', message: 'enabled 必须是布尔值' } })
|
|
795
|
+
try {
|
|
796
|
+
return sendJson(res, 200, { ok: true, data: { autoClear: setAutoClear(body.enabled) } })
|
|
797
|
+
} catch (error) {
|
|
798
|
+
return sendJson(res, 500, { ok: false, error: { code: 'write-failed', message: String(error?.message || error) } })
|
|
799
|
+
}
|
|
800
|
+
}) })
|
|
801
|
+
registerRoute({ kind: 'exact', path: '/api/relay/inbox/clear', handler: localOnly('POST', async (_req, res) => {
|
|
802
|
+
// 面板上那颗按钮自带二次确认;这里不额外要 `confirm` 字段 —— 移动端那条要求它,
|
|
803
|
+
// 是因为那里没有既有的确认口径,而这一页的确认是界面自己的事。
|
|
804
|
+
const result = await clearInbox()
|
|
805
|
+
sendJson(res, 200, { ok: true, ...result })
|
|
806
|
+
}) })
|
|
773
807
|
}
|
|
774
808
|
|
|
775
809
|
// 路由字符串清单放在注册区之后,避免被按路由源码切片的契约测试误认为是真正的 handler。
|
|
@@ -801,6 +835,10 @@ const WEB_ACCESS_ROUTES = new Set([
|
|
|
801
835
|
'/api/relay/restart-sidecar',
|
|
802
836
|
// 开发机的「就地重载」:能换掉本机跑着的实现,属于最该只允许本机调的那一类。
|
|
803
837
|
'/api/relay/reload-implementation',
|
|
838
|
+
// 收件目录的占用与清理(面板那张「收件文件」卡)。
|
|
839
|
+
'/api/relay/inbox/status',
|
|
840
|
+
'/api/relay/inbox/clear',
|
|
841
|
+
'/api/relay/inbox/auto',
|
|
804
842
|
]);
|
|
805
843
|
|
|
806
844
|
// `panelDiagnosis` 一并导出,只为让 test/panel-artifact.test.cjs 能拿临时目录跑那三条分支:
|
package/lib/routes/mobile-api.js
CHANGED
|
@@ -23,6 +23,9 @@ const { sendJson } = require('../http/routing.js');
|
|
|
23
23
|
const { orderCandidates } = require('../relay/lan-network.cjs');
|
|
24
24
|
|
|
25
25
|
const sessions = require('../mobile/sessions.js');
|
|
26
|
+
const sessionModelOptions = require('../mobile/session-model-options.js');
|
|
27
|
+
const sessionFile = require('../mobile/session-file.js');
|
|
28
|
+
const stats = require('../mobile/stats.js');
|
|
26
29
|
const agentPresets = require('../mobile/agent-presets.js');
|
|
27
30
|
const stream = require('../mobile/stream.js');
|
|
28
31
|
const events = require('../mobile/events.js');
|
|
@@ -32,15 +35,19 @@ const workspaces = require('../mobile/workspaces.js');
|
|
|
32
35
|
const archiveClean = require('../mobile/archive-clean.js');
|
|
33
36
|
const sessionLog = require('../mobile/session-log.js');
|
|
34
37
|
const hostSettings = require('../mobile/host-settings.js');
|
|
38
|
+
const channels = require('../mobile/channels.js');
|
|
35
39
|
const deepSeekAccount = require('../mobile/deepseek-account.js');
|
|
36
40
|
const skills = require('../mobile/skills.js');
|
|
37
41
|
const mobileCredentials = require('../mobile/credentials.js');
|
|
38
42
|
const subagents = require('../mobile/subagents.js');
|
|
39
43
|
const monitor = require('../mobile/monitor.js');
|
|
40
44
|
const references = require('../mobile/references.js');
|
|
45
|
+
const schedule = require('../mobile/schedule.js');
|
|
41
46
|
const plugins = require('../mobile/plugins.js');
|
|
42
47
|
const pluginUpdate = require('../mobile/plugin-update.js');
|
|
43
48
|
const uploads = require('../mobile/uploads.js');
|
|
49
|
+
const inboxClean = require('../mobile/inbox-clean.js');
|
|
50
|
+
const git = require('../mobile/git.js');
|
|
44
51
|
const pairing = require('../mobile/pairing.js');
|
|
45
52
|
const { credentialsEntry } = require('../shared/credentials-entry.js');
|
|
46
53
|
// 体检接口要自报身份:版本号只有一个来源,就是包自己的 package.json(与 relay/status.js 同)。
|
|
@@ -118,7 +125,18 @@ function registerMobileRoutes(deps) {
|
|
|
118
125
|
registerRoute({
|
|
119
126
|
kind: 'exact',
|
|
120
127
|
path: '/api/mobile/v1/pair',
|
|
121
|
-
handler: pairing.createPairHandler({
|
|
128
|
+
handler: pairing.createPairHandler({
|
|
129
|
+
state,
|
|
130
|
+
core,
|
|
131
|
+
credentials: credentialsEntry(),
|
|
132
|
+
pairingApproval,
|
|
133
|
+
// 配对响应顺手把宿主当前的直连候选带回去(与 `/connection-info` 同一份取值):
|
|
134
|
+
// 手输公网地址配对时没有二维码,这是手机拿到内网地址的唯一机会。
|
|
135
|
+
lanCandidates: safeLanCandidates,
|
|
136
|
+
ipv6Candidates: safeIpv6Candidates,
|
|
137
|
+
lanPort: lanPortInUse,
|
|
138
|
+
log: (...args) => console.warn('[dsh-mobile-plugin] [pair]', ...args),
|
|
139
|
+
}),
|
|
122
140
|
})
|
|
123
141
|
// 局域网/中转连接信息。**这条由宿主自己回答**:中转只转发,不再代答。
|
|
124
142
|
// 纯局域网配对(App 只扫到局域网二维码、完全不经中转)需要一个宿主自己回答的地址,
|
|
@@ -170,6 +188,8 @@ function registerMobileRoutes(deps) {
|
|
|
170
188
|
route('/api/mobile/v1/session', sessions.handleSession)
|
|
171
189
|
route('/api/mobile/v1/session/messages', sessions.handleMessages)
|
|
172
190
|
route('/api/mobile/v1/session/attachment', sessions.handleSessionAttachment)
|
|
191
|
+
// 交付文件卡要的字节:只放行本会话 present 声明过的路径(见 mobile/session-file.js)。
|
|
192
|
+
route('/api/mobile/v1/session/file', sessionFile.handleSessionFile)
|
|
173
193
|
route('/api/mobile/v1/session/send', sessions.handleSend)
|
|
174
194
|
// 正文水位探针:App 进对话前先问一句「这条会话的正文到哪个水位了」,跟本地缓存水位
|
|
175
195
|
// 一比就知道要不要重拉全量历史(几十字节换几兆)。**只读信号快照**:信号里没有就如实
|
|
@@ -182,7 +202,12 @@ function registerMobileRoutes(deps) {
|
|
|
182
202
|
route('/api/mobile/v1/session/fork', sessions.handleFork)
|
|
183
203
|
route('/api/mobile/v1/session/models', sessions.handleModels)
|
|
184
204
|
route('/api/mobile/v1/session/select-model', sessions.handleSelectModel)
|
|
205
|
+
// 模型之外的附加选项(速度、输出详略……):写一条已声明的行,落地是宿主自己的设置。
|
|
206
|
+
route('/api/mobile/v1/session/model-option', sessionModelOptions.handleModelOption)
|
|
185
207
|
route('/api/mobile/v1/session/stats', sessions.handleStats)
|
|
208
|
+
// 统计总览(统计页)。账本落在宿主家里(`$DSH_HOME/mobile-stats.json`),与会话日志
|
|
209
|
+
// 无关:归档只是挪位置,清理删的是日志,历史数字都还在 —— 见 mobile/stats-ledger.cjs。
|
|
210
|
+
route('/api/mobile/v1/stats/overview', stats.handleStatsOverview)
|
|
186
211
|
// 正文检索(宿主 session.search 的移动端代理):只回 { sessionId, snippet },
|
|
187
212
|
// 标题 / 工作区由 App 用自己轮询到的会话列表拼接,见 mobile/sessions.js。
|
|
188
213
|
route('/api/mobile/v1/session/search', sessions.handleSessionSearch)
|
|
@@ -226,6 +251,24 @@ function registerMobileRoutes(deps) {
|
|
|
226
251
|
// 「清理所有已归档的对话」(侧栏归档视图的文字按钮):按 workspace.list 的归档集合全量清理,
|
|
227
252
|
// 且 body 必须带 confirm: true(磁盘删除不可恢复)。
|
|
228
253
|
route('/api/mobile/v1/workspace/clear-archived', archiveClean.handleClearArchived)
|
|
254
|
+
// Git 管理器(docs/git-manager.md):手机上的仓库概览 / 变更 / 历史 / 分支都是读,
|
|
255
|
+
// 暂存 / 提交 / 切分支是写。能力面是一组固定命名的动词,不接受透传命令;每个请求都带
|
|
256
|
+
// workspaceId 与(可选的)仓库根,插件再校验它确实属于这个项目识别出来的仓库。
|
|
257
|
+
route('/api/mobile/v1/git/repo', git.handleGitRepo)
|
|
258
|
+
route('/api/mobile/v1/git/status', git.handleGitStatus)
|
|
259
|
+
route('/api/mobile/v1/git/diff', git.handleGitDiff)
|
|
260
|
+
route('/api/mobile/v1/git/log', git.handleGitLog)
|
|
261
|
+
route('/api/mobile/v1/git/show', git.handleGitShow)
|
|
262
|
+
route('/api/mobile/v1/git/branches', git.handleGitBranches)
|
|
263
|
+
route('/api/mobile/v1/git/overview', git.handleGitOverview)
|
|
264
|
+
route('/api/mobile/v1/git/stage', git.handleGitStage)
|
|
265
|
+
route('/api/mobile/v1/git/unstage', git.handleGitUnstage)
|
|
266
|
+
route('/api/mobile/v1/git/commit', git.handleGitCommit)
|
|
267
|
+
route('/api/mobile/v1/git/checkout', git.handleGitCheckout)
|
|
268
|
+
// 推当前分支到它的上游:只做这一档,不收分支名 / 远端名 / force 开关。
|
|
269
|
+
route('/api/mobile/v1/git/push', git.handleGitPush)
|
|
270
|
+
// AI 写提交信息:只读 `git diff --cached`,调宿主 llm 服务,回一行文案;不提交、不推送。
|
|
271
|
+
route('/api/mobile/v1/git/commit-message', git.handleGitCommitMessage)
|
|
229
272
|
// 桌面端 `/export` 其实是两步:命令本身只留一行 transcript 标记,真正的下载由
|
|
230
273
|
// @deepseek-ai/dsh-session-log-export 在浏览器侧监听 command/executed 后拉
|
|
231
274
|
// `GET /api/session.export` 完成。手机端没有浏览器下载管理器,而那个端点绑在宿主
|
|
@@ -242,6 +285,16 @@ function registerMobileRoutes(deps) {
|
|
|
242
285
|
// 改设置:与桌面端同一个 settings.mutate(按路径寻址的 op + expectedRevision 乐观并发)。
|
|
243
286
|
route('/api/mobile/v1/host-settings/mutate', hostSettings.handleHostSettingsMutate)
|
|
244
287
|
route('/api/mobile/v1/llm/providers', hostSettings.handleLlmProviders)
|
|
288
|
+
// 宿主的模型目录(`llm.models`):每个提供方下面真正可请求的模型 id,给「用哪个模型做
|
|
289
|
+
// AI commit」这类选择器用 —— 它不需要活动会话,与 `/session/models` 不是一回事。
|
|
290
|
+
route('/api/mobile/v1/llm/models', hostSettings.handleLlmModels)
|
|
291
|
+
// 渠道管理(见 mobile/channels.js):只对「本机自建」那一类(`llm-pi-ai` 里的路由)
|
|
292
|
+
// 提供增删;插件 / 宿主注册的提供方只列出来,一个写操作都不做。获取模型由宿主代问
|
|
293
|
+
// (端点常在内网,手机直连未必通),与桌面面板的 `discoverModels` 同一条路。
|
|
294
|
+
route('/api/mobile/v1/channels', channels.handleChannels)
|
|
295
|
+
route('/api/mobile/v1/channels/discover', channels.handleChannelDiscover)
|
|
296
|
+
route('/api/mobile/v1/channels/save', channels.handleChannelSave)
|
|
297
|
+
route('/api/mobile/v1/channels/delete', channels.handleChannelDelete)
|
|
245
298
|
// 技能目录(宿主 skill.list 的移动端只读代理)。桌面端敲「/」时命令候选菜单里
|
|
246
299
|
// 混着技能(@deepseek-ai/dsh-client-ui-skill 注册的 inputTriggers source),
|
|
247
300
|
// 手机端此前只有 commands.list 的命令目录,技能一个都看不到,见 mobile/skills.js。
|
|
@@ -268,6 +321,15 @@ function registerMobileRoutes(deps) {
|
|
|
268
321
|
route('/api/mobile/v1/file-references', references.handleFileReferences)
|
|
269
322
|
route('/api/mobile/v1/session-references', references.handleSessionReferences)
|
|
270
323
|
|
|
324
|
+
// 定时任务(宿主 ScheduleService 的移动端代理):目录、会话内 active 任务、运行记录、
|
|
325
|
+
// 删除。桌面端有一套完整的定时任务面板,手机端此前一条都看不到;服务同进程直查
|
|
326
|
+
// `ctx.get('schedule')`,宿主没有它时这四条统一回 503 而不是空列表,
|
|
327
|
+
// 见 mobile/schedule.js。`update` 这一批没接:手机端还没有编辑入口。
|
|
328
|
+
route('/api/mobile/v1/schedule/catalog', schedule.handleScheduleCatalog)
|
|
329
|
+
route('/api/mobile/v1/schedule/list', schedule.handleScheduleList)
|
|
330
|
+
route('/api/mobile/v1/schedule/history', schedule.handleScheduleHistory)
|
|
331
|
+
route('/api/mobile/v1/schedule/delete', schedule.handleScheduleDelete)
|
|
332
|
+
|
|
271
333
|
// 桌面端 设置 → 插件 的只读部署快照,由 dsh-host-plugin-inventory 拥有,
|
|
272
334
|
// 经 typertGateway 命名空间 pluginInventory 取,见 mobile/plugins.js。
|
|
273
335
|
route('/api/mobile/v1/plugins', plugins.handlePlugins)
|
|
@@ -288,6 +350,13 @@ function registerMobileRoutes(deps) {
|
|
|
288
350
|
route('/api/mobile/v1/upload/chunk', uploads.handleUploadChunk)
|
|
289
351
|
route('/api/mobile/v1/upload/finish', uploads.handleUploadFinish)
|
|
290
352
|
route('/api/mobile/v1/upload/abort', uploads.handleUploadAbort)
|
|
353
|
+
// 同一个收件目录的出口:上面四条只往里放,这两条报占用、按龄清掉(见 mobile/inbox-clean.js)。
|
|
354
|
+
// 落盘的文件不认识会话,所以没有「删掉这条对话就顺带清掉它传上来的文件」这条路 ——
|
|
355
|
+
// 唯一的出口只能按龄,口径写在那个文件头。
|
|
356
|
+
route('/api/mobile/v1/inbox/status', inboxClean.handleInboxStatus)
|
|
357
|
+
route('/api/mobile/v1/inbox/clear', inboxClean.handleInboxClear)
|
|
358
|
+
// 自动清理开关:默认关,打开后宿主每次启动顺手清一次(面板那颗按钮不受它管)。
|
|
359
|
+
route('/api/mobile/v1/inbox/auto', inboxClean.handleInboxAuto)
|
|
291
360
|
|
|
292
361
|
// 内置浏览器预览(App 侧「点 localhost 链接在内置浏览器里打开」的宿主端一半)。
|
|
293
362
|
//
|
package/lib/session-cleanup.cjs
CHANGED
|
@@ -46,6 +46,8 @@ const HEADER_MAX_BYTES = 64 * 1024
|
|
|
46
46
|
|
|
47
47
|
const { envelopeValue, unwrap } = require('./mobile/envelope.js')
|
|
48
48
|
const { rememberDeletedSessionIds } = require('./mobile/session-cache.js')
|
|
49
|
+
// 收件目录的启动清理与这里共用同一个「宿主刚起来」的窗口,实现见 mobile/inbox-clean.js。
|
|
50
|
+
const { clearInboxOnce } = require('./mobile/inbox-clean.js')
|
|
49
51
|
|
|
50
52
|
function asItems(response) {
|
|
51
53
|
const value = envelopeValue(response)
|
|
@@ -469,6 +471,12 @@ function installSessionCleanup(ctx) {
|
|
|
469
471
|
void pruneOrphanedArchivedSessionsOnce(ctx, (message) => {
|
|
470
472
|
try { ctx.logger?.info?.(message) } catch { /* 日志失败不影响修剪 */ }
|
|
471
473
|
})
|
|
474
|
+
// 收件目录(手机分享进来的文件)也在这里顺手清一次:它只进不出,不点按钮就没人管。
|
|
475
|
+
// 与上面那条同一个「宿主刚起来」的窗口,但两者互不依赖 —— 收件目录的口径只看 mtime,
|
|
476
|
+
// 碰不到任何会话日志(见 mobile/inbox-clean.js 的文件头)。
|
|
477
|
+
void clearInboxOnce((message) => {
|
|
478
|
+
try { ctx.logger?.info?.(message) } catch { /* 日志失败不影响清理 */ }
|
|
479
|
+
})
|
|
472
480
|
}
|
|
473
481
|
registerSessionCleanupTool(ctx)
|
|
474
482
|
}
|
package/lib/shared/sse-write.js
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* 事件 id 怎么算」「写不动时怎么等 drain」都只在这里,两条流只在调用点上声明自己的策略。
|
|
12
12
|
*
|
|
13
13
|
* 两条流刻意保留的差异(不是遗漏,改之前先看各自的理由):
|
|
14
|
-
* -
|
|
15
|
-
* 收尾的 `assistant/message` 带完整正文、重连 `replaySince`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* - 丢帧口径:会话流的软水位只丢**明确可丢的增量**(只有 `assistant/chunk` 草稿:同一轮
|
|
15
|
+
* 收尾的 `assistant/message` 带完整正文、重连 `replaySince` 能补齐)。`session/queue`、
|
|
16
|
+
* `session/jobs`、`session/resync-required` 这些控制帧没有 seq,但绝不能丢;设备级流的
|
|
17
|
+
* 帧全是「有东西变了」的幂等信号,软水位丢弃后由调用方**断流重连**并用
|
|
18
|
+
* `replayKnownSignals()` 补发。由调用方传 `shedWhenBuffered`。
|
|
18
19
|
* - 写不动时:会话流 `await` 一次 drain(20s 超时再收尾);设备级流直接收尾让客户端
|
|
19
20
|
* 重连 —— 它没有 Last-Event-ID 续传,但连上时会 `replayKnownSignals()` 补发当前状态,
|
|
20
21
|
* 所以「断开重连」是它的正常收敛路径。
|
|
@@ -42,8 +43,18 @@ const SSE_CLOSE_BUFFERED_BYTES = 16 * 1024 * 1024;
|
|
|
42
43
|
*/
|
|
43
44
|
const SSE_DRAIN_TIMEOUT_MS = 20_000;
|
|
44
45
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* 默认丢帧口径:只有**明确可丢**的无 seq 增量帧可以丢;有 seq 的一律不能丢。
|
|
48
|
+
*
|
|
49
|
+
* 旧实现是「没有 seq 就丢」,结果把 `session/queue` / `session/jobs` /
|
|
50
|
+
* `session/resync-required` 这些控制帧一起丢了 —— 它们没有 seq,但丢了没有回放路径,
|
|
51
|
+
* 队列/任务面板会永久停在旧状态,gap 提示也到不了客户端。
|
|
52
|
+
*/
|
|
53
|
+
const SHEDDABLE_INCREMENTAL_TYPES = new Set(['assistant/chunk']);
|
|
54
|
+
const shedWithoutSeq = (payload) => {
|
|
55
|
+
if (!payload || !SHEDDABLE_INCREMENTAL_TYPES.has(payload.type)) return false;
|
|
56
|
+
return !Number.isFinite(payload.seq);
|
|
57
|
+
};
|
|
47
58
|
|
|
48
59
|
/** 每条记录后面补的 SSE 注释(客户端忽略内容,只用来把小 chunk 撑过 RN 的缓冲阈值)。 */
|
|
49
60
|
function ssePadding(bytes) {
|
package/lib/shell/updater.js
CHANGED
|
@@ -312,13 +312,13 @@ function autoUpdateFile() {
|
|
|
312
312
|
return path.join(dshHome(), 'storages', 'mobile-plugin', 'auto-update.json')
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
/**
|
|
315
|
+
/** 未设置过默认自动更新;显式关闭保持关闭,配置损坏时不改动盘上插件。 */
|
|
316
316
|
export function autoUpdateEnabled() {
|
|
317
317
|
try {
|
|
318
318
|
const raw = JSON.parse(fs.readFileSync(autoUpdateFile(), 'utf8'))
|
|
319
319
|
return raw?.enabled === true
|
|
320
|
-
} catch {
|
|
321
|
-
return
|
|
320
|
+
} catch (error) {
|
|
321
|
+
return error?.code === 'ENOENT'
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
324
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mobile-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.97",
|
|
4
4
|
"description": "Mobile API (/api/mobile/v1) and relay client for the DSH mobile app — desktop profile build (0.2.0-rc.2 compatible, peers resolved by the host)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"test": "node --test test/*.test.cjs && node test/lan-direct-e2e.cjs && node test/sidecar-facade.cjs"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@deepseek-ai/dsh-tool-ask-user": "0.2.
|
|
48
|
+
"@deepseek-ai/dsh-tool-ask-user": "0.2.1-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-tool-present": "0.2.1-rc.2",
|
|
49
50
|
"qrcode": "1.5.4",
|
|
50
51
|
"ws": "8.18.0"
|
|
51
52
|
},
|