dsh-ebbing-context 0.2.8 → 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 +2 -2
- package/README.md +2 -2
- package/client.js +105 -40
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
Binary file
|
package/README.en.md
CHANGED
|
@@ -64,13 +64,13 @@ After installing, confirm three things: the plugin card shows **running**, both
|
|
|
64
64
|
|
|
65
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
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
|
|
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
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
71
71
|
## Configuration
|
|
72
72
|
|
|
73
|
-
17 items (`triggerPct`, `keepTurns`, `pruneFirst`, `foldCallRecord`, `requireSessionQuery`, `reservedTokens`, `budget.*`, `segment.*`, `title.maxTokens`, `notice.*`), edited in the plugin
|
|
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
74
|
|
|
75
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
76
|
|
package/README.md
CHANGED
|
@@ -64,13 +64,13 @@ dsh plugin add ./dsh-ebbing-context
|
|
|
64
64
|
|
|
65
65
|
1. 打开 profile 的 `cordis.patch.yml`,删掉 `dsh-ebbing-context` 那一节(包括面板里改过的 `config`)。不删的话,下一次启动会因为"patch 指向的条目不存在"在 stderr 刷一条警告——不会坏,但会一直刷。
|
|
66
66
|
2. 日志在 `$DSH_HOME/logs/ebbing-context.log`(滚动时还有 `.1` / `.2`),卸载不会自动删,确认不需要排查后手动删掉即可。
|
|
67
|
-
**建议关掉宿主自带的自动压缩**(`compaction-basic` 的 `auto`,默认 80%
|
|
67
|
+
**建议关掉宿主自带的自动压缩**(`compaction-basic` 的 `auto`,默认 80% 触发)。它一旦触发会把多层摘要重新拍平,分层结构就没了(原文还在会话日志里,可按事件号读回)。插件照样能压,只是效果打折。关的位置取决于你的安装方式与预设。
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
71
71
|
## 配置
|
|
72
72
|
|
|
73
|
-
共 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)或插件行详情页里改,改完点「保存」才生效。
|
|
74
74
|
|
|
75
75
|
默认值、含义与拒绝规则见 [docs/SPEC.md 第 6 节](https://github.com/Light-EI/dsh-ebbing-context/blob/main/docs/SPEC.md#6-配置17-项)。
|
|
76
76
|
|
package/client.js
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
// 浏览器侧:在「插件」面板的这个 bundle 页里画配置表单。
|
|
2
2
|
// 保存走宿主给的 form.mutate(一次提交 = 一个 revision = 一次校验),写回 profile 的 cordis.patch.yml。
|
|
3
3
|
// 这里不做任何读文件、不调用模型;只负责把可设置项画出来。
|
|
4
|
-
//
|
|
4
|
+
// 界面语言跟随宿主:中文环境只显示中文、英文环境只显示英文(两套文案都要维护,见维护手册 §1.30)。
|
|
5
5
|
|
|
6
6
|
window.__ModuleLoader__.load({
|
|
7
7
|
id: 'dsh-ebbing-context',
|
|
8
8
|
factory(require) {
|
|
9
9
|
const React = require('react')
|
|
10
10
|
const h = React.createElement
|
|
11
|
-
const { useState, useEffect } = React
|
|
11
|
+
const { useState, useEffect, useMemo } = React
|
|
12
12
|
|
|
13
13
|
const TEXT = {
|
|
14
14
|
zh: {
|
|
15
|
-
title: '
|
|
15
|
+
title: 'EC上下文设置',
|
|
16
16
|
hint: '改完点最下面的「保存」;离开页面不保存。',
|
|
17
17
|
loading: '正在读取配置…',
|
|
18
18
|
unavailable: '当前客户端读不到这个插件的配置。',
|
|
19
19
|
readonly: '当前 profile 不允许写入配置。',
|
|
20
|
-
noForm: '这个插件没有声明可编辑的配置。',
|
|
21
20
|
// 宿主侧插件没启动时,面板拿不到 form,页面会永远停在「正在读取配置…」。
|
|
22
21
|
// 那种情况下必须有一句能指向原因的话,否则用户只看到一片空白。
|
|
23
22
|
late: '一直停在这里,说明宿主侧的插件没有启动起来。DSH 日志里会有一条加载错误(搜 import),插件卡片的状态也会停在「未运行」而不是「运行中」。',
|
|
@@ -38,12 +37,11 @@ window.__ModuleLoader__.load({
|
|
|
38
37
|
note: '使用注意:如果这套 DSH 开着自带的自动压缩(compaction-basic 的自动压缩,默认在上下文用到约 80% 时自己触发),建议关掉它。它一旦触发,会把这里的多层摘要重新拍平成一段总摘要,分层结构就没了——详细内容要从会话日志里按事件号翻回来,模型不会再看到这些块;插件本身不受影响、照常能压,但效果会打折扣。要关的是「自动压缩」这个行为(通常是一个 auto 之类的开关,或直接停用那个插件),不关也行、只是效果打折。具体在哪一行、怎么改取决于你的安装方式与预设,插件不会替你改宿主配置。不想自己找的话,可以把这句发给你的助手或模型:「帮我关掉 DeepSeek Harness 自带的自动压缩(compaction-basic 的 auto),改完告诉我你动了哪个文件、怎么改回去」,让它去查;也可以先问问它这个功能是做什么的再决定。',
|
|
39
38
|
},
|
|
40
39
|
en: {
|
|
41
|
-
title: 'Context
|
|
40
|
+
title: 'Ebbing Context',
|
|
42
41
|
hint: 'Click "Save" at the bottom when done; leaving this page discards changes.',
|
|
43
42
|
loading: 'Reading configuration…',
|
|
44
43
|
unavailable: 'This client cannot read this plugin\'s configuration.',
|
|
45
44
|
readonly: 'This profile does not allow configuration writes.',
|
|
46
|
-
noForm: 'This plugin declares no editable configuration.',
|
|
47
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".',
|
|
48
46
|
save: 'Save',
|
|
49
47
|
saving: 'Saving…',
|
|
@@ -59,6 +57,19 @@ window.__ModuleLoader__.load({
|
|
|
59
57
|
},
|
|
60
58
|
}
|
|
61
59
|
|
|
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
|
+
|
|
62
73
|
// 每一项:分组、标签、路径、类型、范围、详情(中英各一份,行为完全一致)
|
|
63
74
|
const FIELDS = [
|
|
64
75
|
{
|
|
@@ -107,7 +118,7 @@ window.__ModuleLoader__.load({
|
|
|
107
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.',
|
|
108
119
|
},
|
|
109
120
|
{
|
|
110
|
-
group: '提醒', groupEn: 'Reminders', label: '
|
|
121
|
+
group: '提醒', groupEn: 'Reminders', label: '模型主动拒绝之后门槛重设到原额的百分比', labelEn: 'Threshold reset after the model declines (%)', path: ['notice', 'growthResetPct'], kind: 'number', step: 10, min: 0, max: 100,
|
|
111
122
|
detail: '增长提醒发出去之后,模型如果决定这一轮不压(内容还没做完、或者比较重要),下一个门槛就按原额的百分之多少重设。40(默认)= 原额 65000 的话,再长 26000 才重新触发;100 = 全额重攒;0 = 门槛降到 1 token,下一轮只要再长一点就重新触发。只影响「又长了」这条触发,不影响到线提醒。增长到量但一层都压不动时走另一条路:门槛固定降到一半、基线前移,再长半个门槛重新判一次,直到某次真压得动为止。',
|
|
112
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.',
|
|
113
124
|
},
|
|
@@ -148,6 +159,11 @@ window.__ModuleLoader__.load({
|
|
|
148
159
|
},
|
|
149
160
|
]
|
|
150
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
|
+
|
|
151
167
|
function readPath(obj, path) {
|
|
152
168
|
let cur = obj
|
|
153
169
|
for (const k of path) {
|
|
@@ -168,9 +184,7 @@ window.__ModuleLoader__.load({
|
|
|
168
184
|
borderTop: '1px solid color-mix(in srgb, currentColor 12%, transparent)',
|
|
169
185
|
}
|
|
170
186
|
const labelStyle = { fontSize: '13px', fontWeight: 600, lineHeight: '20px' }
|
|
171
|
-
const labelEnStyle = { fontSize: '12px', fontWeight: 400, opacity: 0.55 }
|
|
172
187
|
const detailStyle = { fontSize: '12px', opacity: 0.7, marginTop: '6px', lineHeight: '18px', maxWidth: '640px' }
|
|
173
|
-
const detailEnStyle = { fontSize: '11px', opacity: 0.5, marginTop: '4px', lineHeight: '17px', maxWidth: '640px' }
|
|
174
188
|
const inputStyle = {
|
|
175
189
|
width: '104px',
|
|
176
190
|
padding: '4px 8px',
|
|
@@ -183,7 +197,6 @@ window.__ModuleLoader__.load({
|
|
|
183
197
|
outline: 'none',
|
|
184
198
|
}
|
|
185
199
|
const groupStyle = { fontSize: '12px', fontWeight: 600, opacity: 0.55, marginTop: '18px', marginBottom: '2px' }
|
|
186
|
-
const groupEnStyle = { fontWeight: 400, opacity: 0.75 }
|
|
187
200
|
// 使用注意:常驻、不折叠、不和错误横幅抢位置——它讲的是"会让插件效果打折"的既有事实。
|
|
188
201
|
const noteStyle = {
|
|
189
202
|
fontSize: '12px',
|
|
@@ -196,7 +209,6 @@ window.__ModuleLoader__.load({
|
|
|
196
209
|
background: 'color-mix(in srgb, currentColor 5%, transparent)',
|
|
197
210
|
opacity: 0.85,
|
|
198
211
|
}
|
|
199
|
-
const noteEnStyle = { fontSize: '11px', lineHeight: '17px', marginTop: '6px', opacity: 0.65 }
|
|
200
212
|
const linkStyle = {
|
|
201
213
|
fontSize: '12px', opacity: 0.7, background: 'none', border: 'none',
|
|
202
214
|
padding: 0, marginLeft: '8px', cursor: 'pointer', color: 'inherit', textDecoration: 'underline',
|
|
@@ -221,13 +233,6 @@ window.__ModuleLoader__.load({
|
|
|
221
233
|
border: '1px solid ' + (kind === 'error' ? 'color-mix(in srgb, #d33 45%, transparent)' : 'color-mix(in srgb, currentColor 18%, transparent)'),
|
|
222
234
|
background: kind === 'error' ? 'color-mix(in srgb, #d33 10%, transparent)' : 'color-mix(in srgb, currentColor 5%, transparent)',
|
|
223
235
|
})
|
|
224
|
-
const bannerEnStyle = { fontSize: '11px', lineHeight: '17px', marginTop: '3px', opacity: 0.7 }
|
|
225
|
-
|
|
226
|
-
// 两块文字(中文在上、英文在下)。用于长说明、按钮提示、错误横幅。
|
|
227
|
-
const biText = (zhText, enText, enStyle) => h('div', null,
|
|
228
|
-
h('div', null, zhText),
|
|
229
|
-
h('div', { style: { marginTop: '2px', ...(enStyle || { opacity: 0.6 }) } }, enText),
|
|
230
|
-
)
|
|
231
236
|
|
|
232
237
|
function ConfigPage(props) {
|
|
233
238
|
const { view, form } = props
|
|
@@ -237,6 +242,12 @@ window.__ModuleLoader__.load({
|
|
|
237
242
|
const [busy, setBusy] = useState(false)
|
|
238
243
|
// 「宿主侧根本没起来」和「配置还在路上」在界面上长得一样,靠时间把它们分开。
|
|
239
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
|
+
}, [])
|
|
240
251
|
|
|
241
252
|
useEffect(() => {
|
|
242
253
|
if (form) return undefined
|
|
@@ -244,6 +255,11 @@ window.__ModuleLoader__.load({
|
|
|
244
255
|
return () => clearTimeout(timer)
|
|
245
256
|
}, [form])
|
|
246
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
|
+
|
|
247
263
|
const st = (form && form.state) || {}
|
|
248
264
|
const revision = st.revision
|
|
249
265
|
|
|
@@ -254,13 +270,13 @@ window.__ModuleLoader__.load({
|
|
|
254
270
|
// 那是"还没读到",不是"没有配置项",别骗用户说没声明。
|
|
255
271
|
if (!form) {
|
|
256
272
|
return h('div', { style: { fontSize: '13px', opacity: 0.7, lineHeight: 1.6 } },
|
|
257
|
-
|
|
273
|
+
late ? T.late : T.loading)
|
|
258
274
|
}
|
|
259
275
|
if (st.status === 'loading') {
|
|
260
|
-
return h('div', { style: { fontSize: '13px', opacity: 0.7 } },
|
|
276
|
+
return h('div', { style: { fontSize: '13px', opacity: 0.7 } }, T.loading)
|
|
261
277
|
}
|
|
262
278
|
if (st.status === 'unavailable') {
|
|
263
|
-
return h('div', { style: { fontSize: '13px', opacity: 0.7 } },
|
|
279
|
+
return h('div', { style: { fontSize: '13px', opacity: 0.7 } }, T.unavailable)
|
|
264
280
|
}
|
|
265
281
|
|
|
266
282
|
const value = st.value || {}
|
|
@@ -379,33 +395,25 @@ window.__ModuleLoader__.load({
|
|
|
379
395
|
}
|
|
380
396
|
|
|
381
397
|
return h('div', { style: { fontSize: '13px', color: 'inherit' } },
|
|
382
|
-
h('div', { style: { fontSize: '14px', fontWeight: 600 } },
|
|
383
|
-
h('div', { style: { fontSize: '12px', fontWeight: 500, opacity: 0.6, marginTop: '2px' } }, TEXT.en.title),
|
|
398
|
+
h('div', { style: { fontSize: '14px', fontWeight: 600 } }, pageTitle()),
|
|
384
399
|
h('div', { style: { ...detailStyle, marginTop: '2px', marginBottom: '8px' } },
|
|
385
|
-
|
|
386
|
-
h('div', { style: noteStyle },
|
|
387
|
-
|
|
388
|
-
h('div', { style: noteEnStyle }, TEXT.en.note)),
|
|
389
|
-
banner ? h('div', { style: bannerStyle(banner.kind) },
|
|
390
|
-
h('div', null, banner.zh),
|
|
391
|
-
h('div', { style: bannerEnStyle }, banner.en)) : null,
|
|
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,
|
|
392
403
|
groups.map((g) => h('div', { key: g.name },
|
|
393
|
-
h('div', { style: groupStyle }, g.name
|
|
404
|
+
h('div', { style: groupStyle }, zh ? g.name : g.nameEn),
|
|
394
405
|
g.items.map((item) => {
|
|
395
406
|
const k = keyOf(item)
|
|
396
407
|
return h('div', { key: k },
|
|
397
408
|
h('div', { style: rowStyle },
|
|
398
409
|
h('div', null,
|
|
399
|
-
h('span', { style: labelStyle }, item.label),
|
|
400
|
-
h('span', { style: labelEnStyle }, ' / ' + item.labelEn),
|
|
410
|
+
h('span', { style: labelStyle }, zh ? item.label : item.labelEn),
|
|
401
411
|
h('button', {
|
|
402
412
|
type: 'button',
|
|
403
413
|
style: linkStyle,
|
|
404
414
|
onClick: () => setOpen((prev) => ({ ...prev, [k]: !prev[k] })),
|
|
405
|
-
},
|
|
406
|
-
open[k] ? h('div', null,
|
|
407
|
-
h('div', { style: detailStyle }, item.detail),
|
|
408
|
-
h('div', { style: detailEnStyle }, item.detailEn)) : null,
|
|
415
|
+
}, open[k] ? T.hide : T.detail),
|
|
416
|
+
open[k] ? h('div', { style: detailStyle }, zh ? item.detail : item.detailEn) : null,
|
|
409
417
|
),
|
|
410
418
|
control(item),
|
|
411
419
|
),
|
|
@@ -418,26 +426,83 @@ window.__ModuleLoader__.load({
|
|
|
418
426
|
style: buttonStyle(true, readonly || busy),
|
|
419
427
|
disabled: readonly || busy,
|
|
420
428
|
onClick: save,
|
|
421
|
-
},
|
|
429
|
+
}, busy ? T.saving : T.save),
|
|
422
430
|
h('button', {
|
|
423
431
|
type: 'button',
|
|
424
432
|
style: buttonStyle(false, readonly || busy || changed.length === 0),
|
|
425
433
|
disabled: readonly || busy || changed.length === 0,
|
|
426
434
|
onClick: () => { setDraft({}); setBanner(null) },
|
|
427
|
-
},
|
|
435
|
+
}, T.discard),
|
|
428
436
|
h('span', { style: { fontSize: '12px', opacity: 0.6, alignSelf: 'center' } },
|
|
429
|
-
changed.length > 0 ? '待保存 ' + changed.length + ' 项
|
|
437
|
+
changed.length > 0 ? (zh ? '待保存 ' + changed.length + ' 项' : changed.length + ' unsaved') : ''),
|
|
430
438
|
),
|
|
431
439
|
)
|
|
432
440
|
}
|
|
433
441
|
|
|
434
442
|
return {
|
|
443
|
+
// 只硬依赖 slots(两个入口都靠它);configForms / locale 用 ctx.inject 等宿主提供后再接:
|
|
444
|
+
// 缺失时降级——插件行配置页照常工作、设置页不出现、标题退回中文,
|
|
445
|
+
// 不会因为新服务缺失让整个 client 半侧不激活(配置页凭空消失)。
|
|
446
|
+
// 假设宿主 cordis 4 的 ctx.inject 存在(本插件最低宿主即 4;该方法缺失时 apply 会抛)。
|
|
435
447
|
inject: ['slots'],
|
|
436
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
|
+
// 「内置插件」→ 本插件 → 配置:保留。和设置面板里的页编辑同一份配置。
|
|
437
462
|
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
438
463
|
name: 'plugins.row.config',
|
|
439
464
|
key: 'dsh-ebbing-context#dsh-ebbing-context',
|
|
440
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
|
+
})
|
|
441
506
|
},
|
|
442
507
|
}
|
|
443
508
|
},
|