gangtise-openapi-cli 0.43.1 → 0.44.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 +86 -15
- package/dist/src/cli.js +29 -18
- package/dist/src/commands/ai.js +20 -29
- package/dist/src/commands/auth.js +5 -4
- package/dist/src/commands/bond.js +5 -4
- package/dist/src/commands/fund.js +100 -0
- package/dist/src/commands/fundamental.js +2 -0
- package/dist/src/commands/groups.js +14 -0
- package/dist/src/commands/indicator.js +5 -5
- package/dist/src/commands/insight.js +3 -4
- package/dist/src/commands/raw.js +4 -3
- package/dist/src/commands/shared.js +35 -6
- package/dist/src/commands/tool.js +2 -2
- package/dist/src/commands/vault.js +30 -30
- package/dist/src/core/args.js +23 -7
- package/dist/src/core/asyncContent.js +1 -1
- package/dist/src/core/auth.js +2 -10
- package/dist/src/core/client.js +67 -19
- package/dist/src/core/commandBodies.js +1 -27
- package/dist/src/core/config.js +9 -5
- package/dist/src/core/download.js +47 -10
- package/dist/src/core/endpoints.js +195 -0
- package/dist/src/core/errors.js +1 -1
- package/dist/src/core/output.js +103 -72
- package/dist/src/core/printer.js +10 -5
- package/dist/src/core/quoteSharding.js +28 -4
- package/dist/src/core/rowSink.js +24 -10
- package/dist/src/core/titleCache.js +2 -10
- package/dist/src/core/transport.js +25 -7
- package/dist/src/core/updateCheck.js +3 -9
- package/gangtise-openapi/SKILL.md +52 -16
- package/gangtise-openapi/references/commands/fund.md +192 -0
- package/gangtise-openapi/references/commands/fundamental.md +2 -2
- package/gangtise-openapi/references/commands/insight.md +2 -2
- package/gangtise-openapi/references/commands/quote.md +1 -1
- package/gangtise-openapi/references/commands/tool.md +1 -1
- package/gangtise-openapi/references/commands/vault.md +1 -1
- package/gangtise-openapi/references/errors.md +4 -4
- package/gangtise-openapi/references/response-schema.md +10 -0
- package/package.json +1 -1
package/dist/src/core/client.js
CHANGED
|
@@ -6,10 +6,10 @@ import { pipeline } from "node:stream/promises";
|
|
|
6
6
|
import { FormData, request } from "undici";
|
|
7
7
|
import { credentialFingerprint, isTokenCacheValid, normalizeToken, readTokenCache, requireAccessCredentials, writeTokenCache } from "./auth.js";
|
|
8
8
|
import { ApiError, attachEnvelopeTraceId, markStructural, ValidationError } from "./errors.js";
|
|
9
|
-
import { ENDPOINTS, resolveTimeoutMs } from "./endpoints.js";
|
|
9
|
+
import { COSTLY_FETCH_CREDITS, ENDPOINTS, resolveTimeoutMs } from "./endpoints.js";
|
|
10
10
|
import { getLookupData } from "./lookupData/index.js";
|
|
11
11
|
import { stagingPath } from "./output.js";
|
|
12
|
-
import { decodeResponseBody, getDispatcher, isVerbose, logTiming,
|
|
12
|
+
import { decodeResponseBody, getDispatcher, isVerbose, JSON_TOTAL_FACTOR, logTiming, markAuthReplay, PAGE_CONCURRENCY, parseRetryAfterMs, quoteBigIntFields, runInOrder, TRANSFER_TOTAL_FACTOR, withRetry } from "./transport.js";
|
|
13
13
|
import { attachRowSink } from "./rowSink.js";
|
|
14
14
|
import { markIncomplete } from "./exitStatus.js";
|
|
15
15
|
const AUTH_RETRY_CODES = new Set(["8000014", "8000015", "0000001008", "999002"]);
|
|
@@ -18,7 +18,6 @@ function hasExpectedShape(expects, payload) {
|
|
|
18
18
|
return Array.isArray(payload);
|
|
19
19
|
return Boolean(payload && typeof payload === "object" && Array.isArray(payload.list));
|
|
20
20
|
}
|
|
21
|
-
export const COSTLY_FETCH_CREDITS = 1000;
|
|
22
21
|
const PROBE_ABOVE_CREDITS = 50;
|
|
23
22
|
const OFFSET_REFUSAL_CODES = new Set(["140002", "100006"]);
|
|
24
23
|
function readEmptyListAsArray(page) {
|
|
@@ -46,6 +45,7 @@ export class GangtiseClient {
|
|
|
46
45
|
allowCostlyFetch = false;
|
|
47
46
|
refreshPromise = null;
|
|
48
47
|
memoCache = null;
|
|
48
|
+
loginToken = null;
|
|
49
49
|
envTokenInvalidated = false;
|
|
50
50
|
rowSinkClaimed = false;
|
|
51
51
|
constructor(config, rowSink) {
|
|
@@ -98,6 +98,7 @@ export class GangtiseClient {
|
|
|
98
98
|
issuedFor: credentialFingerprint(credentials.accessKey, this.config.baseUrl),
|
|
99
99
|
};
|
|
100
100
|
this.memoCache = cache;
|
|
101
|
+
this.loginToken = accessToken;
|
|
101
102
|
try {
|
|
102
103
|
await writeTokenCache(this.config.tokenCachePath, cache);
|
|
103
104
|
}
|
|
@@ -108,23 +109,45 @@ export class GangtiseClient {
|
|
|
108
109
|
return accessToken;
|
|
109
110
|
}
|
|
110
111
|
async refreshAuthIfRecoverable(error, useAuth, authState, usedAuthorization) {
|
|
111
|
-
if (useAuth
|
|
112
|
-
&& !authState.retried
|
|
112
|
+
if (!(useAuth
|
|
113
113
|
&& error instanceof ApiError
|
|
114
114
|
&& error.code
|
|
115
115
|
&& AUTH_RETRY_CODES.has(error.code)
|
|
116
116
|
&& this.config.accessKey
|
|
117
|
-
&& this.config.secretKey)
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
&& this.config.secretKey))
|
|
118
|
+
return;
|
|
119
|
+
if (authState.usedLoginToken)
|
|
120
|
+
return;
|
|
121
|
+
this.envTokenInvalidated = true;
|
|
122
|
+
const replay = () => markAuthReplay(new ApiError(error.message, error.code, error.statusCode, error.details));
|
|
123
|
+
const tryMemory = () => {
|
|
120
124
|
const memoToken = this.memoCache && isTokenCacheValid(this.memoCache) ? normalizeToken(this.memoCache.accessToken) : null;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
if (memoToken === null || usedAuthorization === undefined || memoToken === usedAuthorization)
|
|
126
|
+
return;
|
|
127
|
+
if (memoToken === this.loginToken) {
|
|
128
|
+
authState.usedLoginToken = true;
|
|
129
|
+
throw replay();
|
|
130
|
+
}
|
|
131
|
+
if (!authState.usedUnprovenToken) {
|
|
132
|
+
authState.usedUnprovenToken = true;
|
|
133
|
+
throw replay();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
tryMemory();
|
|
137
|
+
if (!authState.usedUnprovenToken) {
|
|
138
|
+
const expected = credentialFingerprint(this.config.accessKey, this.config.baseUrl);
|
|
139
|
+
const disk = await readTokenCache(this.config.tokenCachePath);
|
|
140
|
+
tryMemory();
|
|
141
|
+
if (disk && isTokenCacheValid(disk, undefined, expected) && normalizeToken(disk.accessToken) !== usedAuthorization) {
|
|
142
|
+
this.memoCache = disk;
|
|
143
|
+
authState.usedUnprovenToken = true;
|
|
144
|
+
throw replay();
|
|
125
145
|
}
|
|
126
|
-
throw markRetryable(new ApiError(error.message, error.code, error.statusCode, error.details));
|
|
127
146
|
}
|
|
147
|
+
this.memoCache = null;
|
|
148
|
+
await this.getAuthorizationHeader(true);
|
|
149
|
+
authState.usedLoginToken = true;
|
|
150
|
+
throw replay();
|
|
128
151
|
}
|
|
129
152
|
buildUrl(path) {
|
|
130
153
|
const base = this.config.baseUrl.endsWith("/") ? this.config.baseUrl : `${this.config.baseUrl}/`;
|
|
@@ -193,15 +216,18 @@ export class GangtiseClient {
|
|
|
193
216
|
}
|
|
194
217
|
const windowRoom = maxWindow === undefined ? Infinity : maxWindow - startFrom;
|
|
195
218
|
const billing = endpoint.billing;
|
|
196
|
-
const guarded =
|
|
219
|
+
const guarded = billing?.per === "row" && !this.allowCostlyFetch
|
|
220
|
+
&& (requestedSize === undefined || requestedSize * billing.price > COSTLY_FETCH_CREDITS);
|
|
197
221
|
const refuseIfCostly = (reportedTotal, fetched) => {
|
|
198
222
|
if (!guarded || !billing)
|
|
199
223
|
return;
|
|
200
|
-
const rows = Math.min(Math.max(reportedTotal - startFrom, 0), windowRoom);
|
|
224
|
+
const rows = Math.min(Math.max(reportedTotal - startFrom, 0), windowRoom, requestedSize ?? Infinity);
|
|
201
225
|
const estimatedCredits = Math.round(rows * billing.price * 100) / 100;
|
|
202
226
|
if (estimatedCredits <= COSTLY_FETCH_CREDITS)
|
|
203
227
|
return;
|
|
204
|
-
|
|
228
|
+
const what = requestedSize === undefined ? `all ${rows} rows` : `${rows} rows (--size ${requestedSize})`;
|
|
229
|
+
const escape = requestedSize === undefined ? "Pass --size N for a bounded subset" : "Pass a smaller --size";
|
|
230
|
+
throw new ValidationError(`fetching ${what} of ${endpoint.key} would cost about ${estimatedCredits} credits (${billing.price} per row), above the ${COSTLY_FETCH_CREDITS}-credit guard. Only ${fetched} row(s) were fetched, to learn the total. ${escape}, or --yes to fetch them all.`);
|
|
205
231
|
};
|
|
206
232
|
let probe;
|
|
207
233
|
let probed = false;
|
|
@@ -215,7 +241,7 @@ export class GangtiseClient {
|
|
|
215
241
|
else {
|
|
216
242
|
refuseIfCostly(probe.total, probe.list.length);
|
|
217
243
|
probeRows = probe.list.length;
|
|
218
|
-
probed = probe.list.length === Math.min(Math.max(probe.total - startFrom, 0), windowRoom);
|
|
244
|
+
probed = probe.list.length === Math.min(Math.max(probe.total - startFrom, 0), windowRoom, requestedSize ?? Infinity);
|
|
219
245
|
}
|
|
220
246
|
}
|
|
221
247
|
const firstPageSize = probed ? 1 : Math.min(requestedSize === undefined ? maxPageSize : Math.min(maxPageSize, requestedSize), windowRoom);
|
|
@@ -446,7 +472,7 @@ export class GangtiseClient {
|
|
|
446
472
|
}
|
|
447
473
|
const dispatcher = getDispatcher();
|
|
448
474
|
const url = this.buildUrl(endpoint.path);
|
|
449
|
-
const authState = {
|
|
475
|
+
const authState = {};
|
|
450
476
|
const timeoutMs = resolveTimeoutMs(this.config.timeoutMs, endpoint);
|
|
451
477
|
return withRetry(async () => {
|
|
452
478
|
const isUpload = endpoint.kind === 'upload';
|
|
@@ -467,6 +493,7 @@ export class GangtiseClient {
|
|
|
467
493
|
body: endpoint.method === 'GET' ? undefined : (isUpload ? body : JSON.stringify(body ?? {})),
|
|
468
494
|
headersTimeout: timeoutMs,
|
|
469
495
|
bodyTimeout: timeoutMs,
|
|
496
|
+
signal: AbortSignal.timeout(timeoutMs * (isUpload ? TRANSFER_TOTAL_FACTOR : JSON_TOTAL_FACTOR)),
|
|
470
497
|
dispatcher,
|
|
471
498
|
});
|
|
472
499
|
const encoding = response.headers['content-encoding'];
|
|
@@ -547,11 +574,12 @@ export class GangtiseClient {
|
|
|
547
574
|
Object.entries(query).forEach(([key, value]) => {
|
|
548
575
|
url.searchParams.set(key, String(value));
|
|
549
576
|
});
|
|
550
|
-
const authState = {
|
|
577
|
+
const authState = {};
|
|
551
578
|
const timeoutMs = resolveTimeoutMs(this.config.timeoutMs, endpoint);
|
|
552
579
|
return withRetry(async () => {
|
|
553
580
|
const authorization = await this.getAuthorizationHeader();
|
|
554
581
|
const startedAt = Date.now();
|
|
582
|
+
const signal = AbortSignal.timeout(timeoutMs * TRANSFER_TOTAL_FACTOR);
|
|
555
583
|
let currentUrl = url;
|
|
556
584
|
let auth = authorization;
|
|
557
585
|
const isPost = endpoint.method === 'POST';
|
|
@@ -563,6 +591,7 @@ export class GangtiseClient {
|
|
|
563
591
|
body: isPost ? JSON.stringify(body ?? {}) : undefined,
|
|
564
592
|
headersTimeout: timeoutMs,
|
|
565
593
|
bodyTimeout: timeoutMs,
|
|
594
|
+
signal,
|
|
566
595
|
dispatcher,
|
|
567
596
|
});
|
|
568
597
|
for (let hops = 0; hops < 3 && response.statusCode >= 300 && response.statusCode < 400; hops++) {
|
|
@@ -580,6 +609,7 @@ export class GangtiseClient {
|
|
|
580
609
|
headers: auth ? { Authorization: auth } : {},
|
|
581
610
|
headersTimeout: timeoutMs,
|
|
582
611
|
bodyTimeout: timeoutMs,
|
|
612
|
+
signal,
|
|
583
613
|
dispatcher,
|
|
584
614
|
});
|
|
585
615
|
}
|
|
@@ -675,6 +705,23 @@ export class GangtiseClient {
|
|
|
675
705
|
},
|
|
676
706
|
});
|
|
677
707
|
}
|
|
708
|
+
refuseIfEstimatedCostly(endpoint, body) {
|
|
709
|
+
const billing = endpoint.billing;
|
|
710
|
+
if (!endpoint.estimateBilledUnits || !billing || this.allowCostlyFetch)
|
|
711
|
+
return;
|
|
712
|
+
let units;
|
|
713
|
+
try {
|
|
714
|
+
units = endpoint.estimateBilledUnits(body && typeof body === "object" ? body : {});
|
|
715
|
+
}
|
|
716
|
+
catch (error) {
|
|
717
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
718
|
+
throw new ValidationError(`${endpoint.key} could not be priced before sending (${why}), and it is billed per ${billing.per} with no upper bound. Fix the request, or pass --yes to send it as it is.`);
|
|
719
|
+
}
|
|
720
|
+
const credits = Math.ceil(units * billing.price);
|
|
721
|
+
if (credits <= COSTLY_FETCH_CREDITS)
|
|
722
|
+
return;
|
|
723
|
+
throw new ValidationError(`${endpoint.key} would cost about ${credits} credits (${billing.price} per ${billing.per}, about ${units} estimated from the requested date range), above the ${COSTLY_FETCH_CREDITS}-credit guard. Narrow the date range, or pass --yes to send it.`);
|
|
724
|
+
}
|
|
678
725
|
async call(endpointKey, body, query, options) {
|
|
679
726
|
const endpoint = ENDPOINTS[endpointKey];
|
|
680
727
|
if (!endpoint) {
|
|
@@ -686,6 +733,7 @@ export class GangtiseClient {
|
|
|
686
733
|
if (endpoint.kind === 'download') {
|
|
687
734
|
return this.download(endpoint, query ?? {}, options, body);
|
|
688
735
|
}
|
|
736
|
+
this.refuseIfEstimatedCostly(endpoint, body);
|
|
689
737
|
if (endpoint.kind === 'json' && endpoint.pagination?.enabled) {
|
|
690
738
|
return this.requestPaginated(endpoint, body);
|
|
691
739
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { maybeArray,
|
|
1
|
+
import { maybeArray, parseIndicatorParams, parseOptionalNumberOption, parseScreenerIndicators } from "./args.js";
|
|
2
2
|
import { ValidationError } from "./errors.js";
|
|
3
3
|
import { QUOTE_MAX_LIMIT } from "./quoteSharding.js";
|
|
4
4
|
export function buildQuoteKlineBody(options) {
|
|
@@ -10,32 +10,6 @@ export function buildQuoteKlineBody(options) {
|
|
|
10
10
|
fieldList: maybeArray(options.field),
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
export function buildWechatMessageListBody(options) {
|
|
14
|
-
return {
|
|
15
|
-
from: parseFrom(options.from),
|
|
16
|
-
size: parseSize(options.size),
|
|
17
|
-
startTime: options.startTime,
|
|
18
|
-
endTime: options.endTime,
|
|
19
|
-
keyword: options.keyword,
|
|
20
|
-
securityList: maybeArray(options.security),
|
|
21
|
-
wechatGroupIdList: maybeArray(options.wechatGroupId),
|
|
22
|
-
industryIdList: maybeArray(options.industry),
|
|
23
|
-
categoryList: maybeArray(options.category),
|
|
24
|
-
tagList: maybeArray(options.tag),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
export function buildWechatChatroomListBody(options) {
|
|
28
|
-
return {
|
|
29
|
-
from: parseFrom(options.from),
|
|
30
|
-
size: parseSize(options.size),
|
|
31
|
-
roomName: options.roomName.length > 0 ? options.roomName.join(",") : undefined,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
export function buildStockPoolStocksBody(options) {
|
|
35
|
-
return {
|
|
36
|
-
poolIdList: options.poolId?.length ? options.poolId : ["all"],
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
13
|
const DATE_PARAM_KEYS = new Set(["tradeDate", "reportDate"]);
|
|
40
14
|
function assertParamCodesBound(groups, codes) {
|
|
41
15
|
if (!groups?.length)
|
package/dist/src/core/config.js
CHANGED
|
@@ -12,14 +12,18 @@ export function loadConfig() {
|
|
|
12
12
|
process.stderr.write(`[gangtise] warning: GANGTISE_TIMEOUT_MS=${rawTimeout} is not in effect, using ${timeoutMs} ms: expected whole milliseconds from ${MIN_TIMEOUT_MS} to ${MAX_TIMEOUT_MS}\n`);
|
|
13
13
|
}
|
|
14
14
|
return {
|
|
15
|
-
baseUrl:
|
|
15
|
+
baseUrl: envValue("GANGTISE_BASE_URL") ?? DEFAULT_BASE_URL,
|
|
16
16
|
timeoutMs,
|
|
17
|
-
accessKey:
|
|
18
|
-
secretKey:
|
|
19
|
-
token:
|
|
20
|
-
tokenCachePath:
|
|
17
|
+
accessKey: envValue("GANGTISE_ACCESS_KEY"),
|
|
18
|
+
secretKey: envValue("GANGTISE_SECRET_KEY"),
|
|
19
|
+
token: envValue("GANGTISE_TOKEN"),
|
|
20
|
+
tokenCachePath: envValue("GANGTISE_TOKEN_CACHE_PATH") ?? DEFAULT_TOKEN_CACHE_PATH,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
function envValue(name) {
|
|
24
|
+
const value = process.env[name]?.trim();
|
|
25
|
+
return value ? value : undefined;
|
|
26
|
+
}
|
|
23
27
|
export const MIN_TIMEOUT_MS = 1_000;
|
|
24
28
|
export const MAX_TIMEOUT_MS = 3_600_000;
|
|
25
29
|
export function resolveTimeoutEnv(raw) {
|
|
@@ -84,10 +84,27 @@ export function extFromContentType(contentType) {
|
|
|
84
84
|
const mime = contentType.split(";")[0].trim().toLowerCase();
|
|
85
85
|
return MIME_EXT[mime] ?? "";
|
|
86
86
|
}
|
|
87
|
+
const KNOWN_EXT = new Set(Object.values(MIME_EXT).concat([".md", ".jpeg", ".htm"]));
|
|
88
|
+
export function extFromUrl(url) {
|
|
89
|
+
try {
|
|
90
|
+
const ext = extname(decodeURIComponent(new URL(url).pathname)).toLowerCase();
|
|
91
|
+
return KNOWN_EXT.has(ext) ? ext : "";
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function resultExt(file) {
|
|
98
|
+
if (file.filename)
|
|
99
|
+
return extname(file.filename);
|
|
100
|
+
if (typeof file.url === "string")
|
|
101
|
+
return extFromUrl(file.url);
|
|
102
|
+
return extFromContentType(file.contentType);
|
|
103
|
+
}
|
|
87
104
|
export async function resolveTitle(client, result, listEndpoint, idField, idValue, options = {}) {
|
|
88
105
|
const titleField = options.titleField ?? "title";
|
|
89
106
|
const file = result;
|
|
90
|
-
const serverExt =
|
|
107
|
+
const serverExt = resultExt(file);
|
|
91
108
|
function buildFilename(rawTitle) {
|
|
92
109
|
let title = sanitizeFilename(rawTitle).trim();
|
|
93
110
|
if (serverExt && !title.toLowerCase().endsWith(serverExt.toLowerCase())) {
|
|
@@ -130,7 +147,7 @@ async function downloadUrlTo(url, outputPath) {
|
|
|
130
147
|
const { pipeline } = await import("node:stream/promises");
|
|
131
148
|
const { dirname } = await import("node:path");
|
|
132
149
|
const { request } = await import("undici");
|
|
133
|
-
const { getDispatcher, logTiming, withRetry } = await import("./transport.js");
|
|
150
|
+
const { getDispatcher, logTiming, TRANSFER_TOTAL_FACTOR, withRetry } = await import("./transport.js");
|
|
134
151
|
const { loadConfig } = await import("./config.js");
|
|
135
152
|
const timeoutMs = loadConfig().timeoutMs;
|
|
136
153
|
await fs.mkdir(dirname(outputPath), { recursive: true });
|
|
@@ -150,7 +167,7 @@ async function downloadUrlTo(url, outputPath) {
|
|
|
150
167
|
method: "GET",
|
|
151
168
|
headersTimeout: timeoutMs,
|
|
152
169
|
bodyTimeout: timeoutMs,
|
|
153
|
-
signal: AbortSignal.timeout(timeoutMs *
|
|
170
|
+
signal: AbortSignal.timeout(timeoutMs * TRANSFER_TOTAL_FACTOR),
|
|
154
171
|
dispatcher: getDispatcher(),
|
|
155
172
|
};
|
|
156
173
|
let currentUrl = url;
|
|
@@ -189,6 +206,23 @@ async function downloadUrlTo(url, outputPath) {
|
|
|
189
206
|
throw error;
|
|
190
207
|
}
|
|
191
208
|
}
|
|
209
|
+
export function autoDownloadName(file, fallbackName) {
|
|
210
|
+
return truncateFilename(file.filename ? sanitizeFilename(file.filename) : sanitizeFilename(fallbackName) + extFromContentType(file.contentType));
|
|
211
|
+
}
|
|
212
|
+
export async function placeStreamedDownload(file, fallbackName, named) {
|
|
213
|
+
let target;
|
|
214
|
+
try {
|
|
215
|
+
target = await uniquePath(truncateFilename(named ?? autoDownloadName(file, fallbackName)));
|
|
216
|
+
await fs.rename(file.savedPath, target);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (target)
|
|
220
|
+
await releaseClaim(target);
|
|
221
|
+
await fs.unlink(file.savedPath).catch(() => { });
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
return target;
|
|
225
|
+
}
|
|
192
226
|
export async function saveDownloadResult(result, fallbackName, output) {
|
|
193
227
|
if (!(result && typeof result === "object")) {
|
|
194
228
|
throw new DownloadError("Unexpected download response");
|
|
@@ -200,8 +234,7 @@ export async function saveDownloadResult(result, fallbackName, output) {
|
|
|
200
234
|
return;
|
|
201
235
|
}
|
|
202
236
|
if (file.data instanceof Uint8Array) {
|
|
203
|
-
const
|
|
204
|
-
const outputPath = output ?? await uniquePath(truncateFilename(autoName));
|
|
237
|
+
const outputPath = output ?? await uniquePath(autoDownloadName(file, fallbackName));
|
|
205
238
|
try {
|
|
206
239
|
await saveOutputIfNeeded(file.data, outputPath, false);
|
|
207
240
|
}
|
|
@@ -227,12 +260,16 @@ export async function saveDownloadResult(result, fallbackName, output) {
|
|
|
227
260
|
return;
|
|
228
261
|
}
|
|
229
262
|
if (typeof file.url === "string") {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
263
|
+
const outputPath = output ?? await uniquePath(truncateFilename(safeFallback + extFromUrl(file.url)));
|
|
264
|
+
try {
|
|
265
|
+
await downloadUrlTo(file.url, outputPath);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (!output)
|
|
269
|
+
await releaseClaim(outputPath);
|
|
270
|
+
throw error;
|
|
234
271
|
}
|
|
235
|
-
process.stdout.write(`${
|
|
272
|
+
process.stdout.write(`${outputPath}\n`);
|
|
236
273
|
return;
|
|
237
274
|
}
|
|
238
275
|
throw new DownloadError("Unexpected download response");
|
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
import { beijingDateString, parseDateOption } from "./args.js";
|
|
2
|
+
import { ValidationError } from "./errors.js";
|
|
3
|
+
export const COSTLY_FETCH_CREDITS = 1000;
|
|
4
|
+
const FORECAST_ROWS_PER_DATE = 3;
|
|
5
|
+
function billedDate(value, key) {
|
|
6
|
+
if (typeof value !== "string")
|
|
7
|
+
throw new ValidationError(`${key} is not a date string`);
|
|
8
|
+
return parseDateOption(value, key);
|
|
9
|
+
}
|
|
10
|
+
export function weekdaysBetween(start, end) {
|
|
11
|
+
const from = Date.parse(`${start}T00:00:00Z`);
|
|
12
|
+
const to = Date.parse(`${end}T00:00:00Z`);
|
|
13
|
+
if (!(to >= from))
|
|
14
|
+
return 0;
|
|
15
|
+
let count = 0;
|
|
16
|
+
for (let t = from; t <= to; t += 86_400_000) {
|
|
17
|
+
const day = new Date(t).getUTCDay();
|
|
18
|
+
if (day !== 0 && day !== 6)
|
|
19
|
+
count++;
|
|
20
|
+
}
|
|
21
|
+
return count;
|
|
22
|
+
}
|
|
23
|
+
export function maxUnitsOf(key) {
|
|
24
|
+
const max = ENDPOINTS[key]?.billing?.maxUnits;
|
|
25
|
+
if (max === undefined)
|
|
26
|
+
throw new Error(`${key} declares no billing.maxUnits`);
|
|
27
|
+
return max;
|
|
28
|
+
}
|
|
1
29
|
export function resolveTimeoutMs(configTimeoutMs, endpoint) {
|
|
2
30
|
return Math.max(configTimeoutMs, endpoint.timeoutMs ?? 0);
|
|
3
31
|
}
|
|
@@ -502,6 +530,11 @@ const ENDPOINT_DEFS = {
|
|
|
502
530
|
kind: "json",
|
|
503
531
|
description: "Query earning forecast (consensus estimates)",
|
|
504
532
|
billing: { per: "row", price: 0.5, maxUnits: Number.POSITIVE_INFINITY },
|
|
533
|
+
estimateBilledUnits: (body) => {
|
|
534
|
+
const end = body.endDate == null ? beijingDateString(new Date()) : billedDate(body.endDate, "endDate");
|
|
535
|
+
const start = body.startDate == null ? new Date(Date.parse(`${end}T00:00:00Z`) - 365 * 86_400_000).toISOString().slice(0, 10) : billedDate(body.startDate, "startDate");
|
|
536
|
+
return weekdaysBetween(start, end) * FORECAST_ROWS_PER_DATE;
|
|
537
|
+
},
|
|
505
538
|
retry: "no-replay",
|
|
506
539
|
},
|
|
507
540
|
"bond.basic-info": {
|
|
@@ -612,6 +645,168 @@ const ENDPOINT_DEFS = {
|
|
|
612
645
|
retry: "no-replay",
|
|
613
646
|
expects: "list",
|
|
614
647
|
},
|
|
648
|
+
"fund.basic-info": {
|
|
649
|
+
method: "POST",
|
|
650
|
+
path: "/application/open-fundamental/fund/basic-info",
|
|
651
|
+
kind: "json",
|
|
652
|
+
description: "Query fund profiles (category, manager, subscription rules, benchmark, tracked index)",
|
|
653
|
+
billing: { per: "call", price: 0.4 },
|
|
654
|
+
retry: "no-replay",
|
|
655
|
+
expects: "list",
|
|
656
|
+
},
|
|
657
|
+
"fund.nav": {
|
|
658
|
+
method: "POST",
|
|
659
|
+
path: "/application/open-quote/fund/nav",
|
|
660
|
+
kind: "json",
|
|
661
|
+
description: "Query fund daily NAV (unit / accumulated / adjusted; money-market yields)",
|
|
662
|
+
billing: { per: "call", price: 0.4 },
|
|
663
|
+
retry: "no-replay",
|
|
664
|
+
expects: "list",
|
|
665
|
+
},
|
|
666
|
+
"fund.fee-rate": {
|
|
667
|
+
method: "POST",
|
|
668
|
+
path: "/application/open-fundamental/fund/fee-rate",
|
|
669
|
+
kind: "json",
|
|
670
|
+
description: "Query fund fee rates (purchase / redemption / management / custodian / sales service)",
|
|
671
|
+
billing: { per: "call", price: 0.4 },
|
|
672
|
+
retry: "no-replay",
|
|
673
|
+
expects: "list",
|
|
674
|
+
},
|
|
675
|
+
"fund.manager-info": {
|
|
676
|
+
method: "POST",
|
|
677
|
+
path: "/application/open-fundamental/fund/manager-info",
|
|
678
|
+
kind: "json",
|
|
679
|
+
description: "Query fund manager profiles by name (exact match; same-name managers all returned)",
|
|
680
|
+
billing: { per: "call", price: 0.4 },
|
|
681
|
+
retry: "no-replay",
|
|
682
|
+
expects: "list",
|
|
683
|
+
},
|
|
684
|
+
"fund.manager-history": {
|
|
685
|
+
method: "POST",
|
|
686
|
+
path: "/application/open-fundamental/fund/manager-history",
|
|
687
|
+
kind: "json",
|
|
688
|
+
description: "Query a fund's past and present managers (tenure + return over tenure)",
|
|
689
|
+
billing: { per: "call", price: 0.4 },
|
|
690
|
+
retry: "no-replay",
|
|
691
|
+
expects: "list",
|
|
692
|
+
},
|
|
693
|
+
"fund.asset-allocation": {
|
|
694
|
+
method: "POST",
|
|
695
|
+
path: "/application/open-fundamental/fund/asset-allocation",
|
|
696
|
+
kind: "json",
|
|
697
|
+
description: "Query a fund's asset allocation per report date (equity / bonds / cash ...)",
|
|
698
|
+
billing: { per: "call", price: 0.4 },
|
|
699
|
+
retry: "no-replay",
|
|
700
|
+
expects: "list",
|
|
701
|
+
},
|
|
702
|
+
"fund.asset-size": {
|
|
703
|
+
method: "POST",
|
|
704
|
+
path: "/application/open-fundamental/fund/asset-size",
|
|
705
|
+
kind: "json",
|
|
706
|
+
description: "Query a fund's shares, subscriptions / redemptions and net assets per report date",
|
|
707
|
+
billing: { per: "call", price: 0.4 },
|
|
708
|
+
retry: "no-replay",
|
|
709
|
+
expects: "list",
|
|
710
|
+
},
|
|
711
|
+
"fund.holder-structure": {
|
|
712
|
+
method: "POST",
|
|
713
|
+
path: "/application/open-fundamental/fund/holder-structure",
|
|
714
|
+
kind: "json",
|
|
715
|
+
description: "Query a fund's holder structure per report date (institutional / individual / employee)",
|
|
716
|
+
billing: { per: "call", price: 0.4 },
|
|
717
|
+
retry: "no-replay",
|
|
718
|
+
expects: "list",
|
|
719
|
+
},
|
|
720
|
+
"fund.top10-holders": {
|
|
721
|
+
method: "POST",
|
|
722
|
+
path: "/application/open-fundamental/fund/top10-holders",
|
|
723
|
+
kind: "json",
|
|
724
|
+
description: "Query a listed fund's top 10 holders per report date",
|
|
725
|
+
billing: { per: "call", price: 0.4 },
|
|
726
|
+
retry: "no-replay",
|
|
727
|
+
expects: "list",
|
|
728
|
+
},
|
|
729
|
+
"fund.stock-portfolio": {
|
|
730
|
+
method: "POST",
|
|
731
|
+
path: "/application/open-fundamental/fund/stock-portfolio",
|
|
732
|
+
kind: "json",
|
|
733
|
+
description: "Query a fund's stock holdings per report date",
|
|
734
|
+
billing: { per: "call", price: 0.4 },
|
|
735
|
+
retry: "no-replay",
|
|
736
|
+
expects: "list",
|
|
737
|
+
},
|
|
738
|
+
"fund.industry-allocation": {
|
|
739
|
+
method: "POST",
|
|
740
|
+
path: "/application/open-fundamental/fund/industry-allocation",
|
|
741
|
+
kind: "json",
|
|
742
|
+
description: "Query a fund's stock holdings grouped by industry (SW / CITIC level 1)",
|
|
743
|
+
billing: { per: "call", price: 0.4 },
|
|
744
|
+
retry: "no-replay",
|
|
745
|
+
expects: "list",
|
|
746
|
+
},
|
|
747
|
+
"fund.bond-portfolio": {
|
|
748
|
+
method: "POST",
|
|
749
|
+
path: "/application/open-fundamental/fund/bond-portfolio",
|
|
750
|
+
kind: "json",
|
|
751
|
+
description: "Query a fund's bond holdings per report date",
|
|
752
|
+
billing: { per: "call", price: 0.4 },
|
|
753
|
+
retry: "no-replay",
|
|
754
|
+
expects: "list",
|
|
755
|
+
},
|
|
756
|
+
"fund.bond-type-allocation": {
|
|
757
|
+
method: "POST",
|
|
758
|
+
path: "/application/open-fundamental/fund/bond-type-allocation",
|
|
759
|
+
kind: "json",
|
|
760
|
+
description: "Query a fund's bond holdings grouped by bond type",
|
|
761
|
+
billing: { per: "call", price: 0.4 },
|
|
762
|
+
retry: "no-replay",
|
|
763
|
+
expects: "list",
|
|
764
|
+
},
|
|
765
|
+
"fund.fund-portfolio": {
|
|
766
|
+
method: "POST",
|
|
767
|
+
path: "/application/open-fundamental/fund/fund-portfolio",
|
|
768
|
+
kind: "json",
|
|
769
|
+
description: "Query a fund-of-funds' holdings of other funds",
|
|
770
|
+
billing: { per: "call", price: 0.4 },
|
|
771
|
+
retry: "no-replay",
|
|
772
|
+
expects: "list",
|
|
773
|
+
},
|
|
774
|
+
"fund.fund-type-allocation": {
|
|
775
|
+
method: "POST",
|
|
776
|
+
path: "/application/open-fundamental/fund/fund-type-allocation",
|
|
777
|
+
kind: "json",
|
|
778
|
+
description: "Query a fund-of-funds' holdings grouped by fund category",
|
|
779
|
+
billing: { per: "call", price: 0.4 },
|
|
780
|
+
retry: "no-replay",
|
|
781
|
+
expects: "list",
|
|
782
|
+
},
|
|
783
|
+
"fund.etf-pcf-header": {
|
|
784
|
+
method: "POST",
|
|
785
|
+
path: "/application/open-fundamental/fund/etf-pcf-header",
|
|
786
|
+
kind: "json",
|
|
787
|
+
description: "Query ETF creation/redemption parameters (latest trade date)",
|
|
788
|
+
billing: { per: "call", price: 0.4 },
|
|
789
|
+
retry: "no-replay",
|
|
790
|
+
expects: "list",
|
|
791
|
+
},
|
|
792
|
+
"fund.etf-pcf-components": {
|
|
793
|
+
method: "POST",
|
|
794
|
+
path: "/application/open-fundamental/fund/etf-pcf-components",
|
|
795
|
+
kind: "json",
|
|
796
|
+
description: "Query the ETF creation/redemption component list (latest trade date)",
|
|
797
|
+
billing: { per: "call", price: 0.4 },
|
|
798
|
+
retry: "no-replay",
|
|
799
|
+
expects: "list",
|
|
800
|
+
},
|
|
801
|
+
"fund.etf-share-change": {
|
|
802
|
+
method: "POST",
|
|
803
|
+
path: "/application/open-fundamental/fund/etf-share-change",
|
|
804
|
+
kind: "json",
|
|
805
|
+
description: "Query ETF shares and scale per trade date",
|
|
806
|
+
billing: { per: "call", price: 0.4 },
|
|
807
|
+
retry: "no-replay",
|
|
808
|
+
expects: "list",
|
|
809
|
+
},
|
|
615
810
|
"ai.stock-summary.list": {
|
|
616
811
|
method: "POST",
|
|
617
812
|
path: "/application/open-ai/stock-summary/getList",
|
package/dist/src/core/errors.js
CHANGED
|
@@ -48,7 +48,7 @@ const ERROR_HINTS = {
|
|
|
48
48
|
"100003": "msg 已指明字段名或取值范围时直接按 msg 改;msg 只说「参数值非法」时多为枚举参数拼写错误(如 --source / --question-category / --answer-important),对照命令 --help 列出的合法值检查。",
|
|
49
49
|
"100004": "检查 --size / --from 是否为非负数且未超单页上限。",
|
|
50
50
|
"100005": "对照命令 --help 列出的合法取值检查。",
|
|
51
|
-
"100006": "缩短日期范围或调小 --size / --limit(list 类端点单页上限为 50,CLI 自动翻页,直发接口时要自己遵守);indicator cross-section / time-series 撞此码是单次单元格数超 30000
|
|
51
|
+
"100006": "缩短日期范围或调小 --size / --limit(list 类端点单页上限为 50,CLI 自动翻页,直发接口时要自己遵守);indicator cross-section / time-series 撞此码是单次单元格数超 30000(截面=证券数×指标数,时序=证券数或指标数×日期数),按这个乘积拆批重跑;fund 命令不分页,撞此码是单次超过 10000 行,减少 --security 只数或缩短日期区间分批查。",
|
|
52
52
|
"110001": "看参数名:`--*-date` 用 YYYY-MM-DD,`--*-time` 用 \"YYYY-MM-DD HH:mm:ss\"(`ai knowledge-batch` 的 --start-time/--end-time 收时间戳或 datetime,CLI 统一转 13 位毫秒)。",
|
|
53
53
|
"110002": "起始晚于结束——检查 --start-date/--end-date 或 --start-time/--end-time 的先后。",
|
|
54
54
|
"110003": "查询时间超出本账号的数据权限范围——把日期移进范围内(整个区间都早于下界时缩短窗口无用),或联系客户经理开通更长历史。",
|