dsh-zspace 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +5 -3
- package/cordis.patch.yml +4 -2
- package/lib/README.md +3 -3
- package/lib/client/browse.js +0 -7
- package/lib/client/download.js +0 -15
- package/lib/client/mutate.js +0 -31
- package/lib/client/session.js +0 -7
- package/lib/client/spaces.js +2 -32
- package/lib/client/transport.js +0 -50
- package/lib/client/upload.js +0 -12
- package/lib/client.js +0 -6
- package/lib/config.js +4 -0
- package/lib/tools/ls.js +1 -1
- package/lib/tools/write.js +73 -0
- package/lib/tools.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# 变更日志
|
|
2
2
|
|
|
3
|
+
## 0.1.2 — 2026-10-06
|
|
4
|
+
|
|
5
|
+
新增一个写入类工具 + 一个配置项,并把文档里的真实池名换成占位符(**无破坏性变更**,工具表新增一行):
|
|
6
|
+
|
|
7
|
+
- 新增 `zspace_write`:把文本直接写到 NAS,省掉"先生成本地临时文件再上传"这一步。默认覆盖同名文件,`overwrite: false` 时先探测存在性并拒绝;体积受 `writeMaxBytes` 约束;底层复用同一条上传路径(自动决定单请求/分片),临时文件无论成败都会清理。
|
|
8
|
+
- 新增配置项 `writeMaxBytes`(默认 5 MB),四处同步:Config schema、DEFAULTS、加载期校验、`cordis.patch.yml`。
|
|
9
|
+
- 文档与工具描述里的示例路径由真实的 `/<具体池>/...` 泛化为 `/<pool>/...`(探测候选路径代码保留,那是发现算法的一部分)。
|
|
10
|
+
- 清掉 0.1.1 拆分脚本引入的 19 处重复 JSDoc。
|
|
11
|
+
- 验证:29 项 mock 测试(新增 zspace_write 的 4 类断言)+ 真机 21 步自检(新增"直写文本→读回内容比对")。
|
|
12
|
+
|
|
3
13
|
## 0.1.1 — 2026-10-06
|
|
4
14
|
|
|
5
15
|
纯内部重构,**对外行为与 API 不变**(`ZSpaceClient` / `ZSpaceError` / `createToolSpecs` / 12 个工具名与参数完全保持):
|
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ dsh plugin --profile desktop add /绝对路径/dsh-zspace-0.1.0.tgz
|
|
|
41
41
|
| `zspace_read` | 把小文本文件读进上下文(默认上限 256 KB,二进制自动识别) |
|
|
42
42
|
| `zspace_download` | 从 NAS 下载到本机(默认落到 `~/Downloads/zspace`) |
|
|
43
43
|
| `zspace_upload` | 上传本地文件,**大文件自动走分片协议**,中文文件名可用 |
|
|
44
|
+
| `zspace_write` | 把文本**直接写到 NAS**(省掉本地临时文件),默认覆盖、可传 `overwrite: false` 拒绝覆盖 |
|
|
44
45
|
| `zspace_mkdir` | 建目录 |
|
|
45
46
|
| `zspace_rename` | 就地重命名 |
|
|
46
47
|
| `zspace_move` | 移动到另一个目录(服务端操作,不下载) |
|
|
@@ -51,11 +52,11 @@ dsh plugin --profile desktop add /绝对路径/dsh-zspace-0.1.0.tgz
|
|
|
51
52
|
|
|
52
53
|
| 写法 | 含义 |
|
|
53
54
|
|---|---|
|
|
54
|
-
| 省略 / `""` |
|
|
55
|
+
| 省略 / `""` | 个人空间根(形如 `/<pool>/my/data`,实际值由插件探测) |
|
|
55
56
|
| `home:相册` | 个人空间下的 `相册` |
|
|
56
|
-
| `public:公共分组/skills` |
|
|
57
|
+
| `public:公共分组/skills` | 公共空间下的路径(形如 `/<pool>/public/...`) |
|
|
57
58
|
| `docs/2026` | 相对个人空间根 |
|
|
58
|
-
|
|
|
59
|
+
| `/<pool>/my/data/x.txt` | NAS 绝对路径,原样使用 |
|
|
59
60
|
|
|
60
61
|
两个根路径由插件按存储池自动探测(`/<pool>/my/data`、`/<pool>/public`),也可在配置里写死。
|
|
61
62
|
|
|
@@ -75,6 +76,7 @@ dsh plugin --profile desktop add /绝对路径/dsh-zspace-0.1.0.tgz
|
|
|
75
76
|
| `homePath` / `publicPath` | 空=自动探测 | 写死个人/公共空间根 |
|
|
76
77
|
| `downloadDir` | 空=`~/Downloads/zspace` | 默认下载目录 |
|
|
77
78
|
| `readMaxBytes` | `262144` | `zspace_read` 上限 |
|
|
79
|
+
| `writeMaxBytes` | `5242880` | `zspace_write` 单次写入上限(更大的内容用 `zspace_upload`) |
|
|
78
80
|
| `listMaxEntries` | `2000` | 单次列目录/树的上限 |
|
|
79
81
|
| `timeoutMs` / `maxRetries` | `60000` / `2` | 请求超时与瞬时失败重试 |
|
|
80
82
|
| `smallUploadMaxBytes` | `8388608` | 超过就用分片上传 |
|
package/cordis.patch.yml
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
# Every value below has a code-side default, so this patch stays valid even on
|
|
7
7
|
# a host without patch-side helper evaluation. `homePath` / `publicPath` are
|
|
8
8
|
# intentionally empty: the plugin discovers them from the NAS storage pools at
|
|
9
|
-
# runtime (
|
|
10
|
-
# working on NAS models that name
|
|
9
|
+
# runtime (personal space looks like /<pool>/my/data, public space like
|
|
10
|
+
# /<pool>/public), which keeps the same file working on NAS models that name
|
|
11
|
+
# their pools differently.
|
|
11
12
|
|
|
12
13
|
- insert:
|
|
13
14
|
- id: zspace
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
publicPath: ""
|
|
20
21
|
downloadDir: ""
|
|
21
22
|
readMaxBytes: 262144
|
|
23
|
+
writeMaxBytes: 5242880
|
|
22
24
|
listMaxEntries: 2000
|
|
23
25
|
timeoutMs: 60000
|
|
24
26
|
maxRetries: 2
|
package/lib/README.md
CHANGED
|
@@ -19,13 +19,13 @@ lib/
|
|
|
19
19
|
│ ├── mutate.js 102 行 mkdir / rename / move / copy / remove
|
|
20
20
|
│ ├── upload.js 129 行 单请求上传 + 分片协议
|
|
21
21
|
│ └── download.js 128 行 流式下载、限量读入内存
|
|
22
|
-
├── tools.js
|
|
22
|
+
├── tools.js 68 行 工具注册表:建 ctx、按顺序调用 13 个工厂
|
|
23
23
|
└── tools/
|
|
24
24
|
├── shared.js 52 行 clampInt / entryLine / guarded
|
|
25
25
|
├── paths.js 84 行 home:/public:/相对/绝对 解析 + 本地路径(含 ~/)
|
|
26
26
|
├── walk.js 118 行 walkDirectory(ls 递归)、findByName(受限查找)
|
|
27
|
-
└── <工具名>.js 35-
|
|
28
|
-
download upload mkdir rename move copy remove
|
|
27
|
+
└── <工具名>.js 35-120 行 每个工具一个文件:status ls stat find read
|
|
28
|
+
download upload write mkdir rename move copy remove
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## 依赖方向(严格单向,无环)
|
package/lib/client/browse.js
CHANGED
|
@@ -39,13 +39,6 @@ export async function list(client, remotePath, options = {}) {
|
|
|
39
39
|
return { entries, truncated };
|
|
40
40
|
}
|
|
41
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
42
|
/**
|
|
50
43
|
* File or directory metadata.
|
|
51
44
|
*
|
package/lib/client/download.js
CHANGED
|
@@ -43,14 +43,6 @@ export async function download(client, remotePath, localDir, options = {}) {
|
|
|
43
43
|
return { localPath: dest, bytes: stat.size };
|
|
44
44
|
}
|
|
45
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
46
|
/**
|
|
55
47
|
* Read the head of a remote file into memory.
|
|
56
48
|
*
|
|
@@ -72,13 +64,6 @@ export async function readFile(client, remotePath, options = {}) {
|
|
|
72
64
|
return { buffer, truncated, bytes: buffer.length };
|
|
73
65
|
}
|
|
74
66
|
|
|
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
67
|
/**
|
|
83
68
|
* Open a download response, retrying transient proxy failures.
|
|
84
69
|
*
|
package/lib/client/mutate.js
CHANGED
|
@@ -18,14 +18,6 @@ export async function mkdir(client, remotePath) {
|
|
|
18
18
|
return toEntry(data ?? {});
|
|
19
19
|
}
|
|
20
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
21
|
/**
|
|
30
22
|
* Rename a file or directory in place.
|
|
31
23
|
*
|
|
@@ -38,14 +30,6 @@ export async function rename(client, remotePath, newName) {
|
|
|
38
30
|
return toEntry(data ?? {});
|
|
39
31
|
}
|
|
40
32
|
|
|
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
33
|
/**
|
|
50
34
|
* Move files or directories into a target directory.
|
|
51
35
|
*
|
|
@@ -57,14 +41,6 @@ export async function move(client, remotePaths, to) {
|
|
|
57
41
|
await client.post("/v2/file/move", { paths: remotePaths, to });
|
|
58
42
|
}
|
|
59
43
|
|
|
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
44
|
/**
|
|
69
45
|
* Copy files or directories into a target directory.
|
|
70
46
|
*
|
|
@@ -76,13 +52,6 @@ export async function copy(client, remotePaths, to) {
|
|
|
76
52
|
await client.post("/v2/file/copy", { paths: remotePaths, to });
|
|
77
53
|
}
|
|
78
54
|
|
|
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
55
|
/**
|
|
87
56
|
* Delete files or directories (the NAS moves them to its recycle bin).
|
|
88
57
|
*
|
package/lib/client/session.js
CHANGED
|
@@ -22,13 +22,6 @@ export function authHeaders(client) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
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
25
|
/**
|
|
33
26
|
* Common form fields. These belong in the request **body**; the NAS rejects
|
|
34
27
|
* them in the query string with `N001212`.
|
package/lib/client/spaces.js
CHANGED
|
@@ -27,12 +27,6 @@ export async function check(client) {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* Storage pools (`/zspool/info`), cached per client.
|
|
32
|
-
*
|
|
33
|
-
* @returns {Promise<Array<{name: string, status: string, totalSize: number, freeSize: number}>>} pools.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
30
|
/**
|
|
37
31
|
* Storage pools (`/zspool/info`), cached per client.
|
|
38
32
|
*
|
|
@@ -51,13 +45,6 @@ export async function pools(client) {
|
|
|
51
45
|
return client._pools;
|
|
52
46
|
}
|
|
53
47
|
|
|
54
|
-
/**
|
|
55
|
-
* Whether a remote directory can be listed.
|
|
56
|
-
*
|
|
57
|
-
* @param {string} remotePath - absolute remote path.
|
|
58
|
-
* @returns {Promise<boolean>} true when listing succeeds.
|
|
59
|
-
*/
|
|
60
|
-
|
|
61
48
|
/**
|
|
62
49
|
* Whether a remote directory can be listed.
|
|
63
50
|
*
|
|
@@ -74,17 +61,7 @@ export async function isDirectory(client, remotePath) {
|
|
|
74
61
|
}
|
|
75
62
|
|
|
76
63
|
/**
|
|
77
|
-
* Locate the personal-space root, e.g.
|
|
78
|
-
*
|
|
79
|
-
* Uses the configured path when set, otherwise tries `/<pool>/my/data` for
|
|
80
|
-
* every storage pool and falls back to the historical `/sata11/my/data`.
|
|
81
|
-
*
|
|
82
|
-
* @returns {Promise<string>} personal-space root.
|
|
83
|
-
* @throws {ZSpaceError} when no candidate is listable.
|
|
84
|
-
*/
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Locate the personal-space root, e.g. `/sata1/my/data`.
|
|
64
|
+
* Locate the personal-space root, e.g. `/<pool>/my/data`.
|
|
88
65
|
*
|
|
89
66
|
* Uses the configured path when set, otherwise tries `/<pool>/my/data` for
|
|
90
67
|
* every storage pool and falls back to the historical `/sata11/my/data`.
|
|
@@ -111,14 +88,7 @@ export async function homePath(client) {
|
|
|
111
88
|
}
|
|
112
89
|
|
|
113
90
|
/**
|
|
114
|
-
* Locate the public-space root, e.g.
|
|
115
|
-
*
|
|
116
|
-
* @returns {Promise<string>} public-space root.
|
|
117
|
-
* @throws {ZSpaceError} when no candidate is listable (account may lack access).
|
|
118
|
-
*/
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Locate the public-space root, e.g. `/sata1/public`.
|
|
91
|
+
* Locate the public-space root, e.g. `/<pool>/public`.
|
|
122
92
|
*
|
|
123
93
|
* @returns {Promise<string>} public-space root.
|
|
124
94
|
* @throws {ZSpaceError} when no candidate is listable (account may lack access).
|
package/lib/client/transport.js
CHANGED
|
@@ -90,13 +90,6 @@ export function endpointUrl(client, endpoint, query) {
|
|
|
90
90
|
return url;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
* Encode form fields, expanding arrays into repeated `key[]` entries.
|
|
95
|
-
*
|
|
96
|
-
* @param {Record<string, unknown>} fields - form fields.
|
|
97
|
-
* @returns {string} urlencoded body.
|
|
98
|
-
*/
|
|
99
|
-
|
|
100
93
|
/**
|
|
101
94
|
* Encode form fields, expanding arrays into repeated `key[]` entries.
|
|
102
95
|
*
|
|
@@ -116,15 +109,6 @@ export function formBody(client, fields) {
|
|
|
116
109
|
return parts.join("&");
|
|
117
110
|
}
|
|
118
111
|
|
|
119
|
-
/**
|
|
120
|
-
* Retry a transport call with exponential backoff.
|
|
121
|
-
*
|
|
122
|
-
* @template T
|
|
123
|
-
* @param {() => Promise<T>} attempt - one attempt.
|
|
124
|
-
* @param {(error: unknown) => boolean} shouldRetry - retry predicate.
|
|
125
|
-
* @returns {Promise<T>} the first successful result.
|
|
126
|
-
*/
|
|
127
|
-
|
|
128
112
|
/**
|
|
129
113
|
* Retry a transport call with exponential backoff.
|
|
130
114
|
*
|
|
@@ -147,23 +131,6 @@ export async function retrying(client, attempt, shouldRetry) {
|
|
|
147
131
|
throw lastError;
|
|
148
132
|
}
|
|
149
133
|
|
|
150
|
-
/**
|
|
151
|
-
* Send one buffered request.
|
|
152
|
-
*
|
|
153
|
-
* @param {object} options - request options.
|
|
154
|
-
* @param {"GET"|"POST"} [options.method] - HTTP method.
|
|
155
|
-
* @param {string} options.endpoint - API path.
|
|
156
|
-
* @param {Record<string, unknown>} [options.query] - extra query values.
|
|
157
|
-
* @param {Record<string, unknown>} [options.form] - form fields (merged with common params).
|
|
158
|
-
* @param {Record<string, unknown>} [options.fields] - raw fields appended without common params.
|
|
159
|
-
* @param {Buffer} [options.body] - raw request body.
|
|
160
|
-
* @param {string} [options.filePath] - stream this file as the request body.
|
|
161
|
-
* @param {Record<string, string>} [options.headers] - extra headers (override auth).
|
|
162
|
-
* @param {string} [options.url] - absolute URL override (sliced upload).
|
|
163
|
-
* @param {number} [options.timeoutMs] - per-attempt timeout.
|
|
164
|
-
* @returns {Promise<{status: number, headers: import("node:http").IncomingHttpHeaders, text: string}>} response.
|
|
165
|
-
*/
|
|
166
|
-
|
|
167
134
|
/**
|
|
168
135
|
* Send one buffered request.
|
|
169
136
|
*
|
|
@@ -268,15 +235,6 @@ export async function send(client, options) {
|
|
|
268
235
|
});
|
|
269
236
|
}
|
|
270
237
|
|
|
271
|
-
/**
|
|
272
|
-
* Validate a buffered JSON response and unwrap its `data`.
|
|
273
|
-
*
|
|
274
|
-
* @param {{status: number, text: string}} response - raw response.
|
|
275
|
-
* @param {string} endpoint - endpoint for error context.
|
|
276
|
-
* @returns {any} the response `data` (or the whole body when it has none).
|
|
277
|
-
* @throws {ZSpaceError} on HTTP failures, HTTP 413 and non-200 business codes.
|
|
278
|
-
*/
|
|
279
|
-
|
|
280
238
|
/**
|
|
281
239
|
* Validate a buffered JSON response and unwrap its `data`.
|
|
282
240
|
*
|
|
@@ -306,14 +264,6 @@ export function parse(client, response, endpoint) {
|
|
|
306
264
|
return body.data !== undefined ? body.data : body;
|
|
307
265
|
}
|
|
308
266
|
|
|
309
|
-
/**
|
|
310
|
-
* POST form fields and return the unwrapped `data`.
|
|
311
|
-
*
|
|
312
|
-
* @param {string} endpoint - API path.
|
|
313
|
-
* @param {Record<string, unknown>} [fields] - form fields.
|
|
314
|
-
* @returns {Promise<any>} response data.
|
|
315
|
-
*/
|
|
316
|
-
|
|
317
267
|
/**
|
|
318
268
|
* POST form fields and return the unwrapped `data`.
|
|
319
269
|
*
|
package/lib/client/upload.js
CHANGED
|
@@ -52,18 +52,6 @@ export async function upload(client, localPath, remoteDir, options = {}) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/**
|
|
56
|
-
* Sliced upload (`/v2/file/upload`), the protocol the desktop client uses
|
|
57
|
-
* for anything the local proxy will not pass through in one request.
|
|
58
|
-
*
|
|
59
|
-
* @param {string} localPath - local file.
|
|
60
|
-
* @param {string} target - absolute remote file path.
|
|
61
|
-
* @param {number} total - total byte size.
|
|
62
|
-
* @param {import("node:fs").Stats} stat - local file stat.
|
|
63
|
-
* @param {(sent: number, total: number) => void} [onProgress] - progress callback.
|
|
64
|
-
* @returns {Promise<void>} resolves when the last slice was accepted.
|
|
65
|
-
*/
|
|
66
|
-
|
|
67
55
|
/**
|
|
68
56
|
* Sliced upload (`/v2/file/upload`), the protocol the desktop client uses
|
|
69
57
|
* for anything the local proxy will not pass through in one request.
|
package/lib/client.js
CHANGED
|
@@ -69,12 +69,6 @@ export class ZSpaceClient {
|
|
|
69
69
|
this._pools = null;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/**
|
|
73
|
-
* Desktop-client credentials, loaded lazily and re-read when `vuex.json` changes.
|
|
74
|
-
*
|
|
75
|
-
* @returns {{token: string, nasId: string, deviceId: string, username: string, device: string, appVersion: string, vuexPath?: string}}
|
|
76
|
-
*/
|
|
77
|
-
|
|
78
72
|
/**
|
|
79
73
|
* Desktop-client credentials, loaded lazily and re-read when `vuex.json` changes.
|
|
80
74
|
*
|
package/lib/config.js
CHANGED
|
@@ -27,6 +27,7 @@ const DEFAULTS = Object.freeze({
|
|
|
27
27
|
publicPath: "",
|
|
28
28
|
downloadDir: "",
|
|
29
29
|
readMaxBytes: 262_144,
|
|
30
|
+
writeMaxBytes: 5 * 1024 * 1024,
|
|
30
31
|
listMaxEntries: 2000,
|
|
31
32
|
timeoutMs: 60_000,
|
|
32
33
|
maxRetries: 2,
|
|
@@ -51,6 +52,8 @@ export const Config = z.object({
|
|
|
51
52
|
downloadDir: z.string().default(""),
|
|
52
53
|
/** Byte cap for `zspace_read`. */
|
|
53
54
|
readMaxBytes: z.natural().default(262_144),
|
|
55
|
+
/** Byte cap for `zspace_write` text content (bigger writes belong in `zspace_upload`). */
|
|
56
|
+
writeMaxBytes: z.natural().default(5 * 1024 * 1024),
|
|
54
57
|
/** Entry ceiling for one listing/tree call. */
|
|
55
58
|
listMaxEntries: z.natural().default(2000),
|
|
56
59
|
/** Per-request timeout in milliseconds. */
|
|
@@ -87,6 +90,7 @@ export function resolveConfig(config) {
|
|
|
87
90
|
export function validateConfig(config) {
|
|
88
91
|
const bounds = [
|
|
89
92
|
["readMaxBytes", config.readMaxBytes],
|
|
93
|
+
["writeMaxBytes", config.writeMaxBytes],
|
|
90
94
|
["listMaxEntries", config.listMaxEntries],
|
|
91
95
|
["timeoutMs", config.timeoutMs],
|
|
92
96
|
["maxRetries", config.maxRetries],
|
package/lib/tools/ls.js
CHANGED
|
@@ -11,7 +11,7 @@ export function createLsTool(ctx) {
|
|
|
11
11
|
description:
|
|
12
12
|
"List a 极空间 / ZSpace NAS directory (跨网络可用). `path` accepts an absolute NAS path, `home:` / `public:` prefixes, or a path relative to the personal space; omit it for the personal-space root. Set `depth` > 1 to walk a subtree. Large directories are paged automatically.",
|
|
13
13
|
parameters: {
|
|
14
|
-
path: { type: "string", description: "Remote directory. Absolute (
|
|
14
|
+
path: { type: "string", description: "Remote directory. Absolute (/<pool>/my/data/…), home:/…, public:/…, or relative to the personal space. Omit for the personal-space root." },
|
|
15
15
|
depth: { type: "integer", description: "How many levels to list, 1-6 (default 1)." },
|
|
16
16
|
showHidden: { type: "boolean", description: "Include hidden entries (default false)." },
|
|
17
17
|
limit: { type: "integer", description: `Maximum entries to return (default 200, cap ${config.listMaxEntries}).` },
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zspace_write` — one atomic capability.
|
|
3
|
+
*
|
|
4
|
+
* @module dsh-zspace/tools
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fsp from "node:fs/promises";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
import { humanSize } from "../format.js";
|
|
12
|
+
|
|
13
|
+
export function createWriteTool(ctx) {
|
|
14
|
+
const { config, getClient, guarded, resolve } = ctx;
|
|
15
|
+
return {
|
|
16
|
+
name: "zspace_write",
|
|
17
|
+
description:
|
|
18
|
+
"Write text straight to a file on the 极空间 / ZSpace NAS (跨网络可用) — no local temp file needed. Use it to park generated notes, reports or config onto the NAS. Overwrites an existing file by default; pass overwrite=false to refuse replacing one. Size is capped by the writeMaxBytes config.",
|
|
19
|
+
parameters: {
|
|
20
|
+
path: { type: "string", required: true, description: "Remote file path (absolute, home:/, public:/, or relative to the personal space)." },
|
|
21
|
+
content: { type: "string", required: true, description: "UTF-8 text to write. Use zspace_upload for binary or large files." },
|
|
22
|
+
overwrite: { type: "boolean", description: "Allow replacing an existing file (default true)." },
|
|
23
|
+
},
|
|
24
|
+
outputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
properties: {
|
|
28
|
+
path: { type: "string", required: true },
|
|
29
|
+
bytes: { type: "integer", required: true },
|
|
30
|
+
method: { type: "string", required: true },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
call: (args) => ({ card: "generic", title: `zspace_write ${args.path}`, kind: "edit" }),
|
|
34
|
+
render: (_args, value) => [
|
|
35
|
+
{
|
|
36
|
+
type: "text",
|
|
37
|
+
text: `已写入 ${value.path}(${humanSize(value.bytes)},${value.method === "sliced" ? "分片" : "单请求"})`,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
execute: (args) =>
|
|
41
|
+
guarded(async () => {
|
|
42
|
+
const client = getClient();
|
|
43
|
+
const target = await resolve(args.path);
|
|
44
|
+
const content = String(args.content ?? "");
|
|
45
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
46
|
+
if (bytes === 0) throw new Error("zspace_write: `content` 不能为空");
|
|
47
|
+
if (bytes > config.writeMaxBytes) {
|
|
48
|
+
throw new Error(`zspace_write: 内容 ${bytes} 字节超过 writeMaxBytes=${config.writeMaxBytes};大文件请用 zspace_upload`);
|
|
49
|
+
}
|
|
50
|
+
if (args.overwrite === false) {
|
|
51
|
+
const exists = await client
|
|
52
|
+
.info(target)
|
|
53
|
+
.then(() => true)
|
|
54
|
+
.catch(() => false);
|
|
55
|
+
if (exists) throw new Error(`zspace_write: ${target} 已存在(overwrite=false 时拒绝覆盖)`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 上传接口只吃本地文件,所以落一个临时文件再走同一条上传路径
|
|
59
|
+
// (单请求 / 分片由 client.upload 自己决定),最后无论如何都清理。
|
|
60
|
+
const staging = await fsp.mkdtemp(path.join(os.tmpdir(), "dsh-zspace-write-"));
|
|
61
|
+
const name = path.posix.basename(target);
|
|
62
|
+
const local = path.join(staging, name);
|
|
63
|
+
try {
|
|
64
|
+
await fsp.writeFile(local, content, "utf8");
|
|
65
|
+
const remoteDir = target.slice(0, target.lastIndexOf("/")) || "/";
|
|
66
|
+
const result = await client.upload(local, remoteDir, { name });
|
|
67
|
+
return { path: target, bytes: result.bytes, method: result.method };
|
|
68
|
+
} finally {
|
|
69
|
+
await fsp.rm(staging, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}
|
package/lib/tools.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createFindTool } from "./tools/find.js";
|
|
|
19
19
|
import { createReadTool } from "./tools/read.js";
|
|
20
20
|
import { createDownloadTool } from "./tools/download.js";
|
|
21
21
|
import { createUploadTool } from "./tools/upload.js";
|
|
22
|
+
import { createWriteTool } from "./tools/write.js";
|
|
22
23
|
import { createMkdirTool } from "./tools/mkdir.js";
|
|
23
24
|
import { createRenameTool } from "./tools/rename.js";
|
|
24
25
|
import { createMoveTool } from "./tools/move.js";
|
|
@@ -57,6 +58,7 @@ export function createToolSpecs({ getClient, config }) {
|
|
|
57
58
|
createReadTool(ctx),
|
|
58
59
|
createDownloadTool(ctx),
|
|
59
60
|
createUploadTool(ctx),
|
|
61
|
+
createWriteTool(ctx),
|
|
60
62
|
createMkdirTool(ctx),
|
|
61
63
|
createRenameTool(ctx),
|
|
62
64
|
createMoveTool(ctx),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-zspace",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "极空间(ZSpace)NAS plugin for DeepSeek Harness: cross-network agent tools for browsing, searching, reading, uploading and organizing NAS files through the desktop client's relay — no LAN, no WebDAV, no SSH, no Python.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|