@dsh-sparrow/dsh-file-manage 0.1.0 → 0.1.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  English | [简体中文](CHANGELOG.zh-CN.md)
4
4
 
5
+ ## 0.1.2-rc.1 (2026-09-05)
6
+
7
+ - Version-line alignment with official dsh 0.1.2-rc.1 (stability line).
8
+ - Pagination and delete race fixes: after quickly reopening the panel or hitting Retry, stale pages no longer merge into the fresh list, "Load more" no longer sticks disabled, and a late delete callback no longer dismisses a newly opened confirmation dialog.
9
+ - Requests that outlive the panel now stop by themselves instead of hanging (15s cap for list/delete, 60s for the count pass), and the count pass stops paging as soon as the client connection drops, so an abandoned count no longer keeps burning API quota.
10
+ - The sidebar "Cloud Files" button now aligns its width with Settings (same as the Archive entry).
11
+ - "Load more" now lives at the end of the file list and the list auto-scrolls to the bottom after each page loads (the button no longer sits in a fixed panel footer).
12
+ - A subtle 🐦 dsh-sparrow brand line closes the panel.
13
+
5
14
  ## 0.1.0 (2026-09-02)
6
15
 
7
16
  - Promoted 0.1.0 (identical to 0.1.0-alpha.3).
@@ -2,6 +2,15 @@
2
2
 
3
3
  简体中文 | [English](CHANGELOG.md)
4
4
 
5
+ ## 0.1.2-rc.1(2026-09-05)
6
+
7
+ - 版本对齐官方 dsh 0.1.2-rc.1(稳定线)。
8
+ - 翻页与删除的并发修复:快速关开面板或点击重试后,陈旧页不再混入新列表、「加载更多」不再卡死,迟到的删除回调不再误关新弹出的确认框。
9
+ - 请求超时后自行停止不再无限挂起(列表/删除 15s、计数 60s 上限),计数翻页在客户端连接断开(超时/导航)时立即停下,被放弃的计数不再持续消耗 API 配额。
10
+ - 侧边栏「云端文件」按钮与 Settings 同宽对齐(与归档入口一致)。
11
+ - 「加载更多」移到文件列表末尾,每页加载后自动滚到列表底部(按钮不再固定在面板底部)。
12
+ - 面板底部新增 🐦 dsh-sparrow 品牌行。
13
+
5
14
  ## 0.1.0 (2026-09-02)
6
15
 
7
16
  - 转正 0.1.0(与 0.1.0-alpha.3 一致)。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 peiyucn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -12,14 +12,14 @@ Large images pasted into DSH are auto-uploaded to the DeepSeek Files API, but th
12
12
  dsh plugin --profile web add @dsh-sparrow/dsh-file-manage
13
13
  ```
14
14
 
15
- Requires dsh ≥ 0.1.2-alpha.4 and a working `pnpm` (`dsh plugin` forwards installation to pnpm).
15
+ Requires dsh ≥ 0.1.2-rc.1 and a working `pnpm` (`dsh plugin` forwards installation to pnpm).
16
16
 
17
17
  > Do **not** run `npm install @dsh-sparrow/dsh-file-manage` directly: that only downloads the package into some `node_modules` and does not register it with the DSH web profile. Use the `dsh plugin` command above, then restart DSH.
18
18
 
19
19
  ## Usage
20
20
 
21
21
  * **Entry**: the "Cloud Files" button at the sidebar footer
22
- * **List**: 20 files per page with "Load more" pagination (official after cursor, newest first); each row shows filename / size / upload time / expiry time (when present)
22
+ * **List**: newest first with "Load more" at the bottom (100 more files per click, official after cursor); each row shows filename / size / upload time / expiry time (when present)
23
23
  * **Quota**: the panel header shows the total count and a netdisk-style quota bar (used storage / the official 25 GiB limit)
24
24
  * **Delete**: per-row delete with confirmation; "DSH auto-uploaded" files (`dsh-` prefix) get an extra note — sessions referencing them will transparently re-upload on next use (may be slower)
25
25
  * **Copy file_id**: one-click copy per row
@@ -40,3 +40,5 @@ Requires dsh ≥ 0.1.2-alpha.4 and a working `pnpm` (`dsh plugin` forwards insta
40
40
  ## Uninstall & leftovers
41
41
 
42
42
  * The plugin keeps no local state; uninstalling leaves the cloud files untouched and DSH behavior unchanged
43
+
44
+ **Changelog**: [CHANGELOG.md](https://github.com/peiyucn/dsh-sparrow/blob/main/plugins/dsh-file-manage/CHANGELOG.md)
package/README.zh-CN.md CHANGED
@@ -12,14 +12,14 @@ DSH 贴图的大图会自动上传到 DeepSeek Files API(云端文件库),
12
12
  dsh plugin --profile web add @dsh-sparrow/dsh-file-manage
13
13
  ```
14
14
 
15
- 适配 dsh ≥ 0.1.2-alpha.4,并需要可用的 `pnpm`(`dsh plugin` 会把安装操作转发给 pnpm)。
15
+ 适配 dsh ≥ 0.1.2-rc.1,并需要可用的 `pnpm`(`dsh plugin` 会把安装操作转发给 pnpm)。
16
16
 
17
17
  > **不要**直接执行 `npm install @dsh-sparrow/dsh-file-manage`:那只会把包下载到某个 `node_modules`,不会注册进 DSH 的 web profile。请使用上面的 `dsh plugin` 命令安装,并在安装后重启 DSH。
18
18
 
19
19
  ## 使用
20
20
 
21
21
  * **入口**:侧边栏底部「云端文件」按钮
22
- * **列表**:每页 20 条,底部「加载更多」翻页(官方 after 游标,最新在前);行内展示文件名 / 大小 / 上传时间 / 到期时间(有则显)
22
+ * **列表**:最新在前,底部「加载更多」每次多显示 100 条(官方 after 游标翻页);行内展示文件名 / 大小 / 上传时间 / 到期时间(有则显)
23
23
  * **配额**:列表顶部显示总数与网盘式配额进度条(已用存储 / 25 GiB 官方上限)
24
24
  * **删除**:行内删除按钮二次确认;「DSH 自动上传」角标(`dsh-` 前缀)的文件有额外提示——删除后旧会话再次引用时官方会自动重新上传(可能稍慢)
25
25
  * **复制 file_id**:行内按钮一键复制
@@ -40,3 +40,5 @@ dsh plugin --profile web add @dsh-sparrow/dsh-file-manage
40
40
  ## 卸载与残留
41
41
 
42
42
  * 插件无任何本地持久化状态;卸载后云端文件原样保留,DSH 行为不受影响
43
+
44
+ **更新日志**:[CHANGELOG.zh-CN.md](https://github.com/peiyucn/dsh-sparrow/blob/main/plugins/dsh-file-manage/CHANGELOG.zh-CN.md)
package/lib/client.js CHANGED
@@ -32,19 +32,56 @@ __export(index_exports, {
32
32
  });
33
33
  module.exports = __toCommonJS(index_exports);
34
34
 
35
+ // src/client/paging.ts
36
+ var RENDER_PAGE_SIZE = 100;
37
+ var FILE_PAGE_SIZE = 200;
38
+ function renderedRowCount(totalRows, renderLimit) {
39
+ const total = Math.max(0, Math.trunc(totalRows));
40
+ const limit = Math.max(0, Math.trunc(renderLimit));
41
+ return Math.min(total, limit);
42
+ }
43
+ function hasLoadMore(totalRows, renderLimit, dataHasMore) {
44
+ return renderedRowCount(totalRows, renderLimit) < totalRows || dataHasMore;
45
+ }
46
+
35
47
  // src/client/api.ts
48
+ var COUNT_CACHE_TTL_MS = 3e4;
49
+ var countCache = null;
50
+ function readCountCache(entry, nowMs) {
51
+ return entry !== null && nowMs < entry.expiresAt ? entry.value : null;
52
+ }
53
+ function invalidateCountCache() {
54
+ countCache = null;
55
+ }
36
56
  var REQUEST_TIMEOUT_MS = 15e3;
37
57
  var COUNT_TIMEOUT_MS = 6e4;
58
+ async function fetchWithTimeout(path, init, timeoutMs) {
59
+ try {
60
+ return await fetch(path, { ...init, signal: AbortSignal.timeout(timeoutMs) });
61
+ } catch (error) {
62
+ if (error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")) {
63
+ throw new Error(`\u8BF7\u6C42\u8D85\u65F6\uFF08${Math.round(timeoutMs / 1e3)}s\uFF09\uFF0C\u8BF7\u91CD\u8BD5`);
64
+ }
65
+ throw error;
66
+ }
67
+ }
68
+ async function readJson(response) {
69
+ try {
70
+ return await response.json();
71
+ } catch {
72
+ return {};
73
+ }
74
+ }
75
+ async function readErrorMessage(response) {
76
+ const payload = await readJson(response);
77
+ return payload.error?.message ?? `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${response.status}\uFF09`;
78
+ }
38
79
  async function listApi(after) {
39
- const params = new URLSearchParams();
80
+ const params = new URLSearchParams({ limit: String(FILE_PAGE_SIZE) });
40
81
  if (after !== void 0) params.set("after", after);
41
- const response = await fetch(`/api/file-manage/list?${params.toString()}`, {
42
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
43
- });
44
- const payload = await response.json();
45
- if (!response.ok) {
46
- throw new Error(payload.error?.message ?? `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${response.status}\uFF09`);
47
- }
82
+ const response = await fetchWithTimeout(`/api/file-manage/list?${params.toString()}`, {}, REQUEST_TIMEOUT_MS);
83
+ if (!response.ok) throw new Error(await readErrorMessage(response));
84
+ const payload = await readJson(response);
48
85
  return {
49
86
  rows: payload.items ?? [],
50
87
  hasMore: payload.hasMore ?? false,
@@ -52,12 +89,12 @@ async function listApi(after) {
52
89
  };
53
90
  }
54
91
  async function countApi() {
55
- const response = await fetch("/api/file-manage/count", { signal: AbortSignal.timeout(COUNT_TIMEOUT_MS) });
56
- const payload = await response.json();
57
- if (!response.ok) {
58
- throw new Error(payload.error?.message ?? `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${response.status}\uFF09`);
59
- }
60
- return {
92
+ const cached = readCountCache(countCache, Date.now());
93
+ if (cached !== null) return cached;
94
+ const response = await fetchWithTimeout("/api/file-manage/count", {}, COUNT_TIMEOUT_MS);
95
+ if (!response.ok) throw new Error(await readErrorMessage(response));
96
+ const payload = await readJson(response);
97
+ const summary = {
61
98
  count: payload.count ?? 0,
62
99
  totalBytes: payload.totalBytes ?? 0,
63
100
  totalBytesLabel: payload.totalBytesLabel ?? "0 B",
@@ -65,17 +102,14 @@ async function countApi() {
65
102
  quotaBytesLabel: payload.quotaBytesLabel ?? "0 B",
66
103
  quotaCount: payload.quotaCount ?? 0
67
104
  };
105
+ countCache = { value: summary, expiresAt: Date.now() + COUNT_CACHE_TTL_MS };
106
+ return summary;
68
107
  }
69
108
  async function deleteApi(id) {
70
109
  const params = new URLSearchParams({ id });
71
- const response = await fetch(`/api/file-manage/files?${params.toString()}`, {
72
- method: "DELETE",
73
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
74
- });
75
- const payload = await response.json();
76
- if (!response.ok) {
77
- throw new Error(payload.error?.message ?? `\u8BF7\u6C42\u5931\u8D25\uFF08HTTP ${response.status}\uFF09`);
78
- }
110
+ const response = await fetchWithTimeout(`/api/file-manage/files?${params.toString()}`, { method: "DELETE" }, REQUEST_TIMEOUT_MS);
111
+ if (!response.ok) throw new Error(await readErrorMessage(response));
112
+ invalidateCountCache();
79
113
  }
80
114
 
81
115
  // src/client/FileManageDock.tsx
@@ -411,17 +445,22 @@ var styles = {
411
445
  fontSize: 12,
412
446
  lineHeight: "18px"
413
447
  },
414
- footerBar: {
448
+ /** 合集品牌 footer:固定面板底部(滚动区之外),顶部分割线与内容区隔开;紧凑规格。 */
449
+ footer: {
415
450
  flex: "none",
416
- display: "flex",
417
- justifyContent: "center",
418
- // 列表已包区块卡,不再加顶部分隔线(避免与卡片叠加显乱)。
419
- padding: "10px 24px 14px"
451
+ boxSizing: "border-box",
452
+ padding: "6px 24px 8px",
453
+ borderTop: "1px solid var(--dsw-alias-border-l1, #e2e5ea)",
454
+ textAlign: "center",
455
+ fontSize: 11,
456
+ lineHeight: "16px",
457
+ color: "var(--dsw-alias-label-tertiary, #8a919f)"
420
458
  }
421
459
  };
422
460
 
423
461
  // src/client/FileManageDock.tsx
424
462
  var import_jsx_runtime = require("react/jsx-runtime");
463
+ var COPIED_RESET_MS = 2e3;
425
464
  function FileManageDock({ wide, listFiles, deleteFile, countFiles, t }) {
426
465
  const [open, setOpen] = (0, import_react.useState)(false);
427
466
  const [rows, setRows] = (0, import_react.useState)([]);
@@ -437,13 +476,20 @@ function FileManageDock({ wide, listFiles, deleteFile, countFiles, t }) {
437
476
  const [summaryPending, setSummaryPending] = (0, import_react.useState)(false);
438
477
  const closeButtonRef = (0, import_react.useRef)(null);
439
478
  const refreshSeqRef = (0, import_react.useRef)(0);
479
+ const bodyRef = (0, import_react.useRef)(null);
480
+ const scrollPendingRef = (0, import_react.useRef)(false);
440
481
  const copiedTimerRef = (0, import_react.useRef)(null);
482
+ const [renderLimit, setRenderLimit] = (0, import_react.useState)(RENDER_PAGE_SIZE);
441
483
  const reload = (0, import_react.useCallback)(() => {
442
484
  const seq = ++refreshSeqRef.current;
443
485
  setLoading(true);
444
486
  setSummaryPending(true);
445
487
  setSummary(null);
446
488
  setError(null);
489
+ setLoadingMore(false);
490
+ setBusyDelete(false);
491
+ scrollPendingRef.current = false;
492
+ setRenderLimit(RENDER_PAGE_SIZE);
447
493
  listFiles().then((page) => {
448
494
  if (seq !== refreshSeqRef.current) return;
449
495
  setRows(page.rows);
@@ -486,18 +532,29 @@ function FileManageDock({ wide, listFiles, deleteFile, countFiles, t }) {
486
532
  if (copiedTimerRef.current !== null) window.clearTimeout(copiedTimerRef.current);
487
533
  }, []);
488
534
  const loadMore = (0, import_react.useCallback)(() => {
489
- if (lastId === void 0 || loadingMore) return;
535
+ if (loadingMore) return;
536
+ if (renderLimit < rows.length) {
537
+ setRenderLimit((limit) => limit + RENDER_PAGE_SIZE);
538
+ scrollPendingRef.current = true;
539
+ return;
540
+ }
541
+ if (lastId === void 0) return;
542
+ const seq = refreshSeqRef.current;
490
543
  setLoadingMore(true);
544
+ scrollPendingRef.current = true;
491
545
  listFiles(lastId).then((page) => {
546
+ if (seq !== refreshSeqRef.current) return;
492
547
  setRows((current) => [...current, ...page.rows]);
493
548
  setHasMore(page.hasMore);
494
549
  setLastId(page.lastId);
550
+ setRenderLimit((limit) => limit + RENDER_PAGE_SIZE);
495
551
  }).catch((reason) => {
552
+ if (seq !== refreshSeqRef.current) return;
496
553
  setError(reason instanceof Error ? reason.message : String(reason));
497
554
  }).finally(() => {
498
- setLoadingMore(false);
555
+ if (seq === refreshSeqRef.current) setLoadingMore(false);
499
556
  });
500
- }, [lastId, loadingMore, listFiles]);
557
+ }, [renderLimit, rows.length, lastId, loadingMore, listFiles]);
501
558
  const copyId = (0, import_react.useCallback)(async (row) => {
502
559
  try {
503
560
  await navigator.clipboard.writeText(row.id);
@@ -505,27 +562,39 @@ function FileManageDock({ wide, listFiles, deleteFile, countFiles, t }) {
505
562
  if (copiedTimerRef.current !== null) window.clearTimeout(copiedTimerRef.current);
506
563
  copiedTimerRef.current = window.setTimeout(() => {
507
564
  setCopied(null);
508
- }, 2e3);
565
+ }, COPIED_RESET_MS);
509
566
  } catch {
510
567
  setCopied(null);
511
568
  setError(t("copyFailed"));
512
569
  }
513
570
  }, [t]);
571
+ (0, import_react.useEffect)(() => {
572
+ if (!scrollPendingRef.current) return;
573
+ scrollPendingRef.current = false;
574
+ const el = bodyRef.current;
575
+ if (el !== null) el.scrollTop = el.scrollHeight;
576
+ }, [rows, renderLimit]);
514
577
  const initialLoading = loading || summaryPending;
515
578
  const quotaRatio = summary === null ? 0 : storageUsageRatio(summary.totalBytes, summary.quotaBytes);
579
+ const visibleCount = renderedRowCount(rows.length, renderLimit);
516
580
  const submitDelete = (0, import_react.useCallback)(() => {
517
581
  if (confirming === null || busyDelete) return;
582
+ const seq = refreshSeqRef.current;
583
+ const deleteId = confirming.id;
518
584
  setBusyDelete(true);
519
- deleteFile(confirming.id).then(() => {
520
- setRows((current) => current.filter((row) => row.id !== confirming.id));
521
- setConfirming(null);
522
- void countFiles().then(setSummary).catch(() => {
585
+ deleteFile(deleteId).then(() => {
586
+ setRows((current) => current.filter((row) => row.id !== deleteId));
587
+ setConfirming((current) => current !== null && current.id === deleteId ? null : current);
588
+ void countFiles().then((next) => {
589
+ if (seq === refreshSeqRef.current) setSummary(next);
590
+ }).catch(() => {
523
591
  });
524
592
  }).catch((reason) => {
593
+ if (seq !== refreshSeqRef.current) return;
525
594
  setError(reason instanceof Error ? reason.message : String(reason));
526
- setConfirming(null);
595
+ setConfirming((current) => current !== null && current.id === deleteId ? null : current);
527
596
  }).finally(() => {
528
- setBusyDelete(false);
597
+ if (seq === refreshSeqRef.current) setBusyDelete(false);
529
598
  });
530
599
  }, [busyDelete, confirming, deleteFile, countFiles]);
531
600
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
@@ -560,9 +629,9 @@ function FileManageDock({ wide, listFiles, deleteFile, countFiles, t }) {
560
629
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-file-manage-quota-text", children: `${summary.totalBytesLabel} / ${summary.quotaBytesLabel} \xB7 ${t("quota.used", { percent: formatUsagePercent(quotaRatio) })}` }),
561
630
  quotaRatio > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-file-manage-quota-fill", style: { width: `${Math.round(quotaRatio * 100)}%` } }) : null
562
631
  ] }),
563
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-file-manage-count", children: rows.length < summary.count ? t("summary.loaded", { loaded: rows.length, count: summary.count }) : t("summary.count", { count: summary.count }) })
632
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-file-manage-count", children: visibleCount < summary.count ? t("summary.loaded", { loaded: visibleCount, count: summary.count }) : t("summary.count", { count: summary.count }) })
564
633
  ] }) : null,
565
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...styles.body, padding: summary !== null ? "0 24px 24px" : "12px 24px 24px" }, className: "dsh-file-manage-body", children: initialLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-file-manage-loading", role: "status", children: [
634
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ref: bodyRef, style: { ...styles.body, padding: summary !== null ? "0 24px 24px" : "12px 24px 24px" }, className: "dsh-file-manage-body", children: initialLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-file-manage-loading", role: "status", children: [
566
635
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-file-manage-spinner", "aria-hidden": true }),
567
636
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("loading") })
568
637
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
@@ -574,7 +643,7 @@ function FileManageDock({ wide, listFiles, deleteFile, countFiles, t }) {
574
643
  }, children: t("retry") })
575
644
  ] }) : null,
576
645
  error === null && rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.secondarySmall, children: t("empty") }) : null,
577
- rows.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-file-manage-card", children: rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.row, children: [
646
+ rows.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-file-manage-card", children: rows.slice(0, renderLimit).map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.row, children: [
578
647
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { minWidth: 0 }, children: [
579
648
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 }, children: [
580
649
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.title, title: row.filename, children: row.filename }),
@@ -611,20 +680,21 @@ function FileManageDock({ wide, listFiles, deleteFile, countFiles, t }) {
611
680
  }
612
681
  )
613
682
  ] })
614
- ] }, row.id)) }) : null
683
+ ] }, row.id)) }) : null,
684
+ hasLoadMore(rows.length, renderLimit, hasMore) && !initialLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", justifyContent: "center", paddingTop: 12 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
685
+ "button",
686
+ {
687
+ type: "button",
688
+ className: "dsh-file-manage-btn",
689
+ disabled: loadingMore,
690
+ onClick: () => {
691
+ void loadMore();
692
+ },
693
+ children: loadingMore ? t("loading") : t("loadMore")
694
+ }
695
+ ) }) : null
615
696
  ] }) }),
616
- hasMore && !initialLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.footerBar, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
617
- "button",
618
- {
619
- type: "button",
620
- className: "dsh-file-manage-btn",
621
- disabled: loadingMore,
622
- onClick: () => {
623
- void loadMore();
624
- },
625
- children: loadingMore ? t("loading") : t("loadMore")
626
- }
627
- ) }) : null
697
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.footer, children: "\u{1F426} dsh-sparrow" })
628
698
  ] }) }) : null,
629
699
  confirming !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-file-manage-confirm-overlay", role: "presentation", onMouseDown: (event) => {
630
700
  if (event.target === event.currentTarget && !busyDelete) setConfirming(null);
package/lib/host.js CHANGED
@@ -9,6 +9,11 @@ const PREFIX = '/api/file-manage';
9
9
  const DEFAULT_BASE_URL = 'https://api.deepseek.com';
10
10
  const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY';
11
11
  const LLM_DEEPSEEK_NS = 'llm-deepseek';
12
+ /**
13
+ * 单页 list / 单条 delete 的上游超时:官方 client 的 fetch 本身无默认超时,客户端断开
14
+ * (关面板 / 15s 超时)后残留的上游请求必须自限,否则会无限挂起并占用官方 API 配额。
15
+ */
16
+ const UPSTREAM_TIMEOUT_MS = 15_000;
12
17
  /** 插件自身错误(面板可读的 code / message + HTTP 状态)。 */
13
18
  class FileManageError extends Error {
14
19
  code;
@@ -92,6 +97,7 @@ export function apply(ctx) {
92
97
  ...query.after === undefined ? {} : { after: DeepSeekFileId(query.after) },
93
98
  limit: query.limit,
94
99
  order: query.order,
100
+ signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
95
101
  });
96
102
  sendJson(res, 200, {
97
103
  items: page.data.map(toFileRow),
@@ -108,6 +114,9 @@ export function apply(ctx) {
108
114
  let totalBytes = 0;
109
115
  let after;
110
116
  for (let page = 0; page < MAX_COUNT_PAGES; page++) {
117
+ // 客户端已断开(超时 / 关面板)时停止翻页:剩余页数没有接收方,白烧官方 API 配额。
118
+ if (res.destroyed)
119
+ break;
111
120
  const result = await client.list({
112
121
  ...after === undefined ? {} : { after },
113
122
  limit: COUNT_PAGE_LIMIT,
@@ -139,7 +148,7 @@ export function apply(ctx) {
139
148
  }
140
149
  const connection = await resolveConnection(ctx);
141
150
  const client = new DeepSeekFilesClient({ baseURL: connection.baseURL, apiKey: connection.apiKey });
142
- await client.delete(DeepSeekFileId(id));
151
+ await client.delete(DeepSeekFileId(id), AbortSignal.timeout(UPSTREAM_TIMEOUT_MS));
143
152
  sendJson(res, 200, { deleted: true, id });
144
153
  return;
145
154
  }
@@ -9,6 +9,17 @@ export interface FileCountSummary {
9
9
  quotaBytesLabel: string;
10
10
  quotaCount: number;
11
11
  }
12
+ /** 总数统计 TTL 缓存时长:面板快速重开/重试不重复 count 全量翻页(count 要翻到底,配额内最多 10 页)。 */
13
+ export declare const COUNT_CACHE_TTL_MS = 30000;
14
+ /** TTL 缓存条目(api.ts 模块级单例:面板单实例,随 client bundle 生命周期,有界一条)。 */
15
+ export interface CountCacheEntry {
16
+ readonly value: FileCountSummary;
17
+ readonly expiresAt: number;
18
+ }
19
+ /** TTL 读缓存(时钟注入的纯逻辑,供单测):未过期返回快照,过期或缺条返回 null。 */
20
+ export declare function readCountCache(entry: CountCacheEntry | null, nowMs: number): FileCountSummary | null;
21
+ /** 删除成功后失效计数缓存:下一次 count 拉取重新翻页,包含删除结果。 */
22
+ export declare function invalidateCountCache(): void;
12
23
  export declare function listApi(after?: string): Promise<{
13
24
  rows: FileRow[];
14
25
  hasMore: boolean;
@@ -0,0 +1,13 @@
1
+ /** 文件面板渲染窗口纯逻辑(对齐 archive-manage spec 09):数据分页与 DOM 渲染窗口分离。 @module dsh-file-manage/client/paging */
2
+ /** 渲染窗口大小:每次渲染的行数上限,「加载更多」按此递增(archive 同款 100)。 */
3
+ export declare const RENDER_PAGE_SIZE = 100;
4
+ /**
5
+ * 数据分页大小:每次 list 拉取的行数。必须大于渲染窗口(200 > 100),
6
+ * 窗口才会真正裁剪 DOM——否则窗口永远跑在数据前面,退化回全量渲染。
7
+ */
8
+ export declare const FILE_PAGE_SIZE = 200;
9
+ /** 窗口内可见行数:min(已加载行数, 窗口上限);负数/小数钳到安全值(不抛)。 */
10
+ export declare function renderedRowCount(totalRows: number, renderLimit: number): number;
11
+ /** 「加载更多」是否可见:窗口未盖满已加载行(纯延伸窗口),或服务端还有更多数据(延伸窗口 + 拉页)。 */
12
+ export declare function hasLoadMore(totalRows: number, renderLimit: number, dataHasMore: boolean): boolean;
13
+ //# sourceMappingURL=paging.d.ts.map
@@ -56,11 +56,16 @@ export declare const styles: {
56
56
  fontSize: number;
57
57
  lineHeight: string;
58
58
  };
59
- readonly footerBar: {
59
+ /** 合集品牌 footer:固定面板底部(滚动区之外),顶部分割线与内容区隔开;紧凑规格。 */
60
+ readonly footer: {
60
61
  flex: string;
61
- display: "flex";
62
- justifyContent: "center";
62
+ boxSizing: "border-box";
63
63
  padding: string;
64
+ borderTop: string;
65
+ textAlign: "center";
66
+ fontSize: number;
67
+ lineHeight: string;
68
+ color: "var(--dsw-alias-label-tertiary, #8a919f)";
64
69
  };
65
70
  };
66
71
  //# sourceMappingURL=styles.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-sparrow/dsh-file-manage",
3
3
  "description": "DSH web plugin: manage DeepSeek Files API cloud files from the sidebar — list with cursor pagination, delete, and copy file_id (reuses the official DeepSeekFilesClient, no extra credentials)",
4
- "version": "0.1.0",
4
+ "version": "0.1.2-rc.1",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "files": [
26
26
  "lib/**/*.js",
27
+ "!lib/client/**",
27
28
  "lib/types/**/*.d.ts",
28
29
  "cordis.patch.yml",
29
30
  "README.md",
@@ -37,8 +38,7 @@
37
38
  "client": {
38
39
  "platform": "web",
39
40
  "inject": [
40
- "@deepseek-ai/dsh-client-runtime",
41
- "@deepseek-ai/dsh-client-locale",
41
+ "@deepseek-ai/dsh-client-locale",
42
42
  "@deepseek-ai/dsh-client-ui-sidebar"
43
43
  ]
44
44
  }
@@ -48,11 +48,11 @@
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@deepseek-ai/cordis": "^0.1.1-rc.2 || ^4.0.1",
51
- "@deepseek-ai/dsh-credentials": "^0.1.2-alpha.4",
52
- "@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.4",
53
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.4",
54
- "@deepseek-ai/dsh-llm-deepseek": "^0.1.2-alpha.4",
55
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.4"
51
+ "@deepseek-ai/dsh-credentials": "^0.1.2-rc.1",
52
+ "@deepseek-ai/dsh-host-webserver": "^0.1.2-rc.1",
53
+ "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
54
+ "@deepseek-ai/dsh-llm-deepseek": "^0.1.2-rc.1",
55
+ "@deepseek-ai/dsh-settings": "^0.1.2-rc.1"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@deepseek-ai/cordis": "0.1.1-rc.2",
@@ -61,11 +61,11 @@
61
61
  "@deepseek-ai/dsh-client-ui-primitives": "0.1.1-rc.2",
62
62
  "@deepseek-ai/dsh-client-ui-sidebar": "0.1.1-rc.2",
63
63
  "@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
64
- "@deepseek-ai/dsh-credentials": "0.1.2-alpha.4",
65
- "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.4",
66
- "@deepseek-ai/dsh-llm": "0.1.2-alpha.4",
67
- "@deepseek-ai/dsh-llm-deepseek": "0.1.2-alpha.4",
68
- "@deepseek-ai/dsh-settings": "0.1.2-alpha.4",
64
+ "@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
65
+ "@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
66
+ "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
67
+ "@deepseek-ai/dsh-llm-deepseek": "0.1.2-rc.1",
68
+ "@deepseek-ai/dsh-settings": "0.1.2-rc.1",
69
69
  "@types/node": "^22.20.0",
70
70
  "@types/react": "~18.3.1",
71
71
  "@types/react-dom": "~18.3.1",
@@ -1,154 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- /** 云端文件面板视图:入口按钮 + 弹窗(列表 / 翻页 / 删除确认 / 复制 / 配额条);样式与请求分别在 styles.ts / api.ts。 */
3
- import { useCallback, useEffect, useRef, useState } from 'react';
4
- import { IconCloseOutline16, IconFolderOpenOutline16 } from '@deepseek-ai/dsh-client-ui-primitives';
5
- import { formatUsagePercent, storageUsageRatio } from './quota.js';
6
- import { styles } from './styles.js';
7
- /** 面板状态:首屏 loading / 错误横幅 / 列表 + 加载更多 / 删除确认框(web 确认框替代原生 confirm)。 */
8
- export function FileManageDock({ wide, listFiles, deleteFile, countFiles, t }) {
9
- const [open, setOpen] = useState(false);
10
- const [rows, setRows] = useState([]);
11
- const [hasMore, setHasMore] = useState(false);
12
- const [lastId, setLastId] = useState(undefined);
13
- const [loading, setLoading] = useState(false);
14
- const [loadingMore, setLoadingMore] = useState(false);
15
- const [error, setError] = useState(null);
16
- const [confirming, setConfirming] = useState(null);
17
- const [busyDelete, setBusyDelete] = useState(false);
18
- const [copied, setCopied] = useState(null);
19
- const [summary, setSummary] = useState(null);
20
- /** 总数统计请求尚未落定:与 loading 一起构成首屏 ready 门(2026-09-01 整页 loading)。 */
21
- const [summaryPending, setSummaryPending] = useState(false);
22
- const closeButtonRef = useRef(null);
23
- /** 刷新代际:快速关/开面板产生并发请求时,只让最新一次的结果落地(陈旧响应竞态,archive 同款)。 */
24
- const refreshSeqRef = useRef(0);
25
- /** 「已复制」反馈 2s 复位(archive 同款)。 */
26
- const copiedTimerRef = useRef(null);
27
- /** 首屏 / 重试共用:列表与总数统计并发拉取,两者都落定才揭开内容(整页 loading 防闪动,2026-09-01)。 */
28
- const reload = useCallback(() => {
29
- const seq = ++refreshSeqRef.current;
30
- setLoading(true);
31
- setSummaryPending(true);
32
- setSummary(null);
33
- setError(null);
34
- listFiles()
35
- .then(page => {
36
- if (seq !== refreshSeqRef.current)
37
- return;
38
- setRows(page.rows);
39
- setHasMore(page.hasMore);
40
- setLastId(page.lastId);
41
- })
42
- .catch(reason => {
43
- if (seq !== refreshSeqRef.current)
44
- return;
45
- setError(reason instanceof Error ? reason.message : String(reason));
46
- })
47
- .finally(() => {
48
- if (seq === refreshSeqRef.current)
49
- setLoading(false);
50
- });
51
- // 总数统计为 best-effort:接口失败只隐藏统计行,不影响列表;同样受刷新代际约束。
52
- void countFiles()
53
- .then(next => {
54
- if (seq === refreshSeqRef.current)
55
- setSummary(next);
56
- })
57
- .catch(() => { })
58
- .finally(() => {
59
- if (seq === refreshSeqRef.current)
60
- setSummaryPending(false);
61
- });
62
- }, [listFiles, countFiles]);
63
- // 官方弹窗行为:打开时聚焦关闭按钮,Esc 关闭。
64
- useEffect(() => {
65
- if (!open)
66
- return;
67
- closeButtonRef.current?.focus();
68
- const onKeyDown = (event) => {
69
- if (event.key === 'Escape')
70
- setOpen(false);
71
- };
72
- document.addEventListener('keydown', onKeyDown);
73
- return () => { document.removeEventListener('keydown', onKeyDown); };
74
- }, [open]);
75
- useEffect(() => {
76
- if (!open)
77
- return;
78
- setConfirming(null);
79
- setSummary(null);
80
- setRows([]);
81
- setHasMore(false);
82
- setLastId(undefined);
83
- reload();
84
- }, [open, reload]);
85
- useEffect(() => () => {
86
- if (copiedTimerRef.current !== null)
87
- window.clearTimeout(copiedTimerRef.current);
88
- }, []);
89
- const loadMore = useCallback(() => {
90
- if (lastId === undefined || loadingMore)
91
- return;
92
- setLoadingMore(true);
93
- listFiles(lastId)
94
- .then(page => {
95
- setRows(current => [...current, ...page.rows]);
96
- setHasMore(page.hasMore);
97
- setLastId(page.lastId);
98
- })
99
- .catch(reason => { setError(reason instanceof Error ? reason.message : String(reason)); })
100
- .finally(() => { setLoadingMore(false); });
101
- }, [lastId, loadingMore, listFiles]);
102
- const copyId = useCallback(async (row) => {
103
- try {
104
- await navigator.clipboard.writeText(row.id);
105
- setCopied(row.id);
106
- if (copiedTimerRef.current !== null)
107
- window.clearTimeout(copiedTimerRef.current);
108
- copiedTimerRef.current = window.setTimeout(() => { setCopied(null); }, 2_000);
109
- }
110
- catch {
111
- setCopied(null);
112
- setError(t('copyFailed'));
113
- }
114
- }, [t]);
115
- /** 首屏 ready 门:列表 + 总数都落定前展示整页 loading(2026-09-01)。 */
116
- const initialLoading = loading || summaryPending;
117
- const quotaRatio = summary === null ? 0 : storageUsageRatio(summary.totalBytes, summary.quotaBytes);
118
- const submitDelete = useCallback(() => {
119
- if (confirming === null || busyDelete)
120
- return;
121
- setBusyDelete(true);
122
- deleteFile(confirming.id)
123
- .then(() => {
124
- setRows(current => current.filter(row => row.id !== confirming.id));
125
- setConfirming(null);
126
- // 删除后刷新总数(best-effort)。
127
- void countFiles().then(setSummary).catch(() => { });
128
- })
129
- .catch(reason => {
130
- setError(reason instanceof Error ? reason.message : String(reason));
131
- setConfirming(null);
132
- })
133
- .finally(() => { setBusyDelete(false); });
134
- }, [busyDelete, confirming, deleteFile, countFiles]);
135
- return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: wide ? 'dsh-file-manage-trigger' : 'dsh-file-manage-trigger dsh-file-manage-trigger-rail', title: t('dialog.title'), "aria-haspopup": "dialog", "aria-expanded": open, onClick: () => { setOpen(value => !value); }, children: [_jsx(IconFolderOpenOutline16, { className: "dsh-file-manage-trigger-icon", size: wide ? 16 : 18 }), wide ? _jsx("span", { className: "dsh-file-manage-trigger-label", children: t('button.label') }) : null] }), open ? (_jsx("div", { style: styles.overlay, role: "presentation", onMouseDown: (event) => {
136
- if (event.target === event.currentTarget)
137
- setOpen(false);
138
- }, children: _jsxs("div", { style: styles.panel, role: "dialog", "aria-modal": "true", "aria-label": t('dialog.title'), children: [_jsxs("div", { className: "dsh-file-manage-panel-header", children: [_jsx("h2", { className: "dsh-file-manage-panel-title", style: { margin: 0 }, children: t('dialog.title') }), _jsx("button", { ref: closeButtonRef, type: "button", className: "dsh-file-manage-close", "aria-label": t('dialog.close'), onClick: () => { setOpen(false); }, children: _jsx(IconCloseOutline16, { size: 14 }) })] }), summary !== null ? (_jsxs("div", { className: "dsh-file-manage-summary", children: [_jsxs("div", { className: "dsh-file-manage-quota-track", children: [_jsx("span", { className: "dsh-file-manage-quota-text", children: `${summary.totalBytesLabel} / ${summary.quotaBytesLabel} · ${t('quota.used', { percent: formatUsagePercent(quotaRatio) })}` }), quotaRatio > 0
139
- ? _jsx("div", { className: "dsh-file-manage-quota-fill", style: { width: `${Math.round(quotaRatio * 100)}%` } })
140
- : null] }), _jsx("p", { className: "dsh-file-manage-count", children: rows.length < summary.count
141
- ? t('summary.loaded', { loaded: rows.length, count: summary.count })
142
- : t('summary.count', { count: summary.count }) })] })) : null, _jsx("div", { style: { ...styles.body, padding: summary !== null ? '0 24px 24px' : '12px 24px 24px' }, className: "dsh-file-manage-body", children: initialLoading ? (_jsxs("div", { className: "dsh-file-manage-loading", role: "status", children: [_jsx("span", { className: "dsh-file-manage-spinner", "aria-hidden": true }), _jsx("span", { children: t('loading') })] })) : (_jsxs(_Fragment, { children: [error !== null ? (_jsxs("p", { role: "alert", style: { ...styles.secondarySmall, margin: '0 0 12px', color: 'var(--dsw-alias-state-error-primary, #c62828)' }, children: [error, ' ', _jsx("button", { type: "button", className: "dsh-file-manage-btn", onClick: () => { reload(); }, children: t('retry') })] })) : null, error === null && rows.length === 0 ? _jsx("p", { style: styles.secondarySmall, children: t('empty') }) : null, rows.length > 0 ? (_jsx("div", { className: "dsh-file-manage-card", children: rows.map(row => (_jsxs("div", { style: styles.row, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }, children: [_jsx("span", { style: styles.title, title: row.filename, children: row.filename }), row.dshOwned ? _jsx("span", { className: "dsh-file-manage-badge", children: t('dshBadge') }) : null] }), _jsxs("div", { style: styles.secondarySmall, children: [row.sizeLabel, " \u00B7 ", row.createdAtLabel, row.expiresAtLabel !== undefined ? ` · ${t('expires', { time: row.expiresAtLabel })}` : ''] })] }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", className: "dsh-file-manage-btn", onClick: () => { void copyId(row); }, children: copied === row.id ? t('copied') : t('copy') }), _jsx("button", { type: "button", className: "dsh-file-manage-btn dsh-file-manage-btn-danger", onClick: () => { setConfirming(row); }, children: t('delete') })] })] }, row.id))) })) : null] })) }), hasMore && !initialLoading ? (_jsx("div", { style: styles.footerBar, children: _jsx("button", { type: "button", className: "dsh-file-manage-btn", disabled: loadingMore, onClick: () => { void loadMore(); }, children: loadingMore ? t('loading') : t('loadMore') }) })) : null] }) })) : null, confirming !== null ? (_jsx("div", { className: "dsh-file-manage-confirm-overlay", role: "presentation", onMouseDown: (event) => {
143
- if (event.target === event.currentTarget && !busyDelete)
144
- setConfirming(null);
145
- }, children: _jsxs("div", { className: "dsh-file-manage-confirm-card", role: "alertdialog", "aria-modal": "true", "aria-label": t('delete'), onKeyDown: (event) => {
146
- if (event.key === 'Escape' && !busyDelete) {
147
- event.stopPropagation();
148
- setConfirming(null);
149
- }
150
- }, children: [_jsx("h3", { className: "dsh-file-manage-confirm-title", children: t('delete') }), _jsx("p", { className: "dsh-file-manage-confirm-desc", children: confirming.dshOwned
151
- ? t('confirm.deleteDsh', { name: confirming.filename })
152
- : t('confirm.delete', { name: confirming.filename }) }), _jsxs("div", { className: "dsh-file-manage-confirm-actions", children: [_jsx("button", { type: "button", className: "dsh-file-manage-btn", disabled: busyDelete, onClick: () => { setConfirming(null); }, children: t('confirm.cancel') }), _jsx("button", { type: "button", className: "dsh-file-manage-btn dsh-file-manage-btn-danger", disabled: busyDelete, onClick: () => { void submitDelete(); }, children: busyDelete ? t('confirm.deleting') : t('confirm.confirm') })] })] }) })) : null] }));
153
- }
154
- //# sourceMappingURL=FileManageDock.js.map
package/lib/client/api.js DELETED
@@ -1,50 +0,0 @@
1
- /** dsh-file-manage 客户端请求封装:host 路由的 fetch 层(超时 + 错误提取)。 @module dsh-file-manage/client/api */
2
- /** 面板请求超时:host 挂起时不让面板永久 loading(根 AGENTS 网络约定)。 */
3
- const REQUEST_TIMEOUT_MS = 15_000;
4
- /** 总数统计超时:host 侧要游标翻到底(配额内最多 10 页),放宽到 60s。 */
5
- const COUNT_TIMEOUT_MS = 60_000;
6
- export async function listApi(after) {
7
- // limit 不传:host 归一化缺省回退 PAGE_SIZE(页大小只活在一处,避免两端漂移)。
8
- const params = new URLSearchParams();
9
- if (after !== undefined)
10
- params.set('after', after);
11
- const response = await fetch(`/api/file-manage/list?${params.toString()}`, {
12
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
13
- });
14
- const payload = await response.json();
15
- if (!response.ok) {
16
- throw new Error(payload.error?.message ?? `请求失败(HTTP ${response.status})`);
17
- }
18
- return {
19
- rows: payload.items ?? [],
20
- hasMore: payload.hasMore ?? false,
21
- ...payload.lastId === undefined ? {} : { lastId: payload.lastId },
22
- };
23
- }
24
- export async function countApi() {
25
- const response = await fetch('/api/file-manage/count', { signal: AbortSignal.timeout(COUNT_TIMEOUT_MS) });
26
- const payload = await response.json();
27
- if (!response.ok) {
28
- throw new Error(payload.error?.message ?? `请求失败(HTTP ${response.status})`);
29
- }
30
- return {
31
- count: payload.count ?? 0,
32
- totalBytes: payload.totalBytes ?? 0,
33
- totalBytesLabel: payload.totalBytesLabel ?? '0 B',
34
- quotaBytes: payload.quotaBytes ?? 0,
35
- quotaBytesLabel: payload.quotaBytesLabel ?? '0 B',
36
- quotaCount: payload.quotaCount ?? 0,
37
- };
38
- }
39
- export async function deleteApi(id) {
40
- const params = new URLSearchParams({ id });
41
- const response = await fetch(`/api/file-manage/files?${params.toString()}`, {
42
- method: 'DELETE',
43
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
44
- });
45
- const payload = await response.json();
46
- if (!response.ok) {
47
- throw new Error(payload.error?.message ?? `请求失败(HTTP ${response.status})`);
48
- }
49
- }
50
- //# sourceMappingURL=api.js.map
@@ -1,81 +0,0 @@
1
- /**
2
- * dsh-file-manage client half:sidebar footer 入口 + 云端文件弹窗。
3
- * 请求封装见 api.ts、样式见 styles.ts、视图见 FileManageDock.tsx;客户端不直接碰任何文件或凭据。
4
- */
5
- import { countApi, deleteApi, listApi } from './api.js';
6
- import { FileManageDock } from './FileManageDock.js';
7
- import { ensureFileManageStyles } from './styles.js';
8
- export const inject = ['slots', 'locale'];
9
- /** 本插件的 locale 字典(zh/en)。 */
10
- const LOCALE_DICTS = {
11
- zh: {
12
- 'button.label': '云端文件',
13
- 'dialog.title': '云端文件(DeepSeek Files API)',
14
- 'dialog.close': '关闭',
15
- 'loading': '加载中…',
16
- 'loadMore': '加载更多',
17
- 'summary.count': '共 {count} 个文件',
18
- 'summary.loaded': '已加载 {loaded} / 共 {count} 个文件',
19
- 'quota.used': '已用 {percent}',
20
- 'empty': '暂无云端文件',
21
- 'dshBadge': 'DSH 自动上传',
22
- 'expires': '到期 {time}',
23
- 'copy': '复制 file_id',
24
- 'copied': '已复制',
25
- 'copyFailed': '复制失败(浏览器可能未授权剪贴板)',
26
- 'delete': '删除',
27
- 'retry': '重试',
28
- 'confirm.confirm': '确认删除',
29
- 'confirm.cancel': '取消',
30
- 'confirm.deleting': '正在删除…',
31
- 'confirm.delete': '删除「{name}」?\n\n删除后,引用它的会话再次使用时官方会自动重新上传(可能稍慢)。',
32
- 'confirm.deleteDsh': '此文件由 DSH 自动上传,可能仍被会话引用。\n\n删除「{name}」?删除后再次引用时官方会自动重新上传。',
33
- },
34
- en: {
35
- 'button.label': 'Cloud Files',
36
- 'dialog.title': 'Cloud Files (DeepSeek Files API)',
37
- 'dialog.close': 'Close',
38
- 'loading': 'Loading…',
39
- 'loadMore': 'Load more',
40
- 'summary.count': '{count} files',
41
- 'summary.loaded': '{loaded} / {count} files',
42
- 'quota.used': '{percent} used',
43
- 'empty': 'No cloud files',
44
- 'dshBadge': 'DSH auto-uploaded',
45
- 'expires': 'expires {time}',
46
- 'copy': 'Copy file_id',
47
- 'copied': 'Copied',
48
- 'copyFailed': 'Copy failed (clipboard may be blocked by the browser)',
49
- 'delete': 'Delete',
50
- 'retry': 'Retry',
51
- 'confirm.confirm': 'Delete',
52
- 'confirm.cancel': 'Cancel',
53
- 'confirm.deleting': 'Deleting…',
54
- 'confirm.delete': 'Delete "{name}"?\n\nSessions that reference it will transparently re-upload on next use (may be slower).',
55
- 'confirm.deleteDsh': 'This file was auto-uploaded by DSH and may still be referenced by sessions.\n\nDelete "{name}"? It will transparently re-upload on next use.',
56
- },
57
- };
58
- /**
59
- * client half 入口:注册 locale 字典 + sidebar footer action。
60
- * @param ctx - 浏览器侧 Cordis 上下文。
61
- */
62
- export function apply(ctx) {
63
- ensureFileManageStyles();
64
- const disposeDictionaries = ctx.locale.register('file-manage', { zh: LOCALE_DICTS.zh, en: LOCALE_DICTS.en });
65
- ctx.effect(() => disposeDictionaries, 'dsh-file-manage: locale dictionaries');
66
- ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
67
- name: 'sidebar.footer.action',
68
- locale: 'file-manage',
69
- id: 'file-manage',
70
- order: 21,
71
- inject: () => ({
72
- listFiles: listApi,
73
- deleteFile: deleteApi,
74
- countFiles: countApi,
75
- }),
76
- }, FileManageDock));
77
- }
78
- export { FileManageDock } from './FileManageDock.js';
79
- export { ensureFileManageStyles } from './styles.js';
80
- export { countApi, deleteApi, listApi } from './api.js';
81
- //# sourceMappingURL=index.js.map
@@ -1,23 +0,0 @@
1
- /** dsh-file-manage 客户端纯逻辑:配额进度条计算(不 import Node 模块)。 @module dsh-file-manage/client/quota */
2
- /** 存储用量比例:钳到 [0, 1];used/quota 非正时返回 0(不除零)。 */
3
- export function storageUsageRatio(usedBytes, quotaBytes) {
4
- if (!Number.isFinite(usedBytes) || usedBytes <= 0)
5
- return 0;
6
- if (!Number.isFinite(quotaBytes) || quotaBytes <= 0)
7
- return 0;
8
- return Math.min(1, Math.max(0, usedBytes / quotaBytes));
9
- }
10
- /** 用量百分比文案(网盘风格自适应精度):≥10% 取整、≥1% 一位、≥0.1% 两位、≥0.01% 三位、更小四位——25GiB 配额下常见 0.01% 量级,避免人人 0.0%。 */
11
- export function formatUsagePercent(ratio) {
12
- const percent = Math.min(1, Math.max(0, ratio)) * 100;
13
- if (percent >= 10)
14
- return `${Math.round(percent)}%`;
15
- if (percent >= 1)
16
- return `${percent.toFixed(1)}%`;
17
- if (percent >= 0.1)
18
- return `${percent.toFixed(2)}%`;
19
- if (percent >= 0.01)
20
- return `${percent.toFixed(3)}%`;
21
- return `${percent.toFixed(4)}%`;
22
- }
23
- //# sourceMappingURL=quota.js.map
@@ -1,326 +0,0 @@
1
- /** dsh-file-manage 客户端样式层:注入样式表 + 面板内联样式(官方设计 token;按 data 属性去重)。 @module dsh-file-manage/client/styles */
2
- /** 注入触发键 / 面板 / 确认框样式;HMR / 重载按 data 属性去重不叠加。 */
3
- export function ensureFileManageStyles() {
4
- if (document.querySelector('style[data-dsh-file-manage]') !== null)
5
- return;
6
- const style = document.createElement('style');
7
- style.dataset.dshFileManage = '';
8
- style.textContent = `/* 官方 .footerActions 是横向 flex 行,slot 包裹层为行内 display:contents:
9
- * 多插件各自的全宽按钮会并排挤到右缘外(只剩一条边)。这里把包裹层改回真实盒子纵排,
10
- * Archive / 云端文件两个按钮上下堆叠、各自占满一行(!important 压过行内 contents)。 */
11
- [data-slot='sidebar.footer.action'] {
12
- display: flex !important;
13
- flex-direction: column;
14
- /* 包裹层是 .footerActions 行容器里的 flex item,需显式撑满,否则两个全宽按钮按内容宽度收缩。 */
15
- width: 100%;
16
- }
17
- .dsh-file-manage-trigger {
18
- flex: none;
19
- display: flex;
20
- align-items: center;
21
- gap: 8px;
22
- width: calc(100% + 4px);
23
- height: 42px;
24
- margin: 4px -2px;
25
- padding: 0 10px 0 8px;
26
- box-sizing: border-box;
27
- border: none;
28
- border-radius: 12px;
29
- background: transparent;
30
- cursor: pointer;
31
- overflow: hidden;
32
- color: var(--dsw-alias-label-primary);
33
- font-family: inherit;
34
- font-size: 14px;
35
- line-height: 22px;
36
- }
37
- .dsh-file-manage-trigger:hover {
38
- background: var(--dsw-alias-interactive-bg-hover);
39
- }
40
- .dsh-file-manage-trigger-rail {
41
- width: 36px;
42
- height: 36px;
43
- margin: 8px 0 10px;
44
- justify-content: center;
45
- gap: 0;
46
- padding: 0;
47
- border-radius: 50%;
48
- corner-shape: round;
49
- }
50
- .dsh-file-manage-trigger-icon {
51
- flex: none;
52
- }
53
- .dsh-file-manage-trigger-label {
54
- overflow: hidden;
55
- text-overflow: ellipsis;
56
- white-space: nowrap;
57
- }
58
- /* 整页 loading:首屏数据(列表 + 总数)落定前占满内容区,避免「打开后加载闪动」(2026-09-01)。 */
59
- .dsh-file-manage-loading {
60
- display: flex;
61
- align-items: center;
62
- justify-content: center;
63
- gap: 10px;
64
- min-height: 240px;
65
- color: var(--dsw-alias-label-secondary, #6b7280);
66
- font-size: 14px;
67
- line-height: 22px;
68
- }
69
- .dsh-file-manage-spinner {
70
- width: 16px;
71
- height: 16px;
72
- border: 2px solid var(--dsw-alias-border-l1, #d4d8e0);
73
- border-top-color: var(--dsw-alias-button-info-fill, #4d6bfe);
74
- border-radius: 50%;
75
- corner-shape: round;
76
- animation: dsh-file-manage-spin 0.8s linear infinite;
77
- }
78
- @keyframes dsh-file-manage-spin {
79
- to { transform: rotate(360deg); }
80
- }
81
- @media (prefers-reduced-motion: reduce) {
82
- .dsh-file-manage-spinner { animation: none; }
83
- }
84
- /* 面板滚动区:官方 settings 同款——elevated surface 重绑 l2 滚动条 token(base 默认 l1,浮层上对比度不对)。 */
85
- .dsh-file-manage-body {
86
- --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
87
- --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
88
- }
89
- /* 面板头:官方 settings 面板同款(54px 高、标题起点 24px)。 */
90
- .dsh-file-manage-panel-header {
91
- flex: none;
92
- display: flex;
93
- align-items: flex-start;
94
- justify-content: space-between;
95
- gap: 8px;
96
- height: 54px;
97
- padding: 20px 14px 8px 24px;
98
- box-sizing: border-box;
99
- }
100
- .dsh-file-manage-panel-title {
101
- font-size: 16px;
102
- font-weight: 500;
103
- line-height: 24px;
104
- color: var(--dsw-alias-label-primary);
105
- }
106
- .dsh-file-manage-close {
107
- display: inline-flex;
108
- align-items: center;
109
- justify-content: center;
110
- width: 28px;
111
- height: 28px;
112
- padding: 0;
113
- border: none;
114
- border-radius: 8px;
115
- outline: none;
116
- background: transparent;
117
- cursor: pointer;
118
- color: var(--dsw-alias-label-primary);
119
- }
120
- .dsh-file-manage-close:hover {
121
- background: var(--dsw-alias-interactive-bg-hover);
122
- }
123
- .dsh-file-manage-badge {
124
- flex: none;
125
- padding: 1px 6px;
126
- border: 1px solid var(--dsw-alias-border-l2, #e2e5ea);
127
- border-radius: 999px;
128
- corner-shape: round;
129
- color: var(--dsw-alias-label-caption, #8a919f);
130
- font-size: 11px;
131
- line-height: 16px;
132
- }
133
- .dsh-file-manage-btn {
134
- /* 官方 Button.sm 同款几何:h28 + r14 胶囊(超椭圆角随官方全局规则自动生效)。 */
135
- height: 28px;
136
- padding: 0 10px;
137
- border: 0.5px solid var(--dsw-alias-border-l3, #d4d8e0);
138
- border-radius: 14px;
139
- outline: none;
140
- background: transparent;
141
- color: var(--dsw-alias-label-primary, #1f2329);
142
- font-size: 12px;
143
- line-height: 18px;
144
- white-space: nowrap;
145
- cursor: pointer;
146
- }
147
- .dsh-file-manage-btn:hover:not(:disabled) {
148
- background: var(--dsw-alias-interactive-bg-hover);
149
- }
150
- .dsh-file-manage-btn:disabled {
151
- opacity: 0.5;
152
- cursor: default;
153
- }
154
- .dsh-file-manage-btn-danger {
155
- color: var(--dsw-alias-state-error-primary, #c62828);
156
- }
157
- /* 删除确认框:官方 web 确认框同款(mask + 毛玻璃 + 480 卡片)。 */
158
- .dsh-file-manage-confirm-overlay {
159
- position: fixed;
160
- inset: 0;
161
- z-index: 1100;
162
- display: flex;
163
- align-items: center;
164
- justify-content: center;
165
- background: var(--dsw-alias-bg-mask-1, rgba(0, 0, 0, 0.28));
166
- backdrop-filter: var(--dsw-mask-blur);
167
- }
168
- .dsh-file-manage-confirm-card {
169
- display: flex;
170
- flex-direction: column;
171
- gap: 12px;
172
- width: min(480px, calc(100vw - 48px));
173
- padding: 20px;
174
- border-radius: 16px;
175
- background: var(--dsw-alias-bg-layer-2, #f6f7f9);
176
- color: var(--dsw-alias-label-primary, #1f2329);
177
- box-shadow: var(--dsw-elevation-prominent, 0 12px 40px rgba(0,0,0,0.22));
178
- }
179
- .dsh-file-manage-confirm-title {
180
- margin: 0;
181
- font-size: 16px;
182
- font-weight: 500;
183
- line-height: 24px;
184
- }
185
- .dsh-file-manage-confirm-desc {
186
- margin: 0;
187
- font-size: 14px;
188
- line-height: 22px;
189
- color: var(--dsw-alias-label-secondary, #6b7280);
190
- white-space: pre-line;
191
- }
192
- .dsh-file-manage-confirm-actions {
193
- display: flex;
194
- justify-content: flex-end;
195
- gap: 8px;
196
- }
197
- /* 用量/进度条固定区:面板头下方、不随列表滚动;左侧 24px 与区块卡线框齐平,
198
- 右侧额外让出滚动条宽度(列表区 scrollbar-gutter: stable 固定占位 8px)。 */
199
- .dsh-file-manage-summary {
200
- flex: none;
201
- padding: 12px calc(24px + var(--dsh-scrollbar-width, 8px)) 8px 24px;
202
- }
203
- .dsh-file-manage-count {
204
- margin: 6px 0 0;
205
- font-size: 12px;
206
- line-height: 18px;
207
- color: var(--dsw-alias-label-secondary, #6b7280);
208
- }
209
- /* 列表区块卡:存档页归档区/备份区同款 token(border-l2 + r12)。 */
210
- .dsh-file-manage-card {
211
- box-sizing: border-box;
212
- display: flex;
213
- flex-direction: column;
214
- margin: 0 0 12px;
215
- padding: 8px 12px 12px;
216
- border: 0.5px solid var(--dsw-alias-border-l4, #e2e5ea);
217
- border-radius: 16px;
218
- }
219
- /* 配额容量条:加厚 + 未使用区斜纹(repeating-linear-gradient),与分割线区分;填充盖住斜纹。
220
- 容量文字绝对定位居中叠加在条上。 */
221
- .dsh-file-manage-quota-track {
222
- position: relative;
223
- height: 16px;
224
- border-radius: 8px;
225
- background-color: var(--dsw-alias-interactive-bg-hover);
226
- background-image: repeating-linear-gradient(
227
- 45deg,
228
- transparent 0px,
229
- transparent 5px,
230
- var(--dsw-alias-border-l1, #d4d8e0) 5px,
231
- var(--dsw-alias-border-l1, #d4d8e0) 7px
232
- );
233
- overflow: hidden;
234
- }
235
- .dsh-file-manage-quota-text {
236
- position: absolute;
237
- inset: 0;
238
- z-index: 1;
239
- display: flex;
240
- align-items: center;
241
- justify-content: center;
242
- font-size: 11px;
243
- line-height: 16px;
244
- white-space: nowrap;
245
- color: var(--dsw-alias-label-primary, #1f2329);
246
- /* 高用量时蓝填充垫底,加一圈底色光晕保证可读。 */
247
- text-shadow: 0 0 4px var(--dsw-alias-bg-layer-2, #f6f7f9);
248
- pointer-events: none;
249
- }
250
- .dsh-file-manage-quota-fill {
251
- height: 100%;
252
- /* 用量极小时(0.01% 量级)宽度趋近 0,兜底 4px 银条让「有使用」可见(网盘同款)。
253
- 不加自身圆角:最小宽度下圆角会长成圆点、视觉鼓出轨道左端,改由轨道 overflow:hidden + 圆角裁切两端。 */
254
- min-width: 4px;
255
- background: var(--dsw-alias-state-business-primary);
256
- transition: width 220ms ease-out;
257
- }
258
- `;
259
- document.head.appendChild(style);
260
- }
261
- /** 面板与行内联样式(对齐官方 Settings / Archive 的面板几何)。 */
262
- export const styles = {
263
- overlay: {
264
- position: 'fixed',
265
- inset: 0,
266
- zIndex: 1000,
267
- display: 'flex',
268
- alignItems: 'center',
269
- justifyContent: 'center',
270
- background: 'var(--dsw-alias-bg-mask-1, rgba(0, 0, 0, 0.28))',
271
- backdropFilter: 'var(--dsw-mask-blur)',
272
- },
273
- panel: {
274
- position: 'relative',
275
- display: 'flex',
276
- flexDirection: 'column',
277
- width: 'min(800px, calc(100vw - 48px))',
278
- // 高度随内容自适应、上限钳到视口(官方 settings 同款上限)。
279
- maxHeight: 'min(800px, calc(100vh - 48px))',
280
- borderRadius: 24,
281
- overflow: 'hidden',
282
- padding: 0,
283
- background: 'var(--dsw-alias-bg-layer-2, #f6f7f9)',
284
- color: 'var(--dsw-alias-label-primary, #1f2329)',
285
- boxShadow: 'var(--dsw-elevation-prominent, 0 12px 40px rgba(0,0,0,0.22))',
286
- },
287
- body: {
288
- flex: 1,
289
- minHeight: 0,
290
- overflow: 'auto',
291
- // 滚动条固定占位:卡片右缘不因滚动条出现/消失而左右漂移(与顶部进度条对齐)。
292
- scrollbarGutter: 'stable',
293
- padding: '0 24px 24px',
294
- },
295
- row: {
296
- display: 'flex',
297
- justifyContent: 'space-between',
298
- gap: 8,
299
- alignItems: 'center',
300
- padding: '8px 0',
301
- borderBottom: '1px solid var(--dsw-alias-border-l1, #e2e5ea)',
302
- },
303
- actions: {
304
- display: 'flex',
305
- gap: 6,
306
- alignItems: 'center',
307
- },
308
- title: {
309
- overflow: 'hidden',
310
- textOverflow: 'ellipsis',
311
- whiteSpace: 'nowrap',
312
- },
313
- secondarySmall: {
314
- color: 'var(--dsw-alias-label-secondary, #6b7280)',
315
- fontSize: 12,
316
- lineHeight: '18px',
317
- },
318
- footerBar: {
319
- flex: 'none',
320
- display: 'flex',
321
- justifyContent: 'center',
322
- // 列表已包区块卡,不再加顶部分隔线(避免与卡片叠加显乱)。
323
- padding: '10px 24px 14px',
324
- },
325
- };
326
- //# sourceMappingURL=styles.js.map