memory-eternal 0.12.0 → 0.14.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.
@@ -2,9 +2,9 @@
2
2
  "$schema": "urn:dsh-std:community-draft:dsh-plugin:0.15",
3
3
  "id": "memory-eternal-tui",
4
4
  "name": "memory-eternal-tui",
5
- "version": "0.12.0",
5
+ "version": "0.14.0",
6
6
  "manifestVersion": "0.15",
7
- "description": "Open the local Memory Atlas Web UI from dsh-TUI.",
7
+ "description": "Open Memory Atlas from dsh-TUI: native /audit scene plus browser launcher.",
8
8
  "facets": {
9
9
  "host": {
10
10
  "entry": "main.js",
@@ -26,6 +26,11 @@
26
26
  "id": "memory-eternal-tui.open",
27
27
  "title": "memory",
28
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"
29
34
  }
30
35
  ]
31
36
  },
@@ -3,12 +3,15 @@
3
3
  // 这里不做第二套 TUI UI:命令只负责把用户送到同一套 Memory Atlas Web UI。
4
4
  // 审批仍然在 Web 里由人完成,不新增 agent 工具,也不触碰审核守卫。
5
5
  import { spawn } from 'node:child_process'
6
+ import { AuditScene } from './scene.js'
6
7
 
7
8
  export const name = 'memory-eternal-tui'
8
9
  export const inject = ['agents']
9
10
 
10
11
  const URL = 'http://127.0.0.1:7999/'
11
12
  const COMMAND_ID = 'memory-eternal-tui.open'
13
+ const AUDIT_COMMAND_ID = 'memory-eternal-tui.audit'
14
+ const AUDIT_SCENE_ID = 'memory-eternal-tui-audit'
12
15
 
13
16
  function openUrl() {
14
17
  const cmd = process.platform === 'win32' ? 'rundll32' : process.platform === 'darwin' ? 'open' : 'xdg-open'
@@ -41,6 +44,32 @@ export function apply(ctx) {
41
44
  const host = ctx.get('tuiPluginHost', false)
42
45
  if (!host) return
43
46
 
47
+ // tuiScenes can mount after this activation on a cold boot. Soft-probe and
48
+ // retry briefly instead of silently leaving /audit dead for the session.
49
+ let sceneDisposer = null
50
+ let sceneTimer = null
51
+ let attempts = 0
52
+ const registerScene = () => {
53
+ sceneTimer = null
54
+ const scenes = ctx.get('tuiScenes', false)
55
+ if (scenes && !sceneDisposer) {
56
+ try {
57
+ sceneDisposer = scenes.register({ id: AUDIT_SCENE_ID, title: 'Memory Atlas / 待审', component: AuditScene }, ctx)
58
+ } catch (error) {
59
+ console.warn('[memory-eternal-tui] scene registration failed:', error)
60
+ }
61
+ }
62
+ if (!sceneDisposer && attempts < 40) {
63
+ attempts += 1
64
+ sceneTimer = setTimeout(registerScene, 50)
65
+ }
66
+ }
67
+ registerScene()
68
+ ctx.effect(() => {
69
+ if (sceneTimer) clearTimeout(sceneTimer)
70
+ if (sceneDisposer) sceneDisposer()
71
+ })
72
+
44
73
  const dispose = host.registerCommand(ctx, COMMAND_ID, {
45
74
  recordInput: false,
46
75
  handler: () => {
@@ -49,6 +78,20 @@ export function apply(ctx) {
49
78
  },
50
79
  })
51
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)
52
95
  }
53
96
 
54
97
  export default { name, inject, apply }
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "memory-eternal-tui",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Open Memory Atlas from dsh-TUI with /memory.",
5
5
  "type": "module",
6
6
  "main": "main.js",
7
7
  "files": [
8
8
  "main.js",
9
+ "scene.js",
10
+ "demo.mjs",
9
11
  "dsh-plugin.json",
10
12
  "cordis.patch.yml",
11
13
  "README.md"
@@ -0,0 +1,151 @@
1
+ // dsh-tui 原生审核场景(最小闭环)
2
+ //
3
+ // 不复制 Web UI:只把「待审队列 + 全选 + 批准/驳回」做成 dsh-tui 原生场景。
4
+ // 日常浏览/长文仍可回到 Web 或外部 TUI 客户端。
5
+
6
+ const API = process.env.MEMORY_ETERNAL_API || 'http://127.0.0.1:7999/memory-eternal/api'
7
+
8
+ async function apiJson(path, options = {}) {
9
+ const res = await fetch(API + path, { ...options, signal: AbortSignal.timeout(10000) })
10
+ const data = await res.json().catch(() => null)
11
+ if (!res.ok || !data || data.ok === false) throw new Error((data && data.error) || `HTTP ${res.status}`)
12
+ return data
13
+ }
14
+
15
+ function timeOf(value) {
16
+ const d = new Date(value || Date.now())
17
+ return Number.isNaN(d.getTime()) ? '' : d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false })
18
+ }
19
+
20
+ export function AuditScene(props) {
21
+ const { React, ui, close } = props
22
+ const { Box, Text, useInput, useTerminalSize } = ui
23
+ const { useState, useEffect } = React
24
+ const size = useTerminalSize ? useTerminalSize() : { columns: 110, rows: 34 }
25
+ const columns = size.columns || 110
26
+ const rows = size.rows || 34
27
+
28
+ const [cards, setCards] = useState([])
29
+ const [index, setIndex] = useState(0)
30
+ const [marked, setMarked] = useState(() => new Set())
31
+ const [status, setStatus] = useState('读取待审队列…')
32
+ const [loading, setLoading] = useState(true)
33
+
34
+ const load = async () => {
35
+ setLoading(true)
36
+ setStatus('读取待审队列…')
37
+ try {
38
+ const data = await apiJson('/cards?status=pending&limit=300')
39
+ setCards((data.cards || []).map((c) => ({
40
+ path: c.path,
41
+ kind: c.kind || 'knowledge',
42
+ title: c.title || '无标题',
43
+ summary: String(c.summary || '').replace(/^#{1,6}\s*/gm, '').replace(/\s+/g, ' ').slice(0, 260),
44
+ source: c.submittedBy || 'memory',
45
+ time: timeOf(c.updated || c.created),
46
+ })))
47
+ setIndex(0)
48
+ setMarked(new Set())
49
+ setStatus('')
50
+ } catch (err) {
51
+ setStatus('读取失败:' + err.message)
52
+ } finally {
53
+ setLoading(false)
54
+ }
55
+ }
56
+
57
+ useEffect(() => { void load() }, [])
58
+
59
+ const current = cards[index]
60
+ const visible = cards
61
+ const selected = marked.size
62
+ const apply = async (decision) => {
63
+ const targets = selected ? visible.filter((c) => marked.has(c.path)) : (current ? [current] : [])
64
+ if (!targets.length) return
65
+ setStatus(decision === 'a' ? `批准 ${targets.length} 张…` : `驳回 ${targets.length} 张…`)
66
+ try {
67
+ await Promise.all(targets.map((c) => apiJson('/audit/' + (decision === 'a' ? 'approve' : 'reject'), {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({ path: c.path }),
71
+ })))
72
+ const ids = new Set(targets.map((c) => c.path))
73
+ setCards((prev) => prev.filter((c) => !ids.has(c.path)))
74
+ setIndex(0)
75
+ setMarked(new Set())
76
+ setStatus(`✓ ${decision === 'a' ? '已批准' : '已驳回'} ${targets.length} 张`)
77
+ } catch (err) {
78
+ setStatus('操作失败:' + err.message)
79
+ }
80
+ }
81
+
82
+ useInput((input, key) => {
83
+ if (key.escape || input === 'q') { close(); return }
84
+ if (input === 'r' && !key.ctrl) { void apply('r'); return }
85
+ if (input === 'a' && !key.ctrl) { void apply('a'); return }
86
+ if (input === 's' || (key.ctrl && input === 'a')) {
87
+ setMarked((prev) => {
88
+ const all = visible.length > 0 && visible.every((c) => prev.has(c.path))
89
+ return all ? new Set() : new Set(visible.map((c) => c.path))
90
+ })
91
+ return
92
+ }
93
+ if (key.upArrow) { setIndex((i) => Math.max(0, i - 1)); return }
94
+ if (key.downArrow) { setIndex((i) => Math.min(Math.max(0, visible.length - 1), i + 1)); return }
95
+ if (key.space || input === ' ') {
96
+ if (!current) return
97
+ setMarked((prev) => {
98
+ const next = new Set(prev)
99
+ if (next.has(current.path)) next.delete(current.path)
100
+ else next.add(current.path)
101
+ return next
102
+ })
103
+ }
104
+ })
105
+
106
+ const listH = Math.max(3, rows - 7)
107
+ const rowW = Math.max(36, Math.floor((columns - 3) * 0.42))
108
+ const previewW = Math.max(20, columns - rowW - 3)
109
+ const start = Math.max(0, Math.min(index - Math.floor(listH / 2), Math.max(0, visible.length - listH)))
110
+ const list = (visible || []).slice(start, start + listH)
111
+ const previewLines = []
112
+ if (current) {
113
+ previewLines.push(current.kind + ' · ' + current.time + ' · ' + current.source)
114
+ previewLines.push('')
115
+ previewLines.push(current.title)
116
+ previewLines.push('')
117
+ for (let i = 0; i < current.summary.length; i += Math.max(16, previewW - 4)) {
118
+ previewLines.push(current.summary.slice(i, i + Math.max(16, previewW - 4)))
119
+ }
120
+ } else {
121
+ previewLines.push(loading ? '读取中…' : '没有待审卡')
122
+ }
123
+
124
+ const h = React.createElement
125
+ return h(Box, { flexDirection: 'column', width: columns, height: rows },
126
+ h(Box, { flexShrink: 0 },
127
+ h(Text, { color: 'remember', bold: true }, '● MEMORY ATLAS'),
128
+ h(Text, { dimColor: true }, ` / 待审 ${cards.length} 张` + (selected ? ` 已选 ${selected}` : '')),
129
+ ),
130
+ h(Text, { dimColor: true }, '─'.repeat(Math.max(1, columns - 1))),
131
+ h(Box, { flexDirection: 'row', flexGrow: 1, overflow: 'hidden' },
132
+ h(Box, { flexDirection: 'column', width: rowW, borderStyle: 'round', flexShrink: 0 },
133
+ h(Text, { dimColor: true }, ` ${selected ? '▣' : '☐'} 全选 ${visible.length} s 全选`),
134
+ ...list.map((c, i) => {
135
+ const on = start + i === index
136
+ const mark = marked.has(c.path) ? '◉' : '○'
137
+ const label = ` ${on ? '▌' : ' '}${mark} [${c.kind}] ${c.title}`
138
+ return h(Text, { key: c.path, inverse: on, wrap: 'truncate' }, label.slice(0, rowW - 2))
139
+ }),
140
+ ),
141
+ h(Box, { flexDirection: 'column', width: previewW, borderStyle: 'round', flexGrow: 1 },
142
+ ...previewLines.slice(0, Math.max(1, rows - 4)).map((line, i) =>
143
+ h(Text, { key: i, wrap: 'truncate', color: i === 0 ? 'remember' : undefined }, ' ' + line)),
144
+ ),
145
+ ),
146
+ h(Text, { color: status.startsWith('✓') ? 'success' : status.startsWith('操作失败') || status.startsWith('读取失败') ? 'error' : undefined, dimColor: !status },
147
+ ' ' + (status || '↑↓ 移动 Space 多选 s 全选 a 批准 r 驳回 Esc 退出')),
148
+ )
149
+ }
150
+
151
+ export default AuditScene