@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,42 @@
1
+ {
2
+ "title": "标签",
3
+ "description": "标签管理正在开发中,敬请期待",
4
+ "detail": {
5
+ "title": "标签详情",
6
+ "description": "标签详情页正在开发中"
7
+ },
8
+ "settings": {
9
+ "title": "标签设置",
10
+ "description": "管理你的标签:增删改色、拖拽排序"
11
+ },
12
+ "emptyTitle": "暂无标签",
13
+ "emptyDesc": "创建你的第一个标签来组织 PRD",
14
+ "createTag": "新建标签",
15
+ "editTag": "编辑标签",
16
+ "nameLabel": "名称",
17
+ "colorLabel": "颜色",
18
+ "nameRequired": "请输入名称",
19
+ "nameConflict": "该名称已存在",
20
+ "saveFailed": "保存失败",
21
+ "saving": "保存中...",
22
+ "save": "保存",
23
+ "cancel": "取消",
24
+ "delete": "删除",
25
+ "deleted": "已删除",
26
+ "deleteFailed": "删除失败",
27
+ "updated": "已更新",
28
+ "created": "已创建",
29
+ "sortFailed": "排序失败",
30
+ "confirmDeleteTitle": "删除标签「{{name}}」?",
31
+ "confirmDeleteDesc": "已关联的 PRD 仍会保留此标签,恢复后自动重新可见。",
32
+ "selector": {
33
+ "placeholder": "选择标签",
34
+ "searchPlaceholder": "搜索或创建...",
35
+ "createTag": "创建「{{name}}」"
36
+ },
37
+ "colorPicker": {
38
+ "selectColor": "选择颜色 {{color}}"
39
+ },
40
+ "edit": "编辑",
41
+ "removeTag": "移除 {{name}}"
42
+ }
@@ -0,0 +1,90 @@
1
+ {
2
+ "title": "工作台",
3
+ "list": {
4
+ "title": "工作空间",
5
+ "subtitle": "管理你的项目、团队协作与需求",
6
+ "create": "新建工作空间",
7
+ "emptyTitle": "暂无工作空间",
8
+ "emptyDescription": "创建第一个工作空间,开始团队协作",
9
+ "noDescription": "暂无介绍",
10
+ "projects": "项目",
11
+ "members": "成员"
12
+ },
13
+ "createWorkspace": {
14
+ "title": "新建工作空间",
15
+ "description": "创建一个工作空间,管理项目、需求和团队成员",
16
+ "nameLabel": "工作空间名称 *",
17
+ "namePlaceholder": "输入工作空间名称",
18
+ "nameRequired": "请输入工作空间名称",
19
+ "descriptionLabel": "介绍",
20
+ "descriptionPlaceholder": "简短介绍(可选)",
21
+ "cancel": "取消",
22
+ "creating": "创建中...",
23
+ "create": "创建",
24
+ "createFailed": "创建失败",
25
+ "upgradeTitle": "已达工作区上限",
26
+ "upgradeDescription": "Free 账号最多创建 1 个工作区。升级 Pro 后不限数量,并可解锁全部高级功能。",
27
+ "upgradeAction": "升级 Pro"
28
+ },
29
+ "detail": {
30
+ "noDescription": "暂无介绍",
31
+ "projects": "项目",
32
+ "members": "成员",
33
+ "createProject": "新建项目",
34
+ "emptyProjectsTitle": "暂无项目",
35
+ "emptyProjectsDescription": "在此工作空间创建第一个项目"
36
+ },
37
+ "settings": {
38
+ "back": "返回工作空间",
39
+ "title": "工作空间设置",
40
+ "basicInfo": "基本信息",
41
+ "icon": "工作空间图标",
42
+ "nameLabel": "工作空间名称 *",
43
+ "nameRequired": "请输入工作空间名称",
44
+ "descriptionLabel": "介绍",
45
+ "descriptionPlaceholder": "简短介绍工作空间目标与范围",
46
+ "saveChanges": "保存修改",
47
+ "updateSuccess": "工作空间已更新",
48
+ "saveFailed": "保存失败",
49
+ "members": "成员管理",
50
+ "inviteEmailPlaceholder": "输入成员邮箱",
51
+ "inviteEmailRequired": "请输入邮箱",
52
+ "inviteSuccess": "邀请已发送",
53
+ "inviteFailed": "邀请失败",
54
+ "inviteLoading": "邀请中...",
55
+ "invite": "邀请",
56
+ "seatsExceeded": "Pro 计划最多 {{max}} 位成员(当前 {{current}}/{{max}})。请联系工作空间所有者升级 Enterprise。",
57
+ "memberRemoved": "成员已移除",
58
+ "removeFailed": "移除失败",
59
+ "roleUpdated": "角色已更新",
60
+ "operationFailed": "操作失败",
61
+ "workspaceDeleted": "工作空间已删除",
62
+ "deleteFailed": "删除失败",
63
+ "roles": {
64
+ "owner": "所有者",
65
+ "admin": "管理员",
66
+ "member": "成员",
67
+ "viewer": "访客"
68
+ },
69
+ "unknownUser": "未知用户",
70
+ "self": "(你)",
71
+ "pending": "待接受",
72
+ "dangerZone": "危险操作",
73
+ "deleteDescription": "删除工作空间将永久移除工作空间及其所有成员关系,此操作不可恢复。",
74
+ "hasProjects": "此工作空间还有 {{count}} 个项目,请先选择处理方式",
75
+ "deleteClear": "清空项目并删除工作空间",
76
+ "deleteTransfer": "转移项目后删除工作空间",
77
+ "deleteWorkspace": "删除工作空间",
78
+ "confirmDelete": "确认删除",
79
+ "confirmDeleteDescription": "此操作将永久删除「{{name}}」及其所有关联数据,无法恢复。",
80
+ "projectHandling": "处理项目方式",
81
+ "clearProjects": "清空项目(不可恢复)",
82
+ "clearProjectsDesc": "工作空间下所有项目将被永久删除。",
83
+ "transferProjects": "转移到其他工作空间",
84
+ "transferProjectsDesc": "选择目标工作空间,保留项目数据。",
85
+ "confirmRemoveMember": "确认移除成员",
86
+ "confirmRemoveMemberDesc": "确定要移除该成员吗?该成员将立即失去工作空间访问权限。",
87
+ "cancel": "取消",
88
+ "confirmRemove": "确认移除"
89
+ }
90
+ }
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import i18next from 'i18next'
3
+ import { initReactI18next } from 'react-i18next'
4
+ import { resources, fallbackLng, defaultNs, type AppLocale } from './config'
5
+
6
+ export function createServerI18n(locale: AppLocale) {
7
+ const i18n = i18next.createInstance()
8
+ i18n.use(initReactI18next)
9
+ i18n.init({
10
+ lng: locale,
11
+ fallbackLng,
12
+ defaultNS: defaultNs,
13
+ resources,
14
+ interpolation: { escapeValue: false },
15
+ react: { useSuspense: false },
16
+ })
17
+ return i18n
18
+ }
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ import type { i18n as I18nInstance } from 'i18next'
3
+ import type { AppLocale, NamespaceKey } from './config'
4
+
5
+ export type { AppLocale, NamespaceKey, I18nInstance }
package/src/index.ts ADDED
@@ -0,0 +1,90 @@
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 { moduleRegistry } from "./modules/registry"
19
+ export { registerDefaultModules } from "./modules/register-defaults"
20
+
21
+ // 全站虚拟化/分页门槛常量(web / admin / desktop 统一)
22
+ export * from "./constants/perf"
23
+
24
+ // AI 服务商元数据(类型、常量、种子数据)
25
+ export * from "./modules/ai-providers"
26
+
27
+ // 第三方依赖版本管理(类型、常量、种子数据)
28
+ export * from "./modules/dependencies"
29
+
30
+ export {
31
+ resolveIpLocation,
32
+ getClientIp,
33
+ } from "./infrastructure/ip-location"
34
+ export {
35
+ parseUserAgent,
36
+ formatParsedUA,
37
+ type ParsedUA,
38
+ } from "./infrastructure/ua-parser"
39
+
40
+ export type {
41
+ AllBlueModule,
42
+ EntityDef,
43
+ AICommandDef,
44
+ AICommandContext,
45
+ AICommandResult,
46
+ AICommandHandler,
47
+ ReferenceDisplayFn,
48
+ } from "./interfaces/module"
49
+
50
+ export type {
51
+ Reference,
52
+ ResolvedRef,
53
+ RefDisplay,
54
+ SearchResult,
55
+ } from "./interfaces/references"
56
+
57
+ export type {
58
+ AiAdapter,
59
+ CLIAgentInfo,
60
+ } from "./interfaces/ai"
61
+
62
+ export type {
63
+ EntityUpdateEvent,
64
+ RealtimeTransport,
65
+ } from "./interfaces/transport"
66
+
67
+ export {
68
+ FeatureRegistry,
69
+ FEATURE_KEYS,
70
+ OSS_BASE_FEATURES,
71
+ isOssFeature,
72
+ } from "./modules/billing/feature-registry"
73
+ export type { FeatureMeta, FeatureKey } from "./modules/billing/feature-registry"
74
+
75
+ export { PlanFeaturesSchema } from "./modules/billing/plan-features-schema"
76
+ export type { PlanFeatures } from "./modules/billing/plan-features-schema"
77
+
78
+ export {
79
+ RESOURCE_KEYS,
80
+ ResourceRegistry,
81
+ isResourceDefined,
82
+ } from "./modules/billing/resource-registry"
83
+ export type {
84
+ ResourceKey,
85
+ ResourceMeta,
86
+ ResourceSnapshot,
87
+ ResourceSnapshots,
88
+ } from "./modules/billing/resource-registry"
89
+
90
+ export type { AvatarPart, AvatarConfig, ShapeType } from "./interfaces/avatar"
@@ -0,0 +1,59 @@
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 { AICommandContext } from "../../interfaces/module"
19
+ import type { CLIAgentInfo } from "../../interfaces/ai"
20
+
21
+ let activeAgentId: string | null = null
22
+
23
+ export function getActiveAgent(): string | null {
24
+ return activeAgentId
25
+ }
26
+
27
+ export function switchAgent(agentId: string): void {
28
+ activeAgentId = agentId
29
+ }
30
+
31
+ export async function executeViaAgent(
32
+ agent: CLIAgentInfo,
33
+ prompt: string,
34
+ _ctx: AICommandContext
35
+ ): Promise<string> {
36
+ if (agent.id === "openai-api") {
37
+ const response = await fetch(`${process.env.AI_BASE_URL || "https://api.openai.com/v1"}/chat/completions`, {
38
+ method: "POST",
39
+ headers: {
40
+ "Content-Type": "application/json",
41
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
42
+ },
43
+ body: JSON.stringify({
44
+ model: process.env.AI_MODEL || "gpt-4o-mini",
45
+ messages: [{ role: "user", content: prompt }],
46
+ temperature: 0.7,
47
+ }),
48
+ })
49
+ const data = await response.json() as { choices: { message: { content: string } }[] }
50
+ return data.choices[0]?.message?.content || ""
51
+ }
52
+
53
+ const { execSync } = await import("child_process")
54
+ const result = execSync(`${agent.binary} -p "${prompt.replace(/"/g, '\\"')}"`, {
55
+ timeout: 60000,
56
+ encoding: "utf-8",
57
+ })
58
+ return result.trim()
59
+ }
@@ -0,0 +1,64 @@
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 { CLIAgentInfo } from "../../interfaces/ai"
19
+
20
+ const KNOWN_AGENTS: { id: string; name: string; binary: string }[] = [
21
+ { id: "claude-code", name: "Claude Code", binary: "claude" },
22
+ { id: "claude-cli", name: "Claude CLI", binary: "claude" },
23
+ { id: "copilot-cli", name: "GitHub Copilot CLI", binary: "github-copilot-cli" },
24
+ { id: "ollama", name: "Ollama", binary: "ollama" },
25
+ { id: "llama-cpp", name: "llama.cpp", binary: "llama-cli" },
26
+ { id: "open-interpreter", name: "Open Interpreter", binary: "interpreter" },
27
+ { id: "aider", name: "Aider", binary: "aider" },
28
+ { id: "continue", name: "Continue", binary: "continue" },
29
+ { id: "codex-cli", name: "Codex CLI", binary: "codex" },
30
+ { id: "swe-agent", name: "SWE-agent", binary: "sweagent" },
31
+ { id: "openhands", name: "OpenHands", binary: "openhands" },
32
+ { id: "taskmaster", name: "TaskMaster", binary: "taskmaster" },
33
+ { id: "gpt-engineer", name: "GPT Engineer", binary: "gptengineer" },
34
+ { id: "mentat", name: "Mentat", binary: "mentat" },
35
+ { id: "sweep", name: "Sweep", binary: "sweep" },
36
+ { id: "mcp-cli", name: "MCP CLI", binary: "mcp" },
37
+ ]
38
+
39
+ export async function detectAgents(): Promise<CLIAgentInfo[]> {
40
+ const { execSync } = await import("child_process")
41
+ return KNOWN_AGENTS.map(({ id, name, binary }) => {
42
+ try {
43
+ const version = execSync(`${binary} --version 2>/dev/null || ${binary} version 2>/dev/null`, {
44
+ timeout: 3000,
45
+ encoding: "utf-8",
46
+ }).trim().slice(0, 50)
47
+ return { id, name, binary, version, available: true }
48
+ } catch {
49
+ return { id, name, binary, version: "", available: false }
50
+ }
51
+ })
52
+ }
53
+
54
+ export function getApiAgent(): CLIAgentInfo | undefined {
55
+ const key = process.env.OPENAI_API_KEY
56
+ if (!key || key === "sk-your-api-key-here") return undefined
57
+ return {
58
+ id: "openai-api",
59
+ name: `OpenAI (${process.env.AI_MODEL || "gpt-4o-mini"})`,
60
+ binary: "api",
61
+ version: "v1",
62
+ available: true,
63
+ }
64
+ }
@@ -0,0 +1,76 @@
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
+ * IP 地理位置查询工具(服务端专用)
20
+ *
21
+ * - 开发环境 / 内网 IP:返回「本地」
22
+ * - 生产环境:使用 ip-api.com 免费查询(中文,每分钟 45 请求限制)
23
+ * 失败时返回「未知」,不阻塞登录流程
24
+ */
25
+
26
+ const PRIVATE_IP_PATTERNS =
27
+ /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|::1$|fc00:|fd00:|fe80:|169\.254\.|localhost)/i
28
+
29
+ export async function resolveIpLocation(
30
+ ip: string | null | undefined,
31
+ ): Promise<string> {
32
+ if (!ip) return "Unknown"
33
+
34
+ // 开发环境 / 内网 IP
35
+ if (PRIVATE_IP_PATTERNS.test(ip)) return "Local"
36
+
37
+ // 生产环境:ip-api.com 免费查询
38
+ try {
39
+ const controller = new AbortController()
40
+ const timeout = setTimeout(() => controller.abort(), 3000)
41
+ const res = await fetch(
42
+ `http://ip-api.com/json/${encodeURIComponent(ip)}?fields=status,country,regionName,city&lang=zh-CN`,
43
+ { signal: controller.signal },
44
+ )
45
+ clearTimeout(timeout)
46
+ if (!res.ok) return "Unknown"
47
+ const data = (await res.json()) as {
48
+ status: string
49
+ country?: string
50
+ regionName?: string
51
+ city?: string
52
+ }
53
+ if (data.status !== "success") return "Unknown"
54
+ const parts = [data.city, data.regionName, data.country].filter(Boolean)
55
+ return parts.length > 0 ? parts.join(", ") : "Unknown"
56
+ } catch {
57
+ return "Unknown"
58
+ }
59
+ }
60
+
61
+ /**
62
+ * 从 Request 中提取客户端真实 IP
63
+ * 优先级:X-Forwarded-For > X-Real-IP > CF-Connecting-IP
64
+ */
65
+ export function getClientIp(request: Request): string | null {
66
+ const xff = request.headers.get("x-forwarded-for")
67
+ if (xff) {
68
+ const first = xff.split(",")[0]?.trim()
69
+ if (first) return first
70
+ }
71
+ const xri = request.headers.get("x-real-ip")
72
+ if (xri) return xri.trim()
73
+ const cfip = request.headers.get("cf-connecting-ip")
74
+ if (cfip) return cfip.trim()
75
+ return null
76
+ }
@@ -0,0 +1,104 @@
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 { getPrismaSeam } from "../../server-seam"
19
+ import type { Reference, ResolvedRef, RefDisplay, SearchResult } from "../../interfaces/references"
20
+ import { moduleRegistry } from "../../modules/registry"
21
+
22
+ async function resolveDisplay(
23
+ targetModule: string,
24
+ targetEntityId: string,
25
+ entityType?: string
26
+ ): Promise<RefDisplay> {
27
+ const mod = moduleRegistry.getModule(targetModule)
28
+ if (!mod) return { title: `[${targetModule}:${targetEntityId}]` }
29
+
30
+ const entity = entityType
31
+ ? mod.entities.find(e => e.type === entityType)
32
+ : mod.entities[0]
33
+ if (!entity) return { title: `[${targetModule}:${targetEntityId}]` }
34
+
35
+ try {
36
+ const fields = [...entity.displayFields, "id"].join(", ")
37
+ const rows = await getPrismaSeam().$queryRawUnsafe<Record<string, unknown>>(
38
+ `SELECT ${fields} FROM \`${entity.table}\` WHERE id = ?`,
39
+ targetEntityId
40
+ )
41
+ if (!rows.length) return { title: `Deleted ${entity.type}`, status: "deleted" }
42
+ return mod.referenceDisplay(entity.type, rows[0])
43
+ } catch {
44
+ return { title: `[${targetEntityId}]` }
45
+ }
46
+ }
47
+
48
+ export async function createReference(ref: Omit<Reference, "id">): Promise<Reference> {
49
+ const seam = getPrismaSeam()
50
+ return seam.reference.create({ data: ref }) as unknown as Reference
51
+ }
52
+
53
+ export async function resolveReferences(
54
+ sourceModule: string,
55
+ sourceEntityId: string
56
+ ): Promise<ResolvedRef[]> {
57
+ const seam = getPrismaSeam()
58
+ const refs = await seam.reference.findMany({
59
+ where: { sourceModule, sourceEntityId },
60
+ }) as unknown as Reference[]
61
+
62
+ return Promise.all(
63
+ refs.map(async (ref) => {
64
+ const display = await resolveDisplay(ref.targetModule, ref.targetEntityId)
65
+ return { ...ref, display }
66
+ })
67
+ )
68
+ }
69
+
70
+ export async function searchReferences(
71
+ query: string,
72
+ types?: string[]
73
+ ): Promise<SearchResult[]> {
74
+ const searchConfigs = moduleRegistry.getSearchFields()
75
+ const filtered = types
76
+ ? searchConfigs.filter(c => types.includes(c.module) || types.includes(c.type))
77
+ : searchConfigs
78
+
79
+ const results: SearchResult[] = []
80
+ for (const config of filtered) {
81
+ const mod = moduleRegistry.getModule(config.module)
82
+ if (!mod) continue
83
+ const searchField = config.fields[0]
84
+ const seam = getPrismaSeam()
85
+ try {
86
+ const rows = await seam.$queryRawUnsafe<Record<string, unknown>>(
87
+ `SELECT id, ${config.fields.join(", ")} FROM \`${mod.entities[0].table}\` WHERE \`${searchField}\` LIKE ? LIMIT 20`,
88
+ `%${query}%`
89
+ )
90
+ for (const row of rows) {
91
+ const display = mod.referenceDisplay(config.type, row)
92
+ results.push({
93
+ id: row.id as string,
94
+ module: config.module,
95
+ type: config.type,
96
+ title: display.title,
97
+ status: display.status,
98
+ url: display.url,
99
+ })
100
+ }
101
+ } catch { /* skip tables that don't exist yet */ }
102
+ }
103
+ return results
104
+ }
@@ -0,0 +1,107 @@
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
+ * User-Agent 解析工具(轻量无依赖实现)
20
+ * 从 UA 字符串提取浏览器、操作系统、设备类型
21
+ */
22
+
23
+ export interface ParsedUA {
24
+ /** 浏览器名称 + 主版本号,如 "Chrome 120"、"Edge 119" */
25
+ browser: string
26
+ /** 操作系统 + 主版本号,如 "Windows 10/11"、"macOS 14"、"iOS 17" */
27
+ os: string
28
+ /** 设备类型:桌面 / iPhone / iPad / Android 手机 / Android 平板 */
29
+ device: string
30
+ }
31
+
32
+ export function parseUserAgent(ua: string | null | undefined): ParsedUA {
33
+ if (!ua) {
34
+ return { browser: "Unknown", os: "Unknown", device: "Unknown" }
35
+ }
36
+
37
+ // ===== 浏览器解析(注意优先级:Edge/Opera 等基于 Chromium 的 UA 也含 Chrome)=====
38
+ let browser = "Unknown"
39
+ if (ua.includes("Edg/")) {
40
+ const m = ua.match(/Edg\/(\d+)/)
41
+ browser = `Edge${m ? " " + m[1] : ""}`
42
+ } else if (ua.includes("OPR/") || ua.includes("Opera")) {
43
+ const m = ua.match(/(?:OPR|Opera)\/(\d+)/)
44
+ browser = `Opera${m ? " " + m[1] : ""}`
45
+ } else if (ua.includes("Firefox/")) {
46
+ const m = ua.match(/Firefox\/(\d+)/)
47
+ browser = `Firefox${m ? " " + m[1] : ""}`
48
+ } else if (ua.includes("Chrome/")) {
49
+ // Chrome / Chromium / Brave / Vivaldi 等,统一显示为 Chrome
50
+ const m = ua.match(/Chrome\/(\d+)/)
51
+ browser = `Chrome${m ? " " + m[1] : ""}`
52
+ } else if (ua.includes("Safari/") && !ua.includes("Chrome")) {
53
+ const m = ua.match(/Version\/(\d+)/)
54
+ browser = `Safari${m ? " " + m[1] : ""}`
55
+ }
56
+
57
+ // ===== 操作系统解析 =====
58
+ let os = "Unknown"
59
+ if (ua.includes("Windows NT 10")) os = "Windows 10/11"
60
+ else if (ua.includes("Windows NT 6.3")) os = "Windows 8.1"
61
+ else if (ua.includes("Windows NT 6.2")) os = "Windows 8"
62
+ else if (ua.includes("Windows NT 6.1")) os = "Windows 7"
63
+ else if (ua.includes("Windows")) os = "Windows"
64
+ else if (ua.includes("Mac OS X") || ua.includes("Macintosh")) {
65
+ // 苹果从 macOS 11 Big Sur 起冻结 UA 为 10_15_7(包括最新的 macOS 26),
66
+ // UA 无法区分真实版本,统一显示 "macOS";仅 10_14 及以下是真实旧版本
67
+ const frozen = /Mac OS X 10_15/.test(ua)
68
+ if (frozen) {
69
+ os = "macOS"
70
+ } else {
71
+ const m = ua.match(/Mac OS X (\d+)(?:[_.](\d+))?/)
72
+ if (m) {
73
+ const major = parseInt(m[1], 10)
74
+ os = major >= 11 ? `macOS ${major}` : `Mac OS X ${m[1]}.${m[2] ?? "0"}`
75
+ } else {
76
+ os = "macOS"
77
+ }
78
+ }
79
+ } else if (ua.includes("Android")) {
80
+ const m = ua.match(/Android ([\d.]+)/)
81
+ os = `Android${m ? " " + m[1] : ""}`
82
+ } else if (/iPhone|iPad|iPod/.test(ua)) {
83
+ const m = ua.match(/OS (\d+)[_ ](?:\d+)?/)
84
+ os = `iOS${m ? " " + m[1] : ""}`
85
+ } else if (ua.includes("Linux")) {
86
+ os = "Linux"
87
+ }
88
+
89
+ // ===== 设备类型解析 =====
90
+ let device = "Desktop"
91
+ if (ua.includes("iPad")) device = "iPad"
92
+ else if (ua.includes("iPhone")) device = "iPhone"
93
+ else if (ua.includes("Android") && ua.includes("Mobile"))
94
+ device = "Android Phone"
95
+ else if (ua.includes("Android")) device = "Android Tablet"
96
+
97
+ return { browser, os, device }
98
+ }
99
+
100
+ /**
101
+ * 将 ParsedUA 拼接为可读字符串,如 "Chrome 120 / Windows 10/11 / 桌面"
102
+ */
103
+ export function formatParsedUA(parsed: ParsedUA): string {
104
+ return [parsed.browser, parsed.os, parsed.device]
105
+ .filter((s) => s && s !== "Unknown")
106
+ .join(" / ")
107
+ }