虚拟显示器

This commit is contained in:
JOJO 2025-12-14 04:20:57 +08:00
parent 2f75c1c8bb
commit 4fbda2cfc8
13 changed files with 1698 additions and 194 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,237 @@
使用统一 diff`@@` 块、`-`/`+`/空格行)对单个文件做精确编辑:追加、插入、替换、删除都可以在一次调用里完成。
硬性规则:
1) 补丁必须被 `*** Begin Patch` 与 `*** End Patch` 包裹。
2) 每个修改块必须以 `@@ [id:数字]` 开头。
3) 块内每一行只能是三类之一:
- 上下文行:以空格开头(` ␠`),表示“文件里必须原样存在”的锚点;
- 删除行:以 `-` 开头,表示要从文件中移除的原文;
- 新增行:以 `+` 开头,表示要写入的新内容。
4) 任何“想新增/想删除/想替换”的内容都必须逐行写 `+` 或 `-`;如果你把多行新内容直接贴上去却不加 `+`,它会被当成上下文锚点去匹配原文件,极易导致“未找到匹配的原文”。
5) 重要语义:一个块里如果完全没有上下文行(空格开头)也没有删除行(`-`那么它会被视为“仅追加append-only也就是把所有 `+` 行追加到文件末尾——这对“给空文件写正文”很合适,但对“插入到中间”是错误的。
正面案例(至少 5 个,且都包含多行原文/多处修改)
1) 给空文件写完整正文(追加到末尾;空文件=正确)
目标:新建 README.md 后一次性写入标题、安装、用法、FAQ多段落、多行
要点:没有上下文/删除行 → 追加模式;空文件时最常用。
*** Begin Patch
@@ [id:1]
+# 项目名称
+
+一个简短说明:这个项目用于……
+
+## 安装
+
+```bash
+pip install -r requirements.txt
+```
+
+## 快速开始
+
+```bash
+python main.py
+```
+
+## 常见问题
+
+- Q: 为什么会报 xxx
+ A: 先检查 yyy再确认 zzz。
+
*** End Patch
2) “不删除,直接插入内容”到函数内部(必须用上下文锚定插入位置)
目标:在 def build_prompt(...): 里插入日志与参数归一化,但不改动其它行。
要点:插入发生在“两个上下文行之间”,上下文必须精确(包含缩进)。
*** Begin Patch
@@ [id:1]
def build_prompt(user_text: str, system_text: str, tags: list):
prompt_parts = []
+ # 参数归一化:去掉首尾空白,避免模型误判
+ user_text = (user_text or "").strip()
+ system_text = (system_text or "").strip()
+
+ logger.debug("build_prompt: tags=%s, user_len=%d", tags, len(user_text))
prompt_parts.append(system_text)
prompt_parts.append(user_text)
if tags:
prompt_parts.append("TAGS: " + ",".join(tags))
*** End Patch
3) 复杂替换:整段函数重构(多行 old/new + 保留稳定上下文)
目标:把旧的 apply_patch()(弱校验)替换成新实现(多分支、异常信息更清晰)。
要点:替换不是“改一行”,而是“删一段、加一段”,并用函数签名/相邻代码作锚点。
*** Begin Patch
@@ [id:1]
class FilePatcher:
def __init__(self, root: Path):
self.root = root
def apply_patch(self, path: str, patch_text: str) -> dict:
- # old: naive replace
- content = (self.root / path).read_text(encoding="utf-8")
- content = content.replace("foo", "bar")
- (self.root / path).write_text(content, encoding="utf-8")
- return {"success": True}
+ full_path = (self.root / path).resolve()
+ if self.root not in full_path.parents and full_path != self.root:
+ return {"success": False, "error": "非法路径:越界访问"}
+
+ if "*** Begin Patch" not in patch_text or "*** End Patch" not in patch_text:
+ return {"success": False, "error": "补丁格式错误:缺少 Begin/End 标记"}
+
+ try:
+ original = full_path.read_text(encoding="utf-8")
+ except Exception as e:
+ return {"success": False, "error": f"读取失败: {e}"}
+
+ # 这里省略:解析 blocks、逐块应用、失败回滚等
+ updated = original
+ try:
+ full_path.write_text(updated, encoding="utf-8")
+ except Exception as e:
+ return {"success": False, "error": f"写入失败: {e}"}
+
+ return {"success": True, "message": "已应用补丁"}
*** End Patch
4) 复杂多块:同一文件里同时“加 import + 替换逻辑 + 插入新 helper + 删除旧函数”
目标:一次调用完成 4 种操作,且每块都有足够上下文,避免误匹配。
要点:不同区域用不同 @@ [id:n] 分块,互不干扰。
*** Begin Patch
@@ [id:1]
-import json
+import json
+import re
from pathlib import Path
@@ [id:2]
def normalize_user_input(text: str) -> str:
- return text
+ text = (text or "").strip()
+ # 压缩多余空白,减少提示词抖动
+ text = re.sub(r"\\s+", " ", text)
+ return text
@@ [id:3]
def load_config(path: str) -> dict:
cfg_path = Path(path)
if not cfg_path.exists():
return {}
data = cfg_path.read_text(encoding="utf-8")
return json.loads(data)
+
+def safe_get(cfg: dict, key: str, default=None):
+ if not isinstance(cfg, dict):
+ return default
+ return cfg.get(key, default)
@@ [id:4]
-def legacy_parse_flags(argv):
- # deprecated, kept for compatibility
- flags = {}
- for item in argv:
- if item.startswith("--"):
- k, _, v = item[2:].partition("=")
- flags[k] = v or True
- return flags
-
def main():
cfg = load_config("config.json")
# ...
*** End Patch
5) 删除示例:删除一整段“废弃配置块”,并顺手修正周围空行(多行删除 + 上下文)
目标:删掉 DEPRECATED_* 配置和旧注释,确保删除位置精确。
要点:删除行必须逐行 `-`;保留上下文行确保定位。
*** Begin Patch
@@ [id:1]
# ==============================
# Runtime Config
# ==============================
-DEPRECATED_TIMEOUT = 5
-DEPRECATED_RETRIES = 1
-# 注意:这些字段将在下个版本移除
-# 请迁移到 NEW_TIMEOUT / NEW_RETRIES
NEW_TIMEOUT = 30
NEW_RETRIES = 3
*** End Patch
如何写“带上下文”的正确姿势(要点)
- 上下文要选“稳定锚点”:函数签名、类名、关键注释、紧邻的两三行缩进代码。
- 不要用“容易变的行”当唯一锚点:时间戳、日志序号、随机 id、生成内容片段。
- 上下文必须字节级一致(空格/Tab/大小写/标点都算),否则会匹配失败。
反面案例(至少 3 个,且都是“真实会踩坑”的类型)
反例 A来自一次常见错误空文件时只有第一行加了 `+`,后面直接贴正文
这会让后面的正文变成“上下文锚点”,工具会去空文件里找这些原文,必然失败(常见报错:未找到匹配的原文)。
*** Begin Patch
@@ [id:1]
+
仰望U9X·电驭苍穹
银箭破空电光闪
三千马力云中藏
*** End Patch
正确做法:正文每一行都要写 `+`(包括空行也写 `+`)。
(对应的正确 patch 示例:向空文件追加多行)
*** Begin Patch
@@ [id:1]
+
+仰望U9X·电驭苍穹
+银箭破空电光闪
+三千马力云中藏
*** End Patch
反例 B想“插入到中间”却只写 `+`(没有任何上下文/删除行)
这种块会被当成“追加到文件末尾”,结果内容跑到文件最后,不会插入到你以为的位置。
*** Begin Patch
@@ [id:1]
+# 我以为会插到某个函数上面
+print("hello")
*** End Patch
正确做法:用上下文锚定插入点(见正面案例 2
(对应的正确 patch 示例:用上下文把内容插入到函数内部,而不是追加到文件末尾)
*** Begin Patch
@@ [id:1]
def main():
config = load_config("config.json")
+ # 这里插入:启动提示(不会移动到文件末尾)
+ print("hello")
run(config)
*** End Patch
反例 C补丁在第一个 `@@` 之前出现内容 / 或漏掉 Begin/End 标记
解析会直接报格式错误(例如:“在检测到第一个 @@ 块之前出现内容”、“缺少 Begin/End 标记”)。
(错误形态示意)
这里先写了一段说明文字(没有 @@
@@ [id:1]
+...
正确做法:确保第一段非空内容必须从 `@@ [id:n]` 开始,并且整体有 Begin/End。
(对应的正确 patch 示例:完整结构、第一段内容从 @@ 块开始)
*** Begin Patch
@@ [id:1]
# ==============================
# Runtime Config
# ==============================
+# 说明:此处新增一行注释作为示例
NEW_TIMEOUT = 30
*** End Patch

View File

@ -20,8 +20,15 @@ from config import (
SUB_AGENT_TASKS_BASE_DIR,
)
from utils.logger import setup_logger
import logging
# 静音子智能体日志(交由前端提示/brief_log处理
logger = setup_logger(__name__)
logger.setLevel(logging.CRITICAL)
logger.disabled = True
logger.propagate = False
for h in list(logger.handlers):
logger.removeHandler(h)
TERMINAL_STATUSES = {"completed", "failed", "timeout"}

View File

@ -575,6 +575,8 @@ const appOptions = {
monitorShowSpeech: 'enqueueModelSpeech',
monitorShowThinking: 'enqueueModelThinking',
monitorEndModelOutput: 'endModelOutput',
monitorShowPendingReply: 'showPendingReply',
monitorPreviewTool: 'previewToolIntent',
monitorQueueTool: 'enqueueToolEvent',
monitorResolveTool: 'resolveToolResult'
}),
@ -1947,6 +1949,9 @@ const appOptions = {
this.chatAddUserMessage(message);
this.socket.emit('send_message', { message: message, conversation_id: this.currentConversationId });
if (typeof this.monitorShowPendingReply === 'function') {
this.monitorShowPendingReply();
}
this.inputClearMessage();
this.inputSetLineCount(1);
this.inputSetMultiline(false);

View File

@ -78,10 +78,15 @@
<span class="traffic-dot red"></span>
<span class="traffic-dot yellow"></span>
<span class="traffic-dot green"></span>
<span ref="terminalHeaderText">命令行</span>
<span ref="terminalHeaderText">终端</span>
</div>
<div class="terminal-body">
<div class="terminal-tabs" ref="terminalTabs">
<div class="terminal-tab-list" ref="terminalTabList"></div>
<button class="terminal-tab add-tab" ref="terminalAddButton">+</button>
</div>
<div class="terminal-output" ref="terminalBody"></div>
</div>
<div class="terminal-body" ref="terminalBody"></div>
<div class="terminal-input" ref="terminalInputLine"></div>
</div>
<div class="window reader-window" ref="readerWindow">
@ -159,7 +164,9 @@
<button data-action="save">保存网页</button>
</div>
<div class="context-menu" ref="terminalMenu">
<button data-action="sleep">暂停终端</button>
<button data-action="snapshot">保存快照</button>
<button data-action="reset">重置终端</button>
<button data-action="close">关闭终端</button>
</div>
<div class="speech-bubble" ref="bubbleEl">
@ -207,8 +214,10 @@ const editorHeaderText = ref<HTMLElement | null>(null);
const editorBody = ref<HTMLElement | null>(null);
const terminalWindow = ref<HTMLElement | null>(null);
const terminalHeaderText = ref<HTMLElement | null>(null);
const terminalTabs = ref<HTMLElement | null>(null);
const terminalTabList = ref<HTMLElement | null>(null);
const terminalAddButton = ref<HTMLElement | null>(null);
const terminalBody = ref<HTMLElement | null>(null);
const terminalInputLine = ref<HTMLElement | null>(null);
const readerWindow = ref<HTMLElement | null>(null);
const readerTitle = ref<HTMLElement | null>(null);
const readerLines = ref<HTMLElement | null>(null);
@ -278,8 +287,10 @@ const mountDirector = async () => {
editorBody: editorBody.value!,
terminalWindow: terminalWindow.value!,
terminalHeaderText: terminalHeaderText.value!,
terminalTabs: terminalTabs.value!,
terminalTabList: terminalTabList.value!,
terminalAddButton: terminalAddButton.value!,
terminalBody: terminalBody.value!,
terminalInputLine: terminalInputLine.value!,
readerWindow: readerWindow.value!,
readerTitle: readerTitle.value!,
readerLines: readerLines.value!,

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,7 @@ export interface MonitorDriver {
setDesktopRoots(roots: string[], options?: { immediate?: boolean }): void;
setManualInteractionEnabled(enabled: boolean): void;
showSpeechBubble(text: string, options?: MonitorBubbleOptions): void;
showWaitingBubble(text?: string): void;
showThinkingBubble(): void;
hideBubble(): void;
previewSceneProgress(name: string): void;

View File

@ -832,6 +832,10 @@ export async function initializeLegacySocket(ctx: any) {
ctx.toolTrackAction(data.name, action);
ctx.$forceUpdate();
ctx.conditionalScrollToBottom();
// 虚拟显示器在模型检测到工具时立即展示“正在XX”预览
if (ctx.monitorPreviewTool) {
ctx.monitorPreviewTool(data);
}
});
// 工具状态更新事件 - 实时显示详细状态

View File

@ -51,6 +51,8 @@ interface MonitorState {
playing: boolean;
awaitingTools: Record<string, string>;
lastTreeSnapshot: string[];
lastSpeechAt: number;
thinkingActive: boolean;
pendingResults: Record<string, PendingResultEntry>;
completedResults: Record<string, any>;
driver: MonitorDriver | null;
@ -127,6 +129,8 @@ export const useMonitorStore = defineStore('monitor', {
playing: false,
awaitingTools: {},
lastTreeSnapshot: [...DEFAULT_ROOTS],
lastSpeechAt: 0,
thinkingActive: false,
pendingResults: {},
completedResults: {},
driver: null,
@ -197,6 +201,7 @@ export const useMonitorStore = defineStore('monitor', {
this.queue = [];
this.playing = false;
}
this.lastSpeechAt = 0;
if (!preserveAwaitingTools) {
this.awaitingTools = {};
}
@ -213,12 +218,13 @@ export const useMonitorStore = defineStore('monitor', {
this.speechBuffer = '';
this.bubbleActive = false;
}
this.thinkingActive = false;
this.pendingProgressScene = null;
this.progressIndicator = { id: null, label: '', scene: null };
this.driver?.resetScene({ desktopRoots: this.lastTreeSnapshot, preserveBubble, preservePointer, preserveWindows });
this.driver?.setManualInteractionEnabled(!this.isLocked);
},
setProgressIndicator(payload: { id: string; label: string; scene: string }) {
setProgressIndicator(payload: { id: string | null; label: string; scene: string }) {
this.progressIndicator = {
id: payload.id,
label: payload.label,
@ -226,6 +232,37 @@ export const useMonitorStore = defineStore('monitor', {
};
monitorProgressDebug('progress-indicator:set', this.progressIndicator);
},
/**
*
* 使 id 便 tool_start/update_action
*/
previewToolIntent(payload: Record<string, any>) {
if (this.bubbleActive) {
this.hideBubble(`tool-intent:${payload?.name || 'unknown'}`);
}
this.speechBuffer = '';
const script = TOOL_SCENE_MAP[payload.name] || 'genericTool';
const progressLabel = getSceneProgressLabel(script);
if (!progressLabel) {
return;
}
this.setStatus(progressLabel);
this.progressIndicator = {
id: null,
label: progressLabel,
scene: script
};
monitorProgressDebug('preview-tool-intent', {
tool: payload.name,
script,
label: progressLabel
});
if (this.driver) {
this.driver.previewSceneProgress(script);
} else {
this.pendingProgressScene = script;
}
},
clearProgressIndicator(id?: string | null) {
if (id && this.progressIndicator.id && id !== this.progressIndicator.id) {
return;
@ -253,17 +290,29 @@ export const useMonitorStore = defineStore('monitor', {
monitorTrace('hideBubble');
this.driver?.hideBubble();
this.bubbleActive = false;
this.thinkingActive = false;
},
resetSpeechBuffer() {
monitorDebug('resetSpeechBuffer');
this.speechBuffer = '';
},
showPendingReply() {
this.setStatus('待机');
if (this.driver && typeof this.driver.showWaitingBubble === 'function') {
this.driver.showWaitingBubble('等待回复');
this.bubbleActive = true;
return;
}
this.driver?.showSpeechBubble('等待回复...', { variant: 'info', duration: 0 });
this.bubbleActive = true;
},
enqueueModelSpeech(text: string) {
if (!text) {
return;
}
this.setStatus('正在规划');
this.speechBuffer = `${this.speechBuffer}${text}`;
this.lastSpeechAt = Date.now();
monitorDebug('enqueueModelSpeech', {
incoming: text,
combined: this.speechBuffer
@ -285,19 +334,24 @@ export const useMonitorStore = defineStore('monitor', {
if (this.playing || !this.driver) {
return;
}
if (this.thinkingActive) {
monitorDebug('enqueueModelThinking already active');
return;
}
this.setStatus('思考中');
monitorDebug('enqueueModelThinking show bubble');
this.driver?.showThinkingBubble();
this.bubbleActive = true;
this.thinkingActive = true;
},
endModelOutput() {
this.setStatus('待机');
this.thinkingActive = false;
},
enqueueToolEvent(payload: Record<string, any>) {
if (this.bubbleActive) {
this.hideBubble(`tool-enqueue:${payload?.name || 'unknown'}`);
}
this.speechBuffer = '';
const now = Date.now();
const recentSpeechGap = now - this.lastSpeechAt;
const MIN_SPEECH_VISIBLE_MS = 480;
const script = TOOL_SCENE_MAP[payload.name] || 'genericTool';
const progressLabel = getSceneProgressLabel(script);
if (progressLabel) {
@ -321,12 +375,24 @@ export const useMonitorStore = defineStore('monitor', {
} else {
this.clearProgressIndicator();
}
if (this.driver) {
monitorProgressDebug('enqueueToolEvent:preview-now', { tool: payload.name, script, id });
this.driver.previewSceneProgress(script);
const doPreview = () => {
if (this.driver) {
monitorProgressDebug('enqueueToolEvent:preview-now', { tool: payload.name, script, id });
this.driver.previewSceneProgress(script);
} else {
monitorProgressDebug('enqueueToolEvent:pending-preview', { tool: payload.name, script, id });
this.pendingProgressScene = script;
}
if (this.bubbleActive) {
this.hideBubble(`tool-enqueue:${payload?.name || 'unknown'}`);
}
};
if (this.bubbleActive && recentSpeechGap >= 0 && recentSpeechGap < MIN_SPEECH_VISIBLE_MS) {
const delay = MIN_SPEECH_VISIBLE_MS - recentSpeechGap;
monitorLifecycleLog('enqueue:delay-preview-for-speech', { delay, recentSpeechGap, tool: payload.name });
setTimeout(doPreview, delay);
} else {
monitorProgressDebug('enqueueToolEvent:pending-preview', { tool: payload.name, script, id });
this.pendingProgressScene = script;
doPreview();
}
this.processQueue();
},
@ -494,53 +560,43 @@ export const useMonitorStore = defineStore('monitor', {
this.setStatus('待机');
monitorLifecycleLog('queue-end');
},
async runScript(event: MonitorEvent) {
if (!this.driver) {
this.queue.unshift(event);
return;
}
const waitKey =
event.payload?.executionId || event.payload?.execution_id || event.id || event.payload?.id || null;
let playbackResult: any = undefined;
let playbackSettled = false;
const waitStartedAt = Date.now();
monitorLifecycleLog('runScript:start', {
script: event.script,
waitKey,
payloadTool: event.payload?.name,
payloadId: event.payload?.id
});
async runScript(event: MonitorEvent) {
if (!this.driver) {
this.queue.unshift(event);
return;
}
const waitKey =
event.payload?.executionId || event.payload?.execution_id || event.id || event.payload?.id || null;
monitorLifecycleLog('runScript:start', {
script: event.script,
waitKey,
payloadTool: event.payload?.name,
payloadId: event.payload?.id
});
const waitForCompletion = async () => {
if (!waitKey) {
playbackSettled = true;
if (playbackResult === undefined) {
playbackResult = null;
}
return playbackResult;
}
if (playbackSettled) {
return playbackResult;
}
try {
playbackResult = await this.waitForResult(waitKey);
} catch (error) {
console.warn('monitor waitForResult error', error);
let playbackResult: any = undefined;
let playbackSettled = false;
const waitForCompletion = async () => {
if (!waitKey) {
playbackSettled = true;
if (playbackResult === undefined) {
playbackResult = null;
} finally {
playbackSettled = true;
}
return playbackResult;
};
await waitForCompletion();
monitorLifecycleLog('runScript:resolved', {
script: event.script,
waitKey,
waitMs: Date.now() - waitStartedAt,
hasResult: playbackResult !== undefined && playbackResult !== null,
status: playbackResult?.status
});
}
if (playbackSettled) {
return playbackResult;
}
try {
playbackResult = await this.waitForResult(waitKey);
} catch (error) {
console.warn('monitor waitForResult error', error);
playbackResult = null;
} finally {
playbackSettled = true;
}
return playbackResult;
};
const transformStatus = (raw?: string) => {
const label = typeof raw === 'string' && raw.trim().length ? raw.trim() : '进行中';

View File

@ -243,10 +243,12 @@
}
.virtual-monitor-surface .terminal-window {
width: 420px;
height: 280px;
top: 180px;
right: 360px;
width: 640px;
height: 400px;
top: 150px;
left: 120px;
display: flex;
flex-direction: column;
}
.virtual-monitor-surface .reader-window {
@ -600,8 +602,9 @@
}
.virtual-monitor-surface .terminal-window .window-header {
background: rgba(6, 12, 28, 0.9);
color: #c7dfff;
background: #e5ecff;
color: #1c2759;
border-bottom: 1px solid rgba(34, 63, 142, 0.12);
}
.virtual-monitor-surface .editor-body {
@ -683,22 +686,210 @@
}
.virtual-monitor-surface .terminal-window {
background: #050a14;
color: #def3ff;
background: linear-gradient(180deg, #f9fbff 0%, #eef2ff 100%);
color: #0c1c3f;
border-color: rgba(60, 97, 190, 0.12);
box-shadow: 0 18px 44px rgba(25, 48, 112, 0.25);
}
.virtual-monitor-surface .session-terminal-window {
width: 520px;
height: 320px;
background: linear-gradient(160deg, #f6f3ec 0%, #ffffff 40%, #f5efe2 100%);
color: #3d3929;
border: 1px solid rgba(118, 103, 84, 0.2);
box-shadow: 0 18px 40px rgba(61, 57, 41, 0.2);
display: flex;
flex-direction: column;
}
.virtual-monitor-surface .session-terminal-window .window-header {
background: rgba(255, 255, 255, 0.9);
color: #5c5243;
border-bottom: 1px solid rgba(118, 103, 84, 0.16);
}
.virtual-monitor-surface .session-terminal-output {
flex: 1;
min-height: 200px;
padding: 12px 16px 0;
background: rgba(255, 255, 255, 0.95);
overflow-y: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.55;
color: #2e2a1f;
scrollbar-width: thin;
scrollbar-color: rgba(136, 120, 99, 0.35) transparent;
box-shadow: inset 0 0 0 1px rgba(118, 103, 84, 0.14);
position: relative;
}
.virtual-monitor-surface .session-terminal-output::-webkit-scrollbar {
width: 6px;
}
.virtual-monitor-surface .session-terminal-output::-webkit-scrollbar-thumb {
background: rgba(136, 120, 99, 0.35);
border-radius: 3px;
}
.virtual-monitor-surface .session-terminal-output:empty::before {
content: '等待终端输出...';
color: #7f7766;
opacity: 0.85;
}
.virtual-monitor-surface .session-terminal-output pre {
margin: 0 0 8px 0;
white-space: pre-wrap;
word-break: break-word;
}
.virtual-monitor-surface .session-terminal-note-line {
color: #9db7d8;
font-style: italic;
}
.virtual-monitor-surface .session-terminal-prompt {
color: #da7756;
font-weight: 700;
font-size: 14px;
letter-spacing: 0.04em;
}
.virtual-monitor-surface .session-terminal-prompt-line {
color: #da7756;
font-weight: 700;
font-size: 13px;
letter-spacing: 0.03em;
margin-bottom: 6px;
}
.virtual-monitor-surface .session-terminal-input-line {
flex: 1;
min-height: 20px;
display: flex;
align-items: center;
padding: 2px 0;
font-size: 13px;
line-height: 1.4;
letter-spacing: 0.02em;
white-space: pre-wrap;
word-break: break-word;
}
.virtual-monitor-surface .terminal-body {
font-family: 'JetBrains Mono', monospace;
min-height: 160px;
background: rgba(0, 0, 0, 0.45);
border-radius: 12px;
color: inherit;
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 14px 14px;
flex: 1;
min-height: 0;
}
.virtual-monitor-surface .terminal-input {
color: #58ffb4;
padding: 0 16px 12px;
.virtual-monitor-surface .terminal-tabs {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 12px;
background: rgba(233, 239, 255, 0.9);
border: 1px solid rgba(66, 99, 181, 0.12);
}
.virtual-monitor-surface .terminal-tab-list {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
flex-wrap: wrap;
}
.virtual-monitor-surface .terminal-tab {
border: 1px solid rgba(66, 99, 181, 0.16);
background: #fff;
color: #1c2759;
border-radius: 10px;
padding: 6px 12px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.01em;
box-shadow: 0 6px 14px rgba(25, 48, 112, 0.12);
}
.virtual-monitor-surface .terminal-tab.active {
background: linear-gradient(135deg, #3056d3, #6d8dff);
color: #fff;
border-color: rgba(48, 86, 211, 0.28);
box-shadow: 0 10px 18px rgba(48, 86, 211, 0.22);
}
.virtual-monitor-surface .terminal-tab.add-tab {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 16px;
border-radius: 10px;
background: #fff;
border: 1px dashed rgba(66, 99, 181, 0.35);
color: #2f448b;
}
.virtual-monitor-surface .terminal-tab.add-tab.lonely {
margin: 0 auto;
box-shadow: 0 8px 18px rgba(25, 48, 112, 0.16);
}
.virtual-monitor-surface .terminal-output {
flex: 1;
min-height: 180px;
background: #ffffff;
border-radius: 14px;
padding: 12px 14px;
color: #1a2240;
font-family: 'JetBrains Mono', monospace;
overflow-y: auto;
box-shadow: inset 0 0 0 1px rgba(66, 99, 181, 0.12), 0 10px 18px rgba(25, 48, 112, 0.12);
scrollbar-width: thin;
scrollbar-color: rgba(133, 160, 220, 0.35) transparent;
}
.virtual-monitor-surface .terminal-output::-webkit-scrollbar {
width: 6px;
}
.virtual-monitor-surface .terminal-output::-webkit-scrollbar-track {
background: transparent;
}
.virtual-monitor-surface .terminal-output::-webkit-scrollbar-thumb {
background: rgba(80, 97, 132, 0.35);
border-radius: 3px;
}
.virtual-monitor-surface .terminal-output:empty::before {
content: '尚未创建终端,点击 + 新建';
color: #5a6b94;
opacity: 0.95;
font-size: 13px;
}
.virtual-monitor-surface .terminal-output pre {
margin: 0 0 8px 0;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
color: #1a2240;
}
.virtual-monitor-surface .terminal-output pre:last-child {
margin-bottom: 0;
}
.virtual-monitor-surface .reader-window.focused {

View File

@ -49,6 +49,7 @@ export const TOOL_ICON_MAP = Object.freeze({
extract_webpage: 'globe',
focus_file: 'eye',
modify_file: 'pencil',
write_file_diff: 'pencil',
ocr_image: 'camera',
read_file: 'book',
rename_file: 'pencil',

View File

@ -111,10 +111,20 @@ def _format_write_file_diff(result_data: Dict[str, Any], raw_text: str) -> str:
lines.append("⚠️ 失败块: " + "".join(fail_descriptions))
if len(failed_blocks) > 3:
lines.append(f"(其余 {len(failed_blocks) - 3} 个失败块略)")
# 通用排查提示:把最常见的坑点一次性说清楚,减少来回沟通成本
lines.append("🔎 排查提示(常见易错点):")
lines.append("- 是否把“要新增/要删除/要替换”的每一行都标了 `+` 或 `-`?(漏标会被当成上下文/锚点)")
lines.append("- 空行也要写成单独一行的 `+`(只有 `+` 和换行),否则空行会消失或被当成上下文导致匹配失败。")
lines.append("- 若目标文件是空文件:应使用“仅追加”写法(块内只有 `+` 行,不要混入未加前缀的正文)。")
lines.append("- 若希望在文件中间插入/替换:必须提供足够的上下文行(以空格开头)或删除行(`-`)来锚定位置,不能只贴 `+`。")
lines.append("- 是否存在空格/Tab/缩进差异、全角半角标点差异、大小写差异?上下文与原文必须字节级一致。")
lines.append("- 是否是 CRLF(\\r\\n) 与 LF(\\n) 混用导致原文匹配失败?可先用终端查看/统一换行后再补丁。")
lines.append("- 是否遗漏 `*** Begin Patch`/`*** End Patch` 或在第一个 `@@` 之前写了其它内容?")
detail_sections: List[str] = []
for item in failed_blocks:
idx = item.get("index")
reason = item.get("reason") or item.get("error") or "未说明原因"
hint = item.get("hint")
block_patch = item.get("block_patch") or item.get("patch")
if not block_patch:
old_text = item.get("old_text") or ""
@ -127,6 +137,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 hint:
detail_sections.append(f" 提示: {hint}")
if block_patch:
detail_sections.append("```diff")
detail_sections.append(block_patch.rstrip("\n"))

View File

@ -25,6 +25,41 @@ import secrets
import logging
import hmac
# 控制台输出策略:默认静默,只保留简要事件
_ORIGINAL_PRINT = print
ENABLE_VERBOSE_CONSOLE = False
def brief_log(message: str):
"""始终输出的简要日志(模型输出/工具调用等关键事件)"""
try:
_ORIGINAL_PRINT(message)
except Exception:
pass
if not ENABLE_VERBOSE_CONSOLE:
import builtins
def _silent_print(*args, **kwargs):
return
builtins.print = _silent_print
# 抑制 Flask/Werkzeug 访问日志,只保留 brief_log 输出
logging.getLogger('werkzeug').setLevel(logging.ERROR)
logging.getLogger('werkzeug').disabled = True
for noisy_logger in ('engineio.server', 'socketio.server'):
logging.getLogger(noisy_logger).setLevel(logging.ERROR)
logging.getLogger(noisy_logger).disabled = True
# 静音子智能体模块错误日志(交由 brief_log 或前端提示处理)
sub_agent_logger = logging.getLogger('modules.sub_agent_manager')
sub_agent_logger.setLevel(logging.CRITICAL)
sub_agent_logger.disabled = True
sub_agent_logger.propagate = False
for h in list(sub_agent_logger.handlers):
sub_agent_logger.removeHandler(h)
# 添加项目根目录到Python路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@ -79,7 +114,7 @@ app.config['SESSION_COOKIE_SECURE'] = _cookie_secure_env in {"1", "true", "yes"}
app.config['SESSION_COOKIE_HTTPONLY'] = True
CORS(app)
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading', logger=False, engineio_logger=False)
class EndpointFilter(logging.Filter):
@ -3696,6 +3731,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
text_started = True
text_streaming = True
sender('text_start', {})
brief_log("模型输出了内容")
await asyncio.sleep(0.05)
if not pending_append:
@ -4252,6 +4288,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
'monitor_snapshot': monitor_snapshot,
'conversation_id': conversation_id
})
brief_log(f"调用了工具: {function_name}")
await asyncio.sleep(0.3)
start_time = time.time()