@allbluecn/web-app 0.22.4 → 0.23.1
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/package.json +4 -14
- package/src/__tests__/integration/login-email-acceptance.test.ts +16 -0
- package/src/__tests__/story-view.test.ts +33 -1
- package/src/components/agents/__tests__/agent-bindings-tab.test.tsx +221 -0
- package/src/components/agents/__tests__/agents-page.test.tsx +284 -0
- package/src/components/agents/agent-bindings-tab.tsx +59 -45
- package/src/components/agents/agent-form-dialog.tsx +3 -3
- package/src/components/agents/agent-selector.tsx +2 -3
- package/src/components/agents/agents-page.tsx +66 -31
- package/src/components/agents/slash-skill-menu.tsx +2 -3
- package/src/components/auth/__tests__/session-guard.test.tsx +74 -0
- package/src/components/auth/session-guard.tsx +8 -0
- package/src/components/billing/login-prompt-dialog.tsx +1 -1
- package/src/components/billing/subscription-provider.tsx +2 -16
- package/src/components/bug/__tests__/peek-modes.test.tsx +8 -0
- package/src/components/bug/__tests__/subgroup-header.test.tsx +202 -40
- package/src/components/bug/peek-panel.tsx +5 -3
- package/src/components/bug/views/bug-list/group.tsx +12 -2
- package/src/components/bug/views/bug-list/index.tsx +94 -67
- package/src/components/command/command-palette.tsx +10 -9
- package/src/components/design/canvas/document/frame-custom-presets.ts +9 -1
- package/src/components/design/canvas/panels/toolbar.tsx +21 -22
- package/src/components/design/canvas/react/__tests__/canvas-context-load-echo.test.tsx +199 -0
- package/src/components/design/canvas/react/__tests__/hooks.test.tsx +17 -3
- package/src/components/design/canvas/react/canvas-context.tsx +47 -26
- package/src/components/design/canvas/react/hooks.ts +39 -34
- package/src/components/design/editor/layout/__tests__/collab-param-wiring.test.tsx +17 -0
- package/src/components/design/editor/layout/__tests__/context-menu.test.tsx +17 -1
- package/src/components/design/editor/layout/__tests__/no-page-save-toast.test.tsx +17 -0
- package/src/components/design/editor/layout/__tests__/persistence-restore.test.tsx +17 -1
- package/src/components/design/editor/layout/__tests__/ssr-localstorage-smoke.test.tsx +17 -1
- package/src/components/design/editor/layout/design-editor-layout.tsx +13 -2
- package/src/components/design/editor/layout/hooks/use-design-auto-save.ts +3 -19
- package/src/components/devtools/devtools-panel.tsx +56 -0
- package/src/components/forms/__tests__/create-app-form.test.tsx +131 -0
- package/src/components/forms/app-field-error.tsx +21 -1
- package/src/components/forms/app-field.tsx +8 -8
- package/src/components/forms/app-submit.tsx +12 -9
- package/src/components/forms/create-app-form.tsx +124 -0
- package/src/components/forms/index.ts +2 -3
- package/src/components/harness/__tests__/gallery-behavior.test.tsx +37 -1
- package/src/components/harness/__tests__/harness-workbench-first-send.test.tsx +4 -1
- package/src/components/harness/__tests__/message-actions.test.tsx +7 -6
- package/src/components/harness/__tests__/use-video-generation.test.tsx +165 -0
- package/src/components/harness/ai-chat-history/index.tsx +5 -3
- package/src/components/harness/gallery/gallery-card.tsx +57 -35
- package/src/components/harness/gallery/gallery-grid.tsx +32 -15
- package/src/components/harness/gallery/gallery-types.ts +19 -0
- package/src/components/harness/gallery/gallery-video-card.tsx +54 -0
- package/src/components/harness/gallery/types.ts +2 -0
- package/src/components/harness/gallery/use-video-generation.ts +95 -0
- package/src/components/harness/harness-workbench-page.tsx +2 -3
- package/src/components/harness/message-actions.tsx +5 -3
- package/src/components/hotkeys/shortcuts-panel.tsx +28 -13
- package/src/components/iteration/__tests__/burndown-chart.test.tsx +118 -0
- package/src/components/iteration/burndown-chart.tsx +54 -23
- package/src/components/knowledge/editor/__tests__/knowledge-doc-editor-collab.test.tsx +96 -0
- package/src/components/knowledge/editor/knowledge-doc-editor.tsx +56 -1
- package/src/components/knowledge/editor/mindmap-doc-editor.tsx +12 -0
- package/src/components/knowledge/editor/use-auto-save-channels.ts +3 -1
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +0 -4
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +11 -11
- package/src/components/layout/sidebar-03/nav-main.tsx +12 -15
- package/src/components/layout/sidebar-03/nav-notifications.tsx +5 -8
- package/src/components/layout/sidebar-03/nav-workspace.tsx +11 -9
- package/src/components/notifications/__tests__/settings-notifications.test.tsx +158 -0
- package/src/components/notifications/settings-notifications.tsx +37 -13
- package/src/components/notifications/team-notifications.tsx +10 -14
- package/src/components/settings/personal-team/account/__tests__/account-client.test.tsx +170 -0
- package/src/components/settings/personal-team/account/account-client.tsx +44 -7
- package/src/components/settings/personal-team/team/team-client.tsx +17 -19
- package/src/components/shared-kit/list-kit/__tests__/frozen-header.test.ts +210 -41
- package/src/components/shared-kit/list-kit/frozen-header.ts +134 -21
- package/src/components/shared-kit/list-kit/kanban/frozen-header.ts +20 -27
- package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +8 -0
- package/src/components/story/__tests__/story-list-table.test.tsx +214 -0
- package/src/components/story/columns.tsx +11 -2
- package/src/components/story/peek-panel.tsx +4 -2
- package/src/components/story/story-list-table.tsx +16 -1
- package/src/components/story/views/story-list/__tests__/frozen-headers.test.tsx +186 -0
- package/src/components/story/views/story-list/group.tsx +12 -1
- package/src/components/story/views/story-list/index.tsx +101 -68
- package/src/components/testcase/__tests__/case-row.test.tsx +134 -2
- package/src/components/testcase/__tests__/peek-panel.test.tsx +8 -0
- package/src/components/testcase/__tests__/step-editor.test.tsx +106 -0
- package/src/components/testcase/list/grouped-list.tsx +50 -22
- package/src/components/testcase/peek-panel.tsx +7 -4
- package/src/components/testcase/step-editor.tsx +196 -150
- package/src/components/tiptap/notion-like/editor-extensions.ts +0 -3
- package/src/components/tiptap/notion-like/notion-like-editor-header.tsx +1 -31
- package/src/components/tiptap/notion-like/notion-like-editor.tsx +32 -1
- package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +2 -28
- package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +2 -28
- package/src/components/workspace/dialogs/create-workspace-dialog.tsx +45 -43
- package/src/components/workspace/dialogs/edit-workspace-dialog.tsx +38 -36
- package/src/design-engine/engine/engine-decor.ts +2 -0
- package/src/design-engine/engine/engine-path.ts +2 -0
- package/src/hooks/__tests__/use-unsaved-guard.test.tsx +180 -0
- package/src/hooks/use-notifications.ts +9 -16
- package/src/hooks/use-unsaved-guard.tsx +87 -0
- package/src/lib/__tests__/pacer.test.ts +33 -96
- package/src/lib/__tests__/query-defaults.test.ts +169 -0
- package/src/lib/__tests__/query-global-error.test.ts +256 -0
- package/src/lib/anyhub/anyhub-store.ts +2 -2
- package/src/lib/changelog/sdk-versions.ts +2 -10
- package/src/lib/collab/presence-store.ts +5 -5
- package/src/lib/collab/use-presence-list-refresh.ts +2 -2
- package/src/lib/collab/use-share-presence.ts +2 -2
- package/src/lib/collections/query-keys.ts +7 -2
- package/src/lib/commands/__tests__/command-palette-state.test.ts +12 -14
- package/src/lib/commands/__tests__/registry.test.ts +6 -3
- package/src/lib/commands/command-palette-state.ts +8 -31
- package/src/lib/hotkeys/__tests__/registry.test.ts +71 -18
- package/src/lib/hotkeys/registry.ts +188 -85
- package/src/lib/hotkeys/types.ts +1 -13
- package/src/lib/pacer.ts +1 -79
- package/src/lib/queries/README.md +67 -0
- package/src/lib/queries/__tests__/agents-queries.test.ts +101 -0
- package/src/lib/queries/__tests__/compliance-queries.test.ts +136 -0
- package/src/lib/queries/__tests__/notifications-queries.test.ts +175 -0
- package/src/lib/queries/__tests__/team-queries.test.ts +87 -0
- package/src/lib/queries/agents.ts +40 -0
- package/src/lib/queries/compliance.ts +69 -0
- package/src/lib/queries/notifications.ts +63 -0
- package/src/lib/queries/team.ts +39 -0
- package/src/lib/query-defaults.ts +8 -0
- package/src/lib/query-global-error.ts +128 -0
- package/src/lib/remote-auth-client.ts +16 -0
- package/src/paraglide/README.md +1 -1
- package/src/paraglide/messages/_index.d.ts +11 -0
- package/src/paraglide/messages/_index.js +5659 -88285
- package/src/paraglide/messages/common_error_generic.d.ts +16 -0
- package/src/paraglide/messages/common_error_generic.js +29 -0
- package/src/paraglide/messages/common_unsaved_body.d.ts +16 -0
- package/src/paraglide/messages/common_unsaved_body.js +29 -0
- package/src/paraglide/messages/common_unsaved_leave.d.ts +16 -0
- package/src/paraglide/messages/common_unsaved_leave.js +29 -0
- package/src/paraglide/messages/common_unsaved_stay.d.ts +16 -0
- package/src/paraglide/messages/common_unsaved_stay.js +29 -0
- package/src/paraglide/messages/common_unsaved_title.d.ts +16 -0
- package/src/paraglide/messages/common_unsaved_title.js +29 -0
- package/src/paraglide/messages/gallery_videofailed1.d.ts +17 -0
- package/src/paraglide/messages/gallery_videofailed1.js +30 -0
- package/src/paraglide/messages/gallery_videogenerating1.d.ts +17 -0
- package/src/paraglide/messages/gallery_videogenerating1.js +30 -0
- package/src/paraglide/messages/gallery_videomissingprompt2.d.ts +17 -0
- package/src/paraglide/messages/gallery_videomissingprompt2.js +30 -0
- package/src/paraglide/messages/gallery_videoprogress1.d.ts +19 -0
- package/src/paraglide/messages/gallery_videoprogress1.js +30 -0
- package/src/paraglide/messages/gallery_videostop1.d.ts +17 -0
- package/src/paraglide/messages/gallery_videostop1.js +30 -0
- package/src/paraglide/messages/story_table_resizecolumn1.d.ts +17 -0
- package/src/paraglide/messages/story_table_resizecolumn1.js +30 -0
- package/src/providers/index.tsx +5 -50
- package/src/routeTree.gen.ts +21 -0
- package/src/router.tsx +21 -8
- package/src/routes/__root.tsx +2 -1
- package/src/routes/__tests__/-edition-routing.test.tsx +6 -1
- package/src/routes/_login/forgot-password/route.lazy.tsx +7 -6
- package/src/routes/_login/login/route.lazy.tsx +10 -9
- package/src/routes/_login/register/route.lazy.tsx +19 -18
- package/src/routes/_login/reset-password/route.lazy.tsx +10 -9
- package/src/routes/_nav/knowledge/compliance/$id.lazy.tsx +9 -12
- package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +16 -13
- package/src/routes/_nav/knowledge/laws/$id.lazy.tsx +1 -1
- package/src/routes/_nav/notifications/route.tsx +6 -16
- package/src/routes/_nav/settings/-settings-tabs.tsx +9 -12
- package/src/routes/api/generate/__tests__/media.test.ts +378 -0
- package/src/routes/api/generate/media.ts +179 -0
- package/src/server/__tests__/agent-client-graph.test.ts +0 -1
- package/src/server/__tests__/agent-crud.test.ts +37 -4
- package/src/server/__tests__/bug-groups-two-level.test.ts +34 -1
- package/src/server/__tests__/chat-conversations.test.ts +37 -4
- package/src/server/__tests__/chat-list-messages.test.ts +40 -7
- package/src/server/__tests__/csv-export.test.ts +36 -3
- package/src/server/__tests__/desktop-bootstrap.test.ts +189 -0
- package/src/server/__tests__/remote-auth.test.ts +72 -0
- package/src/server/__tests__/skill-crud.test.ts +37 -4
- package/src/server/__tests__/sso-domain.test.ts +1 -1
- package/src/server/__tests__/sso-rate-limit-singleton.test.ts +123 -0
- package/src/server/__tests__/task-serverfn-crud.test.ts +35 -2
- package/src/server/__tests__/task-serverfn-queries.test.ts +35 -2
- package/src/server/__tests__/testcase-board.test.ts +38 -5
- package/src/server/__tests__/testcase-bulk.test.ts +38 -5
- package/src/server/__tests__/testcase-list-scope.test.ts +38 -5
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +126 -2
- package/src/server/ai-gateway/__tests__/chat-stream-builder-code-mode.test.ts +4 -0
- package/src/server/ai-gateway/__tests__/chat-stream-builder-memory.test.ts +4 -0
- package/src/server/ai-gateway/__tests__/chat-stream-builder-sandbox.test.ts +4 -0
- package/src/server/ai-gateway/__tests__/chat-stream-builder-skills.test.ts +4 -0
- package/src/server/ai-gateway/__tests__/pgvector-memory-adapter.test.ts +30 -0
- package/src/server/ai-gateway/__tests__/pgvector-memory-conformance.test.ts +92 -0
- package/src/server/ai-gateway/__tests__/source-mode.test.ts +49 -8
- package/src/server/ai-gateway/ai-output-guard.ts +50 -2
- package/src/server/ai-gateway/chat-stream-builder.ts +3 -0
- package/src/server/ai-gateway/media/index.ts +36 -29
- package/src/server/ai-gateway/media/trim-input.ts +52 -0
- package/src/server/ai-gateway/memory/pgvector-adapter.ts +27 -13
- package/src/server/ai-gateway/sandbox/__tests__/harness-sandbox.test.ts +252 -41
- package/src/server/ai-gateway/sandbox/harness-sandbox.ts +6 -67
- package/src/server/ai-gateway/source-mode.ts +24 -8
- package/src/server/desktop-bootstrap.ts +74 -0
- package/src/server/lib/__tests__/rate-limit.test.ts +0 -1
- package/src/server/lib/rate-limit.ts +0 -2
- package/src/server/middlewares/__tests__/auth-context.test.ts +156 -0
- package/src/server/middlewares/auth-context.ts +62 -0
- package/src/server/remote-auth.ts +58 -31
- package/src/server/serverFns/__tests__/ai-hosted-models.test.ts +47 -15
- package/src/server/serverFns/__tests__/ai-source-mode.test.ts +45 -15
- package/src/server/serverFns/__tests__/billing.test.ts +37 -2
- package/src/server/serverFns/__tests__/collab-snapshot.test.ts +41 -14
- package/src/server/serverFns/__tests__/collab.test.ts +41 -14
- package/src/server/serverFns/__tests__/compliance.test.ts +33 -1
- package/src/server/serverFns/__tests__/email-verification.test.ts +35 -1
- package/src/server/serverFns/__tests__/knowledge-breadcrumb.test.ts +46 -6
- package/src/server/serverFns/__tests__/knowledge-delete.test.ts +42 -14
- package/src/server/serverFns/__tests__/knowledge-kb-projects.test.ts +46 -6
- package/src/server/serverFns/__tests__/knowledge-public-compliance.test.ts +36 -2
- package/src/server/serverFns/__tests__/knowledge-readable-kb.test.ts +42 -10
- package/src/server/serverFns/__tests__/knowledge-search.test.ts +45 -9
- package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +36 -2
- package/src/server/serverFns/__tests__/link-preview.test.ts +38 -4
- package/src/server/serverFns/__tests__/notifications-list.test.ts +36 -3
- package/src/server/serverFns/__tests__/ownership-merge.test.ts +38 -4
- package/src/server/serverFns/__tests__/project-delete-transaction.test.ts +39 -2
- package/src/server/serverFns/__tests__/project-kb.test.ts +34 -1
- package/src/server/serverFns/__tests__/project-member-idor.test.ts +34 -1
- package/src/server/serverFns/__tests__/project-team-assign.test.ts +34 -1
- package/src/server/serverFns/__tests__/read-write-merge.test.ts +38 -4
- package/src/server/serverFns/__tests__/story-comments.test.ts +38 -4
- package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +38 -4
- package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +37 -3
- package/src/server/serverFns/__tests__/story-crud.test.ts +36 -3
- package/src/server/serverFns/__tests__/story-links.test.ts +38 -4
- package/src/server/serverFns/__tests__/story-reactions.test.ts +38 -4
- package/src/server/serverFns/__tests__/story-tree.test.ts +38 -4
- package/src/server/serverFns/__tests__/team-accept.test.ts +34 -1
- package/src/server/serverFns/__tests__/team-invitation.test.ts +34 -1
- package/src/server/serverFns/__tests__/team-join-request.test.ts +34 -1
- package/src/server/serverFns/__tests__/team-members.test.ts +34 -1
- package/src/server/serverFns/__tests__/team-ownership-merge.test.ts +34 -1
- package/src/server/serverFns/__tests__/update-bug-guard.test.ts +37 -3
- package/src/server/serverFns/__tests__/update-document-guard.test.ts +38 -4
- package/src/server/serverFns/__tests__/update-page-guard.test.ts +40 -4
- package/src/server/serverFns/__tests__/update-seats.test.ts +33 -1
- package/src/server/serverFns/__tests__/update-story-guard.test.ts +36 -3
- package/src/server/serverFns/__tests__/update-testcase-guard.test.ts +37 -3
- package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +36 -2
- package/src/server/serverFns/__tests__/workspace-kb.test.ts +36 -2
- package/src/server/serverFns/__tests__/workspace.test.ts +34 -1
- package/src/server/serverFns/activity.ts +10 -14
- package/src/server/serverFns/agents/agent-crud.ts +16 -15
- package/src/server/serverFns/agents/bindings.ts +12 -13
- package/src/server/serverFns/agents/skill-crud.ts +16 -15
- package/src/server/serverFns/ai/code-mode-readiness.ts +3 -4
- package/src/server/serverFns/ai/search-capability.ts +3 -4
- package/src/server/serverFns/anyhub/app.ts +10 -14
- package/src/server/serverFns/anyhub/index.ts +7 -10
- package/src/server/serverFns/anyhub/install.ts +4 -4
- package/src/server/serverFns/anyhub/permissions.ts +10 -14
- package/src/server/serverFns/anyhub/proxy.ts +4 -6
- package/src/server/serverFns/auth/email-verification.ts +4 -5
- package/src/server/serverFns/auth/session.ts +9 -0
- package/src/server/serverFns/billing.ts +18 -28
- package/src/server/serverFns/blob-upload.ts +6 -9
- package/src/server/serverFns/bug/__tests__/bug-access-tokens.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-activities.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-fix.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-reactions.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-reorder.test.ts +39 -5
- package/src/server/serverFns/bug/__tests__/bug-repro.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-story.test.ts +34 -1
- package/src/server/serverFns/bug/__tests__/bug-webhooks.test.ts +34 -1
- package/src/server/serverFns/bug/bug-access-tokens.ts +16 -22
- package/src/server/serverFns/bug/bug-activities.ts +4 -6
- package/src/server/serverFns/bug/bug-collect-tokens.ts +13 -18
- package/src/server/serverFns/bug/bug-comments.ts +13 -18
- package/src/server/serverFns/bug/bug-crud/detail.ts +10 -14
- package/src/server/serverFns/bug/bug-crud.ts +28 -38
- package/src/server/serverFns/bug/bug-display-options.ts +8 -11
- package/src/server/serverFns/bug/bug-export.ts +5 -7
- package/src/server/serverFns/bug/bug-fix.ts +10 -14
- package/src/server/serverFns/bug/bug-groups.ts +10 -14
- package/src/server/serverFns/bug/bug-reactions.ts +10 -14
- package/src/server/serverFns/bug/bug-reorder.ts +4 -6
- package/src/server/serverFns/bug/bug-repro.ts +8 -11
- package/src/server/serverFns/bug/bug-story-bulk.ts +13 -18
- package/src/server/serverFns/bug/bug-story.ts +10 -14
- package/src/server/serverFns/bug/bug-views.ts +12 -18
- package/src/server/serverFns/bug/bug-webhooks.ts +13 -18
- package/src/server/serverFns/bug/test-bug-confirm.ts +7 -10
- package/src/server/serverFns/changelog/__tests__/fetch.test.ts +40 -1
- package/src/server/serverFns/changelog/fetch.ts +4 -6
- package/src/server/serverFns/chat/conversations.ts +17 -28
- package/src/server/serverFns/chat/list-messages.ts +4 -7
- package/src/server/serverFns/collab-presence.ts +11 -13
- package/src/server/serverFns/collab-snapshot.ts +6 -7
- package/src/server/serverFns/collab.ts +7 -7
- package/src/server/serverFns/compliance.ts +8 -8
- package/src/server/serverFns/design/__tests__/comments.test.ts +54 -10
- package/src/server/serverFns/design/__tests__/designs.test.ts +39 -16
- package/src/server/serverFns/design/__tests__/page-versions.test.ts +52 -14
- package/src/server/serverFns/design/__tests__/shares-password.test.ts +44 -14
- package/src/server/serverFns/design/__tests__/shares.test.ts +44 -14
- package/src/server/serverFns/design/__tests__/versions.test.ts +44 -14
- package/src/server/serverFns/design/ai/__tests__/agent-conversations.test.ts +50 -21
- package/src/server/serverFns/design/ai/__tests__/providers.test.ts +42 -8
- package/src/server/serverFns/design/ai/agent-conversations.ts +19 -26
- package/src/server/serverFns/design/ai/fill-annotation.ts +5 -7
- package/src/server/serverFns/design/ai/generate-prototype.ts +5 -7
- package/src/server/serverFns/design/ai/hosted-models.ts +4 -5
- package/src/server/serverFns/design/ai/providers.ts +30 -39
- package/src/server/serverFns/design/ai/source-mode.ts +10 -12
- package/src/server/serverFns/design/comments.ts +14 -13
- package/src/server/serverFns/design/designs.ts +13 -19
- package/src/server/serverFns/design/page-versions.ts +17 -18
- package/src/server/serverFns/design/pages.ts +17 -18
- package/src/server/serverFns/design/reviews.ts +8 -9
- package/src/server/serverFns/design/shares-password.ts +13 -19
- package/src/server/serverFns/design/shares.ts +22 -31
- package/src/server/serverFns/design/versions.ts +8 -9
- package/src/server/serverFns/harness/__tests__/attach-prototype.spec.ts +52 -2
- package/src/server/serverFns/harness/__tests__/attach-to-kb.spec.ts +52 -2
- package/src/server/serverFns/harness/__tests__/attach-to-story-ssrf.spec.ts +52 -2
- package/src/server/serverFns/harness/__tests__/attach-to-story.spec.ts +52 -2
- package/src/server/serverFns/harness/__tests__/cancel-run.test.ts +40 -15
- package/src/server/serverFns/harness/__tests__/extract-attachment.spec.ts +52 -2
- package/src/server/serverFns/harness/__tests__/gallery.spec.ts +76 -12
- package/src/server/serverFns/harness/__tests__/github-workspace.spec.ts +38 -6
- package/src/server/serverFns/harness/__tests__/media-prototype.spec.ts +50 -6
- package/src/server/serverFns/harness/__tests__/media.spec.ts +55 -11
- package/src/server/serverFns/harness/__tests__/sandbox-policy.test.ts +41 -16
- package/src/server/serverFns/harness/__tests__/workspace-files.test.ts +41 -16
- package/src/server/serverFns/harness/attach-prototype.ts +4 -4
- package/src/server/serverFns/harness/attach-to-kb.ts +4 -4
- package/src/server/serverFns/harness/attach-to-story.ts +4 -4
- package/src/server/serverFns/harness/cancel-run.ts +4 -6
- package/src/server/serverFns/harness/extract-attachment.ts +3 -4
- package/src/server/serverFns/harness/gallery.ts +19 -23
- package/src/server/serverFns/harness/github-workspace.ts +14 -21
- package/src/server/serverFns/harness/media.ts +18 -117
- package/src/server/serverFns/harness/sandbox-policy.ts +11 -10
- package/src/server/serverFns/harness/search-mention-candidates.ts +4 -6
- package/src/server/serverFns/harness/workspace-files.ts +19 -26
- package/src/server/serverFns/iteration/__tests__/iteration-auto-schedule.test.ts +35 -3
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +35 -3
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +35 -3
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +35 -3
- package/src/server/serverFns/iteration/iteration-auto-schedule.ts +7 -10
- package/src/server/serverFns/iteration/iteration-burndown.ts +4 -6
- package/src/server/serverFns/iteration/iteration-crud.ts +26 -35
- package/src/server/serverFns/iteration/iteration-stories.ts +10 -14
- package/src/server/serverFns/iteration/iteration-transfer.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/categories.ts +11 -15
- package/src/server/serverFns/knowledge/public/bookmarks/create.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/delete.ts +5 -7
- package/src/server/serverFns/knowledge/public/bookmarks/detail.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/favorite.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/list.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/refresh-favicon.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/share.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/sync-github.ts +5 -7
- package/src/server/serverFns/knowledge/public/bookmarks/unshare.ts +4 -6
- package/src/server/serverFns/knowledge/public/bookmarks/update.ts +6 -8
- package/src/server/serverFns/knowledge/public/compliance/create.ts +4 -6
- package/src/server/serverFns/knowledge/public/compliance/delete.ts +5 -7
- package/src/server/serverFns/knowledge/public/compliance/detail.ts +4 -6
- package/src/server/serverFns/knowledge/public/compliance/list.ts +4 -6
- package/src/server/serverFns/knowledge/public/compliance/share.ts +4 -6
- package/src/server/serverFns/knowledge/public/compliance/unshare.ts +4 -6
- package/src/server/serverFns/knowledge/public/compliance/update.ts +5 -7
- package/src/server/serverFns/knowledge/public/dictionary/create.ts +4 -6
- package/src/server/serverFns/knowledge/public/dictionary/delete.ts +5 -7
- package/src/server/serverFns/knowledge/public/dictionary/detail.ts +4 -6
- package/src/server/serverFns/knowledge/public/dictionary/list.ts +4 -6
- package/src/server/serverFns/knowledge/public/dictionary/share.ts +4 -6
- package/src/server/serverFns/knowledge/public/dictionary/unshare.ts +4 -6
- package/src/server/serverFns/knowledge/public/dictionary/update.ts +5 -7
- package/src/server/serverFns/knowledge/public/entity-tags.ts +4 -6
- package/src/server/serverFns/knowledge/public/favorite.ts +7 -10
- package/src/server/serverFns/knowledge/public/legal/create.ts +4 -6
- package/src/server/serverFns/knowledge/public/legal/delete.ts +5 -7
- package/src/server/serverFns/knowledge/public/legal/detail.ts +4 -6
- package/src/server/serverFns/knowledge/public/legal/list.ts +4 -6
- package/src/server/serverFns/knowledge/public/legal/share.ts +4 -6
- package/src/server/serverFns/knowledge/public/legal/unshare.ts +4 -6
- package/src/server/serverFns/knowledge/public/legal/update.ts +5 -7
- package/src/server/serverFns/knowledge-index.ts +12 -16
- package/src/server/serverFns/knowledge.ts +50 -67
- package/src/server/serverFns/notifications.ts +25 -34
- package/src/server/serverFns/plugins/harness-plugins.ts +16 -23
- package/src/server/serverFns/project/module-overview.ts +7 -10
- package/src/server/serverFns/project.ts +48 -68
- package/src/server/serverFns/references/create.ts +4 -6
- package/src/server/serverFns/references/resolve.ts +4 -6
- package/src/server/serverFns/references/search.ts +4 -6
- package/src/server/serverFns/share/design/[designId]/events.ts +4 -6
- package/src/server/serverFns/sso.ts +1 -1
- package/src/server/serverFns/story/__tests__/bulk.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-attachments.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-crud-date-key.test.ts +42 -17
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-gantt-deps.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-read-path-authz.test.ts +40 -4
- package/src/server/serverFns/story/__tests__/story-search.test.ts +34 -1
- package/src/server/serverFns/story/__tests__/story-transfer.test.ts +34 -1
- package/src/server/serverFns/story/link-preview.ts +5 -7
- package/src/server/serverFns/story/story-assoc.ts +28 -38
- package/src/server/serverFns/story/story-attachments.ts +7 -10
- package/src/server/serverFns/story/story-children.ts +4 -6
- package/src/server/serverFns/story/story-comments.ts +13 -18
- package/src/server/serverFns/story/story-crud/activities.ts +4 -6
- package/src/server/serverFns/story/story-crud/archive.ts +7 -10
- package/src/server/serverFns/story/story-crud/bulk.ts +10 -14
- package/src/server/serverFns/story/story-crud/detail.ts +7 -10
- package/src/server/serverFns/story/story-crud/label-suggest.ts +4 -6
- package/src/server/serverFns/story/story-crud/labels.ts +6 -9
- package/src/server/serverFns/story/story-crud/user-brief.ts +4 -6
- package/src/server/serverFns/story/story-crud.ts +13 -18
- package/src/server/serverFns/story/story-dependencies.ts +16 -22
- package/src/server/serverFns/story/story-groups.ts +10 -14
- package/src/server/serverFns/story/story-links.ts +13 -18
- package/src/server/serverFns/story/story-reactions.ts +13 -18
- package/src/server/serverFns/story/story-search.ts +7 -10
- package/src/server/serverFns/story/story-transfer.ts +10 -14
- package/src/server/serverFns/story/story-tree.ts +7 -10
- package/src/server/serverFns/story-display-options.ts +7 -8
- package/src/server/serverFns/story-view.ts +12 -18
- package/src/server/serverFns/tasks/task-crud.ts +13 -17
- package/src/server/serverFns/tasks/task-queries.ts +12 -18
- package/src/server/serverFns/team.ts +46 -66
- package/src/server/serverFns/testcase/__tests__/automation.test.ts +38 -5
- package/src/server/serverFns/testcase/__tests__/evidence-upload.test.ts +37 -3
- package/src/server/serverFns/testcase/__tests__/generation.test.ts +38 -5
- package/src/server/serverFns/testcase/__tests__/plans.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/regression.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/run-cases.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/runs.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/stats.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/testcase-board.test.ts +39 -5
- package/src/server/serverFns/testcase/__tests__/testcase-crud.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/testcase-folders.test.ts +36 -3
- package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +38 -5
- package/src/server/serverFns/testcase/automation.ts +13 -17
- package/src/server/serverFns/testcase/bulk.ts +10 -14
- package/src/server/serverFns/testcase/evidence-upload.ts +5 -7
- package/src/server/serverFns/testcase/generation.ts +10 -13
- package/src/server/serverFns/testcase/plans.ts +23 -31
- package/src/server/serverFns/testcase/regression.ts +7 -10
- package/src/server/serverFns/testcase/run-cases.ts +13 -18
- package/src/server/serverFns/testcase/runs.ts +13 -18
- package/src/server/serverFns/testcase/stats.ts +13 -18
- package/src/server/serverFns/testcase/testcase-activities.ts +4 -6
- package/src/server/serverFns/testcase/testcase-board.ts +4 -6
- package/src/server/serverFns/testcase/testcase-crud.ts +20 -27
- package/src/server/serverFns/testcase/testcase-display-options.ts +8 -11
- package/src/server/serverFns/testcase/testcase-export.ts +5 -7
- package/src/server/serverFns/testcase/testcase-filter-views.ts +12 -18
- package/src/server/serverFns/testcase/testcase-folders.ts +16 -22
- package/src/server/serverFns/testcase/testcase-groups.ts +4 -6
- package/src/server/serverFns/testcase/testcase-reorder.ts +4 -6
- package/src/server/serverFns/translation.ts +4 -6
- package/src/server/serverFns/user/avatar.ts +4 -6
- package/src/server/serverFns/user/settings.ts +4 -6
- package/src/server/serverFns/user/update-locale.ts +4 -6
- package/src/server/serverFns/workspace-kb.ts +26 -35
- package/src/server/serverFns/workspace.ts +43 -59
- package/src/server/serverFns/ws.ts +4 -6
- package/src/server/serverfn-public-allowlist.ts +117 -0
- package/src/server/story/types.ts +0 -1
- package/src/server/sync/__tests__/scopes.test.ts +64 -0
- package/src/server/sync/scopes.ts +7 -0
- package/src/server/testcase/__tests__/auto-bug.test.ts +44 -12
- package/src/__tests__/bench/baselines/format-timestamp.json +0 -44
- package/src/__typeprobe__.d.ts +0 -4
- package/src/components/forms/app-form-dialog.tsx +0 -126
- package/src/components/forms/hooks/use-form-submit.ts +0 -57
- package/src/components/forms/types.ts +0 -34
- package/src/components/harness/code/workspace-picker.tsx +0 -239
- package/src/components/tiptap/extensions/local-file/insert-local-file.ts +0 -35
- package/src/components/tiptap/extensions/local-file/local-file-node.ts +0 -71
- package/src/devframe-docks/ai-chat-iframe.tsx +0 -29
- package/src/devframe-docks/session-iframe.tsx +0 -90
- package/src/lib/__tests__/desktop-detect.test.ts +0 -19
- package/src/lib/collections/index.ts +0 -22
- package/src/lib/collections/knowledge/index.ts +0 -18
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +0 -34
- package/src/lib/collections/reference/bookmark-categories-collection.ts +0 -31
- package/src/lib/collections/reference/labels-collection.ts +0 -31
- package/src/lib/collections/reference/projects-collection.ts +0 -31
- package/src/lib/collections/reference/users-brief-collection.ts +0 -35
- package/src/lib/collections/reference/workspaces-collection.ts +0 -34
- package/src/lib/desktop-detect.ts +0 -29
- package/src/paraglide/messages/en.js +0 -28260
- package/src/paraglide/messages/zh.js +0 -28258
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import { Link
|
|
18
|
+
import { Link } from '@tanstack/react-router'
|
|
19
19
|
import { User, Users, Bot, Settings, CreditCard, Plug, ShieldCheck, Bell, Server, type LucideIcon } from 'lucide-react'
|
|
20
|
-
import { cn } from '@/lib/utils'
|
|
21
20
|
import { mergeSettingsGroups } from '@/components/plugin-slots/plugin-settings-tabs'
|
|
22
21
|
import { m } from '@/paraglide/messages.js'
|
|
23
22
|
|
|
@@ -70,7 +69,6 @@ const desktopGroups: SettingsGroup[] = [
|
|
|
70
69
|
]
|
|
71
70
|
|
|
72
71
|
export function SettingsTabs() {
|
|
73
|
-
const location = useLocation()
|
|
74
72
|
const isDesktop = import.meta.env.VITE_DEPLOY_TARGET === 'desktop'
|
|
75
73
|
const allGroups = isDesktop ? [...desktopGroups, ...groups] : groups
|
|
76
74
|
|
|
@@ -83,19 +81,18 @@ export function SettingsTabs() {
|
|
|
83
81
|
</h2>
|
|
84
82
|
{group.tabs.map((tab) => {
|
|
85
83
|
const Icon = tab.icon
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
84
|
+
// 激活态由 Link 自身判定:默认模糊匹配 = 段边界前缀
|
|
85
|
+
// (子路由如 /settings/ai/providers/xxx 保持所属 tab 高亮,/settings/aix 不误伤),
|
|
86
|
+
// 等价原「pathname === tab.path || startsWith(`${tab.path}/`)」;
|
|
87
|
+
// includeSearch:false 保持纯 pathname 口径(否则带查询串的 URL 不命中激活态)
|
|
89
88
|
return (
|
|
90
89
|
<Link
|
|
91
90
|
key={tab.path}
|
|
92
91
|
to={tab.path}
|
|
93
|
-
className=
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
: 'text-muted-foreground',
|
|
98
|
-
)}
|
|
92
|
+
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-muted hover:text-primary"
|
|
93
|
+
activeProps={{ className: 'bg-muted text-primary' }}
|
|
94
|
+
inactiveProps={{ className: 'text-muted-foreground' }}
|
|
95
|
+
activeOptions={{ includeSearch: false }}
|
|
99
96
|
>
|
|
100
97
|
<Icon className="size-4" />
|
|
101
98
|
{tab.label()}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
// /api/generate/media SSE 路由集成测试。
|
|
19
|
+
//
|
|
20
|
+
// mock streamVideoForUser 的流式产物(官方 generateVideo stream 模式的事件序列),
|
|
21
|
+
// 断言:
|
|
22
|
+
// 1. 官方 toServerSentEventsResponse 编码的 AG-UI 事件序列
|
|
23
|
+
// RUN_STARTED → CUSTOM(video:job:created) → CUSTOM(video:status)* →
|
|
24
|
+
// CUSTOM(generation:result) → RUN_FINISHED
|
|
25
|
+
// 2. useGenerateVideo 客户端信封(generationParamsFromRequest)解析:
|
|
26
|
+
// forwardedProps 里的 prompt/referenceImage 传入媒体管线,runId/threadId 透传
|
|
27
|
+
// 3. request.signal.abort() → 编码器 abort → 上游 generator finally 执行
|
|
28
|
+
// 4. 鉴权/权益/配额/限流/线程归属/AI key 的拒绝面(401/400/404/429/403)
|
|
29
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
30
|
+
import type { StreamChunk } from '@tanstack/ai'
|
|
31
|
+
|
|
32
|
+
const mocks = vi.hoisted(() => ({
|
|
33
|
+
session: vi.fn(),
|
|
34
|
+
feature: vi.fn(async () => undefined),
|
|
35
|
+
resource: vi.fn(async () => undefined),
|
|
36
|
+
rateLimit: vi.fn(async () => undefined),
|
|
37
|
+
resolveAiConfig: vi.fn<() => Promise<{ provider: string; apiKey: string; model: string; baseUrl?: string } | null>>(async () => ({ provider: 'openai', apiKey: 'test', model: 'sora-2' })),
|
|
38
|
+
thread: vi.fn<() => Promise<{ id: string } | null>>(async () => ({ id: 'thread-1' })),
|
|
39
|
+
streamVideo: vi.fn(),
|
|
40
|
+
}))
|
|
41
|
+
|
|
42
|
+
vi.mock('@/lib/session', () => ({
|
|
43
|
+
getCurrentSession: () => mocks.session(),
|
|
44
|
+
}))
|
|
45
|
+
vi.mock('@/server/entitlements', () => ({
|
|
46
|
+
requireFeature: mocks.feature,
|
|
47
|
+
FeatureUnavailableError: class FeatureUnavailableError extends Error {
|
|
48
|
+
constructor(feature: string) {
|
|
49
|
+
super(`feature ${feature} unavailable`)
|
|
50
|
+
this.name = 'FeatureUnavailableError'
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
FeatureForbiddenError: class FeatureForbiddenError extends Error {
|
|
54
|
+
constructor(feature: string) {
|
|
55
|
+
super(`feature ${feature} forbidden`)
|
|
56
|
+
this.name = 'FeatureForbiddenError'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}))
|
|
60
|
+
vi.mock('@/server/resources', () => ({ requireResource: mocks.resource }))
|
|
61
|
+
vi.mock('@/server/middlewares/rate-limit-server-fn', () => ({
|
|
62
|
+
checkAiRateLimit: mocks.rateLimit,
|
|
63
|
+
RateLimitExceededError: class RateLimitExceededError extends Error {
|
|
64
|
+
constructor(
|
|
65
|
+
public readonly retryAfterMs: number,
|
|
66
|
+
public readonly endpoint: string,
|
|
67
|
+
) {
|
|
68
|
+
super(`Rate limit exceeded for ${endpoint}`)
|
|
69
|
+
this.name = 'RateLimitExceededError'
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
}))
|
|
73
|
+
vi.mock('@/server/ai-gateway/adapter', () => ({ resolveAiConfig: mocks.resolveAiConfig }))
|
|
74
|
+
vi.mock('@/server/ai-gateway/media', () => ({ streamVideoForUser: mocks.streamVideo }))
|
|
75
|
+
vi.mock('@allbluecn/database/get-prisma', () => ({
|
|
76
|
+
getPrisma: async () => ({ chatThread: { findFirst: mocks.thread } }),
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
import { Route } from '../media'
|
|
80
|
+
|
|
81
|
+
type MediaRouteServerOptions = {
|
|
82
|
+
handlers?: {
|
|
83
|
+
POST?: (ctx: { request: Request }) => Promise<Response>
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const serverOptions = Route.options.server as MediaRouteServerOptions | undefined
|
|
88
|
+
const post: (ctx: { request: Request }) => Promise<Response> =
|
|
89
|
+
serverOptions?.handlers?.POST ??
|
|
90
|
+
(async () => {
|
|
91
|
+
throw new Error('POST handler is not configured')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ===== mock 流式产物 =====
|
|
95
|
+
|
|
96
|
+
function runStarted(): StreamChunk {
|
|
97
|
+
return { type: 'RUN_STARTED', runId: 'run-1', threadId: 'thread-1', timestamp: Date.now() } as StreamChunk
|
|
98
|
+
}
|
|
99
|
+
function custom(name: string, value: unknown): StreamChunk {
|
|
100
|
+
return { type: 'CUSTOM', name, value, timestamp: Date.now() } as StreamChunk
|
|
101
|
+
}
|
|
102
|
+
function runFinished(): StreamChunk {
|
|
103
|
+
return { type: 'RUN_FINISHED', runId: 'run-1', threadId: 'thread-1', finishReason: 'stop', timestamp: Date.now() } as StreamChunk
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let streamCancelled = false
|
|
107
|
+
|
|
108
|
+
/** 官方 generateVideo stream 模式的完整事件序列(create → poll → result) */
|
|
109
|
+
async function* videoLifecycleStream(): AsyncGenerator<StreamChunk> {
|
|
110
|
+
try {
|
|
111
|
+
yield runStarted()
|
|
112
|
+
yield custom('video:job:created', { jobId: 'job-9' })
|
|
113
|
+
yield custom('video:status', { jobId: 'job-9', status: 'pending', progress: 0 })
|
|
114
|
+
yield custom('video:status', { jobId: 'job-9', status: 'processing', progress: 55 })
|
|
115
|
+
yield custom('generation:result', { jobId: 'job-9', status: 'completed', url: 'data:video/mp4;base64,AAAA' })
|
|
116
|
+
yield runFinished()
|
|
117
|
+
} finally {
|
|
118
|
+
streamCancelled = true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 首个事件后暂停:abort 传播测试用 */
|
|
123
|
+
async function* pausingStream(): AsyncGenerator<StreamChunk> {
|
|
124
|
+
try {
|
|
125
|
+
yield runStarted()
|
|
126
|
+
yield custom('video:job:created', { jobId: 'job-9' })
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 80))
|
|
128
|
+
yield runFinished()
|
|
129
|
+
} finally {
|
|
130
|
+
streamCancelled = true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ===== 请求/断言工具 =====
|
|
135
|
+
|
|
136
|
+
/** useGenerateVideo(fetchServerSentEvents)发出的 AG-UI RunAgentInput 信封 */
|
|
137
|
+
function generateRequest(forwardedProps: unknown, signal?: AbortSignal): Request {
|
|
138
|
+
return new Request('http://localhost/api/generate/media', {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'content-type': 'application/json' },
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
threadId: 'thread-1',
|
|
143
|
+
runId: 'run-1',
|
|
144
|
+
messages: [],
|
|
145
|
+
state: {},
|
|
146
|
+
tools: [],
|
|
147
|
+
context: [],
|
|
148
|
+
forwardedProps,
|
|
149
|
+
}),
|
|
150
|
+
...(signal ? { signal } : {}),
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function readSseEvents(res: Response): Promise<Array<Record<string, unknown>>> {
|
|
155
|
+
const text = await res.text()
|
|
156
|
+
return text
|
|
157
|
+
.split('\n\n')
|
|
158
|
+
.filter((block) => block.trim().length > 0)
|
|
159
|
+
.map((block) => {
|
|
160
|
+
const dataLine = block.split('\n').find((line) => line.startsWith('data: '))
|
|
161
|
+
expect(dataLine, `SSE 帧缺少 data 行: ${block}`).toBeDefined()
|
|
162
|
+
return JSON.parse((dataLine as string).slice('data: '.length)) as Record<string, unknown>
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function eventsOf(events: Array<Record<string, unknown>>, name: string) {
|
|
167
|
+
return events.filter((event) => event.type === 'CUSTOM' && event.name === name)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
vi.clearAllMocks()
|
|
172
|
+
streamCancelled = false
|
|
173
|
+
mocks.session.mockResolvedValue({ user: { id: 'u1' } })
|
|
174
|
+
mocks.resolveAiConfig.mockResolvedValue({ provider: 'openai', apiKey: 'test', model: 'sora-2' })
|
|
175
|
+
mocks.thread.mockResolvedValue({ id: 'thread-1' })
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
afterEach(() => {
|
|
179
|
+
vi.restoreAllMocks()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
describe('/api/generate/media 流式视频生成', () => {
|
|
183
|
+
it('下发官方事件序列 job:created → video:status → generation:result → RUN_FINISHED', async () => {
|
|
184
|
+
mocks.streamVideo.mockResolvedValueOnce(videoLifecycleStream())
|
|
185
|
+
|
|
186
|
+
const res = await post({ request: generateRequest({ prompt: '城市延时摄影' }) })
|
|
187
|
+
|
|
188
|
+
expect(res.status).toBe(200)
|
|
189
|
+
expect(res.headers.get('content-type')).toContain('text/event-stream')
|
|
190
|
+
|
|
191
|
+
const events = await readSseEvents(res)
|
|
192
|
+
const jobCreated = eventsOf(events, 'video:job:created')
|
|
193
|
+
expect(jobCreated).toHaveLength(1)
|
|
194
|
+
expect((jobCreated[0].value as { jobId: string }).jobId).toBe('job-9')
|
|
195
|
+
|
|
196
|
+
const statuses = eventsOf(events, 'video:status')
|
|
197
|
+
expect(statuses).toHaveLength(2)
|
|
198
|
+
expect((statuses[0].value as { status: string; progress?: number })).toMatchObject({ status: 'pending', progress: 0 })
|
|
199
|
+
expect((statuses[1].value as { status: string; progress?: number })).toMatchObject({ status: 'processing', progress: 55 })
|
|
200
|
+
|
|
201
|
+
const results = eventsOf(events, 'generation:result')
|
|
202
|
+
expect(results).toHaveLength(1)
|
|
203
|
+
expect((results[0].value as { url: string }).url).toContain('data:video/mp4')
|
|
204
|
+
|
|
205
|
+
expect(events.some((event) => event.type === 'RUN_STARTED')).toBe(true)
|
|
206
|
+
expect(events.some((event) => event.type === 'RUN_FINISHED')).toBe(true)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('透传 runId/threadId/prompt 与解析出的 AI 配置给媒体管线', async () => {
|
|
210
|
+
mocks.streamVideo.mockResolvedValueOnce(videoLifecycleStream())
|
|
211
|
+
|
|
212
|
+
await post({ request: generateRequest({ prompt: '海边日落', duration: 5 }) })
|
|
213
|
+
|
|
214
|
+
expect(mocks.streamVideo).toHaveBeenCalledTimes(1)
|
|
215
|
+
expect(mocks.streamVideo).toHaveBeenCalledWith(expect.objectContaining({
|
|
216
|
+
userId: 'u1',
|
|
217
|
+
threadId: 'thread-1',
|
|
218
|
+
runId: 'run-1',
|
|
219
|
+
prompt: '海边日落',
|
|
220
|
+
duration: 5,
|
|
221
|
+
adapterKind: 'openai',
|
|
222
|
+
apiKey: 'test',
|
|
223
|
+
model: 'sora-2',
|
|
224
|
+
}))
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('prompt 为 MediaPrompt 分片时原样透传(参考帧由管线归一)', async () => {
|
|
228
|
+
mocks.streamVideo.mockResolvedValueOnce(videoLifecycleStream())
|
|
229
|
+
const parts = [
|
|
230
|
+
{ type: 'text', content: '推进镜头' },
|
|
231
|
+
{ type: 'image', source: { type: 'url', value: 'data:image/png;base64,QQ==' }, metadata: { role: 'start_frame' } },
|
|
232
|
+
]
|
|
233
|
+
await post({ request: generateRequest({ prompt: parts }) })
|
|
234
|
+
expect(mocks.streamVideo).toHaveBeenCalledWith(expect.objectContaining({ prompt: parts }))
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('request.signal.abort() → 编码器 abort,上游 generator finally 执行', async () => {
|
|
238
|
+
const controller = new AbortController()
|
|
239
|
+
mocks.streamVideo.mockResolvedValueOnce(pausingStream())
|
|
240
|
+
|
|
241
|
+
const res = await post({ request: generateRequest({ prompt: '一个长镜头' }, controller.signal) })
|
|
242
|
+
expect(res.status).toBe(200)
|
|
243
|
+
|
|
244
|
+
const reader = res.body?.getReader()
|
|
245
|
+
expect(reader).toBeDefined()
|
|
246
|
+
// 消费到 job:created 帧(编码器按批下发,首帧可能只有 RUN_STARTED)
|
|
247
|
+
const decoder = new TextDecoder()
|
|
248
|
+
let head = ''
|
|
249
|
+
for (;;) {
|
|
250
|
+
const chunk = await reader!.read()
|
|
251
|
+
if (chunk.done) break
|
|
252
|
+
head += decoder.decode(chunk.value)
|
|
253
|
+
if (head.includes('video:job:created')) break
|
|
254
|
+
}
|
|
255
|
+
expect(head).toContain('video:job:created')
|
|
256
|
+
|
|
257
|
+
controller.abort()
|
|
258
|
+
|
|
259
|
+
let closed = false
|
|
260
|
+
try {
|
|
261
|
+
for (;;) {
|
|
262
|
+
const chunk = await reader!.read()
|
|
263
|
+
if (chunk.done) {
|
|
264
|
+
closed = true
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
closed = true
|
|
270
|
+
}
|
|
271
|
+
expect(closed).toBe(true)
|
|
272
|
+
|
|
273
|
+
await vi.waitFor(() => expect(streamCancelled).toBe(true), { timeout: 2000 })
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('鉴权失败返回 401,且不进入权益与媒体管线', async () => {
|
|
277
|
+
mocks.session.mockResolvedValueOnce(null)
|
|
278
|
+
|
|
279
|
+
const res = await post({ request: generateRequest({ prompt: 'p' }) })
|
|
280
|
+
|
|
281
|
+
expect(res.status).toBe(401)
|
|
282
|
+
expect(mocks.feature).not.toHaveBeenCalled()
|
|
283
|
+
expect(mocks.streamVideo).not.toHaveBeenCalled()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('权益拒绝映射:feature forbidden → 403、unavailable → 501、资源超限 → 429、限流 → 429', async () => {
|
|
287
|
+
const { FeatureForbiddenError, FeatureUnavailableError } = await import('@/server/entitlements')
|
|
288
|
+
const { ResourceLimitError } = await import('@/lib/resource-errors')
|
|
289
|
+
const { RateLimitExceededError } = await import('@/server/middlewares/rate-limit-server-fn')
|
|
290
|
+
|
|
291
|
+
mocks.feature.mockRejectedValueOnce(new FeatureForbiddenError('mediaGeneration'))
|
|
292
|
+
expect((await post({ request: generateRequest({ prompt: 'p' }) })).status).toBe(403)
|
|
293
|
+
|
|
294
|
+
mocks.feature.mockRejectedValueOnce(new FeatureUnavailableError('mediaGeneration'))
|
|
295
|
+
expect((await post({ request: generateRequest({ prompt: 'p' }) })).status).toBe(501)
|
|
296
|
+
|
|
297
|
+
mocks.resource.mockRejectedValueOnce(new ResourceLimitError('mediaGenerationsMonthly', 10, 10))
|
|
298
|
+
expect((await post({ request: generateRequest({ prompt: 'p' }) })).status).toBe(429)
|
|
299
|
+
|
|
300
|
+
mocks.rateLimit.mockRejectedValueOnce(new RateLimitExceededError(30_000, 'mediaVideo'))
|
|
301
|
+
const limited = await post({ request: generateRequest({ prompt: 'p' }) })
|
|
302
|
+
expect(limited.status).toBe(429)
|
|
303
|
+
expect(limited.headers.get('Retry-After')).toBe('30')
|
|
304
|
+
expect(mocks.streamVideo).not.toHaveBeenCalled()
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('请求体缺 threadId/runId/forwardedProps → 400,媒体管线未调用', async () => {
|
|
308
|
+
for (const body of [
|
|
309
|
+
JSON.stringify({ runId: 'run-1', forwardedProps: { prompt: 'p' } }),
|
|
310
|
+
JSON.stringify({ threadId: 'thread-1', forwardedProps: { prompt: 'p' } }),
|
|
311
|
+
JSON.stringify({ threadId: 'thread-1', runId: 'run-1' }),
|
|
312
|
+
]) {
|
|
313
|
+
const res = await post({
|
|
314
|
+
request: new Request('http://localhost/api/generate/media', { method: 'POST', headers: { 'content-type': 'application/json' }, body }),
|
|
315
|
+
})
|
|
316
|
+
expect(res.status).toBe(400)
|
|
317
|
+
}
|
|
318
|
+
expect(mocks.streamVideo).not.toHaveBeenCalled()
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('prompt 非字符串/分片形态 → 400', async () => {
|
|
322
|
+
const res = await post({ request: generateRequest({ prompt: 42 }) })
|
|
323
|
+
expect(res.status).toBe(400)
|
|
324
|
+
expect(mocks.streamVideo).not.toHaveBeenCalled()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('线程不属于请求用户 → 404', async () => {
|
|
328
|
+
mocks.thread.mockResolvedValueOnce(null)
|
|
329
|
+
|
|
330
|
+
const res = await post({ request: generateRequest({ prompt: 'p' }) })
|
|
331
|
+
|
|
332
|
+
expect(res.status).toBe(404)
|
|
333
|
+
expect(mocks.streamVideo).not.toHaveBeenCalled()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('AI key 未配置 → 400', async () => {
|
|
337
|
+
mocks.resolveAiConfig.mockResolvedValueOnce(null)
|
|
338
|
+
const res = await post({ request: generateRequest({ prompt: 'p' }) })
|
|
339
|
+
|
|
340
|
+
expect(res.status).toBe(400)
|
|
341
|
+
expect(mocks.streamVideo).not.toHaveBeenCalled()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('参考帧/prompt 图片分片的管线侧错误映射 400(任务 15 审查 ②)', async () => {
|
|
345
|
+
const { FetchGuardError } = await import('@/server/fetch-guard')
|
|
346
|
+
|
|
347
|
+
// SSRF 守卫拒绝(私网/元数据目标)
|
|
348
|
+
mocks.streamVideo.mockRejectedValueOnce(new FetchGuardError('http://169.254.169.254/latest/meta-data', 'private/metadata address'))
|
|
349
|
+
const guarded = await post({ request: generateRequest({ prompt: 'p' }) })
|
|
350
|
+
expect(guarded.status).toBe(400)
|
|
351
|
+
expect(((await guarded.json()) as { error: string }).error).toBe('unfetchable_reference_image')
|
|
352
|
+
|
|
353
|
+
// imageDataUrl 格式/大小校验错误
|
|
354
|
+
mocks.streamVideo.mockRejectedValueOnce(new Error('Image too large'))
|
|
355
|
+
const oversized = await post({ request: generateRequest({ prompt: 'p' }) })
|
|
356
|
+
expect(oversized.status).toBe(400)
|
|
357
|
+
expect(((await oversized.json()) as { error: string }).error).toBe('invalid_media_input')
|
|
358
|
+
|
|
359
|
+
// adapter 无视频能力(模板字符串,前缀匹配)
|
|
360
|
+
mocks.streamVideo.mockRejectedValueOnce(new Error('Video generation is not available for adapter kind: openai'))
|
|
361
|
+
const unavailable = await post({ request: generateRequest({ prompt: 'p' }) })
|
|
362
|
+
expect(unavailable.status).toBe(400)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('管线侧未知异常保持上抛(500 语义,不误映射 400)', async () => {
|
|
366
|
+
mocks.streamVideo.mockRejectedValueOnce(new Error('connection refused'))
|
|
367
|
+
|
|
368
|
+
await expect(post({ request: generateRequest({ prompt: 'p' }) })).rejects.toThrow('connection refused')
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('媒体管线拒绝(线程归属复核)→ 404', async () => {
|
|
372
|
+
mocks.streamVideo.mockRejectedValueOnce(new Error('Thread not found'))
|
|
373
|
+
|
|
374
|
+
const res = await post({ request: generateRequest({ prompt: 'p' }) })
|
|
375
|
+
|
|
376
|
+
expect(res.status).toBe(404)
|
|
377
|
+
})
|
|
378
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
// 媒体生成 SSE 进度通道(任务 15):官方 generateVideo stream 模式(服务端内部
|
|
19
|
+
// create → poll → complete 全生命周期)→ 官方 toServerSentEventsResponse 编码下发,
|
|
20
|
+
// 事件序列 RUN_STARTED → CUSTOM(video:job:created) → CUSTOM(video:status)* →
|
|
21
|
+
// CUSTOM(generation:result) → RUN_FINISHED(失败 RUN_ERROR),与
|
|
22
|
+
// useGenerateVideo 客户端逐事件对应。请求体为 @tanstack/ai-react 生成 hooks 的
|
|
23
|
+
// AG-UI RunAgentInput 信封(业务字段在 forwardedProps),经官方
|
|
24
|
+
// generationParamsFromRequest('video', request) 解析。
|
|
25
|
+
// 鉴权/权益/配额/限流口径沿用被替换的 startVideoFn(轮询面随任务 15 删除);
|
|
26
|
+
// abort 语义:request.signal → AbortController 传入官方编码器,客户端断开即停。
|
|
27
|
+
|
|
28
|
+
import { createFileRoute } from "@tanstack/react-router"
|
|
29
|
+
import { generationParamsFromRequest, toServerSentEventsResponse } from "@tanstack/ai"
|
|
30
|
+
import { z } from "zod"
|
|
31
|
+
import { getCurrentSession } from "@/lib/session"
|
|
32
|
+
import {
|
|
33
|
+
checkAiRateLimit,
|
|
34
|
+
RateLimitExceededError,
|
|
35
|
+
} from "@/server/middlewares/rate-limit-server-fn"
|
|
36
|
+
import { requireFeature, FeatureUnavailableError, FeatureForbiddenError } from "@/server/entitlements"
|
|
37
|
+
import { requireResource } from "@/server/resources"
|
|
38
|
+
import { ResourceLimitError } from "@/lib/resource-errors"
|
|
39
|
+
import { FetchGuardError } from "@/server/fetch-guard"
|
|
40
|
+
import { resolveAiConfig } from "@/server/ai-gateway/adapter"
|
|
41
|
+
import { streamVideoForUser } from "@/server/ai-gateway/media"
|
|
42
|
+
import { getPrisma } from "@allbluecn/database/get-prisma"
|
|
43
|
+
|
|
44
|
+
// referenceImage 为用户可控 URL/数据 URL,媒体管线(imageDataUrl)内做 SSRF 守卫
|
|
45
|
+
// (assertFetchableUrl + redirect:error + 超时)与 20MB 上限;prompt 的 url source
|
|
46
|
+
// 同样在管线内归一,此处仅限输入长度。
|
|
47
|
+
const textPartSchema = z.object({ type: z.literal("text"), content: z.string().min(1).max(4000) })
|
|
48
|
+
const imagePartSchema = z.object({
|
|
49
|
+
type: z.literal("image"),
|
|
50
|
+
source: z.object({ type: z.literal("url"), value: z.string().min(1).max(16_000_000) }),
|
|
51
|
+
// 与官方 MediaInputRole 对齐(z.enum 产出窄化类型,供 streamVideoForUser 的 MediaPrompt 形参直接消费)
|
|
52
|
+
metadata: z.object({ role: z.enum(["reference", "mask", "control", "start_frame", "end_frame", "character"]) }).optional(),
|
|
53
|
+
})
|
|
54
|
+
const promptSchema = z.union([
|
|
55
|
+
z.string().min(1).max(4000),
|
|
56
|
+
z.array(z.union([textPartSchema, imagePartSchema])).min(1).max(8),
|
|
57
|
+
])
|
|
58
|
+
const videoInputSchema = z.object({
|
|
59
|
+
prompt: promptSchema,
|
|
60
|
+
duration: z.number().optional(),
|
|
61
|
+
size: z.string().max(40).optional(),
|
|
62
|
+
referenceImage: z.string().max(16_000_000).optional(),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// 管线内请求侧校验错误(imageDataUrl 格式/大小/下载 + adapter 无视频能力)——
|
|
66
|
+
// 与 zod 拒绝面同属 400;不在集合内的异常(如 prisma 故障)保持 500 语义上抛
|
|
67
|
+
const MEDIA_REQUEST_ERRORS: ReadonlySet<string> = new Set([
|
|
68
|
+
"Invalid image data",
|
|
69
|
+
"Image too large",
|
|
70
|
+
"Image download failed",
|
|
71
|
+
"Invalid image content type",
|
|
72
|
+
"Empty image",
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
function jsonResponse(error: string, status: number, init?: { headers?: HeadersInit; detail?: string }): Response {
|
|
76
|
+
// detail 走 JSON body 而非响应头:错误信息含中文(FetchGuardError 文案),
|
|
77
|
+
// HTTP 头值必须是 ByteString,非 ASCII 会抛 TypeError
|
|
78
|
+
return new Response(JSON.stringify(init?.detail ? { error, detail: init.detail } : { error }), {
|
|
79
|
+
status,
|
|
80
|
+
headers: { "Content-Type": "application/json", ...init?.headers },
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function authorize(request: Request, userId: string): Promise<Response | undefined> {
|
|
85
|
+
try {
|
|
86
|
+
await requireFeature(request, "mediaGeneration")
|
|
87
|
+
await requireResource(request, "mediaGenerationsMonthly")
|
|
88
|
+
await checkAiRateLimit(userId, "mediaVideo")
|
|
89
|
+
return undefined
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err instanceof RateLimitExceededError) {
|
|
92
|
+
return jsonResponse("rate_limit_exceeded", 429, { headers: { "Retry-After": String(Math.ceil(err.retryAfterMs / 1000)) } })
|
|
93
|
+
}
|
|
94
|
+
if (err instanceof FeatureUnavailableError) return jsonResponse("feature_unavailable", 501)
|
|
95
|
+
if (err instanceof FeatureForbiddenError) return jsonResponse("feature_forbidden", 403)
|
|
96
|
+
if (err instanceof ResourceLimitError) return jsonResponse("resource_limit_exceeded", 429)
|
|
97
|
+
throw err
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const Route = createFileRoute("/api/generate/media")({
|
|
102
|
+
server: {
|
|
103
|
+
handlers: {
|
|
104
|
+
POST: async ({ request }) => {
|
|
105
|
+
const session = await getCurrentSession(request)
|
|
106
|
+
if (!session?.user?.id) {
|
|
107
|
+
return new Response("Unauthorized", { status: 401 })
|
|
108
|
+
}
|
|
109
|
+
const userId = session.user.id
|
|
110
|
+
|
|
111
|
+
const denied = await authorize(request, userId)
|
|
112
|
+
if (denied) return denied
|
|
113
|
+
|
|
114
|
+
let params: { input: Record<string, unknown>; threadId: string; runId: string }
|
|
115
|
+
try {
|
|
116
|
+
params = await generationParamsFromRequest("video", request)
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return jsonResponse("invalid_request_body", 400, {
|
|
119
|
+
detail: err instanceof Error ? err.message : "unknown",
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
const parsed = videoInputSchema.safeParse(params.input)
|
|
123
|
+
if (!parsed.success) return jsonResponse("invalid_media_input", 400)
|
|
124
|
+
const input = parsed.data
|
|
125
|
+
|
|
126
|
+
const prisma = await getPrisma()
|
|
127
|
+
const thread = await prisma.chatThread.findFirst({
|
|
128
|
+
where: { id: params.threadId, userId }, select: { id: true },
|
|
129
|
+
})
|
|
130
|
+
if (!thread) return jsonResponse("thread_not_found", 404)
|
|
131
|
+
|
|
132
|
+
const aiConfig = await resolveAiConfig(userId)
|
|
133
|
+
if (!aiConfig) return jsonResponse("ai_key_not_configured", 400)
|
|
134
|
+
|
|
135
|
+
let stream: Awaited<ReturnType<typeof streamVideoForUser>>
|
|
136
|
+
try {
|
|
137
|
+
stream = await streamVideoForUser({
|
|
138
|
+
userId,
|
|
139
|
+
threadId: params.threadId,
|
|
140
|
+
runId: params.runId,
|
|
141
|
+
prompt: input.prompt,
|
|
142
|
+
duration: input.duration,
|
|
143
|
+
size: input.size,
|
|
144
|
+
referenceImage: input.referenceImage,
|
|
145
|
+
adapterKind: aiConfig.provider,
|
|
146
|
+
apiKey: aiConfig.apiKey,
|
|
147
|
+
model: aiConfig.model,
|
|
148
|
+
...(aiConfig.baseUrl ? { baseURL: aiConfig.baseUrl } : {}),
|
|
149
|
+
})
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err instanceof Error && err.message === "Thread not found") {
|
|
152
|
+
return jsonResponse("thread_not_found", 404)
|
|
153
|
+
}
|
|
154
|
+
// 参考帧/prompt 图片分片在管线内做 SSRF 守卫(FetchGuardError)与
|
|
155
|
+
// 格式/大小校验(imageDataUrl 抛普通 Error),adapter 未配置视频能力
|
|
156
|
+
// 同属请求侧问题——统一 400 而非未映射 500(任务 15 审查 ②)
|
|
157
|
+
if (err instanceof FetchGuardError) {
|
|
158
|
+
return jsonResponse("unfetchable_reference_image", 400, { detail: err.message })
|
|
159
|
+
}
|
|
160
|
+
if (err instanceof Error && (MEDIA_REQUEST_ERRORS.has(err.message) || err.message.startsWith("Video generation is not available for adapter kind"))) {
|
|
161
|
+
return jsonResponse("invalid_media_input", 400, { detail: err.message })
|
|
162
|
+
}
|
|
163
|
+
throw err
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const abortController = new AbortController()
|
|
167
|
+
request.signal.addEventListener("abort", () => abortController.abort())
|
|
168
|
+
return toServerSentEventsResponse(stream, {
|
|
169
|
+
abortController,
|
|
170
|
+
headers: {
|
|
171
|
+
"Cache-Control": "no-cache",
|
|
172
|
+
Connection: "keep-alive",
|
|
173
|
+
"X-Accel-Buffering": "no",
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
})
|
|
@@ -32,24 +32,57 @@ const mocks = vi.hoisted(() => ({
|
|
|
32
32
|
}))
|
|
33
33
|
|
|
34
34
|
vi.mock('@tanstack/react-start', () => ({
|
|
35
|
+
// 迁移后经 .middleware([authContextMiddleware]) 注入 context.user:
|
|
36
|
+
// mock 依序执行 middleware 的 server 段(复现框架 executeMiddleware 的
|
|
37
|
+
// context 合并语义),真实鉴权链由被 mock 的 auth-helpers/getRequest 承接
|
|
35
38
|
createServerFn: (config: any) => {
|
|
36
39
|
let validator: any = undefined
|
|
37
|
-
|
|
40
|
+
const api: any = {
|
|
38
41
|
_method: config.method,
|
|
42
|
+
_middlewares: [] as any[],
|
|
43
|
+
middleware(mws: any[]) {
|
|
44
|
+
api._middlewares.push(...mws)
|
|
45
|
+
return api
|
|
46
|
+
},
|
|
39
47
|
validator(schema: any) {
|
|
40
48
|
validator = schema
|
|
41
|
-
return
|
|
49
|
+
return api
|
|
42
50
|
},
|
|
43
51
|
handler(fn: Function) {
|
|
44
|
-
return function fn2(args: any = {}) {
|
|
52
|
+
return async function fn2(args: any = {}) {
|
|
45
53
|
let data = args?.data
|
|
46
54
|
if (validator && data !== undefined) {
|
|
47
55
|
data = validator.parse ? validator.parse(data) : data
|
|
48
56
|
}
|
|
49
|
-
|
|
57
|
+
let context: any = {}
|
|
58
|
+
for (const mw of api._middlewares) {
|
|
59
|
+
const serverFn = mw?.options?.server
|
|
60
|
+
if (!serverFn) continue
|
|
61
|
+
const result = await serverFn({
|
|
62
|
+
data,
|
|
63
|
+
context,
|
|
64
|
+
method: 'POST',
|
|
65
|
+
serverFnMeta: {},
|
|
66
|
+
signal: new AbortController().signal,
|
|
67
|
+
next: async (inner: any) => ({ context: { ...context, ...(inner?.context ?? {}) } }),
|
|
68
|
+
})
|
|
69
|
+
context = result.context
|
|
70
|
+
}
|
|
71
|
+
return fn({ data, context })
|
|
50
72
|
}
|
|
51
73
|
},
|
|
52
74
|
}
|
|
75
|
+
return api
|
|
76
|
+
},
|
|
77
|
+
// auth-context.ts 依赖 createMiddleware({ type: 'function' }).server(fn):
|
|
78
|
+
// mock 产出与真实 middleware 同构的 { options: { server } } 形状
|
|
79
|
+
createMiddleware: (opts: any) => {
|
|
80
|
+
const mw: any = { options: { type: opts?.type } }
|
|
81
|
+
mw.server = (fn: Function) => {
|
|
82
|
+
mw.options.server = fn
|
|
83
|
+
return mw
|
|
84
|
+
}
|
|
85
|
+
return mw
|
|
53
86
|
},
|
|
54
87
|
}))
|
|
55
88
|
|