diff --git a/app/agent/llm/provider.py b/app/agent/llm/provider.py index 43772948..dd31a0a3 100644 --- a/app/agent/llm/provider.py +++ b/app/agent/llm/provider.py @@ -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}") diff --git a/app/agent/middleware/activity_log.py b/app/agent/middleware/activity_log.py index ceddef12..6ca95a87 100644 --- a/app/agent/middleware/activity_log.py +++ b/app/agent/middleware/activity_log.py @@ -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" diff --git a/app/agent/middleware/jobs.py b/app/agent/middleware/jobs.py index 2ceff3e2..983a6a72 100644 --- a/app/agent/middleware/jobs.py +++ b/app/agent/middleware/jobs.py @@ -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( diff --git a/app/agent/middleware/memory.py b/app/agent/middleware/memory.py index 6bfef8ac..077d9c95 100644 --- a/app/agent/middleware/memory.py +++ b/app/agent/middleware/memory.py @@ -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) diff --git a/app/agent/middleware/skills.py b/app/agent/middleware/skills.py index c97b6637..bfe3d5a8 100644 --- a/app/agent/middleware/skills.py +++ b/app/agent/middleware/skills.py @@ -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 diff --git a/app/agent/prompt/__init__.py b/app/agent/prompt/__init__.py index 1ddf992f..89011dd8 100644 --- a/app/agent/prompt/__init__.py +++ b/app/agent/prompt/__init__.py @@ -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( diff --git a/app/agent/runtime.py b/app/agent/runtime.py index c2fea12b..cd1c0b76 100644 --- a/app/agent/runtime.py +++ b/app/agent/runtime.py @@ -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 diff --git a/app/agent/tools/impl/_terminal_session.py b/app/agent/tools/impl/_terminal_session.py index 1fa49731..55e0d91e 100644 --- a/app/agent/tools/impl/_terminal_session.py +++ b/app/agent/tools/impl/_terminal_session.py @@ -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 diff --git a/app/agent/tools/impl/edit_file.py b/app/agent/tools/impl/edit_file.py index 16b1d860..3c44b275 100644 --- a/app/agent/tools/impl/edit_file.py +++ b/app/agent/tools/impl/edit_file.py @@ -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 换行符。" diff --git a/app/agent/tools/impl/execute_command.py b/app/agent/tools/impl/execute_command.py index e2b417ca..c638a17c 100644 --- a/app/agent/tools/impl/execute_command.py +++ b/app/agent/tools/impl/execute_command.py @@ -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 分段写入临时文件。""" diff --git a/app/agent/tools/impl/read_file.py b/app/agent/tools/impl/read_file.py index 303cfdf5..ee99eddf 100644 --- a/app/agent/tools/impl/read_file.py +++ b/app/agent/tools/impl/read_file.py @@ -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: diff --git a/app/api/endpoints/message.py b/app/api/endpoints/message.py index cd68fb5b..d429c96e 100644 --- a/app/api/endpoints/message.py +++ b/app/api/endpoints/message.py @@ -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 ( diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index 2ccf283c..cc18678b 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -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() # 倒序输出 diff --git a/app/api/servcookie.py b/app/api/servcookie.py index 8e7f713d..a2108f31 100644 --- a/app/api/servcookie.py +++ b/app/api/servcookie.py @@ -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 diff --git a/app/chain/system.py b/app/chain/system.py index 29f84c4d..c3bc5cf6 100644 --- a/app/chain/system.py +++ b/app/chain/system.py @@ -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: diff --git a/app/cli.py b/app/cli.py index fd31ffbb..8111f325 100644 --- a/app/cli.py +++ b/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 diff --git a/app/core/plugin.py b/app/core/plugin.py index 5eb10787..c6539593 100644 --- a/app/core/plugin.py +++ b/app/core/plugin.py @@ -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中可能的类名引用 diff --git a/app/doctor/checks.py b/app/doctor/checks.py index 7b98fc65..1c1ea820 100644 --- a/app/doctor/checks.py +++ b/app/doctor/checks.py @@ -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( diff --git a/app/helper/cookiecloud.py b/app/helper/cookiecloud.py index 18055602..42e129d3 100644 --- a/app/helper/cookiecloud.py +++ b/app/helper/cookiecloud.py @@ -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 diff --git a/app/helper/plugin.py b/app/helper/plugin.py index 5623385e..fb538f18 100644 --- a/app/helper/plugin.py +++ b/app/helper/plugin.py @@ -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('#'): diff --git a/app/helper/server.py b/app/helper/server.py index 9bba2007..f5825861 100644 --- a/app/helper/server.py +++ b/app/helper/server.py @@ -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: diff --git a/app/helper/skill.py b/app/helper/skill.py index efd5648b..f8b7b686 100644 --- a/app/helper/skill.py +++ b/app/helper/skill.py @@ -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 "" diff --git a/app/helper/system.py b/app/helper/system.py index 30c08634..9f48daba 100644 --- a/app/helper/system.py +++ b/app/helper/system.py @@ -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: diff --git a/app/modules/filemanager/storages/rclone.py b/app/modules/filemanager/storages/rclone.py index 84e94697..0db4c3bb 100644 --- a/app/modules/filemanager/storages/rclone.py +++ b/app/modules/filemanager/storages/rclone.py @@ -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 diff --git a/app/modules/rtorrent/rtorrent.py b/app/modules/rtorrent/rtorrent.py index 09fc978d..aae7ea55 100644 --- a/app/modules/rtorrent/rtorrent.py +++ b/app/modules/rtorrent/rtorrent.py @@ -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) diff --git a/app/modules/themoviedb/category.py b/app/modules/themoviedb/category.py index 99e7104a..4fb3dbc6 100644 --- a/app/modules/themoviedb/category.py +++ b/app/modules/themoviedb/category.py @@ -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: diff --git a/app/modules/wechatclawbot/__init__.py b/app/modules/wechatclawbot/__init__.py index c4bce8f2..5055ebe8 100644 --- a/app/modules/wechatclawbot/__init__.py +++ b/app/modules/wechatclawbot/__init__.py @@ -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): diff --git a/app/monitor.py b/app/monitor.py index eee41b9c..11d61af5 100644 --- a/app/monitor.py +++ b/app/monitor.py @@ -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}") diff --git a/tests/test_agent_jobs_middleware.py b/tests/test_agent_jobs_middleware.py index 68fc7332..55f69a57 100644 --- a/tests/test_agent_jobs_middleware.py +++ b/tests/test_agent_jobs_middleware.py @@ -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"]