跳转到内容

开发契约

4. 开发契约: 平台与脚本的交互规范

Section titled “4. 开发契约: 平台与脚本的交互规范”

您的 bundle.js 必须导出一个名为 runasync 函数。

run 函数接收一个参数对象,包含 browser, page, 和 context

这是您获取平台能力的核心。

export interface AnbaoContext {
/**
* 通用输入:由用户通过 `@schema` 生成的表单动态填写的参数。
* @example
* const title = context.common.video_title;
* const filePath = context.common.video_file_path; // "C:\\Users\\Me\\Videos\\my_video.mp4"
*/
common: Record<string, any>;
/**
* 当前运行实例匹配到的平台信息。
*/
platform: { name: string; base_url: string };
/**
* 当前运行实例所使用的 Profile 信息。
* 脚本应利用此 Profile 关联的持久化存储来管理登录状态 (Cookies)。
* 首次运行时,脚本需要实现交互式登录流程来建立初始状态。
*/
profile: { name: string };
/**
* 路径 API
*/
paths: {
downloads: string; // 用户系统的“下载”目录
data: string; // 与 Profile 绑定的、可持久化读写的目录
};
/**
* 结构化日志 API。
* 用于在任务日志中创建具有明确步骤和状态的日志条目。
* 这对于向用户展示清晰、可跟进的任务进度至关重要。
*
* @example
* context.log('开始上传视频...', 'info');
* // ... a long running operation ...
* context.log('视频上传成功', 'success');
*/
log: (message: string, level?: 'info' | 'warn' | 'error' | 'success' | 'debug') => void;
/**
* 发送一个系统级通知。
* 这将在 Anbao Agent 的通知中心创建一个新的通知,并(如果用户允许)显示一个原生系统通知。
*
* @example
* context.notify({
* title: '下载完成',
* content: '文件 "report.pdf" 已成功下载到您的下载目录。'
* });
*
* // 发送一个与任务结果相关的通知
* context.notify({
* title: '任务成功',
* content: 'Bilibili 视频发布任务已成功完成。',
* category: 'TaskResult'
* });
*/
notify: (payload: { title: string; content: string; category?: 'ScriptMessage' | 'TaskResult' }) => void;
/**
* 强制退出 API。
* 当脚本遇到可预期的、业务逻辑上的失败时,应调用此函数来优雅地终止任务。
* 它会向平台报告一个明确的错误信息,而不是抛出一个通用的、未处理的异常。
*
* @example
* if (!videoUploaded) {
* context.forceExit('视频上传失败,请检查网络连接。');
* }
*/
forceExit: (errorMessage?: string) => void;
/**
* 请求人工介入 (Human-in-the-Loop)。
* 当脚本遇到无法自动处理的情况(如验证码)时,调用此函数。
* 它会暂停脚本,发送高优先级通知给用户,并在页面注入一个“继续”按钮。
* 用户手动处理完毕后,点击按钮,脚本将从暂停处继续执行。
*
* @param options 介入请求的配置
* @returns {Promise<void>} 一个在用户点击“继续”或超时后完成的 Promise。
*
* @example
* try {
* if (await page.locator('#captcha').isVisible()) {
* throw new AutomationError('Captcha', '检测到验证码,请手动处理。');
* }
* } catch (error) {
* if (error instanceof AutomationError) {
* await context.requestHumanIntervention({ message: error.message });
* } else {
* throw error;
* }
* }
* // 代码将从这里继续
*/
requestHumanIntervention: (options: { message: string; timeout?: number; theme?: 'light' | 'dark' }) => Promise<void>;
}

脚本的执行有三种终止方式:

  • 成功: run 函数正常结束并 return 一个 JSON 可序列化的对象。这是任务成功的唯一标志。
  • 优雅失败: 在函数中调用 context.forceExit("错误信息")。这将立即终止脚本,并将任务状态标记为失败,同时记录您提供的错误信息。
  • 异常失败: run 函数中 throw 一个 Error 对象。平台会捕获这个异常,将任务标记为失败,并记录异常的堆栈信息。

为了向用户和开发者提供最大的灵活性,平台采用三层优先级策略来合并和应用浏览器启动参数 (launchOptions)。最终生效的参数由以下三个来源按从高到低的优先级合并而成:

  1. 最高优先级:临时覆盖 (Temporary Overrides)

    • 来源: 用户在计划任务列表页点击“带参运行”,或在任务失败后点击“以有头模式重试”时,在弹出的对话框中设置的参数。
    • 作用: 赋予用户针对单次运行的最高控制权,主要用于调试或处理突发情况(如手动登录)。这些参数仅当次有效,不会被保存。
  2. 中等优先级:计划任务配置 (Schedule Configuration)

    • 来源: 用户在创建或编辑计划任务时,通过“长期启动参数”选项为该特定任务设置的参数。
    • 作用: 满足用户为不同计划任务设置不同默认行为的需求(例如,某个任务默认就需要有头模式)。这些参数会随计划任务一起保存。
  3. 最低优先级:脚本默认值 (Script’s Default)

    • 来源: 脚本开发者在脚本元数据块中通过 // @launchOptions { ... } 定义的参数。
    • 作用: 提供一个开箱即用的推荐配置,确保脚本在大多数情况下的正常运行。

生效逻辑: 这是一个优先级覆盖 (Priority Override) 模型,不是深度合并。高优先级的参数一旦存在,就会完全取代所有低优先级的参数。

  • 如果用户设置了临时覆盖参数,则只有临时参数会生效。
  • 如果没有临时参数,但用户配置了计划任务参数,则只有计划任务参数会生效。
  • 只有在以上两者都不存在时,脚本默认值才会生效。

例如:如果脚本默认值为 {"slowMo": 50},而用户在计划任务中配置了 {"headless": false},则最终生效的参数只有 {"headless": false}slowMo 将不会被应用。

对于需要登录才能操作的平台,脚本必须能够正确处理已登录、未登录和登录失效这三种状态。

核心原则:脚本自检,平台辅助

Section titled “核心原则:脚本自检,平台辅助”

平台本身不会预检您的登录状态。脚本的健壮性体现在它能够自我检测环境并向用户提供清晰的指引。

  • 检测:脚本在执行核心逻辑前,必须先检查页面是否处于预期的登录状态。
  • 报告:如果未登录,脚本不应尝试复杂的交互,而应立即调用 context.forceExit() 并提供一条明确的错误信息,指导用户下一步该怎么做。

Profile 的核心价值在于它与一个持久化存储目录 (context.paths.data) 绑定。Playwright 的 BrowserContext 会自动将该上下文中的所有浏览数据(包括 Cookies, Local Storage 等)保存到这个目录中。

这意味着,作为开发者,您完全不需要手动读写任何凭据文件

正确的登录处理流程如下:

  1. 启动时检查:在 run 函数的开头,直接导航到需要登录的页面,然后通过检查页面上的特定元素(如头像、用户名)来验证登录是否有效。Playwright 会自动从 context.paths.data 加载并应用该 Profile 的 Cookies。
  2. 失败时清晰退出:如果验证失败,立即调用 context.forceExit() 并提供清晰的指引。
// 无需导入 'fs' 或 'path',平台已处理好一切
export async function run({ page, context }) {
// 1. 导航到目标页并验证登录
// 平台已自动从 Profile 的持久化目录加载 Cookies
await page.goto(`${context.platform.base_url}/dashboard`); // 假设 /dashboard 是需要登录的页面
const isLoggedIn = await page.locator('#your-avatar-or-logout-button-selector').isVisible();
// 2. 如果未登录,提供清晰的、可操作的失败信息
if (!isLoggedIn) {
context.forceExit(
'登录凭据失效或未初始化。请在运行历史中找到本次失败的记录,并使用【以有头模式重试】按钮来手动完成登录。',
);
return; // 确保在 forceExit 后退出函数
}
// --- 主要业务逻辑 ---
context.log('登录验证成功,开始执行任务。', 'info');
// ... 您的代码 ...
// 您无需手动保存任何东西,用户在有头模式下登录后,
// Playwright 会自动将新的登录状态保存到 Profile 目录中。
}

当脚本因为上述原因失败后,用户会在“运行历史”中看到您提供的错误信息。安宝平台的 UI 会提供一个**“以有头模式重试”**的选项,允许用户临时覆盖脚本的 @headless 设置,启动一个带界面的浏览器来完成登录。

一旦用户在有头模式下手动登录成功,Playwright 会自动将新的登录状态(Cookies 等)更新到与该 Profile 关联的持久化目录中。脚本无需任何额外操作,下一次即可在无头模式下成功运行。