@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 CHANGED
@@ -10,8 +10,9 @@
10
10
 
11
11
  - **多平台自动适配** — 自动检测操作系统和架构,下载对应原生客户端二进制
12
12
  - **内网穿透** — 一键将本地服务暴露到公网,支持前台/后台两种保活模式
13
- - **文件托管** — 上传本地文件或构建产物,获取公网下载链接
14
- - **构建部署** — `deploy` 命令一键完成 build → pack → upload
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` → 打包 `dist/` → 上传到文件托管。
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 <filePath> [options]
138
+ hsk-cli host <fileOrDirPath> [options]
133
139
  ```
134
140
 
141
+ 支持上传单文件或目录。传入目录时,原生二进制自动打包为 zip 后上传。
142
+
135
143
  | Option | Required | Description |
136
144
  |--------|----------|-------------|
137
- | `<filePath>` | 是 | 本地文件路径 |
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
- | `--pack-output <file>` | 打包文件名(默认: `dist.zip`) |
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` → `pack` → `upload` 三步。打包使用系统原生 `zip`(macOS/Linux)或 PowerShell `Compress-Archive`(Windows),无需额外依赖.
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](c) for guidelines.
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 uploadBase = fileHosting.getUploadBaseUrl();
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
- `POST ${fileHosting.TICKET_API}`,
196
+ cmdParts.join(' '),
193
197
  {
194
198
  文件路径: filePath,
195
199
  文件大小: stats ? `${(stats.size / 1024 / 1024).toFixed(2)} MB` : '文件不存在',
196
200
  资源ID: options.resourceId || '(新建)',
197
- 预期操作: '计算 SHA-256 → 获取 ticket → 上传/更新文件',
198
- 上传服务: options.resourceId ? `PUT ${uploadBase}/resource/update` : `POST ${uploadBase}/upload`,
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
- `deploy: ${buildCmd} → pack ${buildDir} → upload ${packOutput}`,
325
+ `${buildCmd} → ${cmdParts.join(' ')}`,
320
326
  {
321
327
  构建命令: buildCmd,
322
328
  构建目录: buildDir,
323
- 打包文件: packOutput,
324
- 上传后清理: cleanAfter,
325
- 资源ID: options.resourceId || '(新建)'
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(packResult.path, { resourceId: options.resourceId });
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
 
@@ -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 version = require('./version');
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
- * 检查文件托管 API 配置
8
+ * 解析原生二进制输出,提取 [AI_RESULT] 键值对
45
9
  */
46
- function checkApiConfig() {
47
- // 可通过环境变量覆盖 ticket / upload 服务地址,见 getTicketApiUrl / getUploadBaseUrl
48
- }
49
-
50
- /**
51
- * 计算文件 SHA-256 hash
52
- */
53
- function sha256File(filePath) {
54
- return new Promise((resolve, reject) => {
55
- const hash = crypto.createHash('sha256');
56
- const stream = fs.createReadStream(filePath);
57
- stream.on('data', (chunk) => hash.update(chunk));
58
- stream.on('end', () => resolve(hash.digest('hex')));
59
- stream.on('error', reject);
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
- async function uploadFile(token, filePath, options = {}) {
100
- const { update = false, uploadUrl } = options;
101
- const fileName = path.basename(filePath);
102
- const form = new FormData();
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,
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
- * @param {string} filePath
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
- checkApiConfig();
151
-
152
- if (!fs.existsSync(filePath)) {
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
- const fileName = path.basename(filePath);
166
- const resourceIdInput = options.resourceId || null;
167
- const isUpdate = Boolean(resourceIdInput);
52
+ // 确保原生二进制存在
53
+ const binPath = await download.ensureBinary();
168
54
 
169
- if (process.env.HSK_FILE_HOSTING_DIRECT) {
170
- const form = new FormData();
171
- form.append('file', fs.createReadStream(filePath), { filename: fileName });
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
- const res = await fetch(TICKET_API, {
174
- method: 'POST',
175
- headers: { ...form.getHeaders() },
176
- body: form,
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
- if (!res.ok) {
180
- throw new Error(`文件上传失败: HTTP ${res.status} - ${res.statusText}`);
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
- const fileHash = await sha256File(filePath);
198
- const ticket = await getFileHostingTicket(fileHash, resourceIdInput);
75
+ proc.stdout.on('data', (data) => {
76
+ stdout += data.toString();
77
+ process.stdout.write(data); // 实时转发到终端
78
+ });
199
79
 
200
- const useLegacyUpload = Boolean(ticket.server_address);
201
- let uploadResult;
202
- let uploadError;
80
+ proc.stderr.on('data', (data) => {
81
+ stderr += data.toString();
82
+ process.stderr.write(data);
83
+ });
203
84
 
204
- try {
205
- if (useLegacyUpload) {
206
- uploadResult = await uploadFile(ticket.ticket, filePath, {
207
- uploadUrl: ticket.server_address,
208
- update: isUpdate,
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
- return {
220
- success: true,
221
- mode: isUpdate ? 'update' : 'create',
222
- publicUrl: ticket.public_url,
223
- verifyCode: ticket.verify_code,
224
- resourceId,
225
- uploadHost: useLegacyUpload ? ticket.server_address : getUploadBaseUrl(),
226
- fileName,
227
- fileHash,
228
- filePath,
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.2.2",
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.2",
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.2.2.exe"
6
+ "filename": "hsk-cli-windows-amd64-v0.3.0.exe"
7
7
  },
8
8
  "darwin-amd64": {
9
- "filename": "hsk-cli-darwin-amd64-v0.2.2"
9
+ "filename": "hsk-cli-darwin-amd64-v0.3.0"
10
10
  },
11
11
  "darwin-arm64": {
12
- "filename": "hsk-cli-darwin-arm64-v0.2.2"
12
+ "filename": "hsk-cli-darwin-arm64-v0.3.0"
13
13
  },
14
14
  "linux-amd64": {
15
- "filename": "hsk-cli-linux-amd64-v0.2.2"
15
+ "filename": "hsk-cli-linux-amd64-v0.3.0"
16
16
  }
17
17
  }
18
18
  }