gangtise-openapi-cli 0.43.0 → 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 +93 -19
- 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 +96 -35
- 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 +198 -3
- package/dist/src/core/errors.js +1 -1
- package/dist/src/core/opinionDetail.js +16 -6
- 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 +53 -17
- package/gangtise-openapi/references/commands/fund.md +192 -0
- package/gangtise-openapi/references/commands/fundamental.md +2 -2
- package/gangtise-openapi/references/commands/indicator.md +7 -7
- package/gangtise-openapi/references/commands/insight.md +3 -3
- 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 +5 -5
- package/gangtise-openapi/references/response-schema.md +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { collectNames } from "../core/args.js";
|
|
3
|
+
import { date, format, list, output, query, requiredList, value } from "./shared.js";
|
|
4
|
+
export const fund = new Command("fund").description("Fund APIs: profiles, NAV, fees, managers, scale, holders, holdings and allocation, ETF creation/redemption");
|
|
5
|
+
const code = () => requiredList("--security <code>", "Fund code, e.g. 005827.OF / 159967.SZ / 510300.SH (repeatable, comma-separated accepted)", "fundCodeList");
|
|
6
|
+
const range = (what) => [
|
|
7
|
+
date("--start-date <date>", `${what} range start (yyyy-MM-dd); omit both ends for everything in your account's history window`, "startDate"),
|
|
8
|
+
date("--end-date <date>", `${what} range end (yyyy-MM-dd)`, "endDate"),
|
|
9
|
+
];
|
|
10
|
+
const positionType = () => value("--position-type <type>", "top = top holdings, disclosed every quarter (server default); all = full book, interim and annual reports only", "positionType");
|
|
11
|
+
query(fund, "basic-info", {
|
|
12
|
+
description: "Fund profiles: category, manager, custodian, dates, subscription rules, benchmark, risk level, tracked index",
|
|
13
|
+
endpoint: "fund.basic-info",
|
|
14
|
+
fields: [code(), list("--field <field>", "Field to return (repeatable); omit for all. An unsupported name rejects the whole call with 100003", "fieldList"), format(), output()],
|
|
15
|
+
});
|
|
16
|
+
query(fund, "nav", {
|
|
17
|
+
description: "Daily NAV: unit, accumulated and adjusted NAV, adjustment factor; money-market funds add 7-day annualized yield and income per 10k units",
|
|
18
|
+
endpoint: "fund.nav",
|
|
19
|
+
fields: [code(), ...range("Trade date"), format(), output()],
|
|
20
|
+
});
|
|
21
|
+
query(fund, "fee-rate", {
|
|
22
|
+
description: "Current fee rates by type, one row per condition band (holding period, amount)",
|
|
23
|
+
endpoint: "fund.fee-rate",
|
|
24
|
+
fields: [code(), list("--fee-type <type>", "purchaseFee / redemptionFee / managementFee / custodianFee / saleFee (repeatable); omit for all", "feeTypeList"), format(), output()],
|
|
25
|
+
});
|
|
26
|
+
query(fund, "manager-info", {
|
|
27
|
+
description: "Fund manager profiles by name: career, funds and assets under management, introduction",
|
|
28
|
+
endpoint: "fund.manager-info",
|
|
29
|
+
fields: [requiredList("--manager <name>", "Manager name, e.g. 张坤 (repeatable). Exact match; every manager with that name comes back — tell them apart by currentCompany", "managerNameList", collectNames), format(), output()],
|
|
30
|
+
});
|
|
31
|
+
query(fund, "manager-history", {
|
|
32
|
+
description: "Past and present managers of a fund: tenure, days managed, return over tenure (endDate null = current)",
|
|
33
|
+
endpoint: "fund.manager-history",
|
|
34
|
+
fields: [code(), format(), output()],
|
|
35
|
+
});
|
|
36
|
+
query(fund, "asset-size", {
|
|
37
|
+
description: "Shares at start and end of each report period, subscriptions, redemptions, net assets",
|
|
38
|
+
endpoint: "fund.asset-size",
|
|
39
|
+
fields: [code(), ...range("Report date"), format(), output()],
|
|
40
|
+
});
|
|
41
|
+
query(fund, "holder-structure", {
|
|
42
|
+
description: "Holder count and institutional / individual / employee holdings per report date",
|
|
43
|
+
endpoint: "fund.holder-structure",
|
|
44
|
+
fields: [code(), ...range("Report date"), format(), output()],
|
|
45
|
+
});
|
|
46
|
+
query(fund, "top10-holders", {
|
|
47
|
+
description: "Top 10 holders of a listed fund (LOF / ETF) per report date",
|
|
48
|
+
endpoint: "fund.top10-holders",
|
|
49
|
+
fields: [code(), ...range("Report date"), format(), output()],
|
|
50
|
+
});
|
|
51
|
+
query(fund, "asset-allocation", {
|
|
52
|
+
description: "Asset allocation per report date: amount and % of total assets by asset type",
|
|
53
|
+
endpoint: "fund.asset-allocation",
|
|
54
|
+
fields: [code(), ...range("Report date"), list("--asset-level <level>", "level1 / level2 (repeatable); omit for both", "assetLevelList"), format(), output()],
|
|
55
|
+
});
|
|
56
|
+
query(fund, "stock-portfolio", {
|
|
57
|
+
description: "Stock holdings per report date: shares, value, % of float and of NAV, change vs the previous report",
|
|
58
|
+
endpoint: "fund.stock-portfolio",
|
|
59
|
+
fields: [code(), ...range("Report date"), positionType(), format(), output()],
|
|
60
|
+
});
|
|
61
|
+
query(fund, "industry-allocation", {
|
|
62
|
+
description: "Stock holdings grouped by level-1 industry (SW or CITIC), as % of NAV",
|
|
63
|
+
endpoint: "fund.industry-allocation",
|
|
64
|
+
fields: [code(), ...range("Report date"), value("--industry-standard <name>", "swIndustry = SW level 1 (server default) / citicIndustry = CITIC level 1", "industryStandard"), positionType(), format(), output()],
|
|
65
|
+
});
|
|
66
|
+
query(fund, "bond-portfolio", {
|
|
67
|
+
description: "Bond holdings per report date: quantity, value, % of NAV",
|
|
68
|
+
endpoint: "fund.bond-portfolio",
|
|
69
|
+
fields: [code(), ...range("Report date"), format(), output()],
|
|
70
|
+
});
|
|
71
|
+
query(fund, "bond-type-allocation", {
|
|
72
|
+
description: "Bond holdings grouped by bond type, as % of NAV",
|
|
73
|
+
endpoint: "fund.bond-type-allocation",
|
|
74
|
+
fields: [code(), ...range("Report date"), format(), output()],
|
|
75
|
+
});
|
|
76
|
+
query(fund, "fund-portfolio", {
|
|
77
|
+
description: "Fund-of-funds holdings of other funds: value and % of NAV",
|
|
78
|
+
endpoint: "fund.fund-portfolio",
|
|
79
|
+
fields: [code(), ...range("Report date"), format(), output()],
|
|
80
|
+
});
|
|
81
|
+
query(fund, "fund-type-allocation", {
|
|
82
|
+
description: "Fund-of-funds holdings grouped by fund category (level 1), as % of NAV",
|
|
83
|
+
endpoint: "fund.fund-type-allocation",
|
|
84
|
+
fields: [code(), ...range("Report date"), format(), output()],
|
|
85
|
+
});
|
|
86
|
+
query(fund, "etf-pcf-header", {
|
|
87
|
+
description: "ETF creation/redemption parameters for the latest trade date: unit size, daily limits, cash components",
|
|
88
|
+
endpoint: "fund.etf-pcf-header",
|
|
89
|
+
fields: [code(), format(), output()],
|
|
90
|
+
});
|
|
91
|
+
query(fund, "etf-pcf-components", {
|
|
92
|
+
description: "ETF creation/redemption component list for the latest trade date: quantities, weights, cash substitution",
|
|
93
|
+
endpoint: "fund.etf-pcf-components",
|
|
94
|
+
fields: [code(), format(), output()],
|
|
95
|
+
});
|
|
96
|
+
query(fund, "etf-share-change", {
|
|
97
|
+
description: "ETF shares and scale per trade date, with the change vs the previous one",
|
|
98
|
+
endpoint: "fund.etf-share-change",
|
|
99
|
+
fields: [code(), ...range("Trade date"), format(), output()],
|
|
100
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
2
|
import { beijingDateString, collectList, dateArg, maybeArray, parseOptionalNumberOption } from "../core/args.js";
|
|
3
|
+
import { COSTLY_FETCH_CREDITS } from "../core/endpoints.js";
|
|
3
4
|
import { normalizeRows } from "../core/normalize.js";
|
|
4
5
|
import { parseOutputFormat } from "../core/output.js";
|
|
5
6
|
import { printData } from "../core/printer.js";
|
|
@@ -128,6 +129,7 @@ fundamental.command("earning-forecast")
|
|
|
128
129
|
.option("--start-date <date>", "Start date (default: 1 year before end-date)", dateArg("--start-date"))
|
|
129
130
|
.option("--end-date <date>", "End date (default: today)", dateArg("--end-date"))
|
|
130
131
|
.option("--consensus <name>", "Consensus indicator: netIncome/netIncomeYoy/eps/pe/bps/pb/peg/roe/ps", collectList, [])
|
|
132
|
+
.option("--yes", `Fetch a range estimated above ${COSTLY_FETCH_CREDITS} credits (billed per row; see the refusal for the estimate)`)
|
|
131
133
|
.option("--format <format>", "Output format", "table")
|
|
132
134
|
.option("--output <path>")
|
|
133
135
|
.action((options) => emit(options, (client) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ai } from "./ai.js";
|
|
2
|
+
import { alternative } from "./alternative.js";
|
|
3
|
+
import { auth, lookup } from "./auth.js";
|
|
4
|
+
import { bond } from "./bond.js";
|
|
5
|
+
import { fund } from "./fund.js";
|
|
6
|
+
import { fundamental } from "./fundamental.js";
|
|
7
|
+
import { indicator } from "./indicator.js";
|
|
8
|
+
import { insight } from "./insight.js";
|
|
9
|
+
import { quote } from "./quote.js";
|
|
10
|
+
import { raw } from "./raw.js";
|
|
11
|
+
import { reference } from "./reference.js";
|
|
12
|
+
import { tool } from "./tool.js";
|
|
13
|
+
import { vault } from "./vault.js";
|
|
14
|
+
export const COMMAND_GROUPS = [auth, lookup, insight, quote, fundamental, bond, fund, reference, vault, ai, alternative, indicator, tool, raw];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
|
-
import { collectList, dateArg, screenerExpressionFields, parseNumberOption, parseScreenerIndicators } from "../core/args.js";
|
|
2
|
+
import { collectList, collectNames, dateArg, screenerExpressionFields, parseNumberOption, parseScreenerIndicators } from "../core/args.js";
|
|
3
3
|
import { buildIndicatorCrossSectionBody, buildIndicatorScreenerBody, buildIndicatorTimeSeriesBody } from "../core/commandBodies.js";
|
|
4
4
|
import { checkScreenerBindings, droppedFromMatrix, flattenCrossSection, flattenTimeSeries, isEmptyMatrix, requireIndicatorMatrix, unwrapIndicatorData } from "../core/indicatorMatrix.js";
|
|
5
5
|
import { resolveCalendarType } from "../core/calendarType.js";
|
|
@@ -56,7 +56,7 @@ indicator.command("cross-section")
|
|
|
56
56
|
.requiredOption("--date <date>", "Data date (yyyy-MM-dd); sent as each indicator's tradeDate. Report-period indicators (is_*, financial statements) REJECT tradeDate and require --indicator-param 'code:reportDate=yyyy-MM-dd' instead — check parameterList in 'indicator search'", dateArg("--date"))
|
|
57
57
|
.option("--currency <code>", "Currency: DFT/CNY/HKD/USD/EUR/GBP/JPY/TWD/MOP/AUD (default DFT)")
|
|
58
58
|
.option("--scale <code>", "Scale: 0=个 3=千 4=万 6=百万 8=亿 9=十亿 (default 0)")
|
|
59
|
-
.option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustType=2 for 前复权 (repeat); read exact keys from 'indicator search'. Bare 'code:' (nothing after the colon) declares the indicator takes NO date — required by any indicator whose parameterList has no date key at all, which otherwise rejects the tradeDate --date injects: the pty_* / scr_* static-attribute families (pty_op_scope, scr_exchg_mkt, scr_isin …), plus div_cash_paid_ratio / div_cash_yr (add 'code:fiscalYear=YYYY' too) and pty_shr_reg. It composes with real params, so 'code:' + 'code:scale=8' keeps the scale",
|
|
59
|
+
.option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustType=2 for 前复权 (repeat); read exact keys from 'indicator search'. Bare 'code:' (nothing after the colon) declares the indicator takes NO date — required by any indicator whose parameterList has no date key at all, which otherwise rejects the tradeDate --date injects: the pty_* / scr_* static-attribute families (pty_op_scope, scr_exchg_mkt, scr_isin …), plus div_cash_paid_ratio / div_cash_yr (add 'code:fiscalYear=YYYY' too) and pty_shr_reg. It composes with real params, so 'code:' + 'code:scale=8' keeps the scale", collectNames, [])
|
|
60
60
|
.addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode, unique & order-stable for batch code→value mapping").choices(["name", "code"]).default("name"))
|
|
61
61
|
.option("--format <format>", "Output format", "table")
|
|
62
62
|
.option("--output <path>")
|
|
@@ -77,7 +77,7 @@ indicator.command("time-series")
|
|
|
77
77
|
.option("--calendar-type <type>", "Calendar: ND=natural TD=trading WD=weekday. Omit it and the CLI picks: a free 'indicator search' reads each parameterList and sends TD only when EVERY indicator is trading-day typed, otherwise it leaves the server on ND (report-period indicators land on dates a trading calendar lacks, and TD would answer them with an all-null grid). An explicit value is sent as given, with no lookup")
|
|
78
78
|
.option("--currency <code>", "Currency: DFT/CNY/HKD/USD/EUR/GBP/JPY/TWD/MOP/AUD (default DFT)")
|
|
79
79
|
.option("--scale <code>", "Scale: 0=个 3=千 4=万 6=百万 8=亿 9=十亿 (default 0)")
|
|
80
|
-
.option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustType=2 for 前复权 (repeat); read exact keys from 'indicator search'",
|
|
80
|
+
.option("--indicator-param <spec>", "Per-indicator param 'code:key=value', e.g. qte_close:adjustType=2 for 前复权 (repeat); read exact keys from 'indicator search'", collectNames, [])
|
|
81
81
|
.addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode/securityCode, unique & order-stable for batch mapping").choices(["name", "code"]).default("name"))
|
|
82
82
|
.option("--format <format>", "Output format", "table")
|
|
83
83
|
.option("--output <path>")
|
|
@@ -100,11 +100,11 @@ indicator.command("time-series")
|
|
|
100
100
|
}));
|
|
101
101
|
indicator.command("screener")
|
|
102
102
|
.description("Screen securities by an expression over indicator values (条件选股)")
|
|
103
|
-
.option("--indicator <spec>", "Bind a variable to an indicator, 'F1:code', e.g. F1:qte_mkt_cptl (REQUIRED, repeat)",
|
|
103
|
+
.option("--indicator <spec>", "Bind a variable to an indicator, 'F1:code', e.g. F1:qte_mkt_cptl (REQUIRED, repeat)", collectNames, [])
|
|
104
104
|
.option("--security <code>", "Security code, e.g. 600519.SH, or a sector ID from 'gangtise reference sector-search' (REQUIRED, repeat; union, deduped)", collectList, [])
|
|
105
105
|
.requiredOption("--expression <expr>", "Filter over the bound variables, e.g. 'F1 >= 800 && (F2 >= 20 && F2 <= 30)'; also supports contains/notcontains on string indicators")
|
|
106
106
|
.requiredOption("--date <date>", "Data date (yyyy-MM-dd); sent as every indicator's tradeDate unless it already has one. Report-period indicators (is_*) reject tradeDate: give them --indicator-param 'F1:reportDate=yyyy-MM-dd'", dateArg("--date"))
|
|
107
|
-
.option("--indicator-param <spec>", "Per-variable param 'F1:key=value', e.g. F1:scale=8 (repeat); read exact keys from 'indicator search'. Bare 'F1:' (nothing after the colon) declares that the indicator takes NO date — required by any indicator whose parameterList has no date key at all, which otherwise rejects the tradeDate --date injects: the pty_* / scr_* static-attribute families (pty_op_scope, scr_exchg_sctr, scr_isin …), plus div_cash_paid_ratio / div_cash_yr (add 'F1:fiscalYear=YYYY' too) and pty_shr_reg. It composes with real params, so 'F1:' + 'F1:scale=8' keeps the scale",
|
|
107
|
+
.option("--indicator-param <spec>", "Per-variable param 'F1:key=value', e.g. F1:scale=8 (repeat); read exact keys from 'indicator search'. Bare 'F1:' (nothing after the colon) declares that the indicator takes NO date — required by any indicator whose parameterList has no date key at all, which otherwise rejects the tradeDate --date injects: the pty_* / scr_* static-attribute families (pty_op_scope, scr_exchg_sctr, scr_isin …), plus div_cash_paid_ratio / div_cash_yr (add 'F1:fiscalYear=YYYY' too) and pty_shr_reg. It composes with real params, so 'F1:' + 'F1:scale=8' keeps the scale", collectNames, [])
|
|
108
108
|
.addOption(new Option("--key-by <mode>", "Column key: name=display name (default) | code=indicatorCode").choices(["name", "code"]).default("name"))
|
|
109
109
|
.option("--format <format>", "Output format", "table")
|
|
110
110
|
.option("--output <path>")
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { collectList, dateArg, maybeArray, parseChoiceList, parseFrom, parseSize, parseTimestamp13 } from "../core/args.js";
|
|
2
|
+
import { collectList, dateArg, maybeArray, parseChoiceList, parseEndTimestamp13, parseFrom, parseSize, parseTimestamp13 } from "../core/args.js";
|
|
3
3
|
import { ValidationError } from "../core/errors.js";
|
|
4
4
|
import { fetchOpinionDetails } from "../core/opinionDetail.js";
|
|
5
5
|
import { rowCount } from "../core/rowSink.js";
|
|
6
|
-
import { emit, addDownloadCommand, confirmCostly, multiChoice, value, required, list, numberList, choiceList, count, rankType, searchType, top, flag, format, output, from, size, startTime, endTime, timeFilters, query } from "./shared.js";
|
|
6
|
+
import { emit, addDownloadCommand, confirmCostly, multiChoice, value, required, list, numberList, choiceList, count, rankType, searchType, top, flag, format, output, from, size, startTime, endTime, timeFilters, query, RESEARCH_AREA_CITIC_OR_DIRECTION } from "./shared.js";
|
|
7
7
|
export const insight = new Command("insight").description("Insight APIs");
|
|
8
|
-
const RESEARCH_AREA_CITIC_OR_DIRECTION = "Research area ID: citicIndustry code (1008001xx) or gangtiseIndustry direction code (122000xxx: macro/strategy/fixed-income/quant/overseas). swIndustry (104xx0000) returns 0 here";
|
|
9
8
|
const SW_INDUSTRY_ONLY = "Industry ID -- swIndustry codes only (104xx0000); citicIndustry codes are rejected with 100005 even where constant-category declares them";
|
|
10
9
|
const opinion = new Command("opinion");
|
|
11
10
|
const summary = new Command("summary");
|
|
@@ -204,7 +203,7 @@ query(announcement, "list", {
|
|
|
204
203
|
list("--security <code>", "Security code", "securityList"),
|
|
205
204
|
list("--category <id>", "Category ID", "categoryList"),
|
|
206
205
|
format(), output("Output path"),
|
|
207
|
-
...timeFilters(parseTimestamp13),
|
|
206
|
+
...timeFilters(parseTimestamp13, parseEndTimestamp13),
|
|
208
207
|
],
|
|
209
208
|
});
|
|
210
209
|
addDownloadCommand(announcement, { endpointKey: "insight.announcement.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement", fileType: { description: "File type: 1=PDF 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.announcement.list" });
|
package/dist/src/commands/raw.js
CHANGED
|
@@ -13,16 +13,17 @@ export const raw = new Command("raw").description("Raw API calls").addCommand(ne
|
|
|
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, yes: Boolean(options.yes) });
|
|
17
16
|
let body;
|
|
18
17
|
if (options.body) {
|
|
19
18
|
try {
|
|
20
19
|
body = JSON.parse(options.body);
|
|
21
20
|
}
|
|
22
|
-
catch {
|
|
23
|
-
|
|
21
|
+
catch (error) {
|
|
22
|
+
const position = /position (\d+)/.exec(error instanceof Error ? error.message : "")?.[1];
|
|
23
|
+
throw new ConfigError(`Invalid JSON in --body${position === undefined ? "" : ` (at character ${position})`}; the body is not echoed because it may hold credentials`);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
const client = await createClient({ format, output: options.output, yes: Boolean(options.yes) });
|
|
26
27
|
if (endpoint.kind === "download") {
|
|
27
28
|
if (body !== undefined && endpoint.method !== "POST") {
|
|
28
29
|
throw new ValidationError(`--body is not supported for GET download endpoints (use --query key=value); ${endpointKey} is kind=download`);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { Option } from "commander";
|
|
2
4
|
import { QUOTE_MAX_LIMIT } from "../core/quoteSharding.js";
|
|
3
5
|
import { collectList, dateArg, datetimeArg, maybeArray, numberListArg, parseChoiceList, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize } from "../core/args.js";
|
|
4
6
|
import { loadConfig } from "../core/config.js";
|
|
5
|
-
import { releaseClaim, resolveTitle, saveDownloadResult, uniquePath } from "../core/download.js";
|
|
7
|
+
import { placeStreamedDownload, releaseClaim, resolveTitle, saveDownloadResult, uniquePath } from "../core/download.js";
|
|
6
8
|
import { ENDPOINTS } from "../core/endpoints.js";
|
|
7
9
|
import { ValidationError } from "../core/errors.js";
|
|
8
|
-
import { parseOutputFormat } from "../core/output.js";
|
|
10
|
+
import { parseOutputFormat, stagingPath } from "../core/output.js";
|
|
9
11
|
import { printData } from "../core/printer.js";
|
|
10
12
|
import { ExportSink, getRowSink } from "../core/rowSink.js";
|
|
11
13
|
export async function createClient(stream) {
|
|
@@ -91,7 +93,21 @@ export async function runDownload(client, endpointKey, query, options) {
|
|
|
91
93
|
await saveDownloadResult(result, options.fallbackName, options.output);
|
|
92
94
|
return;
|
|
93
95
|
}
|
|
94
|
-
const
|
|
96
|
+
const temp = stagingPath(path.resolve(".gangtise-download"), "part");
|
|
97
|
+
const result = await client.call(endpointKey, options.body, query, { streamTo: temp });
|
|
98
|
+
const streamed = result;
|
|
99
|
+
if (typeof streamed?.savedPath === "string") {
|
|
100
|
+
let named;
|
|
101
|
+
try {
|
|
102
|
+
named = options.resolveOutputPath ? await options.resolveOutputPath(result) : undefined;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
await fs.unlink(streamed.savedPath).catch(() => { });
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
process.stdout.write(`${await placeStreamedDownload({ ...streamed, savedPath: streamed.savedPath }, options.fallbackName, named)}\n`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
95
111
|
const resolved = options.resolveOutputPath ? await options.resolveOutputPath(result) : undefined;
|
|
96
112
|
const target = resolved ? await uniquePath(resolved) : undefined;
|
|
97
113
|
try {
|
|
@@ -139,8 +155,9 @@ export function assertConfirmed(endpointKey, confirmed, target) {
|
|
|
139
155
|
return;
|
|
140
156
|
throw new ValidationError(`${destructive.warning} 确认要对 ${target} 执行就加上 --yes。`);
|
|
141
157
|
}
|
|
158
|
+
export const RESEARCH_AREA_CITIC_OR_DIRECTION = "Research area ID: citicIndustry code (1008001xx) or gangtiseIndustry direction code (122000xxx: macro/strategy/fixed-income/quant/overseas). swIndustry (104xx0000) returns 0 here";
|
|
142
159
|
export const field = (option, body) => ({ option, body });
|
|
143
|
-
export const confirmCostly = () => field(new Option("--yes", "Fetch
|
|
160
|
+
export const confirmCostly = () => field(new Option("--yes", "Fetch past the credit guard: a fetch without --size, or with a large one, estimated above 1000 credits (the list is billed per row)"));
|
|
144
161
|
export const value = (flags, description, key) => field(new Option(flags, description), (v) => ({ [key]: v }));
|
|
145
162
|
export const required = (flags, description, key) => field(new Option(flags, description).makeOptionMandatory(), (v) => ({ [key]: v }));
|
|
146
163
|
export const date = (flags, description, key, opts = {}) => {
|
|
@@ -149,6 +166,14 @@ export const date = (flags, description, key, opts = {}) => {
|
|
|
149
166
|
return field(opts.required ? option.makeOptionMandatory() : option, (v) => ({ [key]: v }));
|
|
150
167
|
};
|
|
151
168
|
export const list = (flags, description, key) => field(new Option(flags, description).argParser(collectList).default([]), (v) => ({ [key]: maybeArray(v) }));
|
|
169
|
+
export const requiredList = (flags, description, key, parse = collectList) => {
|
|
170
|
+
const option = new Option(flags, description).argParser(parse).makeOptionMandatory();
|
|
171
|
+
return field(option, (v) => {
|
|
172
|
+
if (!v.length)
|
|
173
|
+
throw new ValidationError(`${option.long} is empty: pass at least one value`);
|
|
174
|
+
return { [key]: v };
|
|
175
|
+
});
|
|
176
|
+
};
|
|
152
177
|
export const numberList = (flags, description, key) => {
|
|
153
178
|
const option = new Option(flags, description).default([]);
|
|
154
179
|
option.argParser(numberListArg(option.long));
|
|
@@ -183,7 +208,7 @@ export const size = (description = "Total rows to return; omit to fetch all") =>
|
|
|
183
208
|
const asTyped = (v) => v;
|
|
184
209
|
export const startTime = (description = "Start time", toBody = asTyped) => field(new Option("--start-time <datetime>", description).argParser(datetimeArg("--start-time")), (v) => ({ startTime: toBody(v, "--start-time") }));
|
|
185
210
|
export const endTime = (description = "End time", toBody = asTyped) => field(new Option("--end-time <datetime>", description).argParser(datetimeArg("--end-time")), (v) => ({ endTime: toBody(v, "--end-time") }));
|
|
186
|
-
export const timeFilters = (toBody = asTyped) => [from(), size(), startTime("Start time", toBody), endTime("End time",
|
|
211
|
+
export const timeFilters = (toBody = asTyped, toEndBody = toBody) => [from(), size(), startTime("Start time", toBody), endTime("End time", toEndBody), value("--keyword <keyword>", "Keyword", "keyword")];
|
|
187
212
|
export const requestBody = (fields, options) => Object.assign({}, ...fields.map((f) => f.body?.(options[f.option.attributeName()]) ?? {}));
|
|
188
213
|
export function query(parent, name, spec) {
|
|
189
214
|
const command = parent.command(name);
|
|
@@ -193,5 +218,9 @@ export function query(parent, name, spec) {
|
|
|
193
218
|
const fields = defaultEndpoint?.pagination?.enabled && defaultEndpoint.billing?.per === "row" ? [...spec.fields, confirmCostly()] : spec.fields;
|
|
194
219
|
for (const f of fields)
|
|
195
220
|
command.addOption(f.option);
|
|
196
|
-
return command.action(
|
|
221
|
+
return command.action(async (options) => {
|
|
222
|
+
const body = requestBody(fields, options);
|
|
223
|
+
const endpoint = typeof spec.endpoint === "string" ? spec.endpoint : spec.endpoint(options);
|
|
224
|
+
return emit(options, (client) => client.call(endpoint, body), spec.cache);
|
|
225
|
+
});
|
|
197
226
|
}
|
|
@@ -33,7 +33,7 @@ tool.command("file-parse-check").description("Download a finished file-parse res
|
|
|
33
33
|
}));
|
|
34
34
|
tool.command("web-search").description("Search the public web for research: deduped, source-tiered (T0-T3) results with optional page content (1 credit/call)")
|
|
35
35
|
.requiredOption("--query <text>", "Search query, 1-200 chars. Sent verbatim — no intent rewriting server-side, so spell out what you want")
|
|
36
|
-
.option("--size <number>", "Results to return, 1-20 (1-5 with --include-content)
|
|
36
|
+
.option("--size <number>", "Results to return, 1-20 (1-5 with --include-content); default 10, or 5 with --include-content")
|
|
37
37
|
.addOption(new Option("--freshness <window>", "Time window over publishTime; results with no resolvable publish date are dropped by day/week/month").choices(["day", "week", "month", "none"]))
|
|
38
38
|
.addOption(new Option("--min-tier <tier>", "Lowest source tier to keep; T3 = no filtering").choices(["T0", "T1", "T2", "T3"]))
|
|
39
39
|
.option("--site <domain>", "Restrict to a registered domain or subdomain, e.g. csrc.gov.cn (repeatable, max 10, OR-ed)", collectList, [])
|
|
@@ -41,7 +41,7 @@ tool.command("web-search").description("Search the public web for research: dedu
|
|
|
41
41
|
.option("--max-content-chars <number>", "Max chars per body, 1000-20000 (only with --include-content)")
|
|
42
42
|
.option("--format <format>", "Output format", "table").option("--output <path>")
|
|
43
43
|
.action((options) => emit(options, (client) => {
|
|
44
|
-
const size = parseNumberOption(options.size, "--size", { integer: true, min: 1, max: 20 });
|
|
44
|
+
const size = parseNumberOption(options.size ?? (options.includeContent ? "5" : "10"), "--size", { integer: true, min: 1, max: 20 });
|
|
45
45
|
if (options.includeContent && size > 5)
|
|
46
46
|
throw new ValidationError(`--size ${size} with --include-content: this endpoint caps size at 5 when page bodies are requested. Lower --size, or drop --include-content to fetch up to 20 snippets.`);
|
|
47
47
|
const sites = [...new Set(options.site)];
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
|
-
import { collectList,
|
|
3
|
-
import { buildStockPoolStocksBody, buildWechatChatroomListBody, buildWechatMessageListBody } from "../core/commandBodies.js";
|
|
2
|
+
import { collectList, collectNames } from "../core/args.js";
|
|
4
3
|
import { uploadDriveFile } from "../core/driveUpload.js";
|
|
5
4
|
import { flagFailedItems } from "../core/normalize.js";
|
|
6
|
-
import { emit, addDownloadCommand, assertConfirmed, field, value, required, list, numberList, format, output, from, size, startTime, endTime, query } from "./shared.js";
|
|
5
|
+
import { emit, addDownloadCommand, assertConfirmed, field, value, required, list, numberList, format, output, from, size, startTime, endTime, query, RESEARCH_AREA_CITIC_OR_DIRECTION } from "./shared.js";
|
|
7
6
|
export const vault = new Command("vault").description("Vault APIs");
|
|
8
7
|
query(vault, "drive-list", {
|
|
9
8
|
endpoint: "vault.drive.list",
|
|
@@ -117,7 +116,7 @@ query(vault, "my-conference-list", {
|
|
|
117
116
|
cache: { endpointKey: "vault.my-conference.list", idField: "conferenceId" },
|
|
118
117
|
fields: [
|
|
119
118
|
from(), size(), startTime(), endTime(), value("--keyword <text>", undefined, "keyword"),
|
|
120
|
-
list("--research-area <id>",
|
|
119
|
+
list("--research-area <id>", RESEARCH_AREA_CITIC_OR_DIRECTION, "researchAreaList"),
|
|
121
120
|
list("--security <code>", "Security code", "securityList"),
|
|
122
121
|
list("--institution <id>", "Institution ID", "institutionList"),
|
|
123
122
|
list("--category <name>", "Conference category: earningsCall/strategyMeeting/fundRoadshow/shareholdersMeeting/maMeeting/specialMeeting/companyAnalysis/industryAnalysis/other", "categoryList"),
|
|
@@ -126,33 +125,34 @@ query(vault, "my-conference-list", {
|
|
|
126
125
|
],
|
|
127
126
|
});
|
|
128
127
|
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
|
-
vault
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.action((options) => emit(options, (client) => client.call("vault.wechat-chatroom.list", buildWechatChatroomListBody(options))));
|
|
128
|
+
query(vault, "wechat-message-list", {
|
|
129
|
+
endpoint: "vault.wechat-message.list",
|
|
130
|
+
fields: [
|
|
131
|
+
from(), size(), startTime(), endTime(), value("--keyword <text>", undefined, "keyword"),
|
|
132
|
+
list("--security <code>", "Security code (e.g. 000001.SZ)", "securityList"),
|
|
133
|
+
list("--wechat-group-id <id>", "WeChat group ID", "wechatGroupIdList"),
|
|
134
|
+
list("--industry <id>", "Industry ID -- citicIndustry codes (1008001xx) only; swIndustry codes and unknown values are rejected with 100005", "industryIdList"),
|
|
135
|
+
list("--category <name>", "Message type: text/image/documents/url", "categoryList"),
|
|
136
|
+
list("--tag <name>", "Tag: roadShow/research/strategyMeeting/meetingSummary/industryComment/companyComment/earningsReview", "tagList"),
|
|
137
|
+
format(), output(),
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
query(vault, "wechat-chatroom-list", {
|
|
141
|
+
endpoint: "vault.wechat-chatroom.list",
|
|
142
|
+
fields: [
|
|
143
|
+
from(), size(),
|
|
144
|
+
field(new Option("--room-name <name>", "WeChat group name; repeat or comma-separate for multiple names").argParser(collectNames).default([]), (v) => ({ roomName: v.length > 0 ? v.join(",") : undefined })),
|
|
145
|
+
format(), output(),
|
|
146
|
+
],
|
|
147
|
+
});
|
|
150
148
|
query(vault, "stock-pool-list", { endpoint: "vault.stock-pool.list", fields: [format(), output()] });
|
|
151
|
-
vault
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
query(vault, "stock-pool-stocks", {
|
|
150
|
+
endpoint: "vault.stock-pool.stocks",
|
|
151
|
+
fields: [
|
|
152
|
+
field(new Option("--pool-id <id>", "Pool ID; repeat for multiple; omit (or 'all') for all pools").argParser(collectList), (v) => ({ poolIdList: v?.length ? v : ["all"] })),
|
|
153
|
+
format(), output(),
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
156
|
query(vault, "stock-pool-create", {
|
|
157
157
|
endpoint: "vault.stock-pool.create",
|
|
158
158
|
fields: [
|
package/dist/src/core/args.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { ValidationError } from "./errors.js";
|
|
2
2
|
export function splitCsv(value) {
|
|
3
3
|
return value
|
|
4
|
-
.split(/[
|
|
4
|
+
.split(/[,,、;;\s]+/)
|
|
5
5
|
.map((item) => item.trim())
|
|
6
6
|
.filter(Boolean);
|
|
7
7
|
}
|
|
8
8
|
export function collectList(value, previous = []) {
|
|
9
|
-
return [...previous, ...splitCsv(value)];
|
|
9
|
+
return unique([...previous, ...splitCsv(value)]);
|
|
10
|
+
}
|
|
11
|
+
function unique(values) {
|
|
12
|
+
return [...new Set(values)];
|
|
13
|
+
}
|
|
14
|
+
export function collectNames(value, previous = []) {
|
|
15
|
+
return unique([...previous, ...value.split(/[,,]/).map((item) => item.trim()).filter(Boolean)]);
|
|
10
16
|
}
|
|
11
17
|
export function collectText(value, previous = []) {
|
|
12
18
|
const text = value.trim();
|
|
@@ -44,10 +50,12 @@ export function parseSize(value) {
|
|
|
44
50
|
return parseOptionalNumberOption(value, "--size", { integer: true, min: 1 });
|
|
45
51
|
}
|
|
46
52
|
export function numberListArg(optionName) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const parse = (value, previous = []) => [...new Set([
|
|
54
|
+
...previous,
|
|
55
|
+
...splitCsv(value).map((item) => parseNumberOption(item, optionName, { integer: true })),
|
|
56
|
+
])];
|
|
57
|
+
ACCUMULATING_PARSERS.add(parse);
|
|
58
|
+
return parse;
|
|
51
59
|
}
|
|
52
60
|
export function collectKeyValue(value, previous = {}) {
|
|
53
61
|
const index = value.indexOf("=");
|
|
@@ -155,6 +163,13 @@ export function toTimestamp13(value) {
|
|
|
155
163
|
const instant = new Date(`${y}-${mo}-${d}T${hh}:${mi}:${ss}${BEIJING_OFFSET}`).getTime();
|
|
156
164
|
return Number.isNaN(instant) ? undefined : instant;
|
|
157
165
|
}
|
|
166
|
+
export function parseEndTimestamp13(value, optionName) {
|
|
167
|
+
const parsed = parseTimestamp13(value, optionName);
|
|
168
|
+
if (parsed === undefined || value === undefined)
|
|
169
|
+
return parsed;
|
|
170
|
+
const parts = LOCAL_DATETIME.exec(value);
|
|
171
|
+
return parts && parts[5] === undefined ? parsed + 86_400_000 - 1 : parsed;
|
|
172
|
+
}
|
|
158
173
|
export function parseTimestamp13(value, optionName) {
|
|
159
174
|
if (value === undefined)
|
|
160
175
|
return undefined;
|
|
@@ -204,7 +219,7 @@ function parseParamSpecs(specs, option, syntax) {
|
|
|
204
219
|
params = [];
|
|
205
220
|
groups.set(lhs, params);
|
|
206
221
|
}
|
|
207
|
-
params.push({ paramKey, paramValue });
|
|
222
|
+
params.push({ paramKey, paramValue: /date$/i.test(paramKey) ? parseDateOption(paramValue, `${option} "${spec}"`) : paramValue });
|
|
208
223
|
}
|
|
209
224
|
return { groups, noQueryDate };
|
|
210
225
|
}
|
|
@@ -322,3 +337,4 @@ export function parseScreenerIndicators(bindings, paramSpecs, expression) {
|
|
|
322
337
|
}
|
|
323
338
|
return [...indicators.values()];
|
|
324
339
|
}
|
|
340
|
+
export const ACCUMULATING_PARSERS = new WeakSet([collectList, collectNames, collectText, collectKeyValue]);
|
|
@@ -23,7 +23,7 @@ function terminalFailureLine(error) {
|
|
|
23
23
|
const trace = api?.traceId ? ` [trace ${api.traceId}]` : "";
|
|
24
24
|
const detail = api?.message ? `: ${api.message}` : "";
|
|
25
25
|
return `Content generation failed (terminal${code})${trace}${detail}\n`
|
|
26
|
-
+ "This dataId is final — re-checking it will not change. Resubmitting the generation task bills again
|
|
26
|
+
+ "This dataId is final — re-checking it will not change. Resubmitting the generation task bills again: if the message above points at a parameter or the content, change that first; a plain 业务处理失败 is not about the parameters — resubmit later only if you still need it.\n";
|
|
27
27
|
}
|
|
28
28
|
export async function pollUntilDone(attempt, notReady) {
|
|
29
29
|
for (let n = 1; n <= POLL_MAX_ATTEMPTS; n++) {
|
package/dist/src/core/auth.js
CHANGED
|
@@ -2,7 +2,7 @@ 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 {
|
|
5
|
+
import { writeFileAtomic } from "./output.js";
|
|
6
6
|
export function credentialFingerprint(accessKey, baseUrl) {
|
|
7
7
|
return createHash("sha256").update(`${accessKey}\u0000${baseUrl}`).digest("hex").slice(0, 16);
|
|
8
8
|
}
|
|
@@ -31,15 +31,7 @@ export function redactTokenCache(cache) {
|
|
|
31
31
|
}
|
|
32
32
|
export async function writeTokenCache(filePath, cache) {
|
|
33
33
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
34
|
-
|
|
35
|
-
await fs.writeFile(tmp, JSON.stringify(cache, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
36
|
-
try {
|
|
37
|
-
await fs.rename(tmp, filePath);
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
await fs.unlink(tmp).catch(() => { });
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
34
|
+
await writeFileAtomic(filePath, JSON.stringify(cache, null, 2), { mode: 0o600, suffix: "tmp" });
|
|
43
35
|
}
|
|
44
36
|
export function isTokenCacheValid(cache, bufferSeconds = 300, expectedFingerprint) {
|
|
45
37
|
if (!cache?.accessToken || !cache.expiresAt) {
|