gangtise-openapi-cli 0.41.1 → 0.42.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 +12 -10
- package/dist/src/cli.js +23 -1152
- package/dist/src/commands/ai.js +155 -0
- package/dist/src/commands/alternative.js +56 -0
- package/dist/src/commands/auth.js +37 -0
- package/dist/src/commands/bond.js +76 -0
- package/dist/src/commands/fundamental.js +137 -0
- package/dist/src/commands/indicator.js +134 -0
- package/dist/src/commands/insight.js +337 -0
- package/dist/src/commands/quote.js +147 -0
- package/dist/src/commands/raw.js +49 -0
- package/dist/src/commands/reference.js +58 -0
- package/dist/src/commands/shared.js +145 -0
- package/dist/src/commands/tool.js +55 -0
- package/dist/src/commands/vault.js +203 -0
- package/dist/src/core/args.js +7 -4
- package/dist/src/core/asyncContent.js +32 -19
- package/dist/src/core/client.js +11 -4
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/endpoints.js +3 -0
- package/dist/src/core/exitStatus.js +17 -0
- package/dist/src/core/fileParse.js +3 -20
- package/dist/src/core/opinionDetail.js +39 -0
- package/dist/src/core/output.js +18 -4
- package/dist/src/core/printer.js +7 -8
- package/dist/src/core/rowSink.js +19 -7
- package/dist/src/core/transport.js +4 -3
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +1 -1
- package/gangtise-openapi/references/commands/ai.md +1 -1
- package/gangtise-openapi/references/commands/fundamental.md +1 -1
- package/gangtise-openapi/references/errors.md +6 -3
- package/gangtise-openapi/references/examples.md +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Option } from "commander";
|
|
2
|
+
import { collectList, dateArg, datetimeArg, maybeArray, numberListArg, parseChoiceList, parseFrom, parseNumberOption, parseOptionalNumberOption, parseSize } from "../core/args.js";
|
|
3
|
+
import { loadConfig } from "../core/config.js";
|
|
4
|
+
import { releaseClaim, resolveTitle, saveDownloadResult, uniquePath } from "../core/download.js";
|
|
5
|
+
import { ENDPOINTS } from "../core/endpoints.js";
|
|
6
|
+
import { ValidationError } from "../core/errors.js";
|
|
7
|
+
import { parseOutputFormat } from "../core/output.js";
|
|
8
|
+
import { printData } from "../core/printer.js";
|
|
9
|
+
import { ExportSink } from "../core/rowSink.js";
|
|
10
|
+
export async function createClient(stream) {
|
|
11
|
+
const { GangtiseClient } = await import("../core/client.js");
|
|
12
|
+
const sink = stream?.output && (stream.format === "jsonl" || stream.format === "csv") ? new ExportSink(stream.output, stream.format, stream.cache) : undefined;
|
|
13
|
+
return new GangtiseClient(loadConfig(), sink);
|
|
14
|
+
}
|
|
15
|
+
export async function emit(options, produce, cache) {
|
|
16
|
+
const format = parseOutputFormat(options.format);
|
|
17
|
+
const client = await createClient({ format, output: options.output, cache });
|
|
18
|
+
try {
|
|
19
|
+
await printData(await produce(client), format, options.output, cache);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await client.rowSink?.abort();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function withClient(a, b) {
|
|
26
|
+
const [stream, fn] = typeof a === "function" ? [undefined, a] : [a, b];
|
|
27
|
+
const client = await createClient(stream);
|
|
28
|
+
try {
|
|
29
|
+
await fn(client);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await client.rowSink?.abort();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const DEFAULT_QUOTE_LIMIT = 6000;
|
|
36
|
+
export function flagIfLimitTruncated(data, cap, label, rangeFlags = "--start-date/--end-date", advice) {
|
|
37
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
38
|
+
return;
|
|
39
|
+
const rec = data;
|
|
40
|
+
if (rec.partial === true)
|
|
41
|
+
return;
|
|
42
|
+
if (Array.isArray(rec.list) && rec.list.length >= cap) {
|
|
43
|
+
rec.partial = true;
|
|
44
|
+
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
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function runDownload(client, endpointKey, query, options) {
|
|
48
|
+
if (options.output) {
|
|
49
|
+
const result = await client.call(endpointKey, options.body, query, { streamTo: options.output });
|
|
50
|
+
await saveDownloadResult(result, options.fallbackName, options.output);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const result = await client.call(endpointKey, options.body, query);
|
|
54
|
+
const resolved = options.resolveOutputPath ? await options.resolveOutputPath(result) : undefined;
|
|
55
|
+
const target = resolved ? await uniquePath(resolved) : undefined;
|
|
56
|
+
try {
|
|
57
|
+
await saveDownloadResult(result, options.fallbackName, target);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (target)
|
|
61
|
+
await releaseClaim(target);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function addDownloadCommand(parent, spec) {
|
|
66
|
+
const cmd = parent.command(spec.name ?? "download").requiredOption(`${spec.idOption} <id>`);
|
|
67
|
+
if (spec.fileType) {
|
|
68
|
+
const option = new Option("--file-type <number>", spec.fileType.description).choices(spec.fileType.choices);
|
|
69
|
+
cmd.addOption(spec.fileType.required ? option.makeOptionMandatory() : option.default(spec.fileType.default));
|
|
70
|
+
}
|
|
71
|
+
if (spec.contentTypeDescription)
|
|
72
|
+
cmd.requiredOption("--content-type <type>", spec.contentTypeDescription);
|
|
73
|
+
if (spec.titleListEndpoint) {
|
|
74
|
+
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
|
+
}
|
|
76
|
+
cmd.option("--output <path>").action((options) => withClient(async (client) => {
|
|
77
|
+
const id = options[spec.idField];
|
|
78
|
+
const qp = { [spec.idField]: id };
|
|
79
|
+
if (spec.fileType && options.fileType)
|
|
80
|
+
qp.fileType = parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 });
|
|
81
|
+
if (spec.contentTypeDescription)
|
|
82
|
+
qp.contentType = options.contentType;
|
|
83
|
+
const titleList = spec.titleListEndpoint;
|
|
84
|
+
await runDownload(client, spec.endpointKey, qp, {
|
|
85
|
+
output: options.output,
|
|
86
|
+
fallbackName: `${spec.fallbackPrefix}-${id}`,
|
|
87
|
+
resolveOutputPath: titleList
|
|
88
|
+
? (result) => resolveTitle(client, result, titleList, spec.idField, id, { allowLookup: Boolean(options.resolveTitle) })
|
|
89
|
+
: undefined,
|
|
90
|
+
});
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
export function assertConfirmed(endpointKey, confirmed, target) {
|
|
94
|
+
const destructive = ENDPOINTS[endpointKey]?.destructive;
|
|
95
|
+
if (!destructive || confirmed)
|
|
96
|
+
return;
|
|
97
|
+
throw new ValidationError(`${destructive.warning} 确认要对 ${target} 执行就加上 --yes。`);
|
|
98
|
+
}
|
|
99
|
+
export const field = (option, body) => ({ option, body });
|
|
100
|
+
export const value = (flags, description, key) => field(new Option(flags, description), (v) => ({ [key]: v }));
|
|
101
|
+
export const required = (flags, description, key) => field(new Option(flags, description).makeOptionMandatory(), (v) => ({ [key]: v }));
|
|
102
|
+
export const date = (flags, description, key, opts = {}) => {
|
|
103
|
+
const option = new Option(flags, description);
|
|
104
|
+
option.argParser(dateArg(option.long));
|
|
105
|
+
return field(opts.required ? option.makeOptionMandatory() : option, (v) => ({ [key]: v }));
|
|
106
|
+
};
|
|
107
|
+
export const list = (flags, description, key) => field(new Option(flags, description).argParser(collectList).default([]), (v) => ({ [key]: maybeArray(v) }));
|
|
108
|
+
export const numberList = (flags, description, key) => {
|
|
109
|
+
const option = new Option(flags, description).default([]);
|
|
110
|
+
option.argParser(numberListArg(option.long));
|
|
111
|
+
return field(option, (v) => ({ [key]: v.length ? v : undefined }));
|
|
112
|
+
};
|
|
113
|
+
export const choiceList = (flags, description, key, choices) => {
|
|
114
|
+
const option = new Option(flags, description).argParser(collectList).default([]);
|
|
115
|
+
return field(option, (v) => ({ [key]: parseChoiceList(v, option.long, choices) }));
|
|
116
|
+
};
|
|
117
|
+
export const count = (flags, description, key) => {
|
|
118
|
+
const option = new Option(flags, description);
|
|
119
|
+
return field(option, (v) => ({ [key]: parseOptionalNumberOption(v, option.long, { integer: true, min: 0 }) }));
|
|
120
|
+
};
|
|
121
|
+
const numberEnum = (flags, description, key, choices, fallback) => {
|
|
122
|
+
const option = new Option(flags, description).choices(choices).default(fallback);
|
|
123
|
+
return field(option, (v) => ({ [key]: parseNumberOption(v, option.long, { integer: true, min: 1 }) }));
|
|
124
|
+
};
|
|
125
|
+
export const rankType = () => numberEnum("--rank-type <number>", "Rank type: 1=composite 2=time desc", "rankType", ["1", "2"], "1");
|
|
126
|
+
export const searchType = () => numberEnum("--search-type <number>", "Search type: 1=title 2=fulltext", "searchType", ["1", "2"], "1");
|
|
127
|
+
export const top = (max) => field(new Option("--top <number>", `Max results (default: 10, max: ${max})`).default("10"), (v) => ({ top: parseNumberOption(v, "--top", { integer: true, min: 1, max }) }));
|
|
128
|
+
export const flag = (flags, description) => field(new Option(flags, description));
|
|
129
|
+
export const format = (fallback = "table") => field(new Option("--format <format>", "Output format").default(fallback));
|
|
130
|
+
export const output = (description) => field(new Option("--output <path>", description));
|
|
131
|
+
export const from = () => field(new Option("--from <number>", "Starting offset").default("0"), (v) => ({ from: parseFrom(v) }));
|
|
132
|
+
export const size = (description = "Total rows to return; omit to fetch all") => field(new Option("--size <number>", description), (v) => ({ size: parseSize(v) }));
|
|
133
|
+
const asTyped = (v) => v;
|
|
134
|
+
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") }));
|
|
135
|
+
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") }));
|
|
136
|
+
export const timeFilters = (toBody = asTyped) => [from(), size(), startTime("Start time", toBody), endTime("End time", toBody), value("--keyword <keyword>", "Keyword", "keyword")];
|
|
137
|
+
export const requestBody = (fields, options) => Object.assign({}, ...fields.map((f) => f.body?.(options[f.option.attributeName()]) ?? {}));
|
|
138
|
+
export function query(parent, name, spec) {
|
|
139
|
+
const command = parent.command(name);
|
|
140
|
+
if (spec.description)
|
|
141
|
+
command.description(spec.description);
|
|
142
|
+
for (const f of spec.fields)
|
|
143
|
+
command.addOption(f.option);
|
|
144
|
+
return command.action((options) => emit(options, (client) => client.call(typeof spec.endpoint === "string" ? spec.endpoint : spec.endpoint(options), requestBody(spec.fields, options)), spec.cache));
|
|
145
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
|
+
import { POLL_MAX_ATTEMPTS } from "../core/asyncContent.js";
|
|
3
|
+
import { collectList, maybeArray, parseNumberOption, parseOptionalNumberOption } from "../core/args.js";
|
|
4
|
+
import { ValidationError } from "../core/errors.js";
|
|
5
|
+
import { fetchFileParseResult, pollFileParseResult, submitFileParse } from "../core/fileParse.js";
|
|
6
|
+
import { markFailed } from "../core/exitStatus.js";
|
|
7
|
+
import { emit, withClient } from "./shared.js";
|
|
8
|
+
export const tool = new Command("tool").description("Research tool APIs: PDF parsing");
|
|
9
|
+
tool.command("file-parse").description("Parse a PDF into Markdown + images (async; 0.8 credits/page, billed at submit)")
|
|
10
|
+
.requiredOption("--file <path>", "PDF to upload (max 100MB / 500 pages)")
|
|
11
|
+
.option("--wait", "Wait for the parse to finish and save the result ZIP (blocking, up to ~5 min)")
|
|
12
|
+
.option("--output <path>", "Where to save the result ZIP (used with --wait)")
|
|
13
|
+
.action((options) => withClient(async (client) => {
|
|
14
|
+
const taskId = await submitFileParse(client, options.file);
|
|
15
|
+
if (!options.wait) {
|
|
16
|
+
process.stderr.write(`File parse task submitted. taskId: ${taskId}\n`);
|
|
17
|
+
process.stdout.write(`${JSON.stringify({ taskId, status: "pending", hint: `Run 'gangtise tool file-parse-check --task-id ${taskId}' in ~3 minutes to download the result ZIP` })}\n`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
process.stderr.write(`Got taskId: ${taskId}, waiting for the parse to finish...\n`);
|
|
21
|
+
if (await pollFileParseResult(client, taskId, options.output) !== "ok") {
|
|
22
|
+
process.stderr.write(`Parse result not available after ${POLL_MAX_ATTEMPTS} attempts. Try again later with: gangtise tool file-parse-check --task-id ${taskId}\n`);
|
|
23
|
+
markFailed();
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
tool.command("file-parse-check").description("Download a finished file-parse result ZIP by taskId (free)")
|
|
27
|
+
.requiredOption("--task-id <id>", "taskId from 'tool file-parse'")
|
|
28
|
+
.option("--output <path>", "Where to save the result ZIP")
|
|
29
|
+
.action((options) => withClient(async (client) => {
|
|
30
|
+
if (await fetchFileParseResult(client, options.taskId, options.output) === "pending") {
|
|
31
|
+
process.stdout.write(`${JSON.stringify({ taskId: options.taskId, status: "pending", hint: "Parse not finished yet, retry in ~1 minute" })}\n`);
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
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
|
+
.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)", "10")
|
|
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
|
+
.addOption(new Option("--min-tier <tier>", "Lowest source tier to keep; T3 = no filtering").choices(["T0", "T1", "T2", "T3"]))
|
|
39
|
+
.option("--site <domain>", "Restrict to a registered domain or subdomain, e.g. csrc.gov.cn (repeatable, max 10, OR-ed)", collectList, [])
|
|
40
|
+
.option("--include-content", "Return page body as Markdown; caps --size at 5")
|
|
41
|
+
.option("--max-content-chars <number>", "Max chars per body, 1000-20000 (only with --include-content)")
|
|
42
|
+
.option("--format <format>", "Output format", "table").option("--output <path>")
|
|
43
|
+
.action((options) => emit(options, (client) => {
|
|
44
|
+
const size = parseNumberOption(options.size, "--size", { integer: true, min: 1, max: 20 });
|
|
45
|
+
if (options.includeContent && size > 5)
|
|
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
|
+
const sites = [...new Set(options.site)];
|
|
48
|
+
if (sites.length > 10)
|
|
49
|
+
throw new ValidationError(`--site takes at most 10 distinct domains, got ${sites.length}`);
|
|
50
|
+
return client.call("tool.web-search", {
|
|
51
|
+
query: options.query, size, freshness: options.freshness, minTier: options.minTier,
|
|
52
|
+
siteList: maybeArray(sites), includeContent: options.includeContent || undefined,
|
|
53
|
+
maxContentChars: parseOptionalNumberOption(options.maxContentChars, "--max-content-chars", { integer: true, min: 1000, max: 20000 }),
|
|
54
|
+
});
|
|
55
|
+
}));
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
|
+
import { collectList, datetimeArg } from "../core/args.js";
|
|
3
|
+
import { buildStockPoolStocksBody, buildWechatChatroomListBody, buildWechatMessageListBody } from "../core/commandBodies.js";
|
|
4
|
+
import { uploadDriveFile } from "../core/driveUpload.js";
|
|
5
|
+
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";
|
|
7
|
+
export const vault = new Command("vault").description("Vault APIs");
|
|
8
|
+
query(vault, "drive-list", {
|
|
9
|
+
endpoint: "vault.drive.list",
|
|
10
|
+
cache: { endpointKey: "vault.drive.list", idField: "fileId" },
|
|
11
|
+
fields: [
|
|
12
|
+
from(), size(), startTime(), endTime(), value("--keyword <text>", undefined, "keyword"),
|
|
13
|
+
numberList("--file-type <number>", "File type", "fileTypeList"),
|
|
14
|
+
numberList("--space-type <number>", "Space type", "spaceTypeList"),
|
|
15
|
+
format(), output(),
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
addDownloadCommand(vault, { endpointKey: "vault.drive.download", name: "drive-download", idOption: "--file-id", idField: "fileId", fallbackPrefix: "file", titleListEndpoint: "vault.drive.list" });
|
|
19
|
+
const driveSpaceOption = (help) => new Option("--space-type <n>", help).choices(["1", "2"]).default("1");
|
|
20
|
+
const spaceType = (help) => field(driveSpaceOption(help), (v) => ({ spaceType: Number(v) }));
|
|
21
|
+
query(vault, "drive-folder-list", {
|
|
22
|
+
description: "List the direct subfolders and files of a drive folder (free)",
|
|
23
|
+
endpoint: "vault.drive.folder-list",
|
|
24
|
+
fields: [
|
|
25
|
+
spaceType("Space: 1=my drive 2=tenant drive"),
|
|
26
|
+
value("--parent-id <id>", "Folder ID in that space; omit (or 'root') for the space's root. A folder of the other space is rejected with 100003", "parentId"),
|
|
27
|
+
format("json"), output(),
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
vault.command("drive-upload").description("Upload a file to the AI drive (free; max 100MB per file; trial accounts max 500MB/day)")
|
|
31
|
+
.requiredOption("--file <path>", "File to upload")
|
|
32
|
+
.addOption(driveSpaceOption("Target space: 1=my drive 2=tenant drive (shared with your whole tenant)"))
|
|
33
|
+
.option("--folder-id <id>", "Target folder ID in that space; omit (or 'root') for the space's root")
|
|
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>")
|
|
36
|
+
.action((options) => emit(options, (client) => uploadDriveFile(client, options.file, { spaceType: Number(options.spaceType), folderId: options.folderId, title: options.title })));
|
|
37
|
+
query(vault, "drive-create-folder", {
|
|
38
|
+
description: "Create a drive folder (free). Same-name folders are allowed, so re-running creates a second one",
|
|
39
|
+
endpoint: "vault.drive.create-folder",
|
|
40
|
+
fields: [
|
|
41
|
+
required("--name <name>", "Folder name, max 200 characters (230004 past that)", "folderName"),
|
|
42
|
+
spaceType("Space: 1=my drive 2=tenant drive (shared with your whole tenant)"),
|
|
43
|
+
value("--parent-id <id>", "Parent folder ID in that space; omit (or 'root') for the root", "parentId"),
|
|
44
|
+
format("json"), output(),
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
query(vault, "drive-rename", {
|
|
48
|
+
description: "Rename a drive file or folder (free)",
|
|
49
|
+
endpoint: "vault.drive.rename",
|
|
50
|
+
fields: [
|
|
51
|
+
field(new Option("--type <type>", "What --id names").choices(["file", "folder"]).makeOptionMandatory(), (v) => ({ type: v })),
|
|
52
|
+
required("--id <id>", "File or folder ID", "id"),
|
|
53
|
+
required("--name <name>", "New name, max 200 characters", "name"),
|
|
54
|
+
format("json"), output(),
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
vault.command("drive-move-file").description("Move drive files into a folder of the SAME space (free)")
|
|
58
|
+
.requiredOption("--file-id <id>", "File ID (repeat or comma-separate)", collectList)
|
|
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>")
|
|
61
|
+
.action((options) => emit(options, async (client) => {
|
|
62
|
+
const data = await client.call("vault.drive.move-file", { fileIdList: options.fileId, targetFolderId: options.targetFolderId });
|
|
63
|
+
flagFailedItems(data, "vault drive-move-file");
|
|
64
|
+
return data;
|
|
65
|
+
}));
|
|
66
|
+
query(vault, "drive-move-folder", {
|
|
67
|
+
description: "Move a drive folder under another folder of the SAME space (free)",
|
|
68
|
+
endpoint: "vault.drive.move-folder",
|
|
69
|
+
fields: [
|
|
70
|
+
required("--folder-id <id>", "Folder ID to move", "folderId"),
|
|
71
|
+
required("--target-parent-id <id>", "Destination parent folder ID, or 'root'. Not the folder itself, one of its subfolders, or the other space (230005)", "targetParentId"),
|
|
72
|
+
format("json"), output(),
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
vault.command("drive-copy").description("Copy files to the OTHER space: my drive <-> tenant drive (free; same-space copies are rejected)")
|
|
76
|
+
.requiredOption("--file-id <id>", "Source file ID (repeat or comma-separate)", collectList)
|
|
77
|
+
.requiredOption("--target-folder-id <id>", "Destination folder ID in the other space, or 'root' for that space's root")
|
|
78
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
79
|
+
.action((options) => emit(options, async (client) => {
|
|
80
|
+
const data = await client.call("vault.drive.copy", { copyType: "file", fileIdList: options.fileId, targetFolderId: options.targetFolderId });
|
|
81
|
+
flagFailedItems(data, "vault drive-copy");
|
|
82
|
+
return data;
|
|
83
|
+
}));
|
|
84
|
+
vault.command("drive-delete-file").description("Delete drive files — irreversible (free)")
|
|
85
|
+
.requiredOption("--file-id <id>", "File ID to delete (repeat or comma-separate)", collectList)
|
|
86
|
+
.option("--yes", "Required: confirm the deletion")
|
|
87
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
88
|
+
.action(async (options) => {
|
|
89
|
+
assertConfirmed("vault.drive.delete-file", Boolean(options.yes), options.fileId.join("、"));
|
|
90
|
+
await emit(options, async (client) => {
|
|
91
|
+
const data = await client.call("vault.drive.delete-file", { fileIdList: options.fileId });
|
|
92
|
+
flagFailedItems(data, "vault drive-delete-file");
|
|
93
|
+
return data;
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
vault.command("drive-delete-folder").description("Delete a drive folder AND every subfolder and file inside it — irreversible (free)")
|
|
97
|
+
.requiredOption("--folder-id <id>", "Folder ID to delete")
|
|
98
|
+
.option("--yes", "Required: confirm the deletion")
|
|
99
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
100
|
+
.action(async (options) => {
|
|
101
|
+
assertConfirmed("vault.drive.delete-folder", Boolean(options.yes), options.folderId);
|
|
102
|
+
await emit(options, (client) => client.call("vault.drive.delete-folder", { folderId: options.folderId }));
|
|
103
|
+
});
|
|
104
|
+
query(vault, "record-list", {
|
|
105
|
+
endpoint: "vault.record.list",
|
|
106
|
+
cache: { endpointKey: "vault.record.list", idField: "recordId" },
|
|
107
|
+
fields: [
|
|
108
|
+
from(), size(), startTime(), endTime(), value("--keyword <text>", undefined, "keyword"),
|
|
109
|
+
list("--category <name>", "Recording type: upload/link/mobile/gtNote/pc/share", "categoryList"),
|
|
110
|
+
numberList("--space-type <number>", "Space type: 1=my records / 2=tenant records", "spaceTypeList"),
|
|
111
|
+
format(), output(),
|
|
112
|
+
],
|
|
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" });
|
|
115
|
+
query(vault, "my-conference-list", {
|
|
116
|
+
endpoint: "vault.my-conference.list",
|
|
117
|
+
cache: { endpointKey: "vault.my-conference.list", idField: "conferenceId" },
|
|
118
|
+
fields: [
|
|
119
|
+
from(), size(), startTime(), endTime(), value("--keyword <text>", undefined, "keyword"),
|
|
120
|
+
list("--research-area <id>", "Research area ID: citicIndustry code (1008001xx) or gangtiseIndustry direction code (122000xxx: macro/strategy/fixed-income/quant/overseas). swIndustry (104xx0000) returns 0 here", "researchAreaList"),
|
|
121
|
+
list("--security <code>", "Security code", "securityList"),
|
|
122
|
+
list("--institution <id>", "Institution ID", "institutionList"),
|
|
123
|
+
list("--category <name>", "Conference category: earningsCall/strategyMeeting/fundRoadshow/shareholdersMeeting/maMeeting/specialMeeting/companyAnalysis/industryAnalysis/other", "categoryList"),
|
|
124
|
+
numberList("--source <number>", "Recording source: 1=企微会议助理 2=会议服务微信群 (repeat)", "sourceList"),
|
|
125
|
+
format(), output(),
|
|
126
|
+
],
|
|
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" });
|
|
129
|
+
vault.command("wechat-message-list")
|
|
130
|
+
.option("--from <number>", "Starting offset", "0")
|
|
131
|
+
.option("--size <number>", "Total rows to return; omit to fetch all")
|
|
132
|
+
.option("--start-time <datetime>", "Start time", datetimeArg("--start-time"))
|
|
133
|
+
.option("--end-time <datetime>", "End time", datetimeArg("--end-time"))
|
|
134
|
+
.option("--keyword <text>")
|
|
135
|
+
.option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, [])
|
|
136
|
+
.option("--wechat-group-id <id>", "WeChat group ID", collectList, [])
|
|
137
|
+
.option("--industry <id>", "Industry ID -- citicIndustry codes (1008001xx) only; swIndustry codes and unknown values are rejected with 100005", collectList, [])
|
|
138
|
+
.option("--category <name>", "Message type: text/image/documents/url", collectList, [])
|
|
139
|
+
.option("--tag <name>", "Tag: roadShow/research/strategyMeeting/meetingSummary/industryComment/companyComment/earningsReview", collectList, [])
|
|
140
|
+
.option("--format <format>", "Output format", "table")
|
|
141
|
+
.option("--output <path>")
|
|
142
|
+
.action((options) => emit(options, (client) => client.call("vault.wechat-message.list", buildWechatMessageListBody(options))));
|
|
143
|
+
vault.command("wechat-chatroom-list")
|
|
144
|
+
.option("--from <number>", "Starting offset", "0")
|
|
145
|
+
.option("--size <number>", "Total rows to return; omit to fetch all")
|
|
146
|
+
.option("--room-name <name>", "WeChat group name; repeat or comma-separate for multiple names", collectList, [])
|
|
147
|
+
.option("--format <format>", "Output format", "table")
|
|
148
|
+
.option("--output <path>")
|
|
149
|
+
.action((options) => emit(options, (client) => client.call("vault.wechat-chatroom.list", buildWechatChatroomListBody(options))));
|
|
150
|
+
query(vault, "stock-pool-list", { endpoint: "vault.stock-pool.list", fields: [format(), output()] });
|
|
151
|
+
vault.command("stock-pool-stocks")
|
|
152
|
+
.option("--pool-id <id>", "Pool ID; repeat for multiple; omit (or 'all') for all pools", collectList)
|
|
153
|
+
.option("--format <format>", "Output format", "table")
|
|
154
|
+
.option("--output <path>")
|
|
155
|
+
.action((options) => emit(options, (client) => client.call("vault.stock-pool.stocks", buildStockPoolStocksBody(options))));
|
|
156
|
+
query(vault, "stock-pool-create", {
|
|
157
|
+
endpoint: "vault.stock-pool.create",
|
|
158
|
+
fields: [
|
|
159
|
+
required("--name <name>", "New pool name; max 10 characters (a CJK character counts as 1) and must differ from every existing pool name. A duplicate is rejected with 230006, an over-long name with 230007. Max 30 pools per account (230003)", "poolName"),
|
|
160
|
+
format(), output(),
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
query(vault, "stock-pool-rename", {
|
|
164
|
+
endpoint: "vault.stock-pool.rename",
|
|
165
|
+
fields: [
|
|
166
|
+
required("--pool-id <id>", "Pool ID from 'stock-pool-list'", "poolId"),
|
|
167
|
+
required("--name <name>", "New pool name; same rules as stock-pool-create (max 10 characters, must differ from every existing pool name). Renaming a pool to its own current name succeeds", "poolName"),
|
|
168
|
+
format(), output(),
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
vault.command("stock-pool-add-stock")
|
|
172
|
+
.requiredOption("--pool-id <id>", "Target pool ID from 'stock-pool-list'")
|
|
173
|
+
.requiredOption("--security <code>", "Security code, e.g. 600519.SH (repeat or comma-separate)", collectList)
|
|
174
|
+
.option("--format <format>", "Output format", "table")
|
|
175
|
+
.option("--output <path>")
|
|
176
|
+
.action((options) => emit(options, async (client) => {
|
|
177
|
+
const data = await client.call("vault.stock-pool.add-stock", { poolId: options.poolId, securityCodeList: options.security });
|
|
178
|
+
flagFailedItems(data, "vault stock-pool-add-stock");
|
|
179
|
+
return data;
|
|
180
|
+
}));
|
|
181
|
+
vault.command("stock-pool-remove-stock")
|
|
182
|
+
.requiredOption("--pool-id <id>", "Target pool ID from 'stock-pool-list'")
|
|
183
|
+
.requiredOption("--security <code>", "Security code, e.g. 600519.SH (repeat or comma-separate)", collectList)
|
|
184
|
+
.option("--format <format>", "Output format", "table")
|
|
185
|
+
.option("--output <path>")
|
|
186
|
+
.action((options) => emit(options, async (client) => {
|
|
187
|
+
const data = await client.call("vault.stock-pool.remove-stock", { poolId: options.poolId, securityCodeList: options.security });
|
|
188
|
+
flagFailedItems(data, "vault stock-pool-remove-stock");
|
|
189
|
+
return data;
|
|
190
|
+
}));
|
|
191
|
+
vault.command("stock-pool-delete")
|
|
192
|
+
.requiredOption("--pool-id <id>", "Pool ID to delete; repeat or comma-separate for multiple", collectList)
|
|
193
|
+
.option("--yes", "Required: confirm the deletion")
|
|
194
|
+
.option("--format <format>", "Output format", "table")
|
|
195
|
+
.option("--output <path>")
|
|
196
|
+
.action(async (options) => {
|
|
197
|
+
assertConfirmed("vault.stock-pool.delete", Boolean(options.yes), options.poolId.join("、"));
|
|
198
|
+
await emit(options, async (client) => {
|
|
199
|
+
const data = await client.call("vault.stock-pool.delete", { poolIdList: options.poolId });
|
|
200
|
+
flagFailedItems(data, "vault stock-pool-delete");
|
|
201
|
+
return data;
|
|
202
|
+
});
|
|
203
|
+
});
|
package/dist/src/core/args.js
CHANGED
|
@@ -16,11 +16,14 @@ export function parseNumberOption(value, optionName, config = {}) {
|
|
|
16
16
|
if (value === undefined || String(value).trim() === "") {
|
|
17
17
|
throw new ValidationError(`Invalid ${optionName}: expected a number`);
|
|
18
18
|
}
|
|
19
|
+
if (typeof value === "string" && !(config.integer ? /^-?\d+$/ : /^-?\d+(\.\d+)?$/).test(value.trim())) {
|
|
20
|
+
throw new ValidationError(`Invalid ${optionName}: expected ${config.integer ? "an integer" : "a number"} written in plain decimal, got '${value}'`);
|
|
21
|
+
}
|
|
19
22
|
const parsed = typeof value === "number" ? value : Number(value);
|
|
20
23
|
if (!Number.isFinite(parsed)) {
|
|
21
24
|
throw new ValidationError(`Invalid ${optionName}: expected a finite number`);
|
|
22
25
|
}
|
|
23
|
-
if (config.integer && !Number.
|
|
26
|
+
if (config.integer && !Number.isSafeInteger(parsed)) {
|
|
24
27
|
throw new ValidationError(`Invalid ${optionName}: expected an integer`);
|
|
25
28
|
}
|
|
26
29
|
if (config.min !== undefined && parsed < config.min) {
|
|
@@ -40,10 +43,10 @@ export function parseFrom(value) {
|
|
|
40
43
|
export function parseSize(value) {
|
|
41
44
|
return parseOptionalNumberOption(value, "--size", { integer: true, min: 1 });
|
|
42
45
|
}
|
|
43
|
-
export function
|
|
44
|
-
return [
|
|
46
|
+
export function numberListArg(optionName) {
|
|
47
|
+
return (value, previous = []) => [
|
|
45
48
|
...previous,
|
|
46
|
-
...splitCsv(value).map((item) => parseNumberOption(item,
|
|
49
|
+
...splitCsv(value).map((item) => parseNumberOption(item, optionName, { integer: true })),
|
|
47
50
|
];
|
|
48
51
|
}
|
|
49
52
|
export function collectKeyValue(value, previous = {}) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ApiError } from "./errors.js";
|
|
2
2
|
import { printData } from "./printer.js";
|
|
3
3
|
import { isTransientError } from "./transport.js";
|
|
4
|
+
import { markFailed } from "./exitStatus.js";
|
|
4
5
|
export const POLL_MAX_ATTEMPTS = 14;
|
|
5
6
|
const POLL_INITIAL_DELAY_MS = 5_000;
|
|
6
7
|
const POLL_MAX_DELAY_MS = 30_000;
|
|
@@ -24,34 +25,46 @@ function terminalFailureLine(error) {
|
|
|
24
25
|
return `Content generation failed (terminal${code})${trace}${detail}\n`
|
|
25
26
|
+ "This dataId is final — re-checking it will not change. Resubmitting the generation task bills again for the same result; change the parameters first.\n";
|
|
26
27
|
}
|
|
28
|
+
export async function pollUntilDone(attempt, notReady) {
|
|
29
|
+
for (let n = 1; n <= POLL_MAX_ATTEMPTS; n++) {
|
|
30
|
+
try {
|
|
31
|
+
const state = await attempt();
|
|
32
|
+
if (state !== "pending")
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (!isTransientError(error))
|
|
37
|
+
throw error;
|
|
38
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
39
|
+
process.stderr.write(`Attempt ${n}/${POLL_MAX_ATTEMPTS}: transient error (${msg.slice(0, 80)}), continuing to wait...\n`);
|
|
40
|
+
}
|
|
41
|
+
if (n < POLL_MAX_ATTEMPTS) {
|
|
42
|
+
const delay = nextPollDelayMs(n);
|
|
43
|
+
process.stderr.write(`Attempt ${n}/${POLL_MAX_ATTEMPTS}: ${notReady}, retrying in ${Math.round(delay / 1000)}s...\n`);
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return "timeout";
|
|
48
|
+
}
|
|
27
49
|
export async function pollAsyncContent(client, getContentEndpoint, dataId, format, output) {
|
|
28
|
-
|
|
50
|
+
return pollUntilDone(async () => {
|
|
29
51
|
try {
|
|
30
52
|
const result = await client.call(getContentEndpoint, { dataId });
|
|
31
|
-
if (result?.content
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
53
|
+
if (result?.content == null)
|
|
54
|
+
return "pending";
|
|
55
|
+
await printData(result, format, output);
|
|
56
|
+
return "ok";
|
|
35
57
|
}
|
|
36
58
|
catch (error) {
|
|
37
59
|
if (isAsyncFailed(error)) {
|
|
38
60
|
process.stderr.write(terminalFailureLine(error));
|
|
39
61
|
return "failed";
|
|
40
62
|
}
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
45
|
-
process.stderr.write(`Attempt ${attempt}/${POLL_MAX_ATTEMPTS}: transient error (${msg.slice(0, 80)}), continuing to wait...\n`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (attempt < POLL_MAX_ATTEMPTS) {
|
|
49
|
-
const delay = nextPollDelayMs(attempt);
|
|
50
|
-
process.stderr.write(`Attempt ${attempt}/${POLL_MAX_ATTEMPTS}: content not ready, retrying in ${Math.round(delay / 1000)}s...\n`);
|
|
51
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
63
|
+
if (isAsyncPending(error))
|
|
64
|
+
return "pending";
|
|
65
|
+
throw error;
|
|
52
66
|
}
|
|
53
|
-
}
|
|
54
|
-
return "timeout";
|
|
67
|
+
}, "content not ready");
|
|
55
68
|
}
|
|
56
69
|
export async function checkAsyncContent(client, getContentEndpoint, dataId, format, output) {
|
|
57
70
|
try {
|
|
@@ -64,7 +77,7 @@ export async function checkAsyncContent(client, getContentEndpoint, dataId, form
|
|
|
64
77
|
catch (error) {
|
|
65
78
|
if (isAsyncFailed(error)) {
|
|
66
79
|
process.stderr.write(terminalFailureLine(error));
|
|
67
|
-
|
|
80
|
+
markFailed();
|
|
68
81
|
return;
|
|
69
82
|
}
|
|
70
83
|
if (!isAsyncPending(error))
|
package/dist/src/core/client.js
CHANGED
|
@@ -10,7 +10,13 @@ import { getLookupData } from "./lookupData/index.js";
|
|
|
10
10
|
import { stagingPath } from "./output.js";
|
|
11
11
|
import { decodeResponseBody, getDispatcher, isVerbose, logTiming, markRetryable, PAGE_CONCURRENCY, parseRetryAfterMs, quoteBigIntFields, runInOrder, withRetry } from "./transport.js";
|
|
12
12
|
import { attachRowSink } from "./rowSink.js";
|
|
13
|
+
import { markIncomplete } from "./exitStatus.js";
|
|
13
14
|
const AUTH_RETRY_CODES = new Set(["8000014", "8000015", "0000001008", "999002"]);
|
|
15
|
+
function hasExpectedShape(expects, payload) {
|
|
16
|
+
if (expects === "array")
|
|
17
|
+
return Array.isArray(payload);
|
|
18
|
+
return Boolean(payload && typeof payload === "object" && Array.isArray(payload.list));
|
|
19
|
+
}
|
|
14
20
|
export class GangtiseClient {
|
|
15
21
|
config;
|
|
16
22
|
rowSink;
|
|
@@ -170,7 +176,7 @@ export class GangtiseClient {
|
|
|
170
176
|
});
|
|
171
177
|
if (!this.isPaginatedListResponse(firstPage)) {
|
|
172
178
|
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`);
|
|
173
|
-
|
|
179
|
+
markIncomplete();
|
|
174
180
|
return firstPage;
|
|
175
181
|
}
|
|
176
182
|
const total = firstPage.total;
|
|
@@ -276,7 +282,7 @@ export class GangtiseClient {
|
|
|
276
282
|
failedPages.push(req);
|
|
277
283
|
return [];
|
|
278
284
|
}
|
|
279
|
-
}, (list) => keep(list));
|
|
285
|
+
}, (list) => keep(list), PAGE_CONCURRENCY * 4);
|
|
280
286
|
if (unexpectedShape) {
|
|
281
287
|
process.stderr.write(`[gangtise] warning: a page response had unexpected shape; its rows are missing (counted in failedPages)\n`);
|
|
282
288
|
}
|
|
@@ -393,8 +399,9 @@ export class GangtiseClient {
|
|
|
393
399
|
this.throwHttpError(parsed, response.statusCode, retryAfterMs);
|
|
394
400
|
}
|
|
395
401
|
const payload = this.unwrapEnvelope(parsed, response.statusCode, retryAfterMs);
|
|
396
|
-
if (endpoint.expects
|
|
397
|
-
|
|
402
|
+
if (endpoint.expects && !hasExpectedShape(endpoint.expects, payload)) {
|
|
403
|
+
const got = payload === null ? "null" : Array.isArray(payload) ? "an array" : typeof payload;
|
|
404
|
+
throw markStructural(new ApiError(`${endpoint.key} returned no ${endpoint.expects} payload (got ${got}) — the response layout may have changed`, undefined, response.statusCode, parsed));
|
|
398
405
|
}
|
|
399
406
|
return payload;
|
|
400
407
|
}
|
package/dist/src/core/config.js
CHANGED
|
@@ -3,15 +3,28 @@ import path from "node:path";
|
|
|
3
3
|
export const DEFAULT_BASE_URL = "https://openapi.gangtise.com";
|
|
4
4
|
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
5
5
|
export const DEFAULT_TOKEN_CACHE_PATH = path.join(os.homedir(), ".config", "gangtise", "token.json");
|
|
6
|
+
let timeoutWarned = false;
|
|
6
7
|
export function loadConfig() {
|
|
7
|
-
const
|
|
8
|
-
const timeoutMs =
|
|
8
|
+
const rawTimeout = process.env.GANGTISE_TIMEOUT_MS?.trim();
|
|
9
|
+
const timeoutMs = resolveTimeoutEnv(rawTimeout);
|
|
10
|
+
if (rawTimeout && Number(rawTimeout) !== timeoutMs && !timeoutWarned) {
|
|
11
|
+
timeoutWarned = true;
|
|
12
|
+
process.stderr.write(`[gangtise] warning: GANGTISE_TIMEOUT_MS=${rawTimeout} is not in effect, using ${timeoutMs} ms: expected whole milliseconds from ${MIN_TIMEOUT_MS} to ${MAX_TIMEOUT_MS}\n`);
|
|
13
|
+
}
|
|
9
14
|
return {
|
|
10
15
|
baseUrl: process.env.GANGTISE_BASE_URL ?? DEFAULT_BASE_URL,
|
|
11
|
-
timeoutMs
|
|
16
|
+
timeoutMs,
|
|
12
17
|
accessKey: process.env.GANGTISE_ACCESS_KEY,
|
|
13
18
|
secretKey: process.env.GANGTISE_SECRET_KEY,
|
|
14
19
|
token: process.env.GANGTISE_TOKEN,
|
|
15
20
|
tokenCachePath: process.env.GANGTISE_TOKEN_CACHE_PATH ?? DEFAULT_TOKEN_CACHE_PATH,
|
|
16
21
|
};
|
|
17
22
|
}
|
|
23
|
+
export const MIN_TIMEOUT_MS = 1_000;
|
|
24
|
+
export const MAX_TIMEOUT_MS = 3_600_000;
|
|
25
|
+
export function resolveTimeoutEnv(raw) {
|
|
26
|
+
if (!raw || !/^\d+$/.test(raw.trim()))
|
|
27
|
+
return DEFAULT_TIMEOUT_MS;
|
|
28
|
+
const ms = Number(raw);
|
|
29
|
+
return ms < MIN_TIMEOUT_MS ? DEFAULT_TIMEOUT_MS : Math.min(MAX_TIMEOUT_MS, ms);
|
|
30
|
+
}
|
|
@@ -41,6 +41,7 @@ const ENDPOINT_DEFS = {
|
|
|
41
41
|
kind: "json",
|
|
42
42
|
description: "Get domestic chief opinion bodies by ID (max 20 IDs per call)",
|
|
43
43
|
retry: "no-replay",
|
|
44
|
+
expects: "array",
|
|
44
45
|
},
|
|
45
46
|
"insight.summary.list": {
|
|
46
47
|
method: "POST",
|
|
@@ -198,6 +199,7 @@ const ENDPOINT_DEFS = {
|
|
|
198
199
|
kind: "json",
|
|
199
200
|
description: "Get foreign opinion bodies by ID (max 20 IDs per call)",
|
|
200
201
|
retry: "no-replay",
|
|
202
|
+
expects: "array",
|
|
201
203
|
},
|
|
202
204
|
"insight.independent-opinion.list": {
|
|
203
205
|
method: "POST",
|
|
@@ -433,6 +435,7 @@ const ENDPOINT_DEFS = {
|
|
|
433
435
|
path: "/application/open-fundamental/valuation-analysis",
|
|
434
436
|
kind: "json",
|
|
435
437
|
description: "Query valuation analysis",
|
|
438
|
+
expects: "list",
|
|
436
439
|
},
|
|
437
440
|
"fundamental.top-holders": {
|
|
438
441
|
method: "POST",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const EXIT_FAILED = 1;
|
|
2
|
+
export const EXIT_INCOMPLETE = 3;
|
|
3
|
+
export const EXIT_SUPERSEDED = 4;
|
|
4
|
+
export function markFailed() {
|
|
5
|
+
process.exitCode = EXIT_FAILED;
|
|
6
|
+
}
|
|
7
|
+
export function markIncomplete() {
|
|
8
|
+
if (process.exitCode !== EXIT_FAILED)
|
|
9
|
+
process.exitCode = EXIT_INCOMPLETE;
|
|
10
|
+
}
|
|
11
|
+
export function markSuperseded() {
|
|
12
|
+
if (process.exitCode !== EXIT_FAILED && process.exitCode !== EXIT_INCOMPLETE)
|
|
13
|
+
process.exitCode = EXIT_SUPERSEDED;
|
|
14
|
+
}
|
|
15
|
+
export function decidedExitCode() {
|
|
16
|
+
return Number(process.exitCode ?? 0);
|
|
17
|
+
}
|