@ssamurias/dsh-fish 0.8.9 → 0.9.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.
Files changed (49) hide show
  1. package/AGENTS.md +32 -9
  2. package/CHANGELOG.md +84 -2
  3. package/LICENSE +7 -0
  4. package/README.md +64 -17
  5. package/RELEASE-NOTES-0.8.0.md +87 -0
  6. package/RELEASE-NOTES-0.8.1.md +93 -0
  7. package/RELEASE-NOTES-0.8.2.md +57 -0
  8. package/RELEASE-NOTES-0.8.3.md +71 -0
  9. package/RELEASE-NOTES-0.8.4.md +47 -0
  10. package/RELEASE-NOTES-0.8.5.md +244 -0
  11. package/RELEASE-NOTES-0.8.6.md +224 -0
  12. package/RELEASE-NOTES-0.8.7.md +202 -0
  13. package/RELEASE-NOTES-0.8.8.md +224 -0
  14. package/RELEASE-NOTES-0.9.0.md +267 -0
  15. package/THIRD-PARTY-NOTICES.md +3 -0
  16. package/lib/away.js +7 -2
  17. package/lib/channels.js +53 -13
  18. package/lib/client.js +99 -49
  19. package/lib/compact.js +26 -1
  20. package/lib/derive.js +10 -6
  21. package/lib/desk.js +1 -1
  22. package/lib/dream.js +3 -0
  23. package/lib/dreamtable.js +15 -0
  24. package/lib/drive.js +44 -7
  25. package/lib/freewake.js +17 -1
  26. package/lib/home.js +54 -2
  27. package/lib/inbox.js +7 -2
  28. package/lib/index.js +8 -12
  29. package/lib/interest.js +23 -3
  30. package/lib/log.js +22 -1
  31. package/lib/look.js +2 -2
  32. package/lib/note.js +35 -13
  33. package/lib/notify.js +2 -0
  34. package/lib/observe.js +13 -7
  35. package/lib/outing.js +2 -0
  36. package/lib/persona.js +2 -1
  37. package/lib/proactive.js +12 -7
  38. package/lib/say.js +104 -58
  39. package/lib/scheduler.js +67 -37
  40. package/lib/selfcheck.js +2 -3
  41. package/lib/selfdoc.js +3 -6
  42. package/lib/selfupdate.js +176 -41
  43. package/lib/settings.js +28 -2
  44. package/lib/turns.js +41 -3
  45. package/lib/update.js +14 -0
  46. package/lib/upgrade.js +19 -3
  47. package/lib/voices.js +2 -0
  48. package/package.json +4 -2
  49. package/scripts/fix-legacy-link.mjs +147 -0
package/lib/turns.js CHANGED
@@ -31,7 +31,8 @@
31
31
  * 宿主的 v4 校验只要求 source.kind 是「非空、且不等于 'plugin'」的 producer-owned
32
32
  * 字符串(见 proactive.js 里 assertV4MessageSources 那段注释),所以外渠道的 kind
33
33
  * 是开放集合,任何桥都可以自己起名(qq / telegram / 别的)。认识的那几种挡掉,
34
- * 其余一律当人——宁可多算一轮也不静默丢消息。
34
+ * 宿主拼的上下文按 source.form 挡掉(MACHINE_FORMS),其余一律当人——
35
+ * 宁可多算一轮也不静默丢消息。
35
36
  *
36
37
  * 铁律:这里**基本只读**会话事件、只写 home.json / talk-recent.json / 一条 meta,
37
38
  * 不动对话本身。任何失败都吞掉——记账出错不能连累她说话。
@@ -85,8 +86,38 @@ const MACHINE_KINDS = new Set([
85
86
  // 漏了这一行,她自己醒来说的话就会被记成「他来过」——冷落天数、seen、
86
87
  // turns 全被污染,她越主动,面板越显示「他刚来过」。
87
88
  'fish-wake',
89
+ // 2026-11-05:宿主自己写进会话、又不带 source.form 的几种(带 form 的见 MACHINE_FORMS)。
90
+ // 都是 role:'user' 的系统消息,人说话时不会用这些 kind。
91
+ // 名单对照的是宿主 0.2.2-rc.1 的 MessageSourceMap(dsh-api-session-controller)。
92
+ 'working-directory', // 工作目录变了
93
+ 'cordis-host-runner', // 插件运行器的转告
94
+ 'ptc-mode', // 工具执行模式提示
95
+ 'user-approval', // 审批策略变了
96
+ 'compact-checkpoint', // 压缩检查点
97
+ 'goal', // /goal 的目标
98
+ 'team-message', // 队友之间的话
99
+ 'agent-message', // 子代理、队友之间转的话(form 是 relay,见下)
100
+ // dsh-bili-humanism 给模型排的提示词(手账存完后的提问、人格审阅)。
101
+ // 它们不是 plugin: 前缀,所以要单列,否则写一篇手账就多一条「他来过」。
102
+ 'bili-humanism-post-viewing-question',
103
+ 'bili-humanism-personality-review',
104
+ 'bili-humanism-shadow-review',
88
105
  ])
89
106
 
107
+ /**
108
+ * 宿主拼上下文时写在 source.form 上的形态(宿主前端的 KNOWN_FORMS)。
109
+ *
110
+ * 宿主给模型看的上下文(time-context、model-selection、tool-jobs、plan-mode、
111
+ * action-cards、agent-instructions、session-reference、tmux 快照……)kind 各不相同,
112
+ * 以后还会有新的,靠列 kind 总会漏;它们的共同点是都带这个字段。
113
+ * 漏一个的后果很重:time-context 最多十分钟一条,每条都是一次「他来过」、
114
+ * 一次 turns +1、一条写在他名下的 heard,满 rotateAfter 换房。
115
+ *
116
+ * relay 故意不在表里:外渠道转述人说的话,也可能自称 relay,那种要算人。
117
+ * 所以桥不要用这个表里的 form,用了就会被当上下文(AGENTS.md §7.2)。
118
+ */
119
+ const MACHINE_FORMS = new Set(['snapshot', 'notice', 'catalog', 'instructions', 'recall'])
120
+
90
121
  /** 一条 user 消息最多记这么多字(talkmem 会再截到 160,这里只是别让整段注入进内存) */
91
122
  export const TURN_TEXT_MAX = 2000
92
123
  /** 一轮里连着打好几句时,最多合并这么多条 */
@@ -122,6 +153,7 @@ export function classifyMessage(data) {
122
153
  if (normalizeScene(src.scene) === 'group') return 'group'
123
154
  // 宿主拼的上下文 / 定时消息 / 工具回灌 / 她自己说的话:都不是他
124
155
  if (MACHINE_KINDS.has(kind) || kind.startsWith('plugin:')) return 'context'
156
+ if (MACHINE_FORMS.has(src.form)) return 'context'
125
157
  const rid = String(src.rpcId ?? '')
126
158
  if (rid && PLUGIN_RPC.test(rid)) return 'plugin'
127
159
  if (kind === 'user') {
@@ -130,7 +162,7 @@ export function classifyMessage(data) {
130
162
  if (src.clientTimeZone) return 'human'
131
163
  return 'unknown'
132
164
  }
133
- // 表里没有的 kind = 外渠道(qq / telegram / 别的桥自己起的名字)。
165
+ // 两张表都没挡住的 kind = 外渠道(qq / telegram / 别的桥自己起的名字)。
134
166
  // 宿主对 kind 只要求「非空且不是 'plugin'」,穷举不了,所以默认当人。
135
167
  return 'human'
136
168
  }
@@ -420,12 +452,18 @@ export function registerTurns(ctx, deps = {}) {
420
452
  if (!(rotateAfter > 0) || turns < rotateAfter) return
421
453
 
422
454
  // 该换会话了。换失败不算错:她还在旧会话里,下次再换。
455
+ // from:这一轮是从哪条会话里结出来的。`/say` 那边可能刚刚换过,
456
+ // Home 比对 home.json 之后会返回 skipped,这里就别再建第二条(0.8.8)。
423
457
  const st = await homeStatus(root)
424
- const rot = await rotateTalkSession(ctx, { root, wsId: st.wsId, log })
458
+ const rot = await rotateTalkSession(ctx, { root, wsId: st.wsId, log, from: p.sessionId })
425
459
  if (!rot.ok) {
426
460
  log(`turns · 聊满 ${turns} 轮,该换会话但没换成:${rot.why}`)
427
461
  return
428
462
  }
463
+ if (rot.skipped) {
464
+ log(`turns · 聊满 ${turns} 轮,但屋子已经先换过了(${rot.sessionId}),这条不再换`)
465
+ return
466
+ }
429
467
  // 留一条 meta:换会话这件事要能在事件流里查到(观测台/排查用)。
430
468
  // source 必须是 system——'tool' 会被 schema 拒掉。
431
469
  await appendEvent(
package/lib/update.js CHANGED
@@ -24,11 +24,25 @@ export const REPO = 'ssamurias/dsh-fish'
24
24
  export const REPO_URL = `https://github.com/${REPO}`
25
25
  export const RELEASES_URL = `${REPO_URL}/releases`
26
26
 
27
+ /**
28
+ * npm 包名(2026-11-05 起官方安装源)。
29
+ * `dsh-fish` 这个裸名在 2026-09-11 被别的账号占走了,所以发的是 scoped 全名。
30
+ * 版本号以 npm 为准:GitHub 的 tag / package.json 可能忘了 bump,npm 上不去就发不了版。
31
+ */
32
+ export const NPM_PKG = '@ssamurias/dsh-fish'
33
+ export const NPM_URL = `https://www.npmjs.com/package/${NPM_PKG}`
34
+
27
35
  /**
28
36
  * 版本号来源,按"越靠前越权威"排列(但同时发,不是串行)。
29
37
  * `pick` 从 JSON 里取版本字段:Release 给 `tag_name`,package.json 给 `version`。
30
38
  */
31
39
  const SOURCES = [
40
+ {
41
+ name: 'npm',
42
+ // scoped 包在 registry 里要写成 %2F;这条同时说明"装得到的最新版是哪个"。
43
+ url: `https://registry.npmjs.org/@ssamurias%2Fdsh-fish/latest`,
44
+ pick: (j) => j?.version,
45
+ },
32
46
  {
33
47
  name: 'GitHub API',
34
48
  url: `https://api.github.com/repos/${REPO}/releases/latest`,
package/lib/upgrade.js CHANGED
@@ -20,6 +20,14 @@ import { fileURLToPath } from 'node:url'
20
20
  /** npm 包名(= 插件 `export const name`)。 */
21
21
  export const PKG_NAME = 'dsh-fish'
22
22
 
23
+ /**
24
+ * 能认的包名,新名在前(2026-11-05 改名 @ssamurias/dsh-fish 之后)。
25
+ *
26
+ * 老 profile 里那条依赖的键可能还叫 `dsh-fish`:`link:` 装的用户不会因为换名字
27
+ * 自动改键,所以两边的键都要认,否则升级自检会安静地跳过(= 用户以为升了其实没升)。
28
+ */
29
+ export const PKG_NAMES = ['@ssamurias/dsh-fish', PKG_NAME]
30
+
23
31
  const DEP_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies']
24
32
 
25
33
  /**
@@ -77,8 +85,11 @@ export function profileDirs({ home = homedir(), env = process.env } = {}) {
77
85
  /** 从 profile 的 package.json 里取 `file:` 依赖指向的目录(绝对化)。 */
78
86
  export function fileDepDir(profileDir, pkg) {
79
87
  for (const field of DEP_FIELDS) {
80
- const spec = pkg?.[field]?.[PKG_NAME]
81
- if (typeof spec !== 'string' || !spec.startsWith('file:')) continue
88
+ // 两个键都看:只有 `file:` 的才算(`link:` 是源码直连,不走这套自检)
89
+ const spec = PKG_NAMES.map((n) => pkg?.[field]?.[n]).find(
90
+ (s) => typeof s === 'string' && s.startsWith('file:'),
91
+ )
92
+ if (typeof spec !== 'string') continue
82
93
  const raw = spec.slice('file:'.length).trim()
83
94
  if (!raw) return ''
84
95
  // `file:D:/x`、`file:/x`、`file:../../x` 三种都要认。
@@ -118,7 +129,12 @@ export async function checkUpgrade({ version = '', pkgFile = '', profilesDir = '
118
129
  if (!srcVersion) continue
119
130
  // 这个 profile 装进去的那份是哪个版本?跟正在跑的相等,说明这条依赖就是「当前这条命」。
120
131
  // 一台机器可能有 web / desktop 两个 profile,各自钉着不同的源目录,不能随手抓第一个。
121
- const installed = String((await readJson(join(dir, 'node_modules', PKG_NAME, 'package.json')))?.version ?? '')
132
+ // 名字不同,node_modules 里的目录也就不同(scoped 装法多一层 @ssamurias/)
133
+ let installed = ''
134
+ for (const n of PKG_NAMES) {
135
+ installed = String((await readJson(join(dir, 'node_modules', n, 'package.json')))?.version ?? '')
136
+ if (installed) break
137
+ }
122
138
  hits.push({ dir, srcDir, srcVersion, installed })
123
139
  }
124
140
 
package/lib/voices.js CHANGED
@@ -189,6 +189,8 @@ export function findEcho(voice, events = []) {
189
189
  const after = idx >= 0 ? events.slice(idx + 1) : events
190
190
  for (const e of after) {
191
191
  if (e.type !== 'utterance' && e.type !== 'revise') continue
192
+ // 没认领的聊天行不是信念:算认下,评论就从她眼前消失了,信念却没长
193
+ if (Array.isArray(e.flags) && e.flags.includes('uncommitted')) continue
192
194
  const t = Date.parse(e.ts ?? '')
193
195
  if (!Number.isFinite(t) || t - t0 > ECHO_WINDOW_MS) continue
194
196
  if (similarity(mine, normVoice(e.text)) >= ECHO_MIN_SIM) return e
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssamurias/dsh-fish",
3
- "version": "0.8.9",
3
+ "version": "0.9.0",
4
4
  "description": "养一条会自己长大的鱼:没有人格设置,通过事件流长大。她可以自己刷 B 站、会写日记给你看,四个阶段完成后才会开始有立场。状态库在 ~/.dsh-fish,不能删除记录。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -17,7 +17,9 @@
17
17
  "cordis.patch.yml",
18
18
  "README.md",
19
19
  "AGENTS.md",
20
- "CHANGELOG.md"
20
+ "CHANGELOG.md",
21
+ "RELEASE-NOTES-*.md",
22
+ "scripts"
21
23
  ],
22
24
  "scripts": {
23
25
  "test": "node --test"
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 把老的 `link:` / `file:` 安装迁移到新包名(@ssamurias/dsh-fish)。
4
+ *
5
+ * 为什么需要它:0.8.10 起插件包名从 `dsh-fish` 改成 `@ssamurias/dsh-fish`,
6
+ * 插件自带 cordis.patch.yml 里的条目名也跟着变成完整包名;而老 profile 的
7
+ * node_modules 里只有裸名链接 `dsh-fish` → 条目 import 失败 → **宿主直接启动崩溃**。
8
+ * 升级前跑一遍这个脚本就能避免;已经崩了也能跑(它只改 profile 文件,不需要应用启动)。
9
+ *
10
+ * 用法(先关掉 DeepSeek Harness):
11
+ * node scripts/fix-legacy-link.mjs # 预览
12
+ * node scripts/fix-legacy-link.mjs --write # 真的改
13
+ * node scripts/fix-legacy-link.mjs --write --profile C:\Users\你\.dsh\profiles\desktop
14
+ */
15
+ import fs from 'node:fs'
16
+ import path from 'node:path'
17
+ import os from 'node:os'
18
+
19
+ const NEW = '@ssamurias/dsh-fish'
20
+ const LEGACY = 'dsh-fish'
21
+ const args = process.argv.slice(2)
22
+ const WRITE = args.includes('--write')
23
+ const pIdx = args.indexOf('--profile')
24
+ const onlyProfile = pIdx >= 0 ? args[pIdx + 1] : ''
25
+
26
+ const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '')
27
+ const profilesRoot = path.join(os.homedir(), '.dsh', 'profiles')
28
+
29
+ function profileDirs() {
30
+ if (onlyProfile) return [path.resolve(onlyProfile)]
31
+ try {
32
+ return fs
33
+ .readdirSync(profilesRoot, { withFileTypes: true })
34
+ .filter((d) => d.isDirectory())
35
+ .map((d) => path.join(profilesRoot, d.name))
36
+ .filter((p) => fs.existsSync(path.join(p, 'package.json')))
37
+ } catch {
38
+ return []
39
+ }
40
+ }
41
+
42
+ function readJson(p) {
43
+ try {
44
+ return JSON.parse(fs.readFileSync(p, 'utf8'))
45
+ } catch {
46
+ return null
47
+ }
48
+ }
49
+
50
+ function specOf(pkg) {
51
+ for (const field of ['dependencies', 'devDependencies', 'optionalDependencies']) {
52
+ const spec = pkg?.[field]?.[LEGACY]
53
+ if (typeof spec === 'string' && /^(link:|file:)/.test(spec)) return { field, spec }
54
+ }
55
+ return null
56
+ }
57
+
58
+ function targetOf(profileDir, spec) {
59
+ const raw = spec.replace(/^(link:|file:)/, '').trim()
60
+ if (!raw) return ''
61
+ if (/^[A-Za-z]:[\\/]/.test(raw) || path.isAbsolute(raw)) return path.resolve(raw)
62
+ return path.resolve(profileDir, raw)
63
+ }
64
+
65
+ function linkName(profileDir, name) {
66
+ return path.join(profileDir, 'node_modules', ...name.split('/'))
67
+ }
68
+
69
+ let touched = 0
70
+ for (const dir of profileDirs()) {
71
+ const pkgPath = path.join(dir, 'package.json')
72
+ const pkg = readJson(pkgPath)
73
+ if (!pkg) continue
74
+ const found = specOf(pkg)
75
+ if (!found) {
76
+ console.log(`- ${dir}: 没有老的 ${LEGACY} link/file 依赖,跳过`)
77
+ continue
78
+ }
79
+ const target = targetOf(dir, found.spec)
80
+ const srcPkg = readJson(path.join(target, 'package.json'))
81
+ const srcName = String(srcPkg?.name ?? '')
82
+ console.log(`\n== ${dir}`)
83
+ console.log(` 依赖: ${found.field}["${LEGACY}"] = ${found.spec}`)
84
+ console.log(` 源目录: ${target}`)
85
+ console.log(` 源包名: ${srcName || '(读不到)'}`)
86
+ if (!srcName || srcName === LEGACY) {
87
+ console.log(' → 源目录还是老名字,这次迁移用不上(等你更新到 0.8.10 再跑)')
88
+ continue
89
+ }
90
+ if (srcName !== NEW) console.log(` ! 源包名是 ${srcName},脚本按它迁移`)
91
+ const newName = srcName === LEGACY ? NEW : srcName
92
+
93
+ if (!WRITE) {
94
+ console.log(` [预览] 会:建立 node_modules/${newName} → 源目录;把依赖键改成 ${newName};删掉 ${LEGACY} 键与链接`)
95
+ continue
96
+ }
97
+
98
+ // 1) 备份
99
+ const bak = `${pkgPath}.bak-fishrename-${stamp}`
100
+ fs.copyFileSync(pkgPath, bak)
101
+ console.log(` 备份: ${bak}`)
102
+
103
+ // 2) 建新名链接(Junction 在 Windows 不需要管理员权限)
104
+ const newLink = linkName(dir, newName)
105
+ fs.mkdirSync(path.dirname(newLink), { recursive: true })
106
+ if (!fs.existsSync(newLink)) {
107
+ try {
108
+ fs.symlinkSync(target, newLink, 'junction')
109
+ console.log(` 建链: ${newLink} -> ${target}`)
110
+ } catch (e) {
111
+ console.log(` ! 建链失败: ${e.message}(可手动 New-Item -ItemType Junction)`)
112
+ }
113
+ } else {
114
+ console.log(` 链接已存在: ${newLink}`)
115
+ }
116
+
117
+ // 3) 改依赖键
118
+ delete pkg[found.field][LEGACY]
119
+ pkg.dependencies = pkg.dependencies || {}
120
+ pkg.dependencies[newName] = found.spec
121
+ // bundles 跟着改
122
+ const bundles = pkg?.dsh?.profile?.bundles
123
+ if (Array.isArray(bundles)) {
124
+ pkg.dsh.profile.bundles = Array.from(new Set(bundles.map((b) => (b === LEGACY ? newName : b))))
125
+ }
126
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
127
+ console.log(` 依赖改为 ${found.field}["${newName}"],bundles 已对齐`)
128
+
129
+ // 4) 老链接改名停用(不删,留着能回滚)
130
+ const oldLink = linkName(dir, LEGACY)
131
+ if (fs.existsSync(oldLink)) {
132
+ const off = `${oldLink}.disabled-${stamp}`
133
+ try {
134
+ fs.renameSync(oldLink, off)
135
+ console.log(` 老链接停用: ${off}`)
136
+ } catch (e) {
137
+ console.log(` ! 老链接改名失败: ${e.message}`)
138
+ }
139
+ }
140
+ touched++
141
+ }
142
+
143
+ if (!WRITE) {
144
+ console.log('\n这是预览。确认无误后加 --write 再跑一次(记得先关应用)。')
145
+ } else {
146
+ console.log(`\n改完 ${touched} 个 profile。现在启动 DeepSeek Harness。`)
147
+ }