@allbluecn/web-app 0.5.0 → 0.6.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.
Files changed (170) hide show
  1. package/package.json +239 -30
  2. package/src/components/iteration/auto-schedule-dialog.tsx +150 -0
  3. package/src/components/iteration/burndown-chart.tsx +63 -0
  4. package/src/components/iteration/constants.ts +28 -0
  5. package/src/components/iteration/create-iteration-dialog.tsx +140 -0
  6. package/src/components/iteration/iteration-card.tsx +60 -0
  7. package/src/components/iteration/iteration-editor.tsx +116 -0
  8. package/src/components/iteration/iteration-progress-bar.tsx +44 -0
  9. package/src/components/iteration/iteration-status-badge.tsx +33 -0
  10. package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +21 -30
  11. package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
  12. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
  13. package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
  14. package/src/components/project/create-project-dialog.tsx +44 -57
  15. package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
  16. package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
  17. package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
  18. package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
  19. package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
  20. package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
  21. package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
  22. package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
  23. package/src/components/story/__tests__/constants.test.ts +255 -0
  24. package/src/components/story/ai/story-ai-panel.tsx +23 -15
  25. package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
  26. package/src/components/story/constants.ts +43 -1
  27. package/src/components/story/create-story-dialog.tsx +42 -16
  28. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
  29. package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
  30. package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
  31. package/src/components/story/detail/activity-timeline.tsx +26 -4
  32. package/src/components/story/detail/story-detail-body.tsx +90 -38
  33. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +857 -0
  34. package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
  35. package/src/components/story/detail/story-detail-toolbar/children.tsx +9 -4
  36. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +95 -27
  37. package/src/components/story/detail/story-detail-toolbar.tsx +159 -8
  38. package/src/components/story/detail/story-properties.tsx +21 -4
  39. package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
  40. package/src/components/story/display/group-resolver.tsx +2 -2
  41. package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
  42. package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
  43. package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
  44. package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
  45. package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
  46. package/src/components/story/inline-editors/index.ts +2 -1
  47. package/src/components/story/inline-editors/labels-editor.tsx +159 -3
  48. package/src/components/story/inline-editors/story-select-dialog.tsx +41 -63
  49. package/src/components/story/peek-panel.tsx +121 -21
  50. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
  51. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
  52. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
  53. package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
  54. package/src/components/story/relations/file-preview.tsx +173 -0
  55. package/src/components/story/relations/format.ts +120 -0
  56. package/src/components/story/relations/general-groups.ts +32 -0
  57. package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
  58. package/src/components/story/relations/link-manage-dialog.tsx +192 -0
  59. package/src/components/story/relations/story-ref-row.tsx +17 -27
  60. package/src/components/story/story-panel-header.tsx +25 -19
  61. package/src/components/story/story-toolbar.tsx +3 -3
  62. package/src/components/story/transfer-story-popover.tsx +2 -2
  63. package/src/components/story/types.ts +14 -0
  64. package/src/components/story/views/create-view-dialog.tsx +3 -3
  65. package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
  66. package/src/components/story/views/story-list/group.tsx +1 -1
  67. package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
  68. package/src/components/story/views/story-list/row.tsx +1 -1
  69. package/src/components/workspace/create-workspace-dialog.tsx +41 -49
  70. package/src/lib/__tests__/project-display-id.test.ts +8 -5
  71. package/src/lib/changelog/sdk-versions.ts +20 -20
  72. package/src/lib/iteration/__tests__/status.test.ts +41 -0
  73. package/src/lib/iteration/status.ts +28 -0
  74. package/src/lib/project-display-id.ts +14 -5
  75. package/src/lib/project-module-meta.ts +77 -0
  76. package/src/lib/story/__tests__/video-embed.test.ts +77 -0
  77. package/src/lib/story/video-embed.ts +59 -0
  78. package/src/lib/utils.d.ts +1 -1
  79. package/src/plugins/.gen-manifest.json +13 -0
  80. package/src/plugins/modules.gen.ts +20 -0
  81. package/src/plugins/plugins.gen.ts +20 -0
  82. package/src/plugins/ui.gen.ts +8 -0
  83. package/src/providers/index.tsx +6 -0
  84. package/src/routeTree.gen.ts +96 -0
  85. package/src/routes/__root.tsx +0 -8
  86. package/src/routes/_nav/inspiration/route.tsx +8 -0
  87. package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
  88. package/src/routes/_nav/prd/$id/route.tsx +9 -0
  89. package/src/routes/_nav/prd/index.tsx +9 -0
  90. package/src/routes/_nav/prd/route.tsx +8 -0
  91. package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
  92. package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
  93. package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
  94. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
  95. package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
  96. package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
  97. package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
  98. package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
  99. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +78 -7
  100. package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
  101. package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
  102. package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
  103. package/src/routes/_nav/tags/$id/route.tsx +8 -0
  104. package/src/routes/_nav/tags/index.tsx +8 -0
  105. package/src/routes/_nav/tags/route.tsx +8 -0
  106. package/src/routes/_nav/tags/settings/route.tsx +9 -0
  107. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
  108. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
  109. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  110. package/src/routes/api/attachments/$id/content.ts +60 -0
  111. package/src/routes/api/attachments/upload.ts +148 -0
  112. package/src/routes/share/prd/$prdId/route.tsx +10 -0
  113. package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
  114. package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
  115. package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
  116. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
  117. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
  118. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
  119. package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
  120. package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
  121. package/src/server/serverFns/iteration/burndown-core.ts +60 -0
  122. package/src/server/serverFns/iteration/index.ts +22 -0
  123. package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
  124. package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
  125. package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
  126. package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
  127. package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
  128. package/src/server/serverFns/iteration/schemas.ts +58 -0
  129. package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
  130. package/src/server/serverFns/knowledge.ts +50 -4
  131. package/src/server/serverFns/project/module-overview.ts +109 -0
  132. package/src/server/serverFns/project.ts +45 -2
  133. package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
  134. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
  135. package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
  136. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
  137. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +132 -2
  138. package/src/server/serverFns/story/index.ts +9 -4
  139. package/src/server/serverFns/story/kb-general.ts +39 -0
  140. package/src/server/serverFns/story/kb-scope.ts +52 -0
  141. package/src/server/serverFns/story/link-preview.ts +115 -0
  142. package/src/server/serverFns/story/story-assoc.ts +372 -51
  143. package/src/server/serverFns/story/story-attachments.ts +32 -45
  144. package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
  145. package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
  146. package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
  147. package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
  148. package/src/server/serverFns/story/story-crud/labels.ts +3 -5
  149. package/src/server/serverFns/story/story-crud.ts +53 -6
  150. package/src/server/serverFns/story/story-dependencies.ts +158 -5
  151. package/src/server/serverFns/story/story-links.ts +43 -7
  152. package/src/server/serverFns/story/story-transfer.ts +5 -4
  153. package/src/server/serverFns/story/story-tree.ts +28 -1
  154. package/src/server/serverFns/story/types.ts +38 -0
  155. package/src/styles/globals.css +25 -0
  156. package/src/types/project.ts +5 -5
  157. package/vite.shared.ts +2 -0
  158. package/src/components/project/project-animated-icon.tsx +0 -73
  159. package/src/components/shared/icon-packs/index.ts +0 -1
  160. package/src/components/shared/icon-packs/knowledge.ts +0 -1
  161. package/src/components/shared/icon-packs/prd.ts +0 -1
  162. package/src/components/shared/icon-packs/project.ts +0 -1
  163. package/src/components/shared/icon-picker.tsx +0 -1
  164. package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
  165. package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
  166. package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
  167. package/src/components/story/relations/attachments-panel.tsx +0 -179
  168. package/src/components/story/relations/knowledge-panel.tsx +0 -284
  169. package/src/components/story/relations/links-panel.tsx +0 -240
  170. package/src/lib/project-mock.ts +0 -156
@@ -21,6 +21,8 @@ export {
21
21
  createLabelFn,
22
22
  } from "./story-crud/labels";
23
23
 
24
+ export { suggestStoryLabelsFn } from "./story-crud/label-suggest";
25
+
24
26
  export {
25
27
  listActivitiesFn,
26
28
  } from "./story-crud/activities";
@@ -82,6 +84,7 @@ export {
82
84
 
83
85
  export {
84
86
  addDependenciesFn,
87
+ updateDependencyFn,
85
88
  removeDependencyFn,
86
89
  getDependenciesFn,
87
90
  } from "./story-dependencies";
@@ -93,11 +96,11 @@ export {
93
96
  getLinksFn,
94
97
  } from "./story-links";
95
98
 
99
+ export { checkLinkEmbeddableFn } from "./link-preview";
100
+
96
101
  export {
97
- uploadAttachmentFn,
98
102
  removeAttachmentFn,
99
103
  getAttachmentsFn,
100
- mockCloudStorageFn,
101
104
  } from "./story-attachments";
102
105
 
103
106
  export {
@@ -105,10 +108,12 @@ export {
105
108
  unlinkPrdFn,
106
109
  getLinkedPrdsFn,
107
110
  searchPrdsFn,
108
- linkDocFn,
111
+ listStoryAvailableKbsFn,
112
+ searchKbDocsFn,
113
+ linkDocsFn,
109
114
  unlinkDocFn,
110
115
  getLinkedDocsFn,
111
- searchDocsFn,
116
+ type SearchDocRow,
112
117
  } from "./story-assoc";
113
118
 
114
119
  export {
@@ -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
+ // 独立成文件以保持 story-assoc.ts(serverFn 模块)纯净:客户端可安全导入。
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
+ }
@@ -0,0 +1,52 @@
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 type { Prisma } from "@allbluecn/database";
19
+ import { BUILTIN_INDEPENDENT_DISPLAY_ID } from "@/lib/builtin-project";
20
+
21
+ export interface KbScopeInput {
22
+ projectId: string;
23
+ projectDisplayId: string | null;
24
+ userId: string;
25
+ }
26
+
27
+ /** 是否内置独立需求项目(displayId=STORY) */
28
+ export function isIndependentProject(displayId: string | null): boolean {
29
+ return displayId === BUILTIN_INDEPENDENT_DISPLAY_ID;
30
+ }
31
+
32
+ /**
33
+ * 需求可选知识库范围(Prisma where,用于 KnowledgeBase 查询):
34
+ * - 普通项目:visibility=general(跨用户)∪ 项目关联 KB(关联仅允许 team)
35
+ * - 独立需求(STORY):自己的全部 ∪ 他人 team/general
36
+ */
37
+ export function buildKbScopeWhere(input: KbScopeInput): Prisma.KnowledgeBaseWhereInput {
38
+ if (isIndependentProject(input.projectDisplayId)) {
39
+ return {
40
+ OR: [
41
+ { userId: input.userId },
42
+ { visibility: { in: ["team", "general"] } },
43
+ ],
44
+ };
45
+ }
46
+ return {
47
+ OR: [
48
+ { visibility: "general" },
49
+ { projectLinks: { some: { projectId: input.projectId } } },
50
+ ],
51
+ };
52
+ }
@@ -0,0 +1,115 @@
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 { createServerFn } from "@tanstack/react-start";
19
+ import { getRequest } from "@tanstack/react-start/server";
20
+ import { z } from "zod";
21
+ import { requireAuth } from "@/server/auth-helpers";
22
+
23
+ const checkSchema = z.object({
24
+ url: z.string().url(),
25
+ origin: z.string().url().optional(),
26
+ });
27
+
28
+ /** SSRF 防护:仅允许公网 http/https 地址(拒绝内网 IP、localhost、本地域名) */
29
+ function isPublicHttpUrl(raw: string): boolean {
30
+ let u: URL;
31
+ try {
32
+ u = new URL(raw);
33
+ } catch {
34
+ return false;
35
+ }
36
+ if (u.protocol !== "http:" && u.protocol !== "https:") return false;
37
+ const host = u.hostname.toLowerCase().replace(/^\[|\]$/g, "");
38
+ if (host === "localhost" || host === "::1" || host === "::" || host.endsWith(".local") || host.endsWith(".internal")) {
39
+ return false;
40
+ }
41
+ const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
42
+ if (ipv4) {
43
+ const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
44
+ if (a === 0 || a === 10 || a === 127 || a >= 224) return false;
45
+ if (a === 169 && b === 254) return false;
46
+ if (a === 172 && b >= 16 && b <= 31) return false;
47
+ if (a === 192 && b === 168) return false;
48
+ }
49
+ return true;
50
+ }
51
+
52
+ /** 解析 CSP frame-ancestors 指令,判断目标页面是否允许被应用嵌入 */
53
+ function isFrameAncestorsAllowed(csp: string, targetUrl: string, appOrigin?: string): boolean {
54
+ const directive = csp
55
+ .split(";")
56
+ .map((d) => d.trim().toLowerCase())
57
+ .find((d) => d.startsWith("frame-ancestors"));
58
+ if (!directive) return true;
59
+ const value = directive.slice("frame-ancestors".length).trim();
60
+ if (!value || value.includes("'none'")) return false;
61
+ if (value.includes("*")) return true;
62
+ if (!appOrigin) return false;
63
+ const sources = value.split(/\s+/);
64
+ try {
65
+ // 目标与链接同源时 'self' 视为允许
66
+ const target = new URL(targetUrl);
67
+ const app = new URL(appOrigin);
68
+ if (target.host === app.host) return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ const appHost = appOrigin.replace(/^https?:\/\//, "").toLowerCase();
73
+ return sources.some((src) => src.replace(/^https?:\/\//, "") === appHost);
74
+ }
75
+
76
+ export const checkLinkEmbeddableFn = createServerFn({ method: "POST" })
77
+ .validator(checkSchema)
78
+ .handler(async ({ data }) => {
79
+ const request = getRequest();
80
+ await requireAuth(request);
81
+
82
+ if (!isPublicHttpUrl(data.url)) {
83
+ return { embeddable: false as const, reason: "invalid" as const };
84
+ }
85
+ try {
86
+ const res = await fetch(data.url, {
87
+ method: "GET",
88
+ redirect: "follow",
89
+ headers: {
90
+ "user-agent": "Mozilla/5.0 (compatible; AllBlueEmbedCheck/1.0)",
91
+ accept: "text/html,application/xhtml+xml,*/*;q=0.8",
92
+ },
93
+ signal: AbortSignal.timeout(8000),
94
+ });
95
+ res.body?.cancel().catch(() => {});
96
+ const contentType = (res.headers.get("content-type") || "").toLowerCase();
97
+ const isHtml = !contentType || contentType.includes("text/html") || contentType.includes("xhtml");
98
+ const isPdf = contentType.includes("application/pdf");
99
+ if (!isHtml && !isPdf) {
100
+ return { embeddable: false as const, reason: "not-html" as const };
101
+ }
102
+ const xfo = (res.headers.get("x-frame-options") || "").toLowerCase();
103
+ if (xfo.includes("deny") || xfo.includes("sameorigin")) {
104
+ return { embeddable: false as const, reason: "blocked" as const };
105
+ }
106
+ const csp = res.headers.get("content-security-policy") || "";
107
+ if (csp && !isFrameAncestorsAllowed(csp, data.url, data.origin)) {
108
+ return { embeddable: false as const, reason: "blocked" as const };
109
+ }
110
+ return { embeddable: true as const };
111
+ } catch {
112
+ // 预检失败(超时/网络异常)不阻塞,交给 iframe 实际尝试
113
+ return { embeddable: true as const };
114
+ }
115
+ });