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/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 }