mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-06 07:09:09 +08:00
Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
This commit is contained in:
@@ -1 +1,3 @@
|
||||
"""Agent 持久 Python 会话、逐调用只读边界与工具链桥接。"""
|
||||
"""
|
||||
Agent 持久 Python 会话、逐调用只读边界与工具链桥接。
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""每个 Python cell 独立的宿主身份、只读工具集合和调用预算。"""
|
||||
"""
|
||||
每个 Python cell 独立的宿主身份、只读工具集合和调用预算。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
@@ -32,17 +34,23 @@ Dispatch = Callable[[Any, dict[str, Any]], Awaitable[Any]]
|
||||
|
||||
|
||||
def allowed_tools(tools: list[Any]) -> dict[str, Any]:
|
||||
"""只取本轮实际启用的原生工具对象,插件或 MCP 同名工具不能借用只读声明。"""
|
||||
"""
|
||||
只取本轮实际启用的原生工具对象,插件或 MCP 同名工具不能借用只读声明。
|
||||
"""
|
||||
return {tool.name: tool for tool in tools if type(tool) in _READ_CLASSES
|
||||
and getattr(tool, '_agent_tool_source', None) == 'builtin'}
|
||||
|
||||
|
||||
class CellAuthority:
|
||||
"""当前 cell 的调用窗口;持久 socket 和 Python 状态不延长宿主调用身份。"""
|
||||
"""
|
||||
当前 cell 的调用窗口;持久 socket 和 Python 状态不延长宿主调用身份。
|
||||
"""
|
||||
|
||||
def __init__(self, scope: TerminalScope, context: ToolPolicyContext,
|
||||
tools: dict[str, Any], dispatch: Dispatch) -> None:
|
||||
"""从正在执行的宿主协程复制上下文,每次 cell 都重新建立,结束即撤销。"""
|
||||
"""
|
||||
从正在执行的宿主协程复制上下文,每次 cell 都重新建立,结束即撤销。
|
||||
"""
|
||||
self.scope, self.context, self.tools = scope, context, tools
|
||||
self._dispatch = dispatch
|
||||
self._context = contextvars.copy_context()
|
||||
@@ -53,21 +61,27 @@ class CellAuthority:
|
||||
self.errors: list[dict[str, str]] = []
|
||||
|
||||
def available(self) -> bool:
|
||||
"""角色引用随宿主更新而变化,排队或持久连接不得继续使用已撤销权限。"""
|
||||
"""
|
||||
角色引用随宿主更新而变化,排队或持久连接不得继续使用已撤销权限。
|
||||
"""
|
||||
return (self._active and not self.scope.closed and bool(self.scope.user_id)
|
||||
and self.scope.user_id == self.context.user_id
|
||||
and self.context.principal.role is PrincipalRole.SYSTEM_ADMIN
|
||||
and self.context.origin not in {ToolOrigin.SUBAGENT, ToolOrigin.AGENT_API})
|
||||
|
||||
def _error(self, name: str, reason: str) -> dict[str, Any]:
|
||||
"""即使脚本忽略失败回执,外层结果仍保留最多五条明确错误。"""
|
||||
"""
|
||||
即使脚本忽略失败回执,外层结果仍保留最多五条明确错误。
|
||||
"""
|
||||
if len(self.errors) < 5:
|
||||
self.errors.append({'tool': name, 'error': reason})
|
||||
return {'success': False, 'error': reason}
|
||||
|
||||
@staticmethod
|
||||
def _read_allowed(tool: Any, arguments: dict[str, Any]) -> bool:
|
||||
"""API 的操作副作用与敏感参数都由宿主合同解析,不采用模型声明的读取标签。"""
|
||||
"""
|
||||
API 的操作副作用与敏感参数都由宿主合同解析,不采用模型声明的读取标签。
|
||||
"""
|
||||
if type(tool) is not MoviePilotApiTool:
|
||||
return type(tool) in {ReadFileTool, SearchWebTool}
|
||||
operation = resolve_api_operation(str(arguments.get('operation_id') or ''))
|
||||
@@ -78,7 +92,9 @@ class CellAuthority:
|
||||
return not requests_system_setting_secrets(normalized)
|
||||
|
||||
async def call(self, name: str, arguments: dict[str, Any]) -> Any:
|
||||
"""逐次检查、计数并在当前 cell 上下文执行,串行 RPC 不会超发预算。"""
|
||||
"""
|
||||
逐次检查、计数并在当前 cell 上下文执行,串行 RPC 不会超发预算。
|
||||
"""
|
||||
if not self.available():
|
||||
return self._error(name, '当前 Python 调用窗口已结束或用户权限已变化。')
|
||||
tool = self.tools.get(name)
|
||||
@@ -107,12 +123,16 @@ class CellAuthority:
|
||||
return result
|
||||
|
||||
def _record(self, name: str, arguments: dict[str, Any], started: float, outcome: ExecutionOutcome) -> None:
|
||||
"""每次实际准入调用都有脱敏回执,中断也不能从调用记录中消失。"""
|
||||
"""
|
||||
每次实际准入调用都有脱敏回执,中断也不能从调用记录中消失。
|
||||
"""
|
||||
self.log.append({'tool': name, 'args_preview': summarize_input(arguments, max_chars=80),
|
||||
'duration': round(time.monotonic() - started, 2), 'outcome': outcome.value})
|
||||
|
||||
async def _invoke(self, tool: Any, arguments: dict[str, Any]) -> Any:
|
||||
"""重新检查排队期间变化的身份;工具异常只产生脱敏回执,取消保持传播。"""
|
||||
"""
|
||||
重新检查排队期间变化的身份;工具异常只产生脱敏回执,取消保持传播。
|
||||
"""
|
||||
if not self.available():
|
||||
return self._error(tool.name, '当前 Python 调用窗口已结束或用户权限已变化。')
|
||||
try:
|
||||
@@ -124,7 +144,9 @@ class CellAuthority:
|
||||
return result
|
||||
|
||||
async def retire(self) -> None:
|
||||
"""先同步封口,再取消 cell 遗留调用并等待真实结算。"""
|
||||
"""
|
||||
先同步封口,再取消 cell 遗留调用并等待真实结算。
|
||||
"""
|
||||
self._active = False
|
||||
pending = [task for task in self._pending if not task.done()]
|
||||
if pending:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""按真实会话 owner 管理 Python 内核,空闲 LRU 与后台回收共享同一规则。"""
|
||||
"""
|
||||
按真实会话 owner 管理 Python 内核,空闲 LRU 与后台回收共享同一规则。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
@@ -24,7 +26,16 @@ REAPER_MAX_SECONDS = 300.0
|
||||
|
||||
@dataclass(eq=False)
|
||||
class KernelEntry:
|
||||
"""排队中的 cell 也占用 owner 引用,空闲回收不能杀掉尚未取得锁的调用。"""
|
||||
"""
|
||||
排队中的 cell 也占用 owner 引用,空闲回收不能杀掉尚未取得锁的调用。
|
||||
|
||||
:param scope: 内核所属的可信会话身份和所有权范围
|
||||
:param kernel: 保存该会话 Python 变量状态的内核实例
|
||||
:param lock: 串行执行该内核中代码单元的异步锁
|
||||
:param attached: 正在执行或排队等待内核的任务集合,非空时禁止空闲回收
|
||||
:param last_used: 上次使用的单调时钟秒数,用于计算空闲时长
|
||||
:param retired: 是否已进入回收状态;回收期间禁止继续取得此内核
|
||||
"""
|
||||
|
||||
scope: TerminalScope
|
||||
kernel: PythonKernel
|
||||
@@ -35,17 +46,23 @@ class KernelEntry:
|
||||
|
||||
|
||||
class CodeSessionManager:
|
||||
"""每会话变量跨轮保留,默认四个内核的空闲 LRU 和三十分钟空闲过期。"""
|
||||
"""
|
||||
每会话变量跨轮保留,默认四个内核的空闲 LRU 和三十分钟空闲过期。
|
||||
"""
|
||||
|
||||
def __init__(self, root: Path, *, max_kernels: int = 4, idle_seconds: float = 1800) -> None:
|
||||
"""显式注入暂存根目录,不在模块导入时读取配置或启动后台任务。"""
|
||||
"""
|
||||
显式注入暂存根目录,不在模块导入时读取配置或启动后台任务。
|
||||
"""
|
||||
self.root, self.max_kernels, self.idle_seconds = root, max_kernels, idle_seconds
|
||||
self._entries: dict[tuple[Any, ...], KernelEntry] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
self._reaper: asyncio.Task[Any] | None = None
|
||||
|
||||
def _new_kernel(self, authority: CellAuthority, cwd: Path) -> PythonKernel:
|
||||
"""内核文件按用户隔离,随机代次不会继承旧会话的变量、令牌或 socket。"""
|
||||
"""
|
||||
内核文件按用户隔离,随机代次不会继承旧会话的变量、令牌或 socket。
|
||||
"""
|
||||
user_key = build_user_memory_key(authority.scope.user_id)
|
||||
if not user_key:
|
||||
raise TerminalAccessError()
|
||||
@@ -55,7 +72,9 @@ class CodeSessionManager:
|
||||
blocking=partial(run_agent_blocking, 'default'), schemas=schemas)
|
||||
|
||||
async def _acquire(self, authority: CellAuthority, cwd: Path) -> tuple[KernelEntry, tuple[Any, ...]]:
|
||||
"""键包含 owner 对象、工具集合、解释器和工作目录;身份相同的旧代次也不能复用。"""
|
||||
"""
|
||||
键包含 owner 对象、工具集合、解释器和工作目录;身份相同的旧代次也不能复用。
|
||||
"""
|
||||
scope, tools = authority.scope, tuple(sorted(authority.tools))
|
||||
key = (scope, tools, sys.executable, str(cwd))
|
||||
async with self._lock:
|
||||
@@ -85,7 +104,9 @@ class CodeSessionManager:
|
||||
|
||||
async def execute(self, authority: CellAuthority, code: str, *, cwd: Path,
|
||||
reset: bool = False, timeout: float = 300) -> dict[str, Any]:
|
||||
"""每次执行重新绑定当前 cell 身份,完成后撤销并保留仍有效的 Python 状态。"""
|
||||
"""
|
||||
每次执行重新绑定当前 cell 身份,完成后撤销并保留仍有效的 Python 状态。
|
||||
"""
|
||||
entry, key = await self._acquire(authority, cwd)
|
||||
try:
|
||||
async with entry.lock:
|
||||
@@ -103,7 +124,9 @@ class CodeSessionManager:
|
||||
|
||||
async def _run(self, entry: KernelEntry, authority: CellAuthority, code: str,
|
||||
*, reset: bool, timeout: float) -> dict[str, Any]:
|
||||
"""显式重置、已结束内核或失联会丢失状态;普通脚本异常保留已执行的赋值。"""
|
||||
"""
|
||||
显式重置、已结束内核或失联会丢失状态;普通脚本异常保留已执行的赋值。
|
||||
"""
|
||||
started = time.monotonic()
|
||||
kernel = entry.kernel
|
||||
state_reset = kernel.process is not None and (reset or kernel.closed or kernel.process.returncode is not None)
|
||||
@@ -139,12 +162,16 @@ class CodeSessionManager:
|
||||
|
||||
@staticmethod
|
||||
def _ended(status: str, error: str) -> dict[str, Any]:
|
||||
"""通信和取消边界只报告已确认的状态丢失,不把未知执行情况描述成成功。"""
|
||||
"""
|
||||
通信和取消边界只报告已确认的状态丢失,不把未知执行情况描述成成功。
|
||||
"""
|
||||
return {'status': status, 'error': error, 'output': '', 'exit_code': -1,
|
||||
'kernel': {'mode': 'session', 'ended': True, 'state_lost': True}}
|
||||
|
||||
async def _sweep_locked(self) -> None:
|
||||
"""空闲过期和容量淘汰都跳过活跃或排队中的 cell,完成关闭才移除登记。"""
|
||||
"""
|
||||
空闲过期和容量淘汰都跳过活跃或排队中的 cell,完成关闭才移除登记。
|
||||
"""
|
||||
now = time.monotonic()
|
||||
idle = sorted(((key, entry) for key, entry in self._entries.items() if not entry.attached),
|
||||
key=lambda item: item[1].last_used)
|
||||
@@ -155,14 +182,18 @@ class CodeSessionManager:
|
||||
self._entries.pop(key, None)
|
||||
|
||||
async def reap(self) -> None:
|
||||
"""低频后台回收不依赖下一次调用,七天暂存残留只在专属目录内清理。"""
|
||||
"""
|
||||
低频后台回收不依赖下一次调用,七天暂存残留只在专属目录内清理。
|
||||
"""
|
||||
async with self._lock:
|
||||
await self._sweep_locked()
|
||||
active = {entry.kernel.directory for entry in self._entries.values()}
|
||||
await run_agent_blocking('default', sweep_stale, self.root, active)
|
||||
|
||||
async def _reap_loop(self) -> None:
|
||||
"""登记器关停时回收所有 owner;正常空表退出,下一次使用再惰性启动。"""
|
||||
"""
|
||||
登记器关停时回收所有 owner;正常空表退出,下一次使用再惰性启动。
|
||||
"""
|
||||
try:
|
||||
while self._entries:
|
||||
await asyncio.sleep(min(REAPER_MAX_SECONDS, max(REAPER_MIN_SECONDS, self.idle_seconds / 6)))
|
||||
@@ -172,7 +203,9 @@ class CodeSessionManager:
|
||||
await self.close_owner(scope)
|
||||
|
||||
async def close_owner(self, scope: TerminalScope) -> bool:
|
||||
"""先封住对应代次,再取消所有活跃及排队调用;超时保留登记而非假报已回收。"""
|
||||
"""
|
||||
先封住对应代次,再取消所有活跃及排队调用;超时保留登记而非假报已回收。
|
||||
"""
|
||||
entries = [(key, entry) for key, entry in self._entries.items() if entry.scope is scope]
|
||||
current = asyncio.current_task()
|
||||
tasks: set[asyncio.Task[Any]] = set()
|
||||
@@ -196,7 +229,9 @@ code_session_manager: CodeSessionManager | None = None
|
||||
|
||||
|
||||
def get_code_session_manager(root: Path) -> CodeSessionManager:
|
||||
"""由首次已绑定的 Agent 执行初始化;宿主关闭只访问已存在实例。"""
|
||||
"""
|
||||
由首次已绑定的 Agent 执行初始化;宿主关闭只访问已存在实例。
|
||||
"""
|
||||
global code_session_manager
|
||||
if code_session_manager is None:
|
||||
code_session_manager = CodeSessionManager(root)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""代码执行的模型输出投影,工具失败与内核状态各自如实报告。"""
|
||||
"""
|
||||
代码执行的模型输出投影,工具失败与内核状态各自如实报告。
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
@@ -12,7 +14,9 @@ _ANSI = re.compile(r'\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))')
|
||||
|
||||
def render_result(payload: dict[str, Any], *, directory: Path, output_directory: Path,
|
||||
reused: bool, state_reset: bool, elapsed: float) -> dict[str, Any]:
|
||||
"""完整 stdout 脱敏后才落盘,内核退出后仍能从独立输出目录继续读取。"""
|
||||
"""
|
||||
完整 stdout 脱敏后才落盘,内核退出后仍能从独立输出目录继续读取。
|
||||
"""
|
||||
source = read_cell_stdout(payload, directory) + str(payload.get('raw_stdout') or '')
|
||||
source_captured = len(source.encode('utf-8'))
|
||||
stdout = sanitize_archived_text(_ANSI.sub('', source))
|
||||
|
||||
Reference in New Issue
Block a user