@7ots/cli 0.1.6 → 0.1.8
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 +3 -3
- package/cli/7ots.mjs +13 -1
- package/cli/lib/account.mjs +152 -2
- package/cli/lib/assistant.mjs +107 -38
- package/cli/lib/brain.mjs +13 -2
- package/cli/lib/config.mjs +5 -1
- package/cli/lib/inferences.mjs +30 -6
- package/cli/lib/integrations.mjs +69 -5
- package/cli/lib/orquesta.mjs +44 -1
- package/cli/lib/pet-server.mjs +352 -11
- package/cli/lib/prompts.mjs +31 -17
- package/cli/lib/prowl.mjs +110 -0
- package/cli/pet/electron/director.cjs +37 -5
- package/cli/pet/electron/main.cjs +92 -10
- package/cli/pet/pet.html +149 -10
- package/cli/pet/settings.html +549 -43
- package/dist/7ots.esm.js +216 -137
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +216 -137
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +1 -1
- package/package.json +17 -14
- package/server/channels/agent.mjs +7 -1
- package/server/channels/index.mjs +3 -2
- package/server/platform/connect.mjs +4 -1
- package/server/platform/db.mjs +14 -1
- package/server/platform/device.mjs +101 -4
- package/server/platform/mods.mjs +222 -0
- package/server/platform/orquesta.mjs +78 -14
- package/server/platform/profile.mjs +161 -0
- package/server/platform/routes.mjs +39 -33
- package/server/platform/runtime.mjs +5 -3
- package/server/platform/store.mjs +1 -1
- package/server/platform/vm.mjs +688 -17
- package/server/server.mjs +12 -2
- package/server/tts.mjs +17 -4
- package/src/avatar/three.js +3 -2
- package/src/character/AvatarEditor.js +95 -7
- package/src/character/Character.js +116 -17
- package/src/character/TradingCard.js +226 -0
- package/src/character/mods/index.js +4 -0
- package/src/character/mods/orc.js +64 -0
- package/src/character/mods.js +251 -0
- package/src/character/more/acc-head.js +213 -0
- package/src/character/more/acc-rest.js +342 -0
- package/src/character/more/brows.js +39 -0
- package/src/character/more/cheeks.js +78 -0
- package/src/character/more/eyes.js +288 -0
- package/src/character/more/mouths.js +259 -0
- package/src/character/more/patterns.js +120 -0
- package/src/character/parts.js +100 -0
- package/src/character/skin.js +166 -1
- package/src/character/styles.js +1285 -9
- package/src/character3d/Character3D.js +253 -37
- package/src/character3d/index.js +7 -1
- package/src/character3d/model.js +367 -0
- package/src/i18n/messages/character.js +37 -22
- package/src/i18n/messages/platform.js +33 -0
- package/src/identity/random.js +17 -3
- package/src/identity/schema.js +10 -0
- package/src/index.js +1 -0
|
@@ -32,7 +32,7 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
32
32
|
import { resolveLocale, translator } from '../../src/i18n/index.js';
|
|
33
33
|
import { identityPrompt } from '../identity.mjs';
|
|
34
34
|
import { i18nError } from '../i18n.mjs';
|
|
35
|
-
import { TASK_FINAL, addTask, getIntegration, getPlatformState, getTask, openTasks, setIntegration, setPlatformState, tasksLastDay, updateTask } from './store.mjs';
|
|
35
|
+
import { TASK_FINAL, addTask, getIntegration, getPlatformState, getTask, listOts, openTasks, setIntegration, setPlatformState, tasksLastDay, updateTask } from './store.mjs';
|
|
36
36
|
|
|
37
37
|
const penv = process.env;
|
|
38
38
|
const ORQ = (penv.ORQUESTA_URL || 'https://getorquesta.com').replace(/\/+$/, '');
|
|
@@ -201,16 +201,21 @@ async function bearer(accountId) {
|
|
|
201
201
|
return s.access;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Orquesta API as the account (a 401 with OAuth renews the token once). With `ots`, the request acts
|
|
206
|
+
* for that ot: Orquesta narrows it to the projects linked to the ot (project → Identity → Connected
|
|
207
|
+
* ots) and to the scopes of each link (read / run / deploy) — never more than the account itself.
|
|
208
|
+
*/
|
|
209
|
+
async function orq(accountId, method, path, body, timeoutMs, ots = null) {
|
|
210
|
+
const as = ots ? { 'X-Orquesta-External-Agent': `7ots/${ots.id}` } : {};
|
|
211
|
+
let r = await http(ORQ + path, { method, body, timeoutMs, headers: { Authorization: `Bearer ${await bearer(accountId)}`, ...as } });
|
|
207
212
|
if (r.status === 401) {
|
|
208
213
|
const s = getIntegration(accountId, 'orquesta');
|
|
209
214
|
if (s?.kind !== 'oauth') {
|
|
210
215
|
if (s) setIntegration(accountId, 'orquesta', { ...s, error: 'refused' });
|
|
211
216
|
throw i18nError(409, 'platform.orquesta.reconnect');
|
|
212
217
|
}
|
|
213
|
-
r = await http(ORQ + path, { method, body, headers: { Authorization: `Bearer ${await refresh(accountId)}
|
|
218
|
+
r = await http(ORQ + path, { method, body, timeoutMs, headers: { Authorization: `Bearer ${await refresh(accountId)}`, ...as } });
|
|
214
219
|
if (r.status === 401) {
|
|
215
220
|
setIntegration(accountId, 'orquesta', { ...getIntegration(accountId, 'orquesta'), error: 'refused' });
|
|
216
221
|
throw i18nError(409, 'platform.orquesta.reconnect');
|
|
@@ -219,12 +224,69 @@ async function orq(accountId, method, path, body) {
|
|
|
219
224
|
return r;
|
|
220
225
|
}
|
|
221
226
|
|
|
227
|
+
// ───────────────────────────── the account's own VMs (vm.mjs, provider 'orquesta_user') ─────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Whether the connected account can create servers in its own Orquesta org. Orquesta's infrastructure
|
|
231
|
+
* routes (POST /api/v1/projects, /api/v1/projects/:id/vm, /api/v1/vms) accept only org API keys (oak_)
|
|
232
|
+
* with projects:write + vms:write, never OAuth tokens; a key's scopes are only known when Orquesta
|
|
233
|
+
* answers (403 → 'scopes').
|
|
234
|
+
*/
|
|
235
|
+
export function orquestaVmAccess(accountId) {
|
|
236
|
+
const s = getIntegration(accountId, 'orquesta');
|
|
237
|
+
if (!s) return { connected: false, usable: false, reason: 'notConnected' };
|
|
238
|
+
if (s.error) return { connected: false, usable: false, reason: 'reconnect' };
|
|
239
|
+
if (s.kind !== 'key') return { connected: true, usable: false, reason: 'needsKey' };
|
|
240
|
+
return { connected: true, usable: true, reason: null };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Orquesta's infrastructure API as the account → { ok, status, data }. The account's credential stays
|
|
245
|
+
* sealed here and only ever goes to ORQUESTA_URL; vm.mjs never sees it.
|
|
246
|
+
*/
|
|
247
|
+
export async function orquestaInfra(accountId, method, path, body, timeoutMs = 20_000) {
|
|
248
|
+
if (!/^\/api\/v1\/(projects|vms)(\/|$)/.test(path)) throw new Error('orquestaInfra: path not allowed');
|
|
249
|
+
return orq(accountId, method, path, body, timeoutMs);
|
|
250
|
+
}
|
|
251
|
+
|
|
222
252
|
export async function listProjects(accountId) {
|
|
223
253
|
const r = await orq(accountId, 'GET', '/api/v1/projects');
|
|
224
254
|
if (!r.ok) throw i18nError(424, 'platform.orquesta.failed', { status: r.status });
|
|
225
255
|
return (r.data.projects || []).slice(0, 200).map((p) => ({ id: String(p.id), name: String(p.name || p.slug || p.id).slice(0, 120), online: !!p.agentOnline, lastSeen: p.agentLastSeen || null }));
|
|
226
256
|
}
|
|
227
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Tells Orquesta which ots this account has (name + face), so a project admin can connect them to
|
|
260
|
+
* projects there. An ot with no link yet is seeded with the project chosen in its 7ots panel (read,
|
|
261
|
+
* plus run when tasks are on) — Orquesta ignores the seed once the ot has any link, and never seeds
|
|
262
|
+
* `deploy`. Re-sent when the list changes or hourly; failures only log (older Orquesta: 404).
|
|
263
|
+
*/
|
|
264
|
+
const registered = new Map();
|
|
265
|
+
export async function registerOts(accountId, { force = false } = {}) {
|
|
266
|
+
const ots = listOts(accountId).filter((o) => /^[A-Za-z0-9._:-]{1,128}$/.test(o.id));
|
|
267
|
+
const agents = ots.map((o) => {
|
|
268
|
+
const cfg = otsOrquesta(o);
|
|
269
|
+
return { id: o.id, name: String(o.name || '').slice(0, 80), ...(cfg.project ? { seed: { projectId: cfg.project, scopes: cfg.tasks ? ['read', 'run'] : ['read'] } } : {}) };
|
|
270
|
+
});
|
|
271
|
+
const sig = JSON.stringify(agents);
|
|
272
|
+
const prev = registered.get(accountId);
|
|
273
|
+
if (!force && prev && prev.sig === sig && Date.now() - prev.at < 3600_000) return prev.agents;
|
|
274
|
+
const r = await orq(accountId, 'PUT', '/api/v1/external-agents', { provider: '7ots', agents });
|
|
275
|
+
if (!r.ok) {
|
|
276
|
+
console.warn(`[7ots] orquesta: registering ots → ${r.status}`);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
registered.set(accountId, { sig, at: Date.now(), agents: r.data.agents || [] });
|
|
280
|
+
return r.data.agents || [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The projects Orquesta lets this ot reach, with the scopes of each link ([] when unknown). */
|
|
284
|
+
export async function otLinks(ots) {
|
|
285
|
+
const agents = await registerOts(ots.accountId).catch(() => null);
|
|
286
|
+
const me = (agents || []).find((a) => a.id === ots.id);
|
|
287
|
+
return (me?.projects || []).map((p) => ({ id: String(p.projectId), name: String(p.projectName || p.projectId), scopes: (p.scopes || []).map(String) }));
|
|
288
|
+
}
|
|
289
|
+
|
|
228
290
|
// ───────────────────────────── per-ot settings ─────────────────────────────
|
|
229
291
|
|
|
230
292
|
/** Panel settings of an ot. */
|
|
@@ -298,19 +360,19 @@ function promptContent(ots, { task, who, channel }) {
|
|
|
298
360
|
].join('\n');
|
|
299
361
|
}
|
|
300
362
|
|
|
301
|
-
async function fetchPrompt(accountId, id) {
|
|
302
|
-
const r = await orq(accountId, 'GET', `/api/v1/prompts/${encodeURIComponent(id)}
|
|
363
|
+
async function fetchPrompt(accountId, id, ots) {
|
|
364
|
+
const r = await orq(accountId, 'GET', `/api/v1/prompts/${encodeURIComponent(id)}`, undefined, undefined, ots);
|
|
303
365
|
if (!r.ok) throw i18nError(424, 'platform.orquesta.failed', { status: r.status });
|
|
304
366
|
return r.data.prompt || {};
|
|
305
367
|
}
|
|
306
368
|
|
|
307
369
|
/** Text the agent produced: `result` when Orquesta has it, else the 'output' log lines. */
|
|
308
|
-
async function outputOf(accountId, id, prompt) {
|
|
370
|
+
async function outputOf(accountId, id, prompt, ots) {
|
|
309
371
|
if (typeof prompt.result === 'string' && prompt.result.trim()) return prompt.result.trim().slice(0, 20000);
|
|
310
372
|
const out = [];
|
|
311
373
|
let after = null;
|
|
312
374
|
for (let page = 0; page < 5; page++) {
|
|
313
|
-
const r = await orq(accountId, 'GET', `/api/v1/logs?${new URLSearchParams({ promptId: id, ...(after != null ? { afterSequence: String(after) } : {}) })}
|
|
375
|
+
const r = await orq(accountId, 'GET', `/api/v1/logs?${new URLSearchParams({ promptId: id, ...(after != null ? { afterSequence: String(after) } : {}) })}`, undefined, undefined, ots);
|
|
314
376
|
if (!r.ok) break;
|
|
315
377
|
const logs = r.data.logs || [];
|
|
316
378
|
for (const l of logs) if (l.category === 'output' && l.message) out.push(String(l.message));
|
|
@@ -323,10 +385,10 @@ async function outputOf(accountId, id, prompt) {
|
|
|
323
385
|
/** Refreshes a stored task from Orquesta (only while it isn't finished). */
|
|
324
386
|
async function refreshTask(ots, task) {
|
|
325
387
|
if (TASK_FINAL.includes(task.status)) return task;
|
|
326
|
-
const p = await fetchPrompt(ots.accountId, task.id);
|
|
388
|
+
const p = await fetchPrompt(ots.accountId, task.id, ots);
|
|
327
389
|
const status = ['pending', 'running', 'completed', 'failed', 'cancelled'].includes(p.status) ? p.status : task.status;
|
|
328
390
|
const patch = { status, error: p.error ? String(p.error) : undefined };
|
|
329
|
-
if (TASK_FINAL.includes(status)) patch.result = await outputOf(ots.accountId, task.id, p).catch(() => '');
|
|
391
|
+
if (TASK_FINAL.includes(status)) patch.result = await outputOf(ots.accountId, task.id, p, ots).catch(() => '');
|
|
330
392
|
updateTask(task.id, patch);
|
|
331
393
|
return getTask(ots.id, task.id);
|
|
332
394
|
}
|
|
@@ -362,17 +424,19 @@ export async function runTask(ots, { task, who, channel }) {
|
|
|
362
424
|
if (!TASK_FINAL.includes(cur.status) && Date.now() - cur.createdAt < 24 * 3600_000) return { ok: false, error: t('platform.orquesta.busy', { id: cur.id }) };
|
|
363
425
|
}
|
|
364
426
|
offline = (await listProjects(ots.accountId).catch(() => [])).some((p) => p.id === cfg.project && !p.online);
|
|
427
|
+
await registerOts(ots.accountId).catch(() => null);
|
|
365
428
|
const r = await orq(ots.accountId, 'POST', '/api/v1/prompts', {
|
|
366
429
|
projectId: cfg.project,
|
|
367
430
|
content: promptContent(ots, { task: text, who, channel }),
|
|
368
431
|
context: `7ots ot "${ots.name}" · ${channel} · ${who}`.slice(0, 500),
|
|
369
432
|
tags: ['7ots', channel],
|
|
370
433
|
source: 'api',
|
|
371
|
-
});
|
|
434
|
+
}, undefined, ots);
|
|
372
435
|
if (!r.ok) {
|
|
373
436
|
const code = String(r.data.code || r.data.error || '');
|
|
374
437
|
if (/AGENT_NOT_CONFIGURED/i.test(code)) return { ok: false, error: t('platform.orquesta.noAgent', { url: projectUrl(cfg.project) }), project_url: projectUrl(cfg.project) };
|
|
375
|
-
|
|
438
|
+
// the ot acts with its own links: not connected to that project, or connected without `run`
|
|
439
|
+
if (r.status === 404 || /EXTERNAL_AGENT_NOT_ALLOWED/.test(code)) return { ok: false, error: t('platform.orquesta.notLinked', { url: projectUrl(cfg.project) }), project_url: projectUrl(cfg.project) };
|
|
376
440
|
if (r.status === 429) return { ok: false, error: t('platform.orquesta.rateLimited') };
|
|
377
441
|
return { ok: false, error: t('platform.orquesta.submitFailed', { status: r.status }) };
|
|
378
442
|
}
|
|
@@ -414,7 +478,7 @@ export async function cancelTask(ots, id) {
|
|
|
414
478
|
const row = getTask(ots.id, id);
|
|
415
479
|
if (!row) return null;
|
|
416
480
|
if (TASK_FINAL.includes(row.status)) return row;
|
|
417
|
-
const r = await orq(ots.accountId, 'POST', `/api/v1/prompts/${encodeURIComponent(row.id)}/cancel
|
|
481
|
+
const r = await orq(ots.accountId, 'POST', `/api/v1/prompts/${encodeURIComponent(row.id)}/cancel`, undefined, undefined, ots);
|
|
418
482
|
if (!r.ok) throw i18nError(424, 'platform.orquesta.failed', { status: r.status });
|
|
419
483
|
updateTask(row.id, { status: r.data.status || 'cancelled' });
|
|
420
484
|
return getTask(ots.id, row.id);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perfil público de un ot: lo que ve una persona al abrir /api/o/<id>/identity en el navegador.
|
|
3
|
+
*
|
|
4
|
+
* Portada con el personaje (SVG renderizado aquí, así la primera pintura y la vista previa ya lo
|
|
5
|
+
* traen; luego el navegador lo anima), quién es, sus rasgos únicos (los mismos de traits.json) y la
|
|
6
|
+
* tarjeta de contacto <ots-identity> con sus acciones (escuchar, correo, @handle, llamada, vCard).
|
|
7
|
+
*/
|
|
8
|
+
import { renderCharacter } from '../../src/character/Character.js';
|
|
9
|
+
|
|
10
|
+
const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
11
|
+
const hex = (v, def) => (/^#[0-9a-f]{3,8}$/i.test(v || '') ? v : def);
|
|
12
|
+
|
|
13
|
+
const L = {
|
|
14
|
+
en: { about: 'About', tone: 'Tone', speaks: 'Speaks', traits: 'Unique traits', traitsSub: 'No two ots share this face. The fingerprint is what makes it one of a kind.', print: 'Fingerprint', contact: 'Contact', json: 'JSON', make: 'Create your own ot', unique: 'One of a kind', soon: 'NFT soon', noBio: '{name} hasn’t written a bio yet.' },
|
|
15
|
+
es: { about: 'Sobre mí', tone: 'Tono', speaks: 'Habla', traits: 'Rasgos únicos', traitsSub: 'No hay dos ots con esta cara. La huella es lo que lo hace único.', print: 'Huella', contact: 'Contacto', json: 'JSON', make: 'Crea tu propio ot', unique: 'Único', soon: 'NFT pronto', noBio: '{name} todavía no escribió su bio.' },
|
|
16
|
+
pt: { about: 'Sobre', tone: 'Tom', speaks: 'Fala', traits: 'Traços únicos', traitsSub: 'Não existem dois ots com este rosto. A impressão é o que o torna único.', print: 'Impressão', contact: 'Contato', json: 'JSON', make: 'Crie seu próprio ot', unique: 'Único', soon: 'NFT em breve', noBio: '{name} ainda não escreveu a bio.' },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const LANG_NAMES = { es: 'Español', en: 'English', pt: 'Português', fr: 'Français', de: 'Deutsch', it: 'Italiano', ja: '日本語', zh: '中文', ko: '한국어', ru: 'Русский', ar: 'العربية', hi: 'हिन्दी', nl: 'Nederlands', pl: 'Polski', tr: 'Türkçe', et: 'Eesti' };
|
|
20
|
+
|
|
21
|
+
/** 32 hex → código de barras de colores: la huella se ve, no solo se lee. */
|
|
22
|
+
function printBars(print) {
|
|
23
|
+
if (!print) return '';
|
|
24
|
+
const bars = print.match(/../g).map((b, i) => {
|
|
25
|
+
const n = parseInt(b, 16);
|
|
26
|
+
return `<rect x="${i * 16 + 2}" y="${n % 3 === 0 ? 4 : 0}" width="${4 + (n % 8)}" height="${n % 3 === 0 ? 28 : 32}" rx="1.5" fill="hsl(${Math.round((n / 255) * 360)} 80% 65%)"/>`;
|
|
27
|
+
});
|
|
28
|
+
return `<svg class="bars" viewBox="0 0 256 32" preserveAspectRatio="none" aria-hidden="true">${bars.join('')}</svg>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* card: publicIdentity(...) · meta: traitsMeta(...) · ep: base /api/o/<id> · locale: idioma del visitante.
|
|
33
|
+
*/
|
|
34
|
+
export function profilePage(card, meta, ep, locale = 'en') {
|
|
35
|
+
const l = L[String(locale).slice(0, 2)] || L.en;
|
|
36
|
+
const name = esc(card.name);
|
|
37
|
+
const desc = esc(card.tagline || card.role || '');
|
|
38
|
+
const c1 = hex(card.look?.color, '#6d5dfc');
|
|
39
|
+
const c2 = hex(card.look?.accent, '#22d3ee');
|
|
40
|
+
const look = card.look || {};
|
|
41
|
+
let portrait = `<span class="initial">${esc((look.emoji || card.name.slice(0, 1)).toUpperCase())}</span>`;
|
|
42
|
+
if (look.kind === 'image' && look.image) portrait = `<img src="${esc(look.image)}" alt="" referrerpolicy="no-referrer">`;
|
|
43
|
+
else if (look.character && Object.keys(look.character).length) {
|
|
44
|
+
try {
|
|
45
|
+
portrait = renderCharacter(look.character, {}, { title: card.name });
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
const langs = [...new Set([card.language, ...(card.languages || [])].filter(Boolean))];
|
|
49
|
+
const traits = card.personality?.traits || [];
|
|
50
|
+
const handle = meta?.handle;
|
|
51
|
+
const attrs = (meta?.attributes || []).filter((a) => a.trait_type !== 'Kind');
|
|
52
|
+
const tile = (a) => {
|
|
53
|
+
const v = String(a.value);
|
|
54
|
+
const sw = /^#[0-9a-f]{3,8}$/i.test(v) ? `<i class="sw" style="background:${v}"></i>` : '';
|
|
55
|
+
return `<div class="tile"><dt>${esc(a.trait_type)}</dt><dd>${sw}${esc(sw ? v.toUpperCase() : v.replace(/[-_]/g, ' '))}</dd></div>`;
|
|
56
|
+
};
|
|
57
|
+
const jsonUrl = `${esc(ep)}/identity?format=json`;
|
|
58
|
+
|
|
59
|
+
return `<!doctype html><html lang="${esc(card.language || 'en')}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
60
|
+
<title>${name} · 7ots</title><meta name="description" content="${desc}">
|
|
61
|
+
<meta property="og:title" content="${name} · 7ots"><meta property="og:description" content="${desc}"><meta property="og:type" content="profile">
|
|
62
|
+
<meta name="theme-color" content="${c1}">
|
|
63
|
+
<link rel="alternate" type="application/json" href="${jsonUrl}"><link rel="icon" href="/favicon.ico">
|
|
64
|
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,800&family=Inter:wght@400;500;600&display=swap">
|
|
65
|
+
<style>
|
|
66
|
+
:root{color-scheme:dark;--c1:${c1};--c2:${c2};--bg:#0b0919;--panel:rgb(255 255 255/.045);--line:rgb(255 255 255/.09);--ink:#f3f1ff;--muted:#a7a2c9;--disp:"Bricolage Grotesque",system-ui,sans-serif}
|
|
67
|
+
*{box-sizing:border-box}html,body{margin:0}
|
|
68
|
+
body{min-height:100vh;font:15px/1.55 Inter,system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:var(--bg);overflow-x:hidden}
|
|
69
|
+
body::before{content:"";position:fixed;inset:-20%;z-index:-1;background:radial-gradient(700px 520px at 20% 8%,color-mix(in srgb,var(--c1) 42%,transparent),transparent 65%),radial-gradient(640px 520px at 92% 30%,color-mix(in srgb,var(--c2) 26%,transparent),transparent 65%),radial-gradient(900px 600px at 50% 120%,color-mix(in srgb,var(--c1) 22%,transparent),transparent 60%)}
|
|
70
|
+
a{color:inherit}
|
|
71
|
+
.top{max-width:1080px;margin:0 auto;padding:18px 20px;display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
72
|
+
.brand{display:flex;align-items:center;gap:8px;font:800 17px var(--disp);text-decoration:none;letter-spacing:-.02em}
|
|
73
|
+
.brand img{width:26px;height:26px;border-radius:7px}
|
|
74
|
+
.cta{font:600 13px Inter,sans-serif;text-decoration:none;padding:8px 14px;border-radius:999px;background:var(--ink);color:#120f2a}
|
|
75
|
+
.cta:hover{opacity:.9}
|
|
76
|
+
main{max-width:1080px;margin:0 auto;padding:8px 20px 56px;display:grid;gap:22px}
|
|
77
|
+
.hero{display:grid;grid-template-columns:minmax(0,300px) minmax(0,1fr);gap:36px;align-items:center;padding:28px;border:1px solid var(--line);border-radius:28px;background:linear-gradient(140deg,color-mix(in srgb,var(--c1) 18%,transparent),rgb(255 255 255/.02) 55%);backdrop-filter:blur(6px)}
|
|
78
|
+
.stage{position:relative;aspect-ratio:1;border-radius:24px;display:grid;place-items:center;background:radial-gradient(circle at 50% 42%,color-mix(in srgb,var(--c2) 30%,transparent),transparent 62%),rgb(0 0 0/.18);overflow:hidden}
|
|
79
|
+
.stage::after{content:"";position:absolute;left:18%;right:18%;bottom:9%;height:7%;border-radius:50%;background:rgb(0 0 0/.35);filter:blur(10px)}
|
|
80
|
+
.stage>.av{position:relative;z-index:1;width:82%;height:82%;display:grid;place-items:center}
|
|
81
|
+
.stage svg{width:100%;height:100%;overflow:visible}
|
|
82
|
+
.stage img{width:100%;height:100%;object-fit:cover;border-radius:18px}
|
|
83
|
+
.initial{font:800 120px var(--disp);color:var(--c1)}
|
|
84
|
+
.who{display:grid;gap:12px;align-content:center;min-width:0}
|
|
85
|
+
.badges{display:flex;flex-wrap:wrap;gap:8px}
|
|
86
|
+
.badge{display:inline-flex;align-items:center;gap:6px;font:600 12px Inter,sans-serif;padding:5px 10px;border-radius:999px;border:1px solid var(--line);background:rgb(255 255 255/.05);color:var(--muted)}
|
|
87
|
+
.badge.u{color:var(--ink);border-color:color-mix(in srgb,var(--c2) 55%,transparent);background:color-mix(in srgb,var(--c2) 14%,transparent)}
|
|
88
|
+
.badge svg{width:13px;height:13px}
|
|
89
|
+
h1{margin:0;font:800 clamp(40px,7vw,68px)/1 var(--disp);letter-spacing:-.035em;overflow-wrap:anywhere}
|
|
90
|
+
.handle{font:600 16px Inter,sans-serif;color:var(--c2)}
|
|
91
|
+
.role{margin:0;font-size:18px;color:var(--muted);text-transform:capitalize}
|
|
92
|
+
.tagline{margin:0;font:600 21px/1.35 var(--disp);letter-spacing:-.01em;max-width:34ch}
|
|
93
|
+
.grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,380px);gap:22px;align-items:start}
|
|
94
|
+
.col{display:grid;gap:22px;min-width:0}
|
|
95
|
+
.panel{border:1px solid var(--line);border-radius:22px;background:var(--panel);padding:22px;display:grid;gap:14px}
|
|
96
|
+
.panel h2{margin:0;font:700 13px Inter,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
97
|
+
.panel h2 small{font:600 11px Inter,sans-serif;letter-spacing:0;text-transform:none;padding:3px 8px;border-radius:999px;background:rgb(255 255 255/.07)}
|
|
98
|
+
.bio{margin:0;font-size:16px;white-space:pre-line}
|
|
99
|
+
.bio.empty{color:var(--muted);font-style:italic}
|
|
100
|
+
.kv{display:grid;grid-template-columns:auto 1fr;gap:10px 16px;margin:0;align-items:baseline}
|
|
101
|
+
.kv dt{color:var(--muted);font-size:13px}
|
|
102
|
+
.kv dd{margin:0}
|
|
103
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}
|
|
104
|
+
.chips li{font-size:13px;padding:4px 10px;border-radius:999px;background:rgb(255 255 255/.07);border:1px solid var(--line)}
|
|
105
|
+
.sub{margin:-6px 0 0;color:var(--muted);font-size:13px}
|
|
106
|
+
.tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;margin:0}
|
|
107
|
+
.tile{border:1px solid var(--line);border-radius:14px;padding:11px 13px;background:rgb(0 0 0/.15)}
|
|
108
|
+
.tile dt{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
|
109
|
+
.tile dd{margin:3px 0 0;font-weight:600;text-transform:capitalize;display:flex;align-items:center;gap:7px;overflow-wrap:anywhere}
|
|
110
|
+
.sw{width:14px;height:14px;border-radius:5px;flex:none;box-shadow:inset 0 0 0 1px rgb(255 255 255/.25)}
|
|
111
|
+
.print{display:grid;gap:8px;padding:14px;border-radius:14px;background:rgb(0 0 0/.25);border:1px solid var(--line)}
|
|
112
|
+
.print span{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
|
113
|
+
.print code{font:500 12.5px ui-monospace,SFMono-Regular,Menlo,monospace;overflow-wrap:anywhere;color:var(--ink)}
|
|
114
|
+
.bars{width:100%;height:30px}
|
|
115
|
+
ots-identity{display:block;--ots-bg:#16132e;--ots-fg:#f3f1ff;--ots-line:rgb(255 255 255/.1)}
|
|
116
|
+
.links{display:flex;gap:16px;flex-wrap:wrap;font-size:13px;color:var(--muted);justify-content:center}
|
|
117
|
+
.links a{text-decoration:none}.links a:hover{color:var(--ink)}
|
|
118
|
+
@media (max-width:820px){.hero{grid-template-columns:1fr;gap:22px;padding:20px;text-align:center}.stage{max-width:280px;width:100%;margin:0 auto}.badges,.who{justify-items:center;justify-content:center}.tagline{margin-inline:auto}.grid{grid-template-columns:1fr}}
|
|
119
|
+
@media (max-width:480px){main,.top{padding-inline:16px}.panel{padding:18px}}
|
|
120
|
+
@media (prefers-reduced-motion:no-preference){.stage>.av{animation:float 5s ease-in-out infinite}@keyframes float{50%{transform:translateY(-6px)}}}
|
|
121
|
+
</style></head><body>
|
|
122
|
+
<header class="top"><a class="brand" href="https://7ots.com"><img src="/brand/icon-512.png" alt="">7ots</a><a class="cta" href="https://7ots.com">${l.make}</a></header>
|
|
123
|
+
<main>
|
|
124
|
+
<section class="hero">
|
|
125
|
+
<div class="stage"><div class="av" id="av">${portrait}</div></div>
|
|
126
|
+
<div class="who">
|
|
127
|
+
<div class="badges">${meta?.fingerprint ? `<span class="badge u"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9z"/></svg>${l.unique}</span>` : ''}${langs.map((x) => `<span class="badge">${esc(LANG_NAMES[x] || x.toUpperCase())}</span>`).join('')}</div>
|
|
128
|
+
<h1>${name}</h1>
|
|
129
|
+
${handle ? `<span class="handle">@${esc(handle)}</span>` : ''}
|
|
130
|
+
${card.role ? `<p class="role">${esc(card.role)}</p>` : ''}
|
|
131
|
+
${card.tagline ? `<p class="tagline">“${esc(card.tagline)}”</p>` : ''}
|
|
132
|
+
</div>
|
|
133
|
+
</section>
|
|
134
|
+
<div class="grid">
|
|
135
|
+
<div class="col">
|
|
136
|
+
<section class="panel"><h2>${l.about}</h2>
|
|
137
|
+
${card.bio ? `<p class="bio">${esc(card.bio)}</p>` : `<p class="bio empty">${esc(l.noBio.replace('{name}', card.name))}</p>`}
|
|
138
|
+
<dl class="kv">
|
|
139
|
+
${card.personality?.tone ? `<dt>${l.tone}</dt><dd>${esc(card.personality.tone)}</dd>` : ''}
|
|
140
|
+
${langs.length ? `<dt>${l.speaks}</dt><dd>${esc(langs.map((x) => LANG_NAMES[x] || x).join(' · '))}</dd>` : ''}
|
|
141
|
+
</dl>
|
|
142
|
+
${traits.length ? `<ul class="chips">${traits.map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` : ''}
|
|
143
|
+
</section>
|
|
144
|
+
${attrs.length || meta?.fingerprint ? `<section class="panel"><h2>${l.traits}<small>${l.soon}</small></h2><p class="sub">${l.traitsSub}</p>
|
|
145
|
+
${meta?.fingerprint ? `<div class="print"><span>${l.print}</span>${printBars(meta.fingerprint)}<code>${esc(meta.fingerprint)}</code></div>` : ''}
|
|
146
|
+
<dl class="tiles">${attrs.map(tile).join('')}</dl>
|
|
147
|
+
</section>` : ''}
|
|
148
|
+
</div>
|
|
149
|
+
<div class="col">
|
|
150
|
+
<section class="panel"><h2>${l.contact}</h2><ots-identity src="${jsonUrl}" face lang="${esc(String(locale).slice(0, 2))}"></ots-identity></section>
|
|
151
|
+
<p class="links"><a href="${jsonUrl}">${l.json}</a><a href="${esc(ep)}/traits.json">traits.json</a><a href="${esc(ep)}/identity.vcf">vCard</a><a href="https://7ots.com">7ots.com</a></p>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</main>
|
|
155
|
+
<script src="/api/agent/7ots.js"></script>
|
|
156
|
+
<script>
|
|
157
|
+
(function(){var c=${JSON.stringify(look.kind !== 'image' && look.character ? look.character : null).replace(/</g, '\\u003c')};var el=document.getElementById('av');
|
|
158
|
+
if(!c||!window.SevenOts||!SevenOts.createCharacter)return;try{el.innerHTML='';SevenOts.createCharacter(el,c,{title:${JSON.stringify(card.name).replace(/</g, '\\u003c')}});}catch(e){}})();
|
|
159
|
+
</script>
|
|
160
|
+
</body></html>`;
|
|
161
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* GET /api/platform/ots/:id/usage
|
|
15
15
|
* * /api/platform/ots/:id/admin/* misma forma que /api/agent/admin/* (el backoffice
|
|
16
16
|
* edita un ots con ?ots=<id>)
|
|
17
|
+
* * /api/mods, /api/platform/mods/*, /api/device/mods/* community mods (mods.mjs)
|
|
17
18
|
* GET /api/platform/sites POST { kind, url, name, guide } · POST …/:id/verify · DELETE …/:id (sites.mjs)
|
|
18
19
|
* GET /api/platform/device/:code · POST { approve } GET /api/platform/devices · DELETE …/:id
|
|
19
20
|
*
|
|
@@ -25,9 +26,11 @@
|
|
|
25
26
|
* apuchat identity and Orquesta tasks (…/apuchat/*, …/orquesta/*, /auth/apuchat|orquesta): see connect.mjs
|
|
26
27
|
*/
|
|
27
28
|
|
|
29
|
+
import { createReadStream, existsSync, statSync } from 'node:fs';
|
|
30
|
+
import { join } from 'node:path';
|
|
28
31
|
import { MEET_AVATARS, MEET_SCENES, OPENAI_VOICES, GROK_VOICES, defineIdentity, identityPrompt, publicIdentity, toVCard } from '../identity.mjs';
|
|
29
32
|
import { errorText, reqT, requestLocale } from '../i18n.mjs';
|
|
30
|
-
import { monthOf } from './db.mjs';
|
|
33
|
+
import { dataDir, monthOf } from './db.mjs';
|
|
31
34
|
import { randomIdentity, cleanSeed } from '../../src/identity/random.js';
|
|
32
35
|
import { currentAccount } from './auth.mjs';
|
|
33
36
|
import {
|
|
@@ -39,6 +42,8 @@ import { createConnect } from './connect.mjs';
|
|
|
39
42
|
import { createDevice, deviceFromBearer } from './device.mjs';
|
|
40
43
|
import { createVm } from './vm.mjs';
|
|
41
44
|
import { createSites } from './sites.mjs';
|
|
45
|
+
import { profilePage } from './profile.mjs';
|
|
46
|
+
import { createMods } from './mods.mjs';
|
|
42
47
|
import '../../src/i18n/messages/platform.js';
|
|
43
48
|
|
|
44
49
|
const penv = process.env;
|
|
@@ -119,7 +124,8 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
119
124
|
res.setHeader('Vary', 'Accept');
|
|
120
125
|
if (/text\/html/.test(req.headers.accept || '') && !/[?&]format=json\b/.test(req.url)) {
|
|
121
126
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
|
|
122
|
-
|
|
127
|
+
const ep = `${originOf(req)}/api/o/${ots.id}`;
|
|
128
|
+
return res.end(profilePage(card, traitsMeta(ots, ep), ep, requestLocale(req)));
|
|
123
129
|
}
|
|
124
130
|
return send(res, 200, card);
|
|
125
131
|
}
|
|
@@ -325,6 +331,7 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
325
331
|
if ((await connect.account(req, res, account, path)) !== false) return;
|
|
326
332
|
if (await device.dashboard(req, res, account, path)) return;
|
|
327
333
|
if (await sites(req, res, account, path)) return;
|
|
334
|
+
if ((path === '/mods' || path.startsWith('/mods/')) && (await mods.accountApi(req, res, account, path.slice('/mods'.length)))) return;
|
|
328
335
|
|
|
329
336
|
if (path === '/ots' && req.method === 'GET') {
|
|
330
337
|
const usage = usageByOts(account.id);
|
|
@@ -380,6 +387,7 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
380
387
|
if (sub === '' && req.method === 'DELETE') {
|
|
381
388
|
await stopOtsChannels(ots.id);
|
|
382
389
|
await connect.release(account, ots);
|
|
390
|
+
await vm.releaseOts(ots.id); // its 7ots server (Orquesta VM) goes too; ots_vms cascades with the ot
|
|
383
391
|
deleteOts(ots.id);
|
|
384
392
|
return send(res, 200, { ok: true });
|
|
385
393
|
}
|
|
@@ -402,14 +410,25 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
402
410
|
}
|
|
403
411
|
|
|
404
412
|
const connect = createConnect({ send, readJson, originOf, save });
|
|
405
|
-
const device = createDevice({ send, readJson, rateLimit, base: (req) => (penv.PLATFORM_URL || originOf(req)).replace(/\/+$/, ''), summary: (o) => otsSummary(o) });
|
|
413
|
+
const device = createDevice({ send, readJson, rateLimit, base: (req) => (penv.PLATFORM_URL || originOf(req)).replace(/\/+$/, ''), summary: (o) => otsSummary(o), save });
|
|
406
414
|
const sites = createSites({ send, readJson });
|
|
415
|
+
const mods = createMods({ send, readJson });
|
|
407
416
|
const vm = createVm({ send, readJson, rateLimit, base: (req) => (penv.PLATFORM_URL || originOf(req)).replace(/\/+$/, '') });
|
|
408
417
|
|
|
409
418
|
/** @returns {Promise<boolean>} true si atendió la ruta */
|
|
410
419
|
return async function handle(req, res, url) {
|
|
411
420
|
const p = url.pathname;
|
|
412
421
|
if (await connect.auth(req, res, url)) return true;
|
|
422
|
+
// models of mods (uploaded by a pet when it syncs): public, content-addressed, immutable
|
|
423
|
+
const mf = /^\/mods\/([a-f0-9]{16})\.(glb|vrm)$/.exec(p);
|
|
424
|
+
if (mf && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
425
|
+
const file = join(dataDir(), 'mods', `${mf[1]}.${mf[2]}`);
|
|
426
|
+
if (!existsSync(file)) return send(res, 404, { error: 'not found' }), true;
|
|
427
|
+
res.writeHead(200, { 'Content-Type': 'model/gltf-binary', 'Content-Length': statSync(file).size, 'Cache-Control': 'public, max-age=31536000, immutable', 'Access-Control-Allow-Origin': '*' });
|
|
428
|
+
if (req.method === 'HEAD') return res.end(), true;
|
|
429
|
+
createReadStream(file).pipe(res);
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
413
432
|
const m = p.match(/^\/api\/o\/([a-z0-9_]{4,40})(\/.*)$/);
|
|
414
433
|
if (m) {
|
|
415
434
|
await publicApi(req, res, m[1], m[2]);
|
|
@@ -417,9 +436,12 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
417
436
|
}
|
|
418
437
|
// The landing asks before sending the visitor to sign in: each ot is unique, a repeated face can't be created.
|
|
419
438
|
if (p === '/api/look/free' && req.method === 'POST') {
|
|
420
|
-
const { character } = await readJson(req,
|
|
421
|
-
const
|
|
422
|
-
|
|
439
|
+
const { character, characters } = await readJson(req, 256 * 1024);
|
|
440
|
+
const isObj = (c) => c && typeof c === 'object' && !Array.isArray(c);
|
|
441
|
+
const free = (c) => (isObj(c) ? !lookTaken({ look: { kind: 'character', character: c } }) : false);
|
|
442
|
+
// en lote (las sugerencias al azar del editor): { characters: [...] } → { free: [bool…] }
|
|
443
|
+
if (Array.isArray(characters)) send(res, 200, { free: characters.slice(0, 32).map(free) });
|
|
444
|
+
else send(res, 200, { free: free(character) });
|
|
423
445
|
return true;
|
|
424
446
|
}
|
|
425
447
|
const dv = p.match(/^\/api\/device\/ots\/([a-z0-9_]{4,40})\/vm(\/.*)?$/);
|
|
@@ -429,6 +451,17 @@ export function createPlatform({ send, readJson, rateLimit, callLimit, originOf,
|
|
|
429
451
|
else await vm.device(req, res, who, dv[1], dv[2] || '');
|
|
430
452
|
return true;
|
|
431
453
|
}
|
|
454
|
+
// community mods: public listing, and the same signed-in api for the pet (bearer) as for the dashboard
|
|
455
|
+
if (p === '/api/mods' || p.startsWith('/api/mods/')) {
|
|
456
|
+
if (!(await mods.publicApi(req, res, p.slice('/api/mods'.length)))) send(res, 404, { error: 'not found' });
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
if (p === '/api/device/mods' || p.startsWith('/api/device/mods/')) {
|
|
460
|
+
const who = deviceFromBearer(req);
|
|
461
|
+
if (!who) send(res, 401, { error: 'login' });
|
|
462
|
+
else if (!(await mods.accountApi(req, res, who.account, p.slice('/api/device/mods'.length)))) send(res, 404, { error: 'not found' });
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
432
465
|
if (p.startsWith('/api/vm/')) {
|
|
433
466
|
await vm.worker(req, res, p.slice('/api/vm'.length));
|
|
434
467
|
return true;
|
|
@@ -475,33 +508,6 @@ function sanitizeWidget(w) {
|
|
|
475
508
|
return out;
|
|
476
509
|
}
|
|
477
510
|
|
|
478
|
-
const htmlEsc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
479
|
-
/** Public profile of an ot (what /api/o/<id>/identity shows in a browser): its contact card with the live face. */
|
|
480
|
-
function profilePage(card, ep) {
|
|
481
|
-
const name = htmlEsc(card.name);
|
|
482
|
-
const desc = htmlEsc(card.tagline || card.role || '');
|
|
483
|
-
const lang = htmlEsc(card.language || 'en');
|
|
484
|
-
const c1 = /^#[0-9a-f]{3,8}$/i.test(card.look?.color || '') ? card.look.color : '#6d5dfc';
|
|
485
|
-
const c2 = /^#[0-9a-f]{3,8}$/i.test(card.look?.accent || '') ? card.look.accent : '#22d3ee';
|
|
486
|
-
return `<!doctype html><html lang="${lang}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
487
|
-
<title>${name} · 7ots</title><meta name="description" content="${desc}">
|
|
488
|
-
<meta property="og:title" content="${name} · 7ots"><meta property="og:description" content="${desc}"><meta property="og:type" content="profile">
|
|
489
|
-
<link rel="alternate" type="application/json" href="${htmlEsc(ep)}/identity?format=json"><link rel="icon" href="/favicon.ico">
|
|
490
|
-
<style>
|
|
491
|
-
:root{color-scheme:dark;--c1:${c1};--c2:${c2}}
|
|
492
|
-
*{box-sizing:border-box}html,body{margin:0;min-height:100%}
|
|
493
|
-
body{min-height:100vh;display:grid;place-items:center;padding:32px 16px;font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;color:#ece9ff;
|
|
494
|
-
background:radial-gradient(900px 600px at 15% 0%,color-mix(in srgb,var(--c1) 35%,transparent),transparent 60%),radial-gradient(800px 600px at 100% 100%,color-mix(in srgb,var(--c2) 22%,transparent),transparent 60%),#0d0a24}
|
|
495
|
-
main{width:min(440px,100%);display:grid;gap:16px}
|
|
496
|
-
ots-identity{display:block}
|
|
497
|
-
.foot{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;font-size:12px;color:#a9a3d6}
|
|
498
|
-
.foot a{color:inherit}
|
|
499
|
-
</style></head><body><main>
|
|
500
|
-
<ots-identity src="${htmlEsc(ep)}/identity?format=json" face></ots-identity>
|
|
501
|
-
<p class="foot"><a href="${htmlEsc(ep)}/identity?format=json">JSON</a><a href="${htmlEsc(ep)}/identity.vcf">vCard</a><a href="https://7ots.com">7ots.com</a></p>
|
|
502
|
-
</main><script src="/api/agent/7ots.js"></script></body></html>`;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
511
|
/**
|
|
506
512
|
* The ot's unique traits as NFT-style metadata (ERC-721 / Metaplex shape). The face fingerprint is
|
|
507
513
|
* what makes an ot unique (UNIQUE index); the permanent apuchat @handle is its unique name. When
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { createLLM } from '../llm.mjs';
|
|
17
|
-
import { synthesize as synthesizeWith, ttsProvider } from '../tts.mjs';
|
|
17
|
+
import { synthesize as synthesizeWith, ttsProvider, openaiTtsKey } from '../tts.mjs';
|
|
18
18
|
import { startAgentChannels } from '../channels/index.mjs';
|
|
19
19
|
import { apumailConfigFromEnv } from '../channels/apumail.mjs';
|
|
20
20
|
import { apuchatConfigFromEnv } from '../channels/apuchat.mjs';
|
|
21
21
|
import { i18nError } from '../i18n.mjs';
|
|
22
|
-
import { OTS_KEYS, accountUsage, addUsage, allLiveOts, getOts } from './store.mjs';
|
|
22
|
+
import { OTS_KEYS, accountUsage, addUsage, allLiveOts, getOts, logTurn } from './store.mjs';
|
|
23
23
|
import { orquestaTools } from './orquesta.mjs';
|
|
24
24
|
|
|
25
25
|
const penv = process.env;
|
|
@@ -75,7 +75,7 @@ export function otsEnv(ots) {
|
|
|
75
75
|
|
|
76
76
|
// Voz: la suya o la de apuchat de la plataforma.
|
|
77
77
|
let platformTts = false;
|
|
78
|
-
if (!TTS_KEYS.some((k) => env[k]) && penv.PLATFORM_APUCHAT_VOICE_TOKEN && (!env.TTS_PROVIDER || env.TTS_PROVIDER === 'apuchat')) {
|
|
78
|
+
if (!TTS_KEYS.some((k) => (k === 'OPENAI_API_KEY' ? openaiTtsKey(env) : env[k])) && penv.PLATFORM_APUCHAT_VOICE_TOKEN && (!env.TTS_PROVIDER || env.TTS_PROVIDER === 'apuchat')) {
|
|
79
79
|
platformTts = true;
|
|
80
80
|
// On the platform's token the engine and model are the platform's, not the ot's.
|
|
81
81
|
delete env.APUCHAT_VOICE_PROVIDER;
|
|
@@ -213,6 +213,8 @@ export function otsChannels(ots) {
|
|
|
213
213
|
log: (...a) => console.log(`[7ots:${id}]`, ...a),
|
|
214
214
|
// Orquesta tasks: only for senders on the ot's own list (see orquesta.mjs), read on every message.
|
|
215
215
|
tools: (ctx) => orquestaTools(current(), ctx),
|
|
216
|
+
// mails and DMs go to its conversations (dashboard, and the desktop ot through /api/device/…/inbox)
|
|
217
|
+
onTurn: ({ key, channel, user, reply }) => logTurn(current(), { key, channel: channel === 'dm' ? 'apuchat' : 'apumail', visitor: key.replace(/^\w+:/, ''), user, reply }),
|
|
216
218
|
});
|
|
217
219
|
channels.set(id, { stamp, ch });
|
|
218
220
|
return ch;
|
|
@@ -20,7 +20,7 @@ const parse = (s, fallback) => {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
export const SESSION_TTL = 30 * 24 * 3600_000;
|
|
23
|
-
export const MAX_OTS_PER_ACCOUNT = () => Number(process.env.PLATFORM_MAX_OTS ||
|
|
23
|
+
export const MAX_OTS_PER_ACCOUNT = () => Number(process.env.PLATFORM_MAX_OTS || 7);
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Ajustes que cada ots puede cambiar. Lo que no está aquí lo fija la plataforma
|