diff --git a/.DS_Store b/.DS_Store index c242eec..c358fbf 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/TalkingQ-H5.html b/TalkingQ-H5.html index 545af1d..98a3805 100644 --- a/TalkingQ-H5.html +++ b/TalkingQ-H5.html @@ -1318,6 +1318,496 @@ body.login-mode .page-container { gap: 5px; } +/* ========== SYSTEM SETTINGS ========== */ +.settings-page-header, +.settings-layout { + width: 100%; + max-width: 1320px; + margin-left: auto; + margin-right: auto; +} + +.settings-page-header { + margin-bottom: 20px; +} + +.settings-layout { + display: grid; + grid-template-columns: 220px minmax(0, 1fr); + gap: 20px; + align-items: start; +} + +.settings-sidebar { + position: sticky; + top: 0; + padding: 14px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg-card); + box-shadow: 0 2px 8px rgba(0,0,0,0.03); +} + +.settings-sidebar-title { + padding: 4px 8px 10px; + color: var(--text-muted); + font-size: 11px; + font-weight: 700; +} + +.settings-sidebar-nav { + display: flex; + flex-direction: column; + gap: 4px; +} + +.settings-nav-item { + width: 100%; + min-height: 54px; + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border: 1px solid transparent; + border-radius: 8px; + color: var(--text-secondary); + text-align: left; + transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease; +} + +.settings-nav-item:hover { + color: var(--text-primary); + background: var(--bg-base); +} + +.settings-nav-item.active { + color: var(--primary-dark); + border-color: rgba(82,196,26,0.22); + background: rgba(82,196,26,0.08); +} + +.settings-nav-icon { + width: 32px; + height: 32px; + flex: 0 0 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 8px; + background: var(--bg-base); + color: var(--text-muted); +} + +.settings-nav-item.active .settings-nav-icon { + color: var(--primary-dark); + background: rgba(82,196,26,0.12); +} + +.settings-nav-icon svg { + width: 16px; + height: 16px; +} + +.settings-nav-item > span:last-child { + min-width: 0; +} + +.settings-nav-item strong, +.settings-nav-item small { + display: block; +} + +.settings-nav-item strong { + color: inherit; + font-size: 13px; + font-weight: 650; + line-height: 1.35; +} + +.settings-nav-item small { + margin-top: 2px; + color: var(--text-muted); + font-size: 11px; + line-height: 1.3; +} + +.settings-sidebar-status { + margin-top: 14px; + padding: 12px 10px 2px; + border-top: 1px solid var(--border); +} + +.settings-status-line { + display: flex; + align-items: center; + gap: 7px; + color: var(--text-secondary); + font-size: 12px; + font-weight: 600; +} + +.settings-status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--primary); + box-shadow: 0 0 0 3px rgba(82,196,26,0.12); +} + +.settings-sidebar-status small { + display: block; + margin-top: 6px; + color: var(--text-muted); + font-size: 11px; +} + +.settings-content { + min-width: 0; + display: flex; + flex-direction: column; + gap: 16px; +} + +.settings-two-column { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + align-items: stretch; +} + +.settings-section-card { + min-width: 0; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg-card); + box-shadow: 0 2px 8px rgba(0,0,0,0.03); + scroll-margin-top: 16px; +} + +.settings-two-column .settings-section-card { + display: flex; + flex-direction: column; +} + +.settings-section-header { + min-height: 72px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 16px 20px; + border-bottom: 1px solid var(--border); +} + +.settings-section-heading { + min-width: 0; + display: flex; + align-items: center; + gap: 12px; +} + +.settings-section-icon { + width: 36px; + height: 36px; + flex: 0 0 36px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 9px; + color: var(--primary-dark); + background: rgba(82,196,26,0.1); +} + +.settings-section-icon svg { + width: 18px; + height: 18px; +} + +.settings-section-heading h2 { + color: var(--text-primary); + font-size: 15px; + font-weight: 700; + line-height: 1.4; +} + +.settings-section-heading p { + margin-top: 2px; + color: var(--text-muted); + font-size: 12px; + line-height: 1.4; +} + +.settings-section-count { + flex-shrink: 0; + padding: 5px 9px; + border-radius: 999px; + color: var(--primary-dark); + background: rgba(82,196,26,0.09); + font-size: 11px; + font-weight: 650; +} + +.settings-section-body { + min-width: 0; + padding: 20px; +} + +.settings-two-column .settings-section-body { + flex: 1; +} + +.settings-section-body > .form-group:last-child { + margin-bottom: 0; +} + +.settings-section-footer { + display: flex; + align-items: center; + gap: 8px; + min-height: 62px; + padding: 12px 20px; + border-top: 1px solid var(--border); + background: rgba(0,0,0,0.012); +} + +.settings-key-name { + display: flex; + align-items: center; + gap: 9px; + white-space: nowrap; +} + +.settings-api-table { + border: 1px solid var(--border); + border-radius: 9px; +} + +.settings-key-name strong { + font-size: 13px; + font-weight: 650; +} + +.settings-key-avatar { + width: 30px; + height: 30px; + flex: 0 0 30px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 8px; + color: var(--primary-dark); + background: rgba(82,196,26,0.09); +} + +.settings-key-avatar svg { + width: 14px; + height: 14px; +} + +.settings-key-code { + display: inline-flex; + padding: 5px 8px; + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-secondary); + background: var(--bg-base); + font-size: 12px; + white-space: nowrap; +} + +.settings-muted-cell { + color: var(--text-secondary); + font-size: 12px; + white-space: nowrap; +} + +.settings-table-actions { + display: flex; + align-items: center; + gap: 5px; + white-space: nowrap; +} + +.settings-icon-btn { + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid transparent; + border-radius: 7px; + color: var(--text-muted); + transition: var(--transition); +} + +.settings-icon-btn:hover { + color: var(--primary-dark); + border-color: rgba(82,196,26,0.24); + background: rgba(82,196,26,0.08); +} + +.settings-icon-btn.danger:hover { + color: var(--danger); + border-color: rgba(255,77,79,0.22); + background: rgba(255,77,79,0.07); +} + +.settings-icon-btn svg { + width: 15px; + height: 15px; +} + +.settings-empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 190px; + padding: 28px 20px; + border: 1px dashed var(--border-dark); + border-radius: 9px; + text-align: center; + background: rgba(0,0,0,0.01); +} + +.settings-empty-state > span { + width: 42px; + height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; + border-radius: 10px; + color: var(--primary-dark); + background: rgba(82,196,26,0.1); +} + +.settings-empty-state > span svg { + width: 20px; + height: 20px; +} + +.settings-empty-state strong { + font-size: 14px; +} + +.settings-empty-state p { + margin: 5px 0 14px; + color: var(--text-muted); + font-size: 12px; +} + +.settings-security-note { + display: flex; + align-items: flex-start; + gap: 9px; + margin-top: 14px; + padding: 10px 12px; + border-radius: 8px; + color: var(--text-secondary); + background: rgba(82,196,26,0.055); + font-size: 12px; + line-height: 1.5; +} + +.settings-security-note svg { + width: 15px; + height: 15px; + flex-shrink: 0; + margin-top: 1px; + color: var(--primary-dark); +} + +.settings-checkbox-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 12px 0 2px; + border-top: 1px solid var(--border); +} + +.settings-checkbox-row strong, +.settings-checkbox-row small { + display: block; +} + +.settings-checkbox-row strong { + font-size: 13px; + font-weight: 600; +} + +.settings-checkbox-row small { + margin-top: 3px; + color: var(--text-muted); + font-size: 11px; +} + +.settings-content .notification-principles { + gap: 8px; + margin-bottom: 14px; +} + +.settings-content .notification-principles > div { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + padding: 10px 12px; + border-radius: 8px; + background: var(--bg-base); +} + +.settings-principle-icon { + width: 30px; + height: 30px; + flex: 0 0 30px; + display: inline-flex !important; + align-items: center; + justify-content: center; + border-radius: 7px; + color: var(--primary-dark); + background: var(--bg-card); +} + +.settings-principle-icon svg { + width: 15px; + height: 15px; +} + +.settings-content .notification-principles strong, +.settings-content .notification-principles small { + display: block; +} + +.settings-content .notification-principles strong { + margin: 0; + font-size: 12px; +} + +.settings-content .notification-principles small { + margin-top: 2px; + overflow: hidden; + color: var(--text-muted); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-content .notification-rule-row { + grid-template-columns: minmax(0, 1fr) 230px; + gap: 18px; + padding: 16px; + border-radius: 9px; + background: rgba(0,0,0,0.012); +} + +.settings-content .notification-rule-controls { + padding-left: 16px; + border-left: 1px solid var(--border); +} + .tenant-selector { display: flex; align-items: center; @@ -2169,13 +2659,30 @@ select.form-control { appearance: none; cursor: pointer; } display: flex; align-items: center; justify-content: center; - z-index: 1000; + z-index: 20000; backdrop-filter: blur(2px); opacity: 0; pointer-events: none; transition: opacity 0.2s; } +body.modal-open { + overflow: hidden; +} + +body.modal-open .bailian-config-scroll, +body.modal-open .page-content, +body.modal-open .main-content { + scrollbar-width: none; +} + +body.modal-open .bailian-config-scroll::-webkit-scrollbar, +body.modal-open .page-content::-webkit-scrollbar, +body.modal-open .main-content::-webkit-scrollbar { + width: 0; + height: 0; +} + .modal-overlay.open { opacity: 1; pointer-events: all; @@ -2351,8 +2858,14 @@ select.form-control { appearance: none; cursor: pointer; } line-height: 1.4; } -[data-tooltip]:hover::after { - opacity: 1; +[data-tooltip]:hover::after { + opacity: 1; + transform: translateX(-50%) translateY(-2px); +} + +[data-tooltip]:focus::after, +[data-tooltip]:focus-visible::after { + opacity: 1; transform: translateX(-50%) translateY(-2px); } @@ -2367,6 +2880,38 @@ select.form-control { appearance: none; cursor: pointer; } } /* ========== RESPONSIVE ========== */ +@media (max-width: 1180px) { + .settings-layout { + grid-template-columns: minmax(0, 1fr); + } + + .settings-sidebar { + position: sticky; + top: -1px; + z-index: 5; + padding: 8px; + } + + .settings-sidebar-title, + .settings-sidebar-status { + display: none; + } + + .settings-sidebar-nav { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 6px; + } + + .settings-nav-item { + min-height: 48px; + } + + .settings-section-card { + scroll-margin-top: 82px; + } +} + @media (max-width: 1024px) { .charts-grid { grid-template-columns: 1fr; } .config-panels { grid-template-columns: 1fr; } @@ -2376,6 +2921,18 @@ select.form-control { appearance: none; cursor: pointer; } .session-filter-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); } .notification-principles { grid-template-columns: 1fr; } .notification-rule-row { grid-template-columns: 1fr; } + .settings-content .notification-principles { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + .settings-content .notification-rule-row { + grid-template-columns: 1fr; + } + .settings-content .notification-rule-controls { + padding-top: 12px; + padding-left: 0; + border-top: 1px solid var(--border); + border-left: 0; + } .notif-detail-meta { grid-template-columns: 1fr; } #replayPanel { position: relative !important; } } @@ -2390,6 +2947,21 @@ select.form-control { appearance: none; cursor: pointer; } } .sidebar.mobile-open { transform: translateX(0); } .form-row { grid-template-columns: 1fr; } + .settings-two-column { + grid-template-columns: 1fr; + } + .settings-sidebar-nav { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .settings-section-card { + scroll-margin-top: 138px; + } + .settings-content .notification-principles { + grid-template-columns: 1fr; + } + .settings-content .notification-principles small { + white-space: normal; + } .kpi-grid { grid-template-columns: repeat(2, 1fr); } .session-drilldown-banner { align-items: flex-start; @@ -2416,6 +2988,58 @@ select.form-control { appearance: none; cursor: pointer; } } } +@media (max-width: 560px) { + .settings-page-header { + margin-bottom: 16px; + } + + .settings-nav-item { + min-height: 44px; + padding: 6px 8px; + } + + .settings-nav-icon { + width: 28px; + height: 28px; + flex-basis: 28px; + } + + .settings-nav-item small { + display: none; + } + + .settings-section-header { + min-height: 68px; + align-items: flex-start; + flex-direction: column; + padding: 14px 16px; + } + + .settings-section-header > .btn { + width: 100%; + justify-content: center; + } + + .settings-section-body { + padding: 16px; + } + + .settings-section-footer { + align-items: stretch; + flex-direction: column; + padding: 12px 16px; + } + + .settings-section-footer .btn { + width: 100%; + justify-content: center; + } + + .settings-section-count { + margin-left: 48px; + } +} + @media (max-width: 768px) { .kpi-grid { grid-template-columns: 1fr; } } @@ -4733,24 +5357,24 @@ select.form-control { appearance: none; cursor: pointer; } .kb-type-grid { grid-template-columns: 1fr; } - + .file-compare { grid-template-columns: 1fr; } - + .kb-detail-header { flex-direction: column; } - + .kb-detail-actions { width: 100%; justify-content: flex-start; } - + .wizard-steps { padding: 0; } - + .wizard-step-label { display: none; } @@ -4763,12 +5387,12 @@ select.form-control { appearance: none; cursor: pointer; } .no-print { display: none !important; } - + .main-content { width: 100% !important; margin: 0 !important; } - + .page-container { padding: 0 !important; } @@ -5775,225 +6399,6 @@ select.form-control { appearance: none; cursor: pointer; } color: var(--text-muted); } -.selected-scene-card { - width: 100%; - display: flex; - align-items: center; - gap: 12px; - padding: 14px; - border: 1px solid var(--border); - border-radius: 8px; - background: #fbfdfb; - cursor: pointer; - text-align: left; - transition: var(--transition); -} - -.selected-scene-card:hover { - border-color: var(--primary-light); - background: var(--primary-bg); -} - -.selected-scene-icon, -.scene-modal-icon { - width: 38px; - height: 38px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - background: var(--primary-bg); - color: var(--primary); - flex-shrink: 0; -} - -.selected-scene-main { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 4px; -} - -.selected-scene-name { - font-size: 14px; - font-weight: 700; - color: var(--text-primary); -} - -.selected-scene-version { - font-size: 12px; - line-height: 1.5; - color: var(--text-muted); -} - -.selected-scene-action { - color: var(--text-muted); - display: flex; - align-items: center; -} - -.scene-modal-content { - display: flex; - flex-direction: column; - gap: 20px; -} - -.scene-modal-section { - display: flex; - flex-direction: column; - gap: 10px; -} - -.scene-modal-title { - font-size: 13px; - font-weight: 700; - color: var(--text-primary); -} - -.scene-modal-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; -} - -.scene-modal-card { - display: flex; - align-items: center; - gap: 10px; - padding: 12px; - border: 1px solid var(--border); - border-radius: 8px; - cursor: pointer; - transition: var(--transition); - background: var(--bg-white); -} - -.scene-modal-card input, -.scene-version-card input { - display: none; -} - -.scene-modal-card:hover, -.scene-version-card:hover { - border-color: var(--primary-light); - background: var(--primary-bg); -} - -.scene-modal-card.selected, -.scene-version-card.selected { - border-color: var(--primary); - background: var(--primary-bg); - box-shadow: 0 0 0 2px rgba(82,196,26,0.08); -} - -.scene-modal-card.selected .scene-modal-icon { - background: var(--primary); - color: #fff; -} - -.scene-modal-card[data-tooltip]::after { - width: 220px; - white-space: normal; - text-align: left; - line-height: 1.5; - padding: 8px 10px; -} - -.scene-modal-name { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); -} - -.scene-version-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; -} - -.scene-version-card { - display: flex; - flex-direction: column; - gap: 6px; - padding: 14px; - border: 1px solid var(--border); - border-radius: 8px; - cursor: pointer; - transition: var(--transition); - background: var(--bg-white); -} - -.scene-version-name { - font-size: 14px; - font-weight: 700; - color: var(--text-primary); -} - -.scene-version-desc { - font-size: 12px; - line-height: 1.6; - color: var(--text-secondary); -} - -/* 场景模板网格 */ -.scene-template-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; -} - -.scene-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - padding: 12px 8px; - border: 1px solid var(--border); - border-radius: var(--radius); - cursor: pointer; - transition: var(--transition); - text-align: center; -} - -.scene-item:hover { - border-color: var(--primary-light); - background: var(--primary-bg); -} - -.scene-item.selected { - border-color: var(--primary); - background: var(--primary-bg); -} - -.scene-item span:first-child { - width: 32px; - height: 32px; - border-radius: var(--radius-sm); - background: var(--bg-base); - display: flex; - align-items: center; - justify-content: center; - color: var(--text-secondary); - transition: var(--transition); -} - -.scene-item.selected span:first-child { - background: var(--primary); - color: #fff; -} - -.scene-item span:last-child { - font-size: 11px; - color: var(--text-secondary); - line-height: 1.3; -} - -.scene-item.selected span:last-child { - color: var(--primary-dark); - font-weight: 500; -} - /* 模块标签 */ .module-tabs { display: flex; @@ -6687,33 +7092,21 @@ select.form-control-sm { .agent-config-left { width: auto; } - + .agent-config-right { height: 520px; } } @media (max-width: 768px) { - .scene-template-grid { - grid-template-columns: repeat(2, 1fr); - } - - .scene-modal-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .scene-version-grid { - grid-template-columns: 1fr; - } - .module-tabs { flex-wrap: wrap; } - + .module-tab { flex: 1 1 45%; } - + .skill-grid { grid-template-columns: repeat(3, 1fr); } @@ -6736,13 +7129,11 @@ select.form-control-sm { .bailian-split-grid, .bailian-filter-summary, - .bailian-edge-grid, .bailian-check-grid, .bailian-variable-grid, .bailian-mode-grid, .bailian-model-mode-grid, - .bailian-mini-grid, - .bailian-budget-grid { + .bailian-mini-grid { grid-template-columns: 1fr; } @@ -6750,6 +7141,26 @@ select.form-control-sm { grid-template-columns: 1fr; } + .bailian-command-editor-grid { + grid-template-columns: 1fr; + } + + .bailian-command-library-card { + grid-template-columns: 18px minmax(0, 1fr); + } + + .bailian-command-library-actions, + .bailian-command-editor-actions, + .bailian-command-head-actions { + width: 100%; + justify-content: flex-start; + flex-wrap: wrap; + } + + .bailian-command-library-actions { + grid-column: 2; + } + .bailian-filter-row { grid-template-columns: 1fr; } @@ -6758,8 +7169,12 @@ select.form-control-sm { grid-template-columns: 1fr; } + .bailian-mcp-tool-row { + grid-template-columns: 1fr; + } + .bailian-section-inline-action, - .bailian-filter-preview, + .bailian-filter-summary, .bailian-feature-head, .bailian-filter-head { flex-direction: column; @@ -6799,7 +7214,7 @@ select.form-control-sm { .hotword-select-list { grid-template-columns: 1fr; } - + .config-row { flex-direction: column; align-items: flex-start; @@ -7762,6 +8177,12 @@ body.agent-workbench-mode .page-container { .bailian-agent-config { --agent-green: #52c41a; --agent-green-dark: #389e0d; + --agent-primary: rgba(82,196,26,.82); + --agent-primary-dark: rgba(82,196,26,.88); + --agent-primary-hover: rgba(82,196,26,.96); + --agent-primary-soft: rgba(246,255,237,.78); + --agent-primary-border: rgba(82,196,26,.28); + --agent-accent: rgba(115,209,61,.62); --agent-ink: #111827; --agent-text: #263142; --agent-muted: #7b8494; @@ -7840,10 +8261,31 @@ body.agent-workbench-mode .page-container { .bailian-agent-subtitle { margin-top: 4px; + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; font-size: 12px; color: var(--agent-muted); } +.bailian-autosave-status { + display: inline-flex; + align-items: center; + height: 22px; + padding: 0 8px; + border-radius: 999px; + background: var(--agent-primary-soft); + color: var(--agent-green-dark); + font-size: 11px; + font-weight: 700; +} + +.bailian-autosave-status.saving { + background: #e6f4ff; + color: #096dd9; +} + .bailian-header-actions { justify-content: flex-end; gap: 10px; @@ -7857,7 +8299,6 @@ body.agent-workbench-mode .page-container { .bailian-run-btn, .bailian-add-round, .bailian-add-card, -.bailian-scene-card, .bailian-model-select, .bailian-step-tab { font-family: inherit; @@ -7943,6 +8384,18 @@ body.agent-workbench-mode .page-container { font-size: 13px; } +.bailian-action-btn.danger { + color: var(--danger); + border-color: #ffccc7; + background: #fff; +} + +.bailian-action-btn.danger:hover { + color: #cf1322; + border-color: #ffa39e; + background: #fff2f0; +} + .bailian-action-icon { width: 40px; padding: 0; @@ -7950,15 +8403,15 @@ body.agent-workbench-mode .page-container { .bailian-primary-btn, .bailian-run-btn { - border-color: var(--agent-ink); - background: var(--agent-ink); + border-color: var(--agent-primary-dark); + background: var(--agent-primary-dark); color: #fff; } .bailian-primary-btn:hover, .bailian-run-btn:hover { - border-color: #243044; - background: #243044; + border-color: var(--agent-primary-hover); + background: var(--agent-primary-hover); color: #fff; } @@ -7977,8 +8430,8 @@ body.agent-workbench-mode .page-container { .bailian-status-pill.published { color: var(--agent-green-dark); - background: #f6ffed; - border: 1px solid #b7eb8f; + background: var(--agent-primary-soft); + border: 1px solid var(--agent-primary-border); } .bailian-status-pill.editing { @@ -8014,7 +8467,7 @@ body.agent-workbench-mode .page-container { height: 72px; flex-shrink: 0; display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: stretch; border-bottom: 1px solid var(--agent-border); background: #fff; @@ -8023,11 +8476,11 @@ body.agent-workbench-mode .page-container { .bailian-step-tab { position: relative; display: flex; - flex-direction: column; + flex-direction: row; align-items: center; justify-content: center; - gap: 6px; - padding: 0 12px; + gap: 7px; + padding: 0 10px; color: #8a93a3; background: #fff; transition: color .18s ease, background .18s ease; @@ -8101,6 +8554,14 @@ body.agent-workbench-mode .page-container { scrollbar-color: #dce1eb transparent; } +.bailian-config-footer { + flex-shrink: 0; + padding: 14px 24px 18px; + border-top: 1px solid var(--agent-border); + background: rgba(255, 255, 255, .96); + box-shadow: 0 -4px 14px rgba(17, 24, 39, .03); +} + .bailian-config-scroll::-webkit-scrollbar, .bailian-preview-pane::-webkit-scrollbar { width: 4px; @@ -8143,7 +8604,7 @@ body.agent-workbench-mode .page-container { align-items: center; justify-content: center; border-radius: 50%; - background: #111827; + background: var(--agent-primary-dark); color: #fff; font-size: 13px; font-weight: 700; @@ -8157,8 +8618,7 @@ body.agent-workbench-mode .page-container { color: var(--agent-ink); } -.bailian-card-section, -.bailian-scene-card { +.bailian-card-section { border: 1px solid var(--agent-border); border-radius: 12px; background: #fff; @@ -8170,7 +8630,6 @@ body.agent-workbench-mode .page-container { } .bailian-card-section:hover, -.bailian-scene-card:hover, .bailian-model-select:hover, .bailian-list-card:hover, .bailian-kb-card:hover, @@ -8189,6 +8648,44 @@ body.agent-workbench-mode .page-container { color: var(--agent-ink); } +.bailian-card-section-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin: 0 0 14px; +} + +.bailian-card-section-head h3 { + margin: 0; +} + +.bailian-card-section-head .btn { + flex-shrink: 0; +} + +.bailian-card-section.toggle-only { + padding-top: 16px; + padding-bottom: 16px; +} + +.bailian-card-section.toggle-only .bailian-card-section-head { + min-height: 32px; + margin-bottom: 0; +} + +.bailian-card-section.toggle-only .agent-switch { + flex-shrink: 0; +} + +.bailian-card-section-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + flex-wrap: wrap; +} + .bailian-section-head { display: flex; align-items: center; @@ -8208,23 +8705,6 @@ body.agent-workbench-mode .page-container { gap: 12px; } -.bailian-scene-card { - min-height: 72px; - width: 100%; - display: flex; - align-items: center; - gap: 14px; - padding: 14px 16px; - text-align: left; - color: var(--agent-text); - transition: border-color .18s ease, background .18s ease, transform .18s ease; -} - -.bailian-scene-card:hover { - transform: translateY(-1px); -} - -.bailian-scene-icon, .bailian-model-icon, .bailian-list-icon, .bailian-mini-card span { @@ -8233,17 +8713,10 @@ body.agent-workbench-mode .page-container { justify-content: center; flex-shrink: 0; color: var(--agent-green-dark); - background: #f6ffed; - border: 1px solid #e0f7d2; + background: var(--agent-primary-soft); + border: 1px solid var(--agent-primary-border); } -.bailian-scene-icon { - width: 42px; - height: 42px; - border-radius: 12px; -} - -.bailian-scene-icon i, .bailian-model-icon i, .bailian-list-icon i, .bailian-mini-card i { @@ -8251,40 +8724,6 @@ body.agent-workbench-mode .page-container { height: 18px; } -.bailian-scene-main { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 5px; -} - -.bailian-scene-title-row { - display: flex; - align-items: center; - gap: 10px; -} - -.bailian-scene-title-row strong { - font-size: 15px; - color: var(--agent-ink); -} - -.bailian-scene-title-row em { - font-style: normal; - padding: 2px 9px; - border-radius: 999px; - color: var(--agent-green-dark); - background: #f6ffed; - font-size: 12px; - font-weight: 600; -} - -.bailian-scene-main span:last-child { - font-size: 12px; - color: var(--agent-muted); -} - .bailian-field { display: flex; flex-direction: column; @@ -8323,8 +8762,8 @@ body.agent-workbench-mode .page-container { .bailian-input:focus, .bailian-number-input:focus, .bailian-prompt-editor:focus { - border-color: var(--agent-ink); - box-shadow: 0 0 0 3px rgba(17,24,39,0.06); + border-color: var(--agent-primary-dark); + box-shadow: 0 0 0 3px rgba(82,196,26,0.12); } .bailian-model-select { @@ -8428,6 +8867,10 @@ body.agent-workbench-mode .page-container { color: var(--agent-muted); } +.bailian-feature-manage { + width: fit-content; +} + .bailian-model-desc, .bailian-section-note { font-size: 12px; @@ -8435,6 +8878,10 @@ body.agent-workbench-mode .page-container { color: var(--agent-muted); } +.bailian-model-field { + margin-top: 12px; +} + .bailian-section-note { padding: 10px 12px; border-radius: 10px; @@ -8442,6 +8889,298 @@ body.agent-workbench-mode .page-container { border: 1px solid #edf0f6; } +.bailian-asr-manager { + display: flex; + flex-direction: column; + gap: 16px; +} + +.bailian-asr-form { + padding: 16px; + border: 1px solid var(--agent-border); + border-radius: 12px; + background: #fbfcff; +} + +.bailian-asr-form-head, +.bailian-asr-list-head, +.bailian-asr-model-row, +.bailian-asr-model-actions, +.bailian-asr-form-actions { + display: flex; + align-items: center; +} + +.bailian-asr-form-head, +.bailian-asr-list-head, +.bailian-asr-model-row { + justify-content: space-between; + gap: 12px; +} + +.bailian-asr-form-title { + font-size: 14px; + font-weight: 700; + color: var(--agent-ink); +} + +.bailian-asr-form-desc, +.bailian-asr-list-head span, +.bailian-asr-model-meta { + font-size: 12px; + line-height: 1.6; + color: var(--agent-muted); +} + +.bailian-asr-form-grid { + margin-top: 14px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.bailian-asr-form-wide { + grid-column: 1 / -1; +} + +.bailian-asr-form-actions { + justify-content: flex-end; + gap: 10px; + margin-top: 14px; +} + +.bailian-model-manager-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 14px 16px; + border: 1px solid var(--agent-border); + border-radius: 12px; + background: #fbfcff; +} + +.bailian-model-manager-toolbar .bailian-action-btn, +.bailian-model-manager-toolbar .btn { + flex-shrink: 0; +} + +.bailian-empty-action { + margin-top: 6px; +} + +.bailian-voice-mode-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.bailian-voice-mode-card { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px; + border: 1px solid var(--agent-border); + border-radius: 12px; + background: #fff; + cursor: pointer; + transition: border-color .18s ease, background .18s ease, box-shadow .18s ease; +} + +.bailian-voice-mode-card.active { + border-color: var(--agent-green); + background: var(--agent-primary-soft); + box-shadow: var(--agent-card-shadow); +} + +.bailian-voice-mode-card input { + margin-top: 2px; + accent-color: var(--agent-primary); +} + +.bailian-voice-mode-card strong, +.bailian-voice-mode-card small { + display: block; +} + +.bailian-voice-mode-card strong { + font-size: 13px; + color: var(--agent-ink); +} + +.bailian-voice-mode-card small, +.bailian-form-help { + margin-top: 4px; + font-size: 12px; + line-height: 1.5; + color: var(--agent-muted); +} + +.bailian-voice-mode-panel { + margin-top: 10px; +} + +.bailian-voice-mode-panel.hidden { + display: none; +} + +.bailian-voice-batch-input { + min-height: 128px; + resize: vertical; + line-height: 1.55; +} + +.bailian-inline-check { + margin-top: 14px; + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--agent-muted); + cursor: pointer; +} + +.bailian-inline-check input { + width: 14px; + height: 14px; + accent-color: var(--agent-primary); +} + +.bailian-asr-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.bailian-asr-model-card { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + padding: 14px; + border: 1px solid var(--agent-border); + border-radius: 12px; + background: #fff; +} + +.bailian-asr-model-main { + flex: 1; + min-width: 0; +} + +.bailian-asr-model-row strong { + font-size: 14px; + color: var(--agent-ink); +} + +.bailian-asr-status { + display: inline-flex; + align-items: center; + justify-content: center; + height: 22px; + padding: 0 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + white-space: nowrap; +} + +.bailian-asr-status.neutral { + color: #606a7b; + background: #f3f5f8; + border: 1px solid #e1e5ee; +} + +.bailian-asr-status.pending { + color: #096dd9; + background: #e6f4ff; + border: 1px solid #91caff; +} + +.bailian-asr-status.success { + color: var(--agent-green-dark); + background: var(--agent-primary-soft); + border: 1px solid var(--agent-primary-border); +} + +.bailian-asr-status.danger { + color: #cf1322; + background: #fff1f0; + border: 1px solid #ffa39e; +} + +.bailian-asr-status.disabled { + color: #7b8494; + background: #f7f8fc; + border: 1px solid #e5e7f0; +} + +.bailian-asr-model-meta { + margin-top: 6px; + display: flex; + flex-wrap: wrap; + gap: 8px 14px; +} + +.bailian-asr-error { + margin-top: 8px; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid #ffd8d6; + background: #fff7f6; + color: #cf1322; + font-size: 12px; + line-height: 1.5; +} + +.bailian-asr-model-actions { + flex-shrink: 0; + gap: 8px; + opacity: 1 !important; + visibility: visible !important; +} + +.bailian-model-manager-toolbar .bailian-action-btn, +.bailian-model-manager-toolbar .btn, +.bailian-asr-empty .bailian-action-btn, +.bailian-asr-empty .btn, +.bailian-asr-form-actions .bailian-action-btn, +.bailian-asr-form-actions .btn, +.bailian-asr-model-actions .bailian-action-btn, +.bailian-asr-model-actions .bailian-plain-icon { + display: inline-flex !important; + opacity: 1 !important; + visibility: visible !important; + pointer-events: auto; +} + +.bailian-asr-empty { + min-height: 150px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 24px; + border: 1px dashed var(--agent-border); + border-radius: 12px; + background: #fbfcff; + color: var(--agent-muted); + text-align: center; +} + +.bailian-asr-empty i, +.bailian-asr-empty svg { + width: 30px; + height: 30px; + color: var(--agent-muted); +} + +.bailian-asr-empty strong { + color: var(--agent-ink); + font-size: 14px; +} + .bailian-voice-disabled { padding: 24px; border: 1px dashed var(--agent-border); @@ -8500,6 +9239,42 @@ body.agent-workbench-mode .page-container { color: var(--agent-ink); } +.bailian-title-with-info { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.bailian-info-tip { + width: 18px; + height: 18px; + padding: 0; + border: 0; + border-radius: 999px; + background: transparent; + color: #8a94a6; + cursor: pointer; +} + +.bailian-info-tip:hover, +.bailian-info-tip:focus-visible { + color: var(--agent-ink); + outline: none; +} + +.bailian-info-tip i, +.bailian-info-tip svg { + width: 15px; + height: 15px; +} + +.bailian-info-tip::after { + width: max-content; + max-width: 280px; + white-space: normal; + text-align: left; +} + .bailian-hotword-desc, .bailian-switch-desc, .bailian-section-inline-action, @@ -8521,8 +9296,8 @@ body.agent-workbench-mode .page-container { padding: 0 10px; border-radius: 8px; color: var(--agent-green-dark); - background: #f6ffed; - border: 1px solid #d9f7be; + background: var(--agent-primary-soft); + border: 1px solid var(--agent-primary-border); font-size: 13px; font-weight: 600; white-space: nowrap; @@ -8530,7 +9305,7 @@ body.agent-workbench-mode .page-container { } .bailian-link-btn:hover { - background: #edffd9; + background: #dffcef; transform: translateY(-1px); } @@ -8541,19 +9316,30 @@ body.agent-workbench-mode .page-container { margin-top: 12px; } +.bailian-token-row.compact { + margin-top: 8px; + gap: 6px; +} + .bailian-token { display: inline-flex; align-items: center; height: 28px; padding: 0 10px; border-radius: 999px; - border: 1px solid #d9f7be; - background: #f6ffed; + border: 1px solid var(--agent-primary-border); + background: var(--agent-primary-soft); color: var(--agent-green-dark); font-size: 12px; font-weight: 600; } +.bailian-token.muted { + color: var(--agent-muted); + background: #f7f8fc; + border-color: #e5e7f0; +} + .agent-switch { position: relative; width: 42px; @@ -8589,7 +9375,7 @@ body.agent-workbench-mode .page-container { } .agent-switch input:checked + span { - background: var(--agent-ink); + background: var(--agent-primary-dark); } .agent-switch input:checked + span::after { @@ -8653,69 +9439,37 @@ body.agent-workbench-mode .page-container { margin-left: 2px; } -.bailian-edge-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; -} - -.bailian-edge-card { - padding: 14px; +.bailian-filter-summary { + margin-top: 12px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px; border: 1px solid var(--agent-border); border-radius: 12px; - background: #fff; + background: #fbfcff; } -.bailian-edge-card strong { - display: block; - font-size: 14px; +.bailian-filter-summary-main { + flex: 1; + min-width: 0; +} + +.bailian-filter-summary-title, +.bailian-filter-group-title { + font-size: 13px; + font-weight: 700; color: var(--agent-ink); } -.bailian-edge-card span { - display: block; - margin-top: 6px; +.bailian-filter-summary-meta { + margin-top: 3px; font-size: 12px; - line-height: 1.6; + line-height: 1.5; color: var(--agent-muted); } -.bailian-collapse-head { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding: 0; - border: 0; - background: transparent; - color: var(--agent-text); - font-size: 14px; - font-weight: 600; - cursor: pointer; -} - -.bailian-filter-preview { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - padding-top: 12px; -} - -.bailian-filter-note { - flex: 1; - font-size: 12px; - line-height: 1.6; - color: var(--agent-muted); -} - -.bailian-filter-summary { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - .bailian-filter-group { padding: 14px; border: 1px solid var(--agent-border); @@ -8724,9 +9478,6 @@ body.agent-workbench-mode .page-container { } .bailian-filter-group-title { - font-size: 13px; - font-weight: 700; - color: var(--agent-ink); margin-bottom: 10px; } @@ -8748,8 +9499,8 @@ body.agent-workbench-mode .page-container { .bailian-radio-card:hover, .bailian-radio-card:has(input:checked) { - border-color: var(--agent-ink); - background: #fbfcff; + border-color: var(--agent-green); + background: var(--agent-primary-soft); } .bailian-radio-dot { @@ -8763,8 +9514,8 @@ body.agent-workbench-mode .page-container { } .bailian-radio-card:has(input:checked) .bailian-radio-dot { - border-color: var(--agent-ink); - background: var(--agent-ink); + border-color: var(--agent-primary-dark); + background: var(--agent-primary-dark); } .bailian-radio-card span:last-child { @@ -8783,7 +9534,7 @@ body.agent-workbench-mode .page-container { .bailian-run-btn:hover { transform: translateY(-1px); - box-shadow: 0 8px 18px rgba(17,24,39,.12); + box-shadow: 0 8px 18px rgba(82,196,26,.18); } .prompt-section { @@ -8825,8 +9576,7 @@ body.agent-workbench-mode .page-container { .bailian-variable-grid, .bailian-mode-grid, .bailian-model-mode-grid, -.bailian-mini-grid, -.bailian-budget-grid { +.bailian-mini-grid { display: grid; gap: 12px; } @@ -8835,8 +9585,377 @@ body.agent-workbench-mode .page-container { grid-template-columns: repeat(2, minmax(0, 1fr)); } -.bailian-mode-grid, -.bailian-budget-grid { +.bailian-variable-simple { + margin-top: 14px; + border: 1px solid var(--agent-border); + border-radius: 12px; + background: #fff; + overflow: hidden; +} + +.bailian-variable-simple-head, +.bailian-variable-simple-row { + display: grid; + grid-template-columns: minmax(150px, .72fr) minmax(220px, 1.28fr) 36px; + align-items: center; + gap: 10px; +} + +.bailian-variable-simple-head { + min-height: 42px; + padding: 0 12px; + border-bottom: 1px solid var(--agent-border); + background: #fbfcff; + color: var(--agent-muted); + font-size: 12px; + font-weight: 700; +} + +.bailian-variable-simple-list { + display: flex; + flex-direction: column; +} + +.bailian-variable-simple-row { + padding: 10px 12px; + border-bottom: 1px solid #f0f2f7; +} + +.bailian-variable-simple-row:last-child { + border-bottom: 0; +} + +.bailian-variable-simple-row .bailian-input { + height: 40px; +} + +.bailian-variable-simple-empty { + min-height: 86px; + display: flex; + align-items: center; + justify-content: center; + color: var(--agent-muted); + font-size: 13px; +} + +.bailian-variable-manager { + display: grid; + grid-template-columns: 238px minmax(0, 1fr); + gap: 12px; +} + +.bailian-variable-list-pane, +.bailian-variable-detail-pane { + min-height: 360px; + border: 1px solid var(--agent-border); + border-radius: 12px; + background: #fff; +} + +.bailian-variable-pane-head { + height: 44px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 0 12px; + border-bottom: 1px solid var(--agent-border); + font-size: 13px; + font-weight: 700; + color: var(--agent-ink); +} + +.bailian-variable-pane-head strong { + min-width: 24px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + background: #f2f4f7; + color: var(--agent-muted); + font-size: 12px; +} + +.bailian-variable-list { + max-height: 410px; + overflow: auto; + padding: 8px; +} + +.bailian-variable-row { + width: 100%; + min-height: 58px; + display: grid; + grid-template-columns: minmax(0, 1fr) 28px; + align-items: center; + gap: 8px; + padding: 9px 8px; + border: 1px solid transparent; + border-radius: 10px; + background: transparent; + color: inherit; + text-align: left; + cursor: pointer; +} + +.bailian-variable-row:hover, +.bailian-variable-row.active { + border-color: var(--agent-border); + background: #fbfcff; +} + +.bailian-variable-row.active { + border-color: #cbd5e1; + box-shadow: var(--agent-card-shadow); +} + +.bailian-variable-select { + min-width: 0; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 8px; + padding: 0; + border: 0; + background: transparent; + color: inherit; + text-align: left; + cursor: pointer; +} + +.bailian-variable-row-main { + min-width: 0; +} + +.bailian-variable-row-main strong, +.bailian-variable-row-main small { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bailian-variable-row-main strong { + font-size: 13px; + color: var(--agent-ink); +} + +.bailian-variable-row-main small { + margin-top: 3px; + font-size: 11px; + color: var(--agent-muted); +} + +.bailian-variable-source { + display: inline-flex; + align-items: center; + justify-content: center; + height: 22px; + padding: 0 8px; + border-radius: 999px; + border: 1px solid #e5e7f0; + background: #f7f8fc; + color: var(--agent-muted); + font-size: 11px; + font-weight: 700; + white-space: nowrap; +} + +.bailian-variable-source.green { + color: var(--agent-green-dark); + background: var(--agent-primary-soft); + border-color: var(--agent-primary-border); +} + +.bailian-variable-source.blue { + color: #0958d9; + background: #e6f4ff; + border-color: #91caff; +} + +.bailian-variable-source.purple { + color: #722ed1; + background: #f9f0ff; + border-color: #d3adf7; +} + +.bailian-variable-source.orange { + color: #ad4e00; + background: #fff7e6; + border-color: #ffd591; +} + +.bailian-variable-source.cyan { + color: #08979c; + background: #e6fffb; + border-color: #87e8de; +} + +.bailian-variable-source.red { + color: #cf1322; + background: #fff1f0; + border-color: #ffa39e; +} + +.bailian-variable-delete { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: 8px; + background: transparent; + color: #9aa3b2; + cursor: pointer; +} + +.bailian-variable-delete:hover { + color: var(--danger); + background: #fff2f0; +} + +.bailian-variable-delete i, +.bailian-variable-delete svg { + width: 15px; + height: 15px; +} + +.bailian-variable-detail-pane { + padding: 14px; +} + +.bailian-variable-detail { + display: flex; + flex-direction: column; + gap: 14px; +} + +.bailian-variable-detail-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.bailian-variable-detail-name { + font-size: 16px; + font-weight: 800; + color: var(--agent-ink); +} + +.bailian-variable-detail-desc { + margin-top: 6px; + font-size: 12px; + line-height: 1.6; + color: var(--agent-muted); +} + +.bailian-variable-detail-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.bailian-variable-detail-grid > div { + min-width: 0; + padding: 10px; + border-radius: 10px; + background: #fbfcff; + border: 1px solid #eef1f6; +} + +.bailian-variable-detail-grid span, +.bailian-variable-example span, +.bailian-variable-section-title { + display: block; + font-size: 11px; + color: var(--agent-muted); + margin-bottom: 5px; +} + +.bailian-variable-detail-grid strong { + display: block; + overflow-wrap: anywhere; + font-size: 12px; + color: var(--agent-ink); +} + +.bailian-variable-code { + font-family: "SF Mono", Monaco, Consolas, "Liberation Mono", monospace; +} + +.bailian-variable-example { + padding: 10px; + border-radius: 10px; + background: #fbfcff; + border: 1px solid #eef1f6; +} + +.bailian-variable-example code { + display: block; + overflow-wrap: anywhere; + color: var(--agent-ink); + font-size: 12px; + line-height: 1.5; +} + +.bailian-variable-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.bailian-variable-empty, +.bailian-variable-detail-empty { + height: 100%; + min-height: 240px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 20px; + text-align: center; + color: var(--agent-muted); +} + +.bailian-variable-detail-empty i, +.bailian-variable-detail-empty svg { + width: 28px; + height: 28px; +} + +.bailian-variable-empty strong, +.bailian-variable-detail-empty strong { + color: var(--agent-ink); + font-size: 14px; +} + +.bailian-variable-form { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.bailian-variable-form-wide { + grid-column: 1 / -1; +} + +.bailian-variable-textarea { + min-height: 92px; + resize: vertical; + line-height: 1.6; +} + +.bailian-variable-usage-checks { + display: flex; + flex-wrap: wrap; + gap: 10px 14px; +} + +.bailian-mode-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -8887,7 +10006,7 @@ body.agent-workbench-mode .page-container { .bailian-add-card:hover { color: var(--agent-green-dark); border-color: var(--agent-green); - background: #f6ffed; + background: var(--agent-primary-soft); transform: translateY(-1px); } @@ -8934,14 +10053,220 @@ body.agent-workbench-mode .page-container { .bailian-small-pill { color: var(--agent-green-dark); - background: #f6ffed; - border: 1px solid #d9f7be; + background: var(--agent-primary-soft); + border: 1px solid var(--agent-primary-border); } .bailian-kb-weight { margin-top: 6px; } +.bailian-kb-empty { + min-height: 96px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + border: 1px dashed var(--agent-border); + border-radius: 12px; + background: #fbfcff; + color: var(--agent-muted); + font-size: 13px; +} + +.bailian-kb-empty i, +.bailian-kb-empty svg { + width: 18px; + height: 18px; +} + +.bailian-kb-picker { + display: flex; + flex-direction: column; + gap: 14px; +} + +.bailian-kb-picker-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.bailian-search-field { + flex: 1; + min-width: 240px; + height: 40px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--agent-border); + border-radius: 10px; + background: #fff; + color: var(--agent-muted); +} + +.bailian-search-field i, +.bailian-search-field svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.bailian-search-field input { + width: 100%; + border: 0; + outline: none; + background: transparent; + color: var(--agent-ink); + font-size: 13px; +} + +.bailian-kb-picker-list { + max-height: 460px; + overflow: auto; + display: flex; + flex-direction: column; + gap: 10px; + padding-right: 2px; +} + +.bailian-kb-picker-card { + display: grid; + grid-template-columns: 18px minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + padding: 14px; + border: 1px solid var(--agent-border); + border-radius: 12px; + background: #fff; + cursor: pointer; + transition: border-color .18s ease, background .18s ease, box-shadow .18s ease; +} + +.bailian-kb-picker-card:hover, +.bailian-kb-picker-card.active { + border-color: #cbd5e1; + background: #fbfcff; + box-shadow: var(--agent-card-shadow); +} + +.bailian-kb-picker-card input { + display: none; +} + +.bailian-kb-picker-check { + width: 18px; + height: 18px; + border: 1px solid #cbd5e1; + border-radius: 6px; + background: #fff; + position: relative; +} + +.bailian-kb-picker-card.active .bailian-kb-picker-check { + border-color: var(--agent-green); + background: var(--agent-green); +} + +.bailian-kb-picker-card.active .bailian-kb-picker-check::after { + content: ""; + position: absolute; + left: 5px; + top: 2px; + width: 5px; + height: 9px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.bailian-kb-picker-main { + min-width: 0; + display: flex; + flex-direction: column; + gap: 5px; +} + +.bailian-kb-picker-main strong { + color: var(--agent-ink); + font-size: 14px; +} + +.bailian-kb-picker-main small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--agent-muted); + font-size: 12px; +} + +.bailian-kb-picker-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.bailian-kb-picker-meta em { + font-style: normal; + color: var(--agent-muted); + font-size: 11px; +} + +.bailian-kb-picker-status { + height: 24px; + display: inline-flex; + align-items: center; + padding: 0 8px; + border-radius: 999px; + border: 1px solid #e5e7f0; + background: #f7f8fc; + color: var(--agent-muted); + font-size: 11px; + font-weight: 700; + white-space: nowrap; +} + +.bailian-kb-picker-status.ready { + color: var(--agent-green-dark); + background: var(--agent-primary-soft); + border-color: var(--agent-primary-border); +} + +.bailian-kb-picker-status.processing, +.bailian-kb-picker-status.uploading { + color: #0958d9; + background: #e6f4ff; + border-color: #91caff; +} + +.bailian-kb-picker-status.error { + color: #cf1322; + background: #fff1f0; + border-color: #ffa39e; +} + +.bailian-kb-picker-empty { + min-height: 180px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + border: 1px dashed var(--agent-border); + border-radius: 12px; + background: #fbfcff; + color: var(--agent-muted); + font-size: 13px; +} + +.bailian-kb-picker-empty i, +.bailian-kb-picker-empty svg { + width: 28px; + height: 28px; +} + .bailian-mode-card { cursor: pointer; } @@ -8970,8 +10295,8 @@ body.agent-workbench-mode .page-container { .bailian-mode-card.active, .bailian-mode-card:has(input:checked) { border-color: var(--agent-green); - background: #f6ffed; - box-shadow: 0 0 0 3px rgba(82,196,26,.08); + background: var(--agent-primary-soft); + box-shadow: 0 0 0 3px rgba(82,196,26,.10); } .bailian-mini-grid { @@ -9000,6 +10325,48 @@ body.agent-workbench-mode .page-container { margin: 6px 0 0; } +.bailian-mcp-card-meta { + margin-top: 8px; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.bailian-mcp-card-meta span { + max-width: 220px; + overflow: hidden; + color: var(--agent-muted); + font-size: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bailian-mcp-error { + margin-top: 8px; + color: #cf1322; + font-size: 12px; +} + +.bailian-mcp-tool-block { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 4px; +} + +.bailian-mcp-tool-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 1.2fr) 34px; + gap: 10px; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid #edf0f6; +} + +.bailian-mcp-tool-row:last-child { + border-bottom: 0; +} + .bailian-empty-inline { padding: 12px 0; font-size: 12px; @@ -9146,6 +10513,200 @@ body.agent-workbench-mode .page-container { margin-top: 6px; } +.bailian-command-head-actions { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.bailian-command-empty, +.bailian-command-library-empty { + min-height: 150px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 24px; + border: 1px dashed var(--agent-border); + border-radius: 12px; + background: #fbfcff; + color: var(--agent-muted); + text-align: center; +} + +.bailian-command-empty i, +.bailian-command-empty svg, +.bailian-command-library-empty i, +.bailian-command-library-empty svg { + width: 28px; + height: 28px; +} + +.bailian-command-empty strong, +.bailian-command-library-empty strong { + color: var(--agent-ink); + font-size: 14px; +} + +.bailian-command-empty span, +.bailian-command-library-empty span { + font-size: 12px; + line-height: 1.6; +} + +.bailian-command-library, +.bailian-command-editor { + display: flex; + flex-direction: column; + gap: 12px; +} + +.bailian-command-library-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.bailian-command-library-card { + display: grid; + grid-template-columns: 18px minmax(0, 1fr) auto; + align-items: flex-start; + gap: 12px; + padding: 14px; + border: 1px solid var(--agent-border); + border-radius: 12px; + background: #fff; + transition: border-color .18s ease, background .18s ease, box-shadow .18s ease; +} + +.bailian-command-library-card:hover, +.bailian-command-library-card.active { + border-color: #cbd5e1; + background: #fbfcff; + box-shadow: var(--agent-card-shadow); +} + +.bailian-command-select { + margin-top: 2px; + cursor: pointer; +} + +.bailian-command-select input { + display: none; +} + +.bailian-command-select span { + width: 18px; + height: 18px; + display: inline-flex; + border: 1px solid #cbd5e1; + border-radius: 6px; + background: #fff; + position: relative; +} + +.bailian-command-select input:checked + span { + border-color: var(--agent-green); + background: var(--agent-green); +} + +.bailian-command-select input:checked + span::after { + content: ""; + position: absolute; + left: 5px; + top: 2px; + width: 5px; + height: 9px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.bailian-command-library-main { + min-width: 0; +} + +.bailian-command-library-title { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.bailian-command-library-title strong { + color: var(--agent-ink); + font-size: 14px; +} + +.bailian-command-library-title em { + height: 22px; + display: inline-flex; + align-items: center; + padding: 0 8px; + border-radius: 999px; + background: var(--agent-primary-soft); + color: var(--agent-green-dark); + font-style: normal; + font-size: 11px; + font-weight: 700; +} + +.bailian-command-library-main p { + margin: 6px 0 0; + color: var(--agent-muted); + font-size: 12px; + line-height: 1.6; +} + +.bailian-command-library-meta { + margin-top: 8px; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.bailian-command-library-meta span { + color: var(--agent-muted); + font-size: 11px; +} + +.bailian-command-library-actions { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.bailian-command-editor { + padding: 14px; + border: 1px solid var(--agent-border); + border-radius: 12px; + background: #fbfcff; +} + +.bailian-command-editor-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.bailian-command-editor-wide { + grid-column: 1 / -1; +} + +.bailian-command-editor-textarea { + min-height: 92px; + resize: vertical; + line-height: 1.6; +} + +.bailian-command-editor-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 12px; +} + .bailian-add-round { width: 38px; height: 38px; @@ -9154,13 +10715,13 @@ body.agent-workbench-mode .page-container { justify-content: center; flex-shrink: 0; border-radius: 50%; - background: var(--agent-ink); + background: var(--agent-primary-dark); color: #fff; transition: background .18s ease, transform .18s ease; } .bailian-add-round:hover { - background: #243044; + background: var(--agent-primary-hover); transform: translateY(-1px); } @@ -9219,6 +10780,1110 @@ body.agent-workbench-mode .page-container { color: #a5adbb; } +.bailian-preview-pane.is-runtime { + align-items: stretch; + justify-content: stretch; + padding: 36px; +} + +.bailian-preview-pane.is-runtime.is-voice-runtime { + overflow: visible; + padding: 14px 16px 16px; +} + +.bailian-preview-pane.is-runtime.is-text-runtime { + overflow: visible; + padding: 14px 16px 16px; +} + +.bailian-preview-pane.is-runtime.is-voice-runtime .agent-voice-panel { + overflow: visible; + border-radius: 0; +} + +.bailian-preview-pane.is-runtime.is-text-runtime .agent-text-panel { + overflow: visible; + border-radius: 0; +} + +.agent-runtime-shell { + width: min(760px, 100%); + margin: auto; + display: flex; + flex-direction: column; + gap: 18px; +} + +.agent-runtime-shell.voice { + min-height: min(720px, calc(100vh - 136px)); +} + +.agent-runtime-shell.text-chat { + min-height: min(720px, calc(100vh - 136px)); +} + +.agent-text-panel { + width: 100%; + height: 100%; + min-height: min(720px, calc(100vh - 136px)); + margin: 0 auto; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + overflow: visible; + border: 0; + border-radius: 0; + background: + radial-gradient(circle at 50% 20%, rgba(82,196,26,.075), transparent 34%), + var(--agent-soft); + box-shadow: none; +} + +.agent-text-topbar { + min-height: 52px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 8px clamp(12px, 1.8vw, 20px) 4px; + border-bottom: 0; + background: transparent; +} + +.agent-text-title { + display: flex; + align-items: center; + gap: 8px; +} + +.agent-text-title h2 { + margin: 0; + color: #303846; + font-size: 16px; + line-height: 1.35; + font-weight: 650; + letter-spacing: 0; +} + +.agent-text-key { + margin-top: 6px; + display: flex; + align-items: center; + gap: 8px; + color: var(--agent-muted); + font-size: 11px; +} + +.agent-text-key strong { + max-width: 340px; + overflow: hidden; + color: #4b5563; + font-weight: 700; + text-overflow: ellipsis; + white-space: nowrap; +} + +.agent-text-user { + display: inline-flex; + align-items: center; + gap: 8px; + color: #4b5563; + font-size: 13px; + font-weight: 650; +} + +.agent-text-user i, +.agent-text-user svg { + width: 15px; + height: 15px; +} + +.agent-text-settings { + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: 10px; + background: rgba(255,255,255,.82); + color: #6b7280; + cursor: pointer; + transition: background .18s ease, transform .18s ease; +} + +.agent-text-settings:hover { + background: #fff; + transform: translateY(-1px); +} + +.agent-text-message-area { + min-height: 0; + overflow-y: auto; + padding: clamp(28px, 6vh, 64px) clamp(12px, 1.8vw, 20px) 18px; + background: transparent; + scrollbar-width: none; +} + +.agent-text-message-area::-webkit-scrollbar { + display: none; +} + +.agent-text-message-stream { + width: 100%; + min-height: 100%; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 18px; +} + +.agent-text-empty { + flex: 1; + min-height: 260px; + display: flex; + align-items: center; + justify-content: center; + color: #a0a7b5; + font-size: 13px; +} + +.agent-text-message-row { + display: flex; + width: 100%; +} + +.agent-text-message-row.user { + justify-content: flex-end; +} + +.agent-text-message-row.ai { + justify-content: flex-start; +} + +.agent-text-user-bubble { + max-width: min(78%, 640px); + padding: 12px 16px; + border-radius: 18px 18px 4px 18px; + background: rgba(246,255,237,.88); + box-shadow: 0 2px 8px rgba(82,196,26,.045); + color: #303846; + font-size: 15px; + line-height: 1.65; + word-break: break-word; +} + +.agent-text-ai-message { + max-width: min(82%, 700px); + color: #303846; +} + +.agent-text-ai-label { + margin-bottom: 7px; + color: #8a93a3; + font-size: 12px; + font-weight: 700; +} + +.agent-text-ai-content { + color: #303846; + font-size: 15px; + line-height: 1.85; + word-break: break-word; + white-space: pre-wrap; +} + +.agent-text-actions { + margin-top: 10px; + display: flex; + align-items: center; + gap: 6px; +} + +.agent-text-actions [data-tooltip]::after { + top: calc(100% + 8px); + bottom: auto; +} + +.agent-text-actions [data-tooltip]:hover::after, +.agent-text-actions [data-tooltip]:focus::after, +.agent-text-actions [data-tooltip]:focus-visible::after { + transform: translateX(-50%) translateY(2px); +} + +.agent-text-action-btn { + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid #e5e7f0; + border-radius: 9px; + background: #fff; + color: #8a93a3; + cursor: pointer; + transition: border-color .18s ease, color .18s ease, transform .18s ease; +} + +.agent-text-action-btn:hover { + border-color: var(--agent-primary-border); + color: var(--agent-primary-dark); + transform: translateY(-1px); +} + +.agent-text-action-btn i, +.agent-text-action-btn svg { + width: 14px; + height: 14px; +} + +.agent-text-input-area { + padding: 0 clamp(12px, 1.8vw, 20px) 14px; + border-top: 0; + background: transparent; +} + +.agent-text-input-shell { + width: 100%; + min-height: 66px; + margin: 0 auto; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + padding: 8px; + border: 1px solid rgba(229,231,240,.96); + border-radius: 999px; + background: rgba(255,255,255,.94); + box-shadow: 0 14px 40px rgba(31,41,55,.06); + transition: border-color .18s ease, box-shadow .18s ease; +} + +.agent-text-input-shell:focus-within { + border-color: var(--agent-primary-border); + box-shadow: 0 14px 40px rgba(31,41,55,.06), 0 0 0 3px rgba(82,196,26,.10); +} + +.agent-text-input { + width: 100%; + height: 48px; + padding: 0 16px; + border: 0; + border-radius: 999px; + outline: 0; + background: transparent; + color: #303846; + font-size: 14px; + transition: border-color .18s ease, box-shadow .18s ease; +} + +.agent-text-input:focus { + box-shadow: none; +} + +.agent-text-send { + width: 58px; + height: 48px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: 16px; + background: var(--agent-primary-dark); + color: #fff; + cursor: pointer; + transition: background .18s ease, transform .18s ease; +} + +.agent-text-send:hover { + background: var(--agent-primary-hover); + transform: translateY(-1px); +} + +.agent-text-send:disabled { + cursor: not-allowed; + opacity: .55; + transform: none; +} + +.agent-text-send i, +.agent-text-send svg { + width: 17px; + height: 17px; +} + +.agent-voice-panel { + width: 100%; + height: 100%; + min-height: min(720px, calc(100vh - 136px)); + margin: 0 auto; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + position: relative; + overflow: visible; + border-radius: 0; + background: + radial-gradient(circle at 50% 20%, rgba(82,196,26,.075), transparent 34%), + var(--agent-soft); + animation: agentRuntimeFade .32s ease both; +} + +.agent-voice-panel.recording, +.agent-voice-panel.has-conversation { + animation: none; +} + +@keyframes agentRuntimeFade { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.agent-voice-topbar { + min-height: 52px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 8px clamp(12px, 1.8vw, 20px) 4px; +} + +.agent-voice-title { + display: flex; + align-items: center; + gap: 8px; +} + +.agent-voice-title h2 { + margin: 0; + color: #303846; + font-size: 16px; + line-height: 1.35; + font-weight: 650; +} + +.agent-voice-user { + display: inline-flex; + align-items: center; + gap: 8px; + color: #4b5563; + font-size: 13px; + font-weight: 650; +} + +.agent-voice-user i, +.agent-voice-user svg { + width: 15px; + height: 15px; +} + +.agent-voice-settings { + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + border-radius: 10px; + background: rgba(255,255,255,.82); + color: #6b7280; + cursor: pointer; + transition: background .18s ease, transform .18s ease; +} + +.agent-voice-settings:hover { + background: #fff; + transform: translateY(-1px); +} + +.agent-voice-stage { + min-height: 0; + position: relative; + display: flex; + align-items: flex-start; + justify-content: center; + overflow: auto; + padding: clamp(28px, 6vh, 64px) clamp(12px, 1.8vw, 20px) 18px; + scrollbar-width: none; +} + +.agent-voice-stage::-webkit-scrollbar { + display: none; +} + +.agent-voice-idle-center { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding-top: clamp(10px, 2vh, 24px); + animation: agentRuntimeFade .36s ease both; +} + +.agent-voice-logo { + width: 92px; + height: 92px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 24px; + background: linear-gradient(135deg, var(--agent-green) 0%, var(--agent-accent) 100%); + color: #fff; + box-shadow: 0 18px 46px rgba(82,196,26,.16); + animation: agentVoiceGlow 2.8s ease-in-out infinite; +} + +@keyframes agentVoiceGlow { + 0%, 100% { + transform: scale(1); + box-shadow: 0 18px 46px rgba(82,196,26,.15); + } + 50% { + transform: scale(1.025); + box-shadow: 0 22px 58px rgba(82,196,26,.22); + } +} + +.agent-voice-logo i, +.agent-voice-logo svg { + width: 38px; + height: 38px; +} + +.agent-voice-idle-center h3 { + margin: 22px 0 8px; + color: #303846; + font-size: 18px; + line-height: 1.35; + font-weight: 700; +} + +.agent-voice-idle-center p { + max-width: 420px; + margin: 0; + color: #8a93a3; + font-size: 13px; + line-height: 1.8; +} + +.agent-voice-conversation { + width: 100%; + overflow: visible; + margin: 0 auto; + padding-top: clamp(8px, 2vh, 28px); +} + +.agent-voice-message-stream { + width: 100%; + display: flex; + flex-direction: column; + gap: 18px; +} + +.agent-voice-message-row { + display: flex; + width: 100%; +} + +.agent-voice-message-row.user { + display: flex; + justify-content: flex-end; +} + +.agent-voice-message-row.ai { + justify-content: flex-start; +} + +.agent-voice-user-bubble { + max-width: min(78%, 640px); + padding: 12px 16px; + border-radius: 18px 18px 4px 18px; + background: rgba(246,255,237,.90); + box-shadow: 0 4px 14px rgba(82,196,26,.05); + color: #303846; + font-size: 15px; + line-height: 1.65; +} + +.agent-voice-ai-row, +.agent-voice-ai-message { + display: flex; + flex-direction: column; + align-items: flex-start; + max-width: min(82%, 700px); +} + +.agent-voice-ai-content { + color: #303846; + font-size: 15px; + line-height: 1.65; + white-space: pre-wrap; +} + +.agent-voice-ai-actions { + margin-top: 12px; + display: flex; + align-items: center; + gap: 6px; +} + +.agent-voice-ai-actions:not(.is-visible) { + display: none; +} + +.agent-voice-ai-actions [data-tooltip]::after { + top: calc(100% + 8px); + bottom: auto; +} + +.agent-voice-ai-actions [data-tooltip]:hover::after, +.agent-voice-ai-actions [data-tooltip]:focus::after, +.agent-voice-ai-actions [data-tooltip]:focus-visible::after { + transform: translateX(-50%) translateY(2px); +} + +.agent-voice-latency { + position: absolute; + right: clamp(12px, 1.8vw, 20px); + top: 48%; + transform: translateY(-50%); + padding: 8px 10px; + border: 1px solid #e5e7f0; + border-radius: 999px; + background: rgba(255,255,255,.72); + color: #6b7280; + font-size: 12px; + font-weight: 700; +} + +.agent-voice-control { + min-height: 66px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + margin: 0 clamp(12px, 1.8vw, 20px) 14px; + padding: 8px; + border: 1px solid rgba(229,231,240,.96); + border-radius: 999px; + background: rgba(255,255,255,.94); + box-shadow: 0 14px 40px rgba(31,41,55,.06); +} + +.agent-voice-panel.recording .agent-voice-control { + grid-template-columns: 72px minmax(0, 1fr) auto; +} + +.agent-voice-device { + height: 48px; + display: inline-flex; + align-items: center; + gap: 10px; + min-width: 0; + padding: 0 16px; + border: 0; + border-radius: 999px; + background: transparent; + color: #303846; + cursor: pointer; + transition: background .18s ease; +} + +.agent-voice-device:hover { + background: #f7f8fc; +} + +.agent-voice-device span { + overflow: hidden; + font-size: 14px; + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; +} + +.agent-voice-device i, +.agent-voice-device svg { + width: 18px; + height: 18px; + flex-shrink: 0; + color: #6b7280; +} + +.agent-voice-start-btn, +.agent-voice-hangup-btn, +.agent-voice-mic-btn { + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + cursor: pointer; + transition: transform .18s ease, filter .18s ease, background .18s ease; +} + +.agent-voice-start-btn { + width: 58px; + height: 48px; + border-radius: 16px; + background: linear-gradient(135deg, var(--agent-green) 0%, var(--agent-accent) 100%); + color: #fff; + box-shadow: 0 12px 26px rgba(82,196,26,.18); +} + +.agent-voice-start-btn:hover, +.agent-voice-hangup-btn:hover, +.agent-voice-mic-btn:hover { + transform: translateY(-1px); +} + +.agent-voice-start-btn i, +.agent-voice-start-btn svg, +.agent-voice-hangup-btn i, +.agent-voice-hangup-btn svg, +.agent-voice-mic-btn i, +.agent-voice-mic-btn svg { + width: 20px; + height: 20px; +} + +.agent-voice-record-time { + color: var(--agent-green-dark); + font-size: 16px; + font-weight: 800; + text-align: center; + font-variant-numeric: tabular-nums; +} + +.agent-voice-wave { + height: 44px; + display: flex; + align-items: center; + justify-content: center; + gap: 5px; + overflow: hidden; +} + +.agent-voice-wave span { + width: 6px; + height: 6px; + border-radius: 999px; + background: linear-gradient(180deg, var(--agent-green), var(--agent-accent)); + opacity: .65; + animation: agentVoiceDotWave 920ms ease-in-out infinite; + animation-delay: calc(var(--i) * -42ms); +} + +.agent-voice-wave.muted span { + background: #cbd5e1; + animation-play-state: paused; +} + +@keyframes agentVoiceDotWave { + 0%, 100% { + height: 6px; + opacity: .45; + transform: translateY(0); + } + 40% { + height: 34px; + opacity: 1; + transform: translateY(-1px); + } + 70% { + height: 16px; + opacity: .82; + } +} + +.agent-voice-control-actions { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.agent-voice-mic-btn { + width: 44px; + height: 44px; + border-radius: 16px; + background: #f7f8fc; + color: #303846; +} + +.agent-voice-mic-btn.muted { + background: #fff1f0; + color: #cf1322; +} + +.agent-voice-hangup-btn { + width: 52px; + height: 44px; + border-radius: 16px; + background: #dc2626; + color: #fff; +} + +.agent-runtime-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; +} + +.agent-runtime-kicker { + display: inline-flex; + align-items: center; + height: 24px; + padding: 0 9px; + border-radius: 999px; + background: var(--agent-primary-soft); + color: var(--agent-green-dark); + font-size: 11px; + font-weight: 800; + letter-spacing: 0; +} + +.agent-runtime-head h2 { + margin: 12px 0 8px; + color: var(--agent-ink); + font-size: 22px; + line-height: 1.32; +} + +.agent-runtime-head p { + max-width: 520px; + margin: 0; + color: var(--agent-muted); + font-size: 13px; + line-height: 1.7; +} + +.agent-runtime-metrics { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.agent-runtime-metrics span { + display: inline-flex; + align-items: center; + height: 30px; + padding: 0 10px; + border: 1px solid var(--agent-border); + border-radius: 999px; + background: #fff; + color: var(--agent-ink); + font-size: 12px; + font-weight: 700; +} + +.agent-runtime-auth-card, +.agent-runtime-keybar, +.agent-runtime-conversation, +.agent-runtime-callbar, +.agent-runtime-textbar, +.agent-runtime-note { + border: 1px solid var(--agent-border); + border-radius: 14px; + background: rgba(255,255,255,.86); + box-shadow: var(--agent-card-shadow); +} + +.agent-runtime-auth-card { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 14px; + padding: 18px; +} + +.agent-runtime-note { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + color: var(--agent-muted); + font-size: 12px; + line-height: 1.7; +} + +.agent-runtime-note i, +.agent-runtime-note svg { + width: 16px; + height: 16px; + margin-top: 2px; + color: var(--agent-green-dark); +} + +.agent-runtime-auth-actions { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.agent-runtime-keybar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 13px 15px; +} + +.agent-runtime-keybar span { + display: block; + color: var(--agent-muted); + font-size: 11px; + font-weight: 700; +} + +.agent-runtime-keybar strong { + display: block; + margin-top: 4px; + color: var(--agent-ink); + font-size: 13px; + font-weight: 750; +} + +.agent-runtime-conversation { + flex: 1; + min-height: 300px; + display: flex; + flex-direction: column; + gap: 14px; + padding: 18px; +} + +.agent-runtime-conversation.chat { + min-height: 360px; + max-height: min(520px, calc(100vh - 360px)); + overflow-y: auto; +} + +.agent-runtime-message { + display: grid; + grid-template-columns: 36px minmax(0, 1fr); + gap: 12px; + align-items: flex-start; +} + +.agent-runtime-avatar { + width: 36px; + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 11px; + background: #f7f8fc; + color: var(--agent-muted); +} + +.agent-runtime-message.ai .agent-runtime-avatar { + background: var(--agent-primary-soft); + color: var(--agent-green-dark); +} + +.agent-runtime-avatar i, +.agent-runtime-avatar svg { + width: 17px; + height: 17px; +} + +.agent-runtime-message span { + display: block; + margin-bottom: 6px; + color: var(--agent-muted); + font-size: 12px; + font-weight: 700; +} + +.agent-runtime-message p { + margin: 0; + color: var(--agent-ink); + font-size: 14px; + line-height: 1.75; +} + +.agent-runtime-message.user { + align-self: flex-end; + max-width: 82%; +} + +.agent-runtime-message.user > div:last-child { + padding: 10px 12px; + border-radius: 14px; + background: var(--agent-primary-dark); + color: #fff; +} + +.agent-runtime-message.user span, +.agent-runtime-message.user p { + color: #fff; +} + +.agent-runtime-message.ai { + max-width: 86%; +} + +.agent-runtime-message.ai > div:last-child { + padding: 10px 12px; + border: 1px solid #edf0f6; + border-radius: 14px; + background: #fff; +} + +.agent-runtime-message.pending svg { + animation: spin 1s linear infinite; +} + +.agent-runtime-typing { + display: inline-flex; + align-items: center; + gap: 4px; + height: 18px; +} + +.agent-runtime-typing i { + width: 6px; + height: 6px; + border-radius: 999px; + background: var(--agent-green); + animation: agentTyping 900ms ease-in-out infinite; +} + +.agent-runtime-typing i:nth-child(2) { + animation-delay: .12s; +} + +.agent-runtime-typing i:nth-child(3) { + animation-delay: .24s; +} + +@keyframes agentTyping { + 0%, 100% { + transform: translateY(0); + opacity: .45; + } + 50% { + transform: translateY(-4px); + opacity: 1; + } +} + +.agent-runtime-chat-meta { + align-self: center; + padding: 4px 8px; + border-radius: 999px; + background: #f7f8fc; + color: var(--agent-muted); + font-size: 11px; + font-weight: 700; +} + +.agent-runtime-callbar { + min-height: 74px; + display: grid; + grid-template-columns: minmax(0, 1fr) 44px 54px; + gap: 12px; + align-items: center; + padding: 12px; +} + +.agent-runtime-wave { + height: 42px; + display: flex; + align-items: center; + gap: 4px; + padding: 0 10px; + border-radius: 12px; + background: #f7f8fc; + overflow: hidden; +} + +.agent-runtime-wave span { + width: 4px; + height: 10px; + border-radius: 999px; + background: #c6ceda; + transform-origin: center; +} + +.agent-runtime-wave.active span { + background: var(--agent-green); + animation: agentWave 900ms ease-in-out infinite; + animation-delay: calc(var(--i) * -58ms); +} + +@keyframes agentWave { + 0%, 100% { height: 8px; opacity: .55; } + 45% { height: 32px; opacity: 1; } + 70% { height: 18px; opacity: .78; } +} + +.agent-runtime-round, +.agent-runtime-call { + display: inline-flex; + align-items: center; + justify-content: center; + border: 0; + cursor: pointer; + transition: transform .18s ease, background .18s ease, color .18s ease; +} + +.agent-runtime-round { + width: 44px; + height: 44px; + border-radius: 14px; + background: #f7f8fc; + color: var(--agent-ink); +} + +.agent-runtime-round.muted { + background: #fff1f0; + color: #cf1322; +} + +.agent-runtime-call { + width: 54px; + height: 54px; + border-radius: 18px; + background: var(--agent-primary-dark); + color: #fff; +} + +.agent-runtime-call.danger { + background: #ff4d4f; +} + +.agent-runtime-round:hover, +.agent-runtime-call:hover { + transform: translateY(-1px); +} + +.agent-runtime-round i, +.agent-runtime-round svg, +.agent-runtime-call i, +.agent-runtime-call svg { + width: 20px; + height: 20px; +} + +.agent-runtime-muted-tip { + align-self: flex-end; + color: #cf1322; + font-size: 12px; + font-weight: 700; +} + +.agent-runtime-textbar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + padding: 12px; +} + .bailian-cube { position: relative; width: 112px; @@ -9262,163 +11927,6 @@ body.agent-workbench-mode .page-container { background: linear-gradient(145deg, #f7f8fb, #e5e9f1); } -.bailian-scene-modal { - display: flex; - flex-direction: column; - gap: 22px; -} - -.bailian-scene-modal-section { - display: flex; - flex-direction: column; - gap: 12px; -} - -.bailian-scene-modal-title { - font-size: 14px; - font-weight: 700; - color: #111827; -} - -.bailian-scene-option-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; -} - -.bailian-scene-option, -.bailian-scene-version-option { - border: 1px solid #e5e7f0; - border-radius: 12px; - background: #fff; - cursor: pointer; - transition: border-color .18s ease, background .18s ease, box-shadow .18s ease, transform .18s ease; -} - -.bailian-scene-option { - min-height: 112px; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 10px; - padding: 14px; -} - -.bailian-scene-option input, -.bailian-scene-version-option input { - display: none; -} - -.bailian-scene-option:hover, -.bailian-scene-version-option:hover { - border-color: #cfd5e3; - background: #fbfcff; - transform: translateY(-1px); -} - -.bailian-scene-option.selected, -.bailian-scene-version-option.selected { - border-color: #52c41a; - background: #f6ffed; - box-shadow: 0 0 0 3px rgba(82,196,26,.08); -} - -.bailian-scene-option-icon { - width: 36px; - height: 36px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 10px; - color: #389e0d; - background: #f6ffed; - border: 1px solid #d9f7be; -} - -.bailian-scene-option.selected .bailian-scene-option-icon { - color: #fff; - background: #52c41a; - border-color: #52c41a; -} - -.bailian-scene-option-icon i { - width: 18px; - height: 18px; -} - -.bailian-scene-option-main { - min-width: 0; - display: flex; - flex-direction: column; - gap: 5px; -} - -.bailian-scene-option-main strong, -.bailian-scene-version-option strong { - font-size: 14px; - font-weight: 700; - color: #111827; -} - -.bailian-scene-option-main small, -.bailian-scene-version-option span { - font-size: 12px; - line-height: 1.55; - color: #7b8494; -} - -.bailian-scene-version-grid, -.bailian-scene-language-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - -.bailian-scene-version-option, -.bailian-scene-language-option { - min-height: 96px; - display: flex; - flex-direction: column; - gap: 8px; - padding: 16px; -} - -.bailian-scene-language-option { - border: 1px solid #e5e7f0; - border-radius: 12px; - background: #fff; - cursor: pointer; - transition: border-color .18s ease, background .18s ease, box-shadow .18s ease, transform .18s ease; -} - -.bailian-scene-language-option input { - display: none; -} - -.bailian-scene-language-option:hover { - border-color: #cfd5e3; - background: #fbfcff; - transform: translateY(-1px); -} - -.bailian-scene-language-option.selected { - border-color: #52c41a; - background: #f6ffed; - box-shadow: 0 0 0 3px rgba(82,196,26,.08); -} - -.bailian-scene-language-option strong { - font-size: 14px; - font-weight: 700; - color: #111827; -} - -.bailian-scene-language-option span { - font-size: 12px; - line-height: 1.55; - color: #7b8494; -} - .bailian-required { color: #ff4d4f; font-size: 13px; @@ -9476,9 +11984,30 @@ body.agent-workbench-mode .page-container { overflow: visible; } + .bailian-config-footer { + padding: 14px 18px 18px; + box-shadow: none; + } + .bailian-preview-pane { min-height: 420px; } + + .bailian-preview-pane.is-runtime { + padding: 24px; + } + + .agent-runtime-head, + .agent-runtime-keybar, + .agent-text-topbar, + .agent-voice-topbar { + flex-direction: column; + align-items: stretch; + } + + .agent-runtime-metrics { + justify-content: flex-start; + } } @media (max-width: 640px) { @@ -9500,12 +12029,88 @@ body.agent-workbench-mode .page-container { } .bailian-step-tabs { - height: auto; - grid-template-columns: repeat(2, minmax(0, 1fr)); + height: 64px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .agent-runtime-callbar, + .agent-runtime-textbar, + .agent-text-input-shell { + grid-template-columns: 1fr; + } + + .agent-runtime-round, + .agent-runtime-call, + .agent-text-send { + width: 100%; + } + + .agent-text-panel { + min-height: 640px; + border-radius: 12px; + } + + .agent-voice-panel { + min-height: 640px; + border-radius: 12px; + } + + .agent-text-message-area { + padding: 24px 14px; + } + + .agent-text-user-bubble, + .agent-text-ai-message, + .agent-voice-user-bubble, + .agent-voice-ai-row, + .agent-voice-ai-message { + max-width: 88%; + } + + .agent-voice-stage { + flex-direction: column; + padding: 22px 16px 12px; + } + + .agent-voice-latency { + position: static; + align-self: center; + transform: none; + margin-top: 14px; + } + + .agent-voice-panel.idle .agent-voice-control, + .agent-voice-panel.recording .agent-voice-control { + grid-template-columns: 1fr; + border-radius: 24px; + margin: 0 14px 12px; + padding: 8px; + } + + .agent-voice-control-actions { + justify-content: stretch; + } + + .agent-voice-mic-btn, + .agent-voice-hangup-btn, + .agent-voice-start-btn { + width: 100%; } .bailian-step-tab { - min-height: 58px; + min-height: 64px; + gap: 4px; + padding: 0 4px; + } + + .bailian-step-number { + width: 20px; + height: 20px; + font-size: 11px; + } + + .bailian-step-title { + font-size: 12px; } .bailian-config-scroll { @@ -9518,11 +12123,46 @@ body.agent-workbench-mode .page-container { .bailian-variable-grid, .bailian-mode-grid, - .bailian-mini-grid, - .bailian-budget-grid { + .bailian-model-mode-grid, + .bailian-mini-grid { grid-template-columns: 1fr; } + .bailian-variable-manager, + .bailian-variable-detail-grid, + .bailian-variable-form { + grid-template-columns: 1fr; + } + + .bailian-variable-simple-head { + display: none; + } + + .bailian-variable-simple-row { + grid-template-columns: 1fr 36px; + } + + .bailian-variable-simple-row .bailian-input:first-child { + grid-column: 1 / -1; + } + + .bailian-variable-list-pane, + .bailian-variable-detail-pane { + min-height: auto; + } + + .bailian-variable-list { + max-height: none; + } + + .bailian-variable-actions { + justify-content: stretch; + } + + .bailian-variable-actions .bailian-action-btn { + flex: 1; + } + .bailian-hotword-box, .bailian-switch-row, .bailian-section-inline-action, @@ -9534,10 +12174,61 @@ body.agent-workbench-mode .page-container { .bailian-hotword-box, .bailian-mcp-empty, + .bailian-model-manager-toolbar, + .bailian-kb-picker-toolbar, .bailian-section-inline-action { flex-direction: column; } + .bailian-card-section-head { + align-items: flex-start; + flex-wrap: wrap; + } + + .bailian-card-section-actions { + justify-content: flex-start; + } + + .bailian-search-field { + min-width: 0; + width: 100%; + } + + .bailian-kb-picker-toolbar .btn { + width: 100%; + justify-content: center; + } + + .bailian-kb-picker-card { + grid-template-columns: 18px minmax(0, 1fr); + } + + .bailian-kb-picker-status { + grid-column: 2; + justify-self: flex-start; + } + + .bailian-asr-form-grid { + grid-template-columns: 1fr; + } + + .bailian-voice-mode-grid { + grid-template-columns: 1fr; + } + + .bailian-asr-model-card, + .bailian-asr-model-actions, + .bailian-asr-form-actions { + flex-direction: column; + align-items: stretch; + } + + .bailian-asr-model-actions .bailian-action-btn, + .bailian-asr-form-actions .bailian-action-btn, + .bailian-asr-form-actions .btn { + width: 100%; + } + .bailian-list-card, .bailian-kb-card { flex-wrap: wrap; @@ -9548,12 +12239,6 @@ body.agent-workbench-mode .page-container { min-height: 300px; } - .bailian-scene-option-grid, - .bailian-scene-version-grid, - .bailian-scene-language-grid { - grid-template-columns: 1fr; - } - .bailian-preview-pane { padding: 32px 18px; } @@ -9789,6 +12474,32 @@ body.agent-workbench-mode .page-container { previewVoice: (provider, voice, text) => new Promise(r => setTimeout(r, 600)), testRAGQuery: (agentId, query) => new Promise(r => setTimeout(r, 1000)), createKnowledgeBase: (data) => new Promise(r => setTimeout(r, 600)), + listASRModels: (params) => Promise.resolve({ success: true, data: window.__asrModelRegistry || [], params }), + createASRModel: (data) => new Promise(r => setTimeout(() => r({ success: true, data }), 300)), + updateASRModel: (id, data) => new Promise(r => setTimeout(() => r({ success: true, data: { id, ...data } }), 300)), + deleteASRModel: (id) => new Promise(r => setTimeout(() => r({ success: true, data: { id } }), 300)), + validateASRModel: (id, data) => new Promise(r => setTimeout(() => r({ success: true, data: { id, ...data } }), 600)), + testASRModel: (id, data) => new Promise(r => setTimeout(() => r({ success: true, data: { id, ...data } }), 500)), + setASRModelEnabled: (id, enabled) => new Promise(r => setTimeout(() => r({ success: true, data: { id, enabled } }), 250)), + listTTSModels: (params) => Promise.resolve({ success: true, data: window.__ttsModelRegistry || [], params }), + createTTSModel: (data) => new Promise(r => setTimeout(() => r({ success: true, data }), 300)), + updateTTSModel: (id, data) => new Promise(r => setTimeout(() => r({ success: true, data: { id, ...data } }), 300)), + deleteTTSModel: (id) => new Promise(r => setTimeout(() => r({ success: true, data: { id } }), 300)), + validateTTSModel: (id, data) => new Promise(r => setTimeout(() => r({ success: true, data: { id, ...data } }), 700)), + setTTSModelEnabled: (id, enabled) => new Promise(r => setTimeout(() => r({ success: true, data: { id, enabled } }), 250)), + listTextModels: (params) => Promise.resolve({ success: true, data: window.__textModelRegistry || [], params }), + createTextModel: (data) => new Promise(r => setTimeout(() => r({ success: true, data }), 300)), + updateTextModel: (id, data) => new Promise(r => setTimeout(() => r({ success: true, data: { id, ...data } }), 300)), + deleteTextModel: (id) => new Promise(r => setTimeout(() => r({ success: true, data: { id } }), 300)), + validateTextModel: (id, data) => new Promise(r => setTimeout(() => r({ success: true, data: { id, ...data } }), 700)), + setTextModelEnabled: (id, enabled) => new Promise(r => setTimeout(() => r({ success: true, data: { id, enabled } }), 250)), + listVariables: (agentId, params) => Promise.resolve({ success: true, data: window.__agentVariables || [], params: { agentId, ...params } }), + createVariable: (agentId, data) => new Promise(r => setTimeout(() => r({ success: true, data: { agentId, ...data } }), 300)), + updateVariable: (agentId, variableId, data) => new Promise(r => setTimeout(() => r({ success: true, data: { agentId, variableId, ...data } }), 300)), + deleteVariable: (agentId, variableId) => new Promise(r => setTimeout(() => r({ success: true, data: { agentId, variableId } }), 250)), + injectVariableToPrompt: (agentId, variableId, data) => new Promise(r => setTimeout(() => r({ success: true, data: { agentId, variableId, ...data } }), 250)), + getVariableState: (agentId, sessionId, params) => Promise.resolve({ success: true, data: {}, params: { agentId, sessionId, ...params } }), + updateVariableState: (agentId, sessionId, data) => new Promise(r => setTimeout(() => r({ success: true, data: { agentId, sessionId, ...data } }), 300)), getHotwordGroups: (params) => Promise.resolve({ success: true, data: window.__agentHotwordGroups || [], params }), createHotwordGroup: (data) => new Promise(r => setTimeout(() => r({ success: true, data }), 500)), updateHotwordGroup: (id, data) => new Promise(r => setTimeout(() => r({ success: true, data: { id, ...data } }), 500)), @@ -10121,10 +12832,16 @@ body.agent-workbench-mode .page-container { overlay.className = 'modal-overlay'; overlay.innerHTML = `
`; document.body.appendChild(overlay); - + document.body.classList.add('modal-open'); + const close = () => { overlay.classList.remove('open'); - setTimeout(() => overlay.remove(), 200); + setTimeout(() => { + overlay.remove(); + if (!document.querySelector('.modal-overlay')) { + document.body.classList.remove('modal-open'); + } + }, 200); }; overlay.querySelector('.modal-close').addEventListener('click', close); @@ -10421,7 +13138,7 @@ body.agent-workbench-mode .page-container { window.companies = loadCompanies(); const companies = window.companies; - + // 当前选中的公司,默认为"全部公司" window.currentCompany = 'all'; @@ -11675,7 +14392,7 @@ body.agent-workbench-mode .page-container { } }); } - + try { localStorage.removeItem('talkingq_theme'); document.documentElement.removeAttribute('data-theme'); @@ -13113,7 +15830,6 @@ const AGENT_CONFIG_STEPS = [ { id: 'voice', index: 1, title: '语音交互' }, { id: 'understand', index: 2, title: '理解和生成' }, { id: 'skills', index: 3, title: '技能' }, - { id: 'advanced', index: 4, title: '高级设置' }, ]; const AGENT_CONFIG_DEFAULT_APPS = [ @@ -13123,100 +15839,67 @@ const AGENT_CONFIG_DEFAULT_APPS = [ { id: 'app-004', type: 'multimodal', name: '数学小课堂', status: 'published' }, ]; -const AGENT_SCENE_TEMPLATES = [ - { id: 'ai-audio-glasses', name: 'AI音频眼镜', icon: 'headphones', desc: '随身携带的语音助手' }, - { id: 'ar-glasses', name: 'AR眼镜', icon: 'glasses', desc: '具备屏幕显示能力' }, - { id: 'desktop-robot', name: '桌面机器人', icon: 'bot', desc: '实时语音对话,日常情感陪伴' }, - { id: 'smart-home', name: '智能家居', icon: 'home', desc: '语音控制家中设备' }, - { id: 'toy', name: '儿童玩具', icon: 'star', desc: '陪伴孩子聊天、放音乐、讲故事' }, - { id: 'story-player', name: '儿童故事机', icon: 'book-open', desc: '给孩子讲故事、放儿歌' }, - { id: 'smart-speaker', name: '智能音箱', icon: 'volume-2', desc: '语音控制播放音乐' }, - { id: 'mobile-app', name: '手机应用', icon: 'smartphone', desc: '实时语音互动应用' }, +const AGENT_INTERACTION_LANGUAGES = [ + { id: 'zh', name: '中文' }, + { id: 'en', name: '英语' }, + { id: 'fr', name: '法语' }, + { id: 'de', name: '德语' }, + { id: 'es', name: '西班牙语' }, + { id: 'it', name: '意大利语' }, + { id: 'ru', name: '俄语' }, + { id: 'pt', name: '葡萄牙语' }, + { id: 'ko', name: '韩语' }, + { id: 'ja', name: '日语' }, + { id: 'th', name: '泰语' }, + { id: 'vi', name: '越南语' }, + { id: 'id', name: '印度尼西亚语' }, + { id: 'ms', name: '马来语' }, + { id: 'fil', name: '菲律宾语' }, + { id: 'yue', name: '粤语' }, + { id: 'ar', name: '阿拉伯语' }, + { id: 'hi', name: '印地语' }, + { id: 'tr', name: '土耳其语' }, + { id: 'uk', name: '乌克兰语' }, + { id: 'cs', name: '捷克语' }, + { id: 'da', name: '丹麦语' }, + { id: 'fi', name: '芬兰语' }, + { id: 'is', name: '冰岛语' }, + { id: 'no', name: '挪威语' }, + { id: 'pl', name: '波兰语' }, + { id: 'nl', name: '荷兰语' }, + { id: 'sv', name: '瑞典语' }, ]; -const AGENT_SCENE_VERSIONS = [ - { - id: 'full', - name: '全能版', - desc: '支持意图识别、工具调用、联网搜索、多场景 Agent,适合完整智能体体验。', - }, - { - id: 'lite', - name: '轻量版', - desc: '更快速、更低价的语音闲聊,不启用复杂工具调用和 Agent 编排。', - }, -]; +const SPEECH_MODEL_STATUS_META = { + validating: { label: '连接中', className: 'pending' }, + available: { label: '可用', className: 'success' }, + failed: { label: '连接失败', className: 'danger' }, + disabled: { label: '已禁用', className: 'disabled' }, +}; -const AGENT_SCENE_LANGUAGES = [ - { id: 'zh', name: '中文', desc: '适合中文普通话和中文儿童内容场景' }, - { id: 'en', name: '英文', desc: '适合英语对话、英语学习和双语启蒙场景' }, +const ASR_MODEL_STATUS_META = SPEECH_MODEL_STATUS_META; +const TTS_MODEL_STATUS_META = SPEECH_MODEL_STATUS_META; +const TEXT_MODEL_STATUS_META = SPEECH_MODEL_STATUS_META; + +const MCP_SERVICE_STATUS_META = { + pending: { label: '待连接', className: 'pending' }, + validating: { label: '连接中', className: 'pending' }, + available: { label: '可用', className: 'success' }, + failed: { label: '连接失败', className: 'danger' }, + disabled: { label: '已禁用', className: 'disabled' }, +}; + +const TTS_AUDIO_FORMAT_OPTIONS = [ + { id: 'mp3', name: 'MP3' }, + { id: 'wav', name: 'WAV' }, + { id: 'pcm', name: 'PCM' }, + { id: 'opus', name: 'OPUS' }, + { id: 'flac', name: 'FLAC' }, ]; const AGENT_CONFIG_MOCK = { voice: { - languageOptions: [ - { id: 'zh', label: '中文' }, - { id: 'en', label: '英文' }, - ], - asrModels: [ - { - id: 'fun-asr', - icon: 'audio-lines', - label: 'Fun-ASR实时语音识别', - desc: '中英文自由切换,多地区方言覆盖,适应多样复杂环境。', - }, - { - id: 'qwen3-asr', - icon: 'audio-lines', - label: '通义千问3-ASR-Flash-Realtime', - desc: '高精度语音识别,中英日韩德法俄意西葡阿和多种方言。', - }, - { - id: 'paraformer', - icon: 'audio-lines', - label: 'Paraformer实时语音识别', - desc: '新一代非自回归端到端模型,支持中英日韩德法俄等语言。', - }, - { - id: 'multimodal-lite-asr', - icon: 'audio-lines', - label: '多模态交互轻量版语音识别', - desc: '超低价格,预算友好,支持中英等语言。', - }, - ], - ttsGroups: [ - { - id: 'cosyvoice', - title: 'CosyVoice-实时语音合成', - models: [ - { id: 'cosyvoice-v3-flash', label: 'CosyVoice-v3-Flash大模型', desc: '独家音色,性价比高,支持情感控制。' }, - { id: 'cosyvoice-v3-plus', label: 'CosyVoice-v3-Plus大模型', desc: '情感表达最佳,支持声音复刻。' }, - { id: 'cosyvoice-v35-plus', label: 'CosyVoice-v3.5-Plus大模型', desc: '声音设计/复刻最佳,多种方言和外语。' }, - { id: 'cosyvoice-v35-flash', label: 'CosyVoice-v3.5-Flash大模型', desc: '声音设计/复刻性价比高,多种方言和外语。' }, - { id: 'cosyvoice-v2', label: 'CosyVoice-v2大模型', desc: '音色丰富,70+音色可选,一句话声音复刻。' }, - ], - }, - { - id: 'qwen-tts', - title: '通义千问3-实时语音合成', - models: [ - { id: 'qwen3-tts-flash', label: '通义千问3-TTS-Flash-Realtime', desc: '多语言,拥有17种高表现力的拟人音色。' }, - { id: 'qwen3-tts-instruct', label: '通义千问3-TTS-Instruct-Flash-Realtime', desc: '支持指令控制,调整音调、语速、情感、特点。' }, - { id: 'qwen3-tts-design', label: '通义千问3-TTS-声音设计', desc: '通过文本描述生成定制化音色,支持多维度定义。' }, - { id: 'qwen3-tts-clone', label: '通义千问3-TTS-声音复刻', desc: '10-20秒音频即可生成效果最佳的定制音色。' }, - ], - }, - { - id: 'others', - title: '其他语音合成模型', - models: [ - { id: 'sambert-tts', label: 'Sambert-实时语音合成', desc: '音色选择多,多语言,合成速度快。' }, - { id: 'lite-tts', label: '多模态交互轻量版语音合成', desc: '预算友好,支持多语种和方言。' }, - ], - }, - ], defaultHotwords: ['TalkingQ', '小Q', '白雪公主', '睡前故事', '恐龙百科'], - correctionHint: '通过接口上传词表,对语音识别结果进行实时干预。', }, understand: { intentModel: { @@ -13224,15 +15907,6 @@ const AGENT_CONFIG_MOCK = { label: '意图识别', desc: '关闭将不再触发调用工具/Agent 等基于语义理解的交互功能。', }, - textModels: [ - { id: 'recommended-fast', group: '推荐模型', label: '多模态交互专有模型-高速版', desc: '对话速度更快,适合通用场景、闲聊陪伴场景。' }, - { id: 'recommended-tool', group: '推荐模型', label: '多模态交互专有模型', desc: '指令遵循能力更强,适合工作学习场景。' }, - { id: 'recommended-style', group: '推荐模型', label: '多模态交互专有模型-风格化版', desc: '风格化能力更强,适合角色扮演等场景。' }, - { id: 'qwen', group: '更多模型', label: '多千问', desc: '通用能力均衡,适合大多数任务。' }, - { id: 'deepseek', group: '更多模型', label: 'DeepSeek', desc: '推理和代码表现更强。' }, - { id: 'chatgpt', group: '更多模型', label: 'ChatGPT', desc: '多场景通用对话能力。' }, - { id: 'doubao', group: '更多模型', label: '豆包', desc: '适合中文对话和内容生成。' }, - ], prompt: `# 角色 你是小Q,一个专为 3-10 岁儿童设计的 AI 玩具智能体。 @@ -13253,53 +15927,156 @@ const AGENT_CONFIG_MOCK = { 知识库优先级:高 \`\`\``, variables: [ - { name: 'child_age', desc: '儿童年龄' }, - { name: 'device_mode', desc: '设备当前模式' }, - { name: 'ambient_noise', desc: '环境噪音等级' }, - { name: 'weather', desc: '当前位置天气' }, + { id: 'var-empty-1', name: '', desc: '' }, ], knowledgeBases: [ - { name: '儿童科学百科', desc: '动物、太空、自然科学问答素材', weight: '0.72' }, - { name: '故事素材库 V2', desc: '睡前故事、角色设定、儿童互动剧情', weight: '0.68' }, + { id: 'kb-001', name: '儿童科学百科', desc: '动物、太空、自然科学问答素材', weight: '0.72' }, + { id: 'kb-002', name: '故事素材库 V2', desc: '睡前故事、角色设定、儿童互动剧情', weight: '0.68' }, ], }, skills: { - commands: [ - { icon: 'phone-call', name: '打电话', count: 6, desc: '拨打联系人电话、语音拨号等电话相关控制' }, - { icon: 'alarm-clock', name: '定闹钟', count: 1, desc: '创建、关闭、修改闹钟' }, - { icon: 'volume-2', name: '音量设置', count: 7, desc: '调节媒体音量、铃声音量、静音等' }, - { icon: 'bluetooth', name: '蓝牙', count: 4, desc: '打开/关闭蓝牙、连接蓝牙设备' }, - { icon: 'toggle-right', name: '应用开关', count: 10, desc: '打开/关闭指定 App 或系统功能' }, - { icon: 'cpu', name: '设备控制', count: 5, desc: '系统级设备控制能力(当前已选中)' }, - { icon: 'music', name: '音乐播放', count: 5, desc: '播放音乐、暂停、下一首等' }, - { icon: 'circle-play', name: '多媒体控制', count: 6, desc: '控制媒体播放状态与音频输出' }, - { icon: 'camera', name: '拍照录像', count: 6, desc: '打开相机、拍照、录像' }, - { icon: 'mic', name: '录音', count: 2, desc: '开始/停止录音' }, - { icon: 'book-open', name: '朗读文字', count: 1, desc: '语音朗读输入文本' }, - { icon: 'message-circle', name: '发短信', count: 3, desc: '发送短信消息' }, - { icon: 'sun', name: '亮度设置', count: 5, desc: '调节屏幕亮度' }, - { icon: 'sun-medium', name: '色温设置', count: 3, desc: '调节屏幕色温/护眼模式' }, - { icon: 'video', name: '视频播放', count: 1, desc: '打开并播放视频' }, - { icon: 'map-pinned', name: '打开地图', count: 1, desc: '打开地图导航类应用' }, - { icon: 'monitor', name: '屏幕控制', count: 4, desc: '锁屏、亮屏、熄屏、旋转等屏幕操作' }, - ], - plugins: [ - { icon: 'cloud-sun', name: '天气查询', desc: '查询实时天气、空气质量和未来预报', status: '系统执行' }, - { icon: 'calendar', name: '万年历', desc: '查询日期、农历、节假日和宜忌信息', status: '系统执行' }, - { icon: 'newspaper', name: '新闻资讯', desc: '获取最新新闻资讯和摘要播报', status: '系统执行' }, - { icon: 'navigation', name: '路线规划', desc: '规划出行路线、距离和预计耗时', status: '系统执行' }, - { icon: 'map', name: '本地生活', desc: '查询周边商户、生活服务和地点信息', status: '系统执行' }, - ], - bailianApps: [ - { name: '故事生成器', desc: '基于主题生成短故事' }, - { name: '儿童内容安全', desc: '对回答内容进行安全审查' }, - ], - thirdPartyApps: [ - { name: '音频内容库', desc: '接入第三方儿歌和故事资源' }, - ], + commands: [], + plugins: [], }, }; +const AGENT_KNOWLEDGE_BASE_CATALOG = [ + { id: 'kb-001', name: '儿童科学百科', description: '包含各类儿童科学知识的百科全书', docs: 2840, size: '128MB', typeName: '文档搜索', status: 'ready', tags: ['儿童', '科学'] }, + { id: 'kb-002', name: '故事素材库 V2', description: '精选各类儿童故事素材', docs: 5200, size: '340MB', typeName: '文档搜索', status: 'ready', tags: ['故事', '儿童'] }, + { id: 'kb-003', name: '小学数学知识点', description: '小学数学知识体系整理', docs: 1560, size: '86MB', typeName: '文档搜索', status: 'processing', tags: ['数学', '教育'] }, + { id: 'kb-004', name: '英语启蒙词典', description: '儿童英语启蒙词汇', docs: 3200, size: '210MB', typeName: '图片问答', status: 'ready', tags: ['英语', '启蒙'] }, + { id: 'kb-005', name: '唐诗宋词精选', description: '经典唐诗宋词收录', docs: 890, size: '45MB', typeName: '文档搜索', status: 'ready', tags: ['诗词', '文学'] }, + { id: 'kb-006', name: '历史人物故事', description: '历史名人故事集', docs: 1200, size: '78MB', typeName: '音视频搜索', status: 'error', tags: ['历史', '故事'] }, + { id: 'kb-007', name: '产品参数数据库', description: '公司产品技术参数', docs: 450, size: '32MB', typeName: '数据查询', status: 'ready', tags: ['产品', '技术'] }, + { id: 'kb-008', name: '音乐教育资源库', description: '儿童音乐教育资源', docs: 560, size: '65MB', typeName: '音视频搜索', status: 'uploading', tags: ['音乐', '教育'] }, +]; + +function ensureAsrModelRegistry() { + if (!Array.isArray(window.__asrModelRegistry)) { + window.__asrModelRegistry = []; + } + window.__asrModelRegistry.forEach(normalizeSpeechModelStatus); + return window.__asrModelRegistry; +} + +function ensureTtsModelRegistry() { + if (!Array.isArray(window.__ttsModelRegistry)) { + window.__ttsModelRegistry = []; + } + window.__ttsModelRegistry.forEach(normalizeSpeechModelStatus); + return window.__ttsModelRegistry; +} + +function ensureTextModelRegistry() { + if (!Array.isArray(window.__textModelRegistry)) { + window.__textModelRegistry = []; + } + window.__textModelRegistry.forEach(normalizeSpeechModelStatus); + return window.__textModelRegistry; +} + +function ensureCommandLibrary() { + if (!Array.isArray(window.__agentCommandLibrary)) { + window.__agentCommandLibrary = []; + } + return window.__agentCommandLibrary; +} + +function ensurePluginLibrary() { + if (!Array.isArray(window.__agentPluginLibrary)) { + window.__agentPluginLibrary = []; + } + return window.__agentPluginLibrary; +} + +function ensureMcpServiceLibrary() { + if (!Array.isArray(window.__agentMcpServiceLibrary)) { + window.__agentMcpServiceLibrary = []; + } + return window.__agentMcpServiceLibrary; +} + +function ensureAgentConfigAutosaves() { + if (!window.__agentConfigAutosaves || typeof window.__agentConfigAutosaves !== 'object') { + window.__agentConfigAutosaves = {}; + } + return window.__agentConfigAutosaves; +} + +function ensureAgentRunTimers() { + if (!window.__agentRunTimers || typeof window.__agentRunTimers !== 'object') { + window.__agentRunTimers = {}; + } + return window.__agentRunTimers; +} + +function normalizeSpeechModelStatus(model) { + if (!model) return model; + if (model.status === 'connected') model.status = 'available'; + if (model.status === 'unconnected') { + model.status = 'failed'; + model.errorMessage = model.errorMessage || '尚未完成连接校验'; + } + if (model.enabled === false && model.status === 'available') { + model.status = 'disabled'; + } + return model; +} + +function getNowText() { + return new Date().toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }).replace(/\//g, '-'); +} + +function getTimeOnly(value) { + const text = String(value || ''); + const match = text.match(/(\d{2}:\d{2}:\d{2})/); + return match ? match[1] : text; +} + +function getAvailableAsrModels() { + return ensureAsrModelRegistry() + .filter(model => model.enabled !== false && model.status === 'available') + .map(model => ({ + id: model.id, + label: model.name, + desc: `${model.provider} · ${model.baseUrl}`, + provider: model.provider, + })); +} + +function getAvailableTtsModels() { + return ensureTtsModelRegistry() + .filter(model => model.enabled !== false && model.status === 'available') + .map(model => ({ + id: model.id, + label: model.name, + desc: `${model.provider} · ${getLanguageTypeName(model.languageType)} · ${getTtsVoiceSummary(model)} · ${String(model.audioFormat || '').toUpperCase() || '默认格式'}`, + provider: model.provider, + audioFormat: model.audioFormat, + voiceId: model.voiceId, + })); +} + +function getAvailableTextModels() { + return ensureTextModelRegistry() + .filter(model => model.enabled !== false && model.status === 'available') + .map(model => ({ + id: model.id, + label: model.name, + desc: `${model.provider} · ${model.modelCode} · 上下文 ${model.contextWindow || '-'} tokens`, + provider: model.provider, + modelCode: model.modelCode, + })); +} + registerPage('agent-config', function(container) { if (typeof window.__ensureAgentHotwordGroups === 'function') { window.__ensureAgentHotwordGroups(); @@ -13331,21 +16108,22 @@ registerPage('agent-config', function(container) { const apps = window.__agentManagedApps || AGENT_CONFIG_DEFAULT_APPS; const selectedApp = apps.find(app => app.id === window.__selectedAppId) || apps[0] || AGENT_CONFIG_DEFAULT_APPS[0]; const firstHotwordGroup = window.__agentHotwordGroups?.[0]?.id || ''; + const availableAsrModels = getAvailableAsrModels(); + const availableTtsModels = getAvailableTtsModels(); + const availableTextModels = getAvailableTextModels(); const state = { app: selectedApp, activeStep: 'voice', preview: 'empty', - selectedSceneId: window.__selectedSceneTemplateId || 'toy', - selectedSceneVersion: window.__selectedSceneVersion || 'full', - selectedSceneLanguage: window.__selectedSceneLanguage || 'zh', + saveStatus: 'saved', + lastSavedAt: '', + selectedInteractionLanguage: window.__selectedInteractionLanguage || 'zh', voiceEnabled: true, asrEnabled: true, ttsEnabled: true, - selectedAsrModelId: AGENT_CONFIG_MOCK.voice.asrModels[0]?.id || '', - selectedTtsGroupId: AGENT_CONFIG_MOCK.voice.ttsGroups[0]?.id || '', - selectedTtsModelId: AGENT_CONFIG_MOCK.voice.ttsGroups[0]?.models[0]?.id || '', + selectedAsrModelId: availableAsrModels[0]?.id || '', + selectedTtsModelId: availableTtsModels[0]?.id || '', selectedHotwordGroupId: firstHotwordGroup, - realtimeCorrectionEnabled: true, ttsFilterEnabled: false, ttsFilterPairs: [ { enabled: true, left: '/*', right: '*/' }, @@ -13357,19 +16135,40 @@ registerPage('agent-config', function(container) { interruptSecondaryEnabled: true, interruptMode: 'full', semanticStopEnabled: true, - edgeAlgorithmExpanded: true, - selectedTextModelMode: 'recommended', - selectedTextModelId: AGENT_CONFIG_MOCK.understand.textModels[0]?.id || '', + selectedTextModelId: availableTextModels[0]?.id || '', intentEnabled: true, dialogueContinuationEnabled: true, knowledgeSearchEnabled: true, - searchMode: 'fast', longTermMemoryEnabled: false, + prompt: AGENT_CONFIG_MOCK.understand.prompt, knowledgeBases: AGENT_CONFIG_MOCK.understand.knowledgeBases.map(item => ({ ...item })), - customCommands: [], - customPlugins: [], + variables: AGENT_CONFIG_MOCK.understand.variables.map(item => ({ ...item })), + selectedCommandIds: [], + selectedPluginIds: [], + selectedMcpServiceIds: [], + runSession: { + apiKey: '', + apiKeyConfirmed: false, + apiKeyName: '', + isRecording: false, + micMuted: false, + elapsedSeconds: 0, + latencyMs: null, + transcript: '', + aiResponse: '', + textInput: '', + textResponse: '', + textPending: false, + chatMessages: [], + startedAt: 0, + }, }; + function renderAfterConfigChange() { + scheduleAgentConfigAutoSave(state); + render(); + } + function render() { const scrollArea = container.querySelector('.bailian-config-scroll'); const previousScrollTop = scrollArea?.scrollTop || 0; @@ -13378,8 +16177,9 @@ registerPage('agent-config', function(container) { } container.innerHTML = renderAgentWorkbench(state); renderAgentIcons(container); - bindWorkbenchEvents(container, state, render); - bindPromptCounter(container); + bindWorkbenchEvents(container, state, renderAfterConfigChange); + bindPromptCounter(container, state); + bindRuntimePreviewEvents(container, state, () => updatePreviewOnly(container, state)); requestAnimationFrame(() => { const nextScrollArea = container.querySelector('.bailian-config-scroll'); if (nextScrollArea) { @@ -13403,22 +16203,29 @@ registerPage('agent-config', function(container) { function renderAgentWorkbench(state) { return `
- ${renderAgentHeader(state.app)} + ${renderAgentHeader(state)}
- ${renderPreviewPane(state.preview)} + ${renderPreviewPane(state)}
`; } -function renderAgentHeader(app) { +function renderAgentHeader(state) { + const app = state.app; const status = getAgentStatus(app.status); + const saveText = state.saveStatus === 'saving' + ? '自动保存中' + : state.lastSavedAt ? `已自动保存 ${getTimeOnly(state.lastSavedAt)}` : '等待自动保存'; return `
@@ -13433,7 +16240,10 @@ function renderAgentHeader(app) { ${status.label}
-
ID: ${escapeHtml(app.id)} · AI 智能体配置平台
+
+ ID: ${escapeHtml(app.id)} · AI 智能体配置平台 + ${escapeHtml(saveText)} +
@@ -13441,7 +16251,6 @@ function renderAgentHeader(app) { -
@@ -13474,28 +16283,37 @@ function renderStepContent(stepId, state) { voice: renderVoiceStep, understand: renderUnderstandStep, skills: renderSkillsStep, - advanced: renderAdvancedStep, }; return (renderers[stepId] || renderVoiceStep)(state); } function renderVoiceStep(state) { const data = AGENT_CONFIG_MOCK.voice; - const scene = getSelectedScene(state); - const version = getSelectedSceneVersion(state); - const language = getSelectedSceneLanguage(state); const hotwordGroups = window.__agentHotwordGroups || []; + const asrModels = getAvailableAsrModels(); + const ttsModels = getAvailableTtsModels(); + if (state.selectedAsrModelId && !asrModels.some(model => model.id === state.selectedAsrModelId)) { + state.selectedAsrModelId = ''; + } + if (state.selectedTtsModelId && !ttsModels.some(model => model.id === state.selectedTtsModelId)) { + state.selectedTtsModelId = ''; + } + if (!state.selectedAsrModelId && asrModels.length) { + state.selectedAsrModelId = asrModels[0].id; + } + if (!state.selectedTtsModelId && ttsModels.length) { + state.selectedTtsModelId = ttsModels[0].id; + } return `

语音交互

- ${renderSceneCard(scene, version, language)}
@@ -13519,17 +16337,22 @@ function renderVoiceStep(state) { toggleKey: 'asr-enabled', checked: state.asrEnabled, activeId: state.selectedAsrModelId, - options: data.asrModels, - hint: '前面的开关决定是否启用 ASR,后面的下拉用于切换识别模型', + options: asrModels, + hint: 'API Key 统一在 ASR 模型管理中维护,连接成功且启用后才可选择', + emptyText: '暂无可用 ASR 模型,请先创建并通过自动校验', + manageAction: 'asr-model-manager', + manageText: '管理 ASR 模型', })} ${renderFeatureSelect({ title: '语音合成', toggleKey: 'tts-enabled', checked: state.ttsEnabled, activeId: state.selectedTtsModelId, - options: data.ttsGroups.flatMap(group => group.models.map(model => ({ ...model, groupTitle: group.title }))), - groupedOptions: data.ttsGroups, - hint: '前面的开关决定是否启用 TTS,后面的下拉用于切换播报模型', + options: ttsModels, + hint: 'API Key、Voice ID 和音频格式统一在 TTS 模型管理中维护', + emptyText: '暂无可用 TTS 模型,请先创建并通过自动校验', + manageAction: 'tts-model-manager', + manageText: '管理 TTS 模型', })}
@@ -13545,49 +16368,36 @@ function renderVoiceStep(state) { -
-
即时纠错
-
-
-
开启即时纠错
-
${escapeHtml(data.correctionHint)}
-
- -
-
- 可上传词表对识别结果进行实时干预 - -
-
语音合成播报过滤
-
开启播报过滤
-
特定符号对中的内容将不会进行 TTS 播报
+
+ 开启播报过滤 + +
- ${state.ttsFilterEnabled ? ` -
-
选中符号对内的文字将不会通过 TTS 播报,如 [动作/表情编码]、[设备参数] 及 $$公式$$ 等。
- -
- ` : ''} + ${state.ttsFilterEnabled ? renderTtsFilterSummary(state) : ''}
- ${state.ttsFilterEnabled ? renderTtsFilterSummary(state) : ''}
对话打断
此配置仅用于效果体验
@@ -13616,46 +16426,30 @@ function renderVoiceStep(state) {
-
语义判停与拒识
-
开启语义判停与拒识
-
开启后可能导致对话延迟增加
+
+ 语义判停与拒识 + +
-
基于用户语义,判断是否结束语音输入,以及是否需要模型回复。全双工对话场景下,本功能效果更显著。
-
-
端侧算法
- - ${state.edgeAlgorithmExpanded ? ` -
- ${[ - ['语音唤醒', '默认唤醒词“小云小云”'], - ['端侧 VAD', '语音活性检测,判断是否存在语音信号'], - ['AEC', '回声消除,减少自身播报的干扰'], - ['定向拾音', '增强特定方向拾音,抑制其他方向'], - ].map(([title, desc]) => ` -
- ${escapeHtml(title)} - ${escapeHtml(desc)} -
- `).join('')} -
- ` : ''} -
- ` : `
当前为文字交互模式 - 关闭语音交互后,不展示语音 AI、热词、纠错、播报过滤、打断和端侧算法配置。 + 关闭语音交互后,不展示语音 AI、热词、播报过滤和对话打断配置。
`} @@ -13665,71 +16459,42 @@ function renderVoiceStep(state) { function renderUnderstandStep(state) { const data = AGENT_CONFIG_MOCK.understand; - const selectedMode = state.selectedTextModelMode || 'recommended'; - const selectedModelId = state.selectedTextModelId || data.textModels[0]?.id || ''; - const selectedModel = data.textModels.find(item => item.id === selectedModelId) - || data.textModels.find(item => item.group === '推荐模型') - || data.textModels[0]; + const textModels = getAvailableTextModels(); + if (state.selectedTextModelId && !textModels.some(model => model.id === state.selectedTextModelId)) { + state.selectedTextModelId = ''; + } + if (!state.selectedTextModelId && textModels.length) { + state.selectedTextModelId = textModels[0].id; + } + const selectedModel = textModels.find(item => item.id === state.selectedTextModelId) || textModels[0] || null; return `
- ${renderSection('意图识别', ` -
-
-
开启意图识别
-
${escapeHtml(data.intentModel.desc)}
-
- -
-
关闭将不再触发调用工具/Agent 等基于语义理解的交互功能,适用于低成本、轻量级语音交互场景。
+ ${renderSection('意图识别', '', `${data.intentModel.desc} 关闭将不再触发调用工具/Agent 等基于语义理解的交互功能,适用于低成本、轻量级语音交互场景。`, ` + `)} ${renderSection('文本模型', `
模型选择
-
建议使用推荐模型,为用户提供流畅的对话体验
+
模型凭证统一在文本模型管理中维护,校验通过并启用后才可选择
- 推荐 / 更多 +
-
- - -
- + ${textModels.length ? textModels.map(item => ` + + `).join('') : ''} -
${escapeHtml(selectedModel.desc)}
+ ${selectedModel ? `
${escapeHtml(selectedModel.desc)}
` : ''} `)} - ${renderPromptEditor(data.prompt)} + ${renderPromptEditor(state.prompt || data.prompt)} ${renderSection('对话变量', ` -
- 支持在对话输入中添加变量,为模型提供用户语音之外的更多信息,如天气、周围环境等。 - -
-
- ${data.variables.map(item => ` -
-
${escapeHtml(item.name)}
-
${escapeHtml(item.desc)}
-
- `).join('')} - -
+ ${renderVariableSimpleEditor(state)} + `, '变量是智能体运行时的状态存储,可用于注入 Prompt、节点传递、流程控制和上下文记忆。', ` + `)} ${renderSection('上下文轮数', `
@@ -13739,56 +16504,23 @@ function renderUnderstandStep(state) {
`)} ${renderKnowledgeBaseSection(state.knowledgeBases || data.knowledgeBases)} - ${renderSection('对话承接语', ` -
-
-
开启对话承接语
-
大模型基于用户输入自动生成承接语,减少回复等待时间
-
- -
+ ${renderSection('对话承接语', '', '大模型基于用户输入自动生成承接语,减少回复等待时间', ` + `)} - ${renderSection('互联网搜索', ` -
-
-
开启互联网搜索
-
关闭后不展示搜索模式,仅保留本地知识和模型能力
-
- -
- ${state.knowledgeSearchEnabled !== false ? ` -
- - -
- ` : ''} + ${renderSection('互联网搜索', '', '关闭后不展示搜索模式,仅保留本地知识和模型能力', ` + `)} - ${renderSection('长期记忆', ` -
-
-
开启长期记忆
-
基于对话历史形成专属记忆库,并在后续对话中体现
-
- -
-
开启长期记忆后,结合记忆内容进行回复的对话会增加额外耗时。
+ ${renderSection('长期记忆', '', '基于对话历史形成专属记忆库,并在后续对话中体现。开启长期记忆后,结合记忆内容进行回复的对话会增加额外耗时。', ` + `)}
`; @@ -13796,104 +16528,93 @@ function renderUnderstandStep(state) { function renderSkillsStep(state) { const data = AGENT_CONFIG_MOCK.skills; - const commands = [...data.commands, ...(state.customCommands || [])]; - const plugins = [...data.plugins, ...(state.customPlugins || [])]; + const commandLibrary = ensureCommandLibrary(); + const selectedCommandIds = new Set(state.selectedCommandIds || []); + const commands = commandLibrary.filter(item => selectedCommandIds.has(item.id)); + const pluginLibrary = ensurePluginLibrary(); + const selectedPluginIds = new Set(state.selectedPluginIds || []); + const plugins = pluginLibrary.filter(item => selectedPluginIds.has(item.id)); return `
${renderSection('指令', `
- 当前已选择的指令可以编辑,右侧加号可添加新的指令类型 + 当前智能体只会调用已挂载的指令。指令创建后会保存在指令库中,可重复选择。
- ${commands.map(item => ` + ${commands.length ? commands.map(item => `
-
+
-
${escapeHtml(item.name)} ${item.count || 1} 指令
+
${escapeHtml(item.name)} ${escapeHtml(item.commandType || '自定义指令')}
${escapeHtml(item.desc)}
- - + +
- `).join('')} + `).join('') : ` +
+ + 暂无已挂载指令 + 从 0 开始创建指令,创建后可在指令库中重复选择。 + +
+ `}
`)} ${renderSection('插件', `
- 插件能力默认由系统执行,也可以独立配置启用状态 - + 当前智能体只会调用已挂载的插件。插件创建后会保存在插件库中,可重复选择。 +
- ${plugins.map(item => ` + ${plugins.length ? plugins.map(item => `
-
+
-
${escapeHtml(item.name)}
+
${escapeHtml(item.name)} ${escapeHtml(item.pluginType || '自定义插件')}
${escapeHtml(item.desc)}
${escapeHtml(item.status || '系统执行')} - - + +
- `).join('')} + `).join('') : ` +
+ + 暂无已挂载插件 + 从 0 开始创建插件,创建后可在插件库中重复选择。 + +
+ `}
`)} - ${renderMiniAppSection('百炼应用', data.bailianApps, 'boxes')} - ${renderMiniAppSection('三方应用', data.thirdPartyApps, 'plug')} - ${renderMcpSection()} - ${renderSection('Planning / Function Calling', ` - ${renderSwitchRow('开启 Planning', '让智能体先规划步骤,再调用函数或插件完成复杂任务', true)} - ${renderSwitchRow('开启 Function Calling', '允许模型按照结构化参数调用已授权函数', true)} - `)} + ${renderMcpSection(state)}
`; } -function renderAdvancedStep() { - return ` -
- ${renderSection('高级设置', ` - ${renderSwitchRow('儿童安全模式', '强制启用儿童内容安全策略、危险行为拒答和敏感信息保护', true)} - ${renderSwitchRow('链路追踪', '记录 ASR、LLM、RAG、TTS 的耗时和调用链路', true)} - ${renderSwitchRow('自动降级', '主模型异常时自动切换到备用模型,保证设备可用性', true)} - `)} - ${renderSection('调用预算', ` -
- - -
- `)} - ${renderSection('发布策略', ` -
- ${renderRadio('release', 'gray', '灰度发布 10%', true)} - ${renderRadio('release', 'full', '全量发布')} - ${renderRadio('release', 'schedule', '定时发布')} -
- `)} -
- `; -} - -function renderPreviewPane(previewState) { - const title = previewState === 'running' ? '预览环境已准备好' : '请填好配置后点击运行'; - const desc = previewState === 'running' - ? '当前智能体已加载最新 mock 配置,可继续调整参数后再次运行。' - : '当前应用测试会产生实际调用,按对话次数计费'; +function renderPreviewPane(state) { + if (state.preview === 'voice-auth') { + return renderRuntimeAuthPreview(state, 'voice'); + } + if (state.preview === 'text-auth') { + return renderRuntimeAuthPreview(state, 'text'); + } + if (state.preview === 'voice-runtime') { + return renderVoiceRuntimePreview(state); + } + if (state.preview === 'text-runtime') { + return renderTextRuntimePreview(state); + } return `
-

${title}

-

${desc}

+

请填好配置后点击运行

+

当前应用测试会产生实际调用,按对话次数计费

建议佩戴耳机,并在安静环境下测试,对话体验更佳。 @@ -13903,28 +16624,563 @@ function renderPreviewPane(previewState) { `; } -function renderSceneCard(scene, version, language) { +function renderRuntimeAuthPreview(state, mode = 'voice') { + const savedKeys = getRuntimeApiKeysForApp(state); + const hasKeys = savedKeys.length > 0; + const isVoice = mode === 'voice'; return ` - +
+
+
+
+ ${isVoice ? 'Voice Runtime' : 'Text Runtime'} +

配置运行 API Key

+

运行前已自动保存当前配置。请选择系统设置里已保存的 API Key 后进入${isVoice ? '语音测试' : '文字聊天测试'}。

+
+ 配置已保存 +
+
+ +
+ + ${hasKeys ? '' : ` + + `} +
+
+
+ + 运行面板只引用系统设置中已保存的 API Key。后端接入后,这里应只传递 Key ID,由后端安全读取密钥并创建运行会话。 +
+
+
`; } -function renderSection(title, content) { +function renderVoiceRuntimePreview(state) { + const session = getRunSession(state); + const latency = session.latencyMs == null ? '3377ms' : `${session.latencyMs}ms`; + const recordText = session.isRecording ? formatRuntimeDuration(session.elapsedSeconds) : '00:00'; + const transcript = session.transcript || '正在监听,请开始说话...'; + const aiResponse = session.aiDisplayResponse !== undefined ? session.aiDisplayResponse : (session.aiResponse || ''); + const userName = getAgentRuntimeUserName(); + const hasVoiceConversation = session.isRecording || Boolean(session.transcript || session.aiResponse || session.aiDisplayResponse); return ` -
-

${escapeHtml(title)}

- ${content} +
+
+
+
+

语音交互体验

+ +
+
+ ${escapeHtml(userName)} + + +
+
+ +
+ ${hasVoiceConversation ? ` +
+
+
+
${escapeHtml(transcript)}
+
+
+
+
+ ${aiResponse ? escapeHtml(aiResponse) : ''} +
+
+ + +
+
+
+
+
+
端到端首包延迟:${escapeHtml(latency)}
+ ` : ` +
+ +

${escapeHtml(state.app?.name || 'TalkingQ Agent')}

+

建议佩戴耳机,并在安静环境下测试,对话体验更佳。

+
+ `} +
+ +
+ ${session.isRecording ? ` +
+
${escapeHtml(recordText)}
+
+ +
+ + +
+ ` : ` + + + `} +
+ ${session.micMuted ? '
麦克风已关闭,当前暂停收音。
' : ''} +
+
+ `; +} + +function renderTextRuntimePreview(state) { + const session = getRunSession(state); + const messages = Array.isArray(session.chatMessages) ? session.chatMessages : []; + const userName = getAgentRuntimeUserName(); + return ` +
+
+
+
+

文本交互体验

+ +
+
+ ${escapeHtml(userName)} + + +
+
+
+
+ ${messages.length ? messages.map((message, index) => renderTextChatMessage(message, index)).join('') : ` +
+ 开始输入第一条测试消息 +
+ `} + ${session.textPending ? ` +
+
+
+
+
+ ` : ''} + ${session.latencyMs ? ` +
端到端 ${escapeHtml(String(session.latencyMs))}ms
+ ` : ''} +
+
+
+
+ + +
+
+
+
+ `; +} + +function renderTextChatMessage(message, index) { + if (message.role === 'user') { + return ` +
+
${escapeHtml(message.content)}
+
+ `; + } + const content = message.displayContent !== undefined ? message.displayContent : message.content; + return ` +
+
+
${escapeHtml(content || '')}
+ ${message.content ? ` +
+ + +
+ ` : ''} +
+
+ `; +} + +function getRunSession(state) { + if (!state.runSession) { + state.runSession = {}; + } + return state.runSession; +} + +function getRuntimeApiKeysForApp(state) { + const settings = typeof loadSettings === 'function' + ? loadSettings() + : (window.__talkingqSettings || {}); + if (Array.isArray(settings.apiKeyRecords)) { + const records = settings.apiKeyRecords + .filter(record => String(record.value || '').trim()) + .map(record => ({ + id: record.id, + name: record.name || 'API Key', + provider: record.provider || record.providerId || 'Custom', + value: record.value, + })); + if (records.length) return records; + } + const apiKeys = settings.apiKeys || {}; + return [ + { id: 'openai', name: 'OpenAI API Key', provider: 'OpenAI', value: apiKeys.openai || '' }, + { id: 'ali', name: '阿里云百炼 API Key', provider: 'Alibaba Cloud Bailian', value: apiKeys.ali || '' }, + { id: 'gemini', name: 'Google Gemini API Key', provider: 'Google Gemini', value: apiKeys.gemini || '' }, + ].filter(item => String(item.value || '').trim()); +} + +function formatRuntimeDuration(seconds) { + const safe = Math.max(0, Number(seconds) || 0); + const min = Math.floor(safe / 60); + const sec = safe % 60; + return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; +} + +function getAgentRunTimerKey(state) { + return state.app?.id || 'default'; +} + +function clearAgentRunTimer(state) { + const timers = ensureAgentRunTimers(); + const key = getAgentRunTimerKey(state); + if (timers[key]) { + window.clearInterval(timers[key]); + delete timers[key]; + } +} + +function buildAgentConfigSnapshot(state) { + return { + appId: state.app?.id, + appName: state.app?.name, + savedAt: state.lastSavedAt, + voiceEnabled: state.voiceEnabled, + asrEnabled: state.asrEnabled, + ttsEnabled: state.ttsEnabled, + selectedInteractionLanguage: state.selectedInteractionLanguage, + selectedAsrModelId: state.selectedAsrModelId, + selectedTtsModelId: state.selectedTtsModelId, + selectedTextModelId: state.selectedTextModelId, + selectedHotwordGroupId: state.selectedHotwordGroupId, + ttsFilterEnabled: state.ttsFilterEnabled, + interruptPrimaryEnabled: state.interruptPrimaryEnabled, + interruptSecondaryEnabled: state.interruptSecondaryEnabled, + semanticStopEnabled: state.semanticStopEnabled, + intentEnabled: state.intentEnabled, + dialogueContinuationEnabled: state.dialogueContinuationEnabled, + knowledgeSearchEnabled: state.knowledgeSearchEnabled, + longTermMemoryEnabled: state.longTermMemoryEnabled, + prompt: state.prompt, + variables: state.variables, + knowledgeBases: state.knowledgeBases, + selectedCommandIds: state.selectedCommandIds, + selectedPluginIds: state.selectedPluginIds, + selectedMcpServiceIds: state.selectedMcpServiceIds, + }; +} + +function updateAgentAutosaveIndicator(state) { + const indicator = document.querySelector('#agentAutosaveStatus'); + if (!indicator) return; + indicator.classList.toggle('saving', state.saveStatus === 'saving'); + indicator.textContent = state.saveStatus === 'saving' + ? '自动保存中' + : state.lastSavedAt ? `已自动保存 ${getTimeOnly(state.lastSavedAt)}` : '等待自动保存'; +} + +function autoSaveAgentConfig(state) { + state.saveStatus = 'saved'; + state.lastSavedAt = getNowText(); + const store = ensureAgentConfigAutosaves(); + store[state.app?.id || 'default'] = buildAgentConfigSnapshot(state); + updateAgentAutosaveIndicator(state); +} + +function scheduleAgentConfigAutoSave(state) { + if (state.__saveTimer) { + window.clearTimeout(state.__saveTimer); + } + state.saveStatus = 'saving'; + updateAgentAutosaveIndicator(state); + state.__saveTimer = window.setTimeout(() => { + autoSaveAgentConfig(state); + state.__saveTimer = null; + }, 300); +} + +function getMockTranscriptForSecond(second) { + const fragments = [ + '你好小Q', + '你好小Q,今天可以给我讲一个太空故事吗', + '你好小Q,今天可以给我讲一个太空故事吗,里面要有机器人和小朋友', + '你好小Q,今天可以给我讲一个太空故事吗,里面要有机器人和小朋友,最后要开心一点', + ]; + if (second <= 1) return fragments[0]; + if (second <= 2) return fragments[1]; + if (second <= 4) return fragments[2]; + return fragments[3]; +} + +function getMockAiResponseForSecond(second) { + if (second < 2) return ''; + if (second < 4) return '可以呀,我正在准备一个温柔的太空小故事。'; + return '当然可以。小朋友坐上银色小飞船,和机器人小Q一起飞向月亮。他们发现一颗会唱歌的小星星,最后把星光带回家,送给每一个睡前还想听故事的小朋友。'; +} + +function getAgentRuntimeUserName() { + return window.__currentUser?.displayName + || window.__currentUser?.name + || window.currentUser?.displayName + || window.currentUser?.name + || 'Admin'; +} + +function syncVoiceRuntimeDom(container, state) { + const panel = container.querySelector('.agent-voice-panel.recording'); + if (!panel || state.preview !== 'voice-runtime') return false; + const session = getRunSession(state); + const latency = session.latencyMs == null ? '3377ms' : `${session.latencyMs}ms`; + const recordText = session.isRecording ? formatRuntimeDuration(session.elapsedSeconds) : '00:00'; + const transcript = session.transcript || '正在监听,请开始说话...'; + const aiResponse = session.aiDisplayResponse !== undefined ? session.aiDisplayResponse : (session.aiResponse || ''); + + const transcriptNode = panel.querySelector('[data-voice-runtime="transcript"]'); + if (transcriptNode) transcriptNode.textContent = transcript; + + const aiContentNode = panel.querySelector('[data-voice-runtime="ai-content"]'); + if (aiContentNode) { + if (aiResponse) { + aiContentNode.textContent = aiResponse; + } else if (!aiContentNode.querySelector('.agent-runtime-typing')) { + aiContentNode.innerHTML = ''; + } + } + + const actionsNode = panel.querySelector('[data-voice-runtime="ai-actions"]'); + if (actionsNode) { + actionsNode.classList.toggle('is-visible', Boolean(session.aiResponse)); + actionsNode.setAttribute('aria-hidden', session.aiResponse ? 'false' : 'true'); + } + + const latencyNode = panel.querySelector('[data-voice-runtime="latency"]'); + if (latencyNode) latencyNode.textContent = `端到端首包延迟:${latency}`; + + const recordNode = panel.querySelector('[data-voice-runtime="record-time"]'); + if (recordNode) recordNode.textContent = recordText; + + const waveNode = panel.querySelector('[data-voice-runtime="wave"]'); + if (waveNode) waveNode.classList.toggle('muted', Boolean(session.micMuted)); + + const micButton = panel.querySelector('[data-voice-runtime="mic-button"]'); + if (micButton) { + micButton.classList.toggle('muted', Boolean(session.micMuted)); + micButton.setAttribute('aria-label', session.micMuted ? '打开麦克风' : '关闭麦克风'); + const iconName = session.micMuted ? 'mic-off' : 'mic'; + const icon = micButton.querySelector('i, svg'); + if (icon?.getAttribute('data-lucide') !== iconName) { + micButton.innerHTML = ``; + renderAgentIcons(micButton); + } + } + + const voiceScrollArea = panel.querySelector('.agent-voice-stage') || panel.querySelector('.agent-voice-conversation'); + if (voiceScrollArea) { + voiceScrollArea.scrollTop = voiceScrollArea.scrollHeight; + } + + return true; +} + +function revealVoiceReply(container, state, fullText) { + const session = getRunSession(state); + let cursor = 0; + const step = Math.max(1, Math.ceil(fullText.length / 36)); + if (session.__voiceRevealTimer) { + window.clearInterval(session.__voiceRevealTimer); + } + session.__voiceRevealTimer = window.setInterval(() => { + cursor = Math.min(fullText.length, cursor + step); + session.aiDisplayResponse = fullText.slice(0, cursor); + if (!syncVoiceRuntimeDom(container, state)) { + updatePreviewOnly(container, state); + } + if (cursor >= fullText.length) { + session.aiDisplayResponse = fullText; + window.clearInterval(session.__voiceRevealTimer); + session.__voiceRevealTimer = null; + if (!syncVoiceRuntimeDom(container, state)) { + updatePreviewOnly(container, state); + } + } + }, 38); +} + +function buildMockTextReply(value, retryCount = 0) { + const suffix = retryCount ? `这是第 ${retryCount + 1} 版重新生成结果。` : '这是当前前端 mock 回复效果。'; + return `我收到了:“${value}”。${suffix} 后端接入后,这里会返回真实模型的流式回复。`; +} + +function revealTextReply(container, state, messageIndex, fullText) { + const session = getRunSession(state); + let cursor = 0; + const step = Math.max(1, Math.ceil(fullText.length / 34)); + const timer = window.setInterval(() => { + const messages = Array.isArray(session.chatMessages) ? session.chatMessages : []; + const message = messages[messageIndex]; + if (!message) { + window.clearInterval(timer); + return; + } + cursor = Math.min(fullText.length, cursor + step); + message.displayContent = fullText.slice(0, cursor); + updatePreviewOnly(container, state); + if (cursor >= fullText.length) { + message.displayContent = fullText; + window.clearInterval(timer); + updatePreviewOnly(container, state); + } + }, 38); +} + +function startVoiceRuntimeCall(container, state) { + const session = getRunSession(state); + clearAgentRunTimer(state); + if (session.__voiceRevealTimer) { + window.clearInterval(session.__voiceRevealTimer); + session.__voiceRevealTimer = null; + } + session.isRecording = true; + session.micMuted = false; + session.elapsedSeconds = 0; + session.latencyMs = 3377; + session.transcript = '正在监听,请开始说话...'; + session.aiResponse = ''; + session.aiDisplayResponse = ''; + session.lastVoiceAiResponse = ''; + session.startedAt = Date.now(); + updatePreviewOnly(container, state); + const timers = ensureAgentRunTimers(); + timers[getAgentRunTimerKey(state)] = window.setInterval(() => { + if (!session.isRecording) { + clearAgentRunTimer(state); + return; + } + session.elapsedSeconds = Math.floor((Date.now() - session.startedAt) / 1000); + if (!session.micMuted) { + const nextTranscript = getMockTranscriptForSecond(session.elapsedSeconds); + const nextResponse = getMockAiResponseForSecond(session.elapsedSeconds); + session.transcript = nextTranscript; + session.latencyMs = 3377 + ((session.elapsedSeconds * 83) % 420); + if (nextResponse && nextResponse !== session.lastVoiceAiResponse) { + session.aiResponse = nextResponse; + session.aiDisplayResponse = ''; + session.lastVoiceAiResponse = nextResponse; + revealVoiceReply(container, state, nextResponse); + return; + } + } + if (!syncVoiceRuntimeDom(container, state)) { + updatePreviewOnly(container, state); + } + }, 1000); +} + +function stopVoiceRuntimeCall(container, state) { + const session = getRunSession(state); + clearAgentRunTimer(state); + session.isRecording = false; + session.elapsedSeconds = Math.max(session.elapsedSeconds || 0, Math.floor((Date.now() - (session.startedAt || Date.now())) / 1000)); + session.latencyMs = session.latencyMs || 3377; + session.transcript = session.transcript || '本轮没有检测到有效语音。'; + session.aiResponse = session.aiResponse || '我已经结束本轮收音,可以再次点击电话按钮开始新的语音测试。'; + session.aiDisplayResponse = session.aiDisplayResponse || session.aiResponse; + updatePreviewOnly(container, state); +} + +function renderSection(title, content, tooltip = '', headerAction = '') { + const hasContent = Boolean(String(content || '').trim()); + const titleInfo = tooltip ? ` + + ` : ''; + const titleMarkup = ` +

+ ${escapeHtml(title)} + ${titleInfo} +

+ `; + return ` +
+ ${headerAction ? ` +
+ ${titleMarkup} + ${headerAction} +
+ ` : titleMarkup} + ${hasContent ? content : ''}
`; } @@ -13951,6 +17207,9 @@ function renderFeatureSelect(config) { options = [], groupedOptions = null, hint = '', + emptyText = '暂无可选模型', + manageAction = '', + manageText = '', } = config; const fieldAction = toggleKey === 'asr-enabled' ? 'asr-model' : toggleKey === 'tts-enabled' ? 'tts-model' @@ -13968,8 +17227,8 @@ function renderFeatureSelect(config) {
- + ${options.length === 0 ? `` : groupedOptions ? groupedOptions.map(group => ` ${group.models.map(model => ` @@ -13980,21 +17239,12 @@ function renderFeatureSelect(config) { `).join('')} ${selectedOption ? `
${escapeHtml(selectedOption.desc)}
` : ''} -
- `; -} - -function renderSwitchRow(label, desc, checked) { - return ` -
-
-
${escapeHtml(label)}
-
${escapeHtml(desc)}
-
- + ${manageAction ? ` + + ` : ''}
`; } @@ -14011,29 +17261,1147 @@ function renderRadio(name, value, label, checked = false) { function renderTtsFilterSummary(state) { if (!state.ttsFilterEnabled) return ''; - const commonPairs = (state.ttsFilterPairs || []) - .filter(item => item.enabled !== false) - .map(item => `${item.left} ${item.right}`.trim()); - const singles = state.ttsFilterSingles || []; + const pairKeys = new Set(); + const pairs = (state.ttsFilterPairs || []).reduce((list, item) => { + if (item.enabled === false) return list; + const left = String(item.left || '').trim(); + const right = String(item.right || '').trim(); + if (!left && !right) return list; + const key = `${left}__${right}`; + if (pairKeys.has(key)) return list; + pairKeys.add(key); + list.push(`${left} ${right}`.trim()); + return list; + }, []); + const singles = [...new Set((state.ttsFilterSingles || []).map(item => String(item || '').trim()).filter(Boolean))]; + const tokens = [...pairs, ...singles.map(item => `字符 ${item}`)]; + const visibleTokens = tokens.slice(0, 5); + const hiddenCount = Math.max(tokens.length - visibleTokens.length, 0); return `
-
-
常用过滤符号对
-
- ${commonPairs.length ? commonPairs.map(item => `${escapeHtml(item)}`).join('') : '暂无'} +
+
播报过滤规则
+
${pairs.length} 组符号对 · ${singles.length} 个单字符
+
+ ${visibleTokens.length ? visibleTokens.map(item => `${escapeHtml(item)}`).join('') : '暂无规则'} + ${hiddenCount ? `+${hiddenCount}` : ''}
-
-
自定义过滤符号对与单字符
-
- ${singles.length ? singles.map(item => `${escapeHtml(item)}`).join('') : '暂无'} -
- + +
+ `; +} + +function renderVariableSimpleEditor(state) { + const variables = state.variables || []; + return ` +
+
+ 变量名 + 描述 + +
+
+ ${variables.length ? variables.map(item => ` +
+ + + +
+ `).join('') : ` +
+ 暂无变量,点击右上角添加。 +
+ `}
`; } +function getSpeechModelDisplayStatus(model) { + if (!model) return 'failed'; + if (model.status === 'connected') return 'available'; + if (model.status === 'unconnected') return 'failed'; + if (model.status === 'available' && model.enabled === false) return 'disabled'; + return model.status || 'failed'; +} + +function getAsrModelDisplayStatus(model) { + return getSpeechModelDisplayStatus(model); +} + +function getTtsModelDisplayStatus(model) { + return getSpeechModelDisplayStatus(model); +} + +function getAsrStatusMeta(model) { + return ASR_MODEL_STATUS_META[getAsrModelDisplayStatus(model)] || ASR_MODEL_STATUS_META.failed; +} + +function getTtsStatusMeta(model) { + return TTS_MODEL_STATUS_META[getTtsModelDisplayStatus(model)] || TTS_MODEL_STATUS_META.failed; +} + +function maskSecret(value) { + if (!value) return '未填写'; + return `${String(value).slice(0, 4)}••••••••${String(value).slice(-3)}`; +} + +function getLanguageTypeName(languageId) { + return AGENT_INTERACTION_LANGUAGES.find(item => item.id === languageId)?.name || languageId || '多语言'; +} + +function getTtsVoiceIds(model) { + if (Array.isArray(model?.voiceIds)) { + return model.voiceIds.filter(Boolean); + } + return model?.voiceId ? [model.voiceId] : []; +} + +function getTtsVoiceSummary(model) { + const voiceIds = getTtsVoiceIds(model); + if (!voiceIds.length) return '未配置 Voice'; + if (voiceIds.length === 1) return `Voice ${voiceIds[0]}`; + return `${voiceIds.length} 个 Voice`; +} + +function parseTtsVoiceIds(value) { + return [...new Set(String(value || '') + .split(/[\n,,;;\s]+/) + .map(item => item.trim()) + .filter(Boolean))]; +} + +function getSpeechModelValidationError(model, type) { + const apiKey = String(model.apiKey || '').trim().toLowerCase(); + const baseUrl = String(model.baseUrl || '').trim().toLowerCase(); + const voiceIds = getTtsVoiceIds(model).map(item => String(item).trim().toLowerCase()); + if (!apiKey || apiKey.includes('invalid') || apiKey.startsWith('bad') || apiKey === 'sk-invalid') { + return 'Key 无效'; + } + if (!/^https?:\/\//i.test(model.baseUrl || '') || /down|timeout|error|404|500/.test(baseUrl)) { + return '接口异常'; + } + if (type === 'TTS' && voiceIds.some(voiceId => /missing|notfound|not-found|none/.test(voiceId))) { + return 'Voice 不存在'; + } + if (type === 'TTS' && !TTS_AUDIO_FORMAT_OPTIONS.some(item => item.id === model.audioFormat)) { + return '返回格式错误'; + } + return ''; +} + +function markSpeechModelValidating(model) { + model.status = 'validating'; + model.enabled = false; + model.errorMessage = ''; + model.updatedAt = getNowText(); +} + +function markSpeechModelAvailable(model, shouldEnable = true) { + model.status = shouldEnable ? 'available' : 'disabled'; + model.enabled = Boolean(shouldEnable); + model.errorMessage = ''; + model.validatedAt = getNowText(); + model.updatedAt = getNowText(); +} + +function markSpeechModelFailed(model, message) { + model.status = 'failed'; + model.enabled = false; + model.errorMessage = message || '连接校验失败'; + model.updatedAt = getNowText(); +} + +function renderAsrModelManagerContent(showForm = false) { + const models = ensureAsrModelRegistry(); + return ` +
+
+ +
ASR 模型凭证统一维护在这里。创建后系统会自动校验接口连通性,只有状态为「可用」且已启用的模型才会进入智能体的语音识别下拉框。
+
+ + ${showForm ? ` +
+
+
+
新增 ASR 模型配置
+
研发可先登记模型名称、Provider、Base URL 与密钥;创建后自动开始后台校验。
+
+
+
+ + + + + +
+ +
+ + +
+
+ ` : ` +
+
+
ASR 模型列表
+
模型创建并校验通过后,才会出现在智能体配置下拉框中。
+
+ +
+ `} + +
+
已添加模型
+ ${models.length} 个配置 +
+
+ ${models.length ? models.map(model => { + const meta = getAsrStatusMeta(model); + const status = getAsrModelDisplayStatus(model); + const canToggle = status === 'available' || status === 'disabled'; + return ` +
+
+
+ ${escapeHtml(model.name)} + ${escapeHtml(meta.label)} +
+
+ Provider:${escapeHtml(model.provider)} + Base URL:${escapeHtml(model.baseUrl)} + API Key:${escapeHtml(maskSecret(model.apiKey))} +
+
+ Secret Key:${escapeHtml(maskSecret(model.secretKey))} + 更新时间:${escapeHtml(model.updatedAt || '-')} + ${model.validatedAt ? `最近校验:${escapeHtml(model.validatedAt)}` : ''} +
+ ${model.errorMessage ? `
失败原因:${escapeHtml(model.errorMessage)}
` : ''} +
+
+ + + +
+
+ `; + }).join('') : ` +
+ + 暂无 ASR 模型 + 创建并自动校验成功后,智能体即可选择该识别模型。 + +
+ `} +
+
+ `; +} + +function collectAsrModelForm(body) { + const name = body.querySelector('#asrModelName')?.value.trim(); + const provider = body.querySelector('#asrProviderName')?.value.trim(); + const baseUrl = body.querySelector('#asrBaseUrl')?.value.trim(); + const apiKey = body.querySelector('#asrApiKey')?.value.trim(); + const secretKey = body.querySelector('#asrSecretKey')?.value.trim(); + const enableAfterValidation = body.querySelector('#asrEnableAfterValidation')?.checked !== false; + if (!name || !provider || !baseUrl || !apiKey) { + Toast.warning('请填写模型名称、Provider、Base URL 和 API Key'); + return null; + } + if (!/^https?:\/\//i.test(baseUrl)) { + Toast.warning('Base URL 需要以 http:// 或 https:// 开头'); + return null; + } + return { + id: `asr-${Date.now()}`, + name, + provider, + baseUrl, + apiKey, + secretKey, + enableAfterValidation, + status: 'validating', + enabled: false, + updatedAt: getNowText(), + }; +} + +function validateAsrModelConnection(model) { + const validationError = getSpeechModelValidationError(model, 'ASR'); + if (validationError) { + return new Promise(resolve => setTimeout(() => resolve({ success: false, message: validationError }), 650)); + } + const validate = API.agentDev.validateASRModel || API.agentDev.testASRModel; + return validate(model.id, { + provider: model.provider, + baseUrl: model.baseUrl, + }); +} + +function openAsrModelManagerModal(state, rerender) { + let showCreateForm = false; + const modal = createModal({ + title: 'ASR 模型管理', + size: 'xl', + showCancel: false, + confirmText: '关闭', + content: renderAsrModelManagerContent(showCreateForm), + onConfirm: () => true, + }); + + const refreshModal = () => { + const body = modal.overlay.querySelector('.modal-body'); + if (!body) return; + body.innerHTML = renderAsrModelManagerContent(showCreateForm); + lucide.createIcons({ elements: [body] }); + bindAsrModelManagerEvents(body); + rerender(); + }; + + const finishValidation = async (model, shouldEnable = true) => { + const result = await validateAsrModelConnection(model); + if (result.success === false) { + markSpeechModelFailed(model, result.message || 'ASR 模型连接失败'); + Toast.error(model.errorMessage); + } else { + markSpeechModelAvailable(model, shouldEnable); + if (model.enabled && !state.selectedAsrModelId) state.selectedAsrModelId = model.id; + Toast.success(`ASR 模型「${model.name}」校验通过`); + } + }; + + const bindAsrModelManagerEvents = (body) => { + body.querySelectorAll('[data-asr-action="show-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = true; + refreshModal(); + })); + + body.querySelectorAll('[data-asr-action="cancel-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = false; + refreshModal(); + })); + + body.querySelectorAll('[data-asr-action="create-new"]').forEach(btn => btn.addEventListener('click', async () => { + const model = collectAsrModelForm(body); + if (!model) return; + markSpeechModelValidating(model); + ensureAsrModelRegistry().push(model); + await API.agentDev.createASRModel(model); + Toast.info(`ASR 模型「${model.name}」开始自动校验`, 800); + showCreateForm = false; + refreshModal(); + await finishValidation(model, model.enableAfterValidation !== false); + refreshModal(); + })); + + body.querySelectorAll('[data-asr-action="validate-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureAsrModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + markSpeechModelValidating(model); + refreshModal(); + await finishValidation(model, true); + refreshModal(); + }); + }); + + body.querySelectorAll('[data-asr-action="toggle-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureAsrModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + const status = getAsrModelDisplayStatus(model); + if (status !== 'available' && status !== 'disabled') { + Toast.warning('只有校验通过的模型才可以启用或禁用'); + return; + } + if (status === 'disabled') { + await API.agentDev.setASRModelEnabled(model.id, true); + markSpeechModelAvailable(model, true); + if (!state.selectedAsrModelId) state.selectedAsrModelId = model.id; + Toast.success(`ASR 模型「${model.name}」已启用`); + } else { + await API.agentDev.setASRModelEnabled(model.id, false); + markSpeechModelAvailable(model, false); + if (state.selectedAsrModelId === model.id) state.selectedAsrModelId = ''; + Toast.info(`ASR 模型「${model.name}」已禁用`); + } + refreshModal(); + }); + }); + + body.querySelectorAll('[data-asr-action="delete-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const registry = ensureAsrModelRegistry(); + const model = registry.find(item => item.id === btn.dataset.id); + if (!model) return; + await API.agentDev.deleteASRModel(model.id); + window.__asrModelRegistry = registry.filter(item => item.id !== model.id); + if (state.selectedAsrModelId === model.id) state.selectedAsrModelId = ''; + Toast.success(`ASR 模型「${model.name}」已删除`); + refreshModal(); + }); + }); + }; + + const body = modal.overlay.querySelector('.modal-body'); + if (body) { + lucide.createIcons({ elements: [body] }); + bindAsrModelManagerEvents(body); + } +} + +function renderTtsModelManagerContent(showForm = false) { + const models = ensureTtsModelRegistry(); + return ` +
+
+ +
TTS 模型凭证、Voice ID、音频格式和语言类型统一维护在这里。创建模型后系统会自动校验 Key、接口、Voice 与音频返回,校验通过后才会进入智能体的语音合成下拉框。
+
+ + ${showForm ? ` +
+
+
+
新增 TTS 模型配置
+
无需手动测试连接;点击创建模型后自动进入后台验证流程。
+
+
+
+ + + + + +
+ Voice 配置(可选) +
+ + +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ ` : ` +
+
+
TTS 模型列表
+
模型创建并自动校验通过后,才会出现在智能体配置下拉框中。
+
+ +
+ `} + +
+
已添加模型
+ ${models.length} 个配置 +
+
+ ${models.length ? models.map(model => { + const meta = getTtsStatusMeta(model); + const status = getTtsModelDisplayStatus(model); + const canToggle = status === 'available' || status === 'disabled'; + return ` +
+
+
+ ${escapeHtml(model.name)} + ${escapeHtml(meta.label)} +
+
+ Provider:${escapeHtml(model.provider)} + Base URL:${escapeHtml(model.baseUrl)} + API Key:${escapeHtml(maskSecret(model.apiKey))} +
+
+ Secret Key:${escapeHtml(maskSecret(model.secretKey))} + Voice:${escapeHtml(getTtsVoiceSummary(model))} + 音频格式:${escapeHtml(String(model.audioFormat || '').toUpperCase())} + 语言类型:${escapeHtml(getLanguageTypeName(model.languageType))} +
+
+ 更新时间:${escapeHtml(model.updatedAt || '-')} + ${model.validatedAt ? `最近校验:${escapeHtml(model.validatedAt)}` : ''} +
+ ${model.errorMessage ? `
失败原因:${escapeHtml(model.errorMessage)}
` : ''} +
+
+ + + +
+
+ `; + }).join('') : ` +
+ + 暂无 TTS 模型 + 创建并自动校验成功后,智能体即可选择该合成模型。 + +
+ `} +
+
+ `; +} + +function collectTtsModelForm(body) { + const name = body.querySelector('#ttsModelName')?.value.trim(); + const provider = body.querySelector('#ttsProviderName')?.value.trim(); + const baseUrl = body.querySelector('#ttsBaseUrl')?.value.trim(); + const apiKey = body.querySelector('#ttsApiKey')?.value.trim(); + const secretKey = body.querySelector('#ttsSecretKey')?.value.trim(); + const voiceMode = body.querySelector('input[name="ttsVoiceMode"]:checked')?.value || 'single'; + const voiceId = body.querySelector('#ttsVoiceId')?.value.trim(); + const voiceIds = voiceMode === 'batch' + ? parseTtsVoiceIds(body.querySelector('#ttsVoiceIds')?.value) + : (voiceId ? [voiceId] : []); + const audioFormat = body.querySelector('#ttsAudioFormat')?.value || 'mp3'; + const languageType = body.querySelector('#ttsLanguageType')?.value || 'zh'; + const enableAfterValidation = body.querySelector('#ttsEnableAfterValidation')?.checked !== false; + if (!name || !provider || !baseUrl || !apiKey || !audioFormat || !languageType) { + Toast.warning('请填写模型名称、Provider、Base URL、API Key、音频格式和语言类型'); + return null; + } + if (!/^https?:\/\//i.test(baseUrl)) { + Toast.warning('Base URL 需要以 http:// 或 https:// 开头'); + return null; + } + if (voiceMode === 'batch' && voiceIds.length === 0) { + Toast.warning('批量添加 Voice 时,请至少填写一个 Voice ID'); + return null; + } + if (voiceIds.length > 50) { + Toast.warning('单次最多批量添加 50 个 Voice ID'); + return null; + } + return { + id: `tts-${Date.now()}`, + name, + provider, + baseUrl, + apiKey, + secretKey, + voiceMode, + voiceId: voiceIds[0] || '', + voiceIds, + audioFormat, + languageType, + enableAfterValidation, + status: 'validating', + enabled: false, + updatedAt: getNowText(), + }; +} + +function validateTtsModelConnection(model) { + const validationError = getSpeechModelValidationError(model, 'TTS'); + if (validationError) { + return new Promise(resolve => setTimeout(() => resolve({ success: false, message: validationError }), 750)); + } + const validate = API.agentDev.validateTTSModel || API.agentDev.testTTS; + return validate(model.id, { + provider: model.provider, + baseUrl: model.baseUrl, + voiceId: model.voiceId, + voiceIds: getTtsVoiceIds(model), + audioFormat: model.audioFormat, + languageType: model.languageType, + }); +} + +function openTtsModelManagerModal(state, rerender) { + let showCreateForm = false; + const modal = createModal({ + title: 'TTS 模型管理', + size: 'xl', + showCancel: false, + confirmText: '关闭', + content: renderTtsModelManagerContent(showCreateForm), + onConfirm: () => true, + }); + + const refreshModal = () => { + const body = modal.overlay.querySelector('.modal-body'); + if (!body) return; + body.innerHTML = renderTtsModelManagerContent(showCreateForm); + lucide.createIcons({ elements: [body] }); + bindTtsModelManagerEvents(body); + rerender(); + }; + + const finishValidation = async (model, shouldEnable = true) => { + const result = await validateTtsModelConnection(model); + if (result.success === false) { + markSpeechModelFailed(model, result.message || 'TTS 模型连接失败'); + Toast.error(model.errorMessage); + } else { + markSpeechModelAvailable(model, shouldEnable); + if (model.enabled && !state.selectedTtsModelId) state.selectedTtsModelId = model.id; + Toast.success(`TTS 模型「${model.name}」校验通过`); + } + }; + + const bindTtsModelManagerEvents = (body) => { + const syncTtsVoiceMode = () => { + const mode = body.querySelector('input[name="ttsVoiceMode"]:checked')?.value || 'single'; + body.querySelectorAll('.bailian-voice-mode-card').forEach(card => { + const input = card.querySelector('input[name="ttsVoiceMode"]'); + card.classList.toggle('active', input?.value === mode); + }); + body.querySelectorAll('[data-tts-voice-panel]').forEach(panel => { + panel.classList.toggle('hidden', panel.dataset.ttsVoicePanel !== mode); + }); + }; + + body.querySelectorAll('input[name="ttsVoiceMode"]').forEach(radio => { + radio.addEventListener('change', syncTtsVoiceMode); + }); + syncTtsVoiceMode(); + + body.querySelectorAll('[data-tts-action="show-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = true; + refreshModal(); + })); + + body.querySelectorAll('[data-tts-action="cancel-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = false; + refreshModal(); + })); + + body.querySelectorAll('[data-tts-action="create-new"]').forEach(btn => btn.addEventListener('click', async () => { + const model = collectTtsModelForm(body); + if (!model) return; + markSpeechModelValidating(model); + ensureTtsModelRegistry().push(model); + await API.agentDev.createTTSModel(model); + Toast.info(`TTS 模型「${model.name}」开始自动校验`, 800); + showCreateForm = false; + refreshModal(); + await finishValidation(model, model.enableAfterValidation !== false); + refreshModal(); + })); + + body.querySelectorAll('[data-tts-action="validate-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureTtsModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + markSpeechModelValidating(model); + refreshModal(); + await finishValidation(model, true); + refreshModal(); + }); + }); + + body.querySelectorAll('[data-tts-action="toggle-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureTtsModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + const status = getTtsModelDisplayStatus(model); + if (status !== 'available' && status !== 'disabled') { + Toast.warning('只有校验通过的模型才可以启用或禁用'); + return; + } + if (status === 'disabled') { + await API.agentDev.setTTSModelEnabled(model.id, true); + markSpeechModelAvailable(model, true); + if (!state.selectedTtsModelId) state.selectedTtsModelId = model.id; + Toast.success(`TTS 模型「${model.name}」已启用`); + } else { + await API.agentDev.setTTSModelEnabled(model.id, false); + markSpeechModelAvailable(model, false); + if (state.selectedTtsModelId === model.id) state.selectedTtsModelId = ''; + Toast.info(`TTS 模型「${model.name}」已禁用`); + } + refreshModal(); + }); + }); + + body.querySelectorAll('[data-tts-action="delete-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const registry = ensureTtsModelRegistry(); + const model = registry.find(item => item.id === btn.dataset.id); + if (!model) return; + await API.agentDev.deleteTTSModel(model.id); + window.__ttsModelRegistry = registry.filter(item => item.id !== model.id); + if (state.selectedTtsModelId === model.id) state.selectedTtsModelId = ''; + Toast.success(`TTS 模型「${model.name}」已删除`); + refreshModal(); + }); + }); + }; + + const body = modal.overlay.querySelector('.modal-body'); + if (body) { + lucide.createIcons({ elements: [body] }); + bindTtsModelManagerEvents(body); + } +} + +function getTextModelDisplayStatus(model) { + return getSpeechModelDisplayStatus(model); +} + +function getTextStatusMeta(model) { + return TEXT_MODEL_STATUS_META[getTextModelDisplayStatus(model)] || TEXT_MODEL_STATUS_META.failed; +} + +function getTextModelValidationError(model) { + const apiKey = String(model.apiKey || '').trim().toLowerCase(); + const baseUrl = String(model.baseUrl || '').trim().toLowerCase(); + const modelCode = String(model.modelCode || '').trim().toLowerCase(); + if (!apiKey || apiKey.includes('invalid') || apiKey.startsWith('bad') || apiKey === 'sk-invalid') { + return 'Key 无效'; + } + if (!/^https?:\/\//i.test(model.baseUrl || '') || /down|timeout|error|404|500/.test(baseUrl)) { + return '接口异常'; + } + if (!modelCode || /missing|notfound|not-found|none|404/.test(modelCode)) { + return '模型 ID 不存在'; + } + if (!Number.isFinite(Number(model.contextWindow)) || Number(model.contextWindow) < 1024) { + return '上下文长度无效'; + } + return ''; +} + +function renderTextModelManagerContent(showForm = false) { + const models = ensureTextModelRegistry(); + return ` +
+
+ +
文本模型凭证统一维护在这里。创建模型后系统会自动校验 API Key、接口连通性和模型 ID,状态为「可用」且已启用后才会进入智能体的文本模型下拉框。
+
+ + ${showForm ? ` +
+
+
+
新增文本模型配置
+
适用于通义千问、DeepSeek、ChatGPT、豆包或 OpenAI Compatible 接口。
+
+
+
+ + + + + + + + + +
+ +
+ + +
+
+ ` : ` +
+
+
文本模型列表
+
模型创建并自动校验通过后,才会出现在智能体配置下拉框中。
+
+ +
+ `} + +
+
已添加模型
+ ${models.length} 个配置 +
+
+ ${models.length ? models.map(model => { + const meta = getTextStatusMeta(model); + const status = getTextModelDisplayStatus(model); + const canToggle = status === 'available' || status === 'disabled'; + return ` +
+
+
+ ${escapeHtml(model.name)} + ${escapeHtml(meta.label)} +
+
+ Provider:${escapeHtml(model.provider)} + 模型 ID:${escapeHtml(model.modelCode)} + 协议:${escapeHtml(model.protocol || 'openai-compatible')} +
+
+ Base URL:${escapeHtml(model.baseUrl)} + API Key:${escapeHtml(maskSecret(model.apiKey))} + 上下文:${escapeHtml(model.contextWindow || '-')} tokens + 最大输出:${escapeHtml(model.maxOutputTokens || '-')} tokens +
+
+ Secret Key:${escapeHtml(maskSecret(model.secretKey))} + 更新时间:${escapeHtml(model.updatedAt || '-')} + ${model.validatedAt ? `最近校验:${escapeHtml(model.validatedAt)}` : ''} +
+ ${model.errorMessage ? `
失败原因:${escapeHtml(model.errorMessage)}
` : ''} +
+
+ + + +
+
+ `; + }).join('') : ` +
+ + 暂无文本模型 + 创建并自动校验成功后,智能体即可选择该文本模型。 + +
+ `} +
+
+ `; +} + +function collectTextModelForm(body) { + const name = body.querySelector('#textModelName')?.value.trim(); + const provider = body.querySelector('#textProviderName')?.value.trim(); + const modelCode = body.querySelector('#textModelCode')?.value.trim(); + const protocol = body.querySelector('#textProtocol')?.value || 'openai-compatible'; + const baseUrl = body.querySelector('#textBaseUrl')?.value.trim(); + const apiKey = body.querySelector('#textApiKey')?.value.trim(); + const secretKey = body.querySelector('#textSecretKey')?.value.trim(); + const contextWindow = Number.parseInt(body.querySelector('#textContextWindow')?.value || '128000', 10); + const maxOutputTokens = Number.parseInt(body.querySelector('#textMaxOutputTokens')?.value || '4096', 10); + const enableAfterValidation = body.querySelector('#textEnableAfterValidation')?.checked !== false; + if (!name || !provider || !modelCode || !baseUrl || !apiKey) { + Toast.warning('请填写模型名称、Provider、模型 ID、Base URL 和 API Key'); + return null; + } + if (!/^https?:\/\//i.test(baseUrl)) { + Toast.warning('Base URL 需要以 http:// 或 https:// 开头'); + return null; + } + if (!Number.isFinite(contextWindow) || contextWindow < 1024) { + Toast.warning('上下文长度至少为 1024 tokens'); + return null; + } + if (!Number.isFinite(maxOutputTokens) || maxOutputTokens < 256) { + Toast.warning('最大输出 Token 至少为 256'); + return null; + } + return { + id: `text-${Date.now()}`, + name, + provider, + modelCode, + protocol, + baseUrl, + apiKey, + secretKey, + contextWindow, + maxOutputTokens, + enableAfterValidation, + status: 'validating', + enabled: false, + updatedAt: getNowText(), + }; +} + +function validateTextModelConnection(model) { + const validationError = getTextModelValidationError(model); + if (validationError) { + return new Promise(resolve => setTimeout(() => resolve({ success: false, message: validationError }), 750)); + } + const validate = API.agentDev.validateTextModel || API.agentDev.testTextModel; + if (!validate) { + return Promise.resolve({ success: true, data: model }); + } + return validate(model.id, { + provider: model.provider, + modelCode: model.modelCode, + protocol: model.protocol, + baseUrl: model.baseUrl, + contextWindow: model.contextWindow, + maxOutputTokens: model.maxOutputTokens, + }); +} + +function openTextModelManagerModal(state, rerender) { + let showCreateForm = false; + const modal = createModal({ + title: '文本模型管理', + size: 'xl', + showCancel: false, + confirmText: '关闭', + content: renderTextModelManagerContent(showCreateForm), + onConfirm: () => true, + }); + + const refreshModal = () => { + const body = modal.overlay.querySelector('.modal-body'); + if (!body) return; + body.innerHTML = renderTextModelManagerContent(showCreateForm); + lucide.createIcons({ elements: [body] }); + bindTextModelManagerEvents(body); + rerender(); + }; + + const finishValidation = async (model, shouldEnable = true) => { + const result = await validateTextModelConnection(model); + if (result.success === false) { + markSpeechModelFailed(model, result.message || '文本模型连接失败'); + Toast.error(model.errorMessage); + } else { + markSpeechModelAvailable(model, shouldEnable); + if (model.enabled && !state.selectedTextModelId) state.selectedTextModelId = model.id; + Toast.success(`文本模型「${model.name}」校验通过`); + } + }; + + const bindTextModelManagerEvents = (body) => { + body.querySelectorAll('[data-text-action="show-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = true; + refreshModal(); + })); + + body.querySelectorAll('[data-text-action="cancel-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = false; + refreshModal(); + })); + + body.querySelectorAll('[data-text-action="create-new"]').forEach(btn => btn.addEventListener('click', async () => { + const model = collectTextModelForm(body); + if (!model) return; + markSpeechModelValidating(model); + ensureTextModelRegistry().push(model); + await API.agentDev.createTextModel(model); + Toast.info(`文本模型「${model.name}」开始自动校验`, 800); + showCreateForm = false; + refreshModal(); + await finishValidation(model, model.enableAfterValidation !== false); + refreshModal(); + })); + + body.querySelectorAll('[data-text-action="validate-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureTextModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + markSpeechModelValidating(model); + refreshModal(); + await finishValidation(model, true); + refreshModal(); + }); + }); + + body.querySelectorAll('[data-text-action="toggle-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureTextModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + const status = getTextModelDisplayStatus(model); + if (status !== 'available' && status !== 'disabled') { + Toast.warning('只有校验通过的模型才可以启用或禁用'); + return; + } + if (status === 'disabled') { + await API.agentDev.setTextModelEnabled(model.id, true); + markSpeechModelAvailable(model, true); + if (!state.selectedTextModelId) state.selectedTextModelId = model.id; + Toast.success(`文本模型「${model.name}」已启用`); + } else { + await API.agentDev.setTextModelEnabled(model.id, false); + markSpeechModelAvailable(model, false); + if (state.selectedTextModelId === model.id) state.selectedTextModelId = ''; + Toast.info(`文本模型「${model.name}」已禁用`); + } + refreshModal(); + }); + }); + + body.querySelectorAll('[data-text-action="delete-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const registry = ensureTextModelRegistry(); + const model = registry.find(item => item.id === btn.dataset.id); + if (!model) return; + await API.agentDev.deleteTextModel(model.id); + window.__textModelRegistry = registry.filter(item => item.id !== model.id); + if (state.selectedTextModelId === model.id) state.selectedTextModelId = ''; + Toast.success(`文本模型「${model.name}」已删除`); + refreshModal(); + }); + }); + }; + + const body = modal.overlay.querySelector('.modal-body'); + if (body) { + lucide.createIcons({ elements: [body] }); + bindTextModelManagerEvents(body); + } +} + function renderPromptEditor(prompt) { return `
@@ -14054,16 +18422,9 @@ function renderPromptEditor(prompt) { } function renderKnowledgeBaseSection(items) { - return renderSection(`知识库 ${items.length}/10`, ` -
- 已挂载知识库会参与 RAG 检索和答案补充 -
- - -
-
+ return renderSection('知识库', `
- ${items.map(item => ` + ${items.length ? items.map((item, index) => `
@@ -14071,25 +18432,165 @@ function renderKnowledgeBaseSection(items) {
${escapeHtml(item.desc)}
权重 ${escapeHtml(item.weight)}
- +
- `).join('')} + `).join('') : ` +
+ + 暂无挂载知识库 +
+ `} +
+ `, '', ` +
+ +
`); } -function renderWebSearchSection(modes) { - return renderSection('联网搜索', ` -
- ${modes.map(mode => ` - - `).join('')} -
- `); +function getKnowledgeBaseCatalog() { + const catalog = Array.isArray(window.__knowledgeBaseCatalog) && window.__knowledgeBaseCatalog.length + ? window.__knowledgeBaseCatalog + : AGENT_KNOWLEDGE_BASE_CATALOG; + return catalog.map(item => ({ + id: item.id, + name: item.name, + description: item.description || item.desc || '', + docs: item.docs || item.docCount || 0, + size: item.size || '', + typeName: item.typeName || '文档搜索', + status: item.status || 'ready', + tags: Array.isArray(item.tags) ? item.tags : [], + })); +} + +function getKnowledgeBaseStatusLabel(status) { + const map = { + ready: '已就绪', + processing: '处理中', + uploading: '上传中', + error: '异常', + }; + return map[status] || status || '未知'; +} + +function openKnowledgeBasePickerModal(state, rerender) { + const catalog = getKnowledgeBaseCatalog(); + const attached = state.knowledgeBases || []; + const selectedIds = new Set(attached.map(item => item.id || item.name)); + let searchValue = ''; + + const modal = createModal({ + title: '添加知识库', + size: 'xl', + confirmText: '挂载所选', + cancelText: '取消', + content: ` +
+
+
+ + +
+ +
+
+
+ `, + onConfirm: () => { + if (!selectedIds.size) { + Toast.warning('请先选择需要挂载的知识库'); + return false; + } + const previousById = new Map(attached.map(item => [item.id || item.name, item])); + state.knowledgeBases = catalog + .filter(item => selectedIds.has(item.id)) + .map(item => { + const existing = previousById.get(item.id); + return { + id: item.id, + name: item.name, + desc: item.description, + weight: existing?.weight || '1.00', + docs: item.docs, + status: item.status, + }; + }); + Toast.success(`已挂载 ${state.knowledgeBases.length} 个知识库`); + rerender(); + return true; + }, + }); + + const overlay = modal.overlay; + const listEl = overlay.querySelector('#agentKbPickerList'); + const searchInput = overlay.querySelector('#agentKbPickerSearch'); + + function getFilteredCatalog() { + const keyword = searchValue.trim().toLowerCase(); + if (!keyword) return catalog; + return catalog.filter(item => [ + item.name, + item.description, + item.typeName, + ...item.tags, + ].some(value => String(value || '').toLowerCase().includes(keyword))); + } + + function renderList() { + const filtered = getFilteredCatalog(); + listEl.innerHTML = filtered.length ? filtered.map(item => ` + + `).join('') : ` +
+ + 没有找到匹配的知识库 +
+ `; + + listEl.querySelectorAll('input[type="checkbox"]').forEach(input => { + input.addEventListener('change', () => { + if (input.checked) { + selectedIds.add(input.value); + } else { + selectedIds.delete(input.value); + } + renderList(); + renderAgentIcons(overlay); + }); + }); + } + + searchInput?.addEventListener('input', event => { + searchValue = event.target.value; + renderList(); + renderAgentIcons(overlay); + }); + + overlay.querySelector('#agentCreateKbFromPicker')?.addEventListener('click', () => { + modal.close?.(); + navigateTo('kb-create'); + }); + + renderList(); + renderAgentIcons(overlay); } function renderMiniAppSection(title, items, iconName) { @@ -14110,14 +18611,48 @@ function renderMiniAppSection(title, items, iconName) { `); } -function renderMcpSection() { - return renderSection('MCP服务 0', ` -
-
- MCP服务 0 -

智能体可以通过标准化协议(MCP)连接企业内部服务API并发起调用

-
- +function getMcpStatusMeta(service) { + return MCP_SERVICE_STATUS_META[service?.status || 'pending'] || MCP_SERVICE_STATUS_META.pending; +} + +function renderMcpSection(state) { + const library = ensureMcpServiceLibrary(); + const selectedIds = new Set(state.selectedMcpServiceIds || []); + const services = library.filter(item => selectedIds.has(item.id)); + return renderSection('MCP服务', ` +
+ 智能体可以通过标准化协议 MCP 连接企业内部服务 API,并将服务工具暴露给模型调用。 + +
+
+ ${services.length ? services.map(item => { + const meta = getMcpStatusMeta(item); + const tools = Array.isArray(item.tools) ? item.tools.filter(tool => tool.name) : []; + return ` +
+
+
+
${escapeHtml(item.name)} ${escapeHtml(item.namespace || 'default')}
+
${escapeHtml(item.desc)}
+
+ ${escapeHtml(item.transport || 'Streamable HTTP')} + ${tools.length} 个工具 + ${escapeHtml(item.endpoint || '-')} +
+
+ ${escapeHtml(meta.label)} + + +
+ `; + }).join('') : ` +
+ + 暂无已挂载 MCP 服务 + 从 0 开始创建 MCP 服务,连接通过后可挂载到当前智能体。 + +
+ `}
`); } @@ -14141,7 +18676,6 @@ function bindWorkbenchEvents(container, state, rerender) { 'voice-enabled': 'voiceEnabled', 'asr-enabled': 'asrEnabled', 'tts-enabled': 'ttsEnabled', - 'realtime-correction': 'realtimeCorrectionEnabled', 'tts-filter': 'ttsFilterEnabled', 'interrupt-primary': 'interruptPrimaryEnabled', 'interrupt-secondary': 'interruptSecondaryEnabled', @@ -14159,30 +18693,9 @@ function bindWorkbenchEvents(container, state, rerender) { }); }); - container.querySelectorAll('input[name="textModelMode"]').forEach(radio => { - radio.addEventListener('change', () => { - if (!radio.checked) return; - state.selectedTextModelMode = radio.value; - const group = radio.value === 'recommended' ? '推荐模型' : '更多模型'; - const candidates = AGENT_CONFIG_MOCK.understand.textModels.filter(item => item.group === group); - state.selectedTextModelId = candidates[0]?.id || state.selectedTextModelId; - rerender(); - }); - }); - - container.querySelectorAll('input[name="webSearchMode"]').forEach(radio => { - radio.addEventListener('change', () => { - if (!radio.checked) return; - state.searchMode = radio.value; - rerender(); - }); - }); - container.querySelectorAll('[data-action="text-model"]').forEach(select => { select.addEventListener('change', () => { state.selectedTextModelId = select.value; - const current = AGENT_CONFIG_MOCK.understand.textModels.find(item => item.id === select.value); - if (current) state.selectedTextModelMode = current.group === '更多模型' ? 'more' : 'recommended'; rerender(); }); }); @@ -14197,8 +18710,6 @@ function bindWorkbenchEvents(container, state, rerender) { container.querySelectorAll('[data-action="tts-model"]').forEach(select => { select.addEventListener('change', () => { state.selectedTtsModelId = select.value; - const matchedGroup = AGENT_CONFIG_MOCK.voice.ttsGroups.find(group => group.models.some(model => model.id === select.value)); - state.selectedTtsGroupId = matchedGroup?.id || state.selectedTtsGroupId; rerender(); }); }); @@ -14210,10 +18721,81 @@ function bindWorkbenchEvents(container, state, rerender) { }); }); - container.querySelectorAll('[data-action="scene-language"]').forEach(select => { + container.querySelectorAll('[data-action="interaction-language"]').forEach(select => { select.addEventListener('change', () => { - state.selectedSceneLanguage = select.value; - window.__selectedSceneLanguage = select.value; + state.selectedInteractionLanguage = select.value; + window.__selectedInteractionLanguage = select.value; + rerender(); + }); + }); + + container.querySelectorAll('[data-action="delete-variable"]').forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + event.stopImmediatePropagation(); + const variable = (state.variables || []).find(item => item.id === btn.dataset.variableId); + if (!variable) return; + state.variables = (state.variables || []).filter(item => item.id !== variable.id); + Toast.success(variable.name ? `变量 ${variable.name} 已删除` : '变量已删除'); + rerender(); + }); + }); + + container.querySelectorAll('[data-variable-field]').forEach(input => { + input.addEventListener('input', () => { + const variable = (state.variables || []).find(item => item.id === input.dataset.variableId); + if (!variable) return; + variable[input.dataset.variableField] = input.value; + scheduleAgentConfigAutoSave(state); + }); + }); + + container.querySelectorAll('[data-action="remove-kb"]').forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + event.stopImmediatePropagation(); + const index = Number.parseInt(btn.dataset.kbIndex, 10); + const list = state.knowledgeBases || []; + const target = list[index]; + if (!target) return; + state.knowledgeBases = list.filter((_, itemIndex) => itemIndex !== index); + Toast.success(`知识库「${target.name}」已移除`); + rerender(); + }); + }); + + container.querySelectorAll('[data-action="remove-command"]').forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + event.stopImmediatePropagation(); + const commandId = btn.dataset.commandId; + const command = ensureCommandLibrary().find(item => item.id === commandId); + state.selectedCommandIds = (state.selectedCommandIds || []).filter(id => id !== commandId); + Toast.success(command?.name ? `指令「${command.name}」已从当前智能体移除` : '指令已移除'); + rerender(); + }); + }); + + container.querySelectorAll('[data-action="remove-plugin"]').forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + event.stopImmediatePropagation(); + const pluginId = btn.dataset.pluginId; + const plugin = ensurePluginLibrary().find(item => item.id === pluginId); + state.selectedPluginIds = (state.selectedPluginIds || []).filter(id => id !== pluginId); + Toast.success(plugin?.name ? `插件「${plugin.name}」已从当前智能体移除` : '插件已移除'); + rerender(); + }); + }); + + container.querySelectorAll('[data-action="remove-mcp-service"]').forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + event.stopImmediatePropagation(); + const serviceId = btn.dataset.mcpId; + const service = ensureMcpServiceLibrary().find(item => item.id === serviceId); + state.selectedMcpServiceIds = (state.selectedMcpServiceIds || []).filter(id => id !== serviceId); + Toast.success(service?.name ? `MCP 服务「${service.name}」已从当前智能体移除` : 'MCP 服务已移除'); rerender(); }); }); @@ -14231,26 +18813,63 @@ function bindWorkbenchEvents(container, state, rerender) { } if (action === 'publish') { state.app.status = 'published'; + autoSaveAgentConfig(state); Toast.success('应用已提交发布,预计 2-5 分钟生效'); rerender(); return; } if (action === 'run') { - state.preview = 'running'; - updatePreviewOnly(container, state.preview); - Toast.success('运行预览已启动'); + autoSaveAgentConfig(state); + clearAgentRunTimer(state); + const session = getRunSession(state); + const configuredKeys = getRuntimeApiKeysForApp(state); + const activeKeyStillExists = configuredKeys.some(key => key.value === session.apiKey); + if (!activeKeyStillExists) { + session.apiKeyConfirmed = false; + session.apiKey = ''; + session.apiKeyName = ''; + session.apiKeyProvider = ''; + } + session.isRecording = false; + session.elapsedSeconds = 0; + session.latencyMs = null; + session.transcript = ''; + session.aiResponse = ''; + state.preview = state.voiceEnabled + ? (session.apiKeyConfirmed ? 'voice-runtime' : 'voice-auth') + : (session.apiKeyConfirmed ? 'text-runtime' : 'text-auth'); + updatePreviewOnly(container, state); + Toast.success(state.voiceEnabled + ? '配置已自动保存,请配置运行 API Key' + : '配置已自动保存,请配置文字运行 API Key'); return; } if (action === 'add-kb') { - navigateTo('knowledge-base'); + openKnowledgeBasePickerModal(state, rerender); return; } if (action === 'knowledge-weight') { openKnowledgeWeightModal(state, rerender); return; } - if (action === 'scene') { - openSceneTemplateModal(state, rerender); + if (action === 'asr-model-manager') { + openAsrModelManagerModal(state, rerender); + return; + } + if (action === 'tts-model-manager') { + openTtsModelManagerModal(state, rerender); + return; + } + if (action === 'text-model-manager') { + openTextModelManagerModal(state, rerender); + return; + } + if (action === 'add-variable' || action === 'variable') { + state.variables = [ + ...(state.variables || []), + { id: `var-${Date.now()}`, name: '', desc: '' }, + ]; + rerender(); return; } if (action === 'hotwords') { @@ -14269,22 +18888,18 @@ function bindWorkbenchEvents(container, state, rerender) { openPluginConfigModal(state, rerender); return; } - if (action === 'edge-collapse') { - state.edgeAlgorithmExpanded = !state.edgeAlgorithmExpanded; - rerender(); + if (action === 'mcp-config') { + openMcpConfigModal(state, rerender); return; } const actionText = { history: '打开历史记录', api: '打开 API 接入文档', - license: '打开 License 购买页', model: '切换模型', variable: '打开变量配置', setting: '打开设置', remove: '已移除当前项', - mcp: '添加 MCP 服务', 'add-app': '添加应用', - 'correction-upload': '上传词表', }; Toast.info(actionText[action] || '操作已触发'); event.stopPropagation(); @@ -14292,6 +18907,183 @@ function bindWorkbenchEvents(container, state, rerender) { }); } +function bindRuntimePreviewEvents(container, state, refreshPreview) { + const pane = container.querySelector('.bailian-preview-pane'); + if (!pane) return; + const session = getRunSession(state); + + pane.querySelector('[data-preview-action="confirm-runtime-key"]')?.addEventListener('click', () => { + const select = pane.querySelector('[data-preview-field="runtime-key-select"]'); + const keys = getRuntimeApiKeysForApp(state); + const selected = keys.find(item => item.id === select?.value); + if (!selected) { + Toast.warning('请先选择系统设置中已保存的 API Key'); + select?.focus(); + return; + } + session.apiKey = selected.value; + session.apiKeyName = `${selected.name} · ${maskSecret(selected.value)}`; + session.apiKeyProvider = selected.provider; + session.apiKeyConfirmed = true; + session.isRecording = false; + state.preview = state.preview === 'text-auth' || !state.voiceEnabled ? 'text-runtime' : 'voice-runtime'; + Toast.success(state.preview === 'text-runtime' ? 'API Key 已配置,可以开始文字聊天' : 'API Key 已配置,可以开始语音测试'); + refreshPreview(); + }); + + pane.querySelector('[data-preview-action="go-settings-api-keys"]')?.addEventListener('click', () => { + clearAgentRunTimer(state); + session.isRecording = false; + Toast.info('请在系统设置中添加并保存 API Key'); + navigateTo('settings'); + }); + + pane.querySelector('[data-preview-action="change-runtime-key"]')?.addEventListener('click', () => { + clearAgentRunTimer(state); + session.isRecording = false; + session.apiKeyConfirmed = false; + state.preview = state.voiceEnabled ? 'voice-auth' : 'text-auth'; + refreshPreview(); + }); + + pane.querySelector('[data-preview-action="toggle-call"]')?.addEventListener('click', () => { + if (!session.apiKeyConfirmed) { + Toast.warning('请先配置运行 API Key'); + state.preview = state.voiceEnabled ? 'voice-auth' : 'text-auth'; + refreshPreview(); + return; + } + if (session.isRecording) { + stopVoiceRuntimeCall(container, state); + Toast.info('本轮语音通话已结束', 900); + } else { + startVoiceRuntimeCall(container, state); + Toast.info('已开始录音,请开始说话', 900); + } + }); + + pane.querySelector('[data-preview-action="toggle-mic"]')?.addEventListener('click', () => { + session.micMuted = !session.micMuted; + Toast.info(session.micMuted ? '麦克风已关闭' : '麦克风已打开', 800); + if (!syncVoiceRuntimeDom(container, state)) { + refreshPreview(); + } + }); + + pane.querySelector('[data-preview-action="voice-device"]')?.addEventListener('click', () => { + Toast.info('当前为 MacBook Pro 麦克风,后端接入后可读取浏览器音频设备列表', 1200); + }); + + pane.querySelector('[data-preview-action="voice-runtime-settings"]')?.addEventListener('click', () => { + Toast.info('语音体验设置入口,后续可接入采样率、降噪和播放设备配置', 1200); + }); + + pane.querySelector('[data-preview-action="copy-voice-ai"]')?.addEventListener('click', async () => { + const text = session.aiResponse || session.aiDisplayResponse || ''; + if (!text) return; + try { + await navigator.clipboard?.writeText(text); + Toast.success('AI 回复已复制'); + } catch (error) { + Toast.warning('当前浏览器不允许自动复制,请手动复制'); + } + }); + + pane.querySelector('[data-preview-action="regenerate-voice-ai"]')?.addEventListener('click', () => { + const base = session.aiResponse || '我已经准备好继续语音对话。'; + const next = `${base}(重新生成版本)`; + session.aiResponse = next; + session.aiDisplayResponse = ''; + session.lastVoiceAiResponse = next; + Toast.info('正在重新生成语音回复', 800); + revealVoiceReply(container, state, next); + }); + + pane.querySelector('[data-preview-field="runtime-text-input"]')?.addEventListener('input', event => { + session.textInput = event.target.value; + }); + + pane.querySelector('[data-preview-field="runtime-text-input"]')?.addEventListener('keydown', event => { + if (event.key !== 'Enter' || event.shiftKey) return; + event.preventDefault(); + pane.querySelector('[data-preview-action="send-text-runtime"]')?.click(); + }); + + pane.querySelector('[data-preview-action="send-text-runtime"]')?.addEventListener('click', () => { + const input = pane.querySelector('[data-preview-field="runtime-text-input"]'); + const value = input?.value.trim() || ''; + if (!session.apiKeyConfirmed) { + Toast.warning('请先配置运行 API Key'); + state.preview = 'text-auth'; + refreshPreview(); + return; + } + if (!value) { + Toast.warning('请输入测试内容'); + input?.focus(); + return; + } + session.chatMessages = [ + ...(Array.isArray(session.chatMessages) ? session.chatMessages : []), + { role: 'user', content: value }, + ]; + session.textInput = ''; + session.textPending = true; + session.latencyMs = null; + refreshPreview(); + window.setTimeout(() => { + const reply = buildMockTextReply(value); + session.textPending = false; + session.latencyMs = 390 + (value.length * 13) % 260; + const messages = Array.isArray(session.chatMessages) ? session.chatMessages : []; + messages.push({ + role: 'ai', + sourceText: value, + retryCount: 0, + content: reply, + displayContent: '', + }); + session.chatMessages = messages; + const messageIndex = messages.length - 1; + refreshPreview(); + revealTextReply(container, state, messageIndex, reply); + }, 850); + }); + + pane.querySelectorAll('[data-preview-action="copy-ai-message"]').forEach(btn => { + btn.addEventListener('click', async () => { + const index = Number.parseInt(btn.dataset.messageIndex, 10); + const message = (session.chatMessages || [])[index]; + const text = message?.content || ''; + if (!text) return; + try { + await navigator.clipboard?.writeText(text); + Toast.success('AI 回复已复制'); + } catch (error) { + Toast.warning('当前浏览器不允许自动复制,请手动复制'); + } + }); + }); + + pane.querySelectorAll('[data-preview-action="regenerate-ai-message"]').forEach(btn => { + btn.addEventListener('click', () => { + const index = Number.parseInt(btn.dataset.messageIndex, 10); + const message = (session.chatMessages || [])[index]; + if (!message || message.role !== 'ai') return; + const nextRetry = (message.retryCount || 0) + 1; + const sourceText = message.sourceText || '这条消息'; + const reply = buildMockTextReply(sourceText, nextRetry); + message.retryCount = nextRetry; + message.content = reply; + message.displayContent = ''; + session.latencyMs = 420 + (nextRetry * 37) % 180; + Toast.info('正在重新生成回复', 800); + refreshPreview(); + revealTextReply(container, state, index, reply); + }); + }); +} + function updateActiveStepTab(container, stepId) { container.querySelectorAll('.bailian-step-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.step === stepId); @@ -14334,143 +19126,37 @@ function bindStepScrollSpy(container, state) { }); } -function updatePreviewOnly(container, previewState) { +function updatePreviewOnly(container, state) { const preview = container.querySelector('.bailian-preview-pane'); if (!preview) return; const temp = document.createElement('div'); - temp.innerHTML = renderPreviewPane(previewState); + temp.innerHTML = renderPreviewPane(state); preview.replaceWith(temp.firstElementChild); renderAgentIcons(container.querySelector('.bailian-preview-pane')); + bindRuntimePreviewEvents(container, state, () => updatePreviewOnly(container, state)); + const textMessageArea = container.querySelector('.agent-text-message-area'); + if (textMessageArea) { + textMessageArea.scrollTop = textMessageArea.scrollHeight; + } + const voiceScrollArea = container.querySelector('.agent-voice-stage') || container.querySelector('.agent-voice-conversation'); + if (voiceScrollArea) { + voiceScrollArea.scrollTop = voiceScrollArea.scrollHeight; + } } -function bindPromptCounter(container) { +function bindPromptCounter(container, state) { const editor = container.querySelector('#agentPromptEditor'); const counter = container.querySelector('#promptCounter'); if (!editor || !counter) return; const update = () => { counter.textContent = `${editor.value.length}/8000`; + if (state) { + state.prompt = editor.value; + scheduleAgentConfigAutoSave(state); + } }; editor.addEventListener('input', update); - update(); -} - -function openSceneTemplateModal(state, rerender) { - let pendingSceneId = state.selectedSceneId; - let pendingVersionId = state.selectedSceneVersion; - let pendingLanguageId = state.selectedSceneLanguage; - - if (typeof createModal !== 'function') { - Toast.info('场景模板选择器暂不可用'); - return; - } - - const modal = createModal({ - title: '切换场景模板', - size: 'xl', - confirmText: '应用模板', - cancelText: '取消', - content: ` -
-
-
选择场景
-
- ${AGENT_SCENE_TEMPLATES.map(scene => ` - - `).join('')} -
-
-
-
选择版本
-
- ${AGENT_SCENE_VERSIONS.map(version => ` - - `).join('')} -
-
-
-
对话语言 *
-
- ${AGENT_SCENE_LANGUAGES.map(language => ` - - `).join('')} -
-
-
- `, - onConfirm: () => { - if (!pendingLanguageId || !AGENT_SCENE_LANGUAGES.some(language => language.id === pendingLanguageId)) { - Toast.warning('请选择对话语言'); - return false; - } - state.selectedSceneId = pendingSceneId; - state.selectedSceneVersion = pendingVersionId; - state.selectedSceneLanguage = pendingLanguageId; - window.__selectedSceneTemplateId = pendingSceneId; - window.__selectedSceneVersion = pendingVersionId; - window.__selectedSceneLanguage = pendingLanguageId; - const scene = getSelectedScene(state); - const version = getSelectedSceneVersion(state); - const language = getSelectedSceneLanguage(state); - Toast.success(`已切换至「${scene.name} · ${version.name} · ${language.name}」`); - rerender(); - return true; - }, - }); - - const overlay = modal.overlay; - const syncSceneOptions = () => { - overlay.querySelectorAll('.bailian-scene-option').forEach(option => { - option.classList.toggle('selected', option.dataset.sceneId === pendingSceneId); - option.querySelector('input').checked = option.dataset.sceneId === pendingSceneId; - }); - }; - const syncVersionOptions = () => { - overlay.querySelectorAll('.bailian-scene-version-option').forEach(option => { - option.classList.toggle('selected', option.dataset.versionId === pendingVersionId); - option.querySelector('input').checked = option.dataset.versionId === pendingVersionId; - }); - }; - const syncLanguageOptions = () => { - overlay.querySelectorAll('.bailian-scene-language-option').forEach(option => { - option.classList.toggle('selected', option.dataset.languageId === pendingLanguageId); - option.querySelector('input').checked = option.dataset.languageId === pendingLanguageId; - }); - }; - - overlay.querySelectorAll('.bailian-scene-option').forEach(option => { - option.addEventListener('click', () => { - pendingSceneId = option.dataset.sceneId; - syncSceneOptions(); - }); - }); - overlay.querySelectorAll('.bailian-scene-version-option').forEach(option => { - option.addEventListener('click', () => { - pendingVersionId = option.dataset.versionId; - syncVersionOptions(); - }); - }); - overlay.querySelectorAll('.bailian-scene-language-option').forEach(option => { - option.addEventListener('click', () => { - pendingLanguageId = option.dataset.languageId; - syncLanguageOptions(); - }); - }); - renderAgentIcons(overlay); + counter.textContent = `${editor.value.length}/8000`; } function openKnowledgeWeightModal(state, rerender) { @@ -14521,8 +19207,11 @@ function openTtsFilterModal(state, rerender) { { id: 'square', left: '[', right: ']', label: '[ ]' }, { id: 'curly', left: '{', right: '}', label: '{ }' }, ]; - const draftPairs = (state.ttsFilterPairs || []).map(item => ({ ...item })); - const draftSingles = [...(state.ttsFilterSingles || [])]; + const getPairKey = (pair) => `${String(pair.left || '').trim()}__${String(pair.right || '').trim()}`; + const commonPairKeys = new Set(commonPairs.map(getPairKey)); + const savedPairs = (state.ttsFilterPairs || []).map(item => ({ ...item })); + const draftPairs = savedPairs.filter(pair => !commonPairKeys.has(getPairKey(pair))); + const draftSingles = [...new Set((state.ttsFilterSingles || []).map(item => String(item || '').trim()).filter(Boolean))]; const modal = createModal({ title: '语音合成播报过滤设置', @@ -14537,7 +19226,7 @@ function openTtsFilterModal(state, rerender) {
${commonPairs.map(item => `
暂无知识库
点击上方「添加知识库」开始创建您的第一个知识库
-
@@ -16206,10 +21635,10 @@ registerPage('knowledge-base', function(container) { } // ==================== 事件绑定 ==================== - + function bindEvents() { // 添加知识库 - container.querySelector('#addKbBtn')?.addEventListener('click', showCreateKbModal); + container.querySelector('#addKbBtn')?.addEventListener('click', () => navigateTo('kb-create')); // 搜索 container.querySelector('#kbSearchInput')?.addEventListener('input', debounce((e) => { @@ -16219,11 +21648,14 @@ registerPage('knowledge-base', function(container) { renderPage(); }, 300)); - // 模拟加载 - setTimeout(() => { - isLoading = false; - renderPage(); - }, 500); + // 模拟加载,只在首次进入页面时触发一次,避免重渲染打断下拉菜单等交互。 + if (isLoading && !hasStartedInitialLoad) { + hasStartedInitialLoad = true; + setTimeout(() => { + isLoading = false; + renderPage(); + }, 500); + } } /** @@ -16238,7 +21670,7 @@ registerPage('knowledge-base', function(container) { } // ==================== 添加知识库弹窗 ==================== - + /** * 显示创建知识库弹窗 */ @@ -16371,11 +21803,17 @@ registerPage('knowledge-base', function(container) { `; document.body.appendChild(modalOverlay); + document.body.classList.add('modal-open'); lucide.createIcons({ elements: [modalOverlay] }); const close = () => { modalOverlay.classList.remove('open'); - setTimeout(() => modalOverlay.remove(), 200); + setTimeout(() => { + modalOverlay.remove(); + if (!document.querySelector('.modal-overlay')) { + document.body.classList.remove('modal-open'); + } + }, 200); }; // 关闭事件 @@ -16423,7 +21861,7 @@ registerPage('knowledge-base', function(container) { } else { selectedForAdd.add(id); } - + // 更新确认按钮状态 const confirmBtn = document.getElementById('confirmAddKbBtn'); const countBadge = document.getElementById('selectedCountBadge'); @@ -16433,7 +21871,7 @@ registerPage('knowledge-base', function(container) { if (countBadge) { countBadge.textContent = selectedForAdd.size > 0 ? `(${selectedForAdd.size})` : ''; } - + renderKbList(); }; @@ -16442,12 +21880,12 @@ registerPage('knowledge-base', function(container) { Toast.warning('请先选择知识库'); return; } - + const selectedNames = Array.from(selectedForAdd).map(id => { const kb = kbData.find(k => k.id === id); return kb ? kb.name : id; }); - + Toast.success(`已添加 ${selectedForAdd.size} 个知识库到当前应用`); close(); }; @@ -16461,7 +21899,7 @@ registerPage('knowledge-base', function(container) { } // ==================== 全局操作函数 ==================== - + // 筛选 window.__kbFilter = function(filter) { currentFilter = filter; @@ -16520,7 +21958,7 @@ registerPage('knowledge-base', function(container) { window.__kbToggleDropdown = function(id, btn, e) { if (e) e.stopPropagation(); - + const dropdown = btn.closest('.dropdown'); const menu = dropdown.querySelector('.dropdown-menu'); const isOpen = dropdown.classList.contains('open'); @@ -16536,7 +21974,7 @@ registerPage('knowledge-base', function(container) { if (!isOpen) { dropdown.classList.add('open'); menu.style.display = 'block'; - + __kbCloseDropdownHandler = (ev) => { if (!dropdown.contains(ev.target)) { dropdown.classList.remove('open'); @@ -16550,6 +21988,148 @@ registerPage('knowledge-base', function(container) { } }; + function openEditKbModal(id) { + const kb = getKbById(id); + if (!kb) { + Toast.warning('未找到该知识库'); + return; + } + + createModal({ + title: '编辑知识库', + size: 'lg', + content: ` +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + 前端 mock 会立即更新当前列表;后端接入后调用 PUT /api/v1/knowledge-bases/:id。 +
+ `, + confirmText: '保存', + onConfirm: () => { + const name = document.getElementById('kbEditName')?.value.trim(); + if (!name) { + Toast.warning('请输入知识库名称'); + return false; + } + const type = document.getElementById('kbEditType')?.value || kb.type; + const tags = (document.getElementById('kbEditTags')?.value || '') + .split(/[,,]/) + .map(tag => tag.trim()) + .filter(Boolean); + + kb.name = name; + kb.description = document.getElementById('kbEditDesc')?.value.trim() || ''; + kb.type = type; + kb.typeName = getTypeTag(type).label; + kb.status = document.getElementById('kbEditStatus')?.value || kb.status; + kb.tags = tags; + kb.updatedAt = getNowText(); + syncKnowledgeBaseCatalog(); + renderPage(); + Toast.success('知识库信息已更新'); + } + }); + lucide.createIcons(); + } + + function exportKnowledgeBases(ids) { + const items = ids.map(id => getKbById(id)).filter(Boolean); + if (!items.length) { + Toast.warning('请先选择要导出的知识库'); + return; + } + + createModal({ + title: items.length > 1 ? '批量导出知识库' : '导出知识库', + content: ` +
+ +
+ ${items.map(item => escapeHtml(item.name)).join('、')} +
+
+
+ + +
+ +
+ 浏览器会下载到默认下载目录;后端接入后替换为 GET /api/v1/knowledge-bases/:id/export。 +
+ `, + confirmText: '确认导出', + onConfirm: () => { + const format = document.getElementById('kbExportFormat')?.value || 'json'; + const includeStats = document.getElementById('kbExportIncludeStats')?.checked !== false; + const exportRows = items.map(item => ({ + id: item.id, + name: item.name, + description: item.description, + type: item.type, + typeName: item.typeName, + ...(includeStats ? { + docs: item.docs, + size: item.size, + status: item.status, + tags: item.tags, + } : {}), + createdAt: item.createdAt, + updatedAt: item.updatedAt, + })); + const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, ''); + if (format === 'csv') { + const headers = Object.keys(exportRows[0]); + const csv = [ + headers.join(','), + ...exportRows.map(row => headers.map(key => `"${String(Array.isArray(row[key]) ? row[key].join('|') : row[key] ?? '').replace(/"/g, '""')}"`).join(',')), + ].join('\n'); + downloadTextFile(`knowledge-bases-${stamp}.csv`, csv, 'text/csv;charset=utf-8'); + } else { + downloadTextFile(`knowledge-bases-${stamp}.json`, JSON.stringify(exportRows, null, 2)); + } + Toast.success(`已导出 ${items.length} 个知识库`); + } + }); + } + // 操作 window.__kbDetail = function(id) { navigateTo('kb-detail?id=' + id); @@ -16560,7 +22140,7 @@ registerPage('knowledge-base', function(container) { }; window.__kbEdit = function(id) { - Toast.info('编辑知识库:' + id); + openEditKbModal(id); }; /** @@ -16587,6 +22167,8 @@ registerPage('knowledge-base', function(container) { // TODO: 调用 API DELETE /api/v1/knowledge-bases/:id const idx = kbData.findIndex(k => k.id === id); if (idx !== -1) kbData.splice(idx, 1); + selectedRows.delete(id); + syncKnowledgeBaseCatalog(); renderPage(); Toast.success('知识库已删除'); } @@ -16611,6 +22193,12 @@ registerPage('knowledge-base', function(container) { confirmText: '重建', onConfirm: () => { // TODO: 调用 API POST /api/v1/knowledge-bases/:id/rebuild + if (kb) { + kb.status = 'processing'; + kb.updatedAt = getNowText(); + syncKnowledgeBaseCatalog(); + renderPage(); + } Toast.success('索引重建任务已启动'); } }); @@ -16621,8 +22209,7 @@ registerPage('knowledge-base', function(container) { * API: GET /api/v1/knowledge-bases/:id/export */ window.__kbExport = function(id) { - // TODO: 调用 API GET /api/v1/knowledge-bases/:id/export - Toast.info('正在导出知识库...'); + exportKnowledgeBases([id]); }; /** @@ -16630,8 +22217,7 @@ registerPage('knowledge-base', function(container) { * API: GET /api/v1/knowledge-bases/batch/export */ window.__kbBatchExport = function() { - // TODO: 调用 API GET /api/v1/knowledge-bases/batch/export - Toast.info(`正在导出 ${selectedRows.size} 个知识库...`); + exportKnowledgeBases(Array.from(selectedRows)); }; /** @@ -16639,6 +22225,11 @@ registerPage('knowledge-base', function(container) { * API: DELETE /api/v1/knowledge-bases/batch */ window.__kbBatchDelete = function() { + if (selectedRows.size === 0) { + Toast.warning('请先选择要删除的知识库'); + return; + } + const deleteCount = selectedRows.size; createModal({ title: '批量删除', content: ` @@ -16660,14 +22251,17 @@ registerPage('knowledge-base', function(container) { if (idx !== -1) kbData.splice(idx, 1); }); selectedRows.clear(); + syncKnowledgeBaseCatalog(); renderPage(); - Toast.success(`已删除 ${selectedRows.size + 1} 个知识库`); + Toast.success(`已删除 ${deleteCount} 个知识库`); } }); }; // 导出弹窗函数 - window.__showCreateKbModal = showCreateKbModal; + window.__showCreateKbModal = function() { + navigateTo('kb-create'); + }; // ==================== 初始化 ==================== renderPage(); @@ -16679,7 +22273,7 @@ registerPage('knowledge-base', function(container) { /** * TalkingQ Platform - 创建知识库向导页面 * 包含3步流程:基础信息 -> 数据选择 -> 索引设置 - * + * * API预留: * - POST /api/v1/knowledge-bases - 创建知识库 * - POST /api/v1/knowledge-bases/validate-name - 验证名称唯一性 @@ -16702,7 +22296,7 @@ registerPage('kb-create', function(container) { * Response: { id: string, name: string, status: string } */ const API_CREATE = '/api/v1/knowledge-bases'; - + /** * 验证名称唯一性 * POST /api/v1/knowledge-bases/validate-name @@ -16710,7 +22304,7 @@ registerPage('kb-create', function(container) { * Response: { valid: boolean, message?: string } */ const API_VALIDATE_NAME = '/api/v1/knowledge-bases/validate-name'; - + /** * 文件上传 * POST /api/v1/files/upload @@ -16718,14 +22312,14 @@ registerPage('kb-create', function(container) { * Response: { fileId: string, fileName: string, status: string } */ const API_UPLOAD = '/api/v1/files/upload'; - + /** * 获取上传/解析状态 * GET /api/v1/files/status/:fileId * Response: { status: 'uploading'|'processing'|'ready'|'error', progress?: number, error?: string } */ const API_FILE_STATUS = '/api/v1/files/status/:fileId'; - + /** * 获取类目列表 * GET /api/v1/categories @@ -16842,7 +22436,7 @@ registerPage('kb-create', function(container) { ]; // ==================== 工具函数 ==================== - + function escapeHtml(str) { if (!str) return ''; return str.replace(/&/g, '&') @@ -16888,7 +22482,7 @@ registerPage('kb-create', function(container) { } // ==================== 草稿管理 ==================== - + function loadDraft() { try { const draft = localStorage.getItem(DRAFT_KEY); @@ -16922,37 +22516,37 @@ registerPage('kb-create', function(container) { } // ==================== 验证函数 ==================== - + function validateStep1() { const errors = []; - + if (!formData.name.trim()) { errors.push('请输入知识库名称'); } else if (formData.name.length < 2 || formData.name.length > 64) { errors.push('知识库名称长度需在2-64字符之间'); } - + if (!formData.type) { errors.push('请选择知识库类型'); } - + return errors; } function validateStep2() { const errors = []; - + if (formData.sourceType === 'upload' && formData.uploadedFiles.length === 0) { errors.push('请上传至少一个文件'); } - + if (formData.sourceType === 'upload') { const failedFiles = formData.uploadedFiles.filter(f => f.status === 'error'); if (failedFiles.length > 0) { errors.push(`有 ${failedFiles.length} 个文件上传失败,请处理后重试`); } } - + return errors; } @@ -16961,7 +22555,7 @@ registerPage('kb-create', function(container) { } // ==================== 渲染函数 ==================== - + /** * 渲染向导步骤 */ @@ -16992,7 +22586,7 @@ registerPage('kb-create', function(container) { 基础知识信息 ? - +
@@ -559,71 +636,42 @@ function renderVoiceStep(state) { function renderUnderstandStep(state) { const data = AGENT_CONFIG_MOCK.understand; - const selectedMode = state.selectedTextModelMode || 'recommended'; - const selectedModelId = state.selectedTextModelId || data.textModels[0]?.id || ''; - const selectedModel = data.textModels.find(item => item.id === selectedModelId) - || data.textModels.find(item => item.group === '推荐模型') - || data.textModels[0]; + const textModels = getAvailableTextModels(); + if (state.selectedTextModelId && !textModels.some(model => model.id === state.selectedTextModelId)) { + state.selectedTextModelId = ''; + } + if (!state.selectedTextModelId && textModels.length) { + state.selectedTextModelId = textModels[0].id; + } + const selectedModel = textModels.find(item => item.id === state.selectedTextModelId) || textModels[0] || null; return `
- ${renderSection('意图识别', ` -
-
-
开启意图识别
-
${escapeHtml(data.intentModel.desc)}
-
- -
-
关闭将不再触发调用工具/Agent 等基于语义理解的交互功能,适用于低成本、轻量级语音交互场景。
+ ${renderSection('意图识别', '', `${data.intentModel.desc} 关闭将不再触发调用工具/Agent 等基于语义理解的交互功能,适用于低成本、轻量级语音交互场景。`, ` + `)} ${renderSection('文本模型', `
模型选择
-
建议使用推荐模型,为用户提供流畅的对话体验
+
模型凭证统一在文本模型管理中维护,校验通过并启用后才可选择
- 推荐 / 更多 +
-
- - -
- + ${textModels.length ? textModels.map(item => ` + + `).join('') : ''} -
${escapeHtml(selectedModel.desc)}
+ ${selectedModel ? `
${escapeHtml(selectedModel.desc)}
` : ''} `)} - ${renderPromptEditor(data.prompt)} + ${renderPromptEditor(state.prompt || data.prompt)} ${renderSection('对话变量', ` -
- 支持在对话输入中添加变量,为模型提供用户语音之外的更多信息,如天气、周围环境等。 - -
-
- ${data.variables.map(item => ` -
-
${escapeHtml(item.name)}
-
${escapeHtml(item.desc)}
-
- `).join('')} - -
+ ${renderVariableSimpleEditor(state)} + `, '变量是智能体运行时的状态存储,可用于注入 Prompt、节点传递、流程控制和上下文记忆。', ` + `)} ${renderSection('上下文轮数', `
@@ -633,56 +681,23 @@ function renderUnderstandStep(state) {
`)} ${renderKnowledgeBaseSection(state.knowledgeBases || data.knowledgeBases)} - ${renderSection('对话承接语', ` -
-
-
开启对话承接语
-
大模型基于用户输入自动生成承接语,减少回复等待时间
-
- -
+ ${renderSection('对话承接语', '', '大模型基于用户输入自动生成承接语,减少回复等待时间', ` + `)} - ${renderSection('互联网搜索', ` -
-
-
开启互联网搜索
-
关闭后不展示搜索模式,仅保留本地知识和模型能力
-
- -
- ${state.knowledgeSearchEnabled !== false ? ` -
- - -
- ` : ''} + ${renderSection('互联网搜索', '', '关闭后不展示搜索模式,仅保留本地知识和模型能力', ` + `)} - ${renderSection('长期记忆', ` -
-
-
开启长期记忆
-
基于对话历史形成专属记忆库,并在后续对话中体现
-
- -
-
开启长期记忆后,结合记忆内容进行回复的对话会增加额外耗时。
+ ${renderSection('长期记忆', '', '基于对话历史形成专属记忆库,并在后续对话中体现。开启长期记忆后,结合记忆内容进行回复的对话会增加额外耗时。', ` + `)}
`; @@ -690,104 +705,93 @@ function renderUnderstandStep(state) { function renderSkillsStep(state) { const data = AGENT_CONFIG_MOCK.skills; - const commands = [...data.commands, ...(state.customCommands || [])]; - const plugins = [...data.plugins, ...(state.customPlugins || [])]; + const commandLibrary = ensureCommandLibrary(); + const selectedCommandIds = new Set(state.selectedCommandIds || []); + const commands = commandLibrary.filter(item => selectedCommandIds.has(item.id)); + const pluginLibrary = ensurePluginLibrary(); + const selectedPluginIds = new Set(state.selectedPluginIds || []); + const plugins = pluginLibrary.filter(item => selectedPluginIds.has(item.id)); return `
${renderSection('指令', `
- 当前已选择的指令可以编辑,右侧加号可添加新的指令类型 + 当前智能体只会调用已挂载的指令。指令创建后会保存在指令库中,可重复选择。
- ${commands.map(item => ` + ${commands.length ? commands.map(item => `
-
+
-
${escapeHtml(item.name)} ${item.count || 1} 指令
+
${escapeHtml(item.name)} ${escapeHtml(item.commandType || '自定义指令')}
${escapeHtml(item.desc)}
- - + +
- `).join('')} + `).join('') : ` +
+ + 暂无已挂载指令 + 从 0 开始创建指令,创建后可在指令库中重复选择。 + +
+ `}
`)} ${renderSection('插件', `
- 插件能力默认由系统执行,也可以独立配置启用状态 - + 当前智能体只会调用已挂载的插件。插件创建后会保存在插件库中,可重复选择。 +
- ${plugins.map(item => ` + ${plugins.length ? plugins.map(item => `
-
+
-
${escapeHtml(item.name)}
+
${escapeHtml(item.name)} ${escapeHtml(item.pluginType || '自定义插件')}
${escapeHtml(item.desc)}
${escapeHtml(item.status || '系统执行')} - - + +
- `).join('')} + `).join('') : ` +
+ + 暂无已挂载插件 + 从 0 开始创建插件,创建后可在插件库中重复选择。 + +
+ `}
`)} - ${renderMiniAppSection('百炼应用', data.bailianApps, 'boxes')} - ${renderMiniAppSection('三方应用', data.thirdPartyApps, 'plug')} - ${renderMcpSection()} - ${renderSection('Planning / Function Calling', ` - ${renderSwitchRow('开启 Planning', '让智能体先规划步骤,再调用函数或插件完成复杂任务', true)} - ${renderSwitchRow('开启 Function Calling', '允许模型按照结构化参数调用已授权函数', true)} - `)} + ${renderMcpSection(state)}
`; } -function renderAdvancedStep() { - return ` -
- ${renderSection('高级设置', ` - ${renderSwitchRow('儿童安全模式', '强制启用儿童内容安全策略、危险行为拒答和敏感信息保护', true)} - ${renderSwitchRow('链路追踪', '记录 ASR、LLM、RAG、TTS 的耗时和调用链路', true)} - ${renderSwitchRow('自动降级', '主模型异常时自动切换到备用模型,保证设备可用性', true)} - `)} - ${renderSection('调用预算', ` -
- - -
- `)} - ${renderSection('发布策略', ` -
- ${renderRadio('release', 'gray', '灰度发布 10%', true)} - ${renderRadio('release', 'full', '全量发布')} - ${renderRadio('release', 'schedule', '定时发布')} -
- `)} -
- `; -} - -function renderPreviewPane(previewState) { - const title = previewState === 'running' ? '预览环境已准备好' : '请填好配置后点击运行'; - const desc = previewState === 'running' - ? '当前智能体已加载最新 mock 配置,可继续调整参数后再次运行。' - : '当前应用测试会产生实际调用,按对话次数计费'; +function renderPreviewPane(state) { + if (state.preview === 'voice-auth') { + return renderRuntimeAuthPreview(state, 'voice'); + } + if (state.preview === 'text-auth') { + return renderRuntimeAuthPreview(state, 'text'); + } + if (state.preview === 'voice-runtime') { + return renderVoiceRuntimePreview(state); + } + if (state.preview === 'text-runtime') { + return renderTextRuntimePreview(state); + } return `
-

${title}

-

${desc}

+

请填好配置后点击运行

+

当前应用测试会产生实际调用,按对话次数计费

建议佩戴耳机,并在安静环境下测试,对话体验更佳。 @@ -797,28 +801,563 @@ function renderPreviewPane(previewState) { `; } -function renderSceneCard(scene, version, language) { +function renderRuntimeAuthPreview(state, mode = 'voice') { + const savedKeys = getRuntimeApiKeysForApp(state); + const hasKeys = savedKeys.length > 0; + const isVoice = mode === 'voice'; return ` - +
+
+
+
+ ${isVoice ? 'Voice Runtime' : 'Text Runtime'} +

配置运行 API Key

+

运行前已自动保存当前配置。请选择系统设置里已保存的 API Key 后进入${isVoice ? '语音测试' : '文字聊天测试'}。

+
+ 配置已保存 +
+
+ +
+ + ${hasKeys ? '' : ` + + `} +
+
+
+ + 运行面板只引用系统设置中已保存的 API Key。后端接入后,这里应只传递 Key ID,由后端安全读取密钥并创建运行会话。 +
+
+
`; } -function renderSection(title, content) { +function renderVoiceRuntimePreview(state) { + const session = getRunSession(state); + const latency = session.latencyMs == null ? '3377ms' : `${session.latencyMs}ms`; + const recordText = session.isRecording ? formatRuntimeDuration(session.elapsedSeconds) : '00:00'; + const transcript = session.transcript || '正在监听,请开始说话...'; + const aiResponse = session.aiDisplayResponse !== undefined ? session.aiDisplayResponse : (session.aiResponse || ''); + const userName = getAgentRuntimeUserName(); + const hasVoiceConversation = session.isRecording || Boolean(session.transcript || session.aiResponse || session.aiDisplayResponse); return ` -
-

${escapeHtml(title)}

- ${content} +
+
+
+
+

语音交互体验

+ +
+
+ ${escapeHtml(userName)} + + +
+
+ +
+ ${hasVoiceConversation ? ` +
+
+
+
${escapeHtml(transcript)}
+
+
+
+
+ ${aiResponse ? escapeHtml(aiResponse) : ''} +
+
+ + +
+
+
+
+
+
端到端首包延迟:${escapeHtml(latency)}
+ ` : ` +
+ +

${escapeHtml(state.app?.name || 'TalkingQ Agent')}

+

建议佩戴耳机,并在安静环境下测试,对话体验更佳。

+
+ `} +
+ +
+ ${session.isRecording ? ` +
+
${escapeHtml(recordText)}
+
+ +
+ + +
+ ` : ` + + + `} +
+ ${session.micMuted ? '
麦克风已关闭,当前暂停收音。
' : ''} +
+
+ `; +} + +function renderTextRuntimePreview(state) { + const session = getRunSession(state); + const messages = Array.isArray(session.chatMessages) ? session.chatMessages : []; + const userName = getAgentRuntimeUserName(); + return ` +
+
+
+
+

文本交互体验

+ +
+
+ ${escapeHtml(userName)} + + +
+
+
+
+ ${messages.length ? messages.map((message, index) => renderTextChatMessage(message, index)).join('') : ` +
+ 开始输入第一条测试消息 +
+ `} + ${session.textPending ? ` +
+
+
+
+
+ ` : ''} + ${session.latencyMs ? ` +
端到端 ${escapeHtml(String(session.latencyMs))}ms
+ ` : ''} +
+
+
+
+ + +
+
+
+
+ `; +} + +function renderTextChatMessage(message, index) { + if (message.role === 'user') { + return ` +
+
${escapeHtml(message.content)}
+
+ `; + } + const content = message.displayContent !== undefined ? message.displayContent : message.content; + return ` +
+
+
${escapeHtml(content || '')}
+ ${message.content ? ` +
+ + +
+ ` : ''} +
+
+ `; +} + +function getRunSession(state) { + if (!state.runSession) { + state.runSession = {}; + } + return state.runSession; +} + +function getRuntimeApiKeysForApp(state) { + const settings = typeof loadSettings === 'function' + ? loadSettings() + : (window.__talkingqSettings || {}); + if (Array.isArray(settings.apiKeyRecords)) { + const records = settings.apiKeyRecords + .filter(record => String(record.value || '').trim()) + .map(record => ({ + id: record.id, + name: record.name || 'API Key', + provider: record.provider || record.providerId || 'Custom', + value: record.value, + })); + if (records.length) return records; + } + const apiKeys = settings.apiKeys || {}; + return [ + { id: 'openai', name: 'OpenAI API Key', provider: 'OpenAI', value: apiKeys.openai || '' }, + { id: 'ali', name: '阿里云百炼 API Key', provider: 'Alibaba Cloud Bailian', value: apiKeys.ali || '' }, + { id: 'gemini', name: 'Google Gemini API Key', provider: 'Google Gemini', value: apiKeys.gemini || '' }, + ].filter(item => String(item.value || '').trim()); +} + +function formatRuntimeDuration(seconds) { + const safe = Math.max(0, Number(seconds) || 0); + const min = Math.floor(safe / 60); + const sec = safe % 60; + return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; +} + +function getAgentRunTimerKey(state) { + return state.app?.id || 'default'; +} + +function clearAgentRunTimer(state) { + const timers = ensureAgentRunTimers(); + const key = getAgentRunTimerKey(state); + if (timers[key]) { + window.clearInterval(timers[key]); + delete timers[key]; + } +} + +function buildAgentConfigSnapshot(state) { + return { + appId: state.app?.id, + appName: state.app?.name, + savedAt: state.lastSavedAt, + voiceEnabled: state.voiceEnabled, + asrEnabled: state.asrEnabled, + ttsEnabled: state.ttsEnabled, + selectedInteractionLanguage: state.selectedInteractionLanguage, + selectedAsrModelId: state.selectedAsrModelId, + selectedTtsModelId: state.selectedTtsModelId, + selectedTextModelId: state.selectedTextModelId, + selectedHotwordGroupId: state.selectedHotwordGroupId, + ttsFilterEnabled: state.ttsFilterEnabled, + interruptPrimaryEnabled: state.interruptPrimaryEnabled, + interruptSecondaryEnabled: state.interruptSecondaryEnabled, + semanticStopEnabled: state.semanticStopEnabled, + intentEnabled: state.intentEnabled, + dialogueContinuationEnabled: state.dialogueContinuationEnabled, + knowledgeSearchEnabled: state.knowledgeSearchEnabled, + longTermMemoryEnabled: state.longTermMemoryEnabled, + prompt: state.prompt, + variables: state.variables, + knowledgeBases: state.knowledgeBases, + selectedCommandIds: state.selectedCommandIds, + selectedPluginIds: state.selectedPluginIds, + selectedMcpServiceIds: state.selectedMcpServiceIds, + }; +} + +function updateAgentAutosaveIndicator(state) { + const indicator = document.querySelector('#agentAutosaveStatus'); + if (!indicator) return; + indicator.classList.toggle('saving', state.saveStatus === 'saving'); + indicator.textContent = state.saveStatus === 'saving' + ? '自动保存中' + : state.lastSavedAt ? `已自动保存 ${getTimeOnly(state.lastSavedAt)}` : '等待自动保存'; +} + +function autoSaveAgentConfig(state) { + state.saveStatus = 'saved'; + state.lastSavedAt = getNowText(); + const store = ensureAgentConfigAutosaves(); + store[state.app?.id || 'default'] = buildAgentConfigSnapshot(state); + updateAgentAutosaveIndicator(state); +} + +function scheduleAgentConfigAutoSave(state) { + if (state.__saveTimer) { + window.clearTimeout(state.__saveTimer); + } + state.saveStatus = 'saving'; + updateAgentAutosaveIndicator(state); + state.__saveTimer = window.setTimeout(() => { + autoSaveAgentConfig(state); + state.__saveTimer = null; + }, 300); +} + +function getMockTranscriptForSecond(second) { + const fragments = [ + '你好小Q', + '你好小Q,今天可以给我讲一个太空故事吗', + '你好小Q,今天可以给我讲一个太空故事吗,里面要有机器人和小朋友', + '你好小Q,今天可以给我讲一个太空故事吗,里面要有机器人和小朋友,最后要开心一点', + ]; + if (second <= 1) return fragments[0]; + if (second <= 2) return fragments[1]; + if (second <= 4) return fragments[2]; + return fragments[3]; +} + +function getMockAiResponseForSecond(second) { + if (second < 2) return ''; + if (second < 4) return '可以呀,我正在准备一个温柔的太空小故事。'; + return '当然可以。小朋友坐上银色小飞船,和机器人小Q一起飞向月亮。他们发现一颗会唱歌的小星星,最后把星光带回家,送给每一个睡前还想听故事的小朋友。'; +} + +function getAgentRuntimeUserName() { + return window.__currentUser?.displayName + || window.__currentUser?.name + || window.currentUser?.displayName + || window.currentUser?.name + || 'Admin'; +} + +function syncVoiceRuntimeDom(container, state) { + const panel = container.querySelector('.agent-voice-panel.recording'); + if (!panel || state.preview !== 'voice-runtime') return false; + const session = getRunSession(state); + const latency = session.latencyMs == null ? '3377ms' : `${session.latencyMs}ms`; + const recordText = session.isRecording ? formatRuntimeDuration(session.elapsedSeconds) : '00:00'; + const transcript = session.transcript || '正在监听,请开始说话...'; + const aiResponse = session.aiDisplayResponse !== undefined ? session.aiDisplayResponse : (session.aiResponse || ''); + + const transcriptNode = panel.querySelector('[data-voice-runtime="transcript"]'); + if (transcriptNode) transcriptNode.textContent = transcript; + + const aiContentNode = panel.querySelector('[data-voice-runtime="ai-content"]'); + if (aiContentNode) { + if (aiResponse) { + aiContentNode.textContent = aiResponse; + } else if (!aiContentNode.querySelector('.agent-runtime-typing')) { + aiContentNode.innerHTML = ''; + } + } + + const actionsNode = panel.querySelector('[data-voice-runtime="ai-actions"]'); + if (actionsNode) { + actionsNode.classList.toggle('is-visible', Boolean(session.aiResponse)); + actionsNode.setAttribute('aria-hidden', session.aiResponse ? 'false' : 'true'); + } + + const latencyNode = panel.querySelector('[data-voice-runtime="latency"]'); + if (latencyNode) latencyNode.textContent = `端到端首包延迟:${latency}`; + + const recordNode = panel.querySelector('[data-voice-runtime="record-time"]'); + if (recordNode) recordNode.textContent = recordText; + + const waveNode = panel.querySelector('[data-voice-runtime="wave"]'); + if (waveNode) waveNode.classList.toggle('muted', Boolean(session.micMuted)); + + const micButton = panel.querySelector('[data-voice-runtime="mic-button"]'); + if (micButton) { + micButton.classList.toggle('muted', Boolean(session.micMuted)); + micButton.setAttribute('aria-label', session.micMuted ? '打开麦克风' : '关闭麦克风'); + const iconName = session.micMuted ? 'mic-off' : 'mic'; + const icon = micButton.querySelector('i, svg'); + if (icon?.getAttribute('data-lucide') !== iconName) { + micButton.innerHTML = ``; + renderAgentIcons(micButton); + } + } + + const voiceScrollArea = panel.querySelector('.agent-voice-stage') || panel.querySelector('.agent-voice-conversation'); + if (voiceScrollArea) { + voiceScrollArea.scrollTop = voiceScrollArea.scrollHeight; + } + + return true; +} + +function revealVoiceReply(container, state, fullText) { + const session = getRunSession(state); + let cursor = 0; + const step = Math.max(1, Math.ceil(fullText.length / 36)); + if (session.__voiceRevealTimer) { + window.clearInterval(session.__voiceRevealTimer); + } + session.__voiceRevealTimer = window.setInterval(() => { + cursor = Math.min(fullText.length, cursor + step); + session.aiDisplayResponse = fullText.slice(0, cursor); + if (!syncVoiceRuntimeDom(container, state)) { + updatePreviewOnly(container, state); + } + if (cursor >= fullText.length) { + session.aiDisplayResponse = fullText; + window.clearInterval(session.__voiceRevealTimer); + session.__voiceRevealTimer = null; + if (!syncVoiceRuntimeDom(container, state)) { + updatePreviewOnly(container, state); + } + } + }, 38); +} + +function buildMockTextReply(value, retryCount = 0) { + const suffix = retryCount ? `这是第 ${retryCount + 1} 版重新生成结果。` : '这是当前前端 mock 回复效果。'; + return `我收到了:“${value}”。${suffix} 后端接入后,这里会返回真实模型的流式回复。`; +} + +function revealTextReply(container, state, messageIndex, fullText) { + const session = getRunSession(state); + let cursor = 0; + const step = Math.max(1, Math.ceil(fullText.length / 34)); + const timer = window.setInterval(() => { + const messages = Array.isArray(session.chatMessages) ? session.chatMessages : []; + const message = messages[messageIndex]; + if (!message) { + window.clearInterval(timer); + return; + } + cursor = Math.min(fullText.length, cursor + step); + message.displayContent = fullText.slice(0, cursor); + updatePreviewOnly(container, state); + if (cursor >= fullText.length) { + message.displayContent = fullText; + window.clearInterval(timer); + updatePreviewOnly(container, state); + } + }, 38); +} + +function startVoiceRuntimeCall(container, state) { + const session = getRunSession(state); + clearAgentRunTimer(state); + if (session.__voiceRevealTimer) { + window.clearInterval(session.__voiceRevealTimer); + session.__voiceRevealTimer = null; + } + session.isRecording = true; + session.micMuted = false; + session.elapsedSeconds = 0; + session.latencyMs = 3377; + session.transcript = '正在监听,请开始说话...'; + session.aiResponse = ''; + session.aiDisplayResponse = ''; + session.lastVoiceAiResponse = ''; + session.startedAt = Date.now(); + updatePreviewOnly(container, state); + const timers = ensureAgentRunTimers(); + timers[getAgentRunTimerKey(state)] = window.setInterval(() => { + if (!session.isRecording) { + clearAgentRunTimer(state); + return; + } + session.elapsedSeconds = Math.floor((Date.now() - session.startedAt) / 1000); + if (!session.micMuted) { + const nextTranscript = getMockTranscriptForSecond(session.elapsedSeconds); + const nextResponse = getMockAiResponseForSecond(session.elapsedSeconds); + session.transcript = nextTranscript; + session.latencyMs = 3377 + ((session.elapsedSeconds * 83) % 420); + if (nextResponse && nextResponse !== session.lastVoiceAiResponse) { + session.aiResponse = nextResponse; + session.aiDisplayResponse = ''; + session.lastVoiceAiResponse = nextResponse; + revealVoiceReply(container, state, nextResponse); + return; + } + } + if (!syncVoiceRuntimeDom(container, state)) { + updatePreviewOnly(container, state); + } + }, 1000); +} + +function stopVoiceRuntimeCall(container, state) { + const session = getRunSession(state); + clearAgentRunTimer(state); + session.isRecording = false; + session.elapsedSeconds = Math.max(session.elapsedSeconds || 0, Math.floor((Date.now() - (session.startedAt || Date.now())) / 1000)); + session.latencyMs = session.latencyMs || 3377; + session.transcript = session.transcript || '本轮没有检测到有效语音。'; + session.aiResponse = session.aiResponse || '我已经结束本轮收音,可以再次点击电话按钮开始新的语音测试。'; + session.aiDisplayResponse = session.aiDisplayResponse || session.aiResponse; + updatePreviewOnly(container, state); +} + +function renderSection(title, content, tooltip = '', headerAction = '') { + const hasContent = Boolean(String(content || '').trim()); + const titleInfo = tooltip ? ` + + ` : ''; + const titleMarkup = ` +

+ ${escapeHtml(title)} + ${titleInfo} +

+ `; + return ` +
+ ${headerAction ? ` +
+ ${titleMarkup} + ${headerAction} +
+ ` : titleMarkup} + ${hasContent ? content : ''}
`; } @@ -845,6 +1384,9 @@ function renderFeatureSelect(config) { options = [], groupedOptions = null, hint = '', + emptyText = '暂无可选模型', + manageAction = '', + manageText = '', } = config; const fieldAction = toggleKey === 'asr-enabled' ? 'asr-model' : toggleKey === 'tts-enabled' ? 'tts-model' @@ -862,8 +1404,8 @@ function renderFeatureSelect(config) {
- + ${options.length === 0 ? `` : groupedOptions ? groupedOptions.map(group => ` ${group.models.map(model => ` @@ -874,21 +1416,12 @@ function renderFeatureSelect(config) { `).join('')} ${selectedOption ? `
${escapeHtml(selectedOption.desc)}
` : ''} -
- `; -} - -function renderSwitchRow(label, desc, checked) { - return ` -
-
-
${escapeHtml(label)}
-
${escapeHtml(desc)}
-
- + ${manageAction ? ` + + ` : ''}
`; } @@ -905,29 +1438,1147 @@ function renderRadio(name, value, label, checked = false) { function renderTtsFilterSummary(state) { if (!state.ttsFilterEnabled) return ''; - const commonPairs = (state.ttsFilterPairs || []) - .filter(item => item.enabled !== false) - .map(item => `${item.left} ${item.right}`.trim()); - const singles = state.ttsFilterSingles || []; + const pairKeys = new Set(); + const pairs = (state.ttsFilterPairs || []).reduce((list, item) => { + if (item.enabled === false) return list; + const left = String(item.left || '').trim(); + const right = String(item.right || '').trim(); + if (!left && !right) return list; + const key = `${left}__${right}`; + if (pairKeys.has(key)) return list; + pairKeys.add(key); + list.push(`${left} ${right}`.trim()); + return list; + }, []); + const singles = [...new Set((state.ttsFilterSingles || []).map(item => String(item || '').trim()).filter(Boolean))]; + const tokens = [...pairs, ...singles.map(item => `字符 ${item}`)]; + const visibleTokens = tokens.slice(0, 5); + const hiddenCount = Math.max(tokens.length - visibleTokens.length, 0); return `
-
-
常用过滤符号对
-
- ${commonPairs.length ? commonPairs.map(item => `${escapeHtml(item)}`).join('') : '暂无'} +
+
播报过滤规则
+
${pairs.length} 组符号对 · ${singles.length} 个单字符
+
+ ${visibleTokens.length ? visibleTokens.map(item => `${escapeHtml(item)}`).join('') : '暂无规则'} + ${hiddenCount ? `+${hiddenCount}` : ''}
-
-
自定义过滤符号对与单字符
-
- ${singles.length ? singles.map(item => `${escapeHtml(item)}`).join('') : '暂无'} -
- + +
+ `; +} + +function renderVariableSimpleEditor(state) { + const variables = state.variables || []; + return ` +
+
+ 变量名 + 描述 + +
+
+ ${variables.length ? variables.map(item => ` +
+ + + +
+ `).join('') : ` +
+ 暂无变量,点击右上角添加。 +
+ `}
`; } +function getSpeechModelDisplayStatus(model) { + if (!model) return 'failed'; + if (model.status === 'connected') return 'available'; + if (model.status === 'unconnected') return 'failed'; + if (model.status === 'available' && model.enabled === false) return 'disabled'; + return model.status || 'failed'; +} + +function getAsrModelDisplayStatus(model) { + return getSpeechModelDisplayStatus(model); +} + +function getTtsModelDisplayStatus(model) { + return getSpeechModelDisplayStatus(model); +} + +function getAsrStatusMeta(model) { + return ASR_MODEL_STATUS_META[getAsrModelDisplayStatus(model)] || ASR_MODEL_STATUS_META.failed; +} + +function getTtsStatusMeta(model) { + return TTS_MODEL_STATUS_META[getTtsModelDisplayStatus(model)] || TTS_MODEL_STATUS_META.failed; +} + +function maskSecret(value) { + if (!value) return '未填写'; + return `${String(value).slice(0, 4)}••••••••${String(value).slice(-3)}`; +} + +function getLanguageTypeName(languageId) { + return AGENT_INTERACTION_LANGUAGES.find(item => item.id === languageId)?.name || languageId || '多语言'; +} + +function getTtsVoiceIds(model) { + if (Array.isArray(model?.voiceIds)) { + return model.voiceIds.filter(Boolean); + } + return model?.voiceId ? [model.voiceId] : []; +} + +function getTtsVoiceSummary(model) { + const voiceIds = getTtsVoiceIds(model); + if (!voiceIds.length) return '未配置 Voice'; + if (voiceIds.length === 1) return `Voice ${voiceIds[0]}`; + return `${voiceIds.length} 个 Voice`; +} + +function parseTtsVoiceIds(value) { + return [...new Set(String(value || '') + .split(/[\n,,;;\s]+/) + .map(item => item.trim()) + .filter(Boolean))]; +} + +function getSpeechModelValidationError(model, type) { + const apiKey = String(model.apiKey || '').trim().toLowerCase(); + const baseUrl = String(model.baseUrl || '').trim().toLowerCase(); + const voiceIds = getTtsVoiceIds(model).map(item => String(item).trim().toLowerCase()); + if (!apiKey || apiKey.includes('invalid') || apiKey.startsWith('bad') || apiKey === 'sk-invalid') { + return 'Key 无效'; + } + if (!/^https?:\/\//i.test(model.baseUrl || '') || /down|timeout|error|404|500/.test(baseUrl)) { + return '接口异常'; + } + if (type === 'TTS' && voiceIds.some(voiceId => /missing|notfound|not-found|none/.test(voiceId))) { + return 'Voice 不存在'; + } + if (type === 'TTS' && !TTS_AUDIO_FORMAT_OPTIONS.some(item => item.id === model.audioFormat)) { + return '返回格式错误'; + } + return ''; +} + +function markSpeechModelValidating(model) { + model.status = 'validating'; + model.enabled = false; + model.errorMessage = ''; + model.updatedAt = getNowText(); +} + +function markSpeechModelAvailable(model, shouldEnable = true) { + model.status = shouldEnable ? 'available' : 'disabled'; + model.enabled = Boolean(shouldEnable); + model.errorMessage = ''; + model.validatedAt = getNowText(); + model.updatedAt = getNowText(); +} + +function markSpeechModelFailed(model, message) { + model.status = 'failed'; + model.enabled = false; + model.errorMessage = message || '连接校验失败'; + model.updatedAt = getNowText(); +} + +function renderAsrModelManagerContent(showForm = false) { + const models = ensureAsrModelRegistry(); + return ` +
+
+ +
ASR 模型凭证统一维护在这里。创建后系统会自动校验接口连通性,只有状态为「可用」且已启用的模型才会进入智能体的语音识别下拉框。
+
+ + ${showForm ? ` +
+
+
+
新增 ASR 模型配置
+
研发可先登记模型名称、Provider、Base URL 与密钥;创建后自动开始后台校验。
+
+
+
+ + + + + +
+ +
+ + +
+
+ ` : ` +
+
+
ASR 模型列表
+
模型创建并校验通过后,才会出现在智能体配置下拉框中。
+
+ +
+ `} + +
+
已添加模型
+ ${models.length} 个配置 +
+
+ ${models.length ? models.map(model => { + const meta = getAsrStatusMeta(model); + const status = getAsrModelDisplayStatus(model); + const canToggle = status === 'available' || status === 'disabled'; + return ` +
+
+
+ ${escapeHtml(model.name)} + ${escapeHtml(meta.label)} +
+
+ Provider:${escapeHtml(model.provider)} + Base URL:${escapeHtml(model.baseUrl)} + API Key:${escapeHtml(maskSecret(model.apiKey))} +
+
+ Secret Key:${escapeHtml(maskSecret(model.secretKey))} + 更新时间:${escapeHtml(model.updatedAt || '-')} + ${model.validatedAt ? `最近校验:${escapeHtml(model.validatedAt)}` : ''} +
+ ${model.errorMessage ? `
失败原因:${escapeHtml(model.errorMessage)}
` : ''} +
+
+ + + +
+
+ `; + }).join('') : ` +
+ + 暂无 ASR 模型 + 创建并自动校验成功后,智能体即可选择该识别模型。 + +
+ `} +
+
+ `; +} + +function collectAsrModelForm(body) { + const name = body.querySelector('#asrModelName')?.value.trim(); + const provider = body.querySelector('#asrProviderName')?.value.trim(); + const baseUrl = body.querySelector('#asrBaseUrl')?.value.trim(); + const apiKey = body.querySelector('#asrApiKey')?.value.trim(); + const secretKey = body.querySelector('#asrSecretKey')?.value.trim(); + const enableAfterValidation = body.querySelector('#asrEnableAfterValidation')?.checked !== false; + if (!name || !provider || !baseUrl || !apiKey) { + Toast.warning('请填写模型名称、Provider、Base URL 和 API Key'); + return null; + } + if (!/^https?:\/\//i.test(baseUrl)) { + Toast.warning('Base URL 需要以 http:// 或 https:// 开头'); + return null; + } + return { + id: `asr-${Date.now()}`, + name, + provider, + baseUrl, + apiKey, + secretKey, + enableAfterValidation, + status: 'validating', + enabled: false, + updatedAt: getNowText(), + }; +} + +function validateAsrModelConnection(model) { + const validationError = getSpeechModelValidationError(model, 'ASR'); + if (validationError) { + return new Promise(resolve => setTimeout(() => resolve({ success: false, message: validationError }), 650)); + } + const validate = API.agentDev.validateASRModel || API.agentDev.testASRModel; + return validate(model.id, { + provider: model.provider, + baseUrl: model.baseUrl, + }); +} + +function openAsrModelManagerModal(state, rerender) { + let showCreateForm = false; + const modal = createModal({ + title: 'ASR 模型管理', + size: 'xl', + showCancel: false, + confirmText: '关闭', + content: renderAsrModelManagerContent(showCreateForm), + onConfirm: () => true, + }); + + const refreshModal = () => { + const body = modal.overlay.querySelector('.modal-body'); + if (!body) return; + body.innerHTML = renderAsrModelManagerContent(showCreateForm); + lucide.createIcons({ elements: [body] }); + bindAsrModelManagerEvents(body); + rerender(); + }; + + const finishValidation = async (model, shouldEnable = true) => { + const result = await validateAsrModelConnection(model); + if (result.success === false) { + markSpeechModelFailed(model, result.message || 'ASR 模型连接失败'); + Toast.error(model.errorMessage); + } else { + markSpeechModelAvailable(model, shouldEnable); + if (model.enabled && !state.selectedAsrModelId) state.selectedAsrModelId = model.id; + Toast.success(`ASR 模型「${model.name}」校验通过`); + } + }; + + const bindAsrModelManagerEvents = (body) => { + body.querySelectorAll('[data-asr-action="show-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = true; + refreshModal(); + })); + + body.querySelectorAll('[data-asr-action="cancel-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = false; + refreshModal(); + })); + + body.querySelectorAll('[data-asr-action="create-new"]').forEach(btn => btn.addEventListener('click', async () => { + const model = collectAsrModelForm(body); + if (!model) return; + markSpeechModelValidating(model); + ensureAsrModelRegistry().push(model); + await API.agentDev.createASRModel(model); + Toast.info(`ASR 模型「${model.name}」开始自动校验`, 800); + showCreateForm = false; + refreshModal(); + await finishValidation(model, model.enableAfterValidation !== false); + refreshModal(); + })); + + body.querySelectorAll('[data-asr-action="validate-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureAsrModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + markSpeechModelValidating(model); + refreshModal(); + await finishValidation(model, true); + refreshModal(); + }); + }); + + body.querySelectorAll('[data-asr-action="toggle-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureAsrModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + const status = getAsrModelDisplayStatus(model); + if (status !== 'available' && status !== 'disabled') { + Toast.warning('只有校验通过的模型才可以启用或禁用'); + return; + } + if (status === 'disabled') { + await API.agentDev.setASRModelEnabled(model.id, true); + markSpeechModelAvailable(model, true); + if (!state.selectedAsrModelId) state.selectedAsrModelId = model.id; + Toast.success(`ASR 模型「${model.name}」已启用`); + } else { + await API.agentDev.setASRModelEnabled(model.id, false); + markSpeechModelAvailable(model, false); + if (state.selectedAsrModelId === model.id) state.selectedAsrModelId = ''; + Toast.info(`ASR 模型「${model.name}」已禁用`); + } + refreshModal(); + }); + }); + + body.querySelectorAll('[data-asr-action="delete-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const registry = ensureAsrModelRegistry(); + const model = registry.find(item => item.id === btn.dataset.id); + if (!model) return; + await API.agentDev.deleteASRModel(model.id); + window.__asrModelRegistry = registry.filter(item => item.id !== model.id); + if (state.selectedAsrModelId === model.id) state.selectedAsrModelId = ''; + Toast.success(`ASR 模型「${model.name}」已删除`); + refreshModal(); + }); + }); + }; + + const body = modal.overlay.querySelector('.modal-body'); + if (body) { + lucide.createIcons({ elements: [body] }); + bindAsrModelManagerEvents(body); + } +} + +function renderTtsModelManagerContent(showForm = false) { + const models = ensureTtsModelRegistry(); + return ` +
+
+ +
TTS 模型凭证、Voice ID、音频格式和语言类型统一维护在这里。创建模型后系统会自动校验 Key、接口、Voice 与音频返回,校验通过后才会进入智能体的语音合成下拉框。
+
+ + ${showForm ? ` +
+
+
+
新增 TTS 模型配置
+
无需手动测试连接;点击创建模型后自动进入后台验证流程。
+
+
+
+ + + + + +
+ Voice 配置(可选) +
+ + +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ ` : ` +
+
+
TTS 模型列表
+
模型创建并自动校验通过后,才会出现在智能体配置下拉框中。
+
+ +
+ `} + +
+
已添加模型
+ ${models.length} 个配置 +
+
+ ${models.length ? models.map(model => { + const meta = getTtsStatusMeta(model); + const status = getTtsModelDisplayStatus(model); + const canToggle = status === 'available' || status === 'disabled'; + return ` +
+
+
+ ${escapeHtml(model.name)} + ${escapeHtml(meta.label)} +
+
+ Provider:${escapeHtml(model.provider)} + Base URL:${escapeHtml(model.baseUrl)} + API Key:${escapeHtml(maskSecret(model.apiKey))} +
+
+ Secret Key:${escapeHtml(maskSecret(model.secretKey))} + Voice:${escapeHtml(getTtsVoiceSummary(model))} + 音频格式:${escapeHtml(String(model.audioFormat || '').toUpperCase())} + 语言类型:${escapeHtml(getLanguageTypeName(model.languageType))} +
+
+ 更新时间:${escapeHtml(model.updatedAt || '-')} + ${model.validatedAt ? `最近校验:${escapeHtml(model.validatedAt)}` : ''} +
+ ${model.errorMessage ? `
失败原因:${escapeHtml(model.errorMessage)}
` : ''} +
+
+ + + +
+
+ `; + }).join('') : ` +
+ + 暂无 TTS 模型 + 创建并自动校验成功后,智能体即可选择该合成模型。 + +
+ `} +
+
+ `; +} + +function collectTtsModelForm(body) { + const name = body.querySelector('#ttsModelName')?.value.trim(); + const provider = body.querySelector('#ttsProviderName')?.value.trim(); + const baseUrl = body.querySelector('#ttsBaseUrl')?.value.trim(); + const apiKey = body.querySelector('#ttsApiKey')?.value.trim(); + const secretKey = body.querySelector('#ttsSecretKey')?.value.trim(); + const voiceMode = body.querySelector('input[name="ttsVoiceMode"]:checked')?.value || 'single'; + const voiceId = body.querySelector('#ttsVoiceId')?.value.trim(); + const voiceIds = voiceMode === 'batch' + ? parseTtsVoiceIds(body.querySelector('#ttsVoiceIds')?.value) + : (voiceId ? [voiceId] : []); + const audioFormat = body.querySelector('#ttsAudioFormat')?.value || 'mp3'; + const languageType = body.querySelector('#ttsLanguageType')?.value || 'zh'; + const enableAfterValidation = body.querySelector('#ttsEnableAfterValidation')?.checked !== false; + if (!name || !provider || !baseUrl || !apiKey || !audioFormat || !languageType) { + Toast.warning('请填写模型名称、Provider、Base URL、API Key、音频格式和语言类型'); + return null; + } + if (!/^https?:\/\//i.test(baseUrl)) { + Toast.warning('Base URL 需要以 http:// 或 https:// 开头'); + return null; + } + if (voiceMode === 'batch' && voiceIds.length === 0) { + Toast.warning('批量添加 Voice 时,请至少填写一个 Voice ID'); + return null; + } + if (voiceIds.length > 50) { + Toast.warning('单次最多批量添加 50 个 Voice ID'); + return null; + } + return { + id: `tts-${Date.now()}`, + name, + provider, + baseUrl, + apiKey, + secretKey, + voiceMode, + voiceId: voiceIds[0] || '', + voiceIds, + audioFormat, + languageType, + enableAfterValidation, + status: 'validating', + enabled: false, + updatedAt: getNowText(), + }; +} + +function validateTtsModelConnection(model) { + const validationError = getSpeechModelValidationError(model, 'TTS'); + if (validationError) { + return new Promise(resolve => setTimeout(() => resolve({ success: false, message: validationError }), 750)); + } + const validate = API.agentDev.validateTTSModel || API.agentDev.testTTS; + return validate(model.id, { + provider: model.provider, + baseUrl: model.baseUrl, + voiceId: model.voiceId, + voiceIds: getTtsVoiceIds(model), + audioFormat: model.audioFormat, + languageType: model.languageType, + }); +} + +function openTtsModelManagerModal(state, rerender) { + let showCreateForm = false; + const modal = createModal({ + title: 'TTS 模型管理', + size: 'xl', + showCancel: false, + confirmText: '关闭', + content: renderTtsModelManagerContent(showCreateForm), + onConfirm: () => true, + }); + + const refreshModal = () => { + const body = modal.overlay.querySelector('.modal-body'); + if (!body) return; + body.innerHTML = renderTtsModelManagerContent(showCreateForm); + lucide.createIcons({ elements: [body] }); + bindTtsModelManagerEvents(body); + rerender(); + }; + + const finishValidation = async (model, shouldEnable = true) => { + const result = await validateTtsModelConnection(model); + if (result.success === false) { + markSpeechModelFailed(model, result.message || 'TTS 模型连接失败'); + Toast.error(model.errorMessage); + } else { + markSpeechModelAvailable(model, shouldEnable); + if (model.enabled && !state.selectedTtsModelId) state.selectedTtsModelId = model.id; + Toast.success(`TTS 模型「${model.name}」校验通过`); + } + }; + + const bindTtsModelManagerEvents = (body) => { + const syncTtsVoiceMode = () => { + const mode = body.querySelector('input[name="ttsVoiceMode"]:checked')?.value || 'single'; + body.querySelectorAll('.bailian-voice-mode-card').forEach(card => { + const input = card.querySelector('input[name="ttsVoiceMode"]'); + card.classList.toggle('active', input?.value === mode); + }); + body.querySelectorAll('[data-tts-voice-panel]').forEach(panel => { + panel.classList.toggle('hidden', panel.dataset.ttsVoicePanel !== mode); + }); + }; + + body.querySelectorAll('input[name="ttsVoiceMode"]').forEach(radio => { + radio.addEventListener('change', syncTtsVoiceMode); + }); + syncTtsVoiceMode(); + + body.querySelectorAll('[data-tts-action="show-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = true; + refreshModal(); + })); + + body.querySelectorAll('[data-tts-action="cancel-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = false; + refreshModal(); + })); + + body.querySelectorAll('[data-tts-action="create-new"]').forEach(btn => btn.addEventListener('click', async () => { + const model = collectTtsModelForm(body); + if (!model) return; + markSpeechModelValidating(model); + ensureTtsModelRegistry().push(model); + await API.agentDev.createTTSModel(model); + Toast.info(`TTS 模型「${model.name}」开始自动校验`, 800); + showCreateForm = false; + refreshModal(); + await finishValidation(model, model.enableAfterValidation !== false); + refreshModal(); + })); + + body.querySelectorAll('[data-tts-action="validate-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureTtsModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + markSpeechModelValidating(model); + refreshModal(); + await finishValidation(model, true); + refreshModal(); + }); + }); + + body.querySelectorAll('[data-tts-action="toggle-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureTtsModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + const status = getTtsModelDisplayStatus(model); + if (status !== 'available' && status !== 'disabled') { + Toast.warning('只有校验通过的模型才可以启用或禁用'); + return; + } + if (status === 'disabled') { + await API.agentDev.setTTSModelEnabled(model.id, true); + markSpeechModelAvailable(model, true); + if (!state.selectedTtsModelId) state.selectedTtsModelId = model.id; + Toast.success(`TTS 模型「${model.name}」已启用`); + } else { + await API.agentDev.setTTSModelEnabled(model.id, false); + markSpeechModelAvailable(model, false); + if (state.selectedTtsModelId === model.id) state.selectedTtsModelId = ''; + Toast.info(`TTS 模型「${model.name}」已禁用`); + } + refreshModal(); + }); + }); + + body.querySelectorAll('[data-tts-action="delete-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const registry = ensureTtsModelRegistry(); + const model = registry.find(item => item.id === btn.dataset.id); + if (!model) return; + await API.agentDev.deleteTTSModel(model.id); + window.__ttsModelRegistry = registry.filter(item => item.id !== model.id); + if (state.selectedTtsModelId === model.id) state.selectedTtsModelId = ''; + Toast.success(`TTS 模型「${model.name}」已删除`); + refreshModal(); + }); + }); + }; + + const body = modal.overlay.querySelector('.modal-body'); + if (body) { + lucide.createIcons({ elements: [body] }); + bindTtsModelManagerEvents(body); + } +} + +function getTextModelDisplayStatus(model) { + return getSpeechModelDisplayStatus(model); +} + +function getTextStatusMeta(model) { + return TEXT_MODEL_STATUS_META[getTextModelDisplayStatus(model)] || TEXT_MODEL_STATUS_META.failed; +} + +function getTextModelValidationError(model) { + const apiKey = String(model.apiKey || '').trim().toLowerCase(); + const baseUrl = String(model.baseUrl || '').trim().toLowerCase(); + const modelCode = String(model.modelCode || '').trim().toLowerCase(); + if (!apiKey || apiKey.includes('invalid') || apiKey.startsWith('bad') || apiKey === 'sk-invalid') { + return 'Key 无效'; + } + if (!/^https?:\/\//i.test(model.baseUrl || '') || /down|timeout|error|404|500/.test(baseUrl)) { + return '接口异常'; + } + if (!modelCode || /missing|notfound|not-found|none|404/.test(modelCode)) { + return '模型 ID 不存在'; + } + if (!Number.isFinite(Number(model.contextWindow)) || Number(model.contextWindow) < 1024) { + return '上下文长度无效'; + } + return ''; +} + +function renderTextModelManagerContent(showForm = false) { + const models = ensureTextModelRegistry(); + return ` +
+
+ +
文本模型凭证统一维护在这里。创建模型后系统会自动校验 API Key、接口连通性和模型 ID,状态为「可用」且已启用后才会进入智能体的文本模型下拉框。
+
+ + ${showForm ? ` +
+
+
+
新增文本模型配置
+
适用于通义千问、DeepSeek、ChatGPT、豆包或 OpenAI Compatible 接口。
+
+
+
+ + + + + + + + + +
+ +
+ + +
+
+ ` : ` +
+
+
文本模型列表
+
模型创建并自动校验通过后,才会出现在智能体配置下拉框中。
+
+ +
+ `} + +
+
已添加模型
+ ${models.length} 个配置 +
+
+ ${models.length ? models.map(model => { + const meta = getTextStatusMeta(model); + const status = getTextModelDisplayStatus(model); + const canToggle = status === 'available' || status === 'disabled'; + return ` +
+
+
+ ${escapeHtml(model.name)} + ${escapeHtml(meta.label)} +
+
+ Provider:${escapeHtml(model.provider)} + 模型 ID:${escapeHtml(model.modelCode)} + 协议:${escapeHtml(model.protocol || 'openai-compatible')} +
+
+ Base URL:${escapeHtml(model.baseUrl)} + API Key:${escapeHtml(maskSecret(model.apiKey))} + 上下文:${escapeHtml(model.contextWindow || '-')} tokens + 最大输出:${escapeHtml(model.maxOutputTokens || '-')} tokens +
+
+ Secret Key:${escapeHtml(maskSecret(model.secretKey))} + 更新时间:${escapeHtml(model.updatedAt || '-')} + ${model.validatedAt ? `最近校验:${escapeHtml(model.validatedAt)}` : ''} +
+ ${model.errorMessage ? `
失败原因:${escapeHtml(model.errorMessage)}
` : ''} +
+
+ + + +
+
+ `; + }).join('') : ` +
+ + 暂无文本模型 + 创建并自动校验成功后,智能体即可选择该文本模型。 + +
+ `} +
+
+ `; +} + +function collectTextModelForm(body) { + const name = body.querySelector('#textModelName')?.value.trim(); + const provider = body.querySelector('#textProviderName')?.value.trim(); + const modelCode = body.querySelector('#textModelCode')?.value.trim(); + const protocol = body.querySelector('#textProtocol')?.value || 'openai-compatible'; + const baseUrl = body.querySelector('#textBaseUrl')?.value.trim(); + const apiKey = body.querySelector('#textApiKey')?.value.trim(); + const secretKey = body.querySelector('#textSecretKey')?.value.trim(); + const contextWindow = Number.parseInt(body.querySelector('#textContextWindow')?.value || '128000', 10); + const maxOutputTokens = Number.parseInt(body.querySelector('#textMaxOutputTokens')?.value || '4096', 10); + const enableAfterValidation = body.querySelector('#textEnableAfterValidation')?.checked !== false; + if (!name || !provider || !modelCode || !baseUrl || !apiKey) { + Toast.warning('请填写模型名称、Provider、模型 ID、Base URL 和 API Key'); + return null; + } + if (!/^https?:\/\//i.test(baseUrl)) { + Toast.warning('Base URL 需要以 http:// 或 https:// 开头'); + return null; + } + if (!Number.isFinite(contextWindow) || contextWindow < 1024) { + Toast.warning('上下文长度至少为 1024 tokens'); + return null; + } + if (!Number.isFinite(maxOutputTokens) || maxOutputTokens < 256) { + Toast.warning('最大输出 Token 至少为 256'); + return null; + } + return { + id: `text-${Date.now()}`, + name, + provider, + modelCode, + protocol, + baseUrl, + apiKey, + secretKey, + contextWindow, + maxOutputTokens, + enableAfterValidation, + status: 'validating', + enabled: false, + updatedAt: getNowText(), + }; +} + +function validateTextModelConnection(model) { + const validationError = getTextModelValidationError(model); + if (validationError) { + return new Promise(resolve => setTimeout(() => resolve({ success: false, message: validationError }), 750)); + } + const validate = API.agentDev.validateTextModel || API.agentDev.testTextModel; + if (!validate) { + return Promise.resolve({ success: true, data: model }); + } + return validate(model.id, { + provider: model.provider, + modelCode: model.modelCode, + protocol: model.protocol, + baseUrl: model.baseUrl, + contextWindow: model.contextWindow, + maxOutputTokens: model.maxOutputTokens, + }); +} + +function openTextModelManagerModal(state, rerender) { + let showCreateForm = false; + const modal = createModal({ + title: '文本模型管理', + size: 'xl', + showCancel: false, + confirmText: '关闭', + content: renderTextModelManagerContent(showCreateForm), + onConfirm: () => true, + }); + + const refreshModal = () => { + const body = modal.overlay.querySelector('.modal-body'); + if (!body) return; + body.innerHTML = renderTextModelManagerContent(showCreateForm); + lucide.createIcons({ elements: [body] }); + bindTextModelManagerEvents(body); + rerender(); + }; + + const finishValidation = async (model, shouldEnable = true) => { + const result = await validateTextModelConnection(model); + if (result.success === false) { + markSpeechModelFailed(model, result.message || '文本模型连接失败'); + Toast.error(model.errorMessage); + } else { + markSpeechModelAvailable(model, shouldEnable); + if (model.enabled && !state.selectedTextModelId) state.selectedTextModelId = model.id; + Toast.success(`文本模型「${model.name}」校验通过`); + } + }; + + const bindTextModelManagerEvents = (body) => { + body.querySelectorAll('[data-text-action="show-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = true; + refreshModal(); + })); + + body.querySelectorAll('[data-text-action="cancel-create"]').forEach(btn => btn.addEventListener('click', () => { + showCreateForm = false; + refreshModal(); + })); + + body.querySelectorAll('[data-text-action="create-new"]').forEach(btn => btn.addEventListener('click', async () => { + const model = collectTextModelForm(body); + if (!model) return; + markSpeechModelValidating(model); + ensureTextModelRegistry().push(model); + await API.agentDev.createTextModel(model); + Toast.info(`文本模型「${model.name}」开始自动校验`, 800); + showCreateForm = false; + refreshModal(); + await finishValidation(model, model.enableAfterValidation !== false); + refreshModal(); + })); + + body.querySelectorAll('[data-text-action="validate-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureTextModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + markSpeechModelValidating(model); + refreshModal(); + await finishValidation(model, true); + refreshModal(); + }); + }); + + body.querySelectorAll('[data-text-action="toggle-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const model = ensureTextModelRegistry().find(item => item.id === btn.dataset.id); + if (!model) return; + const status = getTextModelDisplayStatus(model); + if (status !== 'available' && status !== 'disabled') { + Toast.warning('只有校验通过的模型才可以启用或禁用'); + return; + } + if (status === 'disabled') { + await API.agentDev.setTextModelEnabled(model.id, true); + markSpeechModelAvailable(model, true); + if (!state.selectedTextModelId) state.selectedTextModelId = model.id; + Toast.success(`文本模型「${model.name}」已启用`); + } else { + await API.agentDev.setTextModelEnabled(model.id, false); + markSpeechModelAvailable(model, false); + if (state.selectedTextModelId === model.id) state.selectedTextModelId = ''; + Toast.info(`文本模型「${model.name}」已禁用`); + } + refreshModal(); + }); + }); + + body.querySelectorAll('[data-text-action="delete-existing"]').forEach(btn => { + btn.addEventListener('click', async () => { + const registry = ensureTextModelRegistry(); + const model = registry.find(item => item.id === btn.dataset.id); + if (!model) return; + await API.agentDev.deleteTextModel(model.id); + window.__textModelRegistry = registry.filter(item => item.id !== model.id); + if (state.selectedTextModelId === model.id) state.selectedTextModelId = ''; + Toast.success(`文本模型「${model.name}」已删除`); + refreshModal(); + }); + }); + }; + + const body = modal.overlay.querySelector('.modal-body'); + if (body) { + lucide.createIcons({ elements: [body] }); + bindTextModelManagerEvents(body); + } +} + function renderPromptEditor(prompt) { return `
@@ -948,16 +2599,9 @@ function renderPromptEditor(prompt) { } function renderKnowledgeBaseSection(items) { - return renderSection(`知识库 ${items.length}/10`, ` -
- 已挂载知识库会参与 RAG 检索和答案补充 -
- - -
-
+ return renderSection('知识库', `
- ${items.map(item => ` + ${items.length ? items.map((item, index) => `
@@ -965,25 +2609,165 @@ function renderKnowledgeBaseSection(items) {
${escapeHtml(item.desc)}
权重 ${escapeHtml(item.weight)}
- +
- `).join('')} + `).join('') : ` +
+ + 暂无挂载知识库 +
+ `} +
+ `, '', ` +
+ +
`); } -function renderWebSearchSection(modes) { - return renderSection('联网搜索', ` -
- ${modes.map(mode => ` - - `).join('')} -
- `); +function getKnowledgeBaseCatalog() { + const catalog = Array.isArray(window.__knowledgeBaseCatalog) && window.__knowledgeBaseCatalog.length + ? window.__knowledgeBaseCatalog + : AGENT_KNOWLEDGE_BASE_CATALOG; + return catalog.map(item => ({ + id: item.id, + name: item.name, + description: item.description || item.desc || '', + docs: item.docs || item.docCount || 0, + size: item.size || '', + typeName: item.typeName || '文档搜索', + status: item.status || 'ready', + tags: Array.isArray(item.tags) ? item.tags : [], + })); +} + +function getKnowledgeBaseStatusLabel(status) { + const map = { + ready: '已就绪', + processing: '处理中', + uploading: '上传中', + error: '异常', + }; + return map[status] || status || '未知'; +} + +function openKnowledgeBasePickerModal(state, rerender) { + const catalog = getKnowledgeBaseCatalog(); + const attached = state.knowledgeBases || []; + const selectedIds = new Set(attached.map(item => item.id || item.name)); + let searchValue = ''; + + const modal = createModal({ + title: '添加知识库', + size: 'xl', + confirmText: '挂载所选', + cancelText: '取消', + content: ` +
+
+
+ + +
+ +
+
+
+ `, + onConfirm: () => { + if (!selectedIds.size) { + Toast.warning('请先选择需要挂载的知识库'); + return false; + } + const previousById = new Map(attached.map(item => [item.id || item.name, item])); + state.knowledgeBases = catalog + .filter(item => selectedIds.has(item.id)) + .map(item => { + const existing = previousById.get(item.id); + return { + id: item.id, + name: item.name, + desc: item.description, + weight: existing?.weight || '1.00', + docs: item.docs, + status: item.status, + }; + }); + Toast.success(`已挂载 ${state.knowledgeBases.length} 个知识库`); + rerender(); + return true; + }, + }); + + const overlay = modal.overlay; + const listEl = overlay.querySelector('#agentKbPickerList'); + const searchInput = overlay.querySelector('#agentKbPickerSearch'); + + function getFilteredCatalog() { + const keyword = searchValue.trim().toLowerCase(); + if (!keyword) return catalog; + return catalog.filter(item => [ + item.name, + item.description, + item.typeName, + ...item.tags, + ].some(value => String(value || '').toLowerCase().includes(keyword))); + } + + function renderList() { + const filtered = getFilteredCatalog(); + listEl.innerHTML = filtered.length ? filtered.map(item => ` + + `).join('') : ` +
+ + 没有找到匹配的知识库 +
+ `; + + listEl.querySelectorAll('input[type="checkbox"]').forEach(input => { + input.addEventListener('change', () => { + if (input.checked) { + selectedIds.add(input.value); + } else { + selectedIds.delete(input.value); + } + renderList(); + renderAgentIcons(overlay); + }); + }); + } + + searchInput?.addEventListener('input', event => { + searchValue = event.target.value; + renderList(); + renderAgentIcons(overlay); + }); + + overlay.querySelector('#agentCreateKbFromPicker')?.addEventListener('click', () => { + modal.close?.(); + navigateTo('kb-create'); + }); + + renderList(); + renderAgentIcons(overlay); } function renderMiniAppSection(title, items, iconName) { @@ -1004,14 +2788,48 @@ function renderMiniAppSection(title, items, iconName) { `); } -function renderMcpSection() { - return renderSection('MCP服务 0', ` -
-
- MCP服务 0 -

智能体可以通过标准化协议(MCP)连接企业内部服务API并发起调用

-
- +function getMcpStatusMeta(service) { + return MCP_SERVICE_STATUS_META[service?.status || 'pending'] || MCP_SERVICE_STATUS_META.pending; +} + +function renderMcpSection(state) { + const library = ensureMcpServiceLibrary(); + const selectedIds = new Set(state.selectedMcpServiceIds || []); + const services = library.filter(item => selectedIds.has(item.id)); + return renderSection('MCP服务', ` +
+ 智能体可以通过标准化协议 MCP 连接企业内部服务 API,并将服务工具暴露给模型调用。 + +
+
+ ${services.length ? services.map(item => { + const meta = getMcpStatusMeta(item); + const tools = Array.isArray(item.tools) ? item.tools.filter(tool => tool.name) : []; + return ` +
+
+
+
${escapeHtml(item.name)} ${escapeHtml(item.namespace || 'default')}
+
${escapeHtml(item.desc)}
+
+ ${escapeHtml(item.transport || 'Streamable HTTP')} + ${tools.length} 个工具 + ${escapeHtml(item.endpoint || '-')} +
+
+ ${escapeHtml(meta.label)} + + +
+ `; + }).join('') : ` +
+ + 暂无已挂载 MCP 服务 + 从 0 开始创建 MCP 服务,连接通过后可挂载到当前智能体。 + +
+ `}
`); } @@ -1035,7 +2853,6 @@ function bindWorkbenchEvents(container, state, rerender) { 'voice-enabled': 'voiceEnabled', 'asr-enabled': 'asrEnabled', 'tts-enabled': 'ttsEnabled', - 'realtime-correction': 'realtimeCorrectionEnabled', 'tts-filter': 'ttsFilterEnabled', 'interrupt-primary': 'interruptPrimaryEnabled', 'interrupt-secondary': 'interruptSecondaryEnabled', @@ -1053,30 +2870,9 @@ function bindWorkbenchEvents(container, state, rerender) { }); }); - container.querySelectorAll('input[name="textModelMode"]').forEach(radio => { - radio.addEventListener('change', () => { - if (!radio.checked) return; - state.selectedTextModelMode = radio.value; - const group = radio.value === 'recommended' ? '推荐模型' : '更多模型'; - const candidates = AGENT_CONFIG_MOCK.understand.textModels.filter(item => item.group === group); - state.selectedTextModelId = candidates[0]?.id || state.selectedTextModelId; - rerender(); - }); - }); - - container.querySelectorAll('input[name="webSearchMode"]').forEach(radio => { - radio.addEventListener('change', () => { - if (!radio.checked) return; - state.searchMode = radio.value; - rerender(); - }); - }); - container.querySelectorAll('[data-action="text-model"]').forEach(select => { select.addEventListener('change', () => { state.selectedTextModelId = select.value; - const current = AGENT_CONFIG_MOCK.understand.textModels.find(item => item.id === select.value); - if (current) state.selectedTextModelMode = current.group === '更多模型' ? 'more' : 'recommended'; rerender(); }); }); @@ -1091,8 +2887,6 @@ function bindWorkbenchEvents(container, state, rerender) { container.querySelectorAll('[data-action="tts-model"]').forEach(select => { select.addEventListener('change', () => { state.selectedTtsModelId = select.value; - const matchedGroup = AGENT_CONFIG_MOCK.voice.ttsGroups.find(group => group.models.some(model => model.id === select.value)); - state.selectedTtsGroupId = matchedGroup?.id || state.selectedTtsGroupId; rerender(); }); }); @@ -1104,10 +2898,81 @@ function bindWorkbenchEvents(container, state, rerender) { }); }); - container.querySelectorAll('[data-action="scene-language"]').forEach(select => { + container.querySelectorAll('[data-action="interaction-language"]').forEach(select => { select.addEventListener('change', () => { - state.selectedSceneLanguage = select.value; - window.__selectedSceneLanguage = select.value; + state.selectedInteractionLanguage = select.value; + window.__selectedInteractionLanguage = select.value; + rerender(); + }); + }); + + container.querySelectorAll('[data-action="delete-variable"]').forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + event.stopImmediatePropagation(); + const variable = (state.variables || []).find(item => item.id === btn.dataset.variableId); + if (!variable) return; + state.variables = (state.variables || []).filter(item => item.id !== variable.id); + Toast.success(variable.name ? `变量 ${variable.name} 已删除` : '变量已删除'); + rerender(); + }); + }); + + container.querySelectorAll('[data-variable-field]').forEach(input => { + input.addEventListener('input', () => { + const variable = (state.variables || []).find(item => item.id === input.dataset.variableId); + if (!variable) return; + variable[input.dataset.variableField] = input.value; + scheduleAgentConfigAutoSave(state); + }); + }); + + container.querySelectorAll('[data-action="remove-kb"]').forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + event.stopImmediatePropagation(); + const index = Number.parseInt(btn.dataset.kbIndex, 10); + const list = state.knowledgeBases || []; + const target = list[index]; + if (!target) return; + state.knowledgeBases = list.filter((_, itemIndex) => itemIndex !== index); + Toast.success(`知识库「${target.name}」已移除`); + rerender(); + }); + }); + + container.querySelectorAll('[data-action="remove-command"]').forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + event.stopImmediatePropagation(); + const commandId = btn.dataset.commandId; + const command = ensureCommandLibrary().find(item => item.id === commandId); + state.selectedCommandIds = (state.selectedCommandIds || []).filter(id => id !== commandId); + Toast.success(command?.name ? `指令「${command.name}」已从当前智能体移除` : '指令已移除'); + rerender(); + }); + }); + + container.querySelectorAll('[data-action="remove-plugin"]').forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + event.stopImmediatePropagation(); + const pluginId = btn.dataset.pluginId; + const plugin = ensurePluginLibrary().find(item => item.id === pluginId); + state.selectedPluginIds = (state.selectedPluginIds || []).filter(id => id !== pluginId); + Toast.success(plugin?.name ? `插件「${plugin.name}」已从当前智能体移除` : '插件已移除'); + rerender(); + }); + }); + + container.querySelectorAll('[data-action="remove-mcp-service"]').forEach(btn => { + btn.addEventListener('click', event => { + event.preventDefault(); + event.stopImmediatePropagation(); + const serviceId = btn.dataset.mcpId; + const service = ensureMcpServiceLibrary().find(item => item.id === serviceId); + state.selectedMcpServiceIds = (state.selectedMcpServiceIds || []).filter(id => id !== serviceId); + Toast.success(service?.name ? `MCP 服务「${service.name}」已从当前智能体移除` : 'MCP 服务已移除'); rerender(); }); }); @@ -1125,26 +2990,63 @@ function bindWorkbenchEvents(container, state, rerender) { } if (action === 'publish') { state.app.status = 'published'; + autoSaveAgentConfig(state); Toast.success('应用已提交发布,预计 2-5 分钟生效'); rerender(); return; } if (action === 'run') { - state.preview = 'running'; - updatePreviewOnly(container, state.preview); - Toast.success('运行预览已启动'); + autoSaveAgentConfig(state); + clearAgentRunTimer(state); + const session = getRunSession(state); + const configuredKeys = getRuntimeApiKeysForApp(state); + const activeKeyStillExists = configuredKeys.some(key => key.value === session.apiKey); + if (!activeKeyStillExists) { + session.apiKeyConfirmed = false; + session.apiKey = ''; + session.apiKeyName = ''; + session.apiKeyProvider = ''; + } + session.isRecording = false; + session.elapsedSeconds = 0; + session.latencyMs = null; + session.transcript = ''; + session.aiResponse = ''; + state.preview = state.voiceEnabled + ? (session.apiKeyConfirmed ? 'voice-runtime' : 'voice-auth') + : (session.apiKeyConfirmed ? 'text-runtime' : 'text-auth'); + updatePreviewOnly(container, state); + Toast.success(state.voiceEnabled + ? '配置已自动保存,请配置运行 API Key' + : '配置已自动保存,请配置文字运行 API Key'); return; } if (action === 'add-kb') { - navigateTo('knowledge-base'); + openKnowledgeBasePickerModal(state, rerender); return; } if (action === 'knowledge-weight') { openKnowledgeWeightModal(state, rerender); return; } - if (action === 'scene') { - openSceneTemplateModal(state, rerender); + if (action === 'asr-model-manager') { + openAsrModelManagerModal(state, rerender); + return; + } + if (action === 'tts-model-manager') { + openTtsModelManagerModal(state, rerender); + return; + } + if (action === 'text-model-manager') { + openTextModelManagerModal(state, rerender); + return; + } + if (action === 'add-variable' || action === 'variable') { + state.variables = [ + ...(state.variables || []), + { id: `var-${Date.now()}`, name: '', desc: '' }, + ]; + rerender(); return; } if (action === 'hotwords') { @@ -1163,22 +3065,18 @@ function bindWorkbenchEvents(container, state, rerender) { openPluginConfigModal(state, rerender); return; } - if (action === 'edge-collapse') { - state.edgeAlgorithmExpanded = !state.edgeAlgorithmExpanded; - rerender(); + if (action === 'mcp-config') { + openMcpConfigModal(state, rerender); return; } const actionText = { history: '打开历史记录', api: '打开 API 接入文档', - license: '打开 License 购买页', model: '切换模型', variable: '打开变量配置', setting: '打开设置', remove: '已移除当前项', - mcp: '添加 MCP 服务', 'add-app': '添加应用', - 'correction-upload': '上传词表', }; Toast.info(actionText[action] || '操作已触发'); event.stopPropagation(); @@ -1186,6 +3084,183 @@ function bindWorkbenchEvents(container, state, rerender) { }); } +function bindRuntimePreviewEvents(container, state, refreshPreview) { + const pane = container.querySelector('.bailian-preview-pane'); + if (!pane) return; + const session = getRunSession(state); + + pane.querySelector('[data-preview-action="confirm-runtime-key"]')?.addEventListener('click', () => { + const select = pane.querySelector('[data-preview-field="runtime-key-select"]'); + const keys = getRuntimeApiKeysForApp(state); + const selected = keys.find(item => item.id === select?.value); + if (!selected) { + Toast.warning('请先选择系统设置中已保存的 API Key'); + select?.focus(); + return; + } + session.apiKey = selected.value; + session.apiKeyName = `${selected.name} · ${maskSecret(selected.value)}`; + session.apiKeyProvider = selected.provider; + session.apiKeyConfirmed = true; + session.isRecording = false; + state.preview = state.preview === 'text-auth' || !state.voiceEnabled ? 'text-runtime' : 'voice-runtime'; + Toast.success(state.preview === 'text-runtime' ? 'API Key 已配置,可以开始文字聊天' : 'API Key 已配置,可以开始语音测试'); + refreshPreview(); + }); + + pane.querySelector('[data-preview-action="go-settings-api-keys"]')?.addEventListener('click', () => { + clearAgentRunTimer(state); + session.isRecording = false; + Toast.info('请在系统设置中添加并保存 API Key'); + navigateTo('settings'); + }); + + pane.querySelector('[data-preview-action="change-runtime-key"]')?.addEventListener('click', () => { + clearAgentRunTimer(state); + session.isRecording = false; + session.apiKeyConfirmed = false; + state.preview = state.voiceEnabled ? 'voice-auth' : 'text-auth'; + refreshPreview(); + }); + + pane.querySelector('[data-preview-action="toggle-call"]')?.addEventListener('click', () => { + if (!session.apiKeyConfirmed) { + Toast.warning('请先配置运行 API Key'); + state.preview = state.voiceEnabled ? 'voice-auth' : 'text-auth'; + refreshPreview(); + return; + } + if (session.isRecording) { + stopVoiceRuntimeCall(container, state); + Toast.info('本轮语音通话已结束', 900); + } else { + startVoiceRuntimeCall(container, state); + Toast.info('已开始录音,请开始说话', 900); + } + }); + + pane.querySelector('[data-preview-action="toggle-mic"]')?.addEventListener('click', () => { + session.micMuted = !session.micMuted; + Toast.info(session.micMuted ? '麦克风已关闭' : '麦克风已打开', 800); + if (!syncVoiceRuntimeDom(container, state)) { + refreshPreview(); + } + }); + + pane.querySelector('[data-preview-action="voice-device"]')?.addEventListener('click', () => { + Toast.info('当前为 MacBook Pro 麦克风,后端接入后可读取浏览器音频设备列表', 1200); + }); + + pane.querySelector('[data-preview-action="voice-runtime-settings"]')?.addEventListener('click', () => { + Toast.info('语音体验设置入口,后续可接入采样率、降噪和播放设备配置', 1200); + }); + + pane.querySelector('[data-preview-action="copy-voice-ai"]')?.addEventListener('click', async () => { + const text = session.aiResponse || session.aiDisplayResponse || ''; + if (!text) return; + try { + await navigator.clipboard?.writeText(text); + Toast.success('AI 回复已复制'); + } catch (error) { + Toast.warning('当前浏览器不允许自动复制,请手动复制'); + } + }); + + pane.querySelector('[data-preview-action="regenerate-voice-ai"]')?.addEventListener('click', () => { + const base = session.aiResponse || '我已经准备好继续语音对话。'; + const next = `${base}(重新生成版本)`; + session.aiResponse = next; + session.aiDisplayResponse = ''; + session.lastVoiceAiResponse = next; + Toast.info('正在重新生成语音回复', 800); + revealVoiceReply(container, state, next); + }); + + pane.querySelector('[data-preview-field="runtime-text-input"]')?.addEventListener('input', event => { + session.textInput = event.target.value; + }); + + pane.querySelector('[data-preview-field="runtime-text-input"]')?.addEventListener('keydown', event => { + if (event.key !== 'Enter' || event.shiftKey) return; + event.preventDefault(); + pane.querySelector('[data-preview-action="send-text-runtime"]')?.click(); + }); + + pane.querySelector('[data-preview-action="send-text-runtime"]')?.addEventListener('click', () => { + const input = pane.querySelector('[data-preview-field="runtime-text-input"]'); + const value = input?.value.trim() || ''; + if (!session.apiKeyConfirmed) { + Toast.warning('请先配置运行 API Key'); + state.preview = 'text-auth'; + refreshPreview(); + return; + } + if (!value) { + Toast.warning('请输入测试内容'); + input?.focus(); + return; + } + session.chatMessages = [ + ...(Array.isArray(session.chatMessages) ? session.chatMessages : []), + { role: 'user', content: value }, + ]; + session.textInput = ''; + session.textPending = true; + session.latencyMs = null; + refreshPreview(); + window.setTimeout(() => { + const reply = buildMockTextReply(value); + session.textPending = false; + session.latencyMs = 390 + (value.length * 13) % 260; + const messages = Array.isArray(session.chatMessages) ? session.chatMessages : []; + messages.push({ + role: 'ai', + sourceText: value, + retryCount: 0, + content: reply, + displayContent: '', + }); + session.chatMessages = messages; + const messageIndex = messages.length - 1; + refreshPreview(); + revealTextReply(container, state, messageIndex, reply); + }, 850); + }); + + pane.querySelectorAll('[data-preview-action="copy-ai-message"]').forEach(btn => { + btn.addEventListener('click', async () => { + const index = Number.parseInt(btn.dataset.messageIndex, 10); + const message = (session.chatMessages || [])[index]; + const text = message?.content || ''; + if (!text) return; + try { + await navigator.clipboard?.writeText(text); + Toast.success('AI 回复已复制'); + } catch (error) { + Toast.warning('当前浏览器不允许自动复制,请手动复制'); + } + }); + }); + + pane.querySelectorAll('[data-preview-action="regenerate-ai-message"]').forEach(btn => { + btn.addEventListener('click', () => { + const index = Number.parseInt(btn.dataset.messageIndex, 10); + const message = (session.chatMessages || [])[index]; + if (!message || message.role !== 'ai') return; + const nextRetry = (message.retryCount || 0) + 1; + const sourceText = message.sourceText || '这条消息'; + const reply = buildMockTextReply(sourceText, nextRetry); + message.retryCount = nextRetry; + message.content = reply; + message.displayContent = ''; + session.latencyMs = 420 + (nextRetry * 37) % 180; + Toast.info('正在重新生成回复', 800); + refreshPreview(); + revealTextReply(container, state, index, reply); + }); + }); +} + function updateActiveStepTab(container, stepId) { container.querySelectorAll('.bailian-step-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.step === stepId); @@ -1228,143 +3303,37 @@ function bindStepScrollSpy(container, state) { }); } -function updatePreviewOnly(container, previewState) { +function updatePreviewOnly(container, state) { const preview = container.querySelector('.bailian-preview-pane'); if (!preview) return; const temp = document.createElement('div'); - temp.innerHTML = renderPreviewPane(previewState); + temp.innerHTML = renderPreviewPane(state); preview.replaceWith(temp.firstElementChild); renderAgentIcons(container.querySelector('.bailian-preview-pane')); + bindRuntimePreviewEvents(container, state, () => updatePreviewOnly(container, state)); + const textMessageArea = container.querySelector('.agent-text-message-area'); + if (textMessageArea) { + textMessageArea.scrollTop = textMessageArea.scrollHeight; + } + const voiceScrollArea = container.querySelector('.agent-voice-stage') || container.querySelector('.agent-voice-conversation'); + if (voiceScrollArea) { + voiceScrollArea.scrollTop = voiceScrollArea.scrollHeight; + } } -function bindPromptCounter(container) { +function bindPromptCounter(container, state) { const editor = container.querySelector('#agentPromptEditor'); const counter = container.querySelector('#promptCounter'); if (!editor || !counter) return; const update = () => { counter.textContent = `${editor.value.length}/8000`; + if (state) { + state.prompt = editor.value; + scheduleAgentConfigAutoSave(state); + } }; editor.addEventListener('input', update); - update(); -} - -function openSceneTemplateModal(state, rerender) { - let pendingSceneId = state.selectedSceneId; - let pendingVersionId = state.selectedSceneVersion; - let pendingLanguageId = state.selectedSceneLanguage; - - if (typeof createModal !== 'function') { - Toast.info('场景模板选择器暂不可用'); - return; - } - - const modal = createModal({ - title: '切换场景模板', - size: 'xl', - confirmText: '应用模板', - cancelText: '取消', - content: ` -
-
-
选择场景
-
- ${AGENT_SCENE_TEMPLATES.map(scene => ` - - `).join('')} -
-
-
-
选择版本
-
- ${AGENT_SCENE_VERSIONS.map(version => ` - - `).join('')} -
-
-
-
对话语言 *
-
- ${AGENT_SCENE_LANGUAGES.map(language => ` - - `).join('')} -
-
-
- `, - onConfirm: () => { - if (!pendingLanguageId || !AGENT_SCENE_LANGUAGES.some(language => language.id === pendingLanguageId)) { - Toast.warning('请选择对话语言'); - return false; - } - state.selectedSceneId = pendingSceneId; - state.selectedSceneVersion = pendingVersionId; - state.selectedSceneLanguage = pendingLanguageId; - window.__selectedSceneTemplateId = pendingSceneId; - window.__selectedSceneVersion = pendingVersionId; - window.__selectedSceneLanguage = pendingLanguageId; - const scene = getSelectedScene(state); - const version = getSelectedSceneVersion(state); - const language = getSelectedSceneLanguage(state); - Toast.success(`已切换至「${scene.name} · ${version.name} · ${language.name}」`); - rerender(); - return true; - }, - }); - - const overlay = modal.overlay; - const syncSceneOptions = () => { - overlay.querySelectorAll('.bailian-scene-option').forEach(option => { - option.classList.toggle('selected', option.dataset.sceneId === pendingSceneId); - option.querySelector('input').checked = option.dataset.sceneId === pendingSceneId; - }); - }; - const syncVersionOptions = () => { - overlay.querySelectorAll('.bailian-scene-version-option').forEach(option => { - option.classList.toggle('selected', option.dataset.versionId === pendingVersionId); - option.querySelector('input').checked = option.dataset.versionId === pendingVersionId; - }); - }; - const syncLanguageOptions = () => { - overlay.querySelectorAll('.bailian-scene-language-option').forEach(option => { - option.classList.toggle('selected', option.dataset.languageId === pendingLanguageId); - option.querySelector('input').checked = option.dataset.languageId === pendingLanguageId; - }); - }; - - overlay.querySelectorAll('.bailian-scene-option').forEach(option => { - option.addEventListener('click', () => { - pendingSceneId = option.dataset.sceneId; - syncSceneOptions(); - }); - }); - overlay.querySelectorAll('.bailian-scene-version-option').forEach(option => { - option.addEventListener('click', () => { - pendingVersionId = option.dataset.versionId; - syncVersionOptions(); - }); - }); - overlay.querySelectorAll('.bailian-scene-language-option').forEach(option => { - option.addEventListener('click', () => { - pendingLanguageId = option.dataset.languageId; - syncLanguageOptions(); - }); - }); - renderAgentIcons(overlay); + counter.textContent = `${editor.value.length}/8000`; } function openKnowledgeWeightModal(state, rerender) { @@ -1415,8 +3384,11 @@ function openTtsFilterModal(state, rerender) { { id: 'square', left: '[', right: ']', label: '[ ]' }, { id: 'curly', left: '{', right: '}', label: '{ }' }, ]; - const draftPairs = (state.ttsFilterPairs || []).map(item => ({ ...item })); - const draftSingles = [...(state.ttsFilterSingles || [])]; + const getPairKey = (pair) => `${String(pair.left || '').trim()}__${String(pair.right || '').trim()}`; + const commonPairKeys = new Set(commonPairs.map(getPairKey)); + const savedPairs = (state.ttsFilterPairs || []).map(item => ({ ...item })); + const draftPairs = savedPairs.filter(pair => !commonPairKeys.has(getPairKey(pair))); + const draftSingles = [...new Set((state.ttsFilterSingles || []).map(item => String(item || '').trim()).filter(Boolean))]; const modal = createModal({ title: '语音合成播报过滤设置', @@ -1431,7 +3403,7 @@ function openTtsFilterModal(state, rerender) {
${commonPairs.map(item => `