@ssamurias/dsh-fish 0.8.9 → 0.9.1
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 +64 -10
- package/CHANGELOG.md +129 -2
- package/LICENSE +7 -0
- package/README.md +69 -17
- package/RELEASE-NOTES-0.8.0.md +87 -0
- package/RELEASE-NOTES-0.8.1.md +93 -0
- package/RELEASE-NOTES-0.8.2.md +57 -0
- package/RELEASE-NOTES-0.8.3.md +71 -0
- package/RELEASE-NOTES-0.8.4.md +47 -0
- package/RELEASE-NOTES-0.8.5.md +244 -0
- package/RELEASE-NOTES-0.8.6.md +224 -0
- package/RELEASE-NOTES-0.8.7.md +202 -0
- package/RELEASE-NOTES-0.8.8.md +224 -0
- package/RELEASE-NOTES-0.9.0.md +267 -0
- package/RELEASE-NOTES-0.9.1.md +296 -0
- package/RELEASE.md +151 -0
- package/THIRD-PARTY-NOTICES.md +3 -0
- package/lib/ask.js +18 -5
- package/lib/away.js +7 -2
- package/lib/browse.js +1 -0
- package/lib/capture.js +16 -1
- package/lib/channels.js +53 -13
- package/lib/client.js +145 -56
- package/lib/comment.js +74 -0
- package/lib/compact.js +71 -2
- package/lib/curiosity.js +6 -2
- package/lib/derive.js +21 -8
- package/lib/desk.js +4 -2
- package/lib/dream.js +3 -0
- package/lib/dreamtable.js +28 -2
- package/lib/drive.js +58 -9
- package/lib/freewake.js +21 -2
- package/lib/home.js +141 -24
- package/lib/inbox.js +7 -2
- package/lib/index.js +8 -12
- package/lib/interest.js +26 -4
- package/lib/log.js +22 -1
- package/lib/look.js +25 -5
- package/lib/note.js +35 -13
- package/lib/notify.js +2 -0
- package/lib/observe.js +22 -7
- package/lib/outing.js +2 -0
- package/lib/owner.js +5 -2
- package/lib/persona.js +18 -3
- package/lib/proactive.js +78 -18
- package/lib/say.js +104 -58
- package/lib/scheduler.js +162 -60
- package/lib/selfcheck.js +2 -3
- package/lib/selfdoc.js +8 -9
- package/lib/selfupdate.js +183 -41
- package/lib/settings.js +33 -2
- package/lib/status.js +31 -3
- package/lib/store.js +14 -2
- package/lib/tenant.js +12 -0
- package/lib/turns.js +46 -4
- package/lib/update.js +14 -0
- package/lib/upgrade.js +23 -4
- package/lib/voices.js +2 -0
- package/package.json +5 -2
- package/scripts/fix-legacy-link.mjs +254 -0
package/lib/scheduler.js
CHANGED
|
@@ -16,7 +16,7 @@ import { join } from 'node:path'
|
|
|
16
16
|
import { deriveSelf } from './derive.js'
|
|
17
17
|
import { validateEvent } from './schema.js'
|
|
18
18
|
import { readEvents, appendEvent, rootDir, ensureHer, paths, readJson } from './store.js'
|
|
19
|
-
import { loadConfig, dateKey, inWindow, pickWakeTime } from './settings.js'
|
|
19
|
+
import { loadConfig, dateKey, inWindow, pickWakeTime, updateConfig } from './settings.js'
|
|
20
20
|
import { plan } from './generator.js'
|
|
21
21
|
import { findTemplate } from './events.js'
|
|
22
22
|
import { offerPrompt, SLEEP_TODAY } from './persona.js'
|
|
@@ -29,28 +29,28 @@ import { seenOn } from './whatseen.js'
|
|
|
29
29
|
import { renderWatchList } from './history.js'
|
|
30
30
|
import { fetchCover, coverLines } from './cover.js'
|
|
31
31
|
import { riskState, browsePlan, noteRisk, isRiskCode } from './risk.js'
|
|
32
|
-
import { scanReplies, renderReplies } from './comment.js'
|
|
32
|
+
import { scanReplies, renderReplies, repliesScanDue } from './comment.js'
|
|
33
33
|
import { flushReport } from './report.js'
|
|
34
|
-
import { talkSessionId } from './home.js'
|
|
34
|
+
import { talkSessionId, homeStatus } from './home.js'
|
|
35
35
|
import { refreshSelfDoc } from './selfdoc.js'
|
|
36
|
-
import { setWakeSection, clearWakeSection, getWakeSection } from './identity.js'
|
|
36
|
+
import { setWakeSection, clearWakeSection, getWakeSection, liveAgentOf } from './identity.js'
|
|
37
|
+
import { queueNote, deliverQueued } from './sessionnote.js'
|
|
37
38
|
import { awayNow, awayLine } from './outing.js'
|
|
38
39
|
import { askInChat, picked } from './askinchat.js'
|
|
39
40
|
import { restedToday, settleStaleAsks } from './ask.js'
|
|
40
|
-
import {
|
|
41
|
-
import { proactiveState, proactiveSection, recentMaterial, looseEnds, TOOL_ASK_NOTE } from './proactive.js'
|
|
41
|
+
import { proactiveConfig, proactiveState, proactiveSection, recentMaterial, looseEnds, TOOL_ASK_NOTE } from './proactive.js'
|
|
42
42
|
import { deliverWake, ensureSessionLive, WAKE_PING } from './deliver.js'
|
|
43
|
-
import { followWake, planWakeCompaction, compactWake } from './compact.js'
|
|
43
|
+
import { followWake, planWakeCompaction, compactWake, looksLikeNothingSaid, looksLikeStreamFailure } from './compact.js'
|
|
44
44
|
import { logLine } from './log.js'
|
|
45
45
|
import { recordGrant } from './grant.js'
|
|
46
46
|
import { readNotes, renderNotes } from './note.js'
|
|
47
47
|
import { curiosityOf, renderEcho } from './curiosity.js'
|
|
48
48
|
import { deskFor } from './desk.js'
|
|
49
|
-
import { driveOf } from './drive.js'
|
|
50
|
-
import { interestOf,
|
|
49
|
+
import { driveOf, deskHasMaterial } from './drive.js'
|
|
50
|
+
import { interestOf, topicLabel } from './interest.js'
|
|
51
51
|
import { lastLeaveAt, lastSeenAt } from './curiosity.js'
|
|
52
52
|
import { dreamOn } from './dream.js'
|
|
53
|
-
import { hoursSinceLastWake, staleLines, tableFor } from './dreamtable.js'
|
|
53
|
+
import { hoursSinceLastWake, staleForDesk, staleLines, tableFor } from './dreamtable.js'
|
|
54
54
|
import { freeWakeFor } from './freewake.js'
|
|
55
55
|
import { readOutings, readPossessions } from './outing.js'
|
|
56
56
|
|
|
@@ -96,18 +96,27 @@ const DAY_END_CUE = '(今天到这里了。这一刻是我自己的。)'
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
99
|
+
* 等这一轮真说完:认准「我们自己排进去的那一轮」的 turn/end。
|
|
100
100
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* 于是 out/YYYY-MM-DD.md
|
|
101
|
+
* 为什么要有这一步(真机抓到):`sessionController.prompt()` 在把消息**排进队列**时就
|
|
102
|
+
* resolve 了,模型还在后面慢慢想。之前紧接着就 flushReport,报告在她开口前 8 秒就写完,
|
|
103
|
+
* 于是 out/YYYY-MM-DD.md 永远显示「今天她什么都没说」——她其实说了,只是晚了。
|
|
104
104
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
105
|
+
* 坑(issue #36):以前只认会话不认回合——同一会话里任何一个 turn/end 都算数。
|
|
106
|
+
* 手动「叫她一次」时她往往正开在别的一轮里(比如他刚发过话),宿主的 `agent.followup`
|
|
107
|
+
* 会把我们这条排进下一轮,于是**当前那一轮**先 turn/end,处境段(fish:wake)当场被清,
|
|
108
|
+
* 真正醒来那一轮反而什么都没有。
|
|
109
|
+
*
|
|
110
|
+
* 现在的判据:监听挂上之后,必须先见到本条会话的 `turn/start`,之后的 `turn/end`
|
|
111
|
+
* 才算「我们这一轮说完了」;turn/start 之前来的 turn/end 属于上一轮,忽略。
|
|
112
|
+
* 拿不到事件总线(单测/老宿主)时退回原来的即时行为,反正就绪后写一次就够了,
|
|
113
|
+
* 而且 flushReport 是幂等的重写,多写一次不会错。
|
|
108
114
|
*/
|
|
109
115
|
function watchTurnEnd(ctx, sessionId, done) {
|
|
110
116
|
let fired = false
|
|
117
|
+
// 宿主没给事件总线:没有回合可认,退回「等一会儿就动手」的老行为。
|
|
118
|
+
const noBus = typeof ctx.on !== 'function'
|
|
119
|
+
let started = noBus
|
|
111
120
|
const once = () => {
|
|
112
121
|
if (fired) return
|
|
113
122
|
fired = true
|
|
@@ -116,7 +125,8 @@ function watchTurnEnd(ctx, sessionId, done) {
|
|
|
116
125
|
const off = ctx.on?.('session/event', (session, event) => {
|
|
117
126
|
const id = session?.header?.id ?? session?.id
|
|
118
127
|
if (id !== sessionId) return
|
|
119
|
-
if (event?.type === 'turn/
|
|
128
|
+
if (event?.type === 'turn/start') { started = true; return }
|
|
129
|
+
if (event?.type === 'turn/end' && started) once()
|
|
120
130
|
})
|
|
121
131
|
return { off, once, get fired() { return fired } }
|
|
122
132
|
}
|
|
@@ -162,6 +172,17 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
162
172
|
const t = busy.get(id)
|
|
163
173
|
return t !== undefined && Date.now() - t < BUSY_STALE_MS
|
|
164
174
|
}
|
|
175
|
+
// 别的插件知道她在忙(比如在打一局游戏),这个调度器不知道。它们可以
|
|
176
|
+
// `ctx.on('dsh-fish/busy', (sessionId) => true)`,不用知道这个包叫什么、装在哪。
|
|
177
|
+
// 监听要同步返回,只有 true 算忙。抛错、返回 Promise 都按不忙算;Promise 的拒绝要接住,
|
|
178
|
+
// 宿主把未处理的拒绝当致命错误,别人的一个监听不该把整个进程带走。
|
|
179
|
+
const busyElsewhere = (id) => {
|
|
180
|
+
try {
|
|
181
|
+
const r = ctx.bail?.('dsh-fish/busy', id)
|
|
182
|
+
if (typeof r?.then === 'function') r.then(undefined, (e) => log('dsh-fish/busy 监听拒绝', e?.message ?? e))
|
|
183
|
+
return r === true
|
|
184
|
+
} catch { return false }
|
|
185
|
+
}
|
|
165
186
|
const waitIdle = async (id) => {
|
|
166
187
|
const until = Date.now() + IDLE_WAIT_MS
|
|
167
188
|
while (!disposed && isBusy(id) && Date.now() < until) await new Promise((r) => setTimeout(r, 2000))
|
|
@@ -179,12 +200,18 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
179
200
|
* 她可以选择开口,也可以什么都不做——不写任何「你应该」。
|
|
180
201
|
*/
|
|
181
202
|
async function maybeProactive({ events, config, dk, now }) {
|
|
182
|
-
|
|
203
|
+
// 2026-11-05(用户 m02634):「主动性」有开关。关着 = 她不会自己攒劲儿开一轮;
|
|
204
|
+
// 日程唤醒与手动叫醒都不经过这里,照样能用。
|
|
205
|
+
if (config?.drive?.enabled === false) {
|
|
206
|
+
if (proactiveSkip !== '主动性关着') { proactiveSkip = '主动性关着'; log('proactive skip', '主动性关着') }
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
183
209
|
// 2026-11-04(用户 m01466):把她的成长档案交给判定函数。
|
|
184
210
|
// 之前这里是 `proactiveState({ config, events, now })` —— 它收不到 model,
|
|
185
211
|
// 于是第 3 天和第 27 天算出来的概率一模一样(「自己开口的出口不该是恒温的」)。
|
|
186
|
-
// deriveSelf
|
|
187
|
-
|
|
212
|
+
// 关着的时候判定用不上 model,不算:tick 每分钟一次,deriveSelf 是同步的,
|
|
213
|
+
// 事件流长了会卡住主线程,而这个开关默认是关的。开着时和原来一样,先算再判定。
|
|
214
|
+
const model = proactiveConfig(config).enabled ? deriveSelf(events, { growthDays: config.growthDays }) : null
|
|
188
215
|
const st = proactiveState({ config, events, now, model })
|
|
189
216
|
if (!st.ok) {
|
|
190
217
|
if (st.why && st.why !== proactiveSkip) {
|
|
@@ -198,7 +225,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
198
225
|
lastProbe = now
|
|
199
226
|
// ---- 劲儿(2026-11-05,用户 m02031)----
|
|
200
227
|
// 用户的定案:把「什么时候开这一轮」从掷骰子换成攒劲儿。
|
|
201
|
-
// 底噪:每小时 0.12,攒到 0.8
|
|
228
|
+
// 底噪:每小时 0.12,攒到 0.8 想动一次,两次至少隔两小时;
|
|
202
229
|
// 冲动:桌上有没答的问题 +0.06、有新句子 +0.04、他回了她留的话 +0.12(封顶 +0.3);
|
|
203
230
|
// 折扣:她上一次开口没人接,底噪 ×0.7(打扰成本,Horvitz)。
|
|
204
231
|
// 这张桌子是**判据用的摘要**,不是正文那段(正文那段在 wake() 里现算现摆)。
|
|
@@ -210,7 +237,19 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
210
237
|
const leave = lastLeaveAt(events)
|
|
211
238
|
const seen = lastSeenAt(events)
|
|
212
239
|
const echoBack = !!(leave && seen && seen >= leave && now - leave <= 72 * 3600_000)
|
|
213
|
-
const
|
|
240
|
+
const driveDesk = { gaps: gaps.length, stale: 0, echo: echoBack, favorite: interest.length > 0 }
|
|
241
|
+
const pc = proactiveConfig(config)
|
|
242
|
+
const drv = driveOf({ events, now, stage: model.stage, desk: driveDesk, seenCountsAsHeard: pc.idleCountsSeen })
|
|
243
|
+
// 空桌不醒(2026-11-05,用户 m02937):桌上什么都没有时,劲儿再满也不起——
|
|
244
|
+
// 否则她只能回一句「没什么要说的」,既占额度又在对话里留一条噪音。
|
|
245
|
+
if (!deskHasMaterial(driveDesk)) {
|
|
246
|
+
const why = `桌上没有可说的东西(劲儿 ${drv.value.toFixed(2)})`
|
|
247
|
+
if (why !== proactiveSkip) {
|
|
248
|
+
proactiveSkip = why
|
|
249
|
+
log('drive', why)
|
|
250
|
+
}
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
214
253
|
if (!drv.ready) {
|
|
215
254
|
if (drv.why && drv.why !== proactiveSkip) {
|
|
216
255
|
proactiveSkip = `drive ${drv.value.toFixed(2)} · ${drv.why}`
|
|
@@ -222,10 +261,19 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
222
261
|
if (!sc) return false
|
|
223
262
|
const sessionId = await talkSessionId(ctx, { root, log })
|
|
224
263
|
if (!sessionId) return false
|
|
264
|
+
// 她正忙时不投:处境段会挂在正在跑的那一轮上,那一轮结束又把它清掉,
|
|
265
|
+
// 真正的这一轮就只收到一个空格。过 5 分钟再看(定时叫醒那条路一直有这个判断)。
|
|
266
|
+
const elsewhere = busyElsewhere(sessionId)
|
|
267
|
+
if (elsewhere || isBusy(sessionId)) {
|
|
268
|
+
lastProbe = now - 25 * 60_000
|
|
269
|
+
const why = elsewhere ? '别的插件说她在忙(dsh-fish/busy)' : '她正忙'
|
|
270
|
+
if (proactiveSkip !== why) { proactiveSkip = why; log('proactive skip', why) }
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
225
273
|
|
|
226
274
|
const material = recentMaterial(events, { now })
|
|
227
275
|
const loose = looseEnds(events, { now })
|
|
228
|
-
const text = proactiveSection({ now, state: st, material, loose })
|
|
276
|
+
const text = proactiveSection({ now, state: st, material, loose, sendTools: pc.sendTools, countSeen: pc.idleCountsSeen })
|
|
229
277
|
|
|
230
278
|
const requestId = `proactive-${now}`
|
|
231
279
|
// 这一轮她写的话不算他来过:唤醒时间窗开到 turn/end(投递只等入队,见 wakewin.js),
|
|
@@ -254,7 +302,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
254
302
|
|
|
255
303
|
// 投递之前先把这一轮盯起来(2026-11-05,用户 m00803)。认领信号是 rpcId:
|
|
256
304
|
// 每次投递一个唯一值,命中之前一律当「还没开始」,所以早挂不会认错轮。
|
|
257
|
-
const follow = followWake(ctx, { rpcId: requestId, log })
|
|
305
|
+
const follow = followWake(ctx, { rpcId: requestId, log, sendTools: pc.sendTools })
|
|
258
306
|
|
|
259
307
|
// 2026-11-04(用户 m00428):投递换成正解 —— 自建 `source.kind='fish-wake'`
|
|
260
308
|
// 的消息交给 `agent.followup()`。宿主把它画成一张默认收起的折叠条,
|
|
@@ -297,22 +345,32 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
297
345
|
return
|
|
298
346
|
}
|
|
299
347
|
const meta = { day: model.day, stage: model.stage, source: 'timer' }
|
|
300
|
-
|
|
348
|
+
// 「没什么要说的」不是开口(2026-11-05,用户 m02937):她写一句元话语
|
|
349
|
+
// 交代「这轮没话说」,在文本界面上几乎不可避免;按静默记、折掉,别当一次开口。
|
|
350
|
+
// 配了发送工具时正文到不了人,只认工具;没配时还是看正文
|
|
351
|
+
// 上游断流写成的那行红字也不是她开口(2026-11-06 真机看到的 `❌ [ACP] stream error`)
|
|
352
|
+
const broken = out.spoke === true && looksLikeStreamFailure(out.said)
|
|
353
|
+
const quiet = pc.sendTools.length > 0
|
|
354
|
+
? out.toolSpoke !== true
|
|
355
|
+
: (out.spoke !== true || broken || looksLikeNothingSaid(out.said))
|
|
356
|
+
if (!quiet) {
|
|
301
357
|
await appendEvent({ type: 'meta', text: `proactive-turn:${dk}:${now}` }, meta, root).catch(() => {})
|
|
302
358
|
log('proactive', `开了一次空手回合,她开口了(劲儿 ${drv.value.toFixed(2)})`)
|
|
303
359
|
return
|
|
304
360
|
}
|
|
305
361
|
await appendEvent({ type: 'meta', text: `proactive-silent:${dk}:${now}` }, meta, root).catch(() => {})
|
|
306
362
|
// 折叠只在拿到闭合区间时做:超时、或没认出那条 ping,out.endSeq 都是 -1。
|
|
307
|
-
const plan = out.endSeq >= 0
|
|
363
|
+
const plan = out.endSeq >= 0 && pc.foldSilent
|
|
308
364
|
? planWakeCompaction(out.events, { startSeq: out.startSeq, endSeq: out.endSeq, rpcId: requestId })
|
|
309
365
|
: null
|
|
310
366
|
// 一轮可能被宿主的投影切成几段:各段单独换,边界节点原样留着。
|
|
311
367
|
let folded = 0
|
|
312
368
|
if (plan) for (const run of plan.runs) if (await compactWake(out.session, run, { log })) folded += 1
|
|
369
|
+
const tail = broken ? ',这一轮上游断流了(那行报错不算她开口)'
|
|
370
|
+
: out.spoke === true ? ',只说了句没什么要说的' : ''
|
|
313
371
|
log('proactive', folded > 0
|
|
314
|
-
?
|
|
315
|
-
:
|
|
372
|
+
? `醒了一次,没有开口${tail}(这一轮折成 ${folded} 行,盖住 ${plan.shadowed} 个节点)`
|
|
373
|
+
: `醒了一次,没有开口${tail}(${out.ok ? '这轮没什么可折' : out.why})`)
|
|
316
374
|
} catch (err) {
|
|
317
375
|
log('proactive settle', err?.message ?? err)
|
|
318
376
|
}
|
|
@@ -386,6 +444,31 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
386
444
|
return
|
|
387
445
|
}
|
|
388
446
|
|
|
447
|
+
// ---- 有人回她了吗(2026-11-06,用户 m01641)----
|
|
448
|
+
// 原来这里只有 wake() 拼正文时顺带扫一次:没被叫醒的那天就完全看不到回复,
|
|
449
|
+
// 而设置页那个「多久看一次有没有人回复」一直是个空旋钮(scanEveryMin
|
|
450
|
+
// 全仓没有消费者)。现在按那个间隔真去问一次,问到了就把那几句排队投给她。
|
|
451
|
+
// 投递用 queueNote:她醒着就立刻补进当前这条会话,没开着就等她下次打开。
|
|
452
|
+
try {
|
|
453
|
+
if (repliesScanDue(config?.comment, now)) {
|
|
454
|
+
const waiting = await scanReplies({ root, events, dateKey: dk }).catch(() => [])
|
|
455
|
+
// 无论有没有人回都记一次:不记的话每 30 秒就会重新问一遍 B 站(-412 就是这么撞出来的)。
|
|
456
|
+
await updateConfig({ comment: { lastScanAt: new Date(now).toISOString() } }, root).catch(() => {})
|
|
457
|
+
if (waiting.length) {
|
|
458
|
+
const txt = renderReplies(waiting)
|
|
459
|
+
if (txt) {
|
|
460
|
+
await queueNote(root, txt).catch(() => null)
|
|
461
|
+
const h = await homeStatus(root).catch(() => null)
|
|
462
|
+
const agent = h?.sessionId ? liveAgentOf(ctx, h.sessionId) : null
|
|
463
|
+
if (agent) await deliverQueued(agent, root, { home: true }).catch(() => null)
|
|
464
|
+
log('comment', `有人回她了(${waiting.length} 条),已经递过去。`)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
} catch (err) {
|
|
469
|
+
log('comment', `扫回复失败:${err?.message ?? err}`)
|
|
470
|
+
}
|
|
471
|
+
|
|
389
472
|
// ---- 主动说话(0.8.1 本机实验,2026-10-30 用户 m04610)----
|
|
390
473
|
// 它不是唤醒:不占 wakesPerDay,也不推成长事件。只在 .dev 下生效。
|
|
391
474
|
// 开了就 return——这一趟由她自己决定说还是不说,不再叠一次醒一醒。
|
|
@@ -451,8 +534,9 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
451
534
|
// 轮换之后这里查的是旧会话忙不忙,新会话正说着话也照样插进去。
|
|
452
535
|
const sid = (await readJson(join(paths(root).root, 'home.json'), null).catch(() => null))?.sessionId
|
|
453
536
|
?? deps.sessionId
|
|
454
|
-
|
|
455
|
-
|
|
537
|
+
const elsewhere = sid ? busyElsewhere(sid) : false
|
|
538
|
+
if (sid && (elsewhere || isBusy(sid))) {
|
|
539
|
+
const why = elsewhere ? '别的插件说她在忙(dsh-fish/busy),等一等再叫' : '她正在说话,等这一轮结束再叫'
|
|
456
540
|
const next = shouldLogSkip(skipLogged, dk, now, why)
|
|
457
541
|
if (next) { skipLogged = next; log('skip', why) }
|
|
458
542
|
return
|
|
@@ -653,10 +737,16 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
653
737
|
// 旧句子:判「要不要空醒」要用渲染好的那段,摆桌子要的是原始行——一次算好两处都用。
|
|
654
738
|
let staleRows = []
|
|
655
739
|
let free = null
|
|
656
|
-
|
|
740
|
+
{
|
|
657
741
|
try {
|
|
658
742
|
const { events: evs } = await readEvents(root)
|
|
659
|
-
|
|
743
|
+
// 2026-11-06(issue #42):「兴趣」关掉时不再摆旧句子。
|
|
744
|
+
// 原来 freeWakeFor 只要 tableText 非空就判成 table,而 tableText 根本不看 interest 开关——
|
|
745
|
+
// 于是「关掉摆东西」之后,每 6 小时还是会因为旧句子空醒一次,而且那一轮既不想一想、
|
|
746
|
+
// 也不给规则申请、也不给「今天到这里」:14 天里 14 次全是旧句子。
|
|
747
|
+
// 旧句子属于「桌上那几样东西」里的一样,跟着兴趣开关走才自洽。
|
|
748
|
+
const staleOn = config?.interest?.enabled !== false
|
|
749
|
+
if (staleOn && !dreamOn(evs, model.day).length) {
|
|
660
750
|
const shownToday = evs.some((e) => e?.type === 'meta' && String(e.text ?? '').startsWith(`table:${dk}`))
|
|
661
751
|
const [{ outings }, { possessions }] = await Promise.all([readOutings(root), readPossessions(root)])
|
|
662
752
|
const tripSessions = new Set(
|
|
@@ -693,7 +783,15 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
693
783
|
// 顺序是刻意的:先看,再想。如果先出事件提示再塞素材,
|
|
694
784
|
// 她就会先按模板说话、再拿素材硬套——那是模板,不是她。
|
|
695
785
|
// 反过来(先看见世界的动静,再问她怎么想)才像一个人醒来的样子。
|
|
696
|
-
|
|
786
|
+
//
|
|
787
|
+
// 2026-11-05(用户 m01128):摆料那一轮不摆世界。这一轮只该有一张桌子——
|
|
788
|
+
// 旧句子 + 「这一轮没有待办」。B 站动静与惦记清单都会替她提供「最像活的那件事」,
|
|
789
|
+
// 18:44 真机她就是挑了旧句子里唯一像活的一条去核对,没往梦里想。
|
|
790
|
+
// 手动「让她去刷 B 站」(forceBrowse)本来就指定了这一轮做什么,不跟着空醒一起收。
|
|
791
|
+
// 2026-11-05(用户 m02562):刷站不再挂在唤醒上。所有醒来都只有桌子与世界之外的事,
|
|
792
|
+
// 想上网由她自己调工具(bili_*),不再由唤醒替她决定看什么。
|
|
793
|
+
const showWorld = false
|
|
794
|
+
const look = showWorld ? await lookBeforeWake(root, model, config, dk, deps, log) : { text: '', intro: '', picks: [] }
|
|
697
795
|
if (look.text) {
|
|
698
796
|
parts.push('', look.intro, '', look.text)
|
|
699
797
|
// 带链接的清单(2026-10-23 用户提):她能说出"有一条是一个人跑不起来了",
|
|
@@ -723,25 +821,33 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
723
821
|
// 交给 deskFor 并成一段,配额与去重都在那个纯函数里;回音是另一件事(他有没有来过),
|
|
724
822
|
// 仍然单独一句话,不并进桌子。
|
|
725
823
|
// 只摆**事实**:不派活、不写「你可以去想想」、不给任何数字——派活了就又变成任务清单。
|
|
726
|
-
//
|
|
824
|
+
// 2026-11-05(用户 m02634):「兴趣」有开关。关掉 = 桌子那几行(好奇/旧句子/回音)
|
|
825
|
+
// 一概不摆;缺工具那句仍留着,它不是活、是她世界里的常量。
|
|
727
826
|
// 读失败绝不拖垮一次唤醒:她醒着这件事比桌上的东西重要。
|
|
728
|
-
if (
|
|
827
|
+
if (config?.interest?.enabled !== false) {
|
|
729
828
|
try {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
829
|
+
// 摆料那一轮不摆桌子:这一轮只留旧句子(用户 m01128)。
|
|
830
|
+
{
|
|
831
|
+
const { events: evc } = await readEvents(root)
|
|
832
|
+
// 兴趣(2026-11-05,用户 m02031):反复出现的那件事排进同一张桌子,
|
|
833
|
+
// 不另起一段——「一张桌子」是 L 文档定下的约定。
|
|
834
|
+
const favItem = interestOf(evc, now.getTime())[0]
|
|
835
|
+
const fav = favItem ? topicLabel(favItem.text) : ''
|
|
836
|
+
const desk = deskFor({
|
|
837
|
+
gaps: curiosityOf(evc, now),
|
|
838
|
+
// 摆料那一轮 tableText 已经把旧句子摆出来了,桌子别再摆第二遍
|
|
839
|
+
// (2026-11-05 真机 18:44:同三句原话出现了两次)。
|
|
840
|
+
stale: staleForDesk(tableText, staleRows),
|
|
841
|
+
free: !!free,
|
|
842
|
+
favorite: fav,
|
|
843
|
+
at: now,
|
|
844
|
+
})
|
|
845
|
+
if (desk) parts.push('', desk)
|
|
846
|
+
const echo = renderEcho(evc, now)
|
|
847
|
+
if (echo) parts.push('', echo)
|
|
848
|
+
}
|
|
744
849
|
// 缺工具就开口要:每次醒来都摆这一句,不派活,只给路径。
|
|
850
|
+
// 摆料那一轮也留着——它不是活,是她世界里的常量(用户 m01128)。
|
|
745
851
|
parts.push('', TOOL_ASK_NOTE)
|
|
746
852
|
} catch (err) {
|
|
747
853
|
log('curiosity', err?.message ?? err)
|
|
@@ -936,13 +1042,14 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
936
1042
|
}
|
|
937
1043
|
|
|
938
1044
|
// 刷站那几十秒里她可能刚好开始了别的一轮(有人找她)。等那一轮说完再挂处境、再叫:
|
|
939
|
-
//
|
|
1045
|
+
// 挂早了会被那一轮的 turn/end 收走(issue #36 之前 watcher 只认会话不认回合,
|
|
1046
|
+
// 现在它至少要见到我们自己这一轮的 turn/start 才算数,双保险)。
|
|
940
1047
|
if (typeof deps.waitIdle === 'function') await deps.waitIdle(sessionId)
|
|
941
1048
|
|
|
942
1049
|
// 这一轮她写的话不算他来过:唤醒时间窗开到 turn/end,不是开到入队(见 wakewin.js)。
|
|
943
1050
|
// turn/end 不来时跟处境一起由下面的兜底关;没送进去就在 catch 里关。
|
|
944
1051
|
// 处境代次(2026-11-04,用户 m01466):见 maybeProactive 里的同一处注释。
|
|
945
|
-
//
|
|
1052
|
+
// 清的时候要带着自己挂上去时拿到的号,别把别人后来挂的处境一起抹了。
|
|
946
1053
|
let gen = 0
|
|
947
1054
|
const releaseWake = holdWake()
|
|
948
1055
|
const watcher = watchTurnEnd(ctx, sessionId, () => { releaseWake(); void writeReport(); clearWakeSection(gen) })
|
|
@@ -1159,14 +1266,9 @@ export async function wakeNow(ctx, deps = {}) {
|
|
|
1159
1266
|
const model = deriveSelf(events, { growthDays: config.growthDays })
|
|
1160
1267
|
const dk = dateKey()
|
|
1161
1268
|
const counts = { dateKey: dk, sent: 0, questions: 0, proactive: 0 }
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
|
|
1165
|
-
const forcedBrowse = deps.focus === 'browse'
|
|
1166
|
-
if (forcedBrowse) deps = { ...deps, forceBrowse: true }
|
|
1167
|
-
let decision = forcedBrowse
|
|
1168
|
-
? { action: 'wake', prompt: '(现在,去看一眼 B 站首页在推什么。)', template: { id: 'manual-browse' } }
|
|
1169
|
-
: plan(model, config, counts, events, { squeeze: gapSqueeze(gapSinceLastWake(events)) })
|
|
1269
|
+
// 2026-11-05(用户 m02562):「让她去刷 B 站」这个按钮砍掉了,
|
|
1270
|
+
// 手动叫醒一律走 plan(),和定时那条一样。
|
|
1271
|
+
let decision = plan(model, config, counts, events, { squeeze: gapSqueeze(gapSinceLastWake(events)) })
|
|
1170
1272
|
const log = deps.log ?? (() => {})
|
|
1171
1273
|
// 2026-11-04(用户 m01466):手动叫醒也要认最小间隔。
|
|
1172
1274
|
//
|
package/lib/selfcheck.js
CHANGED
|
@@ -18,7 +18,6 @@ import { readConfig, readEvents, readOwner } from './store.js'
|
|
|
18
18
|
import { inWindow, dateKey } from './settings.js'
|
|
19
19
|
import { detectFfmpeg } from './media.js'
|
|
20
20
|
import { FISH_TOOLS, HER_TOOLS, blockedTools } from './restrict.js'
|
|
21
|
-
import { devModeSync } from './devmode.js'
|
|
22
21
|
|
|
23
22
|
const MIN_NODE_MAJOR = 18
|
|
24
23
|
|
|
@@ -225,8 +224,8 @@ export async function runChecks(o = {}) {
|
|
|
225
224
|
// 于是公开版装完自检必然报红:「有 1 个工具没注册上:her_dream」——
|
|
226
225
|
// 和 2026-10-28 那次 read_image 误报同一类错误(拿「名单」当「真装上」的反面,
|
|
227
226
|
// 这次是拿「本机清单」当「公开版清单」)。判据与注册处同一份:`devModeSync`。
|
|
228
|
-
|
|
229
|
-
const expected =
|
|
227
|
+
// 0.9.0 起 her_dream 也恒注册,清单就是清单本身。
|
|
228
|
+
const expected = FISH_TOOLS
|
|
230
229
|
const notRegistered = T && typeof T.get === 'function' ? expected.filter((n) => !T.get(n)) : []
|
|
231
230
|
// read_image 是宿主的工具,别人的机器上可能没有。
|
|
232
231
|
// 2026-10-28:以前它只是跟在「手上的工具」后面一句附注,而且那一项还是绿的。
|
package/lib/selfdoc.js
CHANGED
|
@@ -26,9 +26,9 @@ import { promises as fs } from 'node:fs'
|
|
|
26
26
|
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
|
-
import { devModeSync } from './devmode.js'
|
|
30
29
|
import { deriveSelf } from './derive.js'
|
|
31
|
-
import { personaLine, toolBrief, RECALL_BRIEF, SPEAK_BRIEF, LETTER_BRIEF, NOTE_BRIEF, ASK_BRIEF,
|
|
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'
|
|
31
|
+
import { proactiveConfig } from './proactive.js'
|
|
32
32
|
import { LOOK_BRIEF, WATCH_RULE } from './browse.js'
|
|
33
33
|
import { activeCarry } from './home.js'
|
|
34
34
|
import { activeGrants } from './unlock.js'
|
|
@@ -121,7 +121,7 @@ export async function continuityOf(root = rootDir(), now = Date.now()) {
|
|
|
121
121
|
* @param {object} model deriveSelf 的结果
|
|
122
122
|
* @returns {string}
|
|
123
123
|
*/
|
|
124
|
-
export function renderSelfDoc(model, { budget, carry, continuity, browse = true, speak = false, grants = [], shown, diary = true, sleepEveryDays = 0, bansMuted = false, extraTools = [], dream = false, schedule = false, proactive = false } = {}) {
|
|
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
125
|
const head = [
|
|
126
126
|
// 这里**故意不写**「由 dsh-fish 自动生成」这类话。
|
|
127
127
|
// 这个文件会被宿主原样渲染进她的会话——她能看见每一行。
|
|
@@ -167,7 +167,7 @@ export function renderSelfDoc(model, { budget, carry, continuity, browse = true,
|
|
|
167
167
|
...(dream ? [DREAM_BRIEF, ''] : []),
|
|
168
168
|
// 主动说话(2026-10-30,用户 m04610):her_say 只在 .dev 下注册,
|
|
169
169
|
// 说明书也只在同一个判据下挂——公开版不能让「她」以为有这个工具。
|
|
170
|
-
...(proactive ? [
|
|
170
|
+
...(proactive ? [proactiveBrief(sendTools), ''] : []),
|
|
171
171
|
// 定时提醒(2026-10-30,用户 m04335 / m04504):默认不放,只有用户把
|
|
172
172
|
// schedule_create 加进白名单才挂——不然就是告诉她「你有这个工具」,伸手却没有。
|
|
173
173
|
...(schedule ? [SCHEDULE_BRIEF, ''] : []),
|
|
@@ -263,11 +263,10 @@ export async function writeSelfDoc(root = rootDir(), { log, exec, tenant } = {})
|
|
|
263
263
|
// 2026-10-27(用户 m01371):白名单里额外放行的工具要念给她听,
|
|
264
264
|
// 否则「能不能在 QQ 上说话」她只能回一句不知道。
|
|
265
265
|
extraTools: config?.tools?.allow ?? [],
|
|
266
|
-
// 梦(2026-10-30 m02837
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
proactive: devModeSync(root),
|
|
266
|
+
// 梦(2026-10-30 m02837)与主动说话(m04610):0.9.0 起公开,两处都恒为真。
|
|
267
|
+
dream: true,
|
|
268
|
+
proactive: true,
|
|
269
|
+
sendTools: proactiveConfig(config).sendTools,
|
|
271
270
|
// 定时工具默认不放;用户在白名单里加了 schedule_create 才告诉他这条路通。
|
|
272
271
|
schedule: (config?.tools?.allow ?? []).some((n) => String(n).trim() === 'schedule_create'),
|
|
273
272
|
})
|