@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
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
configSchema: import("openclaw/dist/plugin-sdk/index.js").ChannelConfigSchema;
|
|
6
|
+
register: (api: import("openclaw/plugin-sdk/channel-core").OpenClawPluginApi) => void;
|
|
7
|
+
channelPlugin: import("openclaw/plugin-sdk/channel-core").ChannelPlugin<import("./src/types.js").HedgehogFinanceResolvedAccount>;
|
|
8
|
+
setChannelRuntime?: (runtime: import("openclaw/plugin-sdk/channel-core").PluginRuntime) => void;
|
|
9
|
+
};
|
|
10
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
2
|
+
import { hedgehogFinancePlugin } from "./src/channel.js";
|
|
3
|
+
import { setHedgehogRuntime } from "./src/runtime.js";
|
|
4
|
+
import { allFeaturesTools } from "./src/features/index.js";
|
|
5
|
+
export default defineChannelPluginEntry({
|
|
6
|
+
id: "hedgehog_finance",
|
|
7
|
+
name: "Hedgehog Finance Comprehensive Plugin",
|
|
8
|
+
description: "WebSocket Channel & Watchlist SQLite Tools for Hedgehog App",
|
|
9
|
+
plugin: hedgehogFinancePlugin,
|
|
10
|
+
setRuntime(runtime) {
|
|
11
|
+
setHedgehogRuntime(runtime);
|
|
12
|
+
},
|
|
13
|
+
registerFull(api) {
|
|
14
|
+
// 1. 自动化循环注册 Tool
|
|
15
|
+
Object.entries(allFeaturesTools).forEach(([name, tool]) => {
|
|
16
|
+
if (tool.registerTool === false)
|
|
17
|
+
return;
|
|
18
|
+
const registerable = { ...tool, label: tool.description };
|
|
19
|
+
api.registerTool(registerable, { name });
|
|
20
|
+
});
|
|
21
|
+
api.logger.info("[hedgehog-app] Registered tools and runtime context.");
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D,eAAe,wBAAwB,CAAC;IACvC,EAAE,EAAE,kBAAkB;IACtB,IAAI,EAAE,uCAAuC;IAC7C,WAAW,EAAE,6DAA6D;IAC1E,MAAM,EAAE,qBAAqB;IAC7B,UAAU,CAAC,OAAO;QACjB,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IACD,YAAY,CAAC,GAAG;QACf,kBAAkB;QAClB,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE;YACzD,IAAI,IAAI,CAAC,YAAY,KAAK,KAAK;gBAAE,OAAO;YACxC,MAAM,YAAY,GAAG,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;YAC1D,GAAG,CAAC,YAAY,CAAC,YAAmB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC;IACzE,CAAC;CACD,CAAC,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-plugin-common";
|
|
2
|
+
import type { HedgehogFinanceResolvedAccount } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Hedgehog Finance Channel Plugin
|
|
5
|
+
*/
|
|
6
|
+
export declare const hedgehogFinancePlugin: ChannelPlugin<HedgehogFinanceResolvedAccount>;
|
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as fsAsync from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { WebSocket } from "ws";
|
|
6
|
+
import { emptyChannelConfigSchema } from "openclaw/plugin-sdk/core";
|
|
7
|
+
import { getHedgehogRuntime } from "./runtime.js";
|
|
8
|
+
import { logger } from "./core/logger.js";
|
|
9
|
+
import { allFeaturesTools } from "./features/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Unix timestamp
|
|
12
|
+
*/
|
|
13
|
+
function getCurrentTimestamp() {
|
|
14
|
+
return Date.now();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 获取 OpenClaw state 目录
|
|
18
|
+
*/
|
|
19
|
+
function getStateDir() {
|
|
20
|
+
return process.env.OPENCLAW_STATE_DIR ||
|
|
21
|
+
process.env.CLAWD_STATE_DIR ||
|
|
22
|
+
path.join(os.homedir(), ".openclaw");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* [修改] 异步从 sessions.json 获取 session entry
|
|
26
|
+
*/
|
|
27
|
+
async function getSessionEntryAsync(agentId, sessionKey) {
|
|
28
|
+
try {
|
|
29
|
+
const stateDir = getStateDir();
|
|
30
|
+
const sessionStorePath = path.join(stateDir, "agents", agentId, "sessions", "sessions.json");
|
|
31
|
+
if (!fs.existsSync(sessionStorePath)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const content = await fsAsync.readFile(sessionStorePath, "utf-8");
|
|
35
|
+
const storeData = JSON.parse(content);
|
|
36
|
+
const entry = storeData[sessionKey];
|
|
37
|
+
if (!entry?.sessionId) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
sessionId: entry.sessionId,
|
|
42
|
+
inputTokens: entry.inputTokens || 0,
|
|
43
|
+
outputTokens: entry.outputTokens || 0,
|
|
44
|
+
totalTokens: entry.totalTokens || 0,
|
|
45
|
+
model: entry.model,
|
|
46
|
+
modelProvider: entry.modelProvider,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* [修改] 异步获取 .jsonl 文件的当前行数
|
|
55
|
+
*/
|
|
56
|
+
async function getJsonlLineCountAsync(agentId, sessionId) {
|
|
57
|
+
try {
|
|
58
|
+
const stateDir = getStateDir();
|
|
59
|
+
const jsonlPath = path.join(stateDir, "agents", agentId, "sessions", `${sessionId}.jsonl`);
|
|
60
|
+
if (!fs.existsSync(jsonlPath)) {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
const content = await fsAsync.readFile(jsonlPath, "utf-8");
|
|
64
|
+
return content.trim().split("\n").filter(Boolean).length;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* [修改] 异步从 .jsonl session 文件读取指定行号之后的第一条 assistant 消息的 usage
|
|
72
|
+
*/
|
|
73
|
+
async function readUsageFromJsonlAsync(agentId, sessionId, afterLine) {
|
|
74
|
+
try {
|
|
75
|
+
const stateDir = getStateDir();
|
|
76
|
+
const jsonlPath = path.join(stateDir, "agents", agentId, "sessions", `${sessionId}.jsonl`);
|
|
77
|
+
if (!fs.existsSync(jsonlPath)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const content = await fsAsync.readFile(jsonlPath, "utf-8");
|
|
81
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
82
|
+
// 从 afterLine 开始向后找第一条 assistant 消息
|
|
83
|
+
for (let i = afterLine; i < lines.length; i++) {
|
|
84
|
+
try {
|
|
85
|
+
const entry = JSON.parse(lines[i]);
|
|
86
|
+
if (entry.type === "message" && entry.message?.role === "assistant" && entry.message?.usage) {
|
|
87
|
+
const u = entry.message.usage;
|
|
88
|
+
return {
|
|
89
|
+
input: u.input || 0,
|
|
90
|
+
output: u.output || 0,
|
|
91
|
+
total: u.totalTokens || 0,
|
|
92
|
+
model: entry.message.model,
|
|
93
|
+
provider: entry.message.provider,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* [修改] 异步获取本轮对话的 token 用量
|
|
109
|
+
*/
|
|
110
|
+
async function getCurrentTurnUsageAsync(agentId, sessionKey, sessionIdBefore, lineCountBefore, maxRetries = 60, retryDelayMs = 100) {
|
|
111
|
+
// 第一阶段:尝试从 sessions.json 读取
|
|
112
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
113
|
+
if (attempt > 0) {
|
|
114
|
+
await new Promise(r => setTimeout(r, retryDelayMs));
|
|
115
|
+
}
|
|
116
|
+
const entry = await getSessionEntryAsync(agentId, sessionKey);
|
|
117
|
+
if (entry && entry.inputTokens > 0) {
|
|
118
|
+
return {
|
|
119
|
+
input: entry.inputTokens,
|
|
120
|
+
output: entry.outputTokens,
|
|
121
|
+
total: entry.totalTokens,
|
|
122
|
+
model: entry.model,
|
|
123
|
+
provider: entry.modelProvider,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 第二阶段:Fallback 到 .jsonl 文件
|
|
128
|
+
const entry = await getSessionEntryAsync(agentId, sessionKey);
|
|
129
|
+
const sessionId = entry?.sessionId || sessionIdBefore;
|
|
130
|
+
if (!sessionId) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
134
|
+
if (attempt > 0) {
|
|
135
|
+
await new Promise(r => setTimeout(r, 200));
|
|
136
|
+
}
|
|
137
|
+
const startLine = (sessionIdBefore === sessionId) ? lineCountBefore : 0;
|
|
138
|
+
const usage = await readUsageFromJsonlAsync(agentId, sessionId, startLine);
|
|
139
|
+
if (usage && usage.input > 0) {
|
|
140
|
+
return {
|
|
141
|
+
input: usage.input,
|
|
142
|
+
output: usage.output,
|
|
143
|
+
total: usage.total,
|
|
144
|
+
model: usage.model,
|
|
145
|
+
provider: usage.provider,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Hedgehog Finance Channel Plugin
|
|
153
|
+
*/
|
|
154
|
+
export const hedgehogFinancePlugin = {
|
|
155
|
+
id: "hedgehog_finance",
|
|
156
|
+
meta: {
|
|
157
|
+
id: "hedgehog_finance",
|
|
158
|
+
label: "Hedgehog Finance",
|
|
159
|
+
selectionLabel: "Hedgehog Finance",
|
|
160
|
+
blurb: "Custom WebSocket relay channel for Hedgehog App",
|
|
161
|
+
docsPath: "",
|
|
162
|
+
order: 100,
|
|
163
|
+
},
|
|
164
|
+
configSchema: emptyChannelConfigSchema(),
|
|
165
|
+
capabilities: {
|
|
166
|
+
chatTypes: ["direct"],
|
|
167
|
+
media: false,
|
|
168
|
+
reactions: false,
|
|
169
|
+
threads: false,
|
|
170
|
+
blockStreaming: false,
|
|
171
|
+
},
|
|
172
|
+
config: {
|
|
173
|
+
listAccountIds: (cfg) => {
|
|
174
|
+
const channelConfig = (cfg.channels?.['hedgehog_finance'] || {});
|
|
175
|
+
if (channelConfig.accounts) {
|
|
176
|
+
if (Array.isArray(channelConfig.accounts)) {
|
|
177
|
+
return channelConfig.accounts.map((a) => a.accountId || a.id).filter(Boolean);
|
|
178
|
+
}
|
|
179
|
+
return Object.keys(channelConfig.accounts);
|
|
180
|
+
}
|
|
181
|
+
if (channelConfig.accountId) {
|
|
182
|
+
return [channelConfig.accountId];
|
|
183
|
+
}
|
|
184
|
+
return ["default"];
|
|
185
|
+
},
|
|
186
|
+
resolveAccount: (cfg, accountId) => {
|
|
187
|
+
const channelConfig = (cfg.channels?.['hedgehog_finance'] || {});
|
|
188
|
+
const id = accountId || channelConfig.accountId || "default";
|
|
189
|
+
let accountInfo;
|
|
190
|
+
if (channelConfig.accounts) {
|
|
191
|
+
if (Array.isArray(channelConfig.accounts)) {
|
|
192
|
+
accountInfo = channelConfig.accounts.find((a) => (a.accountId || a.id) === id);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
accountInfo = channelConfig.accounts[id];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const { accounts: _, ...defaults } = channelConfig;
|
|
199
|
+
const finalConfig = { ...defaults };
|
|
200
|
+
if (typeof accountInfo === "string") {
|
|
201
|
+
finalConfig.token = accountInfo;
|
|
202
|
+
}
|
|
203
|
+
else if (accountInfo && typeof accountInfo === "object") {
|
|
204
|
+
Object.assign(finalConfig, accountInfo.config || accountInfo);
|
|
205
|
+
}
|
|
206
|
+
const finalAccountId = finalConfig.accountId || id;
|
|
207
|
+
return {
|
|
208
|
+
accountId: finalAccountId,
|
|
209
|
+
config: {
|
|
210
|
+
token: finalConfig.token || "",
|
|
211
|
+
code: finalConfig.code || `OpenClaw-${os.hostname()}`,
|
|
212
|
+
},
|
|
213
|
+
enabled: accountInfo?.enabled !== false,
|
|
214
|
+
configured: Boolean(finalConfig.token),
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
defaultAccountId: (cfg) => {
|
|
218
|
+
const channelConfig = cfg?.channels?.['hedgehog_finance'];
|
|
219
|
+
return channelConfig?.accountId || "default";
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
gateway: {
|
|
223
|
+
startAccount: async (ctx) => {
|
|
224
|
+
const { account, cfg, log, abortSignal } = ctx;
|
|
225
|
+
const rt = getHedgehogRuntime();
|
|
226
|
+
const accountId = String(account.accountId);
|
|
227
|
+
const childLogger = logger.child({ accountId });
|
|
228
|
+
const token = account.config.token || "";
|
|
229
|
+
const code = account.config.code || `OpenClaw-${os.hostname()}`;
|
|
230
|
+
const relayUrl = `wss://relay.ciweiai.com/relay?id=${accountId}&token=${token}&role=provider&code=${code}`;
|
|
231
|
+
let ws = null;
|
|
232
|
+
let isClosing = false;
|
|
233
|
+
let heartbeatInterval = null;
|
|
234
|
+
// [状态管理分离]
|
|
235
|
+
const sentLengthMap = {};
|
|
236
|
+
const reasoningLengthMap = {};
|
|
237
|
+
const commandOutputMap = new Map(); // [新增] 命令输出缓存: itemId -> fullOutput
|
|
238
|
+
const clearStreamStates = () => {
|
|
239
|
+
Object.keys(sentLengthMap).forEach(k => delete sentLengthMap[k]);
|
|
240
|
+
Object.keys(reasoningLengthMap).forEach(k => delete reasoningLengthMap[k]);
|
|
241
|
+
commandOutputMap.clear();
|
|
242
|
+
};
|
|
243
|
+
let resolveStop;
|
|
244
|
+
const stopPromise = new Promise((resolve) => {
|
|
245
|
+
resolveStop = resolve;
|
|
246
|
+
});
|
|
247
|
+
const stopClient = () => {
|
|
248
|
+
if (isClosing)
|
|
249
|
+
return;
|
|
250
|
+
isClosing = true;
|
|
251
|
+
if (heartbeatInterval)
|
|
252
|
+
clearInterval(heartbeatInterval);
|
|
253
|
+
clearStreamStates(); // [生命周期管理]:清理内存状态
|
|
254
|
+
childLogger.info("Stopping gateway...");
|
|
255
|
+
try {
|
|
256
|
+
ws?.close();
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
childLogger.warn({ err: err.message }, "Error during close");
|
|
260
|
+
}
|
|
261
|
+
ctx.setStatus({
|
|
262
|
+
...ctx.getStatus(),
|
|
263
|
+
running: false,
|
|
264
|
+
lastStopAt: getCurrentTimestamp(),
|
|
265
|
+
});
|
|
266
|
+
resolveStop();
|
|
267
|
+
};
|
|
268
|
+
// [事件处理分离]:抽离出独立的 async 消息处理器
|
|
269
|
+
// channel.ts 中的 handleInboundMessage
|
|
270
|
+
const handleInboundMessage = async (data) => {
|
|
271
|
+
ctx.setStatus({
|
|
272
|
+
...ctx.getStatus(),
|
|
273
|
+
lastEventAt: getCurrentTimestamp(),
|
|
274
|
+
});
|
|
275
|
+
try {
|
|
276
|
+
const appPayload = JSON.parse(data.toString());
|
|
277
|
+
// ==========================================
|
|
278
|
+
// 【新增】手动识别并拦截 RPC 请求 (type: "req")
|
|
279
|
+
// ==========================================
|
|
280
|
+
if (appPayload.type === "req") {
|
|
281
|
+
const { id, method, params } = appPayload;
|
|
282
|
+
if (!method)
|
|
283
|
+
return;
|
|
284
|
+
// 从中央工具注册表中查找方法
|
|
285
|
+
if (method === "ping") {
|
|
286
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
287
|
+
ws.send(JSON.stringify({
|
|
288
|
+
type: "res",
|
|
289
|
+
id: id,
|
|
290
|
+
ok: true,
|
|
291
|
+
payload: { success: true }
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const tool = allFeaturesTools[method];
|
|
297
|
+
if (tool && typeof tool.execute === 'function') {
|
|
298
|
+
childLogger.debug({ method }, "拦截到 RPC 请求");
|
|
299
|
+
try {
|
|
300
|
+
// 直接使用websocket中的 accountId 作为绝对安全的 userId。
|
|
301
|
+
const runContext = {
|
|
302
|
+
userId: accountId,
|
|
303
|
+
runtime: rt
|
|
304
|
+
};
|
|
305
|
+
// 执行业务逻辑:传入业务参数 (params) 和安全上下文 (runContext)
|
|
306
|
+
const resultStr = await tool.execute(params, runContext);
|
|
307
|
+
const resultObj = JSON.parse(resultStr);
|
|
308
|
+
// 按照 OpenClaw 官方 res 协议手动回包(成功状态)
|
|
309
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
310
|
+
ws.send(JSON.stringify({
|
|
311
|
+
type: "res",
|
|
312
|
+
id: id,
|
|
313
|
+
ok: true,
|
|
314
|
+
payload: resultObj
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
childLogger.error({ err: err.message, method }, "RPC 执行失败");
|
|
320
|
+
// 按照 OpenClaw 官方 res 协议手动回包(失败状态)
|
|
321
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
322
|
+
ws.send(JSON.stringify({
|
|
323
|
+
type: "res",
|
|
324
|
+
id: id,
|
|
325
|
+
ok: false,
|
|
326
|
+
error: { message: err.message || "RPC execution failed" }
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
333
|
+
ws.send(JSON.stringify({
|
|
334
|
+
type: "res",
|
|
335
|
+
id: id,
|
|
336
|
+
ok: false,
|
|
337
|
+
error: { message: `Unknown RPC method: ${method}` }
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const { from, text, chatId, id } = appPayload;
|
|
343
|
+
if (!text)
|
|
344
|
+
return;
|
|
345
|
+
const route = rt.channel.routing.resolveAgentRoute({
|
|
346
|
+
cfg,
|
|
347
|
+
channel: "hedgehog_finance",
|
|
348
|
+
accountId: String(accountId),
|
|
349
|
+
peer: { kind: "direct", id: chatId },
|
|
350
|
+
});
|
|
351
|
+
const storePath = rt.channel.session.resolveStorePath(cfg.session?.store, {
|
|
352
|
+
agentId: route.agentId,
|
|
353
|
+
});
|
|
354
|
+
const sessionKey = route.sessionKey;
|
|
355
|
+
const agentId = route.agentId;
|
|
356
|
+
const entryBefore = await getSessionEntryAsync(agentId, sessionKey);
|
|
357
|
+
const sessionIdBefore = entryBefore?.sessionId || null;
|
|
358
|
+
const lineCountBefore = sessionIdBefore ? await getJsonlLineCountAsync(agentId, sessionIdBefore) : 0;
|
|
359
|
+
const context = rt.channel.reply.finalizeInboundContext({
|
|
360
|
+
Body: text,
|
|
361
|
+
From: from,
|
|
362
|
+
To: chatId,
|
|
363
|
+
SessionKey: sessionKey,
|
|
364
|
+
AccountId: route.accountId,
|
|
365
|
+
AgentId: agentId,
|
|
366
|
+
AgentWorkspace: route.agentWorkspace,
|
|
367
|
+
Provider: "hedgehog_finance",
|
|
368
|
+
MessageSid: id,
|
|
369
|
+
});
|
|
370
|
+
await rt.channel.session.recordInboundSession({
|
|
371
|
+
storePath,
|
|
372
|
+
sessionKey: context.SessionKey || sessionKey,
|
|
373
|
+
ctx: context,
|
|
374
|
+
updateLastRoute: {
|
|
375
|
+
sessionKey: route.mainSessionKey,
|
|
376
|
+
channel: "hedgehog_finance",
|
|
377
|
+
to: chatId,
|
|
378
|
+
accountId: String(accountId),
|
|
379
|
+
},
|
|
380
|
+
onRecordError: (err) => {
|
|
381
|
+
childLogger.error({ err: String(err) }, "Failed to record inbound session");
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
const startTime = Date.now();
|
|
385
|
+
const sendEvent = (type, data = {}) => {
|
|
386
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
387
|
+
ws.send(JSON.stringify({
|
|
388
|
+
to: from,
|
|
389
|
+
chatId,
|
|
390
|
+
replyTo: id,
|
|
391
|
+
agentId,
|
|
392
|
+
fromCode: code,
|
|
393
|
+
...data,
|
|
394
|
+
type
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const normalizeId = (rawId) => rawId?.replace(/^(command:|tool:|call_)/, '');
|
|
399
|
+
const replyOpts = {
|
|
400
|
+
verboseLevel: 'full',
|
|
401
|
+
shouldEmitToolResult: true,
|
|
402
|
+
shouldEmitToolOutput: true,
|
|
403
|
+
onPartialReply: (payload) => {
|
|
404
|
+
if (payload.text) {
|
|
405
|
+
const prev = sentLengthMap[chatId] || 0;
|
|
406
|
+
const delta = payload.text.slice(prev);
|
|
407
|
+
sentLengthMap[chatId] = payload.text.length;
|
|
408
|
+
if (delta)
|
|
409
|
+
sendEvent("reply", { text: delta, isPartial: true });
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
onReasoningStream: (payload) => {
|
|
413
|
+
if (payload.text) {
|
|
414
|
+
const prev = reasoningLengthMap[chatId] || 0;
|
|
415
|
+
const delta = payload.text.slice(prev);
|
|
416
|
+
reasoningLengthMap[chatId] = payload.text.length;
|
|
417
|
+
if (delta)
|
|
418
|
+
sendEvent("reasoning", { text: delta });
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
onReasoningEnd: () => sendEvent("reasoning_end"),
|
|
422
|
+
onAssistantMessageStart: () => sendEvent("assistant_message_start"),
|
|
423
|
+
onItemEvent: (payload) => {
|
|
424
|
+
const rawId = payload.itemId || payload.toolCallId || `temp_${payload.kind || 'item'}_${payload.title || payload.name || 'unnamed'}`;
|
|
425
|
+
const itemId = normalizeId(rawId);
|
|
426
|
+
sendEvent("item_event", { ...payload, itemId, toolCallId: itemId });
|
|
427
|
+
},
|
|
428
|
+
onCommandOutput: (payload) => {
|
|
429
|
+
const itemId = normalizeId(payload.itemId || payload.toolCallId || 'global');
|
|
430
|
+
const last = commandOutputMap.get(itemId) || "";
|
|
431
|
+
const full = payload.phase === 'delta' ? (last + (payload.output || "")) : (payload.output || "");
|
|
432
|
+
commandOutputMap.set(itemId, full);
|
|
433
|
+
if (payload.output || payload.exitCode !== undefined || payload.status === 'completed') {
|
|
434
|
+
sendEvent("command_output", { ...payload, output: full, itemId });
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
onEnd: async () => {
|
|
438
|
+
const durationMs = Date.now() - startTime;
|
|
439
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
440
|
+
ws.send(JSON.stringify({
|
|
441
|
+
type: "reply",
|
|
442
|
+
to: from,
|
|
443
|
+
chatId: chatId,
|
|
444
|
+
replyTo: id,
|
|
445
|
+
isFinal: true,
|
|
446
|
+
fromCode: code
|
|
447
|
+
}));
|
|
448
|
+
const turnUsage = await getCurrentTurnUsageAsync(agentId, sessionKey, sessionIdBefore, lineCountBefore);
|
|
449
|
+
if (turnUsage) {
|
|
450
|
+
ws.send(JSON.stringify({
|
|
451
|
+
type: "usage",
|
|
452
|
+
to: from,
|
|
453
|
+
chatId: chatId,
|
|
454
|
+
replyTo: id,
|
|
455
|
+
usage: {
|
|
456
|
+
input: turnUsage.input,
|
|
457
|
+
output: turnUsage.output,
|
|
458
|
+
total: turnUsage.total,
|
|
459
|
+
},
|
|
460
|
+
durationMs: durationMs,
|
|
461
|
+
model: turnUsage.model,
|
|
462
|
+
provider: turnUsage.provider,
|
|
463
|
+
fromCode: code
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
delete sentLengthMap[chatId];
|
|
468
|
+
delete reasoningLengthMap[chatId];
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
// 1. 显式类型化的配置 (保证观测开启)
|
|
472
|
+
const finalCfg = {
|
|
473
|
+
...cfg,
|
|
474
|
+
agents: {
|
|
475
|
+
...cfg.agents,
|
|
476
|
+
defaults: {
|
|
477
|
+
...(cfg.agents?.defaults || {}),
|
|
478
|
+
verboseDefault: 'full'
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
// 3. 准备回复选项
|
|
483
|
+
const finalReplyOpts = { ...replyOpts };
|
|
484
|
+
// 4. 执行稳定分发
|
|
485
|
+
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
486
|
+
cfg: finalCfg,
|
|
487
|
+
ctx: context,
|
|
488
|
+
replyOptions: finalReplyOpts,
|
|
489
|
+
dispatcherOptions: {
|
|
490
|
+
deliver: async (payload, info) => {
|
|
491
|
+
// 原有的兜底逻辑保持不变
|
|
492
|
+
const cd = payload.channelData;
|
|
493
|
+
if (cd && (cd.toolCallId || cd.itemId)) {
|
|
494
|
+
sendEvent("tool_result", {
|
|
495
|
+
...payload,
|
|
496
|
+
toolCallId: normalizeId(String(cd.toolCallId || cd.itemId || ""))
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
childLogger.error({ err: err.message }, "Dispatch error");
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
const connect = () => {
|
|
508
|
+
if (isClosing)
|
|
509
|
+
return;
|
|
510
|
+
childLogger.debug({ relayUrl }, "Connecting to relay");
|
|
511
|
+
ws = new WebSocket(relayUrl);
|
|
512
|
+
ws.on("open", () => {
|
|
513
|
+
childLogger.info({ code }, "Connected");
|
|
514
|
+
ctx.setStatus({
|
|
515
|
+
...ctx.getStatus(),
|
|
516
|
+
running: true,
|
|
517
|
+
lastStartAt: getCurrentTimestamp(),
|
|
518
|
+
lastEventAt: getCurrentTimestamp(),
|
|
519
|
+
lastError: null,
|
|
520
|
+
});
|
|
521
|
+
if (heartbeatInterval)
|
|
522
|
+
clearInterval(heartbeatInterval);
|
|
523
|
+
heartbeatInterval = setInterval(() => {
|
|
524
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
525
|
+
ws.ping();
|
|
526
|
+
}
|
|
527
|
+
}, 30000);
|
|
528
|
+
});
|
|
529
|
+
// 绑定消息处理
|
|
530
|
+
ws.on("message", handleInboundMessage);
|
|
531
|
+
ws.on("error", (err) => {
|
|
532
|
+
childLogger.error({ err: err.message }, "WebSocket error");
|
|
533
|
+
ctx.setStatus({
|
|
534
|
+
...ctx.getStatus(),
|
|
535
|
+
lastError: `Connection error: ${err.message}`,
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
ws.on("close", (closeCode, reason) => {
|
|
539
|
+
if (heartbeatInterval)
|
|
540
|
+
clearInterval(heartbeatInterval);
|
|
541
|
+
clearStreamStates(); // [生命周期管理]:断开时清理状态
|
|
542
|
+
if (!isClosing) {
|
|
543
|
+
const retryDelay = 5000 + Math.random() * 5000;
|
|
544
|
+
childLogger.warn({ closeCode, retryDelay: Math.round(retryDelay / 1000) }, "Connection dropped. Retrying...");
|
|
545
|
+
ctx.setStatus({
|
|
546
|
+
...ctx.getStatus(),
|
|
547
|
+
running: false,
|
|
548
|
+
});
|
|
549
|
+
setTimeout(connect, retryDelay);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
};
|
|
553
|
+
connect();
|
|
554
|
+
abortSignal?.addEventListener("abort", () => {
|
|
555
|
+
log?.info?.(`[hedgehog-app][${accountId}] Abort signal received`);
|
|
556
|
+
stopClient();
|
|
557
|
+
});
|
|
558
|
+
await stopPromise;
|
|
559
|
+
return {
|
|
560
|
+
stop: () => {
|
|
561
|
+
stopClient();
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
status: {
|
|
567
|
+
defaultRuntime: {
|
|
568
|
+
accountId: "default",
|
|
569
|
+
running: false,
|
|
570
|
+
lastEventAt: null,
|
|
571
|
+
lastStartAt: null,
|
|
572
|
+
lastStopAt: null,
|
|
573
|
+
lastError: null,
|
|
574
|
+
},
|
|
575
|
+
collectStatusIssues: (accounts) => {
|
|
576
|
+
return accounts.flatMap((account) => {
|
|
577
|
+
if (!account.configured) {
|
|
578
|
+
return [
|
|
579
|
+
{
|
|
580
|
+
channel: "hedgehog_finance",
|
|
581
|
+
accountId: account.accountId,
|
|
582
|
+
kind: "config",
|
|
583
|
+
message: "Account not configured (missing relay token)",
|
|
584
|
+
},
|
|
585
|
+
];
|
|
586
|
+
}
|
|
587
|
+
return [];
|
|
588
|
+
});
|
|
589
|
+
},
|
|
590
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
591
|
+
configured: snapshot?.configured ?? false,
|
|
592
|
+
running: snapshot?.running ?? false,
|
|
593
|
+
lastStartAt: snapshot?.lastStartAt ?? null,
|
|
594
|
+
lastStopAt: snapshot?.lastStopAt ?? null,
|
|
595
|
+
lastError: snapshot?.lastError ?? null,
|
|
596
|
+
}),
|
|
597
|
+
probeAccount: async ({ account }) => {
|
|
598
|
+
if (!account.configured || !account.config?.token) {
|
|
599
|
+
return { ok: false, error: "Token not configured" };
|
|
600
|
+
}
|
|
601
|
+
return { ok: true, details: { relay: "wss://relay.ciweiai.com/relay" } };
|
|
602
|
+
},
|
|
603
|
+
buildAccountSnapshot: ({ account, runtime, snapshot, probe }) => {
|
|
604
|
+
const running = runtime?.running ?? snapshot?.running ?? false;
|
|
605
|
+
return {
|
|
606
|
+
...snapshot,
|
|
607
|
+
accountId: account.accountId,
|
|
608
|
+
enabled: account.enabled,
|
|
609
|
+
configured: account.configured,
|
|
610
|
+
running,
|
|
611
|
+
lastEventAt: runtime?.lastEventAt ?? snapshot?.lastEventAt ?? null,
|
|
612
|
+
lastStartAt: runtime?.lastStartAt ?? snapshot?.lastStartAt ?? null,
|
|
613
|
+
lastStopAt: runtime?.lastStopAt ?? snapshot?.lastStopAt ?? null,
|
|
614
|
+
lastError: runtime?.lastError ?? snapshot?.lastError ?? null,
|
|
615
|
+
probe,
|
|
616
|
+
};
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
//# sourceMappingURL=channel.js.map
|