gangtise-openapi-cli 0.42.0 → 0.43.1
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 +43 -36
- package/dist/src/cli.js +29 -11
- package/dist/src/commands/ai.js +7 -6
- package/dist/src/commands/fundamental.js +3 -3
- package/dist/src/commands/indicator.js +3 -3
- package/dist/src/commands/insight.js +5 -4
- package/dist/src/commands/quote.js +46 -12
- package/dist/src/commands/raw.js +2 -2
- package/dist/src/commands/shared.js +60 -8
- package/dist/src/commands/vault.js +2 -2
- package/dist/src/core/args.js +6 -5
- package/dist/src/core/auth.js +3 -2
- package/dist/src/core/client.js +114 -11
- package/dist/src/core/commandBodies.js +2 -1
- package/dist/src/core/download.js +9 -5
- package/dist/src/core/endpoints.js +103 -1
- package/dist/src/core/opinionDetail.js +16 -6
- package/dist/src/core/output.js +37 -8
- package/dist/src/core/perSecurity.js +29 -11
- package/dist/src/core/quoteSharding.js +19 -15
- package/dist/src/core/rowSink.js +21 -0
- package/dist/src/core/titleCache.js +28 -2
- package/dist/src/core/transport.js +3 -1
- package/dist/src/core/updateCheck.js +51 -0
- package/dist/src/version.js +19 -1
- package/gangtise-openapi/SKILL.md +50 -49
- package/gangtise-openapi/references/commands/ai.md +34 -15
- package/gangtise-openapi/references/commands/alternative.md +2 -2
- package/gangtise-openapi/references/commands/bond.md +1 -1
- package/gangtise-openapi/references/commands/fundamental.md +4 -4
- package/gangtise-openapi/references/commands/indicator.md +31 -29
- package/gangtise-openapi/references/commands/insight.md +40 -32
- package/gangtise-openapi/references/commands/quote.md +19 -19
- package/gangtise-openapi/references/commands/reference-and-lookup.md +18 -15
- package/gangtise-openapi/references/commands/vault.md +10 -7
- package/gangtise-openapi/references/errors.md +22 -23
- package/gangtise-openapi/references/examples.md +36 -34
- package/gangtise-openapi/references/fields.md +8 -6
- package/gangtise-openapi/references/lookup-ids.md +10 -8
- package/gangtise-openapi/references/response-schema.md +7 -7
- package/package.json +1 -1
|
@@ -7,7 +7,8 @@ import { ValidationError } from "../core/errors.js";
|
|
|
7
7
|
import { flagMissingFields } from "../core/normalize.js";
|
|
8
8
|
import { parseOutputFormat } from "../core/output.js";
|
|
9
9
|
import { printData } from "../core/printer.js";
|
|
10
|
-
import {
|
|
10
|
+
import { rowCount } from "../core/rowSink.js";
|
|
11
|
+
import { emit, withClient, DEFAULT_QUOTE_LIMIT, MAX_QUOTE_LIMIT, flagIfLimitTruncated, noteLateStart } from "./shared.js";
|
|
11
12
|
export const quote = new Command("quote").description("Quote APIs");
|
|
12
13
|
const KLINE_MARKETS = { aShares: 1, hkStocks: 2, usStocks: 1 };
|
|
13
14
|
const LEGACY_ALL_MARKET = (shardDays) => ({ all: shardDays });
|
|
@@ -30,38 +31,61 @@ export const checkMarketKeywords = (securities, accepted, command, noKeywordReas
|
|
|
30
31
|
}
|
|
31
32
|
};
|
|
32
33
|
const canonicalizeMarketKeywords = (securities, accepted) => securities.map((s) => accepted.find((a) => matchesMarketKeyword(s, a)) ?? s);
|
|
34
|
+
function withIdentityFields(fieldList, identity, label) {
|
|
35
|
+
if (!fieldList)
|
|
36
|
+
return fieldList;
|
|
37
|
+
const missing = identity.filter((field) => !fieldList.includes(field));
|
|
38
|
+
if (missing.length === 0)
|
|
39
|
+
return fieldList;
|
|
40
|
+
const says = [missing.includes("securityCode") ? "security" : "", missing.some((f) => f !== "securityCode") ? "time" : ""].filter(Boolean).join(" and ");
|
|
41
|
+
process.stderr.write(`[gangtise] note: ${label} adds ${missing.join(", ")} to --field so each row says which ${says} it belongs to.\n`);
|
|
42
|
+
return [...missing, ...fieldList];
|
|
43
|
+
}
|
|
33
44
|
const addKlineCommand = (name, endpointKey, securityHelp, markets, noKeywordReason) => quote.command(name)
|
|
34
45
|
.option("--security <code>", securityHelp, collectList, [])
|
|
35
46
|
.option("--start-date <date>", "Start date (default: 1 year before end-date)", dateArg("--start-date"))
|
|
36
47
|
.option("--end-date <date>", "End date (default: latest)", dateArg("--end-date"))
|
|
37
|
-
.option("--limit <number>",
|
|
48
|
+
.option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}). With several securities it also sizes the groups they are fetched in, so a small value fetches one security per request`)
|
|
38
49
|
.option("--field <field>", "Field", collectList, [])
|
|
39
50
|
.option("--format <format>", "Output format", "table")
|
|
40
51
|
.option("--output <path>")
|
|
41
52
|
.action((options) => {
|
|
42
53
|
checkMarketKeywords(options.security, Object.keys(markets), `quote ${name}`, noKeywordReason);
|
|
43
54
|
options.security = canonicalizeMarketKeywords(options.security, Object.keys(markets));
|
|
55
|
+
const fullMarketKeyword = Object.keys(markets).find((k) => options.security.includes(k));
|
|
56
|
+
if (fullMarketKeyword && (!options.startDate || !options.endDate)) {
|
|
57
|
+
throw new ValidationError(`quote ${name} --security ${fullMarketKeyword} requires both --start-date and --end-date (the full market is fetched via date shards; an unbounded whole-market request is rejected by the server)`);
|
|
58
|
+
}
|
|
44
59
|
return withClient(options, async (client) => {
|
|
45
60
|
const format = parseOutputFormat(options.format);
|
|
61
|
+
client.rowSink?.watchEarliest("tradeDate");
|
|
46
62
|
const body = buildQuoteKlineBody(options);
|
|
63
|
+
const severalSecurities = (body.securityList?.length ?? 0) > 1 || fullMarketKeyword !== undefined;
|
|
64
|
+
body.fieldList = withIdentityFields(body.fieldList, severalSecurities ? ["securityCode", "tradeDate"] : [], `quote ${name}`);
|
|
47
65
|
const keyword = Object.keys(markets).find((k) => isFullMarket(body, k));
|
|
48
66
|
if (keyword) {
|
|
49
67
|
const data = await callKlineWithSharding(client, endpointKey, body, { shardDays: markets[keyword], fullMarketValue: keyword });
|
|
50
68
|
flagMissingFields(data, body.fieldList, `quote ${name}`);
|
|
69
|
+
noteLateStart(data, body.startDate, "tradeDate", `quote ${name}`);
|
|
51
70
|
await printData(data, format, options.output);
|
|
52
71
|
return;
|
|
53
72
|
}
|
|
54
73
|
const limit = body.limit ?? DEFAULT_QUOTE_LIMIT;
|
|
55
74
|
const securities = body.securityList ?? [];
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
const tradingDays = estimateTradingDays(body.startDate, body.endDate);
|
|
76
|
+
if (securities.length > 1 && securities.length * tradingDays >= limit) {
|
|
77
|
+
const cap = body.limit ?? MAX_QUOTE_LIMIT;
|
|
78
|
+
const groupSize = Math.max(1, Math.floor((cap - 1) / tradingDays));
|
|
79
|
+
const data = await callPerSecurity(client, endpointKey, securities, (codes) => ({ ...body, securityList: codes, limit: cap }), cap, `quote ${name}`, groupSize);
|
|
58
80
|
flagMissingFields(data, body.fieldList, `quote ${name}`);
|
|
81
|
+
noteLateStart(data, body.startDate, "tradeDate", `quote ${name}`);
|
|
59
82
|
await printData(data, format, options.output);
|
|
60
83
|
return;
|
|
61
84
|
}
|
|
62
85
|
const data = await client.call(endpointKey, { ...body, limit });
|
|
63
86
|
flagIfLimitTruncated(data, limit, name);
|
|
64
87
|
flagMissingFields(data, body.fieldList, `quote ${name}`);
|
|
88
|
+
noteLateStart(data, body.startDate, "tradeDate", `quote ${name}`);
|
|
65
89
|
await printData(data, format, options.output);
|
|
66
90
|
});
|
|
67
91
|
});
|
|
@@ -73,28 +97,33 @@ quote.command("minute-kline")
|
|
|
73
97
|
.option("--security <code>", "Security code — A-share .SH/.SZ (SH/SZ only), ETF .SH/.SZ (e.g. 512800.SH), exchange index .SH/.SZ, concept index .GT, industry index .CI/.SWI, global index (e.g. SPX.SPI / N225.NKI / HSI.HI); repeat for several — one request each, run concurrently and merged; no whole-market keyword", collectList, [])
|
|
74
98
|
.option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss)", datetimeArg("--start-time"))
|
|
75
99
|
.option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss)", datetimeArg("--end-time"))
|
|
76
|
-
.option("--limit <number>",
|
|
100
|
+
.option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT})`)
|
|
77
101
|
.option("--field <field>", "Field", collectList, [])
|
|
78
102
|
.option("--format <format>", "Output format", "table")
|
|
79
103
|
.option("--output <path>")
|
|
80
104
|
.action((options) => withClient(options, async (client) => {
|
|
81
105
|
const format = parseOutputFormat(options.format);
|
|
82
|
-
|
|
83
|
-
const
|
|
106
|
+
client.rowSink?.watchEarliest("tradeTime");
|
|
107
|
+
const limit = parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: MAX_QUOTE_LIMIT }) ?? DEFAULT_QUOTE_LIMIT;
|
|
84
108
|
const securities = options.security;
|
|
85
109
|
if (securities.length === 0)
|
|
86
110
|
throw new ValidationError("--security is required (repeat it for several securities)");
|
|
111
|
+
const fieldList = withIdentityFields(maybeArray(options.field), securities.length > 1 ? ["securityCode", "tradeTime"] : [], "quote minute-kline");
|
|
87
112
|
const makeBody = (code) => ({ securityCode: code, startTime: options.startTime, endTime: options.endTime, limit, fieldList });
|
|
88
113
|
const data = securities.length === 1
|
|
89
114
|
? await client.call("quote.minute-kline", makeBody(securities[0]))
|
|
90
|
-
: await callPerSecurity(client, "quote.minute-kline", securities, makeBody, limit, "quote minute-kline");
|
|
115
|
+
: await callPerSecurity(client, "quote.minute-kline", securities, ([code]) => makeBody(code), limit, "quote minute-kline");
|
|
91
116
|
if (securities.length === 1)
|
|
92
117
|
flagIfLimitTruncated(data, limit, "minute-kline", "--start-time/--end-time");
|
|
93
118
|
flagMissingFields(data, fieldList, "quote minute-kline");
|
|
119
|
+
noteLateStart(data, options.startTime, "tradeTime", "quote minute-kline");
|
|
120
|
+
if (rowCount(data) === 0 && (options.startTime || options.endTime)) {
|
|
121
|
+
process.stderr.write(`[gangtise] note: no minute bars came back. Minute bars cover a much shorter history than daily bars, and a range entirely before it returns nothing rather than an error — try a recent range to check.\n`);
|
|
122
|
+
}
|
|
94
123
|
await printData(data, format, options.output);
|
|
95
124
|
}));
|
|
96
125
|
quote.command("realtime")
|
|
97
|
-
.description("Realtime quote snapshot (A-share / HK / US stocks, ETFs, and indices incl.
|
|
126
|
+
.description("Realtime quote snapshot (A-share / HK / US stocks, ETFs, and indices incl. global indices)")
|
|
98
127
|
.option("--security <code>", "Security code — stock .SH/.SZ/.BJ/.HK/.O/.N/.A, ETF .SH/.SZ (e.g. 512800.SH), exchange index .SH/.SZ/.BJ, concept index .GT, industry index .CI/.SWI, global index (e.g. SPX.SPI / N225.NKI / HSI.HI); or one market keyword: aShares / hkStocks / usStocks (must be passed alone; keywords cover stocks only — ETFs and indices have no whole-market keyword)", collectList, [])
|
|
99
128
|
.option("--field <field>", "Field", collectList, [])
|
|
100
129
|
.option("--format <format>", "Output format", "table")
|
|
@@ -102,7 +131,9 @@ quote.command("realtime")
|
|
|
102
131
|
.action((options) => {
|
|
103
132
|
checkMarketKeywords(options.security, REALTIME_MARKETS, "quote realtime");
|
|
104
133
|
return emit(options, async (client) => {
|
|
105
|
-
const
|
|
134
|
+
const securities = options.security;
|
|
135
|
+
const severalSecurities = securities.length > 1 || securities.some((s) => REALTIME_MARKETS.some((keyword) => matchesMarketKeyword(s, keyword)));
|
|
136
|
+
const fieldList = withIdentityFields(maybeArray(options.field), severalSecurities ? ["securityCode"] : [], "quote realtime");
|
|
106
137
|
const data = await client.call("quote.realtime", { securityList: maybeArray(options.security), fieldList });
|
|
107
138
|
flagMissingFields(data, fieldList, "quote realtime");
|
|
108
139
|
return data;
|
|
@@ -113,7 +144,7 @@ quote.command("fund-flow")
|
|
|
113
144
|
.option("--security <code>", "Security code (e.g. 600519.SH / 920982.BJ), or 'aShares' for full A-share market — auto-sharded by day (repeat)", collectList, [])
|
|
114
145
|
.option("--start-date <date>", "Start date yyyy-MM-dd (default: endDate minus 1 year)", dateArg("--start-date"))
|
|
115
146
|
.option("--end-date <date>", "End date yyyy-MM-dd (default: latest trading day)", dateArg("--end-date"))
|
|
116
|
-
.option("--limit <number>",
|
|
147
|
+
.option("--limit <number>", `Max rows per request (default: ${DEFAULT_QUOTE_LIMIT}, max: ${MAX_QUOTE_LIMIT}; single-security cap — aShares auto-shards by day)`)
|
|
117
148
|
.option("--field <field>", "Field, e.g. mainNetInflow/largeInflow/xlargeOutflow (repeat); omit for all", collectList, [])
|
|
118
149
|
.option("--format <format>", "Output format", "table")
|
|
119
150
|
.option("--output <path>")
|
|
@@ -122,11 +153,12 @@ quote.command("fund-flow")
|
|
|
122
153
|
options.security = canonicalizeMarketKeywords(options.security, FUND_FLOW_MARKETS);
|
|
123
154
|
return withClient(options, async (client) => {
|
|
124
155
|
const format = parseOutputFormat(options.format);
|
|
156
|
+
client.rowSink?.watchEarliest("tradeDate");
|
|
125
157
|
const body = {
|
|
126
158
|
securityList: maybeArray(options.security),
|
|
127
159
|
startDate: options.startDate,
|
|
128
160
|
endDate: options.endDate,
|
|
129
|
-
limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max:
|
|
161
|
+
limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: MAX_QUOTE_LIMIT }),
|
|
130
162
|
fieldList: maybeArray(options.field),
|
|
131
163
|
};
|
|
132
164
|
if (isFullMarket(body, "aShares")) {
|
|
@@ -135,6 +167,7 @@ quote.command("fund-flow")
|
|
|
135
167
|
}
|
|
136
168
|
const data = await callKlineWithSharding(client, "quote.fund-flow", body, { shardDays: 1, fullMarketValue: "aShares" });
|
|
137
169
|
flagMissingFields(data, body.fieldList, "quote fund-flow");
|
|
170
|
+
noteLateStart(data, body.startDate, "tradeDate", "quote fund-flow");
|
|
138
171
|
await printData(data, format, options.output);
|
|
139
172
|
return;
|
|
140
173
|
}
|
|
@@ -142,6 +175,7 @@ quote.command("fund-flow")
|
|
|
142
175
|
const data = await client.call("quote.fund-flow", { ...body, limit });
|
|
143
176
|
flagIfLimitTruncated(data, limit, "fund-flow");
|
|
144
177
|
flagMissingFields(data, body.fieldList, "quote fund-flow");
|
|
178
|
+
noteLateStart(data, body.startDate, "tradeDate", "quote fund-flow");
|
|
145
179
|
await printData(data, format, options.output);
|
|
146
180
|
});
|
|
147
181
|
});
|
package/dist/src/commands/raw.js
CHANGED
|
@@ -6,14 +6,14 @@ import { flagFailedItems } from "../core/normalize.js";
|
|
|
6
6
|
import { parseOutputFormat } from "../core/output.js";
|
|
7
7
|
import { printData } from "../core/printer.js";
|
|
8
8
|
import { createClient, runDownload, assertConfirmed } from "./shared.js";
|
|
9
|
-
export const raw = new Command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--yes", "Confirm an irreversible endpoint (required for the ones marked destructive)").option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
|
|
9
|
+
export const raw = new Command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--yes", "Confirm an irreversible endpoint (required for the ones marked destructive), or fetch every row of a per-row billed list past the credit guard").option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
|
|
10
10
|
const endpoint = ENDPOINTS[endpointKey];
|
|
11
11
|
if (!endpoint) {
|
|
12
12
|
throw new ConfigError(`Unknown endpoint key: ${endpointKey}`);
|
|
13
13
|
}
|
|
14
14
|
assertConfirmed(endpointKey, Boolean(options.yes), endpointKey);
|
|
15
15
|
const format = parseOutputFormat(options.format);
|
|
16
|
-
const client = await createClient({ format, output: options.output });
|
|
16
|
+
const client = await createClient({ format, output: options.output, yes: Boolean(options.yes) });
|
|
17
17
|
let body;
|
|
18
18
|
if (options.body) {
|
|
19
19
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Option } from "commander";
|
|
2
|
+
import { QUOTE_MAX_LIMIT } from "../core/quoteSharding.js";
|
|
2
3
|
import { collectList, dateArg, datetimeArg, maybeArray, numberListArg, parseChoiceList, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize } from "../core/args.js";
|
|
3
4
|
import { loadConfig } from "../core/config.js";
|
|
4
5
|
import { releaseClaim, resolveTitle, saveDownloadResult, uniquePath } from "../core/download.js";
|
|
@@ -6,15 +7,17 @@ import { ENDPOINTS } from "../core/endpoints.js";
|
|
|
6
7
|
import { ValidationError } from "../core/errors.js";
|
|
7
8
|
import { parseOutputFormat } from "../core/output.js";
|
|
8
9
|
import { printData } from "../core/printer.js";
|
|
9
|
-
import { ExportSink } from "../core/rowSink.js";
|
|
10
|
+
import { ExportSink, getRowSink } from "../core/rowSink.js";
|
|
10
11
|
export async function createClient(stream) {
|
|
11
12
|
const { GangtiseClient } = await import("../core/client.js");
|
|
12
13
|
const sink = stream?.output && (stream.format === "jsonl" || stream.format === "csv") ? new ExportSink(stream.output, stream.format, stream.cache) : undefined;
|
|
13
|
-
|
|
14
|
+
const client = new GangtiseClient(loadConfig(), sink);
|
|
15
|
+
client.allowCostlyFetch = Boolean(stream?.yes);
|
|
16
|
+
return client;
|
|
14
17
|
}
|
|
15
18
|
export async function emit(options, produce, cache) {
|
|
16
19
|
const format = parseOutputFormat(options.format);
|
|
17
|
-
const client = await createClient({ format, output: options.output, cache });
|
|
20
|
+
const client = await createClient({ format, output: options.output, cache, yes: options.yes });
|
|
18
21
|
try {
|
|
19
22
|
await printData(await produce(client), format, options.output, cache);
|
|
20
23
|
}
|
|
@@ -33,6 +36,7 @@ export async function withClient(a, b) {
|
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
export const DEFAULT_QUOTE_LIMIT = 6000;
|
|
39
|
+
export const MAX_QUOTE_LIMIT = QUOTE_MAX_LIMIT;
|
|
36
40
|
export function flagIfLimitTruncated(data, cap, label, rangeFlags = "--start-date/--end-date", advice) {
|
|
37
41
|
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
38
42
|
return;
|
|
@@ -44,6 +48,43 @@ export function flagIfLimitTruncated(data, cap, label, rangeFlags = "--start-dat
|
|
|
44
48
|
process.stderr.write(`[gangtise] warning: ${label} returned ${rec.list.length} rows = the ${cap}-row limit; results are likely truncated (this endpoint has no pagination). ${advice ?? `Narrow ${rangeFlags} or raise --limit (max 10000), fetching in date batches.`}\n`);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
51
|
+
const LATE_START_DAYS = 14;
|
|
52
|
+
export function earliestRowDate(data, field) {
|
|
53
|
+
const sink = getRowSink(data);
|
|
54
|
+
if (sink)
|
|
55
|
+
return sink.earliest(field);
|
|
56
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
57
|
+
return undefined;
|
|
58
|
+
const rec = data;
|
|
59
|
+
if (!Array.isArray(rec.list))
|
|
60
|
+
return undefined;
|
|
61
|
+
const index = Array.isArray(rec.fieldList) ? rec.fieldList.indexOf(field) : -1;
|
|
62
|
+
let earliest;
|
|
63
|
+
for (const row of rec.list) {
|
|
64
|
+
const value = Array.isArray(row) ? (index >= 0 ? row[index] : undefined) : row && typeof row === "object" ? row[field] : undefined;
|
|
65
|
+
if (typeof value !== "string")
|
|
66
|
+
continue;
|
|
67
|
+
const day = value.slice(0, 10);
|
|
68
|
+
if (earliest === undefined || day < earliest)
|
|
69
|
+
earliest = day;
|
|
70
|
+
}
|
|
71
|
+
return earliest;
|
|
72
|
+
}
|
|
73
|
+
export function noteLateStart(data, requestedStart, field, label) {
|
|
74
|
+
const failedShards = data?.failedShards;
|
|
75
|
+
if (Array.isArray(failedShards) && failedShards.length > 0)
|
|
76
|
+
return;
|
|
77
|
+
const start = requestedStart?.slice(0, 10);
|
|
78
|
+
if (!start || !/^\d{4}-\d{2}-\d{2}$/.test(start))
|
|
79
|
+
return;
|
|
80
|
+
const first = earliestRowDate(data, field);
|
|
81
|
+
if (!first || !/^\d{4}-\d{2}-\d{2}$/.test(first))
|
|
82
|
+
return;
|
|
83
|
+
const lateDays = (Date.parse(`${first}T00:00:00Z`) - Date.parse(`${start}T00:00:00Z`)) / 86_400_000;
|
|
84
|
+
if (lateDays <= LATE_START_DAYS)
|
|
85
|
+
return;
|
|
86
|
+
process.stderr.write(`[gangtise] note: ${label} starts at ${first}, ${lateDays} days after the requested start ${start}. The security may have listed or resumed trading later, or the range reaches past your account's history window for this data — the endpoint returns what lies inside the window without an error.\n`);
|
|
87
|
+
}
|
|
47
88
|
export async function runDownload(client, endpointKey, query, options) {
|
|
48
89
|
if (options.output) {
|
|
49
90
|
const result = await client.call(endpointKey, options.body, query, { streamTo: options.output });
|
|
@@ -68,8 +109,10 @@ export function addDownloadCommand(parent, spec) {
|
|
|
68
109
|
const option = new Option("--file-type <number>", spec.fileType.description).choices(spec.fileType.choices);
|
|
69
110
|
cmd.addOption(spec.fileType.required ? option.makeOptionMandatory() : option.default(spec.fileType.default));
|
|
70
111
|
}
|
|
71
|
-
if (spec.contentTypeDescription)
|
|
72
|
-
|
|
112
|
+
if (spec.contentTypeDescription) {
|
|
113
|
+
const option = new Option("--content-type <type>", spec.contentTypeDescription).makeOptionMandatory();
|
|
114
|
+
cmd.addOption(spec.contentTypeChoices ? option.choices(spec.contentTypeChoices) : option);
|
|
115
|
+
}
|
|
73
116
|
if (spec.titleListEndpoint) {
|
|
74
117
|
cmd.option("--resolve-title", "On a title-cache miss, query the list endpoint for a friendly filename (4 extra requests; most of these list endpoints bill per row). Without it the server filename or <prefix>-<id> is used. Ignored when --output is given");
|
|
75
118
|
}
|
|
@@ -97,6 +140,7 @@ export function assertConfirmed(endpointKey, confirmed, target) {
|
|
|
97
140
|
throw new ValidationError(`${destructive.warning} 确认要对 ${target} 执行就加上 --yes。`);
|
|
98
141
|
}
|
|
99
142
|
export const field = (option, body) => ({ option, body });
|
|
143
|
+
export const confirmCostly = () => field(new Option("--yes", "Fetch every row even when a fetch without --size is estimated above the credit guard (the list is billed per row)"));
|
|
100
144
|
export const value = (flags, description, key) => field(new Option(flags, description), (v) => ({ [key]: v }));
|
|
101
145
|
export const required = (flags, description, key) => field(new Option(flags, description).makeOptionMandatory(), (v) => ({ [key]: v }));
|
|
102
146
|
export const date = (flags, description, key, opts = {}) => {
|
|
@@ -110,8 +154,14 @@ export const numberList = (flags, description, key) => {
|
|
|
110
154
|
option.argParser(numberListArg(option.long));
|
|
111
155
|
return field(option, (v) => ({ [key]: v.length ? v : undefined }));
|
|
112
156
|
};
|
|
113
|
-
export const
|
|
157
|
+
export const multiChoiceValues = new WeakMap();
|
|
158
|
+
export function multiChoice(flags, description, choices) {
|
|
114
159
|
const option = new Option(flags, description).argParser(collectList).default([]);
|
|
160
|
+
multiChoiceValues.set(option, choices);
|
|
161
|
+
return option;
|
|
162
|
+
}
|
|
163
|
+
export const choiceList = (flags, description, key, choices) => {
|
|
164
|
+
const option = multiChoice(flags, description, choices);
|
|
115
165
|
return field(option, (v) => ({ [key]: parseChoiceList(v, option.long, choices) }));
|
|
116
166
|
};
|
|
117
167
|
export const count = (flags, description, key) => {
|
|
@@ -139,7 +189,9 @@ export function query(parent, name, spec) {
|
|
|
139
189
|
const command = parent.command(name);
|
|
140
190
|
if (spec.description)
|
|
141
191
|
command.description(spec.description);
|
|
142
|
-
|
|
192
|
+
const defaultEndpoint = ENDPOINTS[typeof spec.endpoint === "string" ? spec.endpoint : spec.endpoint({})];
|
|
193
|
+
const fields = defaultEndpoint?.pagination?.enabled && defaultEndpoint.billing?.per === "row" ? [...spec.fields, confirmCostly()] : spec.fields;
|
|
194
|
+
for (const f of fields)
|
|
143
195
|
command.addOption(f.option);
|
|
144
|
-
return command.action((options) => emit(options, (client) => client.call(typeof spec.endpoint === "string" ? spec.endpoint : spec.endpoint(options), requestBody(
|
|
196
|
+
return command.action((options) => emit(options, (client) => client.call(typeof spec.endpoint === "string" ? spec.endpoint : spec.endpoint(options), requestBody(fields, options)), spec.cache));
|
|
145
197
|
}
|
|
@@ -111,7 +111,7 @@ query(vault, "record-list", {
|
|
|
111
111
|
format(), output(),
|
|
112
112
|
],
|
|
113
113
|
});
|
|
114
|
-
addDownloadCommand(vault, { endpointKey: "vault.record.download", name: "record-download", idOption: "--record-id", idField: "recordId", fallbackPrefix: "record", contentTypeDescription: "Content type: original/asr/summary", titleListEndpoint: "vault.record.list" });
|
|
114
|
+
addDownloadCommand(vault, { endpointKey: "vault.record.download", name: "record-download", idOption: "--record-id", idField: "recordId", fallbackPrefix: "record", contentTypeDescription: "Content type: original/asr/summary", contentTypeChoices: ["original", "asr", "summary"], titleListEndpoint: "vault.record.list" });
|
|
115
115
|
query(vault, "my-conference-list", {
|
|
116
116
|
endpoint: "vault.my-conference.list",
|
|
117
117
|
cache: { endpointKey: "vault.my-conference.list", idField: "conferenceId" },
|
|
@@ -125,7 +125,7 @@ query(vault, "my-conference-list", {
|
|
|
125
125
|
format(), output(),
|
|
126
126
|
],
|
|
127
127
|
});
|
|
128
|
-
addDownloadCommand(vault, { endpointKey: "vault.my-conference.download", name: "my-conference-download", idOption: "--conference-id", idField: "conferenceId", fallbackPrefix: "conference", contentTypeDescription: "Content type: asr/summary", titleListEndpoint: "vault.my-conference.list" });
|
|
128
|
+
addDownloadCommand(vault, { endpointKey: "vault.my-conference.download", name: "my-conference-download", idOption: "--conference-id", idField: "conferenceId", fallbackPrefix: "conference", contentTypeDescription: "Content type: asr/summary", contentTypeChoices: ["asr", "summary"], titleListEndpoint: "vault.my-conference.list" });
|
|
129
129
|
vault.command("wechat-message-list")
|
|
130
130
|
.option("--from <number>", "Starting offset", "0")
|
|
131
131
|
.option("--size <number>", "Total rows to return; omit to fetch all")
|
package/dist/src/core/args.js
CHANGED
|
@@ -84,11 +84,14 @@ export function isVersionNewer(latest, current) {
|
|
|
84
84
|
export function parseChoiceList(values, optionName, allowed) {
|
|
85
85
|
for (const value of values) {
|
|
86
86
|
if (!allowed.includes(value)) {
|
|
87
|
-
throw new ValidationError(`Invalid ${optionName}: "${value}"
|
|
87
|
+
throw new ValidationError(`Invalid ${optionName}: "${value}". ${unknownChoiceMessage(allowed)}`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
return maybeArray(values);
|
|
91
91
|
}
|
|
92
|
+
export function unknownChoiceMessage(known) {
|
|
93
|
+
return `This CLI version knows ${known.join(", ")}. Check the spelling; if the platform has added a new value, report it so the CLI can be updated.`;
|
|
94
|
+
}
|
|
92
95
|
const YEAR_FIRST_DATE = /^(\d{4})([-/]?)(\d{2})\2(\d{2})$/;
|
|
93
96
|
function normalizeYearFirstDate(value) {
|
|
94
97
|
const parts = YEAR_FIRST_DATE.exec(value);
|
|
@@ -172,10 +175,8 @@ export function parseDatetimeOption(value, optionName) {
|
|
|
172
175
|
export function datetimeArg(optionName) {
|
|
173
176
|
return (value) => parseDatetimeOption(value, optionName);
|
|
174
177
|
}
|
|
175
|
-
export function
|
|
176
|
-
|
|
177
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
178
|
-
return `${d.getFullYear()}-${month}-${day}`;
|
|
178
|
+
export function beijingDateString(d) {
|
|
179
|
+
return new Date(d.getTime() + 8 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
179
180
|
}
|
|
180
181
|
function parseParamSpecs(specs, option, syntax) {
|
|
181
182
|
const groups = new Map();
|
package/dist/src/core/auth.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { createHash
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { ConfigError } from "./errors.js";
|
|
5
|
+
import { stagingPath } from "./output.js";
|
|
5
6
|
export function credentialFingerprint(accessKey, baseUrl) {
|
|
6
7
|
return createHash("sha256").update(`${accessKey}\u0000${baseUrl}`).digest("hex").slice(0, 16);
|
|
7
8
|
}
|
|
@@ -30,7 +31,7 @@ export function redactTokenCache(cache) {
|
|
|
30
31
|
}
|
|
31
32
|
export async function writeTokenCache(filePath, cache) {
|
|
32
33
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
33
|
-
const tmp =
|
|
34
|
+
const tmp = stagingPath(filePath, "tmp");
|
|
34
35
|
await fs.writeFile(tmp, JSON.stringify(cache, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
35
36
|
try {
|
|
36
37
|
await fs.rename(tmp, filePath);
|
package/dist/src/core/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { createWriteStream } from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
@@ -17,9 +18,32 @@ function hasExpectedShape(expects, payload) {
|
|
|
17
18
|
return Array.isArray(payload);
|
|
18
19
|
return Boolean(payload && typeof payload === "object" && Array.isArray(payload.list));
|
|
19
20
|
}
|
|
21
|
+
export const COSTLY_FETCH_CREDITS = 1000;
|
|
22
|
+
const PROBE_ABOVE_CREDITS = 50;
|
|
23
|
+
const OFFSET_REFUSAL_CODES = new Set(["140002", "100006"]);
|
|
24
|
+
function readEmptyListAsArray(page) {
|
|
25
|
+
const rec = page;
|
|
26
|
+
if (rec && typeof rec === "object" && rec.total === 0 && rec.list === null)
|
|
27
|
+
rec.list = [];
|
|
28
|
+
}
|
|
29
|
+
function markTotalCapped(out, why, collected, keepBelow) {
|
|
30
|
+
process.stderr.write(`[gangtise] warning: ${why}. This export is TRUNCATED at ${collected} rows. Narrow the query (e.g. a shorter time range) until total stays below ${keepBelow}, and fetch in slices.\n`);
|
|
31
|
+
out.partial = true;
|
|
32
|
+
out.totalCapped = true;
|
|
33
|
+
}
|
|
34
|
+
function stableStringify(value) {
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
37
|
+
if (value && typeof value === "object") {
|
|
38
|
+
const obj = value;
|
|
39
|
+
return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`;
|
|
40
|
+
}
|
|
41
|
+
return JSON.stringify(value);
|
|
42
|
+
}
|
|
20
43
|
export class GangtiseClient {
|
|
21
44
|
config;
|
|
22
45
|
rowSink;
|
|
46
|
+
allowCostlyFetch = false;
|
|
23
47
|
refreshPromise = null;
|
|
24
48
|
memoCache = null;
|
|
25
49
|
envTokenInvalidated = false;
|
|
@@ -168,12 +192,39 @@ export class GangtiseClient {
|
|
|
168
192
|
throw new ValidationError(`${endpoint.key} serves rows only up to offset ${maxWindow} (from + size ≤ ${maxWindow}); --from ${startFrom} is past it. Narrow the query (e.g. a shorter time range) instead of paging deeper.`);
|
|
169
193
|
}
|
|
170
194
|
const windowRoom = maxWindow === undefined ? Infinity : maxWindow - startFrom;
|
|
171
|
-
const
|
|
172
|
-
const
|
|
195
|
+
const billing = endpoint.billing;
|
|
196
|
+
const guarded = requestedSize === undefined && billing?.per === "row" && !this.allowCostlyFetch;
|
|
197
|
+
const refuseIfCostly = (reportedTotal, fetched) => {
|
|
198
|
+
if (!guarded || !billing)
|
|
199
|
+
return;
|
|
200
|
+
const rows = Math.min(Math.max(reportedTotal - startFrom, 0), windowRoom);
|
|
201
|
+
const estimatedCredits = Math.round(rows * billing.price * 100) / 100;
|
|
202
|
+
if (estimatedCredits <= COSTLY_FETCH_CREDITS)
|
|
203
|
+
return;
|
|
204
|
+
throw new ValidationError(`fetching all ${rows} rows of ${endpoint.key} would cost about ${estimatedCredits} credits (${billing.price} per row), above the ${COSTLY_FETCH_CREDITS}-credit guard for a fetch without --size. Only ${fetched} row(s) were fetched, to learn the total. Pass --size N for a bounded subset, or --yes to fetch them all.`);
|
|
205
|
+
};
|
|
206
|
+
let probe;
|
|
207
|
+
let probed = false;
|
|
208
|
+
let probeRows = 0;
|
|
209
|
+
if (guarded && billing && Math.min(maxPageSize, windowRoom) * billing.price > PROBE_ABOVE_CREDITS) {
|
|
210
|
+
probe = await this.requestJson(endpoint, { ...initialBody, from: startFrom, size: 1 });
|
|
211
|
+
readEmptyListAsArray(probe);
|
|
212
|
+
if (!this.isPaginatedListResponse(probe)) {
|
|
213
|
+
probed = true;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
refuseIfCostly(probe.total, probe.list.length);
|
|
217
|
+
probeRows = probe.list.length;
|
|
218
|
+
probed = probe.list.length === Math.min(Math.max(probe.total - startFrom, 0), windowRoom);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const firstPageSize = probed ? 1 : Math.min(requestedSize === undefined ? maxPageSize : Math.min(maxPageSize, requestedSize), windowRoom);
|
|
222
|
+
const firstPage = probed ? probe : await this.requestJson(endpoint, {
|
|
173
223
|
...initialBody,
|
|
174
224
|
from: startFrom,
|
|
175
225
|
size: firstPageSize,
|
|
176
226
|
});
|
|
227
|
+
readEmptyListAsArray(firstPage);
|
|
177
228
|
if (!this.isPaginatedListResponse(firstPage)) {
|
|
178
229
|
process.stderr.write(`[gangtise] warning: ${endpoint.key} is marked paginated but the first page has an unexpected shape (no numeric total + list); returning it as-is\n`);
|
|
179
230
|
markIncomplete();
|
|
@@ -195,7 +246,45 @@ export class GangtiseClient {
|
|
|
195
246
|
sink.setFieldList(firstPage.fieldList);
|
|
196
247
|
const collected = [];
|
|
197
248
|
let count = 0;
|
|
198
|
-
const
|
|
249
|
+
const rowId = endpoint.rowId;
|
|
250
|
+
const seen = rowId ? new Map() : undefined;
|
|
251
|
+
let duplicateRows = 0;
|
|
252
|
+
let changedRows = 0;
|
|
253
|
+
let idIsRowKey = true;
|
|
254
|
+
const dropRepeats = (rows) => {
|
|
255
|
+
if (!seen || !rowId)
|
|
256
|
+
return rows;
|
|
257
|
+
const onThisPage = new Map();
|
|
258
|
+
return rows.filter((row) => {
|
|
259
|
+
const id = row && typeof row === "object" ? row[rowId] : undefined;
|
|
260
|
+
if (id === undefined || id === null)
|
|
261
|
+
return true;
|
|
262
|
+
const digest = createHash("sha1").update(stableStringify(row)).digest("base64");
|
|
263
|
+
const key = String(id);
|
|
264
|
+
const pageVersions = onThisPage.get(key);
|
|
265
|
+
if (pageVersions && !pageVersions.has(digest))
|
|
266
|
+
idIsRowKey = false;
|
|
267
|
+
if (pageVersions)
|
|
268
|
+
pageVersions.add(digest);
|
|
269
|
+
else
|
|
270
|
+
onThisPage.set(key, new Set([digest]));
|
|
271
|
+
const versions = seen.get(key);
|
|
272
|
+
if (versions === undefined) {
|
|
273
|
+
seen.set(key, new Set([digest]));
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
if (versions.has(digest)) {
|
|
277
|
+
duplicateRows++;
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
versions.add(digest);
|
|
281
|
+
if (!pageVersions)
|
|
282
|
+
changedRows++;
|
|
283
|
+
return true;
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
const keep = async (fetched) => {
|
|
287
|
+
const rows = dropRepeats(fetched);
|
|
199
288
|
const kept = requestedSize === undefined ? rows : rows.slice(0, Math.max(0, requestedSize - count));
|
|
200
289
|
if (kept.length === 0)
|
|
201
290
|
return;
|
|
@@ -209,10 +298,20 @@ export class GangtiseClient {
|
|
|
209
298
|
const checkTotalCap = async (out) => {
|
|
210
299
|
if (probeFits)
|
|
211
300
|
return this.flagIfTotalCapped(endpoint, initialBody, total, out, count);
|
|
212
|
-
|
|
301
|
+
markTotalCapped(out, `${endpoint.key} reported total=${total}, which equals its ${maxWindow}-row offset window: rows past it can be neither fetched nor counted`, count, maxWindow);
|
|
213
302
|
};
|
|
214
303
|
const result = () => {
|
|
215
304
|
const out = { ...firstPage, total, list: collected };
|
|
305
|
+
if (duplicateRows > 0) {
|
|
306
|
+
process.stderr.write(`[gangtise] warning: ${endpoint.key} returned ${duplicateRows} row(s) more than once — its sort order is not unique, so paging also missed as many rows. The duplicates were dropped and the result is partial. Re-run over a shorter time range to fetch the missing rows.\n`);
|
|
307
|
+
out.duplicateRows = duplicateRows;
|
|
308
|
+
out.partial = true;
|
|
309
|
+
}
|
|
310
|
+
if (changedRows > 0 && idIsRowKey && !endpoint.rowIdUnverified) {
|
|
311
|
+
process.stderr.write(`[gangtise] warning: ${endpoint.key} returned ${changedRows} row(s) whose ${rowId} came back on a later page with different content — the list changed while it was being paged, so rows may also have been missed. Both versions were kept (deduplicating by ${rowId} keeps one) and the result is partial. Re-run to fetch a consistent result.\n`);
|
|
312
|
+
out.changedRows = changedRows;
|
|
313
|
+
out.partial = true;
|
|
314
|
+
}
|
|
216
315
|
return sink ? attachRowSink(out, sink) : out;
|
|
217
316
|
};
|
|
218
317
|
await keep(firstPage.list);
|
|
@@ -232,6 +331,7 @@ export class GangtiseClient {
|
|
|
232
331
|
await checkTotalCap(out);
|
|
233
332
|
return out;
|
|
234
333
|
}
|
|
334
|
+
refuseIfCostly(total, count + (probed ? 0 : probeRows));
|
|
235
335
|
const MAX_PAGES = 1000;
|
|
236
336
|
let truncatedByPageCap = false;
|
|
237
337
|
const pageRequests = [];
|
|
@@ -264,6 +364,7 @@ export class GangtiseClient {
|
|
|
264
364
|
from: req.from,
|
|
265
365
|
size: req.size,
|
|
266
366
|
});
|
|
367
|
+
readEmptyListAsArray(page);
|
|
267
368
|
if (!this.isPaginatedListResponse(page)) {
|
|
268
369
|
unexpectedShape = true;
|
|
269
370
|
failedPages.push(req);
|
|
@@ -293,9 +394,10 @@ export class GangtiseClient {
|
|
|
293
394
|
process.stderr.write(`[gangtise] warning: hit the ${MAX_PAGES}-page safety cap; fetched ${count} of ${total} rows. Narrow the query (e.g. a shorter date range) or pass --size to fetch a bounded subset.\n`);
|
|
294
395
|
}
|
|
295
396
|
const short = count < target;
|
|
397
|
+
const shortByRepeatsOnly = short && count + duplicateRows >= target;
|
|
296
398
|
const out = result();
|
|
297
399
|
flagWindowCut(out);
|
|
298
|
-
if (requestedSize === undefined && total > 0 && target === wanted && !short && !totalDrift && !truncatedByPageCap && failedPages.length === 0) {
|
|
400
|
+
if (requestedSize === undefined && total > 0 && target === wanted && (!short || shortByRepeatsOnly) && !totalDrift && !truncatedByPageCap && failedPages.length === 0) {
|
|
299
401
|
await checkTotalCap(out);
|
|
300
402
|
}
|
|
301
403
|
if (short || totalDrift || failedPages.length > 0)
|
|
@@ -310,7 +412,7 @@ export class GangtiseClient {
|
|
|
310
412
|
const skippedHint = aborted ? " A page hit a non-retryable error (e.g. rate limit); remaining pages were skipped." : "";
|
|
311
413
|
process.stderr.write(`[gangtise] warning: ${failedPages.length}/${pageRequests.length} pages not fetched${detail}; results are partial — got ${count}/${total} rows (see failedPages).${skippedHint}\n`);
|
|
312
414
|
}
|
|
313
|
-
else if (short && !truncatedByPageCap && !totalDrift) {
|
|
415
|
+
else if (short && !shortByRepeatsOnly && !truncatedByPageCap && !totalDrift) {
|
|
314
416
|
process.stderr.write(`[gangtise] warning: server returned ${count} of ${total} rows (a later page came back short); results may be incomplete\n`);
|
|
315
417
|
}
|
|
316
418
|
return out;
|
|
@@ -322,13 +424,14 @@ export class GangtiseClient {
|
|
|
322
424
|
if (this.isPaginatedListResponse(probe) && probe.list.length > 0)
|
|
323
425
|
totalCapped = true;
|
|
324
426
|
}
|
|
325
|
-
catch {
|
|
427
|
+
catch (error) {
|
|
428
|
+
if (error instanceof ApiError && error.code !== undefined && OFFSET_REFUSAL_CODES.has(error.code)) {
|
|
429
|
+
markTotalCapped(out, `${endpoint.key} reported total=${total} and refused the row right after it (${error.code}) — most likely a server-side offset window at total, so rows past it may exist and can be neither fetched nor counted`, collectedLength, total);
|
|
430
|
+
}
|
|
326
431
|
}
|
|
327
432
|
if (!totalCapped)
|
|
328
433
|
return;
|
|
329
|
-
|
|
330
|
-
out.partial = true;
|
|
331
|
-
out.totalCapped = true;
|
|
434
|
+
markTotalCapped(out, `${endpoint.key} reported total=${total} but rows exist past that offset — 'total' is a server-side cap, not the real count`, collectedLength, total);
|
|
332
435
|
}
|
|
333
436
|
async login() {
|
|
334
437
|
if (this.config.token && !this.envTokenInvalidated) {
|
|
@@ -420,7 +523,7 @@ export class GangtiseClient {
|
|
|
420
523
|
}).catch((error) => {
|
|
421
524
|
const isIndicatorFetch = endpoint.key === 'indicator.cross-section' || endpoint.key === 'indicator.time-series' || endpoint.key === 'indicator.screener';
|
|
422
525
|
if (isIndicatorFetch && error instanceof ApiError && error.code === '999999') {
|
|
423
|
-
throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList
|
|
526
|
+
throw new ApiError(error.message, error.code, error.statusCode, error.details, error.retryAfterMs, 'EDE 取数故障。先核对参数再重试:参数名以 indicator search 的 parameterList 为准(传错名会报 100003 并指名)、日期匹配指标周期(财务报表类用报告期末如 2025-12-31,PE/PB 等日频估值用交易日)、标的在 scopeList 覆盖内、required 参数已补。注意此码不表示无数据——无数据返回保留行列的 null 单元格;code 或参数名写错报 100003、缺必填参数报 100001,都会指名(fiscalYear 例外:漏传不报错,返回 null 或默认年度的值)。');
|
|
424
527
|
}
|
|
425
528
|
throw error;
|
|
426
529
|
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { maybeArray, parseFrom, parseIndicatorParams, parseOptionalNumberOption, parseScreenerIndicators, parseSize } from "./args.js";
|
|
2
2
|
import { ValidationError } from "./errors.js";
|
|
3
|
+
import { QUOTE_MAX_LIMIT } from "./quoteSharding.js";
|
|
3
4
|
export function buildQuoteKlineBody(options) {
|
|
4
5
|
return {
|
|
5
6
|
securityList: maybeArray(options.security),
|
|
6
7
|
startDate: options.startDate,
|
|
7
8
|
endDate: options.endDate,
|
|
8
|
-
limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max:
|
|
9
|
+
limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1, max: QUOTE_MAX_LIMIT }),
|
|
9
10
|
fieldList: maybeArray(options.field),
|
|
10
11
|
};
|
|
11
12
|
}
|