pi-browser-use-lite 0.1.1 → 0.2.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/CHANGELOG.md +10 -0
- package/README.md +2 -8
- package/README.zh-CN.md +2 -8
- package/browser-extension/background/dispatch.js +4 -11
- package/browser-extension/background/input/pointer.js +42 -5
- package/browser-extension/background/tabs.js +5 -7
- package/browser-extension/background/target.js +4 -14
- package/browser-extension/manifest.json +1 -1
- package/package.json +1 -1
- package/src/commands/index.ts +31 -19
- package/src/config/schema.ts +0 -16
- package/src/config/validate.ts +1 -19
- package/src/index.ts +7 -14
- package/src/policy/index.ts +7 -29
- package/src/tools/exec/tab.ts +1 -5
- package/src/tools/index.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录已发布版本的用户可见变更。更细的过程见 git log。破坏性变更标 **BREAKING**。
|
|
4
4
|
|
|
5
|
+
## 0.2.0 — 2026-11-09
|
|
6
|
+
|
|
7
|
+
- **BREAKING**:删掉 `prompt.guidelines`。默认三条规则搬进 `chrome_act` 自己的 `promptGuidelines`;跨工具的规则写 AGENTS.md。写了这个键的配置会以 `prompt: unknown key` 启动失败。
|
|
8
|
+
- `/chrome` 有参数补全:输入 `/chrome ` 列子命令,`/chrome authorize ` 列 on 与 off,每项带一句说明。
|
|
9
|
+
- 关掉某个工具后,系统提示里不再留下点名它的规则:此前全局规则里的三条 `chrome_act` 规则在关掉 `chrome_act` 后仍然保留。
|
|
10
|
+
- **BREAKING**:删掉 `policy.background` 与 `/chrome background`。点不动页面时由输入路径自己把标签与窗口带到前台;读操作照样不打扰。写了这个键的配置会以 `policy.background: unknown key` 启动失败。
|
|
11
|
+
- 输入前的可见性检查改成自救:不可见就激活标签、把窗口带到前台,仍不可见才报错并说清原因(标签非活动、窗口最小化、被盖住)。
|
|
12
|
+
- `chrome_tab new` 建的新标签在自己的窗口里是活动标签,不再出现「新标签点不动」。
|
|
13
|
+
- README 说清自动化窗口不聚焦地打开、第一次点击或打字会把它带到前台。
|
|
14
|
+
|
|
5
15
|
## 0.1.1 — 2026-11-09
|
|
6
16
|
|
|
7
17
|
- 补中文 README:`README.zh-CN.md`,英文版顶部有切换链接,npm 包同时收录两份。
|
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ The package has two halves. The pi extension registers the tools and runs a loca
|
|
|
19
19
|
Behaviour worth knowing:
|
|
20
20
|
|
|
21
21
|
- Each session gets its own browser window. The tools do not touch your own tabs. The tabs the session opened are closed when the session ends.
|
|
22
|
+
- That window opens unfocused. The first click or keystroke raises it, because Chrome drops input to hidden pages. Reads never raise it.
|
|
22
23
|
- Input goes through trusted CDP events, so pages that reject synthetic events still react.
|
|
23
24
|
- Console and network capture starts at navigation, not at the first read.
|
|
24
25
|
- Every failure carries a class and a `hint` that says what to do next.
|
|
@@ -41,7 +42,7 @@ Then authorize the tools for this session:
|
|
|
41
42
|
/chrome authorize on
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
Authorization ends with the session. Set `policy.authorizeOnStart` to skip this step.
|
|
45
|
+
Authorization ends with the session. Set `policy.authorizeOnStart` to skip this step. Type `/chrome ` and press Tab to list the subcommands.
|
|
45
46
|
|
|
46
47
|
## Configuration
|
|
47
48
|
|
|
@@ -66,8 +67,6 @@ Without the file every value is the default. A wrong type or an unknown key fail
|
|
|
66
67
|
// Session rules
|
|
67
68
|
"policy": {
|
|
68
69
|
"authorizeOnStart": false, // true = the tools work without /chrome authorize on
|
|
69
|
-
"background": true, // true = block every focusing call, chrome_tab switch included.
|
|
70
|
-
// Turn it off when the model must bring a page to the front
|
|
71
70
|
"closeOwnTabsOnExit": true // close the tabs this session opened when the session ends
|
|
72
71
|
},
|
|
73
72
|
|
|
@@ -91,11 +90,6 @@ Without the file every value is the default. A wrong type or an unknown key fail
|
|
|
91
90
|
"keepNewest": 20 // older screenshots in that directory are deleted
|
|
92
91
|
},
|
|
93
92
|
|
|
94
|
-
// Global rules for the system prompt. These replace the three defaults; they do not extend them
|
|
95
|
-
"prompt": {
|
|
96
|
-
"guidelines": ["chrome_act: this is the user's real browser; confirm before destructive actions."]
|
|
97
|
-
},
|
|
98
|
-
|
|
99
93
|
// Tool switches and names. Keep at least one enabled; names must be unique and must not shadow a host tool
|
|
100
94
|
"tools": {
|
|
101
95
|
"chrome_act": { "enabled": true, "name": "chrome_act" },
|
package/README.zh-CN.md
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
值得知道的行为:
|
|
20
20
|
|
|
21
21
|
- 每个会话一个独立浏览器窗口。工具不碰你自己的标签页。会话结束时关掉本会话开的标签。
|
|
22
|
+
- 这个窗口不聚焦地打开。第一次点击或打字会把它带到前台,因为 Chrome 丢掉不可见页面的输入。读操作不会打扰你。
|
|
22
23
|
- 输入走可信 CDP 事件,拒绝合成事件的页面也会响应。
|
|
23
24
|
- 控制台与网络抓取从导航开始,不从第一次读取开始。
|
|
24
25
|
- 每次失败都带类别和一句 `hint`,说明下一步做什么。
|
|
@@ -41,7 +42,7 @@ pi install npm:pi-browser-use-lite
|
|
|
41
42
|
/chrome authorize on
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
授权随会话结束。想省这一步,把 `policy.authorizeOnStart` 设为 true
|
|
45
|
+
授权随会话结束。想省这一步,把 `policy.authorizeOnStart` 设为 true。输入 `/chrome ` 再按 Tab 列出子命令。
|
|
45
46
|
|
|
46
47
|
## 配置
|
|
47
48
|
|
|
@@ -66,8 +67,6 @@ pi install npm:pi-browser-use-lite
|
|
|
66
67
|
// 会话规则
|
|
67
68
|
"policy": {
|
|
68
69
|
"authorizeOnStart": false, // true = 不用 /chrome authorize on 就能用工具
|
|
69
|
-
"background": true, // true = 挡掉所有聚焦动作,chrome_tab switch 也算。
|
|
70
|
-
// 需要模型把页面带到前台时关掉
|
|
71
70
|
"closeOwnTabsOnExit": true // 会话结束时关掉本会话开的标签
|
|
72
71
|
},
|
|
73
72
|
|
|
@@ -91,11 +90,6 @@ pi install npm:pi-browser-use-lite
|
|
|
91
90
|
"keepNewest": 20 // 这个目录里更旧的截图会被删掉
|
|
92
91
|
},
|
|
93
92
|
|
|
94
|
-
// 系统提示里的全局规则。这三条换成你的,不是追加
|
|
95
|
-
"prompt": {
|
|
96
|
-
"guidelines": ["chrome_act: 这是用户真实的浏览器,破坏性操作前先问一句。"]
|
|
97
|
-
},
|
|
98
|
-
|
|
99
93
|
// 工具开关与名字。至少留一个启用;名字不能重复,也不能盖住宿主已有的工具
|
|
100
94
|
"tools": {
|
|
101
95
|
"chrome_act": { "enabled": true, "name": "chrome_act" },
|
|
@@ -71,14 +71,11 @@ export const routes = new Map([
|
|
|
71
71
|
],
|
|
72
72
|
[
|
|
73
73
|
"tab.new",
|
|
74
|
-
async ({ sessionKey, url
|
|
75
|
-
id: await createTarget({ sessionKey, url
|
|
74
|
+
async ({ sessionKey, url } = {}) => ({
|
|
75
|
+
id: await createTarget({ sessionKey, url }),
|
|
76
76
|
}),
|
|
77
77
|
],
|
|
78
|
-
[
|
|
79
|
-
"tab.activate",
|
|
80
|
-
({ tabId, background } = {}) => activateTab({ tabId, background }),
|
|
81
|
-
],
|
|
78
|
+
["tab.activate", ({ tabId } = {}) => activateTab({ tabId })],
|
|
82
79
|
["tab.close", ({ tabId } = {}) => closeTab({ tabId })],
|
|
83
80
|
[
|
|
84
81
|
"page.navigate",
|
|
@@ -205,11 +202,7 @@ function requireTabId(tabId) {
|
|
|
205
202
|
*/
|
|
206
203
|
async function tabIdOf(action, params) {
|
|
207
204
|
if (params.tabId !== undefined) return requireTabId(params.tabId);
|
|
208
|
-
return targetFor({
|
|
209
|
-
sessionKey: params.sessionKey,
|
|
210
|
-
action,
|
|
211
|
-
background: true,
|
|
212
|
-
});
|
|
205
|
+
return targetFor({ sessionKey: params.sessionKey, action });
|
|
213
206
|
}
|
|
214
207
|
|
|
215
208
|
/** 指针层的入参:ref 换成元素中心的视口坐标,其余原样。 */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 指针事件:移动、点击、悬停、拖动、滚轮。全部走 CDP,不做 DOM 假事件回退(DEC-0007)。
|
|
3
|
-
*
|
|
3
|
+
* Chrome 会丢弃不可见页面的可信输入,所以每个入口先调 assertVisible:不可见就把标签与窗口提到前台。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { evaluate, sendCommand } from "./debugger.js";
|
|
@@ -210,14 +210,51 @@ async function viewportCenter(tabId) {
|
|
|
210
210
|
return { x: center.x, y: center.y };
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/** 聚焦之后等页面变可见的次数与间隔:Chrome 要过一个 tick 才改 visibilityState。 */
|
|
214
|
+
const RAISE_TRIES = 6;
|
|
215
|
+
const RAISE_WAIT_MS = 50;
|
|
216
|
+
|
|
217
|
+
function sleep(ms) {
|
|
218
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function visibilityState(tabId) {
|
|
222
|
+
return evaluate(tabId, "document.visibilityState");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 页面为什么不可见,用来让报错指向人能动手的地方。 */
|
|
226
|
+
async function hiddenReason(tabId) {
|
|
227
|
+
const tab = await chrome.tabs.get(tabId).catch(() => undefined);
|
|
228
|
+
if (tab?.active === false) return "it is an inactive tab in its window";
|
|
229
|
+
const win =
|
|
230
|
+
tab?.windowId === undefined
|
|
231
|
+
? undefined
|
|
232
|
+
: await chrome.windows.get(tab.windowId).catch(() => undefined);
|
|
233
|
+
if (win?.state === "minimized") return "its window is minimized";
|
|
234
|
+
if (win?.state === "fullscreen")
|
|
235
|
+
return "its window is behind a full-screen window";
|
|
236
|
+
return "its window is covered by another window";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 输入前确保页面可见。不可见就先激活标签、把窗口带到前台,仍不可见才报错。
|
|
241
|
+
* 激活标签不改窗口焦点,所以这一步只在真需要可信输入时打扰用户。
|
|
242
|
+
*/
|
|
213
243
|
export async function assertVisible(tabId) {
|
|
214
|
-
const state = await
|
|
244
|
+
const state = await visibilityState(tabId);
|
|
215
245
|
if (state === "visible") return;
|
|
216
|
-
if (state
|
|
246
|
+
if (state !== "hidden")
|
|
217
247
|
throw new Error(
|
|
218
|
-
`page_error: tab ${tabId}
|
|
248
|
+
`page_error: could not read document.visibilityState for tab ${tabId}`,
|
|
219
249
|
);
|
|
250
|
+
const reason = await hiddenReason(tabId);
|
|
251
|
+
const tab = await chrome.tabs.update(tabId, { active: true });
|
|
252
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
253
|
+
for (let attempt = 0; attempt < RAISE_TRIES; attempt++) {
|
|
254
|
+
await sleep(RAISE_WAIT_MS);
|
|
255
|
+
if ((await visibilityState(tabId)) === "visible") return;
|
|
256
|
+
}
|
|
220
257
|
throw new Error(
|
|
221
|
-
`page_error:
|
|
258
|
+
`page_error: tab ${tabId} is hidden because ${reason}; Chrome drops trusted input to hidden pages. Bring the window to the front, then retry`,
|
|
222
259
|
);
|
|
223
260
|
}
|
|
@@ -23,17 +23,17 @@ export async function listTabs({ sessionKey } = {}) {
|
|
|
23
23
|
* @param {object} params
|
|
24
24
|
* @param {string} params.sessionKey 会话标识
|
|
25
25
|
* @param {string} [params.url] 要打开的地址,缺省 about:blank
|
|
26
|
-
* @param {boolean} [params.background] 为 true 时新标签不在本会话窗口里激活
|
|
27
26
|
* @returns {Promise<{ id: number, active: boolean, title: string, url: string }>}
|
|
28
27
|
*/
|
|
29
|
-
export async function newTab({ sessionKey, url
|
|
28
|
+
export async function newTab({ sessionKey, url } = {}) {
|
|
30
29
|
requireSessionKey(sessionKey);
|
|
31
30
|
const windowId = await liveWindowId(sessionKey);
|
|
32
31
|
if (windowId !== undefined) {
|
|
32
|
+
// 新标签在自己的窗口里就是活动标签。激活标签不改变窗口焦点,不会打扰用户。
|
|
33
33
|
const tab = await chrome.tabs.create({
|
|
34
34
|
windowId,
|
|
35
35
|
url: url ?? "about:blank",
|
|
36
|
-
active:
|
|
36
|
+
active: true,
|
|
37
37
|
});
|
|
38
38
|
return formatTab(await chrome.tabs.get(tab.id));
|
|
39
39
|
}
|
|
@@ -47,14 +47,12 @@ export async function newTab({ sessionKey, url, background = false } = {}) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* 激活标签,并把它所在的窗口带到前台。
|
|
51
51
|
* @param {object} params
|
|
52
52
|
* @param {number} params.tabId 标签 id
|
|
53
|
-
* @param {boolean} [params.background] 为 true 时拒绝激活
|
|
54
53
|
* @returns {Promise<{ id: number, active: boolean, title: string, url: string }>}
|
|
55
54
|
*/
|
|
56
|
-
export async function activateTab({ tabId
|
|
57
|
-
if (background) throw new Error("background mode blocks tab.activate");
|
|
55
|
+
export async function activateTab({ tabId } = {}) {
|
|
58
56
|
const tab = await chrome.tabs.update(tabId, { active: true });
|
|
59
57
|
await chrome.windows.update(tab.windowId, { focused: true });
|
|
60
58
|
return formatTab(tab);
|
|
@@ -18,17 +18,12 @@ export const OWNED_KEY_PREFIX = "target.owned.";
|
|
|
18
18
|
* @param {object} params
|
|
19
19
|
* @param {string} params.sessionKey 会话标识
|
|
20
20
|
* @param {string} [params.url] 新标签的地址
|
|
21
|
-
* @param {boolean} [params.background] 为 true 时新标签不激活
|
|
22
21
|
* @returns {Promise<number>} 新 target 的标签 id
|
|
23
22
|
*/
|
|
24
|
-
export async function createTarget({
|
|
25
|
-
sessionKey,
|
|
26
|
-
url,
|
|
27
|
-
background = false,
|
|
28
|
-
} = {}) {
|
|
23
|
+
export async function createTarget({ sessionKey, url } = {}) {
|
|
29
24
|
requireSessionKey(sessionKey);
|
|
30
25
|
|
|
31
|
-
const tab = await newTab({ sessionKey, url
|
|
26
|
+
const tab = await newTab({ sessionKey, url });
|
|
32
27
|
await writeRecord(TARGET_KEY_PREFIX + sessionKey, tab.id);
|
|
33
28
|
const owned = await readOwned(sessionKey);
|
|
34
29
|
await writeRecord(OWNED_KEY_PREFIX + sessionKey, [...owned, tab.id]);
|
|
@@ -41,14 +36,9 @@ export async function createTarget({
|
|
|
41
36
|
* @param {string} params.sessionKey 会话标识
|
|
42
37
|
* @param {string} params.action 动作名,决定能不能顺手建 target
|
|
43
38
|
* @param {string} [params.url] 建新标签时的地址
|
|
44
|
-
* @param {boolean} [params.background] 为 true 时新建的标签不激活
|
|
45
39
|
* @returns {Promise<number>} target 标签 id
|
|
46
40
|
*/
|
|
47
|
-
export async function targetFor({
|
|
48
|
-
sessionKey,
|
|
49
|
-
action,
|
|
50
|
-
background = false,
|
|
51
|
-
} = {}) {
|
|
41
|
+
export async function targetFor({ sessionKey, action } = {}) {
|
|
52
42
|
requireSessionKey(sessionKey);
|
|
53
43
|
if (!action) throw new Error("action is required");
|
|
54
44
|
|
|
@@ -59,7 +49,7 @@ export async function targetFor({
|
|
|
59
49
|
`no_target: no automation target for session ${sessionKey}`,
|
|
60
50
|
);
|
|
61
51
|
|
|
62
|
-
return createTarget({ sessionKey
|
|
52
|
+
return createTarget({ sessionKey });
|
|
63
53
|
}
|
|
64
54
|
|
|
65
55
|
/**
|
package/package.json
CHANGED
package/src/commands/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `/chrome` 命令:authorize
|
|
2
|
+
* `/chrome` 命令:authorize 与 status 两个子命令。
|
|
3
3
|
* 命令只改会话状态与读桥的状态,不发浏览器命令;子命令文本由 {@link ChromeCommand.run} 返回。
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -32,11 +32,35 @@ export const INSTALL_STEPS = [
|
|
|
32
32
|
"3. leave the extension enabled; it polls the bridge by itself",
|
|
33
33
|
].join("\n");
|
|
34
34
|
|
|
35
|
-
const SUBCOMMANDS = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
const SUBCOMMANDS = ["/chrome authorize on|off", "/chrome status"].join("\n");
|
|
36
|
+
|
|
37
|
+
/** 一条参数补全。形状与 pi 的 `AutocompleteItem` 一致,命令层不引 TUI 类型。 */
|
|
38
|
+
export interface ChromeCompletion {
|
|
39
|
+
value: string;
|
|
40
|
+
label: string;
|
|
41
|
+
description: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 整条子命令加一句说明,顺序即补全列表的顺序。 */
|
|
45
|
+
const COMPLETIONS: ReadonlyArray<readonly [string, string]> = [
|
|
46
|
+
["authorize on", "unlock the tools for this session"],
|
|
47
|
+
["authorize off", "lock the tools until the next authorize on"],
|
|
48
|
+
["status", "print host, extension, authorization, bridge and target state"],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `/chrome` 的参数补全:按整条子命令做前缀匹配,打到 `authorize ` 只剩 on 与 off。
|
|
53
|
+
* 一个都没匹配上返回 null,让 pi 用它的默认补全。
|
|
54
|
+
*/
|
|
55
|
+
export function completeChromeArgument(
|
|
56
|
+
prefix: string,
|
|
57
|
+
): ChromeCompletion[] | null {
|
|
58
|
+
const typed = prefix.trimStart();
|
|
59
|
+
const items = COMPLETIONS.filter(([value]) => value.startsWith(typed)).map(
|
|
60
|
+
([value, description]) => ({ value, label: value, description }),
|
|
61
|
+
);
|
|
62
|
+
return items.length > 0 ? items : null;
|
|
63
|
+
}
|
|
40
64
|
|
|
41
65
|
export function createChromeCommand(deps: ChromeCommandDeps): ChromeCommand {
|
|
42
66
|
return {
|
|
@@ -45,7 +69,6 @@ export function createChromeCommand(deps: ChromeCommandDeps): ChromeCommand {
|
|
|
45
69
|
if (trimmed === "") return SUBCOMMANDS;
|
|
46
70
|
const [verb, value] = trimmed.split(/\s+/);
|
|
47
71
|
if (verb === "authorize") return toggleAuthorize(deps, value);
|
|
48
|
-
if (verb === "background") return toggleBackground(deps, value);
|
|
49
72
|
if (verb === "status") return statusOf(deps);
|
|
50
73
|
return `bad_args: unknown subcommand: ${trimmed}\nhint: run /chrome for the list`;
|
|
51
74
|
},
|
|
@@ -71,17 +94,7 @@ async function toggleAuthorize(
|
|
|
71
94
|
return "authorized: on";
|
|
72
95
|
}
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
deps: ChromeCommandDeps,
|
|
76
|
-
value: string | undefined,
|
|
77
|
-
): string {
|
|
78
|
-
const on = flagOf(value);
|
|
79
|
-
if (on === undefined) return usage("background on|off");
|
|
80
|
-
deps.policy.setBackground(on);
|
|
81
|
-
return `background: ${on ? "on" : "off"}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** 状态行:桥的四项来自 `bridge.status()`,策略两项来自会话,target 来自宿主会话状态。 */
|
|
97
|
+
/** 状态行:桥的四项来自 `bridge.status()`,授权来自会话,target 来自宿主会话状态。 */
|
|
85
98
|
async function statusOf(deps: ChromeCommandDeps): Promise<string> {
|
|
86
99
|
await deps.bridge.start();
|
|
87
100
|
const bridge = await deps.bridge.describe();
|
|
@@ -90,7 +103,6 @@ async function statusOf(deps: ChromeCommandDeps): Promise<string> {
|
|
|
90
103
|
`host: ${deps.hostVersion}`,
|
|
91
104
|
`extension: ${bridge.extensionVersion ?? "not connected"}`,
|
|
92
105
|
`authorized: ${deps.policy.authorized ? "on" : "off"}`,
|
|
93
|
-
`background: ${deps.policy.background ? "on" : "off"}`,
|
|
94
106
|
`bridge: ${bridge.mode}`,
|
|
95
107
|
`port: ${new URL(bridge.url).port}`,
|
|
96
108
|
`connected: ${bridge.connected ? "yes" : "no"}`,
|
package/src/config/schema.ts
CHANGED
|
@@ -68,30 +68,16 @@ export interface ResolvedConfig {
|
|
|
68
68
|
bridge: { port: number };
|
|
69
69
|
policy: {
|
|
70
70
|
authorizeOnStart: boolean;
|
|
71
|
-
background: boolean;
|
|
72
71
|
closeOwnTabsOnExit: boolean;
|
|
73
72
|
};
|
|
74
73
|
input: { dragSteps: number; scrollSteps: number };
|
|
75
74
|
timing: { navigateTimeoutMs: number };
|
|
76
75
|
output: { maxElements: number; textChars: number };
|
|
77
76
|
screenshot: { dir: string; format: ScreenshotFormat; keepNewest: number };
|
|
78
|
-
prompt: { guidelines: string[] };
|
|
79
77
|
tools: Record<ToolId, ToolSettings>;
|
|
80
78
|
guidance: Partial<Record<ToolId, GuidanceFields>>;
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
/**
|
|
84
|
-
* 跨工具的默认规则。每条以工具名开头:两层规则平铺进系统提示后按原文去重,
|
|
85
|
-
* 不加工具前缀,只有点名才看得出是哪个工具的规则。
|
|
86
|
-
* 全局规则只点名 chrome_act:别的工具可以关掉,点名它们的规则放在该工具的
|
|
87
|
-
* promptGuidelines 里,才能随工具一起消失(SPEC-0001「关掉的工具……规则也一起消失」)。
|
|
88
|
-
*/
|
|
89
|
-
export const DEFAULT_PROMPT_GUIDELINES = [
|
|
90
|
-
"chrome_act: this is the user's real browser; confirm before destructive actions.",
|
|
91
|
-
"chrome_act: pass see: refs on navigate, then click the refs from that snapshot; a ref from an older page fails.",
|
|
92
|
-
"chrome_act: passing see adds a snapshot to the reply and costs extra tokens; ask for it only when the page just changed.",
|
|
93
|
-
] as const;
|
|
94
|
-
|
|
95
81
|
/**
|
|
96
82
|
* 13 个标量字段的默认值。工具记录与文案覆盖不在其中。
|
|
97
83
|
* `screenshot.dir` 跟着 agent 目录走,由 buildConfig 补,不在这里。
|
|
@@ -105,14 +91,12 @@ export const DEFAULTS: Omit<
|
|
|
105
91
|
bridge: { port: 17_318 },
|
|
106
92
|
policy: {
|
|
107
93
|
authorizeOnStart: false,
|
|
108
|
-
background: true,
|
|
109
94
|
closeOwnTabsOnExit: true,
|
|
110
95
|
},
|
|
111
96
|
input: { dragSteps: 10, scrollSteps: 5 },
|
|
112
97
|
timing: { navigateTimeoutMs: 15_000 },
|
|
113
98
|
output: { maxElements: 80, textChars: 18_000 },
|
|
114
99
|
screenshot: { format: "png", keepNewest: 20 },
|
|
115
|
-
prompt: { guidelines: [...DEFAULT_PROMPT_GUIDELINES] },
|
|
116
100
|
};
|
|
117
101
|
|
|
118
102
|
export type Raw = Record<string, unknown>;
|
package/src/config/validate.ts
CHANGED
|
@@ -29,7 +29,6 @@ const TOP_KEYS = [
|
|
|
29
29
|
"timing",
|
|
30
30
|
"output",
|
|
31
31
|
"screenshot",
|
|
32
|
-
"prompt",
|
|
33
32
|
"tools",
|
|
34
33
|
"guidance",
|
|
35
34
|
];
|
|
@@ -45,7 +44,7 @@ export function buildConfig(
|
|
|
45
44
|
const policy = asObject(raw.policy, "policy", errors);
|
|
46
45
|
checkUnknownKeys(
|
|
47
46
|
policy,
|
|
48
|
-
["authorizeOnStart", "
|
|
47
|
+
["authorizeOnStart", "closeOwnTabsOnExit"],
|
|
49
48
|
"policy",
|
|
50
49
|
errors,
|
|
51
50
|
);
|
|
@@ -79,12 +78,6 @@ export function buildConfig(
|
|
|
79
78
|
"policy.authorizeOnStart",
|
|
80
79
|
errors,
|
|
81
80
|
),
|
|
82
|
-
background: readBoolean(
|
|
83
|
-
policy.background,
|
|
84
|
-
DEFAULTS.policy.background,
|
|
85
|
-
"policy.background",
|
|
86
|
-
errors,
|
|
87
|
-
),
|
|
88
81
|
closeOwnTabsOnExit: readBoolean(
|
|
89
82
|
policy.closeOwnTabsOnExit,
|
|
90
83
|
DEFAULTS.policy.closeOwnTabsOnExit,
|
|
@@ -155,7 +148,6 @@ export function buildConfig(
|
|
|
155
148
|
RANGES.keepNewest,
|
|
156
149
|
),
|
|
157
150
|
},
|
|
158
|
-
prompt: readPrompt(raw.prompt, errors),
|
|
159
151
|
tools: readTools(raw.tools, errors),
|
|
160
152
|
guidance: readGuidance(raw.guidance, errors),
|
|
161
153
|
};
|
|
@@ -222,16 +214,6 @@ function checkUnknownKeys(
|
|
|
222
214
|
}
|
|
223
215
|
}
|
|
224
216
|
|
|
225
|
-
function readPrompt(value: unknown, errors: Errors): { guidelines: string[] } {
|
|
226
|
-
const raw = asObject(value, "prompt", errors);
|
|
227
|
-
checkUnknownKeys(raw, ["guidelines"], "prompt", errors);
|
|
228
|
-
const guidelines =
|
|
229
|
-
raw.guidelines === undefined
|
|
230
|
-
? [...DEFAULTS.prompt.guidelines]
|
|
231
|
-
: readStringArray(raw.guidelines, "prompt.guidelines", errors);
|
|
232
|
-
return { guidelines };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
217
|
function readGuidance(
|
|
236
218
|
value: unknown,
|
|
237
219
|
errors: Errors,
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,10 @@ import type {
|
|
|
8
8
|
ToolDefinition,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { Bridge } from "./bridge/index.ts";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
completeChromeArgument,
|
|
13
|
+
createChromeCommand,
|
|
14
|
+
} from "./commands/index.ts";
|
|
12
15
|
import { agentDir, HOST_VERSION, loadConfig } from "./config/index.ts";
|
|
13
16
|
import { createSessionPolicy, type SessionPolicy } from "./policy/index.ts";
|
|
14
17
|
import {
|
|
@@ -41,7 +44,8 @@ export default function piBrowserUseLite(pi: ExtensionAPI): void {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
pi.registerCommand("chrome", {
|
|
44
|
-
description: "authorize
|
|
47
|
+
description: "authorize the tools, or report the bridge state",
|
|
48
|
+
getArgumentCompletions: completeChromeArgument,
|
|
45
49
|
handler: async (args, ctx) => {
|
|
46
50
|
ctx.ui.notify(await chrome.run(args));
|
|
47
51
|
},
|
|
@@ -67,13 +71,6 @@ export default function piBrowserUseLite(pi: ExtensionAPI): void {
|
|
|
67
71
|
policy.endSession();
|
|
68
72
|
await bridge.stop();
|
|
69
73
|
});
|
|
70
|
-
|
|
71
|
-
pi.on("before_agent_start", (event) => {
|
|
72
|
-
event.systemPromptOptions.promptGuidelines = [
|
|
73
|
-
...event.systemPromptOptions.promptGuidelines,
|
|
74
|
-
...config.prompt.guidelines,
|
|
75
|
-
];
|
|
76
|
-
});
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
/**
|
|
@@ -88,7 +85,7 @@ function registerable(
|
|
|
88
85
|
return {
|
|
89
86
|
...tool,
|
|
90
87
|
execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
|
|
91
|
-
const denial = policy.check(
|
|
88
|
+
const denial = policy.check();
|
|
92
89
|
if (denial) return errorResult(denial);
|
|
93
90
|
return await executors.execute(
|
|
94
91
|
tool.id,
|
|
@@ -119,7 +116,3 @@ function createTargetHolder(): SessionTarget {
|
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
/** 参数里的动作名。只有 chrome_act 与 chrome_tab 带 action,别的工具给空串。 */
|
|
122
|
-
function actionOf(params: unknown): string {
|
|
123
|
-
const action = (params as { action?: unknown } | null | undefined)?.action;
|
|
124
|
-
return typeof action === "string" ? action : "";
|
|
125
|
-
}
|
package/src/policy/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 策略插点:授权开关。
|
|
3
|
+
* 状态只在内存,随会话结束清回未授权;判定不做 IO,也没有定时器。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ResolvedConfig } from "../config/index.ts";
|
|
@@ -18,27 +18,15 @@ const NOT_AUTHORIZED: PolicyDenial = {
|
|
|
18
18
|
hint: "Run /chrome authorize on",
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
const FOCUS_ACTION = "switch";
|
|
23
|
-
|
|
24
|
-
const FOCUS_BLOCKED: PolicyDenial = {
|
|
25
|
-
class: "locked",
|
|
26
|
-
message: "background mode blocks tab switch",
|
|
27
|
-
hint: "Run /chrome background off",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** 会话内的授权与背景模式状态。 */
|
|
21
|
+
/** 会话内的授权状态。 */
|
|
31
22
|
export interface SessionPolicy {
|
|
32
23
|
readonly authorized: boolean;
|
|
33
|
-
readonly background: boolean;
|
|
34
24
|
/** 授权开关。只在内存,不落盘。 */
|
|
35
25
|
authorize(on: boolean): void;
|
|
36
|
-
/**
|
|
37
|
-
setBackground(on: boolean): void;
|
|
38
|
-
/** 会话结束:授权回到未授权,背景模式回到配置默认值。 */
|
|
26
|
+
/** 会话结束:授权回到未授权。 */
|
|
39
27
|
endSession(): void;
|
|
40
28
|
/** 插点:一次工具调用先过这里。放行返回 undefined,拒绝返回原因。 */
|
|
41
|
-
check(
|
|
29
|
+
check(): PolicyDenial | undefined;
|
|
42
30
|
}
|
|
43
31
|
|
|
44
32
|
/** 造一份会话策略。`authorizeOnStart` 为真时开局即授权。 */
|
|
@@ -46,28 +34,18 @@ export function createSessionPolicy(
|
|
|
46
34
|
settings: ResolvedConfig["policy"],
|
|
47
35
|
): SessionPolicy {
|
|
48
36
|
let authorized = settings.authorizeOnStart;
|
|
49
|
-
let background = settings.background;
|
|
50
37
|
return {
|
|
51
38
|
get authorized() {
|
|
52
39
|
return authorized;
|
|
53
40
|
},
|
|
54
|
-
get background() {
|
|
55
|
-
return background;
|
|
56
|
-
},
|
|
57
41
|
authorize(on) {
|
|
58
42
|
authorized = on;
|
|
59
43
|
},
|
|
60
|
-
setBackground(on) {
|
|
61
|
-
background = on;
|
|
62
|
-
},
|
|
63
44
|
endSession() {
|
|
64
45
|
authorized = false;
|
|
65
|
-
background = settings.background;
|
|
66
46
|
},
|
|
67
|
-
check(
|
|
68
|
-
|
|
69
|
-
if (background && action === FOCUS_ACTION) return FOCUS_BLOCKED;
|
|
70
|
-
return undefined;
|
|
47
|
+
check() {
|
|
48
|
+
return authorized ? undefined : NOT_AUTHORIZED;
|
|
71
49
|
},
|
|
72
50
|
};
|
|
73
51
|
}
|
package/src/tools/exec/tab.ts
CHANGED
|
@@ -34,7 +34,6 @@ export async function runTab(
|
|
|
34
34
|
case "new": {
|
|
35
35
|
const result = (await call(ctx, "tab.new", {
|
|
36
36
|
url: params.url,
|
|
37
|
-
background: true,
|
|
38
37
|
})) as TabInfo;
|
|
39
38
|
const id = targetIdOf(result);
|
|
40
39
|
if (id !== undefined) ctx.deps.target.set(id);
|
|
@@ -45,10 +44,7 @@ export async function runTab(
|
|
|
45
44
|
}
|
|
46
45
|
case "switch": {
|
|
47
46
|
const tabId = await requireTab(ctx, "switch", params.tab);
|
|
48
|
-
const result = await call(ctx, "tab.activate", {
|
|
49
|
-
tabId,
|
|
50
|
-
background: false,
|
|
51
|
-
});
|
|
47
|
+
const result = await call(ctx, "tab.activate", { tabId });
|
|
52
48
|
return okResult(`ok: switch ${tabId}`, { result });
|
|
53
49
|
}
|
|
54
50
|
case "close": {
|
package/src/tools/index.ts
CHANGED
|
@@ -268,6 +268,9 @@ export const TOOL_SPECS = {
|
|
|
268
268
|
),
|
|
269
269
|
}),
|
|
270
270
|
promptGuidelines: [
|
|
271
|
+
"chrome_act: this is the user's real browser; confirm before destructive actions.",
|
|
272
|
+
"chrome_act: pass see: refs on navigate, then click the refs from that snapshot; a ref from an older page fails.",
|
|
273
|
+
"chrome_act: passing see adds a snapshot to the reply and costs extra tokens; ask for it only when the page just changed.",
|
|
271
274
|
"chrome_act: name the action first, then send only the parameters that action reads.",
|
|
272
275
|
"chrome_act: batch runs steps in order and stops at the first failure unless the step is optional.",
|
|
273
276
|
],
|