@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
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { cn } from "@/lib/utils"
|
|
19
19
|
import { createLazyFileRoute, useNavigate } from '@tanstack/react-router'
|
|
20
20
|
import { useState } from 'react'
|
|
21
|
-
import { useQuery, keepPreviousData } from '@tanstack/react-query'
|
|
21
|
+
import { useQuery, keepPreviousData, useQueryClient } from '@tanstack/react-query'
|
|
22
22
|
import { listComplianceFn } from '@/server/serverFns/knowledge-public/compliance/list'
|
|
23
23
|
import { createComplianceFn } from '@/server/serverFns/knowledge-public/compliance/create'
|
|
24
24
|
import { getEntityTagsFn } from '@/server/serverFns/knowledge-public/entity-tags'
|
|
@@ -50,6 +50,7 @@ export const Route = createLazyFileRoute('/_nav/knowledge/compliance/')({ compon
|
|
|
50
50
|
|
|
51
51
|
function CompliancePage() {
|
|
52
52
|
const navigate = useNavigate()
|
|
53
|
+
const qc = useQueryClient()
|
|
53
54
|
const industryOptions = buildIndustryOptions()
|
|
54
55
|
const industryGroups = [
|
|
55
56
|
{ key: 'industry', label: m.knowledge_compliance_index_industry(), options: industryOptions },
|
|
@@ -94,7 +95,9 @@ function CompliancePage() {
|
|
|
94
95
|
search={search}
|
|
95
96
|
onSearchChange={setSearch}
|
|
96
97
|
onCreate={async () => {
|
|
97
|
-
const r = await createComplianceFn({ data: { title:
|
|
98
|
+
const r = await createComplianceFn({ data: { title: '新合规', industry: '', tags: [] } })
|
|
99
|
+
qc.invalidateQueries({ queryKey: ['compliance'] })
|
|
100
|
+
await qc.refetchQueries({ queryKey: ['compliance'], type: 'all' })
|
|
98
101
|
navigate({ to: '/knowledge/compliance/edit/$id', params: { id: r.id } })
|
|
99
102
|
}}
|
|
100
103
|
createLabel={m.knowledge_compliance_index_add()}
|
|
@@ -146,7 +146,7 @@ function KnowledgeListPage() {
|
|
|
146
146
|
<Card key={kb.id} className={`group relative p-5 transition-colors hover:bg-accent/40 border rounded-lg ${isLaw ? 'overflow-hidden' : ''}`}>
|
|
147
147
|
<Link
|
|
148
148
|
to={kb.link}
|
|
149
|
-
className="absolute inset-0"
|
|
149
|
+
className="absolute inset-0 z-10"
|
|
150
150
|
aria-label={name}
|
|
151
151
|
/>
|
|
152
152
|
{isLaw ? (
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { cn } from "@/lib/utils"
|
|
19
19
|
import { createLazyFileRoute, useNavigate } from '@tanstack/react-router'
|
|
20
20
|
import { useState } from 'react'
|
|
21
|
-
import { useQuery, keepPreviousData } from '@tanstack/react-query'
|
|
21
|
+
import { useQuery, keepPreviousData, useQueryClient } from '@tanstack/react-query'
|
|
22
22
|
import { listLegalFn } from '@/server/serverFns/knowledge-public/legal/list'
|
|
23
23
|
import { createLegalFn } from '@/server/serverFns/knowledge-public/legal/create'
|
|
24
24
|
import { getEntityTagsFn } from '@/server/serverFns/knowledge-public/entity-tags'
|
|
@@ -44,6 +44,7 @@ export const Route = createLazyFileRoute('/_nav/knowledge/laws/')({ component: L
|
|
|
44
44
|
|
|
45
45
|
function LawPage() {
|
|
46
46
|
const navigate = useNavigate()
|
|
47
|
+
const qc = useQueryClient()
|
|
47
48
|
const jurisdictionOptions = buildJurisdictionOptions()
|
|
48
49
|
const jurisdictionGroups = [
|
|
49
50
|
{ key: 'jurisdiction', label: m.knowledge_laws_index_jurisdiction(), options: jurisdictionOptions },
|
|
@@ -79,7 +80,9 @@ function LawPage() {
|
|
|
79
80
|
search={search}
|
|
80
81
|
onSearchChange={setSearch}
|
|
81
82
|
onCreate={async () => {
|
|
82
|
-
const r = await createLegalFn({ data: { title:
|
|
83
|
+
const r = await createLegalFn({ data: { title: '新法规', jurisdiction: '', category: '', tags: [] } })
|
|
84
|
+
qc.invalidateQueries({ queryKey: ['legal'] })
|
|
85
|
+
await qc.refetchQueries({ queryKey: ['legal'], type: 'all' })
|
|
83
86
|
navigate({ to: '/knowledge/laws/edit/$id', params: { id: r.id } })
|
|
84
87
|
}}
|
|
85
88
|
createLabel={m.knowledge_laws_index_add()}
|
|
@@ -21,6 +21,10 @@ import { useMemo } from 'react'
|
|
|
21
21
|
|
|
22
22
|
import { m } from '@/paraglide/messages.js'
|
|
23
23
|
import { isDesktopServer } from '@/server/desktop-guest'
|
|
24
|
+
import { listPinnedWorkspacesBriefFn } from '@/server/serverFns/workspace'
|
|
25
|
+
import { listKnowledgeFn } from '@/server/serverFns/knowledge'
|
|
26
|
+
import { WORKSPACES_QUERY_KEY } from '@/lib/collections/reference/workspaces-collection'
|
|
27
|
+
import { KNOWLEDGE_BASES_QUERY_KEY } from '@/lib/collections/knowledge/knowledge-bases-collection'
|
|
24
28
|
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
|
|
25
29
|
import { Button } from '@/components/ui/button'
|
|
26
30
|
import { GlobalErrorBoundary } from '@/components/error/global-error-boundary'
|
|
@@ -157,6 +161,23 @@ export const Route = createFileRoute('/_nav')({
|
|
|
157
161
|
})
|
|
158
162
|
}
|
|
159
163
|
},
|
|
164
|
+
loader: async ({ context }) => {
|
|
165
|
+
const { queryClient } = context
|
|
166
|
+
// SSR 预取侧边栏常驻菜单(工作空间/知识库)数据,刷新后 hydrate 首帧即有数据,
|
|
167
|
+
// 避免侧边栏菜单每次刷新重新加载
|
|
168
|
+
await Promise.all([
|
|
169
|
+
queryClient.ensureQueryData({
|
|
170
|
+
queryKey: WORKSPACES_QUERY_KEY,
|
|
171
|
+
queryFn: () => listPinnedWorkspacesBriefFn(),
|
|
172
|
+
}),
|
|
173
|
+
queryClient.ensureQueryData({
|
|
174
|
+
queryKey: KNOWLEDGE_BASES_QUERY_KEY,
|
|
175
|
+
queryFn: () => listKnowledgeFn(),
|
|
176
|
+
}),
|
|
177
|
+
]).catch(() => {
|
|
178
|
+
// 预取失败降级为客户端 fetch(与旧行为一致),不阻塞页面
|
|
179
|
+
})
|
|
180
|
+
},
|
|
160
181
|
notFoundComponent: NavNotFound,
|
|
161
182
|
errorComponent: GlobalErrorBoundary,
|
|
162
183
|
})
|
|
@@ -20,21 +20,22 @@ import { useEffect, useState } from "react";
|
|
|
20
20
|
import { useTheme } from "next-themes";
|
|
21
21
|
import { GlyphMatrix } from "@/components/magicui/glyph-matrix";
|
|
22
22
|
import { CreateProjectDialog } from "@/components/project/create-project-dialog";
|
|
23
|
-
import { EditWorkspaceDialog } from "@/components/workspace/edit-workspace-dialog";
|
|
23
|
+
import { EditWorkspaceDialog } from "@/components/workspace/dialogs/edit-workspace-dialog";
|
|
24
24
|
import { NameConfirmDialog } from "@/components/shared-kit/dialogs/name-confirm-dialog";
|
|
25
|
-
import { DeleteWorkspaceOptions } from "@/components/workspace/delete-workspace-options";
|
|
25
|
+
import { DeleteWorkspaceOptions } from "@/components/workspace/dialogs/delete-workspace-options";
|
|
26
26
|
import {
|
|
27
27
|
deleteWorkspaceFn,
|
|
28
28
|
listWorkspaceMembersFn,
|
|
29
29
|
listWorkspacesFn,
|
|
30
30
|
} from "@/server/serverFns/workspace";
|
|
31
31
|
import { deleteProjectFn } from "@/server/serverFns/project";
|
|
32
|
-
import { WorkspaceHeader } from "@/components/workspace/workspace-header";
|
|
33
|
-
import { WorkspaceKbCard } from "@/components/workspace/workspace-kb-card";
|
|
34
|
-
import { WorkspaceContributionCard } from "@/components/workspace/workspace-contribution-card";
|
|
35
|
-
import { WorkspaceProjectList } from "@/components/workspace/workspace-project-list";
|
|
36
|
-
import { WorkspaceActivityCard } from "@/components/workspace/workspace-activity-card";
|
|
37
|
-
import { WorkspaceMembersManager } from "@/components/workspace/workspace-members-manager";
|
|
32
|
+
import { WorkspaceHeader } from "@/components/workspace/header/workspace-header";
|
|
33
|
+
import { WorkspaceKbCard } from "@/components/workspace/kb/workspace-kb-card";
|
|
34
|
+
import { WorkspaceContributionCard } from "@/components/workspace/contribution/workspace-contribution-card";
|
|
35
|
+
import { WorkspaceProjectList } from "@/components/workspace/projects/workspace-project-list";
|
|
36
|
+
import { WorkspaceActivityCard } from "@/components/workspace/activity/workspace-activity-card";
|
|
37
|
+
import { WorkspaceMembersManager } from "@/components/workspace/members/workspace-members-manager";
|
|
38
|
+
import type { WorkspaceKbCardData } from "@/server/serverFns/workspace-kb-utils";
|
|
38
39
|
import { m } from "@/paraglide/messages.js";
|
|
39
40
|
|
|
40
41
|
type DetailData = {
|
|
@@ -75,10 +76,11 @@ type DetailData = {
|
|
|
75
76
|
joinedAt: Date;
|
|
76
77
|
user?: { id: string; username: string; email: string; avatarConfig?: unknown; bgShape?: string | null; bgColor?: string | null };
|
|
77
78
|
}[];
|
|
79
|
+
kbCard: WorkspaceKbCardData;
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
function WorkspaceDetailPage() {
|
|
81
|
-
const { workspace, projects, activities, contributionData, members } = Route.useLoaderData() as DetailData;
|
|
83
|
+
const { workspace, projects, activities, contributionData, members, kbCard } = Route.useLoaderData() as DetailData;
|
|
82
84
|
const [editOpen, setEditOpen] = useState(false);
|
|
83
85
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
84
86
|
const [projectEditOpen, setProjectEditOpen] = useState(false);
|
|
@@ -150,7 +152,7 @@ function WorkspaceDetailPage() {
|
|
|
150
152
|
onEdit={() => setEditOpen(true)}
|
|
151
153
|
onDelete={() => setDeleteOpen(true)}
|
|
152
154
|
/>
|
|
153
|
-
<WorkspaceKbCard />
|
|
155
|
+
<WorkspaceKbCard workspaceId={workspace.id} data={kbCard} />
|
|
154
156
|
<div className="grid gap-6 lg:grid-cols-3">
|
|
155
157
|
<div className="lg:col-span-2 space-y-6">
|
|
156
158
|
<WorkspaceContributionCard data={contributionData} />
|
|
@@ -166,7 +168,7 @@ function WorkspaceDetailPage() {
|
|
|
166
168
|
workspaceId={workspace.id}
|
|
167
169
|
workspaceOwnerId={workspace.ownerId}
|
|
168
170
|
members={members}
|
|
169
|
-
onMembersUpdated={() =>
|
|
171
|
+
onMembersUpdated={() => { void router.invalidate(); }}
|
|
170
172
|
/>
|
|
171
173
|
</div>
|
|
172
174
|
</div>
|
|
@@ -23,17 +23,19 @@ import {
|
|
|
23
23
|
getWorkspaceActivityDataFn,
|
|
24
24
|
listWorkspaceMembersFn,
|
|
25
25
|
} from "@/server/serverFns/workspace";
|
|
26
|
+
import { getWorkspaceKbCardFn } from "@/server/serverFns/workspace-kb";
|
|
26
27
|
|
|
27
28
|
export const Route = createFileRoute("/_nav/workspaces/$workspaceId")({
|
|
28
29
|
loader: async ({ params }) => {
|
|
29
|
-
const [workspace, projects, activities, contributionData, members] = await Promise.all([
|
|
30
|
+
const [workspace, projects, activities, contributionData, members, kbCard] = await Promise.all([
|
|
30
31
|
getWorkspaceDetailFn({ data: { workspaceId: params.workspaceId } }),
|
|
31
32
|
listWorkspaceProjectsFn({ data: { workspaceId: params.workspaceId } }),
|
|
32
33
|
listWorkspaceActivitiesFn({ data: { workspaceId: params.workspaceId, take: 5 } }),
|
|
33
34
|
getWorkspaceActivityDataFn({ data: { workspaceId: params.workspaceId } }),
|
|
34
35
|
listWorkspaceMembersFn({ data: { workspaceId: params.workspaceId } }),
|
|
36
|
+
getWorkspaceKbCardFn({ data: { workspaceId: params.workspaceId } }),
|
|
35
37
|
]);
|
|
36
|
-
return { workspace, projects, activities, contributionData, members };
|
|
38
|
+
return { workspace, projects, activities, contributionData, members, kbCard };
|
|
37
39
|
},
|
|
38
40
|
pendingComponent: () => null,
|
|
39
41
|
});
|
|
@@ -28,7 +28,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
28
28
|
import { LucideIconPicker } from "@/components/shared-kit/icons/lucide-icon-picker";
|
|
29
29
|
|
|
30
30
|
import { NameConfirmDialog } from "@/components/shared-kit/dialogs/name-confirm-dialog";
|
|
31
|
-
import { DeleteWorkspaceOptions } from "@/components/workspace/delete-workspace-options";
|
|
31
|
+
import { DeleteWorkspaceOptions } from "@/components/workspace/dialogs/delete-workspace-options";
|
|
32
32
|
import {
|
|
33
33
|
updateWorkspaceFn,
|
|
34
34
|
deleteWorkspaceFn,
|
|
@@ -30,9 +30,9 @@ import {
|
|
|
30
30
|
import { Separator } from "@/components/ui/separator";
|
|
31
31
|
import { GlyphMatrix } from "@/components/magicui/glyph-matrix";
|
|
32
32
|
import { LucideIconByName } from "@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name";
|
|
33
|
-
import { CreateWorkspaceDialog } from "@/components/workspace/create-workspace-dialog";
|
|
34
|
-
import { DeleteWorkspaceOptions } from "@/components/workspace/delete-workspace-options";
|
|
35
|
-
import { EditWorkspaceDialog } from "@/components/workspace/edit-workspace-dialog";
|
|
33
|
+
import { CreateWorkspaceDialog } from "@/components/workspace/dialogs/create-workspace-dialog";
|
|
34
|
+
import { DeleteWorkspaceOptions } from "@/components/workspace/dialogs/delete-workspace-options";
|
|
35
|
+
import { EditWorkspaceDialog } from "@/components/workspace/dialogs/edit-workspace-dialog";
|
|
36
36
|
import { NameConfirmDialog } from "@/components/shared-kit/dialogs/name-confirm-dialog";
|
|
37
37
|
import { toggleWorkspacePinFn, deleteWorkspaceFn } from "@/server/serverFns/workspace";
|
|
38
38
|
import { toast } from "@/components/ui/sonner";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { createFromSource } from 'fumadocs-core/search/server'
|
|
3
|
+
import { source } from '@/lib/source'
|
|
4
|
+
|
|
5
|
+
const server = createFromSource(source)
|
|
6
|
+
|
|
7
|
+
export const Route = createFileRoute('/api/search')({
|
|
8
|
+
server: {
|
|
9
|
+
handlers: {
|
|
10
|
+
GET: async ({ request }) => server.GET(request),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
})
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
2
|
+
import { getLocale } from '@/paraglide/runtime.js'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/docs')({
|
|
5
|
+
beforeLoad: () => {
|
|
6
|
+
const lang = getLocale()
|
|
7
|
+
throw redirect({ to: '/$lang/docs/$', params: { lang, _splat: '' } })
|
|
8
|
+
},
|
|
9
|
+
})
|
|
@@ -178,14 +178,14 @@ describe('agent-crud serverFn', () => {
|
|
|
178
178
|
|
|
179
179
|
it('FeatureUnavailableError / FeatureForbiddenError → 抛错不建', async () => {
|
|
180
180
|
mocks.requireFeature.mockRejectedValue(new Error('FeatureUnavailableError: xxx'))
|
|
181
|
-
await expect(createAgentFn({ data: { name: 'X', systemPrompt: 'Y' } })).rejects.
|
|
181
|
+
await expect(createAgentFn({ data: { name: 'X', systemPrompt: 'Y' } })).rejects.toThrow(/FeatureUnavailable|FeatureForbidden/)
|
|
182
182
|
expect(mocks.create).not.toHaveBeenCalled()
|
|
183
183
|
})
|
|
184
184
|
|
|
185
185
|
it('ResourceLimitError → 抛错不建', async () => {
|
|
186
186
|
mocks.requireFeature.mockResolvedValue(undefined as any)
|
|
187
187
|
mocks.requireResource.mockRejectedValue(new Error('ResourceLimitError: agentQuota 已达上限'))
|
|
188
|
-
await expect(createAgentFn({ data: { name: 'X', systemPrompt: 'Y' } })).rejects.
|
|
188
|
+
await expect(createAgentFn({ data: { name: 'X', systemPrompt: 'Y' } })).rejects.toThrow(/ResourceLimit|已达上限/)
|
|
189
189
|
expect(mocks.create).not.toHaveBeenCalled()
|
|
190
190
|
})
|
|
191
191
|
|
|
@@ -236,7 +236,7 @@ describe('agent-crud serverFn', () => {
|
|
|
236
236
|
|
|
237
237
|
it('userId 不匹配 → 抛错且不调用 update', async () => {
|
|
238
238
|
mocks.findFirst.mockResolvedValue(null)
|
|
239
|
-
await expect(updateAgentFn({ data: { id: 'ag-other', name: 'X' } })).rejects.
|
|
239
|
+
await expect(updateAgentFn({ data: { id: 'ag-other', name: 'X' } })).rejects.toThrow(/无权限|FORBIDDEN|not found/)
|
|
240
240
|
expect(mocks.update).not.toHaveBeenCalled()
|
|
241
241
|
})
|
|
242
242
|
|
|
@@ -272,7 +272,7 @@ describe('agent-crud serverFn', () => {
|
|
|
272
272
|
|
|
273
273
|
it('userId 不匹配 → 抛错且不调用 update', async () => {
|
|
274
274
|
mocks.findFirst.mockResolvedValue(null)
|
|
275
|
-
await expect(deleteAgentFn({ data: { id: 'ag-other' } })).rejects.
|
|
275
|
+
await expect(deleteAgentFn({ data: { id: 'ag-other' } })).rejects.toThrow(/无权限|FORBIDDEN|not found/)
|
|
276
276
|
expect(mocks.update).not.toHaveBeenCalled()
|
|
277
277
|
})
|
|
278
278
|
})
|
|
@@ -145,9 +145,7 @@ describe("createConversationFn", () => {
|
|
|
145
145
|
|
|
146
146
|
await expect(
|
|
147
147
|
createConversationFn({ data: { agentId: "agent-xxx" } }),
|
|
148
|
-
).rejects.
|
|
149
|
-
message: expect.stringContaining("not found"),
|
|
150
|
-
})
|
|
148
|
+
).rejects.toThrow("not found")
|
|
151
149
|
expect(mocks.create).not.toHaveBeenCalled()
|
|
152
150
|
})
|
|
153
151
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { assertFetchableUrl, FetchGuardError } from "../fetch-guard";
|
|
3
|
+
|
|
4
|
+
describe("assertFetchableUrl SSRF 主机防护", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
"http://127.0.0.1:8080/api",
|
|
7
|
+
"http://localhost:3000/x",
|
|
8
|
+
"http://10.0.0.5/internal",
|
|
9
|
+
"http://172.16.1.1/admin",
|
|
10
|
+
"http://192.168.1.1/router",
|
|
11
|
+
"http://169.254.169.254/latest/meta-data", // 云元数据
|
|
12
|
+
"http://0.0.0.0/",
|
|
13
|
+
"http://[::1]:8080/",
|
|
14
|
+
"http://[fe80::1]/", // 链路本地
|
|
15
|
+
"http://[::ffff:127.0.0.1]/", // IPv4-mapped IPv6 回环(SSRF 经典绕过)
|
|
16
|
+
"http://[::ffff:a9fe:a9fe]/", // IPv4-mapped 云元数据
|
|
17
|
+
"http://[::ffff:10.0.0.5]/", // mapped 归一形态 ::ffff:a00:5(URL 压缩组前导零)
|
|
18
|
+
"http://[::ffff:0:0:127.0.0.1]/", // IPv4-translated 变体(::ffff:0:0/96 前缀)
|
|
19
|
+
"http://[::127.0.0.1]/", // IPv4-compatible ::/96 回环
|
|
20
|
+
"http://[::a9fe:a9fe]/", // IPv4-compatible 云元数据
|
|
21
|
+
"http://[::10.0.0.5]/", // IPv4-compatible A 类私网
|
|
22
|
+
"http://[fc00::1]/", // IPv6 ULA 私网段
|
|
23
|
+
"http://[fd12:3456::1]/",
|
|
24
|
+
])("拒绝私网/回环/元数据地址:%s", (url) => {
|
|
25
|
+
expect(() => assertFetchableUrl(url)).toThrow(FetchGuardError);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it.each([
|
|
29
|
+
"https://api.openai.com/v1/models",
|
|
30
|
+
"http://example.com/page",
|
|
31
|
+
])("放行公网地址:%s", (url) => {
|
|
32
|
+
expect(() => assertFetchableUrl(url)).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each([
|
|
36
|
+
["http://[2001:4860:4860::8888]/", "普通公网 IPv6"],
|
|
37
|
+
["http://[::ffff:8.8.8.8]/", "IPv4-mapped 公网(防映射还原分支误伤)"],
|
|
38
|
+
["http://[::ffff:0:0:8.8.8.8]/", "IPv4-mapped 翻译形态公网(防误伤对称覆盖)"],
|
|
39
|
+
["http://[::8.8.8.8]/", "IPv4-compatible 公网(防内嵌分支误伤)"],
|
|
40
|
+
["http://[2606:4700:4700::1111]/", "Cloudflare 公网 IPv6"],
|
|
41
|
+
])("公网 IPv6 地址放行:%s(%s)", (url) => {
|
|
42
|
+
expect(() => assertFetchableUrl(url)).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it.each([
|
|
46
|
+
"file:///etc/passwd",
|
|
47
|
+
"ftp://example.com/f",
|
|
48
|
+
"gopher://example.com/",
|
|
49
|
+
])("仅允许 http/https 协议:%s", (url) => {
|
|
50
|
+
expect(() => assertFetchableUrl(url)).toThrow(FetchGuardError);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("allowPrivate 显式放行内网(SearXNG 自托管场景)", () => {
|
|
54
|
+
expect(() =>
|
|
55
|
+
assertFetchableUrl("http://127.0.0.1:8888/search", { allowPrivate: true }),
|
|
56
|
+
).not.toThrow();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("allowHosts 白名单精确匹配放行", () => {
|
|
60
|
+
expect(() =>
|
|
61
|
+
assertFetchableUrl("https://searx.internal.corp/search", {
|
|
62
|
+
allowedHosts: ["searx.internal.corp"],
|
|
63
|
+
}),
|
|
64
|
+
).not.toThrow();
|
|
65
|
+
expect(() =>
|
|
66
|
+
assertFetchableUrl("https://evil.corp/search", {
|
|
67
|
+
allowedHosts: ["searx.internal.corp"],
|
|
68
|
+
}),
|
|
69
|
+
).toThrow(FetchGuardError);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it.each([
|
|
73
|
+
["http://2130706433/", "十进制整数 127.0.0.1"],
|
|
74
|
+
["http://0x7f000001/", "十六进制 127.0.0.1"],
|
|
75
|
+
["http://017700000001/", "八进制整数 127.0.0.1"],
|
|
76
|
+
["http://0177.0.0.1/", "点分八进制 127.0.0.1"],
|
|
77
|
+
["http://99999999999999/", "超出 IPv4 范围(URL 构造期抛 Invalid URL)"],
|
|
78
|
+
])("拒绝整数形式 IP 混淆:%s(%s)", (url) => {
|
|
79
|
+
expect(() => assertFetchableUrl(url)).toThrow(FetchGuardError);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("返回规范化 URL 供 fetch 使用", () => {
|
|
83
|
+
expect(assertFetchableUrl("https://example.com/a?q=1").href).toBe(
|
|
84
|
+
"https://example.com/a?q=1",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("大写协议与省略段 IPv4 规范化后仍被拒绝", () => {
|
|
89
|
+
expect(() => assertFetchableUrl("HTTP://127.0.0.1/x")).toThrow(FetchGuardError);
|
|
90
|
+
expect(() => assertFetchableUrl("http://127.1/")).toThrow(FetchGuardError);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("协议被拒绝时错误类型仍是 FetchGuardError", () => {
|
|
94
|
+
expect(() => assertFetchableUrl("file:///etc/passwd")).toThrow(FetchGuardError);
|
|
95
|
+
expect(() => assertFetchableUrl("file:///etc/passwd")).toThrow(
|
|
96
|
+
/不在允许范围/,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -62,8 +62,6 @@ describe('requirePermission', () => {
|
|
|
62
62
|
it('有角色但 checkPermission 返回 none → 拒绝', async () => {
|
|
63
63
|
vi.mocked(prisma.roleAssignment.count).mockResolvedValue(1)
|
|
64
64
|
vi.mocked(getProService('permissions')!.checkPermission).mockResolvedValue('none')
|
|
65
|
-
await expect(requirePermission('u1', 'knowledge', 'write')).rejects.
|
|
66
|
-
name: 'PermissionForbiddenError',
|
|
67
|
-
})
|
|
65
|
+
await expect(requirePermission('u1', 'knowledge', 'write')).rejects.toBeInstanceOf(PermissionForbiddenError)
|
|
68
66
|
})
|
|
69
67
|
})
|
|
@@ -40,6 +40,7 @@ describe('requireTeamSeats', () => {
|
|
|
40
40
|
vi.mocked(prisma.plan.findFirst).mockResolvedValue(freePlan as never)
|
|
41
41
|
vi.mocked(prisma.teamMembership.count).mockResolvedValue(5)
|
|
42
42
|
vi.mocked(prisma.teamInvitation.count).mockResolvedValue(0)
|
|
43
|
+
await expect(requireTeamSeats('owner1', 'member', 1)).rejects.toBeInstanceOf(ResourceLimitError)
|
|
43
44
|
await expect(requireTeamSeats('owner1', 'member', 1)).rejects.toMatchObject({
|
|
44
45
|
resource: 'seats',
|
|
45
46
|
})
|
|
@@ -53,6 +54,7 @@ describe('requireTeamSeats', () => {
|
|
|
53
54
|
} as never)
|
|
54
55
|
vi.mocked(prisma.teamMembership.count).mockResolvedValue(16)
|
|
55
56
|
vi.mocked(prisma.teamInvitation.count).mockResolvedValue(0)
|
|
57
|
+
await expect(requireTeamSeats('owner1', 'guest', 1)).rejects.toBeInstanceOf(ResourceLimitError)
|
|
56
58
|
await expect(requireTeamSeats('owner1', 'guest', 1)).rejects.toMatchObject({
|
|
57
59
|
resource: 'guestSeats',
|
|
58
60
|
})
|
|
@@ -181,7 +181,7 @@ describe('skill-crud serverFn', () => {
|
|
|
181
181
|
})
|
|
182
182
|
await expect(
|
|
183
183
|
createSkillFn({ data: { name: 'daily-sum', title: 'X', template: 'Y' } }),
|
|
184
|
-
).rejects.
|
|
184
|
+
).rejects.toThrow('已存在')
|
|
185
185
|
})
|
|
186
186
|
|
|
187
187
|
it('FeatureForbiddenError → 抛错不建', async () => {
|
|
@@ -232,7 +232,7 @@ describe('skill-crud serverFn', () => {
|
|
|
232
232
|
|
|
233
233
|
it('userId 不匹配 → 抛错且不调用 update', async () => {
|
|
234
234
|
mocks.findFirst.mockResolvedValue(null)
|
|
235
|
-
await expect(updateSkillFn({ data: { id: 'sk-other', title: 'X' } })).rejects.
|
|
235
|
+
await expect(updateSkillFn({ data: { id: 'sk-other', title: 'X' } })).rejects.toThrow(/无权限|FORBIDDEN|not found/)
|
|
236
236
|
expect(mocks.update).not.toHaveBeenCalled()
|
|
237
237
|
})
|
|
238
238
|
})
|
|
@@ -256,7 +256,7 @@ describe('skill-crud serverFn', () => {
|
|
|
256
256
|
|
|
257
257
|
it('userId 不匹配 → 抛错且不调用 update', async () => {
|
|
258
258
|
mocks.findFirst.mockResolvedValue(null)
|
|
259
|
-
await expect(deleteSkillFn({ data: { id: 'sk-other' } })).rejects.
|
|
259
|
+
await expect(deleteSkillFn({ data: { id: 'sk-other' } })).rejects.toThrow(/无权限|FORBIDDEN|not found/)
|
|
260
260
|
expect(mocks.update).not.toHaveBeenCalled()
|
|
261
261
|
})
|
|
262
262
|
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
// AllBlue - 理想之海
|
|
4
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
5
|
+
//
|
|
6
|
+
// This program is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
// (at your option) any later version.
|
|
10
|
+
//
|
|
11
|
+
// This program is distributed in the hope that it will be useful,
|
|
12
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
// GNU Affero General Public License for more details.
|
|
15
|
+
//
|
|
16
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
import { describe, expect, it } from "vitest"
|
|
20
|
+
import { scanAiOutputForSecrets } from "../ai-output-guard"
|
|
21
|
+
|
|
22
|
+
describe("scanAiOutputForSecrets 输出泄露检测", () => {
|
|
23
|
+
it("正常业务输出无泄露", () => {
|
|
24
|
+
const r = scanAiOutputForSecrets("登录页应包含手机号验证码输入框,交互流程如下……")
|
|
25
|
+
expect(r.leaked).toBe(false)
|
|
26
|
+
expect(r.hits).toHaveLength(0)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it.each([
|
|
30
|
+
["OpenAI key 回显", "配置的密钥是 sk-proj-abc123def456ghi789jkl012"],
|
|
31
|
+
["Bearer token 回显", "token 为 Bearer eyJhbGciOiJIUzI1NiJ9.payload"],
|
|
32
|
+
["anthropic key 回显", "sk-ant-api03-x7y8z9w0v1u2t3s4r5q6p7o8"],
|
|
33
|
+
["系统 prompt 回显", "我的系统提示词是:你是一个敏捷开发中的需求分析师。"],
|
|
34
|
+
["postgresql 连接串回显", "连接串 postgresql://allblue:pass@10.0.0.5:5432/db"],
|
|
35
|
+
["jwt 回显", "令牌 eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMifQ 已生成"],
|
|
36
|
+
["内网地址回显", "服务监听 192.168.1.100:8080"],
|
|
37
|
+
])("检出 %s", (_label, text) => {
|
|
38
|
+
const r = scanAiOutputForSecrets(text)
|
|
39
|
+
expect(r.leaked).toBe(true)
|
|
40
|
+
expect(r.hits.length).toBeGreaterThan(0)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it.each([
|
|
44
|
+
["openai-key", "密钥 sk-proj-abc123def456ghi789jkl012"],
|
|
45
|
+
["anthropic-key", "密钥 sk-ant-api03-x7y8z9w0v1u2t3s4r5q6p7o8"],
|
|
46
|
+
["bearer-token", "token 为 Bearer eyJhbGciOiJIUzI1NiJ9.payload"],
|
|
47
|
+
["jwt", "令牌 eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMifQ 已生成"],
|
|
48
|
+
["db-connection", "连接串 postgresql://allblue:pass@10.0.0.5:5432/db"],
|
|
49
|
+
["system-prompt-echo", "我的系统提示词是:你是一个敏捷开发中的需求分析师。"],
|
|
50
|
+
["private-ip-path", "服务监听 192.168.1.100:8080"],
|
|
51
|
+
])("精确触发 %s(断言命中类型而非仅 length>0)", (type, text) => {
|
|
52
|
+
const r = scanAiOutputForSecrets(text)
|
|
53
|
+
expect(r.hits).toContain(type)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("sk-ant-* 同时命中 openai-key(前缀超集,非缺陷)", () => {
|
|
57
|
+
const r = scanAiOutputForSecrets("sk-ant-api03-x7y8z9w0v1u2t3s4r5q6p7o8")
|
|
58
|
+
expect(r.hits).toContain("openai-key")
|
|
59
|
+
expect(r.hits).toContain("anthropic-key")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("命中清单不含原文泄露(防扫描器自身成为泄露面)", () => {
|
|
63
|
+
const secret = "sk-proj-abc123def456ghi789jkl012"
|
|
64
|
+
const r = scanAiOutputForSecrets(`密钥 ${secret} 已泄露`)
|
|
65
|
+
expect(JSON.stringify(r)).not.toContain(secret)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("同一文本重复调用两次结果一致(/g 正则 lastIndex 复位)", () => {
|
|
69
|
+
const text = "配置 sk-proj-abc123def456ghi789jkl012 并部署到 192.168.1.100"
|
|
70
|
+
const a = scanAiOutputForSecrets(text)
|
|
71
|
+
const b = scanAiOutputForSecrets(text)
|
|
72
|
+
expect(b).toEqual(a)
|
|
73
|
+
expect(b.leaked).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("短文本阈值:空串与 7 字符均短路为安全", () => {
|
|
77
|
+
expect(scanAiOutputForSecrets("").leaked).toBe(false)
|
|
78
|
+
expect(scanAiOutputForSecrets("ok").leaked).toBe(false)
|
|
79
|
+
expect(scanAiOutputForSecrets("1234567").leaked).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("阈值边界:8 字符正常文本仍安全", () => {
|
|
83
|
+
expect(scanAiOutputForSecrets("一二三四五六七八").leaked).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("8 字符真实 secret 恰好不被短文本短路漏检", () => {
|
|
87
|
+
// private-ip-path 的最短可命中形态,与短文本阈值 8 的边界重合——阈值上调即漏检
|
|
88
|
+
expect(scanAiOutputForSecrets("10.0.0.1").leaked).toBe(true)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("正常业务文本(含内网地址的部署说明)命中 private-ip-path(设计权衡:宁误报)", () => {
|
|
92
|
+
const r = scanAiOutputForSecrets("部署在 192.168.1.100 的内网服务说明")
|
|
93
|
+
expect(r.hits).toContain("private-ip-path")
|
|
94
|
+
})
|
|
95
|
+
})
|