注册支持用户名/手机号/邮箱 + 镜像站部署脚本 + AI 助手 Tool Calling 重构

- Prisma User 模型新增 username 字段(唯一索引)
- 注册先查重复再创建,返回友好中文提示(非 500)
- 登录支持用户名/手机号/邮箱三种方式
- 前端注册表单增加用户名输入框,预校验 2-20 位格式
- 新增 scripts/deploy.sh:一键构建并部署主站+镜像站+重启后端+重载 Nginx
- 镜像站 www.yuzhiran.com.cn Nginx 配置与主站同步
- AI 助手架构升级:用户端/管理后台均采用完整 Tool Calling 架构
- 新增 UserAiAssistantService(18 工具)+ AiAssistantController
- admin 助手新增 search + mark-all-notifications-read 工具
- 修复注册 500 错误:catch Prisma P2002 → BadRequestException
- Baidu Analytics Script 注入 root layout
This commit is contained in:
yuzhiran-dev
2026-06-01 23:14:42 +08:00
parent 6f3fe50ee0
commit 538de50bb1
329 changed files with 8777 additions and 868 deletions
+35 -51
View File
@@ -1,86 +1,70 @@
'use client'
import { useRouter } from 'next/navigation'
import type { AssistantAction } from './assistant-context'
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'
let routerInstance: ReturnType<typeof useRouter> | null = null
export function setRouter(router: ReturnType<typeof useRouter>) {
routerInstance = router
}
export async function executeAction(action: AssistantAction): Promise<{ success: boolean; message: string }> {
if (!routerInstance) {
export async function executeClientAction(
tool: string,
params: Record<string, any>,
router: AppRouterInstance,
): Promise<{ success: boolean; message: string }> {
if (!router) {
return { success: false, message: 'Router not initialized' }
}
const { action: actionType, description, path, model, prompt, skillId, temperature, top_p, max_tokens } = action
switch (actionType) {
switch (tool) {
case 'navigate':
if (path) {
routerInstance.push(path)
return { success: true, message: `正在导航到:${description || path}` }
if (params.path) {
router.push(params.path)
return { success: true, message: `正在导航到:${params.description || params.path}` }
}
return { success: false, message: '导航路径无效' }
case 'setModel':
if (model) {
sessionStorage.setItem('assistant-set-model', model)
routerInstance.push('/sandbox')
if (params.model) {
sessionStorage.setItem('assistant-set-model', params.model)
router.push('/sandbox')
setTimeout(() => {
window.dispatchEvent(new CustomEvent('assistant-set-model', { detail: model }))
window.dispatchEvent(new CustomEvent('assistant-set-model', { detail: params.model }))
}, 500)
return { success: true, message: `正在切换模型:${model}` }
return { success: true, message: `正在切换模型:${params.model}` }
}
return { success: false, message: '模型名称无效' }
case 'startChat':
if (prompt) {
sessionStorage.setItem('assistant-start-chat', prompt)
routerInstance.push('/sandbox')
if (params.prompt) {
sessionStorage.setItem('assistant-start-chat', params.prompt)
router.push('/sandbox')
setTimeout(() => {
window.dispatchEvent(new CustomEvent('assistant-start-chat', { detail: prompt }))
window.dispatchEvent(new CustomEvent('assistant-start-chat', { detail: params.prompt }))
}, 500)
return { success: true, message: `正在开始对话:${prompt.slice(0, 20)}...` }
return { success: true, message: `正在开始对话:${params.prompt.slice(0, 20)}...` }
}
return { success: false, message: '对话内容无效' }
case 'openSkill':
if (skillId) {
routerInstance.push(`/skills/${skillId}`)
return { success: true, message: `正在打开技能:${description || skillId}` }
if (params.skillId) {
router.push(`/skills/${params.skillId}`)
return { success: true, message: `正在打开技能:${params.description || params.skillId}` }
}
return { success: false, message: '技能 ID 无效' }
case 'setParameter':
const params: Record<string, string> = {}
if (temperature !== undefined) params.temperature = String(temperature)
if (top_p !== undefined) params.top_p = String(top_p)
if (max_tokens !== undefined) params.max_tokens = String(max_tokens)
if (Object.keys(params).length > 0) {
sessionStorage.setItem('assistant-set-params', JSON.stringify(params))
routerInstance.push('/sandbox')
case 'setParameter': {
const storageParams: Record<string, string> = {}
if (params.temperature !== undefined) storageParams.temperature = String(params.temperature)
if (params.top_p !== undefined) storageParams.top_p = String(params.top_p)
if (params.max_tokens !== undefined) storageParams.max_tokens = String(params.max_tokens)
if (Object.keys(storageParams).length > 0) {
sessionStorage.setItem('assistant-set-params', JSON.stringify(storageParams))
router.push('/sandbox')
setTimeout(() => {
window.dispatchEvent(new CustomEvent('assistant-set-params', { detail: params }))
window.dispatchEvent(new CustomEvent('assistant-set-params', { detail: storageParams }))
}, 500)
return { success: true, message: `正在设置参数:${description}` }
return { success: true, message: `正在设置参数:${params.description}` }
}
return { success: false, message: '参数无效' }
}
default:
return { success: false, message: '未知操作类型' }
}
}
export const MODEL_OPTIONS = [
{ value: 'general', label: '通用模式' },
{ value: 'deepseek-v4-flash', label: 'DeepSeek V4 Flash' },
{ value: 'sensenova-6.7-flash-lite', label: 'SenseNova 6.7 Flash Lite' },
]
export const SKILL_IDS = [
'general-chat', 'coding', 'writing', 'english', 'data-analysis',
'ppt-design', 'image-prompt', 'career-guide'
]
+22 -68
View File
@@ -1,112 +1,66 @@
export interface AssistantAction {
action: 'navigate' | 'setModel' | 'startChat' | 'openSkill' | 'setParameter'
path?: string
model?: string
prompt?: string
skillId?: string
temperature?: number
top_p?: number
max_tokens?: number
description: string
}
export interface AssistantContext {
page: string
systemPrompt: string
systemPrompt?: string
starters: string[]
}
const ACTION_FORMAT = `\n\n【特殊指令】当你可以直接帮助用户完成操作时,请返回 JSON 格式的指令(不要包含其他内容):\n- {"action":"navigate","path":"/sandbox","description":"导航到页面"}\n- {"action":"setModel","model":"deepseek-v4-flash","description":"切换模型"}\n- {"action":"startChat","prompt":"用户问题","description":"开始新对话"}\n- {"action":"openSkill","skillId":"coding","description":"打开技能详情"}\n- {"action":"setParameter","temperature":0.9,"description":"设置参数"}\n只有当用户请求的操作可以自动化时才返回指令,否则只返回文字回答。`
const contexts: Record<string, AssistantContext> = {
const PAGE_CONTEXTS: Record<string, { systemPrompt: string; starters: string[] }> = {
'/': {
page: 'home',
systemPrompt: '你是宇之然 AI 学习与实践平台的智能助手。平台提供 AI 沙盒、技能库、提示词工坊、模型百科、课程学习等功能。帮助用户了解平台功能、推荐学习路径。' + ACTION_FORMAT,
systemPrompt: '你是宇之然 AI 学习与实践平台的智能助手。平台提供 AI 沙盒、技能库、提示词工坊、模型百科、课程学习等功能。帮助用户了解平台功能、推荐学习路径。',
starters: ['宇之然 AI 能做什么?', '如何开始学习 AI', '有哪些功能可以使用?', '推荐一个学习路线'],
},
'/sandbox': {
page: 'sandbox',
systemPrompt: '你是宇之然 AI 沙盒的智能助手。AI 沙盒是在线对话实践环境,支持多种模型切换、高级参数调节、历史会话管理。帮助用户了解如何使用沙盒、调试问题。' + ACTION_FORMAT,
systemPrompt: '你是宇之然 AI 沙盒的智能助手。AI 沙盒是在线对话实践环境,支持多种模型切换、高级参数调节、历史会话管理。',
starters: ['如何切换模型?', '高级参数怎么调?', '如何查看历史记录?', '对话次数限制是多少?'],
},
'/skills': {
page: 'skills',
systemPrompt: '你是宇之然技能库的智能助手。技能库提供可组合的 AI 学习技能模块,每个技能包含系统提示词、练习任务和 starter 问题。帮助用户选择合适的技能' + ACTION_FORMAT,
starters: ['有哪些技能可以学习?', '如何选择适合我的技能?', '技能难度怎么区分?', '如何开始练习一个技能?'],
systemPrompt: '你是宇之然技能库的智能助手。技能库提供可组合的 AI 学习技能模块,每个技能包含系统提示词、练习任务和 starter 问题。',
starters: ['有哪些技能可以学习?', '如何选择适合我的技能', '有哪些入门技能?', '如何开始练习一个技能?'],
},
'/learning': {
page: 'learning',
systemPrompt: '你是宇之然学习路径的智能助手。平台提供学情分析(知识领域掌握度)和分阶段学习路径(从入门到精通)。帮助用户制定学习计划、分析薄弱环节' + ACTION_FORMAT,
starters: ['学情分析怎么用?', '学习路径有哪些阶段?', '如何查看薄弱环节?', '推荐学习内容是什么?'],
systemPrompt: '你是宇之然学习路径的智能助手。平台提供学情分析和分阶段学习路径。帮助用户制定学习计划、分析薄弱环节。',
starters: ['查看学情分析', '学习路径有哪些阶段?', '如何查看薄弱环节', '推荐学习内容是什么?'],
},
'/my': {
page: 'my',
systemPrompt: '你是宇之然个人中心的智能助手。个人中心管理会员订阅、查看订单、编辑个人信息。帮助用户管理账户和订阅。' + ACTION_FORMAT,
systemPrompt: '你是宇之然个人中心的智能助手。个人中心管理会员订阅、查看订单、编辑个人信息。',
starters: ['如何开通会员?', '会员有哪些权益?', '如何查看订单记录?', '免费和付费有什么区别?'],
},
'/models': {
page: 'models',
systemPrompt: '你是宇之然模型百科的智能助手。模型百科收录主流 AI 模型信息,包括能力对比、适用场景。帮助用户了解不同模型的差异。' + ACTION_FORMAT,
starters: ['有哪些模型可以参考?', '如何选择合适的模型?', '模型的参数代表什么?', '模型能力怎么对比?'],
systemPrompt: '你是宇之然模型百科的智能助手。模型百科收录主流 AI 模型信息,包括能力对比、适用场景。',
starters: ['有哪些模型可以参考?', '如何选择合适的模型?', '模型能力怎么对比?', '查看所有模型'],
},
'/prompts': {
page: 'prompts',
systemPrompt: '你是宇之然提示词工坊的智能助手。提示词工坊提供提示词编写、测试、优化的工具,支持变量设置、角色设定、保存到提示词库。帮助用户学习提示词工程。' + ACTION_FORMAT,
starters: ['如何编写一个好的提示词?', '什么是角色设定?', '如何测试提示词效果?', '提示词变量怎么用?'],
systemPrompt: '你是宇之然提示词工坊的智能助手。提示词工坊提供提示词编写、测试、优化的工具。',
starters: ['如何编写一个好的提示词?', '查看有哪些提示词', '如何测试提示词效果?', '提示词变量怎么用?'],
},
'/courses': {
page: 'courses',
systemPrompt: '你是宇之然课程专题的智能助手。平台提供 AI 通识、提示词工程、智能体教程等专题课程。帮助用户选择课程、规划学习。' + ACTION_FORMAT,
starters: ['有哪些课程可以学习?', '如何选择适合我的课程?', '课程从哪开始学?', '课程需要什么基础?'],
systemPrompt: '你是宇之然课程专题的智能助手。平台提供 AI 通识、提示词工程、智能体教程等专题课程。',
starters: ['有哪些课程可以学习?', '如何选择适合我的课程?', '课程从哪开始学?', '搜索 AI 课程'],
},
'/community': {
page: 'community',
systemPrompt: '你是宇之然社区的智能助手。社区是用户交流分享的平台,可以发布帖子、评论互动。帮助用户了解社区规则、找到感兴趣的话题。' + ACTION_FORMAT,
systemPrompt: '你是宇之然社区的智能助手。社区是用户交流分享的平台,可以发布帖子、评论互动。',
starters: ['社区有哪些板块?', '如何发帖?', '如何找到感兴趣的内容?', '社区使用有什么规则?'],
},
'/contents': {
page: 'contents',
systemPrompt: '你是宇之然文章频道的智能助手。文章频道提供 AI 相关技术文章、教程和资讯。帮助用户找到想读的内容。' + ACTION_FORMAT,
systemPrompt: '你是宇之然文章频道的智能助手。文章频道提供 AI 相关技术文章、教程和资讯。',
starters: ['有哪些类型的文章?', '推荐几篇热门文章', '最近有哪些新文章?', '如何搜索文章?'],
},
'/tools': {
page: 'tools',
systemPrompt: '你是宇之然 AI 工具集的智能助手。工具集收录各类 AI 工具推荐和使用指南。帮助用户找到合适的工具。' + ACTION_FORMAT,
systemPrompt: '你是宇之然 AI 工具集的智能助手。工具集收录各类 AI 工具推荐和使用指南。',
starters: ['有哪些 AI 工具推荐?', '如何选择适合我的工具?', '有哪些免费工具?', '工具怎么分类?'],
},
'/compare': {
page: 'compare',
systemPrompt: '你是宇之然对比实验室的智能助手。对比实验室支持同题对比不同 AI 模型的回答效果。帮助用户设置对比、分析结果。' + ACTION_FORMAT,
systemPrompt: '你是宇之然对比实验室的智能助手。对比实验室支持同题对比不同 AI 模型的回答效果。',
starters: ['对比实验室怎么用?', '如何添加对比模型?', '对比结果怎么看?', '支持哪些模型对比?'],
},
'/code': {
page: 'code',
systemPrompt: '你是宇之然代码沙盒的智能助手。代码沙盒是在线代码运行环境,支持 React、图表、3D 等模板,可实时预览效果。帮助用户编写和调试代码。' + ACTION_FORMAT,
starters: ['代码沙盒怎么用?', '支持哪些模板?', '如何查看控制台输出?', '可以运行什么类型的代码?'],
},
}
export function getAssistantContext(pathname: string): AssistantContext {
const sorted = Object.keys(contexts).sort((a, b) => b.length - a.length)
const sorted = Object.keys(PAGE_CONTEXTS).sort((a, b) => b.length - a.length)
for (const key of sorted) {
if (pathname.startsWith(key)) {
return contexts[key]
return PAGE_CONTEXTS[key]
}
}
return contexts['/']
return PAGE_CONTEXTS['/']
}
export function parseActionCommand(text: string): AssistantAction | null {
const jsonMatch = text.match(/\{[\s\S]*?"action"\s*:\s*?"[^"]+"[\s\S]*?\}/)
if (!jsonMatch) return null
try {
const parsed = JSON.parse(jsonMatch[0])
if (parsed.action && ['navigate', 'setModel', 'startChat', 'openSkill', 'setParameter'].includes(parsed.action)) {
return parsed as AssistantAction
}
} catch {
return null
}
return null
}
View File
Regular → Executable
View File
Regular → Executable
+1 -1
View File
@@ -1,4 +1,4 @@
export const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api/v1';
export const API_BASE = process.env.NEXT_PUBLIC_API_URL || '/api/v1';
export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://yuzhiran.com';
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File