- fix(provider-pool): 修复 ownsGlobalSlot=false 时空队列未清理的内存泄漏 - fix(provider-api): 新增 sanitizeProviderData/ProviderPools,对 customName 等用户输入字段做 HTML 转义,防止 XSS - fix(docker): 删除 docker-compose.yml 中的代理硬编码配置 - fix(api-server): 重构定时健康检查 timer 管理,支持热更新 enabled 状态(stopHealthCheckTimer + 状态变化追踪) - fix(constants): 提取 HEALTH_CHECK/PASSWORD/NETWORK/RETRY 常量到 constants.js - style(api-server): 移除日志中密码长度记录,防止敏感元信息泄露
392 lines
26 KiB
HTML
392 lines
26 KiB
HTML
<link rel="stylesheet" href="components/section-config.css">
|
||
<!-- Configuration Section -->
|
||
<section id="config" class="section" aria-labelledby="config-title">
|
||
<h2 id="config-title" data-i18n="config.title">配置管理</h2>
|
||
<div class="config-panel">
|
||
<div class="config-form">
|
||
<!-- 基础设置 -->
|
||
<div class="config-group-section">
|
||
<h3 data-i18n="config.basic.title"><i class="fas fa-cog"></i> 基础设置</h3>
|
||
<div class="form-group password-input-group">
|
||
<label for="apiKey" data-i18n="config.apiKey">API密钥</label>
|
||
<div class="password-input-wrapper">
|
||
<div class="input-with-toggle">
|
||
<input type="password" id="apiKey" class="form-control" data-i18n="config.apiKeyPlaceholder" placeholder="请输入API密钥" autocomplete="off">
|
||
<button type="button" class="password-toggle" data-target="apiKey" aria-label="显示/隐藏密码">
|
||
<i class="fas fa-eye" aria-hidden="true"></i>
|
||
</button>
|
||
</div>
|
||
<button type="button" class="btn btn-sm btn-secondary generate-key-btn" id="generateApiKey" data-i18n-title="config.apiKey.generateTitle" title="自动生成API密钥">
|
||
<i class="fas fa-magic"></i> <span data-i18n="config.apiKey.generate">生成</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="host" data-i18n="config.host">监听地址</label>
|
||
<input type="text" id="host" class="form-control" value="127.0.0.1" data-i18n-placeholder="config.hostPlaceholder" placeholder="例如: 127.0.0.1">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="port" data-i18n="config.port">端口</label>
|
||
<input type="number" id="port" class="form-control" value="3000" data-i18n-placeholder="config.portPlaceholder" placeholder="3000">
|
||
</div>
|
||
</div>
|
||
<div class="form-group pool-section">
|
||
<label data-i18n="config.modelProvider">模型提供商 (可多选)</label>
|
||
<div id="modelProvider" class="provider-tags">
|
||
<button type="button" class="provider-tag" data-value="gemini-cli-oauth">
|
||
<i class="fas fa-robot"></i>
|
||
<span data-i18n="dashboard.routing.nodeName.gemini">Gemini CLI OAuth</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="gemini-antigravity">
|
||
<i class="fas fa-rocket"></i>
|
||
<span data-i18n="dashboard.routing.nodeName.antigravity">Gemini Antigravity</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="openai-custom">
|
||
<i class="fas fa-brain"></i>
|
||
<span data-i18n="dashboard.routing.nodeName.openai">OpenAI Custom</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="claude-custom">
|
||
<i class="fas fa-comment-dots"></i>
|
||
<span data-i18n="dashboard.routing.nodeName.claude">Claude Custom</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="claude-kiro-oauth">
|
||
<i class="fas fa-key"></i>
|
||
<span data-i18n="dashboard.routing.nodeName.kiro">Claude Kiro OAuth</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="openai-qwen-oauth">
|
||
<i class="fas fa-cloud"></i>
|
||
<span data-i18n="dashboard.routing.nodeName.qwen">Qwen OAuth</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="openaiResponses-custom">
|
||
<i class="fas fa-reply"></i>
|
||
<span>OpenAI Responses</span>
|
||
</button>
|
||
|
||
<button type="button" class="provider-tag" data-value="openai-codex-oauth">
|
||
<i class="fas fa-code"></i>
|
||
<span data-i18n="dashboard.routing.nodeName.codex">OpenAI Codex OAuth</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="grok-custom">
|
||
<i class="fas fa-search"></i>
|
||
<span data-i18n="dashboard.routing.nodeName.grok">Grok Reverse</span>
|
||
</button>
|
||
</div>
|
||
<small class="form-text" data-i18n="config.modelProviderHelp">点击选择启动时初始化的模型提供商 (必须至少选择一个)</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 代理与网络 -->
|
||
<div class="config-group-section">
|
||
<h3 data-i18n="config.proxy.title"><i class="fas fa-globe"></i> 代理设置</h3>
|
||
<div class="form-group">
|
||
<label for="proxyUrl" data-i18n="config.proxy.url">代理地址</label>
|
||
<input type="text" id="proxyUrl" class="form-control" data-i18n-placeholder="config.proxy.urlPlaceholder" placeholder="例如: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080">
|
||
<small class="form-text" data-i18n="config.proxy.urlNote">支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理</small>
|
||
</div>
|
||
<div class="form-group pool-section">
|
||
<label data-i18n="config.proxy.enabledProviders">启用代理的提供商</label>
|
||
<div id="proxyProviders" class="provider-tags">
|
||
<button type="button" class="provider-tag" data-value="gemini-cli-oauth">
|
||
<i class="fas fa-robot"></i>
|
||
<span>Gemini CLI OAuth</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="gemini-antigravity">
|
||
<i class="fas fa-rocket"></i>
|
||
<span>Gemini Antigravity</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="openai-custom">
|
||
<i class="fas fa-brain"></i>
|
||
<span>OpenAI Custom</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="claude-custom">
|
||
<i class="fas fa-comment-dots"></i>
|
||
<span>Claude Custom</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="claude-kiro-oauth">
|
||
<i class="fas fa-key"></i>
|
||
<span>Claude Kiro OAuth</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="openai-qwen-oauth">
|
||
<i class="fas fa-cloud"></i>
|
||
<span>Qwen OAuth</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="openaiResponses-custom">
|
||
<i class="fas fa-reply"></i>
|
||
<span>OpenAI Responses</span>
|
||
</button>
|
||
|
||
<button type="button" class="provider-tag" data-value="openai-codex-oauth">
|
||
<i class="fas fa-code"></i>
|
||
<span>OpenAI Codex OAuth</span>
|
||
</button>
|
||
<button type="button" class="provider-tag" data-value="grok-custom">
|
||
<i class="fas fa-search"></i>
|
||
<span>Grok Reverse</span>
|
||
</button>
|
||
</div>
|
||
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
|
||
</div>
|
||
<hr>
|
||
<div class="config-row">
|
||
<div class="form-group">
|
||
<label data-i18n="config.proxy.tlsSidecarEnabled">TLS 指纹伪装 (uTLS Sidecar)</label>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" id="tlsSidecarEnabled">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="tlsSidecarPort" data-i18n="config.proxy.tlsSidecarPort">Sidecar 端口</label>
|
||
<input type="number" id="tlsSidecarPort" class="form-control" min="1024" max="65535" value="9090">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="tlsSidecarProxyUrl" data-i18n="config.proxy.tlsSidecarProxyUrl">Sidecar 上游代理</label>
|
||
<input type="text" id="tlsSidecarProxyUrl" class="form-control" data-i18n-placeholder="config.proxy.urlPlaceholder" placeholder="例如: http://127.0.0.1:7890">
|
||
<small class="form-text" data-i18n="config.proxy.urlNote">TLS Sidecar 专用上游代理,留空则不使用代理</small>
|
||
</div>
|
||
<div class="form-group pool-section">
|
||
<label data-i18n="config.proxy.tlsSidecarEnabledProviders">启用 TLS Sidecar 的提供商</label>
|
||
<div id="tlsSidecarProviders" class="provider-tags">
|
||
<!-- 动态渲染 -->
|
||
</div>
|
||
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过 TLS Sidecar 访问的提供商</small>
|
||
</div>
|
||
<small class="form-text" data-i18n="config.proxy.tlsSidecarNote">启用后选中的提供商请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare(需重启服务)</small>
|
||
</div>
|
||
|
||
<!-- 服务治理与高可用 -->
|
||
<div class="config-group-section">
|
||
<h3 data-i18n="config.governance.title"><i class="fas fa-shield-alt"></i> 服务治理</h3>
|
||
<div class="config-row">
|
||
<div class="form-group">
|
||
<label for="requestMaxRetries" data-i18n="config.advanced.maxRetries">请求最大重试次数</label>
|
||
<input type="number" id="requestMaxRetries" class="form-control" min="0" max="10" value="3">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="requestBaseDelay" data-i18n="config.advanced.baseDelay">重试基础延迟(毫秒)</label>
|
||
<input type="number" id="requestBaseDelay" class="form-control" min="0" step="100" value="1000">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="credentialSwitchMaxRetries" data-i18n="config.advanced.credentialSwitchMaxRetries">坏凭证切换最大重试次数</label>
|
||
<input type="number" id="credentialSwitchMaxRetries" class="form-control" min="1" max="50" value="5">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="maxErrorCount" data-i18n="config.advanced.maxErrorCount">节点最大错误阈值</label>
|
||
<input type="number" id="maxErrorCount" class="form-control" value="10" min="1" max="20">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="warmupTarget" data-i18n="config.advanced.warmupTarget">系统预热节点数</label>
|
||
<input type="number" id="warmupTarget" class="form-control" min="0" max="100" value="0">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="refreshConcurrencyPerProvider" data-i18n="config.advanced.refreshConcurrencyPerProvider">提供商刷新并发数</label>
|
||
<input type="number" id="refreshConcurrencyPerProvider" class="form-control" min="1" max="10" value="1">
|
||
</div>
|
||
</div>
|
||
<div class="form-group pool-section">
|
||
<label for="providerFallbackChain" data-i18n="config.advanced.fallbackChain">跨类型 Fallback 链配置 (JSON)</label>
|
||
<textarea id="providerFallbackChain" class="form-control" rows="4" data-i18n-placeholder="config.advanced.fallbackChainPlaceholder"></textarea>
|
||
</div>
|
||
<div class="form-group pool-section">
|
||
<label for="modelFallbackMapping" data-i18n="config.advanced.modelFallbackMapping">跨协议模型 Fallback 映射 (JSON)</label>
|
||
<textarea id="modelFallbackMapping" class="form-control" rows="4" data-i18n-placeholder="config.advanced.modelFallbackMappingPlaceholder"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OAuth 与令牌 -->
|
||
<div class="config-group-section">
|
||
<h3 data-i18n="config.oauth.title"><i class="fas fa-key"></i> OAuth & 令牌</h3>
|
||
<div class="config-row">
|
||
<div class="form-group">
|
||
<label for="cronNearMinutes" data-i18n="config.advanced.cronInterval">令牌刷新间隔(分钟)</label>
|
||
<input type="number" id="cronNearMinutes" class="form-control" min="1" max="60" value="1">
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="config.advanced.cronEnabled">启用自动刷新 (需重启)</label>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" id="cronRefreshToken">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="loginExpiry" data-i18n="config.advanced.loginExpiry">登录过期时间(秒)</label>
|
||
<input type="number" id="loginExpiry" class="form-control" min="60" value="3600">
|
||
<small class="form-text" data-i18n="config.advanced.loginExpiryNote">管理后台登录后的 Token 有效期,默认 3600 秒 (1小时)</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 定时健康检查 -->
|
||
<div class="config-group-section">
|
||
<h3 data-i18n="config.healthCheck.title"><i class="fas fa-heartbeat"></i> 定时健康检查</h3>
|
||
<div class="config-row">
|
||
<div class="form-group">
|
||
<span class="toggle-label" data-i18n="config.healthCheck.enabled">启用定时检查</span>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" id="scheduledHealthCheckEnabled">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<span class="toggle-label" data-i18n="config.healthCheck.startupRun">启动时运行</span>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" id="scheduledHealthCheckStartupRun">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="scheduledHealthCheckInterval" data-i18n="config.healthCheck.interval">检查间隔</label>
|
||
<div class="input-with-quick-select">
|
||
<input type="number" id="scheduledHealthCheckInterval" class="form-control" min="60000" max="3600000" step="60000" value="600000" placeholder="毫秒">
|
||
<div class="quick-select-btns">
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" data-value="300000">5分钟</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" data-value="600000">10分钟</button>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary" data-value="1800000">30分钟</button>
|
||
</div>
|
||
</div>
|
||
<small class="form-text" data-i18n="config.healthCheck.intervalNote">单位毫秒,最小60000ms(1分钟),最大3600000ms(1小时)</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="config.healthCheck.providerTypes">定时检查的供应商</label>
|
||
<div id="scheduledHealthCheckProviders" class="provider-tags">
|
||
<!-- 由 config-manager.js updateConfigProviderConfigs 动态渲染,勿在此处硬编码 -->
|
||
</div>
|
||
<small class="form-text" data-i18n="config.healthCheck.providerTypesNote">选择需要进行定时健康检查的供应商类型</small>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日志管理 -->
|
||
<div class="config-group-section">
|
||
<h3 data-i18n="config.log.title"><i class="fas fa-file-alt"></i> 日志设置</h3>
|
||
<div class="config-row">
|
||
<div class="form-group">
|
||
<label data-i18n="config.log.enabled">启用日志</label>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" id="logEnabled" checked>
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="logOutputMode" data-i18n="config.log.outputMode">日志输出模式</label>
|
||
<select id="logOutputMode" class="form-control">
|
||
<option value="all" selected data-i18n="config.log.outputMode.all">全部 (控制台+文件)</option>
|
||
<option value="console" data-i18n="config.log.outputMode.console">仅控制台</option>
|
||
<option value="file" data-i18n="config.log.outputMode.file">仅文件</option>
|
||
<option value="none" data-i18n="config.log.outputMode.none">禁用</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="logLevel" data-i18n="config.log.level">日志级别</label>
|
||
<select id="logLevel" class="form-control">
|
||
<option value="debug" data-i18n="config.log.level.debug">调试 (debug)</option>
|
||
<option value="info" selected data-i18n="config.log.level.info">信息 (info)</option>
|
||
<option value="warn" data-i18n="config.log.level.warn">警告 (warn)</option>
|
||
<option value="error" data-i18n="config.log.level.error">错误 (error)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="logDir" data-i18n="config.log.dir">日志目录</label>
|
||
<input type="text" id="logDir" class="form-control" value="logs">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="logMaxFileSize" data-i18n="config.log.maxFileSize">最大文件大小(字节)</label>
|
||
<input type="number" id="logMaxFileSize" class="form-control" value="10485760" step="1048576">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="logMaxFiles" data-i18n="config.log.maxFiles">最大保留文件数</label>
|
||
<input type="number" id="logMaxFiles" class="form-control" value="10" min="1">
|
||
</div>
|
||
</div>
|
||
<div class="config-row">
|
||
<div class="form-group">
|
||
<label data-i18n="config.log.includeRequestId">包含请求ID</label>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" id="logIncludeRequestId" checked>
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label data-i18n="config.log.includeTimestamp">包含时间戳</label>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" id="logIncludeTimestamp" checked>
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<hr>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="promptLogMode" data-i18n="config.advanced.promptLogMode">提示日志模式</label>
|
||
<select id="promptLogMode" class="form-control">
|
||
<option value="none" data-i18n="config.advanced.promptLogMode.none">无 (none)</option>
|
||
<option value="console" data-i18n="config.advanced.promptLogMode.console">控制台 (console)</option>
|
||
<option value="file" data-i18n="config.advanced.promptLogMode.file">文件 (file)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="promptLogBaseName" data-i18n="config.advanced.promptLogBaseName">提示日志基础名称</label>
|
||
<input type="text" id="promptLogBaseName" class="form-control">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统与高级 -->
|
||
<div class="config-group-section">
|
||
<h3 data-i18n="config.advanced.title"><i class="fas fa-cogs"></i> 系统与高级</h3>
|
||
<div class="form-group">
|
||
<label for="providerPoolsFilePath" data-i18n="config.advanced.poolFilePath">提供商池配置文件路径</label>
|
||
<input type="text" id="providerPoolsFilePath" class="form-control">
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="systemPromptFilePath" data-i18n="config.advanced.systemPromptFile">系统提示文件路径</label>
|
||
<input type="text" id="systemPromptFilePath" class="form-control">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="systemPromptMode" data-i18n="config.advanced.systemPromptMode">系统提示模式</label>
|
||
<select id="systemPromptMode" class="form-control">
|
||
<option value="append" selected data-i18n="config.advanced.systemPromptMode.append">追加 (append)</option>
|
||
<option value="overwrite" data-i18n="config.advanced.systemPromptMode.overwrite">覆盖 (overwrite)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="systemPrompt" data-i18n="config.advanced.systemPrompt">系统提示内容</label>
|
||
<textarea id="systemPrompt" class="form-control" rows="4"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="adminPassword" data-i18n="config.advanced.adminPassword">后台登录密码</label>
|
||
<div class="password-input-wrapper">
|
||
<div class="input-with-toggle">
|
||
<input type="password" id="adminPassword" class="form-control" autocomplete="new-password">
|
||
<button type="button" class="password-toggle" data-target="adminPassword" aria-label="显示/隐藏密码">
|
||
<i class="fas fa-eye" aria-hidden="true"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<small class="form-text" data-i18n="config.advanced.adminPasswordNote">修改后需要重新登录</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<button class="btn btn-success" id="saveConfig">
|
||
<i class="fas fa-save"></i> <span data-i18n="config.save">保存配置</span>
|
||
</button>
|
||
<button class="btn btn-secondary" id="resetConfig">
|
||
<i class="fas fa-undo"></i> <span data-i18n="config.reset">重置</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|