@ssamurias/dsh-fish 0.9.1 → 0.9.3
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/AGENTS.md +27 -2
- package/CHANGELOG.md +94 -0
- package/README.md +8 -2
- package/RELEASE-NOTES-0.9.2.md +46 -0
- package/RELEASE-NOTES-0.9.3.md +91 -0
- package/RELEASE.md +6 -5
- package/lib/absence.js +41 -27
- package/lib/anchor.js +57 -0
- package/lib/ask.js +73 -25
- package/lib/askinchat.js +83 -1
- package/lib/browse.js +8 -0
- package/lib/capture.js +19 -1
- package/lib/cardanswer.js +84 -0
- package/lib/client.js +90 -13
- package/lib/comment.js +189 -30
- package/lib/deliver.js +46 -0
- package/lib/derive.js +14 -11
- package/lib/desk.js +33 -1
- package/lib/drive.js +19 -3
- package/lib/events.js +25 -1
- package/lib/gapwall.js +73 -0
- package/lib/generator.js +2 -1
- package/lib/grant.js +18 -2
- package/lib/home.js +55 -7
- package/lib/identity.js +111 -14
- package/lib/index.js +104 -54
- package/lib/keep.js +21 -9
- package/lib/keepsake.js +20 -13
- package/lib/note.js +255 -20
- package/lib/observe.js +9 -5
- package/lib/persona.js +47 -33
- package/lib/proactive.js +45 -16
- package/lib/quality.js +32 -8
- package/lib/recall.js +33 -2
- package/lib/redact.js +70 -0
- package/lib/restrict.js +40 -2
- package/lib/rules.js +1 -1
- package/lib/say.js +79 -13
- package/lib/scheduler.js +107 -41
- package/lib/schema.js +18 -0
- package/lib/selfdoc.js +19 -7
- package/lib/speak.js +16 -11
- package/lib/status.js +25 -3
- package/lib/turns.js +42 -1
- package/lib/use.js +212 -0
- package/lib/wish.js +147 -0
- package/package.json +3 -2
package/lib/index.js
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* dispose → 停定时器、注销路由。停用插件 = 零后台活动。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
14
|
+
|
|
13
15
|
import { rootDir, readEvents, appendEvent, readJson, paths, writeAtomic, ensureHer } from './store.js'
|
|
14
16
|
import { loadConfig } from './settings.js'
|
|
15
17
|
import { deriveSelf } from './derive.js'
|
|
@@ -23,6 +25,7 @@ import { registerLetter } from './letter.js'
|
|
|
23
25
|
import { registerNote } from './note.js'
|
|
24
26
|
import { registerBringBack } from './keep.js'
|
|
25
27
|
import { registerAsk } from './ask.js'
|
|
28
|
+
import { answerCard } from './cardanswer.js'
|
|
26
29
|
import { registerDream } from './dream.js'
|
|
27
30
|
import { registerProactive } from './proactive.js'
|
|
28
31
|
import { registerObserve } from './observe.js'
|
|
@@ -38,6 +41,7 @@ import { registerToolRestrict } from './restrict.js'
|
|
|
38
41
|
// 停用时不清的话下次启用会带着上一次的残留(2026-10-25)。
|
|
39
42
|
import { clearWakeSection, registerIdentity } from './identity.js'
|
|
40
43
|
import { registerPossession } from './possess.js'
|
|
44
|
+
import { withUseDuringRegistration } from './use.js'
|
|
41
45
|
|
|
42
46
|
import { checkUpgrade } from './upgrade.js'
|
|
43
47
|
import { checkHost, readHostVersion } from './hostver.js'
|
|
@@ -45,8 +49,11 @@ import { logBoot, makeLogger } from './log.js'
|
|
|
45
49
|
|
|
46
50
|
export const name = 'dsh-fish'
|
|
47
51
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
52
|
+
/**
|
|
53
|
+
* 版本号,必须和 package.json 的 version 一样(test/boot.test.mjs 盯着)。
|
|
54
|
+
* 不只是日志:升级自检和设置页的「检查更新」拿它当「正在跑的版本」。
|
|
55
|
+
*/
|
|
56
|
+
export const VERSION = '0.9.3'
|
|
50
57
|
|
|
51
58
|
// ⚠️ 一个数组写全,不要拆成对象
|
|
52
59
|
// 2026-10-30(m02961):`userQuestions` **不再写进 inject**。写进 inject 的东西缺一个,
|
|
@@ -81,6 +88,11 @@ export function apply(ctx, baseConfig = {}) {
|
|
|
81
88
|
// 写进 inject 的代价是「缺了就整个插件不装」——而缺的那个服务只关掉一条功能。
|
|
82
89
|
const compat = { checked: false, userQuestions: false }
|
|
83
90
|
let heartbeat = null
|
|
91
|
+
let stopped = false
|
|
92
|
+
// 正在认领的那次:shutdown 要等它放开,新实例才不会把旧认领读成「被占着」
|
|
93
|
+
let claiming = null
|
|
94
|
+
// 在途的那次心跳:放开之前要等它,不然它会把放开的认领写回去
|
|
95
|
+
let beating = null
|
|
84
96
|
|
|
85
97
|
async function boot() {
|
|
86
98
|
logBoot(root, `${VERSION} · ${process.version} · ${process.platform}`)
|
|
@@ -119,21 +131,24 @@ export function apply(ctx, baseConfig = {}) {
|
|
|
119
131
|
// 她自己的房间必须先存在:她的会话 cwd 指向这里(不是 root),
|
|
120
132
|
// 这样她不会一睁眼就站在自己的 events.jsonl 旁边。
|
|
121
133
|
const herDir = await ensureHer(root)
|
|
134
|
+
if (stopped) return
|
|
122
135
|
|
|
123
136
|
// ---- 一个 root 只认一个实例(2026-10-28 m01319)----
|
|
124
137
|
// root 是进程级解析的:两个工作区如果都落到同一个家,光看调用者 cwd 分辨不出来
|
|
125
138
|
// (两边 cwd 都在这个 root 里,校验会同时通过)。所以加载时先占位:
|
|
126
139
|
// 谁先占谁是房主;后进来的认出来之后**不写也不崩** —— 她还能说话,只是不落盘。
|
|
127
140
|
try {
|
|
128
|
-
|
|
141
|
+
claiming = claimRoot(root, { version: VERSION })
|
|
142
|
+
const claim = await claiming
|
|
129
143
|
tenancy.tenant = claim
|
|
130
144
|
tenancy.herDir = herDir
|
|
145
|
+
if (stopped) return
|
|
131
146
|
if (!claim.ok) {
|
|
132
147
|
logger.error(`[dsh-fish] ${claim.why}`)
|
|
133
148
|
} else if (claim.instanceId) {
|
|
134
149
|
const id = claim.instanceId
|
|
135
150
|
heartbeat = setInterval(() => {
|
|
136
|
-
beatRoot(root, id)
|
|
151
|
+
beating = beatRoot(root, id)
|
|
137
152
|
.then((still) => {
|
|
138
153
|
if (still === false && tenancy.tenant.ok !== false) {
|
|
139
154
|
// 续不上 = 家被别人接管了。从此不再写,也不再抢回来(否则两边来回覆盖)。
|
|
@@ -185,6 +200,7 @@ export function apply(ctx, baseConfig = {}) {
|
|
|
185
200
|
// 首次启动播种:她最初的那一点点性格,是**第一个事实**,不是可调参数。
|
|
186
201
|
// 之所以写成事件而不是配置项:配置项会被"打开设置页改一下",
|
|
187
202
|
// 事件只能追加——想改就得由她自己 revise,这是 N4 的地基。
|
|
203
|
+
if (stopped) return
|
|
188
204
|
const hasSeed = events.some((e) => e.type === 'seed')
|
|
189
205
|
if (!hasSeed) {
|
|
190
206
|
const seedText = String(
|
|
@@ -209,6 +225,7 @@ export function apply(ctx, baseConfig = {}) {
|
|
|
209
225
|
}
|
|
210
226
|
|
|
211
227
|
const model = deriveSelf(await readEvents(root).then((r) => r.events), { growthDays: config.growthDays })
|
|
228
|
+
if (stopped) return
|
|
212
229
|
|
|
213
230
|
// ---- 她的屋子:工作区 + 专属会话(P3 核心)----
|
|
214
231
|
// 这一步**先于**一切:她得先有个能被你看见的地方,才有"说话"这件事。
|
|
@@ -244,6 +261,7 @@ export function apply(ctx, baseConfig = {}) {
|
|
|
244
261
|
} catch (err) {
|
|
245
262
|
logger.warn('[dsh-fish] 身份文本预写失败', err?.message ?? err)
|
|
246
263
|
}
|
|
264
|
+
if (stopped) return
|
|
247
265
|
try {
|
|
248
266
|
const off = registerIdentity(ctx, { root, herDir, log: (...a) => logger.info(...a) })
|
|
249
267
|
if (typeof off === 'function') disposers.push(off)
|
|
@@ -272,6 +290,7 @@ export function apply(ctx, baseConfig = {}) {
|
|
|
272
290
|
} catch (err) {
|
|
273
291
|
home = { ok: false, why: String(err?.message ?? err).slice(0, 160), wsId: '', sessionId: '' }
|
|
274
292
|
}
|
|
293
|
+
if (stopped) return
|
|
275
294
|
// 观测台要能把「这个家在别处也开着」和普通失败分开报(2026-10-29 m02162)。
|
|
276
295
|
tenancy.home = { ok: home.ok, blocked: home.blocked === true, sessionId: home.sessionId ?? '', why: home.why ?? '' }
|
|
277
296
|
if (home.blocked === true) logger.warn(`[dsh-fish] ${home.why}`)
|
|
@@ -287,6 +306,7 @@ export function apply(ctx, baseConfig = {}) {
|
|
|
287
306
|
}
|
|
288
307
|
}
|
|
289
308
|
|
|
309
|
+
if (stopped) return
|
|
290
310
|
// ---- 身份文本再落一次盘:屋子建好之后 activeCarry 等才齐,内容可能变 ----
|
|
291
311
|
// 这条改动之后它**不再被宿主注入**(那是旧的 AGENTS.md 通道),只是落盘副本;
|
|
292
312
|
// 真正进她上下文的是上面注册的 system prompt 段(每轮组装时现读文件)。
|
|
@@ -311,11 +331,14 @@ export function apply(ctx, baseConfig = {}) {
|
|
|
311
331
|
logger.warn(`[dsh-fish] 口袋排序失败:${String(err?.message ?? err).slice(0, 120)}`)
|
|
312
332
|
}
|
|
313
333
|
|
|
334
|
+
if (stopped) return
|
|
314
335
|
// ---- 侧栏轮数:在侧栏亲口跟她说话也算一轮(issue #6,2026-10-28)----
|
|
315
336
|
// 原来只有 /say 路由会 bumpTurns,用户直接打字时 rotateAfter 永不触发。
|
|
316
337
|
// 观察者只读会话事件,认来源是"真人打的",一轮结束再结账。
|
|
317
338
|
try {
|
|
318
|
-
|
|
339
|
+
// 2026-11-06(外部日志):被占用的实例也在写 `seen:`。轮数观察者现在是写盘方,
|
|
340
|
+
// 必须拿到活着的房主身份——占用权丢了(心跳续不上)会当场改成 ok:false。
|
|
341
|
+
const off = registerTurns(ctx, { root, tenant: tenancy.tenant, log: (...a) => logger.info(...a) })
|
|
319
342
|
if (typeof off === 'function') disposers.push(off)
|
|
320
343
|
} catch (err) {
|
|
321
344
|
logger.warn('[dsh-fish] 轮数观察没挂上', err?.message ?? err)
|
|
@@ -324,50 +347,56 @@ export function apply(ctx, baseConfig = {}) {
|
|
|
324
347
|
// 工具:一个写(growth_commit)、一个看(bili_look)。
|
|
325
348
|
// 两者都必须成功注册;缺了任何一个她都会变残(只会说不会看,或只会记不会看)。
|
|
326
349
|
if (ctx.tools?.register) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
root,
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
350
|
+
// 工具使用经验(2026-11-07,用户 m00882):注册期间把 register 换成
|
|
351
|
+
// 「包一层 execute 再交出去」的版本,用完立刻换回来。这样 12 个 registerXxx
|
|
352
|
+
// 一行都不用改,`defineTool` 的「定义原样交出去」约定也不动。
|
|
353
|
+
withUseDuringRegistration(ctx, root, () => {
|
|
354
|
+
for (const reg of [
|
|
355
|
+
() => registerCapture(ctx, { root, tenant: tenancy }),
|
|
356
|
+
// 2026-11-01(issue #16 第二半,用户 m03454):这两个以前没拿到 tenancy,
|
|
357
|
+
// 于是同机任何工作区加载这个插件都能替她写一条 looked:("她看过了")——
|
|
358
|
+
// 看过就是永久判决,而按下判决的人不留名字。现在与 growth_commit 同一条闸。
|
|
359
|
+
() => registerLook(ctx, { root, client: baseConfig.client, tenant: tenancy }),
|
|
360
|
+
// 工具说明随说话开关走(2026-10-29):关着时说明里不该许诺"可以留评论",
|
|
361
|
+
// 只说「这会儿还说不出去」(P3,2026-11-07:这一节本身不再消失)。
|
|
362
|
+
// 启动时按当时读到的配置定一次,之后由 execute 同步。
|
|
363
|
+
() => registerSpeak(ctx, {
|
|
364
|
+
root,
|
|
365
|
+
client: baseConfig.client,
|
|
366
|
+
tenant: tenancy,
|
|
367
|
+
speakEnabled: config?.comment?.enabled === true,
|
|
368
|
+
}),
|
|
369
|
+
() => registerRecall(ctx, { root }),
|
|
370
|
+
// issue #7:日记不进事件流,所以她需要一个只读的窗口回头翻(her_diary)。
|
|
371
|
+
() => registerHerDiary(ctx, { root }),
|
|
372
|
+
() => registerWatch(ctx, { root, tenant: tenancy }),
|
|
373
|
+
// R4:留话(不审批)。和 growth_commit 一样是纯追加,但收件人明确。
|
|
374
|
+
() => registerLetter(ctx, { root, tenant: tenancy }),
|
|
375
|
+
// issue #22 乙档(2026-11-04):醒来看的那张便条。写完会不会到期、
|
|
376
|
+
// 到期那天摆不摆在最前面,都由 lib/note.js 与 scheduler 的唤醒正文负责,
|
|
377
|
+
// 这个工具只做两件事:记一张、划掉一张。**常驻注册**,不像梦与主动说话那样要 .dev。
|
|
378
|
+
() => registerNote(ctx, { root, tenant: tenancy }),
|
|
379
|
+
// 0.6.17:纪念品。她只能「问」,真带回去要那个人在出门页点头。
|
|
380
|
+
() => registerBringBack(ctx, { root, tenant: tenancy }),
|
|
381
|
+
// 0.7.6(2026-10-30 m01274):请求事件。她有事想征得同意时弹一张卡,
|
|
382
|
+
// 每天 1 次、每周 3 次;只有 time / rest 真会改东西(见 lib/ask.js)。
|
|
383
|
+
() => registerAsk(ctx, { root, tenant: tenancy }),
|
|
384
|
+
// 0.8.1 起(2026-10-30 m02837):梦 A 案——醒来记一句。0.9.0 起公开。
|
|
385
|
+
() => registerDream(ctx, { root, tenant: tenancy }),
|
|
386
|
+
// 0.8.1 起(2026-10-30 m04610):主动说话——她自己判断有没有话想说。0.9.0 起公开。
|
|
387
|
+
// 2026-10-30 真机:这里原来只有 { root, log },guardWrite 拿到 undefined 的 state,
|
|
388
|
+
// checkCaller(undefined) 每次都判「不在这个家」;写入守卫要的是 tenancy。
|
|
389
|
+
() => registerProactive(ctx, { root, tenant: tenancy, log: (...a) => logger.info(...a) }),
|
|
390
|
+
]) {
|
|
391
|
+
try {
|
|
392
|
+
const d = reg()
|
|
393
|
+
if (typeof d === 'function') disposers.push(d)
|
|
394
|
+
else if (d && typeof d.dispose === 'function') disposers.push(() => d.dispose())
|
|
395
|
+
} catch (err) {
|
|
396
|
+
logger.warn('[dsh-fish] 工具注册失败', err?.message ?? err)
|
|
397
|
+
}
|
|
369
398
|
}
|
|
370
|
-
}
|
|
399
|
+
})
|
|
371
400
|
}
|
|
372
401
|
|
|
373
402
|
// 观测台路由
|
|
@@ -394,23 +423,44 @@ export function apply(ctx, baseConfig = {}) {
|
|
|
394
423
|
}
|
|
395
424
|
|
|
396
425
|
// 宿主可能不 await apply,所以自己兜住异常:启动失败不能带走整个插件树
|
|
397
|
-
boot().catch((err) => {
|
|
426
|
+
const booted = boot().catch((err) => {
|
|
398
427
|
logger.error('[dsh-fish] 启动失败', err?.stack ?? err)
|
|
399
428
|
console.error('[dsh-fish] 启动失败', err)
|
|
400
429
|
})
|
|
401
430
|
|
|
431
|
+
// 卡片被别的插件接走之后(dsh-fish/card),人的回答从这里回来(lib/cardanswer.js)。
|
|
432
|
+
// 同步挂上:宿主重启后 IM 插件可能先连上、先收到人的回答,这时启动还没走完。等它走完才知道这个家是不是自己的。
|
|
433
|
+
// 没认领到就不写,和那些工具同一条理由:被占着,或者启动没走到认领那一步(默认值 ok 是真的,不能信)。
|
|
434
|
+
ctx.on?.('dsh-fish/answer', async (answer) => {
|
|
435
|
+
await booted
|
|
436
|
+
if (tenancy.tenant?.ok === false || !tenancy.tenant?.instanceId) {
|
|
437
|
+
return { ok: false, why: tenancy.tenant?.why || '启动没走完,不知道这个家是不是自己的,这次没写。' }
|
|
438
|
+
}
|
|
439
|
+
return answerCard(root, answer)
|
|
440
|
+
})
|
|
441
|
+
|
|
402
442
|
function shutdown() {
|
|
443
|
+
if (stopped) return
|
|
444
|
+
stopped = true
|
|
403
445
|
try { clearInterval(heartbeat) } catch { /* ignore */ }
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
446
|
+
// 返回给 cordis:它等完 effect 才重新 apply,下一次认领一定在放开之后
|
|
447
|
+
const released = Promise.resolve(claiming)
|
|
448
|
+
.then(async (claim) => {
|
|
449
|
+
await beating
|
|
450
|
+
return claim?.ok && claim.instanceId ? releaseRoot(root, claim.instanceId) : false
|
|
451
|
+
})
|
|
452
|
+
.catch(() => {})
|
|
408
453
|
try { scheduler?.dispose?.() } catch { /* ignore */ }
|
|
409
454
|
try { clearWakeSection() } catch { /* ignore */ }
|
|
410
455
|
for (const d of disposers) { try { d() } catch { /* ignore */ } }
|
|
456
|
+
// 磁盘卡住时别把卸载也挂住,依赖这个插件的服务要等它。用不 ref 的定时器,它不该拖住进程
|
|
457
|
+
return Promise.race([released, sleep(2000, undefined, { ref: false })])
|
|
411
458
|
}
|
|
412
459
|
|
|
413
|
-
|
|
460
|
+
// 宿主的 cordis 4 没有 'dispose' 事件,也不管 apply 返回的 { dispose },只认 ctx.effect。
|
|
461
|
+
// 只挂事件的话,插件停用后定时器还在跑,再启用时新旧两个调度器一起叫她。
|
|
462
|
+
if (typeof ctx.effect === 'function') ctx.effect(() => shutdown, 'dsh-fish.shutdown')
|
|
463
|
+
else ctx.on?.('dispose', shutdown)
|
|
414
464
|
|
|
415
465
|
return {
|
|
416
466
|
dispose() {
|
package/lib/keep.js
CHANGED
|
@@ -20,8 +20,8 @@ import { askKeepsake, decideKeepsake } from './keepsake.js'
|
|
|
20
20
|
import { askInChat, picked } from './askinchat.js'
|
|
21
21
|
|
|
22
22
|
/** 点头的两个选项。文案用她那个人的语气,不用「批准/拒绝」。 */
|
|
23
|
-
const YES = '准了'
|
|
24
|
-
const NO = '算了'
|
|
23
|
+
export const YES = '准了'
|
|
24
|
+
export const NO = '算了'
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* 把「想带这个回去」问进会话里,等一个人选。
|
|
@@ -31,12 +31,20 @@ const NO = '算了'
|
|
|
31
31
|
* 算了 → 这东西留在原地
|
|
32
32
|
* 没答(超时/没人接)→ 还挂着,出门页里仍可以点头
|
|
33
33
|
*/
|
|
34
|
-
|
|
34
|
+
/** 卡开着的时候别处(观测台、接卡的插件)先答了:回执照账上的说,不照这张卡上点的说。 */
|
|
35
|
+
function handledElsewhere(d) {
|
|
36
|
+
return { state: 'answered', note: d.item?.status === 'kept' ? '别处先答了,带回来了。' : '别处先答了,没带。', said: 'failed' }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function askBack(ctx, root, { sessionId, item, signal, rawWhy }) {
|
|
35
40
|
const why = String(item?.why ?? '').trim()
|
|
36
41
|
const name = String(item?.rel ?? '').split(/[\\/]/).pop() || String(item?.rel ?? '')
|
|
37
42
|
const r = await askInChat(ctx, {
|
|
38
43
|
sessionId,
|
|
39
44
|
signal,
|
|
45
|
+
root,
|
|
46
|
+
rawTexts: [rawWhy],
|
|
47
|
+
cardId: `keepsake:${item?.id ?? ''}`,
|
|
40
48
|
questions: [
|
|
41
49
|
{
|
|
42
50
|
id: 'keepsake',
|
|
@@ -52,12 +60,14 @@ async function askBack(ctx, root, { sessionId, item, signal }) {
|
|
|
52
60
|
const said = picked(r.answers, 'keepsake')
|
|
53
61
|
if (said === YES) {
|
|
54
62
|
const d = decideKeepsake(root, { id: item.id, yes: true })
|
|
63
|
+
if (d.handled) return handledElsewhere(d)
|
|
55
64
|
return d.ok
|
|
56
65
|
? { state: 'answered', note: '准了,带回来了。', said: 'yes' }
|
|
57
|
-
: { state: 'answered', note:
|
|
66
|
+
: { state: 'answered', note: `准了,${d.note}`, said: 'failed' }
|
|
58
67
|
}
|
|
59
68
|
if (said === NO) {
|
|
60
|
-
decideKeepsake(root, { id: item.id, yes: false })
|
|
69
|
+
const d = decideKeepsake(root, { id: item.id, yes: false })
|
|
70
|
+
if (d.handled) return handledElsewhere(d)
|
|
61
71
|
return { state: 'answered', note: '没带。', said: 'no' }
|
|
62
72
|
}
|
|
63
73
|
return { state: 'unanswered', note: '问了。等他点头。', said: '' }
|
|
@@ -101,9 +111,11 @@ export function registerBringBack(ctx, deps = {}) {
|
|
|
101
111
|
? `[没问:${out.note}]`
|
|
102
112
|
: out.said === 'yes'
|
|
103
113
|
? '[准了]'
|
|
104
|
-
: out.said === '
|
|
105
|
-
?
|
|
106
|
-
: '
|
|
114
|
+
: out.said === 'failed'
|
|
115
|
+
? `[${out.note}]`
|
|
116
|
+
: out.said === 'no'
|
|
117
|
+
? '[算了]'
|
|
118
|
+
: '[已问]',
|
|
107
119
|
},
|
|
108
120
|
],
|
|
109
121
|
},
|
|
@@ -135,7 +147,7 @@ export function registerBringBack(ctx, deps = {}) {
|
|
|
135
147
|
|
|
136
148
|
// 0.7.5:问她那个人。这一步会等到有人选或者超时(默认 5 分钟),
|
|
137
149
|
// 但无论走哪条路,台账里都已经有一条 asked——不会因为没人答就丢掉这件事。
|
|
138
|
-
const back = await askBack(ctx, root, { sessionId, item: r.item, signal: exec?.signal })
|
|
150
|
+
const back = await askBack(ctx, root, { sessionId, item: r.item, signal: exec?.signal, rawWhy: args?.why })
|
|
139
151
|
return { ok: true, note: back.note, said: back.said }
|
|
140
152
|
},
|
|
141
153
|
}),
|
package/lib/keepsake.js
CHANGED
|
@@ -115,7 +115,9 @@ export function askKeepsake(root, { outing = null, sessionId = '', rel = '', why
|
|
|
115
115
|
if (mine.length >= KEEPSAKES_PER_TRIP) {
|
|
116
116
|
return { ok: false, note: '这一趟带得够多了。' }
|
|
117
117
|
}
|
|
118
|
-
if (open.length >= 1)
|
|
118
|
+
if (open.length >= 1) {
|
|
119
|
+
return { ok: false, note: open.some((it) => it.copyFailedAt) ? '上一件他点了头,只是没复制成,还没带回来。' : '上一件还等着点头。' }
|
|
120
|
+
}
|
|
119
121
|
|
|
120
122
|
const cwd = String(outing.cwd ?? '')
|
|
121
123
|
const target = resolve(cwd, raw)
|
|
@@ -156,7 +158,7 @@ export function decideKeepsake(root, { id = '', yes = false, now = Date.now() }
|
|
|
156
158
|
const db = readDb(root)
|
|
157
159
|
const item = db.items.find((it) => it && it.id === String(id))
|
|
158
160
|
if (!item) return { ok: false, note: '没找到这一件。' }
|
|
159
|
-
if (item.status !== 'asked') return { ok: false, note: '这一件已经处理过了。' }
|
|
161
|
+
if (item.status !== 'asked') return { ok: false, item, handled: true, note: '这一件已经处理过了。' }
|
|
160
162
|
|
|
161
163
|
if (!yes) {
|
|
162
164
|
item.status = 'declined'
|
|
@@ -187,23 +189,28 @@ export function decideKeepsake(root, { id = '', yes = false, now = Date.now() }
|
|
|
187
189
|
|
|
188
190
|
const dk = dateKey(new Date(now))
|
|
189
191
|
const dir = keepsakesDir(root, dk)
|
|
192
|
+
const dest = join(dir, `${item.id}-${basename(target)}`)
|
|
190
193
|
try {
|
|
191
194
|
mkdirSync(dir, { recursive: true })
|
|
192
|
-
const dest = join(dir, `${item.id}-${basename(target)}`)
|
|
193
195
|
copyFileSync(target, dest)
|
|
194
|
-
item.status = 'kept'
|
|
195
|
-
item.decidedAt = now
|
|
196
|
-
item.keptAs = `${dk}/${basename(dest)}`
|
|
197
|
-
item.size = st.size
|
|
198
|
-
writeDb(root, db)
|
|
199
|
-
return { ok: true, item, note: '带回来了。' }
|
|
200
196
|
} catch (err) {
|
|
201
|
-
|
|
202
|
-
item.
|
|
203
|
-
|
|
197
|
+
// 他说的是准了,只是这次没复制成(占用、权限)。还挂着,再答一次再试,不记成没带。
|
|
198
|
+
item.copyFailedAt = now
|
|
199
|
+
try { writeDb(root, db) } catch { /* 写不进去也还是 asked,只是下一件的说法不准 */ }
|
|
200
|
+
return { ok: false, item, note: `没复制成(${String(err?.code ?? '出错了')})。` }
|
|
201
|
+
}
|
|
202
|
+
item.status = 'kept'
|
|
203
|
+
item.decidedAt = now
|
|
204
|
+
item.keptAs = `${dk}/${basename(dest)}`
|
|
205
|
+
item.size = st.size
|
|
206
|
+
delete item.copyFailedAt
|
|
207
|
+
try {
|
|
204
208
|
writeDb(root, db)
|
|
205
|
-
|
|
209
|
+
} catch (err) {
|
|
210
|
+
// 盘上还是 asked:再答一次会覆盖同名文件,把账补上
|
|
211
|
+
return { ok: false, item, note: `文件到家了,账没记上(${String(err?.code ?? '出错了')}),再答一次。` }
|
|
206
212
|
}
|
|
213
|
+
return { ok: true, item, note: '带回来了。' }
|
|
207
214
|
}
|
|
208
215
|
|
|
209
216
|
/** 还等着点头的那几件(界面用)。 */
|