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,80 @@
|
|
|
1
|
+
import { normalizeEvenAiDefaultModel } from "./even-ai-settings-store.js";
|
|
2
|
+
|
|
3
|
+
const THROWAWAY_SESSION_PREFIX = "ocuclaw:even-ai:";
|
|
4
|
+
const DEFAULT_DEDICATED_SESSION_KEY = "ocuclaw:even-ai";
|
|
5
|
+
|
|
6
|
+
function trimString(value) {
|
|
7
|
+
if (typeof value !== "string") {
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
return value.trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeSessionKey(value) {
|
|
14
|
+
return trimString(value).toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseModelRef(value) {
|
|
18
|
+
const normalized = normalizeEvenAiDefaultModel(value);
|
|
19
|
+
if (!normalized) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const slash = normalized.indexOf("/");
|
|
23
|
+
if (slash <= 0 || slash >= normalized.length - 1) {
|
|
24
|
+
return {
|
|
25
|
+
modelOverride: normalized,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const providerOverride = normalized.slice(0, slash).trim();
|
|
29
|
+
const modelOverride = normalized.slice(slash + 1).trim();
|
|
30
|
+
if (!providerOverride || !modelOverride) {
|
|
31
|
+
return {
|
|
32
|
+
modelOverride: normalized,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
providerOverride,
|
|
37
|
+
modelOverride,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isEvenAiModelHookSession(sessionKey, dedicatedSessionKey) {
|
|
42
|
+
const normalizedSessionKey = normalizeSessionKey(sessionKey);
|
|
43
|
+
if (!normalizedSessionKey) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
normalizedSessionKey === DEFAULT_DEDICATED_SESSION_KEY ||
|
|
48
|
+
normalizedSessionKey.startsWith(THROWAWAY_SESSION_PREFIX)
|
|
49
|
+
) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const normalizedDedicatedKey = normalizeSessionKey(dedicatedSessionKey);
|
|
53
|
+
return !!normalizedDedicatedKey && normalizedSessionKey === normalizedDedicatedKey;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createEvenAiModelHook(opts = {}) {
|
|
57
|
+
const getSettingsSnapshot =
|
|
58
|
+
typeof opts.getSettingsSnapshot === "function"
|
|
59
|
+
? opts.getSettingsSnapshot
|
|
60
|
+
: () => ({});
|
|
61
|
+
const getDedicatedSessionKey =
|
|
62
|
+
typeof opts.getDedicatedSessionKey === "function"
|
|
63
|
+
? opts.getDedicatedSessionKey
|
|
64
|
+
: () => opts.dedicatedSessionKey;
|
|
65
|
+
|
|
66
|
+
return function evenAiBeforeModelResolve(_event, ctx) {
|
|
67
|
+
const sessionKey = ctx && typeof ctx.sessionKey === "string" ? ctx.sessionKey : "";
|
|
68
|
+
if (!isEvenAiModelHookSession(sessionKey, getDedicatedSessionKey())) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const settings = getSettingsSnapshot() || {};
|
|
72
|
+
const parsed = parseModelRef(settings.defaultModel);
|
|
73
|
+
if (!parsed || !parsed.modelOverride) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default createEvenAiModelHook;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { normalizeEvenAiRoutingMode } from "./even-ai-settings-store.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_EVEN_AI_DEDICATED_SESSION_KEY = "ocuclaw:even-ai";
|
|
4
|
+
|
|
5
|
+
function normalizeSessionKey(value) {
|
|
6
|
+
if (typeof value !== "string") return null;
|
|
7
|
+
const trimmed = value.trim();
|
|
8
|
+
return trimmed ? trimmed : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeDedicatedSessionKey(value) {
|
|
12
|
+
const normalized = normalizeSessionKey(value);
|
|
13
|
+
if (!normalized) {
|
|
14
|
+
return DEFAULT_EVEN_AI_DEDICATED_SESSION_KEY;
|
|
15
|
+
}
|
|
16
|
+
return normalized.toLowerCase().startsWith("ocuclaw:")
|
|
17
|
+
? normalized
|
|
18
|
+
: DEFAULT_EVEN_AI_DEDICATED_SESSION_KEY;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createEvenAiRouter(opts = {}) {
|
|
22
|
+
const sessionService = opts.sessionService;
|
|
23
|
+
if (!sessionService || typeof sessionService.ensureSessionKey !== "function") {
|
|
24
|
+
throw new Error("Even AI router requires sessionService.ensureSessionKey()");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const getRoutingMode =
|
|
28
|
+
typeof opts.getRoutingMode === "function"
|
|
29
|
+
? opts.getRoutingMode
|
|
30
|
+
: () => opts.routingMode;
|
|
31
|
+
const dedicatedSessionKey = normalizeDedicatedSessionKey(
|
|
32
|
+
opts.dedicatedSessionKey,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
async function resolveTargetSession() {
|
|
36
|
+
const routingMode = normalizeEvenAiRoutingMode(getRoutingMode());
|
|
37
|
+
const previousSessionKey =
|
|
38
|
+
typeof sessionService.peekSessionKey === "function"
|
|
39
|
+
? normalizeSessionKey(sessionService.peekSessionKey())
|
|
40
|
+
: null;
|
|
41
|
+
|
|
42
|
+
if (routingMode === "background") {
|
|
43
|
+
return {
|
|
44
|
+
routingMode,
|
|
45
|
+
sessionKey: dedicatedSessionKey,
|
|
46
|
+
previousSessionKey,
|
|
47
|
+
sessionChanged: false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (routingMode === "background_new") {
|
|
52
|
+
if (typeof sessionService.createDetachedSessionKey !== "function") {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Even AI router requires sessionService.createDetachedSessionKey()",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const sessionKey = normalizeSessionKey(
|
|
58
|
+
await Promise.resolve(
|
|
59
|
+
sessionService.createDetachedSessionKey("ocuclaw:even-ai:"),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
if (!sessionKey) {
|
|
63
|
+
throw new Error("Even AI router failed to create a detached session key.");
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
routingMode,
|
|
67
|
+
sessionKey,
|
|
68
|
+
previousSessionKey,
|
|
69
|
+
sessionChanged: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
routingMode: normalizeEvenAiRoutingMode(),
|
|
75
|
+
sessionKey: normalizeSessionKey(sessionService.ensureSessionKey()) || "main",
|
|
76
|
+
previousSessionKey,
|
|
77
|
+
sessionChanged: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
getRoutingMode() {
|
|
83
|
+
return normalizeEvenAiRoutingMode(getRoutingMode());
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
getDedicatedSessionKey() {
|
|
87
|
+
return dedicatedSessionKey;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
resolveActiveSession() {
|
|
91
|
+
return normalizeSessionKey(sessionService.ensureSessionKey()) || "main";
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
resolveTargetSession,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default createEvenAiRouter;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
function normalizeLogger(logger) {
|
|
2
|
+
if (!logger || typeof logger !== "object") {
|
|
3
|
+
return console;
|
|
4
|
+
}
|
|
5
|
+
return {
|
|
6
|
+
info: typeof logger.info === "function" ? logger.info.bind(logger) : console.log,
|
|
7
|
+
warn: typeof logger.warn === "function" ? logger.warn.bind(logger) : console.warn,
|
|
8
|
+
error: typeof logger.error === "function" ? logger.error.bind(logger) : console.error,
|
|
9
|
+
debug:
|
|
10
|
+
typeof logger.debug === "function" ? logger.debug.bind(logger) : console.debug,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeRunId(value) {
|
|
15
|
+
if (typeof value !== "string") return null;
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
return trimmed ? trimmed : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeSessionKey(value) {
|
|
21
|
+
if (typeof value !== "string") return null;
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
return trimmed ? trimmed : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeSessionKeyForCompare(value) {
|
|
27
|
+
const normalized = normalizeSessionKey(value);
|
|
28
|
+
if (!normalized) return "";
|
|
29
|
+
const lowered = normalized.toLowerCase();
|
|
30
|
+
const prefixIndex = lowered.indexOf("ocuclaw:");
|
|
31
|
+
return (prefixIndex >= 0 ? normalized.slice(prefixIndex) : normalized).toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sessionKeysMatch(expected, actual) {
|
|
35
|
+
const normalizedExpected = normalizeSessionKeyForCompare(expected);
|
|
36
|
+
const normalizedActual = normalizeSessionKeyForCompare(actual);
|
|
37
|
+
if (!normalizedExpected || !normalizedActual) return false;
|
|
38
|
+
return (
|
|
39
|
+
normalizedExpected === normalizedActual ||
|
|
40
|
+
normalizedExpected.endsWith(`:${normalizedActual}`) ||
|
|
41
|
+
normalizedActual.endsWith(`:${normalizedExpected}`)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function extractAssistantText(content) {
|
|
46
|
+
if (typeof content === "string") {
|
|
47
|
+
return content;
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(content)) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
return content
|
|
53
|
+
.filter((block) => {
|
|
54
|
+
return (
|
|
55
|
+
block &&
|
|
56
|
+
block.type === "text" &&
|
|
57
|
+
typeof block.text === "string"
|
|
58
|
+
);
|
|
59
|
+
})
|
|
60
|
+
.map((block) => block.text)
|
|
61
|
+
.join("");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createRunWaiterError(code, message, extras = {}) {
|
|
65
|
+
const err = new Error(message);
|
|
66
|
+
err.code = code;
|
|
67
|
+
Object.assign(err, extras);
|
|
68
|
+
return err;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createEvenAiRunWaiter(opts = {}) {
|
|
72
|
+
const gatewayBridge = opts.gatewayBridge;
|
|
73
|
+
if (!gatewayBridge || typeof gatewayBridge.on !== "function") {
|
|
74
|
+
throw new Error("Even AI run waiter requires gatewayBridge.on()");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const logger = normalizeLogger(opts.logger);
|
|
78
|
+
const emitDebug = typeof opts.emitDebug === "function" ? opts.emitDebug : () => {};
|
|
79
|
+
const setTimeoutFn =
|
|
80
|
+
typeof opts.setTimeout === "function" ? opts.setTimeout : setTimeout;
|
|
81
|
+
const clearTimeoutFn =
|
|
82
|
+
typeof opts.clearTimeout === "function" ? opts.clearTimeout : clearTimeout;
|
|
83
|
+
|
|
84
|
+
/** @type {Map<string, {resolve: Function, reject: Function, sessionKey: string|null, timer: any, timeoutMs: number|null}>} */
|
|
85
|
+
const pendingRuns = new Map();
|
|
86
|
+
|
|
87
|
+
function cleanupPending(runId) {
|
|
88
|
+
const pending = pendingRuns.get(runId);
|
|
89
|
+
if (!pending) return null;
|
|
90
|
+
pendingRuns.delete(runId);
|
|
91
|
+
if (pending.timer) {
|
|
92
|
+
clearTimeoutFn(pending.timer);
|
|
93
|
+
}
|
|
94
|
+
return pending;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function rejectRun(runId, err, eventName) {
|
|
98
|
+
const pending = cleanupPending(runId);
|
|
99
|
+
if (!pending) return false;
|
|
100
|
+
emitDebug(
|
|
101
|
+
"evenai",
|
|
102
|
+
eventName,
|
|
103
|
+
err && err.code === "evenai_timeout" ? "warn" : "error",
|
|
104
|
+
{
|
|
105
|
+
sessionKey: pending.sessionKey || undefined,
|
|
106
|
+
runId,
|
|
107
|
+
},
|
|
108
|
+
() => ({
|
|
109
|
+
timeoutMs: pending.timeoutMs,
|
|
110
|
+
code: err && err.code ? err.code : null,
|
|
111
|
+
message: err && err.message ? err.message : String(err),
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
pending.reject(err);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveRun(runId, text) {
|
|
119
|
+
const pending = cleanupPending(runId);
|
|
120
|
+
if (!pending) return false;
|
|
121
|
+
emitDebug(
|
|
122
|
+
"evenai",
|
|
123
|
+
"run_wait_resolved",
|
|
124
|
+
"debug",
|
|
125
|
+
{
|
|
126
|
+
sessionKey: pending.sessionKey || undefined,
|
|
127
|
+
runId,
|
|
128
|
+
},
|
|
129
|
+
() => ({
|
|
130
|
+
timeoutMs: pending.timeoutMs,
|
|
131
|
+
textChars: text.length,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
pending.resolve(text);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const offMessage = gatewayBridge.on("message", (data) => {
|
|
139
|
+
const runId = normalizeRunId(data && data.runId);
|
|
140
|
+
if (!runId || !pendingRuns.has(runId)) return;
|
|
141
|
+
const pending = pendingRuns.get(runId);
|
|
142
|
+
const sessionKey = normalizeSessionKey(data && data.sessionKey);
|
|
143
|
+
if (
|
|
144
|
+
pending &&
|
|
145
|
+
pending.sessionKey &&
|
|
146
|
+
sessionKey &&
|
|
147
|
+
!sessionKeysMatch(pending.sessionKey, sessionKey)
|
|
148
|
+
) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (data && typeof data.role === "string" && data.role !== "assistant") {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
resolveRun(runId, extractAssistantText(data && data.content));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const offActivity = gatewayBridge.on("activity", (data) => {
|
|
158
|
+
const runId = normalizeRunId(data && data.runId);
|
|
159
|
+
if (!runId || !pendingRuns.has(runId)) return;
|
|
160
|
+
const phase = typeof data.phase === "string" ? data.phase.trim().toLowerCase() : "";
|
|
161
|
+
if (phase !== "error") return;
|
|
162
|
+
rejectRun(
|
|
163
|
+
runId,
|
|
164
|
+
createRunWaiterError(
|
|
165
|
+
"evenai_upstream_error",
|
|
166
|
+
"Even AI run failed before completing.",
|
|
167
|
+
),
|
|
168
|
+
"run_wait_failed",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const offDisconnected = gatewayBridge.on("disconnected", () => {
|
|
173
|
+
for (const runId of Array.from(pendingRuns.keys())) {
|
|
174
|
+
rejectRun(
|
|
175
|
+
runId,
|
|
176
|
+
createRunWaiterError(
|
|
177
|
+
"evenai_disconnected",
|
|
178
|
+
"Gateway disconnected while waiting for Even AI completion.",
|
|
179
|
+
),
|
|
180
|
+
"run_wait_disconnected",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
waitForRun(request = {}) {
|
|
187
|
+
const runId = normalizeRunId(request.runId);
|
|
188
|
+
if (!runId) {
|
|
189
|
+
return Promise.reject(
|
|
190
|
+
createRunWaiterError(
|
|
191
|
+
"evenai_missing_run_id",
|
|
192
|
+
"Even AI run waiter requires a runId.",
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (pendingRuns.has(runId)) {
|
|
197
|
+
return Promise.reject(
|
|
198
|
+
createRunWaiterError(
|
|
199
|
+
"evenai_duplicate_wait",
|
|
200
|
+
`Even AI run ${runId} is already being awaited.`,
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const sessionKey = normalizeSessionKey(request.sessionKey);
|
|
206
|
+
const timeoutMs =
|
|
207
|
+
Number.isFinite(request.timeoutMs) && request.timeoutMs > 0
|
|
208
|
+
? Math.floor(request.timeoutMs)
|
|
209
|
+
: null;
|
|
210
|
+
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const pending = {
|
|
213
|
+
resolve,
|
|
214
|
+
reject,
|
|
215
|
+
sessionKey,
|
|
216
|
+
timer: null,
|
|
217
|
+
timeoutMs,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (timeoutMs !== null) {
|
|
221
|
+
pending.timer = setTimeoutFn(() => {
|
|
222
|
+
rejectRun(
|
|
223
|
+
runId,
|
|
224
|
+
createRunWaiterError(
|
|
225
|
+
"evenai_timeout",
|
|
226
|
+
"Even AI request timed out.",
|
|
227
|
+
{ timeoutMs },
|
|
228
|
+
),
|
|
229
|
+
"run_wait_timeout",
|
|
230
|
+
);
|
|
231
|
+
}, timeoutMs);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pendingRuns.set(runId, pending);
|
|
235
|
+
emitDebug(
|
|
236
|
+
"evenai",
|
|
237
|
+
"run_wait_started",
|
|
238
|
+
"debug",
|
|
239
|
+
{ sessionKey: sessionKey || undefined, runId },
|
|
240
|
+
() => ({ timeoutMs }),
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
close() {
|
|
246
|
+
if (typeof offMessage === "function") offMessage();
|
|
247
|
+
if (typeof offActivity === "function") offActivity();
|
|
248
|
+
if (typeof offDisconnected === "function") offDisconnected();
|
|
249
|
+
|
|
250
|
+
for (const runId of Array.from(pendingRuns.keys())) {
|
|
251
|
+
rejectRun(
|
|
252
|
+
runId,
|
|
253
|
+
createRunWaiterError(
|
|
254
|
+
"evenai_waiter_closed",
|
|
255
|
+
"Even AI run waiter stopped.",
|
|
256
|
+
),
|
|
257
|
+
"run_wait_closed",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
logger.debug("[evenai] run waiter closed");
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export default createEvenAiRunWaiter;
|