dsh-fish-memory 0.0.0-stage → 1.4.4

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/lib/client.js ADDED
@@ -0,0 +1,4043 @@
1
+ // DSH 鱼的记忆 · 客户端半边(设置左侧栏一项「鱼的记忆」)
2
+ window.__ModuleLoader__.load({
3
+ id: 'dsh-fish-memory',
4
+ factory: (require) => {
5
+ var module = { exports: {} }
6
+ var exports = module.exports
7
+
8
+ const React = require('react')
9
+ const inject = ['slots']
10
+ const B = '/dsh-fish-memory'
11
+
12
+ const get = (p) => fetch(B + p, { cache: 'no-store' }).then((r) => r.json())
13
+ const post = (p, body) =>
14
+ fetch(B + p, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body || {}) }).then((r) => r.json())
15
+
16
+ const fmt = (t) => {
17
+ if (!t) return '(没记时间)'
18
+ const d = new Date(t)
19
+ const p = (n) => String(n).padStart(2, '0')
20
+ return p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes())
21
+ }
22
+ const rel = (t, now) => {
23
+ if (!t) return '(没记时间)'
24
+ const dayIdx = (x) => Math.floor((new Date(x).getTime() + 8 * 3600000) / 86400000)
25
+ const days = dayIdx(now) - dayIdx(t)
26
+ if (!isFinite(days)) return fmt(t)
27
+ // ⚠⚠ 2026-09-22 修(反馈:「为什么还有今天的事儿」):
28
+ // 原来是 `if (days <= 0) return '今天 …'` —— **负数的(未来的)也被算成今天** ✗
29
+ // 实测:一条日期在未来的记忆→ 页面显示「今天 12:00」✗✗
30
+ // → 今天 / 明天 / N 天后 分开写 ✓
31
+ if (days === 0) return '今天 ' + fmt(t).slice(6)
32
+ if (days === -1) return '明天 ' + fmt(t).slice(6)
33
+ if (days < 0) {
34
+ const 后 = -days
35
+ if (后 < 30) return 后 + ' 天后'
36
+ return fmt(t).slice(0, 5) + '(' + Math.floor(后 / 30) + ' 个月后)'
37
+ }
38
+ if (days === 1) return '昨天 ' + fmt(t).slice(6)
39
+ if (days < 30) return days + ' 天前'
40
+ // ⭐ 2026-09-21 晚:**跨年了就必须带年份** —— 不然「09-17」看不出是哪一年 ✗
41
+ // (反馈:的:「过了一年会怎么显示呢?我现在看没有年份,只有月份」)
42
+ const 今年 = new Date(new Date(now).getTime() + 8 * 3600000).getUTCFullYear()
43
+ const 那年 = new Date(new Date(t).getTime() + 8 * 3600000).getUTCFullYear()
44
+ if (那年 !== 今年) {
45
+ const 差 = 今年 - 那年
46
+ if (差 === 1) return '去年 ' + fmt(t).slice(0, 5)
47
+ return 那年 + '-' + fmt(t).slice(0, 5) + '(' + 差 + ' 年前)'
48
+ }
49
+ // ⚠⚠ 2026-09-22 修(反馈:「不是两个月前啊」):
50
+ // 原来是 `Math.round(days / 30)` —— **四舍五入**把 47 天说成了「2 个月」✗
51
+ // 47 天明明才一个半月。→ 改成**只进不退**(floor),零头够 15 天就说「个多月」✓
52
+ const 月 = Math.floor(days / 30)
53
+ const 零头 = days - 月 * 30
54
+ return fmt(t).slice(0, 5) + '(' + 月 + (零头 >= 15 ? ' 个多' : ' 个') + '月前)'
55
+ }
56
+ const impName = (v) => (v >= 1 ? '非常重要' : v >= 0.5 ? '比较重要' : '随口提过')
57
+
58
+ // ---------- 提炼 / 扩词的进度放在**组件外面** ----------
59
+ // 为什么:跑一轮要十几二十分钟,她中途切走再回来,进度不该消失 ✓
60
+ // ⚠ 两个**各占一格** —— 共用一个格子的话,两个一起跑会互相盖,
61
+ // 看起来就像「点了扩词,过一会儿自己取消了」✗(2026-09-20 踩到)
62
+ const 进度 = { 扩词: { 状态: null, 说明: null }, 提炼: { 状态: null, 说明: null }, 订阅: new Set() }
63
+ // ⭐ 2026-09-22(反馈「点了判断,但他没有弹进度条啊,他就卡着判着呢」):
64
+ // 「补标不淡」原来**只有一行字、没有进度条** —— 跑起来看不出它在动 ✗
65
+ // (其实一直在跑,日志里一批批都有;她以为卡死了)
66
+ // → 给它也配一条进度条 ✓(放在组件外面,切走再回来还在)
67
+ // ⚠ 2026-09-22(她的反馈):这条原来跟提炼/扩词挤在同一张卡里,
68
+ // 点了重判得往上翻才看得见 ✗ → 挪到按钮行下面,并记下**是哪一种**:
69
+ // 「补标不淡」还是「重判一遍」,进度条上写清楚 ✓
70
+ const 补标进度 = { 已: 0, 总: 0, 在跑: false, 重判: false }
71
+ // ⭐⭐ 2026-09-23:**清洗补键**的进度(老数据补事实键)✓
72
+ // 跟 补标 分开一份 —— 两条进度条同时跑的时候不许互相盖 ✗
73
+ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
74
+ const 改进度 = (格, 状态, 说明) => {
75
+ const s = 进度[格] || (进度[格] = { 状态: null, 说明: null })
76
+ s.状态 = 状态
77
+ s.说明 = 说明
78
+ for (const f of 进度.订阅) {
79
+ try {
80
+ f()
81
+ } catch (e) {}
82
+ }
83
+ }
84
+
85
+ // 一次别画太多行 —— 库里 800 多条的时候,全画出来整个页面都会卡(连别的插件面板一起卡)✗
86
+ const 一次最多画 = 200
87
+
88
+ // 权重在页面上按百分比显示 ✓(0.55 → 55)
89
+ const toPct = (v) => Math.round(Number(v) * 1000) / 10
90
+ const fromPct = (v) => Math.round(Number(v) * 10) / 1000
91
+
92
+ const S = {
93
+ wrap: { padding: '16px 6px', fontSize: 13, lineHeight: 1.8, maxWidth: 780 },
94
+ dim: { color: 'var(--dsw-alias-label-tertiary, #8a8a8a)' },
95
+ h: { fontSize: 14, margin: '20px 0 8px', fontWeight: 600 },
96
+ // ⚠⚠ 2026-09-23(她的反馈:「右边堆的东西太多,左边的文字成了竖着的一条」):
97
+ // 这一行**必须能换行** ✗
98
+ // 原来没有 flexWrap —— 行里全是 flex:'none' 的牌子/列(日期、鲜活度、重要度、原文、改、删…),
99
+ // 它们一多,那条 flex:1 的正文就被挤成**竖着的一条**(一个字一行)。
100
+ // 清洗补键给两百多条补上键之后,行里又多一个牌子 → 立刻犯病。
101
+ // 加 flexWrap:装不下就让牌子们**掉到下一行**,正文始终有地方 ✓
102
+ // (跟上面 nameBox 那条是同一个教训)
103
+ // ⚠⚠ 2026-09-24(她说的「能搜到它的词和讲的是,移到下一条的那一格里面去了」):
104
+ // 分隔线**原来画在「行」上** —— 可每条下面还有一两行小字
105
+ // (「提炼前:…」「能搜到它的词:…」「讲的是:…」),
106
+ // 它们在线的**下面** → 看着就像属于**下一条**了 ✗
107
+ // → 线挪到「整条」上(见 S.条),行自己不再画线 ✓
108
+ row: { padding: '6px 8px', display: 'flex', flexWrap: 'wrap', gap: 10, rowGap: 2, alignItems: 'baseline' },
109
+ // 「一整条」的分隔线:**连着下面那几行小字一起**框住 ✓
110
+ 条: { borderBottom: '1px solid rgba(128,128,128,.15)' },
111
+ btn: { padding: '3px 10px', borderRadius: 6, border: '1px solid rgba(128,128,128,.35)', background: 'transparent', color: 'inherit', cursor: 'pointer', fontSize: 12 },
112
+ inp: { width: 72, padding: '2px 6px', borderRadius: 5, border: '1px solid rgba(128,128,128,.35)', background: 'transparent', color: 'inherit', fontSize: 12 },
113
+ link: { color: 'var(--dsw-alias-label-secondary, #8fa8c8)', cursor: 'pointer', textDecoration: 'underline' },
114
+ confirm: { display: 'flex', gap: 8, alignItems: 'center', margin: '4px 0 8px 8px', padding: '6px 10px', borderRadius: 6, background: 'rgba(200,120,120,.12)', border: '1px solid rgba(200,120,120,.35)', fontSize: 12 },
115
+ // ⚠ flexWrap:放大或窗口窄的时候要能换行,不然会横向撑出容器(右边就看不见了)✗
116
+ nameBox: { display: 'flex', flexWrap: 'wrap', gap: 8, rowGap: 6, alignItems: 'center', margin: '10px 0', padding: '6px 10px', borderRadius: 6, background: 'rgba(128,128,128,.10)', border: '1px solid rgba(128,128,128,.22)', fontSize: 12 },
117
+ editBox: { margin: '4px 0 10px 8px', padding: '8px 10px', borderRadius: 6, background: 'rgba(128,128,128,.10)', border: '1px solid rgba(128,128,128,.28)' },
118
+ askBox: { display: 'flex', gap: 8, alignItems: 'center', margin: '8px 0', padding: '8px 10px', borderRadius: 6, background: 'rgba(224,176,112,.12)', border: '1px solid rgba(224,176,112,.4)', fontSize: 12 },
119
+ barOut: { height: 10, borderRadius: 5, background: 'rgba(128,128,128,.20)', overflow: 'hidden', border: '1px solid rgba(128,128,128,.25)' },
120
+ barIn: { height: '100%', background: 'linear-gradient(90deg,#5b8fd6,#8fd0a0)', transition: 'width .25s' },
121
+ area: { width: '100%', boxSizing: 'border-box', padding: '6px 8px', borderRadius: 5, border: '1px solid rgba(128,128,128,.35)', background: 'transparent', color: 'inherit', fontSize: 13, lineHeight: 1.7, fontFamily: 'inherit', resize: 'vertical' },
122
+ btnDanger: { padding: '3px 10px', borderRadius: 6, border: '1px solid rgba(200,120,120,.6)', background: 'rgba(200,120,120,.18)', color: 'inherit', cursor: 'pointer', fontSize: 12 },
123
+ ok: { color: '#8fd0a0' },
124
+ drop: { color: '#c98080' },
125
+ warn: { color: '#e0b070' },
126
+ }
127
+
128
+ const GROUPS = [
129
+ ['weight', '权重', '决定「什么更容易被想起来」。四项加起来必须是 100% —— 你改一项,其余三项自动让位,你填的数会尽量保住。'],
130
+ ['time', '遗忘速度', '数字越大 = 忘得越慢。淡了只是不容易被想起来,不会删。'],
131
+ ['gate', '门槛与条数', '控制「一次塞多少给她」。'],
132
+ ]
133
+
134
+ // ⭐ 版本 / 检查更新 —— 单独一条,不掺进那个大页面(改它的渲染风险太大)
135
+ const 更新命令 = ['dsh plugin --profile web remove dsh-fish-memory', 'dsh plugin --profile web add dsh-fish-memory'].join(String.fromCharCode(10))
136
+ function 更新条() {
137
+ const [up, setUp] = React.useState(null)
138
+ const [busy, setBusy] = React.useState(false)
139
+ const 查 = async () => {
140
+ setBusy(true)
141
+ try { setUp(await get('/update?refresh=1')) } catch (e) { setUp({ ok: false, error: String((e && e.message) || e) }) }
142
+ setBusy(false)
143
+ }
144
+ React.useEffect(() => { get('/update').then((r) => setUp(r)).catch(() => {}) }, [])
145
+ const 说 = !up ? '正在看有没有新版…'
146
+ : up.hasUpdate ? '仓库里已经有 v' + up.latest + ' 了'
147
+ : up.ok ? '已经是最新版' : '暂时查不到(' + (up.error || '网络') + ')—— 这不代表没有问题'
148
+ return React.createElement(
149
+ 'div',
150
+ { style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '10px 12px', marginBottom: 14, borderRadius: 10, background: 'rgba(127,127,127,.07)' } },
151
+ React.createElement(
152
+ 'div',
153
+ { style: { display: 'flex', alignItems: 'center', gap: 10 } },
154
+ React.createElement('div', { style: { flex: 1, fontSize: 13 } }, '插件版本 ' + ((up && up.current) || '—') + ' · ' + 说),
155
+ React.createElement('button', { style: S.btn, disabled: busy, onClick: 查 }, busy ? '查…' : '检查更新'),
156
+ ),
157
+ (up && up.hasUpdate)
158
+ ? React.createElement(
159
+ 'div',
160
+ null,
161
+ React.createElement('div', { style: S.dim }, '更新方法:先卸旧版再装新版(直接装可能拿到缓存里的旧副本)。'),
162
+ React.createElement('div', { style: { fontFamily: 'ui-monospace, Consolas, monospace', fontSize: 11.5, whiteSpace: 'pre-wrap', wordBreak: 'break-all', padding: '8px 10px', borderRadius: 7, background: 'rgba(127,127,127,.1)' } }, 更新命令),
163
+ React.createElement('button', {
164
+ style: S.btn,
165
+ onClick: () => navigator.clipboard.writeText(更新命令).catch(() => {}),
166
+ }, '复制命令'),
167
+ )
168
+ : null,
169
+ )
170
+ }
171
+
172
+ function Section() {
173
+ const [d, setD] = React.useState(null)
174
+ const [err, setErr] = React.useState(null)
175
+ const [open, setOpen] = React.useState(null) // 打开哪个会话(两级结构 ✓)
176
+ const [params, setParams] = React.useState(null)
177
+ const [pin, setPin] = React.useState(null) // 她刚改的那一项权重
178
+ const [editing, setEditing] = React.useState(null) // 正在打字的那格(原样显示她敲的字)
179
+ const [notes, setNotes] = React.useState(null)
180
+ const [busy, setBusy] = React.useState(false)
181
+ const [confirm, setConfirm] = React.useState(null) // 正在等确认的删除(二次确认,手滑不了)
182
+ const [showDeleted, setShowDeleted] = React.useState(false)
183
+ const [selfDraft, setSelfDraft] = React.useState({}) // 每个会话「她自己叫什么」的草稿
184
+ const [ownerDraft, setOwnerDraft] = React.useState({}) // 每个会话「怎么称呼对方」的草稿
185
+ const [pronounDraft, setPronounDraft] = React.useState(null) // 用户的称呼草稿
186
+ const [edit, setEdit] = React.useState(null) // 正在改的那条:{id, text, importance}
187
+ const [pronounAsk, setPronounAsk] = React.useState(null) // 改了称呼,问要不要连旧记忆一起改
188
+ const [imp, setImp] = React.useState(null) // 档案导入:扫描结果 + 勾选状态
189
+ const [impBusy, setImpBusy] = React.useState(false)
190
+ const [impNote, setImpNote] = React.useState(null)
191
+ const [impOpen, setImpOpen] = React.useState(false)
192
+ // 进度放在组件外面(见上面「进度」那段)—— 她切走再回来,进度不该消失 ✓
193
+ const [exp, setExpState] = React.useState(进度.扩词.状态)
194
+ const [expNote, setExpNoteState] = React.useState(进度.扩词.说明)
195
+ const [rf, setRfState] = React.useState(进度.提炼.状态)
196
+ const [rfNote, setRfNoteState] = React.useState(进度.提炼.说明)
197
+ const [复制开, set复制开] = React.useState(false) // 「复制记忆到别的会话」展开没有
198
+ const [复制到, set复制到] = React.useState('') // 复制到哪个会话
199
+ const [搜词, set搜词] = React.useState('') // 记忆列表的检索词
200
+ const setExp = (v) => 改进度('扩词', v, 进度.扩词.说明)
201
+ const setExpNote = (v) => 改进度('扩词', 进度.扩词.状态, v)
202
+ const setRf = (v) => 改进度('提炼', v, 进度.提炼.说明)
203
+ const setRfNote = (v) => 改进度('提炼', 进度.提炼.状态, v)
204
+ const [画全部, set画全部] = React.useState(false) // 记忆列表要不要一次画完(默认只画前 200)
205
+ const [看记忆, set看记忆] = React.useState(false) // 记忆列表展开没有(默认折叠,不然把按钮顶到很下面)
206
+ // ⭐ 2026-09-21:列表顺序。记忆库是 push 顺序(老的在前),而下面只画前 200 条 ——
207
+ // 不翻过来的话,她永远看不到刚写进来的那条(851 条里画的是最老的 200 条)。
208
+ // '新' = 最新的在最上面(默认);'旧' = 最早的在前(想从头翻的时候用)
209
+ const [顺序, set顺序] = React.useState('新')
210
+ // ⭐ 2026-09-24(反馈):**「记了时间的」和「没记时间的」混在一坨了** ✗
211
+ // 库里 850 条只有 188 条有日期 —— 混在一起排,「中间那一段」全是没日期的,排序就白排了。
212
+ // 原话:「要么就是这样分类,记了时间的和没记时间的分开……
213
+ // 不然的话记和没记的就全混在一起了,这个排序就没办法用了」
214
+ // 她要的是**自己切**(不是固定的两块)→ 三种视图轮着切 ✓
215
+ // '全部' = 老样子;'有时间' = 只看有日期的;'没时间' = 只看没日期的
216
+ const [看哪边, set看哪边] = React.useState('全部')
217
+ // ⭐ 2026-09-22(她要的):长期事实那个「收理的文件夹」展开没有(**默认收着**)
218
+ const [看事实, set看事实] = React.useState(false)
219
+ // ⭐ 2026-09-22:参数那一坨默认收起来(它调一次就不动)
220
+ const [看参数, set看参数] = React.useState(false)
221
+ // ⭐⭐ 2026-09-22(反馈:「查重我们是不是还没找到按钮」):
222
+ // 去重一直有,但都是**自动**的(召回时、导入时)—— **库里已有的重复没人管** ✗
223
+ // 这里补一个手动查重:列出来、挑、才合 ✓
224
+ const [查重, set查重] = React.useState(null) // 查重结果:{组, 真重复组数, 汇总组数, 能少}
225
+ const [查重选, set查重选] = React.useState({}) // 第几组 → 留哪条的 id
226
+ // ⭐⭐ 2026-09-23(她真被坑过一次):**哪几组要合**。
227
+ // 原来「按我选的合」是**把每一组都合掉** —— 而每组默认都选中了,
228
+ // 于是一点就把「汇总+明细」那 105 组也合了,**123 条记忆没了**(回收站还被清空)✗
229
+ // → 改成:**「汇总+明细」默认不参与**,要合得一组一组自己勾 ✓
230
+ const [查重合, set查重合] = React.useState({}) // 第几组 → 要不要合
231
+ const [查重开, set查重开] = React.useState(false) // 面板展开没有
232
+ // ⭐⭐ 2026-09-22(她要的):**批量删除**,但**必须二次确认** ✓
233
+ // 原话:「再出一个批量删除,但还是需要 2 次确认的」
234
+ // ① 勾选(每条行首一个框)② 点「批量删」→ 出一个确认条 ③ 点「确定删」才真删
235
+ // 删掉的是**收进「已删」**(不是销毁),能找回来 ✓
236
+ const [选中, set选中] = React.useState({}) // 勾了哪些 id
237
+ const [批删问, set批删问] = React.useState(false) // 二次确认条出来了没
238
+ // ⭐ 2026-09-22(反馈「回收站还没有批量删除,就批量清空」):
239
+ // 「已删掉的」那一栏原来只有「一条条清」和「全清空」→ 补一个**批量清掉** ✓
240
+ // ⚠ 这个是**真销毁**(不是收起来)→ 界面必须问两遍 ✓
241
+ const [选中清, set选中清] = React.useState({}) // 「已删」里勾了哪些
242
+ const [批清问, set批清问] = React.useState(false)
243
+ // ⭐⭐ 2026-09-23(反馈「这里没有批量找回呀」):批量找回的确认
244
+ const [批回问, set批回问] = React.useState(false)
245
+ const [mdl, setMdl] = React.useState(null) // DSH 里有哪些模型可选
246
+ // ⭐⭐ 2026-09-22(她的第十件):**从会话生成档案** —— 给别人用的功能
247
+ // 用户导入自己的会话 → 模型读一遍 → 出一份档案 → 再走「从旧档案导入」变成条目
248
+ // ⚠ 四步分开,因为**第三步调模型、花钱**,得一批一批停下来看 ✓
249
+ const [生成开, set生成开] = React.useState(false) // 面板展开没有
250
+ const [生成会话, set生成会话] = React.useState(null) // 这个工作区有哪些会话
251
+ const [生成选, set生成选] = React.useState({}) // 勾了哪几个
252
+ const [生成计划, set生成计划] = React.useState(null) // 算出来的:几批、多少字、多少 token
253
+ const [生成条目, set生成条目] = React.useState([]) // 攒下来的条目
254
+ const [生成进度, set生成进度] = React.useState(null) // {批号, 批数, 在跑, 模型}
255
+ const [生成档, set生成档] = React.useState(null) // 存好了:{存到, 条数, 预览}
256
+ const [生成备, set生成备] = React.useState(null) // 给模型用的备用模型(可选)
257
+ const [生成诊断, set生成诊断] = React.useState(null) // 列表跟 DSH 对不上时,看这几个数
258
+ // ⭐⭐ 2026-09-22(反馈:「点存档案的地方应该自定义」):
259
+ // 原来存到哪儿是我**写死的**(`.dsh\生成的档案.md`)✗ → 能打、能选 ✓
260
+ const [生成存到, set生成存到] = React.useState('')
261
+ const [生成目录列, set生成目录列] = React.useState(null) // 文件夹选择器的列表
262
+ const [生成选目录, set生成选目录] = React.useState(false)
263
+ const [impDir, setImpDir] = React.useState(null) // 档案目录草稿
264
+ const [impNames, setImpNames] = React.useState(null) // 「指她的名字」草稿
265
+ const [impDirs, setImpDirs] = React.useState(null) // 文件夹选择器的列表
266
+ const [找名字, set找名字] = React.useState(null) // 自动找出来的称呼候选
267
+ const [看原文, set看原文] = React.useState({}) // 哪几条在对照「提炼前」的原文
268
+ // ---- 2026-09-21 新加:后台手动加一条记忆 ----
269
+ const [加内容, set加内容] = React.useState('')
270
+ const [加时间, set加时间] = React.useState('') // 留空 = 现在
271
+ const [加重要, set加重要] = React.useState('0.5') // 三档是快捷,也能自己填
272
+ const [加词, set加词] = React.useState('') // 关键词,空格分开
273
+ const [加在谁, set加在谁] = React.useState('') // 加到哪个会话(默认第一个开着的,没有就第一个)
274
+ const [加结果, set加结果] = React.useState(null) // { ok, 文字 }
275
+ const [加展开, set加展开] = React.useState(false)
276
+
277
+ // 「指她的名字」那一格:读出来、点一下加/减 ✓
278
+ const 名字串 = () => (impNames !== null ? impNames : (imp.data && imp.data.用户名字 ? imp.data.用户名字 : []).join(','))
279
+ const 名字们 = () => 名字串().split(/[,,]/).map((x) => x.trim()).filter(Boolean)
280
+ const 已选名字 = (n) => 名字们().indexOf(n) >= 0
281
+ const 点名字 = (n) => {
282
+ const 有 = 名字们()
283
+ const i = 有.indexOf(n)
284
+ if (i >= 0) 有.splice(i, 1)
285
+ else 有.push(n)
286
+ setImpNames(有.join(','))
287
+ }
288
+ // 「这个不是名字」—— 昵称、旧事里的称呼…… 标了就存起来,以后不再出现、也不会被换掉 ✓
289
+ const 标掉名字 = (n) => {
290
+ const 现有 = 找名字 && Array.isArray(找名字.不是名字) ? 找名字.不是名字.slice() : []
291
+ if (现有.indexOf(n) < 0) 现有.push(n)
292
+ post('/names', { notNames: 现有 })
293
+ .then(() => post('/names/scan', { session: open, dir: 现在目录 }))
294
+ .then((r) => {
295
+ set找名字(r)
296
+ const 剩 = 名字们().filter((x) => x !== n)
297
+ setImpNames(剩.join(','))
298
+ })
299
+ .catch((e) => setExpNote('标不了:' + String((e && e.message) || e)))
300
+ }
301
+
302
+ // 一个候选名字块:点名字=加/减,点 ✕ = 标成「不是名字」✓
303
+ const 一个名字块 = (x, 淡) =>
304
+ React.createElement(
305
+ 'span',
306
+ {
307
+ key: x.name,
308
+ style: Object.assign({}, S.btn, {
309
+ cursor: 'pointer',
310
+ opacity: 淡 ? 0.7 : 1,
311
+ color: 已选名字(x.name) ? '#8fd0a0' : 'inherit',
312
+ }),
313
+ },
314
+ React.createElement('span', { onClick: () => 点名字(x.name) }, (已选名字(x.name) ? '✓ ' : '') + x.name + ' ' + x.n),
315
+ React.createElement(
316
+ 'span',
317
+ {
318
+ style: Object.assign({}, S.dim, { marginLeft: 5, cursor: 'pointer' }),
319
+ title: '不是名字(昵称、旧事里的称呼…)—— 标了就再也不出现,也不会被换掉',
320
+ onClick: (e) => {
321
+ if (e && e.stopPropagation) e.stopPropagation()
322
+ 标掉名字(x.name)
323
+ },
324
+ },
325
+ '✕',
326
+ ),
327
+ )
328
+
329
+ const load = React.useCallback(() => {
330
+ get('/list')
331
+ .then((x) => {
332
+ setD(x)
333
+ if (x && x.policy) setParams(Object.assign({}, x.policy))
334
+ setErr(null)
335
+ })
336
+ .catch((e) => setErr(String((e && e.message) || e)))
337
+ }, [])
338
+
339
+ React.useEffect(() => {
340
+ load()
341
+ }, [load])
342
+
343
+ // ---- 2026-09-21 新加:手动加一条(提交)----
344
+ // 加到哪个会话:她选的;没选就挑第一个开着的,都没有就第一个
345
+ // ⭐ 1.2.8(issue #8「手动加的记忆跑到别的会话去了」):
346
+ // 以前没选就**默默挑第一个开着的会话** —— 两个都开着时,她打开的是 B,记忆却加到了 A。
347
+ // 现在优先用「你正打开的这个会话」,再退回第一个开着的。
348
+ const 加哪个会话 = React.useCallback(() => {
349
+ if (加在谁) return 加在谁
350
+ if (open) return String(open)
351
+ const 列 = (d && d.sessions) || []
352
+ const 开着的 = 列.find((x) => x.enabled)
353
+ return (开着的 || 列[0] || {}).id || ''
354
+ }, [加在谁, open, d])
355
+
356
+ const 提交一条 = React.useCallback(() => {
357
+ const 内容 = 加内容.trim()
358
+ if (!内容) {
359
+ set加结果({ ok: false, 文字: '内容不能空' })
360
+ return
361
+ }
362
+ const 会话 = 加哪个会话()
363
+ if (!会话) {
364
+ set加结果({ ok: false, 文字: '没有会话可加' })
365
+ return
366
+ }
367
+ const 重要 = Number(加重要)
368
+ set加结果({ ok: true, 文字: '加着呢…' })
369
+ post('/add', {
370
+ session: 会话,
371
+ text: 内容,
372
+ at: 加时间.trim(),
373
+ importance: Number.isFinite(重要) ? 重要 : 0.5,
374
+ tags: 加词.trim() ? 加词.trim().split(/[\s,,、]+/).filter(Boolean) : [],
375
+ })
376
+ .then((r) => {
377
+ if (r && r.ok) {
378
+ set加结果({ ok: true, 文字: '加上了 ✓(这个会话现在 ' + r.count + ' 条)' })
379
+ set加内容('')
380
+ set加时间('')
381
+ set加词('')
382
+ load()
383
+ } else {
384
+ set加结果({ ok: false, 文字: (r && (r.error || r.message)) || '没加上' })
385
+ }
386
+ })
387
+ .catch((e) => set加结果({ ok: false, 文字: String((e && e.message) || e) }))
388
+ }, [加内容, 加时间, 加重要, 加词, 加哪个会话, load])
389
+
390
+ // 订阅外面那个「进度」—— 切走再回来也看得到还在跑 ✓
391
+ React.useEffect(() => {
392
+ const 同步 = () => {
393
+ setExpState(进度.扩词.状态)
394
+ setExpNoteState(进度.扩词.说明)
395
+ setRfState(进度.提炼.状态)
396
+ setRfNoteState(进度.提炼.说明)
397
+ }
398
+ 进度.订阅.add(同步)
399
+ 同步()
400
+ return () => {
401
+ 进度.订阅.delete(同步)
402
+ }
403
+ }, [])
404
+
405
+ // 切回这个页面就重读一遍 —— 不然得手动按「刷新」才看得到别处刚发生的变化 ✓
406
+ // ⚠ 但一定要**限流**:库里有 800 多条的时候,/list 要吐 1 MB 左右,
407
+ // 每次切窗口都重读 + 重画一遍,整个页面(连带别的插件面板)都会卡住 ✗
408
+ React.useEffect(() => {
409
+ if (typeof window === 'undefined' || !window.addEventListener) return
410
+ let 上次 = 0
411
+ const 回来就重读 = () => {
412
+ if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return
413
+ if ((进度.扩词.状态 && 进度.扩词.状态.running) || (进度.提炼.状态 && 进度.提炼.状态.running)) return // 正跑着提炼/扩词,别抢
414
+ const 现在 = Date.now()
415
+ if (现在 - 上次 < 10000) return // 10 秒内只读一次
416
+ 上次 = 现在
417
+ load()
418
+ }
419
+ window.addEventListener('focus', 回来就重读)
420
+ document.addEventListener('visibilitychange', 回来就重读)
421
+ return () => {
422
+ window.removeEventListener('focus', 回来就重读)
423
+ document.removeEventListener('visibilitychange', 回来就重读)
424
+ }
425
+ }, [load])
426
+
427
+ if (err) return React.createElement('div', { style: S.wrap }, React.createElement('div', { style: S.drop }, '出错了:' + err))
428
+ if (!d || !params) return React.createElement('div', { style: S.wrap }, '加载中…')
429
+
430
+ const now = Date.now()
431
+
432
+ // ---- 第二级:某个会话的详情 ----
433
+ if (open) {
434
+ const s = (d.sessions || []).find((x) => x.id === open)
435
+ if (!s) return React.createElement('div', { style: S.wrap }, '找不到这个会话')
436
+ const tr = (s.traces || []).slice(-1)[0]
437
+ const 删 = (id, action) => post('/delete', { session: open, id, action }).then(() => { setConfirm(null); load() })
438
+ // ⭐⭐ 2026-09-23(反馈「过期条目没标作废」):
439
+ // 撤销「已被取代」—— 标错了、或者状态又变回来了 ✓
440
+ // (不删任何东西,只把那面小旗子摘掉,摘完它又能被想起来)
441
+ const 撤销作废 = (id) => {
442
+ setBusy(true)
443
+ post('/delete', { session: open, action: 'unsupersede', id: id })
444
+ .then((r) => {
445
+ setBusy(false)
446
+ if (!r || !r.ok) {
447
+ setNotes(['撤销失败:' + ((r && r.message) || '?')])
448
+ return
449
+ }
450
+ setNotes(['✓ ' + r.message])
451
+ load()
452
+ })
453
+ .catch((e) => {
454
+ setBusy(false)
455
+ setNotes(['撤销失败:' + ((e && e.message) || e)])
456
+ })
457
+ }
458
+ // 提炼改坏了 —— 退回原文(单条 / 全部)✓
459
+ const 退原文 = (id) =>
460
+ post('/refine/undo', id === '*' ? { session: open, all: true } : { session: open, id: id })
461
+ .then((r) => {
462
+ setConfirm(null)
463
+ setExpNote('✓ 退回了 ' + ((r && r.退了) || 0) + ' 条原文')
464
+ load()
465
+ })
466
+ .catch((e) => setExpNote('退不回:' + String((e && e.message) || e)))
467
+ // 把这一份记忆复制到别的会话(新对话没有上下文,测起来最干净)✓
468
+ const 复制过去 = () =>
469
+ post('/copy-to', { session: open, to: 复制到 })
470
+ .then((r) => {
471
+ setConfirm(null)
472
+ const 尾巴 = []
473
+ if (r && r.复制了) 尾巴.push('新加 ' + r.复制了 + ' 条')
474
+ else 尾巴.push('一条也没加')
475
+ if (r && r.重复) 尾巴.push('重复跳过 ' + r.重复 + ' 条')
476
+ if (r && r.保留) 尾巴.push('那边原有的 ' + r.保留 + ' 条都留着')
477
+ setExpNote(r && r.ok ? '✓ ' + 尾巴.join(',') + ';那个会话的开关也开了' : '✗ ' + ((r && r.message) || '?'))
478
+ load()
479
+ })
480
+ .catch((e) => setExpNote('✗ ' + String((e && e.message) || e)))
481
+
482
+ // 二次确认条(点「删」之后才出现,手滑不了)
483
+ const 确认条 = (text, okLabel, action, id) =>
484
+ React.createElement(
485
+ 'div',
486
+ { style: S.confirm },
487
+ React.createElement('span', { style: { flex: 1, minWidth: 0, overflowWrap: 'anywhere' } }, text),
488
+ React.createElement('button', { style: S.btnDanger, onClick: () => 删(id, action) }, okLabel),
489
+ React.createElement('button', { style: S.btn, onClick: () => setConfirm(null) }, '算了'),
490
+ )
491
+
492
+ // 改一条:文本框 + 重要度 + 存 / 算了
493
+ const 改一条 = (m) =>
494
+ React.createElement(
495
+ 'div',
496
+ { style: S.editBox },
497
+ React.createElement('textarea', {
498
+ style: S.area,
499
+ rows: 3,
500
+ value: edit.text,
501
+ onChange: (e) => setEdit(Object.assign({}, edit, { text: e.target.value })),
502
+ }),
503
+ React.createElement(
504
+ 'div',
505
+ { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 6 } },
506
+ React.createElement('span', { style: S.dim }, '重要度'),
507
+ React.createElement(
508
+ 'select',
509
+ {
510
+ style: S.inp,
511
+ value: String(edit.importance),
512
+ onChange: (e) => setEdit(Object.assign({}, edit, { importance: Number(e.target.value) })),
513
+ },
514
+ React.createElement('option', { value: '0' }, '随口提过'),
515
+ React.createElement('option', { value: '0.5' }, '比较重要'),
516
+ React.createElement('option', { value: '1' }, '非常重要(会变成「一直」记得)'),
517
+ ),
518
+ React.createElement('span', { style: Object.assign({}, S.dim, { flex: 1, fontSize: 12 }) }, '改之前会自动备份 ✓'),
519
+ React.createElement(
520
+ 'button',
521
+ {
522
+ style: S.btn,
523
+ disabled: !String(edit.text || '').trim(),
524
+ onClick: () =>
525
+ post('/edit', { session: open, id: m.id, text: edit.text, importance: edit.importance }).then(() => {
526
+ setEdit(null)
527
+ load()
528
+ }),
529
+ },
530
+ '保存修改',
531
+ ),
532
+ React.createElement('button', { style: S.btn, onClick: () => setEdit(null) }, '算了'),
533
+ ),
534
+ )
535
+
536
+ const 一条记忆 = (m) =>
537
+ React.createElement(
538
+ 'div',
539
+ // ⚠ 分隔线画在**整条**上(不是里面那个「行」)——
540
+ // 这样下面那几行小字(能搜到它的词 / 讲的是)算在**这一条**里 ✓
541
+ { key: m.id, style: S.条 },
542
+ React.createElement(
543
+ 'div',
544
+ { style: S.row },
545
+ // ⭐ 2026-09-22(她要的批量删除):行首一个勾选框
546
+ React.createElement('input', {
547
+ type: 'checkbox',
548
+ checked: !!选中[m.id],
549
+ title: '勾上它,再点上面的「批量删」',
550
+ onChange: () => set选中(Object.assign({}, 选中, { [m.id]: !选中[m.id] })),
551
+ }),
552
+ React.createElement('div', { style: Object.assign({}, S.dim, { width: 96, flex: 'none' }) }, rel(m.at, now)),
553
+ React.createElement(
554
+ // ⚠⚠ 正文这一格给个**下限**(flexBasis 260 / minWidth 200)✗
555
+ // 光有 flex:1 是不够的:它允许被压到 0,右边牌子一多就成竖条。
556
+ // 有了下限 + 上面那条 flexWrap,装不下时是**牌子换行**,不是正文变窄 ✓
557
+ 'div',
558
+ { style: { flex: '1 1 260px', minWidth: 200, overflowWrap: 'anywhere', wordBreak: 'break-word' } },
559
+ m.已被取代
560
+ ? React.createElement(
561
+ 'span',
562
+ {
563
+ style: Object.assign({}, S.dim, { textDecoration: 'line-through', opacity: 0.62 }),
564
+ title: '这条已被取代(' + (m.已被取代.为什么 || '') + ')—— 不再被想起来,东西还留着',
565
+ },
566
+ m.text,
567
+ )
568
+ : m.text,
569
+ ),
570
+ // ⭐⭐ 2026-09-23(反馈「过期条目没标作废」):
571
+ // 标了「已被取代」的,行里直接挂一个「已被取代」的牌子 + 一个撤销口 ✓
572
+ m.已被取代
573
+ ? React.createElement(
574
+ 'div',
575
+ {
576
+ style: { flex: 'none', fontSize: 11, marginRight: 10, padding: '1px 6px', borderRadius: 8, background: 'rgba(224,176,112,.18)', color: '#e0b070', whiteSpace: 'nowrap' },
577
+ title: (m.已被取代.为什么 || '') + '(' + rel(m.已被取代.作废于, now) + ')—— 它不再被注入,但东西还在',
578
+ },
579
+ '已被取代',
580
+ )
581
+ : null,
582
+ m.已被取代
583
+ ? React.createElement(
584
+ 'div',
585
+ {
586
+ style: Object.assign({}, S.link, { flex: 'none', fontSize: 12, marginRight: 10 }),
587
+ title: '撤销「已被取代」—— 它又能被想起来了(标错了 / 状态又变回来了)',
588
+ onClick: () => 撤销作废(m.id),
589
+ },
590
+ '恢复',
591
+ )
592
+ : null,
593
+ // ⭐⭐ 2026-09-23:**事实键** —— ⚠ 不挂在行里,改挂到下面那行小字 ✓
594
+ // 行里每多一个固定宽的牌子,正文就被挤窄一分;
595
+ // 键本来就是「附注」性质,跟「能搜到它的词」放一起正合适(见下面那条)
596
+ React.createElement(
597
+ 'div',
598
+ { style: Object.assign({}, S.dim, { width: 58, flex: 'none', fontSize: 12 }), title: '鲜活度:越近就越鲜活,只影响排序。长期事实本来就不淡;没记时间的不知道是什么时候的事' },
599
+ // ⭐⭐ 2026-09-22(反馈:「没标时间的还是鲜活 100%」):
600
+ // 光显示百分比会骗人 —— 长期事实本来就不淡,写「100%」像在说「刚发生」✗
601
+ // → 长期事实写「**不淡**」✓
602
+ // ⭐ 2026-09-22 再改(发现流水账来问):**没记时间的也会淡了**
603
+ // (从「记下来的那天」开始算)→ 它们照常显示百分比 ✓
604
+ // (原来显示「没记时间」,那是我上一版判错了 —— 它们并不是不淡)
605
+ m.freshKind === 'fact' ? '不淡'
606
+ : typeof m.activation === 'number' && isFinite(m.activation) ? '鲜活 ' + Math.round(m.activation * 100) + '%' : '—',
607
+ ),
608
+ React.createElement('div', { style: Object.assign({}, S.dim, { width: 62, flex: 'none' }) }, impName(m.importance)),
609
+ m.textBefore
610
+ ? React.createElement(
611
+ 'div',
612
+ {
613
+ style: Object.assign({}, S.link, { flex: 'none', fontSize: 12, marginRight: 10 }),
614
+ title: '看看提炼之前长什么样',
615
+ onClick: () => set看原文(Object.assign({}, 看原文, { [m.id]: !看原文[m.id] })),
616
+ },
617
+ 看原文[m.id] ? '收起' : '原文',
618
+ )
619
+ : null,
620
+ m.textBefore
621
+ ? React.createElement(
622
+ 'div',
623
+ {
624
+ style: Object.assign({}, S.link, { flex: 'none', fontSize: 12, marginRight: 10, color: '#e0b070' }),
625
+ title: '把这条退回提炼前的原文(退回后可以再提炼一遍)',
626
+ onClick: () => setConfirm({ id: m.id, kind: 'refineundo' }),
627
+ },
628
+ '退回',
629
+ )
630
+ : null,
631
+ React.createElement(
632
+ 'div',
633
+ { style: Object.assign({}, S.link, { flex: 'none', fontSize: 12, marginRight: 10 }), onClick: () => setEdit({ id: m.id, text: m.text, importance: typeof m.importance === 'number' ? m.importance : 0.5 }) },
634
+ '改',
635
+ ),
636
+ React.createElement('div', { style: Object.assign({}, S.link, { flex: 'none', fontSize: 12 }), onClick: () => setConfirm({ id: m.id, kind: 'delete' }) }, '删'),
637
+ ),
638
+ edit && edit.id === m.id ? 改一条(m) : null,
639
+ confirm && confirm.id === m.id && confirm.kind === 'refineundo'
640
+ ? React.createElement(
641
+ 'div',
642
+ { style: S.confirm },
643
+ React.createElement('span', { style: { flex: 1 } }, '把这条退回提炼前的原文?退回后它会变回「还没提炼」。'),
644
+ React.createElement('button', { style: S.btn, onClick: () => 退原文(m.id) }, '退回'),
645
+ React.createElement('button', { style: S.btn, onClick: () => setConfirm(null) }, '算了'),
646
+ )
647
+ : null,
648
+ m.textBefore && 看原文[m.id]
649
+ ? React.createElement(
650
+ 'div',
651
+ {
652
+ style: Object.assign({}, S.dim, {
653
+ margin: '0 0 6px 104px',
654
+ fontSize: 12,
655
+ whiteSpace: 'pre-wrap',
656
+ // ⚠ pre-wrap 只在空格/换行处断,长串(网址、长英文)照样横向撑破面板 —— 得补这两条
657
+ overflowWrap: 'anywhere',
658
+ wordBreak: 'break-word',
659
+ borderLeft: '2px solid rgba(224,176,112,.5)',
660
+ paddingLeft: 8,
661
+ }),
662
+ },
663
+ '提炼前:' + m.textBefore,
664
+ )
665
+ : null,
666
+ m.related && m.related.length
667
+ ? React.createElement(
668
+ 'div',
669
+ { style: Object.assign({}, S.dim, { margin: '0 0 6px 104px', fontSize: 11, overflowWrap: 'anywhere', wordBreak: 'break-word' }) },
670
+ '能搜到它的词:' + m.related.join(' '),
671
+ )
672
+ : null,
673
+ // ⭐⭐ 2026-09-23:**「这条讲的是哪件事」** —— 挂在这儿(第二行小字),不占行里的宽度 ✓
674
+ // ⚠⚠ 别用「事实键」这种我自己造的词 ——
675
+ // 她说了「让人看不懂,写通俗一点的」✗
676
+ m.事实键
677
+ ? React.createElement(
678
+ 'div',
679
+ { style: { margin: '0 0 6px 104px', fontSize: 11, overflowWrap: 'anywhere', wordBreak: 'break-word' } },
680
+ React.createElement('span', { style: S.dim }, '讲的是:'),
681
+ React.createElement(
682
+ 'span',
683
+ {
684
+ style: { padding: '1px 6px', borderRadius: 8, background: 'rgba(120,170,220,.16)', color: '#7aaadc', whiteSpace: 'nowrap' },
685
+ title:
686
+ '这条讲的是「' + m.事实键 + '」这件事。' +
687
+ '讲同一件事的记忆只留最新那条 —— 旧的收起来了(还能恢复)',
688
+ },
689
+ m.事实键,
690
+ ),
691
+ )
692
+ : null,
693
+ confirm && confirm.id === m.id && confirm.kind === 'delete'
694
+ ? 确认条('把这条收起来?它会进「已删掉的」,还能找回来。', '收起来', 'delete', m.id)
695
+ : null,
696
+ )
697
+
698
+ const 已删 = s.deleted || []
699
+ // ⭐⭐ 2026-09-24(她说的「那已被取代应该回回收站呀」):
700
+ // 被取代的**从主列表挪走**,收进「已删掉的」那一栏 ✓
701
+ // 理由:它们已经不再被注入了,跟「收起来的」是一个性质,
702
+ // 摆在主列表里只是占地方(还会挂个牌子把正文挤窄)
703
+ //
704
+ // ⚠⚠ 但**不许挪进 `s.deleted`** —— 那是「她亲手删的」那一栏,
705
+ // 「批量清掉」「把已删掉的清空」会**真销毁**它们 ✗
706
+ // 被取代的是**自动标的**(几百条一起),
707
+ // 不该被那两个按钮连坐销毁 —— 那正是丢 157 条那次的教训 ✗✗
708
+ // → 所以:**显示上**收进回收站,**数据上**还留在 memories 里,
709
+ // 只是带一个「已被取代」的旗子 ✓
710
+ const 被取代的 = (s.memories || []).filter((m) => m.已被取代)
711
+ const 活着的 = (s.memories || []).filter((m) => !m.已被取代)
712
+
713
+ // ---- 记忆列表的检索(几百条的时候靠翻是翻不到的)----
714
+ // 正文、提炼前的原文、检索词、来源、id 都能搜 ✓
715
+ const 关键字 = 搜词.trim().toLowerCase()
716
+ const 搜过 = 关键字
717
+ ? 活着的.filter((m) => {
718
+ const 包 = (x) => String(x || '').toLowerCase().indexOf(关键字) >= 0
719
+ return (
720
+ 包(m.text) ||
721
+ 包(m.textBefore) ||
722
+ 包(m.source) ||
723
+ 包(m.id) ||
724
+ (Array.isArray(m.related) && 包(m.related.join(' ')))
725
+ )
726
+ })
727
+ : 活着的
728
+
729
+ // ⭐ 2026-09-24(反馈):**把「记了时间的」和「没记时间的」分开** ✓
730
+ // 上一版它们混在一个列表里 —— 188 条有日期的排在上面,下面 662 条没日期的
731
+ // 挤成「中间一坨」,看着就像排序坏了。原话:
732
+ // 「还有中间没有记日期的,全卡在中间一坨了……不然的话记和没记的就全混在一起了,
733
+ // 这个排序就没办法用了」
734
+ // 她要**自己切** → 三种视图轮着切 ✓
735
+ const 有时间 = (m) => !!m.at
736
+ // ⭐⭐ 2026-09-22(她要的):**「永远不淡」单独收成一栏** ✓
737
+ // 原话:「改了就会归纳到那个永远不会淡的那一行里去了,就是那一行,
738
+ // 我们是**单独做一个收理的文件夹一样**的,不然的话那些就不需要露在外面」
739
+ // ⭐ 又说:「**外面都是会淡的,里面才是不会淡的**,就不用管里面了」
740
+ // ⚠⚠ 2026-09-22 再改(发现两条流水账被误标):
741
+ // **「不会淡」的只有一类:长期事实** ✓
742
+ // 上一版我拿「鲜活度是不是钉住的」当判据 —— 那是**算分的副作用**,不是语义 ✗
743
+ // 结果把 394 条**没抠到日期的流水账**错收进「不会淡的」里 ✗✗
744
+ // (一眼就看出不对)
745
+ // 没记时间的现在**也会淡**(从「记下来的那天」开始算)→ 算「会淡的」✓
746
+ const 不会淡 = (m) => m.category === 'stable_profile'
747
+ const 过滤后 =
748
+ 看哪边 === '有时间'
749
+ ? 搜过.filter(有时间)
750
+ : 看哪边 === '没时间'
751
+ ? 搜过.filter((m) => !有时间(m))
752
+ : 看哪边 === '长期事实'
753
+ ? 搜过.filter(不会淡)
754
+ : 看哪边 === '会淡的'
755
+ ? 搜过.filter((m) => !不会淡(m))
756
+ : 搜过
757
+
758
+ // ⭐ 排好序再画 —— 按**最左边那一列的日期**排 ✗✗
759
+ // 上一版写的是 过滤后.slice().reverse(),那是把数组倒过来(谁先记谁在前翻个个儿),
760
+ // **根本没看日期**。所以补完日期顺序一点没变 —— 她一眼就看出来了 ✗
761
+ // 这一版:有日期的按日期排,**没日期的排最后**(没日期 = 不知道啥时候的事,
762
+ // 搁在中间会把有日期的顶散)✓
763
+ // ⚠ 「没时间」那个视图里全是没日期的 → 保留类里原来的相对次序(谁先记谁在前),
764
+ // 这样「旧的在前 / 新的在前」切一下还能看出入库先后来 ✓
765
+ const 排序 = (m) => {
766
+ const t = m.at ? new Date(m.at).getTime() : NaN
767
+ return isFinite(t) ? t : null
768
+ }
769
+ const 排好 = 过滤后.slice().sort((a, b) => {
770
+ const ta = 排序(a)
771
+ const tb = 排序(b)
772
+ if (ta === null && tb === null) return 0 // 都没日期:保持原来的相对次序
773
+ if (ta === null) return 1 // 没日期的沉到最后
774
+ if (tb === null) return -1
775
+ return 顺序 === '旧' ? ta - tb : tb - ta
776
+ })
777
+ // 「没时间」那个视图里没有日期可比 —— 想按入库先后翻的时候还得能用 ✓
778
+ const 画这个 = 看哪边 === '没时间' && 顺序 === '新' ? 排好.slice().reverse() : 排好
779
+ const 有时间数 = 活着的.filter(有时间).length
780
+ const 没时间数 = 活着的.length - 有时间数
781
+ // ⭐ 2026-09-22:**不会淡的**单独收一栏(「全部」视图里才拆)
782
+ // ⭐ 判据 = 鲜活度钉住的那批(长期事实 + 没时间的)——
783
+ // 原话:「外面都是会淡的,里面才是不会淡的」
784
+ // ⚠ 2026-09-24:口径换成**活着的** —— 被取代的已经收进回收站了,
785
+ // 不该还算在「会淡的/不会淡的」这些数里 ✗
786
+ const 事实总数 = 活着的.filter(不会淡).length
787
+ const 会淡总数 = 活着的.length - 事实总数
788
+
789
+ // ⭐ 2026-09-21 晚:补日期 —— 正文里写着日期、当时却没抠出来的
790
+ // (老版本 抠日期 必须要四位数年份,而档案里写的是「09-17」「8月26日」)
791
+ const 没日期的 = 活着的.filter((m) => !m.at).length
792
+ const 补日期 = () => {
793
+ setConfirm(null)
794
+ setBusy(true)
795
+ post('/backfill-dates', { session: s.id })
796
+ .then((r) => {
797
+ setBusy(false)
798
+ setNotes([
799
+ r && r.补了
800
+ ? '补了 ' + r.补了 + ' 条日期' + (r.猜的 ? '(其中 ' + r.猜的 + ' 条年份是按最近的算的)' : '') + ',还有 ' + r.还是没 + ' 条正文里真没写日期'
801
+ : '没有能补的 —— 正文里找不到日期',
802
+ ])
803
+ load()
804
+ })
805
+ .catch((e) => {
806
+ setBusy(false)
807
+ setNotes(['补日期失败:' + ((e && e.message) || e)])
808
+ })
809
+ }
810
+
811
+ // ⭐⭐ 2026-09-22(反馈):**补标「永远不淡」** ——
812
+ // 提炼原来只改重要度、不碰分类(0.7.6 才修好),
813
+ // 所以实测库里 135 条该「永远不淡」的被标成了会淡的(重要的日子就在里面)。
814
+ // 修好提炼只影响以后的,已经记下的那些得**单独补一遍** ✓
815
+ // 一条条问模型太慢 → 一批一批来,界面循环调(照「补日期」的样子)
816
+ // ⭐⭐ 2026-09-22 晚(反馈:「有好多那种就不该是长期事实的」):
817
+ // 现在判**所有没判过的**(不只「会淡的」)—— 该升的升、**该降的降** ✓
818
+ const 没判过的 = (s.memories || []).filter((m) => !m.stableChecked).length
819
+ // ⭐ 2026-09-22(她要的批量删除):勾了几条
820
+ const 已选几条 = Object.keys(选中).filter((k) => 选中[k]).length
821
+ // ⭐ 2026-09-22:判过几条(用来显示「重判一遍」)
822
+ const 判过的 = (s.memories || []).filter((m) => m.stableChecked).length
823
+ // ⭐ 2026-09-22(回收站的批量清掉):勾了几条
824
+ const 已选清几条 = Object.keys(选中清).filter((k) => 选中清[k]).length
825
+ const 补不淡 = (重判) => {
826
+ setConfirm(null)
827
+ setBusy(true)
828
+ let 累计判 = 0
829
+ let 累计升 = 0
830
+ let 累计降 = 0
831
+ let 连着失败 = 0
832
+ let 连着空 = 0
833
+ let 头一趟 = true
834
+ 补标进度.在跑 = true
835
+ 补标进度.重判 = !!重判
836
+ 补标进度.已 = 0
837
+ 补标进度.总 = (s.memories || []).length
838
+ 补标进度.等 = 0
839
+ const 跑 = () => {
840
+ // ⚠ 重判:只在**第一趟**带这个参数(服务端会把所有「判过」标记清掉再从头判)
841
+ const 体 = { session: s.id, 每批: 20 }
842
+ if (重判 && 头一趟) 体.重判 = true
843
+ 头一趟 = false
844
+ post('/backfill-stable', 体)
845
+ .then((r) => {
846
+ if (!r || !r.ok) {
847
+ // ⚠⚠ 2026-09-22(反馈:跑到一半「模型报错:429 status code (no body)」):
848
+ // 限流是**暂时的**,不该直接放弃 —— 等一等再试同一批 ✓
849
+ // 指数退避:5s → 10s → 20s → 40s → 60s,最多 6 次
850
+ const 是限流 = /429|rate.?limit|too many|限流|超限/i.test(String((r && r.message) || ''))
851
+ if (是限流 && 连着失败 < 6) {
852
+ 连着失败++
853
+ const 等 = Math.min(60000, 5000 * Math.pow(2, 连着失败 - 1))
854
+ 补标进度.等 = Math.round(等 / 1000)
855
+ setNotes([
856
+ '⚠ 模型那边限流了(429),等 ' + Math.round(等 / 1000) + ' 秒再试…(第 ' + 连着失败 + ' 次)',
857
+ '已经判好的都存下来了,不用重头来 ✓',
858
+ ])
859
+ setTimeout(跑, 等)
860
+ return
861
+ }
862
+ 补标进度.在跑 = false
863
+ setBusy(false)
864
+ setNotes([
865
+ '补标失败:' + ((r && r.message) || '?'),
866
+ '已经判好的都存下来了 —— 再点一次会接着跑,不会重头来 ✓',
867
+ ])
868
+ return
869
+ }
870
+ 连着失败 = 0
871
+ 补标进度.等 = 0
872
+ 累计判 += r.判了 || 0
873
+ 累计升 += r.改成不淡 || 0
874
+ 累计降 += r.降成会淡 || 0
875
+ 补标进度.已 = 累计判
876
+ if (r.共) 补标进度.总 = r.共
877
+ // ⚠⚠ 2026-09-22 修(反馈「他就卡着判着呢」):
878
+ // 原来这里判「完了没」用的是 `判了 > 0` ——
879
+ // **一批回 0 条(模型没回东西)就被当成「判完了」直接停** ✗
880
+ // 日志实锤:07:09:21「判了 0 条,还剩 665」之后就没动静了
881
+ // → 改成看**服务端的 done**;一批回 0 就**接着试下一批** ✓
882
+ // 连着 3 批都空才停(说明模型那边真出问题了),并说清楚
883
+ 连着空 = (r.判了 || 0) === 0 ? 连着空 + 1 : 0
884
+ setNotes([
885
+ '正在判… 已判 ' + 累计判 + ' 条:' + 累计升 + ' 条标成「永远不淡」、' +
886
+ 累计降 + ' 条改成会淡' + (r.done ? '' : ',还剩 ' + r.剩 + ' 条') +
887
+ (r.判了 === 0 && !r.done ? ' (这批模型没回东西,再试一批)' : ''),
888
+ ])
889
+ // ⚠ 每批之间**慢一点** —— 300ms 太急,容易撞限流(反馈就是 429 挂的)
890
+ if (!r.done) {
891
+ if (连着空 >= 3) {
892
+ 补标进度.在跑 = false
893
+ setBusy(false)
894
+ setNotes([
895
+ '⚠ 连着 3 批模型都没回东西,先停了(还剩 ' + r.剩 + ' 条)。',
896
+ '已经判好的都存下来了 —— 再点一次会接着跑,不会重头来 ✓',
897
+ ])
898
+ return
899
+ }
900
+ setTimeout(跑, 1500)
901
+ } else {
902
+ 补标进度.在跑 = false
903
+ setBusy(false)
904
+ setNotes([
905
+ '✓ 判完了:看了 ' + 累计判 + ' 条 —— ' + 累计升 + ' 条标成「永远不淡」,' +
906
+ 累计降 + ' 条从「永远不淡」改成会淡',
907
+ '(这一趟把所有还没判过的都过了一遍:该升的升、该降的降)',
908
+ ])
909
+ load()
910
+ }
911
+ })
912
+ .catch((e) => {
913
+ 补标进度.在跑 = false
914
+ setBusy(false)
915
+ setNotes(['补标失败:' + ((e && e.message) || e), '已经判好的都存下来了 —— 再点一次会接着跑 ✓'])
916
+ })
917
+ }
918
+ 跑()
919
+ }
920
+
921
+ // ⭐⭐ 2026-09-23(第三步):**清洗补键** —— 给老记忆补「事实键」✓
922
+ //
923
+ // 为什么单独一步:提炼只影响**以后**导入的;
924
+ // 老库里那些条目是导入时进来的,**本来就没有键** ——
925
+ // 不补的话「同一个 key 只留最新一条」对老数据一点用都没有 ✗
926
+ //
927
+ // ⚠⚠ **只在设置页点按钮跑,不在对话里跑**(她的死规矩):
928
+ // 几百条要调几百次模型,挂在对话那一轮上会把那轮卡死。
929
+ // ⚠ 先「干跑」看一眼,确认了再真洗 —— 这一步要改几百条,别一上来就动库 ✓
930
+ // ⚠ 算「还剩几条要洗」要跟服务端的 `要洗` **同一个口径** ——
931
+ // 洗过的(含判了留空的)不算,不然按钮上的数字永远不消 ✗
932
+ const 没键的 = (s.memories || []).filter((m) => !m.已被取代 && !m.refinedSkip && !m.事实键 && !m.洗键过).length
933
+ // 判了「留空」的那些(洗过、但没键)—— 单独报个数,免得她以为漏了
934
+ const 判留空的 = (s.memories || []).filter((m) => !m.事实键 && m.洗键过).length
935
+ // ⚠⚠ 撤回按钮上的数字要跟**它真能撤的**对得上 ✗
936
+ // 原来用的是「所有带键的」—— 把她自己手工填的也算进去了,
937
+ // 于是按钮写「撤掉这次标的(5)」、真撤只撤掉 2 条,数对不上(她会以为坏了)
938
+ const 可撤的 = (s.memories || []).filter((m) => m.键来源 === '清洗补键' || m.洗键过).length
939
+ const 洗键 = (干跑) => {
940
+ setConfirm(null)
941
+ setBusy(true)
942
+ let 累计洗 = 0
943
+ let 累计给 = 0
944
+ let 累计空 = 0
945
+ let 累计废 = 0
946
+ let 连着失败 = 0
947
+ let 连着空 = 0
948
+ 洗键进度.在跑 = true
949
+ 洗键进度.干跑 = !!干跑
950
+ 洗键进度.已 = 0
951
+ 洗键进度.总 = 没键的
952
+ const 跑 = () => {
953
+ // ⚠ 干跑:只跑**一批**就停 —— 让她看几个例子再决定,
954
+ // 不许拿几百次模型调用去"试" ✗
955
+ const 体 = { session: s.id, 每批: 8, 干跑: !!干跑 }
956
+ post('/wash-keys', 体)
957
+ .then((r) => {
958
+ if (!r || !r.ok) {
959
+ const 是限流 = /429|rate.?limit|too many|限流|超限/i.test(String((r && r.message) || ''))
960
+ if (是限流 && 连着失败 < 6) {
961
+ 连着失败++
962
+ const 等 = Math.min(60000, 5000 * Math.pow(2, 连着失败 - 1))
963
+ setNotes([
964
+ '⚠ 模型那边限流了(429),等 ' + Math.round(等 / 1000) + ' 秒再试…(第 ' + 连着失败 + ' 次)',
965
+ '已经补好的都存下来了,不用重头来 ✓',
966
+ ])
967
+ setTimeout(跑, 等)
968
+ return
969
+ }
970
+ 洗键进度.在跑 = false
971
+ setBusy(false)
972
+ setNotes([
973
+ '清洗失败:' + ((r && r.message) || '?'),
974
+ '已经补好的都存下来了 —— 再点一次会接着跑,不会重头来 ✓',
975
+ ])
976
+ return
977
+ }
978
+ 连着失败 = 0
979
+ 累计洗 += r.洗了 || 0
980
+ 累计给 += r.给了键 || 0
981
+ 累计空 += r.留空 || 0
982
+ 累计废 += r.作废了 || 0
983
+ 洗键进度.已 = 累计洗
984
+ if (r.总数) 洗键进度.总 = 没键的
985
+ // ⚠⚠ 「模型整批没回出东西」才算失败,**不许拿「给了键 === 0」当信号** ✗
986
+ // 模型正常地把一批全判成留空时,给了键也是 0 ——
987
+ // 原来按那个数,连着 3 批就误报「模型没回出键」停下来(实测踩到过)
988
+ 连着空 = (r.没回出东西 || 0) >= (r.洗了 || 0) ? 连着空 + 1 : 0
989
+ if (干跑) {
990
+ // 干跑:把几个例子摆出来,让她看一眼标得对不对 ✓
991
+ 洗键进度.在跑 = false
992
+ setBusy(false)
993
+ const 例 = (r.示例 || []).map((x) => ' 「' + x.键 + '」 ← ' + x.正文)
994
+ setNotes(
995
+ [
996
+ '先试了 ' + (r.洗了 || 0) + ' 条(一个字都没改):标了 ' + (r.给了键 || 0) + ' 条、说不用标 ' + (r.留空 || 0) + ' 条',
997
+ '它标出来是这样的(左边是「这条讲的是哪件事」,右边是记忆本身):',
998
+ ]
999
+ .concat(例.length ? 例 : [' (这批它都说不用标)'])
1000
+ .concat(['', '看着行就点「全部开始」;不行就先别动 —— 这一步不做也不影响别的 ✓']),
1001
+ )
1002
+ return
1003
+ }
1004
+ setNotes([
1005
+ '正在认… 已过 ' + 累计洗 + ' 条:标了 ' + 累计给 + ' 条、说不用标 ' + 累计空 + ' 条' +
1006
+ (累计废 ? '、顺手收起来 ' + 累计废 + ' 条' : '') +
1007
+ (r.done ? '' : ',还剩 ' + r.剩 + ' 条'),
1008
+ ])
1009
+ if (!r.done) {
1010
+ if (连着空 >= 3) {
1011
+ 洗键进度.在跑 = false
1012
+ setBusy(false)
1013
+ setNotes([
1014
+ '⚠ 连着 3 批模型一个字都没回,先停了(还剩 ' + r.剩 + ' 条)。',
1015
+ '已经标好的都存下来了 —— 再点一次会接着跑 ✓',
1016
+ '(要是它只是说「这批都不用标」,那不算出错、会自己往下走)',
1017
+ ])
1018
+ return
1019
+ }
1020
+ setTimeout(跑, 1500)
1021
+ } else {
1022
+ 洗键进度.在跑 = false
1023
+ setBusy(false)
1024
+ setNotes([
1025
+ '✓ 跑完了:过了 ' + 累计洗 + ' 条 —— 标了 ' + 累计给 + ' 条、说不用标 ' + 累计空 + ' 条' +
1026
+ (累计废 ? ',其中 ' + 累计废 + ' 条因为「讲的是同一件事」被收起来了(还留着、能恢复)' : ''),
1027
+ '说不用标的那批一个字都没动 ✓(要退回来就点「撤掉这次标的」)',
1028
+ ])
1029
+ load()
1030
+ }
1031
+ })
1032
+ .catch((e) => {
1033
+ 洗键进度.在跑 = false
1034
+ setBusy(false)
1035
+ setNotes(['跑失败了:' + ((e && e.message) || e), '已经标好的都存下来了 —— 再点一次会接着跑 ✓'])
1036
+ })
1037
+ }
1038
+ 跑()
1039
+ }
1040
+
1041
+ // 整份撤回(把这次自动标的全撤掉、顺手收起来的全放回来)✓
1042
+ const 撤销洗键 = () => {
1043
+ setConfirm(null)
1044
+ setBusy(true)
1045
+ post('/wash-keys/undo', { session: s.id })
1046
+ .then((r) => {
1047
+ setBusy(false)
1048
+ if (!r || !r.ok) {
1049
+ setNotes(['撤回失败:' + ((r && r.message) || '?')])
1050
+ return
1051
+ }
1052
+ setNotes([
1053
+ '✓ 撤完了:撤掉 ' + (r.摘了 || 0) + ' 条自动标的、放回来 ' + (r.恢复 || 0) + ' 条被收起来的',
1054
+ '(你自己填的那些没动 —— 这个按钮只管自动标的)',
1055
+ ])
1056
+ load()
1057
+ })
1058
+ .catch((e) => {
1059
+ setBusy(false)
1060
+ setNotes(['撤回失败:' + ((e && e.message) || e)])
1061
+ })
1062
+ }
1063
+
1064
+ // ⭐ 2026-09-22(反馈「回收站还没有批量删除,就批量清空」):
1065
+ // 「已删掉的」那一栏补一个**批量清掉** ✓
1066
+ // ⚠ 这个是**真销毁**(不是收起来)→ 界面必须问两遍 ✓
1067
+ // ⭐⭐ 2026-09-23(反馈:「那这里没有批量找回呀」):
1068
+ // 「已删掉的」原来只有**批量清掉**,想全捞回来只能一条条点「找回来」✗
1069
+ // → 补一个**批量找回**(勾了哪几条就捞哪几条)
1070
+ // ⚠ `只合并` = 只捞「合并掉的」那批 —— 等于**一键撤销整次合并** ✓
1071
+ // ⚠ 这个**不是销毁**,是往回捞,所以不用问两遍;但界面上还是先说一句捞几条
1072
+ const 批回 = (会话, 只合并) => {
1073
+ const ids = 只合并 ? [] : Object.keys(选中清).filter((k) => 选中清[k])
1074
+ if (!只合并 && !ids.length) {
1075
+ set批回问(false)
1076
+ return
1077
+ }
1078
+ setBusy(true)
1079
+ post('/delete', Object.assign({ session: 会话.id, action: 'restorebatch', ids: ids }, 只合并 ? { 只找回合并掉的: true } : {}))
1080
+ .then((r) => {
1081
+ setBusy(false)
1082
+ set批回问(false)
1083
+ if (!r || !r.ok) {
1084
+ setNotes(['批量找回失败:' + ((r && r.message) || '?')])
1085
+ return
1086
+ }
1087
+ set选中清({})
1088
+ setNotes(['✓ ' + r.message + '(「已删」里还剩 ' + r.剩 + ' 条)'])
1089
+ load()
1090
+ })
1091
+ .catch((e) => {
1092
+ setBusy(false)
1093
+ setNotes(['批量找回失败:' + ((e && e.message) || e)])
1094
+ })
1095
+ }
1096
+ const 批清 = (会话) => {
1097
+ const ids = Object.keys(选中清).filter((k) => 选中清[k])
1098
+ if (!ids.length) {
1099
+ set批清问(false)
1100
+ return
1101
+ }
1102
+ setBusy(true)
1103
+ post('/delete', { session: 会话.id, action: 'purgebatch', ids: ids })
1104
+ .then((r) => {
1105
+ setBusy(false)
1106
+ set批清问(false)
1107
+ if (!r || !r.ok) {
1108
+ setNotes(['批量清掉失败:' + ((r && r.message) || '?')])
1109
+ return
1110
+ }
1111
+ set选中清({})
1112
+ setNotes(['✓ ' + r.message + '(「已删」里还剩 ' + r.剩 + ' 条)'])
1113
+ load()
1114
+ })
1115
+ .catch((e) => {
1116
+ setBusy(false)
1117
+ setNotes(['批量清掉失败:' + ((e && e.message) || e)])
1118
+ })
1119
+ }
1120
+
1121
+ // ⭐⭐ 2026-09-22(反馈:「查重我们是不是还没做啊?我怎么没有找到按钮」):
1122
+ // 去重一直有,但都是**自动**的 —— 召回时(注入前只留一条)、导入时。
1123
+ // **库里已经存着的重复没人管** ✗
1124
+ // ⚠ 不能一键全合:「汇总+明细」那种一长一短,合了会丢信息
1125
+ // → 真重复给「一键合」,汇总+明细只列出来让她自己挑 ✓
1126
+ const 去查重 = (会话) => {
1127
+ setBusy(true)
1128
+ // ⚠⚠ 面板在**记忆列表那一块里**(跟「补标不淡」的确认框一样),
1129
+ // 列表默认是折叠的 —— 不展开就看不见面板 ✗
1130
+ // → 点查重时**顺手把列表展开** ✓(按钮本身在标题行,永远看得见)
1131
+ set看记忆(true)
1132
+ post('/duplicates', { session: 会话.id })
1133
+ .then((r) => {
1134
+ setBusy(false)
1135
+ if (!r || !r.ok) {
1136
+ setNotes(['查重失败:' + ((r && r.message) || '?')])
1137
+ return
1138
+ }
1139
+ const 选 = {}
1140
+ const 勾 = {}
1141
+ ;(r.组 || []).forEach((g, i) => {
1142
+ // 默认留**最长**的那条 —— 「汇总+明细」时长的信息更全,别默认留短的 ✗
1143
+ 选[i] = g.条们.reduce((a, b) => (b.长 > a.长 ? b : a), g.条们[0]).id
1144
+ // ⭐ 「真重复」默认勾上(合了安全);「汇总+明细」默认**不勾**(合了会丢信息)✓
1145
+ 勾[i] = g.类型 === '真重复'
1146
+ })
1147
+ set查重(r)
1148
+ set查重选(选)
1149
+ set查重合(勾)
1150
+ set查重开(true)
1151
+ if (!(r.组 || []).length) setNotes(['✓ 查过了:没有「几乎一样」的记忆'])
1152
+ })
1153
+ .catch((e) => {
1154
+ setBusy(false)
1155
+ setNotes(['查重失败:' + ((e && e.message) || e)])
1156
+ })
1157
+ }
1158
+ // 合掉:每组留一条,其余交给**服务端再验一遍**(不像的会被保住)✓
1159
+ // ⚠⚠ 2026-09-22 修(反馈:「把一些别的对的事实给删了」):
1160
+ // 原来客户端自己算好「删哪些」就发过去,服务端不验 ✗
1161
+ // 现在发的是「组」(留哪条、删哪些),**服务端自己再算一遍相似度** ✓
1162
+ const 合重复 = (会话, 只合真重复) => {
1163
+ if (!查重) return
1164
+ const 组们 = []
1165
+ ;(查重.组 || []).forEach((g, i) => {
1166
+ if (只合真重复 && g.类型 !== '真重复') return
1167
+ // ⭐⭐ 2026-09-23:「按我选的合」**只合勾上的组** ——
1168
+ // 原来它把每一组都合掉(默认全选中),一点就丢掉 123 条 ✗
1169
+ if (!只合真重复 && !查重合[i]) return
1170
+ const 留 = 查重选[i] || g.条们[0].id
1171
+ const 删 = g.条们.filter((t) => t.id !== 留).map((t) => t.id)
1172
+ if (删.length) 组们.push({ 留: 留, 删: 删 })
1173
+ })
1174
+ if (!组们.length) {
1175
+ setNotes([
1176
+ 只合真重复 ? '这一批里没有「真重复」组' : '一组都没勾(「汇总+明细」默认不勾,要合得先勾上)',
1177
+ ])
1178
+ return
1179
+ }
1180
+ setBusy(true)
1181
+ post('/duplicates/merge', { session: 会话.id, 组: 组们 })
1182
+ .then((r) => {
1183
+ setBusy(false)
1184
+ if (!r || !r.ok) {
1185
+ setNotes(['合并:' + ((r && r.message) || '?')])
1186
+ return
1187
+ }
1188
+ const 行 = ['✓ ' + r.message + '(剩下的:' + r.剩 + ' 条)', '合掉的都收在「已删」里,点开能找回来']
1189
+ if (r.保住) {
1190
+ 行.push('⚠ 有 ' + r.保住 + ' 条不够像、没敢合(保住了):')
1191
+ ;(r.保住明细 || []).forEach((x) => 行.push('  ' + x.像度 + ' ' + x.text))
1192
+ }
1193
+ setNotes(行)
1194
+ set查重(null)
1195
+ set查重开(false)
1196
+ load()
1197
+ })
1198
+ .catch((e) => {
1199
+ setBusy(false)
1200
+ setNotes(['合并失败:' + ((e && e.message) || e)])
1201
+ })
1202
+ }
1203
+
1204
+ // ⭐⭐ 2026-09-22(她要的):**批量删** —— 收进「已删」,不是销毁 ✓
1205
+ // 「还是需要 2 次确认的」→ 点按钮只出确认条,确认条上再点才删 ✓
1206
+ const 批删 = (会话) => {
1207
+ const ids = Object.keys(选中).filter((k) => 选中[k])
1208
+ if (!ids.length) {
1209
+ set批删问(false)
1210
+ return
1211
+ }
1212
+ setBusy(true)
1213
+ post('/delete', { session: 会话.id, action: 'batchdelete', ids: ids })
1214
+ .then((r) => {
1215
+ setBusy(false)
1216
+ set批删问(false)
1217
+ if (!r || !r.ok) {
1218
+ setNotes(['批量删失败:' + ((r && r.message) || '?')])
1219
+ return
1220
+ }
1221
+ set选中({})
1222
+ setNotes(['✓ ' + r.message + '(剩下的:' + r.剩 + ' 条)', '删掉的都收在「已删」里,点开能找回来'])
1223
+ load()
1224
+ })
1225
+ .catch((e) => {
1226
+ setBusy(false)
1227
+ setNotes(['批量删失败:' + ((e && e.message) || e)])
1228
+ })
1229
+ }
1230
+
1231
+ // ═══════════════════════════════════════════════════════════
1232
+ // ⭐⭐ 2026-09-22(她的第十件):**从会话生成档案** —— 给别人用的功能
1233
+ //
1234
+ // ① 看这个工作区有哪些会话、勾几个
1235
+ // ② 算要切几批、多少字(**不花钱**)
1236
+ // ③ 一批一批跑(⚠ 这一步调模型、花钱)—— 跑完一批就把条目攒起来,
1237
+ // 下一批带上「已经记过的」,同一件事不会被收两遍
1238
+ // ④ 存成档案文件 → 再走已有的「从旧档案导入」变成记忆条目
1239
+ // ═══════════════════════════════════════════════════════════════
1240
+ const 开生成 = () => {
1241
+ set生成开(true)
1242
+ set生成会话(null)
1243
+ set生成计划(null)
1244
+ set生成条目([])
1245
+ set生成档(null)
1246
+ set生成进度(null)
1247
+ post('/gen-sessions', { session: s.id })
1248
+ .then((r) => {
1249
+ if (!r || !r.ok) {
1250
+ setNotes(['读会话列表失败:' + ((r && r.message) || '?')])
1251
+ return
1252
+ }
1253
+ set生成会话(r.会话 || [])
1254
+ set生成诊断(r.诊断 || null)
1255
+ // 建议路径(服务端按这个会话的工作目录算的)—— 用户能改 ✓
1256
+ if (r.建议存到) set生成存到(String(r.建议存到))
1257
+ // ⚠⚠ 2026-09-22(反馈:「默认不该全选」):
1258
+ // 原来我默认**全勾上** —— 那等于替用户决定「全都处理一遍」,
1259
+ // 而这一步后面是要**调模型、花钱**的 ✗
1260
+ // → 默认**一个都不勾**,让用户自己挑 ✓
1261
+ set生成选({})
1262
+ })
1263
+ .catch((e) => setNotes(['读会话列表失败:' + ((e && e.message) || e)]))
1264
+ }
1265
+
1266
+ const 勾的sid = () => Object.keys(生成选).filter((k) => 生成选[k])
1267
+
1268
+ const 算生成 = () => {
1269
+ const sids = 勾的sid()
1270
+ if (!sids.length) {
1271
+ setNotes(['一个会话都没勾'])
1272
+ return
1273
+ }
1274
+ setBusy(true)
1275
+ setNotes(['算着呢…(要读一遍会话、去重、切段,一个会话约一秒)'])
1276
+ post('/gen-plan', { session: s.id, sids: sids })
1277
+ .then((r) => {
1278
+ setBusy(false)
1279
+ if (!r || !r.ok) {
1280
+ setNotes(['算失败:' + ((r && r.message) || '?')])
1281
+ return
1282
+ }
1283
+ set生成计划(r)
1284
+ set生成条目([])
1285
+ set生成档(null)
1286
+ setNotes([])
1287
+ })
1288
+ .catch((e) => {
1289
+ setBusy(false)
1290
+ setNotes(['算失败:' + ((e && e.message) || e)])
1291
+ })
1292
+ }
1293
+
1294
+ const 跑生成 = () => {
1295
+ const sids = 勾的sid()
1296
+ if (!sids.length || !生成计划) return
1297
+ const 总批 = 生成计划.批数
1298
+ setBusy(true)
1299
+ set生成档(null)
1300
+ const 攒 = 生成条目.slice()
1301
+ let 连着空 = 0
1302
+ const 跑一批 = (批号) => {
1303
+ if (批号 > 总批) {
1304
+ setBusy(false)
1305
+ set生成进度(null)
1306
+ setNotes(['✓ 跑完了:' + 总批 + ' 批,攒了 ' + 攒.length + ' 条。' + (攒.length ? '点「存成档案文件」' : '模型这一轮一条都没收;回文长度记在诊断日志里,换个会话或再点一次试试')])
1307
+ return
1308
+ }
1309
+ set生成进度({ 批号, 批数: 总批, 在跑: true, 模型: (生成进度 && 生成进度.模型) || '' })
1310
+ post('/gen-run', {
1311
+ session: s.id,
1312
+ sids: sids,
1313
+ 批号: 批号,
1314
+ // ⚠ 把前面攒的条目当「已记清单」传过去 —— 这是防重复的机制
1315
+ 已记: 攒.map((x) => x.章 + '|' + x.日期 + '|' + x.话),
1316
+ provider: 生成备 ? 生成备.provider : undefined,
1317
+ model: 生成备 ? 生成备.model : undefined,
1318
+ })
1319
+ .then((r) => {
1320
+ if (!r || !r.ok) {
1321
+ setBusy(false)
1322
+ set生成进度(null)
1323
+ setNotes(['第 ' + 批号 + ' 批失败:' + ((r && r.message) || '?'), '已经跑好的 ' + 攒.length + ' 条留着,再点一次会接着跑'])
1324
+ return
1325
+ }
1326
+ if (r.条目 && r.条目.length) {
1327
+ r.条目.forEach((x) => 攒.push(x))
1328
+ 连着空 = 0
1329
+ } else {
1330
+ 连着空++
1331
+ }
1332
+ set生成条目(攒.slice())
1333
+ set生成进度({ 批号, 批数: 总批, 在跑: true, 模型: r.模型 || '' })
1334
+ if (r.done) {
1335
+ setBusy(false)
1336
+ set生成进度(null)
1337
+ setNotes(['✓ 跑完了:' + 总批 + ' 批,攒了 ' + 攒.length + ' 条。' + (攒.length ? '点「存成档案文件」' : '模型这一轮一条都没收;回文长度记在诊断日志里,换个会话或再点一次试试')])
1338
+ return
1339
+ }
1340
+ // 一批回 0 条不算失败,但也别无限跑 —— 连着 3 批空就停
1341
+ if (连着空 >= 3) {
1342
+ setBusy(false)
1343
+ set生成进度(null)
1344
+ setNotes(['连着 3 批模型没回东西,先停了。再点一次会接着跑'])
1345
+ return
1346
+ }
1347
+ setTimeout(() => 跑一批(批号 + 1), 300)
1348
+ })
1349
+ .catch((e) => {
1350
+ setBusy(false)
1351
+ set生成进度(null)
1352
+ setNotes(['第 ' + 批号 + ' 批失败:' + ((e && e.message) || e), '再点一次会接着跑'])
1353
+ })
1354
+ }
1355
+ 跑一批(1)
1356
+ }
1357
+
1358
+ const 存生成 = () => {
1359
+ if (!生成条目.length) {
1360
+ setNotes(['还没有条目'])
1361
+ return
1362
+ }
1363
+ setBusy(true)
1364
+ post('/gen-save', { session: s.id, 条目: 生成条目, 来源: (生成计划 && 生成计划.天数 + ' 天') || '', 存到: 生成存到 })
1365
+ .then((r) => {
1366
+ setBusy(false)
1367
+ if (!r || !r.ok) {
1368
+ setNotes(['存失败:' + ((r && r.message) || '?')])
1369
+ return
1370
+ }
1371
+ set生成档(r)
1372
+ setNotes(['✓ 档案存到:' + r.存到, '共 ' + r.条数 + ' 条' + (r.去掉重复 ? '(去掉了 ' + r.去掉重复 + ' 条重复)' : '')])
1373
+ })
1374
+ .catch((e) => {
1375
+ setBusy(false)
1376
+ setNotes(['存失败:' + ((e && e.message) || e)])
1377
+ })
1378
+ }
1379
+
1380
+ // ---- 用 DSH 自己的模型给记忆扩词 ----
1381
+ // 「直接用我们 deepseek 的可不可以」→ 可以 ✓ 插件能调宿主模型,不用外部 key
1382
+ const 没扩的 = (s.memories || []).filter((m) => !m.related || !m.related.length).length
1383
+ // 要提炼的:档案导入进来的、还没提炼过的 ✓
1384
+ const 没提炼的 = (s.memories || []).filter((m) => String(m.source || '').indexOf('档案导入') === 0 && !m.refined).length
1385
+ const 扩词区 = () => {
1386
+ const 小件 = []
1387
+ 小件.push(React.createElement('div', { key: 'h', style: S.h }, '提炼 / 扩词(用 DSH 自己的模型)'))
1388
+ 小件.push(
1389
+ React.createElement(
1390
+ 'div',
1391
+ { key: 'd', style: S.dim },
1392
+ React.createElement('div', null, '两件事不一样,分开的:'),
1393
+ React.createElement(
1394
+ 'div',
1395
+ { style: { margin: '4px 0 0 8px' } },
1396
+ React.createElement('b', null, '① 扩词'),
1397
+ ':给每条记忆加一批「以后可能搜到它的词」,比如「她的衣服是蓝色的」加上「颜色 蓝色 衣着 穿搭」。' +
1398
+ '这样问「她喜欢什么颜色」也能捞到它。不改记忆本身,只是加索引。',
1399
+ ),
1400
+ React.createElement(
1401
+ 'div',
1402
+ { style: { margin: '4px 0 0 8px' } },
1403
+ React.createElement('b', null, '② 提炼'),
1404
+ ':把导入进来的档案原文改写成「一条第一人称的记忆」。' +
1405
+ '比如「- 极致温柔:他会第一时间接住她的情绪」→「我会第一时间接住她的情绪」。' +
1406
+ '会改记忆本身(原文留在底里,能对照)。',
1407
+ ),
1408
+ React.createElement(
1409
+ 'div',
1410
+ { style: { margin: '4px 0 0 8px' } },
1411
+ '两个都在后台跑一次就行;跑完之后每轮对话不再调用模型,所以不卡、不额外花钱。',
1412
+ ),
1413
+ ),
1414
+ )
1415
+
1416
+ // ---- 选模型 ----
1417
+ const 当前 = mdl && mdl.选中的 ? mdl.选中的 : (mdl && mdl.默认) || null
1418
+ const 当前串 = 当前 ? 当前.provider + ' / ' + 当前.model : '(还没读到)'
1419
+ 小件.push(
1420
+ React.createElement(
1421
+ 'div',
1422
+ { key: 'pick', style: S.nameBox },
1423
+ React.createElement('div', { style: { flex: 'none', width: 100 } }, '用哪个模型'),
1424
+ mdl
1425
+ ? React.createElement(
1426
+ 'select',
1427
+ {
1428
+ style: Object.assign({}, S.inp, { width: 300 }),
1429
+ value: 当前 ? 当前.provider + '|' + 当前.model : '',
1430
+ onChange: (e) => {
1431
+ const v = String(e.target.value || '')
1432
+ const i = v.indexOf('|')
1433
+ const 选 = i > 0 ? { provider: v.slice(0, i), model: v.slice(i + 1) } : null
1434
+ post('/names', { llmModel: 选 }).then(() => {
1435
+ setMdl(Object.assign({}, mdl, { 选中的: 选 }))
1436
+ setExpNote(选 ? '以后就用 ' + 选.provider + ' / ' + 选.model + ' ✓' : '改回用会话自己的模型 ✓')
1437
+ })
1438
+ },
1439
+ },
1440
+ React.createElement('option', { value: '' }, '(用会话自己的:' + (mdl.默认 ? mdl.默认.provider + '/' + mdl.默认.model : '?') + ')'),
1441
+ (mdl.providers || []).map((p) =>
1442
+ React.createElement(
1443
+ 'optgroup',
1444
+ { key: p.id, label: p.name + '(' + (p.models || []).length + ' 个)' },
1445
+ (p.models || []).map((m) =>
1446
+ React.createElement('option', { key: p.id + '|' + m.id, value: p.id + '|' + m.id }, m.name + ' (' + p.id + ')'),
1447
+ ),
1448
+ ),
1449
+ ),
1450
+ )
1451
+ : React.createElement('span', { style: S.dim }, '点右边「读一下有哪些模型」'),
1452
+ React.createElement(
1453
+ 'button',
1454
+ {
1455
+ style: S.btn,
1456
+ onClick: () => {
1457
+ setExpNote('读着呢…')
1458
+ post('/models', { session: open })
1459
+ .then((r) => {
1460
+ setMdl(r)
1461
+ setExpNote(
1462
+ r && r.ok
1463
+ ? 'DSH 里有 ' + (r.providers || []).length + ' 个 provider,共 ' +
1464
+ (r.providers || []).reduce((a, p) => a + (p.models || []).length, 0) + ' 个模型。现在用:' + 当前串
1465
+ : '✗ ' + ((r && r.message) || '?'),
1466
+ )
1467
+ })
1468
+ .catch((e) => setExpNote('✗ ' + String((e && e.message) || e)))
1469
+ },
1470
+ },
1471
+ '读一下有哪些模型',
1472
+ ),
1473
+ ),
1474
+ )
1475
+
1476
+ // ⚠⚠⚠ 2026-09-22(她的反馈,原话):
1477
+ // 「他那个就是重判的进度条跟那个提炼的是在一起呀。我发现我得往下翻
1478
+ // 才能看到进度条,那就不对呀。重判应该有个单独的进度条吧?如果你
1479
+ // 点了的话,它就会弹出来;如果没点,它就没有。」
1480
+ // 她说得对 —— 补标/重判的进度条原来**跟「模型与扩词」「提炼」挤在
1481
+ // 同一张卡里**,点了重判得往上翻才看得见,没点的人还会以为那条是
1482
+ // 自己点出来的 ✗
1483
+ // → 搬走:谁点的,进度条就跟在谁的按钮旁边(下面那个按钮行里)✓
1484
+ // ---- 进度条 ----
1485
+ if (exp && exp.总) {
1486
+ const 已 = Math.max(0, exp.总 - exp.剩)
1487
+ const 百 = exp.总 ? Math.round((已 / exp.总) * 100) : 0
1488
+ 小件.push(
1489
+ React.createElement(
1490
+ 'div',
1491
+ { key: 'bar', style: { margin: '8px 0' } },
1492
+ React.createElement(
1493
+ 'div',
1494
+ { style: S.barOut },
1495
+ React.createElement('div', { style: Object.assign({}, S.barIn, { width: 百 + '%' }) }),
1496
+ ),
1497
+ React.createElement(
1498
+ 'div',
1499
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 4 }) },
1500
+ (exp.模式 || '扩词') + ':' + 已 + ' / ' + exp.总 + ' 条(' + 百 + '%)' + (exp.剩 ? ' 还剩 ' + exp.剩 + ' 条' : ' ✓ 完事了'),
1501
+ ),
1502
+ ),
1503
+ )
1504
+ }
1505
+ // 提炼的进度条(跟扩词各一条,不互相盖)✓
1506
+ if (rf && rf.总) {
1507
+ const 已 = Math.max(0, rf.总 - rf.剩)
1508
+ const 百 = rf.总 ? Math.round((已 / rf.总) * 100) : 0
1509
+ 小件.push(
1510
+ React.createElement(
1511
+ 'div',
1512
+ { key: 'rfbar', style: { margin: '8px 0' } },
1513
+ React.createElement(
1514
+ 'div',
1515
+ { style: S.barOut },
1516
+ React.createElement('div', { style: Object.assign({}, S.barIn, { width: 百 + '%' }) }),
1517
+ ),
1518
+ React.createElement(
1519
+ 'div',
1520
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 4 }) },
1521
+ '提炼:' + 已 + ' / ' + rf.总 + ' 条(' + 百 + '%)' + (rf.剩 ? ' 还剩 ' + rf.剩 + ' 条' : ' ✓ 完事了'),
1522
+ ),
1523
+ ),
1524
+ )
1525
+ }
1526
+
1527
+ 小件.push(
1528
+ React.createElement(
1529
+ 'div',
1530
+ { key: 'b', style: { display: 'flex', gap: 8, alignItems: 'center', margin: '8px 0', flexWrap: 'wrap' } },
1531
+ React.createElement(
1532
+ 'button',
1533
+ {
1534
+ style: S.btn,
1535
+ disabled: !!(exp && exp.running),
1536
+ onClick: () => {
1537
+ setExpNote('测着呢…')
1538
+ post('/llm-check', { session: open })
1539
+ .then((r) => {
1540
+ setExpNote(
1541
+ r && r.ok
1542
+ ? '✓ 模型通了(' + r.provider + ' / ' + r.model + ',' + r.ms + 'ms)→ 它回了「' + String(r.text || '').slice(0, 20) + '」'
1543
+ : '✗ 不通:' + ((r && r.message) || '?'),
1544
+ )
1545
+ })
1546
+ .catch((e) => setExpNote('✗ ' + String((e && e.message) || e)))
1547
+ },
1548
+ },
1549
+ '先测一下模型',
1550
+ ),
1551
+ React.createElement(
1552
+ 'button',
1553
+ {
1554
+ style: S.btnDanger,
1555
+ disabled: !!(exp && exp.running) || 没扩的 === 0,
1556
+ onClick: () => {
1557
+ let 累计 = 0
1558
+ let 空批 = 0 // 连着几批一条词都没扩出来
1559
+ let 每批 = 6 // 空批就把批量减半 —— 输入越少、思考越少,越不容易被截断 ✓
1560
+ const 跑 = () => {
1561
+ post('/expand', { session: open, 每批: 每批 })
1562
+ .then((r) => {
1563
+ if (!r || !r.ok) {
1564
+ setExp({ running: false, 累计: 累计, 总: s.memories.length, 剩: r ? r.剩 : 0 })
1565
+ setExpNote('✗ ' + ((r && r.message) || '?'))
1566
+ load()
1567
+ return
1568
+ }
1569
+ 累计 += r.改了
1570
+ setExp({ running: !r.done, 累计: 累计, 总: r.总数, 剩: r.剩, 模型: r.provider + '/' + r.model })
1571
+ // ⚠ 一批没扩出词就收手的话,模型偶发抽风会让它看起来「自己取消」✗
1572
+ // 空批就把**批量减半**(输入少 → 思考少 → 不容易被截断),
1573
+ // 连着空三批才停,并说清楚原因
1574
+ if (r.改了 > 0) {
1575
+ 空批 = 0
1576
+ 每批 = 6
1577
+ } else {
1578
+ 空批++
1579
+ 每批 = Math.max(1, Math.floor(每批 / 2))
1580
+ }
1581
+ setExpNote(
1582
+ r.改了
1583
+ ? '这批扩了 ' + r.改了 + ' 条,还剩 ' + r.剩 + ' 条(' + r.ms + 'ms/批,' + r.provider + '/' + r.model + ')'
1584
+ : '模型这批没回可用的词(可能思考吃光了预算)—— 下一批缩到 ' + 每批 + ' 条再试(还剩 ' + r.剩 + ' 条)',
1585
+ )
1586
+ if (!r.done && (r.改了 > 0 || 空批 < 3)) setTimeout(跑, 400)
1587
+ else {
1588
+ setExp({ running: false, 累计: 累计, 总: r.总数, 剩: r.剩 })
1589
+ if (空批 >= 3) {
1590
+ setExpNote('连着 ' + 空批 + ' 批模型没回可用的词(缩到 ' + 每批 + ' 条也不行),先停了 —— 过会儿再点一次「开始扩词」')
1591
+ }
1592
+ load()
1593
+ }
1594
+ })
1595
+ .catch((e) => {
1596
+ setExp({ running: false, 累计: 累计, 总: s.memories.length, 剩: 没扩的 })
1597
+ setExpNote('✗ ' + String((e && e.message) || e))
1598
+ })
1599
+ }
1600
+ setExp({ running: true, 累计: 0, 总: s.memories.length, 剩: 没扩的 })
1601
+ setExpNote('开始…')
1602
+ 跑()
1603
+ },
1604
+ },
1605
+ exp && exp.running ? '扩着呢…' : '开始扩词',
1606
+ ),
1607
+ React.createElement('span', { style: S.dim }, '② 扩词:还没扩的 ' + 没扩的 + ' / ' + (s.memories || []).length + ' 条'),
1608
+ ),
1609
+ )
1610
+
1611
+ // ---- 提炼(导入进来的档案原文 → 真正的记忆)----
1612
+ 小件.push(
1613
+ React.createElement(
1614
+ 'div',
1615
+ { key: 'rf', style: { display: 'flex', gap: 8, alignItems: 'center', margin: '4px 0 8px', flexWrap: 'wrap' } },
1616
+ React.createElement(
1617
+ 'button',
1618
+ {
1619
+ style: S.btnDanger,
1620
+ disabled: !!(rf && rf.running) || 没提炼的 === 0,
1621
+ onClick: () => {
1622
+ let 累计 = 0
1623
+ let 跳 = 0
1624
+ let 词 = 0
1625
+ let 日 = 0
1626
+ let 重 = 0
1627
+ let 没解 = 0
1628
+ const 跑 = () => {
1629
+ post('/refine', { session: open, 每批: 6 })
1630
+ .then((r) => {
1631
+ if (!r || !r.ok) {
1632
+ setRf({ running: false, 累计: 累计, 总: s.memories.length, 剩: r ? r.剩 : 0, 模式: '提炼' })
1633
+ setRfNote('✗ ' + ((r && r.message) || '?'))
1634
+ load()
1635
+ return
1636
+ }
1637
+ 累计 += r.改了
1638
+ 跳 += r.跳过 || 0
1639
+ 词 += r.出了词的 || 0
1640
+ 日 += r.出了日期的 || 0
1641
+ 重 += r.出了重要度 || 0
1642
+ 没解 += r.没解析出来 || 0
1643
+ setRf({ running: !r.done, 累计: 累计, 总: 累计 + r.剩, 剩: r.剩, 模式: '提炼' })
1644
+ setRfNote(
1645
+ '这批改写 ' + r.改了 + ' 条、跳过 ' + (r.跳过 || 0) + ' 条,还剩 ' + r.剩 + ' 条(' +
1646
+ r.ms + 'ms/批,' + r.provider + '/' + r.model + ')',
1647
+ )
1648
+ if (!r.done) setTimeout(跑, 400)
1649
+ else {
1650
+ setRf({ running: false, 累计: 累计, 总: 累计, 剩: 0, 模式: '提炼' })
1651
+ // 检索词是提炼顺手出的,所以跑完这趟通常不用再单独跑扩词 ✓
1652
+ setRfNote(
1653
+ '✓ 提炼完了:改写 ' + 累计 + ' 条,跳过 ' + 跳 + ' 条' +
1654
+ (词 ? ';其中 ' + 词 + ' 条顺手出了检索词,不用再跑扩词了' : '(这批没出检索词,可以再点「② 扩词」补)') +
1655
+ (日 ? ';' + 日 + ' 条顺手出了日期' : '') +
1656
+ (重 ? ';' + 重 + ' 条顺手判了重要度' : '') +
1657
+ (没解 ? ';有 ' + 没解 + ' 条模型没回出东西,点「重跑跳过的」再试一次' : ''),
1658
+ )
1659
+ load()
1660
+ }
1661
+ })
1662
+ .catch((e) => {
1663
+ setRf({ running: false, 累计: 累计, 总: s.memories.length, 剩: 没提炼的, 模式: '提炼' })
1664
+ setRfNote('✗ ' + String((e && e.message) || e))
1665
+ })
1666
+ }
1667
+ setRf({ running: true, 累计: 0, 总: 没提炼的, 剩: 没提炼的, 模式: '提炼' })
1668
+ setRfNote('开始提炼…')
1669
+ 跑()
1670
+ },
1671
+ },
1672
+ rf && rf.running ? '提炼着呢…' : '开始提炼',
1673
+ ),
1674
+ React.createElement('span', { style: S.dim }, '① 提炼:还没提炼的 ' + 没提炼的 + ' 条'),
1675
+ ),
1676
+ )
1677
+ // 提炼改坏了能退回 —— 单条的在下面每条记忆上,这里是整批 ✓
1678
+ {
1679
+ const 提炼过的 = (s.memories || []).filter((m) => m.textBefore).length
1680
+ if (提炼过的 > 0) {
1681
+ 小件.push(
1682
+ React.createElement(
1683
+ 'div',
1684
+ { key: 'undo', style: { margin: '2px 0 6px' } },
1685
+ confirm && confirm.kind === 'refineundoall'
1686
+ ? React.createElement(
1687
+ 'div',
1688
+ { style: S.confirm },
1689
+ React.createElement('span', { style: { flex: 1 } }, '把提炼过的 ' + 提炼过的 + ' 条全退回原文?退回后它们会变回「还没提炼」。'),
1690
+ React.createElement('button', { style: S.btn, onClick: () => 退原文('*') }, '全退回'),
1691
+ React.createElement('button', { style: S.btn, onClick: () => setConfirm(null) }, '算了'),
1692
+ )
1693
+ : React.createElement(
1694
+ 'button',
1695
+ {
1696
+ style: S.btn,
1697
+ disabled: !!(rf && rf.running),
1698
+ title: '把提炼过的记忆全部退回提炼前的原文',
1699
+ onClick: () => setConfirm({ id: '*', kind: 'refineundoall' }),
1700
+ },
1701
+ '退回全部原文(' + 提炼过的 + ' 条)',
1702
+ ),
1703
+ ),
1704
+ )
1705
+ }
1706
+ }
1707
+ // 被「跳过」的(没改写过的)能放回队列重跑 ——
1708
+ // 模型偶尔整批回不出东西,那种不该永远躺着 ✓
1709
+ {
1710
+ const 跳过的 = (s.memories || []).filter((m) => m.refinedSkip && !m.textBefore).length
1711
+ if (跳过的 > 0) {
1712
+ 小件.push(
1713
+ React.createElement(
1714
+ 'div',
1715
+ { key: 'retry', style: { margin: '0 0 6px', fontSize: 12 } },
1716
+ React.createElement(
1717
+ 'button',
1718
+ {
1719
+ style: S.btn,
1720
+ disabled: !!(rf && rf.running),
1721
+ title: '把「跳过」的那些放回队列,再提炼一遍(模型偶尔会整批回不出东西)',
1722
+ onClick: () =>
1723
+ post('/refine/retry', { session: open })
1724
+ .then((r) => {
1725
+ setRfNote('✓ 放回了 ' + ((r && r.放回) || 0) + ' 条,可以再点「开始提炼」')
1726
+ load()
1727
+ })
1728
+ .catch((e) => setRfNote('放不回:' + String((e && e.message) || e))),
1729
+ },
1730
+ '重跑跳过的(' + 跳过的 + ' 条)',
1731
+ ),
1732
+ ),
1733
+ )
1734
+ }
1735
+ }
1736
+ if (expNote) 小件.push(React.createElement('div', { key: 'n', style: Object.assign({}, S.ok, { fontSize: 12, margin: '4px 0' }) }, expNote))
1737
+ if (rfNote) 小件.push(React.createElement('div', { key: 'n2', style: Object.assign({}, S.ok, { fontSize: 12, margin: '4px 0' }) }, rfNote))
1738
+
1739
+ // ---- 复制记忆到别的会话 ----
1740
+ // 新开一个空对话把记忆复制过去 —— 那边一点历史都没有,测起来最干净 ✓
1741
+ {
1742
+ const 别的 = (d.sessions || []).filter((x) => x.id !== open)
1743
+ if (别的.length) {
1744
+ 小件.push(
1745
+ React.createElement(
1746
+ 'div',
1747
+ { key: 'copy', style: { marginTop: 14 } },
1748
+ React.createElement(
1749
+ 'div',
1750
+ { style: Object.assign({}, S.h, { cursor: 'pointer' }), onClick: () => set复制开(!复制开) },
1751
+ (复制开 ? '▾ ' : '▸ ') + '复制记忆到别的会话',
1752
+ ),
1753
+ 复制开
1754
+ ? React.createElement(
1755
+ 'div',
1756
+ null,
1757
+ React.createElement(
1758
+ 'div',
1759
+ { style: S.nameBox },
1760
+ React.createElement('div', { style: { flex: 'none', width: 76 } }, '复制到'),
1761
+ React.createElement(
1762
+ 'select',
1763
+ { style: Object.assign({}, S.inp, { flex: 1, width: 'auto', minWidth: 150 }), value: 复制到, onChange: (e) => set复制到(e.target.value) },
1764
+ React.createElement('option', { value: '' }, '(选一个会话)'),
1765
+ 别的.map((x) =>
1766
+ React.createElement('option', { key: x.id, value: x.id }, (x.title || x.id.slice(0, 18)) + '(' + x.count + ' 条)'),
1767
+ ),
1768
+ ),
1769
+ React.createElement(
1770
+ 'button',
1771
+ {
1772
+ style: S.btnDanger,
1773
+ disabled: !复制到 || !!(exp && exp.running) || !!(rf && rf.running),
1774
+ onClick: () => setConfirm({ id: '*', kind: 'copyto' }),
1775
+ },
1776
+ '复制过去(' + (s.memories || []).length + ' 条)',
1777
+ ),
1778
+ ),
1779
+ confirm && confirm.kind === 'copyto'
1780
+ ? React.createElement(
1781
+ 'div',
1782
+ { style: S.confirm },
1783
+ React.createElement('span', { style: { flex: 1 } }, '把这一份 ' + (s.memories || []).length + ' 条加进那个会话?那边**原来的都留着**,只补它还没有的(同一句会跳过)。'),
1784
+ React.createElement('button', { style: S.btnDanger, onClick: 复制过去 }, '确认复制'),
1785
+ React.createElement('button', { style: S.btn, onClick: () => setConfirm(null) }, '算了'),
1786
+ )
1787
+ : null,
1788
+ React.createElement(
1789
+ 'div',
1790
+ { style: Object.assign({}, S.dim, { fontSize: 12 }) },
1791
+ '做法:新开一个空对话 → 回来这里把它选中 → 复制过去。那边一点历史都没有,她答得上来就一定是插件给的 ✓(复制过去会自动把那个会话的开关打开;只加不删,那边原本记的不会被换掉)',
1792
+ ),
1793
+ )
1794
+ : null,
1795
+ ),
1796
+ )
1797
+ }
1798
+ }
1799
+ return 小件
1800
+ }
1801
+
1802
+ // ---- 从旧档案导入 ----
1803
+ // 原则(照 AAAAGENT 的「导入旧聊天」):
1804
+ // ① 先扫出候选,挑完再落库 —— 不直接写进库
1805
+ // ② 已遗忘的内容不能因为再导一次就复活
1806
+ // ③ 猜不到日期就 null,绝不编
1807
+ const 扫描 = () => {
1808
+ setImpBusy(true)
1809
+ setImpNote(null)
1810
+ post('/import/scan', { session: open })
1811
+ .then((r) => {
1812
+ setImpBusy(false)
1813
+ if (!r || !r.ok) {
1814
+ setImpNote('扫不了:' + ((r && r.message) || '?'))
1815
+ return
1816
+ }
1817
+ // 默认只勾「记忆卡 / 总档案」这种明显是记忆的 ——
1818
+ // 同一个文件夹里还有我自己的工程笔记,那个不能导 ✗
1819
+ //
1820
+ // ⚠⚠ 2026-09-24 修(反馈:「别人导入会踩什么坑」时发现的):
1821
+ // 原来**只认「记忆卡 / 总档案」两个词** —— 别人的档案叫
1822
+ // `my_memories.md`、`关于我.md`、`profile.txt` → **一个都不勾**,
1823
+ // 扫完看着一片空白,很容易以为没扫到 ✗
1824
+ // → 分三层:先认像记忆的名字;一个都没认出来就**勾上有内容的**(排除明显的笔记/说明);
1825
+ // 实在拿不准就全勾上(她还能自己取消)✓
1826
+ const 像记忆 = /记忆卡|总档案|记忆|档案|回忆|自传|memory|memoir|profile|about|diary|journal/i
1827
+ const 像笔记 = /笔记|notes?|log|readme|说明|索引|目录|index|changelog|todo|草稿|draft|脚本|script/i
1828
+ const 文件们 = r.files || []
1829
+ const files = {}
1830
+ const 第一批 = 文件们.filter((f) => 像记忆.test(f.name))
1831
+ if (第一批.length) {
1832
+ for (const f of 文件们) files[f.name] = 像记忆.test(f.name)
1833
+ } else {
1834
+ const 第二批 = 文件们.filter((f) => f.候选 > 0 && !像笔记.test(f.name))
1835
+ if (第二批.length) {
1836
+ for (const f of 文件们) files[f.name] = f.候选 > 0 && !像笔记.test(f.name)
1837
+ } else {
1838
+ // 实在认不出来 → 有内容的都勾上,让她自己取消(总比一个都不勾好)
1839
+ for (const f of 文件们) files[f.name] = f.候选 > 0
1840
+ }
1841
+ }
1842
+ setImp({ data: r, files: files, off: {}, open: {} })
1843
+ setImpOpen(true)
1844
+ // ⭐ 扫到 0 条时把**原因**显示出来 —— 静默的「0 条」最坑人 ✗
1845
+ if (r.提示) setImpNote(r.提示)
1846
+ })
1847
+ .catch((e) => {
1848
+ setImpBusy(false)
1849
+ setImpNote('扫不了:' + String((e && e.message) || e))
1850
+ })
1851
+ }
1852
+
1853
+ const 选中条目 = () => {
1854
+ if (!imp) return []
1855
+ return (imp.data.items || []).filter((it) => imp.files[it.file] && imp.off[it.key] !== true)
1856
+ }
1857
+
1858
+ const 导入 = () => {
1859
+ const 要导 = 选中条目()
1860
+ if (!要导.length) return
1861
+ setImpBusy(true)
1862
+ setImpNote(null)
1863
+ post('/import/apply', { session: open, items: 要导 })
1864
+ .then((r) => {
1865
+ setImpBusy(false)
1866
+ setImpNote(
1867
+ r && r.ok
1868
+ ? '导进去了 ' + r.added + ' 条' + (r.重复 ? '(跳过重复 ' + r.重复 + ' 条)' : '') + (r.近似 ? '(跳过跟已有的几乎一样的 ' + r.近似 + ' 条)' : '') + (r.删过 ? '(跳过你删过的 ' + r.删过 + ' 条)' : '')
1869
+ : '导不了:' + ((r && r.message) || '?'),
1870
+ )
1871
+ setImp(null)
1872
+ load()
1873
+ })
1874
+ .catch((e) => {
1875
+ setImpBusy(false)
1876
+ setImpNote('导不了:' + String((e && e.message) || e))
1877
+ })
1878
+ }
1879
+
1880
+ const 导入区 = () => {
1881
+ const 小件 = []
1882
+ 小件.push(React.createElement('div', { key: 'h', style: S.h }, '从旧档案导入'))
1883
+ 小件.push(
1884
+ React.createElement(
1885
+ 'div',
1886
+ { key: 'd', style: S.dim },
1887
+ '记忆插件只管「往后」—— 装上之后它才开始自己记。你以前那些日子,得你先归纳一下:' +
1888
+ '把旧对话、日记、设定、备忘,整理成几个文本文件(.md / .txt)放进一个文件夹,这里再把它们拆成一条条记忆。',
1889
+ ),
1890
+ )
1891
+ 小件.push(
1892
+ React.createElement(
1893
+ 'div',
1894
+ {
1895
+ key: 'how',
1896
+ style: Object.assign({}, S.dim, {
1897
+ margin: '6px 0 8px',
1898
+ padding: '6px 10px',
1899
+ borderRadius: 6,
1900
+ background: 'rgba(128,128,128,.08)',
1901
+ borderLeft: '2px solid rgba(128,128,128,.35)',
1902
+ }),
1903
+ },
1904
+ React.createElement('div', { style: { fontWeight: 600 } }, '怎么归纳(一句话:一个文件一个主题,一行一件事)'),
1905
+ React.createElement('div', null, '· 分几份写:「我是谁」「重要的人和事」「约定」「时间线」……'),
1906
+ React.createElement('div', null, '· 一行写清一件事,别写成一大段流水账 —— 拆起来才准'),
1907
+ React.createElement('div', null, '· 有日期就写上;没日期不写也行,它不会瞎猜'),
1908
+ React.createElement('div', null, '· 拆完先给你过目,挑中的才进库;导错了能整批撤销'),
1909
+ React.createElement('div', null, '· 想更干净,拆完再点「② 提炼」,让模型改写成第一人称'),
1910
+ ),
1911
+ )
1912
+ 小件.push(
1913
+ React.createElement(
1914
+ 'div',
1915
+ { key: 'b', style: { display: 'flex', gap: 8, alignItems: 'center', margin: '8px 0' } },
1916
+ React.createElement('button', { style: S.btn, disabled: impBusy, onClick: 扫描 }, impBusy ? '扫着呢…' : '扫描档案'),
1917
+ imp ? React.createElement('span', { style: S.dim }, '目录:' + imp.data.dir) : null,
1918
+ imp && imp.data.selfName
1919
+ ? React.createElement('span', { style: S.dim }, '(人称:「' + imp.data.selfName + '」→「我」)')
1920
+ : React.createElement('span', { style: S.warn, fontSize: 12 }, '(这个会话还没设「她自己叫什么」,导进去的人称不会自动换 —— 建议先在上面填好)'),
1921
+ ),
1922
+ )
1923
+ // 只导哪些文件 —— 同一个文件夹里常常还堆着别的文档,得能挑 ✓
1924
+ if (imp && imp.data && (imp.data.files || []).length) {
1925
+ 小件.push(
1926
+ React.createElement(
1927
+ 'div',
1928
+ {
1929
+ key: 'files',
1930
+ style: {
1931
+ margin: '0 0 8px',
1932
+ padding: '6px 10px',
1933
+ borderRadius: 6,
1934
+ background: 'rgba(128,128,128,.08)',
1935
+ border: '1px solid rgba(128,128,128,.2)',
1936
+ fontSize: 12,
1937
+ },
1938
+ },
1939
+ React.createElement('div', { style: S.dim }, '只导这些文件(默认只勾了「记忆卡/总档案」—— 别的文档别导进来):'),
1940
+ React.createElement(
1941
+ 'div',
1942
+ { style: { display: 'flex', flexWrap: 'wrap', gap: 12, marginTop: 4 } },
1943
+ (imp.data.files || []).map((f) =>
1944
+ React.createElement(
1945
+ 'label',
1946
+ {
1947
+ key: f.name,
1948
+ style: { cursor: 'pointer', display: 'flex', gap: 4, alignItems: 'center', opacity: imp.files[f.name] ? 1 : 0.5 },
1949
+ },
1950
+ React.createElement('input', {
1951
+ type: 'checkbox',
1952
+ checked: !!imp.files[f.name],
1953
+ onChange: (e) =>
1954
+ setImp(Object.assign({}, imp, { files: Object.assign({}, imp.files, { [f.name]: e.target.checked }) })),
1955
+ }),
1956
+ f.name + '(' + f.候选 + ' 条)',
1957
+ ),
1958
+ ),
1959
+ ),
1960
+ ),
1961
+ )
1962
+ }
1963
+ if (impNote) 小件.push(React.createElement('div', { key: 'n', style: Object.assign({}, S.ok, { margin: '6px 0' }) }, impNote))
1964
+
1965
+ // 导过几批 + 撤销 —— ⚠ 必须在 `if (!imp)` **之前**:
1966
+ // 导入完 imp 就清空了,放后面的话撤销按钮根本看不到 ✗
1967
+ const 批次 = s.importBatches || []
1968
+ if (批次.length) {
1969
+ 小件.push(
1970
+ React.createElement(
1971
+ 'div',
1972
+ { key: 'bt', style: { marginTop: 4 } },
1973
+ React.createElement('div', { style: { fontWeight: 600, margin: '8px 0 2px' } }, '导过 ' + 批次.length + ' 批'),
1974
+ 批次.map((b) =>
1975
+ React.createElement(
1976
+ 'div',
1977
+ { key: b.id, style: Object.assign({}, S.row, S.条) },
1978
+ React.createElement('div', { style: { flex: 1 } }, (b.at ? rel(b.at, Date.now()) : '(不知道什么时候)') + ' ' + b.n + ' 条'),
1979
+ React.createElement(
1980
+ 'div',
1981
+ {
1982
+ style: Object.assign({}, S.drop, { flex: 'none', fontSize: 12, cursor: 'pointer' }),
1983
+ onClick: () => {
1984
+ setImpBusy(true)
1985
+ post('/import/undo', { session: open, batch: b.id }).then((r) => {
1986
+ setImpBusy(false)
1987
+ setImpNote(
1988
+ r && r.ok
1989
+ ? '撤销了:收起了 ' + r.撤了 + ' 条(在「已删掉的」里,能一条条找回来)'
1990
+ : '撤不了:' + ((r && r.message) || '?'),
1991
+ )
1992
+ load()
1993
+ })
1994
+ },
1995
+ },
1996
+ '撤销这次导入',
1997
+ ),
1998
+ ),
1999
+ ),
2000
+ ),
2001
+ )
2002
+ }
2003
+
2004
+ if (!imp) return 小件
2005
+
2006
+ // 档案目录(能选也能打)+ 档案里指她的名字
2007
+ const 现在目录 = impDir !== null ? impDir : imp.data.dir
2008
+ const 列目录 = (p) => {
2009
+ post('/dirs', { path: p || '' })
2010
+ .then((r) => {
2011
+ setImpDirs(r)
2012
+ if (r && r.ok && r.path) setImpDir(r.path)
2013
+ })
2014
+ .catch((e) => setExpNote('列不了目录:' + String((e && e.message) || e)))
2015
+ }
2016
+ // ⚠ 拆成两行、输入框用弹性宽度 ——
2017
+ // 原来四个控件挤一行,加起来 880px 撑破了 780px 的容器(放大之后更糟)✗
2018
+ 小件.push(
2019
+ React.createElement(
2020
+ 'div',
2021
+ { key: 'cfg', style: S.nameBox },
2022
+ React.createElement('div', { style: { flex: 'none', width: 92 } }, '档案目录'),
2023
+ React.createElement('input', {
2024
+ style: Object.assign({}, S.inp, { flex: 1, width: 'auto', minWidth: 130 }),
2025
+ value: 现在目录,
2026
+ onChange: (e) => setImpDir(e.target.value),
2027
+ }),
2028
+ React.createElement('button', { style: S.btn, onClick: () => 列目录(现在目录) }, '选文件夹'),
2029
+ ),
2030
+ )
2031
+ 小件.push(
2032
+ React.createElement(
2033
+ 'div',
2034
+ { key: 'cfg2', style: Object.assign({}, S.nameBox, { marginTop: -4 }) },
2035
+ React.createElement('div', { style: { flex: 'none', width: 92 } }, '指她的名字'),
2036
+ React.createElement('input', {
2037
+ style: Object.assign({}, S.inp, { flex: 1, width: 'auto', minWidth: 130 }),
2038
+ placeholder: '可以不填 —— 或者点右边「自动找」',
2039
+ value: 名字串(),
2040
+ onChange: (e) => setImpNames(e.target.value),
2041
+ }),
2042
+ React.createElement(
2043
+ 'button',
2044
+ {
2045
+ style: S.btn,
2046
+ onClick: () =>
2047
+ post('/names/scan', { session: open, dir: 现在目录 })
2048
+ .then((r) => set找名字(r))
2049
+ .catch((e) => setExpNote('自动找名字失败:' + String((e && e.message) || e))),
2050
+ },
2051
+ '自动找',
2052
+ ),
2053
+ React.createElement(
2054
+ 'button',
2055
+ {
2056
+ style: S.btn,
2057
+ onClick: () =>
2058
+ post('/names', {
2059
+ archiveDir: 现在目录,
2060
+ userNames: 名字们(),
2061
+ }).then(() => {
2062
+ setImpDir(null)
2063
+ setImpNames(null)
2064
+ 扫描()
2065
+ }),
2066
+ },
2067
+ '存了重扫',
2068
+ ),
2069
+ ),
2070
+ )
2071
+ // 自动找出来的候选 —— 点一下加进上面那格,再点一下减掉 ✓
2072
+ if (找名字) {
2073
+ 小件.push(
2074
+ React.createElement(
2075
+ 'div',
2076
+ { key: 'names', style: { margin: '0 0 8px', padding: '6px 10px', borderRadius: 6, background: 'rgba(128,128,128,.10)', border: '1px solid rgba(128,128,128,.22)', fontSize: 12 } },
2077
+ 找名字.ok === false
2078
+ ? React.createElement('div', { style: S.drop }, '✗ ' + (找名字.message || '找不了'))
2079
+ : React.createElement(
2080
+ 'div',
2081
+ null,
2082
+ React.createElement('div', null, '只喊她的 —— 点一下加进去:'),
2083
+ React.createElement(
2084
+ 'div',
2085
+ { style: { display: 'flex', flexWrap: 'wrap', gap: 6, margin: '6px 0' } },
2086
+ (找名字.候选 || []).map((x) => 一个名字块(x)),
2087
+ ),
2088
+ (找名字.互称 || []).length
2089
+ ? React.createElement(
2090
+ 'div',
2091
+ null,
2092
+ React.createElement('div', { style: Object.assign({}, S.dim, { marginTop: 4 }) }, '互相都叫的 —— 加了会把「' + 找名字.互称[0].name + '」全换成称呼,一般别加:'),
2093
+ React.createElement(
2094
+ 'div',
2095
+ { style: { display: 'flex', flexWrap: 'wrap', gap: 6, margin: '6px 0' } },
2096
+ (找名字.互称 || []).map((x) => 一个名字块(x, true)),
2097
+ ),
2098
+ )
2099
+ : null,
2100
+ React.createElement(
2101
+ 'div',
2102
+ { style: Object.assign({}, S.dim, { fontSize: 11 }) },
2103
+ '数字是出现次数。底下混着界面词没关系,不点就行。填不填都不影响导入。',
2104
+ ),
2105
+ ),
2106
+ ),
2107
+ )
2108
+ }
2109
+ // 现在扫的是哪个目录 —— 用文字再写一遍,不靠输入框 ✓
2110
+ 小件.push(
2111
+ React.createElement(
2112
+ 'div',
2113
+ { key: 'nowdir', style: Object.assign({}, S.dim, { fontSize: 12, margin: '2px 0 6px' }) },
2114
+ '现在扫的是:' + imp.data.dir + ' (' + (imp.data.files || []).length + ' 个文件,' + (imp.data.items || []).length + ' 条候选)',
2115
+ ),
2116
+ )
2117
+ // 文件夹选择器
2118
+ if (impDirs) {
2119
+ 小件.push(
2120
+ React.createElement(
2121
+ 'div',
2122
+ { key: 'dirs', style: { margin: '0 0 8px', padding: '6px 10px', borderRadius: 6, background: 'rgba(128,128,128,.10)', border: '1px solid rgba(128,128,128,.22)', fontSize: 12 } },
2123
+ impDirs.ok === false
2124
+ ? React.createElement('div', { style: S.drop }, '✗ ' + (impDirs.message || '列不了'))
2125
+ : React.createElement(
2126
+ 'div',
2127
+ null,
2128
+ React.createElement(
2129
+ 'div',
2130
+ { style: Object.assign({}, S.dim, { marginBottom: 4 }) },
2131
+ impDirs.path ? '「' + impDirs.path + '」下的文件夹(点一下就进去):' : '选一个盘:',
2132
+ ),
2133
+ React.createElement(
2134
+ 'div',
2135
+ { style: { display: 'flex', gap: 10, flexWrap: 'wrap' } },
2136
+ impDirs.parent
2137
+ ? React.createElement('span', { style: S.link, onClick: () => 列目录(impDirs.parent) }, '↑ 上一级')
2138
+ : null,
2139
+ (impDirs.dirs || []).map((d) =>
2140
+ React.createElement('span', { key: d.path, style: S.link, onClick: () => 列目录(d.path) }, '📁 ' + d.name),
2141
+ ),
2142
+ (impDirs.dirs || []).length === 0 && !impDirs.parent
2143
+ ? React.createElement('span', { style: S.dim }, '(这里没有子文件夹)')
2144
+ : null,
2145
+ ),
2146
+ React.createElement(
2147
+ 'div',
2148
+ { style: Object.assign({}, S.dim, { marginTop: 4 }) },
2149
+ '看好了就点「存了重扫」✓',
2150
+ ),
2151
+ ),
2152
+ ),
2153
+ )
2154
+ }
2155
+
2156
+ // 文件勾选
2157
+ 小件.push(
2158
+ React.createElement(
2159
+ 'div',
2160
+ { key: 'files', style: S.nameBox },
2161
+ React.createElement('div', { style: { flex: 'none', width: 100 } }, '扫到这些文件'),
2162
+ React.createElement(
2163
+ 'div',
2164
+ { style: { flex: 1 } },
2165
+ (imp.data.files || []).map((f) =>
2166
+ React.createElement(
2167
+ 'label',
2168
+ { key: f.name, style: { display: 'block', cursor: 'pointer' } },
2169
+ React.createElement('input', {
2170
+ type: 'checkbox',
2171
+ checked: !!imp.files[f.name],
2172
+ onChange: (e) => setImp(Object.assign({}, imp, { files: Object.assign({}, imp.files, { [f.name]: e.target.checked }) })),
2173
+ }),
2174
+ ' ' + f.name + ' ' + f.候选 + ' 条',
2175
+ /记忆卡|总档案/.test(f.name) ? null : React.createElement('span', { style: S.dim }, ' (不是她的记忆?自己看着勾)'),
2176
+ ),
2177
+ ),
2178
+ ),
2179
+ ),
2180
+ )
2181
+
2182
+ // 按小节分组(只列勾了的文件)
2183
+ const 组 = {}
2184
+ for (const it of imp.data.items || []) {
2185
+ if (!imp.files[it.file]) continue
2186
+ const k = it.file + '|' + (it.section || '(没有小节)')
2187
+ if (!组[k]) 组[k] = { file: it.file, section: it.section || '(没有小节)', items: [] }
2188
+ 组[k].items.push(it)
2189
+ }
2190
+ const 组们 = Object.keys(组)
2191
+ const 选中 = 选中条目()
2192
+ const 字数 = 选中.reduce((a, it) => a + String(it.改后 || it.text).length, 0)
2193
+
2194
+ 小件.push(
2195
+ React.createElement(
2196
+ 'div',
2197
+ { key: 'secbar', style: { display: 'flex', gap: 8, alignItems: 'center', margin: '10px 0 4px' } },
2198
+ React.createElement('span', { style: { fontWeight: 600 } }, '按小节挑'),
2199
+ React.createElement('button', { style: S.btn, onClick: () => setImp(Object.assign({}, imp, { off: {} })) }, '全选'),
2200
+ React.createElement(
2201
+ 'button',
2202
+ {
2203
+ style: S.btn,
2204
+ onClick: () => {
2205
+ const off = {}
2206
+ for (const it of imp.data.items || []) if (imp.files[it.file] && it.importance < 1) off[it.key] = true
2207
+ setImp(Object.assign({}, imp, { off: off }))
2208
+ },
2209
+ },
2210
+ '只留「一直」记得的',
2211
+ ),
2212
+ React.createElement(
2213
+ 'button',
2214
+ {
2215
+ style: S.btn,
2216
+ onClick: () => {
2217
+ const off = {}
2218
+ for (const it of imp.data.items || []) if (imp.files[it.file]) off[it.key] = true
2219
+ setImp(Object.assign({}, imp, { off: off }))
2220
+ },
2221
+ },
2222
+ '全不选',
2223
+ ),
2224
+ ),
2225
+ )
2226
+
2227
+ for (const k of 组们) {
2228
+ const g = 组[k]
2229
+ const 开 = !!imp.open[k]
2230
+ const 选了几条 = g.items.filter((it) => imp.off[it.key] !== true).length
2231
+ 小件.push(
2232
+ React.createElement(
2233
+ 'div',
2234
+ { key: 'sec' + k, style: { margin: '2px 0' } },
2235
+ React.createElement(
2236
+ 'div',
2237
+ { style: { display: 'flex', gap: 8, alignItems: 'baseline' } },
2238
+ React.createElement(
2239
+ 'div',
2240
+ { style: Object.assign({}, S.link, { flex: 1 }), onClick: () => setImp(Object.assign({}, imp, { open: Object.assign({}, imp.open, { [k]: !开 }) })) },
2241
+ (开 ? '▾ ' : '▸ ') + g.section,
2242
+ ),
2243
+ React.createElement('div', { style: Object.assign({}, S.dim, { width: 90, flex: 'none' }) }, 选了几条 + ' / ' + g.items.length + ' 条'),
2244
+ React.createElement(
2245
+ 'div',
2246
+ {
2247
+ style: Object.assign({}, S.link, { flex: 'none', fontSize: 12 }),
2248
+ onClick: () => {
2249
+ const 全选这节 = 选了几条 < g.items.length
2250
+ const off = Object.assign({}, imp.off)
2251
+ for (const it of g.items) if (全选这节) delete off[it.key]
2252
+ else off[it.key] = true
2253
+ setImp(Object.assign({}, imp, { off: off }))
2254
+ },
2255
+ },
2256
+ '全选这节',
2257
+ ),
2258
+ ),
2259
+ 开
2260
+ ? g.items.map((it) =>
2261
+ React.createElement(
2262
+ 'div',
2263
+ { key: it.key, style: { margin: '2px 0 6px 18px', fontSize: 12 } },
2264
+ React.createElement(
2265
+ 'label',
2266
+ { style: { cursor: 'pointer' } },
2267
+ React.createElement('input', {
2268
+ type: 'checkbox',
2269
+ checked: imp.off[it.key] !== true,
2270
+ onChange: (e) => setImp(Object.assign({}, imp, { off: Object.assign({}, imp.off, { [it.key]: !e.target.checked }) })),
2271
+ }),
2272
+ ' ',
2273
+ React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 11 }) }, it.file + ' 第 ' + it.line + ' 行 '),
2274
+ String(it.改后 || it.text),
2275
+ it.at ? React.createElement('span', { style: Object.assign({}, S.ok, { fontSize: 11 }) }, ' [' + it.at.slice(0, 10) + ']') : null,
2276
+ it.改后 && it.改后 !== it.text
2277
+ ? React.createElement('div', { style: Object.assign({}, S.dim, { marginLeft: 18, fontSize: 11 }) }, '原文:' + it.text)
2278
+ : null,
2279
+ ),
2280
+ ),
2281
+ )
2282
+ : null,
2283
+ ),
2284
+ )
2285
+ }
2286
+
2287
+ 小件.push(
2288
+ React.createElement(
2289
+ 'div',
2290
+ { key: 'go', style: Object.assign({}, S.askBox, { marginTop: 10 }) },
2291
+ React.createElement('div', { style: { flex: 1 } }, '已选 ' + 选中.length + ' 条,共约 ' + 字数 + ' 字' + (选中.length ? '(一次只注入 6 条,其余在库里等着)' : '')),
2292
+ React.createElement('button', { style: S.btnDanger, disabled: impBusy || !选中.length, onClick: 导入 }, impBusy ? '导着呢…' : '导入选中的'),
2293
+ ),
2294
+ )
2295
+ return 小件
2296
+ }
2297
+
2298
+ return React.createElement(
2299
+ 'div',
2300
+ { style: S.wrap },
2301
+ React.createElement('div', { style: S.link, onClick: () => setOpen(null) }, '← 所有会话'),
2302
+ React.createElement('div', { style: S.h }, s.title || s.id),
2303
+ React.createElement('div', { style: S.dim }, s.id + ' ' + s.memories.length + ' 条记忆' + (已删.length ? ' (另外还收着 ' + 已删.length + ' 条)' : '')),
2304
+ React.createElement(
2305
+ 'div',
2306
+ { style: { padding: '10px 12px', borderRadius: 8, border: '1px solid rgba(128,128,128,.22)', background: 'rgba(127,127,127,.05)' } },
2307
+ React.createElement(
2308
+ 'div',
2309
+ { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 } },
2310
+ React.createElement('span', { style: { flex: 'none', width: 96, fontSize: 13 } }, '她自己叫什么'),
2311
+ React.createElement('input', {
2312
+ style: Object.assign({}, S.inp, { width: 160 }),
2313
+ placeholder: '她在这儿的名字',
2314
+ value: selfDraft[open] !== undefined ? selfDraft[open] : s.selfName || s.titleName || '',
2315
+ onChange: (e) => setSelfDraft(Object.assign({}, selfDraft, { [open]: e.target.value })),
2316
+ }),
2317
+ s.selfName
2318
+ ? null
2319
+ : (s.titleName ? React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12 }) }, '(先填的是会话标题,确认没问题再点「存」)') : null),
2320
+ ),
2321
+ React.createElement(
2322
+ 'div',
2323
+ { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 } },
2324
+ React.createElement('span', { style: { flex: 'none', width: 96, fontSize: 13 } }, '她怎么称呼你'),
2325
+ React.createElement('input', {
2326
+ style: Object.assign({}, S.inp, { width: 160 }),
2327
+ placeholder: '她平时怎么叫你(小名、昵称都行)',
2328
+ value: ownerDraft[open] !== undefined ? ownerDraft[open] : s.ownerName || '',
2329
+ onChange: (e) => setOwnerDraft(Object.assign({}, ownerDraft, { [open]: e.target.value })),
2330
+ }),
2331
+ ),
2332
+ React.createElement(
2333
+ 'div',
2334
+ { style: { display: 'flex', alignItems: 'center', gap: 10 } },
2335
+ React.createElement(
2336
+ 'button',
2337
+ {
2338
+ style: S.btn,
2339
+ onClick: () =>
2340
+ post('/names', {
2341
+ session: open,
2342
+ selfName: (selfDraft[open] !== undefined ? selfDraft[open] : s.selfName) || '',
2343
+ ownerName: (ownerDraft[open] !== undefined ? ownerDraft[open] : s.ownerName) || '',
2344
+ }).then(() => {
2345
+ setSelfDraft({})
2346
+ setOwnerDraft({})
2347
+ load()
2348
+ }),
2349
+ },
2350
+ '存',
2351
+ ),
2352
+ React.createElement(
2353
+ 'span',
2354
+ { style: Object.assign({}, S.dim, { fontSize: 12, flex: 1, lineHeight: 1.5 }) },
2355
+ '她自己的名字写进记忆时会换成「我」;她怎么称呼你,会换成「你」。已有的记忆也会顺手洗一遍。',
2356
+ ),
2357
+ ),
2358
+ ),
2359
+ // ---- 2026-09-21 新加:她自己后台手动加一条 ----
2360
+ // 跟模型自己记是两回事:它记受上面那个开关管,**手动加不受管** ✓
2361
+ // (开关管的是「他自己记不记、注不注入」,管不到她手动写)
2362
+ React.createElement(
2363
+ 'div',
2364
+ { style: Object.assign({}, S.h, { cursor: 'pointer', marginTop: 16 }), onClick: () => set加展开(!加展开) },
2365
+ (加展开 ? '▾ ' : '▸ ') + '+ 自己加一条(开关关着也能加)',
2366
+ ),
2367
+ 加展开
2368
+ ? React.createElement(
2369
+ 'div',
2370
+ { style: S.editBox },
2371
+ React.createElement(
2372
+ 'div',
2373
+ { style: Object.assign({}, S.nameBox, { margin: '0 0 8px' }) },
2374
+ React.createElement('div', { style: { flex: 'none', width: 44 } }, '加到哪里'),
2375
+ React.createElement(
2376
+ 'select',
2377
+ {
2378
+ style: Object.assign({}, S.inp, { flex: 1, width: 'auto', minWidth: 160 }),
2379
+ value: 加在谁 || open || '',
2380
+ onChange: (e) => set加在谁(e.target.value),
2381
+ },
2382
+ (((d && d.sessions) || []).map((x) => React.createElement(
2383
+ 'option',
2384
+ { key: x.id, value: x.id },
2385
+ (x.title || ('…' + String(x.id).slice(-6))) + (x.enabled ? '' : '(没开记忆)'),
2386
+ ))),
2387
+ ),
2388
+ React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12 }) }, '默认是你正打开的这个'),
2389
+ ),
2390
+ React.createElement('textarea', {
2391
+ style: Object.assign({}, S.area, { minHeight: 54 }),
2392
+ placeholder: '一句话说清。人称:你自己是「我」,她是「她」——写「小明」会自动换成「我」。',
2393
+ value: 加内容,
2394
+ onChange: (e) => set加内容(e.target.value),
2395
+ }),
2396
+ React.createElement(
2397
+ 'div',
2398
+ { style: Object.assign({}, S.nameBox, { margin: '8px 0 0' }) },
2399
+ React.createElement('div', { style: { flex: 'none', width: 44 } }, '时间'),
2400
+ React.createElement('input', {
2401
+ style: Object.assign({}, S.inp, { flex: 1, width: 'auto', minWidth: 150 }),
2402
+ placeholder: '这件事什么时候发生的(留空=现在)',
2403
+ value: 加时间,
2404
+ onChange: (e) => set加时间(e.target.value),
2405
+ }),
2406
+ React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12 }) }, '比如 2026-09-19 或 2026-09-19 14:30'),
2407
+ ),
2408
+ React.createElement(
2409
+ 'div',
2410
+ { style: Object.assign({}, S.nameBox, { margin: '6px 0 0' }) },
2411
+ React.createElement('div', { style: { flex: 'none', width: 44 } }, '重要'),
2412
+ ['0', '0.5', '1'].map((v) =>
2413
+ React.createElement(
2414
+ 'button',
2415
+ {
2416
+ key: v,
2417
+ style: Object.assign({}, S.btn, String(加重要) === v ? { borderColor: '#5b8fd6', color: '#8fb8f0' } : null),
2418
+ onClick: () => set加重要(v),
2419
+ },
2420
+ v === '0' ? '0 随口提过' : v === '0.5' ? '0.5 比较重要' : '1 非常重要',
2421
+ ),
2422
+ ),
2423
+ React.createElement('input', {
2424
+ style: Object.assign({}, S.inp, { width: 58 }),
2425
+ value: 加重要,
2426
+ onChange: (e) => set加重要(e.target.value),
2427
+ }),
2428
+ React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12 }) }, '也能自己填 0~1'),
2429
+ ),
2430
+ React.createElement(
2431
+ 'div',
2432
+ { style: Object.assign({}, S.nameBox, { margin: '6px 0 0' }) },
2433
+ React.createElement('div', { style: { flex: 'none', width: 44 } }, '关键词'),
2434
+ React.createElement('input', {
2435
+ style: Object.assign({}, S.inp, { flex: 1, width: 'auto', minWidth: 150 }),
2436
+ placeholder: '空格分开,方便以后被想起来(可留空)',
2437
+ value: 加词,
2438
+ onChange: (e) => set加词(e.target.value),
2439
+ }),
2440
+ ),
2441
+ React.createElement(
2442
+ 'div',
2443
+ { style: Object.assign({}, S.askBox, { margin: '8px 0 0' }) },
2444
+ React.createElement('button', { style: S.btn, onClick: 提交一条 }, '加进这个会话'),
2445
+ React.createElement(
2446
+ 'span',
2447
+ { style: Object.assign({}, 加结果 && 加结果.ok ? S.ok : S.drop, { fontSize: 12 }) },
2448
+ (加结果 && 加结果.文字) || '',
2449
+ ),
2450
+ ),
2451
+ React.createElement(
2452
+ 'div',
2453
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 4 }) },
2454
+ '加完记得点一下「开始扩词」—— 不然这条只有正文能搜到,检索词还是空的。',
2455
+ ),
2456
+ )
2457
+ : null,
2458
+ // ⚠ 记忆列表**默认折叠** —— 库里几百条的时候,它压在中间会把
2459
+ // 「提炼 / 扩词」那些按钮顶到很下面,她得一直翻才够得着 ✗
2460
+ React.createElement(
2461
+ 'div',
2462
+ { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 16 } },
2463
+ React.createElement(
2464
+ 'div',
2465
+ { style: Object.assign({}, S.h, { cursor: 'pointer', margin: 0, flex: 1 }), onClick: () => set看记忆(!看记忆) },
2466
+ (看记忆 ? '▾ ' : '▸ ') + '记忆列表(' + s.memories.length + ' 条)',
2467
+ ),
2468
+ // ⭐⭐ 2026-09-22(反馈:「查重我们是不是还没做啊?我怎么没有找到按钮」):
2469
+ // ⚠⚠ 第一次我把它放进了**折叠起来的**记忆列表块里 ——
2470
+ // 不点开列表就看不见,那就等于没有按钮 ✗
2471
+ // → 挪到**标题这一行**(这一行永远看得见)✓
2472
+ React.createElement(
2473
+ 'button',
2474
+ {
2475
+ style: S.btn,
2476
+ title:
2477
+ '找出库里「几乎一样」的记忆。自动去重只管召回和导入,管不到已经存着的。' +
2478
+ '真重复能一键合;「汇总+明细」会列出来让你自己挑;合之前自动备份',
2479
+ onClick: () => 去查重(s),
2480
+ },
2481
+ '查重',
2482
+ ),
2483
+ // ⭐⭐ 2026-09-22(她的第十件):**从会话生成档案**
2484
+ // 「别的用户导入自己的会话 → 模型读一遍 → 出一份档案」
2485
+ // ⚠ 这是**给别人用的功能**,跟「手工维护一份档案」是两码事 ✓
2486
+ React.createElement(
2487
+ 'button',
2488
+ {
2489
+ style: S.btn,
2490
+ title:
2491
+ '把你自己的会话读一遍,提炼出一份长期档案(分九章)。' +
2492
+ '出好的档案再走「从旧档案导入」变成记忆条目。' +
2493
+ '会调用模型、会花钱,所以分四步:先看会话 → 算一下要多少 → 一批一批跑 → 存文件',
2494
+ onClick: () => 开生成(),
2495
+ },
2496
+ '从会话生成档案',
2497
+ ),
2498
+ // ⭐⭐ 2026-09-22(她要的):**批量删除** —— 勾了几条就显示几条
2499
+ // ⚠ 二次确认:点这里**不删**,只出确认条;确认条上再点「确定删」才真删 ✓
2500
+ 已选几条 > 0
2501
+ ? React.createElement(
2502
+ 'button',
2503
+ {
2504
+ style: S.btnDanger,
2505
+ title: '把勾上的这几条收进「已删」(不是销毁,能找回来)。点它不会直接删,会先问一遍',
2506
+ onClick: () => set批删问(true),
2507
+ },
2508
+ '批量删(' + 已选几条 + ')',
2509
+ )
2510
+ : null,
2511
+ 已选几条 > 0
2512
+ ? React.createElement(
2513
+ 'button',
2514
+ { style: S.btn, onClick: () => { set选中({}); set批删问(false) } },
2515
+ '取消勾选',
2516
+ )
2517
+ : null,
2518
+ ),
2519
+ // ⭐ 2026-09-24:折叠着也能一眼看出「记了时间的」有多少 ——
2520
+ // 反馈那个「中间一坨」的根源就是 850 条里只有 188 条有日期,
2521
+ // 这个数字摆在标题下面,心里有底 ✓
2522
+ React.createElement(
2523
+ 'div',
2524
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2 }) },
2525
+ '记了时间的 ' + 有时间数 + ' 条 · 没记时间的 ' + 没时间数 + ' 条' +
2526
+ (看哪边 === '全部' ? '' : '(现在只看' + (看哪边 === '有时间' ? '记了时间的' : '没记时间的') + ')'),
2527
+ ),
2528
+ 看记忆
2529
+ ? React.createElement(
2530
+ 'div',
2531
+ null,
2532
+ // ---- 检索框:几百条的时候,靠翻是翻不到的 ----
2533
+ React.createElement(
2534
+ 'div',
2535
+ { style: S.nameBox },
2536
+ React.createElement('div', { style: { flex: 'none', width: 44 } }, '找'),
2537
+ React.createElement('input', {
2538
+ style: Object.assign({}, S.inp, { flex: 1, width: 'auto', minWidth: 140 }),
2539
+ placeholder: '打几个字,只留含它的(正文、原文、检索词、来源都能搜)',
2540
+ value: 搜词,
2541
+ onChange: (e) => set搜词(e.target.value),
2542
+ }),
2543
+ 搜词
2544
+ ? React.createElement('button', { style: S.btn, onClick: () => set搜词('') }, '清掉')
2545
+ : null,
2546
+ // ⭐ 2026-09-24(她要的):**记了时间的 / 没记时间的分开看** ✓
2547
+ // ⭐ 2026-09-22 又加两档:**长期事实 / 会淡的** ✓
2548
+ // 点一下就轮到下一种:
2549
+ // 全部 → 只记了时间的 → 只有没记时间的 → 不会淡的 → 会淡的 → 全部…
2550
+ React.createElement(
2551
+ 'button',
2552
+ {
2553
+ style: S.btn,
2554
+ title:
2555
+ '点一下换个看法:全部 → 只记了时间的 → 只有没记时间的 → ' +
2556
+ '不会淡的 → 会淡的 → 全部。' +
2557
+ '「全部」那个视图里,不会淡的收在一个折叠的分组里(外面只剩会淡的)',
2558
+ onClick: () =>
2559
+ set看哪边(
2560
+ 看哪边 === '全部'
2561
+ ? '有时间'
2562
+ : 看哪边 === '有时间'
2563
+ ? '没时间'
2564
+ : 看哪边 === '没时间'
2565
+ ? '长期事实'
2566
+ : 看哪边 === '长期事实'
2567
+ ? '会淡的'
2568
+ : '全部',
2569
+ ),
2570
+ },
2571
+ '看:' +
2572
+ (看哪边 === '全部'
2573
+ ? '全部'
2574
+ : 看哪边 === '有时间'
2575
+ ? '只记了时间的(' + 有时间数 + ')'
2576
+ : 看哪边 === '没时间'
2577
+ ? '只有没记时间的(' + 没时间数 + ')'
2578
+ : 看哪边 === '长期事实'
2579
+ ? '不会淡的(' + 事实总数 + ')'
2580
+ : '会淡的(' + 会淡总数 + ')'),
2581
+ ),
2582
+ // ⭐ 顺序开关:按日期排,默认「日期新的在前」
2583
+ // (851 条里只画前 200 条,不排的话看到的永远是最老的)
2584
+ React.createElement(
2585
+ 'button',
2586
+ {
2587
+ style: S.btn,
2588
+ title: '按最左边的日期排。没写日期的排在最后 —— 它们不知道是什么时候的事',
2589
+ onClick: () => set顺序(顺序 === '新' ? '旧' : '新'),
2590
+ },
2591
+ '日期:' + (顺序 === '新' ? '新的在前' : '老的在前'),
2592
+ ),
2593
+ // ⭐ 补日期:从正文里认出日期,只补没日期的(补之前自动备份)
2594
+ 没日期的 > 0
2595
+ ? React.createElement(
2596
+ 'button',
2597
+ {
2598
+ style: S.btn,
2599
+ title: '从正文里认出日期(09-17、8月26日…)。只补没日期的,已有的不动;补之前会自动备份',
2600
+ onClick: () => setConfirm({ id: '*', kind: 'backfill' }),
2601
+ },
2602
+ '补日期(' + 没日期的 + ')',
2603
+ )
2604
+ : null,
2605
+ // ⭐⭐ 2026-09-22:补标「永远不淡」——
2606
+ // 提炼原来只改重要度、不碰分类,所以老记忆该不淡的没标上
2607
+ 没判过的 > 0
2608
+ ? React.createElement(
2609
+ 'button',
2610
+ {
2611
+ style: S.btn,
2612
+ title:
2613
+ '拿模型一条条判「该不该一直记得」,判成要记一辈子的就标成「永远不淡」' +
2614
+ '(会收进「长期事实」那一栏)。判过的不会重复问;动之前自动备份',
2615
+ onClick: () => setConfirm({ id: '*', kind: 'stable' }),
2616
+ },
2617
+ '补标不淡(' + 没判过的 + ')',
2618
+ )
2619
+ : null,
2620
+ // ⭐ 2026-09-22(反馈「判定太松了就得改」):判据改严之后,
2621
+ // **已经判过的那批得用新判据重来一遍** —— 否则老结果一直留着 ✗
2622
+ 判过的 > 0
2623
+ ? React.createElement(
2624
+ 'button',
2625
+ {
2626
+ style: S.btn,
2627
+ title:
2628
+ '把所有「判过」的标记清掉,用现在的判据从头判一遍。' +
2629
+ '判据改过之后要用这个 —— 不然老结果一直留着。会重新调模型,动之前自动备份',
2630
+ onClick: () => setConfirm({ id: '*', kind: 'restable' }),
2631
+ },
2632
+ '重判一遍(' + 判过的 + ')',
2633
+ )
2634
+ : null,
2635
+ // ⭐⭐ 2026-09-23(第三步):**老数据清洗补键** ✓
2636
+ // 提炼只管以后;老条目本来就没键 → 这里单独过一遍
2637
+ // ⚠ 先「试几条」再真洗 —— 一条命令改几百条,得让她先看一眼 ✓
2638
+ // ⚠⚠ 按钮上**不许写「清洗补键」** —— 那是我自己造的词,她看不懂 ✗
2639
+ // 改成说**它有什么用**:「讲同一件事的,只留最新那条」
2640
+ 没键的 > 0
2641
+ ? React.createElement(
2642
+ 'button',
2643
+ {
2644
+ style: S.btn,
2645
+ title:
2646
+ '让老记忆认出「讲的是同一件事」。\n' +
2647
+ '比如「她身高一米七」和「她身高一米六八」是同一件事 —— ' +
2648
+ '认出来之后只留最新那条,旧的收起来(还能恢复)。\n' +
2649
+ '光看句子像不像认不出来,所以要一条条问模型。\n' +
2650
+ '⚠ 先点「试 8 条看看」看一眼它标的对不对,再决定要不要全跑。',
2651
+ onClick: () => setConfirm({ id: '*', kind: 'washkeys' }),
2652
+ },
2653
+ '同一件事只留最新(' + 没键的 + ')',
2654
+ )
2655
+ : null,
2656
+ // 补过了就想退回来(几百条自动改的,得能一键回到洗之前)✓
2657
+ 可撤的 > 0
2658
+ ? React.createElement(
2659
+ 'button',
2660
+ {
2661
+ style: S.btn,
2662
+ title:
2663
+ '把这次自动标的全撤掉、顺手收起来的也全放回来 —— 回到没跑之前。\n' +
2664
+ '(只撤自动标的 —— 你自己填的那些不动)',
2665
+ onClick: () => setConfirm({ id: '*', kind: 'unwashkeys' }),
2666
+ },
2667
+ '撤掉这次标的(' + 可撤的 + ')',
2668
+ )
2669
+ : null,
2670
+ ),
2671
+ // ⭐⭐ 2026-09-23:她说了「这个让人看不懂,写通俗一点的」——
2672
+ // 光按钮上有个名字不够,旁边得有一句人话解释它到底干嘛 ✓
2673
+ // (只在按钮露着的时候出现,平时不占地方)
2674
+ 没键的 > 0
2675
+ ? React.createElement(
2676
+ 'div',
2677
+ { style: Object.assign({}, S.dim, { fontSize: 11, margin: '4px 0 0' }) },
2678
+ '「同一件事只留最新」是干嘛的:给老记忆一条条标上「这条讲的是哪件事」。' +
2679
+ '标好之后,讲同一件事的只留最新那条,旧的收起来(还能恢复)。',
2680
+ )
2681
+ : null,
2682
+ // ⭐⭐ 2026-09-23:跑过之后跟她交代一句「剩下的去哪儿了」✓
2683
+ // 模型会把一批判成「这条不用标」(一次性的、拿不准的)——
2684
+ // 那种**不标但算问过了**。不说明的话她会以为漏了/白跑了 ✗
2685
+ 判留空的 > 0
2686
+ ? React.createElement(
2687
+ 'div',
2688
+ { style: Object.assign({}, S.dim, { fontSize: 11, margin: '4px 0 0' }) },
2689
+ '(另外 ' + 判留空的 + ' 条,模型看了说「这条不用标」—— 一次性的、或者拿不准的。' +
2690
+ '那种一个字都没改,也不会再问一遍 ✓)',
2691
+ )
2692
+ : null,
2693
+ confirm && confirm.kind === 'stable'
2694
+ ? React.createElement(
2695
+ 'div',
2696
+ { style: S.askBox },
2697
+ React.createElement(
2698
+ 'div',
2699
+ { style: { flex: 1 } },
2700
+ '拿模型判一遍这 ' + 没判过的 + ' 条该不该「永远不淡」?',
2701
+ React.createElement(
2702
+ 'div',
2703
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2 }) },
2704
+ '会调用模型(走你 DSH 里那个),一批 20 条慢慢跑。判过的不会重复问。',
2705
+ ),
2706
+ ),
2707
+ React.createElement('button', { style: S.btnDanger, disabled: busy, onClick: () => 补不淡(false) }, busy ? '判着呢…' : '开始'),
2708
+ React.createElement('button', { style: S.btn, onClick: () => setConfirm(null) }, '算了'),
2709
+ )
2710
+ : null,
2711
+ // ⭐ 重判:判据改过之后,把老结果清掉重来 ✓
2712
+ confirm && confirm.kind === 'restable'
2713
+ ? React.createElement(
2714
+ 'div',
2715
+ { style: S.askBox },
2716
+ React.createElement(
2717
+ 'div',
2718
+ { style: { flex: 1 } },
2719
+ '把 ' + 判过的 + ' 条的「判过」标记全清掉,用现在的判据重判一遍?',
2720
+ React.createElement(
2721
+ 'div',
2722
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2 }) },
2723
+ '判据改过之后要用这个 —— 不然老结果一直留着。会重新调模型(全库 ' +
2724
+ (s.memories || []).length + ' 条,一批 20 条),动之前自动备份。',
2725
+ ),
2726
+ ),
2727
+ React.createElement('button', { style: S.btnDanger, disabled: busy, onClick: () => 补不淡(true) }, busy ? '判着呢…' : '重判'),
2728
+ React.createElement('button', { style: S.btn, onClick: () => setConfirm(null) }, '算了'),
2729
+ )
2730
+ : null,
2731
+ // ⭐⭐ 2026-09-23:清洗补键 —— 先「试几条」✓
2732
+ confirm && confirm.kind === 'washkeys'
2733
+ ? React.createElement(
2734
+ 'div',
2735
+ { style: S.askBox },
2736
+ React.createElement(
2737
+ 'div',
2738
+ { style: { flex: 1 } },
2739
+ '这 ' + 没键的 + ' 条老记忆,让它们认出「讲的是同一件事」?',
2740
+ React.createElement(
2741
+ 'div',
2742
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2 }) },
2743
+ '每条记忆贴一个说明:「这条讲的是哪件事」。讲同一件事的,以后只留最新那条,' +
2744
+ '旧的收起来(还能恢复)—— 就不用再靠「句子像不像」去猜了。' +
2745
+ '会调用模型(走你 DSH 里那个),一批 8 条慢慢跑,只在设置页跑、不碰对话。动之前自动备份。',
2746
+ ),
2747
+ ),
2748
+ React.createElement('button', { style: S.btn, disabled: busy, onClick: () => 洗键(true) }, busy ? '跑着呢…' : '先试 8 条看看'),
2749
+ React.createElement('button', { style: S.btnDanger, disabled: busy, onClick: () => 洗键(false) }, busy ? '跑着呢…' : '全部开始'),
2750
+ React.createElement('button', { style: S.btn, onClick: () => setConfirm(null) }, '算了'),
2751
+ )
2752
+ : null,
2753
+ // 撤退:把这次自动标的全撤掉
2754
+ confirm && confirm.kind === 'unwashkeys'
2755
+ ? React.createElement(
2756
+ 'div',
2757
+ { style: S.askBox },
2758
+ React.createElement(
2759
+ 'div',
2760
+ { style: { flex: 1 } },
2761
+ '把这次自动标的 ' + 可撤的 + ' 条全撤掉、顺手收起来的也全放回来?',
2762
+ React.createElement(
2763
+ 'div',
2764
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2 }) },
2765
+ '回到没跑之前的样子。只撤自动标的 —— 你自己填的那些不动。' +
2766
+ '记忆本身一条都不会被删。',
2767
+ ),
2768
+ ),
2769
+ React.createElement('button', { style: S.btnDanger, disabled: busy, onClick: 撤销洗键 }, busy ? '撤着呢…' : '撤掉'),
2770
+ React.createElement('button', { style: S.btn, onClick: () => setConfirm(null) }, '算了'),
2771
+ )
2772
+ : null,
2773
+ // ⭐⭐ 2026-09-23:**进度条**(跟补标那条分开,不互相盖)✓
2774
+ 洗键进度.在跑 && 洗键进度.总 > 0
2775
+ ? (() => {
2776
+ const 百 = Math.round((洗键进度.已 / 洗键进度.总) * 100)
2777
+ return React.createElement(
2778
+ 'div',
2779
+ { style: Object.assign({}, S.editBox, { marginTop: 8 }) },
2780
+ React.createElement(
2781
+ 'div',
2782
+ { style: { fontSize: 13, marginBottom: 4 } },
2783
+ '⭐ 正在认「讲的是同一件事」' + (洗键进度.干跑 ? '(先试几条)' : '') + ' 跑着呢',
2784
+ ),
2785
+ React.createElement(
2786
+ 'div',
2787
+ { style: { fontSize: 12, color: '#8a6d3b' } },
2788
+ '已过 ' + 洗键进度.已 + ' / ' + 洗键进度.总 + ' 条(' + 百 + '%)' +
2789
+ ' 还剩 ' + Math.max(0, 洗键进度.总 - 洗键进度.已) + ' 条 (每批 8 条,跑着就动)',
2790
+ ),
2791
+ )
2792
+ })()
2793
+ : null,
2794
+ // ⭐⭐ 2026-09-22(她要的):**补标/重判的进度条,就贴在按钮这一行下面** ✓
2795
+ // 点了才出现,没点就没有 —— 不跟提炼/扩词那条挤在一起 ✓
2796
+ 补标进度.在跑 && 补标进度.总 > 0
2797
+ ? (() => {
2798
+ const 百 = Math.round((补标进度.已 / 补标进度.总) * 100)
2799
+ return React.createElement(
2800
+ 'div',
2801
+ { style: Object.assign({}, S.editBox, { marginTop: 8 }) },
2802
+ React.createElement(
2803
+ 'div',
2804
+ { style: { fontSize: 13, marginBottom: 4 } },
2805
+ '⭐ ' + (补标进度.重判 ? '重判一遍' : '补标不淡') + ' 跑着呢',
2806
+ ),
2807
+ React.createElement(
2808
+ 'div',
2809
+ { style: S.barOut },
2810
+ React.createElement('div', { style: Object.assign({}, S.barIn, { width: 百 + '%' }) }),
2811
+ ),
2812
+ React.createElement(
2813
+ 'div',
2814
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 4 }) },
2815
+ '已判 ' + 补标进度.已 + ' / ' + 补标进度.总 + ' 条(' + 百 + '%)' +
2816
+ ' 还剩 ' + Math.max(0, 补标进度.总 - 补标进度.已) + ' 条' +
2817
+ (补标进度.等 ? ' ⚠ 撞限流了,正在等 ' + 补标进度.等 + ' 秒' : ' (每批 20 条,跑着就动)'),
2818
+ ),
2819
+ )
2820
+ })()
2821
+ : null,
2822
+ // ⭐⭐ 2026-09-22(她要的):**批量删的二次确认条** ✓
2823
+ // 第一次点是「批量删(N)」,出这条;第二次点「确定删」才真删 ✓
2824
+ 批删问 && 已选几条 > 0
2825
+ ? React.createElement(
2826
+ 'div',
2827
+ { style: S.confirm },
2828
+ React.createElement(
2829
+ 'div',
2830
+ { style: { flex: 1 } },
2831
+ '要删掉这 ' + 已选几条 + ' 条吗?',
2832
+ React.createElement(
2833
+ 'div',
2834
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2 }) },
2835
+ '不是销毁 —— 收进「已删」里,点开还能找回来。动之前也会自动备份。',
2836
+ ),
2837
+ ),
2838
+ React.createElement('button', { style: S.btnDanger, disabled: busy, onClick: () => 批删(s) }, busy ? '删着…' : '确定删(' + 已选几条 + ' 条)'),
2839
+ React.createElement('button', { style: S.btn, onClick: () => set批删问(false) }, '算了'),
2840
+ )
2841
+ : null,
2842
+ // ⚠⚠ 2026-09-22(我发现的):`notes` 原来**只在第一级**渲染 ——
2843
+ // 而会话详情是 `if (open) return …` **提前返回**的,
2844
+ // 所以在详情页里 setNotes 的东西**一个字都看不到** ✗
2845
+ // 「补日期」「补标不淡」「查重」的结果提示全落在这儿 → 补一份 ✓
2846
+ notes && notes.length
2847
+ ? React.createElement(
2848
+ 'div',
2849
+ { style: Object.assign({}, S.warn, { margin: '8px 0', fontSize: 12, whiteSpace: 'pre-line' }) },
2850
+ ' · ' + notes.join('\n · '),
2851
+ )
2852
+ : null,
2853
+ // ⭐⭐ 2026-09-22(她的第十件):**从会话生成档案** 的面板
2854
+ // 四步分开 —— 因为第三步**调模型、花钱**,得让人看清再点 ✓
2855
+ 生成开
2856
+ ? React.createElement(
2857
+ 'div',
2858
+ { style: Object.assign({}, S.editBox, { marginTop: 8 }) },
2859
+ [
2860
+ React.createElement(
2861
+ 'div',
2862
+ { key: 'h', style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
2863
+ React.createElement('b', null, '从会话生成档案'),
2864
+ React.createElement(
2865
+ 'span',
2866
+ { style: S.dim },
2867
+ '把会话读一遍,提炼出一份长期档案(九章)。出好的档案再走「从旧档案导入」变成记忆条目。',
2868
+ ),
2869
+ React.createElement('div', { style: { flex: 1 } }),
2870
+ React.createElement('button', { style: S.btn, onClick: () => set生成开(false) }, '收起'),
2871
+ ),
2872
+
2873
+ // ① 选会话
2874
+ React.createElement(
2875
+ 'div',
2876
+ { key: 's', style: { marginTop: 8 } },
2877
+ React.createElement('div', { style: S.dim }, '① 选会话' + (生成会话 ? '(这个工作区里找到 ' + 生成会话.length + ' 个)' : '(读着呢…)')),
2878
+ 生成会话 && 生成会话.length
2879
+ ? React.createElement(
2880
+ 'div',
2881
+ { style: { margin: '4px 0 0 8px', maxHeight: 260, overflow: 'auto' } },
2882
+ 生成会话.map((x) =>
2883
+ React.createElement(
2884
+ 'label',
2885
+ { key: x.sid, style: { display: 'block', fontSize: 12, cursor: 'pointer', padding: '2px 0' } },
2886
+ React.createElement('input', {
2887
+ type: 'checkbox',
2888
+ checked: !!生成选[x.sid],
2889
+ onChange: () => set生成选(Object.assign({}, 生成选, { [x.sid]: !生成选[x.sid] })),
2890
+ }),
2891
+ // ⚠⚠ 2026-09-22(反馈:「不显示名字」):
2892
+ // 光有 `session-a1b2c3d4-…` 这种**没法挑** —— 得显示标题 ✓
2893
+ ' ',
2894
+ React.createElement('span', { style: x.标题 ? null : S.dim }, x.标题 || '(没起名字)'),
2895
+ React.createElement(
2896
+ 'span',
2897
+ { style: Object.assign({}, S.dim, { marginLeft: 6 }) },
2898
+ (x.话数 ? x.话数 + ' 句 ' : '') + x.MB + ' MB ' + x.sid.slice(0, 12) + '…',
2899
+ ),
2900
+ ),
2901
+ ),
2902
+ )
2903
+ : 生成会话
2904
+ ? React.createElement(
2905
+ 'div',
2906
+ { style: Object.assign({}, S.dim, { marginTop: 4 }) },
2907
+ '这个工作区里没找到会话(归档的、子代理跑出来的、空壳的都不列)',
2908
+ )
2909
+ : null,
2910
+ // ⚠ 默认一个都不勾 —— 让用户自己挑(后面是要花钱的)✓
2911
+ 生成会话 && 生成会话.length
2912
+ ? React.createElement(
2913
+ 'div',
2914
+ { style: { marginTop: 4, display: 'flex', gap: 8, alignItems: 'center' } },
2915
+ React.createElement(
2916
+ 'button',
2917
+ {
2918
+ style: S.btn,
2919
+ onClick: () => {
2920
+ const 勾 = {}
2921
+ 生成会话.forEach((x) => {
2922
+ 勾[x.sid] = true
2923
+ })
2924
+ set生成选(勾)
2925
+ },
2926
+ },
2927
+ '全选',
2928
+ ),
2929
+ React.createElement('button', { style: S.btn, onClick: () => set生成选({}) }, '全不选'),
2930
+ React.createElement('span', { style: S.dim }, '勾了 ' + 勾的sid().length + ' 个'),
2931
+ )
2932
+ : null,
2933
+ // ⚠ 诊断:列表跟 DSH 对不上时,这几个数能看出卡在哪一道 ✓
2934
+ 生成诊断
2935
+ ? React.createElement(
2936
+ 'div',
2937
+ { style: Object.assign({}, S.dim, { marginTop: 4, fontSize: 11 }) },
2938
+ '文件夹里 ' + 生成诊断.文件夹里 + ' 个 DSH 名单 ' + 生成诊断.DSH名单 +
2939
+ ' 个 归档 ' + 生成诊断.归档 + ' 个 筛完剩 ' + 生成诊断.筛完剩 + ' 个',
2940
+ (生成诊断.被我筛掉的 || []).length
2941
+ ? React.createElement(
2942
+ 'div',
2943
+ { style: { marginTop: 2 } },
2944
+ '筛掉的:' +
2945
+ (生成诊断.被我筛掉的 || [])
2946
+ .map((x) => (x.标题 || x.sid) + '(' + x.因 + ')')
2947
+ .join(' '),
2948
+ )
2949
+ : null,
2950
+ )
2951
+ : null,
2952
+ React.createElement(
2953
+ 'div',
2954
+ { style: { marginTop: 6, display: 'flex', gap: 8, flexWrap: 'wrap' } },
2955
+ React.createElement(
2956
+ 'button',
2957
+ { style: S.btn, disabled: busy || 勾的sid().length === 0, onClick: () => 算生成() },
2958
+ '② 算一下要多少',
2959
+ ),
2960
+ React.createElement(
2961
+ 'span',
2962
+ { style: S.dim },
2963
+ !勾的sid().length ? '先勾几个会话' : '(这一步不调模型、不花钱;要读一遍会话、去重、切段,慢一点)',
2964
+ ),
2965
+ ),
2966
+ ),
2967
+
2968
+ // ② 计划
2969
+ 生成计划
2970
+ ? React.createElement(
2971
+ 'div',
2972
+ { key: 'p', style: { marginTop: 8 } },
2973
+ React.createElement(
2974
+ 'div',
2975
+ { style: S.dim },
2976
+ '② 算出来了:' + 生成计划.批数 + ' 批 ' + 生成计划.天数 + ' 天 ' +
2977
+ 生成计划.总字.toLocaleString() + ' 字 粗算约 ' + 生成计划.token.toLocaleString() + ' token',
2978
+ ),
2979
+ React.createElement(
2980
+ 'div',
2981
+ { style: Object.assign({}, S.dim, { marginTop: 2 }) },
2982
+ '(每批最多 ' + 生成计划.每批字上限.toLocaleString() + ' 字;挑出来「不是聊天」的 ' +
2983
+ 生成计划.挑出来 + ' 条没算进去)',
2984
+ ),
2985
+ React.createElement(
2986
+ 'div',
2987
+ { style: { marginTop: 6, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
2988
+ React.createElement(
2989
+ 'button',
2990
+ { style: S.btnDanger, disabled: busy, onClick: () => 跑生成() },
2991
+ busy ? '跑着呢…' : '③ 开始跑(会调模型、会花钱)',
2992
+ ),
2993
+ React.createElement(
2994
+ 'span',
2995
+ { style: S.dim },
2996
+ '一批一批跑,跑完一批就把条目攒起来;下一批会带上「已经记过的」,同一件事不会收两遍',
2997
+ ),
2998
+ ),
2999
+ (生成计划.会话 || []).length
3000
+ ? React.createElement(
3001
+ 'div',
3002
+ { style: Object.assign({}, S.dim, { marginTop: 4, fontSize: 12 }) },
3003
+ '会话情况:' +
3004
+ 生成计划.会话
3005
+ .map((x) => (x.标题 || x.sid.slice(0, 12)) + '(独有 ' + x.独有 + '/副本 ' + x.副本 + ')')
3006
+ .join(' '),
3007
+ )
3008
+ : null,
3009
+ )
3010
+ : null,
3011
+
3012
+ // ③ 进度
3013
+ 生成进度 && 生成进度.在跑
3014
+ ? (() => {
3015
+ const 百 = Math.round(((生成进度.批号 - 1) / 生成进度.批数) * 100)
3016
+ return React.createElement(
3017
+ 'div',
3018
+ { key: 'bar', style: { marginTop: 8 } },
3019
+ React.createElement(
3020
+ 'div',
3021
+ { style: S.barOut },
3022
+ React.createElement('div', { style: Object.assign({}, S.barIn, { width: 百 + '%' }) }),
3023
+ ),
3024
+ React.createElement(
3025
+ 'div',
3026
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 4 }) },
3027
+ '第 ' + 生成进度.批号 + ' / ' + 生成进度.批数 + ' 批(' + 百 + '%)' +
3028
+ ' 已攒 ' + 生成条目.length + ' 条' +
3029
+ (生成进度.模型 ? ' ' + 生成进度.模型 : ''),
3030
+ ),
3031
+ )
3032
+ })()
3033
+ : null,
3034
+
3035
+ // ④ 存到哪儿 —— ⚠⚠ 2026-09-22(反馈:「选了文件夹点了过后
3036
+ // 再想换的话就会卡住,就没有那个选项了,就不能再修改了」)
3037
+ // 两个毛病:
3038
+ // ① 这块原来在 `生成条目.length` 里面 —— **跑完才出现** ✗
3039
+ // → 挪到外面,面板一开就能设 ✓
3040
+ // ② 再点「选文件夹」时**不重新读目录**(`if (!生成目录列)`)——
3041
+ // 看到的还是上次那份,像卡住 ✗ → 每次打开都重读 ✓
3042
+ React.createElement(
3043
+ 'div',
3044
+ { key: 'save', style: { marginTop: 8 } },
3045
+ React.createElement(
3046
+ 'div',
3047
+ { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
3048
+ React.createElement('b', null, '④ 存到哪儿'),
3049
+ React.createElement('span', { style: S.dim }, '跑完就存到这个文件 —— 能直接改,也能点「选文件夹」挑'),
3050
+ ),
3051
+ React.createElement(
3052
+ 'div',
3053
+ { style: Object.assign({}, S.nameBox, { marginTop: 6 }) },
3054
+ React.createElement('div', { style: { flex: 'none', width: 72 } }, '存到'),
3055
+ React.createElement('input', {
3056
+ style: Object.assign({}, S.inp, { flex: 1, width: 'auto', minWidth: 140 }),
3057
+ value: 生成存到,
3058
+ placeholder: '要存的完整路径(比如 D:\\档案\\生成的档案.md)',
3059
+ onChange: (e) => set生成存到(e.target.value),
3060
+ }),
3061
+ React.createElement(
3062
+ 'button',
3063
+ {
3064
+ style: S.btn,
3065
+ onClick: () => {
3066
+ const 要开 = !生成选目录
3067
+ set生成选目录(要开)
3068
+ // ⚠ 每次打开都**重读一遍**(原来只在第一次读,再打开看到的是旧的)
3069
+ if (要开) {
3070
+ // 从当前路径所在的目录开始(拿不到就从盘符列表开始)
3071
+ const 起点 = /[\\/]/.test(生成存到) ? 生成存到.replace(/[\\/][^\\/]*$/, '') : ''
3072
+ post('/dirs', { path: 起点 })
3073
+ .then((r) => set生成目录列(r))
3074
+ .catch((e) => setNotes(['列不了目录:' + ((e && e.message) || e)]))
3075
+ }
3076
+ },
3077
+ },
3078
+ 生成选目录 ? '收起' : '选文件夹',
3079
+ ),
3080
+ ),
3081
+ // 文件夹选择器(跟「从旧档案导入」那个是同一个接口)
3082
+ 生成选目录 && 生成目录列
3083
+ ? React.createElement(
3084
+ 'div',
3085
+ { style: { margin: '4px 0 0', padding: '6px 10px', borderRadius: 6, background: 'rgba(128,128,128,.10)', border: '1px solid rgba(128,128,128,.22)', fontSize: 12 } },
3086
+ React.createElement(
3087
+ 'div',
3088
+ { style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 4 } },
3089
+ React.createElement('span', { style: S.dim }, 生成目录列.path ? '当前:' + 生成目录列.path : '选一个盘'),
3090
+ React.createElement('div', { style: { flex: 1 } }),
3091
+ // ⚠ 选择器里也放一个「收起」—— 别只能靠上面那个按钮
3092
+ React.createElement('button', { style: S.btn, onClick: () => set生成选目录(false) }, '收起'),
3093
+ ),
3094
+ 生成目录列.ok === false
3095
+ ? React.createElement('div', { style: S.drop }, '✗ ' + (生成目录列.message || '列不了'))
3096
+ : React.createElement(
3097
+ 'div',
3098
+ null,
3099
+ React.createElement(
3100
+ 'div',
3101
+ { style: Object.assign({}, S.dim, { marginBottom: 4 }) },
3102
+ 生成目录列.path
3103
+ ? '「' + 生成目录列.path + '」下的文件夹(点一下就进去):'
3104
+ : '选一个盘:',
3105
+ ),
3106
+ React.createElement(
3107
+ 'div',
3108
+ { style: { display: 'flex', gap: 10, flexWrap: 'wrap' } },
3109
+ 生成目录列.parent
3110
+ ? React.createElement(
3111
+ 'span',
3112
+ {
3113
+ style: S.link,
3114
+ onClick: () =>
3115
+ post('/dirs', { path: 生成目录列.parent }).then((r) => set生成目录列(r)).catch(() => {}),
3116
+ },
3117
+ '↑ 上一级',
3118
+ )
3119
+ : null,
3120
+ (生成目录列.dirs || []).map((d) =>
3121
+ React.createElement(
3122
+ 'span',
3123
+ {
3124
+ key: d.path,
3125
+ style: S.link,
3126
+ onClick: () =>
3127
+ post('/dirs', { path: d.path }).then((r) => set生成目录列(r)).catch(() => {}),
3128
+ },
3129
+ '📁 ' + d.name,
3130
+ ),
3131
+ ),
3132
+ ),
3133
+ // 「就用这个文件夹」—— 把路径拼成 <文件夹>\生成的档案.md
3134
+ 生成目录列.path
3135
+ ? React.createElement(
3136
+ 'div',
3137
+ { style: { marginTop: 6 } },
3138
+ React.createElement(
3139
+ 'button',
3140
+ {
3141
+ style: S.btn,
3142
+ onClick: () => {
3143
+ const p = String(生成目录列.path).replace(/[\\/]+$/, '')
3144
+ set生成存到(p + '\\生成的档案.md')
3145
+ set生成选目录(false)
3146
+ },
3147
+ },
3148
+ '就用这个文件夹',
3149
+ ),
3150
+ React.createElement(
3151
+ 'span',
3152
+ { style: Object.assign({}, S.dim, { marginLeft: 8 }) },
3153
+ '→ 存成「生成的档案.md」(也可以先点进去子文件夹再点这个)',
3154
+ ),
3155
+ )
3156
+ : React.createElement(
3157
+ 'div',
3158
+ { style: Object.assign({}, S.dim, { marginTop: 6 }) },
3159
+ '先点进一个盘,再点「就用这个文件夹」',
3160
+ ),
3161
+ ),
3162
+ )
3163
+ : null,
3164
+ ),
3165
+
3166
+ // 攒下来的条目 + 存文件
3167
+ // ⚠ 2026-10-26:原来只在「条目 > 0」时渲染 —— 0 条时界面上只留一句
3168
+ // 「点『存成档案文件』」,按钮根本不存在,用户只能到处找 ✗
3169
+ // → 算过计划就画出来;0 条时按钮置灰、旁边写明为什么。
3170
+ 生成计划
3171
+ ? React.createElement(
3172
+ 'div',
3173
+ { key: 'r', style: { marginTop: 8 } },
3174
+ React.createElement(
3175
+ 'div',
3176
+ { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
3177
+ React.createElement('b', null, '攒了 ' + 生成条目.length + ' 条'),
3178
+ React.createElement(
3179
+ 'button',
3180
+ {
3181
+ style: S.btn,
3182
+ disabled: busy || !!(生成进度 && 生成进度.在跑) || !生成条目.length,
3183
+ onClick: () => 存生成(),
3184
+ },
3185
+ '存成档案文件',
3186
+ ),
3187
+ React.createElement(
3188
+ 'span',
3189
+ { style: S.dim },
3190
+ 生成条目.length
3191
+ ? '存成一份 markdown;然后再用下面的「从旧档案导入」把它变成记忆条目'
3192
+ : '还没有条目可存 —— 这一轮模型一条都没收(也可能输出被截断,诊断日志里有回文长度)',
3193
+ ),
3194
+ ),
3195
+ 生成条目.length
3196
+ ? React.createElement(
3197
+ 'div',
3198
+ { style: Object.assign({}, S.dim, { marginTop: 4, maxHeight: 200, overflow: 'auto', fontSize: 12, whiteSpace: 'pre-line' }) },
3199
+ 生成条目.slice(0, 40).map((x) => x.章 + '|' + x.日期 + '|' + x.话).join('\n') +
3200
+ (生成条目.length > 40 ? '\n…(还有 ' + (生成条目.length - 40) + ' 条)' : ''),
3201
+ )
3202
+ : null,
3203
+ )
3204
+ : null,
3205
+
3206
+ // 存好了
3207
+ 生成档
3208
+ ? React.createElement(
3209
+ 'div',
3210
+ { key: 'ok', style: { marginTop: 8 } },
3211
+ React.createElement('div', null, '✓ 档案存到:' + 生成档.存到),
3212
+ React.createElement(
3213
+ 'div',
3214
+ { style: Object.assign({}, S.dim, { marginTop: 2 }) },
3215
+ '共 ' + 生成档.条数 + ' 条' + (生成档.去掉重复 ? '(去掉了 ' + 生成档.去掉重复 + ' 条重复)' : ''),
3216
+ ),
3217
+ )
3218
+ : null,
3219
+ ],
3220
+ )
3221
+ : null,
3222
+ 查重 && 查重开
3223
+ ? React.createElement(
3224
+ 'div', { style: Object.assign({}, S.editBox, { marginTop: 8 }) },
3225
+ [
3226
+ React.createElement(
3227
+ 'div',
3228
+ { key: '头', style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
3229
+ React.createElement('b', null, '查重:'),
3230
+ React.createElement('span', null, (查重.组 || []).length + ' 组、能少 ' + 查重.能少 + ' 条'),
3231
+ React.createElement(
3232
+ 'span',
3233
+ { style: S.dim },
3234
+ '(真重复 ' + 查重.真重复组数 + ' 组 / 汇总+明细 ' + 查重.汇总组数 + ' 组)',
3235
+ ),
3236
+ React.createElement(
3237
+ 'button',
3238
+ { style: S.btn, disabled: busy || !查重.真重复组数, onClick: () => 合重复(s, true) },
3239
+ '一键合真重复(' + 查重.真重复组数 + ' 组)',
3240
+ ),
3241
+ React.createElement(
3242
+ 'button',
3243
+ {
3244
+ style: S.btn,
3245
+ disabled: busy || !(查重.组 || []).some((g, i) => 查重合[i]),
3246
+ onClick: () => 合重复(s, false),
3247
+ },
3248
+ '按我勾的合(' + (查重.组 || []).filter((g, i) => 查重合[i]).length + ' 组、少 ' +
3249
+ (查重.组 || []).reduce((a, g, i) => a + (查重合[i] ? g.条们.length - 1 : 0), 0) + ' 条)',
3250
+ ),
3251
+ React.createElement(
3252
+ 'button',
3253
+ {
3254
+ style: S.btn,
3255
+ title: '把「汇总+明细」那些也勾上。⚠ 一长一短,合了会丢短的里的细节',
3256
+ onClick: () => {
3257
+ const 全 = {}
3258
+ ;(查重.组 || []).forEach((g, i) => {
3259
+ 全[i] = true
3260
+ })
3261
+ set查重合(全)
3262
+ },
3263
+ },
3264
+ '把「汇总+明细」也全勾上',
3265
+ ),
3266
+ React.createElement('button', { style: S.btn, onClick: () => { set查重(null); set查重开(false) } }, '收起'),
3267
+ ),
3268
+ React.createElement(
3269
+ 'div',
3270
+ { key: '说明', style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 4 }) },
3271
+ '「真重复」= 两条长度差不多,合了安全,默认勾上;' +
3272
+ '「汇总+明细」= 一长一短(长的把短的包住了),默认不勾 —— ' +
3273
+ '长的那条未必真的包含了短的每个细节,合了会丢东西。要合哪一组,自己勾那一组。' +
3274
+ '合掉的不销毁,收在「已删」里。',
3275
+ ),
3276
+ ]
3277
+ .concat(
3278
+ (查重.组 || []).slice(0, 60).map((g, i) =>
3279
+ React.createElement(
3280
+ 'div',
3281
+ {
3282
+ key: 'g' + i,
3283
+ style: { marginTop: 10, paddingTop: 8, borderTop: '1px solid rgba(128,128,128,.18)' },
3284
+ },
3285
+ [
3286
+ React.createElement(
3287
+ 'div',
3288
+ {
3289
+ key: 'h',
3290
+ style: { fontSize: 12, marginBottom: 4, display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' },
3291
+ },
3292
+ // ⭐⭐ 2026-09-23:每组一个「要合」的勾 —— 不勾的组「按我勾的合」不会碰 ✓
3293
+ React.createElement('input', {
3294
+ key: 'c',
3295
+ type: 'checkbox',
3296
+ checked: !!查重合[i],
3297
+ title: '勾上它,这一组才会被「按我勾的合」合掉',
3298
+ onChange: () => set查重合(Object.assign({}, 查重合, { [i]: !查重合[i] })),
3299
+ }),
3300
+ '第 ' + (i + 1) + ' 组 · ' + g.条们.length + ' 条 · ',
3301
+ React.createElement('b', { style: g.类型 === '真重复' ? S.ok : S.warn }, g.类型),
3302
+ 查重合[i]
3303
+ ? null
3304
+ : React.createElement(
3305
+ 'span',
3306
+ { style: Object.assign({}, S.dim, { fontSize: 11 }) },
3307
+ '(不参与)',
3308
+ ),
3309
+ ),
3310
+ ].concat(
3311
+ g.条们.map((t) =>
3312
+ React.createElement(
3313
+ 'label',
3314
+ {
3315
+ key: t.id,
3316
+ style: { display: 'flex', gap: 6, alignItems: 'flex-start', padding: '3px 0', cursor: 'pointer' },
3317
+ },
3318
+ [
3319
+ React.createElement('input', {
3320
+ key: 'r',
3321
+ type: 'radio',
3322
+ name: '查重组' + i,
3323
+ checked: 查重选[i] === t.id,
3324
+ onChange: () => set查重选(Object.assign({}, 查重选, { [i]: t.id })),
3325
+ }),
3326
+ React.createElement(
3327
+ 'span',
3328
+ { key: 't', style: { flex: 1, minWidth: 0, overflowWrap: 'anywhere' } },
3329
+ t.text,
3330
+ ),
3331
+ React.createElement(
3332
+ 'span',
3333
+ { key: 'n', style: Object.assign({}, S.dim, { fontSize: 11, whiteSpace: 'nowrap' }) },
3334
+ t.长 + ' 字',
3335
+ ),
3336
+ ],
3337
+ ),
3338
+ ),
3339
+ ),
3340
+ ),
3341
+ ),
3342
+ )
3343
+ .concat(
3344
+ (查重.组 || []).length > 60
3345
+ ? [
3346
+ React.createElement(
3347
+ 'div',
3348
+ { key: '更多', style: Object.assign({}, S.dim, { marginTop: 8 }) },
3349
+ '(只画了前 60 组,先合这些再点一次查重)',
3350
+ ),
3351
+ ]
3352
+ : [],
3353
+ ),
3354
+ )
3355
+ : null,
3356
+ confirm && confirm.kind === 'backfill'
3357
+ ? React.createElement(
3358
+ 'div',
3359
+ { style: S.confirm },
3360
+ React.createElement('span', { style: { flex: 1 } }, '从正文里补日期?只补那 ' + 没日期的 + ' 条没日期的,已有的不动(补之前会自动备份)。'),
3361
+ React.createElement('button', { style: S.btn, onClick: 补日期 }, '补'),
3362
+ React.createElement('button', { style: S.btn, onClick: () => setConfirm(null) }, '算了'),
3363
+ )
3364
+ : null,
3365
+ 搜词 || 看哪边 !== '全部'
3366
+ ? React.createElement(
3367
+ 'div',
3368
+ { style: Object.assign({}, S.dim, { fontSize: 12, margin: '0 0 6px' }) },
3369
+ '这里 ' + 过滤后.length + ' 条 / 共 ' + s.memories.length + ' 条' +
3370
+ (看哪边 === '有时间' ? '(只看记了时间的)' : 看哪边 === '没时间' ? '(只看没记时间的)' : '') +
3371
+ (过滤后.length === 0 ? ' —— 换几个字试试,或者点上面的「看」换个范围' : ''),
3372
+ )
3373
+ : null,
3374
+ React.createElement(
3375
+ 'div',
3376
+ { style: { marginTop: 10 } },
3377
+ 过滤后.length === 0
3378
+ ? React.createElement(
3379
+ 'div',
3380
+ { style: S.dim },
3381
+ 搜词 ? '(没有含「' + 搜词 + '」的)' : '(这个范围里没有记忆)',
3382
+ )
3383
+ : (() => {
3384
+ // ⭐⭐ 2026-09-22(她要的):**「全部」视图里,不会淡的收进一个折叠的分组** ✓
3385
+ // 原话:「那一行,我们是单独做一个收理的文件夹一样的,
3386
+ // 不然的话那些就不需要露在外面」
3387
+ // ⭐ 又说:「**外面都是会淡的,里面才是不会淡的**,就不用管里面了」
3388
+ // → 外面只列**会淡的**;不会淡的(长期事实 + 没时间的)
3389
+ // 塞进「▸ 不会淡的(N 条)」里,点开才看 ✓
3390
+ const 上 = 画这个.slice(0, 画全部 ? 画这个.length : 一次最多画)
3391
+ // ⚠ 搜的时候**不许折叠** —— 不然搜到了却看不见 ✗
3392
+ // (自检逮到的:搜「衣服」时匹配的那条是长期事实,被收进分组里了)
3393
+ if (看哪边 !== '全部' || 搜词) return 上.map(一条记忆)
3394
+ const 会淡的 = 上.filter((m) => !不会淡(m))
3395
+ const 不淡的 = 上.filter(不会淡)
3396
+ return [
3397
+ ...会淡的.map(一条记忆),
3398
+ 不淡的.length
3399
+ ? React.createElement(
3400
+ 'div',
3401
+ { key: '事实组' },
3402
+ React.createElement(
3403
+ 'div',
3404
+ {
3405
+ style: Object.assign({}, S.h, { cursor: 'pointer', marginTop: 14 }),
3406
+ onClick: () => set看事实(!看事实),
3407
+ },
3408
+ (看事实 ? '▾ ' : '▸ ') + '不会淡的(' + 不淡的.length + ' 条)',
3409
+ ),
3410
+ React.createElement(
3411
+ 'div',
3412
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginBottom: 4 }) },
3413
+ '里面这些是长期事实(要记一辈子的事,不会淡)。' +
3414
+ '外面那些都会淡 —— 包括没记时间的(它们从「记下来的那天」开始淡)。',
3415
+ ),
3416
+ 看事实 ? 不淡的.map(一条记忆) : null,
3417
+ )
3418
+ : null,
3419
+ ]
3420
+ })(),
3421
+ ),
3422
+ 过滤后.length > 一次最多画
3423
+ ? React.createElement(
3424
+ 'div',
3425
+ { style: { margin: '6px 0 0', fontSize: 12 } },
3426
+ 画全部
3427
+ ? React.createElement('span', { style: S.link, onClick: () => set画全部(false) }, '↑ 收起来(只画前 ' + 一次最多画 + ' 条)')
3428
+ : React.createElement(
3429
+ 'span',
3430
+ { style: S.link, onClick: () => set画全部(true) },
3431
+ '还有 ' + (过滤后.length - 一次最多画) + ' 条' +
3432
+ (看哪边 === '没时间' ? '(更早记的)' : 顺序 === '新' ? '(更早的)' : '(更新的)') +
3433
+ '没画出来 —— 点这里全部显示(多了会卡)',
3434
+ ),
3435
+ )
3436
+ : null,
3437
+ )
3438
+ : null,
3439
+ React.createElement(
3440
+ 'div',
3441
+ null,
3442
+ React.createElement('div', { style: Object.assign({}, S.h, { cursor: 'pointer' }), onClick: () => setShowDeleted(!showDeleted) }, (showDeleted ? '▾ ' : '▸ ') + '已删掉的(' + (已删.length + 被取代的.length) + ')'),
3443
+ React.createElement('div', { style: S.dim }, '收起来的记忆不会被注入,但还留着,随时能找回来。'),
3444
+ showDeleted
3445
+ ? React.createElement(
3446
+ 'div',
3447
+ { style: { marginTop: 6 } },
3448
+ // ⭐⭐ 2026-09-22(反馈「你放在最下面的呀,你改到最上面去,
3449
+ // 谁那么多条?如果有100条,我得翻到最下面才能全选吗?」):
3450
+ // 工具条原来在**列表最下面** —— 68 条就得翻到底才能点「全选」✗
3451
+ // → 挪到**最上面**,跟标题挨着 ✓
3452
+ // ⭐⭐ 2026-09-23(反馈「点『清空』之后提示在最下面,看都看不到」):
3453
+ // 确认条原来渲染在**列表下面** —— 68 条就得翻到底才看得见那两句问话 ✗
3454
+ // (跟 0.9.7 修过的「工具条在最下面」是同一个毛病)
3455
+ // → 改成**就地替换工具条**:两样都在最上面,点了「清空」,
3456
+ // 工具条那一格立刻变成确认条,不用滚 ✓
3457
+ 已删.length > 0 && confirm && confirm.kind === 'purgeall'
3458
+ ? 确认条('把收着的 ' + 已删.length + ' 条全清掉?找不回来了。', '全清掉', 'purgeall', '*')
3459
+ : null,
3460
+ // ⭐⭐ 2026-09-23(反馈「这里没有批量找回呀」):批量找回的确认
3461
+ // ⚠ 这个**不是销毁**(是往回捞),所以文案跟「清掉」正好相反
3462
+ 已删.length > 0 && !(confirm && confirm.kind === 'purgeall') && 批回问
3463
+ ? React.createElement(
3464
+ 'div',
3465
+ { style: S.confirm },
3466
+ React.createElement(
3467
+ 'div',
3468
+ { style: { flex: 1 } },
3469
+ 批回问.只合并
3470
+ ? '把「合并掉的」那 ' + 已删.filter((m) => m.合并掉的 === true).length + ' 条全捞回来?'
3471
+ : '把勾上的这 ' + 已选清几条 + ' 条捞回来?',
3472
+ React.createElement(
3473
+ 'div',
3474
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2 }) },
3475
+ '捞回来之后它们又会被注入(跟原来一样)。不是销毁,随时能再删。',
3476
+ ),
3477
+ ),
3478
+ React.createElement('button', { style: S.btn, disabled: busy, onClick: () => 批回(s, 批回问.只合并) }, busy ? '捞着…' : '确定找回'),
3479
+ React.createElement('button', { style: S.btn, onClick: () => set批回问(false) }, '算了'),
3480
+ )
3481
+ : null,
3482
+ 已删.length > 0 && !(confirm && confirm.kind === 'purgeall') && !批回问 && 批清问 && 已选清几条 > 0
3483
+ ? React.createElement(
3484
+ 'div',
3485
+ { style: S.confirm },
3486
+ React.createElement(
3487
+ 'div',
3488
+ { style: { flex: 1 } },
3489
+ '要彻底清掉这 ' + 已选清几条 + ' 条吗?',
3490
+ React.createElement(
3491
+ 'div',
3492
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2 }) },
3493
+ '清掉就找不回来了(备份目录里可能还有一份)。想留就用「找回来」。',
3494
+ ),
3495
+ ),
3496
+ React.createElement('button', { style: S.btnDanger, disabled: busy, onClick: () => 批清(s) }, busy ? '清着…' : '确定清掉(' + 已选清几条 + ' 条)'),
3497
+ React.createElement('button', { style: S.btn, onClick: () => set批清问(false) }, '算了'),
3498
+ )
3499
+ : null,
3500
+ 已删.length > 0 && !(confirm && confirm.kind === 'purgeall') && !批回问 && !(批清问 && 已选清几条 > 0)
3501
+ ? React.createElement(
3502
+ 'div',
3503
+ { style: { margin: '8px 0', display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
3504
+ // ⭐ 一键全选 ✓
3505
+ React.createElement(
3506
+ 'button',
3507
+ {
3508
+ style: S.btn,
3509
+ title: '把这一栏里的全勾上(再点一下取消)',
3510
+ onClick: () => {
3511
+ if (已选清几条 === 已删.length) {
3512
+ set选中清({})
3513
+ set批清问(false)
3514
+ return
3515
+ }
3516
+ const 全 = {}
3517
+ for (const m of 已删) 全[m.id] = true
3518
+ set选中清(全)
3519
+ },
3520
+ },
3521
+ 已选清几条 === 已删.length && 已删.length > 0 ? '取消全选' : '全选(' + 已删.length + ')',
3522
+ ),
3523
+ // ⭐ 批量清掉(勾了几条就显示几条)—— 点了**不直接清**,先问一遍 ✓
3524
+ 已选清几条 > 0
3525
+ ? React.createElement(
3526
+ 'button',
3527
+ { style: S.btnDanger, onClick: () => set批清问(true) },
3528
+ '批量清掉(' + 已选清几条 + ')',
3529
+ )
3530
+ : null,
3531
+ // ⭐⭐ 2026-09-23(反馈「这里没有批量找回呀」):**批量找回** ✓
3532
+ 已选清几条 > 0
3533
+ ? React.createElement(
3534
+ 'button',
3535
+ {
3536
+ style: S.btn,
3537
+ title: '把勾上的这几条从「已删」里捞回来(不是销毁,随时能再删)',
3538
+ onClick: () => set批回问({ 只合并: false }),
3539
+ },
3540
+ '批量找回(' + 已选清几条 + ')',
3541
+ )
3542
+ : null,
3543
+ // ⭐ 一键撤销整次合并:把「合并掉的」那批全捞回来 ✓
3544
+ 已删.filter((m) => m.合并掉的 === true).length > 0
3545
+ ? React.createElement(
3546
+ 'button',
3547
+ {
3548
+ style: S.btn,
3549
+ title: '把「合并掉的」那批全捞回来 —— 等于撤销整次合并',
3550
+ onClick: () => set批回问({ 只合并: true }),
3551
+ },
3552
+ '撤销合并:全找回(' + 已删.filter((m) => m.合并掉的 === true).length + ')',
3553
+ )
3554
+ : null,
3555
+ 已选清几条 > 0
3556
+ ? React.createElement(
3557
+ 'button',
3558
+ { style: S.btn, onClick: () => { set选中清({}); set批清问(false); set批回问(false) } },
3559
+ '取消勾选',
3560
+ )
3561
+ : null,
3562
+ React.createElement('button', { style: S.btn, onClick: () => setConfirm({ id: '*', kind: 'purgeall' }) }, '把「已删掉的」清空'),
3563
+ )
3564
+ : null,
3565
+ 已删.length === 0
3566
+ ? React.createElement('div', { style: S.dim }, '(空的)')
3567
+ : 已删.map((m) =>
3568
+ React.createElement(
3569
+ 'div',
3570
+ { key: m.id, style: S.条 },
3571
+ React.createElement(
3572
+ 'div',
3573
+ { style: S.row },
3574
+ // ⭐ 批量清掉:行首一个勾选框(跟上面那栏一样)
3575
+ React.createElement('input', {
3576
+ type: 'checkbox',
3577
+ checked: !!选中清[m.id],
3578
+ title: '勾上它,再点上面的「批量清掉」',
3579
+ onChange: () => set选中清(Object.assign({}, 选中清, { [m.id]: !选中清[m.id] })),
3580
+ }),
3581
+ React.createElement('div', { style: Object.assign({}, S.dim, { width: 108, flex: 'none' }) }, '收于 ' + rel(m.deletedAt, now)),
3582
+ React.createElement('div', { style: Object.assign({}, S.dim, { flex: 1, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }) }, m.text),
3583
+ React.createElement('div', { style: Object.assign({}, S.link, { flex: 'none', fontSize: 12, marginRight: 10 }), onClick: () => 删(m.id, 'restore') }, '找回来'),
3584
+ React.createElement('div', { style: Object.assign({}, S.drop, { flex: 'none', fontSize: 12, cursor: 'pointer' }), onClick: () => setConfirm({ id: m.id, kind: 'purge' }) }, '彻底清掉'),
3585
+ ),
3586
+ confirm && confirm.id === m.id && confirm.kind === 'purge'
3587
+ ? 确认条('彻底清掉就找不回来了(备份里可能还有)。', '确认清掉', 'purge', m.id)
3588
+ : null,
3589
+ ),
3590
+ ),
3591
+ // ⭐⭐ 2026-09-24(她说的「那已被取代应该回回收站呀」):
3592
+ // **被取代的**从主列表挪出来,收在回收站里(就在这一栏下面)✓
3593
+ // ⚠ 放在 `showDeleted ?` **里面** —— 跟回收站一起折叠 ✓
3594
+ // (放外面的话,回收站收起来了它们还杵在那儿,看着像两栏)
3595
+ //
3596
+ // ⚠⚠ 它们**不在 `s.deleted` 里** —— 所以上面那排
3597
+ // 「全选 / 批量清掉 / 把已删掉的清空」**碰不到它们**,
3598
+ // 这是故意的:被取代是自动标的(可能几百条),
3599
+ // 不该被「清空」连坐销毁(丢 157 条那次的教训)✗✗
3600
+ 被取代的.length
3601
+ ? React.createElement(
3602
+ 'div',
3603
+ null,
3604
+ React.createElement(
3605
+ 'div',
3606
+ { style: Object.assign({}, S.h, { marginTop: 16 }) },
3607
+ '被取代的(' + 被取代的.length + ')',
3608
+ ),
3609
+ React.createElement(
3610
+ 'div',
3611
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginBottom: 4 }) },
3612
+ '这些是「讲的是同一件事」里比较旧的那条,自动收起来的 —— 不再被注入,但东西还留着。',
3613
+ ),
3614
+ React.createElement(
3615
+ 'div',
3616
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginBottom: 4 }) },
3617
+ '⚠ 「清空」不会清掉它们(只有你亲手删的那些才会被清掉)。想让它回来就点「恢复」。',
3618
+ ),
3619
+ 被取代的.map((m) =>
3620
+ React.createElement(
3621
+ 'div',
3622
+ { key: m.id, style: S.条 },
3623
+ React.createElement(
3624
+ 'div',
3625
+ { style: S.row },
3626
+ React.createElement(
3627
+ 'div',
3628
+ { style: Object.assign({}, S.dim, { width: 108, flex: 'none' }) },
3629
+ m.已被取代 && m.已被取代.作废于 ? '收于 ' + rel(m.已被取代.作废于, now) : '收起来了',
3630
+ ),
3631
+ React.createElement(
3632
+ 'div',
3633
+ { style: { flex: '1 1 260px', minWidth: 200, overflowWrap: 'anywhere', wordBreak: 'break-word' } },
3634
+ React.createElement(
3635
+ 'span',
3636
+ { style: Object.assign({}, S.dim, { textDecoration: 'line-through', opacity: 0.62 }) },
3637
+ m.text,
3638
+ ),
3639
+ ),
3640
+ React.createElement(
3641
+ 'div',
3642
+ {
3643
+ style: Object.assign({}, S.link, { flex: 'none', fontSize: 12 }),
3644
+ title: '让它回来 —— 又能被想起来了(标错了 / 状态又变回来了)',
3645
+ onClick: () => 撤销作废(m.id),
3646
+ },
3647
+ '恢复',
3648
+ ),
3649
+ ),
3650
+ m.已被取代 && m.已被取代.为什么
3651
+ ? React.createElement(
3652
+ 'div',
3653
+ {
3654
+ style: Object.assign({}, S.dim, {
3655
+ margin: '0 0 6px 104px',
3656
+ fontSize: 11,
3657
+ overflowWrap: 'anywhere',
3658
+ wordBreak: 'break-word',
3659
+ }),
3660
+ },
3661
+ '为什么收起来:' + m.已被取代.为什么,
3662
+ )
3663
+ : null,
3664
+ ),
3665
+ ),
3666
+ )
3667
+ : null,
3668
+ // ⚠ 工具条和两个确认条**都在上面**了(见前面那段)——
3669
+ // 原来「批量清掉」和「清空」的确认条渲染在这儿(列表最下面),
3670
+ // 条数一多就看不见 ✗ → 挪走了,这里不再重复 ✓
3671
+ )
3672
+ : null,
3673
+ ),
3674
+ // ⚠ 2026-09-22:**「备份」原来插在这里**(在「扩词」「导入」前面),
3675
+ // 可它后面跟的却是那两个区块 —— 看着像它俩的标题,其实只是句说明 ✗
3676
+ // (「设置页面会不会有点乱」)
3677
+ // → 挪到**末尾**,跟「这一页到底会怎么变」那些说明放一起 ✓
3678
+ React.createElement('div', null, 扩词区()),
3679
+ React.createElement(
3680
+ 'div',
3681
+ { style: Object.assign({}, S.h, { cursor: 'pointer' }), onClick: () => setImpOpen(!impOpen) },
3682
+ (impOpen ? '▾ ' : '▸ ') + '从旧档案导入',
3683
+ ),
3684
+ React.createElement('div', { style: S.dim }, '把一个文件夹里的档案拆成一条条记忆,你挑完再导。文件夹在下面选。'),
3685
+ impOpen ? React.createElement('div', null, 导入区()) : null,
3686
+ tr
3687
+ ? React.createElement(
3688
+ 'div',
3689
+ null,
3690
+ React.createElement('div', { style: S.h }, '最近一次召回'),
3691
+ React.createElement('div', { style: S.dim }, '线索:「' + String(tr.query || '').slice(0, 50) + '」'),
3692
+ (() => {
3693
+ // ⚠ 按分数排再显示 —— 老记录是按**入库顺序**存的,
3694
+ // 直接取前 12 条全是 0 分的老条目,看不出到底挑中了什么 ✗
3695
+ // (反馈:「这里为什么只有这些,该显示最近标中的」)
3696
+ const 排好 = (tr.rows || []).slice().sort((a, b) => (b.P || 0) - (a.P || 0) || (b.C || 0) - (a.C || 0))
3697
+ const 进去的 = 排好.filter((r) => !r.omission)
3698
+ return React.createElement(
3699
+ 'div',
3700
+ null,
3701
+ React.createElement(
3702
+ 'div',
3703
+ { style: Object.assign({}, S.dim, { fontSize: 12, margin: '2px 0 4px' }) },
3704
+ 进去的.length
3705
+ ? '✓ 挑中并塞进去了 ' + 进去的.length + ' 条(排在最前面)'
3706
+ : '这次一条都没挑中(线索跟记忆对不上)',
3707
+ ),
3708
+ 排好.slice(0, 20).map((r, i) => {
3709
+ const mm = s.memories.find((x) => x.id === r.id) || (s.deleted || []).find((x) => x.id === r.id)
3710
+ const why = r.omission
3711
+ ? { hard_gate: '没命中线索', below_threshold: '分数不够', limit: '超过条数上限', budget: '超过字数上限', duplicate: '跟另一条重复', fact_cap: '长期事实占满了名额' }[r.omission] || r.omission
3712
+ : { assembled: '进去了', consumed: '像是用上了', invalidated: '已失效' }[r.status] || '进去了'
3713
+ const 色 = r.omission
3714
+ ? S.drop
3715
+ : r.status === 'consumed'
3716
+ ? S.ok
3717
+ : S.dim
3718
+ return React.createElement(
3719
+ 'div',
3720
+ { key: i, style: Object.assign({}, S.row, S.条) },
3721
+ React.createElement(
3722
+ 'div',
3723
+ { style: { flex: 1 } },
3724
+ (r.omission ? ' ' : '✓ ') + String((mm && mm.text) || r.id).slice(0, 60),
3725
+ ),
3726
+ React.createElement('div', { style: Object.assign({}, S.dim, { width: 54 }) }, 'C ' + r.C),
3727
+ React.createElement('div', { style: Object.assign({}, S.dim, { width: 54 }) }, 'P ' + r.P),
3728
+ React.createElement('div', { style: Object.assign({}, 色, { width: 96 }) }, why + (r.timeHit ? ' ⭐' : '')),
3729
+ )
3730
+ }),
3731
+ )
3732
+ })(),
3733
+ React.createElement(
3734
+ 'div',
3735
+ { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 6 }) },
3736
+ '「进去了」= 这一轮塞进提示词了;「像是用上了」= 她回复里的实词跟这条重合得多(这是猜的);「已失效」= 这条后来被删了或改过。',
3737
+ ),
3738
+ )
3739
+ : null,
3740
+ // ⭐ 2026-09-22:说明都放末尾(原来「备份」插在中间,看着像别的区块的标题)
3741
+ React.createElement('div', { style: S.h }, '备份'),
3742
+ React.createElement(
3743
+ 'div',
3744
+ { style: S.dim },
3745
+ '记忆一变就会自动备份:上一版存在同目录的 .bak,另外每天存一份,留 14 天。' +
3746
+ '「补日期」「补标不淡」「改」「删」之前也都会先备一份。',
3747
+ ),
3748
+ )
3749
+ }
3750
+
3751
+ // ---- 第一级:会话列表 + 参数 ----
3752
+ const spec = (d.spec || []).slice()
3753
+ const byKey = {}
3754
+ for (const s of spec) byKey[s.key] = s
3755
+ const WK = spec.filter((s) => s.group === 'weight').map((s) => s.key)
3756
+
3757
+ const setOne = (k, native) => {
3758
+ // 函数式更新 —— 连着改两格不会互相覆盖 ✓
3759
+ setParams((prev) => Object.assign({}, prev, { [k]: native }))
3760
+ if (WK.indexOf(k) >= 0) setPin(k)
3761
+ setNotes(null)
3762
+ }
3763
+
3764
+ const wSum = WK.length ? Math.round(WK.reduce((a, k) => a + toPct(params[k] || 0), 0) * 10) / 10 : 0
3765
+
3766
+ // 2026-09-21 修版式:原来五个东西挤在一行里(标签写死 110px + 一整句长提示),
3767
+ // flexWrap 一装不下就拆散成「一行一个」;而 9 个字的标签在 110px 盒子里还会自己折行
3768
+ // —— 她看到那个孤零零的「活」就是「没写时间的算多鲜活」的尾巴。
3769
+ // 现在拆两行:① 标签+数字+单位+范围(短,不会挤)② 提示独占一行,自己换行。
3770
+ // 单位没写就不占位(原来会留一个空的 20px 盒子)。
3771
+ const oneRow = (s) => {
3772
+ const isW = s.group === 'weight'
3773
+ const lo = isW ? toPct(s.min) : s.min
3774
+ const hi = isW ? toPct(s.max) : s.max
3775
+ const cur = isW ? toPct(params[s.key]) : params[s.key]
3776
+ const typing = editing && editing.key === s.key
3777
+ const unit = isW ? '%' : s.unit || ''
3778
+ const head = [
3779
+ React.createElement('span', { key: 'lbl', style: { flex: '0 0 auto' } }, s.label),
3780
+ React.createElement('input', {
3781
+ key: 'inp',
3782
+ type: 'number',
3783
+ style: S.inp,
3784
+ min: lo,
3785
+ max: hi,
3786
+ step: isW ? 0.1 : s.step,
3787
+ value: typing ? editing.text : String(cur),
3788
+ onChange: (e) => {
3789
+ const t = e.target.value
3790
+ setEditing({ key: s.key, text: t })
3791
+ const n = Number(t)
3792
+ if (t !== '' && isFinite(n)) setOne(s.key, isW ? fromPct(n) : n)
3793
+ },
3794
+ onBlur: () => setEditing(null),
3795
+ }),
3796
+ ]
3797
+ if (unit) head.push(React.createElement('span', { key: 'unit', style: S.dim }, unit))
3798
+ head.push(React.createElement('span', { key: 'rng', style: Object.assign({}, S.dim, { fontSize: 11, opacity: 0.75 }) }, '(' + lo + ' ~ ' + hi + ')'))
3799
+ return React.createElement(
3800
+ 'div',
3801
+ { key: s.key, style: { margin: '7px 0' } },
3802
+ React.createElement('div', { style: { display: 'flex', alignItems: 'baseline', gap: 6, flexWrap: 'wrap' } }, head),
3803
+ s.tip
3804
+ ? React.createElement('div', { style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2, lineHeight: 1.5 }) }, s.tip)
3805
+ : null,
3806
+ )
3807
+ }
3808
+
3809
+ const save = (body) => {
3810
+ setBusy(true)
3811
+ setNotes(null)
3812
+ post('/policy', body)
3813
+ .then((r) => {
3814
+ if (r && r.policy) setParams(Object.assign({}, r.policy))
3815
+ setNotes((r && r.notes) || [])
3816
+ setBusy(false)
3817
+ load()
3818
+ })
3819
+ .catch((e) => {
3820
+ setBusy(false)
3821
+ setNotes(['保存失败:' + ((e && e.message) || e)])
3822
+ })
3823
+ }
3824
+
3825
+ const blocks = []
3826
+ for (const [gid, gname, gtip] of GROUPS) {
3827
+ const rows = spec.filter((s) => s.group === gid)
3828
+ if (!rows.length) continue
3829
+ blocks.push(React.createElement('div', { key: gid, style: S.h }, gname))
3830
+ blocks.push(React.createElement('div', { key: gid + '-tip', style: S.dim }, gtip))
3831
+ if (gid === 'weight') {
3832
+ blocks.push(
3833
+ React.createElement(
3834
+ 'div',
3835
+ { key: 'wsum', style: { margin: '8px 0 2px' } },
3836
+ React.createElement('span', { style: { display: 'inline-block', width: 110 } }, '合计'),
3837
+ React.createElement('span', { style: wSum === 100 ? S.ok : S.warn, fontWeight: 600 }, wSum + '%'),
3838
+ wSum === 100
3839
+ ? React.createElement('span', { style: Object.assign({}, S.dim, { marginLeft: 10, fontSize: 12 }) }, '正好 ✓')
3840
+ : React.createElement('span', { style: Object.assign({}, S.dim, { marginLeft: 10, fontSize: 12 }) }, '不是 100% —— 保存时会自动调平'),
3841
+ ),
3842
+ )
3843
+ }
3844
+ for (const s of rows) blocks.push(oneRow(s))
3845
+ }
3846
+
3847
+ return React.createElement(
3848
+ 'div',
3849
+ { style: S.wrap },
3850
+ React.createElement(
3851
+ 'div',
3852
+ { style: { display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 10 } },
3853
+ React.createElement('span', { style: { fontSize: 15, fontWeight: 600 } }, '鱼的记忆'),
3854
+ React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12 }) }, 'v' + (d.version || '?')),
3855
+ ),
3856
+ React.createElement('div', { style: Object.assign({}, S.h, { marginTop: 0 }) }, '哪些会话用记忆'),
3857
+ React.createElement(
3858
+ 'div',
3859
+ { style: S.dim },
3860
+ '装好就是开着的 —— 每个会话都能记、也能读。想给某个会话单独关掉,点它右边那个「关」。每个会话一份记忆,淡了只是不容易被想起来,不会删。',
3861
+ ),
3862
+ React.createElement(
3863
+ 'div',
3864
+ { style: S.nameBox },
3865
+ React.createElement('div', { style: { flex: 'none', width: 132 } }, '用户的称呼'),
3866
+ React.createElement('input', {
3867
+ style: Object.assign({}, S.inp, { width: 90 }),
3868
+ placeholder: 'ta / 她 / 他',
3869
+ value: pronounDraft !== null ? pronounDraft : d.userPronoun || 'ta',
3870
+ onChange: (e) => setPronounDraft(e.target.value),
3871
+ }),
3872
+ React.createElement(
3873
+ 'button',
3874
+ {
3875
+ style: S.btn,
3876
+ onClick: () => {
3877
+ const p = pronounDraft !== null ? pronounDraft : d.userPronoun || 'ta'
3878
+ post('/names', { userPronoun: p }).then((r) => {
3879
+ setPronounDraft(null)
3880
+ // 影响到了旧记忆就先问一句,不偷偷改 ✓
3881
+ if (r && r.称呼影响几条 > 0 && !r.称呼已改) setPronounAsk({ n: r.称呼影响几条, to: p, 例子: r.称呼例子 || [] })
3882
+ else setPronounAsk(null)
3883
+ load()
3884
+ })
3885
+ },
3886
+ },
3887
+ '存',
3888
+ ),
3889
+ React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12, flex: 1 }) }, '记忆里提到你时用哪个字。默认「ta」;设成「她」或「他」就按你设的记。'),
3890
+ ),
3891
+ pronounAsk
3892
+ ? React.createElement(
3893
+ 'div',
3894
+ { style: S.askBox },
3895
+ React.createElement(
3896
+ 'div',
3897
+ { style: { flex: 1 } },
3898
+ React.createElement('div', null, '已有 ' + pronounAsk.n + ' 条旧记忆里还写着旧的字。要一起改成「' + pronounAsk.to + '」吗?'),
3899
+ (pronounAsk.例子 || []).slice(0, 2).map((e, i) =>
3900
+ React.createElement('div', { key: i, style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2 }) }, ' 「' + e.旧 + '」→「' + e.新 + '」'),
3901
+ ),
3902
+ ),
3903
+ React.createElement(
3904
+ 'button',
3905
+ {
3906
+ style: S.btn,
3907
+ onClick: () =>
3908
+ post('/names', { userPronoun: pronounAsk.to, applyToExisting: true }).then(() => {
3909
+ setPronounAsk(null)
3910
+ load()
3911
+ }),
3912
+ },
3913
+ '一起改',
3914
+ ),
3915
+ React.createElement('button', { style: S.btn, onClick: () => setPronounAsk(null) }, '以后再说'),
3916
+ )
3917
+ : null,
3918
+ // ⭐ 1.3.4:**总开关**(管整个插件)+ 下面每个会话自己的开关
3919
+ // 放同一块,是因为它们管的是同一件事(记不记),只是范围不同。
3920
+ React.createElement(
3921
+ 'div',
3922
+ { style: Object.assign({}, S.row, S.条, { alignItems: 'center' }) },
3923
+ React.createElement(
3924
+ 'button',
3925
+ {
3926
+ style: Object.assign({}, S.btn, {
3927
+ flex: 'none',
3928
+ color: d.全局开关 !== false ? '#8fd0a0' : 'inherit',
3929
+ borderColor: d.全局开关 !== false ? 'rgba(143,208,160,.6)' : 'rgba(128,128,128,.35)',
3930
+ }),
3931
+ disabled: busy,
3932
+ onClick: () => {
3933
+ setBusy(true)
3934
+ post('/global', { on: d.全局开关 === false })
3935
+ .then(() => {
3936
+ setBusy(false)
3937
+ load()
3938
+ })
3939
+ .catch(() => setBusy(false))
3940
+ },
3941
+ },
3942
+ (d.全局开关 !== false ? '● ' : '○ ') + 'remember 工具',
3943
+ ),
3944
+ React.createElement(
3945
+ 'div',
3946
+ { style: Object.assign({}, S.dim, { flex: 1, minWidth: 0 }) },
3947
+ d.全局开关 !== false
3948
+ ? '所有会话都能记、也能读。单独不想要哪个,用下面那一行关掉。'
3949
+ : '全关着 —— 哪个会话都不记也不读,工具列表里也不会出现它。',
3950
+ ),
3951
+ ),
3952
+ (d.sessions || []).map((s) =>
3953
+ React.createElement(
3954
+ 'div',
3955
+ { key: s.id, style: Object.assign({}, S.row, S.条) },
3956
+ React.createElement(
3957
+ 'button',
3958
+ {
3959
+ style: Object.assign({}, S.btn, {
3960
+ flex: 'none',
3961
+ width: 56,
3962
+ color: s.enabled ? '#8fd0a0' : 'inherit',
3963
+ borderColor: s.enabled ? 'rgba(143,208,160,.6)' : 'rgba(128,128,128,.35)',
3964
+ }),
3965
+ disabled: busy,
3966
+ onClick: () => {
3967
+ setBusy(true)
3968
+ post('/toggle', { session: s.id, on: !s.enabled })
3969
+ .then(() => {
3970
+ setBusy(false)
3971
+ load()
3972
+ })
3973
+ .catch(() => setBusy(false))
3974
+ },
3975
+ },
3976
+ s.enabled ? '● 开' : '○ 关',
3977
+ ),
3978
+ React.createElement('div', { style: Object.assign({}, S.link, { flex: 1, minWidth: 0, overflowWrap: 'anywhere' }), onClick: () => setOpen(s.id) }, s.title || s.id.slice(0, 24)),
3979
+ s.live ? null : React.createElement('div', { style: Object.assign({}, S.dim, { width: 48, fontSize: 12 }) }, '不在线'),
3980
+ React.createElement('div', { style: Object.assign({}, S.dim, { width: 60 }) }, s.count + ' 条'),
3981
+ React.createElement('div', { style: Object.assign({}, S.dim, { width: 90 }) }, s.lastTrace ? rel(s.lastTrace, now) : '—'),
3982
+ ),
3983
+ ),
3984
+ // ⭐ 2026-09-22(「设置页面会不会有点乱」):**参数默认收起来** ✓
3985
+ // 那是**调一次就不动**的东西,原来直接铺在会话列表下面,占了大半屏,
3986
+ // 把常用的(会话列表)挤到上面去了。收成一行,想看再点开 ✓
3987
+ React.createElement(
3988
+ 'div',
3989
+ { style: Object.assign({}, S.h, { cursor: 'pointer' }), onClick: () => set看参数(!看参数) },
3990
+ (看参数 ? '▾ ' : '▸ ') + '参数(调一次就不动的东西,点开调)',
3991
+ ),
3992
+ 看参数
3993
+ ? React.createElement(
3994
+ 'div',
3995
+ null,
3996
+ React.createElement('div', { style: S.dim }, '这些是所有会话共用的。每项都有范围,超出会被收到范围内。'),
3997
+ blocks,
3998
+ React.createElement(
3999
+ 'div',
4000
+ { style: { marginTop: 12, display: 'flex', gap: 8, alignItems: 'center' } },
4001
+ React.createElement(
4002
+ 'button',
4003
+ { style: S.btn, disabled: busy, onClick: () => save(Object.assign({}, params, { pin })) },
4004
+ busy ? '保存中…' : '保存参数',
4005
+ ),
4006
+ React.createElement('button', { style: S.btn, disabled: busy, onClick: () => save({ reset: true }) }, '恢复默认'),
4007
+ React.createElement('button', { style: S.btn, disabled: busy, onClick: () => load() }, '刷新'),
4008
+ ),
4009
+ )
4010
+ : null,
4011
+ notes && notes.length
4012
+ ? React.createElement(
4013
+ 'div',
4014
+ { style: Object.assign({}, S.warn, { marginTop: 8, fontSize: 12, whiteSpace: 'pre-line' }) },
4015
+ ' · ' + notes.join('\n · '),
4016
+ )
4017
+ : null,
4018
+ React.createElement('div', { style: S.h }, '看某个会话'),
4019
+ React.createElement('div', { style: S.dim }, '点上面的会话名字'),
4020
+ )
4021
+ }
4022
+
4023
+ function apply(ctx) {
4024
+ const slots = ctx.get('slots')
4025
+ if (slots === undefined) return
4026
+ slots.inject('settings.section', () =>
4027
+ slots.register(
4028
+ {
4029
+ name: 'settings.section',
4030
+ id: 'memory',
4031
+ order: 51,
4032
+ label: () => '鱼的记忆',
4033
+ },
4034
+ () => React.createElement('div', null, React.createElement(更新条), React.createElement(Section)),
4035
+ ),
4036
+ )
4037
+ }
4038
+
4039
+ exports.apply = apply
4040
+ exports.inject = inject
4041
+ return module.exports
4042
+ },
4043
+ })