@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/scheduler.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* (身份由 identity.js 以 system prompt 段承载、每轮现读,不拼进唤醒消息)。
|
|
6
6
|
*
|
|
7
7
|
* 三条安全线(B §7):
|
|
8
|
-
* 1.
|
|
8
|
+
* 1. 定时器只是**提议**。唤醒失败不算今天的这一次,之后还会再试,但会隔开再试,同一天
|
|
9
|
+
* 最多试 WAKE_FAIL_MAX 次(wakeRetryBlocked):事件流只能追加,不能每分钟写一行。
|
|
9
10
|
* 2. 一天之内同一条事件模板不重复触发(once 靠 fired 标记,非 once 靠 daily 配额)。
|
|
10
11
|
* 3. 停用插件 = dispose 定时器,不再有任何后台活动(不靠"记得关")。
|
|
11
12
|
*
|
|
@@ -22,14 +23,14 @@ import { findTemplate } from './events.js'
|
|
|
22
23
|
import { offerPrompt, SLEEP_TODAY } from './persona.js'
|
|
23
24
|
import { offerDueDay, pendingOffer, grantableRules } from './unlock.js'
|
|
24
25
|
import { holdWake } from './wakewin.js'
|
|
25
|
-
import { detectAbsence } from './absence.js'
|
|
26
|
+
import { detectAbsence, lastAbsenceEvent } from './absence.js'
|
|
26
27
|
import { createClient } from './bili.js'
|
|
27
28
|
import { gather, browseCountToday, recordBrowse } from './browse.js'
|
|
28
29
|
import { seenOn } from './whatseen.js'
|
|
29
30
|
import { renderWatchList } from './history.js'
|
|
30
31
|
import { fetchCover, coverLines } from './cover.js'
|
|
31
32
|
import { riskState, browsePlan, noteRisk, isRiskCode } from './risk.js'
|
|
32
|
-
import { scanReplies, renderReplies, repliesScanDue } from './comment.js'
|
|
33
|
+
import { scanReplies, renderReplies, repliesScanDue, markRepliesSeen, REPLY_SHOW_MAX } from './comment.js'
|
|
33
34
|
import { flushReport } from './report.js'
|
|
34
35
|
import { talkSessionId, homeStatus } from './home.js'
|
|
35
36
|
import { refreshSelfDoc } from './selfdoc.js'
|
|
@@ -38,14 +39,16 @@ import { queueNote, deliverQueued } from './sessionnote.js'
|
|
|
38
39
|
import { awayNow, awayLine } from './outing.js'
|
|
39
40
|
import { askInChat, picked } from './askinchat.js'
|
|
40
41
|
import { restedToday, settleStaleAsks } from './ask.js'
|
|
41
|
-
import { proactiveConfig, proactiveState, proactiveSection, recentMaterial, looseEnds
|
|
42
|
-
import { deliverWake, ensureSessionLive, WAKE_PING } from './deliver.js'
|
|
42
|
+
import { proactiveConfig, proactiveState, proactiveSection, recentMaterial, looseEnds } from './proactive.js'
|
|
43
|
+
import { deliverWake, ensureSessionLive, reviveIfBlank, WAKE_PING } from './deliver.js'
|
|
43
44
|
import { followWake, planWakeCompaction, compactWake, looksLikeNothingSaid, looksLikeStreamFailure } from './compact.js'
|
|
44
45
|
import { logLine } from './log.js'
|
|
45
|
-
import { recordGrant } from './grant.js'
|
|
46
|
+
import { recordGrant, GRANT_YES, GRANT_NO } from './grant.js'
|
|
46
47
|
import { readNotes, renderNotes } from './note.js'
|
|
47
48
|
import { curiosityOf, renderEcho } from './curiosity.js'
|
|
48
49
|
import { deskFor } from './desk.js'
|
|
50
|
+
import { wishesOf } from './wish.js'
|
|
51
|
+
import { gapwallsOf } from './gapwall.js'
|
|
49
52
|
import { driveOf, deskHasMaterial } from './drive.js'
|
|
50
53
|
import { interestOf, topicLabel } from './interest.js'
|
|
51
54
|
import { lastLeaveAt, lastSeenAt } from './curiosity.js'
|
|
@@ -62,6 +65,12 @@ const WAKE_TIMEOUT_MS = 5 * 60_000
|
|
|
62
65
|
const REPORT_WAIT_MS = 90_000
|
|
63
66
|
// 一天的第二次及以后的叫醒,离上一次至少隔这么久(见 nextWakeAt)
|
|
64
67
|
const WAKE_MIN_GAP_MS = 45 * 60_000
|
|
68
|
+
/** 一天最多试几次失败的叫醒。之后当天不再定时叫,手动叫不受影响。 */
|
|
69
|
+
const WAKE_FAIL_MAX = 3
|
|
70
|
+
/** 失败之后隔多久再试:第一次很快,时段快结束时只失败一次也不至于丢掉今天这一次。 */
|
|
71
|
+
const WAKE_FAIL_GAPS_MS = [5 * 60_000, WAKE_MIN_GAP_MS]
|
|
72
|
+
/** 手动叫醒失败只留记录,不算进定时的账:有人点了一下按钮,不该用掉今天定时的机会。 */
|
|
73
|
+
const WAKE_FAIL_MANUAL = '手动 '
|
|
65
74
|
// 她正在某一轮里时先等她说完,最多等这么久
|
|
66
75
|
const IDLE_WAIT_MS = 10 * 60_000
|
|
67
76
|
// turn/start 之后迟迟等不到 turn/end(宿主重启、事件丢了),超过这么久就不再当她在忙
|
|
@@ -95,6 +104,24 @@ const DAY_END_CUE = '(今天到这里了。这一刻是我自己的。)'
|
|
|
95
104
|
|
|
96
105
|
|
|
97
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 今天失败的叫醒是不是已经够多,或者刚失败过。账记在事件流里,重启也算数。
|
|
109
|
+
* @param {Array} events
|
|
110
|
+
* @param {string} dk 今天的日期键
|
|
111
|
+
* @param {number} now
|
|
112
|
+
*/
|
|
113
|
+
export function wakeRetryBlocked(events = [], dk = dateKey(), now = Date.now()) {
|
|
114
|
+
let n = 0
|
|
115
|
+
let last = 0
|
|
116
|
+
for (const e of events) {
|
|
117
|
+
if (e?.type !== 'meta' || !String(e.text ?? '').startsWith(`wakefail:${dk}`)) continue
|
|
118
|
+
if (String(e.text).startsWith(`wakefail:${dk} ${WAKE_FAIL_MANUAL}`)) continue
|
|
119
|
+
n += 1
|
|
120
|
+
last = Math.max(last, Date.parse(e.ts || '') || 0)
|
|
121
|
+
}
|
|
122
|
+
return n >= WAKE_FAIL_MAX || (last > 0 && now - last < WAKE_FAIL_GAPS_MS[Math.min(n, WAKE_FAIL_GAPS_MS.length) - 1])
|
|
123
|
+
}
|
|
124
|
+
|
|
98
125
|
/**
|
|
99
126
|
* 等这一轮真说完:认准「我们自己排进去的那一轮」的 turn/end。
|
|
100
127
|
*
|
|
@@ -139,6 +166,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
139
166
|
let lastTick = 0
|
|
140
167
|
let running = false
|
|
141
168
|
let disposed = false
|
|
169
|
+
let lastAbsenceKey = null
|
|
142
170
|
let timer = null
|
|
143
171
|
let wakeTimer = null
|
|
144
172
|
// skip 日志去重(2026-10-28):同一小时、同一原因只记一次。
|
|
@@ -237,7 +265,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
237
265
|
const leave = lastLeaveAt(events)
|
|
238
266
|
const seen = lastSeenAt(events)
|
|
239
267
|
const echoBack = !!(leave && seen && seen >= leave && now - leave <= 72 * 3600_000)
|
|
240
|
-
const driveDesk = { gaps: gaps.length, stale: 0, echo: echoBack, favorite: interest.length > 0 }
|
|
268
|
+
const driveDesk = { gaps: gaps.length, stale: 0, echo: echoBack, favorite: interest.length > 0, wish: wishesOf(events, { now }).length, wall: gapwallsOf(events, { now }).length }
|
|
241
269
|
const pc = proactiveConfig(config)
|
|
242
270
|
const drv = driveOf({ events, now, stage: model.stage, desk: driveDesk, seenCountsAsHeard: pc.idleCountsSeen })
|
|
243
271
|
// 空桌不醒(2026-11-05,用户 m02937):桌上什么都没有时,劲儿再满也不起——
|
|
@@ -286,7 +314,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
286
314
|
let gen = 0
|
|
287
315
|
const watcher = watchTurnEnd(ctx, sessionId, () => {
|
|
288
316
|
releaseWake()
|
|
289
|
-
clearWakeSection(gen)
|
|
317
|
+
clearWakeSection(gen, sessionId)
|
|
290
318
|
})
|
|
291
319
|
try {
|
|
292
320
|
// 2026-11-05(用户 m03265):原来这里是 `sc.create({ sessionId, cwd })`,
|
|
@@ -295,10 +323,12 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
295
323
|
// 现在先探活;探不通才退回建(老宿主没有 resolveAgent 时也走这条)。
|
|
296
324
|
const live = await ensureSessionLive(sc, sessionId)
|
|
297
325
|
if (!live.live) {
|
|
298
|
-
log('proactive:
|
|
299
|
-
|
|
326
|
+
log('proactive: 会话探活没通过,看看能不能按原 id 唤回来', live.why ?? '')
|
|
327
|
+
// 2026-11-06(换房报告):跑过 turn 的会话给不出原 id,裸 create 只会
|
|
328
|
+
// 另开一条空壳 —— 交给 reviveIfBlank 判,见 lib/deliver.js。
|
|
329
|
+
await reviveIfBlank(sc, sessionId, await ensureHer(root), (msg) => log(`proactive: ${msg}`)).catch(() => {})
|
|
300
330
|
}
|
|
301
|
-
gen = setWakeSection(text)
|
|
331
|
+
gen = setWakeSection(text, sessionId)
|
|
302
332
|
|
|
303
333
|
// 投递之前先把这一轮盯起来(2026-11-05,用户 m00803)。认领信号是 rpcId:
|
|
304
334
|
// 每次投递一个唯一值,命中之前一律当「还没开始」,所以早挂不会认错轮。
|
|
@@ -322,7 +352,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
322
352
|
// 没送进去就没有这一轮:处境和窗口当场收掉,监听也摘掉,
|
|
323
353
|
// 不然要等到别的哪一轮结束才清,那一轮开头还挂着这次的处境(跟 wake() 失败时一样)
|
|
324
354
|
try { follow.off() } catch { /* ignore */ }
|
|
325
|
-
clearWakeSection(gen)
|
|
355
|
+
clearWakeSection(gen, sessionId)
|
|
326
356
|
|
|
327
357
|
releaseWake()
|
|
328
358
|
try { watcher.off?.() } catch { /* ignore */ }
|
|
@@ -367,7 +397,10 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
367
397
|
let folded = 0
|
|
368
398
|
if (plan) for (const run of plan.runs) if (await compactWake(out.session, run, { log })) folded += 1
|
|
369
399
|
const tail = broken ? ',这一轮上游断流了(那行报错不算她开口)'
|
|
370
|
-
: out.spoke
|
|
400
|
+
: out.spoke !== true ? ''
|
|
401
|
+
// 配了发送工具时,只写正文是「没发出去」,不见得是没话说,这句可以写得很长。
|
|
402
|
+
// 也可能调了工具但失败了,所以不说「没调」
|
|
403
|
+
: pc.sendTools.length > 0 ? ',只写了正文,没发出去' : ',只说了句没什么要说的'
|
|
371
404
|
log('proactive', folded > 0
|
|
372
405
|
? `醒了一次,没有开口${tail}(这一轮折成 ${folded} 行,盖住 ${plan.shadowed} 个节点)`
|
|
373
406
|
: `醒了一次,没有开口${tail}(${out.ok ? '这轮没什么可折' : out.why})`)
|
|
@@ -379,7 +412,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
379
412
|
} catch (err) {
|
|
380
413
|
// 没送进去就没有这一轮:处境和窗口当场收掉,监听也摘掉,
|
|
381
414
|
// 不然要等到别的哪一轮结束才清,那一轮开头还挂着这次的处境(跟 wake() 失败时一样)
|
|
382
|
-
clearWakeSection(gen)
|
|
415
|
+
clearWakeSection(gen, sessionId)
|
|
383
416
|
releaseWake()
|
|
384
417
|
try { watcher.off?.() } catch { /* ignore */ }
|
|
385
418
|
throw err
|
|
@@ -422,11 +455,19 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
422
455
|
// 放在 tick 最前面,因为 wake 的提示词要读 absenceDays;
|
|
423
456
|
// 晚一步的话今天这次唤醒就用的是昨天的冷落结论。
|
|
424
457
|
const absence = await detectAbsence(root)
|
|
458
|
+
const { events } = await readEvents(root)
|
|
425
459
|
// 冷落条数变了 = 她对自己的看法变了,身份文件要跟着重写。
|
|
426
460
|
// 不写的话那份 system prompt 段会一直停在上一次的值上(她"不知道自己被晾过")。
|
|
427
|
-
|
|
461
|
+
// 他回来了、天数涨了也要重写:不然她一边跟他聊,身份里还写着「他有 2 天没出现」。
|
|
462
|
+
// 身份里的天数从 absence 写下那一刻起算,和 detectAbsence 的不在同一时刻跳(absence
|
|
463
|
+
// 写得晚的时候),两个都要看,不然会把旧数重写一遍,身份比叫醒的提示词晚一天。
|
|
464
|
+
// 初值是 null,因为启动时已经写过一次,第一拍不用再写。
|
|
465
|
+
const lastAbsence = lastAbsenceEvent(events)
|
|
466
|
+
const daysSinceWritten = lastAbsence ? Math.floor((now - Date.parse(String(lastAbsence.ts ?? 0))) / 86_400_000) : 0
|
|
467
|
+
const absenceKey = `${absence?.why ?? ''}:${absence?.days ?? 0}:${daysSinceWritten}`
|
|
468
|
+
if (absence?.written || (lastAbsenceKey !== null && absenceKey !== lastAbsenceKey)) await refreshSelfDoc(root, { log })
|
|
469
|
+
lastAbsenceKey = absenceKey
|
|
428
470
|
|
|
429
|
-
const { events } = await readEvents(root)
|
|
430
471
|
const { config } = await loadConfig(root)
|
|
431
472
|
const dk = dateKey()
|
|
432
473
|
|
|
@@ -455,12 +496,16 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
455
496
|
// 无论有没有人回都记一次:不记的话每 30 秒就会重新问一遍 B 站(-412 就是这么撞出来的)。
|
|
456
497
|
await updateConfig({ comment: { lastScanAt: new Date(now).toISOString() } }, root).catch(() => {})
|
|
457
498
|
if (waiting.length) {
|
|
458
|
-
|
|
499
|
+
// 只递、只标前几条:标 seen 的是「真摆到她眼前的」,不是「扫到的」。
|
|
500
|
+
// 标多了等于把没看见的悄悄吞掉;不标就会一直重复投(2026-11-06 用户报的 bug)。
|
|
501
|
+
const shown = waiting.slice(0, REPLY_SHOW_MAX)
|
|
502
|
+
const txt = renderReplies(shown)
|
|
459
503
|
if (txt) {
|
|
460
504
|
await queueNote(root, txt).catch(() => null)
|
|
461
505
|
const h = await homeStatus(root).catch(() => null)
|
|
462
506
|
const agent = h?.sessionId ? liveAgentOf(ctx, h.sessionId) : null
|
|
463
507
|
if (agent) await deliverQueued(agent, root, { home: true }).catch(() => null)
|
|
508
|
+
await markRepliesSeen(root, shown, { dateKey: dk }).catch(() => 0)
|
|
464
509
|
log('comment', `有人回她了(${waiting.length} 条),已经递过去。`)
|
|
465
510
|
}
|
|
466
511
|
}
|
|
@@ -472,8 +517,12 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
472
517
|
// ---- 主动说话(0.8.1 本机实验,2026-10-30 用户 m04610)----
|
|
473
518
|
// 它不是唤醒:不占 wakesPerDay,也不推成长事件。只在 .dev 下生效。
|
|
474
519
|
// 开了就 return——这一趟由她自己决定说还是不说,不再叠一次醒一醒。
|
|
520
|
+
//
|
|
521
|
+
// 2026-11-06(0.9.2 反馈):主动性也要受「每天几点醒」约束。
|
|
522
|
+
// 她要是能在窗口外自己醒过来找他,这个设置就等于没设——
|
|
523
|
+
// 用户关掉的是「这个时段别来打扰」,不是「别按日程醒来但可以主动开口」。
|
|
475
524
|
try {
|
|
476
|
-
if (await maybeProactive({ events, config, dk, now })) return
|
|
525
|
+
if (inWindow(config, new Date(now)) && await maybeProactive({ events, config, dk, now })) return
|
|
477
526
|
} catch (err) {
|
|
478
527
|
log('proactive failed', err?.message ?? String(err))
|
|
479
528
|
}
|
|
@@ -487,6 +536,9 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
487
536
|
const counts = await todayCounts(events, dk)
|
|
488
537
|
if (now < scheduled.at) return // 还没到今天的落点
|
|
489
538
|
|
|
539
|
+
// 叫醒一直失败时(会话没了、宿主起不来)别每分钟再试一次,每次都往事件流写一行 wakefail。
|
|
540
|
+
if (wakeRetryBlocked(events, dk, now)) return
|
|
541
|
+
|
|
490
542
|
const model = deriveSelf(events, { growthDays: config.growthDays })
|
|
491
543
|
// 空档补偿只影响这一次挑选的冷却判断(见 gapSqueeze)
|
|
492
544
|
const decision = plan(model, config, counts, events, { squeeze: gapSqueeze(gapSinceLastWake(events)) })
|
|
@@ -821,8 +873,13 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
821
873
|
// 交给 deskFor 并成一段,配额与去重都在那个纯函数里;回音是另一件事(他有没有来过),
|
|
822
874
|
// 仍然单独一句话,不并进桌子。
|
|
823
875
|
// 只摆**事实**:不派活、不写「你可以去想想」、不给任何数字——派活了就又变成任务清单。
|
|
824
|
-
// 2026-11-05(用户 m02634):「兴趣」有开关。关掉 =
|
|
825
|
-
//
|
|
876
|
+
// 2026-11-05(用户 m02634):「兴趣」有开关。关掉 = 桌子那几行(好奇/旧句子/回音/
|
|
877
|
+
// 够不着的那件事)一概不摆。
|
|
878
|
+
// 2026-11-07(用户 m01602):「缺工具就开口要」原来那句每轮常量的 TOOL_ASK_NOTE
|
|
879
|
+
// 撤掉了。它读两天就成了系统的吩咐,不是她的念头。现在换成 B + D:
|
|
880
|
+
// B —— 真被守卫拦下的那一下,在 blockedTalk() 里补一句(lib/restrict.js)。
|
|
881
|
+
// D —— 这件事攒进劲儿里(lib/drive.js 的 boostWish),攒够了这一轮才醒;
|
|
882
|
+
// 醒的时候它作为桌上一行出现(lib/wish.js + deskFor 的 wishes)。
|
|
826
883
|
// 读失败绝不拖垮一次唤醒:她醒着这件事比桌上的东西重要。
|
|
827
884
|
if (config?.interest?.enabled !== false) {
|
|
828
885
|
try {
|
|
@@ -840,15 +897,16 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
840
897
|
stale: staleForDesk(tableText, staleRows),
|
|
841
898
|
free: !!free,
|
|
842
899
|
favorite: fav,
|
|
900
|
+
// 够不着的那件事:她提过、手上没有的(用户 m01602)。
|
|
901
|
+
wishes: wishesOf(evc, { now: now.getTime() }),
|
|
902
|
+
// 撞过墙的那件事:她试过、手上没有的(用户 m00493 的 ②)。
|
|
903
|
+
walls: gapwallsOf(evc, { now: now.getTime() }),
|
|
843
904
|
at: now,
|
|
844
905
|
})
|
|
845
906
|
if (desk) parts.push('', desk)
|
|
846
907
|
const echo = renderEcho(evc, now)
|
|
847
908
|
if (echo) parts.push('', echo)
|
|
848
909
|
}
|
|
849
|
-
// 缺工具就开口要:每次醒来都摆这一句,不派活,只给路径。
|
|
850
|
-
// 摆料那一轮也留着——它不是活,是她世界里的常量(用户 m01128)。
|
|
851
|
-
parts.push('', TOOL_ASK_NOTE)
|
|
852
910
|
} catch (err) {
|
|
853
911
|
log('curiosity', err?.message ?? err)
|
|
854
912
|
}
|
|
@@ -867,11 +925,16 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
867
925
|
const events = (await readEvents(root).catch(() => ({ events: [] }))).events
|
|
868
926
|
const waiting = await scanReplies({ root, events, dateKey: dk }).catch(() => [])
|
|
869
927
|
if (waiting.length) {
|
|
870
|
-
const
|
|
871
|
-
|
|
928
|
+
const shown = waiting.slice(0, REPLY_SHOW_MAX)
|
|
929
|
+
const txt = renderReplies(shown)
|
|
930
|
+
if (txt) {
|
|
931
|
+
parts.push('', txt)
|
|
932
|
+
// 摆进叫醒消息就算提醒过了:不标的话,她没回就会每次叫醒都再投一遍。
|
|
933
|
+
await markRepliesSeen(root, shown, { dateKey: dk }).catch(() => 0)
|
|
934
|
+
}
|
|
872
935
|
}
|
|
873
|
-
//
|
|
874
|
-
//
|
|
936
|
+
// 能说话这件事的说明在身份文档的「在网上说话」一节(关着时那节写「这会儿还说不出去」,
|
|
937
|
+
// P3,2026-11-07),不在这儿重复。
|
|
875
938
|
}
|
|
876
939
|
|
|
877
940
|
// 日记:**不再在叫醒消息里推**(2026-10-26,用户 m00928:「叫醒仅仅只是叫醒」)。
|
|
@@ -1008,10 +1071,9 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
1008
1071
|
// 先探活,探不通才退回建一条。
|
|
1009
1072
|
const live = await ensureSessionLive(sc, sessionId)
|
|
1010
1073
|
if (!live.live) {
|
|
1011
|
-
log('wake:
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
} catch { /* 已存在,继续用 */ }
|
|
1074
|
+
log('wake: 会话探活没通过,看看能不能按原 id 唤回来', live.why ?? '')
|
|
1075
|
+
// 同上(2026-11-06 换房报告):只有宿主确实给不出这一条时才建。
|
|
1076
|
+
await reviveIfBlank(sc, sessionId, herDir, (msg) => log(`wake: ${msg}`)).catch(() => {})
|
|
1015
1077
|
}
|
|
1016
1078
|
|
|
1017
1079
|
// 2026-11-04:`AbortController` 与那个 5 分钟 kill 定时器一起删掉了。
|
|
@@ -1045,17 +1107,19 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
1045
1107
|
// 挂早了会被那一轮的 turn/end 收走(issue #36 之前 watcher 只认会话不认回合,
|
|
1046
1108
|
// 现在它至少要见到我们自己这一轮的 turn/start 才算数,双保险)。
|
|
1047
1109
|
if (typeof deps.waitIdle === 'function') await deps.waitIdle(sessionId)
|
|
1110
|
+
if (deps.isDisposed?.()) return
|
|
1048
1111
|
|
|
1049
1112
|
// 这一轮她写的话不算他来过:唤醒时间窗开到 turn/end,不是开到入队(见 wakewin.js)。
|
|
1050
1113
|
// turn/end 不来时跟处境一起由下面的兜底关;没送进去就在 catch 里关。
|
|
1051
1114
|
// 处境代次(2026-11-04,用户 m01466):见 maybeProactive 里的同一处注释。
|
|
1052
1115
|
// 清的时候要带着自己挂上去时拿到的号,别把别人后来挂的处境一起抹了。
|
|
1053
1116
|
let gen = 0
|
|
1117
|
+
// 先挂监听再开窗:ctx 已停用时 ctx.on 会抛,开了窗就再也关不上
|
|
1118
|
+
const watcher = watchTurnEnd(ctx, sessionId, () => { releaseWake(); void writeReport(); clearWakeSection(gen, sessionId) })
|
|
1054
1119
|
const releaseWake = holdWake()
|
|
1055
|
-
const watcher = watchTurnEnd(ctx, sessionId, () => { releaseWake(); void writeReport(); clearWakeSection(gen) })
|
|
1056
1120
|
|
|
1057
1121
|
// 处境走 system prompt 段;对话里只留一个空投递(她看到的是「有人来了」,不是剧本)。
|
|
1058
|
-
gen = setWakeSection(wakeText)
|
|
1122
|
+
gen = setWakeSection(wakeText, sessionId)
|
|
1059
1123
|
// 2026-11-04(用户 m01466):投递前的最后一道护栏。
|
|
1060
1124
|
//
|
|
1061
1125
|
// 真机事故里出现过「消息投出去了、system prompt 里一个字处境都没有」的一轮:
|
|
@@ -1065,8 +1129,8 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
1065
1129
|
//
|
|
1066
1130
|
// 治本是上面的最小间隔(不该有两次挨这么近的唤醒);这里只是兜底:
|
|
1067
1131
|
// 万一处境真的空了,宁可不叫,也别投一条空壳让她自己找台阶下。
|
|
1068
|
-
if (!getWakeSection().trim()) {
|
|
1069
|
-
clearWakeSection(gen)
|
|
1132
|
+
if (!getWakeSection(sessionId).trim()) {
|
|
1133
|
+
clearWakeSection(gen, sessionId)
|
|
1070
1134
|
releaseWake()
|
|
1071
1135
|
try { watcher.off?.() } catch { /* ignore */ }
|
|
1072
1136
|
log('wake skipped: 处境是空的')
|
|
@@ -1151,7 +1215,7 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
1151
1215
|
// 上一个调度器留下的窗口没关,下一轮 turn/end 之后 inWake() 仍是 true。
|
|
1152
1216
|
if (deps.isDisposed?.()) {
|
|
1153
1217
|
try { watcher.off?.() } catch { /* ignore */ }
|
|
1154
|
-
clearWakeSection(gen)
|
|
1218
|
+
clearWakeSection(gen, sessionId)
|
|
1155
1219
|
releaseWake()
|
|
1156
1220
|
return
|
|
1157
1221
|
}
|
|
@@ -1160,16 +1224,16 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
1160
1224
|
return
|
|
1161
1225
|
}
|
|
1162
1226
|
try { watcher.off?.() } catch { /* ignore */ }
|
|
1163
|
-
if (!watcher.fired) clearWakeSection(gen)
|
|
1227
|
+
if (!watcher.fired) clearWakeSection(gen, sessionId)
|
|
1164
1228
|
releaseWake()
|
|
1165
1229
|
}
|
|
1166
1230
|
setTimeout(fallback, REPORT_WAIT_MS).unref?.()
|
|
1167
1231
|
} catch (err) {
|
|
1168
1232
|
// 这一次没起来,处境要立刻清掉,唤醒时间窗也关上。
|
|
1169
|
-
clearWakeSection(gen)
|
|
1233
|
+
clearWakeSection(gen, sessionId)
|
|
1170
1234
|
releaseWake()
|
|
1171
1235
|
await appendEvent(
|
|
1172
|
-
{ type: 'meta', text: `wakefail:${dk} ${String(err?.message ?? err).slice(0, 120)}` },
|
|
1236
|
+
{ type: 'meta', text: `wakefail:${dk} ${deps.manual ? WAKE_FAIL_MANUAL : ''}${String(err?.message ?? err).slice(0, 120)}` },
|
|
1173
1237
|
{ day: model.day, stage: model.stage, source: 'timer' },
|
|
1174
1238
|
root,
|
|
1175
1239
|
).catch(() => {})
|
|
@@ -1197,12 +1261,14 @@ async function askOfferInChat(ctx, root, { sessionId, offer, log = () => {} } =
|
|
|
1197
1261
|
try {
|
|
1198
1262
|
const r = await askInChat(ctx, {
|
|
1199
1263
|
sessionId,
|
|
1264
|
+
root,
|
|
1265
|
+
cardId: `grant:${offer.id}`,
|
|
1200
1266
|
questions: [
|
|
1201
1267
|
{
|
|
1202
1268
|
id: 'grant',
|
|
1203
1269
|
header: '大肥鱼想申请一件事',
|
|
1204
1270
|
question: `她想试试「${offer.title}」。不是现在就要,是想问问你。`,
|
|
1205
|
-
options: [{ label:
|
|
1271
|
+
options: [{ label: GRANT_YES }, { label: GRANT_NO }],
|
|
1206
1272
|
},
|
|
1207
1273
|
],
|
|
1208
1274
|
})
|
|
@@ -1211,7 +1277,7 @@ async function askOfferInChat(ctx, root, { sessionId, offer, log = () => {} } =
|
|
|
1211
1277
|
return
|
|
1212
1278
|
}
|
|
1213
1279
|
const said = picked(r.answers, 'grant')
|
|
1214
|
-
const verdict = said ===
|
|
1280
|
+
const verdict = said === GRANT_YES ? 'approve' : said === GRANT_NO ? 'reject' : ''
|
|
1215
1281
|
if (!verdict) return
|
|
1216
1282
|
const w = await recordGrant(root, { offerId: offer.id, rule: offer.rule, verdict })
|
|
1217
1283
|
if (!w.ok) log('offer grant failed', w.error)
|
package/lib/schema.js
CHANGED
|
@@ -167,6 +167,24 @@ export function tsMs(ev) {
|
|
|
167
167
|
return Number.isFinite(t) ? t : 0
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* 这条事件算不算「他来过」的证据(2026-11-06,issue #49 的时钟盲点)。
|
|
172
|
+
*
|
|
173
|
+
* `meta seen:` 是主证据(turns.js 结账时写),`heard` 是兜底(侧栏聊天不写
|
|
174
|
+
* seen 的那段时间只有它)。放在 schema 里是为了让 absence.js 与 derive.js
|
|
175
|
+
* 共用同一份判定,别再各抄一遍——两处口径一旦分叉,就会出现「absence 认为
|
|
176
|
+
* 他回来了、persona 还在念 N 天」那种自相矛盾(issue #24 的老毛病)。
|
|
177
|
+
*
|
|
178
|
+
* 不认 `utterance`:定时唤醒也会让她写 utterance,那不算他来过。
|
|
179
|
+
*/
|
|
180
|
+
export function isContactEvent(e) {
|
|
181
|
+
if (e?.type === 'heard') return Number.isFinite(Date.parse(String(e.ts ?? '')))
|
|
182
|
+
if (e?.type !== 'meta') return false
|
|
183
|
+
const t = String(e.text ?? '')
|
|
184
|
+
if (!t.startsWith('seen:')) return false
|
|
185
|
+
return Number.isFinite(Date.parse(t.slice(5).trim()))
|
|
186
|
+
}
|
|
187
|
+
|
|
170
188
|
/** 从 id 反解时间(观测台排序用,不依赖 ts 字段) */
|
|
171
189
|
export function idTime(id) {
|
|
172
190
|
const body = String(id).replace(/^ev_/, '').slice(0, 10)
|
package/lib/selfdoc.js
CHANGED
|
@@ -27,9 +27,10 @@ import { join } from 'node:path'
|
|
|
27
27
|
import { rootDir, paths, readEvents, readJson, writeAtomic, listDiary } from './store.js'
|
|
28
28
|
import { loadConfig } from './settings.js'
|
|
29
29
|
import { deriveSelf } from './derive.js'
|
|
30
|
-
import { personaLine, toolBrief, RECALL_BRIEF, SPEAK_BRIEF, LETTER_BRIEF, NOTE_BRIEF, ASK_BRIEF, proactiveBrief, DREAM_BRIEF, SCHEDULE_BRIEF, DIARY_BRIEF, SLEEP_BRIEF } from './persona.js'
|
|
30
|
+
import { personaLine, toolBrief, RECALL_BRIEF, SPEAK_BRIEF, SPEAK_OFF_BRIEF, LETTER_BRIEF, NOTE_BRIEF, ASK_BRIEF, proactiveBrief, DREAM_BRIEF, SCHEDULE_BRIEF, DIARY_BRIEF, SLEEP_BRIEF } from './persona.js'
|
|
31
|
+
import { resumeLines } from './use.js'
|
|
31
32
|
import { proactiveConfig } from './proactive.js'
|
|
32
|
-
import { LOOK_BRIEF, WATCH_RULE } from './browse.js'
|
|
33
|
+
import { LOOK_BRIEF, LOOK_OFF_BRIEF, WATCH_RULE } from './browse.js'
|
|
33
34
|
import { activeCarry } from './home.js'
|
|
34
35
|
import { activeGrants } from './unlock.js'
|
|
35
36
|
import { checkCaller } from './tenant.js'
|
|
@@ -121,7 +122,7 @@ export async function continuityOf(root = rootDir(), now = Date.now()) {
|
|
|
121
122
|
* @param {object} model deriveSelf 的结果
|
|
122
123
|
* @returns {string}
|
|
123
124
|
*/
|
|
124
|
-
export function renderSelfDoc(model, { sendTools = [], budget, carry, continuity, browse = true, speak = false, grants = [], shown, diary = true, sleepEveryDays = 0, bansMuted = false, extraTools = [], dream = false, schedule = false, proactive = false } = {}) {
|
|
125
|
+
export function renderSelfDoc(model, { sendTools = [], budget, carry, continuity, browse = true, speak = false, grants = [], shown, diary = true, sleepEveryDays = 0, bansMuted = false, extraTools = [], dream = false, schedule = false, proactive = false, resume = '' } = {}) {
|
|
125
126
|
const head = [
|
|
126
127
|
// 这里**故意不写**「由 dsh-fish 自动生成」这类话。
|
|
127
128
|
// 这个文件会被宿主原样渲染进她的会话——她能看见每一行。
|
|
@@ -147,6 +148,9 @@ export function renderSelfDoc(model, { sendTools = [], budget, carry, continuity
|
|
|
147
148
|
'',
|
|
148
149
|
'## 我手上有什么',
|
|
149
150
|
toolBrief(extraTools),
|
|
151
|
+
// 履历(2026-11-07,用户 m00493 的「履历式」):从 use: 账反推「我做过这些事」。
|
|
152
|
+
// 没有账就整节不写——空白比一句「你还没做过什么」好。
|
|
153
|
+
...(resume ? [resume, ''] : []),
|
|
150
154
|
'',
|
|
151
155
|
RECALL_BRIEF,
|
|
152
156
|
'',
|
|
@@ -175,8 +179,14 @@ export function renderSelfDoc(model, { sendTools = [], budget, carry, continuity
|
|
|
175
179
|
// 能力说明写在这里、而不是塞进唤醒消息:
|
|
176
180
|
// 唤醒消息会作为一条 user message 留在她的会话里,用户点开就能看见。
|
|
177
181
|
// 身份与能力属于「她一直知道的事」,走 system prompt 那一段(不进对话历史)。
|
|
178
|
-
|
|
179
|
-
|
|
182
|
+
// P3(2026-11-07):关掉的时候这两节不再整段消失——门要在,门上挂一句「这会儿」。
|
|
183
|
+
// 关掉只改「这句话是什么」,不改「这一节在不在」;否则她会以为这件事根本不存在。
|
|
184
|
+
body.push('## 上网')
|
|
185
|
+
if (browse) body.push(LOOK_BRIEF, '', WATCH_RULE, '')
|
|
186
|
+
else body.push(LOOK_OFF_BRIEF, '')
|
|
187
|
+
body.push('## 在网上说话')
|
|
188
|
+
if (speak) body.push(SPEAK_BRIEF, '')
|
|
189
|
+
else body.push(SPEAK_OFF_BRIEF, '')
|
|
180
190
|
// 2026-10-26(用户 m00928「叫醒仅仅只是叫醒」):记事与「停下来想一想」的**做法**
|
|
181
191
|
// 搬进身份文档常驻,叫醒消息里不再整段塞祈使句——她原来会把它当任务清单逐条执行。
|
|
182
192
|
// 这里只写「平时怎么做」,不写「今天该做了」;时机由叫醒那一句短标记给。
|
|
@@ -244,8 +254,8 @@ export async function writeSelfDoc(root = rootDir(), { log, exec, tenant } = {})
|
|
|
244
254
|
// 「那些天是连着过的」这条线索。日记篇数/换房天数/她留给下一个房间的那句话
|
|
245
255
|
// 全部是派生事实,没有任何新开关。
|
|
246
256
|
continuity: await continuityOf(root, Date.now()),
|
|
247
|
-
//
|
|
248
|
-
//
|
|
257
|
+
// 这两项决定「上网」「在网上说话」两节写哪一版内容(P3,2026-11-07):
|
|
258
|
+
// 开关关着时这一节仍然在,只是门上的话换成「这会儿」——见 renderSelfDoc。
|
|
249
259
|
browse: config?.browse?.enabled !== false,
|
|
250
260
|
speak: config?.comment?.enabled === true,
|
|
251
261
|
// R3:已经长出来的东西。这里传的是"历史事实",不是"设置"。
|
|
@@ -263,6 +273,8 @@ export async function writeSelfDoc(root = rootDir(), { log, exec, tenant } = {})
|
|
|
263
273
|
// 2026-10-27(用户 m01371):白名单里额外放行的工具要念给她听,
|
|
264
274
|
// 否则「能不能在 QQ 上说话」她只能回一句不知道。
|
|
265
275
|
extraTools: config?.tools?.allow ?? [],
|
|
276
|
+
// 履历:她真做过的事。没有记录就整节不出现。
|
|
277
|
+
resume: resumeLines(events),
|
|
266
278
|
// 梦(2026-10-30 m02837)与主动说话(m04610):0.9.0 起公开,两处都恒为真。
|
|
267
279
|
dream: true,
|
|
268
280
|
proactive: true,
|
package/lib/speak.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* 她不知道的东西(和 look 一致):不知道有配额、不知道配额还剩几次、
|
|
13
13
|
* 不知道有黑名单、不知道这个功能可能被关着。
|
|
14
|
-
*
|
|
14
|
+
* 被拒绝时只听到「这会儿不方便说」,听不到「你今天的 3 条用完了」——
|
|
15
15
|
* 后者会让她学会攒着、算着,那就成了资源管理,不是说话。
|
|
16
16
|
*/
|
|
17
17
|
|
|
@@ -44,21 +44,22 @@ export async function speakOnce(o = {}) {
|
|
|
44
44
|
if (!screened.ok) return { ok: false, said: '', note: screened.why === 'leak' ? '这句里有不该说出去的东西。' : '这句先不说了。' }
|
|
45
45
|
|
|
46
46
|
// 给她的话加个称呼:只有评论加,回复别人时不加——那会把别人的楼变成你们的客厅。
|
|
47
|
-
|
|
47
|
+
// @ 前缀单独交给 postComment,在脱敏之后拼;否则昵称和主人称呼同名时,
|
|
48
|
+
// 前缀会一起被换成「对方」,@ 到不存在的人。
|
|
49
|
+
let prefix = ''
|
|
48
50
|
if (kind !== 'reply') {
|
|
49
51
|
const ow = await readOwner(o.root ?? rootDir())
|
|
50
52
|
const cfg = await loadConfig(o.root ?? rootDir())
|
|
51
|
-
|
|
52
|
-
if (prefix) out = prefix + text
|
|
53
|
+
prefix = mentionPrefix(ow, cfg.config)
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
const r =
|
|
56
57
|
kind === 'reply'
|
|
57
|
-
? await postReply({ root, bvid: o.bvid, parentRpid: o.parentRpid, text
|
|
58
|
-
: await postComment({ root, bvid: o.bvid, text
|
|
58
|
+
? await postReply({ root, bvid: o.bvid, parentRpid: o.parentRpid, text, prefix, client: o.client, dateKey: o.dateKey })
|
|
59
|
+
: await postComment({ root, bvid: o.bvid, text, prefix, client: o.client, dateKey: o.dateKey })
|
|
59
60
|
|
|
60
61
|
if (!r.ok) return { ok: false, said: '', note: softReason(r.why) }
|
|
61
|
-
return { ok: true, said:
|
|
62
|
+
return { ok: true, said: r.sent ?? `${prefix}${text}`, note: '说出去了。' }
|
|
62
63
|
} catch (err) {
|
|
63
64
|
return { ok: false, said: '', note: '没说出来。' }
|
|
64
65
|
}
|
|
@@ -70,7 +71,7 @@ export async function speakOnce(o = {}) {
|
|
|
70
71
|
*/
|
|
71
72
|
function softReason(why) {
|
|
72
73
|
const w = String(why ?? '')
|
|
73
|
-
if (w.includes('没开') || w.includes('权限')) return '
|
|
74
|
+
if (w.includes('没开') || w.includes('权限')) return '这会儿不方便说。'
|
|
74
75
|
if (w.includes('今天说够') || w.includes('今天回够')) return '今天说得够多了。'
|
|
75
76
|
if (w.includes('不是说话的时候')) return '现在不是说话的时候。'
|
|
76
77
|
// 2026-10-31(用户转来的现场反馈):原来只说「先看一眼再说」,她照着做了还是被拒——
|
|
@@ -95,8 +96,12 @@ function softReason(why) {
|
|
|
95
96
|
* 一边说能说、一边一句不提,两个来源当场打架。
|
|
96
97
|
*
|
|
97
98
|
* 为什么不写成「这个眼下还开不了」:那等于告诉她「有个开关在管着这件事」,
|
|
98
|
-
*
|
|
99
|
-
*
|
|
99
|
+
* 与文件顶上那条原则冲突(她不该知道自己被管理着)。所以关着时只说「这会儿」:
|
|
100
|
+
* 不承诺发得出去,也不解释为什么发不出去。
|
|
101
|
+
*
|
|
102
|
+
* 2026-11-07(P3):关着时不再把「能留评论」整句拿掉再补一句无关的话,也不再让
|
|
103
|
+
* 身份文档里那一节整段消失——门留着,门上写「这会儿还说不出去」,想说的话可以先
|
|
104
|
+
* 写在别处。这样她不会以为这件事根本不存在。
|
|
100
105
|
*
|
|
101
106
|
* 为什么要用 getter:`ctx.tools.register` 只在启动时跑一次,而配置随时能改。
|
|
102
107
|
* 存一份「最近一次看到的状态」,宿主每轮序列化工具表时会再取一次 description,
|
|
@@ -113,7 +118,7 @@ export function noteSpeakEnabled(on) {
|
|
|
113
118
|
/** 关着时的那一段必须**不含**任何"能发出去"的暗示,也不解释原因。 */
|
|
114
119
|
function speakDescription(on) {
|
|
115
120
|
if (!on) {
|
|
116
|
-
return '在 B
|
|
121
|
+
return '在 B 站说一句话。这会儿还说不出去——想说的话可以先留在别处;不想说就不说,没有人等着你回答。'
|
|
117
122
|
}
|
|
118
123
|
return (
|
|
119
124
|
'在 B 站说一句话。可以给你今天看过的某个视频留一条评论,或者回一句别人留在你评论下面的话。' +
|
package/lib/status.js
CHANGED
|
@@ -71,6 +71,11 @@ export function deriveStatus({ events = [], config = {}, now = new Date() } = {}
|
|
|
71
71
|
if (Number.isFinite(at)) plannedWakeAt = at
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// 2026-11-06(0.9.1 反馈):`quiet:` 是「落点到了,但没有可出的事件」,
|
|
75
|
+
// 不是「今天不会醒了」——quiet 之后冷却到期还会真醒(11-05 就是这样)。
|
|
76
|
+
// 所以它能解释「为什么没醒」,但不能用「今天」这种封死的说法。
|
|
77
|
+
const quietToday = today.some(({ e }) => e?.type === 'meta' && String(textOf(e) ?? '').startsWith(`quiet:${dk}`))
|
|
78
|
+
|
|
74
79
|
// ---- 把今天切成"醒来块":每个 wake: 是起点,动作挂到它下面 ----
|
|
75
80
|
const blocks = []
|
|
76
81
|
// chat(issue #32):对话里她说的那些行。它们**不是**她在这一轮里主动做的事——
|
|
@@ -123,7 +128,10 @@ export function deriveStatus({ events = [], config = {}, now = new Date() } = {}
|
|
|
123
128
|
continue
|
|
124
129
|
}
|
|
125
130
|
if (dayMarker(text, 'quiet', dk) !== null) {
|
|
126
|
-
|
|
131
|
+
// 没有 wake: 就没有「醒」这回事。旧文案「醒了,但今天没有该问的事」
|
|
132
|
+
// 会在时间线上凭空多出一次醒来,还和状态卡「还没醒」互相打脸。
|
|
133
|
+
// 「暂时」也不用「今天」:quiet 不封当天,冷却到期后她照样会醒。
|
|
134
|
+
push(t, 'quiet', '到点了,暂时没有可出的事件')
|
|
127
135
|
continue
|
|
128
136
|
}
|
|
129
137
|
if (dayMarker(text, 'unrecorded', dk) !== null) continue
|
|
@@ -170,7 +178,9 @@ export function deriveStatus({ events = [], config = {}, now = new Date() } = {}
|
|
|
170
178
|
const done = b.actions.filter((a) => a.kind !== 'quiet' && a.kind !== 'fail' && a.kind !== 'chat')
|
|
171
179
|
b.idle = done.length === 0
|
|
172
180
|
}
|
|
173
|
-
|
|
181
|
+
// 只有真醒过的块才算「醒着但没做」:没有 wake: 的那块(quiet / 他说话时插的几句)
|
|
182
|
+
// 根本不是一次醒来。以前它被算进 idleWakes,于是 wakes=0 而 idleWakes=1。
|
|
183
|
+
counts.idleWakes = blocks.filter((b) => b.wake !== null && b.idle).length
|
|
174
184
|
|
|
175
185
|
// ---- 当前状态 ----
|
|
176
186
|
const lastAct = today.length ? today[today.length - 1] : null
|
|
@@ -219,7 +229,9 @@ export function deriveStatus({ events = [], config = {}, now = new Date() } = {}
|
|
|
219
229
|
const mins = plannedWakeAt ? Math.round((plannedWakeAt - nowMs) / 60_000) : null
|
|
220
230
|
detail = plannedWakeAt && plannedWakeAt > nowMs
|
|
221
231
|
? `约 ${new Date(plannedWakeAt).toTimeString().slice(0, 5)} 会睡醒${mins !== null && mins >= 0 ? `(还有 ${mins} 分钟)` : ''}`
|
|
222
|
-
:
|
|
232
|
+
: (quietToday
|
|
233
|
+
? '到点了,暂时没有可出的事件,还没醒'
|
|
234
|
+
: '今天已经到点,但还没醒')
|
|
223
235
|
} else {
|
|
224
236
|
phase = 'asleep'; label = '沉在水底'
|
|
225
237
|
detail = plannedWakeAt && plannedWakeAt > nowMs
|
|
@@ -241,6 +253,16 @@ export function deriveStatus({ events = [], config = {}, now = new Date() } = {}
|
|
|
241
253
|
const timeline = []
|
|
242
254
|
for (const b of blocks) {
|
|
243
255
|
const hhmm = new Date(b.at).toTimeString().slice(0, 5)
|
|
256
|
+
// 没有 wake: 的块不属于任何一次醒来(orphan)。以前拿当天第一条事件的时间
|
|
257
|
+
// 当块头,于是一天没醒过也会冒出「06:15 醒了,在发呆」(0.9.1 反馈)。
|
|
258
|
+
// 这些动作照原样列出来就好,别替她补一句「醒了」。
|
|
259
|
+
if (b.wake === null) {
|
|
260
|
+
for (const a of b.actions) {
|
|
261
|
+
const h2 = new Date(a.at).toTimeString().slice(0, 5)
|
|
262
|
+
timeline.push({ at: new Date(a.at).toISOString(), kind: a.kind, text: `${h2} ${a.text}` })
|
|
263
|
+
}
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
244
266
|
if (b.idle) {
|
|
245
267
|
timeline.push({ at: new Date(b.at).toISOString(), kind: 'idle', text: `${hhmm} 醒了,在发呆`, idle: true })
|
|
246
268
|
// 「醒来失败」「没有该问的事」正是这一块为什么什么都没做的原因,必须留下来,
|