@hedgehog-finance/hedgehog-plugin 1.0.11

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,469 @@
1
+ import { randomUUID } from "node:crypto";
2
+ // @ts-ignore
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { z } from "openclaw/plugin-sdk/zod";
5
+ import { PluginRuntime } from "openclaw/plugin-sdk";
6
+ import { getDB } from "../../core/database";
7
+ import { watchlistLogic } from "./logic";
8
+ import {
9
+ AddToWatchlistParams,
10
+ AddToWatchlistParamsSchema,
11
+ UpdateWatchlistItemParams,
12
+ UpdateWatchlistItemSchema,
13
+ WatchlistRow,
14
+ CategoryRow,
15
+ TagInput,
16
+ GetWatchlistParams,
17
+ GetWatchlistParamsSchema,
18
+ SyncCategoriesParams,
19
+ SyncCategoriesParamsSchema,
20
+ BatchUpdateSortOrdersParams,
21
+ BatchUpdateSortOrdersParamsSchema
22
+ } from "./schema";
23
+
24
+ /**
25
+ * 辅助函数:统一处理归一化分类项
26
+ */
27
+ function normalizeTags(input: TagInput | undefined): { name: string, weight: number }[] {
28
+ if (!input) return [];
29
+ const items = Array.isArray(input) ? input : [input];
30
+ return items.map(item => {
31
+ if (typeof item === 'string') return { name: item, weight: 0 };
32
+ if (typeof item === 'object' && item !== null && 'name' in item) {
33
+ return { name: item.name, weight: item.weight ?? 0 };
34
+ }
35
+ return null;
36
+ }).filter((i): i is { name: string, weight: number } => i !== null);
37
+ }
38
+
39
+ /**
40
+ * 辅助函数:更新股票的分类标签
41
+ */
42
+ function updateWatchlistTags(db: DatabaseSync, watchlistId: string, userId: string, industry: TagInput | undefined, theme: TagInput | undefined) {
43
+ const normIndustries = normalizeTags(industry);
44
+ for (const item of normIndustries) {
45
+ const categoryId = watchlistLogic._ensureCategory(db, item.name, 'industry', userId);
46
+ if (categoryId) {
47
+ db.prepare(`
48
+ INSERT OR IGNORE INTO watchlist_industry_items (id, watchlistId, userId, categoryId, weight)
49
+ VALUES (?, ?, ?, ?, ?)
50
+ `).run(randomUUID(), watchlistId, userId, categoryId, item.weight);
51
+ }
52
+ }
53
+
54
+ const normThemes = normalizeTags(theme);
55
+ for (const item of normThemes) {
56
+ const categoryId = watchlistLogic._ensureCategory(db, item.name, 'theme', userId);
57
+ if (categoryId) {
58
+ db.prepare(`
59
+ INSERT OR IGNORE INTO watchlist_theme_items (id, watchlistId, userId, categoryId, weight)
60
+ VALUES (?, ?, ?, ?, ?)
61
+ `).run(randomUUID(), watchlistId, userId, categoryId, item.weight);
62
+ }
63
+ }
64
+ }
65
+
66
+ export const watchlistTools = {
67
+ add_to_watchlist: {
68
+ name: "add_to_watchlist",
69
+ description: "添加股票到自选列表",
70
+ parameters: AddToWatchlistParamsSchema,
71
+ execute: async (args: AddToWatchlistParams, ctx: { userId: string, runtime?: PluginRuntime }) => {
72
+ const db = getDB();
73
+ const uId = String(ctx.userId);
74
+ db.exec("BEGIN TRANSACTION");
75
+ try {
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);
99
+ }
100
+
101
+ if (!args.industry && !args.theme && ctx.runtime) {
102
+ watchlistLogic.getStockClassification(ctx.runtime, args.stockName, args.stockCode, args.exchange, uId)
103
+ .then(classification => {
104
+ if (classification) {
105
+ const db2 = getDB();
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);
111
+ }
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
+
121
+ batch_add_to_watchlist: {
122
+ name: "batch_add_to_watchlist",
123
+ description: "批量添加股票到自选列表",
124
+ parameters: z.object({ stocks: z.array(AddToWatchlistParamsSchema) }),
125
+ execute: async (args: { stocks: AddToWatchlistParams[] }, ctx: { userId: string, runtime?: PluginRuntime }) => {
126
+ const db = getDB();
127
+ const uId = String(ctx.userId);
128
+ db.exec("BEGIN TRANSACTION");
129
+ try {
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;
133
+
134
+ for (const stock of args.stocks) {
135
+ const nextOrder = currentMaxOrder + 1024;
136
+ currentMaxOrder = nextOrder;
137
+
138
+ let watchlistId: string;
139
+ const existingItem = db.prepare("SELECT id, isDeleted FROM watchlist WHERE userId = ? AND stockCode = ? AND exchange = ?").get(uId, stock.stockCode, stock.exchange) as WatchlistRow | undefined;
140
+
141
+ if (existingItem) {
142
+ watchlistId = existingItem.id;
143
+ if (existingItem.isDeleted === 1) {
144
+ db.prepare(`
145
+ UPDATE watchlist SET isDeleted = 0, stockName = ?, sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
146
+ `).run(stock.stockName, nextOrder, watchlistId);
147
+ } else {
148
+ db.prepare(`
149
+ UPDATE watchlist SET stockName = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ?
150
+ `).run(stock.stockName, watchlistId);
151
+ }
152
+ } else {
153
+ watchlistId = randomUUID();
154
+ db.prepare(`
155
+ INSERT INTO watchlist (id, userId, stockCode, stockName, exchange, market, sortOrder)
156
+ VALUES (?, ?, ?, ?, ?, ?, ?)
157
+ `).run(watchlistId, uId, stock.stockCode, stock.stockName, stock.exchange, stock.market, nextOrder);
158
+ }
159
+ results.push(watchlistId);
160
+ }
161
+
162
+ if (ctx.runtime && args.stocks.length > 0) {
163
+ if (!args.stocks[0].industry && !args.stocks[0].theme) {
164
+ watchlistLogic.getBatchStockClassification(ctx.runtime, args.stocks, uId)
165
+ .then(batchResults => {
166
+ const db2 = getDB();
167
+ batchResults.forEach((res, i) => {
168
+ if (res) {
169
+ updateWatchlistTags(db2, results[i], uId, res.industry, res.theme);
170
+ }
171
+ });
172
+ }).catch(err => console.error("[Watchlist] 批量自动分类失败:", err));
173
+ } else {
174
+ const db2 = getDB();
175
+ args.stocks.forEach((s, i) => {
176
+ updateWatchlistTags(db2, results[i], uId, s.industry, s.theme);
177
+ });
178
+ }
179
+ }
180
+
181
+ db.exec("COMMIT");
182
+ return JSON.stringify({ success: true, ids: results });
183
+ } catch (e: any) {
184
+ db.exec("ROLLBACK");
185
+ return JSON.stringify({ success: false, error: e.message });
186
+ }
187
+ }
188
+ },
189
+
190
+ get_watchlist: {
191
+ name: "get_watchlist",
192
+ description: "获取自选股列表",
193
+ parameters: GetWatchlistParamsSchema,
194
+ execute: async (args: GetWatchlistParams, ctx: { userId: string }) => {
195
+ try {
196
+ const db = getDB();
197
+ const uId = String(ctx.userId);
198
+ let query: string;
199
+ let params: any[] = [uId];
200
+
201
+ if (args.categoryId && args.categoryType) {
202
+ const table = args.categoryType === "industry" ? "watchlist_industry_items" : "watchlist_theme_items";
203
+ query = `
204
+ SELECT w.*, ci.weight as relWeight
205
+ FROM watchlist w
206
+ JOIN ${table} ci ON w.id = ci.watchlistId
207
+ WHERE w.userId = ? AND w.isDeleted = 0 AND ci.categoryId = ?
208
+ ORDER BY ci.weight DESC, w.sortOrder ASC
209
+ `;
210
+ params.push(args.categoryId);
211
+ } else {
212
+ query = `
213
+ SELECT w.*, (
214
+ SELECT MAX(total.weight) FROM (
215
+ SELECT weight FROM watchlist_industry_items WHERE watchlistId = w.id
216
+ UNION ALL
217
+ SELECT weight FROM watchlist_theme_items WHERE watchlistId = w.id
218
+ UNION ALL
219
+ SELECT 0 as weight
220
+ ) total
221
+ ) as globalWeight
222
+ FROM watchlist w
223
+ WHERE w.userId = ? AND w.isDeleted = 0
224
+ ORDER BY globalWeight DESC, w.sortOrder ASC
225
+ `;
226
+ }
227
+
228
+ const stocks = db.prepare(query).all(...params) as WatchlistRow[];
229
+ const fullList = stocks.map(stock => {
230
+ const industries = db.prepare(`
231
+ SELECT c.name FROM watchlist_categories c
232
+ JOIN watchlist_industry_items i ON c.id = i.categoryId
233
+ WHERE i.watchlistId = ? ORDER BY i.weight DESC
234
+ `).all(stock.id) as { name: string }[];
235
+ const themes = db.prepare(`
236
+ SELECT c.name FROM watchlist_categories c
237
+ JOIN watchlist_theme_items t ON c.id = t.categoryId
238
+ WHERE t.watchlistId = ? ORDER BY t.weight DESC
239
+ `).all(stock.id) as { name: string }[];
240
+ return {
241
+ ...stock,
242
+ industries: industries.map(i => i.name),
243
+ themes: themes.map(t => t.name)
244
+ };
245
+ });
246
+ return JSON.stringify({ success: true, data: fullList });
247
+ } catch (e: any) {
248
+ return JSON.stringify({ success: false, error: e.message });
249
+ }
250
+ }
251
+ },
252
+
253
+ get_thematic_dashboard: {
254
+ name: "get_thematic_dashboard",
255
+ description: "获取聚合后的主题/行业看板数据",
256
+ parameters: z.object({}),
257
+ execute: async (_args: {}, ctx: { userId: string }) => {
258
+ try {
259
+ const db = getDB();
260
+ const uId = String(ctx.userId);
261
+ const industryData = db.prepare(`
262
+ SELECT c.name as category_name, i.weight, w.stockCode
263
+ FROM watchlist_categories c
264
+ JOIN watchlist_industry_items i ON c.id = i.categoryId
265
+ JOIN watchlist w ON i.watchlistId = w.id
266
+ WHERE i.userId = ? AND w.isDeleted = 0
267
+ `).all(uId) as any[];
268
+ const themeData = db.prepare(`
269
+ SELECT c.name as category_name, t.weight, w.stockCode
270
+ FROM watchlist_categories c
271
+ JOIN watchlist_theme_items t ON c.id = t.categoryId
272
+ JOIN watchlist w ON t.watchlistId = w.id
273
+ WHERE t.userId = ? AND w.isDeleted = 0
274
+ `).all(uId) as any[];
275
+ const combined = [...industryData, ...themeData];
276
+ const aggMap = new Map<string, { category_name: string, weight_total: number, stocks: string[] }>();
277
+ combined.forEach(item => {
278
+ const name = item.category_name;
279
+ const existing: { category_name: string, weight_total: number, stocks: string[] } = aggMap.get(name) || { category_name: name, weight_total: 0, stocks: [] };
280
+ existing.weight_total += (item.weight || 0);
281
+ if (!existing.stocks.includes(item.stockCode)) {
282
+ existing.stocks.push(item.stockCode);
283
+ }
284
+ aggMap.set(name, existing);
285
+ });
286
+ const result = Array.from(aggMap.values()).sort((a, b) => b.weight_total - a.weight_total);
287
+ return JSON.stringify({ success: true, data: result });
288
+ } catch (e: any) {
289
+ return JSON.stringify({ success: false, error: e.message });
290
+ }
291
+ }
292
+ },
293
+
294
+ get_watchlist_tabs: {
295
+ name: "get_watchlist_tabs",
296
+ description: "获取分类页签",
297
+ parameters: z.object({}),
298
+ execute: async (_args: {}, ctx: { userId: string }) => {
299
+ try {
300
+ const db = getDB();
301
+ const uId = String(ctx.userId);
302
+ const industries = db.prepare(`
303
+ SELECT DISTINCT c.id, c.name, c.type, c.sortOrder
304
+ FROM watchlist_categories c
305
+ JOIN watchlist_industry_items i ON c.id = i.categoryId
306
+ WHERE i.userId = ?
307
+ ORDER BY c.sortOrder ASC, c.name ASC
308
+ `).all(uId) as any[];
309
+ const themes = db.prepare(`
310
+ SELECT DISTINCT c.id, c.name, c.type, c.sortOrder
311
+ FROM watchlist_categories c
312
+ JOIN watchlist_theme_items t ON c.id = t.categoryId
313
+ WHERE t.userId = ?
314
+ ORDER BY c.sortOrder ASC, c.name ASC
315
+ `).all(uId) as any[];
316
+ const tabs = [
317
+ { id: "all", name: "全部", type: "all" },
318
+ ...industries.map(i => ({ id: i.id, name: i.name, type: "industry" })),
319
+ ...themes.map(t => ({ id: t.id, name: t.name, type: "theme" }))
320
+ ];
321
+ return JSON.stringify({ success: true, data: tabs });
322
+ } catch (e: any) {
323
+ return JSON.stringify({ success: false, error: e.message });
324
+ }
325
+ }
326
+ },
327
+
328
+ smart_reorder_watchlist: {
329
+ name: "smart_reorder_watchlist",
330
+ description: "触发 AI 智能排序",
331
+ parameters: z.object({}),
332
+ execute: async (_args: {}, ctx: { userId: string, runtime?: PluginRuntime }) => {
333
+ if (!ctx.runtime) return JSON.stringify({ success: false, error: "Runtime not available" });
334
+ const db = getDB();
335
+ const uId = String(ctx.userId);
336
+ try {
337
+ const stocks = db.prepare("SELECT stockCode as code, stockName as name FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
338
+ if (stocks.length === 0) return JSON.stringify({ success: true, message: "没有可排序的股票" });
339
+ const sortedResults = await watchlistLogic.applySmartSort(ctx.runtime, `sort-${uId}`, stocks);
340
+ if (sortedResults.length > 0) {
341
+ db.exec("BEGIN TRANSACTION");
342
+ const stmt = db.prepare(`
343
+ UPDATE watchlist SET sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
344
+ WHERE userId = ? AND stockCode = ? AND isDeleted = 0
345
+ `);
346
+ sortedResults.forEach((item: any, i: number) => {
347
+ const currentOrder = i * 10;
348
+ stmt.run(currentOrder, uId, item.code);
349
+ });
350
+ db.exec("COMMIT");
351
+ return JSON.stringify({ success: true, message: "智能排序已完成" });
352
+ }
353
+ return JSON.stringify({ success: false, error: "AI 未能返回有效的排序结果" });
354
+ } catch (e: any) {
355
+ if (db.inTransaction) db.exec("ROLLBACK");
356
+ return JSON.stringify({ success: false, error: e.message });
357
+ }
358
+ }
359
+ },
360
+
361
+ sync_watchlist_categories: {
362
+ name: "sync_watchlist_categories",
363
+ description: "同步分类字典",
364
+ parameters: SyncCategoriesParamsSchema,
365
+ execute: async (args: SyncCategoriesParams, ctx: { userId: string }) => {
366
+ const db = getDB();
367
+ const uId = String(ctx.userId);
368
+ db.exec("BEGIN TRANSACTION");
369
+ try {
370
+ if (args.industries) {
371
+ for (const name of args.industries) {
372
+ db.prepare(`
373
+ INSERT INTO watchlist_categories (id, remoteId, userId, name, type, weight, sortOrder)
374
+ VALUES (?, ?, ?, ?, 'industry', 0, 0)
375
+ ON CONFLICT(userId, remoteId) DO UPDATE SET name = excluded.name, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
376
+ ON CONFLICT(userId, name, type) DO UPDATE SET remoteId = excluded.remoteId, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
377
+ `).run(randomUUID(), name, uId, name);
378
+ }
379
+ }
380
+ if (args.themes) {
381
+ for (const name of args.themes) {
382
+ db.prepare(`
383
+ INSERT INTO watchlist_categories (id, remoteId, userId, name, type, weight, sortOrder)
384
+ VALUES (?, ?, ?, ?, 'theme', 0, 0)
385
+ ON CONFLICT(userId, remoteId) DO UPDATE SET name = excluded.name, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
386
+ ON CONFLICT(userId, name, type) DO UPDATE SET remoteId = excluded.remoteId, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW')
387
+ `).run(randomUUID(), name, uId, name);
388
+ }
389
+ }
390
+ db.exec("COMMIT");
391
+ return JSON.stringify({ success: true });
392
+ } catch (e: any) {
393
+ db.exec("ROLLBACK");
394
+ return JSON.stringify({ success: false, error: e.message });
395
+ }
396
+ }
397
+ },
398
+
399
+ batch_update_sort_orders: {
400
+ name: "batch_update_sort_orders",
401
+ description: "批量更新排序",
402
+ parameters: BatchUpdateSortOrdersParamsSchema,
403
+ execute: async (args: BatchUpdateSortOrdersParams, ctx: { userId: string }) => {
404
+ const db = getDB();
405
+ const uId = String(ctx.userId);
406
+ db.exec("BEGIN TRANSACTION");
407
+ try {
408
+ const stmt = db.prepare(`UPDATE watchlist SET sortOrder = ?, updatedAt = STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'NOW') WHERE id = ? AND userId = ? AND isDeleted = 0`);
409
+ args.orderedIds.forEach((id, i) => {
410
+ const weight = i * 1024;
411
+ stmt.run(weight, id, uId);
412
+ });
413
+ db.exec("COMMIT");
414
+ return JSON.stringify({ success: true });
415
+ } catch (e: any) {
416
+ db.exec("ROLLBACK");
417
+ return JSON.stringify({ success: false, error: e.message });
418
+ }
419
+ }
420
+ },
421
+
422
+ reset_watchlist_classification: {
423
+ name: "reset_watchlist_classification",
424
+ description: "清除并重新进行智能分类",
425
+ parameters: z.object({}),
426
+ execute: async (_args: {}, ctx: { userId: string, runtime?: PluginRuntime }) => {
427
+ if (!ctx.runtime) return JSON.stringify({ success: false, error: "Runtime not available" });
428
+ const db = getDB();
429
+ const uId = String(ctx.userId);
430
+ db.exec("BEGIN TRANSACTION");
431
+ try {
432
+ db.prepare("DELETE FROM watchlist_industry_items WHERE userId = ?").run(uId);
433
+ 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[];
435
+ if (stocks.length > 0) {
436
+ watchlistLogic.getBatchStockClassification(ctx.runtime, stocks, uId)
437
+ .then(batchResults => {
438
+ const db2 = getDB();
439
+ const userStocks = db2.prepare("SELECT id, stockCode, exchange FROM watchlist WHERE userId = ? AND isDeleted = 0").all(uId) as any[];
440
+ batchResults.forEach((res, i) => {
441
+ if (res) {
442
+ const s = stocks[i];
443
+ const match = userStocks.find(us => us.stockCode === s.stockCode && us.exchange === s.exchange);
444
+ if (match) {
445
+ const cats = [
446
+ { name: res.industry.name, type: 'industry' as const, weight: res.industry.weight },
447
+ ...res.theme.map((t: any) => ({ name: t.name, type: 'theme' as const, weight: t.weight }))
448
+ ];
449
+ cats.forEach(c => {
450
+ const catId = watchlistLogic._ensureCategory(db2, c.name, c.type, uId);
451
+ if (catId) {
452
+ const table = c.type === 'industry' ? 'watchlist_industry_items' : 'watchlist_theme_items';
453
+ db2.prepare(`INSERT OR IGNORE INTO ${table} (id, watchlistId, userId, categoryId, weight) VALUES (?, ?, ?, ?, ?)`).run(randomUUID(), match.id, uId, catId, c.weight);
454
+ }
455
+ });
456
+ }
457
+ }
458
+ });
459
+ }).catch(err => console.error("[Watchlist] 重置分类失败:", err));
460
+ }
461
+ db.exec("COMMIT");
462
+ return JSON.stringify({ success: true, message: "重置分类请求已提交" });
463
+ } catch (e: any) {
464
+ db.exec("ROLLBACK");
465
+ return JSON.stringify({ success: false, error: e.message });
466
+ }
467
+ }
468
+ }
469
+ };
package/src/runtime.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
2
+ import path from "path";
3
+ import * as os from "node:os";
4
+ import { logger } from "./core/logger";
5
+
6
+ let runtime: PluginRuntime | null = null;
7
+ let dbPath: string = "";
8
+ let backupDir: string = "";
9
+
10
+ export function setHedgehogRuntime(next: PluginRuntime): void {
11
+ runtime = next;
12
+
13
+ try {
14
+ // 从配置里读取 hedgehog-workspace 的 workspace
15
+ const cfg = next.config.loadConfig();
16
+ const agentList = (cfg.agents?.list || []) as { id: string, workspace?: string }[];
17
+ const hedgehogAgent = agentList.find((a) => a.id === "hedgehog-workspace");
18
+ const workspaceDir = hedgehogAgent?.workspace ||
19
+ cfg.agents?.defaults?.workspace ||
20
+ path.join(os.homedir(), ".openclaw", "hedgehog-workspace");
21
+
22
+ dbPath = path.join(workspaceDir, "data", "business.db");
23
+ backupDir = path.join(workspaceDir, "backups");
24
+ logger.info({ workspaceDir, dbPath }, "resolved workspace");
25
+ } catch (e) {
26
+ logger.error({ err: e }, "Failed to resolve workspace");
27
+ }
28
+ }
29
+
30
+ export function getDbPath(): string {
31
+ if (!dbPath) throw new Error("[hedgehog-app] dbPath not initialized");
32
+ return dbPath;
33
+ }
34
+
35
+ export function getBackupDir(): string {
36
+ if (!backupDir) throw new Error("[hedgehog-app] backupDir not initialized");
37
+ return backupDir;
38
+ }
39
+
40
+ export function getHedgehogRuntime(): PluginRuntime {
41
+ if (!runtime) throw new Error("[hedgehog-app] runtime not initialized");
42
+ return runtime;
43
+ }
package/src/types.ts ADDED
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Hedgehog Finance Resolved Account
5
+ */
6
+ export interface HedgehogFinanceResolvedAccount {
7
+ accountId: string;
8
+ config: {
9
+ token: string;
10
+ code: string;
11
+ };
12
+ enabled: boolean;
13
+ configured: boolean;
14
+ }
15
+
16
+ /**
17
+ * Inbound Message from Relay (Manual Handling)
18
+ */
19
+ export interface RelayInboundMessage {
20
+ type: "req" | "reply" | "item_event" | "usage" | "model" | "reasoning";
21
+ from: string;
22
+ chatId: string;
23
+ id: string;
24
+ text?: string;
25
+ method?: string;
26
+ params?: any;
27
+ replyTo?: string;
28
+ }
29
+
30
+ /**
31
+ * Session entry structure in sessions.json
32
+ */
33
+ export interface OpenClawSessionEntry {
34
+ sessionId: string;
35
+ inputTokens?: number;
36
+ outputTokens?: number;
37
+ totalTokens?: number;
38
+ cacheRead?: number;
39
+ estimatedCostUsd?: number;
40
+ model?: string;
41
+ modelProvider?: string;
42
+ updatedAt?: number;
43
+ }
44
+
45
+ /**
46
+ * Normalized Usage for UI and Internal logic
47
+ */
48
+ export interface TurnUsage {
49
+ input: number;
50
+ output: number;
51
+ total: number;
52
+ cacheRead: number;
53
+ cost: number;
54
+ model: string;
55
+ provider: string;
56
+ }
57
+
58
+ /**
59
+ * Stock Classification Result (AI Schema)
60
+ */
61
+ export const StockClassificationSchema = z.object({
62
+ industry: z.object({
63
+ name: z.string(),
64
+ weight: z.number().min(0).max(100).default(50)
65
+ }).describe("Main industry category with weight"),
66
+ theme: z.array(z.object({
67
+ name: z.string(),
68
+ weight: z.number().min(0).max(100).default(50)
69
+ })).describe("Thematic categories with weights"),
70
+ weight: z.number().min(0).max(100).default(50).describe("Overall priority weight")
71
+ });
72
+
73
+ export type StockClassification = z.infer<typeof StockClassificationSchema>;
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
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
+ }