Add files via upload

This commit is contained in:
公明
2026-07-10 21:19:55 +08:00
committed by GitHub
parent e87011b081
commit 1abfd3d22a
7 changed files with 793 additions and 6 deletions
+19
View File
@@ -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
+403
View File
@@ -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));
+7 -2
View File
@@ -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",
+7 -2
View File
@@ -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": "工具调用详情",
+185
View File
@@ -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 = `<div class="robot-binding-empty-state">
<span class="robot-binding-empty-icon"><svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="2"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2"/></svg></span>
<div><strong>暂无绑定账号</strong><p>生成绑定码并在机器人中发送,即可完成首次绑定。</p></div>
</div>`;
return;
}
const platformLabels = { wechat: '微信', wecom: '企业微信', dingtalk: '钉钉', lark: '飞书', telegram: 'Telegram', slack: 'Slack', discord: 'Discord', qq: 'QQ' };
list.innerHTML = bindings.map(binding => `
<div class="robot-binding-account-card">
<span class="robot-binding-platform-icon">${escapeHtml((platformLabels[binding.platform] || binding.platform || '?').slice(0, 1).toUpperCase())}</span>
<div class="robot-binding-account-main">
<div class="robot-binding-account-name"><strong>${escapeHtml(platformLabels[binding.platform] || binding.platform || '-')}</strong><span>已连接</span></div>
<small>账号标识 ${escapeHtml(binding.external_user_hint || '-')} · 更新于 ${escapeHtml(formatRobotBindingTime(binding.updated_at))}</small>
</div>
<button type="button" class="btn-secondary btn-small robot-binding-unbind-btn" onclick="deleteRobotAccountBinding('${escapeHtml(binding.id || '')}')">解除绑定</button>
</div>`).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;
+60
View File
@@ -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
+112 -2
View File
@@ -148,6 +148,13 @@
</div>
</div>
<div class="user-menu-divider" role="separator"></div>
<div class="user-menu-item" role="menuitem" tabindex="0" onclick="openRobotAccountBinding()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();openRobotAccountBinding();}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span>绑定机器人账号</span>
</div>
<div class="user-menu-item" role="menuitem" tabindex="0" onclick="logout()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();logout();}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -3616,6 +3623,32 @@
<p data-i18n="settings.robots.emptyDesc">从上方列表点击已有平台,或点击“新建机器人”按类型创建。</p>
</div>
<div id="robot-auth-policy-panel" class="settings-subsection" hidden>
<h4>业务鉴权策略</h4>
<div class="settings-form">
<div class="form-group">
<label for="robot-auth-mode">鉴权模式</label>
<select id="robot-auth-mode" class="form-select" onchange="onRobotAuthModeChange()">
<option value="user_binding">逐用户绑定(多人机器人推荐)</option>
<option value="service_account">专用服务账号(仅白名单发送者)</option>
</select>
<small class="form-hint">平台验签负责确认消息来源;这里决定消息最终使用哪个 RBAC 身份。</small>
</div>
<div id="robot-service-account-fields" hidden>
<div class="form-group">
<label for="robot-service-user-id">服务账号 RBAC User ID</label>
<input type="text" id="robot-service-user-id" placeholder="例如 admin 或专用服务账号 User ID" autocomplete="off" oninput="updateRobotServiceAccountWarning()" />
<small id="robot-service-admin-warning" class="form-hint robot-service-admin-warning" hidden>当前使用 admin:白名单发送者将拥有完整平台权限,请仅添加你完全信任的账号。</small>
</div>
<div class="form-group">
<label for="robot-allowed-external-users">允许的平台发送者 ID</label>
<textarea id="robot-allowed-external-users" rows="4" placeholder="每行一个;在机器人中发送“身份”或 whoami 获取"></textarea>
<small class="form-hint">不支持 * 通配符。服务账号模式只有这里列出的真实发送者可以执行。</small>
</div>
</div>
</div>
</div>
<!-- 微信 / iLink -->
<div class="settings-subsection robot-editor-panel robot-wechat-card" id="robot-wechat-subsection" data-robot-editor="wechat" hidden>
<div class="robot-wechat-header">
@@ -3943,6 +3976,14 @@
<li><code>版本</code> <code>version</code><span data-i18n="settingsRobotsExtra.botCmdVersion">显示当前版本号 | Show version</span></li>
</ul>
<p class="robot-cmd-category" data-i18n="settingsRobotsExtra.botCmdCategoryIdentity">身份与鉴权</p>
<ul class="robot-cmd-list">
<li><code>身份</code> <code>whoami</code><span data-i18n="settingsRobotsExtra.botCmdIdentity">显示平台发送者、鉴权模式及当前实际 RBAC 身份 | Show effective identity</span></li>
<li><code>绑定 &lt;绑定码&gt;</code> <code>bind &lt;code&gt;</code><span data-i18n="settingsRobotsExtra.botCmdBindUser">绑定当前平台账号到 RBAC 用户(仅逐用户绑定模式) | Bind RBAC user</span></li>
<li><code>解绑</code> <code>unbind</code><span data-i18n="settingsRobotsExtra.botCmdUnbindUser">解除当前平台账号绑定(仅逐用户绑定模式) | Unbind RBAC user</span></li>
</ul>
<p class="settings-description" data-i18n="settingsRobotsExtra.botAuthHint">专用服务账号模式不接受“绑定/解绑”命令,仅允许机器人配置中白名单内的发送者。</p>
<p class="robot-cmd-category" data-i18n="settingsRobotsExtra.botCmdCategoryConversation">对话</p>
<ul class="robot-cmd-list">
<li><code>列表</code> <code>list</code><span data-i18n="settingsRobotsExtra.botCmdList">列出所有对话标题与 ID | List conversations</span></li>
@@ -3968,7 +4009,7 @@
<li><code>解除项目</code> <code>unbind project</code><span data-i18n="settingsRobotsExtra.botCmdUnbindProject">解除当前对话的项目绑定 | Unbind project</span></li>
</ul>
<p class="settings-description robot-cmd-footer" data-i18n="settingsRobotsExtra.botCommandsFooter">除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis.</p>
<p class="settings-description robot-cmd-footer" data-i18n="settingsRobotsExtra.botCommandsFooter">除以上命令外,直接输入内容将按绑定用户或服务账号的实时 RBAC 权限发送给 AI。Otherwise, text is sent to AI under the effective RBAC identity.</p>
</div>
</div>
</div>
@@ -3976,7 +4017,7 @@
<div class="settings-subsection robot-command-entry">
<div class="robot-command-entry-copy">
<h4 data-i18n="settingsRobotsExtra.botCommandsTitle">机器人命令说明</h4>
<p class="settings-description" data-i18n="settingsRobotsExtra.botCommandsEntryDesc">查看机器人对话中可用的中英文命令,包括对话、角色项目等常用操作。</p>
<p class="settings-description" data-i18n="settingsRobotsExtra.botCommandsEntryDesc">查看机器人对话中可用的中英文命令,包括身份鉴权、对话、角色项目操作。</p>
</div>
<button type="button" class="btn-secondary" onclick="openRobotCommandsModal()" data-i18n="settingsRobotsExtra.viewAllCommands">查看全部命令</button>
</div>
@@ -5695,6 +5736,75 @@
</div>
</div>
<div id="robot-account-binding-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="robot-account-binding-title">
<div class="modal-content robot-account-binding-content">
<div class="modal-header robot-binding-header">
<div class="robot-binding-heading">
<span class="robot-binding-heading-icon" aria-hidden="true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</span>
<div>
<h2 id="robot-account-binding-title">绑定机器人账号</h2>
<p>将 IM 平台身份安全关联到当前 RBAC 用户</p>
</div>
</div>
<button type="button" class="modal-close" onclick="closeRobotAccountBinding()" aria-label="关闭"></button>
</div>
<div class="modal-body robot-binding-body">
<div class="robot-binding-guide">
<span class="robot-binding-guide-number">1</span><span>生成一次性绑定码</span>
<span class="robot-binding-guide-arrow"></span>
<span class="robot-binding-guide-number">2</span><span>在机器人中发送绑定命令</span>
<span class="robot-binding-guide-arrow"></span>
<span class="robot-binding-guide-number">3</span><span>立即继承当前用户权限</span>
</div>
<section id="robot-binding-code-card" class="robot-binding-code-card is-empty">
<div class="robot-binding-code-meta">
<span class="robot-binding-one-time-badge">一次性安全码</span>
<span id="robot-binding-code-state" class="robot-binding-state">等待生成</span>
</div>
<div class="robot-binding-code-row">
<code id="robot-binding-code">••••-••••</code>
<div class="robot-binding-code-actions">
<button type="button" class="btn-primary" id="robot-binding-generate-btn" onclick="generateRobotBindingCode()">生成绑定码</button>
<button type="button" class="btn-secondary robot-binding-copy-btn" id="robot-binding-copy-btn" onclick="copyRobotBindingCode()" disabled>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/></svg>
<span>复制命令</span>
</button>
</div>
</div>
<div id="robot-binding-timer" class="robot-binding-timer" hidden>
<div class="robot-binding-timer-copy">
<span id="robot-binding-expiry">请在机器人中发送绑定命令</span>
<strong id="robot-binding-countdown">05:00</strong>
</div>
<div class="robot-binding-progress" aria-hidden="true"><span id="robot-binding-progress-bar"></span></div>
</div>
<p class="robot-binding-security-note">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" stroke="currentColor" stroke-width="2"/><path d="m9 12 2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
绑定码仅保存哈希、只能使用一次,新码会使旧码立即失效
</p>
</section>
<section class="robot-binding-list-section">
<div class="robot-binding-section-heading">
<div>
<h3>已绑定平台账号</h3>
<p>这些平台身份可使用当前用户的实时 RBAC 权限</p>
</div>
<button type="button" class="btn-secondary btn-small" onclick="loadRobotAccountBindings()">刷新</button>
</div>
<div id="robot-account-binding-list" class="robot-account-binding-list"><div class="robot-binding-list-loading">正在加载绑定信息…</div></div>
</section>
</div>
<div class="modal-footer robot-binding-footer">
<span>如需配置专用服务账号,请前往“系统设置 → 机器人设置”。</span>
<button type="button" class="btn-secondary" onclick="closeRobotAccountBinding()">关闭</button>
</div>
</div>
</div>
<script src="/static/vendor/i18next.min.js"></script>
<script src="/static/js/i18n.js"></script>
<script src="/static/js/theme.js"></script>