@allbluecn/admin-app 0.3.1 → 0.3.3
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 +19 -17
- package/src/components/admin/admin-user-menu.tsx +4 -4
- package/src/components/admin/developer-log.tsx +16 -32
- package/src/components/ai-memory/memory-facts-panel.tsx +30 -70
- package/src/components/ai-prompts/preview-panel.tsx +1 -1
- package/src/components/ai-prompts/prompt-editor.tsx +8 -9
- package/src/components/ai-prompts/prompt-table.tsx +6 -6
- package/src/components/ai-prompts/section-editor.tsx +3 -3
- package/src/components/ai-prompts/variable-panel.tsx +2 -2
- package/src/components/ai-prompts/version-history.tsx +21 -41
- package/src/components/ai-provider/model-list-editor.tsx +2 -3
- package/src/components/ai-provider/provider-form.tsx +7 -7
- package/src/components/ai-provider/provider-table.tsx +17 -40
- package/src/components/auth/admin-login-form.tsx +7 -7
- package/src/components/auth/icon-input.tsx +2 -2
- package/src/components/auth/password-input.tsx +2 -2
- package/src/components/component/component-form-dialog.tsx +10 -18
- package/src/components/component/component-import-dialog.tsx +6 -6
- package/src/components/component/component-preview.tsx +311 -0
- package/src/components/component/component-table.tsx +3 -3
- package/src/components/dependency/changelog-dialog.tsx +2 -2
- package/src/components/dependency/columns.tsx +3 -3
- package/src/components/dependency/dependency-table.tsx +7 -7
- package/src/components/dependency/edit-dialog.tsx +8 -8
- package/src/components/invitations/invitations-page.tsx +26 -49
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +6 -6
- package/src/components/knowledge-public/compliance-form-dialog.tsx +6 -6
- package/src/components/knowledge-public/legal-form-dialog.tsx +7 -7
- package/src/components/layout/sidebar-02/app-sidebar.tsx +1 -2
- package/src/components/layout/sidebar-02/index.tsx +1 -1
- package/src/components/layout/sidebar-02/nav-main.tsx +2 -3
- package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -3
- package/src/components/market/columns.tsx +4 -4
- package/src/components/market/market-app-form.tsx +6 -7
- package/src/components/market/market-app-list.tsx +14 -29
- package/src/components/monitoring/error-list.tsx +2 -2
- package/src/components/monitoring/metrics-stats.tsx +1 -1
- package/src/components/monitoring/request-log-table.tsx +2 -2
- package/src/components/oauth-providers/provider-page.tsx +20 -38
- package/src/components/permissions/assign-dialog.tsx +135 -0
- package/src/components/permissions/create-role-dialog.tsx +135 -0
- package/src/components/permissions/types.ts +49 -0
- package/src/components/user/columns.tsx +6 -6
- package/src/components/user/settings-activity-log.tsx +7 -7
- package/src/components/user/settings-profile/avatar-uploader.tsx +118 -0
- package/src/components/user/settings-profile/social-links.tsx +63 -0
- package/src/components/user/settings-profile.tsx +26 -125
- package/src/components/user/user-create-dialog.tsx +6 -6
- package/src/components/user/user-detail/error-boundary.tsx +19 -0
- package/src/components/user/user-detail/user-detail-dialogs.tsx +203 -0
- package/src/components/user/user-filters.tsx +4 -4
- package/src/lib/auth/page-gate.ts +28 -0
- package/src/lib/auth.config.ts +25 -9
- package/src/lib/i18n/detect.ts +1 -1
- package/src/lib/loader-serial.ts +14 -0
- package/src/paraglide/README.md +1 -1
- package/src/providers/index.tsx +1 -1
- package/src/routeTree.gen.ts +67 -78
- package/src/routes/__root.tsx +4 -4
- package/src/routes/_nav/audit/route.lazy.tsx +1 -2
- package/src/routes/_nav/compliance/route.lazy.tsx +5 -3
- package/src/routes/_nav/components/route.lazy.tsx +14 -34
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +18 -38
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +18 -38
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +6 -6
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +3 -3
- package/src/routes/_nav/licenses/new/route.lazy.tsx +5 -5
- package/src/routes/_nav/licenses/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +2 -2
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +10 -10
- package/src/routes/_nav/monitoring/errors/route.tsx +7 -0
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +13 -15
- package/src/routes/_nav/monitoring/health/route.tsx +14 -0
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +24 -9
- package/src/routes/_nav/monitoring/logs/route.tsx +13 -0
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +12 -14
- package/src/routes/_nav/monitoring/metrics/route.tsx +7 -0
- package/src/routes/_nav/monitoring/overview/route.lazy.tsx +14 -13
- package/src/routes/_nav/monitoring/overview/route.tsx +17 -0
- package/src/routes/_nav/monitoring/route.lazy.tsx +6 -6
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +2 -2
- package/src/routes/_nav/permissions/route.lazy.tsx +21 -311
- package/src/routes/_nav/permissions/route.tsx +1 -1
- package/src/routes/_nav/route.tsx +5 -13
- package/src/routes/_nav/sso/route.lazy.tsx +2 -3
- package/src/routes/_nav/users/$userId/route.lazy.tsx +91 -156
- package/src/routes/_nav/users/-components/users-table.tsx +1 -1
- package/src/routes/_nav/users/route.lazy.tsx +1 -1
- package/src/routes/login/route.lazy.tsx +1 -1
- package/src/routes/setup/route.lazy.tsx +2 -2
- package/src/server/__tests__/admin-billing.test.ts +191 -16
- package/src/server/__tests__/compliance.test.ts +44 -7
- package/src/server/__tests__/permissions.test.ts +10 -5
- package/src/server/__tests__/prompt-defaults.test.ts +3 -3
- package/src/server/admin-ai-alerts.ts +2 -1
- package/src/server/admin-ai-usage.ts +2 -1
- package/src/server/ai-memory/store.ts +6 -2
- package/src/server/{serverFns/ai-prompts → ai-prompts}/prompt-defaults.ts +5 -3
- package/src/server/{serverFns/dependency → dependency}/helpers.ts +4 -3
- package/src/server/knowledge-public/github-sync.ts +2 -1
- package/src/server/license-issue.ts +2 -1
- package/src/server/middlewares/auth-rate-limit.ts +117 -0
- package/src/server/observability/health-check.ts +4 -1
- package/src/server/observability/metrics.ts +17 -15
- package/src/server/observability/request-middleware.ts +2 -1
- package/src/server/observability/timing.ts +14 -16
- package/src/server/serverFns/admin-billing.ts +107 -12
- package/src/server/serverFns/ai-memory/list-users.ts +2 -1
- package/src/server/serverFns/ai-prompts/activate.ts +2 -1
- package/src/server/serverFns/ai-prompts/get.ts +3 -2
- package/src/server/serverFns/ai-prompts/list.ts +3 -2
- package/src/server/serverFns/ai-prompts/rollback.ts +3 -1
- package/src/server/serverFns/ai-prompts/save.ts +5 -3
- package/src/server/serverFns/ai-provider/create.ts +4 -1
- package/src/server/serverFns/ai-provider/delete.ts +4 -1
- package/src/server/serverFns/ai-provider/get.ts +4 -1
- package/src/server/serverFns/ai-provider/list.ts +28 -1
- package/src/server/serverFns/ai-provider/seed-from-seed.ts +2 -1
- package/src/server/serverFns/ai-provider/update.ts +4 -1
- package/src/server/serverFns/anyhub/market.ts +8 -1
- package/src/server/serverFns/audit.ts +14 -1
- package/src/server/serverFns/auth/setup.ts +3 -1
- package/src/server/serverFns/compliance.ts +32 -3
- package/src/server/serverFns/components/import.ts +69 -14
- package/src/server/serverFns/components/list.ts +4 -1
- package/src/server/serverFns/components/mutations.ts +9 -1
- package/src/server/serverFns/dependency/list.ts +28 -5
- package/src/server/serverFns/dependency/sync.ts +7 -2
- package/src/server/serverFns/dependency/update.ts +4 -1
- package/src/server/serverFns/developer-log-auto-generate.ts +63 -32
- package/src/server/serverFns/developer-logs.ts +5 -1
- package/src/server/serverFns/invitations.ts +7 -1
- package/src/server/serverFns/knowledge-public/bookmarks/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/list.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +2 -1
- package/src/server/serverFns/knowledge-public/bookmarks/update.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/delete.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/detail.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/list.ts +2 -1
- package/src/server/serverFns/knowledge-public/compliance/update.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/delete.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/detail.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/list.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/update.ts +2 -1
- package/src/server/serverFns/licenses.ts +8 -2
- package/src/server/serverFns/monitoring/get-errors.ts +2 -1
- package/src/server/serverFns/monitoring/get-health.ts +2 -1
- package/src/server/serverFns/monitoring/get-metrics.ts +2 -1
- package/src/server/serverFns/monitoring/get-request-logs.ts +2 -1
- package/src/server/serverFns/monitoring/report-error.ts +2 -1
- package/src/server/serverFns/monitoring/reset.ts +2 -1
- package/src/server/serverFns/oauth-providers.ts +4 -1
- package/src/server/serverFns/permissions.ts +18 -11
- package/src/server/serverFns/sentry/config.ts +11 -8
- package/src/server/serverFns/sso-config.ts +17 -2
- package/src/server/serverFns/user/create.ts +6 -1
- package/src/server/serverFns/user/delete.ts +6 -1
- package/src/server/serverFns/user/detail.ts +5 -2
- package/src/server/serverFns/user/list.ts +19 -4
- package/src/server/serverFns/user/reset-password.ts +10 -2
- package/src/server/serverFns/user/update-role.ts +10 -2
- package/src/server/serverFns/user/update-status.ts +10 -2
- package/src/styles/globals.css +4 -0
- package/src/components/data-table/data-table-pagination.tsx +0 -112
- package/src/components/data-table/data-table.tsx +0 -218
- package/src/components/data-table/table-detail-layout.tsx +0 -98
- package/src/components/ui/alert-dialog.tsx +0 -187
- package/src/components/ui/alert.tsx +0 -75
- package/src/components/ui/avatar.tsx +0 -106
- package/src/components/ui/badge.tsx +0 -51
- package/src/components/ui/button.tsx +0 -57
- package/src/components/ui/card.tsx +0 -102
- package/src/components/ui/checkbox.tsx +0 -28
- package/src/components/ui/collapsible.tsx +0 -19
- package/src/components/ui/dialog.tsx +0 -155
- package/src/components/ui/dropdown-menu.tsx +0 -267
- package/src/components/ui/field.tsx +0 -236
- package/src/components/ui/input-group.tsx +0 -158
- package/src/components/ui/input.tsx +0 -19
- package/src/components/ui/label.tsx +0 -17
- package/src/components/ui/pagination.tsx +0 -130
- package/src/components/ui/select.tsx +0 -234
- package/src/components/ui/separator.tsx +0 -22
- package/src/components/ui/sheet.tsx +0 -135
- package/src/components/ui/sidebar.tsx +0 -721
- package/src/components/ui/skeleton.tsx +0 -13
- package/src/components/ui/switch.tsx +0 -29
- package/src/components/ui/table.tsx +0 -115
- package/src/components/ui/textarea.tsx +0 -17
- package/src/components/ui/toast.tsx +0 -282
- package/src/components/ui/tooltip.tsx +0 -63
- package/src/hooks/use-mobile.ts +0 -19
- package/src/lib/avatar.ts +0 -41
- /package/src/routes/{api.auth.$.ts → api/auth/$.ts} +0 -0
- /package/src/routes/{api.sentry-test.ts → api/sentry-test.ts} +0 -0
|
@@ -95,7 +95,7 @@ describe('listComplianceExportsFn(admin 合规导出任务列表)', () => {
|
|
|
95
95
|
expect(complianceExport.findMany).not.toHaveBeenCalled()
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
-
it('Pro 服务缺省时走 prisma 分页查询,skip/take 按页计算', async () => {
|
|
98
|
+
it('Pro 服务缺省时走 prisma 分页查询,skip/take 按页计算 + 白名单 select', async () => {
|
|
99
99
|
const items = [{ id: 'e-1' }]
|
|
100
100
|
complianceExport.findMany.mockResolvedValue(items)
|
|
101
101
|
complianceExport.count.mockResolvedValue(42)
|
|
@@ -103,25 +103,62 @@ describe('listComplianceExportsFn(admin 合规导出任务列表)', () => {
|
|
|
103
103
|
const result = await listComplianceExportsFn({ data: { page: 3, pageSize: 10 } })
|
|
104
104
|
|
|
105
105
|
expect(complianceExport.findMany).toHaveBeenCalledWith({
|
|
106
|
+
where: {},
|
|
106
107
|
orderBy: { createdAt: 'desc' },
|
|
107
108
|
skip: 20,
|
|
108
109
|
take: 10,
|
|
110
|
+
select: {
|
|
111
|
+
id: true,
|
|
112
|
+
type: true,
|
|
113
|
+
scope: true,
|
|
114
|
+
targetId: true,
|
|
115
|
+
status: true,
|
|
116
|
+
expiresAt: true,
|
|
117
|
+
error: true,
|
|
118
|
+
requestedBy: true,
|
|
119
|
+
createdAt: true,
|
|
120
|
+
},
|
|
109
121
|
})
|
|
110
|
-
expect(complianceExport.count).toHaveBeenCalledWith()
|
|
122
|
+
expect(complianceExport.count).toHaveBeenCalledWith({ where: {} })
|
|
111
123
|
expect(result).toEqual({ items, total: 42 })
|
|
112
124
|
})
|
|
113
125
|
|
|
126
|
+
it('DB-02:status 过滤参数化,where 同时作用于 findMany 与 count', async () => {
|
|
127
|
+
complianceExport.findMany.mockResolvedValue([])
|
|
128
|
+
complianceExport.count.mockResolvedValue(0)
|
|
129
|
+
|
|
130
|
+
await listComplianceExportsFn({ data: { page: 1, pageSize: 20, status: 'pending' } })
|
|
131
|
+
|
|
132
|
+
const arg = complianceExport.findMany.mock.calls[0][0]
|
|
133
|
+
expect(arg.where).toEqual({ status: 'pending' })
|
|
134
|
+
expect(complianceExport.count).toHaveBeenCalledWith({ where: { status: 'pending' } })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('DB-05:select 白名单排除 content 大字段与 downloadUrl', async () => {
|
|
138
|
+
complianceExport.findMany.mockResolvedValue([])
|
|
139
|
+
complianceExport.count.mockResolvedValue(0)
|
|
140
|
+
|
|
141
|
+
await listComplianceExportsFn({ data: {} })
|
|
142
|
+
|
|
143
|
+
const arg = complianceExport.findMany.mock.calls[0][0]
|
|
144
|
+
expect(arg.select).not.toHaveProperty('content')
|
|
145
|
+
expect(arg.select).not.toHaveProperty('downloadUrl')
|
|
146
|
+
expect(arg.select).not.toHaveProperty('completedAt')
|
|
147
|
+
})
|
|
148
|
+
|
|
114
149
|
it('未传分页参数时使用默认值(page=1, pageSize=20)', async () => {
|
|
115
150
|
complianceExport.findMany.mockResolvedValue([])
|
|
116
151
|
complianceExport.count.mockResolvedValue(0)
|
|
117
152
|
|
|
118
153
|
await listComplianceExportsFn({ data: {} })
|
|
119
154
|
|
|
120
|
-
expect(complianceExport.findMany).toHaveBeenCalledWith(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
155
|
+
expect(complianceExport.findMany).toHaveBeenCalledWith(
|
|
156
|
+
expect.objectContaining({
|
|
157
|
+
orderBy: { createdAt: 'desc' },
|
|
158
|
+
skip: 0,
|
|
159
|
+
take: 20,
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
125
162
|
})
|
|
126
163
|
|
|
127
164
|
it('查询前强制管理员身份与合规功能授权', async () => {
|
|
@@ -51,6 +51,7 @@ vi.mock('@allbluecn/database', () => ({
|
|
|
51
51
|
customRole: {
|
|
52
52
|
count: vi.fn(),
|
|
53
53
|
create: vi.fn(),
|
|
54
|
+
createMany: vi.fn(),
|
|
54
55
|
findMany: vi.fn(),
|
|
55
56
|
findFirst: vi.fn(),
|
|
56
57
|
delete: vi.fn(),
|
|
@@ -101,6 +102,7 @@ import {
|
|
|
101
102
|
const customRole = prisma.customRole as unknown as {
|
|
102
103
|
count: ReturnType<typeof vi.fn>
|
|
103
104
|
create: ReturnType<typeof vi.fn>
|
|
105
|
+
createMany: ReturnType<typeof vi.fn>
|
|
104
106
|
findMany: ReturnType<typeof vi.fn>
|
|
105
107
|
findFirst: ReturnType<typeof vi.fn>
|
|
106
108
|
delete: ReturnType<typeof vi.fn>
|
|
@@ -170,17 +172,20 @@ describe('listRolesFn', () => {
|
|
|
170
172
|
expect(roleAssignment.createMany).not.toHaveBeenCalled()
|
|
171
173
|
})
|
|
172
174
|
|
|
173
|
-
it('首次种子化创建 5
|
|
175
|
+
it('首次种子化创建 5 个系统角色预设(DB-01:一次 createMany 替代逐 preset create)', async () => {
|
|
174
176
|
customRole.count.mockResolvedValue(0)
|
|
177
|
+
customRole.createMany.mockResolvedValue({ count: 5 })
|
|
175
178
|
services.set('permissions', { kind: 'pro' })
|
|
176
179
|
|
|
177
180
|
await listRolesFn({ data: { workspaceId: WS } })
|
|
178
181
|
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
expect(customRole.createMany).toHaveBeenCalledTimes(1)
|
|
183
|
+
const rows = customRole.createMany.mock.calls[0][0].data
|
|
184
|
+
expect(rows.map((r: any) => r.name)).toEqual(['owner', 'admin', 'member', 'editor', 'viewer'])
|
|
181
185
|
// 系统角色必须标记 isSystem,用户不可伪造/删除
|
|
182
|
-
expect(
|
|
183
|
-
expect(
|
|
186
|
+
expect(rows.every((r: any) => r.isSystem === true)).toBe(true)
|
|
187
|
+
expect(rows.every((r: any) => r.workspaceId === WS)).toBe(true)
|
|
188
|
+
expect(customRole.create).not.toHaveBeenCalled()
|
|
184
189
|
})
|
|
185
190
|
|
|
186
191
|
it('种子化时给 ADMIN 用户赋 owner 角色', async () => {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect, vi, beforeEach } from "vitest"
|
|
19
|
-
import {
|
|
19
|
+
import type { PromptSection } from "@allbluecn/kernel"
|
|
20
20
|
|
|
21
21
|
const mocks = vi.hoisted(() => {
|
|
22
22
|
const findMany = vi.fn()
|
|
@@ -34,9 +34,9 @@ vi.mock("@allbluecn/database", () => ({ prisma: mocks.prisma }))
|
|
|
34
34
|
|
|
35
35
|
// 内置定义走真实实现:admin 与 web 共享同一份默认提示词,
|
|
36
36
|
// 这里 mock 掉会掩盖「默认值为空」这类回归。
|
|
37
|
-
import { backfillMissingPromptTemplates } from "../
|
|
37
|
+
import { backfillMissingPromptTemplates } from "../ai-prompts/prompt-defaults"
|
|
38
38
|
import { promptSectionSchema } from "../serverFns/ai-prompts/prompt-schemas"
|
|
39
|
-
import { listPromptDefinitions } from "@allbluecn/
|
|
39
|
+
import { listPromptDefinitions } from "@allbluecn/kernel"
|
|
40
40
|
|
|
41
41
|
interface BackfillRow {
|
|
42
42
|
key: string
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getPrisma } from '@allbluecn/database/get-prisma'
|
|
2
2
|
|
|
3
3
|
export interface AiAlert {
|
|
4
4
|
type: 'quota_exhausted' | 'high_cost' | 'error_spike' | 'provider_failure'
|
|
@@ -31,6 +31,7 @@ export interface AlertCheckOptions {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export async function checkAiAlerts(options: AlertCheckOptions = {}): Promise<AiAlert[]> {
|
|
34
|
+
const prisma = await getPrisma()
|
|
34
35
|
const { hours = 24, errorRateThreshold = 0.05 } = options
|
|
35
36
|
const dailyCostThresholdUsd =
|
|
36
37
|
options.dailyCostThresholdUsd ??
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getPrisma } from '@allbluecn/database/get-prisma'
|
|
2
2
|
|
|
3
3
|
export interface AiUsageOverview {
|
|
4
4
|
totalCalls: number
|
|
@@ -24,6 +24,7 @@ export interface AiUsageOverviewOptions {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export async function getAiUsageOverview(options: AiUsageOverviewOptions = {}): Promise<AiUsageOverview> {
|
|
27
|
+
const prisma = await getPrisma()
|
|
27
28
|
const {
|
|
28
29
|
days = 30,
|
|
29
30
|
workspace: workspaceOpt = {},
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
// apps/admin/src/server/ai-memory/store.ts
|
|
19
|
-
import {
|
|
19
|
+
import { getPrisma } from "@allbluecn/database/get-prisma"
|
|
20
20
|
|
|
21
21
|
// memory_facts 是 pgvector 原生表(Prisma 无法表达 vector 类型,同族参考
|
|
22
|
-
// knowledge_embeddings /
|
|
22
|
+
// knowledge_embeddings / design_embeddings),由 migration 建表、以 raw SQL 读写。
|
|
23
23
|
// 桌面端 SQLite 无此表,本模块仅在 Web/Admin 服务端路径使用。
|
|
24
24
|
|
|
25
25
|
export interface MemoryFactRow {
|
|
@@ -46,6 +46,7 @@ export async function listMemoryFacts(
|
|
|
46
46
|
userId: string,
|
|
47
47
|
limit = MEMORY_FACTS_LIST_LIMIT,
|
|
48
48
|
): Promise<Array<MemoryFactRow>> {
|
|
49
|
+
const prisma = await getPrisma()
|
|
49
50
|
const rows = (await prisma.$queryRawUnsafe(
|
|
50
51
|
`SELECT "id", "content", "threadId", "createdAt"
|
|
51
52
|
FROM "memory_facts"
|
|
@@ -75,6 +76,7 @@ export async function listMemoryFacts(
|
|
|
75
76
|
export async function countMemoryFacts(
|
|
76
77
|
userId: string,
|
|
77
78
|
): Promise<MemoryStats> {
|
|
79
|
+
const prisma = await getPrisma()
|
|
78
80
|
const rows = (await prisma.$queryRawUnsafe(
|
|
79
81
|
`SELECT COUNT(*)::int AS "c", MAX("createdAt") AS "lastWriteAt"
|
|
80
82
|
FROM "memory_facts"
|
|
@@ -89,6 +91,7 @@ export async function countMemoryFacts(
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
export async function deleteMemoryFact(id: string): Promise<number> {
|
|
94
|
+
const prisma = await getPrisma()
|
|
92
95
|
return prisma.$executeRawUnsafe(
|
|
93
96
|
`DELETE FROM "memory_facts" WHERE "id" = $1`,
|
|
94
97
|
id,
|
|
@@ -96,6 +99,7 @@ export async function deleteMemoryFact(id: string): Promise<number> {
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
export async function clearMemoryFacts(userId: string): Promise<number> {
|
|
102
|
+
const prisma = await getPrisma()
|
|
99
103
|
return prisma.$executeRawUnsafe(
|
|
100
104
|
`DELETE FROM "memory_facts" WHERE "userId" = $1`,
|
|
101
105
|
userId,
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
// apps/admin/src/server/
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
18
|
+
// apps/admin/src/server/ai-prompts/prompt-defaults.ts
|
|
19
|
+
import { getPrisma } from "@allbluecn/database/get-prisma"
|
|
20
|
+
import type { Prisma } from "@allbluecn/database"
|
|
21
|
+
import { listPromptDefinitions } from "@allbluecn/kernel"
|
|
21
22
|
|
|
22
23
|
// 提示词的唯一来源是代码内置定义(apps/web ai-gateway/prompts/builtin.ts);
|
|
23
24
|
// admin 后台只是查看与二次编辑的入口,DB 永远不该是「唯一事实」——
|
|
@@ -34,6 +35,7 @@ function toJson(value: unknown): Prisma.InputJsonValue {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export async function backfillMissingPromptTemplates() {
|
|
38
|
+
const prisma = await getPrisma()
|
|
37
39
|
const defaults = listPromptDefinitions()
|
|
38
40
|
const existing = await prisma.systemPromptTemplate.findMany({
|
|
39
41
|
select: { key: true },
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
// apps/admin/src/server/
|
|
18
|
+
// apps/admin/src/server/dependency/helpers.ts
|
|
19
19
|
import { existsSync } from "node:fs"
|
|
20
20
|
import { readFile } from "node:fs/promises"
|
|
21
21
|
import path from "node:path"
|
|
22
|
-
import {
|
|
22
|
+
import { getPrisma } from "@allbluecn/database/get-prisma"
|
|
23
23
|
import {
|
|
24
24
|
DEFAULT_WATCHED_DEPENDENCIES,
|
|
25
25
|
type DependencyUsage,
|
|
@@ -123,7 +123,7 @@ export async function queryNpmRegistry(
|
|
|
123
123
|
/** 从 repository URL 解析 GitHub owner/repo */
|
|
124
124
|
function parseGitHubRepo(repoUrl: string): { owner: string; repo: string } | null {
|
|
125
125
|
// 匹配 github.com/owner/repo(容忍 git+ 前缀、.git 后缀、ssh 协议)
|
|
126
|
-
const m = repoUrl.match(/github\.com[:/]([^/]+)\/([
|
|
126
|
+
const m = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/i)
|
|
127
127
|
if (!m) return null
|
|
128
128
|
return { owner: m[1], repo: m[2] }
|
|
129
129
|
}
|
|
@@ -212,6 +212,7 @@ export function pickCurrentVersion(usages: DependencyUsage[]): string | null {
|
|
|
212
212
|
/** 首次访问时 seed 预设关注列表;若已 seed 则回填缺失的 description(升级兼容) */
|
|
213
213
|
let seeded = false
|
|
214
214
|
export async function ensureSeedData(): Promise<void> {
|
|
215
|
+
const prisma = await getPrisma()
|
|
215
216
|
if (seeded) return
|
|
216
217
|
const count = await prisma.thirdPartyDependency.count()
|
|
217
218
|
if (count === 0) {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { getPrisma } from '@allbluecn/database/get-prisma'
|
|
19
19
|
|
|
20
20
|
export interface GithubData {
|
|
21
21
|
stars: number
|
|
@@ -63,6 +63,7 @@ export async function fetchGithubData(
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export async function syncBookmarkGithub(bookmarkId: string): Promise<boolean> {
|
|
66
|
+
const prisma = await getPrisma()
|
|
66
67
|
const bm = await prisma.websiteBookmark.findUnique({
|
|
67
68
|
where: { id: bookmarkId },
|
|
68
69
|
select: { id: true, isGithub: true, githubOwner: true, githubRepo: true },
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { getPrisma } from '@allbluecn/database/get-prisma'
|
|
19
19
|
import { getProService } from '@/server/pro-services'
|
|
20
20
|
import { FeatureRegistry } from '@allbluecn/kernel'
|
|
21
21
|
import { requireAdmin } from '@/server/auth-helpers'
|
|
@@ -29,6 +29,7 @@ export const issueLicense = async (data: {
|
|
|
29
29
|
features: Record<string, boolean>
|
|
30
30
|
expiresAt: string | null
|
|
31
31
|
}) => {
|
|
32
|
+
const prisma = await getPrisma()
|
|
32
33
|
const session = await requireAdmin()
|
|
33
34
|
|
|
34
35
|
const unknownKeys = Object.keys(data.features).filter(
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
* 认证类端点失败限流(SEC-09 登录 / SEC-10 分享密码)——进程内存态固定窗口。
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ 架构边界:计数器存于单进程内存(Map),**多实例部署不共享**,
|
|
22
|
+
* 实际阈值 = 配置值 × 实例数。本期不实施分布式限流(Redis 等),
|
|
23
|
+
* 多实例部署落地前不得将本模块视为唯一防线。
|
|
24
|
+
*
|
|
25
|
+
* 设计要点:
|
|
26
|
+
* - 仅失败尝试计数(登录:用户不存在/密码错误;分享密码:校验失败),
|
|
27
|
+
* 成功后清零——正常用户与自动化 e2e(成功登录)不受影响
|
|
28
|
+
* - key 归一化:email 小写去空白,避免大小写变体绕过;IP 取不到时归入
|
|
29
|
+
* `unknown` 桶(按 email 维度仍受限流保护)
|
|
30
|
+
* - 测试环境豁免:NODE_ENV=test(vitest)时不计数不拦截,防单测互踩
|
|
31
|
+
* - 账号状态类失败(封禁/未激活)不计入——非口令爆破向量
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const store = new Map<string, { count: number; resetAt: number }>()
|
|
35
|
+
|
|
36
|
+
export const LOGIN_RATE_LIMIT = { max: 5, windowMs: 15 * 60 * 1000 } as const
|
|
37
|
+
export const SHARE_PASSWORD_RATE_LIMIT = { max: 10, windowMs: 15 * 60 * 1000 } as const
|
|
38
|
+
|
|
39
|
+
const CLEANUP_INTERVAL = 60_000
|
|
40
|
+
const cleanupTimer = setInterval(() => {
|
|
41
|
+
const now = Date.now()
|
|
42
|
+
for (const [key, entry] of store) {
|
|
43
|
+
if (now > entry.resetAt) store.delete(key)
|
|
44
|
+
}
|
|
45
|
+
}, CLEANUP_INTERVAL)
|
|
46
|
+
// 防止 timer 阻止进程退出(jsdom 环境 setInterval 返回 number,无 unref)
|
|
47
|
+
cleanupTimer.unref?.()
|
|
48
|
+
|
|
49
|
+
/** 测试环境豁免(vitest 注入 NODE_ENV=test) */
|
|
50
|
+
export function isAuthRateLimitBypassed(): boolean {
|
|
51
|
+
return process.env.NODE_ENV === 'test'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isBlocked(key: string, max: number): boolean {
|
|
55
|
+
if (isAuthRateLimitBypassed()) return false
|
|
56
|
+
const entry = store.get(key)
|
|
57
|
+
if (!entry) return false
|
|
58
|
+
if (Date.now() > entry.resetAt) {
|
|
59
|
+
store.delete(key)
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
return entry.count >= max
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function recordFailure(key: string, windowMs: number): void {
|
|
66
|
+
if (isAuthRateLimitBypassed()) return
|
|
67
|
+
const now = Date.now()
|
|
68
|
+
const entry = store.get(key)
|
|
69
|
+
if (!entry || now > entry.resetAt) {
|
|
70
|
+
store.set(key, { count: 1, resetAt: now + windowMs })
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
entry.count++
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function loginKey(email: string, ip: string | null): string {
|
|
77
|
+
return `login:${email.toLowerCase().trim()}:${ip ?? 'unknown'}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 登录是否已被限流(email+IP 维度)。被限流时调用方应返回与密码错误不可区分的失败。 */
|
|
81
|
+
export function isLoginBlocked(email: string, ip: string | null): boolean {
|
|
82
|
+
return isBlocked(loginKey(email, ip), LOGIN_RATE_LIMIT.max)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 记录一次登录失败(用户不存在或密码错误) */
|
|
86
|
+
export function recordLoginFailure(email: string, ip: string | null): void {
|
|
87
|
+
recordFailure(loginKey(email, ip), LOGIN_RATE_LIMIT.windowMs)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 登录成功后清零失败计数 */
|
|
91
|
+
export function clearLoginFailures(email: string, ip: string | null): void {
|
|
92
|
+
store.delete(loginKey(email, ip))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sharePasswordKey(ip: string | null): string {
|
|
96
|
+
return `sharePwd:${ip ?? 'unknown'}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 分享密码校验是否已被限流(IP 维度)。被限流时调用方应返回 valid:false(不泄露限流细节)。 */
|
|
100
|
+
export function isSharePasswordBlocked(ip: string | null): boolean {
|
|
101
|
+
return isBlocked(sharePasswordKey(ip), SHARE_PASSWORD_RATE_LIMIT.max)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 记录一次分享密码校验失败 */
|
|
105
|
+
export function recordSharePasswordFailure(ip: string | null): void {
|
|
106
|
+
recordFailure(sharePasswordKey(ip), SHARE_PASSWORD_RATE_LIMIT.windowMs)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 分享密码校验成功后清零失败计数 */
|
|
110
|
+
export function clearSharePasswordFailures(ip: string | null): void {
|
|
111
|
+
store.delete(sharePasswordKey(ip))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 测试辅助:清空整个 store(用例隔离用,生产勿调) */
|
|
115
|
+
export function resetAuthRateLimitStoreForTest(): void {
|
|
116
|
+
store.clear()
|
|
117
|
+
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import os from 'node:os'
|
|
19
|
-
import {
|
|
19
|
+
import { getPrisma } from '@allbluecn/database/get-prisma'
|
|
20
20
|
|
|
21
21
|
interface HealthResult {
|
|
22
22
|
status: 'healthy' | 'unhealthy'
|
|
@@ -35,6 +35,7 @@ let healthTimer: ReturnType<typeof setInterval> | null = null
|
|
|
35
35
|
let healthEnabled = true
|
|
36
36
|
|
|
37
37
|
export async function performHealthCheck(): Promise<HealthResult> {
|
|
38
|
+
const prisma = await getPrisma()
|
|
38
39
|
let dbResult: { status: 'connected' | 'error'; latency?: number; error?: string }
|
|
39
40
|
|
|
40
41
|
try {
|
|
@@ -113,6 +114,7 @@ export function isHealthCheckEnabled(): boolean {
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
export async function setHealthCheckEnabled(enabled: boolean): Promise<void> {
|
|
117
|
+
const prisma = await getPrisma()
|
|
116
118
|
if (enabled) {
|
|
117
119
|
startHealthCheckTimer()
|
|
118
120
|
} else {
|
|
@@ -132,6 +134,7 @@ export async function setHealthCheckEnabled(enabled: boolean): Promise<void> {
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
export async function getHealthCheckEnabled(): Promise<boolean> {
|
|
137
|
+
const prisma = await getPrisma()
|
|
135
138
|
const setting = await prisma.systemSetting.findUnique({
|
|
136
139
|
where: { key: 'monitoring_health_check_enabled' },
|
|
137
140
|
})
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { getPrisma } from '@allbluecn/database/get-prisma'
|
|
19
19
|
|
|
20
20
|
interface MetricStats {
|
|
21
21
|
count: number
|
|
@@ -76,21 +76,23 @@ class MetricsCollector {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
async flushToDb(): Promise<void> {
|
|
79
|
+
const prisma = await getPrisma()
|
|
79
80
|
const allStats = this.getAllStats()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
81
|
+
// DB-01/DB-06:快照落盘从逐指标 create(N 次串行查询)改为 1 次 createMany(返回值不被消费)
|
|
82
|
+
// 查询次数 before: N → after: 1
|
|
83
|
+
if (Object.keys(allStats).length === 0) return
|
|
84
|
+
await prisma.monitoringMetricsSnapshot.createMany({
|
|
85
|
+
data: Object.entries(allStats).map(([name, stats]) => ({
|
|
86
|
+
metricName: name,
|
|
87
|
+
count: stats.count,
|
|
88
|
+
avg: stats.avg,
|
|
89
|
+
p50: stats.p50,
|
|
90
|
+
p95: stats.p95,
|
|
91
|
+
p99: stats.p99,
|
|
92
|
+
min: stats.min,
|
|
93
|
+
max: stats.max,
|
|
94
|
+
})),
|
|
95
|
+
})
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
reset(): void {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { createMiddleware } from '@tanstack/react-start'
|
|
19
|
-
import {
|
|
19
|
+
import { getPrisma } from '@allbluecn/database/get-prisma'
|
|
20
20
|
import { metrics } from './metrics'
|
|
21
21
|
import { logger } from './logger'
|
|
22
22
|
import { reportError } from './error-reporter'
|
|
@@ -111,6 +111,7 @@ export const observabilityMiddleware = createMiddleware().server(async ({ reques
|
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
async function saveRequestLogAsync(entry: RequestLogEntry): Promise<void> {
|
|
114
|
+
const prisma = await getPrisma()
|
|
114
115
|
await prisma.monitoringRequestLog.create({
|
|
115
116
|
data: {
|
|
116
117
|
timestamp: new Date(entry.timestamp),
|
|
@@ -23,20 +23,18 @@ export interface TimingResult {
|
|
|
23
23
|
label: string
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function timed(label: string, fn: () => Promise<unknown> | unknown): Promise<TimingResult & { data: unknown }> {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
})
|
|
26
|
+
export async function timed(label: string, fn: () => Promise<unknown> | unknown): Promise<TimingResult & { data: unknown }> {
|
|
27
|
+
const startTime = Date.now()
|
|
28
|
+
try {
|
|
29
|
+
const data = await fn()
|
|
30
|
+
const duration = Date.now() - startTime
|
|
31
|
+
metrics.recordTiming(label, duration)
|
|
32
|
+
logger('info', `Timed operation`, { label, duration })
|
|
33
|
+
return { label, duration, data }
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const duration = Date.now() - startTime
|
|
36
|
+
metrics.recordTiming(label, duration)
|
|
37
|
+
logger('error', `Timed operation failed`, { label, duration, error: (error as Error).message })
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
42
40
|
}
|