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:
@@ -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'
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user