Compare commits
2 Commits
053db95fee
...
55af5b52c6
| Author | SHA1 | Date | |
|---|---|---|---|
| 55af5b52c6 | |||
| 6d330b1388 |
@ -188,6 +188,8 @@ export class MonitorDirector implements MonitorDriver {
|
||||
private destroyFns: Array<() => void> = [];
|
||||
private appIcons = new Map<string, HTMLElement>();
|
||||
private folderIcons = new Map<string, HTMLElement>();
|
||||
// 用于控制编辑器动画全局加速(根据本次补丁的总体改动量动态调整)
|
||||
private editorSpeedBoost = 1;
|
||||
private pendingDesktopFolders = new Set<string>();
|
||||
private fileIcons = new Map<string, HTMLElement>();
|
||||
private browserResultMap = new Map<string, HTMLLIElement>();
|
||||
@ -3517,6 +3519,15 @@ export class MonitorDirector implements MonitorDriver {
|
||||
}
|
||||
const operations = this.buildEditorDiff(currentLines, targetLines);
|
||||
const mergedOperations = this.mergeEditorOperations(operations);
|
||||
// 根据本次动画的总改动量动态提升速度:改动越大,动画越快
|
||||
const totalInsertedChars = mergedOperations.reduce((sum, op) => {
|
||||
if (op.type === 'delete') {
|
||||
return sum;
|
||||
}
|
||||
return sum + (op.text?.length || 0);
|
||||
}, 0);
|
||||
const totalOps = mergedOperations.length;
|
||||
this.editorSpeedBoost = this.computeEditorSpeedBoost(totalInsertedChars, totalOps);
|
||||
editorDebug('animate:operations', { count: mergedOperations.length });
|
||||
if (!mergedOperations.length) {
|
||||
if (!targetLines.length) {
|
||||
@ -3548,6 +3559,8 @@ export class MonitorDirector implements MonitorDriver {
|
||||
this.editorScene.lines = targetLines.slice();
|
||||
this.syncEditorIndices();
|
||||
editorDebug('animate:done', { finalLines: this.editorScene.lines.length });
|
||||
// 重置全局加速
|
||||
this.editorSpeedBoost = 1;
|
||||
}
|
||||
|
||||
private buildEditorDiff(before: string[], after: string[]): EditorOperation[] {
|
||||
@ -3753,10 +3766,14 @@ export class MonitorDirector implements MonitorDriver {
|
||||
await sleep(40);
|
||||
return;
|
||||
}
|
||||
// 行越长、整体改动越大 -> 动画越快
|
||||
const lineBoost = 1 + Math.min(normalized.length / 40, 6);
|
||||
const speed = Math.max(1, lineBoost * this.editorSpeedBoost);
|
||||
const interval = Math.max(4, Math.floor(EDITOR_TYPING_INTERVAL / speed));
|
||||
for (const char of normalized.split('')) {
|
||||
target.textContent = `${target.textContent || ''}${char}`;
|
||||
this.adjustEditorScrollForLine(target);
|
||||
await sleep(EDITOR_TYPING_INTERVAL);
|
||||
await sleep(interval);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3776,9 +3793,12 @@ export class MonitorDirector implements MonitorDriver {
|
||||
await sleep(60);
|
||||
return;
|
||||
}
|
||||
const lineBoost = 1 + Math.min(current.length / 40, 6);
|
||||
const speed = Math.max(1, lineBoost * this.editorSpeedBoost);
|
||||
const interval = Math.max(3, Math.floor(EDITOR_ERASE_INTERVAL / speed));
|
||||
for (let i = current.length - 1; i >= 0; i -= 1) {
|
||||
target.textContent = current.slice(0, i);
|
||||
await sleep(EDITOR_ERASE_INTERVAL);
|
||||
await sleep(interval);
|
||||
}
|
||||
target.textContent = '';
|
||||
}
|
||||
@ -3832,6 +3852,17 @@ export class MonitorDirector implements MonitorDriver {
|
||||
this.prepareEditorScene(lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据本次补丁的总体字符量与操作数,返回动画加速倍数。
|
||||
* - 插入/替换字符越多,加速越明显
|
||||
* - 操作数量越多(分块多),也进一步加速
|
||||
*/
|
||||
private computeEditorSpeedBoost(totalChars: number, totalOps: number): number {
|
||||
const charBoost = Math.min(totalChars / 400, 4); // 0 ~ 4
|
||||
const opBoost = Math.min(totalOps / 120, 2); // 0 ~ 2
|
||||
return 1 + charBoost + opBoost;
|
||||
}
|
||||
|
||||
private async typeSearchQuery(text: string) {
|
||||
this.elements.browserSearchText.textContent = '';
|
||||
for (const char of text.split('')) {
|
||||
|
||||
@ -37,6 +37,32 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
ignoreThinking: false
|
||||
};
|
||||
|
||||
const pendingToolEvents: Array<{ event: string; data: any; handler: () => void }> = [];
|
||||
|
||||
const hasPendingStreamingText = () =>
|
||||
!!streamingState.buffer.length ||
|
||||
!!streamingState.pendingCompleteContent ||
|
||||
streamingState.timer !== null ||
|
||||
streamingState.completionTimer !== null;
|
||||
|
||||
const resetPendingToolEvents = () => {
|
||||
pendingToolEvents.length = 0;
|
||||
};
|
||||
|
||||
const flushPendingToolEvents = () => {
|
||||
if (!pendingToolEvents.length) {
|
||||
return;
|
||||
}
|
||||
const queue = pendingToolEvents.splice(0);
|
||||
queue.forEach((item) => {
|
||||
try {
|
||||
item.handler();
|
||||
} catch (error) {
|
||||
console.warn('延后工具事件处理失败:', item.event, error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const snapshotStreamingState = () => ({
|
||||
bufferLength: streamingState.buffer.length,
|
||||
timerActive: streamingState.timer !== null,
|
||||
@ -297,6 +323,7 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
streamingState.activeMessageIndex = null;
|
||||
streamingState.activeTextAction = null;
|
||||
markStreamingIdleIfPossible('finalizeStreamingText');
|
||||
flushPendingToolEvents();
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -326,6 +353,7 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
streamingState.renderedText = '';
|
||||
streamingState.activeMessageIndex = null;
|
||||
streamingState.activeTextAction = null;
|
||||
resetPendingToolEvents();
|
||||
logStreamingDebug('resetStreamingBuffer', snapshotStreamingState());
|
||||
};
|
||||
|
||||
@ -658,6 +686,7 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
socketLog('AI消息开始');
|
||||
logStreamingDebug('socket:ai_message_start');
|
||||
finalizeStreamingText({ force: true });
|
||||
flushPendingToolEvents();
|
||||
resetStreamingBuffer();
|
||||
ctx.monitorResetSpeech();
|
||||
ctx.cleanupStaleToolActions();
|
||||
@ -807,34 +836,41 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id);
|
||||
return;
|
||||
}
|
||||
const msg = ctx.chatEnsureAssistantMessage();
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
const action = {
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
const handler = () => {
|
||||
const msg = ctx.chatEnsureAssistantMessage();
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
const action = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
arguments: {},
|
||||
argumentSnapshot: null,
|
||||
argumentLabel: '',
|
||||
status: 'preparing',
|
||||
result: null,
|
||||
message: data.message || `准备调用 ${data.name}...`
|
||||
},
|
||||
timestamp: Date.now()
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
arguments: {},
|
||||
argumentSnapshot: null,
|
||||
argumentLabel: '',
|
||||
status: 'preparing',
|
||||
result: null,
|
||||
message: data.message || `准备调用 ${data.name}...`
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
msg.actions.push(action);
|
||||
ctx.preparingTools.set(data.id, action);
|
||||
ctx.toolRegisterAction(action, data.id);
|
||||
ctx.toolTrackAction(data.name, action);
|
||||
ctx.$forceUpdate();
|
||||
ctx.conditionalScrollToBottom();
|
||||
if (ctx.monitorPreviewTool) {
|
||||
ctx.monitorPreviewTool(data);
|
||||
}
|
||||
};
|
||||
msg.actions.push(action);
|
||||
ctx.preparingTools.set(data.id, action);
|
||||
ctx.toolRegisterAction(action, data.id);
|
||||
ctx.toolTrackAction(data.name, action);
|
||||
ctx.$forceUpdate();
|
||||
ctx.conditionalScrollToBottom();
|
||||
// 虚拟显示器:在模型检测到工具时立即展示“正在XX”预览
|
||||
if (ctx.monitorPreviewTool) {
|
||||
ctx.monitorPreviewTool(data);
|
||||
|
||||
if (hasPendingStreamingText()) {
|
||||
pendingToolEvents.push({ event: 'tool_preparing', data, handler });
|
||||
} else {
|
||||
handler();
|
||||
}
|
||||
});
|
||||
|
||||
@ -868,46 +904,54 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
socketLog('跳过tool_start(对话不匹配)', data.conversation_id);
|
||||
return;
|
||||
}
|
||||
let action = null;
|
||||
if (data.preparing_id && ctx.preparingTools.has(data.preparing_id)) {
|
||||
action = ctx.preparingTools.get(data.preparing_id);
|
||||
ctx.preparingTools.delete(data.preparing_id);
|
||||
} else {
|
||||
action = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
||||
}
|
||||
if (!action) {
|
||||
const msg = ctx.chatEnsureAssistantMessage();
|
||||
if (!msg) {
|
||||
return;
|
||||
const handler = () => {
|
||||
let action = null;
|
||||
if (data.preparing_id && ctx.preparingTools.has(data.preparing_id)) {
|
||||
action = ctx.preparingTools.get(data.preparing_id);
|
||||
ctx.preparingTools.delete(data.preparing_id);
|
||||
} else {
|
||||
action = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
||||
}
|
||||
action = {
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
arguments: {},
|
||||
argumentSnapshot: null,
|
||||
argumentLabel: '',
|
||||
status: 'running',
|
||||
result: null
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
msg.actions.push(action);
|
||||
if (!action) {
|
||||
const msg = ctx.chatEnsureAssistantMessage();
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
action = {
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
arguments: {},
|
||||
argumentSnapshot: null,
|
||||
argumentLabel: '',
|
||||
status: 'running',
|
||||
result: null
|
||||
},
|
||||
timestamp: Date.now()
|
||||
};
|
||||
msg.actions.push(action);
|
||||
}
|
||||
action.tool.status = 'running';
|
||||
action.tool.arguments = data.arguments;
|
||||
action.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
|
||||
action.tool.argumentLabel = ctx.buildToolLabel(action.tool.argumentSnapshot);
|
||||
action.tool.message = null;
|
||||
action.tool.id = data.id;
|
||||
action.tool.executionId = data.id;
|
||||
ctx.toolRegisterAction(action, data.id);
|
||||
ctx.toolTrackAction(data.name, action);
|
||||
ctx.$forceUpdate();
|
||||
ctx.conditionalScrollToBottom();
|
||||
ctx.monitorQueueTool(data);
|
||||
};
|
||||
|
||||
if (hasPendingStreamingText()) {
|
||||
pendingToolEvents.push({ event: 'tool_start', data, handler });
|
||||
} else {
|
||||
handler();
|
||||
}
|
||||
action.tool.status = 'running';
|
||||
action.tool.arguments = data.arguments;
|
||||
action.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
|
||||
action.tool.argumentLabel = ctx.buildToolLabel(action.tool.argumentSnapshot);
|
||||
action.tool.message = null;
|
||||
action.tool.id = data.id;
|
||||
action.tool.executionId = data.id;
|
||||
ctx.toolRegisterAction(action, data.id);
|
||||
ctx.toolTrackAction(data.name, action);
|
||||
ctx.$forceUpdate();
|
||||
ctx.conditionalScrollToBottom();
|
||||
ctx.monitorQueueTool(data);
|
||||
});
|
||||
|
||||
// 更新action(工具完成)
|
||||
@ -917,82 +961,89 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
socketLog('跳过update_action(对话不匹配)', data.conversation_id);
|
||||
return;
|
||||
}
|
||||
let targetAction = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
||||
if (!targetAction && data.preparing_id && ctx.preparingTools.has(data.preparing_id)) {
|
||||
targetAction = ctx.preparingTools.get(data.preparing_id);
|
||||
}
|
||||
if (!targetAction) {
|
||||
outer: for (const message of ctx.messages) {
|
||||
if (!message.actions) continue;
|
||||
for (const action of message.actions) {
|
||||
if (action.type !== 'tool') continue;
|
||||
const matchByExecution = action.tool.executionId && action.tool.executionId === data.id;
|
||||
const matchByToolId = action.tool.id === data.id;
|
||||
const matchByPreparingId = action.id === data.preparing_id;
|
||||
if (matchByExecution || matchByToolId || matchByPreparingId) {
|
||||
targetAction = action;
|
||||
break outer;
|
||||
const handler = () => {
|
||||
let targetAction = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
||||
if (!targetAction && data.preparing_id && ctx.preparingTools.has(data.preparing_id)) {
|
||||
targetAction = ctx.preparingTools.get(data.preparing_id);
|
||||
}
|
||||
if (!targetAction) {
|
||||
outer: for (const message of ctx.messages) {
|
||||
if (!message.actions) continue;
|
||||
for (const action of message.actions) {
|
||||
if (action.type !== 'tool') continue;
|
||||
const matchByExecution = action.tool.executionId && action.tool.executionId === data.id;
|
||||
const matchByToolId = action.tool.id === data.id;
|
||||
const matchByPreparingId = action.id === data.preparing_id;
|
||||
if (matchByExecution || matchByToolId || matchByPreparingId) {
|
||||
targetAction = action;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetAction) {
|
||||
if (data.status) {
|
||||
targetAction.tool.status = data.status;
|
||||
}
|
||||
if (data.result !== undefined) {
|
||||
targetAction.tool.result = data.result;
|
||||
}
|
||||
if (targetAction.tool && targetAction.tool.name === 'trigger_easter_egg' && data.result !== undefined) {
|
||||
const eggPromise = ctx.handleEasterEggPayload(data.result);
|
||||
if (eggPromise && typeof eggPromise.catch === 'function') {
|
||||
eggPromise.catch((error) => {
|
||||
console.warn('彩蛋处理异常:', error);
|
||||
});
|
||||
if (targetAction) {
|
||||
if (data.status) {
|
||||
targetAction.tool.status = data.status;
|
||||
}
|
||||
}
|
||||
if (data.message !== undefined) {
|
||||
targetAction.tool.message = data.message;
|
||||
}
|
||||
if (data.awaiting_content) {
|
||||
targetAction.tool.awaiting_content = true;
|
||||
} else if (data.status === 'completed') {
|
||||
targetAction.tool.awaiting_content = false;
|
||||
}
|
||||
if (!targetAction.tool.executionId && (data.execution_id || data.id)) {
|
||||
targetAction.tool.executionId = data.execution_id || data.id;
|
||||
}
|
||||
if (data.arguments) {
|
||||
targetAction.tool.arguments = data.arguments;
|
||||
targetAction.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
|
||||
targetAction.tool.argumentLabel = ctx.buildToolLabel(targetAction.tool.argumentSnapshot);
|
||||
}
|
||||
ctx.toolRegisterAction(targetAction, data.execution_id || data.id);
|
||||
if (data.status && ["completed", "failed", "error"].includes(data.status) && !data.awaiting_content) {
|
||||
ctx.toolUnregisterAction(targetAction);
|
||||
if (data.id) {
|
||||
ctx.preparingTools.delete(data.id);
|
||||
if (data.result !== undefined) {
|
||||
targetAction.tool.result = data.result;
|
||||
}
|
||||
if (data.preparing_id) {
|
||||
ctx.preparingTools.delete(data.preparing_id);
|
||||
if (targetAction.tool && targetAction.tool.name === 'trigger_easter_egg' && data.result !== undefined) {
|
||||
const eggPromise = ctx.handleEasterEggPayload(data.result);
|
||||
if (eggPromise && typeof eggPromise.catch === 'function') {
|
||||
eggPromise.catch((error) => {
|
||||
console.warn('彩蛋处理异常:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (data.message !== undefined) {
|
||||
targetAction.tool.message = data.message;
|
||||
}
|
||||
if (data.awaiting_content) {
|
||||
targetAction.tool.awaiting_content = true;
|
||||
} else if (data.status === 'completed') {
|
||||
targetAction.tool.awaiting_content = false;
|
||||
}
|
||||
if (!targetAction.tool.executionId && (data.execution_id || data.id)) {
|
||||
targetAction.tool.executionId = data.execution_id || data.id;
|
||||
}
|
||||
if (data.arguments) {
|
||||
targetAction.tool.arguments = data.arguments;
|
||||
targetAction.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
|
||||
targetAction.tool.argumentLabel = ctx.buildToolLabel(targetAction.tool.argumentSnapshot);
|
||||
}
|
||||
ctx.toolRegisterAction(targetAction, data.execution_id || data.id);
|
||||
if (data.status && ["completed", "failed", "error"].includes(data.status) && !data.awaiting_content) {
|
||||
ctx.toolUnregisterAction(targetAction);
|
||||
if (data.id) {
|
||||
ctx.preparingTools.delete(data.id);
|
||||
}
|
||||
if (data.preparing_id) {
|
||||
ctx.preparingTools.delete(data.preparing_id);
|
||||
}
|
||||
}
|
||||
ctx.$forceUpdate();
|
||||
ctx.conditionalScrollToBottom();
|
||||
}
|
||||
ctx.$forceUpdate();
|
||||
ctx.conditionalScrollToBottom();
|
||||
}
|
||||
|
||||
// 关键修复:每个工具完成后都更新当前上下文Token
|
||||
if (data.status === 'completed') {
|
||||
setTimeout(() => {
|
||||
ctx.updateCurrentContextTokens();
|
||||
}, 500);
|
||||
try {
|
||||
ctx.fileFetchTree();
|
||||
} catch (error) {
|
||||
console.warn('刷新文件树失败', error);
|
||||
if (data.status === 'completed') {
|
||||
setTimeout(() => {
|
||||
ctx.updateCurrentContextTokens();
|
||||
}, 500);
|
||||
try {
|
||||
ctx.fileFetchTree();
|
||||
} catch (error) {
|
||||
console.warn('刷新文件树失败', error);
|
||||
}
|
||||
}
|
||||
ctx.monitorResolveTool(data);
|
||||
};
|
||||
|
||||
if (hasPendingStreamingText()) {
|
||||
pendingToolEvents.push({ event: 'update_action', data, handler });
|
||||
} else {
|
||||
handler();
|
||||
}
|
||||
ctx.monitorResolveTool(data);
|
||||
});
|
||||
|
||||
ctx.socket.on('append_payload', (data) => {
|
||||
@ -1046,6 +1097,8 @@ export async function initializeLegacySocket(ctx: any) {
|
||||
socketLog('任务完成', data);
|
||||
ctx.scheduleResetAfterTask('socket:task_complete', { preserveMonitorWindows: true });
|
||||
|
||||
resetPendingToolEvents();
|
||||
|
||||
// 任务完成后立即更新Token统计(关键修复)
|
||||
if (ctx.currentConversationId) {
|
||||
ctx.updateCurrentContextTokens();
|
||||
|
||||
@ -97,6 +97,7 @@ def _format_write_file_diff(result_data: Dict[str, Any], raw_text: str) -> str:
|
||||
summary = result_data.get("summary") or result_data.get("message")
|
||||
completed = result_data.get("completed") or []
|
||||
failed_blocks = result_data.get("failed") or []
|
||||
success_blocks = result_data.get("blocks") or []
|
||||
lines = [f"[文件补丁] {path}"]
|
||||
if summary:
|
||||
lines.append(summary)
|
||||
@ -126,6 +127,8 @@ def _format_write_file_diff(result_data: Dict[str, Any], raw_text: str) -> str:
|
||||
reason = item.get("reason") or item.get("error") or "未说明原因"
|
||||
hint = item.get("hint")
|
||||
block_patch = item.get("block_patch") or item.get("patch")
|
||||
# 自动判别常见错误形态,便于快速定位问题
|
||||
diagnostics = _classify_diff_block_issue(item, result_data)
|
||||
if not block_patch:
|
||||
old_text = item.get("old_text") or ""
|
||||
new_text = item.get("new_text") or ""
|
||||
@ -137,6 +140,8 @@ def _format_write_file_diff(result_data: Dict[str, Any], raw_text: str) -> str:
|
||||
if synthetic_lines:
|
||||
block_patch = "\n".join(synthetic_lines)
|
||||
detail_sections.append(f"- #{idx}: {reason}")
|
||||
if diagnostics:
|
||||
detail_sections.append(f" 错误类型: {diagnostics}")
|
||||
if hint:
|
||||
detail_sections.append(f" 提示: {hint}")
|
||||
if block_patch:
|
||||
@ -149,6 +154,23 @@ def _format_write_file_diff(result_data: Dict[str, Any], raw_text: str) -> str:
|
||||
if detail_sections:
|
||||
lines.append("⚠️ 失败块详情:")
|
||||
lines.extend(detail_sections)
|
||||
|
||||
# 对“成功块”做轻量体检:如果检测到潜在格式风险,给出风险提示(不影响 success 判定)
|
||||
risk_sections: List[str] = []
|
||||
for item in success_blocks:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
status = item.get("status")
|
||||
idx = item.get("index")
|
||||
if status != "success":
|
||||
continue
|
||||
diag = _classify_diff_block_issue(item, result_data)
|
||||
if diag:
|
||||
risk_sections.append(f"- #{idx}: {diag}")
|
||||
if risk_sections:
|
||||
lines.append("⚠️ 风险提示(补丁虽成功但格式可能有隐患):")
|
||||
lines.extend(risk_sections)
|
||||
|
||||
if result_data.get("success") is False and result_data.get("error"):
|
||||
lines.append(f"⚠️ 错误: {result_data.get('error')}")
|
||||
formatted = "\n".join(line for line in lines if line)
|
||||
@ -161,6 +183,44 @@ def _format_create_file(result_data: Dict[str, Any]) -> str:
|
||||
return result_data.get("message") or f"已创建空文件: {result_data.get('path', '未知路径')}"
|
||||
|
||||
|
||||
def _classify_diff_block_issue(block: Dict[str, Any], result_data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
针对 write_file_diff 常见的“离谱/易错”用法做启发式判别,返回简短错误类型说明。
|
||||
不改变后端逻辑,只用于提示。
|
||||
"""
|
||||
patch_text = block.get("block_patch") or block.get("patch") or ""
|
||||
lines = patch_text.splitlines()
|
||||
plus = sum(1 for ln in lines if ln.startswith("+"))
|
||||
minus = sum(1 for ln in lines if ln.startswith("-"))
|
||||
context = sum(1 for ln in lines if ln.startswith(" "))
|
||||
total = len([ln for ln in lines if ln.strip() != ""])
|
||||
|
||||
reasons: List[str] = []
|
||||
# 1) 完全没加 + / - :最常见的“把目标当上下文”
|
||||
if total > 0 and plus == 0 and minus == 0:
|
||||
reasons.append("缺少 + / -,整块被当作上下文,无法定位到文件")
|
||||
# 2) 全是 + 且没有上下文/删除:解析为“纯追加”,若目标非末尾插入会失败
|
||||
if plus > 0 and minus == 0 and context == 0:
|
||||
reasons.append("仅包含 + 行,被视为追加块;若想中间插入/替换需提供上下文或 -")
|
||||
# 3) 没有上下文或删除行却不是 append_only(多数是漏写空格前缀)
|
||||
if block.get("append_only") and (context > 0 or minus > 0):
|
||||
reasons.append("块被解析为追加模式,但混入了上下文/删除行,可能写法不一致")
|
||||
# 4) 未找到匹配时,提示检查空格/缩进/全角半角/换行差异
|
||||
reason_text = (block.get("reason") or "").lower()
|
||||
if "未找到匹配" in reason_text:
|
||||
reasons.append("上下文未匹配:检查空格/缩进、全角半角、CRLF/LF、大小写是否与原文完全一致")
|
||||
# 5) 空行未加 '+' 的典型情形:
|
||||
# a) 有空白行但整块没有前缀(此前已由 #1 捕获),仍补充提示
|
||||
# b) 有空白行且块中存在 + / -,说明空行漏写前缀,易导致上下文匹配失败
|
||||
if any(ln == "" or ln.strip() == "" for ln in lines):
|
||||
if plus == 0 and minus == 0:
|
||||
reasons.append("空行未写 `+`,被当作上下文,建议空行写成单独一行 `+`")
|
||||
else:
|
||||
reasons.append("空行未写 `+`(或空格上下文),混入补丁时会被当作上下文,建议空行单独写成 `+`")
|
||||
|
||||
return ";".join(reasons)
|
||||
|
||||
|
||||
def _format_delete_file(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("delete_file", result_data)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user