@aweray/hsk-cli 0.2.2 → 0.4.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/README.md +49 -36
- package/bin/hsk-cli.js +24 -27
- package/lib/fileHosting.js +126 -218
- package/lib/tunnel.js +1 -1
- 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
|
@@ -181,28 +181,32 @@ const hostCmd = program
|
|
|
181
181
|
.option('--json', '以 JSON 格式输出(等同于 --format json)')
|
|
182
182
|
.option('--open', '上传完成后自动打开浏览器进行认领')
|
|
183
183
|
.option('--resource-id <id>', '资源 ID(传入则更新已有资源,否则创建新资源)')
|
|
184
|
+
.option('--reuse', '复用已有资源,不存在则创建新资源')
|
|
184
185
|
.action(async (filePath, options) => {
|
|
185
186
|
const fmt = getFormat(options);
|
|
186
187
|
if (isDryRun(options)) {
|
|
187
188
|
const fs = require('fs');
|
|
188
189
|
const path = require('path');
|
|
189
190
|
const stats = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
|
|
190
|
-
const
|
|
191
|
+
const apiUrl = process.env.HSK_FILE_HOSTING_API;
|
|
192
|
+
const cmdParts = ['hsk-cli file-hosting'];
|
|
193
|
+
if (apiUrl) cmdParts.push(`-api "${apiUrl}"`);
|
|
194
|
+
if (options.resourceId) cmdParts.push(`-resource-id ${options.resourceId}`);
|
|
195
|
+
cmdParts.push(filePath);
|
|
191
196
|
console.log(format.dryRunInfo(
|
|
192
|
-
|
|
197
|
+
cmdParts.join(' '),
|
|
193
198
|
{
|
|
194
199
|
文件路径: filePath,
|
|
195
200
|
文件大小: stats ? `${(stats.size / 1024 / 1024).toFixed(2)} MB` : '文件不存在',
|
|
196
201
|
资源ID: options.resourceId || '(新建)',
|
|
197
|
-
预期操作: '
|
|
198
|
-
|
|
199
|
-
预期输出: 'publicUrl + resourceId',
|
|
202
|
+
预期操作: '原生二进制自动处理:计算 hash → 获取 ticket → 上传/更新',
|
|
203
|
+
预期输出: '[AI_RESULT] public_url + resource_id',
|
|
200
204
|
}
|
|
201
205
|
));
|
|
202
206
|
return;
|
|
203
207
|
}
|
|
204
208
|
try {
|
|
205
|
-
const result = await fileHosting.host(filePath, { resourceId: options.resourceId });
|
|
209
|
+
const result = await fileHosting.host(filePath, { resourceId: options.resourceId, reuse: options.reuse });
|
|
206
210
|
handleOutput(result, fmt);
|
|
207
211
|
if (options.open && result.publicUrl) {
|
|
208
212
|
console.log(chalk.gray('🌐 正在打开浏览器...'));
|
|
@@ -298,8 +302,8 @@ program
|
|
|
298
302
|
.option('--build-dir <dir>', '构建输出目录,如 "dist"')
|
|
299
303
|
.option('--pack-output <file>', '打包输出文件名,如 "dist.zip"', 'dist.zip')
|
|
300
304
|
.option('--no-build', '跳过构建,直接打包上传')
|
|
301
|
-
.option('--no-clean', '上传后保留打包文件')
|
|
302
305
|
.option('--resource-id <id>', '资源 ID(更新已有资源)')
|
|
306
|
+
.option('--reuse', '复用已有资源,不存在则创建新资源')
|
|
303
307
|
.option('--open', '上传完成后自动打开浏览器进行认领')
|
|
304
308
|
.option('--json', '以 JSON 格式输出(等同于 --format json)')
|
|
305
309
|
.action(async (options) => {
|
|
@@ -310,19 +314,24 @@ program
|
|
|
310
314
|
const cfg = config.getDeployConfig();
|
|
311
315
|
const buildCmd = options.buildCmd || cfg.buildCmd;
|
|
312
316
|
const buildDir = options.buildDir || cfg.buildDir;
|
|
313
|
-
const packOutput = options.packOutput || cfg.packOutput;
|
|
314
|
-
const cleanAfter = options.clean !== false && cfg.cleanAfterUpload;
|
|
315
317
|
|
|
316
318
|
if (isDryRun(options)) {
|
|
317
319
|
const info = options.arch ? platform.fromString(options.arch) : platform.detect();
|
|
320
|
+
const apiUrl = process.env.HSK_FILE_HOSTING_API;
|
|
321
|
+
const cmdParts = ['hsk-cli file-hosting'];
|
|
322
|
+
if (apiUrl) cmdParts.push(`-api "${apiUrl}"`);
|
|
323
|
+
if (options.resourceId) cmdParts.push(`-resource-id ${options.resourceId}`);
|
|
324
|
+
if (options.reuse) cmdParts.push('--reuse');
|
|
325
|
+
cmdParts.push(buildDir);
|
|
318
326
|
console.log(format.dryRunInfo(
|
|
319
|
-
|
|
327
|
+
`${buildCmd} → ${cmdParts.join(' ')}`,
|
|
320
328
|
{
|
|
321
329
|
构建命令: buildCmd,
|
|
322
330
|
构建目录: buildDir,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
331
|
+
资源ID: options.resourceId || '(新建)',
|
|
332
|
+
复用检测: options.reuse ? '启用' : '禁用',
|
|
333
|
+
预期操作: '构建 → 原生二进制自动打包目录 → 上传',
|
|
334
|
+
预期输出: '[AI_RESULT] public_url + resource_id',
|
|
326
335
|
}
|
|
327
336
|
));
|
|
328
337
|
return;
|
|
@@ -342,21 +351,9 @@ program
|
|
|
342
351
|
throw new Error(`构建目录不存在: ${buildDir}`);
|
|
343
352
|
}
|
|
344
353
|
|
|
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. 上传
|
|
354
|
+
// 3. 上传(原生二进制自动打包目录)
|
|
352
355
|
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
|
-
}
|
|
356
|
+
const result = await fileHosting.host(buildDir, { resourceId: options.resourceId, reuse: options.reuse });
|
|
360
357
|
|
|
361
358
|
handleOutput(result, fmt);
|
|
362
359
|
|
package/lib/fileHosting.js
CHANGED
|
@@ -1,257 +1,165 @@
|
|
|
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
|
|
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');
|
|
6
|
+
const resourceChecker = require('./resourceChecker');
|
|
42
7
|
|
|
43
8
|
/**
|
|
44
|
-
*
|
|
9
|
+
* 解析原生二进制输出,提取 [AI_RESULT] 键值对
|
|
45
10
|
*/
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
11
|
+
function parseAiResult(stdout) {
|
|
12
|
+
const result = {};
|
|
13
|
+
const lines = stdout.split('\n');
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
const match = line.match(/^\[AI_RESULT\]\s+(\w+):\s*(.+)$/);
|
|
16
|
+
if (match) {
|
|
17
|
+
const key = match[1];
|
|
18
|
+
const value = match[2].trim();
|
|
19
|
+
try {
|
|
20
|
+
result[key] = JSON.parse(value);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
result[key] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
61
27
|
}
|
|
62
28
|
|
|
63
29
|
/**
|
|
64
|
-
*
|
|
30
|
+
* 打开浏览器认领页面
|
|
65
31
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
headers: { ...HEADERS_BASE, 'Content-Type': 'application/json' },
|
|
75
|
-
body: JSON.stringify(payload),
|
|
32
|
+
function openClaimUrl(url) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const platform = process.platform;
|
|
35
|
+
const command = platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open';
|
|
36
|
+
const proc = spawn(command, [url], { stdio: 'ignore', detached: true, shell: platform === 'win32' });
|
|
37
|
+
proc.on('error', () => { /* ignore */ });
|
|
38
|
+
proc.unref();
|
|
39
|
+
resolve();
|
|
76
40
|
});
|
|
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)}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return data;
|
|
91
41
|
}
|
|
92
42
|
|
|
93
43
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* @param {string} filePath - 本地文件路径
|
|
97
|
-
* @param {{ update?: boolean, uploadUrl?: string }} options
|
|
44
|
+
* 文件托管(创建或更新)
|
|
45
|
+
* 调用原生二进制:./hsk-cli file-hosting [options] <file_or_dir>
|
|
98
46
|
*/
|
|
99
|
-
async function
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
form.append('file', fs.createReadStream(filePath), { filename: fileName });
|
|
104
|
-
|
|
105
|
-
const url = uploadUrl || `${getUploadBaseUrl()}${update ? '/resource/update' : '/upload'}`;
|
|
106
|
-
const method = update ? 'PUT' : 'POST';
|
|
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,
|
|
116
|
-
});
|
|
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;
|
|
47
|
+
async function host(filePath, options = {}) {
|
|
48
|
+
const resolvedPath = path.resolve(filePath);
|
|
49
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
50
|
+
throw new Error(`文件不存在: ${resolvedPath}`);
|
|
132
51
|
}
|
|
133
52
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
53
|
+
// === reuse: 检测已有资源是否有效 ===
|
|
54
|
+
if (options.reuse) {
|
|
55
|
+
let existing = null;
|
|
56
|
+
if (options.resourceId) {
|
|
57
|
+
existing = resourceStore.findResource((r) => r.type === 'host' && r.resourceId === options.resourceId);
|
|
58
|
+
} else {
|
|
59
|
+
existing = resourceStore.findResource((r) => r.type === 'host' && r.filePath === resolvedPath);
|
|
60
|
+
}
|
|
61
|
+
if (existing) {
|
|
62
|
+
const check = await resourceChecker.checkResource(existing);
|
|
63
|
+
if (check.valid) {
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
mode: 'reused',
|
|
67
|
+
publicUrl: existing.publicUrl,
|
|
68
|
+
resourceId: existing.resourceId,
|
|
69
|
+
filePath: resolvedPath,
|
|
70
|
+
fileName: path.basename(resolvedPath),
|
|
71
|
+
reused: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// 失效:保存旧 resourceId 用于更新
|
|
75
|
+
if (!options.resourceId) {
|
|
76
|
+
options.resourceId = existing.resourceId;
|
|
77
|
+
}
|
|
138
78
|
}
|
|
139
79
|
}
|
|
140
80
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 完整文件托管流程(创建或更新)
|
|
146
|
-
* @param {string} filePath
|
|
147
|
-
* @param {{ resourceId?: string }} options
|
|
148
|
-
*/
|
|
149
|
-
async function host(filePath, options = {}) {
|
|
150
|
-
checkApiConfig();
|
|
81
|
+
// 确保原生二进制存在
|
|
82
|
+
const binPath = await download.ensureBinary();
|
|
151
83
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
`路径是目录而非文件: ${filePath}\n` +
|
|
158
|
-
`请使用 \`hsk-cli deploy\` 命令上传目录,或手动打包后上传。`
|
|
159
|
-
);
|
|
84
|
+
// 构造参数
|
|
85
|
+
const args = ['file-hosting'];
|
|
86
|
+
const apiUrl = process.env.HSK_FILE_HOSTING_API;
|
|
87
|
+
if (apiUrl) {
|
|
88
|
+
args.push('-api', apiUrl);
|
|
160
89
|
}
|
|
161
|
-
if (
|
|
162
|
-
|
|
90
|
+
if (options.resourceId) {
|
|
91
|
+
args.push('-resource-id', options.resourceId);
|
|
163
92
|
}
|
|
93
|
+
args.push(resolvedPath);
|
|
164
94
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (process.env.HSK_FILE_HOSTING_DIRECT) {
|
|
170
|
-
const form = new FormData();
|
|
171
|
-
form.append('file', fs.createReadStream(filePath), { filename: fileName });
|
|
172
|
-
|
|
173
|
-
const res = await fetch(TICKET_API, {
|
|
174
|
-
method: 'POST',
|
|
175
|
-
headers: { ...form.getHeaders() },
|
|
176
|
-
body: form,
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const proc = spawn(binPath, args, {
|
|
97
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
98
|
+
windowsHide: true,
|
|
177
99
|
});
|
|
178
100
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const json = await res.json();
|
|
184
|
-
const baseUrl = TICKET_API.replace(/\/public\/file-hosting$/, '');
|
|
185
|
-
const publicUrl = `${baseUrl}/${fileName}`;
|
|
101
|
+
let stdout = '';
|
|
102
|
+
let stderr = '';
|
|
186
103
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
fileName,
|
|
192
|
-
filePath,
|
|
193
|
-
serverResponse: json,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const fileHash = await sha256File(filePath);
|
|
198
|
-
const ticket = await getFileHostingTicket(fileHash, resourceIdInput);
|
|
104
|
+
proc.stdout.on('data', (data) => {
|
|
105
|
+
stdout += data.toString();
|
|
106
|
+
process.stdout.write(data); // 实时转发到终端
|
|
107
|
+
});
|
|
199
108
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
109
|
+
proc.stderr.on('data', (data) => {
|
|
110
|
+
stderr += data.toString();
|
|
111
|
+
process.stderr.write(data);
|
|
112
|
+
});
|
|
203
113
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
114
|
+
proc.on('close', (code) => {
|
|
115
|
+
if (code !== 0) {
|
|
116
|
+
reject(new Error(`文件托管失败: ${stderr || stdout}`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result = parseAiResult(stdout);
|
|
121
|
+
|
|
122
|
+
if (!result.public_url || !result.resource_id) {
|
|
123
|
+
reject(new Error(`无法从输出中解析结果:\n${stdout}`));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const isUpdate = Boolean(options.resourceId);
|
|
128
|
+
const resourceId = String(result.resource_id);
|
|
129
|
+
const publicUrl = result.public_url;
|
|
130
|
+
|
|
131
|
+
// 保存资源状态
|
|
132
|
+
resourceStore.saveResource({
|
|
133
|
+
id: `host-${resourceId}`,
|
|
134
|
+
type: 'host',
|
|
135
|
+
filePath: resolvedPath,
|
|
136
|
+
publicUrl,
|
|
137
|
+
resourceId,
|
|
138
|
+
fileName: path.basename(resolvedPath),
|
|
139
|
+
status: 'active',
|
|
140
|
+
createdAt: new Date().toISOString(),
|
|
141
|
+
lastCheckedAt: new Date().toISOString(),
|
|
209
142
|
});
|
|
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
143
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
uploadResponse: uploadResult,
|
|
230
|
-
error: uploadError,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
144
|
+
resolve({
|
|
145
|
+
success: true,
|
|
146
|
+
mode: isUpdate ? 'update' : 'create',
|
|
147
|
+
publicUrl,
|
|
148
|
+
verifyCode: result.verify_code || null,
|
|
149
|
+
resourceId,
|
|
150
|
+
createResult: result.create_result || null,
|
|
151
|
+
stdout: stdout.trim(),
|
|
152
|
+
});
|
|
153
|
+
});
|
|
233
154
|
|
|
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();
|
|
155
|
+
proc.on('error', (err) => {
|
|
156
|
+
reject(new Error(`无法启动文件托管: ${err.message}`));
|
|
157
|
+
});
|
|
245
158
|
});
|
|
246
159
|
}
|
|
247
160
|
|
|
248
161
|
module.exports = {
|
|
249
|
-
TICKET_API,
|
|
250
|
-
getTicketApiUrl,
|
|
251
|
-
getUploadBaseUrl,
|
|
252
|
-
sha256File,
|
|
253
|
-
getFileHostingTicket,
|
|
254
|
-
uploadFile,
|
|
255
162
|
host,
|
|
256
163
|
openClaimUrl,
|
|
164
|
+
parseAiResult,
|
|
257
165
|
};
|
package/lib/tunnel.js
CHANGED
|
@@ -41,7 +41,7 @@ async function start(options) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const binPath = await download.ensureBinary(arch);
|
|
44
|
-
const args = ['-ip', ip, '-port', String(portNum)];
|
|
44
|
+
const args = ['tunnel', '-ip', ip, '-port', String(portNum)];
|
|
45
45
|
|
|
46
46
|
if (detached) {
|
|
47
47
|
const logFile = pidManager.getLogFilePath('tunnel', { ip, port: portNum });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aweray/hsk-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
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.4.2",
|
|
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.4.2.exe"
|
|
7
7
|
},
|
|
8
8
|
"darwin-amd64": {
|
|
9
|
-
"filename": "hsk-cli-darwin-amd64-v0.
|
|
9
|
+
"filename": "hsk-cli-darwin-amd64-v0.4.2"
|
|
10
10
|
},
|
|
11
11
|
"darwin-arm64": {
|
|
12
|
-
"filename": "hsk-cli-darwin-arm64-v0.
|
|
12
|
+
"filename": "hsk-cli-darwin-arm64-v0.4.2"
|
|
13
13
|
},
|
|
14
14
|
"linux-amd64": {
|
|
15
|
-
"filename": "hsk-cli-linux-amd64-v0.
|
|
15
|
+
"filename": "hsk-cli-linux-amd64-v0.4.2"
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|