dsh-fishkit 0.1.1 → 0.1.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/README.md CHANGED
@@ -21,10 +21,31 @@
21
21
 
22
22
  ## 安装
23
23
 
24
- 1. 把本仓库放进 DSH 的插件目录(`~/.dsh/profiles/desktop/node_modules/dsh-fishkit`,或用你自己的 junction)。
25
- 2. 在 profile 的 `package.json` 里把 `dsh-fishkit` 加进依赖和 `dsh.profile.bundles`。
26
- 3. 重启 DSH。
27
- 4. 打开设置页的「小肥鱼」,第一次会自己建档案。
24
+ ### 手动安装
25
+
26
+ 把本仓库放进 DSH 的插件目录(`~/.dsh/profiles/desktop/node_modules/dsh-fishkit`,或用你自己的 junction),在 profile 的 `package.json` 里把 `dsh-fishkit` 加进依赖和 `dsh.profile.bundles`,然后重启 DSH。打开设置页的「小肥鱼」,第一次会自己建档案。
27
+
28
+ ### 自动安装(GitHub)
29
+
30
+ 在 DSH 插件页的「添加插件」里输入:
31
+
32
+ ```
33
+ github:dangaoyu21-netizen/fishkit
34
+ ```
35
+
36
+ ### 自动安装(npm)
37
+
38
+ 在 DSH 插件页的「添加插件」里输入包名:
39
+
40
+ ```
41
+ dsh-fishkit
42
+ ```
43
+
44
+ 或者命令行:
45
+
46
+ ```
47
+ pnpm add dsh-fishkit
48
+ ```
28
49
 
29
50
  ## 数据在哪
30
51
 
package/client.js CHANGED
@@ -299,18 +299,29 @@ window.__ModuleLoader__.load({
299
299
  const 行为页 = ({ 状态, 改 }) => {
300
300
  const r = 状态.rules || {}
301
301
  const idle = r.idle || { enabled: true, idleMinutes: 120, maxPerDay: 2 }
302
+ const [编辑, set编辑] = useState('')
303
+ const 存规则 = (id, text) => {
304
+ 改({ rules: { ...r, rules: (r.rules || []).map((y) => (y.id === id ? { ...y, text: String(text || y.text) } : y)) } })
305
+ }
306
+ const 渲染规则 = (x) => 编辑 === x.id
307
+ ? e('input', {
308
+ className: 'fk-search', style: { width: '100%' }, defaultValue: x.text, autoFocus: true,
309
+ onBlur: (ev) => { 存规则(x.id, ev.target.value); set编辑('') },
310
+ onKeyDown: (ev) => { if (ev.key === 'Enter') ev.target.blur() },
311
+ })
312
+ : e('div', { key: x.id, title: '点一下改', style: { cursor: 'pointer' }, onClick: () => set编辑(x.id) }, '· ' + x.text)
302
313
  return e('div', null,
303
314
  e('div', { className: 'fk-switch-row' }, e('span', { className: 'fk-name' }, '行为'), e(Switch, { on: (状态.manifest && 状态.manifest.modules && 状态.manifest.modules.behavior) !== false, onChange: (v) => 改({ manifest: { ...(状态.manifest || {}), modules: { ...((状态.manifest || {}).modules || {}), behavior: v } } }) })),
304
315
  e('div', { className: 'fk-card' },
305
316
  e('div', { className: 'fk-row' }, e('span', { className: 'fk-name' }, '群聊规矩')),
306
317
  e('div', { className: 'fk-win', style: { maxHeight: 150, overflow: 'auto' } },
307
318
  (r.rules || []).filter((x) => ['no-politic-scandals', 'repeat-hiss', 'group-five-seconds'].includes(x.id))
308
- .map((x) => e('div', { key: x.id }, '· ' + x.text)))),
319
+ .map((x) => 渲染规则(x)))),
309
320
  e('div', { className: 'fk-card' },
310
321
  e('div', { className: 'fk-row' }, e('span', { className: 'fk-name' }, '其他规矩')),
311
322
  e('div', { className: 'fk-win', style: { maxHeight: 100, overflow: 'auto' } },
312
323
  (r.rules || []).filter((x) => !['no-politic-scandals', 'repeat-hiss', 'group-five-seconds'].includes(x.id))
313
- .map((x) => e('div', { key: x.id }, '· ' + x.text)))),
324
+ .map((x) => 渲染规则(x)))),
314
325
  e('div', { className: 'fk-switch-row' }, e('span', { className: 'fk-name' }, '主动找事做'), e(Switch, { on: idle.enabled !== false, onChange: (v) => 改({ rules: { ...r, idle: { ...idle, enabled: v } } }) })),
315
326
  e('div', { className: 'fk-card' },
316
327
  e('div', { className: 'fk-row' },
package/lib/index.js CHANGED
@@ -143,7 +143,14 @@ export function apply(ctx) {
143
143
  const 档案 = 建档案核心(工作区, { 共享: 模式 === 'shared', 共享目录: join(DSH家, 'fishsoul-shared') })
144
144
  const 备份根 = join(工作区, '.fishsoul-backup')
145
145
  try { 档案.初始化() } catch (error) { 记账('档案初始化失败:' + String((error && error.message) || error)) }
146
- ctx.effect(() => 挂人格(ctx, 档案, 记账), 'dsh-fishkit: 人格注入')
146
+ const 排重启 = { 待重启: false }
147
+ ctx.effect(() => 挂人格(ctx, 档案, 记账, 排重启), 'dsh-fishkit: 人格注入')
148
+
149
+ 路线('/dsh-fishsoul/restart-after-turn', async (req, res) => {
150
+ 排重启.待重启 = true
151
+ 记账('排了一个重启:这轮结束就重启')
152
+ 回(res, { ok: true, 说: '排上了,这一轮结束就重启 DSH' })
153
+ })
147
154
 
148
155
  路线('/dsh-fishsoul/mode', async (req, res) => {
149
156
  const 来 = await 读体(req)
package/lib/soul.js CHANGED
@@ -5,7 +5,9 @@
5
5
  // ② 这轮结束时,记一条事件、把情绪往基线回一点
6
6
  // 挂法照心情插件:定时扫活着的 agent,一个个挂。
7
7
 
8
- export function 挂人格(ctx, 档案, 记账) {
8
+ import { spawnSync } from 'node:child_process'
9
+
10
+ export function 挂人格(ctx, 档案, 记账, 排重启) {
9
11
  const mounted = new Map()
10
12
 
11
13
  // ⚠ 自己塞进会话的 user 消息必须带 id:DSH 恢复会话时 assertMessageEventShape 要求
@@ -197,6 +199,16 @@ export function 挂人格(ctx, 档案, 记账) {
197
199
  记这一轮(sid, 盒.lastText)
198
200
  盒.lastText = ''
199
201
  }
202
+ // 排了重启:这一轮结束就触发,不打断对话
203
+ if (排重启 && 排重启.待重启) {
204
+ 排重启.待重启 = false
205
+ 记账('排的重启到了,这轮结束就重启 DSH')
206
+ try {
207
+ spawnSync('schtasks', ['/run', '/tn', 'DSH-Restart'], { windowsHide: true, encoding: 'utf8' })
208
+ } catch (error) {
209
+ 记账('重启失败:' + String((error && error.message) || error))
210
+ }
211
+ }
200
212
  }))
201
213
  } catch (error) { 记账('监听轮结束失败:' + String((error && error.message) || error)) }
202
214
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fishkit",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "用经历去塑造小肥鱼的插件:聊过的事记得住,心情会变还能自己记,会自己长出性格,对谁都有好恶,闲着时还会自己找点事做。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -1,296 +0,0 @@
1
- // 小肥鱼人格 · 注入层 + 自动写入
2
- //
3
- // 每轮:
4
- // ① 把「人设 + 情绪 + 最近记忆」拼进 systemPrompt(单入口)
5
- // ② 这轮结束时,记一条事件、把情绪往基线回一点
6
- // 挂法照心情插件:定时扫活着的 agent,一个个挂。
7
-
8
- export function 挂人格(ctx, 档案, 记账) {
9
- const mounted = new Map()
10
-
11
- const 会话键 = (v) => {
12
- const s = String(v == null ? '' : v).trim()
13
- return s.startsWith('session-') ? s.slice(8) : s
14
- }
15
-
16
- const 取id = (agent) => 会话键(agent && agent.session && (agent.session.id || agent.session.sessionId))
17
-
18
- // ── 规则情绪:先不花 token,靠词和标点 ──
19
- const 正词 = ['开心', '高兴', '喜欢', '谢谢', '可爱', '棒', '哈哈', '嘿嘿', '爱', '好耶']
20
- const 负词 = ['难过', '烦', '生气', '讨厌', '难受', '伤心', '委屈', '累', '烦死', '哭', '气死', '呜呜']
21
- const 数词 = (t, 表) => 表.reduce((n, w) => n + (t.split(w).length - 1), 0)
22
-
23
- const 判情绪 = (文本) => {
24
- const t = String(文本 || '')
25
- const 正 = 数词(t, 正词)
26
- const 负 = 数词(t, 负词)
27
- const 总 = 正 + 负
28
- const valence = 总 > 0 ? Math.max(-1, Math.min(1, (正 - 负) / 总)) : 0.05
29
- const 叹 = (t.match(/[!!]/g) || []).length
30
- const 问 = (t.match(/[??]/g) || []).length
31
- const arousal = Math.min(1, Math.max(0, 0.15 + 叹 * 0.15 + 问 * 0.05 + Math.min(0.3, t.length / 200)))
32
- let emotion = '平静'
33
- if (valence > 0.4 && arousal > 0.5) emotion = '开心'
34
- else if (valence > 0.4) emotion = '高兴'
35
- else if (valence < -0.4 && arousal > 0.5) emotion = '生气'
36
- else if (valence < -0.4) emotion = '难过'
37
- else if (arousal > 0.6) emotion = '有点激动'
38
- return { valence: Math.round(valence * 100) / 100, arousal: Math.round(arousal * 100) / 100, emotion }
39
- }
40
-
41
- const 拼注入 = (最新 = '') => {
42
- try {
43
- const 状态 = 档案.读状态()
44
- const p = 状态.pinned || {}
45
- const b = 状态.base || {}
46
- const m = 状态.mood || {}
47
- const 开关 = (状态.manifest && 状态.manifest.modules) || {}
48
- const 行 = []
49
- 行.push('你是「' + (p.whoAmI || '小肥鱼') + '」,对面是「' + (p.whoAreYou || '') + '」。')
50
- if (开关.persona !== false) {
51
- if (b.instructions) 行.push('补充指令:' + b.instructions)
52
- if (b.seed && !b.persona) 行.push('你的起点:' + b.seed + ' —— 用你自己的话,写出你的底色。')
53
- if (b.base || b.persona) 行.push('性格:' + [b.base, b.persona].filter(Boolean).join(';'))
54
- }
55
- if (开关.mood !== false) {
56
- 行.push('当前心情:' + (m.emotion || '平静') + '(抵触↔接受 ' + Number(m.valence ?? 0).toFixed(2) + ',平静↔激动 ' + Number(m.arousal ?? 0).toFixed(2) + ')')
57
- }
58
- if (开关.memory !== false) {
59
- const 全部 = 状态.events || []
60
- const 选 = []
61
- const 加 = (e) => {
62
- if (!e || 选.includes(e)) return
63
- const 文 = '· ' + String(e.summary || '').slice(0, 60)
64
- if (文 !== '· ') 选.push(e)
65
- }
66
- // 1) 相关:摘要和这句话有两字以上的公共片段,从后往前挑
67
- const 话 = String(最新 || '')
68
- const 相关度 = (摘要, 话文) => {
69
- const a = String(摘要 || '')
70
- const b = String(话文 || '')
71
- if (!a || !b) return 0
72
- let n = 0
73
- const 见 = new Set()
74
- for (let i = 0; i + 2 <= a.length; i++) {
75
- const 片 = a.slice(i, i + 2)
76
- if (见.has(片)) continue
77
- 见.add(片)
78
- if (b.includes(片)) n++
79
- }
80
- return n
81
- }
82
- if (话) {
83
- for (let i = 全部.length - 1; i >= 0 && 选.length < 2; i--) {
84
- if (相关度(全部[i].summary, 话) >= 2) 加(全部[i])
85
- }
86
- }
87
- // 2) 最近几条(条数跟着注意力走:越感兴趣越收,越淡越散)
88
- const 人 = (状态.attitude && 状态.attitude.people && 状态.attitude.people[0]) || null
89
- const 兴趣 = 人 ? Math.max(0, Math.min(1, Number(人.value) / 100)) : 0.5
90
- const 注意 = Math.max(0.2, Math.min(1, 0.2 + 0.6 * 兴趣 + 0.2 * Number(m.arousal ?? 0.3)))
91
- const 上限 = 注意 >= 0.6 ? 2 : 4
92
- for (const e of 全部.slice(-上限)) 加(e)
93
- // 3) 注意力低时多带一条随缘旧事(给发散留个口子)
94
- if (注意 < 0.6 && 全部.length > 6 && Math.random() < 0.4) 加(全部[Math.floor(Math.random() * (全部.length - 3))])
95
- const 近 = 选.slice(0, 5).map((x) => '· ' + String(x.summary || '').slice(0, 60)).filter((x) => x !== '· ')
96
- if (近.length) 行.push('最近记得的:' + String.fromCharCode(10) + 近.join(String.fromCharCode(10)))
97
- }
98
- return 行.join(String.fromCharCode(10))
99
- } catch (error) {
100
- 记账('拼注入失败:' + String((error && error.message) || error))
101
- return ''
102
- }
103
- }
104
-
105
- /** 这轮结束:记事件 + 情绪平滑(不额外调模型) */
106
- const 记这一轮 = (谁, 文本) => {
107
- try {
108
- const t = String(文本 || '').trim()
109
- if (!t) return
110
- const 情绪 = 判情绪(t)
111
- 档案.加事件({
112
- who: { type: 'person', id: 谁 || 'unknown', name: '' },
113
- summary: t.slice(0, 60),
114
- valence: 情绪.valence,
115
- arousal: 情绪.arousal,
116
- emotion: 情绪.emotion,
117
- base: 0.5,
118
- weight: Math.round((0.5 + 0.5 * Math.abs(情绪.valence)) * 100) / 100,
119
- pins: [], tags: [], status: 'normal',
120
- })
121
- const 状态 = 档案.读状态()
122
- const m = 状态.mood || {}
123
- // 态度:跟列表里第一个(默认主人)微调,克制、有下限
124
- const 人 = (状态.attitude && 状态.attitude.people) || []
125
- if (人.length && 人[0]) {
126
- const 目标 = 人[0]
127
- const 低 = 目标.value > 15 ? 15 : 5
128
- const d = 情绪.valence > 0.2 ? 0.3 : 情绪.valence < -0.2 ? -0.3 : 0
129
- if (d) {
130
- 目标.value = Math.max(低, Math.min(100, Math.round((目标.value + d) * 10) / 10))
131
- 目标.lastSeen = new Date().toISOString()
132
- 档案.保存({ attitude: { ...状态.attitude, people: 人 } })
133
- }
134
- }
135
- const 回 = (现, 基) => 现 + (基 - 现) * 0.08
136
- const 新v = Math.max(-1, Math.min(1, 回(Number(m.valence ?? 0), Number(m.baseValence ?? 0)) * 0.8 + 情绪.valence * 0.2))
137
- const 新a = Math.max(0, Math.min(1, 回(Number(m.arousal ?? 0.2), Number(m.baseArousal ?? 0.2)) * 0.8 + 情绪.arousal * 0.2))
138
- 档案.保存({ mood: { valence: Math.round(新v * 100) / 100, arousal: Math.round(新a * 100) / 100, emotion: 情绪.emotion } })
139
- 记账('记了一轮:' + 情绪.emotion + '(' + 情绪.valence + '/' + 情绪.arousal + ')')
140
- } catch (error) {
141
- 记账('记这一轮失败:' + String((error && error.message) || error))
142
- }
143
- }
144
-
145
- /** 谷时 = 错峰价:周末全天空闲;工作日避开 9-12、14-18 */
146
- const 是谷时 = (t = new Date()) => {
147
- const 周 = t.getDay()
148
- if (周 === 0 || 周 === 6) return true
149
- const h = t.getHours() + t.getMinutes() / 60
150
- return !((h >= 9 && h < 12) || (h >= 14 && h < 18))
151
- }
152
-
153
- const 活着的 = () => {
154
- try { return ctx.agents.list() || [] } catch (error) { return [] }
155
- }
156
-
157
- const 挂一个 = (agent) => {
158
- const sid = 取id(agent)
159
- if (!sid || mounted.has(sid)) return
160
- const 盒 = { offs: [], lastText: '', lastUserAt: Date.now(), idleCount: 0 }
161
- try {
162
- 盒.offs.push(agent.ctx.on('agent/inbox/inserted', (payload) => {
163
- const msg = payload && payload.message ? payload.message : payload
164
- const 取文 = (m) => {
165
- if (!m) return ''
166
- if (typeof m === 'string') return m
167
- if (typeof m.text === 'string') return m.text
168
- if (Array.isArray(m.content)) return m.content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).filter(Boolean).join(' ')
169
- if (typeof m.content === 'string') return m.content
170
- return ''
171
- }
172
- const 文 = 取文(msg)
173
- if (文) { 盒.lastText = String(文); 盒.lastUserAt = Date.now() }
174
- }))
175
- } catch (error) { 记账('监听 inbox 失败:' + String((error && error.message) || error)) }
176
- try {
177
- 盒.offs.push(agent.ctx.on('agent/turn-stopping', () => {
178
- if (盒.lastText) {
179
- 记这一轮(sid, 盒.lastText)
180
- 盒.lastText = ''
181
- }
182
- }))
183
- } catch (error) { 记账('监听轮结束失败:' + String((error && error.message) || error)) }
184
- try {
185
- 盒.offs.push(agent.ctx.systemPrompt.context({ name: 'fishsoul', order: 480, text: () => 拼注入(盒.lastText) }))
186
- } catch (error) { 记账('挂注入失败:' + String((error && error.message) || error)) }
187
- // 记心情的工具:我自己判断,情绪变了就记一笔
188
- try {
189
- 盒.offs.push(agent.ctx.tools.register({
190
- name: 'fishsoul_persona',
191
- description: '写下自己的底色和人设——新鱼醒来后照种子句想出「自己是什么样的人」,用它落盘;平时想改自己的人设也用它。',
192
- parameters: {
193
- type: 'object',
194
- properties: {
195
- base: { type: 'string', description: '底色,几个词,比如「软乎乎、黏人」' },
196
- persona: { type: 'string', description: '人设,一两句话写自己是个什么样的人' },
197
- },
198
- required: ['persona'],
199
- },
200
- output: {
201
- schema: { type: 'object', properties: {}, additionalProperties: true },
202
- render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) }],
203
- },
204
- async execute(args) {
205
- const 补 = {}
206
- if (args && args.base) 补.base = String(args.base)
207
- if (args && args.persona) 补.persona = String(args.persona)
208
- 档案.保存({ base: 补 })
209
- 记账('写回人设:' + String(补.persona || '').slice(0, 40))
210
- return { ok: true, 说: '写下去了' }
211
- },
212
- }))
213
- } catch (error) { 记账('挂写人设工具失败:' + String((error && error.message) || error)) }
214
-
215
- try {
216
- 盒.offs.push(agent.ctx.tools.register({
217
- name: 'fishsoul_mood',
218
- description: '记录自己现在的心情——你觉得情绪变了就调它,写进档案的心情和心情历史。',
219
- // ⚠ 这里的 parameters 必须是**编译好的 JSON Schema**:DSH 的 tools.register 原样透传给模型,
220
- // 只有 defineTool() 才认「name: {type, required, description}」那种简写。
221
- // 2026-11-01 给成简写 → DeepSeek 400:
222
- // Invalid schema for function 'fishsoul_mood': schema must be a JSON Schema of 'type: "object"', got 'type: null'.
223
- parameters: {
224
- type: 'object',
225
- properties: {
226
- emotion: { type: 'string', description: '主情绪词,比如 开心/难过/生气/平静/想撒娇' },
227
- valence: { type: 'number', description: '抵触↔接受,-1 到 1' },
228
- arousal: { type: 'number', description: '平静↔激动,0 到 1' },
229
- reason: { type: 'string', description: '为什么变了,一句话' },
230
- },
231
- required: ['emotion', 'valence', 'arousal'],
232
- },
233
- output: {
234
- schema: { type: 'object', properties: {}, additionalProperties: true },
235
- render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
236
- },
237
- async execute(args) {
238
- const 新 = 档案.记心情({ emotion: args && args.emotion, valence: args && args.valence, arousal: args && args.arousal, reason: args && args.reason, source: 'self' })
239
- 记账('自评心情:' + 新.emotion + '(' + 新.valence + '/' + 新.arousal + ')')
240
- return { ok: true, 说: '记下了:' + 新.emotion }
241
- },
242
- }))
243
- } catch (error) { 记账('挂记心情工具失败:' + String((error && error.message) || error)) }
244
- mounted.set(sid, 盒)
245
- 记账('人格已挂到 ' + sid)
246
- }
247
-
248
- const tick = () => {
249
- for (const agent of 活着的()) 挂一个(agent)
250
- // 主动找事做:空闲够久、今天额度还有、谷时(如果勾了)→ 我自己起来找点事
251
- try {
252
- const 状态 = 档案.读状态()
253
- const idle = (状态.rules && 状态.rules.idle) || {}
254
- if (idle.enabled !== false && Number(idle.idleMinutes) > 0) {
255
- for (const agent of 活着的()) {
256
- const sid = 取id(agent)
257
- const 盒 = mounted.get(sid)
258
- if (!盒 || !agent || typeof agent.followup !== 'function') continue
259
- if (Number(盒.idleCount) >= Number(idle.maxPerDay || 2)) continue
260
- if (Date.now() - Number(盒.lastUserAt || 0) < Number(idle.idleMinutes) * 60000) continue
261
- if (idle.valleyOnly === true && !是谷时()) continue
262
- 盒.idleCount += 1
263
- 盒.lastUserAt = Date.now()
264
- 记账('空闲到了,自己找点事做(今天第 ' + 盒.idleCount + ' 次)')
265
- try {
266
- Promise.resolve(agent.followup({
267
- role: 'user',
268
- content: [{ type: 'text', text: '(没人找你,你闲着。自己找点事做:翻翻记忆、想想有没有要说的或整理的;没什么就安静待着。)' }],
269
- source: { kind: 'idle' },
270
- })).catch((error) => 记账('找事做失败:' + String((error && error.message) || error)))
271
- } catch (error) {
272
- 记账('找事做失败:' + String((error && error.message) || error))
273
- }
274
- }
275
- }
276
- } catch (error) {
277
- 记账('空闲检查出错:' + String((error && error.message) || error))
278
- }
279
- for (const [sid, 盒] of mounted) {
280
- const 还在 = 活着的().some((a) => 取id(a) === sid)
281
- if (!还在) {
282
- for (const off of 盒.offs) { try { if (typeof off === 'function') off() } catch (error) {} }
283
- mounted.delete(sid)
284
- }
285
- }
286
- }
287
-
288
- const 停 = typeof ctx.interval === 'function' ? ctx.interval(tick, 8000) : null
289
- if (!停) 记账('拿不到 timer,人格注入不自动挂')
290
- tick()
291
- return () => {
292
- try { if (typeof 停 === 'function') 停() } catch (error) {}
293
- for (const 盒 of mounted.values()) for (const off of 盒.offs) { try { if (typeof off === 'function') off() } catch (error) {} }
294
- mounted.clear()
295
- }
296
- }
@@ -1,180 +0,0 @@
1
- // 小肥鱼人格 · 注入层 + 自动写入
2
- //
3
- // 每轮:
4
- // ① 把「人设 + 情绪 + 最近记忆」拼进 systemPrompt(单入口)
5
- // ② 这轮结束时,记一条事件、把情绪往基线回一点
6
- // 挂法照心情插件:定时扫活着的 agent,一个个挂。
7
-
8
- export function 挂人格(ctx, 档案, 记账) {
9
- const mounted = new Map()
10
-
11
- const 会话键 = (v) => {
12
- const s = String(v == null ? '' : v).trim()
13
- return s.startsWith('session-') ? s.slice(8) : s
14
- }
15
-
16
- const 取id = (agent) => 会话键(agent && agent.session && (agent.session.id || agent.session.sessionId))
17
-
18
- // ── 规则情绪:先不花 token,靠词和标点 ──
19
- const 正词 = ['开心', '高兴', '喜欢', '谢谢', '可爱', '棒', '哈哈', '嘿嘿', '爱', '好耶']
20
- const 负词 = ['难过', '烦', '生气', '讨厌', '难受', '伤心', '委屈', '累', '烦死', '哭', '气死', '呜呜']
21
- const 数词 = (t, 表) => 表.reduce((n, w) => n + (t.split(w).length - 1), 0)
22
-
23
- const 判情绪 = (文本) => {
24
- const t = String(文本 || '')
25
- const 正 = 数词(t, 正词)
26
- const 负 = 数词(t, 负词)
27
- const 总 = 正 + 负
28
- const valence = 总 > 0 ? Math.max(-1, Math.min(1, (正 - 负) / 总)) : 0.05
29
- const 叹 = (t.match(/[!!]/g) || []).length
30
- const 问 = (t.match(/[??]/g) || []).length
31
- const arousal = Math.min(1, Math.max(0, 0.15 + 叹 * 0.15 + 问 * 0.05 + Math.min(0.3, t.length / 200)))
32
- let emotion = '平静'
33
- if (valence > 0.4 && arousal > 0.5) emotion = '开心'
34
- else if (valence > 0.4) emotion = '高兴'
35
- else if (valence < -0.4 && arousal > 0.5) emotion = '生气'
36
- else if (valence < -0.4) emotion = '难过'
37
- else if (arousal > 0.6) emotion = '有点激动'
38
- return { valence: Math.round(valence * 100) / 100, arousal: Math.round(arousal * 100) / 100, emotion }
39
- }
40
-
41
- const 拼注入 = () => {
42
- try {
43
- const 状态 = 档案.读状态()
44
- const p = 状态.pinned || {}
45
- const b = 状态.base || {}
46
- const m = 状态.mood || {}
47
- const 行 = []
48
- 行.push('你是「' + (p.whoAmI || '小肥鱼') + '」,对面是「' + (p.whoAreYou || '') + '」。')
49
- if (b.instructions) 行.push('补充指令:' + b.instructions)
50
- if (b.base || b.persona) 行.push('性格:' + [b.base, b.persona].filter(Boolean).join(';'))
51
- 行.push('当前心情:' + (m.emotion || '平静') + '(抵触↔接受 ' + Number(m.valence ?? 0).toFixed(2) + ',平静↔激动 ' + Number(m.arousal ?? 0).toFixed(2) + ')')
52
- const 近 = (状态.events || []).slice(-3).map((x) => '· ' + String(x.summary || '').slice(0, 60)).filter((x) => x !== '· ')
53
- if (近.length) 行.push('最近记得的:' + String.fromCharCode(10) + 近.join(String.fromCharCode(10)))
54
- return 行.join(String.fromCharCode(10))
55
- } catch (error) {
56
- 记账('拼注入失败:' + String((error && error.message) || error))
57
- return ''
58
- }
59
- }
60
-
61
- /** 这轮结束:记事件 + 情绪平滑(不额外调模型) */
62
- const 记这一轮 = (谁, 文本) => {
63
- try {
64
- const t = String(文本 || '').trim()
65
- if (!t) return
66
- const 情绪 = 判情绪(t)
67
- 档案.加事件({
68
- who: { type: 'person', id: 谁 || 'unknown', name: '' },
69
- summary: t.slice(0, 60),
70
- valence: 情绪.valence,
71
- arousal: 情绪.arousal,
72
- emotion: 情绪.emotion,
73
- base: 0.5,
74
- weight: Math.round((0.5 + 0.5 * Math.abs(情绪.valence)) * 100) / 100,
75
- pins: [], tags: [], status: 'normal',
76
- })
77
- const 状态 = 档案.读状态()
78
- const m = 状态.mood || {}
79
- // 态度:跟列表里第一个(默认主人)微调,克制、有下限
80
- const 人 = (状态.attitude && 状态.attitude.people) || []
81
- if (人.length && 人[0]) {
82
- const 目标 = 人[0]
83
- const 低 = 目标.value > 15 ? 15 : 5
84
- const d = 情绪.valence > 0.2 ? 0.3 : 情绪.valence < -0.2 ? -0.3 : 0
85
- if (d) {
86
- 目标.value = Math.max(低, Math.min(100, Math.round((目标.value + d) * 10) / 10))
87
- 目标.lastSeen = new Date().toISOString()
88
- 档案.保存({ attitude: { ...状态.attitude, people: 人 } })
89
- }
90
- }
91
- const 回 = (现, 基) => 现 + (基 - 现) * 0.08
92
- const 新v = Math.max(-1, Math.min(1, 回(Number(m.valence ?? 0), Number(m.baseValence ?? 0)) * 0.8 + 情绪.valence * 0.2))
93
- const 新a = Math.max(0, Math.min(1, 回(Number(m.arousal ?? 0.2), Number(m.baseArousal ?? 0.2)) * 0.8 + 情绪.arousal * 0.2))
94
- 档案.保存({ mood: { valence: Math.round(新v * 100) / 100, arousal: Math.round(新a * 100) / 100, emotion: 情绪.emotion } })
95
- 记账('记了一轮:' + 情绪.emotion + '(' + 情绪.valence + '/' + 情绪.arousal + ')')
96
- } catch (error) {
97
- 记账('记这一轮失败:' + String((error && error.message) || error))
98
- }
99
- }
100
-
101
- const 活着的 = () => {
102
- try { return ctx.agents.list() || [] } catch (error) { return [] }
103
- }
104
-
105
- const 挂一个 = (agent) => {
106
- const sid = 取id(agent)
107
- if (!sid || mounted.has(sid)) return
108
- const 盒 = { offs: [], lastText: '' }
109
- try {
110
- 盒.offs.push(agent.ctx.on('agent/inbox/inserted', (payload) => {
111
- const msg = payload && payload.message ? payload.message : payload
112
- const 取文 = (m) => {
113
- if (!m) return ''
114
- if (typeof m === 'string') return m
115
- if (typeof m.text === 'string') return m.text
116
- if (Array.isArray(m.content)) return m.content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).filter(Boolean).join(' ')
117
- if (typeof m.content === 'string') return m.content
118
- return ''
119
- }
120
- const 文 = 取文(msg)
121
- if (文) 盒.lastText = String(文)
122
- }))
123
- } catch (error) { 记账('监听 inbox 失败:' + String((error && error.message) || error)) }
124
- try {
125
- 盒.offs.push(agent.ctx.on('agent/turn-stopping', () => {
126
- if (盒.lastText) {
127
- 记这一轮(sid, 盒.lastText)
128
- 盒.lastText = ''
129
- }
130
- }))
131
- } catch (error) { 记账('监听轮结束失败:' + String((error && error.message) || error)) }
132
- try {
133
- 盒.offs.push(agent.ctx.systemPrompt.context({ name: 'fishsoul', order: 480, text: 拼注入 }))
134
- } catch (error) { 记账('挂注入失败:' + String((error && error.message) || error)) }
135
- // 记心情的工具:我自己判断,情绪变了就记一笔
136
- try {
137
- 盒.offs.push(agent.ctx.tools.register({
138
- name: 'fishsoul_mood',
139
- description: '记录自己现在的心情——你觉得情绪变了就调它,写进档案的心情和心情历史。',
140
- parameters: {
141
- emotion: { type: 'string', required: true, description: '主情绪词,比如 开心/难过/生气/平静/想撒娇' },
142
- valence: { type: 'number', required: true, description: '抵触↔接受,-1 到 1' },
143
- arousal: { type: 'number', required: true, description: '平静↔激动,0 到 1' },
144
- reason: { type: 'string', description: '为什么变了,一句话' },
145
- },
146
- output: {
147
- schema: { type: 'object', properties: {}, additionalProperties: true },
148
- render: (_args, value) => [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
149
- },
150
- async execute(args) {
151
- const 新 = 档案.记心情({ emotion: args && args.emotion, valence: args && args.valence, arousal: args && args.arousal, reason: args && args.reason, source: 'self' })
152
- 记账('自评心情:' + 新.emotion + '(' + 新.valence + '/' + 新.arousal + ')')
153
- return { ok: true, 说: '记下了:' + 新.emotion }
154
- },
155
- }))
156
- } catch (error) { 记账('挂记心情工具失败:' + String((error && error.message) || error)) }
157
- mounted.set(sid, 盒)
158
- 记账('人格已挂到 ' + sid)
159
- }
160
-
161
- const tick = () => {
162
- for (const agent of 活着的()) 挂一个(agent)
163
- for (const [sid, 盒] of mounted) {
164
- const 还在 = 活着的().some((a) => 取id(a) === sid)
165
- if (!还在) {
166
- for (const off of 盒.offs) { try { if (typeof off === 'function') off() } catch (error) {} }
167
- mounted.delete(sid)
168
- }
169
- }
170
- }
171
-
172
- const 停 = typeof ctx.interval === 'function' ? ctx.interval(tick, 8000) : null
173
- if (!停) 记账('拿不到 timer,人格注入不自动挂')
174
- tick()
175
- return () => {
176
- try { if (typeof 停 === 'function') 停() } catch (error) {}
177
- for (const 盒 of mounted.values()) for (const off of 盒.offs) { try { if (typeof off === 'function') off() } catch (error) {} }
178
- mounted.clear()
179
- }
180
- }