@ssamurias/dsh-fish 0.9.0 → 0.9.2
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 +60 -4
- package/CHANGELOG.md +85 -1
- package/README.md +13 -6
- package/RELEASE-NOTES-0.9.1.md +296 -0
- package/RELEASE-NOTES-0.9.2.md +46 -0
- package/RELEASE.md +152 -0
- package/lib/absence.js +41 -27
- package/lib/ask.js +91 -30
- package/lib/askinchat.js +83 -1
- package/lib/browse.js +1 -0
- package/lib/capture.js +35 -2
- package/lib/cardanswer.js +84 -0
- package/lib/client.js +116 -19
- package/lib/comment.js +111 -2
- package/lib/compact.js +45 -1
- package/lib/curiosity.js +6 -2
- package/lib/deliver.js +46 -0
- package/lib/derive.js +25 -13
- package/lib/desk.js +3 -1
- package/lib/dreamtable.js +13 -2
- package/lib/drive.js +14 -2
- package/lib/events.js +25 -1
- package/lib/freewake.js +4 -1
- package/lib/generator.js +2 -1
- package/lib/grant.js +18 -2
- package/lib/home.js +142 -29
- package/lib/identity.js +111 -14
- package/lib/index.js +50 -10
- package/lib/interest.js +3 -1
- package/lib/keep.js +21 -9
- package/lib/keepsake.js +20 -13
- package/lib/look.js +23 -3
- package/lib/note.js +255 -20
- package/lib/observe.js +14 -4
- package/lib/owner.js +5 -2
- package/lib/persona.js +26 -9
- package/lib/proactive.js +67 -12
- package/lib/say.js +73 -13
- package/lib/scheduler.js +186 -56
- package/lib/schema.js +18 -0
- package/lib/selfdoc.js +5 -3
- package/lib/selfupdate.js +8 -1
- package/lib/settings.js +5 -0
- package/lib/status.js +56 -6
- package/lib/store.js +14 -2
- package/lib/tenant.js +12 -0
- package/lib/turns.js +47 -2
- package/lib/upgrade.js +4 -1
- package/package.json +2 -1
- package/scripts/fix-legacy-link.mjs +197 -90
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
|
*
|
|
@@ -16,32 +17,33 @@ import { join } from 'node:path'
|
|
|
16
17
|
import { deriveSelf } from './derive.js'
|
|
17
18
|
import { validateEvent } from './schema.js'
|
|
18
19
|
import { readEvents, appendEvent, rootDir, ensureHer, paths, readJson } from './store.js'
|
|
19
|
-
import { loadConfig, dateKey, inWindow, pickWakeTime } from './settings.js'
|
|
20
|
+
import { loadConfig, dateKey, inWindow, pickWakeTime, updateConfig } from './settings.js'
|
|
20
21
|
import { plan } from './generator.js'
|
|
21
22
|
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 } from './comment.js'
|
|
33
|
+
import { scanReplies, renderReplies, repliesScanDue, markRepliesSeen, REPLY_SHOW_MAX } from './comment.js'
|
|
33
34
|
import { flushReport } from './report.js'
|
|
34
|
-
import { talkSessionId } from './home.js'
|
|
35
|
+
import { talkSessionId, homeStatus } from './home.js'
|
|
35
36
|
import { refreshSelfDoc } from './selfdoc.js'
|
|
36
|
-
import { setWakeSection, clearWakeSection, getWakeSection } from './identity.js'
|
|
37
|
+
import { setWakeSection, clearWakeSection, getWakeSection, liveAgentOf } from './identity.js'
|
|
38
|
+
import { queueNote, deliverQueued } from './sessionnote.js'
|
|
37
39
|
import { awayNow, awayLine } from './outing.js'
|
|
38
40
|
import { askInChat, picked } from './askinchat.js'
|
|
39
41
|
import { restedToday, settleStaleAsks } from './ask.js'
|
|
40
|
-
import { proactiveState, proactiveSection, recentMaterial, looseEnds, TOOL_ASK_NOTE } from './proactive.js'
|
|
41
|
-
import { deliverWake, ensureSessionLive, WAKE_PING } from './deliver.js'
|
|
42
|
-
import { followWake, planWakeCompaction, compactWake, looksLikeNothingSaid } from './compact.js'
|
|
42
|
+
import { proactiveConfig, proactiveState, proactiveSection, recentMaterial, looseEnds, TOOL_ASK_NOTE } from './proactive.js'
|
|
43
|
+
import { deliverWake, ensureSessionLive, reviveIfBlank, WAKE_PING } from './deliver.js'
|
|
44
|
+
import { followWake, planWakeCompaction, compactWake, looksLikeNothingSaid, looksLikeStreamFailure } from './compact.js'
|
|
43
45
|
import { logLine } from './log.js'
|
|
44
|
-
import { recordGrant } from './grant.js'
|
|
46
|
+
import { recordGrant, GRANT_YES, GRANT_NO } from './grant.js'
|
|
45
47
|
import { readNotes, renderNotes } from './note.js'
|
|
46
48
|
import { curiosityOf, renderEcho } from './curiosity.js'
|
|
47
49
|
import { deskFor } from './desk.js'
|
|
@@ -61,6 +63,12 @@ const WAKE_TIMEOUT_MS = 5 * 60_000
|
|
|
61
63
|
const REPORT_WAIT_MS = 90_000
|
|
62
64
|
// 一天的第二次及以后的叫醒,离上一次至少隔这么久(见 nextWakeAt)
|
|
63
65
|
const WAKE_MIN_GAP_MS = 45 * 60_000
|
|
66
|
+
/** 一天最多试几次失败的叫醒。之后当天不再定时叫,手动叫不受影响。 */
|
|
67
|
+
const WAKE_FAIL_MAX = 3
|
|
68
|
+
/** 失败之后隔多久再试:第一次很快,时段快结束时只失败一次也不至于丢掉今天这一次。 */
|
|
69
|
+
const WAKE_FAIL_GAPS_MS = [5 * 60_000, WAKE_MIN_GAP_MS]
|
|
70
|
+
/** 手动叫醒失败只留记录,不算进定时的账:有人点了一下按钮,不该用掉今天定时的机会。 */
|
|
71
|
+
const WAKE_FAIL_MANUAL = '手动 '
|
|
64
72
|
// 她正在某一轮里时先等她说完,最多等这么久
|
|
65
73
|
const IDLE_WAIT_MS = 10 * 60_000
|
|
66
74
|
// turn/start 之后迟迟等不到 turn/end(宿主重启、事件丢了),超过这么久就不再当她在忙
|
|
@@ -95,18 +103,45 @@ const DAY_END_CUE = '(今天到这里了。这一刻是我自己的。)'
|
|
|
95
103
|
|
|
96
104
|
|
|
97
105
|
/**
|
|
98
|
-
*
|
|
106
|
+
* 今天失败的叫醒是不是已经够多,或者刚失败过。账记在事件流里,重启也算数。
|
|
107
|
+
* @param {Array} events
|
|
108
|
+
* @param {string} dk 今天的日期键
|
|
109
|
+
* @param {number} now
|
|
110
|
+
*/
|
|
111
|
+
export function wakeRetryBlocked(events = [], dk = dateKey(), now = Date.now()) {
|
|
112
|
+
let n = 0
|
|
113
|
+
let last = 0
|
|
114
|
+
for (const e of events) {
|
|
115
|
+
if (e?.type !== 'meta' || !String(e.text ?? '').startsWith(`wakefail:${dk}`)) continue
|
|
116
|
+
if (String(e.text).startsWith(`wakefail:${dk} ${WAKE_FAIL_MANUAL}`)) continue
|
|
117
|
+
n += 1
|
|
118
|
+
last = Math.max(last, Date.parse(e.ts || '') || 0)
|
|
119
|
+
}
|
|
120
|
+
return n >= WAKE_FAIL_MAX || (last > 0 && now - last < WAKE_FAIL_GAPS_MS[Math.min(n, WAKE_FAIL_GAPS_MS.length) - 1])
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 等这一轮真说完:认准「我们自己排进去的那一轮」的 turn/end。
|
|
125
|
+
*
|
|
126
|
+
* 为什么要有这一步(真机抓到):`sessionController.prompt()` 在把消息**排进队列**时就
|
|
127
|
+
* resolve 了,模型还在后面慢慢想。之前紧接着就 flushReport,报告在她开口前 8 秒就写完,
|
|
128
|
+
* 于是 out/YYYY-MM-DD.md 永远显示「今天她什么都没说」——她其实说了,只是晚了。
|
|
99
129
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
130
|
+
* 坑(issue #36):以前只认会话不认回合——同一会话里任何一个 turn/end 都算数。
|
|
131
|
+
* 手动「叫她一次」时她往往正开在别的一轮里(比如他刚发过话),宿主的 `agent.followup`
|
|
132
|
+
* 会把我们这条排进下一轮,于是**当前那一轮**先 turn/end,处境段(fish:wake)当场被清,
|
|
133
|
+
* 真正醒来那一轮反而什么都没有。
|
|
103
134
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
135
|
+
* 现在的判据:监听挂上之后,必须先见到本条会话的 `turn/start`,之后的 `turn/end`
|
|
136
|
+
* 才算「我们这一轮说完了」;turn/start 之前来的 turn/end 属于上一轮,忽略。
|
|
137
|
+
* 拿不到事件总线(单测/老宿主)时退回原来的即时行为,反正就绪后写一次就够了,
|
|
138
|
+
* 而且 flushReport 是幂等的重写,多写一次不会错。
|
|
107
139
|
*/
|
|
108
140
|
function watchTurnEnd(ctx, sessionId, done) {
|
|
109
141
|
let fired = false
|
|
142
|
+
// 宿主没给事件总线:没有回合可认,退回「等一会儿就动手」的老行为。
|
|
143
|
+
const noBus = typeof ctx.on !== 'function'
|
|
144
|
+
let started = noBus
|
|
110
145
|
const once = () => {
|
|
111
146
|
if (fired) return
|
|
112
147
|
fired = true
|
|
@@ -115,7 +150,8 @@ function watchTurnEnd(ctx, sessionId, done) {
|
|
|
115
150
|
const off = ctx.on?.('session/event', (session, event) => {
|
|
116
151
|
const id = session?.header?.id ?? session?.id
|
|
117
152
|
if (id !== sessionId) return
|
|
118
|
-
if (event?.type === 'turn/
|
|
153
|
+
if (event?.type === 'turn/start') { started = true; return }
|
|
154
|
+
if (event?.type === 'turn/end' && started) once()
|
|
119
155
|
})
|
|
120
156
|
return { off, once, get fired() { return fired } }
|
|
121
157
|
}
|
|
@@ -128,6 +164,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
128
164
|
let lastTick = 0
|
|
129
165
|
let running = false
|
|
130
166
|
let disposed = false
|
|
167
|
+
let lastAbsenceKey = null
|
|
131
168
|
let timer = null
|
|
132
169
|
let wakeTimer = null
|
|
133
170
|
// skip 日志去重(2026-10-28):同一小时、同一原因只记一次。
|
|
@@ -161,6 +198,17 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
161
198
|
const t = busy.get(id)
|
|
162
199
|
return t !== undefined && Date.now() - t < BUSY_STALE_MS
|
|
163
200
|
}
|
|
201
|
+
// 别的插件知道她在忙(比如在打一局游戏),这个调度器不知道。它们可以
|
|
202
|
+
// `ctx.on('dsh-fish/busy', (sessionId) => true)`,不用知道这个包叫什么、装在哪。
|
|
203
|
+
// 监听要同步返回,只有 true 算忙。抛错、返回 Promise 都按不忙算;Promise 的拒绝要接住,
|
|
204
|
+
// 宿主把未处理的拒绝当致命错误,别人的一个监听不该把整个进程带走。
|
|
205
|
+
const busyElsewhere = (id) => {
|
|
206
|
+
try {
|
|
207
|
+
const r = ctx.bail?.('dsh-fish/busy', id)
|
|
208
|
+
if (typeof r?.then === 'function') r.then(undefined, (e) => log('dsh-fish/busy 监听拒绝', e?.message ?? e))
|
|
209
|
+
return r === true
|
|
210
|
+
} catch { return false }
|
|
211
|
+
}
|
|
164
212
|
const waitIdle = async (id) => {
|
|
165
213
|
const until = Date.now() + IDLE_WAIT_MS
|
|
166
214
|
while (!disposed && isBusy(id) && Date.now() < until) await new Promise((r) => setTimeout(r, 2000))
|
|
@@ -187,8 +235,9 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
187
235
|
// 2026-11-04(用户 m01466):把她的成长档案交给判定函数。
|
|
188
236
|
// 之前这里是 `proactiveState({ config, events, now })` —— 它收不到 model,
|
|
189
237
|
// 于是第 3 天和第 27 天算出来的概率一模一样(「自己开口的出口不该是恒温的」)。
|
|
190
|
-
// deriveSelf
|
|
191
|
-
|
|
238
|
+
// 关着的时候判定用不上 model,不算:tick 每分钟一次,deriveSelf 是同步的,
|
|
239
|
+
// 事件流长了会卡住主线程,而这个开关默认是关的。开着时和原来一样,先算再判定。
|
|
240
|
+
const model = proactiveConfig(config).enabled ? deriveSelf(events, { growthDays: config.growthDays }) : null
|
|
192
241
|
const st = proactiveState({ config, events, now, model })
|
|
193
242
|
if (!st.ok) {
|
|
194
243
|
if (st.why && st.why !== proactiveSkip) {
|
|
@@ -215,7 +264,8 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
215
264
|
const seen = lastSeenAt(events)
|
|
216
265
|
const echoBack = !!(leave && seen && seen >= leave && now - leave <= 72 * 3600_000)
|
|
217
266
|
const driveDesk = { gaps: gaps.length, stale: 0, echo: echoBack, favorite: interest.length > 0 }
|
|
218
|
-
const
|
|
267
|
+
const pc = proactiveConfig(config)
|
|
268
|
+
const drv = driveOf({ events, now, stage: model.stage, desk: driveDesk, seenCountsAsHeard: pc.idleCountsSeen })
|
|
219
269
|
// 空桌不醒(2026-11-05,用户 m02937):桌上什么都没有时,劲儿再满也不起——
|
|
220
270
|
// 否则她只能回一句「没什么要说的」,既占额度又在对话里留一条噪音。
|
|
221
271
|
if (!deskHasMaterial(driveDesk)) {
|
|
@@ -237,10 +287,19 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
237
287
|
if (!sc) return false
|
|
238
288
|
const sessionId = await talkSessionId(ctx, { root, log })
|
|
239
289
|
if (!sessionId) return false
|
|
290
|
+
// 她正忙时不投:处境段会挂在正在跑的那一轮上,那一轮结束又把它清掉,
|
|
291
|
+
// 真正的这一轮就只收到一个空格。过 5 分钟再看(定时叫醒那条路一直有这个判断)。
|
|
292
|
+
const elsewhere = busyElsewhere(sessionId)
|
|
293
|
+
if (elsewhere || isBusy(sessionId)) {
|
|
294
|
+
lastProbe = now - 25 * 60_000
|
|
295
|
+
const why = elsewhere ? '别的插件说她在忙(dsh-fish/busy)' : '她正忙'
|
|
296
|
+
if (proactiveSkip !== why) { proactiveSkip = why; log('proactive skip', why) }
|
|
297
|
+
return false
|
|
298
|
+
}
|
|
240
299
|
|
|
241
300
|
const material = recentMaterial(events, { now })
|
|
242
301
|
const loose = looseEnds(events, { now })
|
|
243
|
-
const text = proactiveSection({ now, state: st, material, loose })
|
|
302
|
+
const text = proactiveSection({ now, state: st, material, loose, sendTools: pc.sendTools, countSeen: pc.idleCountsSeen })
|
|
244
303
|
|
|
245
304
|
const requestId = `proactive-${now}`
|
|
246
305
|
// 这一轮她写的话不算他来过:唤醒时间窗开到 turn/end(投递只等入队,见 wakewin.js),
|
|
@@ -253,7 +312,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
253
312
|
let gen = 0
|
|
254
313
|
const watcher = watchTurnEnd(ctx, sessionId, () => {
|
|
255
314
|
releaseWake()
|
|
256
|
-
clearWakeSection(gen)
|
|
315
|
+
clearWakeSection(gen, sessionId)
|
|
257
316
|
})
|
|
258
317
|
try {
|
|
259
318
|
// 2026-11-05(用户 m03265):原来这里是 `sc.create({ sessionId, cwd })`,
|
|
@@ -262,14 +321,16 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
262
321
|
// 现在先探活;探不通才退回建(老宿主没有 resolveAgent 时也走这条)。
|
|
263
322
|
const live = await ensureSessionLive(sc, sessionId)
|
|
264
323
|
if (!live.live) {
|
|
265
|
-
log('proactive:
|
|
266
|
-
|
|
324
|
+
log('proactive: 会话探活没通过,看看能不能按原 id 唤回来', live.why ?? '')
|
|
325
|
+
// 2026-11-06(换房报告):跑过 turn 的会话给不出原 id,裸 create 只会
|
|
326
|
+
// 另开一条空壳 —— 交给 reviveIfBlank 判,见 lib/deliver.js。
|
|
327
|
+
await reviveIfBlank(sc, sessionId, await ensureHer(root), (msg) => log(`proactive: ${msg}`)).catch(() => {})
|
|
267
328
|
}
|
|
268
|
-
gen = setWakeSection(text)
|
|
329
|
+
gen = setWakeSection(text, sessionId)
|
|
269
330
|
|
|
270
331
|
// 投递之前先把这一轮盯起来(2026-11-05,用户 m00803)。认领信号是 rpcId:
|
|
271
332
|
// 每次投递一个唯一值,命中之前一律当「还没开始」,所以早挂不会认错轮。
|
|
272
|
-
const follow = followWake(ctx, { rpcId: requestId, log })
|
|
333
|
+
const follow = followWake(ctx, { rpcId: requestId, log, sendTools: pc.sendTools })
|
|
273
334
|
|
|
274
335
|
// 2026-11-04(用户 m00428):投递换成正解 —— 自建 `source.kind='fish-wake'`
|
|
275
336
|
// 的消息交给 `agent.followup()`。宿主把它画成一张默认收起的折叠条,
|
|
@@ -289,7 +350,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
289
350
|
// 没送进去就没有这一轮:处境和窗口当场收掉,监听也摘掉,
|
|
290
351
|
// 不然要等到别的哪一轮结束才清,那一轮开头还挂着这次的处境(跟 wake() 失败时一样)
|
|
291
352
|
try { follow.off() } catch { /* ignore */ }
|
|
292
|
-
clearWakeSection(gen)
|
|
353
|
+
clearWakeSection(gen, sessionId)
|
|
293
354
|
|
|
294
355
|
releaseWake()
|
|
295
356
|
try { watcher.off?.() } catch { /* ignore */ }
|
|
@@ -314,7 +375,12 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
314
375
|
const meta = { day: model.day, stage: model.stage, source: 'timer' }
|
|
315
376
|
// 「没什么要说的」不是开口(2026-11-05,用户 m02937):她写一句元话语
|
|
316
377
|
// 交代「这轮没话说」,在文本界面上几乎不可避免;按静默记、折掉,别当一次开口。
|
|
317
|
-
|
|
378
|
+
// 配了发送工具时正文到不了人,只认工具;没配时还是看正文
|
|
379
|
+
// 上游断流写成的那行红字也不是她开口(2026-11-06 真机看到的 `❌ [ACP] stream error`)
|
|
380
|
+
const broken = out.spoke === true && looksLikeStreamFailure(out.said)
|
|
381
|
+
const quiet = pc.sendTools.length > 0
|
|
382
|
+
? out.toolSpoke !== true
|
|
383
|
+
: (out.spoke !== true || broken || looksLikeNothingSaid(out.said))
|
|
318
384
|
if (!quiet) {
|
|
319
385
|
await appendEvent({ type: 'meta', text: `proactive-turn:${dk}:${now}` }, meta, root).catch(() => {})
|
|
320
386
|
log('proactive', `开了一次空手回合,她开口了(劲儿 ${drv.value.toFixed(2)})`)
|
|
@@ -322,13 +388,17 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
322
388
|
}
|
|
323
389
|
await appendEvent({ type: 'meta', text: `proactive-silent:${dk}:${now}` }, meta, root).catch(() => {})
|
|
324
390
|
// 折叠只在拿到闭合区间时做:超时、或没认出那条 ping,out.endSeq 都是 -1。
|
|
325
|
-
const plan = out.endSeq >= 0
|
|
391
|
+
const plan = out.endSeq >= 0 && pc.foldSilent
|
|
326
392
|
? planWakeCompaction(out.events, { startSeq: out.startSeq, endSeq: out.endSeq, rpcId: requestId })
|
|
327
393
|
: null
|
|
328
394
|
// 一轮可能被宿主的投影切成几段:各段单独换,边界节点原样留着。
|
|
329
395
|
let folded = 0
|
|
330
396
|
if (plan) for (const run of plan.runs) if (await compactWake(out.session, run, { log })) folded += 1
|
|
331
|
-
const tail =
|
|
397
|
+
const tail = broken ? ',这一轮上游断流了(那行报错不算她开口)'
|
|
398
|
+
: out.spoke !== true ? ''
|
|
399
|
+
// 配了发送工具时,只写正文是「没发出去」,不见得是没话说,这句可以写得很长。
|
|
400
|
+
// 也可能调了工具但失败了,所以不说「没调」
|
|
401
|
+
: pc.sendTools.length > 0 ? ',只写了正文,没发出去' : ',只说了句没什么要说的'
|
|
332
402
|
log('proactive', folded > 0
|
|
333
403
|
? `醒了一次,没有开口${tail}(这一轮折成 ${folded} 行,盖住 ${plan.shadowed} 个节点)`
|
|
334
404
|
: `醒了一次,没有开口${tail}(${out.ok ? '这轮没什么可折' : out.why})`)
|
|
@@ -340,7 +410,7 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
340
410
|
} catch (err) {
|
|
341
411
|
// 没送进去就没有这一轮:处境和窗口当场收掉,监听也摘掉,
|
|
342
412
|
// 不然要等到别的哪一轮结束才清,那一轮开头还挂着这次的处境(跟 wake() 失败时一样)
|
|
343
|
-
clearWakeSection(gen)
|
|
413
|
+
clearWakeSection(gen, sessionId)
|
|
344
414
|
releaseWake()
|
|
345
415
|
try { watcher.off?.() } catch { /* ignore */ }
|
|
346
416
|
throw err
|
|
@@ -383,11 +453,19 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
383
453
|
// 放在 tick 最前面,因为 wake 的提示词要读 absenceDays;
|
|
384
454
|
// 晚一步的话今天这次唤醒就用的是昨天的冷落结论。
|
|
385
455
|
const absence = await detectAbsence(root)
|
|
456
|
+
const { events } = await readEvents(root)
|
|
386
457
|
// 冷落条数变了 = 她对自己的看法变了,身份文件要跟着重写。
|
|
387
458
|
// 不写的话那份 system prompt 段会一直停在上一次的值上(她"不知道自己被晾过")。
|
|
388
|
-
|
|
459
|
+
// 他回来了、天数涨了也要重写:不然她一边跟他聊,身份里还写着「他有 2 天没出现」。
|
|
460
|
+
// 身份里的天数从 absence 写下那一刻起算,和 detectAbsence 的不在同一时刻跳(absence
|
|
461
|
+
// 写得晚的时候),两个都要看,不然会把旧数重写一遍,身份比叫醒的提示词晚一天。
|
|
462
|
+
// 初值是 null,因为启动时已经写过一次,第一拍不用再写。
|
|
463
|
+
const lastAbsence = lastAbsenceEvent(events)
|
|
464
|
+
const daysSinceWritten = lastAbsence ? Math.floor((now - Date.parse(String(lastAbsence.ts ?? 0))) / 86_400_000) : 0
|
|
465
|
+
const absenceKey = `${absence?.why ?? ''}:${absence?.days ?? 0}:${daysSinceWritten}`
|
|
466
|
+
if (absence?.written || (lastAbsenceKey !== null && absenceKey !== lastAbsenceKey)) await refreshSelfDoc(root, { log })
|
|
467
|
+
lastAbsenceKey = absenceKey
|
|
389
468
|
|
|
390
|
-
const { events } = await readEvents(root)
|
|
391
469
|
const { config } = await loadConfig(root)
|
|
392
470
|
const dk = dateKey()
|
|
393
471
|
|
|
@@ -405,11 +483,44 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
405
483
|
return
|
|
406
484
|
}
|
|
407
485
|
|
|
486
|
+
// ---- 有人回她了吗(2026-11-06,用户 m01641)----
|
|
487
|
+
// 原来这里只有 wake() 拼正文时顺带扫一次:没被叫醒的那天就完全看不到回复,
|
|
488
|
+
// 而设置页那个「多久看一次有没有人回复」一直是个空旋钮(scanEveryMin
|
|
489
|
+
// 全仓没有消费者)。现在按那个间隔真去问一次,问到了就把那几句排队投给她。
|
|
490
|
+
// 投递用 queueNote:她醒着就立刻补进当前这条会话,没开着就等她下次打开。
|
|
491
|
+
try {
|
|
492
|
+
if (repliesScanDue(config?.comment, now)) {
|
|
493
|
+
const waiting = await scanReplies({ root, events, dateKey: dk }).catch(() => [])
|
|
494
|
+
// 无论有没有人回都记一次:不记的话每 30 秒就会重新问一遍 B 站(-412 就是这么撞出来的)。
|
|
495
|
+
await updateConfig({ comment: { lastScanAt: new Date(now).toISOString() } }, root).catch(() => {})
|
|
496
|
+
if (waiting.length) {
|
|
497
|
+
// 只递、只标前几条:标 seen 的是「真摆到她眼前的」,不是「扫到的」。
|
|
498
|
+
// 标多了等于把没看见的悄悄吞掉;不标就会一直重复投(2026-11-06 用户报的 bug)。
|
|
499
|
+
const shown = waiting.slice(0, REPLY_SHOW_MAX)
|
|
500
|
+
const txt = renderReplies(shown)
|
|
501
|
+
if (txt) {
|
|
502
|
+
await queueNote(root, txt).catch(() => null)
|
|
503
|
+
const h = await homeStatus(root).catch(() => null)
|
|
504
|
+
const agent = h?.sessionId ? liveAgentOf(ctx, h.sessionId) : null
|
|
505
|
+
if (agent) await deliverQueued(agent, root, { home: true }).catch(() => null)
|
|
506
|
+
await markRepliesSeen(root, shown, { dateKey: dk }).catch(() => 0)
|
|
507
|
+
log('comment', `有人回她了(${waiting.length} 条),已经递过去。`)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} catch (err) {
|
|
512
|
+
log('comment', `扫回复失败:${err?.message ?? err}`)
|
|
513
|
+
}
|
|
514
|
+
|
|
408
515
|
// ---- 主动说话(0.8.1 本机实验,2026-10-30 用户 m04610)----
|
|
409
516
|
// 它不是唤醒:不占 wakesPerDay,也不推成长事件。只在 .dev 下生效。
|
|
410
517
|
// 开了就 return——这一趟由她自己决定说还是不说,不再叠一次醒一醒。
|
|
518
|
+
//
|
|
519
|
+
// 2026-11-06(0.9.2 反馈):主动性也要受「每天几点醒」约束。
|
|
520
|
+
// 她要是能在窗口外自己醒过来找他,这个设置就等于没设——
|
|
521
|
+
// 用户关掉的是「这个时段别来打扰」,不是「别按日程醒来但可以主动开口」。
|
|
411
522
|
try {
|
|
412
|
-
if (await maybeProactive({ events, config, dk, now })) return
|
|
523
|
+
if (inWindow(config, new Date(now)) && await maybeProactive({ events, config, dk, now })) return
|
|
413
524
|
} catch (err) {
|
|
414
525
|
log('proactive failed', err?.message ?? String(err))
|
|
415
526
|
}
|
|
@@ -423,6 +534,9 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
423
534
|
const counts = await todayCounts(events, dk)
|
|
424
535
|
if (now < scheduled.at) return // 还没到今天的落点
|
|
425
536
|
|
|
537
|
+
// 叫醒一直失败时(会话没了、宿主起不来)别每分钟再试一次,每次都往事件流写一行 wakefail。
|
|
538
|
+
if (wakeRetryBlocked(events, dk, now)) return
|
|
539
|
+
|
|
426
540
|
const model = deriveSelf(events, { growthDays: config.growthDays })
|
|
427
541
|
// 空档补偿只影响这一次挑选的冷却判断(见 gapSqueeze)
|
|
428
542
|
const decision = plan(model, config, counts, events, { squeeze: gapSqueeze(gapSinceLastWake(events)) })
|
|
@@ -470,8 +584,9 @@ export function startScheduler(ctx, deps = {}) {
|
|
|
470
584
|
// 轮换之后这里查的是旧会话忙不忙,新会话正说着话也照样插进去。
|
|
471
585
|
const sid = (await readJson(join(paths(root).root, 'home.json'), null).catch(() => null))?.sessionId
|
|
472
586
|
?? deps.sessionId
|
|
473
|
-
|
|
474
|
-
|
|
587
|
+
const elsewhere = sid ? busyElsewhere(sid) : false
|
|
588
|
+
if (sid && (elsewhere || isBusy(sid))) {
|
|
589
|
+
const why = elsewhere ? '别的插件说她在忙(dsh-fish/busy),等一等再叫' : '她正在说话,等这一轮结束再叫'
|
|
475
590
|
const next = shouldLogSkip(skipLogged, dk, now, why)
|
|
476
591
|
if (next) { skipLogged = next; log('skip', why) }
|
|
477
592
|
return
|
|
@@ -675,7 +790,13 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
675
790
|
{
|
|
676
791
|
try {
|
|
677
792
|
const { events: evs } = await readEvents(root)
|
|
678
|
-
|
|
793
|
+
// 2026-11-06(issue #42):「兴趣」关掉时不再摆旧句子。
|
|
794
|
+
// 原来 freeWakeFor 只要 tableText 非空就判成 table,而 tableText 根本不看 interest 开关——
|
|
795
|
+
// 于是「关掉摆东西」之后,每 6 小时还是会因为旧句子空醒一次,而且那一轮既不想一想、
|
|
796
|
+
// 也不给规则申请、也不给「今天到这里」:14 天里 14 次全是旧句子。
|
|
797
|
+
// 旧句子属于「桌上那几样东西」里的一样,跟着兴趣开关走才自洽。
|
|
798
|
+
const staleOn = config?.interest?.enabled !== false
|
|
799
|
+
if (staleOn && !dreamOn(evs, model.day).length) {
|
|
679
800
|
const shownToday = evs.some((e) => e?.type === 'meta' && String(e.text ?? '').startsWith(`table:${dk}`))
|
|
680
801
|
const [{ outings }, { possessions }] = await Promise.all([readOutings(root), readPossessions(root)])
|
|
681
802
|
const tripSessions = new Set(
|
|
@@ -796,8 +917,13 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
796
917
|
const events = (await readEvents(root).catch(() => ({ events: [] }))).events
|
|
797
918
|
const waiting = await scanReplies({ root, events, dateKey: dk }).catch(() => [])
|
|
798
919
|
if (waiting.length) {
|
|
799
|
-
const
|
|
800
|
-
|
|
920
|
+
const shown = waiting.slice(0, REPLY_SHOW_MAX)
|
|
921
|
+
const txt = renderReplies(shown)
|
|
922
|
+
if (txt) {
|
|
923
|
+
parts.push('', txt)
|
|
924
|
+
// 摆进叫醒消息就算提醒过了:不标的话,她没回就会每次叫醒都再投一遍。
|
|
925
|
+
await markRepliesSeen(root, shown, { dateKey: dk }).catch(() => 0)
|
|
926
|
+
}
|
|
801
927
|
}
|
|
802
928
|
// 能说话这件事的说明在身份文档的「在网上说话」一节(开关打开时 selfdoc 才写),
|
|
803
929
|
// 不在这儿重复。
|
|
@@ -937,10 +1063,9 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
937
1063
|
// 先探活,探不通才退回建一条。
|
|
938
1064
|
const live = await ensureSessionLive(sc, sessionId)
|
|
939
1065
|
if (!live.live) {
|
|
940
|
-
log('wake:
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
} catch { /* 已存在,继续用 */ }
|
|
1066
|
+
log('wake: 会话探活没通过,看看能不能按原 id 唤回来', live.why ?? '')
|
|
1067
|
+
// 同上(2026-11-06 换房报告):只有宿主确实给不出这一条时才建。
|
|
1068
|
+
await reviveIfBlank(sc, sessionId, herDir, (msg) => log(`wake: ${msg}`)).catch(() => {})
|
|
944
1069
|
}
|
|
945
1070
|
|
|
946
1071
|
// 2026-11-04:`AbortController` 与那个 5 分钟 kill 定时器一起删掉了。
|
|
@@ -971,19 +1096,22 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
971
1096
|
}
|
|
972
1097
|
|
|
973
1098
|
// 刷站那几十秒里她可能刚好开始了别的一轮(有人找她)。等那一轮说完再挂处境、再叫:
|
|
974
|
-
//
|
|
1099
|
+
// 挂早了会被那一轮的 turn/end 收走(issue #36 之前 watcher 只认会话不认回合,
|
|
1100
|
+
// 现在它至少要见到我们自己这一轮的 turn/start 才算数,双保险)。
|
|
975
1101
|
if (typeof deps.waitIdle === 'function') await deps.waitIdle(sessionId)
|
|
1102
|
+
if (deps.isDisposed?.()) return
|
|
976
1103
|
|
|
977
1104
|
// 这一轮她写的话不算他来过:唤醒时间窗开到 turn/end,不是开到入队(见 wakewin.js)。
|
|
978
1105
|
// turn/end 不来时跟处境一起由下面的兜底关;没送进去就在 catch 里关。
|
|
979
1106
|
// 处境代次(2026-11-04,用户 m01466):见 maybeProactive 里的同一处注释。
|
|
980
|
-
//
|
|
1107
|
+
// 清的时候要带着自己挂上去时拿到的号,别把别人后来挂的处境一起抹了。
|
|
981
1108
|
let gen = 0
|
|
1109
|
+
// 先挂监听再开窗:ctx 已停用时 ctx.on 会抛,开了窗就再也关不上
|
|
1110
|
+
const watcher = watchTurnEnd(ctx, sessionId, () => { releaseWake(); void writeReport(); clearWakeSection(gen, sessionId) })
|
|
982
1111
|
const releaseWake = holdWake()
|
|
983
|
-
const watcher = watchTurnEnd(ctx, sessionId, () => { releaseWake(); void writeReport(); clearWakeSection(gen) })
|
|
984
1112
|
|
|
985
1113
|
// 处境走 system prompt 段;对话里只留一个空投递(她看到的是「有人来了」,不是剧本)。
|
|
986
|
-
gen = setWakeSection(wakeText)
|
|
1114
|
+
gen = setWakeSection(wakeText, sessionId)
|
|
987
1115
|
// 2026-11-04(用户 m01466):投递前的最后一道护栏。
|
|
988
1116
|
//
|
|
989
1117
|
// 真机事故里出现过「消息投出去了、system prompt 里一个字处境都没有」的一轮:
|
|
@@ -993,8 +1121,8 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
993
1121
|
//
|
|
994
1122
|
// 治本是上面的最小间隔(不该有两次挨这么近的唤醒);这里只是兜底:
|
|
995
1123
|
// 万一处境真的空了,宁可不叫,也别投一条空壳让她自己找台阶下。
|
|
996
|
-
if (!getWakeSection().trim()) {
|
|
997
|
-
clearWakeSection(gen)
|
|
1124
|
+
if (!getWakeSection(sessionId).trim()) {
|
|
1125
|
+
clearWakeSection(gen, sessionId)
|
|
998
1126
|
releaseWake()
|
|
999
1127
|
try { watcher.off?.() } catch { /* ignore */ }
|
|
1000
1128
|
log('wake skipped: 处境是空的')
|
|
@@ -1079,7 +1207,7 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
1079
1207
|
// 上一个调度器留下的窗口没关,下一轮 turn/end 之后 inWake() 仍是 true。
|
|
1080
1208
|
if (deps.isDisposed?.()) {
|
|
1081
1209
|
try { watcher.off?.() } catch { /* ignore */ }
|
|
1082
|
-
clearWakeSection(gen)
|
|
1210
|
+
clearWakeSection(gen, sessionId)
|
|
1083
1211
|
releaseWake()
|
|
1084
1212
|
return
|
|
1085
1213
|
}
|
|
@@ -1088,16 +1216,16 @@ async function wake(ctx, root, model, config, decision, dk, log, deps = {}) {
|
|
|
1088
1216
|
return
|
|
1089
1217
|
}
|
|
1090
1218
|
try { watcher.off?.() } catch { /* ignore */ }
|
|
1091
|
-
if (!watcher.fired) clearWakeSection(gen)
|
|
1219
|
+
if (!watcher.fired) clearWakeSection(gen, sessionId)
|
|
1092
1220
|
releaseWake()
|
|
1093
1221
|
}
|
|
1094
1222
|
setTimeout(fallback, REPORT_WAIT_MS).unref?.()
|
|
1095
1223
|
} catch (err) {
|
|
1096
1224
|
// 这一次没起来,处境要立刻清掉,唤醒时间窗也关上。
|
|
1097
|
-
clearWakeSection(gen)
|
|
1225
|
+
clearWakeSection(gen, sessionId)
|
|
1098
1226
|
releaseWake()
|
|
1099
1227
|
await appendEvent(
|
|
1100
|
-
{ type: 'meta', text: `wakefail:${dk} ${String(err?.message ?? err).slice(0, 120)}` },
|
|
1228
|
+
{ type: 'meta', text: `wakefail:${dk} ${deps.manual ? WAKE_FAIL_MANUAL : ''}${String(err?.message ?? err).slice(0, 120)}` },
|
|
1101
1229
|
{ day: model.day, stage: model.stage, source: 'timer' },
|
|
1102
1230
|
root,
|
|
1103
1231
|
).catch(() => {})
|
|
@@ -1125,12 +1253,14 @@ async function askOfferInChat(ctx, root, { sessionId, offer, log = () => {} } =
|
|
|
1125
1253
|
try {
|
|
1126
1254
|
const r = await askInChat(ctx, {
|
|
1127
1255
|
sessionId,
|
|
1256
|
+
root,
|
|
1257
|
+
cardId: `grant:${offer.id}`,
|
|
1128
1258
|
questions: [
|
|
1129
1259
|
{
|
|
1130
1260
|
id: 'grant',
|
|
1131
1261
|
header: '大肥鱼想申请一件事',
|
|
1132
1262
|
question: `她想试试「${offer.title}」。不是现在就要,是想问问你。`,
|
|
1133
|
-
options: [{ label:
|
|
1263
|
+
options: [{ label: GRANT_YES }, { label: GRANT_NO }],
|
|
1134
1264
|
},
|
|
1135
1265
|
],
|
|
1136
1266
|
})
|
|
@@ -1139,7 +1269,7 @@ async function askOfferInChat(ctx, root, { sessionId, offer, log = () => {} } =
|
|
|
1139
1269
|
return
|
|
1140
1270
|
}
|
|
1141
1271
|
const said = picked(r.answers, 'grant')
|
|
1142
|
-
const verdict = said ===
|
|
1272
|
+
const verdict = said === GRANT_YES ? 'approve' : said === GRANT_NO ? 'reject' : ''
|
|
1143
1273
|
if (!verdict) return
|
|
1144
1274
|
const w = await recordGrant(root, { offerId: offer.id, rule: offer.rule, verdict })
|
|
1145
1275
|
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,7 +27,8 @@ 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,
|
|
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'
|
|
31
32
|
import { LOOK_BRIEF, WATCH_RULE } from './browse.js'
|
|
32
33
|
import { activeCarry } from './home.js'
|
|
33
34
|
import { activeGrants } from './unlock.js'
|
|
@@ -120,7 +121,7 @@ export async function continuityOf(root = rootDir(), now = Date.now()) {
|
|
|
120
121
|
* @param {object} model deriveSelf 的结果
|
|
121
122
|
* @returns {string}
|
|
122
123
|
*/
|
|
123
|
-
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 } = {}) {
|
|
124
125
|
const head = [
|
|
125
126
|
// 这里**故意不写**「由 dsh-fish 自动生成」这类话。
|
|
126
127
|
// 这个文件会被宿主原样渲染进她的会话——她能看见每一行。
|
|
@@ -166,7 +167,7 @@ export function renderSelfDoc(model, { budget, carry, continuity, browse = true,
|
|
|
166
167
|
...(dream ? [DREAM_BRIEF, ''] : []),
|
|
167
168
|
// 主动说话(2026-10-30,用户 m04610):her_say 只在 .dev 下注册,
|
|
168
169
|
// 说明书也只在同一个判据下挂——公开版不能让「她」以为有这个工具。
|
|
169
|
-
...(proactive ? [
|
|
170
|
+
...(proactive ? [proactiveBrief(sendTools), ''] : []),
|
|
170
171
|
// 定时提醒(2026-10-30,用户 m04335 / m04504):默认不放,只有用户把
|
|
171
172
|
// schedule_create 加进白名单才挂——不然就是告诉她「你有这个工具」,伸手却没有。
|
|
172
173
|
...(schedule ? [SCHEDULE_BRIEF, ''] : []),
|
|
@@ -265,6 +266,7 @@ export async function writeSelfDoc(root = rootDir(), { log, exec, tenant } = {})
|
|
|
265
266
|
// 梦(2026-10-30 m02837)与主动说话(m04610):0.9.0 起公开,两处都恒为真。
|
|
266
267
|
dream: true,
|
|
267
268
|
proactive: true,
|
|
269
|
+
sendTools: proactiveConfig(config).sendTools,
|
|
268
270
|
// 定时工具默认不放;用户在白名单里加了 schedule_create 才告诉他这条路通。
|
|
269
271
|
schedule: (config?.tools?.allow ?? []).some((n) => String(n).trim() === 'schedule_create'),
|
|
270
272
|
})
|
package/lib/selfupdate.js
CHANGED
|
@@ -137,7 +137,14 @@ export function profileDirOf(moduleUrl) {
|
|
|
137
137
|
// 两种都要认得,否则从 0.8.9 之前装上来的用户会被当成"没有 profile → 开发版"而永远升不了级。
|
|
138
138
|
for (const marker of [`/node_modules/${PKG}/`, `/node_modules/${LEGACY_PKG}/`]) {
|
|
139
139
|
const i = file.lastIndexOf(marker)
|
|
140
|
-
if (i
|
|
140
|
+
if (i < 0) continue
|
|
141
|
+
const base = file.slice(0, i)
|
|
142
|
+
// pnpm 的 isolated 布局(issue #43):真身常在
|
|
143
|
+
// `<profile>/node_modules/.pnpm/@ssamurias+dsh-fish@0.9.1/node_modules/@ssamurias/dsh-fish/lib/...`,
|
|
144
|
+
// 直接截到 marker 会得到 `.pnpm/.../node_modules`,那里没有 package.json,
|
|
145
|
+
// 于是 kind 判成 missing、managed 变 false,下载按钮永远不出现。往上退到 profile 根。
|
|
146
|
+
const pnpm = base.indexOf('/node_modules/.pnpm/')
|
|
147
|
+
return pnpm >= 0 ? base.slice(0, pnpm) : base
|
|
141
148
|
}
|
|
142
149
|
return ''
|
|
143
150
|
}
|
package/lib/settings.js
CHANGED
|
@@ -70,6 +70,11 @@ export const DEFAULTS = {
|
|
|
70
70
|
gainPerHour: 0.05, // 超过 minIdleHours 之后,每多一小时加多少
|
|
71
71
|
maxChance: 0.6, // 概率上限
|
|
72
72
|
cooldownHours: 4, // 两次之间至少隔多久
|
|
73
|
+
// 下面三项给「对话要靠工具才发得出去」的部署(比如宿主里另有插件把多个聊天接进这条会话,
|
|
74
|
+
// 再给她一个发送工具)。默认值和原来的行为一样。
|
|
75
|
+
sendTools: [], // 这些工具调成功才算她开了口;空 = 只认正文
|
|
76
|
+
foldSilent: true, // 没开口的一轮折成一行;一条会话用很久、上下文很大时可以关
|
|
77
|
+
idleCountsSeen: false, // 「他来过」(seen:)也算打断安静;她的对话不进事件流时要开
|
|
73
78
|
},
|
|
74
79
|
|
|
75
80
|
// 她"不想看"的东西(可追加,用户说以后还会加)
|