gangtise-openapi-cli 0.42.0 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +43 -36
  2. package/dist/src/cli.js +29 -11
  3. package/dist/src/commands/ai.js +7 -6
  4. package/dist/src/commands/fundamental.js +3 -3
  5. package/dist/src/commands/indicator.js +3 -3
  6. package/dist/src/commands/insight.js +5 -4
  7. package/dist/src/commands/quote.js +46 -12
  8. package/dist/src/commands/raw.js +2 -2
  9. package/dist/src/commands/shared.js +60 -8
  10. package/dist/src/commands/vault.js +2 -2
  11. package/dist/src/core/args.js +6 -5
  12. package/dist/src/core/auth.js +3 -2
  13. package/dist/src/core/client.js +114 -11
  14. package/dist/src/core/commandBodies.js +2 -1
  15. package/dist/src/core/download.js +9 -5
  16. package/dist/src/core/endpoints.js +103 -1
  17. package/dist/src/core/opinionDetail.js +16 -6
  18. package/dist/src/core/output.js +37 -8
  19. package/dist/src/core/perSecurity.js +29 -11
  20. package/dist/src/core/quoteSharding.js +19 -15
  21. package/dist/src/core/rowSink.js +21 -0
  22. package/dist/src/core/titleCache.js +28 -2
  23. package/dist/src/core/transport.js +3 -1
  24. package/dist/src/core/updateCheck.js +51 -0
  25. package/dist/src/version.js +19 -1
  26. package/gangtise-openapi/SKILL.md +50 -49
  27. package/gangtise-openapi/references/commands/ai.md +34 -15
  28. package/gangtise-openapi/references/commands/alternative.md +2 -2
  29. package/gangtise-openapi/references/commands/bond.md +1 -1
  30. package/gangtise-openapi/references/commands/fundamental.md +4 -4
  31. package/gangtise-openapi/references/commands/indicator.md +31 -29
  32. package/gangtise-openapi/references/commands/insight.md +40 -32
  33. package/gangtise-openapi/references/commands/quote.md +19 -19
  34. package/gangtise-openapi/references/commands/reference-and-lookup.md +18 -15
  35. package/gangtise-openapi/references/commands/vault.md +10 -7
  36. package/gangtise-openapi/references/errors.md +22 -23
  37. package/gangtise-openapi/references/examples.md +36 -34
  38. package/gangtise-openapi/references/fields.md +8 -6
  39. package/gangtise-openapi/references/lookup-ids.md +10 -8
  40. package/gangtise-openapi/references/response-schema.md +7 -7
  41. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { dirname, extname } from "node:path";
3
3
  import { DownloadError } from "./errors.js";
4
- import { saveOutputIfNeeded, stagingPath } from "./output.js";
4
+ import { saveOutputIfNeeded, stagingPath, trackClaim } from "./output.js";
5
5
  import { extractTitles, lookupTitleCache, readTitleCache, TITLE_LOOKUP_SIZE, writeTitleCache } from "./titleCache.js";
6
6
  function sanitizeFilename(name) {
7
7
  return name.replace(/[/\\:*?"<>|\u0000-\u001f]/g, "_");
@@ -15,6 +15,10 @@ function truncateFilename(name, maxBytes = 200) {
15
15
  stem.pop();
16
16
  return stem.join("") + ext;
17
17
  }
18
+ function trackedClaim(p) {
19
+ trackClaim(p);
20
+ return p;
21
+ }
18
22
  export async function uniquePath(p) {
19
23
  const claim = async (f) => {
20
24
  try {
@@ -30,13 +34,13 @@ export async function uniquePath(p) {
30
34
  };
31
35
  await fs.mkdir(dirname(p) || ".", { recursive: true });
32
36
  if (await claim(p))
33
- return p;
37
+ return trackedClaim(p);
34
38
  const ext = extname(p);
35
39
  const stem = p.slice(0, p.length - ext.length);
36
40
  for (let i = 1; i <= 99; i++) {
37
41
  const candidate = `${stem}-${i}${ext}`;
38
42
  if (await claim(candidate))
39
- return candidate;
43
+ return trackedClaim(candidate);
40
44
  }
41
45
  throw new DownloadError(`Refusing to overwrite: 100 files already share the name "${p}" — pass --output or clean up the directory`);
42
46
  }
@@ -199,7 +203,7 @@ export async function saveDownloadResult(result, fallbackName, output) {
199
203
  const autoName = (file.filename ? sanitizeFilename(file.filename) : undefined) ?? (safeFallback + extFromContentType(file.contentType));
200
204
  const outputPath = output ?? await uniquePath(truncateFilename(autoName));
201
205
  try {
202
- await saveOutputIfNeeded(file.data, outputPath);
206
+ await saveOutputIfNeeded(file.data, outputPath, false);
203
207
  }
204
208
  catch (error) {
205
209
  if (!output)
@@ -212,7 +216,7 @@ export async function saveDownloadResult(result, fallbackName, output) {
212
216
  if (typeof file.text === "string") {
213
217
  const outputPath = output ?? await uniquePath(truncateFilename(`${safeFallback}.txt`));
214
218
  try {
215
- await saveOutputIfNeeded(file.text, outputPath);
219
+ await saveOutputIfNeeded(file.text, outputPath, false);
216
220
  }
217
221
  catch (error) {
218
222
  if (!output)
@@ -25,13 +25,16 @@ const ENDPOINT_DEFS = {
25
25
  path: "/application/open-insight/chief-opinion/v2/getList",
26
26
  kind: "json",
27
27
  description: "List domestic institution chief opinions (brief only; body via detail)",
28
+ billing: { per: "row", price: 1 },
28
29
  pagination: { enabled: true, maxPageSize: 50 },
30
+ rowId: "chiefOpinionId",
29
31
  },
30
32
  "insight.opinion.list-with-content": {
31
33
  method: "POST",
32
34
  path: "/application/open-insight/chief-opinion/getList",
33
35
  kind: "json",
34
36
  description: "List domestic institution chief opinions with the full body (v1 list)",
37
+ billing: { per: "row", price: 30 },
35
38
  pagination: { enabled: true, maxPageSize: 50 },
36
39
  retry: "no-replay",
37
40
  },
@@ -40,6 +43,7 @@ const ENDPOINT_DEFS = {
40
43
  path: "/application/open-insight/chief-opinion/getDetail",
41
44
  kind: "json",
42
45
  description: "Get domestic chief opinion bodies by ID (max 20 IDs per call)",
46
+ billing: { per: "row", price: 30, maxUnits: 20 },
43
47
  retry: "no-replay",
44
48
  expects: "array",
45
49
  },
@@ -48,13 +52,16 @@ const ENDPOINT_DEFS = {
48
52
  path: "/application/open-insight/summary/v2/getList",
49
53
  kind: "json",
50
54
  description: "List summaries",
55
+ billing: { per: "row", price: 0.1 },
51
56
  pagination: { enabled: true, maxPageSize: 50 },
57
+ rowId: "summaryId",
52
58
  },
53
59
  "insight.summary.download": {
54
60
  method: "GET",
55
61
  path: "/application/open-insight/summary/v2/download/file",
56
62
  kind: "download",
57
63
  description: "Download summary file",
64
+ billing: { per: "document", price: 50 },
58
65
  retry: "no-replay",
59
66
  },
60
67
  "insight.pamirs-summary.list": {
@@ -63,6 +70,7 @@ const ENDPOINT_DEFS = {
63
70
  kind: "json",
64
71
  description: "List Pamirs expert summaries (requires the expert-summary database)",
65
72
  pagination: { enabled: true, maxPageSize: 50 },
73
+ rowId: "summaryId",
66
74
  },
67
75
  "insight.pamirs-summary.download": {
68
76
  method: "GET",
@@ -76,67 +84,88 @@ const ENDPOINT_DEFS = {
76
84
  path: "/application/open-insight/schedule/roadshow/getList",
77
85
  kind: "json",
78
86
  description: "List roadshows",
87
+ billing: { per: "row", price: 20 },
79
88
  pagination: { enabled: true, maxPageSize: 50 },
89
+ rowId: "id",
90
+ rowIdUnverified: true,
80
91
  },
81
92
  "insight.site-visit.list": {
82
93
  method: "POST",
83
94
  path: "/application/open-insight/schedule/site-visit/getList",
84
95
  kind: "json",
85
96
  description: "List site visits",
97
+ billing: { per: "row", price: 20 },
86
98
  pagination: { enabled: true, maxPageSize: 50 },
99
+ rowId: "id",
100
+ rowIdUnverified: true,
87
101
  },
88
102
  "insight.strategy.list": {
89
103
  method: "POST",
90
104
  path: "/application/open-insight/schedule/strategy-meeting/getList",
91
105
  kind: "json",
92
106
  description: "List strategy meetings",
107
+ billing: { per: "row", price: 20 },
93
108
  pagination: { enabled: true, maxPageSize: 50 },
109
+ rowId: "id",
110
+ rowIdUnverified: true,
94
111
  },
95
112
  "insight.forum.list": {
96
113
  method: "POST",
97
114
  path: "/application/open-insight/schedule/forum/getList",
98
115
  kind: "json",
99
116
  description: "List forums",
117
+ billing: { per: "row", price: 20 },
100
118
  pagination: { enabled: true, maxPageSize: 50 },
119
+ rowId: "id",
120
+ rowIdUnverified: true,
101
121
  },
102
122
  "insight.performance-calendar.list": {
103
123
  method: "POST",
104
124
  path: "/application/open-insight/schedule/performance-calendar/getList",
105
125
  kind: "json",
106
126
  description: "List earnings calendar events (forecast / express / announcement)",
127
+ billing: { per: "row", price: 0.1 },
107
128
  pagination: { enabled: true, maxPageSize: 50 },
129
+ rowId: "performanceReportId",
108
130
  },
109
131
  "insight.performance-calendar.download": {
110
132
  method: "GET",
111
133
  path: "/application/open-insight/schedule/performance-calendar/download/file",
112
134
  kind: "download",
113
135
  description: "Download an earnings report file (A-share 10 credits, HK/US 20)",
136
+ billing: { per: "document", price: 20 },
114
137
  },
115
138
  "insight.research.list": {
116
139
  method: "POST",
117
140
  path: "/application/open-insight/broker-report/getList",
118
141
  kind: "json",
119
142
  description: "List broker research reports",
143
+ billing: { per: "row", price: 0.1 },
120
144
  pagination: { enabled: true, maxPageSize: 50 },
145
+ rowId: "reportId",
121
146
  },
122
147
  "insight.research.download": {
123
148
  method: "GET",
124
149
  path: "/application/open-insight/broker-report/download/file",
125
150
  kind: "download",
126
151
  description: "Download broker research report",
152
+ billing: { per: "document", price: 10 },
127
153
  },
128
154
  "insight.foreign-report.list": {
129
155
  method: "POST",
130
156
  path: "/application/open-insight/foreign-report/getList",
131
157
  kind: "json",
132
158
  description: "List foreign reports",
159
+ billing: { per: "row", price: 0.1 },
133
160
  pagination: { enabled: true, maxPageSize: 50 },
161
+ rowId: "reportId",
134
162
  },
135
163
  "insight.foreign-report.download": {
136
164
  method: "GET",
137
165
  path: "/application/open-insight/foreign-report/download/file",
138
166
  kind: "download",
139
167
  description: "Download foreign report",
168
+ billing: { per: "document", price: 50 },
140
169
  retry: "no-replay",
141
170
  },
142
171
  "insight.announcement.list": {
@@ -144,52 +173,64 @@ const ENDPOINT_DEFS = {
144
173
  path: "/application/open-insight/announcement/getList",
145
174
  kind: "json",
146
175
  description: "List A-share announcements",
176
+ billing: { per: "row", price: 0.1 },
147
177
  pagination: { enabled: true, maxPageSize: 50 },
178
+ rowId: "announcementId",
148
179
  },
149
180
  "insight.announcement.download": {
150
181
  method: "GET",
151
182
  path: "/application/open-insight/announcement/download/file",
152
183
  kind: "download",
153
184
  description: "Download A-share announcement file",
185
+ billing: { per: "document", price: 10 },
154
186
  },
155
187
  "insight.announcement-hk.list": {
156
188
  method: "POST",
157
189
  path: "/application/open-insight/announcement-hk/getList",
158
190
  kind: "json",
159
191
  description: "List HK announcements",
192
+ billing: { per: "row", price: 0.1 },
160
193
  pagination: { enabled: true, maxPageSize: 50 },
194
+ rowId: "announcementId",
161
195
  },
162
196
  "insight.announcement-hk.download": {
163
197
  method: "GET",
164
198
  path: "/application/open-insight/announcement-hk/download/file",
165
199
  kind: "download",
166
200
  description: "Download HK announcement file",
201
+ billing: { per: "document", price: 20 },
167
202
  },
168
203
  "insight.announcement-us.list": {
169
204
  method: "POST",
170
205
  path: "/application/open-insight/announcement-us/getList",
171
206
  kind: "json",
172
207
  description: "List US announcements",
208
+ billing: { per: "row", price: 0.1 },
173
209
  pagination: { enabled: true, maxPageSize: 50 },
210
+ rowId: "announcementId",
174
211
  },
175
212
  "insight.announcement-us.download": {
176
213
  method: "GET",
177
214
  path: "/application/open-insight/announcement-us/download/file",
178
215
  kind: "download",
179
216
  description: "Download US announcement file",
217
+ billing: { per: "document", price: 20 },
180
218
  },
181
219
  "insight.foreign-opinion.list": {
182
220
  method: "POST",
183
221
  path: "/application/open-insight/foreign-opinion/v2/getList",
184
222
  kind: "json",
185
223
  description: "List foreign institution opinions (brief only; body via detail)",
224
+ billing: { per: "row", price: 1 },
186
225
  pagination: { enabled: true, maxPageSize: 50 },
226
+ rowId: "foreignOpinionId",
187
227
  },
188
228
  "insight.foreign-opinion.list-with-content": {
189
229
  method: "POST",
190
230
  path: "/application/open-insight/foreign-opinion/getList",
191
231
  kind: "json",
192
232
  description: "List foreign institution opinions with the full body (v1 list)",
233
+ billing: { per: "row", price: 30 },
193
234
  pagination: { enabled: true, maxPageSize: 50 },
194
235
  retry: "no-replay",
195
236
  },
@@ -198,6 +239,7 @@ const ENDPOINT_DEFS = {
198
239
  path: "/application/open-insight/foreign-opinion/getDetail",
199
240
  kind: "json",
200
241
  description: "Get foreign opinion bodies by ID (max 20 IDs per call)",
242
+ billing: { per: "row", price: 30, maxUnits: 20 },
201
243
  retry: "no-replay",
202
244
  expects: "array",
203
245
  },
@@ -206,32 +248,40 @@ const ENDPOINT_DEFS = {
206
248
  path: "/application/open-insight/independent-opinion/getList",
207
249
  kind: "json",
208
250
  description: "List foreign independent analyst opinions",
251
+ billing: { per: "row", price: 5 },
209
252
  pagination: { enabled: true, maxPageSize: 50 },
253
+ rowId: "independentOpinionId",
254
+ rowIdUnverified: true,
210
255
  },
211
256
  "insight.independent-opinion.download": {
212
257
  method: "GET",
213
258
  path: "/application/open-insight/independent-opinion/download/file",
214
259
  kind: "download",
215
260
  description: "Download foreign independent opinion file",
261
+ billing: { per: "document", price: 30 },
216
262
  },
217
263
  "insight.official-account.list": {
218
264
  method: "POST",
219
265
  path: "/application/open-insight/officialAccount/getList",
220
266
  kind: "json",
221
267
  description: "List WeChat official account articles",
268
+ billing: { per: "row", price: 0.1 },
222
269
  pagination: { enabled: true, maxPageSize: 50 },
270
+ rowId: "articleId",
223
271
  },
224
272
  "insight.official-account.download": {
225
273
  method: "GET",
226
274
  path: "/application/open-insight/officialAccount/download/file",
227
275
  kind: "download",
228
276
  description: "Download WeChat official account article (txt/HTML)",
277
+ billing: { per: "document", price: 10 },
229
278
  },
230
279
  "insight.qa.list": {
231
280
  method: "POST",
232
281
  path: "/application/open-insight/Q&A-data/getList",
233
282
  kind: "json",
234
283
  description: "List investor Q&A (conference/interactive/survey) for a security",
284
+ billing: { per: "row", price: 0.1 },
235
285
  pagination: { enabled: true, maxPageSize: 500 },
236
286
  },
237
287
  "insight.report-image.list": {
@@ -245,7 +295,9 @@ const ENDPOINT_DEFS = {
245
295
  path: "/application/open-insight/summary/highlight/getList",
246
296
  kind: "json",
247
297
  description: "List meeting highlights (核心要点信息流; content is an HTML fragment)",
298
+ billing: { per: "row", price: 5 },
248
299
  pagination: { enabled: true, maxPageSize: 50, maxWindow: 10000 },
300
+ rowId: "highlightId",
249
301
  retry: "no-replay",
250
302
  expects: "list",
251
303
  },
@@ -254,6 +306,7 @@ const ENDPOINT_DEFS = {
254
306
  path: "/application/open-insight/report-image/download/file",
255
307
  kind: "download",
256
308
  description: "Download a research report image by chunkId",
309
+ billing: { per: "document", price: 0.1 },
257
310
  },
258
311
  "reference.securities-search": {
259
312
  method: "POST",
@@ -448,12 +501,15 @@ const ENDPOINT_DEFS = {
448
501
  path: "/application/open-fundamental/earning-forecast",
449
502
  kind: "json",
450
503
  description: "Query earning forecast (consensus estimates)",
504
+ billing: { per: "row", price: 0.5, maxUnits: Number.POSITIVE_INFINITY },
505
+ retry: "no-replay",
451
506
  },
452
507
  "bond.basic-info": {
453
508
  method: "POST",
454
509
  path: "/application/open-fundamental/bond/basic-info",
455
510
  kind: "json",
456
511
  description: "Query bond static profiles (issuance, term, coupon, rating, options)",
512
+ billing: { per: "call", price: 0.4 },
457
513
  retry: "no-replay",
458
514
  expects: "list",
459
515
  },
@@ -462,6 +518,7 @@ const ENDPOINT_DEFS = {
462
518
  path: "/application/open-fundamental/bond/issuer-info",
463
519
  kind: "json",
464
520
  description: "Query bond issuer profiles (by bond code or issuer name)",
521
+ billing: { per: "call", price: 0.4 },
465
522
  retry: "no-replay",
466
523
  expects: "list",
467
524
  },
@@ -470,6 +527,7 @@ const ENDPOINT_DEFS = {
470
527
  path: "/application/open-quote/bond/daily-quote-exchange-cfets",
471
528
  kind: "json",
472
529
  description: "Query bond daily close quotes (exchange + CFETS; dirty/clean price, YTM, duration)",
530
+ billing: { per: "call", price: 0.4 },
473
531
  retry: "no-replay",
474
532
  expects: "list",
475
533
  },
@@ -478,6 +536,7 @@ const ENDPOINT_DEFS = {
478
536
  path: "/application/open-fundamental/bond/valuation-shclearing",
479
537
  kind: "json",
480
538
  description: "Query Shanghai Clearing House bond valuations (price, yield, risk measures)",
539
+ billing: { per: "call", price: 0.4 },
481
540
  retry: "no-replay",
482
541
  expects: "list",
483
542
  },
@@ -486,6 +545,7 @@ const ENDPOINT_DEFS = {
486
545
  path: "/application/open-fundamental/bond/cash-flow",
487
546
  kind: "json",
488
547
  description: "Query bond interest payment and redemption schedule",
548
+ billing: { per: "call", price: 0.4 },
489
549
  retry: "no-replay",
490
550
  expects: "list",
491
551
  },
@@ -494,6 +554,7 @@ const ENDPOINT_DEFS = {
494
554
  path: "/application/open-fundamental/bond/announcement",
495
555
  kind: "json",
496
556
  description: "Query bond announcements (pageNo/pageSize; the only paged endpoint of the family)",
557
+ billing: { per: "call", price: 0.4 },
497
558
  retry: "no-replay",
498
559
  expects: "list",
499
560
  },
@@ -502,6 +563,7 @@ const ENDPOINT_DEFS = {
502
563
  path: "/application/open-fundamental/bond/issuance-detail",
503
564
  kind: "json",
504
565
  description: "Query bond issuance and re-issuance records (bidding, pricing, cover ratios)",
566
+ billing: { per: "call", price: 0.4 },
505
567
  retry: "no-replay",
506
568
  expects: "list",
507
569
  },
@@ -510,6 +572,7 @@ const ENDPOINT_DEFS = {
510
572
  path: "/application/open-fundamental/bond/rating-overview",
511
573
  kind: "json",
512
574
  description: "Query bond / issuer / guarantor ratings side by side (max 10 bonds per call)",
575
+ billing: { per: "row", price: 0.4, maxUnits: 10 },
513
576
  retry: "no-replay",
514
577
  expects: "list",
515
578
  },
@@ -518,6 +581,7 @@ const ENDPOINT_DEFS = {
518
581
  path: "/application/open-fundamental/bond/rating-change",
519
582
  kind: "json",
520
583
  description: "Query bond rating change history (max 10 bonds per call)",
584
+ billing: { per: "security", price: 0.4, maxUnits: 10 },
521
585
  retry: "no-replay",
522
586
  expects: "list",
523
587
  },
@@ -526,6 +590,7 @@ const ENDPOINT_DEFS = {
526
590
  path: "/application/open-fundamental/bond/issuer-rating-change",
527
591
  kind: "json",
528
592
  description: "Query issuer rating change history (by bond code or issuer name)",
593
+ billing: { per: "issuer", price: 0.4, maxUnits: 10 },
529
594
  retry: "no-replay",
530
595
  expects: "list",
531
596
  },
@@ -534,6 +599,7 @@ const ENDPOINT_DEFS = {
534
599
  path: "/application/open-fundamental/bond/issuance-plan",
535
600
  kind: "json",
536
601
  description: "Query the rate-bond issuance calendar over a date range",
602
+ billing: { per: "call", price: 0.4 },
537
603
  retry: "no-replay",
538
604
  expects: "list",
539
605
  },
@@ -542,6 +608,7 @@ const ENDPOINT_DEFS = {
542
608
  path: "/application/open-fundamental/bond/exercise-notice",
543
609
  kind: "json",
544
610
  description: "Query put/call exercise schedules and results for option-embedded bonds",
611
+ billing: { per: "call", price: 0.4 },
545
612
  retry: "no-replay",
546
613
  expects: "list",
547
614
  },
@@ -550,12 +617,16 @@ const ENDPOINT_DEFS = {
550
617
  path: "/application/open-ai/stock-summary/getList",
551
618
  kind: "json",
552
619
  description: "Stock highlights (refined research summary per security)",
620
+ billing: { per: "row", price: 3, maxUnits: 6000 },
621
+ retry: "no-replay",
622
+ timeoutMs: 120_000,
553
623
  },
554
624
  "ai.knowledge-batch": {
555
625
  method: "POST",
556
626
  path: "/application/open-data/ai/search/knowledge/batch",
557
627
  kind: "json",
558
628
  description: "Batch knowledge search",
629
+ billing: { per: "call", price: 10 },
559
630
  retry: "no-replay",
560
631
  },
561
632
  "ai.knowledge-resource.download": {
@@ -569,6 +640,7 @@ const ENDPOINT_DEFS = {
569
640
  path: "/application/open-ai/security-clue/getList",
570
641
  kind: "json",
571
642
  description: "List security clues",
643
+ billing: { per: "row", price: 5 },
572
644
  pagination: { enabled: true, maxPageSize: 500 },
573
645
  },
574
646
  "ai.one-pager": {
@@ -576,6 +648,7 @@ const ENDPOINT_DEFS = {
576
648
  path: "/application/open-ai/agent/one-pager",
577
649
  kind: "json",
578
650
  description: "Generate one pager",
651
+ billing: { per: "call", price: 50 },
579
652
  timeoutMs: 120_000,
580
653
  retry: "no-replay",
581
654
  },
@@ -584,6 +657,7 @@ const ENDPOINT_DEFS = {
584
657
  path: "/application/open-ai/agent/investment-logic",
585
658
  kind: "json",
586
659
  description: "Generate investment logic",
660
+ billing: { per: "call", price: 50 },
587
661
  timeoutMs: 120_000,
588
662
  retry: "no-replay",
589
663
  },
@@ -592,6 +666,7 @@ const ENDPOINT_DEFS = {
592
666
  path: "/application/open-ai/agent/peer-comparison",
593
667
  kind: "json",
594
668
  description: "Generate peer comparison",
669
+ billing: { per: "call", price: 50 },
595
670
  timeoutMs: 120_000,
596
671
  retry: "no-replay",
597
672
  },
@@ -600,6 +675,7 @@ const ENDPOINT_DEFS = {
600
675
  path: "/application/open-ai/agent/earnings-review-getid",
601
676
  kind: "json",
602
677
  description: "Get earnings review ID",
678
+ billing: { per: "call", price: 50 },
603
679
  retry: "no-replay",
604
680
  },
605
681
  "ai.earnings-review.get-content": {
@@ -613,6 +689,7 @@ const ENDPOINT_DEFS = {
613
689
  path: "/application/open-ai/agent/theme-tracking",
614
690
  kind: "json",
615
691
  description: "Get theme tracking daily report",
692
+ billing: { per: "call", price: 50 },
616
693
  timeoutMs: 120_000,
617
694
  retry: "no-replay",
618
695
  },
@@ -621,6 +698,7 @@ const ENDPOINT_DEFS = {
621
698
  path: "/application/open-ai/agent/research-outline",
622
699
  kind: "json",
623
700
  description: "Get company research outline",
701
+ billing: { per: "call", price: 50 },
624
702
  timeoutMs: 120_000,
625
703
  retry: "no-replay",
626
704
  },
@@ -629,6 +707,7 @@ const ENDPOINT_DEFS = {
629
707
  path: "/application/open-ai/hot-topic/getList",
630
708
  kind: "json",
631
709
  description: "List hot topic reports",
710
+ billing: { per: "row", price: 50 },
632
711
  pagination: { enabled: true, maxPageSize: 20 },
633
712
  retry: "no-replay",
634
713
  },
@@ -637,6 +716,7 @@ const ENDPOINT_DEFS = {
637
716
  path: "/application/open-ai/management-discuss/from-announcement",
638
717
  kind: "json",
639
718
  description: "Management discussion from financial reports (half-year/annual)",
719
+ billing: { per: "call", price: 10 },
640
720
  timeoutMs: 120_000,
641
721
  retry: "no-replay",
642
722
  },
@@ -645,6 +725,7 @@ const ENDPOINT_DEFS = {
645
725
  path: "/application/open-ai/management-discuss/from-earningsCall",
646
726
  kind: "json",
647
727
  description: "Management discussion from earnings calls",
728
+ billing: { per: "call", price: 10 },
648
729
  timeoutMs: 120_000,
649
730
  retry: "no-replay",
650
731
  },
@@ -653,6 +734,7 @@ const ENDPOINT_DEFS = {
653
734
  path: "/application/open-ai/agent/viewpoint-debate-getid",
654
735
  kind: "json",
655
736
  description: "Get viewpoint debate ID",
737
+ billing: { per: "call", price: 50 },
656
738
  retry: "no-replay",
657
739
  },
658
740
  "ai.viewpoint-debate.get-content": {
@@ -667,6 +749,7 @@ const ENDPOINT_DEFS = {
667
749
  kind: "json",
668
750
  description: "List vault drive files",
669
751
  pagination: { enabled: true, maxPageSize: 50 },
752
+ rowId: "fileId",
670
753
  },
671
754
  "vault.drive.download": {
672
755
  method: "GET",
@@ -745,6 +828,7 @@ const ENDPOINT_DEFS = {
745
828
  kind: "json",
746
829
  description: "List voice recording transcriptions",
747
830
  pagination: { enabled: true, maxPageSize: 50 },
831
+ rowId: "recordId",
748
832
  },
749
833
  "vault.record.download": {
750
834
  method: "GET",
@@ -757,13 +841,16 @@ const ENDPOINT_DEFS = {
757
841
  path: "/application/open-vault/my-conference/getList",
758
842
  kind: "json",
759
843
  description: "List my conferences",
844
+ billing: { per: "row", price: 0.1 },
760
845
  pagination: { enabled: true, maxPageSize: 50 },
846
+ rowId: "conferenceId",
761
847
  },
762
848
  "vault.my-conference.download": {
763
849
  method: "GET",
764
850
  path: "/application/open-vault/my-conference/download/file",
765
851
  kind: "download",
766
852
  description: "Download my conference resource",
853
+ billing: { per: "document", price: 50 },
767
854
  retry: "no-replay",
768
855
  },
769
856
  "vault.wechat-message.list": {
@@ -771,7 +858,8 @@ const ENDPOINT_DEFS = {
771
858
  path: "/application/open-vault/wechatgroupmsg/list",
772
859
  kind: "json",
773
860
  description: "List WeChat group messages",
774
- pagination: { enabled: true, maxPageSize: 50 },
861
+ pagination: { enabled: true, maxPageSize: 50, maxWindow: 10000 },
862
+ rowId: "msgId",
775
863
  },
776
864
  "vault.wechat-chatroom.list": {
777
865
  method: "POST",
@@ -779,6 +867,7 @@ const ENDPOINT_DEFS = {
779
867
  kind: "json",
780
868
  description: "List WeChat group chatroom IDs",
781
869
  pagination: { enabled: true, maxPageSize: 50 },
870
+ rowId: "chatroomId",
782
871
  },
783
872
  "vault.stock-pool.list": {
784
873
  method: "POST",
@@ -844,6 +933,7 @@ const ENDPOINT_DEFS = {
844
933
  path: "/application/open-alternative/concept/v2/info",
845
934
  kind: "json",
846
935
  description: "Query latest concept (theme index) profile by conceptId",
936
+ billing: { per: "call", price: 50 },
847
937
  retry: "no-replay",
848
938
  },
849
939
  "alternative.concept-info-full": {
@@ -851,6 +941,7 @@ const ENDPOINT_DEFS = {
851
941
  path: "/application/open-alternative/concept/info",
852
942
  kind: "json",
853
943
  description: "Query latest concept profile incl. catalyst events (keyEvents; v1)",
944
+ billing: { per: "call", price: 500 },
854
945
  retry: "no-replay",
855
946
  },
856
947
  "alternative.concept-securities": {
@@ -858,6 +949,7 @@ const ENDPOINT_DEFS = {
858
949
  path: "/application/open-alternative/concept/v2/securities",
859
950
  kind: "json",
860
951
  description: "Query concept (theme index) constituent securities, grouped",
952
+ billing: { per: "call", price: 50 },
861
953
  retry: "no-replay",
862
954
  },
863
955
  "alternative.concept-securities-full": {
@@ -865,6 +957,7 @@ const ENDPOINT_DEFS = {
865
957
  path: "/application/open-alternative/concept/securities",
866
958
  kind: "json",
867
959
  description: "Query concept constituents incl. isKey / inclusionReason (v1)",
960
+ billing: { per: "call", price: 500 },
868
961
  retry: "no-replay",
869
962
  },
870
963
  "indicator.search": {
@@ -900,6 +993,7 @@ const ENDPOINT_DEFS = {
900
993
  path: "/application/open-tool/file-parse/submit",
901
994
  kind: "upload",
902
995
  description: "Submit a PDF for parsing (multipart upload), returns taskId",
996
+ billing: { per: "page", price: 0.8 },
903
997
  timeoutMs: 300_000,
904
998
  retry: "no-replay",
905
999
  bigIntFields: ["taskId"],
@@ -915,10 +1009,18 @@ const ENDPOINT_DEFS = {
915
1009
  path: "/application/open-tool/web-search/search",
916
1010
  kind: "json",
917
1011
  description: "Search the public web for research (tiered sources, optional page content)",
1012
+ billing: { per: "call", price: 1 },
918
1013
  retry: "no-replay",
919
1014
  expects: "list",
920
1015
  },
921
1016
  };
1017
+ export const NO_REPLAY_ABOVE_CREDITS = 3000;
1018
+ export function worstRequestCredits(endpoint) {
1019
+ if (!endpoint.billing)
1020
+ return 0;
1021
+ const units = endpoint.pagination?.maxPageSize ?? endpoint.billing.maxUnits ?? 1;
1022
+ return units * endpoint.billing.price;
1023
+ }
922
1024
  export const ENDPOINTS = Object.fromEntries(Object.entries(ENDPOINT_DEFS).map(([key, def]) => [key, { key, ...def }]));
923
1025
  export function listEndpoints() {
924
1026
  return Object.values(ENDPOINTS).map(({ key, method, path, description }) => ({ key, method, path, description }));
@@ -1,4 +1,4 @@
1
- import { ApiError, ValidationError } from "./errors.js";
1
+ import { ApiError, markStructural, ValidationError } from "./errors.js";
2
2
  export async function fetchOpinionDetails(client, endpointKey, idListField, idField, ids) {
3
3
  const BATCH = 20;
4
4
  const unique = [...new Set(ids)];
@@ -7,18 +7,28 @@ export async function fetchOpinionDetails(client, endpointKey, idListField, idFi
7
7
  const list = [];
8
8
  let unfetched = [];
9
9
  let unfetchedError;
10
+ let nulls = 0;
10
11
  for (let i = 0; i < unique.length; i += BATCH) {
11
12
  try {
12
13
  const rows = await client.call(endpointKey, { [idListField]: unique.slice(i, i + BATCH) });
13
- list.push(...rows);
14
+ const bodies = rows.filter((row) => row !== null && typeof row === "object" && !Array.isArray(row));
15
+ const odd = rows.filter((row) => row !== null && (typeof row !== "object" || Array.isArray(row))).length;
16
+ list.push(...bodies);
17
+ if (odd > 0) {
18
+ throw markStructural(new ApiError(`${endpointKey} returned ${odd} non-object element(s) in its body list — the response layout may have changed`, undefined, undefined, rows));
19
+ }
20
+ nulls += rows.length - bodies.length;
14
21
  }
15
22
  catch (error) {
16
- if (i === 0)
23
+ if (i === 0 && list.length === 0)
17
24
  throw error;
18
- unfetched = unique.slice(i);
25
+ const kept = new Set(list.map((row) => String(row[idField])));
26
+ unfetched = unique.slice(i).filter((id) => !kept.has(id));
19
27
  const message = error instanceof Error ? error.message : String(error);
20
28
  unfetchedError = { message, ...(error instanceof ApiError ? { code: error.code, traceId: error.traceId } : {}) };
21
- 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`);
29
+ process.stderr.write(unfetched.length > 0
30
+ ? `[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`
31
+ : `[gangtise] warning: ${message} (batch ${i / BATCH + 1}); every ID in it came back with a body.\n`);
22
32
  break;
23
33
  }
24
34
  }
@@ -26,7 +36,7 @@ export async function fetchOpinionDetails(client, endpointKey, idListField, idFi
26
36
  const missing = unique.filter((id) => !returned.has(id) && !unfetched.includes(id));
27
37
  const out = { total: list.length, list };
28
38
  if (missing.length > 0) {
29
- 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`);
39
+ process.stderr.write(`[gangtise] warning: no body returned for ${missing.length} ID(s): ${missing.join(", ")}${nulls > 0 ? ` (${nulls} came back as a null element)` : ""} — the server gives no error for an ID it has no body for (e.g. a wrong ID; a just-published opinion may also not have its body yet). Result marked partial (exit 3).\n`);
30
40
  out.missingIds = missing;
31
41
  }
32
42
  if (unfetched.length > 0) {