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
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { isAsyncPending,
|
|
3
|
+
import { isAsyncPending, pollUntilDone } from "./asyncContent.js";
|
|
4
4
|
import { saveDownloadResult } from "./download.js";
|
|
5
5
|
import { ApiError, ValidationError } from "./errors.js";
|
|
6
|
-
import { isTransientError } from "./transport.js";
|
|
7
6
|
export const FILE_PARSE_MAX_BYTES = 100 * 1024 * 1024;
|
|
8
7
|
export async function submitFileParse(client, filePath) {
|
|
9
8
|
const resolved = path.resolve(filePath);
|
|
@@ -50,22 +49,6 @@ export async function fetchFileParseResult(client, taskId, output) {
|
|
|
50
49
|
return "ok";
|
|
51
50
|
}
|
|
52
51
|
export async function pollFileParseResult(client, taskId, output) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (await fetchFileParseResult(client, taskId, output) === "ok")
|
|
56
|
-
return "ok";
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
if (!isTransientError(error))
|
|
60
|
-
throw error;
|
|
61
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
62
|
-
process.stderr.write(`Attempt ${attempt}/${POLL_MAX_ATTEMPTS}: transient error (${msg.slice(0, 80)}), continuing to wait...\n`);
|
|
63
|
-
}
|
|
64
|
-
if (attempt < POLL_MAX_ATTEMPTS) {
|
|
65
|
-
const delay = nextPollDelayMs(attempt);
|
|
66
|
-
process.stderr.write(`Attempt ${attempt}/${POLL_MAX_ATTEMPTS}: parse result not ready, retrying in ${Math.round(delay / 1000)}s...\n`);
|
|
67
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return "timeout";
|
|
52
|
+
const outcome = await pollUntilDone(async () => await fetchFileParseResult(client, taskId, output), "parse result not ready");
|
|
53
|
+
return outcome === "ok" ? "ok" : "timeout";
|
|
71
54
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ApiError, ValidationError } from "./errors.js";
|
|
2
|
+
export async function fetchOpinionDetails(client, endpointKey, idListField, idField, ids) {
|
|
3
|
+
const BATCH = 20;
|
|
4
|
+
const unique = [...new Set(ids)];
|
|
5
|
+
if (!unique.length)
|
|
6
|
+
throw new ValidationError("pass at least one opinion ID");
|
|
7
|
+
const list = [];
|
|
8
|
+
let unfetched = [];
|
|
9
|
+
let unfetchedError;
|
|
10
|
+
for (let i = 0; i < unique.length; i += BATCH) {
|
|
11
|
+
try {
|
|
12
|
+
const rows = await client.call(endpointKey, { [idListField]: unique.slice(i, i + BATCH) });
|
|
13
|
+
list.push(...rows);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (i === 0)
|
|
17
|
+
throw error;
|
|
18
|
+
unfetched = unique.slice(i);
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
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`);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const returned = new Set(list.map((row) => String(row[idField])));
|
|
26
|
+
const missing = unique.filter((id) => !returned.has(id) && !unfetched.includes(id));
|
|
27
|
+
const out = { total: list.length, list };
|
|
28
|
+
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`);
|
|
30
|
+
out.missingIds = missing;
|
|
31
|
+
}
|
|
32
|
+
if (unfetched.length > 0) {
|
|
33
|
+
out.unfetchedIds = unfetched;
|
|
34
|
+
out.unfetchedError = unfetchedError;
|
|
35
|
+
}
|
|
36
|
+
if (missing.length > 0 || unfetched.length > 0)
|
|
37
|
+
out.partial = true;
|
|
38
|
+
return out;
|
|
39
|
+
}
|
package/dist/src/core/output.js
CHANGED
|
@@ -170,16 +170,26 @@ export async function streamOutputToFile(value, format, outputPath) {
|
|
|
170
170
|
stream.on("error", () => { });
|
|
171
171
|
try {
|
|
172
172
|
if (format === "jsonl") {
|
|
173
|
+
let chunk = [];
|
|
173
174
|
for (const item of jsonlItems(value)) {
|
|
174
|
-
|
|
175
|
+
chunk.push(JSON.stringify(item));
|
|
176
|
+
if (chunk.length >= LINES_PER_WRITE) {
|
|
177
|
+
await writeLines(stream, chunk);
|
|
178
|
+
chunk = [];
|
|
179
|
+
}
|
|
175
180
|
}
|
|
181
|
+
await writeLines(stream, chunk);
|
|
176
182
|
}
|
|
177
183
|
else {
|
|
178
|
-
|
|
184
|
+
let chunk = ["\ufeff" + csvColumns.map(csvEscape).join(",")];
|
|
179
185
|
for (const row of csvRows) {
|
|
180
|
-
|
|
181
|
-
|
|
186
|
+
chunk.push(csvColumns.map((column) => csvEscape(formatScalar(row[column]))).join(","));
|
|
187
|
+
if (chunk.length >= LINES_PER_WRITE) {
|
|
188
|
+
await writeLines(stream, chunk);
|
|
189
|
+
chunk = [];
|
|
190
|
+
}
|
|
182
191
|
}
|
|
192
|
+
await writeLines(stream, chunk);
|
|
183
193
|
}
|
|
184
194
|
await new Promise((resolve, reject) => {
|
|
185
195
|
stream.end((err) => err ? reject(err) : resolve());
|
|
@@ -215,6 +225,10 @@ export function csvEscape(value) {
|
|
|
215
225
|
return `"${out.replaceAll("\"", "\"\"")}"`;
|
|
216
226
|
return out;
|
|
217
227
|
}
|
|
228
|
+
export const LINES_PER_WRITE = 1000;
|
|
229
|
+
export function writeLines(stream, lines) {
|
|
230
|
+
return lines.length === 0 ? Promise.resolve() : writeLine(stream, lines.join("\n"));
|
|
231
|
+
}
|
|
218
232
|
export function writeLine(stream, line) {
|
|
219
233
|
return new Promise((resolve, reject) => {
|
|
220
234
|
const ok = stream.write(line + "\n", (err) => err ? reject(err) : undefined);
|
package/dist/src/core/printer.js
CHANGED
|
@@ -5,16 +5,16 @@ import { countOutputRows, digestFile, pickList, renderOutput, saveOutputIfNeeded
|
|
|
5
5
|
import { getRowSink } from "./rowSink.js";
|
|
6
6
|
import { extractTitles, writeTitleCache } from "./titleCache.js";
|
|
7
7
|
import { CLI_VERSION } from "../version.js";
|
|
8
|
+
import { decidedExitCode, EXIT_SUPERSEDED, markIncomplete, markSuperseded } from "./exitStatus.js";
|
|
8
9
|
const LARGE_RESULT_ROWS = 50_000;
|
|
9
|
-
export const SUPERSEDED_EXIT =
|
|
10
|
+
export const SUPERSEDED_EXIT = EXIT_SUPERSEDED;
|
|
10
11
|
export async function warnIfSuperseded(output, digest) {
|
|
11
12
|
const actual = await digestFile(output).catch(() => undefined);
|
|
12
13
|
if (!actual || actual.sha256 === digest.sha256)
|
|
13
14
|
return;
|
|
14
15
|
process.stderr.write(`[gangtise] warning: ${output} was replaced by another export to the same path while this one was finishing — the file there is not this command's output `
|
|
15
16
|
+ `(wrote sha256 ${digest.sha256.slice(0, 12)}…, found ${actual.sha256.slice(0, 12)}…). Give concurrent exports distinct --output paths.\n`);
|
|
16
|
-
|
|
17
|
-
process.exitCode = SUPERSEDED_EXIT;
|
|
17
|
+
markSuperseded();
|
|
18
18
|
}
|
|
19
19
|
function warnIfLargeInMemory(items, format) {
|
|
20
20
|
if (items && items.length >= LARGE_RESULT_ROWS) {
|
|
@@ -63,7 +63,7 @@ async function stageExportMeta(output, format, rows, columns, normalized, comple
|
|
|
63
63
|
format,
|
|
64
64
|
rows,
|
|
65
65
|
complete,
|
|
66
|
-
exitCode:
|
|
66
|
+
exitCode: decidedExitCode(),
|
|
67
67
|
command: redactSecrets(process.argv.slice(2)),
|
|
68
68
|
cliVersion: CLI_VERSION,
|
|
69
69
|
fetchedAt: localIso(new Date()),
|
|
@@ -121,11 +121,10 @@ export async function printData(data, format, output, cache) {
|
|
|
121
121
|
if (typeof meta.total === "number" && format !== "json") {
|
|
122
122
|
process.stderr.write(`Total: ${meta.total}, showing: ${showing}\n`);
|
|
123
123
|
}
|
|
124
|
-
if (meta.partial === true)
|
|
125
|
-
|
|
126
|
-
}
|
|
124
|
+
if (meta.partial === true)
|
|
125
|
+
markIncomplete();
|
|
127
126
|
}
|
|
128
|
-
const complete =
|
|
127
|
+
const complete = decidedExitCode() === 0;
|
|
129
128
|
if (output) {
|
|
130
129
|
const staged = format === "csv" || format === "jsonl"
|
|
131
130
|
? await stageExportMeta(output, format, streamed ? streamed.dataRows : countOutputRows(normalized, format), columns, normalized, complete)
|
package/dist/src/core/rowSink.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createReadStream, createWriteStream } from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { assertColumnarHeader, zipFieldRow } from "./normalize.js";
|
|
5
|
-
import { csvEscape, digestFile, formatScalar, stagingPath, writeLine } from "./output.js";
|
|
5
|
+
import { csvEscape, digestFile, formatScalar, stagingPath, writeLine, writeLines, LINES_PER_WRITE } from "./output.js";
|
|
6
6
|
import { extractTitles, MAX_TITLES_PER_ENDPOINT } from "./titleCache.js";
|
|
7
7
|
export const ROW_SINK = Symbol("gangtise.rowSink");
|
|
8
8
|
const STREAM_THRESHOLD = 1000;
|
|
@@ -100,6 +100,7 @@ export class ExportSink {
|
|
|
100
100
|
}
|
|
101
101
|
async write(rows) {
|
|
102
102
|
const stream = this.stream;
|
|
103
|
+
let chunk = [];
|
|
103
104
|
for (const row of rows) {
|
|
104
105
|
let item = row;
|
|
105
106
|
if (Array.isArray(row)) {
|
|
@@ -118,7 +119,11 @@ export class ExportSink {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
+
chunk.push(JSON.stringify(item));
|
|
123
|
+
if (chunk.length >= LINES_PER_WRITE) {
|
|
124
|
+
await writeLines(stream, chunk);
|
|
125
|
+
chunk = [];
|
|
126
|
+
}
|
|
122
127
|
this.rows++;
|
|
123
128
|
if (this.cache && this.titleCount < MAX_TITLES_PER_ENDPOINT) {
|
|
124
129
|
for (const [id, title] of Object.entries(extractTitles([item], this.cache))) {
|
|
@@ -128,6 +133,7 @@ export class ExportSink {
|
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
}
|
|
136
|
+
await writeLines(stream, chunk);
|
|
131
137
|
}
|
|
132
138
|
async assembleCsv() {
|
|
133
139
|
const out = createWriteStream(this.partPath, { encoding: "utf8" });
|
|
@@ -139,17 +145,18 @@ export class ExportSink {
|
|
|
139
145
|
const input = createReadStream(this.rowsPath, { encoding: "utf8" });
|
|
140
146
|
try {
|
|
141
147
|
let index = 0;
|
|
142
|
-
|
|
148
|
+
let csvLines = [];
|
|
149
|
+
const emit = (line) => {
|
|
143
150
|
if (!line)
|
|
144
151
|
return;
|
|
145
152
|
const item = JSON.parse(line);
|
|
146
153
|
if (objectMode) {
|
|
147
154
|
if (!isObjectRow(item))
|
|
148
155
|
return;
|
|
149
|
-
|
|
156
|
+
csvLines.push(columns.map((column) => csvEscape(formatScalar(item[column]))).join(","));
|
|
150
157
|
}
|
|
151
158
|
else {
|
|
152
|
-
|
|
159
|
+
csvLines.push(`${csvEscape(String(index++))},${csvEscape(formatScalar(item))}`);
|
|
153
160
|
}
|
|
154
161
|
};
|
|
155
162
|
let rest = "";
|
|
@@ -157,9 +164,14 @@ export class ExportSink {
|
|
|
157
164
|
const lines = (rest + String(chunk)).split("\n");
|
|
158
165
|
rest = lines.pop() ?? "";
|
|
159
166
|
for (const line of lines)
|
|
160
|
-
|
|
167
|
+
emit(line);
|
|
168
|
+
if (csvLines.length >= LINES_PER_WRITE) {
|
|
169
|
+
await writeLines(out, csvLines);
|
|
170
|
+
csvLines = [];
|
|
171
|
+
}
|
|
161
172
|
}
|
|
162
|
-
|
|
173
|
+
emit(rest);
|
|
174
|
+
await writeLines(out, csvLines);
|
|
163
175
|
await endStream(out);
|
|
164
176
|
}
|
|
165
177
|
catch (error) {
|
|
@@ -39,7 +39,7 @@ export function getDispatcher() {
|
|
|
39
39
|
cachedDispatcher = new Agent({
|
|
40
40
|
keepAliveTimeout: 60_000,
|
|
41
41
|
keepAliveMaxTimeout: 600_000,
|
|
42
|
-
connections: 16,
|
|
42
|
+
connections: Math.max(16, PAGE_CONCURRENCY),
|
|
43
43
|
pipelining: 1,
|
|
44
44
|
});
|
|
45
45
|
}
|
|
@@ -62,8 +62,9 @@ export async function runWithConcurrency(items, concurrency, fn) {
|
|
|
62
62
|
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
63
63
|
return results;
|
|
64
64
|
}
|
|
65
|
-
export async function runInOrder(items, concurrency, fn, consume) {
|
|
65
|
+
export async function runInOrder(items, concurrency, fn, consume, window = concurrency) {
|
|
66
66
|
const width = Math.max(1, concurrency);
|
|
67
|
+
const maxPending = Math.max(width, window);
|
|
67
68
|
const pending = new Map();
|
|
68
69
|
let next = 0;
|
|
69
70
|
let failure = null;
|
|
@@ -104,7 +105,7 @@ export async function runInOrder(items, concurrency, fn, consume) {
|
|
|
104
105
|
return;
|
|
105
106
|
pending.set(index, result);
|
|
106
107
|
drain();
|
|
107
|
-
while (!failure && pending.size >=
|
|
108
|
+
while (!failure && pending.size >= maxPending) {
|
|
108
109
|
await new Promise((resolve) => waiters.push(resolve));
|
|
109
110
|
}
|
|
110
111
|
});
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.
|
|
1
|
+
export const CLI_VERSION = "0.42.0";
|
|
@@ -115,7 +115,7 @@ gangtise ai theme-tracking --theme-id <id> --date <yyyy-MM-dd> [--type <name>]
|
|
|
115
115
|
**示例:**
|
|
116
116
|
```bash
|
|
117
117
|
# 查"核电"主题 2026-05-09 的晚报
|
|
118
|
-
|
|
118
|
+
gangtise ai theme-tracking --theme-id 121000002 --date 2026-05-09 --type night --format json
|
|
119
119
|
# 返回 [{"type":"night","date":"2026-05-09","content":"..."}]
|
|
120
120
|
```
|
|
121
121
|
|
|
@@ -95,7 +95,7 @@ gangtise fundamental valuation-analysis --security-code <code> --indicator <name
|
|
|
95
95
|
- 🔴 **序列按自然日逐日一行(含周末)**,行数超过 `--limit` 时接口**保留最近的行、丢掉区间开头**。CLI 撞满即标 `partial`、退出码 3 并在 stderr 说明(首行恰好就是 `--start-date` 时说明没丢,不标)。取长区间把 `--limit` 设到不小于区间天数(账号权限窗口内的十年约 3700 行,可设 `--limit 4000`)
|
|
96
96
|
- 🔴 **`--start-date` 早于账号回溯下界时,接口从下界起返回、不报错**(整段都在界外才报 `110003`)。首行晚于 `--start-date` 时 CLI 在 stderr 提示——可能是该证券上市较晚,也可能是撞了权限窗口,看首行日期判断
|
|
97
97
|
- `--skip-null`:丢弃 `value`/`percentileRank` 为 null 的行(最新交易日可能未入库)
|
|
98
|
-
- **返回字段只有 7 个**:`tradeDate` `value` `percentileRank` `average` `median` `upper1Std` `lower1Std`,**没有 `securityCode`**。🔴 `--field` 里**至少要有一个数值列**(`tradeDate` 以外的 6 个):只传 `tradeDate`、只传不存在的名字(含 `securityCode`)时接口返回 0
|
|
98
|
+
- **返回字段只有 7 个**:`tradeDate` `value` `percentileRank` `average` `median` `upper1Std` `lower1Std`,**没有 `securityCode`**。🔴 `--field` 里**至少要有一个数值列**(`tradeDate` 以外的 6 个):只传 `tradeDate`、只传不存在的名字(含 `securityCode`)时接口返回 0 行、**不报错**,读起来像「这只票没有估值数据」(CLI 会在 stderr 提示原因);数值列与不存在的名字混传时,回显的字段比每行的值多,CLI 直接报错退出 1。`tradeDate` 总在第一列返回,不用写进 `--field`(写了 CLI 会在发请求前去掉;同一字段写两次也只发一次)。**建议不传 `--field`**,证券代码本来就是你自己传进去的
|
|
99
99
|
|
|
100
100
|
## A股盈利预测 `fundamental earning-forecast`
|
|
101
101
|
|
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
| 情形 | CLI 行为 | Agent 是否介入 |
|
|
70
70
|
|------|---------|--------------|
|
|
71
71
|
| HTTP 5xx / `ECONNRESET` / 超时 | **自动指数退避重试 ×2**(🔴 贵档端点不重放) | 仍失败提示用户 |
|
|
72
|
-
| `ValidationError` | 本地参数校验失败 | 检查 `--from` / `--size` / `--limit`
|
|
72
|
+
| `ValidationError` | 本地参数校验失败 | 检查 `--from` / `--size` / `--limit` 数值(数值只收十进制写法,`1e3` / `0x10` / `5.0` 会被拒),**不要重试同命令** |
|
|
73
|
+
| 返回结构与约定不符(报错含 `returned no … payload` 与 trace) | 不猜测结构、按失败处理:单次请求的命令退出 1;分批 / 分片中某一批出现时,已取到的保留、其余标为未取(退出 3) | 带 trace 反馈;换参数重试没用 |
|
|
73
74
|
|
|
74
75
|
## 退出码 3:结果少了数据
|
|
75
76
|
|
|
@@ -81,10 +82,12 @@
|
|
|
81
82
|
|
|
82
83
|
## 退出码 4:数据写全了,但文件被另一次导出替换
|
|
83
84
|
|
|
84
|
-
本次导出的数据完整写出了,`--output` 指向的文件却在收尾期间被**另一次写向同一路径的导出**替换掉——文件在,只是不是这条命令的产物。stderr 会给出两边的 `sha256` 前缀。给并发的导出各自的 `--output`
|
|
85
|
+
本次导出的数据完整写出了,`--output` 指向的文件却在收尾期间被**另一次写向同一路径的导出**替换掉——文件在,只是不是这条命令的产物。stderr 会给出两边的 `sha256` 前缀。给并发的导出各自的 `--output` 即可。本次生成的 `.meta.json` 记录的是替换检查之前的判定;并发导出时,数据文件和元信息都可能被另一次导出覆盖。所以退出码 4 以本次进程的退出码和 stderr 为准,`sha256` 只能说明当前的数据文件与旁边的元信息是否匹配。
|
|
85
86
|
|
|
86
87
|
与 `3` 分开是因为行没有缺、缺的是目的地。**两者同时发生时退出码是 `3`**——少了行是更重的那一个,替换另在 stderr 说明。
|
|
87
88
|
|
|
89
|
+
**接管道时**:`gangtise … | head` 这类提前关掉读端的用法,CLI 仍以已判定的退出码(`3` / `4`)结束;shell 默认只看管道里最后一个命令,脚本要 `set -o pipefail` 才看得到。
|
|
90
|
+
|
|
88
91
|
## 其他场景
|
|
89
92
|
|
|
90
93
|
- CLI 未安装 → `npm install -g gangtise-openapi-cli`
|
|
@@ -128,7 +131,7 @@
|
|
|
128
131
|
|
|
129
132
|
**`quote day-kline --security all` 被 CLI 拒**(`'all' is not a whole-market keyword`)→ 服务端不支持该写法 → 换成 `aShares` / `hkStocks` / `usStocks` 之一,单独传。
|
|
130
133
|
|
|
131
|
-
**AI
|
|
134
|
+
**AI 生成命令超时** → `one-pager` 等 7 个 agent 类 CLI 已内置 120 秒超时下限,无需再设 `GANGTISE_TIMEOUT_MS`。超时报错时内容可能已在服务端生成并扣费,**同参数重跑会再扣一次**:先告知用户,别自动重跑(详见 `references/commands/ai.md`)。`stock-summary` / `hot-topic` 可前置 `GANGTISE_TIMEOUT_MS=120000`(写整数毫秒;没按所写数值生效时 stderr 会提示)。
|
|
132
135
|
|
|
133
136
|
**估值结果出现大量 `null`** → 最新交易日数据未入库 → 加 `--skip-null` 过滤掉 `value` / `percentileRank` 为 null 的行。
|
|
134
137
|
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
1. 路由 → ai one-pager
|
|
54
54
|
2. 比亚迪 → 002594.SZ(速查表)
|
|
55
55
|
注意:one-pager 用 --security-code,不是 --security
|
|
56
|
-
3. Pre-flight:首次生成常 >30s
|
|
57
|
-
4.
|
|
56
|
+
3. Pre-flight:首次生成常 >30s,CLI 已内置 120 秒超时下限,无需前缀;告知用户"生成中..."
|
|
57
|
+
4. gangtise ai one-pager --security-code 002594.SZ --format json
|
|
58
58
|
5. 返回 {content: "markdown"} → 直接呈现 content,不要展示 JSON 包装
|
|
59
59
|
```
|
|
60
60
|
|