oomi-ai 0.2.50 → 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.
Files changed (88) hide show
  1. package/README.md +203 -507
  2. package/agent_instructions.md +244 -253
  3. package/bin/oomi-ai.js +4026 -5795
  4. package/bin/sessionBridgeState.js +78 -78
  5. package/lib/openclawPaths.js +70 -71
  6. package/lib/openclawProfile.js +216 -216
  7. package/lib/personaApiClient.js +133 -303
  8. package/lib/spokenMetadata.js +137 -137
  9. package/openclaw.extension.js +341 -341
  10. package/openclaw.plugin.json +17 -17
  11. package/package.json +59 -59
  12. package/persona-app/README.md +27 -0
  13. package/persona-app/registry/v1.json +63 -0
  14. package/persona-app/schema/persona-app.v1.schema.json +90 -0
  15. package/skills/oomi/SKILL.md +165 -182
  16. package/skills/oomi/agent_instructions.md +99 -80
  17. package/lib/channelPluginClient.js +0 -119
  18. package/lib/openclawDevGateway.js +0 -384
  19. package/lib/personaJobExecutor.js +0 -139
  20. package/lib/personaJobPoller.js +0 -112
  21. package/lib/personaPortAllocator.js +0 -36
  22. package/lib/personaRuntimeManager.js +0 -496
  23. package/lib/personaRuntimeProcess.js +0 -924
  24. package/lib/personaRuntimeRegistry.js +0 -67
  25. package/lib/personaRuntimeSupervisor.js +0 -330
  26. package/lib/scaffold.js +0 -108
  27. package/lib/template.js +0 -45
  28. package/skills/oomi/config.json +0 -3
  29. package/skills/oomi/scripts/get_avatar_capabilities.py +0 -40
  30. package/skills/oomi/scripts/get_data.py +0 -49
  31. package/skills/oomi/scripts/install_agent_instructions.py +0 -78
  32. package/skills/oomi/scripts/send_goal.py +0 -53
  33. package/skills/oomi/scripts/sync.py +0 -46
  34. package/skills/oomi/setup.py +0 -41
  35. package/templates/persona-app/.env.example +0 -8
  36. package/templates/persona-app/README.md +0 -58
  37. package/templates/persona-app/eslint.config.js +0 -28
  38. package/templates/persona-app/index.html +0 -18
  39. package/templates/persona-app/oomi.runtime.json +0 -13
  40. package/templates/persona-app/package.json +0 -44
  41. package/templates/persona-app/persona/brief.md +0 -14
  42. package/templates/persona-app/persona.json +0 -14
  43. package/templates/persona-app/public/manifest.webmanifest +0 -8
  44. package/templates/persona-app/public/oomi.health.json +0 -6
  45. package/templates/persona-app/src/App.css +0 -379
  46. package/templates/persona-app/src/App.tsx +0 -17
  47. package/templates/persona-app/src/index.css +0 -53
  48. package/templates/persona-app/src/main.tsx +0 -23
  49. package/templates/persona-app/src/pages/HomePage.tsx +0 -127
  50. package/templates/persona-app/src/pages/ScenePage.tsx +0 -158
  51. package/templates/persona-app/src/persona/config.ts +0 -6
  52. package/templates/persona-app/src/persona/notes.ts +0 -10
  53. package/templates/persona-app/src/spatial.ts +0 -82
  54. package/templates/persona-app/src/vite-env.d.ts +0 -3
  55. package/templates/persona-app/template.json +0 -13
  56. package/templates/persona-app/tsconfig.app.json +0 -23
  57. package/templates/persona-app/tsconfig.json +0 -7
  58. package/templates/persona-app/tsconfig.node.json +0 -21
  59. package/templates/persona-app/vendor/webspatial/FORK.md +0 -6
  60. package/templates/persona-app/vendor/webspatial/core-sdk/LICENSE +0 -21
  61. package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.d.ts +0 -906
  62. package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.global.js +0 -75
  63. package/templates/persona-app/vendor/webspatial/core-sdk/dist/iife/index.global.js.map +0 -1
  64. package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.d.ts +0 -906
  65. package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.js +0 -3131
  66. package/templates/persona-app/vendor/webspatial/core-sdk/dist/index.js.map +0 -1
  67. package/templates/persona-app/vendor/webspatial/core-sdk/package.json +0 -45
  68. package/templates/persona-app/vendor/webspatial/react-sdk/LICENSE +0 -21
  69. package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.d.ts +0 -365
  70. package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.js +0 -4167
  71. package/templates/persona-app/vendor/webspatial/react-sdk/dist/default/index.js.map +0 -1
  72. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.d.ts +0 -82
  73. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.js +0 -66
  74. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.js.map +0 -1
  75. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.d.ts +0 -2
  76. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.js +0 -18
  77. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-dev-runtime.web.js.map +0 -1
  78. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.d.ts +0 -5
  79. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.js +0 -66
  80. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.js.map +0 -1
  81. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.d.ts +0 -1
  82. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.js +0 -18
  83. package/templates/persona-app/vendor/webspatial/react-sdk/dist/jsx/jsx-runtime.web.js.map +0 -1
  84. package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.d.ts +0 -365
  85. package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.js +0 -4207
  86. package/templates/persona-app/vendor/webspatial/react-sdk/dist/web/index.js.map +0 -1
  87. package/templates/persona-app/vendor/webspatial/react-sdk/package.json +0 -94
  88. package/templates/persona-app/vite.config.ts +0 -31
@@ -1,347 +1,347 @@
1
1
  import { inferSpokenMetadataFromContent, normalizeSpokenMetadata } from './lib/spokenMetadata.js';
2
2
 
3
3
  const CHANNEL_ID = 'oomi';
4
- const DEFAULT_SESSION_KEY = 'agent:main:webchat:channel:oomi';
5
- const DEFAULT_TIMEOUT_MS = 15000;
6
-
7
- function toString(value, fallback = '') {
8
- return typeof value === 'string' && value.trim() ? value.trim() : fallback;
9
- }
10
-
11
- function toNumber(value, fallback, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
12
- if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
13
- const normalized = Math.floor(value);
14
- if (normalized < min) return fallback;
15
- if (normalized > max) return max;
16
- return normalized;
17
- }
18
-
19
- function parseAccounts(rawAccounts) {
20
- if (!rawAccounts || typeof rawAccounts !== 'object') return {};
21
- const accounts = {};
22
-
23
- for (const [accountId, raw] of Object.entries(rawAccounts)) {
24
- if (!raw || typeof raw !== 'object') continue;
25
- accounts[accountId] = {
26
- enabled: raw.enabled !== false,
27
- backendUrl: toString(raw.backendUrl),
28
- deviceToken: toString(raw.deviceToken),
29
- defaultSessionKey: toString(raw.defaultSessionKey, DEFAULT_SESSION_KEY),
30
- requestTimeoutMs: toNumber(raw.requestTimeoutMs, DEFAULT_TIMEOUT_MS, { min: 2000, max: 120000 }),
31
- };
32
- }
33
-
34
- return accounts;
35
- }
36
-
37
- function extractChannelConfig(cfg = {}) {
38
- if (!cfg || typeof cfg !== 'object') return {};
39
- if (cfg.channels && typeof cfg.channels === 'object' && cfg.channels[CHANNEL_ID] && typeof cfg.channels[CHANNEL_ID] === 'object') {
40
- return cfg.channels[CHANNEL_ID];
41
- }
42
- if (cfg[CHANNEL_ID] && typeof cfg[CHANNEL_ID] === 'object') {
43
- return cfg[CHANNEL_ID];
44
- }
45
- if (cfg.accounts && typeof cfg.accounts === 'object') {
46
- return cfg;
47
- }
48
- return {};
49
- }
50
-
51
- function normalizeConfig(cfg = {}) {
52
- const channelConfig = extractChannelConfig(cfg);
53
- const configuredAccounts = parseAccounts(channelConfig.accounts);
54
- const accountIds = Object.keys(configuredAccounts);
55
- const defaultAccountId = toString(channelConfig.defaultAccountId, accountIds[0] || 'default');
56
-
57
- if (!configuredAccounts[defaultAccountId]) {
58
- configuredAccounts[defaultAccountId] = {
59
- enabled: true,
60
- backendUrl: '',
61
- deviceToken: '',
62
- defaultSessionKey: DEFAULT_SESSION_KEY,
63
- requestTimeoutMs: DEFAULT_TIMEOUT_MS,
64
- };
65
- }
66
-
67
- return {
68
- defaultAccountId,
69
- accounts: configuredAccounts,
70
- };
71
- }
72
-
73
- function resolveAccount(cfg, accountId) {
74
- const normalized = normalizeConfig(cfg);
75
- const resolvedId = toString(accountId, normalized.defaultAccountId);
76
- const account = normalized.accounts[resolvedId];
77
- if (!account) {
78
- return {
79
- accountId: resolvedId,
80
- account: null,
81
- };
82
- }
83
-
84
- return {
85
- accountId: resolvedId,
86
- account,
87
- };
88
- }
89
-
90
- function extractText(payload) {
91
- if (!payload) return '';
92
- if (typeof payload === 'string') return payload.trim();
93
-
94
- const direct = [payload.text, payload.message, payload.content, payload.body];
95
- for (const value of direct) {
96
- if (typeof value === 'string' && value.trim()) return value.trim();
97
- }
98
-
99
- if (Array.isArray(payload.content)) {
100
- return payload.content
101
- .filter((part) => part && typeof part === 'object' && part.type === 'text' && typeof part.text === 'string')
102
- .map((part) => part.text.trim())
103
- .filter(Boolean)
104
- .join('\n');
105
- }
106
-
107
- return '';
108
- }
109
-
110
- function extractConversationKey(payload) {
111
- const candidates = [
112
- payload?.conversationKey,
113
- payload?.threadId,
114
- payload?.target?.conversationKey,
115
- payload?.target?.threadId,
116
- payload?.target?.id,
117
- payload?.metadata?.conversationKey,
118
- payload?.metadata?.threadId,
119
- ];
120
-
121
- for (const candidate of candidates) {
122
- const value = toString(candidate);
123
- if (value) return value;
124
- }
125
-
126
- return '';
127
- }
128
-
129
- function extractUserId(payload) {
130
- const candidates = [
131
- payload?.userId,
132
- payload?.target?.userId,
133
- payload?.metadata?.userId,
134
- ];
135
-
136
- for (const candidate of candidates) {
137
- const value = toString(candidate);
138
- if (value) return value;
139
- }
140
-
141
- return '';
142
- }
143
-
144
- function nextMessageId() {
145
- return `oomi_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
146
- }
147
-
148
- function extractMessageId(payload) {
149
- const candidates = [
150
- payload?.messageId,
151
- payload?.id,
152
- payload?.requestId,
153
- payload?.idempotencyKey,
154
- payload?.metadata?.messageId,
155
- payload?.metadata?.idempotencyKey,
156
- ];
157
-
158
- for (const candidate of candidates) {
159
- const value = toString(candidate);
160
- if (value) return value;
161
- }
162
-
163
- return nextMessageId();
164
- }
165
-
166
- function extractCorrelationId(payload) {
167
- const candidates = [
168
- payload?.correlationId,
169
- payload?.metadata?.correlationId,
170
- payload?.requestId,
171
- payload?.messageId,
172
- payload?.id,
173
- ];
174
-
175
- for (const candidate of candidates) {
176
- const value = toString(candidate);
177
- if (value) return value;
178
- }
179
-
180
- return '';
181
- }
182
-
183
- function normalizeOutgoingMetadata(payloadMetadata, { accountId, correlationId, content }) {
184
- const metadata =
185
- payloadMetadata && typeof payloadMetadata === 'object' && !Array.isArray(payloadMetadata)
186
- ? { ...payloadMetadata }
187
- : {};
188
-
4
+ const DEFAULT_SESSION_KEY = 'agent:main:webchat:channel:oomi';
5
+ const DEFAULT_TIMEOUT_MS = 15000;
6
+
7
+ function toString(value, fallback = '') {
8
+ return typeof value === 'string' && value.trim() ? value.trim() : fallback;
9
+ }
10
+
11
+ function toNumber(value, fallback, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
12
+ if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
13
+ const normalized = Math.floor(value);
14
+ if (normalized < min) return fallback;
15
+ if (normalized > max) return max;
16
+ return normalized;
17
+ }
18
+
19
+ function parseAccounts(rawAccounts) {
20
+ if (!rawAccounts || typeof rawAccounts !== 'object') return {};
21
+ const accounts = {};
22
+
23
+ for (const [accountId, raw] of Object.entries(rawAccounts)) {
24
+ if (!raw || typeof raw !== 'object') continue;
25
+ accounts[accountId] = {
26
+ enabled: raw.enabled !== false,
27
+ backendUrl: toString(raw.backendUrl),
28
+ deviceToken: toString(raw.deviceToken),
29
+ defaultSessionKey: toString(raw.defaultSessionKey, DEFAULT_SESSION_KEY),
30
+ requestTimeoutMs: toNumber(raw.requestTimeoutMs, DEFAULT_TIMEOUT_MS, { min: 2000, max: 120000 }),
31
+ };
32
+ }
33
+
34
+ return accounts;
35
+ }
36
+
37
+ function extractChannelConfig(cfg = {}) {
38
+ if (!cfg || typeof cfg !== 'object') return {};
39
+ if (cfg.channels && typeof cfg.channels === 'object' && cfg.channels[CHANNEL_ID] && typeof cfg.channels[CHANNEL_ID] === 'object') {
40
+ return cfg.channels[CHANNEL_ID];
41
+ }
42
+ if (cfg[CHANNEL_ID] && typeof cfg[CHANNEL_ID] === 'object') {
43
+ return cfg[CHANNEL_ID];
44
+ }
45
+ if (cfg.accounts && typeof cfg.accounts === 'object') {
46
+ return cfg;
47
+ }
48
+ return {};
49
+ }
50
+
51
+ function normalizeConfig(cfg = {}) {
52
+ const channelConfig = extractChannelConfig(cfg);
53
+ const configuredAccounts = parseAccounts(channelConfig.accounts);
54
+ const accountIds = Object.keys(configuredAccounts);
55
+ const defaultAccountId = toString(channelConfig.defaultAccountId, accountIds[0] || 'default');
56
+
57
+ if (!configuredAccounts[defaultAccountId]) {
58
+ configuredAccounts[defaultAccountId] = {
59
+ enabled: true,
60
+ backendUrl: '',
61
+ deviceToken: '',
62
+ defaultSessionKey: DEFAULT_SESSION_KEY,
63
+ requestTimeoutMs: DEFAULT_TIMEOUT_MS,
64
+ };
65
+ }
66
+
67
+ return {
68
+ defaultAccountId,
69
+ accounts: configuredAccounts,
70
+ };
71
+ }
72
+
73
+ function resolveAccount(cfg, accountId) {
74
+ const normalized = normalizeConfig(cfg);
75
+ const resolvedId = toString(accountId, normalized.defaultAccountId);
76
+ const account = normalized.accounts[resolvedId];
77
+ if (!account) {
78
+ return {
79
+ accountId: resolvedId,
80
+ account: null,
81
+ };
82
+ }
83
+
84
+ return {
85
+ accountId: resolvedId,
86
+ account,
87
+ };
88
+ }
89
+
90
+ function extractText(payload) {
91
+ if (!payload) return '';
92
+ if (typeof payload === 'string') return payload.trim();
93
+
94
+ const direct = [payload.text, payload.message, payload.content, payload.body];
95
+ for (const value of direct) {
96
+ if (typeof value === 'string' && value.trim()) return value.trim();
97
+ }
98
+
99
+ if (Array.isArray(payload.content)) {
100
+ return payload.content
101
+ .filter((part) => part && typeof part === 'object' && part.type === 'text' && typeof part.text === 'string')
102
+ .map((part) => part.text.trim())
103
+ .filter(Boolean)
104
+ .join('\n');
105
+ }
106
+
107
+ return '';
108
+ }
109
+
110
+ function extractConversationKey(payload) {
111
+ const candidates = [
112
+ payload?.conversationKey,
113
+ payload?.threadId,
114
+ payload?.target?.conversationKey,
115
+ payload?.target?.threadId,
116
+ payload?.target?.id,
117
+ payload?.metadata?.conversationKey,
118
+ payload?.metadata?.threadId,
119
+ ];
120
+
121
+ for (const candidate of candidates) {
122
+ const value = toString(candidate);
123
+ if (value) return value;
124
+ }
125
+
126
+ return '';
127
+ }
128
+
129
+ function extractUserId(payload) {
130
+ const candidates = [
131
+ payload?.userId,
132
+ payload?.target?.userId,
133
+ payload?.metadata?.userId,
134
+ ];
135
+
136
+ for (const candidate of candidates) {
137
+ const value = toString(candidate);
138
+ if (value) return value;
139
+ }
140
+
141
+ return '';
142
+ }
143
+
144
+ function nextMessageId() {
145
+ return `oomi_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
146
+ }
147
+
148
+ function extractMessageId(payload) {
149
+ const candidates = [
150
+ payload?.messageId,
151
+ payload?.id,
152
+ payload?.requestId,
153
+ payload?.idempotencyKey,
154
+ payload?.metadata?.messageId,
155
+ payload?.metadata?.idempotencyKey,
156
+ ];
157
+
158
+ for (const candidate of candidates) {
159
+ const value = toString(candidate);
160
+ if (value) return value;
161
+ }
162
+
163
+ return nextMessageId();
164
+ }
165
+
166
+ function extractCorrelationId(payload) {
167
+ const candidates = [
168
+ payload?.correlationId,
169
+ payload?.metadata?.correlationId,
170
+ payload?.requestId,
171
+ payload?.messageId,
172
+ payload?.id,
173
+ ];
174
+
175
+ for (const candidate of candidates) {
176
+ const value = toString(candidate);
177
+ if (value) return value;
178
+ }
179
+
180
+ return '';
181
+ }
182
+
183
+ function normalizeOutgoingMetadata(payloadMetadata, { accountId, correlationId, content }) {
184
+ const metadata =
185
+ payloadMetadata && typeof payloadMetadata === 'object' && !Array.isArray(payloadMetadata)
186
+ ? { ...payloadMetadata }
187
+ : {};
188
+
189
189
  const spoken =
190
190
  normalizeSpokenMetadata(metadata.spoken) ||
191
191
  inferSpokenMetadataFromContent(content);
192
- if (spoken) {
193
- metadata.spoken = spoken;
194
- } else {
195
- delete metadata.spoken;
196
- }
197
-
198
- metadata.accountId = accountId;
199
- if (correlationId) {
200
- metadata.correlationId = correlationId;
201
- } else {
202
- delete metadata.correlationId;
203
- }
204
-
205
- return metadata;
206
- }
207
-
208
- async function postJson({ url, token, body, timeoutMs }) {
209
- const controller = new AbortController();
210
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
211
-
212
- try {
213
- const response = await fetch(url, {
214
- method: 'POST',
215
- headers: {
216
- 'Content-Type': 'application/json',
217
- Authorization: `Bearer ${token}`,
218
- },
219
- body: JSON.stringify(body),
220
- signal: controller.signal,
221
- });
222
-
223
- const payload = await response.json().catch(() => ({}));
224
- return {
225
- ok: response.ok,
226
- status: response.status,
227
- payload,
228
- };
229
- } finally {
230
- clearTimeout(timeout);
231
- }
232
- }
233
-
234
- const oomiChannelPlugin = {
235
- id: CHANNEL_ID,
236
- meta: {
237
- label: 'Oomi',
238
- selectionLabel: 'Oomi (Managed)',
239
- docsPath: '/channels/oomi',
240
- docsLabel: 'oomi',
241
- blurb: 'Managed channel transport for Oomi chat.',
242
- aliases: ['oomi-ai'],
243
- description: 'Managed Oomi channel plugin.',
244
- },
245
- capabilities: {
246
- chatTypes: ['direct'],
247
- media: {
248
- images: false,
249
- audio: false,
250
- files: false,
251
- },
252
- threads: true,
253
- },
254
-
255
- config: {
256
- listAccountIds(cfg) {
257
- const normalized = normalizeConfig(cfg);
258
- return Object.entries(normalized.accounts)
259
- .filter(([, account]) => account.enabled !== false)
260
- .map(([accountId]) => accountId);
261
- },
262
-
263
- resolveAccount(cfg, accountId) {
264
- const { accountId: resolvedAccountId, account } = resolveAccount(cfg, accountId);
265
- if (!account) return null;
266
- return {
267
- id: resolvedAccountId,
268
- ...account,
269
- };
270
- },
271
- },
272
-
273
- outbound: {
274
- deliveryMode: 'direct',
275
-
276
- async sendText(payload = {}) {
277
- const { cfg, accountId } = payload;
278
- const { accountId: resolvedAccountId, account } = resolveAccount(cfg, accountId);
279
-
280
- if (!account || account.enabled === false) {
281
- return {
282
- ok: false,
283
- error: `oomi account is disabled or missing (${resolvedAccountId})`,
284
- };
285
- }
286
- if (!account.backendUrl || !account.deviceToken) {
287
- return {
288
- ok: false,
289
- error: `oomi account is missing backendUrl/deviceToken (${resolvedAccountId})`,
290
- };
291
- }
292
-
293
- const content = extractText(payload);
294
- if (!content) {
295
- return {
296
- ok: false,
297
- error: 'oomi outbound message content is empty',
298
- };
299
- }
300
-
301
- const conversationKey = extractConversationKey(payload);
302
- const userId = extractUserId(payload);
303
- const sessionKey = toString(payload?.sessionKey || payload?.metadata?.sessionKey, account.defaultSessionKey);
304
- const messageId = extractMessageId(payload);
305
- const correlationId = extractCorrelationId(payload);
306
-
307
- const response = await postJson({
308
- url: `${account.backendUrl}/v1/channel/plugin/messages`,
309
- token: account.deviceToken,
310
- timeoutMs: account.requestTimeoutMs,
311
- body: {
312
- messageId,
313
- correlationId,
314
- conversationKey,
315
- userId,
316
- sessionKey,
317
- content,
318
- source: 'openclaw.channel',
319
- metadata: normalizeOutgoingMetadata(payload?.metadata, {
320
- accountId: resolvedAccountId,
321
- correlationId,
322
- content,
323
- }),
324
- },
325
- });
326
-
327
- if (!response.ok) {
328
- const reason = toString(response.payload?.error, `status ${response.status}`);
329
- const code = toString(response.payload?.errorCode);
330
- return {
331
- ok: false,
332
- error: `oomi plugin message publish failed: ${reason}${code ? ` (code=${code})` : ''}`,
333
- code,
334
- };
335
- }
336
-
337
- return {
338
- ok: true,
339
- providerMessageId: toString(response.payload?.message?.messageId),
340
- };
341
- },
342
- },
343
- };
344
-
345
- export default function register(api) {
346
- api.registerChannel({ plugin: oomiChannelPlugin });
347
- }
192
+ if (spoken) {
193
+ metadata.spoken = spoken;
194
+ } else {
195
+ delete metadata.spoken;
196
+ }
197
+
198
+ metadata.accountId = accountId;
199
+ if (correlationId) {
200
+ metadata.correlationId = correlationId;
201
+ } else {
202
+ delete metadata.correlationId;
203
+ }
204
+
205
+ return metadata;
206
+ }
207
+
208
+ async function postJson({ url, token, body, timeoutMs }) {
209
+ const controller = new AbortController();
210
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
211
+
212
+ try {
213
+ const response = await fetch(url, {
214
+ method: 'POST',
215
+ headers: {
216
+ 'Content-Type': 'application/json',
217
+ Authorization: `Bearer ${token}`,
218
+ },
219
+ body: JSON.stringify(body),
220
+ signal: controller.signal,
221
+ });
222
+
223
+ const payload = await response.json().catch(() => ({}));
224
+ return {
225
+ ok: response.ok,
226
+ status: response.status,
227
+ payload,
228
+ };
229
+ } finally {
230
+ clearTimeout(timeout);
231
+ }
232
+ }
233
+
234
+ const oomiChannelPlugin = {
235
+ id: CHANNEL_ID,
236
+ meta: {
237
+ label: 'Oomi',
238
+ selectionLabel: 'Oomi (Managed)',
239
+ docsPath: '/channels/oomi',
240
+ docsLabel: 'oomi',
241
+ blurb: 'Managed channel transport for Oomi chat.',
242
+ aliases: ['oomi-ai'],
243
+ description: 'Managed Oomi channel plugin.',
244
+ },
245
+ capabilities: {
246
+ chatTypes: ['direct'],
247
+ media: {
248
+ images: false,
249
+ audio: false,
250
+ files: false,
251
+ },
252
+ threads: true,
253
+ },
254
+
255
+ config: {
256
+ listAccountIds(cfg) {
257
+ const normalized = normalizeConfig(cfg);
258
+ return Object.entries(normalized.accounts)
259
+ .filter(([, account]) => account.enabled !== false)
260
+ .map(([accountId]) => accountId);
261
+ },
262
+
263
+ resolveAccount(cfg, accountId) {
264
+ const { accountId: resolvedAccountId, account } = resolveAccount(cfg, accountId);
265
+ if (!account) return null;
266
+ return {
267
+ id: resolvedAccountId,
268
+ ...account,
269
+ };
270
+ },
271
+ },
272
+
273
+ outbound: {
274
+ deliveryMode: 'direct',
275
+
276
+ async sendText(payload = {}) {
277
+ const { cfg, accountId } = payload;
278
+ const { accountId: resolvedAccountId, account } = resolveAccount(cfg, accountId);
279
+
280
+ if (!account || account.enabled === false) {
281
+ return {
282
+ ok: false,
283
+ error: `oomi account is disabled or missing (${resolvedAccountId})`,
284
+ };
285
+ }
286
+ if (!account.backendUrl || !account.deviceToken) {
287
+ return {
288
+ ok: false,
289
+ error: `oomi account is missing backendUrl/deviceToken (${resolvedAccountId})`,
290
+ };
291
+ }
292
+
293
+ const content = extractText(payload);
294
+ if (!content) {
295
+ return {
296
+ ok: false,
297
+ error: 'oomi outbound message content is empty',
298
+ };
299
+ }
300
+
301
+ const conversationKey = extractConversationKey(payload);
302
+ const userId = extractUserId(payload);
303
+ const sessionKey = toString(payload?.sessionKey || payload?.metadata?.sessionKey, account.defaultSessionKey);
304
+ const messageId = extractMessageId(payload);
305
+ const correlationId = extractCorrelationId(payload);
306
+
307
+ const response = await postJson({
308
+ url: `${account.backendUrl}/v1/channel/plugin/messages`,
309
+ token: account.deviceToken,
310
+ timeoutMs: account.requestTimeoutMs,
311
+ body: {
312
+ messageId,
313
+ correlationId,
314
+ conversationKey,
315
+ userId,
316
+ sessionKey,
317
+ content,
318
+ source: 'openclaw.channel',
319
+ metadata: normalizeOutgoingMetadata(payload?.metadata, {
320
+ accountId: resolvedAccountId,
321
+ correlationId,
322
+ content,
323
+ }),
324
+ },
325
+ });
326
+
327
+ if (!response.ok) {
328
+ const reason = toString(response.payload?.error, `status ${response.status}`);
329
+ const code = toString(response.payload?.errorCode);
330
+ return {
331
+ ok: false,
332
+ error: `oomi plugin message publish failed: ${reason}${code ? ` (code=${code})` : ''}`,
333
+ code,
334
+ };
335
+ }
336
+
337
+ return {
338
+ ok: true,
339
+ providerMessageId: toString(response.payload?.message?.messageId),
340
+ };
341
+ },
342
+ },
343
+ };
344
+
345
+ export default function register(api) {
346
+ api.registerChannel({ plugin: oomiChannelPlugin });
347
+ }