@michengai/dsh-archive-manager 0.1.2 → 0.1.4
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 +12 -5
- package/README.zh-CN.md +13 -6
- package/assets/screenshots/archive-session-menu.png +0 -0
- package/assets/screenshots/archived-sessions.png +0 -0
- package/lib/client.js +4 -9
- package/lib/projcache.js +24 -17
- package/lib/workspace.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,17 +21,20 @@
|
|
|
21
21
|
<img src="https://img.shields.io/badge/Node.js-22%2B-339933?logo=nodedotjs&logoColor=white" alt="Node.js 22 or later">
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="assets/screenshots/archived-sessions.png" alt="Archived Sessions settings page" width="1200">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
24
28
|
> DSH Archive Manager is a community-maintained plugin, not an official DeepSeek AI product.
|
|
25
29
|
|
|
26
30
|
## Features
|
|
27
31
|
|
|
32
|
+
- Archives a session from the sidebar session menu.
|
|
28
33
|
- Lists archived sessions by workspace in **Settings → Archived Sessions**.
|
|
29
34
|
- Safely restores a session to its original workspace position.
|
|
30
35
|
- Permanently deletes a confirmed session, its workspace association, archive marker, and projection cache.
|
|
31
36
|
- Removes unloaded deleted sessions from connected sidebars immediately.
|
|
32
37
|
|
|
33
|
-

|
|
34
|
-
|
|
35
38
|
## Prerequisites
|
|
36
39
|
|
|
37
40
|
- A working DeepSeek Harness Web installation with `dsh` available in PowerShell.
|
|
@@ -73,11 +76,15 @@ Restart DSH Web and hard-refresh the browser. Local installation applies `cordis
|
|
|
73
76
|
|
|
74
77
|
## Usage
|
|
75
78
|
|
|
76
|
-
1. Open
|
|
77
|
-
2.
|
|
78
|
-
3. Select **Unarchive** to restore a session, or
|
|
79
|
+
1. Open the sidebar session menu and choose **Archive session**.
|
|
80
|
+
2. Open **Settings → Archived Sessions** to inspect sessions by workspace.
|
|
81
|
+
3. Select **Unarchive** to restore a session, or use the delete icon to remove it permanently.
|
|
79
82
|
4. Confirm deletion. **It cannot be undone.**
|
|
80
83
|
|
|
84
|
+
<p align="center">
|
|
85
|
+
<img src="assets/screenshots/archive-session-menu.png" alt="Archive a session from the session menu" width="1200">
|
|
86
|
+
</p>
|
|
87
|
+
|
|
81
88
|
If the entry is missing after installation or upgrade, restart DSH Web and hard-refresh the browser. It is located directly after **Connectors** in Settings.
|
|
82
89
|
|
|
83
90
|
## Data handling limits
|
package/README.zh-CN.md
CHANGED
|
@@ -21,17 +21,20 @@
|
|
|
21
21
|
<img src="https://img.shields.io/badge/Node.js-22%2B-339933?logo=nodedotjs&logoColor=white" alt="Node.js 22 或更高版本">
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="assets/screenshots/archived-sessions.png" alt="已归档会话设置页面" width="1200">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
24
28
|
> DSH Archive Manager 是社区维护的插件,并非 DeepSeek AI 官方产品。
|
|
25
29
|
|
|
26
30
|
## 功能概览
|
|
27
31
|
|
|
32
|
+
- 在侧栏会话菜单中选择「归档会话」。
|
|
28
33
|
- 在「设置 → 已归档」按工作区展示归档会话。
|
|
29
34
|
- 安全取消归档,将会话恢复到原工作区位置。
|
|
30
35
|
- 经确认后永久删除会话、工作区归属、归档标记和投影缓存。
|
|
31
36
|
- 已删除的未加载归档会话会立即从已连接客户端的侧栏移除。
|
|
32
37
|
|
|
33
|
-

|
|
34
|
-
|
|
35
38
|
## 前置条件
|
|
36
39
|
|
|
37
40
|
- 已可正常运行 DeepSeek Harness Web,且可在 PowerShell 中使用 `dsh`。
|
|
@@ -73,12 +76,16 @@ dsh --profile web --dump-config
|
|
|
73
76
|
|
|
74
77
|
## 使用
|
|
75
78
|
|
|
76
|
-
1.
|
|
77
|
-
2.
|
|
78
|
-
3.
|
|
79
|
+
1. 在侧栏右键或打开会话菜单,选择「归档会话」。
|
|
80
|
+
2. 打开「设置 → 已归档」,按工作区查看归档会话。
|
|
81
|
+
3. 点击「取消归档」恢复会话;点击删除图标永久移除会话。
|
|
79
82
|
4. 删除前确认提示。**删除无法撤销。**
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
<p align="center">
|
|
85
|
+
<img src="assets/screenshots/archive-session-menu.png" alt="从会话菜单归档会话" width="1200">
|
|
86
|
+
</p>
|
|
87
|
+
|
|
88
|
+
安装或升级后找不到入口时,重启 DSH Web 并硬刷新浏览器;入口位于「设置」中,连接器之后。
|
|
82
89
|
|
|
83
90
|
## 数据处理边界
|
|
84
91
|
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -2456,7 +2456,7 @@ window.__ModuleLoader__.load({
|
|
|
2456
2456
|
//#endregion
|
|
2457
2457
|
//#region dsh-archive-manager: settings section
|
|
2458
2458
|
const ARCHIVE_SETTINGS_CSS = ".dsham_settings{box-sizing:border-box;width:min(100%,768px);margin:64px auto 80px;padding:0 20px;color:var(--dsw-alias-label-primary)}.dsham_settingsHeader{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:48px}.dsham_settings h2{margin:0;font-size:24px;font-weight:650;letter-spacing:-.02em;line-height:32px}.dsham_settingsDanger{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 11px;color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);border:0;border-radius:9px;cursor:pointer;font-size:12px;font-weight:600}.dsham_settingsDanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)}.dsham_settingsToolbar{display:flex;gap:8px;margin-bottom:40px}.dsham_settingsSearch{display:flex;align-items:center;gap:8px;min-width:0;flex:1;height:32px;padding:0 12px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-button-elevated-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:16px}.dsham_settingsSearch:focus-within{border-color:var(--dsw-alias-label-tertiary)}.dsham_settingsSearch input{width:100%;min-width:0;padding:0;color:var(--dsw-alias-label-primary);background:transparent;border:0;outline:0;font:inherit;font-size:12px}.dsham_settingsSearch input::placeholder{color:var(--dsw-alias-label-tertiary)}.dsham_settingsFilter{min-width:132px;height:30px;padding:0 28px 0 11px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-button-elevated-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:9px;cursor:pointer;font:inherit;font-size:12px}.dsham_settingsGroup{margin:0 0 48px}.dsham_settingsGroupHeading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 14px}.dsham_settingsGroupTitle{display:flex;align-items:center;gap:8px;min-width:0;margin:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.dsham_settingsGroupTitle svg{flex:none;color:var(--dsw-alias-label-secondary)}.dsham_settingsCount{flex:none;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsham_settingsList{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-button-elevated-fill)}.dsham_settingsRow{display:flex;align-items:center;gap:12px;min-height:60px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dsham_settingsRow:last-child{border-bottom:0}.dsham_settingsContent{min-width:0;flex:1}.dsham_settingsTitle{overflow:hidden;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dsham_settingsMeta{margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}.dsham_settingsActions{display:flex;align-items:center;gap:8px}.dsham_settingsAction{height:30px;padding:0 10px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);border:0;border-radius:9px;cursor:pointer;font-size:12px;font-weight:600}.dsham_settingsAction:hover{filter:brightness(1.12)}.dsham_settingsDelete{display:flex;align-items:center;justify-content:center;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:transparent;border:0;border-radius:8px;cursor:pointer}.dsham_settingsDelete:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsEmpty{padding:28px 8px;color:var(--dsw-alias-label-secondary);text-align:center}.dsham_settingsError{margin-top:10px;color:var(--dsw-alias-state-error-primary);font-size:12px}@media(max-width:720px){.dsham_settings{width:100%;margin:28px auto 48px;padding:0 16px}.dsham_settingsHeader{margin-bottom:28px}.dsham_settingsToolbar{flex-wrap:wrap;margin-bottom:28px}.dsham_settingsSearch{flex-basis:100%}.dsham_settingsFilter{flex:1;min-width:0}.dsham_settingsGroup{margin-bottom:32px}.dsham_settingsRow{padding:10px 12px}.dsham_settingsActions{gap:4px}}";
|
|
2459
|
-
const ARCHIVE_SETTINGS_LAYOUT_OVERRIDE = ".dsham_settings{margin:24px auto 80px!important}
|
|
2459
|
+
const ARCHIVE_SETTINGS_LAYOUT_OVERRIDE = ".dsham_settings{margin:24px auto 80px!important}@media(max-width:720px){.dsham_settings{margin:16px auto 48px!important}}";
|
|
2460
2460
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify("dsh-archive-manager/ArchiveSettings.layout.css") + "]") === null) {
|
|
2461
2461
|
const tag = document.createElement("style");
|
|
2462
2462
|
tag.dataset.plugin = "@michengai/dsh-archive-manager";
|
|
@@ -2472,7 +2472,6 @@ window.__ModuleLoader__.load({
|
|
|
2472
2472
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
2473
2473
|
const [error, setError] = (0, react.useState)(null);
|
|
2474
2474
|
const [query, setQuery] = (0, react.useState)("");
|
|
2475
|
-
const [chatType, setChatType] = (0, react.useState)("all");
|
|
2476
2475
|
const [project, setProject] = (0, react.useState)("all");
|
|
2477
2476
|
const groups = (0, react.useMemo)(() => {
|
|
2478
2477
|
const archived = new Set(workspaceState.archivedSessionIds);
|
|
@@ -2496,9 +2495,9 @@ window.__ModuleLoader__.load({
|
|
|
2496
2495
|
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
2497
2496
|
return groups.filter((group) => project === "all" || project === group.title).map((group) => ({
|
|
2498
2497
|
...group,
|
|
2499
|
-
sessions: group.sessions.filter((session) =>
|
|
2498
|
+
sessions: group.sessions.filter((session) => normalizedQuery === "" || displayTitle(session, t).toLocaleLowerCase().includes(normalizedQuery))
|
|
2500
2499
|
})).filter((group) => group.sessions.length > 0);
|
|
2501
|
-
}, [
|
|
2500
|
+
}, [groups, project, query, t]);
|
|
2502
2501
|
const archivedSessionIds = (0, react.useMemo)(() => groups.flatMap((group) => group.sessions.map((session) => session.id)), [groups]);
|
|
2503
2502
|
const onUnarchive = (sessionId) => {
|
|
2504
2503
|
setError(null);
|
|
@@ -2525,7 +2524,7 @@ window.__ModuleLoader__.load({
|
|
|
2525
2524
|
return (0, react_jsx_runtime.jsxs)("section", {
|
|
2526
2525
|
className: "dsham_settings",
|
|
2527
2526
|
"aria-label": t("archives.title"),
|
|
2528
|
-
children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || archivedSessionIds.length === 0, onClick: () => setDeleteTarget({ all: true }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsToolbar", children: [(0, react_jsx_runtime.jsxs)("label", { className: "dsham_settingsSearch", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}), (0, react_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("archives.searchPlaceholder"), "aria-label": t("archives.searchPlaceholder") })] }), (0, react_jsx_runtime.jsx)("select", { className: "dsham_settingsFilter", value:
|
|
2527
|
+
children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || archivedSessionIds.length === 0, onClick: () => setDeleteTarget({ all: true }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsToolbar", children: [(0, react_jsx_runtime.jsxs)("label", { className: "dsham_settingsSearch", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}), (0, react_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("archives.searchPlaceholder"), "aria-label": t("archives.searchPlaceholder") })] }), (0, react_jsx_runtime.jsx)("select", { className: "dsham_settingsFilter", value: project, onChange: (event) => setProject(event.target.value), "aria-label": t("archives.projectFilter"), children: [(0, react_jsx_runtime.jsx)("option", { value: "all", children: t("archives.allProjects") }), groups.map((group) => (0, react_jsx_runtime.jsx)("option", { value: group.title, children: group.title }, group.title))] })] }), groups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.empty") }) : filteredGroups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.emptyFiltered") }) : filteredGroups.map((group) => (0, react_jsx_runtime.jsxs)("section", {
|
|
2529
2528
|
className: "dsham_settingsGroup",
|
|
2530
2529
|
children: [(0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsGroupHeading", children: [(0, react_jsx_runtime.jsxs)("h3", { className: "dsham_settingsGroupTitle", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, {}), group.title] }), (0, react_jsx_runtime.jsx)("span", { className: "dsham_settingsCount", children: t("archives.sessionCount", { n: group.sessions.length }) })] }), (0, react_jsx_runtime.jsx)("div", {
|
|
2531
2530
|
className: "dsham_settingsList",
|
|
@@ -2591,8 +2590,6 @@ window.__ModuleLoader__.load({
|
|
|
2591
2590
|
"archives.empty": "暂无已归档会话",
|
|
2592
2591
|
"archives.emptyFiltered": "没有匹配的已归档聊天",
|
|
2593
2592
|
"archives.searchPlaceholder": "搜索已归档聊天",
|
|
2594
|
-
"archives.chatFilter": "按聊天类型筛选",
|
|
2595
|
-
"archives.allChats": "全部聊天",
|
|
2596
2593
|
"archives.projectFilter": "按项目筛选",
|
|
2597
2594
|
"archives.allProjects": "所有项目",
|
|
2598
2595
|
"archives.sessionCount": "{n} 个聊天",
|
|
@@ -2678,8 +2675,6 @@ window.__ModuleLoader__.load({
|
|
|
2678
2675
|
"archives.empty": "No archived sessions.",
|
|
2679
2676
|
"archives.emptyFiltered": "No archived chats match your filters.",
|
|
2680
2677
|
"archives.searchPlaceholder": "Search archived chats",
|
|
2681
|
-
"archives.chatFilter": "Filter by chat type",
|
|
2682
|
-
"archives.allChats": "All chats",
|
|
2683
2678
|
"archives.projectFilter": "Filter by project",
|
|
2684
2679
|
"archives.allProjects": "All projects",
|
|
2685
2680
|
"archives.sessionCount": "{n} chats",
|
package/lib/projcache.js
CHANGED
|
@@ -10,29 +10,35 @@ import { SessionProjectionCache } from "@deepseek-ai/dsh-session-projection-cach
|
|
|
10
10
|
*
|
|
11
11
|
* - `delete(id)` — permanently remove one session's cached projection row
|
|
12
12
|
* (`table.delete` on the `session_projcache` domain).
|
|
13
|
-
* - `whenIdle()` — resolve once every in-flight
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* after deletion and resurrects the cache entry.
|
|
13
|
+
* - `whenIdle()` — resolve once every in-flight public `write()` has settled.
|
|
14
|
+
* - `delete(id)` installs a tombstone before removing the row, so an earlier
|
|
15
|
+
* write that settles late removes its own stale row instead of resurrecting
|
|
16
|
+
* the deleted cache entry.
|
|
18
17
|
*
|
|
19
18
|
* The default export is a Service subclass (same shape as the shipped
|
|
20
19
|
* `@deepseek-ai/dsh-session-projection-cache` package), so the profile patch
|
|
21
20
|
* can substitute this package for the `session-projection-cache` row with no
|
|
22
21
|
* other wiring change.
|
|
23
22
|
*/
|
|
24
|
-
var ArchiveProjectionCache = class extends SessionProjectionCache {
|
|
25
|
-
/**
|
|
26
|
-
|
|
23
|
+
var ArchiveProjectionCache = class extends SessionProjectionCache {
|
|
24
|
+
/** 已永久删除的会话不会再允许投影缓存写入。 */
|
|
25
|
+
deletedSessionIds = /* @__PURE__ */ new Set();
|
|
26
|
+
/** Tail of in-flight checkpoint writes (settled promises only). */
|
|
27
|
+
writeTail = Promise.resolve();
|
|
27
28
|
constructor(ctx, config) {
|
|
28
29
|
super(ctx, config);
|
|
29
30
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
const task =
|
|
33
|
-
this.writeTail = Promise.allSettled([this.writeTail, task]).then(() => void 0);
|
|
34
|
-
return task;
|
|
35
|
-
}
|
|
31
|
+
/** 跟踪公开写入路径,避免依赖上游私有 `flushSoft` 的实现细节。 */
|
|
32
|
+
write(session) {
|
|
33
|
+
const task = this.writeCore(session);
|
|
34
|
+
this.writeTail = Promise.allSettled([this.writeTail, task]).then(() => void 0);
|
|
35
|
+
return task;
|
|
36
|
+
}
|
|
37
|
+
async writeCore(session) {
|
|
38
|
+
if (this.deletedSessionIds.has(session.id)) return;
|
|
39
|
+
await super.write(session);
|
|
40
|
+
if (this.deletedSessionIds.has(session.id)) await this.requireTable().delete(session.id);
|
|
41
|
+
}
|
|
36
42
|
/**
|
|
37
43
|
* Resolve once every tracked in-flight write has settled (failures
|
|
38
44
|
* included). Fresh calls during an idle cache resolve immediately.
|
|
@@ -46,9 +52,10 @@ var ArchiveProjectionCache = class extends SessionProjectionCache {
|
|
|
46
52
|
* @param id - the session whose cache row to delete.
|
|
47
53
|
* @returns resolution after the row is gone.
|
|
48
54
|
*/
|
|
49
|
-
async delete(id) {
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
async delete(id) {
|
|
56
|
+
this.deletedSessionIds.add(id);
|
|
57
|
+
await this.requireTable().delete(id);
|
|
58
|
+
}
|
|
52
59
|
};
|
|
53
60
|
//#endregion
|
|
54
61
|
export { ArchiveProjectionCache, ArchiveProjectionCache as default };
|
package/lib/workspace.js
CHANGED
|
@@ -146,8 +146,9 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
146
146
|
await rm(transcriptDir, { recursive: true, force: true });
|
|
147
147
|
} catch (error) {
|
|
148
148
|
const message = `cannot delete session "${sessionId}": transcript directory "${transcriptDir}" remains after bookkeeping cleanup`;
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
const detail = `${message}: ${String(error)}`;
|
|
150
|
+
this.ctx.logger.warn(`archive-manager: ${detail}`);
|
|
151
|
+
throw new Error(detail, { cause: error });
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
/** Drop the id from every workspace record and refresh entity snapshots. */
|