dsh-instruction-memory 1.4.3 → 1.4.4
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/CHANGELOG.md +31 -4
- package/README.md +6 -1
- package/install.mjs +186 -0
- package/lib/index.js +4718 -4681
- package/package.json +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@
|
|
|
7
7
|
> 1.0.13 起由 CI 经 OIDC Trusted Publishing 发布(首个带 provenance 的版本)。
|
|
8
8
|
> 这些历史 tag **不应回填**——npm 上的版本号已被占用。
|
|
9
9
|
|
|
10
|
+
## [1.4.4] — 2026-10-29
|
|
11
|
+
|
|
12
|
+
最新版审计(桌面端 0.2.1-rc.3、网页端同一宿主,并对照 npm `next` 上的 0.2.1-rc.4)发现的缺陷修复。
|
|
13
|
+
|
|
14
|
+
### 修复
|
|
15
|
+
|
|
16
|
+
- **注入字数上限真的会越界**:形象区块与条目区块各自预留"另有 N …"尾注空间,但都按 `footerFor(omitted + 1)` 预留、按 `footerFor(omitted)` 渲染——被省略的数量跨过一位数时就少留字符。实测 1.4.3:12 个形象 × 292 字符、上限 800,渲染出 **801** 字符。现在两侧都按"最坏情况(全部省略)"预留,形象区块还会先让出条目尾注所需的字符。
|
|
17
|
+
- **条目被形象挤空时尾注消失**:形象占满预算后条目区块文本为空,于是"另有 N 条指令未包含"永远不会出现,模型在毫无提示的情况下丢掉用户指令。现在会显式追加该尾注。
|
|
18
|
+
- **`injection.available` 恒为 `true`**:现在反映提示词区块是否真的注册成功,失败(`sectionError`)时为 `false`,与 `injection.error` 一致。
|
|
19
|
+
- **快照泄露绝对路径**:`storage.backupPath` / `storage.historyDir` / `storage.schemaFile` 之前直接给绝对路径,现在与 `storage.path` 一样经 `displayPath()` 把家目录折叠成 `~`(真实路径只用于读写,不对外暴露账号名)。
|
|
20
|
+
- **`install.mjs` 不在 npm 包里**:`files` 白名单缺它,而 README 让用户跑 `node install.mjs`。已加入。
|
|
21
|
+
|
|
22
|
+
### 兼容区间
|
|
23
|
+
|
|
24
|
+
- 尾部 `|| >=0.2.0-0` **没有上界**:`0.3.0`、`1.0.0`、`9.9.9` 都被判兼容。现收窄为 `>=0.2.0-0 <0.4.0-0`。
|
|
25
|
+
- 新增 `>=0.2.2-alpha.0 <0.3.0-0`:semver 只在同一 tuple 上匹配预发布号,`0.2.0` 那段够不到 `0.2.2-alpha.N`(npm `alpha` 标签当前即 `0.2.2-alpha.5`)。该线**只做过契约比对,尚未实机启动**。
|
|
26
|
+
- 三处声明(`peerDependencies` / `dsh.engines.dsh` / `dsh.compatibility.dsh`)仍由 `host-range-test.mjs` 生成并校验一致。
|
|
27
|
+
|
|
28
|
+
### 说明(未改动,但需要知道)
|
|
29
|
+
|
|
30
|
+
- 设置页依赖宿主的 `slots` 服务;`@deepseek-ai/cordis` 没有"可选注入",只能声明硬依赖。宿主不提供该服务时插件不装载设置页,只打印一行说明——平台要求,已写入 README。
|
|
31
|
+
- 插件 HTTP 路由在宿主网页端**没有鉴权**(README 早已写明)。桌面端同端口下 `/` 返回 401 而插件路由 200,这一层插件自己修不了,只能靠宿主或反向代理。
|
|
32
|
+
|
|
10
33
|
## [1.4.3] — 2026-10-27
|
|
11
34
|
|
|
12
35
|
技术方案评审团(3 名专家独立评审 + 逐条复现)列出的缺陷**全部修复**,并补上对应的回归断言。
|
|
@@ -63,10 +86,14 @@
|
|
|
63
86
|
|
|
64
87
|
### 测试
|
|
65
88
|
|
|
66
|
-
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
|
|
89
|
+
- `install.mjs` 补断言:真实子进程跑安装 / 卸载 / 幂等 / **拒绝半卸载**(链接是真实目录时不得
|
|
90
|
+
改 `package.json`)。
|
|
91
|
+
- 跨进程与备份保护补断言:真实两个 handler 实例模拟"另一个进程刚保存",以及三种不可用主文件
|
|
92
|
+
(缺失 / 空 / 非空但不可解析)下 `.bak` 的处置。
|
|
93
|
+
- 请求门禁(端口 / `same-site` / 尾点 / 重复 `Host` / MIME 精确匹配)、静音解析、用量条与预算
|
|
94
|
+
上限均补断言;预算不变量另用 3000 组随机存储形状做模糊验证(0 违例)。
|
|
95
|
+
- `npm test` 全套通过:**928 项断言,0 失败**。`lint.mjs` 增加管道子进程不可用时的
|
|
96
|
+
`stdio: inherit` 回退,使语法门禁在受限沙箱内也能给出真实结论(此前会被误报为全部解析失败)。
|
|
70
97
|
|
|
71
98
|
## [1.4.2] — 2026-10-24
|
|
72
99
|
|
package/README.md
CHANGED
|
@@ -215,6 +215,11 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。相对的 `$DS
|
|
|
215
215
|
> "没活干还在动"的副作用。现在文件由**第一个真正需要它的动作**创建:一次保存、一次导入,
|
|
216
216
|
> 或设置页读取 `state`(面板打开时就会读)。
|
|
217
217
|
|
|
218
|
+
> **快照里的路径是折叠过的**(1.4.4 起):`storage.path` / `backupPath` / `historyDir` / `schemaFile` 都把家目录折成 `~`,只用于显示;读写、备份、监听仍用绝对路径。
|
|
219
|
+
> `injection.available` 也不再恒为 `true`——提示词区块注册失败时它是 `false`。
|
|
220
|
+
> 设置页依赖宿主的 `slots` 服务:`@deepseek-ai/cordis` 没有"可选注入",所以这是硬依赖,
|
|
221
|
+
> 宿主不提供时插件不装载设置页(只打印一行说明)。
|
|
222
|
+
|
|
218
223
|
在这之前:`storage.path` 仍会显示路径,但 `storage.onDisk` 为 `false`,设置页显示
|
|
219
224
|
「尚未写入磁盘」(该文案本就存在),`state.injection.registered === false`,`pulls === 0`。
|
|
220
225
|
挂载日志相应为 `from=first run (no store on disk yet) onDisk=no`。
|
|
@@ -593,7 +598,7 @@ DNS 重绑定恰好推翻了它**:`evil.com` 解析到 `127.0.0.1` 之后,
|
|
|
593
598
|
同一串范围(三处逐字节一致,由测试锁定):
|
|
594
599
|
|
|
595
600
|
```
|
|
596
|
-
>=0.1.3-alpha.2 <0.1.4 || >=0.1.4-0 <0.1.5-0 || >=0.1.5-alpha.1 <0.1.6-0 || >=0.1.6-alpha.0 <0.1.7-0 || >=0.1.7-alpha.0 <0.2.0-0 || >=0.2.1-alpha.0 <0.3.0-0 || >=0.2.0-0
|
|
601
|
+
>=0.1.3-alpha.2 <0.1.4 || >=0.1.4-0 <0.1.5-0 || >=0.1.5-alpha.1 <0.1.6-0 || >=0.1.6-alpha.0 <0.1.7-0 || >=0.1.7-alpha.0 <0.2.0-0 || >=0.2.1-alpha.0 <0.3.0-0 || >=0.2.2-alpha.0 <0.3.0-0 || >=0.2.0-0 <0.4.0-0
|
|
597
602
|
```
|
|
598
603
|
|
|
599
604
|
**验证范围与证据等级**——三者不可混为一谈,下面每一行都标了它到底证明了什么:
|
package/install.mjs
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install / uninstall dsh-instruction-memory into the web profile.
|
|
3
|
+
*
|
|
4
|
+
* node install.mjs # install
|
|
5
|
+
* node install.mjs --remove # roll back completely
|
|
6
|
+
*
|
|
7
|
+
* Three idempotent steps:
|
|
8
|
+
* 1. make the package resolvable from the profile's node_modules (junction)
|
|
9
|
+
* 2. register it in `dsh.profile.bundles`, so the boot composes its
|
|
10
|
+
* cordis.patch.yml host row
|
|
11
|
+
* 3. declare it in the profile's `dependencies` as `link:<this checkout>`.
|
|
12
|
+
* A junction plus a bundle entry is enough for the HOST to load the
|
|
13
|
+
* plugin, but NOT enough to be seen by the market: dshmarket's installed
|
|
14
|
+
* list is built from the profile's `dependencies` only
|
|
15
|
+
* (`readInstalled` in dshmarket/src/profile.ts), so a plugin installed
|
|
16
|
+
* by steps 1-2 alone runs while showing up nowhere under 「已安装」.
|
|
17
|
+
*
|
|
18
|
+
* The profile package.json is backed up before the first modification.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync, symlinkSync, lstatSync, unlinkSync, copyFileSync } from 'node:fs'
|
|
21
|
+
import { homedir } from 'node:os'
|
|
22
|
+
import { dirname, join, resolve } from 'node:path'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
|
|
25
|
+
// Profile location follows the same precedence as @deepseek-ai/dsh-home-paths
|
|
26
|
+
// (and this plugin's own store resolution): explicit $DSH_HOME, else ~/.dsh.
|
|
27
|
+
// A relative override is resolved ONCE, for the same reason the plugin does it:
|
|
28
|
+
// every path this script prints and writes must be absolute.
|
|
29
|
+
const DSH_HOME = typeof process.env.DSH_HOME === 'string' && process.env.DSH_HOME.trim() !== ''
|
|
30
|
+
? resolve(process.env.DSH_HOME.trim())
|
|
31
|
+
: join(homedir(), '.dsh')
|
|
32
|
+
const PROF = join(DSH_HOME, 'profiles', 'web')
|
|
33
|
+
// This script lives in the package root, so the source is wherever it runs from.
|
|
34
|
+
const SRC = dirname(fileURLToPath(import.meta.url))
|
|
35
|
+
const NAME = 'dsh-instruction-memory'
|
|
36
|
+
|
|
37
|
+
const remove = process.argv.includes('--remove')
|
|
38
|
+
const link = join(PROF, 'node_modules', NAME)
|
|
39
|
+
const pkgPath = join(PROF, 'package.json')
|
|
40
|
+
const backupPath = pkgPath + '.dsh-instruction-memory.bak'
|
|
41
|
+
|
|
42
|
+
function readPkg() {
|
|
43
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writePkg(pkg) {
|
|
47
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function bundlesOf(pkg) {
|
|
51
|
+
pkg.dsh = pkg.dsh || {}
|
|
52
|
+
pkg.dsh.profile = pkg.dsh.profile || {}
|
|
53
|
+
pkg.dsh.profile.bundles = pkg.dsh.profile.bundles || []
|
|
54
|
+
return pkg.dsh.profile.bundles
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (remove) {
|
|
58
|
+
let touched = false
|
|
59
|
+
// `link` is normally a junction, and unlinking one removes only the link —
|
|
60
|
+
// the real package stays where it is. It can also be a REAL directory (pnpm
|
|
61
|
+
// materialises `link:` dependencies into node_modules as a copy), and there
|
|
62
|
+
// the old `rmSync(link, {recursive: true})` fallback deleted the installed
|
|
63
|
+
// copy itself: an uninstall that silently destroys the thing it was told to
|
|
64
|
+
// detach. Refuse that case and say what to do instead.
|
|
65
|
+
let linkKind = null
|
|
66
|
+
try {
|
|
67
|
+
linkKind = lstatSync(link).isSymbolicLink() ? 'link' : 'directory'
|
|
68
|
+
} catch {
|
|
69
|
+
linkKind = null
|
|
70
|
+
}
|
|
71
|
+
if (linkKind === 'link') {
|
|
72
|
+
// A junction can still refuse to go away (EBUSY/EPERM while DSH holds it
|
|
73
|
+
// open). The old call let that throw as an unhandled rejection, which read
|
|
74
|
+
// as a crash rather than "nothing was removed".
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(link)
|
|
77
|
+
console.log('removed junction: ' + link)
|
|
78
|
+
touched = true
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error('could not remove ' + link + ': ' + (err && err.code ? err.code : String(err)))
|
|
81
|
+
console.error(' close DSH (or any process using the plugin) and run this again.')
|
|
82
|
+
process.exitCode = 1
|
|
83
|
+
}
|
|
84
|
+
} else if (linkKind === 'directory') {
|
|
85
|
+
console.error('refusing to delete ' + link)
|
|
86
|
+
console.error(' it is a real directory (a pnpm-materialised copy), not a junction;')
|
|
87
|
+
console.error(' recursive deletion would remove the installed plugin itself.')
|
|
88
|
+
console.error(' delete it yourself once you have confirmed that is what you want.')
|
|
89
|
+
// Stop HERE: continuing would strip the dependency and the bundle entry
|
|
90
|
+
// while the plugin is still on disk, leaving a profile that reports the
|
|
91
|
+
// plugin as absent and an install that cannot find it either.
|
|
92
|
+
console.log('\nNOT UNINSTALLED — see the refusal above.')
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
if (process.exitCode) {
|
|
96
|
+
// The junction could not be removed; do not report the plugin as detached.
|
|
97
|
+
console.log('\nNOT UNINSTALLED — see the refusal above.')
|
|
98
|
+
process.exit(process.exitCode)
|
|
99
|
+
}
|
|
100
|
+
const pkg = readPkg()
|
|
101
|
+
let changed = false
|
|
102
|
+
if (pkg.dependencies !== undefined && Object.hasOwn(pkg.dependencies, NAME)) {
|
|
103
|
+
delete pkg.dependencies[NAME]
|
|
104
|
+
changed = true
|
|
105
|
+
console.log('removed from dependencies')
|
|
106
|
+
}
|
|
107
|
+
const bundles = pkg.dsh?.profile?.bundles
|
|
108
|
+
const index = Array.isArray(bundles) ? bundles.indexOf(NAME) : -1
|
|
109
|
+
if (index >= 0) {
|
|
110
|
+
bundles.splice(index, 1)
|
|
111
|
+
changed = true
|
|
112
|
+
console.log('removed from dsh.profile.bundles')
|
|
113
|
+
}
|
|
114
|
+
if (changed) {
|
|
115
|
+
writePkg(pkg)
|
|
116
|
+
touched = true
|
|
117
|
+
}
|
|
118
|
+
if (existsSync(backupPath)) console.log('backup kept at: ' + backupPath)
|
|
119
|
+
if (process.exitCode) {
|
|
120
|
+
console.log('\nNOT UNINSTALLED — see the refusal above.')
|
|
121
|
+
} else {
|
|
122
|
+
console.log(touched ? '\nUNINSTALLED — restart DSH to unload the plugin.' : '\nnothing to remove')
|
|
123
|
+
}
|
|
124
|
+
// Honour the failure flag the refused-deletion branch may have set: a bare
|
|
125
|
+
// process.exit(0) would report success for an uninstall that did nothing.
|
|
126
|
+
process.exit(process.exitCode ?? 0)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- install ----
|
|
130
|
+
if (!existsSync(SRC)) {
|
|
131
|
+
console.error('source package not found: ' + SRC)
|
|
132
|
+
process.exit(1)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!existsSync(join(PROF, 'package.json'))) {
|
|
136
|
+
console.error('web profile not found at ' + PROF + '(DSH 尚未初始化该 profile?)')
|
|
137
|
+
process.exit(1)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (existsSync(link)) {
|
|
141
|
+
console.log('junction already present: ' + link)
|
|
142
|
+
} else {
|
|
143
|
+
symlinkSync(SRC, link, 'junction')
|
|
144
|
+
console.log('junction created: ' + link)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Roll the previous backup forward instead of keeping the first one forever:
|
|
148
|
+
// a profile package.json broken by a later run is exactly what the user needs
|
|
149
|
+
// the backup for, and a backup frozen at the first install cannot restore it.
|
|
150
|
+
const lastBackupPath = backupPath + '.last'
|
|
151
|
+
if (existsSync(backupPath)) {
|
|
152
|
+
copyFileSync(backupPath, lastBackupPath)
|
|
153
|
+
console.log('rotated previous backup -> ' + lastBackupPath)
|
|
154
|
+
}
|
|
155
|
+
copyFileSync(pkgPath, backupPath)
|
|
156
|
+
console.log('backed up profile package.json -> ' + backupPath)
|
|
157
|
+
|
|
158
|
+
const pkg = readPkg()
|
|
159
|
+
let changed = false
|
|
160
|
+
|
|
161
|
+
// Step 3: a link: dependency, so the market lists it under 「已安装」.
|
|
162
|
+
// package.json only — the target is the junction from step 1, so no
|
|
163
|
+
// reinstall of the package itself is needed.
|
|
164
|
+
const spec = 'link:' + SRC.replaceAll('\\', '/')
|
|
165
|
+
pkg.dependencies = pkg.dependencies || {}
|
|
166
|
+
if (pkg.dependencies[NAME] === spec) {
|
|
167
|
+
console.log('already a profile dependency: ' + spec)
|
|
168
|
+
} else {
|
|
169
|
+
pkg.dependencies[NAME] = spec
|
|
170
|
+
changed = true
|
|
171
|
+
console.log('declared as a profile dependency: ' + spec)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const bundles = bundlesOf(pkg)
|
|
175
|
+
if (bundles.includes(NAME)) {
|
|
176
|
+
console.log('already in dsh.profile.bundles')
|
|
177
|
+
} else {
|
|
178
|
+
bundles.push(NAME)
|
|
179
|
+
changed = true
|
|
180
|
+
console.log('added to dsh.profile.bundles (position ' + bundles.length + ')')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (changed) writePkg(pkg)
|
|
184
|
+
|
|
185
|
+
console.log('\nINSTALLED. Restart DSH to mount the plugin.')
|
|
186
|
+
console.log('Roll back with: node install.mjs --remove')
|