feat: enhance virtual monitor command/python playback
This commit is contained in:
parent
757e1adaae
commit
8755688c8e
@ -43,6 +43,15 @@ class TerminalOperator:
|
|||||||
self._toolbox: Optional[ToolboxContainer] = None
|
self._toolbox: Optional[ToolboxContainer] = None
|
||||||
self.container_session: Optional["ContainerHandle"] = container_session
|
self.container_session: Optional["ContainerHandle"] = container_session
|
||||||
|
|
||||||
|
def _reset_toolbox(self):
|
||||||
|
"""强制关闭并重建工具终端,保证每次命令/脚本运行独立环境。"""
|
||||||
|
if self._toolbox:
|
||||||
|
try:
|
||||||
|
self._toolbox.shutdown()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._toolbox = None
|
||||||
|
|
||||||
def _detect_python_command(self) -> str:
|
def _detect_python_command(self) -> str:
|
||||||
"""
|
"""
|
||||||
自动检测可用的Python命令
|
自动检测可用的Python命令
|
||||||
@ -137,6 +146,8 @@ class TerminalOperator:
|
|||||||
Returns:
|
Returns:
|
||||||
执行结果字典
|
执行结果字典
|
||||||
"""
|
"""
|
||||||
|
# 每次执行前重置工具容器,防止上一条命令的输出/状态干扰
|
||||||
|
self._reset_toolbox()
|
||||||
# 替换命令中的python3为实际可用的命令
|
# 替换命令中的python3为实际可用的命令
|
||||||
if "python3" in command and self.python_cmd != "python3":
|
if "python3" in command and self.python_cmd != "python3":
|
||||||
command = command.replace("python3", self.python_cmd)
|
command = command.replace("python3", self.python_cmd)
|
||||||
@ -297,6 +308,9 @@ class TerminalOperator:
|
|||||||
"""
|
"""
|
||||||
timeout = timeout or CODE_EXECUTION_TIMEOUT
|
timeout = timeout or CODE_EXECUTION_TIMEOUT
|
||||||
|
|
||||||
|
# 强制重置工具容器,避免上一段代码仍在运行时输出混入
|
||||||
|
self._reset_toolbox()
|
||||||
|
|
||||||
# 创建临时Python文件
|
# 创建临时Python文件
|
||||||
temp_file = self.project_path / ".temp_code.py"
|
temp_file = self.project_path / ".temp_code.py"
|
||||||
|
|
||||||
|
|||||||
@ -89,6 +89,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="window command-window" ref="commandWindow">
|
||||||
|
<div class="window-header">
|
||||||
|
<span class="traffic-dot red"></span>
|
||||||
|
<span class="traffic-dot yellow"></span>
|
||||||
|
<span class="traffic-dot green"></span>
|
||||||
|
<span ref="commandTitle">命令行</span>
|
||||||
|
</div>
|
||||||
|
<div class="command-body">
|
||||||
|
<div class="command-input" ref="commandInput"></div>
|
||||||
|
<div class="command-output" ref="commandOutput"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="window python-window" ref="pythonWindow">
|
||||||
|
<div class="window-header">
|
||||||
|
<span class="traffic-dot red"></span>
|
||||||
|
<span class="traffic-dot yellow"></span>
|
||||||
|
<span class="traffic-dot green"></span>
|
||||||
|
<span ref="pythonTitle">Python</span>
|
||||||
|
</div>
|
||||||
|
<div class="python-body" ref="pythonBody">
|
||||||
|
<div class="python-input" ref="pythonInput"></div>
|
||||||
|
<div class="python-output" ref="pythonOutput"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="window reader-window" ref="readerWindow">
|
<div class="window reader-window" ref="readerWindow">
|
||||||
<div class="window-header">
|
<div class="window-header">
|
||||||
<span class="traffic-dot red"></span>
|
<span class="traffic-dot red"></span>
|
||||||
@ -218,6 +244,15 @@ const terminalTabs = ref<HTMLElement | null>(null);
|
|||||||
const terminalTabList = ref<HTMLElement | null>(null);
|
const terminalTabList = ref<HTMLElement | null>(null);
|
||||||
const terminalAddButton = ref<HTMLElement | null>(null);
|
const terminalAddButton = ref<HTMLElement | null>(null);
|
||||||
const terminalBody = ref<HTMLElement | null>(null);
|
const terminalBody = ref<HTMLElement | null>(null);
|
||||||
|
const commandWindow = ref<HTMLElement | null>(null);
|
||||||
|
const commandTitle = ref<HTMLElement | null>(null);
|
||||||
|
const commandInput = ref<HTMLElement | null>(null);
|
||||||
|
const commandOutput = ref<HTMLElement | null>(null);
|
||||||
|
const pythonWindow = ref<HTMLElement | null>(null);
|
||||||
|
const pythonTitle = ref<HTMLElement | null>(null);
|
||||||
|
const pythonBody = ref<HTMLElement | null>(null);
|
||||||
|
const pythonInput = ref<HTMLElement | null>(null);
|
||||||
|
const pythonOutput = ref<HTMLElement | null>(null);
|
||||||
const readerWindow = ref<HTMLElement | null>(null);
|
const readerWindow = ref<HTMLElement | null>(null);
|
||||||
const readerTitle = ref<HTMLElement | null>(null);
|
const readerTitle = ref<HTMLElement | null>(null);
|
||||||
const readerLines = ref<HTMLElement | null>(null);
|
const readerLines = ref<HTMLElement | null>(null);
|
||||||
@ -251,11 +286,11 @@ const assets = {
|
|||||||
fileIcon: new URL('../../icons/file.svg', import.meta.url).href,
|
fileIcon: new URL('../../icons/file.svg', import.meta.url).href,
|
||||||
apps: {
|
apps: {
|
||||||
browser: new URL('../../icons/globe.svg', import.meta.url).href,
|
browser: new URL('../../icons/globe.svg', import.meta.url).href,
|
||||||
terminal: new URL('../../icons/terminal.svg', import.meta.url).href,
|
terminal: new URL('../../icons/laptop.svg', import.meta.url).href,
|
||||||
command: new URL('../../icons/laptop.svg', import.meta.url).href,
|
command: new URL('../../icons/terminal.svg', import.meta.url).href,
|
||||||
|
python: new URL('../../icons/python.svg', import.meta.url).href,
|
||||||
memory: new URL('../../icons/sticky-note.svg', import.meta.url).href,
|
memory: new URL('../../icons/sticky-note.svg', import.meta.url).href,
|
||||||
todo: new URL('../../icons/clipboard.svg', import.meta.url).href,
|
todo: new URL('../../icons/clipboard.svg', import.meta.url).href,
|
||||||
reader: new URL('../../icons/book.svg', import.meta.url).href,
|
|
||||||
subagent: new URL('../../icons/bot.svg', import.meta.url).href
|
subagent: new URL('../../icons/bot.svg', import.meta.url).href
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -291,6 +326,15 @@ const mountDirector = async () => {
|
|||||||
terminalTabList: terminalTabList.value!,
|
terminalTabList: terminalTabList.value!,
|
||||||
terminalAddButton: terminalAddButton.value!,
|
terminalAddButton: terminalAddButton.value!,
|
||||||
terminalBody: terminalBody.value!,
|
terminalBody: terminalBody.value!,
|
||||||
|
commandWindow: commandWindow.value!,
|
||||||
|
commandTitle: commandTitle.value!,
|
||||||
|
commandInput: commandInput.value!,
|
||||||
|
commandOutput: commandOutput.value!,
|
||||||
|
pythonWindow: pythonWindow.value!,
|
||||||
|
pythonTitle: pythonTitle.value!,
|
||||||
|
pythonBody: pythonBody.value!,
|
||||||
|
pythonInput: pythonInput.value!,
|
||||||
|
pythonOutput: pythonOutput.value!,
|
||||||
readerWindow: readerWindow.value!,
|
readerWindow: readerWindow.value!,
|
||||||
readerTitle: readerTitle.value!,
|
readerTitle: readerTitle.value!,
|
||||||
readerLines: readerLines.value!,
|
readerLines: readerLines.value!,
|
||||||
|
|||||||
@ -44,6 +44,15 @@ export interface MonitorElements {
|
|||||||
terminalTabList: HTMLElement;
|
terminalTabList: HTMLElement;
|
||||||
terminalAddButton: HTMLElement;
|
terminalAddButton: HTMLElement;
|
||||||
terminalBody: HTMLElement;
|
terminalBody: HTMLElement;
|
||||||
|
commandWindow: HTMLElement;
|
||||||
|
commandTitle: HTMLElement;
|
||||||
|
commandInput: HTMLElement;
|
||||||
|
commandOutput: HTMLElement;
|
||||||
|
pythonWindow: HTMLElement;
|
||||||
|
pythonTitle: HTMLElement;
|
||||||
|
pythonBody: HTMLElement;
|
||||||
|
pythonInput: HTMLElement;
|
||||||
|
pythonOutput: HTMLElement;
|
||||||
readerWindow: HTMLElement;
|
readerWindow: HTMLElement;
|
||||||
readerTitle: HTMLElement;
|
readerTitle: HTMLElement;
|
||||||
readerLines: HTMLElement;
|
readerLines: HTMLElement;
|
||||||
@ -129,9 +138,9 @@ const DESKTOP_APPS: Array<{ id: string; label: string; assetKey: string }> = [
|
|||||||
{ id: 'browser', label: '浏览器', assetKey: 'browser' },
|
{ id: 'browser', label: '浏览器', assetKey: 'browser' },
|
||||||
{ id: 'terminal', label: '终端', assetKey: 'terminal' },
|
{ id: 'terminal', label: '终端', assetKey: 'terminal' },
|
||||||
{ id: 'command', label: '命令行', assetKey: 'command' },
|
{ id: 'command', label: '命令行', assetKey: 'command' },
|
||||||
|
{ id: 'python', label: 'Python', assetKey: 'python' },
|
||||||
{ id: 'memory', label: '记忆', assetKey: 'memory' },
|
{ id: 'memory', label: '记忆', assetKey: 'memory' },
|
||||||
{ id: 'todo', label: '看板', assetKey: 'todo' },
|
{ id: 'todo', label: '看板', assetKey: 'todo' },
|
||||||
{ id: 'reader', label: '阅读器', assetKey: 'reader' },
|
|
||||||
{ id: 'subagent', label: '子代理', assetKey: 'subagent' }
|
{ id: 'subagent', label: '子代理', assetKey: 'subagent' }
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -190,6 +199,7 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
private folderIcons = new Map<string, HTMLElement>();
|
private folderIcons = new Map<string, HTMLElement>();
|
||||||
// 用于控制编辑器动画全局加速(根据本次补丁的总体改动量动态调整)
|
// 用于控制编辑器动画全局加速(根据本次补丁的总体改动量动态调整)
|
||||||
private editorSpeedBoost = 1;
|
private editorSpeedBoost = 1;
|
||||||
|
private pythonRunToken = 0;
|
||||||
private pendingDesktopFolders = new Set<string>();
|
private pendingDesktopFolders = new Set<string>();
|
||||||
private fileIcons = new Map<string, HTMLElement>();
|
private fileIcons = new Map<string, HTMLElement>();
|
||||||
private browserResultMap = new Map<string, HTMLLIElement>();
|
private browserResultMap = new Map<string, HTMLLIElement>();
|
||||||
@ -218,6 +228,7 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
placeholder: false
|
placeholder: false
|
||||||
};
|
};
|
||||||
private editorSnapshots = new Map<string, string[]>();
|
private editorSnapshots = new Map<string, string[]>();
|
||||||
|
private commandCurrentText = '';
|
||||||
private progressBubbleTimer: number | null = null;
|
private progressBubbleTimer: number | null = null;
|
||||||
private progressBubbleBase: string | null = null;
|
private progressBubbleBase: string | null = null;
|
||||||
private progressSceneName: string | null = null;
|
private progressSceneName: string | null = null;
|
||||||
@ -226,9 +237,27 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
private waitingBubbleTimer: number | null = null;
|
private waitingBubbleTimer: number | null = null;
|
||||||
private waitingBubbleBase: string | null = null;
|
private waitingBubbleBase: string | null = null;
|
||||||
private progressBubbleActive = false;
|
private progressBubbleActive = false;
|
||||||
|
// 当实际执行进度快于动画播放时,用于压制“正在 xxx”提示
|
||||||
|
private playbackLagging = false;
|
||||||
|
// 记录最近一次 Python 执行的ID,用于丢弃过期动画/结果
|
||||||
|
private latestPythonExecutionId: string | number | null = null;
|
||||||
private lastTerminalSessionId: string | null = null;
|
private lastTerminalSessionId: string | null = null;
|
||||||
private terminalLastFocusedAt = 0;
|
private terminalLastFocusedAt = 0;
|
||||||
|
|
||||||
|
private refreshScreenRect() {
|
||||||
|
const prev = this.screenRect;
|
||||||
|
const rect = this.elements.screen.getBoundingClientRect();
|
||||||
|
this.screenRect = rect;
|
||||||
|
if (prev && prev.width > 0 && prev.height > 0) {
|
||||||
|
const relX = this.pointerBase.x / prev.width;
|
||||||
|
const relY = this.pointerBase.y / prev.height;
|
||||||
|
this.pointerBase = {
|
||||||
|
x: relX * rect.width,
|
||||||
|
y: relY * rect.height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private applySceneStatus(runtime: MonitorSceneRuntime, sceneName: string, fallback: string) {
|
private applySceneStatus(runtime: MonitorSceneRuntime, sceneName: string, fallback: string) {
|
||||||
if (!runtime || typeof runtime.setStatus !== 'function') {
|
if (!runtime || typeof runtime.setStatus !== 'function') {
|
||||||
return;
|
return;
|
||||||
@ -256,7 +285,7 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
(window as any).__TERMINAL_MENU_DEBUG_BUILD = '2025-12-13-1';
|
(window as any).__TERMINAL_MENU_DEBUG_BUILD = '2025-12-13-1';
|
||||||
terminalMenuDebug('constructor:init', { build: (window as any).__TERMINAL_MENU_DEBUG_BUILD });
|
terminalMenuDebug('constructor:init', { build: (window as any).__TERMINAL_MENU_DEBUG_BUILD });
|
||||||
const resizeHandler = () => {
|
const resizeHandler = () => {
|
||||||
this.screenRect = this.elements.screen.getBoundingClientRect();
|
this.refreshScreenRect();
|
||||||
this.layoutFloatingWindows();
|
this.layoutFloatingWindows();
|
||||||
};
|
};
|
||||||
window.addEventListener('resize', resizeHandler, { passive: true });
|
window.addEventListener('resize', resizeHandler, { passive: true });
|
||||||
@ -502,11 +531,19 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
if (progressLabel && !isPlaybackPhase) {
|
if (progressLabel && !isPlaybackPhase) {
|
||||||
ensureProgressBubble();
|
ensureProgressBubble();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 若进入播放阶段且有执行结果已完成,压制“正在…”提示
|
||||||
|
if (isPlaybackPhase) {
|
||||||
|
this.playbackLagging = true;
|
||||||
|
} else {
|
||||||
|
this.playbackLagging = false;
|
||||||
|
}
|
||||||
|
|
||||||
const wrappedRuntime: MonitorSceneRuntime = {
|
const wrappedRuntime: MonitorSceneRuntime = {
|
||||||
...runtime,
|
...runtime,
|
||||||
waitForResult: async (id?: string | number | null) => {
|
waitForResult: async (id?: string | number | null) => {
|
||||||
const waitFn = runtime.waitForResult || (() => Promise.resolve(null));
|
const waitFn = runtime.waitForResult || (() => Promise.resolve(null));
|
||||||
if (!isPlaybackPhase) {
|
if (!isPlaybackPhase && !this.playbackLagging) {
|
||||||
ensureProgressBubble();
|
ensureProgressBubble();
|
||||||
}
|
}
|
||||||
const waitKey = id ?? payload?.executionId ?? payload?.id;
|
const waitKey = id ?? payload?.executionId ?? payload?.id;
|
||||||
@ -514,6 +551,7 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
try {
|
try {
|
||||||
const result = await waitFn(id);
|
const result = await waitFn(id);
|
||||||
progressDebug('playScene:waitForResult:resolved', { scene: name, id: waitKey });
|
progressDebug('playScene:waitForResult:resolved', { scene: name, id: waitKey });
|
||||||
|
this.playbackLagging = false;
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
clearProgressBubble();
|
clearProgressBubble();
|
||||||
@ -527,6 +565,7 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
monitorLifecycleDebug('playScene:end', {
|
monitorLifecycleDebug('playScene:end', {
|
||||||
scene: name
|
scene: name
|
||||||
});
|
});
|
||||||
|
this.playbackLagging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1189,6 +1228,157 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
return top === instance.element;
|
return top === instance.element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resetCommandWindow(title = '命令行', options: { clearOutput?: boolean } = {}) {
|
||||||
|
if (this.elements.commandTitle) {
|
||||||
|
this.elements.commandTitle.textContent = title;
|
||||||
|
}
|
||||||
|
if (this.elements.commandInput) {
|
||||||
|
this.elements.commandInput.textContent = '';
|
||||||
|
}
|
||||||
|
if (options.clearOutput && this.elements.commandOutput) {
|
||||||
|
this.elements.commandOutput.innerHTML = '';
|
||||||
|
}
|
||||||
|
this.commandCurrentText = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async revealCommandWindow(title = '命令行', options: { reset?: boolean; focusInput?: boolean } = {}) {
|
||||||
|
const { reset = true, focusInput = false } = options;
|
||||||
|
const visible = this.isWindowVisible(this.elements.commandWindow);
|
||||||
|
if (!visible) {
|
||||||
|
await this.movePointerToApp('command');
|
||||||
|
await this.click();
|
||||||
|
}
|
||||||
|
if (reset) {
|
||||||
|
this.resetCommandWindow(title, { clearOutput: true });
|
||||||
|
} else {
|
||||||
|
if (this.elements.commandTitle) {
|
||||||
|
this.elements.commandTitle.textContent = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.showWindow(this.elements.commandWindow);
|
||||||
|
if (focusInput) {
|
||||||
|
await this.focusCommandInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async focusCommandInput() {
|
||||||
|
if (!this.elements.commandInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.movePointerToElement(this.elements.commandInput, { duration: 360 });
|
||||||
|
await this.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async typeCommandText(text: string) {
|
||||||
|
if (!this.elements.commandInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = this.elements.commandInput;
|
||||||
|
await this.focusCommandInput();
|
||||||
|
const toDelete = this.commandCurrentText;
|
||||||
|
if (toDelete) {
|
||||||
|
for (let i = toDelete.length; i > 0; i -= 1) {
|
||||||
|
target.textContent = toDelete.slice(0, i - 1);
|
||||||
|
await sleep(18);
|
||||||
|
}
|
||||||
|
this.commandCurrentText = '';
|
||||||
|
}
|
||||||
|
const chars = Array.from(text);
|
||||||
|
for (const ch of chars) {
|
||||||
|
target.textContent = (target.textContent || '') + ch;
|
||||||
|
await sleep(28);
|
||||||
|
}
|
||||||
|
this.commandCurrentText = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private appendCommandOutput(lines: string[]) {
|
||||||
|
if (!this.elements.commandOutput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
lines.forEach(line => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'command-line';
|
||||||
|
row.textContent = line;
|
||||||
|
frag.appendChild(row);
|
||||||
|
});
|
||||||
|
this.elements.commandOutput.appendChild(frag);
|
||||||
|
this.elements.commandOutput.scrollTop = this.elements.commandOutput.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetPythonWindow(title = 'Python') {
|
||||||
|
if (this.elements.pythonTitle) {
|
||||||
|
this.elements.pythonTitle.textContent = title;
|
||||||
|
}
|
||||||
|
if (this.elements.pythonInput) {
|
||||||
|
this.elements.pythonInput.textContent = '';
|
||||||
|
}
|
||||||
|
if (this.elements.pythonOutput) {
|
||||||
|
this.elements.pythonOutput.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private appendPythonOutput(label: string, content: string) {
|
||||||
|
if (!this.elements.pythonOutput) return;
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'python-block output';
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.className = 'python-block-title';
|
||||||
|
title.textContent = label;
|
||||||
|
const pre = document.createElement('pre');
|
||||||
|
pre.textContent = content;
|
||||||
|
wrapper.appendChild(title);
|
||||||
|
wrapper.appendChild(pre);
|
||||||
|
this.elements.pythonOutput.innerHTML = '';
|
||||||
|
this.elements.pythonOutput.appendChild(wrapper);
|
||||||
|
this.scrollPythonToResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async revealPythonWindow(title = 'Python') {
|
||||||
|
await this.movePointerToApp('python');
|
||||||
|
await this.click();
|
||||||
|
this.resetPythonWindow(title);
|
||||||
|
this.showWindow(this.elements.pythonWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async focusPythonInput() {
|
||||||
|
if (!this.elements.pythonInput) return;
|
||||||
|
await this.movePointerToElement(this.elements.pythonInput, { duration: 360 });
|
||||||
|
await this.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async typePythonCode(code: string, options: { deletePrevious?: boolean; animate?: boolean } = {}) {
|
||||||
|
if (!this.elements.pythonInput) return;
|
||||||
|
const { deletePrevious = true, animate = true } = options;
|
||||||
|
const target = this.elements.pythonInput;
|
||||||
|
if (deletePrevious) {
|
||||||
|
target.textContent = '';
|
||||||
|
}
|
||||||
|
await this.focusPythonInput();
|
||||||
|
if (!animate) {
|
||||||
|
target.textContent = code;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chars = Array.from(code);
|
||||||
|
for (const ch of chars) {
|
||||||
|
target.textContent = (target.textContent || '') + ch;
|
||||||
|
await sleep(24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollPythonToTop() {
|
||||||
|
if (this.elements.pythonBody) {
|
||||||
|
this.elements.pythonBody.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollPythonToResult() {
|
||||||
|
if (this.elements.pythonBody && this.elements.pythonOutput) {
|
||||||
|
const top = this.elements.pythonOutput.offsetTop - this.elements.pythonBody.offsetTop;
|
||||||
|
this.elements.pythonBody.scrollTo({ top, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async revealTerminalWindow(instance: TerminalShell, title: string) {
|
private async revealTerminalWindow(instance: TerminalShell, title: string) {
|
||||||
await this.movePointerToApp('terminal');
|
await this.movePointerToApp('terminal');
|
||||||
await this.click();
|
await this.click();
|
||||||
@ -1675,6 +1865,8 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
this.windowAnchors.set(this.elements.folderWindow, { x: 0.68, y: 0.26 });
|
this.windowAnchors.set(this.elements.folderWindow, { x: 0.68, y: 0.26 });
|
||||||
this.windowAnchors.set(this.elements.editorWindow, { x: 0.62, y: 0.58 });
|
this.windowAnchors.set(this.elements.editorWindow, { x: 0.62, y: 0.58 });
|
||||||
this.windowAnchors.set(this.elements.terminalWindow, { x: 0.18, y: 0.42 });
|
this.windowAnchors.set(this.elements.terminalWindow, { x: 0.18, y: 0.42 });
|
||||||
|
this.windowAnchors.set(this.elements.commandWindow, { x: 0.2, y: 0.7 });
|
||||||
|
this.windowAnchors.set(this.elements.pythonWindow, { x: 0.52, y: 0.16 });
|
||||||
this.windowAnchors.set(this.elements.readerWindow, { x: 0.42, y: 0.05 });
|
this.windowAnchors.set(this.elements.readerWindow, { x: 0.42, y: 0.05 });
|
||||||
this.windowAnchors.set(this.elements.memoryWindow, { x: 0.28, y: 0.32 });
|
this.windowAnchors.set(this.elements.memoryWindow, { x: 0.28, y: 0.32 });
|
||||||
this.windowAnchors.set(this.elements.todoWindow, { x: 0.5, y: 0.32 });
|
this.windowAnchors.set(this.elements.todoWindow, { x: 0.5, y: 0.32 });
|
||||||
@ -2099,9 +2291,18 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
|
|
||||||
this.sceneHandlers.runCommand = async (payload, runtime) => {
|
this.sceneHandlers.runCommand = async (payload, runtime) => {
|
||||||
this.applySceneStatus(runtime, 'runCommand', '正在执行命令');
|
this.applySceneStatus(runtime, 'runCommand', '正在执行命令');
|
||||||
const { sessionId } = await this.ensureTerminalSessionReady(payload, { focusPrompt: true, createIfMissing: true });
|
const command = payload?.arguments?.command || payload?.result?.command || 'echo \"Hello\"';
|
||||||
const command = payload?.arguments?.command || 'echo "Hello"';
|
const reuse = this.isWindowVisible(this.elements.commandWindow);
|
||||||
await this.typeSessionCommand(sessionId, command);
|
if (reuse) {
|
||||||
|
this.showWindow(this.elements.commandWindow);
|
||||||
|
await this.focusCommandInput();
|
||||||
|
if (this.elements.commandOutput) {
|
||||||
|
this.elements.commandOutput.innerHTML = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.revealCommandWindow('命令行', { reset: true, focusInput: true });
|
||||||
|
}
|
||||||
|
await this.typeCommandText(command);
|
||||||
const completion = await runtime.waitForResult(payload.executionId || payload.id);
|
const completion = await runtime.waitForResult(payload.executionId || payload.id);
|
||||||
const output = completion?.result?.output || completion?.result?.stdout || '命令执行完成';
|
const output = completion?.result?.output || completion?.result?.stdout || '命令执行完成';
|
||||||
const lines = this.sanitizeTerminalOutput(
|
const lines = this.sanitizeTerminalOutput(
|
||||||
@ -2111,16 +2312,47 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
? output.map(String)
|
? output.map(String)
|
||||||
: [String(output || '')]
|
: [String(output || '')]
|
||||||
);
|
);
|
||||||
this.appendTerminalOutputs(sessionId, command, lines.length ? lines : ['命令执行完成']);
|
this.appendCommandOutput(lines.length ? lines : ['命令执行完成']);
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sceneHandlers.runPython = async (payload, runtime) => {
|
this.sceneHandlers.runPython = async (payload, runtime) => {
|
||||||
this.applySceneStatus(runtime, 'runPython', '正在执行 Python');
|
this.applySceneStatus(runtime, 'runPython', '正在执行 Python');
|
||||||
const { sessionId } = await this.ensureTerminalSessionReady(payload, { focusPrompt: true, createIfMissing: true });
|
const runId =
|
||||||
const code = payload?.arguments?.code || 'print("Hello")';
|
payload?.executionId ||
|
||||||
await this.typeSessionCommand(sessionId, code);
|
payload?.execution_id ||
|
||||||
|
payload?.id ||
|
||||||
|
`${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||||
|
// 丢弃过期的回放任务
|
||||||
|
if (this.latestPythonExecutionId && this.latestPythonExecutionId !== runId) {
|
||||||
|
const older =
|
||||||
|
typeof runId === 'number' && typeof this.latestPythonExecutionId === 'number'
|
||||||
|
? runId < this.latestPythonExecutionId
|
||||||
|
: false;
|
||||||
|
if (older) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.latestPythonExecutionId = runId;
|
||||||
|
const code = payload?.arguments?.code || 'print(\"Hello\")';
|
||||||
|
const codeLines = this.normalizeLines(code);
|
||||||
|
const animate = codeLines.length <= 15;
|
||||||
|
const runToken = ++this.pythonRunToken;
|
||||||
|
const reuse = this.isWindowVisible(this.elements.pythonWindow);
|
||||||
|
if (reuse) {
|
||||||
|
this.showWindow(this.elements.pythonWindow);
|
||||||
|
this.scrollPythonToTop();
|
||||||
|
await this.focusPythonInput();
|
||||||
|
this.elements.pythonOutput.innerHTML = '';
|
||||||
|
} else {
|
||||||
|
await this.revealPythonWindow('Python');
|
||||||
|
}
|
||||||
|
await this.typePythonCode(code, { deletePrevious: true, animate });
|
||||||
const completion = await runtime.waitForResult(payload.executionId || payload.id);
|
const completion = await runtime.waitForResult(payload.executionId || payload.id);
|
||||||
|
if (runToken !== this.pythonRunToken) {
|
||||||
|
// 有新的 Python 运行已启动,本次结果丢弃
|
||||||
|
return;
|
||||||
|
}
|
||||||
const output = completion?.result?.output || completion?.result?.stdout || '>>> 执行完成';
|
const output = completion?.result?.output || completion?.result?.stdout || '>>> 执行完成';
|
||||||
const lines = this.sanitizeTerminalOutput(
|
const lines = this.sanitizeTerminalOutput(
|
||||||
typeof output === 'string'
|
typeof output === 'string'
|
||||||
@ -2129,7 +2361,7 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
? output.map(String)
|
? output.map(String)
|
||||||
: [String(output || '')]
|
: [String(output || '')]
|
||||||
);
|
);
|
||||||
this.appendTerminalOutputs(sessionId, code, lines.length ? lines : ['>>> 执行完成']);
|
this.appendPythonOutput('输出', lines.join('\n') || '>>> 执行完成');
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2771,6 +3003,7 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private async movePointerToApp(appId: string) {
|
private async movePointerToApp(appId: string) {
|
||||||
|
this.refreshScreenRect();
|
||||||
const icon = this.appIcons.get(appId);
|
const icon = this.appIcons.get(appId);
|
||||||
if (icon) {
|
if (icon) {
|
||||||
await this.movePointerToElement(icon);
|
await this.movePointerToElement(icon);
|
||||||
@ -2780,6 +3013,7 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async movePointerToDesktop() {
|
private async movePointerToDesktop() {
|
||||||
|
this.refreshScreenRect();
|
||||||
return this.movePointerToElement(this.elements.desktopGrid, { offsetX: 160, offsetY: 120, duration: 700 });
|
return this.movePointerToElement(this.elements.desktopGrid, { offsetX: 160, offsetY: 120, duration: 700 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2787,6 +3021,7 @@ export class MonitorDirector implements MonitorDriver {
|
|||||||
if (!target) {
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.refreshScreenRect();
|
||||||
this.raiseWindowForTarget(target);
|
this.raiseWindowForTarget(target);
|
||||||
if (!this.progressBubbleBase) {
|
if (!this.progressBubbleBase) {
|
||||||
this.dismissBubble(true);
|
this.dismissBubble(true);
|
||||||
|
|||||||
@ -831,6 +831,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 工具准备中事件 - 实时显示
|
// 工具准备中事件 - 实时显示
|
||||||
ctx.socket.on('tool_preparing', (data) => {
|
ctx.socket.on('tool_preparing', (data) => {
|
||||||
|
if (ctx.dropToolEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
socketLog('工具准备中:', data.name);
|
socketLog('工具准备中:', data.name);
|
||||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||||
socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id);
|
socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id);
|
||||||
@ -876,6 +879,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 工具状态更新事件 - 实时显示详细状态
|
// 工具状态更新事件 - 实时显示详细状态
|
||||||
ctx.socket.on('tool_status', (data) => {
|
ctx.socket.on('tool_status', (data) => {
|
||||||
|
if (ctx.dropToolEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
socketLog('工具状态:', data);
|
socketLog('工具状态:', data);
|
||||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||||
socketLog('跳过tool_status(对话不匹配)', data.conversation_id);
|
socketLog('跳过tool_status(对话不匹配)', data.conversation_id);
|
||||||
@ -899,6 +905,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 工具开始(从准备转为执行)
|
// 工具开始(从准备转为执行)
|
||||||
ctx.socket.on('tool_start', (data) => {
|
ctx.socket.on('tool_start', (data) => {
|
||||||
|
if (ctx.dropToolEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
socketLog('工具开始执行:', data.name);
|
socketLog('工具开始执行:', data.name);
|
||||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||||
socketLog('跳过tool_start(对话不匹配)', data.conversation_id);
|
socketLog('跳过tool_start(对话不匹配)', data.conversation_id);
|
||||||
@ -956,6 +965,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 更新action(工具完成)
|
// 更新action(工具完成)
|
||||||
ctx.socket.on('update_action', (data) => {
|
ctx.socket.on('update_action', (data) => {
|
||||||
|
if (ctx.dropToolEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
socketLog('更新action:', data.id, 'status:', data.status);
|
socketLog('更新action:', data.id, 'status:', data.status);
|
||||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||||
socketLog('跳过update_action(对话不匹配)', data.conversation_id);
|
socketLog('跳过update_action(对话不匹配)', data.conversation_id);
|
||||||
@ -1047,6 +1059,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctx.socket.on('append_payload', (data) => {
|
ctx.socket.on('append_payload', (data) => {
|
||||||
|
if (ctx.dropToolEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
socketLog('收到append_payload事件:', data);
|
socketLog('收到append_payload事件:', data);
|
||||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||||
socketLog('跳过append_payload(对话不匹配)', data.conversation_id);
|
socketLog('跳过append_payload(对话不匹配)', data.conversation_id);
|
||||||
@ -1064,6 +1079,9 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctx.socket.on('modify_payload', (data) => {
|
ctx.socket.on('modify_payload', (data) => {
|
||||||
|
if (ctx.dropToolEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
socketLog('收到modify_payload事件:', data);
|
socketLog('收到modify_payload事件:', data);
|
||||||
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
if (data?.conversation_id && data.conversation_id !== ctx.currentConversationId) {
|
||||||
socketLog('跳过modify_payload(对话不匹配)', data.conversation_id);
|
socketLog('跳过modify_payload(对话不匹配)', data.conversation_id);
|
||||||
@ -1084,6 +1102,13 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
ctx.socket.on('stop_requested', (data) => {
|
ctx.socket.on('stop_requested', (data) => {
|
||||||
socketLog('停止请求已接收:', data.message);
|
socketLog('停止请求已接收:', data.message);
|
||||||
// 可以显示提示信息
|
// 可以显示提示信息
|
||||||
|
try {
|
||||||
|
if (typeof ctx.clearPendingTools === 'function') {
|
||||||
|
ctx.clearPendingTools('socket:stop_requested');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('清理未完成工具失败', error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 任务停止
|
// 任务停止
|
||||||
|
|||||||
@ -251,6 +251,158 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .command-window {
|
||||||
|
width: 520px;
|
||||||
|
height: 260px;
|
||||||
|
top: 540px;
|
||||||
|
left: 120px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f7f9fd;
|
||||||
|
color: #0f172a;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .command-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
background: #f7f9fd;
|
||||||
|
color: #0f172a;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
border-top: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
padding: 8px 10px 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .command-input {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
background: #ffffff;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.08);
|
||||||
|
min-height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .command-output {
|
||||||
|
padding: 12px 14px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #eef2f9;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
flex: 1;
|
||||||
|
max-height: 100%;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .command-line {
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .command-line.highlight {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .python-window {
|
||||||
|
width: 520px;
|
||||||
|
height: 320px;
|
||||||
|
top: 120px;
|
||||||
|
left: 520px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #f9fbff;
|
||||||
|
color: #0f172a;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .python-body {
|
||||||
|
flex: 1;
|
||||||
|
background: #f1f5fb;
|
||||||
|
color: #0f172a;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
border-top: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
box-sizing: border-box;
|
||||||
|
scrollbar-width: none;
|
||||||
|
min-height: 0; /* 防止子元素撑开导致整体溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .python-body::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .python-input {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
min-height: 64px;
|
||||||
|
max-height: 180px;
|
||||||
|
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.08);
|
||||||
|
line-height: 1.55;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.virtual-monitor-surface .python-input::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .python-output {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 140px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.virtual-monitor-surface .python-output::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .python-block {
|
||||||
|
background: #f6f8fe;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .python-block-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d4ed8;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.virtual-monitor-surface .python-block pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
.virtual-monitor-surface .reader-window {
|
.virtual-monitor-surface .reader-window {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
height: 320px;
|
height: 320px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user