echopai 2.0.0 → 2.2.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 +46 -38
- package/dist/_generated/help.js +46 -41
- package/dist/_generated/operations.js +390 -167
- package/dist/bin.js +18 -7
- package/dist/tools/welcome.js +190 -0
- package/dist/verbs/digest.js +16 -14
- package/dist/verbs/financials.js +212 -0
- package/dist/verbs/index.js +14 -6
- package/dist/verbs/search.js +105 -0
- package/dist/verbs/views.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/verbs/research.js +0 -44
package/dist/bin.js
CHANGED
|
@@ -20,20 +20,21 @@ import { invoke } from "./runtime/invoker.js";
|
|
|
20
20
|
import { buildApiCommand } from "./tools/api.js";
|
|
21
21
|
import { buildMcpCommand } from "./tools/mcp.js";
|
|
22
22
|
import { buildRawCallCommand } from "./tools/raw.js";
|
|
23
|
-
import { buildBarsBatchCommand, buildChartCommand, buildDigestCommand, buildHotCommand, buildLookupCommand, buildNewsCommand, buildQuoteCommand,
|
|
23
|
+
import { buildBarsBatchCommand, buildChartCommand, buildDigestCommand, buildFinancialsCommand, buildHotCommand, buildLookupCommand, buildNewsCommand, buildQuoteCommand, buildScanCommand, buildSearchCommand, buildSentimentCommand, buildViewsCommand, } from "./verbs/index.js";
|
|
24
24
|
import { buildCompletionCommand } from "./tools/completion.js";
|
|
25
25
|
import { buildConfigCommand } from "./tools/config.js";
|
|
26
26
|
import { buildLoginCommand, buildLogoutCommand, buildStatusCommand } from "./tools/login.js";
|
|
27
27
|
import { buildDoctorCommand } from "./tools/doctor.js";
|
|
28
28
|
import { buildSchemaCommand } from "./tools/schema.js";
|
|
29
29
|
import { buildTraceCommand } from "./tools/trace.js";
|
|
30
|
+
import { buildWelcomeCommand, printWelcome, shouldShowWelcome } from "./tools/welcome.js";
|
|
30
31
|
import { buildWhoamiCommand } from "./tools/whoami.js";
|
|
31
32
|
import { CLI_VERSION } from "./version.js";
|
|
32
33
|
const program = new Command();
|
|
33
34
|
program
|
|
34
35
|
.name("echopai")
|
|
35
36
|
.description("EchoPai CLI v1 — partner-grade access to stock-market data, news, sentiment,\n" +
|
|
36
|
-
"
|
|
37
|
+
"and analyst views.\n\n" +
|
|
37
38
|
"AI-First: JSON envelope on stdout, JSON errors on stderr, three-state exit\n" +
|
|
38
39
|
"codes (0 success / 1 user error / 2 service error). Set ECHOPAI_KEY env\n" +
|
|
39
40
|
"var or run `echopai login`.")
|
|
@@ -53,6 +54,7 @@ program
|
|
|
53
54
|
.addHelpText("after", "\nAuthentication: ECHOPAI_KEY=<eps_live_<lookup>_<secret>> echopai <cmd>\n" +
|
|
54
55
|
"Raw mirror: echopai raw <noun> <verb> # all OpenAPI ops, e.g. raw news search\n" +
|
|
55
56
|
"Curated verbs: echopai <verb> # task-level entry (Phase 3.6+)\n" +
|
|
57
|
+
"Fundamentals: echopai financials quote-snapshot --code SSE:600519 # PE/PB/PS/换手率/股息率/量比 14-field snapshot\n" +
|
|
56
58
|
"Capability: echopai whoami | echopai doctor\n" +
|
|
57
59
|
"Schema dump: echopai schema export # AI agents pipe this for command surface\n" +
|
|
58
60
|
"Profile: echopai config profile add prod --base-url https://api.echopai.com\n");
|
|
@@ -100,7 +102,7 @@ function applyAiFirstHooks(cmd) {
|
|
|
100
102
|
//
|
|
101
103
|
// Phase 3.7+ — `raw` is now the only place to find spec-driven operations.
|
|
102
104
|
// Top-level previously held spec mirrors but starting this PR the curated
|
|
103
|
-
// verbs (`views` / `news` / `quote` / `sentiment` / `
|
|
105
|
+
// verbs (`views` / `news` / `quote` / `sentiment` / `hot` /
|
|
104
106
|
// `lookup` / `digest` etc.) take precedence at the top level. Use `echopai raw <noun>
|
|
105
107
|
// <verb>` for the raw mirror (e.g. `raw views feed`, `raw quote`, etc.).
|
|
106
108
|
const rawCmd = new Command("raw").description("Raw 1:1 mirror of OpenAPI operations (escape hatch for power users / scripts)");
|
|
@@ -108,7 +110,7 @@ buildCommandTree(rawCmd, dispatch);
|
|
|
108
110
|
rawCmd.addCommand(buildRawCallCommand());
|
|
109
111
|
program.addCommand(rawCmd);
|
|
110
112
|
// Top-level spec commands kept ONLY for nouns that don't conflict with a
|
|
111
|
-
// curated verb. Curated-verb nouns (views/news/quote/sentiment/
|
|
113
|
+
// curated verb. Curated-verb nouns (views/news/quote/sentiment/hot
|
|
112
114
|
// /lookup/digest) are NOT generated at top level — they go below in their canonical
|
|
113
115
|
// curated form. Use `echopai raw <noun>` for the spec mirror.
|
|
114
116
|
const CURATED_NOUNS = new Set([
|
|
@@ -116,10 +118,11 @@ const CURATED_NOUNS = new Set([
|
|
|
116
118
|
"news",
|
|
117
119
|
"quote",
|
|
118
120
|
"sentiment",
|
|
119
|
-
"research",
|
|
120
121
|
"hot",
|
|
121
122
|
"lookup",
|
|
122
123
|
"digest",
|
|
124
|
+
"search",
|
|
125
|
+
"financials",
|
|
123
126
|
]);
|
|
124
127
|
const topLevelSpecDispatch = async (op, args) => {
|
|
125
128
|
await dispatch(op, args);
|
|
@@ -134,18 +137,19 @@ for (const sub of topLevelStub.commands) {
|
|
|
134
137
|
// Curated agent verbs (Phase 3.6+): task-level entries that wrap one or
|
|
135
138
|
// more raw operations + map output for AI agents. Listed before tools so
|
|
136
139
|
// `--help` shows them first.
|
|
137
|
-
// Product priority (see feedback_views_over_news.md): views >
|
|
140
|
+
// Product priority (see feedback_views_over_news.md): views > news
|
|
138
141
|
program.addCommand(buildLookupCommand());
|
|
139
142
|
program.addCommand(buildDigestCommand()); // one-shot fan-out (Phase 5.1)
|
|
143
|
+
program.addCommand(buildSearchCommand()); // hybrid semantic discovery (PLAN_SEARCH_SEMANTIC_UPGRADE)
|
|
140
144
|
program.addCommand(buildQuoteCommand());
|
|
141
145
|
program.addCommand(buildViewsCommand()); // PRIMARY research source
|
|
142
|
-
program.addCommand(buildResearchCommand()); // views quality signal layer
|
|
143
146
|
program.addCommand(buildNewsCommand()); // supplementary breadth
|
|
144
147
|
program.addCommand(buildSentimentCommand());
|
|
145
148
|
program.addCommand(buildHotCommand());
|
|
146
149
|
program.addCommand(buildChartCommand()); // single-code K-line
|
|
147
150
|
program.addCommand(buildBarsBatchCommand()); // multi-code batch K-line (PR 3.2/3.3 server)
|
|
148
151
|
program.addCommand(buildScanCommand()); // full-market scan (PR 3.4 server)
|
|
152
|
+
program.addCommand(buildFinancialsCommand()); // fundamentals: quote-snapshot / pit / reports / series
|
|
149
153
|
// hand-curated tools
|
|
150
154
|
program.addCommand(buildLoginCommand());
|
|
151
155
|
program.addCommand(buildLogoutCommand());
|
|
@@ -158,7 +162,14 @@ program.addCommand(buildWhoamiCommand());
|
|
|
158
162
|
program.addCommand(buildDoctorCommand());
|
|
159
163
|
program.addCommand(buildMcpCommand());
|
|
160
164
|
program.addCommand(buildCompletionCommand());
|
|
165
|
+
program.addCommand(buildWelcomeCommand());
|
|
161
166
|
applyAiFirstHooks(program);
|
|
167
|
+
// Bare `echopai` in a TTY → onboarding screen. Non-TTY / CI / piped output
|
|
168
|
+
// falls through to commander's plain help (preserves AI-first invariant).
|
|
169
|
+
if (shouldShowWelcome(process.argv)) {
|
|
170
|
+
printWelcome();
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
162
173
|
program.parseAsync(process.argv).catch((e) => {
|
|
163
174
|
process.stderr.write(JSON.stringify({
|
|
164
175
|
error: {
|
|
@@ -0,0 +1,190 @@
|
|
|
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 { CLI_VERSION } from "../version.js";
|
|
18
|
+
function maskKey(key) {
|
|
19
|
+
// eps_live_<lookup>_<secret> → keep prefix + first 4 of lookup + masked tail.
|
|
20
|
+
if (key.length <= 16)
|
|
21
|
+
return "***";
|
|
22
|
+
return key.slice(0, 13) + "***";
|
|
23
|
+
}
|
|
24
|
+
function snapshotAuth() {
|
|
25
|
+
try {
|
|
26
|
+
const creds = resolveCredentials({});
|
|
27
|
+
if (creds.profile) {
|
|
28
|
+
return {
|
|
29
|
+
state: "profile",
|
|
30
|
+
profile: creds.profile,
|
|
31
|
+
keyHint: maskKey(creds.key),
|
|
32
|
+
baseUrl: creds.baseUrl,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { state: "env", keyHint: maskKey(creds.key), baseUrl: creds.baseUrl };
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
if (e instanceof AuthMissingError)
|
|
39
|
+
return { state: "missing" };
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Right-pad a string to width `w`, ignoring ANSI escapes for length. */
|
|
44
|
+
function padRight(s, w) {
|
|
45
|
+
// strip ANSI for length measurement
|
|
46
|
+
// eslint-disable-next-line no-control-regex
|
|
47
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
48
|
+
const pad = Math.max(0, w - visible.length);
|
|
49
|
+
return s + " ".repeat(pad);
|
|
50
|
+
}
|
|
51
|
+
const COMMAND_GROUPS = [
|
|
52
|
+
{
|
|
53
|
+
title: "🔍 快速检索",
|
|
54
|
+
items: [
|
|
55
|
+
{ cmd: "echopai lookup --text 茅台", note: "中文名 / 拼音首字母 → canonical_code" },
|
|
56
|
+
{ cmd: "echopai digest --code SSE:600519", note: "一键研究摘要(5 桶 fan-out,容忍部分失败)" },
|
|
57
|
+
{ cmd: "echopai search --query \"AI 算力\"", note: "题材 / 概念语义搜索" },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: "📈 实时行情",
|
|
62
|
+
items: [
|
|
63
|
+
{ cmd: "echopai quote --codes \"SSE:600519,SZSE:000001\"", note: "1-200 只实时报价" },
|
|
64
|
+
{ cmd: "echopai sentiment", note: "市场情绪总览(涨跌停 / 宽度 / 分化)" },
|
|
65
|
+
{ cmd: "echopai hot", note: "东方财富热门股榜(综合搜索 / 关注 / 评论)" },
|
|
66
|
+
{ cmd: "echopai scan", note: "全市场快照(约 5800 只)" },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
title: "💰 基本面 / 估值",
|
|
71
|
+
items: [
|
|
72
|
+
{ cmd: "echopai financials quote-snapshot --code SSE:600519", note: "估值快照 14 字段(PE/PB/PS/换手率/股息率/量比)★头牌★" },
|
|
73
|
+
{ cmd: "echopai financials pit --code SSE:600519 --date 2024-11-01", note: "Point-in-time 财务指标(回测防穿越)" },
|
|
74
|
+
{ cmd: "echopai financials reports --code SSE:600519 --kind annual", note: "最近 N 期财务报告(~25 字段/期)" },
|
|
75
|
+
{ cmd: "echopai financials series --code SSE:600519 --metric roe_simple", note: "单指标历史时间序列" },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: "📰 研究 / 资讯",
|
|
80
|
+
items: [
|
|
81
|
+
{ cmd: "echopai views --code SSE:600519", note: "★主源★ 卖方研报 / 分析师观点(含 research_entity_id 归因)" },
|
|
82
|
+
{ cmd: "echopai news search --query AI --since-hours 24", note: "辅源新闻 / 简讯(短时窗)" },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
title: "📊 K 线",
|
|
87
|
+
items: [
|
|
88
|
+
{ cmd: "echopai chart --code SSE:600519 --from 2026-01-01 --to 2026-05-01", note: "单只 K 线(日线 / 分钟线)" },
|
|
89
|
+
{ cmd: "echopai bars-batch --codes A,B,C --from ... --to ...", note: "批量 K 线(≤100 只)" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
title: "🤖 AI 集成",
|
|
94
|
+
items: [
|
|
95
|
+
{ cmd: "echopai mcp serve", note: "启动 MCP stdio 服务(Claude Desktop / Cursor / Claude Code)" },
|
|
96
|
+
{ cmd: "echopai schema export", note: "导出全部 operation schema 给 agent 喂养" },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
const POINTERS = [
|
|
101
|
+
{ cmd: "echopai --help", note: "完整命令树" },
|
|
102
|
+
{ cmd: "echopai <verb> --help", note: "查看单个命令的参数和示例" },
|
|
103
|
+
{ cmd: "echopai whoami", note: "在线检查 token 能力 / scopes" },
|
|
104
|
+
{ cmd: "echopai doctor", note: "诊断鉴权 / 网络连通性" },
|
|
105
|
+
{ cmd: "echopai status", note: "查看本地 profile + 鉴权状态" },
|
|
106
|
+
];
|
|
107
|
+
const BANNER_PLAIN = [
|
|
108
|
+
" ███████ ██████ ██ ██ ██████ ██████ █████ ██",
|
|
109
|
+
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██",
|
|
110
|
+
" █████ ██ ███████ ██ ██ ██████ ███████ ██",
|
|
111
|
+
" ██ ██ ██ ██ ██ ██ ██ ██ ██ ██",
|
|
112
|
+
" ███████ ██████ ██ ██ ██████ ██ ██ ██ ██",
|
|
113
|
+
];
|
|
114
|
+
export function renderWelcome(now = () => new Date()) {
|
|
115
|
+
const lines = [];
|
|
116
|
+
const auth = snapshotAuth();
|
|
117
|
+
// Banner
|
|
118
|
+
lines.push("");
|
|
119
|
+
for (const row of BANNER_PLAIN)
|
|
120
|
+
lines.push(cyan(row));
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(` ${bold("EchoPai CLI")} ${dim("v" + CLI_VERSION)} ${dim("·")} ${dim("面向 AI Agent 的 A 股数据接入终端")}`);
|
|
123
|
+
lines.push("");
|
|
124
|
+
// Auth status
|
|
125
|
+
if (auth.state === "missing") {
|
|
126
|
+
lines.push(` ${yellow("●")} ${bold("未登录")} ${dim("—")} 运行 ${cyan("`echopai login --key eps_live_<lookup>_<secret>`")} 或设置环境变量 ${cyan("ECHOPAI_KEY")}`);
|
|
127
|
+
}
|
|
128
|
+
else if (auth.state === "env") {
|
|
129
|
+
lines.push(` ${green("●")} ${bold("已登录")}(环境变量 ${cyan("ECHOPAI_KEY")})${dim(`(${auth.keyHint})`)} → ${dim(auth.baseUrl)}`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
lines.push(` ${green("●")} ${bold("已登录")} profile ${cyan(auth.profile)} ${dim(`(${auth.keyHint})`)} → ${dim(auth.baseUrl)}`);
|
|
133
|
+
}
|
|
134
|
+
lines.push("");
|
|
135
|
+
// Command groups
|
|
136
|
+
lines.push(` ${dim("── 常用命令 ─────────────────────────────────────────────────")}`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
// Reserve column 1 for cmd, column 2 for note. Compute padding from the
|
|
139
|
+
// widest cmd across all groups to keep alignment stable.
|
|
140
|
+
const widest = Math.min(72, COMMAND_GROUPS.flatMap((g) => g.items)
|
|
141
|
+
.map((it) => it.cmd.length)
|
|
142
|
+
.reduce((a, b) => Math.max(a, b), 0));
|
|
143
|
+
for (const group of COMMAND_GROUPS) {
|
|
144
|
+
lines.push(` ${bold(group.title)}`);
|
|
145
|
+
for (const it of group.items) {
|
|
146
|
+
const cmd = cyan(it.cmd);
|
|
147
|
+
lines.push(` ${padRight(cmd, widest + 4)} ${dim(it.note)}`);
|
|
148
|
+
}
|
|
149
|
+
lines.push("");
|
|
150
|
+
}
|
|
151
|
+
// Pointers
|
|
152
|
+
lines.push(` ${dim("── 更多 ─────────────────────────────────────────────────────")}`);
|
|
153
|
+
lines.push("");
|
|
154
|
+
for (const it of POINTERS) {
|
|
155
|
+
lines.push(` ${padRight(cyan(it.cmd), widest + 4)} ${dim(it.note)}`);
|
|
156
|
+
}
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(` ${dim("文档: https://docs.echopai.com · 反馈: https://github.com/evanzhangx/EchoPulse/issues")}`);
|
|
159
|
+
lines.push("");
|
|
160
|
+
// Timestamp footer so users know this isn't a stale cache
|
|
161
|
+
const ts = now().toISOString().replace("T", " ").slice(0, 19);
|
|
162
|
+
lines.push(` ${dim("生成于 " + ts + " UTC · Ctrl+C 退出")}`);
|
|
163
|
+
lines.push("");
|
|
164
|
+
return lines.join("\n");
|
|
165
|
+
}
|
|
166
|
+
/** Print welcome screen to stdout (no exit — caller decides). */
|
|
167
|
+
export function printWelcome() {
|
|
168
|
+
process.stdout.write(renderWelcome());
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Decide whether bare `echopai` (no args, no flags) should show the welcome
|
|
172
|
+
* screen. False for AI / CI / piped output — they get commander's plain help.
|
|
173
|
+
*/
|
|
174
|
+
export function shouldShowWelcome(argv) {
|
|
175
|
+
// node + script + (no extra args)
|
|
176
|
+
if (argv.length !== 2)
|
|
177
|
+
return false;
|
|
178
|
+
if (!isTtyHuman)
|
|
179
|
+
return false;
|
|
180
|
+
if (process.env.CI)
|
|
181
|
+
return false;
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
export function buildWelcomeCommand() {
|
|
185
|
+
return new Command("welcome")
|
|
186
|
+
.description("显示欢迎屏(banner + 常用命令 + 鉴权状态)")
|
|
187
|
+
.action(() => {
|
|
188
|
+
printWelcome();
|
|
189
|
+
});
|
|
190
|
+
}
|
package/dist/verbs/digest.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Killer "one-shot research" verb:
|
|
5
5
|
*
|
|
6
|
-
* one canonical_code → views +
|
|
6
|
+
* one canonical_code → views + quote + snapshot + sentiment + news
|
|
7
7
|
*
|
|
8
8
|
* Plan-doc §3.3 makes this the "agent's opening move": one call returns the
|
|
9
9
|
* full picture of a security. Buckets stay separated (no ranking imposed) so
|
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
*
|
|
25
25
|
* Why these 5 ops in the fallback:
|
|
26
26
|
* - views.recent : PRIMARY research signal (analyst opinions on this code)
|
|
27
|
-
* - research.entity-performance-list : quality layer for view authors
|
|
28
27
|
* - quote : current price / change %
|
|
28
|
+
* - financials.quote-snapshot : 14-field valuation (PE/PB/PS/换手/股息/量比) on this code
|
|
29
29
|
* - sentiment.overview : market regime context (not per-code; intentional —
|
|
30
30
|
* no per-code sentiment op exists)
|
|
31
|
-
* - news.
|
|
31
|
+
* - news.list : SUPPLEMENTARY breadth (canonical_code-exact news,
|
|
32
|
+
* via news_item_security mapping table — NOT keyword search,
|
|
33
|
+
* since article text never contains 'SSE:600519' literally)
|
|
32
34
|
*
|
|
33
35
|
* Windows differ on purpose: views=168h (7d), news=24h. Encodes the rule
|
|
34
36
|
* that research time-horizon > news time-horizon.
|
|
@@ -40,11 +42,11 @@ import { CallApiError } from "../runtime/errors.js";
|
|
|
40
42
|
import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
|
|
41
43
|
import { callOp } from "../runtime/verb_runner.js";
|
|
42
44
|
/** Buckets in the order the description establishes (views first, news last). */
|
|
43
|
-
const BUCKETS = ["views", "
|
|
45
|
+
const BUCKETS = ["views", "quote", "snapshot", "sentiment", "news"];
|
|
44
46
|
const CODE_REGEX = /^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/;
|
|
45
47
|
export const digestSpec = {
|
|
46
48
|
name: "digest",
|
|
47
|
-
description: "One-shot research digest for a single security: fan-out across views (PRIMARY),
|
|
49
|
+
description: "One-shot research digest for a single security: fan-out across views (PRIMARY), quote, 14-field valuation snapshot (PE/PB/PS/turnover/dividend/volume-ratio), market sentiment, and supplementary news. Returns separated buckets — does NOT rank views vs news for the agent. Partial failures surface in meta.partial_failures[] without poisoning successful buckets. Use as the agent's first call when asked 'what's going on with <stock>'.",
|
|
48
50
|
inputSchema: {
|
|
49
51
|
code: z
|
|
50
52
|
.string()
|
|
@@ -70,7 +72,7 @@ export const digestSpec = {
|
|
|
70
72
|
.min(1)
|
|
71
73
|
.max(50)
|
|
72
74
|
.default(10)
|
|
73
|
-
.describe("Per-bucket item cap (views/news). Quote/sentiment
|
|
75
|
+
.describe("Per-bucket item cap (views/news). Quote/snapshot/sentiment aren't paginated here."),
|
|
74
76
|
},
|
|
75
77
|
handler: async (args, ctx) => runDigest(args, ctx),
|
|
76
78
|
// Backing ops are listed in two groups: digest.get is the primary
|
|
@@ -84,10 +86,10 @@ export const digestSpec = {
|
|
|
84
86
|
backingOps: [
|
|
85
87
|
"digest.get",
|
|
86
88
|
"views.recent",
|
|
87
|
-
"research.entity-performance-list",
|
|
88
89
|
"quote",
|
|
90
|
+
"financials.quote-snapshot",
|
|
89
91
|
"sentiment.overview",
|
|
90
|
-
"news.
|
|
92
|
+
"news.list",
|
|
91
93
|
],
|
|
92
94
|
};
|
|
93
95
|
/**
|
|
@@ -153,10 +155,10 @@ function shouldFallbackToFanout(e) {
|
|
|
153
155
|
function buildBucketCalls(args, ctx) {
|
|
154
156
|
const opsRequired = {
|
|
155
157
|
views: "views.recent",
|
|
156
|
-
research: "research.entity-performance-list",
|
|
157
158
|
quote: "quote",
|
|
159
|
+
snapshot: "financials.quote-snapshot",
|
|
158
160
|
sentiment: "sentiment.overview",
|
|
159
|
-
news: "news.
|
|
161
|
+
news: "news.list",
|
|
160
162
|
};
|
|
161
163
|
const calls = [];
|
|
162
164
|
for (const bucket of BUCKETS) {
|
|
@@ -184,18 +186,18 @@ function buildBucketCalls(args, ctx) {
|
|
|
184
186
|
limit: args.limit_per_bucket,
|
|
185
187
|
};
|
|
186
188
|
break;
|
|
187
|
-
case "research":
|
|
188
|
-
opArgs = {};
|
|
189
|
-
break;
|
|
190
189
|
case "quote":
|
|
191
190
|
opArgs = { codes: [args.code] };
|
|
192
191
|
break;
|
|
192
|
+
case "snapshot":
|
|
193
|
+
opArgs = { code: args.code };
|
|
194
|
+
break;
|
|
193
195
|
case "sentiment":
|
|
194
196
|
opArgs = { scope: "all_a_ex_st" };
|
|
195
197
|
break;
|
|
196
198
|
case "news":
|
|
197
199
|
opArgs = {
|
|
198
|
-
|
|
200
|
+
security: args.code,
|
|
199
201
|
since_hours: args.news_hours,
|
|
200
202
|
limit: args.limit_per_bucket,
|
|
201
203
|
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `echopai financials ...` + MCP tools `financials_quote_snapshot` / `financials_pit`
|
|
3
|
+
* / `financials_reports` / `financials_series`.
|
|
4
|
+
*
|
|
5
|
+
* Headline endpoint is **quote-snapshot** — one-call 14-field valuation snapshot
|
|
6
|
+
* (PE / PB / PS / 换手率 / 股息率 / 量比 等), TDX + Sina 自算口径,对照过 Tushare。
|
|
7
|
+
*
|
|
8
|
+
* Three sibling endpoints surface deeper financials access for backtests /
|
|
9
|
+
* fundamentals research:
|
|
10
|
+
* - pit — point-in-time indicators at a given trade_date (anti-future-fn)
|
|
11
|
+
* - reports — last N report-period snapshots (~25 fields each)
|
|
12
|
+
* - series — single-metric time series across periods
|
|
13
|
+
*
|
|
14
|
+
* MCP registers four discrete tools so an agent can pick the right granularity;
|
|
15
|
+
* CLI surfaces a single `financials` noun with four sub-commands (preserves
|
|
16
|
+
* the spec-driven shape from openapi.yaml).
|
|
17
|
+
*/
|
|
18
|
+
import { Command, Option } from "commander";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { OPERATIONS } from "../_generated/operations.js";
|
|
21
|
+
import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
|
|
22
|
+
import { callOp } from "../runtime/verb_runner.js";
|
|
23
|
+
const CODE_RE = /^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/;
|
|
24
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
25
|
+
const REPORT_KINDS = ["Q1", "H1", "Q3", "annual", "preliminary"];
|
|
26
|
+
export const financialsQuoteSnapshotSpec = {
|
|
27
|
+
name: "financials_quote_snapshot",
|
|
28
|
+
description: "One-call 14-field valuation snapshot for one A-share: PE / PE-TTM / PB / PS / PS-TTM, total/float/free share, total/circ market cap, turnover rate (raw & float), volume ratio, dividend yield (last-year & TTM). Computed from TDX + Sina real-time (no Tushare dependency), validated against Tushare daily_basic. Use `date` to fetch a historical snapshot; omit for real-time.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
code: z
|
|
31
|
+
.string()
|
|
32
|
+
.regex(CODE_RE)
|
|
33
|
+
.describe("Canonical A-share code (e.g. SSE:600519)"),
|
|
34
|
+
date: z
|
|
35
|
+
.string()
|
|
36
|
+
.regex(DATE_RE)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Trade date YYYY-MM-DD; omit for real-time snapshot"),
|
|
39
|
+
},
|
|
40
|
+
handler: async (args, ctx) => {
|
|
41
|
+
const op = OPERATIONS["financials.quote-snapshot"];
|
|
42
|
+
if (!op)
|
|
43
|
+
throw new Error("financials.quote-snapshot op missing from codegen");
|
|
44
|
+
const callArgs = { code: args.code };
|
|
45
|
+
if (args.date)
|
|
46
|
+
callArgs.date = args.date;
|
|
47
|
+
return callOp(op, callArgs, ctx);
|
|
48
|
+
},
|
|
49
|
+
backingOps: ["financials.quote-snapshot"],
|
|
50
|
+
};
|
|
51
|
+
export const financialsPitSpec = {
|
|
52
|
+
name: "financials_pit",
|
|
53
|
+
description: "Point-in-time financial indicators for one A-share at a given trade_date. Returns the latest report visible AS OF that date (announce_date ≤ date; conservative 90-day fallback when announce_date is missing). Designed for backtests / AI agents to avoid look-ahead bias. ~25 fields incl. EPS / BPS / ROE / margins / revenue / net-income / equity.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
code: z.string().regex(CODE_RE).describe("Canonical A-share code"),
|
|
56
|
+
date: z
|
|
57
|
+
.string()
|
|
58
|
+
.regex(DATE_RE)
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Trade date YYYY-MM-DD; defaults to today"),
|
|
61
|
+
},
|
|
62
|
+
handler: async (args, ctx) => {
|
|
63
|
+
const op = OPERATIONS["financials.pit"];
|
|
64
|
+
if (!op)
|
|
65
|
+
throw new Error("financials.pit op missing from codegen");
|
|
66
|
+
const callArgs = { code: args.code };
|
|
67
|
+
if (args.date)
|
|
68
|
+
callArgs.date = args.date;
|
|
69
|
+
return callOp(op, callArgs, ctx);
|
|
70
|
+
},
|
|
71
|
+
backingOps: ["financials.pit"],
|
|
72
|
+
};
|
|
73
|
+
export const financialsReportsSpec = {
|
|
74
|
+
name: "financials_reports",
|
|
75
|
+
description: "Last N report-period snapshots for one A-share (~25 fields per period). Each item carries `announce_date` for visibility timing. Filter by `kind` to scope to Q1 / H1 / Q3 / annual / preliminary.",
|
|
76
|
+
inputSchema: {
|
|
77
|
+
code: z.string().regex(CODE_RE).describe("Canonical A-share code"),
|
|
78
|
+
limit: z
|
|
79
|
+
.number()
|
|
80
|
+
.int()
|
|
81
|
+
.min(1)
|
|
82
|
+
.max(100)
|
|
83
|
+
.default(12)
|
|
84
|
+
.describe("Max periods (1-100, default 12)"),
|
|
85
|
+
kind: z.enum(REPORT_KINDS).optional().describe("Filter by report kind"),
|
|
86
|
+
},
|
|
87
|
+
handler: async (args, ctx) => {
|
|
88
|
+
const op = OPERATIONS["financials.reports"];
|
|
89
|
+
if (!op)
|
|
90
|
+
throw new Error("financials.reports op missing from codegen");
|
|
91
|
+
const callArgs = { code: args.code, limit: args.limit };
|
|
92
|
+
if (args.kind)
|
|
93
|
+
callArgs.kind = args.kind;
|
|
94
|
+
return callOp(op, callArgs, ctx);
|
|
95
|
+
},
|
|
96
|
+
backingOps: ["financials.reports"],
|
|
97
|
+
};
|
|
98
|
+
export const financialsSeriesSpec = {
|
|
99
|
+
name: "financials_series",
|
|
100
|
+
description: "Time series of a single financial metric for one A-share across reporting periods. `metric` is an indicators-table field name (roe_simple / revenue / ni_parent / debt_asset_ratio / gross_margin / eps_basic, ~150 supported).",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
code: z.string().regex(CODE_RE).describe("Canonical A-share code"),
|
|
103
|
+
metric: z
|
|
104
|
+
.string()
|
|
105
|
+
.min(1)
|
|
106
|
+
.max(64)
|
|
107
|
+
.describe("Indicator field name (e.g. roe_simple, revenue, ni_parent)"),
|
|
108
|
+
from: z.string().regex(DATE_RE).optional().describe("Inclusive earliest report_date"),
|
|
109
|
+
to: z.string().regex(DATE_RE).optional().describe("Inclusive latest report_date"),
|
|
110
|
+
limit: z
|
|
111
|
+
.number()
|
|
112
|
+
.int()
|
|
113
|
+
.min(1)
|
|
114
|
+
.max(200)
|
|
115
|
+
.default(40)
|
|
116
|
+
.describe("Max points (1-200, default 40)"),
|
|
117
|
+
},
|
|
118
|
+
handler: async (args, ctx) => {
|
|
119
|
+
const op = OPERATIONS["financials.series"];
|
|
120
|
+
if (!op)
|
|
121
|
+
throw new Error("financials.series op missing from codegen");
|
|
122
|
+
const callArgs = {
|
|
123
|
+
code: args.code,
|
|
124
|
+
metric: args.metric,
|
|
125
|
+
limit: args.limit,
|
|
126
|
+
};
|
|
127
|
+
if (args.from)
|
|
128
|
+
callArgs.from = args.from;
|
|
129
|
+
if (args.to)
|
|
130
|
+
callArgs.to = args.to;
|
|
131
|
+
return callOp(op, callArgs, ctx);
|
|
132
|
+
},
|
|
133
|
+
backingOps: ["financials.series"],
|
|
134
|
+
};
|
|
135
|
+
function clampInt(raw, min, max, fallback) {
|
|
136
|
+
const n = Math.floor(Number(raw));
|
|
137
|
+
if (!Number.isFinite(n))
|
|
138
|
+
return fallback;
|
|
139
|
+
if (n < min)
|
|
140
|
+
return min;
|
|
141
|
+
if (n > max)
|
|
142
|
+
return max;
|
|
143
|
+
return n;
|
|
144
|
+
}
|
|
145
|
+
export function buildFinancialsCommand() {
|
|
146
|
+
const cmd = new Command("financials").description("Fundamentals — valuation snapshot, point-in-time indicators, recent reports, single-metric time series.");
|
|
147
|
+
const qs = cmd
|
|
148
|
+
.command("quote-snapshot")
|
|
149
|
+
.description(financialsQuoteSnapshotSpec.description);
|
|
150
|
+
qs.addOption(new Option("--code <canonical_code>", "Canonical A-share code (e.g. SSE:600519)")
|
|
151
|
+
.makeOptionMandatory(true));
|
|
152
|
+
qs.addOption(new Option("--date <YYYY-MM-DD>", "Trade date; omit for real-time"));
|
|
153
|
+
qs.action(async (opts) => {
|
|
154
|
+
if (!OPERATIONS["financials.quote-snapshot"]) {
|
|
155
|
+
emitVerbError("internal_error", "financials.quote-snapshot missing", undefined, 2);
|
|
156
|
+
}
|
|
157
|
+
const args = { code: opts.code };
|
|
158
|
+
if (opts.date)
|
|
159
|
+
args.date = opts.date;
|
|
160
|
+
await executeVerb(async (ctx) => financialsQuoteSnapshotSpec.handler(args, ctx));
|
|
161
|
+
});
|
|
162
|
+
const pit = cmd.command("pit").description(financialsPitSpec.description);
|
|
163
|
+
pit.addOption(new Option("--code <canonical_code>", "Canonical A-share code").makeOptionMandatory(true));
|
|
164
|
+
pit.addOption(new Option("--date <YYYY-MM-DD>", "Trade date; defaults to today"));
|
|
165
|
+
pit.action(async (opts) => {
|
|
166
|
+
if (!OPERATIONS["financials.pit"]) {
|
|
167
|
+
emitVerbError("internal_error", "financials.pit missing", undefined, 2);
|
|
168
|
+
}
|
|
169
|
+
const args = { code: opts.code };
|
|
170
|
+
if (opts.date)
|
|
171
|
+
args.date = opts.date;
|
|
172
|
+
await executeVerb(async (ctx) => financialsPitSpec.handler(args, ctx));
|
|
173
|
+
});
|
|
174
|
+
const reports = cmd.command("reports").description(financialsReportsSpec.description);
|
|
175
|
+
reports.addOption(new Option("--code <canonical_code>", "Canonical A-share code").makeOptionMandatory(true));
|
|
176
|
+
reports.addOption(new Option("--limit <n>", "Max periods (1-100)").default("12"));
|
|
177
|
+
reports.addOption(new Option("--kind <kind>", "Filter by report kind").choices([...REPORT_KINDS]));
|
|
178
|
+
reports.action(async (opts) => {
|
|
179
|
+
if (!OPERATIONS["financials.reports"]) {
|
|
180
|
+
emitVerbError("internal_error", "financials.reports missing", undefined, 2);
|
|
181
|
+
}
|
|
182
|
+
const args = {
|
|
183
|
+
code: opts.code,
|
|
184
|
+
limit: clampInt(opts.limit, 1, 100, 12),
|
|
185
|
+
};
|
|
186
|
+
if (opts.kind)
|
|
187
|
+
args.kind = opts.kind;
|
|
188
|
+
await executeVerb(async (ctx) => financialsReportsSpec.handler(args, ctx));
|
|
189
|
+
});
|
|
190
|
+
const series = cmd.command("series").description(financialsSeriesSpec.description);
|
|
191
|
+
series.addOption(new Option("--code <canonical_code>", "Canonical A-share code").makeOptionMandatory(true));
|
|
192
|
+
series.addOption(new Option("--metric <name>", "Indicator field name (e.g. roe_simple)").makeOptionMandatory(true));
|
|
193
|
+
series.addOption(new Option("--from <YYYY-MM-DD>", "Inclusive earliest report_date"));
|
|
194
|
+
series.addOption(new Option("--to <YYYY-MM-DD>", "Inclusive latest report_date"));
|
|
195
|
+
series.addOption(new Option("--limit <n>", "Max points (1-200)").default("40"));
|
|
196
|
+
series.action(async (opts) => {
|
|
197
|
+
if (!OPERATIONS["financials.series"]) {
|
|
198
|
+
emitVerbError("internal_error", "financials.series missing", undefined, 2);
|
|
199
|
+
}
|
|
200
|
+
const args = {
|
|
201
|
+
code: opts.code,
|
|
202
|
+
metric: opts.metric,
|
|
203
|
+
limit: clampInt(opts.limit, 1, 200, 40),
|
|
204
|
+
};
|
|
205
|
+
if (opts.from)
|
|
206
|
+
args.from = opts.from;
|
|
207
|
+
if (opts.to)
|
|
208
|
+
args.to = opts.to;
|
|
209
|
+
await executeVerb(async (ctx) => financialsSeriesSpec.handler(args, ctx));
|
|
210
|
+
});
|
|
211
|
+
return cmd;
|
|
212
|
+
}
|
package/dist/verbs/index.js
CHANGED
|
@@ -8,42 +8,50 @@
|
|
|
8
8
|
export { barsBatchSpec, buildBarsBatchCommand } from "./bars_batch.js";
|
|
9
9
|
export { chartSpec, buildChartCommand } from "./chart.js";
|
|
10
10
|
export { digestSpec, buildDigestCommand } from "./digest.js";
|
|
11
|
+
export { financialsQuoteSnapshotSpec, financialsPitSpec, financialsReportsSpec, financialsSeriesSpec, buildFinancialsCommand, } from "./financials.js";
|
|
11
12
|
export { hotSpec, buildHotCommand } from "./hot.js";
|
|
12
13
|
export { lookupSpec, buildLookupCommand } from "./lookup.js";
|
|
13
14
|
export { newsSpec, buildNewsCommand } from "./news.js";
|
|
14
15
|
export { quoteSpec, buildQuoteCommand } from "./quote.js";
|
|
15
|
-
export { researchSpec, buildResearchCommand } from "./research.js";
|
|
16
16
|
export { scanSpec, buildScanCommand } from "./scan.js";
|
|
17
|
+
export { searchSpec, buildSearchCommand } from "./search.js";
|
|
17
18
|
export { sentimentSpec, buildSentimentCommand } from "./sentiment.js";
|
|
18
19
|
export { viewsSpec, buildViewsCommand } from "./views.js";
|
|
19
20
|
import { barsBatchSpec } from "./bars_batch.js";
|
|
20
21
|
import { chartSpec } from "./chart.js";
|
|
21
22
|
import { digestSpec } from "./digest.js";
|
|
23
|
+
import { financialsQuoteSnapshotSpec, financialsPitSpec, financialsReportsSpec, financialsSeriesSpec, } from "./financials.js";
|
|
22
24
|
import { hotSpec } from "./hot.js";
|
|
23
25
|
import { lookupSpec } from "./lookup.js";
|
|
24
26
|
import { newsSpec } from "./news.js";
|
|
25
27
|
import { quoteSpec } from "./quote.js";
|
|
26
|
-
import { researchSpec } from "./research.js";
|
|
27
28
|
import { scanSpec } from "./scan.js";
|
|
29
|
+
import { searchSpec } from "./search.js";
|
|
28
30
|
import { sentimentSpec } from "./sentiment.js";
|
|
29
31
|
import { viewsSpec } from "./views.js";
|
|
30
32
|
/**
|
|
31
33
|
* Ordering follows product priority (feedback_views_over_news.md):
|
|
32
34
|
* lookup (universal first step) → digest (one-shot fan-out, agent's opening
|
|
33
|
-
* move once it has a canonical_code) →
|
|
34
|
-
*
|
|
35
|
-
* scan
|
|
35
|
+
* move once it has a canonical_code) → search (hybrid semantic discovery
|
|
36
|
+
* for themes / concepts) → quote → views (PRIMARY research) → news
|
|
37
|
+
* (supplementary) → sentiment → hot → chart → bars_batch → scan →
|
|
38
|
+
* financials (valuation snapshot is the headline; pit / reports / series
|
|
39
|
+
* cover deeper fundamentals access).
|
|
36
40
|
*/
|
|
37
41
|
export const ALL_VERB_SPECS = [
|
|
38
42
|
lookupSpec,
|
|
39
43
|
digestSpec,
|
|
44
|
+
searchSpec,
|
|
40
45
|
quoteSpec,
|
|
41
46
|
viewsSpec,
|
|
42
|
-
researchSpec,
|
|
43
47
|
newsSpec,
|
|
44
48
|
sentimentSpec,
|
|
45
49
|
hotSpec,
|
|
46
50
|
chartSpec,
|
|
47
51
|
barsBatchSpec,
|
|
48
52
|
scanSpec,
|
|
53
|
+
financialsQuoteSnapshotSpec,
|
|
54
|
+
financialsPitSpec,
|
|
55
|
+
financialsReportsSpec,
|
|
56
|
+
financialsSeriesSpec,
|
|
49
57
|
];
|