gangtise-openapi-cli 0.40.1 → 0.41.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 +152 -12
- package/dist/src/cli.js +229 -4
- package/dist/src/core/client.js +33 -8
- package/dist/src/core/driveUpload.js +29 -0
- package/dist/src/core/endpoints.js +226 -4
- package/dist/src/core/errors.js +5 -2
- package/dist/src/core/normalize.js +1 -1
- package/dist/src/version.js +1 -1
- package/gangtise-openapi/SKILL.md +67 -16
- package/gangtise-openapi/references/commands/alternative.md +14 -7
- package/gangtise-openapi/references/commands/bond.md +132 -0
- package/gangtise-openapi/references/commands/insight.md +33 -5
- package/gangtise-openapi/references/commands/reference-and-lookup.md +10 -1
- package/gangtise-openapi/references/commands/tool.md +35 -0
- package/gangtise-openapi/references/commands/vault.md +27 -1
- package/gangtise-openapi/references/errors.md +3 -3
- package/gangtise-openapi/references/examples.md +13 -9
- package/gangtise-openapi/references/response-schema.md +18 -4
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { releaseClaim, resolveTitle, saveDownloadResult, uniquePath } from "./co
|
|
|
12
12
|
import { resolveCalendarType } from "./core/calendarType.js";
|
|
13
13
|
import { ENDPOINTS, listEndpoints } from "./core/endpoints.js";
|
|
14
14
|
import { ApiError, ConfigError, ValidationError } from "./core/errors.js";
|
|
15
|
+
import { uploadDriveFile } from "./core/driveUpload.js";
|
|
15
16
|
import { fetchFileParseResult, pollFileParseResult, submitFileParse } from "./core/fileParse.js";
|
|
16
17
|
import { assertColumnarHeader, flagFailedItems, flagMissingFields, normalizeRows, zipFieldRow } from "./core/normalize.js";
|
|
17
18
|
import { parseOutputFormat } from "./core/output.js";
|
|
@@ -172,7 +173,7 @@ const independentOpinion = new Command("independent-opinion");
|
|
|
172
173
|
const officialAccount = new Command("official-account");
|
|
173
174
|
const qa = new Command("qa");
|
|
174
175
|
const reportImage = new Command("report-image");
|
|
175
|
-
addTimeFilters(opinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--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", collectList, []).option("--chief <id>", "Chief ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--concept <id>", "Concept ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--source <source>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.opinion.list", {
|
|
176
|
+
addTimeFilters(opinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--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", collectList, []).option("--chief <id>", "Chief ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--concept <id>", "Concept ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--source <source>", "Source", collectList, []).option("--with-content", "Return the full body inline (v1 list, 30 credits/row) instead of the 200-char brief (1 credit/row). v1 shape: title and body sit under contentList.title / contentList.content, and there is no brief").option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call(options.withContent ? "insight.opinion.list-with-content" : "insight.opinion.list", {
|
|
176
177
|
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
|
|
177
178
|
rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), chiefList: maybeArray(options.chief),
|
|
178
179
|
securityList: maybeArray(options.security), brokerList: maybeArray(options.broker), industryList: maybeArray(options.industry), conceptList: maybeArray(options.concept),
|
|
@@ -336,7 +337,7 @@ addTimeFilters(announcementUs.command("list").addOption(new Option("--search-typ
|
|
|
336
337
|
securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
|
|
337
338
|
}), { endpointKey: "insight.announcement-us.list", idField: "announcementId" }));
|
|
338
339
|
addDownloadCommand(announcementUs, { endpointKey: "insight.announcement-us.download", idOption: "--announcement-id", idField: "announcementId", fallbackPrefix: "announcement-us", fileType: { description: "File type: 1=original PDF 2=Markdown", choices: ["1", "2"], default: "1" }, titleListEndpoint: "insight.announcement-us.list" });
|
|
339
|
-
addTimeFilters(foreignOpinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code (e.g. UBER.N)", collectList, []).option("--region <code>", "Region code -- this endpoint accepts only cn/cnHk/cnTw/us/jp/uk; the other 13 values of regionCategory (sea/gl/fr/de/kr/in/ca/me/othAs/othEur/latAm/oce/af) are rejected here with 100005 though they all work on foreign-report", collectList, []).option("--industry <id>", "Industry ID -- swIndustry codes only (104xx0000); citicIndustry codes are rejected with 100005 even where constant-category declares them", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.foreign-opinion.list", {
|
|
340
|
+
addTimeFilters(foreignOpinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code (e.g. UBER.N)", collectList, []).option("--region <code>", "Region code -- this endpoint accepts only cn/cnHk/cnTw/us/jp/uk; the other 13 values of regionCategory (sea/gl/fr/de/kr/in/ca/me/othAs/othEur/latAm/oce/af) are rejected here with 100005 though they all work on foreign-report", collectList, []).option("--industry <id>", "Industry ID -- swIndustry codes only (104xx0000); citicIndustry codes are rejected with 100005 even where constant-category declares them", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--with-content", "Return the full body inline (v1 list, 30 credits/row) as content / contentTranslate, instead of the 200-char brief (1 credit/row)").option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call(options.withContent ? "insight.foreign-opinion.list-with-content" : "insight.foreign-opinion.list", {
|
|
340
341
|
from: parseFrom(options.from), size: parseSize(options.size),
|
|
341
342
|
startTime: options.startTime, endTime: options.endTime,
|
|
342
343
|
rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
|
|
@@ -345,6 +346,53 @@ addTimeFilters(foreignOpinion.command("list").addOption(new Option("--rank-type
|
|
|
345
346
|
securityList: maybeArray(options.security), brokerList: maybeArray(options.broker),
|
|
346
347
|
ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
|
|
347
348
|
})));
|
|
349
|
+
async function fetchOpinionDetails(client, endpointKey, idListField, idField, ids) {
|
|
350
|
+
const BATCH = 20;
|
|
351
|
+
const unique = [...new Set(ids)];
|
|
352
|
+
if (!unique.length)
|
|
353
|
+
throw new ValidationError("pass at least one opinion ID");
|
|
354
|
+
const list = [];
|
|
355
|
+
let unfetched = [];
|
|
356
|
+
let unfetchedError;
|
|
357
|
+
for (let i = 0; i < unique.length; i += BATCH) {
|
|
358
|
+
try {
|
|
359
|
+
const rows = await client.call(endpointKey, { [idListField]: unique.slice(i, i + BATCH) });
|
|
360
|
+
if (Array.isArray(rows))
|
|
361
|
+
list.push(...rows);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
if (i === 0)
|
|
365
|
+
throw error;
|
|
366
|
+
unfetched = unique.slice(i);
|
|
367
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
368
|
+
unfetchedError = { message, ...(error instanceof ApiError ? { code: error.code, traceId: error.traceId } : {}) };
|
|
369
|
+
process.stderr.write(`[gangtise] warning: ${endpointKey} failed on batch ${i / BATCH + 1} (${message}); ${unfetched.length} ID(s) not fetched: ${unfetched.join(", ")}. The bodies already returned are kept below — re-run for the IDs in unfetchedIds only.\n`);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const returned = new Set(list.map((row) => String(row[idField])));
|
|
374
|
+
const missing = unique.filter((id) => !returned.has(id) && !unfetched.includes(id));
|
|
375
|
+
const out = { total: list.length, list };
|
|
376
|
+
if (missing.length > 0) {
|
|
377
|
+
process.stderr.write(`[gangtise] warning: no body returned for ${missing.length} ID(s): ${missing.join(", ")} — the server skips an ID it has no body for without an error (e.g. a wrong ID; a just-published opinion may also not have its body yet). Result marked partial (exit 3).\n`);
|
|
378
|
+
out.missingIds = missing;
|
|
379
|
+
}
|
|
380
|
+
if (unfetched.length > 0) {
|
|
381
|
+
out.unfetchedIds = unfetched;
|
|
382
|
+
out.unfetchedError = unfetchedError;
|
|
383
|
+
}
|
|
384
|
+
if (missing.length > 0 || unfetched.length > 0)
|
|
385
|
+
out.partial = true;
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
opinion.command("detail").description("Full bodies of domestic chief opinions by ID (30 credits per returned opinion; batched 20 per call)")
|
|
389
|
+
.requiredOption("--chief-opinion-id <id>", "chiefOpinionId from 'insight opinion list' (repeat or comma-separate)", collectList)
|
|
390
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
391
|
+
.action((options) => emit(options, (client) => fetchOpinionDetails(client, "insight.opinion.detail", "chiefOpinionIdList", "chiefOpinionId", options.chiefOpinionId)));
|
|
392
|
+
foreignOpinion.command("detail").description("Full bodies (content + contentTranslate) of foreign opinions by ID (30 credits per returned opinion; batched 20 per call)")
|
|
393
|
+
.requiredOption("--foreign-opinion-id <id>", "foreignOpinionId from 'insight foreign-opinion list' (repeat or comma-separate)", collectList)
|
|
394
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
395
|
+
.action((options) => emit(options, (client) => fetchOpinionDetails(client, "insight.foreign-opinion.detail", "foreignOpinionIdList", "foreignOpinionId", options.foreignOpinionId)));
|
|
348
396
|
addTimeFilters(independentOpinion.command("list").addOption(new Option("--rank-type <number>", "Rank type: 1=composite 2=time desc").choices(["1", "2"]).default("1")).option("--security <code>", "Security code (e.g. GSK.N)", collectList, []).option("--industry <id>", "Industry ID -- swIndustry codes only (104xx0000); citicIndustry codes are rejected with 100005 even where constant-category declares them", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.independent-opinion.list", {
|
|
349
397
|
from: parseFrom(options.from), size: parseSize(options.size),
|
|
350
398
|
startTime: options.startTime, endTime: options.endTime,
|
|
@@ -393,6 +441,20 @@ insight.addCommand(independentOpinion);
|
|
|
393
441
|
insight.addCommand(officialAccount);
|
|
394
442
|
insight.addCommand(qa);
|
|
395
443
|
insight.addCommand(reportImage);
|
|
444
|
+
const highlight = new Command("highlight");
|
|
445
|
+
highlight.command("list").description("Meeting highlights feed: key takeaways per meeting, newest first (5 credits/row)")
|
|
446
|
+
.option("--from <number>", "Starting offset", "0")
|
|
447
|
+
.option("--size <number>", "Total rows to return; omit to fetch all (max page 50). Billed per row — bound this on wide ranges")
|
|
448
|
+
.option("--start-time <datetime>", "Start time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)", datetimeArg("--start-time"))
|
|
449
|
+
.option("--end-time <datetime>", "End time (yyyy-MM-dd or yyyy-MM-dd HH:mm:ss)", datetimeArg("--end-time"))
|
|
450
|
+
.option("--security <code>", "Security code, e.g. 000001.SZ / 09992.HK / AAPL.O (repeatable)", collectList, [])
|
|
451
|
+
.option("--research-area <id>", "Research area ID: citicIndustry (1008001xx) or gangtiseIndustry direction (122000xxx). swIndustry codes are NOT accepted here", collectList, [])
|
|
452
|
+
.option("--format <format>", "Output format", "table").option("--output <path>")
|
|
453
|
+
.action((options) => emit(options, (client) => client.call("insight.highlight.list", {
|
|
454
|
+
from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
|
|
455
|
+
securityList: maybeArray(options.security), researchAreaList: maybeArray(options.researchArea),
|
|
456
|
+
})));
|
|
457
|
+
insight.addCommand(highlight);
|
|
396
458
|
program.addCommand(insight);
|
|
397
459
|
const quote = new Command("quote").description("Quote APIs");
|
|
398
460
|
const KLINE_MARKETS = { aShares: 1, hkStocks: 2, usStocks: 1 };
|
|
@@ -560,6 +622,79 @@ fundamental.command("earning-forecast").requiredOption("--security-code <code>")
|
|
|
560
622
|
return client.call("fundamental.earning-forecast", { securityCode: options.securityCode, startDate, endDate, consensusList: maybeArray(options.consensus) });
|
|
561
623
|
}));
|
|
562
624
|
program.addCommand(fundamental);
|
|
625
|
+
const bond = new Command("bond").description("Bond APIs: profiles, issuers, quotes, valuations, cash flows, ratings, announcements");
|
|
626
|
+
function requireBondCodes(codes, max) {
|
|
627
|
+
if (!codes.length)
|
|
628
|
+
throw new ValidationError("--security is required: pass one or more bond codes, e.g. --security 019742.SH --security 220205.IB");
|
|
629
|
+
const unique = [...new Set(codes)];
|
|
630
|
+
if (max && unique.length > max)
|
|
631
|
+
throw new ValidationError(`${unique.length} distinct bond codes in one call — this endpoint takes at most ${max}. Split them into batches of ${max} and run one call per batch.`);
|
|
632
|
+
return unique;
|
|
633
|
+
}
|
|
634
|
+
const bondSecurityOption = (command) => command
|
|
635
|
+
.option("--security <code>", "Bond code, e.g. 019742.SH / 123456.SZ / 220205.IB (repeatable). Short names and pinyin are rejected — resolve them with 'reference securities-search' first", collectList, [])
|
|
636
|
+
.option("--field <field>", "Field to return (repeatable); omit for all. An unsupported name rejects the whole call with 100003", collectList, [])
|
|
637
|
+
.option("--format <format>", "Output format", "table").option("--output <path>");
|
|
638
|
+
const addBondRange = (name, endpointKey, describe, rangeHelp, max) => bondSecurityOption(bond.command(name).description(describe))
|
|
639
|
+
.option("--start-date <date>", `${rangeHelp} start (yyyy-MM-dd); omit for all history`, dateArg("--start-date"))
|
|
640
|
+
.option("--end-date <date>", `${rangeHelp} end (yyyy-MM-dd); omit for all history`, dateArg("--end-date"))
|
|
641
|
+
.action((options) => emit(options, (client) => client.call(endpointKey, { securityList: requireBondCodes(options.security, max), startDate: options.startDate, endDate: options.endDate, fieldList: maybeArray(options.field) })));
|
|
642
|
+
bondSecurityOption(bond.command("basic-info").description("Bond static profiles: issuance, term, coupon, rating, guarantee, special terms"))
|
|
643
|
+
.action((options) => emit(options, (client) => client.call("bond.basic-info", { securityList: requireBondCodes(options.security, 10000), fieldList: maybeArray(options.field) })));
|
|
644
|
+
bondSecurityOption(bond.command("rating-overview").description("Bond, issuer and guarantor ratings side by side"))
|
|
645
|
+
.action((options) => emit(options, (client) => client.call("bond.rating-overview", { securityList: requireBondCodes(options.security, 10), fieldList: maybeArray(options.field) })));
|
|
646
|
+
addBondRange("cash-flow", "bond.cash-flow", "Interest payment and redemption schedule per bond", "Payment date");
|
|
647
|
+
addBondRange("issuance-detail", "bond.issuance-detail", "Issuance and re-issuance records: bidding, pricing, cover ratios", "Issue announcement date");
|
|
648
|
+
addBondRange("rating-change", "bond.rating-change", "Bond rating change history: current vs previous rating, direction, outlook, agency (max 10 bonds per call)", "Announcement date", 10);
|
|
649
|
+
addBondRange("exercise-notice", "bond.exercise-notice", "Put/call exercise schedule and results for option-embedded bonds", "Exercise date");
|
|
650
|
+
bondSecurityOption(bond.command("daily-quote").description("Daily close quotes (exchange + CFETS): dirty/clean price, YTM, duration, convexity"))
|
|
651
|
+
.requiredOption("--start-date <date>", "Trade date range start (yyyy-MM-dd)", dateArg("--start-date"))
|
|
652
|
+
.requiredOption("--end-date <date>", "Trade date range end (yyyy-MM-dd)", dateArg("--end-date"))
|
|
653
|
+
.action((options) => emit(options, (client) => client.call("bond.daily-quote", { securityList: requireBondCodes(options.security), startDate: options.startDate, endDate: options.endDate, fieldList: maybeArray(options.field) })));
|
|
654
|
+
bondSecurityOption(bond.command("valuation").description("Shanghai Clearing House valuations: price, yield, duration, convexity, PVBP"))
|
|
655
|
+
.requiredOption("--start-date <date>", "Valuation date range start (yyyy-MM-dd)", dateArg("--start-date"))
|
|
656
|
+
.requiredOption("--end-date <date>", "Valuation date range end (yyyy-MM-dd)", dateArg("--end-date"))
|
|
657
|
+
.addOption(new Option("--confidence-level <level>", "Valuation confidence; defaults to 推荐 server-side, which already filters out 不推荐").choices(["推荐", "不推荐"]))
|
|
658
|
+
.action((options) => emit(options, (client) => client.call("bond.valuation", { securityList: requireBondCodes(options.security), startDate: options.startDate, endDate: options.endDate, confidenceLevel: options.confidenceLevel, fieldList: maybeArray(options.field) })));
|
|
659
|
+
function issuerSelector(security, issuer) {
|
|
660
|
+
if (security.length && issuer.length)
|
|
661
|
+
throw new ValidationError("--security and --issuer are mutually exclusive: pass bond codes or issuer names, not both");
|
|
662
|
+
if (!security.length && !issuer.length)
|
|
663
|
+
throw new ValidationError("pass either --security (bond codes) or --issuer (issuer names)");
|
|
664
|
+
return security.length ? { securityList: security } : { issuerNameList: issuer };
|
|
665
|
+
}
|
|
666
|
+
const issuerNameOption = (command) => command.option("--issuer <name>", "Issuer name, full or short (repeatable); fuzzy-matched, one best hit per name", collectList, []);
|
|
667
|
+
issuerNameOption(bondSecurityOption(bond.command("issuer-info").description("Issuer profiles: nature, SW industry, registration, rating, outstanding bonds")))
|
|
668
|
+
.action((options) => emit(options, (client) => client.call("bond.issuer-info", { ...issuerSelector(options.security, options.issuer), fieldList: maybeArray(options.field) })));
|
|
669
|
+
issuerNameOption(bondSecurityOption(bond.command("issuer-rating-change").description("Issuer rating change history (at most 10 issuers matched per call)")))
|
|
670
|
+
.option("--start-date <date>", "Announcement date range start (yyyy-MM-dd); omit for all history", dateArg("--start-date"))
|
|
671
|
+
.option("--end-date <date>", "Announcement date range end (yyyy-MM-dd); omit for all history", dateArg("--end-date"))
|
|
672
|
+
.action((options) => emit(options, (client) => client.call("bond.issuer-rating-change", { ...issuerSelector(options.security, options.issuer), startDate: options.startDate, endDate: options.endDate, fieldList: maybeArray(options.field) })));
|
|
673
|
+
bond.command("issuance-plan").description("Rate-bond issuance calendar over a date range")
|
|
674
|
+
.requiredOption("--start-date <date>", "Issue date range start (yyyy-MM-dd)", dateArg("--start-date"))
|
|
675
|
+
.requiredOption("--end-date <date>", "Issue date range end (yyyy-MM-dd)", dateArg("--end-date"))
|
|
676
|
+
.option("--field <field>", "Field to return (repeatable); omit for all", collectList, [])
|
|
677
|
+
.option("--format <format>", "Output format", "table").option("--output <path>")
|
|
678
|
+
.action((options) => emit(options, (client) => client.call("bond.issuance-plan", { startDate: options.startDate, endDate: options.endDate, fieldList: maybeArray(options.field) })));
|
|
679
|
+
bondSecurityOption(bond.command("announcement").description("Bond announcements, paged. Filter by bond codes OR by announcement date range, never both"))
|
|
680
|
+
.option("--start-date <date>", "Announcement date range start (yyyy-MM-dd); mutually exclusive with --security", dateArg("--start-date"))
|
|
681
|
+
.option("--end-date <date>", "Announcement date range end (yyyy-MM-dd); mutually exclusive with --security", dateArg("--end-date"))
|
|
682
|
+
.option("--page-no <number>", "Page number, from 1. No total is returned: increment until a page comes back empty", "1")
|
|
683
|
+
.option("--page-size <number>", "Rows per page, 1-200", "50")
|
|
684
|
+
.action((options) => emit(options, (client) => {
|
|
685
|
+
const byDate = Boolean(options.startDate || options.endDate);
|
|
686
|
+
if (options.security.length && byDate)
|
|
687
|
+
throw new ValidationError("--security and --start-date/--end-date are mutually exclusive on bond announcement: filter by bond codes or by date range, not both");
|
|
688
|
+
if (!options.security.length && !byDate)
|
|
689
|
+
throw new ValidationError("pass either --security (bond codes) or --start-date/--end-date (announcement date range)");
|
|
690
|
+
return client.call("bond.announcement", {
|
|
691
|
+
securityList: maybeArray(options.security), startDate: options.startDate, endDate: options.endDate,
|
|
692
|
+
pageNo: parseNumberOption(options.pageNo, "--page-no", { integer: true, min: 1 }),
|
|
693
|
+
pageSize: parseNumberOption(options.pageSize, "--page-size", { integer: true, min: 1, max: 200 }),
|
|
694
|
+
fieldList: maybeArray(options.field),
|
|
695
|
+
});
|
|
696
|
+
}));
|
|
697
|
+
program.addCommand(bond);
|
|
563
698
|
const ai = new Command("ai").description("AI APIs");
|
|
564
699
|
ai.command("knowledge-batch").option("--query <text>", "Query text; repeat for up to 5. Each --query is taken whole — commas inside it are part of the question, not separators", collectText, []).option("--top <number>", "Max results (default: 10, max: 20)", "10").option("--resource-type <number>", "Resource type", collectNumberList, []).option("--knowledge-name <name>", "Knowledge name", collectList, []).option("--start-time <datetime>", "13/10-digit epoch or YYYY-MM-DD[ HH:mm[:ss]] (space or T)").option("--end-time <datetime>", "13/10-digit epoch or YYYY-MM-DD[ HH:mm[:ss]] (space or T)").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => {
|
|
565
700
|
if (!options.query.length)
|
|
@@ -696,6 +831,74 @@ program.addCommand(reference);
|
|
|
696
831
|
const vault = new Command("vault").description("Vault APIs");
|
|
697
832
|
vault.command("drive-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--file-type <number>", "File type", collectNumberList, []).option("--space-type <number>", "Space type", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.drive.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, fileTypeList: options.fileType.length ? options.fileType : undefined, spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), { endpointKey: "vault.drive.list", idField: "fileId" }));
|
|
698
833
|
addDownloadCommand(vault, { endpointKey: "vault.drive.download", name: "drive-download", idOption: "--file-id", idField: "fileId", fallbackPrefix: "file", titleListEndpoint: "vault.drive.list" });
|
|
834
|
+
const driveSpaceOption = (help) => new Option("--space-type <n>", help).choices(["1", "2"]).default("1");
|
|
835
|
+
vault.command("drive-folder-list").description("List the direct subfolders and files of a drive folder (free)")
|
|
836
|
+
.addOption(driveSpaceOption("Space: 1=my drive 2=tenant drive"))
|
|
837
|
+
.option("--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")
|
|
838
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
839
|
+
.action((options) => emit(options, (client) => client.call("vault.drive.folder-list", { spaceType: Number(options.spaceType), parentId: options.parentId })));
|
|
840
|
+
vault.command("drive-upload").description("Upload a file to the AI drive (free; max 100MB per file; trial accounts max 500MB/day)")
|
|
841
|
+
.requiredOption("--file <path>", "File to upload")
|
|
842
|
+
.addOption(driveSpaceOption("Target space: 1=my drive 2=tenant drive (shared with your whole tenant)"))
|
|
843
|
+
.option("--folder-id <id>", "Target folder ID in that space; omit (or 'root') for the space's root")
|
|
844
|
+
.option("--title <name>", "Name in the drive, max 200 characters; defaults to the local file name")
|
|
845
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
846
|
+
.action((options) => emit(options, (client) => uploadDriveFile(client, options.file, { spaceType: Number(options.spaceType), folderId: options.folderId, title: options.title })));
|
|
847
|
+
vault.command("drive-create-folder").description("Create a drive folder (free). Same-name folders are allowed, so re-running creates a second one")
|
|
848
|
+
.requiredOption("--name <name>", "Folder name, max 200 characters (230004 past that)")
|
|
849
|
+
.addOption(driveSpaceOption("Space: 1=my drive 2=tenant drive (shared with your whole tenant)"))
|
|
850
|
+
.option("--parent-id <id>", "Parent folder ID in that space; omit (or 'root') for the root")
|
|
851
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
852
|
+
.action((options) => emit(options, (client) => client.call("vault.drive.create-folder", { folderName: options.name, spaceType: Number(options.spaceType), parentId: options.parentId })));
|
|
853
|
+
vault.command("drive-rename").description("Rename a drive file or folder (free)")
|
|
854
|
+
.addOption(new Option("--type <type>", "What --id names").choices(["file", "folder"]).makeOptionMandatory())
|
|
855
|
+
.requiredOption("--id <id>", "File or folder ID")
|
|
856
|
+
.requiredOption("--name <name>", "New name, max 200 characters")
|
|
857
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
858
|
+
.action((options) => emit(options, (client) => client.call("vault.drive.rename", { type: options.type, id: options.id, name: options.name })));
|
|
859
|
+
vault.command("drive-move-file").description("Move drive files into a folder of the SAME space (free)")
|
|
860
|
+
.requiredOption("--file-id <id>", "File ID (repeat or comma-separate)", collectList)
|
|
861
|
+
.requiredOption("--target-folder-id <id>", "Destination folder ID, or 'root'. Files of the other space land in failList (空间不一致)")
|
|
862
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
863
|
+
.action((options) => emit(options, async (client) => {
|
|
864
|
+
const data = await client.call("vault.drive.move-file", { fileIdList: options.fileId, targetFolderId: options.targetFolderId });
|
|
865
|
+
flagFailedItems(data, "vault drive-move-file");
|
|
866
|
+
return data;
|
|
867
|
+
}));
|
|
868
|
+
vault.command("drive-move-folder").description("Move a drive folder under another folder of the SAME space (free)")
|
|
869
|
+
.requiredOption("--folder-id <id>", "Folder ID to move")
|
|
870
|
+
.requiredOption("--target-parent-id <id>", "Destination parent folder ID, or 'root'. Not the folder itself, one of its subfolders, or the other space (230005)")
|
|
871
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
872
|
+
.action((options) => emit(options, (client) => client.call("vault.drive.move-folder", { folderId: options.folderId, targetParentId: options.targetParentId })));
|
|
873
|
+
vault.command("drive-copy").description("Copy files to the OTHER space: my drive <-> tenant drive (free; same-space copies are rejected)")
|
|
874
|
+
.requiredOption("--file-id <id>", "Source file ID (repeat or comma-separate)", collectList)
|
|
875
|
+
.requiredOption("--target-folder-id <id>", "Destination folder ID in the other space, or 'root' for that space's root")
|
|
876
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
877
|
+
.action((options) => emit(options, async (client) => {
|
|
878
|
+
const data = await client.call("vault.drive.copy", { copyType: "file", fileIdList: options.fileId, targetFolderId: options.targetFolderId });
|
|
879
|
+
flagFailedItems(data, "vault drive-copy");
|
|
880
|
+
return data;
|
|
881
|
+
}));
|
|
882
|
+
vault.command("drive-delete-file").description("Delete drive files — irreversible (free)")
|
|
883
|
+
.requiredOption("--file-id <id>", "File ID to delete (repeat or comma-separate)", collectList)
|
|
884
|
+
.option("--yes", "Required: confirm the deletion")
|
|
885
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
886
|
+
.action(async (options) => {
|
|
887
|
+
assertConfirmed("vault.drive.delete-file", Boolean(options.yes), options.fileId.join("、"));
|
|
888
|
+
await emit(options, async (client) => {
|
|
889
|
+
const data = await client.call("vault.drive.delete-file", { fileIdList: options.fileId });
|
|
890
|
+
flagFailedItems(data, "vault drive-delete-file");
|
|
891
|
+
return data;
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
vault.command("drive-delete-folder").description("Delete a drive folder AND every subfolder and file inside it — irreversible (free)")
|
|
895
|
+
.requiredOption("--folder-id <id>", "Folder ID to delete")
|
|
896
|
+
.option("--yes", "Required: confirm the deletion")
|
|
897
|
+
.option("--format <format>", "Output format", "json").option("--output <path>")
|
|
898
|
+
.action(async (options) => {
|
|
899
|
+
assertConfirmed("vault.drive.delete-folder", Boolean(options.yes), options.folderId);
|
|
900
|
+
await emit(options, (client) => client.call("vault.drive.delete-folder", { folderId: options.folderId }));
|
|
901
|
+
});
|
|
699
902
|
vault.command("record-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--category <name>", "Recording type: upload/link/mobile/gtNote/pc/share", collectList, []).option("--space-type <number>", "Space type: 1=my records / 2=tenant records", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.record.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, categoryList: maybeArray(options.category), spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), { endpointKey: "vault.record.list", idField: "recordId" }));
|
|
700
903
|
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" });
|
|
701
904
|
vault.command("my-conference-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>", "Start time", datetimeArg("--start-time")).option("--end-time <datetime>", "End time", datetimeArg("--end-time")).option("--keyword <text>").option("--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", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Conference category: earningsCall/strategyMeeting/fundRoadshow/shareholdersMeeting/maMeeting/specialMeeting/companyAnalysis/industryAnalysis/other", collectList, []).option("--source <number>", "Recording source: 1=企微会议助理 2=会议服务微信群 (repeat)", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.my-conference.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution), categoryList: maybeArray(options.category), sourceList: options.source.length ? options.source : undefined }), { endpointKey: "vault.my-conference.list", idField: "conferenceId" }));
|
|
@@ -753,8 +956,8 @@ alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (rep
|
|
|
753
956
|
}
|
|
754
957
|
await printData(data, format, options.output);
|
|
755
958
|
}));
|
|
756
|
-
alternative.command("concept-info").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise reference concept-search'").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.concept-info", { conceptId: options.conceptId })));
|
|
757
|
-
alternative.command("concept-securities").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise reference concept-search'").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.concept-securities", { conceptId: options.conceptId })));
|
|
959
|
+
alternative.command("concept-info").description("Concept profile: definition, investment logic, industry space, competitive landscape (50 credits/call)").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise reference concept-search'").option("--full", "Also return catalyst events (keyEvents) via the v1 endpoint — 500 credits/call instead of 50").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call(options.full ? "alternative.concept-info-full" : "alternative.concept-info", { conceptId: options.conceptId })));
|
|
960
|
+
alternative.command("concept-securities").description("Concept constituents, grouped (50 credits/call; free when the concept has none)").requiredOption("--concept-id <id>", "Concept (theme index) ID, e.g. 121000130 机器人; discover via 'gangtise reference concept-search'").option("--full", "Also return the key-stock flag (isKey) and inclusion reason via the v1 endpoint — 500 credits/call instead of 50").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call(options.full ? "alternative.concept-securities-full" : "alternative.concept-securities", { conceptId: options.conceptId })));
|
|
758
961
|
program.addCommand(alternative);
|
|
759
962
|
function flagDropped(rows, data, requestedSecurities, requestedIndicators) {
|
|
760
963
|
if (isEmptyMatrix(data)) {
|
|
@@ -871,6 +1074,28 @@ tool.command("file-parse-check").description("Download a finished file-parse res
|
|
|
871
1074
|
process.stdout.write(`${JSON.stringify({ taskId: options.taskId, status: "pending", hint: "Parse not finished yet, retry in ~1 minute" })}\n`);
|
|
872
1075
|
}
|
|
873
1076
|
}));
|
|
1077
|
+
tool.command("web-search").description("Search the public web for research: deduped, source-tiered (T0-T3) results with optional page content (1 credit/call)")
|
|
1078
|
+
.requiredOption("--query <text>", "Search query, 1-200 chars. Sent verbatim — no intent rewriting server-side, so spell out what you want")
|
|
1079
|
+
.option("--size <number>", "Results to return, 1-20 (1-5 with --include-content)", "10")
|
|
1080
|
+
.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"]))
|
|
1081
|
+
.addOption(new Option("--min-tier <tier>", "Lowest source tier to keep; T3 = no filtering").choices(["T0", "T1", "T2", "T3"]))
|
|
1082
|
+
.option("--site <domain>", "Restrict to a registered domain or subdomain, e.g. csrc.gov.cn (repeatable, max 10, OR-ed)", collectList, [])
|
|
1083
|
+
.option("--include-content", "Return page body as Markdown; caps --size at 5")
|
|
1084
|
+
.option("--max-content-chars <number>", "Max chars per body, 1000-20000 (only with --include-content)")
|
|
1085
|
+
.option("--format <format>", "Output format", "table").option("--output <path>")
|
|
1086
|
+
.action((options) => emit(options, (client) => {
|
|
1087
|
+
const size = parseNumberOption(options.size, "--size", { integer: true, min: 1, max: 20 });
|
|
1088
|
+
if (options.includeContent && size > 5)
|
|
1089
|
+
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.`);
|
|
1090
|
+
const sites = [...new Set(options.site)];
|
|
1091
|
+
if (sites.length > 10)
|
|
1092
|
+
throw new ValidationError(`--site takes at most 10 distinct domains, got ${sites.length}`);
|
|
1093
|
+
return client.call("tool.web-search", {
|
|
1094
|
+
query: options.query, size, freshness: options.freshness, minTier: options.minTier,
|
|
1095
|
+
siteList: maybeArray(sites), includeContent: options.includeContent || undefined,
|
|
1096
|
+
maxContentChars: parseOptionalNumberOption(options.maxContentChars, "--max-content-chars", { integer: true, min: 1000, max: 20000 }),
|
|
1097
|
+
});
|
|
1098
|
+
}));
|
|
874
1099
|
program.addCommand(tool);
|
|
875
1100
|
program.command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--yes", "Confirm an irreversible endpoint (required for the ones marked destructive)").option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
|
|
876
1101
|
const endpoint = ENDPOINTS[endpointKey];
|
package/dist/src/core/client.js
CHANGED
|
@@ -157,7 +157,12 @@ export class GangtiseClient {
|
|
|
157
157
|
const startFrom = typeof initialBody.from === 'number' && Number.isFinite(initialBody.from) ? initialBody.from : 0;
|
|
158
158
|
const requestedSize = typeof initialBody.size === 'number' && Number.isFinite(initialBody.size) ? initialBody.size : undefined;
|
|
159
159
|
const maxPageSize = endpoint.pagination?.maxPageSize ?? requestedSize ?? 20;
|
|
160
|
-
const
|
|
160
|
+
const maxWindow = endpoint.pagination?.maxWindow;
|
|
161
|
+
if (maxWindow !== undefined && startFrom >= maxWindow) {
|
|
162
|
+
throw new ValidationError(`${endpoint.key} serves rows only up to offset ${maxWindow} (from + size ≤ ${maxWindow}); --from ${startFrom} is past it. Narrow the query (e.g. a shorter time range) instead of paging deeper.`);
|
|
163
|
+
}
|
|
164
|
+
const windowRoom = maxWindow === undefined ? Infinity : maxWindow - startFrom;
|
|
165
|
+
const firstPageSize = Math.min(requestedSize === undefined ? maxPageSize : Math.min(maxPageSize, requestedSize), windowRoom);
|
|
161
166
|
const firstPage = await this.requestJson(endpoint, {
|
|
162
167
|
...initialBody,
|
|
163
168
|
from: startFrom,
|
|
@@ -170,7 +175,15 @@ export class GangtiseClient {
|
|
|
170
175
|
}
|
|
171
176
|
const total = firstPage.total;
|
|
172
177
|
const available = Math.max(total - startFrom, 0);
|
|
173
|
-
const
|
|
178
|
+
const wanted = requestedSize === undefined ? available : Math.min(requestedSize, available);
|
|
179
|
+
const target = Math.min(wanted, windowRoom);
|
|
180
|
+
const probeFits = maxWindow === undefined || total + 1 <= maxWindow;
|
|
181
|
+
const flagWindowCut = (out) => {
|
|
182
|
+
if (target >= wanted)
|
|
183
|
+
return;
|
|
184
|
+
process.stderr.write(`[gangtise] warning: ${endpoint.key} serves rows only up to offset ${maxWindow} (from + size ≤ ${maxWindow}); ${wanted - target} of the ${wanted} requested rows lie past it and were not fetched. Narrow the query (e.g. a shorter time range) and fetch in slices.\n`);
|
|
185
|
+
out.partial = true;
|
|
186
|
+
};
|
|
174
187
|
const sink = this.claimRowSink();
|
|
175
188
|
if (sink && Array.isArray(firstPage.fieldList))
|
|
176
189
|
sink.setFieldList(firstPage.fieldList);
|
|
@@ -187,6 +200,11 @@ export class GangtiseClient {
|
|
|
187
200
|
for (const row of kept)
|
|
188
201
|
collected.push(row);
|
|
189
202
|
};
|
|
203
|
+
const checkTotalCap = async (out) => {
|
|
204
|
+
if (probeFits)
|
|
205
|
+
return this.flagIfTotalCapped(endpoint, initialBody, total, out, count);
|
|
206
|
+
process.stderr.write(`[gangtise] note: ${endpoint.key} reported total=${total}, which reaches its ${maxWindow}-row offset window, so whether that is the real count cannot be checked. Narrow the query (e.g. a shorter time range) to keep total below ${maxWindow} if you need to be sure.\n`);
|
|
207
|
+
};
|
|
190
208
|
const result = () => {
|
|
191
209
|
const out = { ...firstPage, total, list: collected };
|
|
192
210
|
return sink ? attachRowSink(out, sink) : out;
|
|
@@ -194,6 +212,7 @@ export class GangtiseClient {
|
|
|
194
212
|
await keep(firstPage.list);
|
|
195
213
|
if (firstPage.list.length < firstPageSize) {
|
|
196
214
|
const out = result();
|
|
215
|
+
flagWindowCut(out);
|
|
197
216
|
if (count < target) {
|
|
198
217
|
process.stderr.write(`[gangtise] warning: server returned a short page (${count} rows) but reported total=${total}; treating it as the end of data — results may be incomplete\n`);
|
|
199
218
|
out.partial = true;
|
|
@@ -202,8 +221,9 @@ export class GangtiseClient {
|
|
|
202
221
|
}
|
|
203
222
|
if (count >= target) {
|
|
204
223
|
const out = result();
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
flagWindowCut(out);
|
|
225
|
+
if (requestedSize === undefined && total > firstPageSize && target === wanted)
|
|
226
|
+
await checkTotalCap(out);
|
|
207
227
|
return out;
|
|
208
228
|
}
|
|
209
229
|
const MAX_PAGES = 1000;
|
|
@@ -268,8 +288,9 @@ export class GangtiseClient {
|
|
|
268
288
|
}
|
|
269
289
|
const short = count < target;
|
|
270
290
|
const out = result();
|
|
271
|
-
|
|
272
|
-
|
|
291
|
+
flagWindowCut(out);
|
|
292
|
+
if (requestedSize === undefined && total > 0 && target === wanted && !short && !totalDrift && !truncatedByPageCap && failedPages.length === 0) {
|
|
293
|
+
await checkTotalCap(out);
|
|
273
294
|
}
|
|
274
295
|
if (short || totalDrift || failedPages.length > 0)
|
|
275
296
|
out.partial = true;
|
|
@@ -397,13 +418,17 @@ export class GangtiseClient {
|
|
|
397
418
|
throw error;
|
|
398
419
|
});
|
|
399
420
|
}
|
|
400
|
-
async uploadFile(endpointKey, file) {
|
|
421
|
+
async uploadFile(endpointKey, file, fields) {
|
|
401
422
|
const endpoint = ENDPOINTS[endpointKey];
|
|
402
423
|
if (!endpoint || endpoint.kind !== 'upload') {
|
|
403
424
|
throw new ApiError(`Not an upload endpoint: ${endpointKey}`);
|
|
404
425
|
}
|
|
405
426
|
const form = new FormData();
|
|
406
427
|
form.append('file', new Blob([file.data], { type: file.contentType ?? 'application/octet-stream' }), file.filename);
|
|
428
|
+
for (const [name, value] of Object.entries(fields ?? {})) {
|
|
429
|
+
if (value !== undefined)
|
|
430
|
+
form.append(name, String(value));
|
|
431
|
+
}
|
|
407
432
|
return this.requestJson(endpoint, form);
|
|
408
433
|
}
|
|
409
434
|
async download(endpoint, query, options, body) {
|
|
@@ -546,7 +571,7 @@ export class GangtiseClient {
|
|
|
546
571
|
throw new ApiError(`Unknown endpoint key: ${endpointKey}`);
|
|
547
572
|
}
|
|
548
573
|
if (endpoint.kind === 'upload') {
|
|
549
|
-
throw new ValidationError(`${endpointKey} takes a file upload — use 'gangtise tool file-parse --file <path>'
|
|
574
|
+
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`);
|
|
550
575
|
}
|
|
551
576
|
if (endpoint.kind === 'download') {
|
|
552
577
|
return this.download(endpoint, query ?? {}, options, body);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ValidationError } from "./errors.js";
|
|
4
|
+
export const DRIVE_UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
5
|
+
export const DRIVE_NAME_MAX = 200;
|
|
6
|
+
export async function uploadDriveFile(client, filePath, options) {
|
|
7
|
+
const resolved = path.resolve(filePath);
|
|
8
|
+
let stat;
|
|
9
|
+
try {
|
|
10
|
+
stat = await fs.stat(resolved);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new ValidationError(`File not found: ${filePath}`);
|
|
14
|
+
}
|
|
15
|
+
if (!stat.isFile())
|
|
16
|
+
throw new ValidationError(`Not a file: ${filePath}`);
|
|
17
|
+
if (stat.size === 0)
|
|
18
|
+
throw new ValidationError(`File is empty: ${filePath}`);
|
|
19
|
+
if (stat.size > DRIVE_UPLOAD_MAX_BYTES) {
|
|
20
|
+
throw new ValidationError(`File is ${(stat.size / 1024 / 1024).toFixed(1)}MB — the drive accepts at most 100MB per file`);
|
|
21
|
+
}
|
|
22
|
+
const filename = path.basename(resolved);
|
|
23
|
+
const name = options.title ?? filename;
|
|
24
|
+
if (name.length > DRIVE_NAME_MAX) {
|
|
25
|
+
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`);
|
|
26
|
+
}
|
|
27
|
+
const data = await fs.readFile(resolved);
|
|
28
|
+
return client.uploadFile("vault.drive.upload", { filename, data }, { spaceType: options.spaceType, folderId: options.folderId, title: options.title });
|
|
29
|
+
}
|