feat(browser): complete camofox BrowserProvider with tabs, interaction, download capture and control lease
Contracts for browser API and browser.* agent capabilities; adapter implements close/list/restore sessions, tabs + reconcile, history, click/type/select/scroll/press, extract, waitForDownload (staged via DownloadSink, sha256), stale-ref guard and automation/human actor check. testkit mock camofox gains an interactive site (login redirect, captcha, click-driven page changes, downloads, PNG screenshots). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
62290d4477
commit
c17d4b90bd
12 files changed
+1832
-101
No files matched your search
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
import * as agent from "./agent.ts";
|
||||
import * as assets from "./assets.ts";
|
||||
import * as auth from "./auth.ts";
|
||||
import * as browser from "./browser.ts";
|
||||
import { CapabilityDescriptor, KnownCapabilityId } from "./capability.ts";
|
||||
import { type ApiScope, CursorQuery, cursorPage, HealthStatus, Id, IdParam } from "./common.ts";
|
||||
import * as content from "./content.ts";
|
||||
@@ -2008,6 +2009,167 @@ export const Api = {
|
||||
}),
|
||||
},
|
||||
|
||||
// ── 浏览器(C1-E):站点浏览器会话、原站视图、人工接管;写动作需人工持有控制权(接管) ──
|
||||
browser: {
|
||||
providers: ep({
|
||||
method: "get",
|
||||
path: "/browser/providers",
|
||||
scope: "admin",
|
||||
summary: "浏览器服务列表(地址、凭证引用、启用、能力 supported/verified);probe=true 现场探测并写回",
|
||||
query: browser.BrowserProvidersQuery,
|
||||
response: z.object({ items: z.array(browser.BrowserProviderView) }),
|
||||
}),
|
||||
sessions: ep({
|
||||
method: "get",
|
||||
path: "/browser/sessions",
|
||||
scope: "admin",
|
||||
summary: "浏览器会话列表(以数据库为准;默认只列未关闭的)",
|
||||
query: browser.BrowserSessionsQuery,
|
||||
response: z.object({ items: z.array(browser.BrowserSessionView) }),
|
||||
}),
|
||||
open: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions",
|
||||
scope: "admin",
|
||||
summary: "打开站点浏览器会话(同一站点/账号已有未关闭会话时直接返回它)",
|
||||
body: browser.BrowserSessionOpen,
|
||||
response: browser.BrowserSessionView,
|
||||
}),
|
||||
get: ep({
|
||||
method: "get",
|
||||
path: "/browser/sessions/{id}",
|
||||
scope: "admin",
|
||||
summary: "浏览器会话详情(控制者、标签页、租约、是否需重新读取页面)",
|
||||
params: browser.BrowserSessionParams,
|
||||
response: browser.BrowserSessionView,
|
||||
}),
|
||||
close: ep({
|
||||
method: "delete",
|
||||
path: "/browser/sessions/{id}",
|
||||
scope: "admin",
|
||||
summary: "关闭浏览器会话(关闭上游标签页,撤销租约)",
|
||||
params: browser.BrowserSessionParams,
|
||||
response: browser.BrowserSessionView,
|
||||
}),
|
||||
openTab: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/tabs",
|
||||
scope: "admin",
|
||||
summary: "新建标签页并设为当前(需持有控制权)",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserTabOpen,
|
||||
response: browser.BrowserSessionView,
|
||||
}),
|
||||
closeTab: ep({
|
||||
method: "delete",
|
||||
path: "/browser/sessions/{id}/tabs/{tabId}",
|
||||
scope: "admin",
|
||||
summary: "关闭标签页(需持有控制权)",
|
||||
params: browser.BrowserTabParams,
|
||||
response: browser.BrowserSessionView,
|
||||
}),
|
||||
navigate: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/navigate",
|
||||
scope: "admin",
|
||||
summary: "地址栏导航或后退/前进/刷新(需持有控制权;限站点域名)",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserNavigate,
|
||||
response: browser.BrowserNavigation,
|
||||
}),
|
||||
snapshot: ep({
|
||||
method: "get",
|
||||
path: "/browser/sessions/{id}/snapshot",
|
||||
scope: "admin",
|
||||
summary: "结构化页面快照(凭证已遮蔽)",
|
||||
params: browser.BrowserSessionParams,
|
||||
query: browser.BrowserSnapshotQuery,
|
||||
response: browser.BrowserSnapshot,
|
||||
}),
|
||||
screenshot: ep({
|
||||
method: "get",
|
||||
path: "/browser/sessions/{id}/screenshot",
|
||||
scope: "admin",
|
||||
summary: "当前画面截图(base64,只给原站视图,不进模型)",
|
||||
params: browser.BrowserSessionParams,
|
||||
query: browser.BrowserScreenshotQuery,
|
||||
response: browser.BrowserScreenshotView,
|
||||
}),
|
||||
click: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/click",
|
||||
scope: "admin",
|
||||
summary: "点击(快照引用 / 选择器 / 截图坐标;需持有控制权)",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserClick,
|
||||
response: browser.BrowserActionResult,
|
||||
}),
|
||||
type: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/type",
|
||||
scope: "admin",
|
||||
summary: "输入文本 / 站点账号凭证 / 按键(需持有控制权;凭证由服务端注入)",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserType,
|
||||
response: browser.BrowserActionResult,
|
||||
}),
|
||||
scroll: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/scroll",
|
||||
scope: "admin",
|
||||
summary: "滚动页面(需持有控制权)",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserScroll,
|
||||
response: browser.BrowserActionResult,
|
||||
}),
|
||||
select: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/select",
|
||||
scope: "admin",
|
||||
summary: "下拉框选择(需持有控制权)",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserSelect,
|
||||
response: browser.BrowserActionResult,
|
||||
}),
|
||||
extract: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/extract",
|
||||
scope: "admin",
|
||||
summary: "按 JSON Schema(x-ref)提取页面字段",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserExtract,
|
||||
response: browser.BrowserExtractResult,
|
||||
}),
|
||||
download: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/download",
|
||||
scope: "organize.execute",
|
||||
summary:
|
||||
"捕获浏览器下载 → 后端暂存并核验 infohash、匹配本地资源;给出 submit 时经 POST /acquisition-plans 同一流程提交",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserDownload,
|
||||
response: browser.BrowserDownloadResult,
|
||||
}),
|
||||
acquire: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/control/acquire",
|
||||
scope: "admin",
|
||||
summary: "人工接管:先停止该会话新的自动化输入并等进行中的操作收尾,再发放人工租约",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserControlAcquire,
|
||||
response: browser.BrowserControlResult,
|
||||
}),
|
||||
release: ep({
|
||||
method: "post",
|
||||
path: "/browser/sessions/{id}/control/release",
|
||||
scope: "admin",
|
||||
summary: "交还控制权:Agent 恢复前必须重新读取页面,旧元素引用失效",
|
||||
params: browser.BrowserSessionParams,
|
||||
body: browser.BrowserControlRelease,
|
||||
response: browser.BrowserControlResult,
|
||||
}),
|
||||
},
|
||||
|
||||
// ── 定时任务看板(W7-C):统一注册表 + Agent 巡检;全部只接受控制台会话 ──
|
||||
schedules: {
|
||||
list: ep({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { EntityBase, Id, JsonObject, NonNegInt, Timestamp } from "./common.ts";
|
||||
import { ManualAcquisitionResult } from "./execution.ts";
|
||||
import { InstanceHealth } from "./instances.ts";
|
||||
|
||||
// 浏览器能力合同(REQ §4.4、§4.7.2–4.7.4、§12 浏览器实体)。
|
||||
@@ -192,6 +193,8 @@ export const DownloadArtifact = z.object({
|
||||
bytes: BlobRef,
|
||||
/** 十六进制 sha256 */
|
||||
sha256: z.string().min(1),
|
||||
/** 已脱敏的下载来源地址(上游提供时) */
|
||||
sourceUrl: z.string().nullable().optional(),
|
||||
capturedAt: Timestamp,
|
||||
});
|
||||
export type DownloadArtifact = z.infer<typeof DownloadArtifact>;
|
||||
@@ -219,5 +222,221 @@ export const BrowserErrorKind = z.enum([
|
||||
"budget_exceeded",
|
||||
"control_conflict",
|
||||
"lock_timeout",
|
||||
/** 元素引用来自导航/交接之前的快照,已失效:先重新读取页面 */
|
||||
"stale_ref",
|
||||
]);
|
||||
export type BrowserErrorKind = z.infer<typeof BrowserErrorKind>;
|
||||
|
||||
// ── C1-E:浏览器 API 与 Agent 能力的请求/响应(docs/runbook/browser.md) ──
|
||||
|
||||
/** 查询串布尔 */
|
||||
const QueryBool = z.enum(["true", "false"]).optional();
|
||||
|
||||
/** 浏览器服务(GET /browser/providers);凭证只给引用是否存在,不给明文 */
|
||||
export const BrowserProviderView = z.object({
|
||||
id: Id,
|
||||
name: z.string(),
|
||||
kind: BrowserProviderKind,
|
||||
baseUrl: z.string().nullable(),
|
||||
/** 访问凭据的 secret 引用(明文永不出 API) */
|
||||
secretRef: Id.nullable(),
|
||||
enabled: z.boolean(),
|
||||
/** 最近一次声明/探测到的能力;verified 只由人工核验写入 config.verified */
|
||||
capabilities: BrowserCapabilities.nullable(),
|
||||
verifiedAt: Timestamp.nullable(),
|
||||
healthStatus: InstanceHealth,
|
||||
lastHealthAt: Timestamp.nullable(),
|
||||
/** probe=true 时的即时健康结果 */
|
||||
health: BrowserHealth.nullable(),
|
||||
});
|
||||
export type BrowserProviderView = z.infer<typeof BrowserProviderView>;
|
||||
|
||||
export const BrowserProvidersQuery = z.object({
|
||||
/** true = 现场探测 /health 与 /openapi.json 并写回 */
|
||||
probe: QueryBool,
|
||||
});
|
||||
export type BrowserProvidersQuery = z.infer<typeof BrowserProvidersQuery>;
|
||||
|
||||
/** 会话视图:会话 + 标签页 + 当前有效租约 + 界面可用能力 */
|
||||
export const BrowserSessionView = BrowserSession.extend({
|
||||
tabs: z.array(BrowserTab),
|
||||
currentTabId: z.string().nullable(),
|
||||
lease: BrowserControlLease.nullable(),
|
||||
/** 控制权交还后到重新读取页面之前为 true:旧元素引用一律失效 */
|
||||
needsSnapshot: z.boolean(),
|
||||
/** 等待处理/接管原因(login_required / captcha / human_takeover / lease_expired …) */
|
||||
waitingReason: z.string().nullable(),
|
||||
});
|
||||
export type BrowserSessionView = z.infer<typeof BrowserSessionView>;
|
||||
|
||||
export const BrowserSessionOpen = z.object({
|
||||
siteId: Id,
|
||||
accountId: Id.nullable().optional(),
|
||||
/** 缺省取第一个启用的浏览器服务 */
|
||||
providerId: Id.optional(),
|
||||
/** 缺省打开站点首页;必须在站点域名内 */
|
||||
url: z.url().optional(),
|
||||
});
|
||||
export type BrowserSessionOpen = z.infer<typeof BrowserSessionOpen>;
|
||||
|
||||
export const BrowserSessionsQuery = z.object({
|
||||
siteId: Id.optional(),
|
||||
includeClosed: QueryBool,
|
||||
});
|
||||
export type BrowserSessionsQuery = z.infer<typeof BrowserSessionsQuery>;
|
||||
|
||||
export const BrowserSessionParams = z.object({ id: Id });
|
||||
export const BrowserTabParams = z.object({ id: Id, tabId: z.string().min(1).max(200) });
|
||||
|
||||
export const BrowserTabOpen = z.object({ url: z.url().optional() });
|
||||
|
||||
/** 导航:地址栏(url)或导航按钮(action),二选一(url 优先) */
|
||||
export const BrowserNavigate = z.object({
|
||||
tabId: z.string().min(1).optional(),
|
||||
url: z.url().max(4000).optional(),
|
||||
action: z.enum(["back", "forward", "reload"]).optional(),
|
||||
});
|
||||
export type BrowserNavigate = z.infer<typeof BrowserNavigate>;
|
||||
|
||||
export const BrowserNavigation = z.object({
|
||||
/** 已脱敏的最终地址 */
|
||||
url: z.string(),
|
||||
httpStatus: z.number().int().nullable(),
|
||||
ok: z.boolean(),
|
||||
attempts: z.number().int().min(0),
|
||||
/** 落在登录页(登录失效) */
|
||||
loginRequired: z.boolean(),
|
||||
session: BrowserSessionView,
|
||||
});
|
||||
export type BrowserNavigation = z.infer<typeof BrowserNavigation>;
|
||||
|
||||
export const BrowserSnapshotQuery = z.object({
|
||||
tabId: z.string().min(1).optional(),
|
||||
offset: z.coerce.number().int().min(0).optional(),
|
||||
});
|
||||
|
||||
export const BrowserScreenshotQuery = z.object({ tabId: z.string().min(1).optional() });
|
||||
|
||||
/** 截图:base64 直接给原站视图;不落盘、不进模型(modelSafe 标注) */
|
||||
export const BrowserScreenshotView = z.object({
|
||||
mimeType: z.string(),
|
||||
dataBase64: z.string(),
|
||||
/** 图片像素尺寸(PNG/JPEG 头解析;解析不出为 null) */
|
||||
width: z.number().int().nullable(),
|
||||
height: z.number().int().nullable(),
|
||||
url: z.string().nullable(),
|
||||
modelSafe: z.boolean(),
|
||||
capturedAt: Timestamp,
|
||||
controller: BrowserController,
|
||||
});
|
||||
export type BrowserScreenshotView = z.infer<typeof BrowserScreenshotView>;
|
||||
|
||||
/** 元素定位:快照引用 / CSS 选择器 / 截图像素坐标(原站视图点击) */
|
||||
export const BrowserTarget = z.union([
|
||||
/** 快照元素引用,形如 e12 */
|
||||
z.object({ ref: z.string().min(1).max(40) }),
|
||||
z.object({ selector: z.string().min(1).max(500) }),
|
||||
z.object({ x: z.number().min(0), y: z.number().min(0) }),
|
||||
]);
|
||||
export type BrowserTarget = z.infer<typeof BrowserTarget>;
|
||||
|
||||
export const BrowserClick = z.object({ tabId: z.string().min(1).optional(), target: BrowserTarget });
|
||||
export type BrowserClick = z.infer<typeof BrowserClick>;
|
||||
|
||||
/** 输入:按键(key)> 站点账号凭证字段(credential,服务端解析后直接下发)> 明文(text);target 缺省 = 当前焦点 */
|
||||
export const BrowserType = z.object({
|
||||
tabId: z.string().min(1).optional(),
|
||||
target: BrowserTarget.optional(),
|
||||
text: z.string().max(4000).optional(),
|
||||
credential: z.enum(["username", "password", "totp"]).optional(),
|
||||
key: z.string().min(1).max(40).optional(),
|
||||
submit: z.boolean().optional(),
|
||||
clear: z.boolean().optional(),
|
||||
});
|
||||
export type BrowserType = z.infer<typeof BrowserType>;
|
||||
|
||||
export const BrowserScroll = z.object({
|
||||
tabId: z.string().min(1).optional(),
|
||||
direction: z.enum(["up", "down", "left", "right"]),
|
||||
amount: z.number().int().min(1).max(10_000).default(600),
|
||||
});
|
||||
export type BrowserScroll = z.infer<typeof BrowserScroll>;
|
||||
|
||||
export const BrowserSelect = z.object({
|
||||
tabId: z.string().min(1).optional(),
|
||||
target: BrowserTarget,
|
||||
value: z.string().max(500),
|
||||
});
|
||||
export type BrowserSelect = z.infer<typeof BrowserSelect>;
|
||||
|
||||
export const BrowserActionResult = z.object({ ok: z.literal(true), session: BrowserSessionView });
|
||||
export type BrowserActionResult = z.infer<typeof BrowserActionResult>;
|
||||
|
||||
export const BrowserExtract = z.object({
|
||||
tabId: z.string().min(1).optional(),
|
||||
/** JSON Schema;属性用 x-ref 指向快照元素(camofox /extract) */
|
||||
schema: JsonObject,
|
||||
});
|
||||
export const BrowserExtractResult = z.object({ data: JsonObject });
|
||||
|
||||
/** 捕获下载:可先点击 target 触发;submit 给出时经 POST /acquisition-plans 同一流程提交 */
|
||||
export const BrowserDownload = z.object({
|
||||
tabId: z.string().min(1).optional(),
|
||||
target: BrowserTarget.optional(),
|
||||
timeoutMs: z.number().int().min(500).max(120_000).default(15_000),
|
||||
submit: z
|
||||
.object({ slotId: Id.optional(), entityId: Id.optional(), note: z.string().max(2000).optional() })
|
||||
.optional(),
|
||||
});
|
||||
export type BrowserDownload = z.infer<typeof BrowserDownload>;
|
||||
|
||||
/** 暂存核验结果:种子解析出的 infohash、与本地资源的匹配 */
|
||||
export const BrowserDownloadVerification = z.object({
|
||||
valid: z.boolean(),
|
||||
infohashV1: z.string().nullable(),
|
||||
infohashV2: z.string().nullable(),
|
||||
name: z.string().nullable(),
|
||||
totalSize: NonNegInt.nullable(),
|
||||
private: z.boolean().nullable(),
|
||||
error: z.string().nullable(),
|
||||
});
|
||||
export type BrowserDownloadVerification = z.infer<typeof BrowserDownloadVerification>;
|
||||
|
||||
export const BrowserDownloadResult = z.object({
|
||||
artifact: DownloadArtifact,
|
||||
verification: BrowserDownloadVerification,
|
||||
/** 按 infohash / 站点资源 ID 匹配到的本地资源;null = 不在本地索引 */
|
||||
sourceReleaseId: Id.nullable(),
|
||||
/** 同一会话已捕获过同一文件(sha256 相同):不重复提交 */
|
||||
duplicate: z.boolean(),
|
||||
/** submit 给出且匹配到资源时的提交结果(与 POST /acquisition-plans 相同) */
|
||||
submission: ManualAcquisitionResult.nullable(),
|
||||
note: z.string().nullable(),
|
||||
});
|
||||
export type BrowserDownloadResult = z.infer<typeof BrowserDownloadResult>;
|
||||
|
||||
export const BrowserControlAcquire = z.object({
|
||||
reason: z.string().min(1).max(200).default("人工接管"),
|
||||
/** 租约时长;到期未交还 → 会话转"等待处理",不自动恢复自动化 */
|
||||
ttlMs: z
|
||||
.number()
|
||||
.int()
|
||||
.min(60_000)
|
||||
.max(4 * 3_600_000)
|
||||
.default(30 * 60_000),
|
||||
});
|
||||
export type BrowserControlAcquire = z.infer<typeof BrowserControlAcquire>;
|
||||
|
||||
export const BrowserControlRelease = z.object({
|
||||
leaseId: Id.optional(),
|
||||
reason: z.string().min(1).max(200).default("人工交还"),
|
||||
/** 交还给自动化(缺省)或留在等待处理 */
|
||||
to: z.enum(["automation", "waiting"]).default("automation"),
|
||||
});
|
||||
export type BrowserControlRelease = z.infer<typeof BrowserControlRelease>;
|
||||
|
||||
export const BrowserControlResult = z.object({
|
||||
lease: BrowserControlLease.nullable(),
|
||||
session: BrowserSessionView,
|
||||
});
|
||||
export type BrowserControlResult = z.infer<typeof BrowserControlResult>;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { MediaAsset } from "./assets.ts";
|
||||
import { BrowserController, BrowserDownloadVerification, BrowserSnapshot } from "./browser.ts";
|
||||
import { CAPABILITY_CATALOG, type CapabilitySpec, type KnownCapabilityId } from "./capability.ts";
|
||||
import { Id, JsonObject, NonNegInt } from "./common.ts";
|
||||
import { MediaEntity, MediaKind } from "./content.ts";
|
||||
@@ -318,6 +319,93 @@ export const SiteUserDataOutput = z.object({
|
||||
history: z.array(SiteUserDataPoint),
|
||||
});
|
||||
|
||||
// C1-E 浏览器能力:会话按站点/账号隔离;元素引用只来自最近一次 browser.snapshot,
|
||||
// 控制权交还或导航后旧引用失效;凭证用 credential 字段由服务端注入,不进入模型。
|
||||
const BrowserAgentTarget = z.union([
|
||||
z.object({ ref: z.string().min(1).max(40) }),
|
||||
z.object({ selector: z.string().min(1).max(500) }),
|
||||
]);
|
||||
|
||||
export const BrowserSnapshotInput = z
|
||||
.object({
|
||||
/** 会话 ID;缺省用 siteId 对应的未关闭会话 */
|
||||
sessionId: Id.optional(),
|
||||
siteId: Id.optional(),
|
||||
tabId: z.string().min(1).optional(),
|
||||
offset: z.number().int().min(0).optional(),
|
||||
})
|
||||
.refine((v) => !!v.sessionId || !!v.siteId, { message: "sessionId 与 siteId 至少给一个" });
|
||||
export const BrowserSnapshotOutput = z.object({
|
||||
snapshot: BrowserSnapshot,
|
||||
controller: BrowserController,
|
||||
/** 会话在等待处理(验证码 / 二次认证 / 登录失效)时的原因;此时自动化动作被拒,需人工在原站视图处理 */
|
||||
waitingReason: z.string().nullable(),
|
||||
loginRequired: z.boolean(),
|
||||
});
|
||||
|
||||
export const BrowserNavigateInput = z
|
||||
.object({
|
||||
sessionId: Id.optional(),
|
||||
/** 无会话时按站点(和账号)打开一个 */
|
||||
siteId: Id.optional(),
|
||||
accountId: Id.optional(),
|
||||
tabId: z.string().min(1).optional(),
|
||||
/** 必须在站点域名内 */
|
||||
url: z.url().max(4000),
|
||||
})
|
||||
.refine((v) => !!v.sessionId || !!v.siteId, { message: "sessionId 与 siteId 至少给一个" });
|
||||
export const BrowserNavigateOutput = z.object({
|
||||
sessionId: Id,
|
||||
url: z.string(),
|
||||
httpStatus: z.number().int().nullable(),
|
||||
ok: z.boolean(),
|
||||
attempts: z.number().int(),
|
||||
loginRequired: z.boolean(),
|
||||
controller: BrowserController,
|
||||
});
|
||||
|
||||
export const BrowserInteractInput = z.object({
|
||||
sessionId: Id,
|
||||
tabId: z.string().min(1).optional(),
|
||||
/** request_human:转"等待处理",由人在原站视图接管(验证码、二次认证等) */
|
||||
action: z.enum(["click", "type", "select", "scroll", "press", "request_human"]),
|
||||
target: BrowserAgentTarget.optional(),
|
||||
text: z.string().max(4000).optional(),
|
||||
/** 站点账号凭证字段:服务端解析后直接下发到输入框 */
|
||||
credential: z.enum(["username", "password", "totp"]).optional(),
|
||||
key: z.string().min(1).max(40).optional(),
|
||||
value: z.string().max(500).optional(),
|
||||
direction: z.enum(["up", "down", "left", "right"]).optional(),
|
||||
amount: z.number().int().min(1).max(10_000).optional(),
|
||||
submit: z.boolean().optional(),
|
||||
reason: z.string().max(200).optional(),
|
||||
});
|
||||
export const BrowserInteractOutput = z.object({
|
||||
ok: z.literal(true),
|
||||
controller: BrowserController,
|
||||
/** true = 页面可能已变化,下一步先 browser.snapshot 再用新引用 */
|
||||
needsSnapshot: z.boolean(),
|
||||
});
|
||||
|
||||
export const BrowserDownloadInput = z.object({
|
||||
sessionId: Id,
|
||||
tabId: z.string().min(1).optional(),
|
||||
/** 触发下载的元素(快照引用);缺省只等待已触发的下载 */
|
||||
target: BrowserAgentTarget.optional(),
|
||||
timeoutMs: z.number().int().min(500).max(120_000).default(15_000),
|
||||
});
|
||||
export const BrowserDownloadOutput = z.object({
|
||||
artifactId: Id,
|
||||
filename: z.string(),
|
||||
sizeBytes: NonNegInt,
|
||||
sha256: z.string(),
|
||||
verification: BrowserDownloadVerification,
|
||||
/** 匹配到的本地资源;下一步用 download.plan / download.submit 走同一提交流程 */
|
||||
sourceReleaseId: Id.nullable(),
|
||||
duplicate: z.boolean(),
|
||||
note: z.string().nullable(),
|
||||
});
|
||||
|
||||
/** 能力 ID → 输入/输出 schema(server 注册处理器、agent 生成工具定义时共用) */
|
||||
export const CAPABILITY_IO = {
|
||||
"catalog.lookup": { input: CatalogLookupInput, output: CatalogLookupOutput },
|
||||
@@ -341,6 +429,10 @@ export const CAPABILITY_IO = {
|
||||
"site.health_check": { input: SiteHealthCheckInput, output: SiteHealthCheckOutput },
|
||||
"site.refresh_login": { input: SiteLoginRefreshInput, output: SiteLoginRefreshOutput },
|
||||
"site.user_data": { input: SiteUserDataInput, output: SiteUserDataOutput },
|
||||
"browser.snapshot": { input: BrowserSnapshotInput, output: BrowserSnapshotOutput },
|
||||
"browser.navigate": { input: BrowserNavigateInput, output: BrowserNavigateOutput },
|
||||
"browser.interact": { input: BrowserInteractInput, output: BrowserInteractOutput },
|
||||
"browser.download": { input: BrowserDownloadInput, output: BrowserDownloadOutput },
|
||||
} as const satisfies Record<KnownCapabilityId, { input: z.ZodType; output: z.ZodType }>;
|
||||
|
||||
type CapabilityIo = typeof CAPABILITY_IO;
|
||||
@@ -404,3 +496,11 @@ export type SiteLoginRefreshOutput = z.infer<typeof SiteLoginRefreshOutput>;
|
||||
export type SiteUserDataInput = z.infer<typeof SiteUserDataInput>;
|
||||
export type SiteUserDataOutput = z.infer<typeof SiteUserDataOutput>;
|
||||
export type SubagentBudget = z.infer<typeof SubagentBudget>;
|
||||
export type BrowserSnapshotInput = z.infer<typeof BrowserSnapshotInput>;
|
||||
export type BrowserSnapshotOutput = z.infer<typeof BrowserSnapshotOutput>;
|
||||
export type BrowserNavigateInput = z.infer<typeof BrowserNavigateInput>;
|
||||
export type BrowserNavigateOutput = z.infer<typeof BrowserNavigateOutput>;
|
||||
export type BrowserInteractInput = z.infer<typeof BrowserInteractInput>;
|
||||
export type BrowserInteractOutput = z.infer<typeof BrowserInteractOutput>;
|
||||
export type BrowserDownloadInput = z.infer<typeof BrowserDownloadInput>;
|
||||
export type BrowserDownloadOutput = z.infer<typeof BrowserDownloadOutput>;
|
||||
@@ -72,6 +72,10 @@ export const CAPABILITY_IDS = [
|
||||
"site.health_check",
|
||||
"site.refresh_login",
|
||||
"site.user_data",
|
||||
"browser.snapshot",
|
||||
"browser.navigate",
|
||||
"browser.interact",
|
||||
"browser.download",
|
||||
] as const;
|
||||
export const KnownCapabilityId = z.enum(CAPABILITY_IDS);
|
||||
export type KnownCapabilityId = z.infer<typeof KnownCapabilityId>;
|
||||
@@ -348,6 +352,52 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
true,
|
||||
false,
|
||||
),
|
||||
// C1-E 浏览器能力(REQ §4.4、§4.7.3):经 BrowserProvider 访问用户已有的 camofox-browser;
|
||||
// 导航限站点域名,凭证由服务端注入,人工接管期间自动化动作被拒;下载产物只进后端暂存核验
|
||||
"browser.snapshot": d(
|
||||
"browser.snapshot",
|
||||
"读取原站页面",
|
||||
"读取站点浏览器会话当前页面的结构化快照(可访问性文本 + eN 元素引用,凭证已遮蔽);控制权交还或导航后必须重新读取",
|
||||
"read",
|
||||
"BrowserSnapshot",
|
||||
"read",
|
||||
"low",
|
||||
true,
|
||||
false,
|
||||
),
|
||||
"browser.navigate": d(
|
||||
"browser.navigate",
|
||||
"打开原站页面",
|
||||
"在站点浏览器会话里打开站点域名内的地址(没有会话时按站点打开);受站点限流、超时与重试预算约束",
|
||||
"execute",
|
||||
"BrowserNavigate",
|
||||
"admin",
|
||||
"low",
|
||||
false,
|
||||
false,
|
||||
),
|
||||
"browser.interact": d(
|
||||
"browser.interact",
|
||||
"操作原站页面",
|
||||
"在站点浏览器会话里点击 / 输入 / 选择 / 滚动 / 按键;登录字段用 credential 由服务端注入凭证;验证码或二次认证用 request_human 转人工",
|
||||
"execute",
|
||||
"BrowserInteract",
|
||||
"admin",
|
||||
"medium",
|
||||
false,
|
||||
false,
|
||||
),
|
||||
"browser.download": d(
|
||||
"browser.download",
|
||||
"捕获原站下载",
|
||||
"捕获浏览器会话里的种子下载,交后端暂存并核验 infohash、匹配本地资源;提交下载仍用 download.plan / download.submit",
|
||||
"execute",
|
||||
"BrowserDownload",
|
||||
"organize.execute",
|
||||
"medium",
|
||||
false,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
function d(
|
||||
|
||||
Reference in new issue
Block a user