feat: virtual monitor
This commit is contained in:
parent
2f75c1c8bb
commit
053db95fee
File diff suppressed because one or more lines are too long
237
doc/write_file_diff_description.txt
Normal file
237
doc/write_file_diff_description.txt
Normal 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
|
||||
@ -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"}
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
// 工具状态更新事件 - 实时显示详细状态
|
||||
|
||||
@ -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() : '进行中';
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user