ocuclaw 0.1.0
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/README.md +25 -0
- package/dist/config/runtime-config.js +165 -0
- package/dist/domain/activity-status-adapter.js +1041 -0
- package/dist/domain/conversation-state.js +516 -0
- package/dist/domain/debug-store.js +700 -0
- package/dist/domain/message-emoji-filter.js +249 -0
- package/dist/domain/readability-system-prompt.js +17 -0
- package/dist/even-ai/even-ai-endpoint.js +938 -0
- package/dist/even-ai/even-ai-model-hook.js +80 -0
- package/dist/even-ai/even-ai-router.js +98 -0
- package/dist/even-ai/even-ai-run-waiter.js +265 -0
- package/dist/even-ai/even-ai-settings-store.js +365 -0
- package/dist/gateway/gateway-bridge.js +175 -0
- package/dist/gateway/openclaw-client.js +1570 -0
- package/dist/index.js +38 -0
- package/dist/runtime/downstream-handler.js +2747 -0
- package/dist/runtime/downstream-server.js +1565 -0
- package/dist/runtime/ocuclaw-settings-store.js +237 -0
- package/dist/runtime/protocol-adapter.js +378 -0
- package/dist/runtime/relay-core.js +1977 -0
- package/dist/runtime/relay-service.js +146 -0
- package/dist/runtime/session-service.js +1026 -0
- package/dist/runtime/upstream-runtime.js +931 -0
- package/openclaw.plugin.json +95 -0
- package/package.json +36 -0
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { filterRawEmojiText } from "../domain/message-emoji-filter.js";
|
|
3
|
+
import { composeReadabilitySystemPrompt } from "../domain/readability-system-prompt.js";
|
|
4
|
+
import { normalizeEvenAiSystemPrompt } from "./even-ai-settings-store.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_RESPONSE_MODEL = "ocuclaw-active-session";
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
8
|
+
const DEFAULT_MAX_BODY_BYTES = 65536;
|
|
9
|
+
const DEFAULT_DEDUP_WINDOW_MS = 500;
|
|
10
|
+
export const EVEN_AI_CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
|
|
11
|
+
const REQUEST_HANDLED_MARKER = Symbol.for("ocuclaw.evenai.handled");
|
|
12
|
+
function normalizeLogger(logger) {
|
|
13
|
+
if (!logger || typeof logger !== "object") {
|
|
14
|
+
return console;
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
info: typeof logger.info === "function" ? logger.info.bind(logger) : console.log,
|
|
18
|
+
warn: typeof logger.warn === "function" ? logger.warn.bind(logger) : console.warn,
|
|
19
|
+
error: typeof logger.error === "function" ? logger.error.bind(logger) : console.error,
|
|
20
|
+
debug:
|
|
21
|
+
typeof logger.debug === "function" ? logger.debug.bind(logger) : console.debug,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizePositiveInt(value, fallback) {
|
|
26
|
+
const parsed = Number(value);
|
|
27
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
return Math.floor(parsed);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function trimString(value) {
|
|
34
|
+
if (typeof value !== "string") return "";
|
|
35
|
+
return value.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeDefaultModel(value) {
|
|
39
|
+
return trimString(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeDefaultThinking(value) {
|
|
43
|
+
const normalized = trimString(value).toLowerCase();
|
|
44
|
+
if (
|
|
45
|
+
["off", "minimal", "low", "medium", "high", "xhigh"].includes(normalized)
|
|
46
|
+
) {
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeSessionKey(value) {
|
|
53
|
+
const trimmed = trimString(value);
|
|
54
|
+
return trimmed || null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseBearerToken(headerValue) {
|
|
58
|
+
const raw = trimString(headerValue);
|
|
59
|
+
if (!raw) return "";
|
|
60
|
+
const match = raw.match(/^Bearer\s+(.+)$/i);
|
|
61
|
+
return match ? trimString(match[1]) : "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildCompletionPayload(opts = {}) {
|
|
65
|
+
const createdMs =
|
|
66
|
+
Number.isFinite(opts.createdMs) && opts.createdMs > 0
|
|
67
|
+
? Math.floor(opts.createdMs)
|
|
68
|
+
: Date.now();
|
|
69
|
+
return {
|
|
70
|
+
id: trimString(opts.id) || `chatcmpl-${randomUUID()}`,
|
|
71
|
+
object: "chat.completion",
|
|
72
|
+
created: Math.floor(createdMs / 1000),
|
|
73
|
+
model: trimString(opts.model) || DEFAULT_RESPONSE_MODEL,
|
|
74
|
+
choices: [
|
|
75
|
+
{
|
|
76
|
+
index: 0,
|
|
77
|
+
message: {
|
|
78
|
+
role: "assistant",
|
|
79
|
+
content: typeof opts.content === "string" ? opts.content : "",
|
|
80
|
+
},
|
|
81
|
+
finish_reason: "stop",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildListenInterceptCloseoutPayload(opts = {}) {
|
|
88
|
+
return buildCompletionPayload({
|
|
89
|
+
id: opts.id,
|
|
90
|
+
createdMs: opts.createdMs,
|
|
91
|
+
model: opts.model,
|
|
92
|
+
content: "\u200B",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function extractLastUserText(payload) {
|
|
97
|
+
if (!payload || !Array.isArray(payload.messages)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (let idx = payload.messages.length - 1; idx >= 0; idx -= 1) {
|
|
102
|
+
const message = payload.messages[idx];
|
|
103
|
+
if (!message || message.role !== "user") continue;
|
|
104
|
+
if (typeof message.content !== "string") {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const text = trimString(message.content);
|
|
108
|
+
return text ? message.content : null;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function classifyHandledError(err) {
|
|
114
|
+
const code = trimString(err && err.code).toLowerCase();
|
|
115
|
+
const message = trimString(err && err.message).toLowerCase();
|
|
116
|
+
|
|
117
|
+
if (code === "evenai_timeout") {
|
|
118
|
+
return {
|
|
119
|
+
event: "request_timeout",
|
|
120
|
+
severity: "warn",
|
|
121
|
+
content: "Even AI request timed out. Please try again.",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
code === "evenai_disconnected" ||
|
|
127
|
+
message.includes("gateway not connected") ||
|
|
128
|
+
message.includes("gateway disconnected") ||
|
|
129
|
+
message.includes("gateway closed")
|
|
130
|
+
) {
|
|
131
|
+
return {
|
|
132
|
+
event: "request_disconnected",
|
|
133
|
+
severity: "warn",
|
|
134
|
+
content: "Even AI is unavailable because OpenClaw is disconnected.",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
event: "request_failed",
|
|
140
|
+
severity: "warn",
|
|
141
|
+
content: "Even AI request failed upstream. Please try again.",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function setJsonHeaders(res) {
|
|
146
|
+
if (res.headersSent) return;
|
|
147
|
+
res.statusCode = 200;
|
|
148
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
149
|
+
res.setHeader("cache-control", "no-store");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function writeJson(res, payload) {
|
|
153
|
+
if (res.writableEnded) return;
|
|
154
|
+
setJsonHeaders(res);
|
|
155
|
+
res.end(JSON.stringify(payload));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function matchesEndpointRoute(req) {
|
|
159
|
+
if (!req || typeof req.method !== "string") return false;
|
|
160
|
+
if (req.method.toUpperCase() !== "POST") return false;
|
|
161
|
+
const url = new URL(req.url || "/", "http://127.0.0.1");
|
|
162
|
+
return url.pathname === EVEN_AI_CHAT_COMPLETIONS_PATH;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readRequestBody(req, maxBodyBytes) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const chunks = [];
|
|
168
|
+
let totalBytes = 0;
|
|
169
|
+
let truncated = false;
|
|
170
|
+
|
|
171
|
+
req.on("error", reject);
|
|
172
|
+
req.on("aborted", () => {
|
|
173
|
+
reject(new Error("request aborted"));
|
|
174
|
+
});
|
|
175
|
+
req.on("data", (chunk) => {
|
|
176
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
177
|
+
totalBytes += buffer.length;
|
|
178
|
+
if (truncated) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (totalBytes > maxBodyBytes) {
|
|
182
|
+
truncated = true;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
chunks.push(buffer);
|
|
186
|
+
});
|
|
187
|
+
req.on("end", () => {
|
|
188
|
+
resolve({
|
|
189
|
+
bodyText: Buffer.concat(chunks).toString("utf8"),
|
|
190
|
+
bodyBytes: totalBytes,
|
|
191
|
+
truncated,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function promiseWithTimeout(promise, timeoutMs) {
|
|
198
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
199
|
+
const timeoutErr = new Error("Even AI request timed out.");
|
|
200
|
+
timeoutErr.code = "evenai_timeout";
|
|
201
|
+
timeoutErr.timeoutMs = timeoutMs;
|
|
202
|
+
return Promise.reject(timeoutErr);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
const timer = setTimeout(() => {
|
|
207
|
+
const timeoutErr = new Error("Even AI request timed out.");
|
|
208
|
+
timeoutErr.code = "evenai_timeout";
|
|
209
|
+
timeoutErr.timeoutMs = timeoutMs;
|
|
210
|
+
reject(timeoutErr);
|
|
211
|
+
}, timeoutMs);
|
|
212
|
+
|
|
213
|
+
Promise.resolve(promise).then(
|
|
214
|
+
(value) => {
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
resolve(value);
|
|
217
|
+
},
|
|
218
|
+
(err) => {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
reject(err);
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function createEvenAiEndpoint(opts = {}) {
|
|
227
|
+
const logger = normalizeLogger(opts.logger);
|
|
228
|
+
const httpServer = opts.httpServer || null;
|
|
229
|
+
const enabled = opts.enabled === true;
|
|
230
|
+
const externallyRouted = opts.externallyRouted === true;
|
|
231
|
+
const token = trimString(opts.token);
|
|
232
|
+
const getSystemPrompt =
|
|
233
|
+
typeof opts.getSystemPrompt === "function"
|
|
234
|
+
? opts.getSystemPrompt
|
|
235
|
+
: () => opts.systemPrompt;
|
|
236
|
+
const getSettingsSnapshot =
|
|
237
|
+
typeof opts.getSettingsSnapshot === "function"
|
|
238
|
+
? opts.getSettingsSnapshot
|
|
239
|
+
: () => opts.settingsSnapshot || {};
|
|
240
|
+
const router = opts.router;
|
|
241
|
+
const gatewayBridge = opts.gatewayBridge;
|
|
242
|
+
const runWaiter = opts.runWaiter;
|
|
243
|
+
const emitDebug = typeof opts.emitDebug === "function" ? opts.emitDebug : () => {};
|
|
244
|
+
const onSessionActivated =
|
|
245
|
+
typeof opts.onSessionActivated === "function" ? opts.onSessionActivated : null;
|
|
246
|
+
const onSessionRouted =
|
|
247
|
+
typeof opts.onSessionRouted === "function" ? opts.onSessionRouted : null;
|
|
248
|
+
const recordFirstSentUserMessage =
|
|
249
|
+
typeof opts.recordFirstSentUserMessage === "function"
|
|
250
|
+
? opts.recordFirstSentUserMessage
|
|
251
|
+
: null;
|
|
252
|
+
const dispatchOcuClawUserSend =
|
|
253
|
+
typeof opts.dispatchOcuClawUserSend === "function"
|
|
254
|
+
? opts.dispatchOcuClawUserSend
|
|
255
|
+
: null;
|
|
256
|
+
const emitListenInterceptRecovery =
|
|
257
|
+
typeof opts.emitListenInterceptRecovery === "function"
|
|
258
|
+
? opts.emitListenInterceptRecovery
|
|
259
|
+
: null;
|
|
260
|
+
const isUpstreamConnected =
|
|
261
|
+
typeof opts.isUpstreamConnected === "function"
|
|
262
|
+
? opts.isUpstreamConnected
|
|
263
|
+
: () => false;
|
|
264
|
+
const hasConnectedAppClient =
|
|
265
|
+
typeof opts.hasConnectedAppClient === "function"
|
|
266
|
+
? opts.hasConnectedAppClient
|
|
267
|
+
: () => false;
|
|
268
|
+
const shouldSeedThinkingForRoute =
|
|
269
|
+
typeof opts.shouldSeedThinkingForRoute === "function"
|
|
270
|
+
? opts.shouldSeedThinkingForRoute
|
|
271
|
+
: async () => false;
|
|
272
|
+
const now =
|
|
273
|
+
typeof opts.now === "function" ? opts.now : () => Date.now();
|
|
274
|
+
const requestTimeoutMs = normalizePositiveInt(
|
|
275
|
+
opts.requestTimeoutMs,
|
|
276
|
+
DEFAULT_TIMEOUT_MS,
|
|
277
|
+
);
|
|
278
|
+
const maxBodyBytes = normalizePositiveInt(
|
|
279
|
+
opts.maxBodyBytes,
|
|
280
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
281
|
+
);
|
|
282
|
+
const dedupWindowMs = Math.max(
|
|
283
|
+
0,
|
|
284
|
+
normalizePositiveInt(opts.dedupWindowMs, DEFAULT_DEDUP_WINDOW_MS),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (!gatewayBridge || typeof gatewayBridge.sendMessage !== "function") {
|
|
288
|
+
throw new Error("Even AI endpoint requires gatewayBridge.sendMessage()");
|
|
289
|
+
}
|
|
290
|
+
if (
|
|
291
|
+
!router ||
|
|
292
|
+
(
|
|
293
|
+
typeof router.resolveTargetSession !== "function" &&
|
|
294
|
+
typeof router.resolveActiveSession !== "function"
|
|
295
|
+
)
|
|
296
|
+
) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
"Even AI endpoint requires router.resolveTargetSession() or router.resolveActiveSession()",
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (!runWaiter || typeof runWaiter.waitForRun !== "function") {
|
|
302
|
+
throw new Error("Even AI endpoint requires runWaiter.waitForRun()");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** @type {{requestId: string, fingerprint: string, sessionKey: string|null, startedAtMs: number}|null} */
|
|
306
|
+
let inFlight = null;
|
|
307
|
+
/** @type {{fingerprint: string, startedAtMs: number}|null} */
|
|
308
|
+
let lastAccepted = null;
|
|
309
|
+
|
|
310
|
+
async function handleRequest(req, res) {
|
|
311
|
+
if (!enabled) return false;
|
|
312
|
+
if (!matchesEndpointRoute(req)) return false;
|
|
313
|
+
if (res.writableEnded) return true;
|
|
314
|
+
|
|
315
|
+
req[REQUEST_HANDLED_MARKER] = true;
|
|
316
|
+
res[REQUEST_HANDLED_MARKER] = true;
|
|
317
|
+
|
|
318
|
+
const requestId = `chatcmpl-${randomUUID()}`;
|
|
319
|
+
const startedAtMs = now();
|
|
320
|
+
const authToken = parseBearerToken(req.headers && req.headers.authorization);
|
|
321
|
+
const configuredSystemPrompt = normalizeEvenAiSystemPrompt(getSystemPrompt());
|
|
322
|
+
const systemPrompt = composeReadabilitySystemPrompt(configuredSystemPrompt);
|
|
323
|
+
|
|
324
|
+
emitDebug(
|
|
325
|
+
"evenai",
|
|
326
|
+
"request_received",
|
|
327
|
+
"info",
|
|
328
|
+
null,
|
|
329
|
+
() => ({
|
|
330
|
+
requestId,
|
|
331
|
+
method: req.method || null,
|
|
332
|
+
bodyLimitBytes: maxBodyBytes,
|
|
333
|
+
hasAuthorization: !!authToken,
|
|
334
|
+
userAgentTail:
|
|
335
|
+
req.headers && typeof req.headers["user-agent"] === "string"
|
|
336
|
+
? req.headers["user-agent"].slice(-120)
|
|
337
|
+
: null,
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (!token || authToken !== token) {
|
|
342
|
+
emitDebug(
|
|
343
|
+
"evenai",
|
|
344
|
+
"request_auth_failed",
|
|
345
|
+
"warn",
|
|
346
|
+
null,
|
|
347
|
+
() => ({
|
|
348
|
+
requestId,
|
|
349
|
+
hasConfiguredToken: !!token,
|
|
350
|
+
hasAuthorization: !!authToken,
|
|
351
|
+
}),
|
|
352
|
+
);
|
|
353
|
+
writeJson(
|
|
354
|
+
res,
|
|
355
|
+
buildCompletionPayload({
|
|
356
|
+
id: requestId,
|
|
357
|
+
createdMs: startedAtMs,
|
|
358
|
+
content: "Authentication failed.",
|
|
359
|
+
}),
|
|
360
|
+
);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let bodyResult;
|
|
365
|
+
try {
|
|
366
|
+
bodyResult = await readRequestBody(req, maxBodyBytes);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
emitDebug(
|
|
369
|
+
"evenai",
|
|
370
|
+
"request_body_read_failed",
|
|
371
|
+
"warn",
|
|
372
|
+
null,
|
|
373
|
+
() => ({
|
|
374
|
+
requestId,
|
|
375
|
+
message: err && err.message ? err.message : String(err),
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
writeJson(
|
|
379
|
+
res,
|
|
380
|
+
buildCompletionPayload({
|
|
381
|
+
id: requestId,
|
|
382
|
+
createdMs: startedAtMs,
|
|
383
|
+
content: "Request body could not be read.",
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (bodyResult.truncated) {
|
|
390
|
+
emitDebug(
|
|
391
|
+
"evenai",
|
|
392
|
+
"request_body_too_large",
|
|
393
|
+
"warn",
|
|
394
|
+
null,
|
|
395
|
+
() => ({
|
|
396
|
+
requestId,
|
|
397
|
+
bodyBytes: bodyResult.bodyBytes,
|
|
398
|
+
maxBodyBytes,
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
writeJson(
|
|
402
|
+
res,
|
|
403
|
+
buildCompletionPayload({
|
|
404
|
+
id: requestId,
|
|
405
|
+
createdMs: startedAtMs,
|
|
406
|
+
content: "Request body exceeds the Even AI size limit.",
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let payload;
|
|
413
|
+
try {
|
|
414
|
+
payload = JSON.parse(bodyResult.bodyText);
|
|
415
|
+
} catch (err) {
|
|
416
|
+
emitDebug(
|
|
417
|
+
"evenai",
|
|
418
|
+
"request_invalid_json",
|
|
419
|
+
"warn",
|
|
420
|
+
null,
|
|
421
|
+
() => ({
|
|
422
|
+
requestId,
|
|
423
|
+
bodyBytes: bodyResult.bodyBytes,
|
|
424
|
+
message: err && err.message ? err.message : String(err),
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
writeJson(
|
|
428
|
+
res,
|
|
429
|
+
buildCompletionPayload({
|
|
430
|
+
id: requestId,
|
|
431
|
+
createdMs: startedAtMs,
|
|
432
|
+
content: "Request body must be valid JSON.",
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const settingsSnapshot = getSettingsSnapshot() || {};
|
|
439
|
+
const configuredDefaultModel = normalizeDefaultModel(settingsSnapshot.defaultModel);
|
|
440
|
+
const configuredDefaultThinking = normalizeDefaultThinking(
|
|
441
|
+
settingsSnapshot.defaultThinking,
|
|
442
|
+
);
|
|
443
|
+
const listenEnabled = settingsSnapshot.listenEnabled === true;
|
|
444
|
+
const responseModel =
|
|
445
|
+
trimString(payload && payload.model) ||
|
|
446
|
+
configuredDefaultModel ||
|
|
447
|
+
DEFAULT_RESPONSE_MODEL;
|
|
448
|
+
const userText = extractLastUserText(payload);
|
|
449
|
+
if (!userText) {
|
|
450
|
+
emitDebug(
|
|
451
|
+
"evenai",
|
|
452
|
+
"request_invalid_messages",
|
|
453
|
+
"warn",
|
|
454
|
+
null,
|
|
455
|
+
() => ({
|
|
456
|
+
requestId,
|
|
457
|
+
bodyBytes: bodyResult.bodyBytes,
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
460
|
+
writeJson(
|
|
461
|
+
res,
|
|
462
|
+
buildCompletionPayload({
|
|
463
|
+
id: requestId,
|
|
464
|
+
createdMs: startedAtMs,
|
|
465
|
+
model: responseModel,
|
|
466
|
+
content: "The last user message must be plain text.",
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const upstreamConnected = isUpstreamConnected();
|
|
473
|
+
const interceptListenRequest =
|
|
474
|
+
listenEnabled &&
|
|
475
|
+
upstreamConnected &&
|
|
476
|
+
typeof router.resolveActiveSession === "function" &&
|
|
477
|
+
dispatchOcuClawUserSend &&
|
|
478
|
+
hasConnectedAppClient();
|
|
479
|
+
const fingerprint = createHash("sha1")
|
|
480
|
+
.update(bodyResult.bodyText || "")
|
|
481
|
+
.digest("hex");
|
|
482
|
+
if (
|
|
483
|
+
lastAccepted &&
|
|
484
|
+
lastAccepted.fingerprint === fingerprint &&
|
|
485
|
+
startedAtMs - lastAccepted.startedAtMs <= dedupWindowMs
|
|
486
|
+
) {
|
|
487
|
+
emitDebug(
|
|
488
|
+
"evenai",
|
|
489
|
+
"request_deduplicated",
|
|
490
|
+
"info",
|
|
491
|
+
null,
|
|
492
|
+
() => ({
|
|
493
|
+
requestId,
|
|
494
|
+
dedupWindowMs,
|
|
495
|
+
}),
|
|
496
|
+
);
|
|
497
|
+
if (interceptListenRequest) {
|
|
498
|
+
writeJson(
|
|
499
|
+
res,
|
|
500
|
+
buildListenInterceptCloseoutPayload({
|
|
501
|
+
id: requestId,
|
|
502
|
+
createdMs: startedAtMs,
|
|
503
|
+
model: responseModel,
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
} else {
|
|
507
|
+
writeJson(
|
|
508
|
+
res,
|
|
509
|
+
buildCompletionPayload({
|
|
510
|
+
id: requestId,
|
|
511
|
+
createdMs: startedAtMs,
|
|
512
|
+
model: responseModel,
|
|
513
|
+
content: "",
|
|
514
|
+
}),
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (!interceptListenRequest && inFlight) {
|
|
521
|
+
emitDebug(
|
|
522
|
+
"evenai",
|
|
523
|
+
"request_busy",
|
|
524
|
+
"info",
|
|
525
|
+
{
|
|
526
|
+
sessionKey: inFlight.sessionKey || undefined,
|
|
527
|
+
},
|
|
528
|
+
() => ({
|
|
529
|
+
requestId,
|
|
530
|
+
activeRequestId: inFlight.requestId,
|
|
531
|
+
}),
|
|
532
|
+
);
|
|
533
|
+
writeJson(
|
|
534
|
+
res,
|
|
535
|
+
buildCompletionPayload({
|
|
536
|
+
id: requestId,
|
|
537
|
+
createdMs: startedAtMs,
|
|
538
|
+
model: responseModel,
|
|
539
|
+
content: "Even AI is busy with another request. Please retry shortly.",
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!interceptListenRequest && !upstreamConnected) {
|
|
546
|
+
emitDebug(
|
|
547
|
+
"evenai",
|
|
548
|
+
"request_disconnected",
|
|
549
|
+
"warn",
|
|
550
|
+
null,
|
|
551
|
+
() => ({
|
|
552
|
+
requestId,
|
|
553
|
+
}),
|
|
554
|
+
);
|
|
555
|
+
writeJson(
|
|
556
|
+
res,
|
|
557
|
+
buildCompletionPayload({
|
|
558
|
+
id: requestId,
|
|
559
|
+
createdMs: startedAtMs,
|
|
560
|
+
model: responseModel,
|
|
561
|
+
content: "Even AI is unavailable because OpenClaw is disconnected.",
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (interceptListenRequest) {
|
|
568
|
+
const sessionKey =
|
|
569
|
+
normalizeSessionKey(
|
|
570
|
+
router.resolveActiveSession({
|
|
571
|
+
requestId,
|
|
572
|
+
model: responseModel,
|
|
573
|
+
userText,
|
|
574
|
+
}),
|
|
575
|
+
) || "main";
|
|
576
|
+
lastAccepted = {
|
|
577
|
+
fingerprint,
|
|
578
|
+
startedAtMs,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
emitDebug(
|
|
582
|
+
"evenai",
|
|
583
|
+
"listen_intercepted",
|
|
584
|
+
"info",
|
|
585
|
+
{ sessionKey },
|
|
586
|
+
() => ({
|
|
587
|
+
requestId,
|
|
588
|
+
bodyBytes: bodyResult.bodyBytes,
|
|
589
|
+
messageChars: userText.length,
|
|
590
|
+
model: responseModel,
|
|
591
|
+
listenEnabled,
|
|
592
|
+
}),
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
void (async () => {
|
|
596
|
+
try {
|
|
597
|
+
const dispatchResult = await Promise.resolve(
|
|
598
|
+
dispatchOcuClawUserSend({
|
|
599
|
+
id: requestId,
|
|
600
|
+
text: userText,
|
|
601
|
+
sessionKey,
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
604
|
+
emitDebug(
|
|
605
|
+
"evenai",
|
|
606
|
+
"listen_intercept_dispatch_succeeded",
|
|
607
|
+
"info",
|
|
608
|
+
{
|
|
609
|
+
sessionKey,
|
|
610
|
+
runId:
|
|
611
|
+
dispatchResult &&
|
|
612
|
+
typeof dispatchResult.runId === "string" &&
|
|
613
|
+
dispatchResult.runId.trim()
|
|
614
|
+
? dispatchResult.runId.trim()
|
|
615
|
+
: undefined,
|
|
616
|
+
},
|
|
617
|
+
() => ({
|
|
618
|
+
requestId,
|
|
619
|
+
elapsedMs: now() - startedAtMs,
|
|
620
|
+
status:
|
|
621
|
+
dispatchResult &&
|
|
622
|
+
typeof dispatchResult.status === "string" &&
|
|
623
|
+
dispatchResult.status.trim()
|
|
624
|
+
? dispatchResult.status.trim()
|
|
625
|
+
: null,
|
|
626
|
+
}),
|
|
627
|
+
);
|
|
628
|
+
} catch (err) {
|
|
629
|
+
let cleanupEmitted = false;
|
|
630
|
+
let cleanupConnectedAppClients = null;
|
|
631
|
+
let cleanupError = null;
|
|
632
|
+
if (emitListenInterceptRecovery) {
|
|
633
|
+
try {
|
|
634
|
+
const recoveryResult = await Promise.resolve(
|
|
635
|
+
emitListenInterceptRecovery({
|
|
636
|
+
requestId,
|
|
637
|
+
sessionKey,
|
|
638
|
+
error: err,
|
|
639
|
+
}),
|
|
640
|
+
);
|
|
641
|
+
cleanupEmitted = recoveryResult
|
|
642
|
+
? recoveryResult.cleanupEmitted === true
|
|
643
|
+
: true;
|
|
644
|
+
cleanupConnectedAppClients =
|
|
645
|
+
recoveryResult &&
|
|
646
|
+
Number.isFinite(recoveryResult.connectedAppClients)
|
|
647
|
+
? Math.floor(recoveryResult.connectedAppClients)
|
|
648
|
+
: null;
|
|
649
|
+
} catch (recoveryErr) {
|
|
650
|
+
cleanupError = recoveryErr;
|
|
651
|
+
logger.warn(
|
|
652
|
+
`[evenai] listen intercept cleanup callback failed: ${recoveryErr && recoveryErr.message ? recoveryErr.message : recoveryErr}`,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
emitDebug(
|
|
657
|
+
"evenai",
|
|
658
|
+
"listen_intercept_dispatch_failed",
|
|
659
|
+
"warn",
|
|
660
|
+
{ sessionKey },
|
|
661
|
+
() => ({
|
|
662
|
+
requestId,
|
|
663
|
+
elapsedMs: now() - startedAtMs,
|
|
664
|
+
code: err && err.code ? err.code : null,
|
|
665
|
+
message: err && err.message ? err.message : String(err),
|
|
666
|
+
cleanupEmitted,
|
|
667
|
+
cleanupConnectedAppClients,
|
|
668
|
+
cleanupError:
|
|
669
|
+
cleanupError && cleanupError.message ? cleanupError.message : null,
|
|
670
|
+
}),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
})();
|
|
674
|
+
|
|
675
|
+
writeJson(
|
|
676
|
+
res,
|
|
677
|
+
buildListenInterceptCloseoutPayload({
|
|
678
|
+
id: requestId,
|
|
679
|
+
createdMs: startedAtMs,
|
|
680
|
+
model: responseModel,
|
|
681
|
+
}),
|
|
682
|
+
);
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
let route;
|
|
687
|
+
try {
|
|
688
|
+
route =
|
|
689
|
+
typeof router.resolveTargetSession === "function"
|
|
690
|
+
? await router.resolveTargetSession({
|
|
691
|
+
requestId,
|
|
692
|
+
model: responseModel,
|
|
693
|
+
userText,
|
|
694
|
+
})
|
|
695
|
+
: {
|
|
696
|
+
routingMode: "active",
|
|
697
|
+
sessionKey: router.resolveActiveSession(),
|
|
698
|
+
previousSessionKey: null,
|
|
699
|
+
sessionChanged: false,
|
|
700
|
+
};
|
|
701
|
+
} catch (err) {
|
|
702
|
+
emitDebug(
|
|
703
|
+
"evenai",
|
|
704
|
+
"request_routing_failed",
|
|
705
|
+
"warn",
|
|
706
|
+
null,
|
|
707
|
+
() => ({
|
|
708
|
+
requestId,
|
|
709
|
+
message: err && err.message ? err.message : String(err),
|
|
710
|
+
}),
|
|
711
|
+
);
|
|
712
|
+
writeJson(
|
|
713
|
+
res,
|
|
714
|
+
buildCompletionPayload({
|
|
715
|
+
id: requestId,
|
|
716
|
+
createdMs: startedAtMs,
|
|
717
|
+
model: responseModel,
|
|
718
|
+
content: "Even AI request failed upstream. Please try again.",
|
|
719
|
+
}),
|
|
720
|
+
);
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const sessionKey = normalizeSessionKey(route && route.sessionKey) || "main";
|
|
725
|
+
const routingMode = trimString(route && route.routingMode) || "active";
|
|
726
|
+
const sessionChanged = !!(route && route.sessionChanged);
|
|
727
|
+
if (recordFirstSentUserMessage) {
|
|
728
|
+
try {
|
|
729
|
+
recordFirstSentUserMessage(sessionKey, userText);
|
|
730
|
+
} catch (err) {
|
|
731
|
+
logger.warn(
|
|
732
|
+
`[evenai] first user message record callback failed: ${err && err.message ? err.message : err}`,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (onSessionRouted) {
|
|
737
|
+
try {
|
|
738
|
+
onSessionRouted({
|
|
739
|
+
...route,
|
|
740
|
+
sessionKey,
|
|
741
|
+
routingMode,
|
|
742
|
+
sessionChanged,
|
|
743
|
+
});
|
|
744
|
+
} catch (err) {
|
|
745
|
+
logger.warn(
|
|
746
|
+
`[evenai] session routed callback failed: ${err && err.message ? err.message : err}`,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (sessionChanged && onSessionActivated) {
|
|
751
|
+
try {
|
|
752
|
+
onSessionActivated({
|
|
753
|
+
...route,
|
|
754
|
+
sessionKey,
|
|
755
|
+
routingMode,
|
|
756
|
+
});
|
|
757
|
+
} catch (err) {
|
|
758
|
+
logger.warn(
|
|
759
|
+
`[evenai] session activation callback failed: ${err && err.message ? err.message : err}`,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
inFlight = {
|
|
764
|
+
requestId,
|
|
765
|
+
fingerprint,
|
|
766
|
+
sessionKey,
|
|
767
|
+
startedAtMs,
|
|
768
|
+
};
|
|
769
|
+
lastAccepted = {
|
|
770
|
+
fingerprint,
|
|
771
|
+
startedAtMs,
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
emitDebug(
|
|
775
|
+
"evenai",
|
|
776
|
+
"request_accepted",
|
|
777
|
+
"info",
|
|
778
|
+
{ sessionKey },
|
|
779
|
+
() => ({
|
|
780
|
+
requestId,
|
|
781
|
+
bodyBytes: bodyResult.bodyBytes,
|
|
782
|
+
messageChars: userText.length,
|
|
783
|
+
model: responseModel,
|
|
784
|
+
extraSystemPromptChars: systemPrompt.length,
|
|
785
|
+
routingMode,
|
|
786
|
+
sessionChanged,
|
|
787
|
+
}),
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
const sendOptions = { extraSystemPrompt: systemPrompt };
|
|
792
|
+
if (
|
|
793
|
+
configuredDefaultThinking &&
|
|
794
|
+
await Promise.resolve(
|
|
795
|
+
shouldSeedThinkingForRoute({
|
|
796
|
+
route,
|
|
797
|
+
sessionKey,
|
|
798
|
+
routingMode,
|
|
799
|
+
thinkingLevel: configuredDefaultThinking,
|
|
800
|
+
}),
|
|
801
|
+
)
|
|
802
|
+
) {
|
|
803
|
+
sendOptions.thinking = configuredDefaultThinking;
|
|
804
|
+
}
|
|
805
|
+
const ack = await promiseWithTimeout(
|
|
806
|
+
gatewayBridge.sendMessage(
|
|
807
|
+
userText,
|
|
808
|
+
sessionKey,
|
|
809
|
+
null,
|
|
810
|
+
sendOptions,
|
|
811
|
+
),
|
|
812
|
+
requestTimeoutMs,
|
|
813
|
+
);
|
|
814
|
+
const runId = trimString(ack && ack.runId);
|
|
815
|
+
if (!runId) {
|
|
816
|
+
throw new Error("Even AI upstream ack was missing a runId.");
|
|
817
|
+
}
|
|
818
|
+
if (trimString(ack && ack.status) && trimString(ack.status) !== "accepted") {
|
|
819
|
+
throw new Error(
|
|
820
|
+
trimString(ack && ack.error) || `Even AI upstream returned ${ack.status}.`,
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
emitDebug(
|
|
825
|
+
"evenai",
|
|
826
|
+
"request_dispatched",
|
|
827
|
+
"debug",
|
|
828
|
+
{ sessionKey, runId },
|
|
829
|
+
() => ({
|
|
830
|
+
requestId,
|
|
831
|
+
elapsedMs: now() - startedAtMs,
|
|
832
|
+
}),
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
const remainingTimeoutMs = Math.max(1, requestTimeoutMs - (now() - startedAtMs));
|
|
836
|
+
const assistantText = await runWaiter.waitForRun({
|
|
837
|
+
runId,
|
|
838
|
+
sessionKey,
|
|
839
|
+
timeoutMs: remainingTimeoutMs,
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
emitDebug(
|
|
843
|
+
"evenai",
|
|
844
|
+
"request_completed",
|
|
845
|
+
"info",
|
|
846
|
+
{ sessionKey, runId },
|
|
847
|
+
() => ({
|
|
848
|
+
requestId,
|
|
849
|
+
elapsedMs: now() - startedAtMs,
|
|
850
|
+
textChars: assistantText.length,
|
|
851
|
+
}),
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
writeJson(
|
|
855
|
+
res,
|
|
856
|
+
buildCompletionPayload({
|
|
857
|
+
id: requestId,
|
|
858
|
+
createdMs: startedAtMs,
|
|
859
|
+
model: responseModel,
|
|
860
|
+
content: filterRawEmojiText(assistantText),
|
|
861
|
+
}),
|
|
862
|
+
);
|
|
863
|
+
return true;
|
|
864
|
+
} catch (err) {
|
|
865
|
+
const handled = classifyHandledError(err);
|
|
866
|
+
emitDebug(
|
|
867
|
+
"evenai",
|
|
868
|
+
handled.event,
|
|
869
|
+
handled.severity,
|
|
870
|
+
{ sessionKey },
|
|
871
|
+
() => ({
|
|
872
|
+
requestId,
|
|
873
|
+
elapsedMs: now() - startedAtMs,
|
|
874
|
+
code: err && err.code ? err.code : null,
|
|
875
|
+
message: err && err.message ? err.message : String(err),
|
|
876
|
+
}),
|
|
877
|
+
);
|
|
878
|
+
writeJson(
|
|
879
|
+
res,
|
|
880
|
+
buildCompletionPayload({
|
|
881
|
+
id: requestId,
|
|
882
|
+
createdMs: startedAtMs,
|
|
883
|
+
model: responseModel,
|
|
884
|
+
content: handled.content,
|
|
885
|
+
}),
|
|
886
|
+
);
|
|
887
|
+
return true;
|
|
888
|
+
} finally {
|
|
889
|
+
if (inFlight && inFlight.requestId === requestId) {
|
|
890
|
+
inFlight = null;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const onRequest = (req, res) => {
|
|
896
|
+
handleRequest(req, res).catch((err) => {
|
|
897
|
+
logger.error(`[evenai] endpoint request failed: ${err.message}`);
|
|
898
|
+
if (!res.writableEnded) {
|
|
899
|
+
writeJson(
|
|
900
|
+
res,
|
|
901
|
+
buildCompletionPayload({
|
|
902
|
+
content: "Even AI request failed upstream. Please try again.",
|
|
903
|
+
}),
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
let attached = false;
|
|
910
|
+
if (enabled) {
|
|
911
|
+
if (httpServer && typeof httpServer.prependListener === "function") {
|
|
912
|
+
httpServer.prependListener("request", onRequest);
|
|
913
|
+
attached = true;
|
|
914
|
+
} else if (httpServer && typeof httpServer.on === "function") {
|
|
915
|
+
httpServer.on("request", onRequest);
|
|
916
|
+
attached = true;
|
|
917
|
+
} else if (!externallyRouted) {
|
|
918
|
+
logger.warn("[evenai] evenAiEnabled is set but no shared httpServer was provided");
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
close() {
|
|
924
|
+
if (
|
|
925
|
+
attached &&
|
|
926
|
+
httpServer &&
|
|
927
|
+
typeof httpServer.removeListener === "function"
|
|
928
|
+
) {
|
|
929
|
+
httpServer.removeListener("request", onRequest);
|
|
930
|
+
}
|
|
931
|
+
attached = false;
|
|
932
|
+
},
|
|
933
|
+
|
|
934
|
+
handleRequest,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export default createEvenAiEndpoint;
|