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 +37 -0
- package/SKILL.md +5 -0
- package/client.js +154 -31
- package/identity.md +12 -0
- package/index.js +203 -4
- package/memtree.mjs +47 -2
- package/package.json +5 -2
- package/public/sky/app.js +2336 -0
- package/public/sky/app.js.bak +2336 -0
- package/public/sky/index.html +286 -0
- package/public/sky/vendor/three.core.js +60586 -0
- package/public/sky/vendor/three.module.js +19719 -0
- package/sky.js +332 -0
package/sky.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆星空(内置在可塑性记忆里)—— 宿主侧
|
|
3
|
+
* 只读/回写 ~/.dsh/plastic-memory 的数据;页面在 public/sky 下,渲染全交给本地 Edge 的显卡。
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process'
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, readdirSync, mkdirSync, rmSync, renameSync } from 'node:fs'
|
|
7
|
+
import { dirname, join, extname, resolve, basename } from 'node:path'
|
|
8
|
+
|
|
9
|
+
const HOME = process.env.USERPROFILE || process.env.HOME || '.'
|
|
10
|
+
const ROOT = join(HOME, '.dsh', 'plastic-memory')
|
|
11
|
+
const WS_ROOT = join(ROOT, 'ws')
|
|
12
|
+
const KEEP_TOPS = new Set(['M', 'P', 'R']) // 主人 / 约定 / 喜好:本来就不进遗忘曲线
|
|
13
|
+
const EDGE_CANDIDATES = [
|
|
14
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
15
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const readJson = (p, d) => { try { return JSON.parse(readFileSync(p, 'utf8').replace(/^\uFEFF/, '')) } catch { return d } }
|
|
19
|
+
|
|
20
|
+
const homeOf = (scope) => {
|
|
21
|
+
if (!scope || scope === 'global') return ROOT
|
|
22
|
+
const slug = String(scope).replace(/^ws:/, '').replace(/[^A-Za-z0-9_-]/g, '')
|
|
23
|
+
return join(WS_ROOT, slug)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function listScopes() {
|
|
27
|
+
const out = [{ id: 'global', label: '全局', cwd: '' }]
|
|
28
|
+
try {
|
|
29
|
+
for (const d of readdirSync(WS_ROOT, { withFileTypes: true })) {
|
|
30
|
+
if (!d.isDirectory() || d.name.startsWith('.')) continue
|
|
31
|
+
const meta = readJson(join(WS_ROOT, d.name, 'meta.json'), {})
|
|
32
|
+
out.push({ id: 'ws:' + d.name, label: meta.name || d.name, cwd: meta.cwd || '' })
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
return out
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const readFolders = (home) => {
|
|
39
|
+
const j = readJson(join(home, 'folders.json'), { folders: [] })
|
|
40
|
+
return Array.isArray(j.folders) ? j.folders : []
|
|
41
|
+
}
|
|
42
|
+
const notesPath = (home, k) => join(home, k, 'notes.md')
|
|
43
|
+
|
|
44
|
+
function readNotes(home, k) {
|
|
45
|
+
const p = notesPath(home, k)
|
|
46
|
+
if (!existsSync(p)) return []
|
|
47
|
+
return readFileSync(p, 'utf8').replace(/^\uFEFF/, '').split('\n').filter(Boolean).map((line) => {
|
|
48
|
+
const [id, ts, flag, ...rest] = line.split('|')
|
|
49
|
+
return { id: (id || '').replace(/^\uFEFF/, ''), ts: Number(ts) || 0, flag: flag || '', text: rest.join('|') }
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeNotes(home, k, rows) {
|
|
54
|
+
const p = notesPath(home, k)
|
|
55
|
+
mkdirSync(dirname(p), { recursive: true })
|
|
56
|
+
writeFileSync(p, rows.map((r) => [r.id, r.ts, r.flag, r.text].join('|')).join('\n') + (rows.length ? '\n' : ''), 'utf8')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function topOf(k, folders) {
|
|
60
|
+
let cur = folders.find((f) => f.k === k)
|
|
61
|
+
let guard = 0
|
|
62
|
+
for (let i = 0; cur && i < 8; i++) {
|
|
63
|
+
guard++
|
|
64
|
+
if (!cur.parent) break
|
|
65
|
+
const next = folders.find((f) => f.k === cur.parent)
|
|
66
|
+
if (!next) break
|
|
67
|
+
cur = next
|
|
68
|
+
}
|
|
69
|
+
return cur
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function snapshot(scope) {
|
|
73
|
+
const home = homeOf(scope)
|
|
74
|
+
const folders = readFolders(home)
|
|
75
|
+
const now = Date.now()
|
|
76
|
+
const nameOf = new Map(folders.map((f) => [f.k, f.name]))
|
|
77
|
+
const memories = []
|
|
78
|
+
|
|
79
|
+
for (const f of folders) {
|
|
80
|
+
const top = topOf(f.k, folders)
|
|
81
|
+
const keepByRule = !!top && KEEP_TOPS.has(top.k)
|
|
82
|
+
const fixedByRule = keepByRule || !!(top && top.guard) || !!f.guard
|
|
83
|
+
for (const n of readNotes(home, f.k)) {
|
|
84
|
+
const days = Math.max(0, Math.floor((now - n.ts) / 864e5))
|
|
85
|
+
const manual = n.flag === '*' || n.flag === '✓'
|
|
86
|
+
const pinned = manual || fixedByRule
|
|
87
|
+
memories.push({
|
|
88
|
+
id: n.id,
|
|
89
|
+
folder: f.k,
|
|
90
|
+
folderName: f.name,
|
|
91
|
+
parent: f.parent || '',
|
|
92
|
+
mainName: f.parent ? (nameOf.get(f.parent) || '') : f.name,
|
|
93
|
+
text: n.text,
|
|
94
|
+
flag: n.flag,
|
|
95
|
+
pinned,
|
|
96
|
+
autoFixed: fixedByRule && !manual,
|
|
97
|
+
ts: n.ts,
|
|
98
|
+
days,
|
|
99
|
+
state: pinned ? 'fixed' : days >= 30 ? 'forgotten' : days >= 7 ? 'fading' : 'fresh',
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
scope: scope || 'global',
|
|
106
|
+
folders: folders.map((f) => ({ k: f.k, name: f.name, parent: f.parent || '', guard: !!f.guard })),
|
|
107
|
+
memories,
|
|
108
|
+
pending: (readJson(join(home, '_pending.json'), []) || []).length,
|
|
109
|
+
at: now,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function saveMemory(scope, folder, id, text) {
|
|
114
|
+
const home = homeOf(scope)
|
|
115
|
+
const rows = readNotes(home, folder)
|
|
116
|
+
const row = rows.find((r) => r.id === id)
|
|
117
|
+
if (!row) return { ok: false, error: '没有这条记忆' }
|
|
118
|
+
const body = String(text || '').replace(/[\r\n|]+/g, ' ').trim()
|
|
119
|
+
if (!body) return { ok: false, error: '内容不能空' }
|
|
120
|
+
row.text = body
|
|
121
|
+
writeNotes(home, folder, rows)
|
|
122
|
+
return { ok: true }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function deleteMemory(scope, folder, id) {
|
|
126
|
+
const home = homeOf(scope)
|
|
127
|
+
const rows = readNotes(home, folder)
|
|
128
|
+
const i = rows.findIndex((r) => r.id === id)
|
|
129
|
+
if (i < 0) return { ok: false, error: '没有这条记忆' }
|
|
130
|
+
try {
|
|
131
|
+
mkdirSync(join(home, '_trash'), { recursive: true })
|
|
132
|
+
const day = new Date().toISOString().slice(0, 10)
|
|
133
|
+
appendFileSync(join(home, '_trash', day + '.md'), `${folder}|${rows[i].ts}|${rows[i].flag || '-'}|${rows[i].text}|sky-delete\n`, 'utf8')
|
|
134
|
+
} catch {}
|
|
135
|
+
rows.splice(i, 1)
|
|
136
|
+
writeNotes(home, folder, rows)
|
|
137
|
+
return { ok: true }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function pinMemory(scope, folder, id, pinned) {
|
|
141
|
+
const home = homeOf(scope)
|
|
142
|
+
const rows = readNotes(home, folder)
|
|
143
|
+
const row = rows.find((r) => r.id === id)
|
|
144
|
+
if (!row) return { ok: false, error: '没有这条记忆' }
|
|
145
|
+
row.flag = pinned ? '*' : ''
|
|
146
|
+
writeNotes(home, folder, rows)
|
|
147
|
+
return { ok: true }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function deleteFolder(scope, k) {
|
|
151
|
+
const home = homeOf(scope)
|
|
152
|
+
const j = readJson(join(home, 'folders.json'), { folders: [] })
|
|
153
|
+
const folders = Array.isArray(j.folders) ? j.folders : []
|
|
154
|
+
const target = folders.find((f) => f.k === k)
|
|
155
|
+
if (!target) return { ok: false, error: '没有这个分区' }
|
|
156
|
+
const all = [target, ...folders.filter((f) => f.parent === k)]
|
|
157
|
+
const sink = join(home, '_trash')
|
|
158
|
+
mkdirSync(sink, { recursive: true })
|
|
159
|
+
const day = new Date().toISOString().slice(0, 10)
|
|
160
|
+
const file = join(sink, day + '.md')
|
|
161
|
+
let moved = 0
|
|
162
|
+
for (const f of all) {
|
|
163
|
+
const rows = readNotes(home, f.k)
|
|
164
|
+
if (rows.length) {
|
|
165
|
+
const head = '\n## 删除分区 ' + f.name + '(' + f.k + ') ' + new Date().toISOString() + '\n'
|
|
166
|
+
appendFileSync(file, head + rows.map((r) => f.k + '|' + r.ts + '|' + (r.flag || '-') + '|' + r.text + '|rm-folder').join('\n') + '\n', 'utf8')
|
|
167
|
+
moved += rows.length
|
|
168
|
+
}
|
|
169
|
+
try { rmSync(join(home, f.k), { recursive: true, force: true }) } catch {}
|
|
170
|
+
}
|
|
171
|
+
const gone = new Set(all.map((f) => f.k))
|
|
172
|
+
j.folders = folders.filter((f) => !gone.has(f.k))
|
|
173
|
+
writeFileSync(join(home, 'folders.json'), JSON.stringify(j, null, 1), 'utf8')
|
|
174
|
+
return { ok: true, k, removed: all.length, moved }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function deleteScope(scope) {
|
|
178
|
+
const id = String(scope || '')
|
|
179
|
+
if (!id.startsWith('ws:')) return { ok: false, error: '全局记忆库不能删' }
|
|
180
|
+
const slug = id.slice(3).replace(/[^A-Za-z0-9_-]/g, '')
|
|
181
|
+
const target = join(WS_ROOT, slug)
|
|
182
|
+
if (!existsSync(target)) return { ok: false, error: '没有这个工作区' }
|
|
183
|
+
const bin = join(WS_ROOT, '.deleted')
|
|
184
|
+
mkdirSync(bin, { recursive: true })
|
|
185
|
+
const stamp = new Date().toISOString().replace(/[:]/g, '-').slice(0, 19)
|
|
186
|
+
renameSync(target, join(bin, slug + '-' + stamp))
|
|
187
|
+
return { ok: true, scope: id }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function renameFolder(scope, k, name) {
|
|
191
|
+
const home = homeOf(scope)
|
|
192
|
+
const clean = String(name || '').replace(/[\r\n|]/g, ' ').trim()
|
|
193
|
+
if (!clean) return { ok: false, error: '名字不能空' }
|
|
194
|
+
const j = readJson(join(home, 'folders.json'), { folders: [] })
|
|
195
|
+
const f = (j.folders || []).find((x) => x.k === k)
|
|
196
|
+
if (!f) return { ok: false, error: '没有这个分区' }
|
|
197
|
+
f.name = clean
|
|
198
|
+
writeFileSync(join(home, 'folders.json'), JSON.stringify(j, null, 1), 'utf8')
|
|
199
|
+
return { ok: true }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const edgeInfo = () => {
|
|
203
|
+
const path = EDGE_CANDIDATES.find((p) => existsSync(p))
|
|
204
|
+
return path ? { found: true, path } : { found: false, path: '' }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function openInEdge(url) {
|
|
208
|
+
const info = edgeInfo()
|
|
209
|
+
if (!info.found) return { ok: false, error: '没找到 Edge' }
|
|
210
|
+
try {
|
|
211
|
+
const child = spawn(info.path, ['--new-window', url], { detached: true, stdio: 'ignore', windowsHide: false })
|
|
212
|
+
child.unref()
|
|
213
|
+
return { ok: true, url }
|
|
214
|
+
} catch (e) {
|
|
215
|
+
return { ok: false, error: e?.message || String(e) }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function installEdge() {
|
|
220
|
+
return new Promise((resolvePromise) => {
|
|
221
|
+
let out = ''
|
|
222
|
+
let child
|
|
223
|
+
try {
|
|
224
|
+
child = spawn('winget', ['install', '--id', 'Microsoft.Edge', '-e', '--silent', '--accept-package-agreements', '--accept-source-agreements'], { windowsHide: true })
|
|
225
|
+
} catch (e) {
|
|
226
|
+
resolvePromise({ ok: false, error: '找不到 winget', out: String(e?.message || e) })
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
child.stdout.on('data', (d) => { out += d.toString() })
|
|
230
|
+
child.stderr.on('data', (d) => { out += d.toString() })
|
|
231
|
+
child.on('error', (e) => resolvePromise({ ok: false, error: e?.message || String(e), out }))
|
|
232
|
+
child.on('close', () => resolvePromise({ ok: edgeInfo().found, out: out.split(/\r?\n/).filter(Boolean).slice(-6).join('\n') }))
|
|
233
|
+
setTimeout(() => { try { child.kill() } catch {} }, 180000)
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const MIME = {
|
|
238
|
+
'.html': 'text/html; charset=utf-8',
|
|
239
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
240
|
+
'.css': 'text/css; charset=utf-8',
|
|
241
|
+
'.json': 'application/json; charset=utf-8',
|
|
242
|
+
'.png': 'image/png',
|
|
243
|
+
'.jpg': 'image/jpeg',
|
|
244
|
+
'.jpeg': 'image/jpeg',
|
|
245
|
+
'.svg': 'image/svg+xml',
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function registerSky(ctx, publicRoot) {
|
|
249
|
+
const json = (res, code, body) => {
|
|
250
|
+
res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
251
|
+
res.end(JSON.stringify(body))
|
|
252
|
+
}
|
|
253
|
+
const readBody = (req) => new Promise((resolvePromise) => {
|
|
254
|
+
let s = ''
|
|
255
|
+
req.on('data', (c) => { s += c; if (s.length > 2e6) req.destroy() })
|
|
256
|
+
req.on('end', () => { try { resolvePromise(JSON.parse(s || '{}')) } catch { resolvePromise({}) } })
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
ctx.webServer.register({
|
|
260
|
+
kind: 'prefix',
|
|
261
|
+
path: '/api/memory-sky',
|
|
262
|
+
handler: async (req, res) => {
|
|
263
|
+
const u = new URL(req.url, 'http://127.0.0.1')
|
|
264
|
+
const p = u.pathname.replace(/^\/api\/memory-sky/, '') || '/'
|
|
265
|
+
const scope = u.searchParams.get('scope') || 'global'
|
|
266
|
+
try {
|
|
267
|
+
if (req.method === 'GET' && p === '/data') return json(res, 200, { ok: true, ...snapshot(scope) })
|
|
268
|
+
if (req.method === 'GET' && p === '/scopes') return json(res, 200, { ok: true, scopes: listScopes() })
|
|
269
|
+
if (req.method === 'GET' && p === '/env') return json(res, 200, { ok: true, edge: edgeInfo(), node: process.versions.node })
|
|
270
|
+
if (req.method === 'POST' && p === '/env/fix') {
|
|
271
|
+
const before = edgeInfo()
|
|
272
|
+
if (before.found) return json(res, 200, { ok: true, already: true, edge: before })
|
|
273
|
+
const r = await installEdge()
|
|
274
|
+
return json(res, 200, { ok: r.ok, out: r.out || r.error || '', edge: edgeInfo() })
|
|
275
|
+
}
|
|
276
|
+
if (req.method === 'POST' && p === '/open') {
|
|
277
|
+
const host = req.headers.host || '127.0.0.1'
|
|
278
|
+
return json(res, 200, openInEdge(`http://${host}/api/memory-sky/?scope=${encodeURIComponent(scope)}`))
|
|
279
|
+
}
|
|
280
|
+
if (req.method === 'POST' && p === '/save') {
|
|
281
|
+
const b = await readBody(req)
|
|
282
|
+
return json(res, 200, saveMemory(b.scope || scope, String(b.folder), String(b.id), b.text))
|
|
283
|
+
}
|
|
284
|
+
if (req.method === 'POST' && p === '/del') {
|
|
285
|
+
const b = await readBody(req)
|
|
286
|
+
return json(res, 200, deleteMemory(b.scope || scope, String(b.folder), String(b.id)))
|
|
287
|
+
}
|
|
288
|
+
if (req.method === 'POST' && p === '/pin') {
|
|
289
|
+
const b = await readBody(req)
|
|
290
|
+
return json(res, 200, pinMemory(b.scope || scope, String(b.folder), String(b.id), !!b.pinned))
|
|
291
|
+
}
|
|
292
|
+
if (req.method === 'POST' && p === '/folder') {
|
|
293
|
+
const b = await readBody(req)
|
|
294
|
+
return json(res, 200, renameFolder(b.scope || scope, String(b.k), b.name))
|
|
295
|
+
}
|
|
296
|
+
if (req.method === 'POST' && p === '/folder/rm') {
|
|
297
|
+
const b = await readBody(req)
|
|
298
|
+
return json(res, 200, deleteFolder(b.scope || scope, String(b.k)))
|
|
299
|
+
}
|
|
300
|
+
if (p === '/classify' && req.method === 'GET') {
|
|
301
|
+
const task = readJson(join(homeOf('global'), '_classify.json'), null)
|
|
302
|
+
return json(res, 200, { ok: true, pending: !!task, at: (task && task.at) || 0 })
|
|
303
|
+
}
|
|
304
|
+
if (p === '/classify' && req.method === 'POST') {
|
|
305
|
+
const home = homeOf('global')
|
|
306
|
+
mkdirSync(home, { recursive: true })
|
|
307
|
+
writeFileSync(join(home, '_classify.json'), JSON.stringify({ at: Date.now() }, null, 1), 'utf8')
|
|
308
|
+
return json(res, 200, { ok: true })
|
|
309
|
+
}
|
|
310
|
+
if (p === '/classify/cancel' && req.method === 'POST') {
|
|
311
|
+
try { rmSync(join(homeOf('global'), '_classify.json'), { force: true }) } catch {}
|
|
312
|
+
return json(res, 200, { ok: true })
|
|
313
|
+
}
|
|
314
|
+
if (req.method === 'POST' && p === '/scope/rm') {
|
|
315
|
+
const b = await readBody(req)
|
|
316
|
+
return json(res, 200, deleteScope(b.scope))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const rel = p === '/' ? 'index.html' : p.replace(/^\/+/, '')
|
|
320
|
+
const file = resolve(publicRoot, rel)
|
|
321
|
+
if (!file.startsWith(resolve(publicRoot))) return json(res, 403, { ok: false, error: 'forbidden' })
|
|
322
|
+
if (!existsSync(file)) return json(res, 404, { ok: false, error: 'not found' })
|
|
323
|
+
res.writeHead(200, { 'content-type': MIME[extname(file)] || 'application/octet-stream', 'cache-control': 'no-store' })
|
|
324
|
+
res.end(readFileSync(file))
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return json(res, 500, { ok: false, error: e?.message || String(e) })
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export { basename }
|