@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.
Files changed (95) hide show
  1. package/package.json +117 -0
  2. package/src/constants/perf.ts +21 -0
  3. package/src/i18n/client.ts +32 -0
  4. package/src/i18n/config.ts +104 -0
  5. package/src/i18n/detect.ts +43 -0
  6. package/src/i18n/locales/en/agents.json +134 -0
  7. package/src/i18n/locales/en/apps.json +6 -0
  8. package/src/i18n/locales/en/auth.json +88 -0
  9. package/src/i18n/locales/en/billing.json +78 -0
  10. package/src/i18n/locales/en/chat.json +194 -0
  11. package/src/i18n/locales/en/common.json +203 -0
  12. package/src/i18n/locales/en/dashboard.json +38 -0
  13. package/src/i18n/locales/en/design.json +70 -0
  14. package/src/i18n/locales/en/desktop.json +35 -0
  15. package/src/i18n/locales/en/display.json +37 -0
  16. package/src/i18n/locales/en/editor.json +304 -0
  17. package/src/i18n/locales/en/enterprise.json +1 -0
  18. package/src/i18n/locales/en/invite.json +11 -0
  19. package/src/i18n/locales/en/knowledge.json +451 -0
  20. package/src/i18n/locales/en/mindmap.json +28 -0
  21. package/src/i18n/locales/en/prd.json +43 -0
  22. package/src/i18n/locales/en/projects.json +133 -0
  23. package/src/i18n/locales/en/settings.json +375 -0
  24. package/src/i18n/locales/en/share.json +20 -0
  25. package/src/i18n/locales/en/story.json +785 -0
  26. package/src/i18n/locales/en/tags.json +42 -0
  27. package/src/i18n/locales/en/workspace.json +90 -0
  28. package/src/i18n/locales/zh/agents.json +134 -0
  29. package/src/i18n/locales/zh/apps.json +6 -0
  30. package/src/i18n/locales/zh/auth.json +88 -0
  31. package/src/i18n/locales/zh/billing.json +78 -0
  32. package/src/i18n/locales/zh/chat.json +194 -0
  33. package/src/i18n/locales/zh/common.json +203 -0
  34. package/src/i18n/locales/zh/dashboard.json +38 -0
  35. package/src/i18n/locales/zh/design.json +70 -0
  36. package/src/i18n/locales/zh/desktop.json +35 -0
  37. package/src/i18n/locales/zh/display.json +37 -0
  38. package/src/i18n/locales/zh/editor.json +304 -0
  39. package/src/i18n/locales/zh/enterprise.json +1 -0
  40. package/src/i18n/locales/zh/invite.json +11 -0
  41. package/src/i18n/locales/zh/knowledge.json +451 -0
  42. package/src/i18n/locales/zh/mindmap.json +28 -0
  43. package/src/i18n/locales/zh/prd.json +43 -0
  44. package/src/i18n/locales/zh/projects.json +133 -0
  45. package/src/i18n/locales/zh/settings.json +375 -0
  46. package/src/i18n/locales/zh/share.json +20 -0
  47. package/src/i18n/locales/zh/story.json +785 -0
  48. package/src/i18n/locales/zh/tags.json +42 -0
  49. package/src/i18n/locales/zh/workspace.json +90 -0
  50. package/src/i18n/server.ts +18 -0
  51. package/src/i18n/types.ts +5 -0
  52. package/src/index.ts +90 -0
  53. package/src/infrastructure/ai/adapter.ts +59 -0
  54. package/src/infrastructure/ai/cli-detector.ts +64 -0
  55. package/src/infrastructure/ip-location.ts +76 -0
  56. package/src/infrastructure/references/engine.ts +104 -0
  57. package/src/infrastructure/ua-parser.ts +107 -0
  58. package/src/interfaces/ai.ts +38 -0
  59. package/src/interfaces/avatar.ts +22 -0
  60. package/src/interfaces/module.ts +60 -0
  61. package/src/interfaces/references.ts +44 -0
  62. package/src/interfaces/transport.ts +26 -0
  63. package/src/lib/annotation-template.ts +33 -0
  64. package/src/lib/comment-utils.ts +47 -0
  65. package/src/lib/datetime.ts +115 -0
  66. package/src/lib/desktop-detect.ts +27 -0
  67. package/src/lib/outline-utils.ts +57 -0
  68. package/src/lib/pacer.ts +96 -0
  69. package/src/lib/prd/stack-layout.ts +53 -0
  70. package/src/lib/prd-tree-utils.ts +94 -0
  71. package/src/lib/prototype-registry.ts +24 -0
  72. package/src/lib/query-client-seam.ts +14 -0
  73. package/src/lib/share-events.ts +78 -0
  74. package/src/lib/sidebar-utils.ts +21 -0
  75. package/src/lib/stores/prd-canvas-store.ts +69 -0
  76. package/src/lib/tag-colors.ts +35 -0
  77. package/src/lib/time-format.ts +50 -0
  78. package/src/modules/ai-providers/constants.ts +79 -0
  79. package/src/modules/ai-providers/index.ts +21 -0
  80. package/src/modules/ai-providers/seed-data.ts +222 -0
  81. package/src/modules/ai-providers/types.ts +95 -0
  82. package/src/modules/billing/feature-registry.ts +63 -0
  83. package/src/modules/billing/plan-features-schema.ts +11 -0
  84. package/src/modules/billing/resource-registry.ts +80 -0
  85. package/src/modules/definitions/plugin.module.ts +38 -0
  86. package/src/modules/definitions/prd.module.ts +45 -0
  87. package/src/modules/definitions/prototype.module.ts +38 -0
  88. package/src/modules/dependencies/index.ts +186 -0
  89. package/src/modules/register-defaults.ts +27 -0
  90. package/src/modules/registry.ts +53 -0
  91. package/src/server-seam.ts +51 -0
  92. package/src/types/outline.ts +26 -0
  93. package/src/types/prd.ts +90 -0
  94. package/src/types/prototype.ts +12 -0
  95. 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
+ }
@@ -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
+ }