@allbluecn/web-app 0.10.3 → 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 +21 -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-workspace.tsx +20 -22
- 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/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
- package/src/lib/sanitize.ts +14 -1
- package/src/lib/session.ts +9 -1
- 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/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/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 +114 -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
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
// 知识库文档搜索共享工具(story 关联弹窗 / 工作空间快捷文档弹窗共用)。
|
|
19
|
+
// 服务端工具模块(顶层 import prisma),仅供 serverFn 文件 import。
|
|
20
|
+
|
|
21
|
+
import { prisma } from "@allbluecn/database";
|
|
22
|
+
import {
|
|
23
|
+
GENERAL_GROUP_TABLES,
|
|
24
|
+
EXTERNAL_ID_PREFIX,
|
|
25
|
+
externalDocId,
|
|
26
|
+
type GeneralGroup,
|
|
27
|
+
} from "./story/kb-general";
|
|
28
|
+
|
|
29
|
+
export const SEARCH_DOC_SELECT = {
|
|
30
|
+
id: true,
|
|
31
|
+
title: true,
|
|
32
|
+
parentId: true,
|
|
33
|
+
icon: true,
|
|
34
|
+
type: true,
|
|
35
|
+
kb: { select: { id: true, name: true, icon: true } },
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
/** 搜索/列表行 = DocBrief + 树形父引用(树形渲染用) */
|
|
39
|
+
export type SearchDocRow = {
|
|
40
|
+
id: string;
|
|
41
|
+
title: string;
|
|
42
|
+
kbId: string;
|
|
43
|
+
kbName: string;
|
|
44
|
+
kbIcon: string;
|
|
45
|
+
docIcon: string | null;
|
|
46
|
+
parentId: string | null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function toSearchDocRow(doc: {
|
|
50
|
+
id: string;
|
|
51
|
+
title: string;
|
|
52
|
+
parentId: string | null;
|
|
53
|
+
icon: string | null;
|
|
54
|
+
kb: { id: string; name: string; icon: string };
|
|
55
|
+
}): SearchDocRow {
|
|
56
|
+
return {
|
|
57
|
+
id: doc.id,
|
|
58
|
+
title: doc.title,
|
|
59
|
+
kbId: doc.kb.id,
|
|
60
|
+
kbName: doc.kb.name,
|
|
61
|
+
kbIcon: doc.kb.icon,
|
|
62
|
+
docIcon: doc.icon,
|
|
63
|
+
parentId: doc.parentId,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 通用组条目搜索(复用 story 侧收藏过滤逻辑);summary 无实体表返回空 */
|
|
68
|
+
export async function searchGeneralGroupDocs(
|
|
69
|
+
group: GeneralGroup,
|
|
70
|
+
userId: string,
|
|
71
|
+
query: string,
|
|
72
|
+
): Promise<SearchDocRow[]> {
|
|
73
|
+
if (group === "summary") return [];
|
|
74
|
+
const favoriteIds = await prisma.articleFavorite
|
|
75
|
+
.findMany({ where: { entityType: group, userId }, select: { entityId: true } })
|
|
76
|
+
.then((r) => r.map((f) => f.entityId));
|
|
77
|
+
|
|
78
|
+
const rows = await (
|
|
79
|
+
prisma[GENERAL_GROUP_TABLES[group]!] as unknown as {
|
|
80
|
+
findMany: (args: Record<string, unknown>) => Promise<Array<{ id: string; title: string }>>;
|
|
81
|
+
}
|
|
82
|
+
).findMany({
|
|
83
|
+
where: {
|
|
84
|
+
isDeleted: false,
|
|
85
|
+
...(query ? { title: { contains: query, mode: "insensitive" as const } } : {}),
|
|
86
|
+
OR: [{ userId }, ...(favoriteIds.length > 0 ? [{ id: { in: favoriteIds } }] : [])],
|
|
87
|
+
},
|
|
88
|
+
select: { id: true, title: true },
|
|
89
|
+
orderBy: { updatedAt: "desc" as const },
|
|
90
|
+
take: 100,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return rows.map((row) => ({
|
|
94
|
+
id: externalDocId(group, row.id),
|
|
95
|
+
title: row.title,
|
|
96
|
+
kbId: `${EXTERNAL_ID_PREFIX}${group}`,
|
|
97
|
+
kbName: group,
|
|
98
|
+
kbIcon: "book",
|
|
99
|
+
docIcon: null,
|
|
100
|
+
parentId: null,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 当前用户可作为快捷文档来源的 team 库 id 集合 */
|
|
105
|
+
export async function listUserTeamKbIds(userId: string): Promise<string[]> {
|
|
106
|
+
const rows = await prisma.knowledgeBase.findMany({
|
|
107
|
+
where: { visibility: "team", isDeleted: false, userId },
|
|
108
|
+
select: { id: true },
|
|
109
|
+
});
|
|
110
|
+
return rows.map((r) => r.id);
|
|
111
|
+
}
|
|
@@ -40,7 +40,7 @@ export const createComplianceFn = createServerFn({ method: 'POST' })
|
|
|
40
40
|
.handler(async ({ data }) => {
|
|
41
41
|
const request = getRequest()
|
|
42
42
|
const session = await requireAuth(request)
|
|
43
|
-
|
|
43
|
+
const result = await prisma.industryCompliance.create({
|
|
44
44
|
data: {
|
|
45
45
|
canonicalId: complianceCanonicalId({ title: data.title, code: data.code }),
|
|
46
46
|
title: data.title,
|
|
@@ -57,4 +57,5 @@ export const createComplianceFn = createServerFn({ method: 'POST' })
|
|
|
57
57
|
},
|
|
58
58
|
select: { id: true },
|
|
59
59
|
})
|
|
60
|
+
return result
|
|
60
61
|
})
|
|
@@ -42,7 +42,7 @@ export const createLegalFn = createServerFn({ method: 'POST' })
|
|
|
42
42
|
.handler(async ({ data }) => {
|
|
43
43
|
const request = getRequest()
|
|
44
44
|
const session = await requireAuth(request)
|
|
45
|
-
|
|
45
|
+
const result = await prisma.legalRegulation.create({
|
|
46
46
|
data: {
|
|
47
47
|
canonicalId: legalCanonicalId({ title: data.title, code: data.code }),
|
|
48
48
|
title: data.title,
|
|
@@ -61,4 +61,5 @@ export const createLegalFn = createServerFn({ method: 'POST' })
|
|
|
61
61
|
},
|
|
62
62
|
select: { id: true },
|
|
63
63
|
})
|
|
64
|
+
return result
|
|
64
65
|
})
|
|
@@ -100,7 +100,7 @@ describe("bulkArchiveStoriesFn", () => {
|
|
|
100
100
|
|
|
101
101
|
it("未登录时拒绝且不查询", async () => {
|
|
102
102
|
mocks.currentSession.mockRejectedValue(new Error("unauthorized"));
|
|
103
|
-
await expect(bulkArchiveStoriesFn({ data: { storyIds: ["a"] } })).rejects.
|
|
103
|
+
await expect(bulkArchiveStoriesFn({ data: { storyIds: ["a"] } })).rejects.toThrow("unauthorized");
|
|
104
104
|
expect(mocks.findMany).not.toHaveBeenCalled();
|
|
105
105
|
});
|
|
106
106
|
});
|
|
@@ -68,9 +68,7 @@ describe("suggestLabelsCore", () => {
|
|
|
68
68
|
|
|
69
69
|
it("AI 未配置抛错", async () => {
|
|
70
70
|
mocks.hasAiKey.mockResolvedValue(false)
|
|
71
|
-
await expect(suggestLabelsCore({ name: "x" }, "u1")).rejects.
|
|
72
|
-
message: expect.stringContaining("AI"),
|
|
73
|
-
})
|
|
71
|
+
await expect(suggestLabelsCore({ name: "x" }, "u1")).rejects.toThrow("AI")
|
|
74
72
|
})
|
|
75
73
|
|
|
76
74
|
it("标签超 100 个时按使用频次截断", async () => {
|
|
@@ -93,8 +91,6 @@ describe("suggestLabelsCore", () => {
|
|
|
93
91
|
|
|
94
92
|
it("AI 输出解析失败抛错", async () => {
|
|
95
93
|
mocks.runChat.mockResolvedValue("垃圾输出")
|
|
96
|
-
await expect(suggestLabelsCore({ name: "x" }, "u1")).rejects.
|
|
97
|
-
message: expect.stringContaining("解析失败"),
|
|
98
|
-
})
|
|
94
|
+
await expect(suggestLabelsCore({ name: "x" }, "u1")).rejects.toThrow("解析失败")
|
|
99
95
|
})
|
|
100
96
|
})
|
|
@@ -127,9 +127,7 @@ describe("listStoryAvailableKbsFn", () => {
|
|
|
127
127
|
|
|
128
128
|
it("需求不存在抛错", async () => {
|
|
129
129
|
mocks.storyFindUnique.mockResolvedValue(null);
|
|
130
|
-
await expect(listStoryAvailableKbsFn({ data: { storyId: "nope" } })).rejects.
|
|
131
|
-
message: expect.stringContaining("需求不存在"),
|
|
132
|
-
});
|
|
130
|
+
await expect(listStoryAvailableKbsFn({ data: { storyId: "nope" } })).rejects.toThrow("需求不存在");
|
|
133
131
|
});
|
|
134
132
|
});
|
|
135
133
|
|
|
@@ -153,7 +151,7 @@ describe("searchKbDocsFn 两态", () => {
|
|
|
153
151
|
mocks.kbFindFirst.mockResolvedValue(null); // 不在 scope 内
|
|
154
152
|
await expect(
|
|
155
153
|
searchKbDocsFn({ data: { storyId: "s1", kbId: "kbX", query: "" } }),
|
|
156
|
-
).rejects.
|
|
154
|
+
).rejects.toThrow("不在可用范围");
|
|
157
155
|
});
|
|
158
156
|
|
|
159
157
|
it("全部模式:平铺分页(take 21 判 hasMore)", async () => {
|
|
@@ -307,7 +307,7 @@ describe("updateDependencyFn", () => {
|
|
|
307
307
|
updateDependencyFn({
|
|
308
308
|
data: { depId: "d1", sourceId: "a", targetId: "c", type: "BLOCKS" },
|
|
309
309
|
}),
|
|
310
|
-
).rejects.
|
|
310
|
+
).rejects.toThrow("依赖关系两端不匹配");
|
|
311
311
|
expect(mocks.depUpdate).not.toHaveBeenCalled();
|
|
312
312
|
});
|
|
313
313
|
|
|
@@ -317,7 +317,7 @@ describe("updateDependencyFn", () => {
|
|
|
317
317
|
updateDependencyFn({
|
|
318
318
|
data: { depId: "missing", sourceId: "a", targetId: "b", type: "BLOCKS" },
|
|
319
319
|
}),
|
|
320
|
-
).rejects.
|
|
320
|
+
).rejects.toThrow("依赖关系不存在");
|
|
321
321
|
});
|
|
322
322
|
});
|
|
323
323
|
|
|
@@ -163,7 +163,7 @@ describe("listStoriesInGroupFn", () => {
|
|
|
163
163
|
mocks.count.mockResolvedValue(0);
|
|
164
164
|
await expect(
|
|
165
165
|
listStoriesInGroupFn({ data: { archived: false } }),
|
|
166
|
-
).rejects.
|
|
166
|
+
).rejects.toThrow("unauthorized");
|
|
167
167
|
expect(mocks.findMany).not.toHaveBeenCalled();
|
|
168
168
|
expect(mocks.count).not.toHaveBeenCalled();
|
|
169
169
|
});
|
|
@@ -345,7 +345,7 @@ describe("listStoryGroupsFn", () => {
|
|
|
345
345
|
mocks.currentSession.mockRejectedValue(new Error("unauthorized"));
|
|
346
346
|
await expect(
|
|
347
347
|
listStoryGroupsFn({ data: { archived: false, groupBy: "state" } }),
|
|
348
|
-
).rejects.
|
|
348
|
+
).rejects.toThrow("unauthorized");
|
|
349
349
|
expect(mocks.groupBy).not.toHaveBeenCalled();
|
|
350
350
|
});
|
|
351
351
|
});
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
// 通用知识库纯常量与工具(客户端可安全导入)。
|
|
19
|
+
// 服务端 prisma 相关导出见 kb-general.ts。
|
|
20
|
+
|
|
21
|
+
export const GENERAL_GROUPS = ["legal", "compliance", "dictionary", "bookmark", "summary"] as const;
|
|
22
|
+
export type GeneralGroup = (typeof GENERAL_GROUPS)[number];
|
|
23
|
+
|
|
24
|
+
export const EXTERNAL_ID_PREFIX = "ext:";
|
|
25
|
+
|
|
26
|
+
export function parseExternalDocId(id: string): { group: GeneralGroup; externalId: string } | null {
|
|
27
|
+
if (!id.startsWith(EXTERNAL_ID_PREFIX)) return null;
|
|
28
|
+
const rest = id.slice(EXTERNAL_ID_PREFIX.length);
|
|
29
|
+
const idx = rest.indexOf(":");
|
|
30
|
+
if (idx <= 0) return null;
|
|
31
|
+
const group = rest.slice(0, idx);
|
|
32
|
+
const externalId = rest.slice(idx + 1);
|
|
33
|
+
if (!(GENERAL_GROUPS as readonly string[]).includes(group) || !externalId) return null;
|
|
34
|
+
return { group: group as GeneralGroup, externalId };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function externalDocId(group: GeneralGroup, externalId: string): string {
|
|
38
|
+
return `${EXTERNAL_ID_PREFIX}${group}:${externalId}`;
|
|
39
|
+
}
|
|
@@ -15,25 +15,38 @@
|
|
|
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
|
-
//
|
|
19
|
-
//
|
|
18
|
+
// 通用知识库(法律法规/行业合规/词典词条/网站收藏/周报月报)的纯工具与 Prisma 表映射。
|
|
19
|
+
// 客户端可安全导入的纯常量见 kb-general-consts.ts;含 prisma 的表映射仅服务端可用。
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
import { prisma } from "@allbluecn/database";
|
|
22
|
+
import {
|
|
23
|
+
GENERAL_GROUPS,
|
|
24
|
+
EXTERNAL_ID_PREFIX,
|
|
25
|
+
parseExternalDocId,
|
|
26
|
+
externalDocId,
|
|
27
|
+
type GeneralGroup,
|
|
28
|
+
} from "./kb-general-consts";
|
|
23
29
|
|
|
24
|
-
export
|
|
30
|
+
export {
|
|
31
|
+
GENERAL_GROUPS,
|
|
32
|
+
EXTERNAL_ID_PREFIX,
|
|
33
|
+
parseExternalDocId,
|
|
34
|
+
externalDocId,
|
|
35
|
+
type GeneralGroup,
|
|
36
|
+
};
|
|
25
37
|
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const externalId = rest.slice(idx + 1);
|
|
33
|
-
if (!(GENERAL_GROUPS as readonly string[]).includes(group) || !externalId) return null;
|
|
34
|
-
return { group: group as GeneralGroup, externalId };
|
|
35
|
-
}
|
|
38
|
+
export const GENERAL_GROUP_TABLES = {
|
|
39
|
+
legal: "legalRegulation",
|
|
40
|
+
compliance: "industryCompliance",
|
|
41
|
+
dictionary: "dictionaryTerm",
|
|
42
|
+
bookmark: "websiteBookmark",
|
|
43
|
+
} as const satisfies Record<Exclude<GeneralGroup, "summary">, string>;
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
/** 动态访问通用组表的类型安全入口(运行时仍需 where isDeleted: false) */
|
|
46
|
+
export function generalGroupModel(group: Exclude<GeneralGroup, "summary">) {
|
|
47
|
+
const table = GENERAL_GROUP_TABLES[group];
|
|
48
|
+
return prisma[table] as unknown as {
|
|
49
|
+
findFirst: (args: Record<string, unknown>) => Promise<{ id: string } | null>;
|
|
50
|
+
findMany: (args: Record<string, unknown>) => Promise<Array<{ id: string; title: string }>>;
|
|
51
|
+
};
|
|
39
52
|
}
|
|
@@ -28,62 +28,19 @@ import {
|
|
|
28
28
|
parseExternalDocId,
|
|
29
29
|
externalDocId,
|
|
30
30
|
type GeneralGroup,
|
|
31
|
+
GENERAL_GROUP_TABLES,
|
|
32
|
+
generalGroupModel,
|
|
31
33
|
} from "./kb-general";
|
|
32
34
|
|
|
33
35
|
// ---------- 通用知识库(法律法规/行业合规/词典词条/网站收藏/周报月报) ----------
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const GENERAL_ROW_SELECT = { id: true, title: true } as const;
|
|
43
|
-
|
|
44
|
-
async function searchGeneralGroupDocs(
|
|
45
|
-
group: GeneralGroup,
|
|
46
|
-
userId: string,
|
|
47
|
-
query: string,
|
|
48
|
-
): Promise<SearchDocRow[]> {
|
|
49
|
-
const table = GENERAL_GROUP_TABLES[group as Exclude<GeneralGroup, "summary">];
|
|
50
|
-
if (!table) return []; // summary(周报月报)暂无实体表,无可关联内容
|
|
51
|
-
|
|
52
|
-
const favoriteIds = await prisma.articleFavorite
|
|
53
|
-
.findMany({
|
|
54
|
-
where: { entityType: group, userId },
|
|
55
|
-
select: { entityId: true },
|
|
56
|
-
})
|
|
57
|
-
.then((r) => r.map((f) => f.entityId));
|
|
58
|
-
|
|
59
|
-
const rows = await (
|
|
60
|
-
prisma[table] as unknown as {
|
|
61
|
-
findMany: (args: Record<string, unknown>) => Promise<Array<{ id: string; title: string }>>;
|
|
62
|
-
}
|
|
63
|
-
).findMany({
|
|
64
|
-
where: {
|
|
65
|
-
isDeleted: false,
|
|
66
|
-
...(query ? { title: { contains: query, mode: "insensitive" as const } } : {}),
|
|
67
|
-
OR: [
|
|
68
|
-
{ userId },
|
|
69
|
-
...(favoriteIds.length > 0 ? [{ id: { in: favoriteIds } }] : []),
|
|
70
|
-
],
|
|
71
|
-
},
|
|
72
|
-
select: GENERAL_ROW_SELECT,
|
|
73
|
-
orderBy: { updatedAt: "desc" as const },
|
|
74
|
-
take: 100,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return rows.map((row) => ({
|
|
78
|
-
id: externalDocId(group, row.id),
|
|
79
|
-
title: row.title,
|
|
80
|
-
kbId: `${EXTERNAL_ID_PREFIX}${group}`,
|
|
81
|
-
kbName: group,
|
|
82
|
-
kbIcon: "book",
|
|
83
|
-
docIcon: null,
|
|
84
|
-
parentId: null,
|
|
85
|
-
}));
|
|
86
|
-
}
|
|
37
|
+
export type { SearchDocRow } from "../kb-doc-search";
|
|
38
|
+
import {
|
|
39
|
+
SEARCH_DOC_SELECT as DOC_SELECT,
|
|
40
|
+
searchGeneralGroupDocs,
|
|
41
|
+
toSearchDocRow as toDocBrief,
|
|
42
|
+
type SearchDocRow as SearchDocRow_,
|
|
43
|
+
} from "../kb-doc-search";
|
|
87
44
|
|
|
88
45
|
export const linkPrdFn = createServerFn({ method: "POST" })
|
|
89
46
|
.validator(z.object({ storyId: z.string(), prdId: z.string() }))
|
|
@@ -184,44 +141,6 @@ export const searchPrdsFn = createServerFn({ method: "GET" })
|
|
|
184
141
|
}));
|
|
185
142
|
});
|
|
186
143
|
|
|
187
|
-
const DOC_SELECT = {
|
|
188
|
-
id: true,
|
|
189
|
-
title: true,
|
|
190
|
-
parentId: true,
|
|
191
|
-
icon: true,
|
|
192
|
-
type: true,
|
|
193
|
-
kb: { select: { id: true, name: true, icon: true } },
|
|
194
|
-
} as const;
|
|
195
|
-
|
|
196
|
-
/** 搜索/列表行 = DocBrief + 树形父引用(KnowledgeSelectDialog 树形渲染用) */
|
|
197
|
-
export type SearchDocRow = {
|
|
198
|
-
id: string;
|
|
199
|
-
title: string;
|
|
200
|
-
kbId: string;
|
|
201
|
-
kbName: string;
|
|
202
|
-
kbIcon: string;
|
|
203
|
-
docIcon: string | null;
|
|
204
|
-
parentId: string | null;
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
function toDocBrief(doc: {
|
|
208
|
-
id: string;
|
|
209
|
-
title: string;
|
|
210
|
-
parentId: string | null;
|
|
211
|
-
icon: string | null;
|
|
212
|
-
kb: { id: string; name: string; icon: string };
|
|
213
|
-
}): SearchDocRow {
|
|
214
|
-
return {
|
|
215
|
-
id: doc.id,
|
|
216
|
-
title: doc.title,
|
|
217
|
-
kbId: doc.kb.id,
|
|
218
|
-
kbName: doc.kb.name,
|
|
219
|
-
kbIcon: doc.kb.icon,
|
|
220
|
-
docIcon: doc.icon,
|
|
221
|
-
parentId: doc.parentId,
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
144
|
/** 查需求 → 组装 scope 输入(内部复用) */
|
|
226
145
|
async function resolveStoryScope(storyId: string, userId: string) {
|
|
227
146
|
const story = await prisma.story.findUnique({
|
|
@@ -341,16 +260,11 @@ export const linkDocsFn = createServerFn({ method: "POST" })
|
|
|
341
260
|
const extToLink: Array<{ group: GeneralGroup; externalId: string }> = [];
|
|
342
261
|
for (const { id, parsed } of extParsed) {
|
|
343
262
|
if (existingExt.has(id)) continue; // skipped
|
|
344
|
-
|
|
345
|
-
if (!table) {
|
|
263
|
+
if (!GENERAL_GROUP_TABLES[parsed.group as Exclude<GeneralGroup, "summary">]) {
|
|
346
264
|
failed.push({ docId: id, reason: "outOfScope" });
|
|
347
265
|
continue;
|
|
348
266
|
}
|
|
349
|
-
const found = await (
|
|
350
|
-
prisma[table] as unknown as {
|
|
351
|
-
findFirst: (args: Record<string, unknown>) => Promise<{ id: string } | null>;
|
|
352
|
-
}
|
|
353
|
-
).findFirst({ where: { id: parsed.externalId, isDeleted: false }, select: { id: true } });
|
|
267
|
+
const found = await generalGroupModel(parsed.group as Exclude<GeneralGroup, "summary">).findFirst({ where: { id: parsed.externalId, isDeleted: false }, select: { id: true } });
|
|
354
268
|
if (found) extToLink.push(parsed);
|
|
355
269
|
else failed.push({ docId: id, reason: "outOfScope" });
|
|
356
270
|
}
|
|
@@ -482,15 +396,11 @@ export const getLinkedDocsFn = createServerFn({ method: "GET" })
|
|
|
482
396
|
arr.push(l.externalId);
|
|
483
397
|
byGroup.set(group, arr);
|
|
484
398
|
}
|
|
485
|
-
const extRows:
|
|
399
|
+
const extRows: SearchDocRow_[] = [];
|
|
486
400
|
for (const [group, ids] of byGroup) {
|
|
487
401
|
const table = GENERAL_GROUP_TABLES[group as Exclude<GeneralGroup, "summary">];
|
|
488
402
|
if (!table) continue;
|
|
489
|
-
const rows = await (
|
|
490
|
-
prisma[table] as unknown as {
|
|
491
|
-
findMany: (args: Record<string, unknown>) => Promise<Array<{ id: string; title: string }>>;
|
|
492
|
-
}
|
|
493
|
-
).findMany({ where: { id: { in: ids } }, select: GENERAL_ROW_SELECT });
|
|
403
|
+
const rows = await generalGroupModel(group as Exclude<GeneralGroup, "summary">).findMany({ where: { id: { in: ids } }, select: { id: true, title: true } });
|
|
494
404
|
for (const row of rows) {
|
|
495
405
|
extRows.push({
|
|
496
406
|
id: externalDocId(group, row.id),
|
|
@@ -20,7 +20,7 @@ import { getRequest } from "@tanstack/react-start/server";
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { prisma, Prisma } from "@allbluecn/database";
|
|
22
22
|
import { requireAuth } from "@/server/auth-helpers";
|
|
23
|
-
import { ensureBuiltinIndependentProject } from "@/server/builtin-project";
|
|
23
|
+
import { ensureBuiltinIndependentProject, resolveStoryScopeProjectId } from "@/server/builtin-project";
|
|
24
24
|
import { isUserInTeam } from "@/server/team-access";
|
|
25
25
|
import { BUILTIN_INDEPENDENT_DISPLAY_ID } from "@/lib/builtin-project";
|
|
26
26
|
import type { ActivityItem } from "@/components/story/types";
|
|
@@ -59,10 +59,7 @@ export const listStoriesFn = createServerFn({ method: "GET" })
|
|
|
59
59
|
const session = await requireAuth(request);
|
|
60
60
|
const userId = session.user!.id!;
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
if (!projectId) {
|
|
64
|
-
projectId = await ensureBuiltinIndependentProject(userId);
|
|
65
|
-
}
|
|
62
|
+
const projectId = await resolveStoryScopeProjectId(data.projectId, userId);
|
|
66
63
|
|
|
67
64
|
const where = buildListWhere({ ...data, projectId });
|
|
68
65
|
|
|
@@ -20,7 +20,7 @@ import { getRequest } from "@tanstack/react-start/server";
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { prisma, Prisma } from "@allbluecn/database";
|
|
22
22
|
import { requireAuth } from "@/server/auth-helpers";
|
|
23
|
-
import {
|
|
23
|
+
import { resolveStoryScopeProjectId } from "@/server/builtin-project";
|
|
24
24
|
import { GROUP_KEYS, ORDER_KEYS, ORDER_DIRECTIONS, type GroupKey, type OrderKey } from "@/components/story/display/types";
|
|
25
25
|
import { STORY_STATES, STORY_PRIORITIES, type StoryState, type StoryPriority } from "@/components/story/constants";
|
|
26
26
|
import { STORY_LIST_SELECT_LIGHT, castStoryUsersLight, type StoryListRowLight } from "./types";
|
|
@@ -61,10 +61,7 @@ export const listStoriesInGroupFn = createServerFn({ method: "GET" })
|
|
|
61
61
|
const session = await requireAuth(request);
|
|
62
62
|
const userId = session.user!.id!;
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
if (!projectId) {
|
|
66
|
-
projectId = await ensureBuiltinIndependentProject(userId);
|
|
67
|
-
}
|
|
64
|
+
const projectId = await resolveStoryScopeProjectId(data.projectId, userId);
|
|
68
65
|
|
|
69
66
|
const base = buildListWhere({ ...data, projectId });
|
|
70
67
|
const baseAnd = Array.isArray(base.AND) ? base.AND : base.AND ? [base.AND] : [];
|
|
@@ -126,10 +123,7 @@ export const listAllGroupsItemsFn = createServerFn({ method: "GET" })
|
|
|
126
123
|
const session = await requireAuth(request);
|
|
127
124
|
const userId = session.user!.id!;
|
|
128
125
|
|
|
129
|
-
|
|
130
|
-
if (!projectId) {
|
|
131
|
-
projectId = await ensureBuiltinIndependentProject(userId);
|
|
132
|
-
}
|
|
126
|
+
const projectId = await resolveStoryScopeProjectId(data.projectId, userId);
|
|
133
127
|
|
|
134
128
|
const base = buildListWhere({ ...data, projectId });
|
|
135
129
|
const field = ORDER_FIELD[data.orderBy];
|
|
@@ -240,10 +234,7 @@ export const listStoryGroupsFn = createServerFn({ method: "GET" })
|
|
|
240
234
|
const session = await requireAuth(request);
|
|
241
235
|
const userId = session.user!.id!;
|
|
242
236
|
|
|
243
|
-
|
|
244
|
-
if (!projectId) {
|
|
245
|
-
projectId = await ensureBuiltinIndependentProject(userId);
|
|
246
|
-
}
|
|
237
|
+
const projectId = await resolveStoryScopeProjectId(data.projectId, userId);
|
|
247
238
|
|
|
248
239
|
const where = buildListWhere({ ...data, projectId });
|
|
249
240
|
if (data.groupBy === "none") return { groups: [] };
|
|
@@ -551,7 +551,7 @@ export const listTeamUsersFn = createServerFn({ method: 'GET' }).handler(async (
|
|
|
551
551
|
const [owner, members] = await Promise.all([
|
|
552
552
|
prisma.user.findUnique({ where: { id: ownerId }, select: USER_BRIEF_SELECT }),
|
|
553
553
|
prisma.teamMembership.findMany({
|
|
554
|
-
where: { ownerId },
|
|
554
|
+
where: { ownerId, memberId: { not: ownerId } },
|
|
555
555
|
orderBy: { joinedAt: 'asc' },
|
|
556
556
|
select: { seatType: true, member: { select: USER_BRIEF_SELECT } },
|
|
557
557
|
}),
|