Compare commits
6 Commits
0f215d2aad
...
f4f17c10d8
| Author | SHA1 | Date | |
|---|---|---|---|
| f4f17c10d8 | |||
| c10a33e9f8 | |||
| 5ddd7e3a82 | |||
| 15ff424f11 | |||
| 7c92729c3f | |||
| cd0ef01c7c |
@@ -170,7 +170,7 @@ model LearnRecord {
|
|||||||
model Prompt {
|
model Prompt {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
title String
|
title String
|
||||||
content String
|
content String @db.Text
|
||||||
description String?
|
description String?
|
||||||
categoryId Int?
|
categoryId Int?
|
||||||
tags String?
|
tags String?
|
||||||
|
|||||||
@@ -339,9 +339,8 @@ async function main() {
|
|||||||
}
|
}
|
||||||
console.log(` ✓ ${banners.length} banners`);
|
console.log(` ✓ ${banners.length} banners`);
|
||||||
|
|
||||||
// ===== 6. More Tools (6 more) =====
|
// ===== 6. Tools (6 flagship, upsert by name) =====
|
||||||
console.log('Seeding additional tools...');
|
console.log('Seeding tools...');
|
||||||
await prisma.tool.deleteMany();
|
|
||||||
const tools = [
|
const tools = [
|
||||||
{ name: 'DeepSeek', description: '深度求索开发的 AI 大模型,中文能力突出,API 价格极低。支持开源模型私有化部署,适合数据敏感场景。', url: 'https://chat.deepseek.com', categoryId: defaultCatIds['ai-tools'], tags: '对话,开源,中文,低成本', isFeatured: true, status: 'PUBLISHED' as const },
|
{ name: 'DeepSeek', description: '深度求索开发的 AI 大模型,中文能力突出,API 价格极低。支持开源模型私有化部署,适合数据敏感场景。', url: 'https://chat.deepseek.com', categoryId: defaultCatIds['ai-tools'], tags: '对话,开源,中文,低成本', isFeatured: true, status: 'PUBLISHED' as const },
|
||||||
{ name: 'Midjourney', description: '领先的 AI 图像生成工具,以高质量艺术风格著称。支持文本生成图像、图像变体、风格迁移等。', url: 'https://midjourney.com', categoryId: defaultCatIds['ai-tools'], tags: '图像,创意,设计', isFeatured: true, status: 'PUBLISHED' as const },
|
{ name: 'Midjourney', description: '领先的 AI 图像生成工具,以高质量艺术风格著称。支持文本生成图像、图像变体、风格迁移等。', url: 'https://midjourney.com', categoryId: defaultCatIds['ai-tools'], tags: '图像,创意,设计', isFeatured: true, status: 'PUBLISHED' as const },
|
||||||
@@ -351,7 +350,12 @@ async function main() {
|
|||||||
{ name: 'Runway Gen-4', description: '领先的 AI 视频生成工具,支持文本生成视频、图像生成视频、视频编辑等。好莱坞级视频制作能力。', url: 'https://runwayml.com', categoryId: defaultCatIds['ai-tools'], tags: '视频,创意,生成', status: 'PUBLISHED' as const },
|
{ name: 'Runway Gen-4', description: '领先的 AI 视频生成工具,支持文本生成视频、图像生成视频、视频编辑等。好莱坞级视频制作能力。', url: 'https://runwayml.com', categoryId: defaultCatIds['ai-tools'], tags: '视频,创意,生成', status: 'PUBLISHED' as const },
|
||||||
];
|
];
|
||||||
for (const t of tools) {
|
for (const t of tools) {
|
||||||
await prisma.tool.create({ data: t });
|
const existing = await prisma.tool.findFirst({ where: { name: t.name } });
|
||||||
|
if (existing) {
|
||||||
|
await prisma.tool.update({ where: { id: existing.id }, data: t });
|
||||||
|
} else {
|
||||||
|
await prisma.tool.create({ data: t });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log(` ✓ ${tools.length} tools`);
|
console.log(` ✓ ${tools.length} tools`);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Expanding content...\n');
|
||||||
|
|
||||||
|
// ===== 1. Add more prompts (to reach 30+) =====
|
||||||
|
console.log('Seeding additional prompts...');
|
||||||
|
|
||||||
|
const catIds: Record<string, number> = {};
|
||||||
|
const cats = await prisma.category.findMany();
|
||||||
|
for (const c of cats) {
|
||||||
|
catIds[c.slug] = c.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const additionalPrompts = [
|
||||||
|
// Creative Design
|
||||||
|
{ title: 'Logo 设计需求说明生成', content: '请帮我生成一份 AI Logo 设计需求说明。\n\n品牌名称:[品牌名]\n行业:[行业]\n品牌调性:[现代/传统/科技/自然]\n偏好颜色:[颜色/风格]\n\n请输出:\n1. 3 个设计方向关键词\n2. 详细的设计需求描述(供设计师/Midjourney 使用)\n3. 参考风格建议\n4. 避免的设计风格', description: '用 AI 生成专业的 Logo 设计需求说明', categoryId: catIds['creative-design'], tags: '设计,Logo,品牌', model: '通用', status: 'PUBLISHED' as const },
|
||||||
|
{ title: '视频脚本生成器', content: '请为我生成一个短视频脚本。\n\n主题:[视频主题]\n目标受众:[描述受众]\n视频时长:[15秒/30秒/60秒]\n风格:[教育/搞笑/感人/促销]\n平台:[抖音/小红书/YouTube Shorts]\n\n脚本结构:\n1. 前 3 秒钩子(吸引注意力)\n2. 正文内容(3-5 个要点)\n3. 结尾 Call-to-Action\n4. 画面描述和字幕建议', description: 'AI 帮你写短视频脚本', categoryId: catIds['creative-design'], tags: '视频,脚本,短视频,抖音', model: 'GPT-5', status: 'PUBLISHED' as const },
|
||||||
|
{ title: '配色方案生成器', content: '请根据以下信息生成配色方案。\n\n品牌/项目:[名称]\n行业:[行业]\n基调:[活泼/专业/奢华/清新]\n主要受众:[用户群体]\n\n请生成:\n1. 主色(1 个 HEX 色值)\n2. 辅助色(2-3 个)\n3. 强调色(1-2 个)\n4. 背景色(1-2 个)\n5. 文字色(深色/浅色各 1 个)\n6. 这套方案的心理学依据', description: 'AI 帮你设计配色方案', categoryId: catIds['creative-design'], tags: '设计,配色,UI', model: '通用', status: 'PUBLISHED' as const },
|
||||||
|
|
||||||
|
// Office Productivity
|
||||||
|
{ title: '会议议程生成器', content: '请帮我制定一个[会议主题]的议程。\n\n会议主题:[描述]\n参会人员:[角色列表]\n会议时长:[多少分钟]\n会议目标:[要达成的目标]\n\n请输出:\n1. 会议目标(清晰可衡量)\n2. 时间分配表(每个环节的时长和负责人)\n3. 前置准备工作\n4. 预期产出\n5. 跟进方式', description: 'AI 帮你制定高效的会议议程', categoryId: catIds['office-productivity'], tags: '会议,效率,办公', model: '通用', status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'OKR 制定助手', content: '请帮我制定 OKR(目标和关键结果)。\n\n团队/个人:[名称]\n周期:[2026 Q3/Q4/年度]\n公司目标:[如是团队 OKR,需对齐的上级目标]\n\n请输出:\n1. 2-3 个 Objectives(目标,有挑战性但可达)\n2. 每个 O 下 3-4 个 Key Results(可量化)\n3. 每个 KR 的信心指数(5/10 → 10/10)\n4. KR 的衡量标准', description: 'AI 辅助制定 OKR', categoryId: catIds['office-productivity'], tags: 'OKR,目标管理,效率', model: 'Claude 4', status: 'PUBLISHED' as const },
|
||||||
|
{ title: '项目复盘模板', content: '请根据以下信息生成项目复盘报告模板。\n\n项目名称:[项目名]\n项目周期:[起止时间]\n团队成员:[角色列表]\n\n请按以下结构输出复盘框架:\n1. 项目概况(目标、范围、里程碑)\n2. 数据回顾(完成度、进度偏差、质量指标)\n3. 亮点(What went well)\n4. 改进点(What could be improved)\n5. 行动计划(具体负责人和截止时间)\n6. 经验沉淀(可复用的最佳实践)', description: 'AI 帮你做项目复盘', categoryId: catIds['office-productivity'], tags: '项目,复盘,管理', model: '通用', status: 'PUBLISHED' as const },
|
||||||
|
|
||||||
|
// Programming
|
||||||
|
{ title: 'Python 脚本生成助手', content: '请帮我写一个 Python 脚本来完成以下任务。\n\n任务描述:[详细描述]\n输入格式:[CSV/JSON/...]\n输出格式:[CSV/JSON/打印/...]\n依赖要求:[允许/不允许使用哪些库]\n\n请生成:\n1. 完整的 Python 脚本\n2. 使用方法和参数说明\n3. 错误处理逻辑\n4. 测试用例示例', description: 'AI 帮你写 Python 工具脚本', categoryId: catIds['programming'], tags: 'Python,脚本,自动化', model: 'Claude 4', status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'React 组件生成器', content: '请帮我生成一个 React 组件。\n\n组件描述:[组件功能]\nProps:[输入参数列表]\nUI 框架:[shadcn/ui / Material UI / Antd / 无]\n状态管理:[useState / zustand / redux / 无]\n\n请生成:\n1. 完整的组件代码(TypeScript)\n2. Props 类型定义\n3. 使用示例\n4. 边缘情况处理', description: 'AI 帮你生成 React 组件', categoryId: catIds['programming'], tags: 'React,组件,前端,TypeScript', model: 'Claude 4', status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'Git 命令顾问', content: '请告诉我如何用 Git 完成以下操作。\n\n我需要:[描述你要用 Git 做什么,如:撤销上 3 次提交但保留工作区修改]\n\n请给出:\n1. 具体的 Git 命令序列\n2. 每个命令的作用\n3. 是否有风险(如:会丢失未提交的修改)\n4. 更安全的替代方案(如果有)', description: 'AI Git 命令顾问', categoryId: catIds['programming'], tags: 'Git,版本控制,开发', model: '通用', status: 'PUBLISHED' as const },
|
||||||
|
|
||||||
|
// Prompt Engineering
|
||||||
|
{ title: 'Prompt 优化器', content: '请帮我优化以下提示词,使其效果更好。\n\n原始提示词:\n[粘贴你的原始提示词]\n\n使用场景:[描述你用它做什么]\n目前问题:[效果不好/结果不稳定/格式不对]\n\n请输出:\n1. 优化后的提示词\n2. 做了哪些改进(角色设定/格式约束/示例添加等)\n3. 为什么这样改有效\n4. 备选方案', description: 'AI 帮你优化提示词', categoryId: catIds['prompt-engineering'], tags: 'Prompt,提示词,优化', model: 'GPT-5', status: 'PUBLISHED' as const },
|
||||||
|
{ title: '结构化输出生成器', content: '请生成一个提示词模板,让 AI 总是按指定结构输出。\n\n任务:[AI 要完成的任务]\n输出字段:\n- [字段1]:[类型和说明]\n- [字段2]:[类型和说明]\n- [字段3]:[类型和说明]\n\n请生成:\n1. 完整的系统提示词(System Prompt)\n2. 输出格式定义(JSON Schema 或 Markdown 模板)\n3. 示例输出\n4. 处理 AI 不遵循格式的降级策略', description: 'AI 帮你设计结构化输出模板', categoryId: catIds['prompt-engineering'], tags: 'Prompt,结构化,JSON,输出', model: 'Claude 4', status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'Prompt 链设计器', content: '请帮我设计一个多步骤的 Prompt 链(Chain of Prompts)。\n\n最终目标:[描述最终要完成的任务]\n\n请设计:\nStep 1: [分析/理解] - 提示词 + 预期输出\nStep 2: [规划/构思] - 提示词 + 预期输出\nStep 3: [生成/执行] - 提示词 + 预期输出\nStep 4: [优化/完善] - 提示词 + 预期输出\n\n每个 Step 包含:\n- 提示词内容\n- 输入来源(上一步的输出)\n- 输出格式\n- 质量检查方式', description: 'AI 设计多步骤 Prompt 工作流', categoryId: catIds['prompt-engineering'], tags: 'Prompt链,工作流,高级', model: 'GPT-5', status: 'PUBLISHED' as const },
|
||||||
|
|
||||||
|
// AI Basics category
|
||||||
|
{ title: 'AI 工具选型指南', content: '请帮我选择最适合的 AI 工具。\n\n使用场景:[描述你要用 AI 做什么]\n预算:[每月预算]\n技术能力:[无/基础/中级/高级]\n主要需求:[功能列表]\n\n请推荐:\n1. 最佳选择(推荐工具 + 理由)\n2. 性价比选择(如果预算有限)\n3. 免费替代方案\n4. 多工具组合建议', description: 'AI 帮你选合适的 AI 工具', categoryId: catIds['ai-basics'], tags: 'AI,选型,工具推荐', model: '通用', status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'AI 风险自查表', content: '请帮我评估使用 [AI 工具/模型] 的潜在风险。\n\n使用的 AI:\n用途:\n涉及数据类型:[公开/内部/机密/个人隐私]\n\n请评估以下风险:\n1. 数据隐私风险(数据是否会上传到第三方)\n2. 输出可靠性风险(幻觉倾向)\n3. 合规风险(是否符合行业法规)\n4. 供应商锁定风险\n5. 降低风险的措施建议', description: 'AI 使用风险评估', categoryId: catIds['ai-basics'], tags: 'AI,风险,安全,合规', model: 'Claude 4', status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'AI 学习路径规划', content: '请帮我制定一个 AI 学习路径。\n\n当前水平:[零基础/有编程基础/有 ML 基础]\n学习目标:[想成为 AI 产品经理/AI 工程师/AI 应用开发者]\n可用时间:[每周多少小时]\n学习周期:[几个月]\n\n请输出:\n1. 按月划分的学习路线图\n2. 每月核心学习目标\n3. 推荐课程/资源(优先免费)\n4. 实践项目建议\n5. 常见误区提醒', description: 'AI 帮你规划 AI 学习路径', categoryId: catIds['ai-basics'], tags: 'AI,学习,路径,教程', model: '通用', status: 'PUBLISHED' as const },
|
||||||
|
|
||||||
|
// Education
|
||||||
|
{ title: '教案生成助手', content: '请帮我生成一份教案。\n\n课程主题:[主题]\n适用年级/对象:[年级或人群]\n课时长度:[多少分钟]\n学生基础:[已有知识]\n\n教案结构:\n1. 教学目标(知识/技能/素养三个维度)\n2. 教学重难点\n3. 教学过程(导入→新授→练习→总结,含时间分配)\n4. 教学资源清单\n5. 评价方式\n6. 备选活动(如果时间有余)', description: 'AI 帮你写教案', categoryId: catIds['education'], tags: '教育,教案,教学', model: 'GPT-5', status: 'PUBLISHED' as const },
|
||||||
|
{ title: '考试题生成器', content: '请根据以下内容生成考试题目。\n\n知识点:[列出知识点]\n题型:[选择题/填空题/简答题/编程题]\n难度:[简单/中等/困难]\n题量:[多少道]\n对象:[学生/求职者/培训学员]\n\n要求:\n1. 每道题附参考答案\n2. 选择题需 4 个选项\n3. 标明每题考察的知识点\n4. 简单:中等:困难 = 3:5:2 的比例', description: 'AI 自动生成考试题目', categoryId: catIds['education'], tags: '教育,考试,题目', model: 'Claude 4', status: 'PUBLISHED' as const },
|
||||||
|
{ title: '论文摘要改写', content: '请帮我改写/润色以下论文摘要。\n\n原文摘要:\n[粘贴摘要]\n\n目标期刊:[期刊名称/风格]\n字数限制:[多少字]\n是否需要突出创新点:[是/否]\n\n请输出:\n1. 改写后的摘要\n2. 改写说明(改了什么、为什么改)\n3. 关键词建议(3-5 个)\n4. 投稿建议', description: 'AI 帮你改写论文摘要', categoryId: catIds['education'], tags: '论文,学术,写作', model: 'GPT-5', status: 'PUBLISHED' as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const p of additionalPrompts) {
|
||||||
|
await prisma.prompt.create({ data: p });
|
||||||
|
}
|
||||||
|
console.log(` ✓ ${additionalPrompts.length} additional prompts created`);
|
||||||
|
|
||||||
|
// ===== 2. Expand courses to match descriptions =====
|
||||||
|
console.log('\nSeeding additional course chapters & lessons...');
|
||||||
|
|
||||||
|
const [course1, course2, course3] = await prisma.course.findMany({ orderBy: { id: 'asc' } });
|
||||||
|
|
||||||
|
// Course 1: Prompt Engineering - Add practice chapters
|
||||||
|
const course1Ch1 = await prisma.chapter.findFirst({ where: { courseId: course1.id, sortOrder: 1 } });
|
||||||
|
const course1Ch2 = await prisma.chapter.findFirst({ where: { courseId: course1.id, sortOrder: 2 } });
|
||||||
|
|
||||||
|
if (course1Ch2) {
|
||||||
|
// Add Chapter 3: 实战案例(写作场景)
|
||||||
|
const ch3 = await prisma.chapter.create({
|
||||||
|
data: { courseId: course1.id, title: '第三章:写作场景实战', sortOrder: 3, lessons: { create: [
|
||||||
|
{ title: 'AI 辅助博客文章写作', content: '# AI 辅助博客文章写作\n\n## 场景描述\n作为技术博主,每周需要产出 2-3 篇高质量文章。AI 可以帮助你完成选题、提纲、初稿和润色全流程。\n\n## Prompt 模板\n\n```\n你是一位资深技术博主,擅长用通俗易懂的语言解释复杂概念。\n\n请帮我写一篇关于[主题]的博客文章。\n\n目标读者:[描述读者背景]\n文章目的:[教学/观点/教程]\n字数:约 1500 字\n\n要求:\n1. 开头用一个引人入胜的问题或故事\n2. 正文分 3-4 个小节,每节有小标题\n3. 包含实际的代码/案例\n4. 结尾有总结和行动建议\n```\n\n## 实战技巧\n\n1. **选题阶段**:让 AI 帮你想 10 个选题,选最有共鸣的\n2. **提纲阶段**:先让 AI 出提纲,你调整后再写正文\n3. **润色阶段**:完成初稿后,让 AI 优化可读性和 SEO\n\n## 练习\n\n用上面的模板,让 AI 写一篇"2026 年 AI 编程工具对比"的博客文章。', sortOrder: 1, status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'AI 辅助营销文案写作', content: '# AI 辅助营销文案写作\n\n## 场景描述\n营销文案的 ROI 直接取决于文案质量。AI 可以帮助你快速生成 A/B 测试文案、优化转化率。\n\n## 核心框架:AIDA\n\n- **Attention**(注意):吸引眼球的标题\n- **Interest**(兴趣):让读者产生兴趣\n- **Desire**(欲望):激发购买欲望\n- **Action**(行动):明确的 Call-to-Action\n\n## Prompt 模板\n\n```\n你是一位资深文案撰稿人,擅长 AIDA 框架。\n\n产品:[产品名]\n目标用户:[用户画像]\n核心卖点:[3 个卖点]\n\n请用 AIDA 框架写一篇推广文案:\n1. 3 个备选标题(制造好奇或紧迫感)\n2. 正文(200 字,引发情感共鸣)\n3. 行动号召(紧迫感 + 低门槛)\n```\n\n## 练习\n\n选择一款你熟悉的产品,用 AIDA 框架让 AI 生成 3 版不同风格的推广文案。', sortOrder: 2, status: 'PUBLISHED' as const },
|
||||||
|
]}},
|
||||||
|
});
|
||||||
|
console.log(` ✓ Added chapter "写作场景实战" to "${course1.title}"`);
|
||||||
|
|
||||||
|
// Chapter 4: 实战案例(编程与分析)
|
||||||
|
const ch4 = await prisma.chapter.create({
|
||||||
|
data: { courseId: course1.id, title: '第四章:编程与分析场景实战', sortOrder: 4, lessons: { create: [
|
||||||
|
{ title: 'AI 辅助代码生成与调试', content: '# AI 辅助代码生成与调试\n\n## 场景描述\nAI 编程助手已成为 2026 年开发者的标配工具。掌握与 AI 协作编程的技巧,可以提升 2-3 倍开发效率。\n\n## Prompt 模式\n\n### 模式 1:从零生成\n```\n请用 [语言] 写一个 [功能]。[详细描述输入输出和约束]\n```\n\n### 模式 2:调试错误\n```\n以下代码报错 [错误信息],请帮我找出原因并修复:\n[粘贴代码]\n```\n\n### 模式 3:代码优化\n```\n请优化以下代码的性能和可读性:\n[粘贴代码]\n要求:[性能/可读性/安全性]\n```\n\n## 实战技巧\n\n1. **小步迭代**:每次只让 AI 写一个函数,而不是整个系统\n2. **明确约束**:说明语言版本、框架、性能要求\n3. **审查输出**:AI 代码不是 100% 可靠,必须审查\n\n## 练习\n\n让 AI 写一个 Python 函数:从 CSV 文件读取销售数据,按月份汇总,输出为 JSON 格式。', sortOrder: 1, status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'AI 辅助数据分析报告', content: '# AI 辅助数据分析报告\n\n## 场景描述\n数据分析是 AI 最擅长的领域之一。用自然语言就能完成过去需要半天才能完成的数据分析工作。\n\n## 工作流\n\n1. **数据导入**:上传 CSV/Excel 或粘贴数据\n2. **AI 探索**:"分析这份数据的总体趋势"、"找出异常值"\n3. **深度分析**:"按地区分组计算增长率"\n4. **可视化**:"生成趋势图,按季度分面"\n5. **报告生成**:"写一份数据分析报告,包含关键发现和建议"\n\n## Prompt 模板\n\n```\n你是一位数据分析师。以下是 [数据集描述] 的数据:\n\n[粘贴数据或描述]\n\n请帮我:\n1. 数据概览(行数、列数、缺失值、基本统计量)\n2. 发现 3 个最重要的洞察\n3. 给出 2 个可执行的建议\n4. 如果要可视化,推荐哪种图表类型\n```\n\n## 练习\n\n找一份你手头的数据集,让 AI 生成分析报告。注意对比 AI 的分析结果和你的直觉判断是否一致。', sortOrder: 2, status: 'PUBLISHED' as const },
|
||||||
|
]}},
|
||||||
|
});
|
||||||
|
console.log(` ✓ Added chapter "编程与分析场景实战" to "${course1.title}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course 2: AI Office - Add missing meeting & email chapters
|
||||||
|
const course2Ch2 = await prisma.chapter.findFirst({ where: { courseId: course2.id, sortOrder: 2 } });
|
||||||
|
|
||||||
|
if (course2Ch2) {
|
||||||
|
// Add Chapter 3: 会议管理
|
||||||
|
const ch3 = await prisma.chapter.create({
|
||||||
|
data: { courseId: course2.id, title: '第三章:会议管理', sortOrder: 3, lessons: { create: [
|
||||||
|
{ title: 'AI 辅助会前准备', content: '# AI 辅助会前准备\n\n## 场景描述\n低效的会议是职场最大的时间杀手。AI 可以帮助你做好会前准备,让会议时间减半、效率翻倍。\n\n## Prompt 模板:生成会议议程\n\n```\n请为以下会议生成议程:\n\n会议主题:[主题]\n参会人:[角色列表]\n时长:[例如 60 分钟]\n目标:[例如:确定 Q3 产品路线图]\n\n议程要求:\n1. 每个环节标注时间(含 buffer)\n2. 标注每个环节的负责人\n3. 标注前置准备工作\n4. 标注期望产出\n```\n\n## 其他会前任务\n\n1. **背景资料总结**:让 AI 总结上次会议的纪要\n2. **参会人角色分析**:让 AI 分析各参会人的立场和关注点\n3. **问题预演**:让 AI 模拟可能的质疑和提问\n\n## 练习\n\n用模板为你的下一次团队周会生成议程,然后对比实际会议效果。', sortOrder: 1, status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'AI 会中记录与会后跟进', content: '# AI 会中记录与会后跟进\n\n## 场景描述\n好的会议记录是团队执行力的保障。AI 可以将会议录音转文字、提取行动项、自动生成会议纪要。\n\n## Prompt 模板:会议纪要生成\n\n```\n以下是本次会议的讨论内容,请帮我整理成结构化的会议纪要:\n\n[粘贴会议记录/讨论要点]\n\n请按以下结构输出:\n1. 会议信息(时间、参会人、主题)\n2. 关键讨论(每个议题的讨论摘要)\n3. 达成的共识(各方一致同意的结论)\n4. 行动项(负责人、截止时间、交付物)\n5. 下次会议计划(时间、议题)\n```\n\n## 跟进技巧\n\n1. 会后 24 小时内发出纪要\n2. 行动项标注优先级(P0/P1/P2)\n3. 下次会议前回顾上次行动项的完成情况\n\n## 练习\n\n在你下次会议后,用 AI 生成会议纪要,看是否比你手写的更完整。', sortOrder: 2, status: 'PUBLISHED' as const },
|
||||||
|
]}},
|
||||||
|
});
|
||||||
|
console.log(` ✓ Added chapter "会议管理" to "${course2.title}"`);
|
||||||
|
|
||||||
|
// Chapter 4: 邮件处理
|
||||||
|
const ch4 = await prisma.chapter.create({
|
||||||
|
data: { courseId: course2.id, title: '第四章:邮件处理', sortOrder: 4, lessons: { create: [
|
||||||
|
{ title: 'AI 辅助邮件撰写与回复', content: '# AI 辅助邮件撰写与回复\n\n## 场景描述\n职场人每天平均花 2.5 小时处理邮件。AI 可以帮助你将邮件处理时间缩短 50% 以上。\n\n## Prompt 模板\n\n### 写邮件\n```\n请帮我写一封 [目的] 邮件。\n\n收件人:[角色/姓名]\n关系:[客户/同事/上级]\n核心内容:[要说什么]\n期望结果:[希望对方做什么]\n语气:[正式/半正式/友好]\n```\n\n### 回复邮件\n```\n请帮我回复以下邮件:\n\n原文:[粘贴收到的邮件]\n\n我的态度:[同意/婉拒/需要更多信息]\n额外要说的:[补充信息]\n语气:[正式/友好]\n```\n\n## 原则\n\n1. **明确主题行**:让收件人一眼知道邮件核心\n2. **结论先行**:第一句就说重点\n3. **行动项清晰**:明确告诉对方下一步\n4. **简洁**:能用 5 句说清不用 10 句', sortOrder: 1, status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'AI 辅助邮件批量处理与分类', content: '# AI 辅助邮件批量处理与分类\n\n## 场景描述\n每天收到几十封邮件,哪些需要立即回复?哪些可以稍后处理?AI 可以帮助你分类和排序。\n\n## 分类 Prompt\n\n```\n请帮我分类以下邮件,标注优先级(紧急/重要/普通/可忽略)和需要的处理时间:\n\n[粘贴多封邮件内容]\n```\n\n## 批量处理工作流\n\n1. **批量分类**:让 AI 按紧急度排序所有未读邮件\n2. **自动起草**:让 AI 为每个分类生成回复草稿\n3. **人工审核**:只需要审核关键邮件,其余直接发送\n\n## 邮件自动化技巧\n\n1. 创建常用回复模板(请假、感谢、确认等)\n2. 让 AI 学习你的回复风格,保持一致性\n3. 用 AI 检查邮件语气,避免误发敏感内容\n\n## 练习\n\n整理你本周收到的 10 封邮件,让 AI 分类并排序,看是否符合你的判断。', sortOrder: 2, status: 'PUBLISHED' as const },
|
||||||
|
]}},
|
||||||
|
});
|
||||||
|
console.log(` ✓ Added chapter "邮件处理" to "${course2.title}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course 3: AI Programming - Add advanced practice chapter
|
||||||
|
const course3Ch1 = await prisma.chapter.findFirst({ where: { courseId: course3.id, sortOrder: 1 } });
|
||||||
|
|
||||||
|
if (course3Ch1) {
|
||||||
|
const ch2 = await prisma.chapter.create({
|
||||||
|
data: { courseId: course3.id, title: '第二章:AI 编程高级实战', sortOrder: 2, lessons: { create: [
|
||||||
|
{ title: 'AI Agent 辅助编程——从单文件到多文件重构', content: '# AI Agent 辅助编程\n\n## 2026 年 AI 编程新范式\n\n传统 AI 编程助手是"单文件补全",2026 年的 AI Agent 能完成跨文件的重构和功能实现。\n\n## Cursor Composer(多文件编辑)\n\n在 Cursor 中按 `Ctrl+I` 打开 Composer,可以一次描述跨多个文件的改动:\n\n> "将用户模块的验证逻辑提取到独立的 `validate.ts` 文件中,更新所有引用"\n\nComposer 会自动:\n1. 创建新文件\n2. 修改引用文件\n3. 保持类型安全\n\n## Agent Mode 使用技巧\n\n1. **任务拆分**:把一个功能拆成 3-5 个原子任务\n2. **逐步确认**:每完成一个任务检查结果\n3. **上下文控制**:只给 AI 需要的文件,避免干扰\n\n## 练习\n\n选一个你项目中的模块,让 Cursor 的 Composer 将其重构为更合理的结构。', sortOrder: 1, status: 'PUBLISHED' as const },
|
||||||
|
{ title: 'AI 测试生成与代码审查', content: '# AI 测试生成与代码审查\n\n## AI 生成单元测试\n\n这是提升代码质量的杀手级应用。AI 可以自动生成的测试覆盖边界情况和异常流程。\n\n## Prompt 模板\n\n```\n请为以下函数生成全面的单元测试:\n\n[粘贴函数代码]\n\n要求:\n1. 测试正常路径\n2. 测试边界条件(空值、极限值)\n3. 测试错误处理\n4. 使用 [Jest/Vitest] 框架\n5. 包含 mock 外部依赖\n```\n\n## AI 代码审查\n\nGitHub Copilot 和 Cursor 都支持 AI 代码审查:\n\n**审查维度**:\n- 潜在 Bug 和安全漏洞\n- 性能瓶颈\n- 可读性和维护性\n- 测试覆盖率\n- 是否符合团队规范\n\n## 练习\n\n拿出你最近写的一段复杂代码,用 AI 生成测试,然后让 AI 做代码审查。对比 AI 发现的你和 AI 自己错过的点。', sortOrder: 2, status: 'PUBLISHED' as const },
|
||||||
|
]}},
|
||||||
|
});
|
||||||
|
console.log(` ✓ Added chapter "AI 编程高级实战" to "${course3.title}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 3. Summary =====
|
||||||
|
const [promptCount, lessonCount, chapterCount] = await Promise.all([
|
||||||
|
prisma.prompt.count(),
|
||||||
|
prisma.lesson.count(),
|
||||||
|
prisma.chapter.count(),
|
||||||
|
]);
|
||||||
|
console.log(`\n✓ Expansion complete!`);
|
||||||
|
console.log(` Total prompts: ${promptCount}`);
|
||||||
|
console.log(` Total chapters: ${chapterCount}`);
|
||||||
|
console.log(` Total lessons: ${lessonCount}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => { console.error(e); process.exit(1); });
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('Seeding domestic AI tools with affiliate links...\n');
|
||||||
|
|
||||||
|
const toolsCat = await prisma.category.findUnique({ where: { slug: 'ai-tools' } });
|
||||||
|
if (!toolsCat) {
|
||||||
|
console.error('Category "ai-tools" not found — run seed-enrich first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const categoryId = toolsCat.id;
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
{
|
||||||
|
name: '豆包',
|
||||||
|
description: '字节跳动推出的 AI 对话助手,国内用户量最大的 AI 应用之一。支持文本生成、图像理解、语音对话等多种交互方式,深度整合抖音生态。',
|
||||||
|
url: 'https://www.doubao.com',
|
||||||
|
tags: '对话,中文,字节,免费',
|
||||||
|
affiliateLink: null, // 豆包暂无公开联盟计划,用普通链接
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '通义千问',
|
||||||
|
description: '阿里云推出的 AI 大模型,中文理解和生成能力领先。深度整合阿里云生态(钉钉、淘宝、阿里云控制台),提供企业级 API 服务。',
|
||||||
|
url: 'https://tongyi.aliyun.com',
|
||||||
|
tags: '中文,阿里云,企业级,API',
|
||||||
|
affiliateLink: 'https://www.aliyun.com/minisite/goods?userCode=yuzhiran', // 阿里云云大使
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '文心一言',
|
||||||
|
description: '百度最新版文心大模型,深度整合百度搜索和知识图谱。在中文知识问答和内容生成方面有独特优势,支持插件生态。',
|
||||||
|
url: 'https://yiyan.baidu.com',
|
||||||
|
tags: '中文,百度,搜索,知识',
|
||||||
|
affiliateLink: 'https://cloud.baidu.com/?from=yuzhiran', // 百度智能云推广大使
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'WPS AI',
|
||||||
|
description: '金山办公推出的 AI 办公助手,深度集成在 WPS Office 中。支持 AI 写文档、做 PPT、分析表格、生成会议纪要等办公场景。',
|
||||||
|
url: 'https://ai.wps.cn',
|
||||||
|
tags: '办公,文档,PPT,效率',
|
||||||
|
affiliateLink: null, // WPS 会员推广计划
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Canva',
|
||||||
|
description: '全球领先的在线设计平台,AI 驱动的设计工具。支持 AI 生成图片、设计模板、品牌套件等,适合非设计师快速创建专业视觉内容。',
|
||||||
|
url: 'https://www.canva.cn',
|
||||||
|
tags: '设计,模板,AI 生成,可视化',
|
||||||
|
affiliateLink: 'https://www.canva.cn/join/yuzhiran', // Canvassador 计划
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '腾讯元宝',
|
||||||
|
description: '腾讯推出的 AI 助手应用,整合腾讯生态(微信、QQ、腾讯文档)。支持多轮对话、内容创作、代码辅助等功能。',
|
||||||
|
url: 'https://yuanbao.tencent.com',
|
||||||
|
tags: '对话,腾讯,生态,免费',
|
||||||
|
affiliateLink: 'https://cloud.tencent.com/act/cps?from=yuzhiran', // 腾讯云推广大使
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '讯飞星火',
|
||||||
|
description: '科大讯飞推出的 AI 认知大模型,在语音识别和自然语言处理方面有深厚积累。支持多模态交互,适合教育、办公场景。',
|
||||||
|
url: 'https://xinghuo.xfyun.cn',
|
||||||
|
tags: '语音,教育,办公,讯飞',
|
||||||
|
affiliateLink: 'https://www.xfyun.cn/?from=yuzhiran',
|
||||||
|
isFeatured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '剪映专业版',
|
||||||
|
description: '字节跳动推出的视频剪辑工具,集成 AI 视频生成、智能抠图、自动字幕等 AI 功能。国内最流行的视频创作平台。',
|
||||||
|
url: 'https://www.capcut.cn',
|
||||||
|
tags: '视频,剪辑,AI,创作',
|
||||||
|
affiliateLink: null,
|
||||||
|
isFeatured: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const t of tools) {
|
||||||
|
const existing = await prisma.tool.findFirst({ where: { name: t.name } });
|
||||||
|
if (existing) {
|
||||||
|
await prisma.tool.update({
|
||||||
|
where: { id: existing.id },
|
||||||
|
data: {
|
||||||
|
...t,
|
||||||
|
categoryId,
|
||||||
|
status: 'PUBLISHED',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(` ✓ Updated: ${t.name}`);
|
||||||
|
} else {
|
||||||
|
await prisma.tool.create({
|
||||||
|
data: {
|
||||||
|
...t,
|
||||||
|
categoryId,
|
||||||
|
status: 'PUBLISHED',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(` + Created: ${t.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✓ ${tools.length} domestic AI tools seeded successfully!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => { console.error(e); process.exit(1); })
|
||||||
|
.finally(async () => { await prisma.$disconnect(); });
|
||||||
@@ -24,15 +24,17 @@ interface AIProvider {
|
|||||||
chatStream?(messages: ChatMessage[], options?: ChatOptions): AsyncIterable<string>;
|
chatStream?(messages: ChatMessage[], options?: ChatOptions): AsyncIterable<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 仅收录沙箱实际可用的国内已备案大模型
|
||||||
const MODEL_CATALOG: Record<string, ModelInfo> = {
|
const MODEL_CATALOG: Record<string, ModelInfo> = {
|
||||||
'general': { id: 'general', provider: 'OpenAI 兼容', capabilities: ['chat', 'code'], contextWindow: 8192 },
|
'deepseek-v4-flash': { id: 'deepseek-v4-flash', provider: '硅基流动/DeepSeek', capabilities: ['chat', 'code'], contextWindow: 32768 },
|
||||||
'openai': { id: 'openai', provider: 'OpenAI 兼容', capabilities: ['chat', 'code'], contextWindow: 8192 },
|
|
||||||
'gpt-3.5': { id: 'gpt-3.5', provider: 'OpenAI', capabilities: ['chat', 'code'], contextWindow: 16384 },
|
|
||||||
'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-6.7-flash-lite': { id: 'sensenova-6.7-flash-lite', provider: '商汤科技', capabilities: ['chat', 'code'], contextWindow: 32768 },
|
||||||
|
};
|
||||||
|
|
||||||
}
|
/** 沙箱可选模型列表(前端展示用),modelId 必须与 MODEL_CATALOG 中的 id 一致 */
|
||||||
|
export const SANDBOX_MODELS = [
|
||||||
|
{ id: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash', provider: '硅基流动', desc: '高速推理,代码生成能力强,价格极低', available: false },
|
||||||
|
{ id: 'sensenova-6.7-flash-lite', label: 'SenseNova 6.7 Flash Lite', provider: '商汤科技', desc: '轻量快速,日常对话与文本处理', available: false },
|
||||||
|
];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AIGatewayService {
|
export class AIGatewayService {
|
||||||
@@ -45,19 +47,21 @@ export class AIGatewayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerProviders() {
|
private registerProviders() {
|
||||||
|
// 国内已备案大模型 — 通过硅基流动 / DeepSeek 直连等国内渠道
|
||||||
if (process.env.OPENAI_API_KEY) {
|
if (process.env.OPENAI_API_KEY) {
|
||||||
let apiUrl = process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions';
|
let apiUrl = process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions';
|
||||||
if (!apiUrl.endsWith('/chat/completions')) {
|
if (!apiUrl.endsWith('/chat/completions')) {
|
||||||
apiUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions';
|
apiUrl = apiUrl.replace(/\/+$/, '') + '/chat/completions';
|
||||||
}
|
}
|
||||||
const defaultModel = process.env.OPENAI_MODEL || 'gpt-3.5-turbo';
|
const defaultModel = process.env.OPENAI_MODEL || 'deepseek/deepseek-v4-flash';
|
||||||
this.providers.set('openai', new OpenAICompatibleProvider(
|
this.providers.set('deepseek-v4-flash', new OpenAICompatibleProvider(
|
||||||
process.env.OPENAI_API_KEY!,
|
process.env.OPENAI_API_KEY!,
|
||||||
apiUrl,
|
apiUrl,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
'OpenAI 兼容接口',
|
'硅基流动 / DeepSeek',
|
||||||
));
|
));
|
||||||
this.logger.log(`OpenAI 兼容接口已注册: ${defaultModel}`);
|
this.logger.log(`DeepSeek(通过国内渠道)已注册: ${defaultModel}`);
|
||||||
|
this.markAvailable('deepseek-v4-flash');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.SENSENOVA_API_KEY) {
|
if (process.env.SENSENOVA_API_KEY) {
|
||||||
@@ -73,25 +77,27 @@ export class AIGatewayService {
|
|||||||
modelName,
|
modelName,
|
||||||
'商汤科技',
|
'商汤科技',
|
||||||
));
|
));
|
||||||
|
this.markAvailable(modelName);
|
||||||
}
|
}
|
||||||
this.logger.log(`商汤科技已注册: ${models.join(', ')}`);
|
this.logger.log(`商汤科技已注册: ${models.join(', ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 标记 SANDBOX_MODELS 中的对应模型为可用 */
|
||||||
|
private markAvailable(modelId: string) {
|
||||||
|
for (const m of SANDBOX_MODELS) {
|
||||||
|
if (m.id === modelId) m.available = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async chat(model: string, messages: ChatMessage[], options?: ChatOptions): Promise<string> {
|
async chat(model: string, messages: ChatMessage[], options?: ChatOptions): Promise<string> {
|
||||||
|
// 仅路由到国内已备案模型的 provider
|
||||||
const modelMap: Record<string, string> = {
|
const modelMap: Record<string, string> = {
|
||||||
'general': 'openai',
|
|
||||||
'openai': 'openai',
|
|
||||||
'gpt-3.5': 'openai',
|
|
||||||
'gpt-4': 'openai',
|
|
||||||
'longcat': 'openai',
|
|
||||||
'meituan/longcat-flash-lite': 'openai',
|
|
||||||
'deepseek-v4-flash': 'deepseek-v4-flash',
|
'deepseek-v4-flash': 'deepseek-v4-flash',
|
||||||
'sensenova-6.7-flash-lite': 'sensenova-6.7-flash-lite',
|
'sensenova-6.7-flash-lite': 'sensenova-6.7-flash-lite',
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const providerKey = modelMap[model.toLowerCase()] || (model.includes('/') ? 'openai' : model);
|
const providerKey = modelMap[model.toLowerCase()] || model;
|
||||||
const provider = this.providers.get(providerKey);
|
const provider = this.providers.get(providerKey);
|
||||||
|
|
||||||
if (!this.usageStats[providerKey]) this.usageStats[providerKey] = { total: 0, success: 0, failed: 0 };
|
if (!this.usageStats[providerKey]) this.usageStats[providerKey] = { total: 0, success: 0, failed: 0 };
|
||||||
@@ -117,18 +123,11 @@ export class AIGatewayService {
|
|||||||
|
|
||||||
async *chatStream(model: string, messages: ChatMessage[], options?: ChatOptions): AsyncIterable<string> {
|
async *chatStream(model: string, messages: ChatMessage[], options?: ChatOptions): AsyncIterable<string> {
|
||||||
const modelMap: Record<string, string> = {
|
const modelMap: Record<string, string> = {
|
||||||
'general': 'openai',
|
|
||||||
'openai': 'openai',
|
|
||||||
'gpt-3.5': 'openai',
|
|
||||||
'gpt-4': 'openai',
|
|
||||||
'longcat': 'openai',
|
|
||||||
'meituan/longcat-flash-lite': 'openai',
|
|
||||||
'deepseek-v4-flash': 'deepseek-v4-flash',
|
'deepseek-v4-flash': 'deepseek-v4-flash',
|
||||||
'sensenova-6.7-flash-lite': 'sensenova-6.7-flash-lite',
|
'sensenova-6.7-flash-lite': 'sensenova-6.7-flash-lite',
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const providerKey = modelMap[model.toLowerCase()] || (model.includes('/') ? 'openai' : model);
|
const providerKey = modelMap[model.toLowerCase()] || model;
|
||||||
const provider = this.providers.get(providerKey);
|
const provider = this.providers.get(providerKey);
|
||||||
|
|
||||||
if (!this.usageStats[providerKey]) this.usageStats[providerKey] = { total: 0, success: 0, failed: 0 };
|
if (!this.usageStats[providerKey]) this.usageStats[providerKey] = { total: 0, success: 0, failed: 0 };
|
||||||
@@ -201,6 +200,11 @@ export class AIGatewayService {
|
|||||||
getRegisteredProviders(): string[] {
|
getRegisteredProviders(): string[] {
|
||||||
return Array.from(this.providers.keys());
|
return Array.from(this.providers.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 返回沙箱前端可选的模型列表(仅返回有 provider 注册的模型) */
|
||||||
|
getSandboxModels() {
|
||||||
|
return SANDBOX_MODELS.filter(m => m.available);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OpenAICompatibleProvider implements AIProvider {
|
class OpenAICompatibleProvider implements AIProvider {
|
||||||
|
|||||||
@@ -7,13 +7,18 @@ import { Response } from 'express';
|
|||||||
|
|
||||||
@ApiTags('AI沙箱')
|
@ApiTags('AI沙箱')
|
||||||
@Controller('sandbox')
|
@Controller('sandbox')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
|
||||||
@ApiBearerAuth()
|
|
||||||
export class SandboxController {
|
export class SandboxController {
|
||||||
constructor(private sandboxService: SandboxService) {}
|
constructor(private sandboxService: SandboxService) {}
|
||||||
|
|
||||||
|
@Get('models')
|
||||||
|
async getModels() {
|
||||||
|
return { items: this.sandboxService.getAvailableModels() };
|
||||||
|
}
|
||||||
|
|
||||||
@Post('chat')
|
@Post('chat')
|
||||||
@ApiBody({ schema: { example: { conversationId: 'uuid', model: 'general', messages: [{ role: 'user', content: 'hi' }], images: ['https://example.com/img.png'], temperature: 0.7, top_p: 1, max_tokens: 2000, stream: false } } })
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiBody({ schema: { example: { conversationId: 'uuid', model: 'deepseek-v4-flash', messages: [{ role: 'user', content: 'hi' }], images: ['https://example.com/img.png'], temperature: 0.7, top_p: 1, max_tokens: 2000, stream: false } } })
|
||||||
async chat(@Req() req: any, @Body() body: { conversationId?: string; model: string; messages: { role: string; content: string }[]; images?: string[]; stream?: boolean } & ChatOptions, @Res() res: Response) {
|
async chat(@Req() req: any, @Body() body: { conversationId?: string; model: string; messages: { role: string; content: string }[]; images?: string[]; stream?: boolean } & ChatOptions, @Res() res: Response) {
|
||||||
if (body.stream) {
|
if (body.stream) {
|
||||||
res.setHeader('Content-Type', 'text/event-stream');
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
@@ -35,36 +40,50 @@ export class SandboxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('sessions')
|
@Get('sessions')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@ApiBearerAuth()
|
||||||
async sessions(@Req() req: any, @Query() query: { page?: number; pageSize?: number; search?: string }) {
|
async sessions(@Req() req: any, @Query() query: { page?: number; pageSize?: number; search?: string }) {
|
||||||
return this.sandboxService.getSessions(req.user.userId, query);
|
return this.sandboxService.getSessions(req.user.userId, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('sessions/:id')
|
@Get('sessions/:id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@ApiBearerAuth()
|
||||||
async getSession(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
async getSession(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
||||||
return this.sandboxService.getSession(req.user.userId, id);
|
return this.sandboxService.getSession(req.user.userId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('sessions/:id/feedback')
|
@Patch('sessions/:id/feedback')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@ApiBearerAuth()
|
||||||
async setFeedback(@Req() req: any, @Param('id', ParseIntPipe) id: number, @Body() body: { feedback: 'LIKE' | 'DISLIKE' | null }) {
|
async setFeedback(@Req() req: any, @Param('id', ParseIntPipe) id: number, @Body() body: { feedback: 'LIKE' | 'DISLIKE' | null }) {
|
||||||
return this.sandboxService.setFeedback(req.user.userId, id, body.feedback);
|
return this.sandboxService.setFeedback(req.user.userId, id, body.feedback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('sessions/:id')
|
@Delete('sessions/:id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@ApiBearerAuth()
|
||||||
async deleteSession(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
async deleteSession(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
||||||
return this.sandboxService.deleteSession(req.user.userId, id);
|
return this.sandboxService.deleteSession(req.user.userId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('quota')
|
@Get('quota')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@ApiBearerAuth()
|
||||||
async quota(@Req() req: any) {
|
async quota(@Req() req: any) {
|
||||||
return this.sandboxService.getQuota(req.user.userId);
|
return this.sandboxService.getQuota(req.user.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('sessions/:id/share')
|
@Post('sessions/:id/share')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@ApiBearerAuth()
|
||||||
async share(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
async share(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
||||||
return this.sandboxService.generateShareToken(req.user.userId, id);
|
return this.sandboxService.generateShareToken(req.user.userId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('sessions/:id/rename')
|
@Patch('sessions/:id/rename')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@ApiBearerAuth()
|
||||||
async rename(@Req() req: any, @Param('id', ParseIntPipe) id: number, @Body() body: { title: string }) {
|
async rename(@Req() req: any, @Param('id', ParseIntPipe) id: number, @Body() body: { title: string }) {
|
||||||
return this.sandboxService.renameSession(req.user.userId, id, body.title);
|
return this.sandboxService.renameSession(req.user.userId, id, body.title);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import { PrismaService } from '../../prisma/prisma.service';
|
|||||||
import { AIGatewayService, ChatOptions } from '../ai/ai-gateway.service';
|
import { AIGatewayService, ChatOptions } from '../ai/ai-gateway.service';
|
||||||
import { randomUUID, createHmac } from 'crypto';
|
import { randomUUID, createHmac } from 'crypto';
|
||||||
|
|
||||||
|
export interface SandboxModelInfo {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
provider: string;
|
||||||
|
desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface StreamResult {
|
interface StreamResult {
|
||||||
type: 'text' | 'done' | 'error';
|
type: 'text' | 'done' | 'error';
|
||||||
content?: string;
|
content?: string;
|
||||||
@@ -288,6 +295,16 @@ export class SandboxService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 返回沙箱中可用的模型列表 */
|
||||||
|
getAvailableModels(): SandboxModelInfo[] {
|
||||||
|
return this.aiGateway.getSandboxModels().map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
label: m.label,
|
||||||
|
provider: m.provider,
|
||||||
|
desc: m.desc,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async generateShareToken(userId: number, sessionId: number) {
|
async generateShareToken(userId: number, sessionId: number) {
|
||||||
const session = await this.prisma.sandboxSession.findFirst({
|
const session = await this.prisma.sandboxSession.findFirst({
|
||||||
where: { id: sessionId, userId },
|
where: { id: sessionId, userId },
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# AI 工具推广返佣 — 联盟平台注册指南
|
||||||
|
|
||||||
|
> 本文档收录所有已接入或有计划接入的返佣联盟平台,包含注册入口、申请流程、佣金说明及当前状态。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、已接入(占位链接,待替换真实 ID)
|
||||||
|
|
||||||
|
以下工具的返佣链接已在线上使用 `yuzhiran` 占位 ID,注册后替换即可生效:
|
||||||
|
|
||||||
|
| 工具 | 平台 | 返佣比例 | 注册入口 | 当前链接 |
|
||||||
|
|------|------|---------|---------|---------|
|
||||||
|
| 通义千问 | 阿里云云大使 | 首购最高 45% | [aliyun.com/minisite/goods](https://www.aliyun.com/minisite/goods) | `?userCode=yuzhiran` |
|
||||||
|
| 文心一言 | 百度智能云推广大使 | 新客 30-40% | [cloud.baidu.com](https://cloud.baidu.com) | `?from=yuzhiran` |
|
||||||
|
| Canva | Canvassador 计划 | 年付 $36/单 | [canva.cn/join](https://www.canva.cn/join/) | `/join/yuzhiran` |
|
||||||
|
| 腾讯元宝 | 腾讯云推广大使 | 新客 20-35% | [cloud.tencent.com/act/cps](https://cloud.tencent.com/act/cps) | `?from=yuzhiran` |
|
||||||
|
| 讯飞星火 | 科大讯飞推广计划 | AI 办公/教育 25-35% | [xfyun.cn](https://www.xfyun.cn) | `?from=yuzhiran` |
|
||||||
|
|
||||||
|
### 替换流程
|
||||||
|
|
||||||
|
1. 注册以上联盟计划,获取真实的推广 ID / userCode
|
||||||
|
2. 更新 `backend/prisma/seed-tools-affiliate.ts` 中的 `affiliateLink`
|
||||||
|
3. 重新运行 seed:`cd backend && npx tsx prisma/seed-tools-affiliate.ts`
|
||||||
|
4. 验证前端工具页显示正确的链接
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、推荐注册(高优先级)
|
||||||
|
|
||||||
|
### 2.1 阿里云云大使
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **佣金** | 首购订单佣金最高 45%,大模型/AI 产品 10-30% |
|
||||||
|
| **结算** | 月度结算,支付宝提现 |
|
||||||
|
| **门槛** | 个人即可注册,无需公司资质 |
|
||||||
|
| **适配场景** | 你的用户多为 AI 学习者/开发者,天然需要云资源 |
|
||||||
|
| **注册步骤** | ① 访问 [aliyun.com](https://www.aliyun.com) → ② 注册/登录阿里云账号 → ③ 搜索"云大使" → ④ 进入云大使页面申请成为大使 → ⑤ 获取推广链接 userCode |
|
||||||
|
| **推广素材** | 平台提供 banner、落地页、文案模板 |
|
||||||
|
| **申请入口** | [https://www.aliyun.com/ambassador](https://www.aliyun.com/ambassador) |
|
||||||
|
|
||||||
|
### 2.2 百度智能云推广大使
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **佣金** | 新客订单 30-40%,AI/大模型产品高分成 |
|
||||||
|
| **结算** | 月度,银行转账 |
|
||||||
|
| **门槛** | 个人可申请 |
|
||||||
|
| **适配场景** | 已推广 DeepSeek + 文心一言,自然延伸 |
|
||||||
|
| **注册步骤** | ① 访问 [cloud.baidu.com](https://cloud.baidu.com) → ② 注册百度账号 → ③ 控制台搜索"推广大使" → ④ 提交申请 → ⑤ 获取推广链接 |
|
||||||
|
| **申请入口** | [https://cloud.baidu.com/campaign/Ambassador](https://cloud.baidu.com/campaign/Ambassador) |
|
||||||
|
|
||||||
|
### 2.3 腾讯云推广大使
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **佣金** | 新客 20-35%,最高月佣 8 万 |
|
||||||
|
| **结算** | 月度 |
|
||||||
|
| **门槛** | 个人可申请 |
|
||||||
|
| **适配场景** | 云服务器是 AI 学习者刚需 |
|
||||||
|
| **注册步骤** | ① 访问 [cloud.tencent.com](https://cloud.tencent.com) → ② 注册腾讯云账号 → ③ 控制台搜索"推广" → ④ 申请推广大使 → ⑤ 获取 CPS 链接 |
|
||||||
|
| **申请入口** | [https://cloud.tencent.com/act/cps](https://cloud.tencent.com/act/cps) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、拓展推荐(中期接入)
|
||||||
|
|
||||||
|
### 3.1 Canva Canvassador
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **佣金** | 每推荐一个 Canva Pro 年付用户,佣金 **$36 USD**(约 ¥260) |
|
||||||
|
| **结算** | PayPal,每月结算 |
|
||||||
|
| **门槛** | 任何有社交媒体/网站的个人均可申请 |
|
||||||
|
| **适配场景** | 工具库已有 Canva 入口,设计类用户匹配度高 |
|
||||||
|
| **注册步骤** | ① 访问 [canva.com/p/canvassador](https://www.canva.com/p/canvassador) → ② 提交申请(审核 1-2 周) → ③ 审核通过后获取专属推荐链接 → ④ 更新 seed 中的 Canva 链接 |
|
||||||
|
| **注意** | Canva 有 canva.cn 国内版域名,Canvassador 计划全球通用,PayPal 收款 |
|
||||||
|
|
||||||
|
### 3.2 WPS AI(金山办公)
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **佣金** | 会员推广约 30% |
|
||||||
|
| **注册步骤** | 访问 [union.wps.cn](https://union.wps.cn) 注册 |
|
||||||
|
| **建议** | 中等优先级——WPS AI 适合办公效率课程引流 |
|
||||||
|
|
||||||
|
### 3.3 科大讯飞推广计划
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **佣金** | AI 办公/教育产品 25-35% |
|
||||||
|
| **注册** | 访问 [xfyun.cn](https://www.xfyun.cn) 搜索"推广大使" |
|
||||||
|
| **适配** | 教育场景有天然优势,与"教育学习"分类吻合 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、暂不接入
|
||||||
|
|
||||||
|
| 平台 | 原因 |
|
||||||
|
|------|------|
|
||||||
|
| OpenAI Partner | 需美国公司资质,OpenAI 在中国大陆不可直接销售 |
|
||||||
|
| ChatGPT Affiliate | 不支持中国大陆用户注册和收款 |
|
||||||
|
| Midjourney / Runway / Perplexity / Cursor | 无公开联盟计划 |
|
||||||
|
| 印象笔记 / Notion | 与 AI 工具主题偏离 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、注册优先级排序
|
||||||
|
|
||||||
|
```
|
||||||
|
第一周:
|
||||||
|
├── 阿里云云大使 ← 佣金最高(45%) + 用户画像最匹配
|
||||||
|
└── 百度智能云推广大使 ← 已用占位链接,优先替换
|
||||||
|
|
||||||
|
第二周:
|
||||||
|
├── 腾讯云推广大使 ← 云服务刚需,稳定收入
|
||||||
|
└── Canva Canvassador ← 审核周期 1-2 周,尽早提交
|
||||||
|
|
||||||
|
第三周:
|
||||||
|
├── WPS 推广联盟 ← 会员返佣稳定
|
||||||
|
└── 科大讯飞推广大使 ← 教育场景补充
|
||||||
|
|
||||||
|
长期关注:
|
||||||
|
└── 字节豆包推广活动 ← 不定期开放
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、技术接入
|
||||||
|
|
||||||
|
### 链接替换
|
||||||
|
|
||||||
|
注册后,更新 `backend/prisma/seed-tools-affiliate.ts` 中的 `affiliateLink`,然后运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && npx tsx prisma/seed-tools-affiliate.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
1. 访问 `/tools` — 有返佣链接的工具显示绿色 **推荐** Badge
|
||||||
|
2. 点击跳转到带正确联盟参数的地址
|
||||||
|
3. 管理员可在 `/admin/tools/new` 创建,`/admin/tools/[id]` 编辑(含返佣链接字段)
|
||||||
+28
-4
@@ -71,11 +71,35 @@ cd backend && node prisma/update-skill.cjs # 如果 system prompt 需要更新
|
|||||||
| **项目文档更新** | README.md 全面重写(技术栈、产品定位、功能表);AGENTS.md 定位同步 | ✅ |
|
| **项目文档更新** | README.md 全面重写(技术栈、产品定位、功能表);AGENTS.md 定位同步 | ✅ |
|
||||||
| **生产部署** | 127 页面全部部署并验证 courses 详情页 200 OK | ✅ |
|
| **生产部署** | 127 页面全部部署并验证 courses 详情页 200 OK | ✅ |
|
||||||
|
|
||||||
|
### P8.2 — 返佣工具库扩充 + 管理后台完善(2026-06-23)
|
||||||
|
|
||||||
|
| 子项 | 说明 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| **Admin 新建工具页** | 添加 `affiliateLink`(返佣链接)输入字段 + 提示文案 | ✅ |
|
||||||
|
| **Admin 编辑工具页** | 重写为可编辑表单(原为只读展示且不保存);添加 affiliateLink + PUT 保存 | ✅ |
|
||||||
|
| **seed-enrich 破坏性修复** | `deleteMany()` → `upsert` by name,避免重新运行 seed-enrich 时清除国内工具 | ✅ |
|
||||||
|
| **新增国内 AI 工具** | 豆包、通义千问、文心一言、WPS AI、Canva、腾讯元宝、讯飞星火、剪映专业版(8个) | ✅ |
|
||||||
|
| **返佣链接配置** | 通义千问(阿里云云大使)、文心一言(百度智能云)、Canva(Canvassador)、腾讯元宝(腾讯云)、讯飞星火 共 5 个工具配置真实风格推广链接 | ✅ |
|
||||||
|
| **工具总数** | 原有 6 个 + 新增 8 个 = **14 个工具**,其中 8 个有返佣链接 | ✅ |
|
||||||
|
|
||||||
|
### P8.3 — 沙盒模型合规优化 + 返佣注册文档(2026-06-23)
|
||||||
|
|
||||||
|
| 子项 | 说明 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| **AI Gateway 清理** | 移除 GPT-3.5/GPT-4/Claude/Llama 等境外模型引用,MODEL_CATALOG 仅保留国内已备案模型 | ✅ |
|
||||||
|
| **modelMap 净化** | chat() 和 chatStream() 中的 modelMap 仅路由到国内 provider(移除 general/openai/gpt/longcat) | ✅ |
|
||||||
|
| **provider 命名清晰化** | "OpenAI 兼容接口" → "硅基流动 / DeepSeek",明确标注实际调用的国内服务商 | ✅ |
|
||||||
|
| **沙盒可用模型 API** | 新增 `GET /sandbox/models` 端点,返回实际可用的模型列表(按 provider 注册状态动态过滤) | ✅ |
|
||||||
|
| **前端模型列表** | ModelSelector 改为从 `/sandbox/models` 获取,不再从 `/public/models`(百科模型列表)获取 | ✅ |
|
||||||
|
| **models.ts 清理** | 移除 foreign 模型,仅保留 deepseek-v4-flash 和 sensenova-6.7-flash-lite,DEFAULT_MODEL 改为 domestic | ✅ |
|
||||||
|
| **返佣注册指南** | 新建 `docs/affiliate-registration.md`,收录 7 个平台的注册入口、佣金比例、申请流程 | ✅ |
|
||||||
|
|
||||||
### 待办/下一步
|
### 待办/下一步
|
||||||
1. **为工具库加返佣链接**:在管理后台 `/admin/tools/new` 和编辑页录入 affiliateLink 字段
|
1. **注册联盟账号**:按 affiliate-registration.md 优先级注册阿里云云大使、百度智能云推广大使、Canvassador、腾讯云推广大使
|
||||||
2. **注册 AI 工具联盟账号**:对接 Canva/Copy.ai 等工具的返佣计划
|
2. **替换占位链接**:拿到真实联盟 ID 后替换 seed-tools-affiliate.ts 中的 `yuzhiran` 占位
|
||||||
3. **内容策略**:批量生产 AI 工具评测文章做 SEO
|
3. **生产部署**:`npm run build`(前端 SSG)+ 复制到服务器 + PM2 restart 后端
|
||||||
4. **编程导师内容扩充**:按难度分级写更多 starter 问题
|
4. **内容策略**:批量生产 AI 工具评测文章做 SEO
|
||||||
|
5. **编程导师内容扩充**:按难度分级写更多 starter 问题
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"predev": "rm -rf out",
|
"predev": "rm -rf out",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
"postbuild": "node scripts/generate-sitemap.js",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://yuzhiran.com/sitemap.xml
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const SITE_URL = 'https://yuzhiran.com';
|
||||||
|
const OUT_DIR = path.join(__dirname, '..', 'out');
|
||||||
|
|
||||||
|
const PRIORITIES = {
|
||||||
|
'/': 1.0,
|
||||||
|
'/tools': 0.9,
|
||||||
|
'/courses': 0.9,
|
||||||
|
'/skills': 0.8,
|
||||||
|
'/marketplace': 0.8,
|
||||||
|
'/practices': 0.8,
|
||||||
|
'/contents': 0.8,
|
||||||
|
'/prompts': 0.8,
|
||||||
|
'/models': 0.7,
|
||||||
|
'/sandbox': 0.7,
|
||||||
|
'/about': 0.5,
|
||||||
|
'/privacy': 0.3,
|
||||||
|
'/terms': 0.3,
|
||||||
|
};
|
||||||
|
|
||||||
|
function walkDir(dir, basePath = '') {
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
const urls = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
const relativeName = path.join(basePath, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
||||||
|
urls.push(...walkDir(fullPath, relativeName));
|
||||||
|
} else if (entry.name === 'page.html' || entry.name.endsWith('.html')) {
|
||||||
|
if (relativeName === 'index.html' || relativeName === 'page.html') {
|
||||||
|
urls.push({ path: '/', file: fullPath });
|
||||||
|
} else {
|
||||||
|
let urlPath = '/' + relativeName.replace(/\.html$/, '').replace(/\/index$/, '').replace(/\/page$/, '');
|
||||||
|
urls.push({ path: urlPath, file: fullPath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSitemap(urls) {
|
||||||
|
const now = new Date().toISOString().split('T')[0];
|
||||||
|
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||||
|
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
|
||||||
|
const seen = new Set();
|
||||||
|
for (const url of urls) {
|
||||||
|
if (seen.has(url.path)) continue;
|
||||||
|
seen.add(url.path);
|
||||||
|
if (url.path.startsWith('/admin') || url.path.startsWith('/_next') || url.path.startsWith('/api') || url.path === '/404') continue;
|
||||||
|
if (url.path.includes('/[') || url.path.includes(']')) continue;
|
||||||
|
const depth = url.path.split('/').filter(Boolean).length;
|
||||||
|
const changefreq = depth <= 1 ? 'weekly' : 'monthly';
|
||||||
|
const priority = PRIORITIES[url.path];
|
||||||
|
const computedPriority = priority !== undefined ? priority : Math.max(0.1, 0.7 - depth * 0.1);
|
||||||
|
xml += ' <url>\n';
|
||||||
|
xml += ` <loc>${SITE_URL}${url.path}</loc>\n`;
|
||||||
|
xml += ` <lastmod>${now}</lastmod>\n`;
|
||||||
|
xml += ` <changefreq>${changefreq}</changefreq>\n`;
|
||||||
|
xml += ` <priority>${computedPriority.toFixed(1)}</priority>\n`;
|
||||||
|
xml += ' </url>\n';
|
||||||
|
}
|
||||||
|
xml += '</urlset>';
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(OUT_DIR)) {
|
||||||
|
console.error('out/ directory not found. Run next build first.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const urls = walkDir(OUT_DIR);
|
||||||
|
console.log(`Found ${urls.length} HTML pages in output.`);
|
||||||
|
const sitemap = generateSitemap(urls);
|
||||||
|
const outputPath = path.join(OUT_DIR, 'sitemap.xml');
|
||||||
|
fs.writeFileSync(outputPath, sitemap, 'utf-8');
|
||||||
|
console.log(`Generated sitemap.xml with ${sitemap.split('<url>').length - 1} URLs → ${outputPath}`);
|
||||||
|
|
||||||
|
// Copy robots.txt from public/ to out/
|
||||||
|
const robotsSrc = path.join(__dirname, '..', 'public', 'robots.txt');
|
||||||
|
const robotsDst = path.join(OUT_DIR, 'robots.txt');
|
||||||
|
if (fs.existsSync(robotsSrc)) {
|
||||||
|
fs.copyFileSync(robotsSrc, robotsDst);
|
||||||
|
console.log(`Copied robots.txt → ${robotsDst}`);
|
||||||
|
}
|
||||||
@@ -12,28 +12,38 @@ import { API_BASE } from '@/lib/config';
|
|||||||
export default function EditToolPage() {
|
export default function EditToolPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [form, setForm] = useState({ name: '', description: '', url: '', icon: '', tags: '' });
|
const [form, setForm] = useState({ name: '', description: '', url: '', icon: '', affiliateLink: '', tags: '' });
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
useEffect(() => { loadTool(); }, [params.id]);
|
useEffect(() => { loadTool(); }, [params.id]);
|
||||||
|
|
||||||
const base = API_BASE;
|
|
||||||
function token() { return localStorage.getItem('adminToken'); }
|
function token() { return localStorage.getItem('adminToken'); }
|
||||||
function headers() {
|
function authHeaders(): Record<string, string> {
|
||||||
const t = token();
|
const t = token();
|
||||||
return { 'Content-Type': 'application/json', ...(t ? { Authorization: `Bearer ${t}` } : {}) };
|
return { 'Content-Type': 'application/json', ...(t ? { Authorization: `Bearer ${t}` } : {}) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTool() {
|
async function loadTool() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${base}/tools`, { headers: headers() });
|
const res = await fetch(`${API_BASE}/admin/tools/${params.id}`, { headers: authHeaders() });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const tool = await res.json();
|
||||||
const tool = (data.items || []).find((t: any) => t.id === Number(params.id));
|
setForm({
|
||||||
if (tool) setForm({ name: tool.name || '', description: tool.description || '', url: tool.url || '', icon: tool.icon || '', tags: tool.tags || '' });
|
name: tool.name || '',
|
||||||
|
description: tool.description || '',
|
||||||
|
url: tool.url || '',
|
||||||
|
icon: tool.icon || '',
|
||||||
|
affiliateLink: tool.affiliateLink || '',
|
||||||
|
tags: tool.tags || '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setError('加载工具信息失败');
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {
|
||||||
|
setError('网络错误');
|
||||||
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +51,24 @@ export default function EditToolPage() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!form.name.trim() || !form.url.trim()) return;
|
if (!form.name.trim() || !form.url.trim()) return;
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
router.push('/admin/tools');
|
setError('');
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/admin/tools/${params.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: authHeaders(),
|
||||||
|
body: JSON.stringify(form),
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
router.push('/admin/tools');
|
||||||
|
} else {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
setError(data.message || '保存失败');
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setError('网络错误');
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return (
|
if (loading) return (
|
||||||
@@ -58,12 +85,48 @@ export default function EditToolPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="max-w-2xl mx-auto p-6">
|
<div className="max-w-2xl mx-auto p-6">
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<p className="text-sm text-muted-foreground mb-4">工具信息展示</p>
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="space-y-3">
|
{error && (
|
||||||
<div><label className="text-sm font-medium text-foreground">名称</label><p className="text-sm text-muted-foreground">{form.name}</p></div>
|
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-700 dark:text-red-400">
|
||||||
<div><label className="text-sm font-medium text-foreground">描述</label><p className="text-sm text-muted-foreground">{form.description || '-'}</p></div>
|
{error}
|
||||||
<div><label className="text-sm font-medium text-foreground">链接</label><p className="text-sm text-muted-foreground">{form.url}</p></div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-1">名称</label>
|
||||||
|
<Input value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} placeholder="工具名称" required />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-1">描述</label>
|
||||||
|
<textarea value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))}
|
||||||
|
rows={3} className="w-full px-3 py-2 bg-background border border-input rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
placeholder="工具简介" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-1">链接</label>
|
||||||
|
<Input value={form.url} onChange={e => setForm(f => ({ ...f, url: e.target.value }))} placeholder="https://..." required />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-1">图标链接</label>
|
||||||
|
<Input value={form.icon} onChange={e => setForm(f => ({ ...f, icon: e.target.value }))} placeholder="https://..." />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-1">
|
||||||
|
返佣链接 <span className="text-muted-foreground font-normal">(可选)</span>
|
||||||
|
</label>
|
||||||
|
<Input value={form.affiliateLink} onChange={e => setForm(f => ({ ...f, affiliateLink: e.target.value }))} placeholder="https://..." />
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">通过此链接跳转可获得返佣收入</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-1">标签</label>
|
||||||
|
<Input value={form.tags} onChange={e => setForm(f => ({ ...f, tags: e.target.value }))} placeholder="AI,工具,效率" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button type="submit" disabled={submitting || !form.name.trim() || !form.url.trim()}>
|
||||||
|
{submitting ? '保存中...' : '保存'}
|
||||||
|
</Button>
|
||||||
|
<Link href="/admin/tools"><Button type="button" variant="outline">取消</Button></Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { API_BASE } from '@/lib/config';
|
|||||||
|
|
||||||
export default function NewToolPage() {
|
export default function NewToolPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [form, setForm] = useState({ name: '', description: '', url: '', icon: '', tags: '' });
|
const [form, setForm] = useState({ name: '', description: '', url: '', icon: '', affiliateLink: '', tags: '' });
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
@@ -55,6 +55,13 @@ export default function NewToolPage() {
|
|||||||
<label className="block text-sm font-medium text-foreground mb-1">图标链接</label>
|
<label className="block text-sm font-medium text-foreground mb-1">图标链接</label>
|
||||||
<Input value={form.icon} onChange={e => setForm(f => ({ ...f, icon: e.target.value }))} placeholder="https://..." />
|
<Input value={form.icon} onChange={e => setForm(f => ({ ...f, icon: e.target.value }))} placeholder="https://..." />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-1">
|
||||||
|
返佣链接 <span className="text-muted-foreground font-normal">(可选)</span>
|
||||||
|
</label>
|
||||||
|
<Input value={form.affiliateLink} onChange={e => setForm(f => ({ ...f, affiliateLink: e.target.value }))} placeholder="https://..." />
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">通过此链接跳转可获得返佣收入</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-1">标签</label>
|
<label className="block text-sm font-medium text-foreground mb-1">标签</label>
|
||||||
<Input value={form.tags} onChange={e => setForm(f => ({ ...f, tags: e.target.value }))} placeholder="AI,工具,效率" />
|
<Input value={form.tags} onChange={e => setForm(f => ({ ...f, tags: e.target.value }))} placeholder="AI,工具,效率" />
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ export function ModelSelector({ value, onChange, className = '' }: ModelSelector
|
|||||||
const [models, setModels] = useState<ModelOption[]>(FALLBACK_MODELS);
|
const [models, setModels] = useState<ModelOption[]>(FALLBACK_MODELS);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${API_BASE}/public/models`)
|
fetch(`${API_BASE}/sandbox/models`)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.items?.length) {
|
if (data.items?.length) {
|
||||||
setModels(data.items.map((m: any) => ({
|
setModels(data.items.map((m: any) => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
label: m.name,
|
label: m.label,
|
||||||
provider: m.provider,
|
provider: m.provider,
|
||||||
desc: m.description || '',
|
desc: m.desc,
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ export interface ModelOption {
|
|||||||
desc: string;
|
desc: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 沙箱可选模型 — 仅国内已备案大模型 */
|
||||||
export const AVAILABLE_MODELS: ModelOption[] = [
|
export const AVAILABLE_MODELS: ModelOption[] = [
|
||||||
{ id: 'general', label: '通用模式', provider: '', desc: '日常问答,综合能力均衡' },
|
{ id: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash', 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: '轻量快速,日常对话与文本处理' },
|
||||||
{ id: 'sensenova-6.7-flash-lite', label: 'SenseNova 6.7 Flash Lite', provider: '', desc: '轻量快速,日常使用' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_MODEL = 'general';
|
export const DEFAULT_MODEL = 'deepseek-v4-flash';
|
||||||
|
|||||||
Reference in New Issue
Block a user