@johpaz/hive-sdk 0.1.4 → 0.1.6
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/CHANGELOG.md +129 -0
- package/README.md +78 -23
- package/bun.lock +55 -29
- package/bunfig.toml +4 -2
- package/docs/API-AGENTS.md +78 -27
- package/docs/API-CONTEXT-COMPILER.md +31 -34
- package/docs/API-TOOLS-SKILLS-CHANNELS.md +58 -22
- package/docs/HIVE-HARNESS.md +1 -1
- package/docs/INDEX.md +4 -4
- package/docs/TEMPLATE-HIVE-APP.md +10 -10
- package/package.json +17 -12
- package/packages/cli/package.json +2 -2
- package/packages/cli/src/commands/create-app.test.ts +36 -7
- package/packages/cli/src/commands/init.ts +3 -3
- package/packages/cli/src/commands/run.ts +1 -1
- package/packages/cli/src/commands/test.ts +37 -25
- package/packages/cli/src/commands/trace.ts +30 -28
- package/packages/cli/templates/hive-app/.env.example +10 -2
- package/packages/cli/templates/hive-app/README.md +103 -0
- package/packages/cli/templates/hive-app/hive.config.ts +9 -3
- package/packages/cli/templates/hive-app/src/agents/coordinator.ts +8 -1
- package/packages/cli/templates/hive-app/src/main.ts +12 -19
- package/packages/core/package.json +13 -12
- package/packages/core/src/agent/acceptance-checks.ts +172 -0
- package/packages/core/src/agent/agent-catalog.ts +348 -0
- package/packages/core/src/agent/agent-loop.ts +1373 -0
- package/packages/core/src/agent/capability-search.ts +186 -0
- package/packages/core/src/agent/catalog-selector.ts +103 -0
- package/packages/core/src/agent/{Compaction.ts → compaction.ts} +86 -63
- package/packages/core/src/agent/context-compiler.ts +689 -0
- package/packages/core/src/agent/conversation-store.ts +381 -0
- package/packages/core/src/agent/curator.ts +276 -0
- package/packages/core/src/agent/delegation-runtime.ts +241 -0
- package/packages/core/src/agent/goal-runner.ts +323 -0
- package/packages/core/src/agent/index.ts +17 -12
- package/packages/core/src/agent/llm-client.ts +266 -0
- package/packages/core/src/agent/llm-providers/anthropic.ts +264 -0
- package/packages/core/src/agent/llm-providers/deepseek.ts +8 -0
- package/packages/core/src/agent/{providers → llm-providers}/gemini.ts +98 -60
- package/packages/core/src/agent/llm-providers/groq.ts +5 -0
- package/packages/core/src/agent/llm-providers/hiveagents.ts +253 -0
- package/packages/core/src/agent/{providers → llm-providers}/interface.ts +73 -13
- package/packages/core/src/agent/llm-providers/kimi.ts +8 -0
- package/packages/core/src/agent/llm-providers/minimax.ts +13 -0
- package/packages/core/src/agent/llm-providers/mistral.ts +5 -0
- package/packages/core/src/agent/llm-providers/modelscope.ts +5 -0
- package/packages/core/src/agent/llm-providers/nvidia.ts +5 -0
- package/packages/core/src/agent/{providers → llm-providers}/ollama.ts +31 -5
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +418 -0
- package/packages/core/src/agent/llm-providers/openai.ts +5 -0
- package/packages/core/src/agent/llm-providers/opencode-go.ts +9 -0
- package/packages/core/src/agent/llm-providers/openrouter.ts +5 -0
- package/packages/core/src/agent/llm-providers/qwen.ts +5 -0
- package/packages/core/src/agent/llm-providers/z-ai.ts +5 -0
- package/packages/core/src/agent/minimal-loadout.ts +47 -0
- package/packages/core/src/agent/playbook-selector.ts +119 -0
- package/packages/core/src/agent/{PromptBuilder.ts → prompt-builder.ts} +21 -22
- package/packages/core/src/{harness → agent}/proof-packet.ts +16 -21
- package/packages/core/src/agent/providers/index.ts +35 -16
- package/packages/core/src/agent/reflector.ts +320 -0
- package/packages/core/src/agent/routing-intent.ts +22 -0
- package/packages/core/src/{harness → agent}/run-epoch.ts +4 -3
- package/packages/core/src/{harness → agent}/run-store.ts +142 -81
- package/packages/core/src/agent/{Service.ts → service.ts} +37 -26
- package/packages/core/src/agent/skill-selector.ts +374 -0
- package/packages/core/src/agent/stuck-loop.ts +209 -0
- package/packages/core/src/agent/{selectors/ToolSelector.ts → tool-selector.ts} +188 -178
- package/packages/core/src/{ace/Tracer.ts → agent/tracer.ts} +37 -27
- package/packages/core/src/api/createAgent.test.ts +139 -27
- package/packages/core/src/api/createAgent.ts +232 -44
- package/packages/core/src/artifacts/store.ts +162 -0
- package/packages/core/src/canvas/canvas-manager.ts +161 -0
- package/packages/core/src/canvas/canvas.test.ts +8 -4
- package/packages/core/src/canvas/emitter.ts +131 -80
- package/packages/core/src/canvas/index.ts +1 -3
- package/packages/core/src/channels/base.ts +9 -1
- package/packages/core/src/channels/discord.ts +5 -4
- package/packages/core/src/channels/manager.ts +122 -30
- package/packages/core/src/channels/slack.ts +5 -4
- package/packages/core/src/channels/telegram.ts +36 -6
- package/packages/core/src/channels/webchat.ts +11 -10
- package/packages/core/src/channels/whatsapp.ts +23 -7
- package/packages/core/src/config/index.ts +13 -2
- package/packages/core/src/config/loader.ts +76 -29
- package/packages/core/src/ethics/EthicsGuard.test.ts +90 -36
- package/packages/core/src/ethics/EthicsGuard.ts +51 -47
- package/packages/core/src/events/agent-bus.ts +44 -68
- package/packages/core/src/events/channel-narration.ts +150 -0
- package/packages/core/src/events/narration.ts +82 -0
- package/packages/core/src/events/tool-narration.ts +62 -0
- package/packages/core/src/gateway/delegation-groups.ts +258 -0
- package/packages/core/src/{harness → gateway}/durable-queue.ts +102 -42
- package/packages/core/src/{harness → gateway}/job-store.ts +85 -48
- package/packages/core/src/gateway/lane-queue.ts +173 -0
- package/packages/core/src/gateway/notification-inbox.ts +57 -0
- package/packages/core/src/gateway/server.ts +1 -1
- package/packages/core/src/harness/index.ts +46 -27
- package/packages/core/src/index.ts +33 -27
- package/packages/core/src/mcp/hot-reload.ts +32 -23
- package/packages/core/src/mcp/index.ts +6 -3
- package/packages/core/src/mcp/singleton.ts +1 -4
- package/packages/core/src/mcp/tool-sync.ts +138 -0
- package/packages/core/src/memory/Scratchpad.test.ts +39 -20
- package/packages/core/src/memory/Scratchpad.ts +27 -34
- package/packages/core/src/multimodal/vision-service.ts +44 -38
- package/packages/core/src/resilience/retry.ts +95 -0
- package/packages/core/src/scheduler/CronScheduler.ts +334 -287
- package/packages/core/src/scheduler/index.ts +9 -7
- package/packages/core/src/scheduler/integration.ts +46 -26
- package/packages/core/src/scheduler/scheduler.test.ts +9 -13
- package/packages/core/src/scheduler/types.ts +7 -2
- package/packages/core/src/security/Pairing.ts +1 -1
- package/packages/core/src/skills/bundled/a2ui/a2ui_dashboard/SKILL.md +176 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_form/SKILL.md +202 -0
- package/packages/core/src/skills/bundled/a2ui/a2ui_interactive/SKILL.md +206 -0
- package/packages/core/src/skills/bundled/agents/agent_spawner/SKILL.md +173 -0
- package/packages/core/src/skills/bundled/agents/memory_manager/SKILL.md +143 -0
- package/packages/core/src/skills/bundled/agents/research_and_remember/SKILL.md +139 -0
- package/packages/core/src/skills/bundled/agents/task_orchestrator/SKILL.md +98 -0
- package/packages/core/src/skills/bundled/api/api_client/SKILL.md +132 -0
- package/packages/core/src/skills/bundled/cli/cli_pipeline/SKILL.md +135 -0
- package/packages/core/src/skills/bundled/cli/cli_safe_exec/SKILL.md +125 -0
- package/packages/core/src/skills/bundled/cli/software_engineering/SKILL.md +23 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +188 -0
- package/packages/core/src/skills/bundled/cron_reminder/SKILL.md +112 -0
- package/packages/core/src/skills/bundled/filesystem/file_manager/SKILL.md +118 -0
- package/packages/core/src/skills/bundled/filesystem/file_read_and_summarize/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/filesystem/file_writer/SKILL.md +129 -0
- package/packages/core/src/skills/bundled/filesystem/workspace_file_operator/SKILL.md +22 -0
- package/packages/core/src/skills/bundled/office/office_document_manager/SKILL.md +262 -0
- package/packages/core/src/skills/bundled/search_knowledge/capability_discovery/SKILL.md +75 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +109 -0
- package/packages/core/src/skills/bundled/web/web_monitor/SKILL.md +127 -0
- package/packages/core/src/skills/bundled/web/web_research/SKILL.md +119 -0
- package/packages/core/src/skills/bundled-data.generated.ts +731 -2678
- package/packages/core/src/skills/skills.test.ts +52 -11
- package/packages/core/src/{harness → storage}/boot-id.ts +5 -2
- package/packages/core/src/storage/bootstrap.ts +151 -0
- package/packages/core/src/storage/causal-events.ts +84 -0
- package/packages/core/src/storage/collections.ts +680 -0
- package/packages/core/src/storage/crypto.ts +205 -74
- package/packages/core/src/{harness/db-helpers.ts → storage/hive.ts} +63 -7
- package/packages/core/src/storage/hivedb.ts +61 -0
- package/packages/core/src/storage/index.ts +111 -18
- package/packages/core/src/storage/model-id.ts +53 -0
- package/packages/core/src/storage/onboarding.ts +540 -972
- package/packages/core/src/storage/reconcile.ts +238 -0
- package/packages/core/src/storage/seed.ts +572 -406
- package/packages/core/src/storage/usage.ts +285 -225
- package/packages/core/src/storage/user-email.ts +11 -0
- package/packages/core/src/swarm/AgentExecutor.ts +1 -1
- package/packages/core/src/swarm/EventBridge.ts +1 -1
- package/packages/core/src/swarm/index.ts +12 -9
- package/packages/core/src/tool-runtime/index.ts +146 -23
- package/packages/core/src/tool-runtime/tool-worker.ts +2 -2
- package/packages/core/src/tool-runtime/worker-tools.ts +27 -0
- package/packages/core/src/{canvas/a2ui-tools.ts → tools/a2ui/index.ts} +17 -8
- package/packages/core/src/tools/agents/get-available-models.ts +36 -54
- package/packages/core/src/tools/agents/index.ts +784 -292
- package/packages/core/src/tools/api/api-request.test.ts +164 -0
- package/packages/core/src/tools/api/api-request.ts +174 -0
- package/packages/core/src/tools/api/index.ts +16 -0
- package/packages/core/src/tools/cli/index.ts +4 -0
- package/packages/core/src/tools/core/index.ts +281 -112
- package/packages/core/src/tools/cron/index.ts +121 -124
- package/packages/core/src/tools/index.ts +63 -78
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +3 -1
- package/packages/core/src/tools/types.ts +3 -1
- package/packages/core/src/tools/web/artifact-inspect.ts +23 -0
- package/packages/core/src/tools/web/browser-backend.ts +129 -0
- package/packages/core/src/tools/web/browser-screenshot.ts +26 -5
- package/packages/core/src/tools/web/browser-service.ts +80 -35
- package/packages/core/src/tools/web/browser-type.ts +3 -8
- package/packages/core/src/tools/web/index.ts +4 -4
- package/packages/core/src/tools/web/webview-backend.ts +412 -0
- package/packages/core/src/voice/index.ts +89 -63
- package/packages/core/src/workers/agent.worker.ts +2 -2
- package/packages/core/src/workers/workers.test.ts +3 -10
- package/scripts/bump-version.ts +248 -0
- package/scripts/generate-skill-bundle.ts +108 -0
- package/test/acceptance-checks.test.ts +403 -0
- package/test/agent-loop-terminal-synthesis.test.ts +32 -0
- package/test/browser-backend.test.ts +308 -0
- package/test/catalog-agents-stay-enabled.test.ts +117 -0
- package/test/causal-events.test.ts +117 -0
- package/test/compaction.test.ts +105 -0
- package/test/context-compiler.test.ts +269 -0
- package/test/curator.test.ts +130 -0
- package/test/durable-queue.test.ts +114 -0
- package/test/harness-barrel.test.ts +64 -0
- package/test/hive-helpers.test.ts +130 -0
- package/test/hivedb-search.test.ts +189 -0
- package/test/internal-turns.test.ts +166 -0
- package/test/job-idempotency.test.ts +68 -0
- package/test/job-retry-backoff.test.ts +184 -0
- package/test/job-store.test.ts +381 -0
- package/test/llm-retry.test.ts +97 -0
- package/test/memory-perf.test.ts +774 -0
- package/test/minimal-loadout.test.ts +78 -0
- package/test/model-catalog.test.ts +105 -0
- package/test/preload.ts +12 -0
- package/test/reflector.test.ts +320 -0
- package/test/retention-cap.test.ts +91 -0
- package/test/retired-capabilities-pruned.test.ts +192 -0
- package/test/run-store.test.ts +355 -0
- package/test/scratchpad.test.ts +74 -0
- package/test/secrets-durability.test.ts +119 -0
- package/test/seed-model-reseed.test.ts +155 -0
- package/test/setup-agent-seed.test.ts +264 -0
- package/test/tool-inventory.test.ts +65 -0
- package/test/tool-runtime.test.ts +258 -0
- package/test/tool-selector-runtime-tools.test.ts +117 -0
- package/test/toon.test.ts +429 -0
- package/tsconfig.json +2 -0
- package/packages/core/src/ace/Curator.ts +0 -158
- package/packages/core/src/ace/Reflector.ts +0 -200
- package/packages/core/src/ace/index.ts +0 -4
- package/packages/core/src/agent/AgentRunner.ts +0 -711
- package/packages/core/src/agent/ContextCompiler.ts +0 -567
- package/packages/core/src/agent/ContextGuard.ts +0 -91
- package/packages/core/src/agent/ConversationStore.ts +0 -254
- package/packages/core/src/agent/Hooks.ts +0 -166
- package/packages/core/src/agent/StuckLoop.ts +0 -133
- package/packages/core/src/agent/providers/LLMClient.ts +0 -149
- package/packages/core/src/agent/providers/anthropic.ts +0 -212
- package/packages/core/src/agent/providers/openai-compat.ts +0 -231
- package/packages/core/src/agent/selectors/PlaybookSelector.ts +0 -121
- package/packages/core/src/agent/selectors/SkillSelector.ts +0 -322
- package/packages/core/src/agent/selectors/index.ts +0 -6
- package/packages/core/src/auth/auth.ts +0 -121
- package/packages/core/src/auth/index.ts +0 -1
- package/packages/core/src/canvas/CanvasManager.ts +0 -390
- package/packages/core/src/canvas/canvas-tools.ts +0 -448
- package/packages/core/src/harness/collections.ts +0 -98
- package/packages/core/src/harness/goal-verifier.ts +0 -141
- package/packages/core/src/harness/harness.test.ts +0 -236
- package/packages/core/src/harness/reconcile.ts +0 -149
- package/packages/core/src/mcp/MCPToolAdapter.ts +0 -176
- package/packages/core/src/multimodal/VisionService.ts +0 -293
- package/packages/core/src/scheduler/dag/AgentExecutor.ts +0 -53
- package/packages/core/src/scheduler/dag/DAGScheduler.ts +0 -250
- package/packages/core/src/scheduler/dag/EventBridge.ts +0 -122
- package/packages/core/src/scheduler/dag/TaskGraph.ts +0 -192
- package/packages/core/src/scheduler/dag/TaskNode.ts +0 -97
- package/packages/core/src/scheduler/dag/TaskResult.ts +0 -22
- package/packages/core/src/scheduler/dag/errors.ts +0 -37
- package/packages/core/src/scheduler/dag/index.ts +0 -26
- package/packages/core/src/scheduler/dag/presets/ResearchPreset.ts +0 -97
- package/packages/core/src/scheduler/dag/strategies/ParallelStrategy.ts +0 -21
- package/packages/core/src/scheduler/dag/strategies/PriorityStrategy.ts +0 -46
- package/packages/core/src/storage/HiveDBStorage.ts +0 -64
- package/packages/core/src/storage/SQLiteStorage.ts +0 -414
- package/packages/core/src/storage/hiveSeed.ts +0 -308
- package/packages/core/src/storage/hiveStorage.test.ts +0 -38
- package/packages/core/src/storage/schema.ts +0 -689
- package/packages/core/src/storage/storage.test.ts +0 -37
- package/packages/core/src/swarm/AgentBus.ts +0 -460
- package/packages/core/src/swarm/EventBus.ts +0 -169
- package/packages/core/src/swarm/WorkerPool.ts +0 -236
- package/packages/core/src/tools/bridge-events.ts +0 -26
- package/packages/core/src/tools/canvas/index.ts +0 -375
- package/packages/core/src/tools/codebridge/index.ts +0 -342
- package/packages/core/src/tools/meeting/index.ts +0 -353
- package/packages/core/src/tools/projects/index.ts +0 -37
- package/packages/core/src/tools/projects/project-create.ts +0 -94
- package/packages/core/src/tools/projects/project-done.ts +0 -66
- package/packages/core/src/tools/projects/project-fail.ts +0 -66
- package/packages/core/src/tools/projects/project-list.ts +0 -96
- package/packages/core/src/tools/projects/project-update.ts +0 -72
- package/packages/core/src/tools/projects/task-create.ts +0 -68
- package/packages/core/src/tools/projects/task-evaluate.ts +0 -93
- package/packages/core/src/tools/projects/task-update.ts +0 -93
- package/packages/core/src/tools/voice/index.ts +0 -104
- package/packages/core/src/tools/web/api-request.test.ts +0 -170
- package/packages/core/src/tools/web/api-request.ts +0 -239
- package/test/setup-db.ts +0 -216
- /package/packages/core/src/agent/{NativeTools.ts → native-tools.ts} +0 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebViewBackend — `BrowserBackend` sobre `Bun.WebView` (Bun >= 1.3).
|
|
3
|
+
*
|
|
4
|
+
* Corre in-process: no hay subproceso, ni instalación de ~75 MB, ni descarga de
|
|
5
|
+
* Chrome. Un `evaluate` cuesta ~0.25 ms contra los ~68 ms de piso que tiene cada
|
|
6
|
+
* invocación del CLI de agent-browser. A cambio necesita entorno gráfico, así
|
|
7
|
+
* que no reemplaza a agent-browser en Docker ni en un servidor headless.
|
|
8
|
+
*
|
|
9
|
+
* Dos restricciones del motor mandan sobre el diseño de este archivo:
|
|
10
|
+
*
|
|
11
|
+
* 1. `Bun.WebView` acepta **una sola operación pendiente por vez**; dos
|
|
12
|
+
* llamadas solapadas fallan con `ERR_INVALID_STATE: a simple operation is
|
|
13
|
+
* already pending`. Todo pasa por una cola serializada.
|
|
14
|
+
* 2. No expone árbol de accesibilidad. `snapshot()` se sintetiza recorriendo
|
|
15
|
+
* el DOM, imitando el formato que emite agent-browser para que el modelo
|
|
16
|
+
* vea lo mismo con cualquiera de los dos backends.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { logger } from "../../utils/logger.ts";
|
|
20
|
+
import { resolveWebViewEngine, type BrowserBackend, type ScreenshotOptions, type SnapshotOptions, type WebViewEngine } from "./browser-backend.ts";
|
|
21
|
+
|
|
22
|
+
const log = logger.child("webview-backend");
|
|
23
|
+
|
|
24
|
+
/** Tope del texto del snapshot: un DOM grande no puede comerse el contexto. */
|
|
25
|
+
const SNAPSHOT_CHAR_LIMIT = 20_000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Forma real de `Bun.WebView` en 1.3.14, verificada contra el prototipo.
|
|
29
|
+
*
|
|
30
|
+
* No se usan los tipos de `bun-types` a propósito: declaran `back()`/`forward()`
|
|
31
|
+
* y el runtime expone `goBack()`/`goForward()`. Contra los tipos, la navegación
|
|
32
|
+
* hacia atrás compila y explota en ejecución.
|
|
33
|
+
*/
|
|
34
|
+
interface BunWebView {
|
|
35
|
+
navigate(url: string): Promise<void>;
|
|
36
|
+
evaluate(script: string): Promise<unknown>;
|
|
37
|
+
screenshot(): Promise<Blob>;
|
|
38
|
+
cdp(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
39
|
+
click(selector: string): Promise<void>;
|
|
40
|
+
type(text: string): Promise<void>;
|
|
41
|
+
press(key: string, modifiers?: Record<string, boolean>): Promise<void>;
|
|
42
|
+
scroll(dx: number, dy: number): Promise<void>;
|
|
43
|
+
scrollTo(selector: string): Promise<void>;
|
|
44
|
+
resize(width: number, height: number): Promise<void>;
|
|
45
|
+
goBack(): Promise<void>;
|
|
46
|
+
goForward(): Promise<void>;
|
|
47
|
+
reload(): Promise<void>;
|
|
48
|
+
close(): void;
|
|
49
|
+
readonly url: string;
|
|
50
|
+
readonly title: string;
|
|
51
|
+
readonly loading: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* El script que sintetiza el árbol de accesibilidad. Se inyecta como texto, así
|
|
56
|
+
* que no puede cerrar sobre nada del scope de TypeScript: los parámetros entran
|
|
57
|
+
* interpolados como literales JSON.
|
|
58
|
+
*/
|
|
59
|
+
function buildSnapshotScript(options: Required<SnapshotOptions>): string {
|
|
60
|
+
return `(() => {
|
|
61
|
+
const MAX_DEPTH = ${JSON.stringify(options.depth)};
|
|
62
|
+
const COMPACT = ${JSON.stringify(options.compact)};
|
|
63
|
+
const INTERACTIVE_ONLY = ${JSON.stringify(options.interactiveOnly)};
|
|
64
|
+
const LIMIT = ${SNAPSHOT_CHAR_LIMIT};
|
|
65
|
+
|
|
66
|
+
const ROLE_BY_TAG = {
|
|
67
|
+
A: "link", BUTTON: "button", P: "paragraph", IMG: "img", TEXTAREA: "textbox",
|
|
68
|
+
SELECT: "combobox", OPTION: "option", UL: "list", OL: "list", LI: "listitem",
|
|
69
|
+
TABLE: "table", TR: "row", TD: "cell", TH: "columnheader", FORM: "form",
|
|
70
|
+
NAV: "navigation", MAIN: "main", HEADER: "banner", FOOTER: "contentinfo",
|
|
71
|
+
ASIDE: "complementary", LABEL: "label", ARTICLE: "article", SECTION: "region",
|
|
72
|
+
DIALOG: "dialog", SUMMARY: "button", IFRAME: "iframe", VIDEO: "video", AUDIO: "audio",
|
|
73
|
+
};
|
|
74
|
+
const INTERACTIVE = new Set(["link", "button", "textbox", "checkbox", "radio", "combobox", "option", "searchbox"]);
|
|
75
|
+
const SKIP_TAGS = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "HEAD", "META", "LINK", "TITLE", "SVG", "PATH"]);
|
|
76
|
+
|
|
77
|
+
function inputRole(el) {
|
|
78
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
79
|
+
if (type === "checkbox") return "checkbox";
|
|
80
|
+
if (type === "radio") return "radio";
|
|
81
|
+
if (type === "search") return "searchbox";
|
|
82
|
+
if (type === "button" || type === "submit" || type === "reset" || type === "image") return "button";
|
|
83
|
+
if (type === "hidden") return null;
|
|
84
|
+
return "textbox";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function roleOf(el) {
|
|
88
|
+
const explicit = el.getAttribute("role");
|
|
89
|
+
if (explicit) return explicit.trim().split(/\\s+/)[0];
|
|
90
|
+
if (el.tagName === "INPUT") return inputRole(el);
|
|
91
|
+
if (/^H[1-6]$/.test(el.tagName)) return "heading";
|
|
92
|
+
if (el.tagName === "A") return el.hasAttribute("href") ? "link" : null;
|
|
93
|
+
return ROLE_BY_TAG[el.tagName] || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ownText(el) {
|
|
97
|
+
let text = "";
|
|
98
|
+
for (const node of el.childNodes) {
|
|
99
|
+
if (node.nodeType === 3) text += node.nodeValue;
|
|
100
|
+
// Los inline sin rol propio son parte del nombre del padre, no nodos aparte.
|
|
101
|
+
else if (node.nodeType === 1 && !roleOf(node) && node.childElementCount === 0) {
|
|
102
|
+
text += node.textContent || "";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return text.replace(/\\s+/g, " ").trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function nameOf(el) {
|
|
109
|
+
const aria = el.getAttribute("aria-label");
|
|
110
|
+
if (aria && aria.trim()) return aria.trim();
|
|
111
|
+
|
|
112
|
+
const labelledBy = el.getAttribute("aria-labelledby");
|
|
113
|
+
if (labelledBy) {
|
|
114
|
+
const parts = labelledBy.split(/\\s+/)
|
|
115
|
+
.map((id) => { const target = document.getElementById(id); return target ? (target.textContent || "").trim() : ""; })
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
if (parts.length) return parts.join(" ").replace(/\\s+/g, " ");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (el.tagName === "IMG") return (el.getAttribute("alt") || "").trim();
|
|
121
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
122
|
+
const type = (el.getAttribute("type") || "").toLowerCase();
|
|
123
|
+
if (type === "button" || type === "submit" || type === "reset") return (el.value || "").trim();
|
|
124
|
+
const placeholder = (el.getAttribute("placeholder") || "").trim();
|
|
125
|
+
if (placeholder) return placeholder;
|
|
126
|
+
if (el.labels && el.labels.length) return (el.labels[0].textContent || "").replace(/\\s+/g, " ").trim();
|
|
127
|
+
return (el.getAttribute("name") || "").trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const own = ownText(el);
|
|
131
|
+
if (own) return own;
|
|
132
|
+
return (el.getAttribute("title") || "").trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function visible(el) {
|
|
136
|
+
if (el.hasAttribute("hidden") || el.getAttribute("aria-hidden") === "true") return false;
|
|
137
|
+
const style = getComputedStyle(el);
|
|
138
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function attrsOf(el, role) {
|
|
143
|
+
const attrs = [];
|
|
144
|
+
if (role === "heading") attrs.push("level=" + el.tagName.slice(1));
|
|
145
|
+
if (el.disabled) attrs.push("disabled");
|
|
146
|
+
if (el.checked) attrs.push("checked");
|
|
147
|
+
if (el.getAttribute("aria-expanded")) attrs.push("expanded=" + el.getAttribute("aria-expanded"));
|
|
148
|
+
return attrs;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const lines = [];
|
|
152
|
+
let refSeq = 0;
|
|
153
|
+
let truncated = false;
|
|
154
|
+
|
|
155
|
+
function walk(el, depth) {
|
|
156
|
+
if (truncated) return;
|
|
157
|
+
for (const child of el.children) {
|
|
158
|
+
if (truncated) return;
|
|
159
|
+
if (SKIP_TAGS.has(child.tagName)) continue;
|
|
160
|
+
if (!visible(child)) continue;
|
|
161
|
+
|
|
162
|
+
const role = roleOf(child);
|
|
163
|
+
const name = role ? nameOf(child) : "";
|
|
164
|
+
const interactive = role ? INTERACTIVE.has(role) : false;
|
|
165
|
+
// Un nodo se emite si aporta algo: un rol con nombre, o algo accionable.
|
|
166
|
+
let emit = Boolean(role) && (Boolean(name) || interactive);
|
|
167
|
+
if (INTERACTIVE_ONLY && !interactive) emit = false;
|
|
168
|
+
if (COMPACT && role && !name && !interactive) emit = false;
|
|
169
|
+
|
|
170
|
+
if (emit && depth < MAX_DEPTH) {
|
|
171
|
+
const attrs = attrsOf(child, role);
|
|
172
|
+
if (name || interactive) attrs.push("ref=e" + ++refSeq);
|
|
173
|
+
let label = "- " + role;
|
|
174
|
+
if (name) {
|
|
175
|
+
const shown = COMPACT && name.length > 120 ? name.slice(0, 120) + "…" : name;
|
|
176
|
+
label += ' "' + shown.replace(/"/g, "'") + '"';
|
|
177
|
+
}
|
|
178
|
+
if (attrs.length) label += " [" + attrs.join(", ") + "]";
|
|
179
|
+
const line = " ".repeat(depth) + label;
|
|
180
|
+
if (lines.join("\\n").length + line.length > LIMIT) { truncated = true; return; }
|
|
181
|
+
lines.push(line);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Sin línea propia, los hijos suben de nivel: así el árbol no se llena de
|
|
185
|
+
// sangría por cada <div> de maquetado.
|
|
186
|
+
walk(child, emit && depth < MAX_DEPTH ? depth + 1 : depth);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
walk(document.body || document.documentElement, 0);
|
|
191
|
+
if (truncated) lines.push("… (snapshot truncado)");
|
|
192
|
+
return lines.join("\\n");
|
|
193
|
+
})()`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export class WebViewBackend implements BrowserBackend {
|
|
197
|
+
private view: BunWebView | null = null;
|
|
198
|
+
private _url = "";
|
|
199
|
+
/** Cola de una sola vía: WebView rechaza operaciones solapadas. */
|
|
200
|
+
private queue: Promise<unknown> = Promise.resolve();
|
|
201
|
+
|
|
202
|
+
constructor(
|
|
203
|
+
private readonly options: {
|
|
204
|
+
width?: number;
|
|
205
|
+
height?: number;
|
|
206
|
+
show?: boolean;
|
|
207
|
+
engine?: WebViewEngine;
|
|
208
|
+
} = {},
|
|
209
|
+
) {}
|
|
210
|
+
|
|
211
|
+
private ensureView(): BunWebView {
|
|
212
|
+
if (this.view) return this.view;
|
|
213
|
+
|
|
214
|
+
const WebView = (globalThis as { Bun?: { WebView?: unknown } }).Bun?.WebView;
|
|
215
|
+
if (typeof WebView !== "function") {
|
|
216
|
+
throw new Error("Bun.WebView no está disponible en este runtime (requiere Bun >= 1.3)");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const engine = this.options.engine ?? resolveWebViewEngine();
|
|
220
|
+
if (!engine) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
"Bun.WebView no tiene motor utilizable: WebKit sólo existe en macOS y no se encontró Chrome. " +
|
|
223
|
+
"Instalá Chrome o definí BUN_CHROME_PATH.",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// `url: false` es obligatorio para automatización desatendida: sin eso el
|
|
228
|
+
// motor chrome intenta CONECTARSE a un Chrome que ya esté corriendo, y esa
|
|
229
|
+
// ruta abre un diálogo "Allow remote debugging?" que cuelga el proceso
|
|
230
|
+
// esperando un click que en un servidor no llega nunca.
|
|
231
|
+
const backend =
|
|
232
|
+
engine === "chrome"
|
|
233
|
+
? { type: "chrome" as const, url: false as const, stderr: "ignore" as const }
|
|
234
|
+
: ("webkit" as const);
|
|
235
|
+
|
|
236
|
+
// Sin `url` inicial a propósito: construir con uno deja una navegación
|
|
237
|
+
// pendiente y el primer navigate() explota con ERR_INVALID_STATE.
|
|
238
|
+
const Ctor = WebView as unknown as new (opts: unknown) => BunWebView;
|
|
239
|
+
this.view = new Ctor({
|
|
240
|
+
backend,
|
|
241
|
+
show: this.options.show ?? false,
|
|
242
|
+
width: this.options.width ?? 1280,
|
|
243
|
+
height: this.options.height ?? 800,
|
|
244
|
+
});
|
|
245
|
+
log.info(`✅ WebView abierto (motor: ${engine}, in-process)`);
|
|
246
|
+
return this.view;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Serializa: cada operación espera a que termine la anterior. */
|
|
250
|
+
private run<T>(operation: (view: BunWebView) => Promise<T>): Promise<T> {
|
|
251
|
+
const next = this.queue.then(
|
|
252
|
+
() => operation(this.ensureView()),
|
|
253
|
+
() => operation(this.ensureView()),
|
|
254
|
+
);
|
|
255
|
+
// La cola no debe romperse porque una operación haya fallado.
|
|
256
|
+
this.queue = next.catch(() => undefined);
|
|
257
|
+
return next;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
get url(): string {
|
|
261
|
+
return this.view?.url || this._url;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
get title(): string {
|
|
265
|
+
return this.view?.title || "";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
get loading(): boolean {
|
|
269
|
+
return this.view?.loading ?? false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async navigate(url: string): Promise<void> {
|
|
273
|
+
const target = /^[a-z]+:/i.test(url) ? url : `https://${url}`;
|
|
274
|
+
await this.run((view) => view.navigate(target));
|
|
275
|
+
this._url = this.view?.url || target;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async evaluate<T = unknown>(script: string): Promise<T> {
|
|
279
|
+
const trimmed = script.trim();
|
|
280
|
+
let wrapped = script;
|
|
281
|
+
if (/\bawait\b/.test(script) && !trimmed.startsWith("(async") && !trimmed.startsWith("async function")) {
|
|
282
|
+
wrapped = trimmed.startsWith("return")
|
|
283
|
+
? `(async () => { ${script} })()`
|
|
284
|
+
: `(async () => { return ${script}; })()`;
|
|
285
|
+
}
|
|
286
|
+
return (await this.run((view) => view.evaluate(wrapped))) as T;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async screenshot(_options?: ScreenshotOptions): Promise<string> {
|
|
290
|
+
const blob = await this.run((view) => view.screenshot());
|
|
291
|
+
return Buffer.from(await blob.arrayBuffer()).toString("base64");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async screenshotElement(selector: string): Promise<string> {
|
|
295
|
+
const box = await this.evaluate<{ x: number; y: number; width: number; height: number } | null>(
|
|
296
|
+
`(() => {
|
|
297
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
298
|
+
if (!el) return null;
|
|
299
|
+
el.scrollIntoView({ block: "center", inline: "center" });
|
|
300
|
+
const r = el.getBoundingClientRect();
|
|
301
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
302
|
+
})()`,
|
|
303
|
+
);
|
|
304
|
+
if (!box || box.width <= 0 || box.height <= 0) {
|
|
305
|
+
throw new Error(`screenshot failed: elemento no visible o inexistente: ${selector}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// WebView.screenshot() no recorta, pero el puente CDP sí acepta `clip`.
|
|
309
|
+
const shot = await this.run((view) =>
|
|
310
|
+
view.cdp("Page.captureScreenshot", {
|
|
311
|
+
format: "png",
|
|
312
|
+
clip: { x: box.x, y: box.y, width: box.width, height: box.height, scale: 1 },
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
const data = (shot as { data?: string })?.data;
|
|
316
|
+
if (!data) throw new Error(`screenshot failed: CDP no devolvió imagen para ${selector}`);
|
|
317
|
+
return data;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async snapshot(options?: SnapshotOptions): Promise<string> {
|
|
321
|
+
const script = buildSnapshotScript({
|
|
322
|
+
compact: options?.compact !== false,
|
|
323
|
+
depth: options?.depth ?? 12,
|
|
324
|
+
interactiveOnly: options?.interactiveOnly ?? false,
|
|
325
|
+
});
|
|
326
|
+
return (await this.evaluate<string>(script)) || "";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async click(selector: string, _options?: Record<string, unknown>): Promise<void> {
|
|
330
|
+
await this.run((view) => view.click(selector));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async type(text: string): Promise<void> {
|
|
334
|
+
await this.run((view) => view.type(text));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async typeIn(selector: string, text: string): Promise<void> {
|
|
338
|
+
const focused = await this.evaluate<boolean>(
|
|
339
|
+
`(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return false; el.focus(); return true; })()`,
|
|
340
|
+
);
|
|
341
|
+
if (!focused) throw new Error(`type failed: elemento no encontrado: ${selector}`);
|
|
342
|
+
await this.run((view) => view.type(text));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async fill(selector: string, text: string): Promise<void> {
|
|
346
|
+
// `fill` reemplaza; `type` agrega. Se limpia primero y se disparan los
|
|
347
|
+
// eventos que esperan React y compañía para registrar el cambio.
|
|
348
|
+
const ok = await this.evaluate<boolean>(
|
|
349
|
+
`(() => {
|
|
350
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
351
|
+
if (!el) return false;
|
|
352
|
+
el.focus();
|
|
353
|
+
el.value = "";
|
|
354
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
355
|
+
return true;
|
|
356
|
+
})()`,
|
|
357
|
+
);
|
|
358
|
+
if (!ok) throw new Error(`fill failed: elemento no encontrado: ${selector}`);
|
|
359
|
+
await this.run((view) => view.type(text));
|
|
360
|
+
await this.evaluate(
|
|
361
|
+
`(() => {
|
|
362
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
363
|
+
if (el) el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
364
|
+
})()`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async press(key: string, options?: { modifiers?: string[] }): Promise<void> {
|
|
369
|
+
const modifiers: Record<string, boolean> = {};
|
|
370
|
+
for (const modifier of options?.modifiers ?? []) {
|
|
371
|
+
const normalized = modifier.toLowerCase();
|
|
372
|
+
if (normalized === "control" || normalized === "ctrl") modifiers.ctrl = true;
|
|
373
|
+
else if (normalized === "shift") modifiers.shift = true;
|
|
374
|
+
else if (normalized === "alt") modifiers.alt = true;
|
|
375
|
+
else if (normalized === "meta" || normalized === "cmd") modifiers.meta = true;
|
|
376
|
+
}
|
|
377
|
+
await this.run((view) => view.press(key, modifiers));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async scroll(dx: number, dy: number): Promise<void> {
|
|
381
|
+
await this.run((view) => view.scroll(dx, dy));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async scrollTo(selector: string, _options?: { behavior?: "smooth" | "instant" }): Promise<void> {
|
|
385
|
+
await this.run((view) => view.scrollTo(selector));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async back(): Promise<void> {
|
|
389
|
+
await this.run((view) => view.goBack());
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async forward(): Promise<void> {
|
|
393
|
+
await this.run((view) => view.goForward());
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async reload(): Promise<void> {
|
|
397
|
+
await this.run((view) => view.reload());
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async resize(width: number, height: number): Promise<void> {
|
|
401
|
+
await this.run((view) => view.resize(width, height));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
close(): void {
|
|
405
|
+
try {
|
|
406
|
+
this.view?.close();
|
|
407
|
+
} catch {
|
|
408
|
+
/* ya cerrado */
|
|
409
|
+
}
|
|
410
|
+
this.view = null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { col } from "../storage/hive";
|
|
2
|
+
import type { ChannelDoc, ModelDoc } from "../storage/collections";
|
|
2
3
|
import { loadProviderApiKey } from "../storage/crypto";
|
|
3
4
|
import { logger } from "../utils/logger";
|
|
4
5
|
|
|
@@ -79,20 +80,11 @@ class VoiceService {
|
|
|
79
80
|
return VoiceService.instance;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
getChannelVoiceConfig(channelId: string): VoiceConfig {
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`).get(channelId) as {
|
|
88
|
-
voice_enabled: number;
|
|
89
|
-
tts_enabled: number;
|
|
90
|
-
stt_provider: string | null;
|
|
91
|
-
tts_provider: string | null;
|
|
92
|
-
tts_voice_id: string | null;
|
|
93
|
-
} | undefined;
|
|
94
|
-
|
|
95
|
-
if (!result) {
|
|
83
|
+
async getChannelVoiceConfig(channelId: string): Promise<VoiceConfig> {
|
|
84
|
+
const channelsCol = await col<ChannelDoc>("channels");
|
|
85
|
+
const entry = await channelsCol.get(channelId);
|
|
86
|
+
|
|
87
|
+
if (!entry) {
|
|
96
88
|
return {
|
|
97
89
|
voiceEnabled: false,
|
|
98
90
|
ttsEnabled: false,
|
|
@@ -103,26 +95,62 @@ class VoiceService {
|
|
|
103
95
|
}
|
|
104
96
|
|
|
105
97
|
return {
|
|
106
|
-
voiceEnabled:
|
|
107
|
-
ttsEnabled:
|
|
108
|
-
sttProvider:
|
|
109
|
-
ttsProvider:
|
|
110
|
-
ttsVoiceId:
|
|
98
|
+
voiceEnabled: entry.doc.voice_enabled,
|
|
99
|
+
ttsEnabled: entry.doc.tts_enabled,
|
|
100
|
+
sttProvider: entry.doc.stt_provider,
|
|
101
|
+
ttsProvider: entry.doc.tts_provider,
|
|
102
|
+
ttsVoiceId: entry.doc.tts_voice_id,
|
|
111
103
|
};
|
|
112
104
|
}
|
|
113
105
|
|
|
106
|
+
/** Provider de un modelo según la BD; acepta también un id de provider (canales antiguos). */
|
|
107
|
+
private async getModelProvider(modelId: string): Promise<string | null> {
|
|
108
|
+
try {
|
|
109
|
+
const modelsCol = await col<ModelDoc>("models");
|
|
110
|
+
const model = await modelsCol.get(modelId);
|
|
111
|
+
if (model?.doc.provider_id) return model.doc.provider_id;
|
|
112
|
+
const providersCol = await col<import("../storage/collections").ProviderDoc>("providers");
|
|
113
|
+
const provider = await providersCol.get(modelId);
|
|
114
|
+
return provider?.doc.id || null;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Primer modelo STT/TTS activo de un provider activo, como fallback desde la BD. */
|
|
121
|
+
private async getFirstActiveVoiceModel(type: "stt" | "tts"): Promise<{ id: string; provider: string } | null> {
|
|
122
|
+
try {
|
|
123
|
+
const modelsCol = await col<ModelDoc>("models");
|
|
124
|
+
const providersCol = await col<import("../storage/collections").ProviderDoc>("providers");
|
|
125
|
+
const models = (await modelsCol.findBy("model_type", type)).filter(e => e.doc.active);
|
|
126
|
+
for (const m of models) {
|
|
127
|
+
const provider = await providersCol.get(m.doc.provider_id);
|
|
128
|
+
if (provider?.doc.active) return { id: m.doc.id, provider: m.doc.provider_id };
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
114
136
|
async transcribe(audio: AudioInput, modelId: string): Promise<string> {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
137
|
+
let provider = await this.getModelProvider(modelId);
|
|
138
|
+
let resolvedModelId = modelId;
|
|
139
|
+
|
|
140
|
+
if (!provider) {
|
|
141
|
+
const fallback = await this.getFirstActiveVoiceModel("stt");
|
|
142
|
+
if (!fallback) throw new Error(`STT model "${modelId}" not found and no active STT models in the database`);
|
|
143
|
+
log.warn(`STT model ${modelId} not found in DB, falling back to ${fallback.provider}/${fallback.id}`);
|
|
144
|
+
provider = fallback.provider;
|
|
145
|
+
resolvedModelId = fallback.id;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
switch (provider) {
|
|
149
|
+
case "groq": return this.transcribeWithGroq(audio, resolvedModelId);
|
|
150
|
+
case "openai": return this.transcribeWithOpenAIWhisper(audio);
|
|
151
|
+
default:
|
|
152
|
+
throw new Error(`STT not supported for provider "${provider}" (model ${resolvedModelId})`);
|
|
122
153
|
}
|
|
123
|
-
|
|
124
|
-
log.warn(`Unknown STT provider ${modelId}, defaulting to Groq Whisper`);
|
|
125
|
-
return this.transcribeWithGroq(audio, "whisper-large-v3-turbo");
|
|
126
154
|
}
|
|
127
155
|
|
|
128
156
|
private async getProviderApiKey(providerId: string): Promise<string | null> {
|
|
@@ -229,26 +257,26 @@ class VoiceService {
|
|
|
229
257
|
}
|
|
230
258
|
|
|
231
259
|
async speak(text: string, modelId: string, voiceId?: string): Promise<AudioOutput> {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return this.
|
|
246
|
-
|
|
247
|
-
return this.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
260
|
+
let provider = modelId === "piper-local" ? "piper" : await this.getModelProvider(modelId);
|
|
261
|
+
let resolvedModelId = modelId;
|
|
262
|
+
|
|
263
|
+
if (!provider) {
|
|
264
|
+
const fallback = await this.getFirstActiveVoiceModel("tts");
|
|
265
|
+
if (!fallback) throw new Error(`TTS model "${modelId}" not found and no active TTS models in the database`);
|
|
266
|
+
log.warn(`TTS model ${modelId} not found in DB, falling back to ${fallback.provider}/${fallback.id}`);
|
|
267
|
+
provider = fallback.provider;
|
|
268
|
+
resolvedModelId = fallback.id;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
switch (provider) {
|
|
272
|
+
case "piper": return this.speakWithPiper(text, voiceId);
|
|
273
|
+
case "elevenlabs": return this.speakWithElevenLabs(text, resolvedModelId, voiceId);
|
|
274
|
+
case "openai": return this.speakWithOpenAI(text, resolvedModelId, voiceId);
|
|
275
|
+
case "gemini": return this.speakWithGemini(text, resolvedModelId, voiceId);
|
|
276
|
+
case "qwen": return this.speakWithQwen(text, resolvedModelId, voiceId);
|
|
277
|
+
default:
|
|
278
|
+
throw new Error(`TTS not supported for provider "${provider}" (model ${resolvedModelId})`);
|
|
279
|
+
}
|
|
252
280
|
}
|
|
253
281
|
|
|
254
282
|
private async speakWithPiper(text: string, voiceId?: string): Promise<AudioOutput> {
|
|
@@ -448,21 +476,19 @@ class VoiceService {
|
|
|
448
476
|
};
|
|
449
477
|
}
|
|
450
478
|
|
|
451
|
-
getConfiguredVoiceProviders(): { groq: boolean; elevenlabs: boolean; openai: boolean; gemini: boolean; qwen: boolean } {
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return !!row;
|
|
458
|
-
};
|
|
479
|
+
async getConfiguredVoiceProviders(): Promise<{ groq: boolean; elevenlabs: boolean; openai: boolean; gemini: boolean; qwen: boolean }> {
|
|
480
|
+
const hasDbKey = async (providerId: string): Promise<boolean> => !!(await loadProviderApiKey(providerId));
|
|
481
|
+
|
|
482
|
+
const [groq, elevenlabs, openai, gemini, qwen] = await Promise.all([
|
|
483
|
+
hasDbKey("groq"), hasDbKey("elevenlabs"), hasDbKey("openai"), hasDbKey("gemini"), hasDbKey("qwen"),
|
|
484
|
+
]);
|
|
459
485
|
|
|
460
486
|
return {
|
|
461
|
-
groq:
|
|
462
|
-
elevenlabs:
|
|
463
|
-
openai:
|
|
464
|
-
gemini:
|
|
465
|
-
qwen:
|
|
487
|
+
groq: groq || !!(process.env.GROQ_API_KEY),
|
|
488
|
+
elevenlabs: elevenlabs || !!(process.env.ELEVENLABS_API_KEY),
|
|
489
|
+
openai: openai || !!(process.env.OPENAI_API_KEY),
|
|
490
|
+
gemini: gemini || !!(process.env.GEMINI_API_KEY),
|
|
491
|
+
qwen: qwen || !!(process.env.DASHSCOPE_API_KEY),
|
|
466
492
|
};
|
|
467
493
|
}
|
|
468
494
|
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Sends: { type: "AGENT_RESULT", taskId, result } | { type: "AGENT_CHUNK", taskId, chunk }
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { runAgent } from "../agent/
|
|
9
|
-
import type { StreamChunk } from "../agent/
|
|
8
|
+
import { runAgent } from "../agent/agent-loop.ts";
|
|
9
|
+
import type { StreamChunk } from "../agent/agent-loop.ts";
|
|
10
10
|
|
|
11
11
|
declare var self: {
|
|
12
12
|
onmessage: ((event: { data: WorkerMessage }) => void) | null;
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
process.env.HIVE_DB_PATH = ":memory:";
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
2
4
|
import { createWorker, WorkerPool } from "./index.ts";
|
|
3
|
-
import { setupTestDb, teardownTestDb, insertTestAgent, insertTestProvider } from "../../../../test/setup-db.ts";
|
|
4
5
|
|
|
5
6
|
describe("createWorker", () => {
|
|
6
|
-
beforeAll(() => {
|
|
7
|
-
setupTestDb();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
afterAll(() => {
|
|
11
|
-
teardownTestDb();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
7
|
it("creates a worker instance with config", () => {
|
|
15
8
|
const worker = createWorker({
|
|
16
9
|
name: "test-worker",
|