@onebullex-group/cli 1.3.1 → 1.5.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/CHANGELOG.md +123 -61
- package/README.md +304 -243
- package/dist/chunk-6DC55RF7.js +16 -0
- package/dist/{chunk-PEVZEMJG.js → chunk-NMW73ILO.js} +49 -1
- package/dist/chunk-SGLPG2FL.js +39 -0
- package/dist/{chunk-NKWXNVLA.js → chunk-YE5LER6U.js} +86 -19
- package/dist/core/bill-no.d.ts +19 -0
- package/dist/core/bill-no.js +9 -0
- package/dist/core/download.d.ts +41 -0
- package/dist/core/download.js +15 -0
- package/dist/core/http-client.d.ts +32 -1
- package/dist/core/http-client.js +9 -5
- package/dist/index.js +217 -83
- package/dist/lint-api.d.ts +1 -1
- package/dist/lint-api.js +27 -6
- package/dist/registry.js +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/core/download.ts
|
|
4
|
+
import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { dirname, join, resolve } from "path";
|
|
7
|
+
var DOWNLOAD_PATH = "/v2/system/download";
|
|
8
|
+
var DEFAULT_DOWNLOAD_DIR = join(homedir(), ".onebullex", "downloads");
|
|
9
|
+
function defaultFilename(downloadId) {
|
|
10
|
+
return `position_history_${downloadId.replace(/[^\w.-]/g, "_")}.xlsx`;
|
|
11
|
+
}
|
|
12
|
+
function resolveTarget(out, filename, cwd = process.cwd()) {
|
|
13
|
+
if (!out) return join(DEFAULT_DOWNLOAD_DIR, filename);
|
|
14
|
+
const abs = resolve(cwd, out);
|
|
15
|
+
const looksLikeDir = /[\\/]$/.test(out) || existsSync(abs) && statSync(abs).isDirectory();
|
|
16
|
+
return looksLikeDir ? join(abs, filename) : abs;
|
|
17
|
+
}
|
|
18
|
+
async function downloadToFile(opts) {
|
|
19
|
+
const downloadId = opts.downloadId.trim();
|
|
20
|
+
if (!downloadId) throw new Error("downloadId is required");
|
|
21
|
+
const file = await opts.client.getPrivateBinary(DOWNLOAD_PATH, { downloadId });
|
|
22
|
+
const target = resolveTarget(opts.out, file.filename ?? defaultFilename(downloadId), opts.cwd);
|
|
23
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
24
|
+
writeFileSync(target, file.bytes);
|
|
25
|
+
return {
|
|
26
|
+
downloadId,
|
|
27
|
+
path: target,
|
|
28
|
+
bytes: file.bytes.length,
|
|
29
|
+
contentType: file.contentType
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
DOWNLOAD_PATH,
|
|
35
|
+
DEFAULT_DOWNLOAD_DIR,
|
|
36
|
+
defaultFilename,
|
|
37
|
+
resolveTarget,
|
|
38
|
+
downloadToFile
|
|
39
|
+
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
generateNonce,
|
|
4
|
+
sign
|
|
5
|
+
} from "./chunk-LVNSFQXS.js";
|
|
5
6
|
import {
|
|
6
7
|
redactObject
|
|
7
8
|
} from "./chunk-OOLZIYXS.js";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./chunk-LVNSFQXS.js";
|
|
10
|
+
OneBullExError
|
|
11
|
+
} from "./chunk-JRUFIHU7.js";
|
|
12
12
|
|
|
13
13
|
// src/core/http-client.ts
|
|
14
14
|
function flattenForSign(body) {
|
|
@@ -46,6 +46,37 @@ var HttpClient = class {
|
|
|
46
46
|
async postPrivate(path, body) {
|
|
47
47
|
return this.send("POST", path, void 0, body, true, false);
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* 签名 GET,返回原始字节(用于 /v2/system/download 这类文件流接口)。
|
|
51
|
+
*
|
|
52
|
+
* 业务错误(文件未就绪、次数超限等)仍以 JSON 返回,此时按普通响应校验并
|
|
53
|
+
* 抛 OneBullExError;对齐 Java 端 BaseClientV2.executeWithRetryBytes。
|
|
54
|
+
*/
|
|
55
|
+
async getPrivateBinary(path, query) {
|
|
56
|
+
return this.withRetry(async () => {
|
|
57
|
+
const res = await this.request("GET", path, query, void 0, true);
|
|
58
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
59
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
60
|
+
if (isJsonBody(contentType, bytes)) {
|
|
61
|
+
const json = JSON.parse(new TextDecoder().decode(bytes));
|
|
62
|
+
this.unwrap(json, res.status, path);
|
|
63
|
+
throw new OneBullExError(
|
|
64
|
+
"Expected a file stream but the server returned a JSON success body",
|
|
65
|
+
"UNEXPECTED_JSON",
|
|
66
|
+
res.status,
|
|
67
|
+
path
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (this.cfg.debug) {
|
|
71
|
+
console.error("[debug response]", `${bytes.length} bytes (${contentType})`);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
bytes,
|
|
75
|
+
contentType,
|
|
76
|
+
filename: parseFilename(res.headers.get("content-disposition"))
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
49
80
|
/**
|
|
50
81
|
* GET 请求重试包装:网络错误最多重试 2 次(共 3 次尝试),延迟 300ms / 900ms。
|
|
51
82
|
* OneBullExError(业务错误)不重试。
|
|
@@ -66,12 +97,34 @@ var HttpClient = class {
|
|
|
66
97
|
throw lastErr;
|
|
67
98
|
}
|
|
68
99
|
async send(method, path, query, body, requireAuth, raw) {
|
|
100
|
+
const res = await this.request(method, path, query, body, requireAuth);
|
|
101
|
+
const json = await res.json();
|
|
102
|
+
if (this.cfg.debug) {
|
|
103
|
+
console.error("[debug response]", JSON.stringify(redactObject(json)));
|
|
104
|
+
}
|
|
105
|
+
if (raw) {
|
|
106
|
+
return json;
|
|
107
|
+
}
|
|
108
|
+
return this.unwrap(json, res.status, path);
|
|
109
|
+
}
|
|
110
|
+
/** Normalize: V2 API uses "code"/"msg"; some endpoints use "returnCode"/"msgInfo" */
|
|
111
|
+
unwrap(json, status, path) {
|
|
112
|
+
const returnCode = json.code ?? json.returnCode;
|
|
113
|
+
const msgInfo = json.msg ?? json.msgInfo ?? "";
|
|
114
|
+
if (returnCode !== 0 && returnCode !== "0") {
|
|
115
|
+
throw new OneBullExError(msgInfo, returnCode ?? "UNKNOWN", status, path);
|
|
116
|
+
}
|
|
117
|
+
return json.data;
|
|
118
|
+
}
|
|
119
|
+
/** 签名 + 发送,返回 HTTP 层成功(2xx)的 Response,由调用方决定如何解析 body。 */
|
|
120
|
+
async request(method, path, query, body, requireAuth) {
|
|
69
121
|
const headers = {
|
|
70
122
|
"Content-Type": "application/json",
|
|
71
123
|
Accept: "application/json",
|
|
72
124
|
"User-Agent": "@onebullex-group/cli"
|
|
73
125
|
};
|
|
74
126
|
let url = `${this.cfg.baseUrl}${path}`;
|
|
127
|
+
if (method === "GET") query = normalizeQuery(query);
|
|
75
128
|
const params = method === "GET" ? { ...query ?? {} } : flattenForSign(body);
|
|
76
129
|
if (requireAuth) {
|
|
77
130
|
if (!this.cfg.apiKey || !this.cfg.secret) {
|
|
@@ -124,23 +177,37 @@ var HttpClient = class {
|
|
|
124
177
|
path
|
|
125
178
|
);
|
|
126
179
|
}
|
|
127
|
-
|
|
128
|
-
if (this.cfg.debug) {
|
|
129
|
-
console.error("[debug response]", JSON.stringify(redactObject(json)));
|
|
130
|
-
}
|
|
131
|
-
if (raw) {
|
|
132
|
-
return json;
|
|
133
|
-
}
|
|
134
|
-
const returnCode = json.code ?? json.returnCode;
|
|
135
|
-
const msgInfo = json.msg ?? json.msgInfo ?? "";
|
|
136
|
-
if (returnCode !== 0 && returnCode !== "0") {
|
|
137
|
-
throw new OneBullExError(msgInfo, returnCode ?? "UNKNOWN", res.status, path);
|
|
138
|
-
}
|
|
139
|
-
return json.data;
|
|
180
|
+
return res;
|
|
140
181
|
}
|
|
141
182
|
};
|
|
183
|
+
function normalizeQuery(query) {
|
|
184
|
+
if (!query || typeof query.direction !== "string") return query;
|
|
185
|
+
return { ...query, direction: query.direction.toUpperCase() };
|
|
186
|
+
}
|
|
187
|
+
function isJsonBody(contentType, bytes) {
|
|
188
|
+
if (contentType.toLowerCase().includes("json")) return true;
|
|
189
|
+
let i = 0;
|
|
190
|
+
while (i < bytes.length && /\s/.test(String.fromCharCode(bytes[i] ?? 0))) i++;
|
|
191
|
+
return i < bytes.length && bytes[i] === 123;
|
|
192
|
+
}
|
|
193
|
+
function parseFilename(disposition) {
|
|
194
|
+
if (!disposition) return void 0;
|
|
195
|
+
const star = /filename\*\s*=\s*(?:[\w-]+'[^']*')?([^;]+)/i.exec(disposition);
|
|
196
|
+
const plain = /filename\s*=\s*"?([^";]+)"?/i.exec(disposition);
|
|
197
|
+
let name = star?.[1] ?? plain?.[1];
|
|
198
|
+
if (!name) return void 0;
|
|
199
|
+
try {
|
|
200
|
+
name = decodeURIComponent(name.trim());
|
|
201
|
+
} catch {
|
|
202
|
+
name = name.trim();
|
|
203
|
+
}
|
|
204
|
+
const base = name.split(/[\\/]/).pop()?.trim();
|
|
205
|
+
return base && base !== "." && base !== ".." ? base : void 0;
|
|
206
|
+
}
|
|
142
207
|
|
|
143
208
|
export {
|
|
144
209
|
flattenForSign,
|
|
145
|
-
HttpClient
|
|
210
|
+
HttpClient,
|
|
211
|
+
normalizeQuery,
|
|
212
|
+
parseFilename
|
|
146
213
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 划转幂等单号(billNo)生成与解析。
|
|
3
|
+
*
|
|
4
|
+
* `/v2/balance/transfer` 要求调用方提供唯一的 billNo 作为幂等键。文档标注必填,
|
|
5
|
+
* 但为了 CLI / MCP 两条路的易用性,我们允许省略并在此补一个 UUID v4。
|
|
6
|
+
*
|
|
7
|
+
* 幂等语义提醒:自动生成只在"首次发起"时安全。重试同一笔划转必须复用上一次的
|
|
8
|
+
* billNo,否则服务端会当成两笔新划转。dry-run 预览里展示的就是最终会发出去的
|
|
9
|
+
* billNo,confirm 时应原样带回。
|
|
10
|
+
*/
|
|
11
|
+
/** 生成一个 `obe-` 前缀的 UUID v4,便于在对账时区分来源。 */
|
|
12
|
+
declare function generateBillNo(): string;
|
|
13
|
+
/**
|
|
14
|
+
* 解析调用方传入的 billNo:有值则去掉首尾空白后原样返回,
|
|
15
|
+
* 空/仅空白/未传则生成一个新的。
|
|
16
|
+
*/
|
|
17
|
+
declare function resolveBillNo(supplied: string | undefined | null): string;
|
|
18
|
+
|
|
19
|
+
export { generateBillNo, resolveBillNo };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HttpClient } from './http-client.js';
|
|
2
|
+
import './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 异步导出文件下载(`GET /v2/system/download`)。
|
|
6
|
+
*
|
|
7
|
+
* 接口成功时返回文件流(Excel),未就绪/失败/超限时返回 JSON 错误。CLI 命令与
|
|
8
|
+
* MCP 工具共用这里的落盘逻辑:MCP 不能把二进制塞进 JSON 文本结果,只能写到本地
|
|
9
|
+
* 再把路径返回给 AI。
|
|
10
|
+
*
|
|
11
|
+
* 限制提醒:同一 downloadId 一小时内最多成功下载 5 次 —— 每次调用都会消耗额度,
|
|
12
|
+
* 不要用它来"轮询"任务是否完成。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
declare const DOWNLOAD_PATH = "/v2/system/download";
|
|
16
|
+
/** `--out` 省略时的落盘目录 */
|
|
17
|
+
declare const DEFAULT_DOWNLOAD_DIR: string;
|
|
18
|
+
interface DownloadResult {
|
|
19
|
+
downloadId: string;
|
|
20
|
+
/** 实际写入的绝对路径 */
|
|
21
|
+
path: string;
|
|
22
|
+
bytes: number;
|
|
23
|
+
contentType: string;
|
|
24
|
+
}
|
|
25
|
+
/** 服务端未给出 Content-Disposition 时的文件名,与 Java 端 downloadToFile 一致 */
|
|
26
|
+
declare function defaultFilename(downloadId: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* 解析最终落盘路径:
|
|
29
|
+
* - 未给 out → DEFAULT_DOWNLOAD_DIR/<filename>
|
|
30
|
+
* - out 是已存在的目录,或以 / \ 结尾 → out/<filename>
|
|
31
|
+
* - 否则 out 视为文件路径
|
|
32
|
+
*/
|
|
33
|
+
declare function resolveTarget(out: string | undefined, filename: string, cwd?: string): string;
|
|
34
|
+
declare function downloadToFile(opts: {
|
|
35
|
+
client: HttpClient;
|
|
36
|
+
downloadId: string;
|
|
37
|
+
out?: string;
|
|
38
|
+
cwd?: string;
|
|
39
|
+
}): Promise<DownloadResult>;
|
|
40
|
+
|
|
41
|
+
export { DEFAULT_DOWNLOAD_DIR, DOWNLOAD_PATH, type DownloadResult, defaultFilename, downloadToFile, resolveTarget };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_DOWNLOAD_DIR,
|
|
4
|
+
DOWNLOAD_PATH,
|
|
5
|
+
defaultFilename,
|
|
6
|
+
downloadToFile,
|
|
7
|
+
resolveTarget
|
|
8
|
+
} from "../chunk-SGLPG2FL.js";
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_DOWNLOAD_DIR,
|
|
11
|
+
DOWNLOAD_PATH,
|
|
12
|
+
defaultFilename,
|
|
13
|
+
downloadToFile,
|
|
14
|
+
resolveTarget
|
|
15
|
+
};
|
|
@@ -20,12 +20,43 @@ declare class HttpClient {
|
|
|
20
20
|
raw?: boolean;
|
|
21
21
|
}): Promise<T>;
|
|
22
22
|
postPrivate<T>(path: string, body?: Record<string, unknown>): Promise<T>;
|
|
23
|
+
/**
|
|
24
|
+
* 签名 GET,返回原始字节(用于 /v2/system/download 这类文件流接口)。
|
|
25
|
+
*
|
|
26
|
+
* 业务错误(文件未就绪、次数超限等)仍以 JSON 返回,此时按普通响应校验并
|
|
27
|
+
* 抛 OneBullExError;对齐 Java 端 BaseClientV2.executeWithRetryBytes。
|
|
28
|
+
*/
|
|
29
|
+
getPrivateBinary(path: string, query?: Query): Promise<BinaryResponse>;
|
|
23
30
|
/**
|
|
24
31
|
* GET 请求重试包装:网络错误最多重试 2 次(共 3 次尝试),延迟 300ms / 900ms。
|
|
25
32
|
* OneBullExError(业务错误)不重试。
|
|
26
33
|
*/
|
|
27
34
|
private withRetry;
|
|
28
35
|
private send;
|
|
36
|
+
/** Normalize: V2 API uses "code"/"msg"; some endpoints use "returnCode"/"msgInfo" */
|
|
37
|
+
private unwrap;
|
|
38
|
+
/** 签名 + 发送,返回 HTTP 层成功(2xx)的 Response,由调用方决定如何解析 body。 */
|
|
39
|
+
private request;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* GET query 归一化,在签名之前执行(签名与 URL 用同一份值)。
|
|
43
|
+
*
|
|
44
|
+
* `direction` 在所有 V2 GET 接口里都是大写枚举(游标 `NEXT`/`PREV`,
|
|
45
|
+
* listUnfinished 的 `BUY`/`SELL`),服务端区分大小写,`next` 会返回
|
|
46
|
+
* `invalid_direction`。放在这里而不是各命令里,是因为 MCP 路径不经过
|
|
47
|
+
* commander action。
|
|
48
|
+
*/
|
|
49
|
+
declare function normalizeQuery(query: Query | undefined): Query | undefined;
|
|
50
|
+
interface BinaryResponse {
|
|
51
|
+
bytes: Uint8Array;
|
|
52
|
+
contentType: string;
|
|
53
|
+
/** 从 Content-Disposition 解析出的文件名;服务端未给出时为 undefined */
|
|
54
|
+
filename?: string;
|
|
29
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* 解析 `attachment; filename="x.xlsx"` / `filename*=UTF-8''x.xlsx`。
|
|
58
|
+
* 只取 basename,防止服务端给出 `../` 之类的路径。
|
|
59
|
+
*/
|
|
60
|
+
declare function parseFilename(disposition: string | null): string | undefined;
|
|
30
61
|
|
|
31
|
-
export { type FetchFn, HttpClient, type Query, flattenForSign };
|
|
62
|
+
export { type BinaryResponse, type FetchFn, HttpClient, type Query, flattenForSign, normalizeQuery, parseFilename };
|
package/dist/core/http-client.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
HttpClient,
|
|
4
|
-
flattenForSign
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
flattenForSign,
|
|
5
|
+
normalizeQuery,
|
|
6
|
+
parseFilename
|
|
7
|
+
} from "../chunk-YE5LER6U.js";
|
|
8
|
+
import "../chunk-LVNSFQXS.js";
|
|
7
9
|
import "../chunk-OOLZIYXS.js";
|
|
8
10
|
import "../chunk-ZYO5P4DH.js";
|
|
9
|
-
import "../chunk-
|
|
11
|
+
import "../chunk-JRUFIHU7.js";
|
|
10
12
|
export {
|
|
11
13
|
HttpClient,
|
|
12
|
-
flattenForSign
|
|
14
|
+
flattenForSign,
|
|
15
|
+
normalizeQuery,
|
|
16
|
+
parseFilename
|
|
13
17
|
};
|