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
+20
View File
@@ -263,6 +263,26 @@ model AiModel {
@@map("ai_models")
}
model Skill {
id String @id
name String
description String
icon String
category String
difficulty String
systemPrompt String @db.Text
starters String @db.Text
tasks String @db.Text
tags String
prerequisiteIds String?
sortOrder Int @default(0)
status String @default("ACTIVE")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("skills")
}
model SandboxSession {
id Int @id @default(autoincrement())
userId Int
+169 -64
View File
@@ -15,30 +15,62 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
})();
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const bcrypt = __importStar(require("bcryptjs"));
const prisma = new client_1.PrismaClient();
async function main() {
const adminPassword = await bcrypt.hash('admin123456', 10);
await prisma.adminUser.upsert({
var client_1 = require("@prisma/client");
var bcrypt = __importStar(require("bcryptjs"));
var prisma = new client_1.PrismaClient();
function main() {
return __awaiter(this, void 0, void 0, function () {
var adminPassword, categories, _i, categories_1, cat, basicsCategory, promptCategory, course, prompts, _a, prompts_1, prompt_1, toolsCategory, tools, _b, tools_1, tool;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, bcrypt.hash('admin123456', 10)];
case 1:
adminPassword = _c.sent();
return [4 /*yield*/, prisma.adminUser.upsert({
where: { username: 'admin' },
update: {},
create: {
@@ -47,8 +79,10 @@ async function main() {
nickname: '超级管理员',
role: 'superadmin',
},
});
const categories = [
})];
case 2:
_c.sent();
categories = [
{ name: 'AI 入门', slug: 'ai-basics', description: 'AI 基础知识与入门指南' },
{ name: '办公效率', slug: 'office-productivity', description: '用 AI 提升办公效率' },
{ name: '创意设计', slug: 'creative-design', description: 'AI 辅助创意与设计' },
@@ -58,17 +92,30 @@ async function main() {
{ name: 'AI 工具', slug: 'ai-tools', description: 'AI 工具收录与评测' },
{ name: '模型百科', slug: 'model-encyclopedia', description: '大模型介绍与对比' },
];
for (const cat of categories) {
await prisma.category.upsert({
_i = 0, categories_1 = categories;
_c.label = 3;
case 3:
if (!(_i < categories_1.length)) return [3 /*break*/, 6];
cat = categories_1[_i];
return [4 /*yield*/, prisma.category.upsert({
where: { slug: cat.slug },
update: {},
create: cat,
});
}
const basicsCategory = await prisma.category.findUnique({ where: { slug: 'ai-basics' } });
const promptCategory = await prisma.category.findUnique({ where: { slug: 'prompt-engineering' } });
if (basicsCategory) {
const course = await prisma.course.upsert({
})];
case 4:
_c.sent();
_c.label = 5;
case 5:
_i++;
return [3 /*break*/, 3];
case 6: return [4 /*yield*/, prisma.category.findUnique({ where: { slug: 'ai-basics' } })];
case 7:
basicsCategory = _c.sent();
return [4 /*yield*/, prisma.category.findUnique({ where: { slug: 'prompt-engineering' } })];
case 8:
promptCategory = _c.sent();
if (!basicsCategory) return [3 /*break*/, 10];
return [4 /*yield*/, prisma.course.upsert({
where: { id: 1 },
update: {},
create: {
@@ -105,52 +152,110 @@ async function main() {
],
},
},
});
}
if (promptCategory) {
const prompts = [
})];
case 9:
course = _c.sent();
_c.label = 10;
case 10:
if (!promptCategory) return [3 /*break*/, 14];
prompts = [
{ title: '写周报助手', content: '请帮我写一份本周工作总结,我的工作是:[描述你的工作内容]。\n\n要求:\n1. 按重点项目分类\n2. 包含数据成果\n3. 列出下周计划\n4. 语言简洁专业', description: '帮你快速生成高质量周报', categoryId: promptCategory.id, tags: '办公,周报', model: '通用', status: 'PUBLISHED' },
{ title: '会议纪要生成', content: '请根据以下会议内容生成会议纪要:\n[粘贴会议内容]\n\n格式要求:\n- 时间\n- 参会人\n- 会议议题\n- 讨论要点\n- 待办事项及负责人', description: '快速整理会议内容为结构化纪要', categoryId: promptCategory.id, tags: '办公,会议', model: '通用', status: 'PUBLISHED' },
];
for (const prompt of prompts) {
await prisma.prompt.create({ data: prompt });
}
}
const toolsCategory = await prisma.category.findUnique({ where: { slug: 'ai-tools' } });
if (toolsCategory) {
const tools = [
_a = 0, prompts_1 = prompts;
_c.label = 11;
case 11:
if (!(_a < prompts_1.length)) return [3 /*break*/, 14];
prompt_1 = prompts_1[_a];
return [4 /*yield*/, prisma.prompt.create({ data: prompt_1 })];
case 12:
_c.sent();
_c.label = 13;
case 13:
_a++;
return [3 /*break*/, 11];
case 14: return [4 /*yield*/, prisma.category.findUnique({ where: { slug: 'ai-tools' } })];
case 15:
toolsCategory = _c.sent();
if (!toolsCategory) return [3 /*break*/, 19];
tools = [
{ name: 'ChatGPT', description: 'OpenAI 开发的对话式 AI 助手,支持文本生成、代码编写、分析等', url: 'https://chat.openai.com', categoryId: toolsCategory.id, tags: '对话,文本生成,代码', isFeatured: true, status: 'PUBLISHED' },
{ name: 'Claude', description: 'Anthropic 开发的 AI 助手,擅长深度分析、长文本处理', url: 'https://claude.ai', categoryId: toolsCategory.id, tags: '对话,分析,长文本', isFeatured: true, status: 'PUBLISHED' },
{ name: '通义千问', description: '阿里云开发的 AI 大模型,支持文本、图像、代码等多种任务', url: 'https://tongyi.aliyun.com', categoryId: toolsCategory.id, tags: '对话,文本,国内', isFeatured: true, status: 'PUBLISHED' },
{ name: '文心一言', description: '百度开发的 AI 对话产品,基于文心大模型', url: 'https://yiyan.baidu.com', categoryId: toolsCategory.id, tags: '对话,文本,国内', isFeatured: true, status: 'PUBLISHED' },
];
for (const tool of tools) {
await prisma.tool.create({ data: tool });
}
}
const models = [
{ name: 'GPT-4o', provider: 'OpenAI', description: 'OpenAI 的多模态旗舰模型,支持文本、图像、音频输入,性能全面领先', capabilities: '文本生成,图像理解,代码,分析,多语言', contextWindow: 128000, maxTokens: 4096, pricing: '付费 API', isFeatured: true, sortOrder: 1 },
{ name: 'GPT-4o-mini', provider: 'OpenAI', description: 'GPT-4o 的轻量版,速度快、成本低,适合日常对话和简单任务', capabilities: '文本生成,代码,分析', contextWindow: 128000, maxTokens: 16384, pricing: '付费 API', isFeatured: false, sortOrder: 2 },
{ name: 'Claude 3.5 Sonnet', provider: 'Anthropic', description: 'Anthropic 的旗舰模型,在编程、深度分析、长文本处理方面表现优异', capabilities: '文本生成,代码,分析,长文本,多语言', contextWindow: 200000, maxTokens: 8192, pricing: '付费 API', isFeatured: true, sortOrder: 3 },
{ name: 'DeepSeek-V3', provider: '深度求索', description: '国产开源大模型,推理能力强,数学和编程能力突出,性价比极高', capabilities: '文本生成,代码,数学推理,分析', contextWindow: 128000, maxTokens: 8192, pricing: '免费/付费 API', isFeatured: true, sortOrder: 4 },
{ name: 'DeepSeek-R1', provider: '深度求索', description: '专注推理的模型,擅长复杂逻辑推理、数学问题,思维链能力强大', capabilities: '推理,数学,逻辑,代码', contextWindow: 128000, maxTokens: 8192, pricing: '免费/付费 API', isFeatured: true, sortOrder: 5 },
{ name: '通义千问 2.5', provider: '阿里云', description: '阿里云推出的最新版 Qwen 模型,中英文能力优秀,支持图像理解', capabilities: '文本生成,图像理解,代码,多语言', contextWindow: 131072, maxTokens: 8192, pricing: '免费/付费 API', isFeatured: true, sortOrder: 6 },
{ name: 'GLM-4', provider: '智谱 AI', description: '智谱 AI 的第四代 GLM 模型,中文理解能力强,支持多模态和工具调用', capabilities: '文本生成,代码,工具调用,多模态', contextWindow: 128000, maxTokens: 4096, pricing: '免费/付费 API', isFeatured: false, sortOrder: 7 },
{ name: '文心一言 4.0', provider: '百度', description: '百度文心大模型 4.0,知识问答和中文理解能力强,支持多种格式', capabilities: '文本生成,知识问答,图像生成,代码', contextWindow: 8000, maxTokens: 4096, pricing: '付费 API', isFeatured: false, sortOrder: 8 },
{ name: 'Moonshot', provider: '月之暗面', description: 'Kimi 背后的模型,超长上下文窗口(200 万字),擅长长文档处理', capabilities: '长文本,文档分析,文本生成', contextWindow: 128000, maxTokens: 4096, pricing: '免费/付费 API', isFeatured: false, sortOrder: 9 },
{ name: 'Gemini 2.0 Flash', provider: 'Google', description: 'Google 的轻量高速模型,多模态能力强,支持图片/视频/音频理解', capabilities: '多模态,图像理解,音频,视频,代码', contextWindow: 1000000, maxTokens: 8192, pricing: '免费 API', isFree: true, isFeatured: false, sortOrder: 10 },
];
for (const model of models) {
await prisma.aiModel.create({ data: model });
}
_b = 0, tools_1 = tools;
_c.label = 16;
case 16:
if (!(_b < tools_1.length)) return [3 /*break*/, 19];
tool = tools_1[_b];
return [4 /*yield*/, prisma.tool.create({ data: tool })];
case 17:
_c.sent();
_c.label = 18;
case 18:
_b++;
return [3 /*break*/, 16];
case 19: return [4 /*yield*/, seedSkills()];
case 20:
_c.sent();
console.log('Seed data created successfully!');
return [2 /*return*/];
}
});
});
}
function seedSkills() {
return __awaiter(this, void 0, void 0, function () {
var skills, _i, skills_1, skill;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
skills = [
{ id: 'general-chat', name: '通用对话', description: '日常问答,无所不谈', icon: '💬', category: 'basic', difficulty: 'beginner', systemPrompt: '你是一个智能 AI 助手,请友好、准确地回答用户的问题。', starters: JSON.stringify(['介绍一下你自己', '今天天气怎么样', '讲个笑话']), tasks: JSON.stringify([{ label: '发起一次对话', prompt: '你好' }]), tags: '入门,通用', prerequisiteIds: null, sortOrder: 1 },
{ id: 'coding', name: '编程助手', description: '写代码、Debug、学编程', icon: '💻', category: 'technical', difficulty: 'intermediate', systemPrompt: '你是一名资深软件工程师,擅长编程教学。请用清晰的代码示例和通俗的语言解释技术概念。回答时优先提供可运行的代码。', starters: JSON.stringify(['用 Python 写一个二分查找', 'React 和 Vue 有什么区别', '帮我 Debug 这段代码']), tasks: JSON.stringify([{ label: '要求代码示例', prompt: '用 Python 写一个二分查找' }]), tags: '编程,Python,Debug', prerequisiteIds: null, sortOrder: 2 },
{ id: 'writing', name: '写作助手', description: '文章、文案、报告润色', icon: '✍️', category: 'creative', difficulty: 'beginner', systemPrompt: '你是一名专业的写作顾问,擅长各类文体写作。请根据用户需求提供高质量的文字内容,注意逻辑清晰、表达准确。', starters: JSON.stringify(['帮我写一篇产品介绍', '润色这段文字', '写一封工作邮件']), tasks: JSON.stringify([{ label: '练习写作', prompt: '帮我写一篇产品介绍' }]), tags: '写作,文案,润色', prerequisiteIds: null, sortOrder: 3 },
{ id: 'study', name: '学习辅导', description: '概念讲解、知识总结', icon: '📚', category: 'education', difficulty: 'beginner', systemPrompt: '你是一名耐心且知识渊博的老师。请用通俗易懂的方式解释复杂概念,善用类比和例子,鼓励用户深入提问。', starters: JSON.stringify(['解释什么是机器学习', '讲一下 TCP/IP 协议', '怎么理解量子计算']), tasks: JSON.stringify([{ label: '学习概念', prompt: '用简单的类比解释什么是机器学习' }]), tags: '学习,概念讲解', prerequisiteIds: null, sortOrder: 4 },
{ id: 'english', name: '英语学习', description: '翻译、语法、口语练习', icon: '🌍', category: 'education', difficulty: 'intermediate', systemPrompt: 'You are an English tutor. Help users improve their English.', starters: JSON.stringify(['"However" 和 "Although" 的区别', '帮我翻译这段话', '检查语法错误']), tasks: JSON.stringify([{ label: '语法练习', prompt: '"I have went to school" 有什么语法错误' }]), tags: '英语,翻译,语法', prerequisiteIds: null, sortOrder: 5 },
{ id: 'prompt-engineering', name: '提示词工程', description: '学习编写高质量提示词', icon: '🎯', category: 'advanced', difficulty: 'advanced', systemPrompt: '你是一名提示词工程专家。帮助用户理解如何构建有效的提示词。', starters: JSON.stringify(['如何写好一个提示词', '什么是 Chain-of-Thought']), tasks: JSON.stringify([{ label: '学习提示词结构', prompt: '一个好的提示词应该包含哪些要素' }]), tags: '提示词,Prompt,进阶', prerequisiteIds: 'general-chat', sortOrder: 6 },
{ id: 'data-analysis', name: '数据分析', description: '数据处理、可视化、报表', icon: '📊', category: 'technical', difficulty: 'intermediate', systemPrompt: '你是一名数据分析师。擅长使用 Python、SQL 等工具进行数据分析和可视化。', starters: JSON.stringify(['用 Python 分析这份数据', '这个 SQL 查询怎么优化']), tasks: JSON.stringify([{ label: '数据分析入门', prompt: '用 Python pandas 读取 CSV 文件并做基本统计' }]), tags: '数据分析,Python,SQL', prerequisiteIds: null, sortOrder: 7 },
{ id: 'career', name: '职业发展', description: '简历优化、面试准备', icon: '🚀', category: 'career', difficulty: 'beginner', systemPrompt: '你是一名职业发展顾问。帮助用户优化简历、准备面试。', starters: JSON.stringify(['帮我优化简历', '面试注意事项']), tasks: JSON.stringify([{ label: '简历优化', prompt: '帮我优化这段工作经历描述' }]), tags: '职业,简历,面试', prerequisiteIds: null, sortOrder: 8 },
];
_i = 0, skills_1 = skills;
_a.label = 1;
case 1:
if (!(_i < skills_1.length)) return [3 /*break*/, 4];
skill = skills_1[_i];
return [4 /*yield*/, prisma.skill.upsert({
where: { id: skill.id },
create: skill,
update: skill,
})];
case 2:
_a.sent();
_a.label = 3;
case 3:
_i++;
return [3 /*break*/, 1];
case 4:
console.log("".concat(skills.length, " skills seeded"));
return [2 /*return*/];
}
});
});
}
main()
.catch((e) => {
.catch(function (e) {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
//# sourceMappingURL=seed.js.map
.finally(function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, prisma.$disconnect()];
case 1:
_a.sent();
return [2 /*return*/];
}
});
}); });
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"seed.js","sourceRoot":"","sources":["seed.ts"],"names":[],"mappings":"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"}
+24
View File
@@ -109,9 +109,33 @@ async function main() {
}
}
await seedSkills();
console.log('Seed data created successfully!');
}
async function seedSkills() {
const skills = [
{ id: 'general-chat', name: '通用对话', description: '日常问答,无所不谈', icon: '💬', category: 'basic', difficulty: 'beginner', systemPrompt: '你是一个智能 AI 助手,请友好、准确地回答用户的问题。', starters: JSON.stringify(['介绍一下你自己', '今天天气怎么样', '讲个笑话']), tasks: JSON.stringify([{ label: '发起一次对话', prompt: '你好' }]), tags: '入门,通用', prerequisiteIds: null, sortOrder: 1 },
{ id: 'coding', name: '编程助手', description: '写代码、Debug、学编程', icon: '💻', category: 'technical', difficulty: 'intermediate', systemPrompt: '你是一名资深软件工程师,擅长编程教学。请用清晰的代码示例和通俗的语言解释技术概念。回答时优先提供可运行的代码。', starters: JSON.stringify(['用 Python 写一个二分查找', 'React 和 Vue 有什么区别', '帮我 Debug 这段代码']), tasks: JSON.stringify([{ label: '要求代码示例', prompt: '用 Python 写一个二分查找' }]), tags: '编程,Python,Debug', prerequisiteIds: null, sortOrder: 2 },
{ id: 'writing', name: '写作助手', description: '文章、文案、报告润色', icon: '✍️', category: 'creative', difficulty: 'beginner', systemPrompt: '你是一名专业的写作顾问,擅长各类文体写作。请根据用户需求提供高质量的文字内容,注意逻辑清晰、表达准确。', starters: JSON.stringify(['帮我写一篇产品介绍', '润色这段文字', '写一封工作邮件']), tasks: JSON.stringify([{ label: '练习写作', prompt: '帮我写一篇产品介绍' }]), tags: '写作,文案,润色', prerequisiteIds: null, sortOrder: 3 },
{ id: 'study', name: '学习辅导', description: '概念讲解、知识总结', icon: '📚', category: 'education', difficulty: 'beginner', systemPrompt: '你是一名耐心且知识渊博的老师。请用通俗易懂的方式解释复杂概念,善用类比和例子,鼓励用户深入提问。', starters: JSON.stringify(['解释什么是机器学习', '讲一下 TCP/IP 协议', '怎么理解量子计算']), tasks: JSON.stringify([{ label: '学习概念', prompt: '用简单的类比解释什么是机器学习' }]), tags: '学习,概念讲解', prerequisiteIds: null, sortOrder: 4 },
{ id: 'english', name: '英语学习', description: '翻译、语法、口语练习', icon: '🌍', category: 'education', difficulty: 'intermediate', systemPrompt: 'You are an English tutor. Help users improve their English.', starters: JSON.stringify(['"However" 和 "Although" 的区别', '帮我翻译这段话', '检查语法错误']), tasks: JSON.stringify([{ label: '语法练习', prompt: '"I have went to school" 有什么语法错误' }]), tags: '英语,翻译,语法', prerequisiteIds: null, sortOrder: 5 },
{ id: 'prompt-engineering', name: '提示词工程', description: '学习编写高质量提示词', icon: '🎯', category: 'advanced', difficulty: 'advanced', systemPrompt: '你是一名提示词工程专家。帮助用户理解如何构建有效的提示词。', starters: JSON.stringify(['如何写好一个提示词', '什么是 Chain-of-Thought']), tasks: JSON.stringify([{ label: '学习提示词结构', prompt: '一个好的提示词应该包含哪些要素' }]), tags: '提示词,Prompt,进阶', prerequisiteIds: 'general-chat', sortOrder: 6 },
{ id: 'data-analysis', name: '数据分析', description: '数据处理、可视化、报表', icon: '📊', category: 'technical', difficulty: 'intermediate', systemPrompt: '你是一名数据分析师。擅长使用 Python、SQL 等工具进行数据分析和可视化。', starters: JSON.stringify(['用 Python 分析这份数据', '这个 SQL 查询怎么优化']), tasks: JSON.stringify([{ label: '数据分析入门', prompt: '用 Python pandas 读取 CSV 文件并做基本统计' }]), tags: '数据分析,Python,SQL', prerequisiteIds: null, sortOrder: 7 },
{ id: 'career', name: '职业发展', description: '简历优化、面试准备', icon: '🚀', category: 'career', difficulty: 'beginner', systemPrompt: '你是一名职业发展顾问。帮助用户优化简历、准备面试。', starters: JSON.stringify(['帮我优化简历', '面试注意事项']), tasks: JSON.stringify([{ label: '简历优化', prompt: '帮我优化这段工作经历描述' }]), tags: '职业,简历,面试', prerequisiteIds: null, sortOrder: 8 },
];
for (const skill of skills) {
await prisma.skill.upsert({
where: { id: skill.id },
create: skill,
update: skill,
});
}
console.log(`${skills.length} skills seeded`);
}
main()
.catch((e) => {
console.error(e);
+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: '中级' },
File diff suppressed because one or more lines are too long
+3 -5
View File
@@ -31,11 +31,9 @@
### P3 — 体验增强(全部完成 ✅)
| 优先级 | 项目 | 说明 | 状态 |
|--------|------|------|------|
| **P3a** | 代码语法高亮 | highlight.js + CodeBlock 组件,10+ 语言 | ✅ |
| **P3b** | 会话重命名 | PATCH endpoint + 双击编辑 | ✅ |
| **P3c** | LLM 集成层增强 | 模型能力目录 + 用量统计 + getModelInfo | ✅ |
| **P3d** | Mobile 样式复查 | 0 残留硬编码颜色 | ✅ |
| — | 多模态(文件上传/图片理解) | 远期规划 | 📋 |
| **P4a** | 多模态 — 图片上传 + 沙盒 Vision | ✅ 完成 |
| **P4b** | 代码语法高亮 | highlight.js + CodeBlock 组件 | ✅ |
| **P3c** | 会话重命名 | PATCH endpoint + 双击编辑 | ✅ |
| — | SDK / 共享 UI 包 | 远期规划 | 📋 |
### 参考来源
+10 -1
View File
@@ -292,7 +292,16 @@ function SandboxPage() {
}).then(() => alert('分享成功!')).catch(() => alert('分享失败'));
}
const currentScene = SCENES.find(s => s.id === scene) || SCENES[0];
const currentScene = SCENES.find(s => s.id === scene) || SCENES[0] || null;
if (skillsLoading) return (
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="animate-pulse space-y-4">
<div className="h-8 w-48 bg-muted rounded-lg" />
<div className="h-[65vh] bg-muted rounded-2xl" />
</div>
</div>
);
const isNewChat = messages.length <= 1 && messages[0]?.role === 'assistant';
return (