sage-mem 1.0.2 → 1.1.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/README.md +2 -2
- package/README.zh.md +2 -2
- package/lib/client.js +93 -16
- package/lib/index.js +17 -1
- package/lib/memory/archive.js +102 -1
- package/lib/typert.host.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ Every image below is **rendered from the UI code in this repository** (the same
|
|
|
151
151
|
|
|
152
152
|

|
|
153
153
|
|
|
154
|
-
*File list — the header always shows 「体检 N 个问题」(audit: N issues) and 「待重启生效」(restart to apply), so you know whether you should deal with it without opening anything; greyed rows are archived and can be restored at any time.*
|
|
154
|
+
*File list — the header always shows 「体检 N 个问题」(audit: N issues) and 「待重启生效」(restart to apply), so you know whether you should deal with it without opening anything; greyed rows are archived and can be restored at any time, or deleted for good with 「彻底删除」(irreversible, asks once more).*
|
|
155
155
|
|
|
156
156
|

|
|
157
157
|
|
|
@@ -171,7 +171,7 @@ Every image below is **rendered from the UI code in this repository** (the same
|
|
|
171
171
|
|
|
172
172
|

|
|
173
173
|
|
|
174
|
-
*Archive candidates — each states "why archiving is suggested" (idle days vs the threshold for that type); files are touched only after you confirm one by one or select all. Archiving moves a file into `archive/`, it **does not delete
|
|
174
|
+
*Archive candidates — each states "why archiving is suggested" (idle days vs the threshold for that type); files are touched only after you confirm one by one or select all. Archiving moves a file into `archive/`, it **does not delete**; to actually delete an archived entry, click 「彻底删除」(delete permanently) in the archive view and confirm once more.*
|
|
175
175
|
|
|
176
176
|

|
|
177
177
|
|
package/README.zh.md
CHANGED
|
@@ -153,7 +153,7 @@ SAGE_MEM_DIR=/path/to/your/memory
|
|
|
153
153
|
|
|
154
154
|

|
|
155
155
|
|
|
156
|
-
*文件列表 —— 头部常显「体检 N
|
|
156
|
+
*文件列表 —— 头部常显「体检 N 个问题」与「待重启生效」,不用点开就知道该不该处理;灰底条目是已归档,随时可以恢复,也可以点「彻底删除」真的删掉(不可恢复,要再确认一次)。*
|
|
157
157
|
|
|
158
158
|

|
|
159
159
|
|
|
@@ -173,7 +173,7 @@ SAGE_MEM_DIR=/path/to/your/memory
|
|
|
173
173
|
|
|
174
174
|

|
|
175
175
|
|
|
176
|
-
*归档候选 —— 每条写清「为什么建议归档」(闲置天数 vs 该类型阈值),逐条或全选后确认才动文件。归档是把文件移进 `archive
|
|
176
|
+
*归档候选 —— 每条写清「为什么建议归档」(闲置天数 vs 该类型阈值),逐条或全选后确认才动文件。归档是把文件移进 `archive/`,**不删除**;真要删掉一条归档件,得在档案馆里单独点「彻底删除」并再确认一次。*
|
|
177
177
|
|
|
178
178
|

|
|
179
179
|
|
package/lib/client.js
CHANGED
|
@@ -1605,6 +1605,9 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1605
1605
|
// 归档文件的读 / 写(B1b):形状与活跃记忆那套的 readSchema / writeSchema 对称。
|
|
1606
1606
|
const archivedReadSchema = parseSchema({ "name": isStr, "content": isStr });
|
|
1607
1607
|
const archivedWriteSchema = parseSchema({ "ok": isBool, "file": isStr });
|
|
1608
|
+
// 「彻底删除」的入参:归档代次(`archivedAt`)。它不是装饰 —— 宿主拿它核对
|
|
1609
|
+
// 「要删的是不是用户确认过的那一版」。老归档件没有留痕时传空串,所以只校类型。
|
|
1610
|
+
const archiveStampSchema = strSchema;
|
|
1608
1611
|
|
|
1609
1612
|
// ── autodream 的 codec ──
|
|
1610
1613
|
// 刻意写得宽:这几个方法返回的是配置 / 状态对象,字段会随版本增删。
|
|
@@ -1734,6 +1737,21 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1734
1737
|
result: { mode: "strict", typeSymbol: "sage-mem/types#ArchivedWriteResult", schema: archivedWriteSchema, create: () => archivedWriteSchema },
|
|
1735
1738
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1736
1739
|
},
|
|
1740
|
+
{
|
|
1741
|
+
// 彻底删除一条归档件(不可恢复)。第二个参数是归档代次:宿主拿它核对
|
|
1742
|
+
// 「要删的是不是用户确认过的那一版」,传错(比如列表过期)会被拒绝。
|
|
1743
|
+
id: "sage-mem#memory/deleteArchived",
|
|
1744
|
+
service: "memory",
|
|
1745
|
+
namespace: "memory",
|
|
1746
|
+
method: "deleteArchived",
|
|
1747
|
+
invocation: { kind: "direct" },
|
|
1748
|
+
parameters: [
|
|
1749
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#ArchivedFileName", schema: strSchema, create: () => strSchema } },
|
|
1750
|
+
{ name: "archivedAt", wire: "archivedAt", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#ArchiveStamp", schema: archiveStampSchema, create: () => archiveStampSchema } },
|
|
1751
|
+
],
|
|
1752
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#ArchiveResult", schema: archiveSchema, create: () => archiveSchema },
|
|
1753
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1754
|
+
},
|
|
1737
1755
|
// ── 面板 API(D1):体检 / 归档候选 / 设置 / 保留名原样读写 ──
|
|
1738
1756
|
// 结果形状都在演进(审计清单、候选表、设置项),顶层用 objCodec 接住、
|
|
1739
1757
|
// 内部宽松 —— 与 autodream 那几个 codec 同一套理由:host 加字段不该让旧界面整体报错。
|
|
@@ -2555,7 +2573,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2555
2573
|
|
|
2556
2574
|
/**
|
|
2557
2575
|
* 归档区的一条。与活动卡片共用骨架(类型章 / 标题 / 标签 / 描述),刻意的差异:
|
|
2558
|
-
* 虚线边框 + 整体压灰(退役感)、多一行归档留痕、动作是「编辑 /
|
|
2576
|
+
* 虚线边框 + 整体压灰(退役感)、多一行归档留痕、动作是「编辑 / 恢复 / 彻底删除」。
|
|
2559
2577
|
*
|
|
2560
2578
|
* ⚠️ 灰的只有**内容**(`.smem-card--archived` 那几条只点标题/类型章/标签/正文的字色)。
|
|
2561
2579
|
* 按钮一律保持原样 —— 按钮一灰,用户就以为不能按。
|
|
@@ -2563,7 +2581,10 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2563
2581
|
* ⚠️ 编辑走**原样文本**:readArchived 拿到的全文直接进 textarea,保存时原样交回
|
|
2564
2582
|
* writeArchived。绝不走活跃记忆那套 buildFrontmatter 重建 —— 重建会吞字段,
|
|
2565
2583
|
* 归档文件里还有 archived_at / archived_reason 这种留痕,更动不得。
|
|
2566
|
-
*
|
|
2584
|
+
*
|
|
2585
|
+
* 「彻底删除」是 v1.1.0 加的**唯一**一个不可逆入口,所以它走两段式:
|
|
2586
|
+
* 点一下只展开一句「真的删、不能恢复」的确认条,再点「确认彻底删除」才动手。
|
|
2587
|
+
* 归档的承诺(只移不删)没有被它破坏 —— 归档动作仍然一个字节都不删。
|
|
2567
2588
|
*/
|
|
2568
2589
|
function ArchivedCard(props) {
|
|
2569
2590
|
const item = props.item;
|
|
@@ -2603,19 +2624,39 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2603
2624
|
h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: props.busy, onClick: props.onCancel }, "取消"),
|
|
2604
2625
|
]),
|
|
2605
2626
|
])
|
|
2606
|
-
:
|
|
2607
|
-
|
|
2608
|
-
h("
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
h("
|
|
2614
|
-
type: "button", className: "smem-btn smem-btn-
|
|
2615
|
-
disabled: props.busy,
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2627
|
+
: props.deleting
|
|
2628
|
+
? h("div", { className: "smem-ar-confirm", key: "del" }, [
|
|
2629
|
+
h("div", { key: "what" },
|
|
2630
|
+
"彻底删除「" + item.file + "」:它会从 archive/ 里被真的删掉,不能恢复。" +
|
|
2631
|
+
"要保留它就先点「恢复」,把它放回活动记忆。"),
|
|
2632
|
+
h("div", { className: "smem-ar-hint", key: "hint" },
|
|
2633
|
+
"这一步之后没有任何界面能把内容找回来 —— 只剩你自己在磁盘上留过备份。"),
|
|
2634
|
+
h("div", { className: "smem-row", key: "btns" }, [
|
|
2635
|
+
h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", disabled: props.busy, onClick: function () { props.onConfirmDelete(item.file); } }, "确认彻底删除"),
|
|
2636
|
+
h("button", { type: "button", className: "smem-btn", key: "no", disabled: props.busy, onClick: props.onCancelDelete }, "取消"),
|
|
2637
|
+
]),
|
|
2638
|
+
])
|
|
2639
|
+
: h("div", { className: "smem-card-foot", key: "foot" },
|
|
2640
|
+
h("div", { className: "smem-actions", key: "acts" }, [
|
|
2641
|
+
h("button", {
|
|
2642
|
+
type: "button", className: "smem-btn", key: "edit",
|
|
2643
|
+
disabled: props.busy,
|
|
2644
|
+
onClick: function () { props.onEdit(item.file); },
|
|
2645
|
+
}, "编辑"),
|
|
2646
|
+
h("button", {
|
|
2647
|
+
type: "button", className: "smem-btn smem-btn-primary", key: "restore",
|
|
2648
|
+
disabled: props.busy,
|
|
2649
|
+
onClick: function () { props.onRestore(item.file); },
|
|
2650
|
+
}, props.restoring ? "恢复中…" : "恢复"),
|
|
2651
|
+
// 放在最后、且用 danger-ghost:它是这张卡上唯一不可逆的动作,
|
|
2652
|
+
// 不该跟「恢复」长得一样。
|
|
2653
|
+
h("button", {
|
|
2654
|
+
type: "button", className: "smem-btn smem-btn-danger-ghost", key: "purge",
|
|
2655
|
+
disabled: props.busy,
|
|
2656
|
+
title: "从 archive/ 里彻底删掉,不可恢复(要留就先恢复)",
|
|
2657
|
+
onClick: function () { props.onAskDelete(item.file); },
|
|
2658
|
+
}, "彻底删除"),
|
|
2659
|
+
])),
|
|
2619
2660
|
]);
|
|
2620
2661
|
}
|
|
2621
2662
|
|
|
@@ -3804,6 +3845,10 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
3804
3845
|
const archiveReason = sArchiveReason[0], setArchiveReason = sArchiveReason[1];
|
|
3805
3846
|
const sRestoring = react.useState(null);
|
|
3806
3847
|
const restoring = sRestoring[0], setRestoring = sRestoring[1];
|
|
3848
|
+
// 「彻底删除」的二次确认:正在问哪一条(null = 没在问)。
|
|
3849
|
+
// 与 editing 互斥 —— 同一张卡上不能既在改又在删。
|
|
3850
|
+
const sArchDelete = react.useState(null);
|
|
3851
|
+
const archDelete = sArchDelete[0], setArchDelete = sArchDelete[1];
|
|
3807
3852
|
// 归档文件的「原样文本」编辑器:正在编辑谁、编辑框里是什么、读/存进行中
|
|
3808
3853
|
const sArchEdit = react.useState(null);
|
|
3809
3854
|
const archEdit = sArchEdit[0], setArchEdit = sArchEdit[1];
|
|
@@ -4073,6 +4118,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
4073
4118
|
}
|
|
4074
4119
|
setBusy(true); setError(null); setNotice("");
|
|
4075
4120
|
setRestoring(file);
|
|
4121
|
+
setArchDelete(null);
|
|
4076
4122
|
remote.restore(file)
|
|
4077
4123
|
.then(function (r) {
|
|
4078
4124
|
if (r && r.ok === false) { setError(r.error || "恢复被拒绝"); return; }
|
|
@@ -4084,6 +4130,31 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
4084
4130
|
.then(function () { setBusy(false); setRestoring(null); });
|
|
4085
4131
|
}
|
|
4086
4132
|
|
|
4133
|
+
/**
|
|
4134
|
+
* **彻底删除**一条归档件(不可恢复)。
|
|
4135
|
+
*
|
|
4136
|
+
* 第二个参数是归档代次(列表上看到的 `archivedAt`):宿主拿它核对「要删的是不是
|
|
4137
|
+
* 我确认过的那一版」。不传它会变成「按名字删」—— 名字底下的内容刚才可能已经换人了。
|
|
4138
|
+
*/
|
|
4139
|
+
function deleteArchivedFile(file) {
|
|
4140
|
+
if (typeof remote.deleteArchived !== "function") {
|
|
4141
|
+
setError("宿主侧还没有 deleteArchived 接口(插件版本较旧)");
|
|
4142
|
+
return;
|
|
4143
|
+
}
|
|
4144
|
+
const item = archiveItems.filter(function (x) { return x.file === file; })[0];
|
|
4145
|
+
const stamp = (item && item.archivedAt) || "";
|
|
4146
|
+
setBusy(true); setError(null); setNotice("");
|
|
4147
|
+
remote.deleteArchived(file, stamp)
|
|
4148
|
+
.then(function (r) {
|
|
4149
|
+
if (r && r.ok === false) { setError(r.error || "删除被拒绝"); return; }
|
|
4150
|
+
setArchDelete(null);
|
|
4151
|
+
setNotice("已彻底删除 " + file + " —— 归档区里也没有它了,找不回来");
|
|
4152
|
+
load();
|
|
4153
|
+
})
|
|
4154
|
+
.catch(function (e) { setError(String((e && e.message) || e)); })
|
|
4155
|
+
.then(function () { setBusy(false); });
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4087
4158
|
/**
|
|
4088
4159
|
* 打开归档文件的**原样文本**编辑器。
|
|
4089
4160
|
*
|
|
@@ -4098,6 +4169,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
4098
4169
|
return;
|
|
4099
4170
|
}
|
|
4100
4171
|
setError(null); setNotice("");
|
|
4172
|
+
setArchDelete(null);
|
|
4101
4173
|
setArchEdit(file);
|
|
4102
4174
|
setArchText("");
|
|
4103
4175
|
setArchLoad(true);
|
|
@@ -4368,6 +4440,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
4368
4440
|
item: it,
|
|
4369
4441
|
busy: busy,
|
|
4370
4442
|
restoring: restoring === it.file,
|
|
4443
|
+
deleting: archDelete === it.file,
|
|
4371
4444
|
editing: archEdit === it.file,
|
|
4372
4445
|
loading: archLoad && archEdit === it.file,
|
|
4373
4446
|
saving: archSaving && archEdit === it.file,
|
|
@@ -4377,6 +4450,10 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
4377
4450
|
onSave: saveArchEdit,
|
|
4378
4451
|
onCancel: cancelArchEdit,
|
|
4379
4452
|
onRestore: restoreFile,
|
|
4453
|
+
// 两个「危险动作的确认」互斥:问到一半又去改文本,界面就会同时摆两个确认条。
|
|
4454
|
+
onAskDelete: function (file) { setArchDelete(file); setArchEdit(null); },
|
|
4455
|
+
onCancelDelete: function () { setArchDelete(null); },
|
|
4456
|
+
onConfirmDelete: deleteArchivedFile,
|
|
4380
4457
|
});
|
|
4381
4458
|
}
|
|
4382
4459
|
return h(Card, {
|
|
@@ -4407,7 +4484,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
4407
4484
|
? [
|
|
4408
4485
|
h("div", { className: "smem-ar-banner", key: "banner" },
|
|
4409
4486
|
"档案馆里的记忆不参与检索与星图,原文仍在记忆目录的 archive/ 下 —— 恢复后它就回到活动列表;" +
|
|
4410
|
-
"
|
|
4487
|
+
"「编辑」是原样文本编辑,改完存回同一份文件;「彻底删除」会真的从 archive/ 里删掉它(不可恢复,删除前还要再确认一次)。"),
|
|
4411
4488
|
list,
|
|
4412
4489
|
]
|
|
4413
4490
|
: list;
|
package/lib/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import { readCheckpoint } from './memory/checkpoint.js'
|
|
|
32
32
|
import { memoryPathOf } from './memory/paths.js'
|
|
33
33
|
import { decideWriteIntent, decideEditGuard } from './memory/write-gate.js'
|
|
34
34
|
import { createMemoryTools } from './tools/memory-tools.js'
|
|
35
|
-
import { archiveMemory, restoreMemory } from './memory/archive.js'
|
|
35
|
+
import { archiveMemory, deleteArchivedMemory, restoreMemory } from './memory/archive.js'
|
|
36
36
|
import { flushAccess, pickArchiveCandidates, readAccessLedger, recordAccess, recordRead } from './memory/access.js'
|
|
37
37
|
import { warnLog, writeFileAtomic } from './memory/util.js'
|
|
38
38
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
@@ -1027,6 +1027,21 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
1027
1027
|
return { ok: true, file: safe }
|
|
1028
1028
|
}
|
|
1029
1029
|
|
|
1030
|
+
/**
|
|
1031
|
+
* **彻底删除**一条已归档记忆(不可恢复)。
|
|
1032
|
+
*
|
|
1033
|
+
* 与 `deleteFile` 的区别要在代码里就分开:那个删的是活动记忆,这个删的是档案馆里的退役件;
|
|
1034
|
+
* 归档「只移不删」的承诺不变 —— 走进档案馆之后,删不删由人单独决定。
|
|
1035
|
+
*
|
|
1036
|
+
* `archivedAt` 是**身份判据**(见 `memory/archive.js` 的头注):调用方交回它当时看见的
|
|
1037
|
+
* 归档时间,盘上那份对不上就拒绝。不传 / 传空串只对「本来就没有留痕的老归档件」成立。
|
|
1038
|
+
*/
|
|
1039
|
+
async deleteArchived(name, archivedAt) {
|
|
1040
|
+
const res = await deleteArchivedMemory(MEMORY_DIR, name, archivedAt)
|
|
1041
|
+
if (!res.ok) return { ok: false, error: res.error }
|
|
1042
|
+
return { ok: true, file: res.file }
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1030
1045
|
/**
|
|
1031
1046
|
* 记忆目录体检:结构化审计结果。
|
|
1032
1047
|
*
|
|
@@ -1233,6 +1248,7 @@ markRemote(MemoryGateway, 'restore', 'restore')
|
|
|
1233
1248
|
markRemote(MemoryGateway, 'listArchived', 'listArchived')
|
|
1234
1249
|
markRemote(MemoryGateway, 'readArchived', 'readArchived')
|
|
1235
1250
|
markRemote(MemoryGateway, 'writeArchived', 'writeArchived')
|
|
1251
|
+
markRemote(MemoryGateway, 'deleteArchived', 'deleteArchived')
|
|
1236
1252
|
markRemote(MemoryGateway, 'audit', 'audit')
|
|
1237
1253
|
markRemote(MemoryGateway, 'checkUpdate', 'checkUpdate')
|
|
1238
1254
|
markRemote(MemoryGateway, 'setLocked', 'setLocked')
|
package/lib/memory/archive.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 就是临时目录),拿模块级常量会写错地方 —— 那是会碰到真实记忆的危险错误。
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { ARCHIVE_DIR_NAME, addArchiveMeta, inspectDir, inspectMemoryEntry, localStamp, stripArchiveMeta } from './scan.js'
|
|
18
|
+
import { ARCHIVE_DIR_NAME, addArchiveMeta, inspectDir, inspectMemoryEntry, localStamp, readArchivedMeta, stripArchiveMeta } from './scan.js'
|
|
19
19
|
import { isReserved, nameKey, safeName } from './naming.js'
|
|
20
20
|
import { writeFileAtomic, writeFileExclusive } from './util.js'
|
|
21
21
|
import { isLocked } from './lock.js'
|
|
@@ -290,3 +290,104 @@ export async function restoreMemory(memoryDir, name) {
|
|
|
290
290
|
...(indexError ? { indexError } : {}),
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 从归档区**彻底删除**一条记忆(不可恢复)。
|
|
296
|
+
*
|
|
297
|
+
* 为什么不复用 `deleteFile`:归档的承诺是「只移不删」,活记忆的 `deleteFile` 承诺的
|
|
298
|
+
* 是「删掉活的这份」。两者混在一个名字下,用户就永远分不清自己按的是哪一个;
|
|
299
|
+
* 而这个操作**没有回收站**。
|
|
300
|
+
*
|
|
301
|
+
* 三道判据,缺一不可:
|
|
302
|
+
* 1. **归档区的目录闸门**:它若被换成指向外部的联接,删掉的是别人的文件;
|
|
303
|
+
* 2. **条目闸门**:链接 / 硬链接指向的可能是记忆目录之外的内容,删它等于替别人删文件;
|
|
304
|
+
* 3. **代次(`archivedAt`)**:名字不是身份。用户从列表看到确认框、到请求到达之间,
|
|
305
|
+
* 这条完全可能被恢复回顶层、又被另一版同名记忆归档进来 —— 按名删掉的就不再是他
|
|
306
|
+
* 确认过的那一份。所以调用方必须交回它当时看见的 `archived_at`,对不上就拒绝。
|
|
307
|
+
* 老数据没有留痕时两边都是空串,仍然一致(那是「同一份、只是没有时间戳」)。
|
|
308
|
+
*
|
|
309
|
+
* ⚠️ 删除本身用 **`rename` 到私有名 + `rm` 私有名**:`inspect` 与 `unlink` 之间隔着
|
|
310
|
+
* 读文件,直接 `unlink(full)` 认的是**名字**而不是我们核对过的那份实物。`rename` 是原子的,
|
|
311
|
+
* 之后删的一定是刚移走的那一个 inode。删失败时把名字 `link()` 还回去 —— 绝不让它
|
|
312
|
+
* 以 `.deleting-` 的私有名留在盘上(扫描不认那个名字,等于文件凭空消失)。
|
|
313
|
+
*
|
|
314
|
+
* @param {string} memoryDir — 记忆根目录(调用方给,不许拿模块常量)
|
|
315
|
+
* @param {string} name — 归档区里的文件名
|
|
316
|
+
* @param {string} expectedArchivedAt — 调用方看见的 `archived_at`(没有留痕时传空串)
|
|
317
|
+
* @returns {Promise<{ok:true,file:string}|{ok:false,code:string,error:string}>}
|
|
318
|
+
*/
|
|
319
|
+
export async function deleteArchivedMemory(memoryDir, name, expectedArchivedAt = '') {
|
|
320
|
+
const gate = checkName(name, 'delete')
|
|
321
|
+
if (!gate.ok) return gate
|
|
322
|
+
const safe = gate.safe
|
|
323
|
+
const dirGate = await inspectDir(archiveDirOf(memoryDir))
|
|
324
|
+
if (!dirGate.ok) {
|
|
325
|
+
return {
|
|
326
|
+
ok: false,
|
|
327
|
+
code: dirGate.code === 'missing' ? 'not-archived' : dirGate.code,
|
|
328
|
+
error: `sage-mem: not archived: ${safe}(${dirGate.error})`,
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const full = join(archiveDirOf(memoryDir), safe)
|
|
332
|
+
const srcGate = await inspectMemoryEntry(full, { limitBytes: null })
|
|
333
|
+
if (!srcGate.ok) {
|
|
334
|
+
return {
|
|
335
|
+
ok: false,
|
|
336
|
+
code: srcGate.code === 'missing' ? 'not-archived' : srcGate.code,
|
|
337
|
+
error: `sage-mem: not archived: ${safe}(${srcGate.error})`,
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// 读全文只为取留痕:代次判据必须来自**盘上的那份**,不能信调用方一路传下来的展示值。
|
|
341
|
+
const onDisk = readArchivedMeta(await readFile(full, 'utf8'))
|
|
342
|
+
const expected = String(expectedArchivedAt ?? '')
|
|
343
|
+
if (expected !== onDisk.archivedAt) {
|
|
344
|
+
return {
|
|
345
|
+
ok: false,
|
|
346
|
+
code: 'changed',
|
|
347
|
+
error:
|
|
348
|
+
`sage-mem: ${safe} 在确认之后被换成了另一版(界面上是「${expected || '时间未知'}」,` +
|
|
349
|
+
`盘上是「${onDisk.archivedAt || '时间未知'}」),本次一个字节都没删 —— 请刷新列表再看一眼。`,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const taken = join(archiveDirOf(memoryDir), `.${safe}.deleting-${randomUUID().replace(/-/g, '')}`)
|
|
353
|
+
const moveErr = await rename(full, taken).then(
|
|
354
|
+
() => null,
|
|
355
|
+
(err) => err,
|
|
356
|
+
)
|
|
357
|
+
if (moveErr) {
|
|
358
|
+
if (moveErr?.code === 'ENOENT') {
|
|
359
|
+
return { ok: false, code: 'not-archived', error: `sage-mem: not archived: ${safe}(删除途中它从归档区消失了)` }
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
code: 'move-failed',
|
|
364
|
+
error: `sage-mem: 删除 ${safe} 时移不动它(${moveErr?.code ?? moveErr}:${moveErr?.message ?? moveErr})。文件仍在 archive/ 里、一个字节没动 —— 可稍后重试(若是被其它程序占用,关掉它再来)。`,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const delErr = await rm(taken, { force: true }).then(
|
|
368
|
+
() => null,
|
|
369
|
+
(err) => err,
|
|
370
|
+
)
|
|
371
|
+
if (delErr) {
|
|
372
|
+
const restored = await link(taken, full).then(
|
|
373
|
+
() => true,
|
|
374
|
+
() => false,
|
|
375
|
+
)
|
|
376
|
+
if (!restored) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
code: 'delete-failed',
|
|
380
|
+
error:
|
|
381
|
+
`sage-mem: 删除 ${safe} 失败(${delErr?.code ?? delErr}),且归还原名时被占用 —— ` +
|
|
382
|
+
`**内容仍保留在 archive/${basename(taken)}**,请手工改回 ${safe} 再试。`,
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
await rm(taken, { force: true }).catch(() => {})
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
code: 'delete-failed',
|
|
389
|
+
error: `sage-mem: 删除 ${safe} 失败(${delErr?.code ?? delErr}:${delErr?.message ?? delErr}),文件已还回 archive/${safe},没有丢。`,
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return { ok: true, file: safe }
|
|
393
|
+
}
|
package/lib/typert.host.js
CHANGED
|
@@ -96,6 +96,9 @@ const archivedListSchema = z.object({
|
|
|
96
96
|
// 归档缘由:用户输入的一句话。真正的 YAML 转义在 host 侧 yamlScalar(值里的「: 」「 #」),
|
|
97
97
|
// 这里只兜长度 —— 缘由不该长到把 frontmatter 变成正文。
|
|
98
98
|
const archiveReasonSchema = z.string().max(1000).optional()
|
|
99
|
+
// 归档代次(`archived_at`,`YYYY-MM-DD HH:mm`):面板把它交回「彻底删除」当**身份判据**
|
|
100
|
+
// —— 用户确认过的是「这一版归档件」,名字不是身份。老归档件没有留痕时传空串,所以不设 min。
|
|
101
|
+
const archiveStampSchema = z.string().max(64)
|
|
99
102
|
// starmap.listStars 的可选开关:不看归档星时行为与从前完全一致。
|
|
100
103
|
const includeArchivedSchema = z.boolean().optional()
|
|
101
104
|
|
|
@@ -585,6 +588,22 @@ export const TYPERT = {
|
|
|
585
588
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#ArchivedWriteResult', schema: archivedWriteResultSchema, create: () => archivedWriteResultSchema },
|
|
586
589
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
587
590
|
},
|
|
591
|
+
{
|
|
592
|
+
// **彻底删除**一条归档件(不可恢复)。第二个参数是身份判据,不是装饰:
|
|
593
|
+
// 名字在「用户确认」与「请求到达」之间可能已经被恢复+重新归档成另一版。
|
|
594
|
+
id: 'sage-mem#memory/deleteArchived',
|
|
595
|
+
service: 'memory',
|
|
596
|
+
namespace: 'memory',
|
|
597
|
+
method: 'deleteArchived',
|
|
598
|
+
invocation: { kind: 'direct' },
|
|
599
|
+
parameters: [
|
|
600
|
+
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#ArchivedFileName', schema: fileNameSchema, create: () => fileNameSchema } },
|
|
601
|
+
{ name: 'archivedAt', wire: 'archivedAt', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#ArchiveStamp', schema: archiveStampSchema, create: () => archiveStampSchema } },
|
|
602
|
+
],
|
|
603
|
+
// 成功 `{ ok: true, file }`、失败 `{ ok: false, error }` —— 与归档 / 恢复同形,复用 ArchiveResult。
|
|
604
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#ArchiveResult', schema: archiveResultSchema, create: () => archiveResultSchema },
|
|
605
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
606
|
+
},
|
|
588
607
|
{
|
|
589
608
|
id: 'sage-mem#memory/audit',
|
|
590
609
|
service: 'memory',
|
package/package.json
CHANGED