3468 lines
188 KiB
HTML
3468 lines
188 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Coze Studio 管理后台</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: #f5f7fa;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.admin-container {
|
||
display: flex;
|
||
height: 100vh;
|
||
}
|
||
|
||
.sidebar {
|
||
width: 250px;
|
||
background: #2c3e50;
|
||
color: white;
|
||
padding: 0;
|
||
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
|
||
position: relative;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 20px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.logo-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.logo-text {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #ecf0f1;
|
||
}
|
||
|
||
.nav-menu {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.nav-item {
|
||
margin: 0;
|
||
}
|
||
|
||
.nav-link {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 20px;
|
||
color: #bdc3c7;
|
||
text-decoration: none;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
position: relative;
|
||
}
|
||
|
||
.nav-link:hover {
|
||
background: rgba(52, 152, 219, 0.1);
|
||
color: #3498db;
|
||
}
|
||
|
||
.nav-link.active {
|
||
background: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.nav-icon {
|
||
margin-right: 12px;
|
||
font-size: 16px;
|
||
width: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.nav-arrow {
|
||
margin-left: auto;
|
||
font-size: 12px;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.has-submenu .nav-arrow {
|
||
transform: rotate(-90deg);
|
||
}
|
||
|
||
.has-submenu.expanded .nav-arrow {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
.submenu {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
background: rgba(0, 0, 0, 0.1);
|
||
max-height: 0;
|
||
overflow: hidden;
|
||
transition: max-height 0.3s ease;
|
||
}
|
||
|
||
.has-submenu.expanded .submenu {
|
||
max-height: 200px;
|
||
}
|
||
|
||
.submenu-item {
|
||
margin: 0;
|
||
}
|
||
|
||
.submenu-link {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px 20px 10px 52px;
|
||
color: #95a5a6;
|
||
text-decoration: none;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.submenu-link:hover {
|
||
background: rgba(52, 152, 219, 0.1);
|
||
color: #3498db;
|
||
}
|
||
|
||
.submenu-link.active {
|
||
background: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.submenu-icon {
|
||
margin-right: 8px;
|
||
font-size: 14px;
|
||
width: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.main-content {
|
||
flex: 1;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.top-bar {
|
||
background: white;
|
||
padding: 20px 30px;
|
||
border-bottom: 1px solid #e1e8ed;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.page-header {
|
||
margin: 0;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
margin: 0 0 5px 0;
|
||
}
|
||
|
||
.page-description {
|
||
color: #6c757d;
|
||
font-size: 16px;
|
||
margin: 0;
|
||
}
|
||
|
||
.content-area {
|
||
flex: 1;
|
||
padding: 30px;
|
||
overflow-y: auto;
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.content-frame {
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
|
||
min-height: calc(100vh - 200px);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.loading {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 400px;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid #f3f3f3;
|
||
border-top: 4px solid #667eea;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.error-message {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #dc3545;
|
||
}
|
||
|
||
.error-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.sidebar-footer {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 0 25px;
|
||
border-top: 1px solid rgba(255,255,255,0.2);
|
||
padding-top: 20px;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
color: rgba(255,255,255,0.8);
|
||
font-size: 16px;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
background: rgba(255,255,255,0.2);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 10px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.sidebar {
|
||
width: 200px;
|
||
}
|
||
|
||
.content-area {
|
||
padding: 20px;
|
||
}
|
||
|
||
.top-bar {
|
||
padding: 15px 20px;
|
||
}
|
||
}
|
||
|
||
.welcome-content {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
}
|
||
|
||
.welcome-icon {
|
||
font-size: 64px;
|
||
color: #667eea;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.welcome-title {
|
||
font-size: 24px;
|
||
color: #2c3e50;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.welcome-text {
|
||
color: #7f8c8d;
|
||
font-size: 16px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 20px;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.stat-card {
|
||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||
color: white;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-number {
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
opacity: 0.9;
|
||
}
|
||
/* 透明浮层样式(合并自 config.html) */
|
||
.overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 10001; /* 保证高于所有模态层(9999) */
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||
}
|
||
|
||
.overlay.show {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
|
||
.overlay-content {
|
||
background: white;
|
||
padding: 24px 32px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||
max-width: 400px;
|
||
text-align: center;
|
||
transform: scale(0.9);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.overlay.show .overlay-content {
|
||
transform: scale(1);
|
||
}
|
||
|
||
.overlay-content.success {
|
||
border-left: 4px solid #16a34a;
|
||
color: #16a34a;
|
||
}
|
||
|
||
.overlay-content.error {
|
||
border-left: 4px solid #dc2626;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.overlay-content .icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.overlay-content.success .icon::before {
|
||
content: "✓";
|
||
}
|
||
|
||
.overlay-content.error .icon::before {
|
||
content: "✗";
|
||
}
|
||
|
||
.overlay-content .message {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.action-buttons {
|
||
margin-top: 30px;
|
||
padding-top: 24px;
|
||
border-top: 2px solid #f1f3f4;
|
||
text-align: center;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="admin-container">
|
||
<nav class="sidebar">
|
||
<div class="sidebar-header">
|
||
<div class="logo">
|
||
<div class="logo-icon">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||
<rect x="3" y="3" width="7" height="7" rx="1" fill="#4A90E2"/>
|
||
<rect x="14" y="3" width="7" height="7" rx="1" fill="#4A90E2"/>
|
||
<rect x="3" y="14" width="7" height="7" rx="1" fill="#4A90E2"/>
|
||
<rect x="14" y="14" width="7" height="7" rx="1" fill="#4A90E2"/>
|
||
</svg>
|
||
</div>
|
||
<span class="logo-text">Coze Studio</span>
|
||
</div>
|
||
</div>
|
||
|
||
<ul class="nav-menu">
|
||
<li class="nav-item">
|
||
<a href="#" class="nav-link" data-page="workspace">
|
||
<span class="nav-icon">🏠</span>
|
||
工作台
|
||
</a>
|
||
</li>
|
||
<li class="nav-item has-submenu">
|
||
<a href="#" class="nav-link" data-page="config">
|
||
<span class="nav-icon">⚙️</span>
|
||
配置管理
|
||
<span class="nav-arrow">▼</span>
|
||
</a>
|
||
<ul class="submenu">
|
||
<li class="submenu-item">
|
||
<a href="#" class="submenu-link active" data-page="basic-config">
|
||
<span class="submenu-icon">🔧</span>
|
||
基础配置
|
||
</a>
|
||
</li>
|
||
<li class="submenu-item">
|
||
<a href="#" class="submenu-link" data-page="model-management">
|
||
<span class="submenu-icon">🤖</span>
|
||
模型管理
|
||
</a>
|
||
</li>
|
||
<li class="submenu-item">
|
||
<a href="#" class="submenu-link" data-page="knowledge-config">
|
||
<span class="submenu-icon">📚</span>
|
||
知识库配置
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
|
||
<div class="sidebar-footer">
|
||
<div class="user-info">
|
||
<div class="user-avatar" id="userAvatar">👤</div>
|
||
<div>
|
||
<div id="userName">管理员</div>
|
||
<div id="userStatus" style="font-size: 12px; opacity: 0.7;">在线</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<main class="main-content">
|
||
<div class="top-bar" style="display: none;">
|
||
<div class="page-header">
|
||
<h1 class="page-title" id="pageTitle">基础配置</h1>
|
||
<p class="page-description" id="pageDescription">管理和配置基础配置</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="content-area">
|
||
<div class="content-frame">
|
||
<div id="pageContent">
|
||
<div class="loading">
|
||
<div class="loading-spinner"></div>
|
||
<span>${t('loading')}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- 全局透明浮层(来自 config.html) -->
|
||
<div id="overlay" class="overlay">
|
||
<div id="overlayContent" class="overlay-content">
|
||
<div class="icon"></div>
|
||
<div class="message" id="overlayMessage"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// i18n(简易字典 + 应用函数)
|
||
const I18N = {
|
||
'zh-CN': {
|
||
'nav.workspace': '工作台',
|
||
'nav.config': '配置管理',
|
||
'nav.basic': '基础配置',
|
||
'nav.model': '模型管理',
|
||
'nav.knowledge': '知识库配置',
|
||
|
||
'page.workspace.title': '工作台',
|
||
'page.workspace.desc': '工作台概览和快速操作',
|
||
'page.basic.title': '基础配置',
|
||
'page.basic.desc': '系统基础配置管理',
|
||
'page.model.title': '模型管理',
|
||
'page.model.desc': 'AI模型配置和管理',
|
||
'page.knowledge.title': '知识库配置',
|
||
'page.knowledge.desc': '知识库设置和管理',
|
||
'page.default.title': '页面',
|
||
'page.default.desc': '页面描述',
|
||
'page.contentArea': '这里是{title}页面的内容区域。',
|
||
|
||
'loading': '加载中...',
|
||
'status.online': '在线',
|
||
'error.workspace_forbidden': '无法访问工作台:',
|
||
'error.generic': '发生错误',
|
||
'success.generic': '操作成功',
|
||
|
||
'quick.title': '快速操作',
|
||
'quick.new': '新建',
|
||
'quick.import': '导入',
|
||
'quick.export': '导出',
|
||
'quick.settings': '设置',
|
||
|
||
'feature.module1': '功能模块 1',
|
||
'feature.module2': '功能模块 2',
|
||
'feature.module3': '功能模块 3',
|
||
'feature.desc': '相关功能描述和操作入口',
|
||
|
||
'user.admin': '管理员',
|
||
'workspace.hero.title': '欢迎使用 Coze Studio',
|
||
'workspace.greeting.late': '夜深了,注意休息 🌙',
|
||
'workspace.greeting.morning': '早上好 🌅',
|
||
'workspace.greeting.noon': '中午好 ☀️',
|
||
'workspace.greeting.afternoon': '下午好 🌤️',
|
||
'workspace.greeting.evening': '晚上好 🌆',
|
||
'workspace.card.basic.title': '基础配置',
|
||
'workspace.card.basic.desc': '配置系统基础参数和核心设置,确保系统正常运行。',
|
||
'workspace.card.basic.cta': '开始配置',
|
||
'workspace.card.model.title': '模型管理',
|
||
'workspace.card.model.desc': '管理AI模型配置、参数调优和模型版本控制。',
|
||
'workspace.card.model.cta': '管理模型',
|
||
'workspace.card.knowledge.title': '知识库配置',
|
||
'workspace.card.knowledge.desc': '配置知识库索引、向量存储和检索策略设置。',
|
||
'workspace.card.knowledge.cta': '配置知识库',
|
||
'workspace.footer.cta': '让我们开始构建您的智能助手吧!',
|
||
|
||
'basic.loading': '正在加载配置...',
|
||
'basic.label.admin_emails': '管理员邮箱',
|
||
'basic.placeholder.email_input': '输入邮箱,按回车添加',
|
||
'basic.placeholder.coze_api_token': '输入您的 Coze API Token',
|
||
'basic.help.coze_api_token_apply': '前往此链接申请 Coze API Token',
|
||
'basic.label.enable_coze_saas': '启用 Coze SaaS 插件',
|
||
'basic.label.disable_user_registration': '禁止用户注册',
|
||
'basic.label.whitelist_emails': '允许注册的白名单邮箱',
|
||
'basic.help.whitelist': '当禁止用户注册时,只有白名单中的邮箱才能注册',
|
||
'basic.label.python_env': 'Python 代码运行环境',
|
||
'basic.help.local_risk': 'Local 有被攻击的安全风险,生产环境慎用,建议使用 Sandbox 模式。',
|
||
'basic.sandbox.title': 'Sandbox 配置',
|
||
'basic.label.allow_env': '允许读取的环境变量',
|
||
'basic.label.allow_read': '允许读取的目录地址',
|
||
'basic.label.allow_write': '允许写的目录地址',
|
||
'basic.label.allow_run': '允许执行的命令',
|
||
'basic.label.allow_net': '允许访问的网络地址',
|
||
'basic.label.allow_ffi': '允许访问的 lib 库',
|
||
'basic.label.node_modules_dir': 'Node Modules 目录地址',
|
||
'basic.label.timeout_seconds': '代码执行超时时间(秒)',
|
||
'basic.label.memory_limit_mb': '内存限制(MB)',
|
||
|
||
'server.title': '服务器配置',
|
||
'server.host.label': '服务器主机地址',
|
||
'server.host.help': '格式:http(s)://主机名:端口号 , oauth 鉴权会用到',
|
||
|
||
'btn.save': '保存配置',
|
||
'btn.reload': '重新加载',
|
||
|
||
'error.invalid_email': '请输入有效的邮箱地址',
|
||
'error.email_exists': '该邮箱已存在',
|
||
'error.admin_email_required': '请至少设置一个管理员邮箱',
|
||
'error.server_host_required': '请填写服务器主机地址',
|
||
'error.invalid_admin_emails': '管理员邮箱存在无效地址:{list}',
|
||
'error.invalid_whitelist_emails': '白名单邮箱存在无效地址:{list}',
|
||
'error.save_failed': '保存失败',
|
||
'error.save_failed_json': '保存失败:请检查服务是否启动并返回有效 JSON',
|
||
'error.load_failed_empty': '加载失败:返回数据为空',
|
||
'error.load_failed_json': '加载失败:请检查服务是否启动并返回有效 JSON',
|
||
'success.saved': '保存成功',
|
||
'success.saved_restart_required': '保存成功,服务需要重启生效',
|
||
'overlay.saving': '正在保存配置...',
|
||
'error.missing_required': '请填写必填项:{list}',
|
||
'error.api_error': '接口返回错误',
|
||
'error.save_failed_reason': '保存失败: {reason}',
|
||
'page.manage_config': '管理和配置 {title}',
|
||
'page.empty': '页面内容为空',
|
||
'help.title': '帮助文档',
|
||
'help.quickstart.title': '🚀 快速开始',
|
||
'help.quickstart.step1': '首先配置基础设置,包括 API 密钥和系统参数',
|
||
'help.quickstart.step2': '添加和配置 AI 模型',
|
||
'help.quickstart.step3': '创建知识库并上传文档',
|
||
'help.quickstart.step4': '开始使用您的 AI 助手',
|
||
'help.features.title': '📚 功能说明',
|
||
'help.features.basic': '基础配置:系统设置、API 配置、安全设置',
|
||
'help.features.model': '模型管理:添加、编辑、删除 AI 模型',
|
||
'help.features.kb': '知识库:文档上传、索引管理、检索配置',
|
||
'help.support.title': '🔧 技术支持',
|
||
'help.support.contact': '如果您遇到问题,请联系技术支持团队',
|
||
'help.support.email': '📧 邮箱: {email}',
|
||
'help.support.phone': '📞 电话: {phone}',
|
||
'export.success': '配置文件已导出!',
|
||
'error.page_load_failed_reason': '页面加载失败: {reason}',
|
||
|
||
// 通用
|
||
'common.cancel': '取消',
|
||
'common.save': '保存',
|
||
'common.delete': '删除',
|
||
'common.enabled': '启用',
|
||
'common.disabled': '停用',
|
||
'common.on': '开启',
|
||
'common.off': '关闭',
|
||
'common.saving': '保存中...',
|
||
'common.not_configured': '未配置',
|
||
'common.unknown_error': '未知错误',
|
||
|
||
// 模型管理
|
||
'model.list.title': '模型列表',
|
||
'model.provider.count': '提供方数量:{count}',
|
||
'model.provider.model_count': '模型数量:{count}',
|
||
'model.actions.add': '添加模型',
|
||
'model.provider.unnamed': '未命名提供方',
|
||
'model.provider.unknown': '未知提供方',
|
||
'model.unnamed': '未命名模型',
|
||
|
||
'model.field.id': 'ID',
|
||
'model.field.model': 'Model',
|
||
'model.field.api_key': 'API Key',
|
||
'model.field.base64_url': 'Base64URL',
|
||
'model.field.region': 'Region',
|
||
'model.field.backend': 'Backend',
|
||
'model.field.project': 'Project',
|
||
'model.field.location': 'Location',
|
||
'model.field.azure': 'By Azure',
|
||
'model.field.api_version': 'API Version',
|
||
'model.field.endpoint': '接口地址',
|
||
|
||
'model.error.load_list_failed': '加载模型列表失败',
|
||
'model.error.render_page_failed': '渲染模型页面失败',
|
||
'model.error.delete_missing_id': '删除模型失败: 缺少模型ID',
|
||
'model.confirm.delete': '确认删除该模型?删除后不可恢复',
|
||
'model.error.delete_invalid_json': '删除模型失败: 返回结果不是有效 JSON',
|
||
'model.error.delete_failed': '删除模型失败',
|
||
'model.error.open_modal_failed': '无法打开添加模型表单:组件未渲染',
|
||
'model.error.provider_unrecognized': '无法识别模型提供方,请从提供方卡片点击“添加模型”。',
|
||
'model.error.missing_required_with_api': '请填写必填项:模型展示名称、model、api_key',
|
||
'model.error.missing_required': '请填写必填项:模型展示名称、model',
|
||
'model.error.create_read_failed': '创建模型失败: 无法读取响应内容',
|
||
'model.create.success': '创建成功',
|
||
'model.error.create_failed': '创建模型失败',
|
||
|
||
// 添加模型弹窗
|
||
'model.modal.add.title': '添加模型',
|
||
'model.modal.display_name.label': '模型显示名称',
|
||
'model.modal.display_name.placeholder': '例如:Doubao Seed 1.6',
|
||
'model.modal.model.label': 'Model',
|
||
'model.modal.model.placeholder': '例如:doubao-seed-1-6-250615',
|
||
'model.modal.api_key.label': 'API Key',
|
||
'model.modal.api_key.placeholder': '密钥',
|
||
'model.modal.base_url.label': 'Base URL',
|
||
'model.modal.base_url.placeholder': '选填',
|
||
'model.modal.enable_base64_url.label': '启用 Base64 URL',
|
||
'model.modal.enable_base64_url.help': '使用 base64 编码图片、文件、音频、视频 URL 给模型',
|
||
'model.modal.ark_region.label': 'Region(Ark)',
|
||
'model.modal.ark_region.placeholder': '选填,例如:cn-beijing',
|
||
'model.modal.azure.label': '使用 Azure',
|
||
'model.modal.azure.help': 'OpenAI 设置:勾选表示通过 Azure 使用',
|
||
'model.modal.openai_api_version.label': 'OpenAI API Version',
|
||
'model.modal.openai_api_version.placeholder': '选填,例如:2024-06-01',
|
||
'model.modal.gemini_backend.label': 'Backend',
|
||
'model.modal.gemini_backend.unspecified': '未指定',
|
||
'model.modal.gemini_backend.gemini_api': 'Gemini API',
|
||
'model.modal.gemini_backend.vertex_ai': 'Vertex AI',
|
||
'model.modal.gemini_project.label': 'Project',
|
||
'model.modal.gemini_project.placeholder': '选填,例如:my-project',
|
||
'model.modal.gemini_location.label': 'Location',
|
||
'model.modal.gemini_location.placeholder': '选填,例如:us-central1',
|
||
|
||
// 知识库配置
|
||
'knowledge.error.load_failed_json': '加载知识库配置失败: 返回结果不是有效 JSON',
|
||
'knowledge.error.load_failed_msg': '加载知识库配置失败: {msg}',
|
||
'knowledge.error.load_failed_empty': '加载知识库配置失败: 返回数据为空',
|
||
'knowledge.error.load_failed': '加载知识库配置失败',
|
||
'knowledge.error.load_failed_reason': '加载知识库配置失败: {reason}',
|
||
'knowledge.log.local_preview_no_demo': '本地预览:不使用示例数据',
|
||
'knowledge.section.embedding': 'Embedding 配置',
|
||
'knowledge.connection': '{name} 连接',
|
||
'knowledge.section.rerank': 'Rerank 配置',
|
||
'knowledge.section.ocr': 'OCR 配置',
|
||
'knowledge.section.parser': 'Parser 配置',
|
||
'knowledge.section.builtin_model': '知识库内置模型配置',
|
||
'knowledge.label.choose_builtin_model': '选择内置模型',
|
||
'knowledge.placeholder.select_model': '请选择模型',
|
||
'knowledge.loading.model_list': '正在加载模型列表...',
|
||
'knowledge.empty.model_list': '未获取到模型列表',
|
||
'knowledge.placeholder.model_fallback': '模型 #{id}',
|
||
'knowledge.tip.setup_model_first': '请先配置模型',
|
||
'knowledge.error.model_list_load_failed': '加载模型列表失败',
|
||
'knowledge.error.render_failed': '渲染知识库配置失败',
|
||
'knowledge.error.save_failed': '保存配置失败'
|
||
},
|
||
'en-US': {
|
||
'nav.workspace': 'Workspace',
|
||
'nav.config': 'Configuration',
|
||
'nav.basic': 'Basic Config',
|
||
'nav.model': 'Model Management',
|
||
'nav.knowledge': 'Knowledge Config',
|
||
|
||
'page.workspace.title': 'Workspace',
|
||
'page.workspace.desc': 'Workspace overview and quick actions',
|
||
'page.basic.title': 'Basic Config',
|
||
'page.basic.desc': 'System basic configuration',
|
||
'page.model.title': 'Model Management',
|
||
'page.model.desc': 'AI model configuration and management',
|
||
'page.knowledge.title': 'Knowledge Config',
|
||
'page.knowledge.desc': 'Knowledge base settings and management',
|
||
'page.default.title': 'Page',
|
||
'page.default.desc': 'Page Description',
|
||
'page.contentArea': 'This is the content area of {title}.',
|
||
|
||
'loading': 'Loading...',
|
||
'status.online': 'Online',
|
||
'error.workspace_forbidden': 'Cannot access workspace: ',
|
||
'error.generic': 'Error occurred',
|
||
'success.generic': 'Success',
|
||
|
||
'quick.title': 'Quick Actions',
|
||
'quick.new': 'New',
|
||
'quick.import': 'Import',
|
||
'quick.export': 'Export',
|
||
'quick.settings': 'Settings',
|
||
|
||
'feature.module1': 'Feature Module 1',
|
||
'feature.module2': 'Feature Module 2',
|
||
'feature.module3': 'Feature Module 3',
|
||
'feature.desc': 'Feature description and actions',
|
||
|
||
'user.admin': 'Admin',
|
||
'workspace.hero.title': 'Welcome to Coze Studio',
|
||
'workspace.greeting.late': 'It’s late, rest well 🌙',
|
||
'workspace.greeting.morning': 'Good morning 🌅',
|
||
'workspace.greeting.noon': 'Good noon ☀️',
|
||
'workspace.greeting.afternoon': 'Good afternoon 🌤️',
|
||
'workspace.greeting.evening': 'Good evening 🌆',
|
||
'workspace.card.basic.title': 'Basic Config',
|
||
'workspace.card.basic.desc': 'Configure system parameters and core settings to ensure proper operation.',
|
||
'workspace.card.basic.cta': 'Start Configuring',
|
||
'workspace.card.model.title': 'Model Management',
|
||
'workspace.card.model.desc': 'Manage AI model configs, tuning, and versioning.',
|
||
'workspace.card.model.cta': 'Manage Models',
|
||
'workspace.card.knowledge.title': 'Knowledge Config',
|
||
'workspace.card.knowledge.desc': 'Configure indexing, vector storage, and retrieval strategies.',
|
||
'workspace.card.knowledge.cta': 'Configure Knowledge',
|
||
'workspace.footer.cta': 'Let’s start building your AI assistant!',
|
||
|
||
'basic.loading': 'Loading configuration...',
|
||
'basic.label.admin_emails': 'Admin Emails',
|
||
'basic.placeholder.email_input': 'Type email, press Enter to add',
|
||
'basic.placeholder.coze_api_token': 'Enter your Coze API Token',
|
||
'basic.help.coze_api_token_apply': 'Apply for a Coze API Token here',
|
||
'basic.label.enable_coze_saas': 'Enable Coze SaaS plugin',
|
||
'basic.label.disable_user_registration': 'Disable user registration',
|
||
'basic.label.whitelist_emails': 'Whitelist emails allowed to register',
|
||
'basic.help.whitelist': 'When registration is disabled, only whitelisted emails can register.',
|
||
'basic.label.python_env': 'Python Execution Environment',
|
||
'basic.help.local_risk': 'Local has security risks; use Sandbox in production.',
|
||
'basic.sandbox.title': 'Sandbox Config',
|
||
'basic.label.allow_env': 'Allowed environment variables',
|
||
'basic.label.allow_read': 'Readable directories',
|
||
'basic.label.allow_write': 'Writable directories',
|
||
'basic.label.allow_run': 'Allowed commands',
|
||
'basic.label.allow_net': 'Allowed network addresses',
|
||
'basic.label.allow_ffi': 'Allowed lib (FFI)',
|
||
'basic.label.node_modules_dir': 'Node Modules directory',
|
||
'basic.label.timeout_seconds': 'Code timeout (seconds)',
|
||
'basic.label.memory_limit_mb': 'Memory limit (MB)',
|
||
|
||
'server.title': 'Server Configuration',
|
||
'server.host.label': 'Server host address',
|
||
'server.host.help': 'Format: http(s)://hostname:port , used by oauth auth',
|
||
|
||
'btn.save': 'Save Config',
|
||
'btn.reload': 'Reload',
|
||
|
||
'error.invalid_email': 'Please enter a valid email address',
|
||
'error.email_exists': 'This email already exists',
|
||
'error.admin_email_required': 'Please set at least one admin email',
|
||
'error.server_host_required': 'Please provide the server host address',
|
||
'error.invalid_admin_emails': 'Invalid admin emails: {list}',
|
||
'error.invalid_whitelist_emails': 'Invalid whitelist emails: {list}',
|
||
'error.save_failed': 'Save failed',
|
||
'error.save_failed_json': 'Save failed: check the service and JSON response',
|
||
'error.load_failed_empty': 'Load failed: empty response data',
|
||
'error.load_failed_json': 'Load failed: check the service and JSON response',
|
||
'success.saved': 'Saved successfully',
|
||
'success.saved_restart_required': 'Saved successfully. Restart the service to take effect',
|
||
'overlay.saving': 'Saving configuration...',
|
||
'error.missing_required': 'Please fill required fields: {list}',
|
||
'error.api_error': 'API returned error',
|
||
'error.save_failed_reason': 'Save failed: {reason}',
|
||
'page.manage_config': 'Manage and configure {title}',
|
||
'page.empty': 'Page content is empty',
|
||
'help.title': 'Help',
|
||
'help.quickstart.title': '🚀 Quick Start',
|
||
'help.quickstart.step1': 'Configure basic settings including API keys and system parameters',
|
||
'help.quickstart.step2': 'Add and configure AI models',
|
||
'help.quickstart.step3': 'Create a knowledge base and upload documents',
|
||
'help.quickstart.step4': 'Start using your AI assistant',
|
||
'help.features.title': '📚 Feature Overview',
|
||
'help.features.basic': 'Basic Config: system settings, API config, security settings',
|
||
'help.features.model': 'Model Management: add, edit, and delete AI models',
|
||
'help.features.kb': 'Knowledge Base: document upload, indexing, retrieval config',
|
||
'help.support.title': '🔧 Technical Support',
|
||
'help.support.contact': 'If you encounter issues, contact the support team',
|
||
'help.support.email': '📧 Email: {email}',
|
||
'help.support.phone': '📞 Phone: {phone}',
|
||
'export.success': 'Configuration file exported!',
|
||
'error.page_load_failed_reason': 'Page load failed: {reason}',
|
||
|
||
// Common
|
||
'common.cancel': 'Cancel',
|
||
'common.save': 'Save',
|
||
'common.delete': 'Delete',
|
||
'common.enabled': 'Enabled',
|
||
'common.disabled': 'Disabled',
|
||
'common.on': 'On',
|
||
'common.off': 'Off',
|
||
'common.saving': 'Saving...',
|
||
'common.not_configured': 'Not configured',
|
||
'common.unknown_error': 'Unknown error',
|
||
|
||
// Model Management
|
||
'model.list.title': 'Model List',
|
||
'model.provider.count': 'Providers: {count}',
|
||
'model.provider.model_count': 'Model count: {count}',
|
||
'model.actions.add': 'Add Model',
|
||
'model.provider.unnamed': 'Unnamed provider',
|
||
'model.provider.unknown': 'Unknown provider',
|
||
'model.unnamed': 'Unnamed model',
|
||
|
||
'model.field.id': 'ID',
|
||
'model.field.model': 'Model',
|
||
'model.field.api_key': 'API Key',
|
||
'model.field.base64_url': 'Base64URL',
|
||
'model.field.region': 'Region',
|
||
'model.field.backend': 'Backend',
|
||
'model.field.project': 'Project',
|
||
'model.field.location': 'Location',
|
||
'model.field.azure': 'By Azure',
|
||
'model.field.api_version': 'API Version',
|
||
'model.field.endpoint': 'Endpoint',
|
||
|
||
'model.error.load_list_failed': 'Failed to load model list',
|
||
'model.error.render_page_failed': 'Failed to render model page',
|
||
'model.error.delete_missing_id': 'Delete model failed: missing model ID',
|
||
'model.confirm.delete': 'Delete this model? This cannot be undone',
|
||
'model.error.delete_invalid_json': 'Delete model failed: invalid JSON response',
|
||
'model.error.delete_failed': 'Delete model failed',
|
||
'model.error.open_modal_failed': 'Cannot open add-model form: component not rendered',
|
||
'model.error.provider_unrecognized': 'Unrecognized provider; click “Add Model” on a provider card.',
|
||
'model.error.missing_required_with_api': 'Please fill required: display name, model, api_key',
|
||
'model.error.missing_required': 'Please fill required: display name, model',
|
||
'model.error.create_read_failed': 'Create model failed: cannot read response',
|
||
'model.create.success': 'Created successfully',
|
||
'model.error.create_failed': 'Create model failed',
|
||
|
||
// Add Model Modal
|
||
'model.modal.add.title': 'Add Model',
|
||
'model.modal.display_name.label': 'Display Name',
|
||
'model.modal.display_name.placeholder': 'e.g., Doubao Seed 1.6',
|
||
'model.modal.model.label': 'Model',
|
||
'model.modal.model.placeholder': 'e.g., doubao-seed-1-6-250615',
|
||
'model.modal.api_key.label': 'API Key',
|
||
'model.modal.api_key.placeholder': 'Secret',
|
||
'model.modal.base_url.label': 'Base URL',
|
||
'model.modal.base_url.placeholder': 'Optional',
|
||
'model.modal.enable_base64_url.label': 'Enable Base64 URL',
|
||
'model.modal.enable_base64_url.help': 'Use base64-encoded URLs for images, files, audio, video',
|
||
'model.modal.ark_region.label': 'Region (Ark)',
|
||
'model.modal.ark_region.placeholder': 'Optional, e.g., cn-beijing',
|
||
'model.modal.azure.label': 'Use Azure',
|
||
'model.modal.azure.help': 'OpenAI settings: check to use via Azure',
|
||
'model.modal.openai_api_version.label': 'OpenAI API Version',
|
||
'model.modal.openai_api_version.placeholder': 'Optional, e.g., 2024-06-01',
|
||
'model.modal.gemini_backend.label': 'Backend',
|
||
'model.modal.gemini_backend.unspecified': 'Unspecified',
|
||
'model.modal.gemini_backend.gemini_api': 'Gemini API',
|
||
'model.modal.gemini_backend.vertex_ai': 'Vertex AI',
|
||
'model.modal.gemini_project.label': 'Project',
|
||
'model.modal.gemini_project.placeholder': 'Optional, e.g., my-project',
|
||
'model.modal.gemini_location.label': 'Location',
|
||
'model.modal.gemini_location.placeholder': 'Optional, e.g., us-central1',
|
||
|
||
// Knowledge Config
|
||
'knowledge.error.load_failed_json': 'Load knowledge config failed: invalid JSON response',
|
||
'knowledge.error.load_failed_msg': 'Load knowledge config failed: {msg}',
|
||
'knowledge.error.load_failed_empty': 'Load knowledge config failed: empty response data',
|
||
'knowledge.error.load_failed': 'Load knowledge config failed',
|
||
'knowledge.error.load_failed_reason': 'Load knowledge config failed: {reason}',
|
||
'knowledge.log.local_preview_no_demo': 'Local preview: no demo data used',
|
||
'knowledge.section.embedding': 'Embedding Config',
|
||
'knowledge.connection': '{name} Connection',
|
||
'knowledge.section.rerank': 'Rerank Config',
|
||
'knowledge.section.ocr': 'OCR Config',
|
||
'knowledge.section.parser': 'Parser Config',
|
||
'knowledge.section.builtin_model': 'Knowledge Built-in Model Config',
|
||
'knowledge.label.choose_builtin_model': 'Choose Built-in Model',
|
||
'knowledge.placeholder.select_model': 'Please select a model',
|
||
'knowledge.loading.model_list': 'Loading model list...',
|
||
'knowledge.empty.model_list': 'No model list retrieved',
|
||
'knowledge.placeholder.model_fallback': 'Model #{id}',
|
||
'knowledge.tip.setup_model_first': 'Please configure models first',
|
||
'knowledge.error.model_list_load_failed': 'Failed to load model list',
|
||
'knowledge.error.render_failed': 'Render knowledge config failed',
|
||
'knowledge.error.save_failed': 'Failed to save config'
|
||
},
|
||
};
|
||
|
||
let currentLocale = 'zh-CN';
|
||
function t(key, vars) {
|
||
const dict = I18N[currentLocale] || I18N['zh-CN'];
|
||
let text = dict[key] || I18N['zh-CN'][key] || key;
|
||
if (vars && typeof vars === 'object') {
|
||
text = String(text).replace(/\{(\w+)\}/g, (_, k) => (vars[k] ?? `{${k}}`));
|
||
}
|
||
return text;
|
||
}
|
||
function setLocale(locale) {
|
||
const normalized = (locale === 'en-US' || locale === 'zh-CN') ? locale : (locale && String(locale).toLowerCase().startsWith('en') ? 'en-US' : 'zh-CN');
|
||
currentLocale = normalized;
|
||
try { localStorage.setItem('locale', normalized); } catch(_) {}
|
||
document.documentElement.lang = normalized;
|
||
applyTranslations();
|
||
}
|
||
function setNavLinkText(link, text) {
|
||
if (!link) return;
|
||
const icon = link.querySelector('.nav-icon');
|
||
const arrow = link.querySelector('.nav-arrow');
|
||
if (icon || arrow) {
|
||
const iconHTML = icon ? icon.outerHTML + ' ' : '';
|
||
const arrowHTML = arrow ? ' ' + arrow.outerHTML : '';
|
||
link.innerHTML = iconHTML + (text || '') + arrowHTML;
|
||
} else {
|
||
link.textContent = text || '';
|
||
}
|
||
}
|
||
function applyTranslations() {
|
||
// 左侧导航
|
||
setNavLinkText(document.querySelector('[data-page="workspace"]'), t('nav.workspace'));
|
||
setNavLinkText(document.querySelector('[data-page="config"]'), t('nav.config'));
|
||
const basicLink = document.querySelector('[data-page="basic-config"]');
|
||
if (basicLink) basicLink.innerHTML = '<span class="submenu-icon">🔧</span>' + t('nav.basic');
|
||
const modelLink = document.querySelector('[data-page="model-management"]');
|
||
if (modelLink) modelLink.innerHTML = '<span class="submenu-icon">🤖</span>' + t('nav.model');
|
||
const knowledgeLink = document.querySelector('[data-page="knowledge-config"]');
|
||
if (knowledgeLink) knowledgeLink.innerHTML = '<span class="submenu-icon">📚</span>' + t('nav.knowledge');
|
||
|
||
// 顶部标题(根据当前 hash)
|
||
const hash = window.location.hash.substring(1) || 'workspace';
|
||
const map = {
|
||
'workspace': { title: t('page.workspace.title'), desc: t('page.workspace.desc') },
|
||
'basic-config': { title: t('page.basic.title'), desc: t('page.basic.desc') },
|
||
'model-management': { title: t('page.model.title'), desc: t('page.model.desc') },
|
||
'knowledge-config': { title: t('page.knowledge.title'), desc: t('page.knowledge.desc') },
|
||
};
|
||
const meta = map[hash] || { title: t('page.default.title'), desc: t('page.default.desc') };
|
||
const pt = document.getElementById('pageTitle');
|
||
const pd = document.getElementById('pageDescription');
|
||
if (pt) pt.textContent = meta.title;
|
||
if (pd) pd.textContent = meta.desc;
|
||
|
||
// 侧边栏在线状态
|
||
const us = document.getElementById('userStatus');
|
||
if (us) us.textContent = t('status.online');
|
||
|
||
// 初始 loading 文案
|
||
const loadingSpan = document.querySelector('#pageContent .loading span');
|
||
if (loadingSpan) loadingSpan.textContent = t('loading');
|
||
}
|
||
|
||
// 页面导航功能
|
||
const navLinks = document.querySelectorAll('.nav-link');
|
||
const submenuLinks = document.querySelectorAll('.submenu-link');
|
||
const pageTitle = document.getElementById('pageTitle');
|
||
const pageDescription = document.getElementById('pageDescription');
|
||
const pageContent = document.getElementById('pageContent');
|
||
|
||
function handleApiCode(data) {
|
||
const code = Number(data?.code);
|
||
if (code === 700012006) {
|
||
window.location.href = '/sign?redirect=%2Fadmin';
|
||
return true;
|
||
}
|
||
if (code === 401) {
|
||
const msg = data?.msg || data?.message || t('error.generic');
|
||
alert(msg);
|
||
window.location.href = '/';
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 页面初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 初始化语言(优先使用本地存储,否则使用浏览器语言)
|
||
const storedLocale = (() => { try { return localStorage.getItem('locale'); } catch(_) { return null; } })();
|
||
setLocale(storedLocale || (navigator.language && navigator.language.toLowerCase().startsWith('en') ? 'en-US' : 'zh-CN'));
|
||
// 加载用户信息
|
||
loadUserInfo();
|
||
|
||
// 初始化子菜单功能
|
||
initSubmenuToggle();
|
||
|
||
// 默认展开配置管理子菜单
|
||
const configMenu = document.querySelector('[data-page="config"]');
|
||
if (configMenu) {
|
||
const navItem = configMenu.closest('.nav-item');
|
||
if (navItem) {
|
||
navItem.classList.add('expanded');
|
||
}
|
||
}
|
||
|
||
// 从URL hash获取当前页面,如果没有则显示默认页面
|
||
initPageFromURL();
|
||
});
|
||
|
||
// 从URL初始化页面
|
||
function initPageFromURL() {
|
||
const hash = window.location.hash.substring(1); // 去掉#号
|
||
if (hash) {
|
||
// 如果URL中有hash,加载对应页面
|
||
loadPageFromHash(hash);
|
||
} else {
|
||
// 如果没有hash,显示默认页面
|
||
showDefaultPage();
|
||
}
|
||
}
|
||
|
||
// 从hash加载页面
|
||
function loadPageFromHash(hash) {
|
||
const pageConfig = {
|
||
'workspace': { title: t('page.workspace.title'), description: t('page.workspace.desc') },
|
||
'basic-config': { title: t('page.basic.title'), description: t('page.basic.desc') },
|
||
'model-management': { title: t('page.model.title'), description: t('page.model.desc') },
|
||
'knowledge-config': { title: t('page.knowledge.title'), description: t('page.knowledge.desc') }
|
||
};
|
||
|
||
if (pageConfig[hash]) {
|
||
// 设置对应的导航项为活动状态
|
||
const targetLink = document.querySelector(`[data-page="${hash}"]`);
|
||
if (targetLink) {
|
||
// 移除所有活动状态
|
||
navLinks.forEach(l => l.classList.remove('active'));
|
||
submenuLinks.forEach(l => l.classList.remove('active'));
|
||
|
||
// 添加当前活动状态
|
||
targetLink.classList.add('active');
|
||
|
||
// 如果是子菜单项,确保父菜单展开
|
||
if (targetLink.classList.contains('submenu-link')) {
|
||
const parentNavItem = targetLink.closest('.nav-item');
|
||
if (parentNavItem) {
|
||
parentNavItem.classList.add('expanded');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 加载页面内容
|
||
loadPageContent(hash);
|
||
} else {
|
||
// 如果hash不存在,显示默认页面
|
||
showDefaultPage();
|
||
}
|
||
}
|
||
|
||
// 加载用户信息
|
||
async function loadUserInfo() {
|
||
try {
|
||
// 从当前页面URL获取host
|
||
const currentHost = window.location.origin;
|
||
const apiUrl = `${currentHost}/api/passport/account/info/v2/`;
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({})
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (handleApiCode(result)) {
|
||
return;
|
||
}
|
||
|
||
if (result.code === 0 && result.data) {
|
||
// 应用语言
|
||
const apiLocale = result.data.locale;
|
||
const storedLocale = (() => { try { return localStorage.getItem('locale'); } catch(_) { return null; } })();
|
||
setLocale(apiLocale || storedLocale || (navigator.language && navigator.language.toLowerCase().startsWith('en') ? 'en-US' : 'zh-CN'));
|
||
|
||
// 更新用户名
|
||
const userNameElement = document.getElementById('userName');
|
||
if (userNameElement && result.data.name) {
|
||
userNameElement.textContent = result.data.name;
|
||
}
|
||
|
||
// 更新用户头像
|
||
const userAvatarElement = document.getElementById('userAvatar');
|
||
if (userAvatarElement && result.data.avatar_url) {
|
||
// 创建img元素替换原有的emoji
|
||
const img = document.createElement('img');
|
||
img.src = result.data.avatar_url;
|
||
img.alt = result.data.name || '用户头像';
|
||
img.style.width = '100%';
|
||
img.style.height = '100%';
|
||
img.style.borderRadius = '50%';
|
||
img.style.objectFit = 'cover';
|
||
|
||
// 添加错误处理,如果图片加载失败则显示默认图标
|
||
img.onerror = function() {
|
||
userAvatarElement.innerHTML = '👤';
|
||
};
|
||
|
||
userAvatarElement.innerHTML = '';
|
||
userAvatarElement.appendChild(img);
|
||
}
|
||
} else {
|
||
console.warn('用户信息接口返回异常:', result);
|
||
// 接口异常时也设置一次语言(兜底)
|
||
const storedLocale = (() => { try { return localStorage.getItem('locale'); } catch(_) { return null; } })();
|
||
setLocale(storedLocale || (navigator.language && navigator.language.toLowerCase().startsWith('en') ? 'en-US' : 'zh-CN'));
|
||
}
|
||
} catch (error) {
|
||
console.error('加载用户信息失败:', error);
|
||
// 保持默认显示
|
||
}
|
||
}
|
||
|
||
// 初始化子菜单切换功能
|
||
function initSubmenuToggle() {
|
||
const hasSubmenuItems = document.querySelectorAll('.has-submenu');
|
||
|
||
hasSubmenuItems.forEach(item => {
|
||
const navLink = item.querySelector('.nav-link');
|
||
navLink.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
|
||
// 切换展开状态
|
||
item.classList.toggle('expanded');
|
||
});
|
||
});
|
||
}
|
||
|
||
// 导航点击事件
|
||
navLinks.forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
const parentItem = link.closest('.nav-item');
|
||
|
||
// 如果不是子菜单项,处理页面跳转
|
||
if (!parentItem.classList.contains('has-submenu')) {
|
||
e.preventDefault();
|
||
|
||
// 移除所有活动状态
|
||
navLinks.forEach(l => l.classList.remove('active'));
|
||
submenuLinks.forEach(l => l.classList.remove('active'));
|
||
|
||
// 添加当前活动状态
|
||
link.classList.add('active');
|
||
|
||
const pageName = link.getAttribute('data-page');
|
||
// 更新URL hash
|
||
window.location.hash = pageName;
|
||
loadPageContent(pageName);
|
||
}
|
||
});
|
||
});
|
||
|
||
// 子菜单点击事件
|
||
submenuLinks.forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
|
||
// 移除所有活动状态
|
||
navLinks.forEach(l => l.classList.remove('active'));
|
||
submenuLinks.forEach(l => l.classList.remove('active'));
|
||
|
||
// 添加当前活动状态
|
||
link.classList.add('active');
|
||
|
||
const pageName = link.getAttribute('data-page');
|
||
// 更新URL hash
|
||
window.location.hash = pageName;
|
||
loadPageContent(pageName);
|
||
});
|
||
});
|
||
|
||
// 加载页面内容
|
||
function loadPageContent(pageName) {
|
||
const pageConfig = {
|
||
'workspace': { title: t('page.workspace.title'), description: t('page.workspace.desc') },
|
||
'basic-config': { title: t('page.basic.title'), description: t('page.basic.desc') },
|
||
'model-management': { title: t('page.model.title'), description: t('page.model.desc') },
|
||
'knowledge-config': { title: t('page.knowledge.title'), description: t('page.knowledge.desc') }
|
||
};
|
||
|
||
const config = pageConfig[pageName] || { title: t('page.default.title'), description: t('page.default.desc') };
|
||
|
||
pageTitle.textContent = config.title;
|
||
pageDescription.textContent = config.description;
|
||
|
||
// 显示页面内容
|
||
showPageContent(pageName, config.title);
|
||
}
|
||
|
||
// 显示默认页面
|
||
function showDefaultPage() {
|
||
// 设置工作台为选中状态
|
||
const workspaceLink = document.querySelector('[data-page="workspace"]');
|
||
if (workspaceLink) {
|
||
// 移除所有活动状态
|
||
navLinks.forEach(l => l.classList.remove('active'));
|
||
submenuLinks.forEach(l => l.classList.remove('active'));
|
||
|
||
// 添加工作台的活动状态
|
||
workspaceLink.classList.add('active');
|
||
}
|
||
|
||
// 设置默认页面的URL hash
|
||
window.location.hash = 'workspace';
|
||
loadPageContent('workspace');
|
||
}
|
||
|
||
// 显示页面内容
|
||
function showPageContent(pageName, title) {
|
||
// 如果是工作台页面,先调用权限检查接口
|
||
if (pageName === 'workspace') {
|
||
fetch('/api/admin/config/basic/get')
|
||
.then(resp => resp.json())
|
||
.then(data => {
|
||
if (handleApiCode(data)) {
|
||
return;
|
||
}
|
||
if (Number(data?.code) === 0) {
|
||
showWorkspaceContent();
|
||
return;
|
||
}
|
||
showError(t('error.workspace_forbidden') + (data?.msg ? String(data.msg) : ''));
|
||
})
|
||
.catch(err => {
|
||
console.error('Workspace permission check failed:', err);
|
||
showError(t('error.workspace_forbidden') + (err?.message ? String(err.message) : ''));
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 如果是基础配置页面,加载config.html的内容
|
||
if (pageName === 'basic-config') {
|
||
loadBasicConfigPage();
|
||
return;
|
||
}
|
||
|
||
// 如果是模型管理页面,加载模型列表
|
||
if (pageName === 'model-management') {
|
||
loadModelManagementPage();
|
||
return;
|
||
}
|
||
|
||
// 如果是知识库配置页面,加载知识库配置
|
||
if (pageName === 'knowledge-config') {
|
||
loadKnowledgeConfigPage();
|
||
return;
|
||
}
|
||
|
||
pageContent.innerHTML = `
|
||
<div style="padding: 30px;">
|
||
<div style="background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||
<h2 style="color: #2c3e50; margin-bottom: 20px;">${title}</h2>
|
||
<p style="color: #7f8c8d; margin-bottom: 30px;">${t('page.contentArea', { title })}</p>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px;">
|
||
<div style="background: #f8f9fa; padding: 20px; border-radius: 6px; border-left: 4px solid #3498db;">
|
||
<h4 style="color: #2c3e50; margin: 0 0 10px;">${t('feature.module1')}</h4>
|
||
<p style="color: #7f8c8d; margin: 0; font-size: 14px;">${t('feature.desc')}</p>
|
||
</div>
|
||
<div style="background: #f8f9fa; padding: 20px; border-radius: 6px; border-left: 4px solid #e74c3c;">
|
||
<h4 style="color: #2c3e50; margin: 0 0 10px;">${t('feature.module2')}</h4>
|
||
<p style="color: #7f8c8d; margin: 0; font-size: 14px;">${t('feature.desc')}</p>
|
||
</div>
|
||
<div style="background: #f8f9fa; padding: 20px; border-radius: 6px; border-left: 4px solid #f39c12;">
|
||
<h4 style="color: #2c3e50; margin: 0 0 10px;">${t('feature.module3')}</h4>
|
||
<p style="color: #7f8c8d; margin: 0; font-size: 14px;">${t('feature.desc')}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 30px; padding: 20px; background: #ecf0f1; border-radius: 6px;">
|
||
<h4 style="color: #2c3e50; margin: 0 0 15px;">${t('quick.title')}</h4>
|
||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||
<button style="background: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px;">${t('quick.new')}</button>
|
||
<button style="background: #2ecc71; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px;">${t('quick.import')}</button>
|
||
<button style="background: #f39c12; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px;">${t('quick.export')}</button>
|
||
<button style="background: #95a5a6; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px;">${t('quick.settings')}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 加载基础配置页面(已内联到 index.html)
|
||
function loadBasicConfigPage() {
|
||
ensureConfigStylesInjected();
|
||
pageContent.innerHTML = getBasicConfigMarkup();
|
||
// 绑定弹窗函数到全局浮层
|
||
overrideConfigOverlay();
|
||
// 初始化交互与数据
|
||
setupConfigEventListeners();
|
||
initConfigEmailTags();
|
||
setTimeout(() => {
|
||
if (typeof window.loadConfig === 'function') {
|
||
window.loadConfig();
|
||
}
|
||
}, 0);
|
||
}
|
||
|
||
// 将配置页的弹窗绑定到 index.html 的全局浮层(幂等且无递归)
|
||
function overrideConfigOverlay() {
|
||
if (window.__overlayBound) return;
|
||
const baseShow = (typeof window.showOverlay === 'function') ? window.showOverlay.bind(window) : function(message, type){
|
||
// 兜底:如果原函数不存在,直接调用本地实现
|
||
const overlay = document.getElementById('overlay');
|
||
const overlayContent = document.getElementById('overlayContent');
|
||
const overlayMessage = document.getElementById('overlayMessage');
|
||
if (!overlay || !overlayContent || !overlayMessage) { alert(String(message || '未知错误')); return; }
|
||
overlayMessage.textContent = String(message || '');
|
||
overlayContent.className = `overlay-content ${type === 'success' ? 'success' : 'error'}`;
|
||
overlay.classList.add('show');
|
||
overlay.onclick = function(e){ if (e.target === overlay) hideOverlay(); };
|
||
setTimeout(() => hideOverlay(), 3000);
|
||
};
|
||
const baseHide = (typeof window.hideOverlay === 'function') ? window.hideOverlay.bind(window) : function(){
|
||
const overlay = document.getElementById('overlay');
|
||
if (overlay) overlay.classList.remove('show');
|
||
};
|
||
window.showOverlay = function(message, type) { baseShow(message, type); };
|
||
window.hideOverlay = function() { baseHide(); };
|
||
window.showError = function(message) { baseShow(message || t('error.generic'), 'error'); };
|
||
window.showSuccess = function(message) { baseShow(message || t('success.generic'), 'success'); };
|
||
window.__overlayBound = true;
|
||
}
|
||
|
||
// 注入基础配置页 CSS(移除 overlay 相关样式)
|
||
const CONFIG_PAGE_CSS = `
|
||
.page-content { max-width: 1000px; margin: 0 auto; padding: 30px; }
|
||
.page-header { background: white; border-radius: 12px; padding: 30px; margin-bottom: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); border-left: 4px solid #3498db; }
|
||
.page-title { font-size: 28px; font-weight: 600; color: #2c3e50; margin-bottom: 8px; }
|
||
.page-subtitle { font-size: 16px; color: #6c757d; font-weight: 400; }
|
||
.config-section { background: white; border-radius: 12px; padding: 30px; margin-bottom: 24px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); border: 1px solid #e1e8ed; transition: all 0.3s ease; }
|
||
.config-section:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
|
||
.section-title { font-size: 20px; font-weight: 600; color: #2c3e50; margin-bottom: 24px; display: flex; align-items: center; padding-bottom: 12px; border-bottom: 2px solid #f1f3f4; }
|
||
.section-icon { margin-right: 12px; font-size: 20px; color: #3498db; }
|
||
.form-group { margin-bottom: 24px; position: relative; }
|
||
.form-label { display: block; font-weight: 500; color: #374151; margin-bottom: 8px; font-size: 14px; }
|
||
.form-input { width: 100%; padding: 12px 16px; border: 2px solid #e1e8ed; border-radius: 8px; font-size: 14px; transition: all 0.3s ease; background: white; box-sizing: border-box; font-family: inherit; }
|
||
.form-input:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); }
|
||
.form-input:hover:not(:focus) { border-color: #bdc3c7; }
|
||
.form-textarea { min-height: 100px; resize: vertical; font-family: inherit; }
|
||
.form-select { appearance: none; background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m6 8 4 4 4-4'/%3e%3c/svg%3e"); background-position: right 12px center; background-repeat: no-repeat; background-size: 16px; padding-right: 40px; cursor: pointer; }
|
||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||
.checkbox-group { display: flex; align-items: center; margin-bottom: 16px; padding: 12px 16px; background: #f8f9fa; border-radius: 8px; border: 2px solid #e1e8ed; transition: all 0.3s ease; }
|
||
.checkbox-group:hover { background: #f1f3f4; border-color: #bdc3c7; }
|
||
.checkbox-input { margin-right: 12px; width: 18px; height: 18px; accent-color: #3498db; cursor: pointer; }
|
||
.checkbox-group label { font-weight: 500; color: #374151; cursor: pointer; font-size: 14px; }
|
||
.radio-group { display: flex; gap: 16px; margin-top: 8px; }
|
||
.radio-item { display: flex; align-items: center; padding: 10px 16px; background: white; border-radius: 8px; border: 2px solid #e1e8ed; transition: all 0.3s ease; cursor: pointer; }
|
||
.radio-item:hover { background: #f8f9fa; border-color: #3498db; }
|
||
.radio-item:has(input:checked) { background: #e8f4fd; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); }
|
||
.radio-input { margin-right: 8px; width: 18px; height: 18px; accent-color: #3498db; cursor: pointer; }
|
||
.radio-item label { font-weight: 500; color: #374151; cursor: pointer; font-size: 14px; }
|
||
.btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; margin-right: 12px; display: inline-flex; align-items: center; gap: 6px; text-decoration: none; font-family: inherit; }
|
||
.btn-primary { background: #3498db; color: white; box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3); }
|
||
.btn-primary:hover { background: #2980b9; box-shadow: 0 4px 12px rgba(52, 152, 219, 0.4); }
|
||
.btn-primary:active { transform: translateY(1px); box-shadow: 0 1px 4px rgba(52, 152, 219, 0.3); }
|
||
.btn-secondary { background: #f8f9fa; color: #6c757d; border: 2px solid #e1e8ed; }
|
||
.btn-secondary:hover { background: #e9ecef; border-color: #bdc3c7; color: #495057; }
|
||
.registration-config-group { background: #f8f9fa; border-radius: 12px; padding: 20px; border: 2px solid #e1e8ed; margin-bottom: 20px; position: relative; }
|
||
.registration-config-group::before { content: '👥'; position: absolute; top: -12px; left: 20px; background: white; padding: 0 8px; font-size: 16px; color: #3498db; }
|
||
.registration-config-group .checkbox-group { margin-bottom: 16px; background: white; border: 2px solid #3498db; box-shadow: 0 2px 8px rgba(52, 152, 219, 0.1); }
|
||
.plugin-config-group::before { content: '🧩'; position: absolute; top: -12px; left: 20px; background: white; padding: 0 8px; font-size: 16px; color: #3498db; }
|
||
.whitelist-email-config { margin-left: 30px; padding-left: 20px; border-left: 3px solid #3498db; background: white; border-radius: 8px; padding: 16px 20px; margin-bottom: 0; transition: all 0.3s ease; }
|
||
.whitelist-email-config.show { animation: slideDown 0.3s ease-out; }
|
||
.email-tags-container { border: 2px solid #e1e8ed; border-radius: 8px; padding: 8px; background: white; min-height: 48px; display: flex; flex-wrap: wrap; align-items: flex-start; gap: 6px; cursor: text; transition: border-color 0.3s ease; }
|
||
.email-tags-container:focus-within { border-color: #3498db; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1); }
|
||
.email-tags { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||
.email-tag { background: #3498db; color: white; padding: 4px 8px; border-radius: 16px; font-size: 12px; display: inline-flex; align-items: center; gap: 4px; max-width: 200px; overflow: hidden; }
|
||
.email-tag-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.email-tag-remove { background: none; border: none; color: white; cursor: pointer; padding: 0; width: 16px; height: 16px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; line-height: 1; transition: background-color 0.2s ease; }
|
||
.email-tag-remove:hover { background: rgba(255, 255, 255, 0.2); }
|
||
.email-input { border: none !important; box-shadow: none !important; padding: 4px 0 !important; margin: 0 !important; flex: 1; min-width: 200px; background: transparent !important; }
|
||
.email-input:focus { outline: none !important; border: none !important; box-shadow: none !important; }
|
||
.help-text { font-size: 12px; color: #9ca3af; margin-top: 6px; font-style: italic; }
|
||
.sandbox-config { display: none; margin-top: 20px; padding: 20px; background: #f8f9fa; border-radius: 8px; border: 2px solid #e1e8ed; position: relative; }
|
||
.sandbox-config::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: #3498db; border-radius: 8px 8px 0 0; }
|
||
.sandbox-config.show { display: block; animation: slideDown 0.3s ease-out; }
|
||
@keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
|
||
.sandbox-config h4 { margin-bottom: 16px; color: #2c3e50; font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
||
.sandbox-config h4::before { content: '🔧'; font-size: 16px; }
|
||
.loading { text-align: center; padding: 40px; color: #6c757d; background: white; border-radius: 12px; margin: 20px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.08); }
|
||
.loading::before { content: '⚙️'; font-size: 32px; display: block; margin-bottom: 12px; animation: spin 2s linear infinite; }
|
||
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||
.error { background: #fee2e2; color: #dc2626; padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #dc2626; font-weight: 500; }
|
||
.success { background: #dcfce7; color: #16a34a; padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #16a34a; font-weight: 500; }
|
||
.action-buttons { margin-top: 30px; padding-top: 24px; border-top: 2px solid #f1f3f4; text-align: center; }
|
||
`;
|
||
|
||
function ensureConfigStylesInjected() {
|
||
const existingStyle = document.querySelector('style[data-config-page="true"]');
|
||
if (existingStyle) return;
|
||
const style = document.createElement('style');
|
||
style.setAttribute('data-config-page', 'true');
|
||
style.textContent = CONFIG_PAGE_CSS;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
// 基础配置页静态 DOM
|
||
function getBasicConfigMarkup() {
|
||
return `
|
||
<div class="page-content">
|
||
|
||
<div id="loadingMessage" class="loading" style="display:none;">${t('basic.loading')}</div>
|
||
<div id="errorMessage" class="error" style="display:none;"></div>
|
||
<div id="successMessage" class="success" style="display:none;"></div>
|
||
|
||
<form id="configForm" style="display:none;">
|
||
<div class="config-section">
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.admin_emails')}</label>
|
||
<div class="email-tags-container" id="emailTagsContainer">
|
||
<div class="email-tags" id="emailTags"></div>
|
||
<input type="text" id="adminEmails" class="form-input email-input" placeholder="${t('basic.placeholder.email_input')}" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="registration-config-group plugin-config-group">
|
||
<div class="form-group">
|
||
<label class="form-label">Coze API Token</label>
|
||
<input type="password" id="cozeApiToken" class="form-input" placeholder="${t('basic.placeholder.coze_api_token')}">
|
||
<div style="margin-top:8px;font-size:12px;">
|
||
<a href="https://www.coze.cn/open/oauth/pats" target="_blank" rel="noopener noreferrer" style="color:#2980b9;text-decoration:none;">${t('basic.help.coze_api_token_apply')}</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<div class="checkbox-group">
|
||
<input type="checkbox" id="cozeSaasPluginEnabled" class="checkbox-input" />
|
||
<label for="cozeSaasPluginEnabled">${t('basic.label.enable_coze_saas')}</label>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Coze SaaS API Base URL</label>
|
||
<input type="text" id="cozeSaasApiBaseUrl" class="form-input" placeholder="https://api.coze.cn" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="registration-config-group">
|
||
<div class="checkbox-group">
|
||
<input type="checkbox" id="disableUserRegistration" class="checkbox-input" />
|
||
<label for="disableUserRegistration">${t('basic.label.disable_user_registration')}</label>
|
||
</div>
|
||
|
||
<div id="whitelistEmailGroup" class="form-group whitelist-email-config" style="display:none;">
|
||
<label class="form-label">${t('basic.label.whitelist_emails')}</label>
|
||
<div class="email-tags-container" id="whitelistEmailTagsContainer">
|
||
<div class="email-tags" id="whitelistEmailTags"></div>
|
||
<input type="text" id="allowRegistrationEmail" class="form-input email-input" placeholder="${t('basic.placeholder.email_input')}" />
|
||
</div>
|
||
<div class="help-text">${t('basic.help.whitelist')}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.python_env')}</label>
|
||
<div class="radio-group">
|
||
<div class="radio-item"><input type="radio" id="codeRunnerLocal" name="codeRunnerType" value="0" class="radio-input" /><label for="codeRunnerLocal">Local</label></div>
|
||
<div class="radio-item"><input type="radio" id="codeRunnerSandbox" name="codeRunnerType" value="1" class="radio-input" /><label for="codeRunnerSandbox">Sandbox</label></div>
|
||
</div>
|
||
<div class="help-text">${t('basic.help.local_risk')}</div>
|
||
</div>
|
||
|
||
<div id="sandboxConfig" class="sandbox-config">
|
||
<h4 style="margin-bottom: 15px; color: #2c3e50;">${t('basic.sandbox.title')}</h4>
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.allow_env')}</label>
|
||
<input type="text" id="allowEnv" class="form-input" placeholder="PATH,USERNAME">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.allow_read')}</label>
|
||
<input type="text" id="allowRead" class="form-input" placeholder="/tmp,./data">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.allow_write')}</label>
|
||
<input type="text" id="allowWrite" class="form-input" placeholder="/tmp,./data">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.allow_run')}</label>
|
||
<input type="text" id="allowRun" class="form-input" placeholder="python,git">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.allow_net')}</label>
|
||
<input type="text" id="allowNet" class="form-input" placeholder="cdn.jsdelivr.net">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.allow_ffi')}</label>
|
||
<input type="text" id="allowFfi" class="form-input" placeholder="/usr/lib/libm.so">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.node_modules_dir')}</label>
|
||
<input type="text" id="nodeModulesDir" class="form-input" placeholder="/tmp/path/node_modules">
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.timeout_seconds')}</label>
|
||
<input type="number" id="timeoutSeconds" class="form-input" placeholder="60" min="1" max="3600">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">${t('basic.label.memory_limit_mb')}</label>
|
||
<input type="number" id="memoryLimitMb" class="form-input" placeholder="100" min="1" max="10240">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="config-section">
|
||
<h3 class="section-title"><span class="section-icon">🌐</span>${t('server.title')}</h3>
|
||
<div class="form-group">
|
||
<label class="form-label">${t('server.host.label')}</label>
|
||
<input type="text" id="serverHost" class="form-input" placeholder="localhost:8888" />
|
||
<div class="help-text">${t('server.host.help')}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-buttons">
|
||
<button type="button" class="btn btn-primary" onclick="saveConfig()">💾 ${t('btn.save')}</button>
|
||
<button type="button" class="btn btn-secondary" onclick="loadConfig()">🔄 ${t('btn.reload')}</button>
|
||
</div>
|
||
</form>
|
||
</div>`;
|
||
}
|
||
|
||
// -------------------- 基础配置页脚本(避免与全局命名冲突) --------------------
|
||
let currentConfig = {};
|
||
let emailTags = [];
|
||
let whitelistEmailTags = [];
|
||
|
||
function getConfigTagsArray(containerId) {
|
||
if (containerId === 'emailTags') return emailTags;
|
||
if (containerId === 'whitelistEmailTags') return whitelistEmailTags;
|
||
return [];
|
||
}
|
||
|
||
function initConfigEmailTags() {
|
||
initConfigEmailTagsForInput('adminEmails', 'emailTags', 'emailTags');
|
||
initConfigEmailTagsForInput('allowRegistrationEmail', 'whitelistEmailTags', 'whitelistEmailTags');
|
||
}
|
||
|
||
function isValidEmail(email) {
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
return emailRegex.test(email);
|
||
}
|
||
|
||
function initConfigEmailTagsForInput(inputId, containerId, type) {
|
||
const inputEl = document.getElementById(inputId);
|
||
const container = document.getElementById(containerId);
|
||
if (!inputEl || !container) return;
|
||
|
||
const handleAdd = () => {
|
||
const value = inputEl.value.trim();
|
||
if (value) {
|
||
addConfigEmailTagForInput(type, value);
|
||
inputEl.value = '';
|
||
}
|
||
};
|
||
|
||
inputEl.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Enter' || e.key === ',') {
|
||
e.preventDefault();
|
||
handleAdd();
|
||
} else if (e.key === 'Backspace' && !inputEl.value) {
|
||
const tags = getConfigTagsArray(containerId);
|
||
tags.pop();
|
||
renderConfigEmailTagsForInput(containerId, tags);
|
||
}
|
||
});
|
||
|
||
inputEl.addEventListener('blur', handleAdd);
|
||
}
|
||
|
||
function addConfigEmailTagForInput(type, value) {
|
||
const containerId = type === 'emailTags' ? 'emailTags' : 'whitelistEmailTags';
|
||
const tags = getConfigTagsArray(containerId);
|
||
|
||
if (!isValidEmail(value)) {
|
||
showError('请输入有效的邮箱地址');
|
||
return;
|
||
}
|
||
|
||
if (tags.includes(value)) {
|
||
showError('该邮箱已存在');
|
||
return;
|
||
}
|
||
|
||
tags.push(value);
|
||
renderConfigEmailTagsForInput(containerId, tags);
|
||
}
|
||
|
||
function removeConfigEmailTagForInput(type, index) {
|
||
const containerId = type === 'emailTags' ? 'emailTags' : 'whitelistEmailTags';
|
||
const tags = getConfigTagsArray(containerId);
|
||
tags.splice(index, 1);
|
||
renderConfigEmailTagsForInput(containerId, tags);
|
||
}
|
||
|
||
function renderConfigEmailTagsForInput(containerId, tags) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
container.innerHTML = '';
|
||
tags.forEach((tag, index) => {
|
||
const tagElement = document.createElement('span');
|
||
tagElement.className = 'email-tag';
|
||
tagElement.innerHTML = `
|
||
<span class="email-tag-text" title="${tag}">${tag}</span>
|
||
<button class="email-tag-remove" onclick="removeConfigEmailTagForInput('${containerId === 'emailTags' ? 'emailTags' : 'whitelistEmailTags'}', ${index})">×</button>
|
||
`;
|
||
container.appendChild(tagElement);
|
||
});
|
||
}
|
||
|
||
function setEmailTags(emails) {
|
||
const inputEmails = Array.isArray(emails) ? emails : (emails || '').split(/[,\s]+/).filter(Boolean);
|
||
const nowEmails = emailTags || [];
|
||
const allEmails = Array.from(new Set([...(nowEmails || []), ...(inputEmails || [])]));
|
||
emailTags = allEmails;
|
||
renderConfigEmailTagsForInput('emailTags', emailTags);
|
||
}
|
||
|
||
function getEmailTags() {
|
||
return (emailTags || []).join(',');
|
||
}
|
||
|
||
function setWhitelistEmailTags(emails) {
|
||
const inputEmails = Array.isArray(emails) ? emails : (emails || '').split(/[,\s]+/).filter(Boolean);
|
||
const nowEmails = whitelistEmailTags || [];
|
||
const allEmails = Array.from(new Set([...(nowEmails || []), ...(inputEmails || [])]));
|
||
whitelistEmailTags = allEmails;
|
||
renderConfigEmailTagsForInput('whitelistEmailTags', whitelistEmailTags);
|
||
}
|
||
|
||
function getWhitelistEmailTags() {
|
||
return (whitelistEmailTags || []).join(',');
|
||
}
|
||
|
||
function setupConfigEventListeners() {
|
||
const disableEl = document.getElementById('disableUserRegistration');
|
||
const whitelistGroup = document.getElementById('whitelistEmailGroup');
|
||
if (disableEl && whitelistGroup) {
|
||
disableEl.addEventListener('change', function () {
|
||
toggleConfigWhitelistEmailConfig(disableEl.checked);
|
||
});
|
||
}
|
||
|
||
const localEl = document.getElementById('codeRunnerLocal');
|
||
const sandboxEl = document.getElementById('codeRunnerSandbox');
|
||
if (localEl && sandboxEl) {
|
||
const updateSandbox = () => toggleConfigSandboxConfig(sandboxEl.checked);
|
||
localEl.addEventListener('change', updateSandbox);
|
||
sandboxEl.addEventListener('change', updateSandbox);
|
||
}
|
||
}
|
||
|
||
function toggleConfigWhitelistEmailConfig(show) {
|
||
const el = document.getElementById('whitelistEmailGroup');
|
||
if (!el) return;
|
||
el.style.display = show ? 'block' : 'none';
|
||
if (show) el.classList.add('show'); else el.classList.remove('show');
|
||
}
|
||
|
||
function toggleConfigSandboxConfig(show) {
|
||
const el = document.getElementById('sandboxConfig');
|
||
if (!el) return;
|
||
el.style.display = show ? 'block' : 'none';
|
||
if (show) el.classList.add('show'); else el.classList.remove('show');
|
||
}
|
||
|
||
function loadConfig() {
|
||
showConfigLoading(true);
|
||
hideMessages();
|
||
fetch('/api/admin/config/basic/get')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (handleApiCode(data)) {
|
||
showConfigLoading(false);
|
||
return;
|
||
}
|
||
if (!data || !data.configuration) {
|
||
showError(t('error.load_failed_empty'));
|
||
// 在无后端或接口异常时也展示表单,便于手动配置
|
||
populateForm({});
|
||
showConfigForm(true);
|
||
showConfigLoading(false);
|
||
return;
|
||
}
|
||
currentConfig = data.configuration || {};
|
||
populateForm(currentConfig);
|
||
showConfigForm(true);
|
||
showConfigLoading(false);
|
||
})
|
||
.catch(err => {
|
||
console.error('Failed to load configuration:', err);
|
||
showError(t('error.load_failed_json'));
|
||
// 无后端时展示空表单以便继续操作
|
||
populateForm({});
|
||
showConfigForm(true);
|
||
showConfigLoading(false);
|
||
});
|
||
}
|
||
|
||
function populateForm(cfg) {
|
||
document.getElementById('serverHost').value = cfg.server_host || '';
|
||
|
||
// 管理员邮箱
|
||
setEmailTags(cfg.admin_emails || '');
|
||
|
||
// 插件配置:Coze API Token + SaaS 相关
|
||
const pluginCfg = cfg.plugin_configuration || {};
|
||
const cozeEl = document.getElementById('cozeApiToken');
|
||
if (cozeEl) cozeEl.value = pluginCfg.coze_api_token || cfg.coze_api_token || '';
|
||
const saasEnabledEl = document.getElementById('cozeSaasPluginEnabled');
|
||
if (saasEnabledEl) saasEnabledEl.checked = (pluginCfg.coze_saas_plugin_enabled === true || pluginCfg.coze_saas_plugin_enabled === 'true');
|
||
const saasBaseUrlEl = document.getElementById('cozeSaasApiBaseUrl');
|
||
if (saasBaseUrlEl) saasBaseUrlEl.value = pluginCfg.coze_saas_api_base_url || 'https://api.coze.cn';
|
||
|
||
// 禁止用户注册与白名单
|
||
const disableReg = cfg.disable_user_registration === true || cfg.disable_user_registration === 'true';
|
||
const disableEl = document.getElementById('disableUserRegistration');
|
||
if (disableEl) disableEl.checked = disableReg;
|
||
toggleConfigWhitelistEmailConfig(disableReg);
|
||
setWhitelistEmailTags(cfg.allow_registration_email || '');
|
||
|
||
// 代码运行环境
|
||
const crt = Number(cfg.code_runner_type || 0);
|
||
const localEl = document.getElementById('codeRunnerLocal');
|
||
const sandboxEl = document.getElementById('codeRunnerSandbox');
|
||
if (localEl && sandboxEl) {
|
||
localEl.checked = crt !== 1;
|
||
sandboxEl.checked = crt === 1;
|
||
}
|
||
toggleConfigSandboxConfig(crt === 1);
|
||
|
||
// Sandbox 配置
|
||
const s = cfg.sandbox_config || {};
|
||
if (document.getElementById('allowEnv')) document.getElementById('allowEnv').value = s.allow_env || '';
|
||
if (document.getElementById('allowRead')) document.getElementById('allowRead').value = s.allow_read || '';
|
||
if (document.getElementById('allowWrite')) document.getElementById('allowWrite').value = s.allow_write || '';
|
||
if (document.getElementById('allowRun')) document.getElementById('allowRun').value = s.allow_run || '';
|
||
if (document.getElementById('allowNet')) document.getElementById('allowNet').value = s.allow_net || 'cdn.jsdelivr.net';
|
||
if (document.getElementById('allowFfi')) document.getElementById('allowFfi').value = s.allow_ffi || '';
|
||
if (document.getElementById('nodeModulesDir')) document.getElementById('nodeModulesDir').value = s.node_modules_dir || '';
|
||
if (document.getElementById('timeoutSeconds')) document.getElementById('timeoutSeconds').value = s.timeout_seconds || '';
|
||
if (document.getElementById('memoryLimitMb')) document.getElementById('memoryLimitMb').value = s.memory_limit_mb || '';
|
||
}
|
||
|
||
function collectFormData() {
|
||
const serverHost = document.getElementById('serverHost').value.trim();
|
||
const adminEmails = getEmailTags();
|
||
const allowedEmails = getWhitelistEmailTags();
|
||
const disableReg = document.getElementById('disableUserRegistration').checked;
|
||
const codeRunnerType = document.getElementById('codeRunnerSandbox').checked ? 1 : 0;
|
||
const cozeApiToken = (document.getElementById('cozeApiToken')?.value || '').trim();
|
||
const cozeSaasPluginEnabled = !!(document.getElementById('cozeSaasPluginEnabled')?.checked);
|
||
const cozeSaasApiBaseUrlRaw = (document.getElementById('cozeSaasApiBaseUrl')?.value || '');
|
||
const cozeSaasApiBaseUrl = (cozeSaasApiBaseUrlRaw ? cozeSaasApiBaseUrlRaw : 'https://api.coze.cn').replace(/`/g, '').trim();
|
||
|
||
const sAllowEnv = (document.getElementById('allowEnv')?.value || '').trim();
|
||
const sAllowRead = (document.getElementById('allowRead')?.value || '').trim();
|
||
const sAllowWrite = (document.getElementById('allowWrite')?.value || '').trim();
|
||
const sAllowRun = (document.getElementById('allowRun')?.value || '').trim();
|
||
const sAllowNet = (document.getElementById('allowNet')?.value || '').trim();
|
||
const sAllowFfi = (document.getElementById('allowFfi')?.value || '').trim();
|
||
const sNodeModulesDir = (document.getElementById('nodeModulesDir')?.value || '').trim();
|
||
const sTimeoutSeconds = document.getElementById('timeoutSeconds')?.value;
|
||
const sMemoryLimitMb = document.getElementById('memoryLimitMb')?.value;
|
||
|
||
const configuration = {
|
||
admin_emails: adminEmails,
|
||
disable_user_registration: disableReg,
|
||
allow_registration_email: allowedEmails,
|
||
code_runner_type: codeRunnerType,
|
||
server_host: serverHost,
|
||
plugin_configuration: {
|
||
coze_saas_plugin_enabled: cozeSaasPluginEnabled,
|
||
coze_api_token: cozeApiToken,
|
||
coze_saas_api_base_url: cozeSaasApiBaseUrl
|
||
}
|
||
};
|
||
|
||
if (codeRunnerType === 1) {
|
||
configuration.sandbox_config = {
|
||
allow_env: sAllowEnv,
|
||
allow_read: sAllowRead,
|
||
allow_write: sAllowWrite,
|
||
allow_run: sAllowRun,
|
||
allow_net: sAllowNet || 'cdn.jsdelivr.net',
|
||
allow_ffi: sAllowFfi,
|
||
node_modules_dir: sNodeModulesDir,
|
||
timeout_seconds: sTimeoutSeconds ? parseFloat(sTimeoutSeconds) : undefined,
|
||
memory_limit_mb: sMemoryLimitMb ? parseInt(sMemoryLimitMb, 10) : undefined
|
||
};
|
||
}
|
||
|
||
return { configuration };
|
||
}
|
||
|
||
function saveConfig() {
|
||
const payload = collectFormData();
|
||
if (!payload.configuration.admin_emails) {
|
||
showError(t('error.admin_email_required'));
|
||
return;
|
||
}
|
||
if (!payload.configuration.server_host) {
|
||
showError(t('error.server_host_required'));
|
||
return;
|
||
}
|
||
|
||
// 验证当前标签中的邮箱格式(最终兜底检查)
|
||
const invalidAdmin = (emailTags || []).filter(e => e && !isValidEmail(e));
|
||
const invalidWhitelist = (whitelistEmailTags || []).filter(e => e && !isValidEmail(e));
|
||
if (invalidAdmin.length > 0) {
|
||
showError(t('error.invalid_admin_emails', { list: invalidAdmin.join(', ') }));
|
||
return;
|
||
}
|
||
if (invalidWhitelist.length > 0) {
|
||
showError(t('error.invalid_whitelist_emails', { list: invalidWhitelist.join(', ') }));
|
||
return;
|
||
}
|
||
|
||
showConfigLoading(true);
|
||
hideMessages();
|
||
fetch('/api/admin/config/basic/save', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (handleApiCode(data)) {
|
||
showConfigLoading(false);
|
||
return;
|
||
}
|
||
if (data && Number(data.code) === 0) {
|
||
showSuccess(t('success.saved'));
|
||
} else {
|
||
const msg = (data && (data.msg || data.message)) ? (data.msg || data.message) : t('error.save_failed');
|
||
showError(msg);
|
||
}
|
||
showConfigLoading(false);
|
||
})
|
||
.catch(err => {
|
||
console.error(t('knowledge.error.save_failed') + ':', err);
|
||
showError(t('error.save_failed_json'));
|
||
showConfigLoading(false);
|
||
});
|
||
}
|
||
|
||
function showConfigLoading(show) {
|
||
const el = document.getElementById('loadingMessage');
|
||
if (el) el.style.display = show ? 'block' : 'none';
|
||
}
|
||
|
||
function showConfigForm(show) {
|
||
const el = document.getElementById('configForm');
|
||
if (el) el.style.display = show ? 'block' : 'none';
|
||
}
|
||
|
||
function hideMessages() {
|
||
const errEl = document.getElementById('errorMessage');
|
||
const sucEl = document.getElementById('successMessage');
|
||
if (errEl) errEl.style.display = 'none';
|
||
if (sucEl) sucEl.style.display = 'none';
|
||
}
|
||
|
||
// 显示工作台内容
|
||
function showWorkspaceContent() {
|
||
const now = new Date();
|
||
const localeFmt = (currentLocale === 'en-US') ? 'en-US' : 'zh-CN';
|
||
const today = now.toLocaleDateString(localeFmt, {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
weekday: 'long'
|
||
});
|
||
|
||
const hour = now.getHours();
|
||
let greetingKey = 'workspace.greeting.evening';
|
||
if (hour < 6) {
|
||
greetingKey = 'workspace.greeting.late';
|
||
} else if (hour < 12) {
|
||
greetingKey = 'workspace.greeting.morning';
|
||
} else if (hour < 14) {
|
||
greetingKey = 'workspace.greeting.noon';
|
||
} else if (hour < 18) {
|
||
greetingKey = 'workspace.greeting.afternoon';
|
||
} else {
|
||
greetingKey = 'workspace.greeting.evening';
|
||
}
|
||
const greeting = t(greetingKey);
|
||
|
||
pageContent.innerHTML = `
|
||
<div style="padding: 30px;">
|
||
<div style="text-align: center; margin-bottom: 40px;">
|
||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; border-radius: 15px; box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);">
|
||
<h1 style="margin: 0 0 15px; font-size: 2.5em; font-weight: 300;">${t('workspace.hero.title')}</h1>
|
||
<p style="margin: 0 0 20px; font-size: 1.2em; opacity: 0.9;">${greeting}</p>
|
||
<div style="background: rgba(255,255,255,0.2); padding: 15px; border-radius: 10px; display: inline-block;">
|
||
<p style="margin: 0; font-size: 1.1em; font-weight: 500;">📅 ${today}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 25px;">
|
||
<div style="background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); border-left: 5px solid #3498db;">
|
||
<div style="display: flex; align-items: center; margin-bottom: 15px;">
|
||
<div style="background: #3498db; color: white; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 15px; font-size: 18px;">🔧</div>
|
||
<h3 style="margin: 0; color: #2c3e50;">${t('workspace.card.basic.title')}</h3>
|
||
</div>
|
||
<p style="color: #7f8c8d; margin-bottom: 15px; line-height: 1.6;">${t('workspace.card.basic.desc')}</p>
|
||
<button onclick="loadPageContent('basic-config')" style="background: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all 0.3s;">${t('workspace.card.basic.cta')}</button>
|
||
</div>
|
||
|
||
<div style="background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); border-left: 5px solid #2ecc71;">
|
||
<div style="display: flex; align-items: center; margin-bottom: 15px;">
|
||
<div style="background: #2ecc71; color: white; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 15px; font-size: 18px;">🤖</div>
|
||
<h3 style="margin: 0; color: #2c3e50;">${t('workspace.card.model.title')}</h3>
|
||
</div>
|
||
<p style="color: #7f8c8d; margin-bottom: 15px; line-height: 1.6;">${t('workspace.card.model.desc')}</p>
|
||
<button onclick="loadPageContent('model-management')" style="background: #2ecc71; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all 0.3s;">${t('workspace.card.model.cta')}</button>
|
||
</div>
|
||
|
||
<div style="background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); border-left: 5px solid #f39c12;">
|
||
<div style="display: flex; align-items: center; margin-bottom: 15px;">
|
||
<div style="background: #f39c12; color: white; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 15px; font-size: 18px;">📚</div>
|
||
<h3 style="margin: 0; color: #2c3e50;">${t('workspace.card.knowledge.title')}</h3>
|
||
</div>
|
||
<p style="color: #7f8c8d; margin-bottom: 15px; line-height: 1.6;">${t('workspace.card.knowledge.desc')}</p>
|
||
<button onclick="loadPageContent('knowledge-config')" style="background: #f39c12; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all 0.3s;">${t('workspace.card.knowledge.cta')}</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 30px; text-align: center; color: #95a5a6; font-size: 14px;">
|
||
<p>🚀 ${t('workspace.footer.cta')}</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 模型管理页面:加载与渲染
|
||
async function loadModelManagementPage() {
|
||
try {
|
||
showLoading();
|
||
const currentHost = window.location.origin;
|
||
const apiUrl = `${currentHost}/api/admin/config/model/list`;
|
||
const response = await fetch(apiUrl, { method: 'GET', headers: { 'Accept': 'application/json' } });
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
||
}
|
||
const data = await response.json();
|
||
if (handleApiCode(data)) {
|
||
return;
|
||
}
|
||
renderModelManagement(data);
|
||
} catch (error) {
|
||
console.error(t('model.error.load_list_failed'), error);
|
||
showError(t('model.error.load_list_failed') + ': ' + error.message);
|
||
}
|
||
}
|
||
|
||
function sanitizeUrl(url) {
|
||
if (!url) return '';
|
||
return String(url).replace(/`/g, '').trim();
|
||
}
|
||
|
||
function maskApiKey(key) {
|
||
if (!key) return t('common.not_configured');
|
||
const s = String(key);
|
||
if (s.length <= 10) return '***';
|
||
return s.slice(0, 6) + '***' + s.slice(-4);
|
||
}
|
||
|
||
function renderModelManagement(payload) {
|
||
try {
|
||
const providers = Array.isArray(payload?.provider_model_list) ? payload.provider_model_list : [];
|
||
const html = `
|
||
<div style="padding: 30px;">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
|
||
<div>
|
||
<h2 style="color:#2c3e50;margin:0 0 8px;">${t('model.list.title')}</h2>
|
||
</div>
|
||
<div style="font-size:12px;color:#95a5a6;">${t('model.provider.count', { count: providers.length })}</div>
|
||
</div>
|
||
|
||
<!-- 添加模型弹窗 -->
|
||
<div id="addModelModal" style="position:fixed;inset:0;background:rgba(0,0,0,0.35);display:none;align-items:center;justify-content:center;z-index:9999;">
|
||
<div style="background:#fff;width:480px;max-width:90vw;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,0.2);overflow:hidden;">
|
||
<div style="padding:16px 20px;border-bottom:1px solid #f1f3f4;display:flex;justify-content:space-between;align-items:center;">
|
||
<div style="font-weight:600;color:#2c3e50;">${t('model.modal.add.title')}</div>
|
||
<button onclick="closeAddModelModal()" style="background:transparent;border:none;font-size:20px;color:#6c757d;cursor:pointer;">×</button>
|
||
</div>
|
||
<div style="padding:20px;display:flex;flex-direction:column;gap:12px;">
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
|
||
<img id="modalProviderIcon" src="" alt="" style="width:24px;height:24px;border-radius:6px;object-fit:cover;display:none;">
|
||
<div id="modalProviderName" style="font-size:12px;color:#6c757d;"></div>
|
||
</div>
|
||
<label style="font-size:12px;color:#374151;">${t('model.modal.display_name.label')}<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></label>
|
||
<input id="newModelDisplayName" type="text" style="padding:8px;border:1px solid #e1e8ed;border-radius:8px;width:100%;" placeholder="${t('model.modal.display_name.placeholder')}">
|
||
<label style="font-size:12px;color:#374151;">${t('model.modal.model.label')}<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></label>
|
||
<input id="newModelModel" type="text" style="padding:8px;border:1px solid #e1e8ed;border-radius:8px;width:100%;" placeholder="${t('model.modal.model.placeholder')}">
|
||
<label id="newModelApiKeyLabel" style="font-size:12px;color:#374151;">${t('model.modal.api_key.label')}<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></label>
|
||
<input id="newModelApiKey" type="text" style="padding:8px;border:1px solid #e1e8ed;border-radius:8px;width:100%;" placeholder="${t('model.modal.api_key.placeholder')}">
|
||
<label style="font-size:12px;color:#374151;">${t('model.modal.base_url.label')}</label>
|
||
<input id="newModelBaseUrl" type="text" style="padding:8px;border:1px solid #e1e8ed;border-radius:8px;width:100%;" placeholder="${t('model.modal.base_url.placeholder')}">
|
||
<label style="font-size:12px;color:#374151;">${t('model.modal.enable_base64_url.label')}</label>
|
||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
|
||
<input id="newModelEnableBase64Url" type="checkbox">
|
||
<span style="font-size:12px;color:#6c757d;">${t('model.modal.enable_base64_url.help')}</span>
|
||
</div>
|
||
<label id="newModelArkRegionLabel" style="font-size:12px;color:#374151;display:none;">${t('model.modal.ark_region.label')}</label>
|
||
<input id="newModelArkRegion" type="text" style="padding:8px;border:1px solid #e1e8ed;border-radius:8px;width:100%;display:none;" placeholder="${t('model.modal.ark_region.placeholder')}">
|
||
<label id="newModelOpenAIByAzureLabel" style="font-size:12px;color:#374151;display:none;">${t('model.modal.azure.label')}</label>
|
||
<div id="newModelOpenAIByAzureWrapper" style="display:none;align-items:center;gap:8px;margin-bottom:4px;">
|
||
<input id="newModelOpenAIByAzure" type="checkbox">
|
||
<span style="font-size:12px;color:#6c757d;">${t('model.modal.azure.help')}</span>
|
||
</div>
|
||
<label id="newModelOpenAIApiVersionLabel" style="font-size:12px;color:#374151;display:none;">${t('model.modal.openai_api_version.label')}</label>
|
||
<input id="newModelOpenAIApiVersion" type="text" style="padding:8px;border:1px solid #e1e8ed;border-radius:8px;width:100%;display:none;" placeholder="${t('model.modal.openai_api_version.placeholder')}">
|
||
<label id="newModelGeminiBackendLabel" style="font-size:12px;color:#374151;display:none;">${t('model.modal.gemini_backend.label')}</label>
|
||
<select id="newModelGeminiBackend" style="padding:8px;border:1px solid #e1e8ed;border-radius:8px;width:100%;display:none;">
|
||
<option value="0" selected>${t('model.modal.gemini_backend.unspecified')}</option>
|
||
<option value="1">${t('model.modal.gemini_backend.gemini_api')}</option>
|
||
<option value="2">${t('model.modal.gemini_backend.vertex_ai')}</option>
|
||
</select>
|
||
<label id="newModelGeminiProjectLabel" style="font-size:12px;color:#374151;display:none;">${t('model.modal.gemini_project.label')}</label>
|
||
<input id="newModelGeminiProject" type="text" style="padding:8px;border:1px solid #e1e8ed;border-radius:8px;width:100%;display:none;" placeholder="${t('model.modal.gemini_project.placeholder')}">
|
||
<label id="newModelGeminiLocationLabel" style="font-size:12px;color:#374151;display:none;">${t('model.modal.gemini_location.label')}</label>
|
||
<input id="newModelGeminiLocation" type="text" style="padding:8px;border:1px solid #e1e8ed;border-radius:8px;width:100%;display:none;" placeholder="${t('model.modal.gemini_location.placeholder')}">
|
||
</div>
|
||
<div style="padding:12px 20px;display:flex;justify-content:flex-end;gap:8px;border-top:1px solid #f1f3f4;">
|
||
<button onclick="closeAddModelModal()" style="background:#ecf0f1;color:#2c3e50;border:none;padding:8px 12px;border-radius:6px;cursor:pointer;">${t('common.cancel')}</button>
|
||
<button id="addModelSubmitBtn" onclick="saveNewModel()" style="background:#667eea;color:#fff;border:none;padding:8px 12px;border-radius:6px;cursor:pointer;">${t('common.save')}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr;gap:20px;">
|
||
${providers.map(pm => {
|
||
const p = pm.provider || {};
|
||
const name = p.name?.zh_cn || p.name?.en_us || t('model.provider.unnamed');
|
||
const desc = p.description?.zh_cn || p.description?.en_us || '';
|
||
const icon = p.icon_url || p.icon_uri || '';
|
||
const models = Array.isArray(pm.model_list) ? pm.model_list : [];
|
||
return `
|
||
<div style="background:white;border-radius:12px;box-shadow:0 2px 10px rgba(0,0,0,0.08);border:1px solid #e1e8ed;overflow:hidden;">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid #f1f3f4;background:#f8f9fa;">
|
||
<div style="display:flex;align-items:center;gap:12px;">
|
||
<img src="${icon}" alt="${name}" onerror="this.style.display='none'" style="width:36px;height:36px;border-radius:8px;object-fit:cover;">
|
||
<div>
|
||
<div style="font-weight:600;color:#2c3e50;">${name}</div>
|
||
<div style="color:#6c757d;font-size:12px;">${desc}</div>
|
||
</div>
|
||
</div>
|
||
<button onclick="openAddModelModalWithProvider(this)" data-provider-name="${name}" data-provider-icon="${icon}" data-provider-class="${p.model_class}" style="background:#3498db;color:#fff;border:none;padding:8px 12px;border-radius:6px;font-size:12px;cursor:pointer;">${t('model.actions.add')}</button>
|
||
</div>
|
||
<div style="padding:16px;">
|
||
<div style="color:#7f8c8d;font-size:12px;margin-bottom:12px;">${t('model.provider.model_count', { count: models.length })}</div>
|
||
${models.map(m => {
|
||
const di = m.display_info || {};
|
||
const conn = m.connection?.base_conn_info || {};
|
||
const modelName = di.name || conn.model || t('model.unnamed');
|
||
const baseUrl = sanitizeUrl(conn.base_url || '');
|
||
const apiKeyMasked = maskApiKey(conn.api_key || '');
|
||
const outputTokens = di.output_tokens ?? '-';
|
||
const maxTokens = di.max_tokens ?? '-';
|
||
const statusText = m.status === 1 ? t('common.enabled') : t('common.disabled');
|
||
const statusColor = m.status === 1 ? '#2ecc71' : '#e74c3c';
|
||
const modelId = m.id ?? m.model_id ?? '';
|
||
return `
|
||
<div style="border:1px solid #e1e8ed;border-radius:10px;padding:12px;margin-bottom:12px;background:#ffffff;">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||
<div style="font-weight:500;color:#2c3e50;">${modelName}</div>
|
||
<div style="display:flex;align-items:center;gap:8px;">
|
||
<span style="font-size:12px;color:${statusColor};">${statusText}</span>
|
||
<button onclick="deleteModel('${modelId}')" style="background:#e74c3c;color:#fff;border:none;padding:4px 8px;border-radius:6px;font-size:12px;cursor:pointer;">${t('common.delete')}</button>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;margin-top:10px;font-size:12px;color:#374151;">
|
||
<div>${t('model.field.id')}:${m.id ?? m.model_id ?? '-'}</div>
|
||
<div>${t('model.field.model')}:${conn.model || '-'}</div>
|
||
${p?.model_class === 20 ? '' : `<div>${t('model.field.api_key')}:${apiKeyMasked}</div>`}
|
||
<div>${t('model.field.base64_url')}:${m.enable_base64_url ? t('common.on') : t('common.off')}</div>
|
||
${p?.model_class === 2 ? `<div>${t('model.field.region')}:${m?.connection?.ark?.region || ''}</div>` : ''}
|
||
${p?.model_class === 11 ? `<div>${t('model.field.backend')}:${m?.connection?.gemini?.backend ?? ''}</div><div>${t('model.field.project')}:${m?.connection?.gemini?.project || ''}</div><div>${t('model.field.location')}:${m?.connection?.gemini?.location || ''}</div>` : ''}
|
||
${p?.model_class === 1 ? `<div>${t('model.field.azure')}:${(m?.connection?.openai?.by_azure === undefined) ? '' : (m?.connection?.openai?.by_azure ? 'true' : 'false')}</div><div>${t('model.field.api_version')}:${m?.connection?.openai?.api_version || ''}</div>` : ''}
|
||
</div>
|
||
<div style="margin-top:8px;font-size:12px;color:#374151;word-break:break-all;">
|
||
${t('model.field.endpoint')}:<span style="color:#3498db;">${baseUrl || '-'}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
pageContent.innerHTML = html;
|
||
} catch (e) {
|
||
console.error(t('model.error.render_page_failed'), e);
|
||
showError(t('model.error.render_page_failed') + ': ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteModel(id) {
|
||
try {
|
||
if (!id) {
|
||
showError(t('model.error.delete_missing_id'));
|
||
return;
|
||
}
|
||
const confirmed = window.confirm(t('model.confirm.delete'));
|
||
if (!confirmed) {
|
||
return; // 用户取消
|
||
}
|
||
const resp = await fetch('/api/admin/config/model/delete', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: String(id) })
|
||
});
|
||
if (!resp.ok) {
|
||
throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
|
||
}
|
||
let data;
|
||
try {
|
||
data = await resp.json();
|
||
} catch (err) {
|
||
showError(t('model.error.delete_invalid_json'));
|
||
return;
|
||
}
|
||
if (handleApiCode(data)) {
|
||
return;
|
||
}
|
||
if (data?.code !== 0) {
|
||
showError(t('model.error.delete_failed') + ': ' + (data?.msg || t('common.unknown_error')));
|
||
return;
|
||
}
|
||
// 刷新模型列表
|
||
await loadModelManagementPage();
|
||
} catch (error) {
|
||
showError(t('model.error.delete_failed') + ': ' + error.message);
|
||
}
|
||
}
|
||
|
||
function openAddModelModalWithProvider(btn) {
|
||
const modal = document.getElementById('addModelModal');
|
||
if (!modal) { showError(t('model.error.open_modal_failed')); return; }
|
||
const nameEl = document.getElementById('modalProviderName');
|
||
const iconEl = document.getElementById('modalProviderIcon');
|
||
const providerName = btn?.dataset?.providerName || t('model.provider.unknown');
|
||
const providerIcon = btn?.dataset?.providerIcon || '';
|
||
const providerClassStr = btn?.dataset?.providerClass;
|
||
const providerClass = providerClassStr ? Number(providerClassStr) : undefined;
|
||
window.__selectedProvider = { name: providerName, icon: providerIcon, model_class: providerClass };
|
||
if (nameEl) nameEl.textContent = providerName;
|
||
if (iconEl) {
|
||
if (providerIcon) {
|
||
iconEl.src = providerIcon; iconEl.alt = providerName; iconEl.style.display = 'block';
|
||
} else {
|
||
iconEl.src = ''; iconEl.style.display = 'none';
|
||
}
|
||
}
|
||
// 根据 provider 隐藏或显示 API Key 字段(Ollama 不需要)
|
||
const apiLabel = document.getElementById('newModelApiKeyLabel');
|
||
const apiInput = document.getElementById('newModelApiKey');
|
||
const isOllama = providerClass === 20; // developer_api.ModelClass_Llama
|
||
if (apiLabel) apiLabel.style.display = isOllama ? 'none' : '';
|
||
if (apiInput) apiInput.style.display = isOllama ? 'none' : '';
|
||
// 根据 provider 显示 Ark 的 Region 字段(仅 Doubao/Ark,class=2)
|
||
const arkLabel = document.getElementById('newModelArkRegionLabel');
|
||
const arkInput = document.getElementById('newModelArkRegion');
|
||
const isArk = providerClass === 2;
|
||
if (arkLabel) arkLabel.style.display = isArk ? '' : 'none';
|
||
if (arkInput) arkInput.style.display = isArk ? '' : 'none';
|
||
// 根据 provider 显示 OpenAI 的 Azure 与 API Version 字段(仅 OpenAI,class=1)
|
||
const openaiByAzureLabel = document.getElementById('newModelOpenAIByAzureLabel');
|
||
const openaiByAzureWrapper = document.getElementById('newModelOpenAIByAzureWrapper');
|
||
const openaiApiVersionLabel = document.getElementById('newModelOpenAIApiVersionLabel');
|
||
const openaiApiVersionInput = document.getElementById('newModelOpenAIApiVersion');
|
||
const isOpenAI = providerClass === 1;
|
||
if (openaiByAzureLabel) openaiByAzureLabel.style.display = isOpenAI ? '' : 'none';
|
||
if (openaiByAzureWrapper) openaiByAzureWrapper.style.display = isOpenAI ? 'flex' : 'none';
|
||
if (openaiApiVersionLabel) openaiApiVersionLabel.style.display = isOpenAI ? '' : 'none';
|
||
if (openaiApiVersionInput) openaiApiVersionInput.style.display = isOpenAI ? '' : 'none';
|
||
// 根据 provider 显示 Gemini 的后端、项目、位置(仅 Gemini,class=11)
|
||
const geminiBackendLabel = document.getElementById('newModelGeminiBackendLabel');
|
||
const geminiBackendInput = document.getElementById('newModelGeminiBackend');
|
||
const geminiProjectLabel = document.getElementById('newModelGeminiProjectLabel');
|
||
const geminiProjectInput = document.getElementById('newModelGeminiProject');
|
||
const geminiLocationLabel = document.getElementById('newModelGeminiLocationLabel');
|
||
const geminiLocationInput = document.getElementById('newModelGeminiLocation');
|
||
const isGemini = providerClass === 11;
|
||
if (geminiBackendLabel) geminiBackendLabel.style.display = isGemini ? '' : 'none';
|
||
if (geminiBackendInput) geminiBackendInput.style.display = isGemini ? '' : 'none';
|
||
if (geminiProjectLabel) geminiProjectLabel.style.display = isGemini ? '' : 'none';
|
||
if (geminiProjectInput) geminiProjectInput.style.display = isGemini ? '' : 'none';
|
||
if (geminiLocationLabel) geminiLocationLabel.style.display = isGemini ? '' : 'none';
|
||
if (geminiLocationInput) geminiLocationInput.style.display = isGemini ? '' : 'none';
|
||
modal.style.display = 'flex';
|
||
}
|
||
function closeAddModelModal() {
|
||
const modal = document.getElementById('addModelModal');
|
||
if (modal) modal.style.display = 'none';
|
||
}
|
||
async function saveNewModel() {
|
||
const name = document.getElementById('newModelDisplayName')?.value?.trim() || '';
|
||
const model = document.getElementById('newModelModel')?.value?.trim() || '';
|
||
const apiKey = document.getElementById('newModelApiKey')?.value?.trim() || '';
|
||
const baseUrl = document.getElementById('newModelBaseUrl')?.value?.trim() || '';
|
||
// 允许 Base URL 为空;仅对必填项做校验,并在保存期间禁用按钮与显示提示
|
||
const submitBtn = document.getElementById('addModelSubmitBtn');
|
||
const restoreSavingState = () => {
|
||
if (submitBtn) {
|
||
submitBtn.disabled = false;
|
||
if (submitBtn.dataset.originalText) submitBtn.textContent = submitBtn.dataset.originalText;
|
||
submitBtn.style.opacity = '';
|
||
submitBtn.style.cursor = 'pointer';
|
||
}
|
||
};
|
||
if (submitBtn) {
|
||
submitBtn.disabled = true;
|
||
submitBtn.dataset.originalText = submitBtn.textContent;
|
||
submitBtn.textContent = t('common.saving');
|
||
submitBtn.style.opacity = '0.7';
|
||
submitBtn.style.cursor = 'not-allowed';
|
||
}
|
||
const providerClass = window.__selectedProvider?.model_class;
|
||
if (typeof providerClass !== 'number') {
|
||
showError(t('model.error.provider_unrecognized'));
|
||
restoreSavingState();
|
||
return;
|
||
}
|
||
const requireApiKey = providerClass !== 20; // Ollama 不需要 API Key
|
||
if (!name || !model || (requireApiKey && !apiKey)) {
|
||
const missingMsg = requireApiKey ? t('model.error.missing_required_with_api') : t('model.error.missing_required');
|
||
showError(missingMsg);
|
||
restoreSavingState();
|
||
return;
|
||
}
|
||
// 构造 payload,Base URL 为空时不传该字段;Ollama 不传 api_key
|
||
const baseConn = { model: model };
|
||
if (baseUrl) baseConn.base_url = baseUrl;
|
||
if (requireApiKey && apiKey) baseConn.api_key = apiKey;
|
||
// 读取“启用 Base64 URL”复选框,默认 false
|
||
const enableBase64UrlEl = document.getElementById('newModelEnableBase64Url');
|
||
const enableBase64Url = !!(enableBase64UrlEl && enableBase64UrlEl.checked);
|
||
const arkRegionInput = document.getElementById('newModelArkRegion');
|
||
const arkRegion = arkRegionInput ? arkRegionInput.value.trim() : '';
|
||
const connection = { base_conn_info: baseConn };
|
||
if (providerClass === 2) {
|
||
connection.ark = { region: arkRegion };
|
||
}
|
||
// 读取 OpenAI 扩展字段(仅 providerClass=1)
|
||
const openaiByAzureEl = document.getElementById('newModelOpenAIByAzure');
|
||
const openaiByAzure = !!(openaiByAzureEl && openaiByAzureEl.checked);
|
||
const openaiApiVersionEl = document.getElementById('newModelOpenAIApiVersion');
|
||
const openaiApiVersion = openaiApiVersionEl ? openaiApiVersionEl.value.trim() : '';
|
||
if (providerClass === 1 && (openaiByAzure || openaiApiVersion)) {
|
||
const openaiObj = {};
|
||
// 如果任一字段有值,则组装 openai 对象;by_azure 始终按当前开关状态传递
|
||
openaiObj.by_azure = openaiByAzure;
|
||
if (openaiApiVersion) openaiObj.api_version = openaiApiVersion;
|
||
connection.openai = openaiObj;
|
||
}
|
||
// 读取 Gemini 扩展字段(仅 providerClass=11),按需写入
|
||
const geminiBackendEl = document.getElementById('newModelGeminiBackend');
|
||
const geminiProjectEl = document.getElementById('newModelGeminiProject');
|
||
const geminiLocationEl = document.getElementById('newModelGeminiLocation');
|
||
const geminiBackendRaw = geminiBackendEl ? geminiBackendEl.value.trim() : '';
|
||
const hasGeminiBackend = geminiBackendRaw !== '';
|
||
const geminiBackend = hasGeminiBackend ? parseInt(geminiBackendRaw, 10) : NaN;
|
||
const geminiProject = geminiProjectEl ? geminiProjectEl.value.trim() : '';
|
||
const geminiLocation = geminiLocationEl ? geminiLocationEl.value.trim() : '';
|
||
if (providerClass === 11 && (hasGeminiBackend || geminiProject || geminiLocation)) {
|
||
const geminiObj = {};
|
||
if (hasGeminiBackend && !Number.isNaN(geminiBackend)) geminiObj.backend = geminiBackend;
|
||
if (geminiProject) geminiObj.project = geminiProject;
|
||
if (geminiLocation) geminiObj.location = geminiLocation;
|
||
connection.gemini = geminiObj;
|
||
}
|
||
const payload = {
|
||
model_name: name,
|
||
model_class: providerClass,
|
||
enable_base64_url: enableBase64Url,
|
||
connection
|
||
};
|
||
try {
|
||
const resp = await fetch('/api/admin/config/model/create', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
// 不依赖 resp.ok,统一按“JSON 优先,文本兜底”处理
|
||
let rawText = '';
|
||
try {
|
||
rawText = await resp.text();
|
||
} catch (e) {
|
||
showError(t('model.error.create_read_failed'));
|
||
return;
|
||
}
|
||
let data = null;
|
||
try {
|
||
data = JSON.parse(rawText);
|
||
} catch (e) {
|
||
data = null;
|
||
}
|
||
if (data && typeof data === 'object') {
|
||
if (handleApiCode(data)) {
|
||
return;
|
||
}
|
||
const code = Number(data.code);
|
||
if (code === 0) {
|
||
closeAddModelModal();
|
||
showSuccess(t('model.create.success'));
|
||
await loadModelManagementPage();
|
||
return;
|
||
}
|
||
showError(t('model.error.create_failed') + ': ' + (data?.msg || `HTTP ${resp.status}`));
|
||
return;
|
||
} else {
|
||
showError(`${t('model.error.create_failed')}: HTTP ${resp.status} ${rawText || ''}`);
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
showError(t('model.error.create_failed') + ': ' + error.message);
|
||
} finally {
|
||
restoreSavingState();
|
||
}
|
||
}
|
||
|
||
// 知识库配置页面:加载与渲染(支持本地预览示例数据兜底)
|
||
async function loadKnowledgeConfigPage() {
|
||
ensureConfigStylesInjected();
|
||
try {
|
||
showLoading();
|
||
const currentHost = window.location.origin;
|
||
const apiUrl = `${currentHost}/api/admin/config/knowledge/get`;
|
||
const resp = await fetch(apiUrl, { method: 'GET', headers: { 'Accept': 'application/json' } });
|
||
if (!resp.ok) {
|
||
throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
|
||
}
|
||
let data;
|
||
try {
|
||
data = await resp.json();
|
||
} catch (err) {
|
||
if (isLocalPreview()) {
|
||
renderDemo();
|
||
return;
|
||
}
|
||
showError(t('knowledge.error.load_failed_json'));
|
||
return;
|
||
}
|
||
if (handleApiCode(data)) {
|
||
return;
|
||
}
|
||
if (typeof data?.code === 'number' && data.code !== 0) {
|
||
if (isLocalPreview()) { renderDemo(); return; }
|
||
showError(t('knowledge.error.load_failed_msg', { msg: (data?.msg || t('error.api_error')) }));
|
||
return;
|
||
}
|
||
if (!data?.knowledge_config) {
|
||
if (isLocalPreview()) {
|
||
renderDemo();
|
||
return;
|
||
}
|
||
showError(t('knowledge.error.load_failed_empty'));
|
||
return;
|
||
}
|
||
renderKnowledgeConfig(data.knowledge_config);
|
||
} catch (error) {
|
||
console.error(t('knowledge.error.load_failed') + ':', error);
|
||
if (isLocalPreview()) {
|
||
renderDemo();
|
||
return;
|
||
}
|
||
showError(t('knowledge.error.load_failed_reason', { reason: error.message }));
|
||
}
|
||
|
||
function isLocalPreview() {
|
||
const host = window.location.hostname;
|
||
return host === 'localhost' || host === '127.0.0.1';
|
||
}
|
||
|
||
function renderDemo() {
|
||
// 不再注入示例数据,使用空配置进行页面渲染以便本地预览 UI
|
||
renderKnowledgeConfig({});
|
||
console.info(t('knowledge.log.local_preview_no_demo'));
|
||
}
|
||
}
|
||
|
||
function formatValue(val) {
|
||
if (val === undefined || val === null) return '';
|
||
return String(val);
|
||
}
|
||
|
||
function renderKnowledgeConfig(cfg) {
|
||
try {
|
||
const embedding = cfg?.embedding_config || {};
|
||
const rerank = cfg?.rerank_config || {};
|
||
const ocr = cfg?.ocr_config || {};
|
||
const parser = cfg?.parser_config || {};
|
||
const builtinModelId = cfg?.builtin_model_id ?? '';
|
||
|
||
const conn = embedding?.connection || {};
|
||
const baseConn = conn?.base_conn_info || {};
|
||
const embeddingInfo = conn?.embedding_info || {};
|
||
|
||
const initial = {
|
||
embedding_type: embedding?.type ?? '',
|
||
embedding_max_batch_size: Number(embedding?.max_batch_size ?? 100),
|
||
ark_base_url: baseConn?.base_url || '',
|
||
ark_api_key: baseConn?.api_key || '',
|
||
ark_region: conn?.ark?.region || '',
|
||
ark_model: baseConn?.model || '',
|
||
ark_embedding_dims: embeddingInfo?.dims ?? '0',
|
||
ark_embedding_api_type: conn?.ark?.embedding_api_type || conn?.ark?.api_type || '',
|
||
openai_base_url: baseConn?.base_url || '',
|
||
openai_api_key: baseConn?.api_key || '',
|
||
openai_model: baseConn?.model || '',
|
||
openai_embedding_dims: embeddingInfo?.dims ?? '0',
|
||
openai_embedding_request_dims: conn?.openai?.embedding_request_dims ?? conn?.openai?.request_dims ?? '',
|
||
openai_embedding_by_azure: Boolean(conn?.openai?.embedding_by_azure ?? conn?.openai?.by_azure),
|
||
openai_embedding_api_version: conn?.openai?.embedding_api_version ?? conn?.openai?.api_version ?? '',
|
||
ollama_base_url: baseConn?.base_url || '',
|
||
ollama_model: baseConn?.model || '',
|
||
ollama_embedding_dims: embeddingInfo?.dims ?? '0',
|
||
gemini_base_url: baseConn?.base_url || '',
|
||
gemini_api_key: baseConn?.api_key || '',
|
||
gemini_model: baseConn?.model || '',
|
||
gemini_embedding_dims: embeddingInfo?.dims ?? '0',
|
||
gemini_embedding_backend: conn?.gemini?.embedding_backend ?? conn?.gemini?.backend ?? '',
|
||
gemini_embedding_project: conn?.gemini?.embedding_project ?? conn?.gemini?.project ?? '',
|
||
gemini_embedding_location: conn?.gemini?.embedding_location ?? conn?.gemini?.location ?? '',
|
||
http_address: conn?.http?.address || '',
|
||
http_dims: embeddingInfo?.dims ?? '0',
|
||
rerank_type: rerank?.type ?? '',
|
||
vik_ak: rerank?.vikingdb_config?.ak || '',
|
||
vik_sk: rerank?.vikingdb_config?.sk || '',
|
||
vik_host: rerank?.vikingdb_config?.host || '',
|
||
vik_region: rerank?.vikingdb_config?.region || '',
|
||
vik_model: rerank?.vikingdb_config?.model || '',
|
||
ocr_type: ocr?.type ?? '',
|
||
ocr_ark_ak: ocr?.ark_ak || ocr?.volcengine_ak || '',
|
||
ocr_ark_sk: ocr?.ark_sk || ocr?.volcengine_sk || '',
|
||
ocr_paddleocr_api_url: ocr?.paddleocr_api_url || '',
|
||
parser_type: parser?.type ?? '',
|
||
parser_paddleocr_structure_api_url: parser?.paddleocr_structure_api_url || '',
|
||
builtin_model_id: builtinModelId ?? ''
|
||
};
|
||
|
||
if (initial.embedding_type==0) {
|
||
initial.openai_api_key ='';
|
||
initial.openai_base_url ='';
|
||
initial.openai_model ='';
|
||
initial.openai_embedding_dims ='';
|
||
initial.ollama_base_url ='';
|
||
initial.ollama_model ='';
|
||
initial.ollama_embedding_dims ='';
|
||
initial.gemini_base_url ='';
|
||
initial.gemini_api_key ='';
|
||
initial.gemini_model ='';
|
||
initial.gemini_embedding_dims ='';
|
||
initial.http_address ='';
|
||
initial.http_dims ='';
|
||
}else if (initial.embedding_type==1) {
|
||
initial.ark_base_url ='';
|
||
initial.ark_api_key ='';
|
||
initial.ark_region ='';
|
||
initial.ark_model ='';
|
||
initial.ark_embedding_dims ='';
|
||
initial.ollama_base_url ='';
|
||
initial.ollama_model ='';
|
||
initial.ollama_embedding_dims ='';
|
||
initial.gemini_base_url ='';
|
||
initial.gemini_api_key ='';
|
||
initial.gemini_model ='';
|
||
initial.gemini_embedding_dims ='';
|
||
initial.http_address ='';
|
||
initial.http_dims ='';
|
||
} else if (initial.embedding_type==2) {
|
||
initial.openai_api_key ='';
|
||
initial.openai_base_url ='';
|
||
initial.openai_model ='';
|
||
initial.openai_embedding_dims ='';
|
||
initial.ark_base_url ='';
|
||
initial.ark_api_key ='';
|
||
initial.ark_region ='';
|
||
initial.ark_model ='';
|
||
initial.ark_embedding_dims ='';
|
||
initial.gemini_base_url ='';
|
||
initial.gemini_api_key ='';
|
||
initial.gemini_model ='';
|
||
initial.http_address ='';
|
||
initial.http_dims ='';
|
||
} else if (initial.embedding_type==3) {
|
||
initial.openai_api_key ='';
|
||
initial.openai_base_url ='';
|
||
initial.openai_model ='';
|
||
initial.openai_embedding_dims ='';
|
||
initial.ark_base_url ='';
|
||
initial.ark_api_key ='';
|
||
initial.ark_region ='';
|
||
initial.ark_model ='';
|
||
initial.ark_embedding_dims ='';
|
||
initial.ollama_base_url ='';
|
||
initial.ollama_model ='';
|
||
initial.ollama_embedding_dims ='';
|
||
initial.http_address ='';
|
||
initial.http_dims ='';
|
||
} else if (initial.embedding_type==4) {
|
||
initial.openai_api_key ='';
|
||
initial.openai_base_url ='';
|
||
initial.openai_model ='';
|
||
initial.openai_embedding_dims ='';
|
||
initial.ark_base_url ='';
|
||
initial.ark_api_key ='';
|
||
initial.ark_region ='';
|
||
initial.ark_model ='';
|
||
initial.ark_embedding_dims ='';
|
||
initial.ollama_base_url ='';
|
||
initial.ollama_model ='';
|
||
initial.ollama_embedding_dims ='';
|
||
initial.gemini_base_url ='';
|
||
initial.gemini_api_key ='';
|
||
initial.gemini_model ='';
|
||
initial.gemini_embedding_dims ='';
|
||
}
|
||
|
||
const inputStyle = "width:100%;padding:8px;border:1px solid #e1e8ed;border-radius:6px;font-size:12px;";
|
||
const labelStyle = "font-size:12px;color:#374151;margin-bottom:6px;";
|
||
const groupGrid = "display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;";
|
||
|
||
const html = `
|
||
<div style="padding: 30px;">
|
||
<div style="display:grid;grid-template-columns:1fr;gap:20px;">
|
||
|
||
<!-- Group 1: Embedding 配置 -->
|
||
<div style="background:white;border-radius:12px;box-shadow:0 2px 10px rgba(0,0,0,0.08);border:1px solid #e1e8ed;overflow:hidden;">
|
||
<div style="padding:16px;border-bottom:1px solid #f1f3f4;background:#f8f9fa;">
|
||
<div style="font-weight:600;color:#2c3e50;">${t('knowledge.section.embedding')}</div>
|
||
</div>
|
||
<div style="padding:16px;">
|
||
<div style="${groupGrid}">
|
||
<div>
|
||
<div style="${labelStyle}">Type</div>
|
||
<select id="kb_embedding_type" style="${inputStyle}" onchange="onEmbeddingTypeChange()">
|
||
<option value="0" ${Number(initial.embedding_type) === 0 ? 'selected' : ''}>Ark</option>
|
||
<option value="1" ${Number(initial.embedding_type) === 1 ? 'selected' : ''}>OpenAI</option>
|
||
<option value="2" ${Number(initial.embedding_type) === 2 ? 'selected' : ''}>Ollama</option>
|
||
<option value="3" ${Number(initial.embedding_type) === 3 ? 'selected' : ''}>Gemini</option>
|
||
<option value="4" ${Number(initial.embedding_type) === 4 ? 'selected' : ''}>HTTP</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Max Batch Size</div>
|
||
<input id="kb_embedding_max_batch_size" type="number" style="${inputStyle}" value="${initial.embedding_max_batch_size}" />
|
||
</div>
|
||
</div>
|
||
|
||
<div id="kb_conn_ark" style="margin-top:12px;border:1px solid #f1f3f4;border-radius:8px;padding:12px;">
|
||
<div style="font-weight:600;color:#2c3e50;margin-bottom:8px;">${t('knowledge.connection', { name: 'Ark' })}</div>
|
||
<div style="${groupGrid}">
|
||
<div>
|
||
<div style="${labelStyle}">Base URL<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_ark_base_url" type="text" style="${inputStyle}" value="${initial.ark_base_url}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">API Key<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_ark_api_key" type="text" style="${inputStyle}" value="${initial.ark_api_key}" />
|
||
</div>
|
||
<div style="display:none;">
|
||
<div style="${labelStyle}">Region</div>
|
||
<input id="kb_ark_region" type="text" style="${inputStyle}" value="${initial.ark_region}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Model<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_ark_model" type="text" style="${inputStyle}" value="${initial.ark_model}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Dims</div>
|
||
<input id="kb_ark_embedding_dims" type="number" style="${inputStyle}" value="${initial.ark_embedding_dims}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">API Type</div>
|
||
<select id="kb_ark_embedding_api_type" style="${inputStyle}">
|
||
<option value="text_api" ${initial.ark_embedding_api_type === 'text_api' ? 'selected' : ''}>text_api</option>
|
||
<option value="multi_modal_api" ${initial.ark_embedding_api_type === 'multi_modal_api' ? 'selected' : ''}>multi_modal_api</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="kb_conn_openai" style="margin-top:12px;border:1px solid #f1f3f4;border-radius:8px;padding:12px;">
|
||
<div style="font-weight:600;color:#2c3e50;margin-bottom:8px;">${t('knowledge.connection', { name: 'OpenAI' })}</div>
|
||
<div style="${groupGrid}">
|
||
<!-- 必填字段优先显示:Base URL、Model、API Key、Dims(红点) -->
|
||
<div>
|
||
<div style="${labelStyle}">Base URL<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_openai_base_url" type="text" style="${inputStyle}" value="${initial.openai_base_url}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Model<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_openai_model" type="text" style="${inputStyle}" value="${initial.openai_model}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">API Key<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_openai_api_key" type="text" style="${inputStyle}" value="${initial.openai_api_key}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Dims</div>
|
||
<input id="kb_openai_embedding_dims" type="number" style="${inputStyle}" value="${initial.openai_embedding_dims}" />
|
||
</div>
|
||
<!-- 选填字段靠后显示:By Azure、API Version、Request Dims -->
|
||
<div>
|
||
<div style="${labelStyle}">By Azure</div>
|
||
<input id="kb_openai_embedding_by_azure" type="checkbox" ${initial.openai_embedding_by_azure ? 'checked' : ''} />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Embedding API Version</div>
|
||
<input id="kb_openai_embedding_api_version" type="text" style="${inputStyle}" value="${initial.openai_embedding_api_version}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Embedding Request Dims</div>
|
||
<input id="kb_openai_embedding_request_dims" type="number" style="${inputStyle}" value="${initial.openai_embedding_request_dims}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="kb_conn_ollama" style="margin-top:12px;border:1px solid #f1f3f4;border-radius:8px;padding:12px;">
|
||
<div style="font-weight:600;color:#2c3e50;margin-bottom:8px;">${t('knowledge.connection', { name: 'Ollama' })}</div>
|
||
<div style="${groupGrid}">
|
||
<div>
|
||
<div style="${labelStyle}">Base URL<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_ollama_base_url" type="text" style="${inputStyle}" value="${initial.ollama_base_url}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Model<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_ollama_model" type="text" style="${inputStyle}" value="${initial.ollama_model}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Dims</div>
|
||
<input id="kb_ollama_embedding_dims" type="number" style="${inputStyle}" value="${initial.ollama_embedding_dims}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="kb_conn_gemini" style="margin-top:12px;border:1px solid #f1f3f4;border-radius:8px;padding:12px;">
|
||
<div style="font-weight:600;color:#2c3e50;margin-bottom:8px;">${t('knowledge.connection', { name: 'Gemini' })}</div>
|
||
<div style="${groupGrid}">
|
||
<div>
|
||
<div style="${labelStyle}">Base URL<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_gemini_base_url" type="text" style="${inputStyle}" value="${initial.gemini_base_url}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">API Key<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_gemini_api_key" type="text" style="${inputStyle}" value="${initial.gemini_api_key}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Model<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_gemini_model" type="text" style="${inputStyle}" value="${initial.gemini_model}" />
|
||
</div>
|
||
|
||
<div>
|
||
<div style="${labelStyle}">Backend<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_gemini_embedding_backend" type="number" style="${inputStyle}" value="${initial.gemini_embedding_backend}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Dims</div>
|
||
<input id="kb_gemini_embedding_dims" type="number" style="${inputStyle}" value="${initial.gemini_embedding_dims}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Project</div>
|
||
<input id="kb_gemini_embedding_project" type="text" style="${inputStyle}" value="${initial.gemini_embedding_project}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Location</div>
|
||
<input id="kb_gemini_embedding_location" type="text" style="${inputStyle}" value="${initial.gemini_embedding_location}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="kb_conn_http" style="margin-top:12px;border:1px solid #f1f3f4;border-radius:8px;padding:12px;">
|
||
<div style="font-weight:600;color:#2c3e50;margin-bottom:8px;">${t('knowledge.connection', { name: 'HTTP' })}</div>
|
||
<div style="${groupGrid}">
|
||
<div>
|
||
<div style="${labelStyle}">Address<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_http_address" type="text" style="${inputStyle}" value="${initial.http_address}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Dims</div>
|
||
<input id="kb_http_dims" type="number" style="${inputStyle}" value="${initial.http_dims}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Group 2: Rerank 配置 -->
|
||
<div style="background:white;border-radius:12px;box-shadow:0 2px 10px rgba(0,0,0,0.08);border:1px solid #e1e8ed;overflow:hidden;">
|
||
<div style="padding:16px;border-bottom:1px solid #f1f3f4;background:#f8f9fa;">
|
||
<div style="font-weight:600;color:#2c3e50;">${t('knowledge.section.rerank')}</div>
|
||
</div>
|
||
<div style="padding:16px;">
|
||
<div style="${groupGrid}">
|
||
<div>
|
||
<div style="${labelStyle}">Type</div>
|
||
<select id="kb_rerank_type" style="${inputStyle}" onchange="onRerankTypeChange()">
|
||
<option value="0" ${Number(initial.rerank_type) === 0 ? 'selected' : ''}>VikingDB</option>
|
||
<option value="1" ${Number(initial.rerank_type) === 1 ? 'selected' : ''}>RRF</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div id="kb_conn_vikingdb" style="margin-top:12px;border:1px solid #f1f3f4;border-radius:8px;padding:12px;">
|
||
<div style="font-weight:600;color:#2c3e50;margin-bottom:8px;">VikingDB</div>
|
||
<div style="${groupGrid}">
|
||
<div>
|
||
<div style="${labelStyle}">AK<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_vik_ak" type="text" style="${inputStyle}" value="${initial.vik_ak}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">SK<span style="display:inline-block;width:6px;height:6px;background:#e74c3c;border-radius:50%;margin-left:6px;"></span></div>
|
||
<input id="kb_vik_sk" type="text" style="${inputStyle}" value="${initial.vik_sk}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Host</div>
|
||
<input id="kb_vik_host" type="text" style="${inputStyle}" value="${initial.vik_host}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Region</div>
|
||
<input id="kb_vik_region" type="text" style="${inputStyle}" value="${initial.vik_region}" />
|
||
</div>
|
||
<div>
|
||
<div style="${labelStyle}">Model</div>
|
||
<input id="kb_vik_model" type="text" style="${inputStyle}" value="${initial.vik_model}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Group 3: OCR 配置 -->
|
||
<div style="background:white;border-radius:12px;box-shadow:0 2px 10px rgba(0,0,0,0.08);border:1px solid #e1e8ed;overflow:hidden;">
|
||
<div style="padding:16px;border-bottom:1px solid #f1f3f4;background:#f8f9fa;">
|
||
<div style="font-weight:600;color:#2c3e50;">${t('knowledge.section.ocr')}</div>
|
||
</div>
|
||
<div style="padding:16px;">
|
||
<div style="${groupGrid}">
|
||
<div>
|
||
<div style="${labelStyle}">Type</div>
|
||
<select id="kb_ocr_type" style="${inputStyle}" onchange="onOcrTypeChange()">
|
||
<option value="0" ${Number(initial.ocr_type) === 0 ? 'selected' : ''}>Volcengine</option>
|
||
<option value="1" ${Number(initial.ocr_type) === 1 ? 'selected' : ''}>Paddleocr</option>
|
||
</select>
|
||
</div>
|
||
<div id="kb_conn_ocr_volcengine">
|
||
<div style="${labelStyle}">Volcengine AK</div>
|
||
<input id="kb_ocr_ark_ak" type="text" style="${inputStyle}" value="${initial.ocr_ark_ak}" />
|
||
<div style="${labelStyle};margin-top:12px;">Volcengine SK</div>
|
||
<input id="kb_ocr_ark_sk" type="text" style="${inputStyle}" value="${initial.ocr_ark_sk}" />
|
||
</div>
|
||
<div id="kb_conn_ocr_paddleocr">
|
||
<div style="${labelStyle}">PaddleOCR API URL</div>
|
||
<input id="kb_ocr_paddleocr_api_url" type="text" style="${inputStyle}" value="${initial.ocr_paddleocr_api_url}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Group 4: Parser 配置 -->
|
||
<div style="background:white;border-radius:12px;box-shadow:0 2px 10px rgba(0,0,0,0.08);border:1px solid #e1e8ed;overflow:hidden;">
|
||
<div style="padding:16px;border-bottom:1px solid #f1f3f4;background:#f8f9fa;">
|
||
<div style="font-weight:600;color:#2c3e50;">${t('knowledge.section.parser')}</div>
|
||
</div>
|
||
<div style="padding:16px;">
|
||
<div style="${groupGrid}">
|
||
<div>
|
||
<div style="${labelStyle}">Type</div>
|
||
<select id="kb_parser_type" style="${inputStyle}" onchange="onParserTypeChange()">
|
||
<option value="0" ${Number(initial.parser_type) === 0 ? 'selected' : ''}>Builtin</option>
|
||
<option value="1" ${Number(initial.parser_type) === 1 ? 'selected' : ''}>Paddleocr</option>
|
||
</select>
|
||
</div>
|
||
<div id="kb_conn_parser_paddleocr">
|
||
<div style="${labelStyle}">PaddleOCR API URL</div>
|
||
<input id="kb_parser_paddleocr_structure_api_url" type="text" style="${inputStyle}" value="${initial.parser_paddleocr_structure_api_url}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Group 5: 内置模型配置 -->
|
||
<div id="kb_builtin_model_group" style="background:white;border-radius:12px;box-shadow:0 2px 10px rgba(0,0,0,0.08);border:1px solid #e1e8ed;overflow:hidden;">
|
||
<div style="padding:16px;border-bottom:1px solid #f1f3f4;background:#f8f9fa;">
|
||
<div style="font-weight:600;color:#2c3e50;">${t('knowledge.section.builtin_model')}</div>
|
||
</div>
|
||
<div style="padding:16px;">
|
||
<div style="${groupGrid}">
|
||
<div>
|
||
<div style="${labelStyle}">${t('knowledge.label.choose_builtin_model')}</div>
|
||
<div id="kb_builtin_model_select" style="position:relative;">
|
||
<div id="kb_builtin_model_selected" style="display:flex;align-items:center;gap:8px;border:1px solid #e1e8ed;border-radius:8px;padding:8px;cursor:pointer;">
|
||
<img id="kb_builtin_model_selected_icon" src="" alt="" style="width:20px;height:20px;border-radius:4px;object-fit:contain;display:none;">
|
||
<span id="kb_builtin_model_selected_name" style="color:#2c3e50;">${t('knowledge.placeholder.select_model')}</span>
|
||
<span style="margin-left:auto;color:#7b8a8b;">▼</span>
|
||
</div>
|
||
<div id="kb_builtin_model_dropdown" style="margin-top:8px;background:#fff;border:1px solid #e1e8ed;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.08);max-height:240px;overflow:auto;display:none;z-index:10;">
|
||
<div id="kb_builtin_model_loading" style="padding:8px;color:#7b8a8b;">${t('knowledge.loading.model_list')}</div>
|
||
</div>
|
||
</div>
|
||
<input id="kb_builtin_model_id" type="hidden" value="${initial.builtin_model_id}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
<div class="action-buttons">
|
||
<button type="button" class="btn btn-primary" onclick="saveKnowledgeConfig()">💾 ${t('btn.save')}</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
pageContent.innerHTML = html;
|
||
try {
|
||
const builtinGroup = document.getElementById('kb_builtin_model_group');
|
||
if (builtinGroup && builtinGroup.parentElement) {
|
||
builtinGroup.parentElement.insertBefore(builtinGroup, builtinGroup.parentElement.firstElementChild);
|
||
}
|
||
} catch (_) {}
|
||
try { setEmbeddingTypeUI(Number(initial.embedding_type ?? 0)); } catch (_) {}
|
||
try { setRerankTypeUI(Number(initial.rerank_type ?? 0)); } catch (_) {}
|
||
try { setOcrTypeUI(Number(initial.ocr_type ?? 0)); } catch (_) {}
|
||
try { setParserTypeUI(Number(initial.parser_type ?? 0)); } catch (_) {}
|
||
try { initBuiltinModelSelect(initial.builtin_model_id); } catch (_) {}
|
||
} catch (e) {
|
||
console.error(t('knowledge.error.render_failed') + ':', e);
|
||
showError(t('knowledge.error.render_failed') + ': ' + e.message);
|
||
}
|
||
}
|
||
|
||
// 显示加载状态}
|
||
|
||
// 内置模型下拉:初始化与渲染
|
||
function initBuiltinModelSelect(initialModelId) {
|
||
try {
|
||
const container = document.getElementById('kb_builtin_model_select');
|
||
const selected = document.getElementById('kb_builtin_model_selected');
|
||
const nameEl = document.getElementById('kb_builtin_model_selected_name');
|
||
const iconEl = document.getElementById('kb_builtin_model_selected_icon');
|
||
const dropdownEl = document.getElementById('kb_builtin_model_dropdown');
|
||
const hiddenInput = document.getElementById('kb_builtin_model_id');
|
||
if (!container || !selected || !dropdownEl || !hiddenInput) { return; }
|
||
|
||
const normalizedInitialId = (initialModelId === undefined || initialModelId === null) ? undefined : String(initialModelId).trim();
|
||
|
||
// toggle dropdown
|
||
selected.onclick = function() {
|
||
const isOpen = dropdownEl.style.display !== 'none';
|
||
dropdownEl.style.display = isOpen ? 'none' : 'block';
|
||
};
|
||
document.addEventListener('click', function(e) {
|
||
if (!container.contains(e.target)) {
|
||
dropdownEl.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
dropdownEl.innerHTML = '<div id="kb_builtin_model_loading" style="padding:8px;color:#7b8a8b;">' + t('knowledge.loading.model_list') + '</div>';
|
||
|
||
fetch('/api/admin/config/model/list', { method: 'GET' })
|
||
.then(resp => resp.json())
|
||
.then(data => {
|
||
if (handleApiCode(data)) {
|
||
return;
|
||
}
|
||
const groups = data?.provider_model_list ?? data?.ProviderModelList ?? [];
|
||
dropdownEl.innerHTML = '';
|
||
if (!Array.isArray(groups) || groups.length === 0) {
|
||
dropdownEl.innerHTML = '<div style="padding:8px;color:#bdc3c7;">' + t('knowledge.empty.model_list') + '</div>';
|
||
return;
|
||
}
|
||
|
||
let selectedFound = false;
|
||
|
||
groups.forEach(group => {
|
||
const provider = group.provider ?? group.Provider;
|
||
const icon = provider?.icon_url ?? provider?.IconURL ?? '';
|
||
const models = group.model_list ?? group.ModelList ?? [];
|
||
models.forEach(m => {
|
||
const id = m.id ?? m.ID;
|
||
const name = (m.display_info?.name) ?? (m.DisplayInfo?.Name);
|
||
const opt = document.createElement('div');
|
||
opt.style.display = 'flex';
|
||
opt.style.alignItems = 'center';
|
||
opt.style.gap = '8px';
|
||
opt.style.padding = '8px';
|
||
opt.style.cursor = 'pointer';
|
||
opt.style.borderBottom = '1px solid #f1f3f4';
|
||
opt.onmouseenter = function(){ opt.style.background = '#f8f9fa'; };
|
||
opt.onmouseleave = function(){ opt.style.background = '#fff'; };
|
||
|
||
const img = document.createElement('img');
|
||
img.src = icon || '';
|
||
img.alt = '';
|
||
img.style.width = '20px';
|
||
img.style.height = '20px';
|
||
img.style.borderRadius = '4px';
|
||
img.style.objectFit = 'contain';
|
||
img.style.display = icon ? 'block' : 'none';
|
||
|
||
const span = document.createElement('span');
|
||
span.textContent = name || t('knowledge.placeholder.model_fallback', { id });
|
||
|
||
opt.appendChild(img);
|
||
opt.appendChild(span);
|
||
|
||
opt.onclick = function() {
|
||
hiddenInput.value = String(id);
|
||
nameEl.textContent = span.textContent;
|
||
if (icon) { iconEl.src = icon; iconEl.style.display = 'block'; } else { iconEl.style.display = 'none'; }
|
||
dropdownEl.style.display = 'none';
|
||
};
|
||
|
||
dropdownEl.appendChild(opt);
|
||
|
||
if (!selectedFound && normalizedInitialId !== undefined && normalizedInitialId !== '' && Number(normalizedInitialId) === Number(id)) {
|
||
selectedFound = true;
|
||
hiddenInput.value = String(id);
|
||
nameEl.textContent = span.textContent;
|
||
if (icon) { iconEl.src = icon; iconEl.style.display = 'block'; } else { iconEl.style.display = 'none'; }
|
||
}
|
||
});
|
||
});
|
||
|
||
if (!selectedFound) {
|
||
const first = dropdownEl.firstElementChild;
|
||
if (first) {
|
||
first.click();
|
||
} else {
|
||
nameEl.textContent = t('knowledge.tip.setup_model_first');
|
||
iconEl.style.display = 'none';
|
||
}
|
||
}
|
||
})
|
||
.catch(err => {
|
||
dropdownEl.innerHTML = '<div style="padding:8px;color:#e74c3c;">' + t('knowledge.error.model_list_load_failed') + '</div>';
|
||
console.error(t('knowledge.error.model_list_load_failed') + ':', err);
|
||
});
|
||
|
||
} catch (e) {
|
||
console.error('初始化内置模型下拉失败:', e);
|
||
}
|
||
}
|
||
|
||
function buildKnowledgePayloadFromInputs() {
|
||
const val = (id) => {
|
||
const el = document.getElementById(id);
|
||
return el ? el.value : '';
|
||
};
|
||
const checked = (id) => {
|
||
const el = document.getElementById(id);
|
||
return el ? !!el.checked : false;
|
||
};
|
||
const toNum = (v) => {
|
||
const n = Number(v);
|
||
return isNaN(n) ? undefined : n;
|
||
};
|
||
|
||
const typeVal = toNum(val('kb_embedding_type'));
|
||
// 根据选择的 embedding type,抽取 base_conn_info 与 dims
|
||
const baseConnInfo = (() => {
|
||
switch (typeVal) {
|
||
case 0: // Ark
|
||
return {
|
||
base_url: val('kb_ark_base_url'),
|
||
api_key: val('kb_ark_api_key'),
|
||
model: val('kb_ark_model')
|
||
};
|
||
case 1: // OpenAI
|
||
return {
|
||
base_url: val('kb_openai_base_url'),
|
||
api_key: val('kb_openai_api_key'),
|
||
model: val('kb_openai_model')
|
||
};
|
||
case 2: // Ollama
|
||
return {
|
||
base_url: val('kb_ollama_base_url'),
|
||
model: val('kb_ollama_model')
|
||
};
|
||
case 3: // Gemini
|
||
return {
|
||
base_url: val('kb_gemini_base_url'),
|
||
api_key: val('kb_gemini_api_key'),
|
||
model: val('kb_gemini_model')
|
||
};
|
||
case 4: // HTTP
|
||
default:
|
||
return { base_url: '', api_key: '', model: '' };
|
||
}
|
||
})();
|
||
|
||
const dimsVal = (() => {
|
||
switch (typeVal) {
|
||
case 0: return toNum(val('kb_ark_embedding_dims'));
|
||
case 1: return toNum(val('kb_openai_embedding_dims'));
|
||
case 2: return toNum(val('kb_ollama_embedding_dims'));
|
||
case 3: return toNum(val('kb_gemini_embedding_dims'));
|
||
case 4: return toNum(val('kb_http_dims'));
|
||
default: return undefined;
|
||
}
|
||
})();
|
||
|
||
return {
|
||
knowledge_config: {
|
||
embedding_config: {
|
||
type: typeVal,
|
||
max_batch_size: toNum(val('kb_embedding_max_batch_size')) ?? 100,
|
||
connection: {
|
||
base_conn_info: baseConnInfo,
|
||
embedding_info: { dims: dimsVal },
|
||
ark: {
|
||
api_type: val('kb_ark_embedding_api_type')
|
||
},
|
||
openai: {
|
||
request_dims: toNum(val('kb_openai_embedding_request_dims')),
|
||
by_azure: checked('kb_openai_embedding_by_azure'),
|
||
api_version: val('kb_openai_embedding_api_version')
|
||
},
|
||
gemini: {
|
||
backend: toNum(val('kb_gemini_embedding_backend')),
|
||
project: val('kb_gemini_embedding_project'),
|
||
location: val('kb_gemini_embedding_location')
|
||
},
|
||
http: {
|
||
address: val('kb_http_address')
|
||
}
|
||
}
|
||
},
|
||
rerank_config: {
|
||
type: toNum(val('kb_rerank_type')),
|
||
vikingdb_config: {
|
||
ak: val('kb_vik_ak'),
|
||
sk: val('kb_vik_sk'),
|
||
host: val('kb_vik_host'),
|
||
region: val('kb_vik_region'),
|
||
model: val('kb_vik_model')
|
||
}
|
||
},
|
||
ocr_config: {
|
||
type: toNum(val('kb_ocr_type')),
|
||
volcengine_ak: val('kb_ocr_ark_ak'),
|
||
volcengine_sk: val('kb_ocr_ark_sk'),
|
||
paddleocr_api_url: val('kb_ocr_paddleocr_api_url')
|
||
},
|
||
parser_config: {
|
||
type: toNum(val('kb_parser_type')),
|
||
paddleocr_structure_api_url: val('kb_parser_paddleocr_structure_api_url')
|
||
},
|
||
builtin_model_id: toNum(val('kb_builtin_model_id'))
|
||
}
|
||
};
|
||
}
|
||
|
||
function saveKnowledgeConfig() {
|
||
try {
|
||
const payload = buildKnowledgePayloadFromInputs();
|
||
// 初始化 Rerank 配置显示状态(防止未选择时错误展示)
|
||
try { setRerankTypeUI(Number(document.getElementById('kb_rerank_type')?.value || 0)); } catch (_) {}
|
||
|
||
// Embedding 必填项校验(不同类型不同必填要求)
|
||
const typeVal = Number(document.getElementById('kb_embedding_type')?.value);
|
||
const get = (id) => (document.getElementById(id)?.value || '').trim();
|
||
if (typeVal === 0) {
|
||
// Ark: Base URL、API Key、Model、Dims 必填,API Type 可选
|
||
const dimsVal = Number(document.getElementById('kb_ark_embedding_dims')?.value);
|
||
const missing = [];
|
||
if (!get('kb_ark_base_url')) missing.push('Base URL');
|
||
if (!get('kb_ark_api_key')) missing.push('API Key');
|
||
if (!get('kb_ark_model')) missing.push('Model');
|
||
|
||
if (missing.length > 0) {
|
||
showError(t('error.missing_required', { list: missing.join(', ') }));
|
||
return;
|
||
}
|
||
} else if (typeVal === 1) {
|
||
// OpenAI: Base URL、Model、API Key、Dims 必填
|
||
const dimsVal = Number(document.getElementById('kb_openai_embedding_dims')?.value);
|
||
const missing = [];
|
||
if (!get('kb_openai_base_url')) missing.push('Base URL');
|
||
if (!get('kb_openai_model')) missing.push('Model');
|
||
if (!get('kb_openai_api_key')) missing.push('API Key');
|
||
if (missing.length > 0) {
|
||
showError(t('error.missing_required', { list: missing.join(', ') }));
|
||
return;
|
||
}
|
||
} else if (typeVal === 2) {
|
||
// Ollama: Base URL、Model、Dims 必填
|
||
const dimsVal = Number(document.getElementById('kb_ollama_embedding_dims')?.value);
|
||
const missing = [];
|
||
if (!get('kb_ollama_base_url')) missing.push('Base URL');
|
||
if (!get('kb_ollama_model')) missing.push('Model');
|
||
if (missing.length > 0) {
|
||
showError(t('error.missing_required', { list: missing.join(', ') }));
|
||
return;
|
||
}
|
||
} else if (typeVal === 3) {
|
||
// Gemini: Base URL、API Key、Model、Dims、Backend 必填;Project、Location 选填
|
||
const dimsVal = Number(document.getElementById('kb_gemini_embedding_dims')?.value);
|
||
const backendStr = (document.getElementById('kb_gemini_embedding_backend')?.value || '').trim();
|
||
const missing = [];
|
||
if (!get('kb_gemini_base_url')) missing.push('Base URL');
|
||
if (!get('kb_gemini_api_key')) missing.push('API Key');
|
||
if (!get('kb_gemini_model')) missing.push('Model');
|
||
if (!backendStr) missing.push('Backend');
|
||
if (missing.length > 0) {
|
||
showError(t('error.missing_required', { list: missing.join(', ') }));
|
||
return;
|
||
}
|
||
} else if (typeVal === 4) {
|
||
// HTTP: Address、Dims 必填
|
||
const dimsVal = Number(document.getElementById('kb_http_dims')?.value);
|
||
const missing = [];
|
||
if (!get('kb_http_address')) missing.push('Address');
|
||
if (missing.length > 0) {
|
||
showError(t('error.missing_required', { list: missing.join(', ') }));
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Rerank 必填项校验:VikingDB 的 AK、SK 必填,其它项选填
|
||
try {
|
||
const rerankTypeVal = Number(document.getElementById('kb_rerank_type')?.value);
|
||
if (rerankTypeVal === 0) {
|
||
const missing = [];
|
||
if (!get('kb_vik_ak')) missing.push('AK');
|
||
if (!get('kb_vik_sk')) missing.push('SK');
|
||
if (missing.length > 0) {
|
||
showError(t('error.missing_required', { list: missing.join(', ') }));
|
||
return;
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
|
||
// showOverlay(t('overlay.saving'));
|
||
fetch('/api/admin/config/knowledge/save', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
}).then(async (resp) => {
|
||
let bodyText = '';
|
||
try { bodyText = await resp.text(); } catch (_) {}
|
||
let data = null;
|
||
try { data = JSON.parse(bodyText); } catch (_) { data = null; }
|
||
if (data && typeof data === 'object') {
|
||
if (handleApiCode(data)) { hideOverlay(); return; }
|
||
if (Number(data.code) === 0) { hideOverlay(); showSuccess(t('success.saved_restart_required')); return; }
|
||
hideOverlay(); showError(t('error.save_failed_reason', { reason: (data?.msg || t('error.api_error')) })); return;
|
||
}
|
||
if (!resp.ok) {
|
||
hideOverlay();
|
||
throw new Error(bodyText || ('HTTP ' + resp.status));
|
||
}
|
||
hideOverlay();
|
||
showSuccess(t('success.saved_restart_required'));
|
||
}).catch((err) => {
|
||
hideOverlay();
|
||
console.error('Failed to save knowledge configuration:', err);
|
||
showError(t('error.save_failed_reason', { reason: (err?.message || String(err)) }));
|
||
});
|
||
} catch (e) {
|
||
hideOverlay();
|
||
console.error('Knowledge configuration save exception:', e);
|
||
showError(t('error.save_failed_reason', { reason: e.message }));
|
||
}
|
||
}
|
||
|
||
function setEmbeddingTypeUI(typeVal) {
|
||
const ids = ['kb_conn_ark','kb_conn_openai','kb_conn_ollama','kb_conn_gemini','kb_conn_http'];
|
||
ids.forEach((id, idx) => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.style.display = (Number(typeVal) === idx ? 'block' : 'none');
|
||
});
|
||
}
|
||
|
||
function setRerankTypeUI(typeVal) {
|
||
try {
|
||
const vik = document.getElementById('kb_conn_vikingdb');
|
||
if (!vik) return;
|
||
vik.style.display = Number(typeVal) === 0 ? '' : 'none';
|
||
} catch (_) {}
|
||
}
|
||
function onRerankTypeChange() {
|
||
try {
|
||
const v = Number(document.getElementById('kb_rerank_type')?.value);
|
||
setRerankTypeUI(v);
|
||
} catch (_) {}
|
||
}
|
||
|
||
function setOcrTypeUI(typeVal) {
|
||
try {
|
||
const volc = document.getElementById('kb_conn_ocr_volcengine');
|
||
const pad = document.getElementById('kb_conn_ocr_paddleocr');
|
||
if (!volc || !pad) return;
|
||
const t = Number(typeVal);
|
||
volc.style.display = t === 0 ? '' : 'none';
|
||
pad.style.display = t === 1 ? '' : 'none';
|
||
} catch (_) {}
|
||
}
|
||
function onOcrTypeChange() {
|
||
try {
|
||
const v = Number(document.getElementById('kb_ocr_type')?.value);
|
||
setOcrTypeUI(v);
|
||
} catch (_) {}
|
||
}
|
||
|
||
function setParserTypeUI(typeVal) {
|
||
try {
|
||
const pad = document.getElementById('kb_conn_parser_paddleocr');
|
||
if (!pad) return;
|
||
pad.style.display = Number(typeVal) === 1 ? '' : 'none';
|
||
} catch (_) {}
|
||
}
|
||
function onParserTypeChange() {
|
||
try {
|
||
const v = Number(document.getElementById('kb_parser_type')?.value);
|
||
setParserTypeUI(v);
|
||
} catch (_) {}
|
||
}
|
||
|
||
function onEmbeddingTypeChange() {
|
||
const sel = document.getElementById('kb_embedding_type');
|
||
const val = sel ? Number(sel.value) : 0;
|
||
setEmbeddingTypeUI(val);
|
||
}
|
||
|
||
function showLoading() {
|
||
pageContent.innerHTML = `
|
||
<div class="loading">
|
||
<div class="loading-spinner"></div>
|
||
<span>${t('loading')}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 统一成功提示(透明浮层)
|
||
function showSuccess(message) {
|
||
showOverlay(message || t('success.generic'), 'success');
|
||
}
|
||
|
||
// 统一错误提示(透明浮层)
|
||
function showError(message) {
|
||
showOverlay(message || t('error.generic'), 'error');
|
||
}
|
||
|
||
// 透明浮层:显示(与 config.html 保持一致)
|
||
function showOverlay(message, type) {
|
||
const overlay = document.getElementById('overlay');
|
||
const overlayContent = document.getElementById('overlayContent');
|
||
const overlayMessage = document.getElementById('overlayMessage');
|
||
if (!overlay || !overlayContent || !overlayMessage) {
|
||
alert(String(message || t('error.generic')));
|
||
return;
|
||
}
|
||
overlayMessage.textContent = String(message || '');
|
||
overlayContent.className = `overlay-content ${type === 'success' ? 'success' : 'error'}`;
|
||
overlay.classList.add('show');
|
||
// 背景点击关闭
|
||
overlay.onclick = function(e) {
|
||
if (e.target === overlay) {
|
||
hideOverlay();
|
||
}
|
||
};
|
||
// 3秒后自动隐藏
|
||
setTimeout(() => hideOverlay(), 3000);
|
||
}
|
||
|
||
// 透明浮层:隐藏
|
||
function hideOverlay() {
|
||
const overlay = document.getElementById('overlay');
|
||
if (overlay) {
|
||
overlay.classList.remove('show');
|
||
}
|
||
}
|
||
|
||
// 加载外部页面
|
||
function loadPage(filename) {
|
||
showLoading();
|
||
|
||
fetch(filename)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
return response.text();
|
||
})
|
||
.then(html => {
|
||
// 解析HTML并提取内容
|
||
const parser = new DOMParser();
|
||
const doc = parser.parseFromString(html, 'text/html');
|
||
|
||
const title = doc.querySelector('title')?.textContent || t('page.default.title');
|
||
const content = doc.querySelector('.page-content')?.innerHTML ||
|
||
doc.querySelector('body')?.innerHTML ||
|
||
`<p>${t('page.empty')}</p>`;
|
||
|
||
pageTitle.textContent = title;
|
||
pageDescription.textContent = t('page.manage_config', { title });
|
||
pageContent.innerHTML = content;
|
||
|
||
// 执行页面中的脚本
|
||
const scripts = doc.querySelectorAll('script');
|
||
scripts.forEach(script => {
|
||
if (script.textContent) {
|
||
try {
|
||
eval(script.textContent);
|
||
} catch (e) {
|
||
console.warn('执行页面脚本失败:', e);
|
||
}
|
||
}
|
||
});
|
||
})
|
||
.catch(error => {
|
||
console.error('加载页面失败:', error);
|
||
showError(t('error.page_load_failed_reason', { reason: error.message }));
|
||
});
|
||
}
|
||
|
||
// 快速操作函数
|
||
function showSystemStatus() {
|
||
pageContent.innerHTML = `
|
||
<div style="padding: 30px;">
|
||
<h2 style="margin-bottom: 20px;">📊 系统状态</h2>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px;">
|
||
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; border-left: 4px solid #28a745;">
|
||
<h4 style="color: #28a745; margin: 0 0 10px;">服务状态</h4>
|
||
<p style="margin: 0; font-size: 24px; font-weight: bold;">🟢 正常运行</p>
|
||
</div>
|
||
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; border-left: 4px solid #007bff;">
|
||
<h4 style="color: #007bff; margin: 0 0 10px;">CPU 使用率</h4>
|
||
<p style="margin: 0; font-size: 24px; font-weight: bold;">23%</p>
|
||
</div>
|
||
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; border-left: 4px solid #ffc107;">
|
||
<h4 style="color: #ffc107; margin: 0 0 10px;">内存使用</h4>
|
||
<p style="margin: 0; font-size: 24px; font-weight: bold;">1.2GB / 4GB</p>
|
||
</div>
|
||
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; border-left: 4px solid #17a2b8;">
|
||
<h4 style="color: #17a2b8; margin: 0 0 10px;">运行时间</h4>
|
||
<p style="margin: 0; font-size: 24px; font-weight: bold;">7天 12小时</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function showLogs() {
|
||
pageContent.innerHTML = `
|
||
<div style="padding: 30px;">
|
||
<h2 style="margin-bottom: 20px;">📝 系统日志</h2>
|
||
<div style="background: #1e1e1e; color: #00ff00; padding: 20px; border-radius: 8px; font-family: 'Courier New', monospace; font-size: 14px; max-height: 400px; overflow-y: auto;">
|
||
<div>[2024-01-16 10:30:15] INFO: 系统启动完成</div>
|
||
<div>[2024-01-16 10:30:16] INFO: 加载配置文件成功</div>
|
||
<div>[2024-01-16 10:30:17] INFO: 数据库连接建立</div>
|
||
<div>[2024-01-16 10:30:18] INFO: API 服务启动,端口: 8080</div>
|
||
<div>[2024-01-16 10:35:22] INFO: 用户登录: admin</div>
|
||
<div>[2024-01-16 10:36:45] INFO: 模型配置更新</div>
|
||
<div>[2024-01-16 10:38:12] WARN: 知识库索引队列较长</div>
|
||
<div>[2024-01-16 10:40:33] INFO: 知识库索引完成</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function exportConfig() {
|
||
const config = {
|
||
timestamp: new Date().toISOString(),
|
||
version: "1.0.0",
|
||
settings: {
|
||
api: { endpoint: "https://api.example.com", timeout: 30000 },
|
||
models: ["gpt-4", "gpt-3.5-turbo"],
|
||
knowledge: { maxSize: "10GB", indexing: true }
|
||
}
|
||
};
|
||
|
||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'coze-studio-config.json';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
|
||
alert(t('export.success'));
|
||
}
|
||
|
||
function showHelp() {
|
||
pageContent.innerHTML = `
|
||
<div style="padding: 30px;">
|
||
<h2 style="margin-bottom: 20px;">❓ ${t('help.title')}</h2>
|
||
<div style="display: grid; gap: 20px;">
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||
<h3 style="color: #667eea; margin-top: 0;">${t('help.quickstart.title')}</h3>
|
||
<p>1. ${t('help.quickstart.step1')}</p>
|
||
<p>2. ${t('help.quickstart.step2')}</p>
|
||
<p>3. ${t('help.quickstart.step3')}</p>
|
||
<p>4. ${t('help.quickstart.step4')}</p>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||
<h3 style="color: #667eea; margin-top: 0;">${t('help.features.title')}</h3>
|
||
<p><strong>${t('page.basic.title')}:</strong>${t('help.features.basic')}</p>
|
||
<p><strong>${t('page.model.title')}:</strong>${t('help.features.model')}</p>
|
||
<p><strong>${t('page.knowledge.title')}:</strong>${t('help.features.kb')}</p>
|
||
</div>
|
||
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||
<h3 style="color: #667eea; margin-top: 0;">${t('help.support.title')}</h3>
|
||
<p>${t('help.support.contact')}</p>
|
||
<p>${t('help.support.email', { email: 'support@coze-studio.com' })}</p>
|
||
<p>${t('help.support.phone', { phone: '400-123-4567' })}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |