dsh-mini-remote 0.1.7 → 0.1.9
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 +49 -10
- package/README.en.md +33 -1
- package/README.md +33 -1
- package/client/client.js +272 -1
- package/lib/commands.js +6 -0
- package/lib/events.js +36 -5
- package/lib/history.js +21 -3
- package/lib/index.js +1323 -19
- package/lib/lark.js +1154 -0
- package/lib/page.html +2658 -136
- package/lib/page.js +17 -3
- package/lib/pairing.js +32 -3
- package/lib/permissions.js +1 -0
- package/lib/server.js +522 -30
- package/lib/session-meta-cache.js +100 -0
- package/lib/store.js +253 -2
- package/lib/subagent-cache.js +63 -0
- package/lib/trajectory.js +570 -0
- package/lib/tree.js +107 -15
- package/package.json +2 -1
package/lib/tree.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* 它的会话(要读日志折标题,贵)。
|
|
16
16
|
*/
|
|
17
17
|
import { basename } from 'node:path'
|
|
18
|
+
import { readTailEvents } from './log-tail.js'
|
|
18
19
|
|
|
19
20
|
/** 一个工作区默认最多列多少会话。手机上再多也划不动。 */
|
|
20
21
|
export const SESSIONS_PER_WORKSPACE = 20
|
|
@@ -128,6 +129,8 @@ function rememberTitle(cache, id, title, persisted) {
|
|
|
128
129
|
* 手机页面自己)抢。而且这些标题本来就没人等着,晚几百毫秒没有任何代价。
|
|
129
130
|
*/
|
|
130
131
|
export const TITLE_CONCURRENCY = 2
|
|
132
|
+
const TITLE_READ_TIMEOUT_MS = 8_000
|
|
133
|
+
const TITLE_RETRY_MS = 60_000
|
|
131
134
|
|
|
132
135
|
let titleRunning = 0
|
|
133
136
|
const titleWaiting = []
|
|
@@ -192,6 +195,42 @@ function cachedTitle(cache, id, persisted) {
|
|
|
192
195
|
* 同一批冷会话会被读两遍。这里挡住重复的在途请求,读完即释放(失败也释放)。
|
|
193
196
|
*/
|
|
194
197
|
const titleInflight = new WeakMap()
|
|
198
|
+
const titleFailures = new WeakMap()
|
|
199
|
+
|
|
200
|
+
function legacyTitle(sessionId) {
|
|
201
|
+
if (typeof sessionId !== 'string' || !sessionId.startsWith('import-')) return null
|
|
202
|
+
try {
|
|
203
|
+
const result = readTailEvents(sessionId, { windowBytes: 1024 * 1024, maxEvents: 1000 })
|
|
204
|
+
for (const event of result?.events ?? []) {
|
|
205
|
+
if (event?.type !== 'session/title') continue
|
|
206
|
+
const title = event.data?.title ?? event.data?.text
|
|
207
|
+
if (typeof title === 'string') return title.trim()
|
|
208
|
+
}
|
|
209
|
+
return ''
|
|
210
|
+
} catch {
|
|
211
|
+
return ''
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function markTitleFailure(query, ids) {
|
|
216
|
+
let cache = titleFailures.get(query)
|
|
217
|
+
if (!cache) {
|
|
218
|
+
cache = new Map()
|
|
219
|
+
titleFailures.set(query, cache)
|
|
220
|
+
}
|
|
221
|
+
const until = Date.now() + TITLE_RETRY_MS
|
|
222
|
+
for (const id of ids) cache.set(id, until)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function failedTitle(query, id) {
|
|
226
|
+
const until = titleFailures.get(query)?.get(id)
|
|
227
|
+
if (!until) return false
|
|
228
|
+
if (until <= Date.now()) {
|
|
229
|
+
titleFailures.get(query).delete(id)
|
|
230
|
+
return false
|
|
231
|
+
}
|
|
232
|
+
return true
|
|
233
|
+
}
|
|
195
234
|
|
|
196
235
|
/**
|
|
197
236
|
* 后台把缺的标题读出来,写进缓存。**调用方不 await 它**。
|
|
@@ -200,7 +239,7 @@ const titleInflight = new WeakMap()
|
|
|
200
239
|
* 而读它要加载会话日志——实测一个工作区里几条冷会话就是十几秒。那十几秒是用户在
|
|
201
240
|
* 盯着"正在读取…"干等,不该发生。
|
|
202
241
|
*/
|
|
203
|
-
async function fillTitles(query, cache, ids, persisted) {
|
|
242
|
+
async function fillTitles(query, cache, ids, persisted, workspaceId, onTitle) {
|
|
204
243
|
if (!query?.readTitleSnapshots || !ids.length) return
|
|
205
244
|
let flying = titleInflight.get(query)
|
|
206
245
|
if (!flying) {
|
|
@@ -212,33 +251,72 @@ async function fillTitles(query, cache, ids, persisted) {
|
|
|
212
251
|
for (const id of todo) flying.add(id)
|
|
213
252
|
try {
|
|
214
253
|
// 读之前先过全局闸门(见 TITLE_CONCURRENCY):这一步是这次改动里唯一真正吃 CPU 的地方。
|
|
215
|
-
|
|
254
|
+
let timer
|
|
255
|
+
const controller = new AbortController()
|
|
256
|
+
let items
|
|
257
|
+
try {
|
|
258
|
+
timer = setTimeout(() => controller.abort(), TITLE_READ_TIMEOUT_MS)
|
|
259
|
+
items = await withTitleSlot(() => Promise.race([
|
|
260
|
+
query.readTitleSnapshots(todo, controller.signal),
|
|
261
|
+
new Promise((_, reject) => controller.signal.addEventListener(
|
|
262
|
+
'abort', () => reject(new Error('标题读取超时')), { once: true },
|
|
263
|
+
)),
|
|
264
|
+
]))
|
|
265
|
+
} catch {
|
|
266
|
+
markTitleFailure(query, todo)
|
|
267
|
+
return
|
|
268
|
+
} finally {
|
|
269
|
+
if (timer) clearTimeout(timer)
|
|
270
|
+
}
|
|
216
271
|
for (const item of items ?? []) {
|
|
217
|
-
if (item?.status !== 'fulfilled')
|
|
218
|
-
|
|
272
|
+
if (item?.status !== 'fulfilled') {
|
|
273
|
+
markTitleFailure(query, [item?.sessionId].filter(Boolean))
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
const title = titleText(item.value?.title)
|
|
277
|
+
rememberTitle(cache, item.sessionId, title, persisted)
|
|
278
|
+
if (typeof onTitle === 'function') onTitle({ workspaceId, sessionId: item.sessionId, title })
|
|
219
279
|
}
|
|
220
280
|
} catch (err) {
|
|
221
|
-
|
|
281
|
+
markTitleFailure(query, todo)
|
|
222
282
|
} finally {
|
|
223
283
|
for (const id of todo) flying.delete(id)
|
|
224
284
|
}
|
|
225
285
|
}
|
|
226
286
|
|
|
227
|
-
async function safeRecords(query, { force = false } = {}) {
|
|
287
|
+
async function safeRecords(query, { force = false, meta } = {}) {
|
|
228
288
|
if (!query) return []
|
|
229
289
|
let entry = caches.get(query)
|
|
230
290
|
if (!entry) {
|
|
231
291
|
entry = { at: 0, records: null, inflight: null }
|
|
232
292
|
caches.set(query, entry)
|
|
233
293
|
}
|
|
294
|
+
if (!force && !entry.records && meta?.all) {
|
|
295
|
+
const cached = meta.all()
|
|
296
|
+
if (cached.length) {
|
|
297
|
+
entry.records = cached
|
|
298
|
+
// 这份数据只负责冷启动首屏,后台立刻用 DSH 清单校正。
|
|
299
|
+
entry.at = 0
|
|
300
|
+
}
|
|
301
|
+
}
|
|
234
302
|
if (!force && entry.records && Date.now() - entry.at < CACHE_TTL_MS) return entry.records
|
|
235
|
-
//
|
|
303
|
+
// 有旧记录时直接返回,同时只启动一趟后台校正;没有旧记录才等待首次读取。
|
|
304
|
+
if (entry.records && !force) {
|
|
305
|
+
if (!entry.inflight) entry.inflight = refreshRecords(query, entry, meta)
|
|
306
|
+
return entry.records
|
|
307
|
+
}
|
|
236
308
|
if (entry.inflight) return entry.inflight
|
|
309
|
+
entry.inflight = refreshRecords(query, entry, meta)
|
|
310
|
+
return entry.inflight
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function refreshRecords(query, entry, meta) {
|
|
237
314
|
entry.inflight = (async () => {
|
|
238
315
|
try {
|
|
239
316
|
const records = (await query.listSessions()) ?? []
|
|
240
317
|
entry.at = Date.now()
|
|
241
318
|
entry.records = records
|
|
319
|
+
meta?.replace(records)
|
|
242
320
|
return records
|
|
243
321
|
} catch (err) {
|
|
244
322
|
// 读不到就当作没有:导航栏少几项,不该影响遥控本身。失败不进缓存,下次还会重试。
|
|
@@ -290,8 +368,8 @@ function titleText(value) {
|
|
|
290
368
|
* (闸门脚本就是这么逮到的:接口回 created=true,列表里却找不到。)
|
|
291
369
|
* 默认仍然是「滤掉」,桌面端那些老调用方的行为一点不变。
|
|
292
370
|
*/
|
|
293
|
-
export async function listWorkspaces({ registry, query, runningIds, force, includeEmpty }) {
|
|
294
|
-
const byId = toRecordMap(await safeRecords(query, { force }))
|
|
371
|
+
export async function listWorkspaces({ registry, query, runningIds, force, includeEmpty, meta }) {
|
|
372
|
+
const byId = toRecordMap(await safeRecords(query, { force, meta }))
|
|
295
373
|
const out = []
|
|
296
374
|
for (const ws of safeList(registry)) {
|
|
297
375
|
const ids = (ws.sessionIds ?? []).filter((id) => visible(byId.get(id)))
|
|
@@ -328,7 +406,7 @@ export async function listWorkspaces({ registry, query, runningIds, force, inclu
|
|
|
328
406
|
* - `no-workspace` 工作区不在了(桌面端刚把它删掉)
|
|
329
407
|
* - `failed` DSH 拒绝了,原因原样带回
|
|
330
408
|
*/
|
|
331
|
-
export async function createSessionIn({ controller, registry, workspaceId }) {
|
|
409
|
+
export async function createSessionIn({ controller, registry, workspaceId, agentPreset }) {
|
|
332
410
|
if (!controller || typeof controller.create !== 'function') {
|
|
333
411
|
return { ok: false, reason: 'no-controller' }
|
|
334
412
|
}
|
|
@@ -337,7 +415,11 @@ export async function createSessionIn({ controller, registry, workspaceId }) {
|
|
|
337
415
|
|
|
338
416
|
let created
|
|
339
417
|
try {
|
|
340
|
-
|
|
418
|
+
// agentPreset(2026-10-04,手机上也能选模式):**选了才带**——没选就让 DSH 用它
|
|
419
|
+
// 自己的默认,不猜、不塞一个假值。
|
|
420
|
+
created = await controller.create(agentPreset
|
|
421
|
+
? { workspaceId, agentPreset }
|
|
422
|
+
: { workspaceId })
|
|
341
423
|
} catch (err) {
|
|
342
424
|
return { ok: false, reason: 'failed', error: String(err?.message ?? err) }
|
|
343
425
|
}
|
|
@@ -419,12 +501,12 @@ function shapeWorkspace(ws) {
|
|
|
419
501
|
*/
|
|
420
502
|
export async function listSessionsOf({
|
|
421
503
|
registry, query, agents, knownTitles, persisted, workspaceId,
|
|
422
|
-
limit = SESSIONS_PER_WORKSPACE, force,
|
|
504
|
+
limit = SESSIONS_PER_WORKSPACE, force, onTitle, meta,
|
|
423
505
|
}) {
|
|
424
506
|
const ws = safeList(registry).find((w) => w.id === workspaceId)
|
|
425
507
|
if (!ws) return null
|
|
426
508
|
|
|
427
|
-
const byId = toRecordMap(await safeRecords(query, { force }))
|
|
509
|
+
const byId = toRecordMap(await safeRecords(query, { force, meta }))
|
|
428
510
|
const rows = (ws.sessionIds ?? [])
|
|
429
511
|
.map((id) => byId.get(id))
|
|
430
512
|
.filter(visible)
|
|
@@ -443,6 +525,16 @@ export async function listSessionsOf({
|
|
|
443
525
|
rememberTitle(cache, id, known, persisted)
|
|
444
526
|
continue
|
|
445
527
|
}
|
|
528
|
+
const legacy = legacyTitle(id)
|
|
529
|
+
if (legacy !== null) {
|
|
530
|
+
titles.set(id, legacy)
|
|
531
|
+
rememberTitle(cache, id, legacy, persisted)
|
|
532
|
+
continue
|
|
533
|
+
}
|
|
534
|
+
if (failedTitle(query, id)) {
|
|
535
|
+
titles.set(id, '')
|
|
536
|
+
continue
|
|
537
|
+
}
|
|
446
538
|
const hit = cachedTitle(cache, id, persisted)
|
|
447
539
|
if (hit === null) missing.push(id)
|
|
448
540
|
else if (hit) titles.set(id, hit)
|
|
@@ -451,7 +543,7 @@ export async function listSessionsOf({
|
|
|
451
543
|
// 那几条先显示日期。缓存里已有的照旧直接用,所以反复展开同一个工作区始终是快的。
|
|
452
544
|
if (missing.length && query?.readTitleSnapshots) {
|
|
453
545
|
await Promise.race([
|
|
454
|
-
fillTitles(query, cache, missing, persisted),
|
|
546
|
+
fillTitles(query, cache, missing, persisted, workspaceId, onTitle),
|
|
455
547
|
new Promise((resolve) => setTimeout(resolve, TITLE_WAIT_MS)),
|
|
456
548
|
])
|
|
457
549
|
for (const id of missing) {
|
|
@@ -476,7 +568,7 @@ export async function listSessionsOf({
|
|
|
476
568
|
if (typeof query?.readTitleSnapshots === 'function') {
|
|
477
569
|
for (const r of rows) {
|
|
478
570
|
const id = r.header.id
|
|
479
|
-
if (!titles.has(id) && cachedTitle(cache, id, persisted) === null) pending += 1
|
|
571
|
+
if (!titles.has(id) && !failedTitle(query, id) && cachedTitle(cache, id, persisted) === null) pending += 1
|
|
480
572
|
}
|
|
481
573
|
}
|
|
482
574
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mini-remote",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "DSH 极简移动端遥控器:只把「你的指令」和「AI 的最终回复」送到手机,屏蔽全部中间过程。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
+
"@larksuiteoapi/node-sdk": "^1.74.0",
|
|
81
82
|
"qrcode": "^1.5.4"
|
|
82
83
|
}
|
|
83
84
|
}
|