@dsh-sparrow/dsh-file-manage 0.1.0-alpha.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 +10 -0
- package/CHANGELOG.zh-CN.md +10 -0
- package/README.md +42 -0
- package/README.zh-CN.md +42 -0
- package/cordis.patch.yml +5 -0
- package/docs/images/panel.png +0 -0
- package/lib/client/FileManageDock.js +141 -0
- package/lib/client/api.js +50 -0
- package/lib/client/index.js +81 -0
- package/lib/client/quota.js +23 -0
- package/lib/client/styles.js +297 -0
- package/lib/client.js +705 -0
- package/lib/files.js +82 -0
- package/lib/host.js +157 -0
- package/lib/index.js +3 -0
- package/lib/types/client/FileManageDock.d.ts +20 -0
- package/lib/types/client/api.d.ts +19 -0
- package/lib/types/client/index.d.ts +17 -0
- package/lib/types/client/quota.d.ts +6 -0
- package/lib/types/client/styles.d.ts +66 -0
- package/lib/types/files.d.ts +54 -0
- package/lib/types/host.d.ts +10 -0
- package/lib/types/index.d.ts +3 -0
- package/package.json +75 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
English | [简体中文](CHANGELOG.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
## 0.1.0-alpha.1 (2026-09-01 · pre-release)
|
|
6
|
+
|
|
7
|
+
- First pre-release, published to the `next` channel for validation; features match the planned `0.1.0` first release
|
|
8
|
+
- Sidebar "Cloud Files" entry + panel (styled like the official Settings / Archive): list pages of 20, Load more cursor pagination, per-file delete (stronger notice for `dsh-` auto-uploaded files), one-click copy file_id
|
|
9
|
+
- Total count + drive-style quota bar: used / 25 GiB, adaptive-precision percentage, striped empty area, "Loaded X / N" synced with pagination
|
|
10
|
+
- Reuses the official DeepSeekFilesClient (connection facts from the llm-deepseek settings section + ctx.credentials); no local persistence
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# 更新日志
|
|
2
|
+
|
|
3
|
+
简体中文 | [English](CHANGELOG.md)
|
|
4
|
+
|
|
5
|
+
## 0.1.0-alpha.1 (2026-09-01 · pre-release)
|
|
6
|
+
|
|
7
|
+
- 首个预发布版本,先发 `next` 渠道验证;功能对齐计划中的 `0.1.0` 首版
|
|
8
|
+
- 侧边栏「云端文件」入口 + 面板(样式对齐官方设置 / 归档面板):每页 20 条列表、Load more 游标翻页、单条删除(`dsh-` 自动上传文件给更强提示)、一键复制 file_id
|
|
9
|
+
- 总数 + 网盘式配额条:已用 / 25 GiB、自适应精度百分比、条纹空白区、「已加载 X / N」随翻页同步
|
|
10
|
+
- 复用官方 DeepSeekFilesClient(连接事实取自 llm-deepseek 设置节 + ctx.credentials);无本地持久化
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# dsh-file-manage
|
|
2
|
+
|
|
3
|
+
English | [简体中文](README.zh-CN.md) | [GitHub](https://github.com/peiyucn/dsh-sparrow)
|
|
4
|
+
|
|
5
|
+
DeepSeek Files API cloud file management — a DeepSeek Harness (DSH) web plugin (dsh-sparrow collection member).
|
|
6
|
+
|
|
7
|
+
Large images pasted into DSH are auto-uploaded to the DeepSeek Files API, but there is no official management UI. This plugin adds a "Cloud Files" entry to the sidebar and lists every cloud file under your API key: cursor pagination, upload/expiry times, sizes, single-file deletion, and one-click file_id copy. It reuses the official `DeepSeekFilesClient` — no extra credentials.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
dsh plugin --profile web add @dsh-sparrow/dsh-file-manage
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires dsh ≥ 0.1.2-alpha.3 (verified baseline; earlier versions unverified) and a working `pnpm` (`dsh plugin` forwards installation to pnpm).
|
|
16
|
+
|
|
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
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
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)
|
|
23
|
+
* **Quota**: the panel header shows the total count and a netdisk-style quota bar (used storage / the official 25 GiB limit)
|
|
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
|
+
* **Copy file_id**: one-click copy per row
|
|
26
|
+
* **Errors**: classified messages for auth failures / rate limits / server errors, with retry
|
|
27
|
+
|
|
28
|
+
## Limitations
|
|
29
|
+
|
|
30
|
+
* The official Files API has **no batch-delete endpoint** — no "delete all", only per-file deletion
|
|
31
|
+
* The official API has **no download endpoint** — content cannot be previewed or downloaded
|
|
32
|
+
* Quota: at most 10000 files / 25 GiB per key (official limits)
|
|
33
|
+
* **Where files come from**: DSH auto-uploads to the Files API only on the **DeepSeek model path** (the upload lives in the llm-deepseek adapter layer); with other models the list mostly shows files you or external tools uploaded
|
|
34
|
+
* **Expiry**: DSH auto-uploads files with a default **7-day** expiry (the official API has no permanent option); expired files are transparently re-uploaded on next use — tune `fileExpiresAfterSeconds` (1h–30d) in the `llm-deepseek` settings section
|
|
35
|
+
|
|
36
|
+
## Screenshot
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
## Uninstall & leftovers
|
|
41
|
+
|
|
42
|
+
* The plugin keeps no local state; uninstalling leaves the cloud files untouched and DSH behavior unchanged
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# dsh-file-manage
|
|
2
|
+
|
|
3
|
+
简体中文 | [English](README.md) | [GitHub](https://github.com/peiyucn/dsh-sparrow)
|
|
4
|
+
|
|
5
|
+
DeepSeek Files API 云端文件管理 —— DeepSeek Harness(DSH)Web 插件(dsh-sparrow 合集成员)。
|
|
6
|
+
|
|
7
|
+
DSH 贴图的大图会自动上传到 DeepSeek Files API(云端文件库),但官方没有管理界面。本插件在侧边栏加一个「云端文件」入口,列出你 API key 下的全部云端文件:游标翻页、上传 / 到期时间、大小,支持删除单条与一键复制 file_id。完全复用官方 `DeepSeekFilesClient`,不新增任何凭据。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
dsh plugin --profile web add @dsh-sparrow/dsh-file-manage
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
适配 dsh ≥ 0.1.2-alpha.3(本机验证基线;更早版本未验证),并需要可用的 `pnpm`(`dsh plugin` 会把安装操作转发给 pnpm)。
|
|
16
|
+
|
|
17
|
+
> **不要**直接执行 `npm install @dsh-sparrow/dsh-file-manage`:那只会把包下载到某个 `node_modules`,不会注册进 DSH 的 web profile。请使用上面的 `dsh plugin` 命令安装,并在安装后重启 DSH。
|
|
18
|
+
|
|
19
|
+
## 使用
|
|
20
|
+
|
|
21
|
+
* **入口**:侧边栏底部「云端文件」按钮
|
|
22
|
+
* **列表**:每页 20 条,底部「加载更多」翻页(官方 after 游标,最新在前);行内展示文件名 / 大小 / 上传时间 / 到期时间(有则显)
|
|
23
|
+
* **配额**:列表顶部显示总数与网盘式配额进度条(已用存储 / 25 GiB 官方上限)
|
|
24
|
+
* **删除**:行内删除按钮二次确认;「DSH 自动上传」角标(`dsh-` 前缀)的文件有额外提示——删除后旧会话再次引用时官方会自动重新上传(可能稍慢)
|
|
25
|
+
* **复制 file_id**:行内按钮一键复制
|
|
26
|
+
* **错误提示**:鉴权失败 / 限流 / 服务端错误分类展示,可重试
|
|
27
|
+
|
|
28
|
+
## 限制
|
|
29
|
+
|
|
30
|
+
* 官方 Files API **没有批量删除端点**——不做「全部清理」,只能逐条删
|
|
31
|
+
* 官方**没有下载端点**——不能预览 / 下载文件内容
|
|
32
|
+
* 配额:单 key 最多 10000 个文件 / 25 GiB(官方限制)
|
|
33
|
+
* **文件来源**:云端文件只在 **DeepSeek 系列模型**路径下由 DSH 自动上传(上传发生在 llm-deepseek 适配器层);使用其它模型时列表主要为你手动 / 外部工具上传的文件
|
|
34
|
+
* **有效期**:DSH 自动上传的文件默认 **7 天**有效期(官方无「永久」选项);到期后再次引用会自动重新上传,可在 `llm-deepseek` 设置节的 `fileExpiresAfterSeconds`(1 小时–30 天)调整
|
|
35
|
+
|
|
36
|
+
## 截图
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
## 卸载与残留
|
|
41
|
+
|
|
42
|
+
* 插件无任何本地持久化状态;卸载后云端文件原样保留,DSH 行为不受影响
|
package/cordis.patch.yml
ADDED
|
Binary file
|
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
const closeButtonRef = useRef(null);
|
|
21
|
+
/** 刷新代际:快速关/开面板产生并发请求时,只让最新一次的结果落地(陈旧响应竞态,archive 同款)。 */
|
|
22
|
+
const refreshSeqRef = useRef(0);
|
|
23
|
+
/** 「已复制」反馈 2s 复位(archive 同款)。 */
|
|
24
|
+
const copiedTimerRef = useRef(null);
|
|
25
|
+
const loadFirst = useCallback(() => {
|
|
26
|
+
const seq = ++refreshSeqRef.current;
|
|
27
|
+
setLoading(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
listFiles()
|
|
30
|
+
.then(page => {
|
|
31
|
+
if (seq !== refreshSeqRef.current)
|
|
32
|
+
return;
|
|
33
|
+
setRows(page.rows);
|
|
34
|
+
setHasMore(page.hasMore);
|
|
35
|
+
setLastId(page.lastId);
|
|
36
|
+
})
|
|
37
|
+
.catch(reason => {
|
|
38
|
+
if (seq !== refreshSeqRef.current)
|
|
39
|
+
return;
|
|
40
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
41
|
+
})
|
|
42
|
+
.finally(() => {
|
|
43
|
+
if (seq === refreshSeqRef.current)
|
|
44
|
+
setLoading(false);
|
|
45
|
+
});
|
|
46
|
+
}, [listFiles]);
|
|
47
|
+
// 官方弹窗行为:打开时聚焦关闭按钮,Esc 关闭。
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!open)
|
|
50
|
+
return;
|
|
51
|
+
closeButtonRef.current?.focus();
|
|
52
|
+
const onKeyDown = (event) => {
|
|
53
|
+
if (event.key === 'Escape')
|
|
54
|
+
setOpen(false);
|
|
55
|
+
};
|
|
56
|
+
document.addEventListener('keydown', onKeyDown);
|
|
57
|
+
return () => { document.removeEventListener('keydown', onKeyDown); };
|
|
58
|
+
}, [open]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!open)
|
|
61
|
+
return;
|
|
62
|
+
setConfirming(null);
|
|
63
|
+
setSummary(null);
|
|
64
|
+
loadFirst();
|
|
65
|
+
// 总数统计为 best-effort:接口失败只隐藏统计行,不影响列表;同样受刷新代际约束。
|
|
66
|
+
const seq = refreshSeqRef.current;
|
|
67
|
+
void countFiles()
|
|
68
|
+
.then(next => {
|
|
69
|
+
if (seq === refreshSeqRef.current)
|
|
70
|
+
setSummary(next);
|
|
71
|
+
})
|
|
72
|
+
.catch(() => { });
|
|
73
|
+
}, [open, loadFirst, countFiles]);
|
|
74
|
+
useEffect(() => () => {
|
|
75
|
+
if (copiedTimerRef.current !== null)
|
|
76
|
+
window.clearTimeout(copiedTimerRef.current);
|
|
77
|
+
}, []);
|
|
78
|
+
const loadMore = useCallback(() => {
|
|
79
|
+
if (lastId === undefined || loadingMore)
|
|
80
|
+
return;
|
|
81
|
+
setLoadingMore(true);
|
|
82
|
+
listFiles(lastId)
|
|
83
|
+
.then(page => {
|
|
84
|
+
setRows(current => [...current, ...page.rows]);
|
|
85
|
+
setHasMore(page.hasMore);
|
|
86
|
+
setLastId(page.lastId);
|
|
87
|
+
})
|
|
88
|
+
.catch(reason => { setError(reason instanceof Error ? reason.message : String(reason)); })
|
|
89
|
+
.finally(() => { setLoadingMore(false); });
|
|
90
|
+
}, [lastId, loadingMore, listFiles]);
|
|
91
|
+
const copyId = useCallback(async (row) => {
|
|
92
|
+
try {
|
|
93
|
+
await navigator.clipboard.writeText(row.id);
|
|
94
|
+
setCopied(row.id);
|
|
95
|
+
if (copiedTimerRef.current !== null)
|
|
96
|
+
window.clearTimeout(copiedTimerRef.current);
|
|
97
|
+
copiedTimerRef.current = window.setTimeout(() => { setCopied(null); }, 2_000);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
setCopied(null);
|
|
101
|
+
setError(t('copyFailed'));
|
|
102
|
+
}
|
|
103
|
+
}, [t]);
|
|
104
|
+
const quotaRatio = summary === null ? 0 : storageUsageRatio(summary.totalBytes, summary.quotaBytes);
|
|
105
|
+
const submitDelete = useCallback(() => {
|
|
106
|
+
if (confirming === null || busyDelete)
|
|
107
|
+
return;
|
|
108
|
+
setBusyDelete(true);
|
|
109
|
+
deleteFile(confirming.id)
|
|
110
|
+
.then(() => {
|
|
111
|
+
setRows(current => current.filter(row => row.id !== confirming.id));
|
|
112
|
+
setConfirming(null);
|
|
113
|
+
// 删除后刷新总数(best-effort)。
|
|
114
|
+
void countFiles().then(setSummary).catch(() => { });
|
|
115
|
+
})
|
|
116
|
+
.catch(reason => {
|
|
117
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
118
|
+
setConfirming(null);
|
|
119
|
+
})
|
|
120
|
+
.finally(() => { setBusyDelete(false); });
|
|
121
|
+
}, [busyDelete, confirming, deleteFile, countFiles]);
|
|
122
|
+
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) => {
|
|
123
|
+
if (event.target === event.currentTarget)
|
|
124
|
+
setOpen(false);
|
|
125
|
+
}, 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
|
|
126
|
+
? _jsx("div", { className: "dsh-file-manage-quota-fill", style: { width: `${Math.round(quotaRatio * 100)}%` } })
|
|
127
|
+
: null] }), _jsx("p", { className: "dsh-file-manage-count", children: rows.length < summary.count
|
|
128
|
+
? t('summary.loaded', { loaded: rows.length, count: summary.count })
|
|
129
|
+
: t('summary.count', { count: summary.count }) })] })) : null, _jsxs("div", { style: { ...styles.body, padding: summary !== null ? '0 24px 24px' : '12px 24px 24px' }, className: "dsh-file-manage-body", 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: loadFirst, children: t('retry') })] })) : null, loading ? _jsx("p", { style: styles.secondarySmall, children: t('loading') }) : null, !loading && 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 && !loading ? (_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) => {
|
|
130
|
+
if (event.target === event.currentTarget && !busyDelete)
|
|
131
|
+
setConfirming(null);
|
|
132
|
+
}, children: _jsxs("div", { className: "dsh-file-manage-confirm-card", role: "alertdialog", "aria-modal": "true", "aria-label": t('delete'), onKeyDown: (event) => {
|
|
133
|
+
if (event.key === 'Escape' && !busyDelete) {
|
|
134
|
+
event.stopPropagation();
|
|
135
|
+
setConfirming(null);
|
|
136
|
+
}
|
|
137
|
+
}, children: [_jsx("h3", { className: "dsh-file-manage-confirm-title", children: t('delete') }), _jsx("p", { className: "dsh-file-manage-confirm-desc", children: confirming.dshOwned
|
|
138
|
+
? t('confirm.deleteDsh', { name: confirming.filename })
|
|
139
|
+
: 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] }));
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=FileManageDock.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|