pageqa 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # pageqa
2
+
3
+ 用自然语言驱动的页面测试 agent。基于 **pi-agent-core**(状态化 LLM agent)编排测试步骤,**browserskill(`bsk`)** 驱动真实浏览器执行,自然语言解析由可配置的 LLM 端点完成。最终产出可读文本报告与机器可读 JSON 报告,返回可接入 CI 的退出码。
4
+
5
+ - 运行时:**Node.js + TypeScript**,npm 分发,CLI 入口。
6
+ - 浏览器驱动:**browserskill(`bsk`)**,连接一个已运行的真实浏览器(Chrome/Edge),支持公开页与登录态页面。
7
+ - 自然语言驱动:**pi-agent-core** 状态化 agent,把自然语言意图解析为浏览器操作步骤并自动编排;LLM 后端通过可配置的 OpenAI 兼容端点接入(base URL / API Key / 模型均可配置)。
8
+
9
+ ## 前置
10
+
11
+ ### 1. 安装并配置 browserskill(`bsk`)
12
+
13
+ `bsk` 是连接真实浏览器(Chrome/Edge)的驱动,本项目通过它执行页面操作。
14
+
15
+ - 项目主页与安装方式:**https://github.com/Tencent/BrowserSkill**
16
+ - 按仓库 README 安装 `bsk` CLI 并启动 daemon,然后连接一个已登录的浏览器(支持公开页与登录态页面)。
17
+
18
+ 验证安装与连接:
19
+ ```bash
20
+ bsk status # 确认 daemon 已启动且已连接浏览器
21
+ bsk session start # 创建 session(CLI 也会自动创建)
22
+ ```
23
+
24
+ > 提示:本工具的 `--session <id>` 即来自 `bsk session start` 返回的 `session_id`;不传时 CLI 会自动新建一个(前提 daemon 已连浏览器)。
25
+
26
+ ### 2. LLM 后端(可配置 OpenAI 兼容端点)
27
+
28
+ 自然语言解析依赖一个 OpenAI 兼容的 LLM 端点。端点地址、密钥与模型均可通过配置文件或环境变量设置:
29
+
30
+ - 默认地址:`http://127.0.0.1:3000/v1`(可被覆盖为任意 OpenAI 兼容端点)
31
+ - 默认 Key:`codebuddy-proxy-key`
32
+
33
+ 可用环境变量覆盖(优先级高于配置文件):
34
+ - `PAGEQA_LLM_BASE_URL`
35
+ - `PAGEQA_LLM_API_KEY`
36
+ - `PAGEQA_LLM_MODEL`
37
+
38
+ ### 3. 用户级配置文件(可选但推荐)
39
+
40
+ 工具会在**用户主目录**自动创建配置文件,持久化 LLM 设置,避免每次用环境变量:
41
+
42
+ - 配置目录:`~/.pageqa`(Windows:`%USERPROFILE%\.pageqa`)
43
+ - 配置文件:`config.json`
44
+
45
+ ```json
46
+ {
47
+ "baseUrl": "http://127.0.0.1:3000/v1",
48
+ "apiKey": "codebuddy-proxy-key",
49
+ "model": "hunyuan-2.0-instruct"
50
+ }
51
+ ```
52
+
53
+ - 首次运行(或执行 `pageqa --init-config`)会自动创建该文件,编辑即可切换模型/端点地址/密钥。
54
+ - **优先级(高 → 低)**:环境变量 `PAGEQA_LLM_*` > 用户配置文件 `config.json` > 内置默认值。
55
+ - 例如要改用其他兼容 OpenAI 的端点,把 `baseUrl`/`apiKey`/`model` 改掉即可,无需改代码。
56
+
57
+ ## 安装
58
+
59
+ 本地开发:
60
+ ```bash
61
+ npm install
62
+ npm run build
63
+ ```
64
+
65
+ 全局安装(发布后):
66
+ ```bash
67
+ npm install -g pageqa
68
+ pageqa --init-config # 在用户目录创建配置文件 ~/.pageqa/config.json
69
+ ```
70
+
71
+ > 全局安装后首次运行也会自动创建配置文件;用 `--init-config` 可显式创建/重置。
72
+
73
+ ## 使用
74
+
75
+ ```bash
76
+ # 内联自然语言(多句按行分隔)
77
+ pageqa --session <id> "打开 https://example.com
78
+ 并断言标题包含 Example"
79
+
80
+ # 读取脚本文件(自动识别 `## ` 分隔的多个场景,逐个运行并汇总)
81
+ pageqa examples/smoke.md
82
+
83
+ # JSON 报告
84
+ pageqa --json examples/smoke.md
85
+
86
+ # 强制按多场景套件运行(即使只有一个场景)
87
+ pageqa --suite "打开 https://example.com 并断言标题包含 Example"
88
+ ```
89
+
90
+ ### 多场景套件
91
+
92
+ 脚本(`.md`/`.txt`)中用 `## 场景名` 分隔多个独立测试场景,CLI 会逐个运行、分别给出结论,并汇总整体 PASS/FAIL 与总退出码(任一场景失败则整体失败)。
93
+
94
+ ### CLI 选项
95
+
96
+ | 选项 | 说明 |
97
+ | --- | --- |
98
+ | `--session <id>` | 指定已存在的 bsk session(默认自动创建) |
99
+ | `--json` | 输出 JSON 报告 |
100
+ | `--suite` | 强制按多场景套件运行 |
101
+ | `--init-config` | 在用户目录创建/重置配置文件 |
102
+ | `--out <file>` | 将报告写入文件 |
103
+ | `-h, --help` | 帮助 |
104
+
105
+ 退出码:`0` 全部断言通过;`1` 任一断言失败/错误/无法执行。可直接接入 CI。
106
+
107
+ ## 工作原理
108
+
109
+ ```
110
+ 自然语言意图
111
+ └─> pi-agent-core Agent(LLM: pi-ai 自定义 provider -> 可配置 OpenAI 兼容端点)
112
+ └─> bsk 工具:navigate / snapshot / click / fill / hover / scroll / wait / assert_text
113
+ └─> 真实浏览器(bsk 连接)
114
+ └─> 结论与证据 -> 报告(文本/JSON)+ 退出码
115
+ ```
116
+
117
+ 可用工具(`src/bsk/tools.ts`):
118
+ - `navigate(url)` 打开网页
119
+ - `snapshot()` 读取页面 aria 树与可见文本(标题、段落、链接、按钮等)
120
+ - `click(target)` / `fill(target, value)` / `hover(target)` 元素交互(target 用 `@eN` 引用或 CSS 选择器)
121
+ - `scroll(target)` / `wait(ms)` 滚动与等待
122
+ - `assert_text(expectation)` 断言页面是否包含指定文本,返回「成立/不成立」与证据
123
+
124
+ ## 验证
125
+
126
+ ```bash
127
+ npm test
128
+ ```
129
+
130
+ 冒烟测试覆盖:A1 打开+标题断言、A2 元素交互与断言、A3 失败可读原因与退出码、A4 文本/JSON 报告。需 bsk daemon 连接浏览器且 LLM 端点可用。
131
+
132
+ 也可直接运行套件脚本:
133
+
134
+ ```bash
135
+ pageqa examples/smoke.md --json
136
+ ```
137
+
138
+ ## CI 集成
139
+
140
+ `.github/workflows/ci.yml` 演示如何在 GitHub Actions 中接入:安装 bsk 并连接浏览器 → 构建 → 运行 `examples/smoke.md` → 上传 JSON 报告。失败场景会使退出码非零,从而标记 CI 失败。LLM 端点地址/Key 通过仓库 Secrets(`PAGEQA_LLM_BASE_URL` 等)注入。
141
+
142
+ ## 目录
143
+
144
+ ```text
145
+ src/
146
+ index.ts CLI 入口
147
+ agent.ts 编排器(pi-agent-core Agent + bsk 工具 + 报告)
148
+ llm.ts LLM 后端(pi-ai 自定义 provider -> 可配置 OpenAI 兼容端点)
149
+ bsk/tools.ts browserskill 工具层
150
+ report.ts 报告解析与渲染
151
+ tests/smoke.test.mjs 端到端验证
152
+ ```
@@ -0,0 +1,24 @@
1
+ import { type TestReport } from "./report.js";
2
+ export interface AgentOptions {
3
+ session?: string;
4
+ systemPrompt?: string;
5
+ }
6
+ export interface AgentRunResult {
7
+ report: TestReport;
8
+ text: string;
9
+ json: string;
10
+ transcript: string;
11
+ }
12
+ interface Scenario {
13
+ name: string;
14
+ body: string;
15
+ }
16
+ /**
17
+ * 用 pi-agent-core 编排一次自然语言页面测试:注入 bsk 工具,驱动浏览器执行,整理报告。
18
+ */
19
+ export declare function runAgent(input: string, opts?: AgentOptions): Promise<AgentRunResult>;
20
+ /** 把一个脚本拆分为多个场景(按 `## ` 二级标题分隔)。无标题则整体作为一个场景。 */
21
+ export declare function splitScenarios(script: string): Scenario[];
22
+ /** 批量运行多个场景并汇总报告。任一失败则整体失败。 */
23
+ export declare function runSuite(script: string, opts?: AgentOptions): Promise<AgentRunResult>;
24
+ export {};
package/dist/agent.js ADDED
@@ -0,0 +1,157 @@
1
+ import { Agent } from "@earendil-works/pi-agent-core";
2
+ import { createLlmBackend } from "./llm.js";
3
+ import { createBskTools, ensureSession } from "./bsk/tools.js";
4
+ import { buildReport } from "./report.js";
5
+ const DEFAULT_SYSTEM_PROMPT = [
6
+ "你是「页面测试 agent」。你根据用户的自然语言测试意图,驱动浏览器完成端到端测试。",
7
+ "",
8
+ "可用工具:",
9
+ "- navigate(url): 打开网页",
10
+ "- snapshot(): 读取页面 aria 树与可见文本(标题、段落、链接、按钮等),并定位元素",
11
+ "- click(target) / fill(target, value) / hover(target): 元素交互,target 用 @eN 引用或 CSS 选择器",
12
+ "- scroll(target) / wait(ms): 滚动与等待",
13
+ "- assert_text(expectation): 断言页面是否包含某文本,返回「成立/不成立」与证据",
14
+ "",
15
+ "工作流程:",
16
+ "1. 若需要打开页面,先 navigate。",
17
+ "2. 用 snapshot 读取当前内容并定位要操作的元素。",
18
+ "3. 按用户意图逐步交互;每次交互后如需要可再次 snapshot 确认状态。",
19
+ "4. 用 assert_text 校验关键结果,或在 snapshot 基础上自行判断文本/标题是否匹配。",
20
+ "5. 最后用简洁中文给出结论:每个断言「成立/不成立」,并附证据(实际看到的标题或文本片段)。",
21
+ "6. 不要编造未观察到的内容;若元素不存在或断言失败,明确说明。",
22
+ "",
23
+ "只输出测试结论与证据,不要输出多余解释。",
24
+ ].join("\n");
25
+ /**
26
+ * 用 pi-agent-core 编排一次自然语言页面测试:注入 bsk 工具,驱动浏览器执行,整理报告。
27
+ */
28
+ export async function runAgent(input, opts = {}) {
29
+ const { models, model } = createLlmBackend();
30
+ const session = ensureSession(opts.session);
31
+ const tools = createBskTools({ session });
32
+ const events = [];
33
+ const agent = new Agent({
34
+ initialState: {
35
+ systemPrompt: opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
36
+ model,
37
+ tools,
38
+ },
39
+ streamFn: models.streamSimple.bind(models),
40
+ });
41
+ agent.subscribe((e) => {
42
+ if (e.type === "tool_execution_start")
43
+ events.push("[tool] " + e.toolName);
44
+ else if (e.type === "tool_execution_end")
45
+ events.push("[tool-ok] " + e.toolName);
46
+ else if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
47
+ events.push(e.assistantMessageEvent.delta);
48
+ }
49
+ });
50
+ await agent.prompt(input);
51
+ await agent.waitForIdle();
52
+ const transcript = events.join("");
53
+ const report = buildReport(input, transcript);
54
+ return {
55
+ report,
56
+ text: renderText(report),
57
+ json: JSON.stringify(report, null, 2),
58
+ transcript,
59
+ };
60
+ }
61
+ /** 把一个脚本拆分为多个场景(按 `## ` 二级标题分隔)。无标题则整体作为一个场景。 */
62
+ export function splitScenarios(script) {
63
+ const lines = script.split(/\r?\n/);
64
+ const collected = [];
65
+ let currentName = "";
66
+ let currentLines = [];
67
+ let inScenario = false;
68
+ const flush = () => {
69
+ if (!inScenario)
70
+ return;
71
+ const body = currentLines.join("\n").trim();
72
+ if (body.length > 0)
73
+ collected.push({ name: currentName || "场景 1", body });
74
+ currentLines = [];
75
+ };
76
+ for (const line of lines) {
77
+ const m = line.match(/^##\s+(.*)$/);
78
+ if (m) {
79
+ flush();
80
+ inScenario = true;
81
+ currentName = m[1].trim();
82
+ }
83
+ else if (inScenario) {
84
+ currentLines.push(line);
85
+ }
86
+ // 一级标题(#)与##之前的开场说明文字:不计入任何场景
87
+ }
88
+ flush();
89
+ return collected.length ? collected : [{ name: "场景 1", body: script.trim() }];
90
+ }
91
+ /** 批量运行多个场景并汇总报告。任一失败则整体失败。 */
92
+ export async function runSuite(script, opts = {}) {
93
+ const scenarios = splitScenarios(script);
94
+ const results = [];
95
+ for (const sc of scenarios) {
96
+ const r = await runAgent(sc.body, { ...opts, systemPrompt: undefined });
97
+ results.push(r);
98
+ }
99
+ const overall = results.every((r) => r.report.status === "pass") ? "pass" : "fail";
100
+ const summary = {
101
+ status: overall,
102
+ assertions: results.flatMap((r, i) => r.report.assertions.map((a) => ({
103
+ ...a,
104
+ expectation: "[" + (scenarios[i]?.name ?? String(i + 1)) + "] " + a.expectation,
105
+ }))),
106
+ summary: "共 " + results.length + " 个场景,通过 " +
107
+ results.filter((r) => r.report.status === "pass").length + " 个",
108
+ transcript: results
109
+ .map((r, i) => "## " + (scenarios[i]?.name ?? String(i + 1)) + "\n" + r.transcript.trim())
110
+ .join("\n\n"),
111
+ };
112
+ return {
113
+ report: summary,
114
+ text: renderSuiteText(summary, results, scenarios),
115
+ json: JSON.stringify(summary, null, 2),
116
+ transcript: summary.transcript,
117
+ };
118
+ }
119
+ function verdictTag(v) {
120
+ return v === "pass" ? "PASS" : "FAIL";
121
+ }
122
+ function assertionLine(a) {
123
+ const ev = a.evidence ? " (" + a.evidence + ")" : "";
124
+ return " - [" + verdictTag(a.verdict) + "] " + a.expectation + ev;
125
+ }
126
+ function renderText(r) {
127
+ const lines = [];
128
+ lines.push("=== 页面测试报告 ===");
129
+ lines.push("结论: " + (r.status === "pass" ? "PASS" : "FAIL"));
130
+ lines.push("断言数: " + r.assertions.length);
131
+ for (const a of r.assertions)
132
+ lines.push(assertionLine(a));
133
+ if (r.summary)
134
+ lines.push("摘要: " + r.summary);
135
+ lines.push("---");
136
+ lines.push(r.transcript.trim());
137
+ return lines.join("\n");
138
+ }
139
+ function renderSuiteText(summary, results, scenarios) {
140
+ const lines = [];
141
+ lines.push("=== 页面测试套件报告 ===");
142
+ lines.push("整体结论: " + (summary.status === "pass" ? "PASS" : "FAIL"));
143
+ lines.push("场景数: " + results.length);
144
+ results.forEach((r, i) => {
145
+ lines.push("");
146
+ lines.push("--- 场景 " + (i + 1) + ":" + (scenarios[i]?.name ?? "") +
147
+ " [" + (r.report.status === "pass" ? "PASS" : "FAIL") + "] ---");
148
+ for (const a of r.report.assertions)
149
+ lines.push(assertionLine(a));
150
+ if (r.report.summary)
151
+ lines.push(" 摘要: " + r.report.summary);
152
+ });
153
+ lines.push("");
154
+ lines.push("汇总: " + summary.summary);
155
+ return lines.join("\n");
156
+ }
157
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ export interface BskToolOptions {
3
+ session: string;
4
+ }
5
+ /** 构造一组浏览器操作工具,供 page-test agent 调用。 */
6
+ export declare function createBskTools(opts: BskToolOptions): AgentTool[];
7
+ /** 创建一个 bsk session;若已提供且仍在活跃列表中则复用,否则新建。 */
8
+ export declare function ensureSession(existing?: string): string;
@@ -0,0 +1,172 @@
1
+ import { execFileSync } from "node:child_process";
2
+ /**
3
+ * browserskill(`bsk`)工具层:把 bsk CLI 命令包装成 pi-agent-core 的 AgentTool。
4
+ *
5
+ * bsk 连接一个已运行的真实浏览器,每个命令都需要 --session <id>。
6
+ * 所有命令以 --quiet 抑制进度输出,仅返回结构化/可读结果。
7
+ */
8
+ function bsk(args) {
9
+ return execFileSync("bsk", args, { encoding: "utf-8" }).toString();
10
+ }
11
+ /** 构造 AgentTool 标准的成功返回(含必填 details 字段)。 */
12
+ function ok(text) {
13
+ return { content: [{ type: "text", text }], details: {} };
14
+ }
15
+ /** 构造一组浏览器操作工具,供 page-test agent 调用。 */
16
+ export function createBskTools(opts) {
17
+ const s = opts.session;
18
+ const quiet = ["--session", s, "--quiet"];
19
+ const navigate = {
20
+ name: "navigate",
21
+ label: "Navigate",
22
+ description: "在浏览器标签页打开一个 URL(支持 http/https/about: 等)。导航完成后页面 DOM 就绪。",
23
+ parameters: {
24
+ type: "object",
25
+ properties: { url: { type: "string", description: "目标 URL" } },
26
+ required: ["url"],
27
+ },
28
+ execute: async (_id, params) => {
29
+ const p = params;
30
+ const out = bsk(["navigate", p.url, ...quiet, "--wait-until", "domcontentloaded"]);
31
+ return ok(`已导航到 ${p.url}\n${out}`);
32
+ },
33
+ };
34
+ const snapshot = {
35
+ name: "snapshot",
36
+ label: "Snapshot",
37
+ description: "读取当前页面的 aria 语义树与可见文本(标题、段落、链接、按钮等)。用于读取内容、标题、文本并定位元素。",
38
+ parameters: { type: "object", properties: {} },
39
+ execute: async () => ok(bsk(["snapshot", ...quiet])),
40
+ };
41
+ const click = {
42
+ name: "click",
43
+ label: "Click",
44
+ description: "点击一个元素。可用快照里的 @eN 引用或 CSS 选择器。",
45
+ parameters: {
46
+ type: "object",
47
+ properties: { target: { type: "string", description: "@eN 引用或 CSS 选择器" } },
48
+ required: ["target"],
49
+ },
50
+ execute: async (_id, params) => {
51
+ const p = params;
52
+ const out = bsk(["click", p.target, ...quiet]);
53
+ return ok(`已点击 ${p.target}\n${out}`);
54
+ },
55
+ };
56
+ const fill = {
57
+ name: "fill",
58
+ label: "Fill",
59
+ description: "在输入框/文本域中填入文本(会先清空原有内容)。",
60
+ parameters: {
61
+ type: "object",
62
+ properties: {
63
+ target: { type: "string", description: "@eN 引用或 CSS 选择器" },
64
+ value: { type: "string", description: "要输入的文本" },
65
+ },
66
+ required: ["target", "value"],
67
+ },
68
+ execute: async (_id, params) => {
69
+ const p = params;
70
+ const out = bsk(["fill", p.target, "--value", p.value, ...quiet]);
71
+ return ok(`已在 ${p.target} 填入文本\n${out}`);
72
+ },
73
+ };
74
+ const hover = {
75
+ name: "hover",
76
+ label: "Hover",
77
+ description: "悬停在一个元素上,用于触发悬停菜单/提示。",
78
+ parameters: {
79
+ type: "object",
80
+ properties: { target: { type: "string", description: "@eN 引用或 CSS 选择器" } },
81
+ required: ["target"],
82
+ },
83
+ execute: async (_id, params) => {
84
+ const p = params;
85
+ const out = bsk(["hover", p.target, ...quiet]);
86
+ return ok(`已悬停 ${p.target}\n${out}`);
87
+ },
88
+ };
89
+ const scroll = {
90
+ name: "scroll",
91
+ label: "Scroll",
92
+ description: "滚动到指定元素使其进入视口。",
93
+ parameters: {
94
+ type: "object",
95
+ properties: { target: { type: "string", description: "@eN 引用或 CSS 选择器" } },
96
+ required: ["target"],
97
+ },
98
+ execute: async (_id, params) => {
99
+ const p = params;
100
+ const js = `(() => { const el = document.querySelector(${JSON.stringify(p.target)}); if (el) el.scrollIntoView({block:'center'}); return el ? 'scrolled' : 'element-not-found'; })()`;
101
+ const res = bsk(["evaluate", js, ...quiet]);
102
+ return ok(res);
103
+ },
104
+ };
105
+ const wait = {
106
+ name: "wait",
107
+ label: "Wait",
108
+ description: "等待一段时间(毫秒)或等待页面导航完成。",
109
+ parameters: {
110
+ type: "object",
111
+ properties: { ms: { type: "number", description: "等待毫秒数" } },
112
+ required: [],
113
+ },
114
+ execute: async (_id, params) => {
115
+ const p = params ?? {};
116
+ const ms = p.ms ?? 1000;
117
+ // wait-ms 是 daemon 端 sleep,不接受 --session
118
+ bsk(["wait-ms", String(ms)]);
119
+ return ok(`已等待 ${ms}ms`);
120
+ },
121
+ };
122
+ const assertText = {
123
+ name: "assert_text",
124
+ label: "Assert Text",
125
+ description: "断言当前页面是否包含指定文本。返回「成立」或「不成立」并附上证据(命中片段)。用于校验测试结果。",
126
+ parameters: {
127
+ type: "object",
128
+ properties: { expectation: { type: "string", description: "期望在页面中出现的文本" } },
129
+ required: ["expectation"],
130
+ },
131
+ execute: async (_id, params) => {
132
+ const p = params;
133
+ const snap = bsk(["snapshot", ...quiet]);
134
+ const hit = snap.includes(p.expectation);
135
+ const verdict = hit ? "成立" : "不成立";
136
+ const evidence = hit ? `页面中包含「${p.expectation}」` : `页面中未找到「${p.expectation}」`;
137
+ return ok(`断言「${p.expectation}」:${verdict}。${evidence}`);
138
+ },
139
+ };
140
+ return [navigate, snapshot, click, fill, hover, scroll, wait, assertText];
141
+ }
142
+ /** 创建一个 bsk session;若已提供且仍在活跃列表中则复用,否则新建。 */
143
+ export function ensureSession(existing) {
144
+ if (existing && isActiveSession(existing))
145
+ return existing;
146
+ const out = bsk(["session", "start", "--json"]);
147
+ try {
148
+ const json = JSON.parse(out);
149
+ if (json?.session_id)
150
+ return json.session_id;
151
+ }
152
+ catch {
153
+ const m = out.match(/session_id["\s:]+([a-z0-9]+)/i);
154
+ if (m)
155
+ return m[1];
156
+ }
157
+ throw new Error("无法创建 bsk session,请确认 bsk daemon 已连接浏览器。");
158
+ }
159
+ /** 检查给定 session 是否在 bsk 活跃列表里。 */
160
+ function isActiveSession(id) {
161
+ try {
162
+ const out = bsk(["session", "list", "--json"]);
163
+ const list = JSON.parse(out);
164
+ if (Array.isArray(list))
165
+ return list.some((s) => s?.session_id === id);
166
+ }
167
+ catch {
168
+ // 忽略解析错误
169
+ }
170
+ return false;
171
+ }
172
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/bsk/tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 用户级配置:在用户主目录下创建配置目录与文件,持久化 LLM 后端设置。
3
+ *
4
+ * 配置目录:~/.pageqa(Windows 为 %USERPROFILE%/.pageqa)
5
+ * 配置文件:config.json,字段:
6
+ * - baseUrl: 反代/兼容 OpenAI 的 base URL
7
+ * - apiKey: 访问密钥
8
+ * - model: 模型 ID
9
+ *
10
+ * 优先级(高 -> 低):
11
+ * 环境变量 PAGEQA_LLM_* > 用户配置文件 > 内置默认值
12
+ *
13
+ * 首次运行(配置文件不存在)会自动创建带默认值的配置文件,便于用户日后修改。
14
+ */
15
+ export interface PageQaConfig {
16
+ baseUrl: string;
17
+ apiKey: string;
18
+ model: string;
19
+ }
20
+ export declare const CONFIG_DIR: string;
21
+ export declare const CONFIG_PATH: string;
22
+ /** 读取配置:合并 默认值 < 用户文件 < 环境变量。返回最终生效配置。 */
23
+ export declare function loadConfig(): PageQaConfig;
24
+ /** 确保配置目录存在(首次运行时创建 ~/.pageqa)。 */
25
+ export declare function ensureConfigDir(): string;
26
+ /** 平台无关地展示用户配置目录(便于 README/帮助说明)。 */
27
+ export declare function describeConfigLocation(): string;
package/dist/config.js ADDED
@@ -0,0 +1,53 @@
1
+ import { homedir, platform } from "node:os";
2
+ import { join } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ const DEFAULTS = {
5
+ baseUrl: "http://127.0.0.1:3000/v1",
6
+ apiKey: "codebuddy-proxy-key",
7
+ model: "hunyuan-2.0-instruct",
8
+ };
9
+ export const CONFIG_DIR = join(homedir(), ".pageqa");
10
+ export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
11
+ /** 读取配置:合并 默认值 < 用户文件 < 环境变量。返回最终生效配置。 */
12
+ export function loadConfig() {
13
+ const fromFile = readConfigFile();
14
+ return {
15
+ baseUrl: process.env.PAGEQA_LLM_BASE_URL ?? fromFile.baseUrl ?? DEFAULTS.baseUrl,
16
+ apiKey: process.env.PAGEQA_LLM_API_KEY ?? fromFile.apiKey ?? DEFAULTS.apiKey,
17
+ model: process.env.PAGEQA_LLM_MODEL ?? fromFile.model ?? DEFAULTS.model,
18
+ };
19
+ }
20
+ /** 读取用户配置文件;不存在则创建默认文件并返回默认值。 */
21
+ function readConfigFile() {
22
+ if (!existsSync(CONFIG_PATH)) {
23
+ ensureConfigDir();
24
+ writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULTS, null, 2), "utf8");
25
+ return { ...DEFAULTS };
26
+ }
27
+ try {
28
+ const raw = readFileSync(CONFIG_PATH, "utf8");
29
+ const parsed = JSON.parse(raw);
30
+ return {
31
+ baseUrl: parsed.baseUrl ?? DEFAULTS.baseUrl,
32
+ apiKey: parsed.apiKey ?? DEFAULTS.apiKey,
33
+ model: parsed.model ?? DEFAULTS.model,
34
+ };
35
+ }
36
+ catch {
37
+ return { ...DEFAULTS };
38
+ }
39
+ }
40
+ /** 确保配置目录存在(首次运行时创建 ~/.pageqa)。 */
41
+ export function ensureConfigDir() {
42
+ if (!existsSync(CONFIG_DIR)) {
43
+ mkdirSync(CONFIG_DIR, { recursive: true });
44
+ }
45
+ return CONFIG_DIR;
46
+ }
47
+ /** 平台无关地展示用户配置目录(便于 README/帮助说明)。 */
48
+ export function describeConfigLocation() {
49
+ const home = homedir();
50
+ const tail = platform() === "win32" ? "%USERPROFILE%\\.pageqa" : "~/.pageqa";
51
+ return `${CONFIG_DIR} (${tail} → ${home})`;
52
+ }
53
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAW,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAuB7E,MAAM,QAAQ,GAAiB;IAC7B,OAAO,EAAE,0BAA0B;IACnC,MAAM,EAAE,qBAAqB;IAC7B,KAAK,EAAE,sBAAsB;CAC9B,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;AACrD,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAE3D,0CAA0C;AAC1C,MAAM,UAAU,UAAU;IACxB,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAC;IAClC,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,QAAQ,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO;QAChF,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM;QAC5E,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,KAAK;KACxE,CAAC;AACJ,CAAC;AAED,iCAAiC;AACjC,SAAS,cAAc;IACrB,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAC7B,eAAe,EAAE,CAAC;QAClB,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACtE,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC;IACzB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA0B,CAAC;QACxD,OAAO;YACL,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO;YAC3C,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM;YACxC,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,QAAQ,CAAC,KAAK;SACtC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC;IACzB,CAAC;AACH,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,eAAe;IAC7B,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,qCAAqC;AACrC,MAAM,UAAU,sBAAsB;IACpC,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,MAAM,IAAI,GAAG,QAAQ,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,WAAW,CAAC;IAC7E,OAAO,GAAG,UAAU,KAAK,IAAI,MAAM,IAAI,GAAG,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { runAgent, runSuite } from "./agent.js";
4
+ import { ensureConfigDir, CONFIG_PATH, loadConfig } from "./config.js";
5
+ function parseArgs(argv) {
6
+ const args = { json: false, suite: false, initConfig: false, help: false };
7
+ for (let i = 0; i < argv.length; i++) {
8
+ const a = argv[i];
9
+ switch (a) {
10
+ case "-h":
11
+ case "--help":
12
+ args.help = true;
13
+ break;
14
+ case "--session":
15
+ args.session = argv[++i];
16
+ break;
17
+ case "--json":
18
+ args.json = true;
19
+ break;
20
+ case "--suite":
21
+ args.suite = true;
22
+ break;
23
+ case "--init-config":
24
+ args.initConfig = true;
25
+ break;
26
+ case "--out":
27
+ args.out = argv[++i];
28
+ break;
29
+ default:
30
+ if (!a.startsWith("-"))
31
+ args.input = a;
32
+ }
33
+ }
34
+ return args;
35
+ }
36
+ const HELP = `pageqa - 自然语言驱动的页面测试工具(pi-agent-core + browserskill)
37
+
38
+ 用法:
39
+ pageqa [options] <input>
40
+
41
+ <input> 自然语言脚本文件(.md/.txt),或用引号包裹的内联文本
42
+ 脚本中可用『## 场景名』分隔多个测试场景,自动批量运行
43
+
44
+ 选项:
45
+ --session <id> 指定已存在的 bsk session(默认自动创建)
46
+ --json 输出 JSON 报告
47
+ --suite 强制按多场景套件运行(即使只有一个场景)
48
+ --init-config 在用户目录创建/重置配置文件
49
+ --out <file> 将报告写入文件
50
+ -h, --help 显示帮助
51
+
52
+ 配置:
53
+ - 首次运行会在用户目录自动创建配置文件:~/.pageqa/config.json
54
+ (Windows: %USERPROFILE%\.pageqa\config.json)
55
+ - 可编辑该文件设置 baseUrl / apiKey / model
56
+ - 也可用环境变量覆盖(优先级高于配置文件):
57
+ PAGEQA_LLM_BASE_URL / PAGEQA_LLM_API_KEY / PAGEQA_LLM_MODEL
58
+ - 运行 pageqa --init-config 可显式创建/重置配置文件
59
+
60
+ 前置:
61
+ - 已安装并启动 bsk daemon,且连接了一个浏览器(bsk session start)
62
+ - CodeBuddy 本地反代可用(默认 http://127.0.0.1:3000/v1,模型 hunyuan-2.0-instruct)
63
+
64
+ 示例:
65
+ pageqa --session ulao "打开 https://example.com 并断言标题包含 Example"
66
+ pageqa examples/smoke.md --json
67
+ `;
68
+ function readInput(input) {
69
+ if (!input.includes("\n") && (input.endsWith(".md") || input.endsWith(".txt"))) {
70
+ try {
71
+ return readFileSync(input, "utf8");
72
+ }
73
+ catch {
74
+ // 不是文件:视为内联文本
75
+ }
76
+ }
77
+ return input;
78
+ }
79
+ async function main() {
80
+ const args = parseArgs(process.argv.slice(2));
81
+ if (args.help) {
82
+ process.stdout.write(HELP + "\n");
83
+ return 0;
84
+ }
85
+ if (args.initConfig) {
86
+ const dir = ensureConfigDir();
87
+ loadConfig(); // 触发配置文件创建
88
+ process.stdout.write(`配置文件已创建/确认:${CONFIG_PATH}\n目录:${dir}\n`);
89
+ process.stdout.write(`当前生效配置:${JSON.stringify(loadConfig(), null, 2)}\n`);
90
+ return 0;
91
+ }
92
+ if (!args.input) {
93
+ process.stdout.write(HELP + "\n");
94
+ return 1;
95
+ }
96
+ const input = readInput(args.input);
97
+ try {
98
+ const hasScenarios = /^##\s+/m.test(input);
99
+ const result = hasScenarios || args.suite
100
+ ? await runSuite(input, { session: args.session })
101
+ : await runAgent(input, { session: args.session });
102
+ const out = args.json ? result.json : result.text;
103
+ if (args.out) {
104
+ const { writeFileSync } = await import("node:fs");
105
+ writeFileSync(args.out, out + "\n");
106
+ }
107
+ process.stdout.write(out + "\n");
108
+ return result.report.status === "pass" ? 0 : 1;
109
+ }
110
+ catch (err) {
111
+ process.stderr.write(`执行失败: ${err instanceof Error ? err.stack : String(err)}\n`);
112
+ return 1;
113
+ }
114
+ }
115
+ main()
116
+ .then((code) => process.exit(code))
117
+ .catch((err) => {
118
+ process.stderr.write(`执行失败: ${err instanceof Error ? err.stack : String(err)}\n`);
119
+ process.exit(1);
120
+ });
121
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/llm.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { createModels, type Model, type Api } from "@earendil-works/pi-ai";
2
+ export interface LlmBackend {
3
+ models: ReturnType<typeof createModels>;
4
+ model: Model<Api>;
5
+ }
6
+ export declare function createLlmBackend(): LlmBackend;
package/dist/llm.js ADDED
@@ -0,0 +1,62 @@
1
+ import { createModels, createProvider } from "@earendil-works/pi-ai";
2
+ import * as openaiCompletions from "@earendil-works/pi-ai/api/openai-completions";
3
+ import { loadConfig } from "./config.js";
4
+ /**
5
+ * LLM 后端:基于 @earendil-works/pi-ai 构造一个指向 CodeBuddy 本地反代的自定义 provider。
6
+ *
7
+ * 配置优先级(高 -> 低):环境变量 PAGEQA_LLM_* > 用户配置文件(~/.pageqa/config.json) > 内置默认值。
8
+ * 首次运行会自动在用户主目录创建配置文件,便于用户修改模型/反代地址/密钥。
9
+ */
10
+ const PROVIDER_ID = "codebuddy";
11
+ const MODELS = [
12
+ { id: "hunyuan-2.0-instruct", name: "Hunyuan 2.0 Instruct", contextWindow: 32000, maxOutput: 64000 },
13
+ { id: "hy3", name: "Hy3", contextWindow: 192000, maxOutput: 64000 },
14
+ { id: "deepseek-v4.1-flash", name: "Deepseek-V4.1-Flash", contextWindow: 1_000_000, maxOutput: 128_000 },
15
+ ];
16
+ /** 构造一个静态解析的 ApiKeyAuth(避免交互式 env 探测)。 */
17
+ function staticApiKeyAuth(apiKey, baseUrl) {
18
+ return {
19
+ name: "CodeBuddy Proxy",
20
+ resolve: async () => ({ auth: { apiKey, baseUrl }, source: "static" }),
21
+ };
22
+ }
23
+ export function createLlmBackend() {
24
+ const cfg = loadConfig();
25
+ const DEFAULT_BASE_URL = cfg.baseUrl;
26
+ const DEFAULT_API_KEY = cfg.apiKey;
27
+ const DEFAULT_MODEL = cfg.model;
28
+ const models = createModels();
29
+ const provider = createProvider({
30
+ id: PROVIDER_ID,
31
+ name: "CodeBuddy (本地反代)",
32
+ auth: { apiKey: staticApiKeyAuth(DEFAULT_API_KEY, DEFAULT_BASE_URL) },
33
+ models: MODELS.map((m) => ({
34
+ id: m.id,
35
+ name: m.name,
36
+ api: "openai-completions",
37
+ provider: PROVIDER_ID,
38
+ baseUrl: DEFAULT_BASE_URL,
39
+ input: ["text"],
40
+ reasoning: false,
41
+ contextWindow: m.contextWindow,
42
+ maxTokens: m.maxOutput,
43
+ maxOutput: m.maxOutput,
44
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
45
+ compat: {
46
+ supportsStore: false,
47
+ supportsDeveloperRole: false,
48
+ supportsReasoningEffort: false,
49
+ maxTokensField: "max_tokens",
50
+ supportsLongCacheRetention: false,
51
+ },
52
+ })),
53
+ api: openaiCompletions,
54
+ });
55
+ models.setProvider(provider);
56
+ const model = models.getModel(PROVIDER_ID, DEFAULT_MODEL);
57
+ if (!model) {
58
+ throw new Error(`未找到模型 ${DEFAULT_MODEL}(provider=${PROVIDER_ID})。请确认 CodeBuddy 反代可用。`);
59
+ }
60
+ return { models, model };
61
+ }
62
+ //# sourceMappingURL=llm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm.js","sourceRoot":"","sources":["../src/llm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAwB,MAAM,uBAAuB,CAAC;AAC3F,OAAO,KAAK,iBAAiB,MAAM,8CAA8C,CAAC;AAClF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;;GAKG;AAEH,MAAM,WAAW,GAAG,WAAW,CAAC;AAShC,MAAM,MAAM,GAA2B;IACrC,EAAE,EAAE,EAAE,sBAAsB,EAAE,IAAI,EAAE,sBAAsB,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE;IACpG,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE;IACnE,EAAE,EAAE,EAAE,qBAAqB,EAAE,IAAI,EAAE,qBAAqB,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE;CACzG,CAAC;AAEF,0CAA0C;AAC1C,SAAS,gBAAgB,CAAC,MAAc,EAAE,OAAe;IACvD,OAAO;QACL,IAAI,EAAE,iBAAiB;QACvB,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;KACvE,CAAC;AACJ,CAAC;AAOD,MAAM,UAAU,gBAAgB;IAC9B,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,MAAM,gBAAgB,GAAG,GAAG,CAAC,OAAO,CAAC;IACrC,MAAM,eAAe,GAAG,GAAG,CAAC,MAAM,CAAC;IACnC,MAAM,aAAa,GAAG,GAAG,CAAC,KAAK,CAAC;IAChC,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,QAAQ,GAAG,cAAc,CAAC;QAC9B,EAAE,EAAE,WAAW;QACf,IAAI,EAAE,kBAAkB;QACxB,IAAI,EAAE,EAAE,MAAM,EAAE,gBAAgB,CAAC,eAAe,EAAE,gBAAgB,CAAC,EAAE;QACrE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACzB,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,GAAG,EAAE,oBAAoB;YACzB,QAAQ,EAAE,WAAW;YACrB,OAAO,EAAE,gBAAgB;YACzB,KAAK,EAAE,CAAC,MAAM,CAAC;YACf,SAAS,EAAE,KAAK;YAChB,aAAa,EAAE,CAAC,CAAC,aAAa;YAC9B,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;YAC1D,MAAM,EAAE;gBACN,aAAa,EAAE,KAAK;gBACpB,qBAAqB,EAAE,KAAK;gBAC5B,uBAAuB,EAAE,KAAK;gBAC9B,cAAc,EAAE,YAAY;gBAC5B,0BAA0B,EAAE,KAAK;aAClC;SACF,CAAC,CAAC;QACH,GAAG,EAAE,iBAAiB;KACvB,CAAC,CAAC;IACH,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IAE7B,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAC1D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,SAAS,aAAa,aAAa,WAAW,uBAAuB,CAAC,CAAC;IACzF,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,16 @@
1
+ export interface AssertionResult {
2
+ expectation: string;
3
+ verdict: "pass" | "fail";
4
+ evidence?: string;
5
+ }
6
+ export interface TestReport {
7
+ status: "pass" | "fail";
8
+ assertions: AssertionResult[];
9
+ summary?: string;
10
+ transcript: string;
11
+ }
12
+ /**
13
+ * 从 agent 的结论文本中解析断言结果。agent 会以「成立/不成立」给出每个断言,
14
+ * 并用「断言「X」:成立。证据...」句式。无法解析时降级为基于 PASS/FAIL 关键字的整体判断。
15
+ */
16
+ export declare function buildReport(input: string, transcript: string): TestReport;
package/dist/report.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 从 agent 的结论文本中解析断言结果。agent 会以「成立/不成立」给出每个断言,
3
+ * 并用「断言「X」:成立。证据...」句式。无法解析时降级为基于 PASS/FAIL 关键字的整体判断。
4
+ */
5
+ export function buildReport(input, transcript) {
6
+ const assertions = parseAssertions(transcript);
7
+ let status;
8
+ if (assertions.length > 0) {
9
+ status = assertions.every((a) => a.verdict === "pass") ? "pass" : "fail";
10
+ }
11
+ else {
12
+ // 无结构化断言:依据关键字与「不成立/失败」判定
13
+ const lowered = transcript.toLowerCase();
14
+ const negative = /不成立|未找到|失败|不存在|not found|fail|missing/.test(transcript);
15
+ status = negative ? "fail" : "pass";
16
+ }
17
+ const summary = extractSummary(transcript);
18
+ return { status, assertions, summary, transcript };
19
+ }
20
+ function parseAssertions(text) {
21
+ const results = [];
22
+ // 匹配两种常见句式:
23
+ // 断言「X」:成立/不成立。证据...
24
+ // 断言「X」成立。证据...
25
+ const re = /断言[「"']?([^」"']+?)[""」]?\s*(?:[::]\s*)?(成立|不成立)/g;
26
+ let m;
27
+ while ((m = re.exec(text)) !== null) {
28
+ const expectation = m[1].trim();
29
+ const verdict = m[2] === "成立" ? "pass" : "fail";
30
+ const after = text.slice(m.index + m[0].length).split("\n")[0].trim();
31
+ const evidence = after.startsWith("。") ? after.slice(1).trim() : after;
32
+ results.push({ expectation, verdict, evidence: evidence || undefined });
33
+ }
34
+ return results;
35
+ }
36
+ function extractSummary(text) {
37
+ // 取最后一段非空文本作为摘要
38
+ const paras = text.split("\n").map((l) => l.trim()).filter(Boolean);
39
+ return paras.at(-1) ?? "";
40
+ }
41
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAaA;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,UAAkB;IAC3D,MAAM,UAAU,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAE/C,IAAI,MAAuB,CAAC;IAC5B,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IAC3E,CAAC;SAAM,CAAC;QACN,0BAA0B;QAC1B,MAAM,OAAO,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAG,uCAAuC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC1E,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IACtC,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IAC3C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;AACrD,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,OAAO,GAAsB,EAAE,CAAC;IACtC,YAAY;IACZ,uBAAuB;IACvB,kBAAkB;IAClB,MAAM,EAAE,GAAG,kDAAkD,CAAC;IAC9D,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACpC,MAAM,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QAChD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACtE,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QACvE,OAAO,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,IAAI,SAAS,EAAE,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,cAAc,CAAC,IAAY;IAClC,gBAAgB;IAChB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACpE,OAAO,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,15 @@
1
+ # 页面测试套件示例
2
+
3
+ 用 `## ` 分隔多个测试场景,CLI 会逐个运行并汇总整体结论与退出码。
4
+
5
+ ## A1 打开页面并断言标题
6
+ 打开 https://example.com 并断言标题包含 Example
7
+
8
+ ## A2 交互流程(点击链接 + 等待 + 断言)
9
+ 打开 https://example.com 。点击页面上的「Learn more」链接,等待页面加载,然后断言页面包含 'IANA'。
10
+
11
+ ## A3 失败场景(断言不存在文本)
12
+ 打开 https://example.com 并断言页面包含 'THIS_TEXT_SHOULD_NOT_EXIST_XYZ'
13
+
14
+ > 说明:自然语言意图由 pi-agent-core 驱动的 agent 解析为浏览器操作步骤,并经 browserskill(bsk)连接真实浏览器执行。
15
+ > 默认 LLM 后端为 CodeBuddy 本地反代(混元),无需官网 Key。
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "pageqa",
3
+ "version": "0.1.0",
4
+ "description": "用自然语言驱动的页面测试 agent:基于 pi-agent-core 编排 + browserskill 驱动真实浏览器,经 CodeBuddy 反代(混元)实现免 Key 自然语言测试。",
5
+ "type": "module",
6
+ "bin": {
7
+ "pageqa": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "scripts": {
11
+ "build": "tsc -p tsconfig.json",
12
+ "start": "node dist/index.js",
13
+ "pretest": "npm run build",
14
+ "test": "node --test tests/smoke.test.mjs",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "examples"
21
+ ],
22
+ "keywords": [
23
+ "page-qa",
24
+ "e2e",
25
+ "testing",
26
+ "agent",
27
+ "pi-agent-core",
28
+ "browserskill",
29
+ "bsk"
30
+ ],
31
+ "license": "MIT",
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "dependencies": {
36
+ "@earendil-works/pi-agent-core": "^0.85.1",
37
+ "@earendil-works/pi-ai": "^0.85.1"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^20.0.0",
41
+ "typescript": "^5.5.0"
42
+ }
43
+ }