memory-eternal 0.21.0 → 0.22.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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/cursor-extension/package.json +1 -1
- package/dsh-tui-plugin/dsh-plugin.json +1 -1
- package/dsh-tui-plugin/package.json +1 -1
- package/lib/align.js +16 -2
- package/lib/api.js +57 -5
- package/lib/client.js +16 -16
- package/lib/db.js +12 -0
- package/lib/mcp.js +41 -2
- package/lib/node-bin.js +17 -12
- package/lib/vault.js +113 -4
- package/orca-plugin/orca-plugin.json +1 -1
- package/package.json +1 -1
- package/web/app.js +16 -16
package/lib/db.js
CHANGED
|
@@ -213,6 +213,18 @@ function initTables(db) {
|
|
|
213
213
|
|
|
214
214
|
CREATE INDEX IF NOT EXISTS idx_card_updates_card ON card_updates(card_id);
|
|
215
215
|
|
|
216
|
+
-- 审核制修改:待审副本 → 原卡。agent 只能提交副本,通过后由 setCardStatus 覆盖原卡。
|
|
217
|
+
-- 单独建表(而不是给两张卡表加列):旧库打开时 CREATE TABLE IF NOT EXISTS 即可补上,无需迁移。
|
|
218
|
+
CREATE TABLE IF NOT EXISTS card_revisions (
|
|
219
|
+
revision_path TEXT PRIMARY KEY,
|
|
220
|
+
target_path TEXT NOT NULL,
|
|
221
|
+
target_id INTEGER,
|
|
222
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
223
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
CREATE INDEX IF NOT EXISTS idx_card_revisions_target ON card_revisions(target_path);
|
|
227
|
+
|
|
216
228
|
CREATE TABLE IF NOT EXISTS feedback (
|
|
217
229
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
218
230
|
query TEXT NOT NULL,
|
package/lib/mcp.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// (stdout 是协议通道,被 console.log 污染会破坏握手)。
|
|
6
6
|
//
|
|
7
7
|
// 支持的协议版本:2025-06-18 / 2025-03-26 / 2024-11-05(协商取请求版本)。
|
|
8
|
-
// 工具:memory_recall / memory_capture / memory_stats / memory_audit_list。
|
|
8
|
+
// 工具:memory_recall / memory_capture / memory_stats / memory_audit_list / memory_revise。
|
|
9
|
+
// memory_revise 只**提交**修改副本进审核(不会直接改库、不能批准),与 issue #17 的审核边界一致。
|
|
9
10
|
//
|
|
10
11
|
// 审核边界(issue #17):MCP 侧**只读**。新增的 memory_audit_list 只列待审/驳回,
|
|
11
12
|
// 不提供任何 approve/reject —— 否则任何接了 MCP 的智能体都能批准自己刚写进去的卡,
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
// 启动:node bin/dsh-memory.mjs mcp (或 node lib/mcp.js)
|
|
15
16
|
|
|
16
17
|
import readline from 'node:readline'
|
|
17
|
-
import { search, overview, stats, ensureVault, listCards, countCards } from './vault.js'
|
|
18
|
+
import { search, overview, stats, ensureVault, listCards, countCards, reviseCard } from './vault.js'
|
|
18
19
|
import { runStandaloneCapture, defaultVaultDir } from './capture-run.js'
|
|
19
20
|
import { recordLedger } from './ledger.js'
|
|
20
21
|
|
|
@@ -72,6 +73,25 @@ export const MCP_TOOLS = [
|
|
|
72
73
|
},
|
|
73
74
|
},
|
|
74
75
|
},
|
|
76
|
+
{
|
|
77
|
+
name: 'memory_revise',
|
|
78
|
+
description:
|
|
79
|
+
'提交「审核制修改」:把一张已批准的知识卡复制成待审副本(可改标题/标签/正文),进入审核中心。' +
|
|
80
|
+
'人工批准后才会覆盖原卡;agent 不能直接改已入库内容,也不能自己批准。' +
|
|
81
|
+
'用于修正过期/错误的知识 —— 比再写一张新卡更干净(审核界面会标成「修改」,通过即覆盖)。',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
path: { type: 'string', description: '要修改的原卡 path(memory_recall 结果里的「路径:…」)' },
|
|
86
|
+
title: { type: 'string', description: '新标题(不改就不传)' },
|
|
87
|
+
tags: { type: 'array', items: { type: 'string' }, description: '新标签(不改就不传)' },
|
|
88
|
+
body: { type: 'string', description: '新正文(不改就不传)' },
|
|
89
|
+
reason: { type: 'string', description: '修改原因,会显示在审核中心' },
|
|
90
|
+
source: { type: 'string', description: '来源标记,如 claude-code / codex' },
|
|
91
|
+
},
|
|
92
|
+
required: ['path'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
75
95
|
]
|
|
76
96
|
|
|
77
97
|
function toolResult(text, isError = false) {
|
|
@@ -108,6 +128,25 @@ export async function callTool(name, args, vaultRoot, meta = {}) {
|
|
|
108
128
|
if (!out.ok && out.action === 'failed') return toolResult(`沉淀失败:${out.reason}`, true)
|
|
109
129
|
return toolResult(JSON.stringify(out))
|
|
110
130
|
}
|
|
131
|
+
case 'memory_revise': {
|
|
132
|
+
const rel = String(args?.path || '').trim()
|
|
133
|
+
if (!rel) return toolResult('(未提供要修改的卡片 path)')
|
|
134
|
+
try {
|
|
135
|
+
const src = String(args?.source || meta.submittedBy || 'mcp')
|
|
136
|
+
const out = await reviseCard(vaultRoot, {
|
|
137
|
+
target: rel,
|
|
138
|
+
title: args?.title === undefined ? undefined : String(args.title),
|
|
139
|
+
tags: Array.isArray(args?.tags) ? args.tags : undefined,
|
|
140
|
+
body: args?.body === undefined ? undefined : String(args.body),
|
|
141
|
+
reason: String(args?.reason || ''),
|
|
142
|
+
source: src,
|
|
143
|
+
submittedBy: src,
|
|
144
|
+
})
|
|
145
|
+
return toolResult('已提交修改,待人工审核通过后覆盖原卡。\n修改副本:' + out.path + '\n原卡:' + out.target + (out.reused ? '\n(已存在待审修改,本次就地更新,未新增条目)' : ''))
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return toolResult('提交修改失败:' + ((e && e.message) || e), true)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
111
150
|
case 'memory_audit_list': {
|
|
112
151
|
await ensureVault(vaultRoot)
|
|
113
152
|
const wanted = String(args?.status || 'pending').toLowerCase()
|
package/lib/node-bin.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
//
|
|
12
12
|
// 本模块把「找出一个真正能跑 node 脚本的子进程入口」收敛到一处:
|
|
13
13
|
// 1. MEMORY_ETERNAL_NODE 显式指定(逃生口,便于测试与非常规宿主);
|
|
14
|
-
// 2. 非 Electron
|
|
15
|
-
//
|
|
14
|
+
// 2. 非 Electron 宿主:**优先从 PATH 找 node**(issue #56:brew/nvm/volta 在 PATH 上的入口
|
|
15
|
+
// 是稳定 shim,而 process.execPath 是带版本号的真实路径),execPath 降为兜底;
|
|
16
|
+
// 3. Electron 宿主:同样优先从 PATH 找真正的 node;
|
|
16
17
|
// 4. 都找不到时退回 process.execPath,并由 childEnv() 打开
|
|
17
18
|
// ELECTRON_RUN_AS_NODE,让 Electron 二进制以纯 node 模式运行。
|
|
18
19
|
|
|
@@ -22,8 +23,8 @@ import path from 'node:path'
|
|
|
22
23
|
let cachedBinary = null
|
|
23
24
|
let cachedSource = null
|
|
24
25
|
|
|
25
|
-
/** 在 PATH 中查找真正的 node 可执行文件;找不到返回 null
|
|
26
|
-
function findNodeOnPath() {
|
|
26
|
+
/** 在 PATH 中查找真正的 node 可执行文件;找不到返回 null。(导出便于测试断言 PATH 优先) */
|
|
27
|
+
export function findNodeOnPath() {
|
|
27
28
|
const names = process.platform === 'win32' ? ['node.exe', 'node.cmd', 'node'] : ['node', 'node.exe']
|
|
28
29
|
const dirs = String(process.env.PATH || '').split(path.delimiter)
|
|
29
30
|
for (const dir of dirs) {
|
|
@@ -72,17 +73,21 @@ export function nodeBinary() {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
if (!process.versions.electron) {
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
76
|
+
// issue #56:非 Electron 下**必须 PATH 优先**。process.execPath 是「这一次装的那份 node」的
|
|
77
|
+
// 真实路径(macOS Homebrew:/opt/homebrew/Cellar/node/26.13.0_2/bin/node),它会被写进
|
|
78
|
+
// ~/.claude.json 等外部客户端的 MCP 条目;brew upgrade node 一删旧目录,这些条目就 ENOENT。
|
|
79
|
+
// PATH 上的入口(/opt/homebrew/bin/node、~/.volta/bin/node、nvm 的 shim)才是稳定的。
|
|
80
|
+
// 同时它决定了 align 的 currentNodePath —— 写入侧与校验侧一起收敛到 shim,才不会互相打脸。
|
|
81
|
+
// execPath 仍保留为兜底(PATH 上没有 node 时行为与 #44 之前一致)。
|
|
82
82
|
const viaPath = findNodeOnPath()
|
|
83
83
|
if (viaPath) {
|
|
84
84
|
cachedBinary = viaPath
|
|
85
|
-
cachedSource = 'path
|
|
85
|
+
cachedSource = 'path'
|
|
86
|
+
return cachedBinary
|
|
87
|
+
}
|
|
88
|
+
if (exists(process.execPath)) {
|
|
89
|
+
cachedBinary = process.execPath
|
|
90
|
+
cachedSource = 'execPath'
|
|
86
91
|
return cachedBinary
|
|
87
92
|
}
|
|
88
93
|
cachedBinary = process.execPath
|
package/lib/vault.js
CHANGED
|
@@ -341,7 +341,7 @@ export async function listCards(root, { status, kind, agent, tag = '', limit, of
|
|
|
341
341
|
rows.push(...db.prepare(`SELECT * FROM ${store.table} WHERE path IN (${ph})`).all(...paths))
|
|
342
342
|
}
|
|
343
343
|
const byPath = new Map(rows.map((r) => [r.path, rowToCard(r)]))
|
|
344
|
-
return page.map((r) => byPath.get(r.path)).filter(Boolean)
|
|
344
|
+
return attachRevisions(db, page.map((r) => byPath.get(r.path)).filter(Boolean))
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
const tail = Number.isInteger(limit) && limit > 0
|
|
@@ -355,7 +355,7 @@ export async function listCards(root, { status, kind, agent, tag = '', limit, of
|
|
|
355
355
|
{ kind, agent, tag, tail },
|
|
356
356
|
)
|
|
357
357
|
const finalArgs = Number.isInteger(limit) && limit > 0 ? [...args, limit, Math.max(0, offset | 0)] : args
|
|
358
|
-
return db.prepare(sql).all(...finalArgs).map(rowToCard)
|
|
358
|
+
return attachRevisions(db, db.prepare(sql).all(...finalArgs).map(rowToCard))
|
|
359
359
|
}
|
|
360
360
|
|
|
361
361
|
/** 计数(分页接口要用 total 判断还有没有下一页)。 */
|
|
@@ -464,12 +464,15 @@ export async function readCard(root, rel, { allowUnapproved = false } = {}) {
|
|
|
464
464
|
}
|
|
465
465
|
const status = String(row.status || '')
|
|
466
466
|
const { text } = rowToMarkdown(row)
|
|
467
|
+
// 修改副本要带上「改的是哪张卡」,审核界面据此标注「修改」而不是「新卡」。
|
|
468
|
+
const rev = getRevision(db, rel)
|
|
469
|
+
const revision = rev ? { targetPath: rev.target_path, targetId: rev.target_id } : null
|
|
467
470
|
const updates = db.prepare('SELECT content, created_at FROM card_updates WHERE card_id = ? ORDER BY created_at ASC').all(row.id)
|
|
468
471
|
if (updates.length > 0) {
|
|
469
472
|
const updateLines = updates.map((u) => `- ${u.created_at.slice(0, 10)}:${u.content}`)
|
|
470
|
-
return { path: rel, status, store: mainRow ? 'main' : 'quarantine', text: text + '\n## 更新记录\n\n' + updateLines.join('\n') + '\n' }
|
|
473
|
+
return { path: rel, status, store: mainRow ? 'main' : 'quarantine', text: text + '\n## 更新记录\n\n' + updateLines.join('\n') + '\n', ...(revision ? { revision } : {}) }
|
|
471
474
|
}
|
|
472
|
-
return { path: rel, status, store: mainRow ? 'main' : 'quarantine', text }
|
|
475
|
+
return { path: rel, status, store: mainRow ? 'main' : 'quarantine', text, ...(revision ? { revision } : {}) }
|
|
473
476
|
}
|
|
474
477
|
|
|
475
478
|
/** 状态中文标签(错误信息与日志用)。 */
|
|
@@ -499,6 +502,11 @@ export async function deleteCard(root, rel, { permanent = false } = {}) {
|
|
|
499
502
|
db.prepare('DELETE FROM card_updates WHERE card_id = ?').run(id)
|
|
500
503
|
db.prepare('DELETE FROM feedback WHERE card_path = ?').run(rel)
|
|
501
504
|
}
|
|
505
|
+
// 修改副本与原卡的关联也一起清掉:删原卡时,指向它的待审副本没有应用对象了
|
|
506
|
+
const revs = db.prepare('SELECT revision_path FROM card_revisions WHERE target_path = ?').all(rel)
|
|
507
|
+
for (const r of revs) db.prepare('DELETE FROM quarantine WHERE path = ?').run(r.revision_path)
|
|
508
|
+
db.prepare('DELETE FROM card_revisions WHERE target_path = ?').run(rel)
|
|
509
|
+
db.prepare('DELETE FROM card_revisions WHERE revision_path = ?').run(rel)
|
|
502
510
|
db.prepare('DELETE FROM cards WHERE path = ?').run(rel)
|
|
503
511
|
db.prepare('DELETE FROM quarantine WHERE path = ?').run(rel)
|
|
504
512
|
})
|
|
@@ -522,6 +530,8 @@ export async function restoreCard(root, rel) {
|
|
|
522
530
|
const db = getDb(root)
|
|
523
531
|
const existing = db.prepare('SELECT status FROM quarantine WHERE path = ?').get(rel)
|
|
524
532
|
if (!existing) throw new Error('卡片不在回收站/隔离区: ' + rel)
|
|
533
|
+
// 修改副本不能「恢复」成一张独立卡 —— 那会跟原卡重复。它只能被批准(覆盖原卡)或删除。
|
|
534
|
+
if (getRevision(db, rel)) throw new Error('这是知识卡的修改副本:请在审核中心「批准」以覆盖原卡,或直接删除')
|
|
525
535
|
const r = moveCardBetweenStores(db, 'quarantine', 'cards', rel, { status: MAIN_STATUS, deletedAt: null })
|
|
526
536
|
db.prepare('INSERT INTO audit_log (card_path, old_status, new_status, changed_by, reason) VALUES (?, ?, ?, ?, ?)').run(rel, existing.status, MAIN_STATUS, 'restore', '回收站恢复')
|
|
527
537
|
return { ok: true, restored: true, ...r }
|
|
@@ -539,6 +549,12 @@ export async function setCardStatus(root, rel, status, { changedBy = 'system', r
|
|
|
539
549
|
const existing = findCardRow(db, rel)
|
|
540
550
|
if (!existing) throw new Error('卡片不存在: ' + rel)
|
|
541
551
|
|
|
552
|
+
// 审核制修改:待审副本被批准 = 用副本内容覆盖原卡(身份/状态/类型不变),副本随即消失。
|
|
553
|
+
if (target === MAIN_STATUS && existing.table === 'quarantine') {
|
|
554
|
+
const rev = getRevision(db, rel)
|
|
555
|
+
if (rev) return applyRevision(db, rel, rev, { changedBy, reason })
|
|
556
|
+
}
|
|
557
|
+
|
|
542
558
|
if (target === MAIN_STATUS) {
|
|
543
559
|
if (existing.table === 'cards') {
|
|
544
560
|
db.prepare('UPDATE cards SET status = ?, updated_at = ? WHERE path = ?').run(target, new Date().toISOString(), rel)
|
|
@@ -702,6 +718,99 @@ export async function updateCardContent(root, rel, { title, tags, body, reason }
|
|
|
702
718
|
return { ok: true, path: rel, store: found.table, updated: now }
|
|
703
719
|
}
|
|
704
720
|
|
|
721
|
+
// ── 审核制修改(agent 提交 → 人工审核 → 覆盖原卡)────────────────────────
|
|
722
|
+
|
|
723
|
+
/** 查「待审副本 → 原卡」映射;不是修改副本时返回 null。 */
|
|
724
|
+
function getRevision(db, revisionPath) {
|
|
725
|
+
return db.prepare('SELECT * FROM card_revisions WHERE revision_path = ?').get(revisionPath) || null
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/** 给卡片对象挂上 revision 信息(审核中心据此标「修改」而不是「新卡」)。 */
|
|
729
|
+
function attachRevisions(db, cards) {
|
|
730
|
+
if (!cards.length) return cards
|
|
731
|
+
const ph = cards.map(() => '?').join(',')
|
|
732
|
+
const rows = db.prepare(`SELECT revision_path, target_path, target_id FROM card_revisions WHERE revision_path IN (${ph})`).all(...cards.map((c) => c.path))
|
|
733
|
+
if (!rows.length) return cards
|
|
734
|
+
const byPath = new Map(rows.map((r) => [r.revision_path, r]))
|
|
735
|
+
const targets = [...new Set(rows.map((r) => r.target_path))]
|
|
736
|
+
const tph = targets.map(() => '?').join(',')
|
|
737
|
+
const titles = new Map(db.prepare(`SELECT path, title FROM cards WHERE path IN (${tph})`).all(...targets).map((r) => [r.path, r.title]))
|
|
738
|
+
return cards.map((c) => {
|
|
739
|
+
const r = byPath.get(c.path)
|
|
740
|
+
return r ? { ...c, revision: { targetPath: r.target_path, targetId: r.target_id, targetTitle: titles.get(r.target_path) || '' } } : c
|
|
741
|
+
})
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* 提交一张「审核制修改」:把已批准的卡片复制一份到隔离区(status=pending),
|
|
746
|
+
* 内容用调用方给的新标题/标签/正文。**这里绝不改原卡** —— 只有审核通过(setCardStatus)
|
|
747
|
+
* 才会用副本覆盖原卡。agent 走 /card/revise 或 MCP memory_revise,只能到这一步。
|
|
748
|
+
*
|
|
749
|
+
* 同一张卡已有待审修改时就地更新,避免审核队列里堆一串同一张卡的副本。
|
|
750
|
+
*/
|
|
751
|
+
export async function reviseCard(root, { target, title, tags, body, reason = '', source = '', submittedBy = '' } = {}) {
|
|
752
|
+
const db = getDb(root)
|
|
753
|
+
const rel = String(target || '').trim()
|
|
754
|
+
if (!rel) throw new Error('缺少要修改的卡片 path')
|
|
755
|
+
const found = findCardRow(db, rel)
|
|
756
|
+
if (!found) throw new Error('原卡片不存在: ' + rel)
|
|
757
|
+
if (found.table !== 'cards') throw new Error('只能修改已审核通过的卡片(当前状态:' + found.row.status + ')')
|
|
758
|
+
const targetRow = found.row
|
|
759
|
+
// 已有待审修改时,未传的字段沿用副本当前值(否则第二次提交会把上次的改动抹掉)
|
|
760
|
+
const pending = db.prepare('SELECT q.* FROM card_revisions r JOIN quarantine q ON q.path = r.revision_path WHERE r.target_path = ? AND q.status = ?').get(rel, 'pending')
|
|
761
|
+
const baseRow = pending || targetRow
|
|
762
|
+
const nextTitle = title === undefined ? baseRow.title : String(title).trim()
|
|
763
|
+
const nextTags = tags === undefined ? normalizeTags(baseRow.tags) : normalizeTags(tags)
|
|
764
|
+
const nextBody = body === undefined ? String(baseRow.body).trim() : String(body).trim()
|
|
765
|
+
if (!nextTitle) throw new Error('标题不能为空')
|
|
766
|
+
if (!nextBody) throw new Error('正文不能为空')
|
|
767
|
+
const now = new Date().toISOString()
|
|
768
|
+
if (pending) {
|
|
769
|
+
db.prepare('UPDATE quarantine SET title = ?, tags = ?, body = ?, summary = ?, updated_at = ?, reason = ? WHERE path = ?')
|
|
770
|
+
.run(nextTitle, JSON.stringify(nextTags), nextBody, nextBody.slice(0, 200), now, reason || pending.reason || '', pending.path)
|
|
771
|
+
db.prepare('UPDATE card_revisions SET updated_at = ? WHERE revision_path = ?').run(now, pending.path)
|
|
772
|
+
return { ok: true, path: pending.path, target: rel, targetId: targetRow.id, reused: true, status: 'pending' }
|
|
773
|
+
}
|
|
774
|
+
// 副本放在独立目录 99-Revisions/:不与任何 kind 根目录冲突,也永远不会被当成新卡批准进主库
|
|
775
|
+
const base = `99-Revisions/${safeSlug(targetRow.title || 'card')}`
|
|
776
|
+
let revisionPath = `${base}.md`
|
|
777
|
+
let i = 2
|
|
778
|
+
while (pathTaken(db, revisionPath)) { revisionPath = `${base}-${i}.md`; i++ }
|
|
779
|
+
const id = nextCardId(db)
|
|
780
|
+
db.prepare(`
|
|
781
|
+
INSERT INTO quarantine
|
|
782
|
+
(id, path, kind, title, tags, body, summary, status, source, submitted_by, severity, reason, created_at, updated_at, quarantined_at, quarantine_reason)
|
|
783
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, 'info', ?, ?, ?, datetime('now'), '修改待审')
|
|
784
|
+
`).run(id, revisionPath, targetRow.kind, nextTitle, JSON.stringify(nextTags), nextBody, nextBody.slice(0, 200), source, submittedBy, reason || ('修改知识卡:' + rel), now, now)
|
|
785
|
+
db.prepare('INSERT INTO card_revisions (revision_path, target_path, target_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)').run(revisionPath, rel, targetRow.id, now, now)
|
|
786
|
+
db.prepare('INSERT INTO audit_log (card_path, old_status, new_status, changed_by, reason) VALUES (?, ?, ?, ?, ?)').run(revisionPath, null, 'pending', submittedBy || 'agent', '提交修改:' + rel)
|
|
787
|
+
return { ok: true, path: revisionPath, target: rel, targetId: targetRow.id, reused: false, status: 'pending' }
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* 审核通过修改副本:用副本内容覆盖原卡,副本及其映射一起消失。
|
|
792
|
+
* 只改内容(标题 / 标签 / 正文 / 摘要 + updated_at);状态、类型、来源、创建时间仍属原卡。
|
|
793
|
+
*/
|
|
794
|
+
function applyRevision(db, revisionPath, rev, { changedBy = 'user', reason = '' } = {}) {
|
|
795
|
+
return inTransaction(db, () => {
|
|
796
|
+
const revRow = db.prepare('SELECT * FROM quarantine WHERE path = ?').get(revisionPath)
|
|
797
|
+
if (!revRow) throw new Error('修改副本不存在: ' + revisionPath)
|
|
798
|
+
const target = db.prepare('SELECT * FROM cards WHERE path = ?').get(rev.target_path)
|
|
799
|
+
if (!target) throw new Error('原卡片已不存在(可能已被删除):' + rev.target_path)
|
|
800
|
+
const now = new Date().toISOString()
|
|
801
|
+
const tags = normalizeTags(revRow.tags)
|
|
802
|
+
db.prepare('UPDATE cards SET title = ?, tags = ?, body = ?, summary = ?, updated_at = ? WHERE path = ?')
|
|
803
|
+
.run(revRow.title, JSON.stringify(tags), revRow.body, String(revRow.body).trim().slice(0, 200), now, rev.target_path)
|
|
804
|
+
db.prepare('INSERT INTO card_updates (card_id, content, created_at) VALUES (?, ?, ?)')
|
|
805
|
+
.run(target.id, '经审核修改:' + (reason || ('由 ' + (revRow.submitted_by || 'agent') + ' 提交,' + changedBy + ' 批准')), now)
|
|
806
|
+
db.prepare('DELETE FROM quarantine WHERE path = ?').run(revisionPath)
|
|
807
|
+
db.prepare('DELETE FROM card_revisions WHERE revision_path = ?').run(revisionPath)
|
|
808
|
+
db.prepare('INSERT INTO audit_log (card_path, old_status, new_status, changed_by, reason) VALUES (?, ?, ?, ?, ?)')
|
|
809
|
+
.run(rev.target_path, MAIN_STATUS, MAIN_STATUS, changedBy, '应用修改(来源 ' + revisionPath + ')')
|
|
810
|
+
return { ok: true, status: MAIN_STATUS, store: 'cards', applied: true, path: rev.target_path, revisionPath, updated: now }
|
|
811
|
+
})
|
|
812
|
+
}
|
|
813
|
+
|
|
705
814
|
/** 合并多张卡为一张。 */
|
|
706
815
|
export async function mergeCards(root, paths) {
|
|
707
816
|
const list = (Array.isArray(paths) ? paths : []).map((p) => String(p || '').trim()).filter(Boolean)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"id": "memory-eternal",
|
|
4
4
|
"publisher": "eternalnight996",
|
|
5
5
|
"name": "memory-eternal",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.22.0",
|
|
7
7
|
"description": "Memory Atlas for Orca: open the local memory vault, pending review and knowledge graph from a sandboxed Orca panel.",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "EternalNight996",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-eternal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "记忆核心(Memory Eternal):自研的 DeepSeek Harness 记忆插件,不移植任何既有记忆框架——对话结束后自动沉淀知识卡到本地 Markdown Vault(自研去重、自研 CJK 检索、可 git 管理),设置页提供图形化知识库(统计 / 搜索 / 知识图谱 + 侧边栏一键弹窗),Agent 通过 memory_recall 工具按需召回历史上下文。零人工干预。",
|
|
5
5
|
"icon": "assets/icon.png",
|
|
6
6
|
"license": "MIT",
|