@bit-ark/dsh-archive 0.1.0 → 0.1.2

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 CHANGED
@@ -9,16 +9,24 @@
9
9
  - **入口**:侧边栏 footer 的「归档 N」按钮(宽栏显示文字 + 计数,rail 窄栏为圆形
10
10
  图标按钮)。当前项目没有归档时按钮隐藏;归档第一个会话后自动出现。
11
11
  - **面板**:点击按钮在 footer 上方打开浮动面板,支持 Escape / 点击外部关闭。
12
- 面板结构:头部(标题 + 关闭)→ 滚动列表 → footer(归档计数提示 + 一键删除)。
12
+ 面板结构:头部(标题 + 关闭)→ 滚动列表 → footer(一键删除按钮)。面板的
13
+ 结果提示区显示最近一次删除/恢复的结果或错误明细(批量删除总是给出完整
14
+ 汇总);提示随面板生命周期走——关闭面板(✕ / Escape / 外点)即清除,
15
+ 重开面板是干净的。删除动作保留点击时的 `window.confirm` 二次确认。
13
16
  - **列表**:只显示当前项目的已归档会话——归档会话属于当前项目当且仅当它在该项目的
14
17
  workspace 记账中,或其存储 cwd 等于项目路径。当前项目解析不依赖打开的会话:
15
18
  最近活跃 workspace → 当前会话所属 workspace → 当前会话 cwd 对应的 workspace。
16
19
  - **恢复**:把会话从全局归档集合移除(侧边栏「归档会话」的逆操作),会话回到侧边栏原位置。
17
20
  - **删除**:硬删除——删除会话日志目录(`~/.dsh/sessions/<项目>/<会话>/`)、解除
18
- workspace 记账、移出归档集合,并尽力清理投影缓存行。正在运行(live)的会话会被拒绝。
21
+ workspace 记账、移出归档集合,并尽力清理投影缓存行。正在运行(live)的会话
22
+ 会被拒绝(中文 409 提示)。注意:**归档不会结束会话**——「live」指会话对象
23
+ 仍挂在进程内存的 SessionStore 里,harness 没有公开的强制结束会话 API(UI 的
24
+ 停止/关闭会话都只取消当前 turn),会话会一直 live 到 `dsh web` 进程重启为止;
25
+ 因此删除被拒绝时提示「重启 dsh web 后再删除」。
19
26
  - **一键删除**:面板底部 footer 区域的按钮,确认后把当前项目全部已归档会话一次性
20
27
  批量硬删除——复用单删的同一套步骤(逐会话),运行中的会话跳过、单个硬错误不中断
21
- 整批,完成后显示汇总(删除 / 跳过 / 失败计数与失败明细)。
28
+ 整批。**总是显示完整结果提示**(已删除 / 跳过运行中 / 失败 + 明细),跳过时列出
29
+ 被跳过的会话 id 并解释「重启 dsh web 后可删除」;提示随面板关闭清除,重开干净。
22
30
 
23
31
  ## 结构
24
32
 
@@ -63,8 +71,10 @@ workspace detach,单次失败收集警告不中断)→ 最后才 `rm` 删除
63
71
  失败都停在可恢复状态。
64
72
 
65
73
  `/delete-all` 在单个会话上的语义与单删一致,但批量语义是「能删多少删多少」:
66
- live 会话计入跳过(不拒绝整批),其他硬错误计入失败明细并继续处理剩余会话;
67
- id 列表去重保序、上限 500 个。客户端在调用前先用 `window.confirm` 二次确认。
74
+ live 会话计入跳过(不拒绝整批,被跳过的 id 随 `skippedIds` 返回),其他硬错误
75
+ 计入失败明细并继续处理剩余会话;id 列表去重保序、上限 500 个。客户端在调用前
76
+ 先用 `window.confirm` 二次确认,随后总是把完整结果(已删除 / 跳过 / 失败 +
77
+ 明细)写入面板提示区。
68
78
 
69
79
  ## 通用性说明
70
80
 
package/lib/client.js CHANGED
@@ -54,6 +54,11 @@ function formatTime(value) {
54
54
  return "";
55
55
  }
56
56
  }
57
+ function formatSkippedIds(ids, skipped) {
58
+ const shown = ids.slice(0, 10);
59
+ const suffix = shown.length < skipped ? `\u2026\u7B49 ${skipped} \u4E2A` : "";
60
+ return shown.map((id) => `\u300C${id}\u300D`).join("\u3001") + suffix;
61
+ }
57
62
  function ArchiveIcon({ size }) {
58
63
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
59
64
  "svg",
@@ -290,49 +295,23 @@ function apply(ctx) {
290
295
  lineHeight: "1",
291
296
  cursor: "pointer"
292
297
  },
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
298
  panelFooter: {
304
299
  flex: "none",
305
300
  display: "flex",
306
301
  alignItems: "center",
302
+ justifyContent: "flex-end",
307
303
  gap: "8px",
308
304
  minHeight: "44px",
309
305
  padding: "0 12px",
310
306
  boxSizing: "border-box",
311
307
  borderTop: "1px solid var(--dsw-alias-border-l2)"
312
308
  },
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
309
  panelBody: {
325
310
  flex: 1,
326
311
  minHeight: 0,
327
312
  overflowY: "auto",
328
313
  padding: "8px 12px 12px"
329
314
  },
330
- hint: {
331
- color: "var(--dsw-alias-label-tertiary)",
332
- fontSize: "12px",
333
- lineHeight: "18px",
334
- margin: "4px 0 8px"
335
- },
336
315
  error: {
337
316
  padding: "8px 12px",
338
317
  marginBottom: "8px",
@@ -399,11 +378,14 @@ function apply(ctx) {
399
378
  const [busy, setBusy] = (0, import_react.useState)(null);
400
379
  const [batchBusy, setBatchBusy] = (0, import_react.useState)(false);
401
380
  const [error, setError] = (0, import_react.useState)(null);
402
- const [notice, setNotice] = (0, import_react.useState)(null);
403
381
  const panelRef = (0, import_react.useRef)(null);
404
382
  const triggerRef = (0, import_react.useRef)(null);
405
383
  const busyRef = (0, import_react.useRef)(false);
406
384
  const batchBusyRef = (0, import_react.useRef)(false);
385
+ const closePanel = () => {
386
+ setError(null);
387
+ setOpen(false);
388
+ };
407
389
  const workspace = (0, import_react.useMemo)(() => {
408
390
  const items = workspaces?.items;
409
391
  if (items === void 0 || items.length === 0) return void 0;
@@ -448,13 +430,17 @@ function apply(ctx) {
448
430
  (0, import_react.useEffect)(() => {
449
431
  if (!open) return;
450
432
  const onKey = (event) => {
451
- if (event.key === "Escape") setOpen(false);
433
+ if (event.key === "Escape") {
434
+ setError(null);
435
+ setOpen(false);
436
+ }
452
437
  };
453
438
  const onPointerDown = (event) => {
454
439
  const target = event.target;
455
440
  if (target === null) return;
456
441
  if (panelRef.current?.contains(target) === true) return;
457
442
  if (triggerRef.current?.contains(target) === true) return;
443
+ setError(null);
458
444
  setOpen(false);
459
445
  };
460
446
  document.addEventListener("keydown", onKey);
@@ -472,7 +458,6 @@ function apply(ctx) {
472
458
  if (busyRef.current) return;
473
459
  busyRef.current = true;
474
460
  setError(null);
475
- setNotice(null);
476
461
  setBusy({ id, action });
477
462
  try {
478
463
  await callHost(action, { sessionId: id });
@@ -510,7 +495,6 @@ function apply(ctx) {
510
495
  if (batchBusyRef.current) return;
511
496
  batchBusyRef.current = true;
512
497
  setError(null);
513
- setNotice(null);
514
498
  setBatchBusy(true);
515
499
  void (async () => {
516
500
  try {
@@ -519,23 +503,37 @@ function apply(ctx) {
519
503
  let deleted = 0;
520
504
  let skipped = 0;
521
505
  let failed = 0;
506
+ const skippedIds = [];
522
507
  const failures = [];
523
508
  for (let offset = 0; offset < ids.length; offset += DELETE_BATCH_SIZE) {
524
509
  const payload = await callHost("delete-all", { sessionIds: ids.slice(offset, offset + DELETE_BATCH_SIZE) });
525
510
  deleted += payload.deleted ?? 0;
526
511
  skipped += payload.skipped ?? 0;
527
512
  failed += payload.failed ?? 0;
513
+ const chunkSkipped = payload.skippedIds;
514
+ if (chunkSkipped !== void 0 && chunkSkipped.length > 0) skippedIds.push(...chunkSkipped);
528
515
  const chunkFailures = payload.failures;
529
516
  if (chunkFailures !== void 0 && chunkFailures.length > 0) failures.push(...chunkFailures);
530
517
  }
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")}`;
518
+ let message;
519
+ if (deleted === total) {
520
+ message = `\u5DF2\u5220\u9664\u5168\u90E8 ${total} \u4E2A\u4F1A\u8BDD`;
521
+ } else {
522
+ const parts = [`\u5DF2\u5220\u9664 ${deleted} \u4E2A`];
523
+ if (skipped > 0) {
524
+ const skippedText = skippedIds.length > 0 ? `\uFF1A${formatSkippedIds(skippedIds, skipped)}` : "";
525
+ parts.push(`\u8DF3\u8FC7\u8FD0\u884C\u4E2D ${skipped} \u4E2A${skippedText}`);
526
+ }
527
+ if (failed > 0) parts.push(`\u5931\u8D25 ${failed} \u4E2A`);
528
+ message = parts.join("\uFF0C");
529
+ if (skipped > 0) {
530
+ message += "\u3002\u5F52\u6863\u4E0D\u4F1A\u7ED3\u675F\u4F1A\u8BDD\u2014\u2014\u4F1A\u8BDD\u4F1A\u4E00\u76F4\u5B58\u6D3B\u5230 dsh web \u91CD\u542F\u4E3A\u6B62\uFF1B\u91CD\u542F\u540E\u8FD9\u4E9B\u4F1A\u8BDD\u5373\u53EF\u5220\u9664";
531
+ }
532
+ if (failures.length > 0) {
533
+ message += `\u3002\u5931\u8D25\u660E\u7EC6\uFF1A${failures.map((item) => `${item.sessionId}\uFF08${item.error}\uFF09`).join("\uFF1B")}`;
534
+ }
537
535
  }
538
- setNotice(message);
536
+ setError(message);
539
537
  ctx.sessions.refresh?.().catch?.(() => {
540
538
  });
541
539
  ctx.workspaces.refresh?.().catch?.(() => {
@@ -551,7 +549,7 @@ function apply(ctx) {
551
549
  const openSession = (id) => {
552
550
  try {
553
551
  ctx.sessions.open(id);
554
- setOpen(false);
552
+ closePanel();
555
553
  } catch {
556
554
  }
557
555
  };
@@ -577,16 +575,14 @@ function apply(ctx) {
577
575
  style: styles.close,
578
576
  "aria-label": "\u5173\u95ED",
579
577
  onClick: () => {
580
- setOpen(false);
578
+ closePanel();
581
579
  },
582
580
  children: "\u2715"
583
581
  }
584
582
  )
585
583
  ] }),
586
584
  /* @__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
585
  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
586
  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
587
  rows.map((row) => {
592
588
  const rowBusy = busy !== null && busy.id === row.id;
@@ -665,26 +661,22 @@ function apply(ctx) {
665
661
  ] }, row.id);
666
662
  })
667
663
  ] }),
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
- ] })
664
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("footer", { style: styles.panelFooter, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
665
+ "button",
666
+ {
667
+ type: "button",
668
+ className: "dsh-archive-batch",
669
+ style: {
670
+ ...styles.batchDelete,
671
+ opacity: batchBusy || busy !== null || rows.length === 0 ? 0.5 : 1
672
+ },
673
+ disabled: batchBusy || busy !== null || rows.length === 0,
674
+ onClick: () => {
675
+ deleteAll();
676
+ },
677
+ children: batchBusy ? "\u5220\u9664\u4E2D\u2026" : "\u4E00\u952E\u5220\u9664"
678
+ }
679
+ ) })
688
680
  ]
689
681
  }
690
682
  ),
@@ -700,7 +692,8 @@ function apply(ctx) {
700
692
  "aria-expanded": open,
701
693
  title: wide ? void 0 : `\u5DF2\u5F52\u6863\u4F1A\u8BDD\uFF08${String(rows.length)}\uFF09`,
702
694
  onClick: () => {
703
- setOpen((value) => !value);
695
+ if (open) closePanel();
696
+ else setOpen(true);
704
697
  },
705
698
  children: [
706
699
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArchiveIcon, { size: wide ? 16 : 18 }),
package/lib/client.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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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",
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\uFF1B\u603B\u662F\u663E\u793A\u5B8C\u6574\u7ED3\u679C\n * \u63D0\u793A\uFF08\u5DF2\u5220\u9664 / \u8DF3\u8FC7\u8FD0\u884C\u4E2D / \u5931\u8D25\u660E\u7EC6\uFF0C\u542B\u88AB\u8DF3\u8FC7\u7684\u4F1A\u8BDD id\uFF09\u3002\u63D0\u793A\u968F\u9762\u677F\n * \u751F\u547D\u5468\u671F\u8D70\uFF1A\u5173\u95ED\u9762\u677F\uFF08\u2715 / Escape / \u5916\u70B9\uFF09\u5373\u6E05\u9664\uFF0C\u91CD\u5F00\u9762\u677F\u662F\u5E72\u51C0\u7684\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/** \u88AB\u8DF3\u8FC7\uFF08\u8FD0\u884C\u4E2D\uFF09\u4F1A\u8BDD id \u5217\u8868 \u2192 \u53EF\u8BFB\u6587\u672C\uFF1B\u8FC7\u591A\u65F6\u53EA\u663E\u793A\u524D\u51E0\u4E2A\u3002 */\nfunction formatSkippedIds(ids: readonly string[], skipped: number): string {\n const shown = ids.slice(0, 10)\n const suffix = shown.length < skipped ? `\u2026\u7B49 ${skipped} \u4E2A` : ''\n return shown.map(id => `\u300C${id}\u300D`).join('\u3001') + suffix\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 panelFooter: {\n flex: 'none',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-end',\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 panelBody: {\n flex: 1,\n minHeight: 0,\n overflowY: 'auto',\n padding: '8px 12px 12px',\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 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 /** \u5173\u95ED\u9762\u677F\u5E76\u6E05\u7A7A\u63D0\u793A\uFF1A\u63D0\u793A\u968F\u9762\u677F\u751F\u547D\u5468\u671F\u8D70\uFF0C\u91CD\u5F00\u9762\u677F\u662F\u5E72\u51C0\u7684\u3002 */\n const closePanel = (): void => {\n setError(null)\n setOpen(false)\n }\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\u5173\u95ED\u65F6\u6E05\u7A7A\u63D0\u793A\uFF0C\u4E0E \u2715 \u5173\u95ED\u884C\u4E3A\u4E00\u81F4\u3002\uFF09\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') {\n setError(null)\n setOpen(false)\n }\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 setError(null)\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\u63D0\u793A\u533A\uFF08\u8986\u76D6\u65E7\u63D0\u793A\uFF09\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 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 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 skippedIds: string[] = []\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 skippedIds?: string[]\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 chunkSkipped = payload.skippedIds\n if (chunkSkipped !== undefined && chunkSkipped.length > 0) skippedIds.push(...chunkSkipped)\n const chunkFailures = payload.failures\n if (chunkFailures !== undefined && chunkFailures.length > 0) failures.push(...chunkFailures)\n }\n // \u603B\u662F\u7ED9\u51FA\u5B8C\u6574\u7ED3\u679C\u63D0\u793A\uFF08\u8986\u76D6\u65E7\u63D0\u793A\uFF09\uFF0C\u4E0D\u518D\u9759\u9ED8\uFF1A\n // - \u5168\u90E8\u6210\u529F\uFF1A\u7B80\u5355\u4E00\u53E5\u3002\n // - \u6709\u8DF3\u8FC7\uFF08live \u4F1A\u8BDD\uFF09\uFF1A\u89E3\u91CA\u300C\u4F1A\u8BDD\u8FD0\u884C\u5230 dsh web \u91CD\u542F\u4E3A\u6B62\u300D\uFF0C\u5E76\n // \u5728\u5BBF\u4E3B\u8FD4\u56DE skippedIds \u65F6\u5217\u51FA\u5177\u4F53\u88AB\u8DF3\u8FC7\u7684\u4F1A\u8BDD\u3002\n // - \u6709\u5931\u8D25\uFF1A\u8FFD\u52A0\u9010\u4F1A\u8BDD\u5931\u8D25\u660E\u7EC6\u3002\n let message: string\n if (deleted === total) {\n message = `\u5DF2\u5220\u9664\u5168\u90E8 ${total} \u4E2A\u4F1A\u8BDD`\n } else {\n const parts: string[] = [`\u5DF2\u5220\u9664 ${deleted} \u4E2A`]\n if (skipped > 0) {\n const skippedText = skippedIds.length > 0\n ? `\uFF1A${formatSkippedIds(skippedIds, skipped)}`\n : ''\n parts.push(`\u8DF3\u8FC7\u8FD0\u884C\u4E2D ${skipped} \u4E2A${skippedText}`)\n }\n if (failed > 0) parts.push(`\u5931\u8D25 ${failed} \u4E2A`)\n message = parts.join('\uFF0C')\n if (skipped > 0) {\n message += '\u3002\u5F52\u6863\u4E0D\u4F1A\u7ED3\u675F\u4F1A\u8BDD\u2014\u2014\u4F1A\u8BDD\u4F1A\u4E00\u76F4\u5B58\u6D3B\u5230 dsh web \u91CD\u542F\u4E3A\u6B62\uFF1B\u91CD\u542F\u540E\u8FD9\u4E9B\u4F1A\u8BDD\u5373\u53EF\u5220\u9664'\n }\n if (failures.length > 0) {\n message += `\u3002\u5931\u8D25\u660E\u7EC6\uFF1A${failures.map(item => `${item.sessionId}\uFF08${item.error}\uFF09`).join('\uFF1B')}`\n }\n }\n setError(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 closePanel()\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={() => { closePanel() }}\n >\n \u2715\n </button>\n </header>\n <div style={styles.panelBody}>\n {error !== null ? <div style={styles.error}>{error}</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 <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 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={() => { if (open) closePanel(); else setOpen(true) }}\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
6
  "names": []
7
7
  }
package/lib/index.js CHANGED
@@ -24,7 +24,7 @@ function readJsonBody(req) {
24
24
  if (settled) return;
25
25
  size += chunk.length;
26
26
  if (size > 1e6) {
27
- fail(new HttpError(413, "request body too large"));
27
+ fail(new HttpError(413, "\u8BF7\u6C42\u4F53\u8FC7\u5927\uFF08\u4E0A\u9650 1MB\uFF09"));
28
28
  return;
29
29
  }
30
30
  chunks.push(chunk);
@@ -36,10 +36,10 @@ function readJsonBody(req) {
36
36
  const text = Buffer.concat(chunks).toString("utf8");
37
37
  resolve(text === "" ? {} : JSON.parse(text));
38
38
  } catch {
39
- reject(new HttpError(400, "request body is not valid JSON"));
39
+ reject(new HttpError(400, "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"));
40
40
  }
41
41
  });
42
- req.on("error", () => fail(new HttpError(400, "request stream failed")));
42
+ req.on("error", () => fail(new HttpError(400, "\u8BF7\u6C42\u6D41\u8BFB\u53D6\u5931\u8D25")));
43
43
  });
44
44
  }
45
45
  function sendJson(res, status, value) {
@@ -53,23 +53,23 @@ function sendJson(res, status, value) {
53
53
  function requireSessionId(body) {
54
54
  const sessionId = body.sessionId;
55
55
  if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > 512) {
56
- throw new HttpError(400, "sessionId must be a non-empty string");
56
+ throw new HttpError(400, "sessionId \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
57
57
  }
58
58
  return sessionId;
59
59
  }
60
60
  function requireSessionIds(body) {
61
61
  const raw = body.sessionIds;
62
62
  if (!Array.isArray(raw) || raw.length === 0) {
63
- throw new HttpError(400, "sessionIds must be a non-empty array");
63
+ throw new HttpError(400, "sessionIds \u5FC5\u987B\u662F\u975E\u7A7A\u6570\u7EC4");
64
64
  }
65
65
  if (raw.length > 500) {
66
- throw new HttpError(400, "sessionIds must not exceed 500 entries");
66
+ throw new HttpError(400, "sessionIds \u6700\u591A 500 \u9879");
67
67
  }
68
68
  const seen = /* @__PURE__ */ new Set();
69
69
  const ids = [];
70
70
  for (const value of raw) {
71
71
  if (typeof value !== "string" || value.length === 0 || value.length > 512) {
72
- throw new HttpError(400, "sessionIds must contain only non-empty strings");
72
+ throw new HttpError(400, "sessionIds \u53EA\u80FD\u5305\u542B\u975E\u7A7A\u5B57\u7B26\u4E32");
73
73
  }
74
74
  if (seen.has(value)) continue;
75
75
  seen.add(value);
@@ -83,7 +83,7 @@ async function unarchiveSession(registry, sessionId) {
83
83
  if (typeof internals.enqueueOperation !== "function" || typeof internals.requireState !== "function" || typeof internals.setState !== "function") {
84
84
  throw new HttpError(
85
85
  503,
86
- "workspace registry write chain is unavailable (harness API drift); restore is disabled until this plugin is updated"
86
+ "workspace registry \u5199\u94FE\u4E0D\u53EF\u7528\uFF08harness API \u6F02\u79FB\uFF09\uFF1B\u6062\u590D/\u5220\u9664\u5DF2\u505C\u7528\uFF0C\u8BF7\u66F4\u65B0\u672C\u63D2\u4EF6"
87
87
  );
88
88
  }
89
89
  let changed = false;
@@ -103,10 +103,10 @@ function apply(ctx) {
103
103
  const pathname = new URL(req.url ?? "/", "http://x").pathname;
104
104
  const route = pathname === "/dsh-archive/restore" ? "restore" : pathname === "/dsh-archive/delete" ? "delete" : pathname === "/dsh-archive/delete-all" ? "delete-all" : void 0;
105
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");
106
+ if (route === void 0) throw new HttpError(404, `\u672A\u77E5\u8DEF\u7531 ${JSON.stringify(pathname)}`);
107
+ if (req.method !== "POST") throw new HttpError(405, "\u4EC5\u652F\u6301 POST \u65B9\u6CD5");
108
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");
109
+ if (mediaType !== "application/json") throw new HttpError(415, "content-type \u5FC5\u987B\u662F application/json");
110
110
  const body = await readJsonBody(req);
111
111
  if (route === "restore") {
112
112
  await handleRestore(ctx, requireSessionId(body), res);
@@ -121,7 +121,7 @@ function apply(ctx) {
121
121
  return;
122
122
  }
123
123
  ctx.logger.warn(error instanceof Error ? error : new Error(String(error)));
124
- sendJson(res, 500, { ok: false, error: "internal error" });
124
+ sendJson(res, 500, { ok: false, error: "\u5185\u90E8\u9519\u8BEF" });
125
125
  }
126
126
  };
127
127
  ctx.effect(
@@ -139,10 +139,10 @@ async function handleRestore(ctx, sessionId, res) {
139
139
  archivedSessionIds: [...registry.archivedSessionIds]
140
140
  });
141
141
  }
142
- async function deleteSessionCore(ctx, sessionId) {
142
+ async function deleteSessionCore(ctx, sessionId, headers) {
143
143
  const registry = ctx.workspaceRegistry;
144
- const headers = await ctx.sessionPersistence.list();
145
- const header = headers.find((candidate) => candidate.id === sessionId);
144
+ const allHeaders = headers ?? await ctx.sessionPersistence.list();
145
+ const header = allHeaders.find((candidate) => candidate.id === sessionId);
146
146
  let targetDir = null;
147
147
  if (header !== void 0) {
148
148
  const location = ctx.sessionPersistence.locate(header);
@@ -150,7 +150,7 @@ async function deleteSessionCore(ctx, sessionId) {
150
150
  const dir = dirname(location.path);
151
151
  const dirName = basename(dir);
152
152
  if (dirName !== sessionId && dirName !== encodeURIComponent(sessionId)) {
153
- throw new HttpError(500, `refusing to remove unexpected session directory ${JSON.stringify(dir)}`);
153
+ throw new HttpError(500, `\u62D2\u7EDD\u5220\u9664\u610F\u5916\u7684\u4F1A\u8BDD\u76EE\u5F55 ${JSON.stringify(dir)}`);
154
154
  }
155
155
  targetDir = dir;
156
156
  }
@@ -172,7 +172,7 @@ async function deleteSessionCore(ctx, sessionId) {
172
172
  if (targetDir !== null) {
173
173
  const dirName = basename(targetDir);
174
174
  if (dirName !== sessionId && dirName !== encodeURIComponent(sessionId)) {
175
- throw new HttpError(500, `refusing to remove unexpected session directory ${JSON.stringify(targetDir)}`);
175
+ throw new HttpError(500, `\u62D2\u7EDD\u5220\u9664\u610F\u5916\u7684\u4F1A\u8BDD\u76EE\u5F55 ${JSON.stringify(targetDir)}`);
176
176
  }
177
177
  await rm(targetDir, { recursive: true, force: true });
178
178
  removedFiles = true;
@@ -199,7 +199,10 @@ async function deleteSessionCore(ctx, sessionId) {
199
199
  async function handleDelete(ctx, sessionId, res) {
200
200
  const live = ctx.get("sessions")?.get(sessionId);
201
201
  if (live !== void 0) {
202
- throw new HttpError(409, "session is live in this process; close it before deleting");
202
+ throw new HttpError(
203
+ 409,
204
+ "\u8BE5\u4F1A\u8BDD\u4ECD\u5728\u8FDB\u7A0B\u4E2D\u8FD0\u884C\uFF0C\u65E0\u6CD5\u5220\u9664\u3002\u5F52\u6863\u4E0D\u4F1A\u7ED3\u675F\u4F1A\u8BDD\u2014\u2014\u4F1A\u8BDD\u4F1A\u4E00\u76F4\u5B58\u6D3B\u5230 dsh web \u91CD\u542F\u4E3A\u6B62\uFF1B\u8BF7\u91CD\u542F dsh web \u540E\u518D\u5220\u9664"
205
+ );
203
206
  }
204
207
  const result = await deleteSessionCore(ctx, sessionId);
205
208
  sendJson(res, 200, {
@@ -209,19 +212,32 @@ async function handleDelete(ctx, sessionId, res) {
209
212
  });
210
213
  }
211
214
  async function handleDeleteAll(ctx, sessionIds, res) {
215
+ const sessions = ctx.get("sessions");
216
+ const headers = await ctx.sessionPersistence.list();
212
217
  let deleted = 0;
213
218
  let skipped = 0;
214
219
  let failed = 0;
220
+ const skippedIds = [];
215
221
  const failures = [];
216
222
  for (const sessionId of sessionIds) {
217
- const live = ctx.get("sessions")?.get(sessionId);
223
+ const live = sessions?.get(sessionId);
218
224
  if (live !== void 0) {
219
225
  skipped += 1;
226
+ skippedIds.push(sessionId);
220
227
  continue;
221
228
  }
222
229
  try {
223
- await deleteSessionCore(ctx, sessionId);
224
- deleted += 1;
230
+ const result = await deleteSessionCore(ctx, sessionId, headers);
231
+ if (result.detachErrors.length > 0) {
232
+ failed += 1;
233
+ failures.push({
234
+ sessionId,
235
+ error: `\u6587\u4EF6\u5DF2\u5220\u9664\uFF0C\u4F46 workspace \u8D26\u76EE\u89E3\u9664\u5931\u8D25\uFF1A${result.detachErrors.join("\uFF1B")}`
236
+ });
237
+ ctx.logger.warn(new Error(`archive-tab: batch delete incomplete for '${sessionId}': ${result.detachErrors.join("; ")}`));
238
+ } else {
239
+ deleted += 1;
240
+ }
225
241
  } catch (error) {
226
242
  failed += 1;
227
243
  const message = error instanceof HttpError ? error.message : String(error);
@@ -234,6 +250,7 @@ async function handleDeleteAll(ctx, sessionIds, res) {
234
250
  total: sessionIds.length,
235
251
  deleted,
236
252
  skipped,
253
+ skippedIds,
237
254
  failed,
238
255
  failures
239
256
  });
package/lib/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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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",
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\uFF08409\uFF0C\u4E2D\u6587\u63D0\u793A\uFF09\u3002\n *\n * \u300Clive\u300D= \u4F1A\u8BDD\u5BF9\u8C61\u4ECD\u6302\u5728\u8FDB\u7A0B\u5185\u5B58\u7684 SessionStore \u91CC\uFF1A\u5F52\u6863\u53EA\u9690\u85CF\u5217\u8868\uFF0C\n * \u4E0D\u7ED3\u675F\u4F1A\u8BDD\uFF0C\u4E14 harness \u6CA1\u6709\u516C\u5F00\u7684\u5F3A\u5236\u7ED3\u675F\u4F1A\u8BDD API\uFF08UI \u7684\u505C\u6B62/\u5173\u95ED\n * \u90FD\u53EA\u662F\u53D6\u6D88\u5F53\u524D turn\uFF09\u2014\u2014\u4F1A\u8BDD\u4F1A\u4E00\u76F4 live \u5230 dsh web \u8FDB\u7A0B\u91CD\u542F\u4E3A\u6B62\u3002\n * \u56E0\u6B64\u5220\u9664\u88AB\u62D2\u7EDD\u65F6\u63D0\u793A\u7528\u6237\u91CD\u542F\u540E\u91CD\u8BD5\uFF0C\u800C\u4E0D\u662F\u5C1D\u8BD5\u79C1\u6709\u94FE\u5F3A\u6740\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 * skipped\uFF08id \u968F skippedIds \u8FD4\u56DE\uFF09\uFF1B\u5176\u4ED6\u786C\u9519\u8BEF\uFF08\u76EE\u5F55\u540D\u5B88\u536B\u7B49\uFF09\u8BA1\u5165\n * failed \u4E14\u4E0D\u4E2D\u65AD\u6574\u6279\uFF0C\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, '\u8BF7\u6C42\u4F53\u8FC7\u5927\uFF08\u4E0A\u9650 1MB\uFF09'))\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, '\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON'))\n }\n })\n req.on('error', () => fail(new HttpError(400, '\u8BF7\u6C42\u6D41\u8BFB\u53D6\u5931\u8D25')))\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 \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32')\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 \u5FC5\u987B\u662F\u975E\u7A7A\u6570\u7EC4')\n }\n if (raw.length > 500) {\n throw new HttpError(400, 'sessionIds \u6700\u591A 500 \u9879')\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 \u53EA\u80FD\u5305\u542B\u975E\u7A7A\u5B57\u7B26\u4E32')\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 \u5199\u94FE\u4E0D\u53EF\u7528\uFF08harness API \u6F02\u79FB\uFF09\uFF1B\u6062\u590D/\u5220\u9664\u5DF2\u505C\u7528\uFF0C\u8BF7\u66F4\u65B0\u672C\u63D2\u4EF6',\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, `\u672A\u77E5\u8DEF\u7531 ${JSON.stringify(pathname)}`)\n if (req.method !== 'POST') throw new HttpError(405, '\u4EC5\u652F\u6301 POST \u65B9\u6CD5')\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 \u5FC5\u987B\u662F 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: '\u5185\u90E8\u9519\u8BEF' })\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 *\n * `headers` \u53EF\u9009\uFF1A\u6279\u91CF\u5220\u9664\u65F6\u7531\u8C03\u7528\u65B9\u9884\u5148 list() \u4E00\u6B21\u4F20\u5165\uFF0C\u907F\u514D\u6BCF\u4E2A\u4F1A\u8BDD\n * \u91CD\u590D\u5168\u91CF\u8BFB\u53D6\u6301\u4E45\u5316\u5217\u8868\uFF08N \u4E2A\u4F1A\u8BDD O(N) \u6B21 list \u7684 O(N\u00B2) \u9000\u5316\uFF09\uFF1B\u7F3A\u7701\u65F6\n * \u81EA\u5DF1\u8BFB\u53D6\uFF08\u5355\u5220\u8DEF\u5F84\uFF09\u3002\n */\nasync function deleteSessionCore(\n ctx: any,\n sessionId: string,\n headers?: SessionHeaderLike[],\n): 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 allHeaders = headers ?? (await ctx.sessionPersistence.list()) as SessionHeaderLike[]\n const header = allHeaders.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, `\u62D2\u7EDD\u5220\u9664\u610F\u5916\u7684\u4F1A\u8BDD\u76EE\u5F55 ${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, `\u62D2\u7EDD\u5220\u9664\u610F\u5916\u7684\u4F1A\u8BDD\u76EE\u5F55 ${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(\n 409,\n '\u8BE5\u4F1A\u8BDD\u4ECD\u5728\u8FDB\u7A0B\u4E2D\u8FD0\u884C\uFF0C\u65E0\u6CD5\u5220\u9664\u3002\u5F52\u6863\u4E0D\u4F1A\u7ED3\u675F\u4F1A\u8BDD\u2014\u2014\u4F1A\u8BDD\u4F1A\u4E00\u76F4\u5B58\u6D3B\u5230 dsh web \u91CD\u542F\u4E3A\u6B62\uFF1B\u8BF7\u91CD\u542F dsh web \u540E\u518D\u5220\u9664',\n )\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\uFF1Bdetach \u5931\u8D25\uFF08\u6587\u4EF6\u5DF2\u5220\u4F46\u8D26\u76EE\u672A\u89E3\u9664\uFF09\u540C\u6837\n * \u8BA1\u5165 failed \u5E76\u7ED9\u51FA\u660E\u7EC6\uFF0C\u7EDD\u4E0D\u9759\u9ED8\u8BB0\u4E3A\u6210\u529F\u3002\u54CD\u5E94\u643A\u5E26\u6C47\u603B\u8BA1\u6570\u3001\u88AB\u8DF3\u8FC7\n * \uFF08live\uFF09\u4F1A\u8BDD\u7684 id \u5217\u8868\u4E0E\u9010\u4F1A\u8BDD\u5931\u8D25\u660E\u7EC6\uFF0C\u8BA9\u5BA2\u6237\u7AEF\u80FD\u7ED9\u51FA\u5B8C\u6574\u63D0\u793A\u3002\n *\n * \u6301\u4E45\u5316\u5217\u8868\u53EA\u8BFB\u4E00\u6B21\u4F20\u7ED9 deleteSessionCore \u590D\u7528\uFF08\u89C1\u5176\u6CE8\u91CA\uFF09\uFF1Blist \u5931\u8D25\u5C5E\u4E8E\n * \u670D\u52A1\u7EA7\u6545\u969C\uFF0C\u76F4\u63A5\u6574\u6279 500 \u7531 handler \u515C\u5E95\uFF0C\u800C\u4E0D\u662F\u9010\u4E2A\u8BB0 failed\u3002\n */\nasync function handleDeleteAll(ctx: any, sessionIds: readonly string[], res: ServerResponse): Promise<void> {\n const sessions = ctx.get('sessions')\n const headers = await ctx.sessionPersistence.list() as SessionHeaderLike[]\n let deleted = 0\n let skipped = 0\n let failed = 0\n const skippedIds: string[] = []\n const failures: Array<{ sessionId: string; error: string }> = []\n for (const sessionId of sessionIds) {\n const live = sessions?.get(sessionId)\n if (live !== undefined) {\n skipped += 1\n skippedIds.push(sessionId)\n continue\n }\n try {\n const result = await deleteSessionCore(ctx, sessionId, headers)\n if (result.detachErrors.length > 0) {\n // detach \u5931\u8D25\u610F\u5473\u7740\u6587\u4EF6\u4E0E\u5F52\u6863\u90FD\u5DF2\u5904\u7406\u4F46 workspace \u8D26\u76EE\u672A\u89E3\u9664\u2014\u2014\n // \u5C5E\u4E8E\u300C\u5220\u9664\u4E0D\u5B8C\u6574\u300D\uFF0C\u4E0D\u80FD\u9759\u9ED8\u8BB0\u4E3A\u6210\u529F\u3002\u8BA1\u5165 failed \u5E76\u7ED9\u51FA\u660E\u7EC6\uFF0C\n // \u8BA9\u7528\u6237\u80FD\u6392\u67E5\u6B8B\u7559\u8D26\u76EE\u3002\n failed += 1\n failures.push({\n sessionId,\n error: `\u6587\u4EF6\u5DF2\u5220\u9664\uFF0C\u4F46 workspace \u8D26\u76EE\u89E3\u9664\u5931\u8D25\uFF1A${result.detachErrors.join('\uFF1B')}`,\n })\n ctx.logger.warn(new Error(`archive-tab: batch delete incomplete for '${sessionId}': ${result.detachErrors.join('; ')}`))\n } else {\n deleted += 1\n }\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 skippedIds,\n failed,\n failures,\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bit-ark/dsh-archive",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Adds a 归档 (Archive) tab after the Trajectory tab: lists the current project's archived sessions with restore (unarchive) and hard delete.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",