oomi-ai 0.2.49 → 0.3.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 +227 -463
- package/agent_instructions.md +244 -234
- package/bin/oomi-ai.js +4028 -5797
- package/bin/sessionBridgeState.js +78 -78
- package/lib/openclawPaths.js +70 -71
- package/lib/openclawProfile.js +216 -216
- package/lib/personaApiClient.js +133 -303
- package/lib/spokenMetadata.js +137 -137
- package/openclaw.extension.js +341 -341
- package/openclaw.plugin.json +17 -17
- package/package.json +59 -59
- package/persona-app/README.md +27 -0
- package/persona-app/registry/v1.json +63 -0
- package/persona-app/schema/persona-app.v1.schema.json +90 -0
- package/skills/oomi/SKILL.md +165 -182
- package/skills/oomi/agent_instructions.md +99 -80
- package/lib/channelPluginClient.js +0 -119
- package/lib/openclawDevGateway.js +0 -384
- package/lib/personaJobExecutor.js +0 -139
- package/lib/personaJobPoller.js +0 -112
- package/lib/personaPortAllocator.js +0 -36
- package/lib/personaRuntimeManager.js +0 -496
- package/lib/personaRuntimeProcess.js +0 -924
- package/lib/personaRuntimeRegistry.js +0 -67
- package/lib/personaRuntimeSupervisor.js +0 -330
- package/lib/scaffold.js +0 -108
- package/lib/template.js +0 -45
- package/skills/oomi/config.json +0 -3
- package/skills/oomi/scripts/get_avatar_capabilities.py +0 -40
- package/skills/oomi/scripts/get_data.py +0 -49
- package/skills/oomi/scripts/install_agent_instructions.py +0 -78
- package/skills/oomi/scripts/send_goal.py +0 -53
- package/skills/oomi/scripts/sync.py +0 -46
- package/skills/oomi/setup.py +0 -41
- package/templates/persona-app/.env.example +0 -8
- package/templates/persona-app/README.md +0 -47
- package/templates/persona-app/eslint.config.js +0 -28
- package/templates/persona-app/index.html +0 -18
- package/templates/persona-app/oomi.runtime.json +0 -13
- package/templates/persona-app/package.json +0 -44
- package/templates/persona-app/persona/brief.md +0 -14
- package/templates/persona-app/persona.json +0 -14
- package/templates/persona-app/public/manifest.webmanifest +0 -8
- package/templates/persona-app/public/oomi.health.json +0 -6
- package/templates/persona-app/src/App.css +0 -379
- package/templates/persona-app/src/App.tsx +0 -17
- package/templates/persona-app/src/index.css +0 -53
- package/templates/persona-app/src/main.tsx +0 -23
- package/templates/persona-app/src/pages/HomePage.tsx +0 -127
- package/templates/persona-app/src/pages/ScenePage.tsx +0 -158
- package/templates/persona-app/src/persona/config.ts +0 -6
- package/templates/persona-app/src/persona/notes.ts +0 -9
- package/templates/persona-app/src/spatial.ts +0 -82
- package/templates/persona-app/src/vite-env.d.ts +0 -3
- package/templates/persona-app/template.json +0 -13
- package/templates/persona-app/tsconfig.app.json +0 -23
- package/templates/persona-app/tsconfig.json +0 -7
- package/templates/persona-app/tsconfig.node.json +0 -21
- package/templates/persona-app/vendor/webspatial/FORK.md +0 -6
- package/templates/persona-app/vendor/webspatial/core-sdk/LICENSE +0 -21
- package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.d.ts +0 -906
- package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.global.js +0 -75
- package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.global.js.map +0 -1
- package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.d.ts +0 -906
- package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.js +0 -3131
- package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.js.map +0 -1
- package/templates/persona-app/vendor/webspatial/core-sdk/package.json +0 -45
- package/templates/persona-app/vendor/webspatial/react-sdk/LICENSE +0 -21
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.d.ts +0 -365
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.js +0 -4167
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.js.map +0 -1
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.d.ts +0 -82
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.js +0 -66
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.js.map +0 -1
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.d.ts +0 -2
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.js +0 -18
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.js.map +0 -1
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.d.ts +0 -5
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.js +0 -66
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.js.map +0 -1
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.d.ts +0 -1
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.js +0 -18
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.js.map +0 -1
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.d.ts +0 -365
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.js +0 -4207
- package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.js.map +0 -1
- package/templates/persona-app/vendor/webspatial/react-sdk/package.json +0 -94
- package/templates/persona-app/vite.config.ts +0 -31
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
function trimString(value) {
|
|
2
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function stripTrailingSlash(value) {
|
|
6
|
-
return trimString(value).replace(/\/+$/, '');
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
async function readJsonResponse(response) {
|
|
10
|
-
return response.json().catch(() => ({}));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function requestJson({
|
|
14
|
-
fetchImpl,
|
|
15
|
-
backendUrl,
|
|
16
|
-
path,
|
|
17
|
-
deviceToken,
|
|
18
|
-
method = 'POST',
|
|
19
|
-
body,
|
|
20
|
-
}) {
|
|
21
|
-
const baseUrl = stripTrailingSlash(backendUrl);
|
|
22
|
-
if (!baseUrl) {
|
|
23
|
-
throw new Error('Backend URL is required.');
|
|
24
|
-
}
|
|
25
|
-
const token = trimString(deviceToken);
|
|
26
|
-
if (!token) {
|
|
27
|
-
throw new Error('Device token is required.');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
31
|
-
method,
|
|
32
|
-
headers: {
|
|
33
|
-
'Content-Type': 'application/json',
|
|
34
|
-
Authorization: `Bearer ${token}`,
|
|
35
|
-
},
|
|
36
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const payload = await readJsonResponse(response);
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
const errorMessage =
|
|
42
|
-
trimString(payload?.error) ||
|
|
43
|
-
trimString(payload?.message) ||
|
|
44
|
-
`Channel plugin request failed (${response.status})`;
|
|
45
|
-
throw new Error(errorMessage);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return payload;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function createChannelPluginClient({
|
|
52
|
-
backendUrl,
|
|
53
|
-
deviceToken,
|
|
54
|
-
fetchImpl = globalThis.fetch,
|
|
55
|
-
}) {
|
|
56
|
-
if (typeof fetchImpl !== 'function') {
|
|
57
|
-
throw new Error('fetch implementation is required.');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const resolvedBackendUrl = stripTrailingSlash(backendUrl);
|
|
61
|
-
const resolvedDeviceToken = trimString(deviceToken);
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
pollMessages({
|
|
65
|
-
limit = 20,
|
|
66
|
-
metadataType,
|
|
67
|
-
} = {}) {
|
|
68
|
-
const safeLimit = Number.isFinite(Number(limit)) ? Math.max(1, Math.min(100, Math.floor(Number(limit)))) : 20;
|
|
69
|
-
const payload = { limit: safeLimit };
|
|
70
|
-
const safeMetadataType = trimString(metadataType);
|
|
71
|
-
if (safeMetadataType) {
|
|
72
|
-
payload.metadataType = safeMetadataType;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return requestJson({
|
|
76
|
-
fetchImpl,
|
|
77
|
-
backendUrl: resolvedBackendUrl,
|
|
78
|
-
deviceToken: resolvedDeviceToken,
|
|
79
|
-
path: '/v1/channel/plugin/poll',
|
|
80
|
-
method: 'POST',
|
|
81
|
-
body: payload,
|
|
82
|
-
});
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
ackMessage({
|
|
86
|
-
messageId,
|
|
87
|
-
outcome = 'delivered',
|
|
88
|
-
failureCode,
|
|
89
|
-
}) {
|
|
90
|
-
const safeMessageId = trimString(messageId);
|
|
91
|
-
if (!safeMessageId) {
|
|
92
|
-
throw new Error('Channel message id is required.');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const safeOutcome = trimString(outcome) || 'delivered';
|
|
96
|
-
if (safeOutcome !== 'delivered' && safeOutcome !== 'failed') {
|
|
97
|
-
throw new Error('Ack outcome must be delivered or failed.');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const body = {
|
|
101
|
-
messageId: safeMessageId,
|
|
102
|
-
outcome: safeOutcome,
|
|
103
|
-
};
|
|
104
|
-
const safeFailureCode = trimString(failureCode);
|
|
105
|
-
if (safeFailureCode) {
|
|
106
|
-
body.failureCode = safeFailureCode;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return requestJson({
|
|
110
|
-
fetchImpl,
|
|
111
|
-
backendUrl: resolvedBackendUrl,
|
|
112
|
-
deviceToken: resolvedDeviceToken,
|
|
113
|
-
path: '/v1/channel/plugin/acks',
|
|
114
|
-
method: 'POST',
|
|
115
|
-
body,
|
|
116
|
-
});
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
}
|
|
@@ -1,384 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import WebSocket, { WebSocketServer } from 'ws';
|
|
4
|
-
|
|
5
|
-
import { resolveOpenclawConfigCandidates } from './openclawPaths.js';
|
|
6
|
-
import { inferSpokenMetadataFromContent } from './spokenMetadata.js';
|
|
7
|
-
|
|
8
|
-
const DEFAULT_GATEWAY_HOST = '127.0.0.1';
|
|
9
|
-
const DEFAULT_GATEWAY_PORT = 18789;
|
|
10
|
-
const PRIMER_MARKER = '[oomi:primer:v1]';
|
|
11
|
-
|
|
12
|
-
function trimString(value, fallback = '') {
|
|
13
|
-
return typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function readJsonSafe(filePath) {
|
|
17
|
-
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
20
|
-
} catch {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function resolveGatewayConfig() {
|
|
26
|
-
const config = resolveOpenclawConfigCandidates()
|
|
27
|
-
.map((candidate) => readJsonSafe(candidate))
|
|
28
|
-
.find((entry) => entry && typeof entry === 'object');
|
|
29
|
-
|
|
30
|
-
const gateway = config?.gateway && typeof config.gateway === 'object' ? config.gateway : {};
|
|
31
|
-
const auth = gateway.auth && typeof gateway.auth === 'object' ? gateway.auth : {};
|
|
32
|
-
const port = Number(gateway.port);
|
|
33
|
-
return {
|
|
34
|
-
host: trimString(process.env.OPENCLAW_GATEWAY_HOST, DEFAULT_GATEWAY_HOST),
|
|
35
|
-
port: Number.isFinite(port) && port > 0 ? Math.floor(port) : DEFAULT_GATEWAY_PORT,
|
|
36
|
-
token: trimString(process.env.OPENCLAW_GATEWAY_TOKEN, trimString(auth.token)),
|
|
37
|
-
password: trimString(process.env.OPENCLAW_GATEWAY_PASSWORD, trimString(auth.password)),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function chunkText(text, maxChunkLength = 32) {
|
|
42
|
-
const words = trimString(text).split(/\s+/).filter(Boolean);
|
|
43
|
-
if (words.length === 0) return [];
|
|
44
|
-
|
|
45
|
-
const chunks = [];
|
|
46
|
-
let current = '';
|
|
47
|
-
for (const word of words) {
|
|
48
|
-
const next = current ? `${current} ${word}` : word;
|
|
49
|
-
if (current && next.length > maxChunkLength) {
|
|
50
|
-
chunks.push(current);
|
|
51
|
-
current = word;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
current = next;
|
|
55
|
-
}
|
|
56
|
-
if (current) {
|
|
57
|
-
chunks.push(current);
|
|
58
|
-
}
|
|
59
|
-
return chunks;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function buildHistoryMessage({ role, content, metadata, timestamp = Date.now() }) {
|
|
63
|
-
const message = {
|
|
64
|
-
role,
|
|
65
|
-
content: [{ type: 'text', text: content }],
|
|
66
|
-
ts: timestamp,
|
|
67
|
-
};
|
|
68
|
-
if (metadata && typeof metadata === 'object') {
|
|
69
|
-
message.metadata = metadata;
|
|
70
|
-
}
|
|
71
|
-
return message;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function buildLocalGatewayAssistantText(userText) {
|
|
75
|
-
const normalized = trimString(userText);
|
|
76
|
-
if (!normalized) {
|
|
77
|
-
return 'Local OpenClaw dev agent is connected and ready.';
|
|
78
|
-
}
|
|
79
|
-
return `Local OpenClaw dev agent received: ${normalized}`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function createLocalGatewayAssistantFrames({ sessionKey, replyText, runId, seqStart = 1, timestampStart = Date.now() }) {
|
|
83
|
-
const spoken = inferSpokenMetadataFromContent(replyText);
|
|
84
|
-
const chunks = chunkText(replyText);
|
|
85
|
-
const frames = [];
|
|
86
|
-
let seq = seqStart;
|
|
87
|
-
let ts = timestampStart;
|
|
88
|
-
|
|
89
|
-
frames.push({
|
|
90
|
-
type: 'event',
|
|
91
|
-
event: 'agent',
|
|
92
|
-
payload: {
|
|
93
|
-
runId,
|
|
94
|
-
stream: 'lifecycle',
|
|
95
|
-
data: { phase: 'start' },
|
|
96
|
-
sessionKey,
|
|
97
|
-
seq: seq++,
|
|
98
|
-
ts: ts++,
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
for (const chunk of chunks) {
|
|
103
|
-
frames.push({
|
|
104
|
-
type: 'event',
|
|
105
|
-
event: 'agent',
|
|
106
|
-
payload: {
|
|
107
|
-
runId,
|
|
108
|
-
stream: 'assistant',
|
|
109
|
-
delta: chunk,
|
|
110
|
-
sessionKey,
|
|
111
|
-
seq: seq++,
|
|
112
|
-
ts: ts++,
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
frames.push({
|
|
118
|
-
type: 'event',
|
|
119
|
-
event: 'chat',
|
|
120
|
-
payload: {
|
|
121
|
-
runId,
|
|
122
|
-
sessionKey,
|
|
123
|
-
seq: seq++,
|
|
124
|
-
ts: ts++,
|
|
125
|
-
state: 'final',
|
|
126
|
-
message: {
|
|
127
|
-
role: 'assistant',
|
|
128
|
-
content: [{ type: 'text', text: replyText }],
|
|
129
|
-
timestamp: Date.now(),
|
|
130
|
-
metadata: spoken ? { spoken } : {},
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
frames.push({
|
|
136
|
-
type: 'event',
|
|
137
|
-
event: 'agent',
|
|
138
|
-
payload: {
|
|
139
|
-
runId,
|
|
140
|
-
stream: 'lifecycle',
|
|
141
|
-
data: { phase: 'end' },
|
|
142
|
-
sessionKey,
|
|
143
|
-
seq: seq++,
|
|
144
|
-
ts: ts++,
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
return frames;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function createUnauthorizedResponse(id) {
|
|
152
|
-
return {
|
|
153
|
-
type: 'res',
|
|
154
|
-
id,
|
|
155
|
-
ok: false,
|
|
156
|
-
error: {
|
|
157
|
-
code: 'unauthorized',
|
|
158
|
-
message: 'Local gateway auth rejected the connection request.',
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function createOkResponse(id, payload = {}) {
|
|
164
|
-
return {
|
|
165
|
-
type: 'res',
|
|
166
|
-
id,
|
|
167
|
-
ok: true,
|
|
168
|
-
payload,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function createAbortedEvent(sessionKey) {
|
|
173
|
-
return {
|
|
174
|
-
type: 'event',
|
|
175
|
-
event: 'chat',
|
|
176
|
-
payload: {
|
|
177
|
-
sessionKey,
|
|
178
|
-
state: 'aborted',
|
|
179
|
-
timestamp: Date.now(),
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function scheduleAssistantFrames({ socket, sessionKey, frames, pendingReplies, logger }) {
|
|
185
|
-
const existing = pendingReplies.get(sessionKey);
|
|
186
|
-
if (existing?.timers) {
|
|
187
|
-
for (const timer of existing.timers) {
|
|
188
|
-
clearTimeout(timer);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const timers = [];
|
|
193
|
-
frames.forEach((frame, index) => {
|
|
194
|
-
const timer = setTimeout(() => {
|
|
195
|
-
if (socket.readyState !== WebSocket.OPEN) return;
|
|
196
|
-
socket.send(JSON.stringify(frame));
|
|
197
|
-
if (index === frames.length - 1) {
|
|
198
|
-
pendingReplies.delete(sessionKey);
|
|
199
|
-
}
|
|
200
|
-
}, index * 30);
|
|
201
|
-
timers.push(timer);
|
|
202
|
-
});
|
|
203
|
-
pendingReplies.set(sessionKey, { timers });
|
|
204
|
-
logger?.(`[dev-gateway] queued assistant reply for ${sessionKey} (${frames.length} frames)`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function startLocalGatewayAgentServer({ host, port, token, password, logger = () => {} } = {}) {
|
|
208
|
-
const gatewayConfig = resolveGatewayConfig();
|
|
209
|
-
const bindHost = trimString(host, gatewayConfig.host);
|
|
210
|
-
const bindPort = Number.isFinite(Number(port)) && Number(port) > 0 ? Math.floor(Number(port)) : gatewayConfig.port;
|
|
211
|
-
const authToken = trimString(token, gatewayConfig.token);
|
|
212
|
-
const authPassword = trimString(password, gatewayConfig.password);
|
|
213
|
-
const histories = new Map();
|
|
214
|
-
const pendingReplies = new Map();
|
|
215
|
-
|
|
216
|
-
const server = new WebSocketServer({
|
|
217
|
-
host: bindHost,
|
|
218
|
-
port: bindPort,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const closePendingForSession = (sessionKey) => {
|
|
222
|
-
const pending = pendingReplies.get(sessionKey);
|
|
223
|
-
if (!pending?.timers) return false;
|
|
224
|
-
for (const timer of pending.timers) {
|
|
225
|
-
clearTimeout(timer);
|
|
226
|
-
}
|
|
227
|
-
pendingReplies.delete(sessionKey);
|
|
228
|
-
return true;
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const appendHistory = (sessionKey, message) => {
|
|
232
|
-
const history = histories.get(sessionKey) || [];
|
|
233
|
-
history.push(message);
|
|
234
|
-
histories.set(sessionKey, history);
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
server.on('connection', (socket) => {
|
|
238
|
-
const challengeNonce = `dev-nonce-${randomUUID()}`;
|
|
239
|
-
socket.send(JSON.stringify({
|
|
240
|
-
type: 'event',
|
|
241
|
-
event: 'connect.challenge',
|
|
242
|
-
payload: { nonce: challengeNonce },
|
|
243
|
-
}));
|
|
244
|
-
logger(`[dev-gateway] connection opened, challenge sent (${challengeNonce})`);
|
|
245
|
-
|
|
246
|
-
socket.on('message', (rawMessage) => {
|
|
247
|
-
let frame;
|
|
248
|
-
try {
|
|
249
|
-
frame = JSON.parse(String(rawMessage));
|
|
250
|
-
} catch {
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (!frame || frame.type !== 'req') {
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const requestId = trimString(frame.id);
|
|
259
|
-
const method = trimString(frame.method);
|
|
260
|
-
const params = frame.params && typeof frame.params === 'object' ? frame.params : {};
|
|
261
|
-
const sessionKey = trimString(params.sessionKey, 'agent:main:webchat:channel:oomi');
|
|
262
|
-
|
|
263
|
-
if (method === 'connect') {
|
|
264
|
-
const requestAuth = params.auth && typeof params.auth === 'object' ? params.auth : {};
|
|
265
|
-
const providedToken = trimString(requestAuth.token);
|
|
266
|
-
const providedPassword = trimString(requestAuth.password);
|
|
267
|
-
const authorized =
|
|
268
|
-
(!authToken || providedToken === authToken) &&
|
|
269
|
-
(!authPassword || providedPassword === authPassword);
|
|
270
|
-
socket.send(JSON.stringify(authorized ? createOkResponse(requestId, { sessionKey }) : createUnauthorizedResponse(requestId)));
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (method === 'chat.history') {
|
|
275
|
-
const messages = histories.get(sessionKey) || [];
|
|
276
|
-
socket.send(JSON.stringify(createOkResponse(requestId, { messages })));
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (method === 'chat.abort') {
|
|
281
|
-
closePendingForSession(sessionKey);
|
|
282
|
-
socket.send(JSON.stringify(createOkResponse(requestId)));
|
|
283
|
-
socket.send(JSON.stringify(createAbortedEvent(sessionKey)));
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (method !== 'chat.send') {
|
|
288
|
-
socket.send(JSON.stringify({
|
|
289
|
-
type: 'res',
|
|
290
|
-
id: requestId,
|
|
291
|
-
ok: false,
|
|
292
|
-
error: {
|
|
293
|
-
code: 'unsupported_method',
|
|
294
|
-
message: `Local dev gateway does not handle ${method || 'unknown'}.`,
|
|
295
|
-
},
|
|
296
|
-
}));
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const messageText = trimString(params.message);
|
|
301
|
-
appendHistory(sessionKey, buildHistoryMessage({
|
|
302
|
-
role: 'user',
|
|
303
|
-
content: messageText,
|
|
304
|
-
}));
|
|
305
|
-
socket.send(JSON.stringify(createOkResponse(requestId)));
|
|
306
|
-
|
|
307
|
-
if (!messageText || messageText.includes(PRIMER_MARKER)) {
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const replyText = buildLocalGatewayAssistantText(messageText);
|
|
312
|
-
const runId = `dev-run-${randomUUID()}`;
|
|
313
|
-
const assistantMessage = buildHistoryMessage({
|
|
314
|
-
role: 'assistant',
|
|
315
|
-
content: replyText,
|
|
316
|
-
metadata: {
|
|
317
|
-
spoken: inferSpokenMetadataFromContent(replyText),
|
|
318
|
-
},
|
|
319
|
-
});
|
|
320
|
-
appendHistory(sessionKey, assistantMessage);
|
|
321
|
-
const frames = createLocalGatewayAssistantFrames({
|
|
322
|
-
sessionKey,
|
|
323
|
-
replyText,
|
|
324
|
-
runId,
|
|
325
|
-
});
|
|
326
|
-
scheduleAssistantFrames({
|
|
327
|
-
socket,
|
|
328
|
-
sessionKey,
|
|
329
|
-
frames,
|
|
330
|
-
pendingReplies,
|
|
331
|
-
logger,
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
socket.on('close', () => {
|
|
336
|
-
for (const [sessionKey, pending] of pendingReplies.entries()) {
|
|
337
|
-
if (!pending?.timers) continue;
|
|
338
|
-
for (const timer of pending.timers) {
|
|
339
|
-
clearTimeout(timer);
|
|
340
|
-
}
|
|
341
|
-
pendingReplies.delete(sessionKey);
|
|
342
|
-
}
|
|
343
|
-
logger('[dev-gateway] connection closed');
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
await new Promise((resolve, reject) => {
|
|
348
|
-
server.once('listening', resolve);
|
|
349
|
-
server.once('error', reject);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
logger(`[dev-gateway] listening on ws://${bindHost}:${bindPort}`);
|
|
353
|
-
|
|
354
|
-
return {
|
|
355
|
-
host: bindHost,
|
|
356
|
-
port: bindPort,
|
|
357
|
-
token: authToken,
|
|
358
|
-
password: authPassword,
|
|
359
|
-
close: async () => {
|
|
360
|
-
for (const pending of pendingReplies.values()) {
|
|
361
|
-
if (!pending?.timers) continue;
|
|
362
|
-
for (const timer of pending.timers) {
|
|
363
|
-
clearTimeout(timer);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
pendingReplies.clear();
|
|
367
|
-
await new Promise((resolve, reject) => {
|
|
368
|
-
server.close((error) => {
|
|
369
|
-
if (error) {
|
|
370
|
-
reject(error);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
resolve();
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
},
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
export {
|
|
381
|
-
buildLocalGatewayAssistantText,
|
|
382
|
-
createLocalGatewayAssistantFrames,
|
|
383
|
-
startLocalGatewayAgentServer,
|
|
384
|
-
};
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { scaffoldPersonaApp } from './scaffold.js';
|
|
2
|
-
|
|
3
|
-
export function extractPersonaJobPayload(message = {}) {
|
|
4
|
-
const metadata = message.metadata && typeof message.metadata === 'object' ? message.metadata : {};
|
|
5
|
-
const payload = metadata.payload && typeof metadata.payload === 'object' ? metadata.payload : null;
|
|
6
|
-
|
|
7
|
-
if (metadata.type !== 'persona_job' || !payload) {
|
|
8
|
-
throw new Error('Message is not a persona job payload.');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
return payload;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function executePersonaJob({
|
|
15
|
-
message,
|
|
16
|
-
installWorkspace = async () => {},
|
|
17
|
-
startWorkspace = async () => ({ pid: null, logFilePath: '' }),
|
|
18
|
-
waitForRuntime = async () => {},
|
|
19
|
-
registerRuntime = async () => {},
|
|
20
|
-
destroyWorkspace = async () => ({ deleted: false }),
|
|
21
|
-
onJobStart = async () => {},
|
|
22
|
-
onJobSuccess = async () => {},
|
|
23
|
-
onJobFailure = async () => {},
|
|
24
|
-
}) {
|
|
25
|
-
const payload = extractPersonaJobPayload(message);
|
|
26
|
-
const jobId = String(payload.jobId || message?.metadata?.jobId || '').trim();
|
|
27
|
-
if (!jobId) {
|
|
28
|
-
throw new Error('Persona job payload is missing jobId.');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
if (!['create_persona_runtime', 'destroy_persona_runtime'].includes(payload.jobType)) {
|
|
33
|
-
throw new Error(`Unsupported persona job type: ${payload.jobType || 'unknown'}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
await onJobStart({ jobId, payload });
|
|
37
|
-
|
|
38
|
-
const persona = payload.persona && typeof payload.persona === 'object' ? payload.persona : {};
|
|
39
|
-
const scaffold = payload.scaffold && typeof payload.scaffold === 'object' ? payload.scaffold : {};
|
|
40
|
-
const templateVersion = String(persona.templateVersion || 'v1').trim() || 'v1';
|
|
41
|
-
|
|
42
|
-
if (payload.jobType === 'destroy_persona_runtime') {
|
|
43
|
-
const workspacePath = String(scaffold.outDir || '').trim();
|
|
44
|
-
if (!workspacePath) {
|
|
45
|
-
throw new Error('Destroy persona job payload is missing scaffold.outDir.');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const result = {
|
|
49
|
-
workspacePath,
|
|
50
|
-
...(await destroyWorkspace({
|
|
51
|
-
payload,
|
|
52
|
-
workspacePath,
|
|
53
|
-
})),
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
await onJobSuccess({ jobId, payload, result });
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
ok: true,
|
|
60
|
-
jobId,
|
|
61
|
-
result,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const scaffoldResult = scaffoldPersonaApp({
|
|
66
|
-
slug: String(persona.slug || '').trim(),
|
|
67
|
-
name: String(persona.name || '').trim(),
|
|
68
|
-
description: String(persona.description || '').trim(),
|
|
69
|
-
outDir: String(scaffold.outDir || '').trim(),
|
|
70
|
-
templateVersion,
|
|
71
|
-
force: true,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
await installWorkspace({
|
|
75
|
-
payload,
|
|
76
|
-
workspacePath: scaffoldResult.outDir,
|
|
77
|
-
scaffoldResult,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const runtime = {
|
|
81
|
-
transport: 'local',
|
|
82
|
-
endpoint: `http://127.0.0.1:${scaffoldResult.defaultPort}`,
|
|
83
|
-
localPort: scaffoldResult.defaultPort,
|
|
84
|
-
healthcheckUrl: `http://127.0.0.1:${scaffoldResult.defaultPort}${scaffoldResult.healthPath}`,
|
|
85
|
-
};
|
|
86
|
-
const processInfo = await startWorkspace({
|
|
87
|
-
payload,
|
|
88
|
-
workspacePath: scaffoldResult.outDir,
|
|
89
|
-
scaffoldResult,
|
|
90
|
-
runtime,
|
|
91
|
-
});
|
|
92
|
-
await waitForRuntime({
|
|
93
|
-
payload,
|
|
94
|
-
workspacePath: scaffoldResult.outDir,
|
|
95
|
-
scaffoldResult,
|
|
96
|
-
runtime,
|
|
97
|
-
processInfo,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const result = {
|
|
101
|
-
workspacePath: scaffoldResult.outDir,
|
|
102
|
-
localPort: runtime.localPort,
|
|
103
|
-
transport: runtime.transport,
|
|
104
|
-
endpoint: runtime.endpoint,
|
|
105
|
-
healthcheckUrl: runtime.healthcheckUrl,
|
|
106
|
-
pid: processInfo?.pid || null,
|
|
107
|
-
logFilePath: processInfo?.logFilePath || '',
|
|
108
|
-
templateVersion,
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
await registerRuntime({ jobId, payload, result });
|
|
112
|
-
await onJobSuccess({ jobId, payload, result });
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
ok: true,
|
|
116
|
-
jobId,
|
|
117
|
-
result,
|
|
118
|
-
};
|
|
119
|
-
} catch (error) {
|
|
120
|
-
const messageText = error instanceof Error ? error.message : 'Persona job execution failed.';
|
|
121
|
-
await onJobFailure({
|
|
122
|
-
jobId,
|
|
123
|
-
payload,
|
|
124
|
-
error: {
|
|
125
|
-
code: 'PERSONA_JOB_EXECUTION_FAILED',
|
|
126
|
-
message: messageText,
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
ok: false,
|
|
132
|
-
jobId,
|
|
133
|
-
error: {
|
|
134
|
-
code: 'PERSONA_JOB_EXECUTION_FAILED',
|
|
135
|
-
message: messageText,
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
}
|