@hedgehog-finance/hedgehog-plugin 1.0.19 → 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.
Files changed (46) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/src/channel.js +143 -4
  3. package/dist/src/channel.js.map +1 -1
  4. package/dist/src/core/database.js +156 -0
  5. package/dist/src/core/database.js.map +1 -1
  6. package/dist/src/features/index.js +9 -1
  7. package/dist/src/features/index.js.map +1 -1
  8. package/dist/src/features/notes/schema.d.ts +84 -0
  9. package/dist/src/features/notes/schema.js +40 -0
  10. package/dist/src/features/notes/schema.js.map +1 -0
  11. package/dist/src/features/notes/tools.d.ts +11 -0
  12. package/dist/src/features/notes/tools.js +297 -0
  13. package/dist/src/features/notes/tools.js.map +1 -0
  14. package/dist/src/features/pluginInfo/tools.d.ts +11 -0
  15. package/dist/src/features/pluginInfo/tools.js +49 -0
  16. package/dist/src/features/pluginInfo/tools.js.map +1 -0
  17. package/dist/src/features/profileLibrary/schema.d.ts +29 -0
  18. package/dist/src/features/profileLibrary/schema.js +21 -0
  19. package/dist/src/features/profileLibrary/schema.js.map +1 -0
  20. package/dist/src/features/profileLibrary/tools.d.ts +11 -0
  21. package/dist/src/features/profileLibrary/tools.js +163 -0
  22. package/dist/src/features/profileLibrary/tools.js.map +1 -0
  23. package/dist/src/features/stockAnalysis/schema.d.ts +61 -0
  24. package/dist/src/features/stockAnalysis/schema.js +30 -0
  25. package/dist/src/features/stockAnalysis/schema.js.map +1 -0
  26. package/dist/src/features/stockAnalysis/tools.d.ts +20 -0
  27. package/dist/src/features/stockAnalysis/tools.js +138 -0
  28. package/dist/src/features/stockAnalysis/tools.js.map +1 -0
  29. package/dist/src/features/watchlist/logic.js +7 -60
  30. package/dist/src/features/watchlist/logic.js.map +1 -1
  31. package/dist/src/features/watchlist/tools.js +61 -26
  32. package/dist/src/features/watchlist/tools.js.map +1 -1
  33. package/dist/src/types.d.ts +1 -1
  34. package/package.json +5 -5
  35. package/src/channel.ts +150 -4
  36. package/src/core/database.ts +155 -0
  37. package/src/features/index.ts +9 -1
  38. package/src/features/notes/schema.ts +75 -0
  39. package/src/features/notes/tools.ts +352 -0
  40. package/src/features/pluginInfo/tools.ts +63 -0
  41. package/src/features/profileLibrary/schema.ts +35 -0
  42. package/src/features/profileLibrary/tools.ts +194 -0
  43. package/src/features/stockAnalysis/schema.ts +60 -0
  44. package/src/features/stockAnalysis/tools.ts +192 -0
  45. package/src/features/watchlist/logic.ts +7 -63
  46. package/src/features/watchlist/tools.ts +83 -48
@@ -0,0 +1,35 @@
1
+ import { z } from "openclaw/plugin-sdk/zod";
2
+
3
+ export const AddProfileLibraryParamsSchema = z.object({
4
+ id: z.string().trim().min(1).optional().describe("资料库 ID,不传则自动生成"),
5
+ title: z.string().trim().min(1).describe("资料库标题")
6
+ });
7
+ export type AddProfileLibraryParams = z.infer<typeof AddProfileLibraryParamsSchema>;
8
+
9
+ export const DeleteProfileLibraryParamsSchema = z.object({
10
+ id: z.string().trim().min(1).describe("资料库 ID")
11
+ });
12
+ export type DeleteProfileLibraryParams = z.infer<typeof DeleteProfileLibraryParamsSchema>;
13
+
14
+ export const GetProfileLibraryByIdParamsSchema = z.object({
15
+ id: z.string().trim().min(1).describe("资料库 ID")
16
+ });
17
+ export type GetProfileLibraryByIdParams = z.infer<typeof GetProfileLibraryByIdParamsSchema>;
18
+
19
+ export const QueryProfileLibrariesParamsSchema = z.object({
20
+ keyword: z.string().trim().optional().describe("模糊查询关键词,可匹配标题或 ID"),
21
+ page: z.number().int().min(1).optional().describe("页码,从 1 开始,默认 1"),
22
+ pageSize: z.number().int().min(1).max(100).optional().describe("每页数量,默认 20,最大 100")
23
+ });
24
+ export type QueryProfileLibrariesParams = z.infer<typeof QueryProfileLibrariesParamsSchema>;
25
+
26
+ export const GetProfileLibrariesParamsSchema = z.object({
27
+ page: z.number().int().min(1).optional().describe("页码,从 1 开始,默认 1"),
28
+ pageSize: z.number().int().min(1).max(100).optional().describe("每页数量,默认 20,最大 100")
29
+ });
30
+ export type GetProfileLibrariesParams = z.infer<typeof GetProfileLibrariesParamsSchema>;
31
+
32
+ export interface ProfileLibraryRow {
33
+ id: string;
34
+ title: string;
35
+ }
@@ -0,0 +1,194 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDB } from "../../core/database.js";
3
+ import {
4
+ AddProfileLibraryParams,
5
+ AddProfileLibraryParamsSchema,
6
+ DeleteProfileLibraryParams,
7
+ DeleteProfileLibraryParamsSchema,
8
+ GetProfileLibraryByIdParams,
9
+ GetProfileLibraryByIdParamsSchema,
10
+ GetProfileLibrariesParams,
11
+ GetProfileLibrariesParamsSchema,
12
+ ProfileLibraryRow,
13
+ QueryProfileLibrariesParams,
14
+ QueryProfileLibrariesParamsSchema
15
+ } from "./schema.js";
16
+
17
+ interface RuntimeTool {
18
+ name: string;
19
+ description: string;
20
+ parameters: unknown;
21
+ registerTool?: boolean;
22
+ execute(params: unknown, ctx: { userId: string }): Promise<string>;
23
+ }
24
+
25
+ function escapeLikePattern(value: string): string {
26
+ return value.replace(/[\\%_]/g, (char) => `\\${char}`);
27
+ }
28
+
29
+ function normalizePagination(args: { page?: number; pageSize?: number }) {
30
+ const page = args.page ?? 1;
31
+ const pageSize = args.pageSize ?? 20;
32
+ return {
33
+ page,
34
+ pageSize,
35
+ offset: (page - 1) * pageSize
36
+ };
37
+ }
38
+
39
+ function paginatedResponse(rows: ProfileLibraryRow[], page: number, pageSize: number, total: number) {
40
+ return JSON.stringify({
41
+ success: true,
42
+ data: rows,
43
+ pagination: {
44
+ page,
45
+ pageSize,
46
+ total,
47
+ totalPages: Math.ceil(total / pageSize)
48
+ }
49
+ });
50
+ }
51
+
52
+ async function getProfileLibraries(args: GetProfileLibrariesParams = {}, ctx: { userId: string }) {
53
+ try {
54
+ const db = getDB();
55
+ const uId = String(ctx.userId);
56
+ const { page, pageSize, offset } = normalizePagination(args);
57
+ const total = (db.prepare(`
58
+ SELECT COUNT(*) AS count
59
+ FROM profile_libraries
60
+ WHERE userId = ?
61
+ `).get(uId) as { count: number }).count;
62
+ const rows = db.prepare(`
63
+ SELECT id, title
64
+ FROM profile_libraries
65
+ WHERE userId = ?
66
+ ORDER BY updatedAt DESC, createdAt DESC
67
+ LIMIT ? OFFSET ?
68
+ `).all(uId, pageSize, offset) as ProfileLibraryRow[];
69
+
70
+ return paginatedResponse(rows, page, pageSize, total);
71
+ } catch (e: any) {
72
+ return JSON.stringify({ success: false, error: e.message });
73
+ }
74
+ }
75
+
76
+ async function queryProfileLibraries(args: QueryProfileLibrariesParams = {}, ctx: { userId: string }) {
77
+ try {
78
+ const db = getDB();
79
+ const uId = String(ctx.userId);
80
+ const keyword = args.keyword?.trim();
81
+ const { page, pageSize, offset } = normalizePagination(args);
82
+
83
+ if (keyword) {
84
+ const pattern = `%${escapeLikePattern(keyword)}%`;
85
+ const total = (db.prepare(`
86
+ SELECT COUNT(*) AS count
87
+ FROM profile_libraries
88
+ WHERE userId = ?
89
+ AND (title LIKE ? ESCAPE '\\' OR id LIKE ? ESCAPE '\\')
90
+ `).get(uId, pattern, pattern) as { count: number }).count;
91
+ const rows = db.prepare(`
92
+ SELECT id, title
93
+ FROM profile_libraries
94
+ WHERE userId = ?
95
+ AND (title LIKE ? ESCAPE '\\' OR id LIKE ? ESCAPE '\\')
96
+ ORDER BY updatedAt DESC, createdAt DESC
97
+ LIMIT ? OFFSET ?
98
+ `).all(uId, pattern, pattern, pageSize, offset) as ProfileLibraryRow[];
99
+ return paginatedResponse(rows, page, pageSize, total);
100
+ }
101
+
102
+ return getProfileLibraries(args, ctx);
103
+ } catch (e: any) {
104
+ return JSON.stringify({ success: false, error: e.message });
105
+ }
106
+ }
107
+
108
+ export const profileLibraryTools: Record<string, RuntimeTool> = {
109
+ add_profile_library: {
110
+ name: "add_profile_library",
111
+ description: "新增个人资料库",
112
+ parameters: AddProfileLibraryParamsSchema,
113
+ registerTool: false,
114
+ execute: async (args: AddProfileLibraryParams, ctx: { userId: string }) => {
115
+ try {
116
+ const db = getDB();
117
+ const uId = String(ctx.userId);
118
+ const id = args.id?.trim() || randomUUID();
119
+ const title = args.title.trim();
120
+
121
+ db.prepare(`
122
+ INSERT INTO profile_libraries (id, userId, title)
123
+ VALUES (?, ?, ?)
124
+ ON CONFLICT(id, userId) DO UPDATE SET
125
+ title = excluded.title,
126
+ updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
127
+ `).run(id, uId, title);
128
+
129
+ return JSON.stringify({ success: true, data: { id, title } });
130
+ } catch (e: any) {
131
+ return JSON.stringify({ success: false, error: e.message });
132
+ }
133
+ }
134
+ },
135
+
136
+ delete_profile_library: {
137
+ name: "delete_profile_library",
138
+ description: "删除个人资料库",
139
+ parameters: DeleteProfileLibraryParamsSchema,
140
+ registerTool: false,
141
+ execute: async (args: DeleteProfileLibraryParams, ctx: { userId: string }) => {
142
+ try {
143
+ const db = getDB();
144
+ const uId = String(ctx.userId);
145
+ const info = db.prepare(`
146
+ DELETE FROM profile_libraries
147
+ WHERE id = ? AND userId = ?
148
+ `).run(args.id.trim(), uId);
149
+
150
+ return JSON.stringify({ success: info.changes > 0 });
151
+ } catch (e: any) {
152
+ return JSON.stringify({ success: false, error: e.message });
153
+ }
154
+ }
155
+ },
156
+
157
+ get_profile_library_by_id: {
158
+ name: "get_profile_library_by_id",
159
+ description: "根据 ID 查询个人资料库",
160
+ parameters: GetProfileLibraryByIdParamsSchema,
161
+ registerTool: false,
162
+ execute: async (args: GetProfileLibraryByIdParams, ctx: { userId: string }) => {
163
+ try {
164
+ const db = getDB();
165
+ const uId = String(ctx.userId);
166
+ const row = db.prepare(`
167
+ SELECT id, title
168
+ FROM profile_libraries
169
+ WHERE id = ? AND userId = ?
170
+ `).get(args.id.trim(), uId) as ProfileLibraryRow | undefined;
171
+
172
+ return JSON.stringify({ success: true, data: row ?? null });
173
+ } catch (e: any) {
174
+ return JSON.stringify({ success: false, error: e.message });
175
+ }
176
+ }
177
+ },
178
+
179
+ query_profile_libraries: {
180
+ name: "query_profile_libraries",
181
+ description: "分页查询个人资料库,支持按标题或 ID 模糊查询",
182
+ parameters: QueryProfileLibrariesParamsSchema,
183
+ registerTool: false,
184
+ execute: queryProfileLibraries
185
+ },
186
+
187
+ get_profile_libraries: {
188
+ name: "get_profile_libraries",
189
+ description: "分页获取个人资料库列表",
190
+ parameters: GetProfileLibrariesParamsSchema,
191
+ registerTool: false,
192
+ execute: getProfileLibraries
193
+ }
194
+ };
@@ -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
+ }
@@ -0,0 +1,192 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDB } from "../../core/database.js";
3
+ import {
4
+ ArticleAiAnalysis,
5
+ GetArticleAiAnalysisParamsSchema,
6
+ GetStockAiAnalysisParamsSchema,
7
+ QueryStockAiAnalysisHistoryParamsSchema,
8
+ SaveArticleAiAnalysisParamsSchema,
9
+ SaveStockAiAnalysisParamsSchema,
10
+ StockAiAnalysis
11
+ } from "./schema.js";
12
+
13
+ interface RuntimeTool {
14
+ name: string;
15
+ description: string;
16
+ parameters: unknown;
17
+ registerTool?: boolean;
18
+ execute(params: unknown, ctx: { userId: string }): Promise<string>;
19
+ }
20
+
21
+ export function normalizeStockCode(stockCode: string): string {
22
+ return stockCode.trim().toUpperCase().replace(/\.SS$/i, ".SH");
23
+ }
24
+
25
+ function selectLatestStockAnalysis(
26
+ db: ReturnType<typeof getDB>,
27
+ userId: string,
28
+ stockCode: string
29
+ ): StockAiAnalysis | undefined {
30
+ return db.prepare(`
31
+ SELECT id, stockCode, stockName, market, content, createdAt, updatedAt
32
+ FROM stock_ai_analysis
33
+ WHERE userId = ? AND stockCode = ?
34
+ ORDER BY updatedAt DESC, createdAt DESC
35
+ LIMIT 1
36
+ `).get(userId, stockCode) as StockAiAnalysis | undefined;
37
+ }
38
+
39
+ export function saveStockAiAnalysisRecord(
40
+ db: ReturnType<typeof getDB>,
41
+ userId: string,
42
+ args: {
43
+ stockCode: string;
44
+ stockName: string;
45
+ market: string;
46
+ content: string;
47
+ }
48
+ ): StockAiAnalysis {
49
+ const stockCode = normalizeStockCode(args.stockCode);
50
+ const id = randomUUID();
51
+
52
+ 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);
56
+
57
+ return db.prepare(`
58
+ SELECT id, stockCode, stockName, market, content, createdAt, updatedAt
59
+ FROM stock_ai_analysis
60
+ WHERE userId = ? AND id = ?
61
+ `).get(userId, id) as StockAiAnalysis;
62
+ }
63
+
64
+ function selectLatestArticleAnalysis(
65
+ db: ReturnType<typeof getDB>,
66
+ userId: string,
67
+ sourceId: string,
68
+ analysisType: ArticleAiAnalysis["analysisType"],
69
+ market: string
70
+ ): ArticleAiAnalysis | undefined {
71
+ 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 = ?
75
+ ORDER BY updatedAt DESC, createdAt DESC
76
+ LIMIT 1
77
+ `).get(userId, sourceId, analysisType, market) as ArticleAiAnalysis | undefined;
78
+ }
79
+
80
+ function saveArticleAiAnalysisRecord(
81
+ db: ReturnType<typeof getDB>,
82
+ userId: string,
83
+ args: {
84
+ sourceId: string;
85
+ analysisType: ArticleAiAnalysis["analysisType"];
86
+ market: string;
87
+ content: string;
88
+ }
89
+ ): ArticleAiAnalysis {
90
+ const id = randomUUID();
91
+
92
+ db.prepare(`
93
+ INSERT INTO article_ai_analysis (id, sourceId, userId, analysisType, market, content)
94
+ VALUES (?, ?, ?, ?, ?, ?)
95
+ ON CONFLICT(sourceId, userId, analysisType, market) DO UPDATE SET
96
+ content = excluded.content,
97
+ updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
98
+ `).run(id, args.sourceId, userId, args.analysisType, args.market, args.content);
99
+
100
+ 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;
105
+ }
106
+
107
+ export const stockAnalysisTools: Record<string, RuntimeTool> = {
108
+ get_stock_ai_analysis: {
109
+ name: "get_stock_ai_analysis",
110
+ description: "读取股票 AI 分析的最新一条历史记录;不触发模型分析。",
111
+ parameters: GetStockAiAnalysisParamsSchema,
112
+ registerTool: false,
113
+ async execute(params, ctx) {
114
+ const args = GetStockAiAnalysisParamsSchema.parse(params);
115
+ const db = getDB();
116
+ const data = selectLatestStockAnalysis(db, ctx.userId, normalizeStockCode(args.stockCode));
117
+ return JSON.stringify({ success: true, data: data || null });
118
+ }
119
+ },
120
+ query_stock_ai_analysis_history: {
121
+ name: "query_stock_ai_analysis_history",
122
+ description: "分页读取股票 AI 分析历史记录;不触发模型分析。",
123
+ parameters: QueryStockAiAnalysisHistoryParamsSchema,
124
+ registerTool: false,
125
+ async execute(params, ctx) {
126
+ const args = QueryStockAiAnalysisHistoryParamsSchema.parse(params);
127
+ const db = getDB();
128
+ const stockCode = normalizeStockCode(args.stockCode);
129
+ const offset = (args.page - 1) * args.pageSize;
130
+ const rows = db.prepare(`
131
+ SELECT id, stockCode, stockName, market, content, createdAt, updatedAt
132
+ FROM stock_ai_analysis
133
+ WHERE userId = ? AND stockCode = ? AND market = ?
134
+ ORDER BY updatedAt DESC, createdAt DESC
135
+ LIMIT ? OFFSET ?
136
+ `).all(ctx.userId, stockCode, args.market, args.pageSize, offset) as StockAiAnalysis[];
137
+ const countRow = db.prepare(`
138
+ SELECT COUNT(*) AS total
139
+ FROM stock_ai_analysis
140
+ WHERE userId = ? AND stockCode = ? AND market = ?
141
+ `).get(ctx.userId, stockCode, args.market) as { total: number };
142
+ const total = countRow.total || 0;
143
+
144
+ return JSON.stringify({
145
+ success: true,
146
+ data: rows,
147
+ pagination: {
148
+ page: args.page,
149
+ pageSize: args.pageSize,
150
+ total,
151
+ totalPages: Math.ceil(total / args.pageSize)
152
+ }
153
+ });
154
+ }
155
+ },
156
+ save_stock_ai_analysis: {
157
+ name: "save_stock_ai_analysis",
158
+ description: "追加保存一条股票 AI 分析历史记录。",
159
+ parameters: SaveStockAiAnalysisParamsSchema,
160
+ registerTool: false,
161
+ async execute(params, ctx) {
162
+ const args = SaveStockAiAnalysisParamsSchema.parse(params);
163
+ const db = getDB();
164
+ const data = saveStockAiAnalysisRecord(db, ctx.userId, args);
165
+ return JSON.stringify({ success: true, data });
166
+ }
167
+ },
168
+ get_article_ai_analysis: {
169
+ name: "get_article_ai_analysis",
170
+ description: "读取文章 AI 分析结果;支持信息求证与深度推演,不触发模型分析。",
171
+ parameters: GetArticleAiAnalysisParamsSchema,
172
+ registerTool: false,
173
+ async execute(params, ctx) {
174
+ const args = GetArticleAiAnalysisParamsSchema.parse(params);
175
+ const db = getDB();
176
+ const data = selectLatestArticleAnalysis(db, ctx.userId, args.id, args.analysisType, args.market);
177
+ return JSON.stringify({ success: true, data: data || null });
178
+ }
179
+ },
180
+ save_article_ai_analysis: {
181
+ name: "save_article_ai_analysis",
182
+ description: "保存文章 AI 分析结果;支持信息求证与深度推演。",
183
+ parameters: SaveArticleAiAnalysisParamsSchema,
184
+ registerTool: false,
185
+ async execute(params, ctx) {
186
+ const args = SaveArticleAiAnalysisParamsSchema.parse(params);
187
+ const db = getDB();
188
+ const data = saveArticleAiAnalysisRecord(db, ctx.userId, { ...args, sourceId: args.id });
189
+ return JSON.stringify({ success: true, data });
190
+ }
191
+ }
192
+ };
@@ -3,7 +3,7 @@ import { completeSimple } from "@mariozechner/pi-ai";
3
3
  import { PluginRuntime } from "openclaw/plugin-sdk";
4
4
  import {
5
5
  extractAssistantText,
6
- prepareSimpleCompletionModel
6
+ prepareSimpleCompletionModelForAgent
7
7
  } from "openclaw/plugin-sdk/simple-completion-runtime";
8
8
  import { getDB } from "../../core/database.js";
9
9
  import { logger } from "../../core/logger.js";
@@ -32,44 +32,6 @@ const CLASSIFIER_SYSTEM_PROMPT = [
32
32
  "只允许根据用户消息中提供的行业/主题分类字典和股票列表输出纯 JSON 数组。"
33
33
  ].join("\n");
34
34
 
35
- function resolveModelRef(modelConfig: unknown): string | undefined {
36
- if (typeof modelConfig === "string" && modelConfig.trim()) return modelConfig.trim();
37
- if (!modelConfig || typeof modelConfig !== "object" || Array.isArray(modelConfig)) return undefined;
38
- const primary = (modelConfig as { primary?: unknown }).primary;
39
- return typeof primary === "string" && primary.trim() ? primary.trim() : undefined;
40
- }
41
-
42
- function getConfiguredProviderConfig(cfg: any, provider: string): any | undefined {
43
- const providerConfigs = cfg.models?.providers;
44
- if (!providerConfigs) return undefined;
45
- const exact = providerConfigs[provider];
46
- if (exact) return exact;
47
- const normalized = provider.trim().toLowerCase();
48
- return Object.entries(providerConfigs).find(([key]) => key.trim().toLowerCase() === normalized)?.[1];
49
- }
50
-
51
- function resolveClassifierModelSelection(
52
- cfg: any,
53
- defaultProvider: string,
54
- defaultModel: string
55
- ): { provider: string; model: string } {
56
- const agentEntry = ((cfg.agents?.list || []) as any[]).find((agent) => agent?.id === MAIN_AGENT_ID);
57
- const primary = resolveModelRef(agentEntry?.model)
58
- || resolveModelRef(cfg.agents?.defaults?.model)
59
- || `${defaultProvider}/${defaultModel}`;
60
- const slash = primary.indexOf("/");
61
- if (slash > 0) {
62
- return {
63
- provider: primary.slice(0, slash),
64
- model: primary.slice(slash + 1)
65
- };
66
- }
67
- return {
68
- provider: defaultProvider,
69
- model: primary
70
- };
71
- }
72
-
73
35
  function normalizeStockCodeForCache(stockCode: string, exchange?: string): string {
74
36
  const code = String(stockCode || "")
75
37
  .trim()
@@ -445,17 +407,7 @@ export const watchlistLogic = {
445
407
 
446
408
  async _callClassifierCompletion(rt: PluginRuntime, sessionId: string, prompt: string, timeoutMs: number): Promise<string> {
447
409
  const cfg = rt.config.loadConfig();
448
- const { provider, model } = resolveClassifierModelSelection(cfg, rt.agent.defaults.provider, rt.agent.defaults.model);
449
410
  const maxTokens = resolveClassifierOutputMaxTokens(prompt);
450
- const providerAuth = await rt.modelAuth.resolveApiKeyForProvider({ provider, cfg });
451
- if (!providerAuth.apiKey) {
452
- throw new Error(`No API key found for provider "${provider}".`);
453
- }
454
- const providerConfigs = cfg.models?.providers;
455
- const providerConfig = getConfiguredProviderConfig(cfg, provider);
456
- if (!providerConfig) {
457
- throw new Error(`No model provider config found for "${provider}".`);
458
- }
459
411
  const embeddedCfg = {
460
412
  ...cfg,
461
413
  agents: {
@@ -464,27 +416,19 @@ export const watchlistLogic = {
464
416
  ...cfg.agents?.defaults,
465
417
  systemPromptOverride: CLASSIFIER_SYSTEM_PROMPT
466
418
  }
467
- },
468
- models: {
469
- ...cfg.models,
470
- providers: {
471
- ...providerConfigs,
472
- [provider]: {
473
- ...providerConfig,
474
- auth: "api-key" as const,
475
- apiKey: providerAuth.apiKey
476
- }
477
- }
478
419
  }
479
420
  };
480
- const prepared = await prepareSimpleCompletionModel({
421
+ const prepared = await prepareSimpleCompletionModelForAgent({
481
422
  cfg: embeddedCfg,
482
- provider,
483
- modelId: model
423
+ agentId: MAIN_AGENT_ID,
424
+ allowBundledStaticCatalogFallback: true
484
425
  });
485
426
  if ("error" in prepared) {
486
427
  throw new Error(prepared.error);
487
428
  }
429
+ if (!prepared.auth.apiKey) {
430
+ throw new Error(`No API key found for provider "${prepared.selection.provider}".`);
431
+ }
488
432
  const abortController = new AbortController();
489
433
  const abortTimer = setTimeout(() => {
490
434
  abortController.abort(new Error(`classifier completion timed out after ${timeoutMs}ms`));