From d92bbbea0748d0583c4ccfdcc6700217ee89f262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:56:40 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 469 +++++++++++++++++++- web/static/i18n/en-US.json | 73 ++- web/static/i18n/zh-CN.json | 75 +++- web/static/js/audit.js | 2 +- web/static/js/chat.js | 82 +++- web/static/js/hitl.js | 883 ++++++++++++++++++++++++++++++++++++- web/static/js/router.js | 4 +- web/templates/index.html | 171 ++++++- 8 files changed, 1732 insertions(+), 27 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index 4bb3c660..fc4320e1 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -1513,7 +1513,7 @@ header { .hitl-pending-payload { white-space: pre-wrap; word-break: break-all; - max-height: 160px; + max-height: min(20vh, 180px); overflow: auto; margin: 0 0 4px 0; padding: 10px 12px; @@ -1595,6 +1595,473 @@ header { opacity: 0.8; } +.hitl-context-block { + margin-top: 10px; +} + +.hitl-context-label { + font-size: 12px; + font-weight: 600; + color: #475569; + margin-bottom: 4px; +} + +.hitl-context-text { + margin: 0; + padding: 8px 10px; + border-radius: 8px; + background: #f8fafc; + border: 1px solid #e2e8f0; + font-size: 12px; + line-height: 1.45; + white-space: pre-wrap; + word-break: break-word; + max-height: min(24vh, 210px); + overflow: auto; +} + +.hitl-log-modal-content .hitl-context-text, +.hitl-log-detail-payload .hitl-context-text { + max-height: min(28vh, 260px); +} + +.hitl-log-readonly-section { + margin-bottom: 12px; +} + +.hitl-context-block--execution .hitl-context-text { + background: #f0fdf4; + border-color: #bbf7d0; + max-height: min(20vh, 180px); +} + +.hitl-logs-summary { + font-size: 12px; + color: #64748b; + max-width: 140px; +} + +.hitl-reviewer-toggle { + display: inline-flex; + gap: 0; + padding: 3px; + border-radius: 10px; + background: #f1f5f9; + border: 1px solid rgba(15, 23, 42, 0.08); +} + +.hitl-reviewer-toggle-btn { + appearance: none; + border: none; + background: transparent; + color: #64748b; + font-size: 13px; + font-weight: 500; + line-height: 1.2; + padding: 8px 12px; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; + white-space: nowrap; +} + +.hitl-reviewer-toggle-btn:hover { + color: #0f172a; +} + +.hitl-reviewer-toggle-btn.is-active { + background: #fff; + color: var(--accent-color, #0066ff); + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12); +} + +.hitl-reviewer-toggle-btn:focus-visible { + outline: 2px solid var(--accent-color, #0066ff); + outline-offset: 2px; +} + +.hitl-page-reviewer-bar { + margin-bottom: 16px; + padding: 14px 16px; + border-radius: 14px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: #fff; + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); +} + +.hitl-page-reviewer-main { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px 16px; +} + +.hitl-page-reviewer-label { + font-size: 14px; + font-weight: 600; + color: #0f172a; +} + +.hitl-page-reviewer-hint { + margin: 10px 0 0; + font-size: 12px; + line-height: 1.5; + color: #64748b; +} + +.hitl-page-whitelist-bar { + width: 100%; + padding: 14px 16px; + border-radius: 14px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: #fff; + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); + box-sizing: border-box; +} + +.hitl-page-whitelist-header { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 8px 12px; + margin-bottom: 8px; +} + +.hitl-page-whitelist-label { + font-size: 14px; + font-weight: 600; + color: #0f172a; +} + +.hitl-page-whitelist-hint { + margin: 0 0 10px; + font-size: 12px; + line-height: 1.5; + color: #64748b; +} + +.hitl-page-whitelist-textarea { + width: 100%; + min-height: 140px; + box-sizing: border-box; + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: 10px; + padding: 10px 12px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 12px; + line-height: 1.55; + color: #0f172a; + background: #f8fafc; + resize: vertical; +} + +.hitl-page-whitelist-textarea:focus { + outline: none; + border-color: var(--accent-color, #0066ff); + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.12); + background: #fff; +} + +.hitl-page-strategy-bar { + width: 100%; + padding: 14px 16px; + border-radius: 14px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: #fff; + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); + box-sizing: border-box; +} + +.hitl-page-strategy-header { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 8px 12px; + margin-bottom: 8px; +} + +.hitl-page-strategy-label { + font-size: 14px; + font-weight: 600; + color: #0f172a; +} + +.hitl-page-strategy-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.hitl-strategy-subtabs { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 0 0 8px; +} + +.hitl-strategy-subtab { + border: 1px solid rgba(15, 23, 42, 0.12); + background: #f8fafc; + color: #475569; + border-radius: 8px; + padding: 5px 12px; + font-size: 12px; + font-weight: 500; + cursor: pointer; +} + +.hitl-strategy-subtab:hover { + background: #f1f5f9; + color: #0f172a; +} + +.hitl-strategy-subtab--active { + background: #e0e7ff; + border-color: #a5b4fc; + color: #3730a3; +} + +.hitl-page-strategy-hint { + margin: 0 0 10px; + font-size: 12px; + line-height: 1.5; + color: #64748b; +} + +.hitl-strategy-textarea { + width: 100%; + min-height: 320px; + box-sizing: border-box; + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: 10px; + padding: 10px 12px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 12px; + line-height: 1.55; + color: #0f172a; + background: #f8fafc; + resize: vertical; +} + +.hitl-strategy-textarea:focus { + outline: none; + border-color: var(--accent-color, #0066ff); + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.12); + background: #fff; +} + +.hitl-page-tabs { + display: flex; + gap: 8px; + margin-bottom: 16px; + border-bottom: 1px solid #e2e8f0; + padding-bottom: 0; +} + +.hitl-page-tab { + appearance: none; + border: none; + background: transparent; + color: var(--text-secondary, #64748b); + font-size: 14px; + font-weight: 500; + padding: 10px 14px; + border-bottom: 2px solid transparent; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.hitl-page-tab--active { + color: var(--accent-color, #0066ff); + border-bottom-color: var(--accent-color, #0066ff); +} + +.hitl-tab-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 6px; + border-radius: 999px; + background: #ef4444; + color: #fff; + font-size: 11px; + font-weight: 600; +} + +.hitl-filters { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: flex-end; + margin-bottom: 16px; +} + +.hitl-filters label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 12px; + color: var(--text-secondary, #64748b); +} + +.hitl-filter-input, +.hitl-filter-select { + min-width: 220px; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 8px 10px; + font-size: 13px; + background: #fff; +} + +.hitl-logs-table-wrap { + overflow-x: auto; + border: 1px solid #e2e8f0; + border-radius: 10px; + background: #fff; +} + +.hitl-logs-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.hitl-logs-table th, +.hitl-logs-table td { + padding: 10px 12px; + border-bottom: 1px solid #f1f5f9; + text-align: left; + vertical-align: top; +} + +.hitl-logs-table th { + background: #f8fafc; + color: #475569; + font-weight: 600; + white-space: nowrap; +} + +.hitl-logs-cell-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 12px; + max-width: 180px; + word-break: break-all; +} + +.hitl-logs-actions { + white-space: nowrap; +} + +.hitl-decision-tag { + display: inline-block; + padding: 2px 8px; + border-radius: 999px; + font-size: 12px; + background: #f1f5f9; + color: #334155; +} + +.hitl-decision-tag.hitl-decision--approve { + background: #dcfce7; + color: #166534; +} + +.hitl-decision-tag.hitl-decision--reject { + background: #fee2e2; + color: #991b1b; +} + +.hitl-logs-pagination, +.hitl-pending-pagination { + margin-top: 12px; + padding: 0; +} + +.hitl-logs-pagination .monitor-pagination, +.hitl-pending-pagination .monitor-pagination { + margin-top: 0; + border: 1px solid var(--border-color); + border-radius: 8px; +} + +.hitl-log-modal-content { + max-width: 640px; + width: calc(100vw - 32px); +} + +.hitl-log-detail-meta { + margin: 0; + padding: 0; + display: grid; + gap: 10px; +} + +.hitl-log-detail-row { + display: grid; + grid-template-columns: 88px 1fr; + gap: 12px; + align-items: start; + margin: 0; +} + +.hitl-log-detail-row--full { + grid-template-columns: 1fr; +} + +.hitl-log-detail-row--full dt { + margin-bottom: 4px; +} + +.hitl-log-detail-row dt { + margin: 0; + font-size: 12px; + font-weight: 600; + color: #64748b; +} + +.hitl-log-detail-row dd { + margin: 0; + font-size: 14px; + color: #0f172a; + word-break: break-word; +} + +.hitl-log-detail-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 12px; +} + +.hitl-log-detail-payload { + margin-top: 14px; +} + +.hitl-logs-empty-hint { + margin: 8px 0 0; + font-size: 13px; + color: #64748b; +} + +.btn-link { + appearance: none; + border: none; + background: none; + color: var(--accent-color, #0066ff); + cursor: pointer; + font-size: 13px; + padding: 0 6px 0 0; +} + +.btn-link--danger { + color: #dc2626; +} + .hitl-config-select:focus, .hitl-config-textarea:focus { outline: none; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 0a018bda..af2fca55 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -645,7 +645,10 @@ "agentModeOrchSupervisor": "Supervisor", "hitlTitle": "Human-in-the-loop", "hitlCardSubtitle": "Approvals & allowlist", - "hitlReviewer": "Review", + "hitlReviewerLabel": "Reviewer", + "hitlReviewerHuman": "Human approval", + "hitlReviewerAgent": "Audit Agent", + "hitlReviewerHint": "Switch between human and Audit Agent anytime; rules and whitelist stay the same. You can pre-select even when HITL is off.", "hitlConfigTitle": "Collaboration mode config", "hitlModeLabel": "Mode", "hitlModeOff": "Off", @@ -664,7 +667,75 @@ }, "hitl": { "pageTitle": "HITL approvals", + "pageReviewerLabel": "Current reviewer", + "pageReviewerHint": "Applies to the selected conversation. Without a conversation, saved locally for new chats. Takes effect immediately.", + "pageReviewerSaved": "Reviewer saved.", + "whitelistLabel": "Tool whitelist (no approval)", + "whitelistHint": "One per line or comma-separated. Saved to config.yaml global whitelist and takes effect immediately (synced with chat sidebar).", + "whitelistSaved": "Whitelist saved.", + "whitelistSaveFailed": "Failed to save whitelist", + "strategyLabel": "Audit strategy (prompt)", + "strategyHint": "Whitelisted tools skip approval. Other tools are judged by the model using this prompt when Audit Agent is selected.", + "strategyTabApproval": "Approval mode", + "strategyTabReviewEdit": "Review & edit mode", + "strategyHintApproval": "Whitelisted tools skip approval. In approval mode the Audit Agent only approves or rejects.", + "strategyHintReviewEdit": "In review & edit mode the Audit Agent may narrow parameters via editedArguments before approve; reject if parameters cannot be safely adjusted.", + "strategyReset": "Reset to default", + "strategySaved": "Audit strategy saved.", + "strategySaveFailed": "Failed to save audit strategy", + "tabPending": "Pending", + "tabLogs": "Audit logs", + "tabStrategy": "Audit strategy", + "tabWhitelist": "Tool whitelist", "pendingTitle": "Pending approvals", + "searchLabel": "Search", + "searchPlaceholder": "Tool, conversation, payload, comment…", + "searchApply": "Search", + "filterDecision": "Decision", + "filterDecidedBy": "Reviewer", + "filterAll": "All", + "decisionApprove": "Approve", + "decisionReject": "Reject", + "reviewerHuman": "Human", + "reviewerAgent": "Audit Agent", + "reviewerSystem": "System", + "reviewerManual": "Manual entry", + "logCreate": "New log", + "logModalTitle": "Audit log", + "logModalEdit": "Edit audit log", + "fieldConversation": "Conversation ID", + "fieldTool": "Tool name", + "fieldComment": "Comment", + "fieldPayload": "Payload (JSON)", + "fieldUserMessage": "User message", + "fieldThinking": "Thinking", + "fieldReasoning": "Reasoning chain", + "fieldPlanning": "Planning", + "colId": "ID", + "colTool": "Tool", + "colConversation": "Conversation", + "colDecision": "Decision", + "colDecidedBy": "Reviewer", + "colContext": "Context", + "colTime": "Time", + "colActions": "Actions", + "viewDetail": "Detail", + "logModalView": "Audit log detail", + "fieldExecutionResult": "Execution result", + "executionSuccess": "success", + "executionFailed": "failed", + "edit": "Edit", + "delete": "Delete", + "logsEmpty": "No audit logs", + "logsEmptyHint": "Records are created automatically when HITL approvals are approved or rejected.", + "pageInfo": "{{total}} total", + "prevPage": "Previous", + "nextPage": "Next", + "conversationRequired": "Conversation ID is required", + "toolRequired": "Tool name is required", + "saveFailed": "Save failed", + "deleteConfirm": "Delete this audit log?", + "deleteFailed": "Delete failed", "loading": "Loading...", "emptyState": "No pending approvals", "dismiss": "Dismiss", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index bcff1b72..be583e13 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -633,7 +633,10 @@ "agentModeOrchSupervisor": "Supervisor", "hitlTitle": "人机协同", "hitlCardSubtitle": "审批与白名单", - "hitlReviewer": "Review", + "hitlReviewerLabel": "审批方", + "hitlReviewerHuman": "人工审批", + "hitlReviewerAgent": "审计 Agent", + "hitlReviewerHint": "可在人工与审计 Agent 之间随时切换;规则与白名单不变。人机协同为「关闭」时也可预先选择。", "hitlConfigTitle": "协同模式配置", "hitlModeLabel": "模式", "hitlModeOff": "关闭", @@ -642,7 +645,7 @@ "hitlSensitiveTools": "敏感工具(逗号分隔)", "hitlWhitelistTools": "白名单工具(免审批,逗号分隔)", "hitlWhitelistPlaceholder": "例:read_file, grep 或每行一个工具名(与 config 全局白名单合并)", - "hitlWhitelistHint": "每行一个或逗号分隔;与 config 中全局白名单合并展示。", + "hitlWhitelistHint": "白名单内工具免审批;每行一个或逗号分隔,与 config 全局白名单合并。", "hitlApply": "应用", "hitlApplyOkSync": "人机协同配置已保存并同步到服务器。", "hitlApplyOkWhitelistYaml": "免审批工具已合并进 config.yaml 并生效。协同模式、超时等仍须选中会话后再点「应用」才会写入服务器。", @@ -652,7 +655,75 @@ }, "hitl": { "pageTitle": "人机协同审批", + "pageReviewerLabel": "当前审批方", + "pageReviewerHint": "作用于当前选中会话;未选会话时保存到本机,新建会话时沿用。切换后立即生效。", + "pageReviewerSaved": "审批方已保存。", + "whitelistLabel": "免审批工具白名单", + "whitelistHint": "每行一个或逗号分隔;保存后写入 config.yaml 全局白名单并立即生效(与聊天侧栏同步展示)。", + "whitelistSaved": "白名单已保存。", + "whitelistSaveFailed": "保存白名单失败", + "strategyLabel": "审计策略(提示词)", + "strategyHint": "白名单内工具免审批;其余工具在审批方为「审计 Agent」时,由模型按此提示词自主裁决。", + "strategyTabApproval": "审批模式", + "strategyTabReviewEdit": "审查编辑模式", + "strategyHintApproval": "白名单内工具免审批;审批模式下审计 Agent 仅裁决通过/拒绝。", + "strategyHintReviewEdit": "审查编辑模式下审计 Agent 可通过 editedArguments 收窄参数后放行;无法安全改参时应拒绝。", + "strategyReset": "恢复默认", + "strategySaved": "审计策略已保存。", + "strategySaveFailed": "保存审计策略失败", + "tabPending": "待审计", + "tabLogs": "审计日志", + "tabStrategy": "审计策略", + "tabWhitelist": "工具白名单", "pendingTitle": "待处理审批", + "searchLabel": "搜索", + "searchPlaceholder": "工具名、会话 ID、载荷、备注…", + "searchApply": "搜索", + "filterDecision": "决策", + "filterDecidedBy": "审批方", + "filterAll": "全部", + "decisionApprove": "通过", + "decisionReject": "拒绝", + "reviewerHuman": "人工", + "reviewerAgent": "审计 Agent", + "reviewerSystem": "系统", + "reviewerManual": "手动录入", + "logCreate": "新建日志", + "logModalTitle": "审计日志", + "logModalEdit": "编辑审计日志", + "fieldConversation": "会话 ID", + "fieldTool": "工具名", + "fieldComment": "备注", + "fieldPayload": "载荷 (JSON)", + "fieldUserMessage": "用户原话", + "fieldThinking": "本轮思考", + "fieldReasoning": "推理链", + "fieldPlanning": "规划", + "colId": "ID", + "colTool": "工具", + "colConversation": "会话", + "colDecision": "决策", + "colDecidedBy": "审批方", + "colContext": "上下文", + "colTime": "时间", + "colActions": "操作", + "viewDetail": "详情", + "logModalView": "审计日志详情", + "fieldExecutionResult": "执行结果", + "executionSuccess": "成功", + "executionFailed": "失败", + "edit": "编辑", + "delete": "删除", + "logsEmpty": "暂无审计日志", + "logsEmptyHint": "人机协同审批通过或拒绝后会自动记录在此。", + "pageInfo": "共 {{total}} 条", + "prevPage": "上一页", + "nextPage": "下一页", + "conversationRequired": "请填写会话 ID", + "toolRequired": "请填写工具名", + "saveFailed": "保存失败", + "deleteConfirm": "确定删除这条审计日志?", + "deleteFailed": "删除失败", "loading": "加载中...", "emptyState": "暂无待审批项", "dismiss": "忽略", diff --git a/web/static/js/audit.js b/web/static/js/audit.js index 52fd7cd1..44e323f8 100644 --- a/web/static/js/audit.js +++ b/web/static/js/audit.js @@ -22,7 +22,7 @@ const AUDIT_ACTIONS_BY_CATEGORY = { task: ['create_queue', 'start_queue', 'delete_queue', 'pause_queue', 'rerun_queue', 'delete_batch_task'], tool: ['execution_delete', 'execution_delete_batch'], file: ['upload', 'delete'], - hitl: ['decision'], + hitl: ['decision', 'audit_strategy_update'], role: ['create', 'update', 'delete'], skill: ['create', 'update', 'delete'], agent: ['markdown_create', 'markdown_update', 'markdown_delete'] diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 77cb0aeb..b44d03d0 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -139,11 +139,18 @@ function normalizeHitlMode(mode) { function defaultHitlConfig() { return { mode: HITL_MODE_OFF, + reviewer: 'human', sensitiveTools: '', updatedAt: '' }; } +function normalizeHitlReviewer(v) { + const x = String(v || '').trim().toLowerCase(); + if (x === 'audit_agent' || x === 'agent' || x === 'ai') return 'audit_agent'; + return 'human'; +} + /** 白名单字符串拆成数组(逗号或换行分隔,与 textarea 一致) */ function hitlToolsSplitToArray(s) { return String(s || '') @@ -218,6 +225,7 @@ function getHitlLastGlobalConfig() { if (!parsed || typeof parsed !== 'object') return null; return { mode: normalizeHitlMode(parsed.mode), + reviewer: normalizeHitlReviewer(parsed.reviewer), sensitiveTools: typeof parsed.sensitiveTools === 'string' ? parsed.sensitiveTools : fallback.sensitiveTools, updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '' }; @@ -248,6 +256,7 @@ function getHitlConfigForConversation(conversationId) { if (parsed && typeof parsed === 'object') { draftCfg = { mode: normalizeHitlMode(parsed.mode), + reviewer: normalizeHitlReviewer(parsed.reviewer), sensitiveTools: typeof parsed.sensitiveTools === 'string' ? parsed.sensitiveTools : fallback.sensitiveTools, updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '' }; @@ -258,6 +267,7 @@ function getHitlConfigForConversation(conversationId) { } const g = globalLast ? { mode: normalizeHitlMode(globalLast.mode), + reviewer: normalizeHitlReviewer(globalLast.reviewer), sensitiveTools: typeof globalLast.sensitiveTools === 'string' ? globalLast.sensitiveTools : fallback.sensitiveTools, updatedAt: typeof globalLast.updatedAt === 'string' ? globalLast.updatedAt : '' } : null; @@ -280,6 +290,7 @@ function getHitlConfigForConversation(conversationId) { } return { mode: normalizeHitlMode(parsed.mode), + reviewer: normalizeHitlReviewer(parsed.reviewer), sensitiveTools: typeof parsed.sensitiveTools === 'string' ? parsed.sensitiveTools : fallback.sensitiveTools, updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '' }; @@ -288,10 +299,52 @@ function getHitlConfigForConversation(conversationId) { } } +function setHitlReviewerUI(reviewer) { + const v = normalizeHitlReviewer(reviewer); + const hidden = document.getElementById('hitl-reviewer-select'); + if (hidden) hidden.value = v; + document.querySelectorAll('.hitl-reviewer-toggle-btn').forEach(function (btn) { + const active = btn.getAttribute('data-reviewer') === v; + btn.classList.toggle('is-active', active); + btn.setAttribute('aria-pressed', active ? 'true' : 'false'); + }); +} + +async function onHitlReviewerChanged(reviewer) { + setHitlReviewerUI(reviewer); + const cfg = readHitlConfigFromForm(); + const cid = typeof currentConversationId === 'string' ? currentConversationId.trim() : ''; + saveHitlConfigForConversation(cid, cfg, { syncGlobalLast: true }); + if (cid && typeof window.saveHitlConversationConfig === 'function') { + try { + await window.saveHitlConversationConfig(cid, cfg); + const ok = typeof window.t === 'function' ? window.t('hitl.pageReviewerSaved') : '审批方已保存。'; + showChatToast(ok, 'success'); + } catch (e) { + console.warn('onHitlReviewerChanged', e); + const prefix = typeof window.t === 'function' ? window.t('chat.hitlApplyFail') : '同步到服务器失败'; + showChatToast(prefix, 'error'); + } + } +} + +function bindHitlReviewerToggleListeners() { + document.querySelectorAll('.hitl-reviewer-toggle-btn').forEach(function (btn) { + if (btn.dataset.hitlReviewerBound === '1') return; + btn.dataset.hitlReviewerBound = '1'; + btn.addEventListener('click', function () { + const v = btn.getAttribute('data-reviewer'); + if (!v) return; + onHitlReviewerChanged(v); + }); + }); +} + function saveHitlConfigForConversation(conversationId, cfg, opts) { const syncGlobalLast = !!(opts && opts.syncGlobalLast); const payload = { mode: normalizeHitlMode(cfg && cfg.mode), + reviewer: normalizeHitlReviewer(cfg && cfg.reviewer), sensitiveTools: typeof (cfg && cfg.sensitiveTools) === 'string' ? cfg.sensitiveTools : '', updatedAt: typeof (cfg && cfg.updatedAt) === 'string' ? cfg.updatedAt : '' }; @@ -308,8 +361,10 @@ function saveHitlConfigForConversation(conversationId, cfg, opts) { function readHitlConfigFromForm() { const modeEl = document.getElementById('hitl-mode-select'); + const reviewerEl = document.getElementById('hitl-reviewer-select'); const toolsEl = document.getElementById('hitl-sensitive-tools'); const mode = normalizeHitlMode(modeEl ? modeEl.value : HITL_MODE_OFF); + const reviewer = normalizeHitlReviewer(reviewerEl ? reviewerEl.value : 'human'); let sensitiveTools = toolsEl ? String(toolsEl.value || '').trim() : ''; const g = typeof window !== 'undefined' ? window.csaiHitlGlobalToolWhitelist : null; if (Array.isArray(g) && g.length > 0) { @@ -317,6 +372,7 @@ function readHitlConfigFromForm() { } return { mode, + reviewer, sensitiveTools, updatedAt: new Date().toISOString() }; @@ -330,7 +386,9 @@ function applyHitlConfigToUI(cfg) { const conf = cfg || defaultHitlConfig(); const modeEl = document.getElementById('hitl-mode-select'); const toolsEl = document.getElementById('hitl-sensitive-tools'); - if (modeEl) modeEl.value = normalizeHitlMode(conf.mode); + const uiMode = normalizeHitlMode(conf.mode); + if (modeEl) modeEl.value = uiMode; + setHitlReviewerUI(conf.reviewer); let toolsVal = conf.sensitiveTools || ''; const g = typeof window !== 'undefined' ? window.csaiHitlGlobalToolWhitelist : null; if (Array.isArray(g) && g.length > 0) { @@ -341,6 +399,15 @@ function applyHitlConfigToUI(cfg) { updateHitlStatusUI(conf); } +function bindHitlSidebarModeListener() { + const modeEl = document.getElementById('hitl-mode-select'); + if (!modeEl || modeEl.dataset.hitlModeBound === '1') return; + modeEl.dataset.hitlModeBound = '1'; + modeEl.addEventListener('change', function () { + applyHitlConfigToUI(readHitlConfigFromForm()); + }); +} + function refreshHitlConfigByCurrentConversation() { const cfg = getHitlConfigForConversation(currentConversationId || ''); applyHitlConfigToUI(cfg); @@ -413,6 +480,9 @@ async function applyHitlSidebarConfig() { const localOnly = typeof window.t === 'function' ? window.t('chat.hitlApplyOkLocal') : '已保存到本浏览器。'; showHitlApplyFeedback(localOnly, false); } + if (typeof window.refreshHitlPageWhitelist === 'function') { + window.refreshHitlPageWhitelist(); + } } catch (e) { console.warn('applyHitlSidebarConfig', e); const prefix = typeof window.t === 'function' ? window.t('chat.hitlApplyFail') : '同步到服务器失败'; @@ -449,6 +519,12 @@ if (typeof window !== 'undefined') { window.readHitlConfigFromForm = readHitlConfigFromForm; window.applyHitlConfigToUI = applyHitlConfigToUI; window.saveHitlConfigForConversation = saveHitlConfigForConversation; + window.getHitlConfigForConversation = getHitlConfigForConversation; + bindHitlSidebarModeListener(); + bindHitlReviewerToggleListeners(); + window.setHitlReviewerUI = setHitlReviewerUI; + window.onHitlReviewerChanged = onHitlReviewerChanged; + window.bindHitlReviewerToggleListeners = bindHitlReviewerToggleListeners; window.getHitlLastGlobalConfig = getHitlLastGlobalConfig; window.hitlMergeToolsForDisplay = hitlMergeToolsForDisplay; window.hitlStripGlobalToolsFromFormString = hitlStripGlobalToolsFromFormString; @@ -743,6 +819,9 @@ async function initChatAgentModeFromConfig() { window.csaiHitlGlobalToolWhitelist = tw.slice(); } } + if (typeof window.refreshHitlPageWhitelist === 'function') { + window.refreshHitlPageWhitelist(); + } document.querySelectorAll('.agent-mode-option').forEach(function (el) { const v = el.getAttribute('data-value'); if (v === 'deep' || v === 'plan_execute' || v === 'supervisor') { @@ -959,6 +1038,7 @@ async function sendMessage() { body.hitl = { enabled: true, mode: normalizeHitlMode(hitlCfg.mode), + reviewer: normalizeHitlReviewer(hitlCfg.reviewer), sensitiveTools: sensitiveTools }; } diff --git a/web/static/js/hitl.js b/web/static/js/hitl.js index 142bdbac..b6907939 100644 --- a/web/static/js/hitl.js +++ b/web/static/js/hitl.js @@ -1,3 +1,79 @@ +function hitlReviewerNormalize(v) { + const x = String(v || '').trim().toLowerCase(); + if (x === 'audit_agent' || x === 'agent' || x === 'ai') return 'audit_agent'; + return 'human'; +} + +function hitlParsePayloadObject(raw) { + if (!raw) return {}; + if (typeof raw === 'object') return raw; + try { + const o = JSON.parse(String(raw)); + return o && typeof o === 'object' ? o : {}; + } catch (e) { + return {}; + } +} + +function hitlRenderContextBlocks(payloadObj) { + if (!payloadObj || typeof payloadObj !== 'object') return ''; + const blocks = []; + function addBlock(label, value) { + const s = String(value || '').trim(); + if (!s) return; + blocks.push( + '
' + + '
' + escapeHtml(label) + '
' + + '
' + escapeHtml(s) + '
' + + '
' + ); + } + addBlock(hitlT('fieldUserMessage', 'User message'), payloadObj.userMessage); + addBlock(hitlT('fieldThinking', 'Thinking'), payloadObj.thinking); + addBlock(hitlT('fieldReasoning', 'Reasoning'), payloadObj.reasoningChain); + addBlock(hitlT('fieldPlanning', 'Planning'), payloadObj.planning); + return blocks.join(''); +} + +function hitlRenderExecutionResultBlock(payloadObj) { + if (!payloadObj || typeof payloadObj !== 'object') return ''; + const exec = payloadObj.executionResult; + if (!exec || typeof exec !== 'object') return ''; + const ok = exec.success === true; + const label = hitlT('fieldExecutionResult', 'Execution result') + (ok ? ' (' + hitlT('executionSuccess', 'success') + ')' : ' (' + hitlT('executionFailed', 'failed') + ')'); + const text = String(exec.result || '').trim(); + if (!text) return ''; + return ( + '
' + + '
' + escapeHtml(label) + '
' + + '
' + escapeHtml(text) + '
' + + '
' + ); +} + +function hitlFillLogModalReadonlySections(payloadObj) { + const ctxEl = document.getElementById('hitl-log-context-readonly'); + const execEl = document.getElementById('hitl-log-execution-readonly'); + const ctxHtml = hitlRenderContextBlocks(payloadObj); + const execHtml = hitlRenderExecutionResultBlock(payloadObj); + if (ctxEl) { + ctxEl.innerHTML = ctxHtml; + ctxEl.hidden = !ctxHtml; + } + if (execEl) { + execEl.innerHTML = execHtml; + execEl.hidden = !execHtml; + } +} + +function hitlPayloadSummary(payloadObj) { + const parts = []; + if (payloadObj && payloadObj.userMessage) parts.push(hitlT('fieldUserMessage', 'User')); + if (payloadObj && payloadObj.thinking) parts.push(hitlT('fieldThinking', 'Thinking')); + if (payloadObj && payloadObj.executionResult) parts.push(hitlT('fieldExecutionResult', 'Result')); + return parts.length ? parts.join(' · ') : '—'; +} + function hitlModeNormalize(m) { let v = String(m || '').trim().toLowerCase().replace(/-/g, '_'); if (v === 'feedback' || v === 'followup') { @@ -20,6 +96,81 @@ function hitlT(key, fallback, params) { return fallback; } +const HITL_LOGS_PAGE_SIZE_KEY = 'cyberstrike_hitl_logs_page_size'; +const HITL_PENDING_PAGE_SIZE_KEY = 'cyberstrike_hitl_pending_page_size'; +const HITL_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; + +function hitlPaginationT(key, opts, fallback) { + if (typeof window.t === 'function') { + const keys = (key === 'paginationInfo' || key === 'perPageLabel') + ? ['mcpMonitor.' + key, 'mcp.' + key] + : ['mcp.' + key]; + for (let i = 0; i < keys.length; i++) { + const v = window.t(keys[i], opts || {}); + if (typeof v === 'string' && v && v !== keys[i]) return v; + } + } + return fallback != null ? fallback : key; +} + +function hitlLocale() { + if (typeof window.__locale === 'string' && window.__locale.length) { + return window.__locale.startsWith('zh') ? 'zh-CN' : 'en-US'; + } + return (typeof navigator !== 'undefined' && navigator.language) ? navigator.language : 'en-US'; +} + +function initHitlPageSizeFromStorage(storageKey, fallbackSize, assignFn) { + try { + const saved = parseInt(localStorage.getItem(storageKey), 10); + if (HITL_PAGE_SIZE_OPTIONS.indexOf(saved) >= 0) { + assignFn(saved); + return; + } + } catch (e) { /* ignore */ } + assignFn(fallbackSize); +} + +function renderHitlPagination(containerId, state, goPageFnName, pageSizeChangeFnName, pageSizeSelectId) { + const container = document.getElementById(containerId); + if (!container) return; + const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); }; + const total = state.total || 0; + const currentPage = state.page || 1; + const pageSize = state.pageSize || 20; + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1; + const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total); + const infoText = hitlPaginationT('paginationInfo', { start: start, end: end, total: total }, + '显示 ' + start + '-' + end + ' / 共 ' + total + ' 条记录'); + const perPageLabel = hitlPaginationT('perPageLabel', null, '每页显示'); + const firstPageLabel = hitlPaginationT('firstPage', null, '首页'); + const prevPageLabel = hitlPaginationT('prevPage', null, '上一页'); + const pageInfoText = hitlPaginationT('pageInfo', { page: currentPage, total: totalPages }, + '第 ' + currentPage + ' / ' + totalPages + ' 页'); + const nextPageLabel = hitlPaginationT('nextPage', null, '下一页'); + const lastPageLabel = hitlPaginationT('lastPage', null, '末页'); + const disabledFirst = currentPage === 1 || total === 0; + const disabledLast = currentPage >= totalPages || total === 0; + let html = '
'; + html += '
'; + html += '' + esc(infoText) + ''; + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '' + esc(pageInfoText) + ''; + html += ''; + html += ''; + html += '
'; + container.innerHTML = html; +} + function hitlEffectiveEnabled(cfg) { if (!cfg) return false; if (cfg.enabled === true) return true; @@ -104,12 +255,189 @@ async function mergeHitlGlobalToolWhitelist(sensitiveTools) { return []; } +function hitlPageToolsSplit(s) { + if (typeof window.hitlToolsSplitToArray === 'function') { + return window.hitlToolsSplitToArray(s); + } + return String(s || '').split(/[,\n\r]+/).map(function (x) { return x.trim(); }).filter(Boolean); +} + +function hitlPageToolsMergeDisplay(globalArr, sessionToolsArr) { + if (typeof window.hitlMergeToolsForDisplay === 'function') { + return window.hitlMergeToolsForDisplay(globalArr, sessionToolsArr); + } + const out = []; + const seen = Object.create(null); + function addOne(t) { + const n = String(t || '').trim(); + if (!n) return; + const k = n.toLowerCase(); + if (seen[k]) return; + seen[k] = true; + out.push(n); + } + if (Array.isArray(globalArr)) globalArr.forEach(addOne); + if (Array.isArray(sessionToolsArr)) sessionToolsArr.forEach(addOne); + return out.join(', '); +} + +function showHitlPageWhitelistFeedback(text, isError) { + const el = document.getElementById('hitl-page-whitelist-feedback'); + if (!el) return; + const msg = String(text || '').trim(); + if (!msg) { + el.hidden = true; + el.textContent = ''; + el.className = 'hitl-apply-feedback'; + return; + } + el.hidden = false; + el.textContent = msg; + el.className = 'hitl-apply-feedback' + (isError ? ' hitl-apply-feedback--error' : ''); +} + +function syncHitlSidebarWhitelistDisplay(toolsStr) { + const sidebarEl = document.getElementById('hitl-sensitive-tools'); + if (sidebarEl) sidebarEl.value = toolsStr; +} + +async function fetchHitlGlobalToolWhitelist() { + const resp = await hitlApiFetch('/api/hitl/tool-whitelist', { credentials: 'same-origin' }); + if (!resp.ok) { + throw new Error(await readHitlApiError(resp)); + } + const data = await resp.json(); + const list = Array.isArray(data.toolWhitelist) ? data.toolWhitelist : ( + Array.isArray(data.hitlGlobalToolWhitelist) ? data.hitlGlobalToolWhitelist : [] + ); + if (typeof window !== 'undefined') { + window.csaiHitlGlobalToolWhitelist = list; + } + return list; +} + +async function resolveHitlGlobalToolWhitelist() { + try { + return await fetchHitlGlobalToolWhitelist(); + } catch (e) { + if (typeof window !== 'undefined' && Array.isArray(window.csaiHitlGlobalToolWhitelist)) { + return window.csaiHitlGlobalToolWhitelist.slice(); + } + try { + const resp = await hitlApiFetch('/api/config', { credentials: 'same-origin' }); + if (resp.ok) { + const cfg = await resp.json(); + const tw = cfg.hitl && cfg.hitl.tool_whitelist; + if (Array.isArray(tw)) { + if (typeof window !== 'undefined') { + window.csaiHitlGlobalToolWhitelist = tw.slice(); + } + return tw.slice(); + } + } + } catch (e2) { + console.warn('resolveHitlGlobalToolWhitelist fallback', e2); + } + throw e; + } +} + +function hitlPageWhitelistDisplayValue(globalArr, sessionArr) { + return hitlPageToolsMergeDisplay(globalArr, sessionArr); +} + +async function refreshHitlPageWhitelist() { + const ta = document.getElementById('hitl-page-sensitive-tools'); + if (!ta) return; + const cached = typeof window !== 'undefined' && Array.isArray(window.csaiHitlGlobalToolWhitelist) + ? window.csaiHitlGlobalToolWhitelist + : []; + if (cached.length > 0) { + ta.value = hitlPageWhitelistDisplayValue(cached, []); + } + try { + const globalArr = await resolveHitlGlobalToolWhitelist(); + const cid = getCurrentConversationIdForHitl(); + let sessionArr = []; + if (cid) { + const cfg = typeof window.getHitlConfigForConversation === 'function' + ? window.getHitlConfigForConversation(cid) + : null; + sessionArr = hitlSensitiveToolsToArray(cfg || {}); + } + ta.value = hitlPageWhitelistDisplayValue(globalArr, sessionArr); + syncHitlSidebarWhitelistDisplay(ta.value); + } catch (e) { + console.warn('refreshHitlPageWhitelist', e); + if (!ta.value.trim() && cached.length > 0) { + ta.value = hitlPageWhitelistDisplayValue(cached, []); + } + } +} + +async function putHitlGlobalToolWhitelist(toolWhitelist) { + const list = Array.isArray(toolWhitelist) ? toolWhitelist : []; + const resp = await hitlApiFetch('/api/hitl/tool-whitelist', { + method: 'PUT', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ toolWhitelist: list }) + }); + if (!resp.ok) { + throw new Error(await readHitlApiError(resp)); + } + const data = await resp.json(); + const out = Array.isArray(data.toolWhitelist) ? data.toolWhitelist : ( + Array.isArray(data.hitlGlobalToolWhitelist) ? data.hitlGlobalToolWhitelist : list + ); + if (typeof window !== 'undefined') { + window.csaiHitlGlobalToolWhitelist = out; + } + return out; +} + +async function saveHitlPageWhitelist() { + const ta = document.getElementById('hitl-page-sensitive-tools'); + const btn = document.getElementById('hitl-page-whitelist-save-btn'); + if (!ta) return; + showHitlPageWhitelistFeedback('', false); + if (btn) btn.disabled = true; + try { + const desired = hitlPageToolsSplit(ta.value); + const globalArr = await putHitlGlobalToolWhitelist(desired); + const displayStr = hitlPageToolsMergeDisplay(globalArr, []); + ta.value = displayStr; + syncHitlSidebarWhitelistDisplay(displayStr); + + const cid = getCurrentConversationIdForHitl(); + if (cid) { + const cfg = typeof window.getHitlConfigForConversation === 'function' + ? window.getHitlConfigForConversation(cid) + : { mode: 'off', reviewer: 'human', sensitiveTools: '', timeoutSeconds: 0 }; + const nextCfg = Object.assign({}, cfg, { sensitiveTools: '' }); + if (typeof window.saveHitlConfigForConversation === 'function') { + window.saveHitlConfigForConversation(cid, nextCfg); + } + if (typeof saveHitlConversationConfig === 'function') { + await saveHitlConversationConfig(cid, nextCfg); + } + } + + showHitlPageWhitelistFeedback(hitlT('whitelistSaved', 'Whitelist saved.'), false); + } catch (e) { + showHitlPageWhitelistFeedback(hitlT('whitelistSaveFailed', 'Failed to save whitelist') + ': ' + (e.message || e), true); + } finally { + if (btn) btn.disabled = false; + } +} + async function saveHitlConversationConfig(conversationId, config) { if (!conversationId || !config) return false; const mode = hitlModeNormalize(config.mode || 'off'); const enabled = typeof config.enabled === 'boolean' ? config.enabled : (mode !== 'off'); const sensitiveTools = hitlSensitiveToolsToArray(config); const timeoutSeconds = normalizeHitlTimeoutSeconds(config.timeoutSeconds, 0); + const reviewer = hitlReviewerNormalize(config.reviewer || 'human'); const resp = await hitlApiFetch('/api/hitl/config', { method: 'PUT', credentials: 'same-origin', @@ -118,6 +446,7 @@ async function saveHitlConversationConfig(conversationId, config) { conversationId: conversationId, enabled: enabled, mode: mode, + reviewer: reviewer, sensitiveTools: sensitiveTools, timeoutSeconds: timeoutSeconds }) @@ -192,6 +521,7 @@ async function syncHitlConfigFromServer(conversationId) { const sessionOnlyStr = strip(globalWL, rawArr.join(', ')); const normalizedCfg = Object.assign({}, merged, { mode: uiMode, + reviewer: hitlReviewerNormalize(merged.reviewer || cfg.reviewer || 'human'), sensitiveTools: sessionOnlyStr }); if (typeof window.saveHitlConfigForConversation === 'function') { @@ -288,24 +618,18 @@ async function followAgentRunAfterHitlDecision(conversationId) { } } -async function refreshHitlPending() { +function renderHitlPendingList(items) { const container = document.getElementById('hitl-pending-list'); if (!container) return; - container.innerHTML = '
' + escapeHtml(hitlT('loading', 'Loading...')) + '
'; - try { - const resp = await hitlApiFetch('/api/hitl/pending', { credentials: 'same-origin' }); - if (!resp.ok) { - throw new Error('request failed'); - } - const data = await resp.json(); - const items = Array.isArray(data.items) ? data.items : []; - if (!items.length) { - container.innerHTML = '
' + escapeHtml(hitlT('emptyState', 'No pending approvals')) + '
'; - return; - } - container.innerHTML = items.map(function (item) { + const list = Array.isArray(items) ? items : []; + if (!list.length) { + container.innerHTML = '
' + escapeHtml(hitlT('emptyState', 'No pending approvals')) + '
'; + return; + } + container.innerHTML = list.map(function (item) { + const payloadObj = hitlParsePayloadObject(item.payload || ''); const payload = String(item.payload || ''); - const preview = payload.length > 280 ? (payload.slice(0, 280) + '...') : payload; + const contextHtml = hitlRenderContextBlocks(payloadObj); const mode = String(item.mode || '').trim().toLowerCase(); const allowEdit = mode === 'review_edit'; var escId = escapeHtml(String(item.id || '')); @@ -321,7 +645,9 @@ async function refreshHitlPending() { '' + '' + '
' + escapeHtml(hitlT('conversationLabel', 'Conversation:')) + ' ' + escapeHtml(item.conversationId || '-') + '
' + - '
' + escapeHtml(preview) + '
' + + contextHtml + + hitlRenderExecutionResultBlock(payloadObj) + + '
' + escapeHtml(payload) + '
' + (allowEdit ? ('
' + escapeHtml(hitlT('reviewEditHelp', 'Review & edit mode: provide a JSON object to override tool arguments. Example: {"command":"ls -la"}')) + '
' + '') @@ -335,11 +661,53 @@ async function refreshHitlPending() { '' ); }).join(''); +} + +async function refreshHitlPending() { + const container = document.getElementById('hitl-pending-list'); + if (!container) return; + container.innerHTML = '
' + escapeHtml(hitlT('loading', 'Loading...')) + '
'; + try { + const q = document.getElementById('hitl-pending-search'); + const params = new URLSearchParams({ + page: String(hitlPendingPage), + pageSize: String(hitlPendingPageSize) + }); + if (q && q.value.trim()) params.set('q', q.value.trim()); + const resp = await hitlApiFetch('/api/hitl/pending?' + params.toString(), { credentials: 'same-origin' }); + if (!resp.ok) { + throw new Error('request failed'); + } + const data = await resp.json(); + const items = Array.isArray(data.items) ? data.items : []; + hitlPendingTotal = typeof data.total === 'number' ? data.total : items.length; + const maxPage = Math.max(1, Math.ceil(hitlPendingTotal / hitlPendingPageSize)); + if (hitlPendingPage > maxPage) { + hitlPendingPage = maxPage; + await refreshHitlPending(); + return; + } + const badge = document.getElementById('hitl-pending-count'); + if (badge) { + badge.textContent = String(hitlPendingTotal); + badge.hidden = hitlPendingTotal <= 0; + } + hitlPendingCache = items; + hitlPendingLoaded = true; + renderHitlPendingList(items); + renderHitlPendingPagination(); } catch (e) { + hitlPendingLoaded = false; container.innerHTML = '
' + escapeHtml(hitlT('loadFailed', 'Failed to load')) + '
'; + renderHitlPendingPagination(); } } +function filterHitlPending() { + hitlPendingPage = 1; + refreshHitlPending(); +} + async function submitHitlDecision(interruptId, decision, conversationIdOpt) { const commentBox = document.getElementById('hitl-comment-' + interruptId); const comment = (commentBox && commentBox.value) ? commentBox.value.trim() : ''; @@ -412,7 +780,475 @@ async function dismissHitlItem(interruptId, silent) { refreshHitlPending(); } +let hitlActiveTab = 'pending'; +let hitlLogsPage = 1; +let hitlLogsPageSize = 20; +let hitlLogsTotal = 0; +let hitlLogsCache = []; +let hitlLogsLoaded = false; +let hitlPendingPage = 1; +let hitlPendingPageSize = 20; +let hitlPendingTotal = 0; +let hitlPendingCache = []; +let hitlPendingLoaded = false; + +function switchHitlPageTab(tab) { + const tabs = ['pending', 'logs', 'strategy', 'whitelist']; + hitlActiveTab = tabs.indexOf(tab) >= 0 ? tab : 'pending'; + const pendingTab = document.getElementById('hitl-tab-pending'); + const logsTab = document.getElementById('hitl-tab-logs'); + const strategyTab = document.getElementById('hitl-tab-strategy'); + const whitelistTab = document.getElementById('hitl-tab-whitelist'); + const pendingPanel = document.getElementById('hitl-panel-pending'); + const logsPanel = document.getElementById('hitl-panel-logs'); + const strategyPanel = document.getElementById('hitl-panel-strategy'); + const whitelistPanel = document.getElementById('hitl-panel-whitelist'); + if (pendingTab) { + pendingTab.classList.toggle('hitl-page-tab--active', hitlActiveTab === 'pending'); + pendingTab.setAttribute('aria-selected', hitlActiveTab === 'pending' ? 'true' : 'false'); + } + if (logsTab) { + logsTab.classList.toggle('hitl-page-tab--active', hitlActiveTab === 'logs'); + logsTab.setAttribute('aria-selected', hitlActiveTab === 'logs' ? 'true' : 'false'); + } + if (strategyTab) { + strategyTab.classList.toggle('hitl-page-tab--active', hitlActiveTab === 'strategy'); + strategyTab.setAttribute('aria-selected', hitlActiveTab === 'strategy' ? 'true' : 'false'); + } + if (whitelistTab) { + whitelistTab.classList.toggle('hitl-page-tab--active', hitlActiveTab === 'whitelist'); + whitelistTab.setAttribute('aria-selected', hitlActiveTab === 'whitelist' ? 'true' : 'false'); + } + if (pendingPanel) pendingPanel.hidden = hitlActiveTab !== 'pending'; + if (logsPanel) logsPanel.hidden = hitlActiveTab !== 'logs'; + if (strategyPanel) strategyPanel.hidden = hitlActiveTab !== 'strategy'; + if (whitelistPanel) whitelistPanel.hidden = hitlActiveTab !== 'whitelist'; + refreshHitlActivePanel(); +} + +function refreshHitlPageReviewerBar() { + const cid = getCurrentConversationIdForHitl(); + const cfg = typeof window.getHitlConfigForConversation === 'function' + ? window.getHitlConfigForConversation(cid) + : null; + if (cfg && typeof window.setHitlReviewerUI === 'function') { + window.setHitlReviewerUI(cfg.reviewer); + } + if (typeof window.bindHitlReviewerToggleListeners === 'function') { + window.bindHitlReviewerToggleListeners(); + } +} + +let hitlDefaultAuditPrompt = ''; +let hitlDefaultAuditPromptReviewEdit = ''; +let hitlStrategyMode = 'approval'; + +function switchHitlStrategyMode(mode) { + hitlStrategyMode = mode === 'review_edit' ? 'review_edit' : 'approval'; + const approvalTab = document.getElementById('hitl-strategy-tab-approval'); + const reviewTab = document.getElementById('hitl-strategy-tab-review-edit'); + const approvalTa = document.getElementById('hitl-audit-agent-prompt'); + const reviewTa = document.getElementById('hitl-audit-agent-prompt-review-edit'); + const hintApproval = document.getElementById('hitl-strategy-hint-approval'); + const hintReview = document.getElementById('hitl-strategy-hint-review-edit'); + if (approvalTab) { + approvalTab.classList.toggle('hitl-strategy-subtab--active', hitlStrategyMode === 'approval'); + approvalTab.setAttribute('aria-selected', hitlStrategyMode === 'approval' ? 'true' : 'false'); + } + if (reviewTab) { + reviewTab.classList.toggle('hitl-strategy-subtab--active', hitlStrategyMode === 'review_edit'); + reviewTab.setAttribute('aria-selected', hitlStrategyMode === 'review_edit' ? 'true' : 'false'); + } + if (approvalTa) approvalTa.hidden = hitlStrategyMode !== 'approval'; + if (reviewTa) reviewTa.hidden = hitlStrategyMode !== 'review_edit'; + if (hintApproval) hintApproval.hidden = hitlStrategyMode !== 'approval'; + if (hintReview) hintReview.hidden = hitlStrategyMode !== 'review_edit'; +} + +function showHitlStrategyFeedback(text, isError) { + const el = document.getElementById('hitl-strategy-feedback'); + if (!el) return; + const msg = String(text || '').trim(); + if (!msg) { + el.hidden = true; + el.textContent = ''; + el.className = 'hitl-apply-feedback'; + return; + } + el.hidden = false; + el.textContent = msg; + el.className = 'hitl-apply-feedback' + (isError ? ' hitl-apply-feedback--error' : ''); +} + +async function refreshHitlAuditStrategy() { + const approvalTa = document.getElementById('hitl-audit-agent-prompt'); + const reviewTa = document.getElementById('hitl-audit-agent-prompt-review-edit'); + if (!approvalTa) return; + try { + const resp = await hitlApiFetch('/api/hitl/audit-strategy', { credentials: 'same-origin' }); + if (!resp.ok) return; + const data = await resp.json(); + hitlDefaultAuditPrompt = typeof data.defaultAuditAgentPrompt === 'string' ? data.defaultAuditAgentPrompt : ''; + hitlDefaultAuditPromptReviewEdit = typeof data.defaultAuditAgentPromptReviewEdit === 'string' ? data.defaultAuditAgentPromptReviewEdit : ''; + approvalTa.value = typeof data.auditAgentPrompt === 'string' ? data.auditAgentPrompt : hitlDefaultAuditPrompt; + if (reviewTa) { + reviewTa.value = typeof data.auditAgentPromptReviewEdit === 'string' ? data.auditAgentPromptReviewEdit : hitlDefaultAuditPromptReviewEdit; + } + switchHitlStrategyMode(hitlStrategyMode); + } catch (e) { + console.warn('refreshHitlAuditStrategy', e); + } +} + +async function saveHitlAuditStrategy() { + const approvalTa = document.getElementById('hitl-audit-agent-prompt'); + const reviewTa = document.getElementById('hitl-audit-agent-prompt-review-edit'); + const btn = document.getElementById('hitl-strategy-save-btn'); + if (!approvalTa) return; + showHitlStrategyFeedback('', false); + if (btn) btn.disabled = true; + try { + const resp = await hitlApiFetch('/api/hitl/audit-strategy', { + method: 'PUT', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + auditAgentPrompt: String(approvalTa.value || ''), + auditAgentPromptReviewEdit: reviewTa ? String(reviewTa.value || '') : '' + }) + }); + if (!resp.ok) throw new Error(await readHitlApiError(resp)); + const data = await resp.json(); + if (typeof data.auditAgentPrompt === 'string') approvalTa.value = data.auditAgentPrompt; + if (reviewTa && typeof data.auditAgentPromptReviewEdit === 'string') reviewTa.value = data.auditAgentPromptReviewEdit; + showHitlStrategyFeedback(hitlT('strategySaved', 'Audit strategy saved.'), false); + } catch (e) { + showHitlStrategyFeedback(hitlT('strategySaveFailed', 'Failed to save') + ': ' + (e.message || e), true); + } finally { + if (btn) btn.disabled = false; + } +} + +function resetHitlAuditStrategy() { + const approvalTa = document.getElementById('hitl-audit-agent-prompt'); + const reviewTa = document.getElementById('hitl-audit-agent-prompt-review-edit'); + if (hitlStrategyMode === 'review_edit' && reviewTa) { + reviewTa.value = hitlDefaultAuditPromptReviewEdit || reviewTa.value; + } else if (approvalTa) { + approvalTa.value = hitlDefaultAuditPrompt || approvalTa.value; + } + showHitlStrategyFeedback('', false); +} + +function refreshHitlActivePanel() { + refreshHitlPageReviewerBar(); + if (hitlActiveTab === 'logs') { + refreshHitlLogs(); + } else if (hitlActiveTab === 'strategy') { + refreshHitlAuditStrategy(); + } else if (hitlActiveTab === 'whitelist') { + refreshHitlPageWhitelist(); + } else { + refreshHitlPending(); + } +} + +function hitlDecidedByLabel(v) { + const key = 'reviewer' + String(v || 'human').replace(/_([a-z])/g, function (_, c) { return c.toUpperCase(); }).replace(/^./, function (c) { return c.toUpperCase(); }); + const map = { + human: hitlT('reviewerHuman', 'Human'), + audit_agent: hitlT('reviewerAgent', 'Audit Agent'), + system: hitlT('reviewerSystem', 'System'), + manual: hitlT('reviewerManual', 'Manual') + }; + return map[v] || v || '-'; +} + +function hitlFormatTime(v) { + if (!v) return '-'; + try { + const d = new Date(v); + if (Number.isNaN(d.getTime())) return String(v); + return d.toLocaleString(hitlLocale(), { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + } catch (e) { + return String(v); + } +} + +function renderHitlLogsTable(items) { + const wrap = document.getElementById('hitl-logs-table-wrap'); + if (!wrap) return; + const list = Array.isArray(items) ? items : []; + if (!list.length) { + wrap.innerHTML = + '
' + + '

' + escapeHtml(hitlT('logsEmpty', 'No audit logs')) + '

' + + '

' + escapeHtml(hitlT('logsEmptyHint', 'Records appear here after HITL decisions.')) + '

' + + '
'; + renderHitlLogsPagination(); + return; + } + const rows = list.map(function (item) { + const id = escapeHtml(String(item.id || '')); + const qId = JSON.stringify(String(item.id || '')).replace(/"/g, '"'); + const payloadObj = hitlParsePayloadObject(item.payload || ''); + const decision = String(item.decision || '-'); + const decisionCls = decision === 'approve' ? 'hitl-decision--approve' : (decision === 'reject' ? 'hitl-decision--reject' : ''); + const summary = hitlPayloadSummary(payloadObj); + return ( + '' + + '' + id + '' + + '' + escapeHtml(String(item.toolName || '-')) + '' + + '' + escapeHtml(String(item.conversationId || '-')) + '' + + '' + escapeHtml(hitlDecisionLabel(decision)) + '' + + '' + escapeHtml(hitlDecidedByLabel(item.decidedBy)) + '' + + '' + escapeHtml(summary) + '' + + '' + escapeHtml(hitlFormatTime(item.decidedAt || item.createdAt)) + '' + + '' + + '' + + '' + + '' + ); + }).join(''); + wrap.innerHTML = + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + rows + '
' + escapeHtml(hitlT('colId', 'ID')) + '' + escapeHtml(hitlT('colTool', 'Tool')) + '' + escapeHtml(hitlT('colConversation', 'Conversation')) + '' + escapeHtml(hitlT('colDecision', 'Decision')) + '' + escapeHtml(hitlT('colDecidedBy', 'Reviewer')) + '' + escapeHtml(hitlT('colContext', 'Context')) + '' + escapeHtml(hitlT('colTime', 'Time')) + '' + escapeHtml(hitlT('colActions', 'Actions')) + '
'; + renderHitlLogsPagination(); +} + +async function refreshHitlLogs() { + const wrap = document.getElementById('hitl-logs-table-wrap'); + if (!wrap) return; + wrap.innerHTML = '
' + escapeHtml(hitlT('loading', 'Loading...')) + '
'; + try { + const params = new URLSearchParams({ + page: String(hitlLogsPage), + pageSize: String(hitlLogsPageSize) + }); + const qEl = document.getElementById('hitl-logs-search'); + const decEl = document.getElementById('hitl-logs-decision-filter'); + const byEl = document.getElementById('hitl-logs-decidedby-filter'); + if (qEl && qEl.value.trim()) params.set('q', qEl.value.trim()); + if (decEl && decEl.value && decEl.value !== 'all') params.set('decision', decEl.value); + if (byEl && byEl.value && byEl.value !== 'all') params.set('decidedBy', byEl.value); + const resp = await hitlApiFetch('/api/hitl/logs?' + params.toString(), { credentials: 'same-origin' }); + if (!resp.ok) throw new Error('request failed'); + const data = await resp.json(); + const items = Array.isArray(data.items) ? data.items : []; + hitlLogsTotal = typeof data.total === 'number' ? data.total : items.length; + const maxPage = Math.max(1, Math.ceil(hitlLogsTotal / hitlLogsPageSize)); + if (hitlLogsPage > maxPage) { + hitlLogsPage = maxPage; + await refreshHitlLogs(); + return; + } + hitlLogsCache = items; + hitlLogsLoaded = true; + renderHitlLogsTable(items); + } catch (e) { + hitlLogsLoaded = false; + wrap.innerHTML = '
' + escapeHtml(hitlT('loadFailed', 'Failed to load')) + '
'; + renderHitlLogsPagination(); + } +} + +function filterHitlLogs() { + hitlLogsPage = 1; + refreshHitlLogs(); +} + +function refreshHitlLogsI18n() { + if (!document.getElementById('hitl-logs-table-wrap') || !hitlLogsLoaded) return; + renderHitlLogsTable(hitlLogsCache); +} + +function refreshHitlPendingI18n() { + if (!document.getElementById('hitl-pending-list') || !hitlPendingLoaded) return; + renderHitlPendingList(hitlPendingCache); +} + +function refreshHitlI18n() { + refreshHitlLogsI18n(); + refreshHitlPendingI18n(); + renderHitlLogsPagination(); + renderHitlPendingPagination(); +} + +function renderHitlLogsPagination() { + renderHitlPagination('hitl-logs-pagination', { + total: hitlLogsTotal, + page: hitlLogsPage, + pageSize: hitlLogsPageSize + }, 'hitlLogsGoPage', 'onHitlLogsPageSizeChange', 'hitl-logs-page-size'); +} + +function renderHitlPendingPagination() { + renderHitlPagination('hitl-pending-pagination', { + total: hitlPendingTotal, + page: hitlPendingPage, + pageSize: hitlPendingPageSize + }, 'hitlPendingGoPage', 'onHitlPendingPageSizeChange', 'hitl-pending-page-size'); +} + +function onHitlLogsPageSizeChange() { + const sel = document.getElementById('hitl-logs-page-size'); + if (!sel) return; + const n = parseInt(sel.value, 10); + if (HITL_PAGE_SIZE_OPTIONS.indexOf(n) < 0) return; + hitlLogsPageSize = n; + try { + localStorage.setItem(HITL_LOGS_PAGE_SIZE_KEY, String(n)); + } catch (e) { /* ignore */ } + hitlLogsPage = 1; + refreshHitlLogs(); +} + +function onHitlPendingPageSizeChange() { + const sel = document.getElementById('hitl-pending-page-size'); + if (!sel) return; + const n = parseInt(sel.value, 10); + if (HITL_PAGE_SIZE_OPTIONS.indexOf(n) < 0) return; + hitlPendingPageSize = n; + try { + localStorage.setItem(HITL_PENDING_PAGE_SIZE_KEY, String(n)); + } catch (e) { /* ignore */ } + hitlPendingPage = 1; + refreshHitlPending(); +} + +function hitlLogsGoPage(page) { + const totalPages = Math.max(1, Math.ceil((hitlLogsTotal || 0) / (hitlLogsPageSize || 20))); + if (page < 1 || page > totalPages) return; + hitlLogsPage = page; + refreshHitlLogs(); +} + +function hitlPendingGoPage(page) { + const totalPages = Math.max(1, Math.ceil((hitlPendingTotal || 0) / (hitlPendingPageSize || 20))); + if (page < 1 || page > totalPages) return; + hitlPendingPage = page; + refreshHitlPending(); +} + +function hitlDecisionLabel(decision) { + const d = String(decision || '').toLowerCase(); + if (d === 'approve') return hitlT('decisionApprove', 'Approve'); + if (d === 'reject') return hitlT('decisionReject', 'Reject'); + return decision || '—'; +} + +function hitlFormatPayloadForDisplay(raw) { + if (!raw) return ''; + if (typeof raw === 'object') { + try { + return JSON.stringify(raw, null, 2); + } catch (e) { + return String(raw); + } + } + const s = String(raw).trim(); + if (!s) return ''; + try { + return JSON.stringify(JSON.parse(s), null, 2); + } catch (e) { + return s; + } +} + +async function openHitlLogModal(idOpt) { + const modal = document.getElementById('hitl-log-modal'); + if (!modal || !idOpt) return; + const resp = await hitlApiFetch('/api/hitl/logs/' + encodeURIComponent(idOpt), { credentials: 'same-origin' }); + if (!resp.ok) { + alert(hitlT('loadFailed', 'Failed to load')); + return; + } + const item = await resp.json(); + const payloadObj = hitlParsePayloadObject(item.payload || ''); + const idEl = document.getElementById('hitl-log-detail-id'); + const toolEl = document.getElementById('hitl-log-detail-tool'); + const convEl = document.getElementById('hitl-log-detail-conversation'); + const decisionEl = document.getElementById('hitl-log-detail-decision'); + const decidedByEl = document.getElementById('hitl-log-detail-decided-by'); + const timeEl = document.getElementById('hitl-log-detail-time'); + const commentRow = document.getElementById('hitl-log-detail-comment-row'); + const commentEl = document.getElementById('hitl-log-detail-comment'); + const payloadWrap = document.getElementById('hitl-log-detail-payload-wrap'); + const payloadEl = document.getElementById('hitl-log-detail-payload'); + if (idEl) idEl.textContent = item.id || '—'; + if (toolEl) toolEl.textContent = item.toolName || '—'; + if (convEl) convEl.textContent = item.conversationId || '—'; + if (decisionEl) { + const decision = String(item.decision || ''); + const cls = decision === 'approve' ? 'hitl-decision--approve' : (decision === 'reject' ? 'hitl-decision--reject' : ''); + decisionEl.innerHTML = '' + escapeHtml(hitlDecisionLabel(decision)) + ''; + } + if (decidedByEl) decidedByEl.textContent = hitlDecidedByLabel(item.decidedBy); + if (timeEl) timeEl.textContent = hitlFormatTime(item.decidedAt || item.createdAt); + const comment = String(item.comment || '').trim(); + if (commentRow && commentEl) { + if (comment) { + commentEl.textContent = comment; + commentRow.hidden = false; + } else { + commentEl.textContent = ''; + commentRow.hidden = true; + } + } + hitlFillLogModalReadonlySections(payloadObj); + const payloadText = hitlFormatPayloadForDisplay(item.payload || ''); + if (payloadWrap && payloadEl) { + if (payloadText) { + payloadEl.textContent = payloadText; + payloadWrap.hidden = false; + } else { + payloadEl.textContent = ''; + payloadWrap.hidden = true; + } + } + modal.style.display = 'flex'; +} + +function closeHitlLogModal() { + const modal = document.getElementById('hitl-log-modal'); + if (modal) modal.style.display = 'none'; +} + +window.saveHitlPageWhitelist = saveHitlPageWhitelist; +window.refreshHitlPageWhitelist = refreshHitlPageWhitelist; window.refreshHitlPending = refreshHitlPending; +window.refreshHitlLogs = refreshHitlLogs; +window.refreshHitlActivePanel = refreshHitlActivePanel; +window.switchHitlPageTab = switchHitlPageTab; +window.switchHitlStrategyMode = switchHitlStrategyMode; +window.resetHitlAuditStrategy = resetHitlAuditStrategy; +window.saveHitlAuditStrategy = saveHitlAuditStrategy; +window.refreshHitlAuditStrategy = refreshHitlAuditStrategy; +window.openHitlLogModal = openHitlLogModal; +window.closeHitlLogModal = closeHitlLogModal; +window.hitlLogsGoPage = hitlLogsGoPage; +window.hitlPendingGoPage = hitlPendingGoPage; +window.filterHitlLogs = filterHitlLogs; +window.filterHitlPending = filterHitlPending; +window.onHitlLogsPageSizeChange = onHitlLogsPageSizeChange; +window.onHitlPendingPageSizeChange = onHitlPendingPageSizeChange; window.submitHitlDecision = submitHitlDecision; window.submitHitlDecisionWithPayload = submitHitlDecisionWithPayload; window.dismissHitlItem = dismissHitlItem; @@ -420,7 +1256,7 @@ window.followAgentRunAfterHitlDecision = followAgentRunAfterHitlDecision; window.addEventListener('hitl-interrupt', function () { if (typeof window.currentPage === 'function' && window.currentPage() === 'hitl') { - refreshHitlPending(); + refreshHitlActivePanel(); } }); @@ -428,9 +1264,22 @@ window.addEventListener('pageshow', function () { setTimeout(reconcileHitlUiState, 0); }); document.addEventListener('DOMContentLoaded', function () { + initHitlPageSizeFromStorage(HITL_LOGS_PAGE_SIZE_KEY, 20, function (n) { hitlLogsPageSize = n; }); + initHitlPageSizeFromStorage(HITL_PENDING_PAGE_SIZE_KEY, 20, function (n) { hitlPendingPageSize = n; }); + if (typeof window.bindHitlReviewerToggleListeners === 'function') { + window.bindHitlReviewerToggleListeners(); + } setTimeout(reconcileHitlUiState, 0); }); +document.addEventListener('languagechange', function () { + try { + refreshHitlI18n(); + } catch (e) { + console.warn('languagechange hitl refresh failed', e); + } +}); + // 由 applyHitlSidebarConfig 调用,将侧栏配置同步到后端 window.syncHitlConfigToServerByCurrentConversation = syncHitlConfigToServerByCurrentConversation; window.saveHitlConversationConfig = saveHitlConversationConfig; diff --git a/web/static/js/router.js b/web/static/js/router.js index 235db0d9..7abef667 100644 --- a/web/static/js/router.js +++ b/web/static/js/router.js @@ -335,7 +335,9 @@ async function initPage(pageId) { } break; case 'hitl': - if (typeof refreshHitlPending === 'function') { + if (typeof refreshHitlActivePanel === 'function') { + refreshHitlActivePanel(); + } else if (typeof refreshHitlPending === 'function') { refreshHitlPending(); } break; diff --git a/web/templates/index.html b/web/templates/index.html index 0ddd96ab..716ea27c 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -942,6 +942,19 @@ +
+ +
+ + +
+ +

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

+
@@ -1160,13 +1173,165 @@
-
-

待处理审批

+
+
+ 当前审批方 +
+ + +
+
+

作用于当前选中会话;未选会话时保存到本机,新建会话时沿用。切换后立即生效。

+
+
+ + + + +
+ +
+
+ + +
+
+
+ + + + + + +
+
+ +