feat: admin back-office system + AI assistant with action commands

- Admin analytics (recharts charts, overview stats, trend analysis, time range selector)
- Admin permissions (AdminRole/AdminUser models, role CRUD, permission catalog)
- Admin system config (site/AI/member category tabs, per-key save)
- Admin operations (Banner CRUD, push notification send/delete)
- Admin user management (table, search, create/edit, ban/delete)
- Admin layout (custom top bar with branding + sidebar, admin AI assistant)
- Admin login (adminToken localStorage, adminInfo display)
- Admin AI assistant (purple '运营助手', context-aware prompts, action commands)
- Public AI assistant Phase B (action commands: navigate, setModel, startChat, openSkill, setParameter)
- Assistant context mapping + action executor
- Skills API fix: tags parsing fallback (JSON.parse → split)
- Sandbox crash fix: curScene fallback system prompt
- JWT token expiration: access 2h→7d, refresh 7d→30d, admin 8h→7d
- i18n: assistant-related translations (zh/en)
- PM2 ecosystem config for process management
- AI assistant login prompt fix: admin uses getAdminToken(), public uses apiFetch with token refresh
This commit is contained in:
yuzhiran-dev
2026-05-20 10:38:58 +08:00
parent dd9240e5cf
commit 728edc59ef
33 changed files with 3014 additions and 153 deletions
+87
View File
@@ -0,0 +1,87 @@
'use client'
import { useRouter } from 'next/navigation'
import type { AssistantAction } from './assistant-context'
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) {
return { success: false, message: 'Router not initialized' }
}
const { action: actionType, description, path, model, prompt, skillId, temperature, top_p, max_tokens } = action
switch (actionType) {
case 'navigate':
if (path) {
routerInstance.push(path)
return { success: true, message: `正在导航到:${description || path}` }
}
return { success: false, message: '导航路径无效' }
case 'setModel':
if (model) {
sessionStorage.setItem('assistant-set-model', model)
routerInstance.push('/sandbox')
setTimeout(() => {
window.dispatchEvent(new CustomEvent('assistant-set-model', { detail: model }))
}, 500)
return { success: true, message: `正在切换模型:${model}` }
}
return { success: false, message: '模型名称无效' }
case 'startChat':
if (prompt) {
sessionStorage.setItem('assistant-start-chat', prompt)
routerInstance.push('/sandbox')
setTimeout(() => {
window.dispatchEvent(new CustomEvent('assistant-start-chat', { detail: prompt }))
}, 500)
return { success: true, message: `正在开始对话:${prompt.slice(0, 20)}...` }
}
return { success: false, message: '对话内容无效' }
case 'openSkill':
if (skillId) {
routerInstance.push(`/skills/${skillId}`)
return { success: true, message: `正在打开技能:${description || 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')
setTimeout(() => {
window.dispatchEvent(new CustomEvent('assistant-set-params', { detail: params }))
}, 500)
return { success: true, message: `正在设置参数:${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: 'opencode', label: 'OpenCode Go' },
{ value: 'meituan/longcat-flash-lite', label: '长颈鹿 Flash' },
]
export const SKILL_IDS = [
'general-chat', 'coding', 'writing', 'english', 'data-analysis',
'ppt-design', 'image-prompt', 'career-guide'
]
+112
View File
@@ -0,0 +1,112 @@
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
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> = {
'/': {
page: 'home',
systemPrompt: '你是宇之然 AI 学习与实践平台的智能助手。平台提供 AI 沙盒、技能库、提示词工坊、模型百科、课程学习等功能。帮助用户了解平台功能、推荐学习路径。' + ACTION_FORMAT,
starters: ['宇之然 AI 能做什么?', '如何开始学习 AI', '有哪些功能可以使用?', '推荐一个学习路线'],
},
'/sandbox': {
page: 'sandbox',
systemPrompt: '你是宇之然 AI 沙盒的智能助手。AI 沙盒是在线对话实践环境,支持多种模型切换、高级参数调节、历史会话管理。帮助用户了解如何使用沙盒、调试问题。' + ACTION_FORMAT,
starters: ['如何切换模型?', '高级参数怎么调?', '如何查看历史记录?', '对话次数限制是多少?'],
},
'/skills': {
page: 'skills',
systemPrompt: '你是宇之然技能库的智能助手。技能库提供可组合的 AI 学习技能模块,每个技能包含系统提示词、练习任务和 starter 问题。帮助用户选择合适的技能。' + ACTION_FORMAT,
starters: ['有哪些技能可以学习?', '如何选择适合我的技能?', '技能难度怎么区分?', '如何开始练习一个技能?'],
},
'/learning': {
page: 'learning',
systemPrompt: '你是宇之然学习路径的智能助手。平台提供学情分析(知识领域掌握度)和分阶段学习路径(从入门到精通)。帮助用户制定学习计划、分析薄弱环节。' + ACTION_FORMAT,
starters: ['学情分析怎么用?', '学习路径有哪些阶段?', '如何查看薄弱环节?', '推荐学习内容是什么?'],
},
'/my': {
page: 'my',
systemPrompt: '你是宇之然个人中心的智能助手。个人中心管理会员订阅、查看订单、编辑个人信息。帮助用户管理账户和订阅。' + ACTION_FORMAT,
starters: ['如何开通会员?', '会员有哪些权益?', '如何查看订单记录?', '免费和付费有什么区别?'],
},
'/models': {
page: 'models',
systemPrompt: '你是宇之然模型百科的智能助手。模型百科收录主流 AI 模型信息,包括能力对比、适用场景。帮助用户了解不同模型的差异。' + ACTION_FORMAT,
starters: ['有哪些模型可以参考?', '如何选择合适的模型?', '模型的参数代表什么?', '模型能力怎么对比?'],
},
'/prompts': {
page: 'prompts',
systemPrompt: '你是宇之然提示词工坊的智能助手。提示词工坊提供提示词编写、测试、优化的工具,支持变量设置、角色设定、保存到提示词库。帮助用户学习提示词工程。' + ACTION_FORMAT,
starters: ['如何编写一个好的提示词?', '什么是角色设定?', '如何测试提示词效果?', '提示词变量怎么用?'],
},
'/courses': {
page: 'courses',
systemPrompt: '你是宇之然课程专题的智能助手。平台提供 AI 通识、提示词工程、智能体教程等专题课程。帮助用户选择课程、规划学习。' + ACTION_FORMAT,
starters: ['有哪些课程可以学习?', '如何选择适合我的课程?', '课程从哪开始学?', '课程需要什么基础?'],
},
'/community': {
page: 'community',
systemPrompt: '你是宇之然社区的智能助手。社区是用户交流分享的平台,可以发布帖子、评论互动。帮助用户了解社区规则、找到感兴趣的话题。' + ACTION_FORMAT,
starters: ['社区有哪些板块?', '如何发帖?', '如何找到感兴趣的内容?', '社区使用有什么规则?'],
},
'/contents': {
page: 'contents',
systemPrompt: '你是宇之然文章频道的智能助手。文章频道提供 AI 相关技术文章、教程和资讯。帮助用户找到想读的内容。' + ACTION_FORMAT,
starters: ['有哪些类型的文章?', '推荐几篇热门文章', '最近有哪些新文章?', '如何搜索文章?'],
},
'/tools': {
page: 'tools',
systemPrompt: '你是宇之然 AI 工具集的智能助手。工具集收录各类 AI 工具推荐和使用指南。帮助用户找到合适的工具。' + ACTION_FORMAT,
starters: ['有哪些 AI 工具推荐?', '如何选择适合我的工具?', '有哪些免费工具?', '工具怎么分类?'],
},
'/compare': {
page: 'compare',
systemPrompt: '你是宇之然对比实验室的智能助手。对比实验室支持同题对比不同 AI 模型的回答效果。帮助用户设置对比、分析结果。' + ACTION_FORMAT,
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)
for (const key of sorted) {
if (pathname.startsWith(key)) {
return contexts[key]
}
}
return 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
}