gangtise-openapi-cli 0.51.0 → 0.52.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 +77 -49
- package/dist/src/cli.js +29 -8
- package/dist/src/commands/ai.js +12 -12
- package/dist/src/commands/alternative.js +2 -2
- package/dist/src/commands/bond.js +4 -4
- package/dist/src/commands/fund.js +1 -1
- package/dist/src/commands/fundamental.js +11 -11
- package/dist/src/commands/indicator.js +5 -5
- package/dist/src/commands/insight.js +38 -17
- package/dist/src/commands/quote.js +6 -6
- package/dist/src/commands/raw.js +3 -3
- package/dist/src/commands/reference.js +12 -3
- package/dist/src/commands/shared.js +20 -8
- package/dist/src/commands/tool.js +9 -4
- package/dist/src/commands/update.js +11 -0
- package/dist/src/commands/vault.js +15 -15
- package/dist/src/core/client.js +18 -2
- package/dist/src/core/download.js +12 -3
- package/dist/src/core/driveUpload.js +6 -2
- package/dist/src/core/dryRun.js +68 -0
- package/dist/src/core/endpoints.js +61 -6
- package/dist/src/core/etfComponents.js +45 -8
- package/dist/src/core/exitStatus.js +4 -0
- package/dist/src/core/fileParse.js +10 -3
- package/dist/src/core/printer.js +39 -7
- package/dist/src/core/statePaths.js +41 -0
- package/dist/src/core/transport.js +7 -0
- package/gangtise-openapi/SKILL.md +34 -154
- package/gangtise-openapi/references/billing.md +142 -0
- package/gangtise-openapi/references/commands/fund.md +3 -3
- package/gangtise-openapi/references/commands/fundamental.md +1 -0
- package/gangtise-openapi/references/commands/indicator.md +51 -8
- package/gangtise-openapi/references/commands/insight.md +31 -6
- package/gangtise-openapi/references/commands/reference-and-lookup.md +22 -4
- package/gangtise-openapi/references/commands/tool.md +2 -2
- package/gangtise-openapi/references/commands/vault.md +1 -1
- package/gangtise-openapi/references/errors.md +2 -2
- package/gangtise-openapi/references/examples.md +3 -3
- package/gangtise-openapi/references/lookup-ids.md +13 -0
- package/gangtise-openapi/references/response-schema.md +4 -1
- package/package.json +2 -2
|
@@ -3,13 +3,13 @@ import { collectList, collectNames, parseFrom, parseSize } from "../core/args.js
|
|
|
3
3
|
import { uploadDriveFile } from "../core/driveUpload.js";
|
|
4
4
|
import { ValidationError } from "../core/errors.js";
|
|
5
5
|
import { flagFailedItems } from "../core/normalize.js";
|
|
6
|
-
import { emit, addDownloadCommand, assertConfirmed, field, value, required, requiredList, list, numberList, format, output, from, size, startTime, endTime, query, RESEARCH_AREA_CITIC_OR_DIRECTION } from "./shared.js";
|
|
6
|
+
import { emit, addDownloadCommand, assertConfirmed, field, value, required, requiredList, list, numberList, format, output, from, size, startTime, endTime, query, RESEARCH_AREA_CITIC_OR_DIRECTION, OUTPUT_HELP } from "./shared.js";
|
|
7
7
|
export const vault = new Command("vault").description("Vault APIs");
|
|
8
8
|
query(vault, "drive-list", {
|
|
9
9
|
endpoint: "vault.drive.list",
|
|
10
10
|
cache: { endpointKey: "vault.drive.list", idField: "fileId" },
|
|
11
11
|
fields: [
|
|
12
|
-
from(), size(), startTime(), endTime(), value("--keyword <text>",
|
|
12
|
+
from(), size(), startTime(), endTime(), value("--keyword <text>", "Keyword", "keyword"),
|
|
13
13
|
numberList("--file-type <number>", "File type", "fileTypeList"),
|
|
14
14
|
numberList("--space-type <number>", "Space type", "spaceTypeList"),
|
|
15
15
|
format(), output(),
|
|
@@ -32,7 +32,7 @@ vault.command("drive-upload").description("Upload a file to the AI drive (free;
|
|
|
32
32
|
.addOption(driveSpaceOption("Target space: 1=my drive 2=tenant drive (shared with your whole tenant)"))
|
|
33
33
|
.option("--folder-id <id>", "Target folder ID in that space; omit (or 'root') for the space's root")
|
|
34
34
|
.option("--title <name>", "Name in the drive, max 200 characters; defaults to the local file name")
|
|
35
|
-
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
35
|
+
.option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
|
|
36
36
|
.action((options) => emit(options, (client) => uploadDriveFile(client, options.file, { spaceType: Number(options.spaceType), folderId: options.folderId, title: options.title })));
|
|
37
37
|
query(vault, "drive-create-folder", {
|
|
38
38
|
description: "Create a drive folder (free). Same-name folders are allowed, so re-running creates a second one",
|
|
@@ -57,7 +57,7 @@ query(vault, "drive-rename", {
|
|
|
57
57
|
vault.command("drive-move-file").description("Move drive files into a folder of the SAME space (free)")
|
|
58
58
|
.requiredOption("--file-id <id>", "File ID (repeat or comma-separate)", collectList)
|
|
59
59
|
.requiredOption("--target-folder-id <id>", "Destination folder ID, or 'root'. Files of the other space land in failList (空间不一致)")
|
|
60
|
-
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
60
|
+
.option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
|
|
61
61
|
.action((options) => emit(options, async (client) => {
|
|
62
62
|
const data = await client.call("vault.drive.move-file", { fileIdList: options.fileId, targetFolderId: options.targetFolderId });
|
|
63
63
|
flagFailedItems(data, "vault drive-move-file");
|
|
@@ -77,7 +77,7 @@ vault.command("drive-copy").description("Copy files, or one folder with everythi
|
|
|
77
77
|
.option("--target-folder-id <id>", "Destination folder ID in the other space, or 'root' for that space's root")
|
|
78
78
|
.option("--folder-id <id>", "Source folder ID; use with --target-parent-id")
|
|
79
79
|
.option("--target-parent-id <id>", "Destination parent folder ID in the other space, or 'root' for that space's root")
|
|
80
|
-
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
80
|
+
.option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
|
|
81
81
|
.action((options) => {
|
|
82
82
|
const byFile = options.fileId.length > 0 || options.targetFolderId !== undefined;
|
|
83
83
|
const byFolder = options.folderId !== undefined || options.targetParentId !== undefined;
|
|
@@ -101,7 +101,7 @@ vault.command("drive-copy").description("Copy files, or one folder with everythi
|
|
|
101
101
|
vault.command("drive-delete-file").description("Delete drive files — irreversible (free)")
|
|
102
102
|
.requiredOption("--file-id <id>", "File ID to delete (repeat or comma-separate)", collectList)
|
|
103
103
|
.option("--yes", "Required: confirm the deletion")
|
|
104
|
-
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
104
|
+
.option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
|
|
105
105
|
.action(async (options) => {
|
|
106
106
|
assertConfirmed("vault.drive.delete-file", Boolean(options.yes), options.fileId.join("、"));
|
|
107
107
|
await emit(options, async (client) => {
|
|
@@ -113,7 +113,7 @@ vault.command("drive-delete-file").description("Delete drive files — irreversi
|
|
|
113
113
|
vault.command("drive-delete-folder").description("Delete a drive folder AND every subfolder and file inside it — irreversible (free)")
|
|
114
114
|
.requiredOption("--folder-id <id>", "Folder ID to delete")
|
|
115
115
|
.option("--yes", "Required: confirm the deletion")
|
|
116
|
-
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
116
|
+
.option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
|
|
117
117
|
.action(async (options) => {
|
|
118
118
|
assertConfirmed("vault.drive.delete-folder", Boolean(options.yes), options.folderId);
|
|
119
119
|
await emit(options, (client) => client.call("vault.drive.delete-folder", { folderId: options.folderId }));
|
|
@@ -122,7 +122,7 @@ query(vault, "record-list", {
|
|
|
122
122
|
endpoint: "vault.record.list",
|
|
123
123
|
cache: { endpointKey: "vault.record.list", idField: "recordId" },
|
|
124
124
|
fields: [
|
|
125
|
-
from(), size(), startTime(), endTime(), value("--keyword <text>",
|
|
125
|
+
from(), size(), startTime(), endTime(), value("--keyword <text>", "Keyword", "keyword"),
|
|
126
126
|
list("--category <name>", "Recording type: upload/link/mobile/gtNote/pc/share", "categoryList"),
|
|
127
127
|
numberList("--space-type <number>", "Space type: 1=my records / 2=tenant records", "spaceTypeList"),
|
|
128
128
|
format(), output(),
|
|
@@ -133,7 +133,7 @@ query(vault, "my-conference-list", {
|
|
|
133
133
|
endpoint: "vault.my-conference.list",
|
|
134
134
|
cache: { endpointKey: "vault.my-conference.list", idField: "conferenceId" },
|
|
135
135
|
fields: [
|
|
136
|
-
from(), size(), startTime(), endTime(), value("--keyword <text>",
|
|
136
|
+
from(), size(), startTime(), endTime(), value("--keyword <text>", "Keyword", "keyword"),
|
|
137
137
|
list("--research-area <id>", RESEARCH_AREA_CITIC_OR_DIRECTION, "researchAreaList"),
|
|
138
138
|
list("--security <code>", "Security code", "securityList"),
|
|
139
139
|
list("--institution <id>", "Institution ID", "institutionList"),
|
|
@@ -146,7 +146,7 @@ addDownloadCommand(vault, { endpointKey: "vault.my-conference.download", name: "
|
|
|
146
146
|
query(vault, "wechat-message-list", {
|
|
147
147
|
endpoint: "vault.wechat-message.list",
|
|
148
148
|
fields: [
|
|
149
|
-
from(), size(), startTime(), endTime(), value("--keyword <text>",
|
|
149
|
+
from(), size(), startTime(), endTime(), value("--keyword <text>", "Keyword", "keyword"),
|
|
150
150
|
list("--security <code>", "Security code (e.g. 000001.SZ)", "securityList"),
|
|
151
151
|
list("--wechat-group-id <id>", "WeChat group ID", "wechatGroupIdList"),
|
|
152
152
|
list("--industry <id>", "Industry ID -- citicIndustry codes (1008001xx) only; swIndustry codes and unknown values are rejected with 100005", "industryIdList"),
|
|
@@ -193,7 +193,7 @@ vault.command("stock-pool-add-stock")
|
|
|
193
193
|
.requiredOption("--pool-id <id>", "Target pool ID from 'stock-pool-list'")
|
|
194
194
|
.requiredOption("--security <code>", "Security code, e.g. 600519.SH (repeat or comma-separate)", collectList)
|
|
195
195
|
.option("--format <format>", "Output format", "table")
|
|
196
|
-
.option("--output <path>")
|
|
196
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
197
197
|
.action((options) => emit(options, async (client) => {
|
|
198
198
|
const data = await client.call("vault.stock-pool.add-stock", { poolId: options.poolId, securityCodeList: options.security });
|
|
199
199
|
flagFailedItems(data, "vault stock-pool-add-stock");
|
|
@@ -203,7 +203,7 @@ vault.command("stock-pool-remove-stock")
|
|
|
203
203
|
.requiredOption("--pool-id <id>", "Target pool ID from 'stock-pool-list'")
|
|
204
204
|
.requiredOption("--security <code>", "Security code, e.g. 600519.SH (repeat or comma-separate)", collectList)
|
|
205
205
|
.option("--format <format>", "Output format", "table")
|
|
206
|
-
.option("--output <path>")
|
|
206
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
207
207
|
.action((options) => emit(options, async (client) => {
|
|
208
208
|
const data = await client.call("vault.stock-pool.remove-stock", { poolId: options.poolId, securityCodeList: options.security });
|
|
209
209
|
flagFailedItems(data, "vault stock-pool-remove-stock");
|
|
@@ -213,7 +213,7 @@ vault.command("stock-pool-delete")
|
|
|
213
213
|
.requiredOption("--pool-id <id>", "Pool ID to delete; repeat or comma-separate for multiple", collectList)
|
|
214
214
|
.option("--yes", "Required: confirm the deletion")
|
|
215
215
|
.option("--format <format>", "Output format", "table")
|
|
216
|
-
.option("--output <path>")
|
|
216
|
+
.option("--output <path>", OUTPUT_HELP)
|
|
217
217
|
.action(async (options) => {
|
|
218
218
|
assertConfirmed("vault.stock-pool.delete", Boolean(options.yes), options.poolId.join("、"));
|
|
219
219
|
await emit(options, async (client) => {
|
|
@@ -227,7 +227,7 @@ vault.command("stock-note-list").description("List your stock notes: all, by sec
|
|
|
227
227
|
.option("--note-id <id>", "Note ID (repeat or comma-separate); not together with --security", collectList)
|
|
228
228
|
.option("--from <number>", "Starting offset", "0")
|
|
229
229
|
.option("--size <number>", "Total rows to return; omit to fetch all")
|
|
230
|
-
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
230
|
+
.option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
|
|
231
231
|
.action((options) => {
|
|
232
232
|
if (options.security?.length && options.noteId?.length)
|
|
233
233
|
throw new ValidationError("pass --security or --note-id, not both");
|
|
@@ -256,7 +256,7 @@ query(vault, "stock-note-update", {
|
|
|
256
256
|
vault.command("stock-note-delete").description("Delete stock notes — irreversible (free)")
|
|
257
257
|
.requiredOption("--note-id <id>", "Note ID to delete; repeat or comma-separate, max 100", collectList)
|
|
258
258
|
.option("--yes", "Required: confirm the deletion")
|
|
259
|
-
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
259
|
+
.option("--format <format>", "Output format", "json").option("--output <path>", OUTPUT_HELP)
|
|
260
260
|
.action(async (options) => {
|
|
261
261
|
assertConfirmed("vault.stock-note.delete", Boolean(options.yes), options.noteId.join("、"));
|
|
262
262
|
await emit(options, async (client) => {
|
package/dist/src/core/client.js
CHANGED
|
@@ -9,13 +9,16 @@ import { ApiError, attachEnvelopeTraceId, markStructural, ValidationError } from
|
|
|
9
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, JSON_TOTAL_FACTOR, logTiming, markAuthReplay, PAGE_CONCURRENCY, parseRetryAfterMs, quoteBigIntFields, runInOrder, TRANSFER_TOTAL_FACTOR, withRetry } from "./transport.js";
|
|
12
|
+
import { decodeResponseBody, getDispatcher, isVerbose, JSON_TOTAL_FACTOR, logTiming, markAuthReplay, noteRequestSent, 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
|
+
import { isDryRun, stopForDryRun } from "./dryRun.js";
|
|
15
16
|
const AUTH_RETRY_CODES = new Set(["8000014", "8000015", "0000001008", "999002"]);
|
|
16
17
|
function hasExpectedShape(expects, payload) {
|
|
17
18
|
if (expects === "array")
|
|
18
19
|
return Array.isArray(payload);
|
|
20
|
+
if (expects === "object")
|
|
21
|
+
return Boolean(payload && typeof payload === "object" && !Array.isArray(payload));
|
|
19
22
|
return Boolean(payload && typeof payload === "object" && Array.isArray(payload.list));
|
|
20
23
|
}
|
|
21
24
|
const PROBE_ABOVE_CREDITS = 50;
|
|
@@ -79,7 +82,7 @@ export class GangtiseClient {
|
|
|
79
82
|
return this.refreshPromise;
|
|
80
83
|
}
|
|
81
84
|
async doTokenRefresh() {
|
|
82
|
-
const credentials = requireAccessCredentials(this.config.accessKey, this.config.secretKey);
|
|
85
|
+
const credentials = isDryRun() ? { accessKey: "", secretKey: "" } : requireAccessCredentials(this.config.accessKey, this.config.secretKey);
|
|
83
86
|
const envelope = await this.requestJson(ENDPOINTS["auth.login"], {
|
|
84
87
|
accessKey: credentials.accessKey,
|
|
85
88
|
secretKey: credentials.secretKey,
|
|
@@ -472,6 +475,8 @@ export class GangtiseClient {
|
|
|
472
475
|
if (endpoint.path.startsWith('/guide/')) {
|
|
473
476
|
return this.readLocalLookup(endpoint);
|
|
474
477
|
}
|
|
478
|
+
if (isDryRun())
|
|
479
|
+
return stopForDryRun({ endpoint, url: this.buildUrl(endpoint.path), body });
|
|
475
480
|
const dispatcher = getDispatcher();
|
|
476
481
|
const url = this.buildUrl(endpoint.path);
|
|
477
482
|
const authState = {};
|
|
@@ -488,6 +493,7 @@ export class GangtiseClient {
|
|
|
488
493
|
usedAuthorization = await this.getAuthorizationHeader();
|
|
489
494
|
headers.Authorization = usedAuthorization;
|
|
490
495
|
}
|
|
496
|
+
noteRequestSent();
|
|
491
497
|
const startedAt = Date.now();
|
|
492
498
|
const response = await request(url, {
|
|
493
499
|
method: endpoint.method,
|
|
@@ -562,6 +568,10 @@ export class GangtiseClient {
|
|
|
562
568
|
if (!endpoint || endpoint.kind !== 'upload') {
|
|
563
569
|
throw new ApiError(`Not an upload endpoint: ${endpointKey}`);
|
|
564
570
|
}
|
|
571
|
+
if (isDryRun()) {
|
|
572
|
+
const sent = Object.fromEntries(Object.entries(fields ?? {}).filter(([, value]) => value !== undefined));
|
|
573
|
+
return stopForDryRun({ endpoint, url: this.buildUrl(endpoint.path), body: sent, file: { name: file.filename, bytes: file.data.byteLength } });
|
|
574
|
+
}
|
|
565
575
|
const form = new FormData();
|
|
566
576
|
form.append('file', new Blob([file.data], { type: file.contentType ?? 'application/octet-stream' }), file.filename);
|
|
567
577
|
for (const [name, value] of Object.entries(fields ?? {})) {
|
|
@@ -580,6 +590,7 @@ export class GangtiseClient {
|
|
|
580
590
|
const timeoutMs = resolveTimeoutMs(this.config.timeoutMs, endpoint);
|
|
581
591
|
return withRetry(async () => {
|
|
582
592
|
const authorization = await this.getAuthorizationHeader();
|
|
593
|
+
noteRequestSent();
|
|
583
594
|
const startedAt = Date.now();
|
|
584
595
|
const signal = AbortSignal.timeout(timeoutMs * TRANSFER_TOTAL_FACTOR);
|
|
585
596
|
let currentUrl = url;
|
|
@@ -731,6 +742,11 @@ export class GangtiseClient {
|
|
|
731
742
|
if (endpoint.kind === 'upload') {
|
|
732
743
|
throw new ValidationError(`${endpointKey} takes a file upload — use its dedicated command ('gangtise tool file-parse --file <path>' / 'gangtise vault drive-upload --file <path>'); 'raw call' cannot send files`);
|
|
733
744
|
}
|
|
745
|
+
if (isDryRun() && !endpoint.path.startsWith('/guide/')) {
|
|
746
|
+
const url = this.buildUrl(endpoint.path);
|
|
747
|
+
Object.entries(query ?? {}).forEach(([key, value]) => url.searchParams.set(key, String(value)));
|
|
748
|
+
return stopForDryRun({ endpoint, url, body });
|
|
749
|
+
}
|
|
734
750
|
if (endpoint.kind === 'download') {
|
|
735
751
|
return this.download(endpoint, query ?? {}, options, body);
|
|
736
752
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
-
import { dirname, extname } from "node:path";
|
|
3
|
-
import { DownloadError } from "./errors.js";
|
|
2
|
+
import { dirname, extname, join, sep } from "node:path";
|
|
3
|
+
import { DownloadError, ValidationError } from "./errors.js";
|
|
4
4
|
import { saveOutputIfNeeded, stagingPath, trackClaim } from "./output.js";
|
|
5
|
+
import { assertNotStatePath } from "./statePaths.js";
|
|
5
6
|
import { extractTitles, lookupTitleCache, readTitleCache, TITLE_LOOKUP_SIZE, writeTitleCache } from "./titleCache.js";
|
|
6
7
|
function sanitizeFilename(name) {
|
|
7
8
|
return name.replace(/[/\\:*?"<>|\u0000-\u001f]/g, "_");
|
|
@@ -147,7 +148,7 @@ async function downloadUrlTo(url, outputPath) {
|
|
|
147
148
|
const { pipeline } = await import("node:stream/promises");
|
|
148
149
|
const { dirname } = await import("node:path");
|
|
149
150
|
const { request } = await import("undici");
|
|
150
|
-
const { getDispatcher, logTiming, TRANSFER_TOTAL_FACTOR, withRetry } = await import("./transport.js");
|
|
151
|
+
const { getDispatcher, logTiming, noteRequestSent, TRANSFER_TOTAL_FACTOR, withRetry } = await import("./transport.js");
|
|
151
152
|
const { loadConfig } = await import("./config.js");
|
|
152
153
|
const timeoutMs = loadConfig().timeoutMs;
|
|
153
154
|
await fs.mkdir(dirname(outputPath), { recursive: true });
|
|
@@ -162,6 +163,7 @@ async function downloadUrlTo(url, outputPath) {
|
|
|
162
163
|
};
|
|
163
164
|
const partPath = stagingPath(outputPath);
|
|
164
165
|
await withRetry(async () => {
|
|
166
|
+
noteRequestSent();
|
|
165
167
|
const startedAt = Date.now();
|
|
166
168
|
const requestOptions = {
|
|
167
169
|
method: "GET",
|
|
@@ -206,6 +208,13 @@ async function downloadUrlTo(url, outputPath) {
|
|
|
206
208
|
throw error;
|
|
207
209
|
}
|
|
208
210
|
}
|
|
211
|
+
export async function assertOutputFilePath(output) {
|
|
212
|
+
const endsAsDirectory = output.endsWith("/") || output.endsWith(sep);
|
|
213
|
+
if (endsAsDirectory || (await fs.stat(output).catch(() => undefined))?.isDirectory()) {
|
|
214
|
+
throw new ValidationError(`--output ${output} is a directory: pass a file path (e.g. ${join(output, "<name>")}), or omit --output for an automatic name in the current directory`);
|
|
215
|
+
}
|
|
216
|
+
await assertNotStatePath(output, "--output");
|
|
217
|
+
}
|
|
209
218
|
export function autoDownloadName(file, fallbackName) {
|
|
210
219
|
return truncateFilename(file.filename ? sanitizeFilename(file.filename) : sanitizeFilename(fallbackName) + extFromContentType(file.contentType));
|
|
211
220
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ValidationError } from "./errors.js";
|
|
4
|
+
import { assertNotStatePath } from "./statePaths.js";
|
|
4
5
|
export const DRIVE_UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
5
6
|
export const DRIVE_NAME_MAX = 200;
|
|
6
7
|
export async function uploadDriveFile(client, filePath, options) {
|
|
7
|
-
const
|
|
8
|
+
const shownAs = path.resolve(filePath);
|
|
9
|
+
let resolved;
|
|
8
10
|
let stat;
|
|
9
11
|
try {
|
|
12
|
+
resolved = await fs.realpath(filePath);
|
|
10
13
|
stat = await fs.stat(resolved);
|
|
11
14
|
}
|
|
12
15
|
catch {
|
|
@@ -14,12 +17,13 @@ export async function uploadDriveFile(client, filePath, options) {
|
|
|
14
17
|
}
|
|
15
18
|
if (!stat.isFile())
|
|
16
19
|
throw new ValidationError(`Not a file: ${filePath}`);
|
|
20
|
+
await assertNotStatePath(resolved, "--file");
|
|
17
21
|
if (stat.size === 0)
|
|
18
22
|
throw new ValidationError(`File is empty: ${filePath}`);
|
|
19
23
|
if (stat.size > DRIVE_UPLOAD_MAX_BYTES) {
|
|
20
24
|
throw new ValidationError(`File is ${(stat.size / 1024 / 1024).toFixed(1)}MB — the drive accepts at most 100MB per file`);
|
|
21
25
|
}
|
|
22
|
-
const filename = path.basename(
|
|
26
|
+
const filename = path.basename(shownAs);
|
|
23
27
|
const name = options.title ?? filename;
|
|
24
28
|
if (name.length > DRIVE_NAME_MAX) {
|
|
25
29
|
throw new ValidationError(`File name is ${name.length} characters — the drive accepts at most ${DRIVE_NAME_MAX} (an emoji counts as 2); pass a shorter --title`);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { removeStagingFiles } from "./output.js";
|
|
2
|
+
let dryRun = false;
|
|
3
|
+
let shown = false;
|
|
4
|
+
export function setDryRun(enabled) {
|
|
5
|
+
dryRun = enabled;
|
|
6
|
+
}
|
|
7
|
+
export function isDryRun() {
|
|
8
|
+
return dryRun;
|
|
9
|
+
}
|
|
10
|
+
const toCents = (credits) => Math.round(credits * 100) / 100;
|
|
11
|
+
function maxCredits(endpoint, body) {
|
|
12
|
+
const billing = endpoint.billing;
|
|
13
|
+
if (!billing)
|
|
14
|
+
return undefined;
|
|
15
|
+
const fields = body && typeof body === "object" ? body : {};
|
|
16
|
+
if (billing.per === "call" || billing.per === "document")
|
|
17
|
+
return billing.price;
|
|
18
|
+
if (endpoint.estimateBilledUnits) {
|
|
19
|
+
try {
|
|
20
|
+
return toCents(endpoint.estimateBilledUnits(fields) * billing.price);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (billing.per === "row" && endpoint.pagination?.enabled)
|
|
27
|
+
return typeof fields.size === "number" ? toCents(fields.size * billing.price) : undefined;
|
|
28
|
+
if (!billing.maxUnits)
|
|
29
|
+
return undefined;
|
|
30
|
+
const asked = billing.unitsFrom?.reduce((n, key) => n + (Array.isArray(fields[key]) ? fields[key].length : 0), 0);
|
|
31
|
+
return toCents(Math.min(billing.maxUnits, asked || billing.maxUnits) * billing.price);
|
|
32
|
+
}
|
|
33
|
+
export function describeRequest(plan) {
|
|
34
|
+
const { endpoint } = plan;
|
|
35
|
+
return {
|
|
36
|
+
dryRun: true,
|
|
37
|
+
endpoint: endpoint.key,
|
|
38
|
+
method: endpoint.method,
|
|
39
|
+
url: plan.url.toString(),
|
|
40
|
+
body: endpoint.key === "auth.login" ? "(credentials not shown)" : plan.body ?? null,
|
|
41
|
+
file: plan.file,
|
|
42
|
+
pagination: endpoint.pagination?.enabled
|
|
43
|
+
? `The CLI pages this list itself, at most ${endpoint.pagination.maxPageSize} rows a request, until size rows or the total; without size it fetches every row`
|
|
44
|
+
: undefined,
|
|
45
|
+
billing: endpoint.billing
|
|
46
|
+
? { per: endpoint.billing.per, price: endpoint.billing.price, ...(Number.isFinite(endpoint.billing.maxUnits) ? { maxUnits: endpoint.billing.maxUnits } : {}) }
|
|
47
|
+
: null,
|
|
48
|
+
maxCredits: maxCredits(endpoint, plan.body),
|
|
49
|
+
needsYes: endpoint.destructive ? true : undefined,
|
|
50
|
+
note: "Nothing was sent. Only the command's first request is shown; any later request depends on its answer.",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function describeNoRequest(output) {
|
|
54
|
+
return {
|
|
55
|
+
dryRun: true,
|
|
56
|
+
endpoint: null,
|
|
57
|
+
output: output ?? null,
|
|
58
|
+
note: "This command sends no request. Under --dry-run it prints only this and writes no file.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function stopForDryRun(plan) {
|
|
62
|
+
if (!shown) {
|
|
63
|
+
shown = true;
|
|
64
|
+
removeStagingFiles();
|
|
65
|
+
process.stdout.write(`${JSON.stringify(describeRequest(plan), null, 2)}\n`, () => process.exit(0));
|
|
66
|
+
}
|
|
67
|
+
return new Promise(() => { });
|
|
68
|
+
}
|
|
@@ -71,7 +71,7 @@ const ENDPOINT_DEFS = {
|
|
|
71
71
|
path: "/application/open-insight/chief-opinion/getDetail",
|
|
72
72
|
kind: "json",
|
|
73
73
|
description: "Get domestic chief opinion bodies by ID (max 20 IDs per call)",
|
|
74
|
-
billing: { per: "row", price: 30, maxUnits: 20 },
|
|
74
|
+
billing: { per: "row", price: 30, maxUnits: 20, unitsFrom: ["chiefOpinionIdList"] },
|
|
75
75
|
retry: "no-replay",
|
|
76
76
|
expects: "array",
|
|
77
77
|
},
|
|
@@ -267,7 +267,7 @@ const ENDPOINT_DEFS = {
|
|
|
267
267
|
path: "/application/open-insight/foreign-opinion/getDetail",
|
|
268
268
|
kind: "json",
|
|
269
269
|
description: "Get foreign opinion bodies by ID (max 20 IDs per call)",
|
|
270
|
-
billing: { per: "row", price: 30, maxUnits: 20 },
|
|
270
|
+
billing: { per: "row", price: 30, maxUnits: 20, unitsFrom: ["foreignOpinionIdList"] },
|
|
271
271
|
retry: "no-replay",
|
|
272
272
|
expects: "array",
|
|
273
273
|
},
|
|
@@ -288,6 +288,25 @@ const ENDPOINT_DEFS = {
|
|
|
288
288
|
description: "Download foreign independent opinion file",
|
|
289
289
|
billing: { per: "document", price: 30 },
|
|
290
290
|
},
|
|
291
|
+
"insight.overseas-opinion.list": {
|
|
292
|
+
method: "POST",
|
|
293
|
+
path: "/application/open-insight/overseasOpinion/getList",
|
|
294
|
+
kind: "json",
|
|
295
|
+
description: "List overseas opinions (brief only; body via detail)",
|
|
296
|
+
billing: { per: "row", price: 0.1 },
|
|
297
|
+
pagination: { enabled: true, maxPageSize: 50, maxWindow: 10000 },
|
|
298
|
+
rowId: "overseasOpinionId",
|
|
299
|
+
expects: "list",
|
|
300
|
+
},
|
|
301
|
+
"insight.overseas-opinion.detail": {
|
|
302
|
+
method: "POST",
|
|
303
|
+
path: "/application/open-insight/overseasOpinion/getDetail",
|
|
304
|
+
kind: "json",
|
|
305
|
+
description: "Get overseas opinion bodies by ID (max 20 IDs per call)",
|
|
306
|
+
billing: { per: "row", price: 10, maxUnits: 20, unitsFrom: ["overseasOpinionIdList"] },
|
|
307
|
+
retry: "no-replay",
|
|
308
|
+
expects: "array",
|
|
309
|
+
},
|
|
291
310
|
"insight.official-account.list": {
|
|
292
311
|
method: "POST",
|
|
293
312
|
path: "/application/open-insight/officialAccount/getList",
|
|
@@ -317,6 +336,7 @@ const ENDPOINT_DEFS = {
|
|
|
317
336
|
path: "/application/open-insight/report-image/getList",
|
|
318
337
|
kind: "json",
|
|
319
338
|
description: "Search research report images by keyword (returns chunkId + metadata)",
|
|
339
|
+
expects: "array",
|
|
320
340
|
},
|
|
321
341
|
"insight.highlight.list": {
|
|
322
342
|
method: "POST",
|
|
@@ -341,54 +361,72 @@ const ENDPOINT_DEFS = {
|
|
|
341
361
|
path: "/application/open-reference/securities/search",
|
|
342
362
|
kind: "json",
|
|
343
363
|
description: "Search GTS codes (securities)",
|
|
364
|
+
expects: "list",
|
|
344
365
|
},
|
|
345
366
|
"reference.chiefs-search": {
|
|
346
367
|
method: "POST",
|
|
347
368
|
path: "/application/open-reference/chiefs/search",
|
|
348
369
|
kind: "json",
|
|
349
370
|
description: "Search chief analyst IDs by name / institution / team",
|
|
371
|
+
expects: "list",
|
|
350
372
|
},
|
|
351
373
|
"reference.institution-search": {
|
|
352
374
|
method: "POST",
|
|
353
375
|
path: "/application/open-reference/institutions/search",
|
|
354
376
|
kind: "json",
|
|
355
377
|
description: "Search institution IDs by keyword (domestic broker / foreign / lead / opinion institution)",
|
|
378
|
+
expects: "list",
|
|
356
379
|
},
|
|
357
380
|
"reference.official-account-search": {
|
|
358
381
|
method: "POST",
|
|
359
382
|
path: "/application/open-reference/officialAccount/search",
|
|
360
383
|
kind: "json",
|
|
361
384
|
description: "Search official account (WeChat public account) IDs by name / institution / category",
|
|
385
|
+
expects: "list",
|
|
386
|
+
},
|
|
387
|
+
"reference.overseas-account-search": {
|
|
388
|
+
method: "POST",
|
|
389
|
+
path: "/application/open-reference/overseasAccount/search",
|
|
390
|
+
kind: "json",
|
|
391
|
+
description: "Search overseas opinion account IDs by name fragment",
|
|
392
|
+
pagination: { enabled: true, maxPageSize: 50 },
|
|
393
|
+
rowId: "accountId",
|
|
394
|
+
expects: "list",
|
|
362
395
|
},
|
|
363
396
|
"reference.constant-category": {
|
|
364
397
|
method: "GET",
|
|
365
398
|
path: "/application/open-reference/constants/category",
|
|
366
399
|
kind: "json",
|
|
367
400
|
description: "List constant categories and their API usage scopes",
|
|
401
|
+
expects: "list",
|
|
368
402
|
},
|
|
369
403
|
"reference.constant-list": {
|
|
370
404
|
method: "POST",
|
|
371
405
|
path: "/application/open-reference/constants/getList",
|
|
372
406
|
kind: "json",
|
|
373
407
|
description: "List all constant values of a category",
|
|
408
|
+
expects: "object",
|
|
374
409
|
},
|
|
375
410
|
"reference.concept-search": {
|
|
376
411
|
method: "POST",
|
|
377
412
|
path: "/application/open-reference/concepts/search",
|
|
378
413
|
kind: "json",
|
|
379
414
|
description: "Search concept (theme) IDs by keyword",
|
|
415
|
+
expects: "list",
|
|
380
416
|
},
|
|
381
417
|
"reference.sector-search": {
|
|
382
418
|
method: "POST",
|
|
383
419
|
path: "/application/open-reference/sectors/search",
|
|
384
420
|
kind: "json",
|
|
385
421
|
description: "Search sector IDs by keyword",
|
|
422
|
+
expects: "list",
|
|
386
423
|
},
|
|
387
424
|
"reference.sector-constituents": {
|
|
388
425
|
method: "POST",
|
|
389
426
|
path: "/application/open-reference/sectors/constituents",
|
|
390
427
|
kind: "json",
|
|
391
428
|
description: "List constituent securities of a sector",
|
|
429
|
+
expects: "list",
|
|
392
430
|
},
|
|
393
431
|
"reference.index-weight": {
|
|
394
432
|
method: "POST",
|
|
@@ -458,72 +496,84 @@ const ENDPOINT_DEFS = {
|
|
|
458
496
|
path: "/application/open-fundamental/financial-report/income-statement/accumulated",
|
|
459
497
|
kind: "json",
|
|
460
498
|
description: "Query A-share income statement (accumulated)",
|
|
499
|
+
expects: "list",
|
|
461
500
|
},
|
|
462
501
|
"fundamental.income-statement-quarterly": {
|
|
463
502
|
method: "POST",
|
|
464
503
|
path: "/application/open-fundamental/financial-report/income-statement/quarterly",
|
|
465
504
|
kind: "json",
|
|
466
505
|
description: "Query A-share income statement (quarterly)",
|
|
506
|
+
expects: "list",
|
|
467
507
|
},
|
|
468
508
|
"fundamental.balance-sheet": {
|
|
469
509
|
method: "POST",
|
|
470
510
|
path: "/application/open-fundamental/financial-report/balance-sheet/accumulated",
|
|
471
511
|
kind: "json",
|
|
472
512
|
description: "Query A-share balance sheet (accumulated)",
|
|
513
|
+
expects: "list",
|
|
473
514
|
},
|
|
474
515
|
"fundamental.cash-flow": {
|
|
475
516
|
method: "POST",
|
|
476
517
|
path: "/application/open-fundamental/financial-report/cash-flow-statement/accumulated",
|
|
477
518
|
kind: "json",
|
|
478
519
|
description: "Query A-share cash flow statement (accumulated)",
|
|
520
|
+
expects: "list",
|
|
479
521
|
},
|
|
480
522
|
"fundamental.cash-flow-quarterly": {
|
|
481
523
|
method: "POST",
|
|
482
524
|
path: "/application/open-fundamental/financial-report/cash-flow-statement/quarterly",
|
|
483
525
|
kind: "json",
|
|
484
526
|
description: "Query A-share cash flow statement (quarterly)",
|
|
527
|
+
expects: "list",
|
|
485
528
|
},
|
|
486
529
|
"fundamental.income-statement-hk": {
|
|
487
530
|
method: "POST",
|
|
488
531
|
path: "/application/open-fundamental/financial-report/income-statement/hk",
|
|
489
532
|
kind: "json",
|
|
490
533
|
description: "Query HK income statement (China GAAP)",
|
|
534
|
+
expects: "list",
|
|
491
535
|
},
|
|
492
536
|
"fundamental.balance-sheet-hk": {
|
|
493
537
|
method: "POST",
|
|
494
538
|
path: "/application/open-fundamental/financial-report/balance-sheet/hk",
|
|
495
539
|
kind: "json",
|
|
496
540
|
description: "Query HK balance sheet (China GAAP)",
|
|
541
|
+
expects: "list",
|
|
497
542
|
},
|
|
498
543
|
"fundamental.cash-flow-hk": {
|
|
499
544
|
method: "POST",
|
|
500
545
|
path: "/application/open-fundamental/financial-report/cash-flow-statement/hk",
|
|
501
546
|
kind: "json",
|
|
502
547
|
description: "Query HK cash flow statement (China GAAP)",
|
|
548
|
+
expects: "list",
|
|
503
549
|
},
|
|
504
550
|
"fundamental.income-statement-us": {
|
|
505
551
|
method: "POST",
|
|
506
552
|
path: "/application/open-fundamental/financial-report/income-statement/us",
|
|
507
553
|
kind: "json",
|
|
508
554
|
description: "Query US income statement",
|
|
555
|
+
expects: "list",
|
|
509
556
|
},
|
|
510
557
|
"fundamental.balance-sheet-us": {
|
|
511
558
|
method: "POST",
|
|
512
559
|
path: "/application/open-fundamental/financial-report/balance-sheet/us",
|
|
513
560
|
kind: "json",
|
|
514
561
|
description: "Query US balance sheet",
|
|
562
|
+
expects: "list",
|
|
515
563
|
},
|
|
516
564
|
"fundamental.cash-flow-us": {
|
|
517
565
|
method: "POST",
|
|
518
566
|
path: "/application/open-fundamental/financial-report/cash-flow-statement/us",
|
|
519
567
|
kind: "json",
|
|
520
568
|
description: "Query US cash flow statement",
|
|
569
|
+
expects: "list",
|
|
521
570
|
},
|
|
522
571
|
"fundamental.main-business": {
|
|
523
572
|
method: "POST",
|
|
524
573
|
path: "/application/open-fundamental/main-business",
|
|
525
574
|
kind: "json",
|
|
526
575
|
description: "Query main business composition",
|
|
576
|
+
expects: "list",
|
|
527
577
|
},
|
|
528
578
|
"fundamental.valuation-analysis": {
|
|
529
579
|
method: "POST",
|
|
@@ -537,6 +587,7 @@ const ENDPOINT_DEFS = {
|
|
|
537
587
|
path: "/application/open-fundamental/capital-structure/top-holders",
|
|
538
588
|
kind: "json",
|
|
539
589
|
description: "Query top holders (top10 / top10 float)",
|
|
590
|
+
expects: "list",
|
|
540
591
|
},
|
|
541
592
|
"fundamental.earning-forecast": {
|
|
542
593
|
method: "POST",
|
|
@@ -628,7 +679,7 @@ const ENDPOINT_DEFS = {
|
|
|
628
679
|
path: "/application/open-fundamental/bond/rating-change",
|
|
629
680
|
kind: "json",
|
|
630
681
|
description: "Query bond rating change history (max 10 bonds per call)",
|
|
631
|
-
billing: { per: "security", price: 0.4, maxUnits: 10 },
|
|
682
|
+
billing: { per: "security", price: 0.4, maxUnits: 10, unitsFrom: ["securityList"] },
|
|
632
683
|
retry: "no-replay",
|
|
633
684
|
expects: "list",
|
|
634
685
|
},
|
|
@@ -637,7 +688,7 @@ const ENDPOINT_DEFS = {
|
|
|
637
688
|
path: "/application/open-fundamental/bond/issuer-rating-change",
|
|
638
689
|
kind: "json",
|
|
639
690
|
description: "Query issuer rating change history (by bond code or issuer name)",
|
|
640
|
-
billing: { per: "issuer", price: 0.4, maxUnits: 10 },
|
|
691
|
+
billing: { per: "issuer", price: 0.4, maxUnits: 10, unitsFrom: ["securityList", "issuerNameList"] },
|
|
641
692
|
retry: "no-replay",
|
|
642
693
|
expects: "list",
|
|
643
694
|
},
|
|
@@ -835,7 +886,7 @@ const ENDPOINT_DEFS = {
|
|
|
835
886
|
path: "/application/open-ai/stock-summary/getList",
|
|
836
887
|
kind: "json",
|
|
837
888
|
description: "Stock highlights (refined research summary per security)",
|
|
838
|
-
billing: { per: "row", price: 1, maxUnits: 6000 },
|
|
889
|
+
billing: { per: "row", price: 1, maxUnits: 6000, unitsFrom: ["securityList"] },
|
|
839
890
|
retry: "no-replay",
|
|
840
891
|
timeoutMs: 120_000,
|
|
841
892
|
},
|
|
@@ -1031,6 +1082,7 @@ const ENDPOINT_DEFS = {
|
|
|
1031
1082
|
path: "/application/open-vault/drive/getFolderList",
|
|
1032
1083
|
kind: "json",
|
|
1033
1084
|
description: "List the direct subfolders and files of a drive folder",
|
|
1085
|
+
expects: "object",
|
|
1034
1086
|
},
|
|
1035
1087
|
"vault.drive.create-folder": {
|
|
1036
1088
|
method: "POST",
|
|
@@ -1148,6 +1200,7 @@ const ENDPOINT_DEFS = {
|
|
|
1148
1200
|
path: "/application/open-vault/stock-pool/getStockList",
|
|
1149
1201
|
kind: "json",
|
|
1150
1202
|
description: "List securities in stock pool(s)",
|
|
1203
|
+
expects: "array",
|
|
1151
1204
|
},
|
|
1152
1205
|
"vault.stock-pool.create": {
|
|
1153
1206
|
method: "POST",
|
|
@@ -1218,13 +1271,14 @@ const ENDPOINT_DEFS = {
|
|
|
1218
1271
|
path: "/application/open-alternative/EDB/search",
|
|
1219
1272
|
kind: "json",
|
|
1220
1273
|
description: "Search industry indicator list by keyword",
|
|
1274
|
+
expects: "array",
|
|
1221
1275
|
},
|
|
1222
1276
|
"alternative.edb-data": {
|
|
1223
1277
|
method: "POST",
|
|
1224
1278
|
path: "/application/open-alternative/EDB/getData",
|
|
1225
1279
|
kind: "json",
|
|
1226
1280
|
description: "Get industry indicator time-series data by indicator ID list",
|
|
1227
|
-
billing: { per: "indicator", price: 30, maxUnits: 10 },
|
|
1281
|
+
billing: { per: "indicator", price: 30, maxUnits: 10, unitsFrom: ["indicatorIdList"] },
|
|
1228
1282
|
},
|
|
1229
1283
|
"alternative.concept-info": {
|
|
1230
1284
|
method: "POST",
|
|
@@ -1264,6 +1318,7 @@ const ENDPOINT_DEFS = {
|
|
|
1264
1318
|
kind: "json",
|
|
1265
1319
|
description: "Search data indicators by keyword (returns indicatorCode + params)",
|
|
1266
1320
|
retry: "no-999999",
|
|
1321
|
+
expects: "array",
|
|
1267
1322
|
},
|
|
1268
1323
|
"indicator.cross-section": {
|
|
1269
1324
|
method: "POST",
|