@michengai/dsh-archive-manager 0.1.24 → 0.1.26
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/CHANGELOG.md +12 -0
- package/CHANGELOG.zh-CN.md +12 -0
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/lib/client.js +161 -15
- package/lib/projcache.js +58 -18
- package/lib/workspace.js +64 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
Published release notes are retained below; new versions are added without removing earlier entries.
|
|
6
6
|
|
|
7
|
+
## 0.1.26 — 2026-09-03
|
|
8
|
+
|
|
9
|
+
- Fixed the Windows CI host-test fixture to use the same native realpath representation as DSH's asynchronous session-path index, preventing valid workspace sessions from being filtered out only in the test environment.
|
|
10
|
+
- Added an initialization regression assertion that verifies accounted workspace sessions remain visible after path membership validation.
|
|
11
|
+
|
|
12
|
+
## 0.1.25 — 2026-09-03
|
|
13
|
+
|
|
14
|
+
- Added **Archive all chats** to each workspace action menu. The entry is hidden when no active chats remain, and the client refreshes its session list after a successful batch archive.
|
|
15
|
+
- Added a red outlined confirmation style for **Archive all** with hover and keyboard-focus states.
|
|
16
|
+
- Kept the path-safe `session_projcache_archive_manager_v2` domain as the only write target. Startup now imports missing entries from the short-lived old-name v2 domain, old-name v1 domain, and supported DSH cache formats without overwriting newer records.
|
|
17
|
+
- Added RC2-to-current isolated compatibility coverage and made it a release gate: CI and `prepublishOnly` now run `test:compat`; CI no longer assumes pnpm exists before Corepack enables it.
|
|
18
|
+
|
|
7
19
|
## 0.1.24 — 2026-09-02
|
|
8
20
|
|
|
9
21
|
- Upgraded the development test stack to DSH `0.1.2-alpha.5` and `@deepseek-ai/cordis` `4.0.2`, while retaining the existing `>=0.1.0-rc.5 <0.2.0` DSH runtime peer declarations.
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
以下发布说明会持续保留;新增版本时不再删除较早记录。
|
|
6
6
|
|
|
7
|
+
## 0.1.26 — 2026-09-03
|
|
8
|
+
|
|
9
|
+
- 修复 Windows CI 宿主测试夹具:改用与 DSH 异步会话路径索引一致的原生 `realpath` 表示,避免有效工作区会话仅在测试环境被路径成员校验过滤。
|
|
10
|
+
- 新增初始化回归断言,验证已记账的工作区会话在路径成员校验后仍保持可见。
|
|
11
|
+
|
|
12
|
+
## 0.1.25 — 2026-09-03
|
|
13
|
+
|
|
14
|
+
- 每个工作区操作菜单新增「归档全部聊天」;没有可归档聊天时不显示入口,批量归档成功后立即刷新客户端会话列表。
|
|
15
|
+
- 「全部归档」确认按钮使用红色描边,并提供悬停和键盘焦点状态。
|
|
16
|
+
- 路径安全的 `session_projcache_archive_manager_v2` 域继续作为唯一写入目标;启动时从短暂使用过的旧名 v2 域、旧名 v1 域及受支持的 DSH 缓存格式只读补缺导入,不覆盖更新的记录。
|
|
17
|
+
- 新增从 RC2 到当前版本的隔离兼容回归,并将其纳入发布门禁:CI 与 `prepublishOnly` 均执行 `test:compat`,CI 不再假定 Corepack 启用前系统已存在 pnpm。
|
|
18
|
+
|
|
7
19
|
## 0.1.24 — 2026-09-02
|
|
8
20
|
|
|
9
21
|
- 将开发测试依赖升级至 DSH `0.1.2-alpha.5` 与 `@deepseek-ai/cordis` `4.0.2`,同时保留原有的 DSH 运行时 peer 声明 `>=0.1.0-rc.5 <0.2.0`。
|
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
24
24
|
- Archive a session from the sidebar session menu.
|
|
25
|
+
- Archive every active chat in a workspace from its sidebar action menu.
|
|
25
26
|
- Search archived chats, sort them by update time, creation time, or title, and filter them by workspace in **Settings → Archived**.
|
|
26
27
|
- Restore a session to its original workspace with **Unarchive**.
|
|
27
28
|
- Restore or permanently delete every archived chat in a project group.
|
package/README.zh-CN.md
CHANGED
package/lib/client.js
CHANGED
|
@@ -30,6 +30,12 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
return value;
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
|
+
const workspaceIdSchema = {
|
|
34
|
+
parse(value) {
|
|
35
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError(`workspaceId must be a non-empty string, got ${String(value)}`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
33
39
|
const archivedSetSchema = {
|
|
34
40
|
parse(value) {
|
|
35
41
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
|
|
@@ -61,6 +67,14 @@ window.__ModuleLoader__.load({
|
|
|
61
67
|
return value;
|
|
62
68
|
}
|
|
63
69
|
};
|
|
70
|
+
const archivedWorkspaceBatchSchema = {
|
|
71
|
+
parse(value) {
|
|
72
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
|
|
73
|
+
if (!Array.isArray(value.archivedSessionIds) || value.archivedSessionIds.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIds must be a string array");
|
|
74
|
+
if (!Array.isArray(value.archivedSessionIdsAdded) || value.archivedSessionIdsAdded.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIdsAdded must be a string array");
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
64
78
|
const deletedBatchSchema = {
|
|
65
79
|
parse(value) {
|
|
66
80
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
|
|
@@ -143,6 +157,25 @@ window.__ModuleLoader__.load({
|
|
|
143
157
|
},
|
|
144
158
|
sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
|
|
145
159
|
},
|
|
160
|
+
{
|
|
161
|
+
id: "@michengai/dsh-archive-manager#workspaceRegistry/archiveWorkspaceSessions",
|
|
162
|
+
service: "workspaceRegistry",
|
|
163
|
+
namespace: "workspaceRegistry",
|
|
164
|
+
method: "archiveWorkspaceSessions",
|
|
165
|
+
invocation: { kind: "direct" },
|
|
166
|
+
parameters: [{
|
|
167
|
+
name: "workspaceId",
|
|
168
|
+
wire: "workspaceId",
|
|
169
|
+
source: "json",
|
|
170
|
+
codec: { mode: "strict", typeSymbol: "@deepseek-ai/dsh-workspace/types#WorkspaceId", schema: workspaceIdSchema }
|
|
171
|
+
}],
|
|
172
|
+
result: {
|
|
173
|
+
mode: "strict",
|
|
174
|
+
typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedWorkspaceBatch",
|
|
175
|
+
schema: archivedWorkspaceBatchSchema
|
|
176
|
+
},
|
|
177
|
+
sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
|
|
178
|
+
},
|
|
146
179
|
{
|
|
147
180
|
id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteArchivedSessions",
|
|
148
181
|
service: "workspaceRegistry",
|
|
@@ -607,6 +640,22 @@ window.__ModuleLoader__.load({
|
|
|
607
640
|
if (stray.length > 0) groups.push(buildGroup("", void 0, void 0, void 0, UNGROUPED_LABEL, ungroupedOrder === void 0 ? stray : orderedUngrouped(stray, ungroupedOrder), ungroupedOrder === void 0 ? "recency" : "account"));
|
|
608
641
|
return groups;
|
|
609
642
|
}
|
|
643
|
+
/** 返回工作区中尚未归档的唯一会话数量。 */
|
|
644
|
+
function archiveableWorkspaceSessionCount(workspace, archivedSessionIds) {
|
|
645
|
+
const archived = archivedSessionIds instanceof Set ? archivedSessionIds : new Set(archivedSessionIds);
|
|
646
|
+
return [...new Set(workspace.sessionIds)].filter((sessionId) => !archived.has(sessionId)).length;
|
|
647
|
+
}
|
|
648
|
+
/** 仅在工作区仍有活跃会话时创建批量归档确认目标。 */
|
|
649
|
+
function archiveWorkspaceDialogTarget(workspaces, workspaceId, title, archivedSessionIds) {
|
|
650
|
+
const workspace = workspaces.find((item) => item.workspaceId === workspaceId);
|
|
651
|
+
if (workspace === void 0) return null;
|
|
652
|
+
const count = archiveableWorkspaceSessionCount(workspace, archivedSessionIds);
|
|
653
|
+
return count === 0 ? null : { workspaceId, title, count };
|
|
654
|
+
}
|
|
655
|
+
/** 请求失败时保留确认目标,方便用户查看错误后重试。 */
|
|
656
|
+
function archiveWorkspaceDialogFailureState(target, reason, t) {
|
|
657
|
+
return { target, archiving: false, error: formatArchiveError(reason, t) };
|
|
658
|
+
}
|
|
610
659
|
/** 只把侧栏已支持的待处理交互类型交给行渲染,避免未知类型触发断言。 */
|
|
611
660
|
function visiblePendingKind(kind) {
|
|
612
661
|
switch (kind) {
|
|
@@ -942,7 +991,11 @@ window.__ModuleLoader__.load({
|
|
|
942
991
|
id: "rename",
|
|
943
992
|
label: t("rename"),
|
|
944
993
|
icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {})
|
|
945
|
-
}, {
|
|
994
|
+
}, ...(actions?.canArchive === true ? [{
|
|
995
|
+
id: "archive-workspace",
|
|
996
|
+
label: t("menu.archiveWorkspace"),
|
|
997
|
+
icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutline20, { size: 16 })
|
|
998
|
+
}] : []), {
|
|
946
999
|
id: "delete",
|
|
947
1000
|
label: t("delete.workspace"),
|
|
948
1001
|
icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}),
|
|
@@ -986,9 +1039,10 @@ window.__ModuleLoader__.load({
|
|
|
986
1039
|
items: workspaceMenuItems,
|
|
987
1040
|
onSelect: (id) => {
|
|
988
1041
|
setMenuOpen(false);
|
|
989
|
-
/* v8 ignore next -- workspaceMenuItems carries exactly these
|
|
990
|
-
if (id !== "rename" && id !== "delete") return;
|
|
1042
|
+
/* v8 ignore next -- workspaceMenuItems carries exactly these three rows today. */
|
|
1043
|
+
if (id !== "rename" && id !== "archive-workspace" && id !== "delete") return;
|
|
991
1044
|
if (id === "rename") actions.rename();
|
|
1045
|
+
else if (id === "archive-workspace") actions.archive();
|
|
992
1046
|
else actions.delete();
|
|
993
1047
|
},
|
|
994
1048
|
portal: true,
|
|
@@ -1741,7 +1795,7 @@ window.__ModuleLoader__.load({
|
|
|
1741
1795
|
return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
|
|
1742
1796
|
}
|
|
1743
1797
|
/** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */
|
|
1744
|
-
function SessionTree({ useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds, showArchived, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, onSessionUnarchive, onSessionDelete, insertWorkspaceBefore, insertSessionBefore, orderBy, groupExpansion, setGroupExpanded, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, t }) {
|
|
1798
|
+
function SessionTree({ useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds, showArchived, onRenameRequest, onArchiveRequest, onDeleteRequest, onSessionRename, onSessionArchive, onSessionUnarchive, onSessionDelete, insertWorkspaceBefore, insertSessionBefore, orderBy, groupExpansion, setGroupExpanded, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, t }) {
|
|
1745
1799
|
const list = useSessions((s) => s);
|
|
1746
1800
|
const pendingInteractions = useSessionPendingInteraction((s) => s);
|
|
1747
1801
|
const current = list.current;
|
|
@@ -1937,16 +1991,24 @@ window.__ModuleLoader__.load({
|
|
|
1937
1991
|
}
|
|
1938
1992
|
},
|
|
1939
1993
|
drag: workspaceDragProps,
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1994
|
+
actions: group.workspaceId === void 0 ? void 0 : {
|
|
1995
|
+
canArchive: (() => {
|
|
1996
|
+
const workspace = workspaces.find((item) => item.workspaceId === group.workspaceId);
|
|
1997
|
+
return workspace !== void 0 && archiveableWorkspaceSessionCount(workspace, archivedSessionIds) > 0;
|
|
1998
|
+
})(),
|
|
1999
|
+
rename: () => {
|
|
2000
|
+
/* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
|
|
2001
|
+
if (group.workspaceId !== void 0) onRenameRequest(group.workspaceId, group.label);
|
|
2002
|
+
},
|
|
2003
|
+
archive: () => {
|
|
2004
|
+
/* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
|
|
2005
|
+
if (group.workspaceId !== void 0) onArchiveRequest(group.workspaceId, group.label);
|
|
2006
|
+
},
|
|
2007
|
+
delete: () => {
|
|
2008
|
+
/* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
|
|
2009
|
+
if (group.workspaceId !== void 0) onDeleteRequest(group.workspaceId, group.label);
|
|
2010
|
+
}
|
|
1948
2011
|
}
|
|
1949
|
-
}
|
|
1950
2012
|
}),
|
|
1951
2013
|
(expandedSessionGroups.includes(group.key) ? group.sessions : group.sessions.slice(0, COLLAPSED_SESSION_LIMIT)).map((node) => {
|
|
1952
2014
|
const sameGroupDrag = drag !== null && drag.accountKey === group.key;
|
|
@@ -2216,7 +2278,7 @@ window.__ModuleLoader__.load({
|
|
|
2216
2278
|
* @param props - composed slot props (shell owner share + store + injected actions).
|
|
2217
2279
|
* @returns the region element tree.
|
|
2218
2280
|
*/
|
|
2219
|
-
function WorkspaceBrowser({ wide, expandSidebar, useSessions, useSessionPendingInteraction, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, unarchiveSession, deleteSession, insertSessionBefore, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, renderSlot, t }) {
|
|
2281
|
+
function WorkspaceBrowser({ wide, expandSidebar, useSessions, useSessionPendingInteraction, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, archiveWorkspaceSessions, unarchiveSession, deleteSession, insertSessionBefore, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, renderSlot, t }) {
|
|
2220
2282
|
const useEffectiveSessionPendingInteraction = useSessionPendingInteraction ?? useEmptySessionPendingInteraction;
|
|
2221
2283
|
const workspaces = useWorkspaces((state) => state.items);
|
|
2222
2284
|
const workspacePhase = useWorkspaces((state) => state.phase);
|
|
@@ -2411,6 +2473,34 @@ window.__ModuleLoader__.load({
|
|
|
2411
2473
|
showArchivedToast(formatArchiveError(reason, t));
|
|
2412
2474
|
});
|
|
2413
2475
|
};
|
|
2476
|
+
const [archiveWorkspaceTarget, setArchiveWorkspaceTarget] = (0, react.useState)(null);
|
|
2477
|
+
const [archivingWorkspace, setArchivingWorkspace] = (0, react.useState)(false);
|
|
2478
|
+
const [archiveWorkspaceError, setArchiveWorkspaceError] = (0, react.useState)(null);
|
|
2479
|
+
const closeArchiveWorkspace = () => {
|
|
2480
|
+
if (archivingWorkspace) return;
|
|
2481
|
+
setArchiveWorkspaceTarget(null);
|
|
2482
|
+
setArchiveWorkspaceError(null);
|
|
2483
|
+
};
|
|
2484
|
+
const onArchiveWorkspaceRequest = (workspaceId, title) => {
|
|
2485
|
+
const target = archiveWorkspaceDialogTarget(workspaces, workspaceId, title, archivedSet);
|
|
2486
|
+
if (target === null) return;
|
|
2487
|
+
setArchiveWorkspaceTarget(target);
|
|
2488
|
+
setArchiveWorkspaceError(null);
|
|
2489
|
+
};
|
|
2490
|
+
const confirmArchiveWorkspace = () => {
|
|
2491
|
+
/* v8 ignore next -- the Modal is absent without a target and its button is disabled while archiving. */
|
|
2492
|
+
if (archivingWorkspace || archiveWorkspaceTarget === null) return;
|
|
2493
|
+
setArchivingWorkspace(true);
|
|
2494
|
+
setArchiveWorkspaceError(null);
|
|
2495
|
+
archiveWorkspaceSessions(archiveWorkspaceTarget.workspaceId).then(() => {
|
|
2496
|
+
setArchivingWorkspace(false);
|
|
2497
|
+
setArchiveWorkspaceTarget(null);
|
|
2498
|
+
}).catch((reason) => {
|
|
2499
|
+
const state = archiveWorkspaceDialogFailureState(archiveWorkspaceTarget, reason, t);
|
|
2500
|
+
setArchivingWorkspace(state.archiving);
|
|
2501
|
+
setArchiveWorkspaceError(state.error);
|
|
2502
|
+
});
|
|
2503
|
+
};
|
|
2414
2504
|
const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
|
|
2415
2505
|
const [deleting, setDeleting] = (0, react.useState)(false);
|
|
2416
2506
|
const [deleteCommittedId, setDeleteCommittedId] = (0, react.useState)(null);
|
|
@@ -2675,6 +2765,7 @@ window.__ModuleLoader__.load({
|
|
|
2675
2765
|
setRenameDraft(currentTitle);
|
|
2676
2766
|
setRenameError(null);
|
|
2677
2767
|
},
|
|
2768
|
+
onArchiveRequest: onArchiveWorkspaceRequest,
|
|
2678
2769
|
onDeleteRequest: (workspaceId, title) => {
|
|
2679
2770
|
setDeleteTarget({
|
|
2680
2771
|
workspaceId,
|
|
@@ -2814,6 +2905,34 @@ window.__ModuleLoader__.load({
|
|
|
2814
2905
|
children: deleteError
|
|
2815
2906
|
})]
|
|
2816
2907
|
}),
|
|
2908
|
+
(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2909
|
+
open: archiveWorkspaceTarget !== null,
|
|
2910
|
+
onClose: closeArchiveWorkspace,
|
|
2911
|
+
closeLabel: t("close"),
|
|
2912
|
+
title: archiveWorkspaceTarget === null ? "" : t("archiveWorkspace.title", { n: archiveWorkspaceTarget.count }),
|
|
2913
|
+
...archiveWorkspaceTarget === null ? {} : { description: t("archiveWorkspace.desc", { name: archiveWorkspaceTarget.title }) },
|
|
2914
|
+
footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2915
|
+
variant: "outline",
|
|
2916
|
+
disabled: archivingWorkspace,
|
|
2917
|
+
onClick: closeArchiveWorkspace,
|
|
2918
|
+
children: t("cancel")
|
|
2919
|
+
}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2920
|
+
variant: "outline",
|
|
2921
|
+
className: "dsham_archiveWorkspaceConfirm",
|
|
2922
|
+
disabled: archivingWorkspace,
|
|
2923
|
+
onClick: confirmArchiveWorkspace,
|
|
2924
|
+
children: t("archiveWorkspace.confirm")
|
|
2925
|
+
})] }),
|
|
2926
|
+
children: [archivingWorkspace && (0, react_jsx_runtime.jsx)("div", {
|
|
2927
|
+
className: WorkspaceBrowser_module_css_default.deleteStatus,
|
|
2928
|
+
role: "status",
|
|
2929
|
+
children: t("archiveWorkspace.pending")
|
|
2930
|
+
}), archiveWorkspaceError !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
2931
|
+
className: WorkspaceBrowser_module_css_default.renameError,
|
|
2932
|
+
role: "alert",
|
|
2933
|
+
children: archiveWorkspaceError
|
|
2934
|
+
})]
|
|
2935
|
+
}),
|
|
2817
2936
|
(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2818
2937
|
open: deleteSessionTarget !== null,
|
|
2819
2938
|
onClose: closeDeleteSession,
|
|
@@ -2858,13 +2977,14 @@ window.__ModuleLoader__.load({
|
|
|
2858
2977
|
const ARCHIVE_SETTINGS_BATCH_CSS = ".dsham_settingsHeaderActions,.dsham_settingsGroupMeta{display:flex;align-items:center;gap:8px;flex:none}.dsham_settingsRestoreAll{display:inline-flex;align-items:center;min-height:32px;padding:0 12px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsRestoreAll:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsRestoreAll:disabled,.dsham_settingsDanger:disabled,.dsham_settingsGroupMenu:disabled{cursor:not-allowed;opacity:.5}.dsham_settingsGroupMenu{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;color:var(--dsw-alias-label-tertiary);background:transparent;border:0;border-radius:8px;cursor:pointer}.dsham_settingsGroupMenu:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsStatus{margin-top:10px;color:var(--dsw-alias-label-secondary);font-size:12px}@media(max-width:720px){.dsham_settingsHeader{flex-direction:column}.dsham_settingsHeaderActions{align-self:flex-end}}";
|
|
2859
2978
|
const ARCHIVE_SETTINGS_EXTERNAL_LINK_CSS = ".dsham_settingsTitleRow{display:flex;align-items:center;gap:8px;min-width:0}.dsham_settingsLinks{display:flex;align-items:center;gap:4px}.dsham_settingsExternalLink{display:inline-flex;align-items:center;gap:5px;min-height:28px;padding:0 8px;color:var(--dsw-alias-label-secondary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;font-size:12px;font-weight:500;line-height:18px;text-decoration:none;white-space:nowrap}.dsham_settingsExternalLink:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsExternalLink:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsham_settingsExternalLink svg{flex:none}@media(max-width:720px){.dsham_settingsTitleRow{flex-wrap:wrap}}";
|
|
2860
2979
|
const ARCHIVE_SETTINGS_DELETE_CONFIRM_CSS = ".dsham_settingsDeleteConfirm{color:var(--dsw-alias-state-error-primary)!important;background:transparent!important;border-color:var(--dsw-alias-state-error-primary)!important}.dsham_settingsDeleteConfirm:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)!important}.dsham_settingsDeleteConfirm:focus-visible{outline:2px solid var(--dsw-alias-state-error-secondary);outline-offset:2px}.dsham_settingsDeleteConfirm:disabled{cursor:not-allowed;opacity:.5}";
|
|
2980
|
+
const ARCHIVE_WORKSPACE_CONFIRM_CSS = ".dsham_archiveWorkspaceConfirm{color:var(--dsw-alias-state-error-primary)!important;background:transparent!important;border-color:var(--dsw-alias-state-error-primary)!important}.dsham_archiveWorkspaceConfirm:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)!important}.dsham_archiveWorkspaceConfirm:focus-visible{outline:2px solid var(--dsw-alias-state-error-secondary);outline-offset:2px}.dsham_archiveWorkspaceConfirm:disabled{cursor:not-allowed;opacity:.5}";
|
|
2861
2981
|
const ARCHIVE_SETTINGS_SELECTION_CSS = ".dsham_settingsSelection{display:flex;align-items:center;gap:10px;min-height:40px;margin:-8px 0 16px;padding:4px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-button-elevated-fill))}.dsham_settingsSelectionToggle{display:inline-flex;align-items:center;gap:8px;min-width:0;color:var(--dsw-alias-label-primary);font-size:13px;cursor:pointer}.dsham_settingsCheckbox{width:16px;height:16px;margin:0;accent-color:#3b82f6}.dsham_settingsSelectionCount{margin-right:auto;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsham_settingsSelectionAction{min-height:28px;padding:0 10px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;cursor:pointer;font:inherit;font-size:12px;font-weight:500}.dsham_settingsSelectionAction:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsSelectionDelete{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.dsham_settingsSelectionAction:disabled{cursor:not-allowed;opacity:.5}@media(max-width:720px){.dsham_settingsSelection{flex-wrap:wrap}.dsham_settingsSelectionCount{margin-right:0}.dsham_settingsSelectionAction{margin-left:auto}.dsham_settingsSelectionAction+.dsham_settingsSelectionAction{margin-left:-2px}}";
|
|
2862
2982
|
const ARCHIVE_SETTINGS_LAYOUT_OVERRIDE = ".dsham_settings{margin:0 auto!important}@media(max-width:720px){.dsham_settings{margin:0 auto!important}}";
|
|
2863
2983
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify("dsh-archive-manager/ArchiveSettings.layout.css") + "]") === null) {
|
|
2864
2984
|
const tag = document.createElement("style");
|
|
2865
2985
|
tag.dataset.plugin = "@michengai/dsh-archive-manager";
|
|
2866
2986
|
tag.dataset.pluginCss = "dsh-archive-manager/ArchiveSettings.layout.css";
|
|
2867
|
-
tag.textContent = ARCHIVE_SETTINGS_LAYOUT_OVERRIDE + ARCHIVE_SETTINGS_DELETE_CONFIRM_CSS;
|
|
2987
|
+
tag.textContent = ARCHIVE_SETTINGS_LAYOUT_OVERRIDE + ARCHIVE_SETTINGS_DELETE_CONFIRM_CSS + ARCHIVE_WORKSPACE_CONFIRM_CSS;
|
|
2868
2988
|
document.head.appendChild(tag);
|
|
2869
2989
|
}
|
|
2870
2990
|
/** 设置页筛选/排序:自定义菜单,避免原生 select 弹出系统浅色下拉。 */
|
|
@@ -3436,6 +3556,11 @@ window.__ModuleLoader__.load({
|
|
|
3436
3556
|
"delete.pending": "正在删除工作区…",
|
|
3437
3557
|
"menu.fork": "分叉会话",
|
|
3438
3558
|
"menu.archiveSession": "归档会话",
|
|
3559
|
+
"menu.archiveWorkspace": "归档全部聊天",
|
|
3560
|
+
"archiveWorkspace.title": "归档 {n} 个聊天?",
|
|
3561
|
+
"archiveWorkspace.desc": "这会将“{name}”中的聊天归档。之后你可以在已归档的聊天中找到它们。",
|
|
3562
|
+
"archiveWorkspace.confirm": "全部归档",
|
|
3563
|
+
"archiveWorkspace.pending": "正在归档聊天…",
|
|
3439
3564
|
"sessions.count.one": "{n} 个会话",
|
|
3440
3565
|
"sessions.count.other": "{n} 个会话",
|
|
3441
3566
|
"actions.workspace.aria": "工作区“{name}”的操作",
|
|
@@ -3562,6 +3687,11 @@ window.__ModuleLoader__.load({
|
|
|
3562
3687
|
"delete.pending": "Deleting workspace…",
|
|
3563
3688
|
"menu.fork": "Fork session",
|
|
3564
3689
|
"menu.archiveSession": "Archive session",
|
|
3690
|
+
"menu.archiveWorkspace": "Archive all chats",
|
|
3691
|
+
"archiveWorkspace.title": "Archive {n} chats?",
|
|
3692
|
+
"archiveWorkspace.desc": "This archives the chats in “{name}”. You can find them later in Archived chats.",
|
|
3693
|
+
"archiveWorkspace.confirm": "Archive all",
|
|
3694
|
+
"archiveWorkspace.pending": "Archiving chats…",
|
|
3565
3695
|
"sessions.count.one": "{n} session",
|
|
3566
3696
|
"sessions.count.other": "{n} sessions",
|
|
3567
3697
|
"actions.workspace.aria": "Workspace actions for {name}",
|
|
@@ -3767,6 +3897,12 @@ window.__ModuleLoader__.load({
|
|
|
3767
3897
|
* effect-scoped inside `applyWorkspaceBrowser`).
|
|
3768
3898
|
* @param ctx - client root context.
|
|
3769
3899
|
*/
|
|
3900
|
+
async function archiveWorkspaceSessionsAndRefresh(registry, workspaceId, refresh) {
|
|
3901
|
+
const result = await registry.archiveWorkspaceSessions(workspaceId);
|
|
3902
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
3903
|
+
await refresh();
|
|
3904
|
+
return result.value;
|
|
3905
|
+
}
|
|
3770
3906
|
async function apply(ctx) {
|
|
3771
3907
|
const remote = ctx.get("remote");
|
|
3772
3908
|
let disposeRemote = () => {};
|
|
@@ -3819,6 +3955,11 @@ window.__ModuleLoader__.load({
|
|
|
3819
3955
|
if (!result.ok) throw new Error(result.error.message);
|
|
3820
3956
|
await refreshSessionList();
|
|
3821
3957
|
};
|
|
3958
|
+
const archiveWorkspaceSessions = async (workspaceId) => {
|
|
3959
|
+
const registry = ctx.get("remote.workspaceRegistry");
|
|
3960
|
+
if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
|
|
3961
|
+
return archiveWorkspaceSessionsAndRefresh(registry, workspaceId, refreshSessionList);
|
|
3962
|
+
};
|
|
3822
3963
|
const deleteSession = async (sessionId) => {
|
|
3823
3964
|
const registry = ctx.get("remote.workspaceRegistry");
|
|
3824
3965
|
if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
|
|
@@ -3889,6 +4030,7 @@ window.__ModuleLoader__.load({
|
|
|
3889
4030
|
archiveSession: async (sessionId) => {
|
|
3890
4031
|
await ctx.workspaces.archiveSession(sessionId);
|
|
3891
4032
|
},
|
|
4033
|
+
archiveWorkspaceSessions,
|
|
3892
4034
|
unarchiveSession,
|
|
3893
4035
|
deleteSession,
|
|
3894
4036
|
insertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {
|
|
@@ -3956,6 +4098,10 @@ window.__ModuleLoader__.load({
|
|
|
3956
4098
|
pruneArchivedSelection,
|
|
3957
4099
|
toggleArchivedSelection,
|
|
3958
4100
|
archivedDeleteFeedback,
|
|
4101
|
+
archiveableWorkspaceSessionCount,
|
|
4102
|
+
archiveWorkspaceDialogTarget,
|
|
4103
|
+
archiveWorkspaceDialogFailureState,
|
|
4104
|
+
archiveWorkspaceSessionsAndRefresh,
|
|
3959
4105
|
createWorkspaceViewStore,
|
|
3960
4106
|
bindObservable,
|
|
3961
4107
|
hasSplitClientStore,
|
package/lib/projcache.js
CHANGED
|
@@ -10,21 +10,17 @@ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
|
10
10
|
import { trackTombstone } from "./tombstone.js";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* a fixed-length SHA-256 token and the original id is carried in the value.
|
|
17
|
-
*
|
|
18
|
-
* On rc.2 the runtime has no per-record layout, so the same domain remains a
|
|
19
|
-
* single JSON file. After that user upgrades to alpha.2, every already-safe
|
|
20
|
-
* hashed key can be migrated by the official backend without hitting the
|
|
21
|
-
* legacy colon bug.
|
|
13
|
+
* 当前投影缓存写入独立的 v2 域,避免 DSH alpha.2 将包含冒号的逻辑会话 ID
|
|
14
|
+
* 直接映射为 Windows 文件名。旧名 v1 与旧名 v2 域只在启动时读取并补缺迁入,
|
|
15
|
+
* 迁移不删除原始缓存,任意中断都可在后续启动继续。
|
|
22
16
|
*/
|
|
23
17
|
const storedCheckpointRecord = checkpointRecord.extend({
|
|
24
18
|
sessionId: checkpointIdentity.shape.cwd.unwrap()
|
|
25
19
|
});
|
|
26
20
|
const safeDomainInput = {
|
|
27
|
-
|
|
21
|
+
// 旧版 DSH 不支持同名域的版本迁移。v2 改用新域名,确保升级时不会在
|
|
22
|
+
// 打开目标域之前因旧 v1 元数据而终止启动。
|
|
23
|
+
name: "session_projcache_archive_manager_v2",
|
|
28
24
|
version: 2,
|
|
29
25
|
tables: { sessions: domainTable(storedCheckpointRecord) },
|
|
30
26
|
...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
|
|
@@ -32,11 +28,31 @@ const safeDomainInput = {
|
|
|
32
28
|
const safeProjectionCacheDomainSpec = defineDomain(safeDomainInput);
|
|
33
29
|
|
|
34
30
|
/** The last official whole-file cache format used by DSH rc.2. */
|
|
31
|
+
const legacyOptionalIdentityFields = {};
|
|
32
|
+
for (const field of ["isSeeded", "inheritedEventCount"]) {
|
|
33
|
+
// rc.2 的身份 schema 尚未定义这些字段;向 Zod 传入不存在的字段会在
|
|
34
|
+
// 首次解析历史记录时抛错,因此只能按当前运行时实际的 schema 构造掩码。
|
|
35
|
+
if (Object.hasOwn(checkpointIdentity.shape, field)) legacyOptionalIdentityFields[field] = true;
|
|
36
|
+
}
|
|
35
37
|
const legacyCheckpointRecord = checkpointRecord.extend({
|
|
36
|
-
identity: checkpointIdentity.partial(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
identity: checkpointIdentity.partial(legacyOptionalIdentityFields)
|
|
39
|
+
});
|
|
40
|
+
const legacyStoredCheckpointRecord = legacyCheckpointRecord.extend({
|
|
41
|
+
sessionId: checkpointIdentity.shape.cwd.unwrap()
|
|
42
|
+
});
|
|
43
|
+
/** v0.1.24 短暂使用过的旧名 v2 安全缓存域。 */
|
|
44
|
+
const legacySafeV2ProjectionCacheDomainSpec = defineDomain({
|
|
45
|
+
name: "session_projcache_archive_manager",
|
|
46
|
+
version: 2,
|
|
47
|
+
tables: { sessions: domainTable(storedCheckpointRecord) },
|
|
48
|
+
...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
|
|
49
|
+
});
|
|
50
|
+
/** v0.1.23 及以前归档管理器自身使用的安全缓存域。 */
|
|
51
|
+
const legacySafeProjectionCacheDomainSpec = defineDomain({
|
|
52
|
+
name: "session_projcache_archive_manager",
|
|
53
|
+
version: 1,
|
|
54
|
+
tables: { sessions: domainTable(legacyStoredCheckpointRecord) },
|
|
55
|
+
...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
|
|
40
56
|
});
|
|
41
57
|
const legacyProjectionCacheDomainSpec = defineDomain({
|
|
42
58
|
name: "session_projcache",
|
|
@@ -113,6 +129,23 @@ async function readSourceRecords(ctx, spec, label) {
|
|
|
113
129
|
}
|
|
114
130
|
}
|
|
115
131
|
|
|
132
|
+
async function readLegacySafeRecords(ctx, spec, label) {
|
|
133
|
+
const source = await readSourceRecords(ctx, spec, label);
|
|
134
|
+
const records = [];
|
|
135
|
+
for (const [physicalKey, stored] of source.records) {
|
|
136
|
+
if (stored === null || typeof stored !== "object" || typeof stored.sessionId !== "string") {
|
|
137
|
+
ctx.logger.warn(`archive-manager projcache: ${label} row "${physicalKey}" import skipped because its session id is invalid`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (projectionCacheStorageKey(stored.sessionId) !== physicalKey) {
|
|
141
|
+
ctx.logger.warn(`archive-manager projcache: ${label} row "${physicalKey}" import skipped because its storage key does not match the session id`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
records.push([stored.sessionId, unwrapStoredCheckpoint(stored)]);
|
|
145
|
+
}
|
|
146
|
+
return { ...source, records };
|
|
147
|
+
}
|
|
148
|
+
|
|
116
149
|
/**
|
|
117
150
|
* 旧版记录没有完整身份时,只能安全补齐未播种会话的零继承切点。
|
|
118
151
|
* 播种会话无法从头部反推出精确切点,宁可跳过并按需重建。
|
|
@@ -158,14 +191,18 @@ async function importMissingRecords(ctx, target, records, label) {
|
|
|
158
191
|
}
|
|
159
192
|
|
|
160
193
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
194
|
+
* 合并归档管理器旧名 v2、旧名 v1、官方新版逐会话与旧版整文件缓存。目标域记录
|
|
195
|
+
* 最高优先,随后按新旧顺序只补缺失记录;任一来源失败时仍继续尝试另一个来源。
|
|
163
196
|
*/
|
|
164
197
|
async function importPreviousProjectionCache(ctx, target) {
|
|
198
|
+
const legacySafeV2 = await readLegacySafeRecords(ctx, legacySafeV2ProjectionCacheDomainSpec, "archive-manager v2");
|
|
199
|
+
const legacySafe = await readLegacySafeRecords(ctx, legacySafeProjectionCacheDomainSpec, "archive-manager v1");
|
|
165
200
|
const legacy = await readSourceRecords(ctx, legacyProjectionCacheDomainSpec, "legacy v3");
|
|
166
201
|
const sameSpec = projectionCacheDomainSpec.version === legacyProjectionCacheDomainSpec.version
|
|
167
202
|
&& projectionCacheDomainSpec.layout === legacyProjectionCacheDomainSpec.layout;
|
|
168
203
|
let imported = 0;
|
|
204
|
+
if (legacySafeV2.opened) imported += await importMissingRecords(ctx, target, legacySafeV2.records, "archive-manager v2");
|
|
205
|
+
if (legacySafe.opened) imported += await importMissingRecords(ctx, target, legacySafe.records, "archive-manager v1");
|
|
169
206
|
if (!sameSpec) {
|
|
170
207
|
const current = await readSourceRecords(ctx, projectionCacheDomainSpec, "current");
|
|
171
208
|
if (current.opened) imported += await importMissingRecords(ctx, target, current.records, "current");
|
|
@@ -178,8 +215,9 @@ async function importPreviousProjectionCache(ctx, target) {
|
|
|
178
215
|
* dsh-archive-manager projcache 半边。
|
|
179
216
|
*
|
|
180
217
|
* `ArchiveProjectionCache` 继承上游 `SessionProjectionCache`,保留服务名
|
|
181
|
-
* `sessionProjectionCache` 与 fail-soft
|
|
182
|
-
* `
|
|
218
|
+
* `sessionProjectionCache` 与 fail-soft 写路径,但写入独立、路径安全的
|
|
219
|
+
* `session_projcache_archive_manager_v2` 域。旧名 v1 与 v2 域只在启动时
|
|
220
|
+
* 读取并补缺迁入;归档管理的
|
|
183
221
|
* `deleteSession` 增加三处守护:
|
|
184
222
|
*
|
|
185
223
|
* - `delete(id)` - 永久移除兼容域中该会话经 SHA-256 映射的缓存投影行,
|
|
@@ -289,6 +327,8 @@ export {
|
|
|
289
327
|
SafeSessionTable,
|
|
290
328
|
importPreviousProjectionCache,
|
|
291
329
|
legacyProjectionCacheDomainSpec,
|
|
330
|
+
legacySafeProjectionCacheDomainSpec,
|
|
331
|
+
legacySafeV2ProjectionCacheDomainSpec,
|
|
292
332
|
projectionCacheStorageKey,
|
|
293
333
|
safeProjectionCacheDomainSpec
|
|
294
334
|
};
|
package/lib/workspace.js
CHANGED
|
@@ -65,6 +65,12 @@ const sessionIdSchema = {
|
|
|
65
65
|
return value;
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
|
+
const workspaceIdSchema = {
|
|
69
|
+
parse(value) {
|
|
70
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError(`workspaceId must be a non-empty string, got ${String(value)}`);
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
68
74
|
const archivedSetSchema = {
|
|
69
75
|
parse(value) {
|
|
70
76
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
|
|
@@ -96,6 +102,14 @@ const unarchivedBatchSchema = {
|
|
|
96
102
|
return value;
|
|
97
103
|
}
|
|
98
104
|
};
|
|
105
|
+
const archivedWorkspaceBatchSchema = {
|
|
106
|
+
parse(value) {
|
|
107
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
|
|
108
|
+
if (!Array.isArray(value.archivedSessionIds) || value.archivedSessionIds.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIds must be a string array");
|
|
109
|
+
if (!Array.isArray(value.archivedSessionIdsAdded) || value.archivedSessionIdsAdded.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIdsAdded must be a string array");
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
99
113
|
const deletedBatchSchema = {
|
|
100
114
|
parse(value) {
|
|
101
115
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
|
|
@@ -176,6 +190,25 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
|
|
|
176
190
|
},
|
|
177
191
|
sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
|
|
178
192
|
},
|
|
193
|
+
{
|
|
194
|
+
id: "@michengai/dsh-archive-manager#workspaceRegistry/archiveWorkspaceSessions",
|
|
195
|
+
service: "workspaceRegistry",
|
|
196
|
+
namespace: "workspaceRegistry",
|
|
197
|
+
method: "archiveWorkspaceSessions",
|
|
198
|
+
invocation: { kind: "direct" },
|
|
199
|
+
parameters: [{
|
|
200
|
+
name: "workspaceId",
|
|
201
|
+
wire: "workspaceId",
|
|
202
|
+
source: "json",
|
|
203
|
+
codec: { mode: "strict", typeSymbol: "@deepseek-ai/dsh-workspace/types#WorkspaceId", schema: workspaceIdSchema }
|
|
204
|
+
}],
|
|
205
|
+
result: {
|
|
206
|
+
mode: "strict",
|
|
207
|
+
typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedWorkspaceBatch",
|
|
208
|
+
schema: archivedWorkspaceBatchSchema
|
|
209
|
+
},
|
|
210
|
+
sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
|
|
211
|
+
},
|
|
179
212
|
{
|
|
180
213
|
id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteArchivedSessions",
|
|
181
214
|
service: "workspaceRegistry",
|
|
@@ -248,6 +281,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
248
281
|
markRemoteMethod(this, "unarchiveSession");
|
|
249
282
|
markRemoteMethod(this, "deleteSession");
|
|
250
283
|
markRemoteMethod(this, "unarchiveSessions");
|
|
284
|
+
markRemoteMethod(this, "archiveWorkspaceSessions");
|
|
251
285
|
markRemoteMethod(this, "deleteArchivedSessions");
|
|
252
286
|
markRemoteMethod(this, "archivedSessionMetadata");
|
|
253
287
|
registerHostRemote(this.ctx);
|
|
@@ -328,6 +362,36 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
328
362
|
});
|
|
329
363
|
}
|
|
330
364
|
/**
|
|
365
|
+
* 将一个工作区内所有会话加入归档集合。先确认所有待归档会话仍存在,
|
|
366
|
+
* 再执行单次状态写入,因此未知会话不会导致项目只归档一部分。
|
|
367
|
+
*/
|
|
368
|
+
async archiveWorkspaceSessions(workspaceId) {
|
|
369
|
+
return this.enqueueOperation(async () => {
|
|
370
|
+
workspaceId = workspaceIdSchema.parse(workspaceId);
|
|
371
|
+
const workspace = this.requireTable().get(workspaceId);
|
|
372
|
+
if (workspace === void 0) throw new Error(`unknown workspace "${workspaceId}"`);
|
|
373
|
+
const state = this.requireState();
|
|
374
|
+
const archived = new Set(state.archivedSessionIds);
|
|
375
|
+
const archivedSessionIdsAdded = [...new Set(workspace.sessionIds)].filter((sessionId) => !archived.has(sessionId));
|
|
376
|
+
for (const sessionId of archivedSessionIdsAdded) {
|
|
377
|
+
if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
|
|
378
|
+
}
|
|
379
|
+
if (archivedSessionIdsAdded.length === 0) return {
|
|
380
|
+
archivedSessionIds: [...state.archivedSessionIds],
|
|
381
|
+
archivedSessionIdsAdded
|
|
382
|
+
};
|
|
383
|
+
const next = {
|
|
384
|
+
...state,
|
|
385
|
+
archivedSessionIds: [...state.archivedSessionIds, ...archivedSessionIdsAdded]
|
|
386
|
+
};
|
|
387
|
+
await this.setState(next);
|
|
388
|
+
return {
|
|
389
|
+
archivedSessionIds: [...next.archivedSessionIds],
|
|
390
|
+
archivedSessionIdsAdded
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
331
395
|
* 按宿主权威归档集合一次恢复全部、一个工作区或未分组的归档会话。
|
|
332
396
|
* 目标全部来自已归档集合,因此即使日志已被外部移除,也会清掉陈旧归档标记。
|
|
333
397
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michengai/dsh-archive-manager",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "NPM-installable DSH Web plugin for managing archived sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.21.0",
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "node scripts/check-package.mjs",
|
|
30
30
|
"test": "node --test test/*.test.mjs",
|
|
31
|
+
"test:compat": "node scripts/test-legacy-compatibility.mjs",
|
|
31
32
|
"pack:check": "pnpm pack --dry-run",
|
|
32
|
-
"prepublishOnly": "pnpm build && pnpm test"
|
|
33
|
+
"prepublishOnly": "pnpm build && pnpm test && pnpm test:compat"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
36
|
"@deepseek-ai/cordis": "^4.0.1",
|