@starwish-ai/xingyuan-dsh 0.5.2 → 0.5.3
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 +93 -0
- package/lib/client/i18n.js +6 -0
- package/lib/client/index.js +5 -3
- package/lib/client/pages/settings.js +73 -21
- package/lib/client.js +100 -17
- package/lib/domain.js +2 -1
- package/lib/index.js +6 -1
- package/lib/pref-policy.js +38 -0
- package/lib/pref-settings.js +27 -0
- package/lib/preset/side.js +12 -23
- package/lib/preset/tools.js +16 -5
- package/lib/types/client/i18n.d.ts +3 -0
- package/lib/types/client/pages/settings.d.ts +11 -6
- package/lib/types/domain.d.ts +4 -1
- package/lib/types/pref-policy.d.ts +34 -0
- package/lib/types/pref-settings.d.ts +34 -0
- package/lib/types/preset/side.d.ts +6 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,99 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.3] - 2026-08-28
|
|
9
|
+
|
|
10
|
+
> **Note:** this is a patch release but it carries one breaking change — the `xingyuan`
|
|
11
|
+
> settings namespace is replaced by `xingyuan-pref` (see *Removed*). Consumers of the
|
|
12
|
+
> `./domain` subpath must also adapt to `makeXingyuanStore()` taking a second, required
|
|
13
|
+
> argument. Read *Upgrading from 0.5.2 or earlier* before updating.
|
|
14
|
+
|
|
15
|
+
**Chat preferences are now always editable.** Settings → XingYuan rendered the
|
|
16
|
+
write-confirmation toggle and the memory-injection limit as permanently disabled until a
|
|
17
|
+
XingYuan session had been opened once since the last dsh start; clicking them did nothing
|
|
18
|
+
and produced no error. Both preferences lived in the preset-layer namespace, which only
|
|
19
|
+
comes into existence when the preset is first mounted, so they now live in a
|
|
20
|
+
bundle-resident namespace registered whenever the bundle row is active.
|
|
21
|
+
|
|
22
|
+
### Upgrading from 0.5.2 or earlier
|
|
23
|
+
|
|
24
|
+
The two chat preferences moved from the `xingyuan` block of `$DSH_HOME/settings.yaml` to a
|
|
25
|
+
new `xingyuan-pref` block. **A value you had customised is not carried over** and falls back
|
|
26
|
+
to the default: `confirmWrites` returns to `true` (the safe direction — write actions ask for
|
|
27
|
+
confirmation again) and `memoryInjectLimit` to `40`. Re-apply your choices under
|
|
28
|
+
**Settings → XingYuan → Chat preferences**. The leftover `xingyuan:` block is inert and is
|
|
29
|
+
safe to delete.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- **`xingyuan-pref` settings namespace** (`src/pref-settings.ts`), installed by the bundle
|
|
33
|
+
layer next to the existing `xingyuan-ui` namespace, carrying `confirmWrites` (default
|
|
34
|
+
`true`) and `memoryInjectLimit` (default `40`, integer `5`–`200`). The schema declares
|
|
35
|
+
`.step(1)`, so the Host also rejects fractional values that a hand-edited settings
|
|
36
|
+
document or a direct RPC write could otherwise smuggle in.
|
|
37
|
+
- **Shared preference policy module** (`src/pref-policy.ts`) holding the bounds, defaults
|
|
38
|
+
and input parser for both halves — the host module depends on
|
|
39
|
+
`@deepseek-ai/dsh-settings` and must never reach the browser bundle. This also removes
|
|
40
|
+
the `5`/`200`/`40` literals that were previously repeated in the settings page. Covered
|
|
41
|
+
by `test/pref-policy.test.ts`.
|
|
42
|
+
- **Write verification**: `scope.set()` resolves rather than rejects on failure (the client
|
|
43
|
+
controller catches, recovers and returns silently), so `toastError` never fired. The page
|
|
44
|
+
now compares the snapshot after each write and raises a "did not save" toast when the
|
|
45
|
+
value did not change. Both controls share one scope — and therefore one write queue — so
|
|
46
|
+
the comparison is gated on a per-component write sequence number to stay honest under
|
|
47
|
+
interleaved writes (see below).
|
|
48
|
+
- **Regression lock** (`test/pref-settings.test.ts`): five tests pinning that the bundle
|
|
49
|
+
layer registers `xingyuan-pref` as soon as the settings service is available, that the
|
|
50
|
+
read thunk resolves fresh on every call, that the headless path falls back to the schema
|
|
51
|
+
defaults, and — structurally — that **no namespace bound by the settings page is ever
|
|
52
|
+
registered from the preset layer**. That last one is the abstract shape of this bug and is
|
|
53
|
+
what stops it from coming back.
|
|
54
|
+
- **Four-way readiness notice** for the preferences card: loading, remote/temporary
|
|
55
|
+
connection (`mode === 'memory'`), namespace not registered, and read-only document. The
|
|
56
|
+
previous code only covered the first two, leaving the rest as silently disabled controls.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
- The two preferences could not be changed before the first XingYuan session of the process
|
|
60
|
+
— the root cause described above.
|
|
61
|
+
- The write-confirmation toggle rendered as **on** whenever its value was unavailable
|
|
62
|
+
(`value?.confirmWrites !== false` reads "unknown" as "true"), which is the worst possible
|
|
63
|
+
default for a safety switch. It now falls back to the schema default explicitly.
|
|
64
|
+
- Namespace-unavailable and read-only states disabled the controls with no explanation.
|
|
65
|
+
- The "did not save" toast could fire on a write that **had** been saved. Both controls share
|
|
66
|
+
one settings scope, and the client controller fences writes with a generation counter: when
|
|
67
|
+
a write is superseded by a newer one it only records a pending revision and does **not**
|
|
68
|
+
fold the result into the snapshot, while the superseded write's `.then` still runs first and
|
|
69
|
+
sees the stale value. Verification is now skipped for superseded writes, and the number
|
|
70
|
+
input got the pending guard the toggle already had.
|
|
71
|
+
- Clearing the memory-injection-limit field and blurring reported "enter an integer between 5
|
|
72
|
+
and 200" while simultaneously refilling the field with the saved value — two contradictory
|
|
73
|
+
messages at once. An empty field is again treated as abandoning the edit.
|
|
74
|
+
- Tool descriptions for the creation / check-in / cancel-check-in tools asserted that a system
|
|
75
|
+
confirmation card would appear. Toggle **Confirm write actions** off and the model kept
|
|
76
|
+
telling users a card had been shown when none was. Tool descriptions are baked in at
|
|
77
|
+
registration and dsh-tools requires a plain string (no getter), so they are now worded to
|
|
78
|
+
hold in both states instead of asserting the card. Delete tools are unaffected — deletes
|
|
79
|
+
always confirm.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
- The preset layer no longer registers a settings namespace. It reads preferences through
|
|
83
|
+
`ctx.xingyuan.prefs()` — a thunk that resolves the current value on every call, so edits
|
|
84
|
+
still take effect immediately without re-registering tools or prompts. The remaining
|
|
85
|
+
`Config` fields (`batchWishLimit`, `chartTrendDays`, …) have no settings UI and stay as
|
|
86
|
+
composition-only entry config, which matches the upstream rule that a namespace should
|
|
87
|
+
carry only the user-editable subset.
|
|
88
|
+
- **Breaking for `./domain` consumers**: `makeXingyuanStore(domain)` now takes a second,
|
|
89
|
+
required argument — `makeXingyuanStore(domain, readPrefs)`, where `readPrefs` is a thunk
|
|
90
|
+
returning the resolved chat preferences. The required-ness is deliberate: every call site
|
|
91
|
+
(one in production, five in tests) is surfaced by the compiler rather than silently
|
|
92
|
+
missing the wiring. `XingyuanStore` gained a matching `prefs()` method.
|
|
93
|
+
|
|
94
|
+
### Removed
|
|
95
|
+
- The preset-layer `xingyuan` settings namespace and the `confirmWrites` /
|
|
96
|
+
`memoryInjectLimit` fields of the preset `Config`. **A value customised under the old
|
|
97
|
+
`xingyuan:` section of `$DSH_HOME/settings.yaml` is no longer read and falls back to the
|
|
98
|
+
default** — `confirmWrites` therefore returns to `true`, the conservative direction. The
|
|
99
|
+
stale section is left in the document and is harmless.
|
|
100
|
+
|
|
8
101
|
## [0.5.2] - 2026-08-27
|
|
9
102
|
|
|
10
103
|
### Added
|
package/lib/client/i18n.js
CHANGED
|
@@ -230,11 +230,14 @@ const ZH_DICT = {
|
|
|
230
230
|
'settings.pref.title': '对话偏好',
|
|
231
231
|
'settings.pref.unavailable': '当前连接不支持持久化偏好(远程或临时模式),以下两项不可用。',
|
|
232
232
|
'settings.pref.loading': '偏好加载中…',
|
|
233
|
+
'settings.pref.notRegistered': '偏好暂不可用(组件未就绪),请稍后重试。',
|
|
234
|
+
'settings.pref.readOnly': '当前设置文档为只读,无法保存修改。',
|
|
233
235
|
'settings.pref.confirmWrites': '写操作二次确认',
|
|
234
236
|
'settings.pref.confirmWritesHint': '创建愿望/任务、打卡、取消打卡时先弹应用内确认卡(删除始终确认);关闭后对话中的这类操作将直接执行。',
|
|
235
237
|
'settings.pref.memoryLimit': '记忆注入上限',
|
|
236
238
|
'settings.pref.memoryLimitHint': '每次对话自动注入上下文的记忆条数上限(5-200,默认 40);失焦后保存。',
|
|
237
239
|
'settings.pref.limitInvalid': '请输入 5-200 的整数',
|
|
240
|
+
'settings.pref.writeFailed': '保存未生效,请重试。',
|
|
238
241
|
'settings.tabs.title': '标签页显示',
|
|
239
242
|
'settings.tabs.mode.follow': '跟随会话',
|
|
240
243
|
'settings.tabs.mode.show': '始终显示',
|
|
@@ -536,11 +539,14 @@ const EN_DICT = {
|
|
|
536
539
|
'settings.pref.title': 'Chat preferences',
|
|
537
540
|
'settings.pref.unavailable': 'This connection cannot persist preferences (remote or temporary mode); the two options below are disabled.',
|
|
538
541
|
'settings.pref.loading': 'Loading preferences…',
|
|
542
|
+
'settings.pref.notRegistered': 'Preferences are unavailable right now (component not ready); please retry later.',
|
|
543
|
+
'settings.pref.readOnly': 'The settings document is read-only; changes cannot be saved.',
|
|
539
544
|
'settings.pref.confirmWrites': 'Confirm write actions',
|
|
540
545
|
'settings.pref.confirmWritesHint': 'Show an in-app confirmation before creating wishes/tasks, checking in or canceling (deletes always confirm). Turn off to execute directly.',
|
|
541
546
|
'settings.pref.memoryLimit': 'Memory injection limit',
|
|
542
547
|
'settings.pref.memoryLimitHint': 'Max memories auto-injected per chat (5-200, default 40); saved on blur.',
|
|
543
548
|
'settings.pref.limitInvalid': 'Enter an integer between 5 and 200',
|
|
549
|
+
'settings.pref.writeFailed': 'The change did not save; please try again.',
|
|
544
550
|
'settings.tabs.title': 'Tab visibility',
|
|
545
551
|
'settings.tabs.mode.follow': 'Follow session',
|
|
546
552
|
'settings.tabs.mode.show': 'Always show',
|
package/lib/client/index.js
CHANGED
|
@@ -79,10 +79,12 @@ export function apply(ctx) {
|
|
|
79
79
|
// label 用 thunk 跟随当前语言;locale 声明让壳在语言切换时刷新标签行。
|
|
80
80
|
ctx.slots.inject('conversation.view', () => installTabVisibility(ctx));
|
|
81
81
|
// 星愿设置整页(设置 → 星愿):教练风格/画像(星愿库)+ 二次确认开关与注入上限
|
|
82
|
-
// (
|
|
82
|
+
// (bundle 常驻命名空间 xingyuan-pref)+ 标签页显隐(bundle 常驻命名空间 xingyuan-ui)。
|
|
83
|
+
// 两个偏好命名空间都挂在常驻层:整页由本文件无条件注册,命名空间若随 preset 懒加载
|
|
84
|
+
// 缺席,就会出现「整页可见但两项写不进去且静默失败」。
|
|
83
85
|
ctx.slots.inject('settings.section', () => {
|
|
84
|
-
const
|
|
86
|
+
const prefscope = ctx.settingsScope.bind({ namespace: 'xingyuan-pref' });
|
|
85
87
|
const uiscope = ctx.settingsScope.bind({ namespace: 'xingyuan-ui' });
|
|
86
|
-
return ctx.slots.register({ name: 'settings.section', id: 'xingyuan', order: 60, label: () => t('settings.tabLabel'), locale: XY_NS }, () => SettingsSection({ scope, uiscope }));
|
|
88
|
+
return ctx.slots.register({ name: 'settings.section', id: 'xingyuan', order: 60, label: () => t('settings.tabLabel'), locale: XY_NS }, () => SettingsSection({ scope: prefscope, uiscope }));
|
|
87
89
|
});
|
|
88
90
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** 星愿设置整页(设置 → 星愿):教练风格/画像(星愿库)+ 二次确认开关与注入上限
|
|
2
|
-
* (
|
|
2
|
+
* (bundle 常驻命名空间 xingyuan-pref)+ 标签页显隐(bundle 常驻命名空间 xingyuan-ui,
|
|
3
|
+
* 未选星愿也可调)。 */
|
|
3
4
|
import { createElement, useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
|
4
5
|
import { getJson, postJson } from '../api.js';
|
|
5
6
|
import { toastError } from '../ui.js';
|
|
6
7
|
import { useXyT, activeLocale } from '../i18n.js';
|
|
7
8
|
import { TAB_IDS } from '../../tab-policy.js';
|
|
9
|
+
import { MEMORY_LIMIT_MAX, MEMORY_LIMIT_MIN, PREF_DEFAULTS, parseMemoryLimit, } from '../../pref-policy.js';
|
|
8
10
|
const COACH_IDS = ['gentle', 'strict', 'humorous'];
|
|
9
11
|
const COACH_KEYS = ['settings.coach.gentle', 'settings.coach.strict', 'settings.coach.humorous'];
|
|
10
12
|
/** 显隐三态(顺序即分段按钮顺序;与 tab-policy 字面量同源)。 */
|
|
@@ -28,7 +30,7 @@ export function SettingsSection(props) {
|
|
|
28
30
|
// 正确订阅:useSyncExternalStore + getSnapshot(scope 契约见 dsh-client-runtime contract/settings-scope)
|
|
29
31
|
const snap = useSyncExternalStore((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
30
32
|
const writable = snap.status === 'ready' && snap.writable;
|
|
31
|
-
const limit = String(snap.value?.memoryInjectLimit ??
|
|
33
|
+
const limit = String(snap.value?.memoryInjectLimit ?? PREF_DEFAULTS.memoryInjectLimit);
|
|
32
34
|
// 界面偏好命名空间(标签页显隐)独立订阅:两个命名空间各自走自己的快照
|
|
33
35
|
const uisnap = useSyncExternalStore((listener) => props.uiscope.subscribe(listener), () => props.uiscope.getSnapshot());
|
|
34
36
|
const uiWritable = uisnap.status === 'ready' && uisnap.writable;
|
|
@@ -41,6 +43,9 @@ export function SettingsSection(props) {
|
|
|
41
43
|
// 记忆注入上限:草稿态编辑(null=显示快照值),失焦才校验提交——避免逐键持久化与非法中间值死锁
|
|
42
44
|
const [limitDraft, setLimitDraft] = useState(null);
|
|
43
45
|
const [limitError, setLimitError] = useState(false);
|
|
46
|
+
// 记忆注入上限:写入在途的乐观值(与开关的 pendingToggle 同款,用于禁用控件并顶住回显,
|
|
47
|
+
// 否则提交后到快照回折前会闪回旧值)
|
|
48
|
+
const [pendingLimit, setPendingLimit] = useState(undefined);
|
|
44
49
|
const [saving, setSaving] = useState(false);
|
|
45
50
|
const [savedMsg, setSavedMsg] = useState('');
|
|
46
51
|
// 二次确认开关:本地乐观值(undefined=跟随远端快照)——写入在途时 UI 立即响应,失败回滚
|
|
@@ -51,8 +56,20 @@ export function SettingsSection(props) {
|
|
|
51
56
|
const savedTimer = useRef(undefined);
|
|
52
57
|
// 档案加载代数守卫:慢响应不得覆盖用户已开始的草稿
|
|
53
58
|
const profileSeqRef = useRef(0);
|
|
54
|
-
//
|
|
55
|
-
|
|
59
|
+
// 偏好写序号:两个控件共用同一个 scope(即同一条写队列)。控制器以 writeGeneration
|
|
60
|
+
// 做栅栏——一次写被更新的写取代时只记 pendingRevision、不回折快照,而其 .then 又
|
|
61
|
+
// 先于后继写执行,此时比对快照必然读到旧值。故只有最后结算的那次有权判定成败。
|
|
62
|
+
const writeSeqRef = useRef(0);
|
|
63
|
+
// 开关显示值:乐观本地值优先(写入在途),否则远端快照,值缺席回落 schema 默认。
|
|
64
|
+
// 不可写成 `!== false`——那样「值未知」会被渲染成「已开启」,安全策略类开关尤其不能撒谎。
|
|
65
|
+
const confirmWrites = pendingToggle ?? snap.value?.confirmWrites ?? PREF_DEFAULTS.confirmWrites;
|
|
66
|
+
// 偏好区提示:判定顺序不可换——memory 模式下 status 同样是 unavailable,
|
|
67
|
+
// 先判 mode 才不会把「远程/临时模式」误报成「命名空间未就绪」
|
|
68
|
+
const prefNoticeKey = snap.status === 'loading' ? 'settings.pref.loading'
|
|
69
|
+
: snap.mode === 'memory' ? 'settings.pref.unavailable'
|
|
70
|
+
: snap.status === 'unavailable' ? 'settings.pref.notRegistered'
|
|
71
|
+
: !snap.writable ? 'settings.pref.readOnly'
|
|
72
|
+
: undefined;
|
|
56
73
|
// 标签页显隐显示值:乐观优先,否则远端快照,再兜底 schema 默认
|
|
57
74
|
const tabMode = pendingMode ?? uisnap.value?.tabVisibilityMode ?? 'follow';
|
|
58
75
|
const hiddenTabs = pendingHidden ?? uisnap.value?.hiddenTabs ?? [];
|
|
@@ -116,28 +133,59 @@ export function SettingsSection(props) {
|
|
|
116
133
|
setSaving(true);
|
|
117
134
|
run().catch(toastError).finally(() => setSaving(false));
|
|
118
135
|
};
|
|
136
|
+
/**
|
|
137
|
+
* 校验偏好是否真的落盘:scope.set() 失败时是 resolve 而非 reject(内部 catch 后静默
|
|
138
|
+
* recover),故只能在写入结算后比对快照——值没变即视为未保存,须显式告知用户。
|
|
139
|
+
*
|
|
140
|
+
* seq 为发起写时取的号:只有仍是队列里最后一次写才校验,否则快照本就不会回折本次
|
|
141
|
+
* 的结果(被更新的写取代),比对必然误报。见 writeSeqRef。
|
|
142
|
+
*/
|
|
143
|
+
const verifyWritten = (seq, field, value) => {
|
|
144
|
+
if (seq !== writeSeqRef.current)
|
|
145
|
+
return;
|
|
146
|
+
if (scope.getSnapshot().value?.[field] !== value)
|
|
147
|
+
toastError(new Error(t('settings.pref.writeFailed')));
|
|
148
|
+
};
|
|
119
149
|
const commitLimit = () => {
|
|
120
150
|
const raw = limitDraft;
|
|
121
151
|
setLimitDraft(null);
|
|
122
152
|
setLimitError(false);
|
|
123
|
-
if (raw === null || !writable
|
|
153
|
+
if (raw === null || !writable)
|
|
124
154
|
return;
|
|
125
|
-
|
|
155
|
+
// 空串视为放弃编辑:静默回显保存值。若在此报错,会同时出现「已填回旧值」与
|
|
156
|
+
// 「请输入 5-200 的整数」两条互相矛盾的信息。
|
|
157
|
+
if (raw.trim() === '')
|
|
158
|
+
return;
|
|
159
|
+
const parsed = parseMemoryLimit(raw);
|
|
126
160
|
// 非法输入就地报错(不再静默回弹):用户明确知道哪里错、该怎么改
|
|
127
|
-
if (
|
|
161
|
+
if (parsed === undefined) {
|
|
128
162
|
setLimitError(true);
|
|
129
163
|
return;
|
|
130
164
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
165
|
+
if (parsed.clamped) {
|
|
166
|
+
// 越界但可夹取:按夹取值提交并回显,同时给出行内提示
|
|
167
|
+
const seq = ++writeSeqRef.current;
|
|
168
|
+
setPendingLimit(parsed.value);
|
|
169
|
+
void scope.set('memoryInjectLimit', parsed.value)
|
|
170
|
+
.then(() => {
|
|
171
|
+
setLimitDraft(String(parsed.value));
|
|
172
|
+
setPendingLimit(undefined);
|
|
173
|
+
verifyWritten(seq, 'memoryInjectLimit', parsed.value);
|
|
174
|
+
})
|
|
175
|
+
.catch((err) => { setPendingLimit(undefined); toastError(err); });
|
|
135
176
|
setLimitError(true);
|
|
136
177
|
return;
|
|
137
178
|
}
|
|
138
|
-
if (
|
|
179
|
+
if (parsed.value === snap.value?.memoryInjectLimit)
|
|
139
180
|
return;
|
|
140
|
-
|
|
181
|
+
const seq = ++writeSeqRef.current;
|
|
182
|
+
setPendingLimit(parsed.value);
|
|
183
|
+
void scope.set('memoryInjectLimit', parsed.value)
|
|
184
|
+
.then(() => {
|
|
185
|
+
setPendingLimit(undefined);
|
|
186
|
+
verifyWritten(seq, 'memoryInjectLimit', parsed.value);
|
|
187
|
+
})
|
|
188
|
+
.catch((err) => { setPendingLimit(undefined); toastError(err); });
|
|
141
189
|
};
|
|
142
190
|
const coachLabel = (id) => {
|
|
143
191
|
const index = COACH_IDS.indexOf(id);
|
|
@@ -198,11 +246,11 @@ export function SettingsSection(props) {
|
|
|
198
246
|
className: 'xy-btn xy-btn-primary', disabled: saving, onClick: saveProfile,
|
|
199
247
|
}, saving ? t('settings.profile.saving') : t('settings.profile.save')), savedMsg !== '' ? createElement('span', { className: 'xy-saved', role: 'status' },
|
|
200
248
|
// 勾形装饰对读屏隐藏(语义由文案承担),与卡片完成态同一语法
|
|
201
|
-
createElement('span', { 'aria-hidden': 'true' }, '✓ '), savedMsg) : null), createElement('p', { className: 'xy-hint' }, t('settings.profile.sharedHint'))), createElement('section', { className: 'xy-panel' }, createElement('h3', { className: 'xy-panel-head' }, t('settings.pref.title')),
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
249
|
+
createElement('span', { 'aria-hidden': 'true' }, '✓ '), savedMsg) : null), createElement('p', { className: 'xy-hint' }, t('settings.profile.sharedHint'))), createElement('section', { className: 'xy-panel' }, createElement('h3', { className: 'xy-panel-head' }, t('settings.pref.title')),
|
|
250
|
+
// 整页 section 没有官方卡片那套「按命名空间自动显隐」的保护,失败呈现归注册方
|
|
251
|
+
prefNoticeKey !== undefined
|
|
252
|
+
? createElement('p', { className: 'xy-hint' }, t(prefNoticeKey))
|
|
253
|
+
: null, createElement('label', { className: 'xy-field' }, createElement('span', { className: 'xy-field-head' }, createElement('input', {
|
|
206
254
|
type: 'checkbox',
|
|
207
255
|
className: 'xy-toggle',
|
|
208
256
|
name: 'confirmWrites',
|
|
@@ -214,19 +262,23 @@ export function SettingsSection(props) {
|
|
|
214
262
|
// 乐观写:先落 UI 再等持久化;失败回滚到快照口径并 toast
|
|
215
263
|
const next = e.target.checked;
|
|
216
264
|
setPendingToggle(next);
|
|
265
|
+
const seq = ++writeSeqRef.current;
|
|
217
266
|
void scope.set('confirmWrites', next)
|
|
218
|
-
.then(() => setPendingToggle(undefined))
|
|
267
|
+
.then(() => { setPendingToggle(undefined); verifyWritten(seq, 'confirmWrites', next); })
|
|
219
268
|
.catch((err) => {
|
|
220
269
|
setPendingToggle(undefined);
|
|
221
270
|
toastError(err);
|
|
222
271
|
});
|
|
223
272
|
},
|
|
224
273
|
}), t('settings.pref.confirmWrites')), createElement('p', { className: 'xy-hint' }, t('settings.pref.confirmWritesHint'))), createElement('label', { className: 'xy-field' }, createElement('span', { className: 'xy-field-head' }, t('settings.pref.memoryLimit')), createElement('input', {
|
|
225
|
-
type: 'number', min:
|
|
274
|
+
type: 'number', min: MEMORY_LIMIT_MIN, max: MEMORY_LIMIT_MAX,
|
|
275
|
+
className: 'xy-input xy-input-num', name: 'memoryInjectLimit', inputMode: 'numeric',
|
|
226
276
|
'aria-label': t('settings.pref.memoryLimit'),
|
|
227
277
|
'aria-invalid': limitError || undefined,
|
|
228
278
|
...(limitError ? { 'aria-describedby': 'xy-limit-error' } : {}),
|
|
229
|
-
|
|
279
|
+
// 写入在途时顶住乐观值并禁用(与开关同款),避免闪回旧值与并发写交错
|
|
280
|
+
value: limitDraft ?? (pendingLimit !== undefined ? String(pendingLimit) : limit),
|
|
281
|
+
disabled: !writable || pendingLimit !== undefined,
|
|
230
282
|
onChange: (e) => { setLimitDraft(e.target.value); setLimitError(false); },
|
|
231
283
|
onBlur: commitLimit,
|
|
232
284
|
onKeyDown: (e) => {
|
package/lib/client.js
CHANGED
|
@@ -226,11 +226,14 @@ const ZH_DICT = {
|
|
|
226
226
|
"settings.pref.title": "对话偏好",
|
|
227
227
|
"settings.pref.unavailable": "当前连接不支持持久化偏好(远程或临时模式),以下两项不可用。",
|
|
228
228
|
"settings.pref.loading": "偏好加载中…",
|
|
229
|
+
"settings.pref.notRegistered": "偏好暂不可用(组件未就绪),请稍后重试。",
|
|
230
|
+
"settings.pref.readOnly": "当前设置文档为只读,无法保存修改。",
|
|
229
231
|
"settings.pref.confirmWrites": "写操作二次确认",
|
|
230
232
|
"settings.pref.confirmWritesHint": "创建愿望/任务、打卡、取消打卡时先弹应用内确认卡(删除始终确认);关闭后对话中的这类操作将直接执行。",
|
|
231
233
|
"settings.pref.memoryLimit": "记忆注入上限",
|
|
232
234
|
"settings.pref.memoryLimitHint": "每次对话自动注入上下文的记忆条数上限(5-200,默认 40);失焦后保存。",
|
|
233
235
|
"settings.pref.limitInvalid": "请输入 5-200 的整数",
|
|
236
|
+
"settings.pref.writeFailed": "保存未生效,请重试。",
|
|
234
237
|
"settings.tabs.title": "标签页显示",
|
|
235
238
|
"settings.tabs.mode.follow": "跟随会话",
|
|
236
239
|
"settings.tabs.mode.show": "始终显示",
|
|
@@ -522,11 +525,14 @@ const EN_DICT = {
|
|
|
522
525
|
"settings.pref.title": "Chat preferences",
|
|
523
526
|
"settings.pref.unavailable": "This connection cannot persist preferences (remote or temporary mode); the two options below are disabled.",
|
|
524
527
|
"settings.pref.loading": "Loading preferences…",
|
|
528
|
+
"settings.pref.notRegistered": "Preferences are unavailable right now (component not ready); please retry later.",
|
|
529
|
+
"settings.pref.readOnly": "The settings document is read-only; changes cannot be saved.",
|
|
525
530
|
"settings.pref.confirmWrites": "Confirm write actions",
|
|
526
531
|
"settings.pref.confirmWritesHint": "Show an in-app confirmation before creating wishes/tasks, checking in or canceling (deletes always confirm). Turn off to execute directly.",
|
|
527
532
|
"settings.pref.memoryLimit": "Memory injection limit",
|
|
528
533
|
"settings.pref.memoryLimitHint": "Max memories auto-injected per chat (5-200, default 40); saved on blur.",
|
|
529
534
|
"settings.pref.limitInvalid": "Enter an integer between 5 and 200",
|
|
535
|
+
"settings.pref.writeFailed": "The change did not save; please try again.",
|
|
530
536
|
"settings.tabs.title": "Tab visibility",
|
|
531
537
|
"settings.tabs.mode.follow": "Follow session",
|
|
532
538
|
"settings.tabs.mode.show": "Always show",
|
|
@@ -3766,10 +3772,52 @@ function installTabVisibility(ctx) {
|
|
|
3766
3772
|
};
|
|
3767
3773
|
}
|
|
3768
3774
|
|
|
3775
|
+
//#endregion
|
|
3776
|
+
//#region src/pref-policy.ts
|
|
3777
|
+
/**
|
|
3778
|
+
* 星愿「对话偏好」的纯策略与常量(host/client 两半侧共用 + 测试对拍)。
|
|
3779
|
+
*
|
|
3780
|
+
* 独立成模块而非并入 pref-settings.ts 的理由:后者依赖 host-only 的
|
|
3781
|
+
* `@deepseek-ai/dsh-settings`,绝不可进浏览器包;而设置页需要上下界与默认值
|
|
3782
|
+
* 做输入夹取、回显兜底与不可用态兜底。形态对标 tab-policy.ts。
|
|
3783
|
+
*
|
|
3784
|
+
* 决策口径:
|
|
3785
|
+
* - confirmWrites:创建/打卡/取消打卡类写操作是否二次确认,默认开;
|
|
3786
|
+
* 删除类始终确认,不受此开关控制(破坏性操作不设开关)。
|
|
3787
|
+
* - memoryInjectLimit:记忆注入条数上限,默认 40,合法区间 [5,200] 的整数。
|
|
3788
|
+
* 越界输入夹取到区间内并回显;非整数/非数字/空一律拒绝,不静默改值。
|
|
3789
|
+
*/
|
|
3790
|
+
/** 记忆注入上限的下界(含)。 */
|
|
3791
|
+
const MEMORY_LIMIT_MIN = 5;
|
|
3792
|
+
/** 记忆注入上限的上界(含)。 */
|
|
3793
|
+
const MEMORY_LIMIT_MAX = 200;
|
|
3794
|
+
/** 命名空间缺省(与 xingyuan-pref schema 默认值同源,见 src/pref-settings.ts)。 */
|
|
3795
|
+
const PREF_DEFAULTS = {
|
|
3796
|
+
confirmWrites: true,
|
|
3797
|
+
memoryInjectLimit: 40
|
|
3798
|
+
};
|
|
3799
|
+
/**
|
|
3800
|
+
* 解析「记忆注入上限」输入:非整数、非数字、空串一律返回 undefined(拒绝);
|
|
3801
|
+
* 越界值夹取到 [MIN,MAX] 且 clamped 置 true(调用方据此给出行内提示并回显)。
|
|
3802
|
+
*/
|
|
3803
|
+
function parseMemoryLimit(raw) {
|
|
3804
|
+
const trimmed = raw.trim();
|
|
3805
|
+
if (trimmed === "") return void 0;
|
|
3806
|
+
const n = Number(trimmed);
|
|
3807
|
+
if (!Number.isFinite(n)) return void 0;
|
|
3808
|
+
if (Math.round(n) !== n) return void 0;
|
|
3809
|
+
const value = Math.min(200, Math.max(5, Math.round(n)));
|
|
3810
|
+
return {
|
|
3811
|
+
value,
|
|
3812
|
+
clamped: value !== n
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3769
3816
|
//#endregion
|
|
3770
3817
|
//#region src/client/pages/settings.ts
|
|
3771
3818
|
/** 星愿设置整页(设置 → 星愿):教练风格/画像(星愿库)+ 二次确认开关与注入上限
|
|
3772
|
-
* (
|
|
3819
|
+
* (bundle 常驻命名空间 xingyuan-pref)+ 标签页显隐(bundle 常驻命名空间 xingyuan-ui,
|
|
3820
|
+
* 未选星愿也可调)。 */
|
|
3773
3821
|
const COACH_IDS = [
|
|
3774
3822
|
"gentle",
|
|
3775
3823
|
"strict",
|
|
@@ -3809,7 +3857,7 @@ function SettingsSection(props) {
|
|
|
3809
3857
|
const scope = props.scope;
|
|
3810
3858
|
const snap = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
3811
3859
|
const writable = snap.status === "ready" && snap.writable;
|
|
3812
|
-
const limit = String(snap.value?.memoryInjectLimit ??
|
|
3860
|
+
const limit = String(snap.value?.memoryInjectLimit ?? PREF_DEFAULTS.memoryInjectLimit);
|
|
3813
3861
|
const uisnap = (0, react.useSyncExternalStore)((listener) => props.uiscope.subscribe(listener), () => props.uiscope.getSnapshot());
|
|
3814
3862
|
const uiWritable = uisnap.status === "ready" && uisnap.writable;
|
|
3815
3863
|
const [profile, setProfile] = (0, react.useState)(void 0);
|
|
@@ -3819,6 +3867,7 @@ function SettingsSection(props) {
|
|
|
3819
3867
|
const [interestsDraft, setInterestsDraft] = (0, react.useState)("");
|
|
3820
3868
|
const [limitDraft, setLimitDraft] = (0, react.useState)(null);
|
|
3821
3869
|
const [limitError, setLimitError] = (0, react.useState)(false);
|
|
3870
|
+
const [pendingLimit, setPendingLimit] = (0, react.useState)(void 0);
|
|
3822
3871
|
const [saving, setSaving] = (0, react.useState)(false);
|
|
3823
3872
|
const [savedMsg, setSavedMsg] = (0, react.useState)("");
|
|
3824
3873
|
const [pendingToggle, setPendingToggle] = (0, react.useState)(void 0);
|
|
@@ -3826,7 +3875,9 @@ function SettingsSection(props) {
|
|
|
3826
3875
|
const [pendingHidden, setPendingHidden] = (0, react.useState)(void 0);
|
|
3827
3876
|
const savedTimer = (0, react.useRef)(void 0);
|
|
3828
3877
|
const profileSeqRef = (0, react.useRef)(0);
|
|
3829
|
-
const
|
|
3878
|
+
const writeSeqRef = (0, react.useRef)(0);
|
|
3879
|
+
const confirmWrites = pendingToggle ?? snap.value?.confirmWrites ?? PREF_DEFAULTS.confirmWrites;
|
|
3880
|
+
const prefNoticeKey = snap.status === "loading" ? "settings.pref.loading" : snap.mode === "memory" ? "settings.pref.unavailable" : snap.status === "unavailable" ? "settings.pref.notRegistered" : !snap.writable ? "settings.pref.readOnly" : void 0;
|
|
3830
3881
|
const tabMode = pendingMode ?? uisnap.value?.tabVisibilityMode ?? "follow";
|
|
3831
3882
|
const hiddenTabs = pendingHidden ?? uisnap.value?.hiddenTabs ?? [];
|
|
3832
3883
|
/** 切换显隐模式:乐观写,失效即回滚(分段按钮视觉与持久化口径一致)。 */
|
|
@@ -3876,24 +3927,52 @@ function SettingsSection(props) {
|
|
|
3876
3927
|
setSaving(true);
|
|
3877
3928
|
run().catch(toastError).finally(() => setSaving(false));
|
|
3878
3929
|
};
|
|
3930
|
+
/**
|
|
3931
|
+
* 校验偏好是否真的落盘:scope.set() 失败时是 resolve 而非 reject(内部 catch 后静默
|
|
3932
|
+
* recover),故只能在写入结算后比对快照——值没变即视为未保存,须显式告知用户。
|
|
3933
|
+
*
|
|
3934
|
+
* seq 为发起写时取的号:只有仍是队列里最后一次写才校验,否则快照本就不会回折本次
|
|
3935
|
+
* 的结果(被更新的写取代),比对必然误报。见 writeSeqRef。
|
|
3936
|
+
*/
|
|
3937
|
+
const verifyWritten = (seq, field, value) => {
|
|
3938
|
+
if (seq !== writeSeqRef.current) return;
|
|
3939
|
+
if (scope.getSnapshot().value?.[field] !== value) toastError(new Error(t("settings.pref.writeFailed")));
|
|
3940
|
+
};
|
|
3879
3941
|
const commitLimit = () => {
|
|
3880
3942
|
const raw = limitDraft;
|
|
3881
3943
|
setLimitDraft(null);
|
|
3882
3944
|
setLimitError(false);
|
|
3883
|
-
if (raw === null || !writable
|
|
3884
|
-
|
|
3885
|
-
|
|
3945
|
+
if (raw === null || !writable) return;
|
|
3946
|
+
if (raw.trim() === "") return;
|
|
3947
|
+
const parsed = parseMemoryLimit(raw);
|
|
3948
|
+
if (parsed === void 0) {
|
|
3886
3949
|
setLimitError(true);
|
|
3887
3950
|
return;
|
|
3888
3951
|
}
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3952
|
+
if (parsed.clamped) {
|
|
3953
|
+
const seq = ++writeSeqRef.current;
|
|
3954
|
+
setPendingLimit(parsed.value);
|
|
3955
|
+
scope.set("memoryInjectLimit", parsed.value).then(() => {
|
|
3956
|
+
setLimitDraft(String(parsed.value));
|
|
3957
|
+
setPendingLimit(void 0);
|
|
3958
|
+
verifyWritten(seq, "memoryInjectLimit", parsed.value);
|
|
3959
|
+
}).catch((err) => {
|
|
3960
|
+
setPendingLimit(void 0);
|
|
3961
|
+
toastError(err);
|
|
3962
|
+
});
|
|
3892
3963
|
setLimitError(true);
|
|
3893
3964
|
return;
|
|
3894
3965
|
}
|
|
3895
|
-
if (
|
|
3896
|
-
|
|
3966
|
+
if (parsed.value === snap.value?.memoryInjectLimit) return;
|
|
3967
|
+
const seq = ++writeSeqRef.current;
|
|
3968
|
+
setPendingLimit(parsed.value);
|
|
3969
|
+
scope.set("memoryInjectLimit", parsed.value).then(() => {
|
|
3970
|
+
setPendingLimit(void 0);
|
|
3971
|
+
verifyWritten(seq, "memoryInjectLimit", parsed.value);
|
|
3972
|
+
}).catch((err) => {
|
|
3973
|
+
setPendingLimit(void 0);
|
|
3974
|
+
toastError(err);
|
|
3975
|
+
});
|
|
3897
3976
|
};
|
|
3898
3977
|
const coachLabel = (id) => {
|
|
3899
3978
|
const index = COACH_IDS.indexOf(id);
|
|
@@ -3966,7 +4045,7 @@ function SettingsSection(props) {
|
|
|
3966
4045
|
}, saving ? t("settings.profile.saving") : t("settings.profile.save")), savedMsg !== "" ? (0, react.createElement)("span", {
|
|
3967
4046
|
className: "xy-saved",
|
|
3968
4047
|
role: "status"
|
|
3969
|
-
}, (0, react.createElement)("span", { "aria-hidden": "true" }, "✓ "), savedMsg) : null), (0, react.createElement)("p", { className: "xy-hint" }, t("settings.profile.sharedHint"))), (0, react.createElement)("section", { className: "xy-panel" }, (0, react.createElement)("h3", { className: "xy-panel-head" }, t("settings.pref.title")),
|
|
4048
|
+
}, (0, react.createElement)("span", { "aria-hidden": "true" }, "✓ "), savedMsg) : null), (0, react.createElement)("p", { className: "xy-hint" }, t("settings.profile.sharedHint"))), (0, react.createElement)("section", { className: "xy-panel" }, (0, react.createElement)("h3", { className: "xy-panel-head" }, t("settings.pref.title")), prefNoticeKey !== void 0 ? (0, react.createElement)("p", { className: "xy-hint" }, t(prefNoticeKey)) : null, (0, react.createElement)("label", { className: "xy-field" }, (0, react.createElement)("span", { className: "xy-field-head" }, (0, react.createElement)("input", {
|
|
3970
4049
|
type: "checkbox",
|
|
3971
4050
|
className: "xy-toggle",
|
|
3972
4051
|
name: "confirmWrites",
|
|
@@ -3976,7 +4055,11 @@ function SettingsSection(props) {
|
|
|
3976
4055
|
if (!writable) return;
|
|
3977
4056
|
const next = e.target.checked;
|
|
3978
4057
|
setPendingToggle(next);
|
|
3979
|
-
|
|
4058
|
+
const seq = ++writeSeqRef.current;
|
|
4059
|
+
scope.set("confirmWrites", next).then(() => {
|
|
4060
|
+
setPendingToggle(void 0);
|
|
4061
|
+
verifyWritten(seq, "confirmWrites", next);
|
|
4062
|
+
}).catch((err) => {
|
|
3980
4063
|
setPendingToggle(void 0);
|
|
3981
4064
|
toastError(err);
|
|
3982
4065
|
});
|
|
@@ -3991,8 +4074,8 @@ function SettingsSection(props) {
|
|
|
3991
4074
|
"aria-label": t("settings.pref.memoryLimit"),
|
|
3992
4075
|
"aria-invalid": limitError || void 0,
|
|
3993
4076
|
...limitError ? { "aria-describedby": "xy-limit-error" } : {},
|
|
3994
|
-
value: limitDraft ?? limit,
|
|
3995
|
-
disabled: !writable,
|
|
4077
|
+
value: limitDraft ?? (pendingLimit !== void 0 ? String(pendingLimit) : limit),
|
|
4078
|
+
disabled: !writable || pendingLimit !== void 0,
|
|
3996
4079
|
onChange: (e) => {
|
|
3997
4080
|
setLimitDraft(e.target.value);
|
|
3998
4081
|
setLimitError(false);
|
|
@@ -4119,7 +4202,7 @@ function apply(ctx) {
|
|
|
4119
4202
|
]);
|
|
4120
4203
|
ctx.slots.inject("conversation.view", () => installTabVisibility(ctx));
|
|
4121
4204
|
ctx.slots.inject("settings.section", () => {
|
|
4122
|
-
const
|
|
4205
|
+
const prefscope = ctx.settingsScope.bind({ namespace: "xingyuan-pref" });
|
|
4123
4206
|
const uiscope = ctx.settingsScope.bind({ namespace: "xingyuan-ui" });
|
|
4124
4207
|
return ctx.slots.register({
|
|
4125
4208
|
name: "settings.section",
|
|
@@ -4128,7 +4211,7 @@ function apply(ctx) {
|
|
|
4128
4211
|
label: () => t("settings.tabLabel"),
|
|
4129
4212
|
locale: XY_NS
|
|
4130
4213
|
}, () => SettingsSection({
|
|
4131
|
-
scope,
|
|
4214
|
+
scope: prefscope,
|
|
4132
4215
|
uiscope
|
|
4133
4216
|
}));
|
|
4134
4217
|
});
|
package/lib/domain.js
CHANGED
|
@@ -89,10 +89,11 @@ export const xingyuanDomainSpec = defineDomain({
|
|
|
89
89
|
},
|
|
90
90
|
});
|
|
91
91
|
/** 由已打开领域构造服务句柄(bundle 入口组装用)。 */
|
|
92
|
-
export function makeXingyuanStore(domain) {
|
|
92
|
+
export function makeXingyuanStore(domain, readPrefs) {
|
|
93
93
|
return {
|
|
94
94
|
spec: xingyuanDomainSpec,
|
|
95
95
|
domain,
|
|
96
|
+
prefs: readPrefs,
|
|
96
97
|
newId: () => randomUUID(),
|
|
97
98
|
checkinKey: (taskId, date) => `${taskId}|${date}`,
|
|
98
99
|
};
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
2
|
import { makeXingyuanStore, xingyuanDomainSpec } from './domain.js';
|
|
3
|
+
import { installPrefSettings } from './pref-settings.js';
|
|
3
4
|
import { registerXingyuanRoutes } from './routes/index.js';
|
|
4
5
|
import { ensurePresetRoot } from './preset-root.js';
|
|
5
6
|
import { repairSessionLogs } from './session-log-repair.js';
|
|
@@ -28,6 +29,10 @@ export async function apply(ctx, config) {
|
|
|
28
29
|
});
|
|
29
30
|
// 界面偏好命名空间(标签页显隐)常驻注册:不依赖 settings 服务存在,服务挂载后自动生效
|
|
30
31
|
installUiSettings(ctx);
|
|
32
|
+
// 对话偏好命名空间(二次确认 + 记忆注入上限)常驻注册:与界面偏好同款时序。
|
|
33
|
+
// 返回的读取 thunk 由领域服务持有,preset 层经 ctx.xingyuan.prefs() 读取——
|
|
34
|
+
// 命名空间必须常驻,否则整页可见而数据随 preset 懒加载缺席,写入静默失败。
|
|
35
|
+
const readPrefs = installPrefSettings(ctx);
|
|
31
36
|
// preset 发布成功后再开领域;两步就绪后才 provide,注入方(preset 子树)由
|
|
32
37
|
// cordis inject 语义等待本行激活完成
|
|
33
38
|
await ensurePresetRoot();
|
|
@@ -37,7 +42,7 @@ export async function apply(ctx, config) {
|
|
|
37
42
|
return;
|
|
38
43
|
}
|
|
39
44
|
domain = opened;
|
|
40
|
-
ctx.provide('xingyuan', makeXingyuanStore(opened));
|
|
45
|
+
ctx.provide('xingyuan', makeXingyuanStore(opened, readPrefs));
|
|
41
46
|
registerXingyuanRoutes(webServer, ctx.xingyuan, config);
|
|
42
47
|
if (config.repairSessionLogs) {
|
|
43
48
|
try {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星愿「对话偏好」的纯策略与常量(host/client 两半侧共用 + 测试对拍)。
|
|
3
|
+
*
|
|
4
|
+
* 独立成模块而非并入 pref-settings.ts 的理由:后者依赖 host-only 的
|
|
5
|
+
* `@deepseek-ai/dsh-settings`,绝不可进浏览器包;而设置页需要上下界与默认值
|
|
6
|
+
* 做输入夹取、回显兜底与不可用态兜底。形态对标 tab-policy.ts。
|
|
7
|
+
*
|
|
8
|
+
* 决策口径:
|
|
9
|
+
* - confirmWrites:创建/打卡/取消打卡类写操作是否二次确认,默认开;
|
|
10
|
+
* 删除类始终确认,不受此开关控制(破坏性操作不设开关)。
|
|
11
|
+
* - memoryInjectLimit:记忆注入条数上限,默认 40,合法区间 [5,200] 的整数。
|
|
12
|
+
* 越界输入夹取到区间内并回显;非整数/非数字/空一律拒绝,不静默改值。
|
|
13
|
+
*/
|
|
14
|
+
/** 记忆注入上限的下界(含)。 */
|
|
15
|
+
export const MEMORY_LIMIT_MIN = 5;
|
|
16
|
+
/** 记忆注入上限的上界(含)。 */
|
|
17
|
+
export const MEMORY_LIMIT_MAX = 200;
|
|
18
|
+
/** 命名空间缺省(与 xingyuan-pref schema 默认值同源,见 src/pref-settings.ts)。 */
|
|
19
|
+
export const PREF_DEFAULTS = {
|
|
20
|
+
confirmWrites: true,
|
|
21
|
+
memoryInjectLimit: 40,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* 解析「记忆注入上限」输入:非整数、非数字、空串一律返回 undefined(拒绝);
|
|
25
|
+
* 越界值夹取到 [MIN,MAX] 且 clamped 置 true(调用方据此给出行内提示并回显)。
|
|
26
|
+
*/
|
|
27
|
+
export function parseMemoryLimit(raw) {
|
|
28
|
+
const trimmed = raw.trim();
|
|
29
|
+
if (trimmed === '')
|
|
30
|
+
return undefined;
|
|
31
|
+
const n = Number(trimmed);
|
|
32
|
+
if (!Number.isFinite(n))
|
|
33
|
+
return undefined;
|
|
34
|
+
if (Math.round(n) !== n)
|
|
35
|
+
return undefined;
|
|
36
|
+
const value = Math.min(MEMORY_LIMIT_MAX, Math.max(MEMORY_LIMIT_MIN, Math.round(n)));
|
|
37
|
+
return { value, clamped: value !== n };
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
3
|
+
import { MEMORY_LIMIT_MAX, MEMORY_LIMIT_MIN, PREF_DEFAULTS } from './pref-policy.js';
|
|
4
|
+
/** 对话偏好命名空间(client 半侧 settingsScope 同名配对,勿改)。 */
|
|
5
|
+
export const PREF_NS = settingsNamespace('xingyuan-pref');
|
|
6
|
+
/** 配置 schema(默认值写进 schema;step(1) 让服务端也拒绝小数,防手改文档/RPC 直写)。 */
|
|
7
|
+
export const PrefSettingsSchema = z.object({
|
|
8
|
+
confirmWrites: z.boolean().default(PREF_DEFAULTS.confirmWrites),
|
|
9
|
+
memoryInjectLimit: z.number().step(1).min(MEMORY_LIMIT_MIN).max(MEMORY_LIMIT_MAX)
|
|
10
|
+
.default(PREF_DEFAULTS.memoryInjectLimit),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* bundle 层安装对话偏好命名空间,并返回偏好读取 thunk。
|
|
14
|
+
*
|
|
15
|
+
* thunk 每次调用都读「当前解析值」(schema 默认 → base → 用户层),故设置热改后
|
|
16
|
+
* 下一次读取即生效,无需重建任何注册;settings 服务缺席时返回 PREF_DEFAULTS。
|
|
17
|
+
*/
|
|
18
|
+
export function installPrefSettings(ctx) {
|
|
19
|
+
// 闭包局部,非模块级单例(AGENTS.md §8:不做任何跨重载的模块级单例状态)
|
|
20
|
+
let read = () => PREF_DEFAULTS;
|
|
21
|
+
installSettingsSection(ctx, PREF_NS, PrefSettingsSchema, PREF_DEFAULTS, {
|
|
22
|
+
setSource: (current) => { read = current; },
|
|
23
|
+
// 消费方(工具/提示词)每次执行即时读 thunk,无需在此重建任何东西
|
|
24
|
+
onChange: () => { },
|
|
25
|
+
});
|
|
26
|
+
return () => read();
|
|
27
|
+
}
|
package/lib/preset/side.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
|
-
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
3
2
|
import { registerTools } from './tools.js';
|
|
4
3
|
import { registerPrompts } from './prompts.js';
|
|
5
4
|
/** Cordis 插件名。 */
|
|
@@ -8,8 +7,6 @@ export const name = 'xingyuan-preset-side';
|
|
|
8
7
|
export const inject = ['tools', 'systemPrompt', 'userQuestions', 'xingyuan'];
|
|
9
8
|
/** 配置 schema(默认值写进 schema;加载期校验失败即响亮报错)。 */
|
|
10
9
|
export const Config = z.object({
|
|
11
|
-
// 5-200 与设置页文案/客户端夹取范围同口径,服务端兜底防越界
|
|
12
|
-
memoryInjectLimit: z.number().default(40).min(5).max(200),
|
|
13
10
|
batchWishLimit: z.number().default(50),
|
|
14
11
|
batchTaskLimit: z.number().default(100),
|
|
15
12
|
chartTrendDays: z.number().default(14),
|
|
@@ -17,29 +14,21 @@ export const Config = z.object({
|
|
|
17
14
|
chartMaxDays: z.number().default(90),
|
|
18
15
|
chartRankLimit: z.number().default(10),
|
|
19
16
|
chartRankMax: z.number().default(20),
|
|
20
|
-
confirmWrites: z.boolean().default(true),
|
|
21
17
|
});
|
|
22
|
-
/** 星愿设置命名空间(与浏览器半侧卡片配对)。 */
|
|
23
|
-
export const SETTINGS_NS = settingsNamespace('xingyuan');
|
|
24
18
|
export function apply(ctx, config) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
setSource: (current) => { source = current; },
|
|
29
|
-
onChange: () => { },
|
|
30
|
-
});
|
|
31
|
-
const current = () => source();
|
|
32
|
-
// 以 getter 对象透传:设置卡热改后工具下一次 execute 即读新值(满足 ToolsConfig 形状)
|
|
19
|
+
// 对话偏好:bundle 层常驻命名空间的读取 thunk,每次调用取当前解析值
|
|
20
|
+
const prefs = () => ctx.xingyuan.prefs();
|
|
21
|
+
// 组合层参数进程内不变,直接取 entry config;对话偏好用 getter,保证热改即时生效
|
|
33
22
|
const toolsConfig = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
get confirmWrites() { return
|
|
23
|
+
batchWishLimit: config.batchWishLimit,
|
|
24
|
+
batchTaskLimit: config.batchTaskLimit,
|
|
25
|
+
chartTrendDays: config.chartTrendDays,
|
|
26
|
+
chartDistributionDays: config.chartDistributionDays,
|
|
27
|
+
chartMaxDays: config.chartMaxDays,
|
|
28
|
+
chartRankLimit: config.chartRankLimit,
|
|
29
|
+
chartRankMax: config.chartRankMax,
|
|
30
|
+
get confirmWrites() { return prefs().confirmWrites; },
|
|
42
31
|
};
|
|
43
32
|
registerTools(ctx, toolsConfig);
|
|
44
|
-
registerPrompts(ctx, { get memoryInjectLimit() { return
|
|
33
|
+
registerPrompts(ctx, { get memoryInjectLimit() { return prefs().memoryInjectLimit; } });
|
|
45
34
|
}
|
package/lib/preset/tools.js
CHANGED
|
@@ -113,10 +113,20 @@ function requireTask(store, taskId) {
|
|
|
113
113
|
function wishNameOf(store, task) {
|
|
114
114
|
return task.wishId !== undefined ? store.domain.table('wishes').get(task.wishId)?.title : undefined;
|
|
115
115
|
}
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
/**
|
|
117
|
+
* 写操作的模型侧提示文案(拼进工具 description)。
|
|
118
|
+
*
|
|
119
|
+
* 两类不可混用:
|
|
120
|
+
* - `DELETE_NOTE`:删除类**始终**弹确认卡,不受设置开关影响,故可以断言「会弹卡」。
|
|
121
|
+
* - `CREATE_NOTE` / `CANCEL_CHECKIN_NOTE`:受「写操作二次确认」开关门控(见
|
|
122
|
+
* `confirmGate`),而工具描述是注册时烘焙的静态字符串——dsh-tools 校验
|
|
123
|
+
* `description` 必须是 string(不支持 getter),改设置也不会重建描述。故这两条
|
|
124
|
+
* 一律写成「开与关都成立」的措辞:绝不可断言会弹卡,否则关掉开关后模型会向用户
|
|
125
|
+
* 宣称已弹出确认卡,而实际没有卡片。
|
|
126
|
+
*/
|
|
127
|
+
const DELETE_NOTE = '删除不可恢复:会弹出系统确认卡片,直接调用等待确认结果即可,无需事先询问用户。';
|
|
128
|
+
const CREATE_NOTE = '无需事先询问用户,直接调用即可;结果返回前不要宣称已创建。若系统弹出确认卡片且用户在卡片上取消,则向用户说明并询问要调整的地方。';
|
|
129
|
+
const CANCEL_CHECKIN_NOTE = '无需事先询问用户,直接调用即可;结果返回前不要宣称已取消。';
|
|
120
130
|
/** 分类颜色白名单(schema enum 与展示色共用同一事实源)。 */
|
|
121
131
|
const COLOR_KEY_ENUM = CATEGORY_COLOR_KEYS;
|
|
122
132
|
const COLOR_KEY_NOTE = `可选值:${CATEGORY_COLOR_KEYS.join('/')}(不传则按分类名自动配色)`;
|
|
@@ -629,7 +639,8 @@ export function registerTools(ctx, config) {
|
|
|
629
639
|
ctx.tools.register(defineTool({
|
|
630
640
|
name: 'check_in_task',
|
|
631
641
|
description: '任务打卡。何时使用:用户完成任务并需要记录打卡时使用。支持指定日期打卡(日历补卡/提前勾),不传日期则自动勾选今天(含)起最早未勾选的打卡日——过去的日期不会自动补,补卡请在日历中指定日期。'
|
|
632
|
-
|
|
642
|
+
// 与 CREATE_NOTE 同口径:是否弹卡受设置门控,描述里不可断言(见 DELETE_NOTE 组注释)
|
|
643
|
+
+ '注意:无需事先询问用户,直接调用即可;今天不是打卡日时自动勾选的是未来日期(提前打卡 = 承诺当天完成),回复时请用「打卡日」等通俗用语并如实告知勾选日期。',
|
|
633
644
|
parameters: {
|
|
634
645
|
taskId: { type: 'string', required: true, description: '任务ID,取列表返回的真实值' },
|
|
635
646
|
checkInDate: { type: 'string', description: '打卡日期,可选。yyyy-MM-dd,必须是任务的打卡日;不传则自动勾选今天(含)起最早未勾选的打卡日' },
|
|
@@ -203,11 +203,14 @@ declare const ZH_DICT: {
|
|
|
203
203
|
readonly 'settings.pref.title': "对话偏好";
|
|
204
204
|
readonly 'settings.pref.unavailable': "当前连接不支持持久化偏好(远程或临时模式),以下两项不可用。";
|
|
205
205
|
readonly 'settings.pref.loading': "偏好加载中…";
|
|
206
|
+
readonly 'settings.pref.notRegistered': "偏好暂不可用(组件未就绪),请稍后重试。";
|
|
207
|
+
readonly 'settings.pref.readOnly': "当前设置文档为只读,无法保存修改。";
|
|
206
208
|
readonly 'settings.pref.confirmWrites': "写操作二次确认";
|
|
207
209
|
readonly 'settings.pref.confirmWritesHint': "创建愿望/任务、打卡、取消打卡时先弹应用内确认卡(删除始终确认);关闭后对话中的这类操作将直接执行。";
|
|
208
210
|
readonly 'settings.pref.memoryLimit': "记忆注入上限";
|
|
209
211
|
readonly 'settings.pref.memoryLimitHint': "每次对话自动注入上下文的记忆条数上限(5-200,默认 40);失焦后保存。";
|
|
210
212
|
readonly 'settings.pref.limitInvalid': "请输入 5-200 的整数";
|
|
213
|
+
readonly 'settings.pref.writeFailed': "保存未生效,请重试。";
|
|
211
214
|
readonly 'settings.tabs.title': "标签页显示";
|
|
212
215
|
readonly 'settings.tabs.mode.follow': "跟随会话";
|
|
213
216
|
readonly 'settings.tabs.mode.show': "始终显示";
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/** 星愿设置整页(设置 → 星愿):教练风格/画像(星愿库)+ 二次确认开关与注入上限
|
|
2
|
-
* (
|
|
2
|
+
* (bundle 常驻命名空间 xingyuan-pref)+ 标签页显隐(bundle 常驻命名空间 xingyuan-ui,
|
|
3
|
+
* 未选星愿也可调)。 */
|
|
3
4
|
import { type ReactElement } from 'react';
|
|
4
5
|
import { type TabId, type TabVisibilityMode } from '../../tab-policy.js';
|
|
5
|
-
/**
|
|
6
|
-
interface
|
|
6
|
+
/** xingyuan-pref 命名空间快照(对话偏好字段)。 */
|
|
7
|
+
interface PrefScopeSnapshotLike {
|
|
7
8
|
readonly status: 'loading' | 'ready' | 'unavailable';
|
|
8
9
|
readonly value?: {
|
|
9
10
|
readonly confirmWrites?: boolean;
|
|
10
11
|
readonly memoryInjectLimit?: number;
|
|
11
12
|
};
|
|
12
13
|
readonly writable: boolean;
|
|
14
|
+
/** `host` 与宿主文档同步;`memory` 为远程/临时模式,此时任何写入都不会落盘。 */
|
|
15
|
+
readonly mode: 'host' | 'memory';
|
|
13
16
|
}
|
|
14
17
|
/** xingyuan-ui 命名空间快照(标签页显隐字段)。 */
|
|
15
18
|
interface UiScopeSnapshotLike {
|
|
@@ -30,15 +33,17 @@ export interface UiScopeLike {
|
|
|
30
33
|
/**
|
|
31
34
|
* settingsScope.bind() 返回的真实形状:getSnapshot()/subscribe()/set()/unset()。
|
|
32
35
|
* 注意 scope 本身没有 .value 字段——值在快照里(此前直接读 scope.value 导致整节渲染崩溃)。
|
|
36
|
+
* 另:set() 失败是 resolve 而非 reject(内部 catch 后静默 recover),故调用方须事后
|
|
37
|
+
* 比对快照确认落盘,见下方 verifyWritten。
|
|
33
38
|
*/
|
|
34
|
-
export interface
|
|
35
|
-
getSnapshot():
|
|
39
|
+
export interface PrefScopeLike {
|
|
40
|
+
getSnapshot(): PrefScopeSnapshotLike;
|
|
36
41
|
subscribe(listener: () => void): () => void;
|
|
37
42
|
set(field: string, value: unknown): Promise<void>;
|
|
38
43
|
unset(field: string): Promise<void>;
|
|
39
44
|
}
|
|
40
45
|
export declare function SettingsSection(props: {
|
|
41
|
-
scope:
|
|
46
|
+
scope: PrefScopeLike;
|
|
42
47
|
uiscope: UiScopeLike;
|
|
43
48
|
}): ReactElement;
|
|
44
49
|
export {};
|
package/lib/types/domain.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Domain, type DomainTableSpec } from '@deepseek-ai/dsh-storage-domain';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import type { PrefSettings } from './pref-policy.js';
|
|
3
4
|
export declare const DOMAIN_VERSION = 1;
|
|
4
5
|
/** 教练风格。 */
|
|
5
6
|
export declare const COACH_STYLES: readonly ["gentle", "strict", "humorous"];
|
|
@@ -98,6 +99,8 @@ export type MemoryRecord = TableValue<(typeof xingyuanDomainSpec.tables)['memori
|
|
|
98
99
|
export interface XingyuanStore {
|
|
99
100
|
readonly spec: typeof xingyuanDomainSpec;
|
|
100
101
|
readonly domain: Domain<typeof xingyuanDomainSpec>;
|
|
102
|
+
/** 对话偏好:bundle 层常驻命名空间的解析值(非领域数据),每次调用读当前值。 */
|
|
103
|
+
prefs(): PrefSettings;
|
|
101
104
|
newId(): string;
|
|
102
105
|
checkinKey(taskId: string, date: string): string;
|
|
103
106
|
}
|
|
@@ -108,5 +111,5 @@ declare module '@deepseek-ai/cordis' {
|
|
|
108
111
|
}
|
|
109
112
|
type TableValue<S extends DomainTableSpec> = S extends DomainTableSpec<string, infer V> ? V : never;
|
|
110
113
|
/** 由已打开领域构造服务句柄(bundle 入口组装用)。 */
|
|
111
|
-
export declare function makeXingyuanStore(domain: Domain<typeof xingyuanDomainSpec
|
|
114
|
+
export declare function makeXingyuanStore(domain: Domain<typeof xingyuanDomainSpec>, readPrefs: () => PrefSettings): XingyuanStore;
|
|
112
115
|
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星愿「对话偏好」的纯策略与常量(host/client 两半侧共用 + 测试对拍)。
|
|
3
|
+
*
|
|
4
|
+
* 独立成模块而非并入 pref-settings.ts 的理由:后者依赖 host-only 的
|
|
5
|
+
* `@deepseek-ai/dsh-settings`,绝不可进浏览器包;而设置页需要上下界与默认值
|
|
6
|
+
* 做输入夹取、回显兜底与不可用态兜底。形态对标 tab-policy.ts。
|
|
7
|
+
*
|
|
8
|
+
* 决策口径:
|
|
9
|
+
* - confirmWrites:创建/打卡/取消打卡类写操作是否二次确认,默认开;
|
|
10
|
+
* 删除类始终确认,不受此开关控制(破坏性操作不设开关)。
|
|
11
|
+
* - memoryInjectLimit:记忆注入条数上限,默认 40,合法区间 [5,200] 的整数。
|
|
12
|
+
* 越界输入夹取到区间内并回显;非整数/非数字/空一律拒绝,不静默改值。
|
|
13
|
+
*/
|
|
14
|
+
/** 记忆注入上限的下界(含)。 */
|
|
15
|
+
export declare const MEMORY_LIMIT_MIN = 5;
|
|
16
|
+
/** 记忆注入上限的上界(含)。 */
|
|
17
|
+
export declare const MEMORY_LIMIT_MAX = 200;
|
|
18
|
+
/** 命名空间解析值(schema 默认 → base 层 → 用户层)。 */
|
|
19
|
+
export interface PrefSettings {
|
|
20
|
+
/** 创建/取消打卡类写操作是否二次确认(删除类始终确认)。 */
|
|
21
|
+
confirmWrites: boolean;
|
|
22
|
+
/** 每次对话自动注入上下文的记忆条数上限。 */
|
|
23
|
+
memoryInjectLimit: number;
|
|
24
|
+
}
|
|
25
|
+
/** 命名空间缺省(与 xingyuan-pref schema 默认值同源,见 src/pref-settings.ts)。 */
|
|
26
|
+
export declare const PREF_DEFAULTS: PrefSettings;
|
|
27
|
+
/**
|
|
28
|
+
* 解析「记忆注入上限」输入:非整数、非数字、空串一律返回 undefined(拒绝);
|
|
29
|
+
* 越界值夹取到 [MIN,MAX] 且 clamped 置 true(调用方据此给出行内提示并回显)。
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseMemoryLimit(raw: string): {
|
|
32
|
+
value: number;
|
|
33
|
+
clamped: boolean;
|
|
34
|
+
} | undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 星愿 bundle 层「对话偏好」设置命名空间(xingyuan-pref):
|
|
3
|
+
* 写操作二次确认(confirmWrites)与记忆注入上限(memoryInjectLimit)。
|
|
4
|
+
*
|
|
5
|
+
* 挂在 bundle 常驻层而非 preset 层的理由(本次根因,实测结论):
|
|
6
|
+
* 设置整页(settings.section)由 bundle client 层无条件注册、常驻可见;而 preset
|
|
7
|
+
* 挂载是**按 preset 常驻但懒加载**的(官方 agent-presets:per-preset standing
|
|
8
|
+
* mount,进程内只挂一次、只随整棵树卸载),首次开星愿会话才建立。于是每次 dsh
|
|
9
|
+
* 重启后、开过星愿会话之前,整页可见而命名空间不存在:两项均 unavailable,
|
|
10
|
+
* 写入静默失败(client scope.set() 失败是 resolve 而非 reject),表现为
|
|
11
|
+
* 「点了弹回原样、没有任何提示」。
|
|
12
|
+
*
|
|
13
|
+
* 官方 cookbook 的 settings.plugin.item 卡片按「Host 是否服务该命名空间」自动显隐,
|
|
14
|
+
* 整页 section 没有这层保护——slots.d.ts 契约把失败呈现的责任明确交给注册方。
|
|
15
|
+
* 故此处让数据常驻以对齐常驻 UI,而非让 UI 跟随数据(那会让设置页出现
|
|
16
|
+
* 部分字段时有时无的割裂,且安全策略类设置「有时候找不到」不可接受)。
|
|
17
|
+
*
|
|
18
|
+
* installSettingsSection 经 ctx.inject(['settings']) 等待 settings 服务挂载,
|
|
19
|
+
* 服务缺席(headless 等)时整段不运行,bundle 激活不依赖它。
|
|
20
|
+
*/
|
|
21
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
22
|
+
import z from '@deepseek-ai/schemastery';
|
|
23
|
+
import { type PrefSettings } from './pref-policy.js';
|
|
24
|
+
/** 对话偏好命名空间(client 半侧 settingsScope 同名配对,勿改)。 */
|
|
25
|
+
export declare const PREF_NS: import("@deepseek-ai/dsh-settings").SettingsNamespace;
|
|
26
|
+
/** 配置 schema(默认值写进 schema;step(1) 让服务端也拒绝小数,防手改文档/RPC 直写)。 */
|
|
27
|
+
export declare const PrefSettingsSchema: z<PrefSettings>;
|
|
28
|
+
/**
|
|
29
|
+
* bundle 层安装对话偏好命名空间,并返回偏好读取 thunk。
|
|
30
|
+
*
|
|
31
|
+
* thunk 每次调用都读「当前解析值」(schema 默认 → base → 用户层),故设置热改后
|
|
32
|
+
* 下一次读取即生效,无需重建任何注册;settings 服务缺席时返回 PREF_DEFAULTS。
|
|
33
|
+
*/
|
|
34
|
+
export declare function installPrefSettings(ctx: Context): () => PrefSettings;
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* 星愿 preset 侧入口:工具与提示词只注册在 preset 层,不出现在未选择星愿的会话里。
|
|
3
3
|
* 本文件由 presets/xingyuan/agent.cordis.yml 以裸包名子路径装载;发布服务必须留在
|
|
4
4
|
* isolate realm(本插件只向 ctx 注册贡献,不 provide 服务,天然合规)。
|
|
5
|
+
*
|
|
6
|
+
* 对话偏好(写操作二次确认 / 记忆注入上限)不在本层提供:设置整页由 bundle client
|
|
7
|
+
* 层常驻注册,而 preset 挂载按 preset 常驻却懒加载——首次开星愿会话前命名空间缺席,
|
|
8
|
+
* 整页可见而数据不在,写入静默失败。两项已迁至 bundle 层常驻命名空间 xingyuan-pref
|
|
9
|
+
* (src/pref-settings.ts),本层经 ctx.xingyuan.prefs() 读取。
|
|
5
10
|
*/
|
|
6
11
|
import type { Context } from '@deepseek-ai/cordis';
|
|
7
12
|
import z from '@deepseek-ai/schemastery';
|
|
@@ -10,10 +15,8 @@ import type { XingyuanStore } from '../domain.js';
|
|
|
10
15
|
export declare const name = "xingyuan-preset-side";
|
|
11
16
|
/** 依赖:宿主组装常驻层提供的注册表与星愿领域。 */
|
|
12
17
|
export declare const inject: string[];
|
|
13
|
-
/**
|
|
18
|
+
/** 插件配置:仅组合层可调参数(无 Web 设置界面;用户可编辑偏好见 pref-settings.ts)。 */
|
|
14
19
|
export interface Config {
|
|
15
|
-
/** 记忆注入上限(条),默认 40。 */
|
|
16
|
-
memoryInjectLimit: number;
|
|
17
20
|
/** batch_query_user_data 愿望返回上限,默认 50。 */
|
|
18
21
|
batchWishLimit: number;
|
|
19
22
|
/** batch_query_user_data 任务返回上限,默认 100。 */
|
|
@@ -28,13 +31,9 @@ export interface Config {
|
|
|
28
31
|
chartRankLimit: number;
|
|
29
32
|
/** 排行/进度类返回条数上限,默认 20。 */
|
|
30
33
|
chartRankMax: number;
|
|
31
|
-
/** 创建/取消打卡类写操作二次确认(默认开;删除类始终确认)。 */
|
|
32
|
-
confirmWrites: boolean;
|
|
33
34
|
}
|
|
34
35
|
/** 配置 schema(默认值写进 schema;加载期校验失败即响亮报错)。 */
|
|
35
36
|
export declare const Config: z<Config>;
|
|
36
|
-
/** 星愿设置命名空间(与浏览器半侧卡片配对)。 */
|
|
37
|
-
export declare const SETTINGS_NS: import("@deepseek-ai/dsh-settings").SettingsNamespace;
|
|
38
37
|
export declare function apply(ctx: Context & {
|
|
39
38
|
xingyuan: XingyuanStore;
|
|
40
39
|
}, config: Config): void;
|