@ssamurias/dsh-fish 0.8.9 → 0.9.1
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 +64 -10
- package/CHANGELOG.md +129 -2
- package/LICENSE +7 -0
- package/README.md +69 -17
- package/RELEASE-NOTES-0.8.0.md +87 -0
- package/RELEASE-NOTES-0.8.1.md +93 -0
- package/RELEASE-NOTES-0.8.2.md +57 -0
- package/RELEASE-NOTES-0.8.3.md +71 -0
- package/RELEASE-NOTES-0.8.4.md +47 -0
- package/RELEASE-NOTES-0.8.5.md +244 -0
- package/RELEASE-NOTES-0.8.6.md +224 -0
- package/RELEASE-NOTES-0.8.7.md +202 -0
- package/RELEASE-NOTES-0.8.8.md +224 -0
- package/RELEASE-NOTES-0.9.0.md +267 -0
- package/RELEASE-NOTES-0.9.1.md +296 -0
- package/RELEASE.md +151 -0
- package/THIRD-PARTY-NOTICES.md +3 -0
- package/lib/ask.js +18 -5
- package/lib/away.js +7 -2
- package/lib/browse.js +1 -0
- package/lib/capture.js +16 -1
- package/lib/channels.js +53 -13
- package/lib/client.js +145 -56
- package/lib/comment.js +74 -0
- package/lib/compact.js +71 -2
- package/lib/curiosity.js +6 -2
- package/lib/derive.js +21 -8
- package/lib/desk.js +4 -2
- package/lib/dream.js +3 -0
- package/lib/dreamtable.js +28 -2
- package/lib/drive.js +58 -9
- package/lib/freewake.js +21 -2
- package/lib/home.js +141 -24
- package/lib/inbox.js +7 -2
- package/lib/index.js +8 -12
- package/lib/interest.js +26 -4
- package/lib/log.js +22 -1
- package/lib/look.js +25 -5
- package/lib/note.js +35 -13
- package/lib/notify.js +2 -0
- package/lib/observe.js +22 -7
- package/lib/outing.js +2 -0
- package/lib/owner.js +5 -2
- package/lib/persona.js +18 -3
- package/lib/proactive.js +78 -18
- package/lib/say.js +104 -58
- package/lib/scheduler.js +162 -60
- package/lib/selfcheck.js +2 -3
- package/lib/selfdoc.js +8 -9
- package/lib/selfupdate.js +183 -41
- package/lib/settings.js +33 -2
- package/lib/status.js +31 -3
- package/lib/store.js +14 -2
- package/lib/tenant.js +12 -0
- package/lib/turns.js +46 -4
- package/lib/update.js +14 -0
- package/lib/upgrade.js +23 -4
- package/lib/voices.js +2 -0
- package/package.json +5 -2
- package/scripts/fix-legacy-link.mjs +254 -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
|
* 不动对话本身。任何失败都吞掉——记账出错不能连累她说话。
|
|
@@ -57,8 +58,12 @@ import { stripToolMarkup, hasToolMarkup } from './markup.js'
|
|
|
57
58
|
/**
|
|
58
59
|
* 我们自己 prompt 进去的消息,rpcId 会带这些前缀。
|
|
59
60
|
* 只是"多一道保险":真正的判据是 clientTimeZone(见 classifyMessage)。
|
|
61
|
+
*
|
|
62
|
+
* `carry` 是换会话前请她留话那条(home.js 的 `carry-ask-`)。少了它,那条
|
|
63
|
+
* 消息会掉进 `kind === 'user'` 且没有 clientTimeZone 的 `unknown` 分支:不进
|
|
64
|
+
* 轮数(行为正确),但每换一次会话就往日志里多一行「认不出来源」(2026-11-06)。
|
|
60
65
|
*/
|
|
61
|
-
const PLUGIN_RPC = /^(wake|say|boot|report|letter|sleep|nudge|probe|turn|sys)[-_]/i
|
|
66
|
+
const PLUGIN_RPC = /^(wake|say|boot|report|letter|sleep|nudge|probe|turn|sys|carry)[-_]/i
|
|
62
67
|
|
|
63
68
|
/**
|
|
64
69
|
* 宿主/我们这边自己拼的、**确定不是人打的** source.kind。
|
|
@@ -85,8 +90,38 @@ const MACHINE_KINDS = new Set([
|
|
|
85
90
|
// 漏了这一行,她自己醒来说的话就会被记成「他来过」——冷落天数、seen、
|
|
86
91
|
// turns 全被污染,她越主动,面板越显示「他刚来过」。
|
|
87
92
|
'fish-wake',
|
|
93
|
+
// 2026-11-05:宿主自己写进会话、又不带 source.form 的几种(带 form 的见 MACHINE_FORMS)。
|
|
94
|
+
// 都是 role:'user' 的系统消息,人说话时不会用这些 kind。
|
|
95
|
+
// 名单对照的是宿主 0.2.2-rc.1 的 MessageSourceMap(dsh-api-session-controller)。
|
|
96
|
+
'working-directory', // 工作目录变了
|
|
97
|
+
'cordis-host-runner', // 插件运行器的转告
|
|
98
|
+
'ptc-mode', // 工具执行模式提示
|
|
99
|
+
'user-approval', // 审批策略变了
|
|
100
|
+
'compact-checkpoint', // 压缩检查点
|
|
101
|
+
'goal', // /goal 的目标
|
|
102
|
+
'team-message', // 队友之间的话
|
|
103
|
+
'agent-message', // 子代理、队友之间转的话(form 是 relay,见下)
|
|
104
|
+
// dsh-bili-humanism 给模型排的提示词(手账存完后的提问、人格审阅)。
|
|
105
|
+
// 它们不是 plugin: 前缀,所以要单列,否则写一篇手账就多一条「他来过」。
|
|
106
|
+
'bili-humanism-post-viewing-question',
|
|
107
|
+
'bili-humanism-personality-review',
|
|
108
|
+
'bili-humanism-shadow-review',
|
|
88
109
|
])
|
|
89
110
|
|
|
111
|
+
/**
|
|
112
|
+
* 宿主拼上下文时写在 source.form 上的形态(宿主前端的 KNOWN_FORMS)。
|
|
113
|
+
*
|
|
114
|
+
* 宿主给模型看的上下文(time-context、model-selection、tool-jobs、plan-mode、
|
|
115
|
+
* action-cards、agent-instructions、session-reference、tmux 快照……)kind 各不相同,
|
|
116
|
+
* 以后还会有新的,靠列 kind 总会漏;它们的共同点是都带这个字段。
|
|
117
|
+
* 漏一个的后果很重:time-context 最多十分钟一条,每条都是一次「他来过」、
|
|
118
|
+
* 一次 turns +1、一条写在他名下的 heard,满 rotateAfter 换房。
|
|
119
|
+
*
|
|
120
|
+
* relay 故意不在表里:外渠道转述人说的话,也可能自称 relay,那种要算人。
|
|
121
|
+
* 所以桥不要用这个表里的 form,用了就会被当上下文(AGENTS.md §7.2)。
|
|
122
|
+
*/
|
|
123
|
+
const MACHINE_FORMS = new Set(['snapshot', 'notice', 'catalog', 'instructions', 'recall'])
|
|
124
|
+
|
|
90
125
|
/** 一条 user 消息最多记这么多字(talkmem 会再截到 160,这里只是别让整段注入进内存) */
|
|
91
126
|
export const TURN_TEXT_MAX = 2000
|
|
92
127
|
/** 一轮里连着打好几句时,最多合并这么多条 */
|
|
@@ -122,6 +157,7 @@ export function classifyMessage(data) {
|
|
|
122
157
|
if (normalizeScene(src.scene) === 'group') return 'group'
|
|
123
158
|
// 宿主拼的上下文 / 定时消息 / 工具回灌 / 她自己说的话:都不是他
|
|
124
159
|
if (MACHINE_KINDS.has(kind) || kind.startsWith('plugin:')) return 'context'
|
|
160
|
+
if (MACHINE_FORMS.has(src.form)) return 'context'
|
|
125
161
|
const rid = String(src.rpcId ?? '')
|
|
126
162
|
if (rid && PLUGIN_RPC.test(rid)) return 'plugin'
|
|
127
163
|
if (kind === 'user') {
|
|
@@ -130,7 +166,7 @@ export function classifyMessage(data) {
|
|
|
130
166
|
if (src.clientTimeZone) return 'human'
|
|
131
167
|
return 'unknown'
|
|
132
168
|
}
|
|
133
|
-
//
|
|
169
|
+
// 两张表都没挡住的 kind = 外渠道(qq / telegram / 别的桥自己起的名字)。
|
|
134
170
|
// 宿主对 kind 只要求「非空且不是 'plugin'」,穷举不了,所以默认当人。
|
|
135
171
|
return 'human'
|
|
136
172
|
}
|
|
@@ -420,12 +456,18 @@ export function registerTurns(ctx, deps = {}) {
|
|
|
420
456
|
if (!(rotateAfter > 0) || turns < rotateAfter) return
|
|
421
457
|
|
|
422
458
|
// 该换会话了。换失败不算错:她还在旧会话里,下次再换。
|
|
459
|
+
// from:这一轮是从哪条会话里结出来的。`/say` 那边可能刚刚换过,
|
|
460
|
+
// Home 比对 home.json 之后会返回 skipped,这里就别再建第二条(0.8.8)。
|
|
423
461
|
const st = await homeStatus(root)
|
|
424
|
-
const rot = await rotateTalkSession(ctx, { root, wsId: st.wsId, log })
|
|
462
|
+
const rot = await rotateTalkSession(ctx, { root, wsId: st.wsId, log, from: p.sessionId })
|
|
425
463
|
if (!rot.ok) {
|
|
426
464
|
log(`turns · 聊满 ${turns} 轮,该换会话但没换成:${rot.why}`)
|
|
427
465
|
return
|
|
428
466
|
}
|
|
467
|
+
if (rot.skipped) {
|
|
468
|
+
log(`turns · 聊满 ${turns} 轮,但屋子已经先换过了(${rot.sessionId}),这条不再换`)
|
|
469
|
+
return
|
|
470
|
+
}
|
|
429
471
|
// 留一条 meta:换会话这件事要能在事件流里查到(观测台/排查用)。
|
|
430
472
|
// source 必须是 system——'tool' 会被 schema 拒掉。
|
|
431
473
|
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
|
/**
|
|
@@ -56,7 +64,10 @@ export function compareVersion(a, b) {
|
|
|
56
64
|
|
|
57
65
|
async function readJson(file) {
|
|
58
66
|
try {
|
|
59
|
-
|
|
67
|
+
// issue #38:手改过的 package.json / 配置文件可能带 UTF-8 BOM。
|
|
68
|
+
// 这里以前直接 parse,BOM 会让内容完好的文件读成 null(升级自检于是当作没装)。
|
|
69
|
+
const text = await readFile(file, 'utf8')
|
|
70
|
+
return JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text)
|
|
60
71
|
} catch {
|
|
61
72
|
return null
|
|
62
73
|
}
|
|
@@ -77,8 +88,11 @@ export function profileDirs({ home = homedir(), env = process.env } = {}) {
|
|
|
77
88
|
/** 从 profile 的 package.json 里取 `file:` 依赖指向的目录(绝对化)。 */
|
|
78
89
|
export function fileDepDir(profileDir, pkg) {
|
|
79
90
|
for (const field of DEP_FIELDS) {
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
// 两个键都看:只有 `file:` 的才算(`link:` 是源码直连,不走这套自检)
|
|
92
|
+
const spec = PKG_NAMES.map((n) => pkg?.[field]?.[n]).find(
|
|
93
|
+
(s) => typeof s === 'string' && s.startsWith('file:'),
|
|
94
|
+
)
|
|
95
|
+
if (typeof spec !== 'string') continue
|
|
82
96
|
const raw = spec.slice('file:'.length).trim()
|
|
83
97
|
if (!raw) return ''
|
|
84
98
|
// `file:D:/x`、`file:/x`、`file:../../x` 三种都要认。
|
|
@@ -118,7 +132,12 @@ export async function checkUpgrade({ version = '', pkgFile = '', profilesDir = '
|
|
|
118
132
|
if (!srcVersion) continue
|
|
119
133
|
// 这个 profile 装进去的那份是哪个版本?跟正在跑的相等,说明这条依赖就是「当前这条命」。
|
|
120
134
|
// 一台机器可能有 web / desktop 两个 profile,各自钉着不同的源目录,不能随手抓第一个。
|
|
121
|
-
|
|
135
|
+
// 名字不同,node_modules 里的目录也就不同(scoped 装法多一层 @ssamurias/)
|
|
136
|
+
let installed = ''
|
|
137
|
+
for (const n of PKG_NAMES) {
|
|
138
|
+
installed = String((await readJson(join(dir, 'node_modules', n, 'package.json')))?.version ?? '')
|
|
139
|
+
if (installed) break
|
|
140
|
+
}
|
|
122
141
|
hits.push({ dir, srcDir, srcVersion, installed })
|
|
123
142
|
}
|
|
124
143
|
|
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.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "养一条会自己长大的鱼:没有人格设置,通过事件流长大。她可以自己刷 B 站、会写日记给你看,四个阶段完成后才会开始有立场。状态库在 ~/.dsh-fish,不能删除记录。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
"cordis.patch.yml",
|
|
18
18
|
"README.md",
|
|
19
19
|
"AGENTS.md",
|
|
20
|
-
"CHANGELOG.md"
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"RELEASE-NOTES-*.md",
|
|
22
|
+
"RELEASE.md",
|
|
23
|
+
"scripts"
|
|
21
24
|
],
|
|
22
25
|
"scripts": {
|
|
23
26
|
"test": "node --test"
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 把老的 `dsh-fish` 安装收拾到新包名(@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
|
+
* 0.9.1 修(issue #43):原来只认 `link:` / `file:` 两种依赖写法。可是按更新说明用
|
|
11
|
+
* `dsh plugin add "@ssamurias/dsh-fish@0.9.0"` 升级的人,profile 里留下的是**版本号**,
|
|
12
|
+
* 脚本一句「没有老依赖,跳过」就退出 0 —— 旧键 `dsh-fish` 与旧 bundles 条目原封不动,
|
|
13
|
+
* 下次 pnpm 清理时两条 fish 打架。现在三种写法都收拾:源码目录、tgz、版本号/远端。
|
|
14
|
+
*
|
|
15
|
+
* 用法(先关掉 DeepSeek Harness):
|
|
16
|
+
* node scripts/fix-legacy-link.mjs # 预览
|
|
17
|
+
* node scripts/fix-legacy-link.mjs --write # 真的改
|
|
18
|
+
* node scripts/fix-legacy-link.mjs --write --profile C:\Users\你\.dsh\profiles\desktop
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs'
|
|
21
|
+
import path from 'node:path'
|
|
22
|
+
import os from 'node:os'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
|
|
25
|
+
export const NEW = '@ssamurias/dsh-fish'
|
|
26
|
+
export const LEGACY = 'dsh-fish'
|
|
27
|
+
|
|
28
|
+
const SPEC_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies']
|
|
29
|
+
|
|
30
|
+
export function readJson(p) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** profile 里那条老依赖:字段名 + 原样的 spec。没有就 null。 */
|
|
39
|
+
export function specOf(pkg) {
|
|
40
|
+
for (const field of SPEC_FIELDS) {
|
|
41
|
+
const spec = pkg?.[field]?.[LEGACY]
|
|
42
|
+
if (typeof spec === 'string' && spec.trim()) return { field, spec: spec.trim() }
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 这条 spec 指向什么?
|
|
49
|
+
* dir —— `link:` / `file:` 指向目录(源码目录安装),要建 junction
|
|
50
|
+
* tarball —— `file:` 指向 .tgz(0.8.x 的临时包装法),只清账、不建链接
|
|
51
|
+
* spec —— 版本号 / `^1.2.3` / `github:...`,只清账
|
|
52
|
+
*/
|
|
53
|
+
export function specKind(spec) {
|
|
54
|
+
const s = String(spec ?? '').trim()
|
|
55
|
+
if (!/^(link:|file:)/i.test(s)) return 'spec'
|
|
56
|
+
return /\.(tgz|tar\.gz)$/i.test(s) ? 'tarball' : 'dir'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function targetOf(profileDir, spec) {
|
|
60
|
+
const raw = String(spec).replace(/^(link:|file:)/i, '').trim()
|
|
61
|
+
if (!raw) return ''
|
|
62
|
+
if (/^[A-Za-z]:[\\/]/.test(raw) || path.isAbsolute(raw)) return path.resolve(raw)
|
|
63
|
+
return path.resolve(profileDir, raw)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function linkName(profileDir, name) {
|
|
67
|
+
return path.join(profileDir, 'node_modules', ...name.split('/'))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function bundlesOf(pkg) {
|
|
71
|
+
const prof = pkg?.dsh?.profile ?? (pkg?.dsh && Array.isArray(pkg.dsh.bundles) ? pkg.dsh : null)
|
|
72
|
+
return prof && Array.isArray(prof.bundles) ? prof.bundles : null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 把老依赖从依赖表里清掉;新包名已经在装的话顺手对齐 bundles。 */
|
|
76
|
+
export function cleanDepKey(pkg, found, newName) {
|
|
77
|
+
const done = []
|
|
78
|
+
delete pkg[found.field][LEGACY]
|
|
79
|
+
done.push(`删掉 ${found.field}["${LEGACY}"]`)
|
|
80
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }
|
|
81
|
+
if (deps[newName]) {
|
|
82
|
+
const bundles = bundlesOf(pkg)
|
|
83
|
+
if (bundles && !bundles.includes(newName)) {
|
|
84
|
+
bundles.push(newName)
|
|
85
|
+
done.push(`bundles 补上 ${newName}`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return done
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function dropLegacyBundle(pkg, newName) {
|
|
92
|
+
const bundles = bundlesOf(pkg)
|
|
93
|
+
if (!bundles) return []
|
|
94
|
+
let n = 0
|
|
95
|
+
for (let i = bundles.length - 1; i >= 0; i -= 1) {
|
|
96
|
+
if (bundles[i] === LEGACY) {
|
|
97
|
+
bundles.splice(i, 1)
|
|
98
|
+
n += 1
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const done = n ? [`bundles 删掉 ${LEGACY} ×${n}`] : []
|
|
102
|
+
if (newName && !bundles.includes(newName)) {
|
|
103
|
+
bundles.push(newName)
|
|
104
|
+
done.push(`bundles 补上 ${newName}`)
|
|
105
|
+
}
|
|
106
|
+
return done
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function retireOldLink(profileDir, tag, done = []) {
|
|
110
|
+
const oldLink = linkName(profileDir, LEGACY)
|
|
111
|
+
if (!fs.existsSync(oldLink)) return done
|
|
112
|
+
const off = `${oldLink}.disabled-${tag}`
|
|
113
|
+
try {
|
|
114
|
+
fs.renameSync(oldLink, off)
|
|
115
|
+
done.push(`node_modules/${LEGACY} 改名停用`)
|
|
116
|
+
} catch (e) {
|
|
117
|
+
done.push(`node_modules/${LEGACY} 改名失败:${e?.code ?? e?.message ?? e}`)
|
|
118
|
+
}
|
|
119
|
+
return done
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 收拾一个 profile。返回 { changed, done, notes }。
|
|
124
|
+
* `write=false` 时只算要做什么,不落盘。
|
|
125
|
+
*/
|
|
126
|
+
export function migrateOne(profileDir, { write = false, log = (s) => console.log(s), tag = '' } = {}) {
|
|
127
|
+
const pkgPath = path.join(profileDir, 'package.json')
|
|
128
|
+
const pkg = readJson(pkgPath)
|
|
129
|
+
const notes = []
|
|
130
|
+
const done = []
|
|
131
|
+
if (!pkg) {
|
|
132
|
+
log(`- ${profileDir}: 没有 package.json,跳过`)
|
|
133
|
+
return { changed: false, done, notes }
|
|
134
|
+
}
|
|
135
|
+
const found = specOf(pkg)
|
|
136
|
+
if (!found) {
|
|
137
|
+
log(`- ${profileDir}: 没有老的 ${LEGACY} 依赖,跳过`)
|
|
138
|
+
return { changed: false, done, notes }
|
|
139
|
+
}
|
|
140
|
+
const kind = specKind(found.spec)
|
|
141
|
+
const how = kind === 'dir' ? '源码目录' : kind === 'tarball' ? 'tgz 包' : '版本号/远端'
|
|
142
|
+
log(`\n== ${profileDir}`)
|
|
143
|
+
log(` 老依赖: ${found.field}["${LEGACY}"] = ${found.spec}(${how})`)
|
|
144
|
+
|
|
145
|
+
let newName = NEW
|
|
146
|
+
let target = ''
|
|
147
|
+
if (kind === 'dir') {
|
|
148
|
+
target = targetOf(profileDir, found.spec)
|
|
149
|
+
const srcPkg = readJson(path.join(target, 'package.json'))
|
|
150
|
+
const srcName = String(srcPkg?.name ?? '')
|
|
151
|
+
log(` 源目录: ${target}`)
|
|
152
|
+
log(` 源包名: ${srcName || '(读不到)'}`)
|
|
153
|
+
if (!srcName) notes.push('源目录读不到 package.json,这条按"只清账"处理')
|
|
154
|
+
else if (srcName !== LEGACY && srcName !== NEW) log(` ! 源包名是 ${srcName},脚本按它迁移`)
|
|
155
|
+
if (srcName && srcName !== LEGACY) newName = srcName
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!write) {
|
|
159
|
+
if (kind === 'dir' && target) {
|
|
160
|
+
log(` [预览] 会:建 node_modules/${newName} → 源目录;把依赖键改成 ${newName};删掉 ${LEGACY} 键与链接`)
|
|
161
|
+
} else {
|
|
162
|
+
log(` [预览] 会:删掉 ${found.field}["${LEGACY}"];bundles 里去掉 ${LEGACY};旧 node_modules/${LEGACY} 改名停用`)
|
|
163
|
+
}
|
|
164
|
+
return { changed: true, done, notes }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const bak = `${pkgPath}.bak-fishrename-${tag || 'now'}`
|
|
168
|
+
fs.copyFileSync(pkgPath, bak)
|
|
169
|
+
log(` 备份: ${bak}`)
|
|
170
|
+
|
|
171
|
+
if (kind === 'dir' && target) {
|
|
172
|
+
const newLink = linkName(profileDir, newName)
|
|
173
|
+
fs.mkdirSync(path.dirname(newLink), { recursive: true })
|
|
174
|
+
if (!fs.existsSync(newLink)) {
|
|
175
|
+
try {
|
|
176
|
+
fs.symlinkSync(target, newLink, 'junction')
|
|
177
|
+
done.push(`建链 node_modules/${newName} → ${target}`)
|
|
178
|
+
} catch (e) {
|
|
179
|
+
done.push(`建链失败:${e?.message ?? e}(可手动 New-Item -ItemType Junction)`)
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
done.push(`链接已存在:${newName}`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
delete pkg[found.field][LEGACY]
|
|
187
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) }
|
|
188
|
+
if (deps[newName]) {
|
|
189
|
+
// 新包名已经在依赖里(plugin add 装好了):只把 bundles 对齐,不动依赖表。
|
|
190
|
+
done.push(`删掉 ${found.field}["${LEGACY}"]`)
|
|
191
|
+
done.push(...dropLegacyBundle(pkg, newName))
|
|
192
|
+
} else if (kind === 'dir') {
|
|
193
|
+
// 源码目录:把依赖键改名指向同一条 spec。
|
|
194
|
+
pkg.dependencies = pkg.dependencies ?? {}
|
|
195
|
+
pkg.dependencies[newName] = found.spec
|
|
196
|
+
done.push(`依赖改为 ${found.field}["${newName}"]`)
|
|
197
|
+
const bundles = bundlesOf(pkg)
|
|
198
|
+
if (bundles) {
|
|
199
|
+
for (let i = 0; i < bundles.length; i += 1) if (bundles[i] === LEGACY) bundles[i] = newName
|
|
200
|
+
if (!bundles.includes(newName)) bundles.push(newName)
|
|
201
|
+
done.push(`bundles 对齐 ${newName}`)
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
done.push(`删掉 ${found.field}["${LEGACY}"]`)
|
|
205
|
+
done.push(...dropLegacyBundle(pkg, ''))
|
|
206
|
+
notes.push('新包名不在依赖里:只清账,装不装得上取决于这次 add 是否成功')
|
|
207
|
+
}
|
|
208
|
+
retireOldLink(profileDir, tag || 'now', done)
|
|
209
|
+
|
|
210
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
|
|
211
|
+
done.push('写入 package.json')
|
|
212
|
+
return { changed: true, done, notes }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function profileDirs(onlyProfile = '') {
|
|
216
|
+
if (onlyProfile) return [path.resolve(onlyProfile)]
|
|
217
|
+
const profilesRoot = path.join(os.homedir(), '.dsh', 'profiles')
|
|
218
|
+
try {
|
|
219
|
+
return fs
|
|
220
|
+
.readdirSync(profilesRoot, { withFileTypes: true })
|
|
221
|
+
.filter((d) => d.isDirectory())
|
|
222
|
+
.map((d) => path.join(profilesRoot, d.name))
|
|
223
|
+
.filter((p) => fs.existsSync(path.join(p, 'package.json')))
|
|
224
|
+
} catch {
|
|
225
|
+
return []
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function main() {
|
|
230
|
+
const args = process.argv.slice(2)
|
|
231
|
+
const write = args.includes('--write')
|
|
232
|
+
const pIdx = args.indexOf('--profile')
|
|
233
|
+
const onlyProfile = pIdx >= 0 ? args[pIdx + 1] : ''
|
|
234
|
+
const tag = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)
|
|
235
|
+
let touched = 0
|
|
236
|
+
for (const dir of profileDirs(onlyProfile)) {
|
|
237
|
+
const r = migrateOne(dir, { write, tag })
|
|
238
|
+
for (const n of r.notes) console.log(` 注: ${n}`)
|
|
239
|
+
for (const d of r.done) console.log(` · ${d}`)
|
|
240
|
+
if (write && r.changed) touched += 1
|
|
241
|
+
}
|
|
242
|
+
if (!write) console.log('\n这是预览。确认无误后加 --write 再跑一次(记得先关应用)。')
|
|
243
|
+
else console.log(`\n改完 ${touched} 个 profile。现在启动 DeepSeek Harness。`)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const invokedDirectly = (() => {
|
|
247
|
+
try {
|
|
248
|
+
return Boolean(process.argv[1]) && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
249
|
+
} catch {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
})()
|
|
253
|
+
|
|
254
|
+
if (invokedDirectly) main()
|