From a83490f29cce4a1fcce09c9d37173685da2d0bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:56:18 +0800 Subject: [PATCH] Add files via upload --- config.example.yaml | 47 +- web/static/css/c2.css | 1626 +++++++++++++++++++++++++++++------- web/static/css/style.css | 1390 +++++++++++++++++++++++++++++- web/static/i18n/en-US.json | 62 +- web/static/i18n/zh-CN.json | 62 +- web/static/js/assets.js | 2 +- web/static/js/c2.js | 681 +++++++++++++-- web/static/js/chat.js | 364 +++++++- web/static/js/dashboard.js | 18 +- web/static/js/hitl.js | 23 + web/static/js/monitor.js | 374 ++++++++- web/static/js/projects.js | 7 +- web/static/js/settings.js | 596 +++++++++++-- web/static/js/webshell.js | 213 ++++- web/templates/index.html | 305 ++++--- 15 files changed, 5102 insertions(+), 668 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index eedaf379..b9e51ee0 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -49,35 +49,40 @@ monitor: # 对话相关配置 # ============================================ -# AI 模型配置(支持 OpenAI 兼容 API) -# 必填项:api_key, base_url, model 必须填写才能正常运行 +# AI 通道配置(支持保存多个 OpenAI 兼容 / Claude 通道) +# default_channel 指定新对话与未显式选择通道的任务使用哪个通道。 +# 每个 channels. 必填:api_key, base_url, model。 # 支持的 API 服务商: # - OpenAI: https://api.openai.com/v1 # - DeepSeek: https://api.deepseek.com/v1 # - 其他兼容 OpenAI 协议的 API # 常用模型: gpt-4, gpt-3.5-turbo, deepseek-chat, claude-3-opus 等 -# provider: 可选值 openai(默认) | claude(自动桥接到 Anthropic Claude Messages API) -openai: - provider: openai # API 提供商: openai(默认,兼容OpenAI协议) | claude(自动桥接到Anthropic Claude Messages API) - base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 # API 基础 URL(必填) - api_key: sk-xxxxxxx # API 密钥(必填) - model: qwen3-max # 模型名称(必填) - max_total_tokens: 120000 # LLM 相关上下文的最大 Token 数限制(内存压缩和攻击链构建会共用此配置) - max_completion_tokens: 16384 # 单次生成上限(含 reasoning 与可见输出),防止依赖网关隐式默认值 - # Eino 路径模型推理:DeepSeek/OpenAI 为 thinking / reasoning_effort;Claude 4.6+ 为 adaptive + output_config.effort(仅显式配置 effort 时下发);3.7 为 enabled+budget_tokens:10000(文档示例),effort 不映射,自定义预算用 extra_request_fields - reasoning: - mode: on # auto | on | off;off:OpenAI/Claude 不附加推理字段,DeepSeek 发送 thinking.type=disabled(其默认开启思考) - effort: high # low | medium | high | max | xhigh(最高档:OpenAI 常用 xhigh,部分网关用 max,原样下发);空表示不指定 - allow_client_reasoning: true # false 时忽略对话请求体 reasoning,仅以下方为准 - profile: openai_compat # auto | deepseek_compat | openai_compat | output_config_effort - # extra_request_fields: {} # 可选:管理员自定义根级 JSON 片段(高级) +# provider: 可选值 openai_compatible(默认) | claude(自动桥接到 Anthropic Claude Messages API) +ai: + default_channel: qwen-max + channels: + qwen-max: + name: Qwen Max + provider: openai_compatible + base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 + api_key: sk-xxxxxxx + model: qwen3-max + max_total_tokens: 120000 + max_completion_tokens: 16384 + # Eino 路径模型推理:DeepSeek/OpenAI 为 thinking / reasoning_effort;Claude 4.6+ 为 adaptive + output_config.effort(仅显式配置 effort 时下发);3.7 为 enabled+budget_tokens:10000(文档示例),effort 不映射,自定义预算用 extra_request_fields + reasoning: + mode: on # auto | on | off;off:OpenAI/Claude 不附加推理字段,DeepSeek 发送 thinking.type=disabled(其默认开启思考) + effort: high # low | medium | high | max | xhigh(最高档:OpenAI 常用 xhigh,部分网关用 max,原样下发);空表示不指定 + allow_client_reasoning: true # false 时忽略对话请求体 reasoning,仅以下方为准 + profile: openai_compat # auto | deepseek_compat | openai_compat | output_config_effort + # extra_request_fields: {} # 可选:管理员自定义根级 JSON 片段(高级) # 视觉分析(analyze_image MCP 工具;图片仅在单次 VL 调用中出现,Agent 上下文只保留文字摘要) vision: enabled: false # true 且 model 非空时注册 analyze_image model: qwen-vl # VL 模型名(enabled 时必填) - api_key: "" # 留空则复用 openai.api_key - base_url: "" # 留空则复用 openai.base_url - provider: # 留空则复用 openai.provider(openai | claude) + api_key: "" # 留空则复用默认 AI 通道 api_key + base_url: "" # 留空则复用默认 AI 通道 base_url + provider: # 留空则复用默认 AI 通道 provider(openai_compatible | claude) max_image_bytes: 5242880 # 原始文件上限(字节),默认 5MB max_dimension: 2048 # 长边缩放像素 jpeg_quality: 82 @@ -110,7 +115,7 @@ agent: tool_wait_timeout_seconds: 300 # 工具本轮最多等待(秒);到时返回 execution_id,worker 继续后台执行,可用 wait_tool_execution 继续等待;0=等到完成 external_mcp_max_concurrent_per_server: 5 # 单个外部 MCP server 同时运行的工具数;0=默认2;负数=不限制 external_mcp_max_concurrent_total: 16 # 所有外部 MCP 工具全局并发上限;0=默认16;负数=不限制 - external_mcp_circuit_failure_threshold: 5 # 单个外部 MCP server 连续失败多少次后熔断;0=默认3;负数=关闭熔断 + external_mcp_circuit_failure_threshold: 15 # 单个外部 MCP server 连续失败多少次后熔断;0=默认3;负数=关闭熔断 external_mcp_circuit_cooldown_seconds: 60 # 熔断冷却秒数;0=默认60 shell_no_output_timeout_seconds: 1200 # execute/exec 连续无新输出则终止(秒);通用防挂死;0=默认300;-1=关闭 workspace_root_dir: "" # 会话工作目录根路径(curl/wget 下载、read_file/glob/grep 本地分析);空=tmp/workspace,其下按 projects/{id} 或 conversations/{id} 隔离;勿用系统 /tmp diff --git a/web/static/css/c2.css b/web/static/css/c2.css index 8c8575fe..16260ae6 100644 --- a/web/static/css/c2.css +++ b/web/static/css/c2.css @@ -46,13 +46,13 @@ html[data-theme="dark"] { --c2-amber-dim: rgba(251, 191, 36, 0.14); --c2-purple: #a78bfa; --c2-purple-dim: rgba(167, 139, 250, 0.14); - --c2-surface: #111827; - --c2-surface-alt: #0b1120; - --c2-border: #263244; - --c2-border-hover: #3b4a63; - --c2-text: #e5e7eb; - --c2-text-dim: #a7b0c0; - --c2-text-muted: #6b7280; + --c2-surface: #131a27; + --c2-surface-alt: #0c1220; + --c2-border: #243044; + --c2-border-hover: #3a4b66; + --c2-text: #e8edf5; + --c2-text-dim: #9aa6b8; + --c2-text-muted: #6b778a; --c2-shadow-sm: 0 1px 3px rgba(0,0,0,0.34); --c2-shadow-md: 0 8px 24px rgba(0,0,0,0.38); --c2-shadow-lg: 0 18px 48px rgba(0,0,0,0.45); @@ -106,10 +106,77 @@ html[data-theme="dark"] .c2-session-main { html[data-theme="dark"] .c2-session-sidebar-wrap, html[data-theme="dark"] .c2-sessions-toolbar, html[data-theme="dark"] .c2-session-sidebar { - background: #0b1120 !important; + background: #0a101c !important; border-color: var(--c2-border) !important; } +html[data-theme="dark"] .c2-session-tabs--pills .c2-session-tab:hover { + background: rgba(96, 165, 250, 0.08); + color: var(--c2-text); +} + +html[data-theme="dark"] .c2-session-tabs--pills .c2-session-tab.active { + background: var(--c2-surface); + color: var(--c2-accent); + border-color: var(--c2-border); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35); +} + +html[data-theme="dark"] .c2-session-avatar--darwin { + background: linear-gradient(145deg, #1e1b4b 0%, #312e81 100%); + color: #c7d2fe; + border-color: #4338ca; +} + +html[data-theme="dark"] .c2-session-avatar--windows { + background: linear-gradient(145deg, #0c1a3a 0%, #1e3a5f 100%); + color: #93c5fd; + border-color: #2563eb; +} + +html[data-theme="dark"] .c2-session-avatar--linux { + background: linear-gradient(145deg, #2a1f0a 0%, #78350f 100%); + color: #fcd34d; + border-color: #b45309; +} + +html[data-theme="dark"] .c2-status-badge.queued, +html[data-theme="dark"] .c2-status-badge.cancelled { + background: rgba(148, 163, 184, 0.12); + color: #94a3b8; +} + +html[data-theme="dark"] .c2-status-badge.success { + color: var(--c2-green); +} + +html[data-theme="dark"] .c2-status-badge.running { + color: var(--c2-amber); +} + +html[data-theme="dark"] .c2-task-type-badge--default { + background: rgba(148, 163, 184, 0.12); + color: var(--c2-text-dim); +} + +html[data-theme="dark"] .c2-task-type-badge--control { + color: var(--c2-amber); +} + +html[data-theme="dark"] #page-c2-sessions .page-header-actions .btn-danger:disabled { + opacity: 0.45; +} + +html[data-theme="dark"] .c2-file-icon--dir { + background: rgba(96, 165, 250, 0.12); + color: #93c5fd; +} + +html[data-theme="dark"] .c2-file-icon--file { + background: rgba(148, 163, 184, 0.1); + color: #94a3b8; +} + html[data-theme="dark"] .c2-session-main-empty { background: transparent !important; color: var(--c2-text-dim) !important; @@ -653,6 +720,24 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { word-break: break-all; } +.c2-project-bind-select { + width: min(100%, 220px); + min-height: 28px; + padding: 4px 28px 4px 9px; + border-radius: 6px; + border: 1px solid var(--c2-border); + background: var(--c2-surface); + color: var(--c2-text); + font-size: 12px; + font-weight: 600; +} + +.c2-project-bind-select:focus { + outline: none; + border-color: var(--c2-accent); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.14); +} + .c2-listener-mono { font-family: var(--c2-mono); font-size: 12px; @@ -760,9 +845,9 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { } .c2-session-sidebar-wrap { - width: 300px; - min-width: 260px; - max-width: 340px; + width: 320px; + min-width: 280px; + max-width: 360px; flex-shrink: 0; display: flex; flex-direction: column; @@ -772,40 +857,41 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { .c2-sessions-toolbar { flex-shrink: 0; - padding: 8px 10px; + padding: 12px 12px 10px; border-bottom: 1px solid var(--c2-border); display: flex; flex-direction: column; - gap: 6px; + gap: 8px; } .c2-sessions-filter-row { display: flex; - gap: 6px; + gap: 8px; } .c2-sessions-toolbar .form-control { width: 100%; min-width: 0; - font-size: 12px; - padding: 5px 8px; - line-height: 1.3; + font-size: 12.5px; + padding: 7px 10px; + line-height: 1.35; + border-radius: var(--c2-radius-xs); } .c2-sessions-toolbar-meta { display: flex; align-items: center; - gap: 6px; - font-size: 11px; + gap: 8px; + font-size: 12px; color: var(--c2-text-dim); - min-height: 18px; + min-height: 22px; flex-wrap: wrap; } .c2-sessions-select-all-label { display: inline-flex; align-items: center; - gap: 4px; + gap: 6px; cursor: pointer; user-select: none; flex-shrink: 0; @@ -813,7 +899,7 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { .c2-sessions-quick-links { display: inline-flex; - gap: 6px; + gap: 8px; margin-left: auto; flex-shrink: 0; } @@ -822,7 +908,7 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { border: none; background: none; padding: 0; - font-size: 11px; + font-size: 12px; color: var(--c2-accent); cursor: pointer; } @@ -832,7 +918,7 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { } .c2-sessions-count { - font-size: 11px; + font-size: 12px; color: var(--c2-text-muted); white-space: nowrap; } @@ -841,15 +927,15 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { flex: 1; min-height: 0; overflow-y: auto; - padding: 8px 10px; + padding: 10px 12px 14px; } .c2-session-list-empty { - padding: 20px 8px; + padding: 28px 12px; text-align: center; - font-size: 12px; + font-size: 13px; color: var(--c2-text-dim); - line-height: 1.5; + line-height: 1.55; } .c2-session-main { @@ -857,8 +943,8 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { min-width: 0; min-height: 0; overflow: hidden; - padding: 12px 16px 16px; - background: linear-gradient(180deg, var(--c2-surface-alt) 0%, var(--c2-surface) 180px); + padding: 14px 18px 18px; + background: linear-gradient(180deg, var(--c2-surface-alt) 0%, var(--c2-surface) 200px); display: flex; flex-direction: column; } @@ -871,6 +957,7 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { min-height: 0; display: flex; flex-direction: column; + gap: 0; } .c2-session-header-bar { @@ -888,13 +975,13 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { display: flex; align-items: center; justify-content: space-between; - gap: 12px; - padding: 10px 12px; + gap: 16px; + padding: 12px 14px; margin-bottom: 10px; border: 1px solid var(--c2-border); - border-radius: var(--c2-radius-xs); + border-radius: var(--c2-radius-sm); background: var(--c2-surface); - box-shadow: none; + box-shadow: var(--c2-shadow-sm); flex-shrink: 0; } @@ -920,36 +1007,37 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { } .c2-session-hero__title { - font-size: 16px; - font-weight: 800; + font-size: 17px; + font-weight: 700; color: var(--c2-text); margin: 0; - line-height: 1.2; + line-height: 1.25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; + letter-spacing: -0.01em; } .c2-session-hero__sub { - font-size: 11px; + font-size: 12px; color: var(--c2-text-dim); font-family: var(--c2-mono); - margin-bottom: 4px; + margin-bottom: 6px; } .c2-session-hero__chips { display: flex; flex-wrap: wrap; - gap: 4px; + gap: 6px; } .c2-session-hero-chip { - font-size: 10px; + font-size: 11px; font-weight: 600; - padding: 1px 7px; + padding: 2px 8px; border-radius: 999px; - background: var(--c2-surface); + background: var(--c2-surface-alt); border: 1px solid var(--c2-border); color: var(--c2-text-dim); max-width: 100%; @@ -960,10 +1048,10 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { .c2-session-hero-chip.is-mono { font-family: var(--c2-mono); - font-size: 10px; + font-size: 11px; color: var(--c2-accent); background: var(--c2-accent-dim); - border-color: rgba(59, 130, 246, 0.2); + border-color: rgba(59, 130, 246, 0.22); } .c2-session-hero-root { @@ -977,13 +1065,13 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { } .c2-session-avatar { - width: 36px; - height: 36px; - border-radius: 10px; + width: 42px; + height: 42px; + border-radius: 12px; display: flex; align-items: center; justify-content: center; - font-size: 10px; + font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: -0.03em; @@ -1027,11 +1115,11 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { display: flex; align-items: center; gap: 6px; - padding: 3px 8px; + padding: 6px 10px; border-radius: 999px; background: var(--c2-surface-alt); border: 1px solid var(--c2-border); - font-size: 10px; + font-size: 11px; } .c2-session-hero__heartbeat-label { @@ -1105,14 +1193,14 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { max-width: 320px; } -.c2-session-list { display: flex; flex-direction: column; gap: 6px; } +.c2-session-list { display: flex; flex-direction: column; gap: 8px; } .c2-session-item { position: relative; padding: 0; border-radius: 10px; cursor: pointer; - transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease, transform 0.15s ease; border: 1px solid var(--c2-border); background: var(--c2-surface); overflow: hidden; @@ -1122,8 +1210,8 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { content: ''; position: absolute; left: 0; - top: 10px; - bottom: 10px; + top: 12px; + bottom: 12px; width: 3px; border-radius: 0 3px 3px 0; background: var(--c2-border); @@ -1136,14 +1224,14 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { .c2-session-item-check { position: absolute; - left: 12px; - top: 14px; + left: 14px; + top: 16px; cursor: pointer; z-index: 1; } .c2-session-item-body { - padding: 9px 10px 8px 34px; + padding: 12px 12px 10px 36px; } .c2-session-item:hover { @@ -1152,9 +1240,9 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { } .c2-session-item.active { - background: linear-gradient(135deg, rgba(59, 130, 246, 0.06) 0%, rgba(59, 130, 246, 0.02) 100%); - border-color: var(--c2-accent); - box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.15), var(--c2-shadow-sm); + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.03) 100%); + border-color: rgba(59, 130, 246, 0.45); + box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.12), var(--c2-shadow-sm); } .c2-session-header { @@ -1197,7 +1285,7 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { .c2-session-host { font-weight: 700; - font-size: 13px; + font-size: 13.5px; color: var(--c2-text); line-height: 1.35; overflow: hidden; @@ -1207,7 +1295,7 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { .c2-session-status { font-size: 10px; - padding: 2px 8px; + padding: 3px 8px; border-radius: 999px; text-transform: uppercase; letter-spacing: 0.4px; @@ -1222,16 +1310,16 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { .c2-session-chips { display: flex; flex-wrap: wrap; - gap: 4px; - margin-top: 6px; + gap: 5px; + margin-top: 8px; } -.c2-session-chips--sub { margin-top: 4px; } +.c2-session-chips--sub { margin-top: 5px; } .c2-session-chip { - font-size: 10px; + font-size: 11px; font-weight: 600; - padding: 2px 7px; + padding: 2px 8px; border-radius: 999px; background: var(--c2-surface-alt); color: var(--c2-text-dim); @@ -1264,6 +1352,17 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { padding-right: 0; } +.c2-session-chip--empty { + opacity: 0.55; + font-style: italic; + font-weight: 500; +} + +.c2-session-hero__sub.is-muted { + color: var(--c2-text-muted); + font-style: italic; +} + .c2-session-meta { font-size: 12px; color: var(--c2-text-dim); @@ -1275,8 +1374,8 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { justify-content: space-between; align-items: center; gap: 8px; - margin-top: 6px; - min-height: 22px; + margin-top: 8px; + min-height: 24px; } .c2-session-item-time { @@ -1290,13 +1389,20 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { flex-shrink: 0; font-size: 11px; font-weight: 600; - padding: 3px 8px; + padding: 4px 9px; border-radius: var(--c2-radius-xs); - border: 1px solid var(--c2-border); - background: var(--c2-surface-alt); - color: var(--c2-text-dim); + border: 1px solid transparent; + background: transparent; + color: var(--c2-text-muted); cursor: pointer; - transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease; + opacity: 0; + transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease, opacity 0.15s ease; +} + +.c2-session-item:hover .c2-session-card-delete, +.c2-session-item.active .c2-session-card-delete, +.c2-session-item:focus-within .c2-session-card-delete { + opacity: 1; } .c2-session-card-delete:hover { @@ -1306,7 +1412,7 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { } .c2-session-item.active .c2-session-card-delete { - background: var(--c2-surface); + background: transparent; } .c2-session-title h3 { @@ -1337,7 +1443,7 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { font-family: var(--c2-mono); } -.c2-session-actions { display: flex; gap: 8px; } +.c2-session-actions { display: flex; gap: 8px; align-items: center; } /* Session Tabs */ .c2-session-tabs { @@ -1350,28 +1456,33 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { } .c2-session-tabs--pills { - gap: 4px; + display: inline-flex; + align-self: flex-start; + gap: 2px; padding: 3px; - margin-bottom: 10px; + margin-bottom: 14px; border: 1px solid var(--c2-border); - border-radius: var(--c2-radius-sm); + border-radius: 10px; background: var(--c2-surface-alt); - border-bottom: 1px solid var(--c2-border); + width: auto; + max-width: 100%; } .c2-session-tabs--pills .c2-session-tab { - flex: 1; + flex: 0 0 auto; text-align: center; - padding: 6px 10px; - border-radius: var(--c2-radius-xs); + padding: 7px 16px; + border-radius: 7px; margin-bottom: 0; border: 1px solid transparent; background: transparent; + font-size: 13px; + font-weight: 600; transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; } .c2-session-tabs--pills .c2-session-tab:hover { - background: rgba(255, 255, 255, 0.7); + background: rgba(15, 23, 42, 0.04); color: var(--c2-text); } @@ -1416,12 +1527,113 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { overflow-y: auto; } +/* ============================================================================ + Terminal + ============================================================================ */ + +.c2-terminal-container { + background: #0b1220; + border-radius: var(--c2-radius-sm) var(--c2-radius-sm) 0 0; + flex: 1; + min-height: 200px; + overflow: hidden; + position: relative; + border: 1px solid var(--c2-border); + border-bottom: none; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); +} + +.c2-terminal-container .xterm { + height: 100%; + padding: 10px 10px 6px; + box-sizing: border-box; +} + +.c2-terminal-container .xterm-viewport { + overflow-y: auto !important; + scrollbar-width: thin; + scrollbar-color: transparent transparent; + transition: scrollbar-color 0.2s ease; +} + +.c2-terminal-container .xterm-viewport::-webkit-scrollbar { + width: 8px; +} + +.c2-terminal-container .xterm-viewport::-webkit-scrollbar-track { + background: transparent; +} + +.c2-terminal-container .xterm-viewport::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 8px; + border: 2px solid transparent; + background-clip: padding-box; + transition: background 0.2s ease; +} + +/* 悬停或滚动中才显示滑块 */ +.c2-terminal-container:hover .xterm-viewport, +.c2-terminal-container.is-scrollbar-visible .xterm-viewport { + scrollbar-color: rgba(148, 163, 184, 0.45) transparent; +} + +.c2-terminal-container:hover .xterm-viewport::-webkit-scrollbar-thumb, +.c2-terminal-container.is-scrollbar-visible .xterm-viewport::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.45); + background-clip: padding-box; +} + +.c2-terminal-container:hover .xterm-viewport::-webkit-scrollbar-thumb:hover, +.c2-terminal-container.is-scrollbar-visible .xterm-viewport::-webkit-scrollbar-thumb:hover { + background: rgba(148, 163, 184, 0.7); + background-clip: padding-box; +} + +.c2-terminal-toolbar { + flex-shrink: 0; + display: flex; + gap: 8px; + padding: 10px 14px; + background: linear-gradient(180deg, #152033 0%, #111827 100%); + border-radius: 0 0 var(--c2-radius-sm) var(--c2-radius-sm); + align-items: center; + border: 1px solid var(--c2-border); + border-top: 1px solid rgba(51, 65, 85, 0.7); +} + +.c2-terminal-toolbar .btn-ghost { + color: #94a3b8; + border-color: rgba(148, 163, 184, 0.22); + background: rgba(15, 23, 42, 0.35); +} + +.c2-terminal-toolbar .btn-ghost:hover { + color: #e2e8f0; + border-color: rgba(148, 163, 184, 0.4); + background: rgba(30, 41, 59, 0.7); +} + +.c2-terminal-status { + margin-left: auto; + font-size: 11px; + color: #64748b; + font-family: var(--c2-mono); +} + +/* ============================================================================ + File Manager + ============================================================================ */ + .c2-file-panel { border: 1px solid var(--c2-border); border-radius: var(--c2-radius-sm); background: var(--c2-surface); box-shadow: var(--c2-shadow-sm); overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; } .c2-file-panel .c2-file-toolbar { @@ -1429,10 +1641,247 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { border-radius: 0; border: none; border-bottom: 1px solid var(--c2-border); + padding: 10px 12px; + gap: 8px; + background: linear-gradient(180deg, var(--c2-surface-alt) 0%, var(--c2-surface) 100%); +} + +.c2-file-panel .c2-file-upload-hint { + margin: 0; + border-radius: 0; + border-left: none; + border-right: none; + border-top: none; +} + +.c2-file-panel .c2-file-upload-progress { + margin: 0; + padding: 10px 12px; + border-bottom: 1px solid var(--c2-border); } .c2-file-panel .c2-file-list { - padding: 0 8px 8px; + padding: 0; + border: none; + border-radius: 0; + background: transparent; + overflow: auto; + max-height: min(62vh, 720px); +} + +.c2-file-toolbar { + display: flex; + gap: 8px; + padding: 12px 16px; + background: var(--c2-surface-alt); + border-radius: var(--c2-radius-sm); + margin-bottom: 16px; + align-items: center; + border: 1px solid var(--c2-border); +} + +.c2-path-breadcrumb { + font-family: var(--c2-mono); + font-size: 12px; + color: var(--c2-text-dim); + margin-left: auto; + background: var(--c2-surface); + padding: 6px 12px; + border-radius: var(--c2-radius-xs); + border: 1px solid var(--c2-border); + max-width: min(52vw, 560px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#c2-file-upload-btn.is-disabled, +#c2-file-upload-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + color: var(--c2-text-dim, #94a3b8); + border-color: var(--c2-border, #e2e8f0); +} + +.c2-file-upload-hint { + font-size: 12px; + color: #b45309; + background: rgba(245, 158, 11, 0.08); + border: 1px solid rgba(245, 158, 11, 0.25); + border-radius: var(--c2-radius-xs, 4px); + padding: 10px 14px; + margin: -8px 0 12px; + line-height: 1.55; + word-break: break-word; +} + +html[data-theme="dark"] .c2-file-upload-hint { + color: #fbbf24; + background: rgba(251, 191, 36, 0.1); + border-color: rgba(251, 191, 36, 0.28); +} + +.c2-file-upload-hint[hidden] { + display: none !important; +} + +.c2-file-upload-progress { + display: flex; + align-items: center; + gap: 10px; + margin: -8px 0 12px; + padding: 0 4px; +} + +.c2-file-upload-progress[hidden] { + display: none !important; +} + +.c2-file-upload-progress-track { + flex: 1; + height: 4px; + background: var(--c2-border); + border-radius: 2px; + overflow: hidden; +} + +.c2-file-upload-progress-fill { + height: 100%; + width: 0; + background: var(--c2-accent, #3b82f6); + transition: width 0.2s ease; +} + +.c2-file-upload-progress-label { + font-size: 11px; + color: var(--c2-text-dim); + white-space: nowrap; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; +} + +.c2-file-list { + background: var(--c2-surface); + border-radius: var(--c2-radius); + border: 1px solid var(--c2-border); + overflow: hidden; +} + +.c2-file-table { width: 100%; border-collapse: collapse; } + +.c2-file-table th { + text-align: left; + padding: 11px 16px; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--c2-text-muted); + background: var(--c2-surface-alt); + font-weight: 700; + border-bottom: 1px solid var(--c2-border); + position: sticky; + top: 0; + z-index: 1; +} + +.c2-file-table td { + padding: 11px 16px; + border-top: 1px solid transparent; + border-bottom: 1px solid var(--c2-border); + font-size: 13px; + vertical-align: middle; +} + +.c2-file-table tbody tr:last-child td { + border-bottom: none; +} + +.c2-file-table tbody tr:hover td { + background: var(--c2-accent-dim); +} + +.c2-file-table td:nth-child(2), +.c2-file-table th:nth-child(2) { + width: 100px; + font-variant-numeric: tabular-nums; + font-family: var(--c2-mono); + font-size: 12px; + color: var(--c2-text-dim); + white-space: nowrap; +} + +.c2-file-table td:nth-child(3), +.c2-file-table th:nth-child(3) { + width: 120px; + font-family: var(--c2-mono); + font-size: 12px; + color: var(--c2-text-muted); + white-space: nowrap; +} + +.c2-file-table td:nth-child(4), +.c2-file-table th:nth-child(4) { + width: 1%; + white-space: nowrap; + text-align: right; +} + +.c2-file-name { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + font-weight: 600; + color: var(--c2-text); +} + +.c2-file-name-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c2-file-icon { + width: 28px; + height: 28px; + border-radius: 7px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 0; + position: relative; +} + +.c2-file-icon--dir { + background: rgba(59, 130, 246, 0.1); + color: #3b82f6; +} + +.c2-file-icon--file { + background: rgba(100, 116, 139, 0.1); + color: #64748b; +} + +.c2-file-icon::before { + content: ''; + width: 12px; + height: 10px; + border: 1.6px solid currentColor; + border-radius: 2px; + opacity: 0.9; +} + +.c2-file-icon--dir::before { + width: 14px; + height: 10px; + border-radius: 2px 2px 2px 2px; + border-top-width: 3px; +} + +.c2-file-action-btn { + min-width: 64px; } #c2-session-tasks-list { @@ -1487,147 +1936,104 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { } /* ============================================================================ - Terminal + Task List — aligned with Events audit page ============================================================================ */ -.c2-terminal-container { - background: #0d1117; - border-radius: var(--c2-radius) var(--c2-radius) 0 0; +.c2-tasks-page-wrap { + display: flex; + flex-direction: column; + gap: 16px; + min-height: 0; flex: 1; - min-height: 200px; - overflow: hidden; - position: relative; - border: 1.5px solid #1e293b; - border-bottom: none; } -.c2-terminal-container .xterm { - height: 100%; - padding: 4px 6px; - box-sizing: border-box; -} - -.c2-terminal-container .xterm-viewport { - overflow-y: auto !important; -} - -.c2-terminal-toolbar { - flex-shrink: 0; +.c2-tasks-panel { display: flex; - gap: 8px; - padding: 10px 16px; - background: #1e293b; - border-radius: 0 0 var(--c2-radius) var(--c2-radius); - align-items: center; - border: 1.5px solid #1e293b; - border-top: none; -} - -.c2-terminal-status { - margin-left: auto; - font-size: 11px; - color: #64748b; - font-family: var(--c2-mono); -} - -/* ============================================================================ - File Manager - ============================================================================ */ - -.c2-file-toolbar { - display: flex; - gap: 8px; - padding: 12px 16px; - background: var(--c2-surface-alt); - border-radius: var(--c2-radius-sm); - margin-bottom: 16px; - align-items: center; - border: 1px solid var(--c2-border); -} - -.c2-path-breadcrumb { - font-family: var(--c2-mono); - font-size: 12px; - color: var(--c2-text-dim); - margin-left: auto; - background: var(--c2-surface); - padding: 5px 12px; - border-radius: var(--c2-radius-xs); - border: 1px solid var(--c2-border); -} - -#c2-file-upload-btn.is-disabled, -#c2-file-upload-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - color: var(--c2-text-dim, #94a3b8); - border-color: var(--c2-border, #e2e8f0); -} - -.c2-file-upload-hint { - font-size: 12px; - color: #b45309; - background: rgba(245, 158, 11, 0.08); - border: 1px solid rgba(245, 158, 11, 0.25); - border-radius: var(--c2-radius-xs, 4px); - padding: 8px 12px; - margin: -8px 0 12px; - line-height: 1.5; - word-break: break-word; -} - -.c2-file-upload-hint[hidden] { - display: none !important; -} - -.c2-file-upload-progress { - display: flex; - align-items: center; - gap: 10px; - margin: -8px 0 12px; - padding: 0 4px; -} - -.c2-file-upload-progress[hidden] { - display: none !important; -} - -.c2-file-upload-progress-track { + flex-direction: column; + min-height: 0; flex: 1; - height: 4px; - background: var(--c2-border); - border-radius: 2px; - overflow: hidden; -} - -.c2-file-upload-progress-fill { - height: 100%; - width: 0; - background: var(--c2-accent, #3b82f6); - transition: width 0.2s ease; -} - -.c2-file-upload-progress-label { - font-size: 11px; - color: var(--c2-text-dim); - white-space: nowrap; - max-width: 220px; - overflow: hidden; - text-overflow: ellipsis; -} - -.c2-file-list { background: var(--c2-surface); + border: 1px solid var(--c2-border); border-radius: var(--c2-radius); - border: 1px solid var(--c2-border); + box-shadow: var(--c2-shadow-sm); overflow: hidden; } -.c2-file-table { width: 100%; border-collapse: collapse; } +.c2-tasks-list-container, +.c2-task-list-container { + flex: 1; + min-height: 0; + overflow-x: auto; + overflow-y: hidden; + background: transparent; + border: none; + border-radius: 0; + box-shadow: none; +} -.c2-file-table th { +.c2-tasks-empty { + padding: 48px 24px; + text-align: center; + color: var(--c2-text-dim); + font-size: 14px; +} + +.c2-tasks-table { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + border-collapse: separate; + border-spacing: 0; + table-layout: fixed; + min-width: 980px; +} + +.c2-tasks-table thead { + display: block; + flex: 0 0 auto; +} + +.c2-tasks-table tbody { + display: block; + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--c2-text-dim) 70%, transparent) transparent; +} + +.c2-tasks-table tr { + display: table; + width: 100%; + table-layout: fixed; +} + +.c2-tasks-table tbody::-webkit-scrollbar { + width: 8px; +} + +.c2-tasks-table tbody::-webkit-scrollbar-track { + background: var(--c2-surface); +} + +.c2-tasks-table tbody::-webkit-scrollbar-thumb { + border: 2px solid transparent; + border-radius: 999px; + background: color-mix(in srgb, var(--c2-text-dim) 70%, transparent); + background-clip: content-box; +} + +.c2-tasks-table tbody::-webkit-scrollbar-thumb:hover { + background: color-mix(in srgb, var(--c2-text-muted) 78%, transparent); + background-clip: content-box; +} + +.c2-tasks-table thead th { + z-index: 2; text-align: left; - padding: 12px 18px; + padding: 12px 16px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; @@ -1635,57 +2041,229 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { background: var(--c2-surface-alt); font-weight: 700; border-bottom: 1px solid var(--c2-border); + white-space: nowrap; + box-shadow: 0 1px 0 var(--c2-border); } -.c2-file-table td { - padding: 10px 18px; +.c2-tasks-table th:nth-child(1), +.c2-tasks-table td:nth-child(1) { width: 44px; } +.c2-tasks-table th:nth-child(2), +.c2-tasks-table td:nth-child(2) { width: 180px; } +.c2-tasks-table th:nth-child(3), +.c2-tasks-table td:nth-child(3) { width: 100px; } +.c2-tasks-table th:nth-child(4), +.c2-tasks-table td:nth-child(4) { width: 110px; } +.c2-tasks-table th:nth-child(5), +.c2-tasks-table td:nth-child(5) { width: auto; } +.c2-tasks-table th:nth-child(6), +.c2-tasks-table td:nth-child(6) { width: 140px; } +.c2-tasks-table th:nth-child(7), +.c2-tasks-table td:nth-child(7) { width: 140px; } +.c2-tasks-table th:nth-child(8), +.c2-tasks-table td:nth-child(8) { width: 88px; } +.c2-tasks-table th:nth-child(9), +.c2-tasks-table td:nth-child(9) { width: 96px; } + +.c2-tasks-table td { + padding: 12px 16px; border-top: 1px solid var(--c2-border); font-size: 13px; + vertical-align: middle; + background: var(--c2-surface); } -.c2-file-table tbody tr:hover td { background: var(--c2-surface-alt); } - -.c2-file-name { display: flex; align-items: center; gap: 10px; } -.c2-file-icon { font-size: 16px; } - -/* ============================================================================ - Task List - ============================================================================ */ - -.c2-tasks-page-wrap { - display: flex; - flex-direction: column; - gap: 0; +.c2-tasks-table tbody tr:first-child td { + border-top: none; } -.c2-tasks-toolbar { +.c2-tasks-summary { display: flex; + flex-wrap: wrap; align-items: center; - gap: 12px; - padding: 8px 4px 10px; - border-bottom: 1px solid var(--c2-border); + gap: 10px; + flex-shrink: 0; } -.c2-tasks-select-all-label { +.c2-tasks-stat { display: inline-flex; align-items: center; gap: 8px; - font-size: 13px; + padding: 6px 12px; + border-radius: var(--c2-radius-sm); + background: var(--c2-surface); + border: 1px solid var(--c2-border); + font-size: 12px; color: var(--c2-text-muted); - cursor: pointer; - user-select: none; } -.c2-tasks-select-all-label input { - cursor: pointer; +.c2-tasks-stat strong { + font-size: 15px; + font-weight: 700; + color: var(--c2-text); + font-variant-numeric: tabular-nums; } +.c2-tasks-stat--success strong { color: var(--c2-green); } +.c2-tasks-stat--failed strong { color: var(--c2-red); } +.c2-tasks-stat--pending strong { color: var(--c2-amber); } + +.c2-tasks-filter-card { + display: flex; + flex-direction: column; + gap: 14px; + padding: 16px 18px; + background: var(--c2-surface); + border: 1px solid var(--c2-border); + border-radius: var(--c2-radius); + box-shadow: var(--c2-shadow-sm); + flex-shrink: 0; +} + +.c2-tasks-time-presets { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.c2-tasks-time-presets-label { + font-size: 12px; + font-weight: 600; + color: var(--c2-text-dim); + margin-right: 4px; +} + +.c2-tasks-time-preset-btn { + padding: 5px 12px; + border-radius: 999px; + border: 1px solid var(--c2-border); + background: var(--c2-surface-alt); + color: var(--c2-text-muted); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 0.12s, border-color 0.12s, color 0.12s; +} + +.c2-tasks-time-preset-btn:hover { + border-color: var(--c2-border-hover); + color: var(--c2-text); +} + +.c2-tasks-time-preset-btn.is-active { + background: var(--c2-accent-dim); + border-color: rgba(59, 130, 246, 0.45); + color: var(--c2-accent); + font-weight: 600; +} + +.c2-tasks-filter-fields { + display: grid; + grid-template-columns: minmax(148px, 0.9fr) minmax(148px, 0.9fr) minmax(180px, 1.4fr) auto; + gap: 12px 14px; + align-items: end; +} + +.c2-tasks-field { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.c2-tasks-field > span { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--c2-text-dim); +} + +#page-c2-tasks .c2-tasks-field .form-control { + width: 100%; + height: 36px; + min-height: 36px; + padding: 0 12px; + font-size: 13px; + line-height: 36px; + box-sizing: border-box; +} + +#page-c2-tasks .c2-tasks-field select.form-control { + min-width: 148px; + padding: 0 36px 0 12px; + line-height: 34px; + cursor: pointer; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 12px; +} + +.c2-tasks-filter-actions { + display: flex; + flex-wrap: nowrap; + align-items: center; + gap: 8px; + height: 36px; +} + +.c2-tasks-filter-actions .btn-small { + height: 36px; + min-height: 36px; + padding: 0 14px; + font-size: 13px; + line-height: 1; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; +} + +.c2-tasks-table-col-check, .c2-task-table-col-check { width: 44px; text-align: center; vertical-align: middle; } +.c2-tasks-table-col-actions { + width: 1%; + white-space: nowrap; + text-align: right; +} + +.c2-tasks-row { + cursor: pointer; + transition: background 0.12s ease; +} + +.c2-tasks-row:hover td { + background: var(--c2-surface-alt); +} + +.c2-tasks-row--failed td:first-child { + box-shadow: inset 3px 0 0 var(--c2-red); +} + +.c2-tasks-row--cancelled td:first-child { + box-shadow: inset 3px 0 0 var(--c2-text-dim); +} + +.c2-tasks-row--running td:first-child, +.c2-tasks-row--sent td:first-child { + box-shadow: inset 3px 0 0 var(--c2-amber); +} + +.c2-tasks-row--queued td:first-child { + box-shadow: inset 3px 0 0 #94a3b8; +} + +.c2-tasks-row--success td:first-child { + box-shadow: inset 3px 0 0 var(--c2-green); +} + .c2-task-check-label { display: flex; justify-content: center; @@ -1697,38 +2275,135 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { cursor: pointer; } -.c2-task-list-container { - background: var(--c2-surface); - border-radius: var(--c2-radius); - border: 1px solid var(--c2-border); - overflow-x: auto; - overflow-y: visible; -} - -/* 操作列:仅占按钮宽度,避免 100% 表格把余白摊到最右列 */ -.c2-task-table th.c2-task-table-col-actions, -.c2-task-table td.c2-task-table-col-actions { - width: 1%; +.c2-tasks-col-time { white-space: nowrap; - text-align: right; - vertical-align: middle; + color: var(--c2-text-muted); + font-size: 12px; + font-variant-numeric: tabular-nums; } -.c2-task-table-actions { +.c2-tasks-col-command { + max-width: 360px; + font-family: var(--c2-mono); + font-size: 12px; + font-weight: 500; + color: var(--c2-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c2-tasks-col-mono { + font-family: var(--c2-mono); + font-size: 12px; + color: var(--c2-text-muted); + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c2-tasks-col-duration { + font-variant-numeric: tabular-nums; + color: var(--c2-text-muted); + font-size: 12px; + white-space: nowrap; +} + +.c2-tasks-cell-muted { + color: var(--c2-text-dim); +} + +.c2-tasks-row-actions { display: inline-flex; align-items: center; justify-content: flex-end; - gap: 6px; - flex-wrap: nowrap; + gap: 4px; } -.c2-task-table-actions .btn-small, -.c2-task-table-actions .btn-sm { - min-height: 30px; - min-width: 52px; +.c2-tasks-cancel-btn { + display: inline-flex; + align-items: center; justify-content: center; + height: 28px; + padding: 0 8px; + border: 1px solid transparent; + border-radius: var(--c2-radius-xs); + background: transparent; + color: var(--c2-amber); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: background 0.12s, color 0.12s, border-color 0.12s; } +.c2-tasks-cancel-btn:hover { + background: var(--c2-amber-dim); + border-color: rgba(245, 158, 11, 0.3); +} + +.c2-tasks-delete-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid transparent; + border-radius: var(--c2-radius-xs); + background: transparent; + color: var(--c2-text-dim); + cursor: pointer; + transition: background 0.12s, color 0.12s, border-color 0.12s; +} + +.c2-tasks-delete-btn:hover { + background: var(--c2-red-dim); + border-color: rgba(239, 68, 68, 0.25); + color: var(--c2-red); +} + +#page-c2-tasks .page-content.c2-tasks-page-wrap { + display: flex; + flex-direction: column; + min-height: 0; + flex: 1; + overflow: hidden; +} + +#page-c2-tasks #c2-tasks-pagination { + flex-shrink: 0; + margin: 0; +} + +#page-c2-tasks #c2-tasks-pagination .monitor-pagination { + margin-top: 0; + border: none; + border-top: 1px solid var(--c2-border); + border-radius: 0; + background: var(--c2-surface-alt); + box-shadow: none; +} + +#page-c2-tasks #c2-tasks-pagination .pagination-page-size select { + border-radius: var(--c2-radius-xs); +} + +#page-c2-tasks #c2-tasks-pagination .pagination-controls .btn-secondary { + border-radius: var(--c2-radius-xs); +} + +@media (max-width: 900px) { + .c2-tasks-filter-fields { + grid-template-columns: 1fr; + } + + .c2-tasks-filter-actions { + justify-content: flex-start; + } +} + +/* Legacy task table helpers */ .c2-task-table { width: 100%; border-collapse: collapse; table-layout: auto; } .c2-task-table th { @@ -1845,7 +2520,7 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { align-items: center; justify-content: space-between; gap: 16px; - padding: 10px 12px; + padding: 12px 14px; border: 1px solid var(--c2-border); border-radius: var(--c2-radius-xs); background: var(--c2-surface); @@ -2751,73 +3426,222 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { .c2-session-info-panel { display: flex; flex-direction: column; - gap: 8px; + gap: 12px; padding: 0; - max-width: 920px; + max-width: none; + width: 100%; +} + +.c2-session-info-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + align-items: stretch; } .c2-session-info-block { border: 1px solid var(--c2-border); - border-radius: var(--c2-radius-xs); + border-radius: var(--c2-radius-sm); background: var(--c2-surface); overflow: hidden; + box-shadow: var(--c2-shadow-sm); + display: flex; + flex-direction: column; + min-height: 0; +} + +.c2-session-info-block--note { + flex: 0 0 auto; +} + +.c2-session-info-block--note.is-empty { + border-style: dashed; + box-shadow: none; + background: transparent; } .c2-session-info-block__head { - padding: 5px 10px; - font-size: 11px; + display: flex; + align-items: center; + gap: 8px; + padding: 11px 14px; + font-size: 12px; font-weight: 700; - letter-spacing: 0.03em; - color: var(--c2-text-dim); - background: var(--c2-surface-alt); + letter-spacing: 0.02em; + color: var(--c2-text); + background: linear-gradient(180deg, var(--c2-surface-alt) 0%, var(--c2-surface) 100%); border-bottom: 1px solid var(--c2-border); } +.c2-session-info-block__icon { + width: 18px; + height: 18px; + border-radius: 5px; + flex-shrink: 0; + position: relative; + background: var(--c2-accent-dim); +} + +.c2-session-info-block__icon::before, +.c2-session-info-block__icon::after { + content: ''; + position: absolute; + background: var(--c2-accent); + opacity: 0.75; + border-radius: 1px; +} + +.c2-session-info-block__icon--id::before { + left: 4px; + right: 4px; + top: 5px; + height: 2px; +} + +.c2-session-info-block__icon--id::after { + left: 4px; + width: 7px; + top: 10px; + height: 2px; +} + +.c2-session-info-block__icon--sys { + background: var(--c2-purple-dim); +} + +.c2-session-info-block__icon--sys::before, +.c2-session-info-block__icon--sys::after { + background: var(--c2-purple); +} + +.c2-session-info-block__icon--sys::before { + left: 5px; + top: 4px; + width: 8px; + height: 8px; + border-radius: 2px; + background: transparent; + border: 1.5px solid var(--c2-purple); + opacity: 0.9; +} + +.c2-session-info-block__icon--sys::after { + left: 7px; + top: 12px; + width: 4px; + height: 2px; +} + +.c2-session-info-block__icon--net { + background: var(--c2-green-dim); +} + +.c2-session-info-block__icon--net::before, +.c2-session-info-block__icon--net::after { + background: var(--c2-green); +} + +.c2-session-info-block__icon--net::before { + left: 4px; + top: 8px; + width: 10px; + height: 2px; + border-radius: 1px; +} + +.c2-session-info-block__icon--net::after { + left: 7px; + top: 5px; + width: 4px; + height: 4px; + border-radius: 50%; +} + +.c2-session-info-block__icon--note { + background: var(--c2-amber-dim); +} + +.c2-session-info-block__icon--note::before, +.c2-session-info-block__icon--note::after { + background: var(--c2-amber); +} + +.c2-session-info-block__icon--note::before { + left: 5px; + top: 4px; + width: 8px; + height: 9px; + border-radius: 1px; + background: transparent; + border: 1.5px solid var(--c2-amber); + opacity: 0.9; +} + +.c2-session-info-block__icon--note::after { + left: 7px; + top: 7px; + width: 4px; + height: 1.5px; +} + .c2-session-info-dl { margin: 0; display: flex; flex-direction: column; + flex: 1; + padding: 2px 0 4px; } .c2-session-info-dl__row { display: grid; - grid-template-columns: 100px minmax(0, 1fr); + grid-template-columns: 92px minmax(0, 1fr); gap: 10px; align-items: center; - padding: 4px 10px; - border-bottom: 1px solid var(--c2-border); - min-height: 26px; -} - -.c2-session-info-dl__row--full { - grid-template-columns: 100px minmax(0, 1fr); + padding: 9px 14px; + border-bottom: 1px solid rgba(148, 163, 184, 0.08); + min-height: 36px; } .c2-session-info-dl__row:last-child { border-bottom: none; } +.c2-session-info-dl__row--full { + grid-template-columns: 92px minmax(0, 1fr); +} + .c2-session-info-dl__label { margin: 0; font-size: 11px; font-weight: 600; color: var(--c2-text-muted); - line-height: 1.3; + line-height: 1.35; white-space: nowrap; + letter-spacing: 0.01em; } .c2-session-info-dl__value { margin: 0; - font-size: 12px; + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + font-size: 13px; font-weight: 500; color: var(--c2-text); - line-height: 1.35; - word-break: break-all; + line-height: 1.4; } -.c2-session-info-dl__value.is-mono { +.c2-session-info-dl__text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.c2-session-info-dl__value.is-mono .c2-session-info-dl__text { font-family: var(--c2-mono); - font-size: 11px; + font-size: 12px; } .c2-session-info-dl__value.is-accent { @@ -2830,16 +3654,147 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { font-weight: 700; } +.c2-session-info-dl__value.is-empty { + color: var(--c2-text-muted); + font-weight: 500; + font-style: italic; + opacity: 0.72; +} + +.c2-session-info-dl__value.is-empty.is-accent, +.c2-session-info-dl__value.is-empty.is-warn { + color: var(--c2-text-muted); + font-weight: 500; +} + +html[data-theme="dark"] .c2-session-info-dl__value.is-warn:not(.is-empty) { + color: var(--c2-amber); +} + +.c2-session-info-copy { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + color: var(--c2-text-muted); + cursor: pointer; + opacity: 0; + transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease, border-color 0.15s ease; +} + +.c2-session-info-dl__row:hover .c2-session-info-copy, +.c2-session-info-dl__row:focus-within .c2-session-info-copy { + opacity: 1; +} + +.c2-session-info-copy:hover { + color: var(--c2-accent); + background: var(--c2-accent-dim); + border-color: rgba(59, 130, 246, 0.25); +} + .c2-session-info-block--note .c2-session-info-note { - padding: 6px 10px 8px; - font-size: 12px; - line-height: 1.5; - color: var(--c2-text-dim); + padding: 12px 14px 14px; + font-size: 13px; + line-height: 1.6; + color: var(--c2-text); min-height: 0; white-space: pre-wrap; word-break: break-word; } +.c2-session-info-block--note .c2-session-info-note.is-empty { + color: var(--c2-text-muted); + font-style: italic; + padding: 14px; + text-align: center; +} + +.c2-session-info-block__head .c2-session-note-edit-btn { + margin-left: auto; + border: 1px solid var(--c2-border); + background: var(--c2-surface); + color: var(--c2-text-dim); + font-size: 11px; + font-weight: 600; + padding: 3px 10px; + border-radius: 999px; + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease; +} + +.c2-session-info-block__head .c2-session-note-edit-btn:hover { + color: var(--c2-accent); + border-color: rgba(59, 130, 246, 0.35); + background: var(--c2-accent-dim); +} + +.c2-session-note-editor { + padding: 12px 14px 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.c2-session-note-editor[hidden] { + display: none !important; +} + +.c2-session-note-textarea { + width: 100%; + min-height: 96px; + resize: vertical; + padding: 10px 12px; + font-size: 13px; + line-height: 1.55; + font-family: inherit; + color: var(--c2-text); + background: var(--c2-surface-alt); + border: 1px solid var(--c2-border); + border-radius: var(--c2-radius-xs); + outline: none; + box-sizing: border-box; +} + +.c2-session-note-textarea:focus { + border-color: var(--c2-accent); + box-shadow: 0 0 0 3px var(--c2-accent-dim); +} + +.c2-session-note-editor__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.c2-session-note-counter { + font-size: 11px; + color: var(--c2-text-muted); + font-family: var(--c2-mono); +} + +.c2-session-note-editor__actions { + display: inline-flex; + gap: 8px; +} + +html[data-theme="dark"] .c2-session-note-textarea { + background: #0b1220; +} + +@media (max-width: 1200px) { + .c2-session-info-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + @media (max-width: 900px) { .c2-session-hero { flex-direction: column; @@ -2850,15 +3805,20 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { flex-direction: row; align-items: center; justify-content: space-between; + flex-wrap: wrap; } - .c2-session-info-dl { - display: flex; + .c2-session-info-grid { + grid-template-columns: 1fr; } .c2-session-info-dl__row { grid-template-columns: 84px minmax(0, 1fr); } + + .c2-session-info-copy { + opacity: 1; + } } /* Legacy info grid (profiles etc.) */ @@ -3420,3 +4380,93 @@ html[data-theme="dark"] #page-c2-payloads .c2-form-select-option.is-selected { } .c2-modal.c2-modal--wide { max-width: 100%; } } + +/* ============================================================================ + Session page — action hierarchy & polish + ============================================================================ */ + +#page-c2-sessions .page-header { + padding-bottom: 14px; +} + +#page-c2-sessions .c2-sessions-page-actions { + gap: 8px; +} + +#page-c2-sessions .c2-sessions-page-actions .btn-secondary, +#page-c2-sessions .c2-sessions-page-actions .btn-danger { + font-size: 13px; + padding: 7px 14px; + border-radius: var(--c2-radius-xs); +} + +#page-c2-sessions .c2-sessions-page-actions .btn-secondary.is-danger-soft { + color: var(--c2-red); + border-color: rgba(239, 68, 68, 0.28); +} + +#page-c2-sessions .c2-sessions-page-actions .btn-secondary.is-danger-soft:hover:not(:disabled) { + background: var(--c2-red-dim); + border-color: var(--c2-red); +} + +#page-c2-sessions .c2-sessions-page-actions .btn-danger { + box-shadow: none; +} + +.c2-session-actions .btn-danger { + background: transparent; + border: 1px solid rgba(239, 68, 68, 0.35); + color: var(--c2-red); +} + +.c2-session-actions .btn-danger:hover { + background: var(--c2-red-dim); + border-color: var(--c2-red); +} + +.c2-session-tasks-toolbar { + padding: 12px 14px; +} + +.c2-session-tasks-rows { + padding: 10px; + gap: 8px; +} + +.c2-session-task-view { + min-width: 56px; +} + +html[data-theme="dark"] .c2-session-hero { + background: linear-gradient(180deg, #152033 0%, var(--c2-surface) 100%); +} + +html[data-theme="dark"] .c2-file-panel .c2-file-toolbar { + background: linear-gradient(180deg, #121a2a 0%, var(--c2-surface) 100%); +} + +html[data-theme="dark"] .c2-session-item.active { + background: linear-gradient(135deg, rgba(96, 165, 250, 0.12) 0%, rgba(96, 165, 250, 0.03) 100%); + border-color: rgba(96, 165, 250, 0.4); +} + +html[data-theme="dark"] .c2-session-info-block--note.is-empty { + background: rgba(15, 23, 42, 0.35); + border-color: rgba(59, 74, 99, 0.7); +} + +html[data-theme="dark"] .c2-session-info-dl__row { + border-bottom-color: rgba(36, 48, 68, 0.85); +} + +@media (max-width: 1100px) { + .c2-session-sidebar-wrap { + width: 280px; + min-width: 240px; + } + + .c2-path-breadcrumb { + max-width: 40vw; + } +} diff --git a/web/static/css/style.css b/web/static/css/style.css index 70e46d42..d8739ae1 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -19,6 +19,7 @@ --card-bg: var(--bg-primary); --input-bg: var(--bg-primary); --text-primary: #1a1a1a; + --text-color: var(--text-primary); --text-secondary: #6c757d; --text-muted: #adb5bd; --border-color: #e9ecef; @@ -53,6 +54,7 @@ html[data-theme="dark"] { --card-bg: #111827; --input-bg: #0b1120; --text-primary: #e5e7eb; + --text-color: var(--text-primary); --text-secondary: #a7b0c0; --text-muted: #6b7280; --border-color: #263244; @@ -1802,11 +1804,11 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { } .hitl-apply-btn { - padding: 6px 12px; - border-radius: 10px; + padding: 7px 12px; + border-radius: 8px; font-size: 12px; font-weight: 600; - background: linear-gradient(180deg, var(--accent-color) 0%, var(--accent-hover) 100%); + background: var(--accent-color); color: #fff; border: none; cursor: pointer; @@ -1865,7 +1867,7 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { } .hitl-config-field { - margin-bottom: 14px; + margin-bottom: 10px; } .hitl-config-field:last-child { @@ -1879,22 +1881,22 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { .hitl-config-actions { display: flex; justify-content: flex-end; - margin-top: 14px; - padding-top: 12px; + margin-top: 10px; + padding-top: 10px; border-top: 1px solid rgba(15, 23, 42, 0.06); } .hitl-config-label { display: block; - font-size: 12px; + font-size: 11px; font-weight: 600; color: var(--text-primary); - margin-bottom: 6px; - letter-spacing: -0.01em; + margin-bottom: 5px; + letter-spacing: 0; } .hitl-config-hint { - margin: 8px 0 0; + margin: 6px 0 0; font-size: 11px; line-height: 1.45; color: var(--text-secondary); @@ -1902,9 +1904,9 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { .hitl-config-select { width: 100%; - height: 40px; + height: 36px; border: 1px solid var(--border-color); - border-radius: 10px; + border-radius: 8px; background: var(--bg-primary); padding: 0 36px 0 12px; font-size: 13px; @@ -1921,13 +1923,13 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { .hitl-config-textarea { display: block; width: 100%; - min-height: 72px; - max-height: 200px; + min-height: 68px; + max-height: 150px; resize: vertical; border: 1px solid var(--border-color); - border-radius: 10px; - background: #fafbfc; - padding: 10px 12px; + border-radius: 8px; + background: #fff; + padding: 9px 10px; font-size: 12px; line-height: 1.5; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; @@ -2072,6 +2074,8 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { width: 100%; min-height: 76px; margin-top: 8px; + overflow: hidden; + resize: none; border: 1px solid #e2e8f0; border-radius: 8px; background: #ffffff; @@ -2096,17 +2100,204 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { } .hitl-inline-approval { - margin-top: 8px; - padding: 10px; - border: 1px solid #dbeafe; - background: #f8fbff; + position: relative; + margin-top: 10px; + padding: 0; + border: 1px solid #cfe0ff; + background: #ffffff; border-radius: 8px; + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); + overflow: hidden; } .hitl-inline-approval.hitl-inline-done { opacity: 0.8; } +.hitl-inline-approval::before { + content: ""; + position: absolute; + inset: 0 auto 0 0; + width: 3px; + background: #f59e0b; +} + +.hitl-inline-approval--merged { + margin-top: 12px; + padding-top: 12px; + border: 0; + border-top: 1px dashed rgba(21, 101, 192, 0.22); + border-radius: 0; + background: transparent; + box-shadow: none; + overflow: visible; +} + +.hitl-inline-approval--merged::before { + display: none; +} + +.hitl-inline-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 14px 10px 16px; + border-bottom: 1px solid #e8eef8; + background: linear-gradient(180deg, #fbfdff 0%, #f8fbff 100%); +} + +.hitl-inline-approval--merged .hitl-inline-header { + padding: 0 0 8px 0; + border-bottom: 0; + background: transparent; +} + +.hitl-inline-title, +.hitl-inline-badges { + display: inline-flex; + align-items: center; + min-width: 0; +} + +.hitl-inline-title { + gap: 8px; + color: #0f172a; + font-size: 14px; + font-weight: 700; + white-space: nowrap; +} + +.hitl-inline-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 999px; + background: #fff7ed; + color: #d97706; + border: 1px solid #fed7aa; + font-size: 12px; + font-weight: 800; + line-height: 1; +} + +.hitl-inline-badges { + justify-content: flex-end; + gap: 8px; + flex-wrap: wrap; +} + +.hitl-inline-body { + display: grid; + gap: 10px; + padding: 12px 14px 12px 16px; +} + +.hitl-inline-approval--merged .hitl-inline-body { + padding: 0; +} + +.hitl-inline-summary { + margin: 0; + color: #475569; +} + +.hitl-inline-note { + margin: 0; + padding: 9px 10px; + border: 1px solid #e6edf7; + border-radius: 7px; + background: #f8fafc; + color: #475569; + font-size: 12px; + line-height: 1.5; +} + +.hitl-inline-field { + display: grid; + gap: 6px; +} + +.hitl-inline-approval .hitl-edit-args { + min-height: 96px; + margin-top: 0; +} + +.hitl-inline-approval .hitl-config-input { + height: 36px; + margin: 0; +} + +.hitl-inline-actions { + align-items: center; + justify-content: flex-end; + margin-top: 0; + padding: 10px 14px 10px 16px; + border-top: 1px solid #e8eef8; + background: #fbfdff; +} + +.hitl-inline-approval--merged .hitl-inline-actions { + margin-top: 10px; + padding: 10px 0 0 0; + border-top: 0; + background: transparent; +} + +.hitl-inline-actions .btn-primary, +.hitl-inline-actions .btn-secondary { + min-width: 64px; +} + +.hitl-inline-status { + flex: 1; + min-width: 120px; + margin: 0; + color: #64748b; +} + +.hitl-inline-decision { + display: flex; + align-items: center; + gap: 8px; + min-height: 34px; + margin-top: 12px; + padding-top: 10px; + border-top: 1px dashed rgba(15, 23, 42, 0.12); + color: #475569; + font-size: 12px; +} + +.hitl-inline-approval--merged .hitl-inline-decision { + margin-top: 0; +} + +.hitl-inline-decision strong { + color: #0f172a; + font-size: 13px; +} + +.hitl-inline-decision-dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: #64748b; +} + +.hitl-inline-decision--approve .hitl-inline-decision-dot { + background: #22c55e; +} + +.hitl-inline-decision--reject .hitl-inline-decision-dot { + background: #ef4444; +} + +.hitl-inline-decision-comment { + color: #64748b; +} + .hitl-context-block { margin-top: 10px; } @@ -4054,11 +4245,17 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { .conversation-reasoning-card { border-top: 1px solid var(--border-color); - background: linear-gradient(165deg, #f8fafc 0%, #f1f5f9 55%, #eef2f7 100%); - padding: 11px 12px; + background: #f8fafc; + padding: 10px 12px 12px; flex-shrink: 0; } +.conversation-reasoning-card.conversation-reasoning-collapsed { + height: var(--chat-input-bar-height, 65px); + box-sizing: border-box; + padding: 12px 16px; +} + .conversation-reasoning-card-header { display: flex; align-items: center; @@ -4078,6 +4275,10 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { border-radius: 0; } +.conversation-reasoning-card.conversation-reasoning-collapsed .conversation-reasoning-card-header { + min-height: 40px; +} + .conversation-reasoning-card-header:hover .conversation-reasoning-title, .hitl-sidebar-card-header:hover .hitl-sidebar-title { color: var(--accent-color); @@ -4116,15 +4317,20 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { .conversation-reasoning-icon { flex-shrink: 0; - width: 32px; - height: 32px; + width: 30px; + height: 30px; display: flex; align-items: center; justify-content: center; - border-radius: 9px; - background: linear-gradient(145deg, rgba(0, 102, 255, 0.12), rgba(0, 102, 255, 0.06)); + border-radius: 8px; + background: #eaf2ff; color: var(--accent-color); - border: 1px solid rgba(0, 102, 255, 0.18); + border: 1px solid rgba(49, 130, 206, 0.18); +} + +.conversation-reasoning-card.conversation-reasoning-collapsed .conversation-reasoning-icon { + width: 32px; + height: 32px; } .conversation-reasoning-icon svg { @@ -4158,10 +4364,10 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { } .conversation-reasoning-body { - overflow: hidden; - max-height: 280px; + overflow: visible; + max-height: 820px; opacity: 1; - margin-top: 8px; + margin-top: 10px; padding-bottom: 0; transition: max-height 0.3s ease, opacity 0.2s ease, margin-top 0.3s ease; } @@ -4171,12 +4377,13 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { opacity: 0; margin-top: 0; padding-bottom: 0; + overflow: hidden; pointer-events: none; } .conversation-reasoning-body .chat-reasoning-panel-hint { margin-top: 0; - margin-bottom: 8px; + margin-bottom: 10px; } .chat-reasoning-panel-hint { @@ -4189,21 +4396,669 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { .chat-reasoning-fields { display: flex; flex-direction: column; - gap: 12px; + gap: 10px; +} + +.session-settings-group { + border: 1px solid rgba(148, 163, 184, 0.22); + border-radius: 8px; + background: rgba(255, 255, 255, 0.72); + padding: 10px; +} + +.session-settings-group-title { + margin-bottom: 8px; + color: var(--text-secondary, #4a5568); + font-size: 11px; + font-weight: 700; + line-height: 1.2; +} + +.session-settings-inline { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 8px; + margin-top: 8px; +} + +.session-hitl-config { + background: transparent; + border: none; + padding: 0; + box-shadow: none; +} + +.ai-channel-manager { + border: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 88%, transparent); + border-radius: 10px; + background: var(--card-bg, #fff); + overflow: hidden; +} + +.ai-channel-manager-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + padding: 18px 22px; + border-bottom: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 80%, transparent); + background: color-mix(in srgb, var(--bg-secondary, #fbfcfe) 74%, var(--card-bg, #fff)); +} + +.ai-channel-manager-header h4 { + margin: 0 0 8px; + font-size: 16px; + line-height: 1.25; +} + +.ai-channel-manager-hint { + display: block; + margin: 0; + color: var(--text-muted, #718096); + font-size: 13px; + line-height: 1.45; +} + +.ai-channel-manager-hint.is-success { + color: var(--text-muted, #718096); +} + +.ai-channel-manager-hint.is-error { + color: var(--error-color, #e53e3e); +} + +.ai-channel-save-btn { + flex-shrink: 0; + min-width: 92px; +} + +.ai-channel-manager-body { + display: grid; + grid-template-columns: minmax(280px, 360px) minmax(0, 1fr); + min-height: 0; + align-items: stretch; + background: var(--card-bg, #fff); + overflow: visible; +} + +.ai-channel-sidebar { + position: relative; + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; + overflow: hidden; + border-right: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 76%, transparent); + background: color-mix(in srgb, var(--bg-secondary, #f8fafc) 86%, var(--card-bg, #fff)); + padding: 18px; +} + +.ai-channel-sidebar-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 0; + color: var(--text-secondary, #4a5568); + font-size: 12px; + font-weight: 700; + letter-spacing: 0; +} + +.ai-channel-bulk-actions { + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 5px; + margin: 0; + min-width: 0; + padding: 0; + border: 0; + background: transparent; +} + +.ai-channel-bulk-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 0; + min-height: 26px; + padding: 4px 8px; + border: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 68%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--card-bg, #fff) 38%, transparent); + color: var(--text-secondary, #4a5568); + font-size: 10.5px; + font-weight: 600; + line-height: 1.2; + white-space: nowrap; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; +} + +.ai-channel-bulk-btn:hover { + border-color: color-mix(in srgb, var(--accent-color, #3182ce) 34%, transparent); + background: color-mix(in srgb, var(--accent-color, #3182ce) 8%, transparent); + color: var(--accent-color, #3182ce); +} + +.ai-channel-bulk-btn.danger { + border-color: color-mix(in srgb, var(--error-color, #e53e3e) 28%, transparent); + color: color-mix(in srgb, var(--error-color, #e53e3e) 82%, var(--text-secondary, #4a5568)); +} + +.ai-channel-bulk-btn.danger:hover { + border-color: color-mix(in srgb, var(--error-color, #e53e3e) 44%, transparent); + background: color-mix(in srgb, var(--error-color, #e53e3e) 7%, transparent); + color: var(--error-color, #e53e3e); +} + +.ai-channel-icon-btn { + width: 30px; + height: 30px; + border: 1px solid var(--border-color, #e2e8f0); + border-radius: 8px; + background: var(--card-bg, #fff); + color: var(--accent-color, #3182ce); + font-size: 18px; + line-height: 1; + font-weight: 600; + cursor: pointer; +} + +.ai-channel-icon-btn:hover { + border-color: var(--accent-color, #3182ce); + background: color-mix(in srgb, var(--accent-color, #3182ce) 9%, transparent); +} + +.ai-channel-native-select { + display: none; +} + +.ai-channel-list { + position: absolute; + top: 64px; + right: 13px; + bottom: 18px; + left: 18px; + display: flex; + flex-direction: column; + gap: 10px; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 2px 5px 2px 1px; + scrollbar-gutter: stable; +} + +.ai-channel-list::-webkit-scrollbar { + width: 8px; +} + +.ai-channel-list::-webkit-scrollbar-track { + background: transparent; +} + +.ai-channel-list::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.55); + border-radius: 999px; + border: 2px solid var(--bg-secondary, #f8fafc); +} + +.ai-channel-list::-webkit-scrollbar-thumb:hover { + background: rgba(100, 116, 139, 0.7); +} + +.ai-channel-list-item { + display: grid; + grid-template-columns: 20px minmax(0, 1fr); + align-items: start; + gap: 10px; + width: 100%; + min-height: 92px; + padding: 12px; + border: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 64%, transparent); + border-radius: 8px; + background: color-mix(in srgb, var(--card-bg, #fff) 70%, transparent); + color: var(--text-color, #2d3748); + text-align: left; + cursor: pointer; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03); + transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; +} + +.ai-channel-card-body { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.ai-channel-bulk-check { + width: 18px; + height: 18px; + margin: 2px 0 0; + border: 1.5px solid color-mix(in srgb, var(--text-muted, #718096) 62%, transparent); + border-radius: 6px; + background: color-mix(in srgb, var(--card-bg, #fff) 70%, transparent); + appearance: none; + -webkit-appearance: none; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; +} + +.ai-channel-bulk-check:hover { + border-color: var(--accent-color, #3182ce); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color, #3182ce) 12%, transparent); +} + +.ai-channel-bulk-check:checked { + border-color: var(--accent-color, #3182ce); + background: var(--accent-color, #3182ce); + box-shadow: inset 0 0 0 3px var(--accent-color, #3182ce), inset 0 0 0 4px #fff; +} + +.ai-channel-list-item.checked { + border-color: color-mix(in srgb, var(--accent-color, #3182ce) 38%, transparent); + background: color-mix(in srgb, var(--accent-color, #3182ce) 7%, var(--card-bg, #fff)); +} + +.ai-channel-list-item:hover { + background: var(--card-bg, #fff); + border-color: color-mix(in srgb, var(--accent-color, #3182ce) 24%, transparent); + transform: translateY(-1px); +} + +.ai-channel-list-item.active { + background: var(--card-bg, #fff); + border-color: color-mix(in srgb, var(--accent-color, #3182ce) 58%, transparent); + box-shadow: 0 8px 18px rgba(49, 130, 206, 0.12), inset 3px 0 0 var(--accent-color, #3182ce); +} + +.ai-channel-list-main { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.ai-channel-list-main strong, +.ai-channel-list-meta, +.ai-channel-list-foot span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ai-channel-list-main strong { + min-width: 0; + font-size: 14px; + font-weight: 700; + line-height: 1.25; +} + +.ai-channel-list-meta { + color: var(--text-muted, #718096); + font-size: 12px; + line-height: 1.25; +} + +.ai-channel-list-foot { + display: flex; + justify-content: space-between; + gap: 8px; + color: var(--text-muted, #718096); + font-size: 11px; + line-height: 1.25; +} + +.ai-channel-list-foot span:first-child { + flex: 1 1 auto; + min-width: 0; +} + +.ai-channel-list-foot span:last-child { + flex: 0 1 38%; + max-width: 38%; + text-align: right; +} + +.ai-channel-status-label.ready { + color: #10b981; +} + +.ai-channel-status-label.testing { + color: #3b82f6; +} + +.ai-channel-status-label.failed { + color: #ef4444; +} + +.ai-channel-status-dot { + width: 8px; + height: 8px; + border-radius: 999px; + flex-shrink: 0; + background: #cbd5e1; + box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 10%, transparent); +} + +.ai-channel-status-dot.ready { + background: #10b981; +} + +.ai-channel-status-dot.draft { + background: #f59e0b; +} + +.ai-channel-status-dot.testing { + background: #3b82f6; +} + +.ai-channel-status-dot.failed { + background: #ef4444; +} + +.ai-channel-badge { + flex-shrink: 0; + font-size: 11px; + font-weight: 700; + color: var(--accent-color, #3182ce); + background: rgba(49, 130, 206, 0.1); + border-radius: 999px; + padding: 2px 6px; +} + +.ai-channel-editor { + min-width: 0; + min-height: 0; + overflow-y: visible; + padding: 22px 28px 28px; + background: var(--card-bg, #fff); +} + +.ai-channel-editor-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; + margin-bottom: 18px; + padding-bottom: 18px; + border-bottom: 1px solid color-mix(in srgb, var(--border-color, #e2e8f0) 72%, transparent); +} + +.ai-channel-editor-kicker { + display: block; + margin-bottom: 4px; + color: var(--text-muted, #718096); + font-size: 12px; + font-weight: 700; +} + +.ai-channel-editor-head h5 { + margin: 0; + color: var(--text-color, #2d3748); + font-size: 22px; + font-weight: 700; + line-height: 1.2; +} + +.ai-channel-editor-head p { + margin: 4px 0 0; + color: var(--text-muted, #718096); + font-size: 12px; +} + +.ai-channel-editor-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + padding-top: 1px; +} + +.ai-channel-editor-actions .danger { + color: var(--error-color, #e53e3e); +} + +.ai-channel-editor-form { + max-width: 980px; +} + +.ai-channel-editor-form .form-group { + margin-bottom: 0; +} + +.ai-channel-editor-form .form-group label { + display: block; + margin-bottom: 0; + color: var(--text-primary); + font-size: 0.875rem; + font-weight: 500; + line-height: normal; +} + +.ai-channel-editor-form input[type="text"], +.ai-channel-editor-form input[type="password"], +.ai-channel-editor-form input[type="number"], +.ai-channel-editor-form select { + min-height: 42px; + border-radius: 8px; + border-color: color-mix(in srgb, var(--border-color, #e2e8f0) 88%, transparent); + background: color-mix(in srgb, var(--input-bg, var(--card-bg, #fff)) 92%, transparent); +} + +.ai-channel-editor-form input:focus, +.ai-channel-editor-form select:focus { + border-color: color-mix(in srgb, var(--accent-color, #3182ce) 70%, transparent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-color, #3182ce) 12%, transparent); + outline: none; +} + +.ai-channel-editor-form .form-hint, +.ai-channel-editor-form small { + line-height: 1.45; +} + +html[data-theme="dark"] .ai-channel-manager { + background: #111827; + border-color: rgba(71, 85, 105, 0.42); +} + +html[data-theme="dark"] .ai-channel-manager-header { + background: #111827; + border-bottom-color: rgba(71, 85, 105, 0.34); +} + +html[data-theme="dark"] .ai-channel-sidebar { + background: #101827; + border-right-color: rgba(71, 85, 105, 0.34); +} + +html[data-theme="dark"] .ai-channel-bulk-actions { + background: transparent; + border-color: transparent; +} + +html[data-theme="dark"] .ai-channel-bulk-btn { + background: rgba(15, 23, 42, 0.28); + border-color: rgba(71, 85, 105, 0.36); + color: #a7b3c7; +} + +html[data-theme="dark"] .ai-channel-bulk-btn:hover { + background: rgba(96, 165, 250, 0.1); + border-color: rgba(96, 165, 250, 0.36); + color: #93c5fd; +} + +html[data-theme="dark"] .ai-channel-bulk-btn.danger { + background: rgba(127, 29, 29, 0.08); + border-color: rgba(248, 113, 113, 0.24); + color: #fca5a5; +} + +html[data-theme="dark"] .ai-channel-bulk-btn.danger:hover { + background: rgba(127, 29, 29, 0.18); + border-color: rgba(248, 113, 113, 0.4); +} + +html[data-theme="dark"] .ai-channel-bulk-check { + background: rgba(15, 23, 42, 0.86); + border-color: rgba(148, 163, 184, 0.46); +} + +html[data-theme="dark"] .ai-channel-bulk-check:checked { + background: #60a5fa; + border-color: #60a5fa; + box-shadow: inset 0 0 0 3px #60a5fa, inset 0 0 0 4px #0f172a; +} + +html[data-theme="dark"] .ai-channel-editor { + background: #111827; +} + +html[data-theme="dark"] .ai-channel-editor-head { + border-bottom-color: rgba(71, 85, 105, 0.34); +} + +html[data-theme="dark"] .ai-channel-icon-btn { + background: rgba(15, 23, 42, 0.72); + border-color: rgba(71, 85, 105, 0.44); +} + +html[data-theme="dark"] .ai-channel-icon-btn:hover { + background: rgba(96, 165, 250, 0.12); + border-color: rgba(96, 165, 250, 0.58); +} + +html[data-theme="dark"] .ai-channel-list::-webkit-scrollbar-thumb { + background: rgba(100, 116, 139, 0.58); + border-color: #101827; +} + +html[data-theme="dark"] .ai-channel-list-item { + background: rgba(15, 23, 42, 0.42); + border-color: rgba(71, 85, 105, 0.28); + box-shadow: none; +} + +html[data-theme="dark"] .ai-channel-list-item.checked { + background: rgba(37, 99, 235, 0.1); + border-color: rgba(96, 165, 250, 0.34); +} + +html[data-theme="dark"] .ai-channel-list-item:hover { + background: rgba(30, 41, 59, 0.42); + border-color: rgba(96, 165, 250, 0.26); +} + +html[data-theme="dark"] .ai-channel-list-item.active { + background: rgba(59, 130, 246, 0.08); + border-color: rgba(96, 165, 250, 0.46); + box-shadow: inset 2px 0 0 rgba(96, 165, 250, 0.95); +} + +html[data-theme="dark"] .ai-channel-badge { + background: rgba(96, 165, 250, 0.16); + color: #93c5fd; +} + +html[data-theme="dark"] .ai-channel-editor-actions .btn-secondary { + background: rgba(15, 23, 42, 0.72); +} + +html[data-theme="dark"] .ai-channel-editor-actions .btn-secondary.danger { + background: rgba(127, 29, 29, 0.28); + border-color: rgba(248, 113, 113, 0.42); + color: #fca5a5; +} + +html[data-theme="dark"] .ai-channel-editor-actions .btn-secondary.danger:hover { + background: rgba(153, 27, 27, 0.38); + border-color: rgba(248, 113, 113, 0.68); + color: #fecaca; +} + +html[data-theme="dark"] .ai-channel-editor-form input[type="text"], +html[data-theme="dark"] .ai-channel-editor-form input[type="password"], +html[data-theme="dark"] .ai-channel-editor-form input[type="number"], +html[data-theme="dark"] .ai-channel-editor-form select { + background: rgba(15, 23, 42, 0.52); + border-color: rgba(71, 85, 105, 0.44); +} + +.ai-channel-editor-form #openai-provider, +.ai-channel-editor-form #openai-reasoning-mode, +.ai-channel-editor-form #openai-reasoning-effort, +.ai-channel-editor-form #openai-reasoning-profile { + background: var(--input-bg, var(--card-bg, #fff)); + color: var(--text-primary, #2d3748); +} + +@media (max-width: 980px) { + .ai-channel-manager-header, + .ai-channel-editor-head { + flex-direction: column; + align-items: stretch; + } + + .ai-channel-manager-body { + grid-template-columns: 1fr; + height: auto; + overflow: visible; + } + + .ai-channel-sidebar { + border-right: 0; + border-bottom: 1px solid var(--border-color, #e2e8f0); + max-height: 420px; + } + + .ai-channel-list { + position: static; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + max-height: 360px; + } + + .ai-channel-editor { + overflow-y: visible; + } +} + +@media (max-width: 640px) { + .ai-channel-manager-header, + .ai-channel-sidebar, + .ai-channel-editor { + padding-left: 14px; + padding-right: 14px; + } + + .ai-channel-editor-actions { + justify-content: flex-start; + } + + .ai-channel-bulk-actions { + justify-content: flex-start; + } } .chat-reasoning-field-label { display: block; - font-size: 0.75rem; + font-size: 11px; font-weight: 600; color: var(--text-muted, #718096); - margin-bottom: 6px; + margin-bottom: 5px; } .chat-reasoning-select { width: 100%; box-sizing: border-box; - padding: 0.45rem 0.6rem; + height: 36px; + padding: 0 32px 0 10px; font-size: 0.8125rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 8px; @@ -4211,6 +5066,150 @@ html[data-theme="dark"] .new-chat-btn:focus-visible { color: var(--text-color, #2d3748); } +.session-settings-native-select { + position: absolute; + opacity: 0; + pointer-events: none; + width: 1px; + height: 1px; +} + +.session-settings-select { + position: relative; + width: 100%; +} + +.session-settings-select-trigger { + width: 100%; + height: 36px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 0 10px; + border: 1px solid var(--border-color, #e2e8f0); + border-radius: 8px; + background: #fff; + color: var(--text-color, #2d3748); + font-size: 13px; + font-weight: 600; + text-align: left; + cursor: pointer; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; +} + +.session-settings-select-trigger:hover { + border-color: rgba(49, 130, 206, 0.38); + background: #fbfdff; +} + +.session-settings-select-trigger:focus-visible, +.session-settings-select.open .session-settings-select-trigger { + outline: none; + border-color: var(--accent-color, #3182ce); + box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.14); +} + +.session-settings-select-value { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.session-settings-select-caret { + flex-shrink: 0; + color: var(--text-muted, #718096); + font-size: 17px; + line-height: 1; + transform: translateY(-1px); + transition: transform 0.15s ease, color 0.15s ease; +} + +.session-settings-select.open .session-settings-select-caret { + color: var(--accent-color, #3182ce); + transform: rotate(180deg) translateY(1px); +} + +.session-settings-select-menu { + position: absolute; + z-index: 40; + top: calc(100% + 6px); + left: 0; + right: 0; + display: none; + max-height: 220px; + overflow-y: auto; + padding: 5px; + border: 1px solid rgba(148, 163, 184, 0.28); + border-radius: 8px; + background: #fff; + box-shadow: 0 14px 32px rgba(15, 23, 42, 0.16); +} + +.session-settings-select.open .session-settings-select-menu { + display: block; +} + +.session-settings-select-option { + width: 100%; + min-height: 32px; + display: flex; + align-items: center; + padding: 6px 8px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--text-color, #2d3748); + font-size: 13px; + font-weight: 500; + text-align: left; + cursor: pointer; +} + +.session-settings-select-option:hover { + background: rgba(49, 130, 206, 0.08); +} + +.session-settings-select-option.is-selected { + background: rgba(49, 130, 206, 0.12); + color: var(--accent-color, #3182ce); + font-weight: 700; +} + +.session-settings-select-option-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.conversation-reasoning-card .hitl-reviewer-toggle { + width: 100%; + padding: 3px; + border-radius: 8px; + background: #f1f5f9; + border: 1px solid rgba(148, 163, 184, 0.22); +} + +.conversation-reasoning-card .hitl-reviewer-toggle-btn { + min-height: 30px; + padding: 5px 8px; + border-radius: 6px; + font-size: 12px; +} + +.conversation-reasoning-card .hitl-reviewer-toggle-btn.is-active { + background: #fff; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08); +} + +@media (max-width: 420px) { + .session-settings-inline { + grid-template-columns: 1fr; + } +} + .chat-input-container .chat-input-with-files, .chat-input-primary-row .chat-input-with-files { flex: 1; @@ -6502,6 +7501,18 @@ html[data-theme="dark"] .login-card .login-submit:disabled { border-color: rgba(21, 101, 192, 0.22); } +.tool-args-hitl-edited { + display: inline-flex; + align-items: center; + margin-left: 8px; + padding: 2px 7px; + border-radius: 999px; + background: rgba(245, 158, 11, 0.14); + color: #b45309; + font-size: 11px; + font-weight: 600; +} + .timeline-item-tool_result:has(.tool-result-section.success) .tool-result { background: rgba(40, 167, 69, 0.08); border-color: rgba(40, 167, 69, 0.35); @@ -8724,6 +9735,11 @@ html[data-theme="dark"] .robot-binding-service-hint-icon { box-sizing: border-box; } +#webshell-modal .form-group input:not([type="checkbox"]):not([type="radio"]), +#webshell-modal .form-group select { + font-weight: 400; +} + .form-group select { cursor: pointer; appearance: none; @@ -20116,6 +21132,138 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { border-color: #fca5a5; } +#webshell-modal .webshell-form-native-select { + position: absolute !important; + width: 1px !important; + height: 1px !important; + margin: -1px !important; + padding: 0 !important; + border: 0 !important; + opacity: 0 !important; + pointer-events: none !important; +} + +#webshell-modal .webshell-form-select-ui { + position: relative; + width: 100%; +} + +#webshell-modal .webshell-form-select-trigger { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: 42px; + padding: 0 42px 0 14px; + border: 1px solid var(--border-color, #d8dee8); + border-radius: 8px; + background: var(--input-bg, #fff); + color: var(--text-primary, #111827); + font: inherit; + font-weight: 400; + text-align: left; + cursor: pointer; + transition: border-color .16s ease, box-shadow .16s ease, background .16s ease; +} + +#webshell-modal .webshell-form-select-trigger:hover:not(:disabled), +#webshell-modal .webshell-form-select-ui.open .webshell-form-select-trigger { + border-color: var(--primary-color, #3b82f6); + box-shadow: 0 0 0 3px rgba(59, 130, 246, .12); +} + +#webshell-modal .webshell-form-select-value { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#webshell-modal .webshell-form-select-caret { + position: absolute; + right: 15px; + width: 8px; + height: 8px; + border-right: 2px solid currentColor; + border-bottom: 2px solid currentColor; + opacity: .65; + transform: translateY(-2px) rotate(45deg); + transition: transform .16s ease, opacity .16s ease; +} + +#webshell-modal .webshell-form-select-ui.open .webshell-form-select-caret { + opacity: .9; + transform: translateY(2px) rotate(225deg); +} + +#webshell-modal .webshell-form-select-dropdown { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + z-index: 5200; + display: none; + max-height: 240px; + overflow-y: auto; + padding: 6px; + border: 1px solid var(--border-color, #d8dee8); + border-radius: 10px; + background: var(--input-bg, #fff); + color: var(--text-primary, #111827); + box-shadow: 0 18px 38px rgba(15, 23, 42, .22); +} + +#webshell-modal .webshell-form-select-ui.open { + z-index: 5201; +} + +#webshell-modal .webshell-form-select-ui.open .webshell-form-select-dropdown { + display: block; +} + +#webshell-modal .webshell-form-select-option { + display: grid; + grid-template-columns: 18px minmax(0, 1fr); + align-items: center; + gap: 8px; + width: 100%; + min-height: 36px; + padding: 7px 9px; + border: 0; + border-radius: 7px; + background: transparent; + color: inherit; + font: inherit; + font-weight: 400; + text-align: left; + cursor: pointer; +} + +#webshell-modal .webshell-form-select-option:hover { + background: var(--bg-secondary, #f4f6f8); +} + +#webshell-modal .webshell-form-select-option.is-selected { + background: rgba(59, 130, 246, .14); + color: var(--primary-color, #2563eb); +} + +#webshell-modal .webshell-form-select-check { + visibility: hidden; + font-weight: 800; +} + +#webshell-modal .webshell-form-select-option.is-selected .webshell-form-select-check { + visibility: visible; +} + +#webshell-modal .webshell-form-select-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .webshell-item-url { font-size: 0.78rem; color: var(--text-secondary); @@ -34233,6 +35381,70 @@ html[data-theme="dark"] .detail-code-card { box-shadow: var(--shadow-sm); } +html[data-theme="dark"] .conversation-reasoning-icon { + background: rgba(59, 130, 246, 0.14); + border-color: rgba(96, 165, 250, 0.26); +} + +html[data-theme="dark"] .session-settings-group { + background: rgba(15, 23, 42, 0.86); + border-color: rgba(96, 165, 250, 0.22); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.035), + 0 1px 0 rgba(15, 23, 42, 0.32); +} + +html[data-theme="dark"] .session-hitl-field.session-settings-group { + background: rgba(13, 22, 38, 0.92); + border-color: rgba(96, 165, 250, 0.3); +} + +html[data-theme="dark"] .session-settings-group-title { + color: #cbd5e1; +} + +html[data-theme="dark"] .chat-reasoning-select, +html[data-theme="dark"] .conversation-reasoning-card .hitl-config-select, +html[data-theme="dark"] .conversation-reasoning-card .hitl-config-textarea { + background-color: #0f172a; + border-color: #2b374b; + color: var(--text-primary); +} + +html[data-theme="dark"] .session-settings-select-trigger, +html[data-theme="dark"] .session-settings-select-menu { + background: #0f172a; + border-color: #2b374b; + color: var(--text-primary); +} + +html[data-theme="dark"] .session-settings-select-trigger:hover { + background: #111827; + border-color: rgba(96, 165, 250, 0.4); +} + +html[data-theme="dark"] .session-settings-select-option { + color: var(--text-primary); +} + +html[data-theme="dark"] .session-settings-select-option:hover { + background: rgba(59, 130, 246, 0.16); +} + +html[data-theme="dark"] .session-settings-select-option.is-selected { + background: rgba(59, 130, 246, 0.22); + color: #93c5fd; +} + +html[data-theme="dark"] .conversation-reasoning-card .hitl-reviewer-toggle { + background: #0b1120; + border-color: #263244; +} + +html[data-theme="dark"] .conversation-reasoning-card .hitl-reviewer-toggle-btn.is-active { + background: #1e293b; +} + html[data-theme="dark"] .dashboard-kpi-card:nth-child(1), html[data-theme="dark"] .dashboard-kpi-card:nth-child(2), html[data-theme="dark"] .dashboard-kpi-card:nth-child(3), @@ -34581,6 +35793,11 @@ html[data-theme="dark"] .timeline-item-tool_call.tool-call-running { background: rgba(37, 99, 235, 0.105); } +html[data-theme="dark"] .tool-args-hitl-edited { + background: rgba(245, 158, 11, 0.16); + color: #fcd34d; +} + html[data-theme="dark"] .timeline-item-tool_call.tool-call-completed { border: 1px solid rgba(52, 211, 153, 0.34); border-left: 3px solid #34d399; @@ -35758,8 +36975,65 @@ html[data-theme="dark"] .hitl-pending-actions { } html[data-theme="dark"] .hitl-inline-approval { - background: rgba(96, 165, 250, 0.08) !important; - border-color: rgba(96, 165, 250, 0.22) !important; + background: #0f172a !important; + border-color: rgba(96, 165, 250, 0.28) !important; + box-shadow: none !important; +} + +html[data-theme="dark"] .hitl-inline-approval--merged { + background: transparent !important; + border: 0 !important; + border-top: 1px dashed rgba(96, 165, 250, 0.28) !important; +} + +html[data-theme="dark"] .hitl-inline-approval::before { + background: #f59e0b !important; +} + +html[data-theme="dark"] .hitl-inline-header, +html[data-theme="dark"] .hitl-inline-actions { + background: #111827 !important; + border-color: #263244 !important; +} + +html[data-theme="dark"] .hitl-inline-approval--merged .hitl-inline-header, +html[data-theme="dark"] .hitl-inline-approval--merged .hitl-inline-actions { + background: transparent !important; + border-color: transparent !important; +} + +html[data-theme="dark"] .hitl-inline-title { + color: var(--text-primary) !important; +} + +html[data-theme="dark"] .hitl-inline-icon { + background: rgba(245, 158, 11, 0.14) !important; + color: #fbbf24 !important; + border-color: rgba(245, 158, 11, 0.32) !important; +} + +html[data-theme="dark"] .hitl-inline-summary, +html[data-theme="dark"] .hitl-inline-status { + color: var(--text-secondary) !important; +} + +html[data-theme="dark"] .hitl-inline-decision { + border-top-color: rgba(148, 163, 184, 0.22) !important; + color: var(--text-secondary) !important; +} + +html[data-theme="dark"] .hitl-inline-decision strong { + color: var(--text-primary) !important; +} + +html[data-theme="dark"] .hitl-inline-decision-comment { + color: var(--text-muted) !important; +} + +html[data-theme="dark"] .hitl-inline-note { + background: #0b1120 !important; + border-color: #263244 !important; + color: var(--text-secondary) !important; } html[data-theme="dark"] .hitl-apply-feedback { @@ -36186,6 +37460,32 @@ html[data-theme="dark"] #webshell-modal select option { color: var(--text-primary); } +html[data-theme="dark"] #webshell-modal .webshell-form-select-trigger, +html[data-theme="dark"] #webshell-modal .webshell-form-select-dropdown { + background: #0f172a !important; + color: var(--text-primary) !important; + border-color: #2b374b !important; +} + +html[data-theme="dark"] #webshell-modal .webshell-form-select-trigger:hover:not(:disabled), +html[data-theme="dark"] #webshell-modal .webshell-form-select-ui.open .webshell-form-select-trigger { + border-color: rgba(96, 165, 250, 0.62) !important; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16) !important; +} + +html[data-theme="dark"] #webshell-modal .webshell-form-select-dropdown { + box-shadow: 0 18px 38px rgba(0, 0, 0, 0.38) !important; +} + +html[data-theme="dark"] #webshell-modal .webshell-form-select-option:hover { + background: rgba(30, 41, 59, 0.82) !important; +} + +html[data-theme="dark"] #webshell-modal .webshell-form-select-option.is-selected { + background: rgba(59, 130, 246, 0.22) !important; + color: #bfdbfe !important; +} + /* WebShell memo tab dark theme */ html[data-theme="dark"] #page-webshell .webshell-pane-memo { background: #111827 !important; @@ -41376,7 +42676,6 @@ input.theme-checkbox:disabled { .asset-row-actions .asset-delete { color: #dc2626; } .asset-vulnerability-link { min-width: 28px; padding: 3px 8px; border: 1px solid rgba(239,68,68,.28); border-radius: 999px; background: rgba(239,68,68,.10); color: #dc2626; font-weight: 700; cursor: pointer; } .asset-scan-content { width: min(700px, calc(100vw - 32px)); } -.asset-project-content { width: min(500px, calc(100vw - 32px)); } .asset-bulk-edit-content { width: min(760px, calc(100vw - 32px)); } .asset-import-content { width: min(920px, calc(100vw - 32px)); max-height: calc(100vh - 48px); margin: 24px auto; } .asset-import-body { display: flex; flex-direction: column; gap: 14px; } @@ -41409,9 +42708,16 @@ input.theme-checkbox:disabled { .asset-import-result--error { color: #dc2626; } .asset-import-result--error::before { content: '!'; } .asset-import-preview-section .form-hint { margin: 9px 0 0; color: var(--text-secondary, #64748b); font-size: 11px; } -.asset-project-content .form-hint { margin: 10px 0 0; color: var(--text-secondary, #64748b); font-size: 12px; line-height: 1.5; } -.asset-project-content .settings-custom-select-trigger { height: 42px; border-radius: 8px; background: var(--input-bg, var(--bg-primary)); } -.asset-project-content .settings-custom-select-menu { z-index: 2700; } +#asset-project-modal { align-items: center; justify-content: center; box-sizing: border-box; padding: 24px; } +.asset-project-content { width: min(680px, 100%); min-height: min(620px, calc(100vh - 48px)); max-height: calc(100vh - 48px); margin: 0 auto; overflow: visible; contain: layout style; } +.asset-project-content .modal-header { padding: 24px 28px 18px; } +.asset-project-content .modal-body { padding: 18px 28px 24px; overflow: visible; } +.asset-project-content .modal-footer { padding: 16px 28px 22px; } +.asset-project-content .form-hint { margin: 12px 0 0; color: var(--text-secondary, #64748b); font-size: 13px; line-height: 1.6; } +.asset-project-content .asset-editor-field { gap: 9px; font-size: 14px; } +.asset-project-content .settings-custom-select-trigger { height: 48px; padding: 0 15px; border-radius: 8px; background: var(--input-bg, var(--bg-primary)); font-size: 14px; } +.asset-project-content .settings-custom-select-menu { z-index: 2700; max-height: min(360px, calc(100vh - 280px)); overflow-y: auto; padding: 7px; } +.asset-project-content .settings-custom-select-option { min-height: 44px; padding: 10px 12px; font-size: 14px; } .asset-project-content .settings-custom-select-option.is-disabled { display: none; } .asset-editor-field > .asset-custom-select { width: 100%; } .asset-editor-field > .asset-custom-select .settings-custom-select-trigger { width: 100%; } diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 15b1d3f7..8645688e 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -708,6 +708,12 @@ "reasoningCompactAria": "Open model reasoning options", "reasoningPanelTitle": "Model reasoning", "reasoningPanelHint": "Only Eino single- and multi-agent requests use these; merged with defaults in Settings.", + "sessionSettingsTitle": "Session settings", + "sessionSettingsAria": "Open session settings", + "sessionSettingsHint": "AI channel, reasoning, and HITL settings only affect future messages.", + "aiChannelLabel": "AI channel", + "aiChannelDefault": "Use default channel", + "aiChannelDefaultShort": "Default channel", "reasoningSummaryFollow": "System", "reasoningSummaryDash": "—", "agentModeOrchPlanExecute": "Plan-Exec", @@ -729,7 +735,7 @@ "hitlWhitelistHint": "Separate with commas or new lines; shown merged with the global allowlist in config.", "hitlApply": "Apply", "hitlApplyOkSync": "HITL settings saved and synced to the server.", - "hitlApplyOkWhitelistYaml": "Tool whitelist merged into config.yaml and active. Mode and timeout still require selecting a conversation and clicking Apply to sync session settings to the server.", + "hitlApplyOkWhitelistYaml": "Tool whitelist merged into config.yaml and active. Session settings are saved automatically.", "hitlApplyOkLocal": "Saved in this browser.", "hitlApplyFail": "Failed to sync to server", "hitlStatusOff": "Human-in-the-loop: Off" @@ -2588,11 +2594,34 @@ }, "settingsBasic": { "basicTitle": "Basic settings", - "openaiConfig": "OpenAI config", + "openaiConfig": "AI channel config", + "aiChannelCurrent": "Current channel", + "aiChannelSave": "Save changes", + "aiChannelSetDefault": "Set default", + "aiChannelNew": "New", + "aiChannelCopy": "Copy", + "aiChannelDelete": "Delete", + "aiChannelHint": "Saved channels appear on the left. Saving writes to ai.channels; the default channel is used by new chats and tasks without an explicit channel.", + "aiChannelSavedList": "Saved channels", + "aiChannelListAria": "AI channel list", + "aiChannelEditing": "Editing", + "aiChannelDefaultBadge": "Default", + "aiChannelReady": "Ready", + "aiChannelDraft": "Incomplete", + "aiChannelDefaultMeta": "Default channel", + "aiChannelCustomMeta": "Custom channel", + "aiChannelOpenAICompat": "OpenAI compatible", + "aiChannelModelMissing": "Model missing", + "aiChannelName": "Channel name", + "aiChannelUntitled": "New Channel", + "aiChannelDeleteConfirm": "Delete AI channel \"{name}\"?", + "aiChannelSaved": "Channel saved", + "aiChannelDefaultSaved": "Default channel saved", + "aiChannelCount": "{count} channel(s) saved", "apiProvider": "API Provider", "providerOpenAI": "OpenAI / OpenAI-compatible API", "providerClaude": "Claude (Anthropic Messages API)", - "visionProviderReuseOpenAI": "Reuse OpenAI config (leave empty)", + "visionProviderReuseOpenAI": "Reuse default AI channel", "fofaConfig": "FOFA config", "fofaConfigHint": "Used for asset discovery and import; only an API key is required.", "agentConfig": "Agent config", @@ -2613,8 +2642,11 @@ "maxTotalTokens": "Max Context Tokens", "maxTotalTokensPlaceholder": "120000", "maxTotalTokensHint": "Shared by memory compression and attack chain building. Default: 120000", - "openaiReasoningTitle": "Model reasoning", - "openaiReasoningHint": "Works with the reasoning controls on the chat page.", + "maxCompletionTokens": "Max Output Tokens", + "maxCompletionTokensPlaceholder": "16384", + "maxCompletionTokensHint": "Maximum tokens for a single model response. Default: 16384", + "openaiReasoningTitle": "Reasoning settings", + "openaiReasoningHint": "Default reasoning settings for this AI channel; chat Session settings can override them.", "openaiReasoningProfile": "Wire profile", "openaiReasoningAllowClient": "Allow chat page to override reasoning options", "fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all (optional)", @@ -3839,6 +3871,11 @@ "infoLastCheckin": "Last check-in", "infoNote": "Note", "infoNoteEmpty": "No notes", + "infoCopy": "Copy", + "noteEdit": "Edit", + "notePlaceholder": "Add a note to identify this session…", + "toastNoteSaved": "Note saved", + "toastNoteTooLong": "Note must be at most 2000 characters", "infoSectionIdentity": "Identity", "infoSectionSystem": "System", "infoSectionNetwork": "Network & beacon", @@ -3952,7 +3989,20 @@ "paginationPrev": "Previous", "paginationPage": "Page {{current}} / {{total}}", "paginationNext": "Next", - "paginationLast": "Last" + "paginationLast": "Last", + "statTotal": "Filtered", + "timePresets": "Quick", + "preset15m": "Last 15 min", + "preset1h": "Last 1 hour", + "preset24h": "Last 24 hours", + "preset7d": "Last 7 days", + "presetAll": "All", + "filterAllStatuses": "All statuses", + "filterAllTypes": "All types", + "filterSession": "Session ID", + "filterSessionPlaceholder": "s_xxxxxxxx", + "applyFilters": "Apply filters", + "resetFilters": "Reset" }, "payloads": { "386": "386", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 8ea7c1f1..8c4e7ea9 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -696,6 +696,12 @@ "reasoningCompactAria": "打开模型推理选项", "reasoningPanelTitle": "模型推理", "reasoningPanelHint": "仅 Eino 单代理与多代理请求会带上这些参数;与系统设置中的默认值合并。", + "sessionSettingsTitle": "会话设置", + "sessionSettingsAria": "打开会话设置", + "sessionSettingsHint": "AI 通道、推理设置与人机协同只影响后续消息。", + "aiChannelLabel": "AI 通道", + "aiChannelDefault": "跟随默认通道", + "aiChannelDefaultShort": "默认通道", "reasoningSummaryFollow": "系统", "reasoningSummaryDash": "—", "agentModeOrchPlanExecute": "Plan-Exec", @@ -717,7 +723,7 @@ "hitlWhitelistHint": "白名单内工具免审批;每行一个或逗号分隔,与 config 全局白名单合并。", "hitlApply": "应用", "hitlApplyOkSync": "人机协同配置已保存并同步到服务器。", - "hitlApplyOkWhitelistYaml": "免审批工具已合并进 config.yaml 并生效。协同模式、超时等仍须选中会话后再点「应用」才会写入服务器。", + "hitlApplyOkWhitelistYaml": "免审批工具已合并进 config.yaml 并生效。会话配置会自动保存。", "hitlApplyOkLocal": "已保存到本浏览器。", "hitlApplyFail": "同步到服务器失败", "hitlStatusOff": "人机协同:关闭" @@ -2576,11 +2582,34 @@ }, "settingsBasic": { "basicTitle": "基本设置", - "openaiConfig": "OpenAI 配置", + "openaiConfig": "AI 通道配置", + "aiChannelCurrent": "当前通道", + "aiChannelSave": "保存更改", + "aiChannelSetDefault": "设为默认", + "aiChannelNew": "新增", + "aiChannelCopy": "复制", + "aiChannelDelete": "删除", + "aiChannelHint": "已保存的通道会显示在左侧;保存后写入 ai.channels,默认通道用于新对话和未指定通道的任务。", + "aiChannelSavedList": "已保存通道", + "aiChannelListAria": "AI 通道列表", + "aiChannelEditing": "正在编辑", + "aiChannelDefaultBadge": "默认", + "aiChannelReady": "可用", + "aiChannelDraft": "待完善", + "aiChannelDefaultMeta": "默认通道", + "aiChannelCustomMeta": "自定义通道", + "aiChannelOpenAICompat": "OpenAI 兼容", + "aiChannelModelMissing": "未填写模型", + "aiChannelName": "通道名称", + "aiChannelUntitled": "新通道", + "aiChannelDeleteConfirm": "确定删除 AI 通道「{name}」吗?", + "aiChannelSaved": "通道已保存", + "aiChannelDefaultSaved": "已设为默认通道", + "aiChannelCount": "已保存 {count} 个通道", "apiProvider": "API 提供商", "providerOpenAI": "OpenAI / 兼容 OpenAI 协议", "providerClaude": "Claude (Anthropic Messages API)", - "visionProviderReuseOpenAI": "OpenAI 配置(留空复用)", + "visionProviderReuseOpenAI": "默认 AI 通道(留空复用)", "fofaConfig": "FOFA 配置", "fofaConfigHint": "用于资产发现与导入,仅需配置 API Key。", "agentConfig": "Agent 配置", @@ -2601,8 +2630,11 @@ "maxTotalTokens": "最大上下文 Token 数", "maxTotalTokensPlaceholder": "120000", "maxTotalTokensHint": "内存压缩和攻击链构建共用此配置,默认 120000", - "openaiReasoningTitle": "模型推理", - "openaiReasoningHint": "与对话页「模型推理」下拉配合使用。", + "maxCompletionTokens": "最大输出 Token 数", + "maxCompletionTokensPlaceholder": "16384", + "maxCompletionTokensHint": "单次模型回复的输出上限,默认 16384", + "openaiReasoningTitle": "推理设置", + "openaiReasoningHint": "作为该 AI 通道的默认推理设置;对话页「会话设置」可覆盖。", "openaiReasoningProfile": "线路 profile", "openaiReasoningAllowClient": "允许对话页覆盖推理选项", "fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all(可选)", @@ -3827,6 +3859,11 @@ "infoLastCheckin": "上次心跳", "infoNote": "备注", "infoNoteEmpty": "暂无备注", + "infoCopy": "复制", + "noteEdit": "编辑", + "notePlaceholder": "添加备注,便于识别该会话…", + "toastNoteSaved": "备注已保存", + "toastNoteTooLong": "备注最多 2000 字", "infoSectionIdentity": "身份信息", "infoSectionSystem": "系统环境", "infoSectionNetwork": "网络与信标", @@ -3940,7 +3977,20 @@ "paginationPrev": "上一页", "paginationPage": "第 {{current}} / {{total}} 页", "paginationNext": "下一页", - "paginationLast": "末页" + "paginationLast": "末页", + "statTotal": "当前筛选", + "timePresets": "快捷", + "preset15m": "最近15分钟", + "preset1h": "最近1小时", + "preset24h": "最近24小时", + "preset7d": "最近7天", + "presetAll": "全部", + "filterAllStatuses": "全部状态", + "filterAllTypes": "全部类型", + "filterSession": "会话 ID", + "filterSessionPlaceholder": "s_xxxxxxxx", + "applyFilters": "应用筛选", + "resetFilters": "重置" }, "payloads": { "386": "386", diff --git a/web/static/js/assets.js b/web/static/js/assets.js index c41c0d1a..f44c5211 100644 --- a/web/static/js/assets.js +++ b/web/static/js/assets.js @@ -868,7 +868,7 @@ async function openAssetProjectModal() { } const subtitle = document.getElementById('asset-project-subtitle'); if (subtitle) subtitle.textContent = assetT('assets.bindProjectCount', `将更新 ${assets.length} 个资产`, { count: assets.length }); - if (typeof openAppModal === 'function') openAppModal('asset-project-modal'); + if (typeof openAppModal === 'function') openAppModal('asset-project-modal', { display: 'flex' }); else document.getElementById('asset-project-modal').style.display = 'flex'; if (select) select.focus(); } diff --git a/web/static/js/c2.js b/web/static/js/c2.js index 08f1c9e6..4abaa3dc 100644 --- a/web/static/js/c2.js +++ b/web/static/js/c2.js @@ -14,6 +14,8 @@ tasksPageSize: 10, tasksTotal: 0, tasksPendingQueuedCount: null, + tasksStatusCounts: { success: 0, failed: 0, pending: 0, cancelled: 0 }, + tasksFilter: { status: '', task_type: '', session_id: '', since: '', timePreset: 'all' }, events: [], eventsPage: 1, eventsPageSize: 10, @@ -47,6 +49,111 @@ // API 基础路径 const API_BASE = '/api/c2'; + function activeC2ProjectId() { + try { + return (typeof getActiveProjectId === 'function' ? getActiveProjectId() : '') || ''; + } catch (e) { + return ''; + } + } + + function c2ResourceProjectId(item) { + return String((item && (item.project_id || item.projectId)) || '').trim(); + } + + function c2LooksLikeUuid(value) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(value || '').trim()); + } + + function c2ProjectNameMap() { + try { + return (window.projectNameById && typeof window.projectNameById === 'object') + ? window.projectNameById + : {}; + } catch (e) { + return {}; + } + } + + /** 下拉展示用项目名:优先可读名称,绝不回退成 UUID */ + function c2ProjectDisplayName(id, nameHint) { + const idStr = String(id || '').trim(); + if (!idStr) return ''; + let name = String(nameHint || '').trim(); + if (!name || name === idStr || c2LooksLikeUuid(name)) { + const mapped = String(c2ProjectNameMap()[idStr] || '').trim(); + if (mapped && mapped !== idStr && !c2LooksLikeUuid(mapped)) name = mapped; + } + if (!name || name === idStr || c2LooksLikeUuid(name)) { + if (typeof window.getProjectName === 'function') { + const viaFn = String(window.getProjectName(idStr) || '').trim(); + if (viaFn && viaFn !== idStr && !c2LooksLikeUuid(viaFn)) name = viaFn; + } + } + if (!name || name === idStr || c2LooksLikeUuid(name)) { + return c2t('batchManageModal.unknownProject') || '未知项目'; + } + return name; + } + + function c2ProjectOptionsHtml(selectedId, projectList) { + const selected = String(selectedId || '').trim(); + let html = ``; + let entries = []; + if (Array.isArray(projectList) && projectList.length) { + entries = projectList + .filter((p) => p && p.id) + .map((p) => [String(p.id), String(p.name || '').trim()]); + } else { + entries = Object.entries(c2ProjectNameMap()); + } + const seen = new Set(); + entries.sort((a, b) => c2ProjectDisplayName(a[0], a[1]).localeCompare(c2ProjectDisplayName(b[0], b[1]), undefined, { sensitivity: 'base' })); + entries.forEach(([id, name]) => { + if (!id || seen.has(id)) return; + seen.add(id); + const label = c2ProjectDisplayName(id, name); + html += ``; + }); + if (selected && !seen.has(selected)) { + html += ``; + } + return html; + } + + function ensureC2ProjectsLoaded() { + if (typeof window.ensureProjectsLoaded === 'function') return window.ensureProjectsLoaded(); + if (typeof window.fetchAllProjects === 'function') { + return window.fetchAllProjects(false).then((list) => { + if (typeof window.rebuildProjectNameMap === 'function') window.rebuildProjectNameMap(list || []); + return list || []; + }); + } + return Promise.resolve([]); + } + + /** 确保当前选中项目有可读名称(孤儿 / 未进缓存的 ID) */ + function ensureC2SelectedProjectNamed(projectId) { + const id = String(projectId || '').trim(); + if (!id) return Promise.resolve(); + const mapped = String(c2ProjectNameMap()[id] || '').trim(); + if (mapped && mapped !== id && !c2LooksLikeUuid(mapped)) return Promise.resolve(); + if (typeof window.fetchProjectSummary !== 'function') return Promise.resolve(); + return window.fetchProjectSummary(id).then((p) => { + if (p && p.id && typeof window.rememberProjectsInNameMap === 'function') { + window.rememberProjectsInNameMap([p]); + } + }).catch(function () { /* ignore */ }); + } + + function c2ProjectBindSelectHtml(listener) { + return ``; + } + + function withC2ProjectQuery(url) { + return url; + } + window.__c2DownloadPayload = function(filename) { const url = `${API_BASE}/payloads/${filename}/download`; const fetchFn = (typeof apiFetch === 'function') ? apiFetch : fetch; @@ -339,56 +446,116 @@ return String(os || '?').substring(0, 3).toLowerCase(); } + function isEmptyInfoValue(value) { + if (value == null) return true; + const s = String(value).trim(); + if (!s || s === '-') return true; + const lower = s.toLowerCase(); + return lower === 'unknown' || lower === 'n/a' || lower === 'null' || lower === 'undefined'; + } + + function displayInfoValue(value, fallback) { + if (isEmptyInfoValue(value)) return fallback != null ? fallback : '—'; + return String(value); + } + + function sessionMetaLine(s) { + const user = displayInfoValue(s.username, ''); + const os = displayInfoValue(s.os, ''); + const arch = displayInfoValue(s.arch, ''); + const right = [os, arch].filter(Boolean).join('/') || '—'; + if (!user) return right; + return user + '@' + right; + } + function sessionInfoRow(label, value, opts) { + const empty = isEmptyInfoValue(value); + const display = empty ? '—' : String(value); const valueClasses = ['c2-session-info-dl__value']; if (opts && opts.mono) valueClasses.push('is-mono'); - if (opts && opts.accent) valueClasses.push('is-accent'); - if (opts && opts.warn) valueClasses.push('is-warn'); + if (opts && opts.accent && !empty) valueClasses.push('is-accent'); + if (opts && opts.warn && !empty) valueClasses.push('is-warn'); + if (empty) valueClasses.push('is-empty'); const rowCls = opts && opts.full ? ' c2-session-info-dl__row--full' : ''; + const copyBtn = (opts && opts.copy && !empty) + ? `` + : ''; return `
${escapeHtml(label)}
-
${escapeHtml(value == null || value === '' ? '-' : String(value))}
+
+ ${escapeHtml(display)} + ${copyBtn} +
`; } function renderSessionInfoPanel(s, adminVal, sleepLine) { const lastCheckin = formatTime(s.lastCheckIn); const lastRel = formatRelativeTime(s.lastCheckIn); - const checkinDisplay = lastRel ? `${lastCheckin} (${lastRel})` : lastCheckin; + const checkinDisplay = lastRel ? `${lastCheckin} · ${lastRel}` : lastCheckin; + const noteText = s.note && String(s.note).trim() ? String(s.note) : ''; + const noteEmpty = !noteText; return `
-
-
${escapeHtml(c2t('c2.sessions.infoSectionIdentity'))}
-
- ${sessionInfoRow(c2t('c2.sessions.infoSessionId'), s.id, { mono: true, accent: true, full: true })} - ${sessionInfoRow(c2t('c2.sessions.infoImplantUuid'), s.implantUuid, { mono: true, full: true })} - ${sessionInfoRow(c2t('c2.sessions.infoHostname'), s.hostname)} - ${sessionInfoRow(c2t('c2.sessions.infoUsername'), s.username)} -
-
-
-
${escapeHtml(c2t('c2.sessions.infoSectionSystem'))}
-
- ${sessionInfoRow(c2t('c2.sessions.infoOs'), s.os)} - ${sessionInfoRow(c2t('c2.sessions.infoArch'), s.arch, { mono: true })} - ${sessionInfoRow(c2t('c2.sessions.infoPid'), s.pid, { mono: true })} - ${sessionInfoRow(c2t('c2.sessions.infoProcess'), s.processName || '-', { mono: true })} - ${sessionInfoRow(c2t('c2.sessions.infoAdmin'), adminVal, { warn: s.isAdmin, full: true })} -
-
-
-
${escapeHtml(c2t('c2.sessions.infoSectionNetwork'))}
-
- ${sessionInfoRow(c2t('c2.sessions.infoInternalIp'), s.internalIp || '-', { mono: true, accent: true })} - ${sessionInfoRow(c2t('c2.sessions.infoSleep'), sleepLine)} - ${sessionInfoRow(c2t('c2.sessions.infoFirstSeen'), formatTime(s.firstSeenAt), { mono: true })} - ${sessionInfoRow(c2t('c2.sessions.infoLastCheckin'), checkinDisplay, { mono: true, accent: true })} -
-
-
-
${escapeHtml(c2t('c2.sessions.infoSectionNote'))}
-
${escapeHtml(s.note || c2t('c2.sessions.infoNoteEmpty'))}
+
+
+
+ + ${escapeHtml(c2t('c2.sessions.infoSectionIdentity'))} +
+
+ ${sessionInfoRow(c2t('c2.sessions.infoSessionId'), s.id, { mono: true, accent: true, copy: true })} + ${sessionInfoRow(c2t('c2.sessions.infoImplantUuid'), s.implantUuid, { mono: true, copy: true })} + ${sessionInfoRow(c2t('c2.sessions.infoHostname'), s.hostname)} + ${sessionInfoRow(c2t('c2.sessions.infoUsername'), s.username)} +
+
+
+
+ + ${escapeHtml(c2t('c2.sessions.infoSectionSystem'))} +
+
+ ${sessionInfoRow(c2t('c2.sessions.infoOs'), s.os)} + ${sessionInfoRow(c2t('c2.sessions.infoArch'), s.arch, { mono: true })} + ${sessionInfoRow(c2t('c2.sessions.infoPid'), s.pid, { mono: true })} + ${sessionInfoRow(c2t('c2.sessions.infoProcess'), s.processName || '-', { mono: true })} + ${sessionInfoRow(c2t('c2.sessions.infoAdmin'), adminVal, { warn: !!s.isAdmin })} +
+
+
+
+ + ${escapeHtml(c2t('c2.sessions.infoSectionNetwork'))} +
+
+ ${sessionInfoRow(c2t('c2.sessions.infoInternalIp'), s.internalIp || '-', { mono: true, accent: true, copy: true })} + ${sessionInfoRow(c2t('c2.sessions.infoSleep'), sleepLine)} + ${sessionInfoRow(c2t('c2.sessions.infoFirstSeen'), formatTime(s.firstSeenAt), { mono: true })} + ${sessionInfoRow(c2t('c2.sessions.infoLastCheckin'), checkinDisplay, { mono: true, accent: true })} +
+
+
+
+
+ + ${escapeHtml(c2t('c2.sessions.infoSectionNote'))} + +
+
${escapeHtml(noteText || c2t('c2.sessions.infoNoteEmpty'))}
+
`; } @@ -398,6 +565,7 @@ // ============================================================================ function apiRequest(method, url, data) { + if (method === 'GET') url = withC2ProjectQuery(url); const options = { method: method, headers: { 'Content-Type': 'application/json' } @@ -448,6 +616,21 @@ return c2t('c2.fmt.durationMin', { n: (ms / 60000).toFixed(1) }); } + function formatBytes(n) { + const num = Number(n); + if (!Number.isFinite(num) || num < 0) return String(n == null ? '-' : n); + if (num < 1024) return String(Math.round(num)) + ' B'; + const units = ['KB', 'MB', 'GB', 'TB']; + let v = num / 1024; + let i = 0; + while (v >= 1024 && i < units.length - 1) { + v /= 1024; + i += 1; + } + const digits = v >= 100 ? 0 : (v >= 10 ? 1 : 2); + return v.toFixed(digits) + ' ' + units[i]; + } + function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); @@ -556,6 +739,11 @@ } } + C2.copyText = function(text) { + if (text == null || text === '') return; + copyToClipboard(String(text)); + }; + // ============================================================================ // 页面初始化 // ============================================================================ @@ -611,7 +799,8 @@ C2.loadListeners = function() { Promise.all([ apiRequest('GET', `${API_BASE}/listeners`), - apiRequest('GET', `${API_BASE}/profiles`).catch(function() { return {}; }) + apiRequest('GET', `${API_BASE}/profiles`).catch(function() { return {}; }), + ensureC2ProjectsLoaded().catch(function() { return []; }) ]).then(function(results) { var ldata = results[0]; var pdata = results[1]; @@ -670,6 +859,7 @@ ? '
' + escapeHtml(c2t('c2.listeners.callbackShort')) + '' + escapeHtml(cb) + '
' : ''; const remarkRow = l.remark ? '
' + escapeHtml(l.remark) + '
' : ''; + const projectRow = '
' + escapeHtml(c2t('assets.project') || '所属项目') + '' + c2ProjectBindSelectHtml(l) + '
'; const startedHtml = formatListenerStartedHtml(l.startedAt); const pillLabel = escapeHtml(listenerCardStatusPillLabel(st)); const typeMark = escapeHtml(listenerTypeShortLabel(l.type)); @@ -697,6 +887,7 @@ ${bindVal} ${cbRow} + ${projectRow} ${profileBadge} ${remarkRow} ${startedHtml} @@ -751,8 +942,15 @@ `; - C2.ensureProfilesLoaded().then(() => { + const createSelectedProjectId = activeC2ProjectId(); + Promise.all([ + C2.ensureProfilesLoaded(), + ensureC2ProjectsLoaded().catch(() => []), + ensureC2SelectedProjectNamed(createSelectedProjectId) + ]).then(function (results) { + const projects = results[1] || []; const profileOpts = listenerProfileSelectHtml(''); + const projectOpts = c2ProjectOptionsHtml(createSelectedProjectId, projects); const emptyProfHintCreate = (C2.profiles && C2.profiles.length > 0) ? '' : `
${escapeHtml(c2t('c2.listeners.malleableProfileEmptyListHint'))}
`; @@ -777,6 +975,10 @@ +
+ + +
@@ -864,6 +1066,7 @@ const body = { name, type, bind_host: bindHost, bind_port: bindPort, remark, callback_host: callbackHost, + project_id: (document.getElementById('c2-listener-project-id')?.value || '').trim(), profile_id: profileId }; if (type === 'tcp_reverse' && legacyShell) { @@ -909,6 +1112,38 @@ }); }; + C2.bindListenerProject = function(id, projectId) { + if (typeof requirePermission === 'function' && !requirePermission('c2:write')) { + C2.renderListeners(); + return; + } + const listener = C2.listeners.find(x => x.id === id); + if (!listener) return; + const cfg = C2.getListenerConfig(listener); + const body = { + name: listener.name || '', + bind_host: listener.bindHost || '127.0.0.1', + bind_port: parseInt(listener.bindPort, 10) || 0, + remark: listener.remark || '', + callback_host: C2.getListenerCallbackHost(listener), + project_id: String(projectId || '').trim(), + profile_id: listenerResolvedProfileId(listener), + config: cfg + }; + apiRequest('PUT', `${API_BASE}/listeners/${id}`, body).then(data => { + if (data && data.error) { + showToast(data.error, 'error'); + C2.renderListeners(); + return; + } + showToast(c2t('projects.projectBound') || '已绑定项目', 'success'); + C2.loadListeners(); + }).catch(err => { + showToast(err && err.message ? err.message : '绑定项目失败', 'error'); + C2.renderListeners(); + }); + }; + C2.editListener = function(id) { const l = C2.listeners.find(x => x.id === id); if (!l) return; @@ -931,9 +1166,16 @@
`; - C2.ensureProfilesLoaded().then(() => { + const editSelectedProjectId = c2ResourceProjectId(l); + Promise.all([ + C2.ensureProfilesLoaded(), + ensureC2ProjectsLoaded().catch(() => []), + ensureC2SelectedProjectNamed(editSelectedProjectId) + ]).then(function (results) { + const projects = results[1] || []; const resolvedPid = listenerResolvedProfileId(l); const profileOpts = listenerProfileSelectHtml(resolvedPid); + const projectOpts = c2ProjectOptionsHtml(editSelectedProjectId, projects); const lt = String(l.type || '').toLowerCase(); const httpHint = (lt === 'http_beacon' || lt === 'https_beacon') ? '' @@ -951,6 +1193,10 @@
+
+ + +
@@ -1009,6 +1255,7 @@ const body = { name, bind_host: bindHost, bind_port: bindPort, remark, callback_host: callbackHost, + project_id: (document.getElementById('c2-listener-project-id')?.value || '').trim(), profile_id: profileId }; if (legacyEl) { @@ -1119,9 +1366,16 @@ const batchBtn = document.getElementById('c2-sessions-batch-delete'); const filteredBtn = document.getElementById('c2-sessions-delete-filtered'); const ids = C2.collectCheckedSessionIds(); - if (batchBtn) batchBtn.disabled = ids.length === 0; + if (batchBtn) { + batchBtn.disabled = ids.length === 0; + batchBtn.classList.toggle('btn-danger', ids.length > 0); + batchBtn.classList.toggle('btn-secondary', ids.length === 0); + } const total = (C2.sessions || []).length; - if (filteredBtn) filteredBtn.disabled = total === 0; + if (filteredBtn) { + filteredBtn.disabled = total === 0; + filteredBtn.classList.toggle('is-danger-soft', total > 0); + } const countEl = document.getElementById('c2-sessions-count'); if (countEl) { countEl.textContent = c2t('c2.sessions.filterCount', { n: total, selected: ids.length }); @@ -1191,7 +1445,12 @@ return; } - list.innerHTML = C2.sessions.map(s => ` + list.innerHTML = C2.sessions.map(s => { + const userLabel = displayInfoValue(s.username, '—'); + const osArch = [displayInfoValue(s.os, ''), displayInfoValue(s.arch, '')].filter(Boolean).join('/') || '—'; + const userEmpty = isEmptyInfoValue(s.username); + const osEmpty = isEmptyInfoValue(s.os) && isEmptyInfoValue(s.arch); + return `
@@ -1206,21 +1465,21 @@ ${escapeHtml(sessionStatusLabel(s.status))}
- ${escapeHtml(s.username)} - ${escapeHtml(s.os)}/${escapeHtml(s.arch)} + ${escapeHtml(userLabel)} + ${escapeHtml(osArch)} ${s.isAdmin ? '' + escapeHtml(c2t('c2.sessions.rootBadge')) + '' : ''}
- ${escapeHtml(s.internalIp || '-')} - PID ${s.pid} + ${escapeHtml(s.internalIp || '—')} + PID ${escapeHtml(String(s.pid != null ? s.pid : '—'))}
-
- `).join(''); + `; + }).join(''); if (C2.selectedSessionId && !C2.sessions.find(s => s.id === C2.selectedSessionId)) { C2.selectedSessionId = null; @@ -1270,11 +1529,11 @@ ${escapeHtml(sessionStatusLabel(s.status))} ${s.isAdmin ? '' + escapeHtml(c2t('c2.sessions.rootBadge')) + '' : ''} -
${escapeHtml(s.username)}@${escapeHtml(s.os)}/${escapeHtml(s.arch)}
+
${escapeHtml(sessionMetaLine(s))}
${escapeHtml(s.id)} - ${escapeHtml(s.internalIp || '-')} - PID ${s.pid} + ${escapeHtml(s.internalIp || '—')} + PID ${escapeHtml(String(s.pid != null ? s.pid : '—'))}
@@ -1601,6 +1860,88 @@ }); }; + C2.beginEditSessionNote = function(id) { + const block = document.getElementById('c2-session-note-block'); + const view = document.getElementById('c2-session-note-view'); + const editor = document.getElementById('c2-session-note-editor'); + const input = document.getElementById('c2-session-note-input'); + const editBtn = block && block.querySelector('.c2-session-note-edit-btn'); + if (!view || !editor || !input) return; + const s = (C2.sessions || []).find(x => x.id === id); + const note = s && s.note ? String(s.note) : ''; + input.value = note; + view.hidden = true; + editor.hidden = false; + if (block) block.classList.remove('is-empty'); + if (editBtn) editBtn.hidden = true; + C2.syncSessionNoteCounter(); + if (!input.dataset.boundCounter) { + input.dataset.boundCounter = '1'; + input.addEventListener('input', function () { C2.syncSessionNoteCounter(); }); + } + input.focus(); + input.setSelectionRange(input.value.length, input.value.length); + }; + + C2.syncSessionNoteCounter = function() { + const input = document.getElementById('c2-session-note-input'); + const counter = document.getElementById('c2-session-note-counter'); + if (!input || !counter) return; + counter.textContent = String(input.value.length) + '/2000'; + }; + + C2.cancelEditSessionNote = function() { + const block = document.getElementById('c2-session-note-block'); + const view = document.getElementById('c2-session-note-view'); + const editor = document.getElementById('c2-session-note-editor'); + const editBtn = block && block.querySelector('.c2-session-note-edit-btn'); + if (!view || !editor) return; + editor.hidden = true; + view.hidden = false; + if (editBtn) editBtn.hidden = false; + if (block) { + const s = (C2.sessions || []).find(x => x.id === C2.selectedSessionId); + const empty = !(s && s.note && String(s.note).trim()); + block.classList.toggle('is-empty', empty); + } + }; + + C2.saveSessionNote = function(id) { + if (!id) return; + const input = document.getElementById('c2-session-note-input'); + const saveBtn = document.getElementById('c2-session-note-save'); + if (!input) return; + const note = String(input.value || '').trim(); + if (note.length > 2000) { + showToast(c2t('c2.sessions.toastNoteTooLong'), 'warn'); + return; + } + if (saveBtn) saveBtn.disabled = true; + apiRequest('PUT', `${API_BASE}/sessions/${encodeURIComponent(id)}/note`, { note: note }).then(data => { + if (saveBtn) saveBtn.disabled = false; + if (data.error) { + showToast(String(data.error), 'error'); + return; + } + const saved = data.note != null ? String(data.note) : note; + const s = (C2.sessions || []).find(x => x.id === id); + if (s) s.note = saved; + showToast(c2t('c2.sessions.toastNoteSaved'), 'success'); + const block = document.getElementById('c2-session-note-block'); + const view = document.getElementById('c2-session-note-view'); + const empty = !saved.trim(); + if (view) { + view.textContent = empty ? c2t('c2.sessions.infoNoteEmpty') : saved; + view.classList.toggle('is-empty', empty); + } + if (block) block.classList.toggle('is-empty', empty); + C2.cancelEditSessionNote(); + }).catch(err => { + if (saveBtn) saveBtn.disabled = false; + showToast(err.message || String(err), 'error'); + }); + }; + C2.killSession = function(id) { if (!confirm(c2t('c2.sessions.confirmExitSession'))) return; apiRequest('POST', `${API_BASE}/tasks`, { @@ -2072,6 +2413,8 @@ }); container.setAttribute('tabindex', '0'); + C2.bindTerminalScrollbarAutoHide(container); + C2.terminalInstance = term; if (C2.terminalResizeObserver) { @@ -2090,6 +2433,42 @@ }); }; + C2.bindTerminalScrollbarAutoHide = function(container) { + if (!container) return; + if (C2._terminalScrollbarHideTimer) { + clearTimeout(C2._terminalScrollbarHideTimer); + C2._terminalScrollbarHideTimer = null; + } + const show = function () { + container.classList.add('is-scrollbar-visible'); + if (C2._terminalScrollbarHideTimer) clearTimeout(C2._terminalScrollbarHideTimer); + C2._terminalScrollbarHideTimer = setTimeout(function () { + container.classList.remove('is-scrollbar-visible'); + C2._terminalScrollbarHideTimer = null; + }, 900); + }; + const bindViewport = function () { + const vp = container.querySelector('.xterm-viewport'); + if (!vp) return false; + if (vp.dataset.scrollbarBound === '1') return true; + vp.dataset.scrollbarBound = '1'; + vp.addEventListener('scroll', show, { passive: true }); + return true; + }; + if (!container.dataset.scrollbarUiBound) { + container.dataset.scrollbarUiBound = '1'; + container.addEventListener('wheel', show, { passive: true }); + container.addEventListener('touchmove', show, { passive: true }); + } + if (!bindViewport()) { + const mo = new MutationObserver(function () { + if (bindViewport()) mo.disconnect(); + }); + mo.observe(container, { childList: true, subtree: true }); + setTimeout(function () { mo.disconnect(); }, 3000); + } + }; + C2.fitTerminal = function() { const container = document.getElementById('c2-terminal-container'); if (!container || !C2.terminalFitAddon || !C2.terminalInstance) return; @@ -2392,18 +2771,22 @@ ${entries.map(function(entry) { + const sizeLabel = entry.isDir ? '—' : formatBytes(entry.size); + const iconCls = entry.isDir ? 'c2-file-icon c2-file-icon--dir' : 'c2-file-icon c2-file-icon--file'; return ` - - ${entry.isDir ? '📁' : '📄'} - ${escapeHtml(entry.name)} + +
+ + ${escapeHtml(entry.name)} +
- ${escapeHtml(entry.size)} + ${escapeHtml(sizeLabel)} ${escapeHtml(entry.mode)} ${entry.isDir - ? `` - : `` + ? `` + : `` } @@ -2717,16 +3100,20 @@ // ============================================================================ C2.loadTasks = function(page) { + bindTasksFilterUiOnce(); + readTasksFilterFromDom(); const p = page != null ? page : (C2.tasksPage || 1); C2.tasksPage = p; const ps = C2.tasksPageSize || 10; - apiRequest('GET', `${API_BASE}/tasks?page=${encodeURIComponent(String(p))}&page_size=${encodeURIComponent(String(ps))}`).then(data => { + const qs = buildTasksQueryParams(p, ps); + apiRequest('GET', `${API_BASE}/tasks?${qs}`).then(data => { if (data.error) { showToast(String(data.error), 'error'); return; } C2.tasks = data.tasks || []; C2.tasksTotal = typeof data.total === 'number' ? data.total : (C2.tasks.length || 0); + C2.tasksStatusCounts = data.status_counts || { success: 0, failed: 0, pending: 0, cancelled: 0 }; if (typeof data.pending_queued_count === 'number') { C2.tasksPendingQueuedCount = data.pending_queued_count; } @@ -2736,8 +3123,10 @@ return; } C2.renderTasks(); + C2.renderTasksSummary(); C2.renderTasksPagination(); C2.syncTasksToolbar(); + syncTasksTimePresetButtons(); }).catch(err => { showToast(err.message || String(err), 'error'); }); @@ -2857,6 +3246,105 @@ }).catch(err => showToast(err.message || String(err), 'error')); }; + function readTasksFilterFromDom() { + const statusEl = document.getElementById('c2-tasks-filter-status'); + const typeEl = document.getElementById('c2-tasks-filter-type'); + const sessionEl = document.getElementById('c2-tasks-filter-session'); + C2.tasksFilter = C2.tasksFilter || { status: '', task_type: '', session_id: '', since: '', timePreset: 'all' }; + C2.tasksFilter.status = statusEl ? statusEl.value.trim() : ''; + C2.tasksFilter.task_type = typeEl ? typeEl.value.trim() : ''; + C2.tasksFilter.session_id = sessionEl ? sessionEl.value.trim() : ''; + } + + function buildTasksQueryParams(page, pageSize) { + const params = new URLSearchParams(); + params.set('page', String(page)); + params.set('page_size', String(pageSize)); + const f = C2.tasksFilter || {}; + if (f.status) params.set('status', f.status); + if (f.task_type) params.set('task_type', f.task_type); + if (f.session_id) params.set('session_id', f.session_id); + if (f.since) params.set('since', f.since); + return params.toString(); + } + + function tasksTimePresetToSince(preset) { + if (!preset || preset === 'all') return ''; + const now = Date.now(); + const map = { '15m': 15 * 60 * 1000, '1h': 60 * 60 * 1000, '24h': 24 * 60 * 60 * 1000, '7d': 7 * 24 * 60 * 60 * 1000 }; + const ms = map[preset]; + if (!ms) return ''; + return new Date(now - ms).toISOString(); + } + + function syncTasksTimePresetButtons() { + const wrap = document.getElementById('c2-tasks-time-presets'); + if (!wrap) return; + const active = (C2.tasksFilter && C2.tasksFilter.timePreset) || 'all'; + wrap.querySelectorAll('.c2-tasks-time-preset-btn').forEach(btn => { + btn.classList.toggle('is-active', btn.getAttribute('data-preset') === active); + }); + } + + function bindTasksFilterUiOnce() { + if (document.documentElement.dataset.c2TasksFilterBound === '1') return; + document.documentElement.dataset.c2TasksFilterBound = '1'; + + const presets = document.getElementById('c2-tasks-time-presets'); + if (presets) { + presets.addEventListener('click', (e) => { + const btn = e.target.closest('.c2-tasks-time-preset-btn'); + if (!btn) return; + const preset = btn.getAttribute('data-preset') || 'all'; + C2.tasksFilter = C2.tasksFilter || { status: '', task_type: '', session_id: '', since: '', timePreset: 'all' }; + C2.tasksFilter.timePreset = preset; + C2.tasksFilter.since = tasksTimePresetToSince(preset); + syncTasksTimePresetButtons(); + C2.loadTasks(1); + }); + } + + const sessionInput = document.getElementById('c2-tasks-filter-session'); + if (sessionInput) { + sessionInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') C2.applyTasksFilters(); + }); + } + } + + C2.applyTasksFilters = function() { + readTasksFilterFromDom(); + C2.loadTasks(1); + }; + + C2.resetTasksFilters = function() { + C2.tasksFilter = { status: '', task_type: '', session_id: '', since: '', timePreset: 'all' }; + const statusEl = document.getElementById('c2-tasks-filter-status'); + const typeEl = document.getElementById('c2-tasks-filter-type'); + const sessionEl = document.getElementById('c2-tasks-filter-session'); + if (statusEl) statusEl.value = ''; + if (typeEl) typeEl.value = ''; + if (sessionEl) sessionEl.value = ''; + syncTasksTimePresetButtons(); + C2.loadTasks(1); + }; + + C2.renderTasksSummary = function() { + const summary = document.getElementById('c2-tasks-summary'); + if (!summary) return; + const total = C2.tasksTotal || 0; + const counts = C2.tasksStatusCounts || { success: 0, failed: 0, pending: 0 }; + const set = (id, val) => { + const el = document.getElementById(id); + if (el) el.textContent = String(val); + }; + set('c2-tasks-stat-total', total); + set('c2-tasks-stat-success', counts.success || 0); + set('c2-tasks-stat-failed', counts.failed || 0); + set('c2-tasks-stat-pending', counts.pending || 0); + summary.hidden = total === 0; + }; + C2.loadSessionTasks = function(sessionId) { apiRequest('GET', `${API_BASE}/tasks?session_id=${encodeURIComponent(sessionId)}&limit=50`).then(data => { const container = document.getElementById('c2-session-tasks-list'); @@ -2941,7 +3429,7 @@ } if (C2.tasks.length === 0) { - container.innerHTML = '
' + escapeHtml(c2t('c2.tasks.emptyAll')) + '
'; + container.innerHTML = '
' + escapeHtml(c2t('c2.tasks.emptyAll')) + '
'; if (selAll) selAll.disabled = true; C2.syncTasksToolbar(); return; @@ -2949,59 +3437,76 @@ if (selAll) selAll.disabled = false; const delTitle = escapeHtml(c2t('c2.tasks.deleteOne')); + const cancelTitle = escapeHtml(c2t('c2.tasks.cancelBtn')); + const dash = ''; + const deleteIcon = ''; + container.innerHTML = ` - +
- - - + + + - + + - - + ${C2.tasks.map(t => { const rawId = t.id || ''; - const shortTaskId = rawId.length > 14 ? escapeHtml(rawId.substring(0, 12)) + '\u2026' : escapeHtml(rawId); - const sid = t.sessionId ? escapeHtml(String(t.sessionId).substring(0, 8)) + '\u2026' : '-'; + const tid = escapeHtml(rawId); + const status = String(t.status || ''); + const rowStatus = escapeHtml(status || 'queued'); + const typeCat = taskTypeCategory(t.taskType); + const sessionFull = t.sessionId ? String(t.sessionId) : ''; + const sessionShort = sessionFull + ? escapeHtml(sessionFull.substring(0, 10)) + (sessionFull.length > 10 ? '\u2026' : '') + : ''; + const taskShort = rawId + ? escapeHtml(rawId.substring(0, 10)) + (rawId.length > 10 ? '\u2026' : '') + : ''; const cmd = formatTaskCommand(t); - const cmdShort = truncateCommand(cmd, 48); + const cmdEsc = escapeHtml(cmd); + const canCancel = status === 'queued' || status === 'sent'; return ` - - + - - - - - - - - + + + + + + + - - `; + `; }).join('')}
${escapeHtml(c2t('c2.tasks.colTask'))}${escapeHtml(c2t('c2.tasks.colSession'))} + + ${escapeHtml(c2t('c2.tasks.colCreated'))}${escapeHtml(c2t('c2.tasks.colStatus'))} ${escapeHtml(c2t('c2.tasks.colType'))} ${escapeHtml(c2t('c2.tasks.colCommand'))}${escapeHtml(c2t('c2.tasks.colStatus'))}${escapeHtml(c2t('c2.tasks.colSession'))}${escapeHtml(c2t('c2.tasks.colTask'))} ${escapeHtml(c2t('c2.tasks.colDuration'))}${escapeHtml(c2t('c2.tasks.colCreated'))}${escapeHtml(c2t('c2.tasks.colActions'))}${escapeHtml(c2t('c2.tasks.colActions'))}
-
+ ${shortTaskId}${sid}${escapeHtml(t.taskType || '')}${cmdShort ? escapeHtml(cmdShort) : '-'}${escapeHtml(taskStatusLabel(t.status))}${formatDuration(t.durationMs)}${formatTime(t.createdAt)} -
- - ${t.status === 'queued' || t.status === 'sent' - ? `` - : ''} - +
${escapeHtml(formatTime(t.createdAt))}${escapeHtml(taskStatusLabel(status))}${escapeHtml(t.taskType || '-')}${cmdEsc || dash}${sessionShort || dash}${taskShort || dash}${formatDuration(t.durationMs)} +
+ ${canCancel + ? `` + : ''} +
`; C2.syncTasksToolbar(); if (typeof applyTranslations === 'function') applyTranslations(container); + if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container); }; C2.viewTask = function(id) { diff --git a/web/static/js/chat.js b/web/static/js/chat.js index fecd64dd..01e67e2d 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -80,11 +80,16 @@ let chatAttachmentSeq = 0; // 对话模式:eino_single = Eino ADK 单代理(/api/eino-agent/stream);deep / plan_execute / supervisor = Eino 多代理(/api/multi-agent/stream,请求体 orchestration) const AGENT_MODE_STORAGE_KEY = 'cyberstrike-chat-agent-mode'; +const AI_CHANNEL_STORAGE_KEY = 'cyberstrike-chat-ai-channel'; const REASONING_MODE_LS = 'cyberstrike-chat-reasoning-mode'; const REASONING_EFFORT_LS = 'cyberstrike-chat-reasoning-effort'; +const CHAT_AI_CHANNEL_SUMMARY_NAME_MAX = 10; const CHAT_AGENT_MODE_EINO_SINGLE = 'eino_single'; const CHAT_AGENT_EINO_MODES = ['deep', 'plan_execute', 'supervisor']; let multiAgentAPIEnabled = false; +let chatAIChannels = {}; +let chatDefaultAIChannel = ''; +let chatAIChannelIdByNormalizedId = {}; // 人机协同(HITL)会话级配置 const HITL_STORAGE_PREFIX = 'cyberstrike-chat-hitl'; @@ -96,6 +101,190 @@ const HITL_MODE_APPROVAL = 'approval'; const HITL_MODE_REVIEW_EDIT = 'review_edit'; const HITL_MODE_OPTIONS = [HITL_MODE_OFF, HITL_MODE_APPROVAL, HITL_MODE_REVIEW_EDIT]; let hitlApplyFeedbackTimer = null; +let hitlAutoSaveTimer = null; +const sessionSettingsSelects = new Map(); +let sessionSettingsSelectDocBound = false; + +function sessionSettingsSelectLabel(option) { + return option ? (option.textContent || option.label || option.value || '') : ''; +} + +function syncSessionSettingsSelect(select) { + const reg = sessionSettingsSelects.get(select); + if (!reg) return; + const selected = select.options[select.selectedIndex]; + reg.value.textContent = sessionSettingsSelectLabel(selected); + reg.trigger.disabled = !!select.disabled; + reg.wrapper.classList.toggle('is-disabled', !!select.disabled); + reg.menu.innerHTML = ''; + + Array.prototype.forEach.call(select.options, function (option, index) { + const item = document.createElement('button'); + item.type = 'button'; + item.className = 'session-settings-select-option'; + item.setAttribute('role', 'option'); + item.setAttribute('data-index', String(index)); + item.setAttribute('aria-selected', option.selected ? 'true' : 'false'); + item.disabled = !!option.disabled; + item.classList.toggle('is-selected', !!option.selected); + + const label = document.createElement('span'); + label.className = 'session-settings-select-option-label'; + label.textContent = sessionSettingsSelectLabel(option); + item.appendChild(label); + reg.menu.appendChild(item); + }); +} + +function closeSessionSettingsSelect(select) { + const reg = sessionSettingsSelects.get(select); + if (!reg) return; + reg.wrapper.classList.remove('open'); + reg.trigger.setAttribute('aria-expanded', 'false'); +} + +function closeAllSessionSettingsSelects() { + sessionSettingsSelects.forEach(function (_reg, select) { + closeSessionSettingsSelect(select); + }); +} + +function enhanceSessionSettingsSelect(select) { + if (!select || select.dataset.sessionSettingsSelect === '1') { + if (select) syncSessionSettingsSelect(select); + return; + } + const panel = select.closest && select.closest('.conversation-reasoning-card'); + if (!panel) return; + + select.dataset.sessionSettingsSelect = '1'; + select.classList.add('session-settings-native-select'); + select.tabIndex = -1; + select.setAttribute('aria-hidden', 'true'); + + const wrapper = document.createElement('div'); + wrapper.className = 'session-settings-select'; + const trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.className = 'session-settings-select-trigger'; + trigger.setAttribute('aria-haspopup', 'listbox'); + trigger.setAttribute('aria-expanded', 'false'); + const value = document.createElement('span'); + value.className = 'session-settings-select-value'; + const caret = document.createElement('span'); + caret.className = 'session-settings-select-caret'; + caret.setAttribute('aria-hidden', 'true'); + caret.textContent = '⌄'; + trigger.appendChild(value); + trigger.appendChild(caret); + + const menu = document.createElement('div'); + menu.className = 'session-settings-select-menu'; + menu.setAttribute('role', 'listbox'); + + select.parentNode.insertBefore(wrapper, select); + wrapper.appendChild(trigger); + wrapper.appendChild(menu); + wrapper.appendChild(select); + sessionSettingsSelects.set(select, { wrapper: wrapper, trigger: trigger, value: value, menu: menu }); + + trigger.addEventListener('click', function (event) { + event.stopPropagation(); + if (select.disabled) return; + const willOpen = !wrapper.classList.contains('open'); + closeAllSessionSettingsSelects(); + wrapper.classList.toggle('open', willOpen); + trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); + }); + + trigger.addEventListener('keydown', function (event) { + if (select.disabled) return; + const enabled = Array.prototype.filter.call(select.options, function (option) { return !option.disabled; }); + if (!enabled.length) return; + const currentOption = select.options[select.selectedIndex]; + const current = Math.max(0, enabled.indexOf(currentOption)); + let next = current; + if (event.key === 'ArrowDown') next = Math.min(enabled.length - 1, current + 1); + else if (event.key === 'ArrowUp') next = Math.max(0, current - 1); + else if (event.key === 'Home') next = 0; + else if (event.key === 'End') next = enabled.length - 1; + else if (event.key === 'Escape') { + closeSessionSettingsSelect(select); + return; + } else if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + wrapper.classList.add('open'); + trigger.setAttribute('aria-expanded', 'true'); + return; + } else { + return; + } + event.preventDefault(); + const nextOption = enabled[next]; + if (nextOption && select.value !== nextOption.value) { + select.value = nextOption.value; + select.dispatchEvent(new Event('change', { bubbles: true })); + } + syncSessionSettingsSelect(select); + }); + + menu.addEventListener('click', function (event) { + const item = event.target.closest('.session-settings-select-option'); + if (!item || item.disabled) return; + event.stopPropagation(); + const option = select.options[Number(item.dataset.index)]; + if (option && !option.disabled && select.value !== option.value) { + select.value = option.value; + select.dispatchEvent(new Event('change', { bubbles: true })); + } + syncSessionSettingsSelect(select); + closeSessionSettingsSelect(select); + }); + + select.addEventListener('change', function () { + syncSessionSettingsSelect(select); + }); + syncSessionSettingsSelect(select); +} + +function initSessionSettingsSelects() { + const panel = document.getElementById('conversation-reasoning-body'); + if (!panel) return; + panel.querySelectorAll('select').forEach(enhanceSessionSettingsSelect); + if (!sessionSettingsSelectDocBound) { + document.addEventListener('click', closeAllSessionSettingsSelects); + document.addEventListener('keydown', function (event) { + if (event.key === 'Escape') closeAllSessionSettingsSelects(); + }); + sessionSettingsSelectDocBound = true; + } +} + +function refreshSessionSettingsSelects() { + sessionSettingsSelects.forEach(function (_reg, select) { + syncSessionSettingsSelect(select); + }); +} + +function syncChatReasoningBarHeight() { + const inputBar = document.getElementById('chat-input-container'); + const reasoning = document.getElementById('chat-reasoning-wrapper'); + if (!inputBar || !reasoning) return; + const h = Math.ceil(inputBar.getBoundingClientRect().height || 0); + if (h > 0) { + reasoning.style.setProperty('--chat-input-bar-height', h + 'px'); + } +} + +function initChatReasoningBarHeightSync() { + syncChatReasoningBarHeight(); + window.addEventListener('resize', syncChatReasoningBarHeight); + const inputBar = document.getElementById('chat-input-container'); + if (inputBar && typeof ResizeObserver !== 'undefined') { + const ro = new ResizeObserver(syncChatReasoningBarHeight); + ro.observe(inputBar); + } +} /** 非阻塞提示(与 chat-files-toast 样式共用) */ function showChatToast(message, type) { @@ -391,7 +580,7 @@ function readHitlConfigFromForm() { } function updateHitlStatusUI(_cfg) { - /* 侧栏已改为「应用」按钮生效,不再用角标展示模式 */ + /* 侧栏已改为自动保存,不再用角标展示模式 */ } function applyHitlConfigToUI(cfg) { @@ -409,6 +598,7 @@ function applyHitlConfigToUI(cfg) { } if (toolsEl) toolsEl.value = toolsVal; updateHitlStatusUI(conf); + refreshSessionSettingsSelects(); } function bindHitlSidebarModeListener() { @@ -417,6 +607,9 @@ function bindHitlSidebarModeListener() { modeEl.dataset.hitlModeBound = '1'; modeEl.addEventListener('change', function () { applyHitlConfigToUI(readHitlConfigFromForm()); + refreshSessionSettingsSelects(); + scheduleHitlSidebarAutosave(0); + updateChatReasoningSummary(); }); } @@ -458,7 +651,7 @@ function showHitlApplyFeedback(text, isError, partial) { } } -/** 侧栏人机协同:修改模式/白名单后点此写入本地、合并展示并同步服务端 */ +/** 侧栏人机协同:自动写入本地、合并展示并尽量同步服务端 */ async function applyHitlSidebarConfig() { const btn = document.getElementById('hitl-apply-btn'); showHitlApplyFeedback('', false); @@ -486,7 +679,7 @@ async function applyHitlSidebarConfig() { const ok = typeof window.t === 'function' ? window.t('chat.hitlApplyOkSync') : '人机协同配置已保存并同步到服务器。'; showHitlApplyFeedback(ok, false); } else if (yamlMerged) { - const okYaml = typeof window.t === 'function' ? window.t('chat.hitlApplyOkWhitelistYaml') : '免审批工具已合并进 config.yaml 并生效。协同模式、超时等仍须选中会话后再点「应用」才会写入服务器。'; + const okYaml = typeof window.t === 'function' ? window.t('chat.hitlApplyOkWhitelistYaml') : '免审批工具已合并进 config.yaml 并生效。会话配置会自动保存。'; showHitlApplyFeedback(okYaml, false); } else { const localOnly = typeof window.t === 'function' ? window.t('chat.hitlApplyOkLocal') : '已保存到本浏览器。'; @@ -505,6 +698,29 @@ async function applyHitlSidebarConfig() { } } +function scheduleHitlSidebarAutosave(delayMs) { + if (hitlAutoSaveTimer) { + clearTimeout(hitlAutoSaveTimer); + hitlAutoSaveTimer = null; + } + hitlAutoSaveTimer = setTimeout(function () { + hitlAutoSaveTimer = null; + applyHitlSidebarConfig(); + }, typeof delayMs === 'number' ? delayMs : 500); +} + +function bindHitlSensitiveToolsAutosaveListener() { + const toolsEl = document.getElementById('hitl-sensitive-tools'); + if (!toolsEl || toolsEl.dataset.hitlAutosaveBound === '1') return; + toolsEl.dataset.hitlAutosaveBound = '1'; + toolsEl.addEventListener('input', function () { + scheduleHitlSidebarAutosave(700); + }); + toolsEl.addEventListener('blur', function () { + scheduleHitlSidebarAutosave(0); + }); +} + /** 将 localStorage 规范为 eino_single | deep | plan_execute | supervisor */ function chatAgentModeNormalizeStored(stored, cfg) { const pub = cfg && cfg.multi_agent ? cfg.multi_agent : null; @@ -536,6 +752,7 @@ if (typeof window !== 'undefined') { window.getHitlConfigForConversation = getHitlConfigForConversation; bindHitlSidebarModeListener(); bindHitlReviewerToggleListeners(); + bindHitlSensitiveToolsAutosaveListener(); window.setHitlReviewerUI = setHitlReviewerUI; window.onHitlReviewerChanged = onHitlReviewerChanged; window.bindHitlReviewerToggleListeners = bindHitlReviewerToggleListeners; @@ -638,10 +855,85 @@ function syncReasoningRowVisibility(modeVal) { if (!show) { closeChatReasoningPanel(); } else { + syncChatReasoningBarHeight(); updateChatReasoningSummary(); } } +function normalizeChatAIChannelId(s) { + return String(s || '').trim().toLowerCase().replace(/_/g, '-').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); +} + +function resolveChatAIChannelId(id) { + const raw = String(id || '').trim(); + if (!raw) return ''; + if (chatAIChannels[raw]) return raw; + const normalized = normalizeChatAIChannelId(raw); + return normalized && chatAIChannelIdByNormalizedId[normalized] ? chatAIChannelIdByNormalizedId[normalized] : ''; +} + +function populateChatAIChannelSelect(ai) { + const select = document.getElementById('chat-ai-channel-select'); + if (!select) return; + const cfg = ai && typeof ai === 'object' ? ai : {}; + chatAIChannels = cfg.channels && typeof cfg.channels === 'object' ? cfg.channels : {}; + chatAIChannelIdByNormalizedId = {}; + Object.keys(chatAIChannels).forEach(function (id) { + const normalized = normalizeChatAIChannelId(id); + if (normalized && !chatAIChannelIdByNormalizedId[normalized]) { + chatAIChannelIdByNormalizedId[normalized] = id; + } + }); + chatDefaultAIChannel = resolveChatAIChannelId(cfg.default_channel || ''); + select.innerHTML = ''; + const fallbackOpt = document.createElement('option'); + fallbackOpt.value = ''; + fallbackOpt.textContent = typeof window.t === 'function' ? window.t('chat.aiChannelDefault') : '跟随默认通道'; + select.appendChild(fallbackOpt); + Object.keys(chatAIChannels).sort().forEach(function (id) { + const ch = chatAIChannels[id] || {}; + const opt = document.createElement('option'); + opt.value = id; + opt.textContent = (ch.name || id) + (ch.model ? ' · ' + ch.model : ''); + select.appendChild(opt); + }); + let stored = ''; + try { stored = localStorage.getItem(AI_CHANNEL_STORAGE_KEY) || ''; } catch (e) {} + stored = resolveChatAIChannelId(stored); + select.value = stored || ''; + refreshSessionSettingsSelects(); + updateChatReasoningSummary(); +} + +function selectedChatAIChannelId() { + const select = document.getElementById('chat-ai-channel-select'); + return resolveChatAIChannelId(select ? select.value : ''); +} + +function currentChatAIChannelLabel() { + const id = selectedChatAIChannelId() || chatDefaultAIChannel; + const ch = id ? chatAIChannels[id] : null; + if (!ch) { + return typeof window.t === 'function' ? window.t('chat.aiChannelDefaultShort') : '默认通道'; + } + return ch.name || id; +} + +function truncateChatAIChannelSummaryLabel(label) { + const chars = Array.from(String(label || '')); + if (chars.length <= CHAT_AI_CHANNEL_SUMMARY_NAME_MAX) return chars.join(''); + return chars.slice(0, CHAT_AI_CHANNEL_SUMMARY_NAME_MAX).join('') + '...'; +} + +function persistChatAIChannelPref() { + const id = selectedChatAIChannelId(); + try { + if (id) localStorage.setItem(AI_CHANNEL_STORAGE_KEY, id); + else localStorage.removeItem(AI_CHANNEL_STORAGE_KEY); + } catch (e) {} + updateChatReasoningSummary(); +} + function reasoningSummaryModeLabel(mode) { const m = (mode || 'default').trim(); const t = (typeof window.t === 'function') ? window.t : function (k) { return k; }; @@ -662,8 +954,18 @@ function updateChatReasoningSummary() { const effort = effEl && effEl.value ? String(effEl.value).trim() : ''; const t = (typeof window.t === 'function') ? window.t : function (k) { return k; }; const modePart = reasoningSummaryModeLabel(mode); - const effPart = effort || t('chat.reasoningSummaryDash'); - el.textContent = modePart + ' / ' + effPart; + const reasoningPart = effort || modePart || t('chat.reasoningSummaryDash'); + let hitlPart = ''; + try { + const hitlCfg = readHitlConfigFromForm(); + hitlPart = getHitlModeLabel(hitlCfg.mode); + } catch (e) { + hitlPart = ''; + } + const channelPart = currentChatAIChannelLabel(); + const parts = [truncateChatAIChannelSummaryLabel(channelPart), reasoningPart, hitlPart].filter(Boolean); + el.textContent = parts.join(' / '); + el.title = [channelPart, reasoningPart, hitlPart].filter(Boolean).join(' / '); } function closeChatReasoningPanel() { @@ -677,6 +979,7 @@ function toggleConversationReasoningCard() { const wrap = document.getElementById('chat-reasoning-wrapper'); const toggle = document.getElementById('conversation-reasoning-toggle'); if (!wrap || !toggle) return; + syncChatReasoningBarHeight(); wrap.classList.toggle('conversation-reasoning-collapsed'); const collapsed = wrap.classList.contains('conversation-reasoning-collapsed'); toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); @@ -711,6 +1014,7 @@ function restoreChatReasoningControlsFromStorage() { e.value = v; } } + refreshSessionSettingsSelects(); updateChatReasoningSummary(); } catch (err) { /* ignore */ } } @@ -721,6 +1025,7 @@ function persistChatReasoningPrefs() { const elEff = document.getElementById('chat-reasoning-effort'); if (m) localStorage.setItem(REASONING_MODE_LS, m.value || 'default'); if (elEff) localStorage.setItem(REASONING_EFFORT_LS, elEff.value || ''); + refreshSessionSettingsSelects(); updateChatReasoningSummary(); } catch (err) { /* ignore */ } } @@ -746,12 +1051,15 @@ function buildReasoningRequestPayload() { } if (typeof window !== 'undefined') { + window.persistChatAIChannelPref = persistChatAIChannelPref; + window.populateChatAIChannelSelect = populateChatAIChannelSelect; window.persistChatReasoningPrefs = persistChatReasoningPrefs; window.buildReasoningRequestPayload = buildReasoningRequestPayload; window.closeChatReasoningPanel = closeChatReasoningPanel; window.toggleChatReasoningPanel = toggleChatReasoningPanel; window.toggleConversationReasoningCard = toggleConversationReasoningCard; window.updateChatReasoningSummary = updateChatReasoningSummary; + window.refreshSessionSettingsSelects = refreshSessionSettingsSelects; } function closeAgentModePanel() { @@ -826,6 +1134,7 @@ async function initChatAgentModeFromConfig() { if (!r.ok) return; const cfg = await r.json(); multiAgentAPIEnabled = !!(cfg.multi_agent && cfg.multi_agent.enabled); + populateChatAIChannelSelect(cfg.ai || {}); if (typeof window !== 'undefined') { window.__csaiMultiAgentPublic = cfg.multi_agent || null; const tw = cfg.hitl && cfg.hitl.tool_whitelist; @@ -1042,10 +1351,20 @@ async function sendMessage() { conversationId: currentConversationId, role: typeof getCurrentRole === 'function' ? getCurrentRole() : '' }; + let streamConversationId = body.conversationId ? String(body.conversationId) : null; + const isStreamStillVisibleForRequest = function () { + if (!document.getElementById(progressId)) return false; + if (!streamConversationId) return currentConversationId === body.conversationId; + return currentConversationId === streamConversationId; + }; if (!currentConversationId && typeof getActiveProjectId === 'function') { const pid = getActiveProjectId(); if (pid) body.projectId = pid; } + const aiChannelId = selectedChatAIChannelId(); + if (aiChannelId) { + body.aiChannelId = aiChannelId; + } const hitlCfg = readHitlConfigFromForm(); if (normalizeHitlMode(hitlCfg.mode) !== HITL_MODE_OFF) { const sensitiveTools = hitlToolsSplitToArray(hitlCfg.sensitiveTools || ''); @@ -1078,7 +1397,7 @@ async function sendMessage() { window.CyberStrikeChatScroll.onUserSendMessage(); } const progressElement = document.getElementById(progressId); - registerProgressTask(progressId, currentConversationId); + registerProgressTask(progressId, streamConversationId); loadActiveTasks(); let assistantMessageId = null; let mcpExecutionIds = []; @@ -1105,7 +1424,7 @@ async function sendMessage() { window.__csAgentLiveStream = { active: true, - conversationId: currentConversationId || null, + conversationId: streamConversationId || null, progressId: progressId }; try { @@ -1117,9 +1436,26 @@ async function sendMessage() { if (eventData && eventData.type === 'done') { streamSawDone = true; } + const eventConvId = eventData && eventData.data && eventData.data.conversationId + ? String(eventData.data.conversationId) + : ''; + let justBoundConversation = false; + if (eventConvId) { + if (streamConversationId && streamConversationId !== eventConvId) { + return; + } + if (!streamConversationId && eventData.type === 'conversation') { + streamConversationId = eventConvId; + justBoundConversation = true; + } + } + if (!justBoundConversation && !isStreamStillVisibleForRequest()) { + return; + } handleStreamEvent(eventData, progressElement, progressId, () => assistantMessageId, (id) => { assistantMessageId = id; }, - () => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; }); + () => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; }, + { conversationId: streamConversationId }); }; const processSseLines = typeof processSseDataLinesYielding === 'function' ? processSseDataLinesYielding @@ -1157,13 +1493,13 @@ async function sendMessage() { if (typeof loadActiveTasks === 'function') { loadActiveTasks(); } - const convId = currentConversationId || (body && body.conversationId) || null; + const convId = streamConversationId || (body && body.conversationId) || null; let attached = false; if (convId && typeof window.attachRunningTaskEventStream === 'function') { window.__csAgentLiveStream = { active: false, conversationId: null, progressId: null }; attached = await window.attachRunningTaskEventStream(convId).catch(() => false); } - if (!attached) { + if (!attached && isStreamStillVisibleForRequest()) { const hint = typeof window.t === 'function' ? window.t('chat.streamEndedWithoutDone') : '连接提前结束,未收到任务完成信号。任务可能仍在后端执行,请查看顶部运行中任务或刷新当前对话。'; @@ -1186,6 +1522,12 @@ async function sendMessage() { } } catch (error) { + if (!isStreamStillVisibleForRequest()) { + if (typeof loadActiveTasks === 'function') { + loadActiveTasks(); + } + return; + } removeMessage(progressId); const msg = error && error.message != null ? String(error.message) : String(error); const isNetwork = /network|fetch|Failed to fetch|aborted|AbortError|load failed|NetworkError/i.test(msg); @@ -10201,6 +10543,8 @@ function applyCustomIcon() { // 自定义图标输入框回车键处理 document.addEventListener('DOMContentLoaded', function() { + initSessionSettingsSelects(); + initChatReasoningBarHeightSync(); const customInput = document.getElementById('custom-icon-input'); if (customInput) { customInput.addEventListener('keydown', function(e) { diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js index 8666f2f2..43081317 100644 --- a/web/static/js/dashboard.js +++ b/web/static/js/dashboard.js @@ -29,6 +29,16 @@ var dashboardState = { lastProjectSummary: null, // 最近一次项目仪表盘摘要(供 Tab 切换时重绘) }; +function dashboardProjectScopedUrl(url) { + try { + var pid = typeof getActiveProjectId === 'function' ? (getActiveProjectId() || '') : ''; + if (!pid) return url; + return url + (url.indexOf('?') === -1 ? '?' : '&') + 'project_id=' + encodeURIComponent(pid); + } catch (e) { + return url; + } +} + async function refreshDashboard() { const runningEl = document.getElementById('dashboard-running-tasks'); const vulnTotalEl = document.getElementById('dashboard-vuln-total'); @@ -143,11 +153,11 @@ async function refreshDashboard() { // External MCP 健康度 fetchJson('/api/external-mcp/stats'), // WebShell 已建立的连接(pentest 落地后的 foothold,对运营场景非常关键) - fetchJson('/api/webshell/connections'), + fetchJson(dashboardProjectScopedUrl('/api/webshell/connections')), // C2 仪表盘条:监听器 / 会话 / 待处理任务(任务接口含 pending_queued_count) - fetchJson('/api/c2/listeners'), - fetchJson('/api/c2/sessions?limit=500'), - fetchJson('/api/c2/tasks?page=1&page_size=1'), + fetchJson(dashboardProjectScopedUrl('/api/c2/listeners')), + fetchJson(dashboardProjectScopedUrl('/api/c2/sessions?limit=500')), + fetchJson(dashboardProjectScopedUrl('/api/c2/tasks?page=1&page_size=1')), fetchJson('/api/projects/dashboard-summary?fact_limit=10'), selectedSeverityStatus ? fetchJson('/api/vulnerabilities/stats?status=' + encodeURIComponent(selectedSeverityStatus)) : Promise.resolve(null) ]); diff --git a/web/static/js/hitl.js b/web/static/js/hitl.js index 566a99da..7608f94d 100644 --- a/web/static/js/hitl.js +++ b/web/static/js/hitl.js @@ -615,6 +615,28 @@ function reconcileHitlUiState() { let hitlFollowRunSeq = 0; +function hitlAutoResizeTextarea(textarea) { + if (!textarea) return; + textarea.style.height = 'auto'; + textarea.style.height = Math.max(textarea.scrollHeight, textarea.offsetHeight || 0) + 'px'; +} + +function bindHitlAutoResizeTextareas(root) { + const scope = root || document; + if (!scope || !scope.querySelectorAll) return; + scope.querySelectorAll('.hitl-edit-args').forEach(function (textarea) { + if (textarea.__hitlAutoResizeBound) { + hitlAutoResizeTextarea(textarea); + return; + } + textarea.__hitlAutoResizeBound = true; + hitlAutoResizeTextarea(textarea); + textarea.addEventListener('input', function () { + hitlAutoResizeTextarea(textarea); + }); + }); +} + /** * 审批提交后原 SSE 已断开:轮询任务列表,运行中则拉取过程详情;任务结束后再整页加载会话以对齐终态。 */ @@ -855,6 +877,7 @@ async function refreshHitlPending() { } else { container.innerHTML = workflowHtml + (workflowHtml && toolHtml ? '
' : '') + (toolHtml || ''); } + bindHitlAutoResizeTextareas(container); renderHitlPendingPagination(); } catch (e) { hitlPendingLoaded = false; diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 1cab7920..06d57157 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -4,6 +4,7 @@ let userInterruptModalPending = null; let activeTaskInterval = null; const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次 const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']); +const hitlInterruptToolItemMap = new Map(); /** * 主对话 POST 流仍在读取时,禁止再挂 task-events 补流,否则同一事件会画两遍(与 HITL 是否开启无关)。 @@ -19,6 +20,13 @@ function syncAgentLiveStreamConversationId(cid) { } catch (e) { /* ignore */ } } +function setCurrentConversationIdFromStream(cid) { + currentConversationId = cid; + try { + window.currentConversationId = cid; + } catch (e) { /* ignore */ } +} + function shouldSkipTaskEventReplayAttach(conversationId) { try { const live = window.__csAgentLiveStream; @@ -1984,7 +1992,32 @@ function formatEinoRunRetryTitle(data) { // 处理流式事件 function handleStreamEvent(event, progressElement, progressId, - getAssistantId, setAssistantId, getMcpIds, setMcpIds) { + getAssistantId, setAssistantId, getMcpIds, setMcpIds, options) { + const expectedConversationId = options && options.conversationId + ? String(options.conversationId) + : ''; + const eventConversationId = event && event.data && event.data.conversationId + ? String(event.data.conversationId) + : ''; + if (expectedConversationId) { + if (eventConversationId && eventConversationId !== expectedConversationId) { + return; + } + if ( + typeof window.currentConversationId === 'string' && + window.currentConversationId && + window.currentConversationId !== expectedConversationId + ) { + return; + } + const progressNode = progressId ? document.getElementById(progressId) : null; + const progressConversationId = progressNode && progressNode.dataset + ? String(progressNode.dataset.conversationId || '') + : ''; + if (progressConversationId && progressConversationId !== expectedConversationId) { + return; + } + } const streamScrollWasPinned = typeof window.captureScrollPinState === 'function' ? window.captureScrollPinState() : (typeof window.isChatMessagesPinnedToBottom === 'function' ? window.isChatMessagesPinnedToBottom() : true); @@ -2048,7 +2081,7 @@ function handleStreamEvent(event, progressElement, progressId, } // 更新当前对话ID - currentConversationId = event.data.conversationId; + setCurrentConversationIdFromStream(event.data.conversationId); syncAgentLiveStreamConversationId(event.data.conversationId); updateActiveConversation(); addAttackChainButton(currentConversationId); @@ -2408,29 +2441,38 @@ function handleStreamEvent(event, progressElement, progressId, break; case 'hitl_interrupt': - const hitlItemId = addTimelineItem(timeline, 'warning', { - title: '🧑‍⚖️ HITL', - message: event.message, - data: event.data - }); - renderInlineHitlApproval(hitlItemId, event.data || {}); + const hitlTargetItem = findToolCallItemForHitl(timeline, event.data || {}); + if (hitlTargetItem && hitlTargetItem.id) { + renderInlineHitlApproval(hitlTargetItem.id, event.data || {}); + } else { + const hitlItemId = addTimelineItem(timeline, 'hitl_interrupt', { + title: '🧑‍⚖️ HITL', + message: event.message, + data: event.data + }); + renderInlineHitlApproval(hitlItemId, event.data || {}); + } try { window.dispatchEvent(new CustomEvent('hitl-interrupt', { detail: event.data || {} })); } catch (e) {} break; case 'hitl_resumed': - addTimelineItem(timeline, 'progress', { - title: '✅ HITL', - message: event.message, - data: event.data - }); + if (!resolveInlineHitlDecision(timeline, event.data || {}, 'approve', event.message)) { + addTimelineItem(timeline, 'progress', { + title: '✅ HITL', + message: event.message, + data: event.data + }); + } break; case 'hitl_rejected': - addTimelineItem(timeline, 'error', { - title: '⛔ HITL', - message: event.message, - data: event.data - }); + if (!resolveInlineHitlDecision(timeline, event.data || {}, 'reject', event.message)) { + addTimelineItem(timeline, 'error', { + title: '⛔ HITL', + message: event.message, + data: event.data + }); + } break; case 'user_interrupt_continue': { @@ -2801,7 +2843,7 @@ function handleStreamEvent(event, progressElement, progressId, updateProgressConversation(progressId, responseData.conversationId); break; } - currentConversationId = responseData.conversationId; + setCurrentConversationIdFromStream(responseData.conversationId); syncAgentLiveStreamConversationId(responseData.conversationId); updateActiveConversation(); addAttackChainButton(currentConversationId); @@ -2904,7 +2946,7 @@ function handleStreamEvent(event, progressElement, progressId, break; } - currentConversationId = responseData.conversationId; + setCurrentConversationIdFromStream(responseData.conversationId); syncAgentLiveStreamConversationId(responseData.conversationId); updateActiveConversation(); addAttackChainButton(currentConversationId); @@ -3064,7 +3106,7 @@ function handleStreamEvent(event, progressElement, progressId, } // 更新对话ID if (event.data && event.data.conversationId) { - currentConversationId = event.data.conversationId; + setCurrentConversationIdFromStream(event.data.conversationId); syncAgentLiveStreamConversationId(event.data.conversationId); updateActiveConversation(); addAttackChainButton(currentConversationId); @@ -3115,6 +3157,22 @@ function handleStreamEvent(event, progressElement, progressId, function renderInlineHitlApproval(itemId, data) { const item = document.getElementById(itemId); if (!item || !data || !data.interruptId) return; + if (item.classList.contains('timeline-item-tool_call')) { + const state = toolCallDetailStateByItemId.get(item.id) || {}; + state.hitlData = data; + state.pending = true; + setToolCallDetailState(item, state); + if (data.interruptId) { + hitlInterruptToolItemMap.set(String(data.interruptId), item.id); + } + const existingContent = item.querySelector('.timeline-item-content.tool-call-detail-content'); + if (existingContent) { + existingContent.remove(); + item.classList.remove('tool-call-detail-expanded'); + } + renderToolCallDetailContent(item); + return; + } let contentEl = item.querySelector('.timeline-item-content'); if (!contentEl) { // warning 等类型默认没有内容区域;HITL 内联审批需要可交互容器 @@ -3136,31 +3194,165 @@ function renderInlineHitlApproval(itemId, data) { const allowEdit = mode === 'review_edit'; const argsObj = payload.argumentsObj && typeof payload.argumentsObj === 'object' ? payload.argumentsObj : {}; const argsJSON = JSON.stringify(argsObj, null, 2); + const modeLabel = mode === 'review_edit' ? '审查编辑' : '审批模式'; const panel = document.createElement('div'); panel.className = 'hitl-inline-approval'; - panel.innerHTML = ` -
${escapeHtml(toolName)} 待人工审批。模式:${escapeHtml(mode || '-')}。
- ${allowEdit - ? `
审查编辑参数(JSON,可选):留空表示沿用原参数。
- ` - : '
当前模式不支持改参,仅可通过/拒绝。
' + panel.innerHTML = buildInlineHitlApprovalHtml(data, { + toolName: toolName, + mode: mode, + modeLabel: modeLabel, + allowEdit: allowEdit, + argsJSON: argsJSON + }); + contentEl.appendChild(panel); + bindInlineHitlApproval(panel, data, { allowEdit: allowEdit }); +} + +function resolveInlineHitlDecision(timeline, data, decision, message) { + if (!timeline || !data || !data.interruptId) return false; + const interruptId = String(data.interruptId); + let item = null; + const mappedId = hitlInterruptToolItemMap.get(interruptId); + if (mappedId) item = document.getElementById(mappedId); + if (!item) item = findToolCallItemForHitl(timeline, data); + if (!item) { + item = timeline.querySelector('[data-hitl-interrupt-id="' + hitlEscapeAttrSelector(interruptId) + '"]'); + } + if (!item) return false; + + if (item.classList.contains('timeline-item-tool_call')) { + const state = toolCallDetailStateByItemId.get(item.id) || {}; + state.hitlData = Object.assign({}, state.hitlData || data, { + resolved: true, + decision: decision, + decisionMessage: message || '', + comment: data.comment || (state.hitlData && state.hitlData.comment) || '', + editedArgs: data.editedArgs || data.editedArguments || (state.hitlData && (state.hitlData.editedArgs || state.hitlData.editedArguments)) || null + }); + if (decision === 'approve' && state.hitlData.editedArgs && typeof state.hitlData.editedArgs === 'object') { + state.originalArgs = state.originalArgs || state.args || {}; + state.args = state.hitlData.editedArgs; + state.argsEditedByHitl = true; } -
备注(可选):建议写审批依据。
- -
+ state.pending = decision === 'approve'; + setToolCallDetailState(item, state); + const content = item.querySelector('.timeline-item-content.tool-call-detail-content'); + if (content) { + content.remove(); + item.classList.remove('tool-call-detail-expanded'); + } + renderToolCallDetailContent(item); + return true; + } + + const panel = item.querySelector('.hitl-inline-approval'); + if (panel) { + markInlineHitlDecision(panel, decision, message || ''); + return true; + } + return false; +} + +function findToolCallItemForHitl(timeline, data) { + if (!timeline || !data) return null; + const payload = data.payload && typeof data.payload === 'object' ? data.payload : {}; + const toolCallId = String(data.toolCallId || payload.toolCallId || '').trim(); + if (toolCallId) { + const byId = timeline.querySelector('[data-tool-call-id="' + hitlEscapeAttrSelector(toolCallId) + '"]'); + if (byId && byId.classList.contains('timeline-item-tool_call')) return byId; + } + const toolName = String(data.toolName || payload.toolName || '').trim().toLowerCase(); + if (!toolName) return null; + const shortWant = toolName.indexOf('::') >= 0 ? toolName.split('::').pop() : toolName; + const calls = timeline.querySelectorAll('.timeline-item-tool_call'); + for (let i = calls.length - 1; i >= 0; i--) { + const tn = String(calls[i].dataset.toolName || '').trim().toLowerCase(); + const shortTn = tn.indexOf('::') >= 0 ? tn.split('::').pop() : tn; + if (tn === toolName || tn.endsWith('::' + shortWant) || shortTn === shortWant) { + return calls[i]; + } + } + return null; +} + +function buildInlineHitlApprovalHtml(data, opts) { + const hasToolNameOverride = opts && Object.prototype.hasOwnProperty.call(opts, 'toolName'); + const toolName = hasToolNameOverride ? String(opts.toolName || '') : (data.toolName || '-'); + const mode = opts && opts.mode ? opts.mode : String(data.mode || 'approval').trim().toLowerCase(); + const modeLabel = opts && opts.modeLabel ? opts.modeLabel : (mode === 'review_edit' ? '审查编辑' : '审批模式'); + const allowEdit = opts && opts.allowEdit === true; + const argsJSON = opts && opts.argsJSON ? opts.argsJSON : ''; + const toolBadge = toolName + ? '' + escapeHtml(toolName) + '' + : ''; + if (data && data.resolved) { + const ok = data.decision === 'approve'; + const text = data.decisionMessage || (ok ? '已通过,继续执行' : '已拒绝'); + const comment = data.comment ? '' + escapeHtml(data.comment) + '' : ''; + return ` +
+ + ${escapeHtml(ok ? '审批通过' : '审批拒绝')} + ${escapeHtml(text)} + ${comment} +
+ `; + } + return ` +
+
+ + 待审批 +
+
+ ${toolBadge} + ${escapeHtml(modeLabel)} +
+
+
+
确认上方参数后决定是否继续执行。
+ ${allowEdit + ? `` + : '' + } + +
+
+
-
`; - contentEl.appendChild(panel); +} +function autoResizeHitlTextarea(textarea) { + if (!textarea) return; + textarea.style.height = 'auto'; + textarea.style.height = Math.max(textarea.scrollHeight, textarea.offsetHeight || 0) + 'px'; +} + +function bindInlineHitlApproval(panel, data, opts) { const approveBtn = panel.querySelector('.hitl-inline-approve'); const rejectBtn = panel.querySelector('.hitl-inline-reject'); const commentInput = panel.querySelector('.hitl-inline-comment'); const editInput = panel.querySelector('.hitl-inline-edit'); const statusEl = panel.querySelector('.hitl-inline-status'); + const allowEdit = opts && opts.allowEdit === true; + if (!approveBtn || !rejectBtn || !commentInput || !statusEl) return; + + if (editInput) { + autoResizeHitlTextarea(editInput); + editInput.addEventListener('input', function () { + autoResizeHitlTextarea(editInput); + }); + } const setBusy = function (busy) { approveBtn.disabled = busy; @@ -3197,7 +3389,27 @@ function renderInlineHitlApproval(itemId, data) { setBusy(false); return; } - statusEl.textContent = decision === 'approve' ? '已通过,等待执行继续...' : '已拒绝,反馈已交给模型继续迭代...'; + const msg = decision === 'approve' ? '已通过,等待执行继续...' : '已拒绝,反馈已交给模型继续迭代...'; + const toolItem = panel.closest('.timeline-item-tool_call'); + if (toolItem && toolItem.id) { + const state = toolCallDetailStateByItemId.get(toolItem.id) || {}; + state.hitlData = Object.assign({}, state.hitlData || data, { + resolved: true, + decision: decision, + decisionMessage: msg, + comment: comment, + editedArgs: editedArgs + }); + if (decision === 'approve' && editedArgs && typeof editedArgs === 'object') { + state.originalArgs = state.originalArgs || state.args || {}; + state.args = editedArgs; + state.argsEditedByHitl = true; + } + state.pending = decision === 'approve'; + setToolCallDetailState(toolItem, state); + } + statusEl.textContent = msg; + markInlineHitlDecision(panel, decision, msg); panel.classList.add('hitl-inline-done'); } catch (e) { statusEl.textContent = '提交失败:' + (e && e.message ? e.message : 'unknown error'); @@ -3209,6 +3421,19 @@ function renderInlineHitlApproval(itemId, data) { rejectBtn.onclick = function () { submit('reject'); }; } +function markInlineHitlDecision(panel, decision, message) { + if (!panel) return; + const ok = decision === 'approve'; + panel.classList.add('hitl-inline-done'); + panel.innerHTML = ` +
+ + ${escapeHtml(ok ? '审批通过' : '审批拒绝')} + ${escapeHtml(message || (ok ? '已通过,继续执行' : '已拒绝'))} +
+ `; +} + function renderInlineWorkflowHitlApproval(itemId, data) { const item = document.getElementById(itemId); if (!item || !data) return; @@ -3228,15 +3453,28 @@ function renderInlineWorkflowHitlApproval(itemId, data) { const panel = document.createElement('div'); panel.className = 'workflow-hitl-inline-approval hitl-inline-approval'; panel.innerHTML = ` -
${escapeHtml(label)} 等待人工审批。
- ${prompt ? `
${escapeHtml(prompt)}
` : ''} -
备注(可选)
- -
+
+
+ + 工作流审批 +
+
+ ${escapeHtml(label)} + 审批模式 +
+
+
+ ${prompt ? `
${escapeHtml(prompt)}
` : '
工作流暂停,等待你确认是否继续。
'} + +
+
+
-
`; contentEl.appendChild(panel); @@ -3520,8 +3758,8 @@ async function restoreHitlInlineForConversation(conversationId) { payload: payloadObj, conversationId: item.conversationId || conversationId }; - let hitlItemEl = detailsContainer.querySelector('[data-hitl-interrupt-id="' + hitlEscapeAttrSelector(String(item.id)) + '"]'); - if (!hitlItemEl && item.toolCallId) { + let hitlItemEl = null; + if (item.toolCallId) { hitlItemEl = detailsContainer.querySelector('[data-tool-call-id="' + hitlEscapeAttrSelector(String(item.toolCallId)) + '"]'); } if (!hitlItemEl && item.toolName) { @@ -3538,6 +3776,9 @@ async function restoreHitlInlineForConversation(conversationId) { } } } + if (!hitlItemEl) { + hitlItemEl = detailsContainer.querySelector('[data-hitl-interrupt-id="' + hitlEscapeAttrSelector(String(item.id)) + '"]'); + } if (!hitlItemEl) continue; renderInlineHitlApproval(hitlItemEl.id, hitlData); } @@ -3683,7 +3924,16 @@ async function attachRunningTaskEventStream(conversationId) { if (eventData && eventData.type === 'done') { replaySawDone = true; } - handleStreamEvent(eventData, null, progressId, getAssistantIdFn, setAssistantIdFn, function () { return mcpIds; }, function (ids) { mcpIds = mergeMcpExecutionIDLists(mcpIds, ids || []); }); + if (typeof window.currentConversationId === 'string' && window.currentConversationId !== conversationId) { + return; + } + const eventConvId = eventData && eventData.data && eventData.data.conversationId + ? String(eventData.data.conversationId) + : ''; + if (eventConvId && eventConvId !== conversationId) { + return; + } + handleStreamEvent(eventData, null, progressId, getAssistantIdFn, setAssistantIdFn, function () { return mcpIds; }, function (ids) { mcpIds = mergeMcpExecutionIDLists(mcpIds, ids || []); }, { conversationId: conversationId }); }; while (true) { const chunk = await reader.read(); @@ -3842,7 +4092,7 @@ function buildToolResultSectionHtml(data, opts) { }; const execResultLabel = _t('timeline.executionResult'); const execIdLabel = _t('timeline.executionId'); - const waitingLabel = _t('timeline.running'); + const waitingLabel = opts.pendingText || _t('timeline.running'); if (opts.pending) { return ( '
' + @@ -3982,21 +4232,55 @@ async function renderToolCallDetailContent(item) { buildToolResultSectionHtml(state.resultData, { rawText: state.rawText }) + '
'; } else if (state.pending !== false) { + let pendingOpts = { pending: true }; + if (state.hitlData && state.hitlData.interruptId) { + pendingOpts = { + pending: true, + pendingText: state.hitlData.resolved ? '已通过,等待执行结果' : '等待审批,通过后执行' + }; + } resultBlock = '
' + - buildToolResultSectionHtml({}, { pending: true }) + + buildToolResultSectionHtml({}, pendingOpts) + '
'; } const paramsLabel = typeof window.t === 'function' ? window.t('timeline.params') : '参数:'; + const hitlEditedArgsLabel = state.argsEditedByHitl + ? '已按 HITL 改参执行' + : ''; const argsBlock = state.hideArgs ? '' : '
' + '' + escapeHtml(paramsLabel) + '' + + hitlEditedArgsLabel + '
' + escapeHtml(JSON.stringify(args, null, 2)) + '
' + '
'; - content.innerHTML = '
' + argsBlock + resultBlock + '
'; + let hitlBlock = ''; + if (state.hitlData && state.hitlData.interruptId) { + const hitlPayload = state.hitlData.payload && typeof state.hitlData.payload === 'object' ? state.hitlData.payload : {}; + let hitlMode = String(state.hitlData.mode || '').trim().toLowerCase(); + if (hitlMode === 'feedback' || hitlMode === 'followup') hitlMode = 'approval'; + const hitlAllowEdit = hitlMode === 'review_edit'; + const hitlArgsObj = hitlPayload.argumentsObj && typeof hitlPayload.argumentsObj === 'object' ? hitlPayload.argumentsObj : args; + hitlBlock = '
' + + buildInlineHitlApprovalHtml(state.hitlData, { + toolName: '', + mode: hitlMode, + modeLabel: hitlMode === 'review_edit' ? '审查编辑' : '审批模式', + allowEdit: hitlAllowEdit, + argsJSON: JSON.stringify(hitlArgsObj || {}, null, 2) + }) + + '
'; + } + content.innerHTML = '
' + argsBlock + resultBlock + hitlBlock + '
'; item.appendChild(content); item.classList.add('tool-call-detail-expanded'); updateToolDetailToggleLabel(item); + const hitlPanel = content.querySelector('.hitl-inline-approval'); + if (hitlPanel && state.hitlData) { + let bindMode = String(state.hitlData.mode || '').trim().toLowerCase(); + if (bindMode === 'feedback' || bindMode === 'followup') bindMode = 'approval'; + bindInlineHitlApproval(hitlPanel, state.hitlData, { allowEdit: bindMode === 'review_edit' }); + } } if (typeof document !== 'undefined' && !document.__cyberStrikeToolCallDetailToggleBound) { diff --git a/web/static/js/projects.js b/web/static/js/projects.js index 02557d5f..85d0ccd6 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -596,8 +596,12 @@ function prefetchProjectsForChat() { ensureProjectsLoaded().catch(() => {}); } -/** 新对话时默认不绑定项目;用户需主动选择后才写入共享黑板 */ +/** 新对话沿用用户最近选择的项目;没有选择时才保持未绑定。 */ async function ensureDefaultActiveProjectForNewChat() { + const id = getActiveProjectId(); + if (!id) return ''; + const project = await fetchProjectSummary(id).catch(() => null); + if (project && project.id && project.status !== 'archived') return project.id; setActiveProjectId(''); return ''; } @@ -988,6 +992,7 @@ function updateProjectStats(stats) { async function selectProject(id) { currentProjectId = id; + if (id) setActiveProjectId(id); projectAssetsPagination.page = 1; const searchEl = document.getElementById('project-facts-search'); const catEl = document.getElementById('project-facts-filter-category'); diff --git a/web/static/js/settings.js b/web/static/js/settings.js index 59edb33e..d4240967 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -1,5 +1,9 @@ // 设置相关功能 let currentConfig = null; +let selectedAIChannelId = ''; +const AI_CHANNEL_PROBE_CONCURRENCY = 2; +const selectedAIChannelBulkIds = new Set(); +const aiChannelProbeResults = {}; let allTools = []; let alwaysVisibleToolNames = new Set(); let alwaysVisibleBuiltinToolNames = new Set(); @@ -668,39 +672,10 @@ async function loadConfig(loadTools = true, options = {}) { }); } - // 填充OpenAI配置 - const providerEl = document.getElementById('openai-provider'); - if (providerEl) { - providerEl.value = currentConfig.openai.provider || 'openai'; - } - document.getElementById('openai-api-key').value = currentConfig.openai.api_key || ''; - document.getElementById('openai-base-url').value = currentConfig.openai.base_url || ''; - document.getElementById('openai-model').value = currentConfig.openai.model || ''; - const maxTokensEl = document.getElementById('openai-max-total-tokens'); - if (maxTokensEl) { - maxTokensEl.value = currentConfig.openai.max_total_tokens || 120000; - } - const orm = currentConfig.openai && currentConfig.openai.reasoning ? currentConfig.openai.reasoning : {}; - const orModeEl = document.getElementById('openai-reasoning-mode'); - if (orModeEl) { - const mv = (orm.mode || 'auto').toString().trim().toLowerCase(); - orModeEl.value = ['auto', 'on', 'off'].includes(mv) ? mv : 'auto'; - } - const orEffEl = document.getElementById('openai-reasoning-effort'); - if (orEffEl) { - const ev = (orm.effort || '').toString().trim().toLowerCase(); - orEffEl.value = ['', 'low', 'medium', 'high', 'max', 'xhigh'].includes(ev) ? ev : ''; - } - const orProfEl = document.getElementById('openai-reasoning-profile'); - if (orProfEl) { - const pv = (orm.profile || 'auto').toString().trim().toLowerCase(); - const ok = ['auto', 'deepseek_compat', 'openai_compat', 'output_config_effort']; - orProfEl.value = ok.includes(pv) ? pv : 'auto'; - } - const orAllowEl = document.getElementById('openai-reasoning-allow-client'); - if (orAllowEl) { - orAllowEl.checked = orm.allow_client_reasoning !== false; - } + currentConfig.ai = ensureAIConfigShape(currentConfig); + selectedAIChannelId = currentConfig.ai.default_channel; + renderAIChannelSelect(); + writeAIChannelToMainForm(selectedAIChannelId); fillVisionConfigFromCurrent(currentConfig.vision || {}); initModelListControls(); @@ -1897,7 +1872,14 @@ async function applySettings() { }; const wecomAgentIdVal = document.getElementById('robot-wecom-agent-id')?.value.trim(); - const prevOpenai = (currentConfig && currentConfig.openai) ? currentConfig.openai : {}; + if (!currentConfig) currentConfig = {}; + currentConfig.ai = ensureAIConfigShape(currentConfig); + const activeChannelId = normalizeAIChannelId(selectedAIChannelId || currentConfig.ai.default_channel || 'default'); + currentConfig.ai.channels[activeChannelId] = readAIChannelFromMainForm(activeChannelId); + currentConfig.ai.default_channel = activeChannelId; + renderAIChannelSelect(); + const activeChannel = currentConfig.ai.channels[activeChannelId] || {}; + const prevOpenai = activeChannel; const prevRobots = (currentConfig && currentConfig.robots) ? currentConfig.robots : {}; const prevHitl = (currentConfig && currentConfig.hitl) ? currentConfig.hitl : {}; const hitlRetentionRaw = document.getElementById('hitl-retention-days')?.value; @@ -1909,21 +1891,7 @@ async function applySettings() { return String(s || '').split(/[\n,,]/).map(v => v.trim()).filter(Boolean); }; const config = { - openai: { - ...prevOpenai, - provider: provider, - api_key: apiKey, - base_url: baseUrl, - model: model, - max_total_tokens: parseInt(document.getElementById('openai-max-total-tokens')?.value) || 120000, - reasoning: { - ...(prevOpenai.reasoning || {}), - mode: document.getElementById('openai-reasoning-mode')?.value || 'auto', - effort: (document.getElementById('openai-reasoning-effort')?.value || '').trim(), - profile: document.getElementById('openai-reasoning-profile')?.value || 'auto', - allow_client_reasoning: document.getElementById('openai-reasoning-allow-client')?.checked !== false - } - }, + ai: currentConfig.ai, vision: visionPayload, fofa: { api_key: document.getElementById('fofa-api-key')?.value.trim() || '', @@ -2438,6 +2406,534 @@ function enhanceModelPickSelect(selectId) { syncModelPickDropdown(selectId); } +function normalizeAIChannelId(name) { + const raw = String(name || '').trim().toLowerCase().replace(/_/g, '-'); + const id = raw.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); + return id || 'default'; +} + +function escapeAIChannelHtml(value) { + return String(value == null ? '' : value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function ensureAIConfigShape(cfg) { + const ai = cfg && cfg.ai && typeof cfg.ai === 'object' ? cfg.ai : {}; + const channels = ai.channels && typeof ai.channels === 'object' ? { ...ai.channels } : {}; + let def = normalizeAIChannelId(ai.default_channel || ''); + if (!channels[def]) { + const oa = (cfg && cfg.openai) ? cfg.openai : {}; + channels[def] = { + name: def === 'default' ? 'Default' : def, + provider: oa.provider || 'openai', + api_key: oa.api_key || '', + base_url: oa.base_url || '', + model: oa.model || '', + max_total_tokens: oa.max_total_tokens || 120000, + max_completion_tokens: oa.max_completion_tokens || 0, + reasoning: oa.reasoning || {} + }; + } + return { default_channel: def, channels }; +} + +function readAIChannelFromMainForm(id) { + const prev = currentConfig?.ai?.channels?.[id] || {}; + const maxCompletionTokens = parseInt(document.getElementById('openai-max-completion-tokens')?.value, 10) || 16384; + return { + ...prev, + name: (document.getElementById('ai-channel-name')?.value || '').trim() || prev.name || id, + provider: document.getElementById('openai-provider')?.value || 'openai', + api_key: document.getElementById('openai-api-key')?.value.trim() || '', + base_url: document.getElementById('openai-base-url')?.value.trim() || '', + model: document.getElementById('openai-model')?.value.trim() || '', + max_total_tokens: parseInt(document.getElementById('openai-max-total-tokens')?.value, 10) || 120000, + max_completion_tokens: maxCompletionTokens, + reasoning: { + ...(prev.reasoning || {}), + mode: document.getElementById('openai-reasoning-mode')?.value || 'auto', + effort: (document.getElementById('openai-reasoning-effort')?.value || '').trim(), + profile: document.getElementById('openai-reasoning-profile')?.value || 'auto', + allow_client_reasoning: document.getElementById('openai-reasoning-allow-client')?.checked !== false + } + }; +} + +function writeAIChannelToMainForm(id) { + const ai = ensureAIConfigShape(currentConfig || {}); + const ch = ai.channels[id] || ai.channels[ai.default_channel] || {}; + selectedAIChannelId = id || ai.default_channel; + const nameEl = document.getElementById('ai-channel-name'); + if (nameEl) nameEl.value = ch.name || selectedAIChannelId; + const providerEl = document.getElementById('openai-provider'); + if (providerEl) { + const provider = (ch.provider === 'openai' || !ch.provider) ? 'openai_compatible' : ch.provider; + providerEl.value = provider; + } + const keyEl = document.getElementById('openai-api-key'); + if (keyEl) keyEl.value = ch.api_key || ''; + const baseEl = document.getElementById('openai-base-url'); + if (baseEl) baseEl.value = ch.base_url || ''; + const modelEl = document.getElementById('openai-model'); + if (modelEl) modelEl.value = ch.model || ''; + const maxTokensEl = document.getElementById('openai-max-total-tokens'); + if (maxTokensEl) maxTokensEl.value = ch.max_total_tokens || 120000; + const maxCompletionTokensEl = document.getElementById('openai-max-completion-tokens'); + if (maxCompletionTokensEl) maxCompletionTokensEl.value = ch.max_completion_tokens || 16384; + const r = ch.reasoning || {}; + const modeEl = document.getElementById('openai-reasoning-mode'); + if (modeEl) modeEl.value = ['auto', 'on', 'off'].includes(String(r.mode || '').toLowerCase()) ? String(r.mode).toLowerCase() : 'auto'; + const effEl = document.getElementById('openai-reasoning-effort'); + if (effEl) effEl.value = ['', 'low', 'medium', 'high', 'max', 'xhigh'].includes(String(r.effort || '').toLowerCase()) ? String(r.effort || '').toLowerCase() : ''; + const profileEl = document.getElementById('openai-reasoning-profile'); + if (profileEl) profileEl.value = ['auto', 'deepseek_compat', 'openai_compat', 'output_config_effort'].includes(String(r.profile || '').toLowerCase()) ? String(r.profile || '').toLowerCase() : 'auto'; + const allowEl = document.getElementById('openai-reasoning-allow-client'); + if (allowEl) allowEl.checked = r.allow_client_reasoning !== false; + syncModelListFetchButtons(); +} + +function renderAIChannelSelect() { + if (!currentConfig) return; + currentConfig.ai = ensureAIConfigShape(currentConfig); + const select = document.getElementById('ai-channel-select'); + if (!select) return; + select.innerHTML = ''; + const ids = Object.keys(currentConfig.ai.channels || {}).sort(); + ids.forEach((id) => { + const ch = currentConfig.ai.channels[id] || {}; + const opt = document.createElement('option'); + opt.value = id; + const marker = id === currentConfig.ai.default_channel ? ' *' : ''; + opt.textContent = `${ch.name || id}${marker} · ${ch.model || '-'}`; + select.appendChild(opt); + }); + selectedAIChannelId = selectedAIChannelId && currentConfig.ai.channels[selectedAIChannelId] + ? selectedAIChannelId + : currentConfig.ai.default_channel; + select.value = selectedAIChannelId; + renderAIChannelList(ids); + updateAIChannelEditorChrome(selectedAIChannelId); + const countLabel = typeof window.t === 'function' + ? window.t('settingsBasic.aiChannelCount').replace('{count}', String(ids.length)) + : `已保存 ${ids.length} 个通道`; + showAIChannelSaveHint(countLabel, true); +} + +function channelHostLabel(baseUrl) { + const raw = String(baseUrl || '').trim(); + if (!raw) return '-'; + try { + return new URL(raw).host || raw; + } catch (e) { + return raw.replace(/^https?:\/\//, '').split('/')[0] || raw; + } +} + +function renderAIChannelList(ids) { + const list = document.getElementById('ai-channel-list'); + if (!list || !currentConfig?.ai?.channels) return; + list.innerHTML = ''; + (ids || Object.keys(currentConfig.ai.channels).sort()).forEach((id) => { + const ch = currentConfig.ai.channels[id] || {}; + const isDefault = id === currentConfig.ai.default_channel; + const isComplete = !validateSelectedAIChannelPayload(ch); + const probe = aiChannelProbeResults[id] || null; + const item = document.createElement('div'); + item.className = 'ai-channel-list-item' + (id === selectedAIChannelId ? ' active' : '') + (selectedAIChannelBulkIds.has(id) ? ' checked' : ''); + item.setAttribute('role', 'button'); + item.setAttribute('tabindex', '0'); + item.setAttribute('aria-current', id === selectedAIChannelId ? 'true' : 'false'); + item.onclick = () => selectAIChannelForEditing(id); + item.onkeydown = (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + selectAIChannelForEditing(id); + } + }; + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'ai-channel-bulk-check'; + checkbox.checked = selectedAIChannelBulkIds.has(id); + checkbox.setAttribute('aria-label', `选择 ${ch.name || id}`); + checkbox.onclick = (event) => { + event.stopPropagation(); + if (checkbox.checked) { + selectedAIChannelBulkIds.add(id); + } else { + selectedAIChannelBulkIds.delete(id); + } + item.classList.toggle('checked', checkbox.checked); + }; + const defaultBadge = isDefault ? `${escapeAIChannelHtml(settingsT('settingsBasic.aiChannelDefaultBadge', '默认'))}` : ''; + let statusText = isComplete + ? settingsT('settingsBasic.aiChannelReady', '可用') + : settingsT('settingsBasic.aiChannelDraft', '待完善'); + let statusClass = isComplete ? 'ready' : 'draft'; + if (probe) { + statusText = probe.message || statusText; + statusClass = probe.status || statusClass; + } + const body = document.createElement('div'); + body.className = 'ai-channel-card-body'; + body.innerHTML = ` +
+ + ${escapeAIChannelHtml(ch.name || id)} + ${defaultBadge} +
+
${escapeAIChannelHtml(ch.model || '-')} · ${escapeAIChannelHtml(channelHostLabel(ch.base_url))}
+
+ ${escapeAIChannelHtml(statusText)} + ${escapeAIChannelHtml(id)} +
+ `; + item.appendChild(checkbox); + item.appendChild(body); + list.appendChild(item); + }); +} + +function showAIChannelSaveHint(message, ok) { + const el = document.getElementById('ai-channel-save-hint'); + if (!el) return; + el.textContent = message; + el.classList.toggle('is-error', ok === false); + el.classList.toggle('is-success', ok !== false); +} + +function updateAIChannelEditorChrome(id) { + const ai = ensureAIConfigShape(currentConfig || {}); + const channelId = normalizeAIChannelId(id || ai.default_channel || 'default'); + const ch = ai.channels[channelId] || {}; + const title = document.getElementById('ai-channel-editor-title'); + const meta = document.getElementById('ai-channel-editor-meta'); + if (title) title.textContent = ch.name || channelId; + if (meta) { + const parts = [ + channelId === ai.default_channel + ? settingsT('settingsBasic.aiChannelDefaultMeta', '默认通道') + : settingsT('settingsBasic.aiChannelCustomMeta', '自定义通道'), + ch.provider === 'claude' ? 'Claude' : settingsT('settingsBasic.aiChannelOpenAICompat', 'OpenAI 兼容'), + ch.model || settingsT('settingsBasic.aiChannelModelMissing', '未填写模型'), + channelHostLabel(ch.base_url) + ].filter(Boolean); + meta.textContent = parts.join(' / '); + } +} + +function validateSelectedAIChannelPayload(ch) { + const missing = []; + if (!String(ch.base_url || '').trim()) missing.push('Base URL'); + if (!String(ch.api_key || '').trim()) missing.push('API Key'); + if (!String(ch.model || '').trim()) missing.push('模型'); + if (missing.length) { + return missing.join(', '); + } + return ''; +} + +async function persistAIChannelsToServer(successMessage, options = {}) { + if (typeof requirePermission === 'function' && !requirePermission('config:write')) return false; + if (!currentConfig) currentConfig = {}; + currentConfig.ai = ensureAIConfigShape(currentConfig); + const id = normalizeAIChannelId(selectedAIChannelId || currentConfig.ai.default_channel || 'default'); + const channelPayload = readAIChannelFromMainForm(id); + currentConfig.ai.channels[id] = channelPayload; + selectedAIChannelId = id; + const missing = validateSelectedAIChannelPayload(channelPayload); + if (missing) { + showAIChannelSaveHint(`请填写:${missing}`, false); + alert(`请填写:${missing}`); + return false; + } + renderAIChannelSelect(); + showAIChannelSaveHint('正在保存通道...', true); + try { + const shouldMergeLatest = options.mergeLatest !== false; + const latestResponse = shouldMergeLatest ? await apiFetch('/api/config') : null; + if (latestResponse && latestResponse.ok) { + const latest = await latestResponse.json(); + const latestAI = ensureAIConfigShape(latest || {}); + currentConfig.ai.channels = { + ...(latestAI.channels || {}), + ...(currentConfig.ai.channels || {}), + [id]: channelPayload + }; + if (!currentConfig.ai.default_channel) { + currentConfig.ai.default_channel = latestAI.default_channel || id; + } + } + const updateResponse = await apiFetch('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ai: currentConfig.ai }) + }); + if (!updateResponse.ok) { + const error = await updateResponse.json().catch(() => ({})); + throw new Error(error.error || '保存通道失败'); + } + const applyResponse = await apiFetch('/api/config/apply', { method: 'POST' }); + if (!applyResponse.ok) { + const error = await applyResponse.json().catch(() => ({})); + throw new Error(error.error || '应用通道失败'); + } + const response = await apiFetch('/api/config'); + if (response.ok) { + currentConfig = await response.json(); + currentConfig.ai = ensureAIConfigShape(currentConfig); + selectedAIChannelId = currentConfig.ai.channels[id] ? id : currentConfig.ai.default_channel; + renderAIChannelSelect(); + writeAIChannelToMainForm(selectedAIChannelId); + if (typeof populateChatAIChannelSelect === 'function') { + populateChatAIChannelSelect(currentConfig.ai); + } + } + showAIChannelSaveHint(successMessage || '通道已保存', true); + return true; + } catch (error) { + showAIChannelSaveHint(error.message || '保存通道失败', false); + alert(error.message || '保存通道失败'); + return false; + } +} + +async function persistAIConfigOnlyToServer(successMessage) { + if (typeof requirePermission === 'function' && !requirePermission('config:write')) return false; + if (!currentConfig) return false; + currentConfig.ai = ensureAIConfigShape(currentConfig); + showAIChannelSaveHint('正在保存通道...', true); + try { + const updateResponse = await apiFetch('/api/config', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ai: currentConfig.ai }) + }); + if (!updateResponse.ok) { + const error = await updateResponse.json().catch(() => ({})); + throw new Error(error.error || '保存通道失败'); + } + const applyResponse = await apiFetch('/api/config/apply', { method: 'POST' }); + if (!applyResponse.ok) { + const error = await applyResponse.json().catch(() => ({})); + throw new Error(error.error || '应用通道失败'); + } + if (typeof populateChatAIChannelSelect === 'function') { + populateChatAIChannelSelect(currentConfig.ai); + } + showAIChannelSaveHint(successMessage || '通道已保存', true); + return true; + } catch (error) { + showAIChannelSaveHint(error.message || '保存通道失败', false); + alert(error.message || '保存通道失败'); + return false; + } +} + +async function saveSelectedAIChannel() { + await persistAIChannelsToServer(typeof window.t === 'function' ? window.t('settingsBasic.aiChannelSaved') : '通道已保存'); +} + +async function setSelectedAIChannelDefault() { + if (!currentConfig) return; + currentConfig.ai = ensureAIConfigShape(currentConfig); + const id = normalizeAIChannelId(selectedAIChannelId || currentConfig.ai.default_channel || 'default'); + currentConfig.ai.channels[id] = readAIChannelFromMainForm(id); + currentConfig.ai.default_channel = id; + await persistAIChannelsToServer(typeof window.t === 'function' ? window.t('settingsBasic.aiChannelDefaultSaved') : '已设为默认通道'); +} + +function selectAIChannelForEditing(id) { + if (!currentConfig) return; + if (selectedAIChannelId && currentConfig.ai?.channels?.[selectedAIChannelId]) { + currentConfig.ai.channels[selectedAIChannelId] = readAIChannelFromMainForm(selectedAIChannelId); + } + const next = normalizeAIChannelId(id || currentConfig.ai?.default_channel || 'default'); + selectedAIChannelId = next; + writeAIChannelToMainForm(next); + renderAIChannelSelect(); +} + +function uniqueAIChannelId(base) { + const ai = ensureAIConfigShape(currentConfig || {}); + let id = normalizeAIChannelId(base); + if (!ai.channels[id]) return id; + let i = 2; + while (ai.channels[`${id}-${i}`]) i++; + return `${id}-${i}`; +} + +function createAIChannelFromForm() { + if (!currentConfig) currentConfig = {}; + currentConfig.ai = ensureAIConfigShape(currentConfig); + if (selectedAIChannelId && currentConfig.ai.channels[selectedAIChannelId]) { + currentConfig.ai.channels[selectedAIChannelId] = readAIChannelFromMainForm(selectedAIChannelId); + } + const baseName = (typeof window.t === 'function' ? window.t('settingsBasic.aiChannelUntitled') : 'New Channel'); + const id = uniqueAIChannelId(baseName); + currentConfig.ai.channels[id] = { + name: baseName, + provider: 'openai_compatible', + api_key: '', + base_url: '', + model: '', + max_total_tokens: 120000, + max_completion_tokens: 16384, + reasoning: { mode: 'auto', effort: '', profile: 'auto', allow_client_reasoning: true } + }; + selectedAIChannelId = id; + renderAIChannelSelect(); + writeAIChannelToMainForm(id); + showAIChannelSaveHint('新通道尚未保存,填写后点击「保存更改」。', true); +} + +function copyAIChannelFromForm() { + if (!currentConfig) return; + currentConfig.ai = ensureAIConfigShape(currentConfig); + const source = readAIChannelFromMainForm(selectedAIChannelId || currentConfig.ai.default_channel); + const id = uniqueAIChannelId((source.name || selectedAIChannelId || 'channel') + '-copy'); + currentConfig.ai.channels[id] = { ...source, name: (source.name || id) + ' Copy' }; + selectedAIChannelId = id; + renderAIChannelSelect(); + writeAIChannelToMainForm(id); + showAIChannelSaveHint('复制的通道尚未保存,确认后点击「保存更改」。', true); +} + +function deleteSelectedAIChannel() { + if (!currentConfig) return; + currentConfig.ai = ensureAIConfigShape(currentConfig); + const ids = Object.keys(currentConfig.ai.channels || {}); + if (ids.length <= 1) { + alert('至少保留一个 AI 通道'); + return; + } + const id = selectedAIChannelId || currentConfig.ai.default_channel; + const ch = currentConfig.ai.channels[id] || {}; + const name = ch.name || id; + const msg = typeof window.t === 'function' + ? window.t('settingsBasic.aiChannelDeleteConfirm').replace('{name}', name) + : `确定删除 AI 通道「${name}」吗?`; + if (!confirm(msg)) { + return; + } + delete currentConfig.ai.channels[id]; + currentConfig.ai.default_channel = Object.keys(currentConfig.ai.channels).sort()[0]; + renderAIChannelSelect(); + writeAIChannelToMainForm(currentConfig.ai.default_channel); + persistAIChannelsToServer('通道已删除', { mergeLatest: false }); +} + +function selectedOrAllAIChannelIdsForProbe() { + if (!currentConfig) return []; + currentConfig.ai = ensureAIConfigShape(currentConfig); + if (selectedAIChannelId && currentConfig.ai.channels[selectedAIChannelId]) { + currentConfig.ai.channels[selectedAIChannelId] = readAIChannelFromMainForm(selectedAIChannelId); + } + const checked = Array.from(selectedAIChannelBulkIds).filter((id) => currentConfig.ai.channels[id]); + const ids = checked.length ? checked : Object.keys(currentConfig.ai.channels || {}).sort(); + return ids.filter((id) => !validateSelectedAIChannelPayload(currentConfig.ai.channels[id] || {})); +} + +async function probeSelectedAIChannels() { + if (typeof requirePermission === 'function' && !requirePermission('config:write')) return; + const ids = selectedOrAllAIChannelIdsForProbe(); + if (!ids.length) { + alert('没有可探活的完整通道,请先填写 Base URL、API Key 和模型'); + return; + } + showAIChannelSaveHint(`正在探活 ${ids.length} 个通道...`, true); + ids.forEach((id) => { + aiChannelProbeResults[id] = { status: 'testing', message: '测试中...' }; + }); + renderAIChannelList(); + let okCount = 0; + let nextIndex = 0; + async function probeNextAIChannel() { + const id = ids[nextIndex++]; + if (!id) return; + const ch = currentConfig.ai.channels[id] || {}; + try { + const response = await apiFetch('/api/config/test-openai', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + provider: ch.provider || 'openai_compatible', + base_url: ch.base_url || '', + api_key: ch.api_key || '', + model: ch.model || '' + }) + }); + const result = await response.json().catch(() => ({})); + if (response.ok && result.success) { + okCount += 1; + const latency = result.latency_ms ? ` ${result.latency_ms}ms` : ''; + aiChannelProbeResults[id] = { status: 'ready', message: `可用${latency}` }; + } else { + aiChannelProbeResults[id] = { status: 'failed', message: (result.error || '连接失败') }; + } + } catch (error) { + aiChannelProbeResults[id] = { status: 'failed', message: error.message || '测试出错' }; + } + renderAIChannelList(); + } + const workers = Array.from({ length: Math.min(AI_CHANNEL_PROBE_CONCURRENCY, ids.length) }, async function () { + while (nextIndex < ids.length) { + await probeNextAIChannel(); + } + }); + await Promise.all(workers); + showAIChannelSaveHint(`探活完成:${okCount}/${ids.length} 可用`, okCount === ids.length); +} + +async function deleteCheckedAIChannels() { + if (typeof requirePermission === 'function' && !requirePermission('config:write')) return; + if (!currentConfig) return; + currentConfig.ai = ensureAIConfigShape(currentConfig); + const ids = Array.from(selectedAIChannelBulkIds).filter((id) => currentConfig.ai.channels[id]); + if (!ids.length) { + alert('请先勾选要删除的通道'); + return; + } + const deletable = ids.filter((id) => id !== currentConfig.ai.default_channel); + if (!deletable.length) { + alert('默认通道不能批量删除,请先切换默认通道'); + return; + } + if (Object.keys(currentConfig.ai.channels || {}).length - deletable.length < 1) { + alert('至少保留一个 AI 通道'); + return; + } + const names = deletable.map((id) => currentConfig.ai.channels[id]?.name || id).join('、'); + if (!confirm(`确定删除 ${deletable.length} 个 AI 通道吗?\n${names}`)) { + return; + } + deletable.forEach((id) => { + delete currentConfig.ai.channels[id]; + selectedAIChannelBulkIds.delete(id); + delete aiChannelProbeResults[id]; + }); + if (!currentConfig.ai.channels[selectedAIChannelId]) { + selectedAIChannelId = currentConfig.ai.default_channel; + } + renderAIChannelSelect(); + writeAIChannelToMainForm(selectedAIChannelId); + await persistAIConfigOnlyToServer(`已删除 ${deletable.length} 个通道`); +} + +if (typeof window !== 'undefined') { + window.selectAIChannelForEditing = selectAIChannelForEditing; + window.saveSelectedAIChannel = saveSelectedAIChannel; + window.setSelectedAIChannelDefault = setSelectedAIChannelDefault; + window.createAIChannelFromForm = createAIChannelFromForm; + window.copyAIChannelFromForm = copyAIChannelFromForm; + window.deleteSelectedAIChannel = deleteSelectedAIChannel; + window.probeSelectedAIChannels = probeSelectedAIChannels; + window.deleteCheckedAIChannels = deleteCheckedAIChannels; +} + function initModelListControls() { const providerEl = document.getElementById('openai-provider'); if (providerEl && !providerEl.dataset.modelListBound) { @@ -2941,7 +3437,7 @@ async function saveToolsConfig() { // 构建只包含工具配置的配置对象 const config = { - openai: currentConfig.openai || {}, + ai: ensureAIConfigShape(currentConfig || {}), agent: currentConfig.agent || {}, multi_agent: { enabled: currentConfig?.multi_agent?.enabled === true, diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js index 615b3bec..b1bbec4f 100644 --- a/web/static/js/webshell.js +++ b/web/static/js/webshell.js @@ -622,7 +622,8 @@ function getWebshellConnections() { if (typeof apiFetch === 'undefined') { return Promise.resolve([]); } - return apiFetch('/api/webshell/connections', { method: 'GET' }) + var url = '/api/webshell/connections'; + return apiFetch(url, { method: 'GET' }) .then(function (r) { return r.json(); }) .then(function (list) { return Array.isArray(list) ? list : []; }) .catch(function (e) { @@ -631,11 +632,205 @@ function getWebshellConnections() { }); } +function webshellConnectionProjectId(conn) { + return (conn && (conn.project_id || conn.projectId) || '').trim(); +} + +function webshellProjectOptionsHtml(selectedId) { + var selected = String(selectedId || '').trim(); + var html = ''; + var entries = []; + try { + if (typeof projectNameById !== 'undefined') entries = Object.entries(projectNameById); + } catch (e) {} + entries.sort(function (a, b) { + return String(a[1] || '').localeCompare(String(b[1] || ''), undefined, { sensitivity: 'base' }); + }); + entries.forEach(function (entry) { + var id = entry[0]; + var name = entry[1] || id; + if (!id) return; + html += ''; + }); + if (selected && !entries.some(function (entry) { return entry[0] === selected; })) { + html += ''; + } + return html; +} + +var webshellFormSelectMap = {}; +var webshellFormSelectDocBound = false; +var WEBSHELL_FORM_SELECT_CARET = ''; + +function closeAllWebshellFormSelects() { + Object.keys(webshellFormSelectMap).forEach(function (id) { + var reg = webshellFormSelectMap[id]; + if (!reg || !reg.wrapper) return; + reg.wrapper.classList.remove('open'); + if (reg.trigger) reg.trigger.setAttribute('aria-expanded', 'false'); + }); +} + +function syncWebshellFormSelect(select) { + if (!select || !select.id) return; + var reg = webshellFormSelectMap[select.id]; + if (!reg) return; + var dropdown = reg.dropdown; + var trigger = reg.trigger; + var valueSpan = trigger.querySelector('.webshell-form-select-value'); + dropdown.innerHTML = ''; + Array.prototype.forEach.call(select.options, function (opt) { + var item = document.createElement('button'); + item.type = 'button'; + item.className = 'webshell-form-select-option'; + item.setAttribute('role', 'option'); + item.setAttribute('data-value', opt.value); + item.setAttribute('aria-selected', opt.value === select.value ? 'true' : 'false'); + if (opt.value === select.value) item.classList.add('is-selected'); + + var check = document.createElement('span'); + check.className = 'webshell-form-select-check'; + check.textContent = '✓'; + check.setAttribute('aria-hidden', 'true'); + + var label = document.createElement('span'); + label.className = 'webshell-form-select-label'; + label.textContent = opt.textContent; + + item.appendChild(check); + item.appendChild(label); + dropdown.appendChild(item); + }); + var selectedOpt = select.options[select.selectedIndex]; + if (valueSpan) valueSpan.textContent = selectedOpt ? selectedOpt.textContent : ''; + trigger.disabled = !!select.disabled; + reg.wrapper.classList.toggle('is-disabled', !!select.disabled); +} + +function enhanceWebshellFormSelect(select) { + if (!select || !select.id) return; + var existing = webshellFormSelectMap[select.id]; + if (existing && existing.select !== select) delete webshellFormSelectMap[select.id]; + if (select.dataset.webshellFormCustom === '1') { + syncWebshellFormSelect(select); + return; + } + + select.dataset.webshellFormCustom = '1'; + select.classList.add('webshell-form-native-select'); + select.tabIndex = -1; + select.setAttribute('aria-hidden', 'true'); + + var wrapper = document.createElement('div'); + wrapper.className = 'webshell-form-select-ui'; + + var trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.className = 'webshell-form-select-trigger'; + trigger.setAttribute('aria-haspopup', 'listbox'); + trigger.setAttribute('aria-expanded', 'false'); + + var valueSpan = document.createElement('span'); + valueSpan.className = 'webshell-form-select-value'; + trigger.appendChild(valueSpan); + trigger.insertAdjacentHTML('beforeend', WEBSHELL_FORM_SELECT_CARET); + + var dropdown = document.createElement('div'); + dropdown.className = 'webshell-form-select-dropdown'; + dropdown.setAttribute('role', 'listbox'); + + var parent = select.parentNode; + parent.insertBefore(wrapper, select); + wrapper.appendChild(trigger); + wrapper.appendChild(dropdown); + wrapper.appendChild(select); + + webshellFormSelectMap[select.id] = { wrapper: wrapper, trigger: trigger, dropdown: dropdown, select: select }; + + trigger.addEventListener('click', function (e) { + e.stopPropagation(); + if (select.disabled) return; + var open = wrapper.classList.contains('open'); + closeAllWebshellFormSelects(); + if (!open) { + wrapper.classList.add('open'); + trigger.setAttribute('aria-expanded', 'true'); + } + }); + + dropdown.addEventListener('click', function (e) { + var item = e.target.closest('.webshell-form-select-option'); + if (!item) return; + e.stopPropagation(); + var value = item.getAttribute('data-value'); + if (value === null) return; + if (select.value !== value) { + select.value = value; + select.dispatchEvent(new Event('change', { bubbles: true })); + } + wrapper.classList.remove('open'); + trigger.setAttribute('aria-expanded', 'false'); + syncWebshellFormSelect(select); + }); + + select.addEventListener('change', function () { + syncWebshellFormSelect(select); + }); + + syncWebshellFormSelect(select); +} + +function refreshWebshellFormSelects(root) { + var container = root || document.getElementById('webshell-modal'); + if (!container) return; + Object.keys(webshellFormSelectMap).forEach(function (id) { + if (!document.getElementById(id)) delete webshellFormSelectMap[id]; + }); + container.querySelectorAll('select').forEach(enhanceWebshellFormSelect); + if (!webshellFormSelectDocBound) { + webshellFormSelectDocBound = true; + document.addEventListener('click', closeAllWebshellFormSelects); + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') closeAllWebshellFormSelects(); + }); + } +} + +function populateWebshellProjectSelect(selectedId) { + var sel = document.getElementById('webshell-project-id'); + if (!sel) return Promise.resolve(); + var selected = String(selectedId || '').trim(); + sel.innerHTML = webshellProjectOptionsHtml(selected); + sel.value = selected; + syncWebshellFormSelect(sel); + var loadPromise = Promise.resolve([]); + if (typeof ensureProjectsLoaded === 'function') { + loadPromise = ensureProjectsLoaded(); + } else if (typeof fetchAllProjects === 'function') { + loadPromise = fetchAllProjects(false).then(function (list) { + if (typeof rebuildProjectNameMap === 'function') rebuildProjectNameMap(list || []); + return list || []; + }); + } + return loadPromise.then(function () { + sel.innerHTML = webshellProjectOptionsHtml(selected); + sel.value = selected; + syncWebshellFormSelect(sel); + }).catch(function (e) { + console.warn('加载 WebShell 项目选项失败', e); + }); +} + // 从服务端刷新连接列表并重绘侧栏 function refreshWebshellConnectionsFromServer() { return getWebshellConnections().then(function (list) { webshellConnections = list; renderWebshellList(); + if (typeof ensureProjectsLoaded === 'function') { + ensureProjectsLoaded().then(function () { + renderWebshellList(); + }).catch(function () {}); + } return list; }); } @@ -4636,11 +4831,19 @@ function showAddWebshellModal() { if (osSelEl) osSelEl.value = 'auto'; var encSelEl = document.getElementById('webshell-encoding'); if (encSelEl) encSelEl.value = 'auto'; + var defaultProjectId = ''; + try { + defaultProjectId = typeof getActiveProjectId === 'function' ? (getActiveProjectId() || '') : ''; + } catch (e) {} + populateWebshellProjectSelect(defaultProjectId); document.getElementById('webshell-remark').value = ''; var titleEl = document.getElementById('webshell-modal-title'); if (titleEl) titleEl.textContent = wsT('webshell.addConnection'); var modal = document.getElementById('webshell-modal'); - if (modal) openAppModal(modal); + if (modal) { + openAppModal(modal); + refreshWebshellFormSelects(modal); + } } // 打开编辑连接弹窗(预填当前连接信息) @@ -4662,7 +4865,9 @@ function showEditWebshellModal(connId) { if (osEditEl) osEditEl.value = normalizeWebshellOS(conn.os); var encEditEl = document.getElementById('webshell-encoding'); if (encEditEl) encEditEl.value = normalizeWebshellEncoding(conn.encoding); + populateWebshellProjectSelect(webshellConnectionProjectId(conn)); document.getElementById('webshell-remark').value = conn.remark || ''; + refreshWebshellFormSelects(document.getElementById('webshell-modal')); document.getElementById('webshell-url')?.focus(); }); } @@ -4961,7 +5166,9 @@ function saveWebshellConnection() { var editIdEl = document.getElementById('webshell-edit-id'); var editId = editIdEl ? editIdEl.value.trim() : ''; - var body = { url: url, password: password, type: type, method: method === 'get' ? 'get' : 'post', cmd_param: cmdParam, encoding: encoding, os: osTag, remark: remark || url }; + var projectId = (document.getElementById('webshell-project-id') || {}).value || ''; + if (projectId && typeof projectId.trim === 'function') projectId = projectId.trim(); else projectId = ''; + var body = { url: url, password: password, type: type, method: method === 'get' ? 'get' : 'post', cmd_param: cmdParam, encoding: encoding, os: osTag, remark: remark || url, project_id: projectId }; if (typeof apiFetch === 'undefined') return; var reqUrl = editId ? ('/api/webshell/connections/' + encodeURIComponent(editId)) : '/api/webshell/connections'; diff --git a/web/templates/index.html b/web/templates/index.html index ab1d1d44..5cab5501 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -990,7 +990,7 @@
-
- -
-
-
-
- - -
-
- -
- - +
+
AI
+
+ + +
+
+
+ + +
+
+ + +
- -

可在人工与审计 Agent 之间随时切换;规则与白名单不变。人机协同为「关闭」时也可预先选择。

-
- - -

每行一个或逗号分隔;与 config 中全局白名单合并展示。

-
-
- -
-
-
-
- +
+
人机协同
+
+
+
+ + +
+
+ +
+ + +
+ +

可在人工与审计 Agent 之间随时切换;规则与白名单不变。人机协同为「关闭」时也可预先选择。

+
+
+ + +

每行一个或逗号分隔;与 config 中全局白名单合并展示。

+
+
+
+
+
+
+