@allbluecn/web-app 0.5.0 → 0.5.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.
- package/package.json +11 -7
- package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
- package/src/components/story/constants.ts +11 -1
- package/src/components/story/create-story-dialog.tsx +12 -6
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
- package/src/components/story/detail/activity-timeline.tsx +15 -0
- package/src/components/story/detail/story-detail-body.tsx +1 -0
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +748 -0
- package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
- package/src/components/story/detail/story-detail-toolbar/children.tsx +2 -0
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +29 -21
- package/src/components/story/detail/story-detail-toolbar.tsx +154 -8
- package/src/components/story/inline-editors/story-select-dialog.tsx +19 -4
- 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 +191 -0
- package/src/components/story/types.ts +13 -0
- package/src/lib/changelog/sdk-versions.ts +19 -19
- package/src/lib/story/__tests__/video-embed.test.ts +77 -0
- package/src/lib/story/video-embed.ts +59 -0
- 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/routeTree.gen.ts +42 -0
- package/src/routes/_nav/inspiration/route.tsx +8 -0
- 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/settings/route.lazy.tsx +74 -3
- package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
- 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/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/knowledge.ts +50 -4
- package/src/server/serverFns/project.ts +43 -0
- package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +26 -0
- package/src/server/serverFns/story/index.ts +6 -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 +339 -50
- package/src/server/serverFns/story/story-attachments.ts +12 -43
- package/src/server/serverFns/story/story-crud.ts +21 -4
- package/src/server/serverFns/story/story-dependencies.ts +35 -3
- package/src/server/serverFns/story/story-links.ts +13 -1
- package/src/server/serverFns/story/story-tree.ts +28 -1
- package/src/server/serverFns/story/types.ts +37 -0
- package/src/styles/globals.css +25 -0
- package/vite.shared.ts +2 -0
- 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
|
@@ -0,0 +1,191 @@
|
|
|
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 * as React from "react";
|
|
19
|
+
import { useTranslation } from "react-i18next";
|
|
20
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
21
|
+
import { toast } from "sonner";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { Input } from "@/components/ui/input";
|
|
24
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
25
|
+
import {
|
|
26
|
+
Dialog,
|
|
27
|
+
DialogContent,
|
|
28
|
+
DialogHeader,
|
|
29
|
+
DialogTitle,
|
|
30
|
+
} from "@/components/ui/dialog";
|
|
31
|
+
import { addLinkFn, updateLinkFn } from "@/server/serverFns/story";
|
|
32
|
+
import type { StoryLinkItem } from "../types";
|
|
33
|
+
|
|
34
|
+
export interface LinkManageDialogProps {
|
|
35
|
+
open: boolean;
|
|
36
|
+
onOpenChange: (open: boolean) => void;
|
|
37
|
+
storyId: string;
|
|
38
|
+
data: StoryLinkItem[];
|
|
39
|
+
onUpdate: (links: StoryLinkItem[]) => void;
|
|
40
|
+
/** 添加链接产生的活动,供时间线即时追加 */
|
|
41
|
+
onActivity?: (activity: import("../types").ActivityItem) => void;
|
|
42
|
+
/** 非空时进入编辑态(由合并面板「编辑」触发) */
|
|
43
|
+
initialEdit?: StoryLinkItem | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function LinkManageDialog({
|
|
47
|
+
open,
|
|
48
|
+
onOpenChange,
|
|
49
|
+
storyId,
|
|
50
|
+
data,
|
|
51
|
+
onUpdate,
|
|
52
|
+
onActivity,
|
|
53
|
+
initialEdit = null,
|
|
54
|
+
}: LinkManageDialogProps) {
|
|
55
|
+
const { t } = useTranslation("story");
|
|
56
|
+
const addLink = useServerFn(addLinkFn);
|
|
57
|
+
const updateLink = useServerFn(updateLinkFn);
|
|
58
|
+
const [editing, setEditing] = React.useState<StoryLinkItem | null>(null);
|
|
59
|
+
const [form, setForm] = React.useState({ url: "", title: "", description: "" });
|
|
60
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
61
|
+
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
if (open) {
|
|
64
|
+
setEditing(initialEdit);
|
|
65
|
+
setForm(
|
|
66
|
+
initialEdit
|
|
67
|
+
? { url: initialEdit.url, title: initialEdit.title ?? "", description: initialEdit.description ?? "" }
|
|
68
|
+
: { url: "", title: "", description: "" },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}, [open, initialEdit]);
|
|
72
|
+
|
|
73
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
if (submitting) return;
|
|
76
|
+
setSubmitting(true);
|
|
77
|
+
try {
|
|
78
|
+
if (editing) {
|
|
79
|
+
await updateLink({
|
|
80
|
+
data: {
|
|
81
|
+
linkId: editing.id,
|
|
82
|
+
url: form.url,
|
|
83
|
+
title: form.title || undefined,
|
|
84
|
+
description: form.description || undefined,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
toast.success(t("linkDialog.updated"));
|
|
88
|
+
onUpdate(
|
|
89
|
+
data.map((l) =>
|
|
90
|
+
l.id === editing.id
|
|
91
|
+
? { ...l, url: form.url, title: form.title || null, description: form.description || null }
|
|
92
|
+
: l,
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
const created = await addLink({
|
|
97
|
+
data: {
|
|
98
|
+
storyId,
|
|
99
|
+
url: form.url,
|
|
100
|
+
title: form.title || undefined,
|
|
101
|
+
description: form.description || undefined,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
toast.success(t("linkDialog.added"));
|
|
105
|
+
if (created.activity) onActivity?.(created.activity);
|
|
106
|
+
onUpdate([
|
|
107
|
+
...data,
|
|
108
|
+
{
|
|
109
|
+
id: created.id,
|
|
110
|
+
url: created.url,
|
|
111
|
+
title: created.title,
|
|
112
|
+
description: created.description,
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
setForm({ url: "", title: "", description: "" });
|
|
116
|
+
}
|
|
117
|
+
setEditing(null);
|
|
118
|
+
onOpenChange(false);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
toast.error((err as Error)?.message || t("linkDialog.saveFailed"));
|
|
121
|
+
} finally {
|
|
122
|
+
setSubmitting(false);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
128
|
+
<DialogContent className="sm:max-w-130">
|
|
129
|
+
<DialogHeader>
|
|
130
|
+
<DialogTitle className="text-xl">
|
|
131
|
+
{t(editing ? "linkDialog.editTitle" : "linkDialog.title")}
|
|
132
|
+
</DialogTitle>
|
|
133
|
+
</DialogHeader>
|
|
134
|
+
<form onSubmit={handleSubmit} className="flex h-96 flex-col gap-4 overflow-y-auto px-1 pb-1">
|
|
135
|
+
<div className="space-y-1.5">
|
|
136
|
+
<label htmlFor="link-url" className="text-xs font-medium text-muted-foreground">
|
|
137
|
+
URL
|
|
138
|
+
</label>
|
|
139
|
+
<Input
|
|
140
|
+
id="link-url"
|
|
141
|
+
type="url"
|
|
142
|
+
required
|
|
143
|
+
placeholder={t("linkDialog.urlPlaceholder")}
|
|
144
|
+
value={form.url}
|
|
145
|
+
onChange={(e) => setForm({ ...form, url: e.target.value })}
|
|
146
|
+
className="h-10 text-base"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="space-y-1.5">
|
|
150
|
+
<label htmlFor="link-title" className="text-xs font-medium text-muted-foreground">
|
|
151
|
+
{t("linkDialog.titleOptional")}
|
|
152
|
+
</label>
|
|
153
|
+
<Input
|
|
154
|
+
id="link-title"
|
|
155
|
+
placeholder={t("linkDialog.titlePlaceholder")}
|
|
156
|
+
value={form.title}
|
|
157
|
+
onChange={(e) => setForm({ ...form, title: e.target.value })}
|
|
158
|
+
className="h-10"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
<div className="flex min-h-0 flex-1 flex-col gap-1.5">
|
|
162
|
+
<label htmlFor="link-desc" className="text-xs font-medium text-muted-foreground">
|
|
163
|
+
{t("linkDialog.descOptional")}
|
|
164
|
+
</label>
|
|
165
|
+
<Textarea
|
|
166
|
+
id="link-desc"
|
|
167
|
+
placeholder={t("linkDialog.descPlaceholder")}
|
|
168
|
+
value={form.description}
|
|
169
|
+
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
|
170
|
+
className="min-h-20 flex-1 resize-none"
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
<div className="flex shrink-0 justify-end gap-2 pt-1">
|
|
174
|
+
{editing && (
|
|
175
|
+
<Button
|
|
176
|
+
type="button"
|
|
177
|
+
variant="outline"
|
|
178
|
+
onClick={() => onOpenChange(false)}
|
|
179
|
+
>
|
|
180
|
+
{t("linkDialog.cancel")}
|
|
181
|
+
</Button>
|
|
182
|
+
)}
|
|
183
|
+
<Button type="submit" disabled={submitting}>
|
|
184
|
+
{t(editing ? "linkDialog.save" : "linkDialog.add")}
|
|
185
|
+
</Button>
|
|
186
|
+
</div>
|
|
187
|
+
</form>
|
|
188
|
+
</DialogContent>
|
|
189
|
+
</Dialog>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -145,7 +145,9 @@ export interface StoryAttachmentItem {
|
|
|
145
145
|
fileName: string;
|
|
146
146
|
fileSize: number;
|
|
147
147
|
mimeType: string;
|
|
148
|
+
description?: string | null;
|
|
148
149
|
storageUrl: string;
|
|
150
|
+
fileHash?: string | null;
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
// PRD 简要信息
|
|
@@ -158,6 +160,11 @@ export interface PrdBrief {
|
|
|
158
160
|
export interface DocBrief {
|
|
159
161
|
id: string;
|
|
160
162
|
title: string;
|
|
163
|
+
kbId: string;
|
|
164
|
+
kbName: string;
|
|
165
|
+
kbIcon: string;
|
|
166
|
+
docIcon: string | null;
|
|
167
|
+
parentId: string | null;
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
// 关联 Tab 数据汇总
|
|
@@ -176,6 +183,12 @@ export interface RelationsData {
|
|
|
176
183
|
};
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
/** 关联附件面板统一数据类型 */
|
|
187
|
+
export type AssociatedItem =
|
|
188
|
+
| { kind: "link"; data: StoryLinkItem }
|
|
189
|
+
| { kind: "attachment"; data: StoryAttachmentItem }
|
|
190
|
+
| { kind: "doc"; data: DocBrief };
|
|
191
|
+
|
|
179
192
|
// ====== 依赖关系(对齐 Plane,新模型) ======
|
|
180
193
|
|
|
181
194
|
// 规范化存储类型:语义统一为「source 领先于 target」
|
|
@@ -10,7 +10,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
10
10
|
"name": "@tanstack/react-start",
|
|
11
11
|
"displayName": "TanStack Start",
|
|
12
12
|
"version": "v1.168.49",
|
|
13
|
-
"lastUpdate": "
|
|
13
|
+
"lastUpdate": "Sep 1, 2026",
|
|
14
14
|
"source": "apps/web/package.json#dependencies",
|
|
15
15
|
"docsUrl": "https://tanstack.com/start/latest"
|
|
16
16
|
},
|
|
@@ -18,7 +18,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
18
18
|
"name": "@tanstack/react-router",
|
|
19
19
|
"displayName": "TanStack Router",
|
|
20
20
|
"version": "v1.170.32",
|
|
21
|
-
"lastUpdate": "
|
|
21
|
+
"lastUpdate": "Sep 1, 2026",
|
|
22
22
|
"source": "apps/web/package.json#dependencies",
|
|
23
23
|
"docsUrl": "https://tanstack.com/router/latest"
|
|
24
24
|
},
|
|
@@ -26,7 +26,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
26
26
|
"name": "@tanstack/react-query",
|
|
27
27
|
"displayName": "TanStack Query",
|
|
28
28
|
"version": "v5.102.8",
|
|
29
|
-
"lastUpdate": "
|
|
29
|
+
"lastUpdate": "Sep 1, 2026",
|
|
30
30
|
"source": "apps/web/package.json#dependencies",
|
|
31
31
|
"docsUrl": "https://tanstack.com/query/latest"
|
|
32
32
|
},
|
|
@@ -34,7 +34,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
34
34
|
"name": "@tanstack/ai",
|
|
35
35
|
"displayName": "TanStack AI",
|
|
36
36
|
"version": "v0.52.0",
|
|
37
|
-
"lastUpdate": "
|
|
37
|
+
"lastUpdate": "Sep 1, 2026",
|
|
38
38
|
"source": "apps/web/package.json#dependencies",
|
|
39
39
|
"docsUrl": "https://tanstack.com/ai/latest"
|
|
40
40
|
},
|
|
@@ -42,7 +42,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
42
42
|
"name": "@tanstack/react-table",
|
|
43
43
|
"displayName": "TanStack Table",
|
|
44
44
|
"version": "v9.2.3",
|
|
45
|
-
"lastUpdate": "
|
|
45
|
+
"lastUpdate": "Sep 1, 2026",
|
|
46
46
|
"source": "apps/web/package.json#dependencies",
|
|
47
47
|
"docsUrl": "https://tanstack.com/table/latest"
|
|
48
48
|
},
|
|
@@ -50,7 +50,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
50
50
|
"name": "@tanstack/react-form",
|
|
51
51
|
"displayName": "TanStack Form",
|
|
52
52
|
"version": "v2.0.0-alpha.2",
|
|
53
|
-
"lastUpdate": "
|
|
53
|
+
"lastUpdate": "Sep 1, 2026",
|
|
54
54
|
"source": "apps/web/package.json#dependencies",
|
|
55
55
|
"docsUrl": "https://tanstack.com/form/latest"
|
|
56
56
|
},
|
|
@@ -58,7 +58,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
58
58
|
"name": "@tanstack/charts",
|
|
59
59
|
"displayName": "TanStack Charts",
|
|
60
60
|
"version": "v0.16.0",
|
|
61
|
-
"lastUpdate": "
|
|
61
|
+
"lastUpdate": "Sep 1, 2026",
|
|
62
62
|
"source": "apps/admin/package.json#dependencies",
|
|
63
63
|
"docsUrl": "https://tanstack.com/charts/latest"
|
|
64
64
|
},
|
|
@@ -66,7 +66,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
66
66
|
"name": "@tanstack/react-pacer",
|
|
67
67
|
"displayName": "TanStack Pacer",
|
|
68
68
|
"version": "v0.23.0",
|
|
69
|
-
"lastUpdate": "
|
|
69
|
+
"lastUpdate": "Sep 1, 2026",
|
|
70
70
|
"source": "apps/web/package.json#dependencies",
|
|
71
71
|
"docsUrl": "https://tanstack.com/pacer/latest"
|
|
72
72
|
},
|
|
@@ -74,7 +74,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
74
74
|
"name": "@tanstack/react-hotkeys",
|
|
75
75
|
"displayName": "TanStack Hotkeys",
|
|
76
76
|
"version": "v0.10.0",
|
|
77
|
-
"lastUpdate": "
|
|
77
|
+
"lastUpdate": "Sep 1, 2026",
|
|
78
78
|
"source": "apps/web/package.json#dependencies",
|
|
79
79
|
"docsUrl": "https://tanstack.com/hotkeys/latest"
|
|
80
80
|
},
|
|
@@ -82,7 +82,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
82
82
|
"name": "@tanstack/react-virtual",
|
|
83
83
|
"displayName": "TanStack Virtual",
|
|
84
84
|
"version": "v3.14.10",
|
|
85
|
-
"lastUpdate": "
|
|
85
|
+
"lastUpdate": "Sep 1, 2026",
|
|
86
86
|
"source": "apps/admin/package.json#dependencies",
|
|
87
87
|
"docsUrl": "https://tanstack.com/virtual/latest"
|
|
88
88
|
}
|
|
@@ -95,15 +95,15 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
95
95
|
"items": [
|
|
96
96
|
{
|
|
97
97
|
"name": "@allbluecn/web-app",
|
|
98
|
-
"version": "v0.5.
|
|
99
|
-
"lastUpdate": "
|
|
98
|
+
"version": "v0.5.1",
|
|
99
|
+
"lastUpdate": "Sep 1, 2026",
|
|
100
100
|
"source": "apps/web/package.json",
|
|
101
101
|
"docsUrl": "https://github.com/allbluecn/allblue"
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
"name": "@allbluecn/kernel",
|
|
105
105
|
"version": "v0.3.1",
|
|
106
|
-
"lastUpdate": "
|
|
106
|
+
"lastUpdate": "Sep 1, 2026",
|
|
107
107
|
"source": "packages/kernel/package.json",
|
|
108
108
|
"docsUrl": "https://github.com/allbluecn/allblue"
|
|
109
109
|
}
|
|
@@ -117,42 +117,42 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
117
117
|
{
|
|
118
118
|
"name": "Vite",
|
|
119
119
|
"version": "v8.1.5",
|
|
120
|
-
"lastUpdate": "
|
|
120
|
+
"lastUpdate": "Sep 1, 2026",
|
|
121
121
|
"source": "apps/web/package.json#devDependencies",
|
|
122
122
|
"docsUrl": "https://vite.dev/"
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"name": "Node.js",
|
|
126
126
|
"version": "v24.19.0",
|
|
127
|
-
"lastUpdate": "
|
|
127
|
+
"lastUpdate": "Sep 1, 2026",
|
|
128
128
|
"source": ".nvmrc",
|
|
129
129
|
"docsUrl": "https://nodejs.org/"
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"name": "Prisma",
|
|
133
133
|
"version": "v7.8.0",
|
|
134
|
-
"lastUpdate": "
|
|
134
|
+
"lastUpdate": "Sep 1, 2026",
|
|
135
135
|
"source": "packages/database/package.json#devDependencies",
|
|
136
136
|
"docsUrl": "https://www.prisma.io/docs"
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
"name": "shadcn UI",
|
|
140
140
|
"version": "v1.2.4",
|
|
141
|
-
"lastUpdate": "
|
|
141
|
+
"lastUpdate": "Sep 1, 2026",
|
|
142
142
|
"source": "apps/web/package.json#dependencies",
|
|
143
143
|
"docsUrl": "https://ui.shadcn.com/"
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
"name": "TipTap",
|
|
147
147
|
"version": "v3.27.4",
|
|
148
|
-
"lastUpdate": "
|
|
148
|
+
"lastUpdate": "Sep 1, 2026",
|
|
149
149
|
"source": "packages/shared/package.json#devDependencies",
|
|
150
150
|
"docsUrl": "https://tiptap.dev/"
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
"name": "Lucide React",
|
|
154
154
|
"version": "v1.24.0",
|
|
155
|
-
"lastUpdate": "
|
|
155
|
+
"lastUpdate": "Sep 1, 2026",
|
|
156
156
|
"source": "packages/ui/package.json#dependencies",
|
|
157
157
|
"docsUrl": "https://lucide.dev/"
|
|
158
158
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveVideoEmbed } from "../video-embed";
|
|
3
|
+
|
|
4
|
+
describe("resolveVideoEmbed", () => {
|
|
5
|
+
it("非视频链接返回 null", () => {
|
|
6
|
+
expect(resolveVideoEmbed("https://example.com/page")).toBeNull();
|
|
7
|
+
expect(resolveVideoEmbed("https://lucide.dev/icons/layout-list")).toBeNull();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("直链视频文件走 file 分支(忽略 query/hash)", () => {
|
|
11
|
+
expect(resolveVideoEmbed("https://cdn.example.com/a.mp4")).toEqual({
|
|
12
|
+
kind: "file",
|
|
13
|
+
src: "https://cdn.example.com/a.mp4",
|
|
14
|
+
});
|
|
15
|
+
expect(
|
|
16
|
+
resolveVideoEmbed("https://cdn.example.com/a.webm?token=x#t=1"),
|
|
17
|
+
).toEqual({ kind: "file", src: "https://cdn.example.com/a.webm?token=x#t=1" });
|
|
18
|
+
expect(resolveVideoEmbed("https://cdn.example.com/a.MOV")).toEqual({
|
|
19
|
+
kind: "file",
|
|
20
|
+
src: "https://cdn.example.com/a.MOV",
|
|
21
|
+
});
|
|
22
|
+
// 扩展名仅匹配路径末段,不匹配 query 里的伪扩展名
|
|
23
|
+
expect(resolveVideoEmbed("https://example.com/watch?file=a.mp4")).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("YouTube watch 页转换为 embed 播放器", () => {
|
|
27
|
+
expect(resolveVideoEmbed("https://www.youtube.com/watch?v=dQw4w9WgXcQ")).toEqual({
|
|
28
|
+
kind: "iframe",
|
|
29
|
+
src: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
|
30
|
+
});
|
|
31
|
+
expect(
|
|
32
|
+
resolveVideoEmbed("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=30s"),
|
|
33
|
+
).toEqual({ kind: "iframe", src: "https://www.youtube.com/embed/dQw4w9WgXcQ" });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("YouTube youtu.be 短链与 shorts 转换", () => {
|
|
37
|
+
expect(resolveVideoEmbed("https://youtu.be/dQw4w9WgXcQ")).toEqual({
|
|
38
|
+
kind: "iframe",
|
|
39
|
+
src: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
|
40
|
+
});
|
|
41
|
+
expect(resolveVideoEmbed("https://www.youtube.com/shorts/abc123")).toEqual({
|
|
42
|
+
kind: "iframe",
|
|
43
|
+
src: "https://www.youtube.com/embed/abc123",
|
|
44
|
+
});
|
|
45
|
+
// 已是 embed 链接原样返回
|
|
46
|
+
expect(resolveVideoEmbed("https://www.youtube.com/embed/dQw4w9WgXcQ")).toEqual({
|
|
47
|
+
kind: "iframe",
|
|
48
|
+
src: "https://www.youtube.com/embed/dQw4w9WgXcQ",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("Bilibili 视频页转换为官方播放器(BV 号 + 分 P)", () => {
|
|
53
|
+
expect(
|
|
54
|
+
resolveVideoEmbed("https://www.bilibili.com/video/BV1GJ411x7h7/"),
|
|
55
|
+
).toEqual({
|
|
56
|
+
kind: "iframe",
|
|
57
|
+
src: "https://player.bilibili.com/player.html?bvid=BV1GJ411x7h7&autoplay=0",
|
|
58
|
+
});
|
|
59
|
+
expect(
|
|
60
|
+
resolveVideoEmbed("https://www.bilibili.com/video/BV1GJ411x7h7?p=3"),
|
|
61
|
+
).toEqual({
|
|
62
|
+
kind: "iframe",
|
|
63
|
+
src: "https://player.bilibili.com/player.html?bvid=BV1GJ411x7h7&autoplay=0&p=3",
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("Vimeo 转换为播放器", () => {
|
|
68
|
+
expect(resolveVideoEmbed("https://vimeo.com/76979871")).toEqual({
|
|
69
|
+
kind: "iframe",
|
|
70
|
+
src: "https://player.vimeo.com/video/76979871",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("b23.tv 短链无法本地解析,返回 null 交给原有探测逻辑", () => {
|
|
75
|
+
expect(resolveVideoEmbed("https://b23.tv/BV1GJ411x7h7")).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// 视频链接识别与官方 embed 转换:供关联面板链接预览使用
|
|
2
|
+
// 返回 null 表示非视频链接(交由原有的可嵌入探测逻辑处理)
|
|
3
|
+
|
|
4
|
+
export type VideoEmbed =
|
|
5
|
+
| { kind: "file"; src: string }
|
|
6
|
+
| { kind: "iframe"; src: string };
|
|
7
|
+
|
|
8
|
+
const VIDEO_FILE_RE = /\.(mp4|webm|ogg|ogv|mov|m4v)$/i;
|
|
9
|
+
|
|
10
|
+
const YOUTUBE_ID_RE =
|
|
11
|
+
/^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?(?:.*&)?v=|shorts\/|embed\/)|youtu\.be\/)([A-Za-z0-9_-]{6,})/i;
|
|
12
|
+
|
|
13
|
+
const BILIBILI_BV_RE =
|
|
14
|
+
/^(?:https?:\/\/)?(?:www\.)?bilibili\.com\/video\/(BV[0-9A-Za-z]+)\/?(?:\?.*)?$/i;
|
|
15
|
+
|
|
16
|
+
const VIMEO_ID_RE = /^(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)(?:\?.*)?$/i;
|
|
17
|
+
|
|
18
|
+
/** 解析 B 站分 P 参数(仅接受纯数字,防止参数注入) */
|
|
19
|
+
function getBilibiliPart(rawUrl: string): string | null {
|
|
20
|
+
try {
|
|
21
|
+
const p = new URL(rawUrl).searchParams.get("p");
|
|
22
|
+
return p && /^\d+$/.test(p) ? p : null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveVideoEmbed(rawUrl: string): VideoEmbed | null {
|
|
29
|
+
if (!rawUrl) return null;
|
|
30
|
+
|
|
31
|
+
// 直链视频文件:扩展名只看 URL 路径末段,避免 query 中的伪扩展名误判
|
|
32
|
+
try {
|
|
33
|
+
const parsed = new URL(rawUrl);
|
|
34
|
+
if (VIDEO_FILE_RE.test(parsed.pathname)) {
|
|
35
|
+
return { kind: "file", src: rawUrl };
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const youtube = rawUrl.match(YOUTUBE_ID_RE);
|
|
42
|
+
if (youtube) {
|
|
43
|
+
return { kind: "iframe", src: `https://www.youtube.com/embed/${youtube[1]}` };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const bilibili = rawUrl.match(BILIBILI_BV_RE);
|
|
47
|
+
if (bilibili) {
|
|
48
|
+
const part = getBilibiliPart(rawUrl);
|
|
49
|
+
const src = `https://player.bilibili.com/player.html?bvid=${bilibili[1]}&autoplay=0${part ? `&p=${part}` : ""}`;
|
|
50
|
+
return { kind: "iframe", src };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const vimeo = rawUrl.match(VIMEO_ID_RE);
|
|
54
|
+
if (vimeo) {
|
|
55
|
+
return { kind: "iframe", src: `https://player.vimeo.com/video/${vimeo[1]}` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"routeFiles": [
|
|
3
|
+
"src/routes/_nav/tags/route.tsx",
|
|
4
|
+
"src/routes/_nav/tags/index.tsx",
|
|
5
|
+
"src/routes/_nav/tags/$id/route.tsx",
|
|
6
|
+
"src/routes/_nav/tags/settings/route.tsx",
|
|
7
|
+
"src/routes/_nav/inspiration/route.tsx",
|
|
8
|
+
"src/routes/_nav/prd/route.tsx",
|
|
9
|
+
"src/routes/_nav/prd/index.tsx",
|
|
10
|
+
"src/routes/_nav/prd/$id/route.tsx",
|
|
11
|
+
"src/routes/share/prd/$prdId/route.tsx"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import type { ProFeature } from '@allbluecn/kernel'
|
|
3
|
+
|
|
4
|
+
export const activePluginIds = ['@allbluecn/tags', '@allbluecn/inspiration', '@allbluecn/prd'] as const
|
|
5
|
+
export type ActivePluginId = (typeof activePluginIds)[number]
|
|
6
|
+
|
|
7
|
+
export const implementedModules: Record<ProFeature, boolean> = {
|
|
8
|
+
billing: false,
|
|
9
|
+
hostedAi: false,
|
|
10
|
+
sso: false,
|
|
11
|
+
audit: false,
|
|
12
|
+
permissions: false,
|
|
13
|
+
license: false,
|
|
14
|
+
versionHistory: false,
|
|
15
|
+
automation: false,
|
|
16
|
+
templates: false,
|
|
17
|
+
analytics: false,
|
|
18
|
+
globalSearch: false,
|
|
19
|
+
compliance: false,
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
// profile: oss (validated: 3 plugins)
|
|
3
|
+
// 拓扑序:@allbluecn/tags -> @allbluecn/inspiration -> @allbluecn/prd
|
|
4
|
+
import tags from '@allbluecn/plugins-core/tags'
|
|
5
|
+
import inspiration from '@allbluecn/plugins-core/inspiration'
|
|
6
|
+
import prd from '@allbluecn/plugins-core/prd'
|
|
7
|
+
import { defineProfile, type ProfileDefinition } from '@allbluecn/kernel'
|
|
8
|
+
|
|
9
|
+
export const profile: ProfileDefinition = defineProfile({
|
|
10
|
+
id: 'oss',
|
|
11
|
+
plugins: [tags, inspiration, prd],
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const pluginIds: string[] = ['@allbluecn/tags', '@allbluecn/inspiration', '@allbluecn/prd']
|
|
15
|
+
|
|
16
|
+
export const pluginMiddleware: unknown[] = [
|
|
17
|
+
...tags.contributes.serverMiddleware ?? [],
|
|
18
|
+
...inspiration.contributes.serverMiddleware ?? [],
|
|
19
|
+
...prd.contributes.serverMiddleware ?? [],
|
|
20
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
|
|
2
|
+
import { NavTag } from '@allbluecn/plugins-core/tags/ui'
|
|
3
|
+
import { type ComponentType } from 'react'
|
|
4
|
+
import type { SettingsTabContribution } from '@allbluecn/kernel'
|
|
5
|
+
|
|
6
|
+
export const navItems: ComponentType[] = [NavTag]
|
|
7
|
+
export const settingsTabs: SettingsTabContribution[] = []
|
|
8
|
+
export const dashboardWidgets: ComponentType[] = []
|