echopai 2.2.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/dist/_generated/commands.js +96 -0
- package/dist/_generated/help.js +107 -7
- package/dist/_generated/operations.js +883 -27
- package/dist/bin.js +53 -8
- 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 +42 -7
- package/dist/verbs/announcements.js +195 -0
- package/dist/verbs/concepts.js +393 -0
- package/dist/verbs/digest.js +9 -2
- package/dist/verbs/index.js +37 -6
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `echopai concepts ...` + MCP tools for the concept-index family.
|
|
3
|
+
*
|
|
4
|
+
* Concept = THS / DC / 自定义 概念板块。所有指数采用 PLAN_CONCEPT_INDUSTRY_QUOTE
|
|
5
|
+
* §5.4 链式等权计算(base 1000)。9 个子端点:
|
|
6
|
+
*
|
|
7
|
+
* list —— 全量概念元数据 + 6 字段实时快照(默认 active)
|
|
8
|
+
* snapshot —— 全量 Redis snapshot hash(可 codes= 过滤)
|
|
9
|
+
* show —— 单个概念 meta + 当前成份股 + 个股最新行情
|
|
10
|
+
* alerts —— 当前激活的概念异动(big_move / limit_up_cluster)
|
|
11
|
+
* alerts-history —— 24h 异动历史(可按 rule / 时窗过滤)
|
|
12
|
+
* daily-bars —— 概念指数日 K(链式等权)
|
|
13
|
+
* minute-bars —— 概念指数分钟 K(max 7 天/请求)
|
|
14
|
+
* news —— 与概念名匹配的相关新闻
|
|
15
|
+
* views —— 关联券商观点(concept_views × broker_views JOIN)
|
|
16
|
+
*
|
|
17
|
+
* MCP 注册为 9 个 discrete tool;CLI 收纳到单个 `concepts` noun 下子命令。
|
|
18
|
+
*/
|
|
19
|
+
import { Command, Option } from "commander";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { OPERATIONS } from "../_generated/operations.js";
|
|
22
|
+
import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
|
|
23
|
+
import { callOp } from "../runtime/verb_runner.js";
|
|
24
|
+
const STATUS_VALUES = ["active", "pending_review", "deprecated"];
|
|
25
|
+
// Server-supported sort keys (stockpulse-rs concepts.rs sort_by chain).
|
|
26
|
+
const SERVER_SORT_VALUES = ["pct", "amount", "limit_up", "stock_count"];
|
|
27
|
+
// Client-side sort keys — fetched with server sort=pct (any), then re-sorted
|
|
28
|
+
// over the live snapshot fields returned per item. 涨速 (`speed_3min`) /
|
|
29
|
+
// 换手率 (no per-concept turnover; here it means 个股层面 turnover_rate
|
|
30
|
+
// aggregate is not present — `turnover` is alias of `amount`).
|
|
31
|
+
const CLIENT_SORT_VALUES = ["speed"];
|
|
32
|
+
const SORT_VALUES = [...SERVER_SORT_VALUES, ...CLIENT_SORT_VALUES];
|
|
33
|
+
const RULE_VALUES = ["big_move", "limit_up_cluster"];
|
|
34
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
35
|
+
function clampInt(raw, min, max, fallback) {
|
|
36
|
+
const n = Math.floor(Number(raw));
|
|
37
|
+
if (!Number.isFinite(n))
|
|
38
|
+
return fallback;
|
|
39
|
+
if (n < min)
|
|
40
|
+
return min;
|
|
41
|
+
if (n > max)
|
|
42
|
+
return max;
|
|
43
|
+
return n;
|
|
44
|
+
}
|
|
45
|
+
function parseConceptId(raw) {
|
|
46
|
+
const n = Math.floor(Number(raw));
|
|
47
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
48
|
+
throw new Error(`concept_id must be a positive integer (got ${raw})`);
|
|
49
|
+
}
|
|
50
|
+
return n;
|
|
51
|
+
}
|
|
52
|
+
export const conceptsListSpec = {
|
|
53
|
+
name: "concepts_list",
|
|
54
|
+
description: "List concepts with live 6-field snapshot (pct_change / index_value / up_count / down_count / limit_up_count / amount / speed_3min) joined from Redis. Default sort by pct (desc). Use as the entry point for browsing the concept universe — narrow down by sort and pick top movers, then drill in with `concepts_show` / `concepts_daily_bars`. Sort keys: `pct` / `amount` / `limit_up` / `stock_count` are pushed to the server; `speed` is applied client-side over `speed_3min` (3-minute pct delta).",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
status: z.enum(STATUS_VALUES).default("active").describe("Concept status filter"),
|
|
57
|
+
sort: z
|
|
58
|
+
.enum(SORT_VALUES)
|
|
59
|
+
.default("pct")
|
|
60
|
+
.describe("Sort field: pct / amount / limit_up / stock_count / speed"),
|
|
61
|
+
limit: z
|
|
62
|
+
.number()
|
|
63
|
+
.int()
|
|
64
|
+
.min(1)
|
|
65
|
+
.max(1000)
|
|
66
|
+
.default(500)
|
|
67
|
+
.describe("Max items (1-1000, default 500)"),
|
|
68
|
+
},
|
|
69
|
+
handler: async (args, ctx) => {
|
|
70
|
+
const op = OPERATIONS["concepts.list"];
|
|
71
|
+
if (!op)
|
|
72
|
+
throw new Error("concepts.list op missing");
|
|
73
|
+
const sort = String(args.sort ?? "pct");
|
|
74
|
+
const isClientSort = CLIENT_SORT_VALUES.includes(sort);
|
|
75
|
+
// Client-side sort: fetch the full universe (server max 1000) so the
|
|
76
|
+
// re-rank sees every concept, then slice to user-requested `limit`.
|
|
77
|
+
// Server-side sort: pass-through (server enforces limit honestly).
|
|
78
|
+
const serverArgs = {
|
|
79
|
+
status: args.status,
|
|
80
|
+
sort: isClientSort ? "pct" : sort,
|
|
81
|
+
limit: isClientSort ? 1000 : args.limit,
|
|
82
|
+
};
|
|
83
|
+
const env = await callOp(op, serverArgs, ctx);
|
|
84
|
+
if (!isClientSort)
|
|
85
|
+
return env;
|
|
86
|
+
return resortListEnvelopeBySpeed(env, Number(args.limit ?? 500));
|
|
87
|
+
},
|
|
88
|
+
backingOps: ["concepts.list"],
|
|
89
|
+
};
|
|
90
|
+
function resortListEnvelopeBySpeed(env, limit) {
|
|
91
|
+
const body = env.data;
|
|
92
|
+
if (!body || typeof body !== "object" || !Array.isArray(body.items)) {
|
|
93
|
+
return env;
|
|
94
|
+
}
|
|
95
|
+
const items = body.items.slice();
|
|
96
|
+
items.sort((a, b) => {
|
|
97
|
+
const av = numericOrNeg(a.speed_3min);
|
|
98
|
+
const bv = numericOrNeg(b.speed_3min);
|
|
99
|
+
return bv - av;
|
|
100
|
+
});
|
|
101
|
+
const sliced = items.slice(0, Math.max(1, Math.floor(limit)));
|
|
102
|
+
const data = { ...body, items: sliced };
|
|
103
|
+
const meta = { ...(env.meta ?? {}), client_sort: "speed" };
|
|
104
|
+
return { data, meta };
|
|
105
|
+
}
|
|
106
|
+
function numericOrNeg(v) {
|
|
107
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
108
|
+
return v;
|
|
109
|
+
if (typeof v === "string") {
|
|
110
|
+
const n = Number(v);
|
|
111
|
+
if (Number.isFinite(n))
|
|
112
|
+
return n;
|
|
113
|
+
}
|
|
114
|
+
return Number.NEGATIVE_INFINITY;
|
|
115
|
+
}
|
|
116
|
+
export const conceptsSnapshotSpec = {
|
|
117
|
+
name: "concepts_snapshot",
|
|
118
|
+
description: "Bulk Redis snapshot of all concepts. Use `codes` (comma-separated concept_id list) to filter; omit for the full set. Cheaper than `concepts_list` when only the live numbers are needed without joining meta.",
|
|
119
|
+
inputSchema: {
|
|
120
|
+
codes: z
|
|
121
|
+
.string()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("Comma-separated concept_id list (e.g. `1,2,3`); omit for all."),
|
|
124
|
+
},
|
|
125
|
+
handler: async (args, ctx) => {
|
|
126
|
+
const op = OPERATIONS["concepts.snapshot"];
|
|
127
|
+
if (!op)
|
|
128
|
+
throw new Error("concepts.snapshot op missing");
|
|
129
|
+
const callArgs = {};
|
|
130
|
+
if (args.codes)
|
|
131
|
+
callArgs.codes = args.codes;
|
|
132
|
+
return callOp(op, callArgs, ctx);
|
|
133
|
+
},
|
|
134
|
+
backingOps: ["concepts.snapshot"],
|
|
135
|
+
};
|
|
136
|
+
export const conceptsShowSpec = {
|
|
137
|
+
name: "concepts_show",
|
|
138
|
+
description: "Detail for one concept: meta (full_name / source / status / approved_at / first_listed_at) plus current member securities with their latest snapshots. Use after `concepts_list` to inspect a specific theme.",
|
|
139
|
+
inputSchema: {
|
|
140
|
+
concept_id: z.number().int().positive().describe("Concept primary key (PG concepts.id)"),
|
|
141
|
+
},
|
|
142
|
+
handler: async (args, ctx) => {
|
|
143
|
+
const op = OPERATIONS["concepts.show"];
|
|
144
|
+
if (!op)
|
|
145
|
+
throw new Error("concepts.show op missing");
|
|
146
|
+
return callOp(op, { concept_id: args.concept_id }, ctx);
|
|
147
|
+
},
|
|
148
|
+
backingOps: ["concepts.show"],
|
|
149
|
+
};
|
|
150
|
+
export const conceptsAlertsSpec = {
|
|
151
|
+
name: "concepts_alerts",
|
|
152
|
+
description: "Currently active concept alerts. Two rules: `big_move` (|pct_change| > 3%) and `limit_up_cluster` (limit_up_count >= 3 AND stock_count >= 5). Cheap, non-billable. Use as the agent's signal for 'which themes are running hot right now'.",
|
|
153
|
+
inputSchema: {},
|
|
154
|
+
handler: async (_args, ctx) => {
|
|
155
|
+
const op = OPERATIONS["concepts.alerts"];
|
|
156
|
+
if (!op)
|
|
157
|
+
throw new Error("concepts.alerts op missing");
|
|
158
|
+
return callOp(op, {}, ctx);
|
|
159
|
+
},
|
|
160
|
+
backingOps: ["concepts.alerts"],
|
|
161
|
+
};
|
|
162
|
+
export const conceptsAlertsHistorySpec = {
|
|
163
|
+
name: "concepts_alerts_history",
|
|
164
|
+
description: "Concept-alert historical events (default last 24h). Filter by `rule` (big_move / limit_up_cluster) and Unix-ms `from`/`to` window. Used by AI agent / admin to review what fired and when.",
|
|
165
|
+
inputSchema: {
|
|
166
|
+
from: z
|
|
167
|
+
.number()
|
|
168
|
+
.int()
|
|
169
|
+
.optional()
|
|
170
|
+
.describe("Unix ms; default = now - 24h"),
|
|
171
|
+
to: z.number().int().optional().describe("Unix ms; default = now"),
|
|
172
|
+
rule: z.enum(RULE_VALUES).optional().describe("Filter by alert rule"),
|
|
173
|
+
limit: z
|
|
174
|
+
.number()
|
|
175
|
+
.int()
|
|
176
|
+
.min(1)
|
|
177
|
+
.max(2000)
|
|
178
|
+
.default(500)
|
|
179
|
+
.describe("Max items (1-2000, default 500)"),
|
|
180
|
+
},
|
|
181
|
+
handler: async (args, ctx) => {
|
|
182
|
+
const op = OPERATIONS["concepts.alerts-history"];
|
|
183
|
+
if (!op)
|
|
184
|
+
throw new Error("concepts.alerts-history op missing");
|
|
185
|
+
const callArgs = { limit: args.limit };
|
|
186
|
+
if (args.from !== undefined)
|
|
187
|
+
callArgs.from = args.from;
|
|
188
|
+
if (args.to !== undefined)
|
|
189
|
+
callArgs.to = args.to;
|
|
190
|
+
if (args.rule)
|
|
191
|
+
callArgs.rule = args.rule;
|
|
192
|
+
return callOp(op, callArgs, ctx);
|
|
193
|
+
},
|
|
194
|
+
backingOps: ["concepts.alerts-history"],
|
|
195
|
+
};
|
|
196
|
+
export const conceptsDailyBarsSpec = {
|
|
197
|
+
name: "concepts_daily_bars",
|
|
198
|
+
description: "Daily OHLC for a concept index (chain-linked equal-weight, base 1000). Returns breadth fields and `is_backfilled` flag. Requires `bars:read` scope.",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
concept_id: z.number().int().positive().describe("Concept primary key"),
|
|
201
|
+
from: z.string().regex(DATE_RE).describe("Inclusive start date YYYY-MM-DD"),
|
|
202
|
+
to: z.string().regex(DATE_RE).describe("Inclusive end date YYYY-MM-DD"),
|
|
203
|
+
},
|
|
204
|
+
handler: async (args, ctx) => {
|
|
205
|
+
const op = OPERATIONS["concepts.daily-bars"];
|
|
206
|
+
if (!op)
|
|
207
|
+
throw new Error("concepts.daily-bars op missing");
|
|
208
|
+
return callOp(op, { concept_id: args.concept_id, from: args.from, to: args.to }, ctx);
|
|
209
|
+
},
|
|
210
|
+
backingOps: ["concepts.daily-bars"],
|
|
211
|
+
};
|
|
212
|
+
export const conceptsMinuteBarsSpec = {
|
|
213
|
+
name: "concepts_minute_bars",
|
|
214
|
+
description: "Concept-index minute-level OHLC (chain-linked equal-weight, base 1000). Max 7 days per request. Requires `bars:read` scope.",
|
|
215
|
+
inputSchema: {
|
|
216
|
+
concept_id: z.number().int().positive().describe("Concept primary key"),
|
|
217
|
+
from: z.string().regex(DATE_RE).describe("Inclusive start date YYYY-MM-DD"),
|
|
218
|
+
to: z.string().regex(DATE_RE).describe("Inclusive end date YYYY-MM-DD (max 7 days)"),
|
|
219
|
+
},
|
|
220
|
+
handler: async (args, ctx) => {
|
|
221
|
+
const op = OPERATIONS["concepts.minute-bars"];
|
|
222
|
+
if (!op)
|
|
223
|
+
throw new Error("concepts.minute-bars op missing");
|
|
224
|
+
return callOp(op, { concept_id: args.concept_id, from: args.from, to: args.to }, ctx);
|
|
225
|
+
},
|
|
226
|
+
backingOps: ["concepts.minute-bars"],
|
|
227
|
+
};
|
|
228
|
+
export const conceptsNewsSpec = {
|
|
229
|
+
name: "concepts_news",
|
|
230
|
+
description: "Recent news mentioning the concept name (ILIKE on news.title + content). Short-term implementation; long-term will switch to AI tagging. Requires `news:read` scope.",
|
|
231
|
+
inputSchema: {
|
|
232
|
+
concept_id: z.number().int().positive().describe("Concept primary key"),
|
|
233
|
+
days: z
|
|
234
|
+
.number()
|
|
235
|
+
.int()
|
|
236
|
+
.min(1)
|
|
237
|
+
.max(365)
|
|
238
|
+
.default(30)
|
|
239
|
+
.describe("Lookback window in days (1-365, default 30)"),
|
|
240
|
+
limit: z
|
|
241
|
+
.number()
|
|
242
|
+
.int()
|
|
243
|
+
.min(1)
|
|
244
|
+
.max(200)
|
|
245
|
+
.default(50)
|
|
246
|
+
.describe("Max items (1-200, default 50)"),
|
|
247
|
+
},
|
|
248
|
+
handler: async (args, ctx) => {
|
|
249
|
+
const op = OPERATIONS["concepts.news"];
|
|
250
|
+
if (!op)
|
|
251
|
+
throw new Error("concepts.news op missing");
|
|
252
|
+
return callOp(op, { concept_id: args.concept_id, days: args.days, limit: args.limit }, ctx);
|
|
253
|
+
},
|
|
254
|
+
backingOps: ["concepts.news"],
|
|
255
|
+
};
|
|
256
|
+
export const conceptsViewsSpec = {
|
|
257
|
+
name: "concepts_views",
|
|
258
|
+
description: "Broker views associated with a concept (concept_views × broker_views, ai_status=done), sorted by published_at desc. Primary research source for a theme. Requires `views:read` scope.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
concept_id: z.number().int().positive().describe("Concept primary key"),
|
|
261
|
+
limit: z
|
|
262
|
+
.number()
|
|
263
|
+
.int()
|
|
264
|
+
.min(1)
|
|
265
|
+
.max(200)
|
|
266
|
+
.default(50)
|
|
267
|
+
.describe("Max items (1-200, default 50)"),
|
|
268
|
+
},
|
|
269
|
+
handler: async (args, ctx) => {
|
|
270
|
+
const op = OPERATIONS["concepts.views"];
|
|
271
|
+
if (!op)
|
|
272
|
+
throw new Error("concepts.views op missing");
|
|
273
|
+
return callOp(op, { concept_id: args.concept_id, limit: args.limit }, ctx);
|
|
274
|
+
},
|
|
275
|
+
backingOps: ["concepts.views"],
|
|
276
|
+
};
|
|
277
|
+
export function buildConceptsCommand() {
|
|
278
|
+
const cmd = new Command("concepts").description("Concept indices — list / snapshot / show / alerts / alerts-history / daily-bars / minute-bars / news / views.");
|
|
279
|
+
const list = cmd.command("list").description(conceptsListSpec.description);
|
|
280
|
+
list.addOption(new Option("--status <s>", "Status filter").choices([...STATUS_VALUES]).default("active"));
|
|
281
|
+
list.addOption(new Option("--sort <field>", "Sort field").choices([...SORT_VALUES]).default("pct"));
|
|
282
|
+
list.addOption(new Option("--limit <n>", "Max items (1-1000)").default("500"));
|
|
283
|
+
list.action(async (opts) => {
|
|
284
|
+
if (!OPERATIONS["concepts.list"]) {
|
|
285
|
+
emitVerbError("internal_error", "concepts.list missing", undefined, 2);
|
|
286
|
+
}
|
|
287
|
+
await executeVerb(async (ctx) => conceptsListSpec.handler({
|
|
288
|
+
status: opts.status,
|
|
289
|
+
sort: opts.sort,
|
|
290
|
+
limit: clampInt(opts.limit, 1, 1000, 500),
|
|
291
|
+
}, ctx));
|
|
292
|
+
});
|
|
293
|
+
const snapshot = cmd.command("snapshot").description(conceptsSnapshotSpec.description);
|
|
294
|
+
snapshot.addOption(new Option("--codes <ids>", "Comma-separated concept_id list"));
|
|
295
|
+
snapshot.action(async (opts) => {
|
|
296
|
+
if (!OPERATIONS["concepts.snapshot"]) {
|
|
297
|
+
emitVerbError("internal_error", "concepts.snapshot missing", undefined, 2);
|
|
298
|
+
}
|
|
299
|
+
const args = {};
|
|
300
|
+
if (opts.codes)
|
|
301
|
+
args.codes = opts.codes;
|
|
302
|
+
await executeVerb(async (ctx) => conceptsSnapshotSpec.handler(args, ctx));
|
|
303
|
+
});
|
|
304
|
+
const show = cmd
|
|
305
|
+
.command("show <concept_id>")
|
|
306
|
+
.description(conceptsShowSpec.description);
|
|
307
|
+
show.action(async (conceptIdRaw) => {
|
|
308
|
+
if (!OPERATIONS["concepts.show"]) {
|
|
309
|
+
emitVerbError("internal_error", "concepts.show missing", undefined, 2);
|
|
310
|
+
}
|
|
311
|
+
await executeVerb(async (ctx) => conceptsShowSpec.handler({ concept_id: parseConceptId(conceptIdRaw) }, ctx));
|
|
312
|
+
});
|
|
313
|
+
const alerts = cmd.command("alerts").description(conceptsAlertsSpec.description);
|
|
314
|
+
alerts.action(async () => {
|
|
315
|
+
if (!OPERATIONS["concepts.alerts"]) {
|
|
316
|
+
emitVerbError("internal_error", "concepts.alerts missing", undefined, 2);
|
|
317
|
+
}
|
|
318
|
+
await executeVerb(async (ctx) => conceptsAlertsSpec.handler({}, ctx));
|
|
319
|
+
});
|
|
320
|
+
const alertsHist = cmd
|
|
321
|
+
.command("alerts-history")
|
|
322
|
+
.description(conceptsAlertsHistorySpec.description);
|
|
323
|
+
alertsHist.addOption(new Option("--from <unix_ms>", "Unix ms lower bound; default now-24h"));
|
|
324
|
+
alertsHist.addOption(new Option("--to <unix_ms>", "Unix ms upper bound; default now"));
|
|
325
|
+
alertsHist.addOption(new Option("--rule <r>", "Filter by rule").choices([...RULE_VALUES]));
|
|
326
|
+
alertsHist.addOption(new Option("--limit <n>", "Max items (1-2000)").default("500"));
|
|
327
|
+
alertsHist.action(async (opts) => {
|
|
328
|
+
if (!OPERATIONS["concepts.alerts-history"]) {
|
|
329
|
+
emitVerbError("internal_error", "concepts.alerts-history missing", undefined, 2);
|
|
330
|
+
}
|
|
331
|
+
const args = { limit: clampInt(opts.limit, 1, 2000, 500) };
|
|
332
|
+
if (opts.from !== undefined) {
|
|
333
|
+
const n = Math.floor(Number(opts.from));
|
|
334
|
+
if (Number.isFinite(n))
|
|
335
|
+
args.from = n;
|
|
336
|
+
}
|
|
337
|
+
if (opts.to !== undefined) {
|
|
338
|
+
const n = Math.floor(Number(opts.to));
|
|
339
|
+
if (Number.isFinite(n))
|
|
340
|
+
args.to = n;
|
|
341
|
+
}
|
|
342
|
+
if (opts.rule)
|
|
343
|
+
args.rule = opts.rule;
|
|
344
|
+
await executeVerb(async (ctx) => conceptsAlertsHistorySpec.handler(args, ctx));
|
|
345
|
+
});
|
|
346
|
+
const daily = cmd
|
|
347
|
+
.command("daily-bars <concept_id>")
|
|
348
|
+
.description(conceptsDailyBarsSpec.description);
|
|
349
|
+
daily.addOption(new Option("--from <YYYY-MM-DD>", "Inclusive start date").makeOptionMandatory(true));
|
|
350
|
+
daily.addOption(new Option("--to <YYYY-MM-DD>", "Inclusive end date").makeOptionMandatory(true));
|
|
351
|
+
daily.action(async (conceptIdRaw, opts) => {
|
|
352
|
+
if (!OPERATIONS["concepts.daily-bars"]) {
|
|
353
|
+
emitVerbError("internal_error", "concepts.daily-bars missing", undefined, 2);
|
|
354
|
+
}
|
|
355
|
+
await executeVerb(async (ctx) => conceptsDailyBarsSpec.handler({ concept_id: parseConceptId(conceptIdRaw), from: opts.from, to: opts.to }, ctx));
|
|
356
|
+
});
|
|
357
|
+
const minute = cmd
|
|
358
|
+
.command("minute-bars <concept_id>")
|
|
359
|
+
.description(conceptsMinuteBarsSpec.description);
|
|
360
|
+
minute.addOption(new Option("--from <YYYY-MM-DD>", "Inclusive start date").makeOptionMandatory(true));
|
|
361
|
+
minute.addOption(new Option("--to <YYYY-MM-DD>", "Inclusive end date (max 7 days)").makeOptionMandatory(true));
|
|
362
|
+
minute.action(async (conceptIdRaw, opts) => {
|
|
363
|
+
if (!OPERATIONS["concepts.minute-bars"]) {
|
|
364
|
+
emitVerbError("internal_error", "concepts.minute-bars missing", undefined, 2);
|
|
365
|
+
}
|
|
366
|
+
await executeVerb(async (ctx) => conceptsMinuteBarsSpec.handler({ concept_id: parseConceptId(conceptIdRaw), from: opts.from, to: opts.to }, ctx));
|
|
367
|
+
});
|
|
368
|
+
const news = cmd.command("news <concept_id>").description(conceptsNewsSpec.description);
|
|
369
|
+
news.addOption(new Option("--days <n>", "Lookback window (1-365)").default("30"));
|
|
370
|
+
news.addOption(new Option("--limit <n>", "Max items (1-200)").default("50"));
|
|
371
|
+
news.action(async (conceptIdRaw, opts) => {
|
|
372
|
+
if (!OPERATIONS["concepts.news"]) {
|
|
373
|
+
emitVerbError("internal_error", "concepts.news missing", undefined, 2);
|
|
374
|
+
}
|
|
375
|
+
await executeVerb(async (ctx) => conceptsNewsSpec.handler({
|
|
376
|
+
concept_id: parseConceptId(conceptIdRaw),
|
|
377
|
+
days: clampInt(opts.days, 1, 365, 30),
|
|
378
|
+
limit: clampInt(opts.limit, 1, 200, 50),
|
|
379
|
+
}, ctx));
|
|
380
|
+
});
|
|
381
|
+
const views = cmd.command("views <concept_id>").description(conceptsViewsSpec.description);
|
|
382
|
+
views.addOption(new Option("--limit <n>", "Max items (1-200)").default("50"));
|
|
383
|
+
views.action(async (conceptIdRaw, opts) => {
|
|
384
|
+
if (!OPERATIONS["concepts.views"]) {
|
|
385
|
+
emitVerbError("internal_error", "concepts.views missing", undefined, 2);
|
|
386
|
+
}
|
|
387
|
+
await executeVerb(async (ctx) => conceptsViewsSpec.handler({
|
|
388
|
+
concept_id: parseConceptId(conceptIdRaw),
|
|
389
|
+
limit: clampInt(opts.limit, 1, 200, 50),
|
|
390
|
+
}, ctx));
|
|
391
|
+
});
|
|
392
|
+
return cmd;
|
|
393
|
+
}
|
package/dist/verbs/digest.js
CHANGED
|
@@ -43,10 +43,14 @@ import { executeVerb, emitVerbError } from "../runtime/verb_cmd.js";
|
|
|
43
43
|
import { callOp } from "../runtime/verb_runner.js";
|
|
44
44
|
/** Buckets in the order the description establishes (views first, news last). */
|
|
45
45
|
const BUCKETS = ["views", "quote", "snapshot", "sentiment", "news"];
|
|
46
|
+
// digest 故意保持 A 股 only:fan-out 里 quote / 14-field snapshot / sentiment 都是
|
|
47
|
+
// StockPulse Rust API 上的 A 股 endpoint,HK/US 没有 capability(PR #D capability gate
|
|
48
|
+
// 只把 HK/US 加进 search,没有 quote/chart)。混进 HK/US 会得到 partial_failures 一片红。
|
|
49
|
+
// 等 HK/US 行情接入再放开(独立立项)。
|
|
46
50
|
const CODE_REGEX = /^(SSE|SZSE|BSE|SH|SZ|BJ):[0-9]{6}$/;
|
|
47
51
|
export const digestSpec = {
|
|
48
52
|
name: "digest",
|
|
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>'.",
|
|
53
|
+
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>'. AH dual-listing: for an A+H listed company always pass the A-share canonical (e.g. SSE:601398), not the HK side.",
|
|
50
54
|
inputSchema: {
|
|
51
55
|
code: z
|
|
52
56
|
.string()
|
|
@@ -330,7 +334,10 @@ export function buildDigestCommand() {
|
|
|
330
334
|
cmd.addOption(new Option("--limit-per-bucket <n>", "Items per bucket (1-50)").default("10"));
|
|
331
335
|
cmd.action(async (opts) => {
|
|
332
336
|
if (!CODE_REGEX.test(opts.code)) {
|
|
333
|
-
|
|
337
|
+
const isHkUs = /^(HK|US):/.test(opts.code);
|
|
338
|
+
emitVerbError("invalid_args", `code ${JSON.stringify(opts.code)} is not a canonical_code (expected SSE|SZSE|BSE:NNNNNN)`, isHkUs
|
|
339
|
+
? "digest fan-out depends on A-share quote/chart/sentiment endpoints — HK/US not yet supported. Use `search` or `views` for HK/US."
|
|
340
|
+
: "Use `echopai lookup --text <name>` to resolve to a canonical_code first.", 1);
|
|
334
341
|
}
|
|
335
342
|
const args = {
|
|
336
343
|
code: opts.code,
|