@allbluecn/admin-app 0.2.6 → 0.3.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 +16 -12
- package/src/components/admin/admin-user-menu.tsx +2 -4
- package/src/components/admin/developer-log.tsx +3 -5
- package/src/components/ai-memory/memory-facts-panel.tsx +383 -0
- package/src/components/ai-prompts/preview-panel.tsx +165 -0
- package/src/components/ai-prompts/prompt-editor.tsx +328 -0
- package/src/components/ai-prompts/prompt-table.tsx +141 -0
- package/src/components/ai-prompts/prompt-utils.ts +124 -0
- package/src/components/ai-prompts/section-editor.tsx +135 -0
- package/src/components/ai-prompts/variable-panel.tsx +128 -0
- package/src/components/ai-prompts/version-history.tsx +157 -0
- package/src/components/ai-provider/provider-form.tsx +6 -10
- package/src/components/ai-provider/provider-table.tsx +33 -13
- package/src/components/auth/admin-login-form.tsx +1 -1
- package/src/components/component/component-form-dialog.tsx +4 -4
- package/src/components/component/component-import-dialog.tsx +2 -2
- package/src/components/dependency/changelog-dialog.tsx +2 -4
- package/src/components/dependency/columns.tsx +4 -8
- package/src/components/dependency/dependency-table.tsx +2 -2
- package/src/components/dependency/edit-dialog.tsx +7 -11
- package/src/components/invitations/invitations-page.tsx +4 -8
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +1 -1
- package/src/components/knowledge-public/compliance-form-dialog.tsx +1 -1
- package/src/components/knowledge-public/legal-form-dialog.tsx +2 -2
- package/src/components/layout/sidebar-02/app-sidebar.tsx +17 -9
- package/src/components/layout/sidebar-02/nav-main.tsx +6 -14
- package/src/components/layout/sidebar-02/nav-notifications.tsx +3 -7
- package/src/components/magicui/rainbow-button.tsx +2 -2
- package/src/components/market/columns.tsx +2 -4
- package/src/components/market/market-app-form.tsx +2 -2
- package/src/components/market/market-app-list.tsx +1 -1
- package/src/components/oauth-providers/provider-page.tsx +1 -1
- package/src/components/ui/alert-dialog.tsx +28 -57
- package/src/components/ui/alert.tsx +3 -4
- package/src/components/ui/avatar.tsx +5 -28
- package/src/components/ui/badge.tsx +19 -34
- package/src/components/ui/button.tsx +14 -41
- package/src/components/ui/card.tsx +6 -24
- package/src/components/ui/checkbox.tsx +4 -26
- package/src/components/ui/collapsible.tsx +6 -37
- package/src/components/ui/dialog.tsx +30 -60
- package/src/components/ui/dropdown-menu.tsx +118 -137
- package/src/components/ui/field.tsx +8 -27
- package/src/components/ui/input-group.tsx +13 -26
- package/src/components/ui/input.tsx +4 -21
- package/src/components/ui/label.tsx +3 -27
- package/src/components/ui/pagination.tsx +17 -31
- package/src/components/ui/select.tsx +101 -76
- package/src/components/ui/separator.tsx +4 -27
- package/src/components/ui/sheet.tsx +29 -58
- package/src/components/ui/sidebar.tsx +119 -117
- package/src/components/ui/skeleton.tsx +1 -18
- package/src/components/ui/switch.tsx +5 -24
- package/src/components/ui/table.tsx +1 -19
- package/src/components/ui/textarea.tsx +2 -20
- package/src/components/ui/toast.tsx +282 -0
- package/src/components/ui/tooltip.tsx +34 -45
- package/src/components/user/settings-activity-log.tsx +1 -1
- package/src/components/user/user-create-dialog.tsx +2 -2
- package/src/components/user/user-filters.tsx +2 -2
- package/src/hooks/use-mobile.ts +0 -17
- package/src/lib/pacer.ts +21 -0
- package/src/lib/utils.ts +1 -23
- package/src/paraglide/messages/_index.js +95 -0
- package/src/paraglide/messages/admin_ai_memory_action_clearuser1.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_action_query.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_clear_cancel.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_clear_confirm_action.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_clear_confirm_description.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_clear_confirm_title.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_columns_actions.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_columns_content.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_columns_createdat1.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_columns_threadid1.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_delete_cancel.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_delete_confirm_action.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_delete_confirm_description.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_delete_confirm_title.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_empty.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_emptyhint1.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_error_nouserid2.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_error_query.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_error_title.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_form_user.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_page_description.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_page_title.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_placeholder_user.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_stat_count.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_stat_lastwrite1.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_toast_cleared.js +30 -0
- package/src/paraglide/messages/admin_ai_memory_toast_deleted.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_active.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_back.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_badge_protected.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_cache_hint.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_active.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_key.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_name.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_protected.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_updatedat1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_columns_version.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_description.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_description_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_edit.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_name.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_name_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_note_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_page_description.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_page_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_empty.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_generating.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_hint.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_nocontent1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_section.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_preview_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_protected_hint.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_rollback.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_action.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_description.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_rollback_confirm_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_save.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_add.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_id.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_movedown1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_moveup1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_order.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_remove.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_section_template_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_sections_empty.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_sections_hint.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_sections_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_activated.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_failed.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_protected_locked.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_rolled_back.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_save_failed.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_toast_saved.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_add.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_declarehint1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_description_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_name_placeholder.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_remove.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variable_undeclared.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variables_empty.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_variables_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_current.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_empty.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_hint.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_nonote1.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_title.js +30 -0
- package/src/paraglide/messages/admin_ai_prompts_versions_unknowncreator1.js +30 -0
- package/src/paraglide/messages/admin_ai_table_seedfromseed2.js +30 -0
- package/src/paraglide/messages/admin_ai_table_toast_seedcreated1.js +30 -0
- package/src/paraglide/messages/admin_ai_table_toast_seedfailed1.js +30 -0
- package/src/paraglide/messages/admin_ai_table_toast_seednonew2.js +30 -0
- package/src/paraglide/messages/common_monitoring_aiusage_sandbox1.js +30 -0
- package/src/paraglide/messages/common_monitoring_aiusage_sandboxvalue2.js +30 -0
- package/src/paraglide/messages/common_nav_aiprompts1.js +30 -0
- package/src/routeTree.gen.ts +101 -0
- package/src/routes/__root.tsx +2 -2
- package/src/{components/ui/sonner.tsx → routes/_nav/ai-memory/route.lazy.tsx} +24 -16
- package/src/routes/_nav/ai-memory/route.tsx +31 -0
- package/src/routes/_nav/ai-prompts/$key/route.lazy.tsx +32 -0
- package/src/routes/_nav/ai-prompts/$key/route.tsx +37 -0
- package/src/routes/_nav/ai-prompts/index/route.lazy.tsx +47 -0
- package/src/routes/_nav/ai-prompts/route.lazy.tsx +26 -0
- package/src/routes/_nav/ai-prompts/route.tsx +31 -0
- package/src/routes/_nav/compliance/route.lazy.tsx +2 -4
- package/src/routes/_nav/components/route.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +1 -1
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +10 -1
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +1 -1
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +1 -1
- package/src/routes/_nav/permissions/route.lazy.tsx +2 -2
- package/src/routes/_nav/sso/route.lazy.tsx +1 -1
- package/src/routes/_nav/users/$userId/route.lazy.tsx +3 -3
- package/src/routes/setup/route.lazy.tsx +1 -1
- package/src/server/__tests__/prompt-defaults.test.ts +139 -0
- package/src/server/admin-ai-usage.ts +22 -4
- package/src/server/ai-memory/store.ts +103 -0
- package/src/server/serverFns/ai-memory/clear.ts +31 -0
- package/src/server/serverFns/ai-memory/delete.ts +31 -0
- package/src/server/serverFns/ai-memory/list-users.ts +40 -0
- package/src/server/serverFns/ai-memory/list.ts +54 -0
- package/src/server/serverFns/ai-prompts/activate.ts +53 -0
- package/src/server/serverFns/ai-prompts/get.ts +74 -0
- package/src/server/serverFns/ai-prompts/list.ts +55 -0
- package/src/server/serverFns/ai-prompts/preview.ts +66 -0
- package/src/server/serverFns/ai-prompts/prompt-defaults.ts +63 -0
- package/src/server/serverFns/ai-prompts/prompt-schemas.ts +47 -0
- package/src/server/serverFns/ai-prompts/rollback.ts +86 -0
- package/src/server/serverFns/ai-prompts/save.ts +129 -0
- package/src/server/serverFns/ai-provider/seed-from-seed.ts +64 -0
- package/src/styles/globals.css +92 -77
- package/src/components/layout/sidebar-02/logo.tsx +0 -40
- package/src/components/layout/sidebar-02/team-switcher.tsx +0 -106
- package/src/components/providers/theme-toggle.tsx +0 -59
|
@@ -102,6 +102,7 @@ function AiUsagePage() {
|
|
|
102
102
|
|
|
103
103
|
const wsTotalPages = Math.max(1, Math.ceil((data?.topWorkspacesTotal ?? 0) / 10))
|
|
104
104
|
const usrTotalPages = Math.max(1, Math.ceil((data?.topUsersTotal ?? 0) / 10))
|
|
105
|
+
const sandboxActivity = data?.byActivity.find((a) => a.activity === 'sandbox')
|
|
105
106
|
|
|
106
107
|
if (loading && !data) {
|
|
107
108
|
return (
|
|
@@ -128,7 +129,7 @@ function AiUsagePage() {
|
|
|
128
129
|
</button>
|
|
129
130
|
</div>
|
|
130
131
|
|
|
131
|
-
<div className="grid gap-4 md:grid-cols-
|
|
132
|
+
<div className="grid gap-4 md:grid-cols-4">
|
|
132
133
|
<OverviewCard title={m.common_monitoring_aiUsage_totalCalls()} value={data?.totalCalls?.toLocaleString('en-US') ?? '0'} />
|
|
133
134
|
<OverviewCard
|
|
134
135
|
title={m.common_monitoring_aiUsage_totalCost()}
|
|
@@ -138,6 +139,14 @@ function AiUsagePage() {
|
|
|
138
139
|
title={m.common_monitoring_aiUsage_byokVsManaged()}
|
|
139
140
|
value={data?.bySource.map((s: any) => `${s.source}: ${s.count}`).join(' / ') || '—'}
|
|
140
141
|
/>
|
|
142
|
+
<OverviewCard
|
|
143
|
+
title={m.common_monitoring_aiUsage_sandbox()}
|
|
144
|
+
value={
|
|
145
|
+
sandboxActivity
|
|
146
|
+
? m.common_monitoring_aiUsage_sandboxValue({ minutes: sandboxActivity.minutes, count: sandboxActivity.count })
|
|
147
|
+
: '—'
|
|
148
|
+
}
|
|
149
|
+
/>
|
|
141
150
|
</div>
|
|
142
151
|
|
|
143
152
|
{alerts.length > 0 && (
|
|
@@ -23,7 +23,7 @@ import { PageHeader } from '@/components/common/page-header'
|
|
|
23
23
|
import { ErrorList } from '@/components/monitoring/error-list'
|
|
24
24
|
import { getErrors } from '@/server/serverFns/monitoring/get-errors'
|
|
25
25
|
import { resetMonitoring } from '@/server/serverFns/monitoring/reset'
|
|
26
|
-
import { toast } from '
|
|
26
|
+
import { toast } from '@/components/ui/toast'
|
|
27
27
|
|
|
28
28
|
export const Route = createLazyFileRoute('/_nav/monitoring/errors')({
|
|
29
29
|
component: MonitoringErrors,
|
|
@@ -25,7 +25,7 @@ import { HealthCards } from '@/components/monitoring/health-cards'
|
|
|
25
25
|
import { DataTable } from '@/components/data-table/data-table'
|
|
26
26
|
import { getHealthStatus } from '@/server/serverFns/monitoring/get-health'
|
|
27
27
|
import { getHealthCheckSettings, updateHealthCheckSettings } from '@/server/serverFns/monitoring/get-health-settings'
|
|
28
|
-
import { toast } from '
|
|
28
|
+
import { toast } from '@/components/ui/toast'
|
|
29
29
|
|
|
30
30
|
export const Route = createLazyFileRoute('/_nav/monitoring/health')({
|
|
31
31
|
component: MonitoringHealth,
|
|
@@ -25,7 +25,7 @@ import { Checkbox } from '@/components/ui/checkbox'
|
|
|
25
25
|
import { getRequestLogs } from '@/server/serverFns/monitoring/get-request-logs'
|
|
26
26
|
import { resetMonitoring } from '@/server/serverFns/monitoring/reset'
|
|
27
27
|
import { DataTablePagination } from '@/components/data-table/data-table-pagination'
|
|
28
|
-
import { toast } from '
|
|
28
|
+
import { toast } from '@/components/ui/toast'
|
|
29
29
|
|
|
30
30
|
export const Route = createLazyFileRoute('/_nav/monitoring/logs')({
|
|
31
31
|
component: MonitoringLogs,
|
|
@@ -28,7 +28,7 @@ import { MetricsStats } from '@/components/monitoring/metrics-stats'
|
|
|
28
28
|
import { getMetrics } from '@/server/serverFns/monitoring/get-metrics'
|
|
29
29
|
import { resetMonitoring } from '@/server/serverFns/monitoring/reset'
|
|
30
30
|
import { DataTable } from '@/components/data-table/data-table'
|
|
31
|
-
import { toast } from '
|
|
31
|
+
import { toast } from '@/components/ui/toast'
|
|
32
32
|
|
|
33
33
|
export const Route = createLazyFileRoute('/_nav/monitoring/metrics')({
|
|
34
34
|
component: MonitoringMetrics,
|
|
@@ -21,7 +21,7 @@ import * as Sentry from '@sentry/tanstackstart-react'
|
|
|
21
21
|
import { BugPlay, ExternalLink, Bug, Activity, AlertTriangle, Eye, EyeOff, Save, RotateCcw, CheckCircle2, XCircle, RefreshCw, ShieldCheck } from 'lucide-react'
|
|
22
22
|
import { IconBrandSentry } from '@tabler/icons-react';
|
|
23
23
|
import { useEffect, useState } from 'react'
|
|
24
|
-
import { toast } from '
|
|
24
|
+
import { toast } from '@/components/ui/toast'
|
|
25
25
|
import { getSentryConfig, saveSentryConfig, reconfigureSentryServer } from '@/server/serverFns/sentry/config'
|
|
26
26
|
import type { SentryConfig } from '@/server/serverFns/sentry/config'
|
|
27
27
|
|
|
@@ -2,7 +2,7 @@ import { createLazyFileRoute, Link, useLocation } from '@tanstack/react-router'
|
|
|
2
2
|
import { m } from '@/paraglide/messages.js'
|
|
3
3
|
import { getLocale } from '@/paraglide/runtime.js'
|
|
4
4
|
import { useServerFn } from '@tanstack/react-start'
|
|
5
|
-
import { toast } from '
|
|
5
|
+
import { toast } from '@/components/ui/toast'
|
|
6
6
|
import { useCallback, useEffect, useState } from 'react'
|
|
7
7
|
import type React from 'react'
|
|
8
8
|
import {
|
|
@@ -177,7 +177,7 @@ function CreateRoleDialog({
|
|
|
177
177
|
<Select
|
|
178
178
|
key={action.key}
|
|
179
179
|
value={permissions[`${resource.key}:${action.key}`] || 'none'}
|
|
180
|
-
onValueChange={(value
|
|
180
|
+
onValueChange={(value) => handleLevelChange(resource.key, action.key, value ?? "")}
|
|
181
181
|
>
|
|
182
182
|
<SelectTrigger className="h-8">
|
|
183
183
|
<SelectValue>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { createLazyFileRoute } from '@tanstack/react-router'
|
|
19
19
|
import { m } from '@/paraglide/messages.js'
|
|
20
20
|
import { useServerFn } from '@tanstack/react-start'
|
|
21
|
-
import { toast } from '
|
|
21
|
+
import { toast } from '@/components/ui/toast'
|
|
22
22
|
import type React from 'react'
|
|
23
23
|
import { deleteSsoProviderFn, toggleSsoProviderFn } from '@/server/serverFns/sso-config'
|
|
24
24
|
import { Badge } from '@/components/ui/badge'
|
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
ShoppingBagIcon,
|
|
76
76
|
ShieldCheckIcon,
|
|
77
77
|
} from 'lucide-react'
|
|
78
|
-
import { toast } from '@/components/ui/
|
|
78
|
+
import { toast } from '@/components/ui/toast'
|
|
79
79
|
import { parseUserAgent, formatParsedUA } from '@allbluecn/kernel'
|
|
80
80
|
import type { AvatarConfig } from '@allbluecn/kernel'
|
|
81
81
|
import SettingsActivityLog, {
|
|
@@ -330,7 +330,7 @@ function UserDetailPage() {
|
|
|
330
330
|
</DialogHeader>
|
|
331
331
|
<div className="py-4">
|
|
332
332
|
<Label>{m['admin-users_dialog_role_newRole']()}</Label>
|
|
333
|
-
<Select value={roleDialog.value} onValueChange={(v
|
|
333
|
+
<Select value={roleDialog.value} onValueChange={(v) => setRoleDialog((d) => ({ ...d, value: v ?? "" }))}>
|
|
334
334
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
335
335
|
<SelectContent>
|
|
336
336
|
<SelectItem value="USER">{m['admin-users_role_user']()}</SelectItem>
|
|
@@ -360,7 +360,7 @@ function UserDetailPage() {
|
|
|
360
360
|
</DialogHeader>
|
|
361
361
|
<div className="py-4">
|
|
362
362
|
<Label>{m['admin-users_dialog_status_newStatus']()}</Label>
|
|
363
|
-
<Select value={statusDialog.value} onValueChange={(v
|
|
363
|
+
<Select value={statusDialog.value} onValueChange={(v) => setStatusDialog((d) => ({ ...d, value: v ?? "" }))}>
|
|
364
364
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
365
365
|
<SelectContent>
|
|
366
366
|
<SelectItem value="ACTIVE">{m['admin-users_status_active']()}</SelectItem>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { createLazyFileRoute, useNavigate } from '@tanstack/react-router'
|
|
19
19
|
import { m } from '@/paraglide/messages.js'
|
|
20
20
|
import { useState } from 'react'
|
|
21
|
-
import { toast } from '@/components/ui/
|
|
21
|
+
import { toast } from '@/components/ui/toast'
|
|
22
22
|
import { Loader2, ShieldCheck, User, Mail, Database } from 'lucide-react'
|
|
23
23
|
import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from '@/components/auth/auth-layout'
|
|
24
24
|
import { AuthHeader } from '@/components/auth/auth-header'
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, vi, beforeEach } from "vitest"
|
|
19
|
+
import { type PromptSection } from "@allbluecn/web-app/ai-gateway/prompts/definitions"
|
|
20
|
+
|
|
21
|
+
const mocks = vi.hoisted(() => {
|
|
22
|
+
const findMany = vi.fn()
|
|
23
|
+
const createMany = vi.fn()
|
|
24
|
+
return {
|
|
25
|
+
findMany,
|
|
26
|
+
createMany,
|
|
27
|
+
prisma: {
|
|
28
|
+
systemPromptTemplate: { findMany, createMany },
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
vi.mock("@allbluecn/database", () => ({ prisma: mocks.prisma }))
|
|
34
|
+
|
|
35
|
+
// 内置定义走真实实现:admin 与 web 共享同一份默认提示词,
|
|
36
|
+
// 这里 mock 掉会掩盖「默认值为空」这类回归。
|
|
37
|
+
import { backfillMissingPromptTemplates } from "../serverFns/ai-prompts/prompt-defaults"
|
|
38
|
+
import { promptSectionSchema } from "../serverFns/ai-prompts/prompt-schemas"
|
|
39
|
+
import { listPromptDefinitions } from "@allbluecn/web-app/ai-gateway/prompts/definitions"
|
|
40
|
+
|
|
41
|
+
interface BackfillRow {
|
|
42
|
+
key: string
|
|
43
|
+
name: string
|
|
44
|
+
description: string | null
|
|
45
|
+
sections: PromptSection[]
|
|
46
|
+
variables: unknown[]
|
|
47
|
+
version: number
|
|
48
|
+
isProtected: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const getPrisma = () =>
|
|
52
|
+
(mocks.prisma.systemPromptTemplate as unknown) as {
|
|
53
|
+
findMany: ReturnType<typeof vi.fn>
|
|
54
|
+
createMany: ReturnType<typeof vi.fn>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writtenRows(): BackfillRow[] {
|
|
58
|
+
return getPrisma().createMany.mock.calls[0]?.[0].data ?? []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
vi.clearAllMocks()
|
|
63
|
+
getPrisma().createMany.mockResolvedValue({ count: 0 })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe("backfillMissingPromptTemplates", () => {
|
|
67
|
+
it("DB 为空时补种全部内置 key,且写入的是内置默认正文", async () => {
|
|
68
|
+
const defaults = listPromptDefinitions()
|
|
69
|
+
getPrisma().findMany.mockResolvedValue([])
|
|
70
|
+
|
|
71
|
+
const created = await backfillMissingPromptTemplates()
|
|
72
|
+
|
|
73
|
+
expect(created).toBe(defaults.length)
|
|
74
|
+
expect(writtenRows()).toHaveLength(defaults.length)
|
|
75
|
+
expect(writtenRows().every((row) => row.version === 1 && row.sections.length > 0)).toBe(true)
|
|
76
|
+
const chat = writtenRows().find((row) => row.key === "chat.default")
|
|
77
|
+
expect(chat).toBeDefined()
|
|
78
|
+
expect(chat!.sections.every((section) => section.template.trim().length > 0)).toBe(true)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("只补缺失的 key,已存在的行(含停用/留空)不动", async () => {
|
|
82
|
+
const defaults = listPromptDefinitions()
|
|
83
|
+
const known = defaults.slice(0, 3).map((definition) => definition.key)
|
|
84
|
+
getPrisma().findMany.mockResolvedValue(known.map((key) => ({ key })))
|
|
85
|
+
|
|
86
|
+
const created = await backfillMissingPromptTemplates()
|
|
87
|
+
|
|
88
|
+
expect(created).toBe(defaults.length - 3)
|
|
89
|
+
const writtenKeys = writtenRows().map((row) => row.key)
|
|
90
|
+
for (const key of known) {
|
|
91
|
+
expect(writtenKeys).not.toContain(key)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("无缺失时不写库", async () => {
|
|
96
|
+
getPrisma().findMany.mockResolvedValue(
|
|
97
|
+
listPromptDefinitions().map((definition) => ({ key: definition.key })),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
expect(await backfillMissingPromptTemplates()).toBe(0)
|
|
101
|
+
expect(getPrisma().createMany).not.toHaveBeenCalled()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("createMany 撞唯一约束(并发补种)时吞掉并返回 0,不向上抛", async () => {
|
|
105
|
+
getPrisma().findMany.mockResolvedValue([])
|
|
106
|
+
getPrisma().createMany.mockRejectedValue(
|
|
107
|
+
Object.assign(new Error("unique constraint"), { code: "P2002" }),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
await expect(backfillMissingPromptTemplates()).resolves.toBe(0)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("任何写库异常都被吞成 0,调用方按现状读取", async () => {
|
|
114
|
+
getPrisma().findMany.mockResolvedValue([])
|
|
115
|
+
getPrisma().createMany.mockRejectedValue(new Error("boom"))
|
|
116
|
+
|
|
117
|
+
await expect(backfillMissingPromptTemplates()).resolves.toBe(0)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe("promptSectionSchema 非空约束", () => {
|
|
122
|
+
it("空串与纯空白分区被拒绝", () => {
|
|
123
|
+
for (const template of ["", " ", "\t\n", "\u3000"]) {
|
|
124
|
+
expect(promptSectionSchema.safeParse({ id: "a", order: 1, template }).success).toBe(false)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("合法分区通过,且落库前会被 trim", () => {
|
|
129
|
+
const parsed = promptSectionSchema.safeParse({
|
|
130
|
+
id: "a",
|
|
131
|
+
order: 1,
|
|
132
|
+
template: " 带空白的正文 ",
|
|
133
|
+
})
|
|
134
|
+
expect(parsed.success).toBe(true)
|
|
135
|
+
if (parsed.success) {
|
|
136
|
+
expect(parsed.data.template).toBe("带空白的正文")
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -4,6 +4,7 @@ export interface AiUsageOverview {
|
|
|
4
4
|
totalCalls: number
|
|
5
5
|
totalCostUsd: number | null
|
|
6
6
|
bySource: Array<{ source: string; count: number }>
|
|
7
|
+
byActivity: Array<{ activity: string | null; count: number; minutes: number }>
|
|
7
8
|
topWorkspaces: Array<{
|
|
8
9
|
workspaceId: string
|
|
9
10
|
workspaceName?: string
|
|
@@ -42,6 +43,7 @@ export async function getAiUsageOverview(options: AiUsageOverviewOptions = {}):
|
|
|
42
43
|
topWorkspacesCount,
|
|
43
44
|
topUsersRaw,
|
|
44
45
|
topUsersCount,
|
|
46
|
+
byActivityRaw,
|
|
45
47
|
recentLogs,
|
|
46
48
|
] = await Promise.all([
|
|
47
49
|
prisma.aiUsageLog.count({ where: { createdAt: { gte: since } } }),
|
|
@@ -61,12 +63,12 @@ export async function getAiUsageOverview(options: AiUsageOverviewOptions = {}):
|
|
|
61
63
|
orderBy: { _count: { workspaceId: 'desc' } },
|
|
62
64
|
take: workspacePerPage,
|
|
63
65
|
skip: (workspacePage - 1) * workspacePerPage,
|
|
64
|
-
where: { createdAt: { gte: since } },
|
|
66
|
+
where: { createdAt: { gte: since }, workspaceId: { not: null } },
|
|
65
67
|
}),
|
|
66
68
|
prisma.aiUsageLog.groupBy({
|
|
67
69
|
by: ['workspaceId'],
|
|
68
70
|
_count: true,
|
|
69
|
-
where: { createdAt: { gte: since } },
|
|
71
|
+
where: { createdAt: { gte: since }, workspaceId: { not: null } },
|
|
70
72
|
}).then((r) => r.length),
|
|
71
73
|
prisma.aiUsageLog.groupBy({
|
|
72
74
|
by: ['userId'],
|
|
@@ -81,6 +83,12 @@ export async function getAiUsageOverview(options: AiUsageOverviewOptions = {}):
|
|
|
81
83
|
_count: true,
|
|
82
84
|
where: { createdAt: { gte: since } },
|
|
83
85
|
}).then((r) => r.length),
|
|
86
|
+
prisma.aiUsageLog.groupBy({
|
|
87
|
+
by: ['activity'],
|
|
88
|
+
_count: true,
|
|
89
|
+
_sum: { durationMs: true },
|
|
90
|
+
where: { createdAt: { gte: since } },
|
|
91
|
+
}),
|
|
84
92
|
prisma.aiUsageLog.findMany({
|
|
85
93
|
where: { createdAt: { gte: since } },
|
|
86
94
|
select: { createdAt: true, costUsd: true },
|
|
@@ -99,7 +107,12 @@ export async function getAiUsageOverview(options: AiUsageOverviewOptions = {}):
|
|
|
99
107
|
.map(([date, v]) => ({ date, ...v }))
|
|
100
108
|
.sort((a, b) => a.date.localeCompare(b.date))
|
|
101
109
|
|
|
102
|
-
|
|
110
|
+
// Prisma 按模型字段类型推断 groupBy 输出为 string | null(`not: null` 不改推断),
|
|
111
|
+
// 这里显式收窄,避免 null 行混进 topWorkspaces。
|
|
112
|
+
const topWorkspaceRows = topWorkspacesRaw.filter(
|
|
113
|
+
(w): w is typeof w & { workspaceId: string } => w.workspaceId !== null,
|
|
114
|
+
)
|
|
115
|
+
const workspaceIds = topWorkspaceRows.map((w) => w.workspaceId)
|
|
103
116
|
const userIds = topUsersRaw.map((u) => u.userId)
|
|
104
117
|
const [workspaces, users] = await Promise.all([
|
|
105
118
|
workspaceIds.length > 0
|
|
@@ -116,7 +129,12 @@ export async function getAiUsageOverview(options: AiUsageOverviewOptions = {}):
|
|
|
116
129
|
totalCalls,
|
|
117
130
|
totalCostUsd: costAgg._sum.costUsd ? Number(costAgg._sum.costUsd) : null,
|
|
118
131
|
bySource: bySource.map((b) => ({ source: b.source, count: b._count })),
|
|
119
|
-
|
|
132
|
+
byActivity: byActivityRaw.map((a) => ({
|
|
133
|
+
activity: a.activity,
|
|
134
|
+
count: a._count,
|
|
135
|
+
minutes: Math.ceil((a._sum.durationMs ?? 0) / 60_000),
|
|
136
|
+
})),
|
|
137
|
+
topWorkspaces: topWorkspaceRows.map((w) => ({
|
|
120
138
|
workspaceId: w.workspaceId,
|
|
121
139
|
workspaceName: wsNameMap.get(w.workspaceId),
|
|
122
140
|
count: w._count,
|
|
@@ -0,0 +1,103 @@
|
|
|
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/ai-memory/store.ts
|
|
19
|
+
import { prisma } from "@allbluecn/database"
|
|
20
|
+
|
|
21
|
+
// memory_facts 是 pgvector 原生表(Prisma 无法表达 vector 类型,同族参考
|
|
22
|
+
// knowledge_embeddings / prd_embeddings),由 migration 建表、以 raw SQL 读写。
|
|
23
|
+
// 桌面端 SQLite 无此表,本模块仅在 Web/Admin 服务端路径使用。
|
|
24
|
+
|
|
25
|
+
export interface MemoryFactRow {
|
|
26
|
+
id: string
|
|
27
|
+
content: string
|
|
28
|
+
threadId: string
|
|
29
|
+
createdAt: Date
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MemoryStats {
|
|
33
|
+
count: number
|
|
34
|
+
lastWriteAt: Date | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const MEMORY_FACTS_LIST_LIMIT = 500
|
|
38
|
+
|
|
39
|
+
function toIso(value: unknown): string | null {
|
|
40
|
+
if (value === null || value === undefined) return null
|
|
41
|
+
const date = value instanceof Date ? value : new Date(String(value))
|
|
42
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function listMemoryFacts(
|
|
46
|
+
userId: string,
|
|
47
|
+
limit = MEMORY_FACTS_LIST_LIMIT,
|
|
48
|
+
): Promise<Array<MemoryFactRow>> {
|
|
49
|
+
const rows = (await prisma.$queryRawUnsafe(
|
|
50
|
+
`SELECT "id", "content", "threadId", "createdAt"
|
|
51
|
+
FROM "memory_facts"
|
|
52
|
+
WHERE "userId" = $1
|
|
53
|
+
ORDER BY "createdAt" DESC
|
|
54
|
+
LIMIT $2`,
|
|
55
|
+
userId,
|
|
56
|
+
limit,
|
|
57
|
+
)) as Array<{
|
|
58
|
+
id: string
|
|
59
|
+
content: string
|
|
60
|
+
threadId: string
|
|
61
|
+
createdAt: Date | string
|
|
62
|
+
}>
|
|
63
|
+
|
|
64
|
+
return rows.map((row) => ({
|
|
65
|
+
id: row.id,
|
|
66
|
+
content: row.content,
|
|
67
|
+
threadId: row.threadId,
|
|
68
|
+
createdAt:
|
|
69
|
+
row.createdAt instanceof Date
|
|
70
|
+
? row.createdAt
|
|
71
|
+
: new Date(String(row.createdAt)),
|
|
72
|
+
}))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function countMemoryFacts(
|
|
76
|
+
userId: string,
|
|
77
|
+
): Promise<MemoryStats> {
|
|
78
|
+
const rows = (await prisma.$queryRawUnsafe(
|
|
79
|
+
`SELECT COUNT(*)::int AS "c", MAX("createdAt") AS "lastWriteAt"
|
|
80
|
+
FROM "memory_facts"
|
|
81
|
+
WHERE "userId" = $1`,
|
|
82
|
+
userId,
|
|
83
|
+
)) as Array<{ c: number | string; lastWriteAt: Date | string | null }>
|
|
84
|
+
const row = rows[0]
|
|
85
|
+
return {
|
|
86
|
+
count: row ? Number(row.c) : 0,
|
|
87
|
+
lastWriteAt: toIso(row?.lastWriteAt) ? new Date(toIso(row!.lastWriteAt)!) : null,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function deleteMemoryFact(id: string): Promise<number> {
|
|
92
|
+
return prisma.$executeRawUnsafe(
|
|
93
|
+
`DELETE FROM "memory_facts" WHERE "id" = $1`,
|
|
94
|
+
id,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function clearMemoryFacts(userId: string): Promise<number> {
|
|
99
|
+
return prisma.$executeRawUnsafe(
|
|
100
|
+
`DELETE FROM "memory_facts" WHERE "userId" = $1`,
|
|
101
|
+
userId,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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/srcRust/server/serverFns/ai-memory/clear.ts
|
|
19
|
+
import { createServerFn } from "@tanstack/react-start"
|
|
20
|
+
import { z } from "zod"
|
|
21
|
+
import { requireAdmin } from "../../auth-helpers"
|
|
22
|
+
import { clearMemoryFacts } from "../../ai-memory/store"
|
|
23
|
+
|
|
24
|
+
/** 清空某用户的全部跨会话记忆事实 */
|
|
25
|
+
export const clearMemoryFactsFn = createServerFn({ method: "POST" })
|
|
26
|
+
.validator(z.object({ userId: z.string().trim().min(1) }))
|
|
27
|
+
.handler(async ({ data }) => {
|
|
28
|
+
await requireAdmin()
|
|
29
|
+
const count = await clearMemoryFacts(data.userId)
|
|
30
|
+
return { deleted: count }
|
|
31
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
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/adminRust/src/server/serverFns/ai-memory/delete.ts
|
|
19
|
+
import { createServerFn } from "@tanstack/react-start"
|
|
20
|
+
import { z } from "zod"
|
|
21
|
+
import { requireAdmin } from "../../auth-helpers"
|
|
22
|
+
import { deleteMemoryFact } from "../../ai-memory/store"
|
|
23
|
+
|
|
24
|
+
/** 删除单条记忆事实 */
|
|
25
|
+
export const deleteMemoryFactFn = createServerFn({ method: "POST" })
|
|
26
|
+
.validator(z.object({ id: z.string().trim().min(1) }))
|
|
27
|
+
.handler(async ({ data }) => {
|
|
28
|
+
await requireAdmin()
|
|
29
|
+
const count = await deleteMemoryFact(data.id)
|
|
30
|
+
return { deleted: count }
|
|
31
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
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/ai-memory/list-users.ts
|
|
19
|
+
import { createServerFn } from "@tanstack/react-start"
|
|
20
|
+
import { z } from "zod"
|
|
21
|
+
import { prisma } from "@allbluecn/database"
|
|
22
|
+
import { requireAdmin } from "../../auth-helpers"
|
|
23
|
+
|
|
24
|
+
/** 列出可查询的用户(admin 按 userId 查记忆事实的前置选择) */
|
|
25
|
+
export const listMemoryUsersFn = createServerFn({ method: "GET" })
|
|
26
|
+
.validator(z.object({}))
|
|
27
|
+
.handler(async () => {
|
|
28
|
+
await requireAdmin()
|
|
29
|
+
const users = await prisma.user.findMany({
|
|
30
|
+
orderBy: [{ username: "asc" }],
|
|
31
|
+
take: 200,
|
|
32
|
+
select: { id: true, username: true, email: true, createdAt: true },
|
|
33
|
+
})
|
|
34
|
+
return users.map((user) => ({
|
|
35
|
+
id: user.id,
|
|
36
|
+
username: user.username,
|
|
37
|
+
email: user.email,
|
|
38
|
+
createdAt: user.createdAt.toISOString(),
|
|
39
|
+
}))
|
|
40
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
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/ai-memory/list.ts
|
|
19
|
+
import { createServerFn } from "@tanstack/react-start"
|
|
20
|
+
import { z } from "zod"
|
|
21
|
+
import { requireAdmin } from "../../auth-helpers"
|
|
22
|
+
import {
|
|
23
|
+
MEMORY_FACTS_LIST_LIMIT,
|
|
24
|
+
countMemoryFacts,
|
|
25
|
+
listMemoryFacts,
|
|
26
|
+
} from "../../ai-memory/store"
|
|
27
|
+
|
|
28
|
+
/** 按用户列出其沉淀的跨会话记忆事实 */
|
|
29
|
+
export const listMemoryFactsFn = createServerFn({ method: "GET" })
|
|
30
|
+
.validator(
|
|
31
|
+
z.object({
|
|
32
|
+
userId: z.string().trim().min(1),
|
|
33
|
+
limit: z.number().int().min(1).max(MEMORY_FACTS_LIST_LIMIT).optional(),
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
.handler(async ({ data }) => {
|
|
37
|
+
await requireAdmin()
|
|
38
|
+
const { userId, limit } = data
|
|
39
|
+
const [facts, stats] = await Promise.all([
|
|
40
|
+
listMemoryFacts(userId, limit),
|
|
41
|
+
countMemoryFacts(userId),
|
|
42
|
+
])
|
|
43
|
+
return {
|
|
44
|
+
userId,
|
|
45
|
+
count: stats.count,
|
|
46
|
+
lastWriteAt: stats.lastWriteAt?.toISOString() ?? null,
|
|
47
|
+
facts: facts.map((fact) => ({
|
|
48
|
+
id: fact.id,
|
|
49
|
+
content: fact.content,
|
|
50
|
+
threadId: fact.threadId,
|
|
51
|
+
createdAt: fact.createdAt.toISOString(),
|
|
52
|
+
})),
|
|
53
|
+
}
|
|
54
|
+
})
|