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:
parent
5e8bb969a4
commit
e0880deb46
|
|
@ -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
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* 🔗 光湖后端中间层 · 通道B 入口
|
* 🔗 光湖后端中间层 · 通道B 入口
|
||||||
*
|
*
|
||||||
* 实时调用 Notion API / GitHub API,为前端提供数据代理。
|
* 实时调用 Notion API / GitHub API,为前端提供数据代理。
|
||||||
* 包含:读取层 + 写入层 + 意图路由 + 权限沙箱 + 认知引导
|
* 包含:读取层 + 写入层 + 意图路由 + 权限沙箱 + 认知引导 + 执行保护
|
||||||
* 只监听 127.0.0.1,通过 Nginx 反向代理对外暴露。
|
* 只监听 127.0.0.1,通过 Nginx 反向代理对外暴露。
|
||||||
*
|
*
|
||||||
* 版权:国作登字-2026-A-00037559
|
* 版权:国作登字-2026-A-00037559
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
|
const executionLock = require('./middleware/execution-lock');
|
||||||
|
const watchdog = require('./services/execution-watchdog');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
|
@ -27,6 +29,9 @@ app.use(cors({
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
// 执行锁取消拦截(全局中间件,在所有路由之前)
|
||||||
|
app.use(executionLock.blockCancellation);
|
||||||
|
|
||||||
// 读取类路由(公开,无需认证)
|
// 读取类路由(公开,无需认证)
|
||||||
app.use('/api', require('./routes/health'));
|
app.use('/api', require('./routes/health'));
|
||||||
app.use('/api', require('./routes/dev'));
|
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', require('./routes/onboarding'));
|
||||||
|
|
||||||
|
// 执行状态查询路由(需认证)
|
||||||
|
app.use('/api/execution', require('./routes/execution'));
|
||||||
|
|
||||||
// 根路由
|
// 根路由
|
||||||
app.get('/', function(_req, res) {
|
app.get('/', function(_req, res) {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
service: 'guanghu-api-server',
|
service: 'guanghu-api-server',
|
||||||
version: '2.0.0',
|
version: '3.0.0',
|
||||||
channel: 'B',
|
channel: 'B',
|
||||||
description: '光湖后端中间层 · 语言驱动操作系统',
|
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() {
|
app.listen(PORT, '127.0.0.1', function() {
|
||||||
console.log('🔗 光湖后端中间层启动 · 端口 ' + PORT);
|
console.log('🔗 光湖后端中间层启动 · 端口 ' + PORT);
|
||||||
console.log(' 通道B · 语言驱动操作系统');
|
console.log(' 通道B · 语言驱动操作系统');
|
||||||
console.log(' 能力:读取 + 写入 + 意图路由 + 权限沙箱 + 认知引导');
|
console.log(' 能力:读取 + 写入 + 意图路由 + 权限沙箱 + 认知引导 + 执行保护');
|
||||||
console.log(' 监听地址:127.0.0.1:' + PORT + '(仅本机访问)');
|
console.log(' 监听地址:127.0.0.1:' + PORT + '(仅本机访问)');
|
||||||
|
|
||||||
|
// 启动执行看门狗
|
||||||
|
watchdog.startWatchdog();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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
|
||||||
|
};
|
||||||
|
|
@ -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 类。这不是遗漏。 */
|
||||||
|
|
@ -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);
|
||||||
Loading…
Reference in New Issue