@allbluecn/web-app 0.10.2 → 0.11.0
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 +26 -11
- package/public/favicon.ico +0 -0
- package/public/logo/allblue-logo-app-icon.svg +7 -0
- package/public/logo/allblue-logo-dark-square.svg +4 -0
- package/public/logo/allblue-logo-dark.svg +4 -0
- package/public/logo/allblue-logo-light-square.svg +4 -0
- package/public/logo/allblue-logo-light.svg +4 -0
- package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
- package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
- package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
- package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
- package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
- package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
- package/src/__tests__/security/xss-sanitize.test.ts +99 -0
- package/src/__tests__/setup.ts +9 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
- package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
- package/src/components/error/global-error-boundary.tsx +2 -2
- package/src/components/knowledge/general-groups.ts +15 -0
- package/src/components/knowledge/kb-filter-popover.tsx +143 -0
- package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
- package/src/components/layout/sidebar-03/nav-secondary.tsx +57 -47
- package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
- package/src/components/mdx.tsx +9 -0
- package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
- package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
- package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
- package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
- package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
- package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
- package/src/components/story/relations/general-groups.ts +1 -15
- package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
- package/src/components/story/story-list-view.tsx +7 -1
- package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
- package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
- package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
- package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
- package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
- package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
- package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
- package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
- package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
- package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
- package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
- package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
- package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
- package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
- package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
- package/src/lib/__tests__/msw-sample.test.ts +68 -0
- package/src/lib/__tests__/pgvector.test.ts +2 -2
- package/src/lib/changelog/sdk-versions.ts +21 -21
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
- package/src/lib/collections/reference/workspaces-collection.ts +3 -1
- package/src/lib/fetch-json.ts +23 -0
- package/src/lib/fumadocs-i18n.ts +7 -0
- package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
- package/src/lib/layout.shared.tsx +40 -0
- package/src/lib/sanitize.ts +14 -1
- package/src/lib/session.ts +9 -1
- package/src/lib/source.ts +15 -0
- package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
- package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
- package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
- package/src/routeTree.gen.ts +65 -0
- package/src/routes/$lang/docs/$/route.lazy.tsx +62 -0
- package/src/routes/$lang/docs/$/route.tsx +52 -0
- package/src/routes/-tests/docs-redirect.test.ts +45 -0
- package/src/routes/__root.tsx +13 -7
- package/src/routes/__tests__/-edition-routing.test.tsx +4 -4
- package/src/routes/__tests__/route-tree-structure.test.ts +65 -0
- package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
- package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
- package/src/routes/_nav/route.tsx +21 -0
- package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
- package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
- package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
- package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
- package/src/routes/api/search.ts +13 -0
- package/src/routes/docs.tsx +9 -0
- package/src/server/__tests__/agent-crud.test.ts +4 -4
- package/src/server/__tests__/chat-conversations.test.ts +1 -3
- package/src/server/__tests__/fetch-guard.test.ts +99 -0
- package/src/server/__tests__/permissions.test.ts +1 -3
- package/src/server/__tests__/resources.team-seats.test.ts +2 -0
- package/src/server/__tests__/skill-crud.test.ts +3 -3
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
- package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
- package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
- package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
- package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
- package/src/server/ai-gateway/ai-output-guard.ts +77 -0
- package/src/server/ai-gateway/index.ts +5 -0
- package/src/server/ai-gateway/kb-context-guard.ts +46 -0
- package/src/server/ai-gateway/llm-fence.ts +82 -0
- package/src/server/ai-gateway/persistence/run-store.ts +19 -0
- package/src/server/ai-gateway/story-prompt.ts +4 -2
- package/src/server/ai-gateway/tools/definitions.ts +12 -6
- package/src/server/ai-gateway/tools/server.ts +97 -54
- package/src/server/builtin-project.ts +21 -0
- package/src/server/email/sender.ts +8 -2
- package/src/server/fetch-guard.ts +170 -0
- package/src/server/middlewares/security-headers.ts +113 -0
- package/src/server/serverFns/__tests__/billing.test.ts +263 -0
- package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
- package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
- package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
- package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
- package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
- package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
- package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
- package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
- package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
- package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
- package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
- package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
- package/src/server/serverFns/__tests__/team-invitation.test.ts +3 -3
- package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
- package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
- package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
- package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
- package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
- package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
- package/src/server/serverFns/kb-doc-search.ts +111 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
- package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
- package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
- package/src/server/serverFns/story/kb-general-consts.ts +39 -0
- package/src/server/serverFns/story/kb-general.ts +30 -17
- package/src/server/serverFns/story/story-assoc.ts +13 -103
- package/src/server/serverFns/story/story-crud.ts +2 -5
- package/src/server/serverFns/story/story-groups.ts +4 -13
- package/src/server/serverFns/team.ts +1 -1
- package/src/server/serverFns/workspace-kb-utils.ts +138 -0
- package/src/server/serverFns/workspace-kb.ts +403 -0
- package/src/server/serverFns/workspace.ts +4 -1
- package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
- package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
- package/src/shared/ai-cli/adapter.ts +2 -2
- package/src/shared/ai-cli/cli-detector.ts +18 -9
- package/src/start.ts +2 -1
- package/src/styles/globals.css +116 -0
- package/src/types/jest-dom-vitest.d.ts +8 -7
- package/vite.shared.ts +18 -1
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/window.svg +0 -1
- package/src/lib/__tests__/cli-detector.test.ts +0 -80
- /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
- /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
- /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
- /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
- /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
- /package/src/components/workspace/{workspace-project-list.tsx → projects/workspace-project-list.tsx} +0 -0
|
@@ -140,7 +140,7 @@ describe('listTagsFn', () => {
|
|
|
140
140
|
it('未登录时抛 UnauthorizedError', async () => {
|
|
141
141
|
mocks.getCurrentSession.mockResolvedValue(null)
|
|
142
142
|
|
|
143
|
-
await expect(listTagsFn()).rejects.
|
|
143
|
+
await expect(listTagsFn()).rejects.toThrow(/UNAUTHORIZED/)
|
|
144
144
|
})
|
|
145
145
|
})
|
|
146
146
|
|
|
@@ -194,7 +194,7 @@ describe('createTagFn', () => {
|
|
|
194
194
|
it('未登录时抛 UnauthorizedError', async () => {
|
|
195
195
|
mocks.getCurrentSession.mockResolvedValue(null)
|
|
196
196
|
|
|
197
|
-
await expect(createTagFn({ data: { name: 'AI' } })).rejects.
|
|
197
|
+
await expect(createTagFn({ data: { name: 'AI' } })).rejects.toThrow(/UNAUTHORIZED/)
|
|
198
198
|
})
|
|
199
199
|
})
|
|
200
200
|
|
|
@@ -227,7 +227,7 @@ describe('updateTagFn', () => {
|
|
|
227
227
|
mocks.getCurrentSession.mockResolvedValue({ user: { id: 'user-1' } })
|
|
228
228
|
mocks.findFirst.mockResolvedValue(null)
|
|
229
229
|
|
|
230
|
-
await expect(updateTagFn({ data: { id: 't-other', name: 'X' } })).rejects.
|
|
230
|
+
await expect(updateTagFn({ data: { id: 't-other', name: 'X' } })).rejects.toThrow(/FORBIDDEN/)
|
|
231
231
|
expect(mocks.update).not.toHaveBeenCalled()
|
|
232
232
|
})
|
|
233
233
|
|
|
@@ -237,13 +237,13 @@ describe('updateTagFn', () => {
|
|
|
237
237
|
.mockResolvedValueOnce({ id: 't1', name: 'AI', userId: 'user-1' })
|
|
238
238
|
.mockResolvedValueOnce({ id: 't2', name: 'AI-New', userId: 'user-1' })
|
|
239
239
|
|
|
240
|
-
await expect(updateTagFn({ data: { id: 't1', name: 'AI-New' } })).rejects.
|
|
240
|
+
await expect(updateTagFn({ data: { id: 't1', name: 'AI-New' } })).rejects.toThrow(/TAG_NAME_CONFLICT/)
|
|
241
241
|
})
|
|
242
242
|
|
|
243
243
|
it('未登录时抛 UnauthorizedError', async () => {
|
|
244
244
|
mocks.getCurrentSession.mockResolvedValue(null)
|
|
245
245
|
|
|
246
|
-
await expect(updateTagFn({ data: { id: 't1', name: 'X' } })).rejects.
|
|
246
|
+
await expect(updateTagFn({ data: { id: 't1', name: 'X' } })).rejects.toThrow(/UNAUTHORIZED/)
|
|
247
247
|
})
|
|
248
248
|
})
|
|
249
249
|
|
|
@@ -258,7 +258,7 @@ describe('deleteTagFn', () => {
|
|
|
258
258
|
mocks.getCurrentSession.mockResolvedValue({ user: { id: 'user-1' } })
|
|
259
259
|
mocks.findFirst.mockResolvedValue(null)
|
|
260
260
|
|
|
261
|
-
await expect(deleteTagFn({ data: { id: 't-other' } })).rejects.
|
|
261
|
+
await expect(deleteTagFn({ data: { id: 't-other' } })).rejects.toThrow(/FORBIDDEN/)
|
|
262
262
|
expect(mocks.update).not.toHaveBeenCalled()
|
|
263
263
|
})
|
|
264
264
|
|
|
@@ -279,7 +279,7 @@ describe('deleteTagFn', () => {
|
|
|
279
279
|
it('未登录时抛 UnauthorizedError', async () => {
|
|
280
280
|
mocks.getCurrentSession.mockResolvedValue(null)
|
|
281
281
|
|
|
282
|
-
await expect(deleteTagFn({ data: { id: 't1' } })).rejects.
|
|
282
|
+
await expect(deleteTagFn({ data: { id: 't1' } })).rejects.toThrow(/UNAUTHORIZED/)
|
|
283
283
|
})
|
|
284
284
|
})
|
|
285
285
|
|
|
@@ -329,7 +329,7 @@ describe('reorderTagsFn', () => {
|
|
|
329
329
|
it('未登录时抛 UnauthorizedError', async () => {
|
|
330
330
|
mocks.getCurrentSession.mockResolvedValue(null)
|
|
331
331
|
|
|
332
|
-
await expect(reorderTagsFn({ data: { orderedIds: ['t1'] } })).rejects.
|
|
332
|
+
await expect(reorderTagsFn({ data: { orderedIds: ['t1'] } })).rejects.toThrow(/UNAUTHORIZED/)
|
|
333
333
|
})
|
|
334
334
|
})
|
|
335
335
|
|
|
@@ -342,7 +342,7 @@ describe('assignTagsToPrdFn', () => {
|
|
|
342
342
|
mocks.getCurrentSession.mockResolvedValue({ user: { id: 'user-1' } })
|
|
343
343
|
mocks.prdFindFirst.mockResolvedValue(null)
|
|
344
344
|
|
|
345
|
-
await expect(assignTagsToPrdFn({ data: { prdId: 'p1', tagIds: ['t1'] } })).rejects.
|
|
345
|
+
await expect(assignTagsToPrdFn({ data: { prdId: 'p1', tagIds: ['t1'] } })).rejects.toThrow(/FORBIDDEN/)
|
|
346
346
|
})
|
|
347
347
|
|
|
348
348
|
it('校验 tagId 不属于自己时抛 FORBIDDEN', async () => {
|
|
@@ -350,7 +350,7 @@ describe('assignTagsToPrdFn', () => {
|
|
|
350
350
|
mocks.prdFindFirst.mockResolvedValue({ id: 'p1' })
|
|
351
351
|
mocks.findFirst.mockResolvedValue(null)
|
|
352
352
|
|
|
353
|
-
await expect(assignTagsToPrdFn({ data: { prdId: 'p1', tagIds: ['t-other'] } })).rejects.
|
|
353
|
+
await expect(assignTagsToPrdFn({ data: { prdId: 'p1', tagIds: ['t-other'] } })).rejects.toThrow(/FORBIDDEN/)
|
|
354
354
|
})
|
|
355
355
|
|
|
356
356
|
it('事务内 diff 增删', async () => {
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { Route as rootRouteImport } from './routes/__root'
|
|
|
14
14
|
import { Route as LoginRouteRouteImport } from './routes/_login/route'
|
|
15
15
|
import { Route as NavRouteRouteImport } from './routes/_nav/route'
|
|
16
16
|
import { Route as PublicRouteRouteImport } from './routes/_public/route'
|
|
17
|
+
import { Route as DocsRouteImport } from './routes/docs'
|
|
17
18
|
import { Route as ShareRouteRouteImport } from './routes/share/route'
|
|
18
19
|
import { Route as WorkspaceRouteImport } from './routes/workspace'
|
|
19
20
|
import { Route as _desktopOnboardingRouteRouteImport } from './routes/__desktop/onboarding/route'
|
|
@@ -46,11 +47,13 @@ import { Route as PublicPricingRouteRouteImport } from './routes/_public/pricing
|
|
|
46
47
|
import { Route as ApiBillingWebhookRouteImport } from './routes/api/billing-webhook'
|
|
47
48
|
import { Route as ApiChatRouteImport } from './routes/api/chat'
|
|
48
49
|
import { Route as ApiHealthRouteImport } from './routes/api/health'
|
|
50
|
+
import { Route as ApiSearchRouteImport } from './routes/api/search'
|
|
49
51
|
import { Route as ApiStoryAiRouteImport } from './routes/api/story-ai'
|
|
50
52
|
import { Route as InviteTokenRouteRouteImport } from './routes/invite/$token/route'
|
|
51
53
|
import { Route as ServerAiStreamRouteImport } from './routes/server.ai-stream'
|
|
52
54
|
import { Route as ServerWsRouteImport } from './routes/server.ws'
|
|
53
55
|
import { Route as ShareVersionIdRouteRouteImport } from './routes/share/$versionId/route'
|
|
56
|
+
import { Route as LangDocsSplatRouteRouteImport } from './routes/$lang/docs/$/route'
|
|
54
57
|
import { Route as NavAppsAnyhubRouteRouteImport } from './routes/_nav/apps/anyhub/route'
|
|
55
58
|
import { Route as NavChatThreadIdRouteImport } from './routes/_nav/chat/$threadId'
|
|
56
59
|
import { Route as NavDesignIndexRouteImport } from './routes/_nav/design/index'
|
|
@@ -160,6 +163,11 @@ const PublicRouteRoute = PublicRouteRouteImport.update({
|
|
|
160
163
|
id: '/_public',
|
|
161
164
|
getParentRoute: () => rootRouteImport,
|
|
162
165
|
} as any)
|
|
166
|
+
const DocsRoute = DocsRouteImport.update({
|
|
167
|
+
id: '/docs',
|
|
168
|
+
path: '/docs',
|
|
169
|
+
getParentRoute: () => rootRouteImport,
|
|
170
|
+
} as any)
|
|
163
171
|
const ShareRouteRoute = ShareRouteRouteImport.update({
|
|
164
172
|
id: '/share',
|
|
165
173
|
path: '/share',
|
|
@@ -346,6 +354,11 @@ const ApiHealthRoute = ApiHealthRouteImport.update({
|
|
|
346
354
|
path: '/api/health',
|
|
347
355
|
getParentRoute: () => rootRouteImport,
|
|
348
356
|
} as any)
|
|
357
|
+
const ApiSearchRoute = ApiSearchRouteImport.update({
|
|
358
|
+
id: '/api/search',
|
|
359
|
+
path: '/api/search',
|
|
360
|
+
getParentRoute: () => rootRouteImport,
|
|
361
|
+
} as any)
|
|
349
362
|
const ApiStoryAiRoute = ApiStoryAiRouteImport.update({
|
|
350
363
|
id: '/api/story-ai',
|
|
351
364
|
path: '/api/story-ai',
|
|
@@ -375,6 +388,13 @@ const ShareVersionIdRouteRoute = ShareVersionIdRouteRouteImport.update({
|
|
|
375
388
|
} as any).lazy(() =>
|
|
376
389
|
import('./routes/share/$versionId/route.lazy').then((d) => d.Route),
|
|
377
390
|
)
|
|
391
|
+
const LangDocsSplatRouteRoute = LangDocsSplatRouteRouteImport.update({
|
|
392
|
+
id: '/$lang/docs/$',
|
|
393
|
+
path: '/$lang/docs/$',
|
|
394
|
+
getParentRoute: () => rootRouteImport,
|
|
395
|
+
} as any).lazy(() =>
|
|
396
|
+
import('./routes/$lang/docs/$/route.lazy').then((d) => d.Route),
|
|
397
|
+
)
|
|
378
398
|
const NavAppsAnyhubRouteRoute = NavAppsAnyhubRouteRouteImport.update({
|
|
379
399
|
id: '/anyhub',
|
|
380
400
|
path: '/anyhub',
|
|
@@ -878,6 +898,7 @@ const ApiSharePrdPrdIdEventsRoute = ApiSharePrdPrdIdEventsRouteImport.update({
|
|
|
878
898
|
export interface FileRoutesByFullPath {
|
|
879
899
|
'/': typeof PublicIndexRouteRoute
|
|
880
900
|
'/share': typeof ShareRouteRouteWithChildren
|
|
901
|
+
'/docs': typeof DocsRoute
|
|
881
902
|
'/workspace': typeof WorkspaceRoute
|
|
882
903
|
'/onboarding': typeof _desktopOnboardingRouteRoute
|
|
883
904
|
'/forgot-password': typeof LoginForgotPasswordRouteRoute
|
|
@@ -910,9 +931,11 @@ export interface FileRoutesByFullPath {
|
|
|
910
931
|
'/api/billing-webhook': typeof ApiBillingWebhookRoute
|
|
911
932
|
'/api/chat': typeof ApiChatRoute
|
|
912
933
|
'/api/health': typeof ApiHealthRoute
|
|
934
|
+
'/api/search': typeof ApiSearchRoute
|
|
913
935
|
'/api/story-ai': typeof ApiStoryAiRoute
|
|
914
936
|
'/server/ai-stream': typeof ServerAiStreamRoute
|
|
915
937
|
'/server/ws': typeof ServerWsRoute
|
|
938
|
+
'/$lang/docs/$': typeof LangDocsSplatRouteRoute
|
|
916
939
|
'/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
|
|
917
940
|
'/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
|
|
918
941
|
'/knowledge/$kbId': typeof NavKnowledgeKbIdRouteRouteWithChildren
|
|
@@ -987,6 +1010,7 @@ export interface FileRoutesByFullPath {
|
|
|
987
1010
|
export interface FileRoutesByTo {
|
|
988
1011
|
'/': typeof PublicIndexRouteRoute
|
|
989
1012
|
'/share': typeof ShareRouteRouteWithChildren
|
|
1013
|
+
'/docs': typeof DocsRoute
|
|
990
1014
|
'/workspace': typeof WorkspaceRoute
|
|
991
1015
|
'/onboarding': typeof _desktopOnboardingRouteRoute
|
|
992
1016
|
'/forgot-password': typeof LoginForgotPasswordRouteRoute
|
|
@@ -1013,9 +1037,11 @@ export interface FileRoutesByTo {
|
|
|
1013
1037
|
'/api/billing-webhook': typeof ApiBillingWebhookRoute
|
|
1014
1038
|
'/api/chat': typeof ApiChatRoute
|
|
1015
1039
|
'/api/health': typeof ApiHealthRoute
|
|
1040
|
+
'/api/search': typeof ApiSearchRoute
|
|
1016
1041
|
'/api/story-ai': typeof ApiStoryAiRoute
|
|
1017
1042
|
'/server/ai-stream': typeof ServerAiStreamRoute
|
|
1018
1043
|
'/server/ws': typeof ServerWsRoute
|
|
1044
|
+
'/$lang/docs/$': typeof LangDocsSplatRouteRoute
|
|
1019
1045
|
'/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
|
|
1020
1046
|
'/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
|
|
1021
1047
|
'/prd/$id': typeof NavPrdIdRouteRoute
|
|
@@ -1085,6 +1111,7 @@ export interface FileRoutesById {
|
|
|
1085
1111
|
'/_nav': typeof NavRouteRouteWithChildren
|
|
1086
1112
|
'/_public': typeof PublicRouteRouteWithChildren
|
|
1087
1113
|
'/share': typeof ShareRouteRouteWithChildren
|
|
1114
|
+
'/docs': typeof DocsRoute
|
|
1088
1115
|
'/workspace': typeof WorkspaceRoute
|
|
1089
1116
|
'/_public/': typeof PublicIndexRouteRoute
|
|
1090
1117
|
'/__desktop/onboarding': typeof _desktopOnboardingRouteRoute
|
|
@@ -1118,9 +1145,11 @@ export interface FileRoutesById {
|
|
|
1118
1145
|
'/api/billing-webhook': typeof ApiBillingWebhookRoute
|
|
1119
1146
|
'/api/chat': typeof ApiChatRoute
|
|
1120
1147
|
'/api/health': typeof ApiHealthRoute
|
|
1148
|
+
'/api/search': typeof ApiSearchRoute
|
|
1121
1149
|
'/api/story-ai': typeof ApiStoryAiRoute
|
|
1122
1150
|
'/server/ai-stream': typeof ServerAiStreamRoute
|
|
1123
1151
|
'/server/ws': typeof ServerWsRoute
|
|
1152
|
+
'/$lang/docs/$': typeof LangDocsSplatRouteRoute
|
|
1124
1153
|
'/_nav/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
|
|
1125
1154
|
'/_nav/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
|
|
1126
1155
|
'/_nav/knowledge/$kbId': typeof NavKnowledgeKbIdRouteRouteWithChildren
|
|
@@ -1197,6 +1226,7 @@ export interface FileRouteTypes {
|
|
|
1197
1226
|
fullPaths:
|
|
1198
1227
|
| '/'
|
|
1199
1228
|
| '/share'
|
|
1229
|
+
| '/docs'
|
|
1200
1230
|
| '/workspace'
|
|
1201
1231
|
| '/onboarding'
|
|
1202
1232
|
| '/forgot-password'
|
|
@@ -1229,9 +1259,11 @@ export interface FileRouteTypes {
|
|
|
1229
1259
|
| '/api/billing-webhook'
|
|
1230
1260
|
| '/api/chat'
|
|
1231
1261
|
| '/api/health'
|
|
1262
|
+
| '/api/search'
|
|
1232
1263
|
| '/api/story-ai'
|
|
1233
1264
|
| '/server/ai-stream'
|
|
1234
1265
|
| '/server/ws'
|
|
1266
|
+
| '/$lang/docs/$'
|
|
1235
1267
|
| '/apps/anyhub'
|
|
1236
1268
|
| '/design/$projectId'
|
|
1237
1269
|
| '/knowledge/$kbId'
|
|
@@ -1306,6 +1338,7 @@ export interface FileRouteTypes {
|
|
|
1306
1338
|
to:
|
|
1307
1339
|
| '/'
|
|
1308
1340
|
| '/share'
|
|
1341
|
+
| '/docs'
|
|
1309
1342
|
| '/workspace'
|
|
1310
1343
|
| '/onboarding'
|
|
1311
1344
|
| '/forgot-password'
|
|
@@ -1332,9 +1365,11 @@ export interface FileRouteTypes {
|
|
|
1332
1365
|
| '/api/billing-webhook'
|
|
1333
1366
|
| '/api/chat'
|
|
1334
1367
|
| '/api/health'
|
|
1368
|
+
| '/api/search'
|
|
1335
1369
|
| '/api/story-ai'
|
|
1336
1370
|
| '/server/ai-stream'
|
|
1337
1371
|
| '/server/ws'
|
|
1372
|
+
| '/$lang/docs/$'
|
|
1338
1373
|
| '/apps/anyhub'
|
|
1339
1374
|
| '/design/$projectId'
|
|
1340
1375
|
| '/prd/$id'
|
|
@@ -1403,6 +1438,7 @@ export interface FileRouteTypes {
|
|
|
1403
1438
|
| '/_nav'
|
|
1404
1439
|
| '/_public'
|
|
1405
1440
|
| '/share'
|
|
1441
|
+
| '/docs'
|
|
1406
1442
|
| '/workspace'
|
|
1407
1443
|
| '/_public/'
|
|
1408
1444
|
| '/__desktop/onboarding'
|
|
@@ -1436,9 +1472,11 @@ export interface FileRouteTypes {
|
|
|
1436
1472
|
| '/api/billing-webhook'
|
|
1437
1473
|
| '/api/chat'
|
|
1438
1474
|
| '/api/health'
|
|
1475
|
+
| '/api/search'
|
|
1439
1476
|
| '/api/story-ai'
|
|
1440
1477
|
| '/server/ai-stream'
|
|
1441
1478
|
| '/server/ws'
|
|
1479
|
+
| '/$lang/docs/$'
|
|
1442
1480
|
| '/_nav/apps/anyhub'
|
|
1443
1481
|
| '/_nav/design/$projectId'
|
|
1444
1482
|
| '/_nav/knowledge/$kbId'
|
|
@@ -1516,15 +1554,18 @@ export interface RootRouteChildren {
|
|
|
1516
1554
|
NavRouteRoute: typeof NavRouteRouteWithChildren
|
|
1517
1555
|
PublicRouteRoute: typeof PublicRouteRouteWithChildren
|
|
1518
1556
|
ShareRouteRoute: typeof ShareRouteRouteWithChildren
|
|
1557
|
+
DocsRoute: typeof DocsRoute
|
|
1519
1558
|
WorkspaceRoute: typeof WorkspaceRoute
|
|
1520
1559
|
_desktopOnboardingRouteRoute: typeof _desktopOnboardingRouteRoute
|
|
1521
1560
|
InviteTokenRouteRoute: typeof InviteTokenRouteRoute
|
|
1522
1561
|
ApiBillingWebhookRoute: typeof ApiBillingWebhookRoute
|
|
1523
1562
|
ApiChatRoute: typeof ApiChatRoute
|
|
1524
1563
|
ApiHealthRoute: typeof ApiHealthRoute
|
|
1564
|
+
ApiSearchRoute: typeof ApiSearchRoute
|
|
1525
1565
|
ApiStoryAiRoute: typeof ApiStoryAiRoute
|
|
1526
1566
|
ServerAiStreamRoute: typeof ServerAiStreamRoute
|
|
1527
1567
|
ServerWsRoute: typeof ServerWsRoute
|
|
1568
|
+
LangDocsSplatRouteRoute: typeof LangDocsSplatRouteRoute
|
|
1528
1569
|
ApiLicenseActivateRouteRoute: typeof ApiLicenseActivateRouteRoute
|
|
1529
1570
|
ApiLicenseHeartbeatRouteRoute: typeof ApiLicenseHeartbeatRouteRoute
|
|
1530
1571
|
ApiAttachmentsUploadRoute: typeof ApiAttachmentsUploadRoute
|
|
@@ -1562,6 +1603,13 @@ declare module '@tanstack/react-router' {
|
|
|
1562
1603
|
preLoaderRoute: typeof PublicRouteRouteImport
|
|
1563
1604
|
parentRoute: typeof rootRouteImport
|
|
1564
1605
|
}
|
|
1606
|
+
'/docs': {
|
|
1607
|
+
id: '/docs'
|
|
1608
|
+
path: '/docs'
|
|
1609
|
+
fullPath: '/docs'
|
|
1610
|
+
preLoaderRoute: typeof DocsRouteImport
|
|
1611
|
+
parentRoute: typeof rootRouteImport
|
|
1612
|
+
}
|
|
1565
1613
|
'/share': {
|
|
1566
1614
|
id: '/share'
|
|
1567
1615
|
path: '/share'
|
|
@@ -1786,6 +1834,13 @@ declare module '@tanstack/react-router' {
|
|
|
1786
1834
|
preLoaderRoute: typeof ApiHealthRouteImport
|
|
1787
1835
|
parentRoute: typeof rootRouteImport
|
|
1788
1836
|
}
|
|
1837
|
+
'/api/search': {
|
|
1838
|
+
id: '/api/search'
|
|
1839
|
+
path: '/api/search'
|
|
1840
|
+
fullPath: '/api/search'
|
|
1841
|
+
preLoaderRoute: typeof ApiSearchRouteImport
|
|
1842
|
+
parentRoute: typeof rootRouteImport
|
|
1843
|
+
}
|
|
1789
1844
|
'/api/story-ai': {
|
|
1790
1845
|
id: '/api/story-ai'
|
|
1791
1846
|
path: '/api/story-ai'
|
|
@@ -1821,6 +1876,13 @@ declare module '@tanstack/react-router' {
|
|
|
1821
1876
|
preLoaderRoute: typeof ShareVersionIdRouteRouteImport
|
|
1822
1877
|
parentRoute: typeof ShareRouteRoute
|
|
1823
1878
|
}
|
|
1879
|
+
'/$lang/docs/$': {
|
|
1880
|
+
id: '/$lang/docs/$'
|
|
1881
|
+
path: '/$lang/docs/$'
|
|
1882
|
+
fullPath: '/$lang/docs/$'
|
|
1883
|
+
preLoaderRoute: typeof LangDocsSplatRouteRouteImport
|
|
1884
|
+
parentRoute: typeof rootRouteImport
|
|
1885
|
+
}
|
|
1824
1886
|
'/_nav/apps/anyhub': {
|
|
1825
1887
|
id: '/_nav/apps/anyhub'
|
|
1826
1888
|
path: '/anyhub'
|
|
@@ -2761,15 +2823,18 @@ const rootRouteChildren: RootRouteChildren = {
|
|
|
2761
2823
|
NavRouteRoute: NavRouteRouteWithChildren,
|
|
2762
2824
|
PublicRouteRoute: PublicRouteRouteWithChildren,
|
|
2763
2825
|
ShareRouteRoute: ShareRouteRouteWithChildren,
|
|
2826
|
+
DocsRoute: DocsRoute,
|
|
2764
2827
|
WorkspaceRoute: WorkspaceRoute,
|
|
2765
2828
|
_desktopOnboardingRouteRoute: _desktopOnboardingRouteRoute,
|
|
2766
2829
|
InviteTokenRouteRoute: InviteTokenRouteRoute,
|
|
2767
2830
|
ApiBillingWebhookRoute: ApiBillingWebhookRoute,
|
|
2768
2831
|
ApiChatRoute: ApiChatRoute,
|
|
2769
2832
|
ApiHealthRoute: ApiHealthRoute,
|
|
2833
|
+
ApiSearchRoute: ApiSearchRoute,
|
|
2770
2834
|
ApiStoryAiRoute: ApiStoryAiRoute,
|
|
2771
2835
|
ServerAiStreamRoute: ServerAiStreamRoute,
|
|
2772
2836
|
ServerWsRoute: ServerWsRoute,
|
|
2837
|
+
LangDocsSplatRouteRoute: LangDocsSplatRouteRoute,
|
|
2773
2838
|
ApiLicenseActivateRouteRoute: ApiLicenseActivateRouteRoute,
|
|
2774
2839
|
ApiLicenseHeartbeatRouteRoute: ApiLicenseHeartbeatRouteRoute,
|
|
2775
2840
|
ApiAttachmentsUploadRoute: ApiAttachmentsUploadRoute,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createLazyFileRoute, Link } from '@tanstack/react-router'
|
|
2
|
+
import { Suspense, useEffect } from 'react'
|
|
3
|
+
import { DocsLayout } from 'fumadocs-ui/layouts/docs'
|
|
4
|
+
import {
|
|
5
|
+
DocsBody,
|
|
6
|
+
DocsDescription,
|
|
7
|
+
DocsPage,
|
|
8
|
+
DocsTitle,
|
|
9
|
+
} from 'fumadocs-ui/layouts/docs/page'
|
|
10
|
+
import { useFumadocsLoader } from 'fumadocs-core/source/client'
|
|
11
|
+
import { baseOptions, isDocsLang, type DocsLang } from '@/lib/layout.shared'
|
|
12
|
+
import { getMDXComponents } from '@/components/mdx'
|
|
13
|
+
import { docs } from '@/lib/source'
|
|
14
|
+
import { setLocale } from '@/paraglide/runtime.js'
|
|
15
|
+
|
|
16
|
+
export const Route = createLazyFileRoute('/$lang/docs/$')({
|
|
17
|
+
component: DocsRouteComponent,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function Content({ path, lang }: { path: string; lang: DocsLang }) {
|
|
21
|
+
const page = docs.getPage(path)
|
|
22
|
+
if (!page) throw new Error(`unknown page: ${path}`)
|
|
23
|
+
|
|
24
|
+
const { toc, body: MdxContent } = page
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<DocsPage toc={toc} className="pt-8">
|
|
28
|
+
<Link
|
|
29
|
+
to="/dashboard"
|
|
30
|
+
className="text-fd-muted-foreground hover:text-fd-foreground mb-2 inline-flex items-center gap-1 text-sm"
|
|
31
|
+
>
|
|
32
|
+
← {lang === 'en' ? 'Back to App' : '返回应用'}
|
|
33
|
+
</Link>
|
|
34
|
+
<DocsTitle>{page.title}</DocsTitle>
|
|
35
|
+
<DocsDescription>{page.description}</DocsDescription>
|
|
36
|
+
<DocsBody>
|
|
37
|
+
<MdxContent components={getMDXComponents()} />
|
|
38
|
+
</DocsBody>
|
|
39
|
+
</DocsPage>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function DocsRouteComponent() {
|
|
44
|
+
const { lang: rawLang } = Route.useParams()
|
|
45
|
+
const data = useFumadocsLoader(Route.useLoaderData())
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (isDocsLang(rawLang)) setLocale(rawLang)
|
|
49
|
+
}, [rawLang])
|
|
50
|
+
|
|
51
|
+
if (!isDocsLang(rawLang)) return null
|
|
52
|
+
|
|
53
|
+
const lang: DocsLang = rawLang
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<DocsLayout {...baseOptions(lang)} tree={data.pageTree}>
|
|
57
|
+
<Suspense>
|
|
58
|
+
<Content path={data.path} lang={lang} />
|
|
59
|
+
</Suspense>
|
|
60
|
+
</DocsLayout>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createFileRoute, notFound } from '@tanstack/react-router'
|
|
2
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
3
|
+
import { docs, source } from '@/lib/source'
|
|
4
|
+
|
|
5
|
+
// fumadocs-ui 16.15.8 的 getLayoutTabs 遍历 root/folder 的 children 时
|
|
6
|
+
// 会 `for of child` 递归调用 next,但 page 节点(叶子)无 children 字段导致
|
|
7
|
+
// `node.children is not iterable`。给每个无 children 的节点补 `children: []`。
|
|
8
|
+
function fixPageTreeChildren<T extends { data: unknown }>(tree: T): T {
|
|
9
|
+
const patch = (node: { type?: string; children?: unknown[] }): void => {
|
|
10
|
+
if (!node || typeof node !== 'object') return
|
|
11
|
+
if (Array.isArray(node.children)) {
|
|
12
|
+
for (const child of node.children) patch(child as Parameters<typeof patch>[0])
|
|
13
|
+
} else {
|
|
14
|
+
;(node as { children: unknown[] }).children = []
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
patch(tree.data as { children?: unknown[] })
|
|
18
|
+
return tree
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const loadDocsPage = createServerFn({ method: 'GET' })
|
|
22
|
+
.validator((data: { slugs: string[]; lang: string }) => data)
|
|
23
|
+
.handler(async ({ data: { slugs, lang } }) => {
|
|
24
|
+
const page = source.getPage(slugs, lang)
|
|
25
|
+
if (!page) return null
|
|
26
|
+
|
|
27
|
+
const pageTree = fixPageTreeChildren(await source.serializePageTree(source.getPageTree(lang)))
|
|
28
|
+
return {
|
|
29
|
+
path: page.path,
|
|
30
|
+
title: page.data.title,
|
|
31
|
+
description: page.data.description,
|
|
32
|
+
pageTree,
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const Route = createFileRoute('/$lang/docs/$')({
|
|
37
|
+
loader: async ({ params }) => {
|
|
38
|
+
const slugs = params._splat?.split('/') ?? []
|
|
39
|
+
const data = await loadDocsPage({ data: { slugs, lang: params.lang } })
|
|
40
|
+
if (!data) throw notFound()
|
|
41
|
+
await docs.getPage(data.path)?.preload()
|
|
42
|
+
return data
|
|
43
|
+
},
|
|
44
|
+
head: ({ loaderData }) => ({
|
|
45
|
+
meta: [
|
|
46
|
+
{ title: loaderData ? `${loaderData.title} - AllBlue` : 'AllBlue' },
|
|
47
|
+
...(loaderData?.description
|
|
48
|
+
? [{ name: 'description', content: loaderData.description }]
|
|
49
|
+
: []),
|
|
50
|
+
],
|
|
51
|
+
}),
|
|
52
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
vi.mock('@/paraglide/runtime.js', () => ({
|
|
4
|
+
getLocale: vi.fn(() => 'zh'),
|
|
5
|
+
}))
|
|
6
|
+
|
|
7
|
+
import { getLocale } from '@/paraglide/runtime.js'
|
|
8
|
+
import { Route } from '../docs'
|
|
9
|
+
|
|
10
|
+
type BeforeLoadFn = (ctx: never) => unknown
|
|
11
|
+
const beforeLoad = Route.options.beforeLoad as unknown as BeforeLoadFn
|
|
12
|
+
|
|
13
|
+
async function catchRedirectTarget(fn: () => unknown): Promise<string> {
|
|
14
|
+
try {
|
|
15
|
+
await fn()
|
|
16
|
+
return ''
|
|
17
|
+
} catch (e) {
|
|
18
|
+
const err = e as {
|
|
19
|
+
href?: string
|
|
20
|
+
options?: { to?: string; params?: Record<string, string> }
|
|
21
|
+
}
|
|
22
|
+
if (err.href) return err.href
|
|
23
|
+
if (err.options?.to) {
|
|
24
|
+
const p = err.options.params ?? {}
|
|
25
|
+
return err.options.to.replace(/\$(\w+)/g, (_, name: string) => p[name] ?? `$${name}`)
|
|
26
|
+
}
|
|
27
|
+
return JSON.stringify(e)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('/docs redirect', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('zh locale 重定向到 /zh/docs', async () => {
|
|
37
|
+
vi.mocked(getLocale).mockReturnValue('zh')
|
|
38
|
+
await expect(catchRedirectTarget(() => beforeLoad({} as never))).resolves.toContain('/zh/docs')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('en locale 重定向到 /en/docs', async () => {
|
|
42
|
+
vi.mocked(getLocale).mockReturnValue('en')
|
|
43
|
+
await expect(catchRedirectTarget(() => beforeLoad({} as never))).resolves.toContain('/en/docs')
|
|
44
|
+
})
|
|
45
|
+
})
|
package/src/routes/__root.tsx
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
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, Outlet, createRootRouteWithContext, HeadContent, Scripts } from '@tanstack/react-router'
|
|
18
|
+
import { Link, Outlet, createRootRouteWithContext, HeadContent, Scripts, useParams } from '@tanstack/react-router'
|
|
19
19
|
import { HotkeysProvider } from '@tanstack/react-hotkeys'
|
|
20
20
|
import { ShortcutsPanel } from '@/components/hotkeys/shortcuts-panel'
|
|
21
21
|
import { Analytics } from '@vercel/analytics/react'
|
|
@@ -23,6 +23,9 @@ import { SpeedInsights } from '@vercel/speed-insights/react'
|
|
|
23
23
|
import globalsCss from '@/styles/globals.css?url'
|
|
24
24
|
import type { RouterContext } from '@/lib/router-context'
|
|
25
25
|
import { Providers } from '@/providers'
|
|
26
|
+
import { RootProvider } from 'fumadocs-ui/provider/tanstack'
|
|
27
|
+
import { i18nProvider } from 'fumadocs-ui/i18n'
|
|
28
|
+
import { translations } from '@/lib/layout.shared'
|
|
26
29
|
import { Toaster } from '@/components/ui/sonner'
|
|
27
30
|
import { DevtoolsPanel } from '@/components/devtools'
|
|
28
31
|
import { getSessionFn } from '@/server/serverFns/auth/session'
|
|
@@ -62,6 +65,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
|
|
62
65
|
|
|
63
66
|
function RootComponent() {
|
|
64
67
|
const session = Route.useRouteContext({ select: (c) => c.session }) as Session | null
|
|
68
|
+
const { lang } = useParams({ strict: false }) as { lang?: string | undefined }
|
|
65
69
|
return (
|
|
66
70
|
<html lang={getLocale()} suppressHydrationWarning>
|
|
67
71
|
<head>
|
|
@@ -69,12 +73,14 @@ function RootComponent() {
|
|
|
69
73
|
</head>
|
|
70
74
|
<body className="w-full h-full min-h-screen antialiased">
|
|
71
75
|
<Providers session={session}>
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
<RootProvider i18n={i18nProvider(translations, lang)}>
|
|
77
|
+
<HotkeysProvider>
|
|
78
|
+
<Outlet />
|
|
79
|
+
<ShortcutsPanel />
|
|
80
|
+
<Toaster position="top-center" />
|
|
81
|
+
{import.meta.env.DEV && <DevtoolsPanel />}
|
|
82
|
+
</HotkeysProvider>
|
|
83
|
+
</RootProvider>
|
|
78
84
|
</Providers>
|
|
79
85
|
<Scripts />
|
|
80
86
|
<Analytics />
|
|
@@ -67,7 +67,7 @@ describe('_nav beforeLoad', () => {
|
|
|
67
67
|
)
|
|
68
68
|
expect(r?.to).toBe('/login')
|
|
69
69
|
expect(r?.search?.redirect).toBe('/prd?x=1')
|
|
70
|
-
})
|
|
70
|
+
}, 20000)
|
|
71
71
|
|
|
72
72
|
it('SaaS 已登录:放行', async () => {
|
|
73
73
|
const { Route } = await importNavRoute({ profileId: 'saas', desktop: false })
|
|
@@ -76,21 +76,21 @@ describe('_nav beforeLoad', () => {
|
|
|
76
76
|
Route.options.beforeLoad!({ context: { session: { user: { id: 'u1' } } }, location } as never),
|
|
77
77
|
),
|
|
78
78
|
).toBeNull()
|
|
79
|
-
})
|
|
79
|
+
}, 20000)
|
|
80
80
|
|
|
81
81
|
it('OSS 未登录:放行(登录后置)', async () => {
|
|
82
82
|
const { Route } = await importNavRoute({ profileId: 'oss', desktop: false })
|
|
83
83
|
expect(
|
|
84
84
|
catchRedirect(() => Route.options.beforeLoad!({ context: { session: null }, location } as never)),
|
|
85
85
|
).toBeNull()
|
|
86
|
-
})
|
|
86
|
+
}, 20000)
|
|
87
87
|
|
|
88
88
|
it('desktop:无条件放行', async () => {
|
|
89
89
|
const { Route } = await importNavRoute({ profileId: 'saas', desktop: true })
|
|
90
90
|
expect(
|
|
91
91
|
catchRedirect(() => Route.options.beforeLoad!({ context: { session: null }, location } as never)),
|
|
92
92
|
).toBeNull()
|
|
93
|
-
})
|
|
93
|
+
}, 20000)
|
|
94
94
|
})
|
|
95
95
|
|
|
96
96
|
describe('GlobalErrorBoundary', () => {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { readFileSync } from "node:fs"
|
|
3
|
+
import { resolve } from "node:path"
|
|
4
|
+
import { describe, expect, it } from "vitest"
|
|
5
|
+
|
|
6
|
+
// routeTree.gen.ts 由 TanStack Router 插件生成,是路由结构的单一事实源。
|
|
7
|
+
// 本测试锁定关键路由路径集合:误删路由文件 / 改名 path 未同步调用方时立即红灯。
|
|
8
|
+
// 清单项均为已用 grep 校准的真实生成字面量(单引号 + 完整路径串)。
|
|
9
|
+
const content = readFileSync(
|
|
10
|
+
resolve(import.meta.dirname, "../../routeTree.gen.ts"),
|
|
11
|
+
"utf-8"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const MUST_EXIST_PATHS: string[] = [
|
|
15
|
+
// 认证区(_login pathless layout)
|
|
16
|
+
"'/login'",
|
|
17
|
+
"'/register'",
|
|
18
|
+
"'/forgot-password'",
|
|
19
|
+
"'/reset-password'",
|
|
20
|
+
// 导航区(_nav pathless layout,登录后落地页)
|
|
21
|
+
"'/dashboard'",
|
|
22
|
+
"'/workspaces'",
|
|
23
|
+
"'/prd'",
|
|
24
|
+
"'/settings'",
|
|
25
|
+
"'/knowledge'",
|
|
26
|
+
"'/apps'",
|
|
27
|
+
// 公开 / 分享(_public layout + share 公开页)
|
|
28
|
+
"'/share/$versionId'",
|
|
29
|
+
"'/share/prd/$prdId'",
|
|
30
|
+
// API
|
|
31
|
+
"'/api/auth/$'",
|
|
32
|
+
"'/api/health'",
|
|
33
|
+
"'/api/chat'",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const DYNAMIC_PATH_PATTERNS: RegExp[] = [
|
|
37
|
+
/prd\/\$id/,
|
|
38
|
+
/workspaces\/\$workspaceId/,
|
|
39
|
+
/settings\/ai\/\$providerId/,
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
describe("routeTree 结构完整性", () => {
|
|
43
|
+
it.each(MUST_EXIST_PATHS)("关键路由 %s 存在", (p) => {
|
|
44
|
+
expect(content).toContain(p)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
for (const re of DYNAMIC_PATH_PATTERNS) {
|
|
48
|
+
it(`动态路由 ${re} 存在`, () => {
|
|
49
|
+
expect(re.test(content)).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
it("pathless 布局路由 _nav/_login/_public 存在", () => {
|
|
54
|
+
expect(content).toContain("_nav")
|
|
55
|
+
expect(content).toContain("_login")
|
|
56
|
+
expect(content).toContain("_public")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// 反向守卫(面向未来):routeTree.gen.ts 由路由插件扫描 src/routes/** 生成,
|
|
60
|
+
// 若测试文件被误放进 routes 目录会被一并收录为路由。当前恒真,防未来回归。
|
|
61
|
+
it("测试文件本身未被路由插件收录(防误扫为路由)", () => {
|
|
62
|
+
expect(content).not.toContain("route-tree-structure")
|
|
63
|
+
expect(content).not.toContain("__tests__")
|
|
64
|
+
})
|
|
65
|
+
})
|