@allbluecn/shared 0.2.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 +117 -0
- package/src/constants/perf.ts +21 -0
- package/src/i18n/client.ts +32 -0
- package/src/i18n/config.ts +104 -0
- package/src/i18n/detect.ts +43 -0
- package/src/i18n/locales/en/agents.json +134 -0
- package/src/i18n/locales/en/apps.json +6 -0
- package/src/i18n/locales/en/auth.json +88 -0
- package/src/i18n/locales/en/billing.json +78 -0
- package/src/i18n/locales/en/chat.json +194 -0
- package/src/i18n/locales/en/common.json +203 -0
- package/src/i18n/locales/en/dashboard.json +38 -0
- package/src/i18n/locales/en/design.json +70 -0
- package/src/i18n/locales/en/desktop.json +35 -0
- package/src/i18n/locales/en/display.json +37 -0
- package/src/i18n/locales/en/editor.json +304 -0
- package/src/i18n/locales/en/enterprise.json +1 -0
- package/src/i18n/locales/en/invite.json +11 -0
- package/src/i18n/locales/en/knowledge.json +451 -0
- package/src/i18n/locales/en/mindmap.json +28 -0
- package/src/i18n/locales/en/prd.json +43 -0
- package/src/i18n/locales/en/projects.json +133 -0
- package/src/i18n/locales/en/settings.json +375 -0
- package/src/i18n/locales/en/share.json +20 -0
- package/src/i18n/locales/en/story.json +785 -0
- package/src/i18n/locales/en/tags.json +42 -0
- package/src/i18n/locales/en/workspace.json +90 -0
- package/src/i18n/locales/zh/agents.json +134 -0
- package/src/i18n/locales/zh/apps.json +6 -0
- package/src/i18n/locales/zh/auth.json +88 -0
- package/src/i18n/locales/zh/billing.json +78 -0
- package/src/i18n/locales/zh/chat.json +194 -0
- package/src/i18n/locales/zh/common.json +203 -0
- package/src/i18n/locales/zh/dashboard.json +38 -0
- package/src/i18n/locales/zh/design.json +70 -0
- package/src/i18n/locales/zh/desktop.json +35 -0
- package/src/i18n/locales/zh/display.json +37 -0
- package/src/i18n/locales/zh/editor.json +304 -0
- package/src/i18n/locales/zh/enterprise.json +1 -0
- package/src/i18n/locales/zh/invite.json +11 -0
- package/src/i18n/locales/zh/knowledge.json +451 -0
- package/src/i18n/locales/zh/mindmap.json +28 -0
- package/src/i18n/locales/zh/prd.json +43 -0
- package/src/i18n/locales/zh/projects.json +133 -0
- package/src/i18n/locales/zh/settings.json +375 -0
- package/src/i18n/locales/zh/share.json +20 -0
- package/src/i18n/locales/zh/story.json +785 -0
- package/src/i18n/locales/zh/tags.json +42 -0
- package/src/i18n/locales/zh/workspace.json +90 -0
- package/src/i18n/server.ts +18 -0
- package/src/i18n/types.ts +5 -0
- package/src/index.ts +90 -0
- package/src/infrastructure/ai/adapter.ts +59 -0
- package/src/infrastructure/ai/cli-detector.ts +64 -0
- package/src/infrastructure/ip-location.ts +76 -0
- package/src/infrastructure/references/engine.ts +104 -0
- package/src/infrastructure/ua-parser.ts +107 -0
- package/src/interfaces/ai.ts +38 -0
- package/src/interfaces/avatar.ts +22 -0
- package/src/interfaces/module.ts +60 -0
- package/src/interfaces/references.ts +44 -0
- package/src/interfaces/transport.ts +26 -0
- package/src/lib/annotation-template.ts +33 -0
- package/src/lib/comment-utils.ts +47 -0
- package/src/lib/datetime.ts +115 -0
- package/src/lib/desktop-detect.ts +27 -0
- package/src/lib/outline-utils.ts +57 -0
- package/src/lib/pacer.ts +96 -0
- package/src/lib/prd/stack-layout.ts +53 -0
- package/src/lib/prd-tree-utils.ts +94 -0
- package/src/lib/prototype-registry.ts +24 -0
- package/src/lib/query-client-seam.ts +14 -0
- package/src/lib/share-events.ts +78 -0
- package/src/lib/sidebar-utils.ts +21 -0
- package/src/lib/stores/prd-canvas-store.ts +69 -0
- package/src/lib/tag-colors.ts +35 -0
- package/src/lib/time-format.ts +50 -0
- package/src/modules/ai-providers/constants.ts +79 -0
- package/src/modules/ai-providers/index.ts +21 -0
- package/src/modules/ai-providers/seed-data.ts +222 -0
- package/src/modules/ai-providers/types.ts +95 -0
- package/src/modules/billing/feature-registry.ts +63 -0
- package/src/modules/billing/plan-features-schema.ts +11 -0
- package/src/modules/billing/resource-registry.ts +80 -0
- package/src/modules/definitions/plugin.module.ts +38 -0
- package/src/modules/definitions/prd.module.ts +45 -0
- package/src/modules/definitions/prototype.module.ts +38 -0
- package/src/modules/dependencies/index.ts +186 -0
- package/src/modules/register-defaults.ts +27 -0
- package/src/modules/registry.ts +53 -0
- package/src/server-seam.ts +51 -0
- package/src/types/outline.ts +26 -0
- package/src/types/prd.ts +90 -0
- package/src/types/prototype.ts +12 -0
- package/src/virtual.d.ts +4 -0
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
export interface CLIAgentInfo {
|
|
19
|
+
id: string
|
|
20
|
+
name: string
|
|
21
|
+
binary: string
|
|
22
|
+
version: string
|
|
23
|
+
available: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AiAdapter {
|
|
27
|
+
detect(): Promise<CLIAgentInfo[]>
|
|
28
|
+
execute(
|
|
29
|
+
commandId: string,
|
|
30
|
+
ctx: { entityType: string; entityIds: string[]; userInput?: string }
|
|
31
|
+
): Promise<{ content: string }>
|
|
32
|
+
stream(
|
|
33
|
+
commandId: string,
|
|
34
|
+
ctx: { entityType: string; entityIds: string[]; userInput?: string }
|
|
35
|
+
): AsyncGenerator<string>
|
|
36
|
+
getActiveAgent(): string
|
|
37
|
+
switchAgent(agentId: string): void
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
export type AvatarPart = "face" | "eyes" | "eyebrows" | "mouth" | "nose" | "glasses" | "hair" | "accessories" | "details" | "beard"
|
|
19
|
+
|
|
20
|
+
export type AvatarConfig = Record<AvatarPart, number>
|
|
21
|
+
|
|
22
|
+
export type ShapeType = "circle" | "rounded" | "square"
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
export interface EntityDef {
|
|
19
|
+
type: string
|
|
20
|
+
table: string
|
|
21
|
+
searchFields: string[]
|
|
22
|
+
displayFields: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AICommandContext {
|
|
26
|
+
entityType: string
|
|
27
|
+
entityIds: string[]
|
|
28
|
+
userInput?: string
|
|
29
|
+
module: AllBlueModule
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AICommandResult {
|
|
33
|
+
content: string
|
|
34
|
+
actions?: { type: string; payload: unknown }[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type AICommandHandler = (ctx: AICommandContext) => Promise<AICommandResult>
|
|
38
|
+
|
|
39
|
+
export interface AICommandDef {
|
|
40
|
+
id: string
|
|
41
|
+
label: string
|
|
42
|
+
icon: string
|
|
43
|
+
entityTypes: string[]
|
|
44
|
+
handler: AICommandHandler
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type ReferenceDisplayFn = (
|
|
48
|
+
type: string,
|
|
49
|
+
data: Record<string, unknown>
|
|
50
|
+
) => { title: string; status?: string; priority?: string; url?: string }
|
|
51
|
+
|
|
52
|
+
export interface AllBlueModule {
|
|
53
|
+
id: string
|
|
54
|
+
name: string
|
|
55
|
+
icon: string
|
|
56
|
+
workspaceRoute: string
|
|
57
|
+
entities: EntityDef[]
|
|
58
|
+
aiCommands: AICommandDef[]
|
|
59
|
+
referenceDisplay: ReferenceDisplayFn
|
|
60
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
export interface RefDisplay {
|
|
19
|
+
title: string
|
|
20
|
+
status?: string
|
|
21
|
+
priority?: string
|
|
22
|
+
url?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Reference {
|
|
26
|
+
id: string
|
|
27
|
+
sourceModule: string
|
|
28
|
+
sourceEntityId: string
|
|
29
|
+
targetModule: string
|
|
30
|
+
targetEntityId: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResolvedRef extends Reference {
|
|
34
|
+
display: RefDisplay
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SearchResult {
|
|
38
|
+
id: string
|
|
39
|
+
module: string
|
|
40
|
+
type: string
|
|
41
|
+
title: string
|
|
42
|
+
status?: string
|
|
43
|
+
url?: string
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
export interface EntityUpdateEvent {
|
|
19
|
+
type: string
|
|
20
|
+
id: string
|
|
21
|
+
fields: Record<string, unknown>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RealtimeTransport {
|
|
25
|
+
subscribe(entityIds: string[], onUpdate: (event: EntityUpdateEvent) => void): () => void
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { DOMParser } from "@tiptap/pm/model";
|
|
19
|
+
import type { Editor } from "@tiptap/react";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 将 HTML 字符串转为 ProseMirror content 节点数组(用于 insertContent 的 content 字段)。
|
|
23
|
+
* 客户端调用(onAnnotate 事件触发),document 可用。
|
|
24
|
+
*/
|
|
25
|
+
export function htmlToPmContent(editor: Editor, html: string): any[] {
|
|
26
|
+
if (typeof document === "undefined" || !html) return [{ type: "paragraph" }];
|
|
27
|
+
const wrapper = document.createElement("div");
|
|
28
|
+
wrapper.innerHTML = html;
|
|
29
|
+
const parser = DOMParser.fromSchema(editor.schema);
|
|
30
|
+
const parsed = parser.parse(wrapper);
|
|
31
|
+
const json = parsed.toJSON();
|
|
32
|
+
return (json as any).content ?? [{ type: "paragraph" }];
|
|
33
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
export function maskEmail(email?: string | null): string {
|
|
19
|
+
if (!email) return "未知用户";
|
|
20
|
+
const [local, domain] = email.split("@");
|
|
21
|
+
if (!domain) return email;
|
|
22
|
+
const prefix = local.length > 3 ? local.slice(0, 3) : local;
|
|
23
|
+
return `${prefix}***@${domain}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatTime(date: Date | string): string {
|
|
27
|
+
const d = new Date(date);
|
|
28
|
+
const now = new Date();
|
|
29
|
+
const diff = now.getTime() - d.getTime();
|
|
30
|
+
const minutes = Math.floor(diff / 60000);
|
|
31
|
+
if (minutes < 1) return "刚刚";
|
|
32
|
+
if (minutes < 60) return `${minutes}分钟前`;
|
|
33
|
+
const hours = Math.floor(minutes / 60);
|
|
34
|
+
if (hours < 24) return `${hours}小时前`;
|
|
35
|
+
const days = Math.floor(hours / 24);
|
|
36
|
+
if (days < 7) return `${days}天前`;
|
|
37
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function formatEditedTime(date: Date | string): string {
|
|
41
|
+
const time = formatTime(date);
|
|
42
|
+
return `修改于${time}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getUserInitial(username: string): string {
|
|
46
|
+
return (username || "?").charAt(0).toUpperCase();
|
|
47
|
+
}
|
|
@@ -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 { formatInTimeZone, toZonedTime, fromZonedTime } from 'date-fns-tz'
|
|
19
|
+
import { startOfDay, subDays } from 'date-fns'
|
|
20
|
+
|
|
21
|
+
const FALLBACK_TIMEZONE = 'Asia/Shanghai'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 解析生效时区:项目时区 > 用户时区 > 浏览器时区 > Asia/Shanghai
|
|
25
|
+
*/
|
|
26
|
+
export function resolveTimezone(
|
|
27
|
+
projectTz?: string | null,
|
|
28
|
+
userTz?: string | null,
|
|
29
|
+
): string {
|
|
30
|
+
const browserTz =
|
|
31
|
+
typeof Intl !== 'undefined'
|
|
32
|
+
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
33
|
+
: FALLBACK_TIMEZONE
|
|
34
|
+
return projectTz || userTz || browserTz || FALLBACK_TIMEZONE
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 时区感知的时间格式化。
|
|
39
|
+
* @param date UTC Date 对象(来自 DB)
|
|
40
|
+
* @param timezone IANA 时区,如 "Asia/Shanghai"
|
|
41
|
+
* @param fmt date-fns 格式,默认 "yyyy-MM-dd HH:mm"
|
|
42
|
+
*/
|
|
43
|
+
export function formatDate(
|
|
44
|
+
date: Date,
|
|
45
|
+
timezone: string,
|
|
46
|
+
fmt = 'yyyy-MM-dd HH:mm',
|
|
47
|
+
): string {
|
|
48
|
+
try {
|
|
49
|
+
return formatInTimeZone(date, timezone, fmt)
|
|
50
|
+
} catch {
|
|
51
|
+
return formatInTimeZone(date, FALLBACK_TIMEZONE, fmt)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 相对时间(今天/昨天/具体日期),基于时区计算「今天的 0 点」。
|
|
57
|
+
* @param now 当前时间(测试注入;生产用默认 new Date())
|
|
58
|
+
*/
|
|
59
|
+
export function formatRelative(
|
|
60
|
+
date: Date,
|
|
61
|
+
timezone: string,
|
|
62
|
+
now: Date = new Date(),
|
|
63
|
+
): string {
|
|
64
|
+
try {
|
|
65
|
+
const todayStart = startOfDayInTimeZone(now, timezone)
|
|
66
|
+
const yesterdayStart = new Date(todayStart)
|
|
67
|
+
yesterdayStart.setDate(yesterdayStart.getDate() - 1)
|
|
68
|
+
|
|
69
|
+
const dateStart = startOfDayInTimeZone(date, timezone)
|
|
70
|
+
|
|
71
|
+
if (dateStart.getTime() === todayStart.getTime()) return '今天'
|
|
72
|
+
if (dateStart.getTime() === yesterdayStart.getTime()) return '昨天'
|
|
73
|
+
return formatInTimeZone(date, timezone, 'yyyy-MM-dd')
|
|
74
|
+
} catch {
|
|
75
|
+
return formatInTimeZone(date, FALLBACK_TIMEZONE, 'yyyy-MM-dd')
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 获取指定时区下某天的 0 点(UTC Date 对象)。
|
|
81
|
+
*/
|
|
82
|
+
function startOfDayInTimeZone(date: Date, timezone: string): Date {
|
|
83
|
+
const zoned = toZonedTime(date, timezone)
|
|
84
|
+
const dayStart = startOfDay(zoned)
|
|
85
|
+
return fromZonedTime(dayStart, timezone)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 对话历史日期分组标签(今天/昨天/本周/具体日期)。
|
|
90
|
+
* @param date UTC Date 对象
|
|
91
|
+
* @param timezone IANA 时区
|
|
92
|
+
*/
|
|
93
|
+
export function formatChatDate(date: Date, timezone: string): string {
|
|
94
|
+
try {
|
|
95
|
+
const now = new Date()
|
|
96
|
+
const todayStart = startOfDayInTimeZone(now, timezone)
|
|
97
|
+
const yesterdayStart = subDays(todayStart, 1)
|
|
98
|
+
const weekStart = subDays(todayStart, 7)
|
|
99
|
+
|
|
100
|
+
const dateStart = startOfDayInTimeZone(date, timezone)
|
|
101
|
+
|
|
102
|
+
if (dateStart.getTime() === todayStart.getTime()) return '今天'
|
|
103
|
+
if (dateStart.getTime() === yesterdayStart.getTime()) return '昨天'
|
|
104
|
+
if (dateStart.getTime() >= weekStart.getTime()) return '本周'
|
|
105
|
+
|
|
106
|
+
const currentYear = Number(formatInTimeZone(now, timezone, 'yyyy'))
|
|
107
|
+
const dateYear = Number(formatInTimeZone(date, timezone, 'yyyy'))
|
|
108
|
+
if (dateYear === currentYear) {
|
|
109
|
+
return formatInTimeZone(date, timezone, 'M月d日')
|
|
110
|
+
}
|
|
111
|
+
return formatInTimeZone(date, timezone, 'yyyy年M月d日')
|
|
112
|
+
} catch {
|
|
113
|
+
return formatInTimeZone(date, FALLBACK_TIMEZONE, 'yyyy-MM-dd')
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect if code is running inside Tauri desktop environment.
|
|
3
|
+
* Uses `window.__TAURI_INTERNALS__` (Tauri 2) with fallback to `window.__TAURI__` (Tauri 1 compat).
|
|
4
|
+
*/
|
|
5
|
+
export function isDesktop(): boolean {
|
|
6
|
+
if (typeof window === 'undefined') return false
|
|
7
|
+
return Boolean(
|
|
8
|
+
(window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ ||
|
|
9
|
+
(window as unknown as { __TAURI__?: unknown }).__TAURI__,
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dynamically import Tauri core API. Returns null if not in desktop env.
|
|
15
|
+
* Use this to avoid bundling @tauri-apps/api in web production.
|
|
16
|
+
*/
|
|
17
|
+
export async function getTauriInvoke(): Promise<
|
|
18
|
+
((cmd: string, args?: Record<string, unknown>) => Promise<unknown>) | null
|
|
19
|
+
> {
|
|
20
|
+
if (!isDesktop()) return null
|
|
21
|
+
try {
|
|
22
|
+
const core = await import('@tauri-apps/api/core')
|
|
23
|
+
return core.invoke
|
|
24
|
+
} catch {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { CanvasElement } from "../types/prototype";
|
|
19
|
+
import { OutlineNode } from "../types/outline";
|
|
20
|
+
|
|
21
|
+
export function buildOutline(elements: CanvasElement[]): OutlineNode[] {
|
|
22
|
+
const containers = elements.filter(e => isContainer(e));
|
|
23
|
+
const atoms = elements.filter(e => !isContainer(e));
|
|
24
|
+
const roots: OutlineNode[] = [];
|
|
25
|
+
const usedAtoms = new Set<string>();
|
|
26
|
+
|
|
27
|
+
for (const c of containers) {
|
|
28
|
+
const rect = { x: c.x, y: c.y, w: c.width, h: c.height };
|
|
29
|
+
const children = atoms
|
|
30
|
+
.filter(a => !usedAtoms.has(a.id) && isInside(a, rect))
|
|
31
|
+
.map(a => { usedAtoms.add(a.id); return toAtomNode(a); });
|
|
32
|
+
roots.push({ id: `container-${c.id}`, label: (c.overrides ?? c.props)?.name || c.type, type: "container", elementId: c.id, children });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const a of atoms) {
|
|
36
|
+
if (!usedAtoms.has(a.id)) {
|
|
37
|
+
roots.push(toAtomNode(a));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return roots;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isContainer(el: CanvasElement): boolean {
|
|
45
|
+
return (el.width >= 160 && el.height >= 100) || (el.overrides ?? el.props)?.isContainer === true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isInside(atom: CanvasElement, rect: { x: number; y: number; w: number; h: number }): boolean {
|
|
49
|
+
return atom.x >= rect.x && atom.y >= rect.y &&
|
|
50
|
+
atom.x + atom.width <= rect.x + rect.w &&
|
|
51
|
+
atom.y + atom.height <= rect.y + rect.h;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toAtomNode(el: CanvasElement): OutlineNode {
|
|
55
|
+
const p = el.overrides ?? el.props;
|
|
56
|
+
return { id: `atom-${el.id}`, label: p?.text || p?.label || el.type, type: "atom", elementId: el.id, children: [] };
|
|
57
|
+
}
|
package/src/lib/pacer.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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 { RateLimiter } from "@tanstack/pacer"
|
|
19
|
+
import { useDebouncedCallback, useThrottledCallback } from "@tanstack/react-pacer"
|
|
20
|
+
|
|
21
|
+
export const EDITOR_AUTO_SAVE_MS = 2000
|
|
22
|
+
export const EDITOR_CHANGE_DEBOUNCE_MS = 500
|
|
23
|
+
export const SEARCH_DEBOUNCE_MS = 300
|
|
24
|
+
export const SCROLL_THROTTLE_MS = 150
|
|
25
|
+
export const AI_RATE_LIMIT = {
|
|
26
|
+
chat: { max: 10, windowMs: 60_000 },
|
|
27
|
+
execute: { max: 5, windowMs: 60_000 },
|
|
28
|
+
generatePrototype: { max: 5, windowMs: 60_000 },
|
|
29
|
+
testProvider: { max: 10, windowMs: 60_000 },
|
|
30
|
+
detect: { max: 10, windowMs: 60_000 },
|
|
31
|
+
webSearch: { max: 20, windowMs: 60_000 },
|
|
32
|
+
} as const
|
|
33
|
+
|
|
34
|
+
export interface RateLimitConfig {
|
|
35
|
+
max: number
|
|
36
|
+
windowMs: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { useDebouncedCallback }
|
|
40
|
+
export { useThrottledCallback }
|
|
41
|
+
|
|
42
|
+
interface ServerFnRateLimitResult {
|
|
43
|
+
allowed: boolean
|
|
44
|
+
retryAfterMs: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const limiterCache = new Map<
|
|
48
|
+
string,
|
|
49
|
+
{ limiter: RateLimiter<(...args: unknown[]) => boolean>; lastAccess: number }
|
|
50
|
+
>()
|
|
51
|
+
|
|
52
|
+
const CACHE_TTL_MS = 5 * 60 * 1000
|
|
53
|
+
|
|
54
|
+
function purgeStaleEntries() {
|
|
55
|
+
const now = Date.now()
|
|
56
|
+
for (const [key, entry] of limiterCache) {
|
|
57
|
+
if (now - entry.lastAccess > CACHE_TTL_MS) {
|
|
58
|
+
entry.limiter.reset()
|
|
59
|
+
limiterCache.delete(key)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setInterval(purgeStaleEntries, 60_000)
|
|
65
|
+
|
|
66
|
+
export function getRateLimiter(
|
|
67
|
+
key: string,
|
|
68
|
+
config: RateLimitConfig
|
|
69
|
+
): RateLimiter<(...args: unknown[]) => boolean> {
|
|
70
|
+
const existing = limiterCache.get(key)
|
|
71
|
+
if (existing) {
|
|
72
|
+
existing.lastAccess = Date.now()
|
|
73
|
+
return existing.limiter
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const limiter = new RateLimiter(() => true, {
|
|
77
|
+
limit: config.max,
|
|
78
|
+
window: config.windowMs,
|
|
79
|
+
windowType: "sliding",
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
limiterCache.set(key, { limiter, lastAccess: Date.now() })
|
|
83
|
+
return limiter
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function checkRateLimit(
|
|
87
|
+
key: string,
|
|
88
|
+
config: RateLimitConfig
|
|
89
|
+
): ServerFnRateLimitResult {
|
|
90
|
+
const limiter = getRateLimiter(key, config)
|
|
91
|
+
const allowed = limiter.maybeExecute()
|
|
92
|
+
return {
|
|
93
|
+
allowed,
|
|
94
|
+
retryAfterMs: allowed ? 0 : limiter.getMsUntilNextWindow(),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { CanvasElement } from "../../types/prototype"
|
|
19
|
+
|
|
20
|
+
export interface StackLayoutOptions {
|
|
21
|
+
startX: number
|
|
22
|
+
startY: number
|
|
23
|
+
gap: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 计算新组件在画板上的纵向堆叠位置。
|
|
28
|
+
* 起始 y = 现有元素的最大 (y + height) + gap,若无现有元素则用 startY。
|
|
29
|
+
* items 的 height 用于决定每个新组件的纵向偏移,确保大小不一的组件不会重叠。
|
|
30
|
+
*/
|
|
31
|
+
export function computeStackPositions(
|
|
32
|
+
existing: Pick<CanvasElement, "x" | "y" | "height">[],
|
|
33
|
+
items: Array<Pick<CanvasElement, "height">>,
|
|
34
|
+
options: StackLayoutOptions,
|
|
35
|
+
): Array<{ x: number; y: number }> {
|
|
36
|
+
if (items.length === 0) return []
|
|
37
|
+
|
|
38
|
+
const { startX, startY, gap } = options
|
|
39
|
+
|
|
40
|
+
const maxBottom = existing.reduce(
|
|
41
|
+
(max, el) => Math.max(max, el.y + el.height),
|
|
42
|
+
0,
|
|
43
|
+
)
|
|
44
|
+
const baseY = existing.length > 0 ? maxBottom + gap : startY
|
|
45
|
+
|
|
46
|
+
const positions: Array<{ x: number; y: number }> = []
|
|
47
|
+
let cursorY = baseY
|
|
48
|
+
for (const item of items) {
|
|
49
|
+
positions.push({ x: startX, y: cursorY })
|
|
50
|
+
cursorY += item.height + gap
|
|
51
|
+
}
|
|
52
|
+
return positions
|
|
53
|
+
}
|