feat: 技能库改为数据库存储 (Prisma Skill 模型 + seed)

- Prisma 新增 Skill 模型 (id/name/description/icon/category/difficulty/systemPrompt/starters/tasks/tags/prerequisiteIds)
- SkillsService 从 DB 读取替代硬编码常量
- seed.ts 新增 seedSkills() 写入 8 个技能
- SkillsController 保持不变,API 返回 JSON 解析后的 starters/tasks
This commit is contained in:
yuzhiran-dev
2026-05-18 16:23:52 +08:00
parent 79d7535c1c
commit 150ae3dcfd
8 changed files with 337 additions and 294 deletions
+45 -157
View File
@@ -1,173 +1,61 @@
import { Injectable } from '@nestjs/common';
export interface SkillTask {
label: string;
prompt: string;
}
export interface Skill {
id: string;
name: string;
description: string;
icon: string;
category: string;
difficulty: 'beginner' | 'intermediate' | 'advanced';
systemPrompt: string;
starters: string[];
tasks: SkillTask[];
tags: string[];
prerequisites?: string[];
}
const SKILLS: Skill[] = [
{
id: 'general-chat',
name: '通用对话',
description: '日常问答,无所不谈',
icon: '💬',
category: 'basic',
difficulty: 'beginner',
systemPrompt: '你是一个智能 AI 助手,请友好、准确地回答用户的问题。',
starters: ['介绍一下你自己', '今天天气怎么样', '讲个笑话'],
tasks: [
{ label: '发起一次对话', prompt: '你好' },
{ label: '追问细节', prompt: '能详细说说吗' },
],
tags: ['入门', '通用'],
},
{
id: 'coding',
name: '编程助手',
description: '写代码、Debug、学编程',
icon: '💻',
category: 'technical',
difficulty: 'intermediate',
systemPrompt: '你是一名资深软件工程师,擅长编程教学。请用清晰的代码示例和通俗的语言解释技术概念。回答时优先提供可运行的代码。',
starters: ['用 Python 写一个二分查找', 'React 和 Vue 有什么区别', '帮我 Debug 这段代码'],
tasks: [
{ label: '要求代码示例', prompt: '用 Python 写一个二分查找' },
{ label: 'Debug 练习', prompt: '这段代码为什么报错:\n```python\ndef foo():\n return 1/0\n```' },
],
tags: ['编程', 'Python', 'Debug'],
},
{
id: 'writing',
name: '写作助手',
description: '文章、文案、报告润色',
icon: '✍️',
category: 'creative',
difficulty: 'beginner',
systemPrompt: '你是一名专业的写作顾问,擅长各类文体写作。请根据用户需求提供高质量的文字内容,注意逻辑清晰、表达准确。',
starters: ['帮我写一篇产品介绍', '润色这段文字', '写一封工作邮件'],
tasks: [
{ label: '练习写作', prompt: '帮我写一篇产品介绍' },
{ label: '润色修改', prompt: '请帮我润色这段文字,让它更专业' },
],
tags: ['写作', '文案', '润色'],
},
{
id: 'study',
name: '学习辅导',
description: '概念讲解、知识总结',
icon: '📚',
category: 'education',
difficulty: 'beginner',
systemPrompt: '你是一名耐心且知识渊博的老师。请用通俗易懂的方式解释复杂概念,善用类比和例子,鼓励用户深入提问。',
starters: ['解释什么是机器学习', '讲一下 TCP/IP 协议', '怎么理解量子计算'],
tasks: [
{ label: '学习概念', prompt: '用简单的类比解释什么是机器学习' },
{ label: '深入提问', prompt: '机器学习和深度学习有什么区别' },
],
tags: ['学习', '概念讲解'],
},
{
id: 'english',
name: '英语学习',
description: '翻译、语法、口语练习',
icon: '🌍',
category: 'education',
difficulty: 'intermediate',
systemPrompt: 'You are an English tutor. Help users improve their English. Respond primarily in Chinese but provide English examples. Correct grammar and offer better expressions.',
starters: ['"However" 和 "Although" 的区别', '帮我翻译这段话', '检查语法错误'],
tasks: [
{ label: '语法练习', prompt: '"I have went to school" 有什么语法错误' },
{ label: '翻译练习', prompt: '把这段话翻译成英文:今天天气很好' },
],
tags: ['英语', '翻译', '语法'],
},
{
id: 'prompt-engineering',
name: '提示词工程',
description: '学习编写高质量提示词',
icon: '🎯',
category: 'advanced',
difficulty: 'advanced',
systemPrompt: '你是一名提示词工程专家。帮助用户理解如何构建有效的提示词,包括角色设定、任务描述、输出格式和约束条件。给出具体示例和最佳实践。',
starters: ['如何写好一个提示词', '什么是 Chain-of-Thought', '示例:角色提示词'],
tasks: [
{ label: '学习提示词结构', prompt: '一个好的提示词应该包含哪些要素' },
{ label: '实践编写', prompt: '帮我设计一个提示词,角色是资深编辑,任务是润色文章' },
],
tags: ['提示词', 'Prompt', '进阶'],
prerequisites: ['general-chat'],
},
{
id: 'data-analysis',
name: '数据分析',
description: '数据处理、可视化、报表',
icon: '📊',
category: 'technical',
difficulty: 'intermediate',
systemPrompt: '你是一名数据分析师。擅长使用 Python、SQL 等工具进行数据分析和可视化。请提供清晰的代码和图表描述。',
starters: ['用 Python 分析这份数据', '这个 SQL 查询怎么优化', '帮我做个数据可视化'],
tasks: [
{ label: '数据分析入门', prompt: '用 Python pandas 读取 CSV 文件并做基本统计' },
{ label: '可视化练习', prompt: '帮我用 matplotlib 画一个柱状图' },
],
tags: ['数据分析', 'Python', 'SQL'],
},
{
id: 'career',
name: '职业发展',
description: '简历优化、面试准备',
icon: '🚀',
category: 'career',
difficulty: 'beginner',
systemPrompt: '你是一名职业发展顾问。帮助用户优化简历、准备面试、规划职业发展。提供具体、可操作的建议。',
starters: ['帮我优化简历', '面试注意事项', '职业规划建议'],
tasks: [
{ label: '简历优化', prompt: '帮我优化这段工作经历描述,让它更有吸引力' },
{ label: '模拟面试', prompt: '模拟一次前端工程师的技术面试' },
],
tags: ['职业', '简历', '面试'],
},
];
import { PrismaService } from '../../prisma/prisma.service';
@Injectable()
export class SkillsService {
findAll(params: { category?: string; difficulty?: string; search?: string; tag?: string }) {
let filtered = [...SKILLS];
if (params.category) filtered = filtered.filter(s => s.category === params.category);
if (params.difficulty) filtered = filtered.filter(s => s.difficulty === params.difficulty);
constructor(private prisma: PrismaService) {}
async findAll(params: { category?: string; difficulty?: string; search?: string; tag?: string }) {
const where: any = { status: 'ACTIVE' };
if (params.category) where.category = params.category;
if (params.difficulty) where.difficulty = params.difficulty;
if (params.search) {
const q = params.search.toLowerCase();
filtered = filtered.filter(s => s.name.includes(q) || s.description.includes(q) || s.tags.some(t => t.includes(q)));
const q = params.search;
where.OR = [
{ name: { contains: q } },
{ description: { contains: q } },
{ tags: { contains: q } },
];
}
if (params.tag) filtered = filtered.filter(s => s.tags.includes(params.tag!));
return { items: filtered, total: filtered.length };
if (params.tag) where.tags = { contains: params.tag };
const items = await this.prisma.skill.findMany({
where,
orderBy: { sortOrder: 'asc' },
});
return {
items: items.map(s => ({
...s,
starters: JSON.parse(s.starters),
tasks: JSON.parse(s.tasks),
})),
total: items.length,
};
}
findById(id: string) {
return SKILLS.find(s => s.id === id) || null;
async findById(id: string) {
const skill = await this.prisma.skill.findUnique({ where: { id } });
if (!skill) return null;
return {
...skill,
starters: JSON.parse(skill.starters),
tasks: JSON.parse(skill.tasks),
};
}
getCategories() {
const cats = [...new Set(SKILLS.map(s => s.category))];
async getCategories() {
const cats = await this.prisma.skill.findMany({
where: { status: 'ACTIVE' },
select: { category: true },
distinct: ['category'],
orderBy: { sortOrder: 'asc' },
});
const labels: Record<string, string> = { basic: '基础', technical: '技术', creative: '创意', education: '教育', advanced: '进阶', career: '职业' };
return cats.map(c => ({ id: c, name: labels[c] || c }));
return cats.map(c => ({ id: c.category, name: labels[c.category] || c.category }));
}
getDifficulties() {
async getDifficulties() {
return [
{ id: 'beginner', name: '入门' },
{ id: 'intermediate', name: '中级' },