@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,67 @@
1
+ import { describe, expect, it, beforeEach, vi } from "vitest"
2
+ import type { CLIAgentInfo } from "@allbluecn/kernel"
3
+
4
+ const execFileSyncMock = vi.hoisted(() => vi.fn())
5
+ const execSyncMock = vi.hoisted(() => vi.fn())
6
+
7
+ vi.mock("node:child_process", async (importOriginal) => {
8
+ const actual = await importOriginal<typeof import("node:child_process")>()
9
+ return {
10
+ ...actual,
11
+ execFileSync: execFileSyncMock,
12
+ execSync: execSyncMock,
13
+ }
14
+ })
15
+
16
+ vi.mock("child_process", async (importOriginal) => {
17
+ const actual = await importOriginal<typeof import("child_process")>()
18
+ return {
19
+ ...actual,
20
+ execFileSync: execFileSyncMock,
21
+ execSync: execSyncMock,
22
+ }
23
+ })
24
+
25
+ const testAgent: CLIAgentInfo = {
26
+ id: "claude-code",
27
+ name: "Claude Code",
28
+ binary: "claude",
29
+ version: "v1",
30
+ available: true,
31
+ }
32
+
33
+ describe("executeViaAgent 命令执行安全", () => {
34
+ beforeEach(() => {
35
+ execFileSyncMock.mockReset()
36
+ execSyncMock.mockReset()
37
+ vi.resetModules()
38
+ })
39
+
40
+ it("用 execFileSync 数组参数调用 CLI(无 shell 拼接)", async () => {
41
+ execFileSyncMock.mockReturnValue(" hello response \n")
42
+ const { executeViaAgent } = await import("../adapter")
43
+ const result = await executeViaAgent(testAgent, "你好", {} as never)
44
+ expect(result).toBe("hello response")
45
+ expect(execFileSyncMock).toHaveBeenCalledWith(
46
+ "claude",
47
+ ["-p", "你好"],
48
+ expect.objectContaining({ timeout: 60000, encoding: "utf-8" })
49
+ )
50
+ })
51
+
52
+ it("execSync 未被调用(锁定无 shell 注入不变量)", async () => {
53
+ execFileSyncMock.mockReturnValue("ok")
54
+ const { executeViaAgent } = await import("../adapter")
55
+ await executeViaAgent(testAgent, "test", {} as never)
56
+ expect(execSyncMock).not.toHaveBeenCalled()
57
+ })
58
+
59
+ it("prompt 中的 shell 元字符作为字面量传递(防注入回归)", async () => {
60
+ const dangerous = `"; rm -rf /; echo $(whoami) \`id\`" && whoami`
61
+ execFileSyncMock.mockReturnValue("safe")
62
+ const { executeViaAgent } = await import("../adapter")
63
+ await executeViaAgent(testAgent, dangerous, {} as never)
64
+ expect(execFileSyncMock.mock.calls[0]![1]).toEqual(["-p", dangerous])
65
+ expect(execSyncMock).not.toHaveBeenCalled()
66
+ })
67
+ })
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest'
2
+
3
+ const execFileSyncMock = vi.hoisted(() => vi.fn())
4
+ const execSyncMock = vi.hoisted(() => vi.fn())
5
+
6
+ vi.mock('child_process', async (importOriginal) => {
7
+ const actual = await importOriginal<typeof import('child_process')>()
8
+ return {
9
+ ...actual,
10
+ execFileSync: execFileSyncMock,
11
+ execSync: execSyncMock,
12
+ }
13
+ })
14
+
15
+ import { detectAgents, getApiAgent } from '../cli-detector'
16
+
17
+ describe('getApiAgent', () => {
18
+ beforeEach(() => {
19
+ vi.resetModules()
20
+ delete process.env.OPENAI_API_KEY
21
+ delete process.env.AI_MODEL
22
+ })
23
+
24
+ it('未设置 API key 时返回 undefined', async () => {
25
+ const { getApiAgent: fn } = await import('../cli-detector')
26
+ expect(fn()).toBeUndefined()
27
+ })
28
+
29
+ it('API key 为占位符时返回 undefined', async () => {
30
+ process.env.OPENAI_API_KEY = 'sk-your-api-key-here'
31
+ const { getApiAgent: fn } = await import('../cli-detector')
32
+ expect(fn()).toBeUndefined()
33
+ })
34
+
35
+ it('API key 有效时返回 agent 信息', async () => {
36
+ process.env.OPENAI_API_KEY = 'sk-real-key'
37
+ const { getApiAgent: fn } = await import('../cli-detector')
38
+ const agent = fn()
39
+ expect(agent).toBeDefined()
40
+ expect(agent?.id).toBe('openai-api')
41
+ expect(agent?.available).toBe(true)
42
+ })
43
+
44
+ it('使用 AI_MODEL 环境变量显示模型名', async () => {
45
+ process.env.OPENAI_API_KEY = 'sk-real-key'
46
+ process.env.AI_MODEL = 'gpt-4'
47
+ const { getApiAgent: fn } = await import('../cli-detector')
48
+ expect(fn()?.name).toContain('gpt-4')
49
+ })
50
+ })
51
+
52
+ describe('detectAgents 安全探测', () => {
53
+ beforeEach(() => {
54
+ execFileSyncMock.mockReset()
55
+ execSyncMock.mockReset()
56
+ })
57
+
58
+ it('用 execFileSync 数组参数探测版本(无 shell 拼接)', async () => {
59
+ execFileSyncMock.mockReturnValueOnce('v1.2.3')
60
+ const agents = await detectAgents()
61
+ const claude = agents.find((a) => a.id === 'claude-code')!
62
+ expect(claude.available).toBe(true)
63
+ expect(claude.version).toBe('v1.2.3')
64
+ expect(execFileSyncMock).toHaveBeenCalledWith(
65
+ expect.any(String),
66
+ expect.arrayContaining(['--version']),
67
+ expect.objectContaining({ timeout: expect.any(Number), encoding: 'utf-8' })
68
+ )
69
+ expect(execSyncMock).not.toHaveBeenCalled()
70
+ })
71
+
72
+ it('--version 失败时回退 version 子命令(两步探测,无 shell ||)', async () => {
73
+ execFileSyncMock
74
+ .mockImplementationOnce(() => { throw new Error('not found') })
75
+ .mockReturnValueOnce('2.0.0')
76
+ const agents = await detectAgents()
77
+ const first = agents[0]!
78
+ expect(first.available).toBe(true)
79
+ expect(first.version).toBe('2.0.0')
80
+ const calls = execFileSyncMock.mock.calls
81
+ expect(calls[0]?.[1]).toEqual(['--version'])
82
+ expect(calls[1]?.[1]).toEqual(['version'])
83
+ expect(calls[0]![0]).toBe(calls[1]![0])
84
+ })
85
+
86
+ it('两步都失败标记不可用且不抛错', async () => {
87
+ execFileSyncMock.mockImplementation(() => { throw new Error('not found') })
88
+ const agents = await detectAgents()
89
+ expect(agents).toHaveLength(16)
90
+ expect(agents.every((a) => a.available === false)).toBe(true)
91
+ expect(agents.every((a) => a.version === '')).toBe(true)
92
+ })
93
+
94
+ it('版本串截断到 50 字符且去除首尾空白', async () => {
95
+ execFileSyncMock.mockReturnValue(` ${'x'.repeat(80)} \n`)
96
+ const agents = await detectAgents()
97
+ expect(agents[0]!.version).toBe('x'.repeat(50))
98
+ })
99
+ })
@@ -50,8 +50,8 @@ export async function executeViaAgent(
50
50
  return data.choices[0]?.message?.content || ""
51
51
  }
52
52
 
53
- const { execSync } = await import("child_process")
54
- const result = execSync(`${agent.binary} -p "${prompt.replace(/"/g, '\\"')}"`, {
53
+ const { execFileSync } = await import("child_process")
54
+ const result = execFileSync(agent.binary, ["-p", prompt], {
55
55
  timeout: 60000,
56
56
  encoding: "utf-8",
57
57
  })
@@ -36,18 +36,27 @@ const KNOWN_AGENTS: { id: string; name: string; binary: string }[] = [
36
36
  { id: "mcp-cli", name: "MCP CLI", binary: "mcp" },
37
37
  ]
38
38
 
39
- export async function detectAgents(): Promise<CLIAgentInfo[]> {
40
- const { execSync } = await import("child_process")
41
- return KNOWN_AGENTS.map(({ id, name, binary }) => {
39
+ function probeVersion(
40
+ execFileSync: (file: string, args: string[], options: { timeout: number; encoding: "utf-8" }) => string,
41
+ binary: string,
42
+ ): string {
43
+ const opts = { timeout: 3000, encoding: "utf-8" } as const
44
+ try {
45
+ return execFileSync(binary, ["--version"], opts).trim().slice(0, 50)
46
+ } catch {
42
47
  try {
43
- const version = execSync(`${binary} --version 2>/dev/null || ${binary} version 2>/dev/null`, {
44
- timeout: 3000,
45
- encoding: "utf-8",
46
- }).trim().slice(0, 50)
47
- return { id, name, binary, version, available: true }
48
+ return execFileSync(binary, ["version"], opts).trim().slice(0, 50)
48
49
  } catch {
49
- return { id, name, binary, version: "", available: false }
50
+ return ""
50
51
  }
52
+ }
53
+ }
54
+
55
+ export async function detectAgents(): Promise<CLIAgentInfo[]> {
56
+ const { execFileSync } = await import("child_process")
57
+ return KNOWN_AGENTS.map(({ id, name, binary }) => {
58
+ const version = probeVersion(execFileSync, binary)
59
+ return { id, name, binary, version, available: version !== "" }
51
60
  })
52
61
  }
53
62
 
package/src/start.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { createStart, createCsrfMiddleware, createMiddleware } from '@tanstack/react-start'
19
+ import { securityHeadersMiddleware } from '@/server/middlewares/security-headers'
19
20
  import { requestContextMiddleware } from '@/server/middlewares/request'
20
21
  import { auditRequestMiddleware } from '@/server/middlewares/audit'
21
22
  import { neonKeepaliveMiddleware } from '@/server/keepalive/middleware'
@@ -97,6 +98,6 @@ export const startInstance = createStart(async () => {
97
98
  } as any)
98
99
  }
99
100
  return {
100
- requestMiddleware: [requestContextMiddleware, neonKeepaliveMiddleware, aiPortsMiddleware, apiCorsMiddleware, csrfMiddleware, auditRequestMiddleware],
101
+ requestMiddleware: [securityHeadersMiddleware, requestContextMiddleware, neonKeepaliveMiddleware, aiPortsMiddleware, apiCorsMiddleware, csrfMiddleware, auditRequestMiddleware],
101
102
  }
102
103
  })
@@ -247,6 +247,120 @@
247
247
  --color-5: oklch(90.7% 0.231 133);
248
248
  }
249
249
 
250
+ /* 知识库快捷文档标签莫兰迪色板(LABEL_COLORS 消费,light 原色 / dark 从种子色派生) */
251
+ :root {
252
+ --kb-row-mix: 45%;
253
+ --kb-seed-mrd: #9CAF97;
254
+ --kb-label-mrd-bg: #E8EFE5;
255
+ --kb-label-mrd-border: #9CAF97;
256
+ --kb-label-mrd-text: #5C705A;
257
+ --kb-seed-brd: #9FB2C4;
258
+ --kb-label-brd-bg: #E5EBF1;
259
+ --kb-label-brd-border: #9FB2C4;
260
+ --kb-label-brd-text: #54687A;
261
+ --kb-seed-feasibility: #C4B896;
262
+ --kb-label-feasibility-bg: #F1EDDF;
263
+ --kb-label-feasibility-border: #C4B896;
264
+ --kb-label-feasibility-text: #7A6E4C;
265
+ --kb-seed-competitor: #C4A48E;
266
+ --kb-label-competitor-bg: #F1E8E2;
267
+ --kb-label-competitor-border: #C4A48E;
268
+ --kb-label-competitor-text: #7A5F4C;
269
+ --kb-seed-charter: #B0A0B8;
270
+ --kb-label-charter-bg: #EDE7F0;
271
+ --kb-label-charter-border: #B0A0B8;
272
+ --kb-label-charter-text: #6A5A72;
273
+ --kb-seed-infoArchitecture: #96B8B0;
274
+ --kb-label-infoArchitecture-bg: #E2EFEC;
275
+ --kb-label-infoArchitecture-border: #96B8B0;
276
+ --kb-label-infoArchitecture-text: #4E6E68;
277
+ --kb-seed-techArchitecture: #8FA3AD;
278
+ --kb-label-techArchitecture-bg: #E1E8EB;
279
+ --kb-label-techArchitecture-border: #8FA3AD;
280
+ --kb-label-techArchitecture-text: #4E626C;
281
+ --kb-seed-dataStructure: #C09898;
282
+ --kb-label-dataStructure-bg: #F0E4E4;
283
+ --kb-label-dataStructure-border: #C09898;
284
+ --kb-label-dataStructure-text: #755050;
285
+ --kb-seed-integration: #B3A48E;
286
+ --kb-label-integration-bg: #EEE9E0;
287
+ --kb-label-integration-border: #B3A48E;
288
+ --kb-label-integration-text: #6E6046;
289
+ --kb-seed-copywriting: #C4A0A8;
290
+ --kb-label-copywriting-bg: #F2E7E9;
291
+ --kb-label-copywriting-border: #C4A0A8;
292
+ --kb-label-copywriting-text: #77545C;
293
+ --kb-seed-dictionary: #A8A88E;
294
+ --kb-label-dictionary-bg: #EBEBE0;
295
+ --kb-label-dictionary-border: #A8A88E;
296
+ --kb-label-dictionary-text: #68684C;
297
+ --kb-seed-legal: #8FA8B3;
298
+ --kb-label-legal-bg: #DCE8EC;
299
+ --kb-label-legal-border: #8FA8B3;
300
+ --kb-label-legal-text: #4C626C;
301
+ --kb-seed-compliance: #9FA88C;
302
+ --kb-label-compliance-bg: #E4E8DC;
303
+ --kb-label-compliance-border: #9FA88C;
304
+ --kb-label-compliance-text: #5E664E;
305
+ --kb-seed-kbDictionary: #B09A8C;
306
+ --kb-label-kbDictionary-bg: #EAE4DC;
307
+ --kb-label-kbDictionary-border: #B09A8C;
308
+ --kb-label-kbDictionary-text: #6E5A4C;
309
+ --kb-seed-website: #98A4B8;
310
+ --kb-label-website-bg: #E0E4EA;
311
+ --kb-label-website-border: #98A4B8;
312
+ --kb-label-website-text: #545E70;
313
+ }
314
+
315
+ .dark {
316
+ --kb-row-mix: 65%;
317
+ --kb-label-mrd-bg: color-mix(in srgb, var(--kb-seed-mrd) 20%, #1d1f1c);
318
+ --kb-label-mrd-border: color-mix(in srgb, var(--kb-seed-mrd) 45%, #1d1f1c);
319
+ --kb-label-mrd-text: color-mix(in srgb, var(--kb-seed-mrd) 60%, #dfe3dc);
320
+ --kb-label-brd-bg: color-mix(in srgb, var(--kb-seed-brd) 20%, #1c1e21);
321
+ --kb-label-brd-border: color-mix(in srgb, var(--kb-seed-brd) 45%, #1c1e21);
322
+ --kb-label-brd-text: color-mix(in srgb, var(--kb-seed-brd) 60%, #dce1e7);
323
+ --kb-label-feasibility-bg: color-mix(in srgb, var(--kb-seed-feasibility) 20%, #1f1d16);
324
+ --kb-label-feasibility-border: color-mix(in srgb, var(--kb-seed-feasibility) 45%, #1f1d16);
325
+ --kb-label-feasibility-text: color-mix(in srgb, var(--kb-seed-feasibility) 60%, #e6e2d5);
326
+ --kb-label-competitor-bg: color-mix(in srgb, var(--kb-seed-competitor) 20%, #1f1b18);
327
+ --kb-label-competitor-border: color-mix(in srgb, var(--kb-seed-competitor) 45%, #1f1b18);
328
+ --kb-label-competitor-text: color-mix(in srgb, var(--kb-seed-competitor) 60%, #e6ddd6);
329
+ --kb-label-charter-bg: color-mix(in srgb, var(--kb-seed-charter) 20%, #1d1b1f);
330
+ --kb-label-charter-border: color-mix(in srgb, var(--kb-seed-charter) 45%, #1d1b1f);
331
+ --kb-label-charter-text: color-mix(in srgb, var(--kb-seed-charter) 60%, #e2dde6);
332
+ --kb-label-infoArchitecture-bg: color-mix(in srgb, var(--kb-seed-infoArchitecture) 20%, #1b1e1d);
333
+ --kb-label-infoArchitecture-border: color-mix(in srgb, var(--kb-seed-infoArchitecture) 45%, #1b1e1d);
334
+ --kb-label-infoArchitecture-text: color-mix(in srgb, var(--kb-seed-infoArchitecture) 60%, #dbe4e1);
335
+ --kb-label-techArchitecture-bg: color-mix(in srgb, var(--kb-seed-techArchitecture) 20%, #1b1d1f);
336
+ --kb-label-techArchitecture-border: color-mix(in srgb, var(--kb-seed-techArchitecture) 45%, #1b1d1f);
337
+ --kb-label-techArchitecture-text: color-mix(in srgb, var(--kb-seed-techArchitecture) 60%, #dbe1e5);
338
+ --kb-label-dataStructure-bg: color-mix(in srgb, var(--kb-seed-dataStructure) 20%, #1f1b1b);
339
+ --kb-label-dataStructure-border: color-mix(in srgb, var(--kb-seed-dataStructure) 45%, #1f1b1b);
340
+ --kb-label-dataStructure-text: color-mix(in srgb, var(--kb-seed-dataStructure) 60%, #e6dcdc);
341
+ --kb-label-integration-bg: color-mix(in srgb, var(--kb-seed-integration) 20%, #1e1c18);
342
+ --kb-label-integration-border: color-mix(in srgb, var(--kb-seed-integration) 45%, #1e1c18);
343
+ --kb-label-integration-text: color-mix(in srgb, var(--kb-seed-integration) 60%, #e3ded4);
344
+ --kb-label-copywriting-bg: color-mix(in srgb, var(--kb-seed-copywriting) 20%, #1f1b1c);
345
+ --kb-label-copywriting-border: color-mix(in srgb, var(--kb-seed-copywriting) 45%, #1f1b1c);
346
+ --kb-label-copywriting-text: color-mix(in srgb, var(--kb-seed-copywriting) 60%, #e6dce0);
347
+ --kb-label-dictionary-bg: color-mix(in srgb, var(--kb-seed-dictionary) 20%, #1d1d19);
348
+ --kb-label-dictionary-border: color-mix(in srgb, var(--kb-seed-dictionary) 45%, #1d1d19);
349
+ --kb-label-dictionary-text: color-mix(in srgb, var(--kb-seed-dictionary) 60%, #e2e2da);
350
+ --kb-label-legal-bg: color-mix(in srgb, var(--kb-seed-legal) 20%, #1a1e20);
351
+ --kb-label-legal-border: color-mix(in srgb, var(--kb-seed-legal) 45%, #1a1e20);
352
+ --kb-label-legal-text: color-mix(in srgb, var(--kb-seed-legal) 60%, #d9e2e7);
353
+ --kb-label-compliance-bg: color-mix(in srgb, var(--kb-seed-compliance) 20%, #1c1e1a);
354
+ --kb-label-compliance-border: color-mix(in srgb, var(--kb-seed-compliance) 45%, #1c1e1a);
355
+ --kb-label-compliance-text: color-mix(in srgb, var(--kb-seed-compliance) 60%, #dee2d6);
356
+ --kb-label-kbDictionary-bg: color-mix(in srgb, var(--kb-seed-kbDictionary) 20%, #1e1c1a);
357
+ --kb-label-kbDictionary-border: color-mix(in srgb, var(--kb-seed-kbDictionary) 45%, #1e1c1a);
358
+ --kb-label-kbDictionary-text: color-mix(in srgb, var(--kb-seed-kbDictionary) 60%, #e3ddd6);
359
+ --kb-label-website-bg: color-mix(in srgb, var(--kb-seed-website) 20%, #1b1d21);
360
+ --kb-label-website-border: color-mix(in srgb, var(--kb-seed-website) 45%, #1b1d21);
361
+ --kb-label-website-text: color-mix(in srgb, var(--kb-seed-website) 60%, #dbe0e8);
362
+ }
363
+
250
364
  @layer base {
251
365
  * {
252
366
  border-color: var(--color-border);
@@ -15,17 +15,18 @@
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
- // Vitest 4 类型兼容层:@testing-library/jest-dom 的 vitest.d.ts 增强 `module 'vitest'` 的
19
- // Assertion 接口,但 Vitest 4 的 `vitest` 模块仅 re-export `@vitest/expect` 的
20
- // `Assertion`(dist/index.d.ts:15),接口定义在 `@vitest/expect` 包内,跨包接口合并不生效。
21
- // 这里显式对 `@vitest/expect` 的 `Assertion` / `AsymmetricMatchersContaining` 做增强,
18
+ // Vitest 5 类型兼容层:Vitest 5 将 expect 断言内联进主包(不再依赖 @vitest/expect),
19
+ // `declare module 'vitest'` 的接口增强直接生效。但 Vitest 5 的 `Assertion` 接口
20
+ // 使用双类型参数 `Assertion<R, T>`(R = matcher 返回类型,T = received 值类型),
21
+ // 而 @testing-library/jest-dom 官方 types/vitest.d.ts 仍声明单参数 `Assertion<T = any>`,
22
+ // 泛型参数个数不一致导致声明合并报 TS2428。这里以正确的双参数签名重写增强,
22
23
  // 使 `toBeInTheDocument` / `toBeDisabled` 等 jest-dom matcher 类型可用。
23
24
  // 运行时 matcher 注册在 src/__tests__/setup.ts 的 `import '@testing-library/jest-dom/vitest'`。
24
25
  import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers'
25
26
 
26
- declare module '@vitest/expect' {
27
- interface Assertion<T = any>
28
- extends TestingLibraryMatchers<any, T> {}
27
+ declare module 'vitest' {
28
+ interface Assertion<R extends void | Promise<void> = void, T = unknown>
29
+ extends TestingLibraryMatchers<R, T> {}
29
30
  interface AsymmetricMatchersContaining
30
31
  extends TestingLibraryMatchers<any, any> {}
31
32
  }
package/vite.shared.ts CHANGED
@@ -30,7 +30,7 @@
30
30
 
31
31
  import { defineConfig, type PluginOption } from 'vite'
32
32
  import path from 'node:path'
33
- import { readFileSync } from 'node:fs'
33
+ import { existsSync, readFileSync } from 'node:fs'
34
34
  import { execFileSync } from 'node:child_process'
35
35
 
36
36
  // 从 apps/web/package.json 读取版本号,构建时注入到客户端/SSR
@@ -80,6 +80,90 @@ function prismaDirnamePolyfill(rootDir: string): PluginOption {
80
80
  }
81
81
  }
82
82
 
83
+ function cssTreeRuntimeAssetsPlugin(rootDir: string): PluginOption {
84
+ return {
85
+ name: 'css-tree-runtime-assets',
86
+ async closeBundle() {
87
+ // nitro 把 css-tree / jsdom(isomorphic-dompurify 依赖)外部化打平为
88
+ // _libs/*.mjs,其内部运行时 require 的资源文件不随 trace 拷贝 ——
89
+ // closeBundle 后按产物的解析路径补齐,否则冷启动 ENOENT 全站 500。
90
+ const { mkdir, readdir, copyFile } = await import('node:fs/promises')
91
+ const funcDir = path.resolve(rootDir, '.vercel/output/functions/__server.func')
92
+ const pnpmDir = path.resolve(rootDir, '../../node_modules/.pnpm')
93
+ try {
94
+ // a) jsdom 的 xhr-sync-worker.js(运行时相对 require)
95
+ const isoVdirs = (await readdir(pnpmDir)).filter((d) => d.startsWith('isomorphic-dompurify@'))
96
+ for (const v of isoVdirs) {
97
+ const worker = path.join(pnpmDir, v, 'node_modules/jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js')
98
+ if (existsSync(worker)) {
99
+ await mkdir(path.join(funcDir, '_libs'), { recursive: true })
100
+ await copyFile(worker, path.join(funcDir, '_libs/xhr-sync-worker.js'))
101
+ }
102
+ }
103
+ // a2) jsdom XHR 的 syncWorker resolve 在 bundle 后路径失真(真实 worker 文件
104
+ // 已拷贝到 _libs 根,仍解析失败则降级空串——服务端 syncWorker 走 null 分支)
105
+ const isoLib = path.join(funcDir, '_libs/isomorphic-dompurify.mjs')
106
+ {
107
+ const { readFile, writeFile } = await import('node:fs/promises')
108
+ let code = await readFile(isoLib, 'utf8')
109
+ const target = '__require.resolve("./xhr-sync-worker.js")'
110
+ if (code.includes(target)) {
111
+ code = code.replaceAll(
112
+ target,
113
+ '(()=>{try{return __require.resolve("./xhr-sync-worker.js")}catch{return ""}})()',
114
+ )
115
+ await writeFile(isoLib, code)
116
+ }
117
+ }
118
+ // b) css-tree data/patch.json(dom-selector 内 createRequire('../data/patch.json'))
119
+ // css-tree 是 transitive 包,从 .pnpm 虚拟目录扫描定位
120
+ const ctVdirs = (await readdir(pnpmDir)).filter((d) => d.startsWith('css-tree@'))
121
+ let cssTreePkg: string | null = null
122
+ for (const v of ctVdirs) {
123
+ const cand = path.join(pnpmDir, v, 'node_modules/css-tree')
124
+ if (existsSync(path.join(cand, 'data/patch.json'))) {
125
+ cssTreePkg = cand
126
+ break
127
+ }
128
+ }
129
+ if (!cssTreePkg) throw new Error('css-tree data/patch.json 未找到')
130
+ const patchDestDir = path.join(funcDir, '_libs/data')
131
+ await mkdir(patchDestDir, { recursive: true })
132
+ await copyFile(path.join(cssTreePkg, 'data/patch.json'), path.join(patchDestDir, 'patch.json'))
133
+ // c) mdn-data css json(require('mdn-data/css/*.json') 向上解析 node_modules)
134
+ const mdnCssDir = path.join(path.dirname(cssTreePkg), 'mdn-data/css')
135
+ const dest = path.join(funcDir, 'node_modules/mdn-data/css')
136
+ await mkdir(dest, { recursive: true })
137
+ for (const f of await readdir(mdnCssDir)) {
138
+ if (f.endsWith('.json')) await copyFile(path.join(mdnCssDir, f), path.join(dest, f))
139
+ }
140
+ } catch (e) {
141
+ console.error('[css-tree-runtime-assets] 资源补齐失败:', e instanceof Error ? e.message : e)
142
+ }
143
+ },
144
+ }
145
+ }
146
+
147
+ function runtimeResourceInlinePlugin(): PluginOption {
148
+ return {
149
+ name: 'runtime-resource-inline',
150
+ transform(code, id) {
151
+ // jsdom 模块顶层同步读取默认样式表,nitro 打平 bundle 后
152
+ // __dirname 相对路径失效,冷启动 ENOENT 全站 500 → 构建期内联
153
+ if (id.includes('jsdom') && id.includes('computed-style.js') && code.includes('default-stylesheet.css')) {
154
+ const cssPath = path.resolve(path.dirname(id), '../../../browser/default-stylesheet.css')
155
+ const css = readFileSync(cssPath, 'utf8')
156
+ const replaced = code.replace(
157
+ /fs\.readFileSync\(\s*path\.resolve\(__dirname,\s*"\.\.\/\.\.\/\.\.\/browser\/default-stylesheet\.css"\),\s*\{\s*encoding:\s*"utf-8"\s*\}\s*\)/,
158
+ JSON.stringify(css),
159
+ )
160
+ if (replaced !== code) return { code: replaced, map: null }
161
+ }
162
+ return undefined
163
+ },
164
+ }
165
+ }
166
+
83
167
  function tiptapAliasPlugin(rootDir: string): PluginOption {
84
168
  const tiptapSrc = path.resolve(rootDir, '../../packages/tiptap/src')
85
169
  return {
@@ -108,7 +192,7 @@ export function createWebViteConfig(opts: WebViteConfigOptions) {
108
192
  const appUpdateDate = getAppUpdateDate(opts.rootDir)
109
193
  return defineConfig(async () => {
110
194
  const deployTarget = process.env.DEPLOY_TARGET
111
- const plugins: PluginOption[] = [...opts.basePlugins, tiptapAliasPlugin(opts.rootDir)]
195
+ const plugins: PluginOption[] = [...opts.basePlugins, tiptapAliasPlugin(opts.rootDir), runtimeResourceInlinePlugin()]
112
196
 
113
197
  // Vercel 平台自动注入 VERCEL_ENV 环境变量,nitro 插件始终挂载。DEPLOY_TARGET=vercel
114
198
  // 保留作为本地兼容触发器(Pro 仓 build.sh 及本地 vercel preview 均依赖它)。
@@ -116,7 +200,7 @@ export function createWebViteConfig(opts: WebViteConfigOptions) {
116
200
  if (isVercelTarget) {
117
201
  const { nitro } = await import('nitro/vite')
118
202
  plugins.splice(2, 0, nitro({ preset: 'vercel' }))
119
- plugins.push(prismaDirnamePolyfill(opts.rootDir))
203
+ plugins.push(prismaDirnamePolyfill(opts.rootDir), cssTreeRuntimeAssetsPlugin(opts.rootDir))
120
204
  }
121
205
 
122
206
  if (deployTarget === 'cloudflare') {
@@ -139,8 +223,25 @@ export function createWebViteConfig(opts: WebViteConfigOptions) {
139
223
  },
140
224
  server: {
141
225
  port: 3000,
142
- allowedHosts: ['192.168.106.16', '192.168.106.223', 'localhost'],
226
+ host: true,
227
+ // host.docker.internal:容器内安全扫描(ZAP/Nuclei,见
228
+ // scripts/security/)经该地址访问宿主 dev server;Vite 默认拒绝
229
+ // 非白名单 Host 头(返回 403 "Blocked request")。仅 dev server 生效。
230
+ allowedHosts: ['localhost', 'host.docker.internal'],
143
231
  cors: { origin: false },
232
+ // Vite 内部中间件(/src/*、/@fs/*、.map)的响应绕过 TanStack Start
233
+ // requestMiddleware,故在此层补基础安全头;CSP/HSTS 仍由 middleware
234
+ // 统一注入(apps/web/src/server/middlewares/security-headers.ts),
235
+ // 生产构建无 Vite dev server,middleware 即唯一来源。
236
+ headers: {
237
+ 'X-Content-Type-Options': 'nosniff',
238
+ 'X-Frame-Options': 'DENY',
239
+ 'Cross-Origin-Resource-Policy': 'same-origin',
240
+ 'Cross-Origin-Opener-Policy': 'same-origin',
241
+ 'Permissions-Policy':
242
+ 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), fullscreen=(self)',
243
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
244
+ },
144
245
  },
145
246
  plugins,
146
247
  resolve: {
package/public/file.svg DELETED
@@ -1 +0,0 @@
1
- <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
package/public/globe.svg DELETED
@@ -1 +0,0 @@
1
- <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
package/public/window.svg DELETED
@@ -1 +0,0 @@
1
- <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -1,80 +0,0 @@
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
- import { describe, it, expect, vi, beforeEach } from 'vitest'
19
-
20
- describe('getApiAgent', () => {
21
- beforeEach(() => {
22
- vi.resetModules()
23
- delete process.env.OPENAI_API_KEY
24
- delete process.env.AI_MODEL
25
- })
26
-
27
- it('returns undefined when no API key set', async () => {
28
- const { getApiAgent } = await import('@/shared/ai-cli/cli-detector')
29
- expect(getApiAgent()).toBeUndefined()
30
- })
31
-
32
- it('returns undefined when API key is placeholder', async () => {
33
- process.env.OPENAI_API_KEY = 'sk-your-api-key-here'
34
- const { getApiAgent } = await import('@/shared/ai-cli/cli-detector')
35
- expect(getApiAgent()).toBeUndefined()
36
- })
37
-
38
- it('returns agent info when API key is valid', async () => {
39
- process.env.OPENAI_API_KEY = 'sk-real-key'
40
- const { getApiAgent } = await import('@/shared/ai-cli/cli-detector')
41
- const agent = getApiAgent()
42
- expect(agent).toBeDefined()
43
- expect(agent?.id).toBe('openai-api')
44
- expect(agent?.available).toBe(true)
45
- })
46
-
47
- it('uses AI_MODEL env in display name', async () => {
48
- process.env.OPENAI_API_KEY = 'sk-real-key'
49
- process.env.AI_MODEL = 'gpt-4'
50
- const { getApiAgent } = await import('@/shared/ai-cli/cli-detector')
51
- expect(getApiAgent()?.name).toContain('gpt-4')
52
- })
53
- })
54
-
55
- describe('detectAgents', () => {
56
- beforeEach(() => {
57
- vi.resetModules()
58
- })
59
-
60
- it('returns 16 known agents with executable check', async () => {
61
- const { detectAgents } = await import('@/shared/ai-cli/cli-detector')
62
- const agents = await detectAgents()
63
- expect(agents).toHaveLength(16)
64
- agents.forEach(a => {
65
- expect(a.id).toBeDefined()
66
- expect(a.binary).toBeDefined()
67
- })
68
- })
69
-
70
- it('handles all agents failing gracefully', async () => {
71
- vi.doMock('child_process', () => ({
72
- execSync: vi.fn(() => { throw new Error('not found') }),
73
- }))
74
-
75
- const { detectAgents } = await import('@/shared/ai-cli/cli-detector')
76
- const agents = await detectAgents()
77
- expect(agents).toHaveLength(16)
78
- agents.forEach(a => expect(a.available).toBe(false))
79
- })
80
- })