sage-mem 1.5.2 → 1.6.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 +4 -0
- package/README.zh.md +3 -0
- package/lib/client.js +134 -83
- package/lib/index.js +12 -0
- package/lib/memory/open-folder.js +57 -0
- package/lib/typert.host.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,10 @@ Most memory schemes take the "SQLite + a long-running worker" route: once memori
|
|
|
80
80
|
|
|
81
81
|
The UI also carries four more capabilities (all of them optional): the memory management page, type and tag filtering, the memory star map, and audit & archiving.
|
|
82
82
|
|
|
83
|
+
The memory management page's file list also has an **Open memory folder** button: it opens the
|
|
84
|
+
memory directory in the system file manager (Explorer / Finder / the Linux file manager) through a
|
|
85
|
+
detached host-side process and returns without waiting for the window.
|
|
86
|
+
|
|
83
87
|
## Quick Start
|
|
84
88
|
|
|
85
89
|
Requires **DSH `0.1.2-rc.1` or higher** (see [Compatibility](#compatibility)) and **Node.js `>= 18`**.
|
package/README.zh.md
CHANGED
|
@@ -82,6 +82,9 @@ sage-mem 补上这一层,做法是:**把该记住的事写成一张张卡片
|
|
|
82
82
|
|
|
83
83
|
界面上还有四块能力(都可以不用):记忆管理页、类型与标签筛选、记忆星图、体检与归档。
|
|
84
84
|
|
|
85
|
+
记忆管理页的「文件列表」头部还有一个「打开记忆文件夹」按钮:用系统文件管理器打开当前记忆目录
|
|
86
|
+
(Windows Explorer / macOS Finder / Linux 文件管理器)。host 侧起一个 detached 进程就返回,不等窗口。
|
|
87
|
+
|
|
85
88
|
## 快速开始
|
|
86
89
|
|
|
87
90
|
需要 **DSH `0.1.2-rc.1` 或更高**(见[兼容性](#兼容性))、**Node.js `>= 18`**。
|
package/lib/client.js
CHANGED
|
@@ -1727,6 +1727,16 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
1727
1727
|
result: { mode: "strict", typeSymbol: "sage-mem/types#AuditReport", schema: objCodec, create: () => objCodec },
|
|
1728
1728
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1729
1729
|
},
|
|
1730
|
+
{
|
|
1731
|
+
id: "sage-mem#memory/openMemoryDir",
|
|
1732
|
+
service: "memory",
|
|
1733
|
+
namespace: "memory",
|
|
1734
|
+
method: "openMemoryDir",
|
|
1735
|
+
invocation: { kind: "direct" },
|
|
1736
|
+
parameters: [],
|
|
1737
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#OpenFolderResult", schema: objCodec, create: () => objCodec },
|
|
1738
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1739
|
+
},
|
|
1730
1740
|
{
|
|
1731
1741
|
id: "sage-mem#memory/checkUpdate",
|
|
1732
1742
|
service: "memory",
|
|
@@ -2174,7 +2184,7 @@ const StarmapRuntime = (function (require, _react) {
|
|
|
2174
2184
|
".smem-empty{display:flex;flex-direction:column;align-items:flex-start;gap:8px;}",
|
|
2175
2185
|
".smem-tabs-sep{width:1px;align-self:stretch;margin:6px 2px;background:var(--dsw-alias-border-l2);}",
|
|
2176
2186
|
// 窄窗口(≤420px):头部带与筛选条各让一行,卡片动作允许换行、分隔符隐藏。
|
|
2177
|
-
"@media (max-width:420px){.smem-root{gap:10px;}.smem-head{gap:6px;}.smem-card-title{min-width:0;word-break:break-word;}.smem-actions{flex-wrap:wrap;}.smem-actions-sep{display:none;}.smem-num{width:56px;}.smem-search{min-width:100px;}}",
|
|
2187
|
+
"@media (max-width:420px){.smem-root{gap:10px;}.smem-head{gap:6px;}.smem-head-actions{flex-wrap:wrap;}.smem-card-title{min-width:0;word-break:break-word;}.smem-actions{flex-wrap:wrap;}.smem-actions-sep{display:none;}.smem-num{width:56px;}.smem-search{min-width:100px;}}",
|
|
2178
2188
|
// ── 底部一行仓库链接(引流,不抢视线)──
|
|
2179
2189
|
".smem-star{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;font-size:12px;color:var(--dsw-alias-label-tertiary,#8b93a1);padding:2px 2px 0;}",
|
|
2180
2190
|
".smem-star-link{color:var(--dsw-alias-brand-primary,#4d76e6);text-decoration:underline;}",
|
|
@@ -3741,6 +3751,97 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3741
3751
|
notice ? h("div", { className: "smem-ok", key: "ok" }, "✓ " + notice) : null,
|
|
3742
3752
|
]);
|
|
3743
3753
|
}
|
|
3754
|
+
// ── 文件列表这一屏(1.5.3 从 Section 里抽出;D23 计划的 72 片)──────────
|
|
3755
|
+
// 只负责「按状态/筛选把卡片摆出来 + 四种空态」;状态、筛选语义、动作回调全部由 Section 传。
|
|
3756
|
+
// 抽出的判据:它是 Section 里最大的一块纯渲染,而卡片组件(Card / ArchivedCard)本来就在 50 共享层。
|
|
3757
|
+
function FilesView(props) {
|
|
3758
|
+
const { loading, totalCount, openAdd, statusFilter, setStatusFilter, items, archiveItems, filtered, setTypeFilter, setTagFilter, setQuery, busy, restoring, archDelete, archEdit, archLoad, archSaving, archText, setArchText, openArchEdit, saveArchEdit, cancelArchEdit, restoreFile, setArchDelete, setArchEdit, deleteArchivedFile, expanded, deleting, archiving, archiveReason, setArchiveReason, setArchiving, setDeleting, confirmArchive, toggleLock, openEdit, confirmDelete } = props;
|
|
3759
|
+
let filesView;
|
|
3760
|
+
if (loading && totalCount === 0) {
|
|
3761
|
+
filesView = h("div", { className: "smem-skeleton", key: "loading", "data-loading": "1" }, [0, 1, 2].map(function (i) {
|
|
3762
|
+
return h("div", { className: "smem-skeleton-row", key: i });
|
|
3763
|
+
}));
|
|
3764
|
+
} else if (totalCount === 0) {
|
|
3765
|
+
filesView = h("div", { className: "smem-empty", key: "empty" }, [
|
|
3766
|
+
h("div", { key: "t" }, "还没有记忆文件。对话中值得记住的事会写到这里,也可以手动写入。"),
|
|
3767
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
|
|
3768
|
+
]);
|
|
3769
|
+
} else if (statusFilter === "archived" && archiveItems.length === 0) {
|
|
3770
|
+
filesView = h("div", { className: "smem-empty", key: "empty" }, [
|
|
3771
|
+
h("div", { key: "t" }, "档案馆是空的 —— 在活动记忆那张卡上点「归档」,它就会收到这里。"),
|
|
3772
|
+
h("button", { type: "button", className: "smem-btn", key: "back", onClick: function () { setStatusFilter("active"); } }, "回文件列表"),
|
|
3773
|
+
]);
|
|
3774
|
+
} else if (statusFilter === "active" && items.length === 0) {
|
|
3775
|
+
filesView = h("div", { className: "smem-empty", key: "empty" }, [
|
|
3776
|
+
h("div", { key: "t" }, "活动记忆是空的 —— 状态筛到「已归档」还能捞出 " + archiveItems.length + " 条。"),
|
|
3777
|
+
h("button", { type: "button", className: "smem-btn", key: "go", onClick: function () { setStatusFilter("archived"); } }, "切到已归档"),
|
|
3778
|
+
]);
|
|
3779
|
+
} else if (filtered.length === 0) {
|
|
3780
|
+
filesView = h("div", { className: "smem-empty", key: "empty" }, [
|
|
3781
|
+
h("div", { key: "t" }, "没有符合搜索或筛选条件的文件。"),
|
|
3782
|
+
h("button", { type: "button", className: "smem-btn", key: "clear", onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); } }, "清除筛选"),
|
|
3783
|
+
]);
|
|
3784
|
+
} else {
|
|
3785
|
+
const list = h("div", { className: "smem-list", key: "list" }, filtered.map(function (it) {
|
|
3786
|
+
if (it.archived === true) {
|
|
3787
|
+
return h(ArchivedCard, {
|
|
3788
|
+
key: it.file,
|
|
3789
|
+
item: it,
|
|
3790
|
+
busy: busy,
|
|
3791
|
+
restoring: restoring === it.file,
|
|
3792
|
+
deleting: archDelete === it.file,
|
|
3793
|
+
editing: archEdit === it.file,
|
|
3794
|
+
loading: archLoad && archEdit === it.file,
|
|
3795
|
+
saving: archSaving && archEdit === it.file,
|
|
3796
|
+
text: archText,
|
|
3797
|
+
onText: setArchText,
|
|
3798
|
+
onEdit: openArchEdit,
|
|
3799
|
+
onSave: saveArchEdit,
|
|
3800
|
+
onCancel: cancelArchEdit,
|
|
3801
|
+
onRestore: restoreFile,
|
|
3802
|
+
// 两个「危险动作的确认」互斥:问到一半又去改文本,界面就会同时摆两个确认条。
|
|
3803
|
+
onAskDelete: function (file) { setArchDelete(file); setArchEdit(null); },
|
|
3804
|
+
onCancelDelete: function () { setArchDelete(null); },
|
|
3805
|
+
onConfirmDelete: deleteArchivedFile,
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
return h(Card, {
|
|
3809
|
+
key: it.file,
|
|
3810
|
+
item: it,
|
|
3811
|
+
expanded: !!expanded[it.file],
|
|
3812
|
+
deleting: deleting === it.file,
|
|
3813
|
+
archiving: archiving === it.file,
|
|
3814
|
+
archiveReason: archiveReason,
|
|
3815
|
+
busy: busy,
|
|
3816
|
+
onArchiveReason: setArchiveReason,
|
|
3817
|
+
onAskArchive: function (file) { setArchiving(file); setArchiveReason(""); setDeleting(null); },
|
|
3818
|
+
onCancelArchive: function () { setArchiving(null); setArchiveReason(""); },
|
|
3819
|
+
onConfirmArchive: confirmArchive,
|
|
3820
|
+
onToggleLock: toggleLock,
|
|
3821
|
+
onToggle: function (file) {
|
|
3822
|
+
const nx = {};
|
|
3823
|
+
nx[file] = !expanded[file];
|
|
3824
|
+
setExpanded(Object.assign({}, expanded, nx));
|
|
3825
|
+
},
|
|
3826
|
+
onEdit: openEdit,
|
|
3827
|
+
onAskDelete: function (file) { setDeleting(file); setArchiving(null); },
|
|
3828
|
+
onCancelDelete: function () { setDeleting(null); },
|
|
3829
|
+
onConfirmDelete: confirmDelete,
|
|
3830
|
+
});
|
|
3831
|
+
}));
|
|
3832
|
+
filesView = statusFilter === "archived"
|
|
3833
|
+
? [
|
|
3834
|
+
h("div", { className: "smem-ar-banner", key: "banner" },
|
|
3835
|
+
"档案馆里的记忆不参与检索与星图,原文仍在记忆目录的 archive/ 下 —— 恢复后它就回到活动列表;" +
|
|
3836
|
+
"「编辑」是原样文本编辑,改完存回同一份文件;「彻底删除」会真的从 archive/ 里删掉它(不可恢复,删除前还要再确认一次)。"),
|
|
3837
|
+
list,
|
|
3838
|
+
]
|
|
3839
|
+
: list;
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
return filesView;
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3744
3845
|
// ── 筛选条(1.5.2 从 Section 里抽出)────────────────────────────────
|
|
3745
3846
|
// 只接收 props、只渲染;筛选语义(哪些项互斥、怎么 AND/OR)仍在 Section 里算好再传进来。
|
|
3746
3847
|
// 抽出来的收益是 Section 少了 60 行 UI 细节,而筛选条可以单独看、单独改(比如以后再加
|
|
@@ -4025,6 +4126,20 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4025
4126
|
setFormFm(null); setFormBaseType(""); setFormTags("");
|
|
4026
4127
|
}
|
|
4027
4128
|
|
|
4129
|
+
function openMemoryFolder() {
|
|
4130
|
+
setError(null);
|
|
4131
|
+
if (!remote || typeof remote.openMemoryDir !== "function") {
|
|
4132
|
+
setError("宿主侧还没有 openMemoryDir 接口(插件版本较旧)");
|
|
4133
|
+
return;
|
|
4134
|
+
}
|
|
4135
|
+
remote.openMemoryDir()
|
|
4136
|
+
.then(function (res) {
|
|
4137
|
+
const result = unwrap(res);
|
|
4138
|
+
if (result && result.ok === false) setError(result.error || "打开记忆目录失败");
|
|
4139
|
+
})
|
|
4140
|
+
.catch(function (e) { setError(String((e && e.message) || e)); });
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4028
4143
|
function openEdit(file) {
|
|
4029
4144
|
setError(null);
|
|
4030
4145
|
setBusy(true);
|
|
@@ -4481,88 +4596,23 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4481
4596
|
|
|
4482
4597
|
// 「文件列表」这一屏:状态筛选决定看哪一批,灰色卡片与留痕块自带「退役」语义。
|
|
4483
4598
|
// banner 只在**专门看归档**时出现,切到全部时不重复说一遍。
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
}
|
|
4500
|
-
|
|
4501
|
-
h("div", { key: "t" }, "活动记忆是空的 —— 状态筛到「已归档」还能捞出 " + archiveItems.length + " 条。"),
|
|
4502
|
-
h("button", { type: "button", className: "smem-btn", key: "go", onClick: function () { setStatusFilter("archived"); } }, "切到已归档"),
|
|
4503
|
-
]);
|
|
4504
|
-
} else if (filtered.length === 0) {
|
|
4505
|
-
filesView = h("div", { className: "smem-empty", key: "empty" }, [
|
|
4506
|
-
h("div", { key: "t" }, "没有符合搜索或筛选条件的文件。"),
|
|
4507
|
-
h("button", { type: "button", className: "smem-btn", key: "clear", onClick: function () { setStatusFilter("all"); setTypeFilter([]); setTagFilter([]); setQuery(""); } }, "清除筛选"),
|
|
4508
|
-
]);
|
|
4509
|
-
} else {
|
|
4510
|
-
const list = h("div", { className: "smem-list", key: "list" }, filtered.map(function (it) {
|
|
4511
|
-
if (it.archived === true) {
|
|
4512
|
-
return h(ArchivedCard, {
|
|
4513
|
-
key: it.file,
|
|
4514
|
-
item: it,
|
|
4515
|
-
busy: busy,
|
|
4516
|
-
restoring: restoring === it.file,
|
|
4517
|
-
deleting: archDelete === it.file,
|
|
4518
|
-
editing: archEdit === it.file,
|
|
4519
|
-
loading: archLoad && archEdit === it.file,
|
|
4520
|
-
saving: archSaving && archEdit === it.file,
|
|
4521
|
-
text: archText,
|
|
4522
|
-
onText: setArchText,
|
|
4523
|
-
onEdit: openArchEdit,
|
|
4524
|
-
onSave: saveArchEdit,
|
|
4525
|
-
onCancel: cancelArchEdit,
|
|
4526
|
-
onRestore: restoreFile,
|
|
4527
|
-
// 两个「危险动作的确认」互斥:问到一半又去改文本,界面就会同时摆两个确认条。
|
|
4528
|
-
onAskDelete: function (file) { setArchDelete(file); setArchEdit(null); },
|
|
4529
|
-
onCancelDelete: function () { setArchDelete(null); },
|
|
4530
|
-
onConfirmDelete: deleteArchivedFile,
|
|
4531
|
-
});
|
|
4532
|
-
}
|
|
4533
|
-
return h(Card, {
|
|
4534
|
-
key: it.file,
|
|
4535
|
-
item: it,
|
|
4536
|
-
expanded: !!expanded[it.file],
|
|
4537
|
-
deleting: deleting === it.file,
|
|
4538
|
-
archiving: archiving === it.file,
|
|
4539
|
-
archiveReason: archiveReason,
|
|
4540
|
-
busy: busy,
|
|
4541
|
-
onArchiveReason: setArchiveReason,
|
|
4542
|
-
onAskArchive: function (file) { setArchiving(file); setArchiveReason(""); setDeleting(null); },
|
|
4543
|
-
onCancelArchive: function () { setArchiving(null); setArchiveReason(""); },
|
|
4544
|
-
onConfirmArchive: confirmArchive,
|
|
4545
|
-
onToggleLock: toggleLock,
|
|
4546
|
-
onToggle: function (file) {
|
|
4547
|
-
const nx = {};
|
|
4548
|
-
nx[file] = !expanded[file];
|
|
4549
|
-
setExpanded(Object.assign({}, expanded, nx));
|
|
4550
|
-
},
|
|
4551
|
-
onEdit: openEdit,
|
|
4552
|
-
onAskDelete: function (file) { setDeleting(file); setArchiving(null); },
|
|
4553
|
-
onCancelDelete: function () { setDeleting(null); },
|
|
4554
|
-
onConfirmDelete: confirmDelete,
|
|
4555
|
-
});
|
|
4556
|
-
}));
|
|
4557
|
-
filesView = statusFilter === "archived"
|
|
4558
|
-
? [
|
|
4559
|
-
h("div", { className: "smem-ar-banner", key: "banner" },
|
|
4560
|
-
"档案馆里的记忆不参与检索与星图,原文仍在记忆目录的 archive/ 下 —— 恢复后它就回到活动列表;" +
|
|
4561
|
-
"「编辑」是原样文本编辑,改完存回同一份文件;「彻底删除」会真的从 archive/ 里删掉它(不可恢复,删除前还要再确认一次)。"),
|
|
4562
|
-
list,
|
|
4563
|
-
]
|
|
4564
|
-
: list;
|
|
4565
|
-
}
|
|
4599
|
+
const filesView = h(FilesView, {
|
|
4600
|
+
loading: loading, totalCount: totalCount, openAdd: openAdd,
|
|
4601
|
+
statusFilter: statusFilter, setStatusFilter: setStatusFilter,
|
|
4602
|
+
items: items, archiveItems: archiveItems, filtered: filtered,
|
|
4603
|
+
setTypeFilter: setTypeFilter, setTagFilter: setTagFilter, setQuery: setQuery,
|
|
4604
|
+
busy: busy, restoring: restoring,
|
|
4605
|
+
archDelete: archDelete, archEdit: archEdit, archLoad: archLoad, archSaving: archSaving,
|
|
4606
|
+
archText: archText, setArchText: setArchText,
|
|
4607
|
+
openArchEdit: openArchEdit, saveArchEdit: saveArchEdit, cancelArchEdit: cancelArchEdit,
|
|
4608
|
+
restoreFile: restoreFile, setArchDelete: setArchDelete, setArchEdit: setArchEdit, deleteArchivedFile: deleteArchivedFile,
|
|
4609
|
+
expanded: expanded, deleting: deleting, archiving: archiving,
|
|
4610
|
+
archiveReason: archiveReason, setArchiveReason: setArchiveReason,
|
|
4611
|
+
setArchiving: setArchiving, setDeleting: setDeleting,
|
|
4612
|
+
confirmArchive: confirmArchive, toggleLock: toggleLock,
|
|
4613
|
+
openEdit: openEdit, confirmDelete: confirmDelete,
|
|
4614
|
+
});
|
|
4615
|
+
|
|
4566
4616
|
|
|
4567
4617
|
return h("div", { className: "smem-root" }, [
|
|
4568
4618
|
h("div", { className: "smem-tabs", key: "tabs", role: "tablist" }, [
|
|
@@ -4606,6 +4656,7 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4606
4656
|
// 也推到最右,看起来像"跑出格子了")。
|
|
4607
4657
|
h("span", { className: "smem-head-spacer", key: "sp" }),
|
|
4608
4658
|
h("span", { className: "smem-head-actions", key: "act" }, tab === "files" ? [
|
|
4659
|
+
h("button", { type: "button", className: "smem-btn", key: "open", disabled: busy || !dirOk, title: "在系统文件管理器里打开记忆目录", onClick: openMemoryFolder }, "打开记忆文件夹"),
|
|
4609
4660
|
h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, "刷新"),
|
|
4610
4661
|
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
|
|
4611
4662
|
] : null),
|
package/lib/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import { archiveMemory, deleteArchivedMemory, restoreMemory } from './memory/arc
|
|
|
27
27
|
import { flushAccess, pickArchiveCandidates, readAccessLedger } from './memory/access.js'
|
|
28
28
|
import { warnLog, writeFileAtomic } from './memory/util.js'
|
|
29
29
|
import { readLockSet, setLocked as setLockedImpl } from './memory/lock.js'
|
|
30
|
+
import { openMemoryFolder } from './memory/open-folder.js'
|
|
30
31
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
31
32
|
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'
|
|
32
33
|
import { join } from 'node:path'
|
|
@@ -360,6 +361,16 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
360
361
|
return auditMemoryDir(MEMORY_DIR)
|
|
361
362
|
}
|
|
362
363
|
|
|
364
|
+
/**
|
|
365
|
+
* 用系统文件管理器打开记忆目录(设置页「打开记忆文件夹」)。
|
|
366
|
+
*
|
|
367
|
+
* host 侧起一个 detached 进程就返回,不等待窗口出现;路径是插件自己的记忆目录,
|
|
368
|
+
* 不接受界面传入,避免把「打开任意路径」变成一个能力。
|
|
369
|
+
*/
|
|
370
|
+
async openMemoryDir() {
|
|
371
|
+
return openMemoryFolder(MEMORY_DIR)
|
|
372
|
+
}
|
|
373
|
+
|
|
363
374
|
/**
|
|
364
375
|
* 自动归档候选 + 当前策略与阈值(面板「执行归档」前先看清单)。
|
|
365
376
|
*
|
|
@@ -558,6 +569,7 @@ markRemote(MemoryGateway, 'readArchived', 'readArchived')
|
|
|
558
569
|
markRemote(MemoryGateway, 'writeArchived', 'writeArchived')
|
|
559
570
|
markRemote(MemoryGateway, 'deleteArchived', 'deleteArchived')
|
|
560
571
|
markRemote(MemoryGateway, 'audit', 'audit')
|
|
572
|
+
markRemote(MemoryGateway, 'openMemoryDir', 'openMemoryDir')
|
|
561
573
|
markRemote(MemoryGateway, 'checkUpdate', 'checkUpdate')
|
|
562
574
|
markRemote(MemoryGateway, 'setLocked', 'setLocked')
|
|
563
575
|
markRemote(MemoryGateway, 'archiveCandidates', 'archiveCandidates')
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 打开记忆目录(设置页「打开记忆文件夹」按钮的 host 侧实现)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么放在 host 侧:浏览器不能起进程;而 DSH 的 `open-in-app` 路由属于界面内部接口,
|
|
5
|
+
* 老版本 DSH 不一定有。这里只依赖 `node:child_process`,三条命令覆盖三个平台:
|
|
6
|
+
* - Windows → `explorer.exe <dir>`
|
|
7
|
+
* - macOS → `open <dir>`
|
|
8
|
+
* - 其它 → `xdg-open <dir>`
|
|
9
|
+
*
|
|
10
|
+
* 进程是**一次性、detached** 的:不等待退出(Explorer / Finder 是常驻进程,等它等于
|
|
11
|
+
* 永不返回),也不持有 stdio。这条路径不改变插件「无 worker / 无端口 / 无常驻进程」的性质。
|
|
12
|
+
*
|
|
13
|
+
* 返回 `{ ok, error? }`,不抛:设置页按 ok 显示错误。`spawn` 的 'error' 事件是异步的,
|
|
14
|
+
* 调用方拿不到,所以这里只挂一个空处理器,避免「命令不存在」变成未捕获异常 ——
|
|
15
|
+
* 返回值表达的是「已发起」,不是「窗口已经出现」。
|
|
16
|
+
*/
|
|
17
|
+
import { spawn } from 'node:child_process'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 平台 → 打开目录的命令与参数(纯函数,测试直接钉它)。
|
|
21
|
+
* @param {string} platform — `process.platform` 的取值
|
|
22
|
+
* @param {string} dir — 绝对路径
|
|
23
|
+
* @returns {{command: string, args: string[]}}
|
|
24
|
+
*/
|
|
25
|
+
export function folderOpenCommand(platform, dir) {
|
|
26
|
+
if (typeof dir !== 'string' || dir.length === 0) throw new Error('sage-mem: memory directory is unknown')
|
|
27
|
+
if (platform === 'win32') return { command: 'explorer.exe', args: [dir] }
|
|
28
|
+
if (platform === 'darwin') return { command: 'open', args: [dir] }
|
|
29
|
+
return { command: 'xdg-open', args: [dir] }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 发起「打开目录」,立即返回。
|
|
34
|
+
* @param {string} dir — 记忆目录绝对路径
|
|
35
|
+
* @param {{platform?: string, spawn?: Function}} [options] — 测试注入点
|
|
36
|
+
* @returns {{ok: boolean, error?: string}}
|
|
37
|
+
*/
|
|
38
|
+
export function openMemoryFolder(dir, options = {}) {
|
|
39
|
+
const platform = options.platform || process.platform
|
|
40
|
+
const spawnImpl = options.spawn || spawn
|
|
41
|
+
|
|
42
|
+
let plan
|
|
43
|
+
try {
|
|
44
|
+
plan = folderOpenCommand(platform, dir)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return { ok: false, error: String(error?.message ?? error) }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const child = spawnImpl(plan.command, plan.args, { detached: true, stdio: 'ignore', windowsHide: true })
|
|
51
|
+
child.on?.('error', () => {})
|
|
52
|
+
child.unref?.()
|
|
53
|
+
return { ok: true }
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return { ok: false, error: `sage-mem: cannot open ${dir}: ${error?.message ?? error}` }
|
|
56
|
+
}
|
|
57
|
+
}
|
package/lib/typert.host.js
CHANGED
|
@@ -181,6 +181,12 @@ const auditReportSchema = z.object({
|
|
|
181
181
|
'problems': z.number().readonly(),
|
|
182
182
|
}).readonly()
|
|
183
183
|
|
|
184
|
+
/** 「打开记忆目录」的结果:成功只有 ok,失败带原因。 */
|
|
185
|
+
const openFolderResultSchema = z.object({
|
|
186
|
+
'ok': z.boolean().readonly(),
|
|
187
|
+
'error': z.string().readonly().optional(),
|
|
188
|
+
}).readonly()
|
|
189
|
+
|
|
184
190
|
const archiveCandidateSchema = z.object({
|
|
185
191
|
'file': z.string().readonly(),
|
|
186
192
|
'type': z.string().readonly(),
|
|
@@ -616,6 +622,16 @@ export const TYPERT = {
|
|
|
616
622
|
result: { mode: 'strict', typeSymbol: 'sage-mem/types#AuditReport', schema: auditReportSchema, create: () => auditReportSchema },
|
|
617
623
|
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
618
624
|
},
|
|
625
|
+
{
|
|
626
|
+
id: 'sage-mem#memory/openMemoryDir',
|
|
627
|
+
service: 'memory',
|
|
628
|
+
namespace: 'memory',
|
|
629
|
+
method: 'openMemoryDir',
|
|
630
|
+
invocation: { kind: 'direct' },
|
|
631
|
+
parameters: [],
|
|
632
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#OpenFolderResult', schema: openFolderResultSchema, create: () => openFolderResultSchema },
|
|
633
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
634
|
+
},
|
|
619
635
|
{
|
|
620
636
|
// 面板进入时静默查一次;只有「有新版本」才在界面上提醒。
|
|
621
637
|
id: 'sage-mem#memory/checkUpdate',
|
package/package.json
CHANGED