@aweray/hsk-cli 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -36
- package/bin/hsk-cli.js +19 -25
- package/lib/fileHosting.js +100 -221
- package/package.json +2 -3
- package/versions.json +5 -5
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
- **多平台自动适配** — 自动检测操作系统和架构,下载对应原生客户端二进制
|
|
12
12
|
- **内网穿透** — 一键将本地服务暴露到公网,支持前台/后台两种保活模式
|
|
13
|
-
- **文件托管** —
|
|
14
|
-
- **构建部署** — `deploy` 命令一键完成 build →
|
|
13
|
+
- **文件托管** — 上传本地文件或目录(自动打包),获取公网下载链接
|
|
14
|
+
- **构建部署** — `deploy` 命令一键完成 build → upload,原生二进制自动打包目录
|
|
15
|
+
- **资源复用** — 隧道支持 `--reuse` 检测,有效则复用,失效则重建
|
|
15
16
|
- **AI 友好** — 支持 `--json` 结构化输出,便于 AI Agent 解析和自动化
|
|
16
17
|
- **版本同步** — npm 包版本与原生二进制版本一致,自动匹配升级
|
|
17
18
|
|
|
@@ -61,10 +62,14 @@ hsk-cli tunnel --ip 127.0.0.1 --port 9000 --detach
|
|
|
61
62
|
|
|
62
63
|
后台模式启动后 CLI 立即退出,隧道持续运行。使用 `tunnel list` / `tunnel stop` 管理。
|
|
63
64
|
|
|
64
|
-
### 4.
|
|
65
|
+
### 4. 文件托管(上传文件或目录)
|
|
65
66
|
|
|
66
67
|
```bash
|
|
68
|
+
# 上传单文件
|
|
67
69
|
hsk-cli host ./document.pdf
|
|
70
|
+
|
|
71
|
+
# 上传目录(原生二进制自动打包为 zip)
|
|
72
|
+
hsk-cli host ./dist/
|
|
68
73
|
```
|
|
69
74
|
|
|
70
75
|
### 5. 构建并部署项目
|
|
@@ -73,7 +78,7 @@ hsk-cli host ./document.pdf
|
|
|
73
78
|
hsk-cli deploy
|
|
74
79
|
```
|
|
75
80
|
|
|
76
|
-
自动执行 `npm run build` →
|
|
81
|
+
自动执行 `npm run build` → 上传构建产物到文件托管。原生二进制自动打包目录。
|
|
77
82
|
|
|
78
83
|
---
|
|
79
84
|
|
|
@@ -104,6 +109,7 @@ hsk-cli tunnel --ip <IP> --port <PORT> [options]
|
|
|
104
109
|
| `--arch <arch>` | 否 | 强制指定客户端架构: `win32`, `win64`, `macos-x64`, `macos-arm64`, `linux-x64`, `linux-arm64` |
|
|
105
110
|
| `--force-download` | 否 | 强制重新下载客户端二进制 |
|
|
106
111
|
| `--detach` | 否 | 后台模式,CLI 立即退出,隧道持续运行 |
|
|
112
|
+
| `--reuse` | 否 | 复用已有隧道,不存在则创建 |
|
|
107
113
|
| `--json` | 否 | 以 JSON 格式输出 |
|
|
108
114
|
|
|
109
115
|
#### `tunnel list` — 查看后台隧道
|
|
@@ -126,21 +132,21 @@ hsk-cli tunnel stop --all
|
|
|
126
132
|
|
|
127
133
|
### File Hosting
|
|
128
134
|
|
|
129
|
-
#### `host` —
|
|
135
|
+
#### `host` — 文件托管
|
|
130
136
|
|
|
131
137
|
```bash
|
|
132
|
-
hsk-cli host <
|
|
138
|
+
hsk-cli host <fileOrDirPath> [options]
|
|
133
139
|
```
|
|
134
140
|
|
|
141
|
+
支持上传单文件或目录。传入目录时,原生二进制自动打包为 zip 后上传。
|
|
142
|
+
|
|
135
143
|
| Option | Required | Description |
|
|
136
144
|
|--------|----------|-------------|
|
|
137
|
-
| `<
|
|
145
|
+
| `<fileOrDirPath>` | 是 | 本地文件或目录路径 |
|
|
138
146
|
| `--resource-id <id>` | 否 | 资源 ID(传入则更新已有资源) |
|
|
139
147
|
| `--open` | 否 | 上传完成后自动打开浏览器进行认领 |
|
|
140
148
|
| `--json` | 否 | 以 JSON 格式输出 |
|
|
141
149
|
|
|
142
|
-
> **注意**: `host` 仅支持单文件。上传目录(如 build/dist)请使用 `deploy` 命令。
|
|
143
|
-
|
|
144
150
|
### Deploy
|
|
145
151
|
|
|
146
152
|
#### `deploy` — 构建并部署项目
|
|
@@ -149,20 +155,18 @@ hsk-cli host <filePath> [options]
|
|
|
149
155
|
hsk-cli deploy [options]
|
|
150
156
|
```
|
|
151
157
|
|
|
152
|
-
一键执行:**构建** →
|
|
158
|
+
一键执行:**构建** → **上传** 到文件托管。原生二进制自动处理目录打包。
|
|
153
159
|
|
|
154
160
|
| Option | Description |
|
|
155
161
|
|--------|-------------|
|
|
156
162
|
| `--build-cmd <cmd>` | 构建命令(默认: `npm run build`) |
|
|
157
163
|
| `--build-dir <dir>` | 构建输出目录(默认: `dist`) |
|
|
158
|
-
| `--
|
|
159
|
-
| `--no-build` | 跳过构建,直接打包上传现有目录 |
|
|
160
|
-
| `--no-clean` | 上传后保留打包文件 |
|
|
164
|
+
| `--no-build` | 跳过构建,直接上传现有目录 |
|
|
161
165
|
| `--resource-id <id>` | 更新已有资源 |
|
|
162
166
|
| `--open` | 上传完成后自动打开浏览器 |
|
|
163
167
|
| `--json` | 以 JSON 格式输出 |
|
|
164
168
|
|
|
165
|
-
**配置优先级**: 命令行参数 > 项目 `package.json` 的 `hsk.deploy` > 全局 `~/.hsk/config.json` > 默认配置
|
|
169
|
+
**配置优先级**: 命令行参数 > 项目 `package.json` 的 `hsk.deploy` > 全局 `~/.hsk/config.json` 的 `deploy` > 默认配置
|
|
166
170
|
|
|
167
171
|
**项目配置示例(`package.json`)**:
|
|
168
172
|
|
|
@@ -174,9 +178,7 @@ hsk-cli deploy [options]
|
|
|
174
178
|
"hsk": {
|
|
175
179
|
"deploy": {
|
|
176
180
|
"buildCmd": "npm run build",
|
|
177
|
-
"buildDir": "dist"
|
|
178
|
-
"packOutput": "dist.zip",
|
|
179
|
-
"cleanAfterUpload": true
|
|
181
|
+
"buildDir": "dist"
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
}
|
|
@@ -184,6 +186,14 @@ hsk-cli deploy [options]
|
|
|
184
186
|
|
|
185
187
|
### System
|
|
186
188
|
|
|
189
|
+
#### `status` — 检查资源状态
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
hsk-cli status [--json]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
检查已发布的隧道资源状态(进程存活 + URL 可访问)。
|
|
196
|
+
|
|
187
197
|
#### `download` — 预下载客户端
|
|
188
198
|
|
|
189
199
|
```bash
|
|
@@ -220,12 +230,19 @@ hsk-cli tunnel list
|
|
|
220
230
|
|
|
221
231
|
# 停止全部后台隧道
|
|
222
232
|
hsk-cli tunnel stop --all
|
|
233
|
+
|
|
234
|
+
# 复用已有隧道(有效则复用,失效则重建)
|
|
235
|
+
hsk-cli tunnel --ip 127.0.0.1 --port 9000 --reuse
|
|
223
236
|
```
|
|
224
237
|
|
|
225
|
-
###
|
|
238
|
+
### 上传文件或目录并自动认领
|
|
226
239
|
|
|
227
240
|
```bash
|
|
241
|
+
# 上传单文件
|
|
228
242
|
hsk-cli host ./report.pdf --open
|
|
243
|
+
|
|
244
|
+
# 上传目录(原生二进制自动打包为 zip)
|
|
245
|
+
hsk-cli host ./dist/ --open
|
|
229
246
|
```
|
|
230
247
|
|
|
231
248
|
### 跨平台预下载
|
|
@@ -240,6 +257,7 @@ hsk-cli download --arch linux-x64
|
|
|
240
257
|
```bash
|
|
241
258
|
hsk-cli tunnel --ip 127.0.0.1 --port 9000 --json
|
|
242
259
|
hsk-cli host ./document.pdf --json
|
|
260
|
+
hsk-cli status --json
|
|
243
261
|
```
|
|
244
262
|
|
|
245
263
|
### 构建并部署
|
|
@@ -251,7 +269,7 @@ hsk-cli deploy
|
|
|
251
269
|
# 命令行覆盖配置
|
|
252
270
|
hsk-cli deploy --build-cmd "yarn build" --build-dir "build"
|
|
253
271
|
|
|
254
|
-
#
|
|
272
|
+
# 跳过构建,直接上传现有目录
|
|
255
273
|
hsk-cli deploy --no-build
|
|
256
274
|
|
|
257
275
|
# 更新已有资源
|
|
@@ -269,9 +287,7 @@ hsk-cli deploy --resource-id abc123 --open
|
|
|
269
287
|
"hsk": {
|
|
270
288
|
"deploy": {
|
|
271
289
|
"buildCmd": "npm run build",
|
|
272
|
-
"buildDir": "dist"
|
|
273
|
-
"packOutput": "dist.zip",
|
|
274
|
-
"cleanAfterUpload": true
|
|
290
|
+
"buildDir": "dist"
|
|
275
291
|
}
|
|
276
292
|
}
|
|
277
293
|
}
|
|
@@ -283,23 +299,17 @@ hsk-cli deploy --resource-id abc123 --open
|
|
|
283
299
|
{
|
|
284
300
|
"deploy": {
|
|
285
301
|
"buildCmd": "npm run build",
|
|
286
|
-
"buildDir": "dist"
|
|
287
|
-
"packOutput": "dist.zip",
|
|
288
|
-
"cleanAfterUpload": true
|
|
302
|
+
"buildDir": "dist"
|
|
289
303
|
}
|
|
290
304
|
}
|
|
291
305
|
```
|
|
292
306
|
|
|
293
307
|
### 环境变量
|
|
294
308
|
|
|
295
|
-
| Variable | Description |
|
|
296
|
-
|
|
297
|
-
| `HSK_DOWNLOAD_URL` | 客户端二进制下载基地址 |
|
|
298
|
-
| `HSK_FILE_HOSTING_API` | Ticket API 完整 URL |
|
|
299
|
-
| `HSK_FILE_HOSTING_TICKET_HOST` | Ticket 服务主机 |
|
|
300
|
-
| `HSK_FILE_HOSTING_UPLOAD_HOST` | 上传服务主机 |
|
|
301
|
-
| `HSK_FILE_HOSTING_UPLOAD_PORT` | 上传服务端口 |
|
|
302
|
-
| `HSK_FILE_HOSTING_UPLOAD_SCHEME` | 上传服务协议 |
|
|
309
|
+
| Variable | Description | Default |
|
|
310
|
+
|----------|-------------|---------|
|
|
311
|
+
| `HSK_DOWNLOAD_URL` | 客户端二进制下载基地址 | 从 `versions.json` 读取 |
|
|
312
|
+
| `HSK_FILE_HOSTING_API` | Ticket API 完整 URL,覆盖原生二进制默认 host | 原生二进制内置默认值 |
|
|
303
313
|
|
|
304
314
|
---
|
|
305
315
|
|
|
@@ -325,9 +335,10 @@ hsk-cli deploy --resource-id abc123 --open
|
|
|
325
335
|
└─────────────────┘
|
|
326
336
|
```
|
|
327
337
|
|
|
328
|
-
- **npm
|
|
338
|
+
- **npm 包**(启动器):提供命令行接口、平台检测、自动下载、版本管理、资源状态持久化
|
|
329
339
|
- **versions.json**:定义 npm 包版本与原生二进制版本的映射关系
|
|
330
340
|
- **原生二进制**:按平台编译(Go/Rust/C++),实际执行网络穿透和文件传输
|
|
341
|
+
- **资源状态**:保存在 `{projectRoot}/.hsk/resources.json` 或 `~/.hsk/resources.json`
|
|
331
342
|
|
|
332
343
|
---
|
|
333
344
|
|
|
@@ -338,7 +349,9 @@ hsk-cli deploy --resource-id abc123 --open
|
|
|
338
349
|
- The npm package version and native binary version are kept in sync. When you update the npm package, the corresponding binary will be downloaded automatically.
|
|
339
350
|
- **前台模式(默认)**: CLI 启动客户端二进制作为子进程,实时输出日志,捕获到公网地址后持续运行。按 `Ctrl+C` 发送 `SIGTERM` 优雅停止。
|
|
340
351
|
- **后台模式(`--detach`)**: CLI 启动子进程后使用 `detached: true` 解除父子绑定,立即退出。子进程在后台独立运行,日志写入 `~/.hsk/logs/`,PID 记录保存到 `~/.hsk/pids/`.
|
|
341
|
-
- **Deploy**: `hsk-cli deploy` 执行 `build` → `
|
|
352
|
+
- **Deploy**: `hsk-cli deploy` 执行 `build` → `upload` 两步。原生二进制自动打包目录,无需额外依赖.
|
|
353
|
+
- **File Hosting**: `hsk-cli host` 支持单文件和目录上传。传入目录时,原生二进制自动打包为 zip.
|
|
354
|
+
- **资源复用**: `hsk-cli tunnel --reuse` 先检测已有隧道(进程存活 + URL 可访问),有效则复用,失效则重新创建.
|
|
342
355
|
- Enable `--dry-run` to preview operations without executing them.
|
|
343
356
|
|
|
344
357
|
---
|
|
@@ -349,7 +362,7 @@ See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
|
349
362
|
|
|
350
363
|
## Contributing
|
|
351
364
|
|
|
352
|
-
We welcome contributions! Please see [CONTRIBUTING.md](
|
|
365
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
353
366
|
|
|
354
367
|
## License
|
|
355
368
|
|
package/bin/hsk-cli.js
CHANGED
|
@@ -187,16 +187,19 @@ const hostCmd = program
|
|
|
187
187
|
const fs = require('fs');
|
|
188
188
|
const path = require('path');
|
|
189
189
|
const stats = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
|
|
190
|
-
const
|
|
190
|
+
const apiUrl = process.env.HSK_FILE_HOSTING_API || 'https://onion-forward-api.oraybeta.com/public/file-hosting';
|
|
191
|
+
const cmdParts = ['hsk-cli file-hosting'];
|
|
192
|
+
if (apiUrl) cmdParts.push(`-api "${apiUrl}"`);
|
|
193
|
+
if (options.resourceId) cmdParts.push(`-resource-id ${options.resourceId}`);
|
|
194
|
+
cmdParts.push(filePath);
|
|
191
195
|
console.log(format.dryRunInfo(
|
|
192
|
-
|
|
196
|
+
cmdParts.join(' '),
|
|
193
197
|
{
|
|
194
198
|
文件路径: filePath,
|
|
195
199
|
文件大小: stats ? `${(stats.size / 1024 / 1024).toFixed(2)} MB` : '文件不存在',
|
|
196
200
|
资源ID: options.resourceId || '(新建)',
|
|
197
|
-
预期操作: '
|
|
198
|
-
|
|
199
|
-
预期输出: 'publicUrl + resourceId',
|
|
201
|
+
预期操作: '原生二进制自动处理:计算 hash → 获取 ticket → 上传/更新',
|
|
202
|
+
预期输出: '[AI_RESULT] public_url + resource_id',
|
|
200
203
|
}
|
|
201
204
|
));
|
|
202
205
|
return;
|
|
@@ -310,19 +313,22 @@ program
|
|
|
310
313
|
const cfg = config.getDeployConfig();
|
|
311
314
|
const buildCmd = options.buildCmd || cfg.buildCmd;
|
|
312
315
|
const buildDir = options.buildDir || cfg.buildDir;
|
|
313
|
-
const packOutput = options.packOutput || cfg.packOutput;
|
|
314
|
-
const cleanAfter = options.clean !== false && cfg.cleanAfterUpload;
|
|
315
316
|
|
|
316
317
|
if (isDryRun(options)) {
|
|
317
318
|
const info = options.arch ? platform.fromString(options.arch) : platform.detect();
|
|
319
|
+
const apiUrl = process.env.HSK_FILE_HOSTING_API || 'https://onion-forward-api.oraybeta.com/public/file-hosting';
|
|
320
|
+
const cmdParts = ['hsk-cli file-hosting'];
|
|
321
|
+
if (apiUrl) cmdParts.push(`-api "${apiUrl}"`);
|
|
322
|
+
if (options.resourceId) cmdParts.push(`-resource-id ${options.resourceId}`);
|
|
323
|
+
cmdParts.push(buildDir);
|
|
318
324
|
console.log(format.dryRunInfo(
|
|
319
|
-
|
|
325
|
+
`${buildCmd} → ${cmdParts.join(' ')}`,
|
|
320
326
|
{
|
|
321
327
|
构建命令: buildCmd,
|
|
322
328
|
构建目录: buildDir,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
329
|
+
资源ID: options.resourceId || '(新建)',
|
|
330
|
+
预期操作: '构建 → 原生二进制自动打包目录 → 上传',
|
|
331
|
+
预期输出: '[AI_RESULT] public_url + resource_id',
|
|
326
332
|
}
|
|
327
333
|
));
|
|
328
334
|
return;
|
|
@@ -342,21 +348,9 @@ program
|
|
|
342
348
|
throw new Error(`构建目录不存在: ${buildDir}`);
|
|
343
349
|
}
|
|
344
350
|
|
|
345
|
-
// 3.
|
|
346
|
-
console.log(chalk.cyan(`📦 打包 ${buildDir}...`));
|
|
347
|
-
const packResult = await pack.packDir(buildDir, packOutput);
|
|
348
|
-
console.log(chalk.green(`✅ 打包完成: ${packResult.path}`));
|
|
349
|
-
console.log(chalk.gray(` 大小: ${(packResult.size / 1024 / 1024).toFixed(2)} MB`));
|
|
350
|
-
|
|
351
|
-
// 4. 上传
|
|
351
|
+
// 3. 上传(原生二进制自动打包目录)
|
|
352
352
|
console.log(chalk.cyan('☁️ 上传中...'));
|
|
353
|
-
const result = await fileHosting.host(
|
|
354
|
-
|
|
355
|
-
// 5. 清理
|
|
356
|
-
if (cleanAfter) {
|
|
357
|
-
pack.cleanupPack(packResult.path);
|
|
358
|
-
console.log(chalk.gray('🧹 已清理打包文件'));
|
|
359
|
-
}
|
|
353
|
+
const result = await fileHosting.host(buildDir, { resourceId: options.resourceId });
|
|
360
354
|
|
|
361
355
|
handleOutput(result, fmt);
|
|
362
356
|
|
package/lib/fileHosting.js
CHANGED
|
@@ -1,257 +1,136 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const crypto = require('crypto');
|
|
4
|
-
const fetch = require('node-fetch');
|
|
5
|
-
const FormData = require('form-data');
|
|
6
3
|
const { spawn } = require('child_process');
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const DEFAULT_CONFIG = {
|
|
10
|
-
ticketHost: 'onion-forward-api.oraybeta.com',
|
|
11
|
-
ticketPort: 443,
|
|
12
|
-
ticketScheme: 'https',
|
|
13
|
-
uploadHost: 'onion-fw-test2.oraybeta.com',
|
|
14
|
-
uploadPort: 8010,
|
|
15
|
-
uploadScheme: 'https',
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const HEADERS_BASE = {
|
|
19
|
-
'User-Agent': `HSK-CLI/${version}`,
|
|
20
|
-
Accept: '*/*',
|
|
21
|
-
Connection: 'keep-alive',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function getTicketApiUrl() {
|
|
25
|
-
if (process.env.HSK_FILE_HOSTING_API) {
|
|
26
|
-
return process.env.HSK_FILE_HOSTING_API.replace(/\/+$/, '');
|
|
27
|
-
}
|
|
28
|
-
const host = process.env.HSK_FILE_HOSTING_TICKET_HOST || DEFAULT_CONFIG.ticketHost;
|
|
29
|
-
const port = process.env.HSK_FILE_HOSTING_TICKET_PORT || DEFAULT_CONFIG.ticketPort;
|
|
30
|
-
const scheme = process.env.HSK_FILE_HOSTING_TICKET_SCHEME || DEFAULT_CONFIG.ticketScheme;
|
|
31
|
-
return `${scheme}://${host}:${port}/public/file-hosting`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getUploadBaseUrl() {
|
|
35
|
-
const host = process.env.HSK_FILE_HOSTING_UPLOAD_HOST || DEFAULT_CONFIG.uploadHost;
|
|
36
|
-
const port = process.env.HSK_FILE_HOSTING_UPLOAD_PORT || DEFAULT_CONFIG.uploadPort;
|
|
37
|
-
const scheme = process.env.HSK_FILE_HOSTING_UPLOAD_SCHEME || DEFAULT_CONFIG.uploadScheme;
|
|
38
|
-
return `${scheme}://${host}:${port}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const TICKET_API = getTicketApiUrl();
|
|
4
|
+
const download = require('./download');
|
|
5
|
+
const resourceStore = require('./resourceStore');
|
|
42
6
|
|
|
43
7
|
/**
|
|
44
|
-
*
|
|
8
|
+
* 解析原生二进制输出,提取 [AI_RESULT] 键值对
|
|
45
9
|
*/
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 获取文件托管 ticket(创建不传 resourceId,更新时传 resourceId)
|
|
65
|
-
*/
|
|
66
|
-
async function getFileHostingTicket(fileHash, resourceId = null) {
|
|
67
|
-
const payload = { file_hash: fileHash };
|
|
68
|
-
if (resourceId) {
|
|
69
|
-
payload.resource_id = resourceId;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const res = await fetch(TICKET_API, {
|
|
73
|
-
method: 'POST',
|
|
74
|
-
headers: { ...HEADERS_BASE, 'Content-Type': 'application/json' },
|
|
75
|
-
body: JSON.stringify(payload),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
if (!res.ok) {
|
|
79
|
-
const text = await res.text();
|
|
80
|
-
throw new Error(`获取 ticket 失败: HTTP ${res.status} - ${text.slice(0, 500)}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const json = await res.json();
|
|
84
|
-
const data = json.data;
|
|
85
|
-
|
|
86
|
-
if (!data || !data.ticket) {
|
|
87
|
-
throw new Error(`获取 ticket 响应格式异常: ${JSON.stringify(json)}`);
|
|
10
|
+
function parseAiResult(stdout) {
|
|
11
|
+
const result = {};
|
|
12
|
+
const lines = stdout.split('\n');
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
const match = line.match(/^\[AI_RESULT\]\s+(\w+):\s*(.+)$/);
|
|
15
|
+
if (match) {
|
|
16
|
+
const key = match[1];
|
|
17
|
+
const value = match[2].trim();
|
|
18
|
+
try {
|
|
19
|
+
result[key] = JSON.parse(value);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
result[key] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
88
24
|
}
|
|
89
|
-
|
|
90
|
-
return data;
|
|
25
|
+
return result;
|
|
91
26
|
}
|
|
92
27
|
|
|
93
28
|
/**
|
|
94
|
-
*
|
|
95
|
-
* @param {string} token - Bearer ticket
|
|
96
|
-
* @param {string} filePath - 本地文件路径
|
|
97
|
-
* @param {{ update?: boolean, uploadUrl?: string }} options
|
|
29
|
+
* 打开浏览器认领页面
|
|
98
30
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const res = await fetch(url, {
|
|
109
|
-
method,
|
|
110
|
-
headers: {
|
|
111
|
-
...HEADERS_BASE,
|
|
112
|
-
Authorization: `Bearer ${token}`,
|
|
113
|
-
...form.getHeaders(),
|
|
114
|
-
},
|
|
115
|
-
body: form,
|
|
31
|
+
function openClaimUrl(url) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const platform = process.platform;
|
|
34
|
+
const command = platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open';
|
|
35
|
+
const proc = spawn(command, [url], { stdio: 'ignore', detached: true, shell: platform === 'win32' });
|
|
36
|
+
proc.on('error', () => { /* ignore */ });
|
|
37
|
+
proc.unref();
|
|
38
|
+
resolve();
|
|
116
39
|
});
|
|
117
|
-
|
|
118
|
-
if (!res.ok) {
|
|
119
|
-
const text = await res.text();
|
|
120
|
-
throw new Error(`文件${update ? '更新' : '上传'}失败: HTTP ${res.status} - ${text.slice(0, 500)}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return res.json();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function resolveResourceId(ticketData, uploadResult, resourceIdInput) {
|
|
127
|
-
let resourceId = ticketData.resource_id || resourceIdInput || null;
|
|
128
|
-
const resultData = uploadResult && uploadResult.data;
|
|
129
|
-
|
|
130
|
-
if (resultData && typeof resultData === 'object' && resultData.resource_id) {
|
|
131
|
-
resourceId = resultData.resource_id;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!resourceId && ticketData.public_url) {
|
|
135
|
-
const parts = ticketData.public_url.replace(/\/+$/, '').split('/');
|
|
136
|
-
if (parts.length) {
|
|
137
|
-
resourceId = parts[parts.length - 1];
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return resourceId;
|
|
142
40
|
}
|
|
143
41
|
|
|
144
42
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* @param {{ resourceId?: string }} options
|
|
43
|
+
* 文件托管(创建或更新)
|
|
44
|
+
* 调用原生二进制:./hsk-cli file-hosting [options] <file_or_dir>
|
|
148
45
|
*/
|
|
149
46
|
async function host(filePath, options = {}) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
throw new Error(`文件不存在: ${filePath}`);
|
|
154
|
-
}
|
|
155
|
-
if (fs.statSync(filePath).isDirectory()) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`路径是目录而非文件: ${filePath}\n` +
|
|
158
|
-
`请使用 \`hsk-cli deploy\` 命令上传目录,或手动打包后上传。`
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
if (!fs.statSync(filePath).isFile()) {
|
|
162
|
-
throw new Error(`路径不是文件: ${filePath}`);
|
|
47
|
+
const resolvedPath = path.resolve(filePath);
|
|
48
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
49
|
+
throw new Error(`文件不存在: ${resolvedPath}`);
|
|
163
50
|
}
|
|
164
51
|
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
const isUpdate = Boolean(resourceIdInput);
|
|
52
|
+
// 确保原生二进制存在
|
|
53
|
+
const binPath = await download.ensureBinary();
|
|
168
54
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
55
|
+
// 构造参数
|
|
56
|
+
const args = ['file-hosting'];
|
|
57
|
+
const apiUrl = process.env.HSK_FILE_HOSTING_API;
|
|
58
|
+
if (apiUrl) {
|
|
59
|
+
args.push('-api', apiUrl);
|
|
60
|
+
}
|
|
61
|
+
if (options.resourceId) {
|
|
62
|
+
args.push('-resource-id', options.resourceId);
|
|
63
|
+
}
|
|
64
|
+
args.push(resolvedPath);
|
|
172
65
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const proc = spawn(binPath, args, {
|
|
68
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
69
|
+
windowsHide: true,
|
|
177
70
|
});
|
|
178
71
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const json = await res.json();
|
|
184
|
-
const baseUrl = TICKET_API.replace(/\/public\/file-hosting$/, '');
|
|
185
|
-
const publicUrl = `${baseUrl}/${fileName}`;
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
success: true,
|
|
189
|
-
mode: 'direct',
|
|
190
|
-
publicUrl,
|
|
191
|
-
fileName,
|
|
192
|
-
filePath,
|
|
193
|
-
serverResponse: json,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
72
|
+
let stdout = '';
|
|
73
|
+
let stderr = '';
|
|
196
74
|
|
|
197
|
-
|
|
198
|
-
|
|
75
|
+
proc.stdout.on('data', (data) => {
|
|
76
|
+
stdout += data.toString();
|
|
77
|
+
process.stdout.write(data); // 实时转发到终端
|
|
78
|
+
});
|
|
199
79
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
80
|
+
proc.stderr.on('data', (data) => {
|
|
81
|
+
stderr += data.toString();
|
|
82
|
+
process.stderr.write(data);
|
|
83
|
+
});
|
|
203
84
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
85
|
+
proc.on('close', (code) => {
|
|
86
|
+
if (code !== 0) {
|
|
87
|
+
reject(new Error(`文件托管失败: ${stderr || stdout}`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result = parseAiResult(stdout);
|
|
92
|
+
|
|
93
|
+
if (!result.public_url || !result.resource_id) {
|
|
94
|
+
reject(new Error(`无法从输出中解析结果:\n${stdout}`));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const isUpdate = Boolean(options.resourceId);
|
|
99
|
+
const resourceId = String(result.resource_id);
|
|
100
|
+
const publicUrl = result.public_url;
|
|
101
|
+
|
|
102
|
+
// 保存资源状态
|
|
103
|
+
resourceStore.saveResource({
|
|
104
|
+
id: `host-${resourceId}`,
|
|
105
|
+
type: 'host',
|
|
106
|
+
filePath: resolvedPath,
|
|
107
|
+
publicUrl,
|
|
108
|
+
resourceId,
|
|
109
|
+
fileName: path.basename(resolvedPath),
|
|
110
|
+
status: 'active',
|
|
111
|
+
createdAt: new Date().toISOString(),
|
|
112
|
+
lastCheckedAt: new Date().toISOString(),
|
|
209
113
|
});
|
|
210
|
-
} else {
|
|
211
|
-
uploadResult = await uploadFile(ticket.ticket, filePath, { update: isUpdate });
|
|
212
|
-
}
|
|
213
|
-
} catch (err) {
|
|
214
|
-
uploadError = err.message;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const resourceId = resolveResourceId(ticket, uploadResult, resourceIdInput);
|
|
218
114
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
uploadResponse: uploadResult,
|
|
230
|
-
error: uploadError,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
115
|
+
resolve({
|
|
116
|
+
success: true,
|
|
117
|
+
mode: isUpdate ? 'update' : 'create',
|
|
118
|
+
publicUrl,
|
|
119
|
+
verifyCode: result.verify_code || null,
|
|
120
|
+
resourceId,
|
|
121
|
+
createResult: result.create_result || null,
|
|
122
|
+
stdout: stdout.trim(),
|
|
123
|
+
});
|
|
124
|
+
});
|
|
233
125
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
function openClaimUrl(url) {
|
|
238
|
-
return new Promise((resolve) => {
|
|
239
|
-
const platform = process.platform;
|
|
240
|
-
const command = platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open';
|
|
241
|
-
const proc = spawn(command, [url], { stdio: 'ignore', detached: true, shell: platform === 'win32' });
|
|
242
|
-
proc.on('error', () => { /* ignore */ });
|
|
243
|
-
proc.unref();
|
|
244
|
-
resolve();
|
|
126
|
+
proc.on('error', (err) => {
|
|
127
|
+
reject(new Error(`无法启动文件托管: ${err.message}`));
|
|
128
|
+
});
|
|
245
129
|
});
|
|
246
130
|
}
|
|
247
131
|
|
|
248
132
|
module.exports = {
|
|
249
|
-
TICKET_API,
|
|
250
|
-
getTicketApiUrl,
|
|
251
|
-
getUploadBaseUrl,
|
|
252
|
-
sha256File,
|
|
253
|
-
getFileHostingTicket,
|
|
254
|
-
uploadFile,
|
|
255
133
|
host,
|
|
256
134
|
openClaimUrl,
|
|
135
|
+
parseAiResult,
|
|
257
136
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aweray/hsk-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "HSK CLI - 内网穿透 & 文件托管,支持 Windows/macOS/Linux 多平台",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"commander": "^11.0.0",
|
|
30
30
|
"chalk": "^4.1.2",
|
|
31
|
-
"node-fetch": "^2.7.0"
|
|
32
|
-
"form-data": "^4.0.0"
|
|
31
|
+
"node-fetch": "^2.7.0"
|
|
33
32
|
},
|
|
34
33
|
"homepage": "https://hsk.oray.com",
|
|
35
34
|
"files": [
|
package/versions.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.3.0",
|
|
3
3
|
"downloadBaseUrl": "https://dw.oray.com/onion/cli",
|
|
4
4
|
"binaries": {
|
|
5
5
|
"windows-amd64": {
|
|
6
|
-
"filename": "hsk-cli-windows-amd64-v0.
|
|
6
|
+
"filename": "hsk-cli-windows-amd64-v0.3.0.exe"
|
|
7
7
|
},
|
|
8
8
|
"darwin-amd64": {
|
|
9
|
-
"filename": "hsk-cli-darwin-amd64-v0.
|
|
9
|
+
"filename": "hsk-cli-darwin-amd64-v0.3.0"
|
|
10
10
|
},
|
|
11
11
|
"darwin-arm64": {
|
|
12
|
-
"filename": "hsk-cli-darwin-arm64-v0.
|
|
12
|
+
"filename": "hsk-cli-darwin-arm64-v0.3.0"
|
|
13
13
|
},
|
|
14
14
|
"linux-amd64": {
|
|
15
|
-
"filename": "hsk-cli-linux-amd64-v0.
|
|
15
|
+
"filename": "hsk-cli-linux-amd64-v0.3.0"
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|