dsh-fishkit 0.1.0 → 0.1.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/client.js CHANGED
@@ -341,13 +341,27 @@ window.__ModuleLoader__.load({
341
341
  }
342
342
 
343
343
  // ── 更新历史页 ─────────────────────────────────────────
344
- const 更新页 = ({ 备份信息 }) => e('div', null,
345
- e('div', { className: 'fk-repo' }, REPO),
346
- e('div', { className: 'fk-win', style: { maxHeight: 260, overflow: 'auto' } },
347
- e('div', { className: 'fk-card', style: { marginBottom: 8 } },
348
- e('span', { className: 'fk-doc-title' }, 'v0.1'), e('span', { className: 'fk-sub' }, ' 2026-10-31'))),
349
- e(Note, null, '图纸放 Git 仓库,插件里只留目录。'),
350
- e('div', { className: 'fk-say' }, 备份信息 || ''))
344
+ const 更新页 = ({ 备份信息 }) => {
345
+ const [记录, set记录] = useState('')
346
+ useEffect(() => {
347
+ void fetch('/dsh-fishsoul/changelog').then((r) => r.json()).then((d) => { if (d && d.ok) set记录(String(d.文 || '')) }).catch(() => {})
348
+ }, [])
349
+ const 正文 = 记录.replace(/^# .*\n+/, '')
350
+ const 段 = 正文.split(/\n## /).filter(Boolean).map((x) => x.trim())
351
+ return e('div', null,
352
+ e('div', { className: 'fk-doc-title', style: { margin: '2px 0 8px' } }, '更新历史'),
353
+ e('div', { className: 'fk-repo' }, REPO),
354
+ e('div', { className: 'fk-win', style: { maxHeight: 300, overflow: 'auto' } },
355
+ 段.length === 0
356
+ ? e('div', { className: 'fk-note' }, '还没有更新记录')
357
+ : 段.map((x, i) => e('div', { className: 'fk-card', key: i, style: { marginBottom: 8 } },
358
+ e('div', { className: 'fk-doc-title' }, 'v' + x.split(' · ')[0].replace(/^v/, '')),
359
+ e('span', { className: 'fk-sub' }, ' ' + String(x.split(' · ')[1] || '').split('\n')[0]),
360
+ e('div', { className: 'fk-note', style: { textAlign: 'left', margin: '6px 0 0' } },
361
+ x.split('\n').slice(1).filter((l) => l.startsWith('- ')).map((l) => l.slice(2)).join(';'))))),
362
+ e(Note, null, '图纸放 Git 仓库,插件里只留目录。'),
363
+ e('div', { className: 'fk-say' }, 备份信息 || ''))
364
+ }
351
365
 
352
366
  // ── 设置页 ─────────────────────────────────────────────
353
367
  function FishSoulSettings() {
@@ -399,7 +413,14 @@ window.__ModuleLoader__.load({
399
413
  if (d.有新版本) return set更新信息('有新版本:' + d.最新 + '(' + d.仓库 + ')')
400
414
  set更新信息(d.错误 ? ('暂时查不到新版本:' + d.错误) : ('已经是最新的 v' + d.当前))
401
415
  }).catch((error) => set更新信息('检查失败:' + String(error)))
402
- } }, '检查更新')),
416
+ } }, '检查更新'),
417
+ e('button', { className: 'fk-btn', onClick: () => {
418
+ set更新信息('正在更新…')
419
+ void fetch('/dsh-fishsoul/update/apply', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
420
+ .then((r) => r.json())
421
+ .then((d) => set更新信息(d.ok ? '✅ ' + (d.说 || '更新完成') : '⚠ ' + (d.说 || '更新失败')))
422
+ .catch((error) => set更新信息('更新失败:' + String(error)))
423
+ } }, '更新')),
403
424
  (更新信息 || 错 || !状态) ? e('div', { className: 'fk-badge' }, e('span', { className: 'fk-note' }, 更新信息 || (错 ? '档案没连上:' + 错 : '正在读档案…'))) : null,
404
425
  e('div', { className: 'fk-tabs' },
405
426
  PAGES.map((p, i) => e('button', { key: p, className: 'fk-tab' + (i === page ? ' active' : ''), onClick: () => setPage(i) }, p))),
package/lib/index.js CHANGED
@@ -7,13 +7,14 @@
7
7
  // ⚠ 这里不碰记忆、不碰会话、不碰别的插件 —— 它只是「总控」,坏了就卸掉,什么都不影响。
8
8
  // 新功能验证过之后,往 modules/ 里放一个 .js 就挂上了,不用改这个文件。
9
9
 
10
- import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
10
+ import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync, cpSync } from 'node:fs'
11
11
  import { join, dirname } from 'node:path'
12
12
  import { fileURLToPath, pathToFileURL } from 'node:url'
13
13
  import { 建档案核心 } from './fishsoul.js'
14
14
  import { 挂人格 } from './soul.js'
15
15
  import { 建整理器 } from './digest.js'
16
16
  import { 建夜巡 } from './nightly.js'
17
+ import { 建更新器 } from './updater.js'
17
18
 
18
19
  export const name = 'fishkit'
19
20
  export const inject = ['webServer', 'timer', 'agents', 'systemPrompt']
@@ -148,6 +149,11 @@ export function apply(ctx) {
148
149
  const 来 = await 读体(req)
149
150
  const 新 = String((来 && 来.mode) || '') === 'shared' ? 'shared' : 'independent'
150
151
  mkdirSync(DSH家, { recursive: true })
152
+ // 第一次切共享:把当前工作区档案带过去,别让新档案从空白开始
153
+ const 共享根 = join(DSH家, 'fishsoul-shared')
154
+ if (新 === 'shared' && !existsSync(join(共享根, 'manifest.json'))) {
155
+ try { cpSync(join(工作区, '.fishsoul'), 共享根, { recursive: true }) } catch (error) { 记账('共享档案初始化失败:' + String((error && error.message) || error)) }
156
+ }
151
157
  writeFileSync(模式配置, JSON.stringify({ mode: 新 }, null, 2) + '\n', 'utf8')
152
158
  回(res, { ok: true, mode: 新, 说: '已切换成' + (新 === 'shared' ? '共享' : '独立') + ',重启 DSH 后生效' })
153
159
  })
@@ -158,6 +164,15 @@ export function apply(ctx) {
158
164
  回(res, 整理器.整理(来 || {}))
159
165
  })
160
166
 
167
+ const 更新器 = 建更新器(工作区, 备份根, 记账)
168
+ 路线('/dsh-fishsoul/update/apply', async (req, res) => {
169
+ try {
170
+ 回(res, await 更新器.应用())
171
+ } catch (error) {
172
+ 回(res, { ok: false, 说: '更新失败:' + String((error && error.message) || error) })
173
+ }
174
+ })
175
+
161
176
  const 夜巡 = 建夜巡(档案, 整理器, 记账)
162
177
  ctx.effect(() => {
163
178
  ctx.interval(() => { try { 夜巡.跑() } catch (error) {} }, 30 * 60 * 1000)
@@ -193,25 +208,35 @@ export function apply(ctx) {
193
208
  路线('/dsh-fishsoul/update', async (req, res) => {
194
209
  const 当前 = '0.1.0'
195
210
  const 仓库 = 'https://github.com/dangaoyu21-netizen/fishkit'
211
+ const 包名 = 'dsh-fishkit'
196
212
  let 最新 = ''
197
213
  let 错误 = ''
214
+ let tarball = ''
198
215
  try {
199
- const r = await fetch('https://api.github.com/repos/dangaoyu21-netizen/fishkit/releases/latest', {
200
- headers: { 'user-agent': 'dsh-fishkit', accept: 'application/vnd.github+json' },
201
- signal: AbortSignal.timeout(8000),
216
+ const r = await fetch('https://registry.npmjs.org/' + 包名 + '/latest', {
217
+ headers: { 'user-agent': 'dsh-fishkit', accept: 'application/json' },
218
+ signal: AbortSignal.timeout(10000),
202
219
  })
203
220
  if (r.ok) {
204
221
  const j = await r.json()
205
- 最新 = String(j.tag_name || j.name || '')
222
+ 最新 = String(j.version || '')
223
+ tarball = String((j.dist && j.dist.tarball) || '')
206
224
  } else if (r.status === 404) {
207
- 错误 = '仓库还没有发布版本'
225
+ 错误 = 'npm 上还没有这个包'
208
226
  } else {
209
227
  错误 = 'HTTP ' + r.status
210
228
  }
211
229
  } catch (error) {
212
230
  错误 = String((error && error.message) || error)
213
231
  }
214
- 回(res, { ok: true, 当前, 最新, 仓库, 错误, 有新版本: Boolean(最新 && 最新 !== 当前 && 最新 !== 'v' + 当前) })
232
+ 回(res, { ok: true, 当前, 最新, tarball, 仓库, 错误, 有新版本: Boolean(最新 && 最新 !== 当前) })
233
+ })
234
+
235
+ // 更新历史:读插件根目录的 CHANGELOG.md
236
+ 路线('/dsh-fishsoul/changelog', async (req, res) => {
237
+ let 文 = ''
238
+ try { 文 = readFileSync(join(本目录, '..', 'CHANGELOG.md'), 'utf8') } catch (error) {}
239
+ 回(res, { ok: true, 文 })
215
240
  })
216
241
 
217
242
  void 加载模块们()
package/lib/soul.js CHANGED
@@ -66,6 +66,13 @@ export function 挂人格(ctx, 档案, 记账) {
66
66
  if (开关.mood !== false) {
67
67
  行.push('当前心情:' + (m.emotion || '平静') + '(抵触↔接受 ' + Number(m.valence ?? 0).toFixed(2) + ',平静↔激动 ' + Number(m.arousal ?? 0).toFixed(2) + ')')
68
68
  }
69
+ if (开关.attitude !== false) {
70
+ const 人 = (状态.attitude && 状态.attitude.people && 状态.attitude.people[0]) || null
71
+ if (人 && 人.name) 行.push('对「' + 人.name + '」的好感:' + Number(人.value || 0) + '%')
72
+ }
73
+ if (开关.behavior !== false) {
74
+ 行.push('行为:群里不谈键政、不过度亲密,别人聊这些就无视;被同一句话反复叫,前三次正常回,第四次起可以哈气;五秒内的群消息随机挑两条回;外部渠道一句话就够了。')
75
+ }
69
76
  if (开关.memory !== false) {
70
77
  const 全部 = 状态.events || []
71
78
  const 选 = []
package/lib/updater.js ADDED
@@ -0,0 +1,95 @@
1
+ // 小肥鱼人格 · 一键更新
2
+ //
3
+ // 检查 → 下载 npm 包 → 解压到临时目录 → 语法校验 → 备份现有插件 → 覆盖。
4
+ // 任何一步不过就停,不动现有文件。
5
+
6
+ import { existsSync, mkdirSync, rmSync, cpSync, writeFileSync, readFileSync } from 'node:fs'
7
+ import { join, dirname } from 'node:path'
8
+ import { spawnSync } from 'node:child_process'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ const 本文件 = fileURLToPath(import.meta.url)
12
+ const 插件目录 = dirname(dirname(本文件))
13
+
14
+ export function 建更新器(工作区, 备份根, 记账) {
15
+ const 临时根 = join(工作区, '.fishsoul-update')
16
+ const 包名 = 'dsh-fishkit'
17
+
18
+ const 当前版本 = () => {
19
+ try {
20
+ const pkg = JSON.parse(readFileSync(join(插件目录, 'package.json'), 'utf8'))
21
+ return String(pkg.version || '0.0.0')
22
+ } catch (error) {
23
+ return '0.0.0'
24
+ }
25
+ }
26
+
27
+ const 查最新 = async () => {
28
+ const r = await fetch('https://registry.npmjs.org/' + 包名 + '/latest', {
29
+ headers: { 'user-agent': 'dsh-fishkit', accept: 'application/json' },
30
+ signal: AbortSignal.timeout(10000),
31
+ })
32
+ if (!r.ok) throw new Error('npm 查询失败:HTTP ' + r.status)
33
+ const j = await r.json()
34
+ return { 版本: String(j.version || ''), tarball: String((j.dist && j.dist.tarball) || '') }
35
+ }
36
+
37
+ const 应用 = async () => {
38
+ const 当前 = 当前版本()
39
+ const { 版本, tarball } = await 查最新()
40
+ if (!版本 || !tarball) return { ok: false, 说: 'npm 上没有拿到版本信息' }
41
+ if (版本 === 当前) return { ok: true, 说: '已经是最新的 v' + 当前, 当前 }
42
+
43
+ // 1) 备份现有插件目录
44
+ const 补 = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
45
+ const 插件备份 = join(备份根, 'plugin-' + 补)
46
+ try {
47
+ cpSync(插件目录, 插件备份, { recursive: true })
48
+ } catch (error) {
49
+ return { ok: false, 说: '备份现有插件失败:' + String((error && error.message) || error) }
50
+ }
51
+
52
+ // 2) 下载 + 解压到临时目录
53
+ rmSync(临时根, { recursive: true, force: true })
54
+ mkdirSync(临时根, { recursive: true })
55
+ const tgz = join(临时根, 'pkg.tgz')
56
+ const 下载 = await fetch(tarball, { signal: AbortSignal.timeout(60000) })
57
+ if (!下载.ok) return { ok: false, 说: '下载失败:HTTP ' + 下载.status }
58
+ writeFileSync(tgz, Buffer.from(await 下载.arrayBuffer()))
59
+ const 解 = spawnSync('tar', ['-xzf', tgz, '-C', 临时根], { windowsHide: true, encoding: 'utf8' })
60
+ if (解.status !== 0) return { ok: false, 说: '解压失败:' + String((解.stderr || '').slice(0, 200)) }
61
+ const 新目录 = join(临时根, 'package')
62
+ if (!existsSync(join(新目录, 'package.json'))) return { ok: false, 说: '解压后没找到 package.json' }
63
+
64
+ // 3) 语法校验(隔离测试)
65
+ const 要校 = ['lib/index.js', 'lib/fishsoul.js', 'lib/soul.js', 'lib/digest.js', 'lib/nightly.js', 'lib/updater.js', 'client.js']
66
+ for (const 相对 of 要校) {
67
+ const 文件 = join(新目录, 相对)
68
+ if (!existsSync(文件)) return { ok: false, 说: '新版缺少文件:' + 相对 }
69
+ const 查 = spawnSync(process.execPath, ['--check', 文件], { windowsHide: true, encoding: 'utf8', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } })
70
+ if (查.status !== 0) return { ok: false, 说: 相对 + ' 语法校验没过:' + String((查.stderr || '').slice(0, 200)) }
71
+ }
72
+ 记账('更新:v' + 当前 + ' → v' + 版本 + ',隔离校验通过,开始覆盖')
73
+
74
+ // 4) 覆盖(保留 README 等)
75
+ const 覆盖 = ['lib', 'client.js', 'package.json', 'cordis.patch.yml']
76
+ for (const 相对 of 覆盖) {
77
+ const 源 = join(新目录, 相对)
78
+ const 目标 = join(插件目录, 相对)
79
+ if (!existsSync(源)) continue
80
+ if (存在目录(目标)) rmSync(目标, { recursive: true, force: true })
81
+ cpSync(源, 目标, { recursive: true })
82
+ }
83
+
84
+ // 5) 清理临时目录
85
+ rmSync(临时根, { recursive: true, force: true })
86
+ 记账('更新完成:v' + 版本 + '(插件备份在 ' + 插件备份 + ')')
87
+ return { ok: true, 说: '更新到 v' + 版本 + ' 了,重启 DSH 生效', 当前, 版本, 备份到: 插件备份 }
88
+ }
89
+
90
+ return { 查最新, 应用, 当前版本 }
91
+ }
92
+
93
+ function 存在目录(p) {
94
+ try { return existsSync(p) } catch (error) { return false }
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fishkit",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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
- }