主题切换
附录 D:自动化安全与异常处理
当应用开始控制鼠标和键盘时,安全与可控性变得至关重要。本附录涵盖自动化操作的边界控制、用户授权、紧急停止和审计日志等生产级实践,帮助你的 RPA 应用在自动化能力与系统安全之间找到平衡。
D.1 无限循环防护与超时机制
自动化任务最容易出现的问题之一就是无限循环——当某个条件始终无法满足时,程序会永远等待下去。生产级 RPA 必须设置多层次的超时保护。
单步操作超时
为每一个自动化操作设置最大执行时间,超时则抛出异常并终止当前步骤:
typescript
interface StepOptions {
timeout?: number // 单步超时(毫秒),默认 30000
retryCount?: number // 重试次数,默认 0
retryInterval?: number // 重试间隔(毫秒),默认 1000
}
class AutomationStep {
async executeWithTimeout<T>(
action: () => Promise<T>,
options: StepOptions = {}
): Promise<T> {
const { timeout = 30000 } = options
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`操作超时(${timeout}ms):步骤未在限定时间内完成`))
}, timeout)
action()
.then(resolve)
.catch(reject)
.finally(() => clearTimeout(timer))
})
}
}全局执行超时
整个自动化流程也应该有总时间上限:
typescript
class FlowExecutor {
private globalTimeout = 10 * 60 * 1000 // 10 分钟全局超时
async execute(flow: AutomationFlow): Promise<FlowResult> {
const startTime = Date.now()
const results: StepResult[] = []
for (const step of flow.steps) {
// 检查全局超时
if (Date.now() - startTime > this.globalTimeout) {
throw new Error(`流程全局超时(${this.globalTimeout}ms),已强制终止`)
}
try {
const result = await this.executeStep(step)
results.push(result)
} catch (error) {
results.push({ step: step.id, status: 'error', error })
// 根据策略决定是否继续
if (flow.stopOnError) break
}
}
return { results, duration: Date.now() - startTime }
}
}循环次数限制
对于包含循环的流程,必须限制最大迭代次数:
typescript
class SafeLoop {
private maxIterations = 1000
async while(
condition: () => Promise<boolean>,
action: () => Promise<void>
): Promise<void> {
let count = 0
while (await condition()) {
if (++count > this.maxIterations) {
throw new Error(`循环次数超过安全上限(${this.maxIterations}),强制退出`)
}
await action()
}
}
}建议
超时时间的设定应该根据实际操作类型调整。鼠标移动可以设为 10 秒,而等待某个应用启动可能需要 60 秒以上。
D.2 用户授权与确认流
在 RPA 应用控制用户输入设备之前,必须获得明确的用户授权。这不仅是一个安全最佳实践,也是避免法律风险的重要措施。
权限分级设计
typescript
type PermissionLevel = 'observer' | 'assistant' | 'full-control'
interface PermissionConfig {
level: PermissionLevel
description: string
requiresConfirmation: boolean
}
const PERMISSIONS: Record<PermissionLevel, PermissionConfig> = {
'observer': {
level: 'observer',
description: '仅观察和截图,不操作鼠标键盘',
requiresConfirmation: false
},
'assistant': {
level: 'assistant',
description: '辅助操作:仅在用户触发后执行预设动作',
requiresConfirmation: true
},
'full-control': {
level: 'full-control',
description: '完全控制:可自动移动鼠标、输入键盘',
requiresConfirmation: true
}
}控制前确认弹窗
在 Electron 主进程中,使用 dialog 模块创建授权确认:
typescript
import { dialog } from 'electron'
async function requestAutomationPermission(
level: PermissionLevel,
reason: string
): Promise<boolean> {
const config = PERMISSIONS[level]
if (!config.requiresConfirmation) return true
const { response } = await dialog.showMessageBox({
type: 'warning',
title: '自动化操作授权',
message: `应用请求${config.description}权限`,
detail: `原因:${reason}\n\n确认后应用将在 30 秒内获得控制权限。`,
buttons: ['拒绝', '授权(30秒)', '始终授权'],
defaultId: 1,
cancelId: 0,
checkboxLabel: '不再询问(可在设置中更改)'
})
return response > 0
}首次使用的用户告知协议
应用在首次启动时应展示使用协议,明确告知用户应用将控制输入设备:
typescript
import { dialog } from 'electron'
import Store from 'electron-store'
const store = new Store()
async function showFirstRunAgreement(): Promise<boolean> {
if (store.get('agreementAccepted')) return true
const { response, checkboxChecked } = await dialog.showMessageBox({
type: 'info',
title: '欢迎使用 RPA 桌面助手',
message: '在使用自动化功能前,请阅读以下说明',
detail: `本应用可以模拟鼠标移动、点击和键盘输入,以帮你自动完成重复性工作。
安全说明:
• 自动化过程中请保持对电脑的关注
• 可随时按 Esc 键紧急停止
• 所有操作都会被记录到审计日志
• 你可以在设置中随时关闭自动化权限
点击"同意并继续"表示你了解并接受上述说明。`,
buttons: ['退出应用', '同意并继续'],
defaultId: 1
})
if (response === 1) {
store.set('agreementAccepted', true)
return true
}
return false
}D.3 紧急停止(Panic Key)
无论自动化流程执行到什么阶段,用户都必须有能力立即中断它。这是 RPA 应用最核心的安全机制。
全局快捷键注册
在 Electron 主进程中注册全局快捷键:
typescript
import { globalShortcut, dialog } from 'electron'
import { EventEmitter } from 'events'
class PanicKeyManager extends EventEmitter {
private isRegistered = false
register(): boolean {
if (this.isRegistered) return true
// 注册 Esc 键作为紧急停止(在 macOS 上需要特殊处理)
const success = globalShortcut.register('Escape', () => {
this.emit('panic', { reason: 'user_pressed_escape', timestamp: Date.now() })
})
// 备用组合键
const backupSuccess = globalShortcut.register('Ctrl+Shift+Q', () => {
this.emit('panic', { reason: 'user_pressed_combo', timestamp: Date.now() })
})
this.isRegistered = success || backupSuccess
return this.isRegistered
}
unregister(): void {
globalShortcut.unregister('Escape')
globalShortcut.unregister('Ctrl+Shift+Q')
this.isRegistered = false
}
// 检查快捷键是否被其他应用占用
isAvailable(): boolean {
return globalShortcut.isRegistered('Escape') === false
}
}
export const panicKey = new PanicKeyManager()立即停止所有自动化操作
当 panic 事件触发时,需要层层传递停止信号:
typescript
class AutomationEngine extends EventEmitter {
private isRunning = false
private currentAbortController: AbortController | null = null
private pressedKeys: Set<Key> = new Set() // 仅追踪实际按下的键
constructor() {
super()
// 监听 panic 事件
panicKey.on('panic', (event) => {
console.log('🚨 紧急停止触发:', event.reason)
this.emergencyStop()
})
}
async start(flow: AutomationFlow, signal?: AbortSignal): Promise<FlowResult> {
this.isRunning = true
this.currentAbortController = new AbortController()
// 合并外部 signal 和内部 abort controller
if (signal) {
signal.addEventListener('abort', () => this.currentAbortController?.abort())
}
try {
const result = await this.executeFlow(flow, this.currentAbortController.signal)
return result
} finally {
this.isRunning = false
this.currentAbortController = null
}
}
emergencyStop(): void {
if (!this.isRunning) return
// 1. 发送中止信号
this.currentAbortController?.abort()
// 2. 立即释放所有按下的按键
this.releaseAllKeys()
// 3. 通知渲染进程
this.emit('stopped', { reason: 'emergency_stop' })
// 4. 记录到日志
auditLog.record({
action: 'EMERGENCY_STOP',
timestamp: new Date().toISOString(),
details: '用户触发了紧急停止'
})
}
private async releaseAllKeys(): Promise<void> {
const { keyboard } = require('@nut-tree/nut-js')
// 仅释放实际按下的键,而非遍历所有 Key 枚举值
for (const key of this.pressedKeys) {
try { await keyboard.releaseKey(key) } catch { /* 忽略释放失败的键 */ }
}
this.pressedKeys.clear()
}
}注意
在 Windows 上,某些游戏或安全软件可能会拦截全局快捷键。建议在 UI 中同时提供"停止"按钮作为备选方案。
D.4 操作审计日志
审计日志是 RPA 应用在生产环境中不可或缺的功能,它记录了"谁在什么时候做了什么"。
日志结构设计
typescript
interface AuditEntry {
id: string
timestamp: string
sessionId: string
action: 'MOUSE_MOVE' | 'MOUSE_CLICK' | 'KEYBOARD_TYPE' | 'SCREENSHOT'
| 'FLOW_START' | 'FLOW_END' | 'EMERGENCY_STOP' | 'ERROR'
status: 'success' | 'failure' | 'cancelled'
details: {
coordinates?: { x: number; y: number }
target?: string
duration?: number
errorMessage?: string
}
}
class AuditLogger {
private logPath: string
constructor(appDataPath: string) {
this.logPath = path.join(appDataPath, 'audit-logs')
if (!fs.existsSync(this.logPath)) {
fs.mkdirSync(this.logPath, { recursive: true })
}
}
async record(entry: Omit<AuditEntry, 'id' | 'timestamp'>): Promise<void> {
const fullEntry: AuditEntry = {
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
...entry
}
const dateStr = new Date().toISOString().split('T')[0]
const filePath = path.join(this.logPath, `audit-${dateStr}.jsonl`)
await fs.promises.appendFile(
filePath,
JSON.stringify(fullEntry) + '\n',
'utf-8'
)
}
// 读取最近 N 条日志
async getRecent(count: number = 100): Promise<AuditEntry[]> {
// 实现略:读取最新的 jsonl 文件并解析
}
}在自动化流程中埋点
typescript
class AuditedExecutor extends FlowExecutor {
constructor(private auditLogger: AuditLogger) {
super()
}
async executeStep(step: AutomationStep): Promise<StepResult> {
const startTime = Date.now()
await this.auditLogger.record({
sessionId: this.sessionId,
action: this.mapStepToAction(step.type),
status: 'success',
details: { target: step.target }
})
try {
const result = await super.executeStep(step)
await this.auditLogger.record({
sessionId: this.sessionId,
action: this.mapStepToAction(step.type),
status: 'success',
details: { duration: Date.now() - startTime }
})
return result
} catch (error) {
await this.auditLogger.record({
sessionId: this.sessionId,
action: 'ERROR',
status: 'failure',
details: { errorMessage: error.message }
})
throw error
}
}
}D.5 失败自动截图与重试
当自动化操作失败时,自动截图可以帮助开发者快速定位问题原因。
失败时自动截图
typescript
import { screen } from '@nut-tree/nut-js'
class FailureScreenshot {
constructor(private saveDir: string) {}
async capture(context: string): Promise<string> {
const timestamp = Date.now()
const filename = `failure-${context}-${timestamp}.png`
const filepath = path.join(this.saveDir, filename)
// 并行获取屏幕尺寸,避免嵌套 Promise 链
const [w, h] = await Promise.all([screen.width(), screen.height()])
const img = await screen.capture({ left: 0, top: 0, width: w, height: h })
await img.toFile(filepath)
return filepath
}
}指数退避重试策略
typescript
async function withRetry<T>(
operation: () => Promise<T>,
options: {
maxRetries?: number
baseDelay?: number
maxDelay?: number
onRetry?: (attempt: number, error: Error) => void
} = {}
): Promise<T> {
const {
maxRetries = 3,
baseDelay = 1000,
maxDelay = 30000,
onRetry
} = options
let lastError: Error
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation()
} catch (error) {
lastError = error
if (attempt < maxRetries) {
// 指数退避:1s, 2s, 4s, 8s...
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay)
onRetry?.(attempt + 1, error)
await sleep(delay)
}
}
}
throw new Error(`操作在 ${maxRetries + 1} 次尝试后仍然失败:${lastError.message}`)
}D.6 事务与回滚概念
复杂的自动化流程往往涉及多个步骤,如果中间某步失败,可能需要恢复到之前的状态。
状态快照
typescript
interface StateSnapshot {
timestamp: number
activeWindow: string | null
mousePosition: { x: number; y: number }
clipboardContent: string
}
class StateManager {
async captureSnapshot(): Promise<StateSnapshot> {
const { mouse } = require('@nut-tree/nut-js')
const { clipboard } = require('electron')
return {
timestamp: Date.now(),
activeWindow: await this.getActiveWindowTitle(),
mousePosition: await mouse.getPosition(),
clipboardContent: clipboard.readText()
}
}
async restoreSnapshot(snapshot: StateSnapshot): Promise<void> {
const { mouse, window: winApi } = require('@nut-tree/nut-js')
// 恢复鼠标位置
await mouse.move(straightTo(new Point(
snapshot.mousePosition.x,
snapshot.mousePosition.y
)))
// 尝试激活原来的窗口
if (snapshot.activeWindow) {
const windows = await winApi.getWindows()
for (const win of windows) {
const title = await win.title
if (title === snapshot.activeWindow) {
await winApi.focus(win)
break
}
}
}
}
}实际案例:表单填写的回滚
typescript
class FormTransaction {
private snapshots: StateSnapshot[] = []
private completedSteps: string[] = []
constructor(private stateManager: StateManager) {}
async begin(): Promise<void> {
this.snapshots = [await this.stateManager.captureSnapshot()]
this.completedSteps = []
}
async step(name: string, action: () => Promise<void>): Promise<void> {
// 每步前保存快照
this.snapshots.push(await this.stateManager.captureSnapshot())
try {
await action()
this.completedSteps.push(name)
} catch (error) {
console.error(`步骤 "${name}" 失败,准备回滚...`)
await this.rollback()
throw error
}
}
async rollback(): Promise<void> {
// 恢复到最初状态
if (this.snapshots.length > 0) {
await this.stateManager.restoreSnapshot(this.snapshots[0])
}
// 执行清理动作(如关闭临时打开的窗口)
for (const step of [...this.completedSteps].reverse()) {
await this.cleanupStep(step)
}
}
private async cleanupStep(stepName: string): Promise<void> {
// 根据步骤名称执行对应的清理逻辑
switch (stepName) {
case 'open_modal':
// 按 ESC 关闭弹窗
await keyboard.pressKey(Key.Escape)
await keyboard.releaseKey(Key.Escape)
break
// ... 其他清理逻辑
}
}
}
// 使用示例
async function fillFormWithRollback(data: FormData): Promise<void> {
const tx = new FormTransaction(stateManager)
await tx.begin()
await tx.step('focus_name_field', async () => {
await clickOnField('name')
await typeText(data.name)
})
await tx.step('focus_email_field', async () => {
await clickOnField('email')
await typeText(data.email)
})
await tx.step('submit_form', async () => {
await clickButton('提交')
})
}回滚的局限性
并非所有操作都可回滚(如已发送的邮件、已删除的文件)。在设计自动化流程时,应尽量将不可逆操作放在最后执行。