@allbluecn/web-app 0.10.3 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/package.json +24 -11
  2. package/public/favicon-dark.svg +1 -0
  3. package/public/favicon.ico +0 -0
  4. package/public/favicon.svg +1 -0
  5. package/public/logo/allblue-logo-app-icon.svg +7 -0
  6. package/public/logo/allblue-logo-dark-square.svg +4 -0
  7. package/public/logo/allblue-logo-dark.png +0 -0
  8. package/public/logo/allblue-logo-dark.svg +4 -0
  9. package/public/logo/allblue-logo-light-square.svg +4 -0
  10. package/public/logo/allblue-logo-light.png +0 -0
  11. package/public/logo/allblue-logo-light.svg +4 -0
  12. package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
  13. package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
  14. package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
  15. package/src/__tests__/integration/login-email-acceptance.test.ts +202 -0
  16. package/src/__tests__/integration/login-email-p2-acceptance.test.ts +142 -0
  17. package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
  18. package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
  19. package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
  20. package/src/__tests__/security/xss-sanitize.test.ts +99 -0
  21. package/src/__tests__/setup.ts +9 -0
  22. package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
  23. package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
  24. package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
  25. package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
  26. package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
  27. package/src/components/auth/icon-input.tsx +9 -1
  28. package/src/components/auth/password-input.tsx +9 -1
  29. package/src/components/changelog/changelog-page.tsx +44 -1
  30. package/src/components/error/global-error-boundary.tsx +2 -2
  31. package/src/components/forms/app-field.tsx +3 -1
  32. package/src/components/knowledge/general-groups.ts +15 -0
  33. package/src/components/knowledge/kb-filter-popover.tsx +143 -0
  34. package/src/components/knowledge/knowledge-toolbar.tsx +17 -21
  35. package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
  36. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
  37. package/src/components/layout/sidebar-03/app-sidebar.tsx +4 -15
  38. package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
  39. package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
  40. package/src/components/notifications/settings-notifications.tsx +41 -1
  41. package/src/components/settings/personal-team/account/settings-profile.tsx +68 -1
  42. package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
  43. package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
  44. package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
  45. package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
  46. package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
  47. package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
  48. package/src/components/story/relations/general-groups.ts +1 -15
  49. package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
  50. package/src/components/story/story-list-view.tsx +7 -1
  51. package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
  52. package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
  53. package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
  54. package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
  55. package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
  56. package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
  57. package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
  58. package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
  59. package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
  60. package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
  61. package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
  62. package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
  63. package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
  64. package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
  65. package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
  66. package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
  67. package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
  68. package/src/lib/__tests__/edition-desktop.test.ts +23 -0
  69. package/src/lib/__tests__/msw-sample.test.ts +68 -0
  70. package/src/lib/__tests__/pgvector.test.ts +2 -2
  71. package/src/lib/app-version.ts +1 -1
  72. package/src/lib/auth/auth.config.ts +123 -99
  73. package/src/lib/changelog/sdk-versions.ts +21 -21
  74. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
  75. package/src/lib/collections/reference/workspaces-collection.ts +3 -1
  76. package/src/lib/edition-desktop.ts +22 -0
  77. package/src/lib/fetch-json.ts +23 -0
  78. package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
  79. package/src/lib/sanitize.ts +14 -1
  80. package/src/lib/session.ts +10 -2
  81. package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
  82. package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
  83. package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
  84. package/src/routeTree.gen.ts +114 -0
  85. package/src/routes/__root.tsx +20 -3
  86. package/src/routes/__tests__/-auth-schemas.test.ts +62 -0
  87. package/src/routes/__tests__/-edition-routing.test.tsx +18 -5
  88. package/src/routes/__tests__/route-tree-structure.test.ts +66 -0
  89. package/src/routes/_login/changelog/confirm/route.lazy.tsx +72 -0
  90. package/src/routes/_login/changelog/confirm/route.tsx +28 -0
  91. package/src/routes/_login/email-unsubscribe/route.lazy.tsx +76 -0
  92. package/src/routes/_login/email-unsubscribe/route.tsx +28 -0
  93. package/src/routes/_login/forgot-password/route.lazy.tsx +2 -1
  94. package/src/routes/_login/login/route.lazy.tsx +4 -2
  95. package/src/routes/_login/register/route.lazy.tsx +28 -17
  96. package/src/routes/_login/register/route.tsx +8 -1
  97. package/src/routes/_login/reset-password/route.lazy.tsx +4 -2
  98. package/src/routes/_login/schemas/-auth.ts +30 -7
  99. package/src/routes/_login/verify-email/route.lazy.tsx +82 -0
  100. package/src/routes/_login/verify-email/route.tsx +28 -0
  101. package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
  102. package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
  103. package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
  104. package/src/routes/_nav/route.tsx +26 -0
  105. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
  106. package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
  107. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
  108. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  109. package/src/routes/_public/index/route.tsx +2 -1
  110. package/src/routes/api/auth/desktop-callback.ts +2 -2
  111. package/src/routes/api/billing-webhook.ts +4 -0
  112. package/src/routes/api/cron/changelog-release.ts +100 -0
  113. package/src/routes/api/cron/subscription-reminders.ts +110 -0
  114. package/src/routes/api.auth.$.ts +3 -5
  115. package/src/server/__tests__/agent-crud.test.ts +4 -4
  116. package/src/server/__tests__/auth-registration.test.ts +60 -0
  117. package/src/server/__tests__/chat-conversations.test.ts +1 -3
  118. package/src/server/__tests__/fetch-guard.test.ts +99 -0
  119. package/src/server/__tests__/permissions.test.ts +1 -3
  120. package/src/server/__tests__/resources.team-seats.test.ts +2 -0
  121. package/src/server/__tests__/skill-crud.test.ts +3 -3
  122. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
  123. package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
  124. package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
  125. package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
  126. package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
  127. package/src/server/ai-gateway/ai-output-guard.ts +77 -0
  128. package/src/server/ai-gateway/index.ts +5 -0
  129. package/src/server/ai-gateway/kb-context-guard.ts +46 -0
  130. package/src/server/ai-gateway/llm-fence.ts +82 -0
  131. package/src/server/ai-gateway/persistence/run-store.ts +19 -0
  132. package/src/server/ai-gateway/story-prompt.ts +4 -2
  133. package/src/server/ai-gateway/tools/definitions.ts +12 -6
  134. package/src/server/ai-gateway/tools/server.ts +97 -54
  135. package/src/server/auth-registration.ts +40 -0
  136. package/src/server/billing-email.ts +39 -0
  137. package/src/server/builtin-project.ts +21 -0
  138. package/src/server/email/__tests__/deliverability.test.ts +68 -0
  139. package/src/server/email/__tests__/index.test.ts +32 -0
  140. package/src/server/email/__tests__/outbox.test.tsx +129 -0
  141. package/src/server/email/__tests__/product-templates.test.tsx +106 -0
  142. package/src/server/email/__tests__/registry.test.tsx +50 -0
  143. package/src/server/email/deliverability.ts +57 -0
  144. package/src/server/email/drivers/console.ts +11 -0
  145. package/src/server/email/drivers/resend.ts +31 -0
  146. package/src/server/email/drivers/smtp.ts +27 -0
  147. package/src/server/email/index.ts +56 -0
  148. package/src/server/email/outbox.ts +141 -0
  149. package/src/server/email/preferences.ts +20 -0
  150. package/src/server/email/registry.ts +38 -0
  151. package/src/server/email/sender.ts +36 -13
  152. package/src/server/email/templates/_base/button.tsx +12 -0
  153. package/src/server/email/templates/_base/email-layout.tsx +117 -0
  154. package/src/server/email/templates/_base/theme-fonts.tsx +43 -0
  155. package/src/server/email/templates/_base/theme.ts +22 -0
  156. package/src/server/email/templates/auth/reset-password.tsx +58 -0
  157. package/src/server/email/templates/auth/verify-email.tsx +59 -0
  158. package/src/server/email/templates/auth/welcome.tsx +49 -0
  159. package/src/server/email/templates/billing/payment-receipt.tsx +51 -0
  160. package/src/server/email/templates/billing/subscription-canceled.tsx +51 -0
  161. package/src/server/email/templates/billing/subscription-ending.tsx +51 -0
  162. package/src/server/email/templates/notification/mirror.tsx +50 -0
  163. package/src/server/email/templates/product/changelog-confirm.tsx +53 -0
  164. package/src/server/email/templates/product/changelog-release.tsx +65 -0
  165. package/src/server/email/templates/team/apply-notify.tsx +53 -0
  166. package/src/server/email/templates/team/approved.tsx +53 -0
  167. package/src/server/email/templates/team/invite.tsx +57 -0
  168. package/src/server/fetch-guard.ts +170 -0
  169. package/src/server/middlewares/security-headers.ts +122 -0
  170. package/src/server/notifications/__tests__/email-mirror.test.ts +193 -0
  171. package/src/server/notifications/__tests__/verify-reminder.test.ts +94 -0
  172. package/src/server/notifications/notification-service.ts +71 -1
  173. package/src/server/notifications/verify-reminder.ts +55 -0
  174. package/src/server/oauth-config.ts +59 -0
  175. package/src/server/serverFns/__tests__/auth-server-schemas.test.ts +168 -0
  176. package/src/server/serverFns/__tests__/billing.test.ts +263 -0
  177. package/src/server/serverFns/__tests__/email-verification.test.ts +149 -0
  178. package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
  179. package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
  180. package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
  181. package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
  182. package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
  183. package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
  184. package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
  185. package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
  186. package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
  187. package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
  188. package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
  189. package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
  190. package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
  191. package/src/server/serverFns/__tests__/team-invitation.test.ts +5 -5
  192. package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
  193. package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
  194. package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
  195. package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
  196. package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
  197. package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
  198. package/src/server/serverFns/auth/email-verification.ts +132 -0
  199. package/src/server/serverFns/auth/forgot-password.ts +22 -2
  200. package/src/server/serverFns/auth/providers.ts +4 -3
  201. package/src/server/serverFns/auth/register.ts +63 -19
  202. package/src/server/serverFns/auth/registration-mode.ts +23 -0
  203. package/src/server/serverFns/auth/reset-password.ts +1 -1
  204. package/src/server/serverFns/auth/{invitation.ts → verify-reminder.ts} +9 -26
  205. package/src/server/serverFns/changelog/__tests__/subscribe.test.ts +119 -0
  206. package/src/server/serverFns/changelog/subscribe.ts +89 -0
  207. package/src/server/serverFns/email-unsubscribe.ts +66 -0
  208. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
  209. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
  210. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
  211. package/src/server/serverFns/kb-doc-search.ts +111 -0
  212. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  213. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  214. package/src/server/serverFns/notifications.ts +10 -0
  215. package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
  216. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
  217. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
  218. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
  219. package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
  220. package/src/server/serverFns/story/kb-general-consts.ts +39 -0
  221. package/src/server/serverFns/story/kb-general.ts +30 -17
  222. package/src/server/serverFns/story/story-assoc.ts +13 -103
  223. package/src/server/serverFns/story/story-crud.ts +2 -5
  224. package/src/server/serverFns/story/story-groups.ts +4 -13
  225. package/src/server/serverFns/team.ts +50 -12
  226. package/src/server/serverFns/workspace-kb-utils.ts +138 -0
  227. package/src/server/serverFns/workspace-kb.ts +403 -0
  228. package/src/server/serverFns/workspace.ts +4 -1
  229. package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
  230. package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
  231. package/src/shared/ai-cli/adapter.ts +2 -2
  232. package/src/shared/ai-cli/cli-detector.ts +18 -9
  233. package/src/start.ts +2 -1
  234. package/src/styles/globals.css +114 -0
  235. package/src/types/jest-dom-vitest.d.ts +8 -7
  236. package/vite.shared.ts +105 -4
  237. package/public/file.svg +0 -1
  238. package/public/globe.svg +0 -1
  239. package/public/window.svg +0 -1
  240. package/src/lib/__tests__/cli-detector.test.ts +0 -80
  241. /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
  242. /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
  243. /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
  244. /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
  245. /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
  246. /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
- return prisma.industryCompliance.create({
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
- return prisma.legalRegulation.create({
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
  })
@@ -11,10 +11,13 @@ import {
11
11
  type NotificationItem,
12
12
  type NotificationListResult,
13
13
  type CategoryFrequencies,
14
+ type CategoryEmailFlags,
14
15
  } from '@allbluecn/kernel'
15
16
  import {
16
17
  getOrCreatePreferences,
17
18
  DEFAULT_CATEGORIES,
19
+ DEFAULT_EMAIL_CATEGORIES,
20
+ normalizeEmailCategories,
18
21
  } from '@/server/notifications/notification-service'
19
22
 
20
23
  const PAGE_SIZE = 20
@@ -132,6 +135,10 @@ export const getNotificationPreferencesFn = createServerFn({ method: 'GET' })
132
135
 
133
136
  const preferencesSchema = z.object({
134
137
  categories: z.record(z.string(), z.enum(NOTIFICATION_FREQUENCIES)),
138
+ // 外部直连 serverFn 时可能不传 emailCategories,normalize 兜底全 false
139
+ emailCategories: z
140
+ .record(z.enum(NOTIFICATION_CATEGORIES), z.boolean())
141
+ .optional(),
135
142
  dndEnabled: z.boolean(),
136
143
  dndStart: z.string().regex(/^\d{2}:\d{2}$/),
137
144
  dndEnd: z.string().regex(/^\d{2}:\d{2}$/),
@@ -152,17 +159,20 @@ export const updateNotificationPreferencesFn = createServerFn({
152
159
  categories[c] = v as CategoryFrequencies[NotificationCategory]
153
160
  }
154
161
  }
162
+ const emailCategories = normalizeEmailCategories(data.emailCategories)
155
163
  await prisma.notificationPreference.upsert({
156
164
  where: { userId },
157
165
  create: {
158
166
  userId,
159
167
  categories,
168
+ emailCategories,
160
169
  dndEnabled: data.dndEnabled,
161
170
  dndStart: data.dndStart,
162
171
  dndEnd: data.dndEnd,
163
172
  },
164
173
  update: {
165
174
  categories,
175
+ emailCategories,
166
176
  dndEnabled: data.dndEnabled,
167
177
  dndStart: data.dndStart,
168
178
  dndEnd: data.dndEnd,
@@ -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.toMatchObject({ message: expect.stringContaining("unauthorized") });
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.toMatchObject({
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.toMatchObject({
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.toMatchObject({
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.toMatchObject({ message: expect.stringContaining("不在可用范围") });
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.toMatchObject({ message: expect.stringContaining("依赖关系两端不匹配") });
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.toMatchObject({ message: expect.stringContaining("依赖关系不存在") });
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.toMatchObject({ message: expect.stringContaining("unauthorized") });
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.toMatchObject({ message: expect.stringContaining("unauthorized") });
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
- // 独立成文件以保持 story-assoc.ts(serverFn 模块)纯净:客户端可安全导入。
18
+ // 通用知识库(法律法规/行业合规/词典词条/网站收藏/周报月报)的纯工具与 Prisma 表映射。
19
+ // 客户端可安全导入的纯常量见 kb-general-consts.ts;含 prisma 的表映射仅服务端可用。
20
20
 
21
- export const GENERAL_GROUPS = ["legal", "compliance", "dictionary", "bookmark", "summary"] as const;
22
- export type GeneralGroup = (typeof GENERAL_GROUPS)[number];
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 const EXTERNAL_ID_PREFIX = "ext:";
30
+ export {
31
+ GENERAL_GROUPS,
32
+ EXTERNAL_ID_PREFIX,
33
+ parseExternalDocId,
34
+ externalDocId,
35
+ type GeneralGroup,
36
+ };
25
37
 
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
- }
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
- export function externalDocId(group: GeneralGroup, externalId: string): string {
38
- return `${EXTERNAL_ID_PREFIX}${group}:${externalId}`;
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
- const GENERAL_GROUP_TABLES = {
36
- legal: "legalRegulation",
37
- compliance: "industryCompliance",
38
- dictionary: "dictionaryTerm",
39
- bookmark: "websiteBookmark",
40
- } as const satisfies Record<Exclude<GeneralGroup, "summary">, string>;
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
- const table = GENERAL_GROUP_TABLES[parsed.group as Exclude<GeneralGroup, "summary">];
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: SearchDocRow[] = [];
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
- let projectId = data.projectId;
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 { ensureBuiltinIndependentProject } from "@/server/builtin-project";
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
- let projectId = data.projectId;
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
- let projectId = data.projectId;
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
- let projectId = data.projectId;
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: [] };