@allbluecn/admin-app 0.1.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 (276) hide show
  1. package/package.json +86 -0
  2. package/src/client.tsx +31 -0
  3. package/src/components/admin/admin-user-menu.tsx +90 -0
  4. package/src/components/admin/developer-log.tsx +356 -0
  5. package/src/components/ai-provider/model-list-editor.tsx +147 -0
  6. package/src/components/ai-provider/provider-form.tsx +434 -0
  7. package/src/components/ai-provider/provider-icon.tsx +43 -0
  8. package/src/components/ai-provider/provider-logo.tsx +87 -0
  9. package/src/components/ai-provider/provider-table.tsx +258 -0
  10. package/src/components/auth/admin-login-form.tsx +214 -0
  11. package/src/components/auth/auth-brand-panel.tsx +60 -0
  12. package/src/components/auth/auth-header.tsx +38 -0
  13. package/src/components/auth/auth-layout.tsx +66 -0
  14. package/src/components/auth/auth-providers.tsx +34 -0
  15. package/src/components/auth/icon-input.tsx +46 -0
  16. package/src/components/auth/oauth-section.tsx +51 -0
  17. package/src/components/auth/password-input.tsx +67 -0
  18. package/src/components/common/page-header.tsx +42 -0
  19. package/src/components/common/placeholder-page.tsx +42 -0
  20. package/src/components/component/component-form-dialog.tsx +348 -0
  21. package/src/components/component/component-import-dialog.tsx +235 -0
  22. package/src/components/component/component-table.tsx +153 -0
  23. package/src/components/data-table/data-table-pagination.tsx +114 -0
  24. package/src/components/data-table/data-table.tsx +218 -0
  25. package/src/components/data-table/table-detail-layout.tsx +99 -0
  26. package/src/components/dependency/changelog-dialog.tsx +108 -0
  27. package/src/components/dependency/columns.tsx +232 -0
  28. package/src/components/dependency/dependency-table.tsx +368 -0
  29. package/src/components/dependency/edit-dialog.tsx +200 -0
  30. package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
  31. package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
  32. package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
  33. package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
  34. package/src/components/layout/sidebar-02/index.tsx +33 -0
  35. package/src/components/layout/sidebar-02/logo.tsx +40 -0
  36. package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
  37. package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
  38. package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
  39. package/src/components/magicui/animated-theme-toggler.tsx +272 -0
  40. package/src/components/magicui/rainbow-button.tsx +78 -0
  41. package/src/components/market/columns.tsx +151 -0
  42. package/src/components/market/market-app-form.tsx +237 -0
  43. package/src/components/market/market-app-list.tsx +156 -0
  44. package/src/components/monitoring/error-list.tsx +143 -0
  45. package/src/components/monitoring/health-cards.tsx +152 -0
  46. package/src/components/monitoring/metrics-stats.tsx +184 -0
  47. package/src/components/monitoring/request-log-table.tsx +179 -0
  48. package/src/components/providers/theme-toggle.tsx +59 -0
  49. package/src/components/settings/avatar/avatar-config.ts +46 -0
  50. package/src/components/ui/alert-dialog.tsx +216 -0
  51. package/src/components/ui/alert.tsx +76 -0
  52. package/src/components/ui/avatar.tsx +129 -0
  53. package/src/components/ui/badge.tsx +66 -0
  54. package/src/components/ui/button.tsx +84 -0
  55. package/src/components/ui/card.tsx +120 -0
  56. package/src/components/ui/checkbox.tsx +50 -0
  57. package/src/components/ui/collapsible.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +185 -0
  59. package/src/components/ui/dropdown-menu.tsx +286 -0
  60. package/src/components/ui/field.tsx +255 -0
  61. package/src/components/ui/input-group.tsx +171 -0
  62. package/src/components/ui/input.tsx +36 -0
  63. package/src/components/ui/label.tsx +41 -0
  64. package/src/components/ui/pagination.tsx +146 -0
  65. package/src/components/ui/select.tsx +209 -0
  66. package/src/components/ui/separator.tsx +45 -0
  67. package/src/components/ui/sheet.tsx +164 -0
  68. package/src/components/ui/sidebar.tsx +719 -0
  69. package/src/components/ui/skeleton.tsx +30 -0
  70. package/src/components/ui/sonner.tsx +37 -0
  71. package/src/components/ui/switch.tsx +48 -0
  72. package/src/components/ui/table.tsx +133 -0
  73. package/src/components/ui/textarea.tsx +35 -0
  74. package/src/components/ui/tooltip.tsx +74 -0
  75. package/src/components/user/columns.tsx +153 -0
  76. package/src/components/user/settings-activity-log.tsx +358 -0
  77. package/src/components/user/settings-profile/constants.ts +42 -0
  78. package/src/components/user/settings-profile/types.ts +47 -0
  79. package/src/components/user/settings-profile.tsx +664 -0
  80. package/src/components/user/user-create-dialog.tsx +137 -0
  81. package/src/components/user/user-filters.tsx +117 -0
  82. package/src/hooks/use-mobile.ts +36 -0
  83. package/src/instrument.client.ts +34 -0
  84. package/src/lib/anyhub/capabilities.ts +59 -0
  85. package/src/lib/anyhub/types.ts +43 -0
  86. package/src/lib/auth-client.ts +83 -0
  87. package/src/lib/auth-utils.ts +24 -0
  88. package/src/lib/auth.config.ts +264 -0
  89. package/src/lib/avatar.ts +41 -0
  90. package/src/lib/i18n/client.ts +32 -0
  91. package/src/lib/i18n/config.ts +40 -0
  92. package/src/lib/i18n/detect.ts +43 -0
  93. package/src/lib/i18n/locales/en/admin-ai.json +128 -0
  94. package/src/lib/i18n/locales/en/admin-billing.json +56 -0
  95. package/src/lib/i18n/locales/en/admin-users.json +88 -0
  96. package/src/lib/i18n/locales/en/common.json +877 -0
  97. package/src/lib/i18n/locales/en/enterprise.json +208 -0
  98. package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
  99. package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
  100. package/src/lib/i18n/locales/zh/admin-users.json +88 -0
  101. package/src/lib/i18n/locales/zh/common.json +877 -0
  102. package/src/lib/i18n/locales/zh/enterprise.json +208 -0
  103. package/src/lib/i18n/server.ts +18 -0
  104. package/src/lib/i18n/types.ts +5 -0
  105. package/src/lib/monitoring-benchmarks.ts +85 -0
  106. package/src/lib/monitoring-descriptions.ts +78 -0
  107. package/src/lib/prisma.ts +18 -0
  108. package/src/lib/router-context.ts +38 -0
  109. package/src/lib/session.ts +38 -0
  110. package/src/lib/utils.ts +23 -0
  111. package/src/providers/index.tsx +45 -0
  112. package/src/routeTree.gen.ts +1199 -0
  113. package/src/router.tsx +51 -0
  114. package/src/routes/__root.tsx +170 -0
  115. package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
  116. package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
  117. package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
  118. package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
  119. package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
  120. package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
  121. package/src/routes/_nav/ai-providers/route.tsx +31 -0
  122. package/src/routes/_nav/apps/route.lazy.tsx +30 -0
  123. package/src/routes/_nav/apps/route.tsx +20 -0
  124. package/src/routes/_nav/audit/route.lazy.tsx +100 -0
  125. package/src/routes/_nav/audit/route.tsx +25 -0
  126. package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
  127. package/src/routes/_nav/billing/invoices/route.tsx +25 -0
  128. package/src/routes/_nav/billing/route.lazy.tsx +48 -0
  129. package/src/routes/_nav/billing/route.tsx +27 -0
  130. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
  131. package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
  132. package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
  133. package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
  134. package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
  135. package/src/routes/_nav/compliance/route.tsx +25 -0
  136. package/src/routes/_nav/components/route.lazy.tsx +184 -0
  137. package/src/routes/_nav/components/route.tsx +40 -0
  138. package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
  139. package/src/routes/_nav/dashboard/route.tsx +20 -0
  140. package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
  141. package/src/routes/_nav/dependencies/route.tsx +32 -0
  142. package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
  143. package/src/routes/_nav/developer-logs/route.tsx +14 -0
  144. package/src/routes/_nav/docs/route.lazy.tsx +30 -0
  145. package/src/routes/_nav/docs/route.tsx +20 -0
  146. package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
  147. package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
  148. package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
  149. package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
  150. package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
  151. package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
  152. package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
  153. package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
  154. package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
  155. package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
  156. package/src/routes/_nav/licenses/route.tsx +25 -0
  157. package/src/routes/_nav/market/route.lazy.tsx +31 -0
  158. package/src/routes/_nav/market/route.tsx +20 -0
  159. package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
  160. package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
  161. package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
  162. package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
  163. package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
  164. package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
  165. package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
  166. package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
  167. package/src/routes/_nav/monitoring/route.tsx +20 -0
  168. package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
  169. package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
  170. package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
  171. package/src/routes/_nav/permissions/route.tsx +26 -0
  172. package/src/routes/_nav/route.lazy.tsx +31 -0
  173. package/src/routes/_nav/route.tsx +34 -0
  174. package/src/routes/_nav/settings/route.lazy.tsx +30 -0
  175. package/src/routes/_nav/settings/route.tsx +20 -0
  176. package/src/routes/_nav/sso/route.lazy.tsx +145 -0
  177. package/src/routes/_nav/sso/route.tsx +25 -0
  178. package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
  179. package/src/routes/_nav/users/$userId/route.tsx +27 -0
  180. package/src/routes/_nav/users/-components/users-table.tsx +38 -0
  181. package/src/routes/_nav/users/route.lazy.tsx +74 -0
  182. package/src/routes/_nav/users/route.tsx +43 -0
  183. package/src/routes/api/compliance-download/$jobId.ts +35 -0
  184. package/src/routes/api/observability/errors.ts +41 -0
  185. package/src/routes/api/observability/health.ts +30 -0
  186. package/src/routes/api.auth.$.ts +31 -0
  187. package/src/routes/api.sentry-test.ts +33 -0
  188. package/src/routes/index/route.lazy.tsx +22 -0
  189. package/src/routes/index/route.tsx +24 -0
  190. package/src/routes/login/route.lazy.tsx +46 -0
  191. package/src/routes/login/route.tsx +28 -0
  192. package/src/routes/sentry-tunnel.ts +61 -0
  193. package/src/routes/setup/route.lazy.tsx +94 -0
  194. package/src/routes/setup/route.tsx +28 -0
  195. package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
  196. package/src/server/__tests__/licenses-issue.test.ts +98 -0
  197. package/src/server/admin-ai-alerts.ts +177 -0
  198. package/src/server/admin-ai-usage.ts +134 -0
  199. package/src/server/auth-helpers.ts +51 -0
  200. package/src/server/entitlements.ts +24 -0
  201. package/src/server/knowledge-public/canonical-id.ts +55 -0
  202. package/src/server/knowledge-public/github-sync.ts +86 -0
  203. package/src/server/license-issue.ts +78 -0
  204. package/src/server/middlewares/request.ts +26 -0
  205. package/src/server/observability/debug-middleware.ts +39 -0
  206. package/src/server/observability/error-reporter.ts +99 -0
  207. package/src/server/observability/health-check.ts +140 -0
  208. package/src/server/observability/index.ts +33 -0
  209. package/src/server/observability/logger.ts +49 -0
  210. package/src/server/observability/metrics.ts +105 -0
  211. package/src/server/observability/request-middleware.ts +125 -0
  212. package/src/server/observability/timing.ts +42 -0
  213. package/src/server/pro-services.ts +32 -0
  214. package/src/server/serverFns/admin-billing.ts +94 -0
  215. package/src/server/serverFns/ai-provider/create.ts +92 -0
  216. package/src/server/serverFns/ai-provider/delete.ts +42 -0
  217. package/src/server/serverFns/ai-provider/get.ts +35 -0
  218. package/src/server/serverFns/ai-provider/list.ts +92 -0
  219. package/src/server/serverFns/ai-provider/update.ts +73 -0
  220. package/src/server/serverFns/anyhub/market.ts +64 -0
  221. package/src/server/serverFns/audit.ts +57 -0
  222. package/src/server/serverFns/auth/session.ts +26 -0
  223. package/src/server/serverFns/auth/setup.ts +73 -0
  224. package/src/server/serverFns/compliance.ts +85 -0
  225. package/src/server/serverFns/components/import.ts +197 -0
  226. package/src/server/serverFns/components/list.ts +51 -0
  227. package/src/server/serverFns/components/mutations.ts +84 -0
  228. package/src/server/serverFns/dependency/helpers.ts +245 -0
  229. package/src/server/serverFns/dependency/list.ts +127 -0
  230. package/src/server/serverFns/dependency/sync.ts +123 -0
  231. package/src/server/serverFns/dependency/update.ts +51 -0
  232. package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
  233. package/src/server/serverFns/developer-logs.ts +132 -0
  234. package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
  235. package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
  236. package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
  237. package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
  238. package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
  239. package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
  240. package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
  241. package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
  242. package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
  243. package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
  244. package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
  245. package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
  246. package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
  247. package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
  248. package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
  249. package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
  250. package/src/server/serverFns/licenses.ts +92 -0
  251. package/src/server/serverFns/locale.ts +10 -0
  252. package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
  253. package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
  254. package/src/server/serverFns/monitoring/get-errors.ts +43 -0
  255. package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
  256. package/src/server/serverFns/monitoring/get-health.ts +55 -0
  257. package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
  258. package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
  259. package/src/server/serverFns/monitoring/report-error.ts +55 -0
  260. package/src/server/serverFns/monitoring/reset.ts +70 -0
  261. package/src/server/serverFns/permissions.ts +214 -0
  262. package/src/server/serverFns/sentry/config.ts +88 -0
  263. package/src/server/serverFns/sso-config.ts +83 -0
  264. package/src/server/serverFns/user/create.ts +40 -0
  265. package/src/server/serverFns/user/delete.ts +29 -0
  266. package/src/server/serverFns/user/detail.ts +45 -0
  267. package/src/server/serverFns/user/list.ts +56 -0
  268. package/src/server/serverFns/user/reset-password.ts +28 -0
  269. package/src/server/serverFns/user/update-role.ts +26 -0
  270. package/src/server/serverFns/user/update-status.ts +26 -0
  271. package/src/server.ts +27 -0
  272. package/src/start.ts +85 -0
  273. package/src/styles/_keyframe-animations.scss +91 -0
  274. package/src/styles/_variables.scss +296 -0
  275. package/src/styles/globals.css +200 -0
  276. package/src/types/start-authjs.d.ts +27 -0
@@ -0,0 +1,245 @@
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
+ // apps/admin/src/server/serverFns/dependency/helpers.ts
19
+ import { existsSync } from "node:fs"
20
+ import { readFile } from "node:fs/promises"
21
+ import path from "node:path"
22
+ import { prisma } from "@allbluecn/database"
23
+ import {
24
+ DEFAULT_WATCHED_DEPENDENCIES,
25
+ type DependencyUsage,
26
+ type VersionStatus,
27
+ } from "@allbluecn/kernel"
28
+
29
+ /** 同步查找 monorepo 根(含 pnpm-workspace.yaml) */
30
+ function findMonorepoRootSync(): string {
31
+ let dir = process.cwd()
32
+ for (let i = 0; i < 8; i++) {
33
+ if (existsSync(path.join(dir, "pnpm-workspace.yaml"))) return dir
34
+ const parent = path.dirname(dir)
35
+ if (parent === dir) break
36
+ dir = parent
37
+ }
38
+ return process.cwd()
39
+ }
40
+
41
+ /** monorepo 中要扫描的 package.json 相对路径 */
42
+ const PACKAGE_FILES = [
43
+ "apps/web/package.json",
44
+ "apps/admin/package.json",
45
+ "packages/database/package.json",
46
+ "packages/shared/package.json",
47
+ "packages/ui/package.json",
48
+ ]
49
+
50
+ interface ParsedPkgFile {
51
+ /** 相对 monorepo 根的目录,如 "apps/web" */
52
+ app: string
53
+ /** name → version range(合并 dependencies + devDependencies) */
54
+ deps: Record<string, string>
55
+ }
56
+
57
+ /** 读取 monorepo 所有 package.json */
58
+ export async function readMonorepoPackages(): Promise<ParsedPkgFile[]> {
59
+ const root = findMonorepoRootSync()
60
+ const out: ParsedPkgFile[] = []
61
+ for (const rel of PACKAGE_FILES) {
62
+ const abs = path.join(root, rel)
63
+ try {
64
+ const raw = await readFile(abs, "utf-8")
65
+ const json = JSON.parse(raw) as {
66
+ dependencies?: Record<string, string>
67
+ devDependencies?: Record<string, string>
68
+ }
69
+ out.push({
70
+ app: path.dirname(rel),
71
+ deps: { ...json.dependencies, ...json.devDependencies },
72
+ })
73
+ } catch {
74
+ // 文件不存在跳过
75
+ }
76
+ }
77
+ return out
78
+ }
79
+
80
+ /** 聚合某个包在 monorepo 中的使用情况 */
81
+ export async function aggregateUsages(
82
+ pkgName: string,
83
+ files?: ParsedPkgFile[],
84
+ ): Promise<DependencyUsage[]> {
85
+ const list = files ?? (await readMonorepoPackages())
86
+ const usages: DependencyUsage[] = []
87
+ for (const f of list) {
88
+ const range = f.deps[pkgName]
89
+ if (range) usages.push({ app: f.app, declaredRange: range })
90
+ }
91
+ return usages
92
+ }
93
+
94
+ /** 查询 npm registry + GitHub Releases,返回 latest 版本 + homepage + changelog */
95
+ export async function queryNpmRegistry(
96
+ pkgName: string,
97
+ ): Promise<{ latestVersion: string; homepage: string | null; changelog: string | null }> {
98
+ // scope 包 URL 编码:@tiptap/core → @tiptap%2Fcore
99
+ const encoded = pkgName.replace(/\//g, "%2F")
100
+ const url = `https://registry.npmjs.org/${encoded}/latest`
101
+ const res = await fetch(url, {
102
+ headers: { Accept: "application/json" },
103
+ })
104
+ if (!res.ok) {
105
+ throw new Error(`npm registry ${res.status}: ${res.statusText}`)
106
+ }
107
+ const json = (await res.json()) as {
108
+ version: string
109
+ homepage?: string | null
110
+ repository?: { url?: string } | null
111
+ }
112
+ const homepage = json.homepage ?? json.repository?.url ?? null
113
+ // 清洗 git+ 前缀
114
+ const cleanHomepage = homepage?.replace(/^git\+/, "").replace(/\.git$/, "") ?? null
115
+
116
+ // 尝试获取 GitHub Release changelog
117
+ const repoUrl = json.repository?.url ?? cleanHomepage
118
+ const changelog = repoUrl ? await fetchGitHubChangelog(repoUrl, json.version) : null
119
+
120
+ return { latestVersion: json.version, homepage: cleanHomepage, changelog }
121
+ }
122
+
123
+ /** 从 repository URL 解析 GitHub owner/repo */
124
+ function parseGitHubRepo(repoUrl: string): { owner: string; repo: string } | null {
125
+ // 匹配 github.com/owner/repo(容忍 git+ 前缀、.git 后缀、ssh 协议)
126
+ const m = repoUrl.match(/github\.com[:/]([^/]+)\/([^/\.]+)/i)
127
+ if (!m) return null
128
+ return { owner: m[1], repo: m[2] }
129
+ }
130
+
131
+ /**
132
+ * 查询 GitHub Releases 获取指定版本的 changelog。
133
+ * tag 尝试顺序:v$version → $version → $name@$version(monorepo 风格)
134
+ * 失败返回 null(不阻塞同步)。
135
+ */
136
+ async function fetchGitHubChangelog(
137
+ repoUrl: string,
138
+ version: string,
139
+ ): Promise<string | null> {
140
+ const gh = parseGitHubRepo(repoUrl)
141
+ if (!gh) return null
142
+
143
+ const headers: Record<string, string> = {
144
+ Accept: "application/vnd.github+json",
145
+ "X-GitHub-Api-Version": "2022-11-28",
146
+ "User-Agent": "allblue-admin-dependency-sync",
147
+ }
148
+ // 可选 GitHub Token 提升 API 速率限制(60→5000/h)
149
+ const ghToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN
150
+ if (ghToken) headers.Authorization = `Bearer ${ghToken}`
151
+
152
+ const tags = [`v${version}`, version]
153
+ for (const tag of tags) {
154
+ try {
155
+ const res = await fetch(
156
+ `https://api.github.com/repos/${gh.owner}/${gh.repo}/releases/tags/${encodeURIComponent(tag)}`,
157
+ { headers },
158
+ )
159
+ if (res.ok) {
160
+ const data = (await res.json()) as { body?: string | null; html_url?: string }
161
+ if (data.body) return data.body.trim()
162
+ }
163
+ if (res.status === 404) continue // 试下一个 tag
164
+ // 速率限制或其他错误,停止尝试
165
+ break
166
+ } catch {
167
+ break
168
+ }
169
+ }
170
+ return null
171
+ }
172
+
173
+ /**
174
+ * 比较版本,返回落后程度。
175
+ * currentRange 形如 "^3.23.6" / "~1.0.0" / "19.2.4"
176
+ */
177
+ export function compareVersions(
178
+ currentRange: string | null,
179
+ latest: string | null,
180
+ ): VersionStatus {
181
+ if (!latest) return "unknown"
182
+ if (!currentRange) return "unknown"
183
+ const cur = parseSemver(currentRange)
184
+ const lat = parseSemver(latest)
185
+ if (!cur || !lat) return "unknown"
186
+ if (cur[0] !== lat[0]) return "major"
187
+ if (cur[1] !== lat[1]) return "minor"
188
+ if (cur[2] !== lat[2]) return "patch"
189
+ return "latest"
190
+ }
191
+
192
+ /** 从 semver range 提取 [major, minor, patch] */
193
+ function parseSemver(range: string): [number, number, number] | null {
194
+ // 去 prefix ^ ~ > >= 和 v 前缀,取前三段
195
+ const m = range.replace(/^[\^~>=<v]+/, "").split(".")
196
+ const major = Number(m[0])
197
+ const minor = Number(m[1] ?? 0)
198
+ const patch = Number(m[2] ?? 0)
199
+ if (Number.isNaN(major) || Number.isNaN(minor) || Number.isNaN(patch)) return null
200
+ return [major, minor, patch]
201
+ }
202
+
203
+ /** 取 usages 中第一个可解析的 semver,作为表格展示用 currentVersion */
204
+ export function pickCurrentVersion(usages: DependencyUsage[]): string | null {
205
+ for (const u of usages) {
206
+ const parsed = parseSemver(u.declaredRange)
207
+ if (parsed) return parsed.join(".")
208
+ }
209
+ return usages[0]?.declaredRange ?? null
210
+ }
211
+
212
+ /** 首次访问时 seed 预设关注列表;若已 seed 则回填缺失的 description(升级兼容) */
213
+ let seeded = false
214
+ export async function ensureSeedData(): Promise<void> {
215
+ if (seeded) return
216
+ const count = await prisma.thirdPartyDependency.count()
217
+ if (count === 0) {
218
+ await prisma.thirdPartyDependency.createMany({
219
+ data: DEFAULT_WATCHED_DEPENDENCIES.map((d, i) => ({
220
+ name: d.name,
221
+ displayName: d.displayName,
222
+ description: d.description,
223
+ homepage: d.homepage,
224
+ category: d.category,
225
+ priority: d.priority,
226
+ isWatched: true,
227
+ sortOrder: i,
228
+ })),
229
+ })
230
+ } else {
231
+ // 回填:对预设包中 description 为空的行补上(兼容本次新增 description 字段)
232
+ for (const d of DEFAULT_WATCHED_DEPENDENCIES) {
233
+ await prisma.thirdPartyDependency.updateMany({
234
+ where: { name: d.name, description: null },
235
+ data: { description: d.description },
236
+ })
237
+ }
238
+ }
239
+ seeded = true
240
+ }
241
+
242
+ /** 包名 → npm 页面地址 */
243
+ export function npmUrlOf(name: string): string {
244
+ return `https://www.npmjs.com/package/${name}`
245
+ }
@@ -0,0 +1,127 @@
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
+ // apps/admin/src/server/serverFns/dependency/list.ts
19
+ import { createServerFn } from "@tanstack/react-start"
20
+ import { z } from "zod"
21
+ import { prisma } from "@allbluecn/database"
22
+ import type { ThirdPartyDependencyRow } from "@allbluecn/kernel"
23
+ import {
24
+ aggregateUsages,
25
+ compareVersions,
26
+ ensureSeedData,
27
+ npmUrlOf,
28
+ pickCurrentVersion,
29
+ readMonorepoPackages,
30
+ } from "./helpers"
31
+
32
+ const listInput = z.object({
33
+ /** 仅返回关注列表 */
34
+ watchedOnly: z.boolean().optional(),
35
+ category: z.string().optional(),
36
+ search: z.string().optional(),
37
+ })
38
+
39
+ /**
40
+ * 列出所有依赖。
41
+ * 1. 首次访问自动 seed 预设关注列表
42
+ * 2. 读 monorepo 所有 package.json,聚合当前版本(实时)
43
+ * 3. 合并 DB 缓存的 latestVersion
44
+ * 4. DB 中未跟踪的包(出现在 package.json 但不在 DB)也会返回,归入「全部依赖」
45
+ */
46
+ export const listDependenciesFn = createServerFn({ method: "GET" })
47
+ .validator(listInput)
48
+ .handler(async ({ data }) => {
49
+ await ensureSeedData()
50
+
51
+ // 一次读取所有 package.json,供聚合复用
52
+ const pkgFiles = await readMonorepoPackages()
53
+ // 收集 monorepo 全部依赖名(去重)
54
+ const allPkgNames = new Set<string>()
55
+ for (const f of pkgFiles) {
56
+ for (const n of Object.keys(f.deps)) {
57
+ // 排除 workspace 内部包
58
+ if (!n.startsWith("@allblue/")) allPkgNames.add(n)
59
+ }
60
+ }
61
+
62
+ // 读 DB 所有记录
63
+ const where: { isWatched?: boolean; category?: string } = {}
64
+ if (data.watchedOnly) where.isWatched = true
65
+ if (data.category) where.category = data.category
66
+ const dbRows = await prisma.thirdPartyDependency.findMany({ where })
67
+ const dbMap = new Map(dbRows.map((r) => [r.name, r]))
68
+
69
+ // 构造结果集:DB 中所有记录 + package.json 中存在但 DB 未跟踪的包
70
+ const resultNameSet = new Set<string>()
71
+ if (!data.watchedOnly) {
72
+ // 全部模式:包含 DB 所有 + package.json 所有
73
+ for (const n of allPkgNames) resultNameSet.add(n)
74
+ for (const r of dbRows) resultNameSet.add(r.name)
75
+ } else {
76
+ // 关注模式:仅 DB 中 isWatched=true
77
+ for (const r of dbRows) if (r.isWatched) resultNameSet.add(r.name)
78
+ }
79
+
80
+ const rows: ThirdPartyDependencyRow[] = []
81
+ for (const name of resultNameSet) {
82
+ const db = dbMap.get(name)
83
+ const usages = await aggregateUsages(name, pkgFiles)
84
+ const currentVersion = pickCurrentVersion(usages)
85
+ const latestVersion = db?.latestVersion ?? null
86
+ const versionStatus = compareVersions(currentVersion, latestVersion)
87
+ const searchLower = data.search?.toLowerCase()
88
+ const displayName = db?.displayName ?? db?.name ?? name
89
+ if (
90
+ searchLower &&
91
+ !name.toLowerCase().includes(searchLower) &&
92
+ !displayName.toLowerCase().includes(searchLower)
93
+ ) {
94
+ continue
95
+ }
96
+
97
+ rows.push({
98
+ id: db?.id ?? `untracked:${name}`,
99
+ name,
100
+ displayName,
101
+ description: db?.description ?? null,
102
+ homepage: db?.homepage ?? null,
103
+ npmUrl: npmUrlOf(name),
104
+ category: (db?.category ?? "other") as ThirdPartyDependencyRow["category"],
105
+ isWatched: db?.isWatched ?? false,
106
+ priority: (db?.priority ?? "normal") as ThirdPartyDependencyRow["priority"],
107
+ usages,
108
+ currentVersion,
109
+ latestVersion,
110
+ latestChangelog: db?.latestChangelog ?? null,
111
+ lastSyncAt: db?.lastSyncAt ? db.lastSyncAt.toISOString() : null,
112
+ versionStatus,
113
+ notes: db?.notes ?? null,
114
+ sortOrder: db?.sortOrder ?? 9999,
115
+ updatedAt: db?.updatedAt ? db.updatedAt.toISOString() : new Date(0).toISOString(),
116
+ })
117
+ }
118
+
119
+ // 排序:关注优先 → sortOrder → 名称
120
+ rows.sort((a, b) => {
121
+ if (a.isWatched !== b.isWatched) return a.isWatched ? -1 : 1
122
+ if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
123
+ return a.name.localeCompare(b.name)
124
+ })
125
+
126
+ return rows
127
+ })
@@ -0,0 +1,123 @@
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
+ // apps/admin/src/server/serverFns/dependency/sync.ts
19
+ import { createServerFn } from "@tanstack/react-start"
20
+ import { z } from "zod"
21
+ import { prisma } from "@allbluecn/database"
22
+ import type { SyncDependencyResult } from "@allbluecn/kernel"
23
+ import { queryNpmRegistry } from "./helpers"
24
+
25
+ const syncInput = z.object({
26
+ /** npm 包名;不存在于 DB 时会 upsert 创建 */
27
+ name: z.string().min(1),
28
+ })
29
+
30
+ /**
31
+ * 同步单个包的最新版本(实时调 npm registry,写入 DB 缓存)。
32
+ * 包不存在时自动创建一行(isWatched 默认 false,后续可编辑加入关注)。
33
+ */
34
+ export const syncDependencyFn = createServerFn({ method: "POST" })
35
+ .validator(syncInput)
36
+ .handler(async ({ data }): Promise<SyncDependencyResult> => {
37
+ try {
38
+ const { latestVersion, homepage, changelog } = await queryNpmRegistry(data.name)
39
+ await prisma.thirdPartyDependency.upsert({
40
+ where: { name: data.name },
41
+ create: {
42
+ name: data.name,
43
+ displayName: data.name,
44
+ homepage,
45
+ latestVersion,
46
+ latestChangelog: changelog,
47
+ lastSyncAt: new Date(),
48
+ isWatched: false, // 未跟踪包默认不加入关注列表(schema 默认 true 是给 seed 用的)
49
+ },
50
+ update: {
51
+ latestVersion,
52
+ latestChangelog: changelog,
53
+ lastSyncAt: new Date(),
54
+ // homepage 只在 DB 为空时回填,不覆盖 admin 已编辑值
55
+ ...(homepage ? {} : {}),
56
+ },
57
+ })
58
+ // 若 DB 有空 homepage 且 npm 返回了,单独更新一次
59
+ if (homepage) {
60
+ await prisma.thirdPartyDependency.updateMany({
61
+ where: { name: data.name, homepage: null },
62
+ data: { homepage },
63
+ })
64
+ }
65
+ return { name: data.name, success: true, latestVersion, homepage, changelog }
66
+ } catch (e) {
67
+ return {
68
+ name: data.name,
69
+ success: false,
70
+ error: e instanceof Error ? e.message : String(e),
71
+ }
72
+ }
73
+ })
74
+
75
+ const syncAllInput = z.object({
76
+ /** 要同步的包名列表(前端传当前视图的全部包名,含未跟踪的会 upsert 创建) */
77
+ names: z.array(z.string().min(1)),
78
+ })
79
+
80
+ /**
81
+ * 批量同步(顺序请求避免 npm registry 速率限制)。
82
+ * 接收明确的包名列表,未跟踪的包会 upsert 创建 DB 行。
83
+ * 返回每个包的结果。
84
+ */
85
+ export const syncAllDependenciesFn = createServerFn({ method: "POST" })
86
+ .validator(syncAllInput)
87
+ .handler(async ({ data }): Promise<SyncDependencyResult[]> => {
88
+ const results: SyncDependencyResult[] = []
89
+ // 顺序执行(npm registry 建议不并发轰炸)
90
+ for (const name of data.names) {
91
+ try {
92
+ const { latestVersion, homepage, changelog } = await queryNpmRegistry(name)
93
+ await prisma.thirdPartyDependency.upsert({
94
+ where: { name },
95
+ create: {
96
+ name,
97
+ displayName: name,
98
+ homepage,
99
+ latestVersion,
100
+ latestChangelog: changelog,
101
+ lastSyncAt: new Date(),
102
+ isWatched: false, // 未跟踪包默认不加入关注列表
103
+ },
104
+ update: { latestVersion, latestChangelog: changelog, lastSyncAt: new Date() },
105
+ })
106
+ // 若 DB 有空 homepage 且 npm 返回了,单独回填(不覆盖 admin 已编辑值)
107
+ if (homepage) {
108
+ await prisma.thirdPartyDependency.updateMany({
109
+ where: { name, homepage: null },
110
+ data: { homepage },
111
+ })
112
+ }
113
+ results.push({ name, success: true, latestVersion, homepage, changelog })
114
+ } catch (e) {
115
+ results.push({
116
+ name,
117
+ success: false,
118
+ error: e instanceof Error ? e.message : String(e),
119
+ })
120
+ }
121
+ }
122
+ return results
123
+ })
@@ -0,0 +1,51 @@
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
+ // apps/admin/src/server/serverFns/dependency/update.ts
19
+ import { createServerFn } from "@tanstack/react-start"
20
+ import { z } from "zod"
21
+ import { prisma } from "@allbluecn/database"
22
+ import type { UpdateDependencyInput } from "@allbluecn/kernel"
23
+
24
+ const updateInput = z.object({
25
+ id: z.string().min(1),
26
+ displayName: z.string().optional(),
27
+ description: z.string().nullable().optional(),
28
+ homepage: z.string().nullable().optional(),
29
+ category: z.enum(["framework", "ui", "editor", "ai", "state", "auth", "tool", "other"]).optional(),
30
+ isWatched: z.boolean().optional(),
31
+ priority: z.enum(["high", "normal", "low"]).optional(),
32
+ notes: z.string().nullable().optional(),
33
+ sortOrder: z.number().optional(),
34
+ }) satisfies z.ZodType<UpdateDependencyInput & { id: string }>
35
+
36
+ /** 编辑依赖元数据(名称不可改) */
37
+ export const updateDependencyFn = createServerFn({ method: "POST" })
38
+ .validator(updateInput)
39
+ .handler(async ({ data }) => {
40
+ const { id, ...patch } = data
41
+ // 过滤 undefined 字段,避免覆盖为 null
42
+ const cleanPatch: Record<string, unknown> = {}
43
+ for (const [k, v] of Object.entries(patch)) {
44
+ if (v !== undefined) cleanPatch[k] = v
45
+ }
46
+ const updated = await prisma.thirdPartyDependency.update({
47
+ where: { id },
48
+ data: cleanPatch,
49
+ })
50
+ return { id: updated.id, updatedAt: updated.updatedAt.toISOString() }
51
+ })