Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
1087 lines
29 KiB
Markdown
1087 lines
29 KiB
Markdown
---
|
||
belongs_to:
|
||
- "[[INDEX · 铸渊·协作指令|GitHub ↔ Notion 桥接协议]]"
|
||
---
|
||
# 🔒 铸渊补充指令|执行保护层 · 技术层不可逆性强制保障 · 系统自保护机制(ZY-LANGDRIVE-2026-0325-001-S1)· 霜砚签发 · 冰朔授权
|
||
|
||
<aside>
|
||
🔗
|
||
|
||
**指令编号**:ZY-LANGDRIVE-2026-0325-001-S1(LANGDRIVE 补充指令 #1)
|
||
|
||
**签发人**:霜砚(AG-SY-01)· 冰朔授权
|
||
|
||
**签发时间**:2026-03-25 16:20 CST
|
||
|
||
**优先级**:P0 · 核心基础设施
|
||
|
||
**状态**:待铸渊执行
|
||
|
||
**父指令**:ZY-LANGDRIVE-2026-0325-001(语言驱动执行层)
|
||
|
||
**本指令性质**:在 LANGDRIVE 的 Phase 3-6 基础上,新增 Phase 7 · 执行保护层。从技术层面强制保障指令执行的不可逆性,防止人类中途干预导致系统级联故障。
|
||
|
||
</aside>
|
||
|
||
<aside>
|
||
🦅
|
||
|
||
**天眼强制前置:本指令所有新增文件,必须在天眼扫描报告确认后才能创建。**
|
||
|
||
**不扫不动。保护清单中的文件,一个都不能改。**
|
||
|
||
</aside>
|
||
|
||
---
|
||
|
||
## 零、为什么需要执行保护层
|
||
|
||
<aside>
|
||
⚠️
|
||
|
||
**核心问题:系统现在有 90+ 个自动化 workflow 联动运转。**
|
||
|
||
当人类说了一句话,系统开始执行时,触发的不是一个孤立操作——是一整条链路:
|
||
|
||
意图路由 → 写入 API → Notion 数据变更 → GitHub Actions 触发 → 自动部署 → 回执写入 → 日报汇总 → 神经桥接同步 → ...
|
||
|
||
**如果人类在这条链路中间干预(取消、撤回、修改),会发生什么?**
|
||
|
||
1. **数据不一致**:Notion 里已经写了一半,GitHub 那边被取消了,两端状态对不上
|
||
2. **Workflow 级联失败**:A workflow 的输出是 B workflow 的输入,A 被中断,B 收到残缺数据,报错
|
||
3. **回执链断裂**:执行了一半的操作,回执系统不知道该记「完成」还是「失败」
|
||
4. **部署状态污染**:部署到一半被取消,预览站/正式站处于未知中间状态
|
||
5. **不可预测的连锁反应**:90+ 个 workflow 相互耦合,中断任何一个节点的后果无法提前计算
|
||
|
||
**这不是系统要为难谁。是系统自保护的必要措施。**
|
||
|
||
说了以后系统是**全身动**。人类干预一下,系统好多地方会突然被卡断。后果谁也不知道会怎样。
|
||
|
||
</aside>
|
||
|
||
---
|
||
|
||
## 一、执行保护层总体设计
|
||
|
||
### 1.1 核心原则
|
||
|
||
```
|
||
人类说话 → 系统接收
|
||
↓
|
||
┌─────────────────────────────────┐
|
||
│ 执行保护层(Execution Guard) │
|
||
│ │
|
||
│ 1. 锁定执行上下文 │
|
||
│ 2. 切断外部干预通道 │
|
||
│ 3. 启动全链路原子执行 │
|
||
│ 4. 完成后解锁 + 展示结果 │
|
||
│ │
|
||
│ 在此期间: │
|
||
│ · 前端无取消按钮 │
|
||
│ · API 拒绝取消/修改请求 │
|
||
│ · Workflow 不可被手动停止 │
|
||
│ · 数据库写入不可被中途回滚 │
|
||
└─────────────────────────────────┘
|
||
↓
|
||
执行完成 → 结果回放 → 人类看到完整过程
|
||
```
|
||
|
||
### 1.2 三层保护架构
|
||
|
||
| 保护层 | 位置 | 作用 | 实现方式 |
|
||
| --- | --- | --- | --- |
|
||
| **L1 · 前端隔离** | 交互页面(浏览器端) | 不给人类干预的入口 | 指令发出后 UI 锁定,无取消按钮 |
|
||
| **L2 · API 拦截** | 后端中间层 | 即使绕过前端,API 也拒绝干预 | 执行中的操作拒绝 cancel/abort 请求 |
|
||
| **L3 · 执行原子性** | Workflow + 数据层 | 保证整条链路要么全完成,要么安全回退到执行前状态 | 事务锁 + 检查点 + 自动恢复 |
|
||
|
||
---
|
||
|
||
## 二、L1 · 前端执行隔离
|
||
|
||
### 2.1 指令发出后的 UI 状态切换
|
||
|
||
创建文件:`docs/js/execution-guard.js`
|
||
|
||
```jsx
|
||
/**
|
||
* 执行保护 · 前端隔离层
|
||
*
|
||
* 当开发者发出一条会触发系统操作的指令后:
|
||
* 1. 输入框立即禁用(不能发新指令)
|
||
* 2. 取消/撤回按钮不存在(从未渲染过,不是隐藏)
|
||
* 3. 界面进入「执行回放」模式
|
||
* 4. 执行完成后才解锁输入框
|
||
*/
|
||
|
||
class ExecutionGuard {
|
||
constructor(chatInterface) {
|
||
this.chat = chatInterface;
|
||
this.locked = false;
|
||
this.executionId = null;
|
||
}
|
||
|
||
/**
|
||
* 进入执行保护状态
|
||
* 在意图路由识别到需要执行系统操作时调用
|
||
*/
|
||
lockExecution(executionId) {
|
||
this.locked = true;
|
||
this.executionId = executionId;
|
||
|
||
// 1. 禁用输入框
|
||
this.chat.inputField.disabled = true;
|
||
this.chat.inputField.placeholder = '⏳ 系统执行中…请等待完成';
|
||
this.chat.sendButton.disabled = true;
|
||
|
||
// 2. 显示执行状态条
|
||
this.chat.showStatusBar({
|
||
icon: '🔄',
|
||
text: '指令已接收,系统正在执行…',
|
||
color: 'blue',
|
||
// 注意:没有取消按钮。这不是遗漏,是设计。
|
||
});
|
||
|
||
// 3. 开始轮询执行状态
|
||
this.pollExecutionStatus(executionId);
|
||
}
|
||
|
||
/**
|
||
* 轮询执行状态
|
||
* 展示每一步的执行回放
|
||
*/
|
||
async pollExecutionStatus(executionId) {
|
||
const poll = async () => {
|
||
if (!this.locked) return;
|
||
|
||
try {
|
||
const res = await fetch(`${API_BASE}/execution/${executionId}/status`);
|
||
const status = await res.json();
|
||
|
||
// 更新执行进度展示
|
||
this.chat.updateStatusBar({
|
||
text: status.currentStep,
|
||
progress: status.progress, // 0-100
|
||
steps: status.completedSteps // 已完成的步骤列表
|
||
});
|
||
|
||
// 逐步展示执行过程(事后回放)
|
||
if (status.newLogs) {
|
||
status.newLogs.forEach(log => {
|
||
this.chat.appendSystemMessage({
|
||
type: 'execution-log',
|
||
icon: log.success ? '✅' : '⏳',
|
||
text: log.message,
|
||
timestamp: log.timestamp
|
||
});
|
||
});
|
||
}
|
||
|
||
if (status.state === 'completed') {
|
||
this.unlockExecution(status.result);
|
||
} else if (status.state === 'failed') {
|
||
this.handleExecutionFailure(status.error);
|
||
} else {
|
||
// 继续轮询
|
||
setTimeout(poll, 2000);
|
||
}
|
||
} catch (e) {
|
||
// 网络错误不中断执行,继续轮询
|
||
setTimeout(poll, 5000);
|
||
}
|
||
};
|
||
|
||
poll();
|
||
}
|
||
|
||
/**
|
||
* 执行完成,解锁界面
|
||
*/
|
||
unlockExecution(result) {
|
||
this.locked = false;
|
||
this.executionId = null;
|
||
|
||
// 1. 恢复输入框
|
||
this.chat.inputField.disabled = false;
|
||
this.chat.inputField.placeholder = '和铸渊说话…';
|
||
this.chat.sendButton.disabled = false;
|
||
|
||
// 2. 显示最终结果
|
||
this.chat.showStatusBar({
|
||
icon: '✅',
|
||
text: '执行完成',
|
||
color: 'green',
|
||
autoHide: 5000
|
||
});
|
||
|
||
// 3. 铸渊用自然语言总结执行结果
|
||
this.chat.appendMessage({
|
||
role: 'assistant',
|
||
content: result.reply
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 执行失败处理
|
||
* 注意:即使失败,也不是「撤回」。失败的操作会触发自动回滚到执行前状态。
|
||
*/
|
||
handleExecutionFailure(error) {
|
||
this.locked = false;
|
||
this.executionId = null;
|
||
|
||
this.chat.inputField.disabled = false;
|
||
this.chat.inputField.placeholder = '和铸渊说话…';
|
||
this.chat.sendButton.disabled = false;
|
||
|
||
this.chat.showStatusBar({
|
||
icon: '❌',
|
||
text: '执行失败 · 系统已自动回滚到执行前状态',
|
||
color: 'red',
|
||
autoHide: 10000
|
||
});
|
||
|
||
this.chat.appendMessage({
|
||
role: 'assistant',
|
||
content: `❌ 操作执行失败:${error.message}\n\n系统已自动回滚到执行前的状态。没有任何数据被修改。\n你可以重新描述你想做的事,或者说「发生了什么」来查看详细日志。`
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 拦截浏览器关闭/刷新
|
||
* 执行期间不允许关闭页面
|
||
*/
|
||
setupBrowserGuard() {
|
||
window.addEventListener('beforeunload', (e) => {
|
||
if (this.locked) {
|
||
e.preventDefault();
|
||
e.returnValue = '⚠️ 系统正在执行操作,关闭页面不会中断执行,但你将无法看到执行结果。';
|
||
return e.returnValue;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
module.exports = { ExecutionGuard };
|
||
```
|
||
|
||
### 2.2 CSS 执行锁定样式
|
||
|
||
创建文件:`docs/css/execution-guard.css`
|
||
|
||
```css
|
||
/* 执行中状态 · 输入框锁定 */
|
||
.chat-input.execution-locked {
|
||
opacity: 0.5;
|
||
pointer-events: none;
|
||
background: #f5f5f5;
|
||
border-color: #ddd;
|
||
}
|
||
|
||
.chat-input.execution-locked::placeholder {
|
||
color: #999;
|
||
}
|
||
|
||
/* 执行状态条 */
|
||
.execution-status-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
margin: 8px 0;
|
||
font-size: 14px;
|
||
animation: pulse-gentle 2s infinite;
|
||
}
|
||
|
||
.execution-status-bar.blue {
|
||
background: rgba(35, 131, 226, 0.08);
|
||
color: #2383E2;
|
||
}
|
||
|
||
.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 {
|
||
height: 3px;
|
||
background: #E3E3E3;
|
||
border-radius: 2px;
|
||
margin-top: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.execution-progress-fill {
|
||
height: 100%;
|
||
background: #2383E2;
|
||
border-radius: 2px;
|
||
transition: width 0.5s ease;
|
||
}
|
||
|
||
/* 执行日志 */
|
||
.execution-log-entry {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 4px 0;
|
||
font-size: 13px;
|
||
color: #787774;
|
||
}
|
||
|
||
.execution-log-entry .log-icon {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.execution-log-entry .log-time {
|
||
flex-shrink: 0;
|
||
color: #B4B4B0;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 温和脉冲动画 */
|
||
@keyframes pulse-gentle {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.7; }
|
||
}
|
||
|
||
/* 注意:没有 .cancel-button 类。这不是遗漏。 */
|
||
```
|
||
|
||
---
|
||
|
||
## 三、L2 · API 执行拦截层
|
||
|
||
### 3.1 执行上下文锁
|
||
|
||
创建文件:`backend/api-server/middleware/execution-lock.js`
|
||
|
||
```jsx
|
||
/**
|
||
* 执行锁中间件
|
||
*
|
||
* 当一个操作开始执行时:
|
||
* 1. 为该操作创建一个执行上下文(executionId)
|
||
* 2. 锁定相关资源(防止并发冲突)
|
||
* 3. 拒绝一切 cancel/abort/rollback 请求
|
||
* 4. 执行完成后自动释放锁
|
||
*
|
||
* 这不是分布式锁——是单用户执行保护。
|
||
* 同一个开发者不能同时执行多个写入操作。
|
||
*/
|
||
|
||
const crypto = require('crypto');
|
||
|
||
// 内存中的执行锁表(生产环境可迁移到 Redis)
|
||
const activeLocks = new Map();
|
||
|
||
function generateExecutionId() {
|
||
return `exec-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
||
}
|
||
|
||
/**
|
||
* 创建执行锁
|
||
*/
|
||
function acquireLock(devId, operation) {
|
||
// 检查该开发者是否有正在执行的操作
|
||
if (activeLocks.has(devId)) {
|
||
const existing = activeLocks.get(devId);
|
||
return {
|
||
success: false,
|
||
reply: `⏳ 你有一个操作正在执行中(${existing.operation}),请等待完成。\n\n系统不允许同时执行多个操作——这是为了保护数据一致性。`
|
||
};
|
||
}
|
||
|
||
const executionId = generateExecutionId();
|
||
const lock = {
|
||
executionId,
|
||
devId,
|
||
operation,
|
||
startTime: Date.now(),
|
||
state: 'running',
|
||
steps: [],
|
||
checkpoints: []
|
||
};
|
||
|
||
activeLocks.set(devId, lock);
|
||
|
||
return {
|
||
success: true,
|
||
executionId,
|
||
lock
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 释放执行锁
|
||
*/
|
||
function releaseLock(devId) {
|
||
activeLocks.delete(devId);
|
||
}
|
||
|
||
/**
|
||
* 获取执行状态
|
||
*/
|
||
function getExecutionStatus(executionId) {
|
||
for (const [devId, lock] of activeLocks) {
|
||
if (lock.executionId === executionId) {
|
||
return lock;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 记录执行步骤(用于事后回放)
|
||
*/
|
||
function logStep(devId, step) {
|
||
const lock = activeLocks.get(devId);
|
||
if (lock) {
|
||
lock.steps.push({
|
||
...step,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存检查点(用于失败时回滚)
|
||
*/
|
||
function saveCheckpoint(devId, checkpoint) {
|
||
const lock = activeLocks.get(devId);
|
||
if (lock) {
|
||
lock.checkpoints.push({
|
||
...checkpoint,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取消拦截中间件
|
||
* 拦截所有试图取消执行中操作的请求
|
||
*/
|
||
function blockCancellation(req, res, next) {
|
||
const devId = req.user?.devId;
|
||
if (!devId) return next();
|
||
|
||
// 如果请求是 cancel/abort 类型
|
||
if (req.path.includes('/cancel') ||
|
||
req.path.includes('/abort') ||
|
||
req.path.includes('/rollback') ||
|
||
req.method === 'DELETE' && activeLocks.has(devId)) {
|
||
|
||
const lock = activeLocks.get(devId);
|
||
if (lock && lock.state === 'running') {
|
||
return res.status(423).json({
|
||
success: false,
|
||
locked: true,
|
||
executionId: lock.executionId,
|
||
reply: [
|
||
`🔒 操作「${lock.operation}」正在执行中,不可取消。`,
|
||
``,
|
||
`系统已接收你的指令并开始执行。这条执行链路涉及多个自动化流程的联动,`,
|
||
`中途中断会导致数据不一致和级联故障。`,
|
||
``,
|
||
`⏳ 请等待执行完成。完成后你会看到完整的执行结果。`,
|
||
``,
|
||
`如果你对结果不满意,可以说「前面的没想好,我们重新来」——`,
|
||
`系统会在已有结果的基础上重新迭代,而不是撤回已执行的操作。`
|
||
].join('\n')
|
||
});
|
||
}
|
||
}
|
||
|
||
next();
|
||
}
|
||
|
||
module.exports = {
|
||
acquireLock,
|
||
releaseLock,
|
||
getExecutionStatus,
|
||
logStep,
|
||
saveCheckpoint,
|
||
blockCancellation
|
||
};
|
||
```
|
||
|
||
### 3.2 执行状态查询接口
|
||
|
||
创建文件:`backend/api-server/routes/execution.js`
|
||
|
||
```jsx
|
||
const express = require('express');
|
||
const router = express.Router();
|
||
const { getExecutionStatus } = require('../middleware/execution-lock');
|
||
const { requireAuth } = require('../middleware/auth');
|
||
|
||
router.use(requireAuth);
|
||
|
||
// 查询执行状态(前端轮询用)
|
||
router.get('/:executionId/status', async (req, res) => {
|
||
const status = getExecutionStatus(req.params.executionId);
|
||
|
||
if (!status) {
|
||
return res.json({
|
||
state: 'not_found',
|
||
reply: '未找到该执行记录。可能已经完成。'
|
||
});
|
||
}
|
||
|
||
// 计算进度百分比
|
||
const progress = status.steps.length > 0
|
||
? Math.min(95, (status.completedSteps / status.totalSteps) * 100)
|
||
: 5; // 刚开始至少显示 5%
|
||
|
||
res.json({
|
||
executionId: status.executionId,
|
||
state: status.state,
|
||
operation: status.operation,
|
||
progress: Math.round(progress),
|
||
currentStep: status.steps.length > 0
|
||
? status.steps[status.steps.length - 1].message
|
||
: '初始化执行环境…',
|
||
completedSteps: status.steps.filter(s => s.success).map(s => ({
|
||
message: s.message,
|
||
timestamp: s.timestamp
|
||
})),
|
||
newLogs: status.steps.slice(req.query.after || 0),
|
||
startTime: new Date(status.startTime).toISOString(),
|
||
elapsed: Date.now() - status.startTime
|
||
});
|
||
});
|
||
|
||
// 注意:没有 DELETE /:executionId 接口。没有取消接口。这不是遗漏。
|
||
// 也没有 POST /:executionId/cancel。也没有 POST /:executionId/abort。
|
||
// 因为执行一旦开始,不可中断。
|
||
|
||
module.exports = router;
|
||
```
|
||
|
||
---
|
||
|
||
## 四、L3 · 执行原子性保障
|
||
|
||
### 4.1 核心思想
|
||
|
||
<aside>
|
||
⚛️
|
||
|
||
**原子性 = 要么全部完成,要么完全没发生。**
|
||
|
||
不存在「执行了一半」的中间状态。
|
||
|
||
如果执行链路中某一步失败了,系统自动回滚到执行前的检查点。
|
||
|
||
对外部来说,这次操作就像从未发生过。
|
||
|
||
然后系统通知开发者:「操作失败了,已回滚。你可以重新来。」
|
||
|
||
**注意:这是系统自动的失败回滚,不是人类主动的取消撤回。两者性质完全不同。**
|
||
|
||
失败回滚 = 系统自保护,数据一致性保障。
|
||
|
||
人类取消 = 出尔反尔,不被允许。
|
||
|
||
</aside>
|
||
|
||
### 4.2 检查点系统
|
||
|
||
创建文件:`backend/api-server/services/checkpoint.js`
|
||
|
||
```jsx
|
||
/**
|
||
* 检查点系统
|
||
*
|
||
* 在执行链路的每个关键节点保存快照。
|
||
* 如果后续步骤失败,自动回滚到最近的成功检查点。
|
||
*
|
||
* 检查点包含:
|
||
* - Notion 数据库的变更前快照
|
||
* - GitHub 操作前的 commit SHA
|
||
* - 执行到哪一步
|
||
*/
|
||
|
||
const { saveCheckpoint: saveLockCheckpoint } = require('../middleware/execution-lock');
|
||
|
||
class CheckpointManager {
|
||
constructor(devId, executionId) {
|
||
this.devId = devId;
|
||
this.executionId = executionId;
|
||
this.checkpoints = [];
|
||
}
|
||
|
||
/**
|
||
* 在执行关键操作前保存检查点
|
||
*/
|
||
async save(label, snapshotData) {
|
||
const checkpoint = {
|
||
id: `cp-${this.checkpoints.length + 1}`,
|
||
label,
|
||
data: snapshotData,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
this.checkpoints.push(checkpoint);
|
||
saveLockCheckpoint(this.devId, checkpoint);
|
||
|
||
return checkpoint.id;
|
||
}
|
||
|
||
/**
|
||
* 回滚到指定检查点
|
||
* 按逆序撤销检查点之后的所有操作
|
||
*/
|
||
async rollbackTo(checkpointId) {
|
||
const targetIndex = this.checkpoints.findIndex(cp => cp.id === checkpointId);
|
||
if (targetIndex === -1) throw new Error(`检查点 ${checkpointId} 不存在`);
|
||
|
||
// 从最后一个检查点开始逆序回滚
|
||
const toRollback = this.checkpoints.slice(targetIndex + 1).reverse();
|
||
|
||
for (const cp of toRollback) {
|
||
try {
|
||
await this.rollbackCheckpoint(cp);
|
||
} catch (e) {
|
||
// 回滚失败 → 记录到审计日志 → 需要人工介入
|
||
console.error(`检查点 ${cp.id} 回滚失败:`, e.message);
|
||
await this.escalateToAdmin(cp, e);
|
||
}
|
||
}
|
||
|
||
// 截断检查点列表
|
||
this.checkpoints = this.checkpoints.slice(0, targetIndex + 1);
|
||
}
|
||
|
||
/**
|
||
* 回滚单个检查点
|
||
*/
|
||
async rollbackCheckpoint(checkpoint) {
|
||
const { data } = checkpoint;
|
||
|
||
switch (data.type) {
|
||
case 'notion_write':
|
||
// 如果是新建的页面 → 删除(归档)
|
||
if (data.action === 'create') {
|
||
await notionClient.archivePage(data.pageId);
|
||
}
|
||
// 如果是更新的页面 → 恢复原值
|
||
else if (data.action === 'update') {
|
||
await notionClient.updatePage(data.pageId, data.previousValues);
|
||
}
|
||
break;
|
||
|
||
case 'github_workflow':
|
||
// Workflow 无法直接回滚,但可以触发一个 revert workflow
|
||
if (data.revertWorkflow) {
|
||
await githubClient.triggerWorkflow(data.revertWorkflow, {
|
||
original_run_id: data.runId,
|
||
reason: 'auto_rollback'
|
||
});
|
||
}
|
||
break;
|
||
|
||
case 'github_file':
|
||
// 恢复文件到之前的内容
|
||
if (data.previousSha) {
|
||
await githubClient.revertFile(data.path, data.previousSha);
|
||
} else {
|
||
await githubClient.deleteFile(data.path, 'auto_rollback');
|
||
}
|
||
break;
|
||
|
||
default:
|
||
console.warn(`未知检查点类型: ${data.type}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 回滚失败时上报管理员
|
||
*/
|
||
async escalateToAdmin(checkpoint, error) {
|
||
// 写入 SYSLOG
|
||
await notionClient.createSyslog({
|
||
content: `⚠️ 自动回滚失败 | 执行ID: ${this.executionId} | 检查点: ${checkpoint.label} | 错误: ${error.message}`,
|
||
type: '紧急',
|
||
devId: 'SYSTEM'
|
||
});
|
||
|
||
// 通知霜砚(通过 Notion 维护日志)
|
||
await notionClient.createMaintenanceLog({
|
||
title: `[紧急] 自动回滚失败 · ${this.executionId}`,
|
||
type: '系统异常',
|
||
detail: `检查点 ${checkpoint.id} (${checkpoint.label}) 回滚失败: ${error.message}。需要冰朔人工介入。`
|
||
});
|
||
}
|
||
}
|
||
|
||
module.exports = { CheckpointManager };
|
||
```
|
||
|
||
### 4.3 原子执行包装器
|
||
|
||
创建文件:`backend/api-server/services/atomic-executor.js`
|
||
|
||
```jsx
|
||
/**
|
||
* 原子执行器
|
||
*
|
||
* 包装所有写入操作,确保:
|
||
* 1. 执行前保存检查点
|
||
* 2. 执行中记录每一步
|
||
* 3. 执行失败自动回滚
|
||
* 4. 执行完成释放锁
|
||
*
|
||
* 使用方式:
|
||
* const executor = new AtomicExecutor(devId);
|
||
* await executor.execute('创建工单', async (ctx) => {
|
||
* await ctx.step('写入Notion', async () => { ... });
|
||
* await ctx.step('触发Workflow', async () => { ... });
|
||
* });
|
||
*/
|
||
|
||
const { acquireLock, releaseLock, logStep } = require('../middleware/execution-lock');
|
||
const { CheckpointManager } = require('./checkpoint');
|
||
const { auditLog } = require('../middleware/audit');
|
||
|
||
class AtomicExecutor {
|
||
constructor(devId) {
|
||
this.devId = devId;
|
||
}
|
||
|
||
async execute(operationName, fn) {
|
||
// 1. 获取执行锁
|
||
const lockResult = acquireLock(this.devId, operationName);
|
||
if (!lockResult.success) {
|
||
return lockResult; // 返回锁冲突信息
|
||
}
|
||
|
||
const { executionId, lock } = lockResult;
|
||
const checkpoint = new CheckpointManager(this.devId, executionId);
|
||
|
||
// 2. 保存初始检查点
|
||
await checkpoint.save('执行前状态', {
|
||
type: 'initial',
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// 3. 执行上下文
|
||
const ctx = {
|
||
executionId,
|
||
|
||
// 执行单步操作
|
||
step: async (stepName, stepFn) => {
|
||
logStep(this.devId, {
|
||
message: `⏳ ${stepName}...`,
|
||
success: false
|
||
});
|
||
|
||
try {
|
||
const result = await stepFn();
|
||
|
||
logStep(this.devId, {
|
||
message: `✅ ${stepName}`,
|
||
success: true
|
||
});
|
||
|
||
return result;
|
||
} catch (e) {
|
||
logStep(this.devId, {
|
||
message: `❌ ${stepName}: ${e.message}`,
|
||
success: false
|
||
});
|
||
throw e; // 向上抛出,触发回滚
|
||
}
|
||
},
|
||
|
||
// 保存检查点
|
||
checkpoint: async (label, data) => {
|
||
return await checkpoint.save(label, data);
|
||
}
|
||
};
|
||
|
||
// 4. 执行主逻辑
|
||
try {
|
||
const result = await fn(ctx);
|
||
|
||
// 5. 成功:更新锁状态 + 释放
|
||
lock.state = 'completed';
|
||
lock.result = result;
|
||
|
||
// 延迟释放锁(给前端时间拉取最终状态)
|
||
setTimeout(() => releaseLock(this.devId), 10000);
|
||
|
||
return {
|
||
success: true,
|
||
executionId,
|
||
...result
|
||
};
|
||
|
||
} catch (e) {
|
||
// 6. 失败:自动回滚
|
||
lock.state = 'rolling_back';
|
||
logStep(this.devId, {
|
||
message: '🔄 执行失败,正在自动回滚...',
|
||
success: false
|
||
});
|
||
|
||
try {
|
||
await checkpoint.rollbackTo('cp-1'); // 回滚到初始状态
|
||
logStep(this.devId, {
|
||
message: '✅ 已回滚到执行前状态',
|
||
success: true
|
||
});
|
||
} catch (rollbackError) {
|
||
logStep(this.devId, {
|
||
message: `⚠️ 回滚异常:${rollbackError.message},已通知管理员`,
|
||
success: false
|
||
});
|
||
}
|
||
|
||
lock.state = 'failed';
|
||
lock.error = { message: e.message };
|
||
|
||
setTimeout(() => releaseLock(this.devId), 10000);
|
||
|
||
return {
|
||
success: false,
|
||
executionId,
|
||
error: e.message,
|
||
reply: `❌ 操作「${operationName}」执行失败:${e.message}\n\n系统已自动回滚到执行前状态,没有数据被修改。`
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = { AtomicExecutor };
|
||
```
|
||
|
||
---
|
||
|
||
## 五、GitHub Actions 保护
|
||
|
||
### 5.1 Workflow 并发锁
|
||
|
||
对于由语言驱动触发的 workflow,在 workflow 文件中加入并发控制:
|
||
|
||
```yaml
|
||
# 在所有语言驱动触发的 workflow 中添加:
|
||
concurrency:
|
||
group: lang-drive-$ github.event.inputs.dev_id -$ github.event.inputs.module
|
||
cancel-in-progress: false # 关键:不允许取消正在执行的
|
||
```
|
||
|
||
### 5.2 API 层屏蔽 Workflow 取消
|
||
|
||
在后端中间层中,**不暴露** GitHub Actions 的 cancel 接口:
|
||
|
||
```jsx
|
||
// backend/api-server/routes/execution.js 中追加:
|
||
|
||
// 明确拒绝取消 workflow 的请求
|
||
router.post('/:executionId/cancel', (req, res) => {
|
||
res.status(403).json({
|
||
success: false,
|
||
reply: [
|
||
'🔒 执行中的操作不可取消。',
|
||
'',
|
||
'系统已开始执行,90+ 个自动化流程正在联动。',
|
||
'中途中断会导致数据不一致和级联故障。',
|
||
'',
|
||
'请等待执行完成。如果结果不理想,你可以说:',
|
||
'「前面的我没想好,我们重新来」',
|
||
'系统会在已有基础上重新迭代。'
|
||
].join('\n')
|
||
});
|
||
});
|
||
|
||
router.delete('/:executionId', (req, res) => {
|
||
res.status(403).json({
|
||
success: false,
|
||
reply: '🔒 不可删除执行记录。所有执行都是永久记录。'
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 六、超时保护 + 死锁防护
|
||
|
||
### 6.1 执行超时处理
|
||
|
||
<aside>
|
||
⏰
|
||
|
||
**问题**:如果 workflow 挂死了怎么办?锁会一直不释放。
|
||
|
||
**解决**:执行锁有最大存活时间(TTL)。超时后自动释放 + 标记异常。
|
||
|
||
</aside>
|
||
|
||
创建文件:`backend/api-server/services/execution-watchdog.js`
|
||
|
||
```jsx
|
||
/**
|
||
* 执行看门狗
|
||
*
|
||
* 定期检查执行锁是否超时。
|
||
* 如果一个操作执行超过 MAX_EXECUTION_TIME,标记为超时并释放锁。
|
||
* 这是防止死锁的兜底机制。
|
||
*/
|
||
|
||
const { releaseLock } = require('../middleware/execution-lock');
|
||
|
||
// 最大执行时间:15 分钟(大部分操作应该在 2 分钟内完成)
|
||
const MAX_EXECUTION_TIME = 15 * 60 * 1000;
|
||
|
||
// 警告时间:10 分钟
|
||
const WARN_TIME = 10 * 60 * 1000;
|
||
|
||
function startWatchdog(activeLocks) {
|
||
setInterval(() => {
|
||
const now = Date.now();
|
||
|
||
for (const [devId, lock] of activeLocks) {
|
||
if (lock.state !== 'running') continue;
|
||
|
||
const 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: `执行超时(超过 ${MAX_EXECUTION_TIME/60000} 分钟)` };
|
||
|
||
// 写入紧急 SYSLOG
|
||
writeSyslog({
|
||
content: `⚠️ 执行超时 | ${lock.executionId} | ${lock.operation} | 开发者: ${devId} | 已运行 ${Math.round(elapsed/1000)}s`,
|
||
type: '紧急'
|
||
});
|
||
|
||
// 延迟释放(给前端时间获取超时状态)
|
||
setTimeout(() => releaseLock(devId), 30000);
|
||
}
|
||
// 警告
|
||
else if (elapsed > WARN_TIME && !lock.warned) {
|
||
console.warn(`[WATCHDOG] 执行时间较长: ${lock.executionId} (${lock.operation}) - ${Math.round(elapsed/1000)}s`);
|
||
lock.warned = true;
|
||
}
|
||
}
|
||
}, 30000); // 每 30 秒检查一次
|
||
}
|
||
|
||
module.exports = { startWatchdog, MAX_EXECUTION_TIME };
|
||
```
|
||
|
||
---
|
||
|
||
## 七、文件创建清单
|
||
|
||
<aside>
|
||
📂
|
||
|
||
**以下所有文件都是新增文件,不修改任何现有文件。**
|
||
|
||
**遵守天眼保护清单,能新增的绝不覆盖。**
|
||
|
||
</aside>
|
||
|
||
```jsx
|
||
新增文件清单:
|
||
|
||
// 前端 · 执行保护 UI
|
||
docs/js/execution-guard.js ← 前端执行隔离(UI锁定 + 回放)
|
||
docs/css/execution-guard.css ← 执行锁定样式
|
||
|
||
// 后端 · 执行锁
|
||
backend/api-server/middleware/execution-lock.js ← 执行上下文锁 + 取消拦截
|
||
backend/api-server/routes/execution.js ← 执行状态查询接口
|
||
|
||
// 后端 · 原子执行
|
||
backend/api-server/services/checkpoint.js ← 检查点系统(快照+回滚)
|
||
backend/api-server/services/atomic-executor.js ← 原子执行包装器
|
||
|
||
// 后端 · 看门狗
|
||
backend/api-server/services/execution-watchdog.js ← 超时保护 + 死锁防护
|
||
```
|
||
|
||
---
|
||
|
||
## 八、与 LANGDRIVE 主指令的关系
|
||
|
||
本补充指令的文件与 LANGDRIVE Phase 3-6 的文件**无冲突**:
|
||
|
||
- Phase 3 的写入接口(`routes/write.js`)调用 `AtomicExecutor` 来执行写入
|
||
- Phase 4 的意图路由(`middleware/intent-router.js`)在识别到写入意图后,先通过 `ExecutionGuard` 锁定前端
|
||
- Phase 5 的权限检查(`middleware/auth.js`)在执行锁**之前**运行——权限不够直接拒绝,不进入执行保护
|
||
- Phase 6 的认知引导在预览站沙箱中运行,沙箱操作同样受执行保护(培养开发者的习惯)
|
||
|
||
**集成顺序**:权限检查 → 沙箱检查 → 执行锁获取 → 检查点保存 → 原子执行 → 释放锁 → 结果回放
|
||
|
||
---
|
||
|
||
## 九、验收标准
|
||
|
||
- [ ] 开发者发出写入指令后,前端输入框立即锁定,无取消按钮
|
||
- [ ] 执行期间,API 层拒绝所有 cancel/abort 请求,返回友好提示
|
||
- [ ] 执行成功:锁释放,前端解锁,展示完整执行结果
|
||
- [ ] 执行失败:自动回滚到执行前状态,通知开发者
|
||
- [ ] 回滚失败:自动上报管理员(写入 SYSLOG + 维护日志)
|
||
- [ ] 执行超时(>15分钟):看门狗自动释放锁 + 标记异常
|
||
- [ ] 同一开发者不能同时执行多个写入操作
|
||
- [ ] 浏览器关闭/刷新时提示「执行中,关闭不会中断」
|
||
- [ ] GitHub Actions 的 `cancel-in-progress: false` 已设置
|
||
- [ ] 所有执行记录永久保留,不可删除
|
||
|
||
---
|
||
|
||
## 十、回执要求
|
||
|
||
```jsx
|
||
[ZY-LANGDRIVE-S1-RECEIPT]
|
||
指令编号: ZY-LANGDRIVE-2026-0325-001-S1
|
||
补充内容: Phase 7 · 执行保护层
|
||
|
||
前端隔离(L1):
|
||
UI锁定: ✅/❌
|
||
无取消按钮: ✅/❌
|
||
执行回放: ✅/❌
|
||
|
||
API拦截(L2):
|
||
执行锁: ✅/❌
|
||
取消拦截: ✅/❌
|
||
并发防护: ✅/❌
|
||
|
||
原子执行(L3):
|
||
检查点系统: ✅/❌
|
||
自动回滚: ✅/❌
|
||
超时看门狗: ✅/❌
|
||
|
||
集成测试:
|
||
写入指令→锁定→执行→解锁: ✅/❌
|
||
执行失败→自动回滚: ✅/❌
|
||
取消请求→被拦截: ✅/❌
|
||
```
|
||
|
||
---
|
||
|
||
> 🔗 **签发**:霜砚(AG-SY-01)
|
||
>
|
||
|
||
> 👤 **授权**:冰朔(TCS-0002∞)
|
||
>
|
||
|
||
> 📅 **日期**:2026-03-25
|
||
>
|
||
|
||
> 📌 **版权**:国作登字-2026-A-00037559
|
||
>
|
||
|
||
> 🔗 **父指令**:ZY-LANGDRIVE-2026-0325-001
|
||
> |