@bit-ark/dsh-archive 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +726 -0
- package/lib/client.js.map +7 -0
- package/lib/index.js +246 -0
- package/lib/index.js.map +7 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# dsh-archive
|
|
2
|
+
|
|
3
|
+
为 DeepSeek Harness Web GUI 增加侧边栏「归档」入口:footer 动作按钮 + 浮动面板。
|
|
4
|
+
|
|
5
|
+
归档的生命周期起点和终点都在侧边栏(右键「归档会话」→ 会话消失;恢复 → 会话回到
|
|
6
|
+
列表),因此管理界面也放在侧边栏,而不是会话区的 tab(会话 tab 行是 session 作用域
|
|
7
|
+
的视图,归档是项目级管理,作用域不匹配,且没有打开会话时不可达)。
|
|
8
|
+
|
|
9
|
+
- **入口**:侧边栏 footer 的「归档 N」按钮(宽栏显示文字 + 计数,rail 窄栏为圆形
|
|
10
|
+
图标按钮)。当前项目没有归档时按钮隐藏;归档第一个会话后自动出现。
|
|
11
|
+
- **面板**:点击按钮在 footer 上方打开浮动面板,支持 Escape / 点击外部关闭。
|
|
12
|
+
面板结构:头部(标题 + 关闭)→ 滚动列表 → footer(归档计数提示 + 一键删除)。
|
|
13
|
+
- **列表**:只显示当前项目的已归档会话——归档会话属于当前项目当且仅当它在该项目的
|
|
14
|
+
workspace 记账中,或其存储 cwd 等于项目路径。当前项目解析不依赖打开的会话:
|
|
15
|
+
最近活跃 workspace → 当前会话所属 workspace → 当前会话 cwd 对应的 workspace。
|
|
16
|
+
- **恢复**:把会话从全局归档集合移除(侧边栏「归档会话」的逆操作),会话回到侧边栏原位置。
|
|
17
|
+
- **删除**:硬删除——删除会话日志目录(`~/.dsh/sessions/<项目>/<会话>/`)、解除
|
|
18
|
+
workspace 记账、移出归档集合,并尽力清理投影缓存行。正在运行(live)的会话会被拒绝。
|
|
19
|
+
- **一键删除**:面板底部 footer 区域的按钮,确认后把当前项目全部已归档会话一次性
|
|
20
|
+
批量硬删除——复用单删的同一套步骤(逐会话),运行中的会话跳过、单个硬错误不中断
|
|
21
|
+
整批,完成后显示汇总(删除 / 跳过 / 失败计数与失败明细)。
|
|
22
|
+
|
|
23
|
+
## 结构
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/index.ts 宿主半:POST /dsh-archive/restore | /delete | /delete-all
|
|
27
|
+
src/client/index.tsx 客户端半:sidebar.footer.action slot 注册(id: archive-panel)
|
|
28
|
+
build.mjs esbuild 构建(lib/index.js + lib/client.js 工厂包装)
|
|
29
|
+
cordis.patch.yml 自带组合层:插入 dsh-archive 行
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 构建
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
pnpm install # 首次(仅 esbuild)
|
|
36
|
+
pnpm build # 修改 src/ 后重新构建 lib/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 安装(已完成则跳过)
|
|
40
|
+
|
|
41
|
+
在 deepseek-harness checkout 目录执行(插件已发布到 npm,按名安装即可):
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pnpm dsh plugin --profile web add @bit-ark/dsh-archive
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
该命令从 npm 拉取包并挂进 `~/.dsh/profiles/web`(依赖与 `dsh.profile.bundles`
|
|
48
|
+
由 `dsh plugin` 自动维护),之后每次 `pnpm dsh web` 启动都会加载本插件。
|
|
49
|
+
|
|
50
|
+
## 生效方式
|
|
51
|
+
|
|
52
|
+
- 宿主半改动:重建后重启 `pnpm dsh web`。
|
|
53
|
+
- 客户端半改动:`pnpm build` 后刷新页面即可(重启更稳妥)。dev:web watcher
|
|
54
|
+
不覆盖树外插件,属预期行为。
|
|
55
|
+
|
|
56
|
+
## 删除的步骤顺序(防脏状态)
|
|
57
|
+
|
|
58
|
+
`/delete` 与 `/delete-all` 共用同一个逐会话删除核心。步骤刻意排序为:只读校验
|
|
59
|
+
(live 检查、日志目录定位与目录名守卫)→ registry 域写入(unarchive → 逐个
|
|
60
|
+
workspace detach,单次失败收集警告不中断)→ 最后才 `rm` 删除文件目录 → 尽力
|
|
61
|
+
清理投影缓存。这样写链 503 或任何校验失败都会在任何破坏性操作之前中止,不会出现
|
|
62
|
+
「文件已删但归档集合还残留」的半删除状态。该流程并非跨域事务,仍非原子,但任何
|
|
63
|
+
失败都停在可恢复状态。
|
|
64
|
+
|
|
65
|
+
`/delete-all` 在单个会话上的语义与单删一致,但批量语义是「能删多少删多少」:
|
|
66
|
+
live 会话计入跳过(不拒绝整批),其他硬错误计入失败明细并继续处理剩余会话;
|
|
67
|
+
id 列表去重保序、上限 500 个。客户端在调用前先用 `window.confirm` 二次确认。
|
|
68
|
+
|
|
69
|
+
## 通用性说明
|
|
70
|
+
|
|
71
|
+
- 插件只消费 harness 的公开服务(`webServer` / `workspaceRegistry` 的公开面 /
|
|
72
|
+
`sessionPersistence` / `sessions` / 客户端各 store 与 slot)。
|
|
73
|
+
- 「恢复」需要 registry 的运行时私有写链(harness 无公开 unarchive API)。
|
|
74
|
+
成员经特性探测,若未来 harness 改名,接口返回 503 并提示,绝不猜测性写入——
|
|
75
|
+
这是本插件对 harness 内部实现的唯一触碰点。
|
|
76
|
+
- 「下载」对接 harness 核心端点 `/api/session.export`,与 session-log-download
|
|
77
|
+
插件无关;本部署未装载 session-log-download,其会话头按钮位由 notifier 铃铛占据。
|
|
78
|
+
|
|
79
|
+
## 已知边界
|
|
80
|
+
|
|
81
|
+
- 已归档但所属 workspace 已删除的"孤儿"会话(目录不存在)不属于任何项目,
|
|
82
|
+
任何项目的面板都不会显示它们;重新注册原目录为 workspace 后即可管理。
|
|
83
|
+
- session-query-sqlite 在本部署为 `openAt: never` 的纯内存索引,无磁盘残留,
|
|
84
|
+
故不处理;若部署改为持久索引,需要在此补充清理。
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "dsh-archive", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/client/index.tsx
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
apply: () => apply,
|
|
25
|
+
inject: () => inject
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
30
|
+
var inject = ["slots", "sessions", "workspaces"];
|
|
31
|
+
var DELETE_BATCH_SIZE = 200;
|
|
32
|
+
async function callHost(endpoint, body) {
|
|
33
|
+
const response = await fetch(`/dsh-archive/${endpoint}`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "content-type": "application/json" },
|
|
36
|
+
body: JSON.stringify(body)
|
|
37
|
+
});
|
|
38
|
+
let payload;
|
|
39
|
+
try {
|
|
40
|
+
payload = await response.json();
|
|
41
|
+
} catch {
|
|
42
|
+
payload = void 0;
|
|
43
|
+
}
|
|
44
|
+
if (!response.ok || payload === void 0 || payload.ok !== true) {
|
|
45
|
+
throw new Error(payload?.error ?? `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${String(response.status)}\uFF09`);
|
|
46
|
+
}
|
|
47
|
+
return payload;
|
|
48
|
+
}
|
|
49
|
+
function formatTime(value) {
|
|
50
|
+
if (value === void 0 || Number.isNaN(value)) return "";
|
|
51
|
+
try {
|
|
52
|
+
return new Date(value).toLocaleString();
|
|
53
|
+
} catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function ArchiveIcon({ size }) {
|
|
58
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
59
|
+
"svg",
|
|
60
|
+
{
|
|
61
|
+
width: size,
|
|
62
|
+
height: size,
|
|
63
|
+
viewBox: "0 0 16 16",
|
|
64
|
+
fill: "none",
|
|
65
|
+
stroke: "currentColor",
|
|
66
|
+
strokeWidth: "1.2",
|
|
67
|
+
strokeLinecap: "round",
|
|
68
|
+
strokeLinejoin: "round",
|
|
69
|
+
"aria-hidden": true,
|
|
70
|
+
children: [
|
|
71
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "1.5", y: "2", width: "13", height: "3.5", rx: "1" }),
|
|
72
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M2.5 5.5v7a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5v-7" }),
|
|
73
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M6 8.5h4" })
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
var downloadStates = {};
|
|
79
|
+
var downloadListeners = /* @__PURE__ */ new Set();
|
|
80
|
+
var downloadInflight = /* @__PURE__ */ new Set();
|
|
81
|
+
var downloadsSnapshot = { bySession: {} };
|
|
82
|
+
function subscribeDownloads(listener) {
|
|
83
|
+
downloadListeners.add(listener);
|
|
84
|
+
return () => {
|
|
85
|
+
downloadListeners.delete(listener);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function emitDownloads() {
|
|
89
|
+
downloadsSnapshot = { bySession: { ...downloadStates } };
|
|
90
|
+
for (const listener of downloadListeners) listener();
|
|
91
|
+
}
|
|
92
|
+
function getDownloadsSnapshot() {
|
|
93
|
+
return downloadsSnapshot;
|
|
94
|
+
}
|
|
95
|
+
function hostBase() {
|
|
96
|
+
const origin = globalThis.location?.origin;
|
|
97
|
+
return origin !== void 0 && origin !== "null" ? origin : "http://dsh.internal";
|
|
98
|
+
}
|
|
99
|
+
function sessionLogZipFilename(sessionId) {
|
|
100
|
+
return `dsh-session-${String(sessionId).replace(/[^A-Za-z0-9_-]/g, "_")}.zip`;
|
|
101
|
+
}
|
|
102
|
+
function exportUrl(sessionId) {
|
|
103
|
+
const url = new URL("/api/session.export", hostBase());
|
|
104
|
+
url.searchParams.set("sessionId", sessionId);
|
|
105
|
+
url.searchParams.set("includeDescendants", "true");
|
|
106
|
+
return url.toString();
|
|
107
|
+
}
|
|
108
|
+
async function downloadSessionLog(sessionId) {
|
|
109
|
+
if (downloadInflight.has(sessionId)) return;
|
|
110
|
+
downloadInflight.add(sessionId);
|
|
111
|
+
downloadStates[sessionId] = { status: "downloading" };
|
|
112
|
+
emitDownloads();
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(exportUrl(sessionId), { method: "HEAD" });
|
|
115
|
+
if (!response.ok) throw new Error(`\u5BFC\u51FA\u5931\u8D25\uFF1AHTTP ${response.status}`);
|
|
116
|
+
const anchor = document.createElement("a");
|
|
117
|
+
anchor.href = exportUrl(sessionId);
|
|
118
|
+
anchor.download = sessionLogZipFilename(sessionId);
|
|
119
|
+
anchor.click();
|
|
120
|
+
downloadStates[sessionId] = { status: "success" };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
downloadStates[sessionId] = {
|
|
123
|
+
status: "error",
|
|
124
|
+
error: error instanceof Error ? error.message : String(error)
|
|
125
|
+
};
|
|
126
|
+
} finally {
|
|
127
|
+
downloadInflight.delete(sessionId);
|
|
128
|
+
}
|
|
129
|
+
emitDownloads();
|
|
130
|
+
}
|
|
131
|
+
function pruneDownloads(activeIds) {
|
|
132
|
+
let changed = false;
|
|
133
|
+
for (const id of Object.keys(downloadStates)) {
|
|
134
|
+
if (!activeIds.has(id)) {
|
|
135
|
+
delete downloadStates[id];
|
|
136
|
+
changed = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (changed) emitDownloads();
|
|
140
|
+
}
|
|
141
|
+
function apply(ctx) {
|
|
142
|
+
const STYLE_ID = "dsh-archive-style";
|
|
143
|
+
if (typeof document !== "undefined" && document.getElementById(STYLE_ID) === null) {
|
|
144
|
+
const styleEl = document.createElement("style");
|
|
145
|
+
styleEl.id = STYLE_ID;
|
|
146
|
+
styleEl.setAttribute("data-plugin", "dsh-archive");
|
|
147
|
+
styleEl.textContent = `
|
|
148
|
+
.dsh-archive-badge {
|
|
149
|
+
background: transparent;
|
|
150
|
+
}
|
|
151
|
+
.dsh-archive-badge:hover,
|
|
152
|
+
.dsh-archive-badge[data-active] {
|
|
153
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
154
|
+
}
|
|
155
|
+
.dsh-archive-close:hover {
|
|
156
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
157
|
+
}
|
|
158
|
+
.dsh-archive-batch:hover:not(:disabled) {
|
|
159
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);
|
|
160
|
+
}
|
|
161
|
+
`;
|
|
162
|
+
document.head.appendChild(styleEl);
|
|
163
|
+
}
|
|
164
|
+
const styles = {
|
|
165
|
+
// 侧边栏 footer 把动作排成一行水平 flex(flex-direction: row, nowrap)。
|
|
166
|
+
// 条目是占满整行的 flex 项(可收缩,未来有兄弟动作时共享空间而非被挤出),
|
|
167
|
+
// 徽标几何与设置触发按钮一致——同为 34px 高、12px 圆角、14px/22px 字体,
|
|
168
|
+
// hover 高亮向外溢出 4px。rail 窄栏保持 36px 圆形。
|
|
169
|
+
layer: {
|
|
170
|
+
position: "relative",
|
|
171
|
+
flex: "1 1 auto",
|
|
172
|
+
minWidth: 0,
|
|
173
|
+
display: "flex",
|
|
174
|
+
alignItems: "center"
|
|
175
|
+
},
|
|
176
|
+
layerRail: {
|
|
177
|
+
flex: "none"
|
|
178
|
+
},
|
|
179
|
+
badge: {
|
|
180
|
+
display: "flex",
|
|
181
|
+
alignItems: "center",
|
|
182
|
+
gap: "8px",
|
|
183
|
+
width: "calc(100% + 8px)",
|
|
184
|
+
height: "34px",
|
|
185
|
+
margin: "4px -4px 4px",
|
|
186
|
+
padding: "6px 10px",
|
|
187
|
+
boxSizing: "border-box",
|
|
188
|
+
border: "none",
|
|
189
|
+
borderRadius: "12px",
|
|
190
|
+
color: "var(--dsw-alias-label-primary)",
|
|
191
|
+
fontFamily: "inherit",
|
|
192
|
+
fontSize: "14px",
|
|
193
|
+
lineHeight: "22px",
|
|
194
|
+
cursor: "pointer",
|
|
195
|
+
overflow: "hidden",
|
|
196
|
+
whiteSpace: "nowrap"
|
|
197
|
+
},
|
|
198
|
+
badgeRail: {
|
|
199
|
+
justifyContent: "center",
|
|
200
|
+
gap: "0",
|
|
201
|
+
width: "36px",
|
|
202
|
+
height: "36px",
|
|
203
|
+
margin: "8px 0 10px",
|
|
204
|
+
padding: "0",
|
|
205
|
+
borderRadius: "50%"
|
|
206
|
+
},
|
|
207
|
+
badgeLabel: {
|
|
208
|
+
flex: "none",
|
|
209
|
+
minWidth: 0,
|
|
210
|
+
overflow: "hidden",
|
|
211
|
+
textOverflow: "ellipsis",
|
|
212
|
+
whiteSpace: "nowrap"
|
|
213
|
+
},
|
|
214
|
+
badgeCount: {
|
|
215
|
+
flex: "none",
|
|
216
|
+
marginLeft: "auto",
|
|
217
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
218
|
+
fontSize: "12px",
|
|
219
|
+
lineHeight: "16px",
|
|
220
|
+
fontVariantNumeric: "tabular-nums"
|
|
221
|
+
},
|
|
222
|
+
panel: {
|
|
223
|
+
position: "fixed",
|
|
224
|
+
left: "12px",
|
|
225
|
+
bottom: "128px",
|
|
226
|
+
zIndex: 30,
|
|
227
|
+
display: "flex",
|
|
228
|
+
flexDirection: "column",
|
|
229
|
+
width: "420px",
|
|
230
|
+
maxWidth: "calc(100vw - 24px)",
|
|
231
|
+
maxHeight: "60vh",
|
|
232
|
+
overflow: "hidden",
|
|
233
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
234
|
+
borderRadius: "12px",
|
|
235
|
+
background: "var(--dsw-alias-bg-base)",
|
|
236
|
+
boxShadow: "var(--dsw-shadow-lv2)"
|
|
237
|
+
},
|
|
238
|
+
panelHeader: {
|
|
239
|
+
flex: "none",
|
|
240
|
+
display: "flex",
|
|
241
|
+
alignItems: "center",
|
|
242
|
+
gap: "8px",
|
|
243
|
+
minHeight: "44px",
|
|
244
|
+
padding: "0 12px",
|
|
245
|
+
boxSizing: "border-box",
|
|
246
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2)"
|
|
247
|
+
},
|
|
248
|
+
panelTitle: {
|
|
249
|
+
fontSize: "13px",
|
|
250
|
+
fontWeight: 500,
|
|
251
|
+
lineHeight: "20px",
|
|
252
|
+
color: "var(--dsw-alias-label-primary)"
|
|
253
|
+
},
|
|
254
|
+
panelProject: {
|
|
255
|
+
minWidth: 0,
|
|
256
|
+
overflow: "hidden",
|
|
257
|
+
textOverflow: "ellipsis",
|
|
258
|
+
whiteSpace: "nowrap",
|
|
259
|
+
fontSize: "12px",
|
|
260
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
261
|
+
},
|
|
262
|
+
close: {
|
|
263
|
+
flex: "none",
|
|
264
|
+
marginLeft: "auto",
|
|
265
|
+
display: "inline-flex",
|
|
266
|
+
alignItems: "center",
|
|
267
|
+
justifyContent: "center",
|
|
268
|
+
width: "24px",
|
|
269
|
+
height: "24px",
|
|
270
|
+
padding: 0,
|
|
271
|
+
border: "none",
|
|
272
|
+
borderRadius: "6px",
|
|
273
|
+
background: "transparent",
|
|
274
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
275
|
+
fontSize: "14px",
|
|
276
|
+
cursor: "pointer"
|
|
277
|
+
},
|
|
278
|
+
batchDelete: {
|
|
279
|
+
flex: "none",
|
|
280
|
+
display: "inline-flex",
|
|
281
|
+
alignItems: "center",
|
|
282
|
+
height: "24px",
|
|
283
|
+
padding: "0 12px",
|
|
284
|
+
boxSizing: "border-box",
|
|
285
|
+
borderRadius: "6px",
|
|
286
|
+
border: "1px solid var(--dsw-alias-state-error-primary)",
|
|
287
|
+
background: "transparent",
|
|
288
|
+
color: "var(--dsw-alias-state-error-primary)",
|
|
289
|
+
fontSize: "12px",
|
|
290
|
+
lineHeight: "1",
|
|
291
|
+
cursor: "pointer"
|
|
292
|
+
},
|
|
293
|
+
footerHint: {
|
|
294
|
+
flex: 1,
|
|
295
|
+
minWidth: 0,
|
|
296
|
+
overflow: "hidden",
|
|
297
|
+
textOverflow: "ellipsis",
|
|
298
|
+
whiteSpace: "nowrap",
|
|
299
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
300
|
+
fontSize: "12px",
|
|
301
|
+
lineHeight: "16px"
|
|
302
|
+
},
|
|
303
|
+
panelFooter: {
|
|
304
|
+
flex: "none",
|
|
305
|
+
display: "flex",
|
|
306
|
+
alignItems: "center",
|
|
307
|
+
gap: "8px",
|
|
308
|
+
minHeight: "44px",
|
|
309
|
+
padding: "0 12px",
|
|
310
|
+
boxSizing: "border-box",
|
|
311
|
+
borderTop: "1px solid var(--dsw-alias-border-l2)"
|
|
312
|
+
},
|
|
313
|
+
notice: {
|
|
314
|
+
padding: "8px 12px",
|
|
315
|
+
marginBottom: "8px",
|
|
316
|
+
borderRadius: "8px",
|
|
317
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
318
|
+
background: "var(--dsw-alias-bg-base)",
|
|
319
|
+
color: "var(--dsw-alias-label-primary)",
|
|
320
|
+
fontSize: "12px",
|
|
321
|
+
whiteSpace: "pre-wrap",
|
|
322
|
+
wordBreak: "break-all"
|
|
323
|
+
},
|
|
324
|
+
panelBody: {
|
|
325
|
+
flex: 1,
|
|
326
|
+
minHeight: 0,
|
|
327
|
+
overflowY: "auto",
|
|
328
|
+
padding: "8px 12px 12px"
|
|
329
|
+
},
|
|
330
|
+
hint: {
|
|
331
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
332
|
+
fontSize: "12px",
|
|
333
|
+
lineHeight: "18px",
|
|
334
|
+
margin: "4px 0 8px"
|
|
335
|
+
},
|
|
336
|
+
error: {
|
|
337
|
+
padding: "8px 12px",
|
|
338
|
+
marginBottom: "8px",
|
|
339
|
+
borderRadius: "8px",
|
|
340
|
+
border: "1px solid var(--dsw-alias-state-error-primary)",
|
|
341
|
+
color: "var(--dsw-alias-state-error-primary)",
|
|
342
|
+
fontSize: "12px",
|
|
343
|
+
whiteSpace: "pre-wrap",
|
|
344
|
+
wordBreak: "break-all"
|
|
345
|
+
},
|
|
346
|
+
row: {
|
|
347
|
+
display: "flex",
|
|
348
|
+
alignItems: "center",
|
|
349
|
+
gap: "12px",
|
|
350
|
+
padding: "10px 12px",
|
|
351
|
+
marginBottom: "8px",
|
|
352
|
+
borderRadius: "8px",
|
|
353
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
354
|
+
background: "var(--dsw-alias-bg-base)"
|
|
355
|
+
},
|
|
356
|
+
rowMain: {
|
|
357
|
+
flex: 1,
|
|
358
|
+
minWidth: 0
|
|
359
|
+
},
|
|
360
|
+
rowTitle: {
|
|
361
|
+
overflow: "hidden",
|
|
362
|
+
textOverflow: "ellipsis",
|
|
363
|
+
whiteSpace: "nowrap",
|
|
364
|
+
cursor: "pointer",
|
|
365
|
+
fontSize: "13px",
|
|
366
|
+
color: "var(--dsw-alias-label-primary)"
|
|
367
|
+
},
|
|
368
|
+
rowMeta: {
|
|
369
|
+
marginTop: "2px",
|
|
370
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
371
|
+
fontSize: "12px"
|
|
372
|
+
},
|
|
373
|
+
buttonBase: {
|
|
374
|
+
flex: "none",
|
|
375
|
+
padding: "4px 12px",
|
|
376
|
+
borderRadius: "6px",
|
|
377
|
+
fontSize: "12px",
|
|
378
|
+
cursor: "pointer",
|
|
379
|
+
background: "transparent"
|
|
380
|
+
},
|
|
381
|
+
empty: {
|
|
382
|
+
padding: "24px 0",
|
|
383
|
+
textAlign: "center",
|
|
384
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
385
|
+
fontSize: "12px"
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
function ArchiveFooterAction(props) {
|
|
389
|
+
const wide = props.wide !== false;
|
|
390
|
+
const workspaces = (0, import_react.useSyncExternalStore)(
|
|
391
|
+
ctx.workspaces.list.subscribe,
|
|
392
|
+
() => ctx.workspaces.list.getSnapshot()
|
|
393
|
+
);
|
|
394
|
+
const sessions = (0, import_react.useSyncExternalStore)(
|
|
395
|
+
ctx.sessions.list.subscribe,
|
|
396
|
+
() => ctx.sessions.list.getSnapshot()
|
|
397
|
+
);
|
|
398
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
399
|
+
const [busy, setBusy] = (0, import_react.useState)(null);
|
|
400
|
+
const [batchBusy, setBatchBusy] = (0, import_react.useState)(false);
|
|
401
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
402
|
+
const [notice, setNotice] = (0, import_react.useState)(null);
|
|
403
|
+
const panelRef = (0, import_react.useRef)(null);
|
|
404
|
+
const triggerRef = (0, import_react.useRef)(null);
|
|
405
|
+
const busyRef = (0, import_react.useRef)(false);
|
|
406
|
+
const batchBusyRef = (0, import_react.useRef)(false);
|
|
407
|
+
const workspace = (0, import_react.useMemo)(() => {
|
|
408
|
+
const items = workspaces?.items;
|
|
409
|
+
if (items === void 0 || items.length === 0) return void 0;
|
|
410
|
+
const recent = workspaces?.recentWorkspaceId;
|
|
411
|
+
if (recent !== void 0) {
|
|
412
|
+
const byRecent = items.find((item) => item.workspaceId === recent);
|
|
413
|
+
if (byRecent !== void 0) return byRecent;
|
|
414
|
+
}
|
|
415
|
+
const current = sessions?.current;
|
|
416
|
+
if (current !== void 0) {
|
|
417
|
+
const byMembership = items.find((item) => item.sessionIds.includes(current));
|
|
418
|
+
if (byMembership !== void 0) return byMembership;
|
|
419
|
+
const currentCwd = sessions?.byId[current]?.cwd;
|
|
420
|
+
if (currentCwd !== void 0) {
|
|
421
|
+
const byPath = items.find((item) => item.path === currentCwd);
|
|
422
|
+
if (byPath !== void 0) return byPath;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return void 0;
|
|
426
|
+
}, [workspaces, sessions]);
|
|
427
|
+
const projectLabel = workspace?.title;
|
|
428
|
+
const rows = (0, import_react.useMemo)(() => {
|
|
429
|
+
const archived = workspaces?.archivedSessionIds;
|
|
430
|
+
if (archived === void 0 || archived.length === 0 || workspace === void 0) return [];
|
|
431
|
+
const result = [];
|
|
432
|
+
for (const id of archived) {
|
|
433
|
+
const summary = sessions?.byId[id];
|
|
434
|
+
const inAccount = workspace.sessionIds.includes(id);
|
|
435
|
+
const sameProject = summary?.cwd !== void 0 && summary.cwd === workspace.path;
|
|
436
|
+
if (!inAccount && !sameProject) continue;
|
|
437
|
+
result.push({
|
|
438
|
+
id,
|
|
439
|
+
title: summary?.displayTitle ?? id,
|
|
440
|
+
updatedAt: summary?.updatedAt
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return result;
|
|
444
|
+
}, [workspaces, sessions, workspace]);
|
|
445
|
+
(0, import_react.useEffect)(() => {
|
|
446
|
+
pruneDownloads(new Set(rows.map((row) => row.id)));
|
|
447
|
+
}, [rows]);
|
|
448
|
+
(0, import_react.useEffect)(() => {
|
|
449
|
+
if (!open) return;
|
|
450
|
+
const onKey = (event) => {
|
|
451
|
+
if (event.key === "Escape") setOpen(false);
|
|
452
|
+
};
|
|
453
|
+
const onPointerDown = (event) => {
|
|
454
|
+
const target = event.target;
|
|
455
|
+
if (target === null) return;
|
|
456
|
+
if (panelRef.current?.contains(target) === true) return;
|
|
457
|
+
if (triggerRef.current?.contains(target) === true) return;
|
|
458
|
+
setOpen(false);
|
|
459
|
+
};
|
|
460
|
+
document.addEventListener("keydown", onKey);
|
|
461
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
462
|
+
return () => {
|
|
463
|
+
document.removeEventListener("keydown", onKey);
|
|
464
|
+
document.removeEventListener("mousedown", onPointerDown);
|
|
465
|
+
};
|
|
466
|
+
}, [open]);
|
|
467
|
+
const downloads = (0, import_react.useSyncExternalStore)(
|
|
468
|
+
(listener) => subscribeDownloads(listener),
|
|
469
|
+
() => getDownloadsSnapshot()
|
|
470
|
+
);
|
|
471
|
+
const run = async (action, id) => {
|
|
472
|
+
if (busyRef.current) return;
|
|
473
|
+
busyRef.current = true;
|
|
474
|
+
setError(null);
|
|
475
|
+
setNotice(null);
|
|
476
|
+
setBusy({ id, action });
|
|
477
|
+
try {
|
|
478
|
+
await callHost(action, { sessionId: id });
|
|
479
|
+
ctx.sessions.refresh?.().catch?.(() => {
|
|
480
|
+
});
|
|
481
|
+
ctx.workspaces.refresh?.().catch?.(() => {
|
|
482
|
+
});
|
|
483
|
+
} catch (err) {
|
|
484
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
485
|
+
} finally {
|
|
486
|
+
busyRef.current = false;
|
|
487
|
+
setBusy(null);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
const restore = (id) => {
|
|
491
|
+
void run("restore", id);
|
|
492
|
+
};
|
|
493
|
+
const remove = (id, title) => {
|
|
494
|
+
const confirmed = window.confirm(
|
|
495
|
+
`\u786E\u5B9A\u8981\u6C38\u4E45\u5220\u9664\u4F1A\u8BDD\u300C${title}\u300D\u5417\uFF1F
|
|
496
|
+
|
|
497
|
+
\u8FD9\u662F\u786C\u5220\u9664\uFF1A\u4F1A\u8BDD\u7684\u6240\u6709\u6587\u4EF6\u90FD\u4F1A\u88AB\u5220\u9664\uFF0C\u65E0\u6CD5\u6062\u590D\u3002`
|
|
498
|
+
);
|
|
499
|
+
if (confirmed) void run("delete", id);
|
|
500
|
+
};
|
|
501
|
+
const deleteAll = () => {
|
|
502
|
+
if (rows.length === 0) return;
|
|
503
|
+
const confirmed = window.confirm(
|
|
504
|
+
`\u786E\u5B9A\u8981\u4E00\u952E\u5220\u9664\u5168\u90E8 ${rows.length} \u4E2A\u5DF2\u5F52\u6863\u4F1A\u8BDD\u5417\uFF1F
|
|
505
|
+
|
|
506
|
+
\u8FD9\u662F\u786C\u5220\u9664\uFF1A\u6BCF\u4E2A\u4F1A\u8BDD\u7684\u6240\u6709\u6587\u4EF6\u90FD\u4F1A\u88AB\u5220\u9664\uFF0C\u65E0\u6CD5\u6062\u590D\u3002
|
|
507
|
+
\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u4F1A\u88AB\u8DF3\u8FC7\u3002`
|
|
508
|
+
);
|
|
509
|
+
if (!confirmed) return;
|
|
510
|
+
if (batchBusyRef.current) return;
|
|
511
|
+
batchBusyRef.current = true;
|
|
512
|
+
setError(null);
|
|
513
|
+
setNotice(null);
|
|
514
|
+
setBatchBusy(true);
|
|
515
|
+
void (async () => {
|
|
516
|
+
try {
|
|
517
|
+
const ids = rows.map((row) => row.id);
|
|
518
|
+
const total = ids.length;
|
|
519
|
+
let deleted = 0;
|
|
520
|
+
let skipped = 0;
|
|
521
|
+
let failed = 0;
|
|
522
|
+
const failures = [];
|
|
523
|
+
for (let offset = 0; offset < ids.length; offset += DELETE_BATCH_SIZE) {
|
|
524
|
+
const payload = await callHost("delete-all", { sessionIds: ids.slice(offset, offset + DELETE_BATCH_SIZE) });
|
|
525
|
+
deleted += payload.deleted ?? 0;
|
|
526
|
+
skipped += payload.skipped ?? 0;
|
|
527
|
+
failed += payload.failed ?? 0;
|
|
528
|
+
const chunkFailures = payload.failures;
|
|
529
|
+
if (chunkFailures !== void 0 && chunkFailures.length > 0) failures.push(...chunkFailures);
|
|
530
|
+
}
|
|
531
|
+
const parts = [`\u5DF2\u5220\u9664 ${deleted} \u4E2A`];
|
|
532
|
+
if (skipped > 0) parts.push(`\u8DF3\u8FC7\u8FD0\u884C\u4E2D ${skipped} \u4E2A`);
|
|
533
|
+
if (failed > 0) parts.push(`\u5931\u8D25 ${failed} \u4E2A`);
|
|
534
|
+
let message = parts.join("\uFF0C");
|
|
535
|
+
if (failures.length > 0) {
|
|
536
|
+
message += `\uFF1A${failures.map((item) => `${item.sessionId}\uFF08${item.error}\uFF09`).join("\uFF1B")}`;
|
|
537
|
+
}
|
|
538
|
+
setNotice(message);
|
|
539
|
+
ctx.sessions.refresh?.().catch?.(() => {
|
|
540
|
+
});
|
|
541
|
+
ctx.workspaces.refresh?.().catch?.(() => {
|
|
542
|
+
});
|
|
543
|
+
} catch (err) {
|
|
544
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
545
|
+
} finally {
|
|
546
|
+
batchBusyRef.current = false;
|
|
547
|
+
setBatchBusy(false);
|
|
548
|
+
}
|
|
549
|
+
})();
|
|
550
|
+
};
|
|
551
|
+
const openSession = (id) => {
|
|
552
|
+
try {
|
|
553
|
+
ctx.sessions.open(id);
|
|
554
|
+
setOpen(false);
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: wide ? styles.layer : { ...styles.layer, ...styles.layerRail }, children: [
|
|
559
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
560
|
+
"section",
|
|
561
|
+
{
|
|
562
|
+
ref: panelRef,
|
|
563
|
+
style: styles.panel,
|
|
564
|
+
"aria-label": "\u5DF2\u5F52\u6863\u4F1A\u8BDD",
|
|
565
|
+
children: [
|
|
566
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { style: styles.panelHeader, children: [
|
|
567
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.panelTitle, children: "\u5DF2\u5F52\u6863\u4F1A\u8BDD" }),
|
|
568
|
+
projectLabel !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: styles.panelProject, children: [
|
|
569
|
+
"\xB7 ",
|
|
570
|
+
projectLabel
|
|
571
|
+
] }) : null,
|
|
572
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
573
|
+
"button",
|
|
574
|
+
{
|
|
575
|
+
type: "button",
|
|
576
|
+
className: "dsh-archive-close",
|
|
577
|
+
style: styles.close,
|
|
578
|
+
"aria-label": "\u5173\u95ED",
|
|
579
|
+
onClick: () => {
|
|
580
|
+
setOpen(false);
|
|
581
|
+
},
|
|
582
|
+
children: "\u2715"
|
|
583
|
+
}
|
|
584
|
+
)
|
|
585
|
+
] }),
|
|
586
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.panelBody, children: [
|
|
587
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.hint, children: "\u4EC5\u663E\u793A\u5F53\u524D\u9879\u76EE\u7684\u5F52\u6863\u3002\u4E0B\u8F7D\u4F1A\u5BFC\u51FA\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7 ZIP\uFF1B\u6062\u590D\u4F1A\u628A\u4F1A\u8BDD\u653E\u56DE\u4FA7\u8FB9\u680F\uFF1B\u5220\u9664\u4F1A\u6C38\u4E45\u79FB\u9664\u4F1A\u8BDD\u7684\u5168\u90E8\u6587\u4EF6\uFF0C\u65E0\u6CD5\u6062\u590D\u3002" }),
|
|
588
|
+
error !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.error, children: error }) : null,
|
|
589
|
+
notice !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.notice, children: notice }) : null,
|
|
590
|
+
rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.empty, children: "\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u5DF2\u5F52\u6863\u7684\u4F1A\u8BDD" }) : null,
|
|
591
|
+
rows.map((row) => {
|
|
592
|
+
const rowBusy = busy !== null && busy.id === row.id;
|
|
593
|
+
const anyBusy = busy !== null || batchBusy;
|
|
594
|
+
const time = formatTime(row.updatedAt);
|
|
595
|
+
const dlEntry = downloads?.bySession?.[row.id];
|
|
596
|
+
const downloading = dlEntry?.status === "downloading";
|
|
597
|
+
const dlError = dlEntry?.status === "error" ? dlEntry.error ?? "\u5BFC\u51FA\u5931\u8D25" : null;
|
|
598
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.row, children: [
|
|
599
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.rowMain, children: [
|
|
600
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
601
|
+
"div",
|
|
602
|
+
{
|
|
603
|
+
style: styles.rowTitle,
|
|
604
|
+
title: `\u6253\u5F00\u4F1A\u8BDD ${row.title}`,
|
|
605
|
+
onClick: () => {
|
|
606
|
+
openSession(row.id);
|
|
607
|
+
},
|
|
608
|
+
children: row.title
|
|
609
|
+
}
|
|
610
|
+
),
|
|
611
|
+
time !== "" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.rowMeta, children: time }) : null
|
|
612
|
+
] }),
|
|
613
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
614
|
+
"button",
|
|
615
|
+
{
|
|
616
|
+
type: "button",
|
|
617
|
+
title: dlError ?? "\u4E0B\u8F7D\u4F1A\u8BDD\u65E5\u5FD7 (ZIP)",
|
|
618
|
+
disabled: anyBusy || downloading,
|
|
619
|
+
style: {
|
|
620
|
+
...styles.buttonBase,
|
|
621
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
622
|
+
color: "var(--dsw-alias-label-primary)",
|
|
623
|
+
opacity: anyBusy || downloading ? 0.5 : 1
|
|
624
|
+
},
|
|
625
|
+
onClick: () => {
|
|
626
|
+
void downloadSessionLog(row.id);
|
|
627
|
+
},
|
|
628
|
+
children: downloading ? "\u4E0B\u8F7D\u4E2D\u2026" : dlError !== null ? "\u91CD\u8BD5" : "\u4E0B\u8F7D"
|
|
629
|
+
}
|
|
630
|
+
),
|
|
631
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
632
|
+
"button",
|
|
633
|
+
{
|
|
634
|
+
type: "button",
|
|
635
|
+
disabled: anyBusy,
|
|
636
|
+
style: {
|
|
637
|
+
...styles.buttonBase,
|
|
638
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
639
|
+
color: "var(--dsw-alias-label-primary)",
|
|
640
|
+
opacity: anyBusy && !rowBusy ? 0.5 : 1
|
|
641
|
+
},
|
|
642
|
+
onClick: () => {
|
|
643
|
+
restore(row.id);
|
|
644
|
+
},
|
|
645
|
+
children: rowBusy && busy?.action === "restore" ? "\u6062\u590D\u4E2D\u2026" : "\u6062\u590D"
|
|
646
|
+
}
|
|
647
|
+
),
|
|
648
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
649
|
+
"button",
|
|
650
|
+
{
|
|
651
|
+
type: "button",
|
|
652
|
+
disabled: anyBusy,
|
|
653
|
+
style: {
|
|
654
|
+
...styles.buttonBase,
|
|
655
|
+
border: "1px solid var(--dsw-alias-state-error-primary)",
|
|
656
|
+
color: "var(--dsw-alias-state-error-primary)",
|
|
657
|
+
opacity: anyBusy && !rowBusy ? 0.5 : 1
|
|
658
|
+
},
|
|
659
|
+
onClick: () => {
|
|
660
|
+
remove(row.id, row.title);
|
|
661
|
+
},
|
|
662
|
+
children: rowBusy && busy?.action === "delete" ? "\u5220\u9664\u4E2D\u2026" : "\u5220\u9664"
|
|
663
|
+
}
|
|
664
|
+
)
|
|
665
|
+
] }, row.id);
|
|
666
|
+
})
|
|
667
|
+
] }),
|
|
668
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("footer", { style: styles.panelFooter, children: [
|
|
669
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.footerHint, children: rows.length === 0 ? "\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u5DF2\u5F52\u6863\u7684\u4F1A\u8BDD" : `\u5171 ${rows.length} \u4E2A\u5F52\u6863\u4F1A\u8BDD\uFF08\u8FD0\u884C\u4E2D\u7684\u4F1A\u8DF3\u8FC7\uFF09` }),
|
|
670
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
671
|
+
"button",
|
|
672
|
+
{
|
|
673
|
+
type: "button",
|
|
674
|
+
className: "dsh-archive-batch",
|
|
675
|
+
style: {
|
|
676
|
+
...styles.batchDelete,
|
|
677
|
+
opacity: batchBusy || busy !== null || rows.length === 0 ? 0.5 : 1
|
|
678
|
+
},
|
|
679
|
+
disabled: batchBusy || busy !== null || rows.length === 0,
|
|
680
|
+
title: rows.length === 0 ? "\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u5DF2\u5F52\u6863\u7684\u4F1A\u8BDD" : `\u4E00\u952E\u5220\u9664\u5F53\u524D\u9879\u76EE\u5168\u90E8 ${rows.length} \u4E2A\u5DF2\u5F52\u6863\u4F1A\u8BDD\uFF08\u8FD0\u884C\u4E2D\u7684\u4F1A\u8DF3\u8FC7\uFF09`,
|
|
681
|
+
onClick: () => {
|
|
682
|
+
deleteAll();
|
|
683
|
+
},
|
|
684
|
+
children: batchBusy ? "\u5220\u9664\u4E2D\u2026" : "\u4E00\u952E\u5220\u9664"
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
] })
|
|
688
|
+
]
|
|
689
|
+
}
|
|
690
|
+
),
|
|
691
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
692
|
+
"button",
|
|
693
|
+
{
|
|
694
|
+
ref: triggerRef,
|
|
695
|
+
type: "button",
|
|
696
|
+
className: "dsh-archive-badge",
|
|
697
|
+
style: wide ? styles.badge : { ...styles.badge, ...styles.badgeRail },
|
|
698
|
+
"data-active": open || void 0,
|
|
699
|
+
"aria-label": `\u5DF2\u5F52\u6863\u4F1A\u8BDD\uFF08${String(rows.length)}\uFF09`,
|
|
700
|
+
"aria-expanded": open,
|
|
701
|
+
title: wide ? void 0 : `\u5DF2\u5F52\u6863\u4F1A\u8BDD\uFF08${String(rows.length)}\uFF09`,
|
|
702
|
+
onClick: () => {
|
|
703
|
+
setOpen((value) => !value);
|
|
704
|
+
},
|
|
705
|
+
children: [
|
|
706
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArchiveIcon, { size: wide ? 16 : 18 }),
|
|
707
|
+
wide && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
708
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.badgeLabel, children: "\u5F52\u6863" }),
|
|
709
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.badgeCount, children: rows.length })
|
|
710
|
+
] })
|
|
711
|
+
]
|
|
712
|
+
}
|
|
713
|
+
)
|
|
714
|
+
] });
|
|
715
|
+
}
|
|
716
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register(
|
|
717
|
+
{
|
|
718
|
+
name: "sidebar.footer.action",
|
|
719
|
+
id: "archive-panel"
|
|
720
|
+
},
|
|
721
|
+
ArchiveFooterAction
|
|
722
|
+
));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return module.exports; } });
|
|
726
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/client/index.tsx"],
|
|
4
|
+
"sourcesContent": ["/**\n * dsh-archive \u2014 \u5BA2\u6237\u7AEF\u534A\uFF08Browser\uFF09\u3002\n *\n * \u5728\u4FA7\u8FB9\u680F footer \u52A8\u4F5C\u5217\u8868\u6CE8\u518C\u4E00\u4E2A\u6761\u76EE\uFF1A\u5F52\u6863\u52A8\u4F5C\u3002footer \u52A8\u4F5C\u5E38\u9A7B\u663E\u793A\n * \uFF08\u5BBD\u680F = \u6587\u5B57 + \u8BA1\u6570\uFF0Crail \u7A84\u680F = \u7EAF\u56FE\u6807\uFF1B\u9879\u76EE\u65E0\u5F52\u6863\u65F6\u8BA1\u6570\u4E3A 0\uFF09\u3002\u70B9\u51FB\n * \u540E\u5728 footer \u4E0A\u65B9\u6253\u5F00\u6D6E\u52A8\u9762\u677F\uFF0C\u53EA\u5217\u51FA\u300C\u5F53\u524D\u9879\u76EE\u300D\u7684\u5DF2\u5F52\u6863\u4F1A\u8BDD\u2014\u2014\u5F52\u6863\u4F1A\u8BDD\n * \u5C5E\u4E8E\u5F53\u524D\u9879\u76EE\u5F53\u4E14\u4EC5\u5F53\uFF1A\u5B83\u5728\u5F53\u524D workspace \u7684\u8D26\u76EE\u4E2D\uFF0C\u6216\u5176\u5B58\u50A8\u7684 cwd \u7B49\u4E8E\n * \u9879\u76EE\u8DEF\u5F84\u3002\u6BCF\u884C\u63D0\u4F9B\u4E09\u4E2A\u52A8\u8BCD\uFF1A\n *\n * - \u4E0B\u8F7D\uFF1A\u5BF9\u63A5 harness \u6838\u5FC3\u7AEF\u70B9\u7684\u81EA\u5305\u542B\u5BFC\u51FA\uFF08/api/session.export\uFF0C\u4E0E\n * session-log-download \u63D2\u4EF6\u65E0\u5173\uFF1B\u540E\u8005\u4E0D\u5728\u672C\u90E8\u7F72\u7684 bundle \u5217\u8868\u91CC\uFF0C\u5176\u5934\u90E8\n * \u6309\u94AE\u4F4D\u88AB notifier \u94C3\u94DB\u5360\u636E\uFF09\u3002\u6BCF\u4E2A\u4F1A\u8BDD\u7684\u5BFC\u51FA\u72B6\u6001\u5B58\u5728\u672C\u63D2\u4EF6\u81EA\u5DF1\u7684 store\n * \u91CC\uFF08\u6A21\u5757\u7EA7 downloadStates\uFF09\uFF0C\u884C UI \u7A7A\u95F2\u6E32\u67D3\u4E5F\u4E0D\u4E22\u72B6\u6001\u3002\n * - \u6062\u590D\uFF1APOST /dsh-archive/restore\u2014\u2014\u5BBF\u4E3B\u628A\u4F1A\u8BDD\u79FB\u51FA\u5F52\u6863\u96C6\u5408\uFF1B\u968F\u540E\u5E7F\u64AD\n * \u7684 host/archived-sessions-changed \u5E27\u4F1A\u8BA9\u4FA7\u8FB9\u680F\u4E0E\u9762\u677F\u81EA\u52A8\u5237\u65B0\u3002\n * - \u5220\u9664\uFF1A\u786E\u8BA4\u540E POST /dsh-archive/delete\u2014\u2014\u5BBF\u4E3B\u786C\u5220\u9664\uFF08\u6301\u4E45\u5316\u5DE5\u4EF6 +\n * workspace \u8D26\u76EE + \u5F52\u6863\u96C6\u5408\uFF09\uFF1B\u6210\u529F\u540E\u672C\u9875\u4E3B\u52A8\u5237\u65B0 sessions \u4E0E workspaces\n * \u5217\u8868\uFF0C\u8BA9\u4FA7\u8FB9\u680F\u7ACB\u5373\u6D88\u5931\u3001\u4E0D\u6B8B\u7559\u5E7D\u7075\u6761\u76EE\u3002\n * - \u4E00\u952E\u5220\u9664\uFF1A\u9762\u677F\u5E95\u90E8 footer \u533A\u57DF\u7684\u6309\u94AE\uFF0C\u786E\u8BA4\u540E POST\n * /dsh-archive/delete-all\u2014\u2014\u628A\u5F53\u524D\u9879\u76EE\u5168\u90E8\u5DF2\u5F52\u6863\u4F1A\u8BDD\u4E00\u6B21\u6027\u6279\u91CF\u786C\u5220\u9664\n * \uFF08\u5BBF\u4E3B\u4FA7\u8DF3\u8FC7\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\uFF09\uFF0C\u6210\u529F\u540E\u540C\u6837\u5237\u65B0\u4E24\u4E2A\u5217\u8868\u5E76\u663E\u793A\u6C47\u603B\n * \uFF08\u5220\u9664 / \u8DF3\u8FC7 / \u5931\u8D25\u8BA1\u6570\u4E0E\u5931\u8D25\u660E\u7EC6\uFF09\u3002\n *\n * \u300C\u5F53\u524D\u9879\u76EE\u300D\u4E0D\u4F9D\u8D56\u4EFB\u4F55\u6253\u5F00\u7684\u4F1A\u8BDD\u5373\u53EF\u89E3\u6790\uFF1A\u6700\u8FD1\u6D3B\u8DC3\u7684 workspace \u2192 \u6301\u6709\n * \u5F53\u524D\u4F1A\u8BDD\u7684 workspace \u8D26\u76EE \u2192 \u5F53\u524D\u4F1A\u8BDD cwd \u5BF9\u5E94\u7684 workspace\u3002\u884C\u6570\u636E\u5168\u90E8\u6765\u81EA\n * \u8FD0\u884C\u65F6\u65E2\u6709 store\uFF08ctx.workspaces.list \u63D0\u4F9B\u5F52\u6863\u96C6\u5408\uFF0Cctx.sessions.list \u63D0\u4F9B\n * \u6807\u9898\uFF09\uFF1B\u53EA\u6709\u4E24\u4E2A\u53D8\u66F4\u64CD\u4F5C\u9700\u8981\u672C\u63D2\u4EF6\u81EA\u5DF1\u7684\u5BBF\u4E3B\u8DEF\u7531\u3002\n *\n * \u653E\u7F6E\u7406\u7531\uFF1A\u5F52\u6863\u662F\u4FA7\u8FB9\u680F\u52A8\u4F5C\uFF08workspace \u6D4F\u89C8\u5668\u53F3\u952E\u300C\u5F52\u6863\u4F1A\u8BDD\u300D\uFF09\uFF0C\u6062\u590D\u540E\n * \u4E5F\u56DE\u5230\u540C\u4E00\u5217\u8868\u2014\u2014\u7BA1\u7406\u754C\u9762\u653E\u5728\u751F\u547D\u5468\u671F\u8D77\u70B9\u4E0E\u7EC8\u70B9\u6240\u5728\u7684\u4FA7\u8FB9\u680F\uFF0C\u800C\u4E0D\u662F\u653E\u5728\n * session \u4F5C\u7528\u57DF\u7684\u4F1A\u8BDD tab \u884C\u91CC\u3002\n */\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'\n\nexport const inject = ['slots', 'sessions', 'workspaces']\n\n/** \u6279\u91CF\u5220\u9664\u5355\u6B21\u8BF7\u6C42\u7684 id \u4E0A\u9650\uFF08\u5BBF\u4E3B\u786C\u4E0A\u9650 500\uFF1B\u53D6 200 \u7559\u4F59\u91CF\u4E14\u8FDB\u5EA6\u53EF\u611F\u77E5\uFF09\u3002 */\nconst DELETE_BATCH_SIZE = 200\n\n/** \u7EBF\u4E0A\u6570\u636E\u5F62\u72B6\uFF08\u5BBF\u4E3B\u57DF\u5BF9\u8C61\u7684\u7EAF JSON \u6295\u5F71\uFF09\u3002 */\ninterface WorkspaceRow {\n workspaceId: string\n path: string\n title: string\n sessionIds: readonly string[]\n}\ninterface WorkspaceListSnapshot {\n items: readonly WorkspaceRow[]\n archivedSessionIds: readonly string[]\n recentWorkspaceId?: string\n}\ninterface SessionSummaryRow {\n id: string\n displayTitle: string\n cwd?: string\n updatedAt: number\n}\ninterface SessionListSnapshot {\n byId: Record<string, SessionSummaryRow | undefined>\n current?: string\n}\n\n/** \u9762\u677F\u91CC\u7684\u4E00\u884C\u5F52\u6863\u4F1A\u8BDD\u3002 */\ninterface ArchiveRow {\n id: string\n title: string\n updatedAt?: number\n}\n\n/**\n * \u8C03\u7528\u5BBF\u4E3B\u7AEF\u70B9\uFF08restore | delete | delete-all\uFF09\u3002\n * \u6210\u529F\u8981\u6C42 HTTP 200 \u4E14 payload.ok === true\uFF0C\u5426\u5219\u629B\u51FA\u53EF\u8BFB\u9519\u8BEF\u3002\n */\nasync function callHost(endpoint: 'restore' | 'delete' | 'delete-all', body: Record<string, unknown>): Promise<Record<string, unknown>> {\n const response = await fetch(`/dsh-archive/${endpoint}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n let payload: { ok?: boolean; error?: string } | undefined\n try {\n payload = await response.json() as { ok?: boolean; error?: string }\n } catch {\n payload = undefined\n }\n if (!response.ok || payload === undefined || payload.ok !== true) {\n throw new Error(payload?.error ?? `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${String(response.status)}\uFF09`)\n }\n return payload as Record<string, unknown>\n}\n\n/** \u65F6\u95F4\u6233 \u2192 \u672C\u5730\u53EF\u8BFB\u65F6\u95F4\u5B57\u7B26\u4E32\uFF08\u975E\u6CD5\u8F93\u5165\u8FD4\u56DE\u7A7A\u4E32\uFF09\u3002 */\nfunction formatTime(value: number | undefined): string {\n if (value === undefined || Number.isNaN(value)) return ''\n try {\n return new Date(value).toLocaleString()\n } catch {\n return ''\n }\n}\n\n/** \u5185\u8054\u5F52\u6863\u7BB1\u56FE\u6807\uFF08\u4E0D\u5F15\u5165\u56FE\u6807\u5305\u4F9D\u8D56\uFF0C\u4FDD\u6301\u63D2\u4EF6\u81EA\u5305\u542B\uFF09\u3002 */\nfunction ArchiveIcon({ size }: { size: number }) {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n >\n <rect x=\"1.5\" y=\"2\" width=\"13\" height=\"3.5\" rx=\"1\" />\n <path d=\"M2.5 5.5v7a1.5 1.5 0 0 0 1.5 1.5h8a1.5 1.5 0 0 0 1.5-1.5v-7\" />\n <path d=\"M6 8.5h4\" />\n </svg>\n )\n}\n\n/**\n * \u6BCF\u4E2A\u4F1A\u8BDD\u7684\u65E5\u5FD7\u5BFC\u51FA\u72B6\u6001\uFF08\u672C\u63D2\u4EF6\u79C1\u6709 store\uFF0C\u884C UI \u7A7A\u95F2\u6E32\u67D3\u4E5F\u4FDD\u7559\uFF09\u3002\n * \u6A21\u5757\u7EA7\u800C\u975E\u7EC4\u4EF6\u7EA7\uFF1A\u9762\u677F\u5173\u95ED\u518D\u6253\u5F00\u3001\u884C\u91CD\u6E32\u67D3\u65F6\u4E0B\u8F7D\u72B6\u6001\u4E0D\u4E22\u3002\n */\ntype DownloadStatus = 'downloading' | 'success' | 'error'\ninterface DownloadEntry { status: DownloadStatus; error?: string }\nconst downloadStates: Record<string, DownloadEntry> = {}\nconst downloadListeners = new Set<() => void>()\n/** \u5728\u9014\u4E0B\u8F7D\u7684 sessionId \u96C6\u5408\uFF1A\u540C\u6B65\u62E6\u622A\u540C\u4E00\u4F1A\u8BDD\u7684\u91CD\u590D\u70B9\u51FB\uFF08\u6309\u94AE\u7981\u7528\u8981\u7B49\u6E32\u67D3\uFF09\u3002 */\nconst downloadInflight = new Set<string>()\n\n/**\n * \u4F9B useSyncExternalStore \u4F7F\u7528\u7684\u7F13\u5B58\u5FEB\u7167\u3002\u5305\u88C5\u5C42\u5FC5\u987B\u4FDD\u6301\u5F15\u7528\u7A33\u5B9A\uFF1A\n * useSyncExternalStore \u7528 Object.is \u6BD4\u8F83 getSnapshot() \u7684\u7ED3\u679C\uFF0C\u82E5\u6BCF\u6B21\u8BFB\u53D6\n * \u90FD\u8FD4\u56DE\u65B0\u5BF9\u8C61\u4F1A\u9020\u6210\u65E0\u9650\u91CD\u6E32\u67D3\uFF08React #185\uFF09\u3002\u53EA\u6709 emitDownloads \u5728\u72B6\u6001\n * \u771F\u6B63\u53D8\u5316\u65F6\u624D\u66FF\u6362\u5FEB\u7167\u3002\n */\nlet downloadsSnapshot: { bySession: Record<string, DownloadEntry> } = { bySession: {} }\n/** \u8BA2\u9605\u4E0B\u8F7D\u72B6\u6001\u53D8\u5316\uFF1B\u8FD4\u56DE\u53D6\u6D88\u8BA2\u9605\u51FD\u6570\uFF08useSyncExternalStore \u5951\u7EA6\uFF09\u3002 */\nfunction subscribeDownloads(listener: () => void): () => void {\n downloadListeners.add(listener)\n return () => { downloadListeners.delete(listener) }\n}\n/** \u5E7F\u64AD\u5FEB\u7167\u53D8\u66F4\uFF1A\u91CD\u5EFA\u5FEB\u7167\u5BF9\u8C61\u5E76\u9010\u4E2A\u901A\u77E5\u8BA2\u9605\u8005\u3002 */\nfunction emitDownloads(): void {\n downloadsSnapshot = { bySession: { ...downloadStates } }\n for (const listener of downloadListeners) listener()\n}\n/** \u5F53\u524D\u5FEB\u7167\uFF08\u5F15\u7528\u7A33\u5B9A\uFF0C\u4EC5\u5728 emitDownloads \u65F6\u66FF\u6362\uFF09\u3002 */\nfunction getDownloadsSnapshot(): { bySession: Record<string, DownloadEntry> } {\n return downloadsSnapshot\n}\n\n/** \u540C\u6E90 Host \u57FA\u5740\uFF08\u8FDE\u63A5\u8F7D\u4F53\u7684 null-origin \u515C\u5E95\uFF09\u3002 */\nfunction hostBase(): string {\n const origin = (globalThis as { location?: { origin?: string } }).location?.origin\n return origin !== undefined && origin !== 'null' ? origin : 'http://dsh.internal'\n}\n\n/** \u4E3A\u67D0\u4E2A\u4F1A\u8BDD id \u751F\u6210\u5B89\u5168\u7684\u6D4F\u89C8\u5668\u4E0B\u8F7D\u6587\u4EF6\u540D\u3002 */\nfunction sessionLogZipFilename(sessionId: string): string {\n return `dsh-session-${String(sessionId).replace(/[^A-Za-z0-9_-]/g, '_')}.zip`\n}\n\n/** /api/session.export \u7684 URL \u2014\u2014 \u6838\u5FC3 API\uFF0C\u4E0E session-log-download \u65E0\u5173\u3002 */\nfunction exportUrl(sessionId: string): string {\n const url = new URL('/api/session.export', hostBase())\n url.searchParams.set('sessionId', sessionId)\n url.searchParams.set('includeDescendants', 'true')\n return url.toString()\n}\n\n/**\n * \u628A\u4E00\u4E2A\u5F52\u6863\u4F1A\u8BDD\u7684\u65E5\u5FD7\u4F5C\u4E3A ZIP \u901A\u8FC7\u6D4F\u89C8\u5668\u4E0B\u8F7D\u7BA1\u7406\u5668\u5BFC\u51FA\u3002\n * \u5148 HEAD \u63A2\u6D4B\u53EF\u7528\u6027\uFF0C\u518D\u7528 <a download> \u89E6\u53D1 GET \u4E0B\u8F7D\uFF1B\u72B6\u6001\u5199\u5165\u79C1\u6709 store\u3002\n * downloadInflight \u505A\u540C\u6B65\u53BB\u91CD\uFF1A\u540C\u4E00\u4F1A\u8BDD\u7684\u7B2C\u4E8C\u6B21\u70B9\u51FB\uFF08\u6E32\u67D3\u7981\u7528\u751F\u6548\u524D\uFF09\n * \u76F4\u63A5\u5FFD\u7565\uFF0C\u907F\u514D\u53CC\u53D1 HEAD+GET\u3002\n *\n * \u5DF2\u77E5\u9650\u5236\uFF1A<a download> \u89E6\u53D1\u7684\u662F\u6D4F\u89C8\u5668\u539F\u751F\u4E0B\u8F7D\uFF0C\u65E0\u6CD5\u56DE\u8BFB GET \u7ED3\u679C\u2014\u2014HEAD\n * \u901A\u8FC7\u4F46 GET \u671F\u95F4\u5931\u8D25\uFF08\u5982\u4F1A\u8BDD\u6070\u5728\u4E24\u6B65\u4E4B\u95F4\u88AB\u5220\uFF09\u65F6\uFF0C\u6D4F\u89C8\u5668\u4F1A\u628A\u9519\u8BEF\u4F53\u5B58\u6210\n * .zip \u4E14 UI \u5DF2\u6807\u8BB0\u6210\u529F\u3002\u4E0E harness \u81EA\u5E26 session-log-download \u63A7\u5236\u5668\u540C\u6B3E\n * \u884C\u4E3A\uFF08\u5E73\u53F0\u7EA7\u9650\u5236\uFF09\uFF0C\u6545\u4E0D\u505A\u4E8C\u6B21\u6821\u9A8C\u3002\n */\nasync function downloadSessionLog(sessionId: string): Promise<void> {\n if (downloadInflight.has(sessionId)) return\n downloadInflight.add(sessionId)\n downloadStates[sessionId] = { status: 'downloading' }\n emitDownloads()\n try {\n const response = await fetch(exportUrl(sessionId), { method: 'HEAD' })\n if (!response.ok) throw new Error(`\u5BFC\u51FA\u5931\u8D25\uFF1AHTTP ${response.status}`)\n const anchor = document.createElement('a')\n anchor.href = exportUrl(sessionId)\n anchor.download = sessionLogZipFilename(sessionId)\n anchor.click()\n downloadStates[sessionId] = { status: 'success' }\n } catch (error: unknown) {\n downloadStates[sessionId] = {\n status: 'error',\n error: error instanceof Error ? error.message : String(error),\n }\n } finally {\n downloadInflight.delete(sessionId)\n }\n emitDownloads()\n}\n\n/**\n * \u6E05\u7406\u5DF2\u4E0D\u5B58\u5728\u4F1A\u8BDD\u7684\u4E0B\u8F7D\u72B6\u6001\uFF1A\u884C\u6D88\u5931\uFF08\u4F1A\u8BDD\u88AB\u5220 / \u79FB\u51FA\u9879\u76EE\uFF09\u540E\uFF0C\u6B8B\u7559\u7684\n * success/error \u6761\u76EE\u4F1A\u5728\u4F1A\u8BDD id \u5C06\u6765\u590D\u7528\u65F6\u9519\u8BEF\u5730\u91CD\u65B0\u51FA\u73B0\uFF1B\u9762\u677F\u5173\u95ED\u65F6\u7EDF\u4E00\n * \u4FEE\u526A\u4E00\u6B21\uFF08\u6A21\u5757\u7EA7 store \u957F\u9A7B\uFF0C\u53EA\u9760\u65B0\u589E\u8986\u76D6\u4F1A\u65E0\u9650\u81A8\u80C0\uFF09\u3002\n */\nfunction pruneDownloads(activeIds: ReadonlySet<string>): void {\n let changed = false\n for (const id of Object.keys(downloadStates)) {\n if (!activeIds.has(id)) {\n delete downloadStates[id]\n changed = true\n }\n }\n if (changed) emitDownloads()\n}\n\nexport function apply(ctx: any): void {\n // \u4E00\u5F20\u5E26\u6807\u8BB0\u7684\u6837\u5F0F\u8868\uFF0C\u8865\u4E0A\u5185\u8054\u6837\u5F0F\u8868\u8FBE\u4E0D\u4E86\u7684 hover / active \u6001\u3002\n // \u4E0E dsh-update \u540C\u6837\u7684\u7EA6\u5B9A\uFF08id + data-plugin\uFF09\uFF1B\u5B88\u536B\u9632\u6B62\u91CD\u590D\u6302\u8F7D\u91CD\u590D\u63D2\u6807\u7B7E\u3002\n const STYLE_ID = 'dsh-archive-style'\n if (typeof document !== 'undefined' && document.getElementById(STYLE_ID) === null) {\n const styleEl = document.createElement('style')\n styleEl.id = STYLE_ID\n styleEl.setAttribute('data-plugin', 'dsh-archive')\n styleEl.textContent = `\n .dsh-archive-badge {\n background: transparent;\n }\n .dsh-archive-badge:hover,\n .dsh-archive-badge[data-active] {\n background: var(--dsw-alias-interactive-bg-hover);\n }\n .dsh-archive-close:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n }\n .dsh-archive-batch:hover:not(:disabled) {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);\n }\n `\n document.head.appendChild(styleEl)\n }\n\n const styles = {\n // \u4FA7\u8FB9\u680F footer \u628A\u52A8\u4F5C\u6392\u6210\u4E00\u884C\u6C34\u5E73 flex\uFF08flex-direction: row, nowrap\uFF09\u3002\n // \u6761\u76EE\u662F\u5360\u6EE1\u6574\u884C\u7684 flex \u9879\uFF08\u53EF\u6536\u7F29\uFF0C\u672A\u6765\u6709\u5144\u5F1F\u52A8\u4F5C\u65F6\u5171\u4EAB\u7A7A\u95F4\u800C\u975E\u88AB\u6324\u51FA\uFF09\uFF0C\n // \u5FBD\u6807\u51E0\u4F55\u4E0E\u8BBE\u7F6E\u89E6\u53D1\u6309\u94AE\u4E00\u81F4\u2014\u2014\u540C\u4E3A 34px \u9AD8\u300112px \u5706\u89D2\u300114px/22px \u5B57\u4F53\uFF0C\n // hover \u9AD8\u4EAE\u5411\u5916\u6EA2\u51FA 4px\u3002rail \u7A84\u680F\u4FDD\u6301 36px \u5706\u5F62\u3002\n layer: {\n position: 'relative',\n flex: '1 1 auto',\n minWidth: 0,\n display: 'flex',\n alignItems: 'center',\n },\n layerRail: {\n flex: 'none',\n },\n badge: {\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n width: 'calc(100% + 8px)',\n height: '34px',\n margin: '4px -4px 4px',\n padding: '6px 10px',\n boxSizing: 'border-box',\n border: 'none',\n borderRadius: '12px',\n color: 'var(--dsw-alias-label-primary)',\n fontFamily: 'inherit',\n fontSize: '14px',\n lineHeight: '22px',\n cursor: 'pointer',\n overflow: 'hidden',\n whiteSpace: 'nowrap',\n },\n badgeRail: {\n justifyContent: 'center',\n gap: '0',\n width: '36px',\n height: '36px',\n margin: '8px 0 10px',\n padding: '0',\n borderRadius: '50%',\n },\n badgeLabel: {\n flex: 'none',\n minWidth: 0,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n },\n badgeCount: {\n flex: 'none',\n marginLeft: 'auto',\n color: 'var(--dsw-alias-label-tertiary)',\n fontSize: '12px',\n lineHeight: '16px',\n fontVariantNumeric: 'tabular-nums',\n },\n panel: {\n position: 'fixed',\n left: '12px',\n bottom: '128px',\n zIndex: 30,\n display: 'flex',\n flexDirection: 'column',\n width: '420px',\n maxWidth: 'calc(100vw - 24px)',\n maxHeight: '60vh',\n overflow: 'hidden',\n border: '1px solid var(--dsw-alias-border-l1)',\n borderRadius: '12px',\n background: 'var(--dsw-alias-bg-base)',\n boxShadow: 'var(--dsw-shadow-lv2)',\n },\n panelHeader: {\n flex: 'none',\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n minHeight: '44px',\n padding: '0 12px',\n boxSizing: 'border-box',\n borderBottom: '1px solid var(--dsw-alias-border-l2)',\n },\n panelTitle: {\n fontSize: '13px',\n fontWeight: 500,\n lineHeight: '20px',\n color: 'var(--dsw-alias-label-primary)',\n },\n panelProject: {\n minWidth: 0,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n fontSize: '12px',\n color: 'var(--dsw-alias-label-tertiary)',\n },\n close: {\n flex: 'none',\n marginLeft: 'auto',\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '24px',\n height: '24px',\n padding: 0,\n border: 'none',\n borderRadius: '6px',\n background: 'transparent',\n color: 'var(--dsw-alias-label-tertiary)',\n fontSize: '14px',\n cursor: 'pointer',\n },\n batchDelete: {\n flex: 'none',\n display: 'inline-flex',\n alignItems: 'center',\n height: '24px',\n padding: '0 12px',\n boxSizing: 'border-box',\n borderRadius: '6px',\n border: '1px solid var(--dsw-alias-state-error-primary)',\n background: 'transparent',\n color: 'var(--dsw-alias-state-error-primary)',\n fontSize: '12px',\n lineHeight: '1',\n cursor: 'pointer',\n },\n footerHint: {\n flex: 1,\n minWidth: 0,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n color: 'var(--dsw-alias-label-tertiary)',\n fontSize: '12px',\n lineHeight: '16px',\n },\n panelFooter: {\n flex: 'none',\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n minHeight: '44px',\n padding: '0 12px',\n boxSizing: 'border-box',\n borderTop: '1px solid var(--dsw-alias-border-l2)',\n },\n notice: {\n padding: '8px 12px',\n marginBottom: '8px',\n borderRadius: '8px',\n border: '1px solid var(--dsw-alias-border-l2)',\n background: 'var(--dsw-alias-bg-base)',\n color: 'var(--dsw-alias-label-primary)',\n fontSize: '12px',\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-all',\n },\n panelBody: {\n flex: 1,\n minHeight: 0,\n overflowY: 'auto',\n padding: '8px 12px 12px',\n },\n hint: {\n color: 'var(--dsw-alias-label-tertiary)',\n fontSize: '12px',\n lineHeight: '18px',\n margin: '4px 0 8px',\n },\n error: {\n padding: '8px 12px',\n marginBottom: '8px',\n borderRadius: '8px',\n border: '1px solid var(--dsw-alias-state-error-primary)',\n color: 'var(--dsw-alias-state-error-primary)',\n fontSize: '12px',\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-all',\n },\n row: {\n display: 'flex',\n alignItems: 'center',\n gap: '12px',\n padding: '10px 12px',\n marginBottom: '8px',\n borderRadius: '8px',\n border: '1px solid var(--dsw-alias-border-l2)',\n background: 'var(--dsw-alias-bg-base)',\n },\n rowMain: {\n flex: 1,\n minWidth: 0,\n },\n rowTitle: {\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n cursor: 'pointer',\n fontSize: '13px',\n color: 'var(--dsw-alias-label-primary)',\n },\n rowMeta: {\n marginTop: '2px',\n color: 'var(--dsw-alias-label-tertiary)',\n fontSize: '12px',\n },\n buttonBase: {\n flex: 'none',\n padding: '4px 12px',\n borderRadius: '6px',\n fontSize: '12px',\n cursor: 'pointer',\n background: 'transparent',\n },\n empty: {\n padding: '24px 0',\n textAlign: 'center',\n color: 'var(--dsw-alias-label-tertiary)',\n fontSize: '12px',\n },\n } as const\n\n /** \u4FA7\u8FB9\u680F footer \u7684\u5F52\u6863\u52A8\u4F5C\uFF1A\u5FBD\u6807 + \u6D6E\u52A8\u9762\u677F\u3002 */\n function ArchiveFooterAction(props: { wide?: boolean }): any {\n const wide = props.wide !== false\n // \u8BA2\u9605\u8FD0\u884C\u65F6 store\uFF1Aworkspaces\uFF08\u5F52\u6863\u96C6\u5408 / \u9879\u76EE\u89E3\u6790\uFF09\u3001sessions\uFF08\u6807\u9898\uFF09\u3002\n const workspaces = useSyncExternalStore(\n ctx.workspaces.list.subscribe as (listener: () => void) => () => void,\n () => ctx.workspaces.list.getSnapshot() as WorkspaceListSnapshot,\n )\n const sessions = useSyncExternalStore(\n ctx.sessions.list.subscribe as (listener: () => void) => () => void,\n () => ctx.sessions.list.getSnapshot() as SessionListSnapshot,\n )\n const [open, setOpen] = useState(false)\n const [busy, setBusy] = useState<{ id: string; action: 'restore' | 'delete' } | null>(null)\n const [batchBusy, setBatchBusy] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [notice, setNotice] = useState<string | null>(null)\n const panelRef = useRef<HTMLDivElement | null>(null)\n const triggerRef = useRef<HTMLButtonElement | null>(null)\n // \u540C\u6B65\u5728\u9014\u5B88\u536B\uFF1AsetBusy \u8981\u7B49\u4E0B\u4E00\u6B21\u6E32\u67D3\u624D\u7981\u7528\u6309\u94AE\uFF0C\u540C\u4E00 tick \u91CC\u7684\u7B2C\u4E8C\u6B21\n // \u70B9\u51FB\u4F1A\u91CD\u65B0\u8FDB\u5165 run/deleteAll\uFF08React 18 \u6279\u5904\u7406\uFF09\u2014\u2014\u7528 ref \u5728\u8FDB\u5165\u65F6\n // \u7ACB\u5373\u62E6\u622A\uFF0C\u675C\u7EDD\u53CC\u51FB\u53CC\u53D1\uFF08\u6279\u91CF\u5220\u9664\u4F1A\u53CC\u8BA1\u6570\u3001\u4E0B\u8F7D\u4F1A\u53CC\u53D1 HEAD+GET\uFF09\u3002\n const busyRef = useRef(false)\n const batchBusyRef = useRef(false)\n\n // \u4E0D\u4F9D\u8D56\u6253\u5F00\u4F1A\u8BDD\u89E3\u6790\u5F53\u524D\u9879\u76EE\uFF1A\u6700\u8FD1\u6D3B\u8DC3 workspace \u2192 \u6301\u6709\u5F53\u524D\u4F1A\u8BDD\u7684\n // workspace \u2192 \u5F53\u524D\u4F1A\u8BDD cwd \u5BF9\u5E94\u7684 workspace\u3002\n const workspace = useMemo((): WorkspaceRow | undefined => {\n const items = workspaces?.items\n if (items === undefined || items.length === 0) return undefined\n const recent = workspaces?.recentWorkspaceId\n if (recent !== undefined) {\n const byRecent = items.find(item => item.workspaceId === recent)\n if (byRecent !== undefined) return byRecent\n }\n const current = sessions?.current\n if (current !== undefined) {\n const byMembership = items.find(item => item.sessionIds.includes(current))\n if (byMembership !== undefined) return byMembership\n const currentCwd = sessions?.byId[current]?.cwd\n if (currentCwd !== undefined) {\n const byPath = items.find(item => item.path === currentCwd)\n if (byPath !== undefined) return byPath\n }\n }\n return undefined\n }, [workspaces, sessions])\n\n const projectLabel = workspace?.title\n\n // \u53EA\u53D6\u5F53\u524D\u9879\u76EE\u7684\u5F52\u6863\uFF1Aworkspace \u8D26\u76EE\u6210\u5458\uFF0C\u6216\u5B58\u50A8 cwd \u7B49\u4E8E\u9879\u76EE\u8DEF\u5F84\u3002\n const rows = useMemo((): ArchiveRow[] => {\n const archived = workspaces?.archivedSessionIds\n if (archived === undefined || archived.length === 0 || workspace === undefined) return []\n const result: ArchiveRow[] = []\n for (const id of archived) {\n const summary = sessions?.byId[id]\n const inAccount = workspace.sessionIds.includes(id)\n const sameProject = summary?.cwd !== undefined && summary.cwd === workspace.path\n if (!inAccount && !sameProject) continue\n result.push({\n id,\n title: summary?.displayTitle ?? id,\n updatedAt: summary?.updatedAt,\n })\n }\n return result\n }, [workspaces, sessions, workspace])\n\n // \u4F1A\u8BDD\u5217\u8868\u53D8\u5316\u65F6\u4FEE\u526A\u4E0B\u8F7D\u72B6\u6001\uFF1A\u5DF2\u4E0D\u5728\u5F53\u524D\u9879\u76EE\u91CC\u7684\u4F1A\u8BDD\uFF08\u88AB\u5220/\u79FB\u51FA\uFF09\u7684\n // \u6B8B\u7559 success/error \u6761\u76EE\u6E05\u6389\uFF0C\u907F\u514D\u72B6\u6001\u65E0\u9650\u5806\u79EF\u6216 id \u590D\u7528\u65F6\u8BEF\u663E\u793A\u3002\n useEffect(() => {\n pruneDownloads(new Set(rows.map(row => row.id)))\n }, [rows])\n\n // Escape \u5173\u95ED\uFF1B\u70B9\u51FB\u9762\u677F\u4E0E\u89E6\u53D1\u6309\u94AE\u4E4B\u5916\u7684\u533A\u57DF\u5173\u95ED\u3002\n // \uFF08\u89E6\u53D1\u6309\u94AE\u88AB\u6392\u9664\u5728\u5916\uFF0C\u8BA9\u70B9\u51FB\u4E8B\u4EF6\u81EA\u5DF1\u8D1F\u8D23\u5F00\u5173\u2014\u2014\u5426\u5219 mousedown \u5173\u95ED\u540E\n // \u7D27\u63A5\u7684 click \u5207\u6362\u4F1A\u7ACB\u523B\u91CD\u65B0\u6253\u5F00\u3002\uFF09\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') setOpen(false)\n }\n const onPointerDown = (event: MouseEvent): void => {\n const target = event.target as Node | null\n if (target === null) return\n if (panelRef.current?.contains(target) === true) return\n if (triggerRef.current?.contains(target) === true) return\n setOpen(false)\n }\n document.addEventListener('keydown', onKey)\n document.addEventListener('mousedown', onPointerDown)\n return () => {\n document.removeEventListener('keydown', onKey)\n document.removeEventListener('mousedown', onPointerDown)\n }\n }, [open])\n\n // \u6BCF\u4E2A\u4F1A\u8BDD\u7684\u5B9E\u65F6\u5BFC\u51FA\u72B6\u6001\uFF08\u79C1\u6709 store\uFF09\uFF0C\u884C\u6309\u94AE\u636E\u6B64\u663E\u793A\u300C\u4E0B\u8F7D\u4E2D\u2026\u300D\u5E76\u7981\u7528\uFF0C\n // \u51FA\u9519\u65F6\u53D8\u6210\u300C\u91CD\u8BD5\u300D\u3002\n const downloads = useSyncExternalStore(\n (listener: () => void) => subscribeDownloads(listener),\n () => getDownloadsSnapshot(),\n )\n\n /** \u6267\u884C\u6062\u590D / \u5220\u9664\uFF1B\u5931\u8D25\u628A\u53EF\u8BFB\u9519\u8BEF\u5199\u5165\u9762\u677F\u3002 */\n const run = async (action: 'restore' | 'delete', id: string): Promise<void> => {\n if (busyRef.current) return\n busyRef.current = true\n setError(null)\n setNotice(null)\n setBusy({ id, action })\n try {\n await callHost(action, { sessionId: id })\n // \u5220\u9664\u540E\u4E3B\u52A8\u5237\u65B0\uFF1Asessions \u5217\u8868\uFF08\u4F1A\u8BDD\u6D88\u5931\uFF09+ workspaces \u5217\u8868\n // \uFF08workspace \u8D26\u76EE\u5DF2\u89E3\u9664\uFF0C\u907F\u514D\u4FA7\u8FB9\u680F\u6B8B\u7559\u5E7D\u7075\u6761\u76EE\uFF09\u3002\n // \u6062\u590D\u4E5F\u5237\u65B0 workspaces\uFF1A\u867D\u7136\u5BBF\u4E3B\u5199\u94FE\u4F1A\u5E7F\u64AD archived-sessions-changed\n // \u9A71\u52A8\u5B9E\u65F6\u5237\u65B0\uFF0C\u4F46\u90A3\u4F9D\u8D56 harness \u5185\u90E8\u4E8B\u4EF6\uFF0C\u8FD9\u91CC\u4E3B\u52A8\u5237\u65B0\u515C\u5E95\u3002\n ctx.sessions.refresh?.().catch?.(() => {})\n ctx.workspaces.refresh?.().catch?.(() => {})\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err))\n } finally {\n busyRef.current = false\n setBusy(null)\n }\n }\n\n /** \u6062\u590D\u5355\u4E2A\u5F52\u6863\u4F1A\u8BDD\uFF08\u56DE\u5230\u4FA7\u8FB9\u680F\u539F\u4F4D\u7F6E\uFF09\u3002 */\n const restore = (id: string): void => { void run('restore', id) }\n /** \u786C\u5220\u9664\u5355\u4E2A\u5F52\u6863\u4F1A\u8BDD\uFF08\u542B\u4E8C\u6B21\u786E\u8BA4\uFF0C\u786E\u8BA4\u6587\u6848\u8BF4\u660E\u4E0D\u53EF\u6062\u590D\uFF09\u3002 */\n const remove = (id: string, title: string): void => {\n const confirmed = window.confirm(\n `\u786E\u5B9A\u8981\u6C38\u4E45\u5220\u9664\u4F1A\u8BDD\u300C${title}\u300D\u5417\uFF1F\\n\\n\u8FD9\u662F\u786C\u5220\u9664\uFF1A\u4F1A\u8BDD\u7684\u6240\u6709\u6587\u4EF6\u90FD\u4F1A\u88AB\u5220\u9664\uFF0C\u65E0\u6CD5\u6062\u590D\u3002`,\n )\n if (confirmed) void run('delete', id)\n }\n /**\n * \u4E00\u952E\u5220\u9664\uFF1A\u628A\u5F53\u524D\u9879\u76EE\u5168\u90E8\u5DF2\u5F52\u6863\u4F1A\u8BDD\u4E00\u6B21\u6027\u786C\u5220\u9664\u3002\n * \u5BBF\u4E3B\u5355\u6B21\u4E0A\u9650 500 \u4E2A id\uFF08\u8D85\u9650\u6574\u6279 400\uFF09\uFF0C\u8FD9\u91CC\u6309 200/\u6279\u5206\u7247\u4E32\u884C\uFF0C\n * \u4FDD\u8BC1\u5F52\u6863\u6570 >500 \u7684\u9879\u76EE\u4E5F\u80FD\u6574\u6279\u5220\u5B8C\u800C\u975E\u6574\u6279\u5931\u8D25\u3002\n */\n const deleteAll = (): void => {\n if (rows.length === 0) return\n const confirmed = window.confirm(\n `\u786E\u5B9A\u8981\u4E00\u952E\u5220\u9664\u5168\u90E8 ${rows.length} \u4E2A\u5DF2\u5F52\u6863\u4F1A\u8BDD\u5417\uFF1F\\n\\n\u8FD9\u662F\u786C\u5220\u9664\uFF1A\u6BCF\u4E2A\u4F1A\u8BDD\u7684\u6240\u6709\u6587\u4EF6\u90FD\u4F1A\u88AB\u5220\u9664\uFF0C\u65E0\u6CD5\u6062\u590D\u3002\\n\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u4F1A\u88AB\u8DF3\u8FC7\u3002`,\n )\n if (!confirmed) return\n if (batchBusyRef.current) return\n batchBusyRef.current = true\n setError(null)\n setNotice(null)\n setBatchBusy(true)\n void (async () => {\n try {\n const ids = rows.map(row => row.id)\n const total = ids.length\n let deleted = 0\n let skipped = 0\n let failed = 0\n const failures: Array<{ sessionId: string; error: string }> = []\n for (let offset = 0; offset < ids.length; offset += DELETE_BATCH_SIZE) {\n const payload = await callHost('delete-all', { sessionIds: ids.slice(offset, offset + DELETE_BATCH_SIZE) }) as {\n total?: number\n deleted?: number\n skipped?: number\n failed?: number\n failures?: Array<{ sessionId: string; error: string }>\n }\n deleted += payload.deleted ?? 0\n skipped += payload.skipped ?? 0\n failed += payload.failed ?? 0\n const chunkFailures = payload.failures\n if (chunkFailures !== undefined && chunkFailures.length > 0) failures.push(...chunkFailures)\n }\n const parts: string[] = [`\u5DF2\u5220\u9664 ${deleted} \u4E2A`]\n if (skipped > 0) parts.push(`\u8DF3\u8FC7\u8FD0\u884C\u4E2D ${skipped} \u4E2A`)\n if (failed > 0) parts.push(`\u5931\u8D25 ${failed} \u4E2A`)\n let message = parts.join('\uFF0C')\n if (failures.length > 0) {\n message += `\uFF1A${failures.map(item => `${item.sessionId}\uFF08${item.error}\uFF09`).join('\uFF1B')}`\n }\n setNotice(message)\n // \u5220\u9664\u540E\u4E3B\u52A8\u5237\u65B0\uFF1Asessions \u5217\u8868\uFF08\u4F1A\u8BDD\u6D88\u5931\uFF09+ workspaces \u5217\u8868\n // \uFF08workspace \u8D26\u76EE\u5DF2\u89E3\u9664\uFF0C\u907F\u514D\u4FA7\u8FB9\u680F\u6B8B\u7559\u5E7D\u7075\u6761\u76EE\uFF09\u3002\n ctx.sessions.refresh?.().catch?.(() => {})\n ctx.workspaces.refresh?.().catch?.(() => {})\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err))\n } finally {\n batchBusyRef.current = false\n setBatchBusy(false)\n }\n })()\n }\n /** \u6253\u5F00\u4F1A\u8BDD\uFF08\u4ECE\u9762\u677F\u5207\u5230\u4F1A\u8BDD\u533A\uFF09\uFF1B\u5931\u8D25\u901A\u5E38\u662F\u56E0\u4E3A\u5217\u8868\u5237\u65B0\u4E2D\u884C\u5DF2\u6D88\u5931\u3002 */\n const openSession = (id: string): void => {\n try {\n ctx.sessions.open(id)\n setOpen(false)\n } catch {\n // \u884C\u5728\u5217\u8868\u5237\u65B0\u65F6\u6D88\u5931\u7684\u7ADE\u6001\uFF1A\u4E0B\u4E00\u6B21\u6E32\u67D3\u81EA\u7136\u6E05\u6389\u3002\n }\n }\n\n return (\n <div style={wide ? styles.layer : { ...styles.layer, ...styles.layerRail }}>\n {open && (\n <section\n ref={panelRef}\n style={styles.panel}\n aria-label=\"\u5DF2\u5F52\u6863\u4F1A\u8BDD\"\n >\n <header style={styles.panelHeader}>\n <span style={styles.panelTitle}>\u5DF2\u5F52\u6863\u4F1A\u8BDD</span>\n {projectLabel !== undefined\n ? <span style={styles.panelProject}>\u00B7 {projectLabel}</span>\n : null}\n <button\n type=\"button\"\n className=\"dsh-archive-close\"\n style={styles.close}\n aria-label=\"\u5173\u95ED\"\n onClick={() => { setOpen(false) }}\n >\n \u2715\n </button>\n </header>\n <div style={styles.panelBody}>\n <div style={styles.hint}>\n \u4EC5\u663E\u793A\u5F53\u524D\u9879\u76EE\u7684\u5F52\u6863\u3002\u4E0B\u8F7D\u4F1A\u5BFC\u51FA\u8BE5\u4F1A\u8BDD\u7684\u65E5\u5FD7 ZIP\uFF1B\u6062\u590D\u4F1A\u628A\u4F1A\u8BDD\u653E\u56DE\u4FA7\u8FB9\u680F\uFF1B\u5220\u9664\u4F1A\u6C38\u4E45\u79FB\u9664\u4F1A\u8BDD\u7684\u5168\u90E8\u6587\u4EF6\uFF0C\u65E0\u6CD5\u6062\u590D\u3002\n </div>\n {error !== null ? <div style={styles.error}>{error}</div> : null}\n {notice !== null ? <div style={styles.notice}>{notice}</div> : null}\n {rows.length === 0\n ? <div style={styles.empty}>\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u5DF2\u5F52\u6863\u7684\u4F1A\u8BDD</div>\n : null}\n {rows.map(row => {\n const rowBusy = busy !== null && busy.id === row.id\n const anyBusy = busy !== null || batchBusy\n const time = formatTime(row.updatedAt)\n const dlEntry = downloads?.bySession?.[row.id]\n const downloading = dlEntry?.status === 'downloading'\n const dlError = dlEntry?.status === 'error' ? (dlEntry.error ?? '\u5BFC\u51FA\u5931\u8D25') : null\n return (\n <div key={row.id} style={styles.row}>\n <div style={styles.rowMain}>\n <div\n style={styles.rowTitle}\n title={`\u6253\u5F00\u4F1A\u8BDD ${row.title}`}\n onClick={() => { openSession(row.id) }}\n >\n {row.title}\n </div>\n {time !== '' ? <div style={styles.rowMeta}>{time}</div> : null}\n </div>\n <button\n type=\"button\"\n title={dlError ?? '\u4E0B\u8F7D\u4F1A\u8BDD\u65E5\u5FD7 (ZIP)'}\n disabled={anyBusy || downloading}\n style={{\n ...styles.buttonBase,\n border: '1px solid var(--dsw-alias-border-l2)',\n color: 'var(--dsw-alias-label-primary)',\n opacity: anyBusy || downloading ? 0.5 : 1,\n }}\n onClick={() => { void downloadSessionLog(row.id) }}\n >\n {downloading ? '\u4E0B\u8F7D\u4E2D\u2026' : dlError !== null ? '\u91CD\u8BD5' : '\u4E0B\u8F7D'}\n </button>\n <button\n type=\"button\"\n disabled={anyBusy}\n style={{\n ...styles.buttonBase,\n border: '1px solid var(--dsw-alias-border-l2)',\n color: 'var(--dsw-alias-label-primary)',\n opacity: anyBusy && !rowBusy ? 0.5 : 1,\n }}\n onClick={() => { restore(row.id) }}\n >\n {rowBusy && busy?.action === 'restore' ? '\u6062\u590D\u4E2D\u2026' : '\u6062\u590D'}\n </button>\n <button\n type=\"button\"\n disabled={anyBusy}\n style={{\n ...styles.buttonBase,\n border: '1px solid var(--dsw-alias-state-error-primary)',\n color: 'var(--dsw-alias-state-error-primary)',\n opacity: anyBusy && !rowBusy ? 0.5 : 1,\n }}\n onClick={() => { remove(row.id, row.title) }}\n >\n {rowBusy && busy?.action === 'delete' ? '\u5220\u9664\u4E2D\u2026' : '\u5220\u9664'}\n </button>\n </div>\n )\n })}\n </div>\n <footer style={styles.panelFooter}>\n <span style={styles.footerHint}>\n {rows.length === 0\n ? '\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u5DF2\u5F52\u6863\u7684\u4F1A\u8BDD'\n : `\u5171 ${rows.length} \u4E2A\u5F52\u6863\u4F1A\u8BDD\uFF08\u8FD0\u884C\u4E2D\u7684\u4F1A\u8DF3\u8FC7\uFF09`}\n </span>\n <button\n type=\"button\"\n className=\"dsh-archive-batch\"\n style={{\n ...styles.batchDelete,\n opacity: batchBusy || busy !== null || rows.length === 0 ? 0.5 : 1,\n }}\n disabled={batchBusy || busy !== null || rows.length === 0}\n title={rows.length === 0\n ? '\u5F53\u524D\u9879\u76EE\u6CA1\u6709\u5DF2\u5F52\u6863\u7684\u4F1A\u8BDD'\n : `\u4E00\u952E\u5220\u9664\u5F53\u524D\u9879\u76EE\u5168\u90E8 ${rows.length} \u4E2A\u5DF2\u5F52\u6863\u4F1A\u8BDD\uFF08\u8FD0\u884C\u4E2D\u7684\u4F1A\u8DF3\u8FC7\uFF09`}\n onClick={() => { deleteAll() }}\n >\n {batchBusy ? '\u5220\u9664\u4E2D\u2026' : '\u4E00\u952E\u5220\u9664'}\n </button>\n </footer>\n </section>\n )}\n <button\n ref={triggerRef}\n type=\"button\"\n className=\"dsh-archive-badge\"\n style={wide ? styles.badge : { ...styles.badge, ...styles.badgeRail }}\n data-active={open || undefined}\n aria-label={`\u5DF2\u5F52\u6863\u4F1A\u8BDD\uFF08${String(rows.length)}\uFF09`}\n aria-expanded={open}\n title={wide ? undefined : `\u5DF2\u5F52\u6863\u4F1A\u8BDD\uFF08${String(rows.length)}\uFF09`}\n onClick={() => { setOpen(value => !value) }}\n >\n <ArchiveIcon size={wide ? 16 : 18} />\n {wide && (\n <>\n <span style={styles.badgeLabel}>\u5F52\u6863</span>\n <span style={styles.badgeCount}>{rows.length}</span>\n </>\n )}\n </button>\n </div>\n )\n }\n\n // \u6CE8\u518C\u5230\u4FA7\u8FB9\u680F footer \u52A8\u4F5C\u5217\u8868\uFF08id: archive-panel\uFF09\u3002\n ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register(\n {\n name: 'sidebar.footer.action',\n id: 'archive-panel',\n },\n ArchiveFooterAction,\n ))\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname } from "node:path";
|
|
4
|
+
var name = "dsh-archive";
|
|
5
|
+
var inject = ["webServer", "workspaceRegistry", "sessionPersistence"];
|
|
6
|
+
var HttpError = class extends Error {
|
|
7
|
+
constructor(status, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.name = "HttpError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
function readJsonBody(req) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let size = 0;
|
|
17
|
+
let settled = false;
|
|
18
|
+
const fail = (error) => {
|
|
19
|
+
if (settled) return;
|
|
20
|
+
settled = true;
|
|
21
|
+
reject(error);
|
|
22
|
+
};
|
|
23
|
+
req.on("data", (chunk) => {
|
|
24
|
+
if (settled) return;
|
|
25
|
+
size += chunk.length;
|
|
26
|
+
if (size > 1e6) {
|
|
27
|
+
fail(new HttpError(413, "request body too large"));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
chunks.push(chunk);
|
|
31
|
+
});
|
|
32
|
+
req.on("end", () => {
|
|
33
|
+
if (settled) return;
|
|
34
|
+
settled = true;
|
|
35
|
+
try {
|
|
36
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
37
|
+
resolve(text === "" ? {} : JSON.parse(text));
|
|
38
|
+
} catch {
|
|
39
|
+
reject(new HttpError(400, "request body is not valid JSON"));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
req.on("error", () => fail(new HttpError(400, "request stream failed")));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function sendJson(res, status, value) {
|
|
46
|
+
const body = JSON.stringify(value);
|
|
47
|
+
res.writeHead(status, {
|
|
48
|
+
"content-type": "application/json; charset=utf-8",
|
|
49
|
+
"cache-control": "no-store"
|
|
50
|
+
});
|
|
51
|
+
res.end(body);
|
|
52
|
+
}
|
|
53
|
+
function requireSessionId(body) {
|
|
54
|
+
const sessionId = body.sessionId;
|
|
55
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > 512) {
|
|
56
|
+
throw new HttpError(400, "sessionId must be a non-empty string");
|
|
57
|
+
}
|
|
58
|
+
return sessionId;
|
|
59
|
+
}
|
|
60
|
+
function requireSessionIds(body) {
|
|
61
|
+
const raw = body.sessionIds;
|
|
62
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
63
|
+
throw new HttpError(400, "sessionIds must be a non-empty array");
|
|
64
|
+
}
|
|
65
|
+
if (raw.length > 500) {
|
|
66
|
+
throw new HttpError(400, "sessionIds must not exceed 500 entries");
|
|
67
|
+
}
|
|
68
|
+
const seen = /* @__PURE__ */ new Set();
|
|
69
|
+
const ids = [];
|
|
70
|
+
for (const value of raw) {
|
|
71
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 512) {
|
|
72
|
+
throw new HttpError(400, "sessionIds must contain only non-empty strings");
|
|
73
|
+
}
|
|
74
|
+
if (seen.has(value)) continue;
|
|
75
|
+
seen.add(value);
|
|
76
|
+
ids.push(value);
|
|
77
|
+
}
|
|
78
|
+
return ids;
|
|
79
|
+
}
|
|
80
|
+
async function unarchiveSession(registry, sessionId) {
|
|
81
|
+
if (!registry.archivedSessionIds.includes(sessionId)) return { changed: false };
|
|
82
|
+
const internals = registry;
|
|
83
|
+
if (typeof internals.enqueueOperation !== "function" || typeof internals.requireState !== "function" || typeof internals.setState !== "function") {
|
|
84
|
+
throw new HttpError(
|
|
85
|
+
503,
|
|
86
|
+
"workspace registry write chain is unavailable (harness API drift); restore is disabled until this plugin is updated"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
let changed = false;
|
|
90
|
+
await internals.enqueueOperation(async () => {
|
|
91
|
+
const state = internals.requireState();
|
|
92
|
+
if (!state.archivedSessionIds.includes(sessionId)) return;
|
|
93
|
+
await internals.setState({
|
|
94
|
+
...state,
|
|
95
|
+
archivedSessionIds: state.archivedSessionIds.filter((id) => id !== sessionId)
|
|
96
|
+
});
|
|
97
|
+
changed = true;
|
|
98
|
+
});
|
|
99
|
+
return { changed };
|
|
100
|
+
}
|
|
101
|
+
function apply(ctx) {
|
|
102
|
+
const handler = async (req, res) => {
|
|
103
|
+
const pathname = new URL(req.url ?? "/", "http://x").pathname;
|
|
104
|
+
const route = pathname === "/dsh-archive/restore" ? "restore" : pathname === "/dsh-archive/delete" ? "delete" : pathname === "/dsh-archive/delete-all" ? "delete-all" : void 0;
|
|
105
|
+
try {
|
|
106
|
+
if (route === void 0) throw new HttpError(404, `unknown route ${JSON.stringify(pathname)}`);
|
|
107
|
+
if (req.method !== "POST") throw new HttpError(405, "method not allowed; use POST");
|
|
108
|
+
const mediaType = (req.headers["content-type"] ?? "").split(";", 1)[0]?.trim().toLowerCase();
|
|
109
|
+
if (mediaType !== "application/json") throw new HttpError(415, "content type must be application/json");
|
|
110
|
+
const body = await readJsonBody(req);
|
|
111
|
+
if (route === "restore") {
|
|
112
|
+
await handleRestore(ctx, requireSessionId(body), res);
|
|
113
|
+
} else if (route === "delete") {
|
|
114
|
+
await handleDelete(ctx, requireSessionId(body), res);
|
|
115
|
+
} else {
|
|
116
|
+
await handleDeleteAll(ctx, requireSessionIds(body), res);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof HttpError) {
|
|
120
|
+
sendJson(res, error.status, { ok: false, error: error.message });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
|
|
124
|
+
sendJson(res, 500, { ok: false, error: "internal error" });
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
ctx.effect(
|
|
128
|
+
() => ctx.webServer.register({ kind: "prefix", path: "/dsh-archive", handler }),
|
|
129
|
+
"dsh-archive: routes"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
async function handleRestore(ctx, sessionId, res) {
|
|
133
|
+
const registry = ctx.workspaceRegistry;
|
|
134
|
+
const { changed } = await unarchiveSession(registry, sessionId);
|
|
135
|
+
sendJson(res, 200, {
|
|
136
|
+
ok: true,
|
|
137
|
+
sessionId,
|
|
138
|
+
changed,
|
|
139
|
+
archivedSessionIds: [...registry.archivedSessionIds]
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function deleteSessionCore(ctx, sessionId) {
|
|
143
|
+
const registry = ctx.workspaceRegistry;
|
|
144
|
+
const headers = await ctx.sessionPersistence.list();
|
|
145
|
+
const header = headers.find((candidate) => candidate.id === sessionId);
|
|
146
|
+
let targetDir = null;
|
|
147
|
+
if (header !== void 0) {
|
|
148
|
+
const location = ctx.sessionPersistence.locate(header);
|
|
149
|
+
if (location !== void 0 && typeof location.path === "string" && location.path !== "") {
|
|
150
|
+
const dir = dirname(location.path);
|
|
151
|
+
const dirName = basename(dir);
|
|
152
|
+
if (dirName !== sessionId && dirName !== encodeURIComponent(sessionId)) {
|
|
153
|
+
throw new HttpError(500, `refusing to remove unexpected session directory ${JSON.stringify(dir)}`);
|
|
154
|
+
}
|
|
155
|
+
targetDir = dir;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const { changed: unarchived } = await unarchiveSession(registry, sessionId);
|
|
159
|
+
let detachedWorkspaces = 0;
|
|
160
|
+
const detachErrors = [];
|
|
161
|
+
for (const workspace of registry.list()) {
|
|
162
|
+
if (!workspace.sessionIds.includes(sessionId)) continue;
|
|
163
|
+
try {
|
|
164
|
+
await workspace.detachSession(sessionId);
|
|
165
|
+
detachedWorkspaces += 1;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
detachErrors.push(`workspace ${workspace.id}: ${String(error)}`);
|
|
168
|
+
ctx.logger.warn(new Error(`archive-tab: detach failed for '${sessionId}' in workspace '${workspace.id}': ${String(error)}`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
let removedFiles = false;
|
|
172
|
+
if (targetDir !== null) {
|
|
173
|
+
const dirName = basename(targetDir);
|
|
174
|
+
if (dirName !== sessionId && dirName !== encodeURIComponent(sessionId)) {
|
|
175
|
+
throw new HttpError(500, `refusing to remove unexpected session directory ${JSON.stringify(targetDir)}`);
|
|
176
|
+
}
|
|
177
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
178
|
+
removedFiles = true;
|
|
179
|
+
}
|
|
180
|
+
let clearedProjectionCache = false;
|
|
181
|
+
const cache = ctx.get("sessionProjectionCache");
|
|
182
|
+
const table = cache?.table;
|
|
183
|
+
if (table !== void 0 && typeof table.delete === "function") {
|
|
184
|
+
try {
|
|
185
|
+
clearedProjectionCache = await table.delete(sessionId);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
ctx.logger.warn(new Error(`archive-tab: projection-cache cleanup failed for '${sessionId}': ${String(error)}`));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
removedFiles,
|
|
192
|
+
detachedWorkspaces,
|
|
193
|
+
unarchived,
|
|
194
|
+
clearedProjectionCache,
|
|
195
|
+
persisted: header !== void 0,
|
|
196
|
+
detachErrors
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function handleDelete(ctx, sessionId, res) {
|
|
200
|
+
const live = ctx.get("sessions")?.get(sessionId);
|
|
201
|
+
if (live !== void 0) {
|
|
202
|
+
throw new HttpError(409, "session is live in this process; close it before deleting");
|
|
203
|
+
}
|
|
204
|
+
const result = await deleteSessionCore(ctx, sessionId);
|
|
205
|
+
sendJson(res, 200, {
|
|
206
|
+
ok: true,
|
|
207
|
+
sessionId,
|
|
208
|
+
...result
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function handleDeleteAll(ctx, sessionIds, res) {
|
|
212
|
+
let deleted = 0;
|
|
213
|
+
let skipped = 0;
|
|
214
|
+
let failed = 0;
|
|
215
|
+
const failures = [];
|
|
216
|
+
for (const sessionId of sessionIds) {
|
|
217
|
+
const live = ctx.get("sessions")?.get(sessionId);
|
|
218
|
+
if (live !== void 0) {
|
|
219
|
+
skipped += 1;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
await deleteSessionCore(ctx, sessionId);
|
|
224
|
+
deleted += 1;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
failed += 1;
|
|
227
|
+
const message = error instanceof HttpError ? error.message : String(error);
|
|
228
|
+
failures.push({ sessionId, error: message });
|
|
229
|
+
ctx.logger.warn(new Error(`archive-tab: batch delete failed for '${sessionId}': ${message}`));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
sendJson(res, 200, {
|
|
233
|
+
ok: true,
|
|
234
|
+
total: sessionIds.length,
|
|
235
|
+
deleted,
|
|
236
|
+
skipped,
|
|
237
|
+
failed,
|
|
238
|
+
failures
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
export {
|
|
242
|
+
apply,
|
|
243
|
+
inject,
|
|
244
|
+
name
|
|
245
|
+
};
|
|
246
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * dsh-archive \u2014 \u5BBF\u4E3B\u534A\uFF08Host\uFF09\u3002\n *\n * \u5728 DSH web \u670D\u52A1\u4E0A\u6CE8\u518C\u4E24\u4E2A JSON \u7AEF\u70B9\uFF08\u8DEF\u5F84\u524D\u7F00 /dsh-archive\uFF09\uFF1A\n *\n * - POST /dsh-archive/restore { sessionId }\n * \u628A\u4F1A\u8BDD\u4ECE workspace registry \u7684\u5168\u5C40\u5F52\u6863\u96C6\u5408\u4E2D\u79FB\u9664\uFF08\u4FA7\u8FB9\u680F\u300C\u5F52\u6863\u4F1A\u8BDD\u300D\n * \u52A8\u4F5C\u7684\u9006\u64CD\u4F5C\uFF1Bharness \u672C\u8EAB\u6CA1\u6709\u516C\u5F00\u7684 unarchive API\uFF09\u3002\u5199\u5165\u8D70 registry\n * \u81EA\u5DF1\u7684\u9886\u57DF\u4E32\u884C\u94FE\uFF0C\u56E0\u6B64 api-proxy \u7684 `domain/changed` \u76D1\u542C\u4F1A\u5E7F\u64AD\n * `host/archived-sessions-changed` \u5E27\uFF0C\u6240\u6709\u5DF2\u8FDE\u63A5\u7684\u5BA2\u6237\u7AEF\uFF08\u542B\u4FA7\u8FB9\u680F\uFF09\n * \u90FD\u4F1A\u5B9E\u65F6\u5237\u65B0\u3002\n *\n * - POST /dsh-archive/delete { sessionId }\n * \u786C\u5220\u9664\uFF1A\u79FB\u9664\u4F1A\u8BDD\u7684\u6301\u4E45\u5316\u5DE5\u4EF6\u76EE\u5F55\uFF08`sessionPersistence.locate()` \u6307\u5411\n * \u7684\u76EE\u6807\uFF09\u3001\u628A\u4F1A\u8BDD\u4ECE\u6BCF\u4E2A workspace \u8D26\u76EE\u4E0A\u89E3\u9664\u3001\u79FB\u51FA\u5F52\u6863\u96C6\u5408\uFF0C\u5E76\u5C3D\u529B\u6E05\u7406\n * \u6295\u5F71\u7F13\u5B58\u884C\u3002\u6B63\u5728\u8FD0\u884C\u7684\uFF08live\uFF09\u4F1A\u8BDD\u4F1A\u88AB\u62D2\u7EDD\u3002\n *\n * - POST /dsh-archive/delete-all { sessionIds: string[] }\n * \u4E00\u952E\u6279\u91CF\u786C\u5220\u9664\uFF1A\u6309\u5E8F\u5BF9\u6BCF\u4E2A id \u6267\u884C\u4E0E /delete \u76F8\u540C\u7684\u6B65\u9AA4\uFF08\u5171\u4EAB\u540C\u4E00\u4E2A\n * \u6838\u5FC3\u51FD\u6570\uFF09\u3002\u4E0E\u5355\u5220\u4E0D\u540C\uFF0Clive \u4F1A\u8BDD\u4E0D\u4F1A\u8BA9\u6574\u6279\u5931\u8D25\u2014\u2014\u8DF3\u8FC7\u5E76\u8BA1\u5165\n * skippedLive\uFF1B\u5176\u4ED6\u786C\u9519\u8BEF\uFF08\u76EE\u5F55\u540D\u5B88\u536B\u7B49\uFF09\u8BA1\u5165 failed \u4E14\u4E0D\u4E2D\u65AD\u6574\u6279\uFF0C\n * \u9010\u4F1A\u8BDD\u7ED3\u679C\u968F\u54CD\u5E94\u8FD4\u56DE\u3002\u4E0A\u9650 500 \u4E2A id\uFF0C\u53BB\u91CD\u4FDD\u5E8F\u3002\n *\n * \u5220\u9664\u987A\u5E8F\uFF08\u9632\u810F\u72B6\u6001\u7684\u5173\u952E\uFF09\uFF1A\n * 1. \u53EA\u8BFB\u6821\u9A8C\uFF08live \u68C0\u67E5\u3001header/locate\u3001\u76EE\u5F55\u540D\u5B88\u536B\u3001\u5199\u94FE\u63A2\u6D4B\uFF09\uFF1B\n * 2. \u5148\u505A registry \u57DF\u5199\u5165\uFF08unarchive \u2192 \u9010\u4E2A workspace detach\uFF0C\u5355\u6B21\u5931\u8D25\n * \u6536\u96C6\u8B66\u544A\u800C\u975E\u4E2D\u65AD\uFF09\uFF1B\n * 3. \u6700\u540E\u624D `rm` \u5220\u9664\u6587\u4EF6\u76EE\u5F55\uFF08\u4ECD\u4FDD\u7559\u76EE\u5F55\u540D\u5B88\u536B\uFF09\uFF1B\n * 4. \u5C3D\u529B\u6E05\u7406\u6295\u5F71\u7F13\u5B58\u3002\n * \u8FD9\u6837\u4EFB\u4F55 503/500 \u90FD\u4F1A\u5728\u4EFB\u4F55\u7834\u574F\u6027\u64CD\u4F5C\u4E4B\u524D\u4E2D\u6B62\uFF0C\u4E0D\u4F1A\u51FA\u73B0\u300C\u6587\u4EF6\u5DF2\u5220\u4F46\u5F52\u6863\n * \u96C6\u5408\u8FD8\u6B8B\u7559\u300D\u7684\u534A\u5220\u9664\u72B6\u6001\u3002\n *\n * unarchive \u9700\u8981 registry \u7684\u8FD0\u884C\u65F6\u79C1\u6709\u5199\u94FE\uFF08harness \u65E0\u516C\u5F00 unarchive \u63A5\u53E3\uFF09\u3002\n * \u6210\u5458\u5728\u8C03\u7528\u65F6\u7279\u6027\u63A2\u6D4B\uFF1B\u82E5\u672A\u6765 harness \u6539\u540D\uFF0C\u7AEF\u70B9\u8FD4\u56DE 503 \u5E76\u63D0\u793A\uFF0C\u7EDD\u4E0D\u731C\u6D4B\n * \u6027\u5199\u5165\uFF08\u8FD9\u662F\u672C\u63D2\u4EF6\u5BF9 harness \u5185\u90E8\u5B9E\u73B0\u7684\u552F\u4E00\u89E6\u78B0\u70B9\uFF0C\u89C1 README \u901A\u7528\u6027\u8BF4\u660E\uFF09\u3002\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { rm } from 'node:fs/promises'\nimport { basename, dirname } from 'node:path'\n\n/** \u63D2\u4EF6\u540D\uFF1A\u4E0E\u5305\u540D / \u7EC4\u5408\u884C id / \u5BA2\u6237\u7AEF\u6A21\u5757 id \u4FDD\u6301\u4E00\u81F4\uFF08dsh-* \u524D\u7F00\uFF09\u3002 */\nexport const name = 'dsh-archive'\n\n/** \u672C\u884C\u79BB\u4E0D\u5F00\u7684 Web \u7EC4\u5408\u670D\u52A1\uFF08\u786C\u4F9D\u8D56\uFF0C\u6CE8\u5165\u58F0\u660E\uFF09\u3002 */\nexport const inject = ['webServer', 'workspaceRegistry', 'sessionPersistence']\n\n/** \u672C\u63D2\u4EF6\u89E6\u53CA\u7684 harness \u7C7B\u578B\u7684\u6700\u5C0F\u7ED3\u6784\u89C6\u56FE\uFF08\u907F\u514D\u5F15\u5165 harness \u7C7B\u578B\u4F9D\u8D56\uFF09\u3002 */\ninterface SessionHeaderLike {\n id: string\n cwd?: string\n}\ninterface SessionLocationLike {\n kind: string\n path: string\n}\ninterface WorkspaceLike {\n id: string\n sessionIds: readonly string[]\n detachSession(sessionId: string): Promise<void>\n}\ninterface RegistryLike {\n readonly archivedSessionIds: readonly string[]\n list(): WorkspaceLike[]\n}\n\n/** registry \u7684\u8FD0\u884C\u65F6\u79C1\u6709\u5199\u94FE\uFF08\u4F7F\u7528\u524D\u7279\u6027\u63A2\u6D4B\uFF1Bharness \u6539\u540D\u65F6\u8D70 503 \u5B89\u5168\u5931\u8D25\uFF09\u3002 */\ninterface RegistryInternals {\n enqueueOperation?: <T>(operation: () => Promise<T>) => Promise<T>\n requireState?: () => { archivedSessionIds: readonly string[]; [key: string]: unknown }\n setState?: (state: { archivedSessionIds: readonly string[]; [key: string]: unknown }) => Promise<void>\n}\n\n/** \u5E26 HTTP \u72B6\u6001\u7801\u7684\u9519\u8BEF\uFF1Ahandler \u4F9D\u6B64\u8FD4\u56DE\u5BF9\u5E94\u54CD\u5E94\uFF0C\u800C\u975E\u4E00\u5F8B 500\u3002 */\nclass HttpError extends Error {\n constructor(readonly status: number, message: string) {\n super(message)\n this.name = 'HttpError'\n }\n}\n\n/**\n * \u8BFB\u53D6\u5E76 JSON \u89E3\u6790\u8BF7\u6C42\u4F53\uFF08\u4E0A\u9650 1MB\uFF0C\u9632\u6B62\u6076\u610F\u5927\u8BF7\u6C42\u5360\u6EE1\u5185\u5B58\uFF09\u3002\n * \u89E3\u6790\u5931\u8D25 / \u6D41\u9519\u8BEF\u4EE5 HttpError reject\u3002\n */\nfunction readJsonBody(req: IncomingMessage): Promise<Record<string, unknown>> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let size = 0\n let settled = false\n const fail = (error: HttpError) => {\n if (settled) return\n settled = true\n reject(error)\n }\n req.on('data', (chunk: Buffer) => {\n if (settled) return\n size += chunk.length\n if (size > 1_000_000) {\n // \u8D85\u9650\uFF1Areject \u540E\u7531 handler \u5199 413 \u54CD\u5E94\u3002\u8FD9\u91CC\u4E0D\u80FD req.destroy()\u2014\u2014\n // \u9500\u6BC1\u8BF7\u6C42\u6D41\u4F1A\u8FDE\u5E26\u6495\u6389\u540C\u4E00\u4E2A socket\uFF0C\u540E\u9762\u7684 sendJson \u5199 413 \u4F1A\u56E0\n // ERR_STREAM_DESTROYED \u843D\u7A7A\uFF08\u5BA2\u6237\u7AEF\u53EA\u770B\u5230\u8FDE\u63A5\u88AB\u6390\u65AD\uFF09\u3002\u4E0D\u518D\u6536\n // \u540E\u7EED chunk\uFF08settled \u5B88\u536B\uFF09\uFF0C\u8BA9\u54CD\u5E94\u6B63\u5E38\u5199\u5B8C\u3001\u8FDE\u63A5\u81EA\u7136\u5173\u95ED\u5373\u53EF\u3002\n fail(new HttpError(413, 'request body too large'))\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n if (settled) return\n settled = true\n try {\n const text = Buffer.concat(chunks).toString('utf8')\n resolve(text === '' ? {} : JSON.parse(text) as Record<string, unknown>)\n } catch {\n reject(new HttpError(400, 'request body is not valid JSON'))\n }\n })\n req.on('error', () => fail(new HttpError(400, 'request stream failed')))\n })\n}\n\n/** \u5199\u4E00\u4E2A JSON \u54CD\u5E94\uFF08\u7981\u7528\u7F13\u5B58\uFF0C\u907F\u514D\u6D4F\u89C8\u5668\u5BF9\u63A5\u53E3\u7ED3\u679C\u505A\u5FEB\u7167\uFF09\u3002 */\nfunction sendJson(res: ServerResponse, status: number, value: unknown): void {\n const body = JSON.stringify(value)\n res.writeHead(status, {\n 'content-type': 'application/json; charset=utf-8',\n 'cache-control': 'no-store',\n })\n res.end(body)\n}\n\n/** \u6821\u9A8C\u5E76\u53D6\u56DE body \u4E2D\u7684 sessionId\uFF08\u975E\u7A7A\u5B57\u7B26\u4E32\uFF0C\u4E0A\u9650 512 \u5B57\u7B26\uFF09\u3002 */\nfunction requireSessionId(body: Record<string, unknown>): string {\n const sessionId = body.sessionId\n if (typeof sessionId !== 'string' || sessionId.length === 0 || sessionId.length > 512) {\n throw new HttpError(400, 'sessionId must be a non-empty string')\n }\n return sessionId\n}\n\n/**\n * \u6821\u9A8C\u5E76\u53D6\u56DE body \u4E2D\u7684 sessionIds\uFF08\u6279\u91CF\u5220\u9664\u7528\uFF09\u3002\n * \u5FC5\u987B\u662F\u6570\u7EC4\uFF0C\u81F3\u5C11 1 \u4E2A\u3001\u81F3\u591A 500 \u4E2A\u975E\u7A7A\u5B57\u7B26\u4E32\uFF1B\u53BB\u91CD\u4FDD\u5E8F\uFF0C\u9632\u6B62\u91CD\u590D\u63D0\u4EA4\n * \u540C\u4E00\u4F1A\u8BDD\u5BFC\u81F4\u540C\u4E00\u76EE\u5F55\u88AB rm \u4E24\u6B21\uFF08rm force \u5E42\u7B49\uFF0C\u4F46\u8BA1\u6570\u4F1A\u5931\u771F\uFF09\u3002\n */\nfunction requireSessionIds(body: Record<string, unknown>): string[] {\n const raw = body.sessionIds\n if (!Array.isArray(raw) || raw.length === 0) {\n throw new HttpError(400, 'sessionIds must be a non-empty array')\n }\n if (raw.length > 500) {\n throw new HttpError(400, 'sessionIds must not exceed 500 entries')\n }\n const seen = new Set<string>()\n const ids: string[] = []\n for (const value of raw) {\n if (typeof value !== 'string' || value.length === 0 || value.length > 512) {\n throw new HttpError(400, 'sessionIds must contain only non-empty strings')\n }\n if (seen.has(value)) continue\n seen.add(value)\n ids.push(value)\n }\n return ids\n}\n\n/**\n * \u901A\u8FC7 registry \u81EA\u8EAB\u7684\u4E32\u884C\u5316\u94FE\uFF0C\u628A\u4E00\u4E2A\u4F1A\u8BDD id \u4ECE\u6301\u4E45\u5F52\u6863\u96C6\u5408\u4E2D\u79FB\u9664\u3002\n * \u5E42\u7B49\uFF1Aid \u672C\u5C31\u4E0D\u5728\u96C6\u5408\u4E2D\u65F6\u8FD4\u56DE `changed: false` \u4E14\u4E0D\u5199\u76D8\u3002\n * \u82E5 id \u5728\u96C6\u5408\u4E2D\u4F46\u5199\u94FE\u4E0D\u53EF\u7528\uFF08harness API \u6F02\u79FB\uFF09\uFF0C\u629B 503\u2014\u2014\u8C03\u7528\u65B9\u5FC5\u987B\u628A\n * \u672C\u51FD\u6570\u653E\u5728\u4EFB\u4F55\u7834\u574F\u6027\u64CD\u4F5C\u4E4B\u524D\uFF0C\u4EE5\u4FDD\u8BC1 503 \u65F6\u6CA1\u6709\u4EFB\u4F55\u526F\u4F5C\u7528\u3002\n */\nasync function unarchiveSession(registry: RegistryLike, sessionId: string): Promise<{ changed: boolean }> {\n if (!registry.archivedSessionIds.includes(sessionId)) return { changed: false }\n const internals = registry as unknown as RegistryInternals\n if (\n typeof internals.enqueueOperation !== 'function'\n || typeof internals.requireState !== 'function'\n || typeof internals.setState !== 'function'\n ) {\n throw new HttpError(\n 503,\n 'workspace registry write chain is unavailable (harness API drift); restore is disabled until this plugin is updated',\n )\n }\n let changed = false\n await internals.enqueueOperation(async () => {\n const state = internals.requireState!()\n if (!state.archivedSessionIds.includes(sessionId)) return\n await internals.setState!({\n ...state,\n archivedSessionIds: state.archivedSessionIds.filter(id => id !== sessionId),\n })\n changed = true\n })\n return { changed }\n}\n\nexport function apply(ctx: any): void {\n const handler = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const pathname = new URL(req.url ?? '/', 'http://x').pathname\n const route = pathname === '/dsh-archive/restore'\n ? 'restore'\n : pathname === '/dsh-archive/delete'\n ? 'delete'\n : pathname === '/dsh-archive/delete-all'\n ? 'delete-all'\n : undefined\n try {\n if (route === undefined) throw new HttpError(404, `unknown route ${JSON.stringify(pathname)}`)\n if (req.method !== 'POST') throw new HttpError(405, 'method not allowed; use POST')\n // CSRF \u56F4\u680F\uFF1A\u4E0E harness /api \u9762\u540C\u6B3E\u7EA6\u5B9A\u2014\u2014\u53EA\u6536 application/json\u3002\n // \u6D4F\u89C8\u5668\u5BF9 text/plain \u7B49\u300C\u7B80\u5355\u8BF7\u6C42\u300D\u4E0D\u505A CORS \u9884\u68C0\uFF0C\u6076\u610F\u9875\u9762\u53EF\u8DE8\u7AD9\n // \u76F2\u53D1\u7834\u574F\u6027 POST\uFF08\u8FD9\u91CC\u4F1A rm \u4F1A\u8BDD\u76EE\u5F55\uFF09\uFF0C\u6309\u5185\u5BB9\u7C7B\u578B\u62D2\u6389\u3002\n const mediaType = (req.headers['content-type'] ?? '').split(';', 1)[0]?.trim().toLowerCase()\n if (mediaType !== 'application/json') throw new HttpError(415, 'content type must be application/json')\n const body = await readJsonBody(req)\n if (route === 'restore') {\n await handleRestore(ctx, requireSessionId(body), res)\n } else if (route === 'delete') {\n await handleDelete(ctx, requireSessionId(body), res)\n } else {\n await handleDeleteAll(ctx, requireSessionIds(body), res)\n }\n } catch (error) {\n if (error instanceof HttpError) {\n sendJson(res, error.status, { ok: false, error: error.message })\n return\n }\n ctx.logger.warn(error instanceof Error ? error : new Error(String(error)))\n sendJson(res, 500, { ok: false, error: 'internal error' })\n }\n }\n\n // \u8DEF\u7531\u6302\u5230 web \u670D\u52A1\u7684\u524D\u7F00\u8868\uFF1Beffect \u4FDD\u8BC1\u63D2\u4EF6\u505C\u6B62/\u66F4\u65B0\u65F6\u81EA\u52A8\u6458\u9664\u3002\n ctx.effect(\n () => ctx.webServer.register({ kind: 'prefix', path: '/dsh-archive', handler }),\n 'dsh-archive: routes',\n )\n}\n\n/** \u6062\u590D\uFF1A\u628A\u4F1A\u8BDD\u79FB\u51FA\u5F52\u6863\u96C6\u5408\uFF08\u5199\u94FE 503 \u65F6\u5148\u4E8E\u4EFB\u4F55\u526F\u4F5C\u7528\u5931\u8D25\uFF09\u3002 */\nasync function handleRestore(ctx: any, sessionId: string, res: ServerResponse): Promise<void> {\n const registry = ctx.workspaceRegistry as RegistryLike\n const { changed } = await unarchiveSession(registry, sessionId)\n sendJson(res, 200, {\n ok: true,\n sessionId,\n changed,\n archivedSessionIds: [...registry.archivedSessionIds],\n })\n}\n\n/**\n * \u5355\u4E2A\u4F1A\u8BDD\u7684\u786C\u5220\u9664\u6838\u5FC3\u6B65\u9AA4\uFF08\u5355\u5220 /delete \u4E0E\u6279\u91CF /delete-all \u5171\u7528\uFF09\u3002\n * \u6B65\u9AA4\u987A\u5E8F\u89C1\u6587\u4EF6\u5934\u6CE8\u91CA\uFF1A\u5148\u53EA\u8BFB\u6821\u9A8C\uFF0C\u518D\u5199 registry\uFF08unarchive \u2192 detach\uFF09\uFF0C\n * \u6700\u540E\u624D\u5220\u6587\u4EF6\uFF0C\u4EE5\u4FDD\u8BC1\u4EFB\u4F55\u5931\u8D25\u90FD\u505C\u5728\u53EF\u6062\u590D\u72B6\u6001\u3002\u8FD4\u56DE\u7ED3\u679C\u5B57\u6BB5\uFF0C\u786C\u9519\u8BEF\n * \uFF08\u76EE\u5F55\u540D\u5B88\u536B\u7B49\uFF09\u629B HttpError\uFF0C\u7531\u8C03\u7528\u65B9\u51B3\u5B9A\u662F 500 \u8FD8\u662F\u6574\u6279\u7EE7\u7EED\u3002\n */\nasync function deleteSessionCore(ctx: any, sessionId: string): Promise<{\n removedFiles: boolean\n detachedWorkspaces: number\n unarchived: boolean\n clearedProjectionCache: boolean\n persisted: boolean\n detachErrors: string[]\n}> {\n const registry = ctx.workspaceRegistry as RegistryLike\n const headers = (await ctx.sessionPersistence.list()) as SessionHeaderLike[]\n const header = headers.find(candidate => candidate.id === sessionId)\n\n // 1. \u53EA\u8BFB\u5B9A\u4F4D\uFF1A\u7B97\u51FA\u65E5\u5FD7\u6240\u5728\u76EE\u5F55\u5E76\u5148\u884C\u505A\u76EE\u5F55\u540D\u5B88\u536B\uFF08\u4E0D\u5728\u6B64\u5904\u5220\u4EFB\u4F55\u4E1C\u897F\uFF09\u3002\n // \u76EE\u5F55\u540D\u5FC5\u987B\u662F\u4F1A\u8BDD id \u672C\u8EAB\uFF08harness \u7684 encodeSegment \u5BF9\u771F\u5B9E id\n // `session-<n>` / `*-session-<uuid>` \u662F\u6052\u7B49\u53D8\u6362\uFF0C\u4E0E encodeURIComponent\n // \u4E00\u81F4\uFF1B\u8FD9\u91CC\u53CC\u4FDD\u9669\u53EA\u5141\u8BB8\u8FD9\u4E24\u79CD\u5199\u6CD5\uFF0C\u7EDD\u4E0D\u5220\u9664\u540D\u5B57\u5BF9\u4E0D\u4E0A\u7684\u76EE\u5F55\uFF09\u3002\n let targetDir: string | null = null\n if (header !== undefined) {\n const location = ctx.sessionPersistence.locate(header) as SessionLocationLike | undefined\n if (location !== undefined && typeof location.path === 'string' && location.path !== '') {\n const dir = dirname(location.path)\n const dirName = basename(dir)\n if (dirName !== sessionId && dirName !== encodeURIComponent(sessionId)) {\n throw new HttpError(500, `refusing to remove unexpected session directory ${JSON.stringify(dir)}`)\n }\n targetDir = dir\n }\n }\n\n // 2. \u5148\u505A registry \u57DF\u5199\u5165\u3002\n // 2a. unarchive \u4F18\u5148\uFF1A\u82E5\u4F1A\u8BDD\u5728\u5F52\u6863\u96C6\u5408\u4E2D\u800C\u5199\u94FE\u4E0D\u53EF\u7528\uFF0C\u8FD9\u91CC\u5C31\u629B 503\uFF0C\n // \u4EFB\u4F55 detach / \u5220\u6587\u4EF6\u90FD\u8FD8\u6CA1\u6709\u53D1\u751F\uFF0C\u72B6\u6001\u4FDD\u6301\u539F\u6837\u3002\n const { changed: unarchived } = await unarchiveSession(registry, sessionId)\n\n // 2b. \u9010\u4E2A workspace \u89E3\u9664\u8D26\u76EE\uFF08\u516C\u5F00\u3001\u5E42\u7B49 API\uFF09\u3002\u5355\u4E2A\u5931\u8D25\u4E0D\u4E2D\u65AD\u6574\u4F53\uFF0C\n // \u6536\u96C6\u5230 detachErrors \u968F\u54CD\u5E94\u8FD4\u56DE\u5E76\u5728\u65E5\u5FD7\u91CC\u8B66\u544A\u3002\n let detachedWorkspaces = 0\n const detachErrors: string[] = []\n for (const workspace of registry.list()) {\n if (!workspace.sessionIds.includes(sessionId)) continue\n try {\n await workspace.detachSession(sessionId)\n detachedWorkspaces += 1\n } catch (error) {\n detachErrors.push(`workspace ${workspace.id}: ${String(error)}`)\n ctx.logger.warn(new Error(`archive-tab: detach failed for '${sessionId}' in workspace '${workspace.id}': ${String(error)}`))\n }\n }\n\n // 3. \u6700\u540E\u624D\u662F\u7834\u574F\u6027\u6587\u4EF6\u5220\u9664\uFF08\u5B88\u536B\u5728\u6B65\u9AA4 1 \u5DF2\u5148\u884C\u6821\u9A8C\uFF0C\u8FD9\u91CC\u518D\u6838\u9A8C\u4E00\u6B21\uFF09\u3002\n let removedFiles = false\n if (targetDir !== null) {\n const dirName = basename(targetDir)\n if (dirName !== sessionId && dirName !== encodeURIComponent(sessionId)) {\n throw new HttpError(500, `refusing to remove unexpected session directory ${JSON.stringify(targetDir)}`)\n }\n await rm(targetDir, { recursive: true, force: true })\n removedFiles = true\n }\n\n // 4. \u5C3D\u529B\u6E05\u7406\u6295\u5F71\u7F13\u5B58\u884C\uFF1B\u6B8B\u7559\u884C\u662F\u60F0\u6027\u7684\uFF08\u6309\u8EAB\u4EFD\u5B88\u536B\u7684\u8BFB\u53D6\u6C38\u8FDC\u5339\u914D\u4E0D\u5230\n // \u5DF2\u5220\u9664\u7684\u65E5\u5FD7\uFF09\uFF0C\u4F46\u7528\u6237\u8981\u6C42\u96F6\u6B8B\u7559\uFF0C\u5931\u8D25\u4EC5\u544A\u8B66\u4E0D\u81F4\u547D\u3002\n let clearedProjectionCache = false\n const cache = ctx.get('sessionProjectionCache')\n const table = (cache as { table?: { delete(key: string): Promise<boolean> } } | undefined)?.table\n if (table !== undefined && typeof table.delete === 'function') {\n try {\n clearedProjectionCache = await table.delete(sessionId)\n } catch (error) {\n ctx.logger.warn(new Error(`archive-tab: projection-cache cleanup failed for '${sessionId}': ${String(error)}`))\n }\n }\n\n return {\n removedFiles,\n detachedWorkspaces,\n unarchived,\n clearedProjectionCache,\n persisted: header !== undefined,\n detachErrors,\n }\n}\n\n/** \u5355\u5220\uFF1Alive \u68C0\u67E5 \u2192 \u6838\u5FC3\u6B65\u9AA4 \u2192 200 \u54CD\u5E94\u3002 */\nasync function handleDelete(ctx: any, sessionId: string, res: ServerResponse): Promise<void> {\n // 0. live \u68C0\u67E5\uFF1A\u6B63\u5728\u8FD0\u884C\u7684\u4F1A\u8BDD\u4ECD\u7ED1\u5B9A agent \u4E0E\u53EF\u80FD\u7684\u6D4F\u89C8\u5668\u9762\u677F\uFF0C\u62D2\u7EDD\u5220\u9664\u3002\n const live = ctx.get('sessions')?.get(sessionId)\n if (live !== undefined) {\n throw new HttpError(409, 'session is live in this process; close it before deleting')\n }\n\n const result = await deleteSessionCore(ctx, sessionId)\n sendJson(res, 200, {\n ok: true,\n sessionId,\n ...result,\n })\n}\n\n/**\n * \u4E00\u952E\u6279\u91CF\u786C\u5220\u9664\uFF1A\u5BF9\u6BCF\u4E2A id \u590D\u7528 deleteSessionCore\u3002live \u4F1A\u8BDD\u8DF3\u8FC7\u5E76\u8BA1\u5165\n * skipped\uFF08\u6279\u91CF\u8BED\u4E49\uFF1A\u80FD\u5220\u591A\u5C11\u5220\u591A\u5C11\uFF0C\u4E0D\u8BA9\u4E00\u4E2A\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u5361\u6B7B\u6574\u6279\uFF09\uFF1B\n * \u786C\u9519\u8BEF\u8BA1\u5165 failed \u4E14\u4E0D\u4E2D\u65AD\u6574\u6279\u3002\u54CD\u5E94\u643A\u5E26\u6C47\u603B\u8BA1\u6570\u4E0E\u9010\u4F1A\u8BDD\u7ED3\u679C\u3002\n */\nasync function handleDeleteAll(ctx: any, sessionIds: readonly string[], res: ServerResponse): Promise<void> {\n let deleted = 0\n let skipped = 0\n let failed = 0\n const failures: Array<{ sessionId: string; error: string }> = []\n for (const sessionId of sessionIds) {\n const live = ctx.get('sessions')?.get(sessionId)\n if (live !== undefined) {\n skipped += 1\n continue\n }\n try {\n await deleteSessionCore(ctx, sessionId)\n deleted += 1\n } catch (error) {\n failed += 1\n const message = error instanceof HttpError ? error.message : String(error)\n failures.push({ sessionId, error: message })\n ctx.logger.warn(new Error(`archive-tab: batch delete failed for '${sessionId}': ${message}`))\n }\n }\n\n sendJson(res, 200, {\n ok: true,\n total: sessionIds.length,\n deleted,\n skipped,\n failed,\n failures,\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bit-ark/dsh-archive",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Adds a 归档 (Archive) tab after the Trajectory tab: lists the current project's archived sessions with restore (unarchive) and hard delete.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib",
|
|
14
|
+
"cordis.patch.yml"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "node build.mjs"
|
|
18
|
+
},
|
|
19
|
+
"dsh": {
|
|
20
|
+
"bundle": {
|
|
21
|
+
"patch": "./cordis.patch.yml"
|
|
22
|
+
},
|
|
23
|
+
"client": {
|
|
24
|
+
"platform": "web"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"esbuild": "^0.25.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|