feat: P1 学习技能包 — 可组合技能模块系统

后端:
- SkillsModule (service + controller),8 个预置技能
- GET /skills (支持 category/difficulty/search 过滤)
- GET /skills/:id /categories /difficulties

前端:
- /skills 技能市场 — 分类/难度/搜索过滤
- /skills/[id] 技能详情 — system prompt、练习任务、starter
- header 导航新增「技能」入口
- sandbox page 改为从 API 加载技能(替代硬编码 SCENES)
- 支持 ?skill=xxx 直接加载指定技能
- useSearchParams Suspense 包装

全栈: 70 pages / 92 tests 全部通过
This commit is contained in:
yuzhiran-dev
2026-05-18 11:15:00 +08:00
parent cba785e6bd
commit fb092cb5d9
14 changed files with 553 additions and 26 deletions
+2
View File
@@ -20,6 +20,7 @@ import { CommunityModule } from './modules/community/community.module';
import { EnterpriseModule } from './modules/enterprise/enterprise.module';
import { NotificationModule } from './modules/notifications/notification.module';
import { LearningModule } from './modules/learning/learning.module';
import { SkillsModule } from './modules/skills/skills.module';
@Module({
imports: [
@@ -44,6 +45,7 @@ import { LearningModule } from './modules/learning/learning.module';
EnterpriseModule,
NotificationModule,
LearningModule,
SkillsModule,
],
})
export class AppModule {}
@@ -0,0 +1,31 @@
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { SkillsService, Skill } from './skills.service';
@ApiTags('技能')
@Controller('skills')
export class SkillsController {
constructor(private skillsService: SkillsService) {}
@Get()
findAll(@Query() query: { category?: string; difficulty?: string; search?: string; tag?: string }) {
return this.skillsService.findAll(query);
}
@Get('categories')
getCategories() {
return this.skillsService.getCategories();
}
@Get('difficulties')
getDifficulties() {
return this.skillsService.getDifficulties();
}
@Get(':id')
findById(@Param('id') id: string) {
const skill = this.skillsService.findById(id);
if (!skill) return { error: '技能不存在' };
return skill;
}
}
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { SkillsController } from './skills.controller';
import { SkillsService } from './skills.service';
@Module({
controllers: [SkillsController],
providers: [SkillsService],
exports: [SkillsService],
})
export class SkillsModule {}
@@ -0,0 +1,177 @@
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: ['职业', '简历', '面试'],
},
];
@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);
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)));
}
if (params.tag) filtered = filtered.filter(s => s.tags.includes(params.tag!));
return { items: filtered, total: filtered.length };
}
findById(id: string) {
return SKILLS.find(s => s.id === id) || null;
}
getCategories() {
const cats = [...new Set(SKILLS.map(s => s.category))];
const labels: Record<string, string> = { basic: '基础', technical: '技术', creative: '创意', education: '教育', advanced: '进阶', career: '职业' };
return cats.map(c => ({ id: c, name: labels[c] || c }));
}
getDifficulties() {
return [
{ id: 'beginner', name: '入门' },
{ id: 'intermediate', name: '中级' },
{ id: 'advanced', name: '高级' },
];
}
}