dsh-zspace 0.1.0 → 0.1.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 +21 -0
- package/README.md +15 -1
- package/lib/README.md +74 -0
- package/lib/client/browse.js +65 -0
- package/lib/client/download.js +130 -0
- package/lib/client/errors.js +56 -0
- package/lib/client/mutate.js +103 -0
- package/lib/client/session.js +79 -0
- package/lib/client/spaces.js +152 -0
- package/lib/client/transport.js +335 -0
- package/lib/client/upload.js +129 -0
- package/lib/client.js +77 -737
- package/lib/config.js +105 -0
- package/lib/index.js +7 -126
- package/lib/prompt.js +26 -0
- package/lib/tools/copy.js +37 -0
- package/lib/tools/download.js +44 -0
- package/lib/tools/find.js +76 -0
- package/lib/tools/ls.js +72 -0
- package/lib/tools/mkdir.js +34 -0
- package/lib/tools/move.js +37 -0
- package/lib/tools/paths.js +84 -0
- package/lib/tools/read.js +55 -0
- package/lib/tools/remove.js +40 -0
- package/lib/tools/rename.js +39 -0
- package/lib/tools/shared.js +52 -0
- package/lib/tools/stat.js +53 -0
- package/lib/tools/status.js +116 -0
- package/lib/tools/upload.js +58 -0
- package/lib/tools/walk.js +117 -0
- package/lib/tools.js +49 -788
- package/package.json +3 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 变更日志
|
|
2
|
+
|
|
3
|
+
## 0.1.1 — 2026-10-06
|
|
4
|
+
|
|
5
|
+
纯内部重构,**对外行为与 API 不变**(`ZSpaceClient` / `ZSpaceError` / `createToolSpecs` / 12 个工具名与参数完全保持):
|
|
6
|
+
|
|
7
|
+
- `lib/client.js` 857 → 197 行,按能力拆到 `lib/client/`:`errors` / `transport` / `session` / `spaces` / `browse` / `mutate` / `upload` / `download`;
|
|
8
|
+
- `lib/tools.js` 805 → 66 行:12 个工具各自成文件(`lib/tools/<工具名>.js`),公共能力抽成 `shared` / `paths` / `walk`;
|
|
9
|
+
- `lib/index.js` 206 → 87 行,配置抽到 `lib/config.js`、系统提示文案抽到 `lib/prompt.js`;
|
|
10
|
+
- 新增 `lib/README.md`:目录职责表 + 依赖方向 + 「加一个工具/加一个客户端能力」的步骤;
|
|
11
|
+
- 测试仍为 28 项 mock 用例,拆分前后全绿;真机自检脚本未改。
|
|
12
|
+
|
|
13
|
+
## 0.1.0 — 2026-10-05
|
|
14
|
+
|
|
15
|
+
首个版本:
|
|
16
|
+
|
|
17
|
+
- 跨网络通道:桌面客户端本地代理 `127.0.0.1:13579` → 极空间云中转 → NAS,无需同一局域网 / WebDAV / SSH / Python;
|
|
18
|
+
- 12 个工具:`zspace_status` `ls` `stat` `find` `read` `download` `upload` `mkdir` `rename` `move` `copy` `remove`;
|
|
19
|
+
- 路径约定:`home:` / `public:` / 相对个人空间 / NAS 绝对路径,空间根按存储池自动探测;
|
|
20
|
+
- 上传:≤8 MB 走 `/v2/file/create`,更大或遇 HTTP 413 自动回退桌面客户端分片协议(2 MB/片),中文文件名可用;
|
|
21
|
+
- 安全:凭据实时读桌面端 `vuex.json`(不落库);`zspace_remove` 强制 `confirm: true` 且 NAS 侧进回收站;错误码映射为可执行提示。
|
package/README.md
CHANGED
|
@@ -91,10 +91,24 @@ dsh plugin --profile desktop add /绝对路径/dsh-zspace-0.1.0.tgz
|
|
|
91
91
|
- NAS 网页层的 `/file_search/file_search` 在当前固件上**忽略关键字**(换任何关键词都返回同一批 100 项),所以 `zspace_find` 用的是**受限目录遍历**而不是该接口,结果会明确报告扫描范围;
|
|
92
92
|
- 单次 list 最多 50 条(插件已自动分页);远端模式分片上限 2 MB/片。
|
|
93
93
|
|
|
94
|
+
## 代码结构
|
|
95
|
+
|
|
96
|
+
按「**一个文件 = 一个能独立解释的能力**」组织,完整职责表与扩展步骤见 [lib/README.md](lib/README.md):
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
lib/index.js 插件接线(name/inject 转发 Config + apply) lib/client/ 协议按能力拆分
|
|
100
|
+
lib/config.js 配置 schema、默认值、边界校验 transport URL/编码/重试/校验
|
|
101
|
+
lib/prompt.js 系统提示里的用法说明 spaces 探活、存储池、空间根
|
|
102
|
+
lib/tools.js 工具注册表(只做注册) browse/mutate 列表 / 增删改
|
|
103
|
+
lib/tools/*.js 12 个工具,各一个文件 upload/download 分片上传 / 流式下载
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
依赖是严格单向无环:`index → tools → client → {errors,transport} → format/auth`。加一个工具 = 新建 `lib/tools/<名字>.js` + 在 `lib/tools.js` 注册一行。
|
|
107
|
+
|
|
94
108
|
## 测试
|
|
95
109
|
|
|
96
110
|
```sh
|
|
97
|
-
node --test test/client.test.js test/tools.test.js test/adaptor.test.js #
|
|
111
|
+
node --test test/client.test.js test/tools.test.js test/adaptor.test.js # 28 项单测/集成(mock 代理)
|
|
98
112
|
node scripts/live-selftest.js # 真机端到端自检(会创建并清理临时目录)
|
|
99
113
|
```
|
|
100
114
|
|
package/lib/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# lib/ 结构与约定
|
|
2
|
+
|
|
3
|
+
一句话:**一个文件 = 一个能独立解释的能力**。入口只做接线,协议只管传输,工具一个文件一个。
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
lib/
|
|
7
|
+
├── index.js 87 行 插件入口:name / inject / Config 转发 / apply(生命周期 + 注册)
|
|
8
|
+
├── config.js 105 行 配置:schemastery 的 Config、代码侧 DEFAULTS、边界校验
|
|
9
|
+
├── prompt.js 26 行 系统提示里的用法说明(promptEnabled 开关)
|
|
10
|
+
├── auth.js 174 行 从桌面客户端 vuex.json 取登录态(按 mtime 缓存)
|
|
11
|
+
├── format.js 91 行 时间/大小/路径小工具(无 host 依赖,纯 Node)
|
|
12
|
+
├── client.js 197 行 客户端 facade:持有会话状态,方法薄委托到 client/*
|
|
13
|
+
├── client/
|
|
14
|
+
│ ├── errors.js 56 行 NAS 业务码 + ZSpaceError(含 hint 提示)
|
|
15
|
+
│ ├── transport.js 335 行 URL 形状、表单编码、重试、响应校验、流读取
|
|
16
|
+
│ ├── session.js 79 行 Cookie、公共参数、安全身份
|
|
17
|
+
│ ├── spaces.js 153 行 代理探活、存储池、个人/公共空间根探测
|
|
18
|
+
│ ├── browse.js 64 行 分页列目录、单条元信息
|
|
19
|
+
│ ├── mutate.js 102 行 mkdir / rename / move / copy / remove
|
|
20
|
+
│ ├── upload.js 129 行 单请求上传 + 分片协议
|
|
21
|
+
│ └── download.js 128 行 流式下载、限量读入内存
|
|
22
|
+
├── tools.js 66 行 工具注册表:建 ctx、按顺序调用 12 个工厂
|
|
23
|
+
└── tools/
|
|
24
|
+
├── shared.js 52 行 clampInt / entryLine / guarded
|
|
25
|
+
├── paths.js 84 行 home:/public:/相对/绝对 解析 + 本地路径(含 ~/)
|
|
26
|
+
├── walk.js 118 行 walkDirectory(ls 递归)、findByName(受限查找)
|
|
27
|
+
└── <工具名>.js 35-116 行 每个工具一个文件:status ls stat find read
|
|
28
|
+
download upload mkdir rename move copy remove
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 依赖方向(严格单向,无环)
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
index.js ──> config.js ──> auth.js
|
|
35
|
+
│ prompt.js
|
|
36
|
+
└──> tools.js ──> tools/<工具>.js ──> tools/{shared,paths,walk}.js
|
|
37
|
+
└──> client.js ──> client/<能力>.js ──> client/{errors,transport}.js
|
|
38
|
+
└──> format.js / auth.js
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `tools/<工具>.js` **互不 import**;公共能力走 `shared/paths/walk`。
|
|
42
|
+
- `tools/<工具>.js` 通过工厂参数拿到依赖(`ctx`),不直接摸 client 单例:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
export function createLsTool(ctx) {
|
|
46
|
+
const { clampInt, config, entryLine, getClient, guarded, resolve, walkDirectory } = ctx;
|
|
47
|
+
return { name: "zspace_ls", description, parameters, outputSchema, render, call, execute };
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `client/<能力>.js` 导出 `fn(client, ...args)` 纯函数,`client.js` 把方法名绑到同一个 `client` 实例;
|
|
52
|
+
好处是每块能力都能脱离 class 单测,而对外仍然是 `new ZSpaceClient()`。
|
|
53
|
+
|
|
54
|
+
## 加东西的流程
|
|
55
|
+
|
|
56
|
+
**加一个工具**(三步)
|
|
57
|
+
|
|
58
|
+
1. `lib/tools/<名字>.js`:导出 `create<Name>Tool(ctx)`,返回
|
|
59
|
+
`{ name, description, parameters, outputSchema, render, call, execute }`;
|
|
60
|
+
2. `lib/tools.js`:`import` 该工厂,并在 `createToolSpecs` 的 `return [...]` 里加一行 —— **顺序即模型看到的工具表顺序**,破坏性写入放后面;
|
|
61
|
+
3. `test/tools.test.js`:补一条断言(路径解析/参数护栏/outputSchema 自洽)。
|
|
62
|
+
|
|
63
|
+
**加一个客户端能力**
|
|
64
|
+
|
|
65
|
+
1. `lib/client/<能力>.js`:`export async function foo(client, ...)`;
|
|
66
|
+
2. `lib/client.js`:`import` 后加一个薄委托方法;
|
|
67
|
+
3. `lib/client.d.ts`(对外 API 变了才动)。
|
|
68
|
+
|
|
69
|
+
## 约定
|
|
70
|
+
|
|
71
|
+
- 单文件控制在 ~200 行内(`transport.js` 是唯一例外:编码→发送→校验逻辑内聚在一起);
|
|
72
|
+
- 每个文件顶部一句话职责 + `@module dsh-zspace/...`;
|
|
73
|
+
- 对外 API 只有 `lib/index.js`(插件)与 `lib/client.js`(`ZSpaceClient` / `ZSpaceError`)两个入口,子模块视为内部结构,不进 `package.json` 的 `exports`;
|
|
74
|
+
- 逻辑改动必须让 28 项 mock 测试保持全绿,触及传输/上传时另跑 `node scripts/live-selftest.js` 真机验证。
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only directory access: paged listing and single-entry metadata.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-zspace/client
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { toEntry } from "../format.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* List one directory, paging until the NAS has no more rows.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} remotePath - absolute remote directory.
|
|
13
|
+
* @param {{showHidden?: boolean, maxEntries?: number}} [options] - listing options.
|
|
14
|
+
* @returns {Promise<{entries: Array<ReturnType<typeof toEntry>>, truncated: boolean}>} listing.
|
|
15
|
+
*/
|
|
16
|
+
export async function list(client, remotePath, options = {}) {
|
|
17
|
+
const maxEntries = options.maxEntries ?? client.listMaxEntries;
|
|
18
|
+
/** @type {Array<ReturnType<typeof toEntry>>} */
|
|
19
|
+
const entries = [];
|
|
20
|
+
let start = 0;
|
|
21
|
+
let truncated = false;
|
|
22
|
+
for (;;) {
|
|
23
|
+
const data = await client.post("/v2/file/list", {
|
|
24
|
+
path: remotePath,
|
|
25
|
+
show_hidden: options.showHidden ? "1" : "0",
|
|
26
|
+
start: String(start),
|
|
27
|
+
limit: String(client.pageSize),
|
|
28
|
+
});
|
|
29
|
+
const page = Array.isArray(data?.list) ? data.list : [];
|
|
30
|
+
for (const row of page) entries.push(toEntry(row));
|
|
31
|
+
if (page.length < client.pageSize) break;
|
|
32
|
+
start += page.length;
|
|
33
|
+
if (entries.length >= maxEntries) {
|
|
34
|
+
truncated = true;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (entries.length > maxEntries) return { entries: entries.slice(0, maxEntries), truncated: true };
|
|
39
|
+
return { entries, truncated };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* File or directory metadata.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} remotePath - absolute remote path.
|
|
46
|
+
* @returns {Promise<ReturnType<typeof toEntry>>} normalized entry.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* File or directory metadata.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} remotePath - absolute remote path.
|
|
53
|
+
* @returns {Promise<ReturnType<typeof toEntry>>} normalized entry.
|
|
54
|
+
*/
|
|
55
|
+
export async function info(client, remotePath) {
|
|
56
|
+
const data = await client.post("/v2/file/info", { path: remotePath });
|
|
57
|
+
return toEntry(data ?? {});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create one directory.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} remotePath - absolute path of the new directory.
|
|
64
|
+
* @returns {Promise<ReturnType<typeof toEntry>>} the created entry.
|
|
65
|
+
*/
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Downloads: streamed file download and capped reads into memory.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-zspace/client
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fsp from "node:fs/promises";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import https from "node:https";
|
|
10
|
+
import http from "node:http";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { pipeline } from "node:stream/promises";
|
|
13
|
+
import { ZSpaceError } from "./errors.js";
|
|
14
|
+
import { readStream } from "./transport.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Download one file to a local directory.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} remotePath - absolute remote file.
|
|
20
|
+
* @param {string} localDir - local destination directory (created if needed).
|
|
21
|
+
* @param {{name?: string}} [options] - destination name override.
|
|
22
|
+
* @returns {Promise<{localPath: string, bytes: number}>} written file.
|
|
23
|
+
*/
|
|
24
|
+
export async function download(client, remotePath, localDir, options = {}) {
|
|
25
|
+
const name = options.name || path.posix.basename(remotePath);
|
|
26
|
+
const dest = path.join(localDir, name);
|
|
27
|
+
await fsp.mkdir(localDir, { recursive: true });
|
|
28
|
+
const response = await client.openDownload(remotePath);
|
|
29
|
+
|
|
30
|
+
const status = response.statusCode ?? 0;
|
|
31
|
+
const contentType = String(response.headers["content-type"] ?? "");
|
|
32
|
+
if (status >= 400 || contentType.includes("json")) {
|
|
33
|
+
const { buffer } = await readStream(response);
|
|
34
|
+
client.parse({ status, text: buffer.toString("utf8") }, "/v2/file/download");
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
await pipeline(response, fs.createWriteStream(dest));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
await fsp.rm(dest, { force: true });
|
|
40
|
+
throw new ZSpaceError("EDOWNLOAD", `写入本地文件失败:${dest}(${error.message})`, { cause: error });
|
|
41
|
+
}
|
|
42
|
+
const stat = await fsp.stat(dest);
|
|
43
|
+
return { localPath: dest, bytes: stat.size };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read the head of a remote file into memory.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} remotePath - absolute remote file.
|
|
50
|
+
* @param {{maxBytes?: number}} [options] - byte cap.
|
|
51
|
+
* @returns {Promise<{buffer: Buffer, truncated: boolean, bytes: number}>} content.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Read the head of a remote file into memory.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} remotePath - absolute remote file.
|
|
58
|
+
* @param {{maxBytes?: number}} [options] - byte cap.
|
|
59
|
+
* @returns {Promise<{buffer: Buffer, truncated: boolean, bytes: number}>} content.
|
|
60
|
+
*/
|
|
61
|
+
export async function readFile(client, remotePath, options = {}) {
|
|
62
|
+
const maxBytes = options.maxBytes ?? 262_144;
|
|
63
|
+
const response = await client.openDownload(remotePath);
|
|
64
|
+
const status = response.statusCode ?? 0;
|
|
65
|
+
const contentType = String(response.headers["content-type"] ?? "");
|
|
66
|
+
if (status >= 400 || contentType.includes("json")) {
|
|
67
|
+
const { buffer } = await readStream(response);
|
|
68
|
+
client.parse({ status, text: buffer.toString("utf8") }, "/v2/file/download");
|
|
69
|
+
}
|
|
70
|
+
const { buffer, truncated } = await readStream(response, maxBytes);
|
|
71
|
+
response.destroy();
|
|
72
|
+
return { buffer, truncated, bytes: buffer.length };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Open a download response, retrying transient proxy failures.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} remotePath - absolute remote file.
|
|
79
|
+
* @returns {Promise<import("node:http").IncomingMessage>} the open response (caller consumes it).
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Open a download response, retrying transient proxy failures.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} remotePath - absolute remote file.
|
|
86
|
+
* @returns {Promise<import("node:http").IncomingMessage>} the open response (caller consumes it).
|
|
87
|
+
*/
|
|
88
|
+
export async function openDownload(client, remotePath) {
|
|
89
|
+
const url = client.endpointUrl("/v2/file/download", { path: remotePath, remote_port: "8050" });
|
|
90
|
+
return await client.retrying(
|
|
91
|
+
() =>
|
|
92
|
+
new Promise((resolve, reject) => {
|
|
93
|
+
const request = (client.baseUrl.startsWith("https:") ? https : http).request(
|
|
94
|
+
url,
|
|
95
|
+
{ method: "GET", headers: client.authHeaders() },
|
|
96
|
+
(response) => {
|
|
97
|
+
const status = response.statusCode ?? 0;
|
|
98
|
+
if (status >= 500 || status === 429) {
|
|
99
|
+
response.resume();
|
|
100
|
+
reject(new ZSpaceError(`HTTP${status}`, `HTTP ${status} 下载失败:${remotePath}`, { endpoint: "/v2/file/download" }));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
resolve(response);
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
request.setTimeout(client.timeoutMs, () => request.destroy(new ZSpaceError("ETIMEDOUT", `下载超时:${remotePath}`)));
|
|
107
|
+
request.on("error", (error) => {
|
|
108
|
+
if (error instanceof ZSpaceError) reject(error);
|
|
109
|
+
else reject(new ZSpaceError("EPROXY", `下载失败(${remotePath}):${error.message}`, { endpoint: "/v2/file/download" }));
|
|
110
|
+
});
|
|
111
|
+
request.end();
|
|
112
|
+
}),
|
|
113
|
+
error =>
|
|
114
|
+
error instanceof ZSpaceError &&
|
|
115
|
+
(error.code === "ETIMEDOUT" || error.code === "EPROXY" || /^HTTP(5\d\d|429)$/.test(error.code)),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Upload a local file into a remote directory.
|
|
121
|
+
*
|
|
122
|
+
* Files at or below `smallUploadMaxBytes` use `/v2/file/create`; larger ones
|
|
123
|
+
* (and any request the local proxy rejects with HTTP 413) use the desktop
|
|
124
|
+
* client's sliced `/v2/file/upload` protocol.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} localPath - local file.
|
|
127
|
+
* @param {string} remoteDir - absolute remote directory.
|
|
128
|
+
* @param {{name?: string, onProgress?: (sent: number, total: number) => void}} [options] - upload options.
|
|
129
|
+
* @returns {Promise<{remotePath: string, bytes: number, method: "create"|"sliced"}>} upload result.
|
|
130
|
+
*/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NAS business codes and the single error type every layer raises.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-zspace/client
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Business codes the desktop client treats as fatal during sliced uploads —
|
|
9
|
+
* retrying them only burns time (permission / conflicting dir / safe-box).
|
|
10
|
+
*/
|
|
11
|
+
export const UPLOAD_FATAL_CODES = new Set(["N001302", "N001331", "N001603", "N001397"]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* One ZSpace API or transport failure.
|
|
15
|
+
*
|
|
16
|
+
* `code` is the NAS business code (`N001411`, …) when the response parsed,
|
|
17
|
+
* or a synthetic marker (`HTTP413`, `EPROXY`, …) otherwise.
|
|
18
|
+
*/
|
|
19
|
+
export class ZSpaceError extends Error {
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} code - NAS business code or synthetic marker.
|
|
22
|
+
* @param {string} message - human-readable message.
|
|
23
|
+
* @param {{endpoint?: string, cause?: unknown}} [options] - context.
|
|
24
|
+
*/
|
|
25
|
+
constructor(code, message, options = {}) {
|
|
26
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
27
|
+
this.name = "ZSpaceError";
|
|
28
|
+
this.code = String(code);
|
|
29
|
+
this.endpoint = options.endpoint ?? "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Actionable next step for known codes, or `undefined`.
|
|
34
|
+
*
|
|
35
|
+
* @returns {string|undefined} hint text.
|
|
36
|
+
*/
|
|
37
|
+
get hint() {
|
|
38
|
+
const message = this.message;
|
|
39
|
+
if (this.code === "N001411" || /无权限/.test(message)) {
|
|
40
|
+
return "没有权限:确认该账号对目标路径可访问(「/」与裸盘根目录总是拒绝的,先 ls 已知的空间根,例如 home: 或 public:)。";
|
|
41
|
+
}
|
|
42
|
+
if (this.code === "N001315" || /不存在/.test(message)) {
|
|
43
|
+
return "路径不存在:先列出上级目录确认拼写,注意大小写与中文全角字符。";
|
|
44
|
+
}
|
|
45
|
+
if (this.code === "N001212") {
|
|
46
|
+
return "参数有误:路径必须以 / 开头(或用 home: / public: / 相对路径)。";
|
|
47
|
+
}
|
|
48
|
+
if (this.code === "HTTP413") {
|
|
49
|
+
return "本地代理拒绝超大请求体:大文件会自动改走分片上传,若仍失败请降低分片大小。";
|
|
50
|
+
}
|
|
51
|
+
if (this.code === "EPROXY") {
|
|
52
|
+
return "连不上极空间桌面客户端本地代理:确认客户端已安装、已登录并在运行(端口 13579)。";
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side mutations: mkdir, rename, move, copy, remove.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-zspace/client
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { splitRemote, toEntry } from "../format.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create one directory.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} remotePath - absolute path of the new directory.
|
|
13
|
+
* @returns {Promise<ReturnType<typeof toEntry>>} the created entry.
|
|
14
|
+
*/
|
|
15
|
+
export async function mkdir(client, remotePath) {
|
|
16
|
+
const { parent, name } = splitRemote(remotePath);
|
|
17
|
+
const data = await client.post("/v2/file/newdir", { parent, name, rename: "0" });
|
|
18
|
+
return toEntry(data ?? {});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rename a file or directory in place.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} remotePath - absolute current path.
|
|
25
|
+
* @param {string} newName - new base name (no directory part).
|
|
26
|
+
* @returns {Promise<ReturnType<typeof toEntry>>} the renamed entry.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Rename a file or directory in place.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} remotePath - absolute current path.
|
|
33
|
+
* @param {string} newName - new base name (no directory part).
|
|
34
|
+
* @returns {Promise<ReturnType<typeof toEntry>>} the renamed entry.
|
|
35
|
+
*/
|
|
36
|
+
export async function rename(client, remotePath, newName) {
|
|
37
|
+
const data = await client.post("/v2/file/modify", { path: remotePath, newname: newName });
|
|
38
|
+
return toEntry(data ?? {});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Move files or directories into a target directory.
|
|
43
|
+
*
|
|
44
|
+
* @param {string[]} remotePaths - absolute source paths.
|
|
45
|
+
* @param {string} to - absolute destination directory.
|
|
46
|
+
* @returns {Promise<void>} resolves when the NAS accepted the move.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Move files or directories into a target directory.
|
|
51
|
+
*
|
|
52
|
+
* @param {string[]} remotePaths - absolute source paths.
|
|
53
|
+
* @param {string} to - absolute destination directory.
|
|
54
|
+
* @returns {Promise<void>} resolves when the NAS accepted the move.
|
|
55
|
+
*/
|
|
56
|
+
export async function move(client, remotePaths, to) {
|
|
57
|
+
await client.post("/v2/file/move", { paths: remotePaths, to });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Copy files or directories into a target directory.
|
|
62
|
+
*
|
|
63
|
+
* @param {string[]} remotePaths - absolute source paths.
|
|
64
|
+
* @param {string} to - absolute destination directory.
|
|
65
|
+
* @returns {Promise<void>} resolves when the NAS accepted the copy.
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Copy files or directories into a target directory.
|
|
70
|
+
*
|
|
71
|
+
* @param {string[]} remotePaths - absolute source paths.
|
|
72
|
+
* @param {string} to - absolute destination directory.
|
|
73
|
+
* @returns {Promise<void>} resolves when the NAS accepted the copy.
|
|
74
|
+
*/
|
|
75
|
+
export async function copy(client, remotePaths, to) {
|
|
76
|
+
await client.post("/v2/file/copy", { paths: remotePaths, to });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Delete files or directories (the NAS moves them to its recycle bin).
|
|
81
|
+
*
|
|
82
|
+
* @param {string[]} remotePaths - absolute paths to remove.
|
|
83
|
+
* @returns {Promise<void>} resolves when the NAS accepted the deletion.
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Delete files or directories (the NAS moves them to its recycle bin).
|
|
88
|
+
*
|
|
89
|
+
* @param {string[]} remotePaths - absolute paths to remove.
|
|
90
|
+
* @returns {Promise<void>} resolves when the NAS accepted the deletion.
|
|
91
|
+
*/
|
|
92
|
+
export async function remove(client, remotePaths) {
|
|
93
|
+
await client.post("/v2/file/remove", { paths: remotePaths });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Download one file to a local directory.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} remotePath - absolute remote file.
|
|
100
|
+
* @param {string} localDir - local destination directory (created if needed).
|
|
101
|
+
* @param {{name?: string}} [options] - destination name override.
|
|
102
|
+
* @returns {Promise<{localPath: string, bytes: number}>} written file.
|
|
103
|
+
*/
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session view of the desktop client's login state: cookies, common params, safe identity.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-zspace/client
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Auth cookies for every request.
|
|
9
|
+
*
|
|
10
|
+
* @returns {Record<string, string>} request headers carrying the session.
|
|
11
|
+
*/
|
|
12
|
+
export function authHeaders(client) {
|
|
13
|
+
const creds = client.credentials;
|
|
14
|
+
return {
|
|
15
|
+
Cookie: [
|
|
16
|
+
`token=${creds.token}`,
|
|
17
|
+
`zenithtoken=${creds.token}`,
|
|
18
|
+
`nas_id=${creds.nasId}`,
|
|
19
|
+
`nasid=${creds.nasId}`,
|
|
20
|
+
`device_id=${creds.deviceId}`,
|
|
21
|
+
].join("; "),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Common form fields. These belong in the request **body**; the NAS rejects
|
|
27
|
+
* them in the query string with `N001212`.
|
|
28
|
+
*
|
|
29
|
+
* @returns {Record<string, string>} common fields.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Common form fields. These belong in the request **body**; the NAS rejects
|
|
34
|
+
* them in the query string with `N001212`.
|
|
35
|
+
*
|
|
36
|
+
* @returns {Record<string, string>} common fields.
|
|
37
|
+
*/
|
|
38
|
+
export function commonParams(client) {
|
|
39
|
+
const creds = client.credentials;
|
|
40
|
+
return {
|
|
41
|
+
token: creds.token,
|
|
42
|
+
nasid: creds.nasId,
|
|
43
|
+
plat: "web",
|
|
44
|
+
version: client.apiVersion,
|
|
45
|
+
device_id: creds.deviceId,
|
|
46
|
+
_l: "zh_cn",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build an endpoint URL with the mandatory random query suffix.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} endpoint - API path, e.g. `/v2/file/list`.
|
|
54
|
+
* @param {Record<string, unknown>} [query] - extra query values.
|
|
55
|
+
* @returns {string} absolute URL.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Identity of the account this client acts as (no secrets).
|
|
60
|
+
*
|
|
61
|
+
* @returns {{username: string, nasId: string, deviceId: string, vuexPath: string}} safe identity fields.
|
|
62
|
+
*/
|
|
63
|
+
export function identity(client) {
|
|
64
|
+
const creds = client.credentials;
|
|
65
|
+
return {
|
|
66
|
+
username: creds.username ?? "",
|
|
67
|
+
nasId: creds.nasId ?? "",
|
|
68
|
+
deviceId: creds.deviceId ?? "",
|
|
69
|
+
vuexPath: creds.vuexPath ?? "",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── transport ────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Auth cookies for every request.
|
|
77
|
+
*
|
|
78
|
+
* @returns {Record<string, string>} request headers carrying the session.
|
|
79
|
+
*/
|