fix: run sub-agent tools inside container
This commit is contained in:
parent
f3179e2a97
commit
96d0e68347
@ -124,7 +124,8 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
self.todo_manager = TodoManager(self.context_manager)
|
self.todo_manager = TodoManager(self.context_manager)
|
||||||
self.sub_agent_manager = SubAgentManager(
|
self.sub_agent_manager = SubAgentManager(
|
||||||
project_path=self.project_path,
|
project_path=self.project_path,
|
||||||
data_dir=str(self.data_dir)
|
data_dir=str(self.data_dir),
|
||||||
|
container_session=container_session,
|
||||||
)
|
)
|
||||||
self.easter_egg_manager = EasterEggManager()
|
self.easter_egg_manager = EasterEggManager()
|
||||||
self._announced_sub_agent_tasks = set()
|
self._announced_sub_agent_tasks = set()
|
||||||
@ -245,6 +246,11 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
|
|||||||
self.terminal_ops.set_container_session(session)
|
self.terminal_ops.set_container_session(session)
|
||||||
if getattr(self, "file_manager", None):
|
if getattr(self, "file_manager", None):
|
||||||
self.file_manager.set_container_session(session)
|
self.file_manager.set_container_session(session)
|
||||||
|
if getattr(self, "sub_agent_manager", None):
|
||||||
|
try:
|
||||||
|
self.sub_agent_manager.set_container_session(session)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _ensure_conversation(self):
|
def _ensure_conversation(self):
|
||||||
"""确保CLI模式下存在可用的对话ID"""
|
"""确保CLI模式下存在可用的对话ID"""
|
||||||
|
|||||||
354
easyagent/src/tools/container_bridge.js
Normal file
354
easyagent/src/tools/container_bridge.js
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
function getContainerContext() {
|
||||||
|
const name = process.env.EASYAGENT_CONTAINER_NAME;
|
||||||
|
if (!name) return null;
|
||||||
|
return {
|
||||||
|
containerName: name,
|
||||||
|
dockerBin: process.env.EASYAGENT_DOCKER_BIN || 'docker',
|
||||||
|
mountPath: (process.env.EASYAGENT_CONTAINER_MOUNT_PATH || '/workspace').replace(/\/+$/, '') || '/workspace',
|
||||||
|
containerVenv: process.env.EASYAGENT_CONTAINER_VENV || '/opt/agent-venv',
|
||||||
|
containerPython: process.env.EASYAGENT_CONTAINER_PYTHON || '/opt/agent-venv/bin/python3',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePosix(p) {
|
||||||
|
return p.split(path.sep).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveContainerPath(workspace, inputPath, ctx) {
|
||||||
|
const hostRoot = path.resolve(workspace);
|
||||||
|
const containerRoot = ctx.mountPath;
|
||||||
|
let hostPath = hostRoot;
|
||||||
|
let containerPath = containerRoot;
|
||||||
|
|
||||||
|
if (inputPath) {
|
||||||
|
if (path.isAbsolute(inputPath)) {
|
||||||
|
if (inputPath.startsWith(containerRoot)) {
|
||||||
|
containerPath = inputPath;
|
||||||
|
const rel = path.posix.relative(containerRoot, inputPath);
|
||||||
|
hostPath = path.join(hostRoot, ...rel.split('/'));
|
||||||
|
} else if (inputPath.startsWith(hostRoot)) {
|
||||||
|
hostPath = inputPath;
|
||||||
|
const rel = path.relative(hostRoot, inputPath);
|
||||||
|
containerPath = path.posix.join(containerRoot, normalizePosix(rel));
|
||||||
|
} else {
|
||||||
|
return { error: '路径必须位于工作区内' };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hostPath = path.resolve(hostRoot, inputPath);
|
||||||
|
if (!hostPath.startsWith(hostRoot)) {
|
||||||
|
return { error: '路径必须位于工作区内' };
|
||||||
|
}
|
||||||
|
const rel = path.relative(hostRoot, hostPath);
|
||||||
|
containerPath = path.posix.join(containerRoot, normalizePosix(rel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const relContainer = path.posix.relative(containerRoot, containerPath);
|
||||||
|
return {
|
||||||
|
hostRoot,
|
||||||
|
hostPath,
|
||||||
|
containerPath,
|
||||||
|
relativePath: relContainer || '.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONTAINER_SCRIPT = `
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import fnmatch
|
||||||
|
|
||||||
|
def _resolve(root: pathlib.Path, rel: str) -> pathlib.Path:
|
||||||
|
base = root.resolve()
|
||||||
|
target = (base / rel).resolve()
|
||||||
|
if not str(target).startswith(str(base)):
|
||||||
|
raise ValueError("路径越界: %s" % rel)
|
||||||
|
return target
|
||||||
|
|
||||||
|
def _read_text(target: pathlib.Path):
|
||||||
|
with target.open('r', encoding='utf-8', errors='ignore') as fh:
|
||||||
|
data = fh.read()
|
||||||
|
lines = data.splitlines(keepends=True)
|
||||||
|
return data, lines
|
||||||
|
|
||||||
|
def _ensure_file(target: pathlib.Path):
|
||||||
|
if not target.exists():
|
||||||
|
return {"success": False, "error": "文件不存在"}
|
||||||
|
if not target.is_file():
|
||||||
|
return {"success": False, "error": "不是文件"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _read_text_segment(root, payload):
|
||||||
|
rel = payload.get("path")
|
||||||
|
start = payload.get("start_line")
|
||||||
|
end = payload.get("end_line")
|
||||||
|
target = _resolve(root, rel)
|
||||||
|
err = _ensure_file(target)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
data, lines = _read_text(target)
|
||||||
|
total = len(lines)
|
||||||
|
line_start = start if start and start > 0 else 1
|
||||||
|
line_end = end if end and end >= line_start else total
|
||||||
|
if line_start > total:
|
||||||
|
return {"success": False, "error": "起始行超出文件长度"}
|
||||||
|
line_end = min(line_end, total)
|
||||||
|
snippet = "".join(lines[line_start - 1 : line_end])
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"path": rel,
|
||||||
|
"content": snippet,
|
||||||
|
"line_start": line_start,
|
||||||
|
"line_end": line_end,
|
||||||
|
"total_lines": total
|
||||||
|
}
|
||||||
|
|
||||||
|
def _read_file(root, payload):
|
||||||
|
rel = payload.get("path")
|
||||||
|
target = _resolve(root, rel)
|
||||||
|
err = _ensure_file(target)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
with target.open('r', encoding='utf-8', errors='ignore') as fh:
|
||||||
|
content = fh.read()
|
||||||
|
return {"success": True, "path": rel, "content": content, "size": len(content)}
|
||||||
|
|
||||||
|
def _write_file(root, payload):
|
||||||
|
rel = payload.get("path")
|
||||||
|
content = payload.get("content") or ""
|
||||||
|
mode = payload.get("mode") or "w"
|
||||||
|
target = _resolve(root, rel)
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with target.open(mode, encoding='utf-8') as fh:
|
||||||
|
fh.write(content)
|
||||||
|
return {"success": True, "path": rel, "size": len(content), "mode": mode}
|
||||||
|
|
||||||
|
def _search_text(root, payload):
|
||||||
|
rel = payload.get("path")
|
||||||
|
target = _resolve(root, rel)
|
||||||
|
err = _ensure_file(target)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
data, lines = _read_text(target)
|
||||||
|
total = len(lines)
|
||||||
|
query = payload.get("query") or ""
|
||||||
|
if not query:
|
||||||
|
return {"success": False, "error": "缺少搜索关键词"}
|
||||||
|
max_matches = payload.get("max_matches") or 20
|
||||||
|
before = payload.get("context_before") or 0
|
||||||
|
after = payload.get("context_after") or 0
|
||||||
|
case_sensitive = bool(payload.get("case_sensitive"))
|
||||||
|
query_cmp = query if case_sensitive else query.lower()
|
||||||
|
|
||||||
|
def contains(text):
|
||||||
|
text_cmp = text if case_sensitive else text.lower()
|
||||||
|
return query_cmp in text_cmp
|
||||||
|
|
||||||
|
matches = []
|
||||||
|
for idx, line in enumerate(lines, start=1):
|
||||||
|
if contains(line):
|
||||||
|
win_start = max(1, idx - before)
|
||||||
|
win_end = min(total, idx + after)
|
||||||
|
if matches and win_start <= matches[-1]["line_end"]:
|
||||||
|
matches[-1]["line_end"] = max(matches[-1]["line_end"], win_end)
|
||||||
|
matches[-1]["hits"].append(idx)
|
||||||
|
else:
|
||||||
|
if len(matches) >= max_matches:
|
||||||
|
break
|
||||||
|
matches.append({
|
||||||
|
"line_start": win_start,
|
||||||
|
"line_end": win_end,
|
||||||
|
"hits": [idx]
|
||||||
|
})
|
||||||
|
for window in matches:
|
||||||
|
snippet_lines = lines[window["line_start"] - 1 : window["line_end"]]
|
||||||
|
window["snippet"] = "".join(snippet_lines)
|
||||||
|
|
||||||
|
return {"success": True, "path": rel, "matches": matches}
|
||||||
|
|
||||||
|
def _extract_segments(root, payload):
|
||||||
|
rel = payload.get("path")
|
||||||
|
target = _resolve(root, rel)
|
||||||
|
err = _ensure_file(target)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
segments = payload.get("segments") or []
|
||||||
|
if not segments:
|
||||||
|
return {"success": False, "error": "缺少要提取的行区间"}
|
||||||
|
_, lines = _read_text(target)
|
||||||
|
total = len(lines)
|
||||||
|
extracted = []
|
||||||
|
for seg in segments:
|
||||||
|
start = max(1, int(seg.get("start_line") or 1))
|
||||||
|
end = max(start, int(seg.get("end_line") or start))
|
||||||
|
end = min(end, total)
|
||||||
|
extracted.append({
|
||||||
|
"label": seg.get("label") or "segment",
|
||||||
|
"line_start": start,
|
||||||
|
"line_end": end,
|
||||||
|
"content": "".join(lines[start - 1:end])
|
||||||
|
})
|
||||||
|
return {"success": True, "path": rel, "segments": extracted}
|
||||||
|
|
||||||
|
def _matches_glob(rel_path, patterns):
|
||||||
|
if not patterns:
|
||||||
|
return True
|
||||||
|
for pat in patterns:
|
||||||
|
if fnmatch.fnmatch(rel_path, pat):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _search_workspace(root, payload):
|
||||||
|
mode = payload.get("mode")
|
||||||
|
query = payload.get("query") or ""
|
||||||
|
use_regex = bool(payload.get("use_regex"))
|
||||||
|
case_sensitive = bool(payload.get("case_sensitive"))
|
||||||
|
max_results = payload.get("max_results") or 20
|
||||||
|
max_matches_per_file = payload.get("max_matches_per_file") or 3
|
||||||
|
include_glob = payload.get("include_glob") or ["**/*"]
|
||||||
|
exclude_glob = payload.get("exclude_glob") or []
|
||||||
|
max_file_size = payload.get("max_file_size")
|
||||||
|
|
||||||
|
matcher = re.compile(query, 0 if case_sensitive else re.IGNORECASE) if use_regex else None
|
||||||
|
results = []
|
||||||
|
matches = []
|
||||||
|
root = root.resolve()
|
||||||
|
|
||||||
|
for dirpath, _, filenames in os.walk(root):
|
||||||
|
for filename in filenames:
|
||||||
|
full = pathlib.Path(dirpath) / filename
|
||||||
|
try:
|
||||||
|
rel = full.relative_to(root).as_posix()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not _matches_glob(rel, include_glob):
|
||||||
|
continue
|
||||||
|
if exclude_glob and _matches_glob(rel, exclude_glob):
|
||||||
|
continue
|
||||||
|
if mode == "file":
|
||||||
|
hay = filename if case_sensitive else filename.lower()
|
||||||
|
needle = query if case_sensitive else query.lower()
|
||||||
|
ok = matcher.search(filename) if matcher else (needle in hay)
|
||||||
|
if ok:
|
||||||
|
matches.append(str(full))
|
||||||
|
if len(matches) >= max_results:
|
||||||
|
return {"success": True, "mode": "file", "root": str(root), "query": query, "matches": matches}
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mode != "content":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if max_file_size:
|
||||||
|
try:
|
||||||
|
if full.stat().st_size > max_file_size:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with full.open('r', encoding='utf-8', errors='ignore') as fh:
|
||||||
|
lines = fh.readlines()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
file_matches = []
|
||||||
|
for idx, line in enumerate(lines, start=1):
|
||||||
|
hay = line if case_sensitive else line.lower()
|
||||||
|
needle = query if case_sensitive else query.lower()
|
||||||
|
ok = matcher.search(line) if matcher else (needle in hay)
|
||||||
|
if ok:
|
||||||
|
file_matches.append({"line": idx, "snippet": line.strip()})
|
||||||
|
if len(file_matches) >= max_matches_per_file:
|
||||||
|
break
|
||||||
|
if file_matches:
|
||||||
|
results.append({"file": str(full), "matches": file_matches})
|
||||||
|
if len(results) >= max_results:
|
||||||
|
return {"success": True, "mode": "content", "root": str(root), "query": query, "results": results}
|
||||||
|
|
||||||
|
if mode == "file":
|
||||||
|
return {"success": True, "mode": "file", "root": str(root), "query": query, "matches": matches}
|
||||||
|
if mode == "content":
|
||||||
|
return {"success": True, "mode": "content", "root": str(root), "query": query, "results": results}
|
||||||
|
return {"success": False, "error": "未知 search_workspace mode"}
|
||||||
|
|
||||||
|
ACTION_MAP = {
|
||||||
|
"read_text_segment": _read_text_segment,
|
||||||
|
"read_file": _read_file,
|
||||||
|
"write_file": _write_file,
|
||||||
|
"search_text": _search_text,
|
||||||
|
"extract_segments": _extract_segments,
|
||||||
|
"search_workspace": _search_workspace,
|
||||||
|
}
|
||||||
|
|
||||||
|
def main():
|
||||||
|
raw = sys.stdin.read()
|
||||||
|
payload = json.loads(raw or "{}")
|
||||||
|
action = payload.get("action")
|
||||||
|
root = pathlib.Path(payload.get("root") or "/workspace")
|
||||||
|
handler = ACTION_MAP.get(action)
|
||||||
|
if not handler:
|
||||||
|
sys.stdout.write(json.dumps({"success": False, "error": "未知操作"}, ensure_ascii=False))
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
result = handler(root, payload.get("payload") or {})
|
||||||
|
except Exception as exc:
|
||||||
|
result = {"success": False, "error": str(exc)}
|
||||||
|
sys.stdout.write(json.dumps(result, ensure_ascii=False))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
`;
|
||||||
|
|
||||||
|
function buildEnvArgs(ctx) {
|
||||||
|
const venv = ctx.containerVenv || '/opt/agent-venv';
|
||||||
|
const basePath = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
||||||
|
const pathValue = `${venv}/bin:${basePath}`;
|
||||||
|
return ['-e', `VIRTUAL_ENV=${venv}`, '-e', `PATH=${pathValue}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
function execContainerAction(ctx, action, payload, timeoutMs, rootOverride) {
|
||||||
|
const request = { action, root: rootOverride || ctx.mountPath, payload };
|
||||||
|
const args = [ctx.dockerBin, 'exec', '-i'];
|
||||||
|
const workdir = rootOverride || ctx.mountPath;
|
||||||
|
if (workdir) {
|
||||||
|
args.push('-w', workdir);
|
||||||
|
}
|
||||||
|
args.push(...buildEnvArgs(ctx));
|
||||||
|
args.push(ctx.containerName, ctx.containerPython || 'python3', '-c', CONTAINER_SCRIPT);
|
||||||
|
const result = spawnSync(args[0], args.slice(1), {
|
||||||
|
input: JSON.stringify(request),
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: timeoutMs || 60000,
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
return { success: false, error: result.error.message || String(result.error) };
|
||||||
|
}
|
||||||
|
if (result.status !== 0) {
|
||||||
|
const stderr = (result.stderr || '').trim();
|
||||||
|
const stdout = (result.stdout || '').trim();
|
||||||
|
return { success: false, error: stderr || stdout || '容器返回错误' };
|
||||||
|
}
|
||||||
|
const output = (result.stdout || '').trim();
|
||||||
|
if (!output) {
|
||||||
|
return { success: false, error: '容器未返回结果' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(output);
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, error: `容器响应无法解析: ${output.slice(0, 200)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getContainerContext,
|
||||||
|
resolveContainerPath,
|
||||||
|
execContainerAction,
|
||||||
|
buildEnvArgs,
|
||||||
|
};
|
||||||
@ -3,6 +3,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { structuredPatch } = require('diff');
|
const { structuredPatch } = require('diff');
|
||||||
|
const { getContainerContext, resolveContainerPath, execContainerAction } = require('./container_bridge');
|
||||||
|
|
||||||
function resolvePath(workspace, p) {
|
function resolvePath(workspace, p) {
|
||||||
if (path.isAbsolute(p)) return p;
|
if (path.isAbsolute(p)) return p;
|
||||||
@ -44,6 +45,47 @@ function editFileTool(workspace, args) {
|
|||||||
const target = resolvePath(workspace, args.file_path);
|
const target = resolvePath(workspace, args.file_path);
|
||||||
const oldString = args.old_string ?? '';
|
const oldString = args.old_string ?? '';
|
||||||
const newString = args.new_string ?? '';
|
const newString = args.new_string ?? '';
|
||||||
|
const ctx = getContainerContext();
|
||||||
|
if (ctx) {
|
||||||
|
const resolved = resolveContainerPath(workspace, args.file_path || '.', ctx);
|
||||||
|
if (resolved.error) return { success: false, error: resolved.error };
|
||||||
|
const rel = resolved.relativePath;
|
||||||
|
if (!oldString) {
|
||||||
|
const writeResp = execContainerAction(ctx, 'write_file', {
|
||||||
|
path: rel,
|
||||||
|
content: newString || '',
|
||||||
|
mode: 'w',
|
||||||
|
});
|
||||||
|
if (!writeResp.success) return { success: false, error: writeResp.error || '写入失败' };
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
path: resolved.containerPath,
|
||||||
|
replacements: newString ? 1 : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const readResp = execContainerAction(ctx, 'read_file', { path: rel });
|
||||||
|
if (!readResp.success) return { success: false, error: readResp.error || '读取失败' };
|
||||||
|
const original = readResp.content || '';
|
||||||
|
if (!original.includes(oldString)) {
|
||||||
|
return { success: false, error: 'old_string 未匹配到内容' };
|
||||||
|
}
|
||||||
|
const updated = original.split(oldString).join(newString);
|
||||||
|
const replacements = original.split(oldString).length - 1;
|
||||||
|
const writeResp = execContainerAction(ctx, 'write_file', {
|
||||||
|
path: rel,
|
||||||
|
content: updated,
|
||||||
|
mode: 'w',
|
||||||
|
});
|
||||||
|
if (!writeResp.success) return { success: false, error: writeResp.error || '写入失败' };
|
||||||
|
const diff = diffSummary(original, updated);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
path: resolved.containerPath,
|
||||||
|
replacements,
|
||||||
|
diff,
|
||||||
|
};
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let creating = false;
|
let creating = false;
|
||||||
if (!fs.existsSync(target)) {
|
if (!fs.existsSync(target)) {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { getContainerContext, resolveContainerPath, execContainerAction } = require('./container_bridge');
|
||||||
|
|
||||||
function resolvePath(workspace, p) {
|
function resolvePath(workspace, p) {
|
||||||
if (path.isAbsolute(p)) return p;
|
if (path.isAbsolute(p)) return p;
|
||||||
@ -20,6 +21,70 @@ function applyMaxChars(text, maxChars) {
|
|||||||
function readFileTool(workspace, args) {
|
function readFileTool(workspace, args) {
|
||||||
const target = resolvePath(workspace, args.path);
|
const target = resolvePath(workspace, args.path);
|
||||||
const type = args.type || 'read';
|
const type = args.type || 'read';
|
||||||
|
const ctx = getContainerContext();
|
||||||
|
if (ctx) {
|
||||||
|
const resolved = resolveContainerPath(workspace, args.path || '.', ctx);
|
||||||
|
if (resolved.error) return { success: false, error: resolved.error };
|
||||||
|
const rel = resolved.relativePath;
|
||||||
|
if (type === 'read') {
|
||||||
|
const resp = execContainerAction(ctx, 'read_text_segment', {
|
||||||
|
path: rel,
|
||||||
|
start_line: args.start_line,
|
||||||
|
end_line: args.end_line,
|
||||||
|
});
|
||||||
|
if (!resp.success) return { success: false, error: resp.error || '读取失败' };
|
||||||
|
const capped = applyMaxChars(resp.content || '', args.max_chars);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
type: 'read',
|
||||||
|
path: resolved.containerPath,
|
||||||
|
line_start: resp.line_start,
|
||||||
|
line_end: resp.line_end,
|
||||||
|
content: capped.text,
|
||||||
|
truncated: capped.truncated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === 'search') {
|
||||||
|
const resp = execContainerAction(ctx, 'search_text', {
|
||||||
|
path: rel,
|
||||||
|
query: args.query,
|
||||||
|
case_sensitive: !!args.case_sensitive,
|
||||||
|
max_matches: args.max_matches || 20,
|
||||||
|
context_before: args.context_before || 0,
|
||||||
|
context_after: args.context_after || 0,
|
||||||
|
});
|
||||||
|
if (!resp.success) return { success: false, error: resp.error || '搜索失败' };
|
||||||
|
const matches = (resp.matches || []).map((item, idx) => ({
|
||||||
|
id: `match_${idx + 1}`,
|
||||||
|
line_start: item.line_start,
|
||||||
|
line_end: item.line_end,
|
||||||
|
hits: item.hits || [],
|
||||||
|
snippet: item.snippet || '',
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
type: 'search',
|
||||||
|
path: resolved.containerPath,
|
||||||
|
query: args.query || '',
|
||||||
|
case_sensitive: !!args.case_sensitive,
|
||||||
|
matches,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (type === 'extract') {
|
||||||
|
const resp = execContainerAction(ctx, 'extract_segments', {
|
||||||
|
path: rel,
|
||||||
|
segments: args.segments || [],
|
||||||
|
});
|
||||||
|
if (!resp.success) return { success: false, error: resp.error || '提取失败' };
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
type: 'extract',
|
||||||
|
path: resolved.containerPath,
|
||||||
|
segments: resp.segments || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { success: false, error: '未知 read_file type' };
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const raw = fs.readFileSync(target, 'utf8');
|
const raw = fs.readFileSync(target, 'utf8');
|
||||||
if (type === 'read') {
|
if (type === 'read') {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const mime = require('mime-types');
|
const mime = require('mime-types');
|
||||||
|
const { getContainerContext, resolveContainerPath } = require('./container_bridge');
|
||||||
|
|
||||||
function resolvePath(workspace, p) {
|
function resolvePath(workspace, p) {
|
||||||
if (path.isAbsolute(p)) return p;
|
if (path.isAbsolute(p)) return p;
|
||||||
@ -10,7 +11,15 @@ function resolvePath(workspace, p) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readMediafileTool(workspace, args) {
|
function readMediafileTool(workspace, args) {
|
||||||
const target = resolvePath(workspace, args.path);
|
const ctx = getContainerContext();
|
||||||
|
let target = resolvePath(workspace, args.path);
|
||||||
|
let displayPath = target;
|
||||||
|
if (ctx) {
|
||||||
|
const resolved = resolveContainerPath(workspace, args.path || '.', ctx);
|
||||||
|
if (resolved.error) return { success: false, error: resolved.error };
|
||||||
|
target = resolved.hostPath;
|
||||||
|
displayPath = resolved.containerPath;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const stat = fs.statSync(target);
|
const stat = fs.statSync(target);
|
||||||
if (!stat.isFile()) return { success: false, error: '不是文件' };
|
if (!stat.isFile()) return { success: false, error: '不是文件' };
|
||||||
@ -22,7 +31,7 @@ function readMediafileTool(workspace, args) {
|
|||||||
const data = fs.readFileSync(target);
|
const data = fs.readFileSync(target);
|
||||||
const b64 = data.toString('base64');
|
const b64 = data.toString('base64');
|
||||||
const type = mt.startsWith('image/') ? 'image' : 'video';
|
const type = mt.startsWith('image/') ? 'image' : 'video';
|
||||||
return { success: true, path: target, mime: mt, type, b64 };
|
return { success: true, path: displayPath, mime: mt, type, b64 };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { success: false, error: err.message || String(err) };
|
return { success: false, error: err.message || String(err) };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { exec } = require('child_process');
|
const { exec, spawn } = require('child_process');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { getContainerContext, resolveContainerPath, buildEnvArgs } = require('./container_bridge');
|
||||||
|
|
||||||
function resolvePath(workspace, p) {
|
function resolvePath(workspace, p) {
|
||||||
if (!p) return workspace;
|
if (!p) return workspace;
|
||||||
@ -14,12 +15,86 @@ function runCommandTool(workspace, args, abortSignal) {
|
|||||||
const cmd = args.command;
|
const cmd = args.command;
|
||||||
const timeoutSec = Number(args.timeout || 0);
|
const timeoutSec = Number(args.timeout || 0);
|
||||||
const cwd = resolvePath(workspace, args.working_dir || '.');
|
const cwd = resolvePath(workspace, args.working_dir || '.');
|
||||||
|
const ctx = getContainerContext();
|
||||||
if (!cmd) return resolve({ success: false, error: 'command 不能为空' });
|
if (!cmd) return resolve({ success: false, error: 'command 不能为空' });
|
||||||
if (abortSignal && abortSignal.aborted) {
|
if (abortSignal && abortSignal.aborted) {
|
||||||
return resolve({ success: false, error: '任务被用户取消', cancelled: true });
|
return resolve({ success: false, error: '任务被用户取消', cancelled: true });
|
||||||
}
|
}
|
||||||
const timeoutMs = Math.max(0, timeoutSec) * 1000;
|
const timeoutMs = Math.max(0, timeoutSec) * 1000;
|
||||||
let finished = false;
|
let finished = false;
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
const resolved = resolveContainerPath(workspace, args.working_dir || '.', ctx);
|
||||||
|
if (resolved.error) {
|
||||||
|
return resolve({ success: false, error: resolved.error });
|
||||||
|
}
|
||||||
|
const workdir = resolved.containerPath || ctx.mountPath;
|
||||||
|
const execArgs = [ 'exec', '-i' ];
|
||||||
|
if (workdir) execArgs.push('-w', workdir);
|
||||||
|
execArgs.push(...buildEnvArgs(ctx));
|
||||||
|
const venv = ctx.containerVenv || '/opt/agent-venv';
|
||||||
|
const basePath = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
||||||
|
const exportPrefix = `export VIRTUAL_ENV=${venv}; export PATH=${venv}/bin:${basePath}; `;
|
||||||
|
const wrappedCmd = `${exportPrefix}${cmd}`;
|
||||||
|
execArgs.push(ctx.containerName, 'sh', '-c', wrappedCmd);
|
||||||
|
const child = spawn(ctx.dockerBin, execArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
const onData = (chunk, buffer) => buffer.push(chunk);
|
||||||
|
const stdoutBuf = [];
|
||||||
|
const stderrBuf = [];
|
||||||
|
child.stdout.on('data', (chunk) => onData(chunk, stdoutBuf));
|
||||||
|
child.stderr.on('data', (chunk) => onData(chunk, stderrBuf));
|
||||||
|
const done = (result) => {
|
||||||
|
if (finished) return;
|
||||||
|
finished = true;
|
||||||
|
if (abortSignal) abortSignal.removeEventListener('abort', onAbort);
|
||||||
|
return resolve(result);
|
||||||
|
};
|
||||||
|
const onAbort = () => {
|
||||||
|
if (finished) return;
|
||||||
|
finished = true;
|
||||||
|
try {
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
} catch (_) {}
|
||||||
|
return resolve({ success: false, error: '任务被用户取消', cancelled: true });
|
||||||
|
};
|
||||||
|
if (abortSignal) abortSignal.addEventListener('abort', onAbort, { once: true });
|
||||||
|
let timer = null;
|
||||||
|
if (timeoutMs) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
} catch (_) {}
|
||||||
|
const output = Buffer.concat(stdoutBuf).toString() + Buffer.concat(stderrBuf).toString();
|
||||||
|
done({
|
||||||
|
success: false,
|
||||||
|
status: 'timeout',
|
||||||
|
error: '命令超时',
|
||||||
|
return_code: null,
|
||||||
|
output,
|
||||||
|
timeout: timeoutSec,
|
||||||
|
});
|
||||||
|
}, timeoutMs);
|
||||||
|
}
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
const output = Buffer.concat(stdoutBuf).toString() + Buffer.concat(stderrBuf).toString();
|
||||||
|
if (finished) return;
|
||||||
|
if (code && code !== 0) {
|
||||||
|
return done({
|
||||||
|
success: false,
|
||||||
|
status: 'error',
|
||||||
|
error: `exit ${code}`,
|
||||||
|
return_code: code,
|
||||||
|
output,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return done({ success: true, status: 'ok', output });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const child = exec(cmd, { cwd, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, shell: true }, (err, stdout, stderr) => {
|
const child = exec(cmd, { cwd, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, shell: true }, (err, stdout, stderr) => {
|
||||||
if (finished) return;
|
if (finished) return;
|
||||||
finished = true;
|
finished = true;
|
||||||
|
|||||||
@ -4,6 +4,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fg = require('fast-glob');
|
const fg = require('fast-glob');
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
|
const { getContainerContext, resolveContainerPath, execContainerAction } = require('./container_bridge');
|
||||||
|
|
||||||
function resolvePath(workspace, p) {
|
function resolvePath(workspace, p) {
|
||||||
if (!p) return workspace;
|
if (!p) return workspace;
|
||||||
@ -31,6 +32,26 @@ async function searchWorkspaceTool(workspace, args) {
|
|||||||
const includeGlob = Array.isArray(args.include_glob) && args.include_glob.length ? args.include_glob : ['**/*'];
|
const includeGlob = Array.isArray(args.include_glob) && args.include_glob.length ? args.include_glob : ['**/*'];
|
||||||
const excludeGlob = Array.isArray(args.exclude_glob) ? args.exclude_glob : [];
|
const excludeGlob = Array.isArray(args.exclude_glob) ? args.exclude_glob : [];
|
||||||
const maxFileSize = args.max_file_size || null;
|
const maxFileSize = args.max_file_size || null;
|
||||||
|
const ctx = getContainerContext();
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
const resolved = resolveContainerPath(workspace, args.root || '.', ctx);
|
||||||
|
if (resolved.error) return { success: false, error: resolved.error };
|
||||||
|
const resp = execContainerAction(ctx, 'search_workspace', {
|
||||||
|
mode,
|
||||||
|
query,
|
||||||
|
use_regex: useRegex,
|
||||||
|
case_sensitive: caseSensitive,
|
||||||
|
max_results: maxResults,
|
||||||
|
max_matches_per_file: maxMatchesPerFile,
|
||||||
|
include_glob: includeGlob,
|
||||||
|
exclude_glob: excludeGlob,
|
||||||
|
max_file_size: maxFileSize,
|
||||||
|
root: resolved.containerPath,
|
||||||
|
}, null, resolved.containerPath);
|
||||||
|
if (!resp.success) return { success: false, error: resp.error || '搜索失败' };
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
if (mode === 'file') {
|
if (mode === 'file') {
|
||||||
const files = await fg(includeGlob, { cwd: root, dot: true, ignore: excludeGlob, onlyFiles: true, absolute: true });
|
const files = await fg(includeGlob, { cwd: root, dot: true, ignore: excludeGlob, onlyFiles: true, absolute: true });
|
||||||
|
|||||||
@ -4,8 +4,10 @@ import json
|
|||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
import os
|
||||||
from typing import Dict, List, Optional, Any, Tuple
|
import shutil
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
from typing import Dict, List, Optional, Any, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
OUTPUT_FORMATS,
|
OUTPUT_FORMATS,
|
||||||
@ -18,6 +20,9 @@ from config import (
|
|||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from modules.user_container_manager import ContainerHandle
|
||||||
|
|
||||||
# 静音子智能体日志(交由前端提示/brief_log处理)
|
# 静音子智能体日志(交由前端提示/brief_log处理)
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
logger.setLevel(logging.CRITICAL)
|
logger.setLevel(logging.CRITICAL)
|
||||||
@ -31,14 +36,24 @@ TERMINAL_STATUSES = {"completed", "failed", "timeout"}
|
|||||||
class SubAgentManager:
|
class SubAgentManager:
|
||||||
"""负责主智能体与子智能体的任务调度(子进程模式)。"""
|
"""负责主智能体与子智能体的任务调度(子进程模式)。"""
|
||||||
|
|
||||||
def __init__(self, project_path: str, data_dir: str):
|
def __init__(
|
||||||
|
self,
|
||||||
|
project_path: str,
|
||||||
|
data_dir: str,
|
||||||
|
container_session: Optional["ContainerHandle"] = None,
|
||||||
|
):
|
||||||
self.project_path = Path(project_path).resolve()
|
self.project_path = Path(project_path).resolve()
|
||||||
self.data_dir = Path(data_dir).resolve()
|
self.data_dir = Path(data_dir).resolve()
|
||||||
self.base_dir = Path(SUB_AGENT_TASKS_BASE_DIR).resolve()
|
self.base_dir = Path(SUB_AGENT_TASKS_BASE_DIR).resolve()
|
||||||
self.state_file = Path(SUB_AGENT_STATE_FILE).resolve()
|
self.state_file = Path(SUB_AGENT_STATE_FILE).resolve()
|
||||||
|
self.container_session: Optional["ContainerHandle"] = container_session
|
||||||
|
|
||||||
# easyagent批处理入口
|
# easyagent批处理入口(优先使用项目目录内的副本)
|
||||||
self.easyagent_batch = Path(__file__).parent.parent / "easyagent" / "src" / "batch" / "index.js"
|
candidate_batch = self.project_path / "easyagent" / "src" / "batch" / "index.js"
|
||||||
|
if candidate_batch.exists():
|
||||||
|
self.easyagent_batch = candidate_batch
|
||||||
|
else:
|
||||||
|
self.easyagent_batch = Path(__file__).parent.parent / "easyagent" / "src" / "batch" / "index.js"
|
||||||
|
|
||||||
self.base_dir.mkdir(parents=True, exist_ok=True)
|
self.base_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@ -111,31 +126,51 @@ class SubAgentManager:
|
|||||||
progress_file = task_root / "progress.jsonl"
|
progress_file = task_root / "progress.jsonl"
|
||||||
|
|
||||||
# 构建用户消息
|
# 构建用户消息
|
||||||
user_message = self._build_user_message(agent_id, summary, task, deliverables_path, timeout_seconds)
|
prompt_workspace = self._get_runtime_path(self.project_path)
|
||||||
|
deliverables_display = self._get_runtime_path(deliverables_path)
|
||||||
|
user_message = self._build_user_message(agent_id, summary, task, deliverables_display, timeout_seconds)
|
||||||
task_file.write_text(user_message, encoding="utf-8")
|
task_file.write_text(user_message, encoding="utf-8")
|
||||||
|
|
||||||
# 构建系统提示
|
# 构建系统提示
|
||||||
system_prompt = self._build_system_prompt()
|
system_prompt = self._build_system_prompt(prompt_workspace)
|
||||||
system_prompt_file.write_text(system_prompt, encoding="utf-8")
|
system_prompt_file.write_text(system_prompt, encoding="utf-8")
|
||||||
|
|
||||||
# 启动子进程
|
# 启动子进程
|
||||||
timeout_seconds = timeout_seconds or SUB_AGENT_DEFAULT_TIMEOUT
|
timeout_seconds = timeout_seconds or SUB_AGENT_DEFAULT_TIMEOUT
|
||||||
cmd = [
|
cmd = self._build_sub_agent_command(
|
||||||
"node",
|
batch_path=str(self.easyagent_batch),
|
||||||
str(self.easyagent_batch),
|
workspace_path=str(self.project_path),
|
||||||
"--workspace", str(self.project_path),
|
task_file=str(task_file),
|
||||||
"--task-file", str(task_file),
|
system_prompt_file=str(system_prompt_file),
|
||||||
"--system-prompt-file", str(system_prompt_file),
|
output_file=str(output_file),
|
||||||
"--output-file", str(output_file),
|
stats_file=str(stats_file),
|
||||||
"--stats-file", str(stats_file),
|
progress_file=str(progress_file),
|
||||||
"--progress-file", str(progress_file),
|
agent_id=agent_id,
|
||||||
"--agent-id", str(agent_id),
|
timeout_seconds=timeout_seconds,
|
||||||
"--timeout", str(timeout_seconds),
|
model_key=model_key,
|
||||||
]
|
thinking_mode=thinking_mode,
|
||||||
if model_key:
|
)
|
||||||
cmd.extend(["--model-key", model_key])
|
execution_mode = "host"
|
||||||
if thinking_mode:
|
container_name = None
|
||||||
cmd.extend(["--thinking-mode", thinking_mode])
|
env = os.environ.copy()
|
||||||
|
|
||||||
|
if self._should_use_container():
|
||||||
|
container = self.container_session
|
||||||
|
container_name = getattr(container, "container_name", None) if container else None
|
||||||
|
docker_bin = (getattr(container, "sandbox_bin", None) if container else None) or shutil.which("docker")
|
||||||
|
if not container_name:
|
||||||
|
return {"success": False, "error": "容器模式下缺少 container_name,无法启动子智能体。"}
|
||||||
|
if not docker_bin:
|
||||||
|
return {"success": False, "error": "容器模式下未找到 docker,可执行子智能体失败。"}
|
||||||
|
env["EASYAGENT_CONTAINER_NAME"] = container_name
|
||||||
|
env["EASYAGENT_CONTAINER_MOUNT_PATH"] = (
|
||||||
|
getattr(container, "mount_path", None) or "/workspace"
|
||||||
|
)
|
||||||
|
env["EASYAGENT_DOCKER_BIN"] = docker_bin
|
||||||
|
env["EASYAGENT_CONTAINER_VENV"] = os.environ.get("CONTAINER_PYTHON_VENV", "/opt/agent-venv")
|
||||||
|
env["EASYAGENT_CONTAINER_PYTHON"] = os.environ.get("CONTAINER_PYTHON_CMD", "/opt/agent-venv/bin/python3")
|
||||||
|
env["EASYAGENT_CONTAINER_MODE"] = "1"
|
||||||
|
execution_mode = "docker"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
@ -143,6 +178,7 @@ class SubAgentManager:
|
|||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
cwd=str(self.project_path),
|
cwd=str(self.project_path),
|
||||||
|
env=env,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"success": False, "error": f"启动子智能体失败: {exc}"}
|
return {"success": False, "error": f"启动子智能体失败: {exc}"}
|
||||||
@ -166,6 +202,8 @@ class SubAgentManager:
|
|||||||
"stats_file": str(stats_file),
|
"stats_file": str(stats_file),
|
||||||
"progress_file": str(progress_file),
|
"progress_file": str(progress_file),
|
||||||
"pid": process.pid,
|
"pid": process.pid,
|
||||||
|
"execution_mode": execution_mode,
|
||||||
|
"container_name": container_name,
|
||||||
}
|
}
|
||||||
self.tasks[task_id] = task_record
|
self.tasks[task_id] = task_record
|
||||||
self.processes[task_id] = process
|
self.processes[task_id] = process
|
||||||
@ -261,9 +299,61 @@ class SubAgentManager:
|
|||||||
"system_message": f"🛑 子智能体{task.get('agent_id')} 已被手动关闭。",
|
"system_message": f"🛑 子智能体{task.get('agent_id')} 已被手动关闭。",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def set_container_session(self, session: Optional["ContainerHandle"]):
|
||||||
|
"""更新容器会话信息。"""
|
||||||
|
self.container_session = session
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 内部工具方法
|
# 内部工具方法
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
def _should_use_container(self) -> bool:
|
||||||
|
return bool(self.container_session and getattr(self.container_session, "mode", None) == "docker")
|
||||||
|
|
||||||
|
def _get_runtime_path(self, host_path: Path) -> str:
|
||||||
|
"""将宿主机路径映射为容器内路径(仅用于提示展示)。"""
|
||||||
|
if not self._should_use_container():
|
||||||
|
return str(host_path)
|
||||||
|
mount_path = (getattr(self.container_session, "mount_path", None) or "/workspace").rstrip("/") or "/workspace"
|
||||||
|
try:
|
||||||
|
relative = host_path.resolve().relative_to(self.project_path)
|
||||||
|
except Exception:
|
||||||
|
return mount_path
|
||||||
|
if str(relative) in {"", "."}:
|
||||||
|
return mount_path
|
||||||
|
return str(PurePosixPath(mount_path) / PurePosixPath(relative.as_posix()))
|
||||||
|
|
||||||
|
def _build_sub_agent_command(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
batch_path: str,
|
||||||
|
workspace_path: str,
|
||||||
|
task_file: str,
|
||||||
|
system_prompt_file: str,
|
||||||
|
output_file: str,
|
||||||
|
stats_file: str,
|
||||||
|
progress_file: str,
|
||||||
|
agent_id: int,
|
||||||
|
timeout_seconds: int,
|
||||||
|
model_key: Optional[str],
|
||||||
|
thinking_mode: Optional[str],
|
||||||
|
) -> List[str]:
|
||||||
|
cmd = [
|
||||||
|
"node",
|
||||||
|
batch_path,
|
||||||
|
"--workspace", workspace_path,
|
||||||
|
"--task-file", task_file,
|
||||||
|
"--system-prompt-file", system_prompt_file,
|
||||||
|
"--output-file", output_file,
|
||||||
|
"--stats-file", stats_file,
|
||||||
|
"--progress-file", progress_file,
|
||||||
|
"--agent-id", str(agent_id),
|
||||||
|
"--timeout", str(timeout_seconds),
|
||||||
|
]
|
||||||
|
if model_key:
|
||||||
|
cmd.extend(["--model-key", model_key])
|
||||||
|
if thinking_mode:
|
||||||
|
cmd.extend(["--thinking-mode", thinking_mode])
|
||||||
|
return cmd
|
||||||
def _check_task_status(self, task: Dict) -> Dict:
|
def _check_task_status(self, task: Dict) -> Dict:
|
||||||
"""检查任务状态,如果完成则解析输出。"""
|
"""检查任务状态,如果完成则解析输出。"""
|
||||||
task_id = task["task_id"]
|
task_id = task["task_id"]
|
||||||
@ -429,7 +519,7 @@ class SubAgentManager:
|
|||||||
agent_id: int,
|
agent_id: int,
|
||||||
summary: str,
|
summary: str,
|
||||||
task: str,
|
task: str,
|
||||||
deliverables_path: Path,
|
deliverables_path: str,
|
||||||
timeout_seconds: Optional[int],
|
timeout_seconds: Optional[int],
|
||||||
) -> str:
|
) -> str:
|
||||||
"""构建发送给子智能体的用户消息。"""
|
"""构建发送给子智能体的用户消息。"""
|
||||||
@ -448,7 +538,7 @@ class SubAgentManager:
|
|||||||
|
|
||||||
完成任务后,请调用 finish_task 工具提交完成报告。"""
|
完成任务后,请调用 finish_task 工具提交完成报告。"""
|
||||||
|
|
||||||
def _build_system_prompt(self) -> str:
|
def _build_system_prompt(self, workspace_path: str) -> str:
|
||||||
"""构建子智能体的系统提示。"""
|
"""构建子智能体的系统提示。"""
|
||||||
import platform
|
import platform
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -527,7 +617,7 @@ class SubAgentManager:
|
|||||||
|
|
||||||
# 当前环境
|
# 当前环境
|
||||||
|
|
||||||
- 工作区路径: {self.project_path}
|
- 工作区路径: {workspace_path}
|
||||||
- 系统: {system_info}
|
- 系统: {system_info}
|
||||||
- 当前时间: {current_time}
|
- 当前时间: {current_time}
|
||||||
|
|
||||||
|
|||||||
@ -95,7 +95,8 @@ class MainTerminal:
|
|||||||
self.todo_manager = TodoManager(self.context_manager)
|
self.todo_manager = TodoManager(self.context_manager)
|
||||||
self.sub_agent_manager = SubAgentManager(
|
self.sub_agent_manager = SubAgentManager(
|
||||||
project_path=self.project_path,
|
project_path=self.project_path,
|
||||||
data_dir=str(self.data_dir)
|
data_dir=str(self.data_dir),
|
||||||
|
container_session=None,
|
||||||
)
|
)
|
||||||
self._announced_sub_agent_tasks = set()
|
self._announced_sub_agent_tasks = set()
|
||||||
self.silent_tool_disable = False # 是否静默工具禁用提示
|
self.silent_tool_disable = False # 是否静默工具禁用提示
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user