memory-eternal 0.15.0 → 0.17.0
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/.claude-plugin/plugin.json +27 -20
- package/.codex-plugin/plugin.json +24 -24
- package/.cursor-plugin/plugin.json +14 -14
- package/README.md +138 -654
- package/README.zh-CN.md +138 -0
- package/bin/dsh-memory.mjs +33 -3
- package/cursor-extension/package.json +54 -54
- package/dsh-tui-plugin/README.md +63 -3
- package/dsh-tui-plugin/demo.mjs +113 -25
- package/dsh-tui-plugin/dsh-plugin.json +42 -42
- package/dsh-tui-plugin/main.js +65 -31
- package/dsh-tui-plugin/package.json +51 -31
- package/dsh-tui-plugin/scene.js +125 -16
- package/index.js +28 -3
- package/lib/api.js +89 -14
- package/lib/capture-run.js +35 -8
- package/lib/capture.js +25 -2
- package/lib/client.js +116 -20
- package/lib/db.js +19 -0
- package/lib/ledger.js +107 -0
- package/lib/mcp.js +8 -2
- package/lib/setup.js +67 -5
- package/orca-plugin/orca-plugin.json +36 -36
- package/package.json +7 -4
- package/scripts/release.mjs +5 -4
- package/web/app.js +121 -25
- package/web/index.html +14 -0
- package/README.en.md +0 -621
package/dsh-tui-plugin/demo.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Memory Atlas TUI —— 完整交互原型
|
|
3
3
|
// node dsh-tui-plugin/demo.mjs
|
|
4
4
|
// 静态帧:node dsh-tui-plugin/demo.mjs --once --no-color
|
|
5
|
+
// 定位列表:node dsh-tui-plugin/demo.mjs --once --no-color --screen=library --index=900
|
|
5
6
|
// 全选预览:node dsh-tui-plugin/demo.mjs --once --no-color --select-all
|
|
6
7
|
//
|
|
7
8
|
// 示例数据,不连接真实 vault,不写任何卡片。
|
|
@@ -179,6 +180,8 @@ const writeMode = argv.has('--write')
|
|
|
179
180
|
const API = process.env.MEMORY_ETERNAL_API || 'http://127.0.0.1:7999/memory-eternal/api'
|
|
180
181
|
const screenArg = (process.argv.find((a) => a.startsWith('--screen=')) || '').split('=')[1]
|
|
181
182
|
const scrollArg = Number((process.argv.find((a) => a.startsWith('--scroll=')) || '').split('=')[1] || 0)
|
|
183
|
+
// --index=N:设定初始选中项;列表窗口会跟着它滚动,便于自动化验收位置
|
|
184
|
+
const indexArg = Number((process.argv.find((a) => a.startsWith('--index=')) || '').split('=')[1])
|
|
182
185
|
const useColor = !argv.has('--no-color') && !process.env.NO_COLOR
|
|
183
186
|
|
|
184
187
|
const sgr = (code, text) => (useColor ? code + text + C.reset : text)
|
|
@@ -227,13 +230,55 @@ const wrap = (text, n) => {
|
|
|
227
230
|
function panel(title, lines, w, h, accent = C.border) {
|
|
228
231
|
const inner = Math.max(1, w - 2)
|
|
229
232
|
const t = ' ' + title + ' '
|
|
230
|
-
|
|
233
|
+
// 顶框总宽必须等于内框 + 左右边框(= w):标题两侧各多占 1 列,否则右边会多出 1 列
|
|
234
|
+
const top = sgr(accent, '╭─') + sgr(accent, t) + sgr(accent, '─'.repeat(Math.max(0, inner - width(t) - 1)) + '╮')
|
|
231
235
|
const bottom = sgr(accent, '╰' + '─'.repeat(inner) + '╯')
|
|
232
236
|
const body = []
|
|
233
237
|
for (let i = 0; i < h - 2; i++) body.push(sgr(C.border, '│') + pad(clip(lines[i] || '', inner), inner) + sgr(C.border, '│'))
|
|
234
238
|
return [top, ...body, bottom].join('\n')
|
|
235
239
|
}
|
|
236
240
|
|
|
241
|
+
// ── 列表窗口 + 滚动条 ────────────────────────────────────────────────
|
|
242
|
+
// 记忆库 / 待审队列不再全量渲染:只渲染 listTop 起的一屏,右侧 1 列画
|
|
243
|
+
// 滚动条(滑轨 │ / 滑块 █),并保证 state.index 始终落在窗口内。
|
|
244
|
+
const SCROLL_THUMB = '█'
|
|
245
|
+
const SCROLL_TRACK = '│'
|
|
246
|
+
|
|
247
|
+
function windowInfo(state, total, visible) {
|
|
248
|
+
const count = Math.max(0, Number.isFinite(total) ? Math.floor(total) : 0)
|
|
249
|
+
const vis = Math.max(1, Number.isFinite(visible) ? Math.floor(visible) : 1)
|
|
250
|
+
let index = Math.max(0, Math.min(Math.floor(Number(state.index) || 0), Math.max(0, count - 1)))
|
|
251
|
+
let top = Math.max(0, Math.floor(Number(state.listTop) || 0))
|
|
252
|
+
if (index < top) top = index
|
|
253
|
+
else if (index >= top + vis) top = index - vis + 1
|
|
254
|
+
top = Math.min(top, Math.max(0, count - vis))
|
|
255
|
+
state.index = index
|
|
256
|
+
state.listTop = top
|
|
257
|
+
return { total: count, visible: vis, index, top, end: Math.min(count, top + vis) }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function scrollChar(row, rows, view) {
|
|
261
|
+
if (!view || view.total <= view.visible || rows <= 0) return ' '
|
|
262
|
+
const thumbH = Math.min(rows, Math.max(1, Math.round((view.visible / view.total) * rows)))
|
|
263
|
+
const maxTop = Math.max(1, view.total - view.visible)
|
|
264
|
+
const pos = Math.round((Math.min(view.top, maxTop) / maxTop) * (rows - thumbH))
|
|
265
|
+
return row >= pos && row < pos + thumbH ? sgr(C.ember, SCROLL_THUMB) : sgr(C.faint, SCROLL_TRACK)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// panel 会把每行 clip/pad 到 inner 宽度:这里先把正文压到 inner-1,再补滚动条列,
|
|
269
|
+
// 所以右边框不会被挤掉;不需要滚动时该列留空。
|
|
270
|
+
function scrollPanel(title, lines, w, h, accent, view) {
|
|
271
|
+
const inner = Math.max(1, w - 2)
|
|
272
|
+
const rows = Math.max(0, h - 2)
|
|
273
|
+
if (!view) return panel(title, lines, w, h, accent)
|
|
274
|
+
const cw = Math.max(1, inner - 1)
|
|
275
|
+
const composed = []
|
|
276
|
+
for (let i = 0; i < rows; i++) {
|
|
277
|
+
composed.push(pad(clip(lines[i] || '', cw), cw) + scrollChar(i, rows, view))
|
|
278
|
+
}
|
|
279
|
+
return panel(title, composed, w, h, accent)
|
|
280
|
+
}
|
|
281
|
+
|
|
237
282
|
const kindOf = (c) => KIND[c.kind] || KIND.project
|
|
238
283
|
const filtered = (state) => {
|
|
239
284
|
const q = state.query.trim().toLowerCase()
|
|
@@ -261,14 +306,18 @@ function searchRow(state, w) {
|
|
|
261
306
|
return ' ' + sgr(C.faint, '/ ') + sgr(state.searchMode ? C.text : C.faint, text) + ' ' + sgr(C.faint, 'Enter 确认 · Esc 清空')
|
|
262
307
|
}
|
|
263
308
|
|
|
264
|
-
function queueLines(cards, state, w) {
|
|
309
|
+
function queueLines(cards, state, w, view) {
|
|
265
310
|
if (!cards.length) return ['', sgr(C.green, ' ✓ 没有匹配的卡片'), '', sgr(C.dim, ' 按 Esc 清空搜索 / 筛选')]
|
|
266
311
|
const lines = []
|
|
267
312
|
const chosen = cards.filter((c) => state.marked.has(c.id)).length
|
|
268
313
|
const all = cards.length > 0 && chosen === cards.length
|
|
269
314
|
lines.push(` ${all ? sgr(C.ember, '▣') : sgr(C.faint, '☐')} ${sgr(C.dim, all ? '反选' : '全选')} ${cards.length} ${chosen ? sgr(C.amber, '已选 ' + chosen) : sgr(C.faint, '已选 0')} ${keycap('s', all ? '反选' : '全选')}`)
|
|
270
315
|
lines.push('')
|
|
271
|
-
|
|
316
|
+
// 只渲染窗口内的项:view.top..view.end(0 张 / 全量时退化为原行为)
|
|
317
|
+
const from = view ? view.top : 0
|
|
318
|
+
const to = view ? view.end : cards.length
|
|
319
|
+
for (let i = from; i < to; i++) {
|
|
320
|
+
const card = cards[i]
|
|
272
321
|
const k = kindOf(card)
|
|
273
322
|
const selected = i === state.index
|
|
274
323
|
const marked = state.marked.has(card.id)
|
|
@@ -282,7 +331,7 @@ function queueLines(cards, state, w) {
|
|
|
282
331
|
if (selected) meta = tone(pad(clip(meta, w), w), '', C.selectedBg)
|
|
283
332
|
lines.push(clip(meta, w))
|
|
284
333
|
lines.push('')
|
|
285
|
-
}
|
|
334
|
+
}
|
|
286
335
|
return lines
|
|
287
336
|
}
|
|
288
337
|
|
|
@@ -313,11 +362,17 @@ function previewLines(card, w) {
|
|
|
313
362
|
|
|
314
363
|
function auditView(state, w, h) {
|
|
315
364
|
const cards = filtered(state)
|
|
316
|
-
const selected = cards[state.index]
|
|
317
365
|
const qw = Math.max(40, Math.floor((w - 1) * 0.4))
|
|
318
366
|
const pw = w - qw - 1
|
|
319
367
|
const ph = h - 2
|
|
320
|
-
|
|
368
|
+
// 队列面板内高 = ph-2;前 2 行是全选/已选说明,其余按每卡 3 行开窗
|
|
369
|
+
const rows = Math.max(0, ph - 2)
|
|
370
|
+
const visible = Math.max(1, Math.floor(Math.max(0, rows - 2) / 3) || 1)
|
|
371
|
+
const view = windowInfo(state, cards.length, visible)
|
|
372
|
+
state.listPage = view.visible
|
|
373
|
+
const selected = cards[view.index]
|
|
374
|
+
const pos = cards.length ? ` · ${view.top + 1}-${view.end}/${cards.length}` : ''
|
|
375
|
+
const q = scrollPanel(`队列 ${cards.length}${state.marked.size ? ' ◉' + state.marked.size : ''}${pos}`, queueLines(cards, state, qw - 3, view), qw, ph, C.border, view)
|
|
321
376
|
const p = panel('判词', previewLines(selected, pw - 2), pw, ph, C.ember)
|
|
322
377
|
const ql = q.split('\n'), pl = p.split('\n')
|
|
323
378
|
const out = [typeRow(state, w), '']
|
|
@@ -336,19 +391,33 @@ function cardRow(card, selected, w, compact) {
|
|
|
336
391
|
|
|
337
392
|
function libraryView(state, w, h) {
|
|
338
393
|
const cards = filtered(state)
|
|
339
|
-
const
|
|
394
|
+
const inner = Math.max(1, w - 2)
|
|
395
|
+
// 右侧留 1 列给滚动条:正文宽度 = 内框宽度 - 1
|
|
396
|
+
const cw = Math.max(1, inner - 1)
|
|
397
|
+
const bodyRows = Math.max(0, h - 2)
|
|
340
398
|
const feature = cards[0] || CARDS[0]
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
399
|
+
const header = [
|
|
400
|
+
typeRow(state, cw), '',
|
|
401
|
+
searchRow(state, cw), '',
|
|
402
|
+
`${sgr(C.ember, '✦ 今日回看')} ${sgr(C.text, feature.title)}`,
|
|
403
|
+
` ${sgr(C.dim, feature.summary)}`,
|
|
404
|
+
` ${feature.tags.map((t) => tone(' #' + t + ' ', C.ember, C.chipBg)).join(' ')}`,
|
|
405
|
+
'', sgr(C.border, '─'.repeat(Math.max(1, cw))), '',
|
|
406
|
+
`${sgr(C.dim, '全部记忆')} ${sgr(C.faint, cards.length + ' 张 · 按 Enter 读卡')}`,
|
|
407
|
+
'',
|
|
408
|
+
]
|
|
409
|
+
const listRows = Math.max(0, bodyRows - header.length)
|
|
410
|
+
// 每张卡固定 2 行正文 + 1 行空行;只渲染这一屏
|
|
411
|
+
const visible = Math.max(1, Math.floor(listRows / 3) || 1)
|
|
412
|
+
const view = windowInfo(state, cards.length, visible)
|
|
413
|
+
state.listPage = view.visible
|
|
414
|
+
const lines = header.slice()
|
|
415
|
+
for (let i = view.top; i < view.end; i++) {
|
|
416
|
+
lines.push(...cardRow(cards[i], i === view.index, cw, true))
|
|
349
417
|
lines.push('')
|
|
350
|
-
}
|
|
351
|
-
|
|
418
|
+
}
|
|
419
|
+
const title = cards.length ? `记忆库 · ${view.top + 1}-${view.end}/${cards.length}` : '记忆库 · 0/0'
|
|
420
|
+
return [scrollPanel(title, lines, w, h, C.border, view)]
|
|
352
421
|
}
|
|
353
422
|
|
|
354
423
|
function readerView(state, w, h) {
|
|
@@ -478,6 +547,8 @@ function helpView(w, h) {
|
|
|
478
547
|
` ${keycap('1')}–${keycap('6')} 记忆库 / 待审 / 星图 / 状态 / 回收 / 设置`,
|
|
479
548
|
` ${keycap('Tab')} 切换页面`,
|
|
480
549
|
` ${keycap('↑↓')} ${keycap('jk')} 移动`,
|
|
550
|
+
` ${keycap('PgUp/PgDn')} 列表翻页(记忆库 / 待审)`,
|
|
551
|
+
` ${keycap('g/G')} 列表首 / 尾`,
|
|
481
552
|
` ${keycap('←→')} ${keycap('t')} 切换类型`,
|
|
482
553
|
` ${keycap('Enter')} 读卡 / 切换设置`,
|
|
483
554
|
` ${keycap('Space')} 多选 / 切换设置`,
|
|
@@ -501,16 +572,19 @@ function footer(state, w) {
|
|
|
501
572
|
if (state.screen === 'settings') return ' ' + [keycap('↑↓', '移动'), keycap('Enter', '切换'), keycap('Space', '切换'), keycap('Esc', '返回')].join(' ')
|
|
502
573
|
if (state.screen === 'recycle') return ' ' + [keycap('↑↓', '移动'), keycap('r', '恢复'), keycap('d', '删除')].join(' ')
|
|
503
574
|
if (state.screen === 'audit' && state.marked.size) return ' ' + [keycap('a', '批准 ' + state.marked.size), keycap('r', '驳回'), keycap('s', '反选'), keycap('Esc', '清空')].join(' ')
|
|
504
|
-
if (state.screen === 'audit') return ' ' + [keycap('↑↓', '移动'), keycap('←→', '类型'), keycap('Space', '多选'), keycap('s', '全选'), keycap('a', '批准'), keycap('r', '驳回'), keycap('Enter', '读卡'), keycap('1-6', '页面'), keycap('?', '帮助')].join(' ')
|
|
505
|
-
if (state.screen === 'library') return ' ' + [keycap('↑↓', '移动'), keycap('←→', '类型'), keycap('/', '搜索'), keycap('Enter', '读卡'), keycap('1-6', '页面')].join(' ')
|
|
575
|
+
if (state.screen === 'audit') return ' ' + [keycap('↑↓', '移动'), keycap('PgUp/PgDn', '翻页'), keycap('g/G', '首/尾'), keycap('←→', '类型'), keycap('Space', '多选'), keycap('s', '全选'), keycap('a', '批准'), keycap('r', '驳回'), keycap('Enter', '读卡'), keycap('1-6', '页面'), keycap('?', '帮助')].join(' ')
|
|
576
|
+
if (state.screen === 'library') return ' ' + [keycap('↑↓', '移动'), keycap('PgUp/PgDn', '翻页'), keycap('g/G', '首/尾'), keycap('←→', '类型'), keycap('/', '搜索'), keycap('Enter', '读卡'), keycap('1-6', '页面')].join(' ')
|
|
506
577
|
if (state.screen === 'graph') return ' ' + [keycap('↑↓', '切换卡片'), keycap('Enter', '读卡'), keycap('1-6', '页面')].join(' ')
|
|
507
578
|
if (state.screen === 'status') return ' ' + [keycap('1-6', '页面'), keycap('Tab', '切换'), keycap('?', '帮助')].join(' ')
|
|
508
579
|
return ' ' + [keycap('1-6', '页面'), keycap('Tab', '切换'), keycap('?', '帮助')].join(' ')
|
|
509
580
|
}
|
|
510
581
|
|
|
511
582
|
function frame(state) {
|
|
512
|
-
|
|
513
|
-
const
|
|
583
|
+
// 非 TTY(管道 / --once 重定向)时退化到 COLUMNS/LINES,便于自动化验收终端尺寸
|
|
584
|
+
const cols = Number(process.stdout.columns) || Number(process.env.COLUMNS) || 0
|
|
585
|
+
const rowsRaw = Number(process.stdout.rows) || Number(process.env.LINES) || 0
|
|
586
|
+
const w = Math.max(72, Math.min(useColor ? 140 : 120, cols || 120))
|
|
587
|
+
const rows = Math.max(24, rowsRaw || 36)
|
|
514
588
|
const screen = state.help ? 'help' : state.screen
|
|
515
589
|
const label = screen === 'help' ? '帮助' : SCREENS[screen]?.label || '记忆库'
|
|
516
590
|
const totalNow = state.screen === 'library' ? (state.libraryCards || state.cards).length : state.cards.length
|
|
@@ -615,6 +689,7 @@ async function loadLive(state) {
|
|
|
615
689
|
path: c.path,
|
|
616
690
|
}))
|
|
617
691
|
state.index = 0
|
|
692
|
+
state.listTop = 0
|
|
618
693
|
state.marked.clear()
|
|
619
694
|
if (selectAllAtStart) state.cards.forEach((c) => state.marked.add(c.id))
|
|
620
695
|
if (!state.cards.length) {
|
|
@@ -639,7 +714,12 @@ async function main() {
|
|
|
639
714
|
settings: SETTINGS.map((s) => ({ ...s })), settingsIndex: 0, help: screenArg === 'help', toast: null,
|
|
640
715
|
live: false, liveError: '', overview: null, libraryCards: CARDS.slice(), graphData: null, graphIndex: 0,
|
|
641
716
|
}
|
|
717
|
+
state.initialIndex = Number.isFinite(indexArg) && indexArg > 0 ? Math.floor(indexArg) : 0
|
|
718
|
+
state.listTop = 0
|
|
719
|
+
state.listPage = 1
|
|
642
720
|
if (liveMode) await loadLive(state)
|
|
721
|
+
// --index=N 在实时数据加载后仍要生效(loadLive 内部会把 index 归零)
|
|
722
|
+
if (state.initialIndex) state.index = state.initialIndex
|
|
643
723
|
if (once) { draw(state); return }
|
|
644
724
|
if (!process.stdout.isTTY) { process.stderr.write('请在有 TTY 的终端里运行;预览用 --once\n'); process.exitCode = 1; return }
|
|
645
725
|
|
|
@@ -695,6 +775,7 @@ async function main() {
|
|
|
695
775
|
const ids = new Set(targets.map((c) => c.id))
|
|
696
776
|
state.cards = state.cards.filter((c) => !ids.has(c.id))
|
|
697
777
|
state.index = 0
|
|
778
|
+
state.listTop = 0
|
|
698
779
|
state.marked.clear()
|
|
699
780
|
toast(`${decision === 'a' ? '✓ 已批准' : '✕ 已驳回'} ${targets.length} 张:${targets[0].title}`, decision === 'a')
|
|
700
781
|
}
|
|
@@ -725,6 +806,7 @@ async function main() {
|
|
|
725
806
|
const i = ids.indexOf(state.screen === 'reader' ? state.readerBack : state.screen)
|
|
726
807
|
state.screen = ids[(i + dir + ids.length) % ids.length]
|
|
727
808
|
state.index = 0
|
|
809
|
+
state.listTop = 0
|
|
728
810
|
draw(state)
|
|
729
811
|
}
|
|
730
812
|
|
|
@@ -745,7 +827,7 @@ async function main() {
|
|
|
745
827
|
if (key.name === 'tab') { cycle(key.shift ? -1 : 1); return }
|
|
746
828
|
if (/^[1-6]$/.test(str)) {
|
|
747
829
|
const id = ['library', 'audit', 'graph', 'status', 'recycle', 'settings'][Number(str) - 1]
|
|
748
|
-
state.screen = id; state.index = 0; draw(state); return
|
|
830
|
+
state.screen = id; state.index = 0; state.listTop = 0; draw(state); return
|
|
749
831
|
}
|
|
750
832
|
if (state.screen === 'reader') {
|
|
751
833
|
if (key.name === 'escape') { state.screen = state.readerBack; draw(state); return }
|
|
@@ -790,22 +872,28 @@ async function main() {
|
|
|
790
872
|
if (str === 't' || key.name === 'right' || str === 'l') {
|
|
791
873
|
const keys = ['all', 'project', 'knowledge', 'mistake', 'content', 'tool', 'prompt']
|
|
792
874
|
state.kind = keys[(keys.indexOf(state.kind) + 1) % keys.length]
|
|
793
|
-
state.index = 0; state.marked.clear(); draw(state); return
|
|
875
|
+
state.index = 0; state.listTop = 0; state.marked.clear(); draw(state); return
|
|
794
876
|
}
|
|
795
877
|
if (key.name === 'left' || str === 'h') {
|
|
796
878
|
const keys = ['all', 'project', 'knowledge', 'mistake', 'content', 'tool', 'prompt']
|
|
797
879
|
state.kind = keys[(keys.indexOf(state.kind) - 1 + keys.length) % keys.length]
|
|
798
|
-
state.index = 0; state.marked.clear(); draw(state); return
|
|
880
|
+
state.index = 0; state.listTop = 0; state.marked.clear(); draw(state); return
|
|
799
881
|
}
|
|
800
882
|
if (state.screen === 'audit' && (str === 's' || (key.ctrl && key.name === 'a'))) {
|
|
801
883
|
const all = cards.length > 0 && cards.every((c) => state.marked.has(c.id))
|
|
802
884
|
cards.forEach((c) => all ? state.marked.delete(c.id) : state.marked.add(c.id))
|
|
803
885
|
draw(state); return
|
|
804
886
|
}
|
|
887
|
+
// 列表翻页:一次走一屏(state.listPage 由 libraryView / auditView 渲染时写入)
|
|
888
|
+
const pageStep = Math.max(1, state.listPage || 1)
|
|
889
|
+
if (key.name === 'pageup') { state.index = Math.max(0, state.index - pageStep); draw(state); return }
|
|
890
|
+
if (key.name === 'pagedown') { state.index = Math.min(Math.max(0, cards.length - 1), state.index + pageStep); draw(state); return }
|
|
891
|
+
if (str === 'g') { state.index = 0; draw(state); return }
|
|
892
|
+
if (str === 'G') { state.index = Math.max(0, cards.length - 1); draw(state); return }
|
|
805
893
|
if (key.name === 'up' || str === 'k') { state.index = Math.max(0, state.index - 1); draw(state); return }
|
|
806
894
|
if (key.name === 'down' || str === 'j') { state.index = Math.min(Math.max(0, cards.length - 1), state.index + 1); draw(state); return }
|
|
807
895
|
if (key.name === 'return' || key.name === 'enter') { openReader(); return }
|
|
808
|
-
if (key.name === 'escape') { state.query = ''; state.kind = 'all'; state.marked.clear(); state.index = 0; draw(state); return }
|
|
896
|
+
if (key.name === 'escape') { state.query = ''; state.kind = 'all'; state.marked.clear(); state.index = 0; state.listTop = 0; draw(state); return }
|
|
809
897
|
if (state.screen === 'audit' && (key.name === 'space' || str === ' ')) {
|
|
810
898
|
const c = cards[state.index]; if (c) { state.marked.has(c.id) ? state.marked.delete(c.id) : state.marked.add(c.id) } draw(state); return
|
|
811
899
|
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "urn:dsh-std:community-draft:dsh-plugin:0.15",
|
|
3
|
-
"id": "memory-eternal-tui",
|
|
4
|
-
"name": "memory-eternal-tui",
|
|
5
|
-
"version": "0.
|
|
6
|
-
"manifestVersion": "0.15",
|
|
7
|
-
"description": "Open Memory Atlas from dsh-TUI: native /audit scene plus browser launcher.",
|
|
8
|
-
"facets": {
|
|
9
|
-
"host": {
|
|
10
|
-
"entry": "main.js",
|
|
11
|
-
"apiVersion": "v1alpha1"
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
"requires": {
|
|
15
|
-
"contracts": [
|
|
16
|
-
{
|
|
17
|
-
"apiVersion": "commands.dsh/v1alpha1",
|
|
18
|
-
"kind": "Command"
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
|
-
"permissions": [],
|
|
23
|
-
"contributes": {
|
|
24
|
-
"commands": [
|
|
25
|
-
{
|
|
26
|
-
"id": "memory-eternal-tui.open",
|
|
27
|
-
"title": "memory",
|
|
28
|
-
"description": "Open Memory Atlas in the browser"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"id": "memory-eternal-tui.audit",
|
|
32
|
-
"title": "audit",
|
|
33
|
-
"description": "Open the native Memory Atlas audit scene"
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
},
|
|
37
|
-
"subscriptions": [],
|
|
38
|
-
"license": "MIT",
|
|
39
|
-
"source": {
|
|
40
|
-
"repository": "https://github.com/EternalNight996/memory-eternal"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"$schema": "urn:dsh-std:community-draft:dsh-plugin:0.15",
|
|
3
|
+
"id": "memory-eternal-tui",
|
|
4
|
+
"name": "memory-eternal-tui",
|
|
5
|
+
"version": "0.17.0",
|
|
6
|
+
"manifestVersion": "0.15",
|
|
7
|
+
"description": "Open Memory Atlas from dsh-TUI: native /audit scene plus browser launcher.",
|
|
8
|
+
"facets": {
|
|
9
|
+
"host": {
|
|
10
|
+
"entry": "main.js",
|
|
11
|
+
"apiVersion": "v1alpha1"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"requires": {
|
|
15
|
+
"contracts": [
|
|
16
|
+
{
|
|
17
|
+
"apiVersion": "commands.dsh/v1alpha1",
|
|
18
|
+
"kind": "Command"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"permissions": [],
|
|
23
|
+
"contributes": {
|
|
24
|
+
"commands": [
|
|
25
|
+
{
|
|
26
|
+
"id": "memory-eternal-tui.open",
|
|
27
|
+
"title": "memory",
|
|
28
|
+
"description": "Open Memory Atlas in the browser"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "memory-eternal-tui.audit",
|
|
32
|
+
"title": "audit",
|
|
33
|
+
"description": "Open the native Memory Atlas audit scene"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"subscriptions": [],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"source": {
|
|
40
|
+
"repository": "https://github.com/EternalNight996/memory-eternal"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dsh-tui-plugin/main.js
CHANGED
|
@@ -41,17 +41,72 @@ function openMemory() {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function apply(ctx) {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// ── 为什么不能「拿不到 tuiPluginHost 就直接 return」────────────────────────
|
|
45
|
+
// tuiPluginHost / tuiScenes 都是**后挂载**的 TUI seam:cordis 只保证插件被拉起来,
|
|
46
|
+
// 不保证它激活时宿主 seam 已经就绪。旧实现 `const host = ctx.get('tuiPluginHost', false); if (!host) return`
|
|
47
|
+
// 一旦抢跑就永久放弃 —— 表现为命令面板里根本没有 /memory、/audit(issue #50 用户实测)。
|
|
48
|
+
// 参照 dsh-tui-find(同宿主同 API):软探测 + 50ms 小步轮询,挂上再注册;10s 还没挂
|
|
49
|
+
// 就安静收手(确实没有 TUI 运行时的组合不该报错)。
|
|
50
|
+
const HOST_POLL_MS = 50
|
|
51
|
+
const HOST_MAX_ATTEMPTS = 200 // ≈10s
|
|
52
|
+
let disposed = false
|
|
53
|
+
let hostTimer = null
|
|
54
|
+
let hostAttempts = 0
|
|
55
|
+
let hostDisposer = null
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
const probe = (name) => {
|
|
58
|
+
try { return ctx.get(name, false) } catch { return false }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const registerCommands = (host) => {
|
|
62
|
+
const disposers = []
|
|
63
|
+
disposers.push(host.registerCommand(ctx, COMMAND_ID, {
|
|
64
|
+
recordInput: false,
|
|
65
|
+
handler: () => {
|
|
66
|
+
openMemory()
|
|
67
|
+
return { kind: 'success', text: `Memory Atlas 已打开:${URL}` }
|
|
68
|
+
},
|
|
69
|
+
}))
|
|
70
|
+
disposers.push(host.registerCommand(ctx, AUDIT_COMMAND_ID, {
|
|
71
|
+
recordInput: false,
|
|
72
|
+
handler: () => {
|
|
73
|
+
const runtime = probe('tuiScenes')
|
|
74
|
+
if (!runtime) return { kind: 'error', text: '当前 dsh-tui 没有 scenes 运行时' }
|
|
75
|
+
if (runtime.active?.id === AUDIT_SCENE_ID) runtime.close()
|
|
76
|
+
const opened = runtime.open(AUDIT_SCENE_ID)
|
|
77
|
+
return opened
|
|
78
|
+
? { kind: 'success', text: 'Memory Atlas / 待审 已打开' }
|
|
79
|
+
: { kind: 'error', text: '审核场景未注册' }
|
|
80
|
+
},
|
|
81
|
+
}))
|
|
82
|
+
return () => { for (const d of disposers) { try { if (typeof d === 'function') d() } catch { /* 卸载失败不影响其他 */ } } }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const tryRegisterCommands = () => {
|
|
86
|
+
hostTimer = null
|
|
87
|
+
if (disposed) return
|
|
88
|
+
const host = probe('tuiPluginHost')
|
|
89
|
+
if (!host) {
|
|
90
|
+
hostAttempts += 1
|
|
91
|
+
if (hostAttempts < HOST_MAX_ATTEMPTS) hostTimer = setTimeout(tryRegisterCommands, HOST_POLL_MS)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
hostDisposer = registerCommands(host)
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn('[memory-eternal-tui] command registration failed:', error)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
tryRegisterCommands()
|
|
101
|
+
|
|
102
|
+
// tuiScenes 同样可能晚于本插件挂载:沿用原来的软探测 + 50ms 重试(最多 ~2s)。
|
|
49
103
|
let sceneDisposer = null
|
|
50
104
|
let sceneTimer = null
|
|
51
105
|
let attempts = 0
|
|
52
106
|
const registerScene = () => {
|
|
53
107
|
sceneTimer = null
|
|
54
|
-
|
|
108
|
+
if (disposed) return
|
|
109
|
+
const scenes = probe('tuiScenes')
|
|
55
110
|
if (scenes && !sceneDisposer) {
|
|
56
111
|
try {
|
|
57
112
|
sceneDisposer = scenes.register({ id: AUDIT_SCENE_ID, title: 'Memory Atlas / 待审', component: AuditScene }, ctx)
|
|
@@ -65,33 +120,12 @@ export function apply(ctx) {
|
|
|
65
120
|
}
|
|
66
121
|
}
|
|
67
122
|
registerScene()
|
|
68
|
-
|
|
123
|
+
|
|
124
|
+
ctx.effect(() => () => {
|
|
125
|
+
disposed = true
|
|
126
|
+
if (hostTimer) clearTimeout(hostTimer)
|
|
69
127
|
if (sceneTimer) clearTimeout(sceneTimer)
|
|
128
|
+
if (hostDisposer) hostDisposer()
|
|
70
129
|
if (sceneDisposer) sceneDisposer()
|
|
71
130
|
})
|
|
72
|
-
|
|
73
|
-
const dispose = host.registerCommand(ctx, COMMAND_ID, {
|
|
74
|
-
recordInput: false,
|
|
75
|
-
handler: () => {
|
|
76
|
-
openMemory()
|
|
77
|
-
return { kind: 'success', text: `Memory Atlas 已打开:${URL}` }
|
|
78
|
-
},
|
|
79
|
-
})
|
|
80
|
-
ctx.effect(() => dispose)
|
|
81
|
-
|
|
82
|
-
const disposeAudit = host.registerCommand(ctx, AUDIT_COMMAND_ID, {
|
|
83
|
-
recordInput: false,
|
|
84
|
-
handler: () => {
|
|
85
|
-
const runtime = ctx.get('tuiScenes', false)
|
|
86
|
-
if (!runtime) return { kind: 'error', text: '当前 dsh-tui 没有 scenes 运行时' }
|
|
87
|
-
if (runtime.active?.id === AUDIT_SCENE_ID) runtime.close()
|
|
88
|
-
const opened = runtime.open(AUDIT_SCENE_ID)
|
|
89
|
-
return opened
|
|
90
|
-
? { kind: 'success', text: 'Memory Atlas / 待审 已打开' }
|
|
91
|
-
: { kind: 'error', text: '审核场景未注册' }
|
|
92
|
-
},
|
|
93
|
-
})
|
|
94
|
-
ctx.effect(() => disposeAudit)
|
|
95
131
|
}
|
|
96
|
-
|
|
97
|
-
export default { name, inject, apply }
|
|
@@ -1,31 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "memory-eternal-tui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "main.js",
|
|
7
|
-
"files": [
|
|
8
|
-
"main.js",
|
|
9
|
-
"scene.js",
|
|
10
|
-
"demo.mjs",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"dsh
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "memory-eternal-tui",
|
|
3
|
+
"version": "0.17.0",
|
|
4
|
+
"description": "Memory Atlas for dsh-TUI: /audit scene with reason/severity, batch confirmation and reject reasons.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "main.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"main.js",
|
|
9
|
+
"scene.js",
|
|
10
|
+
"demo-audit.mjs",
|
|
11
|
+
"demo.mjs",
|
|
12
|
+
"dsh-plugin.json",
|
|
13
|
+
"cordis.patch.yml",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dsh",
|
|
18
|
+
"dsh-tui",
|
|
19
|
+
"dsh-tui-plugin",
|
|
20
|
+
"memory-eternal",
|
|
21
|
+
"memory",
|
|
22
|
+
"audit",
|
|
23
|
+
"plugin"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "EternalNight996",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/EternalNight996/memory-eternal.git",
|
|
30
|
+
"directory": "dsh-tui-plugin"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/EternalNight996/memory-eternal/tree/main/dsh-tui-plugin",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/EternalNight996/memory-eternal/issues"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"registry": "https://registry.npmjs.org/",
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22"
|
|
42
|
+
},
|
|
43
|
+
"dsh": {
|
|
44
|
+
"bundle": {
|
|
45
|
+
"patch": "./cordis.patch.yml"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
50
|
+
}
|
|
51
|
+
}
|