@michengai/dsh-archive-manager 0.1.29 → 0.1.31
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 +13 -2
- package/CHANGELOG.zh-CN.md +13 -2
- package/README.md +9 -5
- package/README.zh-CN.md +8 -4
- package/lib/client.js +332 -4
- package/lib/index.js +10 -2
- package/lib/plugin-updater.js +257 -0
- package/lib/workspace.js +9 -11
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
Published release notes are retained below; new versions are added without removing earlier entries.
|
|
6
6
|
|
|
7
|
+
## Unreleased
|
|
8
|
+
|
|
9
|
+
## 0.1.31 - 2026-09-07
|
|
10
|
+
|
|
11
|
+
- Added independent in-product update checks with automatic updates when a verified DSH update service is available and a profile-specific manual fallback otherwise.
|
|
12
|
+
- Removed the update-button dependency on `react-dom/client` so the client can load on Hosts that do not register that module id.
|
|
13
|
+
|
|
14
|
+
## 0.1.30 — 2026-09-04
|
|
15
|
+
|
|
16
|
+
- Kept failed archived-session deletions retryable until their transcript artifact is removed.
|
|
17
|
+
- Restricted transcript cleanup to the backend-owned artifact path and refreshed client session lists after deletion.
|
|
18
|
+
|
|
7
19
|
## 0.1.29 — 2026-09-03
|
|
8
20
|
|
|
9
|
-
-
|
|
10
|
-
- Extended the approved minimum-release-age entries to include the exact RC packages, keeping clean installs reproducible under the existing supply-chain policy.
|
|
21
|
+
- Added compatibility with DeepSeek Harness `0.1.2-rc.1`.
|
|
11
22
|
|
|
12
23
|
## 0.1.28 — 2026-09-03
|
|
13
24
|
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,10 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
以下发布说明会持续保留;新增版本时不再删除较早记录。
|
|
6
6
|
|
|
7
|
+
## 未发布
|
|
8
|
+
|
|
9
|
+
## 0.1.31 - 2026-09-07
|
|
10
|
+
|
|
11
|
+
- 新增独立的插件内检查更新能力:仅在已验证的 DSH 更新服务可用时自动更新,其余环境提供与 Profile 对应的手工更新命令。
|
|
12
|
+
- 更新按钮图标不再依赖 `react-dom/client`,避免宿主未注册该模块 id 时客户端无法加载。
|
|
13
|
+
|
|
14
|
+
## 0.1.30 — 2026-09-04
|
|
15
|
+
|
|
16
|
+
- 归档会话物理删除失败时保留可重试状态,直至转录工件删除成功。
|
|
17
|
+
- 转录清理限制为后端拥有的工件路径,并在删除后刷新客户端会话列表。
|
|
18
|
+
|
|
7
19
|
## 0.1.29 — 2026-09-03
|
|
8
20
|
|
|
9
|
-
-
|
|
10
|
-
- 将供应链最小发布时间策略的已批准条目扩展至对应 RC 精确版本,保证干净环境可复现安装。
|
|
21
|
+
- 支持 DeepSeek Harness `0.1.2-rc.1`。
|
|
11
22
|
|
|
12
23
|
## 0.1.28 — 2026-09-03
|
|
13
24
|
|
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
- Archive a session from the sidebar session menu.
|
|
25
25
|
- Archive every active chat in a workspace from its sidebar action menu.
|
|
26
|
-
- Search archived chats, sort them by update time, creation time, or title, and filter them by workspace in **Settings → Archived**.
|
|
26
|
+
- Search archived chats, sort them by update time, creation time, or title, and filter them by workspace in **Settings → Archived sessions**.
|
|
27
27
|
- Restore a session to its original workspace with **Unarchive**.
|
|
28
28
|
- Restore or permanently delete every archived chat in a project group.
|
|
29
29
|
- Restore all archived chats from the page header.
|
|
@@ -38,9 +38,9 @@ Open the sidebar session menu and choose **Archive session**:
|
|
|
38
38
|
|
|
39
39
|

|
|
40
40
|
|
|
41
|
-
Search, sort, filter by project, unarchive, or permanently delete chats in **Settings → Archived**:
|
|
41
|
+
Search, sort, filter by project, unarchive, or permanently delete chats in **Settings → Archived sessions**:
|
|
42
42
|
|
|
43
|
-

|
|
44
44
|
|
|
45
45
|
## DSH product ecosystem
|
|
46
46
|
|
|
@@ -85,7 +85,7 @@ Install the DSH plugin from source at https://github.com/MichengAI/dsh-archive-m
|
|
|
85
85
|
| Codex | Send one of the sentences above to Codex and let it install locally. |
|
|
86
86
|
| WorkBuddy | Send one of the sentences above to WorkBuddy; for a source install you can also paste `https://github.com/MichengAI/dsh-archive-manager`. |
|
|
87
87
|
|
|
88
|
-
Codex and WorkBuddy only install the plugin. After that, open DSH Web and use **Settings → Archived**.
|
|
88
|
+
Codex and WorkBuddy only install the plugin. After that, open DSH Web and use **Settings → Archived sessions**.
|
|
89
89
|
|
|
90
90
|
You can also run the same npm command yourself:
|
|
91
91
|
|
|
@@ -128,10 +128,14 @@ dsh --profile web --dump-config
|
|
|
128
128
|
|
|
129
129
|
Restart DSH Web and hard-refresh the browser. `dsh plugin ... add .` reads the package metadata and `cordis.patch.yml`; do not install by copying `lib` directly.
|
|
130
130
|
|
|
131
|
+
## Updates
|
|
132
|
+
|
|
133
|
+
The settings title shows the installed version and a **Check for updates** button. When a newer release is available, **Update automatically** runs only when the DSH CLI or Desktop update service is available; otherwise, the dialog provides a profile-specific manual command to copy and run.
|
|
134
|
+
|
|
131
135
|
## Usage
|
|
132
136
|
|
|
133
137
|
1. Open the sidebar session menu and choose **Archive session**.
|
|
134
|
-
2. Open **Settings → Archived** to inspect sessions by workspace.
|
|
138
|
+
2. Open **Settings → Archived sessions** to inspect sessions by workspace.
|
|
135
139
|
3. Search by title, sort by update time, creation time, or title, or filter the list by project.
|
|
136
140
|
4. Select **Unarchive** to restore one session, or select **Restore all** in the page header.
|
|
137
141
|
5. Open a project heading's action menu to restore or delete all of that project's archived chats.
|
package/README.zh-CN.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
- 在侧栏会话菜单中选择「归档会话」。
|
|
25
25
|
- 在工作区侧栏操作菜单中一键归档该工作区全部未归档聊天。
|
|
26
|
-
- 在「设置 →
|
|
26
|
+
- 在「设置 → 归档会话」按工作区查看归档会话,并支持搜索、按更新时间/创建时间/标题排序和按项目筛选。
|
|
27
27
|
- 安全取消归档,将会话恢复到原工作区位置。
|
|
28
28
|
- 在项目分组中批量恢复或永久删除该项目的全部已归档聊天。
|
|
29
29
|
- 在页面顶部一键恢复全部已归档聊天。
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|

|
|
40
40
|
|
|
41
|
-
在「设置 →
|
|
41
|
+
在「设置 → 归档会话」中搜索、排序、按项目筛选、取消归档或永久删除:
|
|
42
42
|
|
|
43
43
|

|
|
44
44
|
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
| Codex | 把上面其中一句发给 Codex,让它在本机执行安装。 |
|
|
86
86
|
| WorkBuddy | 把上面其中一句发给 WorkBuddy;源码安装也可同时粘贴仓库地址 `https://github.com/MichengAI/dsh-archive-manager`。 |
|
|
87
87
|
|
|
88
|
-
Codex 和 WorkBuddy 只负责代装;装好后仍要打开 DSH Web 使用「设置 →
|
|
88
|
+
Codex 和 WorkBuddy 只负责代装;装好后仍要打开 DSH Web 使用「设置 → 归档会话」。
|
|
89
89
|
|
|
90
90
|
也可以自己执行同一条 npm 命令:
|
|
91
91
|
|
|
@@ -128,10 +128,14 @@ dsh --profile web --dump-config
|
|
|
128
128
|
|
|
129
129
|
完成后重启 DSH Web 并硬刷新浏览器。`dsh plugin ... add .` 会读取当前目录的包信息和 `cordis.patch.yml`;不要改为直接复制 `lib` 目录。
|
|
130
130
|
|
|
131
|
+
## 在线更新
|
|
132
|
+
|
|
133
|
+
设置标题会显示当前版本和“检查更新”按钮。发现新版后,只有检测到 DSH CLI 或 Desktop 更新服务时才可使用“自动更新”;其他环境会在弹窗中提供可复制、与当前 Profile 对应的手工更新命令。
|
|
134
|
+
|
|
131
135
|
## 使用
|
|
132
136
|
|
|
133
137
|
1. 在侧栏右键或打开会话菜单,选择「归档会话」。
|
|
134
|
-
2. 打开「设置 →
|
|
138
|
+
2. 打开「设置 → 归档会话」,按工作区查看归档会话。
|
|
135
139
|
3. 按标题搜索,按更新时间、创建时间或标题排序,或按项目筛选列表。
|
|
136
140
|
4. 点击「取消归档」恢复单个会话,或在顶部点击「全部恢复」。
|
|
137
141
|
5. 打开项目标题右侧菜单,可恢复或删除该项目的全部已归档聊天。
|
package/lib/client.js
CHANGED
|
@@ -1,4 +1,295 @@
|
|
|
1
1
|
(() => {
|
|
2
|
+
// src/plugin-update-ui.js
|
|
3
|
+
var UPDATE_HEADER = "x-michengai-plugin-update";
|
|
4
|
+
var STYLE_ID = "michengai-plugin-update-ui";
|
|
5
|
+
var CSS = `
|
|
6
|
+
.mpi-version{margin-left:8px;color:var(--dsw-alias-label-tertiary,#9da1aa);font-family:inherit;font-size:12px;font-weight:500;line-height:18px;letter-spacing:0;white-space:nowrap;vertical-align:baseline}.mpi-check{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:28px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2,#4b4d52);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary,#b8bbc2);font:inherit;font-size:12px;font-weight:500;line-height:18px;white-space:nowrap;cursor:pointer}.mpi-check:hover{background:var(--dsw-alias-interactive-bg-hover,#3a3b3f);color:var(--dsw-alias-label-primary,#fff)}.mpi-check:focus-visible,.mpi-action:focus-visible,.mpi-dialog-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4f8cff);outline-offset:2px}.mpi-icon{display:inline-flex;flex:0 0 auto;width:16px;height:16px;align-items:center;justify-content:center;pointer-events:none}.mpi-icon svg{display:block;width:16px;height:16px}
|
|
7
|
+
.mpi-overlay{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.mpi-dialog{position:relative;box-sizing:border-box;width:min(680px,100%);max-height:calc(100vh - 48px);overflow:auto;border:1px solid var(--dsw-alias-border-l2,#4b4d52);border-radius:14px;padding:22px;background:var(--dsw-alias-bg-layer-2,var(--dsw-specific-menu,#202124));color:var(--dsw-alias-label-primary,#fff);box-shadow:var(--dsw-shadow-lv3,0 16px 48px rgba(0,0,0,.24));font-family:inherit}.mpi-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.mpi-dialog h2{margin:0;font-size:18px;line-height:26px}.mpi-dialog-close{display:inline-flex;flex:0 0 28px;width:28px;height:28px;align-items:center;justify-content:center;padding:0;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary,#b8bbc2);cursor:pointer}.mpi-dialog-close:hover{background:var(--dsw-alias-interactive-bg-hover,#3a3b3f);color:var(--dsw-alias-label-primary,#fff)}.mpi-intro{margin:8px 0 18px;color:var(--dsw-alias-label-secondary,#c2c4ca);font-size:13px;line-height:20px}.mpi-meta{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:8px 18px;margin:0 0 16px;font-size:12px;line-height:18px}.mpi-meta dt{color:var(--dsw-alias-label-secondary,#c2c4ca)}.mpi-meta dd{margin:0;font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.mpi-status{margin:0 0 18px;border-radius:7px;padding:12px 14px;background:var(--dsw-alias-bg-layer-3,var(--dsw-specific-menu-item-hover,#252527));font-size:13px;font-weight:600;line-height:20px}.mpi-status[data-kind=error]{color:var(--dsw-alias-state-error-primary,#ff6464)}.mpi-status[data-kind=success]{color:var(--dsw-alias-state-success-primary,#36d67a)}.mpi-manual{border-top:1px solid var(--dsw-alias-border-l2,#4b4d52);padding-top:16px}.mpi-manual h3{margin:0 0 6px;font-size:14px;line-height:20px}.mpi-manual p{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#c2c4ca);font-size:12px;line-height:18px}.mpi-command{display:flex;align-items:center;gap:8px;border:1px solid var(--dsw-alias-border-l2,#4b4d52);border-radius:7px;padding:10px;background:var(--dsw-alias-bg-layer-3,var(--dsw-specific-menu-item-hover,#252527))}.mpi-command code{min-width:0;flex:1;overflow:auto;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:18px;white-space:nowrap}.mpi-actions{display:flex;align-items:center;justify-content:flex-end;gap:12px;margin-top:18px}.mpi-actions-group{display:flex;gap:8px}.mpi-action{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;border:1px solid var(--dsw-alias-border-l2,#4b4d52);border-radius:7px;padding:6px 10px;background:transparent;color:inherit;font:inherit;font-size:12px;cursor:pointer}.mpi-action:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#414247)}.mpi-action:disabled{cursor:not-allowed;opacity:.55}.mpi-primary{border-color:var(--dsw-alias-state-business-primary,#4f8cff);background:var(--dsw-alias-state-business-primary,#4f8cff);color:#fff}.mpi-progress{height:4px;margin-top:10px;overflow:hidden;border-radius:99px;background:var(--dsw-alias-border-l2,#4b4d52)}.mpi-progress::after{display:block;width:32%;height:100%;background:var(--dsw-alias-state-business-primary,#4f8cff);content:'';animation:mpi-wave 1.15s ease-in-out infinite}@keyframes mpi-wave{from{transform:translateX(-110%)}to{transform:translateX(330%)}}@media(max-width:560px){.mpi-overlay{padding:10px}.mpi-dialog{max-height:calc(100vh - 20px);padding:16px}.mpi-actions{align-items:stretch}.mpi-actions-group{justify-content:flex-end;flex-wrap:wrap}.mpi-meta{grid-template-columns:1fr;gap:2px}.mpi-meta dd{margin-bottom:6px}}
|
|
8
|
+
`;
|
|
9
|
+
var ZH = {
|
|
10
|
+
check: "\u68C0\u67E5\u66F4\u65B0",
|
|
11
|
+
update: "\u66F4\u65B0",
|
|
12
|
+
close: "\u5173\u95ED",
|
|
13
|
+
recheck: "\u91CD\u65B0\u68C0\u67E5",
|
|
14
|
+
auto: "\u81EA\u52A8\u66F4\u65B0",
|
|
15
|
+
updating: "\u6B63\u5728\u66F4\u65B0\u2026",
|
|
16
|
+
copy: "\u590D\u5236\u547D\u4EE4",
|
|
17
|
+
copied: "\u5DF2\u590D\u5236",
|
|
18
|
+
copyFailed: "\u590D\u5236\u5931\u8D25",
|
|
19
|
+
checking: "\u6B63\u5728\u68C0\u67E5\u66F4\u65B0\u2026",
|
|
20
|
+
latest: "\u5DF2\u662F\u6700\u65B0\u7248\u672C",
|
|
21
|
+
found: "\u53D1\u73B0\u65B0\u7248\u672C",
|
|
22
|
+
failed: "\u68C0\u67E5\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
|
|
23
|
+
current: "\u8FD0\u884C\u7248\u672C",
|
|
24
|
+
latestLabel: "\u6700\u65B0\u7248\u672C",
|
|
25
|
+
profile: "\u76EE\u6807 profile",
|
|
26
|
+
unknown: "\u672A\u77E5",
|
|
27
|
+
manual: "\u624B\u5DE5\u66F4\u65B0",
|
|
28
|
+
manualHint: "\u81EA\u52A8\u66F4\u65B0\u5931\u8D25\u65F6\uFF0C\u53EF\u5728\u5F53\u524D DSH \u7EC8\u7AEF\u6267\u884C\u4EE5\u4E0B\u547D\u4EE4\uFF0C\u5B8C\u6210\u540E\u91CD\u542F DSH Web\u3002",
|
|
29
|
+
intro: "\u4EC5\u68C0\u67E5\u5E76\u66F4\u65B0\u5F53\u524D\u63D2\u4EF6\uFF0C\u4E0D\u4F1A\u8054\u52A8\u5B89\u88C5\u5176\u4ED6\u63D2\u4EF6\u3002",
|
|
30
|
+
restart: "\u66F4\u65B0\u5B8C\u6210\uFF0C\u8BF7\u91CD\u542F DSH Web\u3002",
|
|
31
|
+
restarting: "\u66F4\u65B0\u5B8C\u6210\uFF0C\u6B63\u5728\u91CD\u542F DSH Desktop\u2026",
|
|
32
|
+
unavailable: "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u81EA\u52A8\u66F4\u65B0\uFF0C\u8BF7\u4F7F\u7528\u624B\u5DE5\u66F4\u65B0\u547D\u4EE4\u3002"
|
|
33
|
+
};
|
|
34
|
+
var EN = {
|
|
35
|
+
check: "Check for updates",
|
|
36
|
+
update: "Update",
|
|
37
|
+
close: "Close",
|
|
38
|
+
recheck: "Check again",
|
|
39
|
+
auto: "Update automatically",
|
|
40
|
+
updating: "Updating\u2026",
|
|
41
|
+
copy: "Copy command",
|
|
42
|
+
copied: "Copied",
|
|
43
|
+
copyFailed: "Copy failed",
|
|
44
|
+
checking: "Checking for updates\u2026",
|
|
45
|
+
latest: "You are up to date",
|
|
46
|
+
found: "New version available",
|
|
47
|
+
failed: "Could not check for updates. Try again later.",
|
|
48
|
+
current: "Running version",
|
|
49
|
+
latestLabel: "Latest version",
|
|
50
|
+
profile: "Target profile",
|
|
51
|
+
unknown: "Unknown",
|
|
52
|
+
manual: "Manual update",
|
|
53
|
+
manualHint: "If automatic update fails, run this command in the current DSH terminal, then restart DSH Web.",
|
|
54
|
+
intro: "Only this plugin is checked and updated. Other plugins are not changed.",
|
|
55
|
+
restart: "Update complete. Restart DSH Web.",
|
|
56
|
+
restarting: "Update complete. Restarting DSH Desktop\u2026",
|
|
57
|
+
unavailable: "Automatic update is unavailable. Use the manual command."
|
|
58
|
+
};
|
|
59
|
+
function strings() {
|
|
60
|
+
const lang = document.documentElement.lang.toLowerCase();
|
|
61
|
+
const settings = document.querySelector('[role="dialog"]')?.textContent ?? "";
|
|
62
|
+
return lang.startsWith("en") || settings.includes("Settings") && !settings.includes("\u8BBE\u7F6E") ? EN : ZH;
|
|
63
|
+
}
|
|
64
|
+
function ensureStyle() {
|
|
65
|
+
if (document.getElementById(STYLE_ID) !== null) return;
|
|
66
|
+
const style = document.createElement("style");
|
|
67
|
+
style.id = STYLE_ID;
|
|
68
|
+
style.textContent = CSS;
|
|
69
|
+
(document.head ?? document.documentElement).append(style);
|
|
70
|
+
}
|
|
71
|
+
function validPayload(value) {
|
|
72
|
+
if (value === null || typeof value !== "object") return false;
|
|
73
|
+
const item = value;
|
|
74
|
+
return typeof item.packageName === "string" && typeof item.currentVersion === "string" && typeof item.updateAvailable === "boolean" && typeof item.profileName === "string" && typeof item.canAutoUpdate === "boolean" && typeof item.latestCheckFailed === "boolean" && (item.latestVersion === void 0 || typeof item.latestVersion === "string");
|
|
75
|
+
}
|
|
76
|
+
async function requestStatus(endpoint, method, signal) {
|
|
77
|
+
const signalOption = signal === void 0 ? {} : { signal };
|
|
78
|
+
const response = await fetch(endpoint, method === "GET" ? { cache: "no-store", ...signalOption } : {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "content-type": "application/json", [UPDATE_HEADER]: "1" },
|
|
81
|
+
body: "{}",
|
|
82
|
+
...signalOption
|
|
83
|
+
});
|
|
84
|
+
const value = await response.json();
|
|
85
|
+
if (!response.ok || !validPayload(value)) throw new Error(typeof value.error === "string" ? value.error : strings().failed);
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function manualPluginUpdateCommand(profileName, packageName, version) {
|
|
89
|
+
const profile = profileName.trim() === "" ? "" : ` --profile ${profileName.trim()}`;
|
|
90
|
+
return `dsh plugin${profile} add ${packageName}@${version} --registry=https://registry.npmjs.org/`;
|
|
91
|
+
}
|
|
92
|
+
function handlePluginUpdateEscape(event, close) {
|
|
93
|
+
if (event.key !== "Escape") return false;
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
event.stopPropagation();
|
|
96
|
+
event.stopImmediatePropagation();
|
|
97
|
+
close();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
function observePluginUpdate(options) {
|
|
101
|
+
if (typeof document === "undefined" || document.body === null) return () => {
|
|
102
|
+
};
|
|
103
|
+
ensureStyle();
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
let payload;
|
|
106
|
+
let overlay;
|
|
107
|
+
let frame;
|
|
108
|
+
const setButtonContent = (button, label, iconName) => {
|
|
109
|
+
const icon = options.createIcon(iconName);
|
|
110
|
+
icon.classList.add("mpi-icon");
|
|
111
|
+
icon.setAttribute("aria-hidden", "true");
|
|
112
|
+
const text = document.createElement("span");
|
|
113
|
+
text.dataset.mpiLabel = "";
|
|
114
|
+
text.textContent = label;
|
|
115
|
+
button.replaceChildren(icon, text);
|
|
116
|
+
};
|
|
117
|
+
const setButtonLabel = (button, label) => {
|
|
118
|
+
const text = button.querySelector("[data-mpi-label]");
|
|
119
|
+
if (text === null) button.textContent = label;
|
|
120
|
+
else text.textContent = label;
|
|
121
|
+
};
|
|
122
|
+
const applyControls = () => {
|
|
123
|
+
const row = document.querySelector(options.titleRowSelector);
|
|
124
|
+
if (row === null) return;
|
|
125
|
+
const heading = row.querySelector("h1,h2");
|
|
126
|
+
if (heading !== null && payload !== void 0) {
|
|
127
|
+
let version = heading.querySelector(`.mpi-version[data-package="${options.packageName}"]`);
|
|
128
|
+
if (version === null) {
|
|
129
|
+
version = document.createElement("span");
|
|
130
|
+
version.className = "mpi-version";
|
|
131
|
+
version.dataset.package = options.packageName;
|
|
132
|
+
heading.append(version);
|
|
133
|
+
}
|
|
134
|
+
const versionLabel = `v${payload.currentVersion}`;
|
|
135
|
+
if (version.textContent !== versionLabel) version.textContent = versionLabel;
|
|
136
|
+
}
|
|
137
|
+
const links = row.querySelector(options.linksSelector);
|
|
138
|
+
if (links === null || links.querySelector(`[data-mpi-check="${options.packageName}"]`) !== null) return;
|
|
139
|
+
const button = document.createElement("button");
|
|
140
|
+
button.type = "button";
|
|
141
|
+
button.className = "mpi-check";
|
|
142
|
+
button.dataset.mpiCheck = options.packageName;
|
|
143
|
+
setButtonContent(button, strings().check, "refresh");
|
|
144
|
+
button.addEventListener("click", openDialog);
|
|
145
|
+
links.append(button);
|
|
146
|
+
};
|
|
147
|
+
const load = async () => {
|
|
148
|
+
payload = await requestStatus(options.endpoint, "GET", controller.signal);
|
|
149
|
+
applyControls();
|
|
150
|
+
return payload;
|
|
151
|
+
};
|
|
152
|
+
const closeDialog = () => {
|
|
153
|
+
overlay?.remove();
|
|
154
|
+
overlay = void 0;
|
|
155
|
+
};
|
|
156
|
+
function openDialog() {
|
|
157
|
+
closeDialog();
|
|
158
|
+
const text = strings();
|
|
159
|
+
overlay = document.createElement("div");
|
|
160
|
+
overlay.className = "mpi-overlay";
|
|
161
|
+
const dialog = document.createElement("section");
|
|
162
|
+
dialog.className = "mpi-dialog";
|
|
163
|
+
dialog.setAttribute("role", "dialog");
|
|
164
|
+
dialog.setAttribute("aria-modal", "true");
|
|
165
|
+
dialog.innerHTML = `<header class="mpi-head"><h2></h2><button type="button" class="mpi-dialog-close" data-action="close"></button></header><p class="mpi-intro"></p><dl class="mpi-meta"><dt></dt><dd data-role="current"></dd><dt></dt><dd data-role="latest"></dd><dt></dt><dd data-role="profile"></dd></dl><div class="mpi-status" role="status"></div><div class="mpi-progress" hidden></div><section class="mpi-manual"><h3></h3><p></p><div class="mpi-command"><code></code><button type="button" class="mpi-action" data-action="copy"></button></div></section><footer class="mpi-actions"><div class="mpi-actions-group"><button type="button" class="mpi-action" data-action="check"></button><button type="button" class="mpi-action mpi-primary" data-action="update"></button></div></footer>`;
|
|
166
|
+
const name = document.documentElement.lang.toLowerCase().startsWith("en") ? options.enName : options.zhName;
|
|
167
|
+
dialog.querySelector("h2").textContent = `${name} ${text.update}`;
|
|
168
|
+
dialog.querySelector(".mpi-intro").textContent = text.intro;
|
|
169
|
+
const terms = dialog.querySelectorAll("dt");
|
|
170
|
+
terms[0].textContent = text.current;
|
|
171
|
+
terms[1].textContent = text.latestLabel;
|
|
172
|
+
terms[2].textContent = text.profile;
|
|
173
|
+
dialog.querySelector(".mpi-manual h3").textContent = text.manual;
|
|
174
|
+
dialog.querySelector(".mpi-manual p").textContent = text.manualHint;
|
|
175
|
+
const status = dialog.querySelector(".mpi-status");
|
|
176
|
+
const progress = dialog.querySelector(".mpi-progress");
|
|
177
|
+
const command = dialog.querySelector(".mpi-command code");
|
|
178
|
+
const close = dialog.querySelector("[data-action=close]");
|
|
179
|
+
const check = dialog.querySelector("[data-action=check]");
|
|
180
|
+
const update = dialog.querySelector("[data-action=update]");
|
|
181
|
+
const copy = dialog.querySelector("[data-action=copy]");
|
|
182
|
+
setButtonContent(close, text.close, "close");
|
|
183
|
+
close.querySelector("[data-mpi-label]")?.remove();
|
|
184
|
+
close.setAttribute("aria-label", text.close);
|
|
185
|
+
close.title = text.close;
|
|
186
|
+
setButtonContent(check, text.recheck, "refresh");
|
|
187
|
+
setButtonContent(update, text.auto, "download");
|
|
188
|
+
setButtonContent(copy, text.copy, "copy");
|
|
189
|
+
let busy = false;
|
|
190
|
+
const setMessage = (message, kind = "") => {
|
|
191
|
+
status.textContent = message;
|
|
192
|
+
status.dataset.kind = kind;
|
|
193
|
+
};
|
|
194
|
+
const setBusy = (value) => {
|
|
195
|
+
busy = value;
|
|
196
|
+
check.disabled = value;
|
|
197
|
+
copy.disabled = value;
|
|
198
|
+
update.disabled = value || payload?.canAutoUpdate !== true || payload.updateAvailable !== true;
|
|
199
|
+
progress.hidden = !value;
|
|
200
|
+
};
|
|
201
|
+
const render = () => {
|
|
202
|
+
dialog.querySelector("[data-role=current]").textContent = payload === void 0 ? text.unknown : `v${payload.currentVersion}`;
|
|
203
|
+
dialog.querySelector("[data-role=latest]").textContent = payload?.latestVersion === void 0 ? text.unknown : `v${payload.latestVersion}`;
|
|
204
|
+
dialog.querySelector("[data-role=profile]").textContent = payload?.profileName ?? text.unknown;
|
|
205
|
+
command.textContent = manualPluginUpdateCommand(payload?.profileName ?? "", options.packageName, payload?.latestVersion ?? "latest");
|
|
206
|
+
update.disabled = busy || payload?.canAutoUpdate !== true || payload.updateAvailable !== true;
|
|
207
|
+
if (payload === void 0) setMessage(text.checking);
|
|
208
|
+
else if (payload.latestCheckFailed) setMessage(text.failed, "error");
|
|
209
|
+
else if (payload.updateAvailable) setMessage(`${text.found}: v${payload.latestVersion ?? text.unknown}`);
|
|
210
|
+
else setMessage(text.latest, "success");
|
|
211
|
+
if (payload !== void 0 && !payload.canAutoUpdate && payload.updateAvailable) setMessage(text.unavailable);
|
|
212
|
+
};
|
|
213
|
+
const checkNow = async () => {
|
|
214
|
+
if (busy) return;
|
|
215
|
+
setBusy(true);
|
|
216
|
+
setMessage(text.checking);
|
|
217
|
+
try {
|
|
218
|
+
await load();
|
|
219
|
+
setBusy(false);
|
|
220
|
+
render();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
setBusy(false);
|
|
223
|
+
setMessage(error instanceof Error ? error.message : text.failed, "error");
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const updateNow = async () => {
|
|
227
|
+
if (busy) return;
|
|
228
|
+
setBusy(true);
|
|
229
|
+
setButtonLabel(update, text.updating);
|
|
230
|
+
setMessage(text.updating);
|
|
231
|
+
try {
|
|
232
|
+
payload = await requestStatus(options.endpoint, "POST", controller.signal);
|
|
233
|
+
applyControls();
|
|
234
|
+
render();
|
|
235
|
+
setMessage(payload.autoReload === true ? text.restarting : text.restart, "success");
|
|
236
|
+
} catch (error) {
|
|
237
|
+
setMessage(error instanceof Error ? error.message : text.failed, "error");
|
|
238
|
+
} finally {
|
|
239
|
+
setButtonLabel(update, text.auto);
|
|
240
|
+
setBusy(false);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
close.addEventListener("click", closeDialog);
|
|
244
|
+
check.addEventListener("click", () => {
|
|
245
|
+
void checkNow();
|
|
246
|
+
});
|
|
247
|
+
update.addEventListener("click", () => {
|
|
248
|
+
void updateNow();
|
|
249
|
+
});
|
|
250
|
+
copy.addEventListener("click", () => {
|
|
251
|
+
void navigator.clipboard?.writeText(command.textContent ?? "").then(() => {
|
|
252
|
+
setButtonLabel(copy, text.copied);
|
|
253
|
+
setTimeout(() => {
|
|
254
|
+
setButtonLabel(copy, text.copy);
|
|
255
|
+
}, 1400);
|
|
256
|
+
}).catch(() => {
|
|
257
|
+
setButtonLabel(copy, text.copyFailed);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
overlay.addEventListener("click", (event) => {
|
|
261
|
+
if (event.target === overlay) closeDialog();
|
|
262
|
+
});
|
|
263
|
+
overlay.addEventListener("keydown", (event) => {
|
|
264
|
+
handlePluginUpdateEscape(event, closeDialog);
|
|
265
|
+
}, true);
|
|
266
|
+
overlay.append(dialog);
|
|
267
|
+
document.body.append(overlay);
|
|
268
|
+
render();
|
|
269
|
+
close.focus();
|
|
270
|
+
void checkNow();
|
|
271
|
+
}
|
|
272
|
+
const observer = new MutationObserver(() => {
|
|
273
|
+
if (frame !== void 0) return;
|
|
274
|
+
frame = window.requestAnimationFrame(() => {
|
|
275
|
+
frame = void 0;
|
|
276
|
+
applyControls();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
280
|
+
applyControls();
|
|
281
|
+
void load().catch(() => {
|
|
282
|
+
});
|
|
283
|
+
return () => {
|
|
284
|
+
controller.abort();
|
|
285
|
+
observer.disconnect();
|
|
286
|
+
closeDialog();
|
|
287
|
+
if (frame !== void 0) window.cancelAnimationFrame(frame);
|
|
288
|
+
document.querySelectorAll(`[data-mpi-check="${options.packageName}"],.mpi-version[data-package="${options.packageName}"]`).forEach((node) => node.remove());
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/client.js
|
|
2
293
|
window.__ModuleLoader__.load({
|
|
3
294
|
id: "@michengai/dsh-archive-manager",
|
|
4
295
|
factory: (require2) => {
|
|
@@ -16,6 +307,32 @@
|
|
|
16
307
|
let react_jsx_runtime = require2("react/jsx-runtime");
|
|
17
308
|
let react = require2("react");
|
|
18
309
|
let _deepseek_ai_dsh_client_ui_primitives = require2("@deepseek-ai/dsh-client-ui-primitives");
|
|
310
|
+
const UPDATE_ICON_PATHS = {
|
|
311
|
+
refresh: ["M13.5 5.5V2.5m0 0h-3m3 0-2.1 2.1A5.5 5.5 0 1 0 13.2 12"],
|
|
312
|
+
download: ["M8 2v8m0 0 3-3m-3 3-3-3M3 13v2h10v-2"],
|
|
313
|
+
copy: ["M5 5h8v8H5z", "M3 3h8"],
|
|
314
|
+
close: ["m4 4 8 8M12 4 4 12"]
|
|
315
|
+
};
|
|
316
|
+
function createPluginUpdateIcon(name) {
|
|
317
|
+
const element = document.createElement("span");
|
|
318
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
319
|
+
svg.setAttribute("viewBox", "0 0 16 16");
|
|
320
|
+
svg.setAttribute("width", "16");
|
|
321
|
+
svg.setAttribute("height", "16");
|
|
322
|
+
svg.setAttribute("fill", "none");
|
|
323
|
+
svg.setAttribute("stroke", "currentColor");
|
|
324
|
+
svg.setAttribute("stroke-width", "1.5");
|
|
325
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
326
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
327
|
+
svg.setAttribute("aria-hidden", "true");
|
|
328
|
+
for (const d of UPDATE_ICON_PATHS[name]) {
|
|
329
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
330
|
+
path.setAttribute("d", d);
|
|
331
|
+
svg.append(path);
|
|
332
|
+
}
|
|
333
|
+
element.append(svg);
|
|
334
|
+
return element;
|
|
335
|
+
}
|
|
19
336
|
const sessionIdSchema = {
|
|
20
337
|
parse(value) {
|
|
21
338
|
if (typeof value !== "string" || value.length === 0) throw new TypeError(`sessionId must be a non-empty string, got ${String(value)}`);
|
|
@@ -3180,9 +3497,9 @@
|
|
|
3180
3497
|
"menu.addWorkspace": "\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026",
|
|
3181
3498
|
"menu.unarchive": "\u53D6\u6D88\u5F52\u6863",
|
|
3182
3499
|
"menu.deleteSession": "\u5220\u9664\u4F1A\u8BDD",
|
|
3183
|
-
"archived.badge": "\
|
|
3500
|
+
"archived.badge": "\u5F52\u6863\u4F1A\u8BDD",
|
|
3184
3501
|
"archived.notOpenable": "\u5DF2\u5F52\u6863\uFF0C\u53D6\u6D88\u5F52\u6863\u540E\u53EF\u7EE7\u7EED\u5BF9\u8BDD",
|
|
3185
|
-
"archives.title": "\
|
|
3502
|
+
"archives.title": "\u5F52\u6863\u4F1A\u8BDD",
|
|
3186
3503
|
"archives.description": "\u7BA1\u7406\u5DF2\u5F52\u6863\u7684\u4F1A\u8BDD\u3002",
|
|
3187
3504
|
"archives.viewProject": "GitHub",
|
|
3188
3505
|
"archives.feedback": "\u95EE\u9898\u53CD\u9988",
|
|
@@ -3310,9 +3627,9 @@
|
|
|
3310
3627
|
"menu.addWorkspace": "Add workspace\u2026",
|
|
3311
3628
|
"menu.unarchive": "Unarchive",
|
|
3312
3629
|
"menu.deleteSession": "Delete session",
|
|
3313
|
-
"archived.badge": "Archived",
|
|
3630
|
+
"archived.badge": "Archived sessions",
|
|
3314
3631
|
"archived.notOpenable": "This session is archived. Unarchive it to continue the conversation.",
|
|
3315
|
-
"archives.title": "Archived
|
|
3632
|
+
"archives.title": "Archived sessions",
|
|
3316
3633
|
"archives.description": "Manage archived sessions.",
|
|
3317
3634
|
"archives.viewProject": "GitHub",
|
|
3318
3635
|
"archives.feedback": "Issues",
|
|
@@ -3589,6 +3906,15 @@
|
|
|
3589
3906
|
zh,
|
|
3590
3907
|
en
|
|
3591
3908
|
}), "dsh-archive-manager: dictionaries");
|
|
3909
|
+
ctx.effect(() => observePluginUpdate({
|
|
3910
|
+
endpoint: "/api/michengai/dsh-archive-manager/update",
|
|
3911
|
+
packageName: "@michengai/dsh-archive-manager",
|
|
3912
|
+
titleRowSelector: ".dsham_settingsTitleRow",
|
|
3913
|
+
linksSelector: ".dsham_settingsLinks",
|
|
3914
|
+
zhName: "\u5F52\u6863\u4F1A\u8BDD",
|
|
3915
|
+
enName: "Archived sessions",
|
|
3916
|
+
createIcon: createPluginUpdateIcon
|
|
3917
|
+
}), "dsh-archive-manager: plugin update ui");
|
|
3592
3918
|
const uiWorkspaceAt = () => ctx.get("uiWorkspace");
|
|
3593
3919
|
const searchSessions = async (query, signal) => {
|
|
3594
3920
|
const result = await ctx.sessions.search(query, signal);
|
|
@@ -3626,6 +3952,7 @@
|
|
|
3626
3952
|
if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
|
|
3627
3953
|
const result = await registry.deleteSession(sessionId);
|
|
3628
3954
|
if (!result.ok) throw new Error(result.error.message);
|
|
3955
|
+
await refreshSessionList();
|
|
3629
3956
|
};
|
|
3630
3957
|
const unarchiveSessions = async (target) => {
|
|
3631
3958
|
const registry = ctx.get("remote.workspaceRegistry");
|
|
@@ -3640,6 +3967,7 @@
|
|
|
3640
3967
|
if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
|
|
3641
3968
|
const result = await registry.deleteArchivedSessions(target);
|
|
3642
3969
|
if (!result.ok) throw new Error(result.error.message);
|
|
3970
|
+
await refreshSessionList();
|
|
3643
3971
|
return result.value;
|
|
3644
3972
|
};
|
|
3645
3973
|
const archivedSessionMetadata = async () => {
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { registerPluginUpdater } from "./plugin-updater.js";
|
|
2
|
+
const inject = ["webServer"];
|
|
3
|
+
function apply(ctx) {
|
|
4
|
+
return registerPluginUpdater(ctx, {
|
|
5
|
+
endpoint: "/api/michengai/dsh-archive-manager/update",
|
|
6
|
+
packageName: "@michengai/dsh-archive-manager",
|
|
7
|
+
manifestUrl: new URL("../package.json", import.meta.url)
|
|
8
|
+
});
|
|
2
9
|
}
|
|
3
10
|
export {
|
|
4
|
-
apply
|
|
11
|
+
apply,
|
|
12
|
+
inject
|
|
5
13
|
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
const PLUGIN_UPDATE_HEADER = "x-michengai-plugin-update";
|
|
8
|
+
const PLUGIN_UPDATE_IPC = "apply-plugin-updates";
|
|
9
|
+
function header(request, name) {
|
|
10
|
+
const value = request.headers?.[name];
|
|
11
|
+
return Array.isArray(value) ? value[0] : value;
|
|
12
|
+
}
|
|
13
|
+
function isLoopbackAddress(value) {
|
|
14
|
+
const address = value?.toLowerCase().replace(/^\[|\]$/g, "");
|
|
15
|
+
return address === "localhost" || address === "localhost." || address === "::1" || address?.startsWith("127.") === true || address?.startsWith("::ffff:127.") === true;
|
|
16
|
+
}
|
|
17
|
+
function isTrustedUpdateRequest(request) {
|
|
18
|
+
if (header(request, PLUGIN_UPDATE_HEADER) !== "1") return false;
|
|
19
|
+
if (!isLoopbackAddress(request.socket?.remoteAddress)) return false;
|
|
20
|
+
const site = header(request, "sec-fetch-site");
|
|
21
|
+
if (site !== void 0 && site !== "same-origin") return false;
|
|
22
|
+
const origin = header(request, "origin");
|
|
23
|
+
const host = header(request, "host");
|
|
24
|
+
if (origin === void 0 || host === void 0) return false;
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(origin);
|
|
27
|
+
return (url.protocol === "http:" || url.protocol === "https:") && isLoopbackAddress(url.hostname) && url.host === host;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function validProfileName(value) {
|
|
33
|
+
return typeof value === "string" && value !== "" && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") && !/[\0-\x1f\x7f]/.test(value);
|
|
34
|
+
}
|
|
35
|
+
function profileNameFromArgv(argv) {
|
|
36
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
37
|
+
if (argv[index] === "--profile") return argv[index + 1];
|
|
38
|
+
if (argv[index]?.startsWith("--profile=")) return argv[index].slice("--profile=".length);
|
|
39
|
+
}
|
|
40
|
+
return argv[2] === "web" ? "web" : void 0;
|
|
41
|
+
}
|
|
42
|
+
function isDshCliEntry(entry, manifest, packageRoot) {
|
|
43
|
+
if (typeof manifest !== "object" || manifest === null) return false;
|
|
44
|
+
if (manifest.name !== "@deepseek-ai/dsh") return false;
|
|
45
|
+
const bin = typeof manifest.bin === "string" ? manifest.bin : typeof manifest.bin === "object" && manifest.bin !== null ? manifest.bin.dsh : void 0;
|
|
46
|
+
return typeof bin === "string" && bin !== "" && !isAbsolute(bin) && resolve(packageRoot, bin) === resolve(entry);
|
|
47
|
+
}
|
|
48
|
+
function cliEntry() {
|
|
49
|
+
const value = process.argv[1];
|
|
50
|
+
if (value === void 0 || value === "") return void 0;
|
|
51
|
+
const entry = value.startsWith("file:") ? fileURLToPath(value) : resolve(process.cwd(), value);
|
|
52
|
+
if (!existsSync(entry)) return void 0;
|
|
53
|
+
for (let directory = dirname(entry); ; ) {
|
|
54
|
+
const manifestPath = resolve(directory, "package.json");
|
|
55
|
+
if (existsSync(manifestPath)) {
|
|
56
|
+
try {
|
|
57
|
+
if (isDshCliEntry(entry, JSON.parse(readFileSync(manifestPath, "utf8")), directory)) return entry;
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const parent = dirname(directory);
|
|
62
|
+
if (parent === directory) return void 0;
|
|
63
|
+
directory = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function runtime(ctx) {
|
|
67
|
+
const profiles = ctx.get?.("desktopProfiles");
|
|
68
|
+
const desktopPnpm = ctx.get?.("desktopPnpm");
|
|
69
|
+
if (profiles?.current !== void 0) {
|
|
70
|
+
const current = profiles.current;
|
|
71
|
+
if (!validProfileName(current.name) || typeof current.dir !== "string" || !isAbsolute(current.dir)) {
|
|
72
|
+
throw new Error("\u5F53\u524D Desktop Profile \u4FE1\u606F\u65E0\u6548\uFF0C\u8BF7\u91CD\u542F\u540E\u91CD\u8BD5\u3002");
|
|
73
|
+
}
|
|
74
|
+
return { profileName: current.name, profileDir: resolve(current.dir), ...typeof desktopPnpm?.runPlugin === "function" ? { desktopPnpm } : {} };
|
|
75
|
+
}
|
|
76
|
+
const profileDir = resolve(process.env.DSH_PROFILE_DIR ?? resolve(homedir(), ".dsh", "profiles", "web"));
|
|
77
|
+
const selected = profileNameFromArgv(process.argv);
|
|
78
|
+
const profileName = validProfileName(selected) ? selected : validProfileName(basename(profileDir)) ? basename(profileDir) : "web";
|
|
79
|
+
const entry = cliEntry();
|
|
80
|
+
return { profileName, profileDir, ...entry === void 0 ? {} : { cliEntry: entry } };
|
|
81
|
+
}
|
|
82
|
+
function parseSemver(value) {
|
|
83
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
|
|
84
|
+
if (match === null) return void 0;
|
|
85
|
+
return { core: [Number(match[1]), Number(match[2]), Number(match[3])], prerelease: match[4]?.split(".") ?? [] };
|
|
86
|
+
}
|
|
87
|
+
function isNewerVersion(currentValue, candidateValue) {
|
|
88
|
+
const current = parseSemver(currentValue);
|
|
89
|
+
const candidate = parseSemver(candidateValue);
|
|
90
|
+
if (current === void 0 || candidate === void 0) return false;
|
|
91
|
+
for (let index = 0; index < 3; index += 1) {
|
|
92
|
+
if (candidate.core[index] !== current.core[index]) return candidate.core[index] > current.core[index];
|
|
93
|
+
}
|
|
94
|
+
return comparePrerelease(candidate.prerelease, current.prerelease) > 0;
|
|
95
|
+
}
|
|
96
|
+
function comparePrerelease(left, right) {
|
|
97
|
+
if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1;
|
|
98
|
+
const length = Math.max(left.length, right.length);
|
|
99
|
+
for (let index = 0; index < length; index += 1) {
|
|
100
|
+
const a = left[index];
|
|
101
|
+
const b = right[index];
|
|
102
|
+
if (a === void 0 || b === void 0) return a === b ? 0 : a === void 0 ? -1 : 1;
|
|
103
|
+
if (a === b) continue;
|
|
104
|
+
const aNumeric = /^\d+$/.test(a);
|
|
105
|
+
const bNumeric = /^\d+$/.test(b);
|
|
106
|
+
if (aNumeric && bNumeric) {
|
|
107
|
+
const aNumber = BigInt(a);
|
|
108
|
+
const bNumber = BigInt(b);
|
|
109
|
+
if (aNumber !== bNumber) return aNumber > bNumber ? 1 : -1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
|
|
113
|
+
return a > b ? 1 : -1;
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
let latestCache;
|
|
118
|
+
async function latestVersion(packageName) {
|
|
119
|
+
if (latestCache?.packageName === packageName && Date.now() < latestCache.expiresAt) return latestCache.version;
|
|
120
|
+
try {
|
|
121
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { signal: AbortSignal.timeout(8e3) });
|
|
122
|
+
if (!response.ok) return void 0;
|
|
123
|
+
const value = await response.json();
|
|
124
|
+
if (typeof value.version !== "string" || value.version === "") return void 0;
|
|
125
|
+
latestCache = { packageName, version: value.version, expiresAt: Date.now() + 5 * 6e4 };
|
|
126
|
+
return value.version;
|
|
127
|
+
} catch {
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function currentVersion(manifestUrl) {
|
|
132
|
+
const value = JSON.parse(await readFile(manifestUrl, "utf8"));
|
|
133
|
+
if (typeof value.version !== "string" || value.version === "") throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u5F53\u524D\u63D2\u4EF6\u7248\u672C\u3002");
|
|
134
|
+
return value.version;
|
|
135
|
+
}
|
|
136
|
+
async function status(options, target) {
|
|
137
|
+
const current = await currentVersion(options.manifestUrl);
|
|
138
|
+
const latest = await latestVersion(options.packageName);
|
|
139
|
+
return {
|
|
140
|
+
packageName: options.packageName,
|
|
141
|
+
currentVersion: current,
|
|
142
|
+
...latest === void 0 ? {} : { latestVersion: latest },
|
|
143
|
+
latestCheckFailed: latest === void 0,
|
|
144
|
+
updateAvailable: latest !== void 0 && isNewerVersion(current, latest),
|
|
145
|
+
profileName: target.profileName,
|
|
146
|
+
canAutoUpdate: target.desktopPnpm !== void 0 || target.cliEntry !== void 0
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function runCliInstall(target, packageSpec) {
|
|
150
|
+
if (target.cliEntry === void 0) throw new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u81EA\u52A8\u66F4\u65B0\uFF0C\u8BF7\u4F7F\u7528\u624B\u5DE5\u66F4\u65B0\u547D\u4EE4\u3002");
|
|
151
|
+
await new Promise((resolvePromise, reject) => {
|
|
152
|
+
const child = spawn(process.execPath, [target.cliEntry, "plugin", "--profile", target.profileName, "add", "--config.minimumReleaseAge=0", packageSpec, "--registry=https://registry.npmjs.org/"], {
|
|
153
|
+
cwd: target.profileDir,
|
|
154
|
+
windowsHide: true,
|
|
155
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
156
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
157
|
+
});
|
|
158
|
+
let detail = "";
|
|
159
|
+
child.stdout?.on("data", (chunk) => {
|
|
160
|
+
detail = (detail + String(chunk)).slice(-4e3);
|
|
161
|
+
});
|
|
162
|
+
child.stderr?.on("data", (chunk) => {
|
|
163
|
+
detail = (detail + String(chunk)).slice(-4e3);
|
|
164
|
+
});
|
|
165
|
+
const timer = setTimeout(() => {
|
|
166
|
+
child.kill();
|
|
167
|
+
reject(new Error("\u66F4\u65B0\u8D85\u65F6\uFF0C\u8BF7\u6539\u7528\u624B\u5DE5\u66F4\u65B0\u3002"));
|
|
168
|
+
}, 10 * 6e4);
|
|
169
|
+
child.once("error", (error) => {
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
reject(error);
|
|
172
|
+
});
|
|
173
|
+
child.once("exit", (code) => {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
if (code === 0) resolvePromise();
|
|
176
|
+
else reject(new Error(detail.trim() || `\u66F4\u65B0\u8FDB\u7A0B\u9000\u51FA\u7801 ${String(code)}`));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async function install(target, packageSpec) {
|
|
181
|
+
if (target.desktopPnpm === void 0) return runCliInstall(target, packageSpec);
|
|
182
|
+
const handle = target.desktopPnpm.runPlugin(["add", "--config.minimumReleaseAge=0", packageSpec, "--registry=https://registry.npmjs.org/"], target.profileDir);
|
|
183
|
+
const result = await handle.done;
|
|
184
|
+
if (result.exitCode !== 0) throw new Error(`\u66F4\u65B0\u8FDB\u7A0B\u9000\u51FA\u7801 ${String(result.exitCode)}\u3002`);
|
|
185
|
+
}
|
|
186
|
+
function json(response, statusCode, value) {
|
|
187
|
+
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
188
|
+
response.end(JSON.stringify(value));
|
|
189
|
+
}
|
|
190
|
+
function publicError(error) {
|
|
191
|
+
const message = error instanceof Error ? error.message : "\u66F4\u65B0\u6682\u4E0D\u53EF\u7528\u3002";
|
|
192
|
+
return /[A-Za-z]:[\\/]|\/(?:home|root|Users|var|tmp)\//.test(message) ? "\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B\u670D\u52A1\u7AEF\u65E5\u5FD7\u3002" : message;
|
|
193
|
+
}
|
|
194
|
+
function registerPluginUpdater(ctx, options) {
|
|
195
|
+
const host = ctx;
|
|
196
|
+
let installing = false;
|
|
197
|
+
return host.webServer.register({
|
|
198
|
+
kind: "exact",
|
|
199
|
+
path: options.endpoint,
|
|
200
|
+
handler: async (request, response) => {
|
|
201
|
+
try {
|
|
202
|
+
const target = runtime(host);
|
|
203
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
204
|
+
const payload = await status(options, target);
|
|
205
|
+
response.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
206
|
+
response.end(request.method === "HEAD" ? void 0 : JSON.stringify(payload));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (request.method !== "POST") {
|
|
210
|
+
response.writeHead(405, { allow: "GET, HEAD, POST" });
|
|
211
|
+
response.end();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!isTrustedUpdateRequest(request)) {
|
|
215
|
+
json(response, 403, { error: "\u5DF2\u62D2\u7EDD\u975E\u672C\u673A\u540C\u6E90\u66F4\u65B0\u8BF7\u6C42\u3002" });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (installing) {
|
|
219
|
+
json(response, 409, { error: "\u5F53\u524D\u63D2\u4EF6\u6B63\u5728\u66F4\u65B0\uFF0C\u8BF7\u7A0D\u5019\u3002" });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const before = await status(options, target);
|
|
223
|
+
if (before.latestVersion === void 0) {
|
|
224
|
+
json(response, 503, { error: "\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\u6700\u65B0\u7248\u672C\u3002" });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (!before.updateAvailable) {
|
|
228
|
+
json(response, 200, before);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
installing = true;
|
|
232
|
+
try {
|
|
233
|
+
await install(target, `${options.packageName}@${before.latestVersion}`);
|
|
234
|
+
} finally {
|
|
235
|
+
installing = false;
|
|
236
|
+
}
|
|
237
|
+
const notifyParent = target.desktopPnpm === void 0 && typeof process.send === "function";
|
|
238
|
+
const autoReload = target.desktopPnpm !== void 0 || notifyParent;
|
|
239
|
+
json(response, 200, { ...before, updatedVersion: before.latestVersion, restartRequired: true, autoReload });
|
|
240
|
+
if (notifyParent) setTimeout(() => {
|
|
241
|
+
process.send?.(PLUGIN_UPDATE_IPC);
|
|
242
|
+
}, 150).unref?.();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
ctx.logger.warn(`plugin updater failed: ${String(error)}`);
|
|
245
|
+
json(response, 503, { error: publicError(error) });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
export {
|
|
251
|
+
PLUGIN_UPDATE_HEADER,
|
|
252
|
+
PLUGIN_UPDATE_IPC,
|
|
253
|
+
isDshCliEntry,
|
|
254
|
+
isNewerVersion,
|
|
255
|
+
isTrustedUpdateRequest,
|
|
256
|
+
registerPluginUpdater
|
|
257
|
+
};
|
package/lib/workspace.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
2
|
import {
|
|
4
3
|
WorkspaceRegistry
|
|
5
4
|
} from "@deepseek-ai/dsh-workspace";
|
|
@@ -484,6 +483,10 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
484
483
|
} else if (sessions !== void 0) await this.publishColdSessionRemoval(sessionId, sessions);
|
|
485
484
|
const projCache = this.ctx.get("sessionProjectionCache");
|
|
486
485
|
await projCache?.whenIdle?.();
|
|
486
|
+
if (projCache !== void 0) await projCache.delete(sessionId);
|
|
487
|
+
await this.deleteDescendants(sessionId);
|
|
488
|
+
await this.cleanSpill(sessionId);
|
|
489
|
+
await this.removeTranscriptDirectory(sessionId);
|
|
487
490
|
const state = this.requireState();
|
|
488
491
|
if (state.archivedSessionIds.includes(sessionId)) {
|
|
489
492
|
await this.setState({
|
|
@@ -492,10 +495,6 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
492
495
|
});
|
|
493
496
|
}
|
|
494
497
|
await this.removeFromWorkspaceAccounts(sessionId);
|
|
495
|
-
if (projCache !== void 0) await projCache.delete(sessionId);
|
|
496
|
-
await this.deleteDescendants(sessionId);
|
|
497
|
-
await this.cleanSpill(sessionId);
|
|
498
|
-
await this.removeTranscriptDirectory(sessionId);
|
|
499
498
|
this.forgetIndexedSession(sessionId);
|
|
500
499
|
if (deletedHeader !== void 0) this.deletedIdentities.set(sessionId, headerIdentity(deletedHeader));
|
|
501
500
|
return { deleted: true };
|
|
@@ -578,22 +577,21 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
578
577
|
this.ctx.logger.warn(`archive-manager: could not publish removal for stored session "${sessionId}": ${String(error)}`);
|
|
579
578
|
}
|
|
580
579
|
}
|
|
581
|
-
/**
|
|
580
|
+
/** 删除后端定位到的会话转录工件,绝不推导或删除其父目录。 */
|
|
582
581
|
async removeTranscriptDirectory(sessionId) {
|
|
583
582
|
const persistence = this.ctx.get("sessionPersistence");
|
|
584
583
|
if (persistence === void 0 || typeof persistence.locate !== "function") {
|
|
585
|
-
throw new Error(`cannot delete session "${sessionId}": the session persistence backend does not expose locate() to resolve its transcript
|
|
584
|
+
throw new Error(`cannot delete session "${sessionId}": the session persistence backend does not expose locate() to resolve its transcript artifact`);
|
|
586
585
|
}
|
|
587
586
|
const header = await this.readSessionHeader(sessionId);
|
|
588
587
|
const location = persistence.locate(header);
|
|
589
588
|
if (location === void 0 || typeof location.path !== "string") {
|
|
590
|
-
throw new Error(`cannot delete session "${sessionId}": the session persistence backend could not resolve its transcript
|
|
589
|
+
throw new Error(`cannot delete session "${sessionId}": the session persistence backend could not resolve its transcript artifact`);
|
|
591
590
|
}
|
|
592
|
-
const transcriptDir = dirname(location.path);
|
|
593
591
|
try {
|
|
594
|
-
await rm(
|
|
592
|
+
await rm(location.path, { recursive: true, force: true });
|
|
595
593
|
} catch (error) {
|
|
596
|
-
const message = `cannot delete session "${sessionId}": transcript
|
|
594
|
+
const message = `cannot delete session "${sessionId}": transcript artifact "${location.path}" remains before bookkeeping cleanup`;
|
|
597
595
|
const detail = `${message}: ${String(error)}`;
|
|
598
596
|
this.ctx.logger.warn(`archive-manager: ${detail}`);
|
|
599
597
|
throw new Error(detail, { cause: error });
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@michengai/dsh-archive-manager",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "NPM-installable DSH Web plugin for managing archived sessions.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"packageManager": "pnpm@11.
|
|
6
|
+
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": "^22.19.0 || >=24.0.0"
|
|
9
9
|
},
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.5 <0.2.0",
|
|
44
44
|
"@deepseek-ai/dsh-client-ui-sidebar": ">=0.1.0-rc.5 <0.2.0",
|
|
45
45
|
"@deepseek-ai/dsh-client-ui-workspace": ">=0.1.0-rc.5 <0.2.0",
|
|
46
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.5 <0.2.0",
|
|
46
47
|
"@deepseek-ai/dsh-session": ">=0.1.0-rc.5 <0.2.0",
|
|
47
48
|
"@deepseek-ai/dsh-session-projection-cache": ">=0.1.0-rc.5 <0.2.0",
|
|
48
49
|
"@deepseek-ai/dsh-spill-local": ">=0.1.0-rc.5 <0.2.0",
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
"@deepseek-ai/dsh-client-ui-sidebar": "0.1.2-rc.1",
|
|
71
72
|
"@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
|
|
72
73
|
"@deepseek-ai/dsh-client-ui-workspace": "0.1.2-rc.1",
|
|
74
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
|
|
73
75
|
"@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
|
|
74
76
|
"@deepseek-ai/dsh-scope": "0.1.2-rc.1",
|
|
75
77
|
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
|