@nextclaw/x-harness 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/LICENSE +231 -6
- package/README.md +15 -1
- package/SOURCE.md +9 -0
- package/cli/main.js +232 -200
- package/dist/api.d.ts +56 -2
- package/dist/api.js +130 -79
- package/dist/boundary.js +1 -0
- package/dist/browser-common.d.ts +11 -0
- package/dist/browser-common.js +34 -18
- package/dist/browser-dom-capabilities.d.ts +7 -2
- package/dist/browser-dom-capabilities.js +123 -102
- package/dist/browser-dom-dependencies.js +1 -0
- package/dist/browser-dom-editing.d.ts +20 -2
- package/dist/browser-dom-editing.js +148 -116
- package/dist/browser-dom-navigation.d.ts +11 -2
- package/dist/browser-dom-navigation.js +66 -50
- package/dist/browser-dom-submission.d.ts +6 -1
- package/dist/browser-dom-submission.js +37 -18
- package/dist/browser-dom.d.ts +13 -1
- package/dist/browser-dom.js +113 -66
- package/dist/browser-native.d.ts +14 -1
- package/dist/browser-native.js +161 -128
- package/dist/creator-native.d.ts +3 -0
- package/dist/creator-native.js +113 -0
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.js +10 -0
- package/dist/growth-batch.d.ts +6 -3
- package/dist/growth-batch.js +157 -135
- package/dist/growth-dependencies.js +1 -0
- package/dist/growth-execution.d.ts +8 -2
- package/dist/growth-execution.js +68 -37
- package/dist/growth-parallel.d.ts +6 -2
- package/dist/growth-parallel.js +78 -47
- package/dist/growth-plan.d.ts +8 -3
- package/dist/growth-plan.js +45 -23
- package/dist/growth-policy.d.ts +11 -2
- package/dist/growth-policy.js +80 -53
- package/dist/growth-publishing.d.ts +7 -2
- package/dist/growth-publishing.js +72 -46
- package/dist/growth-reading.d.ts +6 -2
- package/dist/growth-reading.js +47 -25
- package/dist/growth-recovery.d.ts +7 -4
- package/dist/growth-recovery.js +177 -148
- package/dist/growth-state.d.ts +10 -2
- package/dist/growth-state.js +67 -30
- package/dist/growth-tasks.d.ts +19 -16
- package/dist/growth-tasks.js +153 -90
- package/dist/growth.d.ts +25 -1
- package/dist/growth.js +207 -124
- package/dist/platform-capabilities.d.ts +6 -2
- package/dist/platform-capabilities.js +18 -8
- package/dist/platform-collections.d.ts +10 -3
- package/dist/platform-collections.js +62 -35
- package/dist/platform-dependencies.js +1 -0
- package/dist/platform-diagnostics.d.ts +7 -2
- package/dist/platform-diagnostics.js +31 -19
- package/dist/platform-observation.d.ts +10 -2
- package/dist/platform-observation.js +44 -23
- package/dist/platform-targets.d.ts +10 -3
- package/dist/platform-targets.js +39 -14
- package/dist/platform-writing.d.ts +10 -2
- package/dist/platform-writing.js +63 -32
- package/dist/platform.d.ts +22 -1
- package/dist/platform.js +120 -52
- package/dist/readers-ax-host.d.ts +11 -1
- package/dist/readers-ax-host.js +108 -99
- package/dist/readers-ax-program.js +5 -0
- package/dist/readers-ax-tree.js +1 -0
- package/dist/readers-cli.js +79 -0
- package/dist/readers.js +9 -0
- package/dist/round-state.js +18 -1
- package/dist/store.d.ts +22 -4
- package/dist/store.js +173 -151
- package/dist/types.js +1 -0
- package/generated/observation.mjs +1 -1
- package/package.json +23 -9
- package/skill/SKILL.md +2 -2
- package/skill/references/reads.md +1 -1
- package/skill/references/setup.md +1 -1
|
@@ -1,50 +1,71 @@
|
|
|
1
1
|
import { xURL } from "./platform-rules.js";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
export class DOMNavigation {
|
|
3
|
+
#session;
|
|
4
|
+
#tab;
|
|
5
|
+
#observe;
|
|
6
|
+
searchSources;
|
|
7
|
+
waitForFolder;
|
|
8
|
+
waitForPosts;
|
|
9
|
+
openObserved;
|
|
10
|
+
refreshPerson;
|
|
11
|
+
refreshPosts;
|
|
12
|
+
constructor(dependencies) {
|
|
13
|
+
const { session, tab, observe } = dependencies;
|
|
14
|
+
this.#session = session;
|
|
15
|
+
this.#tab = tab;
|
|
16
|
+
this.#observe = observe;
|
|
17
|
+
this.searchSources = this.handleSearchSources.bind(this);
|
|
18
|
+
this.waitForFolder = this.handleWaitForFolder.bind(this);
|
|
19
|
+
this.waitForPosts = this.handleWaitForPosts.bind(this);
|
|
20
|
+
this.openObserved = this.handleOpenObserved.bind(this);
|
|
21
|
+
this.refreshPerson = this.handleRefreshPerson.bind(this);
|
|
22
|
+
this.refreshPosts = this.handleRefreshPosts.bind(this);
|
|
23
|
+
}
|
|
24
|
+
async handleSearchSources(command) {
|
|
5
25
|
if (typeof command.query !== "string" ||
|
|
6
26
|
!command.query.trim() ||
|
|
7
27
|
command.query.length > 300)
|
|
8
28
|
throw new Error("需一条有效搜索词");
|
|
9
|
-
session.current = null;
|
|
29
|
+
this.#session.current = null;
|
|
10
30
|
const destination = `https://x.com/search?q=${encodeURIComponent(command.query)}&src=typed_query&f=live`;
|
|
11
|
-
if (typeof tab.url !== "function" ||
|
|
12
|
-
await tab.
|
|
31
|
+
if (typeof this.#tab.url !== "function" ||
|
|
32
|
+
(await this.#tab.url()) !== destination)
|
|
33
|
+
await this.#tab.goto(destination);
|
|
13
34
|
return;
|
|
14
35
|
}
|
|
15
|
-
async
|
|
16
|
-
await tab.playwright
|
|
36
|
+
async handleWaitForFolder(command) {
|
|
37
|
+
await this.#tab.playwright
|
|
17
38
|
.getByText(command.folder, { exact: true })
|
|
18
39
|
.waitFor({ state: "visible", timeoutMs: 2000 });
|
|
19
40
|
return;
|
|
20
41
|
}
|
|
21
|
-
async
|
|
22
|
-
await tab.playwright
|
|
42
|
+
async handleWaitForPosts(command) {
|
|
43
|
+
await this.#tab.playwright
|
|
23
44
|
.getByRole("article")
|
|
24
45
|
.first()
|
|
25
46
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
26
47
|
return;
|
|
27
48
|
}
|
|
28
|
-
async
|
|
49
|
+
async handleOpenObserved(command) {
|
|
29
50
|
xURL(command.url);
|
|
30
51
|
if (!command.evidenceRef)
|
|
31
52
|
throw new Error("导航缺已观察证据");
|
|
32
53
|
// A direct adapter caller must also preserve a page already at the target.
|
|
33
54
|
// Explicit recovery uses forceNavigation; durable verification uses refreshPerson.
|
|
34
55
|
if (!command.forceNavigation &&
|
|
35
|
-
typeof tab.url === "function" &&
|
|
36
|
-
(await tab.url()) === xURL(command.url).href)
|
|
56
|
+
typeof this.#tab.url === "function" &&
|
|
57
|
+
(await this.#tab.url()) === xURL(command.url).href)
|
|
37
58
|
return;
|
|
38
|
-
const originalPage = session.current?.result?.page;
|
|
39
|
-
session.replyBinding = null;
|
|
40
|
-
session.current = null;
|
|
59
|
+
const originalPage = this.#session.current?.result?.page;
|
|
60
|
+
this.#session.replyBinding = null;
|
|
61
|
+
this.#session.current = null;
|
|
41
62
|
// Keep the loaded X client when the exact observed permalink is already visible.
|
|
42
63
|
// An ambiguous/missing link retains the existing navigation path, never an arbitrary click.
|
|
43
64
|
const path = xURL(command.url).pathname + xURL(command.url).search;
|
|
44
65
|
let link;
|
|
45
66
|
if (!command.forceNavigation &&
|
|
46
|
-
typeof tab.playwright?.locator === "function") {
|
|
47
|
-
link = tab.playwright
|
|
67
|
+
typeof this.#tab.playwright?.locator === "function") {
|
|
68
|
+
link = this.#tab.playwright
|
|
48
69
|
.locator(`a[href=${JSON.stringify(path)}]`)
|
|
49
70
|
.filter({ visible: true });
|
|
50
71
|
if (command.view === "person" &&
|
|
@@ -63,9 +84,9 @@ export function createDOMNavigation(dependencies) {
|
|
|
63
84
|
if (clickedLink)
|
|
64
85
|
await link.click();
|
|
65
86
|
else
|
|
66
|
-
await tab.goto(command.url);
|
|
87
|
+
await this.#tab.goto(command.url);
|
|
67
88
|
try {
|
|
68
|
-
await tab.playwright.waitForURL(command.url, { timeoutMs: 5000 });
|
|
89
|
+
await this.#tab.playwright.waitForURL(command.url, { timeoutMs: 5000 });
|
|
69
90
|
}
|
|
70
91
|
catch (error) {
|
|
71
92
|
// One inert client-link timeout may use the same observed URL directly.
|
|
@@ -74,7 +95,7 @@ export function createDOMNavigation(dependencies) {
|
|
|
74
95
|
!originalPage ||
|
|
75
96
|
!/Timed out waiting for URL/.test(error instanceof Error ? error.message : ""))
|
|
76
97
|
throw error;
|
|
77
|
-
const check = await observe({
|
|
98
|
+
const check = await this.#observe({
|
|
78
99
|
view: command.view,
|
|
79
100
|
handle: command.handle,
|
|
80
101
|
limit: 1,
|
|
@@ -83,39 +104,39 @@ export function createDOMNavigation(dependencies) {
|
|
|
83
104
|
check.needsStateCheck ||
|
|
84
105
|
check.alerts?.length)
|
|
85
106
|
throw error;
|
|
86
|
-
await tab.goto(command.url);
|
|
87
|
-
await tab.playwright.waitForURL(command.url, { timeoutMs: 5000 });
|
|
107
|
+
await this.#tab.goto(command.url);
|
|
108
|
+
await this.#tab.playwright.waitForURL(command.url, { timeoutMs: 5000 });
|
|
88
109
|
}
|
|
89
110
|
if (command.view === "person") {
|
|
90
111
|
// Profile readiness belongs to profile content; self profiles have no Follow button.
|
|
91
|
-
await tab.playwright
|
|
112
|
+
await this.#tab.playwright
|
|
92
113
|
.locator('[data-testid="UserName"]')
|
|
93
114
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
94
115
|
}
|
|
95
116
|
else if (command.view === "composer") {
|
|
96
|
-
await tab.playwright
|
|
117
|
+
await this.#tab.playwright
|
|
97
118
|
.getByRole("textbox", { name: /^(帖子文本|Post text)$/i })
|
|
98
119
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
99
120
|
}
|
|
100
121
|
else if (command.view === "posts") {
|
|
101
|
-
await tab.playwright
|
|
122
|
+
await this.#tab.playwright
|
|
102
123
|
.getByRole("article")
|
|
103
124
|
.first()
|
|
104
125
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
105
126
|
}
|
|
106
127
|
return;
|
|
107
128
|
}
|
|
108
|
-
async
|
|
109
|
-
const at = xURL(await tab.url());
|
|
110
|
-
session.replyBinding = null;
|
|
129
|
+
async handleRefreshPerson(command) {
|
|
130
|
+
const at = xURL(await this.#tab.url());
|
|
131
|
+
this.#session.replyBinding = null;
|
|
111
132
|
if (at.href !== xURL(command.url).href ||
|
|
112
133
|
at.pathname.toLowerCase() !== `/${command.handle}`.toLowerCase())
|
|
113
134
|
throw new Error("持久核验页面不匹配");
|
|
114
|
-
session.current = null;
|
|
135
|
+
this.#session.current = null;
|
|
115
136
|
// Let the actual UI request settle before reloading; optimistic Following is not durable proof.
|
|
116
137
|
let idleTimedOut = false;
|
|
117
138
|
try {
|
|
118
|
-
await tab.playwright.waitForLoadState({
|
|
139
|
+
await this.#tab.playwright.waitForLoadState({
|
|
119
140
|
state: "networkidle",
|
|
120
141
|
timeoutMs: 8000,
|
|
121
142
|
});
|
|
@@ -127,39 +148,34 @@ export function createDOMNavigation(dependencies) {
|
|
|
127
148
|
// must expose the target relationship before any fresh observation.
|
|
128
149
|
idleTimedOut = true;
|
|
129
150
|
}
|
|
130
|
-
await tab.reload();
|
|
131
|
-
await tab.playwright
|
|
151
|
+
await this.#tab.reload();
|
|
152
|
+
await this.#tab.playwright
|
|
132
153
|
.getByRole("button", {
|
|
133
154
|
name: new RegExp(`^(?:正在关注|取消关注|关注|回关) @${command.handle}$`, "i"),
|
|
134
155
|
})
|
|
135
156
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
136
157
|
if (idleTimedOut)
|
|
137
|
-
session.refreshNotice =
|
|
158
|
+
this.#session.refreshNotice =
|
|
138
159
|
"networkidle超时,已沿真实reload与目标关系控件恢复核验";
|
|
139
160
|
return;
|
|
140
161
|
}
|
|
141
|
-
async
|
|
142
|
-
session.replyBinding = null;
|
|
162
|
+
async handleRefreshPosts(command) {
|
|
163
|
+
this.#session.replyBinding = null;
|
|
143
164
|
if (!command.evidenceRef ||
|
|
144
|
-
xURL(await tab.url()).href !== xURL(command.url).href)
|
|
165
|
+
xURL(await this.#tab.url()).href !== xURL(command.url).href)
|
|
145
166
|
throw new Error("公开核验页面不匹配");
|
|
146
|
-
session.current = null;
|
|
147
|
-
await tab.reload();
|
|
167
|
+
this.#session.current = null;
|
|
168
|
+
await this.#tab.reload();
|
|
148
169
|
// Native observation settles the real loading transition when the tab exposes it.
|
|
149
|
-
if (typeof tab.getAXState === "function")
|
|
150
|
-
await tab.getAXState({ emit: false });
|
|
151
|
-
await tab.playwright
|
|
170
|
+
if (typeof this.#tab.getAXState === "function")
|
|
171
|
+
await this.#tab.getAXState({ emit: false });
|
|
172
|
+
await this.#tab.playwright
|
|
152
173
|
.getByRole("article")
|
|
153
174
|
.first()
|
|
154
175
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
155
176
|
return;
|
|
156
177
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
waitForPosts,
|
|
161
|
-
openObserved,
|
|
162
|
-
refreshPerson,
|
|
163
|
-
refreshPosts,
|
|
164
|
-
};
|
|
178
|
+
}
|
|
179
|
+
export function createDOMNavigation(...args) {
|
|
180
|
+
return new DOMNavigation(...args);
|
|
165
181
|
}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { DOMDependencies } from "./browser-dom-dependencies.js";
|
|
2
|
-
export declare
|
|
2
|
+
export declare class DOMSubmission {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(dependencies: Pick<DOMDependencies, "submitAX" | "session" | "tab" | "observe" | "unique" | "locator">);
|
|
5
|
+
finishDOMAction(command: any, prepared: any, query: any, result: any): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createDOMSubmission(...args: ConstructorParameters<typeof DOMSubmission>): (command: any, prepared: any, query: any, result: any) => Promise<void>;
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { xURL } from "./platform-rules.js";
|
|
2
2
|
import { valueOf } from "./browser-common.js";
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export class DOMSubmission {
|
|
4
|
+
#submitAX;
|
|
5
|
+
#session;
|
|
6
|
+
#tab;
|
|
7
|
+
#observe;
|
|
8
|
+
#unique;
|
|
9
|
+
#locator;
|
|
10
|
+
constructor(dependencies) {
|
|
11
|
+
this.finishDOMAction = this.finishDOMAction.bind(this);
|
|
12
|
+
const { submitAX, session, tab, observe, unique, locator } = dependencies;
|
|
13
|
+
this.#submitAX = submitAX;
|
|
14
|
+
this.#session = session;
|
|
15
|
+
this.#tab = tab;
|
|
16
|
+
this.#observe = observe;
|
|
17
|
+
this.#unique = unique;
|
|
18
|
+
this.#locator = locator;
|
|
19
|
+
}
|
|
20
|
+
async finishDOMAction(command, prepared, query, result) {
|
|
6
21
|
const { loc, submittedEditor } = prepared;
|
|
7
22
|
const binding = command.operation === "openReply"
|
|
8
23
|
? {
|
|
@@ -13,8 +28,9 @@ export function createDOMSubmission(dependencies) {
|
|
|
13
28
|
}
|
|
14
29
|
: null;
|
|
15
30
|
let nativeSubmitIndex;
|
|
16
|
-
if (submitAX &&
|
|
17
|
-
|
|
31
|
+
if (this.#submitAX &&
|
|
32
|
+
["submitPost", "submitReply"].includes(command.operation)) {
|
|
33
|
+
const native = await this.#submitAX.observe({ mode: "composer" });
|
|
18
34
|
const editors = native.evidence?.filter((e) => /^(?:文本输入区(?:域)?|文本区域|文本栏) /.test(e.label)) ?? [];
|
|
19
35
|
const buttons = native.evidence?.filter((e) => new RegExp(command.operation === "submitReply"
|
|
20
36
|
? "^按钮 (回复|Reply)(?:,|$)"
|
|
@@ -30,9 +46,9 @@ export function createDOMSubmission(dependencies) {
|
|
|
30
46
|
}
|
|
31
47
|
if (["submitPost", "submitReply"].includes(command.operation))
|
|
32
48
|
await command.beforeSubmit?.();
|
|
33
|
-
session.current = null;
|
|
49
|
+
this.#session.current = null;
|
|
34
50
|
if (nativeSubmitIndex !== undefined)
|
|
35
|
-
await submitAX.act({
|
|
51
|
+
await this.#submitAX.act({
|
|
36
52
|
index: nativeSubmitIndex,
|
|
37
53
|
operation: "click",
|
|
38
54
|
read: { mode: "composer" },
|
|
@@ -42,15 +58,15 @@ export function createDOMSubmission(dependencies) {
|
|
|
42
58
|
else
|
|
43
59
|
await loc.click();
|
|
44
60
|
if (binding) {
|
|
45
|
-
session.replyBinding = binding;
|
|
46
|
-
await tab.playwright
|
|
61
|
+
this.#session.replyBinding = binding;
|
|
62
|
+
await this.#tab.playwright
|
|
47
63
|
.getByRole("textbox", { name: /^(帖子文本|Post text)$/i })
|
|
48
64
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
49
|
-
if (xURL(await tab.url()).pathname === "/compose/post")
|
|
50
|
-
await tab.playwright
|
|
65
|
+
if (xURL(await this.#tab.url()).pathname === "/compose/post")
|
|
66
|
+
await this.#tab.playwright
|
|
51
67
|
.getByText(`回复 @${binding.author}`, { exact: true })
|
|
52
68
|
.waitFor({ state: "visible", timeoutMs: 5000 });
|
|
53
|
-
const composer = await observe({ view: "composer" });
|
|
69
|
+
const composer = await this.#observe({ view: "composer" });
|
|
54
70
|
const quote = composer.replyQuotes?.length === 1 ? composer.replyQuotes[0] : null;
|
|
55
71
|
if (!composer.replyToId &&
|
|
56
72
|
!composer.needsStateCheck &&
|
|
@@ -62,11 +78,11 @@ export function createDOMSubmission(dependencies) {
|
|
|
62
78
|
quote?.length >= 30 &&
|
|
63
79
|
binding.body?.startsWith(quote)) {
|
|
64
80
|
// Expand the observed quote; the unchanged full-context binding gate must still pass.
|
|
65
|
-
const dialog = await unique(locator("dialog").filter({ visible: true }));
|
|
66
|
-
const more = await unique(dialog.getByRole("button", { name: /^(显示更多|Show more)$/i }));
|
|
67
|
-
session.current = null;
|
|
81
|
+
const dialog = await this.#unique(this.#locator("dialog").filter({ visible: true }));
|
|
82
|
+
const more = await this.#unique(dialog.getByRole("button", { name: /^(显示更多|Show more)$/i }));
|
|
83
|
+
this.#session.current = null;
|
|
68
84
|
await more.click();
|
|
69
|
-
await observe({ view: "composer" });
|
|
85
|
+
await this.#observe({ view: "composer" });
|
|
70
86
|
}
|
|
71
87
|
}
|
|
72
88
|
// Inline replies keep the editor mounted. Wait for this submitted text to clear before
|
|
@@ -74,9 +90,12 @@ export function createDOMSubmission(dependencies) {
|
|
|
74
90
|
if (submittedEditor)
|
|
75
91
|
await submittedEditor.waitFor({ state: "hidden", timeoutMs: 10000 });
|
|
76
92
|
if (["submitPost", "submitReply"].includes(command.operation))
|
|
77
|
-
await tab.playwright.waitForLoadState({
|
|
93
|
+
await this.#tab.playwright.waitForLoadState({
|
|
78
94
|
state: "networkidle",
|
|
79
95
|
timeoutMs: 8000,
|
|
80
96
|
});
|
|
81
|
-
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function createDOMSubmission(...args) {
|
|
100
|
+
return new DOMSubmission(...args).finishDOMAction;
|
|
82
101
|
}
|
package/dist/browser-dom.d.ts
CHANGED
|
@@ -1,2 +1,14 @@
|
|
|
1
1
|
export { readXNetworkDiagnostics } from "./browser-dom-network.js";
|
|
2
|
-
export declare
|
|
2
|
+
export declare class DOMXBrowser {
|
|
3
|
+
#private;
|
|
4
|
+
capabilities: (operation: any) => any;
|
|
5
|
+
observe: (query: any) => Promise<any>;
|
|
6
|
+
constructor(tab: any, { readDOM, nativeSubmit }?: any);
|
|
7
|
+
private handleObserve;
|
|
8
|
+
private locator;
|
|
9
|
+
private unique;
|
|
10
|
+
collectCreator({ handle, ...options }: import("./types.js").CreatorOptions): Promise<import("./boundary.js").BoundaryRecord>;
|
|
11
|
+
screenshot(): any;
|
|
12
|
+
act(command: any): Promise<any>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createDOMXBrowser(...args: ConstructorParameters<typeof DOMXBrowser>): any;
|
package/dist/browser-dom.js
CHANGED
|
@@ -7,29 +7,85 @@ import { createDOMNavigation } from "./browser-dom-navigation.js";
|
|
|
7
7
|
import { createDOMEditing } from "./browser-dom-editing.js";
|
|
8
8
|
import { createDOMSubmission } from "./browser-dom-submission.js";
|
|
9
9
|
export { readXNetworkDiagnostics } from "./browser-dom-network.js";
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
export class DOMXBrowser {
|
|
11
|
+
#tab;
|
|
12
|
+
#readDOM;
|
|
13
|
+
#nativeSubmit;
|
|
14
|
+
#session;
|
|
15
|
+
#submitAX;
|
|
16
|
+
#navigation;
|
|
17
|
+
#editing;
|
|
18
|
+
#finishDOMAction;
|
|
19
|
+
capabilities;
|
|
20
|
+
observe;
|
|
21
|
+
constructor(tab, { readDOM = readXObservation, nativeSubmit = false } = {}) {
|
|
22
|
+
this.act = this.act.bind(this);
|
|
23
|
+
this.screenshot = this.screenshot.bind(this);
|
|
24
|
+
this.collectCreator = this.collectCreator.bind(this);
|
|
25
|
+
this.#tab = tab;
|
|
26
|
+
this.#readDOM = readDOM;
|
|
27
|
+
this.#nativeSubmit = nativeSubmit;
|
|
28
|
+
this.#session = {};
|
|
29
|
+
this.#submitAX = this.#nativeSubmit
|
|
30
|
+
? createAXHarness({
|
|
31
|
+
getAXState: (options) => this.#tab.getAXState(options),
|
|
32
|
+
click: (index) => this.#tab.click(index),
|
|
33
|
+
})
|
|
34
|
+
: null;
|
|
35
|
+
this.#navigation = createDOMNavigation({
|
|
36
|
+
session: this.#session,
|
|
37
|
+
tab: this.#tab,
|
|
38
|
+
observe: this.handleObserve.bind(this),
|
|
39
|
+
});
|
|
40
|
+
this.#editing = createDOMEditing({
|
|
41
|
+
session: this.#session,
|
|
42
|
+
unique: this.unique.bind(this),
|
|
43
|
+
locator: this.locator.bind(this),
|
|
44
|
+
tab: this.#tab,
|
|
45
|
+
observe: this.handleObserve.bind(this),
|
|
46
|
+
});
|
|
47
|
+
this.#finishDOMAction = createDOMSubmission({
|
|
48
|
+
submitAX: this.#submitAX,
|
|
49
|
+
session: this.#session,
|
|
50
|
+
tab: this.#tab,
|
|
51
|
+
observe: this.handleObserve.bind(this),
|
|
52
|
+
unique: this.unique.bind(this),
|
|
53
|
+
locator: this.locator.bind(this),
|
|
54
|
+
});
|
|
55
|
+
Object.assign(this, createDOMCapabilities({
|
|
56
|
+
tab: this.#tab,
|
|
57
|
+
nativeSubmit: this.#nativeSubmit,
|
|
58
|
+
}));
|
|
59
|
+
this.observe = this.handleObserve.bind(this);
|
|
60
|
+
Object.assign(this, serialized({
|
|
61
|
+
...createDOMCapabilities({
|
|
62
|
+
tab: this.#tab,
|
|
63
|
+
nativeSubmit: this.#nativeSubmit,
|
|
64
|
+
}),
|
|
65
|
+
observe: this.handleObserve.bind(this),
|
|
66
|
+
collectCreator: this.collectCreator,
|
|
67
|
+
screenshot: () => {
|
|
68
|
+
this.#session.current = null;
|
|
69
|
+
return this.#tab.screenshot();
|
|
70
|
+
},
|
|
71
|
+
act: this.act,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
async handleObserve(query) {
|
|
75
|
+
this.#session.current = null;
|
|
76
|
+
const evaluate = this.#tab?.playwright?.evaluate?.bind(this.#tab.playwright);
|
|
21
77
|
if (!evaluate)
|
|
22
78
|
throw new Error("缺playwright.evaluate能力");
|
|
23
|
-
const result = await evaluate(readDOM, {
|
|
79
|
+
const result = await evaluate(this.#readDOM, {
|
|
24
80
|
...query,
|
|
25
81
|
limit: query.limit ?? 1,
|
|
26
82
|
});
|
|
27
83
|
const raw = result;
|
|
28
84
|
// This observed disclosure confirms AI provenance; preserve it without treating it as a rate/login failure.
|
|
29
85
|
result.notices = (result.alerts ?? []).filter((text) => text.trim() === "检测到 AI 生成内容");
|
|
30
|
-
if (session.refreshNotice) {
|
|
31
|
-
result.notices.push(session.refreshNotice);
|
|
32
|
-
session.refreshNotice = null;
|
|
86
|
+
if (this.#session.refreshNotice) {
|
|
87
|
+
result.notices.push(this.#session.refreshNotice);
|
|
88
|
+
this.#session.refreshNotice = null;
|
|
33
89
|
}
|
|
34
90
|
result.alerts = (result.alerts ?? []).filter((text) => text.trim() !== "检测到 AI 生成内容");
|
|
35
91
|
if (query.view === "composer") {
|
|
@@ -37,67 +93,58 @@ export function createDOMXBrowser(tab, { readDOM = readXObservation, nativeSubmi
|
|
|
37
93
|
raw.editors?.length === 1 ? raw.editors[0].text : null;
|
|
38
94
|
if (result.inlineComposer &&
|
|
39
95
|
raw.editors?.length === 1 &&
|
|
40
|
-
session.replyBinding?.page === result.page &&
|
|
96
|
+
this.#session.replyBinding?.page === result.page &&
|
|
41
97
|
result.page.match(/\/status\/(\d+)$/)?.[1] ===
|
|
42
|
-
session.replyBinding.postId)
|
|
43
|
-
result.replyToId = session.replyBinding.postId;
|
|
98
|
+
this.#session.replyBinding.postId)
|
|
99
|
+
result.replyToId = this.#session.replyBinding.postId;
|
|
44
100
|
if (!result.inlineComposer &&
|
|
45
101
|
raw.editors?.length === 1 &&
|
|
46
|
-
session.replyBinding?.body &&
|
|
47
|
-
session.replyBinding?.author &&
|
|
102
|
+
this.#session.replyBinding?.body &&
|
|
103
|
+
this.#session.replyBinding?.author &&
|
|
48
104
|
result.replyContexts?.length === 1 &&
|
|
49
|
-
(result.replyContexts[0].includes(session.replyBinding.body) ||
|
|
105
|
+
(result.replyContexts[0].includes(this.#session.replyBinding.body) ||
|
|
50
106
|
(result.replyQuotes?.length === 1 &&
|
|
51
107
|
result.replyQuotes[0].length >= 80 &&
|
|
52
|
-
session.replyBinding.body.startsWith(result.replyQuotes[0]))) &&
|
|
53
|
-
result.replyContexts[0].includes(`回复 @${session.replyBinding.author}`))
|
|
54
|
-
result.replyToId = session.replyBinding.postId;
|
|
108
|
+
this.#session.replyBinding.body.startsWith(result.replyQuotes[0]))) &&
|
|
109
|
+
result.replyContexts[0].includes(`回复 @${this.#session.replyBinding.author}`))
|
|
110
|
+
result.replyToId = this.#session.replyBinding.postId;
|
|
55
111
|
}
|
|
56
|
-
session.current = { query, result };
|
|
112
|
+
this.#session.current = { query, result };
|
|
57
113
|
return result;
|
|
58
114
|
}
|
|
59
|
-
|
|
60
|
-
|
|
115
|
+
locator(role, label = undefined) {
|
|
116
|
+
return this.#tab.playwright.getByRole(role, label ? { name: label, exact: true } : {});
|
|
117
|
+
}
|
|
118
|
+
async unique(loc) {
|
|
61
119
|
if ((await loc.count()) !== 1)
|
|
62
120
|
throw new Error("DOM语义控件不唯一");
|
|
63
121
|
return loc;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const handler = Object.hasOwn(editing, command.operation)
|
|
93
|
-
? editing[command.operation]
|
|
94
|
-
: undefined;
|
|
95
|
-
if (!handler)
|
|
96
|
-
throw new Error("未封装的DOM动作");
|
|
97
|
-
const prepared = await handler(command);
|
|
98
|
-
if (prepared.completed)
|
|
99
|
-
return prepared.value;
|
|
100
|
-
return finishDOMAction(command, prepared, query, result);
|
|
101
|
-
},
|
|
102
|
-
});
|
|
122
|
+
}
|
|
123
|
+
async collectCreator({ handle, ...options }) {
|
|
124
|
+
this.#session.current = null;
|
|
125
|
+
return collectCreatorDOM(this.#tab, handle, options);
|
|
126
|
+
}
|
|
127
|
+
screenshot() {
|
|
128
|
+
this.#session.current = null;
|
|
129
|
+
return this.#tab.screenshot();
|
|
130
|
+
}
|
|
131
|
+
async act(command) {
|
|
132
|
+
if (Object.hasOwn(this.#navigation, command.operation))
|
|
133
|
+
return this.#navigation[command.operation](command);
|
|
134
|
+
if (!this.#session.current)
|
|
135
|
+
throw new Error("DOM语义操作必须先observe");
|
|
136
|
+
const { query, result } = this.#session.current;
|
|
137
|
+
const handler = Object.hasOwn(this.#editing, command.operation)
|
|
138
|
+
? this.#editing[command.operation]
|
|
139
|
+
: undefined;
|
|
140
|
+
if (!handler)
|
|
141
|
+
throw new Error("未封装的DOM动作");
|
|
142
|
+
const prepared = await handler(command);
|
|
143
|
+
if (prepared.completed)
|
|
144
|
+
return prepared.value;
|
|
145
|
+
return this.#finishDOMAction(command, prepared, query, result);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export function createDOMXBrowser(...args) {
|
|
149
|
+
return new DOMXBrowser(...args);
|
|
103
150
|
}
|
package/dist/browser-native.d.ts
CHANGED
|
@@ -1 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import type { BoundaryRecord } from "./boundary.js";
|
|
2
|
+
export declare class NativeXBrowser {
|
|
3
|
+
#private;
|
|
4
|
+
observe: (query: any) => Promise<any>;
|
|
5
|
+
constructor(app: any);
|
|
6
|
+
private navigateNativeTarget;
|
|
7
|
+
private handleObserve;
|
|
8
|
+
private one;
|
|
9
|
+
collectCreator(args: import("./types.js").CreatorOptions): Promise<BoundaryRecord>;
|
|
10
|
+
capabilities(operation: any): any;
|
|
11
|
+
act(command: any): Promise<any>;
|
|
12
|
+
screenshot(): any;
|
|
13
|
+
}
|
|
14
|
+
export declare function createNativeXBrowser(...args: ConstructorParameters<typeof NativeXBrowser>): any;
|