guanghulab/docs/js/execution-guard.js
2026-05-10 13:12:44 +08:00

248 lines
8.2 KiB
JavaScript

/**
* 执行保护 · 前端隔离层 (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);