diff --git a/config.example.yaml b/config.example.yaml index 68a728bc..b9a02c38 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -345,6 +345,11 @@ knowledge: robots: wechat: # 微信 iLink(个人微信 ClawBot,扫码绑定) enabled: false + # 鉴权默认 user_binding;专用机器人可改 service_account,并必须限制真实发送者 + auth: + mode: user_binding # user_binding | service_account + # service_user_id: "admin 或专用服务账号的 RBAC user ID" + # allowed_external_users: ["t:tenant|u:sender"] bot_token: "" ilink_bot_id: "" ilink_user_id: "" @@ -353,6 +358,8 @@ robots: bot_agent: CyberStrikeAI/1.0 wecom: # 企业微信 enabled: false + auth: + mode: user_binding token: "" encoding_aes_key: "" corp_id: "" @@ -360,30 +367,42 @@ robots: agent_id: 0 dingtalk: # 钉钉 enabled: false + auth: + mode: user_binding client_id: "" client_secret: "" allow_conversation_id_fallback: false lark: # 飞书 enabled: false + auth: + mode: user_binding app_id: "" app_secret: "" verify_token: "" allow_chat_id_fallback: false telegram: # Telegram enabled: false + auth: + mode: user_binding bot_token: "" bot_username: "" allow_group_messages: false slack: # Slack enabled: false + auth: + mode: user_binding bot_token: "" app_token: "" discord: # Discord enabled: false + auth: + mode: user_binding bot_token: "" allow_guild_messages: false qq: # QQ 机器人 enabled: false + auth: + mode: user_binding app_id: "" client_secret: "" sandbox: true diff --git a/web/static/css/style.css b/web/static/css/style.css index 30022588..047dcb38 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -6089,6 +6089,21 @@ html[data-theme="dark"] .user-menu-dropdown { display: none !important; } +.robot-service-admin-warning { + display: block; + margin-top: 8px; + padding: 9px 11px; + border: 1px solid rgba(220, 53, 69, 0.25); + border-radius: 8px; + color: var(--error-color) !important; + background: rgba(220, 53, 69, 0.07); + line-height: 1.45; +} + +.robot-service-admin-warning[hidden] { + display: none !important; +} + .robot-create-modal-content { max-width: 680px; } @@ -6103,6 +6118,394 @@ html[data-theme="dark"] .user-menu-dropdown { overflow-y: auto; } +/* Robot account binding */ +.robot-account-binding-content { + width: min(760px, calc(100% - 32px)); + max-width: 760px; + max-height: 90vh; + border-radius: 18px; +} + +.robot-binding-header { + padding: 22px 26px; + background: var(--bg-primary); +} + +.robot-binding-heading { + display: flex; + align-items: center; + gap: 13px; + min-width: 0; +} + +.robot-binding-heading-icon { + width: 42px; + height: 42px; + flex: 0 0 42px; + display: grid; + place-items: center; + color: #fff; + border-radius: 12px; + background: var(--brand-gradient); + box-shadow: 0 8px 18px rgba(88, 80, 236, 0.24); +} + +.robot-binding-heading h2 { + font-size: 1.3rem; + background: none; + -webkit-text-fill-color: currentColor; +} + +.robot-binding-heading p { + margin: 3px 0 0; + color: var(--text-secondary); + font-size: 0.82rem; +} + +.robot-binding-body { + padding: 24px 26px 26px; + overflow-y: auto; + background: var(--bg-secondary); +} + +.robot-binding-guide { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 18px; + color: var(--text-secondary); + font-size: 0.82rem; +} + +.robot-binding-guide-number { + width: 22px; + height: 22px; + display: inline-grid; + place-items: center; + border-radius: 50%; + color: var(--accent-color); + background: rgba(0, 102, 255, 0.1); + font-size: 0.72rem; + font-weight: 700; +} + +.robot-binding-guide-arrow { + margin: 0 5px; + color: var(--text-muted); +} + +.robot-binding-code-card { + padding: 20px; + border: 1px solid var(--border-color); + border-radius: 14px; + background: var(--bg-primary); + box-shadow: var(--shadow-sm); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.robot-binding-code-card.is-active { + border-color: rgba(0, 102, 255, 0.32); + box-shadow: 0 10px 28px rgba(0, 102, 255, 0.08); +} + +.robot-binding-code-card.is-expired { + border-color: rgba(220, 53, 69, 0.28); +} + +.robot-binding-code-meta, +.robot-binding-timer-copy, +.robot-binding-section-heading, +.robot-binding-account-name { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.robot-binding-one-time-badge { + padding: 4px 9px; + border-radius: 999px; + color: #5b35c9; + background: rgba(124, 58, 237, 0.1); + font-size: 0.72rem; + font-weight: 700; +} + +.robot-binding-state { + color: var(--text-secondary); + font-size: 0.78rem; + font-weight: 600; +} + +.is-active .robot-binding-state { + color: var(--success-color); +} + +.is-expired .robot-binding-state { + color: var(--error-color); +} + +.robot-binding-code-row { + display: flex; + align-items: center; + gap: 18px; + margin: 18px 0 15px; +} + +.robot-binding-code-row code { + flex: 1; + min-width: 190px; + color: var(--text-primary); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: clamp(1.7rem, 4vw, 2.25rem); + font-weight: 700; + letter-spacing: 0.16em; + line-height: 1.2; + white-space: nowrap; +} + +.is-empty .robot-binding-code-row code { + color: var(--text-muted); +} + +.is-expired .robot-binding-code-row code { + color: var(--error-color); + font-size: 1.4rem; + letter-spacing: 0.08em; +} + +.robot-binding-code-actions { + display: flex; + gap: 9px; +} + +.robot-binding-copy-btn { + display: inline-flex; + align-items: center; + gap: 7px; +} + +.robot-binding-timer { + margin-top: 4px; +} + +.robot-binding-timer-copy { + margin-bottom: 8px; + color: var(--text-secondary); + font-size: 0.82rem; +} + +.robot-binding-timer-copy strong { + color: var(--accent-color); + font: 700 0.95rem ui-monospace, SFMono-Regular, Menlo, monospace; + letter-spacing: 0.04em; +} + +.is-expired .robot-binding-timer-copy strong, +.is-expired .robot-binding-timer-copy span { + color: var(--error-color); +} + +.robot-binding-progress { + height: 6px; + overflow: hidden; + border-radius: 999px; + background: var(--bg-tertiary); +} + +.robot-binding-progress span { + display: block; + width: 100%; + height: 100%; + border-radius: inherit; + background: var(--brand-gradient); + transition: width 0.25s linear; +} + +.robot-binding-security-note { + display: flex; + align-items: center; + gap: 7px; + margin: 15px 0 0; + color: var(--text-secondary); + font-size: 0.76rem; +} + +.robot-binding-security-note svg { + flex: 0 0 auto; + color: var(--success-color); +} + +.robot-binding-list-section { + margin-top: 22px; +} + +.robot-binding-section-heading { + margin-bottom: 12px; +} + +.robot-binding-section-heading h3 { + margin: 0; + font-size: 1rem; +} + +.robot-binding-section-heading p { + margin: 2px 0 0; + color: var(--text-secondary); + font-size: 0.78rem; +} + +.robot-account-binding-list { + display: grid; + gap: 9px; +} + +.robot-binding-account-card { + display: flex; + align-items: center; + gap: 12px; + padding: 13px 14px; + border: 1px solid var(--border-color); + border-radius: 11px; + background: var(--bg-primary); + transition: border-color 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease; +} + +.robot-binding-account-card:hover { + border-color: rgba(0, 102, 255, 0.3); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +.robot-binding-platform-icon, +.robot-binding-empty-icon { + width: 38px; + height: 38px; + flex: 0 0 38px; + display: grid; + place-items: center; + border-radius: 10px; + color: var(--accent-color); + background: rgba(0, 102, 255, 0.1); + font-size: 0.92rem; + font-weight: 700; +} + +.robot-binding-account-main { + flex: 1; + min-width: 0; +} + +.robot-binding-account-name { + justify-content: flex-start; +} + +.robot-binding-account-name span { + padding: 2px 7px; + border-radius: 999px; + color: var(--success-color); + background: rgba(40, 167, 69, 0.1); + font-size: 0.68rem; + font-weight: 700; +} + +.robot-binding-account-main small { + display: block; + margin-top: 3px; + overflow: hidden; + color: var(--text-secondary); + text-overflow: ellipsis; + white-space: nowrap; +} + +.robot-binding-empty-state, +.robot-binding-list-loading { + display: flex; + align-items: center; + gap: 12px; + padding: 18px; + border: 1px dashed var(--border-color); + border-radius: 11px; + color: var(--text-secondary); + background: var(--bg-primary); +} + +.robot-binding-empty-state strong { + color: var(--text-primary); +} + +.robot-binding-empty-state p { + margin: 2px 0 0; + font-size: 0.78rem; +} + +.robot-binding-footer { + justify-content: space-between; +} + +.robot-binding-footer > span { + color: var(--text-secondary); + font-size: 0.76rem; +} + +html[data-theme="dark"] .robot-binding-guide-number, +html[data-theme="dark"] .robot-binding-platform-icon, +html[data-theme="dark"] .robot-binding-empty-icon { + background: rgba(96, 165, 250, 0.14); +} + +html[data-theme="dark"] .robot-binding-one-time-badge { + color: #c4b5fd; + background: rgba(167, 139, 250, 0.14); +} + +@media (max-width: 680px) { + .robot-account-binding-content { + width: calc(100% - 20px); + margin: 10px auto; + max-height: calc(100vh - 20px); + } + + .robot-binding-header, + .robot-binding-body { + padding-left: 18px; + padding-right: 18px; + } + + .robot-binding-guide { + justify-content: flex-start; + } + + .robot-binding-guide-arrow { + display: none; + } + + .robot-binding-code-row { + align-items: stretch; + flex-direction: column; + } + + .robot-binding-code-actions .btn-primary, + .robot-binding-code-actions .btn-secondary { + flex: 1; + } + + .robot-binding-account-card { + align-items: flex-start; + flex-wrap: wrap; + } + + .robot-binding-unbind-btn { + margin-left: 50px; + } + + .robot-binding-footer > span { + display: none; + } +} + .robot-type-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index faf6421d..de1280fa 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2658,14 +2658,19 @@ }, "settingsRobotsExtra": { "botCommandsTitle": "Bot command instructions", - "botCommandsEntryDesc": "View the Chinese and English commands available in robot chats, including common conversation, role, and project actions.", + "botCommandsEntryDesc": "View robot commands for identity, authorization, conversations, roles, and projects.", "viewAllCommands": "View all commands", "botCommandsDesc": "You can send the following commands in chat (Chinese and English supported):", "botCmdCategoryGeneral": "General", + "botCmdCategoryIdentity": "Identity and authorization", "botCmdCategoryConversation": "Conversation", "botCmdCategoryRole": "Role", "botCmdCategoryProject": "Project", "botCmdHelp": "Show this help", + "botCmdIdentity": "Show the platform sender, authorization mode, and effective RBAC identity", + "botCmdBindUser": "Bind this platform identity to an RBAC user (user-binding mode only)", + "botCmdUnbindUser": "Remove this platform identity binding (user-binding mode only)", + "botAuthHint": "Service-account mode does not accept bind or unbind commands; only configured allowlisted senders are accepted.", "botCmdList": "List conversations", "botCmdSwitch": "Switch to conversation", "botCmdNew": "Start new conversation", @@ -2680,7 +2685,7 @@ "botCmdNewProject": "Create project and bind current conversation", "botCmdBindProject": "Bind current conversation to a project", "botCmdUnbindProject": "Unbind project from current conversation", - "botCommandsFooter": "Otherwise, send any text for AI penetration testing / security analysis." + "botCommandsFooter": "Otherwise, text is sent to AI under the bound user or service account's current RBAC permissions." }, "mcpDetailModal": { "title": "Tool call details", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index b972f54f..631245a1 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2646,14 +2646,19 @@ }, "settingsRobotsExtra": { "botCommandsTitle": "机器人命令说明", - "botCommandsEntryDesc": "查看机器人对话中可用的中英文命令,包括对话、角色、项目等常用操作。", + "botCommandsEntryDesc": "查看机器人对话中可用的中英文命令,包括身份鉴权、对话、角色和项目操作。", "viewAllCommands": "查看全部命令", "botCommandsDesc": "在对话中可发送以下命令(支持中英文):", "botCmdCategoryGeneral": "通用", + "botCmdCategoryIdentity": "身份与鉴权", "botCmdCategoryConversation": "对话", "botCmdCategoryRole": "角色", "botCmdCategoryProject": "项目", "botCmdHelp": "显示本帮助 | Show this help", + "botCmdIdentity": "显示平台发送者、鉴权模式及当前实际 RBAC 身份 | Show effective identity", + "botCmdBindUser": "绑定当前平台账号到 RBAC 用户(仅逐用户绑定模式) | Bind RBAC user", + "botCmdUnbindUser": "解除当前平台账号绑定(仅逐用户绑定模式) | Unbind RBAC user", + "botAuthHint": "专用服务账号模式不接受“绑定/解绑”命令,仅允许机器人配置中白名单内的发送者。", "botCmdList": "列出所有对话标题与 ID | List conversations", "botCmdSwitch": "指定对话继续 | Switch to conversation", "botCmdNew": "开启新对话 | Start new conversation", @@ -2668,7 +2673,7 @@ "botCmdNewProject": "创建项目并绑定当前对话 | Create & bind project", "botCmdBindProject": "将当前对话绑定到项目 | Bind conversation", "botCmdUnbindProject": "解除当前对话的项目绑定 | Unbind project", - "botCommandsFooter": "除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis." + "botCommandsFooter": "除以上命令外,直接输入内容将按绑定用户或服务账号的实时 RBAC 权限发送给 AI。Otherwise, text is sent to AI under the effective RBAC identity." }, "mcpDetailModal": { "title": "工具调用详情", diff --git a/web/static/js/auth.js b/web/static/js/auth.js index 6d85ccc3..05057b4c 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -8,6 +8,10 @@ let authScope = ''; let authPromise = null; let authPromiseResolvers = []; let isAppInitialized = false; +let robotBindingCountdownTimer = null; +let robotBindingExpiresAt = 0; +let robotBindingLifetimeMs = 5 * 60 * 1000; +let activeRobotBindingCode = ''; function isTokenValid() { return !!authToken && authTokenExpiry instanceof Date && authTokenExpiry.getTime() > Date.now(); @@ -698,6 +702,182 @@ document.addEventListener('languagechange', function () { renderUserMenuProfile(); }); +async function openRobotAccountBinding() { + setUserMenuOpen(false); + openAppModal('robot-account-binding-modal'); + if (robotBindingExpiresAt) updateRobotBindingCountdown(); + await loadRobotAccountBindings(); +} + +function closeRobotAccountBinding() { + closeAppModal('robot-account-binding-modal'); +} + +async function generateRobotBindingCode() { + const generateBtn = document.getElementById('robot-binding-generate-btn'); + if (generateBtn) { + generateBtn.disabled = true; + generateBtn.textContent = '正在生成…'; + } + let response; + let data; + try { + response = await apiFetch('/api/auth/robot-binding-code', { method: 'POST' }); + data = await response.json().catch(() => ({})); + } catch (error) { + if (typeof showNotification === 'function') showNotification('生成绑定码失败,请检查网络连接', 'error'); + resetRobotBindingGenerateButton(); + return; + } + if (!response.ok || !data.code) { + if (typeof showNotification === 'function') showNotification(data.error || '生成绑定码失败', 'error'); + resetRobotBindingGenerateButton(); + return; + } + const codeEl = document.getElementById('robot-binding-code'); + const copyBtn = document.getElementById('robot-binding-copy-btn'); + const card = document.getElementById('robot-binding-code-card'); + const timer = document.getElementById('robot-binding-timer'); + const state = document.getElementById('robot-binding-code-state'); + activeRobotBindingCode = data.code; + robotBindingLifetimeMs = Math.max(1000, Number(data.expires_in_seconds || 300) * 1000); + // Use the server-provided duration instead of comparing wall clocks, so a + // client machine with clock skew still gets an accurate countdown. + robotBindingExpiresAt = Date.now() + robotBindingLifetimeMs; + if (codeEl) codeEl.textContent = data.code; + if (copyBtn) copyBtn.disabled = false; + if (card) card.className = 'robot-binding-code-card is-active'; + if (timer) timer.hidden = false; + if (state) state.textContent = '等待绑定'; + if (generateBtn) { + generateBtn.disabled = false; + generateBtn.textContent = '重新生成'; + } + startRobotBindingCountdown(); +} + +function resetRobotBindingGenerateButton() { + const generateBtn = document.getElementById('robot-binding-generate-btn'); + if (generateBtn) { + generateBtn.disabled = false; + generateBtn.textContent = activeRobotBindingCode ? '重新生成' : '生成绑定码'; + } +} + +function startRobotBindingCountdown() { + if (robotBindingCountdownTimer) clearInterval(robotBindingCountdownTimer); + updateRobotBindingCountdown(); + robotBindingCountdownTimer = setInterval(updateRobotBindingCountdown, 250); +} + +function updateRobotBindingCountdown() { + if (!robotBindingExpiresAt) return; + const remaining = Math.max(0, robotBindingExpiresAt - Date.now()); + const seconds = Math.ceil(remaining / 1000); + const minutesPart = String(Math.floor(seconds / 60)).padStart(2, '0'); + const secondsPart = String(seconds % 60).padStart(2, '0'); + const countdown = document.getElementById('robot-binding-countdown'); + const progress = document.getElementById('robot-binding-progress-bar'); + const expiry = document.getElementById('robot-binding-expiry'); + if (countdown) countdown.textContent = `${minutesPart}:${secondsPart}`; + if (progress) progress.style.width = `${Math.max(0, Math.min(100, remaining / robotBindingLifetimeMs * 100))}%`; + if (expiry && activeRobotBindingCode) expiry.textContent = `请发送:绑定 ${activeRobotBindingCode}`; + if (remaining <= 0) expireRobotBindingCode(); +} + +function expireRobotBindingCode() { + if (robotBindingCountdownTimer) clearInterval(robotBindingCountdownTimer); + robotBindingCountdownTimer = null; + robotBindingExpiresAt = 0; + activeRobotBindingCode = ''; + const card = document.getElementById('robot-binding-code-card'); + const codeEl = document.getElementById('robot-binding-code'); + const state = document.getElementById('robot-binding-code-state'); + const expiry = document.getElementById('robot-binding-expiry'); + const countdown = document.getElementById('robot-binding-countdown'); + const progress = document.getElementById('robot-binding-progress-bar'); + const copyBtn = document.getElementById('robot-binding-copy-btn'); + if (card) card.className = 'robot-binding-code-card is-expired'; + if (codeEl) codeEl.textContent = '已失效'; + if (state) state.textContent = '已过期'; + if (expiry) expiry.textContent = '绑定码已过期,请重新生成'; + if (countdown) countdown.textContent = '00:00'; + if (progress) progress.style.width = '0%'; + if (copyBtn) copyBtn.disabled = true; + resetRobotBindingGenerateButton(); + const generateBtn = document.getElementById('robot-binding-generate-btn'); + if (generateBtn) generateBtn.textContent = '重新生成'; + loadRobotAccountBindings(); +} + +async function copyRobotBindingCode() { + if (!activeRobotBindingCode || !robotBindingExpiresAt || robotBindingExpiresAt <= Date.now()) return; + const command = `绑定 ${activeRobotBindingCode}`; + try { + await navigator.clipboard.writeText(command); + } catch (_) { + const textarea = document.createElement('textarea'); + textarea.value = command; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + textarea.remove(); + } + const copyBtn = document.getElementById('robot-binding-copy-btn'); + const label = copyBtn?.querySelector('span'); + if (label) label.textContent = '已复制'; + setTimeout(() => { if (label) label.textContent = '复制命令'; }, 1400); + if (typeof showNotification === 'function') showNotification('绑定命令已复制', 'success'); +} + +async function loadRobotAccountBindings() { + const list = document.getElementById('robot-account-binding-list'); + if (!list) return; + const response = await apiFetch('/api/auth/robot-bindings'); + const data = await response.json().catch(() => ({})); + if (!response.ok) { + list.textContent = data.error || '获取绑定失败'; + return; + } + const bindings = Array.isArray(data.bindings) ? data.bindings : []; + if (!bindings.length) { + list.innerHTML = `
+ +
暂无绑定账号

生成绑定码并在机器人中发送,即可完成首次绑定。

+
`; + return; + } + const platformLabels = { wechat: '微信', wecom: '企业微信', dingtalk: '钉钉', lark: '飞书', telegram: 'Telegram', slack: 'Slack', discord: 'Discord', qq: 'QQ' }; + list.innerHTML = bindings.map(binding => ` +
+ ${escapeHtml((platformLabels[binding.platform] || binding.platform || '?').slice(0, 1).toUpperCase())} +
+ + 账号标识 ${escapeHtml(binding.external_user_hint || '-')} · 更新于 ${escapeHtml(formatRobotBindingTime(binding.updated_at))} +
+ +
`).join(''); +} + +function formatRobotBindingTime(value) { + const date = new Date(value || ''); + if (Number.isNaN(date.getTime())) return '未知时间'; + return date.toLocaleString([], { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); +} + +async function deleteRobotAccountBinding(id) { + if (!id || !window.confirm('确定解除该机器人账号绑定吗?')) return; + const response = await apiFetch(`/api/auth/robot-bindings/${encodeURIComponent(id)}`, { method: 'DELETE' }); + if (!response.ok) { + const data = await response.json().catch(() => ({})); + if (typeof showNotification === 'function') showNotification(data.error || '解绑失败', 'error'); + return; + } + await loadRobotAccountBindings(); +} + // 退出登录 async function logout() { // 关闭下拉菜单 @@ -727,6 +907,11 @@ async function logout() { // 导出函数供HTML使用 window.toggleUserMenu = toggleUserMenu; +window.openRobotAccountBinding = openRobotAccountBinding; +window.closeRobotAccountBinding = closeRobotAccountBinding; +window.generateRobotBindingCode = generateRobotBindingCode; +window.copyRobotBindingCode = copyRobotBindingCode; +window.deleteRobotAccountBinding = deleteRobotAccountBinding; window.logout = logout; window.hasPermission = hasPermission; window.hasAnyPermission = hasAnyPermission; diff --git a/web/static/js/settings.js b/web/static/js/settings.js index 3e74d922..c81d60dc 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -7,6 +7,7 @@ let alwaysVisibleBuiltinToolNames = new Set(); // key: 唯一工具标识符(toolKey),value: { enabled: boolean, is_external: boolean, external_mcp: string } let toolStateMap = new Map(); let activeRobotEditor = ''; +let robotAuthDrafts = {}; function settingsT(key, fallback) { if (typeof window.t === 'function') { @@ -258,6 +259,9 @@ function refreshRobotManager() { } function openRobotEditor(type) { + if (activeRobotEditor && activeRobotEditor !== type) { + robotAuthDrafts[activeRobotEditor] = readRobotAuthPolicyEditor(); + } activeRobotEditor = type; const empty = document.getElementById('robot-editor-empty'); if (empty) empty.hidden = true; @@ -269,6 +273,53 @@ function openRobotEditor(type) { if (panel) { panel.scrollIntoView({ behavior: 'smooth', block: 'start' }); } + loadRobotAuthPolicyEditor(type); +} + +function loadRobotAuthPolicyEditor(type) { + const panel = document.getElementById('robot-auth-policy-panel'); + if (!panel) return; + panel.hidden = !type; + const auth = robotAuthDrafts[type] || (currentConfig?.robots?.[type]?.auth) || {}; + const mode = auth.mode === 'service_account' ? 'service_account' : 'user_binding'; + const modeInput = document.getElementById('robot-auth-mode'); + const serviceUserInput = document.getElementById('robot-service-user-id'); + const allowlistInput = document.getElementById('robot-allowed-external-users'); + if (modeInput) modeInput.value = mode; + if (serviceUserInput) serviceUserInput.value = auth.service_user_id || ''; + if (allowlistInput) allowlistInput.value = Array.isArray(auth.allowed_external_users) ? auth.allowed_external_users.join('\n') : ''; + onRobotAuthModeChange(); +} + +function onRobotAuthModeChange() { + const serviceFields = document.getElementById('robot-service-account-fields'); + if (serviceFields) serviceFields.hidden = document.getElementById('robot-auth-mode')?.value !== 'service_account'; + updateRobotServiceAccountWarning(); +} + +function updateRobotServiceAccountWarning() { + const warning = document.getElementById('robot-service-admin-warning'); + if (!warning) return; + const isServiceMode = document.getElementById('robot-auth-mode')?.value === 'service_account'; + const userID = document.getElementById('robot-service-user-id')?.value.trim().toLowerCase() || ''; + warning.hidden = !(isServiceMode && userID === 'admin'); +} + +function readRobotAuthPolicyEditor() { + const mode = document.getElementById('robot-auth-mode')?.value === 'service_account' ? 'service_account' : 'user_binding'; + if (mode === 'user_binding') return { mode }; + const allowed = (document.getElementById('robot-allowed-external-users')?.value || '') + .split(/[\n,,]/).map(value => value.trim()).filter(Boolean); + return { + mode, + service_user_id: document.getElementById('robot-service-user-id')?.value.trim() || '', + allowed_external_users: Array.from(new Set(allowed)) + }; +} + +function robotAuthPayload(type, prevRobots) { + if (type === activeRobotEditor) return readRobotAuthPolicyEditor(); + return robotAuthDrafts[type] || (prevRobots[type] && prevRobots[type].auth) || { mode: 'user_binding' }; } function openRobotCreateModal() { @@ -904,6 +955,7 @@ async function loadConfig(loadTools = true, options = {}) { syncC2NavFromConfig(currentConfig); // 填充机器人配置 + robotAuthDrafts = {}; const robots = currentConfig.robots || {}; const wechat = robots.wechat || {}; const wecom = robots.wecom || {}; @@ -1894,6 +1946,7 @@ async function applySettings() { ...(prevRobots.session && typeof prevRobots.session === 'object' ? { session: prevRobots.session } : {}), wechat: { enabled: document.getElementById('robot-wechat-enabled')?.checked === true, + auth: robotAuthPayload('wechat', prevRobots), base_url: document.getElementById('robot-wechat-base-url')?.value.trim() || 'https://ilinkai.weixin.qq.com', bot_type: document.getElementById('robot-wechat-bot-type')?.value.trim() || '3', bot_agent: document.getElementById('robot-wechat-bot-agent')?.value.trim() || 'CyberStrikeAI/1.0', @@ -1906,6 +1959,7 @@ async function applySettings() { }, wecom: { enabled: document.getElementById('robot-wecom-enabled')?.checked === true, + auth: robotAuthPayload('wecom', prevRobots), token: document.getElementById('robot-wecom-token')?.value.trim() || '', encoding_aes_key: document.getElementById('robot-wecom-encoding-aes-key')?.value.trim() || '', corp_id: document.getElementById('robot-wecom-corp-id')?.value.trim() || '', @@ -1914,12 +1968,14 @@ async function applySettings() { }, dingtalk: { enabled: document.getElementById('robot-dingtalk-enabled')?.checked === true, + auth: robotAuthPayload('dingtalk', prevRobots), client_id: document.getElementById('robot-dingtalk-client-id')?.value.trim() || '', client_secret: document.getElementById('robot-dingtalk-client-secret')?.value.trim() || '', allow_conversation_id_fallback: !!(prevRobots.dingtalk && prevRobots.dingtalk.allow_conversation_id_fallback) }, lark: { enabled: document.getElementById('robot-lark-enabled')?.checked === true, + auth: robotAuthPayload('lark', prevRobots), app_id: document.getElementById('robot-lark-app-id')?.value.trim() || '', app_secret: document.getElementById('robot-lark-app-secret')?.value.trim() || '', verify_token: document.getElementById('robot-lark-verify-token')?.value.trim() || '', @@ -1927,6 +1983,7 @@ async function applySettings() { }, telegram: { enabled: document.getElementById('robot-telegram-enabled')?.checked === true, + auth: robotAuthPayload('telegram', prevRobots), bot_token: document.getElementById('robot-telegram-bot-token')?.value.trim() || '', bot_username: document.getElementById('robot-telegram-bot-username')?.value.trim() || '', allow_group_messages: document.getElementById('robot-telegram-allow-group')?.checked === true, @@ -1936,16 +1993,19 @@ async function applySettings() { }, slack: { enabled: document.getElementById('robot-slack-enabled')?.checked === true, + auth: robotAuthPayload('slack', prevRobots), bot_token: document.getElementById('robot-slack-bot-token')?.value.trim() || '', app_token: document.getElementById('robot-slack-app-token')?.value.trim() || '' }, discord: { enabled: document.getElementById('robot-discord-enabled')?.checked === true, + auth: robotAuthPayload('discord', prevRobots), bot_token: document.getElementById('robot-discord-bot-token')?.value.trim() || '', allow_guild_messages: document.getElementById('robot-discord-allow-guild')?.checked === true }, qq: { enabled: document.getElementById('robot-qq-enabled')?.checked === true, + auth: robotAuthPayload('qq', prevRobots), app_id: document.getElementById('robot-qq-app-id')?.value.trim() || '', client_secret: document.getElementById('robot-qq-client-secret')?.value.trim() || '', sandbox: document.getElementById('robot-qq-sandbox')?.checked === true diff --git a/web/templates/index.html b/web/templates/index.html index ee293e24..43e76732 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -148,6 +148,13 @@ + + + @@ -3976,7 +4017,7 @@

机器人命令说明

-

查看机器人对话中可用的中英文命令,包括对话、角色、项目等常用操作。

+

查看机器人对话中可用的中英文命令,包括身份鉴权、对话、角色和项目操作。

@@ -5695,6 +5736,75 @@ + +