dsh-surfcore-memory-engine 0.0.0-stage → 0.8.8

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 ADDED
@@ -0,0 +1,13 @@
1
+ # 更新记录
2
+
3
+ ## 0.8.8 — 首个公开版本
4
+
5
+ - 分数寿命引擎:`score` = 按日常速度还能活多少分钟
6
+ - 三档变速:日常(1 分/分钟)、长期(1 分/30 分钟)、永久(不掉)
7
+ - 升档三条路:分数 + 重要度;升档不回退;长期档名额限制
8
+ - 被想起加分:初始分 × 20% × 系数,保底 / 单次上限 / 冷却 / 累计上限
9
+ - 遗忘后可从全量档案捞回,每次 +20%(封顶 +50%)
10
+ - 每日挑提炼候选(同一话题碎片),由模型判断合并;提炼寿命 = 最久那条 × 1.2
11
+ - 设置页:定位 → 校验 → 应用 / 回滚,带机器级状态(已完成 / 半完成 / 可升级)
12
+ - 全部数值可在设置页调整,写入 `~/.dsh/surfcore/memory-engine.json`
13
+ - 应用前自动备份、失败自动还原、文件被外部改动时拒绝操作
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AX's DeepSeek (AXdfczbh)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ # SurfCore 版权与许可声明
2
+
3
+ 本插件包含对 [SurfCore](https://github.com/Mieksido/surfcore) 的修改文件。SurfCore 采用 MIT 许可,原版权声明如下:
4
+
5
+ ```
6
+ MIT License
7
+
8
+ Copyright (c) 2026 Derpyu520 and qq-bridge / QSH contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ ```
28
+
29
+ ## 修改文件清单
30
+
31
+ | 文件 | 改动 |
32
+ | --- | --- |
33
+ | `payload/src/decay-v2.js` | **新增**:分数寿命引擎(档位、升档、被想起加分、回忆捞回、提炼合并、数值覆盖文件读取) |
34
+ | `payload/src/memory-card.js` | 修改:入库盖章、结算、注入渲染、提炼候选、捞回入口接入新引擎 |
35
+ | `payload/plugins/surfcore/lib/memory-card.js` | 修改:对外快照增加档位 / 剩余时间字段 |
36
+
37
+ 这些修改同样以 MIT 许可发布,并保留上述版权声明。原始文件来自 SurfCore v0.8.8。
package/README.md CHANGED
@@ -1,3 +1,135 @@
1
- # Temporary Holding Version
1
+ # DeepSeek 的记忆引擎
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **【免责声明】本插件只是个人偏好下的记忆逻辑改造,请勿与 SurfCore 原插件互相攀比;如涉及侵权,请联系作者,作者将立即删除。**
4
+
5
+ > 此插件由 AXdfczbh 提供设想,AX 和他的 deepseek 完善讨论,AX 的 deepseek 进行代码落实。
6
+
7
+ 给 [SurfCore](https://github.com/Mieksido/surfcore) 的记忆卡换一套「分数寿命」逻辑的 DSH 插件。
8
+
9
+ 装好后在 设置 → 记忆引擎 里三步:**定位 → 校验 → 应用**,然后重启 DSH。
10
+
11
+ ## 它改了什么
12
+
13
+ | | SurfCore 原版 | 本插件 |
14
+ | --- | --- | --- |
15
+ | 衰减方式 | 按天降档的清晰度:清晰 → 清楚 → 模糊 → 只剩印象 → 遗忘 | **分数 = 按日常速度还能活多少分钟**,每分钟扣一分 |
16
+ | 档位 | 无(只有清晰度阶梯) | **日常 / 长期 / 永久** 三档,只改变漏电速度 |
17
+ | 升档 | 无 | 分数够高且重要度够 → 升长期;重要度够高 → 永久;升了不回退 |
18
+ | 被想起 | 无(提到只刷新时间) | **被提起会加分**:初始分 × 20% × 系数,有保底与封顶 |
19
+ | 忘记之后 | 移出卡片 | 移出前先进全量档案,**可以从档案里捞回来** |
20
+ | 提炼 | 无 | 每天挑出「同一话题的碎片」当候选,由模型判断是否合并 |
21
+
22
+ ## 安装
23
+
24
+ - **插件市场**:搜 `dsh-surfcore-memory-engine`;
25
+ - **本地开发**:把本目录 link 进 DSH profile 的 `dependencies`,并在 `dsh.profile.bundles` 里加上包名。
26
+
27
+ 装好后重启 DSH,设置页会出现「记忆引擎」。
28
+
29
+ ## 使用
30
+
31
+ 1. **定位**:点「自动定位」,它会从 DSH 的 profile 依赖里找 SurfCore;找不到时点「手动指定」,填**仓库根目录**(有 `src/memory-card.js` 的那一层,不是 `plugins/surfcore`)。
32
+ 2. **校验**(必须):逐项检查版本、文件指纹、写权限、引擎开关。**每次启动 DSH 后都要重新校验**,不点的话应用 / 回滚 / 改数值全是灰的。
33
+ 3. **应用**:自动备份 → 写入三个文件 → 把配置里的 `memoryCard.engine` 切成 `v2`。之后**重启 DSH** 生效。
34
+ 4. **回滚**:从备份还原,随时可退。
35
+
36
+ 校验会显示状态:`可应用` / `已完成` / `半完成(文件新了但开关没开)` / `旧版补丁(可升级)` / `不通过`。
37
+
38
+ ## 数值(可选)
39
+
40
+ 校验通过后,「数值」区会展开。所有数值都可以改,改完点保存,**重启 DSH 后生效**;点「恢复默认」会删掉覆盖文件。
41
+
42
+ | 分组 | 内容 |
43
+ | --- | --- |
44
+ | 底分(按来源) | 私聊·用户 / 私聊·我自己 / 桌宠·用户 / 桌宠·我自己 / B站·视频 / B站·动态 / 群聊 |
45
+ | 流速 · 上限 | 日常档流速、长期档流速、初始分封顶、最久时长上限 |
46
+ | 升档门槛 · 赠送 · 名额 | 两条升档路的分数与重要度、赠送天数、永久线、长期档名额 |
47
+ | 加权倍率 | 你亲口说的、我回过话、带情绪、关于我们、第一次、冲突、承诺 |
48
+ | 封顶倍率 · 加分 | 两档累计加分上限、单次基础加分、单次上限、保底、冷却、情绪/关系加成 |
49
+ | 重要度各项 | 11 项 |
50
+ | 提炼 | 提炼加成 |
51
+
52
+ 数值会写到 `~/.dsh/surfcore/memory-engine.json`,引擎启动时读取。**越界值会被夹住**,不会把记忆弄坏。
53
+
54
+ ## 适配版本
55
+
56
+ - 只支持 **SurfCore 0.8.8**,本插件版本号与 SurfCore 保持一致(同为 `0.8.8`)。
57
+ - SurfCore 升级到新版本后,本插件需要重新适配——版本对不上时「校验」会明确说 `版本已适配 ✗`,并拒绝应用。
58
+
59
+ ## 安全边界
60
+
61
+ - 只改三个文件:`src/decay-v2.js`(新增)、`src/memory-card.js`、`plugins/surfcore/lib/memory-card.js`,外加配置里的一个开关;
62
+ - 写之前先备份到 `<SurfCore>/.memlogic-backup/<时间戳>/`;
63
+ - 任何一步失败自动整体还原;
64
+ - 文件被外部改过(指纹不认识)时拒绝应用 / 回滚,要求重新校验。
65
+
66
+ ## 许可
67
+
68
+ 本插件以 **MIT** 发布。它包含对 SurfCore 的修改文件,SurfCore 同样采用 MIT 许可——原版权声明见 [NOTICE-SurfCore.md](NOTICE-SurfCore.md)。
69
+
70
+ ## 作者
71
+
72
+ - 设想:AXdfczbh
73
+ - 讨论完善:AX 和他的 DeepSeek
74
+ - 代码实现:AX 的 DeepSeek
75
+
76
+ ---
77
+
78
+ # DeepSeek Memory Engine
79
+
80
+ > **Disclaimer: this plugin is a personal-preference rework of SurfCore's memory logic — please do not compare it against the original plugin. If it infringes any rights, contact the author and it will be removed immediately.**
81
+
82
+ > Idea by AXdfczbh; refined through discussion by AX and his DeepSeek; implemented by AX's DeepSeek.
83
+
84
+ A DSH plugin that swaps SurfCore's memory card for a "score-lifetime" model.
85
+
86
+ Install, then go to Settings → Memory Engine: **Locate → Verify → Apply**, then restart DSH.
87
+
88
+ ## What it changes
89
+
90
+ | | SurfCore (original) | This plugin |
91
+ | --- | --- | --- |
92
+ | Decay | Clarity drops one step per day: vivid → clear → fuzzy → trace → forgotten | **Score = minutes the memory can still live at normal speed**; one point per minute |
93
+ | Tiers | None | **Daily / Long / Forever**, changing only the decay rate |
94
+ | Promotion | None | High score + importance → Long; high importance → Forever; promotions never fall back |
95
+ | Being mentioned | None (only refreshes the timestamp) | **Adds points**: initial × 20% × factor, with a floor and a per-call cap |
96
+ | After forgetting | Removed from the card | Moved to the full archive first, **can be recalled from it** |
97
+ | Consolidation | None | Daily candidate grouping of same-topic fragments; the model decides whether to merge |
98
+
99
+ ## Install
100
+
101
+ - **Plugin market**: search `dsh-surfcore-memory-engine`;
102
+ - **Local development**: link this directory into the DSH profile `dependencies` and add the package name to `dsh.profile.bundles`.
103
+
104
+ Restart DSH; a "Memory Engine" section appears in Settings.
105
+
106
+ ## Usage
107
+
108
+ 1. **Locate** — it resolves SurfCore from the DSH profile dependency; if that fails, specify the repository root manually (the directory containing `src/memory-card.js`, not `plugins/surfcore`).
109
+ 2. **Verify** (required) — checks version, file fingerprints, write permission and the engine switch. **Re-verify after every DSH start**; Apply / Rollback / tuning stay disabled until then.
110
+ 3. **Apply** — backs up, writes the three files, sets `memoryCard.engine` to `v2`. Restart DSH afterwards.
111
+ 4. **Rollback** — restores from the backup at any time.
112
+
113
+ ## Tuning (optional)
114
+
115
+ All numbers are editable and saved to `~/.dsh/surfcore/memory-engine.json`; restart DSH to apply. "Restore defaults" deletes the override file. Out-of-range values are clamped.
116
+
117
+ Groups: base scores by source, decay rates and caps, promotion thresholds / bonus days / slots, weight multipliers, recall caps and gains, importance weights, consolidation factor.
118
+
119
+ ## Version compatibility
120
+
121
+ - Supports **SurfCore 0.8.8** only; the plugin version tracks SurfCore's (both `0.8.8`).
122
+ - On a different SurfCore version, Verify fails with an explicit mismatch and refuses to apply.
123
+
124
+ ## Safety
125
+
126
+ - Touches only `src/decay-v2.js` (new), `src/memory-card.js`, `plugins/surfcore/lib/memory-card.js`, plus one config switch;
127
+ - Backs up to `<SurfCore>/.memlogic-backup/<timestamp>/` before writing;
128
+ - Restores automatically if any step fails;
129
+ - Refuses to apply or roll back when the files were changed externally (unknown fingerprint).
130
+
131
+ ## License
132
+
133
+ MIT. This package contains modified files from SurfCore, which is also MIT — see [NOTICE-SurfCore.md](NOTICE-SurfCore.md) for the original copyright notice.
134
+
135
+
package/client.js ADDED
@@ -0,0 +1,441 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-surfcore-memory-engine',
3
+ factory(require) {
4
+ const React = require('react');
5
+ const h = React.createElement;
6
+
7
+ const api = async (path, body) => {
8
+ const res = await fetch(`/api/memory-engine/${path}`, {
9
+ method: body === undefined ? 'GET' : 'POST',
10
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
11
+ cache: 'no-store',
12
+ body: body === undefined ? undefined : JSON.stringify(body),
13
+ });
14
+ return res.json().catch(() => ({}));
15
+ };
16
+
17
+ const box = { border: '1px solid rgba(127,127,127,.28)', borderRadius: 10, padding: '12px 14px', marginBottom: 12 };
18
+ const rowStyle = { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginTop: 8 };
19
+ const btn = (primary, disabled) => ({
20
+ padding: '6px 14px', borderRadius: 8, fontSize: 13,
21
+ cursor: disabled ? 'not-allowed' : 'pointer',
22
+ border: primary ? '1px solid transparent' : '1px solid rgba(127,127,127,.4)',
23
+ background: primary ? '#3b6fe0' : 'transparent',
24
+ color: primary ? '#fff' : 'inherit',
25
+ opacity: disabled ? 0.5 : 1,
26
+ });
27
+ const input = {
28
+ padding: '6px 10px', borderRadius: 8, border: '1px solid rgba(127,127,127,.4)',
29
+ background: 'transparent', color: 'inherit', minWidth: 260, fontSize: 13,
30
+ };
31
+ const numInput = { ...input, minWidth: 96, width: 96 };
32
+ const banner = {
33
+ applied: { text: '✓ 已更新完成 · v2 正在运行', border: '#3f8f4f', bg: 'rgba(63,143,79,.12)' },
34
+ half: { text: '⚠ 文件已更新,但引擎开关没打开', border: '#c08a2e', bg: 'rgba(192,138,46,.12)' },
35
+ outdated: { text: '旧版补丁 · 可以升级到新版引擎', border: '#3b6fe0', bg: 'rgba(59,111,224,.10)' },
36
+ ready: { text: '校验通过,可以应用', border: '#3b6fe0', bg: 'rgba(59,111,224,.10)' },
37
+ blocked: { text: '有项目没通过,不能应用', border: '#c0504d', bg: 'rgba(192,80,77,.12)' },
38
+ };
39
+
40
+ const DAY = 1440;
41
+ const round2 = (value) => Math.round((Number(value) || 0) * 100) / 100;
42
+
43
+ function SettingsPage() {
44
+ const [root, setRoot] = React.useState('');
45
+ const [version, setVersion] = React.useState('');
46
+ const [checks, setChecks] = React.useState([]);
47
+ const [status, setStatus] = React.useState('none');
48
+ const [verifiedAt, setVerifiedAt] = React.useState(0);
49
+ const [manual, setManual] = React.useState('');
50
+ const [showManual, setShowManual] = React.useState(false);
51
+ const [busy, setBusy] = React.useState(false);
52
+ const [msg, setMsg] = React.useState(null);
53
+ const [showPlan, setShowPlan] = React.useState(false);
54
+ const [form, setForm] = React.useState(null);
55
+ const [tuneMsg, setTuneMsg] = React.useState(null);
56
+ const [tuneBusy, setTuneBusy] = React.useState(false);
57
+
58
+ const verified = ['ready', 'applied', 'half', 'outdated'].includes(status);
59
+ const canRollback = ['applied', 'half', 'outdated'].includes(status) && !busy;
60
+ const canApply = (status === 'ready' || status === 'half' || status === 'outdated') && !busy;
61
+
62
+ const applyVerifyData = (data) => {
63
+ setChecks(Array.isArray(data?.checks) ? data.checks : []);
64
+ const next = data?.status ?? 'none';
65
+ setStatus(next);
66
+ if (data?.verifiedAt) setVerifiedAt(Number(data.verifiedAt));
67
+ if (['ready', 'applied', 'half', 'outdated'].includes(next)) void loadTuning();
68
+ };
69
+
70
+ const run = async (label, fn) => {
71
+ setBusy(true);
72
+ setMsg(null);
73
+ try {
74
+ const data = await fn();
75
+ if (data?.ok === false) setMsg({ kind: 'error', text: `${label}:${data?.error ?? '失败'}` });
76
+ return data;
77
+ } catch (error) {
78
+ setMsg({ kind: 'error', text: `${label}失败:${error?.message ?? error}` });
79
+ return null;
80
+ } finally {
81
+ setBusy(false);
82
+ }
83
+ };
84
+
85
+ const doLocate = (path) => run('定位', async () => {
86
+ const data = await api('locate', { path: path ?? '' });
87
+ if (data?.ok) {
88
+ setRoot(data.root ?? '');
89
+ setVersion(data.version ?? '');
90
+ setChecks([]);
91
+ setStatus('none');
92
+ setVerifiedAt(0);
93
+ setForm(null);
94
+ if (!path) setManual(data.root ?? '');
95
+ } else {
96
+ setShowManual(true);
97
+ }
98
+ return data;
99
+ });
100
+
101
+ const doVerify = () => run('校验', async () => {
102
+ const data = await api('verify');
103
+ if (data?.ok !== false) applyVerifyData(data);
104
+ return data;
105
+ });
106
+
107
+ const doVerifySilent = async () => {
108
+ const data = await api('verify');
109
+ if (data?.ok !== false) applyVerifyData(data);
110
+ };
111
+
112
+ const doApply = () => {
113
+ const label = status === 'half' ? '启用 v2' : '应用';
114
+ if (!window.confirm(status === 'half'
115
+ ? '文件已经是新版了,只差把引擎开关打开。现在启用 v2 吗?'
116
+ : '要把记忆引擎换成 v2 吗?会先自动备份,应用后需要重启 DSH。')) return Promise.resolve(null);
117
+ return run(label, async () => {
118
+ const data = await api('apply', {});
119
+ if (data?.ok) {
120
+ setMsg({ kind: 'ok', text: `${data.onlyConfig ? '已把开关切到 v2' : '已应用'}。下一步:${data.next ?? '重启 DSH'}` });
121
+ await doVerifySilent();
122
+ }
123
+ return data;
124
+ });
125
+ };
126
+
127
+ const doRollback = () => {
128
+ if (!window.confirm('要回滚到旧版记忆逻辑吗?回滚后需要重启 DSH。')) return Promise.resolve(null);
129
+ return run('回滚', async () => {
130
+ const data = await api('rollback');
131
+ if (data?.ok) {
132
+ setMsg({ kind: 'ok', text: `已回滚。下一步:${data.next ?? '重启 DSH'}` });
133
+ await doVerifySilent();
134
+ }
135
+ return data;
136
+ });
137
+ };
138
+
139
+ const loadTuning = async () => {
140
+ try {
141
+ const data = await api('tuning');
142
+ if (data?.ok === false) return;
143
+ const d = data.defaults ?? {};
144
+ const c = data.current ?? {};
145
+ const base = { ...(d.base ?? {}), ...(c.base ?? {}) };
146
+ const promote = { ...(d.promote ?? {}), ...(c.promote ?? {}) };
147
+ const recall = { ...(d.recall ?? {}), ...(c.recall ?? {}) };
148
+ const tiers = {
149
+ daily: { ...(d.tiers?.daily ?? {}), ...(c.tiers?.daily ?? {}) },
150
+ long: { ...(d.tiers?.long ?? {}), ...(c.tiers?.long ?? {}) },
151
+ };
152
+ const caps = { ...(d.caps ?? {}), ...(c.caps ?? {}) };
153
+ const mult = { ...(d.mult ?? {}), ...(c.mult ?? {}) };
154
+ const importance = { ...(d.importance ?? {}), ...(c.importance ?? {}) };
155
+ const consolidate = { ...(d.consolidate ?? {}), ...(c.consolidate ?? {}) };
156
+ const num = (value, fallback) => (Number.isFinite(Number(value)) ? Number(value) : fallback);
157
+ const toDays = (score, fallback) => round2(num(score, fallback * DAY) / DAY);
158
+ const cooldownHours = c.recall?.cooldownMs ? c.recall.cooldownMs / 3_600_000 : num(d.recall?.cooldownHours, 6);
159
+ setForm({
160
+ qqOwnerDays: toDays(base['qq-owner'], 3),
161
+ qqSelfDays: toDays(base['qq-self'], 3),
162
+ petOwnerDays: toDays(base['pet-owner'], 1.5),
163
+ petSelfDays: toDays(base['pet-self'], 1.5),
164
+ biliVideoDays: toDays(base['bili-video'], 2),
165
+ biliDynamicDays: toDays(base['bili-dynamic'], 2),
166
+ groupHours: round2(num(base['qq-group'], 30) / 60),
167
+ dailyMinutes: round2(1 / num(tiers.daily?.rate, 1)),
168
+ longMinutes: round2(1 / num(tiers.long?.rate, 1 / 30)),
169
+ initialCapDays: toDays(caps.initialCap, 50_000 / DAY),
170
+ maxScoreYears: round2(num(caps.maxScore, 2_628_000) / DAY / 365),
171
+ path1Days: toDays(promote.path1?.score, 12000 / DAY),
172
+ path1Imp: num(promote.path1?.importance, 60),
173
+ path2Days: toDays(promote.path2?.score, 6000 / DAY),
174
+ path2Imp: num(promote.path2?.importance, 80),
175
+ bonusDays: num(promote.bonusDays, 90),
176
+ permImp: num(promote.path3?.importance, 90),
177
+ longSlots: num(promote.longSlots, 30),
178
+ mult: {
179
+ owner: num(mult.owner, 1.5), replied: num(mult.replied, 1.25),
180
+ emotion: num(mult.emotion, 1.5), relation: num(mult.relation, 1.5),
181
+ first: num(mult.first, 1.5), conflict: num(mult.conflict, 1.5),
182
+ promise: num(mult.promise, 1.5),
183
+ },
184
+ capDaily: 1 + num(tiers.daily?.recallCapPct, 0.5),
185
+ capLong: 1 + num(tiers.long?.recallCapPct, 1.0),
186
+ recall: {
187
+ basePct: round2(num(recall.basePct, 0.2) * 100),
188
+ singleCapPct: round2(num(recall.singleCapPct, 0.3) * 100),
189
+ floorMin: num(recall.floor, 120),
190
+ cooldownHours: round2(cooldownHours),
191
+ emotionBoost: num(recall.emotionBoost, 1.25),
192
+ relationBoost: num(recall.relationBoost, 1.25),
193
+ },
194
+ importance: {
195
+ base: num(importance.base, 20), owner: num(importance.owner, 25),
196
+ self: num(importance.self, 10), direct: num(importance.direct, 15),
197
+ emotionMax: num(importance.emotionMax, 20), relation: num(importance.relation, 15),
198
+ aboutMe: num(importance.aboutMe, 10), first: num(importance.first, 15),
199
+ conflict: num(importance.conflict, 12), promise: num(importance.promise, 10),
200
+ groupIrrelevant: num(importance.groupIrrelevant, -15),
201
+ },
202
+ factor: num(consolidate.factor, 1.2),
203
+ });
204
+ } catch {}
205
+ };
206
+
207
+ const set = (patch) => setForm((current) => ({ ...(current ?? {}), ...patch }));
208
+ const setNested = (group, patch) => setForm((current) => ({ ...(current ?? {}), [group]: { ...(current?.[group] ?? {}), ...patch } }));
209
+
210
+ const saveTuning = async () => {
211
+ if (!form) return;
212
+ setTuneBusy(true);
213
+ setTuneMsg(null);
214
+ try {
215
+ const values = {
216
+ base: {
217
+ 'qq-owner': Math.round(form.qqOwnerDays * DAY),
218
+ 'qq-self': Math.round(form.qqSelfDays * DAY),
219
+ 'pet-owner': Math.round(form.petOwnerDays * DAY),
220
+ 'pet-self': Math.round(form.petSelfDays * DAY),
221
+ 'bili-video': Math.round(form.biliVideoDays * DAY),
222
+ 'bili-dynamic': Math.round(form.biliDynamicDays * DAY),
223
+ 'qq-group': Math.round(form.groupHours * 60),
224
+ },
225
+ mult: form.mult,
226
+ importance: form.importance,
227
+ promote: {
228
+ path1: { score: Math.round(form.path1Days * DAY), importance: Math.round(form.path1Imp) },
229
+ path2: { score: Math.round(form.path2Days * DAY), importance: Math.round(form.path2Imp) },
230
+ path3: { importance: Math.round(form.permImp) },
231
+ bonusDays: Math.round(form.bonusDays),
232
+ longSlots: Math.round(form.longSlots),
233
+ },
234
+ recall: {
235
+ basePct: form.recall.basePct / 100,
236
+ singleCapPct: form.recall.singleCapPct / 100,
237
+ floor: Math.round(form.recall.floorMin),
238
+ cooldownHours: Number(form.recall.cooldownHours),
239
+ emotionBoost: Number(form.recall.emotionBoost),
240
+ relationBoost: Number(form.recall.relationBoost),
241
+ },
242
+ tiers: {
243
+ daily: { rate: 1 / Math.max(0.1, Number(form.dailyMinutes)), recallCapPct: Math.max(0, form.capDaily - 1) },
244
+ long: { rate: 1 / Math.max(0.1, Number(form.longMinutes)), recallCapPct: Math.max(0, form.capLong - 1) },
245
+ },
246
+ caps: {
247
+ initialCap: Math.round(form.initialCapDays * DAY),
248
+ maxScore: Math.round(form.maxScoreYears * 365 * DAY),
249
+ },
250
+ consolidate: { factor: Number(form.factor) },
251
+ };
252
+ const data = await api('tuning', { values });
253
+ setTuneMsg(data?.ok === false ? `保存失败:${data.error}` : '已保存。重启 DSH 后生效。');
254
+ } catch (error) {
255
+ setTuneMsg(`保存失败:${error?.message ?? error}`);
256
+ } finally {
257
+ setTuneBusy(false);
258
+ }
259
+ };
260
+
261
+ const resetTuning = async () => {
262
+ setTuneBusy(true);
263
+ setTuneMsg(null);
264
+ try {
265
+ const data = await api('tuning', { reset: true });
266
+ setTuneMsg(data?.ok === false ? `恢复失败:${data.error}` : '已恢复默认值。重启 DSH 后生效。');
267
+ await loadTuning();
268
+ } catch (error) {
269
+ setTuneMsg(`恢复失败:${error?.message ?? error}`);
270
+ } finally {
271
+ setTuneBusy(false);
272
+ }
273
+ };
274
+
275
+ React.useEffect(() => { void doLocate(''); }, []);
276
+
277
+ const num = (label, value, onChange, unit, step) => h('div', {
278
+ style: { display: 'flex', alignItems: 'center', gap: 6, marginRight: 14, marginTop: 6 },
279
+ },
280
+ h('span', { style: { fontSize: 12, opacity: 0.75, minWidth: 108 } }, label),
281
+ h('input', {
282
+ type: 'number', style: numInput, value: value ?? '',
283
+ step: step ?? '0.1', min: '0',
284
+ onChange: (e) => onChange(Number(e.target.value)),
285
+ }),
286
+ unit ? h('span', { style: { fontSize: 11, opacity: 0.55 } }, unit) : null);
287
+
288
+ const section = (title, open, ...children) => h('details', { open, style: { marginTop: 8 } },
289
+ h('summary', { style: { cursor: 'pointer', fontSize: 13, fontWeight: 600, opacity: 0.9 } }, title),
290
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', marginTop: 2 } }, ...children));
291
+
292
+ const bannerInfo = banner[status] ?? null;
293
+ const applyLabel = status === 'half' ? '启用 v2' : (status === 'applied' ? '已完成' : '应用');
294
+
295
+ return h('div', null,
296
+ showPlan ? h('div', {
297
+ style: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,.72)', zIndex: 9999, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 },
298
+ onClick: () => setShowPlan(false),
299
+ },
300
+ h('img', {
301
+ src: '/api/memory-engine/plan.svg', alt: '记忆逻辑方案图',
302
+ style: { maxWidth: '100%', maxHeight: '100%', borderRadius: 12, boxShadow: '0 12px 48px rgba(0,0,0,.6)' },
303
+ })) : null,
304
+
305
+ h('div', { style: { fontSize: 15, fontWeight: 600, marginBottom: 4 } }, '记忆引擎'),
306
+ h('div', { style: { opacity: 0.7, fontSize: 13, marginBottom: 8 } },
307
+ '把 SurfCore 的记忆逻辑升级成“分数引擎”。只改记忆那部分,不碰其他功能。'),
308
+ h('button', { style: { ...btn(false, false), marginBottom: 10 }, onClick: () => setShowPlan(true) }, '查看方案图'),
309
+
310
+ bannerInfo ? h('div', { style: { ...box, borderColor: bannerInfo.border, background: bannerInfo.bg, fontWeight: 600 } }, bannerInfo.text) : null,
311
+
312
+ h('div', { style: box },
313
+ h('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 6 } }, '第 1 步 · 找到 SurfCore'),
314
+ h('div', null, root ? `位置:${root}${version ? `(v${version})` : ''}` : '还没定位。'),
315
+ h('div', { style: rowStyle },
316
+ h('button', { style: btn(!root, busy), disabled: busy, onClick: () => void doLocate('') }, '自动定位'),
317
+ h('button', { style: btn(false, busy), disabled: busy, onClick: () => setShowManual((v) => !v) }, showManual ? '收起手动' : '手动指定'),
318
+ ),
319
+ showManual ? h('div', { style: rowStyle },
320
+ h('input', { style: input, placeholder: 'SurfCore 根目录(有 src/memory-card.js 的那层)', value: manual, onChange: (e) => setManual(e.target.value) }),
321
+ h('button', { style: btn(false, busy), disabled: busy, onClick: () => void doLocate(manual) }, '用这个路径'),
322
+ ) : null,
323
+ ),
324
+
325
+ h('div', { style: box },
326
+ h('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 6 } }, '第 2 步 · 校验'),
327
+ h('div', { style: rowStyle },
328
+ h('button', { style: btn(false, busy || !root), disabled: busy || !root, onClick: () => void doVerify() }, '校验'),
329
+ ),
330
+ checks.length ? h('div', { style: { marginTop: 10, fontSize: 13 } },
331
+ checks.map((check, index) => h('div', { key: index, style: { margin: '3px 0' } },
332
+ `${check.ok ? '✓' : '✗'} ${check.name}${check.detail ? ` — ${check.detail}` : ''}`)),
333
+ ) : null,
334
+ status === 'blocked' ? h('div', { style: { marginTop: 8, fontSize: 13, color: '#e0a24a' } },
335
+ '版本不对的话,去 SurfCore 发布页换成对应版本再试。') : null,
336
+ ),
337
+
338
+ h('div', { style: box },
339
+ h('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 6 } }, '第 3 步 · 应用 / 回滚'),
340
+ h('div', { style: rowStyle },
341
+ h('button', { style: btn(true, !canApply), disabled: !canApply, onClick: () => void doApply() }, applyLabel),
342
+ h('button', { style: btn(false, !canRollback), disabled: !canRollback, onClick: () => void doRollback() }, '回滚'),
343
+ ),
344
+ h('div', { style: { marginTop: 8, fontSize: 12, opacity: 0.7 } },
345
+ !verified ? '应用和回滚都要先通过“校验”;文件改过之后也要重新校验。'
346
+ : (status === 'applied' ? '这台机器已经是新版,不需要再应用;需要退回旧版可以点回滚。'
347
+ : '应用前会自动备份;应用后需要重启 DSH 才生效。')),
348
+ verifiedAt ? h('div', { style: { marginTop: 4, fontSize: 12, opacity: 0.5 } },
349
+ `上次校验:${new Date(verifiedAt).toLocaleTimeString('zh-CN', { hour12: false })}`) : null,
350
+ ),
351
+
352
+ h('div', { style: box },
353
+ h('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 6 } }, '数值(可选)'),
354
+ h('div', { style: { fontSize: 12, opacity: 0.65 } }, '不填就用默认值。改完保存,重启 DSH 后生效。'),
355
+ !verified ? h('div', { style: { fontSize: 13, opacity: 0.6, marginTop: 8 } }, '先点上面的“校验”,通过后才能调数值。')
356
+ : (!form ? h('div', { style: { fontSize: 13, opacity: 0.6, marginTop: 8 } }, '正在读取数值…') : h('div', null,
357
+ section('底分(按来源)', true,
358
+ num('私聊 · 主人', form.qqOwnerDays, (v) => set({ qqOwnerDays: v }), '天', '0.5'),
359
+ num('私聊 · 我自己', form.qqSelfDays, (v) => set({ qqSelfDays: v }), '天', '0.5'),
360
+ num('桌宠 · 主人', form.petOwnerDays, (v) => set({ petOwnerDays: v }), '天', '0.5'),
361
+ num('桌宠 · 我自己', form.petSelfDays, (v) => set({ petSelfDays: v }), '天', '0.5'),
362
+ num('B站 · 视频', form.biliVideoDays, (v) => set({ biliVideoDays: v }), '天', '0.5'),
363
+ num('B站 · 动态', form.biliDynamicDays, (v) => set({ biliDynamicDays: v }), '天', '0.5'),
364
+ num('群聊', form.groupHours, (v) => set({ groupHours: v }), '小时', '0.5'),
365
+ ),
366
+ section('流速 · 上限', false,
367
+ num('日常档流速', form.dailyMinutes, (v) => set({ dailyMinutes: v }), '1 分 = ? 分钟', '1'),
368
+ num('长期档流速', form.longMinutes, (v) => set({ longMinutes: v }), '1 分 = ? 分钟', '1'),
369
+ num('初始分封顶', form.initialCapDays, (v) => set({ initialCapDays: v }), '天', '1'),
370
+ num('最久时长上限', form.maxScoreYears, (v) => set({ maxScoreYears: v }), '年', '0.5'),
371
+ ),
372
+ section('升档门槛 · 赠送 · 名额', false,
373
+ num('门槛一(分数)', form.path1Days, (v) => set({ path1Days: v }), '天', '0.5'),
374
+ num('门槛一(重要度)', form.path1Imp, (v) => set({ path1Imp: v }), '分', '1'),
375
+ num('门槛二(分数)', form.path2Days, (v) => set({ path2Days: v }), '天', '0.5'),
376
+ num('门槛二(重要度)', form.path2Imp, (v) => set({ path2Imp: v }), '分', '1'),
377
+ num('赠送额度', form.bonusDays, (v) => set({ bonusDays: v }), '天', '1'),
378
+ num('永久线(重要度)', form.permImp, (v) => set({ permImp: v }), '分', '1'),
379
+ num('长期档名额', form.longSlots, (v) => set({ longSlots: v }), '条', '1'),
380
+ ),
381
+ section('加权倍率', false,
382
+ num('你亲口说的', form.mult.owner, (v) => setNested('mult', { owner: v }), '×', '0.05'),
383
+ num('我回过话', form.mult.replied, (v) => setNested('mult', { replied: v }), '×', '0.05'),
384
+ num('带情绪', form.mult.emotion, (v) => setNested('mult', { emotion: v }), '×', '0.05'),
385
+ num('关于我们', form.mult.relation, (v) => setNested('mult', { relation: v }), '×', '0.05'),
386
+ num('第一次', form.mult.first, (v) => setNested('mult', { first: v }), '×', '0.05'),
387
+ num('冲突 / 和好', form.mult.conflict, (v) => setNested('mult', { conflict: v }), '×', '0.05'),
388
+ num('承诺 / 约定', form.mult.promise, (v) => setNested('mult', { promise: v }), '×', '0.05'),
389
+ ),
390
+ section('封顶倍率 · 加分', false,
391
+ num('日常档封顶', form.capDaily, (v) => set({ capDaily: v }), '倍', '0.1'),
392
+ num('长期档封顶', form.capLong, (v) => set({ capLong: v }), '倍', '0.1'),
393
+ num('单次基础加分', form.recall.basePct, (v) => setNested('recall', { basePct: v }), '%', '1'),
394
+ num('单次上限', form.recall.singleCapPct, (v) => setNested('recall', { singleCapPct: v }), '%', '1'),
395
+ num('保底', form.recall.floorMin, (v) => setNested('recall', { floorMin: v }), '分钟', '10'),
396
+ num('冷却', form.recall.cooldownHours, (v) => setNested('recall', { cooldownHours: v }), '小时', '1'),
397
+ num('情绪加成', form.recall.emotionBoost, (v) => setNested('recall', { emotionBoost: v }), '×', '0.05'),
398
+ num('关系加成', form.recall.relationBoost, (v) => setNested('recall', { relationBoost: v }), '×', '0.05'),
399
+ ),
400
+ section('重要度各项', false,
401
+ num('底', form.importance.base, (v) => setNested('importance', { base: v }), '分', '1'),
402
+ num('你说的', form.importance.owner, (v) => setNested('importance', { owner: v }), '分', '1'),
403
+ num('我自己的话', form.importance.self, (v) => setNested('importance', { self: v }), '分', '1'),
404
+ num('直接对我说', form.importance.direct, (v) => setNested('importance', { direct: v }), '分', '1'),
405
+ num('情绪上限', form.importance.emotionMax, (v) => setNested('importance', { emotionMax: v }), '分', '1'),
406
+ num('关于我们', form.importance.relation, (v) => setNested('importance', { relation: v }), '分', '1'),
407
+ num('关于我自己', form.importance.aboutMe, (v) => setNested('importance', { aboutMe: v }), '分', '1'),
408
+ num('第一次', form.importance.first, (v) => setNested('importance', { first: v }), '分', '1'),
409
+ num('冲突 / 和好', form.importance.conflict, (v) => setNested('importance', { conflict: v }), '分', '1'),
410
+ num('承诺 / 约定', form.importance.promise, (v) => setNested('importance', { promise: v }), '分', '1'),
411
+ num('群聊无关', form.importance.groupIrrelevant, (v) => setNested('importance', { groupIrrelevant: v }), '分', '1'),
412
+ ),
413
+ section('提炼', false,
414
+ num('提炼加成', form.factor, (v) => set({ factor: v }), '×', '0.1'),
415
+ ),
416
+ )),
417
+ h('div', { style: rowStyle },
418
+ h('button', { style: btn(false, !verified || tuneBusy || !form), disabled: !verified || tuneBusy || !form, onClick: () => void saveTuning() }, '保存数值'),
419
+ h('button', { style: btn(false, !verified || tuneBusy), disabled: !verified || tuneBusy, onClick: () => void resetTuning() }, '恢复默认'),
420
+ ),
421
+ tuneMsg ? h('div', { style: { marginTop: 8, fontSize: 13 } }, tuneMsg) : null,
422
+ ),
423
+
424
+ msg ? h('div', { style: { ...box, borderColor: msg.kind === 'error' ? '#c0504d' : '#4a8f4a' } }, msg.text) : null,
425
+ );
426
+ }
427
+
428
+ return {
429
+ inject: ['slots'],
430
+ apply(ctx) {
431
+ ctx.slots.inject('settings.section', () =>
432
+ ctx.slots.register(
433
+ { name: 'settings.section', id: 'memory-engine', order: 70, label: '记忆引擎' },
434
+ SettingsPage,
435
+ ),
436
+ );
437
+ },
438
+ };
439
+ },
440
+ });
441
+
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: memory-engine
3
+ name: 'dsh-surfcore-memory-engine'