dsh-ebbing-context 0.2.7 → 0.2.9
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 +0 -0
- package/README.en.md +100 -0
- package/README.md +5 -3
- package/client.js +210 -70
- package/index.js +10 -6
- package/lib/blocks.js +1 -1
- package/lib/compress.js +11 -4
- package/lib/map.js +23 -11
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
Binary file
|
package/README.en.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Ebbing Context
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Light-EI/dsh-ebbing-context/actions/workflows/test.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
[中文](README.md) | **English**
|
|
7
|
+
|
|
8
|
+
**A layered context-compaction plugin for DSH that simulates memory decay.**
|
|
9
|
+
|
|
10
|
+
The model decides where to compress and writes the summaries itself; the plugin only writes session events the host already understands, degrading old content step by step into coarser representations:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
raw text (verbatim) → L1 block (conclusions and data of a span) → L2 block → L3 block (keywords and sentences only)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The older it is, the coarser it gets — but **nothing is ever deleted**. The raw text stays in the session log, and with the "session search" plugin installed it can be retrieved by event number.
|
|
17
|
+
|
|
18
|
+
`ebbing` means "fading away": whether to compress, and how far, is the model's own decision under context pressure — not a scheduled job.
|
|
19
|
+
|
|
20
|
+
> **Status**: feature-complete, all tests green, and compaction has run on a real machine. What is missing is a **reproducible verification record** — the maps, logs and before/after comparisons were not archived. Target host: DSH 0.2.1-rc.3 ~ 0.2.2-alpha.2. Back up your profile first and try it in a fresh session.
|
|
21
|
+
> Per-version changes: [CHANGELOG](CHANGELOG.md).
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
- **Two tools + one command**: `context_map` (layer structure and usage), `compress` (perform one compaction), `/context-status` (read-only, same map).
|
|
28
|
+
- **Key name = the layer to compress**: `l0` compresses raw text → L1 blocks; `l1` compresses L1 → L2; `l2` compresses L2 → L3; `l3` folds the whole L3 region into one block (the only key without `to`).
|
|
29
|
+
- **`to` must be copied from the map**: `l0` takes a turn number (`T` + the real turn from the map); `l1`/`l2` take the 1-based block index within that region. A wrong value is rejected, and the rejection carries a refreshed map.
|
|
30
|
+
- **Cascade**: when compressing a deeper layer, every shallower layer that can still be compressed must be given in the same batch, otherwise the whole batch is rejected (the prefix cache is invalidated only once).
|
|
31
|
+
- **Zero ledger**: no database, no state files; layers are recomputed from the session surface every turn, and a block's identity is its first line `〔L1|T16–T19|ev 4121–4390〕title`.
|
|
32
|
+
- **Reminds, never acts**: when usage hits the line, grows a lot, or a layer exceeds its quota, it wakes the model at the end of the turn with a fresh map. It never compresses by itself and never pushes you toward `/compact`.
|
|
33
|
+
|
|
34
|
+
The full design (layer model, cascade, protection boundaries, configuration, host contracts, limitations) is in [docs/SPEC.md](https://github.com/Light-EI/dsh-ebbing-context/blob/main/docs/SPEC.md) (currently in Chinese).
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
> Published on npm: `dsh plugin add dsh-ebbing-context` (or `npm install dsh-ebbing-context`). The two methods below still work.
|
|
41
|
+
|
|
42
|
+
**Method 1: from npm (recommended)**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
dsh plugin add dsh-ebbing-context
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Method 2: from a source checkout**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/Light-EI/dsh-ebbing-context.git
|
|
52
|
+
dsh plugin add ./dsh-ebbing-context
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Method 3: mount into the profile manually**
|
|
56
|
+
|
|
57
|
+
1. Put the repository contents into the profile's `node_modules/dsh-ebbing-context/`
|
|
58
|
+
2. Add a `dsh-ebbing-context:` line to the profile's `cordis.patch.yml` (path rules are in [docs/SPEC.md](https://github.com/Light-EI/dsh-ebbing-context/blob/main/docs/SPEC.md), section "directory and development")
|
|
59
|
+
3. Restart DSH
|
|
60
|
+
|
|
61
|
+
After installing, confirm three things: the plugin card shows **running**, both `context_map` / `compress` tools are present, and `/context-status` renders the map.
|
|
62
|
+
|
|
63
|
+
**Uninstall** (`dsh plugin remove dsh-ebbing-context` leaves two more steps):
|
|
64
|
+
|
|
65
|
+
1. Open the profile's `cordis.patch.yml` and delete the `dsh-ebbing-context` section (including any `config` you changed in the panel). If you leave it, every start prints a warning on stderr about a patch pointing at a missing entry — harmless, but it keeps printing.
|
|
66
|
+
2. The log lives at `$DSH_HOME/logs/ebbing-context.log` (plus `.1` / `.2` when rotated). Uninstalling does not delete it; remove it manually once you no longer need it for troubleshooting.
|
|
67
|
+
**Recommended: turn off the host's built-in auto-compaction** (`compaction-basic`'s `auto`, triggered at 80% by default). Once it fires it re-flattens the layered summaries and the layered structure is gone (the original text is still in the session log and can be read back by event number). The plugin still compresses, just with a discount. Where to turn it off depends on your install method and preset.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
17 items (`triggerPct`, `keepTurns`, `pruneFirst`, `foldCallRecord`, `requireSessionQuery`, `reservedTokens`, `budget.*`, `segment.*`, `title.maxTokens`, `notice.*`), edited in Settings → Ebbing Context (the dedicated settings page) or the plugin row's detail page; changes take effect only after you click "Save".
|
|
74
|
+
|
|
75
|
+
Defaults, meanings and rejection rules are in [docs/SPEC.md section 6](https://github.com/Light-EI/dsh-ebbing-context/blob/main/docs/SPEC.md#6-配置17-项) (Chinese).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Dependencies and boundaries
|
|
80
|
+
|
|
81
|
+
- Requires the host to provide `agent/pre-step`, `agent/turn-stopping`, `agent/followup` and the session-surface API.
|
|
82
|
+
- By default it requires a "session search" tool (`session_event_read` / `session_event_search`); if the raw text cannot be retrieved, compression is disabled.
|
|
83
|
+
- It only performs paired replacements or edits the body of a `tool/result`; it never deletes a single node.
|
|
84
|
+
- No network, no file reads, no telemetry.
|
|
85
|
+
|
|
86
|
+
## Known limitations
|
|
87
|
+
|
|
88
|
+
- Parallel tool calls are not folded (and their results are not rewritten); the summary body stays in the context as an argument (the plugin may suggest sending it separately later).
|
|
89
|
+
- Folding happens only before the next request goes out; a call that misses that window is not folded, and its record stays as is.
|
|
90
|
+
- Images cannot be retrieved; they are counted by number only.
|
|
91
|
+
- If the turn outline is corrupt (the log is missing a `turn/start` or out of order), that turn reports a red error and is disabled.
|
|
92
|
+
- Region usage is self-estimated by the plugin (about 0.6 token per Chinese character, 0.3 per English character); it tracks the host's meter closely but can differ by a few percent.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT.
|
|
97
|
+
|
|
98
|
+
## Contributing
|
|
99
|
+
|
|
100
|
+
Issues / PRs are welcome. Before you start, read the "host contracts" section of [docs/SPEC.md](https://github.com/Light-EI/dsh-ebbing-context/blob/main/docs/SPEC.md); those contracts are the precondition for correctness.
|
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](https://github.com/Light-EI/dsh-ebbing-context/actions/workflows/test.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
|
+
**中文** | [English](README.en.md)
|
|
7
|
+
|
|
6
8
|
**模拟记忆衰退的 DSH 分层上下文压缩插件。**
|
|
7
9
|
|
|
8
10
|
模型自己决定压哪里、自己写摘要;插件只写宿主本来就认识的会话事件,把旧内容一层层降成更粗的表示:
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
|
|
25
27
|
- **两个工具 + 一条命令**:`context_map`(分层结构与占用)、`compress`(执行一次压缩)、`/context-status`(只读,同地图)。
|
|
26
28
|
- **键名 = 要压的那一层**:`l0` 压原文 → L1 块;`l1` 压 L1 → L2;`l2` 压 L2 → L3;`l3` 把整个 L3 区压成一块(唯一不写 `to` 的键)。
|
|
27
|
-
- **`to` 只能从地图上抄**:`l0` 写轮号(`
|
|
29
|
+
- **`to` 只能从地图上抄**:`l0` 写轮号(`T` + 地图里的真实轮号),`l1`/`l2` 写该区从 1 起的块序号。写错就被拒,拒绝回执里带刷新后的地图。
|
|
28
30
|
- **级联**:压深层时,比它浅且还能压的层必须同批给完,否则整批拒绝(只断一次前缀缓存)。
|
|
29
31
|
- **零台账**:不建库、不写状态文件;层级每轮从会话表层现算,块的身份写在正文第一行 `〔L1|T16–T19|ev 4121–4390〕标题`。
|
|
30
32
|
- **提醒但不代劳**:占用到线、又长了、某层超配额时,在本轮收尾把模型叫醒并附一张新地图。从不自动压缩,也不催 `/compact`。
|
|
@@ -62,13 +64,13 @@ dsh plugin add ./dsh-ebbing-context
|
|
|
62
64
|
|
|
63
65
|
1. 打开 profile 的 `cordis.patch.yml`,删掉 `dsh-ebbing-context` 那一节(包括面板里改过的 `config`)。不删的话,下一次启动会因为"patch 指向的条目不存在"在 stderr 刷一条警告——不会坏,但会一直刷。
|
|
64
66
|
2. 日志在 `$DSH_HOME/logs/ebbing-context.log`(滚动时还有 `.1` / `.2`),卸载不会自动删,确认不需要排查后手动删掉即可。
|
|
65
|
-
**建议关掉宿主自带的自动压缩**(`compaction-basic` 的 `auto`,默认 80%
|
|
67
|
+
**建议关掉宿主自带的自动压缩**(`compaction-basic` 的 `auto`,默认 80% 触发)。它一旦触发会把多层摘要重新拍平,分层结构就没了(原文还在会话日志里,可按事件号读回)。插件照样能压,只是效果打折。关的位置取决于你的安装方式与预设。
|
|
66
68
|
|
|
67
69
|
---
|
|
68
70
|
|
|
69
71
|
## 配置
|
|
70
72
|
|
|
71
|
-
共 17 项(`triggerPct`、`keepTurns`、`pruneFirst`、`foldCallRecord`、`requireSessionQuery`、`reservedTokens`、`budget.*`、`segment.*`、`title.maxTokens`、`notice
|
|
73
|
+
共 17 项(`triggerPct`、`keepTurns`、`pruneFirst`、`foldCallRecord`、`requireSessionQuery`、`reservedTokens`、`budget.*`、`segment.*`、`title.maxTokens`、`notice.*`),在系统设置 →「EC上下文设置」(英文环境为 Ebbing Context)或插件行详情页里改,改完点「保存」才生效。
|
|
72
74
|
|
|
73
75
|
默认值、含义与拒绝规则见 [docs/SPEC.md 第 6 节](https://github.com/Light-EI/dsh-ebbing-context/blob/main/docs/SPEC.md#6-配置17-项)。
|
|
74
76
|
|
package/client.js
CHANGED
|
@@ -1,113 +1,169 @@
|
|
|
1
1
|
// 浏览器侧:在「插件」面板的这个 bundle 页里画配置表单。
|
|
2
2
|
// 保存走宿主给的 form.mutate(一次提交 = 一个 revision = 一次校验),写回 profile 的 cordis.patch.yml。
|
|
3
3
|
// 这里不做任何读文件、不调用模型;只负责把可设置项画出来。
|
|
4
|
+
// 界面语言跟随宿主:中文环境只显示中文、英文环境只显示英文(两套文案都要维护,见维护手册 §1.30)。
|
|
4
5
|
|
|
5
6
|
window.__ModuleLoader__.load({
|
|
6
7
|
id: 'dsh-ebbing-context',
|
|
7
8
|
factory(require) {
|
|
8
9
|
const React = require('react')
|
|
9
10
|
const h = React.createElement
|
|
10
|
-
const { useState, useEffect } = React
|
|
11
|
+
const { useState, useEffect, useMemo } = React
|
|
11
12
|
|
|
12
13
|
const TEXT = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
14
|
+
zh: {
|
|
15
|
+
title: 'EC上下文设置',
|
|
16
|
+
hint: '改完点最下面的「保存」;离开页面不保存。',
|
|
17
|
+
loading: '正在读取配置…',
|
|
18
|
+
unavailable: '当前客户端读不到这个插件的配置。',
|
|
19
|
+
readonly: '当前 profile 不允许写入配置。',
|
|
20
|
+
// 宿主侧插件没启动时,面板拿不到 form,页面会永远停在「正在读取配置…」。
|
|
21
|
+
// 那种情况下必须有一句能指向原因的话,否则用户只看到一片空白。
|
|
22
|
+
late: '一直停在这里,说明宿主侧的插件没有启动起来。DSH 日志里会有一条加载错误(搜 import),插件卡片的状态也会停在「未运行」而不是「运行中」。',
|
|
23
|
+
save: '保存',
|
|
24
|
+
saving: '保存中…',
|
|
25
|
+
discard: '放弃修改',
|
|
26
|
+
detail: '详情',
|
|
27
|
+
hide: '收起',
|
|
28
|
+
saved: '已保存。',
|
|
29
|
+
nothing: '没有改动。',
|
|
30
|
+
refused: '宿主拒绝了这次保存,改动没有生效。可能是值超出允许范围,或该字段不允许这样设置。',
|
|
31
|
+
failed: '保存失败:',
|
|
32
|
+
invalid: '这些值不合法,没有提交:',
|
|
33
|
+
// 使用注意:宿主自带的自动压缩一旦触发,会把这里的分层摘要拍平。
|
|
34
|
+
// 放在页面最上方、不折叠——它比任何一个配置项都更容易把人坑到。
|
|
35
|
+
// 不写死"改哪个文件"(配置入口随安装方式与预设而变),但要说清"关的是什么",
|
|
36
|
+
// 并直接给一句可以复制给助手/模型的话——用户不必自己去找配置文件。
|
|
37
|
+
note: '使用注意:如果这套 DSH 开着自带的自动压缩(compaction-basic 的自动压缩,默认在上下文用到约 80% 时自己触发),建议关掉它。它一旦触发,会把这里的多层摘要重新拍平成一段总摘要,分层结构就没了——详细内容要从会话日志里按事件号翻回来,模型不会再看到这些块;插件本身不受影响、照常能压,但效果会打折扣。要关的是「自动压缩」这个行为(通常是一个 auto 之类的开关,或直接停用那个插件),不关也行、只是效果打折。具体在哪一行、怎么改取决于你的安装方式与预设,插件不会替你改宿主配置。不想自己找的话,可以把这句发给你的助手或模型:「帮我关掉 DeepSeek Harness 自带的自动压缩(compaction-basic 的 auto),改完告诉我你动了哪个文件、怎么改回去」,让它去查;也可以先问问它这个功能是做什么的再决定。',
|
|
38
|
+
},
|
|
39
|
+
en: {
|
|
40
|
+
title: 'Ebbing Context',
|
|
41
|
+
hint: 'Click "Save" at the bottom when done; leaving this page discards changes.',
|
|
42
|
+
loading: 'Reading configuration…',
|
|
43
|
+
unavailable: 'This client cannot read this plugin\'s configuration.',
|
|
44
|
+
readonly: 'This profile does not allow configuration writes.',
|
|
45
|
+
late: 'Stuck here means the host-side plugin did not start. The DSH log will contain a load error (search for "import"), and the plugin card will stay at "not running" instead of "running".',
|
|
46
|
+
save: 'Save',
|
|
47
|
+
saving: 'Saving…',
|
|
48
|
+
discard: 'Discard changes',
|
|
49
|
+
detail: 'Details',
|
|
50
|
+
hide: 'Hide',
|
|
51
|
+
saved: 'Saved.',
|
|
52
|
+
nothing: 'No changes.',
|
|
53
|
+
refused: 'The host rejected this save, so nothing was applied. A value may be out of the allowed range, or this field may not be settable this way.',
|
|
54
|
+
failed: 'Save failed: ',
|
|
55
|
+
invalid: 'These values were not submitted: ',
|
|
56
|
+
note: 'Note: if this DSH installation has the host\'s built-in auto-compaction enabled (compaction-basic\'s auto, triggered by default at about 80% of the context), turning it off is recommended. Once it fires it re-flattens these layered summaries into one big summary and the layered structure is gone — details would have to be read back from the session log by event number, and the model will not see these blocks any more. The plugin itself is unaffected and still compresses, just with a discount. What you want to turn off is the "auto-compaction" behavior (usually an auto switch, or simply disabling that plugin); leaving it on is fine too, just with the discount. Which line to change and how depends on your install method and preset — the plugin will not touch host configuration for you. If you would rather not hunt for it, send this to your assistant or model: "Turn off DeepSeek Harness\'s built-in auto-compaction (compaction-basic\'s auto), and tell me which file you changed and how to revert it", and let it look; you can also ask what the feature does first and then decide.',
|
|
57
|
+
},
|
|
37
58
|
}
|
|
38
59
|
|
|
39
|
-
//
|
|
60
|
+
// 设置面板里的标题跟宿主语言走:中文「EC上下文设置」、英文「Ebbing Context」。
|
|
61
|
+
// 只认一个判据(宿主 locale 的 active),正文与标题不会分叉;拿不到 locale 服务时退回中文。
|
|
62
|
+
let localeApi = null // 用来读 active、订阅语言切换;在 apply 里从 ctx.get('locale') 取
|
|
63
|
+
const activeLocale = () => {
|
|
64
|
+
try {
|
|
65
|
+
const snap = localeApi && typeof localeApi.getLocale === 'function' ? localeApi.getLocale() : null
|
|
66
|
+
return snap && snap.active ? String(snap.active) : 'zh'
|
|
67
|
+
} catch {
|
|
68
|
+
return 'zh'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const pageTitle = () => (activeLocale().startsWith('zh') ? TEXT.zh.title : TEXT.en.title)
|
|
72
|
+
|
|
73
|
+
// 每一项:分组、标签、路径、类型、范围、详情(中英各一份,行为完全一致)
|
|
40
74
|
const FIELDS = [
|
|
41
75
|
{
|
|
42
|
-
group: '配额', label: 'L1 占比', path: ['budget', 'l1'], kind: 'number', step: 0.01, min: 0, max: 1,
|
|
76
|
+
group: '配额', groupEn: 'Quota', label: 'L1 占比', labelEn: 'L1 share', path: ['budget', 'l1'], kind: 'number', step: 0.01, min: 0, max: 1,
|
|
43
77
|
detail: '第一层把「原文」折成块,整层合计最多占可用输入的比例。0.10 表示最多占 10%。三层合计默认 0.19(10% + 6% + 3%);调大后留给原文的空间会变小。',
|
|
78
|
+
detailEn: 'The first layer folds raw text into blocks; this caps the whole layer as a share of usable input. 0.10 means at most 10%. The three layers total 0.19 by default (10% + 6% + 3%); raising them leaves less room for raw text.',
|
|
44
79
|
},
|
|
45
80
|
{
|
|
46
|
-
group: '配额', label: 'L2 占比', path: ['budget', 'l2'], kind: 'number', step: 0.01, min: 0, max: 1,
|
|
81
|
+
group: '配额', groupEn: 'Quota', label: 'L2 占比', labelEn: 'L2 share', path: ['budget', 'l2'], kind: 'number', step: 0.01, min: 0, max: 1,
|
|
47
82
|
detail: '第二层把「L1 块」再折一次,整层合计最多占可用输入的比例。',
|
|
83
|
+
detailEn: 'The second layer folds L1 blocks again; this caps the whole layer as a share of usable input.',
|
|
48
84
|
},
|
|
49
85
|
{
|
|
50
|
-
group: '配额', label: 'L3 占比', path: ['budget', 'l3'], kind: 'number', step: 0.01, min: 0, max: 1,
|
|
86
|
+
group: '配额', groupEn: 'Quota', label: 'L3 占比', labelEn: 'L3 share', path: ['budget', 'l3'], kind: 'number', step: 0.01, min: 0, max: 1,
|
|
51
87
|
detail: '第三层只放关键词句,用来保住任务目标、约束和结论。整层合计最多占可用输入的比例。',
|
|
88
|
+
detailEn: 'The third layer keeps only keywords and key sentences, preserving task goals, constraints and conclusions; this caps the whole layer as a share of usable input.',
|
|
52
89
|
},
|
|
53
90
|
{
|
|
54
|
-
group: '单块上限', label: 'L1 单块上限', path: ['segment', 'l1'], kind: 'number', step: 100, min: 64,
|
|
91
|
+
group: '单块上限', groupEn: 'Block limit', label: 'L1 单块上限', labelEn: 'L1 block limit', path: ['segment', 'l1'], kind: 'number', step: 100, min: 64,
|
|
55
92
|
detail: '一个 L1 块正文最多多少 token(按等效 token 算:中文 1 字约 0.6、英文 1 字约 0.3)。超了这次调用会被整批拒绝,拒绝里给出实际值与上限;怎么调整由模型自己判断(换一段更小的范围再压),不要为了塞进上限删内容。',
|
|
93
|
+
detailEn: 'Maximum tokens of one L1 block body (equivalent-token basis: about 0.6 per Chinese character, 0.3 per English character). If exceeded, the whole call is rejected and the rejection reports the actual value and the limit; the model decides how to adjust (compress a smaller range), and must not delete content just to fit.',
|
|
56
94
|
},
|
|
57
95
|
{
|
|
58
|
-
group: '单块上限', label: 'L2 单块上限', path: ['segment', 'l2'], kind: 'number', step: 100, min: 64,
|
|
96
|
+
group: '单块上限', groupEn: 'Block limit', label: 'L2 单块上限', labelEn: 'L2 block limit', path: ['segment', 'l2'], kind: 'number', step: 100, min: 64,
|
|
59
97
|
detail: '一个 L2 块正文最多多少 token。超过同样整批拒绝,并给出实际值与上限。',
|
|
98
|
+
detailEn: 'Maximum tokens of one L2 block body. Exceeding it likewise rejects the whole batch and reports the actual value and the limit.',
|
|
60
99
|
},
|
|
61
100
|
{
|
|
62
|
-
group: '单块上限', label: 'L3 单块上限', path: ['segment', 'l3'], kind: 'number', step: 100, min: 64,
|
|
101
|
+
group: '单块上限', groupEn: 'Block limit', label: 'L3 单块上限', labelEn: 'L3 block limit', path: ['segment', 'l3'], kind: 'number', step: 100, min: 64,
|
|
63
102
|
detail: '一个 L3 块正文最多多少 token。压 l2 时新块追加在 L3 区末尾、旧 L3 块不动;压 l3 时整个 L3 区并成一块,旧块一起并进去——所以这一块的上限也决定了 L3 区最后一次压缩能留下多少。',
|
|
103
|
+
detailEn: 'Maximum tokens of one L3 block body. Compressing l2 appends a new block at the end of the L3 region (old L3 blocks stay); compressing l3 folds the whole L3 region into one block, old blocks included — so this limit also decides how much the final L3 compaction can keep.',
|
|
64
104
|
},
|
|
65
105
|
{
|
|
66
|
-
group: '单块上限', label: '块标题上限', path: ['title', 'maxTokens'], kind: 'number', step: 5, min: 4,
|
|
106
|
+
group: '单块上限', groupEn: 'Block limit', label: '块标题上限', labelEn: 'Block title limit', path: ['title', 'maxTokens'], kind: 'number', step: 5, min: 4,
|
|
67
107
|
detail: '每个块正文首行那个标题最多多少 token。标题是模型之后认块用的,太短会认错,太长浪费。',
|
|
108
|
+
detailEn: 'Maximum tokens of the title on the first line of a block. The model uses titles to recognize blocks later: too short and it misidentifies them, too long wastes tokens.',
|
|
68
109
|
},
|
|
69
110
|
{
|
|
70
|
-
group: '提醒', label: '开启提醒', path: ['notice', 'enabled'], kind: 'boolean',
|
|
111
|
+
group: '提醒', groupEn: 'Reminders', label: '开启提醒', labelEn: 'Enable reminders', path: ['notice', 'enabled'], kind: 'boolean',
|
|
71
112
|
detail: '提醒的总开关。开着时,插件在每一轮收尾(该叫醒时把模型叫醒)递一条提醒并附上当前地图,让模型自己决定压不压——它从不替模型压缩。关掉就完全不提醒,压缩本身不受影响。',
|
|
113
|
+
detailEn: 'Master switch for reminders. When on, at the end of each turn (when it is time) the plugin wakes the model with a reminder and the current map, and the model decides whether to compress — it never compresses on the model\'s behalf. Off means no reminders at all; compression itself is unaffected.',
|
|
72
114
|
},
|
|
73
115
|
{
|
|
74
|
-
group: '提醒', label: '增长提醒阈值', path: ['notice', 'growthTokens'], kind: 'number', step: 1000, min: 0,
|
|
116
|
+
group: '提醒', groupEn: 'Reminders', label: '增长提醒阈值', labelEn: 'Growth reminder threshold', path: ['notice', 'growthTokens'], kind: 'number', step: 1000, min: 0,
|
|
75
117
|
detail: '到线之前的另一条路:自上次提醒后又长了这么多 token 就提醒一次,即使总量还没到线。写 0 只关掉这一条(到线与某层超出设计占比仍会提醒)。长到这个数却一层都压不动时,门槛会自动砍到一半、基线前移,再长半个门槛就重新判一次。',
|
|
118
|
+
detailEn: 'A second path besides the usage line: remind once the session has grown by this many tokens since the last reminder, even before the total hits the line. 0 disables only this path (the usage line and per-layer quota still remind). If the threshold is reached but no layer can be compressed, the threshold is halved and the baseline moves forward, then another half-threshold of growth re-evaluates.',
|
|
76
119
|
},
|
|
77
120
|
{
|
|
78
|
-
group: '提醒', label: '
|
|
121
|
+
group: '提醒', groupEn: 'Reminders', label: '模型主动拒绝之后门槛重设到原额的百分比', labelEn: 'Threshold reset after the model declines (%)', path: ['notice', 'growthResetPct'], kind: 'number', step: 10, min: 0, max: 100,
|
|
79
122
|
detail: '增长提醒发出去之后,模型如果决定这一轮不压(内容还没做完、或者比较重要),下一个门槛就按原额的百分之多少重设。40(默认)= 原额 65000 的话,再长 26000 才重新触发;100 = 全额重攒;0 = 门槛降到 1 token,下一轮只要再长一点就重新触发。只影响「又长了」这条触发,不影响到线提醒。增长到量但一层都压不动时走另一条路:门槛固定降到一半、基线前移,再长半个门槛重新判一次,直到某次真压得动为止。',
|
|
123
|
+
detailEn: 'After a growth reminder goes out, if the model decides not to compress this turn (content unfinished or important), the next threshold is reset to this percentage of the original. 40 (default) = with an original threshold of 65000, another 26000 tokens are needed to trigger again; 100 = save the full amount again; 0 = the threshold drops to 1 token, so the next bit of growth triggers again. Only affects the "grew again" path, not the usage line. When growth has occurred but no layer can be compressed, another path applies: the threshold halves and the baseline moves forward, re-evaluated every half-threshold until a compaction actually succeeds.',
|
|
80
124
|
},
|
|
81
125
|
{
|
|
82
|
-
group: '提醒', label: '子代理也参与', path: ['notice', 'notifySubagents'], kind: 'boolean',
|
|
126
|
+
group: '提醒', groupEn: 'Reminders', label: '子代理也参与', labelEn: 'Include subagents', path: ['notice', 'notifySubagents'], kind: 'boolean',
|
|
83
127
|
detail: '开(默认):子代理按同一套规则被标记与叫醒。它也会碰到已压缩的表层,没有轮次标记就看不出「这块盖住了哪几轮」,压过的信息在它眼里等于丢了。关:子代理不叫醒、不写轮次标记、不报红字。',
|
|
128
|
+
detailEn: 'On (default): subagents are marked and woken by the same rules. They also see compressed surfaces; without turn markers they cannot tell which turns a block covers, so compressed information is effectively lost to them. Off: subagents are not woken, no turn markers are written, no red errors.',
|
|
84
129
|
},
|
|
85
130
|
{
|
|
86
|
-
group: '提醒', label: '提醒触发占比', path: ['triggerPct'], kind: 'number', step: 0.01, min: 0.1, max: 0.99,
|
|
131
|
+
group: '提醒', groupEn: 'Reminders', label: '提醒触发占比', labelEn: 'Reminder trigger share', path: ['triggerPct'], kind: 'number', step: 0.01, min: 0.1, max: 0.99,
|
|
87
132
|
detail: '那条线画在哪。0.75 表示用到「可用输入」的 75% 开始提醒。可用输入 = 上下文窗口 − 输出预留,不是裸窗口大小。只在越过这条线时提醒一次,停在线上不会每轮重复催;掉到线下(比如压完之后)再越线才会再提醒一次。',
|
|
133
|
+
detailEn: 'Where the line is drawn. 0.75 means start reminding at 75% of usable input. Usable input = context window − output reserve, not the raw window size. It reminds once when crossing the line; staying above it does not nag every turn; dropping below (e.g. after compaction) and crossing again reminds once more.',
|
|
88
134
|
},
|
|
89
135
|
{
|
|
90
|
-
group: '行为', label: '原文区留住最后几轮', path: ['keepTurns'], kind: 'number', step: 1, min: 1, max: 50,
|
|
136
|
+
group: '行为', groupEn: 'Behavior', label: '原文区留住最后几轮', labelEn: 'Raw turns kept', path: ['keepTurns'], kind: 'number', step: 1, min: 1, max: 50,
|
|
91
137
|
detail: '原文区必须留住的最后 N 轮:compress 的 l0 最多只写到「当前轮 − 1 − N」。当前轮本来就压不了;这条是给最近几轮留缓冲,免得刚压完就要按事件号取回原文。',
|
|
138
|
+
detailEn: 'The last N turns the raw region must keep: compress l0 can only write up to "current turn − 1 − N". The current turn can never be compressed; this buffer keeps the most recent turns intact so raw text does not have to be fetched back by event number right after a compaction.',
|
|
92
139
|
},
|
|
93
140
|
{
|
|
94
|
-
group: '行为', label: '剪大工具返回', path: ['pruneFirst'], kind: 'boolean',
|
|
141
|
+
group: '行为', groupEn: 'Behavior', label: '剪大工具返回', labelEn: 'Prune large tool results', path: ['pruneFirst'], kind: 'boolean',
|
|
95
142
|
detail: '把超大的工具返回剪短,别让它们白占上下文。宿主的 toolResultPruner 是全局服务:任何工具返回只要超过 8192 字符,就把中间挖掉、只留开头 4096 与结尾 1024,并插一句「[... tool result middle pruned ...]」,对所有工具一视同仁(本插件 compress 自己那条返回也一样)。它只动工具返回,不动模型写的摘要、也不动压缩块——块是 user/message,不是 tool/result,所以摘要剪不到;被剪的中间那段的原文仍在会话日志里,可按事件号取回。宿主原本把它当「压缩的前置步骤」:只有它的自动压缩要动手时才叫。关掉宿主自动压缩后,改由本插件在每次压缩前叫一次——不压缩就不剪,模型正在用的内容不会被提前改写。关掉这个开关就完全不剪,原文原样留着。',
|
|
143
|
+
detailEn: 'Shorten oversized tool results so they do not take up space for nothing. The host\'s toolResultPruner is a global service: any tool result over 8192 characters has its middle cut out, keeping the first 4096 and last 1024, with "[... tool result middle pruned ...]" inserted — for all tools alike (including this plugin\'s own compress return). It only touches tool results, never model-written summaries or compressed blocks (blocks are user/message, not tool/result, so summaries are never pruned); the pruned middle stays in the session log and is retrievable by event number. The host originally called it as a "pre-step of compaction", only when its own auto-compaction was about to act. With host auto-compaction off, this plugin calls it once before each compression — no compression, no pruning, so content the model is using is not rewritten early. Turn this switch off to disable pruning entirely; raw text stays as is.',
|
|
96
144
|
},
|
|
97
145
|
{
|
|
98
|
-
group: '行为', label: '折叠压缩调用记录', path: ['foldCallRecord'], kind: 'boolean',
|
|
146
|
+
group: '行为', groupEn: 'Behavior', label: '折叠压缩调用记录', labelEn: 'Fold compaction call records', path: ['foldCallRecord'], kind: 'boolean',
|
|
99
147
|
detail: '压缩成功后,把「这次调用 compress + 它的返回」一起折成一句短注(说明调过、成功了、压了什么)。摘要正文本来就已经写成顶部的块,留在调用参数里的那份是纯重复。宿主不允许改写助手消息本身,只删调用又会留下孤儿返回,所以这一对只能在下一次请求之前整体换成一句用户消息——代价是你看不到那次调用的完整返回。',
|
|
148
|
+
detailEn: 'After a successful compaction, fold "this compress call + its return" into one short note (saying it was called, succeeded, and what was compressed). The summary body is already at the top as a block, so the copy in the call arguments is pure duplication. The host does not allow rewriting assistant messages, and deleting only the call would leave an orphan result, so the pair is replaced as a whole with one user message before the next request — at the cost of not seeing that call\'s full return.',
|
|
100
149
|
},
|
|
101
150
|
{
|
|
102
|
-
group: '行为', label: '要求可取回原文', path: ['requireSessionQuery'], kind: 'boolean',
|
|
151
|
+
group: '行为', groupEn: 'Behavior', label: '要求可取回原文', labelEn: 'Require raw-text retrieval', path: ['requireSessionQuery'], kind: 'boolean',
|
|
103
152
|
detail: '它不改变压缩本身:开或关,compress 都照常工作。它只决定「没有取回工具时要不要停用」——开着时,一旦发现没装「会话搜索」,两个工具就停用并说明原因,提醒也不再叫醒,宁可上下文大着也不做取不回的事;关掉时照常压;能不能按事件号查回来,取决于这台机器装没装会话搜索。',
|
|
153
|
+
detailEn: 'Does not change compaction itself: on or off, compress works as usual. It only decides whether to disable when the retrieval tool is missing: when on, if "session search" is not installed, both tools are disabled with the reason, reminders stop, and the context is left large rather than compacting something that cannot be retrieved; when off, compaction proceeds as usual, and whether it can be looked up by event number depends on whether session search is installed on this machine.',
|
|
104
154
|
},
|
|
105
155
|
{
|
|
106
|
-
group: '行为', label: '输出预留兜底', path: ['reservedTokens'], kind: 'number', step: 1000, min: 0,
|
|
156
|
+
group: '行为', groupEn: 'Behavior', label: '输出预留兜底', labelEn: 'Output reserve fallback', path: ['reservedTokens'], kind: 'number', step: 1000, min: 0,
|
|
107
157
|
detail: '读不到宿主的输出预留值时,用它扣减可用窗口。0 表示不扣。注意:宿主能给出预留值时这一项不生效。',
|
|
158
|
+
detailEn: 'Used to subtract from the usable window when the host\'s output reserve value cannot be read. 0 means subtract nothing. Note: this has no effect when the host provides the reserve value.',
|
|
108
159
|
},
|
|
109
160
|
]
|
|
110
161
|
|
|
162
|
+
// 设置面板左侧导航的独立页要读写的命名空间 = profile 里 Loader 行的入口 id。
|
|
163
|
+
// 不是包名、也不是「插件」页的 slot key(`<包名>#<行 id>`);这条 id 写在
|
|
164
|
+
// dsh.bundle.patch.yml 的 insert 行里,和插件行 id 相同。改安装方式时这里要跟着改。
|
|
165
|
+
const CONFIG_NS = 'dsh-ebbing-context'
|
|
166
|
+
|
|
111
167
|
function readPath(obj, path) {
|
|
112
168
|
let cur = obj
|
|
113
169
|
for (const k of path) {
|
|
@@ -186,6 +242,12 @@ window.__ModuleLoader__.load({
|
|
|
186
242
|
const [busy, setBusy] = useState(false)
|
|
187
243
|
// 「宿主侧根本没起来」和「配置还在路上」在界面上长得一样,靠时间把它们分开。
|
|
188
244
|
const [late, setLate] = useState(false)
|
|
245
|
+
// 宿主语言切换时重画标题(「插件」页那一份没有 locale 注入,只能自己订阅)。
|
|
246
|
+
const [, bumpLocale] = useState(0)
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
if (!localeApi || typeof localeApi.subscribe !== 'function') return undefined
|
|
249
|
+
return localeApi.subscribe(() => bumpLocale((n) => n + 1))
|
|
250
|
+
}, [])
|
|
189
251
|
|
|
190
252
|
useEffect(() => {
|
|
191
253
|
if (form) return undefined
|
|
@@ -193,6 +255,11 @@ window.__ModuleLoader__.load({
|
|
|
193
255
|
return () => clearTimeout(timer)
|
|
194
256
|
}, [form])
|
|
195
257
|
|
|
258
|
+
// 界面只显示当前语言(跟随宿主 locale),不再中英同屏。
|
|
259
|
+
// 字段的 label/detailEn、TEXT.zh/TEXT.en 仍然两套都保留——改文案时两边都要改。
|
|
260
|
+
const zh = activeLocale().startsWith('zh')
|
|
261
|
+
const T = zh ? TEXT.zh : TEXT.en
|
|
262
|
+
|
|
196
263
|
const st = (form && form.state) || {}
|
|
197
264
|
const revision = st.revision
|
|
198
265
|
|
|
@@ -202,14 +269,15 @@ window.__ModuleLoader__.load({
|
|
|
202
269
|
// form 由「插件」面板的宿主侧拼出来({ state, mutate }),插件条目还没进配置目录时它是 undefined:
|
|
203
270
|
// 那是"还没读到",不是"没有配置项",别骗用户说没声明。
|
|
204
271
|
if (!form) {
|
|
205
|
-
return h(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
272
|
+
return h('div', { style: { fontSize: '13px', opacity: 0.7, lineHeight: 1.6 } },
|
|
273
|
+
late ? T.late : T.loading)
|
|
274
|
+
}
|
|
275
|
+
if (st.status === 'loading') {
|
|
276
|
+
return h('div', { style: { fontSize: '13px', opacity: 0.7 } }, T.loading)
|
|
277
|
+
}
|
|
278
|
+
if (st.status === 'unavailable') {
|
|
279
|
+
return h('div', { style: { fontSize: '13px', opacity: 0.7 } }, T.unavailable)
|
|
210
280
|
}
|
|
211
|
-
if (st.status === 'loading') return h('div', { style: { fontSize: '13px', opacity: 0.7 } }, TEXT.loading)
|
|
212
|
-
if (st.status === 'unavailable') return h('div', { style: { fontSize: '13px', opacity: 0.7 } }, TEXT.unavailable)
|
|
213
281
|
|
|
214
282
|
const value = st.value || {}
|
|
215
283
|
const readonly = st.writable === false
|
|
@@ -243,7 +311,7 @@ window.__ModuleLoader__.load({
|
|
|
243
311
|
|
|
244
312
|
async function save() {
|
|
245
313
|
if (busy || readonly) return
|
|
246
|
-
if (changed.length === 0) { setBanner({ kind: 'ok',
|
|
314
|
+
if (changed.length === 0) { setBanner({ kind: 'ok', zh: TEXT.zh.nothing, en: TEXT.en.nothing }); return }
|
|
247
315
|
|
|
248
316
|
const bad = []
|
|
249
317
|
const ops = []
|
|
@@ -254,14 +322,27 @@ window.__ModuleLoader__.load({
|
|
|
254
322
|
continue
|
|
255
323
|
}
|
|
256
324
|
const num = Number(raw)
|
|
257
|
-
if (raw === '' || !Number.isFinite(num)) {
|
|
258
|
-
|
|
259
|
-
|
|
325
|
+
if (raw === '' || !Number.isFinite(num)) {
|
|
326
|
+
bad.push({ zh: item.label + '(不是数字)', en: item.labelEn + ' (not a number)' })
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
if (item.min !== undefined && num < item.min) {
|
|
330
|
+
bad.push({ zh: item.label + '(不能小于 ' + item.min + ')', en: item.labelEn + ' (must be ≥ ' + item.min + ')' })
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
if (item.max !== undefined && num > item.max) {
|
|
334
|
+
bad.push({ zh: item.label + '(不能大于 ' + item.max + ')', en: item.labelEn + ' (must be ≤ ' + item.max + ')' })
|
|
335
|
+
continue
|
|
336
|
+
}
|
|
260
337
|
ops.push({ op: 'set', path: item.path, value: num })
|
|
261
338
|
}
|
|
262
339
|
|
|
263
340
|
if (bad.length > 0) {
|
|
264
|
-
setBanner({
|
|
341
|
+
setBanner({
|
|
342
|
+
kind: 'error',
|
|
343
|
+
zh: TEXT.zh.invalid + bad.map((b) => b.zh).join(';'),
|
|
344
|
+
en: TEXT.en.invalid + bad.map((b) => b.en).join('; '),
|
|
345
|
+
})
|
|
265
346
|
return
|
|
266
347
|
}
|
|
267
348
|
|
|
@@ -270,12 +351,13 @@ window.__ModuleLoader__.load({
|
|
|
270
351
|
const ok = await form.mutate(ops, revision)
|
|
271
352
|
if (ok) {
|
|
272
353
|
setDraft({})
|
|
273
|
-
setBanner({ kind: 'ok',
|
|
354
|
+
setBanner({ kind: 'ok', zh: TEXT.zh.saved, en: TEXT.en.saved })
|
|
274
355
|
} else {
|
|
275
|
-
setBanner({ kind: 'error',
|
|
356
|
+
setBanner({ kind: 'error', zh: TEXT.zh.refused, en: TEXT.en.refused })
|
|
276
357
|
}
|
|
277
358
|
} catch (err) {
|
|
278
|
-
|
|
359
|
+
const msg = err && err.message ? err.message : String(err)
|
|
360
|
+
setBanner({ kind: 'error', zh: TEXT.zh.failed + msg, en: TEXT.en.failed + msg })
|
|
279
361
|
} finally {
|
|
280
362
|
setBusy(false)
|
|
281
363
|
}
|
|
@@ -307,30 +389,31 @@ window.__ModuleLoader__.load({
|
|
|
307
389
|
const groups = []
|
|
308
390
|
for (const item of FIELDS) {
|
|
309
391
|
if (groups.length === 0 || groups[groups.length - 1].name !== item.group) {
|
|
310
|
-
groups.push({ name: item.group, items: [] })
|
|
392
|
+
groups.push({ name: item.group, nameEn: item.groupEn, items: [] })
|
|
311
393
|
}
|
|
312
394
|
groups[groups.length - 1].items.push(item)
|
|
313
395
|
}
|
|
314
396
|
|
|
315
397
|
return h('div', { style: { fontSize: '13px', color: 'inherit' } },
|
|
316
|
-
h('div', { style: { fontSize: '14px', fontWeight: 600 } },
|
|
317
|
-
h('div', { style: { ...detailStyle, marginTop: '2px', marginBottom: '8px' } },
|
|
318
|
-
|
|
319
|
-
|
|
398
|
+
h('div', { style: { fontSize: '14px', fontWeight: 600 } }, pageTitle()),
|
|
399
|
+
h('div', { style: { ...detailStyle, marginTop: '2px', marginBottom: '8px' } },
|
|
400
|
+
readonly ? T.readonly : T.hint),
|
|
401
|
+
h('div', { style: noteStyle }, T.note),
|
|
402
|
+
banner ? h('div', { style: bannerStyle(banner.kind) }, zh ? banner.zh : banner.en) : null,
|
|
320
403
|
groups.map((g) => h('div', { key: g.name },
|
|
321
|
-
h('div', { style: groupStyle }, g.name),
|
|
404
|
+
h('div', { style: groupStyle }, zh ? g.name : g.nameEn),
|
|
322
405
|
g.items.map((item) => {
|
|
323
406
|
const k = keyOf(item)
|
|
324
407
|
return h('div', { key: k },
|
|
325
408
|
h('div', { style: rowStyle },
|
|
326
409
|
h('div', null,
|
|
327
|
-
h('span', { style: labelStyle }, item.label),
|
|
410
|
+
h('span', { style: labelStyle }, zh ? item.label : item.labelEn),
|
|
328
411
|
h('button', {
|
|
329
412
|
type: 'button',
|
|
330
413
|
style: linkStyle,
|
|
331
414
|
onClick: () => setOpen((prev) => ({ ...prev, [k]: !prev[k] })),
|
|
332
|
-
}, open[k] ?
|
|
333
|
-
open[k] ? h('div', { style: detailStyle }, item.detail) : null,
|
|
415
|
+
}, open[k] ? T.hide : T.detail),
|
|
416
|
+
open[k] ? h('div', { style: detailStyle }, zh ? item.detail : item.detailEn) : null,
|
|
334
417
|
),
|
|
335
418
|
control(item),
|
|
336
419
|
),
|
|
@@ -343,26 +426,83 @@ window.__ModuleLoader__.load({
|
|
|
343
426
|
style: buttonStyle(true, readonly || busy),
|
|
344
427
|
disabled: readonly || busy,
|
|
345
428
|
onClick: save,
|
|
346
|
-
}, busy ?
|
|
429
|
+
}, busy ? T.saving : T.save),
|
|
347
430
|
h('button', {
|
|
348
431
|
type: 'button',
|
|
349
432
|
style: buttonStyle(false, readonly || busy || changed.length === 0),
|
|
350
433
|
disabled: readonly || busy || changed.length === 0,
|
|
351
434
|
onClick: () => { setDraft({}); setBanner(null) },
|
|
352
|
-
},
|
|
435
|
+
}, T.discard),
|
|
353
436
|
h('span', { style: { fontSize: '12px', opacity: 0.6, alignSelf: 'center' } },
|
|
354
|
-
changed.length > 0 ? '待保存 ' + changed.length + ' 项' : ''),
|
|
437
|
+
changed.length > 0 ? (zh ? '待保存 ' + changed.length + ' 项' : changed.length + ' unsaved') : ''),
|
|
355
438
|
),
|
|
356
439
|
)
|
|
357
440
|
}
|
|
358
441
|
|
|
359
442
|
return {
|
|
443
|
+
// 只硬依赖 slots(两个入口都靠它);configForms / locale 用 ctx.inject 等宿主提供后再接:
|
|
444
|
+
// 缺失时降级——插件行配置页照常工作、设置页不出现、标题退回中文,
|
|
445
|
+
// 不会因为新服务缺失让整个 client 半侧不激活(配置页凭空消失)。
|
|
446
|
+
// 假设宿主 cordis 4 的 ctx.inject 存在(本插件最低宿主即 4;该方法缺失时 apply 会抛)。
|
|
360
447
|
inject: ['slots'],
|
|
361
448
|
apply(ctx) {
|
|
449
|
+
// 每次 apply 都先复位:插件被重载/停用后不能留着上一份宿主 runtime 的引用。
|
|
450
|
+
localeApi = null
|
|
451
|
+
|
|
452
|
+
// 标题跟宿主语言走;只读 active,不注册词典(标题和正文用同一个判据)。
|
|
453
|
+
// 用 ctx.inject 而不是 ctx.get:服务如果比本插件晚就位,回调会等它出现再跑。
|
|
454
|
+
ctx.inject(['locale'], (scope) => {
|
|
455
|
+
scope.effect(() => {
|
|
456
|
+
localeApi = scope.locale && typeof scope.locale.getLocale === 'function' ? scope.locale : null
|
|
457
|
+
return () => { localeApi = null }
|
|
458
|
+
}, 'ebbing-context: locale')
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
// 「内置插件」→ 本插件 → 配置:保留。和设置面板里的页编辑同一份配置。
|
|
362
462
|
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
363
463
|
name: 'plugins.row.config',
|
|
364
464
|
key: 'dsh-ebbing-context#dsh-ebbing-context',
|
|
365
465
|
}, ConfigPage))
|
|
466
|
+
|
|
467
|
+
// 设置弹窗左侧导航里的独立页(和「通知提醒」同一层)。不能往别的插件注册的页里塞行:
|
|
468
|
+
// settings.section 的 ownerProps 只有 close,dsh-notify-me 的页也没有留子槽;
|
|
469
|
+
// 所以自己占一项,数据走 configForms.get(入口 id) 的共享读写队列。
|
|
470
|
+
// 同时等 configForms 与 locale:设置页第一次渲染就要拿到正确语言(宿主 locale 的 apply
|
|
471
|
+
// 是 async、可能晚于本插件;缺一个就整页不挂,不拿中文顶替)。行页不受影响。
|
|
472
|
+
// whileServed:宿主没把这条命名空间发出来时(例如插件被停用)页面不出现,
|
|
473
|
+
// 不会留一个点开就报「读不到配置」的空页。
|
|
474
|
+
ctx.inject(['configForms', 'locale'], (scope) => {
|
|
475
|
+
const forms = scope.configForms
|
|
476
|
+
if (!forms || typeof forms.get !== 'function' || typeof forms.whileServed !== 'function') return
|
|
477
|
+
localeApi = scope.locale && typeof scope.locale.getLocale === 'function' ? scope.locale : null
|
|
478
|
+
const formController = forms.get(CONFIG_NS)
|
|
479
|
+
|
|
480
|
+
function SettingsPage() {
|
|
481
|
+
const [snap, setSnap] = useState(() => formController.getSnapshot())
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
const update = () => setSnap(formController.getSnapshot())
|
|
484
|
+
update()
|
|
485
|
+
return formController.subscribe(update)
|
|
486
|
+
}, [])
|
|
487
|
+
const st = snap || { status: 'loading' }
|
|
488
|
+
const status = st.status === 'ready' || st.status === 'unavailable' ? st.status : 'loading'
|
|
489
|
+
// ConfigPage 认的 form 形状和「插件」页给的一样:{ state, mutate }。
|
|
490
|
+
// 直接接共享表单的 snapshot 与写队列,两个入口的保存语义完全一致。
|
|
491
|
+
// useMemo:snapshot 没变时保持 form 引用稳定,ConfigPage 的 [form] effect 不会每轮重跑。
|
|
492
|
+
const pageForm = useMemo(() => ({
|
|
493
|
+
state: { status, value: st.value, revision: st.revision, writable: st.writable },
|
|
494
|
+
mutate: (ops, revision) => formController.mutate(ops, revision),
|
|
495
|
+
}), [status, st.value, st.revision, st.writable])
|
|
496
|
+
return h(ConfigPage, { view: 'page', form: pageForm })
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
scope.effect(() => forms.whileServed([CONFIG_NS], () => scope.slots.inject('settings.section', () => scope.slots.register({
|
|
500
|
+
name: 'settings.section',
|
|
501
|
+
id: 'ebbing-context',
|
|
502
|
+
order: 46, // 两个第三方页都是 45(通知提醒 dsh-notify-me / 归档会话 archive-manager,后者被它自己的开关挡着才不显示);46 排在它们之后、MCP 连接器(80)之前
|
|
503
|
+
label: () => pageTitle(),
|
|
504
|
+
}, SettingsPage))), 'ebbing-context: settings page')
|
|
505
|
+
})
|
|
366
506
|
},
|
|
367
507
|
}
|
|
368
508
|
},
|
package/index.js
CHANGED
|
@@ -140,7 +140,7 @@ export function receiptLines(results, extras = {}) {
|
|
|
140
140
|
lines.push(`这次切点跨过了轮次记录里缺失的 ${swallowed.map((t) => `T${t}`).join('、')}——它们的内容已经一起压进块里,块头标签只写到 to 那一轮。`)
|
|
141
141
|
}
|
|
142
142
|
if (extras.mapText) lines.push(extras.mapText)
|
|
143
|
-
// 收手提醒放在地图之后:否则"可写 to
|
|
143
|
+
// 收手提醒放在地图之后:否则"可写 to 的轮号/序号"紧跟在"不要继续压缩"后面,两句话会打架。
|
|
144
144
|
lines.push(RECEIPT_STOP_LINE)
|
|
145
145
|
return lines
|
|
146
146
|
}
|
|
@@ -1284,7 +1284,7 @@ function noticeText(ctx, config, record, count, log) {
|
|
|
1284
1284
|
// 让"压完就收手"在真正决定压哪几轮的位置上生效,而不是只出现在提醒正文的说明段。
|
|
1285
1285
|
parts.join('') +
|
|
1286
1286
|
(mapText
|
|
1287
|
-
? '
|
|
1287
|
+
? '决定要压时:照下面给的位置,一次调用把要压的层一起给全(深浅同批,区间自己权衡;级联与填表顺序见 compress 的说明)。' +
|
|
1288
1288
|
'没有可压位置就直接告诉用户这一版压完了,并说明宿主自带压缩会把分层拍平、要不要用由用户决定。'
|
|
1289
1289
|
: '决定要压时:这次没能生成地图(插件内部出错,先别压,按原计划继续),别凭记忆压——先调一次 context_map 拿到位置再动手;' +
|
|
1290
1290
|
'如果连 context_map 也读不到,就照实告诉用户这一版没法压,别自己猜轮号。') +
|
|
@@ -1385,6 +1385,7 @@ function compressTool(ctx, config, log, foldQueues, reminded) {
|
|
|
1385
1385
|
'l0 的产物是 L1 块,l1 的产物是 L2 块,l2 的产物是 L3 块(落在 L3 区尾部,旧 L3 块原样不动),l3 把整个 L3 区压成一块(唯一不写 to 的键)。' +
|
|
1386
1386
|
'压缩是自下往上长的:哪一层还是空的,就不能压它上面那一层。' +
|
|
1387
1387
|
'压哪个键,就必须同批把比它浅、且还能压的层一起压掉,只动深层会整批被拒(某层不足 2 块、或只剩被保护的最后几轮时不用给)。' +
|
|
1388
|
+
'to 只写一个锚点:写最小的号 = 只压最老的一段;写最大的号 = 一次把可压段整段压掉(可压段只是"已结束、不在保护圈",不代表里面的事都做完了),通常不是默认选择——要压就压已经完成的部分,压到第一个还没收尾的轮次之前就停。' +
|
|
1388
1389
|
'别压还没做完的轮次:那一轮里的事还在进行(目标还没收尾、todo 没打完、还在验证、等你拍板)就先留着——压掉之后你只剩自己写的那几行摘要。' +
|
|
1389
1390
|
'填表按时间线顺序,最老的一层先写:l3 → l2 → l1 → l0。' +
|
|
1390
1391
|
'一次调用可以给多个键,一起给只断一次前缀缓存,所以攒够了一次压。' +
|
|
@@ -1397,11 +1398,11 @@ function compressTool(ctx, config, log, foldQueues, reminded) {
|
|
|
1397
1398
|
properties: {
|
|
1398
1399
|
l3: blockSpec('把整个 L3 区压成一块(不写 to)', false, titleLimit, true),
|
|
1399
1400
|
l2: blockSpec('压 L2 区最老的一块或几块,到 to 指定的那块为止', true, titleLimit, false,
|
|
1400
|
-
'写并到第几个块:1 = 该区最老的块,最多到「块数 − 1
|
|
1401
|
+
'写并到第几个块:1 = 该区最老的块,最多到「块数 − 1」。从地图「可压位置」里抄一个序号,不能写成区间。'),
|
|
1401
1402
|
l1: blockSpec('压 L1 区最老的一块或几块,到 to 指定的那块为止', true, titleLimit, false,
|
|
1402
|
-
'写并到第几个块:1 = 该区最老的块,最多到「块数 − 1
|
|
1403
|
+
'写并到第几个块:1 = 该区最老的块,最多到「块数 − 1」。从地图「可压位置」里抄一个序号,不能写成区间。'),
|
|
1403
1404
|
l0: blockSpec('压原文区最旧的一段', true, titleLimit, false,
|
|
1404
|
-
'
|
|
1405
|
+
'写到哪一轮为止:从地图「可压位置」里抄一个 T 号(只抄一个,不能写成区间;压到那轮结束,不会切到轮中);只能写已经结束、且不在 keepTurns 里的轮。'),
|
|
1405
1406
|
},
|
|
1406
1407
|
},
|
|
1407
1408
|
output: textOutput(),
|
|
@@ -1447,7 +1448,10 @@ function compressTool(ctx, config, log, foldQueues, reminded) {
|
|
|
1447
1448
|
// 代价(450–690 token/次)是设计上认过的账,不许拿"省 token"当理由改成紧凑版。
|
|
1448
1449
|
const mapText = fullMap(ctx, agent, config, log)
|
|
1449
1450
|
if (mapText) issueFor(ctx, agent, log)
|
|
1450
|
-
return {
|
|
1451
|
+
return {
|
|
1452
|
+
text: `没能压缩:${result.reason}${landed}` +
|
|
1453
|
+
(mapText ? '\n\n' + mapText : '\n\n(地图刷新失败:先调 context_map,再按它给的可压位置重试)'),
|
|
1454
|
+
}
|
|
1451
1455
|
}
|
|
1452
1456
|
log('compress-ok', { keys: result.results.map((r) => r.key), prunedChars: result.prunedChars })
|
|
1453
1457
|
// 登记待折叠:真正的替换发生在下一次 pre-step(那时工具返回才存在)。
|
package/lib/blocks.js
CHANGED
|
@@ -687,7 +687,7 @@ export function readZones(ctx, session, agent, log) {
|
|
|
687
687
|
// 数字 -> 该区从 1 起的序号(l1 / l2 用;l3 不收)
|
|
688
688
|
export function resolveTurnAnchor(zones, label, keepTurns = 0) {
|
|
689
689
|
const m = /^T(\d+)$/.exec(String(label || '').trim())
|
|
690
|
-
if (!m) return { ok: false, reason: '
|
|
690
|
+
if (!m) return { ok: false, reason: '锚点写法不对:轮号要写成 T 加数字(真实轮号从地图「可压位置」抄),不要只写数字' }
|
|
691
691
|
const turn = Number(m[1])
|
|
692
692
|
if (zones.turnStarts === null) {
|
|
693
693
|
return {
|
package/lib/compress.js
CHANGED
|
@@ -22,9 +22,9 @@ const KEYS = ['l3', 'l2', 'l1', 'l0']
|
|
|
22
22
|
const FIELD_NAMES = ['title', 'summary', 'to']
|
|
23
23
|
|
|
24
24
|
// 填表格式校验:格式错就一次性说清楚错在哪、该长什么样,别让模型猜。
|
|
25
|
-
function checkShape(args) {
|
|
25
|
+
export function checkShape(args) {
|
|
26
26
|
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
27
|
-
return { ok: false, reason: '参数必须是对象,形如 {"l0":{"title":"…","summary":"…","to":"
|
|
27
|
+
return { ok: false, reason: '参数必须是对象,形如 {"l0":{"title":"…","summary":"…","to":"…"},"l1":{"title":"…","summary":"…","to":"…"}}(l0 的 to 是轮号、照地图「可压位置」抄,l1/l2 的 to 是块序号,l3 不写 to)' }
|
|
28
28
|
}
|
|
29
29
|
const unknown = Object.keys(args).filter((k) => !KEYS.includes(k))
|
|
30
30
|
if (unknown.length) {
|
|
@@ -52,12 +52,19 @@ function checkShape(args) {
|
|
|
52
52
|
if (to !== undefined && typeof to !== 'string') {
|
|
53
53
|
return {
|
|
54
54
|
ok: false,
|
|
55
|
-
reason: `${key}.to 要写成字符串:l0
|
|
55
|
+
reason: `${key}.to 要写成字符串:l0 写轮号(照地图「可压位置」抄),l1/l2 写块序号(形如 3);l3 不写 to`,
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
if (key === 'l3' && to !== undefined) return { ok: false, reason: 'l3 不写 to:它一次把整个 L3 区压成一块' }
|
|
59
59
|
if (key !== 'l3' && to === undefined) {
|
|
60
|
-
return { ok: false, reason: `${key} 必须写 to:${key === 'l0' ? '
|
|
60
|
+
return { ok: false, reason: `${key} 必须写 to:${key === 'l0' ? '轮号——照地图「可压位置」里的 T 号写' : '该区从 1 起的块序号——照地图「可压位置」抄'}` }
|
|
61
|
+
}
|
|
62
|
+
// 地图里「可写 to 的轮号:T99…T102」/「可写 to 的序号:1…6」是"可选范围"(写其中任意一个号),
|
|
63
|
+
// 不是让照抄整段区间。另一个会话的模型实测把区间整段抄进 to;这里提前给一句"应该写哪个"。
|
|
64
|
+
// 分隔符既认旧写法(–、~、至、到),也认地图现在用的省略号(…),以及连写两个的「……」。
|
|
65
|
+
const range = /^\s*[Tt]?(\d+)\s*[-–—~~至到…]{1,2}\s*[Tt]?(\d+)\s*$/.exec(String(to).trim())
|
|
66
|
+
if (range) {
|
|
67
|
+
return { ok: false, reason: `${key}.to 只写一个${key === 'l0' ? '轮号' : '块序号'},不要写区间「${String(to).trim()}」——这里应写 ${key === 'l0' ? 'T' : ''}${range[2]}` }
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
return { ok: true }
|
package/lib/map.js
CHANGED
|
@@ -104,14 +104,14 @@ export function renderMap(ctx, agent, config, usage = null) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
lines.push('')
|
|
107
|
-
lines.push('可压位置(to
|
|
107
|
+
lines.push('可压位置(to 就从这里抄,别自己数;下面给的是可选范围,to 只写其中一个号,别照抄成区间)')
|
|
108
108
|
if (zones.ambiguous) {
|
|
109
109
|
lines.push(
|
|
110
110
|
'这张地图只供查看:表层里有内容为空的步骤(宿主把它们跳过了),而表层上已经有压缩块,' +
|
|
111
111
|
'块的位置推不出来。这一版整批拒绝压缩,免得把旧摘要再压成一份新的。别用宿主自带的压缩顶替(那会把分层拍平)'
|
|
112
112
|
)
|
|
113
|
-
// 下面每一行都是"可写 to
|
|
114
|
-
// 同一张地图里既说"压不了"又说"可写 to
|
|
113
|
+
// 下面每一行都是"可写 to 的轮号/序号",在这个状态下它们全是假话(compress 会整批拒绝),
|
|
114
|
+
// 同一张地图里既说"压不了"又说"可写 to 的轮号:T16"只会把模型骗去撞一次拒绝。直接收尾。
|
|
115
115
|
return { text: lines.join('\n'), zones }
|
|
116
116
|
}
|
|
117
117
|
const keepTurns = Math.max(0, Number(config.keepTurns) || 0)
|
|
@@ -162,7 +162,7 @@ export function renderMap(ctx, agent, config, usage = null) {
|
|
|
162
162
|
? `暂不可用:keepTurns=${keepTurns} 把现有轮次全保住了(当前才第 ${zones.currentTurn ?? '?'} 轮),可压范围是空的;把 keepTurns 调小或等轮次多了再压`
|
|
163
163
|
: rawTurnList.length
|
|
164
164
|
? (worthCompressing(zones, 'l0', keepTurns)
|
|
165
|
-
? `可写 to
|
|
165
|
+
? `可写 to 的轮号:T${oldest}${writable > oldest ? `…T${writable}` : ''}(原文区最老的是 T${oldest},当前第 ${zones.currentTurn ?? '?'} 轮${kept};写哪个号就压到哪个号,to 只写一个号、别写成区间;只压已经收尾、结果已落地的内容,还在推进或接下来还要用的留着);` +
|
|
166
166
|
`落在表层上的轮:${rawTurnShown}${rawTurnMore}。` +
|
|
167
167
|
(missingInRange.length
|
|
168
168
|
? `缺的轮号 ${missingInRange.map((t) => `T${t}`).join('、')} 写不得——它们没有 ` +
|
|
@@ -172,8 +172,8 @@ export function renderMap(ctx, agent, config, usage = null) {
|
|
|
172
172
|
? (Number.isFinite(oldestCost) && oldestCost < MIN_L0_COST
|
|
173
173
|
// 只给两端的事实:最早那一轮自己压不动,是因为它连块头开销都盖不住。
|
|
174
174
|
// 不写"该并到哪一轮"——那等于替模型选区间(SPEC §6:只陈述事实)。
|
|
175
|
-
? `最早那一轮(T${oldest})单独是 ${oldestCost} token(${MIN_L0_COST} 是整段的叫醒门槛,不管单轮;能不能压要看地板 ${productFloor('l0', config)}
|
|
176
|
-
: `T${oldest} 单独是 ${oldestCost} token
|
|
175
|
+
? `最早那一轮(T${oldest})单独是 ${oldestCost} token(${MIN_L0_COST} 是整段的叫醒门槛,不管单轮;能不能压要看地板 ${productFloor('l0', config)});写到 T${writable} 会连压整段合计 ${Number.isFinite(totalL0Cost) ? totalL0Cost : '?'} token,细节只留在摘要里`
|
|
176
|
+
: `T${oldest} 单独是 ${oldestCost} token;写到 T${writable} 会连压整段合计 ${Number.isFinite(totalL0Cost) ? totalL0Cost : '?'} token,细节只留在摘要里`)
|
|
177
177
|
: `只写 T${oldest}:被压 ${oldestCost === null ? '?' : oldestCost} token`)
|
|
178
178
|
// 压完必须比被压内容小(§4.7):剩下这一小段连块头开销都盖不住时,
|
|
179
179
|
// 地图先说"压不了",别把模型叫来压一次然后被拒(口径一致)。
|
|
@@ -200,9 +200,19 @@ export function renderMap(ctx, agent, config, usage = null) {
|
|
|
200
200
|
// 两边的事实:可压段一共多少 token、以及留给"摘要+标题"的剩余预算。
|
|
201
201
|
// 后者 = 可压段 − 一块的最小产物成本(摘要≈0 时的开销),所以模型写得比它长就过不了收益校验。
|
|
202
202
|
const budgetOf = (cost, key) => Math.max(0, cost - floorTok(key))
|
|
203
|
+
// 产物层的单块上限(buildOp 里 summaryTokens > cap 会直接拒):l1 压出的是 L2 块(2000),
|
|
204
|
+
// 只报"按收益算的预算"会给出一个根本写不出来的数(实测 5406 对 L2 上限 2000)。
|
|
205
|
+
const productCap = (key) => config.segment[key === 'l0' ? 'l1' : key === 'l1' ? 'l2' : 'l3']
|
|
206
|
+
const productLayerName = (key) => (key === 'l0' ? 'l1' : key === 'l1' ? 'l2' : 'l3').toUpperCase()
|
|
203
207
|
const tooSmall = (cost, key) =>
|
|
204
208
|
`暂不可用:可压的块合计 ${cost} token,盖不住一块的最小产物成本(约 ${floorTok(key)} token),压完反而更大`
|
|
205
|
-
const budgetNote = (cost, key) =>
|
|
209
|
+
const budgetNote = (cost, key) => {
|
|
210
|
+
const byRatio = budgetOf(cost, key)
|
|
211
|
+
const cap = productCap(key)
|
|
212
|
+
return byRatio > cap
|
|
213
|
+
? `这一段合计 ${cost} token;摘要正文不超过 ${cap}(${productLayerName(key)} 单块上限),摘要+标题合计不超过 ${byRatio} token 才过收益校验`
|
|
214
|
+
: `这一段合计 ${cost} token;摘要+标题合计不超过 ${byRatio} token 才过收益校验`
|
|
215
|
+
}
|
|
206
216
|
// 只并第一块时的预算:整段的数字对 to=1 是虚构的(97 的块配不下 119 的预算)。
|
|
207
217
|
const firstOnly = (key) => {
|
|
208
218
|
const list = zones.zones[key] || []
|
|
@@ -214,7 +224,9 @@ export function renderMap(ctx, agent, config, usage = null) {
|
|
|
214
224
|
const cost = firstOnly(key)
|
|
215
225
|
if (!cost) return ''
|
|
216
226
|
return cost >= floorTok(key)
|
|
217
|
-
|
|
227
|
+
// to=1 的产物同样是产物层的块:预算要按单块上限截断,否则首块很大时会给一个写不出来的数
|
|
228
|
+
// (qwen 快审 1163c1a 时实测:首块 4200 字 → 给 2390,实际 L2 上限 2000)。
|
|
229
|
+
? `to=1 只并最老的一块(${cost} token,够改写开销;摘要+标题 ≤ ${Math.min(budgetOf(cost, key), productCap(key))})`
|
|
218
230
|
: `to=1 只并最老的一块(${cost} token,单独写必被拒)`
|
|
219
231
|
}
|
|
220
232
|
lines.push(
|
|
@@ -223,7 +235,7 @@ export function renderMap(ctx, agent, config, usage = null) {
|
|
|
223
235
|
`l1 压 L1 区最老的${maxL1 >= 1 ? ` 1–${maxL1} 块` : '一段'} -> ` +
|
|
224
236
|
(maxL1 >= 1
|
|
225
237
|
? (costL1 >= floorTok('l1')
|
|
226
|
-
? `${firstOk('l1') || maxL1 < 2 ? `可写 to
|
|
238
|
+
? `${firstOk('l1') || maxL1 < 2 ? `可写 to 的序号:1${maxL1 > 1 ? `…${maxL1}` : ''}` : `可写 to 的序号:2${maxL1 > 2 ? `…${maxL1}` : ''}(to=1 单独不可行)`}(该区共 ${zones.zones.l1.length} 块,1 最老;最后一块不能压;to 只写一个序号、别写成区间)。` +
|
|
227
239
|
`${firstNote('l1')};to=${maxL1} 一次并 ${maxL1} 块。${budgetNote(costL1, 'l1')}`
|
|
228
240
|
: tooSmall(costL1, 'l1'))
|
|
229
241
|
: '暂不可用:L1 区不足 2 块(最后一块不能压)')
|
|
@@ -232,7 +244,7 @@ export function renderMap(ctx, agent, config, usage = null) {
|
|
|
232
244
|
`l2 压 L2 区最老的${maxL2 >= 1 ? ` 1–${maxL2} 块` : '一段'}(产物是新的 L3 块,旧 L3 不动)-> ` +
|
|
233
245
|
(maxL2 >= 1
|
|
234
246
|
? (costL2 >= floorTok('l2')
|
|
235
|
-
? `${firstOk('l2') || maxL2 < 2 ? `可写 to
|
|
247
|
+
? `${firstOk('l2') || maxL2 < 2 ? `可写 to 的序号:1${maxL2 > 1 ? `…${maxL2}` : ''}` : `可写 to 的序号:2${maxL2 > 2 ? `…${maxL2}` : ''}(to=1 单独不可行)`}(该区共 ${zones.zones.l2.length} 块,1 最老;最后一块不能压;to 只写一个序号、别写成区间)。` +
|
|
236
248
|
`${firstNote('l2')};to=${maxL2} 一次并 ${maxL2} 块。${budgetNote(costL2, 'l2')}`
|
|
237
249
|
: tooSmall(costL2, 'l2'))
|
|
238
250
|
: '暂不可用:L2 区不足 2 块(最后一块不能压)')
|
|
@@ -244,7 +256,7 @@ export function renderMap(ctx, agent, config, usage = null) {
|
|
|
244
256
|
|
|
245
257
|
lines.push('')
|
|
246
258
|
lines.push('压缩须知')
|
|
247
|
-
lines.push('·
|
|
259
|
+
lines.push('· 只压已经结束、结果已经落地的轮次;当前轮和还需要的轮绝对不可压——压掉它们等于把正在做的工作从上下文里删掉。不要一压到底:压到第一个还没收尾的轮次之前就停,拿不准是否收尾就按没收尾处理、留着。地图列出的最大号只是上界,不是默认值:要压就压已经完成的部分。')
|
|
248
260
|
lines.push('· 压缩专用轮:压完就收手——不要越过上一轮还没收尾的未决项,也不要在压缩成功后转做其它任务;未决的事原样留着。')
|
|
249
261
|
lines.push('· 级联:压 l1/l2/l3 时,比它浅、且还能压的层要同批一起给,只给深层会整批被拒。')
|
|
250
262
|
lines.push('· 压完必须比被压掉的内容小,不达标整批不写并返回两侧实测值;区间取多大由你权衡(越大省得越多、细节留得越少)。')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ebbing-context",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"locale/*.json",
|
|
22
22
|
"icon.svg",
|
|
23
23
|
"README.md",
|
|
24
|
+
"README.en.md",
|
|
24
25
|
"CHANGELOG.md"
|
|
25
26
|
],
|
|
26
27
|
"keywords": [
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
"node": ">=20"
|
|
52
53
|
},
|
|
53
54
|
"scripts": {
|
|
54
|
-
"test": "node test/blocks.test.mjs && node test/compress.test.mjs && node test/compress-e2e.test.mjs && node test/callfold.test.mjs && node test/client.test.mjs && node test/config.test.mjs && node test/map.test.mjs && node test/prestep.test.mjs && node test/usage.test.mjs && node test/scope.test.mjs && node test/imageprice.test.mjs && node test/log.test.mjs && node test/state.test.mjs",
|
|
55
|
+
"test": "node test/blocks.test.mjs && node test/compress.test.mjs && node test/compress-e2e.test.mjs && node test/callfold.test.mjs && node test/client.test.mjs && node test/client-render.test.mjs && node test/config.test.mjs && node test/map.test.mjs && node test/prestep.test.mjs && node test/usage.test.mjs && node test/scope.test.mjs && node test/imageprice.test.mjs && node test/log.test.mjs && node test/state.test.mjs",
|
|
55
56
|
"check": "node --check index.js && node --check client.js && node --check lib/blocks.js && node --check lib/callfold.js && node --check lib/compress.js && node --check lib/config.js && node --check lib/license.js && node --check lib/log.js && node --check lib/map.js && node --check lib/messages.js && node --check lib/tokens.js && node --check lib/imageprice.js && node --check lib/state.js",
|
|
56
57
|
"pack": "node -e \"require('node:fs').mkdirSync('dist',{recursive:true})\" && npm pack --pack-destination dist",
|
|
57
58
|
"prepublishOnly": "npm run check && npm test"
|