mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-01 16:38:48 +02:00
Add files via upload
This commit is contained in:
+386
-82
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 = '';
|
||||
|
||||
+126
-95
@@ -3446,23 +3446,27 @@
|
||||
<div class="ai-channel-manager-header">
|
||||
<div>
|
||||
<h4 data-i18n="settingsBasic.openaiConfig">AI 通道配置</h4>
|
||||
<p id="ai-channel-save-hint" class="ai-channel-manager-hint" data-i18n="settingsBasic.aiChannelHint">已保存的通道会显示在左侧;保存后写入 ai.channels,默认通道用于新对话和未指定通道的任务。</p>
|
||||
<p id="ai-channel-save-hint" class="ai-channel-manager-hint" data-i18n="settingsBasic.aiChannelHint">通过下拉切换已保存通道;保存后写入 ai.channels,默认通道用于新对话和未指定通道的任务。</p>
|
||||
</div>
|
||||
<div class="ai-channel-header-actions">
|
||||
<button type="button" class="btn-secondary" onclick="createAIChannelFromForm()" data-i18n="settingsBasic.aiChannelNew">新增</button>
|
||||
<button type="button" class="btn-primary ai-channel-save-btn" onclick="saveSelectedAIChannel()" data-i18n="settingsBasic.aiChannelSave">保存更改</button>
|
||||
</div>
|
||||
<button type="button" class="btn-primary ai-channel-save-btn" onclick="saveSelectedAIChannel()" data-i18n="settingsBasic.aiChannelSave">保存更改</button>
|
||||
</div>
|
||||
<div class="ai-channel-manager-body">
|
||||
<aside class="ai-channel-sidebar" aria-label="AI 通道列表" data-i18n="settingsBasic.aiChannelListAria" data-i18n-attr="aria-label">
|
||||
<div class="ai-channel-sidebar-head">
|
||||
<span data-i18n="settingsBasic.aiChannelSavedList">已保存通道</span>
|
||||
<div class="ai-channel-bulk-actions">
|
||||
<button type="button" class="ai-channel-bulk-btn" onclick="probeSelectedAIChannels()" title="检测所选或全部完整通道是否可用">批量探活</button>
|
||||
<button type="button" class="ai-channel-bulk-btn danger" onclick="deleteCheckedAIChannels()" title="删除已勾选的非默认通道">删除所选</button>
|
||||
<button type="button" class="ai-channel-icon-btn" onclick="createAIChannelFromForm()" title="新增通道" aria-label="新增通道">+</button>
|
||||
</div>
|
||||
<div class="ai-channel-switcher" aria-label="AI 通道选择" data-i18n="settingsBasic.aiChannelListAria" data-i18n-attr="aria-label">
|
||||
<div class="ai-channel-switcher-field">
|
||||
<label for="ai-channel-select" data-i18n="settingsBasic.aiChannelCurrent">当前通道</label>
|
||||
<select id="ai-channel-select" class="ai-channel-switch-select" onchange="selectAIChannelForEditing(this.value)"></select>
|
||||
</div>
|
||||
<div class="ai-channel-switcher-actions">
|
||||
<button type="button" class="btn-secondary" onclick="probeSelectedAIChannels()" data-i18n="settingsBasic.aiChannelBulkProbe" data-i18n-title="settingsBasic.aiChannelBulkProbeTitle" data-i18n-attr="title">批量探活</button>
|
||||
<button type="button" class="btn-secondary" onclick="copyAIChannelFromForm()" data-i18n="settingsBasic.aiChannelCopy">复制</button>
|
||||
<button type="button" class="btn-secondary" onclick="setSelectedAIChannelDefault()" data-i18n="settingsBasic.aiChannelSetDefault">设为默认</button>
|
||||
<button type="button" class="btn-secondary danger" onclick="deleteSelectedAIChannel()" data-i18n="settingsBasic.aiChannelDelete">删除</button>
|
||||
</div>
|
||||
<select id="ai-channel-select" class="ai-channel-native-select" onchange="selectAIChannelForEditing(this.value)" aria-hidden="true" tabindex="-1"></select>
|
||||
<div id="ai-channel-list" class="ai-channel-list" aria-live="polite"></div>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="ai-channel-editor">
|
||||
<div class="ai-channel-editor-head">
|
||||
<div>
|
||||
@@ -3470,91 +3474,118 @@
|
||||
<h5 id="ai-channel-editor-title">-</h5>
|
||||
<p id="ai-channel-editor-meta">-</p>
|
||||
</div>
|
||||
<div class="ai-channel-editor-actions">
|
||||
<button type="button" class="btn-secondary" onclick="setSelectedAIChannelDefault()" data-i18n="settingsBasic.aiChannelSetDefault">设为默认</button>
|
||||
<button type="button" class="btn-secondary" onclick="copyAIChannelFromForm()" data-i18n="settingsBasic.aiChannelCopy">复制</button>
|
||||
<button type="button" class="btn-secondary danger" onclick="deleteSelectedAIChannel()" data-i18n="settingsBasic.aiChannelDelete">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-form ai-channel-editor-form">
|
||||
<div class="form-group">
|
||||
<label for="ai-channel-name" data-i18n="settingsBasic.aiChannelName">通道名称</label>
|
||||
<input type="text" id="ai-channel-name" placeholder="Qwen Max" maxlength="24" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-provider" data-i18n="settingsBasic.apiProvider">API 提供商</label>
|
||||
<select id="openai-provider" style="width: 100%; padding: 0.5rem 0.75rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 6px; background: var(--card-bg, #fff); color: var(--text-color, #2d3748); font-size: 0.875rem;">
|
||||
<option value="openai_compatible" data-i18n="settingsBasic.providerOpenAI">OpenAI / 兼容 OpenAI 协议</option>
|
||||
<option value="claude" data-i18n="settingsBasic.providerClaude">Claude (Anthropic Messages API)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-base-url">Base URL <span style="color: red;">*</span></label>
|
||||
<input type="text" id="openai-base-url" data-i18n="settingsBasic.openaiBaseUrlPlaceholder" data-i18n-attr="placeholder" placeholder="https://api.openai.com/v1" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-api-key">API Key <span style="color: red;">*</span></label>
|
||||
<input type="password" id="openai-api-key" data-i18n="settingsBasic.openaiApiKeyPlaceholder" data-i18n-attr="placeholder" placeholder="输入OpenAI API Key" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-model"><span data-i18n="settingsBasic.model">模型</span> <span style="color: red;">*</span></label>
|
||||
<div class="model-pick-row">
|
||||
<input type="text" id="openai-model" class="model-pick-input" data-i18n="settingsBasic.modelPlaceholder" data-i18n-attr="placeholder" placeholder="gpt-4" required />
|
||||
<select id="openai-model-select" class="model-pick-native" style="display: none;" title="" aria-hidden="true" tabindex="-1">
|
||||
<option value="" disabled data-i18n="settingsBasic.modelsListSelectPlaceholder">请选择模型</option>
|
||||
</select>
|
||||
<a href="javascript:void(0)" id="fetch-openai-models-btn" class="model-pick-fetch-link" onclick="fetchModelList('openai')" data-i18n="settingsBasic.fetchModels">获取列表</a>
|
||||
</div>
|
||||
<small id="fetch-openai-models-hint" class="form-hint" style="display: none; font-size: 0.75rem; margin-top: 4px;"></small>
|
||||
<span id="fetch-openai-models-result" style="font-size: 0.75rem; margin-top: 2px; display: block;"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-max-total-tokens"><span data-i18n="settingsBasic.maxTotalTokens">最大上下文 Token 数</span></label>
|
||||
<input type="number" id="openai-max-total-tokens" data-i18n="settingsBasic.maxTotalTokensPlaceholder" data-i18n-attr="placeholder" placeholder="120000" min="1000" step="1000" />
|
||||
<small style="color: var(--text-muted, #718096); font-size: 0.75rem;" data-i18n="settingsBasic.maxTotalTokensHint">内存压缩和攻击链构建共用此配置,默认 120000</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-max-completion-tokens"><span data-i18n="settingsBasic.maxCompletionTokens">最大输出 Token 数</span></label>
|
||||
<input type="number" id="openai-max-completion-tokens" data-i18n="settingsBasic.maxCompletionTokensPlaceholder" data-i18n-attr="placeholder" placeholder="16384" min="1" step="256" />
|
||||
<small style="color: var(--text-muted, #718096); font-size: 0.75rem;" data-i18n="settingsBasic.maxCompletionTokensHint">单次模型回复的输出上限,默认 16384</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="settingsBasic.openaiReasoningTitle">推理设置</label>
|
||||
<small class="form-hint" data-i18n="settingsBasic.openaiReasoningHint">作为该 AI 通道的默认推理设置;对话页「会话设置」可覆盖。</small>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; align-items: center;">
|
||||
<label for="openai-reasoning-mode" style="font-size: 0.8125rem;" data-i18n="chat.reasoningModeLabel">模式</label>
|
||||
<select id="openai-reasoning-mode" style="min-width: 140px; padding: 0.35rem 0.5rem; border-radius: 6px; border: 1px solid var(--border-color, #e2e8f0);">
|
||||
<option value="auto" data-i18n="chat.reasoningModeAuto">自动</option>
|
||||
<option value="on" data-i18n="chat.reasoningModeOn">开启</option>
|
||||
<option value="off" data-i18n="chat.reasoningModeOff">关闭</option>
|
||||
</select>
|
||||
<label for="openai-reasoning-effort" style="font-size: 0.8125rem;" data-i18n="chat.reasoningEffortLabel">强度</label>
|
||||
<select id="openai-reasoning-effort" style="min-width: 140px; padding: 0.35rem 0.5rem; border-radius: 6px; border: 1px solid var(--border-color, #e2e8f0);">
|
||||
<option value="" data-i18n="chat.reasoningEffortUnset">不指定</option>
|
||||
<option value="low">low</option>
|
||||
<option value="medium">medium</option>
|
||||
<option value="high">high</option>
|
||||
<option value="xhigh">xhigh</option>
|
||||
<option value="max">max</option>
|
||||
</select>
|
||||
<label for="openai-reasoning-profile" style="font-size: 0.8125rem;" data-i18n="settingsBasic.openaiReasoningProfile">线路</label>
|
||||
<select id="openai-reasoning-profile" style="min-width: 220px; padding: 0.35rem 0.5rem; border-radius: 6px; border: 1px solid var(--border-color, #e2e8f0);">
|
||||
<option value="auto">auto</option>
|
||||
<option value="deepseek_compat">deepseek_compat</option>
|
||||
<option value="openai_compat">openai_compat</option>
|
||||
<option value="output_config_effort">output_config_effort</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="checkbox-label" style="margin-top: 8px;">
|
||||
<input type="checkbox" id="openai-reasoning-allow-client" class="modern-checkbox" checked />
|
||||
<span class="checkbox-custom"></span>
|
||||
<span class="checkbox-text" data-i18n="settingsBasic.openaiReasoningAllowClient">允许对话页覆盖推理选项</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 2px;">
|
||||
<a href="javascript:void(0)" id="test-openai-btn" onclick="testOpenAIConnection()" style="font-size: 0.8125rem; color: var(--accent-color, #3182ce); text-decoration: none; cursor: pointer; user-select: none;" data-i18n="settingsBasic.testConnection">测试连接</a>
|
||||
<span id="test-openai-result" style="font-size: 0.8125rem;"></span>
|
||||
</div>
|
||||
<section class="ai-channel-form-section">
|
||||
<div class="ai-channel-form-section-head">
|
||||
<div>
|
||||
<h6 data-i18n="settingsBasic.aiChannelConnectionSection">连接信息</h6>
|
||||
<p data-i18n="settingsBasic.aiChannelConnectionHint">先确认服务商、地址、密钥和模型,测试连接会使用这些信息。</p>
|
||||
</div>
|
||||
<div class="ai-channel-section-actions">
|
||||
<button type="button" id="test-openai-btn" class="btn-secondary" onclick="testOpenAIConnection()" data-i18n="settingsBasic.testConnection">测试连接</button>
|
||||
<span id="test-openai-result" class="form-inline-result connection-test-result"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-channel-form-grid">
|
||||
<div class="form-group">
|
||||
<label for="ai-channel-name" data-i18n="settingsBasic.aiChannelName">通道名称</label>
|
||||
<input type="text" id="ai-channel-name" placeholder="Qwen Max" maxlength="24" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-provider" data-i18n="settingsBasic.apiProvider">API 提供商</label>
|
||||
<select id="openai-provider">
|
||||
<option value="openai_compatible" data-i18n="settingsBasic.providerOpenAI">OpenAI / 兼容 OpenAI 协议</option>
|
||||
<option value="claude" data-i18n="settingsBasic.providerClaude">Claude (Anthropic Messages API)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group span-2">
|
||||
<label for="openai-base-url">Base URL <span class="required-mark">*</span></label>
|
||||
<input type="text" id="openai-base-url" data-i18n="settingsBasic.openaiBaseUrlPlaceholder" data-i18n-attr="placeholder" placeholder="https://api.openai.com/v1" required />
|
||||
</div>
|
||||
<div class="form-group span-2">
|
||||
<label for="openai-api-key">API Key <span class="required-mark">*</span></label>
|
||||
<input type="password" id="openai-api-key" data-i18n="settingsBasic.openaiApiKeyPlaceholder" data-i18n-attr="placeholder" placeholder="输入OpenAI API Key" required />
|
||||
</div>
|
||||
<div class="form-group span-2">
|
||||
<label for="openai-model"><span data-i18n="settingsBasic.model">模型</span> <span class="required-mark">*</span></label>
|
||||
<div class="model-pick-row">
|
||||
<input type="text" id="openai-model" class="model-pick-input" data-i18n="settingsBasic.modelPlaceholder" data-i18n-attr="placeholder" placeholder="gpt-4" required />
|
||||
<select id="openai-model-select" class="model-pick-native" style="display: none;" title="" aria-hidden="true" tabindex="-1">
|
||||
<option value="" disabled data-i18n="settingsBasic.modelsListSelectPlaceholder">请选择模型</option>
|
||||
</select>
|
||||
<a href="javascript:void(0)" id="fetch-openai-models-btn" class="model-pick-fetch-link" onclick="fetchModelList('openai')" data-i18n="settingsBasic.fetchModels">获取列表</a>
|
||||
</div>
|
||||
<small id="fetch-openai-models-hint" class="form-hint" style="display: none;"></small>
|
||||
<span id="fetch-openai-models-result" class="form-inline-result"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ai-channel-form-section">
|
||||
<div class="ai-channel-form-section-head">
|
||||
<div>
|
||||
<h6 data-i18n="settingsBasic.aiChannelLimitsSection">额度设置</h6>
|
||||
<p data-i18n="settingsBasic.aiChannelLimitsHint">Token 上限控制上下文窗口和单次输出。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-channel-form-grid">
|
||||
<div class="form-group">
|
||||
<label for="openai-max-total-tokens"><span data-i18n="settingsBasic.maxTotalTokens">最大上下文 Token 数</span></label>
|
||||
<input type="number" id="openai-max-total-tokens" data-i18n="settingsBasic.maxTotalTokensPlaceholder" data-i18n-attr="placeholder" placeholder="120000" min="1000" step="1000" />
|
||||
<small class="form-hint" data-i18n="settingsBasic.maxTotalTokensHint">内存压缩和攻击链构建共用此配置,默认 120000</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-max-completion-tokens"><span data-i18n="settingsBasic.maxCompletionTokens">最大输出 Token 数</span></label>
|
||||
<input type="number" id="openai-max-completion-tokens" data-i18n="settingsBasic.maxCompletionTokensPlaceholder" data-i18n-attr="placeholder" placeholder="16384" min="1" step="256" />
|
||||
<small class="form-hint" data-i18n="settingsBasic.maxCompletionTokensHint">单次模型回复的输出上限,默认 16384</small>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details class="ai-channel-form-section ai-channel-advanced-section">
|
||||
<summary>
|
||||
<span>
|
||||
<strong data-i18n="settingsBasic.openaiReasoningTitle">推理设置</strong>
|
||||
<small data-i18n="settingsBasic.openaiReasoningHint">作为该 AI 通道的默认推理设置;对话页「会话设置」可覆盖。</small>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="ai-channel-reasoning-grid">
|
||||
<div class="form-group">
|
||||
<label for="openai-reasoning-mode" data-i18n="chat.reasoningModeLabel">模式</label>
|
||||
<select id="openai-reasoning-mode">
|
||||
<option value="auto" data-i18n="chat.reasoningModeAuto">自动</option>
|
||||
<option value="on" data-i18n="chat.reasoningModeOn">开启</option>
|
||||
<option value="off" data-i18n="chat.reasoningModeOff">关闭</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-reasoning-effort" data-i18n="chat.reasoningEffortLabel">强度</label>
|
||||
<select id="openai-reasoning-effort">
|
||||
<option value="" data-i18n="chat.reasoningEffortUnset">不指定</option>
|
||||
<option value="low">low</option>
|
||||
<option value="medium">medium</option>
|
||||
<option value="high">high</option>
|
||||
<option value="xhigh">xhigh</option>
|
||||
<option value="max">max</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-reasoning-profile" data-i18n="settingsBasic.openaiReasoningProfile">线路</label>
|
||||
<select id="openai-reasoning-profile">
|
||||
<option value="auto">auto</option>
|
||||
<option value="deepseek_compat">deepseek_compat</option>
|
||||
<option value="openai_compat">openai_compat</option>
|
||||
<option value="output_config_effort">output_config_effort</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label class="checkbox-label ai-channel-reasoning-toggle">
|
||||
<input type="checkbox" id="openai-reasoning-allow-client" class="modern-checkbox" checked />
|
||||
<span class="checkbox-custom"></span>
|
||||
<span class="checkbox-text" data-i18n="settingsBasic.openaiReasoningAllowClient">允许对话页覆盖推理选项</span>
|
||||
</label>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user