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
+3 -3
View File
@@ -4,7 +4,7 @@
## 项目概览
宇之然 AI 学习与实践平台。前端 Next.js + shadcn/ui,后端 NestJS + PostgreSQL。
宇之然 AI 学习与实践平台。前端 Next.js + shadcn/ui,后端 NestJS + MySQL。
| 项目 | 值 |
|------|-----|
@@ -25,7 +25,7 @@
### 后端
- **框架**: NestJS 模块化架构
- **ORM**: TypeORM + PostgreSQLschema 在 `database/migrations/`
- **ORM**: Prisma + MySQLschema 在 `prisma/schema.prisma`
- **认证**: JWT + Passport
- **API 前缀**: `/api/v1`
- **CORS**: `origin: true`, `maxAge: 0`(避免浏览器预检缓存)
@@ -82,7 +82,7 @@
- `users.service.ts``if (status)` 曾误写为 `if (params.status)`(已修复)
- 静态构建 (`next build`) 已修复 —— 所有动态路由都通过 server wrapper 模式导出 `generateStaticParams()`60 页面全部生成)
- `public/favicon.ico``app/favicon.ico` 不能共存,会触发 500
- 数据库 schema 通过 TypeORM migration 管理, 位于 `backend/src/database/migrations/`
- 数据库 schema 通过 Prisma migration 管理, 位于 `backend/prisma/migrations/`
## 项目管理流程
@@ -138,7 +138,7 @@ export class AnalyticsController {
if (type === 'users') {
const data = await this.prisma.$queryRaw<{ date: string; count: bigint }[]>`
SELECT DATE(created_at) as date, COUNT(*) as count
FROM User
FROM users
WHERE deleted_at IS NULL AND created_at >= ${startDate}
GROUP BY DATE(created_at)
ORDER BY date
@@ -149,7 +149,7 @@ export class AnalyticsController {
if (type === 'orders') {
const data = await this.prisma.$queryRaw<{ date: string; count: bigint; revenue: bigint }[]>`
SELECT DATE(created_at) as date, COUNT(*) as count, COALESCE(SUM(amount), 0) as revenue
FROM \`Order\`
FROM orders
WHERE status = 'PAID' AND created_at >= ${startDate}
GROUP BY DATE(created_at)
ORDER BY date
@@ -160,7 +160,7 @@ export class AnalyticsController {
if (type === 'sessions') {
const data = await this.prisma.$queryRaw<{ date: string; count: bigint }[]>`
SELECT DATE(created_at) as date, COUNT(*) as count
FROM sandbox_session
FROM sandbox_sessions
WHERE created_at >= ${startDate}
GROUP BY DATE(created_at)
ORDER BY date
@@ -51,7 +51,7 @@ describe('AdminService', () => {
id: 1,
username: 'admin',
passwordHash: '$2a$10$hashed',
role: 'superadmin',
role: { name: 'superadmin' },
status: 'ACTIVE',
};
+4 -4
View File
@@ -31,7 +31,7 @@ const MODEL_CATALOG: Record<string, ModelInfo> = {
'gpt-4': { id: 'gpt-4', provider: 'OpenAI', capabilities: ['chat', 'code', 'vision'], contextWindow: 32768 },
'deepseek-v4-flash': { id: 'deepseek-v4-flash', provider: '商汤科技', capabilities: ['chat', 'code'], contextWindow: 32768 },
'sensenova-6.7-flash-lite': { id: 'sensenova-6.7-flash-lite', provider: '商汤科技', capabilities: ['chat', 'code'], contextWindow: 32768 },
'sensenova-u1-fast': { id: 'sensenova-u1-fast', provider: '商汤科技', capabilities: ['chat', 'code', 'vision'], contextWindow: 65536 },
}
@Injectable()
@@ -65,7 +65,7 @@ export class AIGatewayService {
if (!apiUrl.endsWith('/chat/completions')) {
apiUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions';
}
const models = ['deepseek-v4-flash', 'sensenova-6.7-flash-lite', 'sensenova-u1-fast'];
const models = ['deepseek-v4-flash', 'sensenova-6.7-flash-lite'];
for (const modelName of models) {
this.providers.set(modelName, new OpenAICompatibleProvider(
process.env.SENSENOVA_API_KEY!,
@@ -88,7 +88,7 @@ export class AIGatewayService {
'meituan/longcat-flash-lite': 'openai',
'deepseek-v4-flash': 'deepseek-v4-flash',
'sensenova-6.7-flash-lite': 'sensenova-6.7-flash-lite',
'sensenova-u1-fast': 'sensenova-u1-fast',
};
const providerKey = modelMap[model.toLowerCase()] || (model.includes('/') ? 'openai' : model);
@@ -125,7 +125,7 @@ export class AIGatewayService {
'meituan/longcat-flash-lite': 'openai',
'deepseek-v4-flash': 'deepseek-v4-flash',
'sensenova-6.7-flash-lite': 'sensenova-6.7-flash-lite',
'sensenova-u1-fast': 'sensenova-u1-fast',
};
const providerKey = modelMap[model.toLowerCase()] || (model.includes('/') ? 'openai' : model);
@@ -6,8 +6,8 @@ describe('AIGatewayService', () => {
beforeEach(async () => {
// Clear env before each test
delete process.env.DEEPSEEK_API_KEY;
delete process.env.DASHSCOPE_API_KEY;
delete process.env.OPENAI_API_KEY;
delete process.env.SENSENOVA_API_KEY;
const module: TestingModule = await Test.createTestingModule({
providers: [AIGatewayService],
@@ -70,8 +70,8 @@ describe('AIGatewayService', () => {
});
describe('model routing', () => {
it('should route deepseek models to deepseek provider (but fallback to mock)', async () => {
const result = await service.chat('deepseek-chat', [
it('should route deepseek-v4-flash to its provider (but fallback to mock)', async () => {
const result = await service.chat('deepseek-v4-flash', [
{ role: 'user', content: '你好' },
]);
@@ -79,8 +79,8 @@ describe('AIGatewayService', () => {
expect(result).toContain('宇之然 AI 助手');
});
it('should route qwen models to dashscope provider (but fallback to mock)', async () => {
const result = await service.chat('qwen-max', [
it('should route general model to openai provider (but fallback to mock)', async () => {
const result = await service.chat('general', [
{ role: 'user', content: '你好' },
]);
@@ -1,5 +1,6 @@
import { Controller, Get, Post, Body, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Controller, Get, Post, Body, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { ToolsService } from './tools.service';
@ApiTags('AI工具')
@@ -13,6 +14,8 @@ export class ToolsController {
}
@Post()
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth()
async create(@Body() body: { name: string; description?: string; url: string; icon?: string; categoryId?: number; tags?: string }) {
return this.toolsService.create(body);
}
+88 -123
View File
@@ -1,7 +1,7 @@
# 宇之然 AI - 技术架构设计文档
> 版本:v1.0
> 日期:2026-05-08
> 版本:v1.1
> 日期:2026-05-29
---
@@ -12,16 +12,16 @@
```
┌─────────────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌────────────┐ ┌──────────────┐ ┌────────────
│ │ Next.js │ Uni-app │ │ 微信小程序
│ │ 官网 (SSR) │ │ App (跨平台) │ │ │ │
│ └────────────┘ └──────────────┘ └────────────
│ ┌────────────────────┐ ┌──────────────────┐
│ │ Next.js │ │ 微信小程序
│ │ 官网 (静态导出) │ │ (规划中)
│ └────────────────────┘ └──────────────────┘
└──────────────────────────┬──────────────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────────────┐
│ API 网关层
│ Nginx / 阿里云 SLB → API Gateway
│ 限流 / 鉴权 / 日志 / 路由转发
│ API 网关层 (NestJS)
│ Nginx → PM2 → NestJS
│ 限流 / JWT 鉴权 / 日志 / 路由转发 │
└──────────────────────────┬──────────────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────────────┐
@@ -36,14 +36,14 @@
┌──────────────────────────▼──────────────────────────────────────────┐
│ AI 网关层 │
│ 统一 API → 模型路由 → 负载均衡 → 结果缓存
通义千问 │ 文心一言 │ GLM │ DeepSeek │ Kimi ...
统一 API → 模型路由 → 流式响应
qnaigc 兼容接口 → DeepSeek / SenseNova / 其他 OpenAI 兼容模型
└──────────────────────────┬──────────────────────────────────────────┘
┌──────────────────────────▼──────────────────────────────────────────┐
│ 数据层 │
│ MySQL 8.0 │ Redis 7 ES 8.x OSS RocketMQ
│ (主从/读写分离) (缓存/会话) (搜索) (存储) (消息队列)
│ MySQL 8.0 │ Redis 7
│ (主库) │ (缓存/会话)
└─────────────────────────────────────────────────────────────────────┘
```
@@ -55,23 +55,18 @@
| 技术 | 版本 | 说明 |
|------|------|------|
| Next.js | 14+ | React 框架,SSR/SSG 支持 |
| Next.js | 14+ | React 框架,静态导出 (`output: 'export'`) |
| TypeScript | 5.x | 类型安全 |
| TailwindCSS | 3.x | 原子化 CSS |
| Shadcn/ui | latest | UI 组件库 |
| React Query | 5.x | 数据请求管理 |
| Zustand | latest | 状态管理 |
| next-themes | latest | 暗黑模式切换 |
### 2.2 移动端(App / 小程序
### 2.2 移动端(规划中
| 技术 | 说明 |
|------|------|
| Uni-app / Taro | 跨端框架 |
| Vue 3 / React | 视框架而定 |
| Pinia / Zustand | 状态管理 |
| uView / NutUI | 移动端组件库 |
> 推荐 Uni-app + Vue 3,对微信小程序适配最成熟。
| 微信小程序 | 后续规划 |
| Taro / uni-app | 评估中 |
### 2.3 后端
@@ -79,64 +74,41 @@
|------|------|------|
| Node.js | 20 LTS | 运行时 |
| NestJS | 10.x | Node.js 后端框架 |
| Prisma | 5.x | ORM |
| JWT | - | 鉴权 |
| Zod | latest | 数据校验 |
或备选方案:
| 技术 | 说明 |
|------|------|
| Go + Gin / Fiber | 高性能,适合 AI 网关 |
| Python + FastAPI | 适合 AI 数据处理任务 |
> 建议:核心业务用 NestJS,AI 网关用 Go。
| Prisma | 5.x | ORMMySQL |
| JWT | - | 鉴权Access Token 2h + Refresh Token 7d|
### 2.4 数据库
| 组件 | 用途 | 部署 |
|------|------|------|
| MySQL 8.0 | 业务主库(用户/课程/订单等) | 阿里云 RDS |
| Redis 7 | 缓存/会话/限流计数器 | 阿里云 Redis |
| Elasticsearch 8.x | 内容搜索 | 阿里云 ES |
| 阿里云 OSS | 图片/视频/文件存储 | 阿里云 OSS |
| RocketMQ | 异步任务/消息通知 | 阿里云 RocketMQ |
| MySQL 8.0 | 业务主库(用户/课程/订单等) | 本地 / 云 RDS |
| Redis 7 | 缓存/会话/限流计数器 | 本地 / 云 Redis |
### 2.5 AI 网关
```
用户请求 → 网关层
├── 请求校验 → 内容安全审核
├── 模型路由(基于用户配置/成本/负载)
│ ├── 通义千问 (qwen-max)
── 文心一言 (ERNIE-4.0)
├── GLM-4
│ ├── DeepSeek-V3
│ └── Kimi (moonshot-v1)
├── 结果缓存(Redis,相同 prompt 命中缓存)
├── 流式响应处理
└── 计费/用量统计
用户请求 → AIGatewayService
├── 模型路由(基于 modelMap 配置)
├── general → OPENAI_MODEL (default: deepseek/deepseek-v4-flash)
│ ├── deepseek-v4-flash → deepseek/deepseek-v4-flash
── sensenova-6.7-flash-lite → sensenova-6.7-flash-lite
├── 流式响应 (ReadableStream SSE)
└── 用量统计
```
### 2.6 内容安全
| 服务 | 用途 |
|------|------|
| 阿里云内容安全 | 文本/图片鉴黄、涉政、违禁检测 |
| 腾讯云天御 | UGC 内容安全审核 |
| 自定义敏感词库 | 行业特定敏感词过滤 |
AI 通过 OpenAI 兼容 API (`api.qnaigc.com`) 统一接入,不直接对接各模型厂商。
---
## 3. 数据库设计概要
### 3.1 核心表结构
### 3.1 核心表结构 (Prisma Schema)
```
users — 用户表
├── id, phone, email, password_hash, avatar, status, created_at
├── user_profiles — 用户扩展信息
└── user_learn_records — 学习记录
├── profiles — 用户扩展信息
└── learn_records — 学习记录
courses — 课程表
├── id, title, description, cover, category, price, status
@@ -172,15 +144,17 @@ contents — 内容(文章/资讯)
admin_users — 管理员表
├── id, username, password_hash, role, permission, last_login
└── admin_logs — 操作日志
skills — 技能包表
├── id, name, description, category, difficulty, system_prompt, icon
└── skill_tasks — 练习任务
```
### 3.2 数据库设计原则
### 3.2 Schema 管理
- **软删除**:所有业务表增加 `deleted_at` 字段
- **审计字段**`created_at``updated_at` 必备
- **索引策略**:覆盖常用查询场景,避免全表扫描
- **分表策略**`sandbox_sessions` 按用户 ID 分表
- **读写分离**:主库写入,从库查询
- 使用 Prisma Migrate 管理 schema 变更
- migration 文件位于 `backend/prisma/migrations/`
- 种子数据在 `backend/prisma/seed.ts`
---
@@ -206,6 +180,11 @@ GET /api/v1/sandbox/quota — 沙箱额度查询
POST /api/v1/orders/create — 创建订单
GET /api/v1/orders/:id — 订单查询
POST /api/v1/orders/callback — 支付回调
GET /api/v1/skills — 技能列表(支持过滤)
GET /api/v1/skills/:id — 技能详情
GET /api/v1/skills/categories — 技能分类
GET /api/v1/skills/difficulties — 难度等级
```
### 4.2 通用响应格式
@@ -223,68 +202,58 @@ POST /api/v1/orders/callback — 支付回调
}
```
### 4.3 鉴权方案
- **JWT Token**Access Token 2h + Refresh Token 7d
- 管理后台:Session + Cookie
- API 签名:管理端 API 需签名校验
---
## 5. 部署架构
### 5.1 当前部署
```
┌─────────────┐
DNS
│ (阿里云 DNS)
PM2
│ (进程管理)
└──────┬──────┘
┌──────▼──────┐
│ CDN │
│ (静态资源) │
└──────┬──────┘
┌──────▼──────┐
│ SLB │
│ (负载均衡) │
└──────┬──────┘
┌────────────┼────────────┐
│ │ │
┌──────▼──┐ ┌─────▼────┐ ┌────▼────┐
│ Next.js │ │ NestJS │ │ Admin │
│ 官网 │ │ API 服务 │ │ Panel │
└─────────┘ └─────┬────┘ └─────────┘
┌───────────┼───────────┐
│ │ │
┌──────▼──┐ ┌───────┐ ┌───▼────┐
MySQL │ │ Redis │ │ ES
│ RDS │ │ │ │
└─────────┘ └────────┘ └────────┘
┌──────▼──┐ ┌─────▼────┐ ┌───▼────┐
Node.js │ │ NestJS │ │ Redis
│ server.js│ │ API 服务 │ │ │
│ (静态) │ │ PM2 │ └────────┘
└─────────┘ └─────┬────┘
┌──────▼──────┐
│ MySQL │
│ 8.0 │
└─────────────┘
```
### 5.1 环境规划
| 服务 | 端口 | 管理方式 |
|------|------|----------|
| 前端 (静态文件) | 3000 | PM2: `node server.js` |
| 后端 (NestJS) | 4000 | PM2: `node dist/main.js` |
| MySQL | 3306 | 系统服务 |
| Redis | 6379 | 系统服务 |
| 环境 | 用途 | 配置 |
|------|------|------|
| 开发 (dev) | 本地开发 | 个人电脑 / Docker Compose |
| 测试 (staging) | 联调测试 | 阿里云 ECS 2C4G |
| 生产 (production) | 正式运营 | 阿里云 ECS 4C8G × 2 + RDS 2C4G |
### 5.2 CI/CD 流程
### 5.2 构建流程
```
Git Push → GitHub Actions
├── Lint & Type Check
├── Unit Test
├── Build
└── Deploy to 阿里云
├── 构建 Docker 镜像
├── Push 到阿里云 CR
└── 滚动更新 ECS/Pod
# 前端
npm run build → out/ (静态文件)
pm2 restart frontend
# 后端
npx nest build → dist/
pm2 restart backend --update-env
```
### 5.3 环境
| 环境 | 用途 |
|------|------|
| 开发 (dev) | 本地 `npm run dev` / `npm run start:dev` |
| 生产 (production) | PM2 + 静态导出 |
---
## 6. 性能与安全
@@ -296,18 +265,15 @@ Git Push → GitHub Actions
| API 响应时间 (P95) | < 200ms |
| 首屏加载时间 | < 1.5s |
| AI 对话首 Token 延迟 | < 1s |
| 并发用户 | 支持 1000+ 同时在线 |
| 系统可用性 | 99.9% |
| 静态页面加载 | 即时(CDN 缓存) |
### 6.2 安全措施
- HTTPS 全站加密
- API 限流(单用户 100次/分钟
- SQL 注入防护(Prisma ORM 参数化查询)
- XSS/CSRF 防护
- JWT 鉴权(沙箱等核心 API
- API 限流
- 密码 bcrypt 加密
- 敏感信息脱敏
- 定期安全扫描 + 渗透测试
---
@@ -315,8 +281,7 @@ Git Push → GitHub Actions
| 阶段 | 任务 |
|------|------|
| MVP | 单体应用快速验证 |
| V2 | 服务化拆分 |
| V3 | AI 网关独立部署 |
| V4 | 引入 K8s 容器编排 |
| V5 | 多数据中心容灾 |
| 当前 | 单体 NestJS + Next.js 静态导出 |
| V2 | AI 网关独立部署 |
| V3 | 微信小程序上线 |
| V4 | 服务化拆分 |
+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';