@xopcai/xopc 0.0.230 → 0.0.232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/feishu/src/schema/config-schema.d.ts +3 -3
- package/dist/extensions/telegram/src/config-schema.d.ts +4 -4
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/extensions/weixin/src/config-schema.d.ts +3 -3
- package/dist/gateway/static/root/assets/{activity-store-M0nAY5XB.js → activity-store-VXCWz43N.js} +1 -1
- package/dist/gateway/static/root/assets/agent-avatar-dicebear-BXRaZYW8.js +1 -0
- package/dist/gateway/static/root/assets/agent-defaults-settings-panel-dR7HMaRX.js +1 -0
- package/dist/gateway/static/root/assets/agents-Bileycwe.js +1 -0
- package/dist/gateway/static/root/assets/api-wGPgL8xY.js +1 -0
- package/dist/gateway/static/root/assets/{app-management-settings-panel-CwAPK4Jm.js → app-management-settings-panel-BJfClfPx.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-DsGLvqTb.js → appearance-settings-C_R0vUAu.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-Ee9qOQEx.js → apps-page-CBow8pBI.js} +1 -1
- package/dist/gateway/static/root/assets/{archive-plugin-7_DXaMTP.js → archive-plugin-DnFHkSHE.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-B4qqigAQ.js → attachment-load-DZrEwytM.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-B8ImehSW.js → automations-page-DpkKajui.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace-BBntVWgv.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-DaJbyYc2.js → binary-plugins-Ckc_Csue.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-Kl25XWJc.js → block-editor-DAC6w-iV.js} +4 -4
- package/dist/gateway/static/root/assets/browser-settings-page-DrsMd-Po.js +8 -0
- package/dist/gateway/static/root/assets/browser-workflow-inputs-BGd7R8GO.js +1 -0
- package/dist/gateway/static/root/assets/{browser-workflows-page-DjMxs33B.js → browser-workflows-page-DUfj7ntM.js} +2 -2
- package/dist/gateway/static/root/assets/capabilities-settings-panel-97gkGeSV.js +7 -0
- package/dist/gateway/static/root/assets/channels-settings-CuBsz8UI.js +1 -0
- package/dist/gateway/static/root/assets/{channel-recipient-api-C5ABr73b.js → channels-status-swr-CfboECLt.js} +1 -1
- package/dist/gateway/static/root/assets/{chat-terminal-dock-BiU_4Rq6.js → chat-terminal-dock-CA9fndbZ.js} +2 -2
- package/dist/gateway/static/root/assets/cloud-C4-yfnnk.js +1 -0
- package/dist/gateway/static/root/assets/{code-editor-dFzqyDXS.js → code-editor-D4_k-6QN.js} +1 -1
- package/dist/gateway/static/root/assets/connectors-page-SuL7td7Z.js +2 -0
- package/dist/gateway/static/root/assets/cron-expression-CqPu7HYJ.js +2 -0
- package/dist/gateway/static/root/assets/date-picker-C8dJsCgk.js +1 -0
- package/dist/gateway/static/root/assets/dependency-picker-Iz2wYimy.js +1 -0
- package/dist/gateway/static/root/assets/{desktop-pet-ux9DxchT.js → desktop-pet-C3h40FmG.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-BVVCBgYK.js → desktop-pet-settings-YfeVVzLe.js} +1 -1
- package/dist/gateway/static/root/assets/dist-DqPZM2MH2.js +1 -0
- package/dist/gateway/static/root/assets/{extension-debug-page-Dh6DDuvQ.js → extension-debug-page-CChgztz_.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-8OKQDEat.js → extension-page-DF6ct6_y.js} +1 -1
- package/dist/gateway/static/root/assets/extension-settings-page-jgBsjn0F.js +1 -0
- package/dist/gateway/static/root/assets/files-Dp8_uGTO.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-config-api-Dlc6eaTG.js → gateway-config-api-CVajuIGL.js} +1 -1
- package/dist/gateway/static/root/assets/gateway-config-swr-BGQn8duh.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-settings-DqQgiAUv.js → gateway-settings-BbssNGid.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-startup-retry-B_zrtccP.js → gateway-startup-retry-D2Rnh9fX.js} +1 -1
- package/dist/gateway/static/root/assets/heartbeat-settings-CJtXu8iD.js +1 -0
- package/dist/gateway/static/root/assets/home-page-Be1o1AXQ.js +1 -0
- package/dist/gateway/static/root/assets/html-workspace-editor-C3DKAkMc.js +1 -0
- package/dist/gateway/static/root/assets/{image-generation-api-YKP2me5Z.js → image-generation-api-D7W2p5_8.js} +1 -1
- package/dist/gateway/static/root/assets/index-B2OJSXX9.js +70 -0
- package/dist/gateway/static/root/assets/index-rHjAElxG.css +2 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-BSFNqxzY.js → keyboard-shortcuts-settings-BvHYyNus.js} +1 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-COfelgk7.js +2 -0
- package/dist/gateway/static/root/assets/{local-apps-page-DzU6yZ9-.js → local-apps-page-Cb8t2wWM.js} +1 -1
- package/dist/gateway/static/root/assets/locale-store-DLwVxS3V.js +1 -0
- package/dist/gateway/static/root/assets/logs-page-DA8oDjeB.js +2 -0
- package/dist/gateway/static/root/assets/management-settings-B8Jie4-J.js +1 -0
- package/dist/gateway/static/root/assets/markdown-editor-nDLmA_LF.js +2 -0
- package/dist/gateway/static/root/assets/markdown-split-BD1r4o2i.js +1 -0
- package/dist/gateway/static/root/assets/{markdown-view-DvM5PWEH.js → markdown-view-B-BHEcNd.js} +1 -1
- package/dist/gateway/static/root/assets/markdown-view-B1bGSQqt.css +1 -0
- package/dist/gateway/static/root/assets/{media-plugins-B8vMe5VX.js → media-plugins-BCev8fkW.js} +1 -1
- package/dist/gateway/static/root/assets/messages-Dp1OxSKt.js +3 -0
- package/dist/gateway/static/root/assets/mobile-device-api-DACoHhhk.js +1 -0
- package/dist/gateway/static/root/assets/models-hub-cache-C8zjqmIv.js +1 -0
- package/dist/gateway/static/root/assets/{notes-api-BFsOGYkd.js → notes-api-DEdNKw0I.js} +1 -1
- package/dist/gateway/static/root/assets/{notes-page-B-YZ7iuK.js → notes-page-BBTLjJwa.js} +1 -1
- package/dist/gateway/static/root/assets/notes-workbench-DICgUGPv.js +4 -0
- package/dist/gateway/static/root/assets/oauth-api-BNCO2YK5.js +1 -0
- package/dist/gateway/static/root/assets/popover-select-3E24E65N.js +1 -0
- package/dist/gateway/static/root/assets/{product-open-page-DkWcyki5.js → product-open-page-fEBbRekH.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-CqdkznYJ.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-dHDXpMCg.js +1 -0
- package/dist/gateway/static/root/assets/{registry-api-BFZrVYfy.js → registry-api-Cvr4Qm7N.js} +1 -1
- package/dist/gateway/static/root/assets/remote-access-hub-2feZudJw.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-Bq_ETeal.js → runtime-tools-settings-panel-BT5B74IH.js} +2 -2
- package/dist/gateway/static/root/assets/{schema-form-BogTlrsP.js → schema-form-C4tB34S5.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-CO2JCFdy.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-DxMVaqdn.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-Coy1Cbp6.js → settings-form-section-D1oXhkgI.js} +1 -1
- package/dist/gateway/static/root/assets/settings-loading-skeleton-CldfX6FJ.js +1 -0
- package/dist/gateway/static/root/assets/settings-page-c3LJnbvE.js +1 -0
- package/dist/gateway/static/root/assets/settings-shell-layer-context-DCp6ykjQ.js +1 -0
- package/dist/gateway/static/root/assets/setup-status-panel-DZNHb6A5.js +1 -0
- package/dist/gateway/static/root/assets/share-preview-page-DptCK_mk.js +2 -0
- package/dist/gateway/static/root/assets/shares-settings-DakZ2AGN.js +1 -0
- package/dist/gateway/static/root/assets/skill-reload-api-BOiNkWJC.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-Brxb6QnW.js → skills-page-OHBeBtSg.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-FB5LtEGm.js → skills-page.utils-B9WZbOLC.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-ChosWt8n.js → system-settings-panel-Dj_DpOGw.js} +1 -1
- package/dist/gateway/static/root/assets/task-detail-page-D5Nbe3C8.js +1 -0
- package/dist/gateway/static/root/assets/theme-store-Cpd3Gd5R.js +1 -0
- package/dist/gateway/static/root/assets/{use-autosave-2SqnmSKD.js → use-autosave-DCl5Yq7N.js} +1 -1
- package/dist/gateway/static/root/assets/user-context-page-CAPSUK8B.js +1 -0
- package/dist/gateway/static/root/assets/{vendor-codemirror-DRr7hFaD.js → vendor-codemirror--gTq3YAI.js} +16 -16
- package/dist/gateway/static/root/assets/voice-api-key-field-BB_ZQV8F.js +1 -0
- package/dist/gateway/static/root/assets/voice-config-api-BNiYmpl5.js +1 -0
- package/dist/gateway/static/root/assets/{work-discovery-overlay-CK3aZDdE.js → work-discovery-overlay-BrIdtdum.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-BjOuddkX.js → workflow-run-setup-panel-hKh-uCLo.js} +2 -2
- package/dist/gateway/static/root/assets/workflows-page-ahDvypGO.js +5 -0
- package/dist/gateway/static/root/index.html +19 -15
- package/dist/package.js +1 -1
- package/dist/src/agent/agent-manager.d.ts +1 -2
- package/dist/src/agent/agent-manager.js +29 -44
- package/dist/src/agent/background-review/run-background-review.js +10 -15
- package/dist/src/agent/embedded/run-for-session.js +1 -1
- package/dist/src/agent/external-tools/mcp-provider.js +3 -13
- package/dist/src/agent/image/generation/runtime.js +1 -1
- package/dist/src/agent/image/tool-model-config.js +13 -14
- package/dist/src/agent/mcp/bundle-mcp-policy.d.ts +0 -9
- package/dist/src/agent/mcp/bundle-mcp-policy.js +1 -13
- package/dist/src/agent/mcp/bundle-mcp-runtime.js +1 -1
- package/dist/src/agent/mcp/index.js +2 -2
- package/dist/src/agent/media-generation/runtime-shared.d.ts +1 -1
- package/dist/src/agent/media-generation/runtime-shared.js +1 -1
- package/dist/src/agent/memory/create-memory-manager.js +1 -1
- package/dist/src/agent/memory/manager.d.ts +1 -1
- package/dist/src/{agent-runtime/memory-runtime.d.ts → agent/memory/runtime.d.ts} +1 -1
- package/dist/src/{agent-runtime/memory-runtime.js → agent/memory/runtime.js} +2 -3
- package/dist/src/agent/orchestration/agent-orchestrator.d.ts +0 -3
- package/dist/src/agent/orchestration/agent-orchestrator.js +1 -3
- package/dist/src/agent/orchestration/run-agent-turn-with-timeout.js +2 -2
- package/dist/src/agent/service.d.ts +1 -0
- package/dist/src/agent/service.js +5 -3
- package/dist/src/agent/skills/skill-manage-ops.js +1 -1
- package/dist/src/agent/starter-agents.js +1 -8
- package/dist/src/agent/tools/executor.d.ts +1 -1
- package/dist/src/agent/tools/workflow-tool.js +1 -5
- package/dist/src/agent/tools/write.js +9 -12
- package/dist/src/agent/tools/xopc-use-tool.js +2 -2
- package/dist/src/agent-config/index.d.ts +2 -0
- package/dist/src/agent-config/index.js +11 -0
- package/dist/src/agent-config/resolver.d.ts +11 -0
- package/dist/src/agent-config/resolver.js +113 -0
- package/dist/src/agent-config/schema.d.ts +491 -0
- package/dist/src/agent-config/schema.js +172 -0
- package/dist/src/auth/oauth/index.js +2 -2
- package/dist/src/auth/oauth/registry.d.ts +1 -1
- package/dist/src/auth/oauth/registry.js +5 -1
- package/dist/src/auth/oauth/xopc-cloud.d.ts +1 -0
- package/dist/src/auth/oauth/xopc-cloud.js +8 -2
- 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/config-paths.js +1 -1
- package/dist/src/cli/commands/agents.js +2 -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 +12 -4
- package/dist/src/cli/commands/image.js +3 -4
- package/dist/src/cli/commands/models.js +9 -3
- package/dist/src/cli/commands/onboard/model.js +12 -11
- package/dist/src/cli/commands/onboard.js +1 -1
- package/dist/src/cli/commands/tunnel.js +3 -18
- package/dist/src/cli/utils/init-workspace-core.d.ts +1 -1
- package/dist/src/cli/utils/init-workspace-core.js +1 -1
- package/dist/src/commands/agents.config.d.ts +1 -3
- package/dist/src/commands/agents.config.js +9 -26
- package/dist/src/config/agent-model-intents.d.ts +10 -0
- package/dist/src/config/agent-model-intents.js +39 -0
- package/dist/src/config/agent-profile.d.ts +7 -9
- package/dist/src/config/agent-profile.js +30 -28
- package/dist/src/config/default-model.d.ts +1 -11
- package/dist/src/config/default-model.js +3 -11
- package/dist/src/config/index.js +3 -3
- package/dist/src/config/integration.d.ts +1 -1
- package/dist/src/config/reload.js +1 -1
- package/dist/src/config/rules.js +3 -3
- package/dist/src/config/schema.d.ts +187 -452
- package/dist/src/config/schema.js +53 -131
- package/dist/src/extensions/activation-context.js +7 -11
- package/dist/src/files/file-service.d.ts +36 -0
- package/dist/src/files/file-service.js +282 -0
- package/dist/src/gateway/agent-builtin-tools.d.ts +250 -6
- package/dist/src/gateway/agent-builtin-tools.js +284 -46
- package/dist/src/gateway/agents-admin.d.ts +20 -62
- package/dist/src/gateway/agents-admin.js +36 -228
- package/dist/src/gateway/device-routes.d.ts +3 -0
- package/dist/src/gateway/device-routes.js +47 -0
- package/dist/src/gateway/global-defaults-admin.d.ts +6 -20
- package/dist/src/gateway/global-defaults-admin.js +20 -120
- package/dist/src/gateway/heartbeat/service.js +1 -1
- package/dist/src/gateway/hono/app.js +6 -4
- package/dist/src/gateway/hono/lib/config-payload.d.ts +54 -137
- package/dist/src/gateway/hono/lib/config-payload.js +2 -4
- package/dist/src/gateway/hono/middleware/auth.js +33 -1
- package/dist/src/gateway/hono/middleware/scopes.d.ts +2 -12
- package/dist/src/gateway/hono/middleware/scopes.js +9 -18
- package/dist/src/gateway/hono/routes/agents.js +64 -100
- package/dist/src/gateway/hono/routes/connectors.js +1 -1
- package/dist/src/gateway/hono/routes/device-push.d.ts +2 -0
- package/dist/src/gateway/hono/routes/{mobile.js → device-push.js} +40 -18
- package/dist/src/gateway/hono/routes/devices.d.ts +4 -0
- package/dist/src/gateway/hono/routes/devices.js +214 -0
- package/dist/src/gateway/hono/routes/files.d.ts +3 -0
- package/dist/src/gateway/hono/routes/files.js +176 -0
- package/dist/src/gateway/hono/routes/global-defaults.js +5 -9
- package/dist/src/gateway/hono/routes/index.js +4 -2
- package/dist/src/gateway/hono/routes/lazy-bundles.js +12 -20
- package/dist/src/gateway/hono/routes/models.js +2 -2
- package/dist/src/gateway/hono/routes/projects.js +0 -508
- package/dist/src/gateway/hono/routes/realtime.js +8 -1
- package/dist/src/gateway/hono/routes/shares.js +410 -21
- package/dist/src/gateway/hono/routes/status.js +9 -16
- package/dist/src/gateway/hono/routes/tunnel.d.ts +0 -2
- package/dist/src/gateway/hono/routes/tunnel.js +10 -199
- package/dist/src/gateway/hono/routes/voice.js +1 -1
- package/dist/src/gateway/hono/routes/workspace.js +15 -974
- package/dist/src/gateway/image-generation-setup.d.ts +2 -2
- package/dist/src/gateway/image-generation-setup.js +8 -7
- package/dist/src/gateway/public-url.d.ts +1 -1
- package/dist/src/gateway/security/gateway-principal.d.ts +11 -0
- package/dist/src/gateway/security/gateway-principal.js +12 -0
- package/dist/src/gateway/security/gateway-scopes.d.ts +7 -0
- package/dist/src/gateway/security/gateway-scopes.js +66 -0
- package/dist/src/gateway/security/index.d.ts +2 -1
- package/dist/src/gateway/security/index.js +3 -2
- package/dist/src/gateway/service.js +4 -4
- package/dist/src/gateway/xopc-cloud-capability-setup.js +19 -21
- package/dist/src/heartbeat/index.js +1 -1
- package/dist/src/migrations/index.d.ts +0 -1
- package/dist/src/migrations/index.js +1 -2
- package/dist/src/migrations/runner.js +1 -2
- package/dist/src/notifications/device-store.d.ts +1 -1
- package/dist/src/notifications/device-store.js +11 -11
- package/dist/src/notifications/store.js +3 -3
- package/dist/src/providers/index.js +1 -1
- package/dist/src/providers/model-reference-auditor.js +14 -9
- package/dist/src/realtime/runtime.d.ts +2 -0
- package/dist/src/realtime/runtime.js +27 -3
- package/dist/src/realtime/tickets.d.ts +6 -1
- package/dist/src/realtime/tickets.js +2 -1
- package/dist/src/remote-access/exposure-manager.js +3 -3
- package/dist/src/share/hosted-session-share.d.ts +104 -0
- package/dist/src/share/hosted-session-share.js +276 -0
- package/dist/src/share/note-share-service.js +4 -24
- package/dist/src/share/session-share-projector.d.ts +31 -0
- package/dist/src/share/session-share-projector.js +79 -0
- package/dist/src/share/session-share-service.d.ts +89 -0
- package/dist/src/share/session-share-service.js +325 -0
- package/dist/src/share/share-asset-ticket.d.ts +7 -0
- package/dist/src/share/share-asset-ticket.js +30 -0
- package/dist/src/share/share-landing.d.ts +4 -3
- package/dist/src/share/share-landing.js +5 -5
- package/dist/src/share/share-store.d.ts +27 -2
- package/dist/src/share/share-store.js +71 -8
- package/dist/src/share/share-types.d.ts +17 -2
- package/dist/src/share/share-url.js +2 -2
- package/dist/src/storage/sqlite/device-access-repository.d.ts +54 -0
- package/dist/src/storage/sqlite/device-access-repository.js +194 -0
- package/dist/src/storage/sqlite/device-pairing-repository.d.ts +22 -0
- package/dist/src/storage/sqlite/device-pairing-repository.js +86 -0
- package/dist/src/storage/sqlite/gateway-identity-repository.d.ts +10 -0
- package/dist/src/storage/sqlite/gateway-identity-repository.js +50 -0
- package/dist/src/storage/sqlite/index.d.ts +3 -0
- package/dist/src/storage/sqlite/index.js +4 -1
- package/dist/src/storage/sqlite/migrations/138_device_access.sql +96 -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/tui/backends/embedded-backend.js +1 -1
- package/dist/src/tui/backends/gateway-realtime-backend.js +11 -3
- package/dist/src/tunnel/consent.d.ts +1 -1
- package/dist/src/tunnel/consent.js +1 -1
- package/dist/src/tunnel/gateway-lifecycle.js +1 -1
- package/dist/src/tunnel/index.d.ts +1 -4
- package/dist/src/tunnel/index.js +3 -5
- package/dist/src/tunnel/tunnel-audit.d.ts +1 -1
- package/dist/src/tunnel/tunnel-config.js +1 -1
- package/dist/src/tunnel/tunnel-service.d.ts +2 -3
- package/dist/src/tunnel/tunnel-service.js +1 -20
- package/dist/src/tunnel/tunnel-types.d.ts +0 -6
- package/dist/src/workflows/draft/workflow-draft-service.js +2 -2
- package/dist/src/workflows/service/workflow-run-service.js +3 -3
- package/package.json +3 -3
- package/dist/gateway/static/root/assets/agent-avatar-dicebear-Bp_wmuGW.js +0 -1
- package/dist/gateway/static/root/assets/agents-CKG93dlc.js +0 -68
- package/dist/gateway/static/root/assets/api-BywYxpaS.js +0 -1
- package/dist/gateway/static/root/assets/automations-workspace-BNFAfNwC.js +0 -6
- package/dist/gateway/static/root/assets/browser-settings-page-Cg6NgwE_.js +0 -8
- package/dist/gateway/static/root/assets/browser-workflow-inputs-8NC2qDRF.js +0 -1
- package/dist/gateway/static/root/assets/capabilities-settings-panel-NvJKgF1u.js +0 -7
- package/dist/gateway/static/root/assets/capability-presets-api-o0y9WgAI.js +0 -1
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-C2uBdALe.js +0 -3
- package/dist/gateway/static/root/assets/channels-settings-CIcYKMEs.js +0 -1
- package/dist/gateway/static/root/assets/channels-status-swr-38FsOnR0.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-CEEcmUiP.js +0 -2
- package/dist/gateway/static/root/assets/cron-expression-Cs_5pUPT.js +0 -2
- package/dist/gateway/static/root/assets/date-picker-y1kWrFVU.js +0 -1
- package/dist/gateway/static/root/assets/dependency-picker-skc62zrn.js +0 -1
- package/dist/gateway/static/root/assets/directory-picker-path-field-V8vZP0Dy.js +0 -1
- package/dist/gateway/static/root/assets/dist-J_ySZvz0.js +0 -1
- package/dist/gateway/static/root/assets/extension-settings-page-BgSV0Nny.js +0 -1
- package/dist/gateway/static/root/assets/file-code-corner-byQA1VfD.js +0 -1
- package/dist/gateway/static/root/assets/gateway-config-swr-BWGjnmDl.js +0 -1
- package/dist/gateway/static/root/assets/heartbeat-settings-Pxqk5GFH.js +0 -1
- package/dist/gateway/static/root/assets/home-page-1xjo6wE4.js +0 -1
- package/dist/gateway/static/root/assets/html-workspace-editor-bXK5tq64.js +0 -1
- package/dist/gateway/static/root/assets/index-Bbd_dXui.js +0 -70
- package/dist/gateway/static/root/assets/index-D_hrEKXM.css +0 -2
- package/dist/gateway/static/root/assets/local-app-workbench-page-heQ4-PJ7.js +0 -2
- package/dist/gateway/static/root/assets/locale-store-CXhjfe4q.js +0 -1
- package/dist/gateway/static/root/assets/logs-page-BblnS6Jy.js +0 -2
- package/dist/gateway/static/root/assets/management-settings-B8hM56HS.js +0 -1
- package/dist/gateway/static/root/assets/markdown-editor-TWGaF7Xa.js +0 -2
- package/dist/gateway/static/root/assets/markdown-split-DRbZjxMK.js +0 -1
- package/dist/gateway/static/root/assets/markdown-view-CLzTdVLW.css +0 -1
- package/dist/gateway/static/root/assets/messages-BMdD0LWk.js +0 -3
- package/dist/gateway/static/root/assets/notes-workbench-CAM09C8b.js +0 -4
- package/dist/gateway/static/root/assets/popover-select-CccGri5Y.js +0 -1
- package/dist/gateway/static/root/assets/project-detail-page-BIPIwgU2.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-B_kzTn1K.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-CtD8DsD-.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-B8n9HI1y.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-xlhdRTME.js +0 -1
- package/dist/gateway/static/root/assets/settings-loading-skeleton-B5GU7iCG.js +0 -1
- package/dist/gateway/static/root/assets/settings-page-Dd-BRu12.js +0 -1
- package/dist/gateway/static/root/assets/setup-status-panel-CwEFAVWx.js +0 -1
- package/dist/gateway/static/root/assets/share-preview-page-DzXw9UQJ.js +0 -2
- package/dist/gateway/static/root/assets/shares-settings-Bk6UQOwS.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-DrmQZ3Au.js +0 -1
- package/dist/gateway/static/root/assets/task-detail-page-CW2sJbdc.js +0 -1
- package/dist/gateway/static/root/assets/theme-store-DOdCjyuw.js +0 -1
- package/dist/gateway/static/root/assets/typed-models-lib-q5-Vfqd8.js +0 -1
- package/dist/gateway/static/root/assets/user-context-page-Bq9KB-Oj.js +0 -1
- package/dist/gateway/static/root/assets/voice-api-key-field-DtTIF6M8.js +0 -1
- package/dist/gateway/static/root/assets/workflows-page-CcbGEQol.js +0 -5
- package/dist/src/agent-manifest/index.d.ts +0 -5
- package/dist/src/agent-manifest/index.js +0 -10
- package/dist/src/agent-manifest/preset-chain.d.ts +0 -10
- package/dist/src/agent-manifest/preset-chain.js +0 -30
- package/dist/src/agent-manifest/prompt.d.ts +0 -2
- package/dist/src/agent-manifest/prompt.js +0 -49
- package/dist/src/agent-manifest/resolver.d.ts +0 -32
- package/dist/src/agent-manifest/resolver.js +0 -114
- package/dist/src/agent-manifest/schema.d.ts +0 -695
- package/dist/src/agent-manifest/schema.js +0 -226
- package/dist/src/agent-manifest/validator.d.ts +0 -25
- package/dist/src/agent-manifest/validator.js +0 -74
- package/dist/src/agent-runtime/boundary-guard.d.ts +0 -12
- package/dist/src/agent-runtime/boundary-guard.js +0 -21
- package/dist/src/agent-runtime/index.d.ts +0 -7
- package/dist/src/agent-runtime/index.js +0 -8
- package/dist/src/agent-runtime/model-router.d.ts +0 -11
- package/dist/src/agent-runtime/model-router.js +0 -14
- package/dist/src/agent-runtime/run-trace.d.ts +0 -27
- package/dist/src/agent-runtime/run-trace.js +0 -51
- package/dist/src/agent-runtime/runtime-profile.d.ts +0 -26
- package/dist/src/agent-runtime/runtime-profile.js +0 -42
- package/dist/src/agent-runtime/tool-registry.d.ts +0 -28
- package/dist/src/agent-runtime/tool-registry.js +0 -50
- package/dist/src/agent-runtime/workflow-runtime.d.ts +0 -31
- package/dist/src/agent-runtime/workflow-runtime.js +0 -32
- package/dist/src/config/agent-typed-models.d.ts +0 -13
- package/dist/src/config/agent-typed-models.js +0 -48
- package/dist/src/gateway/capability-presets-admin.d.ts +0 -88
- package/dist/src/gateway/capability-presets-admin.js +0 -303
- package/dist/src/gateway/file-path-classifier.d.ts +0 -30
- package/dist/src/gateway/file-path-classifier.js +0 -105
- package/dist/src/gateway/file-reference-errors.d.ts +0 -6
- package/dist/src/gateway/file-reference-errors.js +0 -21
- package/dist/src/gateway/file-reference-registry.d.ts +0 -34
- package/dist/src/gateway/file-reference-registry.js +0 -45
- package/dist/src/gateway/hono/lib/agent-model.d.ts +0 -35
- package/dist/src/gateway/hono/lib/agent-model.js +0 -97
- package/dist/src/gateway/hono/routes/capability-presets.d.ts +0 -3
- package/dist/src/gateway/hono/routes/capability-presets.js +0 -187
- package/dist/src/gateway/hono/routes/mobile.d.ts +0 -3
- package/dist/src/gateway/security/operator-scopes.d.ts +0 -37
- package/dist/src/gateway/security/operator-scopes.js +0 -147
- package/dist/src/migrations/config/default-capability-preset.d.ts +0 -10
- package/dist/src/migrations/config/default-capability-preset.js +0 -133
- package/dist/src/tunnel/enable-lan-pairing.d.ts +0 -8
- package/dist/src/tunnel/enable-lan-pairing.js +0 -41
- package/dist/src/tunnel/pair-context.d.ts +0 -36
- package/dist/src/tunnel/pair-context.js +0 -77
- package/dist/src/tunnel/pair-url.d.ts +0 -43
- package/dist/src/tunnel/pair-url.js +0 -117
- package/dist/src/tunnel/pairing-rate-limit.d.ts +0 -12
- package/dist/src/tunnel/pairing-rate-limit.js +0 -28
- package/dist/src/tunnel/pairing.d.ts +0 -21
- package/dist/src/tunnel/pairing.js +0 -103
- package/dist/src/tunnel/tunnel-qr.d.ts +0 -9
- package/dist/src/tunnel/tunnel-qr.js +0 -29
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { resolveStateDir } from "../config/paths-state.js";
|
|
2
|
+
import { createLogger } from "../utils/logger/index.js";
|
|
3
|
+
import { init_logger } from "../utils/logger.js";
|
|
4
|
+
import { init_paths } from "../config/paths.js";
|
|
5
|
+
import { resolveMediaReference } from "../media/media-reference.js";
|
|
6
|
+
import { projectSessionShare } from "./session-share-projector.js";
|
|
7
|
+
import { issueShareAssetTicket, verifyShareAssetTicket } from "./share-asset-ticket.js";
|
|
8
|
+
import { basename, join } from "node:path";
|
|
9
|
+
import { createReadStream } from "node:fs";
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import { copyFile, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
12
|
+
//#region src/share/session-share-service.ts
|
|
13
|
+
init_paths();
|
|
14
|
+
init_logger();
|
|
15
|
+
const log = createLogger("SessionShareService");
|
|
16
|
+
const MANIFEST_SCHEMA_VERSION = 1;
|
|
17
|
+
const MAX_SHARE_MESSAGES = 1e4;
|
|
18
|
+
const MAX_MANIFEST_BYTES = 10 * 1024 * 1024;
|
|
19
|
+
const MAX_ATTACHMENT_COUNT = 20;
|
|
20
|
+
const MAX_ATTACHMENT_BYTES = 32 * 1024 * 1024;
|
|
21
|
+
const MAX_TOTAL_ATTACHMENT_BYTES = 100 * 1024 * 1024;
|
|
22
|
+
const ASSET_TICKET_TTL_MS = 10 * 6e4;
|
|
23
|
+
var SessionShareSnapshotConflictError = class extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("Session changed after the share preview was reviewed");
|
|
26
|
+
this.name = "SessionShareSnapshotConflictError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var SessionShareService = class {
|
|
30
|
+
constructor(store, source) {
|
|
31
|
+
this.store = store;
|
|
32
|
+
this.source = source;
|
|
33
|
+
}
|
|
34
|
+
async preview(sessionKey) {
|
|
35
|
+
const source = await this.loadSource(sessionKey);
|
|
36
|
+
const projection = projectSessionShare(source.snapshot.entries);
|
|
37
|
+
this.validateMessages(projection.messages);
|
|
38
|
+
return {
|
|
39
|
+
sessionId: source.snapshot.sessionId,
|
|
40
|
+
cutoffSeq: source.snapshot.lastSeq,
|
|
41
|
+
metadataUpdatedAt: source.metadata.updatedAt,
|
|
42
|
+
title: source.metadata.name?.trim() || "Shared conversation",
|
|
43
|
+
snapshotAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
44
|
+
messageCount: projection.messages.length,
|
|
45
|
+
messages: projection.messages,
|
|
46
|
+
toolActivities: projection.toolActivities,
|
|
47
|
+
attachmentCandidates: projection.attachmentCandidates
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async create(sessionKey, input) {
|
|
51
|
+
const source = await this.loadExpectedSource(sessionKey, input);
|
|
52
|
+
const id = randomUUID();
|
|
53
|
+
const projection = projectSessionShare(source.snapshot.entries);
|
|
54
|
+
const includeToolActivities = input.includeToolActivities === true;
|
|
55
|
+
const manifestBase = this.buildManifestBase(id, source, projection, {
|
|
56
|
+
includeToolActivities,
|
|
57
|
+
selectedAttachmentIds: input.attachmentIds ?? []
|
|
58
|
+
});
|
|
59
|
+
const built = await this.createArtifact(id, manifestBase, projection, input.attachmentIds ?? []);
|
|
60
|
+
try {
|
|
61
|
+
return this.store.createSessionShare({
|
|
62
|
+
id,
|
|
63
|
+
sourceSessionId: built.manifest.source.sessionId,
|
|
64
|
+
cutoffSeq: built.manifest.source.cutoffSeq,
|
|
65
|
+
artifactRelativePath: this.artifactRelativePath(id),
|
|
66
|
+
artifactSize: built.totalSize,
|
|
67
|
+
messageCount: built.manifest.messages.length,
|
|
68
|
+
attachmentCount: built.manifest.attachments.length,
|
|
69
|
+
includeToolActivities,
|
|
70
|
+
fileName: built.manifest.title,
|
|
71
|
+
ttlMs: input.ttlMs,
|
|
72
|
+
maxViews: input.maxViews,
|
|
73
|
+
description: input.description,
|
|
74
|
+
gatewayTokenHash: input.gatewayTokenHash
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
await this.removeArtifact(id);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async refresh(sessionKey, shareId, input) {
|
|
82
|
+
const record = this.store.getById(shareId);
|
|
83
|
+
if (!record || record.kind !== "session") throw new Error("Session share not found");
|
|
84
|
+
const source = await this.loadExpectedSource(sessionKey, input);
|
|
85
|
+
if (source.snapshot.sessionId !== record.sourceSessionId) throw new Error("Session share belongs to a previous session");
|
|
86
|
+
const previous = await this.readManifest(record);
|
|
87
|
+
const includeToolActivities = input.includeToolActivities ?? record.includeToolActivities;
|
|
88
|
+
const attachmentIds = input.attachmentIds ?? previous.attachments.map((attachment) => attachment.id);
|
|
89
|
+
const projection = projectSessionShare(source.snapshot.entries);
|
|
90
|
+
const manifestBase = this.buildManifestBase(shareId, source, projection, {
|
|
91
|
+
includeToolActivities,
|
|
92
|
+
selectedAttachmentIds: attachmentIds
|
|
93
|
+
});
|
|
94
|
+
const finalDir = this.artifactDir(shareId);
|
|
95
|
+
const stagedDir = `${finalDir}.refresh-${randomUUID()}`;
|
|
96
|
+
const backupDir = `${finalDir}.backup-${randomUUID()}`;
|
|
97
|
+
const built = await this.writeArtifact(stagedDir, manifestBase, projection, attachmentIds);
|
|
98
|
+
let movedOld = false;
|
|
99
|
+
try {
|
|
100
|
+
await rename(finalDir, backupDir);
|
|
101
|
+
movedOld = true;
|
|
102
|
+
await rename(stagedDir, finalDir);
|
|
103
|
+
const updated = this.store.updateSessionSnapshot(shareId, {
|
|
104
|
+
cutoffSeq: built.manifest.source.cutoffSeq,
|
|
105
|
+
artifactSize: built.totalSize,
|
|
106
|
+
messageCount: built.manifest.messages.length,
|
|
107
|
+
attachmentCount: built.manifest.attachments.length,
|
|
108
|
+
includeToolActivities,
|
|
109
|
+
fileName: built.manifest.title
|
|
110
|
+
});
|
|
111
|
+
if (!updated) throw new Error("Session share not found");
|
|
112
|
+
await rm(backupDir, {
|
|
113
|
+
recursive: true,
|
|
114
|
+
force: true
|
|
115
|
+
}).catch((err) => {
|
|
116
|
+
log.warn({
|
|
117
|
+
err,
|
|
118
|
+
shareId
|
|
119
|
+
}, "Failed to remove previous Session share artifact");
|
|
120
|
+
});
|
|
121
|
+
return updated;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
await rm(stagedDir, {
|
|
124
|
+
recursive: true,
|
|
125
|
+
force: true
|
|
126
|
+
});
|
|
127
|
+
if (movedOld) {
|
|
128
|
+
await rm(finalDir, {
|
|
129
|
+
recursive: true,
|
|
130
|
+
force: true
|
|
131
|
+
});
|
|
132
|
+
await rename(backupDir, finalDir).catch(() => void 0);
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
list(sessionId) {
|
|
138
|
+
return this.store.getSessionShares(sessionId);
|
|
139
|
+
}
|
|
140
|
+
async readManifest(record) {
|
|
141
|
+
const path = join(resolveStateDir(), record.artifactRelativePath, "manifest.json");
|
|
142
|
+
const file = await stat(path);
|
|
143
|
+
if (!file.isFile() || file.size > MAX_MANIFEST_BYTES) throw new Error("Invalid Session share artifact");
|
|
144
|
+
const manifest = JSON.parse(await readFile(path, "utf8"));
|
|
145
|
+
if (manifest.schemaVersion !== MANIFEST_SCHEMA_VERSION || manifest.shareId !== record.id || typeof manifest.title !== "string" || typeof manifest.snapshotAt !== "string" || !manifest.source || manifest.source.sessionId !== record.sourceSessionId || manifest.source.cutoffSeq !== record.cutoffSeq || !Array.isArray(manifest.messages) || !manifest.messages.every(isSessionShareMessage) || !Array.isArray(manifest.toolActivities) || !manifest.toolActivities.every(isSessionShareToolActivity) || !Array.isArray(manifest.attachments) || !manifest.attachments.every(isSessionShareAttachment)) throw new Error("Invalid Session share artifact");
|
|
146
|
+
this.validateMessages(manifest.messages);
|
|
147
|
+
this.validateManifestRelations(record, manifest);
|
|
148
|
+
return manifest;
|
|
149
|
+
}
|
|
150
|
+
issueAssetTicket(record) {
|
|
151
|
+
return issueShareAssetTicket(record, ASSET_TICKET_TTL_MS);
|
|
152
|
+
}
|
|
153
|
+
verifyAssetTicket(record, ticket) {
|
|
154
|
+
return verifyShareAssetTicket(record, ticket);
|
|
155
|
+
}
|
|
156
|
+
async resolveAsset(record, attachmentId) {
|
|
157
|
+
const attachment = (await this.readManifest(record)).attachments.find((item) => item.id === attachmentId);
|
|
158
|
+
if (!attachment || attachment.artifactFileName !== attachment.id) return null;
|
|
159
|
+
const path = join(resolveStateDir(), record.artifactRelativePath, "assets", attachment.id);
|
|
160
|
+
if (await checksumFile(path).catch(() => "") !== attachment.checksum) return null;
|
|
161
|
+
return {
|
|
162
|
+
path,
|
|
163
|
+
attachment
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async removeArtifact(shareId) {
|
|
167
|
+
await rm(this.artifactDir(shareId), {
|
|
168
|
+
recursive: true,
|
|
169
|
+
force: true
|
|
170
|
+
}).catch((err) => {
|
|
171
|
+
log.warn({
|
|
172
|
+
err,
|
|
173
|
+
shareId
|
|
174
|
+
}, "Failed to remove Session share artifact");
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async loadExpectedSource(sessionKey, expected) {
|
|
178
|
+
const source = await this.loadSource(sessionKey);
|
|
179
|
+
if (source.snapshot.sessionId !== expected.expectedSessionId || source.snapshot.lastSeq !== expected.expectedCutoffSeq || source.metadata.updatedAt !== expected.expectedMetadataUpdatedAt) throw new SessionShareSnapshotConflictError();
|
|
180
|
+
return source;
|
|
181
|
+
}
|
|
182
|
+
async loadSource(sessionKey) {
|
|
183
|
+
const [metadata, snapshot] = await Promise.all([this.source.getMetadata(sessionKey), this.source.getSnapshot(sessionKey)]);
|
|
184
|
+
if (!metadata || !snapshot || !metadata.sessionId || metadata.sessionId !== snapshot.sessionId) throw new Error("Session not found");
|
|
185
|
+
return {
|
|
186
|
+
metadata,
|
|
187
|
+
snapshot
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
buildManifestBase(shareId, source, projection, options) {
|
|
191
|
+
this.validateMessages(projection.messages);
|
|
192
|
+
const selected = new Set(options.selectedAttachmentIds);
|
|
193
|
+
return {
|
|
194
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
195
|
+
shareId,
|
|
196
|
+
source: {
|
|
197
|
+
sessionId: source.snapshot.sessionId,
|
|
198
|
+
cutoffSeq: source.snapshot.lastSeq
|
|
199
|
+
},
|
|
200
|
+
title: source.metadata.name?.trim() || "Shared conversation",
|
|
201
|
+
snapshotAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
202
|
+
messages: projection.messages.map((message) => ({
|
|
203
|
+
...message,
|
|
204
|
+
attachmentIds: message.attachmentIds.filter((id) => selected.has(id))
|
|
205
|
+
})),
|
|
206
|
+
toolActivities: options.includeToolActivities ? projection.toolActivities : []
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async createArtifact(shareId, manifestBase, projection, attachmentIds) {
|
|
210
|
+
const finalDir = this.artifactDir(shareId);
|
|
211
|
+
const stagedDir = `${finalDir}.staged-${randomUUID()}`;
|
|
212
|
+
const built = await this.writeArtifact(stagedDir, manifestBase, projection, attachmentIds);
|
|
213
|
+
try {
|
|
214
|
+
await rename(stagedDir, finalDir);
|
|
215
|
+
return built;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
await rm(stagedDir, {
|
|
218
|
+
recursive: true,
|
|
219
|
+
force: true
|
|
220
|
+
});
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async writeArtifact(targetDir, manifestBase, projection, attachmentIds) {
|
|
225
|
+
const selected = [...new Set(attachmentIds)];
|
|
226
|
+
if (selected.length > MAX_ATTACHMENT_COUNT) throw new Error(`Select at most ${MAX_ATTACHMENT_COUNT} attachments`);
|
|
227
|
+
const candidates = new Map(projection.attachmentCandidates.map((candidate) => [candidate.id, candidate]));
|
|
228
|
+
for (const id of selected) if (!candidates.has(id) || !projection.attachmentUris.has(id)) throw new Error(`Attachment is not part of this session snapshot: ${id}`);
|
|
229
|
+
await mkdir(join(targetDir, "assets"), { recursive: true });
|
|
230
|
+
const attachments = [];
|
|
231
|
+
let totalAttachmentSize = 0;
|
|
232
|
+
try {
|
|
233
|
+
for (const id of selected) {
|
|
234
|
+
const candidate = candidates.get(id);
|
|
235
|
+
const source = await resolveMediaReference(projection.attachmentUris.get(id));
|
|
236
|
+
const sourceStat = await stat(source.path);
|
|
237
|
+
if (!sourceStat.isFile()) throw new Error(`Attachment is not a file: ${candidate.fileName}`);
|
|
238
|
+
if (sourceStat.size > MAX_ATTACHMENT_BYTES) throw new Error(`Attachment is too large: ${candidate.fileName}`);
|
|
239
|
+
totalAttachmentSize += sourceStat.size;
|
|
240
|
+
if (totalAttachmentSize > MAX_TOTAL_ATTACHMENT_BYTES) throw new Error("Selected attachments are too large");
|
|
241
|
+
const target = join(targetDir, "assets", id);
|
|
242
|
+
await copyFile(source.path, target);
|
|
243
|
+
const copied = await stat(target);
|
|
244
|
+
if (!copied.isFile() || copied.size !== sourceStat.size) throw new Error(`Attachment changed while copying: ${candidate.fileName}`);
|
|
245
|
+
attachments.push({
|
|
246
|
+
id,
|
|
247
|
+
messageId: candidate.messageId,
|
|
248
|
+
mimeType: publicMimeType(candidate.mimeType),
|
|
249
|
+
fileName: publicFileName(candidate.fileName),
|
|
250
|
+
size: copied.size,
|
|
251
|
+
artifactFileName: id,
|
|
252
|
+
checksum: await checksumFile(target)
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const manifest = {
|
|
256
|
+
...manifestBase,
|
|
257
|
+
attachments
|
|
258
|
+
};
|
|
259
|
+
const bytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
260
|
+
if (bytes.length > MAX_MANIFEST_BYTES) throw new Error("Session share is too large");
|
|
261
|
+
await writeFile(join(targetDir, "manifest.json"), bytes, { mode: 384 });
|
|
262
|
+
return {
|
|
263
|
+
manifest,
|
|
264
|
+
totalSize: bytes.length + totalAttachmentSize
|
|
265
|
+
};
|
|
266
|
+
} catch (error) {
|
|
267
|
+
await rm(targetDir, {
|
|
268
|
+
recursive: true,
|
|
269
|
+
force: true
|
|
270
|
+
});
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
artifactRelativePath(shareId) {
|
|
275
|
+
return join("share-artifacts", shareId);
|
|
276
|
+
}
|
|
277
|
+
artifactDir(shareId) {
|
|
278
|
+
return join(resolveStateDir(), this.artifactRelativePath(shareId));
|
|
279
|
+
}
|
|
280
|
+
validateMessages(messages) {
|
|
281
|
+
if (messages.length === 0) throw new Error("Session has no shareable messages");
|
|
282
|
+
if (messages.length > MAX_SHARE_MESSAGES) throw new Error(`Session has more than ${MAX_SHARE_MESSAGES} shareable messages`);
|
|
283
|
+
}
|
|
284
|
+
validateManifestRelations(record, manifest) {
|
|
285
|
+
if (manifest.messages.length !== record.messageCount || manifest.attachments.length !== record.attachmentCount) throw new Error("Invalid Session share artifact");
|
|
286
|
+
if (!record.includeToolActivities && manifest.toolActivities.length > 0) throw new Error("Invalid Session share artifact");
|
|
287
|
+
const attachments = new Map(manifest.attachments.map((attachment) => [attachment.id, attachment]));
|
|
288
|
+
if (attachments.size !== manifest.attachments.length) throw new Error("Invalid Session share artifact");
|
|
289
|
+
const messages = new Set(manifest.messages.map((message) => message.id));
|
|
290
|
+
for (const message of manifest.messages) for (const attachmentId of message.attachmentIds) {
|
|
291
|
+
const attachment = attachments.get(attachmentId);
|
|
292
|
+
if (!attachment || attachment.messageId !== message.id) throw new Error("Invalid Session share artifact");
|
|
293
|
+
}
|
|
294
|
+
if (manifest.attachments.some((attachment) => !messages.has(attachment.messageId))) throw new Error("Invalid Session share artifact");
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
function publicFileName(value) {
|
|
298
|
+
return basename(value.replaceAll("\\", "/")).replace(/[\u0000-\u001f\u007f]/g, "_").trim().slice(0, 200) || "attachment";
|
|
299
|
+
}
|
|
300
|
+
function publicMimeType(value) {
|
|
301
|
+
const normalized = value.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
302
|
+
return /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/.test(normalized) ? normalized : "application/octet-stream";
|
|
303
|
+
}
|
|
304
|
+
async function checksumFile(path) {
|
|
305
|
+
const hash = createHash("sha256");
|
|
306
|
+
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
307
|
+
return hash.digest("hex");
|
|
308
|
+
}
|
|
309
|
+
function isSessionShareMessage(value) {
|
|
310
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
311
|
+
const message = value;
|
|
312
|
+
return typeof message.id === "string" && (message.role === "user" || message.role === "assistant") && typeof message.markdown === "string" && typeof message.createdAt === "string" && Array.isArray(message.attachmentIds) && message.attachmentIds.every((id) => typeof id === "string");
|
|
313
|
+
}
|
|
314
|
+
function isSessionShareToolActivity(value) {
|
|
315
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
316
|
+
const activity = value;
|
|
317
|
+
return typeof activity.id === "string" && (activity.messageId === void 0 || typeof activity.messageId === "string") && typeof activity.toolName === "string" && (activity.status === "completed" || activity.status === "failed") && typeof activity.createdAt === "string";
|
|
318
|
+
}
|
|
319
|
+
function isSessionShareAttachment(value) {
|
|
320
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
321
|
+
const attachment = value;
|
|
322
|
+
return typeof attachment.id === "string" && /^attachment-[a-f0-9]{24}$/.test(attachment.id) && typeof attachment.messageId === "string" && typeof attachment.mimeType === "string" && typeof attachment.fileName === "string" && typeof attachment.size === "number" && attachment.artifactFileName === attachment.id && typeof attachment.checksum === "string" && /^[a-f0-9]{64}$/.test(attachment.checksum);
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
export { SessionShareService, SessionShareSnapshotConflictError };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface TicketedShareRecord {
|
|
2
|
+
id: string;
|
|
3
|
+
snapshotRevision: number;
|
|
4
|
+
assetTicketSecret: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function issueShareAssetTicket(record: TicketedShareRecord, ttlMs: number): string;
|
|
7
|
+
export declare function verifyShareAssetTicket(record: TicketedShareRecord, ticket: string): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
//#region src/share/share-asset-ticket.ts
|
|
3
|
+
function issueShareAssetTicket(record, ttlMs) {
|
|
4
|
+
const payload = Buffer.from(JSON.stringify({
|
|
5
|
+
shareId: record.id,
|
|
6
|
+
revision: record.snapshotRevision,
|
|
7
|
+
expiresAt: Date.now() + ttlMs
|
|
8
|
+
})).toString("base64url");
|
|
9
|
+
return `${payload}.${createHmac("sha256", record.assetTicketSecret).update(payload).digest("base64url")}`;
|
|
10
|
+
}
|
|
11
|
+
function verifyShareAssetTicket(record, ticket) {
|
|
12
|
+
const [payload, signature] = ticket.split(".");
|
|
13
|
+
if (!payload || !signature) return false;
|
|
14
|
+
const expected = createHmac("sha256", record.assetTicketSecret).update(payload).digest();
|
|
15
|
+
let actual;
|
|
16
|
+
try {
|
|
17
|
+
actual = Buffer.from(signature, "base64url");
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) return false;
|
|
22
|
+
try {
|
|
23
|
+
const value = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
24
|
+
return value.shareId === record.id && value.revision === record.snapshotRevision && typeof value.expiresAt === "number" && Date.now() < value.expiresAt;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { issueShareAssetTicket, verifyShareAssetTicket };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DirectoryListing } from './share-store.js';
|
|
2
|
-
import type { NoteShareRecord, ShareRecord } from './share-types.js';
|
|
2
|
+
import type { NoteShareRecord, SessionShareRecord, ShareRecord } from './share-types.js';
|
|
3
3
|
export interface ShareLandingOgOptions {
|
|
4
4
|
/** Absolute https/http URL of this landing page (for og:url). Omit if non-public. */
|
|
5
5
|
absoluteShareUrl?: string | null;
|
|
@@ -10,8 +10,9 @@ export interface ShareLandingOgOptions {
|
|
|
10
10
|
/** Description override (default: type + size). */
|
|
11
11
|
description?: string;
|
|
12
12
|
}
|
|
13
|
-
/** Lightweight public shell for
|
|
14
|
-
export declare function
|
|
13
|
+
/** Lightweight public shell for state-owned snapshots. Crawlers get metadata; browsers continue to the SPA preview. */
|
|
14
|
+
export declare function renderSnapshotShareLandingPage(record: NoteShareRecord | SessionShareRecord, previewUrl: string, options: {
|
|
15
|
+
label: 'Note' | 'conversation';
|
|
15
16
|
og?: ShareLandingOgOptions;
|
|
16
17
|
}): string;
|
|
17
18
|
/** Render the file download confirmation landing page (does not consume downloadCount). */
|
|
@@ -35,10 +35,10 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
|
|
|
35
35
|
.empty{text-align:center;color:#94a3b8;padding:2rem}
|
|
36
36
|
`;
|
|
37
37
|
}
|
|
38
|
-
/** Lightweight public shell for
|
|
39
|
-
function
|
|
38
|
+
/** Lightweight public shell for state-owned snapshots. Crawlers get metadata; browsers continue to the SPA preview. */
|
|
39
|
+
function renderSnapshotShareLandingPage(record, previewUrl, options) {
|
|
40
40
|
const title = escapeHtml(record.fileName);
|
|
41
|
-
const description = escapeHtml(record.description ??
|
|
41
|
+
const description = escapeHtml(record.description ?? `A ${options.label} shared via xopc`);
|
|
42
42
|
const target = escapeHtml(previewUrl);
|
|
43
43
|
return `<!DOCTYPE html>
|
|
44
44
|
<html lang="zh">
|
|
@@ -57,7 +57,7 @@ ${renderOgTags({
|
|
|
57
57
|
<meta http-equiv="refresh" content="0;url=${target}">
|
|
58
58
|
</head>
|
|
59
59
|
<body>
|
|
60
|
-
<p><a href="${target}">Open shared
|
|
60
|
+
<p><a href="${target}">Open shared ${options.label}</a></p>
|
|
61
61
|
<script>location.replace(${JSON.stringify(previewUrl)})<\/script>
|
|
62
62
|
</body>
|
|
63
63
|
</html>`;
|
|
@@ -239,4 +239,4 @@ function renderShareExpiredPage(reason) {
|
|
|
239
239
|
</html>`;
|
|
240
240
|
}
|
|
241
241
|
//#endregion
|
|
242
|
-
export { renderFolderLandingPage,
|
|
242
|
+
export { renderFolderLandingPage, renderShareExpiredPage, renderShareLandingPage, renderSnapshotShareLandingPage };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ShareRecord, ShareConfig, CreateShareParams, WorkspaceShareRecord, NoteShareRecord, DirectoryShareRecord } from './share-types.js';
|
|
1
|
+
import type { ShareRecord, ShareConfig, CreateShareParams, WorkspaceShareRecord, NoteShareRecord, SessionShareRecord, DirectoryShareRecord } from './share-types.js';
|
|
2
2
|
export interface DirectoryListingEntry {
|
|
3
3
|
name: string;
|
|
4
4
|
/** Workspace/share-relative POSIX path. */
|
|
@@ -57,6 +57,31 @@ export declare class ShareStore {
|
|
|
57
57
|
fileName: string;
|
|
58
58
|
}): NoteShareRecord | null;
|
|
59
59
|
getNoteShares(noteId: string): NoteShareRecord[];
|
|
60
|
+
createSessionShare(input: {
|
|
61
|
+
id: string;
|
|
62
|
+
sourceSessionId: string;
|
|
63
|
+
cutoffSeq: number;
|
|
64
|
+
artifactRelativePath: string;
|
|
65
|
+
artifactSize: number;
|
|
66
|
+
messageCount: number;
|
|
67
|
+
attachmentCount?: number;
|
|
68
|
+
includeToolActivities?: boolean;
|
|
69
|
+
snapshotRevision?: number;
|
|
70
|
+
fileName: string;
|
|
71
|
+
ttlMs?: number;
|
|
72
|
+
maxViews?: number | null;
|
|
73
|
+
description?: string;
|
|
74
|
+
gatewayTokenHash: string;
|
|
75
|
+
}): SessionShareRecord;
|
|
76
|
+
getSessionShares(sessionId: string): SessionShareRecord[];
|
|
77
|
+
updateSessionSnapshot(id: string, patch: {
|
|
78
|
+
cutoffSeq: number;
|
|
79
|
+
artifactSize: number;
|
|
80
|
+
messageCount: number;
|
|
81
|
+
attachmentCount: number;
|
|
82
|
+
includeToolActivities: boolean;
|
|
83
|
+
fileName: string;
|
|
84
|
+
}): SessionShareRecord | null;
|
|
60
85
|
private persistAndAudit;
|
|
61
86
|
getById(id: string): ShareRecord | null;
|
|
62
87
|
getByToken(token: string): ShareRecord | null;
|
|
@@ -112,7 +137,7 @@ export declare class ShareStore {
|
|
|
112
137
|
private cleanupExpired;
|
|
113
138
|
shutdown(): void;
|
|
114
139
|
private resolveAndValidatePath;
|
|
115
|
-
private
|
|
140
|
+
private cleanupStateArtifact;
|
|
116
141
|
}
|
|
117
142
|
export declare function resolveMimeType(fileName: string): string;
|
|
118
143
|
/** HTTP Content-Type with charset for text-like bodies (browser inline preview). */
|
|
@@ -254,6 +254,69 @@ var ShareStore = class {
|
|
|
254
254
|
getNoteShares(noteId) {
|
|
255
255
|
return this.getAllShares().filter((record) => record.kind === "note" && record.sourceNoteId === noteId);
|
|
256
256
|
}
|
|
257
|
+
createSessionShare(input) {
|
|
258
|
+
if (!this.config.enabled) throw new Error("Session sharing is disabled");
|
|
259
|
+
if (this.getActiveShares().length >= this.config.maxActiveShares) throw new Error(`Maximum active shares reached (${this.config.maxActiveShares})`);
|
|
260
|
+
const ttlMs = input.ttlMs ?? this.config.defaultTtlMs;
|
|
261
|
+
if (ttlMs < 6e4 || ttlMs > this.config.maxTtlMs) throw new Error(`TTL must be between 60s and ${this.config.maxTtlMs / 1e3}s`);
|
|
262
|
+
if (input.maxViews !== void 0 && input.maxViews !== null && (input.maxViews < 1 || input.maxViews > 1e3)) throw new Error("maxViews must be between 1 and 1000");
|
|
263
|
+
const now = /* @__PURE__ */ new Date();
|
|
264
|
+
const record = {
|
|
265
|
+
id: input.id,
|
|
266
|
+
token: randomBytes(32).toString("base64url"),
|
|
267
|
+
kind: "session",
|
|
268
|
+
fileName: input.fileName,
|
|
269
|
+
fileSize: input.artifactSize,
|
|
270
|
+
mimeType: "application/x-xopc-session",
|
|
271
|
+
workspaceRelativePath: "",
|
|
272
|
+
sourceSessionId: input.sourceSessionId,
|
|
273
|
+
cutoffSeq: input.cutoffSeq,
|
|
274
|
+
artifactRelativePath: input.artifactRelativePath,
|
|
275
|
+
messageCount: input.messageCount,
|
|
276
|
+
attachmentCount: input.attachmentCount ?? 0,
|
|
277
|
+
includeToolActivities: input.includeToolActivities ?? false,
|
|
278
|
+
snapshotRevision: input.snapshotRevision ?? 1,
|
|
279
|
+
assetTicketSecret: randomBytes(32).toString("base64url"),
|
|
280
|
+
createdAt: now.toISOString(),
|
|
281
|
+
expiresAt: new Date(now.getTime() + ttlMs).toISOString(),
|
|
282
|
+
maxViews: input.maxViews ?? null,
|
|
283
|
+
downloadCount: 0,
|
|
284
|
+
revoked: false,
|
|
285
|
+
createdByTokenHash: input.gatewayTokenHash,
|
|
286
|
+
description: input.description
|
|
287
|
+
};
|
|
288
|
+
this.shares.set(record.id, record);
|
|
289
|
+
this.tokenIndex.set(record.token, record.id);
|
|
290
|
+
this.persistAndAudit(record, "share.create", `Session share created: ${record.fileName}`, {
|
|
291
|
+
sourceSessionId: record.sourceSessionId,
|
|
292
|
+
cutoffSeq: record.cutoffSeq,
|
|
293
|
+
messageCount: record.messageCount,
|
|
294
|
+
ttlMs
|
|
295
|
+
});
|
|
296
|
+
return record;
|
|
297
|
+
}
|
|
298
|
+
getSessionShares(sessionId) {
|
|
299
|
+
return this.getAllShares().filter((record) => record.kind === "session" && record.sourceSessionId === sessionId);
|
|
300
|
+
}
|
|
301
|
+
updateSessionSnapshot(id, patch) {
|
|
302
|
+
const record = this.shares.get(id);
|
|
303
|
+
if (!record || record.kind !== "session") return null;
|
|
304
|
+
record.cutoffSeq = patch.cutoffSeq;
|
|
305
|
+
record.fileSize = patch.artifactSize;
|
|
306
|
+
record.messageCount = patch.messageCount;
|
|
307
|
+
record.attachmentCount = patch.attachmentCount;
|
|
308
|
+
record.includeToolActivities = patch.includeToolActivities;
|
|
309
|
+
record.fileName = patch.fileName;
|
|
310
|
+
record.snapshotRevision += 1;
|
|
311
|
+
this.persistSync();
|
|
312
|
+
logShareAudit("share.update", {
|
|
313
|
+
shareId: id,
|
|
314
|
+
sourceSessionId: record.sourceSessionId,
|
|
315
|
+
cutoffSeq: record.cutoffSeq,
|
|
316
|
+
snapshotRevision: record.snapshotRevision
|
|
317
|
+
}, `Session share snapshot updated: ${record.fileName}`);
|
|
318
|
+
return record;
|
|
319
|
+
}
|
|
257
320
|
persistAndAudit(record, event, message, extra) {
|
|
258
321
|
this.persistSync();
|
|
259
322
|
logShareAudit(event, {
|
|
@@ -455,7 +518,7 @@ var ShareStore = class {
|
|
|
455
518
|
const record = this.shares.get(id);
|
|
456
519
|
if (!record) return false;
|
|
457
520
|
record.revoked = true;
|
|
458
|
-
this.
|
|
521
|
+
this.cleanupStateArtifact(record);
|
|
459
522
|
this.invalidateListingCache(id);
|
|
460
523
|
this.persistSync();
|
|
461
524
|
logShareAudit("share.revoke", {
|
|
@@ -471,7 +534,7 @@ var ShareStore = class {
|
|
|
471
534
|
const record = this.shares.get(id);
|
|
472
535
|
if (record && !record.revoked) {
|
|
473
536
|
record.revoked = true;
|
|
474
|
-
this.
|
|
537
|
+
this.cleanupStateArtifact(record);
|
|
475
538
|
this.invalidateListingCache(id);
|
|
476
539
|
count++;
|
|
477
540
|
logShareAudit("share.revoke", {
|
|
@@ -489,7 +552,7 @@ var ShareStore = class {
|
|
|
489
552
|
let count = 0;
|
|
490
553
|
for (const record of this.shares.values()) if (!record.revoked && now >= new Date(record.expiresAt).getTime()) {
|
|
491
554
|
record.revoked = true;
|
|
492
|
-
this.
|
|
555
|
+
this.cleanupStateArtifact(record);
|
|
493
556
|
this.invalidateListingCache(record.id);
|
|
494
557
|
count++;
|
|
495
558
|
}
|
|
@@ -583,7 +646,7 @@ var ShareStore = class {
|
|
|
583
646
|
let removed = 0;
|
|
584
647
|
for (const [id, record] of this.shares) {
|
|
585
648
|
const expiredMs = now - new Date(record.expiresAt).getTime();
|
|
586
|
-
if (expiredMs >= 0) this.
|
|
649
|
+
if (expiredMs >= 0) this.cleanupStateArtifact(record);
|
|
587
650
|
if (expiredMs > EXPIRED_RETENTION_MS) {
|
|
588
651
|
this.shares.delete(id);
|
|
589
652
|
this.tokenIndex.delete(record.token);
|
|
@@ -629,14 +692,14 @@ var ShareStore = class {
|
|
|
629
692
|
if (relToRoot.startsWith("..") || relToRoot.split(/[/\\]/).includes("..")) throw new Error("Path is outside workspace");
|
|
630
693
|
return abs;
|
|
631
694
|
}
|
|
632
|
-
|
|
633
|
-
if (record.kind !== "note") return;
|
|
695
|
+
cleanupStateArtifact(record) {
|
|
696
|
+
if (record.kind !== "note" && record.kind !== "session") return;
|
|
634
697
|
const expected = join("share-artifacts", record.id);
|
|
635
698
|
if (record.artifactRelativePath !== expected) {
|
|
636
699
|
log.warn({
|
|
637
700
|
shareId: record.id,
|
|
638
701
|
artifactRelativePath: record.artifactRelativePath
|
|
639
|
-
}, "Skipped unsafe
|
|
702
|
+
}, "Skipped unsafe share artifact cleanup path");
|
|
640
703
|
return;
|
|
641
704
|
}
|
|
642
705
|
rm(join(resolveStateDir(), expected), {
|
|
@@ -646,7 +709,7 @@ var ShareStore = class {
|
|
|
646
709
|
log.warn({
|
|
647
710
|
err,
|
|
648
711
|
shareId: record.id
|
|
649
|
-
}, "Failed to clean
|
|
712
|
+
}, "Failed to clean share artifact");
|
|
650
713
|
});
|
|
651
714
|
}
|
|
652
715
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type WorkspaceShareKind = 'file' | 'directory';
|
|
2
|
-
export type ShareKind = WorkspaceShareKind | 'note';
|
|
2
|
+
export type ShareKind = WorkspaceShareKind | 'note' | 'session';
|
|
3
3
|
export interface ShareDirectoryMeta {
|
|
4
4
|
/** 'browse' shows a file tree; 'zip-only' hides structure and only offers ZIP. */
|
|
5
5
|
mode: 'browse' | 'zip-only';
|
|
@@ -81,8 +81,23 @@ export interface NoteShareRecord extends ShareRecordBase {
|
|
|
81
81
|
assetTicketSecret: string;
|
|
82
82
|
directory?: undefined;
|
|
83
83
|
}
|
|
84
|
+
export interface SessionShareRecord extends ShareRecordBase {
|
|
85
|
+
kind: 'session';
|
|
86
|
+
/** Sessions are state-owned objects, not workspace paths. */
|
|
87
|
+
workspaceRelativePath: '';
|
|
88
|
+
sourceSessionId: string;
|
|
89
|
+
cutoffSeq: number;
|
|
90
|
+
artifactRelativePath: string;
|
|
91
|
+
messageCount: number;
|
|
92
|
+
attachmentCount: number;
|
|
93
|
+
includeToolActivities: boolean;
|
|
94
|
+
snapshotRevision: number;
|
|
95
|
+
/** Private HMAC key for short-lived public asset tickets. */
|
|
96
|
+
assetTicketSecret: string;
|
|
97
|
+
directory?: undefined;
|
|
98
|
+
}
|
|
84
99
|
export type WorkspaceShareRecord = FileShareRecord | DirectoryShareRecord;
|
|
85
|
-
export type ShareRecord = WorkspaceShareRecord | NoteShareRecord;
|
|
100
|
+
export type ShareRecord = WorkspaceShareRecord | NoteShareRecord | SessionShareRecord;
|
|
86
101
|
export interface CreateShareParams {
|
|
87
102
|
/** Workspace-relative file path. */
|
|
88
103
|
path: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { enumerateLanGatewayCandidates } from "../gateway/host.js";
|
|
1
2
|
import { loadTunnelState } from "../tunnel/tunnel-state.js";
|
|
2
|
-
import { resolveLanGatewayUrl } from "../tunnel/tunnel-qr.js";
|
|
3
3
|
//#region src/share/share-url.ts
|
|
4
4
|
function resolveShareUrl(token, ctx) {
|
|
5
5
|
const tunnelState = loadTunnelState();
|
|
@@ -32,7 +32,7 @@ function resolveShareUrl(token, ctx) {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
function buildLanUrl(host, port, path) {
|
|
35
|
-
const lanBase =
|
|
35
|
+
const lanBase = isLoopbackHost(host) ? null : enumerateLanGatewayCandidates(port).find((candidate) => candidate.address === host)?.url ?? `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
|
|
36
36
|
if (!lanBase) return null;
|
|
37
37
|
return `${lanBase.replace(/\/+$/, "")}${path}`;
|
|
38
38
|
}
|