@hedgehog-finance/hedgehog-plugin 1.0.12 → 1.0.13
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 +10 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/src/channel.d.ts +6 -0
- package/dist/src/channel.js +620 -0
- package/dist/src/channel.js.map +1 -0
- package/dist/src/core/database.d.ts +2 -0
- package/dist/src/core/database.js +220 -0
- package/dist/src/core/database.js.map +1 -0
- package/dist/src/core/logger.d.ts +3 -0
- package/dist/src/core/logger.js +20 -0
- package/dist/src/core/logger.js.map +1 -0
- package/dist/src/features/index.d.ts +22 -0
- package/dist/src/features/index.js +8 -0
- package/dist/src/features/index.js.map +1 -0
- package/dist/src/features/watchlist/logic.d.ts +48 -0
- package/dist/src/features/watchlist/logic.js +607 -0
- package/dist/src/features/watchlist/logic.js.map +1 -0
- package/dist/src/features/watchlist/schema.d.ts +85 -0
- package/dist/src/features/watchlist/schema.js +29 -0
- package/dist/src/features/watchlist/schema.js.map +1 -0
- package/dist/src/features/watchlist/store.d.ts +1 -0
- package/dist/src/features/watchlist/store.js +2 -0
- package/dist/src/features/watchlist/store.js.map +1 -0
- package/dist/src/features/watchlist/tools.d.ts +135 -0
- package/dist/src/features/watchlist/tools.js +572 -0
- package/dist/src/features/watchlist/tools.js.map +1 -0
- package/dist/src/runtime.d.ts +5 -0
- package/dist/src/runtime.js +40 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/types.d.ts +99 -0
- package/dist/src/types.js +16 -0
- package/dist/src/types.js.map +1 -0
- package/index.ts +4 -4
- package/package.json +23 -6
- package/src/channel.ts +26 -4
- package/src/core/database.ts +90 -3
- package/src/features/index.ts +2 -1
- package/src/features/watchlist/logic.ts +503 -128
- package/src/features/watchlist/schema.ts +1 -6
- package/src/features/watchlist/tools.ts +248 -103
- package/src/runtime.ts +3 -3
- package/src/types.ts +1 -1
- package/tsconfig.json +0 -16
|
@@ -24,10 +24,7 @@ export const AddToWatchlistParamsSchema = z.object({
|
|
|
24
24
|
stockCode: z.string(),
|
|
25
25
|
stockName: z.string(),
|
|
26
26
|
exchange: ExchangeEnum,
|
|
27
|
-
market: MarketEnum
|
|
28
|
-
// 支持:1. 字符串 "白酒" 2. 数组 ["白酒"] 3. 带权重的对象 { name: "白酒", weight: 99 }
|
|
29
|
-
industry: z.union([z.string(), z.array(z.string()), z.any()]).optional(),
|
|
30
|
-
theme: z.union([z.string(), z.array(z.string()), z.any()]).optional()
|
|
27
|
+
market: MarketEnum
|
|
31
28
|
});
|
|
32
29
|
|
|
33
30
|
export type AddToWatchlistParams = z.infer<typeof AddToWatchlistParamsSchema>;
|
|
@@ -63,5 +60,3 @@ export interface CategoryRow {
|
|
|
63
60
|
type?: 'industry' | 'theme';
|
|
64
61
|
weight?: number;
|
|
65
62
|
}
|
|
66
|
-
|
|
67
|
-
export type TagInput = string | string[] | { name: string; weight?: number } | { name: string; weight?: number }[];
|
|
@@ -3,8 +3,9 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
4
|
import { z } from "openclaw/plugin-sdk/zod";
|
|
5
5
|
import { PluginRuntime } from "openclaw/plugin-sdk";
|
|
6
|
-
import { getDB } from "../../core/database";
|
|
7
|
-
import {
|
|
6
|
+
import { getDB } from "../../core/database.js";
|
|
7
|
+
import { logger } from "../../core/logger.js";
|
|
8
|
+
import { watchlistLogic } from "./logic.js";
|
|
8
9
|
import {
|
|
9
10
|
AddToWatchlistParams,
|
|
10
11
|
AddToWatchlistParamsSchema,
|
|
@@ -12,34 +13,64 @@ import {
|
|
|
12
13
|
UpdateWatchlistItemSchema,
|
|
13
14
|
WatchlistRow,
|
|
14
15
|
CategoryRow,
|
|
15
|
-
TagInput,
|
|
16
16
|
GetWatchlistParams,
|
|
17
17
|
GetWatchlistParamsSchema,
|
|
18
18
|
SyncCategoriesParams,
|
|
19
19
|
SyncCategoriesParamsSchema,
|
|
20
20
|
BatchUpdateSortOrdersParams,
|
|
21
21
|
BatchUpdateSortOrdersParamsSchema
|
|
22
|
-
} from "./schema";
|
|
22
|
+
} from "./schema.js";
|
|
23
|
+
|
|
24
|
+
let watchlistMutationQueue: Promise<void> = Promise.resolve();
|
|
25
|
+
|
|
26
|
+
function enqueueWatchlistMutation<T>(task: () => Promise<T>): Promise<T> {
|
|
27
|
+
const previous = watchlistMutationQueue;
|
|
28
|
+
let release!: () => void;
|
|
29
|
+
watchlistMutationQueue = new Promise<void>((resolve) => {
|
|
30
|
+
release = resolve;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return previous
|
|
34
|
+
.catch(() => undefined)
|
|
35
|
+
.then(task)
|
|
36
|
+
.finally(release);
|
|
37
|
+
}
|
|
23
38
|
|
|
24
39
|
/**
|
|
25
40
|
* 辅助函数:统一处理归一化分类项
|
|
26
41
|
*/
|
|
27
|
-
function normalizeTags(input:
|
|
42
|
+
function normalizeTags(input: { name: string; weight?: number } | { name: string; weight?: number }[] | null | undefined): { name: string, weight: number }[] {
|
|
28
43
|
if (!input) return [];
|
|
29
44
|
const items = Array.isArray(input) ? input : [input];
|
|
30
|
-
return items
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
return items
|
|
46
|
+
.filter((item): item is { name: string; weight?: number } => typeof item?.name === 'string' && item.name.trim().length > 0)
|
|
47
|
+
.map(item => ({ name: item.name, weight: item.weight ?? 0 }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeWatchlistStock(stock: AddToWatchlistParams): AddToWatchlistParams {
|
|
51
|
+
return {
|
|
52
|
+
...stock,
|
|
53
|
+
stockCode: watchlistLogic._normalizeStockCodeForCache(stock.stockCode, stock.exchange)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function watchlistStockKey(stock: AddToWatchlistParams): string {
|
|
58
|
+
return `${watchlistLogic._normalizeStockCodeForCache(stock.stockCode, stock.exchange)}:${stock.exchange}`;
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
/**
|
|
40
62
|
* 辅助函数:更新股票的分类标签
|
|
41
63
|
*/
|
|
42
|
-
function updateWatchlistTags(
|
|
64
|
+
function updateWatchlistTags(
|
|
65
|
+
db: DatabaseSync,
|
|
66
|
+
watchlistId: string,
|
|
67
|
+
userId: string,
|
|
68
|
+
industry: { name: string; weight?: number } | null | undefined,
|
|
69
|
+
theme: { name: string; weight?: number }[] | undefined
|
|
70
|
+
) {
|
|
71
|
+
db.prepare("DELETE FROM watchlist_industry_items WHERE watchlistId = ? AND userId = ?").run(watchlistId, userId);
|
|
72
|
+
db.prepare("DELETE FROM watchlist_theme_items WHERE watchlistId = ? AND userId = ?").run(watchlistId, userId);
|
|
73
|
+
|
|
43
74
|
const normIndustries = normalizeTags(industry);
|
|
44
75
|
for (const item of normIndustries) {
|
|
45
76
|
const categoryId = watchlistLogic._ensureCategory(db, item.name, 'industry', userId);
|
|
@@ -63,77 +94,75 @@ function updateWatchlistTags(db: DatabaseSync, watchlistId: string, userId: stri
|
|
|
63
94
|
}
|
|
64
95
|
}
|
|
65
96
|
|
|
97
|
+
function upsertStockClassificationCache(
|
|
98
|
+
db: DatabaseSync,
|
|
99
|
+
stock: AddToWatchlistParams,
|
|
100
|
+
classification: {
|
|
101
|
+
industry: { name: string; weight?: number };
|
|
102
|
+
theme?: { name: string; weight?: number }[];
|
|
103
|
+
}
|
|
104
|
+
) {
|
|
105
|
+
const cacheCode = watchlistLogic._normalizeStockCodeForCache(stock.stockCode, stock.exchange);
|
|
106
|
+
const legacyCode = stock.stockCode.trim().toUpperCase().replace(/\.(SH|SS|SZ|HK|US)$/i, "");
|
|
107
|
+
db.prepare(`
|
|
108
|
+
INSERT OR REPLACE INTO global_stock_metadata (stockCode, exchange, stockName, industryJson, themeJson)
|
|
109
|
+
VALUES (?, ?, ?, ?, ?)
|
|
110
|
+
`).run(
|
|
111
|
+
cacheCode,
|
|
112
|
+
stock.exchange,
|
|
113
|
+
stock.stockName,
|
|
114
|
+
JSON.stringify(classification.industry),
|
|
115
|
+
JSON.stringify(classification.theme || [])
|
|
116
|
+
);
|
|
117
|
+
if (legacyCode && legacyCode !== cacheCode) {
|
|
118
|
+
db.prepare(`
|
|
119
|
+
DELETE FROM global_stock_metadata WHERE stockCode = ? AND exchange = ?
|
|
120
|
+
`).run(legacyCode, stock.exchange);
|
|
121
|
+
}
|
|
122
|
+
logger.info({
|
|
123
|
+
stockCode: stock.stockCode,
|
|
124
|
+
cacheCode,
|
|
125
|
+
exchange: stock.exchange,
|
|
126
|
+
industry: classification.industry.name,
|
|
127
|
+
themeCount: classification.theme?.length || 0
|
|
128
|
+
}, "[Watchlist] classification cache upserted");
|
|
129
|
+
}
|
|
130
|
+
|
|
66
131
|
export const watchlistTools = {
|
|
67
132
|
add_to_watchlist: {
|
|
68
133
|
name: "add_to_watchlist",
|
|
69
134
|
description: "添加股票到自选列表",
|
|
70
135
|
parameters: AddToWatchlistParamsSchema,
|
|
136
|
+
registerTool: false,
|
|
71
137
|
execute: async (args: AddToWatchlistParams, ctx: { userId: string, runtime?: PluginRuntime }) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const sortRow = db.prepare("SELECT MAX(sortOrder) as max FROM watchlist WHERE userId=? AND isDeleted=0").get(uId) as { max: number } | undefined;
|
|
77
|
-
const nextOrder = (sortRow?.max ?? 0) + 1024;
|
|
78
|
-
|
|
79
|
-
let watchlistId: string;
|
|
80
|
-
const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?").get(uId, args.stockCode, args.exchange) as WatchlistRow | undefined;
|
|
81
|
-
|
|
82
|
-
if (existingItem) {
|
|
83
|
-
watchlistId = existingItem.id;
|
|
84
|
-
if (existingItem.isDeleted === 1) {
|
|
85
|
-
db.prepare(`
|
|
86
|
-
UPDATE watchlist SET isDeleted = 0, stockName = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
|
|
87
|
-
`).run(args.stockName, nextOrder, watchlistId);
|
|
88
|
-
} else {
|
|
89
|
-
db.prepare(`
|
|
90
|
-
UPDATE watchlist SET stockName = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
|
|
91
|
-
`).run(args.stockName, watchlistId);
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
watchlistId = randomUUID();
|
|
95
|
-
db.prepare(`
|
|
96
|
-
INSERT INTO watchlist (id, userId, stockCode, stockName, exchange, market, sortOrder)
|
|
97
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
98
|
-
`).run(watchlistId, uId, args.stockCode, args.stockName, args.exchange, args.market, nextOrder);
|
|
138
|
+
return enqueueWatchlistMutation(async () => {
|
|
139
|
+
const uId = String(ctx.userId);
|
|
140
|
+
if (!ctx.runtime) {
|
|
141
|
+
return JSON.stringify({ success: false, error: "无法分析行业/主题关系:runtime 不可用" });
|
|
99
142
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
updateWatchlistTags(db2, watchlistId, uId, classification.industry, classification.theme);
|
|
107
|
-
}
|
|
108
|
-
}).catch(err => console.error("[Watchlist] 自动分类失败:", err));
|
|
109
|
-
} else {
|
|
110
|
-
updateWatchlistTags(db, watchlistId, uId, args.industry, args.theme);
|
|
143
|
+
const db = getDB();
|
|
144
|
+
const stock = normalizeWatchlistStock(args);
|
|
145
|
+
const existingBeforeClassify = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?")
|
|
146
|
+
.get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
|
|
147
|
+
if (existingBeforeClassify?.isDeleted === 0) {
|
|
148
|
+
return JSON.stringify({ success: true, skipped: true, reason: "duplicate", id: existingBeforeClassify.id });
|
|
111
149
|
}
|
|
112
|
-
db.exec("COMMIT");
|
|
113
|
-
return JSON.stringify({ success: true, id: watchlistId });
|
|
114
|
-
} catch (e: any) {
|
|
115
|
-
db.exec("ROLLBACK");
|
|
116
|
-
return JSON.stringify({ success: false, error: e.message });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
150
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const results: string[] = [];
|
|
131
|
-
const sortRow = db.prepare("SELECT MAX(sortOrder) as max FROM watchlist WHERE userId=? AND isDeleted=0").get(uId) as { max: number } | undefined;
|
|
132
|
-
let currentMaxOrder = sortRow?.max ?? 0;
|
|
151
|
+
let classification: Awaited<ReturnType<typeof watchlistLogic.getStockClassification>>;
|
|
152
|
+
try {
|
|
153
|
+
classification = await watchlistLogic.getStockClassification(ctx.runtime, stock.stockName, stock.stockCode, stock.exchange, uId);
|
|
154
|
+
} catch (e: any) {
|
|
155
|
+
return JSON.stringify({ success: false, error: e.message });
|
|
156
|
+
}
|
|
157
|
+
if (!classification) {
|
|
158
|
+
return JSON.stringify({ success: false, error: "行业/主题关系分析失败" });
|
|
159
|
+
}
|
|
133
160
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
if (db.inTransaction) db.exec("ROLLBACK");
|
|
162
|
+
db.exec("BEGIN TRANSACTION");
|
|
163
|
+
try {
|
|
164
|
+
const sortRow = db.prepare("SELECT MAX(sortOrder) as max FROM watchlist WHERE userId=? AND isDeleted=0").get(uId) as { max: number } | undefined;
|
|
165
|
+
const nextOrder = (sortRow?.max ?? 0) + 1024;
|
|
137
166
|
|
|
138
167
|
let watchlistId: string;
|
|
139
168
|
const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?").get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
|
|
@@ -145,9 +174,8 @@ export const watchlistTools = {
|
|
|
145
174
|
UPDATE watchlist SET isDeleted = 0, stockName = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
|
|
146
175
|
`).run(stock.stockName, nextOrder, watchlistId);
|
|
147
176
|
} else {
|
|
148
|
-
db.
|
|
149
|
-
|
|
150
|
-
`).run(stock.stockName, watchlistId);
|
|
177
|
+
db.exec("ROLLBACK");
|
|
178
|
+
return JSON.stringify({ success: true, skipped: true, reason: "duplicate", id: watchlistId });
|
|
151
179
|
}
|
|
152
180
|
} else {
|
|
153
181
|
watchlistId = randomUUID();
|
|
@@ -156,34 +184,145 @@ export const watchlistTools = {
|
|
|
156
184
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
157
185
|
`).run(watchlistId, uId, stock.stockCode, stock.stockName, stock.exchange, stock.market, nextOrder);
|
|
158
186
|
}
|
|
159
|
-
|
|
187
|
+
|
|
188
|
+
upsertStockClassificationCache(db, stock, classification);
|
|
189
|
+
updateWatchlistTags(db, watchlistId, uId, classification.industry, classification.theme);
|
|
190
|
+
db.exec("COMMIT");
|
|
191
|
+
return JSON.stringify({ success: true, id: watchlistId });
|
|
192
|
+
} catch (e: any) {
|
|
193
|
+
if (db.inTransaction) db.exec("ROLLBACK");
|
|
194
|
+
return JSON.stringify({ success: false, error: e.message });
|
|
160
195
|
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
},
|
|
161
199
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
200
|
+
batch_add_to_watchlist: {
|
|
201
|
+
name: "batch_add_to_watchlist",
|
|
202
|
+
description: "批量添加股票到自选列表。添加多只股票时必须使用这个工具,不要循环调用 add_to_watchlist。",
|
|
203
|
+
parameters: z.object({ stocks: z.array(AddToWatchlistParamsSchema) }),
|
|
204
|
+
execute: async (args: { stocks: AddToWatchlistParams[] }, ctx: { userId: string, runtime?: PluginRuntime }) => {
|
|
205
|
+
return enqueueWatchlistMutation(async () => {
|
|
206
|
+
if (!Array.isArray(args.stocks) || args.stocks.length === 0) {
|
|
207
|
+
return JSON.stringify({ success: false, error: "批量添加失败:stocks 不能为空" });
|
|
208
|
+
}
|
|
209
|
+
if (!ctx.runtime) {
|
|
210
|
+
return JSON.stringify({ success: false, error: "无法分析行业/主题关系:runtime 不可用" });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const db = getDB();
|
|
214
|
+
const uId = String(ctx.userId);
|
|
215
|
+
const uniqueStocks: AddToWatchlistParams[] = [];
|
|
216
|
+
const inputSeen = new Set<string>();
|
|
217
|
+
const skipped: { stockCode: string; exchange: string; reason: "input_duplicate" | "duplicate"; id?: string }[] = [];
|
|
218
|
+
for (const rawStock of args.stocks) {
|
|
219
|
+
const stock = normalizeWatchlistStock(rawStock);
|
|
220
|
+
const key = watchlistStockKey(stock);
|
|
221
|
+
if (inputSeen.has(key)) {
|
|
222
|
+
skipped.push({ stockCode: stock.stockCode, exchange: stock.exchange, reason: "input_duplicate" });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
inputSeen.add(key);
|
|
226
|
+
uniqueStocks.push(stock);
|
|
227
|
+
}
|
|
228
|
+
const stocksToAdd: AddToWatchlistParams[] = [];
|
|
229
|
+
for (const stock of uniqueStocks) {
|
|
230
|
+
const existing = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?")
|
|
231
|
+
.get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
|
|
232
|
+
if (existing?.isDeleted === 0) {
|
|
233
|
+
skipped.push({ stockCode: stock.stockCode, exchange: stock.exchange, reason: "duplicate", id: existing.id });
|
|
173
234
|
} else {
|
|
174
|
-
|
|
175
|
-
args.stocks.forEach((s, i) => {
|
|
176
|
-
updateWatchlistTags(db2, results[i], uId, s.industry, s.theme);
|
|
177
|
-
});
|
|
235
|
+
stocksToAdd.push(stock);
|
|
178
236
|
}
|
|
179
237
|
}
|
|
238
|
+
logger.info({
|
|
239
|
+
count: args.stocks?.length ?? 0,
|
|
240
|
+
addCount: stocksToAdd.length,
|
|
241
|
+
skippedCount: skipped.length,
|
|
242
|
+
codes: args.stocks?.map(stock => stock.stockCode)
|
|
243
|
+
}, "[Watchlist] batch_add_to_watchlist received");
|
|
244
|
+
if (stocksToAdd.length === 0) {
|
|
245
|
+
return JSON.stringify({ success: true, ids: [], skipped });
|
|
246
|
+
}
|
|
180
247
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
248
|
+
let batchResults: Awaited<ReturnType<typeof watchlistLogic.classifyStocksTogether>>;
|
|
249
|
+
try {
|
|
250
|
+
batchResults = await watchlistLogic.classifyStocksTogether(ctx.runtime, stocksToAdd, uId);
|
|
251
|
+
} catch (e: any) {
|
|
252
|
+
logger.warn({
|
|
253
|
+
count: stocksToAdd.length,
|
|
254
|
+
codes: stocksToAdd.map(stock => stock.stockCode),
|
|
255
|
+
error: e.message || String(e)
|
|
256
|
+
}, "[Watchlist] batch_add_to_watchlist classification failed");
|
|
257
|
+
return JSON.stringify({
|
|
258
|
+
success: false,
|
|
259
|
+
failedStage: "classification",
|
|
260
|
+
error: e.message || "批量添加失败:行业/主题关系分析失败"
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (db.inTransaction) db.exec("ROLLBACK");
|
|
265
|
+
db.exec("BEGIN TRANSACTION");
|
|
266
|
+
try {
|
|
267
|
+
const results: string[] = [];
|
|
268
|
+
const writtenItems: { id: string; stock: AddToWatchlistParams; classification: typeof batchResults[number] }[] = [];
|
|
269
|
+
const sortRow = db.prepare("SELECT MAX(sortOrder) as max FROM watchlist WHERE userId=? AND isDeleted=0").get(uId) as { max: number } | undefined;
|
|
270
|
+
let currentMaxOrder = sortRow?.max ?? 0;
|
|
271
|
+
|
|
272
|
+
stocksToAdd.forEach((stock, i) => {
|
|
273
|
+
const nextOrder = currentMaxOrder + 1024;
|
|
274
|
+
currentMaxOrder = nextOrder;
|
|
275
|
+
const classification = batchResults[i];
|
|
276
|
+
if (!classification) {
|
|
277
|
+
throw new Error(`行业/主题关系分析失败: ${stock.stockName}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let watchlistId: string;
|
|
281
|
+
const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?").get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
|
|
282
|
+
|
|
283
|
+
if (existingItem) {
|
|
284
|
+
watchlistId = existingItem.id;
|
|
285
|
+
if (existingItem.isDeleted === 1) {
|
|
286
|
+
db.prepare(`
|
|
287
|
+
UPDATE watchlist SET isDeleted = 0, stockName = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
|
|
288
|
+
`).run(stock.stockName, nextOrder, watchlistId);
|
|
289
|
+
} else {
|
|
290
|
+
skipped.push({ stockCode: stock.stockCode, exchange: stock.exchange, reason: "duplicate", id: watchlistId });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
watchlistId = randomUUID();
|
|
295
|
+
db.prepare(`
|
|
296
|
+
INSERT INTO watchlist (id, userId, stockCode, stockName, exchange, market, sortOrder)
|
|
297
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
298
|
+
`).run(watchlistId, uId, stock.stockCode, stock.stockName, stock.exchange, stock.market, nextOrder);
|
|
299
|
+
}
|
|
300
|
+
results.push(watchlistId);
|
|
301
|
+
writtenItems.push({ id: watchlistId, stock, classification });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (writtenItems.length > 0) {
|
|
305
|
+
writtenItems.forEach(({ id, stock, classification }) => {
|
|
306
|
+
upsertStockClassificationCache(db, stock, classification);
|
|
307
|
+
updateWatchlistTags(db, id, uId, classification.industry, classification.theme);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (results.length !== stocksToAdd.length) {
|
|
312
|
+
logger.info({
|
|
313
|
+
input: args.stocks.length,
|
|
314
|
+
addRequested: stocksToAdd.length,
|
|
315
|
+
written: results.length,
|
|
316
|
+
skipped: skipped.length
|
|
317
|
+
}, "[Watchlist] batch_add_to_watchlist skipped duplicates");
|
|
318
|
+
}
|
|
319
|
+
db.exec("COMMIT");
|
|
320
|
+
return JSON.stringify({ success: true, ids: results, skipped });
|
|
321
|
+
} catch (e: any) {
|
|
322
|
+
if (db.inTransaction) db.exec("ROLLBACK");
|
|
323
|
+
return JSON.stringify({ success: false, error: e.message });
|
|
324
|
+
}
|
|
325
|
+
});
|
|
187
326
|
}
|
|
188
327
|
},
|
|
189
328
|
|
|
@@ -431,9 +570,9 @@ export const watchlistTools = {
|
|
|
431
570
|
try {
|
|
432
571
|
db.prepare("DELETE FROM watchlist_industry_items WHERE userId = ?").run(uId);
|
|
433
572
|
db.prepare("DELETE FROM watchlist_theme_items WHERE userId = ?").run(uId);
|
|
434
|
-
const stocks = db.prepare("SELECT stockName, stockCode, exchange FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
|
|
573
|
+
const stocks = db.prepare("SELECT stockName, stockCode, exchange, market FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
|
|
435
574
|
if (stocks.length > 0) {
|
|
436
|
-
watchlistLogic.getBatchStockClassification(ctx.runtime, stocks, uId)
|
|
575
|
+
watchlistLogic.getBatchStockClassification(ctx.runtime, stocks, uId, { forceRefresh: true })
|
|
437
576
|
.then(batchResults => {
|
|
438
577
|
const db2 = getDB();
|
|
439
578
|
const userStocks = db2.prepare("SELECT id, stockCode, exchange FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
|
|
@@ -442,8 +581,14 @@ export const watchlistTools = {
|
|
|
442
581
|
const s = stocks[i];
|
|
443
582
|
const match = userStocks.find(us => us.stockCode === s.stockCode && us.exchange === s.exchange);
|
|
444
583
|
if (match) {
|
|
584
|
+
upsertStockClassificationCache(db2, {
|
|
585
|
+
stockName: s.stockName,
|
|
586
|
+
stockCode: s.stockCode,
|
|
587
|
+
exchange: s.exchange,
|
|
588
|
+
market: s.market
|
|
589
|
+
}, res);
|
|
445
590
|
const cats = [
|
|
446
|
-
{ name: res.industry.name, type: 'industry' as const, weight: res.industry.weight },
|
|
591
|
+
...(res.industry ? [{ name: res.industry.name, type: 'industry' as const, weight: res.industry.weight }] : []),
|
|
447
592
|
...res.theme.map((t: any) => ({ name: t.name, type: 'theme' as const, weight: t.weight }))
|
|
448
593
|
];
|
|
449
594
|
cats.forEach(c => {
|
|
@@ -466,4 +611,4 @@ export const watchlistTools = {
|
|
|
466
611
|
}
|
|
467
612
|
}
|
|
468
613
|
}
|
|
469
|
-
};
|
|
614
|
+
};
|
package/src/runtime.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import * as os from "node:os";
|
|
4
|
-
import { logger } from "./core/logger";
|
|
4
|
+
import { logger } from "./core/logger.js";
|
|
5
5
|
|
|
6
6
|
let runtime: PluginRuntime | null = null;
|
|
7
7
|
let dbPath: string = "";
|
|
@@ -11,10 +11,10 @@ export function setHedgehogRuntime(next: PluginRuntime): void {
|
|
|
11
11
|
runtime = next;
|
|
12
12
|
|
|
13
13
|
try {
|
|
14
|
-
// 从配置里读取 hedgehog-
|
|
14
|
+
// 从配置里读取 hedgehog-finance 的 workspace
|
|
15
15
|
const cfg = next.config.loadConfig();
|
|
16
16
|
const agentList = (cfg.agents?.list || []) as { id: string, workspace?: string }[];
|
|
17
|
-
const hedgehogAgent = agentList.find((a) => a.id === "hedgehog-
|
|
17
|
+
const hedgehogAgent = agentList.find((a) => a.id === "hedgehog-finance");
|
|
18
18
|
const workspaceDir = hedgehogAgent?.workspace ||
|
|
19
19
|
cfg.agents?.defaults?.workspace ||
|
|
20
20
|
path.join(os.homedir(), ".openclaw", "hedgehog-workspace");
|
package/src/types.ts
CHANGED
|
@@ -62,7 +62,7 @@ export const StockClassificationSchema = z.object({
|
|
|
62
62
|
industry: z.object({
|
|
63
63
|
name: z.string(),
|
|
64
64
|
weight: z.number().min(0).max(100).default(50)
|
|
65
|
-
}).describe("
|
|
65
|
+
}).describe("Required main industry category with weight"),
|
|
66
66
|
theme: z.array(z.object({
|
|
67
67
|
name: z.string(),
|
|
68
68
|
weight: z.number().min(0).max(100).default(50)
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "Bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"baseUrl": ".",
|
|
10
|
-
"types": ["node"],
|
|
11
|
-
"paths": {
|
|
12
|
-
"openclaw/*": ["node_modules/openclaw/*"]
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"include": ["index.ts", "src/**/*.ts"]
|
|
16
|
-
}
|