mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-10 07:54:14 +08:00
Handle invalid UTF-8 with replacement decoding
This commit is contained in:
@@ -891,7 +891,7 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
if not self._models_dev_cache_path.exists():
|
||||
payload = None
|
||||
else:
|
||||
payload = json.loads(self._models_dev_cache_path.read_text(encoding="utf-8"))
|
||||
payload = json.loads(self._models_dev_cache_path.read_text(encoding="utf-8", errors="replace"))
|
||||
except Exception as err:
|
||||
logger.warning(f"读取 models.dev provider 缓存失败: {err}")
|
||||
payload = None
|
||||
@@ -1424,7 +1424,7 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
if not self._models_dev_cache_path.exists():
|
||||
return None
|
||||
async with aiofiles.open(
|
||||
self._models_dev_cache_path, mode="r", encoding="utf-8"
|
||||
self._models_dev_cache_path, mode="r", encoding="utf-8", errors="replace"
|
||||
) as stream:
|
||||
return json.loads(await stream.read())
|
||||
except Exception as err:
|
||||
@@ -1437,7 +1437,7 @@ class LLMProviderManager(metaclass=Singleton):
|
||||
if not self._MODELS_DEV_BUNDLED_PATH.exists():
|
||||
return None
|
||||
payload = json.loads(
|
||||
self._MODELS_DEV_BUNDLED_PATH.read_text(encoding="utf-8")
|
||||
self._MODELS_DEV_BUNDLED_PATH.read_text(encoding="utf-8", errors="replace")
|
||||
)
|
||||
except Exception as err:
|
||||
logger.warning(f"读取本地 models.dev 离线文件失败: {err}")
|
||||
|
||||
@@ -133,7 +133,7 @@ def load_activity_log_index(activity_dir: str, days: int = PROMPT_LOAD_DAYS) ->
|
||||
if not log_path.is_file():
|
||||
continue
|
||||
try:
|
||||
content = log_path.read_text(encoding="utf-8")
|
||||
content = log_path.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception as e:
|
||||
logger.warning(f"读取活动日志索引失败 {log_path}: {e}")
|
||||
continue
|
||||
@@ -197,7 +197,7 @@ def query_activity_logs(
|
||||
if not log_path.is_file():
|
||||
continue
|
||||
try:
|
||||
content = log_path.read_text(encoding="utf-8")
|
||||
content = log_path.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception as e:
|
||||
logger.warning(f"读取活动日志失败 {log_path}: {e}")
|
||||
continue
|
||||
@@ -480,7 +480,7 @@ class ActivityLogMiddleware(AgentMiddleware[ActivityLogState, ContextT, Response
|
||||
entry = f"- **{now_str}** {summary}\n"
|
||||
try:
|
||||
if await log_path.exists():
|
||||
existing = await log_path.read_text(encoding="utf-8")
|
||||
existing = await log_path.read_text(encoding="utf-8", errors="replace")
|
||||
await log_path.write_text(existing + entry, encoding="utf-8")
|
||||
else:
|
||||
header = f"# {today_str} 活动日志\n\n"
|
||||
|
||||
@@ -128,7 +128,7 @@ def _parse_job_metadata(
|
||||
async def _alist_jobs(source_path: AsyncPath) -> list[JobMetadata]:
|
||||
"""异步列出指定路径下的所有任务。
|
||||
|
||||
扫描包含 JOB.md 的目录并解析其元数据。
|
||||
扫描包含 JOB.md 的目录并解析其元数据,遇到非法 UTF-8 字节时以替换字符兜底。
|
||||
"""
|
||||
jobs: list[JobMetadata] = []
|
||||
|
||||
@@ -151,7 +151,10 @@ async def _alist_jobs(source_path: AsyncPath) -> list[JobMetadata]:
|
||||
for job_path in job_dirs:
|
||||
job_md_path = job_path / "JOB.md"
|
||||
|
||||
job_content = await job_md_path.read_text(encoding="utf-8")
|
||||
job_content = await job_md_path.read_text(
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
|
||||
# 解析元数据
|
||||
job_metadata = _parse_job_metadata(
|
||||
|
||||
@@ -335,7 +335,7 @@ class MemoryMiddleware(AgentMiddleware[MemoryState, ContextT, ResponseT]): # no
|
||||
MAX_MEMORY_FILE_SIZE,
|
||||
)
|
||||
continue
|
||||
contents[path] = await file_path.read_text(encoding="utf-8")
|
||||
contents[path] = await file_path.read_text(encoding="utf-8", errors="replace")
|
||||
logger.debug("Loaded memory from: %s", path)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to read memory file %s: %s", path, e)
|
||||
|
||||
@@ -234,7 +234,7 @@ async def _alist_skills(source_path: AsyncPath) -> list[SkillMetadata]:
|
||||
for skill_path in skill_dirs:
|
||||
skill_md_path = skill_path / "SKILL.md"
|
||||
|
||||
skill_content = await skill_md_path.read_text(encoding="utf-8")
|
||||
skill_content = await skill_md_path.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
# 解析元数据
|
||||
skill_metadata = _parse_skill_metadata(
|
||||
@@ -312,7 +312,7 @@ Remember: Skills make you more capable and consistent. When in doubt, check if a
|
||||
def _extract_version(skill_md: Path) -> int:
|
||||
"""从 SKILL.md 文件中快速提取 version 字段,无法提取时返回 0。"""
|
||||
try:
|
||||
content = skill_md.read_text(encoding="utf-8")
|
||||
content = skill_md.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return 0
|
||||
|
||||
@@ -102,7 +102,7 @@ class PromptManager:
|
||||
|
||||
prompt_file = self.prompts_dir / prompt_name
|
||||
try:
|
||||
with open(prompt_file, "r", encoding="utf-8") as f:
|
||||
with open(prompt_file, "r", encoding="utf-8", errors="replace") as f:
|
||||
content = f.read().strip()
|
||||
# 缓存提示词
|
||||
self.prompts_cache[prompt_name] = content
|
||||
@@ -187,7 +187,7 @@ class PromptManager:
|
||||
return self._system_tasks_cache
|
||||
|
||||
try:
|
||||
content = system_tasks_path.read_text(encoding="utf-8")
|
||||
content = system_tasks_path.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception as err: # noqa: BLE001
|
||||
logger.error(f"读取系统任务定义失败: {system_tasks_path}, 错误: {err}")
|
||||
raise PromptConfigError(
|
||||
|
||||
@@ -702,7 +702,7 @@ class AgentRuntimeManager:
|
||||
if not path.exists():
|
||||
raise AgentRuntimeConfigError(f"缺少配置文件: {path}")
|
||||
try:
|
||||
content = path.read_text(encoding="utf-8")
|
||||
content = path.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception as err: # noqa: BLE001
|
||||
raise AgentRuntimeConfigError(f"读取配置文件失败 {path}: {err}") from err
|
||||
|
||||
|
||||
@@ -533,7 +533,7 @@ class _TerminalSessionManager:
|
||||
if len(encoded) > remaining:
|
||||
if remaining > 0:
|
||||
output_parts.append(
|
||||
encoded[:remaining].decode("utf-8", errors="ignore")
|
||||
encoded[:remaining].decode("utf-8", errors="replace")
|
||||
)
|
||||
output_truncated = True
|
||||
break
|
||||
|
||||
@@ -59,7 +59,7 @@ class EditFileTool(MoviePilotTool):
|
||||
return f"错误:{resolved_path} 不是一个文件"
|
||||
|
||||
if await path.exists():
|
||||
content = await path.read_text(encoding="utf-8")
|
||||
content = await path.read_text(encoding="utf-8", errors="replace")
|
||||
if old_text not in content:
|
||||
logger.warning(f"编辑文件 {resolved_path} 失败:未找到指定的旧文本块")
|
||||
return f"错误:在文件 {resolved_path} 中未找到指定的旧文本。请确保包含所有的空格、缩进 and 换行符。"
|
||||
|
||||
@@ -58,7 +58,7 @@ class _CommandOutput:
|
||||
"""按 UTF-8 字节数截断文本,避免截断后出现非法字符。"""
|
||||
if byte_limit <= 0:
|
||||
return ""
|
||||
return text.encode("utf-8")[:byte_limit].decode("utf-8", errors="ignore")
|
||||
return text.encode("utf-8")[:byte_limit].decode("utf-8", errors="replace")
|
||||
|
||||
def _write_chunk(self, stream_name: str, text: str) -> None:
|
||||
"""把输出分片按 stdout/stderr 分段写入临时文件。"""
|
||||
|
||||
@@ -55,7 +55,7 @@ class ReadFileTool(MoviePilotTool):
|
||||
if not await path.is_file():
|
||||
return f"错误:{resolved_path} 不是一个文件"
|
||||
|
||||
content = await path.read_text(encoding="utf-8")
|
||||
content = await path.read_text(encoding="utf-8", errors="replace")
|
||||
truncated = False
|
||||
|
||||
if start_line is not None or end_line is not None:
|
||||
@@ -75,7 +75,7 @@ class ReadFileTool(MoviePilotTool):
|
||||
# 检查大小限制
|
||||
content_bytes = content.encode("utf-8")
|
||||
if len(content_bytes) > MAX_READ_SIZE:
|
||||
content = content_bytes[:MAX_READ_SIZE].decode("utf-8", errors="ignore")
|
||||
content = content_bytes[:MAX_READ_SIZE].decode("utf-8", errors="replace")
|
||||
truncated = True
|
||||
|
||||
if truncated:
|
||||
|
||||
@@ -44,7 +44,7 @@ async def user_message(
|
||||
args = request.query_params
|
||||
source = args.get("source")
|
||||
content_type = request.headers.get("content-type", "")
|
||||
body_text = body.decode("utf-8", errors="ignore")
|
||||
body_text = body.decode("utf-8", errors="replace")
|
||||
image_markers = [
|
||||
marker
|
||||
for marker in (
|
||||
|
||||
@@ -1002,7 +1002,7 @@ async def get_logging(
|
||||
|
||||
# 读取历史日志
|
||||
async with aiofiles.open(
|
||||
log_path, mode="r", encoding="utf-8", errors="ignore"
|
||||
log_path, mode="r", encoding="utf-8", errors="replace"
|
||||
) as f:
|
||||
# 优化大文件读取策略
|
||||
if file_size > 100 * 1024:
|
||||
@@ -1031,7 +1031,7 @@ async def get_logging(
|
||||
|
||||
# 实时监听新日志
|
||||
async with aiofiles.open(
|
||||
log_path, mode="r", encoding="utf-8", errors="ignore"
|
||||
log_path, mode="r", encoding="utf-8", errors="replace"
|
||||
) as f:
|
||||
# 移动文件指针到文件末尾,继续监听新增内容
|
||||
await f.seek(0, 2)
|
||||
@@ -1070,7 +1070,7 @@ async def get_logging(
|
||||
try:
|
||||
# 使用 aiofiles 异步读取文件
|
||||
async with aiofiles.open(
|
||||
log_path, mode="r", encoding="utf-8", errors="ignore"
|
||||
log_path, mode="r", encoding="utf-8", errors="replace"
|
||||
) as file:
|
||||
text = await file.read()
|
||||
# 倒序输出
|
||||
|
||||
@@ -70,7 +70,7 @@ async def update_cookie(req: schemas.CookieData):
|
||||
content = json.dumps({"encrypted": req.encrypted})
|
||||
async with aiofiles.open(file_path, encoding="utf-8", mode="w") as file:
|
||||
await file.write(content)
|
||||
async with aiofiles.open(file_path, encoding="utf-8", mode="r") as file:
|
||||
async with aiofiles.open(file_path, encoding="utf-8", errors="replace", mode="r") as file:
|
||||
read_content = await file.read()
|
||||
if read_content == content:
|
||||
return {"action": "done"}
|
||||
@@ -89,7 +89,7 @@ async def load_encrypt_data(uuid: str) -> Dict[str, Any]:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
|
||||
# 读取文件
|
||||
async with aiofiles.open(file_path, encoding="utf-8", mode="r") as file:
|
||||
async with aiofiles.open(file_path, encoding="utf-8", errors="replace", mode="r") as file:
|
||||
read_content = await file.read()
|
||||
data = json.loads(read_content.encode("utf-8"))
|
||||
return data
|
||||
|
||||
@@ -292,7 +292,7 @@ class SystemChain(ChainBase):
|
||||
version_file = Path(settings.FRONTEND_PATH) / "version.txt"
|
||||
if version_file.exists():
|
||||
try:
|
||||
with open(version_file, 'r') as f:
|
||||
with open(version_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||
version = str(f.read()).strip()
|
||||
return version
|
||||
except Exception as err:
|
||||
|
||||
16
app/cli.py
16
app/cli.py
@@ -53,7 +53,7 @@ def _read_json_file(path: Path) -> Optional[Dict[str, Any]]:
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
return json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
@@ -158,7 +158,7 @@ def _http_request(
|
||||
"text": raw,
|
||||
}
|
||||
except HTTPError as exc:
|
||||
raw = exc.read().decode("utf-8", errors="ignore")
|
||||
raw = exc.read().decode("utf-8", errors="replace")
|
||||
try:
|
||||
data = json.loads(raw) if raw else None
|
||||
except json.JSONDecodeError:
|
||||
@@ -198,7 +198,7 @@ def _frontend_health(runtime: Optional[Dict[str, Any]] = None, timeout: float =
|
||||
request = Request(url=url, headers={"Accept": "text/plain"}, method="GET")
|
||||
try:
|
||||
with urlopen(request, timeout=timeout) as response:
|
||||
raw = response.read().decode("utf-8", errors="ignore").strip()
|
||||
raw = response.read().decode("utf-8", errors="replace").strip()
|
||||
return response.status == 200, {"version": raw}
|
||||
except (HTTPError, URLError):
|
||||
return False, None
|
||||
@@ -233,7 +233,7 @@ def _github_api_json(url: str, *, repo: str) -> Any:
|
||||
with opener.open(request, timeout=10.0) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
except HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="ignore")
|
||||
detail = exc.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"访问 GitHub API 失败(HTTP {exc.code}): {detail or url}") from exc
|
||||
except URLError as exc:
|
||||
raise RuntimeError(f"访问 GitHub API 失败:{exc.reason}") from exc
|
||||
@@ -342,7 +342,7 @@ def _best_effort_auto_update() -> None:
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="ignore",
|
||||
errors="replace",
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
@@ -451,7 +451,7 @@ def _annotation_name(annotation: Any) -> str:
|
||||
def _tail_lines(path: Path, count: int) -> list[str]:
|
||||
if not path.exists():
|
||||
raise click.ClickException(f"日志文件不存在:{path}")
|
||||
with path.open("r", encoding="utf-8", errors="ignore") as handle:
|
||||
with path.open("r", encoding="utf-8", errors="replace") as handle:
|
||||
return [line.rstrip("\n") for line in deque(handle, maxlen=count)]
|
||||
|
||||
|
||||
@@ -459,7 +459,7 @@ def _follow_file(path: Path) -> None:
|
||||
if not path.exists():
|
||||
raise click.ClickException(f"日志文件不存在:{path}")
|
||||
|
||||
with path.open("r", encoding="utf-8", errors="ignore") as handle:
|
||||
with path.open("r", encoding="utf-8", errors="replace") as handle:
|
||||
handle.seek(0, os.SEEK_END)
|
||||
while True:
|
||||
line = handle.readline()
|
||||
@@ -826,7 +826,7 @@ def _installed_frontend_version() -> Optional[str]:
|
||||
if not FRONTEND_VERSION_FILE.exists():
|
||||
return None
|
||||
try:
|
||||
return FRONTEND_VERSION_FILE.read_text(encoding="utf-8").strip() or None
|
||||
return FRONTEND_VERSION_FILE.read_text(encoding="utf-8", errors="replace").strip() or None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
@@ -428,7 +428,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
||||
return None
|
||||
|
||||
# 读取 __init__.py 文件,查找插件主类名
|
||||
with open(init_file, "r", encoding="utf-8") as f:
|
||||
with open(init_file, "r", encoding="utf-8", errors="replace") as f:
|
||||
source_code = f.read()
|
||||
|
||||
tree = ast.parse(source_code)
|
||||
@@ -1928,7 +1928,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
||||
修改Python文件中的类名和插件信息
|
||||
"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
content = f.read()
|
||||
|
||||
# 替换类名
|
||||
@@ -2017,7 +2017,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
||||
# 处理JS文件
|
||||
if file_path.suffix == '.js':
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
content = f.read()
|
||||
|
||||
# 替换类名引用(精确匹配)
|
||||
@@ -2042,7 +2042,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
||||
# 处理CSS文件
|
||||
elif file_path.suffix == '.css':
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
content = f.read()
|
||||
|
||||
# 替换CSS中可能的类名引用
|
||||
|
||||
@@ -131,7 +131,7 @@ def _read_json(path: Path) -> Optional[dict[str, Any]]:
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
data = json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
return data if isinstance(data, dict) else None
|
||||
@@ -249,7 +249,7 @@ def _backend_health_payload(port: int, timeout: float = BACKEND_HEALTH_TIMEOUT)
|
||||
with urlopen(request, timeout=timeout) as response:
|
||||
if response.status != 200:
|
||||
return None
|
||||
raw = response.read().decode("utf-8", errors="ignore")
|
||||
raw = response.read().decode("utf-8", errors="replace")
|
||||
except (HTTPError, URLError):
|
||||
return None
|
||||
except OSError:
|
||||
@@ -672,7 +672,7 @@ def _check_frontend_assets(runner: DoctorRunnerProtocol) -> None:
|
||||
|
||||
version = ""
|
||||
try:
|
||||
version = (frontend_dir / "version.txt").read_text(encoding="utf-8").strip()
|
||||
version = (frontend_dir / "version.txt").read_text(encoding="utf-8", errors="replace").strip()
|
||||
except OSError:
|
||||
version = "unknown"
|
||||
runner.add(
|
||||
|
||||
@@ -126,7 +126,7 @@ class CookieCloudHelper:
|
||||
return {}
|
||||
|
||||
# 读取文件
|
||||
with open(file_path, encoding="utf-8", mode="r") as file:
|
||||
with open(file_path, encoding="utf-8", errors="replace", mode="r") as file:
|
||||
read_content = file.read()
|
||||
data = json.loads(read_content.encode("utf-8"))
|
||||
return data
|
||||
|
||||
@@ -232,7 +232,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
if not package_file.exists():
|
||||
return {}
|
||||
try:
|
||||
content = package_file.read_text(encoding="utf-8")
|
||||
content = package_file.read_text(encoding="utf-8", errors="replace")
|
||||
payload = json.loads(content)
|
||||
except Exception as e:
|
||||
logger.warn(f"读取本地插件包 {package_file} 失败:{e}")
|
||||
@@ -1087,7 +1087,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
return roots
|
||||
|
||||
try:
|
||||
with open(requirements_file, "r", encoding="utf-8") as f:
|
||||
with open(requirements_file, "r", encoding="utf-8", errors="replace") as f:
|
||||
for raw_line in f:
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
@@ -1289,7 +1289,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
"""
|
||||
conflicts = []
|
||||
try:
|
||||
with open(requirements_file, "r", encoding="utf-8") as f:
|
||||
with open(requirements_file, "r", encoding="utf-8", errors="replace") as f:
|
||||
for raw_line in f:
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
@@ -1890,7 +1890,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
"""
|
||||
dependencies = {}
|
||||
try:
|
||||
with open(requirements_file, "r", encoding="utf-8") as f:
|
||||
with open(requirements_file, "r", encoding="utf-8", errors="replace") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
@@ -2440,7 +2440,7 @@ class PluginHelper(metaclass=WeakSingleton):
|
||||
"""
|
||||
dependencies = {}
|
||||
try:
|
||||
async with aiofiles.open(requirements_file, "r", encoding="utf-8") as f:
|
||||
async with aiofiles.open(requirements_file, "r", encoding="utf-8", errors="replace") as f:
|
||||
async for line in f:
|
||||
line = str(line).strip()
|
||||
if line and not line.startswith('#'):
|
||||
|
||||
@@ -238,7 +238,7 @@ class MoviePilotServerHelper:
|
||||
version_file = Path(settings.FRONTEND_PATH) / "version.txt"
|
||||
if version_file.exists():
|
||||
try:
|
||||
with open(version_file, "r") as file:
|
||||
with open(version_file, "r", encoding="utf-8", errors="replace") as file:
|
||||
version = str(file.read()).strip()
|
||||
return version or FRONTEND_VERSION
|
||||
except Exception as err:
|
||||
|
||||
@@ -343,7 +343,7 @@ class SkillHelper(metaclass=WeakSingleton):
|
||||
if not meta_path.exists():
|
||||
return {}
|
||||
try:
|
||||
payload = json.loads(meta_path.read_text(encoding="utf-8"))
|
||||
payload = json.loads(meta_path.read_text(encoding="utf-8", errors="replace"))
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
except Exception as e:
|
||||
logger.warning("读取技能来源元数据失败:%s - %s", meta_path, e)
|
||||
@@ -381,7 +381,7 @@ class SkillHelper(metaclass=WeakSingleton):
|
||||
if not skill_md.exists():
|
||||
continue
|
||||
try:
|
||||
content = skill_md.read_text(encoding="utf-8")
|
||||
content = skill_md.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception as e:
|
||||
logger.warning("读取技能文件失败:%s - %s", skill_md, e)
|
||||
continue
|
||||
@@ -979,7 +979,7 @@ class SkillHelper(metaclass=WeakSingleton):
|
||||
|
||||
content = getattr(response, "content", b"")
|
||||
if isinstance(content, bytes):
|
||||
return content.decode("utf-8", errors="ignore")
|
||||
return content.decode("utf-8", errors="replace")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
return ""
|
||||
|
||||
@@ -55,7 +55,7 @@ class SystemHelper(ConfigReloadMixin):
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
payload = json.loads(path.read_text(encoding="utf-8", errors="replace"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return None
|
||||
return payload if isinstance(payload, dict) else None
|
||||
@@ -149,7 +149,7 @@ class SystemHelper(ConfigReloadMixin):
|
||||
return None
|
||||
|
||||
try:
|
||||
raw_mode = path.read_text(encoding="utf-8")
|
||||
raw_mode = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError as err:
|
||||
logger.warning(f"读取一次性升级标记失败: {err}")
|
||||
raw_mode = ""
|
||||
@@ -214,7 +214,7 @@ class SystemHelper(ConfigReloadMixin):
|
||||
"""
|
||||
container_id = None
|
||||
try:
|
||||
with open("/proc/self/mountinfo", "r") as f:
|
||||
with open("/proc/self/mountinfo", "r", encoding="utf-8", errors="replace") as f:
|
||||
data = f.read()
|
||||
index_resolv_conf = data.find("resolv.conf")
|
||||
if index_resolv_conf != -1:
|
||||
|
||||
@@ -597,7 +597,7 @@ class Rclone(StorageBase):
|
||||
if not file_path or not Path(file_path).exists():
|
||||
return None
|
||||
# 读取rclone文件,检查是否有[MP]节点配置
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
if not lines:
|
||||
return None
|
||||
|
||||
@@ -287,7 +287,7 @@ class Rtorrent:
|
||||
if isinstance(content, bytes):
|
||||
# 检查是否为磁力链接(bytes形式)
|
||||
if content.startswith(b"magnet:"):
|
||||
content = content.decode("utf-8", errors="ignore")
|
||||
content = content.decode("utf-8", errors="replace")
|
||||
else:
|
||||
# 种子文件内容,使用load.raw
|
||||
raw = xmlrpc.client.Binary(content)
|
||||
|
||||
@@ -44,7 +44,7 @@ class CategoryHelper(metaclass=WeakSingleton):
|
||||
try:
|
||||
if not self._category_path.exists():
|
||||
shutil.copy(settings.INNER_CONFIG_PATH / "category.yaml", self._category_path)
|
||||
with open(self._category_path, mode='r', encoding='utf-8') as f:
|
||||
with open(self._category_path, mode='r', encoding='utf-8', errors='replace') as f:
|
||||
try:
|
||||
yaml_loader = ruamel.yaml.YAML()
|
||||
self._categorys = yaml_loader.load(f)
|
||||
@@ -67,7 +67,7 @@ class CategoryHelper(metaclass=WeakSingleton):
|
||||
if not self._category_path.exists():
|
||||
return config
|
||||
try:
|
||||
with open(self._category_path, 'r', encoding='utf-8') as f:
|
||||
with open(self._category_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
yaml_loader = ruamel.yaml.YAML()
|
||||
data = yaml_loader.load(f)
|
||||
if data:
|
||||
|
||||
@@ -78,7 +78,7 @@ class WechatClawBotModule(_ModuleBase, _MessageBase[WechatClawBot]):
|
||||
if isinstance(body, dict):
|
||||
payload = body
|
||||
elif isinstance(body, bytes):
|
||||
payload = json.loads(body.decode("utf-8", errors="ignore"))
|
||||
payload = json.loads(body.decode("utf-8", errors="replace"))
|
||||
else:
|
||||
payload = json.loads(body)
|
||||
while isinstance(payload, str):
|
||||
|
||||
@@ -491,14 +491,14 @@ class Monitor(ConfigReloadMixin, metaclass=SingletonClass):
|
||||
if system == 'Linux':
|
||||
# 检查 inotify 限制
|
||||
try:
|
||||
with open('/proc/sys/fs/inotify/max_user_watches', 'r') as f:
|
||||
with open('/proc/sys/fs/inotify/max_user_watches', 'r', encoding='utf-8', errors='replace') as f:
|
||||
limits['max_user_watches'] = int(f.read().strip())
|
||||
except Exception as e:
|
||||
logger.debug(f"读取 inotify 限制失败: {e}")
|
||||
limits['max_user_watches'] = 8192 # 默认值
|
||||
|
||||
try:
|
||||
with open('/proc/sys/fs/inotify/max_user_instances', 'r') as f:
|
||||
with open('/proc/sys/fs/inotify/max_user_instances', 'r', encoding='utf-8', errors='replace') as f:
|
||||
limits['max_user_instances'] = int(f.read().strip())
|
||||
except Exception as e:
|
||||
logger.debug(f"读取 inotify 实例限制失败: {e}")
|
||||
|
||||
@@ -1,68 +1,62 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
from anyio import Path as AsyncPath
|
||||
|
||||
from app.agent.middleware.jobs import _alist_jobs, filter_active_jobs
|
||||
|
||||
|
||||
class JobsMiddlewareTest(unittest.TestCase):
|
||||
def test_filter_active_jobs_only_keeps_pending_and_in_progress(self):
|
||||
jobs_metadata = [
|
||||
{
|
||||
"id": "pending-job",
|
||||
"name": "待执行任务",
|
||||
"description": "desc",
|
||||
"path": "/tmp/pending/JOB.md",
|
||||
"schedule": "once",
|
||||
"status": "pending",
|
||||
"last_run": None,
|
||||
},
|
||||
{
|
||||
"id": "running-job",
|
||||
"name": "执行中任务",
|
||||
"description": "desc",
|
||||
"path": "/tmp/running/JOB.md",
|
||||
"schedule": "recurring",
|
||||
"status": "in_progress",
|
||||
"last_run": "2026-05-10 10:00",
|
||||
},
|
||||
{
|
||||
"id": "completed-recurring-job",
|
||||
"name": "已完成循环任务",
|
||||
"description": "desc",
|
||||
"path": "/tmp/completed/JOB.md",
|
||||
"schedule": "recurring",
|
||||
"status": "completed",
|
||||
"last_run": "2026-05-10 11:00",
|
||||
},
|
||||
{
|
||||
"id": "cancelled-job",
|
||||
"name": "已取消任务",
|
||||
"description": "desc",
|
||||
"path": "/tmp/cancelled/JOB.md",
|
||||
"schedule": "once",
|
||||
"status": "cancelled",
|
||||
"last_run": None,
|
||||
},
|
||||
]
|
||||
def test_filter_active_jobs_only_keeps_pending_and_in_progress():
|
||||
"""筛选任务时只保留待执行和执行中的任务。"""
|
||||
jobs_metadata = [
|
||||
{
|
||||
"id": "pending-job",
|
||||
"name": "待执行任务",
|
||||
"description": "desc",
|
||||
"path": "/tmp/pending/JOB.md",
|
||||
"schedule": "once",
|
||||
"status": "pending",
|
||||
"last_run": None,
|
||||
},
|
||||
{
|
||||
"id": "running-job",
|
||||
"name": "执行中任务",
|
||||
"description": "desc",
|
||||
"path": "/tmp/running/JOB.md",
|
||||
"schedule": "recurring",
|
||||
"status": "in_progress",
|
||||
"last_run": "2026-05-10 10:00",
|
||||
},
|
||||
{
|
||||
"id": "completed-recurring-job",
|
||||
"name": "已完成循环任务",
|
||||
"description": "desc",
|
||||
"path": "/tmp/completed/JOB.md",
|
||||
"schedule": "recurring",
|
||||
"status": "completed",
|
||||
"last_run": "2026-05-10 11:00",
|
||||
},
|
||||
{
|
||||
"id": "cancelled-job",
|
||||
"name": "已取消任务",
|
||||
"description": "desc",
|
||||
"path": "/tmp/cancelled/JOB.md",
|
||||
"schedule": "once",
|
||||
"status": "cancelled",
|
||||
"last_run": None,
|
||||
},
|
||||
]
|
||||
|
||||
active_job_ids = [job["id"] for job in filter_active_jobs(jobs_metadata)]
|
||||
active_job_ids = [job["id"] for job in filter_active_jobs(jobs_metadata)]
|
||||
|
||||
self.assertEqual(["pending-job", "running-job"], active_job_ids)
|
||||
assert active_job_ids == ["pending-job", "running-job"]
|
||||
|
||||
|
||||
class JobsMiddlewareAsyncTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_alist_jobs_sorts_job_directories_by_name(self):
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
root = Path(tempdir)
|
||||
|
||||
for job_id in ("z-job", "a-job", "m-job"):
|
||||
job_dir = root / job_id
|
||||
job_dir.mkdir()
|
||||
(job_dir / "JOB.md").write_text(
|
||||
f"""---
|
||||
def test_alist_jobs_sorts_job_directories_by_name(tmp_path):
|
||||
"""加载任务元数据时按任务目录名稳定排序。"""
|
||||
for job_id in ("z-job", "a-job", "m-job"):
|
||||
job_dir = tmp_path / job_id
|
||||
job_dir.mkdir()
|
||||
(job_dir / "JOB.md").write_text(
|
||||
f"""---
|
||||
name: {job_id}
|
||||
description: test
|
||||
schedule: once
|
||||
@@ -70,9 +64,30 @@ status: pending
|
||||
---
|
||||
# {job_id}
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
jobs = await _alist_jobs(AsyncPath(str(root)))
|
||||
jobs = anyio.run(_alist_jobs, AsyncPath(str(tmp_path)))
|
||||
|
||||
self.assertEqual(["a-job", "m-job", "z-job"], [job["id"] for job in jobs])
|
||||
assert [job["id"] for job in jobs] == ["a-job", "m-job", "z-job"]
|
||||
|
||||
|
||||
def test_alist_jobs_tolerates_invalid_utf8_bytes(tmp_path):
|
||||
"""加载任务元数据时非法 UTF-8 字节不应中断整个 Agent。"""
|
||||
job_dir = tmp_path / "broken-job"
|
||||
job_dir.mkdir()
|
||||
(job_dir / "JOB.md").write_bytes(
|
||||
b"""---
|
||||
name: broken-job
|
||||
description: test
|
||||
schedule: once
|
||||
status: pending
|
||||
---
|
||||
# broken-job
|
||||
\x90
|
||||
"""
|
||||
)
|
||||
|
||||
jobs = anyio.run(_alist_jobs, AsyncPath(str(tmp_path)))
|
||||
|
||||
assert [job["id"] for job in jobs] == ["broken-job"]
|
||||
|
||||
Reference in New Issue
Block a user