feat(plugins): add source-aware evidence and recording timelines
This commit is contained in:
1 parent
72ff342605
commit
ef7af9988e
61 files changed
+3847
-108
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user