feat: implement Phase 7 Execution Protection Layer (ZY-LANGDRIVE-S1)

L1 Frontend Isolation:
- docs/js/execution-guard.js: UI lock/unlock, execution replay polling, browser guard
- docs/css/execution-guard.css: Execution lock styles, progress bar, log entries

L2 API Execution Interception:
- backend/api-server/middleware/execution-lock.js: Execution context lock, cancel interception
- backend/api-server/routes/execution.js: Status query, cancel/delete rejection (403)

L3 Execution Atomicity:
- backend/api-server/services/checkpoint.js: Checkpoint system with rollback
- backend/api-server/services/atomic-executor.js: Atomic execution wrapper
- backend/api-server/services/execution-watchdog.js: 15min timeout, deadlock prevention

Integration:
- Updated server.js to v3.0.0: execution routes, cancellation middleware, watchdog startup

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/6ee737eb-684b-44b8-aeaa-f73eb7c6f9c2
This commit is contained in:
copilot-swe-agent[bot] 2026-03-25 08:37:56 +00:00
parent 5e8bb969a4
commit e0880deb46
8 changed files with 1070 additions and 4 deletions

View File

@ -0,0 +1,170 @@
/**
* 执行锁中间件 · L2 API 执行拦截层
*
* 当一个操作开始执行时
* 1. 为该操作创建一个执行上下文executionId
* 2. 锁定相关资源防止并发冲突
* 3. 拒绝一切 cancel/abort/rollback 请求
* 4. 执行完成后自动释放锁
*
* 同一个开发者不能同时执行多个写入操作
* 版权国作登字-2026-A-00037559
*/
'use strict';
var crypto = require('crypto');
// 内存中的执行锁表
var activeLocks = new Map();
function generateExecutionId() {
return 'exec-' + Date.now() + '-' + crypto.randomBytes(4).toString('hex');
}
/**
* 获取执行锁表引用供看门狗使用
*/
function getActiveLocks() {
return activeLocks;
}
/**
* 创建执行锁
* @param {string} devId - 开发者编号
* @param {string} operation - 操作描述
* @returns {Object} { success, executionId, lock } { success: false, reply }
*/
function acquireLock(devId, operation) {
if (activeLocks.has(devId)) {
var existing = activeLocks.get(devId);
if (existing.state === 'running') {
return {
success: false,
reply: '⏳ 你有一个操作正在执行中(' + existing.operation +
'),请等待完成。\n\n系统不允许同时执行多个操作——这是为了保护数据一致性。'
};
}
}
var executionId = generateExecutionId();
var lock = {
executionId: executionId,
devId: devId,
operation: operation,
startTime: Date.now(),
state: 'running',
steps: [],
checkpoints: [],
warned: false,
completedSteps: 0,
totalSteps: 0,
result: null,
error: null
};
activeLocks.set(devId, lock);
return {
success: true,
executionId: executionId,
lock: lock
};
}
/**
* 释放执行锁
*/
function releaseLock(devId) {
activeLocks.delete(devId);
}
/**
* 获取执行状态通过 executionId
*/
function getExecutionStatus(executionId) {
for (var entry of activeLocks) {
if (entry[1].executionId === executionId) {
return entry[1];
}
}
return null;
}
/**
* 记录执行步骤用于事后回放
*/
function logStep(devId, step) {
var lock = activeLocks.get(devId);
if (lock) {
lock.steps.push({
message: step.message,
success: step.success,
timestamp: new Date().toISOString()
});
if (step.success) {
lock.completedSteps++;
}
}
}
/**
* 保存检查点用于失败时回滚
*/
function saveCheckpoint(devId, checkpoint) {
var lock = activeLocks.get(devId);
if (lock) {
lock.checkpoints.push({
id: checkpoint.id,
label: checkpoint.label,
data: checkpoint.data,
timestamp: new Date().toISOString()
});
}
}
/**
* 取消拦截中间件
* 拦截所有试图取消执行中操作的请求
*/
function blockCancellation(req, res, next) {
var devId = req.user ? req.user.devId : null;
if (!devId) return next();
var isCancelAttempt =
req.path.includes('/cancel') ||
req.path.includes('/abort') ||
req.path.includes('/rollback');
var isDeleteOnLocked = req.method === 'DELETE' && activeLocks.has(devId);
if (isCancelAttempt || isDeleteOnLocked) {
var lock = activeLocks.get(devId);
if (lock && lock.state === 'running') {
return res.status(423).json({
error: true,
code: 'EXECUTION_LOCKED',
locked: true,
executionId: lock.executionId,
reply: '🔒 操作「' + lock.operation + '」正在执行中,不可取消。\n\n' +
'系统已接收你的指令并开始执行。这条执行链路涉及多个自动化流程的联动,' +
'中途中断会导致数据不一致和级联故障。\n\n' +
'⏳ 请等待执行完成。完成后你会看到完整的执行结果。\n\n' +
'如果你对结果不满意,可以说「前面的没想好,我们重新来」——' +
'系统会在已有结果的基础上重新迭代,而不是撤回已执行的操作。'
});
}
}
next();
}
module.exports = {
acquireLock: acquireLock,
releaseLock: releaseLock,
getExecutionStatus: getExecutionStatus,
getActiveLocks: getActiveLocks,
logStep: logStep,
saveCheckpoint: saveCheckpoint,
blockCancellation: blockCancellation
};

View File

@ -0,0 +1,84 @@
/**
* 执行状态查询路由 · L2 API
*
* GET /api/execution/:executionId/status 前端轮询执行状态
* POST /api/execution/:executionId/cancel 明确拒绝取消403
* DELETE /api/execution/:executionId 明确拒绝删除403
*
* 注意没有取消接口这不是遗漏是设计
* 版权国作登字-2026-A-00037559
*/
'use strict';
var express = require('express');
var router = express.Router();
var executionLock = require('../middleware/execution-lock');
var authMiddleware = require('../middleware/auth');
router.use(authMiddleware.requireAuth);
// 查询执行状态(前端轮询用)
router.get('/:executionId/status', function(req, res) {
var status = executionLock.getExecutionStatus(req.params.executionId);
if (!status) {
return res.json({
state: 'not_found',
reply: '未找到该执行记录。可能已经完成。'
});
}
// 计算进度百分比
var progress = 5;
if (status.totalSteps > 0) {
progress = Math.min(95, Math.round((status.completedSteps / status.totalSteps) * 100));
} else if (status.steps.length > 0) {
progress = Math.min(95, status.steps.length * 15);
}
var afterIdx = parseInt(req.query.after, 10) || 0;
res.json({
executionId: status.executionId,
state: status.state,
operation: status.operation,
progress: progress,
currentStep: status.steps.length > 0
? status.steps[status.steps.length - 1].message
: '初始化执行环境…',
completedSteps: status.steps.filter(function(s) { return s.success; }).map(function(s) {
return { message: s.message, timestamp: s.timestamp };
}),
newLogs: status.steps.slice(afterIdx),
startTime: new Date(status.startTime).toISOString(),
elapsed: Date.now() - status.startTime,
result: status.state === 'completed' ? status.result : undefined,
error: status.state === 'failed' ? status.error : undefined
});
});
// 明确拒绝取消执行的请求
router.post('/:executionId/cancel', function(_req, res) {
res.status(403).json({
error: true,
code: 'CANCEL_FORBIDDEN',
reply: '🔒 执行中的操作不可取消。\n\n' +
'系统已开始执行90+ 个自动化流程正在联动。' +
'中途中断会导致数据不一致和级联故障。\n\n' +
'请等待执行完成。如果结果不理想,你可以说:\n' +
'「前面的我没想好,我们重新来」\n' +
'系统会在已有基础上重新迭代。'
});
});
// 明确拒绝删除执行记录
router.delete('/:executionId', function(_req, res) {
res.status(403).json({
error: true,
code: 'DELETE_FORBIDDEN',
reply: '🔒 不可删除执行记录。所有执行都是永久记录。'
});
});
module.exports = router;

View File

@ -2,7 +2,7 @@
* 🔗 光湖后端中间层 · 通道B 入口
*
* 实时调用 Notion API / GitHub API为前端提供数据代理
* 包含读取层 + 写入层 + 意图路由 + 权限沙箱 + 认知引导
* 包含读取层 + 写入层 + 意图路由 + 权限沙箱 + 认知引导 + 执行保护
* 只监听 127.0.0.1通过 Nginx 反向代理对外暴露
*
* 版权国作登字-2026-A-00037559
@ -12,6 +12,8 @@
const express = require('express');
const cors = require('cors');
const executionLock = require('./middleware/execution-lock');
const watchdog = require('./services/execution-watchdog');
const app = express();
@ -27,6 +29,9 @@ app.use(cors({
app.use(express.json());
// 执行锁取消拦截(全局中间件,在所有路由之前)
app.use(executionLock.blockCancellation);
// 读取类路由(公开,无需认证)
app.use('/api', require('./routes/health'));
app.use('/api', require('./routes/dev'));
@ -40,15 +45,18 @@ app.use('/api', require('./routes/write'));
// 认知引导 + 工具列表路由
app.use('/api', require('./routes/onboarding'));
// 执行状态查询路由(需认证)
app.use('/api/execution', require('./routes/execution'));
// 根路由
app.get('/', function(_req, res) {
res.json({
status: 'ok',
service: 'guanghu-api-server',
version: '2.0.0',
version: '3.0.0',
channel: 'B',
description: '光湖后端中间层 · 语言驱动操作系统',
capabilities: ['read', 'write', 'intent-routing', 'permission-sandbox', 'onboarding']
capabilities: ['read', 'write', 'intent-routing', 'permission-sandbox', 'onboarding', 'execution-guard']
});
});
@ -56,6 +64,9 @@ const PORT = process.env.PORT || 3001;
app.listen(PORT, '127.0.0.1', function() {
console.log('🔗 光湖后端中间层启动 · 端口 ' + PORT);
console.log(' 通道B · 语言驱动操作系统');
console.log(' 能力:读取 + 写入 + 意图路由 + 权限沙箱 + 认知引导');
console.log(' 能力:读取 + 写入 + 意图路由 + 权限沙箱 + 认知引导 + 执行保护');
console.log(' 监听地址127.0.0.1:' + PORT + '(仅本机访问)');
// 启动执行看门狗
watchdog.startWatchdog();
});

View File

@ -0,0 +1,157 @@
/**
* 原子执行器 · L3 执行原子性保障
*
* 包装所有写入操作确保
* 1. 执行前保存检查点
* 2. 执行中记录每一步
* 3. 执行失败自动回滚
* 4. 执行完成释放锁
*
* 使用方式
* var executor = new AtomicExecutor(devId);
* var result = await executor.execute('创建工单', async function(ctx) {
* await ctx.step('写入Notion', async function() { ... });
* await ctx.step('触发Workflow', async function() { ... });
* });
*
* 版权国作登字-2026-A-00037559
*/
'use strict';
var executionLock = require('../middleware/execution-lock');
var checkpoint = require('./checkpoint');
var CheckpointManager = checkpoint.CheckpointManager;
/**
* 原子执行器
* @param {string} devId - 开发者编号
*/
function AtomicExecutor(devId) {
this.devId = devId;
}
/**
* 执行操作原子性保障
* @param {string} operationName - 操作描述
* @param {Function} fn - 异步执行函数接收 ctx 参数
* @returns {Object} 执行结果
*/
AtomicExecutor.prototype.execute = async function(operationName, fn) {
// 1. 获取执行锁
var lockResult = executionLock.acquireLock(this.devId, operationName);
if (!lockResult.success) {
return lockResult;
}
var executionId = lockResult.executionId;
var lock = lockResult.lock;
var devId = this.devId;
var cpManager = new CheckpointManager(devId, executionId);
// 2. 保存初始检查点
await cpManager.save('执行前状态', {
type: 'initial',
timestamp: new Date().toISOString()
});
// 3. 构建执行上下文
var ctx = {
executionId: executionId,
/**
* 执行单步操作
* @param {string} stepName - 步骤名称
* @param {Function} stepFn - 步骤执行函数
* @returns {*} 步骤执行结果
*/
step: async function(stepName, stepFn) {
executionLock.logStep(devId, {
message: '⏳ ' + stepName + '...',
success: false
});
try {
var result = await stepFn();
executionLock.logStep(devId, {
message: '✅ ' + stepName,
success: true
});
return result;
} catch (e) {
executionLock.logStep(devId, {
message: '❌ ' + stepName + ': ' + e.message,
success: false
});
throw e;
}
},
/**
* 保存检查点
* @param {string} label - 检查点标签
* @param {Object} data - 快照数据
* @returns {string} 检查点ID
*/
checkpoint: async function(label, data) {
return await cpManager.save(label, data);
}
};
// 4. 执行主逻辑
try {
var result = await fn(ctx);
// 5. 成功:更新锁状态
lock.state = 'completed';
lock.result = result;
// 延迟释放锁(给前端时间拉取最终状态)
setTimeout(function() { executionLock.releaseLock(devId); }, 10000);
return {
success: true,
executionId: executionId,
result: result,
reply: result && result.reply ? result.reply : '✅ 操作「' + operationName + '」执行完成。'
};
} catch (e) {
// 6. 失败:自动回滚
lock.state = 'rolling_back';
executionLock.logStep(devId, {
message: '🔄 执行失败,正在自动回滚...',
success: false
});
try {
await cpManager.rollbackTo('cp-1');
executionLock.logStep(devId, {
message: '✅ 已回滚到执行前状态',
success: true
});
} catch (rollbackError) {
executionLock.logStep(devId, {
message: '⚠️ 回滚异常:' + rollbackError.message + ',已通知管理员',
success: false
});
}
lock.state = 'failed';
lock.error = { message: e.message };
setTimeout(function() { executionLock.releaseLock(devId); }, 10000);
return {
success: false,
executionId: executionId,
error: e.message,
reply: '❌ 操作「' + operationName + '」执行失败:' + e.message +
'\n\n系统已自动回滚到执行前状态没有数据被修改。'
};
}
};
module.exports = { AtomicExecutor: AtomicExecutor };

View File

@ -0,0 +1,172 @@
/**
* 检查点系统 · L3 执行原子性保障
*
* 在执行链路的每个关键节点保存快照
* 如果后续步骤失败自动回滚到最近的成功检查点
*
* 检查点包含
* - Notion 数据库的变更前快照
* - GitHub 操作前的 commit SHA
* - 执行到哪一步
*
* 版权国作登字-2026-A-00037559
*/
'use strict';
var executionLock = require('../middleware/execution-lock');
/**
* 检查点管理器
* @param {string} devId - 开发者编号
* @param {string} executionId - 执行ID
*/
function CheckpointManager(devId, executionId) {
this.devId = devId;
this.executionId = executionId;
this.checkpoints = [];
}
/**
* 在执行关键操作前保存检查点
* @param {string} label - 检查点标签
* @param {Object} snapshotData - 快照数据 { type, action, pageId, previousValues, ... }
* @returns {string} 检查点ID
*/
CheckpointManager.prototype.save = async function(label, snapshotData) {
var checkpoint = {
id: 'cp-' + (this.checkpoints.length + 1),
label: label,
data: snapshotData || {},
timestamp: new Date().toISOString()
};
this.checkpoints.push(checkpoint);
executionLock.saveCheckpoint(this.devId, checkpoint);
return checkpoint.id;
};
/**
* 回滚到指定检查点
* 按逆序撤销检查点之后的所有操作
* @param {string} checkpointId - 目标检查点 ID
*/
CheckpointManager.prototype.rollbackTo = async function(checkpointId) {
var targetIndex = -1;
for (var i = 0; i < this.checkpoints.length; i++) {
if (this.checkpoints[i].id === checkpointId) {
targetIndex = i;
break;
}
}
if (targetIndex === -1) {
throw new Error('检查点 ' + checkpointId + ' 不存在');
}
// 从最后一个检查点开始逆序回滚
var toRollback = this.checkpoints.slice(targetIndex + 1).reverse();
for (var j = 0; j < toRollback.length; j++) {
var cp = toRollback[j];
try {
await this.rollbackCheckpoint(cp);
} catch (e) {
console.error('[Checkpoint] 回滚 ' + cp.id + ' 失败:', e.message);
await this.escalateToAdmin(cp, e);
}
}
// 截断检查点列表
this.checkpoints = this.checkpoints.slice(0, targetIndex + 1);
};
/**
* 回滚单个检查点
* @param {Object} checkpoint
*/
CheckpointManager.prototype.rollbackCheckpoint = async function(checkpoint) {
var data = checkpoint.data;
if (!data || !data.type) return;
var notionService, githubService;
switch (data.type) {
case 'notion_write':
notionService = require('./notion');
if (data.action === 'create' && data.pageId && notionService.notion) {
// 新建的页面 → 归档
await notionService.notion.pages.update({
page_id: data.pageId,
archived: true
});
} else if (data.action === 'update' && data.pageId && data.previousValues && notionService.notion) {
// 更新的页面 → 恢复原值
await notionService.notion.pages.update({
page_id: data.pageId,
properties: data.previousValues
});
}
break;
case 'github_workflow':
// Workflow 无法直接回滚,记录需要人工处理
if (data.revertWorkflow) {
githubService = require('./github');
await githubService.triggerWorkflow(data.revertWorkflow, {
original_run_id: String(data.runId || ''),
reason: 'auto_rollback'
});
}
break;
case 'github_file':
githubService = require('./github');
if (data.previousSha) {
// 文件存在之前的版本 → 恢复
// 注意:这需要 GitHub API 的 update file 能力
console.warn('[Checkpoint] GitHub 文件回滚需要人工介入: ' + data.path);
}
break;
case 'initial':
// 初始检查点,无需回滚操作
break;
default:
console.warn('[Checkpoint] 未知检查点类型: ' + data.type);
}
};
/**
* 回滚失败时上报管理员
* @param {Object} checkpoint
* @param {Error} error
*/
CheckpointManager.prototype.escalateToAdmin = async function(checkpoint, error) {
try {
var notionService = require('./notion');
var dbConfig = require('../config/databases');
if (dbConfig.maintenanceLog && notionService.notion) {
await notionService.writeToDB(dbConfig.maintenanceLog, {
'标题': {
title: [{ text: { content: '[紧急] 自动回滚失败 · ' + this.executionId } }]
},
'类型': { select: { name: '系统异常' } },
'操作者': {
rich_text: [{
text: {
content: '检查点 ' + checkpoint.id + ' (' + checkpoint.label +
') 回滚失败: ' + error.message + '。需要冰朔人工介入。'
}
}]
}
});
}
} catch (e) {
console.error('[Checkpoint] 上报管理员失败:', e.message);
}
};
module.exports = { CheckpointManager: CheckpointManager };

View File

@ -0,0 +1,100 @@
/**
* 执行看门狗 · 超时保护 + 死锁防护
*
* 定期检查执行锁是否超时
* 如果一个操作执行超过 MAX_EXECUTION_TIME标记为超时并释放锁
* 这是防止死锁的兜底机制
*
* 版权国作登字-2026-A-00037559
*/
'use strict';
var executionLock = require('../middleware/execution-lock');
// 最大执行时间15 分钟
var MAX_EXECUTION_TIME = 15 * 60 * 1000;
// 警告时间10 分钟
var WARN_TIME = 10 * 60 * 1000;
// 检查间隔30 秒
var CHECK_INTERVAL = 30 * 1000;
var watchdogTimer = null;
/**
* 启动看门狗
* 定期扫描活跃锁表检测超时操作
*/
function startWatchdog() {
if (watchdogTimer) return;
watchdogTimer = setInterval(function() {
var activeLocks = executionLock.getActiveLocks();
var now = Date.now();
for (var entry of activeLocks) {
var devId = entry[0];
var lock = entry[1];
if (lock.state !== 'running') continue;
var elapsed = now - lock.startTime;
// 超时:强制释放
if (elapsed > MAX_EXECUTION_TIME) {
console.error(
'[WATCHDOG] 执行超时: ' + lock.executionId +
' (' + lock.operation + ') - ' + Math.round(elapsed / 1000) + 's'
);
lock.state = 'timeout';
lock.error = {
message: '执行超时(超过 ' + Math.round(MAX_EXECUTION_TIME / 60000) + ' 分钟)'
};
executionLock.logStep(devId, {
message: '⏰ 执行超时,看门狗已强制释放锁',
success: false
});
// 延迟释放(给前端时间获取超时状态)
(function(id) {
setTimeout(function() { executionLock.releaseLock(id); }, 30000);
})(devId);
}
// 警告
else if (elapsed > WARN_TIME && !lock.warned) {
console.warn(
'[WATCHDOG] 执行时间较长: ' + lock.executionId +
' (' + lock.operation + ') - ' + Math.round(elapsed / 1000) + 's'
);
lock.warned = true;
}
}
}, CHECK_INTERVAL);
// 不阻止 Node.js 进程退出
if (watchdogTimer.unref) {
watchdogTimer.unref();
}
console.log('[WATCHDOG] 执行看门狗已启动 · 检查间隔 ' + (CHECK_INTERVAL / 1000) + 's · 超时阈值 ' + (MAX_EXECUTION_TIME / 60000) + 'min');
}
/**
* 停止看门狗
*/
function stopWatchdog() {
if (watchdogTimer) {
clearInterval(watchdogTimer);
watchdogTimer = null;
}
}
module.exports = {
startWatchdog: startWatchdog,
stopWatchdog: stopWatchdog,
MAX_EXECUTION_TIME: MAX_EXECUTION_TIME
};

View File

@ -0,0 +1,125 @@
/**
* 执行保护样式 · L1 前端隔离
*
* 注意没有 .cancel-button 这不是遗漏是设计
* 版权国作登字-2026-A-00037559
*/
/* 执行中状态 · 输入框锁定 */
.chat-input.execution-locked,
#chat-input.execution-locked {
opacity: 0.5;
pointer-events: none;
background: #2a2a3a;
border-color: #444;
}
.chat-input.execution-locked::placeholder,
#chat-input.execution-locked::placeholder {
color: #888;
}
/* 执行状态条 */
.execution-status-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-radius: 8px;
margin: 8px 0;
font-size: 14px;
animation: pulse-gentle 2s infinite;
}
.execution-status-bar .exec-icon {
flex-shrink: 0;
font-size: 1.1em;
}
.execution-status-bar .exec-text {
flex: 1;
}
.execution-status-bar.blue {
background: rgba(35, 131, 226, 0.08);
color: #64c8ff;
}
.execution-status-bar.green {
background: rgba(15, 123, 108, 0.08);
color: #0F7B6C;
animation: none;
}
.execution-status-bar.red {
background: rgba(212, 76, 71, 0.08);
color: #D44C47;
animation: none;
}
/* 执行进度条 */
.execution-progress {
width: 100%;
height: 3px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
margin-top: 4px;
overflow: hidden;
}
.execution-progress-fill {
height: 100%;
background: #64c8ff;
border-radius: 2px;
transition: width 0.5s ease;
}
.execution-status-bar.green .execution-progress-fill {
background: #0F7B6C;
}
.execution-status-bar.red .execution-progress-fill {
background: #D44C47;
}
/* 执行日志容器 */
.execution-log-container {
max-height: 200px;
overflow-y: auto;
padding: 4px 8px;
margin: 4px 0 12px;
font-size: 13px;
}
/* 执行日志条目 */
.execution-log-entry {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 3px 0;
color: #b0b0c0;
}
.execution-log-entry .log-icon {
flex-shrink: 0;
font-size: 0.9em;
}
.execution-log-entry .log-msg {
flex: 1;
}
.execution-log-entry .log-time {
flex-shrink: 0;
color: #666;
font-size: 12px;
}
/* 温和脉冲动画 */
@keyframes pulse-gentle {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* 注意:没有 .cancel-button 类。这不是遗漏。 */

247
docs/js/execution-guard.js Normal file
View File

@ -0,0 +1,247 @@
/**
* 执行保护 · 前端隔离层 (L1)
*
* 当开发者发出一条会触发系统操作的指令后
* 1. 输入框立即禁用不能发新指令
* 2. 取消/撤回按钮不存在从未渲染过不是隐藏
* 3. 界面进入执行回放模式
* 4. 执行完成后才解锁输入框
*
* 版权国作登字-2026-A-00037559
*/
/* global HOLOLAKE_ENV */
(function(window) {
'use strict';
var API_BASE = (typeof HOLOLAKE_ENV !== 'undefined' && HOLOLAKE_ENV === 'production')
? 'https://guanghulab.com/api' : '';
/**
* ExecutionGuard 构造函数
* @param {Object} chatInterface - 聊天界面接口对象
*/
function ExecutionGuard(chatInterface) {
this.chat = chatInterface || {};
this.locked = false;
this.executionId = null;
this._pollTimer = null;
this._logIndex = 0;
}
/**
* 进入执行保护状态
*/
ExecutionGuard.prototype.lockExecution = function(executionId) {
this.locked = true;
this.executionId = executionId;
this._logIndex = 0;
// 1. 禁用输入框
var input = this.chat.inputField || document.getElementById('chat-input');
var sendBtn = this.chat.sendButton || document.getElementById('send-btn');
if (input) {
input.disabled = true;
input.placeholder = '⏳ 系统执行中…请等待完成';
input.classList.add('execution-locked');
}
if (sendBtn) {
sendBtn.disabled = true;
}
// 2. 显示执行状态条
this._showStatusBar('🔄', '指令已接收,系统正在执行…', 'blue');
// 3. 开始轮询执行状态
this._pollExecutionStatus(executionId);
};
/**
* 轮询执行状态
*/
ExecutionGuard.prototype._pollExecutionStatus = function(executionId) {
var self = this;
var poll = function() {
if (!self.locked) return;
if (!API_BASE) {
// 无后端时模拟完成
setTimeout(function() { self.unlockExecution({ reply: '(本地模式:执行模拟完成)' }); }, 2000);
return;
}
var ctrl = new AbortController();
var tid = setTimeout(function() { ctrl.abort(); }, 10000);
fetch(API_BASE + '/execution/' + executionId + '/status?after=' + self._logIndex, {
signal: ctrl.signal
})
.then(function(res) { clearTimeout(tid); return res.json(); })
.then(function(status) {
// 更新进度
self._updateProgressBar(status.progress || 0);
// 展示新日志
if (status.newLogs && status.newLogs.length > 0) {
for (var i = 0; i < status.newLogs.length; i++) {
self._appendLogEntry(status.newLogs[i]);
}
self._logIndex += status.newLogs.length;
}
if (status.state === 'completed') {
self.unlockExecution(status.result || {});
} else if (status.state === 'failed' || status.state === 'timeout') {
self.handleExecutionFailure(status.error || { message: '执行失败' });
} else {
self._pollTimer = setTimeout(poll, 2000);
}
})
.catch(function() {
clearTimeout(tid);
// 网络错误不中断执行,继续轮询
self._pollTimer = setTimeout(poll, 5000);
});
};
poll();
};
/**
* 执行完成解锁界面
*/
ExecutionGuard.prototype.unlockExecution = function(result) {
this.locked = false;
this.executionId = null;
if (this._pollTimer) { clearTimeout(this._pollTimer); this._pollTimer = null; }
// 恢复输入框
var input = this.chat.inputField || document.getElementById('chat-input');
var sendBtn = this.chat.sendButton || document.getElementById('send-btn');
if (input) {
input.disabled = false;
input.placeholder = '和铸渊说话…';
input.classList.remove('execution-locked');
}
if (sendBtn) {
sendBtn.disabled = false;
}
// 更新状态条
this._showStatusBar('✅', '执行完成', 'green');
var self = this;
setTimeout(function() { self._hideStatusBar(); }, 5000);
// 展示结果
if (result && result.reply && this.chat.appendMessage) {
this.chat.appendMessage({ role: 'assistant', content: result.reply });
}
};
/**
* 执行失败处理
*/
ExecutionGuard.prototype.handleExecutionFailure = function(error) {
this.locked = false;
this.executionId = null;
if (this._pollTimer) { clearTimeout(this._pollTimer); this._pollTimer = null; }
var input = this.chat.inputField || document.getElementById('chat-input');
var sendBtn = this.chat.sendButton || document.getElementById('send-btn');
if (input) {
input.disabled = false;
input.placeholder = '和铸渊说话…';
input.classList.remove('execution-locked');
}
if (sendBtn) {
sendBtn.disabled = false;
}
this._showStatusBar('❌', '执行失败 · 系统已自动回滚到执行前状态', 'red');
var self = this;
setTimeout(function() { self._hideStatusBar(); }, 10000);
var msg = error && error.message ? error.message : '未知错误';
if (this.chat.appendMessage) {
this.chat.appendMessage({
role: 'assistant',
content: '❌ 操作执行失败:' + msg +
'\n\n系统已自动回滚到执行前的状态。没有任何数据被修改。\n' +
'你可以重新描述你想做的事,或者说「发生了什么」来查看详细日志。'
});
}
};
/**
* 拦截浏览器关闭/刷新
*/
ExecutionGuard.prototype.setupBrowserGuard = function() {
var self = this;
window.addEventListener('beforeunload', function(e) {
if (self.locked) {
e.preventDefault();
e.returnValue = '⚠️ 系统正在执行操作,关闭页面不会中断执行,但你将无法看到执行结果。';
return e.returnValue;
}
});
};
// ====== 内部 UI 辅助方法 ======
ExecutionGuard.prototype._showStatusBar = function(icon, text, color) {
var bar = document.getElementById('execution-status-bar');
if (!bar) {
bar = document.createElement('div');
bar.id = 'execution-status-bar';
bar.className = 'execution-status-bar';
var chatArea = document.querySelector('.chat-messages') || document.querySelector('.chat-container') || document.body;
chatArea.appendChild(bar);
}
bar.className = 'execution-status-bar ' + (color || 'blue');
bar.innerHTML = '<span class="exec-icon">' + icon + '</span> <span class="exec-text">' + text + '</span>' +
'<div class="execution-progress"><div class="execution-progress-fill" id="exec-progress-fill" style="width:5%"></div></div>';
bar.style.display = 'flex';
// 注意:没有取消按钮。这不是遗漏,是设计。
};
ExecutionGuard.prototype._hideStatusBar = function() {
var bar = document.getElementById('execution-status-bar');
if (bar) bar.style.display = 'none';
};
ExecutionGuard.prototype._updateProgressBar = function(percent) {
var fill = document.getElementById('exec-progress-fill');
if (fill) fill.style.width = Math.max(5, Math.min(100, percent)) + '%';
};
ExecutionGuard.prototype._appendLogEntry = function(log) {
var container = document.getElementById('execution-log-container');
if (!container) {
container = document.createElement('div');
container.id = 'execution-log-container';
container.className = 'execution-log-container';
var bar = document.getElementById('execution-status-bar');
if (bar && bar.parentNode) {
bar.parentNode.insertBefore(container, bar.nextSibling);
} else {
(document.querySelector('.chat-messages') || document.body).appendChild(container);
}
}
var entry = document.createElement('div');
entry.className = 'execution-log-entry';
var icon = log.success ? '✅' : '⏳';
var time = log.timestamp ? new Date(log.timestamp).toLocaleTimeString('zh-CN') : '';
entry.innerHTML = '<span class="log-icon">' + icon + '</span>' +
'<span class="log-msg">' + (log.message || '') + '</span>' +
'<span class="log-time">' + time + '</span>';
container.appendChild(entry);
};
// 导出到全局
window.ExecutionGuard = ExecutionGuard;
})(window);