feat(runtime): 客户端事件窗口缺口处理——Web 快照对账 + CLI 窗口对齐
- Web stores/task.ts:检测 offset < window_start 发合成事件
event_window_gap(复用 runtime_queue_sync 解耦模式)
- lifecycle.ts 新增 case:fetchAndDisplayHistory({force:true})
会话 JSON 权威重读对账(协议 §5.4)+ toast 提示
- i18n:stores.eventWindowGap / eventWindowGapReload(zh/en)
- CLI api.ts TaskPollResult 补 window_start;App.tsx pollTask
检测后提示用户并对齐窗口续读
- 协议文档 §5.2 标注客户端兑现状态
验证:i18n 栏杆通过;CLI typecheck+build 通过;改动文件
eslint 0 errors;前端 vite build 受沙箱读边界限制未跑
(PostCSS 搜索 /Users/jojo/package.json EPERM,环境问题)
This commit is contained in:
parent
f43b03484d
commit
0381557fe9
@ -493,7 +493,14 @@ export default function App() {
|
|||||||
const renderState = createEventRenderState();
|
const renderState = createEventRenderState();
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const result = await api.pollTask(taskId, offset);
|
const result = await api.pollTask(taskId, offset);
|
||||||
offset = result.next_offset;
|
// 事件窗口缺口检测(协议 §5.2):中间事件已被裁剪,提示用户并对齐窗口继续
|
||||||
|
const windowStart = Number(result.window_start ?? 0);
|
||||||
|
if (offset > 0 && offset < windowStart) {
|
||||||
|
addItem({ kind: 'system', body: `部分实时输出已被裁剪(窗口起点 ${windowStart}),以最终结果为准` });
|
||||||
|
offset = windowStart;
|
||||||
|
} else {
|
||||||
|
offset = result.next_offset;
|
||||||
|
}
|
||||||
for (const event of result.events || []) {
|
for (const event of result.events || []) {
|
||||||
if (event.type === 'tool_approval_required') {
|
if (event.type === 'tool_approval_required') {
|
||||||
const approval = event.data?.approval || event.data;
|
const approval = event.data?.approval || event.data;
|
||||||
|
|||||||
@ -17,6 +17,8 @@ export type TaskPollResult = {
|
|||||||
conversation_id?: string;
|
conversation_id?: string;
|
||||||
events: Array<{ idx: number; type: string; data: any; ts?: number }>;
|
events: Array<{ idx: number; type: string; data: any; ts?: number }>;
|
||||||
next_offset: number;
|
next_offset: number;
|
||||||
|
// 事件窗口当前最小 idx(协议 §5.2):offset 落后于此值说明中间事件已被裁剪
|
||||||
|
window_start?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ApiClient {
|
export class ApiClient {
|
||||||
|
|||||||
@ -392,6 +392,21 @@ export const lifecycleMethods = {
|
|||||||
if (this._rebuildingFromScratch) break;
|
if (this._rebuildingFromScratch) break;
|
||||||
this.handleRuntimeQueueSync(eventData);
|
this.handleRuntimeQueueSync(eventData);
|
||||||
break;
|
break;
|
||||||
|
case 'event_window_gap':
|
||||||
|
// 事件窗口缺口(协议 §5.2/§5.4):中间事件已被服务端裁剪,
|
||||||
|
// 以会话 JSON 为权威强制重读历史对账。重建期间历史本就是新加载的,跳过。
|
||||||
|
if (this._rebuildingFromScratch) break;
|
||||||
|
debugLog('[TaskPolling] 事件窗口缺口,触发会话快照对账:', eventData);
|
||||||
|
if ((window as any).__vueApp?.uiPushToast) {
|
||||||
|
(window as any).__vueApp.uiPushToast({
|
||||||
|
title: t('stores.eventWindowGap'),
|
||||||
|
message: t('stores.eventWindowGapReload'),
|
||||||
|
type: 'warning',
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.fetchAndDisplayHistory({ force: true });
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
debugLog(`[TaskPolling] 未知事件类型: ${eventType}`);
|
debugLog(`[TaskPolling] 未知事件类型: ${eventType}`);
|
||||||
|
|||||||
@ -60,6 +60,8 @@ export default {
|
|||||||
fetchTaskListFailed: 'Failed to fetch task list',
|
fetchTaskListFailed: 'Failed to fetch task list',
|
||||||
pollingFluctuation: 'Polling fluctuation',
|
pollingFluctuation: 'Polling fluctuation',
|
||||||
pollingUnstableRetry: 'Message updates are temporarily unstable, retrying automatically',
|
pollingUnstableRetry: 'Message updates are temporarily unstable, retrying automatically',
|
||||||
|
eventWindowGap: 'Sync gap detected',
|
||||||
|
eventWindowGapReload: 'Some live updates were trimmed; resyncing conversation content',
|
||||||
requestFailed: 'Request failed',
|
requestFailed: 'Request failed',
|
||||||
fileTreeUnavailableHostMode: 'File tree unavailable in host mode',
|
fileTreeUnavailableHostMode: 'File tree unavailable in host mode',
|
||||||
unlimited: 'Unlimited',
|
unlimited: 'Unlimited',
|
||||||
|
|||||||
@ -60,6 +60,8 @@ export default {
|
|||||||
fetchTaskListFailed: '获取任务列表失败',
|
fetchTaskListFailed: '获取任务列表失败',
|
||||||
pollingFluctuation: '轮询波动',
|
pollingFluctuation: '轮询波动',
|
||||||
pollingUnstableRetry: '消息更新暂时不稳定,正在自动重试',
|
pollingUnstableRetry: '消息更新暂时不稳定,正在自动重试',
|
||||||
|
eventWindowGap: '消息同步缺口',
|
||||||
|
eventWindowGapReload: '部分实时更新已被裁剪,正在重新同步对话内容',
|
||||||
requestFailed: '请求失败',
|
requestFailed: '请求失败',
|
||||||
fileTreeUnavailableHostMode: '宿主机模式下文件树不可用',
|
fileTreeUnavailableHostMode: '宿主机模式下文件树不可用',
|
||||||
unlimited: '未限制',
|
unlimited: '未限制',
|
||||||
|
|||||||
@ -258,6 +258,34 @@ export const useTaskStore = defineStore('task', {
|
|||||||
let sawTaskCompleteEvent = false;
|
let sawTaskCompleteEvent = false;
|
||||||
let sawTaskStoppedEvent = false;
|
let sawTaskStoppedEvent = false;
|
||||||
|
|
||||||
|
// 事件窗口缺口检测(协议 §5.2):请求 offset 落后于窗口起点,说明中间事件
|
||||||
|
// 已被裁剪,禁止当作“无新事件”静默吞掉——通知上层走会话快照对账(§5.4),
|
||||||
|
// 本批返回的事件(idx >= window_start)是连续段,游标照常推进。
|
||||||
|
const windowStart = Number(data?.window_start ?? 0);
|
||||||
|
if (fromOffset > 0 && fromOffset < windowStart) {
|
||||||
|
debugLog('[Task] 事件窗口缺口:', { from: fromOffset, windowStart });
|
||||||
|
taskPollDiag('task-poll-window-gap', {
|
||||||
|
requestId,
|
||||||
|
taskId,
|
||||||
|
from: fromOffset,
|
||||||
|
windowStart,
|
||||||
|
nextOffset: data?.next_offset
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
eventHandler({
|
||||||
|
type: 'event_window_gap',
|
||||||
|
data: {
|
||||||
|
task_id: data?.task_id || this.currentTaskId,
|
||||||
|
conversation_id: data?.conversation_id || null,
|
||||||
|
from_offset: fromOffset,
|
||||||
|
window_start: windowStart
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[Task] 处理 event_window_gap 失败:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理新事件
|
// 处理新事件
|
||||||
if (data.events && data.events.length > 0) {
|
if (data.events && data.events.length > 0) {
|
||||||
debugLog(`[Task] 收到 ${data.events.length} 个新事件`);
|
debugLog(`[Task] 收到 ${data.events.length} 个新事件`);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user