echopai 2.3.0 → 2.4.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 +63 -348
- package/dist/bin.js +8298 -190
- package/package.json +11 -13
- package/dist/_generated/commands.js +0 -378
- package/dist/_generated/help.js +0 -295
- package/dist/_generated/operations.js +0 -2385
- package/dist/runtime/auth.js +0 -95
- package/dist/runtime/envelope.js +0 -52
- package/dist/runtime/errors.js +0 -186
- package/dist/runtime/filters.js +0 -153
- package/dist/runtime/format.js +0 -143
- package/dist/runtime/http.js +0 -65
- package/dist/runtime/idempotency.js +0 -18
- package/dist/runtime/invoker.js +0 -391
- package/dist/runtime/io.js +0 -16
- package/dist/runtime/paginator.js +0 -146
- package/dist/runtime/trace.js +0 -99
- package/dist/runtime/tty.js +0 -51
- package/dist/runtime/update_check.js +0 -120
- package/dist/runtime/update_worker.js +0 -63
- package/dist/runtime/verb_cmd.js +0 -72
- package/dist/runtime/verb_runner.js +0 -152
- package/dist/runtime/whoami_cache.js +0 -109
- package/dist/tools/api.js +0 -81
- package/dist/tools/completion.js +0 -116
- package/dist/tools/config.js +0 -123
- package/dist/tools/doctor.js +0 -183
- package/dist/tools/login.js +0 -99
- package/dist/tools/mcp.js +0 -141
- package/dist/tools/raw.js +0 -96
- package/dist/tools/schema.js +0 -58
- package/dist/tools/trace.js +0 -54
- package/dist/tools/upgrade.js +0 -103
- package/dist/tools/welcome.js +0 -225
- package/dist/tools/whoami.js +0 -132
- package/dist/verbs/_spec.js +0 -15
- package/dist/verbs/announcements.js +0 -195
- package/dist/verbs/bars_batch.js +0 -66
- package/dist/verbs/chart.js +0 -110
- package/dist/verbs/concepts.js +0 -393
- package/dist/verbs/digest.js +0 -351
- package/dist/verbs/financials.js +0 -212
- package/dist/verbs/hot.js +0 -29
- package/dist/verbs/index.js +0 -88
- package/dist/verbs/limit_up.js +0 -156
- package/dist/verbs/lookup.js +0 -72
- package/dist/verbs/market.js +0 -185
- package/dist/verbs/news.js +0 -81
- package/dist/verbs/quote.js +0 -53
- package/dist/verbs/scan.js +0 -42
- package/dist/verbs/search.js +0 -105
- package/dist/verbs/sentiment.js +0 -231
- package/dist/verbs/views.js +0 -85
- package/dist/version.js +0 -5
package/dist/verbs/search.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai search ...` + MCP tool `search`.
|
|
3
|
-
*
|
|
4
|
-
* HYBRID semantic search across news + analyst views.
|
|
5
|
-
*
|
|
6
|
-
* Use this verb when:
|
|
7
|
-
* - the user is exploring a THEME / 概念 ("锂电池", "芯片", "AI 算力") — pure
|
|
8
|
-
* vector + concept expansion brings out the full supply chain even when
|
|
9
|
-
* the query word never appears verbatim in the doc.
|
|
10
|
-
* - the user gives a non-canonical company name or analyst name — entity
|
|
11
|
-
* lane resolves it through securities / research_entities tables.
|
|
12
|
-
*
|
|
13
|
-
* Prefer this over `news` / `views` for open-ended discovery. For strict
|
|
14
|
-
* time-window listing or filtered feed, keep using `views` / `news`.
|
|
15
|
-
*/
|
|
16
|
-
import { Command, Option } from "commander";
|
|
17
|
-
import { z } from "zod";
|
|
18
|
-
import { OPERATIONS } from "../_generated/operations.js";
|
|
19
|
-
import { executeVerb } from "../runtime/verb_cmd.js";
|
|
20
|
-
import { callOp } from "../runtime/verb_runner.js";
|
|
21
|
-
export const searchSpec = {
|
|
22
|
-
name: "search",
|
|
23
|
-
description: "HYBRID semantic search across analyst views + news (views weighted higher per feedback_views_over_news). Combines entity lookup (company codes, analyst names), pgvector kNN, trigram fuzzy and exact ILIKE; RRF-fused then reranked. Best for theme/concept queries: '锂电池' brings out 正极材料/碳酸锂/宁德时代; '芯片' brings out 存储芯片/洁净室/晶圆代工. Use `news` or `views` instead if you need strict time-window listing.",
|
|
24
|
-
inputSchema: {
|
|
25
|
-
q: z
|
|
26
|
-
.string()
|
|
27
|
-
.min(1)
|
|
28
|
-
.max(200)
|
|
29
|
-
.describe("Free-text query (Chinese or English)"),
|
|
30
|
-
type: z
|
|
31
|
-
.enum(["news", "views", "all"])
|
|
32
|
-
.default("all")
|
|
33
|
-
.describe("Search scope (views weighted higher in 'all')"),
|
|
34
|
-
mode: z
|
|
35
|
-
.enum(["hybrid", "exact"])
|
|
36
|
-
.default("hybrid")
|
|
37
|
-
.describe("hybrid = semantic + concept expansion + rerank; exact = ILIKE only"),
|
|
38
|
-
hours: z
|
|
39
|
-
.number()
|
|
40
|
-
.int()
|
|
41
|
-
.min(1)
|
|
42
|
-
.max(720)
|
|
43
|
-
.optional()
|
|
44
|
-
.describe("Lookback window in hours; omit for no time limit"),
|
|
45
|
-
limit: z
|
|
46
|
-
.number()
|
|
47
|
-
.int()
|
|
48
|
-
.min(1)
|
|
49
|
-
.max(50)
|
|
50
|
-
.default(20)
|
|
51
|
-
.describe("Max items returned"),
|
|
52
|
-
},
|
|
53
|
-
handler: async (args, ctx) => {
|
|
54
|
-
const op = OPERATIONS["search.semantic"];
|
|
55
|
-
if (!op)
|
|
56
|
-
throw new Error("search.semantic op missing from codegen");
|
|
57
|
-
const callArgs = {
|
|
58
|
-
q: args.q,
|
|
59
|
-
type: args.type,
|
|
60
|
-
mode: args.mode,
|
|
61
|
-
limit: args.limit,
|
|
62
|
-
};
|
|
63
|
-
if (args.hours !== undefined)
|
|
64
|
-
callArgs.hours = args.hours;
|
|
65
|
-
return callOp(op, callArgs, ctx);
|
|
66
|
-
},
|
|
67
|
-
backingOps: ["search.semantic"],
|
|
68
|
-
};
|
|
69
|
-
function clamp(raw, min, max, fallback) {
|
|
70
|
-
const n = Math.floor(Number(raw));
|
|
71
|
-
if (!Number.isFinite(n))
|
|
72
|
-
return fallback;
|
|
73
|
-
if (n < min)
|
|
74
|
-
return min;
|
|
75
|
-
if (n > max)
|
|
76
|
-
return max;
|
|
77
|
-
return n;
|
|
78
|
-
}
|
|
79
|
-
export function buildSearchCommand() {
|
|
80
|
-
const cmd = new Command(searchSpec.name).description(searchSpec.description);
|
|
81
|
-
cmd.argument("<query...>", "Search query (Chinese or English)");
|
|
82
|
-
cmd.addOption(new Option("--type <scope>", "Search scope")
|
|
83
|
-
.choices(["news", "views", "all"])
|
|
84
|
-
.default("all"));
|
|
85
|
-
cmd.addOption(new Option("--mode <mode>", "hybrid | exact")
|
|
86
|
-
.choices(["hybrid", "exact"])
|
|
87
|
-
.default("hybrid"));
|
|
88
|
-
cmd.addOption(new Option("--exact", "shortcut for --mode exact"));
|
|
89
|
-
cmd.addOption(new Option("--hours <n>", "Lookback window (1-720 hours)"));
|
|
90
|
-
cmd.addOption(new Option("--limit <n>", "Max items (1-50)").default("20"));
|
|
91
|
-
cmd.action(async (queryTokens, opts) => {
|
|
92
|
-
const q = queryTokens.join(" ").trim();
|
|
93
|
-
const args = {
|
|
94
|
-
q,
|
|
95
|
-
type: opts.type,
|
|
96
|
-
mode: opts.exact ? "exact" : opts.mode,
|
|
97
|
-
limit: clamp(opts.limit, 1, 50, 20),
|
|
98
|
-
};
|
|
99
|
-
if (opts.hours !== undefined) {
|
|
100
|
-
args.hours = clamp(opts.hours, 1, 720, 24);
|
|
101
|
-
}
|
|
102
|
-
await executeVerb(async (ctx) => searchSpec.handler(args, ctx));
|
|
103
|
-
});
|
|
104
|
-
return cmd;
|
|
105
|
-
}
|
package/dist/verbs/sentiment.js
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai sentiment ...` + MCP tools `sentiment` (overview alias, BC) /
|
|
3
|
-
* `sentiment_overview` / `sentiment_breadth` / `sentiment_turnover` /
|
|
4
|
-
* `sentiment_pct_distribution`.
|
|
5
|
-
*
|
|
6
|
-
* 四个子端点全部支持 `--scope` 切换股票池;`overview` / `breadth` /
|
|
7
|
-
* `turnover` 进一步支持 `--date YYYY-MM-DD` 取历史任意交易日的当日聚合
|
|
8
|
-
* (盘中实时为 Redis cache,历史走 ClickHouse `market_breadth_intraday` /
|
|
9
|
-
* `market_turnover_intraday`)。`turnover` 还支持 `--days 1-5` 拉同分钟同比。
|
|
10
|
-
*
|
|
11
|
-
* 顶层 `echopai sentiment` 无子命令时仍走 overview(保持 v2.2.0 兼容)。
|
|
12
|
-
*/
|
|
13
|
-
import { Command, Option } from "commander";
|
|
14
|
-
import { z } from "zod";
|
|
15
|
-
import { OPERATIONS } from "../_generated/operations.js";
|
|
16
|
-
import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
|
|
17
|
-
import { callOp } from "../runtime/verb_runner.js";
|
|
18
|
-
const SCOPE_VALUES = [
|
|
19
|
-
"all_a",
|
|
20
|
-
"all_a_ex_st",
|
|
21
|
-
"main_board",
|
|
22
|
-
"star_market",
|
|
23
|
-
"chinext",
|
|
24
|
-
"bse",
|
|
25
|
-
];
|
|
26
|
-
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
27
|
-
function clampInt(raw, min, max, fallback) {
|
|
28
|
-
const n = Math.floor(Number(raw));
|
|
29
|
-
if (!Number.isFinite(n))
|
|
30
|
-
return fallback;
|
|
31
|
-
if (n < min)
|
|
32
|
-
return min;
|
|
33
|
-
if (n > max)
|
|
34
|
-
return max;
|
|
35
|
-
return n;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Legacy `sentiment` spec — kept as alias of overview for v2.2.0 backward
|
|
39
|
-
* compatibility (MCP clients that registered `sentiment` keep working).
|
|
40
|
-
*/
|
|
41
|
-
export const sentimentSpec = {
|
|
42
|
-
name: "sentiment",
|
|
43
|
-
description: "Aggregate market sentiment indicators (limit-up/down counts, breadth, divergence index, top movers). Defaults to all_a_ex_st (all A-shares ex-ST). Pass `at_date` (YYYY-MM-DD) for any historical trading day; omit for today's latest snapshot. Backward-compat alias of `sentiment_overview` — prefer the latter in new agent code.",
|
|
44
|
-
inputSchema: {
|
|
45
|
-
scope: z
|
|
46
|
-
.enum(SCOPE_VALUES)
|
|
47
|
-
.default("all_a_ex_st")
|
|
48
|
-
.describe("Universe filter"),
|
|
49
|
-
at_date: z
|
|
50
|
-
.string()
|
|
51
|
-
.regex(DATE_RE)
|
|
52
|
-
.optional()
|
|
53
|
-
.describe("Trade date YYYY-MM-DD; omit for today's realtime snapshot"),
|
|
54
|
-
},
|
|
55
|
-
handler: async (args, ctx) => {
|
|
56
|
-
const op = OPERATIONS["sentiment.overview"];
|
|
57
|
-
if (!op)
|
|
58
|
-
throw new Error("sentiment.overview op missing");
|
|
59
|
-
const callArgs = { scope: args.scope };
|
|
60
|
-
if (args.at_date)
|
|
61
|
-
callArgs.trade_date = args.at_date;
|
|
62
|
-
return callOp(op, callArgs, ctx);
|
|
63
|
-
},
|
|
64
|
-
backingOps: ["sentiment.overview"],
|
|
65
|
-
};
|
|
66
|
-
export const sentimentOverviewSpec = {
|
|
67
|
-
name: "sentiment_overview",
|
|
68
|
-
description: "Aggregate sentiment snapshot for one trading day's latest minute: up/down/flat counts, limit-up/down, gt3/lt3 breadth, divergence index + label, MA20/50/200 breadth, 52w new high/low. `at_date` omitted = today realtime (Redis); explicit YYYY-MM-DD = historical from ClickHouse `market_breadth_intraday` last minute.",
|
|
69
|
-
inputSchema: {
|
|
70
|
-
scope: z.enum(SCOPE_VALUES).default("all_a_ex_st").describe("Universe filter"),
|
|
71
|
-
at_date: z
|
|
72
|
-
.string()
|
|
73
|
-
.regex(DATE_RE)
|
|
74
|
-
.optional()
|
|
75
|
-
.describe("Trade date YYYY-MM-DD; omit for today realtime"),
|
|
76
|
-
},
|
|
77
|
-
handler: async (args, ctx) => {
|
|
78
|
-
const op = OPERATIONS["sentiment.overview"];
|
|
79
|
-
if (!op)
|
|
80
|
-
throw new Error("sentiment.overview op missing");
|
|
81
|
-
const callArgs = { scope: args.scope };
|
|
82
|
-
if (args.at_date)
|
|
83
|
-
callArgs.trade_date = args.at_date;
|
|
84
|
-
return callOp(op, callArgs, ctx);
|
|
85
|
-
},
|
|
86
|
-
backingOps: ["sentiment.overview"],
|
|
87
|
-
};
|
|
88
|
-
export const sentimentBreadthSpec = {
|
|
89
|
-
name: "sentiment_breadth",
|
|
90
|
-
description: "Intraday breadth time series for one trading day (up to 241 minute bars; 13:00 excluded due to sparse Sina ticks). `at_date` omitted = today (pre-open returns empty); explicit YYYY-MM-DD = historical replay. Returns per-minute rows of up/down/flat/limit_up/limit_down/breadth/divergence fields — same shape as `sentiment_overview` per row.",
|
|
91
|
-
inputSchema: {
|
|
92
|
-
scope: z.enum(SCOPE_VALUES).default("all_a_ex_st").describe("Universe filter"),
|
|
93
|
-
at_date: z
|
|
94
|
-
.string()
|
|
95
|
-
.regex(DATE_RE)
|
|
96
|
-
.optional()
|
|
97
|
-
.describe("Trade date YYYY-MM-DD; omit for today"),
|
|
98
|
-
},
|
|
99
|
-
handler: async (args, ctx) => {
|
|
100
|
-
const op = OPERATIONS["sentiment.breadth"];
|
|
101
|
-
if (!op)
|
|
102
|
-
throw new Error("sentiment.breadth op missing");
|
|
103
|
-
const callArgs = { scope: args.scope };
|
|
104
|
-
if (args.at_date)
|
|
105
|
-
callArgs.trade_date = args.at_date;
|
|
106
|
-
return callOp(op, callArgs, ctx);
|
|
107
|
-
},
|
|
108
|
-
backingOps: ["sentiment.breadth"],
|
|
109
|
-
};
|
|
110
|
-
export const sentimentTurnoverSpec = {
|
|
111
|
-
name: "sentiment_turnover",
|
|
112
|
-
description: "Intraday turnover time series with `current_turnover` / `predicted_total` / `delta_vs_yesterday` headline fields. `at_date` omitted = today (pre-open returns nulls); explicit YYYY-MM-DD = historical. `days` (1-5) pulls that day + N-1 prior trading days for same-minute YoY comparison.",
|
|
113
|
-
inputSchema: {
|
|
114
|
-
scope: z.enum(SCOPE_VALUES).default("all_a_ex_st").describe("Universe filter"),
|
|
115
|
-
at_date: z
|
|
116
|
-
.string()
|
|
117
|
-
.regex(DATE_RE)
|
|
118
|
-
.optional()
|
|
119
|
-
.describe("Trade date YYYY-MM-DD; omit for today"),
|
|
120
|
-
days: z
|
|
121
|
-
.number()
|
|
122
|
-
.int()
|
|
123
|
-
.min(1)
|
|
124
|
-
.max(5)
|
|
125
|
-
.default(1)
|
|
126
|
-
.describe("at_date 当日 + 之前 days-1 天(1-5,默认 1)"),
|
|
127
|
-
},
|
|
128
|
-
handler: async (args, ctx) => {
|
|
129
|
-
const op = OPERATIONS["sentiment.turnover"];
|
|
130
|
-
if (!op)
|
|
131
|
-
throw new Error("sentiment.turnover op missing");
|
|
132
|
-
const callArgs = { scope: args.scope, days: args.days };
|
|
133
|
-
if (args.at_date)
|
|
134
|
-
callArgs.trade_date = args.at_date;
|
|
135
|
-
return callOp(op, callArgs, ctx);
|
|
136
|
-
},
|
|
137
|
-
backingOps: ["sentiment.turnover"],
|
|
138
|
-
};
|
|
139
|
-
export const sentimentPctDistributionSpec = {
|
|
140
|
-
name: "sentiment_pct_distribution",
|
|
141
|
-
description: "Real-time A-share market pct-change distribution: bucket counts at ≤-10% / -9% / ... / +10% / 涨停, plus universe total. Snapshot only — historical replay not yet exposed.",
|
|
142
|
-
inputSchema: {},
|
|
143
|
-
handler: async (_args, ctx) => {
|
|
144
|
-
const op = OPERATIONS["sentiment.pct-distribution"];
|
|
145
|
-
if (!op)
|
|
146
|
-
throw new Error("sentiment.pct-distribution op missing");
|
|
147
|
-
return callOp(op, {}, ctx);
|
|
148
|
-
},
|
|
149
|
-
backingOps: ["sentiment.pct-distribution"],
|
|
150
|
-
};
|
|
151
|
-
function addScope(cmd) {
|
|
152
|
-
cmd.addOption(new Option("--scope <universe>", "Universe filter")
|
|
153
|
-
.choices([...SCOPE_VALUES])
|
|
154
|
-
.default("all_a_ex_st"));
|
|
155
|
-
}
|
|
156
|
-
function addDate(cmd) {
|
|
157
|
-
cmd.addOption(new Option("--date <YYYY-MM-DD>", "Trade date; omit for today's realtime"));
|
|
158
|
-
}
|
|
159
|
-
export function buildSentimentCommand() {
|
|
160
|
-
const cmd = new Command("sentiment").description("Market sentiment — overview / breadth / turnover / pct-distribution; supports any historical trade_date via --date.");
|
|
161
|
-
// No-subcommand path: `echopai sentiment [--scope X]` → overview (BC with
|
|
162
|
-
// v2.2.0 where sentiment was a single-shot verb). NOTE: `--date` is
|
|
163
|
-
// intentionally NOT declared at parent level — commander would consume
|
|
164
|
-
// it from `sentiment overview --date X` and never propagate to the
|
|
165
|
-
// subcommand. For historical date queries, always use the explicit
|
|
166
|
-
// subcommand form: `echopai sentiment overview --date 2026-05-20`.
|
|
167
|
-
addScope(cmd);
|
|
168
|
-
cmd.action(async (opts, command) => {
|
|
169
|
-
// commander invokes parent action even when a subcommand runs unless
|
|
170
|
-
// we guard. Skip if any argv token after the noun is a registered
|
|
171
|
-
// subcommand name.
|
|
172
|
-
const subNames = new Set(command.commands.map((c) => c.name()));
|
|
173
|
-
const argv = process.argv.slice(2);
|
|
174
|
-
if (argv.some((tok) => subNames.has(tok)))
|
|
175
|
-
return;
|
|
176
|
-
if (!OPERATIONS["sentiment.overview"]) {
|
|
177
|
-
emitVerbError("internal_error", "sentiment.overview missing", undefined, 2);
|
|
178
|
-
}
|
|
179
|
-
await executeVerb(async (ctx) => sentimentSpec.handler({ scope: opts.scope }, ctx));
|
|
180
|
-
});
|
|
181
|
-
const overview = cmd.command("overview").description(sentimentOverviewSpec.description);
|
|
182
|
-
addScope(overview);
|
|
183
|
-
addDate(overview);
|
|
184
|
-
overview.action(async (opts) => {
|
|
185
|
-
if (!OPERATIONS["sentiment.overview"]) {
|
|
186
|
-
emitVerbError("internal_error", "sentiment.overview missing", undefined, 2);
|
|
187
|
-
}
|
|
188
|
-
const args = { scope: opts.scope };
|
|
189
|
-
if (opts.date)
|
|
190
|
-
args.at_date = opts.date;
|
|
191
|
-
await executeVerb(async (ctx) => sentimentOverviewSpec.handler(args, ctx));
|
|
192
|
-
});
|
|
193
|
-
const breadth = cmd.command("breadth").description(sentimentBreadthSpec.description);
|
|
194
|
-
addScope(breadth);
|
|
195
|
-
addDate(breadth);
|
|
196
|
-
breadth.action(async (opts) => {
|
|
197
|
-
if (!OPERATIONS["sentiment.breadth"]) {
|
|
198
|
-
emitVerbError("internal_error", "sentiment.breadth missing", undefined, 2);
|
|
199
|
-
}
|
|
200
|
-
const args = { scope: opts.scope };
|
|
201
|
-
if (opts.date)
|
|
202
|
-
args.at_date = opts.date;
|
|
203
|
-
await executeVerb(async (ctx) => sentimentBreadthSpec.handler(args, ctx));
|
|
204
|
-
});
|
|
205
|
-
const turnover = cmd.command("turnover").description(sentimentTurnoverSpec.description);
|
|
206
|
-
addScope(turnover);
|
|
207
|
-
addDate(turnover);
|
|
208
|
-
turnover.addOption(new Option("--days <n>", "at_date 当日 + 之前 days-1 天 (1-5)").default("1"));
|
|
209
|
-
turnover.action(async (opts) => {
|
|
210
|
-
if (!OPERATIONS["sentiment.turnover"]) {
|
|
211
|
-
emitVerbError("internal_error", "sentiment.turnover missing", undefined, 2);
|
|
212
|
-
}
|
|
213
|
-
const args = {
|
|
214
|
-
scope: opts.scope,
|
|
215
|
-
days: clampInt(opts.days, 1, 5, 1),
|
|
216
|
-
};
|
|
217
|
-
if (opts.date)
|
|
218
|
-
args.at_date = opts.date;
|
|
219
|
-
await executeVerb(async (ctx) => sentimentTurnoverSpec.handler(args, ctx));
|
|
220
|
-
});
|
|
221
|
-
const pctDist = cmd
|
|
222
|
-
.command("pct-distribution")
|
|
223
|
-
.description(sentimentPctDistributionSpec.description);
|
|
224
|
-
pctDist.action(async () => {
|
|
225
|
-
if (!OPERATIONS["sentiment.pct-distribution"]) {
|
|
226
|
-
emitVerbError("internal_error", "sentiment.pct-distribution missing", undefined, 2);
|
|
227
|
-
}
|
|
228
|
-
await executeVerb(async (ctx) => sentimentPctDistributionSpec.handler({}, ctx));
|
|
229
|
-
});
|
|
230
|
-
return cmd;
|
|
231
|
-
}
|
package/dist/verbs/views.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai views ...` + MCP tool `views`.
|
|
3
|
-
*
|
|
4
|
-
* PRIMARY research source (feedback_views_over_news.md).
|
|
5
|
-
*/
|
|
6
|
-
import { Command, Option } from "commander";
|
|
7
|
-
import { z } from "zod";
|
|
8
|
-
import { OPERATIONS } from "../_generated/operations.js";
|
|
9
|
-
import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
|
|
10
|
-
import { callOp } from "../runtime/verb_runner.js";
|
|
11
|
-
export const viewsSpec = {
|
|
12
|
-
name: "views",
|
|
13
|
-
description: "PRIMARY research source for stock judgement: analyst views / sell-side reports / long-form opinion stream, with research_entity_id attribution. Prefer this over `news` when forming an investment opinion. AH dual-listing: for an A+H listed company pass the A-share canonical to get the consolidated coverage (most domestic analyst views attribute to the A-share security); use HK code only if the user explicitly asks for the HK perspective.",
|
|
14
|
-
inputSchema: {
|
|
15
|
-
code: z
|
|
16
|
-
.string()
|
|
17
|
-
// Any-market:与 openapi components.schemas.CanonicalCodeAny 一致
|
|
18
|
-
// 研究/观点类 endpoint 不限 A 股;HK:00700 / US:AAPL 也允许(PR #D 拉齐)
|
|
19
|
-
.regex(/^((SSE|SZSE|BSE):[0-9]{6}|HK:[0-9]{5}|US:[A-Z][A-Z0-9.\-]{0,5})$/)
|
|
20
|
-
.optional()
|
|
21
|
-
.describe("Filter by security canonical_code (e.g. SSE:600519, HK:00700, US:AAPL)"),
|
|
22
|
-
analyst: z.string().optional().describe("Analyst Chinese name (exact / fuzzy)"),
|
|
23
|
-
institution: z.string().optional().describe("Broker / institution Chinese name"),
|
|
24
|
-
since_days: z
|
|
25
|
-
.number()
|
|
26
|
-
.int()
|
|
27
|
-
.min(1)
|
|
28
|
-
.max(90)
|
|
29
|
-
.default(7)
|
|
30
|
-
.describe("Lookback window in days (research time-horizon is longer than news)"),
|
|
31
|
-
limit: z.number().int().min(1).max(100).default(30).describe("Max items"),
|
|
32
|
-
},
|
|
33
|
-
handler: async (args, ctx) => {
|
|
34
|
-
const op = OPERATIONS["views.recent"];
|
|
35
|
-
if (!op)
|
|
36
|
-
throw new Error("views.recent op missing from codegen");
|
|
37
|
-
const callArgs = {
|
|
38
|
-
since_days: args.since_days,
|
|
39
|
-
limit: args.limit,
|
|
40
|
-
};
|
|
41
|
-
if (args.code)
|
|
42
|
-
callArgs.security = args.code;
|
|
43
|
-
if (args.analyst)
|
|
44
|
-
callArgs.analyst = args.analyst;
|
|
45
|
-
if (args.institution)
|
|
46
|
-
callArgs.institution = args.institution;
|
|
47
|
-
return callOp(op, callArgs, ctx);
|
|
48
|
-
},
|
|
49
|
-
backingOps: ["views.recent"],
|
|
50
|
-
};
|
|
51
|
-
function clamp(raw, min, max, fallback) {
|
|
52
|
-
const n = Math.floor(Number(raw));
|
|
53
|
-
if (!Number.isFinite(n))
|
|
54
|
-
return fallback;
|
|
55
|
-
if (n < min)
|
|
56
|
-
return min;
|
|
57
|
-
if (n > max)
|
|
58
|
-
return max;
|
|
59
|
-
return n;
|
|
60
|
-
}
|
|
61
|
-
export function buildViewsCommand() {
|
|
62
|
-
const cmd = new Command(viewsSpec.name).description(viewsSpec.description);
|
|
63
|
-
cmd.addOption(new Option("--code <canonical_code>", "Filter by security canonical_code"));
|
|
64
|
-
cmd.addOption(new Option("--analyst <name>", "Analyst Chinese name (exact / fuzzy)"));
|
|
65
|
-
cmd.addOption(new Option("--institution <name>", "Broker / institution Chinese name"));
|
|
66
|
-
cmd.addOption(new Option("--since-days <n>", "Lookback window (1-90 days)").default("7"));
|
|
67
|
-
cmd.addOption(new Option("--limit <n>", "Max items (1-100)").default("30"));
|
|
68
|
-
cmd.action(async (opts) => {
|
|
69
|
-
if (!OPERATIONS["views.recent"]) {
|
|
70
|
-
emitVerbError("internal_error", "views.recent missing from codegen", undefined, 2);
|
|
71
|
-
}
|
|
72
|
-
const args = {
|
|
73
|
-
since_days: clamp(opts.sinceDays, 1, 90, 7),
|
|
74
|
-
limit: clamp(opts.limit, 1, 100, 30),
|
|
75
|
-
};
|
|
76
|
-
if (opts.code)
|
|
77
|
-
args.code = opts.code;
|
|
78
|
-
if (opts.analyst)
|
|
79
|
-
args.analyst = opts.analyst;
|
|
80
|
-
if (opts.institution)
|
|
81
|
-
args.institution = opts.institution;
|
|
82
|
-
await executeVerb(async (ctx) => viewsSpec.handler(args, ctx));
|
|
83
|
-
});
|
|
84
|
-
return cmd;
|
|
85
|
-
}
|