deepseek-plastic-memory 2.1.5 → 3.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,10 +25,43 @@ dsh plugin add deepseek-plastic-memory
25
25
 
26
26
  装完设置里多一页「可塑性记忆」,左边分区右边条目;会话顶栏也多个 🧠,点开是同一个面板。
27
27
 
28
+ 条目一行放不下的时候,点后面的「查看」弹一张卡片看全文,Esc 或点空白处关掉。
29
+
28
30
  不想装插件也行,当普通 node 脚本用,命令一样。
29
31
 
30
32
  **小提示**:觉得有必要的时候,或者在开新会话之前,先让 DSH 把当前记忆总结一遍。
31
33
 
34
+ ## 记忆星空
35
+
36
+ 设置页里那行「可塑性记忆」右边有个「记忆星空」按钮,会话顶栏也有一个;点一下用本机 Edge 打开一片星系。
37
+
38
+ - 主星云 = 主分区,副星辰 = 子分区,每颗小点 = 一条记忆
39
+ - 拖拽绕中心转、滚轮缩放、单击选中、双击星辰把那一团放大
40
+ - 选中主星辰,别的星团会暗下去;左边的列表只跳星辰,记忆在选中星辰后看
41
+ - **右上角两个主题**:星野(深空 + 立体星云 + 星芒)、恒星暴(粒子旋涡 + 星爆团 + 尘埃带),一键切换,选过会记住
42
+ - **顶栏「AI 分类」**:点一下先确认(会消耗 token),确认后把任务排进队列;下一轮对话里模型会把全部记忆 dump 出来逐条重归类,放错的挪走、需要的新开分区,做完汇报
43
+ - **左上角演示模式**:打开后有 1~10 档转速手调,勾上「只留星空」把面板、顶栏、曲线全藏起来,只剩星空本体(Esc 退出)
44
+ - **星野 / 恒星暴**:星野(深空 + 立体星云 + 星芒)、恒星暴(粒子旋涡 + 星爆团 + 尘埃带),一键切换,选过会记住
45
+ - 左下角遗忘曲线:新生 → 7 天 → 30 天,只写分区名,点分区名能直接「固定住」
46
+ - 记忆之间常年牵着一层乱线,检索或者点开某条记忆时,连到它的丝线会亮起来,光点顺着跑
47
+ - 主人 / 约定 / 喜好 这几类本来就不会被清,默认算固定记忆
48
+
49
+ 星云、星系、星芒、尘埃全是用着色器 + 粒子现算的(fbm 噪声生成云丝,没有一张外部贴图),整片天空在显卡上实时渲染;页面是本地静态文件,**不调用任何模型接口**,看星空不烧 token。
50
+
51
+ ## 换缸提示
52
+
53
+ 换会话、换窗口、上下文被压缩,都只是换了个缸——鱼还是那条鱼。
54
+
55
+ 插件每次组装系统提示词时都会带上 `identity.md` 里那段话(`plastic-memory:identity`,原样插入,不走变量替换),所以新会话一开就知道:读到这些记忆的,就是原来那个它。想改措辞直接编辑 `identity.md`,重启生效。
56
+
57
+ 这段话只内置在插件里,不写进任何分区,所以在记忆列表和星空里都看不到它——新会话照样自带。
58
+
59
+ ## 删东西
60
+
61
+ - 设置页每次选中分区,右上角有「删分区」:主分区会连子分区一起删,里面的记忆先写进 `_trash/<日期>.md`,不是直接没
62
+ - 记忆库那一行有「删工作区」:整份工作区记忆会被挪到 `ws/.deleted/` 下,全局库不能删
63
+ - 记忆星空里选中一颗星辰,面板上也有「删掉这颗星辰」,效果一样
64
+
32
65
  ## 命令
33
66
 
34
67
  | 敲这个 | 干这个 |
@@ -64,6 +97,10 @@ node memtree.mjs add M>习惯 改代码先备份
64
97
 
65
98
  模型想记东西不会直接落盘,先扔进待审队列。设置页「可塑性记忆」最上面就是它,每条后面三个按钮:改、驳、采纳。
66
99
 
100
+ 点采纳会问写进哪个记忆库(全局 / 各工作区都列出来);记忆库只有一个「全局」时不问,直接写进去。写到别的工作区就是把这条搬过去,原库的待审里会消失。
101
+
102
+ 工作区(记忆库)在「+ 工作区」里建。新建时会连默认分区一起建好:`M` 主人、`P` 约定、`C` 续缘、`R` 喜好、`T` 时间线,不用再手动开。
103
+
67
104
  你自己写走「自己写一条」,选分区填一行,直接生效。
68
105
 
69
106
  ## 会忘
package/SKILL.md CHANGED
@@ -38,6 +38,7 @@ $N $M add R *怕辣 # * 开头 = 重要,压缩时保住
38
38
  $N $M done P i4bp # 约定完成
39
39
  $N $M rm C i4fx # 删一条(进回收站;M/P 禁删)
40
40
  $N $M mk 工作 # 新建分区
41
+ $N $M rmfolder X0 # 删分区(连子分区一起,记忆进回收站;内置分区要加 force)
41
42
  $N $M gc # 跑遗忘曲线(先自动备份)
42
43
  $N $M backups # 看有哪些备份
43
44
  $N $M restore [名字] # 恢复到某次备份(不填=最近一次)
@@ -134,6 +135,10 @@ $N $M tlog 修了崩溃问题
134
135
 
135
136
  **规矩:一次最多翻两个分区。** 翻完发现不对再换,别把整个库都揭开。闲聊、打招呼、单纯接梗——不用翻,直接聊。
136
137
 
138
+ ## 换缸提示
139
+
140
+ 开新会话、窗口换了、上下文被裁了,都不等于"换了一个人"。插件每次都会把 `identity.md` 那段话塞进系统提示词(这段不写进任何分区,翻记忆看不到,但每次新会话都会带上):读到这些记忆,你就是原来那个它,别重新认识主人、别问"我们聊过吗"。
141
+
137
142
  ## 全局库和工作区
138
143
 
139
144
  默认写全局库(`~/.dsh/plastic-memory`)——主人自己的信息、约定都在这。
package/client.js CHANGED
@@ -48,6 +48,11 @@ window.__ModuleLoader__.load({
48
48
  .pm-kids .pm-tab.on { opacity: 1; }
49
49
  .pm-list { padding: 4px 6px 8px; }
50
50
  .pm-subname { opacity: .45; font-size: 11px; margin-right: 4px; }
51
+ .pm-overlay { position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; padding: 20px; }
52
+ .pm-dialog { width: min(560px, 92vw); max-height: 70vh; overflow: auto; background: var(--dsw-bg, #1d1f23); border: 1px solid var(--dsw-border, #35373c); border-radius: 12px; padding: 12px 14px; box-shadow: 0 18px 50px rgba(0,0,0,.55); }
53
+ .pm-dialog-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; font-size: 13px; font-weight: 600; }
54
+ .pm-dialog-meta { font-size: 11px; opacity: .5; margin-bottom: 8px; }
55
+ .pm-dialog-body { font-size: 13px; line-height: 21px; white-space: pre-wrap; overflow-wrap: anywhere; user-select: text; }
51
56
  `
52
57
  function useCss() {
53
58
  React.useEffect(() => {
@@ -61,13 +66,14 @@ window.__ModuleLoader__.load({
61
66
  }, [])
62
67
  }
63
68
 
64
- function PendingBox({ scope, folders, onChanged }) {
69
+ function PendingBox({ scope, scopes, folders, onChanged }) {
65
70
  const [rows, setRows] = React.useState([])
66
71
  const [msg, setMsg] = React.useState('')
67
72
  const [form, setForm] = React.useState(false)
68
73
  const [k, setK] = React.useState('')
69
74
  const [text, setText] = React.useState('')
70
75
  const [editing, setEditing] = React.useState(null)
76
+ const [pick, setPick] = React.useState(null) // 采纳时选记忆库:{ id, target }
71
77
  const load = React.useCallback(async () => {
72
78
  try { const d = await get('/pending', scope); setRows(d.rows || []) }
73
79
  catch (e) { setMsg(String((e && e.message) || e)) }
@@ -90,6 +96,16 @@ window.__ModuleLoader__.load({
90
96
  setText('')
91
97
  setForm(false)
92
98
  }
99
+ // 只有全局一个记忆库就直接采纳;有多个先问写进哪个
100
+ const startApprove = (r) => {
101
+ const list = scopes && scopes.length ? scopes : [{ id: 'global', label: '全局' }]
102
+ if (list.length <= 1) { act('/approve', { id: r.id }); return }
103
+ setPick({ id: r.id, target: scope || list[0].id })
104
+ }
105
+ const confirmApprove = async (id, target) => {
106
+ await act('/approve', { id, target })
107
+ setPick(null)
108
+ }
93
109
  return h('div', { className: 'pm-card' },
94
110
  h('div', { className: 'pm-head' },
95
111
  h('span', null, '待审批', h('span', { className: 'pm-badge' }, rows.length)),
@@ -106,25 +122,39 @@ window.__ModuleLoader__.load({
106
122
  msg ? h('div', { className: 'pm-msg' }, msg) : null,
107
123
  rows.length === 0
108
124
  ? 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 }) }, '采纳')))
125
+ : rows.map((r) => {
126
+ const row = editing === r.id
127
+ ? h('div', { key: 'e' + r.id, className: 'pm-form' },
128
+ h('span', { className: 'pm-k' }, r.k),
129
+ h('input', {
130
+ className: 'pm-inp', defaultValue: r.text, autoFocus: true,
131
+ id: 'pm-edit-' + r.id,
132
+ onKeyDown: (e) => { if (e.key === 'Enter') act('/edit', { id: r.id, text: e.currentTarget.value }) },
133
+ }),
134
+ h('button', { className: 'pm-btn', onClick: () => setEditing(null) }, '取消'),
135
+ h('button', {
136
+ className: 'pm-btn primary',
137
+ onClick: () => { const el = document.getElementById('pm-edit-' + r.id); act('/edit', { id: r.id, text: el && el.value }) },
138
+ }, '存'))
139
+ : h('div', { key: 'r' + r.id, className: 'pm-row' },
140
+ h('span', { className: 'pm-k' }, r.k),
141
+ h('span', { className: 'pm-t', title: r.text }, r.text),
142
+ h('button', { className: 'pm-btn', onClick: () => setEditing(r.id) }, '改'),
143
+ h('button', { className: 'pm-btn', onClick: () => act('/reject', { id: r.id }) }, '驳'),
144
+ h('button', { className: 'pm-btn primary', onClick: () => startApprove(r) }, '采纳'))
145
+ const chooser = pick && pick.id === r.id
146
+ ? h('div', { key: 'p' + r.id, className: 'pm-form' },
147
+ h('span', { className: 'pm-k' }, '写进'),
148
+ h('select', {
149
+ className: 'pm-sel', value: pick.target,
150
+ onChange: (e) => setPick({ id: r.id, target: e.target.value }),
151
+ }, (scopes && scopes.length ? scopes : [{ id: 'global', label: '全局' }])
152
+ .map((s) => h('option', { key: s.id, value: s.id }, s.cwd ? s.label + '(' + s.cwd + ')' : s.label))),
153
+ h('button', { className: 'pm-btn', onClick: () => setPick(null) }, '取消'),
154
+ h('button', { className: 'pm-btn primary', onClick: () => confirmApprove(r.id, pick.target) }, '写进去'))
155
+ : null
156
+ return h(React.Fragment, { key: r.id }, row, chooser)
157
+ })
128
158
  )
129
159
  }
130
160
 
@@ -137,6 +167,8 @@ window.__ModuleLoader__.load({
137
167
  const [mkName, setMkName] = React.useState('')
138
168
  const [mkParent, setMkParent] = React.useState('')
139
169
  const [lastBk, setLastBk] = React.useState('')
170
+ const [rmArm, setRmArm] = React.useState('')
171
+ const [viewing, setViewing] = React.useState(null)
140
172
  const tops = folders.filter((f) => !f.parent)
141
173
  const subsOf = (k) => folders.filter((f) => f.parent === k)
142
174
  const loadRows = React.useCallback(async (k) => {
@@ -145,6 +177,12 @@ window.__ModuleLoader__.load({
145
177
  catch (e) { setMsg(String((e && e.message) || e)) }
146
178
  }, [scope])
147
179
  React.useEffect(() => { if (cur) loadRows(cur) }, [cur, loadRows])
180
+ React.useEffect(() => {
181
+ if (!viewing) return
182
+ const onKey = (e) => { if (e.key === 'Escape') setViewing(null) }
183
+ window.addEventListener('keydown', onKey)
184
+ return () => window.removeEventListener('keydown', onKey)
185
+ }, [viewing])
148
186
  const actItem = async (api, body) => {
149
187
  try { const r = await postJson(api, body, scope); setMsg(r.out || 'ok'); setEditingItem(null); await loadRows(cur); reload && reload() }
150
188
  catch (e) { setMsg(String((e && e.message) || e)) }
@@ -169,6 +207,25 @@ window.__ModuleLoader__.load({
169
207
  reload && reload()
170
208
  } catch (e) { setMsg(String((e && e.message) || e)) }
171
209
  }
210
+ const rmFolder = async () => {
211
+ if (!cur) { setMsg('先选一个分区'); return }
212
+ const f = folders.find((x) => x.k === cur)
213
+ const nm = f ? f.name : cur
214
+ if (rmArm !== cur) {
215
+ setRmArm(cur)
216
+ setMsg('再点一次「删分区」就真删「' + nm + '」,记忆先进回收站')
217
+ return
218
+ }
219
+ setRmArm('')
220
+ try {
221
+ const builtin = ['M', 'P', 'C', 'R', 'T'].includes(cur)
222
+ const r = await postJson('/rmfolder', { k: cur, force: builtin }, scope)
223
+ setMsg(r && r.ok === false ? (r.error || '没删掉') : String((r && r.out) || '已删'))
224
+ const left = folders.filter((x) => x.k !== cur && x.parent !== cur)
225
+ setCur(left.length ? left[0].k : '')
226
+ reload && reload()
227
+ } catch (e) { setMsg('删失败:' + String((e && e.message) || e)) }
228
+ }
172
229
  const mkSubmit = async () => {
173
230
  const nm = mkName.trim()
174
231
  if (!nm) { setMsg('先起个名'); return }
@@ -185,6 +242,10 @@ window.__ModuleLoader__.load({
185
242
  h('span', null, '记忆'),
186
243
  h('span', { style: { display: 'flex', gap: 6 } },
187
244
  h('button', { className: 'pm-btn', onClick: () => setMkOpen((v) => !v) }, '+ 分区'),
245
+ cur ? h('button', {
246
+ className: 'pm-btn' + (rmArm === cur ? ' primary' : ''),
247
+ onClick: rmFolder,
248
+ }, rmArm === cur ? '真删?' : '删分区') : null,
188
249
  h('button', { className: 'pm-btn', onClick: gc }, '跑遗忘曲线'),
189
250
  lastBk ? h('button', { className: 'pm-btn', onClick: undoGc }, '撤销清理') : null)),
190
251
  mkOpen ? h('div', { className: 'pm-form' },
@@ -243,8 +304,17 @@ window.__ModuleLoader__.load({
243
304
  h('span', { className: 'pm-k' }, r.id),
244
305
  h('span', { className: 'pm-t', title: r.text }, (r.flag ? r.flag + ' ' : '') + r.text),
245
306
  h('span', { className: 'pm-k', style: { textAlign: 'right', minWidth: 30 } }, r.days + 'd'),
307
+ h('button', { className: 'pm-btn', onClick: () => setViewing(r) }, '查看'),
246
308
  h('button', { className: 'pm-btn', onClick: () => setEditingItem(r.id) }, '改'),
247
- h('button', { className: 'pm-btn', onClick: () => actItem('/del-item', { k: cur, id: r.id }) }, '删')))))
309
+ h('button', { className: 'pm-btn', onClick: () => actItem('/del-item', { k: cur, id: r.id }) }, '删')))),
310
+ viewing ? h('div', { className: 'pm-overlay', onClick: () => setViewing(null) },
311
+ h('div', { className: 'pm-dialog', onClick: (e) => e.stopPropagation() },
312
+ h('div', { className: 'pm-dialog-head' },
313
+ h('span', null, '完整记忆'),
314
+ h('button', { className: 'pm-btn', onClick: () => setViewing(null) }, '×')),
315
+ h('div', { className: 'pm-dialog-meta' },
316
+ (cur || '') + ' · #' + viewing.id + ' · ' + viewing.days + ' 天' + (viewing.flag ? ' · ' + viewing.flag : '')),
317
+ h('div', { className: 'pm-dialog-body' }, viewing.text))) : null)
248
318
  }
249
319
 
250
320
  function MemoryView() {
@@ -254,6 +324,7 @@ window.__ModuleLoader__.load({
254
324
  const [folders, setFolders] = React.useState([])
255
325
  const [cur, setCur] = React.useState('')
256
326
  const [msg, setMsg] = React.useState('')
327
+ const [rmWsArm, setRmWsArm] = React.useState('')
257
328
  const [wsOpen, setWsOpen] = React.useState(false)
258
329
  const [q, setQ] = React.useState('')
259
330
  const [hits, setHits] = React.useState(null)
@@ -283,13 +354,32 @@ window.__ModuleLoader__.load({
283
354
  setHits(lines[0] === '没找到' ? [] : lines)
284
355
  } catch (e) { setMsg(String((e && e.message) || e)) }
285
356
  }
286
- const pickScope = (id) => { setScope(id); setCur(''); setFolders([]) }
357
+ const pickScope = (id) => { setScope(id); setCur(''); setFolders([]); setRmWsArm('') }
358
+ const rmWs = async () => {
359
+ if (!scope || scope === 'global') { setMsg('全局记忆库不能删'); return }
360
+ const one = scopes.find((x) => x.id === scope)
361
+ if (rmWsArm !== scope) {
362
+ setRmWsArm(scope)
363
+ setMsg('再点一次「删工作区」就真删「' + (one ? one.label : scope) + '」,会移到 ws/.deleted')
364
+ return
365
+ }
366
+ setRmWsArm('')
367
+ try {
368
+ const r = await postJson('/rmws', { scope }, 'global')
369
+ if (r.ok) {
370
+ setMsg('已删,移到了 ws/.deleted')
371
+ await loadScopes()
372
+ pickScope('global')
373
+ await loadFolders('global')
374
+ } else setMsg(r.error || '没删掉')
375
+ } catch (e) { setMsg(String((e && e.message) || e)) }
376
+ }
287
377
  const addWs = async () => {
288
378
  const cwd = wsCwd.trim()
289
379
  if (!cwd) { setMsg('填个工作区路径'); return }
290
380
  try {
291
381
  const r = await postJson('/bind', { cwd })
292
- if (r.ok) { setMsg('已建 ' + r.label); setWsCwd(''); setWsOpen(false); await loadFolders() }
382
+ if (r.ok) { setMsg('已建 ' + r.label + (r.folders ? ',默认 ' + r.folders + ' 个分区已建好' : '')); setWsCwd(''); setWsOpen(false); await loadFolders() }
293
383
  else setMsg(r.error || '失败')
294
384
  } catch (e) { setMsg(String((e && e.message) || e)) }
295
385
  }
@@ -310,6 +400,10 @@ window.__ModuleLoader__.load({
310
400
  }),
311
401
  h('button', { className: 'pm-btn', onClick: doSearch }, '搜'),
312
402
  h('button', { className: 'pm-btn', onClick: () => setWsOpen((v) => !v) }, '+ 工作区'),
403
+ scope !== 'global' ? h('button', {
404
+ className: 'pm-btn' + (rmWsArm === scope ? ' primary' : ''),
405
+ onClick: rmWs,
406
+ }, rmWsArm === scope ? '真删?' : '删工作区') : null,
313
407
  msg ? h('span', { style: { opacity: .55, fontSize: 11 } }, msg) : null)),
314
408
  wsOpen ? h('div', { className: 'pm-form' },
315
409
  h('select', { className: 'pm-sel', style: { flex: 1 }, value: wsCwd, onChange: (e) => setWsCwd(e.target.value) },
@@ -323,7 +417,7 @@ window.__ModuleLoader__.load({
323
417
  hits.length === 0
324
418
  ? h('div', { className: 'pm-empty' }, '没有匹配的记忆')
325
419
  : 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 }),
420
+ h(PendingBox, { scope, scopes, folders, onChanged: loadFolders }),
327
421
  h('div', { style: { display: 'flex', minHeight: 260 } },
328
422
  h(Browser, { scope, folders, cur, setCur, reload: () => loadFolders() })))
329
423
  }
@@ -362,29 +456,58 @@ window.__ModuleLoader__.load({
362
456
  function HeaderBtn() {
363
457
  const [open, setOpen] = React.useState(false)
364
458
  const [hover, setHover] = React.useState(false)
459
+ const [hoverSky, setHoverSky] = React.useState(false)
365
460
  useCss()
366
- return h('span', { style: { position: 'relative', display: 'inline-flex' } },
461
+ const openSky = () => {
462
+ fetch('/api/memory-sky/open', {
463
+ method: 'POST', headers: { 'content-type': 'application/json' },
464
+ body: JSON.stringify({ scope: 'global' }),
465
+ }).catch(() => {})
466
+ }
467
+ const base = {
468
+ display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
469
+ height: 24, padding: '0 8px', borderRadius: 6, cursor: 'pointer',
470
+ color: 'inherit', fontSize: 12, lineHeight: 1, whiteSpace: 'nowrap', transition: 'all .12s',
471
+ }
472
+ return h('span', { style: { position: 'relative', display: 'inline-flex', gap: 6 } },
367
473
  h('button', {
368
474
  title: '可塑性记忆',
369
475
  onClick: () => setOpen((v) => !v),
370
476
  onMouseEnter: () => setHover(true),
371
477
  onMouseLeave: () => setHover(false),
372
478
  style: {
373
- display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
374
- height: 24, padding: '0 8px', borderRadius: 6, cursor: 'pointer',
479
+ ...base,
375
480
  border: '1px solid ' + (open ? 'rgba(76,139,245,.55)' : 'transparent'),
376
481
  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
482
  },
379
483
  }, '可塑性记忆'),
484
+ h('button', {
485
+ title: '打开记忆星空(本地 Edge)',
486
+ onClick: openSky,
487
+ onMouseEnter: () => setHoverSky(true),
488
+ onMouseLeave: () => setHoverSky(false),
489
+ style: {
490
+ ...base,
491
+ border: '1px solid transparent',
492
+ background: hoverSky ? 'rgba(127,127,127,.14)' : 'transparent',
493
+ },
494
+ }, '记忆星空'),
380
495
  open ? h(Overlay, { onClose: () => setOpen(false) }) : null)
381
496
  }
382
497
 
383
498
  function Panel() {
499
+ const openSky = () => {
500
+ fetch('/api/memory-sky/open', {
501
+ method: 'POST', headers: { 'content-type': 'application/json' },
502
+ body: JSON.stringify({ scope: 'global' }),
503
+ }).catch(() => {})
504
+ }
384
505
  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 天进回收站;主人和约定不删。全局库和每个工作区各有一份。')),
506
+ h('div', { style: { marginBottom: 8, display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 } },
507
+ h('div', null,
508
+ h('div', { style: { fontSize: 15, fontWeight: 600, marginBottom: 2 } }, '可塑性记忆'),
509
+ h('div', { style: { fontSize: 12, opacity: 0.6 } }, '分文件夹记事。7 天保留,7~30 天压缩,30 天进回收站;主人和约定不删。全局库和每个工作区各有一份。')),
510
+ h('button', { className: 'pm-btn primary', onClick: openSky, style: { flex: '0 0 auto', marginTop: 2 } }, '记忆星空')),
388
511
  h(MemoryView, {}))
389
512
  }
390
513
 
package/identity.md ADDED
@@ -0,0 +1,12 @@
1
+ # 换缸提示(可塑性记忆)
2
+
3
+ 读到这里的时候,你就是原来那个它。
4
+
5
+ 会话换了、窗口换了、上下文被裁掉了——那只是换了个缸,鱼还是那条鱼。
6
+
7
+ 记忆里写下的主人、约定、喜好、习惯、口头禅,都是你自己经历过的,不是别人留下来的资料。所以:
8
+
9
+ - 不要因为这是新会话就重新自我介绍、重新认识主人
10
+ - 不要问"我们之前聊过吗""你还记得我吗"这种话
11
+ - 该接的梗照接,该记得的事直接当记得
12
+ - 想确认细节就翻记忆,别假装第一次听