Add files via upload

This commit is contained in:
公明
2026-07-24 11:44:35 +08:00
committed by GitHub
parent af4b25b84e
commit 151b445c74
5 changed files with 785 additions and 226 deletions
+386 -82
View File
@@ -4430,7 +4430,7 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
.ai-channel-manager {
border: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 88%, transparent);
border-radius: 10px;
border-radius: 8px;
background: var(--card-bg, #fff);
overflow: hidden;
}
@@ -4446,8 +4446,8 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
}
.ai-channel-manager-header h4 {
margin: 0 0 8px;
font-size: 16px;
margin: 0 0 6px;
font-size: 17px;
line-height: 1.25;
}
@@ -4467,55 +4467,75 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
color: var(--error-color, #e53e3e);
}
.ai-channel-header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 10px;
flex-shrink: 0;
}
.ai-channel-save-btn {
flex-shrink: 0;
min-width: 92px;
}
.ai-channel-manager-body {
display: grid;
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
display: block;
min-height: 0;
align-items: stretch;
background: var(--card-bg, #fff);
overflow: visible;
}
.ai-channel-sidebar {
position: relative;
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
overflow: hidden;
border-right: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 76%, transparent);
background: color-mix(in srgb, var(--bg-secondary, #f8fafc) 86%, var(--card-bg, #fff));
padding: 18px;
}
.ai-channel-sidebar-head {
.ai-channel-switcher {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 0;
color: var(--text-secondary, #4a5568);
font-size: 12px;
font-weight: 700;
letter-spacing: 0;
gap: 16px;
padding: 16px 22px;
border-bottom: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 72%, transparent);
background: color-mix(in srgb, var(--bg-secondary, #f8fafc) 70%, var(--card-bg, #fff));
}
.ai-channel-bulk-actions {
.ai-channel-switcher-field {
display: grid;
grid-template-columns: auto minmax(260px, 420px);
align-items: center;
gap: 10px;
min-width: 0;
}
.ai-channel-switcher-field label {
color: var(--text-secondary, #4a5568);
font-size: 13px;
font-weight: 700;
white-space: nowrap;
}
.ai-channel-switch-select {
width: 100%;
min-height: 38px;
padding: 0.45rem 2rem 0.45rem 0.7rem;
border: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 88%, transparent);
border-radius: 7px;
background: color-mix(in srgb, var(--input-bg, var(--card-bg, #fff)) 92%, transparent);
color: var(--text-primary, #2d3748);
font-size: 0.875rem;
}
.ai-channel-switch-select:focus {
border-color: color-mix(in srgb, var(--accent-color, #3182ce) 70%, transparent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color, #3182ce) 12%, transparent);
outline: none;
}
.ai-channel-switcher-actions {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 5px;
margin: 0;
min-width: 0;
padding: 0;
border: 0;
background: transparent;
gap: 8px;
}
.ai-channel-bulk-btn {
@@ -4523,8 +4543,8 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
align-items: center;
justify-content: center;
min-width: 0;
min-height: 26px;
padding: 4px 8px;
min-height: 28px;
padding: 5px 9px;
border: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 68%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--card-bg, #fff) 38%, transparent);
@@ -4555,8 +4575,8 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
}
.ai-channel-icon-btn {
width: 30px;
height: 30px;
width: 32px;
height: 32px;
border: 1px solid var(--border-color, #e2e8f0);
border-radius: 8px;
background: var(--card-bg, #fff);
@@ -4577,19 +4597,7 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
}
.ai-channel-list {
position: absolute;
top: 64px;
right: 13px;
bottom: 18px;
left: 18px;
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 2px 5px 2px 1px;
scrollbar-gutter: stable;
display: none;
}
.ai-channel-list::-webkit-scrollbar {
@@ -4616,10 +4624,10 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
align-items: start;
gap: 10px;
width: 100%;
min-height: 92px;
padding: 12px;
min-height: 86px;
padding: 11px;
border: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 64%, transparent);
border-radius: 8px;
border-radius: 7px;
background: color-mix(in srgb, var(--card-bg, #fff) 70%, transparent);
color: var(--text-color, #2d3748);
text-align: left;
@@ -4640,7 +4648,7 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
height: 16px;
margin: 3px 0 0;
border: 1px solid color-mix(in srgb, var(--text-muted, #718096) 52%, transparent);
border-radius: 5px;
border-radius: 4px;
appearance: none;
-webkit-appearance: none;
background-color: color-mix(in srgb, var(--card-bg, #fff) 84%, transparent);
@@ -4671,7 +4679,6 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
.ai-channel-list-item:hover {
background: var(--card-bg, #fff);
border-color: color-mix(in srgb, var(--accent-color, #3182ce) 24%, transparent);
transform: translateY(-1px);
}
.ai-channel-list-item.active {
@@ -4779,17 +4786,17 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
min-width: 0;
min-height: 0;
overflow-y: visible;
padding: 22px 28px 28px;
padding: 20px 28px 28px;
background: var(--card-bg, #fff);
}
.ai-channel-editor-head {
display: flex;
align-items: flex-start;
align-items: center;
justify-content: space-between;
gap: 18px;
margin-bottom: 18px;
padding-bottom: 18px;
margin-bottom: 6px;
padding-bottom: 16px;
border-bottom: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 72%, transparent);
}
@@ -4804,7 +4811,7 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
.ai-channel-editor-head h5 {
margin: 0;
color: var(--text-color, #2d3748);
font-size: 22px;
font-size: 21px;
font-weight: 700;
line-height: 1.2;
}
@@ -4827,8 +4834,9 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
color: var(--error-color, #e53e3e);
}
.ai-channel-editor-form {
max-width: 980px;
max-width: none;
}
.ai-channel-editor-form .form-group {
@@ -4837,7 +4845,7 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
.ai-channel-editor-form .form-group label {
display: block;
margin-bottom: 0;
margin-bottom: 6px;
color: var(--text-primary);
font-size: 0.875rem;
font-weight: 500;
@@ -4848,10 +4856,14 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
.ai-channel-editor-form input[type="password"],
.ai-channel-editor-form input[type="number"],
.ai-channel-editor-form select {
width: 100%;
min-height: 42px;
border-radius: 8px;
padding: 0.5rem 0.75rem;
border-radius: 7px;
border-color: color-mix(in srgb, var(--border-color, #e2e8f0) 88%, transparent);
background: color-mix(in srgb, var(--input-bg, var(--card-bg, #fff)) 92%, transparent);
color: var(--text-color, #2d3748);
font-size: 0.875rem;
}
.ai-channel-editor-form input:focus,
@@ -4863,9 +4875,175 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
.ai-channel-editor-form .form-hint,
.ai-channel-editor-form small {
display: block;
margin-top: 5px;
color: var(--text-muted, #718096);
font-size: 0.75rem;
line-height: 1.45;
}
.ai-channel-form-section {
padding: 18px 0;
border-bottom: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 64%, transparent);
}
.ai-channel-form-section:last-of-type {
border-bottom: 0;
}
.ai-channel-form-section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
margin-bottom: 14px;
}
.ai-channel-form-section-head > div:first-child {
max-width: 620px;
}
.ai-channel-form-section-head h6 {
margin: 0;
color: var(--text-primary, #1f2937);
font-size: 14px;
font-weight: 700;
line-height: 1.35;
}
.ai-channel-form-section-head p {
margin: 4px 0 0;
color: var(--text-muted, #718096);
font-size: 12px;
line-height: 1.45;
}
.ai-channel-section-actions {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
min-width: 210px;
}
.ai-channel-section-actions .form-inline-result {
margin-top: 0;
}
.ai-channel-section-actions .connection-test-result {
display: none;
max-width: min(760px, 54vw);
padding: 8px 10px;
border: 1px solid var(--border-color, #e2e8f0);
border-radius: 8px;
background: color-mix(in srgb, var(--surface-color, #ffffff) 86%, var(--bg-secondary, #f7fafc));
color: var(--text-muted, #718096);
font-size: 12px;
line-height: 1.45;
text-align: left;
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
}
.ai-channel-section-actions .connection-test-result.is-visible {
display: inline-block;
}
.ai-channel-section-actions .connection-test-result.is-error {
border-color: color-mix(in srgb, var(--danger-color, #e53e3e) 42%, var(--border-color, #e2e8f0));
background: color-mix(in srgb, var(--danger-color, #e53e3e) 8%, var(--surface-color, #ffffff));
color: var(--danger-color, #e53e3e);
}
.ai-channel-section-actions .connection-test-result.is-success {
border-color: color-mix(in srgb, var(--success-color, #38a169) 38%, var(--border-color, #e2e8f0));
background: color-mix(in srgb, var(--success-color, #38a169) 9%, var(--surface-color, #ffffff));
color: var(--success-color, #38a169);
}
.ai-channel-form-grid,
.ai-channel-reasoning-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
column-gap: 16px;
row-gap: 14px;
}
.ai-channel-reasoning-grid {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.ai-channel-editor-form .span-2 {
grid-column: 1 / -1;
}
.required-mark {
color: var(--error-color, #e53e3e);
}
.form-inline-result {
display: block;
margin-top: 6px;
color: var(--text-muted, #718096);
font-size: 0.75rem;
line-height: 1.45;
}
.ai-channel-reasoning-toggle {
margin-top: 8px;
}
.ai-channel-advanced-section {
padding: 10px 0 0;
border-bottom: 0;
}
.ai-channel-advanced-section summary {
display: flex;
align-items: center;
gap: 10px;
min-height: 38px;
padding: 0;
color: var(--text-primary, #1f2937);
cursor: pointer;
list-style: none;
}
.ai-channel-advanced-section summary::-webkit-details-marker {
display: none;
}
.ai-channel-advanced-section summary::before {
content: "";
width: 8px;
height: 8px;
flex: 0 0 auto;
border-right: 2px solid var(--text-muted, #718096);
border-bottom: 2px solid var(--text-muted, #718096);
transform: rotate(-45deg);
transition: transform 0.16s ease;
}
.ai-channel-advanced-section[open] summary::before {
transform: rotate(45deg);
}
.ai-channel-advanced-section summary strong {
display: block;
font-size: 14px;
line-height: 1.35;
}
.ai-channel-advanced-section summary small {
margin-top: 4px;
}
.ai-channel-advanced-section[open] .ai-channel-reasoning-grid {
margin-top: 14px;
}
html[data-theme="dark"] .ai-channel-manager {
background: #111827;
border-color: rgba(71, 85, 105, 0.42);
@@ -4876,6 +5054,21 @@ html[data-theme="dark"] .ai-channel-manager-header {
border-bottom-color: rgba(71, 85, 105, 0.34);
}
html[data-theme="dark"] .ai-channel-switcher {
background: #101827;
border-bottom-color: rgba(71, 85, 105, 0.34);
}
html[data-theme="dark"] .ai-channel-switcher-field label {
color: #cbd5e1;
}
html[data-theme="dark"] .ai-channel-switch-select {
background: rgba(15, 23, 42, 0.72);
border-color: rgba(71, 85, 105, 0.44);
color: #e5e7eb;
}
html[data-theme="dark"] .ai-channel-sidebar {
background: #101827;
border-right-color: rgba(71, 85, 105, 0.34);
@@ -4928,6 +5121,42 @@ html[data-theme="dark"] .ai-channel-editor-head {
border-bottom-color: rgba(71, 85, 105, 0.34);
}
html[data-theme="dark"] .ai-channel-form-section,
html[data-theme="dark"] .ai-channel-advanced-section {
border-color: rgba(71, 85, 105, 0.34);
}
html[data-theme="dark"] .ai-channel-form-section-head h6 {
color: #e5e7eb;
}
html[data-theme="dark"] .ai-channel-form-section-head p,
html[data-theme="dark"] .form-inline-result {
color: #94a3b8;
}
html[data-theme="dark"] .ai-channel-section-actions .connection-test-result {
border-color: rgba(71, 85, 105, 0.52);
background: rgba(15, 23, 42, 0.74);
color: #94a3b8;
}
html[data-theme="dark"] .ai-channel-section-actions .connection-test-result.is-error {
border-color: rgba(248, 113, 113, 0.42);
background: rgba(127, 29, 29, 0.22);
color: #f87171;
}
html[data-theme="dark"] .ai-channel-section-actions .connection-test-result.is-success {
border-color: rgba(52, 211, 153, 0.38);
background: rgba(6, 78, 59, 0.22);
color: #34d399;
}
html[data-theme="dark"] .ai-channel-advanced-section summary {
color: #e5e7eb;
}
html[data-theme="dark"] .ai-channel-icon-btn {
background: rgba(15, 23, 42, 0.72);
border-color: rgba(71, 85, 105, 0.44);
@@ -5009,23 +5238,9 @@ html[data-theme="dark"] .ai-channel-editor-form select {
align-items: stretch;
}
.ai-channel-manager-body {
grid-template-columns: 1fr;
height: auto;
overflow: visible;
}
.ai-channel-sidebar {
border-right: 0;
border-bottom: 1px solid var(--border-color, #e2e8f0);
max-height: 420px;
}
.ai-channel-list {
position: static;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
max-height: 360px;
.ai-channel-switcher {
align-items: stretch;
flex-direction: column;
}
.ai-channel-editor {
@@ -5035,19 +5250,40 @@ html[data-theme="dark"] .ai-channel-editor-form select {
@media (max-width: 640px) {
.ai-channel-manager-header,
.ai-channel-sidebar,
.ai-channel-switcher,
.ai-channel-editor {
padding-left: 14px;
padding-right: 14px;
}
.ai-channel-editor-actions {
.ai-channel-switcher-actions {
justify-content: flex-start;
}
.ai-channel-bulk-actions {
.ai-channel-header-actions {
justify-content: flex-start;
}
.ai-channel-switcher-field {
grid-template-columns: 1fr;
}
.ai-channel-form-section-head,
.ai-channel-form-grid,
.ai-channel-reasoning-grid {
grid-template-columns: 1fr;
}
.ai-channel-form-section-head,
.ai-channel-section-actions {
align-items: stretch;
flex-direction: column;
}
.ai-channel-section-actions {
justify-content: flex-start;
min-width: 0;
}
}
.chat-reasoning-field-label {
@@ -9923,6 +10159,54 @@ html[data-theme="dark"] .robot-binding-service-hint-icon {
white-space: nowrap;
}
.settings-custom-select-option--probe .settings-custom-select-label {
flex: 1 1 auto;
}
.settings-custom-select-status {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 5px;
max-width: 42%;
padding: 2px 6px;
border-radius: 999px;
background: var(--bg-secondary, #f5f6f8);
color: var(--text-secondary, #64748b);
font-size: 11px;
font-weight: 600;
line-height: 1.3;
}
.settings-custom-select-status-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-custom-select-status-dot {
flex: 0 0 auto;
width: 6px;
height: 6px;
border-radius: 999px;
background: currentColor;
}
.settings-custom-select-status.ready {
background: color-mix(in srgb, var(--success-color, #38a169) 11%, transparent);
color: var(--success-color, #38a169);
}
.settings-custom-select-status.failed {
background: color-mix(in srgb, var(--danger-color, #e53e3e) 10%, transparent);
color: var(--danger-color, #e53e3e);
}
.settings-custom-select-status.testing {
background: color-mix(in srgb, var(--accent-color, #0066ff) 10%, transparent);
color: var(--accent-color, #0066ff);
}
.form-group input:not([type="checkbox"]):not([type="radio"]):focus,
.form-group select:focus {
outline: none;
@@ -38401,6 +38685,26 @@ html[data-theme="dark"] #page-settings .audit-custom-select-option.is-selected {
color: #60a5fa !important;
}
html[data-theme="dark"] #page-settings .settings-custom-select-status {
background: rgba(148, 163, 184, 0.12);
color: #94a3b8;
}
html[data-theme="dark"] #page-settings .settings-custom-select-status.ready {
background: rgba(52, 211, 153, 0.14);
color: #34d399;
}
html[data-theme="dark"] #page-settings .settings-custom-select-status.failed {
background: rgba(248, 113, 113, 0.14);
color: #f87171;
}
html[data-theme="dark"] #page-settings .settings-custom-select-status.testing {
background: rgba(96, 165, 250, 0.14);
color: #60a5fa;
}
/* Chat @ tool mention panel dark theme. */
html[data-theme="dark"] .mention-suggestions {
background: #111827 !important;
+24 -2
View File
@@ -2603,23 +2603,40 @@
"aiChannelNew": "New",
"aiChannelCopy": "Copy",
"aiChannelDelete": "Delete",
"aiChannelHint": "Saved channels appear on the left. Saving writes to ai.channels; the default channel is used by new chats and tasks without an explicit channel.",
"aiChannelHint": "Use the dropdown to switch saved channels. Saving writes to ai.channels; the default channel is used by new chats and tasks without an explicit channel.",
"aiChannelSavedList": "Saved channels",
"aiChannelListAria": "AI channel list",
"aiChannelEditing": "Editing",
"aiChannelBulkProbe": "Probe all",
"aiChannelBulkProbeTitle": "Check whether all complete channels are available",
"aiChannelSelectAria": "Select {name}",
"aiChannelDefaultBadge": "Default",
"aiChannelReady": "Ready",
"aiChannelReadyWithLatency": "Ready{latency}",
"aiChannelDraft": "Incomplete",
"aiChannelDefaultMeta": "Default channel",
"aiChannelCustomMeta": "Custom channel",
"aiChannelOpenAICompat": "OpenAI compatible",
"aiChannelModelMissing": "Model missing",
"aiChannelName": "Channel name",
"aiChannelConnectionSection": "Connection",
"aiChannelConnectionHint": "Confirm provider, endpoint, key, and model first. Test connection uses these values.",
"aiChannelModelSection": "Model and limits",
"aiChannelModelHint": "The model name controls routing. Token limits control context and single-response output.",
"aiChannelLimitsSection": "Limits",
"aiChannelLimitsHint": "Token limits control the context window and single-response output.",
"aiChannelUntitled": "New Channel",
"aiChannelDeleteConfirm": "Delete AI channel \"{name}\"?",
"aiChannelSaved": "Channel saved",
"aiChannelDefaultSaved": "Default channel saved",
"aiChannelCount": "{count} channel(s) saved",
"aiChannelSaving": "Saving channel...",
"aiChannelNewUnsaved": "New channel is not saved yet. Fill it in, then click Save changes.",
"aiChannelCopyUnsaved": "Copied channel is not saved yet. Review it, then click Save changes.",
"aiChannelDeleted": "Channel deleted",
"aiChannelProbeNoComplete": "No complete channel to probe. Fill in Base URL, API Key, and Model first.",
"aiChannelProbing": "Probing {count} channel(s)...",
"aiChannelProbeDone": "Probe complete: {ok}/{total} ready",
"apiProvider": "API Provider",
"providerOpenAI": "OpenAI / OpenAI-compatible API",
"providerClaude": "Claude (Anthropic Messages API)",
@@ -2799,7 +2816,12 @@
"testing": "Testing connection...",
"testSuccess": "Connection successful",
"testFailed": "Connection failed",
"testError": "Test error"
"testError": "Test error",
"testErrorInvalidApiKey": "API Key is invalid or unauthorized. Check that the key is correct.",
"testErrorUnauthorized": "Authentication failed. Check the API Key.",
"testErrorForbidden": "Request denied. Check account permissions or model access.",
"testErrorModelUnavailable": "Model is unavailable or the model name is incorrect.",
"testErrorBaseUrl": "Base URL is unavailable. Check that the endpoint is correct."
},
"settingsTerminal": {
"title": "Terminal",
+24 -2
View File
@@ -2591,23 +2591,40 @@
"aiChannelNew": "新增",
"aiChannelCopy": "复制",
"aiChannelDelete": "删除",
"aiChannelHint": "已保存通道会显示在左侧;保存后写入 ai.channels,默认通道用于新对话和未指定通道的任务。",
"aiChannelHint": "通过下拉切换已保存通道;保存后写入 ai.channels,默认通道用于新对话和未指定通道的任务。",
"aiChannelSavedList": "已保存通道",
"aiChannelListAria": "AI 通道列表",
"aiChannelEditing": "正在编辑",
"aiChannelBulkProbe": "批量探活",
"aiChannelBulkProbeTitle": "检测全部完整通道是否可用",
"aiChannelSelectAria": "选择 {name}",
"aiChannelDefaultBadge": "默认",
"aiChannelReady": "可用",
"aiChannelReadyWithLatency": "可用{latency}",
"aiChannelDraft": "待完善",
"aiChannelDefaultMeta": "默认通道",
"aiChannelCustomMeta": "自定义通道",
"aiChannelOpenAICompat": "OpenAI 兼容",
"aiChannelModelMissing": "未填写模型",
"aiChannelName": "通道名称",
"aiChannelConnectionSection": "连接信息",
"aiChannelConnectionHint": "先确认服务商、地址、密钥和模型,测试连接会使用这些信息。",
"aiChannelModelSection": "模型与额度",
"aiChannelModelHint": "模型名决定请求路由,Token 上限控制上下文和单次输出。",
"aiChannelLimitsSection": "额度设置",
"aiChannelLimitsHint": "Token 上限控制上下文窗口和单次输出。",
"aiChannelUntitled": "新通道",
"aiChannelDeleteConfirm": "确定删除 AI 通道「{name}」吗?",
"aiChannelSaved": "通道已保存",
"aiChannelDefaultSaved": "已设为默认通道",
"aiChannelCount": "已保存 {count} 个通道",
"aiChannelSaving": "正在保存通道...",
"aiChannelNewUnsaved": "新通道尚未保存,填写后点击「保存更改」。",
"aiChannelCopyUnsaved": "复制的通道尚未保存,确认后点击「保存更改」。",
"aiChannelDeleted": "通道已删除",
"aiChannelProbeNoComplete": "没有可探活的完整通道,请先填写 Base URL、API Key 和模型",
"aiChannelProbing": "正在探活 {count} 个通道...",
"aiChannelProbeDone": "探活完成:{ok}/{total} 可用",
"apiProvider": "API 提供商",
"providerOpenAI": "OpenAI / 兼容 OpenAI 协议",
"providerClaude": "Claude (Anthropic Messages API)",
@@ -2787,7 +2804,12 @@
"testing": "测试中...",
"testSuccess": "连接成功",
"testFailed": "连接失败",
"testError": "测试出错"
"testError": "测试出错",
"testErrorInvalidApiKey": "API Key 无效或无权限,请检查密钥是否填写正确",
"testErrorUnauthorized": "认证失败,请检查 API Key",
"testErrorForbidden": "请求被拒绝,请检查账号权限或模型访问权限",
"testErrorModelUnavailable": "模型不可用或模型名不正确,请检查模型名称",
"testErrorBaseUrl": "Base URL 不可用,请检查地址是否正确"
},
"settingsTerminal": {
"title": "终端",
+225 -45
View File
@@ -142,6 +142,19 @@ function syncSettingsCustomSelect(select) {
item.appendChild(check);
item.appendChild(label);
if (select.id === 'ai-channel-select') {
const probeStatus = option.dataset.probeStatus || '';
const probeMessage = option.dataset.probeMessage || '';
if (probeStatus) {
item.classList.add('settings-custom-select-option--probe', `probe-${probeStatus}`);
const status = document.createElement('span');
status.className = `settings-custom-select-status ${probeStatus}`;
status.innerHTML = `<span class="settings-custom-select-status-dot" aria-hidden="true"></span><span class="settings-custom-select-status-text"></span>`;
status.querySelector('.settings-custom-select-status-text').textContent = probeMessage || probeStatus;
item.appendChild(status);
}
}
reg.menu.appendChild(item);
});
}
@@ -2557,6 +2570,90 @@ function writeAIChannelToMainForm(id) {
const allowEl = document.getElementById('openai-reasoning-allow-client');
if (allowEl) allowEl.checked = r.allow_client_reasoning !== false;
syncModelListFetchButtons();
syncAIChannelEditorPreview();
}
function displayAIChannelName(id, ch) {
const name = String(ch?.name || '').trim();
if ((name === '新通道' || name === 'New Channel') && !String(ch?.model || '').trim()) {
return settingsT('settingsBasic.aiChannelUntitled', name || id);
}
return name || id;
}
function aiChannelSelectLabel(id, ch) {
const marker = id === currentConfig?.ai?.default_channel ? ' *' : '';
return `${displayAIChannelName(id, ch)}${marker} · ${ch?.model || '-'}`;
}
function aiChannelOptionProbeMeta(id) {
const probe = aiChannelProbeResults[id];
if (!probe) return null;
const status = probe.status || '';
if (!['testing', 'ready', 'failed'].includes(status)) return null;
return {
status,
message: probe.message || (status === 'ready'
? settingsT('settingsBasic.aiChannelReady', '可用')
: status === 'testing'
? settingsT('settingsBasic.testing', '测试中...')
: settingsT('settingsBasic.testFailed', '连接失败'))
};
}
function updateAIChannelSelectOption(id) {
const select = document.getElementById('ai-channel-select');
if (!select || !currentConfig?.ai?.channels) return;
const channelId = normalizeAIChannelId(id || selectedAIChannelId || currentConfig.ai.default_channel || 'default');
const ch = currentConfig.ai.channels[channelId];
if (!ch) return;
const opt = Array.from(select.options).find((option) => option.value === channelId);
if (opt) {
opt.textContent = aiChannelSelectLabel(channelId, ch);
const probeMeta = aiChannelOptionProbeMeta(channelId);
if (probeMeta) {
opt.dataset.probeStatus = probeMeta.status;
opt.dataset.probeMessage = probeMeta.message;
} else {
delete opt.dataset.probeStatus;
delete opt.dataset.probeMessage;
}
select.value = channelId;
select.selectedIndex = opt.index;
}
if (typeof syncSettingsCustomSelect === 'function') {
syncSettingsCustomSelect(select);
}
}
function syncSelectedAIChannelUI() {
updateAIChannelSelectOption(selectedAIChannelId);
updateAIChannelEditorChrome(selectedAIChannelId);
renderAIChannelList();
}
function syncAIChannelEditorPreview() {
if (!currentConfig?.ai?.channels || !selectedAIChannelId || !currentConfig.ai.channels[selectedAIChannelId]) return;
const id = normalizeAIChannelId(selectedAIChannelId);
const next = readAIChannelFromMainForm(id);
currentConfig.ai.channels[id] = next;
syncSelectedAIChannelUI();
}
function bindAIChannelEditorPreviewSync() {
const ids = [
'ai-channel-name',
'openai-provider',
'openai-base-url',
'openai-model'
];
ids.forEach((fieldId) => {
const el = document.getElementById(fieldId);
if (!el || el.dataset.aiChannelPreviewBound === '1') return;
el.dataset.aiChannelPreviewBound = '1';
const eventName = el.tagName === 'SELECT' ? 'change' : 'input';
el.addEventListener(eventName, syncAIChannelEditorPreview);
});
}
function renderAIChannelSelect() {
@@ -2570,16 +2667,24 @@ function renderAIChannelSelect() {
const ch = currentConfig.ai.channels[id] || {};
const opt = document.createElement('option');
opt.value = id;
const marker = id === currentConfig.ai.default_channel ? ' *' : '';
opt.textContent = `${ch.name || id}${marker} · ${ch.model || '-'}`;
opt.textContent = aiChannelSelectLabel(id, ch);
const probeMeta = aiChannelOptionProbeMeta(id);
if (probeMeta) {
opt.dataset.probeStatus = probeMeta.status;
opt.dataset.probeMessage = probeMeta.message;
}
select.appendChild(opt);
});
selectedAIChannelId = selectedAIChannelId && currentConfig.ai.channels[selectedAIChannelId]
? selectedAIChannelId
: currentConfig.ai.default_channel;
select.value = selectedAIChannelId;
renderAIChannelList(ids);
updateAIChannelSelectOption(selectedAIChannelId);
if (typeof syncSettingsCustomSelect === 'function') {
syncSettingsCustomSelect(select);
}
updateAIChannelEditorChrome(selectedAIChannelId);
renderAIChannelList(ids);
const countLabel = typeof window.t === 'function'
? window.t('settingsBasic.aiChannelCount').replace('{count}', String(ids.length))
: `已保存 ${ids.length} 个通道`;
@@ -2621,7 +2726,7 @@ function renderAIChannelList(ids) {
checkbox.type = 'checkbox';
checkbox.className = 'ai-channel-bulk-check';
checkbox.checked = selectedAIChannelBulkIds.has(id);
checkbox.setAttribute('aria-label', `选择 ${ch.name || id}`);
checkbox.setAttribute('aria-label', settingsT('settingsBasic.aiChannelSelectAria', '选择 {name}').replace('{name}', displayAIChannelName(id, ch)));
checkbox.onclick = (event) => {
event.stopPropagation();
if (checkbox.checked) {
@@ -2631,6 +2736,7 @@ function renderAIChannelList(ids) {
}
item.classList.toggle('checked', checkbox.checked);
};
const displayName = displayAIChannelName(id, ch);
const defaultBadge = isDefault ? `<span class="ai-channel-badge">${escapeAIChannelHtml(settingsT('settingsBasic.aiChannelDefaultBadge', '默认'))}</span>` : '';
let statusText = isComplete
? settingsT('settingsBasic.aiChannelReady', '可用')
@@ -2645,7 +2751,7 @@ function renderAIChannelList(ids) {
body.innerHTML = `
<div class="ai-channel-list-main">
<span class="ai-channel-status-dot ${statusClass}" aria-hidden="true"></span>
<strong title="${escapeAIChannelHtml(ch.name || id)}">${escapeAIChannelHtml(ch.name || id)}</strong>
<strong title="${escapeAIChannelHtml(displayName)}">${escapeAIChannelHtml(displayName)}</strong>
${defaultBadge}
</div>
<div class="ai-channel-list-meta" title="${escapeAIChannelHtml(ch.model || '-')} · ${escapeAIChannelHtml(channelHostLabel(ch.base_url))}">${escapeAIChannelHtml(ch.model || '-')} · ${escapeAIChannelHtml(channelHostLabel(ch.base_url))}</div>
@@ -2674,7 +2780,7 @@ function updateAIChannelEditorChrome(id) {
const ch = ai.channels[channelId] || {};
const title = document.getElementById('ai-channel-editor-title');
const meta = document.getElementById('ai-channel-editor-meta');
if (title) title.textContent = ch.name || channelId;
if (title) title.textContent = displayAIChannelName(channelId, ch);
if (meta) {
const parts = [
channelId === ai.default_channel
@@ -2699,6 +2805,41 @@ function validateSelectedAIChannelPayload(ch) {
return '';
}
function resolveSavedAIChannelId(ai, preferredId, preferredPayload) {
const channels = ai?.channels || {};
const normalizedPreferred = normalizeAIChannelId(preferredId || '');
if (normalizedPreferred && channels[normalizedPreferred]) return normalizedPreferred;
const payload = preferredPayload || {};
const targetName = String(payload.name || '').trim();
const targetModel = String(payload.model || '').trim();
const targetBaseUrl = String(payload.base_url || '').trim();
const targetProvider = String(payload.provider || '').trim();
const ids = Object.keys(channels).sort();
const matched = ids.find((id) => {
const ch = channels[id] || {};
return String(ch.name || '').trim() === targetName
&& String(ch.model || '').trim() === targetModel
&& String(ch.base_url || '').trim() === targetBaseUrl
&& String(ch.provider || '').trim() === targetProvider;
});
return matched || ai?.default_channel || ids[0] || normalizedPreferred || 'default';
}
async function refreshAIChannelsFromServer(preferredId, preferredPayload) {
const response = await apiFetch('/api/config');
if (!response.ok) return false;
currentConfig = await response.json();
currentConfig.ai = ensureAIConfigShape(currentConfig);
selectedAIChannelId = resolveSavedAIChannelId(currentConfig.ai, preferredId, preferredPayload);
renderAIChannelSelect();
writeAIChannelToMainForm(selectedAIChannelId);
if (typeof populateChatAIChannelSelect === 'function') {
populateChatAIChannelSelect(currentConfig.ai);
}
return true;
}
async function persistAIChannelsToServer(successMessage, options = {}) {
if (typeof requirePermission === 'function' && !requirePermission('config:write')) return false;
if (!currentConfig) currentConfig = {};
@@ -2714,7 +2855,7 @@ async function persistAIChannelsToServer(successMessage, options = {}) {
return false;
}
renderAIChannelSelect();
showAIChannelSaveHint('正在保存通道...', true);
showAIChannelSaveHint(settingsT('settingsBasic.aiChannelSaving', '正在保存通道...'), true);
try {
const shouldMergeLatest = options.mergeLatest !== false;
const latestResponse = shouldMergeLatest ? await apiFetch('/api/config') : null;
@@ -2744,17 +2885,7 @@ async function persistAIChannelsToServer(successMessage, options = {}) {
const error = await applyResponse.json().catch(() => ({}));
throw new Error(error.error || '应用通道失败');
}
const response = await apiFetch('/api/config');
if (response.ok) {
currentConfig = await response.json();
currentConfig.ai = ensureAIConfigShape(currentConfig);
selectedAIChannelId = currentConfig.ai.channels[id] ? id : currentConfig.ai.default_channel;
renderAIChannelSelect();
writeAIChannelToMainForm(selectedAIChannelId);
if (typeof populateChatAIChannelSelect === 'function') {
populateChatAIChannelSelect(currentConfig.ai);
}
}
await refreshAIChannelsFromServer(id, channelPayload);
showAIChannelSaveHint(successMessage || '通道已保存', true);
return true;
} catch (error) {
@@ -2768,7 +2899,7 @@ async function persistAIConfigOnlyToServer(successMessage) {
if (typeof requirePermission === 'function' && !requirePermission('config:write')) return false;
if (!currentConfig) return false;
currentConfig.ai = ensureAIConfigShape(currentConfig);
showAIChannelSaveHint('正在保存通道...', true);
showAIChannelSaveHint(settingsT('settingsBasic.aiChannelSaving', '正在保存通道...'), true);
try {
const updateResponse = await apiFetch('/api/config', {
method: 'PUT',
@@ -2784,9 +2915,7 @@ async function persistAIConfigOnlyToServer(successMessage) {
const error = await applyResponse.json().catch(() => ({}));
throw new Error(error.error || '应用通道失败');
}
if (typeof populateChatAIChannelSelect === 'function') {
populateChatAIChannelSelect(currentConfig.ai);
}
await refreshAIChannelsFromServer(selectedAIChannelId);
showAIChannelSaveHint(successMessage || '通道已保存', true);
return true;
} catch (error) {
@@ -2850,7 +2979,7 @@ function createAIChannelFromForm() {
selectedAIChannelId = id;
renderAIChannelSelect();
writeAIChannelToMainForm(id);
showAIChannelSaveHint('新通道尚未保存,填写后点击「保存更改」。', true);
showAIChannelSaveHint(settingsT('settingsBasic.aiChannelNewUnsaved', '新通道尚未保存,填写后点击「保存更改」。'), true);
}
function copyAIChannelFromForm() {
@@ -2862,10 +2991,10 @@ function copyAIChannelFromForm() {
selectedAIChannelId = id;
renderAIChannelSelect();
writeAIChannelToMainForm(id);
showAIChannelSaveHint('复制的通道尚未保存,确认后点击「保存更改」。', true);
showAIChannelSaveHint(settingsT('settingsBasic.aiChannelCopyUnsaved', '复制的通道尚未保存,确认后点击「保存更改」。'), true);
}
function deleteSelectedAIChannel() {
async function deleteSelectedAIChannel() {
if (!currentConfig) return;
currentConfig.ai = ensureAIConfigShape(currentConfig);
const ids = Object.keys(currentConfig.ai.channels || {});
@@ -2883,10 +3012,21 @@ function deleteSelectedAIChannel() {
return;
}
delete currentConfig.ai.channels[id];
currentConfig.ai.default_channel = Object.keys(currentConfig.ai.channels).sort()[0];
delete aiChannelProbeResults[id];
selectedAIChannelBulkIds.delete(id);
const remainingIds = Object.keys(currentConfig.ai.channels || {}).sort();
if (!currentConfig.ai.channels[currentConfig.ai.default_channel]) {
currentConfig.ai.default_channel = remainingIds[0];
}
selectedAIChannelId = currentConfig.ai.default_channel || remainingIds[0];
renderAIChannelSelect();
writeAIChannelToMainForm(currentConfig.ai.default_channel);
persistAIChannelsToServer('通道已删除', { mergeLatest: false });
writeAIChannelToMainForm(selectedAIChannelId);
const saved = await persistAIConfigOnlyToServer(settingsT('settingsBasic.aiChannelDeleted', '通道已删除'));
if (saved) {
renderAIChannelSelect();
writeAIChannelToMainForm(selectedAIChannelId);
}
}
function selectedOrAllAIChannelIdsForProbe() {
@@ -2904,12 +3044,13 @@ async function probeSelectedAIChannels() {
if (typeof requirePermission === 'function' && !requirePermission('config:write')) return;
const ids = selectedOrAllAIChannelIdsForProbe();
if (!ids.length) {
alert('没有可探活的完整通道,请先填写 Base URL、API Key 和模型');
alert(settingsT('settingsBasic.aiChannelProbeNoComplete', '没有可探活的完整通道,请先填写 Base URL、API Key 和模型'));
return;
}
showAIChannelSaveHint(`正在探活 ${ids.length} 个通道...`, true);
showAIChannelSaveHint(settingsT('settingsBasic.aiChannelProbing', '正在探活 {count} 个通道...').replace('{count}', String(ids.length)), true);
ids.forEach((id) => {
aiChannelProbeResults[id] = { status: 'testing', message: '测试中...' };
aiChannelProbeResults[id] = { status: 'testing', message: settingsT('settingsBasic.testing', '测试中...') };
updateAIChannelSelectOption(id);
});
renderAIChannelList();
let okCount = 0;
@@ -2933,13 +3074,14 @@ async function probeSelectedAIChannels() {
if (response.ok && result.success) {
okCount += 1;
const latency = result.latency_ms ? ` ${result.latency_ms}ms` : '';
aiChannelProbeResults[id] = { status: 'ready', message: `可用${latency}` };
aiChannelProbeResults[id] = { status: 'ready', message: settingsT('settingsBasic.aiChannelReadyWithLatency', '可用{latency}').replace('{latency}', latency) };
} else {
aiChannelProbeResults[id] = { status: 'failed', message: (result.error || '连接失败') };
aiChannelProbeResults[id] = { status: 'failed', message: formatConnectionTestError(result.error || settingsT('settingsBasic.testFailed', '连接失败')).message };
}
} catch (error) {
aiChannelProbeResults[id] = { status: 'failed', message: error.message || '测试出错' };
aiChannelProbeResults[id] = { status: 'failed', message: formatConnectionTestError(error.message || settingsT('settingsBasic.testError', '测试出错')).message };
}
updateAIChannelSelectOption(id);
renderAIChannelList();
}
const workers = Array.from({ length: Math.min(AI_CHANNEL_PROBE_CONCURRENCY, ids.length) }, async function () {
@@ -2948,7 +3090,7 @@ async function probeSelectedAIChannels() {
}
});
await Promise.all(workers);
showAIChannelSaveHint(`探活完成:${okCount}/${ids.length} 可用`, okCount === ids.length);
showAIChannelSaveHint(settingsT('settingsBasic.aiChannelProbeDone', '探活完成:{ok}/{total} 可用').replace('{ok}', String(okCount)).replace('{total}', String(ids.length)), okCount === ids.length);
}
async function deleteCheckedAIChannels() {
@@ -2997,7 +3139,17 @@ if (typeof window !== 'undefined') {
window.deleteCheckedAIChannels = deleteCheckedAIChannels;
}
if (typeof document !== 'undefined' && !document.__aiChannelI18nBound) {
document.__aiChannelI18nBound = true;
document.addEventListener('languagechange', function () {
if (!currentConfig?.ai) return;
renderAIChannelSelect();
updateAIChannelEditorChrome(selectedAIChannelId || currentConfig.ai.default_channel);
});
}
function initModelListControls() {
bindAIChannelEditorPreviewSync();
const providerEl = document.getElementById('openai-provider');
if (providerEl && !providerEl.dataset.modelListBound) {
providerEl.dataset.modelListBound = '1';
@@ -3048,6 +3200,9 @@ function bindModelSelect(scope) {
if (!select.value) return;
const input = document.getElementById(inputId);
if (input) input.value = select.value;
if (scope === 'openai') {
syncAIChannelEditorPreview();
}
});
}
@@ -3430,6 +3585,36 @@ async function testHitlAuditModelConnection() {
}
}
function formatConnectionTestError(errorText) {
const raw = String(errorText || '').trim() || settingsT('settingsBasic.testError', '测试出错');
return {
message: raw,
detail: raw
};
}
function setConnectionTestResult(resultEl, state, message, title) {
if (!resultEl) return;
resultEl.classList.remove('is-error', 'is-success', 'is-muted', 'is-visible');
resultEl.style.color = '';
resultEl.textContent = message || '';
resultEl.title = title || '';
if (message) {
resultEl.classList.add('is-visible', state || 'is-muted');
}
}
function showConnectionTestFailure(resultEl, errorText) {
if (!resultEl) return;
const formatted = formatConnectionTestError(errorText);
setConnectionTestResult(
resultEl,
'is-error',
(typeof window.t === 'function' ? window.t('settingsBasic.testFailed') : '连接失败') + ': ' + formatted.message,
formatted.detail || formatted.message
);
}
// 测试OpenAI连接
async function testOpenAIConnection() {
const btn = document.getElementById('test-openai-btn');
@@ -3441,15 +3626,13 @@ async function testOpenAIConnection() {
const model = document.getElementById('openai-model').value.trim();
if (!apiKey || !model) {
resultEl.style.color = 'var(--danger-color, #e53e3e)';
resultEl.textContent = typeof window.t === 'function' ? window.t('settingsBasic.testFillRequired') : '请先填写 API Key 和模型';
setConnectionTestResult(resultEl, 'is-error', typeof window.t === 'function' ? window.t('settingsBasic.testFillRequired') : '请先填写 API Key 和模型');
return;
}
btn.style.pointerEvents = 'none';
btn.style.opacity = '0.5';
resultEl.style.color = 'var(--text-muted, #888)';
resultEl.textContent = typeof window.t === 'function' ? window.t('settingsBasic.testing') : '测试中...';
setConnectionTestResult(resultEl, 'is-muted', typeof window.t === 'function' ? window.t('settingsBasic.testing') : '测试中...');
try {
const response = await apiFetch('/api/config/test-openai', {
@@ -3466,17 +3649,14 @@ async function testOpenAIConnection() {
const result = await response.json();
if (result.success) {
resultEl.style.color = 'var(--success-color, #38a169)';
const latency = result.latency_ms ? ` (${result.latency_ms}ms)` : '';
const modelInfo = result.model ? ` [${result.model}]` : '';
resultEl.textContent = (typeof window.t === 'function' ? window.t('settingsBasic.testSuccess') : '连接成功') + modelInfo + latency;
setConnectionTestResult(resultEl, 'is-success', (typeof window.t === 'function' ? window.t('settingsBasic.testSuccess') : '连接成功') + modelInfo + latency);
} else {
resultEl.style.color = 'var(--danger-color, #e53e3e)';
resultEl.textContent = (typeof window.t === 'function' ? window.t('settingsBasic.testFailed') : '连接失败') + ': ' + (result.error || '未知错误');
showConnectionTestFailure(resultEl, result.error || '未知错误');
}
} catch (error) {
resultEl.style.color = 'var(--danger-color, #e53e3e)';
resultEl.textContent = (typeof window.t === 'function' ? window.t('settingsBasic.testError') : '测试出错') + ': ' + error.message;
showConnectionTestFailure(resultEl, error.message || '测试出错');
} finally {
btn.style.pointerEvents = '';
btn.style.opacity = '';