echopai 2.1.0 → 2.3.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 +13 -1
- package/dist/_generated/commands.js +98 -42
- package/dist/_generated/help.js +118 -53
- package/dist/_generated/operations.js +1056 -356
- package/dist/bin.js +67 -13
- package/dist/runtime/invoker.js +4 -0
- package/dist/runtime/update_check.js +120 -0
- package/dist/runtime/update_worker.js +63 -0
- package/dist/runtime/verb_cmd.js +2 -0
- package/dist/tools/upgrade.js +103 -0
- package/dist/tools/welcome.js +225 -0
- package/dist/verbs/announcements.js +195 -0
- package/dist/verbs/concepts.js +393 -0
- package/dist/verbs/digest.js +24 -15
- package/dist/verbs/financials.js +212 -0
- package/dist/verbs/index.js +43 -7
- package/dist/verbs/limit_up.js +156 -0
- package/dist/verbs/lookup.js +1 -1
- package/dist/verbs/market.js +185 -0
- package/dist/verbs/news.js +17 -3
- package/dist/verbs/quote.js +1 -1
- package/dist/verbs/sentiment.js +191 -6
- package/dist/verbs/views.js +5 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/verbs/research.js +0 -44
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `echopai` (bare) / `echopai welcome` — onboarding screen.
|
|
3
|
+
*
|
|
4
|
+
* Goal: give a TTY user a Claude-Code-style landing page when they type
|
|
5
|
+
* `echopai` with no args. Banner → auth status → grouped command examples
|
|
6
|
+
* → pointers to deeper help. AI / CI / non-TTY callers fall back to
|
|
7
|
+
* commander's plain help (handled by the bin.ts dispatcher).
|
|
8
|
+
*
|
|
9
|
+
* No network calls. We probe credentials locally (env / config file) only —
|
|
10
|
+
* a live `whoami` would slow the screen and is the wrong default for the
|
|
11
|
+
* "user just typed `echopai`" path. `echopai status` / `echopai whoami`
|
|
12
|
+
* cover the live-check use case.
|
|
13
|
+
*/
|
|
14
|
+
import { Command } from "commander";
|
|
15
|
+
import { resolveCredentials, AuthMissingError } from "../runtime/auth.js";
|
|
16
|
+
import { bold, cyan, dim, green, isTtyHuman, yellow } from "../runtime/tty.js";
|
|
17
|
+
import { maybeEmitUpdateBanner } from "../runtime/update_check.js";
|
|
18
|
+
import { CLI_VERSION } from "../version.js";
|
|
19
|
+
function maskKey(key) {
|
|
20
|
+
// eps_live_<lookup>_<secret> → keep prefix + first 4 of lookup + masked tail.
|
|
21
|
+
if (key.length <= 16)
|
|
22
|
+
return "***";
|
|
23
|
+
return key.slice(0, 13) + "***";
|
|
24
|
+
}
|
|
25
|
+
function snapshotAuth() {
|
|
26
|
+
try {
|
|
27
|
+
const creds = resolveCredentials({});
|
|
28
|
+
if (creds.profile) {
|
|
29
|
+
return {
|
|
30
|
+
state: "profile",
|
|
31
|
+
profile: creds.profile,
|
|
32
|
+
keyHint: maskKey(creds.key),
|
|
33
|
+
baseUrl: creds.baseUrl,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return { state: "env", keyHint: maskKey(creds.key), baseUrl: creds.baseUrl };
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
if (e instanceof AuthMissingError)
|
|
40
|
+
return { state: "missing" };
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Right-pad a string to width `w`, ignoring ANSI escapes for length. */
|
|
45
|
+
function padRight(s, w) {
|
|
46
|
+
// strip ANSI for length measurement
|
|
47
|
+
// eslint-disable-next-line no-control-regex
|
|
48
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
49
|
+
const pad = Math.max(0, w - visible.length);
|
|
50
|
+
return s + " ".repeat(pad);
|
|
51
|
+
}
|
|
52
|
+
const COMMAND_GROUPS = [
|
|
53
|
+
{
|
|
54
|
+
title: "🔍 快速检索",
|
|
55
|
+
items: [
|
|
56
|
+
{ cmd: "echopai lookup --text 贵州茅台", note: "中文名 / 拼音首字母 → canonical_code" },
|
|
57
|
+
{ cmd: "echopai digest --code SSE:600519", note: "一键研究摘要(5 桶 fan-out,容忍部分失败)" },
|
|
58
|
+
{ cmd: "echopai search --query \"AI 算力\"", note: "题材 / 概念语义搜索" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: "📈 实时行情",
|
|
63
|
+
items: [
|
|
64
|
+
{ cmd: "echopai quote --codes \"SSE:600519,SZSE:000001\"", note: "1-200 只实时报价" },
|
|
65
|
+
{ cmd: "echopai market status", note: "A 股会话状态(pre-open/open/lunch/closed)+ 节假日" },
|
|
66
|
+
{ cmd: "echopai market movers --sort pct --top 20", note: "/market 涨幅榜 top 20(pct/pct_asc/speed/amount/turnover/total_mv)" },
|
|
67
|
+
{ cmd: "echopai market movers --sort speed --top 30", note: "3 分钟涨速榜(短线脉冲)" },
|
|
68
|
+
{ cmd: "echopai market movers --sort amount --exchange SSE", note: "上证成交额榜" },
|
|
69
|
+
{ cmd: "echopai sentiment", note: "市场情绪总览(实时;涨跌停 / 宽度 / 分化)" },
|
|
70
|
+
{ cmd: "echopai sentiment overview --date 2026-05-20", note: "任意历史日的情绪聚合(最后一分钟)" },
|
|
71
|
+
{ cmd: "echopai sentiment breadth --date 2026-05-20", note: "全天 ~241 分钟时序(任意日)" },
|
|
72
|
+
{ cmd: "echopai sentiment turnover --date 2026-05-20 --days 5", note: "成交额时序 + 5 日同分钟同比" },
|
|
73
|
+
{ cmd: "echopai hot", note: "机构推荐热股榜" },
|
|
74
|
+
{ cmd: "echopai scan", note: "全市场快照(约 5800 只)" },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
title: "🚀 涨停板",
|
|
79
|
+
items: [
|
|
80
|
+
{ cmd: "echopai limit-up summary", note: "涨停数 / 炸板数 / 跌停数 / 连板梯队" },
|
|
81
|
+
{ cmd: "echopai limit-up pool", note: "当日涨停股逐股明细(连板 / 封单 / 一字板)" },
|
|
82
|
+
{ cmd: "echopai limit-up history --days 30", note: "近 N 日涨停 / 最高连板趋势" },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
title: "🧩 概念板块",
|
|
87
|
+
items: [
|
|
88
|
+
{ cmd: "echopai concepts list --sort pct", note: "概念涨幅榜(pct/amount/limit_up/stock_count/speed)" },
|
|
89
|
+
{ cmd: "echopai concepts list --sort speed", note: "概念 3 分钟涨速榜(客户端排序 speed_3min)" },
|
|
90
|
+
{ cmd: "echopai concepts alerts", note: "当前激活概念异动(big_move / limit_up_cluster)" },
|
|
91
|
+
{ cmd: "echopai concepts show <concept_id>", note: "单概念 meta + 成份股 + 最新行情" },
|
|
92
|
+
{ cmd: "echopai concepts daily-bars <concept_id> --from ... --to ...", note: "概念指数 K 线(链式等权 base 1000)" },
|
|
93
|
+
{ cmd: "echopai concepts news <concept_id>", note: "概念关联新闻(ILIKE)" },
|
|
94
|
+
{ cmd: "echopai concepts views <concept_id>", note: "概念关联券商观点(主源研究)" },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
title: "💰 财务数据",
|
|
99
|
+
items: [
|
|
100
|
+
{ cmd: "echopai financials quote-snapshot --code SSE:600519", note: "估值快照 14 字段(PE/PB/PS/换手率/股息率/量比)★头牌★" },
|
|
101
|
+
{ cmd: "echopai financials pit --code SSE:600519 --date 2024-11-01", note: "Point-in-time 财务指标(回测防穿越)" },
|
|
102
|
+
{ cmd: "echopai financials reports --code SSE:600519 --kind annual", note: "最近 N 期财务报告(~25 字段/期)" },
|
|
103
|
+
{ cmd: "echopai financials series --code SSE:600519 --metric roe_simple", note: "单指标历史时间序列" },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
title: "📰 市场观点/研究/资讯",
|
|
108
|
+
items: [
|
|
109
|
+
{ cmd: "echopai views --code SSE:600519", note: "★主源★ 卖方研报 / 分析师观点" },
|
|
110
|
+
{ cmd: "echopai news search --query AI --since-hours 24", note: "辅源新闻 / 简讯(短时窗)" },
|
|
111
|
+
{ cmd: "echopai announcements feed --type annual_report", note: "全市场公告 feed(cninfo,按 published_at desc)" },
|
|
112
|
+
{ cmd: "echopai announcements stock --code SSE:600519 --since-days 30", note: "单股公告历史窗口(最长 5 年)" },
|
|
113
|
+
{ cmd: "echopai announcements detail <uuid>", note: "单条公告完整正文 + meta" },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
title: "📊 K 线",
|
|
118
|
+
items: [
|
|
119
|
+
{ cmd: "echopai chart --code SSE:600519 --from 2026-01-01 --to 2026-05-01", note: "单只 K 线(日线 / 分钟线)" },
|
|
120
|
+
{ cmd: "echopai bars-batch --codes A,B,C --from ... --to ...", note: "批量 K 线(≤100 只)" },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
title: "🤖 AI 集成",
|
|
125
|
+
items: [
|
|
126
|
+
{ cmd: "echopai mcp serve", note: "启动 MCP stdio 服务(Claude Desktop / Cursor / Claude Code)" },
|
|
127
|
+
{ cmd: "echopai schema export", note: "导出全部 operation schema 给 agent 喂养" },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
const POINTERS = [
|
|
132
|
+
{ cmd: "echopai --help", note: "完整命令树" },
|
|
133
|
+
{ cmd: "echopai <verb> --help", note: "查看单个命令的参数和示例" },
|
|
134
|
+
{ cmd: "echopai whoami", note: "在线检查 token 能力 / scopes" },
|
|
135
|
+
{ cmd: "echopai doctor", note: "诊断鉴权 / 网络连通性" },
|
|
136
|
+
{ cmd: "echopai status", note: "查看本地 profile + 鉴权状态" },
|
|
137
|
+
{ cmd: "echopai upgrade [--exec]", note: "检查新版(24h cache),--exec 直接 npm i -g 升级" },
|
|
138
|
+
];
|
|
139
|
+
const BANNER_PLAIN = [
|
|
140
|
+
" ███████ ██████ ██ ██ ██████ ██████ █████ ██",
|
|
141
|
+
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██",
|
|
142
|
+
" █████ ██ ███████ ██ ██ ██████ ███████ ██",
|
|
143
|
+
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██",
|
|
144
|
+
" ███████ ██████ ██ ██ ██████ ██ ██ ██ ██",
|
|
145
|
+
];
|
|
146
|
+
export function renderWelcome(now = () => new Date()) {
|
|
147
|
+
const lines = [];
|
|
148
|
+
const auth = snapshotAuth();
|
|
149
|
+
// Banner
|
|
150
|
+
lines.push("");
|
|
151
|
+
for (const row of BANNER_PLAIN)
|
|
152
|
+
lines.push(cyan(row));
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push(` ${bold("EchoPai CLI")} ${dim("v" + CLI_VERSION)} ${dim("·")} ${dim("面向 AI Agent 的 A 股数据接入终端")}`);
|
|
155
|
+
lines.push("");
|
|
156
|
+
// Auth status
|
|
157
|
+
if (auth.state === "missing") {
|
|
158
|
+
lines.push(` ${yellow("●")} ${bold("未登录")} ${dim("—")} 运行 ${cyan("`echopai login --key eps_live_<lookup>_<secret>`")} 或设置环境变量 ${cyan("ECHOPAI_KEY")}`);
|
|
159
|
+
}
|
|
160
|
+
else if (auth.state === "env") {
|
|
161
|
+
lines.push(` ${green("●")} ${bold("已登录")}(环境变量 ${cyan("ECHOPAI_KEY")})${dim(`(${auth.keyHint})`)} → ${dim(auth.baseUrl)}`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
lines.push(` ${green("●")} ${bold("已登录")} profile ${cyan(auth.profile)} ${dim(`(${auth.keyHint})`)} → ${dim(auth.baseUrl)}`);
|
|
165
|
+
}
|
|
166
|
+
lines.push("");
|
|
167
|
+
// Command groups
|
|
168
|
+
lines.push(` ${dim("── 常用命令 ─────────────────────────────────────────────────")}`);
|
|
169
|
+
lines.push("");
|
|
170
|
+
// Reserve column 1 for cmd, column 2 for note. Compute padding from the
|
|
171
|
+
// widest cmd across all groups to keep alignment stable.
|
|
172
|
+
const widest = Math.min(72, COMMAND_GROUPS.flatMap((g) => g.items)
|
|
173
|
+
.map((it) => it.cmd.length)
|
|
174
|
+
.reduce((a, b) => Math.max(a, b), 0));
|
|
175
|
+
for (const group of COMMAND_GROUPS) {
|
|
176
|
+
lines.push(` ${bold(group.title)}`);
|
|
177
|
+
for (const it of group.items) {
|
|
178
|
+
const cmd = cyan(it.cmd);
|
|
179
|
+
lines.push(` ${padRight(cmd, widest + 4)} ${dim(it.note)}`);
|
|
180
|
+
}
|
|
181
|
+
lines.push("");
|
|
182
|
+
}
|
|
183
|
+
// Pointers
|
|
184
|
+
lines.push(` ${dim("── 更多 ─────────────────────────────────────────────────────")}`);
|
|
185
|
+
lines.push("");
|
|
186
|
+
for (const it of POINTERS) {
|
|
187
|
+
lines.push(` ${padRight(cyan(it.cmd), widest + 4)} ${dim(it.note)}`);
|
|
188
|
+
}
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push(` ${dim("官网: https://www.echopai.com · 反馈: service@echopai.com")}`);
|
|
191
|
+
lines.push("");
|
|
192
|
+
// Timestamp footer so users know this isn't a stale cache
|
|
193
|
+
const ts = now().toISOString().replace("T", " ").slice(0, 19);
|
|
194
|
+
lines.push(` ${dim("生成于 " + ts + " UTC · Ctrl+C 退出")}`);
|
|
195
|
+
lines.push("");
|
|
196
|
+
return lines.join("\n");
|
|
197
|
+
}
|
|
198
|
+
/** Print welcome screen to stdout (no exit — caller decides). */
|
|
199
|
+
export function printWelcome() {
|
|
200
|
+
process.stdout.write(renderWelcome());
|
|
201
|
+
// Surface available update from local cache (worker refreshes it in the
|
|
202
|
+
// background). No network call here — fast even when there's nothing to say.
|
|
203
|
+
maybeEmitUpdateBanner();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Decide whether bare `echopai` (no args, no flags) should show the welcome
|
|
207
|
+
* screen. False for AI / CI / piped output — they get commander's plain help.
|
|
208
|
+
*/
|
|
209
|
+
export function shouldShowWelcome(argv) {
|
|
210
|
+
// node + script + (no extra args)
|
|
211
|
+
if (argv.length !== 2)
|
|
212
|
+
return false;
|
|
213
|
+
if (!isTtyHuman)
|
|
214
|
+
return false;
|
|
215
|
+
if (process.env.CI)
|
|
216
|
+
return false;
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
export function buildWelcomeCommand() {
|
|
220
|
+
return new Command("welcome")
|
|
221
|
+
.description("显示欢迎屏(banner + 常用命令 + 鉴权状态)")
|
|
222
|
+
.action(() => {
|
|
223
|
+
printWelcome();
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `echopai announcements ...` + MCP tools `announcements_feed` /
|
|
3
|
+
* `announcements_stock` / `announcements_detail`.
|
|
4
|
+
*
|
|
5
|
+
* A 股公告 (cninfo 主源):
|
|
6
|
+
* - feed —— 全市场最近公告,按 published_at desc
|
|
7
|
+
* - stock —— 指定 canonical_code 的公告历史窗口(最长 5 年)
|
|
8
|
+
* - detail —— 单条公告完整正文(content_md + content_text + meta)
|
|
9
|
+
*
|
|
10
|
+
* 三个 op 共享 `announcements:read` scope,calling 协议参数与底层 OpenAPI
|
|
11
|
+
* 一致(type slug / since_days 等)。MCP 注册成三个 discrete tool,AI agent
|
|
12
|
+
* 可按 "我要看 X 的财报披露" / "今天市场公告" / "把这条公告全文给我" 各自取用。
|
|
13
|
+
*/
|
|
14
|
+
import { Command, Option } from "commander";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { OPERATIONS } from "../_generated/operations.js";
|
|
17
|
+
import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
|
|
18
|
+
import { callOp } from "../runtime/verb_runner.js";
|
|
19
|
+
const CODE_RE = /^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/;
|
|
20
|
+
const ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/;
|
|
21
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
22
|
+
export const announcementsFeedSpec = {
|
|
23
|
+
name: "announcements_feed",
|
|
24
|
+
description: "Recent A-share announcement feed (cninfo source, sorted by published_at desc). Filter by `type` slug (e.g. annual_report / equity_change / restructuring) or `since` lower bound. Use when surveying market-wide disclosures over a recent window — for a specific stock prefer `announcements_stock`.",
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: z
|
|
27
|
+
.string()
|
|
28
|
+
.max(64)
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("分类 slug 过滤(如 annual_report / equity_change / restructuring)。"),
|
|
31
|
+
since: z
|
|
32
|
+
.string()
|
|
33
|
+
.regex(ISO_DATETIME_RE)
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("ISO datetime 下限(published_at >= since)。"),
|
|
36
|
+
limit: z
|
|
37
|
+
.number()
|
|
38
|
+
.int()
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(200)
|
|
41
|
+
.default(50)
|
|
42
|
+
.describe("Max items (1-200, default 50)"),
|
|
43
|
+
offset: z
|
|
44
|
+
.number()
|
|
45
|
+
.int()
|
|
46
|
+
.min(0)
|
|
47
|
+
.default(0)
|
|
48
|
+
.describe("Pagination offset"),
|
|
49
|
+
},
|
|
50
|
+
handler: async (args, ctx) => {
|
|
51
|
+
const op = OPERATIONS["announcements.feed"];
|
|
52
|
+
if (!op)
|
|
53
|
+
throw new Error("announcements.feed op missing");
|
|
54
|
+
const callArgs = {
|
|
55
|
+
limit: args.limit,
|
|
56
|
+
offset: args.offset,
|
|
57
|
+
};
|
|
58
|
+
if (args.type)
|
|
59
|
+
callArgs.type = args.type;
|
|
60
|
+
if (args.since)
|
|
61
|
+
callArgs.since = args.since;
|
|
62
|
+
return callOp(op, callArgs, ctx);
|
|
63
|
+
},
|
|
64
|
+
backingOps: ["announcements.feed"],
|
|
65
|
+
};
|
|
66
|
+
export const announcementsStockSpec = {
|
|
67
|
+
name: "announcements_stock",
|
|
68
|
+
description: "Announcement history for a single A-share security (cninfo). Default lookback 30 days, max 5 years. AH dual-listing: pass the A-share canonical (announcements are A-share only — HK side has separate HKEX disclosures not covered here).",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
code: z
|
|
71
|
+
.string()
|
|
72
|
+
.regex(CODE_RE)
|
|
73
|
+
.describe("A-share canonical code (e.g. SSE:600519)"),
|
|
74
|
+
since_days: z
|
|
75
|
+
.number()
|
|
76
|
+
.int()
|
|
77
|
+
.min(1)
|
|
78
|
+
.max(1825)
|
|
79
|
+
.default(30)
|
|
80
|
+
.describe("Lookback window in days (1-1825, default 30)"),
|
|
81
|
+
type: z
|
|
82
|
+
.string()
|
|
83
|
+
.max(64)
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("分类 slug 过滤(与 feed 同集合)"),
|
|
86
|
+
limit: z
|
|
87
|
+
.number()
|
|
88
|
+
.int()
|
|
89
|
+
.min(1)
|
|
90
|
+
.max(200)
|
|
91
|
+
.default(50)
|
|
92
|
+
.describe("Max items (1-200, default 50)"),
|
|
93
|
+
offset: z
|
|
94
|
+
.number()
|
|
95
|
+
.int()
|
|
96
|
+
.min(0)
|
|
97
|
+
.default(0)
|
|
98
|
+
.describe("Pagination offset"),
|
|
99
|
+
},
|
|
100
|
+
handler: async (args, ctx) => {
|
|
101
|
+
const op = OPERATIONS["announcements.stock"];
|
|
102
|
+
if (!op)
|
|
103
|
+
throw new Error("announcements.stock op missing");
|
|
104
|
+
const callArgs = {
|
|
105
|
+
code: args.code,
|
|
106
|
+
since_days: args.since_days,
|
|
107
|
+
limit: args.limit,
|
|
108
|
+
offset: args.offset,
|
|
109
|
+
};
|
|
110
|
+
if (args.type)
|
|
111
|
+
callArgs.type = args.type;
|
|
112
|
+
return callOp(op, callArgs, ctx);
|
|
113
|
+
},
|
|
114
|
+
backingOps: ["announcements.stock"],
|
|
115
|
+
};
|
|
116
|
+
export const announcementsDetailSpec = {
|
|
117
|
+
name: "announcements_detail",
|
|
118
|
+
description: "Fetch one announcement by id (UUID from feed/stock items[].id). Returns full content_md + content_text + cninfo meta + parse status. Use when the summary in feed/stock is not enough and you need the full document body for citation or NLP.",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
announcement_id: z
|
|
121
|
+
.string()
|
|
122
|
+
.regex(UUID_RE)
|
|
123
|
+
.describe("UUID (feed / stock 返回的 items[].id)"),
|
|
124
|
+
},
|
|
125
|
+
handler: async (args, ctx) => {
|
|
126
|
+
const op = OPERATIONS["announcements.detail"];
|
|
127
|
+
if (!op)
|
|
128
|
+
throw new Error("announcements.detail op missing");
|
|
129
|
+
return callOp(op, { announcement_id: args.announcement_id }, ctx);
|
|
130
|
+
},
|
|
131
|
+
backingOps: ["announcements.detail"],
|
|
132
|
+
};
|
|
133
|
+
function clampInt(raw, min, max, fallback) {
|
|
134
|
+
const n = Math.floor(Number(raw));
|
|
135
|
+
if (!Number.isFinite(n))
|
|
136
|
+
return fallback;
|
|
137
|
+
if (n < min)
|
|
138
|
+
return min;
|
|
139
|
+
if (n > max)
|
|
140
|
+
return max;
|
|
141
|
+
return n;
|
|
142
|
+
}
|
|
143
|
+
export function buildAnnouncementsCommand() {
|
|
144
|
+
const cmd = new Command("announcements").description("A-share announcements (cninfo) — recent feed, per-stock history, single-item detail.");
|
|
145
|
+
const feed = cmd.command("feed").description(announcementsFeedSpec.description);
|
|
146
|
+
feed.addOption(new Option("--type <slug>", "Filter by 分类 slug (annual_report, equity_change, ...)"));
|
|
147
|
+
feed.addOption(new Option("--since <ISO-datetime>", "Lower bound published_at (RFC3339)"));
|
|
148
|
+
feed.addOption(new Option("--limit <n>", "Max items (1-200)").default("50"));
|
|
149
|
+
feed.addOption(new Option("--offset <n>", "Pagination offset").default("0"));
|
|
150
|
+
feed.action(async (opts) => {
|
|
151
|
+
if (!OPERATIONS["announcements.feed"]) {
|
|
152
|
+
emitVerbError("internal_error", "announcements.feed missing", undefined, 2);
|
|
153
|
+
}
|
|
154
|
+
const args = {
|
|
155
|
+
limit: clampInt(opts.limit, 1, 200, 50),
|
|
156
|
+
offset: Math.max(0, Math.floor(Number(opts.offset)) || 0),
|
|
157
|
+
};
|
|
158
|
+
if (opts.type)
|
|
159
|
+
args.type = opts.type;
|
|
160
|
+
if (opts.since)
|
|
161
|
+
args.since = opts.since;
|
|
162
|
+
await executeVerb(async (ctx) => announcementsFeedSpec.handler(args, ctx));
|
|
163
|
+
});
|
|
164
|
+
const stock = cmd.command("stock").description(announcementsStockSpec.description);
|
|
165
|
+
stock.addOption(new Option("--code <canonical_code>", "A-share canonical (e.g. SSE:600519)")
|
|
166
|
+
.makeOptionMandatory(true));
|
|
167
|
+
stock.addOption(new Option("--since-days <n>", "Lookback window in days (1-1825)").default("30"));
|
|
168
|
+
stock.addOption(new Option("--type <slug>", "Filter by 分类 slug"));
|
|
169
|
+
stock.addOption(new Option("--limit <n>", "Max items (1-200)").default("50"));
|
|
170
|
+
stock.addOption(new Option("--offset <n>", "Pagination offset").default("0"));
|
|
171
|
+
stock.action(async (opts) => {
|
|
172
|
+
if (!OPERATIONS["announcements.stock"]) {
|
|
173
|
+
emitVerbError("internal_error", "announcements.stock missing", undefined, 2);
|
|
174
|
+
}
|
|
175
|
+
const args = {
|
|
176
|
+
code: opts.code,
|
|
177
|
+
since_days: clampInt(opts.sinceDays, 1, 1825, 30),
|
|
178
|
+
limit: clampInt(opts.limit, 1, 200, 50),
|
|
179
|
+
offset: Math.max(0, Math.floor(Number(opts.offset)) || 0),
|
|
180
|
+
};
|
|
181
|
+
if (opts.type)
|
|
182
|
+
args.type = opts.type;
|
|
183
|
+
await executeVerb(async (ctx) => announcementsStockSpec.handler(args, ctx));
|
|
184
|
+
});
|
|
185
|
+
const detail = cmd
|
|
186
|
+
.command("detail <announcement_id>")
|
|
187
|
+
.description(announcementsDetailSpec.description);
|
|
188
|
+
detail.action(async (announcementId) => {
|
|
189
|
+
if (!OPERATIONS["announcements.detail"]) {
|
|
190
|
+
emitVerbError("internal_error", "announcements.detail missing", undefined, 2);
|
|
191
|
+
}
|
|
192
|
+
await executeVerb(async (ctx) => announcementsDetailSpec.handler({ announcement_id: announcementId }, ctx));
|
|
193
|
+
});
|
|
194
|
+
return cmd;
|
|
195
|
+
}
|