diff --git a/backend/api-server/config/approvers.json b/backend/api-server/config/approvers.json new file mode 100644 index 00000000..68bbff6e --- /dev/null +++ b/backend/api-server/config/approvers.json @@ -0,0 +1,41 @@ +{ + "version": "1.0.0", + "description": "天眼终审授权人配置 · 部署到正式站前的人类终审", + "rules": { + "男频": { + "approvers": [ + { "devId": "DEV-002", "name": "肥猫", "role": "男频内容授权人" } + ], + "requireAll": true + }, + "女频": { + "approvers": [ + { "devId": "DEV-010", "name": "桔子", "role": "女频内容授权人" } + ], + "requireAll": true + }, + "系统": { + "approvers": [ + { "devId": "DEV-002", "name": "肥猫", "role": "男频内容授权人" }, + { "devId": "DEV-010", "name": "桔子", "role": "女频内容授权人" } + ], + "requireAll": true + }, + "跨频": { + "approvers": [ + { "devId": "DEV-002", "name": "肥猫", "role": "男频内容授权人" }, + { "devId": "DEV-010", "name": "桔子", "role": "女频内容授权人" } + ], + "requireAll": true + } + }, + "defaults": { + "channel": "系统", + "timeoutHours": 72, + "noResponseAction": "不通过(不回应视为不通过)" + }, + "audit": { + "logEveryDecision": true, + "fields": ["timestamp", "approvalId", "devId", "decision", "channel", "module"] + } +} diff --git a/backend/api-server/config/autonomy-rules.json b/backend/api-server/config/autonomy-rules.json new file mode 100644 index 00000000..3c1d330d --- /dev/null +++ b/backend/api-server/config/autonomy-rules.json @@ -0,0 +1,75 @@ +{ + "version": "1.0.0", + "description": "系统自治运行规则 · Phase 9", + "dualLineArchitecture": { + "lineA": { + "name": "操作系统底座线", + "owner": "TCS-0002", + "scope": ["权限系统", "部署流水线", "神经网络升级", "执行保护层", "架构变更"], + "interactionModes": { + "instructionMode": { + "description": "冰朔说方向 → 霜砚拆指令 → 铸渊执行", + "trigger": ["签发指令", "写个指令", "下发指令"], + "suitableFor": "架构级、系统级变更" + }, + "naturalLanguageMode": { + "description": "冰朔直接和系统说话,系统即时响应", + "trigger": "默认模式(直接说需求/问问题)", + "suitableFor": "日常操作、快速调整" + } + }, + "priority": "最高 · 冰朔说话 = 最高优先级,无论哪种模式" + }, + "lineB": { + "name": "具体功能线", + "owners": "开发者团队", + "scope": ["Persona Studio", "工单系统", "数据看板", "动态漫画", "各模块功能"], + "interactionMode": "日常交互(和铸渊说话 → 系统执行)" + } + }, + "autonomyRules": { + "systemCanDo": [ + "自主调整内部运行逻辑(不违反铁律)", + "自主升级能力模块(记录到审计日志)", + "自主优化瓶颈路径", + "定期生成系统健康报告", + "异常时主动向霜砚报告" + ], + "systemMustNot": [ + "违反语言系统两条铁律", + "绕过天眼审核", + "绕过人类授权步骤", + "修改权限分层(权限变更只有冰朔能触发)" + ] + }, + "deploymentFlow": { + "stages": [ + { + "stage": 1, + "name": "部署到预览站", + "target": "GitHub Pages (qinfendebingshuo.github.io/guanghulab/docs/)", + "description": "真实可访问的预览环境,让人类确认执行结果" + }, + { + "stage": 2, + "name": "天眼审核", + "checks": ["代码安全", "结构完整性", "功能回归"], + "passAction": "进入 Stage 3", + "failAction": "标记问题,开发者修复后重新提交" + }, + { + "stage": 3, + "name": "人类终审授权", + "description": "授权人必须手动点击确认,不回应视为不通过", + "configFile": "approvers.json" + }, + { + "stage": 4, + "name": "自动发布到正式站", + "target": "guanghulab.com", + "trigger": "deploy-to-server.yml", + "description": "授权即发布,无需人类再次操作" + } + ] + } +} diff --git a/backend/api-server/config/permissions.js b/backend/api-server/config/permissions.js index 58c5d6fa..0343cae2 100644 --- a/backend/api-server/config/permissions.js +++ b/backend/api-server/config/permissions.js @@ -44,9 +44,10 @@ var PERMISSION_LEVELS = { 'agent:read', 'repo:read', 'deploy:preview', 'deploy:production', - 'maintenance:log' + 'maintenance:log', + 'approval:decide' ], - description: '可操作自己的模块,含正式站部署' + description: '可操作自己的模块,含正式站部署和授权决策' }, 3: { name: '管理者', @@ -60,7 +61,9 @@ var PERMISSION_LEVELS = { 'deploy:preview', 'deploy:production', 'broadcast:create', 'maintenance:log', - 'permission:manage' + 'permission:manage', + 'approval:decide', 'approval:create', + 'system:internal' ], description: '系统管理员,全部权限' } diff --git a/backend/api-server/routes/approval.js b/backend/api-server/routes/approval.js new file mode 100644 index 00000000..4f253ed7 --- /dev/null +++ b/backend/api-server/routes/approval.js @@ -0,0 +1,312 @@ +/** + * 部署授权流程路由 · Phase 8 + * + * 天眼审核通过后 → 推送授权请求给授权人 → 授权人确认/拒绝 → 自动发布 + * 系统绝不自动跳过授权步骤。没有人类点确认,就不发布。 + * + * POST /api/approval/request — 创建授权请求(天眼/系统内部调用) + * POST /api/approval/:id/decide — 授权人确认/拒绝 + * GET /api/approval/:id — 查询单个授权状态 + * GET /api/approval/pending — 查询待处理的授权列表 + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var express = require('express'); +var router = express.Router(); +var fs = require('fs'); +var path = require('path'); +var authMiddleware = require('../middleware/auth'); +var auditMiddleware = require('../middleware/audit'); +var approversConfig = require('../config/approvers.json'); + +// ====== 内存中的授权记录(生产环境可迁移到持久化存储)====== +var approvalStore = new Map(); + +// ====== 辅助函数 ====== + +/** + * 根据频道获取授权人列表 + */ +function getApprovers(channel) { + var rule = approversConfig.rules[channel] || approversConfig.rules[approversConfig.defaults.channel]; + return rule ? rule.approvers : approversConfig.rules['系统'].approvers; +} + +/** + * 写入审计日志(授权专用) + */ +function writeApprovalAudit(entry) { + var logDir = process.env.AUDIT_LOG_DIR || path.join(__dirname, '../../logs/audit'); + try { + fs.mkdirSync(logDir, { recursive: true }); + var today = new Date().toISOString().split('T')[0]; + var logFile = path.join(logDir, 'approval-' + today + '.jsonl'); + fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) { + if (err) console.error('授权审计日志写入失败:', err.message); + }); + } catch (e) { + console.error('授权审计日志写入失败:', e.message); + } +} + +// ====== 路由 ====== + +// 所有授权路由需要认证 +router.use(authMiddleware.requireAuth); +router.use(auditMiddleware.auditLog); + +/** + * 创建授权请求(天眼审核通过后由系统调用) + */ +router.post('/request', function(req, res) { + // 仅系统内部或管理员可创建授权请求 + if (req.user.permissionLevel < 3 && req.user.devId !== 'SYSTEM') { + return res.status(403).json({ + error: true, + code: 'PERMISSION_DENIED', + reply: '🔒 仅系统内部或管理员可创建授权请求。' + }); + } + + var body = req.body || {}; + var deployId = body.deployId; + var module = body.module; + var channel = body.channel || '系统'; + var reviewReport = body.reviewReport || {}; + + if (!deployId || !module) { + return res.status(400).json({ + error: true, + code: 'MISSING_FIELDS', + reply: '❌ 缺少必要字段:deployId, module' + }); + } + + var approvers = getApprovers(channel); + var approvalId = 'APPROVAL-' + Date.now() + '-' + Math.random().toString(36).substring(2, 6); + + var approval = { + id: approvalId, + deployId: deployId, + module: module, + channel: channel, + reviewReport: reviewReport, + approvers: approvers.map(function(a) { return a.devId; }), + approverDetails: approvers, + decisions: {}, + status: 'pending', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + approvalStore.set(approvalId, approval); + + writeApprovalAudit({ + action: 'approval_created', + approvalId: approvalId, + deployId: deployId, + module: module, + channel: channel, + approvers: approvers.map(function(a) { return a.devId; }), + createdBy: req.user.devId, + timestamp: new Date().toISOString() + }); + + res.json({ + success: true, + approvalId: approvalId, + approvers: approvers, + reply: '📋 授权请求已创建(' + approvalId + ')。已推送给授权人:' + + approvers.map(function(a) { return a.name + '(' + a.devId + ')'; }).join('、') + '。' + }); +}); + +/** + * 授权人确认/拒绝 + */ +router.post('/:approvalId/decide', function(req, res) { + var approvalId = req.params.approvalId; + var decision = (req.body || {}).decision; // 'approved' | 'rejected' + var reason = (req.body || {}).reason || ''; + var devId = req.user.devId; + + var approval = approvalStore.get(approvalId); + if (!approval) { + return res.status(404).json({ + error: true, + code: 'NOT_FOUND', + reply: '❌ 授权请求 ' + approvalId + ' 不存在。' + }); + } + + if (approval.status !== 'pending') { + return res.status(400).json({ + error: true, + code: 'ALREADY_DECIDED', + reply: '❌ 该授权请求已处理完毕(状态:' + approval.status + ')。' + }); + } + + // 验证是否为授权人 + if (!approval.approvers.includes(devId)) { + return res.status(403).json({ + error: true, + code: 'NOT_APPROVER', + reply: '🔒 你不是这个部署的授权人。授权人:' + + approval.approverDetails.map(function(a) { return a.name; }).join('、') + }); + } + + if (decision !== 'approved' && decision !== 'rejected') { + return res.status(400).json({ + error: true, + code: 'INVALID_DECISION', + reply: '❌ decision 必须是 "approved" 或 "rejected"。' + }); + } + + // 记录决定 + approval.decisions[devId] = { + decision: decision, + reason: reason, + timestamp: new Date().toISOString() + }; + approval.updatedAt = new Date().toISOString(); + + // 写入审计日志(不可逆) + writeApprovalAudit({ + action: 'approval_decision', + approvalId: approvalId, + devId: devId, + decision: decision, + reason: reason, + module: approval.module, + channel: approval.channel, + timestamp: new Date().toISOString() + }); + + if (decision === 'rejected') { + approval.status = 'rejected'; + return res.json({ + success: true, + reply: '❌ 已拒绝。部署不会执行。原因已记录到审计日志。' + }); + } + + // 检查是否所有授权人都已通过 + var allApproved = approval.approvers.every(function(id) { + return approval.decisions[id] && approval.decisions[id].decision === 'approved'; + }); + + if (allApproved) { + approval.status = 'approved'; + + // 触发正式站部署 + var githubService; + try { + githubService = require('../services/github'); + } catch (_) { + githubService = null; + } + + if (githubService && githubService.triggerWorkflow) { + githubService.triggerWorkflow('deploy-to-server.yml', { + module: approval.module, + deploy_id: approval.deployId, + approved_by: approval.approvers.join(','), + target: 'production' + }).then(function() { + writeApprovalAudit({ + action: 'deploy_triggered', + approvalId: approvalId, + module: approval.module, + approvedBy: approval.approvers, + timestamp: new Date().toISOString() + }); + }).catch(function(err) { + console.error('部署触发失败:', err.message); + writeApprovalAudit({ + action: 'deploy_trigger_failed', + approvalId: approvalId, + error: err.message, + timestamp: new Date().toISOString() + }); + }); + } + + return res.json({ + success: true, + reply: '✅ 所有授权人已通过。铸渊已自动触发正式站(guanghulab.com)部署。' + }); + } + + // 还有授权人未决定 + var pending = approval.approvers.filter(function(id) { return !approval.decisions[id]; }); + res.json({ + success: true, + reply: '✅ 你已授权通过。等待其他授权人确认:' + pending.join('、') + }); +}); + +/** + * 查询单个授权状态 + */ +router.get('/:approvalId', function(req, res) { + var approval = approvalStore.get(req.params.approvalId); + if (!approval) { + return res.status(404).json({ + error: true, + code: 'NOT_FOUND', + reply: '❌ 授权请求不存在。' + }); + } + + res.json({ + success: true, + approval: { + id: approval.id, + deployId: approval.deployId, + module: approval.module, + channel: approval.channel, + status: approval.status, + approvers: approval.approverDetails, + decisions: approval.decisions, + createdAt: approval.createdAt, + updatedAt: approval.updatedAt + } + }); +}); + +/** + * 查询待处理的授权列表(授权人查自己待处理的) + */ +router.get('/', function(req, res) { + var devId = req.user.devId; + var pending = []; + + for (var entry of approvalStore) { + var approval = entry[1]; + if (approval.status === 'pending' && approval.approvers.includes(devId) && !approval.decisions[devId]) { + pending.push({ + id: approval.id, + module: approval.module, + channel: approval.channel, + createdAt: approval.createdAt + }); + } + } + + res.json({ + success: true, + pending: pending, + count: pending.length, + reply: pending.length > 0 + ? '📋 你有 ' + pending.length + ' 个待授权请求。' + : '✅ 没有待处理的授权请求。' + }); +}); + +module.exports = router; diff --git a/backend/api-server/server.js b/backend/api-server/server.js index f46d225a..3ac5f8a9 100644 --- a/backend/api-server/server.js +++ b/backend/api-server/server.js @@ -48,23 +48,29 @@ app.use('/api', require('./routes/onboarding')); // 执行状态查询路由(需认证) app.use('/api/execution', require('./routes/execution')); +// 部署授权流程路由(需认证) +app.use('/api/approval', require('./routes/approval')); + // 根路由 app.get('/', function(_req, res) { res.json({ status: 'ok', service: 'guanghu-api-server', - version: '3.0.0', + version: '4.0.0', channel: 'B', description: '光湖后端中间层 · 语言驱动操作系统', - capabilities: ['read', 'write', 'intent-routing', 'permission-sandbox', 'onboarding', 'execution-guard'] + capabilities: [ + 'read', 'write', 'intent-routing', 'permission-sandbox', + 'onboarding', 'execution-guard', 'approval-flow', 'autonomy' + ] }); }); const PORT = process.env.PORT || 3001; app.listen(PORT, '127.0.0.1', function() { console.log('🔗 光湖后端中间层启动 · 端口 ' + PORT); - console.log(' 通道B · 语言驱动操作系统'); - console.log(' 能力:读取 + 写入 + 意图路由 + 权限沙箱 + 认知引导 + 执行保护'); + console.log(' 通道B · 语言驱动操作系统 v4.0.0'); + console.log(' 能力:读取 + 写入 + 意图路由 + 权限沙箱 + 认知引导 + 执行保护 + 授权流程 + 系统自治'); console.log(' 监听地址:127.0.0.1:' + PORT + '(仅本机访问)'); // 启动执行看门狗 diff --git a/backend/api-server/services/autonomy-engine.js b/backend/api-server/services/autonomy-engine.js new file mode 100644 index 00000000..2d8403a9 --- /dev/null +++ b/backend/api-server/services/autonomy-engine.js @@ -0,0 +1,123 @@ +/** + * 系统自治运行引擎 · Phase 9 + * + * 系统自主监控运行状态,自动优化,异常时主动报告。 + * 遵循自治规则:不违反铁律、不绕过天眼审核、不绕过人类授权。 + * + * 版权:国作登字-2026-A-00037559 + */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var autonomyRules = require('../config/autonomy-rules.json'); + +var AUTONOMY_LOG_DIR = process.env.AUTONOMY_LOG_DIR || + path.join(__dirname, '../../logs/autonomy'); + +/** + * 写入自治日志(所有自主操作记录到审计日志) + */ +function writeAutonomyLog(entry) { + try { + fs.mkdirSync(AUTONOMY_LOG_DIR, { recursive: true }); + var today = new Date().toISOString().split('T')[0]; + var logFile = path.join(AUTONOMY_LOG_DIR, 'autonomy-' + today + '.jsonl'); + fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) { + if (err) console.error('自治日志写入失败:', err.message); + }); + } catch (e) { + console.error('自治日志写入失败:', e.message); + } +} + +/** + * 检查自治操作是否合规 + * @param {string} action - 操作描述 + * @returns {Object} { allowed: boolean, reason: string } + */ +function checkAutonomyCompliance(action) { + var forbidden = autonomyRules.autonomyRules.systemMustNot; + var actionLower = action.toLowerCase(); + + // 检查是否触及禁止项 + if (actionLower.includes('权限') && actionLower.includes('修改')) { + return { allowed: false, reason: '权限变更只有冰朔能触发' }; + } + if (actionLower.includes('绕过') && actionLower.includes('天眼')) { + return { allowed: false, reason: '不得绕过天眼审核' }; + } + if (actionLower.includes('绕过') && actionLower.includes('授权')) { + return { allowed: false, reason: '不得绕过人类授权步骤' }; + } + + return { allowed: true, reason: 'ok' }; +} + +/** + * 检测冰朔交互模式 + * @param {string} message - 用户消息 + * @returns {string} 'instruction' | 'natural' + */ +function detectInteractionMode(message) { + var instructionTriggers = autonomyRules.dualLineArchitecture.lineA.interactionModes.instructionMode.trigger; + var msgLower = message.toLowerCase(); + + for (var i = 0; i < instructionTriggers.length; i++) { + if (msgLower.includes(instructionTriggers[i].toLowerCase())) { + return 'instruction'; + } + } + return 'natural'; +} + +/** + * 记录自主优化操作 + * @param {string} action - 操作描述 + * @param {Object} details - 操作详情 + */ +function logAutonomousAction(action, details) { + var compliance = checkAutonomyCompliance(action); + if (!compliance.allowed) { + console.error('[AUTONOMY] 自治操作被阻止: ' + action + ' — ' + compliance.reason); + writeAutonomyLog({ + type: 'blocked', + action: action, + reason: compliance.reason, + timestamp: new Date().toISOString() + }); + return false; + } + + writeAutonomyLog({ + type: 'autonomous_action', + action: action, + details: details || {}, + timestamp: new Date().toISOString() + }); + return true; +} + +/** + * 获取部署流水线阶段定义 + */ +function getDeploymentStages() { + return autonomyRules.deploymentFlow.stages; +} + +/** + * 获取双线架构配置 + */ +function getDualLineConfig() { + return autonomyRules.dualLineArchitecture; +} + +module.exports = { + checkAutonomyCompliance: checkAutonomyCompliance, + detectInteractionMode: detectInteractionMode, + logAutonomousAction: logAutonomousAction, + getDeploymentStages: getDeploymentStages, + getDualLineConfig: getDualLineConfig, + writeAutonomyLog: writeAutonomyLog +}; diff --git a/docs/css/approval.css b/docs/css/approval.css new file mode 100644 index 00000000..c4c422bd --- /dev/null +++ b/docs/css/approval.css @@ -0,0 +1,165 @@ +/** + * 部署授权样式 · Phase 8 + * 版权:国作登字-2026-A-00037559 + */ + +/* 授权提醒徽章 */ +.approval-badge { + position: fixed; + top: 16px; + right: 16px; + background: #ff6b35; + color: #fff; + padding: 8px 16px; + border-radius: 20px; + font-size: 14px; + cursor: pointer; + z-index: 9999; + box-shadow: 0 2px 12px rgba(255, 107, 53, 0.4); + animation: badge-pulse 2s infinite; +} + +.approval-badge:hover { + background: #e65a2a; +} + +@keyframes badge-pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +/* 授权容器 */ +.approval-container { + position: fixed; + top: 60px; + right: 16px; + z-index: 9998; + display: none; +} + +/* 授权面板 */ +.approval-panel { + background: #1e1e2e; + border: 1px solid #333; + border-radius: 12px; + width: 380px; + max-height: 70vh; + overflow-y: auto; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.approval-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 16px; + border-bottom: 1px solid #333; + font-size: 15px; + font-weight: 600; + color: #e0e0e0; +} + +.approval-close-btn { + background: none; + border: none; + color: #888; + font-size: 20px; + cursor: pointer; + padding: 0 4px; +} + +.approval-close-btn:hover { + color: #fff; +} + +.approval-panel-body { + padding: 8px; +} + +/* 授权条目 */ +.approval-item { + background: #252535; + border-radius: 8px; + padding: 12px; + margin-bottom: 8px; +} + +.approval-item-info { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + margin-bottom: 10px; +} + +.approval-item-info strong { + color: #e0e0e0; + font-size: 14px; +} + +.approval-channel { + background: #3a3a5a; + color: #aaa; + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; +} + +.approval-time { + color: #666; + font-size: 12px; +} + +/* 授权按钮 */ +.approval-item-actions { + display: flex; + gap: 8px; +} + +.approval-btn { + flex: 1; + padding: 8px 12px; + border: none; + border-radius: 6px; + font-size: 13px; + cursor: pointer; + transition: background 0.2s; +} + +.approval-btn.approve { + background: rgba(15, 123, 108, 0.2); + color: #0F7B6C; +} + +.approval-btn.approve:hover { + background: rgba(15, 123, 108, 0.4); +} + +.approval-btn.reject { + background: rgba(212, 76, 71, 0.2); + color: #D44C47; +} + +.approval-btn.reject:hover { + background: rgba(212, 76, 71, 0.4); +} + +/* 通知消息 */ +.approval-notification { + position: fixed; + bottom: 24px; + right: 24px; + background: #1e1e2e; + border: 1px solid #333; + color: #e0e0e0; + padding: 12px 20px; + border-radius: 8px; + font-size: 14px; + z-index: 10000; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + transition: opacity 0.5s; +} + +.approval-notification.fade-out { + opacity: 0; +} diff --git a/docs/js/approval-ui.js b/docs/js/approval-ui.js new file mode 100644 index 00000000..18d7aa9b --- /dev/null +++ b/docs/js/approval-ui.js @@ -0,0 +1,230 @@ +/** + * 部署授权 UI · Phase 8 + * + * 授权人在交互页面收到授权请求推送时,展示授权弹窗。 + * 授权人必须主动点击确认,不回应视为不通过。 + * 系统绝不自动跳过授权步骤。 + * + * 版权:国作登字-2026-A-00037559 + */ + +/* global HOLOLAKE_ENV */ + +(function(window) { + 'use strict'; + + var API_BASE = (typeof HOLOLAKE_ENV !== 'undefined' && HOLOLAKE_ENV === 'production') + ? 'https://guanghulab.com/api' : ''; + + /** + * ApprovalUI 构造函数 + */ + function ApprovalUI() { + this._pollTimer = null; + this._container = null; + } + + /** + * 初始化:开始轮询待授权请求 + * @param {string} devId - 当前登录的开发者编号 + * @param {string} token - 认证 token + */ + ApprovalUI.prototype.init = function(devId, token) { + this.devId = devId; + this.token = token; + this._ensureContainer(); + this._startPolling(); + }; + + /** + * 确保授权容器存在 + */ + ApprovalUI.prototype._ensureContainer = function() { + if (this._container) return; + this._container = document.createElement('div'); + this._container.id = 'approval-container'; + this._container.className = 'approval-container'; + document.body.appendChild(this._container); + }; + + /** + * 开始轮询待授权请求(每 30 秒) + */ + ApprovalUI.prototype._startPolling = function() { + var self = this; + if (!API_BASE) return; + + var poll = function() { + self._fetchPendingApprovals(); + }; + + poll(); + this._pollTimer = setInterval(poll, 30000); + }; + + /** + * 停止轮询 + */ + ApprovalUI.prototype.destroy = function() { + if (this._pollTimer) { + clearInterval(this._pollTimer); + this._pollTimer = null; + } + }; + + /** + * 获取待处理的授权请求 + */ + ApprovalUI.prototype._fetchPendingApprovals = function() { + var self = this; + if (!API_BASE) return; + + fetch(API_BASE + '/approval', { + headers: { + 'x-dev-id': this.devId, + 'Authorization': 'Bearer ' + this.token + } + }) + .then(function(res) { return res.json(); }) + .then(function(data) { + if (data.success && data.pending && data.pending.length > 0) { + self._showApprovalBadge(data.pending.length); + self._renderPendingList(data.pending); + } else { + self._hideApprovalBadge(); + } + }) + .catch(function() { /* 静默失败 */ }); + }; + + /** + * 显示授权提醒徽章 + */ + ApprovalUI.prototype._showApprovalBadge = function(count) { + var badge = document.getElementById('approval-badge'); + if (!badge) { + badge = document.createElement('div'); + badge.id = 'approval-badge'; + badge.className = 'approval-badge'; + document.body.appendChild(badge); + } + badge.textContent = '🔔 ' + count + ' 个待授权'; + badge.style.display = 'block'; + var self = this; + badge.onclick = function() { self._togglePanel(); }; + }; + + /** + * 隐藏授权徽章 + */ + ApprovalUI.prototype._hideApprovalBadge = function() { + var badge = document.getElementById('approval-badge'); + if (badge) badge.style.display = 'none'; + if (this._container) this._container.style.display = 'none'; + }; + + /** + * 切换授权面板显示 + */ + ApprovalUI.prototype._togglePanel = function() { + if (this._container) { + this._container.style.display = + this._container.style.display === 'none' ? 'block' : 'none'; + } + }; + + /** + * 渲染待授权列表 + */ + ApprovalUI.prototype._renderPendingList = function(pending) { + if (!this._container) return; + + var html = '