@ilaomianyang/dsh-memory-graph 0.10.4 → 0.12.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.
- package/CHANGELOG.md +40 -0
- package/README.en.md +73 -12
- package/README.md +73 -12
- package/client.js +433 -1
- package/docs/update-settings.png +0 -0
- package/index.js +42 -3
- package/package.json +5 -3
- package/src/auto.js +78 -14
- package/src/capture.js +37 -175
- package/src/config.js +37 -0
- package/src/distillapi.js +4 -1
- package/src/distillrun.js +7 -3
- package/src/extract.js +42 -18
- package/src/graph.js +10 -5
- package/src/graphdata.js +10 -1
- package/src/history.js +467 -0
- package/src/pageapi.js +2 -2
- package/src/schema.js +55 -0
- package/src/tools.js +76 -2
- package/src/turns.js +265 -0
- package/src/update.js +406 -0
- package/src/vault.js +21 -11
- package/test/auto.mjs +68 -0
- package/test/client.mjs +15 -5
- package/test/fixtures/session.v4.jsonl.zstd +0 -0
- package/test/history.mjs +249 -0
- package/test/tools.mjs +2 -1
- package/test/update.mjs +401 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0 — 2026-11-03
|
|
4
|
+
|
|
5
|
+
装完之后就不用再管它了:插件自己去问有没有新版本,积压的记忆自己一批批追平,视图跟着库走。
|
|
6
|
+
|
|
7
|
+
**Added**
|
|
8
|
+
|
|
9
|
+
- **自我更新**。设置页最上面一行「版本与更新」:当前版本、检查更新、立即更新。启动时查一次,之后每 `update.checkEveryHours` 小时(默认 12)最多一次——一次 `GET <registry>/@ilaomianyang%2fdsh-memory-graph/latest`,不带查询串、不带记忆库路径、不带任何对话内容;结果缓存进库里的 `.update-state.json`,离线时保留上一次的答案,不会把"查不到"显示成"已是最新"。
|
|
10
|
+
- 安装走宿主自己的 `pluginManager.installBundle`(和 `dsh plugin` 同一条 pnpm 路径,尊重已配的 registry),装完照实说「需要重启 Harness 生效」还是「已生效」。**`link:` / `file:` 本地安装一律拒绝**,并说明会覆盖开发目录——这句是拒绝,不是报错。
|
|
11
|
+
- `update.autoInstall`(默认关):启动时发现新版本就自己装。它在任何对话开始之前动手,是唯一"换掉插件不会打断你正在说的话"的时刻;`link:` 安装同样拒绝。`memory_stats` 也报一行版本。
|
|
12
|
+
- 新路由 `/api/memory-graph.update`:GET 读状态(`current/latest/behind/canInstall/installKind/busy/message`),POST 是唯一能装的方法,所以页面加载或预取永远不会误装。
|
|
13
|
+
- **追平模式**(`auto.catchUp`,默认关)。原来的两条规则一次只消化一批,两千条积压要等几十天;打开追平后,只要还攒着**一整批**(≥ `maxEpisodesPerRun`)就按自己的节奏(`catchUpEveryMinutes`,默认 10 分钟)一批批蒸下去,一天最多 `auto.maxBatchesPerDay` 批(默认 6,0 = 不限),追平就自己停。每日批次计数存进 `.auto-state.json` 的 `batchDay`/`batchesToday`,重启不重置;批次在调用**之前**就计数,失败也算用掉一个名额——上限才是真的上限。静默时段压过追平。
|
|
14
|
+
- `memory_backfill` 写完后顺手重画 `README.md` 和 `graph.html`(报告里 `refreshed`),导入两千条情节后打开视图看到的是当下的库,不是导入前的空图。
|
|
15
|
+
- `test/update.mjs` 27 项 + `test/auto.mjs` 加 6 项追平检查。检查数 244 → 277。
|
|
16
|
+
|
|
17
|
+
**Changed**
|
|
18
|
+
|
|
19
|
+
- 设置页多了 7 个旋钮:`auto.catchUp` / `auto.catchUpEveryMinutes` / `auto.maxBatchesPerDay`,以及新的「版本与更新」分区(`update.enabled` / `checkEveryHours` / `autoInstall` / `registry`)。版本那一行不是 config 字段,是这一块自己 GET/POST 出来的 UI,刻意不占 `mg-card` / `mg-row` 这些类名。
|
|
20
|
+
- `README.md` / `README.en.md`:「自动蒸馏」从两条规则写成三条,新增「它自己更新」一节,配置表 34 → 41 行,YAML 例子带上 `auto.catchUp` 与 `update`。
|
|
21
|
+
|
|
22
|
+
## 0.11.0 — 2026-11-03
|
|
23
|
+
|
|
24
|
+
历史会话也能进库了,宿主侧的文案统一成中文。
|
|
25
|
+
|
|
26
|
+
**Added**
|
|
27
|
+
|
|
28
|
+
- `memory_backfill`:把**插件装上之前**的会话历史一次性导入记忆库。采集只看得见宿主运行时发生的对话,所以新库一开始是空的;这个工具回放磁盘上的 `~/.dsh/sessions/<项目>/<会话>/session.v4.jsonl.zstd`,走**与实时采集完全相同**的过滤与脱敏(`src/turns.js` 是唯一实现),因此回填出来的情节和实时采集的是同一种记录。时间戳取自会话文件里那一轮,情节落进**它当天**的 `episodes/YYYY-MM-DD.jsonl`,不是全堆在今天。默认 `dryRun`,只统计不写库。
|
|
29
|
+
- 幂等:一轮对话的身份是 `(会话, 轮次)`,库里已有就跳过,所以还在长的会话下次再跑只补新的部分;删档重装、换机器、装晚了都是同一条命令。
|
|
30
|
+
- 实测(本机):133 个会话 → 2078 条情节,85 条已有跳过,0 坏行、0 读不了,17 秒。
|
|
31
|
+
- `test/history.mjs` 12 项 + `test/fixtures/session.v4.jsonl.zstd`(真的 zstd 帧、真的 v4 事件形状)。检查数 232 → 244。
|
|
32
|
+
|
|
33
|
+
**Fixed**
|
|
34
|
+
|
|
35
|
+
- 会话文件是**拼接的 zstd 容器**(每次追加写一帧),而 `zstdDecompressSync` 和 fzstd 都只解第一帧——按流读只会拿到第一行会话头。`src/history.js` 现在带一个帧扫描器(和 harness 自己用的算法同构:帧头 → 3 字节块头 → 可选校验和),逐帧解码;Node ≥ 22.15 用原生 zstd(全保真),更老的 Node 退回 fzstd,报告里会写明用的是哪个解码器。单帧坏掉只丢那一批,不丢整个会话。
|
|
36
|
+
- `vault.appendEpisode` 的日期改为取记录自己的 `t`,不再一律写今天——否则回填三个月历史会让 `episodes/` 看起来全发生在今天(实测 27 个日期文件)。
|
|
37
|
+
|
|
38
|
+
**Changed**
|
|
39
|
+
|
|
40
|
+
- 宿主侧的用户可见文本统一成中文(英文保留在后,形如「中文 · English」):抽取器/蒸馏的失败说明、路由错误体、失败页兜底、审计行、自动蒸馏的日志、图谱页的按钮与 tooltip、`confidence` 徽章的三种取值。共 45 条 / 10 个文件。`autoDecision` 的 reason 码、枚举码、发给模型的 `SYSTEM_PROMPT` 保持英文。
|
|
41
|
+
- `src/graphdata.js` 的 400 响应补了 `message`,页签不再把机器码 `missing-session` 直接印出来(`reason` 未动)。
|
|
42
|
+
|
|
3
43
|
## 0.10.4 — 2026-11-03
|
|
4
44
|
|
|
5
45
|
**Changed**
|
package/README.en.md
CHANGED
|
@@ -96,6 +96,23 @@ After the restart it is **Settings → Memory Graph**, between Models and Plugin
|
|
|
96
96
|
|
|
97
97
|
Uninstall with `plugin_manager action=remove_bundle target="@ilaomianyang/dsh-memory-graph"`. The vault is not touched — it lives in `~/dsh-memory`, independent of the plugin.
|
|
98
98
|
|
|
99
|
+
## It updates itself
|
|
100
|
+
|
|
101
|
+
The copy installed from npm gets a **Version & updates** row at the top of its Settings page: the version you are running, **Check**, and **Update**.
|
|
102
|
+
|
|
103
|
+

|
|
104
|
+
|
|
105
|
+
That is the real page (on a development machine the install is a `link:`, so Update is greyed out and says why next to it): the version, whether it is current, which registry, when it last checked — and the four `update.*` switches below.
|
|
106
|
+
|
|
107
|
+
- It checks once at startup, then at most every `update.checkEveryHours` hours (default 12). The request is one `GET` of `<registry>/@ilaomianyang%2fdsh-memory-graph/latest` — no query string, no vault path, no conversation content. The answer is cached in the vault's `.update-state.json`, so an offline machine keeps the last answer instead of showing "could not check" as "up to date".
|
|
108
|
+
- Nothing is installed until you press Update. Installation goes through the host's own `pluginManager.installBundle` — the same path `dsh plugin` and the plugin manager use — so it respects your configured registry instead of inventing a second package manager.
|
|
109
|
+
- **A `link:` install is refused**, and the refusal is a sentence rather than an error: "updating would overwrite your working copy". For a clone, updating is `git pull` and a reload, as above.
|
|
110
|
+
- An install usually needs a Harness restart (same caching reason as `client.js` / `package.json`). The page says **restart Harness to apply** rather than pretending it is already running the new code.
|
|
111
|
+
- `update.autoInstall` (off by default) installs a newer version at startup without being asked. It acts before any conversation starts, which is the one moment swapping the plugin cannot interrupt something you were saying. A `link:` install is still refused.
|
|
112
|
+
- `memory_stats` reports the version too, so the model does not have to guess which copy it is talking to.
|
|
113
|
+
|
|
114
|
+
Turn `update.enabled` off to stop it going online entirely: after that it sends nothing, and the page only shows the current version.
|
|
115
|
+
|
|
99
116
|
## What you get on disk
|
|
100
117
|
|
|
101
118
|
```
|
|
@@ -145,6 +162,13 @@ One file is one memory. Append-only. No binaries, no lockfiles. Run `git init` i
|
|
|
145
162
|
retrieval:
|
|
146
163
|
limit: 8
|
|
147
164
|
hops: 1
|
|
165
|
+
auto:
|
|
166
|
+
enabled: true
|
|
167
|
+
catchUp: true # drain a backlog, one batch at a time
|
|
168
|
+
maxBatchesPerDay: 6
|
|
169
|
+
update:
|
|
170
|
+
enabled: true # check for a newer version at startup
|
|
171
|
+
autoInstall: false
|
|
148
172
|
```
|
|
149
173
|
|
|
150
174
|
| Key | Default | Meaning | On the page |
|
|
@@ -183,37 +207,52 @@ One file is one memory. Append-only. No binaries, no lockfiles. Run `git init` i
|
|
|
183
207
|
| `auto.minGapMinutes` | `30` | shared by both rules: minimum gap between two automatic runs | ✓ |
|
|
184
208
|
| `auto.quietHours` | `''` | window in which nothing runs (e.g. `23:00-08:00`, may cross midnight) | ✓ |
|
|
185
209
|
| `auto.project` | `''` | project automatic cards are filed under | ✓ |
|
|
210
|
+
| `auto.catchUp` | `false` | catch-up: keep running batches while a full batch is still waiting | ✓ |
|
|
211
|
+
| `auto.catchUpEveryMinutes` | `10` | minutes between catch-up batches (its own cadence) | ✓ |
|
|
212
|
+
| `auto.maxBatchesPerDay` | `6` | most automatic batches per day, 0 lifts the cap; the one ceiling on automatic spending | ✓ |
|
|
213
|
+
| `update.enabled` | `true` | ask npm for a newer version regularly (checks only, nothing about the vault leaves) | ✓ |
|
|
214
|
+
| `update.checkEveryHours` | `12` | how long a check result stays good for | ✓ |
|
|
215
|
+
| `update.autoInstall` | `false` | install a newer version at startup (a `link:` install is refused) | ✓ |
|
|
216
|
+
| `update.registry` | `''` | which npm registry to ask; empty = the harness's, then npmjs | ✓ |
|
|
186
217
|
| `retrieval.limit` | `8` | how many cards one search returns | ✓ |
|
|
187
218
|
| `retrieval.hops` | `1` | entity expansion depth, `0` = keyword only | ✓ |
|
|
188
219
|
| `retrieval.snippetChars` | `320` | how much of each hit the model gets to read | ✓ |
|
|
189
220
|
| `retrieval.timelineLimit` | `20` | how far back `memory_timeline` looks by default | ✓ |
|
|
190
221
|
|
|
191
|
-
Why only those
|
|
222
|
+
Why only those 41 are editable there: the harness projects **only fields declared `volatile`** — the ones read each time they are used. Everything marked ✓ is read at call time, so one save changes the next call. `enabled` / `vaultDir` / `defaultProject` / `crossProject` are consumed during activation and would need a remount, so they are left out rather than shown as knobs that do nothing; the page's footer says exactly that.
|
|
192
223
|
|
|
193
|
-
### When it distils on its own:
|
|
224
|
+
### When it distils on its own: three rules
|
|
194
225
|
|
|
195
226
|
By default it does **not**. With `auto.enabled` on, the plugin looks once a minute at whether a run is due — and that look is arithmetic only (how many episodes are waiting, how long since the last attempt, is this a quiet hour). **No model call, no cost.** Only a rule that matches starts a run, and it runs the same `distill()`.
|
|
196
227
|
|
|
197
|
-
The
|
|
228
|
+
The three rules are independent: any one alone, or all of them.
|
|
198
229
|
|
|
199
230
|
| Rule | Setting | Fires when |
|
|
200
231
|
| --- | --- | --- |
|
|
201
232
|
| Distil once enough pile up | `auto.minEpisodes` | undistilled episodes ≥ this number (say 10) |
|
|
202
233
|
| Distil on a schedule | `auto.everyMinutes` | this many minutes have passed since the last automatic run (1440 = daily) |
|
|
234
|
+
| Catch up | `auto.catchUp` | a **full batch** is still waiting (≥ `maxEpisodesPerRun`): run batch after batch, `auto.catchUpEveryMinutes` apart (default 10), at most `auto.maxBatchesPerDay` batches a day (default 6, 0 lifts the cap) |
|
|
235
|
+
|
|
236
|
+
Catch-up is for a **backlog** — an imported history, a week away from the machine, two thousand undistilled episodes. The other two rules digest one batch per trigger and would take dozens of days; catch-up keeps going until less than a batch is left, then goes quiet.
|
|
237
|
+
|
|
238
|
+
- It has **its own cadence** (`catchUpEveryMinutes`) and is not held back by `minGapMinutes`; the other two rules still are.
|
|
239
|
+
- `maxBatchesPerDay` is the only knob that puts a **ceiling on automatic spending**: the default six batches is about 240 episodes a day. Raise it to drain faster, or set `quietHours` so it does not run at night.
|
|
240
|
+
- An interrupted catch-up (restart, shutdown, another run holding the lock) loses nothing: the counter lives in `.auto-state.json`, the next interval continues, and nothing already distilled is distilled twice.
|
|
241
|
+
- 2080 episodes at the defaults: six batches a day × 40 episodes ≈ nine days. Set the cap to 50 to finish in a day.
|
|
203
242
|
|
|
204
|
-
Three gates
|
|
243
|
+
Three gates the rules share:
|
|
205
244
|
|
|
206
|
-
- `auto.minGapMinutes` (default 30):
|
|
207
|
-
- `auto.quietHours` (default empty): nothing runs inside this window; it may cross midnight (`23:00-08:00`). Unreadable text is ignored rather than silencing the feature forever.
|
|
245
|
+
- `auto.minGapMinutes` (default 30): the two **non-catch-up** rules never fire closer than this. It is checked before them — a vault that just crossed the threshold should not be asked again a minute later, and every ask costs money.
|
|
246
|
+
- `auto.quietHours` (default empty): nothing runs inside this window; it may cross midnight (`23:00-08:00`). Unreadable text is ignored rather than silencing the feature forever. Catch-up obeys it too.
|
|
208
247
|
- `auto.project` (default empty): which project automatic cards are filed under; empty uses the newest episode's project.
|
|
209
248
|
|
|
210
249
|
A few deliberate choices:
|
|
211
250
|
|
|
212
251
|
- **The schedule clock starts when you switch it on**: it does not run on the spot — a daily schedule should not spend the moment you flip the switch. That clock is stored separately (`startedAt`) and is not treated as "the last run", so it does not push the threshold rule back by one minimum gap either.
|
|
213
|
-
- **The gap is armed before the call**, so a run that produced nothing still counts and will not be re-asked the next minute.
|
|
252
|
+
- **The gap is armed before the call**, so a run that produced nothing still counts and will not be re-asked the next minute. The catch-up counter works the same way: it is incremented before the call, so a failed batch still spends its slot and the daily cap stays a real cap. Both live in `.auto-state.json` in the vault (`lastAttemptAt`, `startedAt`, `batchDay`, `batchesToday`), so **a restart does not reset them**.
|
|
214
253
|
- That file is not `.distill-state.json`: the latter is the **cursor** (which lines have been read), the former is the **attempt clock**. Merging them would make "read, but produced nothing" impossible to express.
|
|
215
254
|
- Automatic runs, the button and `memory_distill` share **one lock**: while a run is in flight, the other callers are told so rather than queued, so two of them can never spend at once.
|
|
216
|
-
- To stop it: `auto.enabled = false` (master switch), or set
|
|
255
|
+
- To stop it: `auto.enabled = false` (master switch), or set the rules to 0 (the switch stays on, nothing matches, nothing runs).
|
|
217
256
|
|
|
218
257
|

|
|
219
258
|
|
|
@@ -267,11 +306,32 @@ Point 3 means that if the key is already configured for the harness (`DEEPSEEK_A
|
|
|
267
306
|
| `memory_forget` | `retract` (keeps the trace) or `purge` (deletes the file) |
|
|
268
307
|
| `memory_distill` | turn raw episodes into cards with the extractor model |
|
|
269
308
|
| `memory_stats` | vault location, counts, last distillation |
|
|
309
|
+
| `memory_backfill` | import the sessions from **before the plugin was installed** (re-runnable; known turns are skipped) |
|
|
270
310
|
|
|
271
311
|
`memory_graph` is the "neuron" part made concrete: an edge is not invented, it is **two cards mentioning the same entity**. It claims co-mention, never causation — the model decides what that means.
|
|
272
312
|
|
|
273
313
|
**Nothing is injected automatically.** Memory only enters the context when the model asks for it. Most turns do not need long-term memory, and injecting it would be pure cost.
|
|
274
314
|
|
|
315
|
+
## Past sessions count too
|
|
316
|
+
|
|
317
|
+
Capture only sees conversations that happen **while the host is running**, so a freshly installed vault starts empty: the dozens of sessions before it will never appear on their own. `memory_backfill` fills that gap by replaying the session files on disk (`~/.dsh/sessions/<project>/<session>/session.v4.jsonl.zstd`) into episodes of **the same shape** live capture writes.
|
|
318
|
+
|
|
319
|
+
- **Same filter, one implementation**: `<system-reminder>` blocks stripped, runtime snapshots (runtime context, compaction checkpoints) never counted as something a person said, length caps, redaction — all shared with live capture through `src/turns.js`, so the two paths cannot quietly diverge.
|
|
320
|
+
- **The original timestamp**: `t` comes from the turn in the log, and the episode lands in **its own** `episodes/YYYY-MM-DD.jsonl` rather than piling up under today.
|
|
321
|
+
- **Safe to run twice**: a turn is identified by `(session, turn)` and skipped when the vault already has it, so re-running after a session grew imports only the new part. Reinstalls, new machines and late installs are all the same command.
|
|
322
|
+
- **Dry run by default** (`dryRun`): see the numbers before anything is written.
|
|
323
|
+
|
|
324
|
+
In one line:
|
|
325
|
+
|
|
326
|
+
```text
|
|
327
|
+
memory_backfill {dryRun: true} → 133 sessions scanned / 2078 episodes would be written
|
|
328
|
+
memory_backfill {dryRun: false} → 2078 episodes written; 85 already present, skipped
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
What lands is **episodes**, not cards: distil once (`memory_distill`, the Settings button, or the daily auto run at 40 episodes a batch) and they enter the retrieval layer. Two thousand episodes is fifty model batches — that is a spend worth deciding on deliberately, and if you would rather have it drain itself, switch on `auto.catchUp`: it runs at most `auto.maxBatchesPerDay` batches a day and stops when it has caught up. A backfill also re-renders `README.md` and `graph.html` when it finishes, so those two files show the vault as it is now rather than as it was before the import.
|
|
332
|
+
|
|
333
|
+
A session file is a **concatenated zstd container** (one frame per appended batch), so reading it means cutting it frame by frame — `src/history.js` carries a scanner equivalent to the one the harness itself uses.
|
|
334
|
+
|
|
275
335
|
## Look at your memory
|
|
276
336
|
|
|
277
337
|
`memory_stats` tells you where the vault is; in its root there is also **`graph.html`** — double-click it, nothing to install, no server:
|
|
@@ -383,7 +443,7 @@ Side benefit: an injected message no longer cuts a turn in half, so an episode's
|
|
|
383
443
|
No network, no real vault:
|
|
384
444
|
|
|
385
445
|
```bash
|
|
386
|
-
npm test # 43 + 13 + 35 + 12 + 22 + 54 + 15 + 8 + 11 +
|
|
446
|
+
npm test # 43 + 13 + 35 + 12 + 22 + 54 + 15 + 8 + 11 + 25 + 12 + 27 = 277 checks, plus the 25-question retrieval eval
|
|
387
447
|
npm run eval # retrieval eval only (offline, deterministic)
|
|
388
448
|
```
|
|
389
449
|
|
|
@@ -392,10 +452,11 @@ npm run eval # retrieval eval only (offline, deterministic)
|
|
|
392
452
|
- `test/client.mjs` — mounts the page on a minimal React stand-in: the controls drawn equal the fields that should show, switching `transport` reveals the http knobs, an empty inline key is never sent as a deletion, an out-of-range number never reaches the wire, a refused save clears the drafts and explains itself, restore sends `unset` rather than `set`, a value that merely repeats the inherited default is not badged as a customisation, the stylesheet is a single `<style>` tagged with the package name, and `client.js` contains no `export`/`import` (the loader evaluates it as a script). It then evaluates `client.js` in a fake page carrying `window.__ModuleLoader__` and walks the real browser path — factory, `apply`, both slot registrations — checking that `react` is the only module required, the registration id is the package name, the orders are 25 and 30, and the labels follow the page language. Finally it drives the **Memory tab**: one request per paint carrying this `sessionId`, every node and edge the host sent drawn, a click opening the card's title and body in the side panel, and both the empty session and the failing route explaining themselves instead of going blank. It also checks the **sidebar entry + main panel**: one rail entry and one page keyed by the same id, the action bar a sibling of the scroll box rather than a sticky child of it, the panel embedding the served route instead of a second renderer, and no vault path in the `src`
|
|
393
453
|
- `test/graphdata.mjs` — the Memory tab's data: episodes filtered by `session`, majority project, the recall query taking the newest turns, a deterministic layout inside the box, every edge pointing at a node that exists, a cross-project card arriving through a shared entity, the caps reporting that they held, no assistant text in the payload, and the route answering 400 / 200 / 500 for missing session, normal, and a broken vault
|
|
394
454
|
- `test/capture.mjs` — drives the capture listener with real `session/event` shapes, including injected context
|
|
395
|
-
- `test/tools.mjs` — runs `execute` + `render` for all
|
|
455
|
+
- `test/tools.mjs` — runs `execute` + `render` for all nine tools
|
|
396
456
|
- `test/page.mjs` — the route behind the sidebar panel: `/api/memory-graph.page` sits on the authenticated channel, an empty vault still answers with a page, it is **the same renderer as the `graph.html` on disk** (only the timestamp differs), `X-Frame-Options: SAMEORIGIN` so only the app can frame it, a broken vault prints the reason on the page (escaped), no vault at all is a 500 rather than a crash, and no vault path ever appears in the bytes
|
|
397
457
|
- `test/distillapi.mjs` — the Distill now door: `/api/memory-graph.distill` on the authenticated `/api` channel, running the same `distill()` (live vault, the model selected in Settings, the live redaction rules), one press is one run, a concurrent second press is refused instead of queued, an extractor refusal is a 200 and not a 500, a throw is a 500 with the lock still released, views are re-rendered only when cards were written, a render failure only warns, the card list is capped at 40, and a route with no runner answers 500 instead of throwing at the browser
|
|
398
|
-
- `test/auto.mjs` — the automatic distiller: each rule fires on its own, whichever matches first runs when both are on, the minimum gap is checked before the rules, a quiet window may cross midnight, a tick only decides (an off switch does not even read the vault), one tick is at most one run, the gap is armed before the call, a restart reads `.auto-state.json` instead of starting over, a run that wrote nothing still arms the gap, an in-flight run is not stacked on, failures are logged with their reason, the project reaches the run, an unreadable vault is a skip and not a crash, and the tick asks for enough episodes to see the threshold
|
|
458
|
+
- `test/auto.mjs` — the automatic distiller: each rule fires on its own, whichever matches first runs when both are on, the minimum gap is checked before the rules, a quiet window may cross midnight, a tick only decides (an off switch does not even read the vault), one tick is at most one run, the gap is armed before the call, a restart reads `.auto-state.json` instead of starting over, a run that wrote nothing still arms the gap, an in-flight run is not stacked on, failures are logged with their reason, the project reaches the run, an unreadable vault is a skip and not a crash, and the tick asks for enough episodes to see the threshold; catch-up gets its own group: less than a full batch is not a backlog, it keeps its own ten-minute cadence instead of the minimum gap, it stops at the daily cap and starts again the next day
|
|
459
|
+
- `test/update.mjs` — self-update: version comparison (a prerelease sorts below its release, a `v` prefix is ignored), a `link:`/`file:` install is recognised as a working copy and therefore refuses to update itself, the registry version and the status line in prose, one registry request inside the window, `force` does not re-ask within a minute, the cache expires on its own, an outage keeps the last answer instead of claiming "up to date", turning checks off sends nothing, a configured registry wins over the host's, an unusable plugin manager does not stop the check, installing asks for the exact version through `installBundle`, applied / restart-required / failed each read differently, a second install while one runs is refused rather than queued, `autoInstall` acts only when allowed and only on a registry install, and the route layer: GET is uncached, POST is the only verb that can install, no checker is a 503
|
|
399
460
|
- `test/extract.mjs` — the extractor: `fetch` stubbed for the http route (prompt construction, key resolution order, tolerant JSON parsing, per-card project attribution, cursor advance, dry run writing nothing, secrets stopped before the write), then a fake `llm` service for the harness route (borrows the Settings model and thinking level, honours an explicit override, explains a missing selection, handles every terminal `finish`, reports cancellation, rejects an empty completion, and never touches `fetch`), and finally batching: the batch size derived from the output budget, a long backlog split across calls, a truncated batch halved and retried, earlier batches kept when a later one fails, and `dryRun` previewing only the first batch
|
|
400
461
|
|
|
401
462
|
Two suites do touch the network (and cost money), so they are not in `npm test`:
|
|
@@ -422,7 +483,7 @@ The tag triggers `.github/workflows/publish.yml`, which does four things:
|
|
|
422
483
|
|
|
423
484
|
1. checks that the tag and `package.json` agree — a mismatch fails instead of publishing a package whose version lies;
|
|
424
485
|
2. checks whether that version is already on the registry — if so it skips, so re-running a tag is a no-op rather than an error;
|
|
425
|
-
3. `npm publish --provenance --access public` (`prepublishOnly` runs the
|
|
486
|
+
3. `npm publish --provenance --access public` (`prepublishOnly` runs the 277 checks again; a red build cannot reach the registry);
|
|
426
487
|
4. adds "Built and signed on GitHub Actions" to the npm page, proving the tarball came from this commit of this repository.
|
|
427
488
|
|
|
428
489
|
Two ways to authenticate; the workflow accepts both (it uses `NPM_TOKEN` when the secret exists and falls back to OIDC when it does not):
|
package/README.md
CHANGED
|
@@ -90,6 +90,23 @@ plugin_manager action=install_bundle target="<clone 下来的绝对路径>"
|
|
|
90
90
|
|
|
91
91
|
重启后打开 **设置 → 记忆图谱**(在「模型」和「内置插件」之间)。卸载:`plugin_manager action=remove_bundle target="@ilaomianyang/dsh-memory-graph"`。**记忆库不会被删**,它在 `~/dsh-memory`,独立于插件。
|
|
92
92
|
|
|
93
|
+
## 它自己更新
|
|
94
|
+
|
|
95
|
+
从 npm 装的那份,设置页最上面有「版本与更新」一行:当前版本号、**检查更新**、**立即更新**。
|
|
96
|
+
|
|
97
|
+

|
|
98
|
+
|
|
99
|
+
上面这张是真机(开发机上是 `link:` 安装,所以「更新」是灰的,旁边就写着原因):版本号、是否最新、用的哪个源、上次检查时间,下面四个开关是 `update.*`。
|
|
100
|
+
|
|
101
|
+
- 插件启动时自己查一次,之后每 `update.checkEveryHours` 小时(默认 12)最多查一次。查的是 `<注册表>/@ilaomianyang%2fdsh-memory-graph/latest`——一次 GET,不带查询串、不带你的记忆库路径、不带任何对话内容。结果记在库里的 `.update-state.json`,离线时保留上一次的答案,不会把"查不到"显示成"已是最新"。
|
|
102
|
+
- 点「立即更新」才装。安装走宿主自己的 `pluginManager.installBundle`,和 `dsh plugin` / 插件管理里那条路是同一条,尊重你配的 registry,不再造第二个包管理器。
|
|
103
|
+
- **`link:` 本地安装会拒绝更新**,不是报错,是明说:"这会覆盖你的开发目录"。从源码装的那份,更新就是 `git pull` + 重载插件,见上一节。
|
|
104
|
+
- 装完通常要重启 Harness 才生效(和 `client.js` / `package.json` 同一类原因)。页面会直接写「需要重启 Harness 生效」,不会假装已经好了。
|
|
105
|
+
- `update.autoInstall`(默认关):启动时发现新版本就自己装,不用点。它在任何对话开始之前动手,是唯一"换掉插件不会打断你正在说的话"的时刻。`link:` 安装同样拒绝。
|
|
106
|
+
- `memory_stats` 也会报一行版本,模型想知道自己手上是哪一版时不用猜。
|
|
107
|
+
|
|
108
|
+
不想让它联网就把 `update.enabled` 关掉:那之后一次请求都不发,页面只显示当前版本。
|
|
109
|
+
|
|
93
110
|
## 你的记忆库长什么样
|
|
94
111
|
|
|
95
112
|
```
|
|
@@ -137,6 +154,13 @@ plugin_manager action=install_bundle target="<clone 下来的绝对路径>"
|
|
|
137
154
|
retrieval:
|
|
138
155
|
limit: 8
|
|
139
156
|
hops: 1
|
|
157
|
+
auto:
|
|
158
|
+
enabled: true
|
|
159
|
+
catchUp: true # 有积压就一批批追平(见下)
|
|
160
|
+
maxBatchesPerDay: 6
|
|
161
|
+
update:
|
|
162
|
+
enabled: true # 启动时查一次有没有新版本
|
|
163
|
+
autoInstall: false
|
|
140
164
|
```
|
|
141
165
|
|
|
142
166
|
| 键 | 默认 | 说明 | 在设置页 |
|
|
@@ -175,37 +199,52 @@ plugin_manager action=install_bundle target="<clone 下来的绝对路径>"
|
|
|
175
199
|
| `auto.minGapMinutes` | `30` | 两条规则共用:两次自动蒸馏至少隔多久 | ✓ |
|
|
176
200
|
| `auto.quietHours` | `''` | 不跑的时间段(如 `23:00-08:00`,可跨午夜) | ✓ |
|
|
177
201
|
| `auto.project` | `''` | 自动蒸馏出的卡片记到哪个项目 | ✓ |
|
|
202
|
+
| `auto.catchUp` | `false` | 追平模式:还攒着一整批就一批批蒸下去,直到追平 | ✓ |
|
|
203
|
+
| `auto.catchUpEveryMinutes` | `10` | 追平模式每批之间隔多少分钟(自成一套节奏) | ✓ |
|
|
204
|
+
| `auto.maxBatchesPerDay` | `6` | 一天最多自动蒸几批,0 = 不限;这是自动花钱的唯一上限 | ✓ |
|
|
205
|
+
| `update.enabled` | `true` | 定期问 npm 有没有新版本(只查,不装,不带任何记忆库信息) | ✓ |
|
|
206
|
+
| `update.checkEveryHours` | `12` | 查到的结果保持多久,期间不重复请求 | ✓ |
|
|
207
|
+
| `update.autoInstall` | `false` | 启动时发现新版本就自己装(`link:` 安装会拒绝) | ✓ |
|
|
208
|
+
| `update.registry` | `''` | 用哪个 npm 源查;空 = 宿主的源,再退到 npmjs | ✓ |
|
|
178
209
|
| `retrieval.limit` | `8` | 一次检索最多返回几张卡片 | ✓ |
|
|
179
210
|
| `retrieval.hops` | `1` | 联想扩散跳数,0 = 只做关键词 | ✓ |
|
|
180
211
|
| `retrieval.snippetChars` | `320` | 每条结果给模型看多少字摘要 | ✓ |
|
|
181
212
|
| `retrieval.timelineLimit` | `20` | `memory_timeline` 默认回看多少条 | ✓ |
|
|
182
213
|
|
|
183
|
-
为什么只有这
|
|
214
|
+
为什么只有这 41 个能在这页改?Harness 的设置机制只投影**声明为 volatile 的字段**,也就是"每次用到时才读"的那些。上面带 ✓ 的都是这种:改一次,下一次调用就换了。而 `enabled` / `vaultDir` / `defaultProject` / `crossProject` 是在激活时消费的,改了得重载插件,所以不放进页面装样子——页面底部会直接把这件事写出来,而不是给你几个点了没反应的开关。
|
|
184
215
|
|
|
185
|
-
###
|
|
216
|
+
### 什么时候它会自己蒸:三条规则
|
|
186
217
|
|
|
187
218
|
默认**不自动**。打开 `auto.enabled` 之后,插件每分钟看一次「现在该不该蒸」——这一眼只做算术(未蒸馏条数、距上次多久、是不是静默时段),**不调模型、不花钱**。命中规则才真的跑一次,跑的还是同一个 `distill()`。
|
|
188
219
|
|
|
189
|
-
|
|
220
|
+
三条规则互相独立,可以只开一条,也可以都开:
|
|
190
221
|
|
|
191
222
|
| 规则 | 配置 | 什么时候触发 |
|
|
192
223
|
| --- | --- | --- |
|
|
193
224
|
| 攒够就蒸 | `auto.minEpisodes` | 未蒸馏情节 ≥ 这个数(比如 10 条) |
|
|
194
225
|
| 定时蒸 | `auto.everyMinutes` | 距上次自动蒸馏 ≥ 这么多分钟(1440 = 每天一次) |
|
|
226
|
+
| 追平 | `auto.catchUp` | 还攒着**一整批**(≥ `maxEpisodesPerRun` 条)时就一批批蒸下去,每批之间隔 `auto.catchUpEveryMinutes`(默认 10),一天最多 `auto.maxBatchesPerDay` 批(默认 6,0 = 不限) |
|
|
227
|
+
|
|
228
|
+
「追平」是给**积压**用的:导入了历史会话、或者一周没开机回来,未蒸馏的可能有两千条——前两条规则一次只消化一批,靠它们要等几十天。打开追平,它会自己一批批往前推,直到剩下不到一整批为止,然后安安静静等下一次。
|
|
229
|
+
|
|
230
|
+
- 它**自己**有节奏(`catchUpEveryMinutes`),不受 `minGapMinutes` 限制;另外两条规则仍然受。
|
|
231
|
+
- `maxBatchesPerDay` 是唯一给自动花钱**设上限**的旋钮:默认 6 批 ≈ 240 条情节/天。想快点追平就调大,不想它半夜跑就把 `quietHours` 设上。
|
|
232
|
+
- 追平跑到一半被打断(重启、关机、锁被别人拿着)没关系:计数在 `.auto-state.json` 里,第二天(或下一个间隔)继续,已经蒸过的不会重蒸。
|
|
233
|
+
- 2080 条积压 + 默认配置:6 批/天 × 40 条 ≈ 9 天追平;想一天追完就把上限调到 50。
|
|
195
234
|
|
|
196
|
-
|
|
235
|
+
另外三个闸门,规则之间共用:
|
|
197
236
|
|
|
198
|
-
- `auto.minGapMinutes`(默认 30
|
|
199
|
-
- `auto.quietHours`(默认空):这段时间里不跑,可以跨午夜(`23:00-08:00
|
|
237
|
+
- `auto.minGapMinutes`(默认 30):两条**非追平**规则共用:两次自动蒸馏至少隔这么久。它先于那两条规则判断——刚攒够就被反复问是没意义的,而且每次问都要花钱。
|
|
238
|
+
- `auto.quietHours`(默认空):这段时间里不跑,可以跨午夜(`23:00-08:00`)。写不对就当没写,不会因此永远不跑。追平也听它。
|
|
200
239
|
- `auto.project`(默认空):自动蒸出来的卡片记到哪个项目;留空用最近那条情节的项目。
|
|
201
240
|
|
|
202
241
|
几个刻意的选择:
|
|
203
242
|
|
|
204
243
|
- **定时蒸的时钟从打开那一刻算起**:刚打开不会立刻跑一次——「每天一次」不该在你按下开关的瞬间就花掉一次调用。时钟单独存(`startedAt`),不当作"上次运行",所以它不会顺带把「攒够就蒸」推迟一个最短间隔。
|
|
205
|
-
- **间隔在调用之前就记下**,所以一次跑空了(没有新卡片)也算数,不会因为"没产出"
|
|
244
|
+
- **间隔在调用之前就记下**,所以一次跑空了(没有新卡片)也算数,不会因为"没产出"而在下一分钟再问一遍。追平的批次计数同理:调用之前就 +1,失败也算用掉一个名额——上限才是真的上限。记录在库里的 `.auto-state.json`(`lastAttemptAt` / `startedAt` / `batchDay` / `batchesToday`),**重启不重置**。
|
|
206
245
|
- 它和 `.distill-state.json` 是两件事:后者是**游标**(哪些行已经读过),前者是**尝试时间**。混在一起会让"读过了但没产出"变得无法表达。
|
|
207
246
|
- 自动、按钮、`memory_distill` 共用**一把锁**:正在跑的时候,第二条路拿到的是"已经有一次在跑",不会排队、不会两份一起花。
|
|
208
|
-
- 想关掉:`auto.enabled=false
|
|
247
|
+
- 想关掉:`auto.enabled=false`(总开关),或者把规则都设 0(留着开关但没规则,等于不跑)。
|
|
209
248
|
|
|
210
249
|

|
|
211
250
|
|
|
@@ -259,11 +298,32 @@ plugin_manager action=install_bundle target="<clone 下来的绝对路径>"
|
|
|
259
298
|
| `memory_forget` | `retract` 撤回(留痕)或 `purge` 删除 |
|
|
260
299
|
| `memory_distill` | 用抽取模型把原始事件蒸馏成卡片 |
|
|
261
300
|
| `memory_stats` | 库位置、数量、上次蒸馏时间 |
|
|
301
|
+
| `memory_backfill` | 把**插件装上之前**的会话历史导入记忆库(可重复跑,已有轮次跳过) |
|
|
262
302
|
|
|
263
303
|
`memory_graph` 是"神经元"那部分的具体形态:边不是凭空生成的,而是**两张卡片提到同一个实体**。它不声称因果,只声称"这两个东西在同一段记忆里出现过",模型看到之后自己判断。
|
|
264
304
|
|
|
265
305
|
**没有自动注入。** 记忆只在模型主动调用工具时才进入上下文——这是刻意的:大多数轮次根本不需要长期记忆,注入了就是纯成本。
|
|
266
306
|
|
|
307
|
+
## 历史会话也能进
|
|
308
|
+
|
|
309
|
+
采集只看得见**宿主开着的时候**发生的对话,所以新装的库一开始是空的——装之前的几十个会话不会自己冒出来。`memory_backfill` 补这一段:它回放磁盘上的会话文件(`~/.dsh/sessions/<项目目录>/<会话>/session.v4.jsonl.zstd`),转成和实时采集**同样格式**的情节。
|
|
310
|
+
|
|
311
|
+
- **走的还是那套过滤**:`<system-reminder>` 剥掉、运行时快照(runtime context、压缩检查点)不算人话、限长、脱敏,全部复用 `src/turns.js`——实时和回填只有一份实现,不会两边慢慢走偏。
|
|
312
|
+
- **时间是原来的时间**:`t` 取自会话文件里那一轮的时间戳,情节落进它**当天**的 `episodes/YYYY-MM-DD.jsonl`,不是全堆在今天。
|
|
313
|
+
- **可以重复跑**:一轮对话的唯一身份是 `(会话, 轮次)`,库里已经有了就跳过;所以一个还在长的会话,下次再跑只补新的部分。删档重装、换机器、插件装晚了,都是这一条命令的事。
|
|
314
|
+
- **默认只统计不写库**(`dryRun`),先看数字再决定。
|
|
315
|
+
|
|
316
|
+
一句话的形状:
|
|
317
|
+
|
|
318
|
+
```text
|
|
319
|
+
memory_backfill {dryRun: true} → 133 个会话 / 将写入 2078 条情节
|
|
320
|
+
memory_backfill {dryRun: false} → 已写入 2078 条情节;已存在跳过 85 条
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
回填出来的是**情节**,还没变成卡片:蒸馏一次(`memory_distill`、设置页按钮,或让自动蒸馏一天跑一批,默认每批 40 条)才会进检索层。2000 条情节要花的就是 50 批模型调用,这件事该由你决定什么时候花——想让它自己慢慢消化完,打开 `auto.catchUp`(追平模式):它一天最多跑 `auto.maxBatchesPerDay` 批,追平就自己停。回填结束时会顺手把 `README.md` 和 `graph.html` 按新情节重画一次,所以打开这两个文件看到的是当下的库,不是导入前的样子。
|
|
324
|
+
|
|
325
|
+
会话文件是**拼接的 zstd 容器**(每次追加写一帧),所以读它要先把帧切开——`src/history.js` 里有一个和 harness 自己用的算法同构的帧扫描器。
|
|
326
|
+
|
|
267
327
|
## 看一眼你的记忆
|
|
268
328
|
|
|
269
329
|
`memory_stats` 会告诉你库在哪;库里根目录还有一个 **`graph.html`**,双击就能开,不用装东西、不用起服务:
|
|
@@ -376,7 +436,7 @@ Harness 会把运行时快照、技能目录、时间戳、审批通知、压缩
|
|
|
376
436
|
零依赖,不联网,不碰真实记忆库:
|
|
377
437
|
|
|
378
438
|
```bash
|
|
379
|
-
npm test # 43 + 13 + 35 + 12 + 22 + 54 + 15 + 8 + 11 +
|
|
439
|
+
npm test # 43 + 13 + 35 + 12 + 22 + 54 + 15 + 8 + 11 + 25 + 12 + 27 = 277 项检查,另加 25 题检索评测
|
|
380
440
|
npm run eval # 只跑检索评测(离线,确定性的)
|
|
381
441
|
```
|
|
382
442
|
|
|
@@ -385,10 +445,11 @@ npm run eval # 只跑检索评测(离线,确定性的)
|
|
|
385
445
|
- `test/client.mjs` — 把设置页挂在一个够用的 React 替身上跑:页面画出的控件 = 该显示的字段、切换 `transport` 后 http 那几项才出现、空的内联 key 不会被当成"删除"发出去、越界的数字到不了线上、宿主拒绝后清空草稿并报错、「恢复默认」发的是 `unset` 而不是 `set`、只在真的和继承值不同时才打「已自定义」(同一次保存会钉住整行,值等于默认的不算改过)、样式表只有一个带包名标记的 `<style>`、`client.js` 里不许出现 `export`/`import`(loader 按脚本求值);再把 `client.js` 真的放进一个带 `window.__ModuleLoader__` 的假页面里求值,走一遍 factory → `apply` 的浏览器路径(只 `require('react')`、注册 id 是包名、四处 order 分别 25 / 30 / 30、标签跟随页面语言、动作栏是滚动区的兄弟而不是它里面的 sticky 子元素);再验证**侧栏入口 + 主面板**(`sidebar.panellist` 与 `main` 用同一个 id 才连得上、面板里嵌的是那条路由而不是第二套渲染、src 里不带库路径);最后驱动**会话页签**:只按 `sessionId` 请求 `/api/memory-graph.graph`、宿主给的节点和边一个不少地画出来、点卡片在右侧展开标题与正文、点空白收起、空会话和失败的路由都给人话
|
|
386
446
|
- `test/graphdata.mjs` — 会话页签的数据:按 `session` 过滤情节(别的会话一条都不带)、项目多数决、召回查询取最近几轮并截断、布局确定且不越界、每条边都指向存在的节点、跨项目卡片通过共享实体挂进来、上限与截断照实报、assistant 原文不进 payload;路由对"缺 session / 正常 / 库坏掉"三种情况分别回 400 / 200 / 500
|
|
387
447
|
- `test/capture.mjs` — 用真实的 `session/event` 形状驱动采集,验证一轮一情节、密钥被拦、短轮不入库、注入消息不入库
|
|
388
|
-
- `test/tools.mjs` — 逐个跑
|
|
448
|
+
- `test/tools.mjs` — 逐个跑 9 个工具的 `execute` + `render`;另外验证模型要的那次蒸馏走的是共享 runner(而不是绕开它直接调 `distill`),否则它可能和一次自动蒸馏撞在一起
|
|
389
449
|
- `test/page.mjs` — 侧栏面板背后那条路由:`/api/memory-graph.page` 在受认证通道上、空库也给一页而不是报错、**和落到磁盘的 `graph.html` 是同一份渲染**(只差时间戳)、`X-Frame-Options: SAMEORIGIN` 所以只有应用能嵌、库读坏了把原因印在页面上(转义后)、没有库是 500 不是崩溃、页面里不带任何库路径
|
|
390
450
|
- `test/distillapi.mjs` — 「蒸馏一次」那条路:`/api/memory-graph.distill` 在受认证的 `/api` 通道上、跑的是同一个 `distill()`(活库 + 设置里的模型 + 打码规则)、一次按下一次运行、并发第二次被拒(不排队)、抽取器的拒绝是 200 而不是 500、抛错是 500 且锁照样解开、写了卡片才重建视图、渲染失败只 warn、卡片列表截到 40、没有 runner 时回 500 而不是把异常扔给浏览器
|
|
391
|
-
- `test/auto.mjs` —
|
|
451
|
+
- `test/auto.mjs` — 自动蒸馏:三条规则各自触发、都开时谁先满足谁先跑、最短间隔先于两条非追平规则判断、静默时段跨午夜且压过追平、tick 只做决定(关着时连库都不读)、一次 tick 最多一次运行、间隔在调用前就记下、重启后读回 `.auto-state.json` 所以不会重跑(追平的每日批次计数也一样读回来)、跑空了也记间隔、正在跑时不叠加、失败照原因记日志、项目名透传、库读不了是跳过不是崩、阈值大时请求的条数够看到它;追平单独一组:不足一整批不算积压、按自己的节奏(10 分钟)而不是 30 分钟的间隔、到每日上限就停、第二天重置
|
|
452
|
+
- `test/update.mjs` — 自我更新:版本比较(预发布低于正式、`v` 前缀)、`link:`/`file:` 安装被识别成"工作副本"因而拒绝更新、注册表版本与状态行的中文说法、窗口内只问一次注册表、`force` 一分钟内不重复请求、缓存到期自己失效、离线时保留上一次答案而不是显示"已是最新"、关掉检查就一次请求都不发、配置的源优先于宿主的源、宿主没有 pluginManager 也不影响检查、安装只走 `installBundle` 且要的是确切版本、装成功/需重启/失败各自的说法、正在装的时候第二次拒绝而不是排队、`autoInstall` 只在允许且是注册表安装时动手、路由层 GET 不缓存 / POST 是唯一能装的方法 / 没有 checker 时 503
|
|
392
453
|
- `test/extract.mjs` — 抽取器:`fetch` 打桩验证 http 路线(提示词构造、key 解析顺序、JSON 解析容错、每张卡片归到自己的项目、游标前进、dry run 不落盘、密钥在写入前被拦住),再用一个假的 `llm` 服务验证 harness 路线(借用设置里的模型与思考档、显式覆盖、没有选中模型时的报错、`finish` 各种终态、取消、空回复、以及全程不碰 `fetch`),最后验证分批:按输出上限折算每批条数、长后台分多次调用、被截断就对半拆、某一批失败时前面几批的卡片留着、`dryRun` 只预览第一批
|
|
393
454
|
|
|
394
455
|
下面两个测试会真的联网/花钱,所以不在 `npm test` 里,要自己跑:
|
|
@@ -415,7 +476,7 @@ npm version minor -m "chore(release): v%s" && git push --follow-tags # minor /
|
|
|
415
476
|
|
|
416
477
|
1. 核对 tag 和 `package.json` 的版本号——不一致就失败,不会发出一个版本号对不上的包;
|
|
417
478
|
2. 查这个版本是不是已经在注册表上——在就跳过,重推同一个 tag 是空操作而不是报错;
|
|
418
|
-
3. `npm publish --provenance --access public`(`prepublishOnly` 会再跑一遍那
|
|
479
|
+
3. `npm publish --provenance --access public`(`prepublishOnly` 会再跑一遍那 277 项,红的进不去);
|
|
419
480
|
4. npm 页面上多一条「Built and signed on GitHub Actions」,证明这个 tarball 是从这个仓库的这个提交构建的。
|
|
420
481
|
|
|
421
482
|
凭据有两条路,工作流两条都认(有 `NPM_TOKEN` 就用它,没有就走 OIDC):
|