memory-eternal 0.11.0 → 0.13.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.
@@ -0,0 +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.13.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
+ }
@@ -0,0 +1,97 @@
1
+ // memory-eternal-tui — dsh-TUI 的官方命令入口。
2
+ //
3
+ // 这里不做第二套 TUI UI:命令只负责把用户送到同一套 Memory Atlas Web UI。
4
+ // 审批仍然在 Web 里由人完成,不新增 agent 工具,也不触碰审核守卫。
5
+ import { spawn } from 'node:child_process'
6
+ import { AuditScene } from './scene.js'
7
+
8
+ export const name = 'memory-eternal-tui'
9
+ export const inject = ['agents']
10
+
11
+ const URL = 'http://127.0.0.1:7999/'
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'
15
+
16
+ function openUrl() {
17
+ const cmd = process.platform === 'win32' ? 'rundll32' : process.platform === 'darwin' ? 'open' : 'xdg-open'
18
+ const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', URL] : [URL]
19
+ try {
20
+ const child = spawn(cmd, args, { detached: true, stdio: 'ignore', windowsHide: true })
21
+ child.on('error', () => {})
22
+ child.unref()
23
+ } catch { /* 平台没有 opener 时由调用方提示手动访问 */ }
24
+ }
25
+
26
+ function openMemory() {
27
+ // 优先让 CLI 确保常驻 Web 已启动并打开浏览器;命令不存在时直接打开 URL。
28
+ try {
29
+ const child = spawn('dsh-memory', ['open'], {
30
+ detached: true,
31
+ stdio: 'ignore',
32
+ shell: true,
33
+ windowsHide: true,
34
+ })
35
+ child.on('error', () => openUrl())
36
+ child.on('exit', (code) => { if (code !== 0) openUrl() })
37
+ child.unref()
38
+ } catch {
39
+ openUrl()
40
+ }
41
+ }
42
+
43
+ export function apply(ctx) {
44
+ const host = ctx.get('tuiPluginHost', false)
45
+ if (!host) return
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
+
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
+ }
96
+
97
+ export default { name, inject, apply }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "memory-eternal-tui",
3
+ "version": "0.13.0",
4
+ "description": "Open Memory Atlas from dsh-TUI with /memory.",
5
+ "type": "module",
6
+ "main": "main.js",
7
+ "files": [
8
+ "main.js",
9
+ "scene.js",
10
+ "demo.mjs",
11
+ "dsh-plugin.json",
12
+ "cordis.patch.yml",
13
+ "README.md"
14
+ ],
15
+ "keywords": [
16
+ "dsh",
17
+ "dsh-tui",
18
+ "memory-eternal",
19
+ "memory",
20
+ "plugin"
21
+ ],
22
+ "license": "MIT",
23
+ "dsh": {
24
+ "bundle": {
25
+ "patch": "./cordis.patch.yml"
26
+ }
27
+ },
28
+ "peerDependencies": {
29
+ "@deepseek-ai/cordis": "^4.0.1"
30
+ }
31
+ }
@@ -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
package/lib/client.js CHANGED
@@ -701,7 +701,7 @@ Option A, because:
701
701
  .mc-audit-batchcard > .mc-btn { width: 100%; }
702
702
  .mc-audit-batchcard > div[style*="flex: 1"] { display: none; }
703
703
  }
704
- `;function Ea(t){t.effect(()=>t.locale.register(kt,{zh:da,en:ca}),"memory-eternal: locale");let a=t.locale.bind(kt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:kt,order:25,label:()=>a("nav"),locale:kt,inject:()=>({})},()=>o.default.createElement(Wa,{t:a}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${kt}:footer`,order:100,label:()=>a("nav"),locale:kt,inject:()=>({})},i=>o.default.createElement($a,{t:a,locale:t.locale,wide:!(i&&i.wide===!1)}))),"memory-eternal: sidebar footer action")}function Oa(){let[t,a]=(0,o.useState)($+"/ui/app");return(0,o.useEffect)(()=>{let i=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{i&&c&&c.ok&&c.alive!==!1&&c.url&&a(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch($+"/ui/app",{method:"HEAD"}).then(c=>{if(i&&!c.ok)return g()}).catch(()=>i?g():void 0),()=>{i=!1}},[]),t}function Wa({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsx)(ua,{t,onReload:()=>{},version:0})]})}function $a({t,locale:a,wide:i}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${i?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>c(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(Ua,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(_a,{t,locale:a,onClose:()=>c(!1)})]})}function _a({t,locale:a,onClose:i,tab:g}){let[c,s]=(0,o.useState)(!1),h=(0,o.useRef)(null),k=Oa(),S=(0,o.useCallback)(N=>a&&typeof a.subscribe=="function"?a.subscribe(N):()=>{},[a]),L=(0,o.useCallback)(()=>a&&typeof a.getSnapshot=="function"?a.getSnapshot():null,[a]),M=(0,o.useSyncExternalStore)(S,L),x=M&&(M.active==="zh"||M.active==="en")?M.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let N=m.current?`lang=${m.current}`:"";if(g==="config")return`${$}/ui/config?tab=config${N?`&${N}`:""}`;let P=k.replace(/\/$/,""),T=P.endsWith("/ui/app")?"?":"/?";return g?`${P}${T}tab=${g}${N?`&${N}`:""}`:N?`${P}${T}${N}`:k},[k,g]);return(0,o.useEffect)(()=>{let N=h.current&&h.current.contentWindow;N&&x&&N.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let N=P=>{P.key==="Escape"&&i()};return window.addEventListener("keydown",N),()=>window.removeEventListener("keydown",N)},[i]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:i,children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:N=>N.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{style:{position:"absolute",top:8,right:8,zIndex:10,display:"flex",gap:4,padding:"4px 6px",background:"var(--dsw-alias-bg-overlay, rgba(17,24,39,0.45))",borderRadius:8,backdropFilter:"blur(4px)"},children:[(0,e.jsx)("button",{type:"button",onClick:()=>s(N=>!N),"aria-label":t(c?"exitFullscreen":"enterFullscreen"),title:t(c?"exitFullscreen":"enterFullscreen"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:14,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:c?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:i,"aria-label":t("close"),title:t("close"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:16,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:"\xD7"})]}),(0,e.jsx)("iframe",{ref:h,src:z,title:"memory-eternal",allow:"clipboard-write; clipboard-read",style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"var(--dsw-alias-bg-base, #fff)"}})]})})]})}function Ua(){return(0,e.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,e.jsx)("defs",{children:(0,e.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,e.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,e.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,e.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,e.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,e.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var St="0.11.0";function Ga({t,inModal:a,onClose:i}){let g=typeof window<"u"&&(!!window.__DSH_BOOT__||String(location.pathname||"").includes("/memory-eternal/api/ui/app")),[c,s]=(0,o.useState)(null),[h,k]=(0,o.useState)([]),[S,L]=(0,o.useState)(!0),[M,x]=(0,o.useState)(null),[m,z]=(0,o.useState)(""),[N,P]=(0,o.useState)("all"),[T,E]=(0,o.useState)(""),[K,se]=(0,o.useState)("all"),[V,Z]=(0,o.useState)("all"),[te,G]=(0,o.useState)(()=>{try{return new Set(JSON.parse(localStorage.getItem("me-pinned-v5")||"[]"))}catch{return new Set}}),[fe,ue]=(0,o.useState)(!1),_=(0,o.useCallback)(b=>{G(r=>{let v=new Set(r);v.has(b)?v.delete(b):v.add(b);try{localStorage.setItem("me-pinned-v5",JSON.stringify([...v]))}catch{}return v})},[]),[Q,je]=(0,o.useState)(""),[O,Be]=(0,o.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),r={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&r[b])return r[b]}catch{}return"cards"}),[f,F]=(0,o.useState)(()=>new Set([O])),W=(0,o.useCallback)(b=>{F(r=>r.has(b)?r:new Set(r).add(b)),Be(b)},[]),[B,Ce]=(0,o.useState)("recent"),[ze,pe]=(0,o.useState)(null),ve=(0,o.useRef)(null),y=(0,o.useRef)(null),[ae,X]=(0,o.useState)(!1),[Ne,ne]=(0,o.useState)(null),[xe,Ge]=(0,o.useState)(!1),[p,D]=(0,o.useState)(!1),[A,Y]=(0,o.useState)(!1),[Se,le]=(0,o.useState)("group"),[H,we]=(0,o.useState)(!1),be=(0,o.useCallback)(()=>{we(!1);try{localStorage.setItem("me-onboard-v2","1")}catch{}},[]),u=(0,o.useCallback)((b="group")=>{le(b),Y(!0)},[]);(0,o.useEffect)(()=>{try{if(localStorage.getItem("me-onboard-v2")||O!=="cards")return;let b=setTimeout(()=>we(!0),900);return()=>clearTimeout(b)}catch{}},[O]);let[d,J]=(0,o.useState)(!0),[he,Me]=(0,o.useState)(null),[Te,n]=(0,o.useState)(0),ot=(0,o.useCallback)(()=>n(b=>b+1),[]),ct=c?.quarantine?.pending??0,Ke=c?.quarantine?.deleted??0,Pe=(0,o.useRef)(null),[Xe,bt]=(0,o.useState)(0),[it,Re]=(0,o.useState)(!1),et=(0,o.useRef)(null),Fe=(0,o.useRef)(null),l=(0,o.useCallback)(async(b=N,r=T,v=V,C={})=>{let R=C.append===!0;try{R?Re(!0):(L(!0),z(""));let j=new URLSearchParams;b&&b!=="all"&&j.set("kind",b),r.trim()&&j.set("q",r.trim());let ee=C.tag!==void 0?C.tag:Q;ee&&j.set("tag",ee),j.set("status",v||"all"),j.set("limit",String(ja)),j.set("offset",String(C.offset||0)),B==="title"&&j.set("sort","title");let ge=await(await fetch(`${$}/cards?${j.toString()}`)).json();if(ge.ok){let Ae=ge.cards||[];k(De=>R?[...De,...Ae.filter(ye=>!De.some(ke=>ke.path===ye.path))]:Ae),bt(Number(ge.total)||Ae.length)}}catch(j){z(String(j&&j.message?j.message:j))}finally{R?Re(!1):L(!1)}},[N,T,V,B,Q]),I=(0,o.useCallback)(async()=>{try{let[b,r]=await Promise.all([fetch(`${$}/overview`).then(v=>v.json()),fetch(`${$}/capture-log`).then(v=>v.json()).catch(()=>null)]);b.ok&&s(b),x(r&&r.ok&&r.available&&r.health||null)}catch(b){z(String(b&&b.message?b.message:b))}finally{L(!1)}},[]),U=async b=>{try{let v=await(await fetch(`${$}/delete?path=${encodeURIComponent(b)}`)).json();v.ok?(pe({ok:!0,msg:t("deleted")}),await I(),ot()):pe({ok:!1,msg:v.error||t("deleteFail")})}catch{pe({ok:!1,msg:t("deleteFail")})}ve.current&&clearTimeout(ve.current),ve.current=setTimeout(()=>pe(null),2e3)};(0,o.useEffect)(()=>(I(),()=>{Pe.current&&clearTimeout(Pe.current)}),[I]);let oe=b=>{E(b),Pe.current&&clearTimeout(Pe.current),Pe.current=setTimeout(()=>l(N,b,V),280)},ie=b=>{P(b),l(b,T,V)},q=b=>{let r=String(b||"");je(r),ne(null),l(N,T,V,{tag:r})};(0,o.useEffect)(()=>{O==="cards"&&l(N,T,V)},[V,O,Te,B,Q]),(0,o.useEffect)(()=>{if(O!=="cards"||S||it||h.length>=Xe)return;let b=et.current,r=Fe.current;if(!b)return;let v=new IntersectionObserver(C=>{C[0]&&C[0].isIntersecting&&l(N,T,V,{append:!0,offset:h.length})},{root:r||null,rootMargin:"400px"});return v.observe(b),()=>v.disconnect()},[O,h,Xe,S,it,N,T,V,l]);let re=async b=>{try{let r=b.status;if(!r&&b.path)try{let ee=((await fetch(`${$}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(de=>de.json())).cards||[]).find(de=>de.path===b.path);ee&&(r=ee.status)}catch{}let v=r&&r!=="approved"?`&status=${encodeURIComponent(r)}`:"",C=await fetch(`${$}/card?path=${encodeURIComponent(b.path||b.id)}${v}`),R=await C.json();R.ok?ne({path:R.path,title:b.title,text:R.text}):z(R.error||`HTTP ${C.status}`)}catch(r){z(String(r&&r.message?r.message:r))}},He=async(b,r)=>{X(!0);try{let v=await fetch(`${$}/export`);if(!v.ok)throw new Error("HTTP "+v.status);let C=await v.json();if(!C.ok)throw new Error(C.error||"export failed");let R,j,ee;if(b==="json"){j="memory-vault.json",ee="application/json";let Ae=C.cards.map(De=>({path:De.path,title:De.title,kind:De.kind,status:De.status,store:De.store,text:De.text}));R=JSON.stringify({format:C.format||"memory-eternal-vault",formatVersion:C.formatVersion||1,exportedAt:C.exportedAt||new Date().toISOString(),count:Ae.length,cards:Ae},null,2)}else j="memory-vault.md",ee="text/markdown;charset=utf-8",R=C.cards.map(Ae=>Ae.text.trim()).filter(Boolean).join(`
704
+ `;function Ea(t){t.effect(()=>t.locale.register(kt,{zh:da,en:ca}),"memory-eternal: locale");let a=t.locale.bind(kt);t.effect(()=>t.slots.inject("settings.section",()=>t.slots.register({name:"settings.section",id:kt,order:25,label:()=>a("nav"),locale:kt,inject:()=>({})},()=>o.default.createElement(Wa,{t:a}))),"memory-eternal: settings section"),t.effect(()=>t.slots.inject("sidebar.footer.action",()=>t.slots.register({name:"sidebar.footer.action",id:`${kt}:footer`,order:100,label:()=>a("nav"),locale:kt,inject:()=>({})},i=>o.default.createElement($a,{t:a,locale:t.locale,wide:!(i&&i.wide===!1)}))),"memory-eternal: sidebar footer action")}function Oa(){let[t,a]=(0,o.useState)($+"/ui/app");return(0,o.useEffect)(()=>{let i=!0,g=()=>fetch("/memory-eternal/api/web-info").then(c=>c.json()).then(c=>{i&&c&&c.ok&&c.alive!==!1&&c.url&&a(c.url.endsWith("/")?c.url:c.url+"/")}).catch(()=>{});return fetch($+"/ui/app",{method:"HEAD"}).then(c=>{if(i&&!c.ok)return g()}).catch(()=>i?g():void 0),()=>{i=!1}},[]),t}function Wa({t}){return(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0},children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsx)(ua,{t,onReload:()=>{},version:0})]})}function $a({t,locale:a,wide:i}){let[g,c]=(0,o.useState)(!1);return(0,e.jsxs)("div",{className:`me-footer${i?"":" rail"}`,children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsxs)("button",{type:"button",className:"me-footer-btn",onClick:()=>c(!0),"aria-label":t("nav"),title:t("nav"),children:[(0,e.jsx)("span",{className:"me-footer-ico","aria-hidden":"true",children:(0,e.jsx)(Ua,{})}),(0,e.jsx)("span",{className:"me-footer-label",children:t("nav")})]}),g&&(0,e.jsx)(_a,{t,locale:a,onClose:()=>c(!1)})]})}function _a({t,locale:a,onClose:i,tab:g}){let[c,s]=(0,o.useState)(!1),h=(0,o.useRef)(null),k=Oa(),S=(0,o.useCallback)(N=>a&&typeof a.subscribe=="function"?a.subscribe(N):()=>{},[a]),L=(0,o.useCallback)(()=>a&&typeof a.getSnapshot=="function"?a.getSnapshot():null,[a]),M=(0,o.useSyncExternalStore)(S,L),x=M&&(M.active==="zh"||M.active==="en")?M.active:null,m=(0,o.useRef)(null);m.current===null&&x&&(m.current=x);let z=(0,o.useMemo)(()=>{let N=m.current?`lang=${m.current}`:"";if(g==="config")return`${$}/ui/config?tab=config${N?`&${N}`:""}`;let P=k.replace(/\/$/,""),T=P.endsWith("/ui/app")?"?":"/?";return g?`${P}${T}tab=${g}${N?`&${N}`:""}`:N?`${P}${T}${N}`:k},[k,g]);return(0,o.useEffect)(()=>{let N=h.current&&h.current.contentWindow;N&&x&&N.postMessage({source:"memory-eternal",type:"locale",lang:x},"*")},[x]),(0,o.useEffect)(()=>{let N=P=>{P.key==="Escape"&&i()};return window.addEventListener("keydown",N),()=>window.removeEventListener("keydown",N)},[i]),(0,e.jsxs)("div",{className:"me-overlay-top",onClick:i,children:[(0,e.jsx)("style",{children:Ue}),(0,e.jsx)("div",{className:"me-modal"+(c?" me-modal-full":""),onClick:N=>N.stopPropagation(),children:(0,e.jsxs)("div",{style:{display:"flex",flexDirection:"column",height:"100%",minHeight:0,position:"relative"},children:[(0,e.jsxs)("div",{style:{position:"absolute",top:8,right:8,zIndex:10,display:"flex",gap:4,padding:"4px 6px",background:"var(--dsw-alias-bg-overlay, rgba(17,24,39,0.45))",borderRadius:8,backdropFilter:"blur(4px)"},children:[(0,e.jsx)("button",{type:"button",onClick:()=>s(N=>!N),"aria-label":t(c?"exitFullscreen":"enterFullscreen"),title:t(c?"exitFullscreen":"enterFullscreen"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:14,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:c?"\u2921":"\u2922"}),(0,e.jsx)("button",{type:"button",onClick:i,"aria-label":t("close"),title:t("close"),style:{width:26,height:26,borderRadius:6,border:"1px solid rgba(255,255,255,0.18)",background:"rgba(255,255,255,0.08)",color:"#fff",cursor:"pointer",fontSize:16,lineHeight:1,display:"flex",alignItems:"center",justifyContent:"center"},children:"\xD7"})]}),(0,e.jsx)("iframe",{ref:h,src:z,title:"memory-eternal",allow:"clipboard-write; clipboard-read",style:{width:"100%",flex:1,border:0,borderRadius:"inherit",background:"var(--dsw-alias-bg-base, #fff)"}})]})})]})}function Ua(){return(0,e.jsxs)("svg",{viewBox:"0 0 24 24","aria-hidden":"true",children:[(0,e.jsx)("defs",{children:(0,e.jsxs)("linearGradient",{id:"mg-brain",x1:"0",y1:"0",x2:"1",y2:"1",children:[(0,e.jsx)("stop",{offset:"0%",stopColor:"#FFC46B"}),(0,e.jsx)("stop",{offset:"50%",stopColor:"#F97316"}),(0,e.jsx)("stop",{offset:"100%",stopColor:"#EA580C"})]})}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 0-6.4.1 4.1 4.1 0 0 0-2.7 5.9 4.1 4.1 0 0 0 .6 6.7A4.2 4.2 0 0 0 12 18.2Z"}),(0,e.jsx)("path",{fill:"url(#mg-brain)",d:"M12 4.5a3.2 3.2 0 0 1 6.4.1 4.1 4.1 0 0 1 2.7 5.9 4.1 4.1 0 0 1-.6 6.7A4.2 4.2 0 0 1 12 18.2Z"}),(0,e.jsx)("path",{d:"M12 9.2c-.8-1.4-1.7-2-3.1-2.2M12 9.2c.8-1.4 1.7-2 3.1-2.2M12 12.4c-1.2 1-3.2 1.6-5 1.4M12 12.4c1.2 1 3.2 1.6 5 1.4M12 13.2v4",fill:"none",stroke:"rgba(255,255,255,0.62)",strokeWidth:"1",strokeLinecap:"round"}),(0,e.jsx)("circle",{cx:"12",cy:"8.6",r:"1.1",fill:"rgba(255,255,255,0.85)"})]})}var St="0.13.0";function Ga({t,inModal:a,onClose:i}){let g=typeof window<"u"&&(!!window.__DSH_BOOT__||String(location.pathname||"").includes("/memory-eternal/api/ui/app")),[c,s]=(0,o.useState)(null),[h,k]=(0,o.useState)([]),[S,L]=(0,o.useState)(!0),[M,x]=(0,o.useState)(null),[m,z]=(0,o.useState)(""),[N,P]=(0,o.useState)("all"),[T,E]=(0,o.useState)(""),[K,se]=(0,o.useState)("all"),[V,Z]=(0,o.useState)("all"),[te,G]=(0,o.useState)(()=>{try{return new Set(JSON.parse(localStorage.getItem("me-pinned-v5")||"[]"))}catch{return new Set}}),[fe,ue]=(0,o.useState)(!1),_=(0,o.useCallback)(b=>{G(r=>{let v=new Set(r);v.has(b)?v.delete(b):v.add(b);try{localStorage.setItem("me-pinned-v5",JSON.stringify([...v]))}catch{}return v})},[]),[Q,je]=(0,o.useState)(""),[O,Be]=(0,o.useState)(()=>{try{let b=new URLSearchParams(typeof location<"u"?location.search:"").get("tab"),r={cards:"cards",graph:"graph",usage:"stats",stats:"stats",audit:"audit",recycle:"optimize",optimize:"optimize",config:"config"};if(b&&r[b])return r[b]}catch{}return"cards"}),[f,F]=(0,o.useState)(()=>new Set([O])),W=(0,o.useCallback)(b=>{F(r=>r.has(b)?r:new Set(r).add(b)),Be(b)},[]),[B,Ce]=(0,o.useState)("recent"),[ze,pe]=(0,o.useState)(null),ve=(0,o.useRef)(null),y=(0,o.useRef)(null),[ae,X]=(0,o.useState)(!1),[Ne,ne]=(0,o.useState)(null),[xe,Ge]=(0,o.useState)(!1),[p,D]=(0,o.useState)(!1),[A,Y]=(0,o.useState)(!1),[Se,le]=(0,o.useState)("group"),[H,we]=(0,o.useState)(!1),be=(0,o.useCallback)(()=>{we(!1);try{localStorage.setItem("me-onboard-v2","1")}catch{}},[]),u=(0,o.useCallback)((b="group")=>{le(b),Y(!0)},[]);(0,o.useEffect)(()=>{try{if(localStorage.getItem("me-onboard-v2")||O!=="cards")return;let b=setTimeout(()=>we(!0),900);return()=>clearTimeout(b)}catch{}},[O]);let[d,J]=(0,o.useState)(!0),[he,Me]=(0,o.useState)(null),[Te,n]=(0,o.useState)(0),ot=(0,o.useCallback)(()=>n(b=>b+1),[]),ct=c?.quarantine?.pending??0,Ke=c?.quarantine?.deleted??0,Pe=(0,o.useRef)(null),[Xe,bt]=(0,o.useState)(0),[it,Re]=(0,o.useState)(!1),et=(0,o.useRef)(null),Fe=(0,o.useRef)(null),l=(0,o.useCallback)(async(b=N,r=T,v=V,C={})=>{let R=C.append===!0;try{R?Re(!0):(L(!0),z(""));let j=new URLSearchParams;b&&b!=="all"&&j.set("kind",b),r.trim()&&j.set("q",r.trim());let ee=C.tag!==void 0?C.tag:Q;ee&&j.set("tag",ee),j.set("status",v||"all"),j.set("limit",String(ja)),j.set("offset",String(C.offset||0)),B==="title"&&j.set("sort","title");let ge=await(await fetch(`${$}/cards?${j.toString()}`)).json();if(ge.ok){let Ae=ge.cards||[];k(De=>R?[...De,...Ae.filter(ye=>!De.some(ke=>ke.path===ye.path))]:Ae),bt(Number(ge.total)||Ae.length)}}catch(j){z(String(j&&j.message?j.message:j))}finally{R?Re(!1):L(!1)}},[N,T,V,B,Q]),I=(0,o.useCallback)(async()=>{try{let[b,r]=await Promise.all([fetch(`${$}/overview`).then(v=>v.json()),fetch(`${$}/capture-log`).then(v=>v.json()).catch(()=>null)]);b.ok&&s(b),x(r&&r.ok&&r.available&&r.health||null)}catch(b){z(String(b&&b.message?b.message:b))}finally{L(!1)}},[]),U=async b=>{try{let v=await(await fetch(`${$}/delete?path=${encodeURIComponent(b)}`)).json();v.ok?(pe({ok:!0,msg:t("deleted")}),await I(),ot()):pe({ok:!1,msg:v.error||t("deleteFail")})}catch{pe({ok:!1,msg:t("deleteFail")})}ve.current&&clearTimeout(ve.current),ve.current=setTimeout(()=>pe(null),2e3)};(0,o.useEffect)(()=>(I(),()=>{Pe.current&&clearTimeout(Pe.current)}),[I]);let oe=b=>{E(b),Pe.current&&clearTimeout(Pe.current),Pe.current=setTimeout(()=>l(N,b,V),280)},ie=b=>{P(b),l(b,T,V)},q=b=>{let r=String(b||"");je(r),ne(null),l(N,T,V,{tag:r})};(0,o.useEffect)(()=>{O==="cards"&&l(N,T,V)},[V,O,Te,B,Q]),(0,o.useEffect)(()=>{if(O!=="cards"||S||it||h.length>=Xe)return;let b=et.current,r=Fe.current;if(!b)return;let v=new IntersectionObserver(C=>{C[0]&&C[0].isIntersecting&&l(N,T,V,{append:!0,offset:h.length})},{root:r||null,rootMargin:"400px"});return v.observe(b),()=>v.disconnect()},[O,h,Xe,S,it,N,T,V,l]);let re=async b=>{try{let r=b.status;if(!r&&b.path)try{let ee=((await fetch(`${$}/cards?status=all&q=${encodeURIComponent(b.title||"")}&limit=50`).then(de=>de.json())).cards||[]).find(de=>de.path===b.path);ee&&(r=ee.status)}catch{}let v=r&&r!=="approved"?`&status=${encodeURIComponent(r)}`:"",C=await fetch(`${$}/card?path=${encodeURIComponent(b.path||b.id)}${v}`),R=await C.json();R.ok?ne({path:R.path,title:b.title,text:R.text}):z(R.error||`HTTP ${C.status}`)}catch(r){z(String(r&&r.message?r.message:r))}},He=async(b,r)=>{X(!0);try{let v=await fetch(`${$}/export`);if(!v.ok)throw new Error("HTTP "+v.status);let C=await v.json();if(!C.ok)throw new Error(C.error||"export failed");let R,j,ee;if(b==="json"){j="memory-vault.json",ee="application/json";let Ae=C.cards.map(De=>({path:De.path,title:De.title,kind:De.kind,status:De.status,store:De.store,text:De.text}));R=JSON.stringify({format:C.format||"memory-eternal-vault",formatVersion:C.formatVersion||1,exportedAt:C.exportedAt||new Date().toISOString(),count:Ae.length,cards:Ae},null,2)}else j="memory-vault.md",ee="text/markdown;charset=utf-8",R=C.cards.map(Ae=>Ae.text.trim()).filter(Boolean).join(`
705
705
 
706
706
  ---
707
707
 
@@ -0,0 +1,58 @@
1
+ # Orca plugin — Memory Atlas 启动器
2
+
3
+ 在 Orca 右侧栏添加一个「记忆」面板,一键在终端执行 `dsh-memory open`,打开本地 Memory Atlas Web UI。
4
+
5
+ ## 安装
6
+
7
+ ### 推荐:Git URL
8
+
9
+ Orca → Settings → Plugins → Install plugin → **Git URL**:
10
+
11
+ ```text
12
+ https://github.com/EternalNight996/memory-eternal-orca#v0.12.1
13
+ ```
14
+
15
+ Orca 会固定到 `v0.12.0` tag。
16
+
17
+ ### 本地目录
18
+
19
+ 1. 确认本机已安装并运行 `memory-eternal`(默认 Web 服务 `http://127.0.0.1:7999/`)。
20
+ 2. 确认终端能找到 CLI:
21
+
22
+ ```bash
23
+ dsh-memory --help
24
+ ```
25
+
26
+ 3. 在 Orca 中安装本目录:
27
+
28
+ ```text
29
+ Orca → Plugins → Install plugin → 选择 orca-plugin/
30
+ ```
31
+
32
+ 4. 打开右侧栏的「记忆」图标,选择终端,点「打开 Memory Atlas」。
33
+
34
+ ## 它做什么
35
+
36
+ - 调 Orca 官方 `workspace.readContext` 读取当前工作区终端
37
+ - 调 Orca 官方 `terminal.sendText` 执行 `dsh-memory open`
38
+ - 不 iframe、不 fetch、不复制第二套 UI
39
+
40
+ ## 为什么不是完整内嵌 UI
41
+
42
+ Orca panel 使用严格沙箱 CSP:
43
+
44
+ ```text
45
+ default-src 'none'
46
+ connect-src 'none'
47
+ img-src data:
48
+ ```
49
+
50
+ 因此 panel 内不能 iframe `127.0.0.1:7999`、不能 fetch API、不能 `window.open`。
51
+ 完整 Memory Atlas 仍由 Web UI 提供;Orca 只做官方能力内的启动器。
52
+
53
+ ## 权限
54
+
55
+ - `workspace:read`:读取当前工作区终端列表
56
+ - `terminal:send`:向指定终端发送命令
57
+
58
+ 没有任何内存卡写入、审核批准或 agent 权限。
Binary file
@@ -0,0 +1,36 @@
1
+ {
2
+ "manifestVersion": 1,
3
+ "id": "memory-eternal",
4
+ "publisher": "eternalnight996",
5
+ "name": "memory-eternal",
6
+ "version": "0.13.0",
7
+ "description": "Memory Atlas for Orca: open the local memory vault, pending review and knowledge graph from a sandboxed Orca panel.",
8
+ "author": {
9
+ "name": "EternalNight996",
10
+ "url": "https://github.com/EternalNight996"
11
+ },
12
+ "repository": "https://github.com/EternalNight996/memory-eternal",
13
+ "icon": "assets/icon.png",
14
+ "engines": {
15
+ "orca": ">=1.4.0"
16
+ },
17
+ "pluginApi": 1,
18
+ "contributes": {
19
+ "panels": [
20
+ {
21
+ "id": "memory",
22
+ "title": "记忆",
23
+ "icon": "brain",
24
+ "entry": "panel.html"
25
+ }
26
+ ]
27
+ },
28
+ "capabilities": [
29
+ {
30
+ "kind": "workspace:read"
31
+ },
32
+ {
33
+ "kind": "terminal:send"
34
+ }
35
+ ]
36
+ }