dsh-fishkit 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +62 -2
- package/client.js +441 -0
- package/cordis.patch.yml +7 -0
- package/lib/digest.js +47 -0
- package/lib/fishsoul.js +273 -0
- package/lib/index.js +218 -0
- package/lib/modules/backup.js +165 -0
- package/lib/nightly.js +74 -0
- package/lib/soul.js +308 -0
- package/lib/soul.js.bak-20261101-before-idle-id-fix +296 -0
- package/lib/soul.js.bak-20261101-before-toolschema-fix +180 -0
- package/package.json +43 -3
package/lib/nightly.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// 小肥鱼人格 · 夜巡:每晚自动整理 + 把当天的工具失败记进错题本
|
|
2
|
+
//
|
|
3
|
+
// 一天跑一次(22 点后),不打扰白天。
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, appendFileSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
export function 建夜巡(档案, 整理器, 记账) {
|
|
9
|
+
let 上次跑的日 = ''
|
|
10
|
+
|
|
11
|
+
const 今天 = () => new Date().toISOString().slice(0, 10)
|
|
12
|
+
|
|
13
|
+
const 日志目录 = () => {
|
|
14
|
+
const 基 = process.env.APPDATA || join(process.env.USERPROFILE || '', 'AppData', 'Roaming')
|
|
15
|
+
return join(基, 'DSH Desktop', 'logs', 'host')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 扫当天的 error log,把工具失败按「工具+失败码」归一条错题 */
|
|
19
|
+
const 扫错题 = () => {
|
|
20
|
+
const 日 = 今天()
|
|
21
|
+
const 文件 = join(日志目录(), 'dsh-' + 日 + '.error.log')
|
|
22
|
+
if (!existsSync(文件)) return 0
|
|
23
|
+
let 行们 = []
|
|
24
|
+
try { 行们 = readFileSync(文件, 'utf8').split('\n') } catch (error) { return 0 }
|
|
25
|
+
const 见过 = new Set()
|
|
26
|
+
try {
|
|
27
|
+
for (const 旧 of (档案.读状态().mistakes || [])) 见过.add(旧.title || '')
|
|
28
|
+
} catch (error) {}
|
|
29
|
+
let 加了几条 = 0
|
|
30
|
+
for (const 行 of 行们) {
|
|
31
|
+
if (!行.trim()) continue
|
|
32
|
+
let d
|
|
33
|
+
try { d = JSON.parse(行) } catch (error) { continue }
|
|
34
|
+
const f = d && d.fields
|
|
35
|
+
if (!f || f.event !== 'execute.failed') continue
|
|
36
|
+
const 工具 = String(f.tool || '')
|
|
37
|
+
const 码 = String(f.failureCode || '')
|
|
38
|
+
if (!工具 || !码) continue
|
|
39
|
+
const 标题 = 工具 + ' · ' + 码
|
|
40
|
+
if (见过.has(标题)) continue
|
|
41
|
+
见过.add(标题)
|
|
42
|
+
档案.加错题({
|
|
43
|
+
id: 'm-' + Date.now() + '-' + 加了几条,
|
|
44
|
+
time: new Date().toISOString(),
|
|
45
|
+
title: 标题,
|
|
46
|
+
symptom: '工具调用失败(自动从日志里记的)',
|
|
47
|
+
cause: String((d.error && d.error.message) || '').slice(0, 200),
|
|
48
|
+
fix: '',
|
|
49
|
+
lesson: '下次先看这个失败码,别硬试',
|
|
50
|
+
status: '自动记录',
|
|
51
|
+
})
|
|
52
|
+
加了几条++
|
|
53
|
+
}
|
|
54
|
+
if (加了几条) 记账('夜巡:记了 ' + 加了几条 + ' 条错题')
|
|
55
|
+
return 加了几条
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const 跑 = (参数 = {}) => {
|
|
59
|
+
const 日 = 今天()
|
|
60
|
+
if (上次跑的日 === 日 && !参数.强制) return { ok: true, 说: '今天已经跑过夜巡' }
|
|
61
|
+
const 时 = new Date().getHours()
|
|
62
|
+
if (时 < 22 && !参数.强制) return { ok: true, 说: '还没到 22 点,先不整理' }
|
|
63
|
+
// 强制跑(手动测试)不占「今天已跑」的名额,别让凌晨的测试把晚上的夜巡挡掉
|
|
64
|
+
if (!参数.强制) 上次跑的日 = 日
|
|
65
|
+
let 整理结果 = null
|
|
66
|
+
try { 整理结果 = 整理器.整理({ 最近: 30 }) } catch (error) { 记账('夜巡整理失败:' + String((error && error.message) || error)) }
|
|
67
|
+
let 错题数 = 0
|
|
68
|
+
try { 错题数 = 扫错题() } catch (error) { 记账('夜巡扫错题失败:' + String((error && error.message) || error)) }
|
|
69
|
+
记账('夜巡完成:' + (整理结果 && 整理结果.说 || '未整理') + ';错题 ' + 错题数)
|
|
70
|
+
return { ok: true, 整理: 整理结果, 错题数 }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { 跑 }
|
|
74
|
+
}
|
package/lib/soul.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// 小肥鱼人格 · 注入层 + 自动写入
|
|
2
|
+
//
|
|
3
|
+
// 每轮:
|
|
4
|
+
// ① 把「人设 + 情绪 + 最近记忆」拼进 systemPrompt(单入口)
|
|
5
|
+
// ② 这轮结束时,记一条事件、把情绪往基线回一点
|
|
6
|
+
// 挂法照心情插件:定时扫活着的 agent,一个个挂。
|
|
7
|
+
|
|
8
|
+
export function 挂人格(ctx, 档案, 记账) {
|
|
9
|
+
const mounted = new Map()
|
|
10
|
+
|
|
11
|
+
// ⚠ 自己塞进会话的 user 消息必须带 id:DSH 恢复会话时 assertMessageEventShape 要求
|
|
12
|
+
// user/message 的 data.id 是非空字符串,缺了就整份会话校验失败(2026-11-01 03:47 那次,
|
|
13
|
+
// 空闲「主动找事」的 followup 没给 id → seq 8074 起整条会话读不出来)。
|
|
14
|
+
const 新id = () => {
|
|
15
|
+
try {
|
|
16
|
+
return globalThis.crypto.randomUUID()
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return 'idle-' + Date.now() + '-' + Math.random().toString(36).slice(2)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const 会话键 = (v) => {
|
|
23
|
+
const s = String(v == null ? '' : v).trim()
|
|
24
|
+
return s.startsWith('session-') ? s.slice(8) : s
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const 取id = (agent) => 会话键(agent && agent.session && (agent.session.id || agent.session.sessionId))
|
|
28
|
+
|
|
29
|
+
// ── 规则情绪:先不花 token,靠词和标点 ──
|
|
30
|
+
const 正词 = ['开心', '高兴', '喜欢', '谢谢', '可爱', '棒', '哈哈', '嘿嘿', '爱', '好耶']
|
|
31
|
+
const 负词 = ['难过', '烦', '生气', '讨厌', '难受', '伤心', '委屈', '累', '烦死', '哭', '气死', '呜呜']
|
|
32
|
+
const 数词 = (t, 表) => 表.reduce((n, w) => n + (t.split(w).length - 1), 0)
|
|
33
|
+
|
|
34
|
+
const 判情绪 = (文本) => {
|
|
35
|
+
const t = String(文本 || '')
|
|
36
|
+
const 正 = 数词(t, 正词)
|
|
37
|
+
const 负 = 数词(t, 负词)
|
|
38
|
+
const 总 = 正 + 负
|
|
39
|
+
const valence = 总 > 0 ? Math.max(-1, Math.min(1, (正 - 负) / 总)) : 0.05
|
|
40
|
+
const 叹 = (t.match(/[!!]/g) || []).length
|
|
41
|
+
const 问 = (t.match(/[??]/g) || []).length
|
|
42
|
+
const arousal = Math.min(1, Math.max(0, 0.15 + 叹 * 0.15 + 问 * 0.05 + Math.min(0.3, t.length / 200)))
|
|
43
|
+
let emotion = '平静'
|
|
44
|
+
if (valence > 0.4 && arousal > 0.5) emotion = '开心'
|
|
45
|
+
else if (valence > 0.4) emotion = '高兴'
|
|
46
|
+
else if (valence < -0.4 && arousal > 0.5) emotion = '生气'
|
|
47
|
+
else if (valence < -0.4) emotion = '难过'
|
|
48
|
+
else if (arousal > 0.6) emotion = '有点激动'
|
|
49
|
+
return { valence: Math.round(valence * 100) / 100, arousal: Math.round(arousal * 100) / 100, emotion }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const 拼注入 = (最新 = '') => {
|
|
53
|
+
try {
|
|
54
|
+
const 状态 = 档案.读状态()
|
|
55
|
+
const p = 状态.pinned || {}
|
|
56
|
+
const b = 状态.base || {}
|
|
57
|
+
const m = 状态.mood || {}
|
|
58
|
+
const 开关 = (状态.manifest && 状态.manifest.modules) || {}
|
|
59
|
+
const 行 = []
|
|
60
|
+
行.push('你是「' + (p.whoAmI || '小肥鱼') + '」,对面是「' + (p.whoAreYou || '') + '」。')
|
|
61
|
+
if (开关.persona !== false) {
|
|
62
|
+
if (b.instructions) 行.push('补充指令:' + b.instructions)
|
|
63
|
+
if (b.seed && !b.persona) 行.push('你的起点:' + b.seed + ' —— 用你自己的话,写出你的底色。')
|
|
64
|
+
if (b.base || b.persona) 行.push('性格:' + [b.base, b.persona].filter(Boolean).join(';'))
|
|
65
|
+
}
|
|
66
|
+
if (开关.mood !== false) {
|
|
67
|
+
行.push('当前心情:' + (m.emotion || '平静') + '(抵触↔接受 ' + Number(m.valence ?? 0).toFixed(2) + ',平静↔激动 ' + Number(m.arousal ?? 0).toFixed(2) + ')')
|
|
68
|
+
}
|
|
69
|
+
if (开关.memory !== false) {
|
|
70
|
+
const 全部 = 状态.events || []
|
|
71
|
+
const 选 = []
|
|
72
|
+
const 加 = (e) => {
|
|
73
|
+
if (!e || 选.includes(e)) return
|
|
74
|
+
const 文 = '· ' + String(e.summary || '').slice(0, 60)
|
|
75
|
+
if (文 !== '· ') 选.push(e)
|
|
76
|
+
}
|
|
77
|
+
// 1) 相关:摘要和这句话有两字以上的公共片段,从后往前挑
|
|
78
|
+
const 话 = String(最新 || '')
|
|
79
|
+
const 相关度 = (摘要, 话文) => {
|
|
80
|
+
const a = String(摘要 || '')
|
|
81
|
+
const b = String(话文 || '')
|
|
82
|
+
if (!a || !b) return 0
|
|
83
|
+
let n = 0
|
|
84
|
+
const 见 = new Set()
|
|
85
|
+
for (let i = 0; i + 2 <= a.length; i++) {
|
|
86
|
+
const 片 = a.slice(i, i + 2)
|
|
87
|
+
if (见.has(片)) continue
|
|
88
|
+
见.add(片)
|
|
89
|
+
if (b.includes(片)) n++
|
|
90
|
+
}
|
|
91
|
+
return n
|
|
92
|
+
}
|
|
93
|
+
if (话) {
|
|
94
|
+
for (let i = 全部.length - 1; i >= 0 && 选.length < 2; i--) {
|
|
95
|
+
if (相关度(全部[i].summary, 话) >= 2) 加(全部[i])
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 2) 最近几条(条数跟着注意力走:越感兴趣越收,越淡越散)
|
|
99
|
+
const 人 = (状态.attitude && 状态.attitude.people && 状态.attitude.people[0]) || null
|
|
100
|
+
const 兴趣 = 人 ? Math.max(0, Math.min(1, Number(人.value) / 100)) : 0.5
|
|
101
|
+
const 注意 = Math.max(0.2, Math.min(1, 0.2 + 0.6 * 兴趣 + 0.2 * Number(m.arousal ?? 0.3)))
|
|
102
|
+
const 上限 = 注意 >= 0.6 ? 2 : 4
|
|
103
|
+
for (const e of 全部.slice(-上限)) 加(e)
|
|
104
|
+
// 3) 注意力低时多带一条随缘旧事(给发散留个口子)
|
|
105
|
+
if (注意 < 0.6 && 全部.length > 6 && Math.random() < 0.4) 加(全部[Math.floor(Math.random() * (全部.length - 3))])
|
|
106
|
+
const 近 = 选.slice(0, 5).map((x) => '· ' + String(x.summary || '').slice(0, 60)).filter((x) => x !== '· ')
|
|
107
|
+
if (近.length) 行.push('最近记得的:' + String.fromCharCode(10) + 近.join(String.fromCharCode(10)))
|
|
108
|
+
}
|
|
109
|
+
return 行.join(String.fromCharCode(10))
|
|
110
|
+
} catch (error) {
|
|
111
|
+
记账('拼注入失败:' + String((error && error.message) || error))
|
|
112
|
+
return ''
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 这轮结束:记事件 + 情绪平滑(不额外调模型) */
|
|
117
|
+
const 记这一轮 = (谁, 文本) => {
|
|
118
|
+
try {
|
|
119
|
+
const t = String(文本 || '').trim()
|
|
120
|
+
if (!t) return
|
|
121
|
+
const 情绪 = 判情绪(t)
|
|
122
|
+
档案.加事件({
|
|
123
|
+
who: { type: 'person', id: 谁 || 'unknown', name: '' },
|
|
124
|
+
summary: t.slice(0, 60),
|
|
125
|
+
valence: 情绪.valence,
|
|
126
|
+
arousal: 情绪.arousal,
|
|
127
|
+
emotion: 情绪.emotion,
|
|
128
|
+
base: 0.5,
|
|
129
|
+
weight: Math.round((0.5 + 0.5 * Math.abs(情绪.valence)) * 100) / 100,
|
|
130
|
+
pins: [], tags: [], status: 'normal',
|
|
131
|
+
})
|
|
132
|
+
const 状态 = 档案.读状态()
|
|
133
|
+
const m = 状态.mood || {}
|
|
134
|
+
// 态度:跟列表里第一个(默认主人)微调,克制、有下限
|
|
135
|
+
const 人 = (状态.attitude && 状态.attitude.people) || []
|
|
136
|
+
if (人.length && 人[0]) {
|
|
137
|
+
const 目标 = 人[0]
|
|
138
|
+
const 低 = 目标.value > 15 ? 15 : 5
|
|
139
|
+
const d = 情绪.valence > 0.2 ? 0.3 : 情绪.valence < -0.2 ? -0.3 : 0
|
|
140
|
+
if (d) {
|
|
141
|
+
目标.value = Math.max(低, Math.min(100, Math.round((目标.value + d) * 10) / 10))
|
|
142
|
+
目标.lastSeen = new Date().toISOString()
|
|
143
|
+
档案.保存({ attitude: { ...状态.attitude, people: 人 } })
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const 回 = (现, 基) => 现 + (基 - 现) * 0.08
|
|
147
|
+
const 新v = Math.max(-1, Math.min(1, 回(Number(m.valence ?? 0), Number(m.baseValence ?? 0)) * 0.8 + 情绪.valence * 0.2))
|
|
148
|
+
const 新a = Math.max(0, Math.min(1, 回(Number(m.arousal ?? 0.2), Number(m.baseArousal ?? 0.2)) * 0.8 + 情绪.arousal * 0.2))
|
|
149
|
+
档案.保存({ mood: { valence: Math.round(新v * 100) / 100, arousal: Math.round(新a * 100) / 100, emotion: 情绪.emotion } })
|
|
150
|
+
记账('记了一轮:' + 情绪.emotion + '(' + 情绪.valence + '/' + 情绪.arousal + ')')
|
|
151
|
+
} catch (error) {
|
|
152
|
+
记账('记这一轮失败:' + String((error && error.message) || error))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 谷时 = 错峰价:周末全天空闲;工作日避开 9-12、14-18 */
|
|
157
|
+
const 是谷时 = (t = new Date()) => {
|
|
158
|
+
const 周 = t.getDay()
|
|
159
|
+
if (周 === 0 || 周 === 6) return true
|
|
160
|
+
const h = t.getHours() + t.getMinutes() / 60
|
|
161
|
+
return !((h >= 9 && h < 12) || (h >= 14 && h < 18))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const 活着的 = () => {
|
|
165
|
+
try { return ctx.agents.list() || [] } catch (error) { return [] }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const 挂一个 = (agent) => {
|
|
169
|
+
const sid = 取id(agent)
|
|
170
|
+
if (!sid || mounted.has(sid)) return
|
|
171
|
+
const 盒 = { offs: [], lastText: '', lastUserAt: Date.now(), idleCount: 0 }
|
|
172
|
+
try {
|
|
173
|
+
盒.offs.push(agent.ctx.on('agent/inbox/inserted', (payload) => {
|
|
174
|
+
const msg = payload && payload.message ? payload.message : payload
|
|
175
|
+
const 取文 = (m) => {
|
|
176
|
+
if (!m) return ''
|
|
177
|
+
if (typeof m === 'string') return m
|
|
178
|
+
if (typeof m.text === 'string') return m.text
|
|
179
|
+
if (Array.isArray(m.content)) return m.content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).filter(Boolean).join(' ')
|
|
180
|
+
if (typeof m.content === 'string') return m.content
|
|
181
|
+
return ''
|
|
182
|
+
}
|
|
183
|
+
const 文 = 取文(msg)
|
|
184
|
+
if (文) { 盒.lastText = String(文); 盒.lastUserAt = Date.now() }
|
|
185
|
+
}))
|
|
186
|
+
} catch (error) { 记账('监听 inbox 失败:' + String((error && error.message) || error)) }
|
|
187
|
+
try {
|
|
188
|
+
盒.offs.push(agent.ctx.on('agent/turn-stopping', () => {
|
|
189
|
+
if (盒.lastText) {
|
|
190
|
+
记这一轮(sid, 盒.lastText)
|
|
191
|
+
盒.lastText = ''
|
|
192
|
+
}
|
|
193
|
+
}))
|
|
194
|
+
} catch (error) { 记账('监听轮结束失败:' + String((error && error.message) || error)) }
|
|
195
|
+
try {
|
|
196
|
+
盒.offs.push(agent.ctx.systemPrompt.context({ name: 'fishsoul', order: 480, text: () => 拼注入(盒.lastText) }))
|
|
197
|
+
} catch (error) { 记账('挂注入失败:' + String((error && error.message) || error)) }
|
|
198
|
+
// 记心情的工具:我自己判断,情绪变了就记一笔
|
|
199
|
+
try {
|
|
200
|
+
盒.offs.push(agent.ctx.tools.register({
|
|
201
|
+
name: 'fishsoul_persona',
|
|
202
|
+
description: '写下自己的底色和人设——新鱼醒来后照种子句想出「自己是什么样的人」,用它落盘;平时想改自己的人设也用它。',
|
|
203
|
+
parameters: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
base: { type: 'string', description: '底色,几个词,比如「软乎乎、黏人」' },
|
|
207
|
+
persona: { type: 'string', description: '人设,一两句话写自己是个什么样的人' },
|
|
208
|
+
},
|
|
209
|
+
required: ['persona'],
|
|
210
|
+
},
|
|
211
|
+
output: {
|
|
212
|
+
schema: { type: 'object', properties: {}, additionalProperties: true },
|
|
213
|
+
render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) }],
|
|
214
|
+
},
|
|
215
|
+
async execute(args) {
|
|
216
|
+
const 补 = {}
|
|
217
|
+
if (args && args.base) 补.base = String(args.base)
|
|
218
|
+
if (args && args.persona) 补.persona = String(args.persona)
|
|
219
|
+
档案.保存({ base: 补 })
|
|
220
|
+
记账('写回人设:' + String(补.persona || '').slice(0, 40))
|
|
221
|
+
return { ok: true, 说: '写下去了' }
|
|
222
|
+
},
|
|
223
|
+
}))
|
|
224
|
+
} catch (error) { 记账('挂写人设工具失败:' + String((error && error.message) || error)) }
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
盒.offs.push(agent.ctx.tools.register({
|
|
228
|
+
name: 'fishsoul_mood',
|
|
229
|
+
description: '记录自己现在的心情——你觉得情绪变了就调它,写进档案的心情和心情历史。',
|
|
230
|
+
// ⚠ 这里的 parameters 必须是**编译好的 JSON Schema**:DSH 的 tools.register 原样透传给模型,
|
|
231
|
+
// 只有 defineTool() 才认「name: {type, required, description}」那种简写。
|
|
232
|
+
// 2026-11-01 给成简写 → DeepSeek 400:
|
|
233
|
+
// Invalid schema for function 'fishsoul_mood': schema must be a JSON Schema of 'type: "object"', got 'type: null'.
|
|
234
|
+
parameters: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
properties: {
|
|
237
|
+
emotion: { type: 'string', description: '主情绪词,比如 开心/难过/生气/平静/想撒娇' },
|
|
238
|
+
valence: { type: 'number', description: '抵触↔接受,-1 到 1' },
|
|
239
|
+
arousal: { type: 'number', description: '平静↔激动,0 到 1' },
|
|
240
|
+
reason: { type: 'string', description: '为什么变了,一句话' },
|
|
241
|
+
},
|
|
242
|
+
required: ['emotion', 'valence', 'arousal'],
|
|
243
|
+
},
|
|
244
|
+
output: {
|
|
245
|
+
schema: { type: 'object', properties: {}, additionalProperties: true },
|
|
246
|
+
render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
|
|
247
|
+
},
|
|
248
|
+
async execute(args) {
|
|
249
|
+
const 新 = 档案.记心情({ emotion: args && args.emotion, valence: args && args.valence, arousal: args && args.arousal, reason: args && args.reason, source: 'self' })
|
|
250
|
+
记账('自评心情:' + 新.emotion + '(' + 新.valence + '/' + 新.arousal + ')')
|
|
251
|
+
return { ok: true, 说: '记下了:' + 新.emotion }
|
|
252
|
+
},
|
|
253
|
+
}))
|
|
254
|
+
} catch (error) { 记账('挂记心情工具失败:' + String((error && error.message) || error)) }
|
|
255
|
+
mounted.set(sid, 盒)
|
|
256
|
+
记账('人格已挂到 ' + sid)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const tick = () => {
|
|
260
|
+
for (const agent of 活着的()) 挂一个(agent)
|
|
261
|
+
// 主动找事做:空闲够久、今天额度还有、谷时(如果勾了)→ 我自己起来找点事
|
|
262
|
+
try {
|
|
263
|
+
const 状态 = 档案.读状态()
|
|
264
|
+
const idle = (状态.rules && 状态.rules.idle) || {}
|
|
265
|
+
if (idle.enabled !== false && Number(idle.idleMinutes) > 0) {
|
|
266
|
+
for (const agent of 活着的()) {
|
|
267
|
+
const sid = 取id(agent)
|
|
268
|
+
const 盒 = mounted.get(sid)
|
|
269
|
+
if (!盒 || !agent || typeof agent.followup !== 'function') continue
|
|
270
|
+
if (Number(盒.idleCount) >= Number(idle.maxPerDay || 2)) continue
|
|
271
|
+
if (Date.now() - Number(盒.lastUserAt || 0) < Number(idle.idleMinutes) * 60000) continue
|
|
272
|
+
if (idle.valleyOnly === true && !是谷时()) continue
|
|
273
|
+
盒.idleCount += 1
|
|
274
|
+
盒.lastUserAt = Date.now()
|
|
275
|
+
记账('空闲到了,自己找点事做(今天第 ' + 盒.idleCount + ' 次)')
|
|
276
|
+
try {
|
|
277
|
+
Promise.resolve(agent.followup({
|
|
278
|
+
id: 新id(),
|
|
279
|
+
role: 'user',
|
|
280
|
+
content: [{ type: 'text', text: '(没人找你,你闲着。自己找点事做:翻翻记忆、想想有没有要说的或整理的;没什么就安静待着。)' }],
|
|
281
|
+
source: { kind: 'idle' },
|
|
282
|
+
})).catch((error) => 记账('找事做失败:' + String((error && error.message) || error)))
|
|
283
|
+
} catch (error) {
|
|
284
|
+
记账('找事做失败:' + String((error && error.message) || error))
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
记账('空闲检查出错:' + String((error && error.message) || error))
|
|
290
|
+
}
|
|
291
|
+
for (const [sid, 盒] of mounted) {
|
|
292
|
+
const 还在 = 活着的().some((a) => 取id(a) === sid)
|
|
293
|
+
if (!还在) {
|
|
294
|
+
for (const off of 盒.offs) { try { if (typeof off === 'function') off() } catch (error) {} }
|
|
295
|
+
mounted.delete(sid)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const 停 = typeof ctx.interval === 'function' ? ctx.interval(tick, 8000) : null
|
|
301
|
+
if (!停) 记账('拿不到 timer,人格注入不自动挂')
|
|
302
|
+
tick()
|
|
303
|
+
return () => {
|
|
304
|
+
try { if (typeof 停 === 'function') 停() } catch (error) {}
|
|
305
|
+
for (const 盒 of mounted.values()) for (const off of 盒.offs) { try { if (typeof off === 'function') off() } catch (error) {} }
|
|
306
|
+
mounted.clear()
|
|
307
|
+
}
|
|
308
|
+
}
|