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:
@@ -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: '中级' },
|
||||
|
||||
Reference in New Issue
Block a user