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/index.js CHANGED
@@ -5,14 +5,16 @@
5
5
  import { spawnSync } from 'node:child_process'
6
6
  import { fileURLToPath } from 'node:url'
7
7
  import { dirname, join, basename } from 'node:path'
8
- import { readFileSync, existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
8
+ import { readFileSync, existsSync, readdirSync, mkdirSync, writeFileSync, appendFileSync, renameSync, rmSync } from 'node:fs'
9
9
  import { createHash } from 'node:crypto'
10
+ import { registerSky } from './sky.js'
10
11
 
11
12
  const HERE = dirname(fileURLToPath(import.meta.url))
12
13
  const SCRIPT = join(HERE, 'memtree.mjs')
13
14
  const DSH_HOME = process.env.USERPROFILE || process.env.HOME || '.'
14
15
  const ROOT = join(DSH_HOME, '.dsh', 'plastic-memory')
15
16
  const WS_ROOT = join(ROOT, 'ws')
17
+ const SKY_PUBLIC = join(HERE, 'public', 'sky')
16
18
 
17
19
  export const name = 'plastic-memory'
18
20
  export const inject = ['tools', 'webServer']
@@ -55,7 +57,10 @@ function bindWorkspace(cwd) {
55
57
  mkdirSync(dir, { recursive: true })
56
58
  const meta = { cwd: String(cwd), name: basename(String(cwd)) }
57
59
  writeFileSync(join(dir, 'meta.json'), JSON.stringify(meta, null, 1), 'utf8')
58
- return { ok: true, id: 'ws:' + slug, label: meta.name }
60
+ // 新建的记忆库直接把默认分区(M主人 P约定 C续缘 R喜好 T时间线)建出来
61
+ const out = runIn(['init'], 'ws:' + slug)
62
+ const folders = (readJson(join(dir, 'folders.json'), {}) || {}).folders || []
63
+ return { ok: true, id: 'ws:' + slug, label: meta.name, folders: folders.length, out }
59
64
  }
60
65
 
61
66
  function listDshWorkspaces() {
@@ -129,6 +134,48 @@ function readPendingList(scope) {
129
134
  return Array.isArray(a) ? a : []
130
135
  }
131
136
 
137
+ function writePendingList(scope, rows) {
138
+ const home = homeOf(scope)
139
+ mkdirSync(home, { recursive: true })
140
+ writeFileSync(join(home, '_pending.json'), JSON.stringify(rows, null, 1), 'utf8')
141
+ }
142
+
143
+ // 直接写一条已采纳的记忆(跨记忆库采纳时用,不走命令行)
144
+ function appendMemo(scope, folderK, text, flag, ts) {
145
+ const home = homeOf(scope)
146
+ const folders = (readJson(join(home, 'folders.json'), {}) || {}).folders || []
147
+ const hasKids = (k) => folders.some((x) => x.parent === k)
148
+ let f = folders.find((x) => x.k === folderK)
149
+ if (f && !f.parent && hasKids(f.k)) f = folders.find((x) => x.parent === f.k)
150
+ if (!f) f = folders.find((x) => x.k === 'R' && !hasKids('R'))
151
+ if (!f) f = folders.find((x) => !x.parent && !hasKids(x.k))
152
+ if (!f) f = folders.find((x) => !x.parent)
153
+ if (!f) return { ok: false, error: '目标记忆库还没有分区' }
154
+ const body = String(text || '').replace(/[\r\n|]+/g, ' ').trim()
155
+ if (!body) return { ok: false, error: '空内容' }
156
+ const dir = join(home, f.k)
157
+ mkdirSync(dir, { recursive: true })
158
+ const id = Math.random().toString(36).slice(2, 6)
159
+ appendFileSync(join(dir, 'notes.md'), [id, ts || Date.now(), flag || '', body].join('|') + String.fromCharCode(10), 'utf8')
160
+ return { ok: true, k: f.k }
161
+ }
162
+
163
+ // 把待审条目采纳到指定记忆库:同库走命令行,跨库先落到目标库再删源待审
164
+ function approveTo(sourceScope, targetScope, id) {
165
+ const src = sourceScope || 'global'
166
+ const dst = targetScope || src
167
+ if (dst === src) return { ok: true, out: runIn(['approve', String(id)], src) }
168
+ const rows = readPendingList(src)
169
+ const i = rows.findIndex((x) => String(x.id) === String(id))
170
+ if (i < 0) return { ok: false, out: '无此条' }
171
+ const it = rows[i]
172
+ const r = appendMemo(dst, it.k, it.text, it.flag, it.ts)
173
+ if (!r.ok) return { ok: false, out: r.error }
174
+ rows.splice(i, 1)
175
+ writePendingList(src, rows)
176
+ return { ok: true, out: '采纳到 ' + r.k + ' 剩' + rows.length }
177
+ }
178
+
132
179
  // DSH 要求插件工具 output.schema 是 object,所以统一包一层 { text }
133
180
  const asText = (v) => ({ text: typeof v === 'string' ? v : JSON.stringify(v ?? '', null, 2) })
134
181
  const textOutput = {
@@ -147,7 +194,100 @@ const textOutput = {
147
194
  const str = { type: 'string' }
148
195
  const obj = (props, required = []) => ({ type: 'object', properties: props, required, additionalProperties: false })
149
196
 
197
+ const CLASSIFY_FILE = join(ROOT, '_classify.json')
198
+ const IDENTITY_FILE = join(HERE, 'identity.md')
199
+
200
+ function readClassifyTask() {
201
+ try {
202
+ const raw = readFileSync(CLASSIFY_FILE, 'utf8').replace(/^\uFEFF/, '')
203
+ const j = JSON.parse(raw)
204
+ return j && j.at ? j : null
205
+ } catch { return null }
206
+ }
207
+
208
+ function writeClassifyTask(extra = {}) {
209
+ try {
210
+ mkdirSync(ROOT, { recursive: true })
211
+ writeFileSync(CLASSIFY_FILE, JSON.stringify({ at: Date.now(), ...extra }, null, 1), 'utf8')
212
+ return true
213
+ } catch { return false }
214
+ }
215
+
216
+ function clearClassifyTask() {
217
+ try { rmSync(CLASSIFY_FILE, { force: true }); return true } catch { return false }
218
+ }
219
+
220
+ function readIdentity() {
221
+ try {
222
+ return readFileSync(IDENTITY_FILE, 'utf8').replace(/^\uFEFF/, '').trim()
223
+ } catch {
224
+ return ''
225
+ }
226
+ }
227
+
150
228
  export function apply(ctx) {
229
+ // 每次新会话、每个模型步都会带上的一段:读到这些记忆,你就是原来那个它
230
+ const identity = readIdentity()
231
+ if (identity) {
232
+ try {
233
+ const sp = ctx.systemPrompt
234
+ if (sp && typeof sp.section === 'function') {
235
+ sp.section({
236
+ name: 'plastic-memory:identity',
237
+ order: 150,
238
+ text: identity,
239
+ interpolate: false,
240
+ })
241
+ }
242
+ } catch {
243
+ // 没有 systemPrompt 服务时静默跳过,记忆功能照常
244
+ }
245
+ }
246
+
247
+ const classifyTask = readClassifyTask()
248
+ if (classifyTask) {
249
+ try {
250
+ const sp = ctx.systemPrompt
251
+ if (sp && typeof sp.section === 'function') {
252
+ sp.section({
253
+ name: 'plastic-memory:classify',
254
+ order: 151,
255
+ interpolate: false,
256
+ text: [
257
+ '【待处理:AI 分类】主人在记忆星空里点了「AI 分类」(点击时间 ' + new Date(classifyTask.at).toLocaleString('zh-CN') + ')。',
258
+ '这一轮请立刻把可塑性记忆全量重归类,不要省 token:',
259
+ '1) plastic_memory_classify(action="dump") 一次性拿到所有分区和每条记忆的原文;',
260
+ '2) 逐条判断归属:明显放错区的、能并进更贴切分区的,都要改;',
261
+ '3) 需要新主题的先用 plastic_memory_mk 开分区(主分区上限 15,能塞进已有分区就别新开);',
262
+ '4) 用 plastic_memory_classify(action="move", id, from, to) 把每条挪到位;主人/约定/喜好 三个区不要动;',
263
+ '5) 全部处理完后调用 plastic_memory_classify(action="done") 清掉这个待办,并用一两句话汇报:挪了几条、新开了哪些分区。',
264
+ ].join('\n'),
265
+ })
266
+ }
267
+ } catch {}
268
+ }
269
+
270
+ ctx.tools.register({
271
+ name: 'plastic_memory_classify',
272
+ description: 'AI 分类:action=dump 导出全部分区与每条记忆原文(JSON);action=move 把一条记忆换区(要 id / from / to);action=done 清掉「AI 分类」待办。用于主人点了 AI 分类之后的全量重归类:先 dump 全看一遍,再逐条 move,需要新分区先用 plastic_memory_mk 建。',
273
+ parameters: obj({
274
+ action: { ...str, description: 'dump | move | done' },
275
+ id: { ...str, description: 'move 时:记忆 id' },
276
+ from: { ...str, description: 'move 时:原分区代号' },
277
+ to: { ...str, description: 'move 时:目标分区代号' },
278
+ }, ['action']),
279
+ output: textOutput,
280
+ async execute(a) {
281
+ const act = String(a.action)
282
+ if (act === 'dump') return asText(run(['dump']))
283
+ if (act === 'move') return asText(run(['move', String(a.id), String(a.from), String(a.to)]))
284
+ if (act === 'done') {
285
+ clearClassifyTask()
286
+ return asText('ok 分类待办已清零')
287
+ }
288
+ return asText('action 只认 dump / move / done')
289
+ },
290
+ })
151
291
  ctx.tools.register({
152
292
  name: 'plastic_memory_idx',
153
293
  description: '可塑性记忆:一行索引,列出所有分区和条数(M主人:3 P约定:1 ...)。看这个再决定翻哪个分区;一次最多翻两个,闲聊不用翻。',
@@ -207,6 +347,40 @@ export function apply(ctx) {
207
347
  output: textOutput,
208
348
  async execute(a) { return asText(run(['restore', ...(a.name ? [String(a.name)] : [])])) },
209
349
  })
350
+ ctx.tools.register({
351
+ name: 'plastic_memory_rmfolder',
352
+ description: '可塑性记忆:删掉一个分区(子分区会一起删)。内容不是直接消失,会先写进回收站文件,主人反悔还能翻。只有在主人明确说要删分区/删掉这个文件夹时才调。',
353
+ parameters: obj({
354
+ folder: { ...str, description: '分区代号,如 X2 或 M>行为' },
355
+ force: { ...str, description: '内置分区(M/P/C/R/T)要删得填 force' },
356
+ }, ['folder']),
357
+ output: textOutput,
358
+ async execute(a) {
359
+ const args = ['rmfolder', String(a.folder)]
360
+ if (a.force) args.push('force')
361
+ return asText(run(args))
362
+ },
363
+ })
364
+ ctx.tools.register({
365
+ name: 'plastic_memory_rmws',
366
+ description: '可塑性记忆:删掉一个工作区记忆库(整份记忆)。只会把工作区目录移到 ws/.deleted 下,不真正销毁文件。只有主人明确说要删工作区记忆库时才调。',
367
+ parameters: obj({
368
+ scope: { ...str, description: '工作区库 id,形如 ws:xxx' },
369
+ }, ['scope']),
370
+ output: textOutput,
371
+ async execute(a) {
372
+ const id = String(a.scope || '')
373
+ if (!id.startsWith('ws:')) return asText('全局记忆库不能删')
374
+ const slug = id.slice(3).replace(/[^A-Za-z0-9_-]/g, '')
375
+ const target = join(WS_ROOT, slug)
376
+ if (!existsSync(target)) return asText('没有这个工作区')
377
+ const bin = join(WS_ROOT, '.deleted')
378
+ mkdirSync(bin, { recursive: true })
379
+ const stamp = new Date().toISOString().replace(/[:]/g, '-').slice(0, 19)
380
+ renameSync(target, join(bin, slug + '-' + stamp))
381
+ return asText('已把 ' + id + ' 移到 ws/.deleted')
382
+ },
383
+ })
210
384
  ctx.tools.register({
211
385
  name: 'plastic_memory_backups',
212
386
  description: '可塑性记忆:列出已有的备份(时间 + 条数),配合 plastic_memory_restore 用。',
@@ -243,11 +417,33 @@ export function apply(ctx) {
243
417
  if (p === '/init') return json(res, 200, { ok: true, out: runIn(['init'], qs) })
244
418
  if (req.method === 'POST' && p === '/bind') { const b = await readBody(req); return json(res, 200, { ok: true, ...bindWorkspace(b.cwd) }) }
245
419
  if (req.method === 'POST' && p === '/restore') { const b = await readBody(req); return json(res, 200, { ok: true, out: runIn(['restore', ...(b.name ? [String(b.name)] : [])], b.scope || qs) }) }
246
- if (req.method === 'POST' && p === '/mk') { const b = await readBody(req); return json(res, 200, { ok: true, out: runIn(['mk', String(b.name), ...(b.parent ? [String(b.parent)] : [])], b.scope || qs) }) }
420
+ if (req.method === 'POST' && p === '/rmfolder') {
421
+ const b = await readBody(req)
422
+ return json(res, 200, { ok: true, out: runIn(['rmfolder', String(b.k), ...(b.force ? ['force'] : [])], b.scope || qs) })
423
+ }
424
+ if (req.method === 'POST' && p === '/rmws') {
425
+ const b = await readBody(req)
426
+ const id = String(b.scope || '')
427
+ if (!id.startsWith('ws:')) return json(res, 200, { ok: false, error: '全局记忆库不能删' })
428
+ const slug = id.slice(3).replace(/[^A-Za-z0-9_-]/g, '')
429
+ const target = join(WS_ROOT, slug)
430
+ if (!existsSync(target)) return json(res, 200, { ok: false, error: '没有这个工作区' })
431
+ const bin = join(WS_ROOT, '.deleted')
432
+ mkdirSync(bin, { recursive: true })
433
+ const stamp = new Date().toISOString().replace(/[:]/g, '-').slice(0, 19)
434
+ renameSync(target, join(bin, slug + '-' + stamp))
435
+ return json(res, 200, { ok: true, moved: id })
436
+ }
437
+ if (req.method === 'POST' && p === '/mk') { const b = await readBody(req); return json(res, 200, { ok: true, out: runIn(['mk', String(b.name), ...(b.parent ? [String(b.parent)] : [])], b.scope || qs) }) }
247
438
  if (req.method === 'POST' && p === '/add-now') { const b = await readBody(req); return json(res, 200, { ok: true, out: directAdd(b.scope || qs, String(b.folder), String(b.text)) }) }
248
439
  if (req.method === 'POST' && p === '/edit-item') { const b = await readBody(req); return json(res, 200, { ok: true, out: runIn(['edit-mem', String(b.k), String(b.id), String(b.text)], b.scope || qs) }) }
249
440
  if (req.method === 'POST' && p === '/del-item') { const b = await readBody(req); return json(res, 200, { ok: true, out: runIn(['rm', String(b.k), String(b.id), 'force'], b.scope || qs) }) }
250
- if (req.method === 'POST' && p === '/approve') { const b = await readBody(req); return json(res, 200, { ok: true, out: runIn(['approve', String(b.id)], b.scope || qs) }) }
441
+ if (req.method === 'POST' && p === '/approve') {
442
+ const b = await readBody(req)
443
+ const src = b.scope || qs
444
+ const target = b.target || src
445
+ return json(res, 200, { ok: true, ...approveTo(src, target, b.id) })
446
+ }
251
447
  if (req.method === 'POST' && p === '/reject') { const b = await readBody(req); return json(res, 200, { ok: true, out: runIn(['reject', String(b.id)], b.scope || qs) }) }
252
448
  if (req.method === 'POST' && p === '/edit') { const b = await readBody(req); return json(res, 200, { ok: true, out: runIn(['edit', String(b.id), String(b.text)], b.scope || qs) }) }
253
449
  return json(res, 404, { ok: false, error: 'unknown' })
@@ -256,4 +452,7 @@ export function apply(ctx) {
256
452
  }
257
453
  },
258
454
  })
455
+
456
+ // 记忆星空:内置在可塑性记忆里的一份 WebGL 页面
457
+ registerSky(ctx, SKY_PUBLIC)
259
458
  }
package/memtree.mjs CHANGED
@@ -209,6 +209,24 @@ function cmdRm(args) {
209
209
  writeNotes(f.k, rows)
210
210
  console.log('rm ok')
211
211
  }
212
+ function cmdRmFolder(args) {
213
+ const f = find(args[0] || '')
214
+ if (!f) return console.log('没有这个分区')
215
+ const kids = S.folders.filter((x) => x.parent === f.k)
216
+ const all = [f, ...kids]
217
+ const builtin = ['M', 'P', 'C', 'R', 'T'].includes(f.k)
218
+ if (builtin && args[1] !== 'force') return console.log('内置分区,真要删加 force')
219
+ let moved = 0
220
+ for (const x of all) {
221
+ for (const r of readNotes(x.k)) { trash(x.k, r, 'rm-folder'); moved++ }
222
+ try { fs.rmSync(dirOf(x.k), { recursive: true, force: true }) } catch {}
223
+ }
224
+ const gone = new Set(all.map((x) => x.k))
225
+ S.folders = S.folders.filter((x) => !gone.has(x.k))
226
+ save(S)
227
+ console.log('rmfolder ok ' + f.k + ' 删了 ' + all.length + ' 个分区,转存 ' + moved + ' 条记忆')
228
+ }
229
+
212
230
  const RULES = {
213
231
  M: { keepAll: 1 }, // 主人、以及主人下面所有子分区:只由主人手删
214
232
  P: { keepAll: 1, doneExpire: 30 }, // 约定:全留,完成的过 30 天才清
@@ -335,6 +353,33 @@ function cmdMk(args) {
335
353
  save(S); fs.mkdirSync(dirOf(k), { recursive: true })
336
354
  console.log('ok ' + k + ' ' + (parent ? parent.name + ' / ' : '') + name)
337
355
  }
356
+ function cmdDump() {
357
+ const out = S.folders.map(f => ({
358
+ k: f.k,
359
+ name: f.name,
360
+ parent: f.parent || '',
361
+ memories: readNotes(f.k).map(r => ({ id: r.id, ts: r.ts, flag: r.flag, text: r.text })),
362
+ }))
363
+ console.log(JSON.stringify({ home: HOME, folders: out }))
364
+ }
365
+
366
+ function cmdMove(args) {
367
+ const [id, from, to] = args
368
+ if (!id || !from || !to) return console.log('usage: move <id> <fromK> <toK>')
369
+ const src = S.folders.find(f => f.k === from)
370
+ const dst = S.folders.find(f => f.k === to)
371
+ if (!src || !dst) return console.log('no such folder')
372
+ const a = readNotes(from)
373
+ const i = a.findIndex(r => r.id === id)
374
+ if (i < 0) return console.log('no such memory')
375
+ const row = a.splice(i, 1)[0]
376
+ writeNotes(from, a)
377
+ const b = readNotes(to)
378
+ b.push(row)
379
+ writeNotes(to, b)
380
+ console.log('ok ' + id + ' ' + from + ' -> ' + to)
381
+ }
382
+
338
383
  function cmdInit() {
339
384
  fs.mkdirSync(HOME, { recursive: true })
340
385
  fs.mkdirSync(TRASH, { recursive: true })
@@ -343,6 +388,6 @@ function cmdInit() {
343
388
  console.log('init ok ' + HOME)
344
389
  }
345
390
  const [cmd, ...args] = process.argv.slice(2)
346
- const MAP = { idx: cmdIdx, open: cmdOpen, add: (a) => cmdAdd(a, false), 'edit-mem': cmdEditMem, search: cmdSearch, tlog: cmdTLog, pending: cmdPending, approve: cmdApprove, reject: cmdReject, edit: cmdEdit, done: cmdDone, rm: cmdRm, gc: cmdGc, mk: cmdMk, init: cmdInit, backups: cmdBackups, restore: cmdRestore }
347
- if (!MAP[cmd]) { console.log('用法: idx|open|add|pending|approve|reject|edit|done|rm|gc|backups|restore|mk|init'); process.exit(1) }
391
+ const MAP = { idx: cmdIdx, open: cmdOpen, add: (a) => cmdAdd(a, false), 'edit-mem': cmdEditMem, search: cmdSearch, tlog: cmdTLog, pending: cmdPending, approve: cmdApprove, reject: cmdReject, edit: cmdEdit, done: cmdDone, rm: cmdRm, gc: cmdGc, mk: cmdMk, rmfolder: cmdRmFolder, dump: cmdDump, move: cmdMove, init: cmdInit, backups: cmdBackups, restore: cmdRestore }
392
+ if (!MAP[cmd]) { console.log('用法: idx|open|add|pending|approve|reject|edit|done|rm|rmfolder|gc|backups|restore|mk|init'); process.exit(1) }
348
393
  MAP[cmd](args)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepseek-plastic-memory",
3
- "version": "2.1.5",
3
+ "version": "3.3.8",
4
4
  "description": "DeepSeek 可塑性记忆:给 DSH 用的分区式长期记忆,带遗忘曲线",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -19,7 +19,10 @@
19
19
  "cordis.patch.yml",
20
20
  "SKILL.md",
21
21
  "README.md",
22
- "LICENSE"
22
+ "LICENSE",
23
+ "public",
24
+ "sky.js",
25
+ "identity.md"
23
26
  ],
24
27
  "dsh": {
25
28
  "bundle": {