@xopcai/xopc 0.0.278 → 0.0.283
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/dist/browser-ext/_locales/en/messages.json +34 -4
- package/dist/browser-ext/_locales/zh_CN/messages.json +34 -4
- package/dist/browser-ext/dist/assets/{auth-ClQTK565.js → auth-DB30ywek.js} +18 -53
- package/dist/browser-ext/dist/assets/pcm-capture-worklet-CGN9Mlhg.js +39 -0
- package/dist/browser-ext/dist/assets/{sidepanel-CeGqObOa.css → sidepanel-BbSQr-6P.css} +40 -6
- package/dist/browser-ext/dist/background.js +1 -1
- package/dist/browser-ext/dist/sidepanel.html +2 -2
- package/dist/browser-ext/dist/sidepanel.js +1219 -191
- package/dist/browser-ext/manifest.json +2 -1
- package/dist/extensions/feishu/src/adapters/cli-login.js +3 -3
- package/dist/extensions/feishu/src/outbound/media-load.js +1 -1
- package/dist/extensions/telegram/src/inbound-processor.js +2 -2
- package/dist/extensions/telegram/src/outbound-sender.js +1 -1
- package/dist/extensions/telegram/src/plugin.js +2 -2
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/extensions/weixin/src/api/api.js +2 -2
- package/dist/extensions/weixin/src/auth/accounts.js +12 -12
- package/dist/extensions/weixin/src/messaging/debug-mode.js +5 -5
- package/dist/extensions/weixin/src/messaging/inbound.js +11 -11
- package/dist/gateway/static/root/assets/{activity-store-SJZ5fS17.js → activity-store-iPtG94qr.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-lRVOwbrK.js → agent-avatar-dicebear-DQ74xqLi.js} +1 -1
- package/dist/gateway/static/root/assets/agent-defaults-settings-panel-SElFkQd6.js +1 -0
- package/dist/gateway/static/root/assets/{agents-DNQNTDkU.js → agents-DYazUaZ6.js} +1 -1
- package/dist/gateway/static/root/assets/{api-CnJAVHBu.js → api-KMJzMqvZ.js} +1 -1
- package/dist/gateway/static/root/assets/{app-management-settings-panel-oHTY5PDs.js → app-management-settings-panel-B42TpqIb.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-B0YnZ3ig.js → appearance-settings-D9Lfe7Sk.js} +1 -1
- package/dist/gateway/static/root/assets/apps-page-BzrodN8C.js +1 -0
- package/dist/gateway/static/root/assets/{archive-plugin-D4x142le.js → archive-plugin-BdmQwj4x.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-zibD2Hod.js → attachment-load-B4S0_nji.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-preview-renderer-CfEDFYPE.js → attachment-preview-renderer-wzIchDSv.js} +4 -4
- package/dist/gateway/static/root/assets/attachment-process-heavy-Db4nTY5N.js +5 -0
- package/dist/gateway/static/root/assets/{automations-page-Dkp7gp_S.js → automations-page-B3TmHNjQ.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace--A4I0jMu.js +6 -0
- package/dist/gateway/static/root/assets/binary-plugins-B3cXF_DP.js +2 -0
- package/dist/gateway/static/root/assets/{block-editor-r5nCBBan.js → block-editor-05zyBXBe.js} +3 -3
- package/dist/gateway/static/root/assets/{browser-automation-inputs-DcVLoIzc.js → browser-automation-inputs-BTIufVbM.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-automations-page-BnL4JnTM.js → browser-automations-page-DRBA9nwe.js} +2 -2
- package/dist/gateway/static/root/assets/browser-settings-page-DDTXUNWK.js +8 -0
- package/dist/gateway/static/root/assets/capabilities-settings-panel-Ll5UeXq5.js +8 -0
- package/dist/gateway/static/root/assets/channels-settings-BzL15bkU.js +1 -0
- package/dist/gateway/static/root/assets/{channels-status-swr-BTT2sIGg.js → channels-status-swr-DMYQNs-J.js} +1 -1
- package/dist/gateway/static/root/assets/{chat-terminal-dock-CWVnJtBp.js → chat-terminal-dock-Dx5KQyo5.js} +2 -2
- package/dist/gateway/static/root/assets/connectors-page-RPw1AOMb.js +2 -0
- package/dist/gateway/static/root/assets/{date-picker-XjPSlgq_.js → date-picker-CkDROZoe.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-CkyfEP88.js → dependency-picker-Bl8HcvzL.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-BAE_NVx5.js → desktop-pet-BcknAjbW.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-BUw6V2R9.js → desktop-pet-settings-BuCpXQPm.js} +1 -1
- package/dist/gateway/static/root/assets/device-pairing-wizard-D0_tCX49.js +1 -0
- package/dist/gateway/static/root/assets/{extension-debug-page-giBeIVUT.js → extension-debug-page-qRpb2FFa.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-Bqo9bNtx.js → extension-page-C7gs9wu4.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-BX-6AA3k.js → extension-settings-page-CyJP1CMZ.js} +1 -1
- package/dist/gateway/static/root/assets/gateway-config-swr-Cpmyrr6j.js +1 -0
- package/dist/gateway/static/root/assets/gateway-settings-DOeeTdWE.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-startup-retry-CVTa5HSS.js → gateway-startup-retry-DoiPeb65.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-settings-DWFAopk_.js → heartbeat-settings-CV6Dolcn.js} +1 -1
- package/dist/gateway/static/root/assets/home-page-Djw1H_Go.js +1 -0
- package/dist/gateway/static/root/assets/{image-generation-api-DtMRG1oF.js → image-generation-api-CqjHWl2T.js} +1 -1
- package/dist/gateway/static/root/assets/index-B_Sg5BKz.css +2 -0
- package/dist/gateway/static/root/assets/index-CHgOG1aR.js +74 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-BKGPpBnV.js → keyboard-shortcuts-settings-B3XqA41D.js} +1 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-BPqgyYJq.js +2 -0
- package/dist/gateway/static/root/assets/{local-apps-page-B06ToOxB.js → local-apps-page-BcIVYp0J.js} +1 -1
- package/dist/gateway/static/root/assets/{locale-store-BkJ7g35d.js → locale-store-1urrBBCQ.js} +1 -1
- package/dist/gateway/static/root/assets/{logs-page-DibI1GsP.js → logs-page-BHax3XlS.js} +2 -2
- package/dist/gateway/static/root/assets/{management-settings-DJ6yMZCd.js → management-settings-DNFCdgzR.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-view-DF3aJP_5.js → markdown-view-BZmcFIWa.js} +1 -1
- package/dist/gateway/static/root/assets/{media-plugins-CdrLNAks.js → media-plugins-BDxNhoBC.js} +1 -1
- package/dist/gateway/static/root/assets/messages-DYFcjtmG.js +3 -0
- package/dist/gateway/static/root/assets/note-detail-panel-Cs46LFXO.js +5 -0
- package/dist/gateway/static/root/assets/notes-page-BUkcn_0t.js +8 -0
- package/dist/gateway/static/root/assets/{page-tabs-B7LA4z-x.js → page-tabs-agH3a2PI.js} +1 -1
- package/dist/gateway/static/root/assets/pdf-runtime-DU_sMPjG.js +2 -0
- package/dist/gateway/static/root/assets/pdf.worker.min-B4E34YdZ.js +48 -0
- package/dist/gateway/static/root/assets/{preview-open-alternatives-DTQRN7WR.js → preview-open-alternatives-XEZ7zDOP.js} +1 -1
- package/dist/gateway/static/root/assets/proactive-page-Bn484J9v.js +1 -0
- package/dist/gateway/static/root/assets/{product-open-page-BazdmFYo.js → product-open-page-lXAbj85Z.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-LcH21ixD.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-DelBiwyB.js +1 -0
- package/dist/gateway/static/root/assets/{registry-api-CTq8EECj.js → registry-api-CxQstD00.js} +1 -1
- package/dist/gateway/static/root/assets/remote-access-hub-3BjFYXCf.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-D-DZf4I1.js → runtime-tools-settings-panel-DErKZlVJ.js} +2 -2
- package/dist/gateway/static/root/assets/{schema-form-B-Yj9DVW.js → schema-form-DhCmJQVJ.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-CVFirgVd.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-DyfBpqkY.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-BxPUqwY6.js → settings-form-section-Dg6chotg.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-zLn8lIrs.js → settings-page-LyuN5N3M.js} +1 -1
- package/dist/gateway/static/root/assets/setup-status-panel-CrXhTzHr.js +1 -0
- package/dist/gateway/static/root/assets/share-preview-page-CZvmf4Ob.js +2 -0
- package/dist/gateway/static/root/assets/shares-settings-DyM_D8hM.js +1 -0
- package/dist/gateway/static/root/assets/skill-reload-api-Bys58-15.js +1 -0
- package/dist/gateway/static/root/assets/skills-page-sWNvFo6D.js +2 -0
- package/dist/gateway/static/root/assets/{skills-page.utils-BR5bk2vT.js → skills-page.utils-Eu0WVHTS.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-C0ptJutB.js → system-settings-panel-DQKkM7B2.js} +1 -1
- package/dist/gateway/static/root/assets/task-detail-page-B18NcZQJ.js +1 -0
- package/dist/gateway/static/root/assets/{theme-store-mV6Cm2vj.js → theme-store-Dc5s20V3.js} +1 -1
- package/dist/gateway/static/root/assets/tunnel-api-TKQpPVd6.js +1 -0
- package/dist/gateway/static/root/assets/{use-autosave-Dc5aN2px.js → use-autosave-CkVlrFk1.js} +1 -1
- package/dist/gateway/static/root/assets/user-model-page-ul-zr58b.js +2 -0
- package/dist/gateway/static/root/assets/{voice-api-key-field-CY0DpJui.js → voice-api-key-field-B12fFN-L.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-config-api-D3DdcH4H.js → voice-config-api-Mk78BrpP.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-Brg9yJBr.js → work-discovery-overlay-CT9Zy36i.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-D3fq7boB.js → workflow-run-setup-panel-BwefQliE.js} +2 -2
- package/dist/gateway/static/root/assets/workflows-page-BY_AWs7S.js +6 -0
- package/dist/gateway/static/root/index.html +10 -11
- package/dist/package.js +1 -1
- package/dist/packages/gateway-contract/src/discussions.js +17 -0
- package/dist/packages/gateway-contract/src/proactive.js +4 -2
- package/dist/src/agent/agent-manager.js +17 -17
- package/dist/src/agent/agent-scope.js +2 -2
- package/dist/src/agent/background-review/run-background-review.js +4 -4
- package/dist/src/agent/context/coordinator.js +3 -3
- package/dist/src/agent/context/execution-context.js +4 -4
- package/dist/src/agent/context/execution-scope.js +2 -2
- package/dist/src/agent/context/project-context.d.ts +1 -0
- package/dist/src/agent/context/project-context.js +16 -8
- package/dist/src/agent/embedded/index.js +2 -2
- package/dist/src/agent/embedded/model-runtime.js +1 -1
- package/dist/src/agent/embedded/run-for-session.js +5 -5
- package/dist/src/agent/embedded/run-turn.js +2 -2
- package/dist/src/agent/embedded/session-tool-result-guard.js +3 -3
- package/dist/src/agent/external-tools/composio-provider.js +1 -1
- package/dist/src/agent/external-tools/gateway-tools.js +1 -1
- package/dist/src/agent/external-tools/mcp-provider.js +2 -2
- package/dist/src/agent/image/generation/openai-images-provider.js +1 -1
- package/dist/src/agent/image/generation/providers/dashscope.js +1 -1
- package/dist/src/agent/image/generation/providers/fal.js +1 -1
- package/dist/src/agent/image/generation/providers/google.js +1 -1
- package/dist/src/agent/image/generation/runtime.js +1 -1
- package/dist/src/agent/image/index.js +1 -1
- package/dist/src/agent/image/load-image-media.js +1 -1
- package/dist/src/agent/image/tool-model-config.js +1 -1
- package/dist/src/agent/inbound/attachment-pipeline.js +1 -1
- package/dist/src/agent/inbound/inbound-loop.js +2 -2
- package/dist/src/agent/index.js +1 -1
- package/dist/src/agent/mcp/bundle-mcp-gateway.js +1 -1
- package/dist/src/agent/mcp/bundle-mcp-runtime.js +2 -2
- package/dist/src/agent/mcp/index.js +2 -2
- package/dist/src/agent/mcp/mcp-transport.js +1 -1
- package/dist/src/agent/mcp/oauth/mcp-oauth-manager.js +1 -1
- package/dist/src/agent/mcp/oauth/mcp-oauth-store.js +1 -1
- package/dist/src/agent/memory/index.js +2 -2
- package/dist/src/agent/messaging/outbound-coordinator.js +1 -1
- package/dist/src/agent/orchestration/agent-orchestrator.js +5 -5
- package/dist/src/agent/prompt/service-prompt-builder.js +2 -2
- package/dist/src/agent/sandbox/exec-policy.js +1 -1
- package/dist/src/agent/service/direct-turn-helpers.js +6 -6
- package/dist/src/agent/service/process-direct-one-shot.js +1 -1
- package/dist/src/agent/service/process-direct-streaming.js +5 -5
- package/dist/src/agent/service/webchat-tts.js +2 -2
- package/dist/src/agent/service.js +19 -19
- package/dist/src/agent/session/session-inspector.js +1 -1
- package/dist/src/agent/skills/filesystem-watcher.js +1 -1
- package/dist/src/agent/skills/hub-lock.js +1 -1
- package/dist/src/agent/skills/hub-pull.js +3 -3
- package/dist/src/agent/skills/marketplace/adapters/store/adapter.js +1 -1
- package/dist/src/agent/skills/marketplace/adapters/store/store-api-client.js +1 -1
- package/dist/src/agent/source-context/task-context.d.ts +3 -0
- package/dist/src/agent/source-context/task-context.js +24 -0
- package/dist/src/agent/source-context/types.d.ts +2 -2
- package/dist/src/agent/source-context/types.js +1 -1
- package/dist/src/agent/tasks/task-judge-service.js +5 -5
- package/dist/src/agent/tool-manuals/xopc-use.d.ts +1 -1
- package/dist/src/agent/tool-manuals/xopc-use.js +1 -0
- package/dist/src/agent/tools/create-share-tool.js +3 -3
- package/dist/src/agent/tools/delegate-tool.js +3 -3
- package/dist/src/agent/tools/executor.js +1 -1
- package/dist/src/agent/tools/factory.js +13 -13
- package/dist/src/agent/tools/image-generate-tool.js +3 -3
- package/dist/src/agent/tools/index.js +5 -5
- package/dist/src/agent/tools/list-dir.js +1 -1
- package/dist/src/agent/tools/publish-artifacts.js +2 -2
- package/dist/src/agent/tools/read.js +1 -1
- package/dist/src/agent/tools/review-workspace.js +1 -1
- package/dist/src/agent/tools/send-media.js +2 -2
- package/dist/src/agent/tools/skills-tools.js +1 -1
- package/dist/src/agent/tools/tts-tool.js +1 -1
- package/dist/src/agent/tools/user-context-tool.js +1 -1
- package/dist/src/agent/tools/write.js +2 -2
- package/dist/src/agent/tools/xopc-use-tool.js +6 -6
- package/dist/src/agent/workflow/subagent-runner.js +1 -1
- package/dist/src/ai-assist/text-assist.js +1 -1
- package/dist/src/auth/credentials.js +2 -2
- package/dist/src/auth/oauth/xopc-cloud.js +1 -1
- package/dist/src/auth/profiles/store.js +1 -1
- package/dist/src/auth/sync-provider-auth.js +2 -2
- package/dist/src/browser/providers/browser-ext-install.js +1 -1
- package/dist/src/capabilities/store-connector.js +1 -1
- package/dist/src/channels/attachments/voice-stt-webchat.js +2 -2
- package/dist/src/channels/index.js +2 -2
- package/dist/src/channels/outbound-sender.js +1 -1
- package/dist/src/channels/pairing/allow-from-file.js +9 -9
- package/dist/src/channels/pairing/pairing-store.js +6 -6
- package/dist/src/chat-commands/builtins/config.js +2 -2
- package/dist/src/chat-commands/builtins/review.js +1 -1
- package/dist/src/chat-commands/builtins/session.js +1 -1
- package/dist/src/chat-commands/builtins/thinking.js +1 -1
- package/dist/src/chat-commands/builtins/tts.js +1 -1
- package/dist/src/chat-commands/builtins/understanding.js +1 -1
- package/dist/src/chat-commands/builtins/workflow.js +2 -2
- package/dist/src/chat-commands/context.js +2 -2
- package/dist/src/chat-commands/index.js +3 -3
- package/dist/src/cli/commands/agent.js +6 -6
- package/dist/src/cli/commands/agents.js +1 -1
- package/dist/src/cli/commands/config.js +1 -1
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/cli/commands/doctor/checks/provider-auth.js +3 -3
- package/dist/src/cli/commands/extension-dev.js +1 -1
- package/dist/src/cli/commands/extension.js +1 -1
- package/dist/src/cli/commands/gateway/shared.js +1 -1
- package/dist/src/cli/commands/gateway/token.js +1 -1
- package/dist/src/cli/commands/image.js +1 -1
- package/dist/src/cli/commands/init.js +1 -1
- package/dist/src/cli/commands/models.js +1 -1
- package/dist/src/cli/commands/onboard.js +1 -1
- package/dist/src/cli/commands/project.js +2 -2
- package/dist/src/cli/commands/runtime.js +1 -1
- package/dist/src/cli/commands/skills.js +2 -2
- package/dist/src/cli/commands/tui.js +1 -1
- package/dist/src/cli/commands/tunnel.js +3 -3
- package/dist/src/cli/extension-cli-register.js +4 -4
- package/dist/src/cli/utils/gateway-client.js +1 -1
- package/dist/src/config/index.js +5 -5
- package/dist/src/config/loader.js +2 -2
- package/dist/src/config/models-json.js +1 -1
- package/dist/src/connectors/connected-source-understanding.js +2 -2
- package/dist/src/connectors/connection-recovery-service.js +3 -3
- package/dist/src/discussions/action-tasks.d.ts +21 -0
- package/dist/src/discussions/action-tasks.js +93 -0
- package/dist/src/discussions/analyzer.d.ts +21 -1
- package/dist/src/discussions/analyzer.js +229 -62
- package/dist/src/discussions/audio-repair.d.ts +8 -0
- package/dist/src/discussions/audio-repair.js +41 -0
- package/dist/src/discussions/edits.d.ts +22 -0
- package/dist/src/discussions/edits.js +81 -0
- package/dist/src/discussions/errors.d.ts +4 -0
- package/dist/src/discussions/errors.js +10 -0
- package/dist/src/discussions/export.d.ts +2 -0
- package/dist/src/discussions/export.js +23 -0
- package/dist/src/discussions/index.js +3 -2
- package/dist/src/discussions/live-worker.js +2 -2
- package/dist/src/discussions/organizer-worker.js +14 -5
- package/dist/src/discussions/organizer.d.ts +1 -1
- package/dist/src/discussions/organizer.js +60 -24
- package/dist/src/discussions/reconcile.d.ts +22 -0
- package/dist/src/discussions/reconcile.js +55 -0
- package/dist/src/discussions/recording.d.ts +9 -0
- package/dist/src/discussions/recording.js +219 -0
- package/dist/src/discussions/recordingJobs.d.ts +14 -0
- package/dist/src/discussions/recordingJobs.js +69 -0
- package/dist/src/discussions/repository.d.ts +4 -2
- package/dist/src/discussions/repository.js +15 -10
- package/dist/src/discussions/revisions.d.ts +12 -0
- package/dist/src/discussions/revisions.js +36 -0
- package/dist/src/discussions/sealer.d.ts +1 -0
- package/dist/src/discussions/sealer.js +62 -12
- package/dist/src/discussions/service.d.ts +16 -15
- package/dist/src/discussions/service.js +177 -91
- package/dist/src/discussions/types.d.ts +4 -139
- package/dist/src/discussions/types.js +1 -11
- package/dist/src/extensions/api.js +3 -3
- package/dist/src/extensions/bundled-extension-activation.js +1 -1
- package/dist/src/extensions/index.js +1 -1
- package/dist/src/extensions/loader.js +1 -1
- package/dist/src/extensions/lockfile.js +1 -1
- package/dist/src/extensions/sdk/index.js +1 -1
- package/dist/src/files/file-service.d.ts +1 -0
- package/dist/src/files/file-service.js +77 -0
- package/dist/src/gateway/agents-admin.js +4 -4
- package/dist/src/gateway/chat-stream/mapper.js +2 -2
- package/dist/src/gateway/device-routes.js +2 -2
- package/dist/src/gateway/file-path-classifier.js +2 -2
- package/dist/src/gateway/hono/app.js +6 -5
- package/dist/src/gateway/hono/lib/config-payload.js +3 -3
- package/dist/src/gateway/hono/lib/static-ui.js +1 -0
- package/dist/src/gateway/hono/routes/agents.js +1 -1
- package/dist/src/gateway/hono/routes/auth-registry-extensions.js +1 -1
- package/dist/src/gateway/hono/routes/browser.js +1 -1
- package/dist/src/gateway/hono/routes/commands-skills.js +3 -3
- package/dist/src/gateway/hono/routes/config-patch/gateway.js +1 -1
- package/dist/src/gateway/hono/routes/context-sources.js +1 -2
- package/dist/src/gateway/hono/routes/discussions.js +126 -19
- package/dist/src/gateway/hono/routes/files.js +1 -22
- package/dist/src/gateway/hono/routes/index.js +0 -2
- package/dist/src/gateway/hono/routes/lazy-bundles.js +16 -0
- package/dist/src/gateway/hono/routes/mcp.js +2 -2
- package/dist/src/gateway/hono/routes/media.js +1 -1
- package/dist/src/gateway/hono/routes/models.js +5 -5
- package/dist/src/gateway/hono/routes/notes.js +1 -1
- package/dist/src/gateway/hono/routes/proactive-control.js +4 -38
- package/dist/src/gateway/hono/routes/project-understanding.d.ts +3 -0
- package/dist/src/gateway/hono/routes/project-understanding.js +65 -0
- package/dist/src/gateway/hono/routes/projects.js +22 -6
- package/dist/src/gateway/hono/routes/session-input-handler.d.ts +2 -2
- package/dist/src/gateway/hono/routes/session-input-handler.js +2 -2
- package/dist/src/gateway/hono/routes/sessions.js +1 -1
- package/dist/src/gateway/hono/routes/shares.js +3 -3
- package/dist/src/gateway/hono/routes/site-shares.js +1 -1
- package/dist/src/gateway/hono/routes/status.js +1 -1
- package/dist/src/gateway/hono/routes/tasks.js +4 -3
- package/dist/src/gateway/hono/routes/user-model.js +4 -4
- package/dist/src/gateway/hono/routes/voice.js +3 -3
- package/dist/src/gateway/hono/routes/work-discovery.d.ts +0 -2
- package/dist/src/gateway/hono/routes/work-discovery.js +2 -16
- package/dist/src/gateway/hono/routes/workflows.js +2 -2
- package/dist/src/gateway/image-generation-setup.js +1 -1
- package/dist/src/gateway/lock.js +10 -10
- package/dist/src/gateway/media-access.js +2 -2
- package/dist/src/gateway/ports.js +6 -6
- package/dist/src/gateway/security/gateway-scopes.js +1 -2
- package/dist/src/gateway/server.js +1 -1
- package/dist/src/gateway/service/agent-runner.js +2 -2
- package/dist/src/gateway/service/config-coordinator.js +4 -4
- package/dist/src/gateway/service/marketplace-service.js +3 -3
- package/dist/src/gateway/service/run-gateway-agent.js +3 -3
- package/dist/src/gateway/service.d.ts +3 -0
- package/dist/src/gateway/service.js +45 -30
- package/dist/src/gateway/session-context-summary.js +2 -2
- package/dist/src/gateway/workspace-editor-path.js +2 -2
- package/dist/src/infra/gateway-processes.js +2 -2
- package/dist/src/infra/path-env.js +3 -3
- package/dist/src/infra/update-global.js +10 -10
- package/dist/src/knowledge-memory/repository.d.ts +2 -0
- package/dist/src/knowledge-memory/repository.js +3 -1
- package/dist/src/local-apps/service.js +1 -1
- package/dist/src/media-shared/http/ssrf-guard.js +1 -1
- package/dist/src/media-understanding/audio-transcription-runner.d.ts +1 -0
- package/dist/src/media-understanding/audio-transcription-runner.js +1 -0
- package/dist/src/media-understanding/runner.js +1 -0
- package/dist/src/media-understanding/types.d.ts +8 -0
- package/dist/src/notes/agent-context.js +3 -3
- package/dist/src/notes/service.d.ts +3 -1
- package/dist/src/notes/store.d.ts +3 -1
- package/dist/src/notes/store.js +5 -3
- package/dist/src/proactive/actions/service.js +1 -1
- package/dist/src/proactive/execution/agent-executor.js +1 -1
- package/dist/src/proactive/execution/context.js +1 -1
- package/dist/src/proactive/execution/worker.js +1 -1
- package/dist/src/proactive/experience.d.ts +38 -21
- package/dist/src/proactive/experience.js +109 -13
- package/dist/src/proactive/follow-ups.js +1 -1
- package/dist/src/proactive/inbox/cards.js +9 -3
- package/dist/src/proactive/inbox/delivery.js +2 -2
- package/dist/src/proactive/inbox/digest.js +2 -2
- package/dist/src/proactive/inbox/notification-policy.js +1 -1
- package/dist/src/proactive/inbox/repository.js +1 -1
- package/dist/src/proactive/inbox/service.js +1 -1
- package/dist/src/proactive/index.js +1 -1
- package/dist/src/proactive/maintenance.js +0 -1
- package/dist/src/proactive/scenarios/control.d.ts +0 -35
- package/dist/src/proactive/scenarios/control.js +7 -53
- package/dist/src/proactive/temporal/worker.js +1 -1
- package/dist/src/projects/project-skill-service.js +2 -2
- package/dist/src/projects/project-store.js +6 -4
- package/dist/src/providers/auth-runtime/resolve-auth.js +1 -1
- package/dist/src/providers/index.js +2 -2
- package/dist/src/providers/model-reference-auditor.js +1 -1
- package/dist/src/providers/model-registry.js +1 -1
- package/dist/src/providers/register-runtime-providers.js +1 -1
- package/dist/src/remote-access/exposure-manager.js +1 -1
- package/dist/src/session/client-history.d.ts +1 -1
- package/dist/src/session/client-history.js +1 -1
- package/dist/src/session/index.js +2 -2
- package/dist/src/session/session-title.js +1 -1
- package/dist/src/session/store.js +3 -3
- package/dist/src/session/thinking-resolve.js +1 -1
- package/dist/src/share/share-store.js +1 -1
- package/dist/src/share/share-thumbnail.js +1 -1
- package/dist/src/share/site-share-store.js +1 -1
- package/dist/src/storage/sqlite/connection.js +9 -9
- package/dist/src/storage/sqlite/migrations/166_remove_proactive_previews.sql +1 -0
- package/dist/src/storage/sqlite/migrations/167_discussion_recording_chunks.sql +7 -0
- package/dist/src/storage/sqlite/migrations/168_discussion_evidence.sql +16 -0
- package/dist/src/storage/sqlite/migrations/169_discussion_action_tasks.sql +8 -0
- package/dist/src/storage/sqlite/migrations/170_discussion_edits.sql +8 -0
- package/dist/src/storage/sqlite/migrations/171_discussion_recording_jobs.sql +14 -0
- package/dist/src/storage/sqlite/migrations/172_project_understanding.sql +3 -0
- package/dist/src/storage/sqlite/migrations/173_mobile_discussion_source.sql +5 -0
- package/dist/src/storage/sqlite/migrations/__tests__/mobile-discussion-source.test.ts +20 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
- package/dist/src/storage/sqlite/migrations/runner.js +3 -3
- package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
- package/dist/src/storage/sqlite/schema.js +1 -1
- package/dist/src/tasks/execution-context.js +1 -1
- package/dist/src/tasks/home-query-service.js +3 -3
- package/dist/src/tasks/index.js +3 -3
- package/dist/src/tasks/task-application-service.js +1 -1
- package/dist/src/tasks/task-context-assembler.js +1 -1
- package/dist/src/tasks/task-handoff-service.js +1 -1
- package/dist/src/tasks/task-repository.d.ts +1 -0
- package/dist/src/tasks/task-repository.js +4 -0
- package/dist/src/tasks/task-run-coordinator.js +1 -1
- package/dist/src/tui/backends/embedded-backend.js +9 -9
- package/dist/src/tui/backends/gateway-realtime-backend.js +2 -2
- package/dist/src/tui/extension-host/load-extensions.js +1 -1
- package/dist/src/tui/tui-command-formatters.js +1 -1
- package/dist/src/tui/tui-commands.js +5 -5
- package/dist/src/tui/tui-picker-overlay.js +1 -1
- package/dist/src/tui/tui-skills-autocomplete.js +1 -1
- package/dist/src/tui/tui-startup-resources.js +5 -5
- package/dist/src/tui/tui.js +3 -3
- package/dist/src/tunnel/gateway-lifecycle.js +1 -1
- package/dist/src/voice/audio/normalize.js +28 -5
- package/dist/src/voice/local/runtime-worker.js +1 -1
- package/dist/src/voice/realtime/memory-context.js +3 -3
- package/dist/src/voice/stt/index.js +2 -2
- package/dist/src/voice/stt/preflight.js +1 -1
- package/dist/src/voice/stt/providers/local-transcription.js +1 -1
- package/dist/src/voice/stt/transcribe-core.js +2 -1
- package/dist/src/voice/stt/types.d.ts +1 -0
- package/dist/src/voice/tts/index.js +3 -3
- package/dist/src/voice/tts/merge-config.js +1 -1
- package/dist/src/voice/tts/payload.js +1 -1
- package/dist/src/voice/tts/speak-core.js +1 -1
- package/dist/src/work-discovery/analyzer.d.ts +1 -0
- package/dist/src/work-discovery/analyzer.js +28 -22
- package/dist/src/work-discovery/index.js +3 -3
- package/dist/src/work-discovery/investigator.js +1 -1
- package/dist/src/work-discovery/project-understanding.d.ts +7 -0
- package/dist/src/work-discovery/project-understanding.js +38 -0
- package/dist/src/work-discovery/repository.d.ts +3 -1
- package/dist/src/work-discovery/repository.js +18 -5
- package/dist/src/work-discovery/service.d.ts +8 -0
- package/dist/src/work-discovery/service.js +165 -17
- package/dist/src/work-discovery/types.d.ts +2 -0
- package/dist/src/workflows/context/workflow-context.js +1 -1
- package/dist/src/workflows/service/workflow-run-service.js +6 -6
- package/dist/src/workflows/service/workflow-session-bridge.js +1 -1
- package/package.json +4 -4
- package/dist/gateway/static/root/assets/agent-defaults-settings-panel-DAqn5DdL.js +0 -1
- package/dist/gateway/static/root/assets/apps-page-Bh0etSmL.js +0 -1
- package/dist/gateway/static/root/assets/attachment-process-heavy-C5CqLCsX.js +0 -5
- package/dist/gateway/static/root/assets/automations-workspace-w4McwueN.js +0 -6
- package/dist/gateway/static/root/assets/binary-plugins-CfUzu1Hc.js +0 -2
- package/dist/gateway/static/root/assets/browser-settings-page-DxX-t0EJ.js +0 -8
- package/dist/gateway/static/root/assets/capabilities-settings-panel-CvacBQAI.js +0 -8
- package/dist/gateway/static/root/assets/channels-settings-CJzSNo-L.js +0 -1
- package/dist/gateway/static/root/assets/config-reload-event-Bjh2zoKp.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-DNBhBT0b.js +0 -2
- package/dist/gateway/static/root/assets/device-pairing-wizard-DsR5NQEC.js +0 -1
- package/dist/gateway/static/root/assets/excel-worksheet-utils-DpdB-DjU.js +0 -1
- package/dist/gateway/static/root/assets/gateway-config-swr-cd8owslz.js +0 -1
- package/dist/gateway/static/root/assets/gateway-settings-Dil7vuyU.js +0 -1
- package/dist/gateway/static/root/assets/home-page-Dpeuvazg.js +0 -1
- package/dist/gateway/static/root/assets/index-B03HGpzt.js +0 -74
- package/dist/gateway/static/root/assets/index-CXF9-BpI.css +0 -2
- package/dist/gateway/static/root/assets/local-app-workbench-page-CBtemxFc.js +0 -2
- package/dist/gateway/static/root/assets/messages-BGc-OJWg.js +0 -3
- package/dist/gateway/static/root/assets/note-detail-panel-GOxomxOK.js +0 -5
- package/dist/gateway/static/root/assets/notes-page-CQBgk3_x.js +0 -8
- package/dist/gateway/static/root/assets/pdf.worker.min-CHFwMXne.mjs +0 -29
- package/dist/gateway/static/root/assets/proactive-card-BF3741px.js +0 -1
- package/dist/gateway/static/root/assets/proactive-page-1EM217v0.js +0 -1
- package/dist/gateway/static/root/assets/project-detail-page-B1Pm-fQd.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-DUrxyNR9.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-C6VzHUwx.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-DtemHfkv.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-ByEVQe0c.js +0 -1
- package/dist/gateway/static/root/assets/setup-status-panel-BwIrk3Jx.js +0 -1
- package/dist/gateway/static/root/assets/share-preview-page-DAc_FkVc.js +0 -2
- package/dist/gateway/static/root/assets/shares-settings-Dwdv6QyS.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-CDzQWN60.js +0 -1
- package/dist/gateway/static/root/assets/skills-page-CO5XVkbY.js +0 -2
- package/dist/gateway/static/root/assets/task-detail-page-DOjcKOzQ.js +0 -1
- package/dist/gateway/static/root/assets/tunnel-api-BABuQvGD.js +0 -1
- package/dist/gateway/static/root/assets/user-model-page-UhsSP_pC.js +0 -2
- package/dist/gateway/static/root/assets/workflows-page-S7XXNBWa.js +0 -6
- package/dist/src/proactive/scenarios/preview.d.ts +0 -13
- package/dist/src/proactive/scenarios/preview.js +0 -81
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
+
import { DISCUSSION_MAX_DURATION_MS } from "../../packages/gateway-contract/src/discussions.js";
|
|
3
|
+
import { decodeWavToMonoFloat32 } from "../voice/local/wav.js";
|
|
4
|
+
import { forEachNormalizedAudioSegment } from "../voice/audio/normalize.js";
|
|
5
|
+
import { DiscussionServiceError } from "./errors.js";
|
|
6
|
+
import { getDiscussionCapture, updateDiscussionCapture } from "./repository.js";
|
|
7
|
+
import { resolveNoteMediaDir } from "../notes/paths.js";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { createReadStream } from "node:fs";
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import { mkdir, open, rename, rm } from "node:fs/promises";
|
|
12
|
+
//#region src/discussions/recording.ts
|
|
13
|
+
const invalid = (message) => new DiscussionServiceError("invalid_input", message);
|
|
14
|
+
const directory = (capture) => join(resolveNoteMediaDir(capture.noteId), ".recording");
|
|
15
|
+
function listRecordingChunks(id) {
|
|
16
|
+
return getSqliteDatabase().prepare("SELECT sequence, sha256, bytes FROM discussion_recording_chunks WHERE discussion_id = ? ORDER BY sequence").all(id);
|
|
17
|
+
}
|
|
18
|
+
/** Called under the discussion mutation queue. Receipt follows durable disk write. */
|
|
19
|
+
async function saveRecordingChunk(capture, sequence, sha256, body) {
|
|
20
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0 || sequence > 1e4 || !/^[a-f0-9]{64}$/.test(sha256)) throw invalid("Invalid recording chunk metadata");
|
|
21
|
+
const chunks = listRecordingChunks(capture.id);
|
|
22
|
+
const existing = chunks.find((chunk) => chunk.sequence === sequence);
|
|
23
|
+
if (existing) {
|
|
24
|
+
await body.cancel();
|
|
25
|
+
if (existing.sha256 !== sha256) throw new DiscussionServiceError("conflict", "Chunk already contains different audio");
|
|
26
|
+
return existing;
|
|
27
|
+
}
|
|
28
|
+
const root = directory(capture);
|
|
29
|
+
await mkdir(root, { recursive: true });
|
|
30
|
+
const temporary = join(root, `${sequence}-${randomUUID()}.tmp`);
|
|
31
|
+
const file = await open(temporary, "wx");
|
|
32
|
+
const reader = body.getReader();
|
|
33
|
+
const hash = createHash("sha256");
|
|
34
|
+
let bytes = 0;
|
|
35
|
+
const previousBytes = chunks.reduce((total, chunk) => total + chunk.bytes, 0);
|
|
36
|
+
try {
|
|
37
|
+
while (true) {
|
|
38
|
+
const next = await reader.read();
|
|
39
|
+
if (next.done) break;
|
|
40
|
+
bytes += next.value.byteLength;
|
|
41
|
+
if (bytes > 8388608 || previousBytes + bytes > 1073741824) throw invalid("Recording exceeds upload size limit");
|
|
42
|
+
hash.update(next.value);
|
|
43
|
+
await file.writeFile(next.value);
|
|
44
|
+
}
|
|
45
|
+
if (!bytes || hash.digest("hex") !== sha256) throw invalid("Recording chunk checksum mismatch");
|
|
46
|
+
await file.sync();
|
|
47
|
+
await file.close();
|
|
48
|
+
const latest = getDiscussionCapture(capture.id);
|
|
49
|
+
if (!latest || latest.status === "cancelled" || latest.audioDeletedAt) throw new DiscussionServiceError("conflict", "Recording was deleted or cancelled");
|
|
50
|
+
await rename(temporary, join(root, String(sequence)));
|
|
51
|
+
const dir = await open(root, "r");
|
|
52
|
+
try {
|
|
53
|
+
await dir.sync();
|
|
54
|
+
} finally {
|
|
55
|
+
await dir.close();
|
|
56
|
+
}
|
|
57
|
+
getSqliteDatabase().prepare("INSERT INTO discussion_recording_chunks (discussion_id, sequence, sha256, bytes) VALUES (?, ?, ?, ?)").run(capture.id, sequence, sha256, bytes);
|
|
58
|
+
return {
|
|
59
|
+
sequence,
|
|
60
|
+
sha256,
|
|
61
|
+
bytes
|
|
62
|
+
};
|
|
63
|
+
} finally {
|
|
64
|
+
await reader.cancel().catch(() => void 0);
|
|
65
|
+
await file.close().catch(() => void 0);
|
|
66
|
+
await rm(temporary, { force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function validateRecordingManifest(capture, input) {
|
|
70
|
+
if (input.containerMode !== void 0 && (input.containerMode !== "independent_wav" || input.mimeType !== "audio/wav")) throw invalid("Unsupported recording container");
|
|
71
|
+
const mimeType = input.mimeType.split(";")[0];
|
|
72
|
+
if (![
|
|
73
|
+
"audio/wav",
|
|
74
|
+
"audio/x-wav",
|
|
75
|
+
"audio/webm",
|
|
76
|
+
"audio/ogg",
|
|
77
|
+
"audio/mpeg",
|
|
78
|
+
"audio/mp4"
|
|
79
|
+
].includes(mimeType)) throw invalid("Unsupported audio type");
|
|
80
|
+
const chunks = listRecordingChunks(capture.id);
|
|
81
|
+
if (!Number.isSafeInteger(input.chunkCount) || input.chunkCount < 1 || chunks.length !== input.chunkCount || chunks.some((chunk, index) => chunk.sequence !== index)) throw invalid("Recording has missing chunks");
|
|
82
|
+
}
|
|
83
|
+
async function completeRecording(capture, input, notes, assertLease, signal) {
|
|
84
|
+
validateRecordingManifest(capture, input);
|
|
85
|
+
const chunks = listRecordingChunks(capture.id);
|
|
86
|
+
const target = join(directory(capture), `assembled-${randomUUID()}`);
|
|
87
|
+
const file = await open(target, "w");
|
|
88
|
+
const hash = createHash("sha256");
|
|
89
|
+
try {
|
|
90
|
+
try {
|
|
91
|
+
if (input.containerMode === "independent_wav") {
|
|
92
|
+
const samples = chunks.reduce((total, chunk) => total + (chunk.bytes - 44) / 2, 0);
|
|
93
|
+
if (!Number.isSafeInteger(samples) || samples <= 0 || samples > 16e3 * 72e5 / 1e3) throw invalid("Invalid PCM recording duration");
|
|
94
|
+
const header = recordingWavHeader(samples);
|
|
95
|
+
hash.update(header);
|
|
96
|
+
await file.writeFile(header);
|
|
97
|
+
}
|
|
98
|
+
for (const chunk of chunks) {
|
|
99
|
+
const chunkHash = createHash("sha256");
|
|
100
|
+
let bytes = 0;
|
|
101
|
+
try {
|
|
102
|
+
if (input.containerMode === "independent_wav") {
|
|
103
|
+
signal.throwIfAborted();
|
|
104
|
+
const buffers = [];
|
|
105
|
+
for await (const buffer of createReadStream(join(directory(capture), String(chunk.sequence)))) {
|
|
106
|
+
signal.throwIfAborted();
|
|
107
|
+
bytes += buffer.length;
|
|
108
|
+
if (bytes > 8388608 || bytes > chunk.bytes) throw invalid("Stored WAV chunk is corrupt");
|
|
109
|
+
buffers.push(buffer);
|
|
110
|
+
}
|
|
111
|
+
const data = Buffer.concat(buffers, bytes);
|
|
112
|
+
if (data.length !== chunk.bytes || data.length <= 44 || data.length > 8388608 || data.length % 2 !== 0 || !data.subarray(0, 44).equals(recordingWavHeader((data.length - 44) / 2)) || createHash("sha256").update(data).digest("hex") !== chunk.sha256) throw invalid("Stored WAV chunk is corrupt");
|
|
113
|
+
hash.update(data.subarray(44));
|
|
114
|
+
await file.writeFile(data.subarray(44));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
for await (const buffer of createReadStream(join(directory(capture), String(chunk.sequence)))) {
|
|
118
|
+
signal.throwIfAborted();
|
|
119
|
+
hash.update(buffer);
|
|
120
|
+
chunkHash.update(buffer);
|
|
121
|
+
bytes += buffer.length;
|
|
122
|
+
await file.writeFile(buffer);
|
|
123
|
+
}
|
|
124
|
+
if (bytes !== chunk.bytes || chunkHash.digest("hex") !== chunk.sha256) throw invalid("Stored recording chunk is corrupt");
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (!signal.aborted && (error instanceof DiscussionServiceError || error.code === "ENOENT")) {
|
|
127
|
+
assertLease();
|
|
128
|
+
getSqliteDatabase().prepare("DELETE FROM discussion_recording_chunks WHERE discussion_id=? AND sequence=?").run(capture.id, chunk.sequence);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await file.sync();
|
|
134
|
+
} finally {
|
|
135
|
+
await file.close();
|
|
136
|
+
}
|
|
137
|
+
let durationMs = 0;
|
|
138
|
+
await forEachNormalizedAudioSegment({
|
|
139
|
+
filePath: target,
|
|
140
|
+
segmentSeconds: 30,
|
|
141
|
+
maxDurationSeconds: DISCUSSION_MAX_DURATION_MS / 1e3,
|
|
142
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(18e4)])
|
|
143
|
+
}, async (buffer) => {
|
|
144
|
+
durationMs += decodeWavToMonoFloat32(buffer).durationSeconds * 1e3;
|
|
145
|
+
});
|
|
146
|
+
if (durationMs < 1e3) throw invalid("Recording must contain at least one second of audio");
|
|
147
|
+
const latest = getDiscussionCapture(capture.id);
|
|
148
|
+
if (!latest || latest.status === "cancelled" || latest.audioDeletedAt) throw new DiscussionServiceError("conflict", "Recording was deleted or cancelled");
|
|
149
|
+
assertLease();
|
|
150
|
+
const audioSha256 = hash.digest("hex");
|
|
151
|
+
const attachment = await notes.addAttachment(capture.noteId, {
|
|
152
|
+
name: input.fileName.trim().slice(0, 200) || "meeting.webm",
|
|
153
|
+
buffer: { filePath: target },
|
|
154
|
+
mimeType: input.mimeType,
|
|
155
|
+
duration: Math.round(durationMs / 1e3),
|
|
156
|
+
retainWithoutReference: true
|
|
157
|
+
}, `meeting-recording:${capture.id}`);
|
|
158
|
+
if (!attachment) throw new DiscussionServiceError("not_found", "Discussion note not found");
|
|
159
|
+
const persisted = await notes.getAttachmentPath(capture.noteId, attachment.id);
|
|
160
|
+
if (!persisted) throw new Error("Saved recording file is missing");
|
|
161
|
+
const savedFile = await open(persisted.filePath, "r");
|
|
162
|
+
try {
|
|
163
|
+
await savedFile.sync();
|
|
164
|
+
} finally {
|
|
165
|
+
await savedFile.close();
|
|
166
|
+
}
|
|
167
|
+
const savedDirectory = await open(resolveNoteMediaDir(capture.noteId), "r");
|
|
168
|
+
try {
|
|
169
|
+
await savedDirectory.sync();
|
|
170
|
+
} finally {
|
|
171
|
+
await savedDirectory.close();
|
|
172
|
+
}
|
|
173
|
+
const updated = runSqliteWriteTransaction(() => {
|
|
174
|
+
assertLease();
|
|
175
|
+
return updateDiscussionCapture(capture.id, {
|
|
176
|
+
status: "stopping",
|
|
177
|
+
audioAttachmentId: attachment.id,
|
|
178
|
+
mimeType: input.mimeType,
|
|
179
|
+
audioSha256,
|
|
180
|
+
durationMs: Math.round(durationMs),
|
|
181
|
+
audioSizeBytes: chunks.reduce((sum, chunk) => sum + chunk.bytes, 0)
|
|
182
|
+
}, [
|
|
183
|
+
"recording",
|
|
184
|
+
"stopping",
|
|
185
|
+
"needs_attention"
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
if (!updated) throw new DiscussionServiceError("conflict", "Recording changed while completing");
|
|
189
|
+
await removeRecordingChunks(capture);
|
|
190
|
+
return updated;
|
|
191
|
+
} finally {
|
|
192
|
+
await rm(target, { force: true });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function recordingWavHeader(samples) {
|
|
196
|
+
const header = Buffer.alloc(44);
|
|
197
|
+
header.write("RIFF");
|
|
198
|
+
header.writeUInt32LE(36 + samples * 2, 4);
|
|
199
|
+
header.write("WAVEfmt ", 8);
|
|
200
|
+
header.writeUInt32LE(16, 16);
|
|
201
|
+
header.writeUInt16LE(1, 20);
|
|
202
|
+
header.writeUInt16LE(1, 22);
|
|
203
|
+
header.writeUInt32LE(16e3, 24);
|
|
204
|
+
header.writeUInt32LE(32e3, 28);
|
|
205
|
+
header.writeUInt16LE(2, 32);
|
|
206
|
+
header.writeUInt16LE(16, 34);
|
|
207
|
+
header.write("data", 36);
|
|
208
|
+
header.writeUInt32LE(samples * 2, 40);
|
|
209
|
+
return header;
|
|
210
|
+
}
|
|
211
|
+
async function removeRecordingChunks(capture) {
|
|
212
|
+
await rm(directory(capture), {
|
|
213
|
+
recursive: true,
|
|
214
|
+
force: true
|
|
215
|
+
});
|
|
216
|
+
getSqliteDatabase().prepare("DELETE FROM discussion_recording_chunks WHERE discussion_id = ?").run(capture.id);
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
export { completeRecording, listRecordingChunks, removeRecordingChunks, saveRecordingChunk, validateRecordingManifest };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DiscussionRecordingJob, DiscussionRecordingManifest } from '@xopcai/gateway-contract';
|
|
2
|
+
export declare function getRecordingJob(discussionId: string): DiscussionRecordingJob | null;
|
|
3
|
+
export declare function getRecordingJobInput(discussionId: string): DiscussionRecordingManifest | null;
|
|
4
|
+
export declare function submitRecordingJob(discussionId: string, input: DiscussionRecordingManifest): DiscussionRecordingJob;
|
|
5
|
+
export declare function claimRecordingJob(owner: string, now?: number): {
|
|
6
|
+
input: DiscussionRecordingManifest;
|
|
7
|
+
id: string;
|
|
8
|
+
discussionId: string;
|
|
9
|
+
state: "queued" | "running" | "completed" | "failed" | "cancelled";
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function renewRecordingJob(id: string, owner: string, now?: number): boolean;
|
|
13
|
+
export declare function finishRecordingJob(id: string, owner: string, error?: string): boolean;
|
|
14
|
+
export declare function cancelRecordingJob(discussionId: string): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
+
import { DiscussionServiceError } from "./errors.js";
|
|
3
|
+
import { listRecordingChunks } from "./recording.js";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
//#region src/discussions/recordingJobs.ts
|
|
6
|
+
const LEASE_MS = 6e4;
|
|
7
|
+
function mapJob(row) {
|
|
8
|
+
return {
|
|
9
|
+
id: row.id,
|
|
10
|
+
discussionId: row.discussion_id,
|
|
11
|
+
state: row.state,
|
|
12
|
+
...row.error ? { error: row.error } : {}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function getRecordingJob(discussionId) {
|
|
16
|
+
const row = getSqliteDatabase().prepare("SELECT * FROM discussion_recording_jobs WHERE discussion_id=?").get(discussionId);
|
|
17
|
+
return row ? mapJob(row) : null;
|
|
18
|
+
}
|
|
19
|
+
function getRecordingJobInput(discussionId) {
|
|
20
|
+
const row = getSqliteDatabase().prepare("SELECT input_json FROM discussion_recording_jobs WHERE discussion_id=?").get(discussionId);
|
|
21
|
+
return row ? JSON.parse(row.input_json) : null;
|
|
22
|
+
}
|
|
23
|
+
function submitRecordingJob(discussionId, input) {
|
|
24
|
+
const inputJson = JSON.stringify({
|
|
25
|
+
chunkCount: input.chunkCount,
|
|
26
|
+
mimeType: input.mimeType,
|
|
27
|
+
fileName: input.fileName,
|
|
28
|
+
lastSequence: input.lastSequence,
|
|
29
|
+
...input.containerMode ? { containerMode: input.containerMode } : {}
|
|
30
|
+
});
|
|
31
|
+
return runSqliteWriteTransaction((db) => {
|
|
32
|
+
const chunks = listRecordingChunks(discussionId);
|
|
33
|
+
const manifestHash = createHash("sha256").update(inputJson).update(JSON.stringify(chunks)).digest("hex");
|
|
34
|
+
const existing = db.prepare("SELECT * FROM discussion_recording_jobs WHERE discussion_id=?").get(discussionId);
|
|
35
|
+
if (existing && existing.input_json !== inputJson) throw new DiscussionServiceError("conflict", "Recording manifest differs from the submitted recording");
|
|
36
|
+
if (existing && chunks.length && existing.manifest_hash !== manifestHash) throw new DiscussionServiceError("conflict", "Recording contents differ from the submitted manifest");
|
|
37
|
+
if (existing?.state === "cancelled") throw new DiscussionServiceError("conflict", "Recording job was cancelled");
|
|
38
|
+
if (existing?.state === "failed") db.prepare("UPDATE discussion_recording_jobs SET state='queued', error=NULL, updated_at=? WHERE id=?").run(Date.now(), existing.id);
|
|
39
|
+
if (!existing) db.prepare("INSERT INTO discussion_recording_jobs (id, discussion_id, input_json, manifest_hash, state, created_at, updated_at) VALUES (?, ?, ?, ?, 'queued', ?, ?)").run(randomUUID(), discussionId, inputJson, manifestHash, Date.now(), Date.now());
|
|
40
|
+
return getRecordingJob(discussionId);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function claimRecordingJob(owner, now = Date.now()) {
|
|
44
|
+
return runSqliteWriteTransaction((db) => {
|
|
45
|
+
const row = db.prepare(`SELECT j.* FROM discussion_recording_jobs j JOIN discussion_captures d ON d.id=j.discussion_id
|
|
46
|
+
WHERE (j.state='queued' OR (j.state='running' AND j.lease_until<=?))
|
|
47
|
+
AND (d.status IN ('recording','stopping','needs_attention') OR d.audio_attachment_id IS NOT NULL)
|
|
48
|
+
AND d.status<>'cancelled' AND d.audio_deleted_at IS NULL
|
|
49
|
+
ORDER BY j.created_at LIMIT 1`).get(now);
|
|
50
|
+
if (!row) return null;
|
|
51
|
+
db.prepare("UPDATE discussion_recording_jobs SET state='running', lease_owner=?, lease_until=?, updated_at=? WHERE id=?").run(owner, now + LEASE_MS, now, row.id);
|
|
52
|
+
return {
|
|
53
|
+
...mapJob(row),
|
|
54
|
+
input: JSON.parse(row.input_json)
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function renewRecordingJob(id, owner, now = Date.now()) {
|
|
59
|
+
return getSqliteDatabase().prepare("UPDATE discussion_recording_jobs SET lease_until=? WHERE id=? AND state='running' AND lease_owner=? AND lease_until>?").run(now + LEASE_MS, id, owner, now).changes === 1;
|
|
60
|
+
}
|
|
61
|
+
function finishRecordingJob(id, owner, error) {
|
|
62
|
+
return getSqliteDatabase().prepare(`UPDATE discussion_recording_jobs SET state=?, error=?, lease_owner=NULL, lease_until=NULL, updated_at=?
|
|
63
|
+
WHERE id=? AND state='running' AND lease_owner=? AND lease_until>?`).run(error === void 0 ? "completed" : "failed", error?.slice(0, 1e3) ?? null, Date.now(), id, owner, Date.now()).changes === 1;
|
|
64
|
+
}
|
|
65
|
+
function cancelRecordingJob(discussionId) {
|
|
66
|
+
getSqliteDatabase().prepare("UPDATE discussion_recording_jobs SET state='cancelled', lease_owner=NULL, lease_until=NULL, updated_at=? WHERE discussion_id=? AND state<>'completed'").run(Date.now(), discussionId);
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { cancelRecordingJob, claimRecordingJob, finishRecordingJob, getRecordingJob, getRecordingJobInput, renewRecordingJob, submitRecordingJob };
|
|
@@ -10,7 +10,7 @@ export declare function listDiscussionCaptures(query?: ListDiscussionsQuery): Di
|
|
|
10
10
|
export declare function getDiscussionMetrics(): DiscussionMetrics;
|
|
11
11
|
export declare function updateDiscussionCapture(id: string, patch: Partial<Omit<DiscussionCapture, 'id' | 'clientRequestId' | 'noteId' | 'createdAt'>>, expectedStatuses?: DiscussionStatus[]): DiscussionCapture | null;
|
|
12
12
|
export declare function claimNextDiscussionCapture(owner: string, now?: number, leaseMs?: number): DiscussionCapture | null;
|
|
13
|
-
export declare function releaseDiscussionWorkLease(id: string): void;
|
|
13
|
+
export declare function releaseDiscussionWorkLease(id: string, owner: string): void;
|
|
14
14
|
export declare function createDiscussionTranscriptSegment(input: {
|
|
15
15
|
discussionId: string;
|
|
16
16
|
sequence: number;
|
|
@@ -24,7 +24,7 @@ export declare function getDiscussionTranscriptSegment(discussionId: string, seq
|
|
|
24
24
|
export declare function listDiscussionTranscriptSegments(discussionId: string): DiscussionTranscriptSegment[];
|
|
25
25
|
export declare function claimNextDiscussionTranscriptSegment(owner: string, now?: number, leaseMs?: number): ClaimedDiscussionTranscriptSegment | null;
|
|
26
26
|
export declare function completeDiscussionTranscriptSegment(discussionId: string, sequence: number, owner: string, text: string, provider: string, language?: string, confidence?: number): DiscussionTranscriptSegment | null;
|
|
27
|
-
export declare function correctDiscussionTranscriptSegment(discussionId: string, sequence: number, displayText: string, expectedRevision: number): DiscussionTranscriptSegment | null;
|
|
27
|
+
export declare function correctDiscussionTranscriptSegment(discussionId: string, sequence: number, displayText: string, expectedRevision: number, speakerLabel?: string): DiscussionTranscriptSegment | null;
|
|
28
28
|
export declare function retryOrFailDiscussionTranscriptSegment(discussionId: string, sequence: number, owner: string, error: string, exhausted: boolean): DiscussionTranscriptSegment | null;
|
|
29
29
|
export declare function deleteDiscussionSegmentAudio(discussionId: string): void;
|
|
30
30
|
export declare function createDiscussionOrganization(input: {
|
|
@@ -32,9 +32,11 @@ export declare function createDiscussionOrganization(input: {
|
|
|
32
32
|
inputTranscriptSha256: string;
|
|
33
33
|
promptVersion: string;
|
|
34
34
|
modelRef: string;
|
|
35
|
+
transcriptRevision: number;
|
|
35
36
|
}): DiscussionOrganizationRecord;
|
|
36
37
|
export declare function getLatestDiscussionOrganization(discussionId: string): DiscussionOrganizationRecord | null;
|
|
37
38
|
export declare function completeDiscussionOrganization(id: string, organization: DiscussionOrganization): DiscussionOrganizationRecord | null;
|
|
38
39
|
export declare function failDiscussionOrganization(id: string, error: string): void;
|
|
39
40
|
export declare function getDiscussionCaptureSettings(): DiscussionCaptureSettings;
|
|
40
41
|
export declare function acknowledgeDiscussionCaptureConsent(policyVersion: number): DiscussionCaptureSettings;
|
|
42
|
+
export declare function renewDiscussionWorkLease(id: string, owner: string): boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getSqliteDatabase, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
-
import { DISCUSSION_STATUSES } from "
|
|
2
|
+
import { DISCUSSION_STATUSES } from "../../packages/gateway-contract/src/discussions.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
//#region src/discussions/repository.ts
|
|
5
5
|
function optional(condition, value) {
|
|
@@ -17,6 +17,7 @@ function discussionFromRow(row) {
|
|
|
17
17
|
createdAt: row.created_at,
|
|
18
18
|
updatedAt: row.updated_at
|
|
19
19
|
};
|
|
20
|
+
capture.template = row.organization_template;
|
|
20
21
|
if (row.project_id) capture.projectId = row.project_id;
|
|
21
22
|
if (row.audio_attachment_id) capture.audioAttachmentId = row.audio_attachment_id;
|
|
22
23
|
if (row.duration_ms != null) capture.durationMs = row.duration_ms;
|
|
@@ -71,6 +72,7 @@ function organizationFromRow(row) {
|
|
|
71
72
|
revision: row.revision,
|
|
72
73
|
inputTranscriptSha256: row.input_transcript_sha256,
|
|
73
74
|
promptVersion: row.prompt_version,
|
|
75
|
+
transcriptRevision: row.transcript_revision,
|
|
74
76
|
modelRef: row.model_ref,
|
|
75
77
|
...optional(row.organization_json, { organization: JSON.parse(row.organization_json) }),
|
|
76
78
|
status: row.status,
|
|
@@ -161,7 +163,7 @@ function updateDiscussionCapture(id, patch, expectedStatuses) {
|
|
|
161
163
|
mime_type=?, audio_size_bytes=?, audio_sha256=?, canonical_transcript=?, canonical_transcript_sha256=?,
|
|
162
164
|
transcript_language=?, transcript_revision=?, generated_title=?, project_inference_score=?,
|
|
163
165
|
project_inference_source=?, failure_stage=?, failure_code=?, failure_message=?, recording_started_at=?,
|
|
164
|
-
recording_stopped_at=?, updated_at=?, completed_at=?, audio_deleted_at=? WHERE id=?${statusGuard}`).run(next.projectId ?? null, next.audioAttachmentId ?? null, next.source, next.status, next.durationMs ?? null, next.expectedLastSequence ?? null, next.mimeType ?? null, next.audioSizeBytes ?? null, next.audioSha256 ?? null, next.canonicalTranscript ?? null, next.canonicalTranscriptSha256 ?? null, next.transcriptLanguage ?? null, next.transcriptRevision, next.generatedTitle ?? null, next.projectInferenceScore ?? null, next.projectInferenceSource ?? null, next.failureStage ?? null, next.failureCode ?? null, next.failureMessage ?? null, next.recordingStartedAt, next.recordingStoppedAt ?? null, next.updatedAt, next.completedAt ?? null, next.audioDeletedAt ?? null, id, ...expectedStatuses ?? []).changes) ? getDiscussionCapture(id) : null;
|
|
166
|
+
recording_stopped_at=?, updated_at=?, completed_at=?, audio_deleted_at=?, organization_template=? WHERE id=?${statusGuard}`).run(next.projectId ?? null, next.audioAttachmentId ?? null, next.source, next.status, next.durationMs ?? null, next.expectedLastSequence ?? null, next.mimeType ?? null, next.audioSizeBytes ?? null, next.audioSha256 ?? null, next.canonicalTranscript ?? null, next.canonicalTranscriptSha256 ?? null, next.transcriptLanguage ?? null, next.transcriptRevision, next.generatedTitle ?? null, next.projectInferenceScore ?? null, next.projectInferenceSource ?? null, next.failureStage ?? null, next.failureCode ?? null, next.failureMessage ?? null, next.recordingStartedAt, next.recordingStoppedAt ?? null, next.updatedAt, next.completedAt ?? null, next.audioDeletedAt ?? null, next.template ?? "general", id, ...expectedStatuses ?? []).changes) ? getDiscussionCapture(id) : null;
|
|
165
167
|
}
|
|
166
168
|
function claimNextDiscussionCapture(owner, now = Date.now(), leaseMs = 5 * 6e4) {
|
|
167
169
|
return runSqliteWriteTransaction((db) => {
|
|
@@ -173,8 +175,8 @@ function claimNextDiscussionCapture(owner, now = Date.now(), leaseMs = 5 * 6e4)
|
|
|
173
175
|
WHERE id=? AND status='organizing' AND (work_lease_owner IS NULL OR work_lease_expires_at <= ?)`).run(owner, now + leaseMs, now, row.id, now).changes ? getDiscussionCapture(row.id) : null;
|
|
174
176
|
});
|
|
175
177
|
}
|
|
176
|
-
function releaseDiscussionWorkLease(id) {
|
|
177
|
-
runSqliteWriteTransaction((db) => db.prepare("UPDATE discussion_captures SET work_lease_owner=NULL, work_lease_expires_at=NULL WHERE id=?").run(id));
|
|
178
|
+
function releaseDiscussionWorkLease(id, owner) {
|
|
179
|
+
runSqliteWriteTransaction((db) => db.prepare("UPDATE discussion_captures SET work_lease_owner=NULL, work_lease_expires_at=NULL WHERE id=? AND work_lease_owner=?").run(id, owner));
|
|
178
180
|
}
|
|
179
181
|
function createDiscussionTranscriptSegment(input) {
|
|
180
182
|
const existing = getDiscussionTranscriptSegment(input.discussionId, input.sequence);
|
|
@@ -228,12 +230,12 @@ function completeDiscussionTranscriptSegment(discussionId, sequence, owner, text
|
|
|
228
230
|
return changed;
|
|
229
231
|
}) ? getDiscussionTranscriptSegment(discussionId, sequence) : null;
|
|
230
232
|
}
|
|
231
|
-
function correctDiscussionTranscriptSegment(discussionId, sequence, displayText, expectedRevision) {
|
|
233
|
+
function correctDiscussionTranscriptSegment(discussionId, sequence, displayText, expectedRevision, speakerLabel) {
|
|
232
234
|
const now = Date.now();
|
|
233
235
|
return runSqliteWriteTransaction((db) => {
|
|
234
|
-
const changed = db.prepare(`UPDATE discussion_transcript_segments SET display_text=?, corrected_by_user=1,
|
|
236
|
+
const changed = db.prepare(`UPDATE discussion_transcript_segments SET display_text=?, speaker_label=CASE WHEN ? THEN ? ELSE speaker_label END, corrected_by_user=1,
|
|
235
237
|
corrected_at=?, revision=revision+1, updated_at=? WHERE discussion_id=? AND sequence=?
|
|
236
|
-
AND status='confirmed' AND revision=?`).run(displayText, now, now, discussionId, sequence, expectedRevision).changes;
|
|
238
|
+
AND status='confirmed' AND revision=?`).run(displayText, speakerLabel === void 0 ? 0 : 1, speakerLabel?.trim() || null, now, now, discussionId, sequence, expectedRevision).changes;
|
|
237
239
|
if (changed) db.prepare("UPDATE discussion_captures SET transcript_revision=transcript_revision+1, updated_at=? WHERE id=?").run(now, discussionId);
|
|
238
240
|
return changed;
|
|
239
241
|
}) ? getDiscussionTranscriptSegment(discussionId, sequence) : null;
|
|
@@ -253,8 +255,8 @@ function createDiscussionOrganization(input) {
|
|
|
253
255
|
const revision = (getLatestDiscussionOrganization(input.discussionId)?.revision ?? 0) + 1;
|
|
254
256
|
const id = randomUUID();
|
|
255
257
|
runSqliteWriteTransaction((db) => db.prepare(`INSERT INTO discussion_organizations (
|
|
256
|
-
id, discussion_id, revision, input_transcript_sha256, prompt_version, model_ref, status, created_at
|
|
257
|
-
) VALUES (?, ?, ?, ?, ?, ?, 'running', ?)`).run(id, input.discussionId, revision, input.inputTranscriptSha256, input.promptVersion, input.modelRef, now));
|
|
258
|
+
id, discussion_id, revision, input_transcript_sha256, prompt_version, model_ref, transcript_revision, status, created_at
|
|
259
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'running', ?)`).run(id, input.discussionId, revision, input.inputTranscriptSha256, input.promptVersion, input.modelRef, input.transcriptRevision, now));
|
|
258
260
|
return getLatestDiscussionOrganization(input.discussionId);
|
|
259
261
|
}
|
|
260
262
|
function getLatestDiscussionOrganization(discussionId) {
|
|
@@ -285,5 +287,8 @@ function acknowledgeDiscussionCaptureConsent(policyVersion) {
|
|
|
285
287
|
SET consent_acknowledged_at=?, updated_at=? WHERE workspace_id='default' AND consent_policy_version=?`).run(now, now, policyVersion));
|
|
286
288
|
return getDiscussionCaptureSettings();
|
|
287
289
|
}
|
|
290
|
+
function renewDiscussionWorkLease(id, owner) {
|
|
291
|
+
return Number(getSqliteDatabase().prepare("UPDATE discussion_captures SET work_lease_expires_at=? WHERE id=? AND work_lease_owner=? AND status='organizing'").run(Date.now() + 5 * 6e4, id, owner).changes) === 1;
|
|
292
|
+
}
|
|
288
293
|
//#endregion
|
|
289
|
-
export { acknowledgeDiscussionCaptureConsent, claimNextDiscussionCapture, claimNextDiscussionTranscriptSegment, completeDiscussionOrganization, completeDiscussionTranscriptSegment, correctDiscussionTranscriptSegment, createDiscussionCapture, createDiscussionOrganization, createDiscussionTranscriptSegment, deleteDiscussionSegmentAudio, failDiscussionOrganization, getDiscussionCapture, getDiscussionCaptureByClientRequestId, getDiscussionCaptureByNoteId, getDiscussionCaptureSettings, getDiscussionMetrics, getDiscussionTranscriptSegment, getLatestDiscussionOrganization, listDiscussionCaptures, listDiscussionTranscriptSegments, releaseDiscussionWorkLease, retryOrFailDiscussionTranscriptSegment, updateDiscussionCapture };
|
|
294
|
+
export { acknowledgeDiscussionCaptureConsent, claimNextDiscussionCapture, claimNextDiscussionTranscriptSegment, completeDiscussionOrganization, completeDiscussionTranscriptSegment, correctDiscussionTranscriptSegment, createDiscussionCapture, createDiscussionOrganization, createDiscussionTranscriptSegment, deleteDiscussionSegmentAudio, failDiscussionOrganization, getDiscussionCapture, getDiscussionCaptureByClientRequestId, getDiscussionCaptureByNoteId, getDiscussionCaptureSettings, getDiscussionMetrics, getDiscussionTranscriptSegment, getLatestDiscussionOrganization, listDiscussionCaptures, listDiscussionTranscriptSegments, releaseDiscussionWorkLease, renewDiscussionWorkLease, retryOrFailDiscussionTranscriptSegment, updateDiscussionCapture };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DiscussionTranscriptSegment } from './types.js';
|
|
2
|
+
export declare function saveTranscriptRevision(id: string): number;
|
|
3
|
+
export declare function readTranscriptRevision(id: string, revision: number): DiscussionTranscriptSegment[] | null;
|
|
4
|
+
export declare function replaceTranscriptSegments(id: string, segments: Array<{
|
|
5
|
+
text: string;
|
|
6
|
+
startedAtMs: number;
|
|
7
|
+
endedAtMs: number;
|
|
8
|
+
speakerLabel?: string;
|
|
9
|
+
correctedByUser?: boolean;
|
|
10
|
+
rawText?: string;
|
|
11
|
+
}>): void;
|
|
12
|
+
export declare function refreshCanonicalTranscript(id: string): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
+
import { getDiscussionCapture, listDiscussionTranscriptSegments, updateDiscussionCapture } from "./repository.js";
|
|
3
|
+
import { assembleDiscussionTranscript } from "./transcript.js";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
//#region src/discussions/revisions.ts
|
|
6
|
+
function saveTranscriptRevision(id) {
|
|
7
|
+
const capture = getDiscussionCapture(id);
|
|
8
|
+
if (!capture) throw new Error("Discussion not found");
|
|
9
|
+
getSqliteDatabase().prepare("INSERT OR IGNORE INTO discussion_transcript_revisions (discussion_id, revision, segments_json, created_at) VALUES (?, ?, ?, ?)").run(id, capture.transcriptRevision, JSON.stringify(listDiscussionTranscriptSegments(id)), Date.now());
|
|
10
|
+
return capture.transcriptRevision;
|
|
11
|
+
}
|
|
12
|
+
function readTranscriptRevision(id, revision) {
|
|
13
|
+
const row = getSqliteDatabase().prepare("SELECT segments_json FROM discussion_transcript_revisions WHERE discussion_id=? AND revision=?").get(id, revision);
|
|
14
|
+
return row ? JSON.parse(row.segments_json) : null;
|
|
15
|
+
}
|
|
16
|
+
function replaceTranscriptSegments(id, segments) {
|
|
17
|
+
runSqliteWriteTransaction((db) => {
|
|
18
|
+
saveTranscriptRevision(id);
|
|
19
|
+
db.prepare("DELETE FROM discussion_transcript_segments WHERE discussion_id=?").run(id);
|
|
20
|
+
const insert = db.prepare(`INSERT INTO discussion_transcript_segments
|
|
21
|
+
(discussion_id, sequence, audio_sha256, started_at_ms, ended_at_ms, status, raw_text, display_text, speaker_label, revision, corrected_by_user, attempt_count, created_at, updated_at)
|
|
22
|
+
VALUES (?, ?, ?, ?, ?, 'confirmed', ?, ?, ?, 1, ?, 0, ?, ?)`);
|
|
23
|
+
segments.forEach((segment, index) => insert.run(id, index, createHash("sha256").update(segment.text).digest("hex"), segment.startedAtMs, segment.endedAtMs, segment.rawText ?? segment.text, segment.text, segment.speakerLabel ?? null, segment.correctedByUser ? 1 : 0, Date.now(), Date.now()));
|
|
24
|
+
db.prepare("UPDATE discussion_captures SET transcript_revision=transcript_revision+1 WHERE id=?").run(id);
|
|
25
|
+
saveTranscriptRevision(id);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function refreshCanonicalTranscript(id) {
|
|
29
|
+
const text = assembleDiscussionTranscript(listDiscussionTranscriptSegments(id));
|
|
30
|
+
updateDiscussionCapture(id, {
|
|
31
|
+
canonicalTranscript: text,
|
|
32
|
+
canonicalTranscriptSha256: createHash("sha256").update(text).digest("hex")
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { readTranscriptRevision, refreshCanonicalTranscript, replaceTranscriptSegments, saveTranscriptRevision };
|
|
@@ -2,6 +2,7 @@ import type { Config } from '../config/schema.js';
|
|
|
2
2
|
import type { NotesService } from '../notes/service.js';
|
|
3
3
|
import type { DiscussionCapture } from './types.js';
|
|
4
4
|
export interface DiscussionSealerDeps {
|
|
5
|
+
processRecordingJob?: () => Promise<void>;
|
|
5
6
|
notes: NotesService;
|
|
6
7
|
getConfig: () => Config;
|
|
7
8
|
transcribeRecording?: (filePath: string, capture: DiscussionCapture, signal?: AbortSignal) => Promise<{
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { createLogger } from "../utils/logger/index.js";
|
|
2
2
|
import { init_logger } from "../utils/logger.js";
|
|
3
|
+
import { decodeWavToMonoFloat32 } from "../voice/local/wav.js";
|
|
3
4
|
import { forEachNormalizedAudioSegment } from "../voice/audio/normalize.js";
|
|
4
|
-
import { isSTTAvailable } from "../voice/stt/availability.js";
|
|
5
5
|
import { transcribe } from "../voice/stt/transcribe-core.js";
|
|
6
|
+
import { isSTTAvailable } from "../voice/stt/availability.js";
|
|
6
7
|
import { mergeSttConfigFromAppConfig } from "../voice/stt/config.js";
|
|
7
8
|
import "../voice/stt/index.js";
|
|
8
9
|
import { getDiscussionCapture, listDiscussionCaptures, listDiscussionTranscriptSegments, updateDiscussionCapture } from "./repository.js";
|
|
9
|
-
import {
|
|
10
|
+
import { assembleDiscussionTranscript } from "./transcript.js";
|
|
11
|
+
import { replaceTranscriptSegments, saveTranscriptRevision } from "./revisions.js";
|
|
12
|
+
import { missingAudioRanges, slicePcmWav } from "./audio-repair.js";
|
|
13
|
+
import { getRecordingJob } from "./recordingJobs.js";
|
|
10
14
|
import { createHash } from "node:crypto";
|
|
11
15
|
//#region src/discussions/sealer.ts
|
|
12
16
|
init_logger();
|
|
@@ -34,6 +38,7 @@ var DiscussionSealer = class {
|
|
|
34
38
|
if (this.running) return;
|
|
35
39
|
this.running = true;
|
|
36
40
|
try {
|
|
41
|
+
await this.deps.processRecordingJob?.();
|
|
37
42
|
const capture = [...listDiscussionCaptures({
|
|
38
43
|
status: "sealing",
|
|
39
44
|
limit: 1
|
|
@@ -50,6 +55,8 @@ var DiscussionSealer = class {
|
|
|
50
55
|
async sealWhenReady(capture, now) {
|
|
51
56
|
const ageMs = now - (capture.recordingStoppedAt ?? now);
|
|
52
57
|
if (!capture.audioAttachmentId) {
|
|
58
|
+
const job = getRecordingJob(capture.id);
|
|
59
|
+
if (job && (job.state === "queued" || job.state === "running")) return;
|
|
53
60
|
if (ageMs >= UPLOAD_GRACE_MS) this.fail(capture, "audio_upload", "recording_missing", "Original recording was not uploaded");
|
|
54
61
|
return;
|
|
55
62
|
}
|
|
@@ -65,6 +72,7 @@ var DiscussionSealer = class {
|
|
|
65
72
|
const confirmed = segments.filter((segment) => segment.status === "confirmed");
|
|
66
73
|
const result = expected >= 0 && missing.length === 0 && !pending && confirmed.length === expected + 1 ? { text: assembleDiscussionTranscript(confirmed) } : await this.transcribeFullRecording(sealing);
|
|
67
74
|
const text = result.text.trim();
|
|
75
|
+
saveTranscriptRevision(capture.id);
|
|
68
76
|
if (!text) throw new Error("Discussion transcription produced no text");
|
|
69
77
|
const updated = updateDiscussionCapture(capture.id, {
|
|
70
78
|
status: "organizing",
|
|
@@ -85,22 +93,64 @@ var DiscussionSealer = class {
|
|
|
85
93
|
async transcribeFullRecording(capture) {
|
|
86
94
|
const attachment = await this.deps.notes.getAttachmentPath(capture.noteId, capture.audioAttachmentId);
|
|
87
95
|
if (!attachment) throw new Error("Discussion audio file is missing");
|
|
88
|
-
if (this.deps.transcribeRecording)
|
|
96
|
+
if (this.deps.transcribeRecording) {
|
|
97
|
+
const result = await this.deps.transcribeRecording(attachment.filePath, capture);
|
|
98
|
+
replaceTranscriptSegments(capture.id, [{
|
|
99
|
+
text: result.text,
|
|
100
|
+
startedAtMs: 0,
|
|
101
|
+
endedAtMs: capture.durationMs ?? 1e3
|
|
102
|
+
}]);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
89
105
|
const config = this.deps.getConfig();
|
|
90
106
|
const sttConfig = mergeSttConfigFromAppConfig(config.tools?.media?.audio, config.tools?.media);
|
|
91
107
|
if (!isSTTAvailable(sttConfig)) throw new Error("STT is not configured");
|
|
92
|
-
let
|
|
108
|
+
let offset = 0;
|
|
109
|
+
const confirmed = listDiscussionTranscriptSegments(capture.id).filter((segment) => segment.status === "confirmed");
|
|
110
|
+
const segments = confirmed.map((segment) => ({
|
|
111
|
+
text: segment.displayText ?? segment.rawText ?? "",
|
|
112
|
+
rawText: segment.rawText,
|
|
113
|
+
startedAtMs: segment.startedAtMs,
|
|
114
|
+
endedAtMs: segment.endedAtMs,
|
|
115
|
+
speakerLabel: segment.speakerLabel,
|
|
116
|
+
correctedByUser: segment.correctedByUser
|
|
117
|
+
}));
|
|
93
118
|
let language;
|
|
94
|
-
await forEachNormalizedAudioSegment({
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
await forEachNormalizedAudioSegment({
|
|
120
|
+
filePath: attachment.filePath,
|
|
121
|
+
maxDurationSeconds: 7200
|
|
122
|
+
}, async (buffer, index) => {
|
|
123
|
+
const decoded = decodeWavToMonoFloat32(buffer);
|
|
124
|
+
const durationMs = decoded.durationSeconds * 1e3;
|
|
125
|
+
const gaps = missingAudioRanges(offset, offset + durationMs, confirmed);
|
|
126
|
+
for (const [gapIndex, gap] of gaps.entries()) {
|
|
127
|
+
const audio = gaps.length === 1 && gap.startedAtMs === offset && gap.endedAtMs === offset + durationMs ? buffer : slicePcmWav(decoded, gap.startedAtMs - offset, gap.endedAtMs - offset);
|
|
128
|
+
if (audio.length <= 44) continue;
|
|
129
|
+
const result = await transcribe(audio, sttConfig, {
|
|
130
|
+
mime: "audio/wav",
|
|
131
|
+
fileName: `discussion-recovery-${index}-${gapIndex}.wav`
|
|
132
|
+
});
|
|
133
|
+
if (result.segments?.length) for (const part of result.segments) {
|
|
134
|
+
if (!Number.isFinite(part.startMs) || !Number.isFinite(part.endMs) || part.startMs < 0 || part.endMs <= part.startMs || part.endMs > gap.endedAtMs - gap.startedAtMs + 100) throw new Error("Provider returned an invalid transcript time range");
|
|
135
|
+
segments.push({
|
|
136
|
+
text: part.text,
|
|
137
|
+
startedAtMs: gap.startedAtMs + part.startMs,
|
|
138
|
+
endedAtMs: Math.min(gap.endedAtMs, gap.startedAtMs + part.endMs),
|
|
139
|
+
...part.speaker ? { speakerLabel: `${index + 1}:${gapIndex + 1}:${part.speaker}` } : {}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else if (result.text.trim()) segments.push({
|
|
143
|
+
text: result.text,
|
|
144
|
+
...gap
|
|
145
|
+
});
|
|
146
|
+
language ??= result.language;
|
|
147
|
+
}
|
|
148
|
+
offset += durationMs;
|
|
101
149
|
});
|
|
150
|
+
segments.sort((a, b) => a.startedAtMs - b.startedAtMs);
|
|
151
|
+
replaceTranscriptSegments(capture.id, segments);
|
|
102
152
|
return {
|
|
103
|
-
text,
|
|
153
|
+
text: assembleDiscussionTranscript(listDiscussionTranscriptSegments(capture.id)),
|
|
104
154
|
...language ? { language } : {}
|
|
105
155
|
};
|
|
106
156
|
}
|