@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.
- package/dist/index.d.ts +1 -1
- package/dist/src/channel.js +143 -4
- package/dist/src/channel.js.map +1 -1
- package/dist/src/core/database.js +156 -0
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/features/index.js +9 -1
- package/dist/src/features/index.js.map +1 -1
- package/dist/src/features/notes/schema.d.ts +84 -0
- package/dist/src/features/notes/schema.js +40 -0
- package/dist/src/features/notes/schema.js.map +1 -0
- package/dist/src/features/notes/tools.d.ts +11 -0
- package/dist/src/features/notes/tools.js +297 -0
- package/dist/src/features/notes/tools.js.map +1 -0
- package/dist/src/features/pluginInfo/tools.d.ts +11 -0
- package/dist/src/features/pluginInfo/tools.js +49 -0
- package/dist/src/features/pluginInfo/tools.js.map +1 -0
- package/dist/src/features/profileLibrary/schema.d.ts +29 -0
- package/dist/src/features/profileLibrary/schema.js +21 -0
- package/dist/src/features/profileLibrary/schema.js.map +1 -0
- package/dist/src/features/profileLibrary/tools.d.ts +11 -0
- package/dist/src/features/profileLibrary/tools.js +163 -0
- package/dist/src/features/profileLibrary/tools.js.map +1 -0
- package/dist/src/features/stockAnalysis/schema.d.ts +61 -0
- package/dist/src/features/stockAnalysis/schema.js +30 -0
- package/dist/src/features/stockAnalysis/schema.js.map +1 -0
- package/dist/src/features/stockAnalysis/tools.d.ts +20 -0
- package/dist/src/features/stockAnalysis/tools.js +138 -0
- package/dist/src/features/stockAnalysis/tools.js.map +1 -0
- package/dist/src/features/watchlist/logic.js +7 -60
- package/dist/src/features/watchlist/logic.js.map +1 -1
- package/dist/src/features/watchlist/tools.js +61 -26
- package/dist/src/features/watchlist/tools.js.map +1 -1
- package/dist/src/types.d.ts +1 -1
- package/package.json +5 -5
- package/src/channel.ts +150 -4
- package/src/core/database.ts +155 -0
- package/src/features/index.ts +9 -1
- package/src/features/notes/schema.ts +75 -0
- package/src/features/notes/tools.ts +352 -0
- package/src/features/pluginInfo/tools.ts +63 -0
- package/src/features/profileLibrary/schema.ts +35 -0
- package/src/features/profileLibrary/tools.ts +194 -0
- package/src/features/stockAnalysis/schema.ts +60 -0
- package/src/features/stockAnalysis/tools.ts +192 -0
- package/src/features/watchlist/logic.ts +7 -63
- 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
|
-
|
|
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
|
|
421
|
+
const prepared = await prepareSimpleCompletionModelForAgent({
|
|
481
422
|
cfg: embeddedCfg,
|
|
482
|
-
|
|
483
|
-
|
|
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`));
|