tickflow-assist 0.3.7 → 0.3.9
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 +8 -6
- package/dist/background/realtime-monitor.worker.d.ts +1 -1
- package/dist/background/realtime-monitor.worker.js +3 -4
- package/dist/bootstrap.js +9 -0
- package/dist/dev/run-monitor-loop.js +0 -1
- package/dist/plugin-commands.js +27 -0
- package/dist/prompts/analysis/pre-market-brief-prompt.d.ts +1 -1
- package/dist/prompts/analysis/pre-market-brief-prompt.js +4 -3
- package/dist/services/alert-service.js +34 -4
- package/dist/services/monitor-service.d.ts +1 -1
- package/dist/services/monitor-service.js +18 -9
- package/dist/services/mx-search-service.d.ts +8 -1
- package/dist/services/mx-search-service.js +400 -10
- package/dist/services/pre-market-brief-service.js +343 -39
- package/dist/services/watchlist-service.d.ts +5 -1
- package/dist/services/watchlist-service.js +8 -3
- package/dist/tools/eastmoney-watchlist.tool.d.ts +31 -0
- package/dist/tools/eastmoney-watchlist.tool.js +294 -0
- package/dist/tools/mx-data.tool.d.ts +8 -0
- package/dist/tools/mx-data.tool.js +94 -0
- package/dist/tools/mx-select-stock.tool.js +6 -2
- package/dist/tools/screen-stock-candidates.tool.d.ts +34 -0
- package/dist/tools/screen-stock-candidates.tool.js +477 -0
- package/dist/types/mx-data.d.ts +23 -0
- package/dist/types/mx-data.js +1 -0
- package/dist/types/mx-select-stock.d.ts +1 -0
- package/dist/types/mx-self-select.d.ts +30 -0
- package/dist/types/mx-self-select.js +1 -0
- package/openclaw.plugin.json +143 -24
- package/package.json +9 -9
- package/skills/stock-analysis/SKILL.md +31 -2
- package/skills/usage-help/SKILL.md +33 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { normalizeSymbol } from "../utils/symbol.js";
|
|
2
|
+
const MX_ZIXUAN_DAILY_LIMIT = 200;
|
|
3
|
+
const DEFAULT_SYNC_ENRICH_PROFILE = false;
|
|
4
|
+
export function listEastmoneyWatchlistTool(mxApiService) {
|
|
5
|
+
return {
|
|
6
|
+
name: "list_eastmoney_watchlist",
|
|
7
|
+
description: "List Eastmoney account self-selected stocks via MX zixuan. Uses 1 call from the 200/day MX zixuan quota.",
|
|
8
|
+
async run() {
|
|
9
|
+
try {
|
|
10
|
+
const result = await mxApiService.getSelfSelectWatchlist();
|
|
11
|
+
return renderEastmoneyWatchlist(result.stocks, 1);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return `查询东方财富自选失败😔 ${formatErrorMessage(error)}`;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function syncEastmoneyWatchlistTool(mxApiService, watchlistService) {
|
|
20
|
+
return {
|
|
21
|
+
name: "sync_eastmoney_watchlist",
|
|
22
|
+
description: "Import Eastmoney self-selected stocks into the local TickFlow Assist watchlist. Uses 1 call from the 200/day MX zixuan quota.",
|
|
23
|
+
optional: true,
|
|
24
|
+
async run({ rawInput }) {
|
|
25
|
+
let input;
|
|
26
|
+
try {
|
|
27
|
+
input = parseSyncInput(rawInput);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return `同步东方财富自选失败😔 ${formatErrorMessage(error)}`;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const remote = await mxApiService.getSelfSelectWatchlist();
|
|
34
|
+
const local = await watchlistService.list();
|
|
35
|
+
const localSymbols = new Set(local.map((item) => item.symbol));
|
|
36
|
+
const existingCount = remote.stocks.filter((stock) => localSymbols.has(stock.symbol)).length;
|
|
37
|
+
const missing = remote.stocks.filter((stock) => !localSymbols.has(stock.symbol));
|
|
38
|
+
const candidates = missing.slice(0, input.limit ?? undefined);
|
|
39
|
+
const added = [];
|
|
40
|
+
const failed = [];
|
|
41
|
+
for (const stock of candidates) {
|
|
42
|
+
try {
|
|
43
|
+
const result = await watchlistService.add(stock.symbol, null, {
|
|
44
|
+
enrichProfile: input.refreshProfiles,
|
|
45
|
+
name: stock.name,
|
|
46
|
+
});
|
|
47
|
+
added.push(result.item);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
failed.push({ stock, error: formatErrorMessage(error) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const limitedOut = missing.length - candidates.length;
|
|
54
|
+
return [
|
|
55
|
+
"🔄 东方财富自选 -> 本地关注列表",
|
|
56
|
+
`妙想自选接口调用: 1 次(每日额度 ${MX_ZIXUAN_DAILY_LIMIT} 次)`,
|
|
57
|
+
`东方财富自选: ${remote.stocks.length} 只 | 本地已有: ${existingCount} 只 | 待同步: ${missing.length} 只 | 本次新增: ${added.length} 只 | 失败: ${failed.length} 只`,
|
|
58
|
+
limitedOut > 0 ? `因 limit 限制未处理: ${limitedOut} 只` : null,
|
|
59
|
+
input.refreshProfiles
|
|
60
|
+
? "行业/概念: 本次同步已尝试刷新"
|
|
61
|
+
: "行业/概念: 本次同步默认不刷新,可后续调用 refresh_watchlist_profiles",
|
|
62
|
+
formatAddedItems(added),
|
|
63
|
+
formatSyncFailures(failed),
|
|
64
|
+
].filter(Boolean).join("\n");
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return `同步东方财富自选失败😔 ${formatErrorMessage(error)}`;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function pushEastmoneyWatchlistTool(mxApiService, watchlistService) {
|
|
73
|
+
return {
|
|
74
|
+
name: "push_eastmoney_watchlist",
|
|
75
|
+
description: "Add local TickFlow Assist watchlist symbols to Eastmoney self-select. Uses 1 MX zixuan call per stock from the 200/day quota.",
|
|
76
|
+
optional: true,
|
|
77
|
+
async run({ rawInput }) {
|
|
78
|
+
let input;
|
|
79
|
+
try {
|
|
80
|
+
input = parseSymbolListInput(rawInput);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return `推送东方财富自选失败😔 ${formatErrorMessage(error)}`;
|
|
84
|
+
}
|
|
85
|
+
const local = await watchlistService.list();
|
|
86
|
+
const targets = selectLocalTargets(local, input);
|
|
87
|
+
if (targets.length === 0) {
|
|
88
|
+
return input.symbols
|
|
89
|
+
? `⚠️ 本地关注列表中未找到指定股票: ${input.symbols.join("、")}`
|
|
90
|
+
: "⚠️ 本地关注列表为空,无法推送到东方财富自选。";
|
|
91
|
+
}
|
|
92
|
+
if (targets.length > MX_ZIXUAN_DAILY_LIMIT) {
|
|
93
|
+
return `⚠️ 本次需要 ${targets.length} 次妙想自选调用,超过每日额度 ${MX_ZIXUAN_DAILY_LIMIT} 次,已取消。请缩小 symbols 或 limit。`;
|
|
94
|
+
}
|
|
95
|
+
const results = [];
|
|
96
|
+
for (const item of targets) {
|
|
97
|
+
try {
|
|
98
|
+
const result = await mxApiService.manageSelfSelect(buildEastmoneyAddQuery(item));
|
|
99
|
+
results.push({ item, ok: true, message: result.message ?? "已提交" });
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
results.push({ item, ok: false, message: formatErrorMessage(error) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return renderManageResults("⬆️ 本地关注列表 -> 东方财富自选", targets.length, results);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function removeEastmoneyWatchlistTool(mxApiService) {
|
|
110
|
+
return {
|
|
111
|
+
name: "remove_eastmoney_watchlist",
|
|
112
|
+
description: "Remove one stock from Eastmoney self-select via MX zixuan. Uses 1 call from the 200/day quota and does not remove the local watchlist item.",
|
|
113
|
+
optional: true,
|
|
114
|
+
async run({ rawInput }) {
|
|
115
|
+
let input;
|
|
116
|
+
try {
|
|
117
|
+
input = parseRemoveEastmoneyInput(rawInput);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
return `删除东方财富自选失败😔 ${formatErrorMessage(error)}`;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const result = await mxApiService.manageSelfSelect(`把${input.target}从我的自选股列表删除`);
|
|
124
|
+
return [
|
|
125
|
+
"🗑️ 东方财富自选删除",
|
|
126
|
+
`妙想自选接口调用: 1 次(每日额度 ${MX_ZIXUAN_DAILY_LIMIT} 次)`,
|
|
127
|
+
`目标: ${input.target}`,
|
|
128
|
+
`结果: ${result.message ?? "已提交"}`,
|
|
129
|
+
"说明: 本操作不删除 TickFlow Assist 本地关注列表,如需本地删除请调用 remove_stock。",
|
|
130
|
+
].join("\n");
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return `删除东方财富自选失败😔 ${formatErrorMessage(error)}`;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function parseSyncInput(rawInput) {
|
|
139
|
+
if (typeof rawInput === "object" && rawInput !== null) {
|
|
140
|
+
const input = rawInput;
|
|
141
|
+
return {
|
|
142
|
+
limit: parseOptionalPositiveInteger(input.limit),
|
|
143
|
+
refreshProfiles: parseOptionalBoolean(input.refreshProfiles, DEFAULT_SYNC_ENRICH_PROFILE),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
limit: null,
|
|
148
|
+
refreshProfiles: DEFAULT_SYNC_ENRICH_PROFILE,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function parseSymbolListInput(rawInput) {
|
|
152
|
+
if (typeof rawInput === "string" && rawInput.trim()) {
|
|
153
|
+
const text = rawInput.trim();
|
|
154
|
+
if (["all", "全部", "全量", "所有"].includes(text.toLowerCase())) {
|
|
155
|
+
return { symbols: null, limit: null };
|
|
156
|
+
}
|
|
157
|
+
return { symbols: [normalizeSymbol(text)], limit: null };
|
|
158
|
+
}
|
|
159
|
+
if (typeof rawInput === "object" && rawInput !== null) {
|
|
160
|
+
const input = rawInput;
|
|
161
|
+
const single = String(input.symbol ?? input.code ?? "").trim();
|
|
162
|
+
const rawSymbols = Array.isArray(input.symbols)
|
|
163
|
+
? input.symbols
|
|
164
|
+
: single
|
|
165
|
+
? [single]
|
|
166
|
+
: null;
|
|
167
|
+
return {
|
|
168
|
+
symbols: rawSymbols ? rawSymbols.map((item) => normalizeSymbol(String(item))).filter(Boolean) : null,
|
|
169
|
+
limit: parseOptionalPositiveInteger(input.limit),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { symbols: null, limit: null };
|
|
173
|
+
}
|
|
174
|
+
function parseRemoveEastmoneyInput(rawInput) {
|
|
175
|
+
if (typeof rawInput === "string" && rawInput.trim()) {
|
|
176
|
+
return { target: rawInput.trim() };
|
|
177
|
+
}
|
|
178
|
+
if (typeof rawInput === "object" && rawInput !== null) {
|
|
179
|
+
const input = rawInput;
|
|
180
|
+
const target = String(input.symbol ?? input.code ?? input.name ?? input.target ?? "").trim();
|
|
181
|
+
if (target) {
|
|
182
|
+
return { target };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
throw new Error("remove_eastmoney_watchlist requires symbol/name");
|
|
186
|
+
}
|
|
187
|
+
function parseOptionalPositiveInteger(value) {
|
|
188
|
+
if (value == null || String(value).trim() === "") {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const numeric = Number(value);
|
|
192
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
193
|
+
throw new Error("limit must be greater than 0");
|
|
194
|
+
}
|
|
195
|
+
return Math.trunc(numeric);
|
|
196
|
+
}
|
|
197
|
+
function parseOptionalBoolean(value, fallback) {
|
|
198
|
+
if (typeof value === "boolean") {
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
if (typeof value === "string") {
|
|
202
|
+
const normalized = value.trim().toLowerCase();
|
|
203
|
+
if (["true", "1", "yes", "on"].includes(normalized)) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (["false", "0", "no", "off"].includes(normalized)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return fallback;
|
|
211
|
+
}
|
|
212
|
+
function selectLocalTargets(items, input) {
|
|
213
|
+
const selected = input.symbols
|
|
214
|
+
? items.filter((item) => input.symbols.includes(item.symbol))
|
|
215
|
+
: items;
|
|
216
|
+
return selected.slice(0, input.limit ?? undefined);
|
|
217
|
+
}
|
|
218
|
+
function buildEastmoneyAddQuery(item) {
|
|
219
|
+
return `把${item.symbol.slice(0, 6)} ${item.name}添加到我的自选股列表`;
|
|
220
|
+
}
|
|
221
|
+
function renderEastmoneyWatchlist(stocks, callCount) {
|
|
222
|
+
if (stocks.length === 0) {
|
|
223
|
+
return [
|
|
224
|
+
"📊 东方财富自选股列表",
|
|
225
|
+
`妙想自选接口调用: ${callCount} 次(每日额度 ${MX_ZIXUAN_DAILY_LIMIT} 次)`,
|
|
226
|
+
"ℹ️ 当前东方财富自选股列表为空。",
|
|
227
|
+
].join("\n");
|
|
228
|
+
}
|
|
229
|
+
const lines = [
|
|
230
|
+
"📊 东方财富自选股列表",
|
|
231
|
+
`妙想自选接口调用: ${callCount} 次(每日额度 ${MX_ZIXUAN_DAILY_LIMIT} 次)`,
|
|
232
|
+
`共 ${stocks.length} 只`,
|
|
233
|
+
"",
|
|
234
|
+
];
|
|
235
|
+
for (const stock of stocks) {
|
|
236
|
+
const marketFields = [
|
|
237
|
+
stock.latestPrice ? `最新价 ${stock.latestPrice}` : null,
|
|
238
|
+
stock.changePercent ? `涨跌幅 ${formatPercent(stock.changePercent)}` : null,
|
|
239
|
+
stock.changeAmount ? `涨跌额 ${stock.changeAmount}` : null,
|
|
240
|
+
stock.turnoverRate ? `换手率 ${formatPercent(stock.turnoverRate)}` : null,
|
|
241
|
+
stock.volumeRatio ? `量比 ${stock.volumeRatio}` : null,
|
|
242
|
+
].filter(Boolean);
|
|
243
|
+
lines.push(`• ${stock.name}(${stock.symbol})${marketFields.length > 0 ? ` | ${marketFields.join(" | ")}` : ""}`);
|
|
244
|
+
}
|
|
245
|
+
return lines.join("\n");
|
|
246
|
+
}
|
|
247
|
+
function renderManageResults(title, callCount, results) {
|
|
248
|
+
const success = results.filter((result) => result.ok);
|
|
249
|
+
const failed = results.filter((result) => !result.ok);
|
|
250
|
+
return [
|
|
251
|
+
title,
|
|
252
|
+
`妙想自选接口调用: ${callCount} 次(每日额度 ${MX_ZIXUAN_DAILY_LIMIT} 次)`,
|
|
253
|
+
`成功: ${success.length} 只 | 失败: ${failed.length} 只`,
|
|
254
|
+
formatOperationSection("成功", success),
|
|
255
|
+
formatOperationSection("失败", failed),
|
|
256
|
+
].filter(Boolean).join("\n");
|
|
257
|
+
}
|
|
258
|
+
function formatOperationSection(title, results) {
|
|
259
|
+
if (results.length === 0) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return [
|
|
263
|
+
`${title}:`,
|
|
264
|
+
...results.map((result) => `• ${result.item.name}(${result.item.symbol}): ${result.message}`),
|
|
265
|
+
].join("\n");
|
|
266
|
+
}
|
|
267
|
+
function formatAddedItems(items) {
|
|
268
|
+
if (items.length === 0) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return [
|
|
272
|
+
"新增:",
|
|
273
|
+
...items.map((item) => `• ${item.name}(${item.symbol})`),
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
function formatSyncFailures(failed) {
|
|
277
|
+
if (failed.length === 0) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
return [
|
|
281
|
+
"失败:",
|
|
282
|
+
...failed.map(({ stock, error }) => `• ${stock.name}(${stock.symbol}): ${error}`),
|
|
283
|
+
].join("\n");
|
|
284
|
+
}
|
|
285
|
+
function formatPercent(value) {
|
|
286
|
+
const numeric = Number(value);
|
|
287
|
+
if (!Number.isFinite(numeric)) {
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
return `${numeric > 0 ? "+" : ""}${value}%`;
|
|
291
|
+
}
|
|
292
|
+
function formatErrorMessage(error) {
|
|
293
|
+
return error instanceof Error ? error.message : String(error);
|
|
294
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const DEFAULT_LIMIT = 20;
|
|
2
|
+
const MAX_LIMIT = 100;
|
|
3
|
+
const MAX_TABLES = 5;
|
|
4
|
+
function parseInput(rawInput) {
|
|
5
|
+
if (typeof rawInput === "string" && rawInput.trim()) {
|
|
6
|
+
return {
|
|
7
|
+
query: rawInput.trim(),
|
|
8
|
+
limit: DEFAULT_LIMIT,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
if (typeof rawInput === "object" && rawInput !== null) {
|
|
12
|
+
const obj = rawInput;
|
|
13
|
+
const query = String(obj.query ?? obj.toolQuery ?? obj.keyword ?? obj.q ?? "").trim();
|
|
14
|
+
const limit = obj.limit == null ? DEFAULT_LIMIT : Number(obj.limit);
|
|
15
|
+
if (!query) {
|
|
16
|
+
throw new Error("mx_data requires query");
|
|
17
|
+
}
|
|
18
|
+
if (!Number.isFinite(limit) || limit <= 0) {
|
|
19
|
+
throw new Error("mx_data limit must be > 0");
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
query,
|
|
23
|
+
limit: Math.min(Math.trunc(limit), MAX_LIMIT),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
throw new Error("invalid mx_data input");
|
|
27
|
+
}
|
|
28
|
+
export function mxDataTool(mxApiService) {
|
|
29
|
+
return {
|
|
30
|
+
name: "mx_data",
|
|
31
|
+
description: "Query official Eastmoney MX financial data by natural language, including quotes, financial metrics, capital flow, company profile, shareholders, and relationships.",
|
|
32
|
+
async run({ rawInput }) {
|
|
33
|
+
const input = parseInput(rawInput);
|
|
34
|
+
const result = await mxApiService.queryData(input.query);
|
|
35
|
+
const lines = [
|
|
36
|
+
`📊 妙想数据: ${input.query}`,
|
|
37
|
+
`状态: ${result.status ?? "-"} | 消息: ${result.message ?? "-"}`,
|
|
38
|
+
`结果: ${result.tables.length} 个表 | 总行数: ${result.totalRows}`,
|
|
39
|
+
];
|
|
40
|
+
if (result.questionId) {
|
|
41
|
+
lines.push(`查询ID: ${result.questionId}`);
|
|
42
|
+
}
|
|
43
|
+
if (result.entityTags.length > 0) {
|
|
44
|
+
lines.push("", "查询证券:");
|
|
45
|
+
for (const entity of result.entityTags) {
|
|
46
|
+
const main = [entity.fullName, entity.secuCode].filter(Boolean).join(" ");
|
|
47
|
+
const extra = [entity.marketChar, entity.entityTypeName, entity.className].filter(Boolean).join(" / ");
|
|
48
|
+
lines.push(`- ${extra ? `${main} (${extra})` : main}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (result.conditionParts.length > 0) {
|
|
52
|
+
lines.push("", "查询条件:");
|
|
53
|
+
lines.push(...result.conditionParts.slice(0, 3));
|
|
54
|
+
}
|
|
55
|
+
if (result.tables.length === 0) {
|
|
56
|
+
lines.push("", "⚠️ 未解析到有效数据表");
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
59
|
+
const tables = result.tables.slice(0, MAX_TABLES);
|
|
60
|
+
lines.push("", `表格预览: 展示 ${tables.length}/${result.tables.length} 个表,每表最多 ${input.limit} 行`);
|
|
61
|
+
for (const table of tables) {
|
|
62
|
+
lines.push("", formatTableHeader(table));
|
|
63
|
+
lines.push(renderCsv(table, input.limit));
|
|
64
|
+
if (table.rows.length > input.limit) {
|
|
65
|
+
lines.push(`... 仅展示前 ${input.limit} 行,共 ${table.rows.length} 行`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (result.tables.length > MAX_TABLES) {
|
|
69
|
+
lines.push(`\n... 另有 ${result.tables.length - MAX_TABLES} 个表未展示`);
|
|
70
|
+
}
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function formatTableHeader(table) {
|
|
76
|
+
const identity = [table.entityName, table.code].filter(Boolean).join(" ");
|
|
77
|
+
return `### ${table.title}${identity ? ` (${identity})` : ""}`;
|
|
78
|
+
}
|
|
79
|
+
function renderCsv(table, limit) {
|
|
80
|
+
if (table.fieldnames.length === 0) {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
const csvLines = [table.fieldnames.map(escapeCsv).join(",")];
|
|
84
|
+
for (const row of table.rows.slice(0, limit)) {
|
|
85
|
+
csvLines.push(table.fieldnames.map((fieldname) => escapeCsv(row[fieldname] ?? "")).join(","));
|
|
86
|
+
}
|
|
87
|
+
return csvLines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
function escapeCsv(value) {
|
|
90
|
+
if (/[",\n]/.test(value)) {
|
|
91
|
+
return `"${value.replace(/"/g, "\"\"")}"`;
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
@@ -43,11 +43,12 @@ export function mxSelectStockTool(mxApiService) {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
function renderMxSelectStockResult(input, result) {
|
|
46
|
+
const visibleRows = result.dataList.slice(0, input.pageSize);
|
|
46
47
|
const lines = [
|
|
47
48
|
`🧠 妙想选股: ${input.keyword}`,
|
|
48
49
|
`状态: ${result.status ?? "-"} | 业务码: ${result.code ?? "-"} | 消息: ${result.msg ?? result.message ?? "-"}`,
|
|
49
50
|
`结果类型: ${result.resultType ?? "-"} | 总数: ${result.total} | 总记录: ${result.totalRecordCount}`,
|
|
50
|
-
`页码: ${input.pageNo} | 页大小: ${input.pageSize} |
|
|
51
|
+
`页码: ${input.pageNo} | 页大小: ${input.pageSize} | 接口返回: ${result.dataList.length} | 展示: ${visibleRows.length} | 数据来源: ${result.dataSource}`,
|
|
51
52
|
];
|
|
52
53
|
if (result.parserText) {
|
|
53
54
|
lines.push(`解析: ${result.parserText}`);
|
|
@@ -73,7 +74,10 @@ function renderMxSelectStockResult(input, result) {
|
|
|
73
74
|
}
|
|
74
75
|
lines.push("");
|
|
75
76
|
lines.push("CSV:");
|
|
76
|
-
lines.push(renderCsv(result.columns,
|
|
77
|
+
lines.push(renderCsv(result.columns, visibleRows));
|
|
78
|
+
if (result.dataList.length > visibleRows.length) {
|
|
79
|
+
lines.push(`... 仅展示前 ${visibleRows.length} 行,接口本次返回 ${result.dataList.length} 行`);
|
|
80
|
+
}
|
|
77
81
|
return lines.join("\n");
|
|
78
82
|
}
|
|
79
83
|
function formatColumnMeta(column) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type TickflowApiKeyLevel } from "../config/tickflow-access.js";
|
|
2
|
+
import { FinancialService } from "../services/financial-service.js";
|
|
3
|
+
import { AnalysisService } from "../services/analysis-service.js";
|
|
4
|
+
import { KlineService } from "../services/kline-service.js";
|
|
5
|
+
import { MxApiService } from "../services/mx-search-service.js";
|
|
6
|
+
import { QuoteService } from "../services/quote-service.js";
|
|
7
|
+
import { WatchlistService } from "../services/watchlist-service.js";
|
|
8
|
+
import type { MxSelectStockResult } from "../types/mx-select-stock.js";
|
|
9
|
+
interface StockCandidate {
|
|
10
|
+
rank: number;
|
|
11
|
+
symbol: string;
|
|
12
|
+
code: string;
|
|
13
|
+
market: string | null;
|
|
14
|
+
name: string;
|
|
15
|
+
mx: {
|
|
16
|
+
latestPrice: string | null;
|
|
17
|
+
changePct: string | null;
|
|
18
|
+
pe: string | null;
|
|
19
|
+
pb: string | null;
|
|
20
|
+
turnoverRate: string | null;
|
|
21
|
+
volumeRatio: string | null;
|
|
22
|
+
amount: string | null;
|
|
23
|
+
marketValue: string | null;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare function screenStockCandidatesTool(tickflowApiKeyLevel: TickflowApiKeyLevel, mxApiService: MxApiService, quoteService: QuoteService, klineService: KlineService, financialService: FinancialService, watchlistService: WatchlistService, analysisService: AnalysisService): {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
run({ rawInput }: {
|
|
30
|
+
rawInput?: unknown;
|
|
31
|
+
}): Promise<string>;
|
|
32
|
+
};
|
|
33
|
+
export declare function extractStockCandidatesFromMxResult(result: MxSelectStockResult, limit: number): StockCandidate[];
|
|
34
|
+
export {};
|