@hedgehog-finance/hedgehog-plugin 1.0.21 → 1.0.22

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.
Files changed (111) hide show
  1. package/dist/index.d.ts +4 -3
  2. package/dist/index.js +49 -6
  3. package/dist/index.js.map +1 -1
  4. package/dist/setup-api.d.ts +2 -0
  5. package/dist/setup-api.js +8 -0
  6. package/dist/setup-api.js.map +1 -0
  7. package/dist/src/channel.js +25 -23
  8. package/dist/src/channel.js.map +1 -1
  9. package/dist/src/core/database.js +449 -39
  10. package/dist/src/core/database.js.map +1 -1
  11. package/dist/src/dailyMorningBriefingCron.d.ts +46 -0
  12. package/dist/src/dailyMorningBriefingCron.js +82 -0
  13. package/dist/src/dailyMorningBriefingCron.js.map +1 -0
  14. package/dist/src/features/chartOutput.d.ts +2 -0
  15. package/dist/src/features/chartOutput.js +35 -0
  16. package/dist/src/features/chartOutput.js.map +1 -0
  17. package/dist/src/features/dailyMorningBriefing/schema.d.ts +56 -0
  18. package/dist/src/features/dailyMorningBriefing/schema.js +22 -0
  19. package/dist/src/features/dailyMorningBriefing/schema.js.map +1 -0
  20. package/dist/src/features/dailyMorningBriefing/tools.d.ts +12 -0
  21. package/dist/src/features/dailyMorningBriefing/tools.js +204 -0
  22. package/dist/src/features/dailyMorningBriefing/tools.js.map +1 -0
  23. package/dist/src/features/deepReasoning/schema.d.ts +43 -0
  24. package/dist/src/features/deepReasoning/schema.js +17 -0
  25. package/dist/src/features/deepReasoning/schema.js.map +1 -0
  26. package/dist/src/features/deepReasoning/tools.d.ts +12 -0
  27. package/dist/src/features/deepReasoning/tools.js +163 -0
  28. package/dist/src/features/deepReasoning/tools.js.map +1 -0
  29. package/dist/src/features/index.d.ts +2 -1
  30. package/dist/src/features/index.js +9 -1
  31. package/dist/src/features/index.js.map +1 -1
  32. package/dist/src/features/informationVerification/schema.d.ts +43 -0
  33. package/dist/src/features/informationVerification/schema.js +17 -0
  34. package/dist/src/features/informationVerification/schema.js.map +1 -0
  35. package/dist/src/features/informationVerification/tools.d.ts +12 -0
  36. package/dist/src/features/informationVerification/tools.js +162 -0
  37. package/dist/src/features/informationVerification/tools.js.map +1 -0
  38. package/dist/src/features/notes/schema.d.ts +136 -39
  39. package/dist/src/features/notes/schema.js +13 -10
  40. package/dist/src/features/notes/schema.js.map +1 -1
  41. package/dist/src/features/notes/tools.d.ts +1 -0
  42. package/dist/src/features/notes/tools.js +47 -14
  43. package/dist/src/features/notes/tools.js.map +1 -1
  44. package/dist/src/features/pluginInfo/schema.d.ts +79 -0
  45. package/dist/src/features/pluginInfo/schema.js +14 -0
  46. package/dist/src/features/pluginInfo/schema.js.map +1 -0
  47. package/dist/src/features/pluginInfo/tools.d.ts +1 -0
  48. package/dist/src/features/pluginInfo/tools.js +157 -2
  49. package/dist/src/features/pluginInfo/tools.js.map +1 -1
  50. package/dist/src/features/profileLibrary/schema.d.ts +34 -6
  51. package/dist/src/features/profileLibrary/schema.js +1 -1
  52. package/dist/src/features/profileLibrary/schema.js.map +1 -1
  53. package/dist/src/features/stockAnalysis/schema.d.ts +224 -31
  54. package/dist/src/features/stockAnalysis/schema.js +76 -12
  55. package/dist/src/features/stockAnalysis/schema.js.map +1 -1
  56. package/dist/src/features/stockAnalysis/tools.d.ts +6 -4
  57. package/dist/src/features/stockAnalysis/tools.js +389 -44
  58. package/dist/src/features/stockAnalysis/tools.js.map +1 -1
  59. package/dist/src/features/stockBasic/schema.d.ts +149 -0
  60. package/dist/src/features/stockBasic/schema.js +26 -0
  61. package/dist/src/features/stockBasic/schema.js.map +1 -0
  62. package/dist/src/features/stockBasic/tools.d.ts +12 -0
  63. package/dist/src/features/stockBasic/tools.js +124 -0
  64. package/dist/src/features/stockBasic/tools.js.map +1 -0
  65. package/dist/src/features/watchlist/logic.d.ts +3 -3
  66. package/dist/src/features/watchlist/logic.js +47 -46
  67. package/dist/src/features/watchlist/logic.js.map +1 -1
  68. package/dist/src/features/watchlist/schema.d.ts +89 -54
  69. package/dist/src/features/watchlist/schema.js +7 -4
  70. package/dist/src/features/watchlist/schema.js.map +1 -1
  71. package/dist/src/features/watchlist/tools.d.ts +106 -59
  72. package/dist/src/features/watchlist/tools.js +182 -104
  73. package/dist/src/features/watchlist/tools.js.map +1 -1
  74. package/dist/src/openclawConfig.d.ts +6 -0
  75. package/dist/src/openclawConfig.js +74 -0
  76. package/dist/src/openclawConfig.js.map +1 -0
  77. package/dist/src/openclawConstants.d.ts +4 -0
  78. package/dist/src/openclawConstants.js +10 -0
  79. package/dist/src/openclawConstants.js.map +1 -0
  80. package/dist/src/runtime.js +20 -0
  81. package/dist/src/runtime.js.map +1 -1
  82. package/index.ts +52 -5
  83. package/openclaw.plugin.json +24 -0
  84. package/package.json +21 -6
  85. package/setup-api.ts +10 -0
  86. package/src/channel.ts +26 -25
  87. package/src/core/database.ts +447 -40
  88. package/src/dailyMorningBriefingCron.ts +129 -0
  89. package/src/features/chartOutput.ts +35 -0
  90. package/src/features/dailyMorningBriefing/schema.ts +38 -0
  91. package/src/features/dailyMorningBriefing/tools.ts +246 -0
  92. package/src/features/deepReasoning/schema.ts +22 -0
  93. package/src/features/deepReasoning/tools.ts +182 -0
  94. package/src/features/index.ts +11 -2
  95. package/src/features/informationVerification/schema.ts +22 -0
  96. package/src/features/informationVerification/tools.ts +181 -0
  97. package/src/features/notes/schema.ts +17 -12
  98. package/src/features/notes/tools.ts +54 -17
  99. package/src/features/pluginInfo/schema.ts +19 -0
  100. package/src/features/pluginInfo/tools.ts +173 -2
  101. package/src/features/profileLibrary/schema.ts +1 -1
  102. package/src/features/stockAnalysis/schema.ts +99 -17
  103. package/src/features/stockAnalysis/tools.ts +447 -49
  104. package/src/features/stockBasic/schema.ts +33 -0
  105. package/src/features/stockBasic/tools.ts +157 -0
  106. package/src/features/watchlist/logic.ts +56 -53
  107. package/src/features/watchlist/schema.ts +11 -6
  108. package/src/features/watchlist/tools.ts +191 -106
  109. package/src/openclawConfig.ts +101 -0
  110. package/src/openclawConstants.ts +11 -0
  111. package/src/runtime.ts +19 -0
@@ -1,80 +1,291 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getDB } from "../../core/database.js";
3
+ import { CHART_OUTPUT_GUIDANCE, ensureChartPlaceholdersInBody } from "../chartOutput.js";
3
4
  import {
4
5
  ArticleAiAnalysis,
6
+ BuildStockAiAnalysisMessageParams,
7
+ BuildStockAiAnalysisMessageParamsSchema,
5
8
  GetArticleAiAnalysisParamsSchema,
9
+ GetStockAiAnalysisDetailParamsSchema,
6
10
  GetStockAiAnalysisParamsSchema,
11
+ QueryArticleAiAnalysisHistoryParamsSchema,
12
+ QueryStockAiAnalysisStocksParamsSchema,
13
+ SaveArticleDeepReasoningParamsSchema,
7
14
  QueryStockAiAnalysisHistoryParamsSchema,
8
15
  SaveArticleAiAnalysisParamsSchema,
9
16
  SaveStockAiAnalysisParamsSchema,
10
- StockAiAnalysis
17
+ StockAiAnalysis,
18
+ StockAiAnalysisStockSummary
11
19
  } from "./schema.js";
12
20
 
13
21
  interface RuntimeTool {
14
22
  name: string;
23
+ label?: string;
15
24
  description: string;
16
25
  parameters: unknown;
17
26
  registerTool?: boolean;
18
- execute(params: unknown, ctx: { userId: string }): Promise<string>;
27
+ execute(params: unknown, ctx?: { userId: string }): Promise<string>;
19
28
  }
20
29
 
21
- export function normalizeStockCode(stockCode: string): string {
22
- return stockCode.trim().toUpperCase().replace(/\.SS$/i, ".SH");
30
+ const STOCK_AI_ANALYSIS_SKILL = "hedgehog-stock-research";
31
+
32
+ const BuildStockAiAnalysisMessageAgentToolSchema = {
33
+ type: "object",
34
+ additionalProperties: false,
35
+ required: ["stock_code", "stock_name"],
36
+ properties: {
37
+ stock_code: { type: "string", description: "股票代码" },
38
+ stock_name: { type: "string", description: "股票名称" },
39
+ market: { type: "string", description: "市场类型,默认 CN" }
40
+ }
41
+ };
42
+
43
+ export function normalizeStockCode(stock_code: string): string {
44
+ return stock_code.trim().toUpperCase().replace(/\.SS$/i, ".SH");
45
+ }
46
+
47
+ function resolveToolUserId(ctx?: { userId: string }): string {
48
+ return ctx?.userId || "default";
49
+ }
50
+
51
+ function buildStockAiAnalysisContent(args: BuildStockAiAnalysisMessageParams): string {
52
+ return [
53
+ `分析一下${args.stock_name}股票`,
54
+ "",
55
+ `股票代码:${normalizeStockCode(args.stock_code)}`,
56
+ `股票名称:${args.stock_name}`,
57
+ `市场:${args.market}`
58
+ ].join("\n");
59
+ }
60
+
61
+ function buildStockAiAnalysisMessage(args: BuildStockAiAnalysisMessageParams): string {
62
+ const stock_code = normalizeStockCode(args.stock_code);
63
+ const buildGeneratingSaveParams = () => JSON.stringify({
64
+ stock_code,
65
+ stock_name: args.stock_name,
66
+ market: args.market,
67
+ status: "generating",
68
+ content: ""
69
+ });
70
+ const buildFinalSaveParams = (status: "completed" | "failed") => JSON.stringify({
71
+ stock_code,
72
+ stock_name: args.stock_name,
73
+ market: args.market,
74
+ status,
75
+ content: "..."
76
+ });
77
+ return JSON.stringify({
78
+ cw_system_prompt: [
79
+ `开始分析前必须先调用 save_stock_ai_analysis,参数为 ${buildGeneratingSaveParams()};如果返回 skipped=true,必须立即停止,不要继续调用 skill 或生成正文。`,
80
+ `必须触发并使用 ${STOCK_AI_ANALYSIS_SKILL} skill 生成个股 AI 分析报告。`,
81
+ `生成成功后必须调用 save_stock_ai_analysis,参数为 ${buildFinalSaveParams("completed")},content 传最终完整个股分析正文。`,
82
+ `生成失败后必须调用 save_stock_ai_analysis,参数为 ${buildFinalSaveParams("failed")},content 存放完整错误信息。`,
83
+ "如果最终内容包含 [图表数据],正文必须已经包含所有对应图表占位符。"
84
+ ].join("\n"),
85
+ cw_context: JSON.stringify({
86
+ stock_code,
87
+ stock_name: args.stock_name,
88
+ market: args.market,
89
+ saveMode: "tool"
90
+ }),
91
+ cw_market: args.market,
92
+ cw_stock_code: stock_code,
93
+ cw_stock_name: args.stock_name,
94
+ cw_content: buildStockAiAnalysisContent({ ...args, stock_code }),
95
+ cw_output: [
96
+ `输出结构以 ${STOCK_AI_ANALYSIS_SKILL} skill 的交付模板为准。`,
97
+ CHART_OUTPUT_GUIDANCE
98
+ ].join("\n")
99
+ });
23
100
  }
24
101
 
25
102
  function selectLatestStockAnalysis(
103
+ db: ReturnType<typeof getDB>,
104
+ stock_code: string,
105
+ market: string
106
+ ): StockAiAnalysis | undefined {
107
+ return db.prepare(`
108
+ SELECT id, stock_code, stock_name, market, status, content, createdAt, updatedAt
109
+ FROM stock_ai_analysis
110
+ WHERE stock_code = ? AND market = ?
111
+ ORDER BY updatedAt DESC, createdAt DESC
112
+ LIMIT 1
113
+ `).get(stock_code, market) as StockAiAnalysis | undefined;
114
+ }
115
+
116
+ function selectLatestGeneratingStockAnalysis(
26
117
  db: ReturnType<typeof getDB>,
27
118
  userId: string,
28
- stockCode: string
119
+ stock_code: string,
120
+ market: string
121
+ ): StockAiAnalysis | undefined {
122
+ return db.prepare(`
123
+ SELECT id, stock_code, stock_name, market, status, content, createdAt, updatedAt
124
+ FROM stock_ai_analysis
125
+ WHERE userId = ? AND stock_code = ? AND market = ? AND status = 'generating'
126
+ ORDER BY updatedAt DESC, createdAt DESC
127
+ LIMIT 1
128
+ `).get(userId, stock_code, market) as StockAiAnalysis | undefined;
129
+ }
130
+
131
+ function selectStockAnalysisDetail(
132
+ db: ReturnType<typeof getDB>,
133
+ id: string
29
134
  ): StockAiAnalysis | undefined {
30
135
  return db.prepare(`
31
- SELECT id, stockCode, stockName, market, content, createdAt, updatedAt
136
+ SELECT id, stock_code, stock_name, market, status, content, createdAt, updatedAt
32
137
  FROM stock_ai_analysis
33
- WHERE userId = ? AND stockCode = ?
138
+ WHERE id = ?
34
139
  ORDER BY updatedAt DESC, createdAt DESC
35
140
  LIMIT 1
36
- `).get(userId, stockCode) as StockAiAnalysis | undefined;
141
+ `).get(id) as StockAiAnalysis | undefined;
142
+ }
143
+
144
+ function queryStockAnalysisStocks(
145
+ db: ReturnType<typeof getDB>,
146
+ userId: string,
147
+ market: string,
148
+ page: number,
149
+ pageSize: number
150
+ ): { rows: StockAiAnalysisStockSummary[]; total: number } {
151
+ const offset = (page - 1) * pageSize;
152
+ const rows = db.prepare(`
153
+ WITH grouped AS (
154
+ SELECT
155
+ stock_code,
156
+ market,
157
+ COUNT(*) AS analysisCount,
158
+ MAX(updatedAt || '|' || createdAt || '|' || id) AS latestKey
159
+ FROM stock_ai_analysis
160
+ WHERE userId = ? AND market = ?
161
+ GROUP BY stock_code, market
162
+ )
163
+ SELECT
164
+ a.stock_code,
165
+ a.stock_name,
166
+ a.market,
167
+ a.id AS latestAnalysisId,
168
+ a.status AS latestStatus,
169
+ a.createdAt AS latestCreatedAt,
170
+ a.updatedAt AS latestUpdatedAt,
171
+ g.analysisCount
172
+ FROM grouped g
173
+ JOIN stock_ai_analysis a
174
+ ON a.stock_code = g.stock_code
175
+ AND a.market = g.market
176
+ AND (a.updatedAt || '|' || a.createdAt || '|' || a.id) = g.latestKey
177
+ WHERE a.userId = ?
178
+ ORDER BY a.updatedAt DESC, a.createdAt DESC
179
+ LIMIT ? OFFSET ?
180
+ `).all(userId, market, userId, pageSize, offset) as StockAiAnalysisStockSummary[];
181
+ const countRow = db.prepare(`
182
+ SELECT COUNT(*) AS total
183
+ FROM (
184
+ SELECT 1
185
+ FROM stock_ai_analysis
186
+ WHERE userId = ? AND market = ?
187
+ GROUP BY stock_code, market
188
+ )
189
+ `).get(userId, market) as { total: number };
190
+ return { rows, total: countRow.total || 0 };
37
191
  }
38
192
 
39
193
  export function saveStockAiAnalysisRecord(
40
194
  db: ReturnType<typeof getDB>,
41
195
  userId: string,
42
196
  args: {
43
- stockCode: string;
44
- stockName: string;
197
+ stock_code: string;
198
+ stock_name?: string;
45
199
  market: string;
46
200
  content: string;
201
+ status?: string;
47
202
  }
48
203
  ): StockAiAnalysis {
49
- const stockCode = normalizeStockCode(args.stockCode);
204
+ const stock_code = normalizeStockCode(args.stock_code);
205
+ const status = args.status || "completed";
206
+ const stock_name = args.stock_name?.trim() || stock_code;
207
+ const content = status === "completed" ? ensureChartPlaceholdersInBody(args.content) : args.content.trim();
50
208
  const id = randomUUID();
51
209
 
210
+ const generating = status === "generating"
211
+ ? undefined
212
+ : selectLatestGeneratingStockAnalysis(db, userId, stock_code, args.market);
213
+ if (generating) {
214
+ db.prepare(`
215
+ UPDATE stock_ai_analysis
216
+ SET status = ?,
217
+ content = ?,
218
+ updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
219
+ WHERE id = ? AND userId = ?
220
+ `).run(status, content, generating.id, userId);
221
+ return selectStockAnalysisDetail(db, generating.id) as StockAiAnalysis;
222
+ }
223
+
52
224
  db.prepare(`
53
- INSERT INTO stock_ai_analysis (id, userId, stockCode, stockName, market, content)
54
- VALUES (?, ?, ?, ?, ?, ?)
55
- `).run(id, userId, stockCode, args.stockName, args.market, args.content);
225
+ INSERT INTO stock_ai_analysis (id, userId, stock_code, stock_name, market, status, content)
226
+ VALUES (?, ?, ?, ?, ?, ?, ?)
227
+ `).run(id, userId, stock_code, stock_name, args.market, status, content);
56
228
 
57
229
  return db.prepare(`
58
- SELECT id, stockCode, stockName, market, content, createdAt, updatedAt
230
+ SELECT id, stock_code, stock_name, market, status, content, createdAt, updatedAt
59
231
  FROM stock_ai_analysis
60
232
  WHERE userId = ? AND id = ?
61
233
  `).get(userId, id) as StockAiAnalysis;
62
234
  }
63
235
 
236
+ function tableForArticleAnalysis(analysisType: ArticleAiAnalysis["analysisType"]): string {
237
+ return analysisType === "verification" ? "news_fact_check_analysis" : "news_deep_reasoning_analysis";
238
+ }
239
+
64
240
  function selectLatestArticleAnalysis(
65
241
  db: ReturnType<typeof getDB>,
66
242
  userId: string,
67
243
  sourceId: string,
68
244
  analysisType: ArticleAiAnalysis["analysisType"],
69
- market: string
245
+ market: string = "CN"
246
+ ): ArticleAiAnalysis | undefined {
247
+ const table = tableForArticleAnalysis(analysisType);
248
+ if (analysisType === "deduction") {
249
+ return db.prepare(`
250
+ SELECT id, sourceId, ? AS analysisType, sourceTitle, market, status, content, createdAt, updatedAt
251
+ FROM ${table}
252
+ WHERE userId = ? AND sourceId = ? AND market = ?
253
+ ORDER BY updatedAt DESC, createdAt DESC
254
+ LIMIT 1
255
+ `).get(analysisType, userId, sourceId, market) as ArticleAiAnalysis | undefined;
256
+ }
257
+ return db.prepare(`
258
+ SELECT id, sourceId, ? AS analysisType, sourceTitle, status, content, createdAt, updatedAt
259
+ FROM ${table}
260
+ WHERE userId = ? AND sourceId = ?
261
+ ORDER BY updatedAt DESC, createdAt DESC
262
+ LIMIT 1
263
+ `).get(analysisType, userId, sourceId) as ArticleAiAnalysis | undefined;
264
+ }
265
+
266
+ function selectLatestGeneratingArticleAnalysis(
267
+ db: ReturnType<typeof getDB>,
268
+ userId: string,
269
+ sourceId: string,
270
+ analysisType: ArticleAiAnalysis["analysisType"]
70
271
  ): ArticleAiAnalysis | undefined {
272
+ const table = tableForArticleAnalysis(analysisType);
273
+ if (analysisType === "deduction") {
274
+ return db.prepare(`
275
+ SELECT id, sourceId, ? AS analysisType, sourceTitle, market, status, content, createdAt, updatedAt
276
+ FROM ${table}
277
+ WHERE userId = ? AND sourceId = ? AND status = 'generating'
278
+ ORDER BY updatedAt DESC, createdAt DESC
279
+ LIMIT 1
280
+ `).get(analysisType, userId, sourceId) as ArticleAiAnalysis | undefined;
281
+ }
71
282
  return db.prepare(`
72
- SELECT id, sourceId, analysisType, market, content, createdAt, updatedAt
73
- FROM article_ai_analysis
74
- WHERE userId = ? AND sourceId = ? AND analysisType = ? AND market = ?
283
+ SELECT id, sourceId, ? AS analysisType, sourceTitle, status, content, createdAt, updatedAt
284
+ FROM ${table}
285
+ WHERE userId = ? AND sourceId = ? AND status = 'generating'
75
286
  ORDER BY updatedAt DESC, createdAt DESC
76
287
  LIMIT 1
77
- `).get(userId, sourceId, analysisType, market) as ArticleAiAnalysis | undefined;
288
+ `).get(analysisType, userId, sourceId) as ArticleAiAnalysis | undefined;
78
289
  }
79
290
 
80
291
  function saveArticleAiAnalysisRecord(
@@ -82,63 +293,100 @@ function saveArticleAiAnalysisRecord(
82
293
  userId: string,
83
294
  args: {
84
295
  sourceId: string;
85
- analysisType: ArticleAiAnalysis["analysisType"];
86
- market: string;
296
+ sourceTitle: string;
87
297
  content: string;
298
+ status: string;
88
299
  }
89
300
  ): ArticleAiAnalysis {
90
301
  const id = randomUUID();
91
302
 
92
303
  db.prepare(`
93
- INSERT INTO article_ai_analysis (id, sourceId, userId, analysisType, market, content)
304
+ INSERT INTO news_fact_check_analysis (id, sourceId, sourceTitle, userId, status, content)
94
305
  VALUES (?, ?, ?, ?, ?, ?)
95
- ON CONFLICT(sourceId, userId, analysisType, market) DO UPDATE SET
306
+ ON CONFLICT(sourceId, userId) DO UPDATE SET
307
+ sourceTitle = CASE WHEN excluded.sourceTitle != '' THEN excluded.sourceTitle ELSE news_fact_check_analysis.sourceTitle END,
308
+ status = excluded.status,
309
+ content = excluded.content,
310
+ updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
311
+ `).run(id, args.sourceId, args.sourceTitle, userId, args.status, args.content);
312
+
313
+ return db.prepare(`
314
+ SELECT id, sourceId, 'verification' AS analysisType, sourceTitle, status, content, createdAt, updatedAt
315
+ FROM news_fact_check_analysis
316
+ WHERE userId = ? AND sourceId = ?
317
+ `).get(userId, args.sourceId) as ArticleAiAnalysis;
318
+ }
319
+
320
+ function saveArticleDeepReasoningRecord(
321
+ db: ReturnType<typeof getDB>,
322
+ userId: string,
323
+ args: {
324
+ sourceId: string;
325
+ sourceTitle: string;
326
+ market: string;
327
+ content: string;
328
+ status: string;
329
+ }
330
+ ): ArticleAiAnalysis {
331
+ const id = randomUUID();
332
+
333
+ db.prepare(`
334
+ INSERT INTO news_deep_reasoning_analysis (id, sourceId, sourceTitle, userId, market, status, content)
335
+ VALUES (?, ?, ?, ?, ?, ?, ?)
336
+ ON CONFLICT(sourceId, userId, market) DO UPDATE SET
337
+ sourceTitle = CASE WHEN excluded.sourceTitle != '' THEN excluded.sourceTitle ELSE news_deep_reasoning_analysis.sourceTitle END,
338
+ status = excluded.status,
96
339
  content = excluded.content,
97
340
  updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
98
- `).run(id, args.sourceId, userId, args.analysisType, args.market, args.content);
341
+ `).run(id, args.sourceId, args.sourceTitle, userId, args.market, args.status, args.content);
99
342
 
100
343
  return db.prepare(`
101
- SELECT id, sourceId, analysisType, market, content, createdAt, updatedAt
102
- FROM article_ai_analysis
103
- WHERE userId = ? AND sourceId = ? AND analysisType = ? AND market = ?
104
- `).get(userId, args.sourceId, args.analysisType, args.market) as ArticleAiAnalysis;
344
+ SELECT id, sourceId, 'deduction' AS analysisType, sourceTitle, market, status, content, createdAt, updatedAt
345
+ FROM news_deep_reasoning_analysis
346
+ WHERE userId = ? AND sourceId = ? AND market = ?
347
+ `).get(userId, args.sourceId, args.market) as ArticleAiAnalysis;
105
348
  }
106
349
 
107
350
  export const stockAnalysisTools: Record<string, RuntimeTool> = {
108
351
  get_stock_ai_analysis: {
109
352
  name: "get_stock_ai_analysis",
110
- description: "读取股票 AI 分析的最新一条历史记录;不触发模型分析。",
353
+ description: "查询指定股票最新一条 AI 分析详情。该接口仅读取已持久化的分析结果,返回完整 content,不触发新的模型生成流程。",
111
354
  parameters: GetStockAiAnalysisParamsSchema,
112
355
  registerTool: false,
113
356
  async execute(params, ctx) {
114
357
  const args = GetStockAiAnalysisParamsSchema.parse(params);
115
358
  const db = getDB();
116
- const data = selectLatestStockAnalysis(db, ctx.userId, normalizeStockCode(args.stockCode));
359
+ const data = selectLatestStockAnalysis(db, normalizeStockCode(args.stock_code), args.market);
117
360
  return JSON.stringify({ success: true, data: data || null });
118
361
  }
119
362
  },
120
363
  query_stock_ai_analysis_history: {
121
364
  name: "query_stock_ai_analysis_history",
122
- description: "分页读取股票 AI 分析历史记录;不触发模型分析。",
365
+ description: "分页查询股票 AI 分析记录列表。支持按股票代码和市场过滤,返回记录标识、股票信息、状态和时间字段;列表结果不包含 content,详情内容请使用详情查询接口获取。",
123
366
  parameters: QueryStockAiAnalysisHistoryParamsSchema,
124
367
  registerTool: false,
125
368
  async execute(params, ctx) {
126
- const args = QueryStockAiAnalysisHistoryParamsSchema.parse(params);
369
+ const args = QueryStockAiAnalysisHistoryParamsSchema.parse(params ?? {});
127
370
  const db = getDB();
128
- const stockCode = normalizeStockCode(args.stockCode);
129
371
  const offset = (args.page - 1) * args.pageSize;
372
+ const conditions = ["userId = ?", "market = ?"];
373
+ const queryParams: unknown[] = [resolveToolUserId(ctx), args.market];
374
+ if (args.stock_code) {
375
+ conditions.push("stock_code = ?");
376
+ queryParams.push(normalizeStockCode(args.stock_code));
377
+ }
130
378
  const rows = db.prepare(`
131
- SELECT id, stockCode, stockName, market, content, createdAt, updatedAt
379
+ SELECT id, stock_code, stock_name, market, status, createdAt, updatedAt
132
380
  FROM stock_ai_analysis
133
- WHERE userId = ? AND stockCode = ? AND market = ?
381
+ WHERE ${conditions.join(" AND ")}
134
382
  ORDER BY updatedAt DESC, createdAt DESC
135
383
  LIMIT ? OFFSET ?
136
- `).all(ctx.userId, stockCode, args.market, args.pageSize, offset) as StockAiAnalysis[];
384
+ `).all(...queryParams, args.pageSize, offset);
137
385
  const countRow = db.prepare(`
138
386
  SELECT COUNT(*) AS total
139
387
  FROM stock_ai_analysis
140
- WHERE userId = ? AND stockCode = ? AND market = ?
141
- `).get(ctx.userId, stockCode, args.market) as { total: number };
388
+ WHERE ${conditions.join(" AND ")}
389
+ `).get(...queryParams) as { total: number };
142
390
  const total = countRow.total || 0;
143
391
 
144
392
  return JSON.stringify({
@@ -153,39 +401,189 @@ export const stockAnalysisTools: Record<string, RuntimeTool> = {
153
401
  });
154
402
  }
155
403
  },
404
+ query_stock_ai_analysis_stocks: {
405
+ name: "query_stock_ai_analysis_stocks",
406
+ description: "分页查询当前用户所有已经产生过个股 AI 分析记录的股票列表。按股票代码和市场去重,返回每只股票最近一次分析记录 ID、最近状态、最近更新时间和累计分析次数;不返回分析正文 content。",
407
+ parameters: QueryStockAiAnalysisStocksParamsSchema,
408
+ registerTool: false,
409
+ async execute(params, ctx) {
410
+ const args = QueryStockAiAnalysisStocksParamsSchema.parse(params ?? {});
411
+ const db = getDB();
412
+ const { rows, total } = queryStockAnalysisStocks(db, resolveToolUserId(ctx), args.market, args.page, args.pageSize);
413
+ return JSON.stringify({
414
+ success: true,
415
+ data: rows,
416
+ pagination: {
417
+ page: args.page,
418
+ pageSize: args.pageSize,
419
+ total,
420
+ totalPages: Math.ceil(total / args.pageSize)
421
+ }
422
+ });
423
+ }
424
+ },
425
+ get_stock_ai_analysis_detail: {
426
+ name: "get_stock_ai_analysis_detail",
427
+ description: "根据分析记录 ID 查询股票 AI 分析详情。返回指定记录的完整分析内容 content 及元数据,用于详情页展示或历史记录回放。",
428
+ parameters: GetStockAiAnalysisDetailParamsSchema,
429
+ registerTool: false,
430
+ async execute(params, ctx) {
431
+ const args = GetStockAiAnalysisDetailParamsSchema.parse(params);
432
+ const db = getDB();
433
+ const data = selectStockAnalysisDetail(db, args.id);
434
+ return JSON.stringify({ success: true, data: data || null });
435
+ }
436
+ },
437
+ build_stock_ai_analysis_message: {
438
+ name: "build_stock_ai_analysis_message",
439
+ label: "构建个股分析消息",
440
+ description: "根据股票代码、名称和市场构建用于主动 RPC 发起 Agent 个股 AI 分析任务的标准消息。该工具只返回提示词消息体,不触发定时任务,也不保存分析结果。",
441
+ parameters: BuildStockAiAnalysisMessageAgentToolSchema,
442
+ registerTool: false,
443
+ async execute(params, ctx) {
444
+ const args = BuildStockAiAnalysisMessageParamsSchema.parse(params);
445
+ const db = getDB();
446
+ const userId = resolveToolUserId(ctx);
447
+ const stock_code = normalizeStockCode(args.stock_code);
448
+ const generating = selectLatestGeneratingStockAnalysis(db, userId, stock_code, args.market);
449
+ if (generating) {
450
+ return JSON.stringify({
451
+ success: true,
452
+ skipped: true,
453
+ reason: "already_generating",
454
+ data: generating
455
+ });
456
+ }
457
+ const message = buildStockAiAnalysisMessage({ ...args, stock_code });
458
+ return JSON.stringify({
459
+ success: true,
460
+ data: {
461
+ message,
462
+ payload: JSON.parse(message),
463
+ stock_code,
464
+ saveParams: {
465
+ stock_code,
466
+ stock_name: args.stock_name,
467
+ market: args.market
468
+ },
469
+ skill: STOCK_AI_ANALYSIS_SKILL
470
+ }
471
+ });
472
+ }
473
+ },
156
474
  save_stock_ai_analysis: {
157
475
  name: "save_stock_ai_analysis",
158
- description: "追加保存一条股票 AI 分析历史记录。",
476
+ description: [
477
+ "保存个股 AI 分析结果。生成前必须先以 status=generating、content=\"\" 调用,并传入 stock_code、stock_name、market;生成成功后以 status=completed 保存完整正文 content;生成失败后以 status=failed 保存完整错误信息。",
478
+ "个股分析的 cw_output 需要包含图表相关要求:",
479
+ CHART_OUTPUT_GUIDANCE
480
+ ].join("\n"),
159
481
  parameters: SaveStockAiAnalysisParamsSchema,
160
- registerTool: false,
482
+ registerTool: true,
161
483
  async execute(params, ctx) {
162
484
  const args = SaveStockAiAnalysisParamsSchema.parse(params);
163
485
  const db = getDB();
164
- const data = saveStockAiAnalysisRecord(db, ctx.userId, args);
486
+ const userId = resolveToolUserId(ctx);
487
+ if (args.status === "generating") {
488
+ const stock_code = normalizeStockCode(args.stock_code);
489
+ const generating = selectLatestGeneratingStockAnalysis(db, userId, stock_code, args.market);
490
+ if (generating) {
491
+ return JSON.stringify({ success: true, skipped: true, reason: "already_generating", data: generating });
492
+ }
493
+ }
494
+ const data = saveStockAiAnalysisRecord(db, userId, args);
165
495
  return JSON.stringify({ success: true, data });
166
496
  }
167
497
  },
168
498
  get_article_ai_analysis: {
169
499
  name: "get_article_ai_analysis",
170
- description: "读取文章 AI 分析结果;支持信息求证与深度推演,不触发模型分析。",
500
+ description: "根据资讯来源 ID 查询文章 AI 分析详情。支持信息求证和深度推演两类结果,返回完整 content;该接口仅读取已持久化数据,不触发新的模型生成流程。",
171
501
  parameters: GetArticleAiAnalysisParamsSchema,
172
502
  registerTool: false,
173
503
  async execute(params, ctx) {
174
504
  const args = GetArticleAiAnalysisParamsSchema.parse(params);
175
505
  const db = getDB();
176
- const data = selectLatestArticleAnalysis(db, ctx.userId, args.id, args.analysisType, args.market);
506
+ const data = selectLatestArticleAnalysis(db, resolveToolUserId(ctx), args.sourceId, args.analysisType, args.market);
177
507
  return JSON.stringify({ success: true, data: data || null });
178
508
  }
179
509
  },
180
- save_article_ai_analysis: {
181
- name: "save_article_ai_analysis",
182
- description: "保存文章 AI 分析结果;支持信息求证与深度推演。",
183
- parameters: SaveArticleAiAnalysisParamsSchema,
510
+ query_article_ai_analysis_history: {
511
+ name: "query_article_ai_analysis_history",
512
+ description: "分页查询文章 AI 分析记录列表。支持按分析类型过滤,返回记录标识、sourceId、状态和时间字段;列表结果不包含 content,详情内容请按 sourceId 查询。",
513
+ parameters: QueryArticleAiAnalysisHistoryParamsSchema,
184
514
  registerTool: false,
515
+ async execute(params, ctx) {
516
+ const args = QueryArticleAiAnalysisHistoryParamsSchema.parse(params ?? {});
517
+ const db = getDB();
518
+ const userId = resolveToolUserId(ctx);
519
+ const table = tableForArticleAnalysis(args.analysisType);
520
+ const offset = (args.page - 1) * args.pageSize;
521
+ const marketCondition = args.analysisType === "deduction" ? " AND market = ?" : "";
522
+ const queryParams = args.analysisType === "deduction"
523
+ ? [args.analysisType, userId, args.market, args.pageSize, offset]
524
+ : [args.analysisType, userId, args.pageSize, offset];
525
+ const rows = db.prepare(`
526
+ SELECT id, sourceId, ? AS analysisType, sourceTitle, ${args.analysisType === "deduction" ? "market," : ""} status, createdAt, updatedAt
527
+ FROM ${table}
528
+ WHERE userId = ?${marketCondition}
529
+ ORDER BY updatedAt DESC, createdAt DESC
530
+ LIMIT ? OFFSET ?
531
+ `).all(...queryParams);
532
+ const countParams = args.analysisType === "deduction" ? [userId, args.market] : [userId];
533
+ const countRow = db.prepare(`
534
+ SELECT COUNT(*) AS total
535
+ FROM ${table}
536
+ WHERE userId = ?${marketCondition}
537
+ `).get(...countParams) as { total: number };
538
+ const total = countRow.total || 0;
539
+
540
+ return JSON.stringify({
541
+ success: true,
542
+ data: rows,
543
+ pagination: {
544
+ page: args.page,
545
+ pageSize: args.pageSize,
546
+ total,
547
+ totalPages: Math.ceil(total / args.pageSize)
548
+ }
549
+ });
550
+ }
551
+ },
552
+ save_information_verification: {
553
+ name: "save_information_verification",
554
+ description: "保存新闻信息求证结果。生成前必须先以 status=generating、content=\"\" 调用,并传入 sourceId、sourceTitle;生成成功后以 status=completed 保存完整正文 content;生成失败后以 status=failed 保存完整错误信息。",
555
+ parameters: SaveArticleAiAnalysisParamsSchema,
556
+ registerTool: true,
185
557
  async execute(params, ctx) {
186
558
  const args = SaveArticleAiAnalysisParamsSchema.parse(params);
187
559
  const db = getDB();
188
- const data = saveArticleAiAnalysisRecord(db, ctx.userId, { ...args, sourceId: args.id });
560
+ const userId = resolveToolUserId(ctx);
561
+ if (args.status === "generating") {
562
+ const generating = selectLatestGeneratingArticleAnalysis(db, userId, args.sourceId, "verification");
563
+ if (generating) {
564
+ return JSON.stringify({ success: true, skipped: true, reason: "already_generating", data: generating });
565
+ }
566
+ }
567
+ const data = saveArticleAiAnalysisRecord(db, userId, args);
568
+ return JSON.stringify({ success: true, data });
569
+ }
570
+ },
571
+ save_article_deep_reasoning_analysis: {
572
+ name: "save_article_deep_reasoning_analysis",
573
+ description: "保存新闻深度推演结果。生成前必须先以 status=generating、content=\"\" 调用,并传入 sourceId、sourceTitle、sourceContent、market;生成成功后以 status=completed 保存完整正文 content;生成失败后以 status=failed 保存完整错误信息。",
574
+ parameters: SaveArticleDeepReasoningParamsSchema,
575
+ registerTool: true,
576
+ async execute(params, ctx) {
577
+ const args = SaveArticleDeepReasoningParamsSchema.parse(params);
578
+ const db = getDB();
579
+ const userId = resolveToolUserId(ctx);
580
+ if (args.status === "generating") {
581
+ const generating = selectLatestGeneratingArticleAnalysis(db, userId, args.sourceId, "deduction");
582
+ if (generating) {
583
+ return JSON.stringify({ success: true, skipped: true, reason: "already_generating", data: generating });
584
+ }
585
+ }
586
+ const data = saveArticleDeepReasoningRecord(db, userId, args);
189
587
  return JSON.stringify({ success: true, data });
190
588
  }
191
589
  }
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+
3
+ export const StockBasicItemSchema = z.object({
4
+ act_ent_type: z.string().optional().default(""),
5
+ act_name: z.string().optional().default(""),
6
+ area: z.string().optional().default(""),
7
+ cnspell: z.string().optional().default(""),
8
+ curr_type: z.string().optional().default(""),
9
+ enname: z.string().optional().default(""),
10
+ exchange: z.string().trim().min(1),
11
+ fullname: z.string().optional().default(""),
12
+ industry: z.string().optional().default(""),
13
+ is_hs: z.string().optional().default(""),
14
+ list_date: z.string().optional().default(""),
15
+ market: z.string().optional().default(""),
16
+ name: z.string().trim().min(1),
17
+ stock_code: z.string().trim().min(1),
18
+ symbol: z.string().trim().min(1)
19
+ });
20
+ export type StockBasicItem = z.infer<typeof StockBasicItemSchema>;
21
+
22
+ export const SyncStockBasicParamsSchema = z.object({
23
+ stocks: z.array(StockBasicItemSchema).min(1)
24
+ });
25
+ export type SyncStockBasicParams = z.infer<typeof SyncStockBasicParamsSchema>;
26
+
27
+ export const GetStockBasicListParamsSchema = z.object({}).nullish();
28
+ export type GetStockBasicListParams = z.infer<typeof GetStockBasicListParamsSchema>;
29
+
30
+ export const GetStockBasicInfoParamsSchema = z.object({
31
+ stock_code: z.string().trim().min(1).describe("股票代码")
32
+ });
33
+ export type GetStockBasicInfoParams = z.infer<typeof GetStockBasicInfoParamsSchema>;