// page-operate 工具包的服务端与浏览器侧共用契约。 // // 工具一律在服务端执行,而"填哪个输入框、点哪个按钮"只有浏览器里当前那张页面知道, // 所以本工具走 §2.9.1 的工具结果事件桥:服务端只把入参规整成效果指令原样返回, // 浏览器侧的 mountPageOperateEffect 收到事件后才真正动手。两端隔着一条 DOM 事件, // 工具名 / effect 名 / 字段上限必须共用同一份常量,故单独成文件。 // // 本文件不许 import 服务端模块(ai / zod / node:*):它同时被浏览器侧引用。 /** 工具名。宿主关掉本包时该工具就不存在,模型也不会看到相关能力段。 */ export const PAGE_OPERATE_TOOL = 'operatePage'; /** 效果名:工具返回值里的 `effect` 字段,浏览器侧据此认领事件。 */ export const PAGE_OPERATE_EFFECT = 'operate-page'; /** 支持的动作。fill=往输入项填值;click=点击按钮。 */ export const PAGE_OPERATE_ACTIONS = ['fill', 'click'] as const; export type PageOperateAction = (typeof PAGE_OPERATE_ACTIONS)[number]; /** 控件名上限:模型偶尔把整句话塞进来,超长先截断再匹配。 */ export const PAGE_OPERATE_TARGET_MAX_LENGTH = 100; /** 填入值上限:允许粘一段 JSON/文本,但不做无上限透传。 */ export const PAGE_OPERATE_VALUE_MAX_LENGTH = 4000; export interface PageOperateEffect { effect: typeof PAGE_OPERATE_EFFECT; /** 已归一小写;非法动作归空串,交由浏览器侧提示,而不是静默丢弃。 */ action: PageOperateAction | ''; /** 控件名(输入项标签 / 按钮文字),已 trim 与截断;命中判定在浏览器侧做。 */ target: string; /** action=fill 时要填入的内容;click 恒为空串。 */ value: string; } export function isPageOperateAction(value: string): value is PageOperateAction { return (PAGE_OPERATE_ACTIONS as readonly string[]).includes(value); } /** 服务端与事件桥共用的入参规整:两边都不信模型给的原样字符串。 */ export function normalizePageOperateEffect(input: { action?: unknown; target?: unknown; value?: unknown; }): PageOperateEffect { const rawAction = typeof input?.action === 'string' ? input.action.trim().toLowerCase() : ''; const action = isPageOperateAction(rawAction) ? rawAction : ''; const target = typeof input?.target === 'string' ? input.target.trim().slice(0, PAGE_OPERATE_TARGET_MAX_LENGTH) : ''; const value = action === 'fill' && typeof input?.value === 'string' ? input.value.trim().slice(0, PAGE_OPERATE_VALUE_MAX_LENGTH) : ''; return { effect: PAGE_OPERATE_EFFECT, action, target, value }; }