@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.
- package/package.json +86 -0
- package/src/client.tsx +31 -0
- package/src/components/admin/admin-user-menu.tsx +90 -0
- package/src/components/admin/developer-log.tsx +356 -0
- package/src/components/ai-provider/model-list-editor.tsx +147 -0
- package/src/components/ai-provider/provider-form.tsx +434 -0
- package/src/components/ai-provider/provider-icon.tsx +43 -0
- package/src/components/ai-provider/provider-logo.tsx +87 -0
- package/src/components/ai-provider/provider-table.tsx +258 -0
- package/src/components/auth/admin-login-form.tsx +214 -0
- package/src/components/auth/auth-brand-panel.tsx +60 -0
- package/src/components/auth/auth-header.tsx +38 -0
- package/src/components/auth/auth-layout.tsx +66 -0
- package/src/components/auth/auth-providers.tsx +34 -0
- package/src/components/auth/icon-input.tsx +46 -0
- package/src/components/auth/oauth-section.tsx +51 -0
- package/src/components/auth/password-input.tsx +67 -0
- package/src/components/common/page-header.tsx +42 -0
- package/src/components/common/placeholder-page.tsx +42 -0
- package/src/components/component/component-form-dialog.tsx +348 -0
- package/src/components/component/component-import-dialog.tsx +235 -0
- package/src/components/component/component-table.tsx +153 -0
- package/src/components/data-table/data-table-pagination.tsx +114 -0
- package/src/components/data-table/data-table.tsx +218 -0
- package/src/components/data-table/table-detail-layout.tsx +99 -0
- package/src/components/dependency/changelog-dialog.tsx +108 -0
- package/src/components/dependency/columns.tsx +232 -0
- package/src/components/dependency/dependency-table.tsx +368 -0
- package/src/components/dependency/edit-dialog.tsx +200 -0
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
- package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
- package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
- package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
- package/src/components/layout/sidebar-02/index.tsx +33 -0
- package/src/components/layout/sidebar-02/logo.tsx +40 -0
- package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
- package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
- package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
- package/src/components/magicui/animated-theme-toggler.tsx +272 -0
- package/src/components/magicui/rainbow-button.tsx +78 -0
- package/src/components/market/columns.tsx +151 -0
- package/src/components/market/market-app-form.tsx +237 -0
- package/src/components/market/market-app-list.tsx +156 -0
- package/src/components/monitoring/error-list.tsx +143 -0
- package/src/components/monitoring/health-cards.tsx +152 -0
- package/src/components/monitoring/metrics-stats.tsx +184 -0
- package/src/components/monitoring/request-log-table.tsx +179 -0
- package/src/components/providers/theme-toggle.tsx +59 -0
- package/src/components/settings/avatar/avatar-config.ts +46 -0
- package/src/components/ui/alert-dialog.tsx +216 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/avatar.tsx +129 -0
- package/src/components/ui/badge.tsx +66 -0
- package/src/components/ui/button.tsx +84 -0
- package/src/components/ui/card.tsx +120 -0
- package/src/components/ui/checkbox.tsx +50 -0
- package/src/components/ui/collapsible.tsx +50 -0
- package/src/components/ui/dialog.tsx +185 -0
- package/src/components/ui/dropdown-menu.tsx +286 -0
- package/src/components/ui/field.tsx +255 -0
- package/src/components/ui/input-group.tsx +171 -0
- package/src/components/ui/input.tsx +36 -0
- package/src/components/ui/label.tsx +41 -0
- package/src/components/ui/pagination.tsx +146 -0
- package/src/components/ui/select.tsx +209 -0
- package/src/components/ui/separator.tsx +45 -0
- package/src/components/ui/sheet.tsx +164 -0
- package/src/components/ui/sidebar.tsx +719 -0
- package/src/components/ui/skeleton.tsx +30 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +48 -0
- package/src/components/ui/table.tsx +133 -0
- package/src/components/ui/textarea.tsx +35 -0
- package/src/components/ui/tooltip.tsx +74 -0
- package/src/components/user/columns.tsx +153 -0
- package/src/components/user/settings-activity-log.tsx +358 -0
- package/src/components/user/settings-profile/constants.ts +42 -0
- package/src/components/user/settings-profile/types.ts +47 -0
- package/src/components/user/settings-profile.tsx +664 -0
- package/src/components/user/user-create-dialog.tsx +137 -0
- package/src/components/user/user-filters.tsx +117 -0
- package/src/hooks/use-mobile.ts +36 -0
- package/src/instrument.client.ts +34 -0
- package/src/lib/anyhub/capabilities.ts +59 -0
- package/src/lib/anyhub/types.ts +43 -0
- package/src/lib/auth-client.ts +83 -0
- package/src/lib/auth-utils.ts +24 -0
- package/src/lib/auth.config.ts +264 -0
- package/src/lib/avatar.ts +41 -0
- package/src/lib/i18n/client.ts +32 -0
- package/src/lib/i18n/config.ts +40 -0
- package/src/lib/i18n/detect.ts +43 -0
- package/src/lib/i18n/locales/en/admin-ai.json +128 -0
- package/src/lib/i18n/locales/en/admin-billing.json +56 -0
- package/src/lib/i18n/locales/en/admin-users.json +88 -0
- package/src/lib/i18n/locales/en/common.json +877 -0
- package/src/lib/i18n/locales/en/enterprise.json +208 -0
- package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
- package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
- package/src/lib/i18n/locales/zh/admin-users.json +88 -0
- package/src/lib/i18n/locales/zh/common.json +877 -0
- package/src/lib/i18n/locales/zh/enterprise.json +208 -0
- package/src/lib/i18n/server.ts +18 -0
- package/src/lib/i18n/types.ts +5 -0
- package/src/lib/monitoring-benchmarks.ts +85 -0
- package/src/lib/monitoring-descriptions.ts +78 -0
- package/src/lib/prisma.ts +18 -0
- package/src/lib/router-context.ts +38 -0
- package/src/lib/session.ts +38 -0
- package/src/lib/utils.ts +23 -0
- package/src/providers/index.tsx +45 -0
- package/src/routeTree.gen.ts +1199 -0
- package/src/router.tsx +51 -0
- package/src/routes/__root.tsx +170 -0
- package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
- package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
- package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
- package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
- package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
- package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
- package/src/routes/_nav/ai-providers/route.tsx +31 -0
- package/src/routes/_nav/apps/route.lazy.tsx +30 -0
- package/src/routes/_nav/apps/route.tsx +20 -0
- package/src/routes/_nav/audit/route.lazy.tsx +100 -0
- package/src/routes/_nav/audit/route.tsx +25 -0
- package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
- package/src/routes/_nav/billing/invoices/route.tsx +25 -0
- package/src/routes/_nav/billing/route.lazy.tsx +48 -0
- package/src/routes/_nav/billing/route.tsx +27 -0
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
- package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
- package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
- package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
- package/src/routes/_nav/compliance/route.tsx +25 -0
- package/src/routes/_nav/components/route.lazy.tsx +184 -0
- package/src/routes/_nav/components/route.tsx +40 -0
- package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
- package/src/routes/_nav/dashboard/route.tsx +20 -0
- package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
- package/src/routes/_nav/dependencies/route.tsx +32 -0
- package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
- package/src/routes/_nav/developer-logs/route.tsx +14 -0
- package/src/routes/_nav/docs/route.lazy.tsx +30 -0
- package/src/routes/_nav/docs/route.tsx +20 -0
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
- package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
- package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
- package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
- package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
- package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
- package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
- package/src/routes/_nav/licenses/route.tsx +25 -0
- package/src/routes/_nav/market/route.lazy.tsx +31 -0
- package/src/routes/_nav/market/route.tsx +20 -0
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
- package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
- package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
- package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
- package/src/routes/_nav/monitoring/route.tsx +20 -0
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
- package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
- package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
- package/src/routes/_nav/permissions/route.tsx +26 -0
- package/src/routes/_nav/route.lazy.tsx +31 -0
- package/src/routes/_nav/route.tsx +34 -0
- package/src/routes/_nav/settings/route.lazy.tsx +30 -0
- package/src/routes/_nav/settings/route.tsx +20 -0
- package/src/routes/_nav/sso/route.lazy.tsx +145 -0
- package/src/routes/_nav/sso/route.tsx +25 -0
- package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
- package/src/routes/_nav/users/$userId/route.tsx +27 -0
- package/src/routes/_nav/users/-components/users-table.tsx +38 -0
- package/src/routes/_nav/users/route.lazy.tsx +74 -0
- package/src/routes/_nav/users/route.tsx +43 -0
- package/src/routes/api/compliance-download/$jobId.ts +35 -0
- package/src/routes/api/observability/errors.ts +41 -0
- package/src/routes/api/observability/health.ts +30 -0
- package/src/routes/api.auth.$.ts +31 -0
- package/src/routes/api.sentry-test.ts +33 -0
- package/src/routes/index/route.lazy.tsx +22 -0
- package/src/routes/index/route.tsx +24 -0
- package/src/routes/login/route.lazy.tsx +46 -0
- package/src/routes/login/route.tsx +28 -0
- package/src/routes/sentry-tunnel.ts +61 -0
- package/src/routes/setup/route.lazy.tsx +94 -0
- package/src/routes/setup/route.tsx +28 -0
- package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
- package/src/server/__tests__/licenses-issue.test.ts +98 -0
- package/src/server/admin-ai-alerts.ts +177 -0
- package/src/server/admin-ai-usage.ts +134 -0
- package/src/server/auth-helpers.ts +51 -0
- package/src/server/entitlements.ts +24 -0
- package/src/server/knowledge-public/canonical-id.ts +55 -0
- package/src/server/knowledge-public/github-sync.ts +86 -0
- package/src/server/license-issue.ts +78 -0
- package/src/server/middlewares/request.ts +26 -0
- package/src/server/observability/debug-middleware.ts +39 -0
- package/src/server/observability/error-reporter.ts +99 -0
- package/src/server/observability/health-check.ts +140 -0
- package/src/server/observability/index.ts +33 -0
- package/src/server/observability/logger.ts +49 -0
- package/src/server/observability/metrics.ts +105 -0
- package/src/server/observability/request-middleware.ts +125 -0
- package/src/server/observability/timing.ts +42 -0
- package/src/server/pro-services.ts +32 -0
- package/src/server/serverFns/admin-billing.ts +94 -0
- package/src/server/serverFns/ai-provider/create.ts +92 -0
- package/src/server/serverFns/ai-provider/delete.ts +42 -0
- package/src/server/serverFns/ai-provider/get.ts +35 -0
- package/src/server/serverFns/ai-provider/list.ts +92 -0
- package/src/server/serverFns/ai-provider/update.ts +73 -0
- package/src/server/serverFns/anyhub/market.ts +64 -0
- package/src/server/serverFns/audit.ts +57 -0
- package/src/server/serverFns/auth/session.ts +26 -0
- package/src/server/serverFns/auth/setup.ts +73 -0
- package/src/server/serverFns/compliance.ts +85 -0
- package/src/server/serverFns/components/import.ts +197 -0
- package/src/server/serverFns/components/list.ts +51 -0
- package/src/server/serverFns/components/mutations.ts +84 -0
- package/src/server/serverFns/dependency/helpers.ts +245 -0
- package/src/server/serverFns/dependency/list.ts +127 -0
- package/src/server/serverFns/dependency/sync.ts +123 -0
- package/src/server/serverFns/dependency/update.ts +51 -0
- package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
- package/src/server/serverFns/developer-logs.ts +132 -0
- package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
- package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
- package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
- package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
- package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
- package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
- package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
- package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
- package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
- package/src/server/serverFns/licenses.ts +92 -0
- package/src/server/serverFns/locale.ts +10 -0
- package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
- package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
- package/src/server/serverFns/monitoring/get-errors.ts +43 -0
- package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
- package/src/server/serverFns/monitoring/get-health.ts +55 -0
- package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
- package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
- package/src/server/serverFns/monitoring/report-error.ts +55 -0
- package/src/server/serverFns/monitoring/reset.ts +70 -0
- package/src/server/serverFns/permissions.ts +214 -0
- package/src/server/serverFns/sentry/config.ts +88 -0
- package/src/server/serverFns/sso-config.ts +83 -0
- package/src/server/serverFns/user/create.ts +40 -0
- package/src/server/serverFns/user/delete.ts +29 -0
- package/src/server/serverFns/user/detail.ts +45 -0
- package/src/server/serverFns/user/list.ts +56 -0
- package/src/server/serverFns/user/reset-password.ts +28 -0
- package/src/server/serverFns/user/update-role.ts +26 -0
- package/src/server/serverFns/user/update-status.ts +26 -0
- package/src/server.ts +27 -0
- package/src/start.ts +85 -0
- package/src/styles/_keyframe-animations.scss +91 -0
- package/src/styles/_variables.scss +296 -0
- package/src/styles/globals.css +200 -0
- 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
|
+
})
|