deepseek-plastic-memory 0.0.0-stage → 2.1.5

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/client.js ADDED
@@ -0,0 +1,409 @@
1
+ /**
2
+ * DeepSeek 可塑性记忆 —— 浏览器半端
3
+ * 设置里一页 + 会话顶栏一个入口;支持全局 / 各工作区 两套记忆
4
+ */
5
+ window.__ModuleLoader__.load({
6
+ id: 'deepseek-plastic-memory',
7
+ factory: (require) => {
8
+ const React = require('react')
9
+ const h = React.createElement
10
+ const API = '/api/plastic-memory'
11
+ const q = (scope) => '?scope=' + encodeURIComponent(scope || 'global')
12
+ const get = (p, scope) => fetch(API + p + (p.includes('?') ? '&' : q(scope))).then((r) => r.json())
13
+ const postJson = (p, body, scope) => fetch(API + p + q(scope), {
14
+ method: 'POST',
15
+ headers: { 'content-type': 'application/json' },
16
+ body: JSON.stringify(body || {}),
17
+ }).then((r) => r.json())
18
+
19
+ const CSS = `
20
+ .pm-root { display: flex; flex-direction: column; gap: 10px; }
21
+ .pm-card { border: 1px solid var(--dsw-border, #333); border-radius: 10px; overflow: hidden; }
22
+ .pm-head { display: flex; align-items: center; justify-content: space-between; padding: 9px 12px; font-size: 13px; font-weight: 600; background: color-mix(in srgb, currentColor 6%, transparent); }
23
+ .pm-badge { font-size: 11px; font-weight: 500; opacity: .7; margin-left: 6px; }
24
+ .pm-row { display: flex; align-items: center; gap: 8px; padding: 6px 12px; font-size: 13px; border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent); }
25
+ .pm-row:hover { background: color-mix(in srgb, currentColor 4%, transparent); }
26
+ .pm-k { flex: 0 0 auto; font-size: 11px; opacity: .55; min-width: 42px; }
27
+ .pm-t { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
28
+ .pm-empty { padding: 10px 12px; font-size: 12px; opacity: .45; }
29
+ .pm-btn { border: 1px solid var(--dsw-border, #3a3a3a); background: transparent; color: inherit; border-radius: 6px; font-size: 11px; padding: 2px 8px; cursor: pointer; opacity: .85; transition: .12s; }
30
+ .pm-btn:hover { opacity: 1; background: color-mix(in srgb, currentColor 8%, transparent); }
31
+ .pm-btn.primary { border-color: #4c8bf5; color: #6aa6ff; }
32
+ .pm-btn.primary:hover { background: rgba(76,139,245,.14); }
33
+ .pm-form { display: flex; gap: 6px; padding: 10px 12px; border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent); }
34
+ .pm-sel, .pm-inp { background: color-mix(in srgb, currentColor 5%, transparent); border: 1px solid var(--dsw-border, #3a3a3a); border-radius: 6px; color: inherit; font-size: 12px; padding: 5px 8px; outline: none; }
35
+ .pm-inp { flex: 1 1 auto; min-width: 0; }
36
+ .pm-inp:focus, .pm-sel:focus { border-color: #4c8bf5; }
37
+ .pm-msg { font-size: 11px; opacity: .55; padding: 0 12px 8px; }
38
+ .pm-scope { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent); font-size: 12px; }
39
+ .pm-scope .pm-sel { padding: 3px 8px; font-size: 12px; }
40
+ .pm-tabs { display: flex; flex-direction: column; gap: 2px; padding: 8px 8px 4px; }
41
+ .pm-tab { border: none; background: transparent; color: inherit; font-size: 12.5px; padding: 5px 10px; border-radius: 7px; cursor: pointer; opacity: .7; text-align: left; display: flex; align-items: center; gap: 6px; }
42
+ .pm-tab:hover { opacity: 1; background: color-mix(in srgb, currentColor 6%, transparent); }
43
+ .pm-tab.on { opacity: 1; background: color-mix(in srgb, currentColor 11%, transparent); font-weight: 600; }
44
+ .pm-tab .pm-caret { width: 12px; opacity: .6; }
45
+ .pm-tab .pm-count { margin-left: auto; opacity: .45; font-size: 11px; }
46
+ .pm-kids { margin: 0 0 2px 20px; padding-left: 10px; border-left: 2px solid color-mix(in srgb, currentColor 16%, transparent); display: flex; flex-direction: column; gap: 1px; }
47
+ .pm-kids .pm-tab { font-size: 12px; opacity: .65; }
48
+ .pm-kids .pm-tab.on { opacity: 1; }
49
+ .pm-list { padding: 4px 6px 8px; }
50
+ .pm-subname { opacity: .45; font-size: 11px; margin-right: 4px; }
51
+ `
52
+ function useCss() {
53
+ React.useEffect(() => {
54
+ const id = 'pm-style'
55
+ const old = document.getElementById(id)
56
+ if (old) old.remove()
57
+ const s = document.createElement('style')
58
+ s.id = id
59
+ s.textContent = CSS
60
+ document.head.appendChild(s)
61
+ }, [])
62
+ }
63
+
64
+ function PendingBox({ scope, folders, onChanged }) {
65
+ const [rows, setRows] = React.useState([])
66
+ const [msg, setMsg] = React.useState('')
67
+ const [form, setForm] = React.useState(false)
68
+ const [k, setK] = React.useState('')
69
+ const [text, setText] = React.useState('')
70
+ const [editing, setEditing] = React.useState(null)
71
+ const load = React.useCallback(async () => {
72
+ try { const d = await get('/pending', scope); setRows(d.rows || []) }
73
+ catch (e) { setMsg(String((e && e.message) || e)) }
74
+ }, [scope])
75
+ React.useEffect(() => { load() }, [load])
76
+ React.useEffect(() => { if (!k && folders.length) setK(folders[0].k) }, [folders])
77
+ const act = async (api, body) => {
78
+ try {
79
+ const r = await postJson(api, body, scope)
80
+ setMsg(r.out || 'ok')
81
+ await load()
82
+ setEditing(null)
83
+ onChanged && onChanged()
84
+ } catch (e) { setMsg(String((e && e.message) || e)) }
85
+ }
86
+ const submit = async () => {
87
+ const t = text.trim()
88
+ if (!t) { setMsg('先写点啥'); return }
89
+ await act('/add-now', { folder: k, text: t })
90
+ setText('')
91
+ setForm(false)
92
+ }
93
+ return h('div', { className: 'pm-card' },
94
+ h('div', { className: 'pm-head' },
95
+ h('span', null, '待审批', h('span', { className: 'pm-badge' }, rows.length)),
96
+ h('button', { className: 'pm-btn primary', onClick: () => setForm((v) => !v) }, form ? '收起' : '+ 记一条')),
97
+ form ? h('div', { className: 'pm-form' },
98
+ h('select', { className: 'pm-sel', value: k, onChange: (e) => setK(e.target.value) },
99
+ folders.map((f) => h('option', { key: f.k, value: f.k }, f.parent ? f.parent + ' › ' + f.name : f.k + ' ' + f.name))),
100
+ h('input', {
101
+ className: 'pm-inp', placeholder: '写点什么,越短越好', value: text, autoFocus: true,
102
+ onChange: (e) => setText(e.target.value),
103
+ onKeyDown: (e) => { if (e.key === 'Enter') submit() },
104
+ }),
105
+ h('button', { className: 'pm-btn primary', onClick: submit }, '写入')) : null,
106
+ msg ? h('div', { className: 'pm-msg' }, msg) : null,
107
+ rows.length === 0
108
+ ? h('div', { className: 'pm-empty' }, '没有待审的记忆')
109
+ : rows.map((r) => editing === r.id
110
+ ? h('div', { key: r.id, className: 'pm-form' },
111
+ h('span', { className: 'pm-k' }, r.k),
112
+ h('input', {
113
+ className: 'pm-inp', defaultValue: r.text, autoFocus: true,
114
+ id: 'pm-edit-' + r.id,
115
+ onKeyDown: (e) => { if (e.key === 'Enter') act('/edit', { id: r.id, text: e.currentTarget.value }) },
116
+ }),
117
+ h('button', { className: 'pm-btn', onClick: () => setEditing(null) }, '取消'),
118
+ h('button', {
119
+ className: 'pm-btn primary',
120
+ onClick: () => { const el = document.getElementById('pm-edit-' + r.id); act('/edit', { id: r.id, text: el && el.value }) },
121
+ }, '存'))
122
+ : h('div', { key: r.id, className: 'pm-row' },
123
+ h('span', { className: 'pm-k' }, r.k),
124
+ h('span', { className: 'pm-t', title: r.text }, r.text),
125
+ h('button', { className: 'pm-btn', onClick: () => setEditing(r.id) }, '改'),
126
+ h('button', { className: 'pm-btn', onClick: () => act('/reject', { id: r.id }) }, '驳'),
127
+ h('button', { className: 'pm-btn primary', onClick: () => act('/approve', { id: r.id }) }, '采纳')))
128
+ )
129
+ }
130
+
131
+ function Browser({ scope, folders, cur, setCur, reload }) {
132
+ const [rows, setRows] = React.useState([])
133
+ const [msg, setMsg] = React.useState('')
134
+ const [expanded, setExpanded] = React.useState({})
135
+ const [editingItem, setEditingItem] = React.useState(null)
136
+ const [mkOpen, setMkOpen] = React.useState(false)
137
+ const [mkName, setMkName] = React.useState('')
138
+ const [mkParent, setMkParent] = React.useState('')
139
+ const [lastBk, setLastBk] = React.useState('')
140
+ const tops = folders.filter((f) => !f.parent)
141
+ const subsOf = (k) => folders.filter((f) => f.parent === k)
142
+ const loadRows = React.useCallback(async (k) => {
143
+ if (!k) return
144
+ try { const o = await get('/open?k=' + encodeURIComponent(k), scope); setRows(o.rows || []) }
145
+ catch (e) { setMsg(String((e && e.message) || e)) }
146
+ }, [scope])
147
+ React.useEffect(() => { if (cur) loadRows(cur) }, [cur, loadRows])
148
+ const actItem = async (api, body) => {
149
+ try { const r = await postJson(api, body, scope); setMsg(r.out || 'ok'); setEditingItem(null); await loadRows(cur); reload && reload() }
150
+ catch (e) { setMsg(String((e && e.message) || e)) }
151
+ }
152
+ const gc = async () => {
153
+ try {
154
+ const r = await postJson('/gc', {}, scope)
155
+ const out = String(r.out || 'ok')
156
+ setMsg(out)
157
+ const m = /备份:s*(S+)/.exec(out)
158
+ setLastBk(m ? m[1] : '')
159
+ await loadRows(cur)
160
+ reload && reload()
161
+ } catch (e) { setMsg(String((e && e.message) || e)) }
162
+ }
163
+ const undoGc = async () => {
164
+ try {
165
+ const r = await postJson('/restore', { name: lastBk, scope }, scope)
166
+ setMsg(String(r.out || 'ok'))
167
+ setLastBk('')
168
+ await loadRows(cur)
169
+ reload && reload()
170
+ } catch (e) { setMsg(String((e && e.message) || e)) }
171
+ }
172
+ const mkSubmit = async () => {
173
+ const nm = mkName.trim()
174
+ if (!nm) { setMsg('先起个名'); return }
175
+ try {
176
+ const r = await postJson('/mk', { name: nm, parent: mkParent }, scope)
177
+ setMsg(r.out || 'ok')
178
+ setMkName('')
179
+ setMkOpen(false)
180
+ reload && reload()
181
+ } catch (e) { setMsg(String((e && e.message) || e)) }
182
+ }
183
+ return h('div', { className: 'pm-card', style: { flex: 1, minWidth: 0 } },
184
+ h('div', { className: 'pm-head' },
185
+ h('span', null, '记忆'),
186
+ h('span', { style: { display: 'flex', gap: 6 } },
187
+ h('button', { className: 'pm-btn', onClick: () => setMkOpen((v) => !v) }, '+ 分区'),
188
+ h('button', { className: 'pm-btn', onClick: gc }, '跑遗忘曲线'),
189
+ lastBk ? h('button', { className: 'pm-btn', onClick: undoGc }, '撤销清理') : null)),
190
+ mkOpen ? h('div', { className: 'pm-form' },
191
+ h('select', { className: 'pm-sel', value: mkParent, onChange: (e) => setMkParent(e.target.value) },
192
+ h('option', { value: '' }, '— 新建主分区 —'),
193
+ tops.map((f) => h('option', { key: f.k, value: f.k }, '放进 ' + f.k + ' ' + f.name))),
194
+ h('input', {
195
+ className: 'pm-inp', placeholder: '分区名,例如 行为', value: mkName,
196
+ onChange: (e) => setMkName(e.target.value),
197
+ onKeyDown: (e) => { if (e.key === 'Enter') mkSubmit() },
198
+ }),
199
+ h('button', { className: 'pm-btn primary', onClick: mkSubmit }, '建')) : null,
200
+ h('div', { className: 'pm-tabs' },
201
+ tops.map((f) => {
202
+ const kids = subsOf(f.k)
203
+ const open = !!expanded[f.k]
204
+ return h(React.Fragment, { key: f.k },
205
+ h('button', {
206
+ className: 'pm-tab' + (cur === f.k ? ' on' : ''),
207
+ onClick: () => {
208
+ if (kids.length) setExpanded((e) => ({ ...e, [f.k]: !e[f.k] }))
209
+ setCur(f.k)
210
+ },
211
+ },
212
+ h('span', { className: 'pm-caret' }, kids.length ? (open ? '▾' : '▸') : ''),
213
+ h('span', null, f.k + ' ' + f.name),
214
+ h('span', { className: 'pm-count' }, f.n)),
215
+ open && kids.length ? h('div', { className: 'pm-kids' },
216
+ kids.map((c) => h('button', {
217
+ key: c.k,
218
+ className: 'pm-tab' + (cur === c.k ? ' on' : ''),
219
+ onClick: () => setCur(c.k),
220
+ },
221
+ h('span', { className: 'pm-subname' }, f.name + ' ›'),
222
+ h('span', null, c.name),
223
+ h('span', { className: 'pm-count' }, c.n)))) : null)
224
+ })),
225
+ msg ? h('div', { className: 'pm-msg' }, msg) : null,
226
+ h('div', { className: 'pm-list' },
227
+ rows.length === 0
228
+ ? h('div', { className: 'pm-empty' }, '这个分区还是空的')
229
+ : rows.map((r) => editingItem === r.id
230
+ ? h('div', { key: r.id, className: 'pm-form' },
231
+ h('span', { className: 'pm-k' }, r.id),
232
+ h('input', {
233
+ className: 'pm-inp', defaultValue: r.text, autoFocus: true,
234
+ id: 'pm-item-' + r.id,
235
+ onKeyDown: (e) => { if (e.key === 'Enter') actItem('/edit-item', { k: cur, id: r.id, text: e.currentTarget.value }) },
236
+ }),
237
+ h('button', { className: 'pm-btn', onClick: () => setEditingItem(null) }, '取消'),
238
+ h('button', {
239
+ className: 'pm-btn primary',
240
+ onClick: () => { const el = document.getElementById('pm-item-' + r.id); actItem('/edit-item', { k: cur, id: r.id, text: el && el.value }) },
241
+ }, '存'))
242
+ : h('div', { key: r.id, className: 'pm-row' },
243
+ h('span', { className: 'pm-k' }, r.id),
244
+ h('span', { className: 'pm-t', title: r.text }, (r.flag ? r.flag + ' ' : '') + r.text),
245
+ h('span', { className: 'pm-k', style: { textAlign: 'right', minWidth: 30 } }, r.days + 'd'),
246
+ h('button', { className: 'pm-btn', onClick: () => setEditingItem(r.id) }, '改'),
247
+ h('button', { className: 'pm-btn', onClick: () => actItem('/del-item', { k: cur, id: r.id }) }, '删')))))
248
+ }
249
+
250
+ function MemoryView() {
251
+ useCss()
252
+ const [scope, setScope] = React.useState('global')
253
+ const [scopes, setScopes] = React.useState([{ id: 'global', label: '全局' }])
254
+ const [folders, setFolders] = React.useState([])
255
+ const [cur, setCur] = React.useState('')
256
+ const [msg, setMsg] = React.useState('')
257
+ const [wsOpen, setWsOpen] = React.useState(false)
258
+ const [q, setQ] = React.useState('')
259
+ const [hits, setHits] = React.useState(null)
260
+ const [wsCwd, setWsCwd] = React.useState('')
261
+ const [workspaces, setWorkspaces] = React.useState([])
262
+ const loadScopes = React.useCallback(async () => {
263
+ try { const sc = await get('/scopes'); setScopes(sc.scopes || [{ id: 'global', label: '全局' }]) } catch {}
264
+ }, [])
265
+ const loadFolders = React.useCallback(async (s) => {
266
+ try {
267
+ const lf = await get('/list', s || scope)
268
+ const fs2 = (lf.folders || [])
269
+ setFolders(fs2)
270
+ if (!fs2.some((x) => x.k === cur)) setCur(fs2.length ? fs2[0].k : '')
271
+ } catch (e) { setMsg(String((e && e.message) || e)) }
272
+ }, [scope, cur])
273
+ React.useEffect(() => { loadScopes() }, [wsOpen, loadScopes])
274
+ React.useEffect(() => { loadFolders() }, [scope])
275
+ React.useEffect(() => { get('/workspaces').then((d) => setWorkspaces(d.workspaces || [])).catch(() => {}) }, [wsOpen])
276
+
277
+ const doSearch = async () => {
278
+ const kw = q.trim()
279
+ if (!kw) { setHits(null); return }
280
+ try {
281
+ const r = await get('/search?q=' + encodeURIComponent(kw), scope)
282
+ const lines = String(r.out || '').split(String.fromCharCode(10)).filter(Boolean)
283
+ setHits(lines[0] === '没找到' ? [] : lines)
284
+ } catch (e) { setMsg(String((e && e.message) || e)) }
285
+ }
286
+ const pickScope = (id) => { setScope(id); setCur(''); setFolders([]) }
287
+ const addWs = async () => {
288
+ const cwd = wsCwd.trim()
289
+ if (!cwd) { setMsg('填个工作区路径'); return }
290
+ try {
291
+ const r = await postJson('/bind', { cwd })
292
+ if (r.ok) { setMsg('已建 ' + r.label); setWsCwd(''); setWsOpen(false); await loadFolders() }
293
+ else setMsg(r.error || '失败')
294
+ } catch (e) { setMsg(String((e && e.message) || e)) }
295
+ }
296
+ return h('div', { className: 'pm-root' },
297
+ h('div', { style: { fontSize: 12, opacity: 0.62, lineHeight: '18px', margin: '2px 2px 8px' } },
298
+ '小提示:觉得有必要的时候,或者在开新会话之前,先让 DSH 把当前记忆总结一遍。'),
299
+ h('div', { className: 'pm-card' },
300
+ h('div', { className: 'pm-scope' },
301
+ h('span', { style: { opacity: .6 } }, '记忆库'),
302
+ h('select', {
303
+ className: 'pm-sel', value: scope,
304
+ onChange: (e) => pickScope(e.target.value),
305
+ }, scopes.map((s) => h('option', { key: s.id, value: s.id }, s.cwd ? s.label + '(' + s.cwd + ')' : s.label))),
306
+ h('input', {
307
+ className: 'pm-inp', placeholder: '搜记忆…', value: q, style: { flex: '1 1 160px', minWidth: 120 },
308
+ onChange: (e) => setQ(e.target.value),
309
+ onKeyDown: (e) => { if (e.key === 'Enter') doSearch(); if (e.key === 'Escape') { setQ(''); setHits(null) } },
310
+ }),
311
+ h('button', { className: 'pm-btn', onClick: doSearch }, '搜'),
312
+ h('button', { className: 'pm-btn', onClick: () => setWsOpen((v) => !v) }, '+ 工作区'),
313
+ msg ? h('span', { style: { opacity: .55, fontSize: 11 } }, msg) : null)),
314
+ wsOpen ? h('div', { className: 'pm-form' },
315
+ h('select', { className: 'pm-sel', style: { flex: 1 }, value: wsCwd, onChange: (e) => setWsCwd(e.target.value) },
316
+ h('option', { value: '' }, '— 选一个 DSH 工作区 —'),
317
+ workspaces.map((w) => h('option', { key: w.id, value: w.path }, (w.bound ? '✓ ' : '') + w.title + ' ' + w.path))),
318
+ h('button', { className: 'pm-btn primary', onClick: addWs }, '建记忆库')) : null,
319
+ hits ? h('div', { className: 'pm-card' },
320
+ h('div', { className: 'pm-head' },
321
+ h('span', null, '搜索「' + q.trim() + '」'),
322
+ h('button', { className: 'pm-btn', onClick: () => { setQ(''); setHits(null) } }, '清掉')),
323
+ hits.length === 0
324
+ ? h('div', { className: 'pm-empty' }, '没有匹配的记忆')
325
+ : h('div', { className: 'pm-list' }, hits.map((line, i) => h('div', { key: i, className: 'pm-row' }, h('span', { className: 'pm-t' }, line))))) : null,
326
+ h(PendingBox, { scope, folders, onChanged: loadFolders }),
327
+ h('div', { style: { display: 'flex', minHeight: 260 } },
328
+ h(Browser, { scope, folders, cur, setCur, reload: () => loadFolders() })))
329
+ }
330
+
331
+ function Overlay({ onClose }) {
332
+ React.useEffect(() => {
333
+ const onKey = (e) => { if (e.key === 'Escape') onClose() }
334
+ window.addEventListener('keydown', onKey)
335
+ return () => window.removeEventListener('keydown', onKey)
336
+ }, [])
337
+ return h('div', {
338
+ onClick: onClose,
339
+ style: {
340
+ position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,.42)',
341
+ backdropFilter: 'blur(2px)', display: 'flex', alignItems: 'center',
342
+ justifyContent: 'center', padding: '24px 16px',
343
+ },
344
+ }, h('div', {
345
+ onClick: (e) => e.stopPropagation(),
346
+ style: {
347
+ background: 'var(--dsw-bg, #1d1f23)', color: 'var(--dsw-fg, #e8e8e8)',
348
+ borderRadius: 14, padding: 14, width: 'min(760px, 94vw)', maxHeight: '80vh',
349
+ overflow: 'auto', border: '1px solid var(--dsw-border, #35373c)',
350
+ boxShadow: '0 18px 50px rgba(0,0,0,.55)',
351
+ },
352
+ },
353
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10, padding: '0 2px' } },
354
+ h('span', { style: { fontWeight: 600, fontSize: 14 } }, '可塑性记忆'),
355
+ h('button', {
356
+ onClick: onClose, className: 'pm-btn',
357
+ style: { width: 24, height: 24, padding: 0, borderRadius: 999, lineHeight: '20px', fontSize: 14 },
358
+ }, '×')),
359
+ h(MemoryView, {})))
360
+ }
361
+
362
+ function HeaderBtn() {
363
+ const [open, setOpen] = React.useState(false)
364
+ const [hover, setHover] = React.useState(false)
365
+ useCss()
366
+ return h('span', { style: { position: 'relative', display: 'inline-flex' } },
367
+ h('button', {
368
+ title: '可塑性记忆',
369
+ onClick: () => setOpen((v) => !v),
370
+ onMouseEnter: () => setHover(true),
371
+ onMouseLeave: () => setHover(false),
372
+ style: {
373
+ display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
374
+ height: 24, padding: '0 8px', borderRadius: 6, cursor: 'pointer',
375
+ border: '1px solid ' + (open ? 'rgba(76,139,245,.55)' : 'transparent'),
376
+ background: open ? 'rgba(76,139,245,.16)' : (hover ? 'rgba(127,127,127,.14)' : 'transparent'),
377
+ color: 'inherit', fontSize: 12, lineHeight: 1, whiteSpace: 'nowrap', transition: 'all .12s',
378
+ },
379
+ }, '可塑性记忆'),
380
+ open ? h(Overlay, { onClose: () => setOpen(false) }) : null)
381
+ }
382
+
383
+ function Panel() {
384
+ return h('div', { style: { maxWidth: 780, padding: '2px 0 12px' } },
385
+ h('div', { style: { marginBottom: 8 } },
386
+ h('div', { style: { fontSize: 15, fontWeight: 600, marginBottom: 2 } }, '可塑性记忆'),
387
+ h('div', { style: { fontSize: 12, opacity: 0.6 } }, '分文件夹记事。7 天保留,7~30 天压缩,30 天进回收站;主人和约定不删。全局库和每个工作区各有一份。')),
388
+ h(MemoryView, {}))
389
+ }
390
+
391
+ return {
392
+ inject: ['slots'],
393
+ apply(ctx) {
394
+ const slots = (typeof ctx.get === 'function' ? ctx.get('slots') : undefined) || ctx.slots
395
+ if (!slots) return
396
+ slots.inject('settings.section', () => slots.register(
397
+ { name: 'settings.section', id: 'plastic-memory', order: 26, label: '可塑性记忆' },
398
+ () => h(Panel),
399
+ ))
400
+ try {
401
+ slots.inject('conversation.session.header.actions', () => slots.register(
402
+ { name: 'conversation.session.header.actions', id: 'plastic-memory-quick', order: 40 },
403
+ () => h(HeaderBtn),
404
+ ))
405
+ } catch (e) { /* 老版本没有这个槽位就算了 */ }
406
+ },
407
+ }
408
+ },
409
+ })
@@ -0,0 +1,5 @@
1
+ # DeepSeek 可塑性记忆:DSH 插件挂载声明
2
+ - insert:
3
+ - id: plastic-memory
4
+ name: deepseek-plastic-memory
5
+ config: {}