@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.
- package/package.json +239 -30
- package/src/components/iteration/auto-schedule-dialog.tsx +150 -0
- package/src/components/iteration/burndown-chart.tsx +63 -0
- package/src/components/iteration/constants.ts +28 -0
- package/src/components/iteration/create-iteration-dialog.tsx +140 -0
- package/src/components/iteration/iteration-card.tsx +60 -0
- package/src/components/iteration/iteration-editor.tsx +116 -0
- package/src/components/iteration/iteration-progress-bar.tsx +44 -0
- package/src/components/iteration/iteration-status-badge.tsx +33 -0
- package/src/components/knowledge/dialog/knowledge-form-dialog.tsx +21 -30
- package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +2 -2
- package/src/components/layout/sidebar-03/nav-workspace.tsx +2 -2
- package/src/components/project/create-project-dialog.tsx +44 -57
- package/src/components/shared/__tests__/lucide-curated-icons.test.ts +83 -0
- package/src/components/shared/__tests__/lucide-icon-data.test.ts +131 -0
- package/src/components/shared/__tests__/lucide-icon-picker.test.tsx +197 -0
- package/src/components/shared/lucide-icon-picker/curated-icons.ts +478 -0
- package/src/components/shared/lucide-icon-picker/icon-data.ts +109 -0
- package/src/components/shared/lucide-icon-picker/index.tsx +82 -0
- package/src/components/shared/lucide-icon-picker/lucide-icon-by-name.tsx +78 -0
- package/src/components/shared/lucide-icon-picker/panel.tsx +163 -0
- package/src/components/story/__tests__/constants.test.ts +255 -0
- package/src/components/story/ai/story-ai-panel.tsx +23 -15
- package/src/components/story/ai/story-ai-preview-list.tsx +19 -0
- package/src/components/story/constants.ts +43 -1
- package/src/components/story/create-story-dialog.tsx +42 -16
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
- package/src/components/story/detail/__tests__/categories.test.tsx +62 -0
- package/src/components/story/detail/__tests__/dependencies-button.test.tsx +49 -0
- package/src/components/story/detail/activity-timeline.tsx +26 -4
- package/src/components/story/detail/story-detail-body.tsx +90 -38
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +857 -0
- package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
- package/src/components/story/detail/story-detail-toolbar/children.tsx +9 -4
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +95 -27
- package/src/components/story/detail/story-detail-toolbar.tsx +159 -8
- package/src/components/story/detail/story-properties.tsx +21 -4
- package/src/components/story/display/__tests__/group-fields.test.ts +107 -0
- package/src/components/story/display/group-resolver.tsx +2 -2
- package/src/components/story/inline-editors/__tests__/dependency-type-select.test.tsx +155 -0
- package/src/components/story/inline-editors/__tests__/labels-editor.test.tsx +155 -0
- package/src/components/story/inline-editors/__tests__/priority-editor.test.tsx +173 -0
- package/src/components/story/inline-editors/__tests__/state-editor.test.tsx +174 -0
- package/src/components/story/inline-editors/dependency-type-select.tsx +180 -0
- package/src/components/story/inline-editors/index.ts +2 -1
- package/src/components/story/inline-editors/labels-editor.tsx +159 -3
- package/src/components/story/inline-editors/story-select-dialog.tsx +41 -63
- package/src/components/story/peek-panel.tsx +121 -21
- package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
- package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
- package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
- package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
- package/src/components/story/relations/file-preview.tsx +173 -0
- package/src/components/story/relations/format.ts +120 -0
- package/src/components/story/relations/general-groups.ts +32 -0
- package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
- package/src/components/story/relations/link-manage-dialog.tsx +192 -0
- package/src/components/story/relations/story-ref-row.tsx +17 -27
- package/src/components/story/story-panel-header.tsx +25 -19
- package/src/components/story/story-toolbar.tsx +3 -3
- package/src/components/story/transfer-story-popover.tsx +2 -2
- package/src/components/story/types.ts +14 -0
- package/src/components/story/views/create-view-dialog.tsx +3 -3
- package/src/components/story/views/story-list/__tests__/group-header.test.tsx +219 -0
- package/src/components/story/views/story-list/group.tsx +1 -1
- package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
- package/src/components/story/views/story-list/row.tsx +1 -1
- package/src/components/workspace/create-workspace-dialog.tsx +41 -49
- package/src/lib/__tests__/project-display-id.test.ts +8 -5
- package/src/lib/changelog/sdk-versions.ts +20 -20
- package/src/lib/iteration/__tests__/status.test.ts +41 -0
- package/src/lib/iteration/status.ts +28 -0
- package/src/lib/project-display-id.ts +14 -5
- package/src/lib/project-module-meta.ts +77 -0
- package/src/lib/story/__tests__/video-embed.test.ts +77 -0
- package/src/lib/story/video-embed.ts +59 -0
- package/src/lib/utils.d.ts +1 -1
- package/src/plugins/.gen-manifest.json +13 -0
- package/src/plugins/modules.gen.ts +20 -0
- package/src/plugins/plugins.gen.ts +20 -0
- package/src/plugins/ui.gen.ts +8 -0
- package/src/providers/index.tsx +6 -0
- package/src/routeTree.gen.ts +96 -0
- package/src/routes/__root.tsx +0 -8
- package/src/routes/_nav/inspiration/route.tsx +8 -0
- package/src/routes/_nav/knowledge/index.lazy.tsx +2 -2
- package/src/routes/_nav/prd/$id/route.tsx +9 -0
- package/src/routes/_nav/prd/index.tsx +9 -0
- package/src/routes/_nav/prd/route.tsx +8 -0
- package/src/routes/_nav/projects/$projectId/$storyId/route.lazy.tsx +52 -25
- package/src/routes/_nav/projects/$projectId/$storyId/route.tsx +6 -3
- package/src/routes/_nav/projects/$projectId/index.lazy.tsx +32 -15
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +289 -0
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.tsx +27 -0
- package/src/routes/_nav/projects/$projectId/iterations/index.lazy.tsx +111 -0
- package/src/routes/_nav/projects/$projectId/iterations/index.tsx +24 -0
- package/src/routes/_nav/projects/$projectId/route.tsx +4 -2
- package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +78 -7
- package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
- package/src/routes/_nav/stories/$storyId/route.lazy.tsx +5 -1
- package/src/routes/_nav/stories/$storyId/route.tsx +4 -0
- package/src/routes/_nav/tags/$id/route.tsx +8 -0
- package/src/routes/_nav/tags/index.tsx +8 -0
- package/src/routes/_nav/tags/route.tsx +8 -0
- package/src/routes/_nav/tags/settings/route.tsx +9 -0
- package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +3 -3
- package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +4 -4
- package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
- package/src/routes/api/attachments/$id/content.ts +60 -0
- package/src/routes/api/attachments/upload.ts +148 -0
- package/src/routes/share/prd/$prdId/route.tsx +10 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
- package/src/server/serverFns/iteration/__tests__/auto-schedule-core.test.ts +62 -0
- package/src/server/serverFns/iteration/__tests__/burndown-core.test.ts +90 -0
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +211 -0
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +155 -0
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +136 -0
- package/src/server/serverFns/iteration/auto-schedule-core.ts +54 -0
- package/src/server/serverFns/iteration/auto-schedule-internal.ts +88 -0
- package/src/server/serverFns/iteration/burndown-core.ts +60 -0
- package/src/server/serverFns/iteration/index.ts +22 -0
- package/src/server/serverFns/iteration/iteration-auto-schedule.ts +66 -0
- package/src/server/serverFns/iteration/iteration-burndown.ts +70 -0
- package/src/server/serverFns/iteration/iteration-crud.ts +208 -0
- package/src/server/serverFns/iteration/iteration-stories.ts +111 -0
- package/src/server/serverFns/iteration/iteration-transfer.ts +42 -0
- package/src/server/serverFns/iteration/schemas.ts +58 -0
- package/src/server/serverFns/iteration/transfer-internal.ts +62 -0
- package/src/server/serverFns/knowledge.ts +50 -4
- package/src/server/serverFns/project/module-overview.ts +109 -0
- package/src/server/serverFns/project.ts +45 -2
- package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
- package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +100 -0
- package/src/server/serverFns/story/__tests__/labels.test.ts +67 -0
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +132 -2
- package/src/server/serverFns/story/index.ts +9 -4
- package/src/server/serverFns/story/kb-general.ts +39 -0
- package/src/server/serverFns/story/kb-scope.ts +52 -0
- package/src/server/serverFns/story/link-preview.ts +115 -0
- package/src/server/serverFns/story/story-assoc.ts +372 -51
- package/src/server/serverFns/story/story-attachments.ts +32 -45
- package/src/{components/project/project-icon.tsx → server/serverFns/story/story-crud/label-palette.ts} +16 -19
- package/src/server/serverFns/story/story-crud/label-suggest-core.ts +88 -0
- package/src/server/serverFns/story/story-crud/label-suggest.ts +38 -0
- package/src/{components/knowledge/knowledge-icon.tsx → server/serverFns/story/story-crud/label-upsert.ts} +13 -19
- package/src/server/serverFns/story/story-crud/labels.ts +3 -5
- package/src/server/serverFns/story/story-crud.ts +53 -6
- package/src/server/serverFns/story/story-dependencies.ts +158 -5
- package/src/server/serverFns/story/story-links.ts +43 -7
- package/src/server/serverFns/story/story-transfer.ts +5 -4
- package/src/server/serverFns/story/story-tree.ts +28 -1
- package/src/server/serverFns/story/types.ts +38 -0
- package/src/styles/globals.css +25 -0
- package/src/types/project.ts +5 -5
- package/vite.shared.ts +2 -0
- package/src/components/project/project-animated-icon.tsx +0 -73
- package/src/components/shared/icon-packs/index.ts +0 -1
- package/src/components/shared/icon-packs/knowledge.ts +0 -1
- package/src/components/shared/icon-packs/prd.ts +0 -1
- package/src/components/shared/icon-packs/project.ts +0 -1
- package/src/components/shared/icon-picker.tsx +0 -1
- package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
- package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
- package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
- package/src/components/story/relations/attachments-panel.tsx +0 -179
- package/src/components/story/relations/knowledge-panel.tsx +0 -284
- package/src/components/story/relations/links-panel.tsx +0 -240
- 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
|
-
|
|
111
|
+
listStoryAvailableKbsFn,
|
|
112
|
+
searchKbDocsFn,
|
|
113
|
+
linkDocsFn,
|
|
109
114
|
unlinkDocFn,
|
|
110
115
|
getLinkedDocsFn,
|
|
111
|
-
|
|
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
|
+
});
|