feat(plugins): add source-aware evidence and recording timelines

This commit is contained in:
chenmingxuan committed 2026-10-16 10:20:11 +08:00
1 parent 72ff342605
commit ef7af9988e
61 files changed
+3847 -108

No files matched your search

+61 -1
View File
@@ -57,8 +57,44 @@ class PluginObservation(PluginModel):
refresh_tools: list[str] = Field(default_factory=list)
class PluginToolContract(PluginModel):
"""Host-enforced capability description; independent of a tool's prose Skill."""
effect: Literal["read", "write"]
observation: bool = False
idempotent: bool = False
target_argument: str = ""
cancellation: Literal["confirmed", "unknown"] = "unknown"
evidence: list[Literal["text", "image", "file", "state"]] = Field(default_factory=list)
@model_validator(mode="after")
def observation_is_read_only(self):
if self.observation and self.effect != "read":
raise ValueError("自动观察工具必须声明为只读")
return self
class PluginCapabilities(PluginModel):
kind: Literal["visual", "terminal", "api", "file", "other"] = "other"
recording: Literal["none", "windows"] = "none"
tools: dict[str, PluginToolContract] = Field(default_factory=dict)
required_tools: list[str] = Field(default_factory=list)
class EvidenceSource(PluginModel):
kind: Literal["android", "computer", "terminal", "api", "file", "other"] = "other"
target_id: str = Field(min_length=1, max_length=256)
label: str = Field(default="", max_length=500)
captured_at: float | None = None
width: int = Field(default=0, ge=0, le=32768)
height: int = Field(default=0, ge=0, le=32768)
# Physical desktop coordinates, never normalized model coordinates.
window: dict[str, int] | None = None
screen: dict[str, int] | None = None
class PluginManifest(PluginModel):
schema_version: Literal[1] = 1
schema_version: Literal[1, 2] = 1
id: Identifier
name: str = Field(min_length=1, max_length=100)
version: str
@@ -76,6 +112,7 @@ class PluginManifest(PluginModel):
resources: list[str] = Field(default_factory=list)
targets: list[ExecutionTarget] = Field(default_factory=lambda: ["mixed"])
observation: PluginObservation | None = None
capabilities: PluginCapabilities | None = None
@field_validator("version")
@classmethod
@@ -98,6 +135,18 @@ class PluginManifest(PluginModel):
for name, field in self.config.items():
if field.secret and field.default:
raise ValueError(f"密钥字段 {name} 不允许默认值")
if self.schema_version == 1 and self.capabilities is not None:
raise ValueError("能力契约需要 schema_version=2;旧清单保持兼容")
if self.schema_version == 2:
if self.capabilities is None:
raise ValueError("第二版插件必须声明 capabilities")
if self.observation:
for tool in {self.observation.tool, *self.observation.refresh_tools}:
spec = self.capabilities.tools.get(tool)
if spec is None or not spec.observation or spec.effect != "read":
raise ValueError(f"观察工具 {tool} 缺少只读能力声明")
if self.capabilities.recording != "none" and self.capabilities.kind != "visual":
raise ValueError("只有视觉插件可以声明屏幕录像")
return self
def check_compatible(self, artemis_version: str | None = None) -> None:
@@ -164,6 +213,17 @@ class PluginCallResult(PluginModel):
version: str = ""
tool: str = ""
image_names: list[str] = Field(default_factory=list)
call_id: str = ""
source: EvidenceSource | None = None
source_id: str = ""
error_code: str = ""
execution: Literal["not_started", "completed", "partial", "unknown"] = "unknown"
side_effect: Literal["none", "possible", "confirmed"] = "possible"
retryable: bool = False
requires_observation: bool = False
attachments: list[dict[str, Any]] = Field(default_factory=list)
pre_image_valid: bool = False
recording: dict[str, Any] | None = None
def text(self) -> str:
import json