docs & cleanup: update stack info, remove dead code, fix backend bugs

- Fix AGENTS.md and 技术架构设计.md to reflect actual stack (Prisma+MySQL, static export, PM2)
- Remove 4 unused frontend components (page-transition, page-layout, image-upload, section-card)
- Fix card.tsx hardcoded colors → CSS variables
- Remove provider name from model selector, remove sensenova-u1-fast from models
- Fix analytics.controller.ts raw SQL table names (runtime bug)
- Add JWT auth guard to tools POST endpoint
- Fix AI gateway test: update env vars and model names
- Fix admin test: mock role structure for Prisma relation
This commit is contained in:
yuzhiran-dev
2026-05-29 10:26:50 +08:00
parent 417fb266d4
commit 6f3fe50ee0
15 changed files with 118 additions and 304 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ import { cn } from '@/lib/utils'
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('rounded-xl border border-gray-200 bg-white text-gray-900 shadow-sm dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100', className)} {...props} />
<div ref={ref} className={cn('rounded-xl border border-border bg-card text-foreground shadow-sm', className)} {...props} />
),
)
Card.displayName = 'Card'
@@ -24,7 +24,7 @@ CardTitle.displayName = 'CardTitle'
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-sm text-gray-500 dark:text-gray-400', className)} {...props} />
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
),
)
CardDescription.displayName = 'CardDescription'
@@ -1,82 +0,0 @@
'use client';
import { useState, useRef } from 'react';
import { API_BASE } from '@/lib/config';
interface ImageUploadProps {
onUploaded: (url: string) => void;
defaultImage?: string;
accept?: string;
}
export function ImageUpload({ onUploaded, defaultImage, accept = 'image/*' }: ImageUploadProps) {
const [uploading, setUploading] = useState(false);
const [preview, setPreview] = useState(defaultImage || '');
const [error, setError] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
async function handleFile(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
const token = localStorage.getItem('token');
if (!token) {
setError('请先登录');
return;
}
setUploading(true);
setError('');
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch(`${API_BASE}/upload`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: formData,
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.message || '上传失败');
}
const data = await res.json();
setPreview(`${API_BASE}${data.url}`);
onUploaded(data.url);
} catch (err: any) {
setError(err.message);
} finally {
setUploading(false);
}
}
return (
<div className="space-y-2">
<div
className="relative w-32 h-32 border-2 border-dashed border-gray-200 rounded-lg overflow-hidden cursor-pointer hover:border-brand-400 transition-colors bg-gray-50"
onClick={() => inputRef.current?.click()}
>
{preview ? (
<img src={preview} alt="preview" className="w-full h-full object-cover" />
) : (
<div className="flex items-center justify-center w-full h-full text-gray-400">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v16m8-8H4" />
</svg>
</div>
)}
{uploading && (
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
<div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
</div>
)}
</div>
<input ref={inputRef} type="file" accept={accept} onChange={handleFile} className="hidden" />
{error && <p className="text-xs text-red-500">{error}</p>}
<p className="text-xs text-gray-400"> 5MB jpg/png/gif/webp</p>
</div>
);
}
@@ -52,7 +52,7 @@ export function ModelSelector({ value, onChange, className = '' }: ModelSelector
<span className={`w-2 h-2 rounded-full shrink-0 ${m.id === value ? 'bg-brand-600' : 'bg-muted-foreground/30'}`} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground">{m.label}</div>
<div className="text-xs text-muted-foreground">{m.provider} · {m.desc}</div>
<div className="text-xs text-muted-foreground">{m.desc}</div>
</div>
{m.id === value && (
<svg className="w-4 h-4 text-brand-600 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -1,25 +0,0 @@
import type { ReactNode } from 'react'
interface PageLayoutProps {
title: string
description?: string
backHref?: string
children: ReactNode
}
export function PageLayout({ title, description, backHref, children }: PageLayoutProps) {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="mb-8">
{backHref && (
<a href={backHref} className="text-sm text-muted-foreground hover:text-brand-600 mb-2 inline-block">
&larr;
</a>
)}
<h1 className="text-3xl font-bold text-foreground">{title}</h1>
{description && <p className="mt-2 text-muted-foreground">{description}</p>}
</div>
{children}
</div>
)
}
@@ -1,15 +0,0 @@
'use client'
import { motion } from 'framer-motion'
export function PageTransition({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
{children}
</motion.div>
)
}
@@ -1,30 +0,0 @@
import type { ReactNode } from 'react'
interface CardProps {
children: ReactNode
className?: string
padding?: 'sm' | 'md' | 'lg'
}
export function Card({ children, className = '', padding = 'md' }: CardProps) {
const pads = { sm: 'p-4', md: 'p-6', lg: 'p-8' }
return (
<div className={`bg-card rounded-2xl border border-border shadow-sm ${pads[padding]} ${className}`}>
{children}
</div>
)
}
interface StatCardProps {
label: string
value: string | number
}
export function StatCard({ label, value }: StatCardProps) {
return (
<div className="bg-card rounded-xl border border-border p-6">
<div className="text-sm text-muted-foreground mb-1">{label}</div>
<div className="text-2xl font-bold text-foreground">{value}</div>
</div>
)
}
+1 -2
View File
@@ -77,8 +77,7 @@ export async function executeAction(action: AssistantAction): Promise<{ success:
export const MODEL_OPTIONS = [
{ value: 'general', label: '通用模式' },
{ value: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash' },
{ value: 'opencode', label: 'OpenCode Go' },
{ value: 'meituan/longcat-flash-lite', label: '长颈鹿 Flash' },
{ value: 'sensenova-6.7-flash-lite', label: 'SenseNova 6.7 Flash Lite' },
]
export const SKILL_IDS = [
+3 -4
View File
@@ -6,10 +6,9 @@ export interface ModelOption {
}
export const AVAILABLE_MODELS: ModelOption[] = [
{ id: 'general', label: '通用模式', provider: 'OpenAI 兼容', desc: '日常问答,综合能力均衡' },
{ id: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash', provider: '商汤科技', desc: '高速推理,代码生成强' },
{ id: 'sensenova-6.7-flash-lite', label: 'SenseNova 6.7 Flash Lite', provider: '商汤科技', desc: '轻量快速,日常使用' },
{ id: 'sensenova-u1-fast', label: 'SenseNova U1 Fast', provider: '商汤科技', desc: '高性能推理,复杂任务' },
{ id: 'general', label: '通用模式', provider: '', desc: '日常问答,综合能力均衡' },
{ id: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash', provider: '', desc: '高速推理,代码生成强' },
{ id: 'sensenova-6.7-flash-lite', label: 'SenseNova 6.7 Flash Lite', provider: '', desc: '轻量快速,日常使用' },
];
export const DEFAULT_MODEL = 'general';