@hedgehog-finance/hedgehog-plugin 1.0.20 → 1.0.21

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.
@@ -0,0 +1,30 @@
1
+ import { z } from "openclaw/plugin-sdk/zod";
2
+ export const GetStockAiAnalysisParamsSchema = z.object({
3
+ stockCode: z.string().trim().min(1).describe("股票代码"),
4
+ market: z.string().trim().min(1).default("CN").describe("市场类型,默认 CN")
5
+ });
6
+ export const QueryStockAiAnalysisHistoryParamsSchema = z.object({
7
+ stockCode: z.string().trim().min(1).describe("股票代码"),
8
+ market: z.string().trim().min(1).default("CN").describe("市场类型,默认 CN"),
9
+ page: z.number().int().min(1).default(1).describe("页码"),
10
+ pageSize: z.number().int().min(1).max(50).default(20).describe("每页数量")
11
+ });
12
+ export const SaveStockAiAnalysisParamsSchema = z.object({
13
+ stockCode: z.string().trim().min(1).describe("股票代码"),
14
+ stockName: z.string().trim().min(1).describe("股票名称"),
15
+ market: z.string().trim().min(1).default("CN").describe("市场类型,默认 CN"),
16
+ content: z.string().trim().min(1).describe("AI 分析内容")
17
+ });
18
+ export const ArticleAiAnalysisKindSchema = z.enum(["verification", "deduction"]);
19
+ export const GetArticleAiAnalysisParamsSchema = z.object({
20
+ id: z.string().trim().min(1).describe("文章来源 ID,例如 news-5、report-5、announce-5"),
21
+ analysisType: ArticleAiAnalysisKindSchema.describe("分析类型:verification 信息求证,deduction 深度推演"),
22
+ market: z.string().trim().min(1).default("CN").describe("市场类型,默认 CN")
23
+ });
24
+ export const SaveArticleAiAnalysisParamsSchema = z.object({
25
+ id: z.string().trim().min(1).describe("文章来源 ID,例如 news-5、report-5、announce-5"),
26
+ analysisType: ArticleAiAnalysisKindSchema.describe("分析类型:verification 信息求证,deduction 深度推演"),
27
+ market: z.string().trim().min(1).default("CN").describe("市场类型,默认 CN"),
28
+ content: z.string().trim().min(1).describe("AI 分析内容")
29
+ });
30
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../../src/features/stockAnalysis/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { getDB } from "../../core/database.js";
2
+ import { StockAiAnalysis } from "./schema.js";
3
+ interface RuntimeTool {
4
+ name: string;
5
+ description: string;
6
+ parameters: unknown;
7
+ registerTool?: boolean;
8
+ execute(params: unknown, ctx: {
9
+ userId: string;
10
+ }): Promise<string>;
11
+ }
12
+ export declare function normalizeStockCode(stockCode: string): string;
13
+ export declare function saveStockAiAnalysisRecord(db: ReturnType<typeof getDB>, userId: string, args: {
14
+ stockCode: string;
15
+ stockName: string;
16
+ market: string;
17
+ content: string;
18
+ }): StockAiAnalysis;
19
+ export declare const stockAnalysisTools: Record<string, RuntimeTool>;
20
+ export {};
@@ -0,0 +1,138 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDB } from "../../core/database.js";
3
+ import { GetArticleAiAnalysisParamsSchema, GetStockAiAnalysisParamsSchema, QueryStockAiAnalysisHistoryParamsSchema, SaveArticleAiAnalysisParamsSchema, SaveStockAiAnalysisParamsSchema } from "./schema.js";
4
+ export function normalizeStockCode(stockCode) {
5
+ return stockCode.trim().toUpperCase().replace(/\.SS$/i, ".SH");
6
+ }
7
+ function selectLatestStockAnalysis(db, userId, stockCode) {
8
+ return db.prepare(`
9
+ SELECT id, stockCode, stockName, market, content, createdAt, updatedAt
10
+ FROM stock_ai_analysis
11
+ WHERE userId = ? AND stockCode = ?
12
+ ORDER BY updatedAt DESC, createdAt DESC
13
+ LIMIT 1
14
+ `).get(userId, stockCode);
15
+ }
16
+ export function saveStockAiAnalysisRecord(db, userId, args) {
17
+ const stockCode = normalizeStockCode(args.stockCode);
18
+ const id = randomUUID();
19
+ db.prepare(`
20
+ INSERT INTO stock_ai_analysis (id, userId, stockCode, stockName, market, content)
21
+ VALUES (?, ?, ?, ?, ?, ?)
22
+ `).run(id, userId, stockCode, args.stockName, args.market, args.content);
23
+ return db.prepare(`
24
+ SELECT id, stockCode, stockName, market, content, createdAt, updatedAt
25
+ FROM stock_ai_analysis
26
+ WHERE userId = ? AND id = ?
27
+ `).get(userId, id);
28
+ }
29
+ function selectLatestArticleAnalysis(db, userId, sourceId, analysisType, market) {
30
+ return db.prepare(`
31
+ SELECT id, sourceId, analysisType, market, content, createdAt, updatedAt
32
+ FROM article_ai_analysis
33
+ WHERE userId = ? AND sourceId = ? AND analysisType = ? AND market = ?
34
+ ORDER BY updatedAt DESC, createdAt DESC
35
+ LIMIT 1
36
+ `).get(userId, sourceId, analysisType, market);
37
+ }
38
+ function saveArticleAiAnalysisRecord(db, userId, args) {
39
+ const id = randomUUID();
40
+ db.prepare(`
41
+ INSERT INTO article_ai_analysis (id, sourceId, userId, analysisType, market, content)
42
+ VALUES (?, ?, ?, ?, ?, ?)
43
+ ON CONFLICT(sourceId, userId, analysisType, market) DO UPDATE SET
44
+ content = excluded.content,
45
+ updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
46
+ `).run(id, args.sourceId, userId, args.analysisType, args.market, args.content);
47
+ return db.prepare(`
48
+ SELECT id, sourceId, analysisType, market, content, createdAt, updatedAt
49
+ FROM article_ai_analysis
50
+ WHERE userId = ? AND sourceId = ? AND analysisType = ? AND market = ?
51
+ `).get(userId, args.sourceId, args.analysisType, args.market);
52
+ }
53
+ export const stockAnalysisTools = {
54
+ get_stock_ai_analysis: {
55
+ name: "get_stock_ai_analysis",
56
+ description: "读取股票 AI 分析的最新一条历史记录;不触发模型分析。",
57
+ parameters: GetStockAiAnalysisParamsSchema,
58
+ registerTool: false,
59
+ async execute(params, ctx) {
60
+ const args = GetStockAiAnalysisParamsSchema.parse(params);
61
+ const db = getDB();
62
+ const data = selectLatestStockAnalysis(db, ctx.userId, normalizeStockCode(args.stockCode));
63
+ return JSON.stringify({ success: true, data: data || null });
64
+ }
65
+ },
66
+ query_stock_ai_analysis_history: {
67
+ name: "query_stock_ai_analysis_history",
68
+ description: "分页读取股票 AI 分析历史记录;不触发模型分析。",
69
+ parameters: QueryStockAiAnalysisHistoryParamsSchema,
70
+ registerTool: false,
71
+ async execute(params, ctx) {
72
+ const args = QueryStockAiAnalysisHistoryParamsSchema.parse(params);
73
+ const db = getDB();
74
+ const stockCode = normalizeStockCode(args.stockCode);
75
+ const offset = (args.page - 1) * args.pageSize;
76
+ const rows = db.prepare(`
77
+ SELECT id, stockCode, stockName, market, content, createdAt, updatedAt
78
+ FROM stock_ai_analysis
79
+ WHERE userId = ? AND stockCode = ? AND market = ?
80
+ ORDER BY updatedAt DESC, createdAt DESC
81
+ LIMIT ? OFFSET ?
82
+ `).all(ctx.userId, stockCode, args.market, args.pageSize, offset);
83
+ const countRow = db.prepare(`
84
+ SELECT COUNT(*) AS total
85
+ FROM stock_ai_analysis
86
+ WHERE userId = ? AND stockCode = ? AND market = ?
87
+ `).get(ctx.userId, stockCode, args.market);
88
+ const total = countRow.total || 0;
89
+ return JSON.stringify({
90
+ success: true,
91
+ data: rows,
92
+ pagination: {
93
+ page: args.page,
94
+ pageSize: args.pageSize,
95
+ total,
96
+ totalPages: Math.ceil(total / args.pageSize)
97
+ }
98
+ });
99
+ }
100
+ },
101
+ save_stock_ai_analysis: {
102
+ name: "save_stock_ai_analysis",
103
+ description: "追加保存一条股票 AI 分析历史记录。",
104
+ parameters: SaveStockAiAnalysisParamsSchema,
105
+ registerTool: false,
106
+ async execute(params, ctx) {
107
+ const args = SaveStockAiAnalysisParamsSchema.parse(params);
108
+ const db = getDB();
109
+ const data = saveStockAiAnalysisRecord(db, ctx.userId, args);
110
+ return JSON.stringify({ success: true, data });
111
+ }
112
+ },
113
+ get_article_ai_analysis: {
114
+ name: "get_article_ai_analysis",
115
+ description: "读取文章 AI 分析结果;支持信息求证与深度推演,不触发模型分析。",
116
+ parameters: GetArticleAiAnalysisParamsSchema,
117
+ registerTool: false,
118
+ async execute(params, ctx) {
119
+ const args = GetArticleAiAnalysisParamsSchema.parse(params);
120
+ const db = getDB();
121
+ const data = selectLatestArticleAnalysis(db, ctx.userId, args.id, args.analysisType, args.market);
122
+ return JSON.stringify({ success: true, data: data || null });
123
+ }
124
+ },
125
+ save_article_ai_analysis: {
126
+ name: "save_article_ai_analysis",
127
+ description: "保存文章 AI 分析结果;支持信息求证与深度推演。",
128
+ parameters: SaveArticleAiAnalysisParamsSchema,
129
+ registerTool: false,
130
+ async execute(params, ctx) {
131
+ const args = SaveArticleAiAnalysisParamsSchema.parse(params);
132
+ const db = getDB();
133
+ const data = saveArticleAiAnalysisRecord(db, ctx.userId, { ...args, sourceId: args.id });
134
+ return JSON.stringify({ success: true, data });
135
+ }
136
+ }
137
+ };
138
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../../../src/features/stockAnalysis/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAC/C,OAAO,EAEN,gCAAgC,EAChC,8BAA8B,EAC9B,uCAAuC,EACvC,iCAAiC,EACjC,+BAA+B,EAE/B,MAAM,aAAa,CAAC;AAUrB,MAAM,UAAU,kBAAkB,CAAC,SAAiB;IACnD,OAAO,SAAS,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AAChE,CAAC;AAED,SAAS,yBAAyB,CACjC,EAA4B,EAC5B,MAAc,EACd,SAAiB;IAEjB,OAAO,EAAE,CAAC,OAAO,CAAC;;;;;;EAMjB,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAgC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,yBAAyB,CACxC,EAA4B,EAC5B,MAAc,EACd,IAKC;IAED,MAAM,SAAS,GAAG,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACrD,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;IAExB,EAAE,CAAC,OAAO,CAAC;;;EAGV,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAEzE,OAAO,EAAE,CAAC,OAAO,CAAC;;;;EAIjB,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,CAAoB,CAAC;AACvC,CAAC;AAED,SAAS,2BAA2B,CACnC,EAA4B,EAC5B,MAAc,EACd,QAAgB,EAChB,YAA+C,EAC/C,MAAc;IAEd,OAAO,EAAE,CAAC,OAAO,CAAC;;;;;;EAMjB,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,CAAkC,CAAC;AACjF,CAAC;AAED,SAAS,2BAA2B,CACnC,EAA4B,EAC5B,MAAc,EACd,IAKC;IAED,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;IAExB,EAAE,CAAC,OAAO,CAAC;;;;;;EAMV,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAEhF,OAAO,EAAE,CAAC,OAAO,CAAC;;;;EAIjB,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,MAAM,CAAsB,CAAC;AACpF,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAgC;IAC9D,qBAAqB,EAAE;QACtB,IAAI,EAAE,uBAAuB;QAC7B,WAAW,EAAE,8BAA8B;QAC3C,UAAU,EAAE,8BAA8B;QAC1C,YAAY,EAAE,KAAK;QACnB,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG;YACxB,MAAM,IAAI,GAAG,8BAA8B,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC1D,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;YACnB,MAAM,IAAI,GAAG,yBAAyB,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YAC3F,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;QAC9D,CAAC;KACD;IACD,+BAA+B,EAAE;QAChC,IAAI,EAAE,iCAAiC;QACvC,WAAW,EAAE,2BAA2B;QACxC,UAAU,EAAE,uCAAuC;QACnD,YAAY,EAAE,KAAK;QACnB,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG;YACxB,MAAM,IAAI,GAAG,uCAAuC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACnE,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;YACnB,MAAM,SAAS,GAAG,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACrD,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;YAC/C,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC;;;;;;IAMvB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAsB,CAAC;YACvF,MAAM,QAAQ,GAAG,EAAE,CAAC,OAAO,CAAC;;;;IAI3B,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,CAAsB,CAAC;YAChE,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC;YAElC,OAAO,IAAI,CAAC,SAAS,CAAC;gBACrB,OAAO,EAAE,IAAI;gBACb,IAAI,EAAE,IAAI;gBACV,UAAU,EAAE;oBACX,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,KAAK;oBACL,UAAU,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;iBAC5C;aACD,CAAC,CAAC;QACJ,CAAC;KACD;IACD,sBAAsB,EAAE;QACvB,IAAI,EAAE,wBAAwB;QAC9B,WAAW,EAAE,qBAAqB;QAClC,UAAU,EAAE,+BAA+B;QAC3C,YAAY,EAAE,KAAK;QACnB,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG;YACxB,MAAM,IAAI,GAAG,+BAA+B,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC3D,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;YACnB,MAAM,IAAI,GAAG,yBAAyB,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAC7D,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,CAAC;KACD;IACD,uBAAuB,EAAE;QACxB,IAAI,EAAE,yBAAyB;QAC/B,WAAW,EAAE,mCAAmC;QAChD,UAAU,EAAE,gCAAgC;QAC5C,YAAY,EAAE,KAAK;QACnB,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG;YACxB,MAAM,IAAI,GAAG,gCAAgC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5D,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;YACnB,MAAM,IAAI,GAAG,2BAA2B,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAClG,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;QAC9D,CAAC;KACD;IACD,wBAAwB,EAAE;QACzB,IAAI,EAAE,0BAA0B;QAChC,WAAW,EAAE,2BAA2B;QACxC,UAAU,EAAE,iCAAiC;QAC7C,YAAY,EAAE,KAAK;QACnB,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG;YACxB,MAAM,IAAI,GAAG,iCAAiC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC7D,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;YACnB,MAAM,IAAI,GAAG,2BAA2B,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;YACzF,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,CAAC;KACD;CACD,CAAC"}
@@ -61,7 +61,6 @@ export declare const StockClassificationSchema: z.ZodObject<{
61
61
  }>, "many">;
62
62
  weight: z.ZodDefault<z.ZodNumber>;
63
63
  }, "strip", z.ZodTypeAny, {
64
- weight: number;
65
64
  industry: {
66
65
  name: string;
67
66
  weight: number;
@@ -70,6 +69,7 @@ export declare const StockClassificationSchema: z.ZodObject<{
70
69
  name: string;
71
70
  weight: number;
72
71
  }[];
72
+ weight: number;
73
73
  }, {
74
74
  industry: {
75
75
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hedgehog-finance/hedgehog-plugin",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "Hedgehog App WebSocket channel and Watchlist Tools for OpenClaw",
5
5
  "keywords": [
6
6
  "bot",
package/src/channel.ts CHANGED
@@ -14,11 +14,13 @@ import type {
14
14
  } from "openclaw/plugin-sdk/channel-contract";
15
15
  import { getHedgehogRuntime } from "./runtime.js";
16
16
  import { logger } from "./core/logger.js";
17
+ import { getDB } from "./core/database.js";
17
18
  import type {
18
19
  HedgehogFinanceResolvedAccount,
19
20
  RelayInboundMessage
20
21
  } from "./types.js";
21
22
  import { allFeaturesTools } from "./features/index.js";
23
+ import { saveStockAiAnalysisRecord } from "./features/stockAnalysis/tools.js";
22
24
 
23
25
  function getCurrentTimestamp(): number {
24
26
  return Date.now();
@@ -162,6 +164,90 @@ function isReasoningPayload(payload: any, info?: any): boolean {
162
164
  return isReasoningReplyPayload(payload);
163
165
  }
164
166
 
167
+ function parseStockAnalysisRequest(text: string, chatId?: string) {
168
+ let body: unknown;
169
+ try {
170
+ body = JSON.parse(text);
171
+ } catch {
172
+ return null;
173
+ }
174
+
175
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
176
+ return null;
177
+ }
178
+
179
+ const payload = body as Record<string, unknown>;
180
+ const cwContent = typeof payload.cw_content === "string" ? payload.cw_content.trim() : "";
181
+
182
+ let stockCode = "";
183
+ let stockName = "";
184
+
185
+ // 1. Try parsing cw_context if it exists
186
+ const cwContext = typeof payload.cw_context === "string" ? payload.cw_context.trim() : "";
187
+ if (cwContext) {
188
+ try {
189
+ const parsedContext = JSON.parse(cwContext);
190
+ if (parsedContext && typeof parsedContext === "object" && !Array.isArray(parsedContext)) {
191
+ stockCode = typeof parsedContext.stockCode === "string" ? parsedContext.stockCode.trim() : "";
192
+ stockName = typeof parsedContext.stockName === "string" ? parsedContext.stockName.trim() : "";
193
+ }
194
+ } catch {
195
+ // If not a valid JSON string, treat cwContext as the plain stockCode
196
+ stockCode = cwContext;
197
+ }
198
+ }
199
+
200
+ // 2. If we found a stockCode but no stockName, attempt database lookups
201
+ if (stockCode && !stockName) {
202
+ try {
203
+ const db = getDB();
204
+ const normalizedCode = stockCode.toUpperCase().replace(/\.SS$/i, ".SH");
205
+ // Query global_stock_metadata first
206
+ let row = db.prepare(`SELECT stockName FROM global_stock_metadata WHERE stockCode = ? OR stockCode = ? LIMIT 1`)
207
+ .get(normalizedCode, normalizedCode.replace(/\.SH$/i, "").replace(/\.SZ$/i, "").replace(/\.HK$/i, "")) as { stockName: string } | undefined;
208
+ if (!row) {
209
+ // Fallback to watchlist
210
+ row = db.prepare(`SELECT stockName FROM watchlist WHERE stockCode = ? LIMIT 1`)
211
+ .get(normalizedCode) as { stockName: string } | undefined;
212
+ }
213
+ if (row?.stockName) {
214
+ stockName = row.stockName;
215
+ } else {
216
+ stockName = stockCode; // fallback to code if name not found in db
217
+ }
218
+ } catch {
219
+ stockName = stockCode;
220
+ }
221
+ }
222
+
223
+ // 3. Fallback to legacy cwContent/chatId parsing if no stockCode was found via cw_context
224
+ if (!stockCode) {
225
+ if (!cwContent.startsWith("分析一下") || !cwContent.endsWith("股票")) {
226
+ return null;
227
+ }
228
+
229
+ const chatIdMatch = typeof chatId === "string"
230
+ ? /^stock_analysis_(.+)_\d+$/.exec(chatId)
231
+ : null;
232
+ stockCode = typeof payload.cw_stock_code === "string"
233
+ ? payload.cw_stock_code.trim()
234
+ : chatIdMatch?.[1]?.trim() || "";
235
+ stockName = typeof payload.cw_stock_name === "string"
236
+ ? payload.cw_stock_name.trim()
237
+ : cwContent.replace(/^分析一下/, "").replace(/股票$/, "").trim();
238
+ }
239
+
240
+ const market = typeof payload.cw_market === "string" && payload.cw_market.trim()
241
+ ? payload.cw_market.trim()
242
+ : "CN";
243
+
244
+ if (!stockCode || !stockName) {
245
+ return null;
246
+ }
247
+
248
+ return { stockCode, stockName, market };
249
+ }
250
+
165
251
  async function getJsonlLineCountAsync(agentId: string, sessionId: string): Promise<number> {
166
252
  try {
167
253
  const stateDir = getStateDir();
@@ -618,6 +704,41 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
618
704
  sendEvent("reply", { text: delta, isPartial: true });
619
705
  };
620
706
 
707
+ const stockAnalysisRequest = parseStockAnalysisRequest(text, chatId);
708
+ let stockAnalysisReplyText = "";
709
+ let didSaveStockAnalysis = false;
710
+ const appendStockAnalysisReplyText = (content: string) => {
711
+ if (!stockAnalysisRequest) return;
712
+ const visibleContent = extractVisibleReplyText(content);
713
+ if (!visibleContent) return;
714
+
715
+ if (!stockAnalysisReplyText || visibleContent.startsWith(stockAnalysisReplyText)) {
716
+ stockAnalysisReplyText = visibleContent;
717
+ return;
718
+ }
719
+ if (stockAnalysisReplyText.includes(visibleContent)) return;
720
+ stockAnalysisReplyText += visibleContent;
721
+ };
722
+ const saveStockAnalysisReply = (content: string) => {
723
+ if (!stockAnalysisRequest || didSaveStockAnalysis) return;
724
+ appendStockAnalysisReplyText(content);
725
+ const visibleContent = stockAnalysisReplyText.trim();
726
+ if (!visibleContent) return;
727
+
728
+ try {
729
+ saveStockAiAnalysisRecord(getDB(), accountId, {
730
+ ...stockAnalysisRequest,
731
+ content: visibleContent
732
+ });
733
+ didSaveStockAnalysis = true;
734
+ } catch (err: any) {
735
+ const message = err?.message || "保存股票 AI 分析失败";
736
+ childLogger.error({ err: message, chatId, stockCode: stockAnalysisRequest.stockCode }, "保存股票 AI 分析失败");
737
+ sendEvent("error", { error: message });
738
+ throw err;
739
+ }
740
+ };
741
+
621
742
  const normalizeId = (rawId?: string) => rawId?.replace(/^(command:|tool:|call_)/, '');
622
743
  let hasSentModelEvent = false;
623
744
 
@@ -790,6 +911,12 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
790
911
  sendReasoningText(payload.text);
791
912
  return;
792
913
  }
914
+ appendStockAnalysisReplyText(payload.text);
915
+ if (info.kind === "final") {
916
+ saveStockAnalysisReply(payload.text);
917
+ sendEvent("reply", { text: extractVisibleReplyText(payload.text), isFinal: true, replace: true });
918
+ return;
919
+ }
793
920
  sendReplyText({ text: payload.text, replace: true });
794
921
  }
795
922
 
@@ -803,6 +930,7 @@ export const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount
803
930
  },
804
931
  }
805
932
  });
933
+ saveStockAnalysisReply(stockAnalysisReplyText);
806
934
  await sendFinalReplyAndUsage();
807
935
  } finally {
808
936
  delete replyTextStateMap[chatId];
@@ -153,6 +153,88 @@ function runStockNotesMigrations(db: DatabaseSync) {
153
153
  db.exec("CREATE INDEX IF NOT EXISTS idx_stock_notes_user_stock ON stock_notes(userId, watchlistId, updatedAt DESC)");
154
154
  }
155
155
 
156
+ function runStockAiAnalysisMigrations(db: DatabaseSync) {
157
+ const indexes = db.prepare("PRAGMA index_list(stock_ai_analysis)").all() as { name: string; unique: number }[];
158
+ const hasUniqueStockIndex = indexes.some(index => {
159
+ if (index.unique !== 1) return false;
160
+ const columns = db.prepare(`PRAGMA index_info(${index.name})`).all() as { name: string }[];
161
+ const columnNames = columns.map(column => column.name);
162
+ return columnNames.includes("userId") && columnNames.includes("stockCode");
163
+ });
164
+ if (!hasUniqueStockIndex) return;
165
+
166
+ db.exec("BEGIN");
167
+ try {
168
+ db.exec(`
169
+ DROP TABLE IF EXISTS stock_ai_analysis_history;
170
+
171
+ CREATE TABLE stock_ai_analysis_history (
172
+ id TEXT NOT NULL,
173
+ userId TEXT NOT NULL,
174
+ stockCode TEXT NOT NULL,
175
+ stockName TEXT NOT NULL,
176
+ market TEXT NOT NULL DEFAULT 'CN',
177
+ content TEXT NOT NULL,
178
+ createdAt DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')),
179
+ updatedAt DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')),
180
+ PRIMARY KEY(id, userId)
181
+ );
182
+
183
+ INSERT INTO stock_ai_analysis_history (id, userId, stockCode, stockName, market, content, createdAt, updatedAt)
184
+ SELECT id, userId, stockCode, stockName, market, content, createdAt, updatedAt
185
+ FROM stock_ai_analysis;
186
+
187
+ DROP TABLE stock_ai_analysis;
188
+ ALTER TABLE stock_ai_analysis_history RENAME TO stock_ai_analysis;
189
+ CREATE INDEX IF NOT EXISTS idx_stock_ai_analysis_user_stock_updated ON stock_ai_analysis(userId, stockCode, updatedAt DESC);
190
+ `);
191
+ db.exec("COMMIT");
192
+ } catch (e) {
193
+ if (db.inTransaction) db.exec("ROLLBACK");
194
+ throw e;
195
+ }
196
+ }
197
+
198
+ function runArticleAiAnalysisMigrations(db: DatabaseSync) {
199
+ const columns = db.prepare("PRAGMA table_info(article_ai_analysis)").all() as { name: string }[];
200
+ if (columns.length === 0 || columns.some(column => column.name === "sourceId")) {
201
+ return;
202
+ }
203
+
204
+ db.exec("BEGIN");
205
+ try {
206
+ db.exec(`
207
+ DROP TABLE IF EXISTS article_ai_analysis_v2;
208
+
209
+ CREATE TABLE article_ai_analysis_v2 (
210
+ id TEXT NOT NULL,
211
+ sourceId TEXT NOT NULL,
212
+ userId TEXT NOT NULL,
213
+ analysisType TEXT NOT NULL,
214
+ market TEXT NOT NULL DEFAULT 'CN',
215
+ content TEXT NOT NULL,
216
+ createdAt DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')),
217
+ updatedAt DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')),
218
+ PRIMARY KEY(id, userId),
219
+ UNIQUE(sourceId, userId, analysisType, market)
220
+ );
221
+
222
+ INSERT INTO article_ai_analysis_v2 (id, sourceId, userId, analysisType, market, content, createdAt, updatedAt)
223
+ SELECT lower(hex(randomblob(16))), id, userId, analysisType, market, content, createdAt, updatedAt
224
+ FROM article_ai_analysis;
225
+
226
+ DROP TABLE article_ai_analysis;
227
+ ALTER TABLE article_ai_analysis_v2 RENAME TO article_ai_analysis;
228
+ CREATE INDEX IF NOT EXISTS idx_article_ai_analysis_user_source_type_updated
229
+ ON article_ai_analysis(userId, sourceId, analysisType, updatedAt DESC);
230
+ `);
231
+ db.exec("COMMIT");
232
+ } catch (e) {
233
+ if (db.inTransaction) db.exec("ROLLBACK");
234
+ throw e;
235
+ }
236
+ }
237
+
156
238
  export function getDB(): DatabaseSync {
157
239
  if (!_db) {
158
240
  const dbPath = getDbPath();
@@ -263,10 +345,39 @@ export function getDB(): DatabaseSync {
263
345
  UNIQUE(noteId, userId, profileLibraryId)
264
346
  );
265
347
  CREATE INDEX IF NOT EXISTS idx_stock_note_profile_libraries_user_note ON stock_note_profile_libraries(userId, noteId);
348
+
349
+ CREATE TABLE IF NOT EXISTS stock_ai_analysis (
350
+ id TEXT NOT NULL,
351
+ userId TEXT NOT NULL,
352
+ stockCode TEXT NOT NULL,
353
+ stockName TEXT NOT NULL,
354
+ market TEXT NOT NULL DEFAULT 'CN',
355
+ content TEXT NOT NULL,
356
+ createdAt DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')),
357
+ updatedAt DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')),
358
+ PRIMARY KEY(id, userId)
359
+ );
360
+ CREATE INDEX IF NOT EXISTS idx_stock_ai_analysis_user_stock_updated ON stock_ai_analysis(userId, stockCode, updatedAt DESC);
361
+
362
+ CREATE TABLE IF NOT EXISTS article_ai_analysis (
363
+ id TEXT NOT NULL,
364
+ sourceId TEXT NOT NULL,
365
+ userId TEXT NOT NULL,
366
+ analysisType TEXT NOT NULL,
367
+ market TEXT NOT NULL DEFAULT 'CN',
368
+ content TEXT NOT NULL,
369
+ createdAt DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')),
370
+ updatedAt DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')),
371
+ PRIMARY KEY(id, userId),
372
+ UNIQUE(sourceId, userId, analysisType, market)
373
+ );
374
+ CREATE INDEX IF NOT EXISTS idx_article_ai_analysis_user_source_type_updated ON article_ai_analysis(userId, sourceId, analysisType, updatedAt DESC);
266
375
  `);
267
376
 
268
377
  runWatchlistDedupMigrations(_db);
269
378
  runStockNotesMigrations(_db);
379
+ runStockAiAnalysisMigrations(_db);
380
+ runArticleAiAnalysisMigrations(_db);
270
381
  }
271
382
  return _db;
272
383
  }
@@ -1,6 +1,8 @@
1
1
  import { watchlistTools } from "./watchlist/tools.js";
2
2
  import { profileLibraryTools } from "./profileLibrary/tools.js";
3
3
  import { noteTools } from "./notes/tools.js";
4
+ import { stockAnalysisTools } from "./stockAnalysis/tools.js";
5
+ import { pluginInfoTools } from "./pluginInfo/tools.js";
4
6
 
5
7
  export interface RuntimeTool {
6
8
  name: string;
@@ -14,5 +16,7 @@ export interface RuntimeTool {
14
16
  export const allFeaturesTools: Record<string, RuntimeTool> = {
15
17
  ...watchlistTools,
16
18
  ...profileLibraryTools,
17
- ...noteTools
19
+ ...noteTools,
20
+ ...stockAnalysisTools,
21
+ ...pluginInfoTools
18
22
  };
@@ -0,0 +1,63 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { z } from "openclaw/plugin-sdk/zod";
5
+
6
+ interface RuntimeTool {
7
+ name: string;
8
+ description: string;
9
+ parameters: unknown;
10
+ registerTool?: boolean;
11
+ execute(params: unknown, ctx: { userId: string }): Promise<string>;
12
+ }
13
+
14
+ const GetPluginVersionParamsSchema = z.object({}).nullish();
15
+
16
+ let cachedPluginVersion: string | null = null;
17
+
18
+ function findPackageJsonPath(startDir: string): string | null {
19
+ let currentDir = startDir;
20
+
21
+ while (true) {
22
+ const candidate = path.join(currentDir, "package.json");
23
+ if (fs.existsSync(candidate)) return candidate;
24
+
25
+ const parentDir = path.dirname(currentDir);
26
+ if (parentDir === currentDir) return null;
27
+ currentDir = parentDir;
28
+ }
29
+ }
30
+
31
+ function getPluginVersion(): string {
32
+ if (cachedPluginVersion) return cachedPluginVersion;
33
+
34
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
35
+ const packageJsonPath = findPackageJsonPath(moduleDir);
36
+ if (!packageJsonPath) {
37
+ throw new Error("无法找到插件 package.json");
38
+ }
39
+
40
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { version?: unknown };
41
+ if (typeof packageJson.version !== "string" || !packageJson.version.trim()) {
42
+ throw new Error("插件 package.json 缺少有效版本号");
43
+ }
44
+
45
+ cachedPluginVersion = packageJson.version.trim();
46
+ return cachedPluginVersion;
47
+ }
48
+
49
+ export const pluginInfoTools: Record<string, RuntimeTool> = {
50
+ get_plugin_version: {
51
+ name: "get_plugin_version",
52
+ description: "获取当前 Hedgehog 插件版本号",
53
+ parameters: GetPluginVersionParamsSchema,
54
+ registerTool: false,
55
+ async execute(params: unknown) {
56
+ GetPluginVersionParamsSchema.parse(params);
57
+ return JSON.stringify({
58
+ success: true,
59
+ version: getPluginVersion()
60
+ });
61
+ }
62
+ }
63
+ };
@@ -0,0 +1,60 @@
1
+ import { z } from "openclaw/plugin-sdk/zod";
2
+
3
+ export const GetStockAiAnalysisParamsSchema = z.object({
4
+ stockCode: z.string().trim().min(1).describe("股票代码"),
5
+ market: z.string().trim().min(1).default("CN").describe("市场类型,默认 CN")
6
+ });
7
+ export type GetStockAiAnalysisParams = z.infer<typeof GetStockAiAnalysisParamsSchema>;
8
+
9
+ export const QueryStockAiAnalysisHistoryParamsSchema = z.object({
10
+ stockCode: z.string().trim().min(1).describe("股票代码"),
11
+ market: z.string().trim().min(1).default("CN").describe("市场类型,默认 CN"),
12
+ page: z.number().int().min(1).default(1).describe("页码"),
13
+ pageSize: z.number().int().min(1).max(50).default(20).describe("每页数量")
14
+ });
15
+ export type QueryStockAiAnalysisHistoryParams = z.infer<typeof QueryStockAiAnalysisHistoryParamsSchema>;
16
+
17
+ export const SaveStockAiAnalysisParamsSchema = z.object({
18
+ stockCode: z.string().trim().min(1).describe("股票代码"),
19
+ stockName: z.string().trim().min(1).describe("股票名称"),
20
+ market: z.string().trim().min(1).default("CN").describe("市场类型,默认 CN"),
21
+ content: z.string().trim().min(1).describe("AI 分析内容")
22
+ });
23
+ export type SaveStockAiAnalysisParams = z.infer<typeof SaveStockAiAnalysisParamsSchema>;
24
+
25
+ export interface StockAiAnalysis {
26
+ id: string;
27
+ stockCode: string;
28
+ stockName: string;
29
+ market: string;
30
+ content: string;
31
+ createdAt: string;
32
+ updatedAt: string;
33
+ }
34
+
35
+ export const ArticleAiAnalysisKindSchema = z.enum(["verification", "deduction"]);
36
+
37
+ export const GetArticleAiAnalysisParamsSchema = z.object({
38
+ id: z.string().trim().min(1).describe("文章来源 ID,例如 news-5、report-5、announce-5"),
39
+ analysisType: ArticleAiAnalysisKindSchema.describe("分析类型:verification 信息求证,deduction 深度推演"),
40
+ market: z.string().trim().min(1).default("CN").describe("市场类型,默认 CN")
41
+ });
42
+ export type GetArticleAiAnalysisParams = z.infer<typeof GetArticleAiAnalysisParamsSchema>;
43
+
44
+ export const SaveArticleAiAnalysisParamsSchema = z.object({
45
+ id: z.string().trim().min(1).describe("文章来源 ID,例如 news-5、report-5、announce-5"),
46
+ analysisType: ArticleAiAnalysisKindSchema.describe("分析类型:verification 信息求证,deduction 深度推演"),
47
+ market: z.string().trim().min(1).default("CN").describe("市场类型,默认 CN"),
48
+ content: z.string().trim().min(1).describe("AI 分析内容")
49
+ });
50
+ export type SaveArticleAiAnalysisParams = z.infer<typeof SaveArticleAiAnalysisParamsSchema>;
51
+
52
+ export interface ArticleAiAnalysis {
53
+ id: string;
54
+ sourceId: string;
55
+ analysisType: GetArticleAiAnalysisParams["analysisType"];
56
+ market: string;
57
+ content: string;
58
+ createdAt: string;
59
+ updatedAt: string;
60
+ }