dsh-mini-remote 0.1.7 → 0.1.8

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/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,13 +251,34 @@ 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
- const items = await withTitleSlot(() => query.readTitleSnapshots(todo))
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') continue // 读失败不进缓存,下次还会重试
218
- rememberTitle(cache, item.sessionId, titleText(item.value?.title), persisted)
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
  }
@@ -419,7 +479,7 @@ function shapeWorkspace(ws) {
419
479
  */
420
480
  export async function listSessionsOf({
421
481
  registry, query, agents, knownTitles, persisted, workspaceId,
422
- limit = SESSIONS_PER_WORKSPACE, force,
482
+ limit = SESSIONS_PER_WORKSPACE, force, onTitle,
423
483
  }) {
424
484
  const ws = safeList(registry).find((w) => w.id === workspaceId)
425
485
  if (!ws) return null
@@ -443,6 +503,16 @@ export async function listSessionsOf({
443
503
  rememberTitle(cache, id, known, persisted)
444
504
  continue
445
505
  }
506
+ const legacy = legacyTitle(id)
507
+ if (legacy !== null) {
508
+ titles.set(id, legacy)
509
+ rememberTitle(cache, id, legacy, persisted)
510
+ continue
511
+ }
512
+ if (failedTitle(query, id)) {
513
+ titles.set(id, '')
514
+ continue
515
+ }
446
516
  const hit = cachedTitle(cache, id, persisted)
447
517
  if (hit === null) missing.push(id)
448
518
  else if (hit) titles.set(id, hit)
@@ -451,7 +521,7 @@ export async function listSessionsOf({
451
521
  // 那几条先显示日期。缓存里已有的照旧直接用,所以反复展开同一个工作区始终是快的。
452
522
  if (missing.length && query?.readTitleSnapshots) {
453
523
  await Promise.race([
454
- fillTitles(query, cache, missing, persisted),
524
+ fillTitles(query, cache, missing, persisted, workspaceId, onTitle),
455
525
  new Promise((resolve) => setTimeout(resolve, TITLE_WAIT_MS)),
456
526
  ])
457
527
  for (const id of missing) {
@@ -476,7 +546,7 @@ export async function listSessionsOf({
476
546
  if (typeof query?.readTitleSnapshots === 'function') {
477
547
  for (const r of rows) {
478
548
  const id = r.header.id
479
- if (!titles.has(id) && cachedTitle(cache, id, persisted) === null) pending += 1
549
+ if (!titles.has(id) && !failedTitle(query, id) && cachedTitle(cache, id, persisted) === null) pending += 1
480
550
  }
481
551
  }
482
552
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mini-remote",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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
  }