@nextclaw/x-harness 0.1.0 → 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 +17 -3
- package/SOURCE.md +9 -0
- package/cli/main.js +232 -188
- package/dist/api.d.ts +56 -2
- package/dist/api.js +178 -75
- 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 +9 -0
- package/dist/browser-dom-capabilities.js +131 -0
- package/dist/browser-dom-dependencies.d.ts +16 -0
- package/dist/browser-dom-dependencies.js +1 -0
- package/dist/browser-dom-editing.d.ts +166 -0
- package/dist/browser-dom-editing.js +418 -0
- package/dist/browser-dom-navigation.d.ts +18 -0
- package/dist/browser-dom-navigation.js +181 -0
- package/dist/browser-dom-network.d.ts +1 -0
- package/dist/browser-dom-network.js +128 -0
- package/dist/browser-dom-submission.d.ts +7 -0
- package/dist/browser-dom-submission.js +101 -0
- package/dist/browser-dom.d.ts +14 -2
- package/dist/browser-dom.js +120 -824
- package/dist/browser-native.d.ts +14 -1
- package/dist/browser-native.js +180 -143
- package/dist/browser.js +6 -2
- package/dist/creator-native.d.ts +3 -0
- package/dist/creator-native.js +113 -0
- package/dist/creator.js +1 -1
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.js +10 -0
- package/dist/growth-batch.d.ts +8 -0
- package/dist/growth-batch.js +190 -0
- package/dist/growth-constants.d.ts +5 -0
- package/dist/growth-constants.js +13 -0
- package/dist/growth-dependencies.d.ts +33 -0
- package/dist/growth-dependencies.js +1 -0
- package/dist/growth-execution.d.ts +17 -0
- package/dist/growth-execution.js +346 -0
- package/dist/growth-parallel.d.ts +8 -0
- package/dist/growth-parallel.js +332 -0
- package/dist/growth-plan.d.ts +9 -0
- package/dist/growth-plan.js +229 -0
- package/dist/growth-policy.d.ts +18 -0
- package/dist/growth-policy.js +208 -0
- package/dist/growth-publishing.d.ts +10 -0
- package/dist/growth-publishing.js +266 -0
- package/dist/growth-reading.d.ts +8 -0
- package/dist/growth-reading.js +157 -0
- package/dist/growth-recovery.d.ts +9 -0
- package/dist/growth-recovery.js +187 -0
- package/dist/growth-state.d.ts +16 -0
- package/dist/growth-state.js +220 -0
- package/dist/growth-tasks.d.ts +20 -0
- package/dist/growth-tasks.js +164 -0
- package/dist/growth.d.ts +26 -1
- package/dist/growth.js +211 -1908
- package/dist/platform-capabilities.d.ts +8 -0
- package/dist/platform-capabilities.js +29 -0
- package/dist/platform-collections.d.ts +14 -0
- package/dist/platform-collections.js +250 -0
- package/dist/platform-dependencies.d.ts +43 -0
- package/dist/platform-dependencies.js +1 -0
- package/dist/platform-diagnostics.d.ts +14 -0
- package/dist/platform-diagnostics.js +63 -0
- package/dist/platform-observation.d.ts +16 -0
- package/dist/platform-observation.js +96 -0
- package/dist/platform-rules.d.ts +17 -0
- package/dist/platform-rules.js +41 -0
- package/dist/platform-targets.d.ts +14 -0
- package/dist/platform-targets.js +154 -0
- package/dist/platform-writing.d.ts +16 -0
- package/dist/platform-writing.js +202 -0
- package/dist/platform.d.ts +23 -2
- package/dist/platform.js +125 -678
- package/dist/readers-actions.d.ts +2 -0
- package/dist/readers-actions.js +66 -0
- package/dist/readers-ax-host.d.ts +12 -0
- package/dist/readers-ax-host.js +134 -0
- package/dist/readers-ax-parser.d.ts +1 -0
- package/dist/readers-ax-parser.js +69 -0
- package/dist/readers-ax-program.d.ts +1 -0
- package/dist/readers-ax-program.js +5 -0
- package/dist/readers-ax-tree.d.ts +19 -0
- package/dist/readers-ax-tree.js +111 -0
- package/dist/readers-ax-views.d.ts +87 -0
- package/dist/readers-ax-views.js +247 -0
- package/dist/readers-cli.d.ts +6 -0
- package/dist/readers-cli.js +79 -0
- package/dist/readers-composer.d.ts +2 -0
- package/dist/readers-composer.js +131 -0
- package/dist/readers-navigation.d.ts +2 -0
- package/dist/readers-navigation.js +66 -0
- package/dist/readers-posts.d.ts +2 -0
- package/dist/readers-posts.js +141 -0
- package/dist/readers-profile.d.ts +3 -0
- package/dist/readers-profile.js +157 -0
- package/dist/readers.d.ts +9 -20
- package/dist/readers.js +9 -1045
- package/dist/results.js +1 -1
- package/dist/round-state.js +20 -3
- package/dist/store.d.ts +22 -4
- package/dist/store.js +173 -151
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/generated/observation.mjs +3 -2
- package/package.json +23 -9
- package/skill/SKILL.md +17 -36
- package/skill/references/api.md +7 -34
- package/skill/references/reads.md +23 -0
- package/skill/references/recovery.md +25 -0
- package/skill/references/setup.md +23 -0
- package/skill/references/writes.md +51 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readComposer } from "./readers-composer.js";
|
|
2
|
+
import { readPosts } from "./readers-posts.js";
|
|
3
|
+
// Upload only through the observed image button and browser file chooser.
|
|
4
|
+
export async function attachImage(tab, { path } = {}) {
|
|
5
|
+
if (!/^\/.*\.(?:png|jpe?g|webp)$/i.test(path ?? ""))
|
|
6
|
+
throw new Error("需要绝对图片路径");
|
|
7
|
+
if (typeof tab?.playwright?.evaluate !== "function" ||
|
|
8
|
+
typeof tab?.playwright?.getByRole !== "function" ||
|
|
9
|
+
typeof tab?.playwright?.waitForEvent !== "function")
|
|
10
|
+
throw new Error("harness能力不足:DOM图片选择器");
|
|
11
|
+
const before = await tab.playwright.evaluate(readComposer);
|
|
12
|
+
if (before.needsStateCheck || before.alerts?.length)
|
|
13
|
+
throw new Error("图片上传前编辑器未确认");
|
|
14
|
+
const buttons = before.buttons.filter((b) => /^(添加照片或视频|Add photos or video)$/i.test(b.label) && !b.disabled);
|
|
15
|
+
if (buttons.length !== 1)
|
|
16
|
+
throw new Error("图片按钮未唯一观察");
|
|
17
|
+
const button = tab.playwright.getByRole("button", {
|
|
18
|
+
name: buttons[0].label,
|
|
19
|
+
exact: true,
|
|
20
|
+
});
|
|
21
|
+
if ((await button.count()) !== 1 || typeof button.click !== "function")
|
|
22
|
+
throw new Error("图片按钮未唯一定位");
|
|
23
|
+
const chooserPromise = tab.playwright.waitForEvent("filechooser", {
|
|
24
|
+
timeoutMs: 10000,
|
|
25
|
+
});
|
|
26
|
+
await button.click();
|
|
27
|
+
const chooser = await chooserPromise;
|
|
28
|
+
if (typeof chooser?.setFiles !== "function")
|
|
29
|
+
throw new Error("harness能力不足:setFiles");
|
|
30
|
+
await chooser.setFiles([path]);
|
|
31
|
+
return tab.playwright.evaluate(readComposer);
|
|
32
|
+
}
|
|
33
|
+
export async function openReply(tab, { postId } = {}) {
|
|
34
|
+
if (!/^\d+$/.test(postId ?? ""))
|
|
35
|
+
throw new Error("需要已观察公开ID");
|
|
36
|
+
if (typeof tab?.playwright?.evaluate !== "function" ||
|
|
37
|
+
typeof tab?.playwright?.locator !== "function")
|
|
38
|
+
throw new Error("harness能力不足:精准回复范围");
|
|
39
|
+
const state = await tab.playwright.evaluate(readPosts, {
|
|
40
|
+
targetId: postId,
|
|
41
|
+
limit: 1,
|
|
42
|
+
});
|
|
43
|
+
const post = state.posts[0];
|
|
44
|
+
if (state.needsStateCheck ||
|
|
45
|
+
state.alerts.length ||
|
|
46
|
+
state.posts.length !== 1 ||
|
|
47
|
+
post.missing.length ||
|
|
48
|
+
post.needsExpansion)
|
|
49
|
+
throw new Error("回复根帖证据未完整");
|
|
50
|
+
const buttons = post.buttons.filter((b) => /回复|Reply/i.test(b.label) && !b.disabled);
|
|
51
|
+
if (buttons.length !== 1)
|
|
52
|
+
throw new Error("回复按钮未唯一观察");
|
|
53
|
+
const article = tab.playwright
|
|
54
|
+
.locator("article")
|
|
55
|
+
.filter({ has: tab.playwright.locator(`a[href="${post.primary}"]`) });
|
|
56
|
+
if ((await article.count()) !== 1)
|
|
57
|
+
throw new Error("回复文章范围不唯一");
|
|
58
|
+
const button = article.getByRole("button", {
|
|
59
|
+
name: buttons[0].label,
|
|
60
|
+
exact: true,
|
|
61
|
+
});
|
|
62
|
+
if ((await button.count()) !== 1)
|
|
63
|
+
throw new Error("回复控件不唯一");
|
|
64
|
+
await button.click();
|
|
65
|
+
return tab.playwright.evaluate(readComposer);
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function createAXTabHarness(tab: any): any;
|
|
2
|
+
export declare class AXHarness {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(app: any);
|
|
5
|
+
private requireMethods;
|
|
6
|
+
private indexes;
|
|
7
|
+
private handleObserve;
|
|
8
|
+
observe(options?: any): Promise<any>;
|
|
9
|
+
act({ index, operation, value, read, }?: any): Promise<any>;
|
|
10
|
+
screenshot(): Promise<any>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createAXHarness(...args: ConstructorParameters<typeof AXHarness>): any;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readAX } from "./readers-ax-parser.js";
|
|
2
|
+
// Native operations share one observation boundary. No raw AX leaves this session.
|
|
3
|
+
// Browser Tab input takes (index, value), while native App input takes (value).
|
|
4
|
+
export function createAXTabHarness(tab) {
|
|
5
|
+
return createAXHarness({
|
|
6
|
+
getAXState: (options) => tab.getAXState(options),
|
|
7
|
+
click: (index) => tab.click(index),
|
|
8
|
+
pressKey: (key) => tab.pressKey(null, key),
|
|
9
|
+
paste: (value) => tab.paste(null, value),
|
|
10
|
+
scroll: (...args) => tab.scroll(...args),
|
|
11
|
+
getScreenshot: (options) => tab.getScreenshot(options),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export class AXHarness {
|
|
15
|
+
#app;
|
|
16
|
+
#controls;
|
|
17
|
+
#readable;
|
|
18
|
+
#busy;
|
|
19
|
+
constructor(app) {
|
|
20
|
+
this.screenshot = this.screenshot.bind(this);
|
|
21
|
+
this.act = this.act.bind(this);
|
|
22
|
+
this.observe = this.observe.bind(this);
|
|
23
|
+
this.#app = app;
|
|
24
|
+
this.#controls = new Map();
|
|
25
|
+
this.#readable = false;
|
|
26
|
+
this.#busy = false;
|
|
27
|
+
}
|
|
28
|
+
requireMethods(methods) {
|
|
29
|
+
const missing = methods.filter((name) => typeof this.#app?.[name] !== "function");
|
|
30
|
+
if (missing.length) {
|
|
31
|
+
this.#controls.clear();
|
|
32
|
+
this.#readable = false;
|
|
33
|
+
throw new Error(`harness能力不足:${missing.join("/")};先补原入口并验证,未执行操作`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
indexes(value) {
|
|
37
|
+
if (!value || typeof value !== "object")
|
|
38
|
+
return;
|
|
39
|
+
if (Number.isInteger(value.index) && typeof value.label === "string")
|
|
40
|
+
this.#controls.set(value.index, value.label);
|
|
41
|
+
for (const child of Object.values(value))
|
|
42
|
+
this.indexes(child);
|
|
43
|
+
}
|
|
44
|
+
async handleObserve(options = {}) {
|
|
45
|
+
this.#controls.clear();
|
|
46
|
+
this.#readable = false;
|
|
47
|
+
readAX("", options);
|
|
48
|
+
this.requireMethods(["getAXState"]);
|
|
49
|
+
const result = readAX(await this.#app.getAXState({ emit: false, disableDiffing: true }), options);
|
|
50
|
+
this.indexes(result);
|
|
51
|
+
this.#readable = true;
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
async observe(options = {}) {
|
|
55
|
+
if (this.#busy)
|
|
56
|
+
throw new Error("AX操作进行中,禁止并发读取");
|
|
57
|
+
this.#busy = true;
|
|
58
|
+
try {
|
|
59
|
+
return await this.handleObserve(options);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
this.#busy = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async act({ index, operation = "click", value, read = {}, } = {}) {
|
|
66
|
+
if (this.#busy)
|
|
67
|
+
throw new Error("AX操作进行中,禁止并发写入");
|
|
68
|
+
if (!this.#readable || !this.#controls.has(index))
|
|
69
|
+
throw new Error("必须先observe;index须来自最新定向证据");
|
|
70
|
+
if (!["click", "fill", "paste", "pressKey", "scroll", "secondary"].includes(operation))
|
|
71
|
+
throw new Error("未封装的AX操作,先补能力并验证");
|
|
72
|
+
// Validate before any side effect; a malformed follow-up read must never submit.
|
|
73
|
+
readAX("", read);
|
|
74
|
+
if (["fill", "paste", "pressKey", "secondary"].includes(operation) &&
|
|
75
|
+
typeof value !== "string")
|
|
76
|
+
throw new Error("AX操作value须为字符串");
|
|
77
|
+
if (["fill", "paste", "pressKey"].includes(operation) &&
|
|
78
|
+
!/^(?:文本输入区(?:域)?|文本区域|文本栏|组合框) /.test(this.#controls.get(index)))
|
|
79
|
+
throw new Error("键盘操作须定位最新证据中的编辑控件");
|
|
80
|
+
if (operation === "secondary" &&
|
|
81
|
+
!this.#controls.get(index).includes(`Secondary Actions: ${value}`))
|
|
82
|
+
throw new Error("未观察到该辅助动作");
|
|
83
|
+
if (operation === "scroll" &&
|
|
84
|
+
!["up", "down", "left", "right"].includes(value))
|
|
85
|
+
throw new Error("无效滚动方向");
|
|
86
|
+
const methods = operation === "fill"
|
|
87
|
+
? ["click", "pressKey", "paste"]
|
|
88
|
+
: ["paste", "pressKey"].includes(operation)
|
|
89
|
+
? ["click", operation]
|
|
90
|
+
: [operation === "secondary" ? "performSecondaryAction" : operation];
|
|
91
|
+
this.requireMethods([...methods, "getAXState"]);
|
|
92
|
+
this.#busy = true;
|
|
93
|
+
this.#controls.clear();
|
|
94
|
+
this.#readable = false;
|
|
95
|
+
try {
|
|
96
|
+
if (operation === "click")
|
|
97
|
+
await this.#app.click(index);
|
|
98
|
+
else if (["fill", "paste", "pressKey"].includes(operation)) {
|
|
99
|
+
await this.#app.click(index);
|
|
100
|
+
if (operation === "fill") {
|
|
101
|
+
await this.#app.pressKey("super+a");
|
|
102
|
+
await this.#app.paste(value);
|
|
103
|
+
}
|
|
104
|
+
else
|
|
105
|
+
await this.#app[operation](value);
|
|
106
|
+
}
|
|
107
|
+
else if (operation === "scroll")
|
|
108
|
+
await this.#app.scroll(index, value, 1);
|
|
109
|
+
else
|
|
110
|
+
await this.#app.performSecondaryAction(index, value);
|
|
111
|
+
return await this.handleObserve(read);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
this.#busy = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async screenshot() {
|
|
118
|
+
if (this.#busy)
|
|
119
|
+
throw new Error("AX操作进行中,禁止并发截图");
|
|
120
|
+
this.requireMethods(["getScreenshot"]);
|
|
121
|
+
this.#busy = true;
|
|
122
|
+
this.#controls.clear();
|
|
123
|
+
this.#readable = false;
|
|
124
|
+
try {
|
|
125
|
+
return await this.#app.getScreenshot({ emit: false });
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
this.#busy = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function createAXHarness(...args) {
|
|
133
|
+
return new AXHarness(...args);
|
|
134
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readAX(state: any, { mode, offset, limit, targetId, targetHandle, }?: any): any;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Native Chrome fallback: process a suppressed full AX observation in the REPL.
|
|
2
|
+
// Keeps visible leaf evidence and action indexes; never reconstructs a diff as a full page.
|
|
3
|
+
import { parseAXNodes, createAXTree } from "./readers-ax-tree.js";
|
|
4
|
+
import { projectAXFilepicker, projectAXMedia, projectAXNavigation, projectAXPeople, projectAXPosts, projectAXProfile, projectAXComposer, } from "./readers-ax-views.js";
|
|
5
|
+
export function readAX(state, { mode = "posts", offset = 0, limit = 1, targetId = "", targetHandle = "", } = {}) {
|
|
6
|
+
if (![
|
|
7
|
+
"posts",
|
|
8
|
+
"composer",
|
|
9
|
+
"profile",
|
|
10
|
+
"people",
|
|
11
|
+
"navigation",
|
|
12
|
+
"filepicker",
|
|
13
|
+
"media",
|
|
14
|
+
].includes(mode))
|
|
15
|
+
throw new Error("AX mode必须为posts/composer/profile/people/navigation");
|
|
16
|
+
if (typeof state !== "string")
|
|
17
|
+
throw new Error("AX state必须为完整字符串");
|
|
18
|
+
if (!Number.isInteger(offset) ||
|
|
19
|
+
offset < 0 ||
|
|
20
|
+
!Number.isInteger(limit) ||
|
|
21
|
+
limit < 1 ||
|
|
22
|
+
limit > 5 ||
|
|
23
|
+
(targetId && !/^\d+$/.test(targetId)))
|
|
24
|
+
throw new Error("无效AX读取窗口或ID");
|
|
25
|
+
if (targetHandle && (mode !== "people" || !/^\w{1,15}$/.test(targetHandle)))
|
|
26
|
+
throw new Error("targetHandle必须为people模式已观察的handle");
|
|
27
|
+
const alertLines = state
|
|
28
|
+
.split("\n")
|
|
29
|
+
.filter((line) => /抱歉.*限制|速度限制|请求异常|访问受限|验证码|验证挑战|登录以继续|captcha|rate limit/i.test(line));
|
|
30
|
+
const unknown = (reason) => ({
|
|
31
|
+
mode,
|
|
32
|
+
posts: [],
|
|
33
|
+
evidence: [],
|
|
34
|
+
alerts: alertLines,
|
|
35
|
+
missing: [reason],
|
|
36
|
+
needsStateCheck: true,
|
|
37
|
+
pageComplete: false,
|
|
38
|
+
});
|
|
39
|
+
if (/following is a diff|Removed element IDs:|no change in the accessibility tree/i.test(state))
|
|
40
|
+
return unknown("full-ax-required");
|
|
41
|
+
const page = state.match(/URL:\s*((?:https?:\/\/)?x\.com\/[^\s,]*)/)?.[1];
|
|
42
|
+
if (!page && !["navigation", "filepicker"].includes(mode))
|
|
43
|
+
return unknown("x-page-unconfirmed");
|
|
44
|
+
const nodes = parseAXNodes(state);
|
|
45
|
+
const tree = createAXTree(nodes);
|
|
46
|
+
const context = {
|
|
47
|
+
mode,
|
|
48
|
+
page,
|
|
49
|
+
offset,
|
|
50
|
+
limit,
|
|
51
|
+
targetId,
|
|
52
|
+
targetHandle,
|
|
53
|
+
unknown,
|
|
54
|
+
alertLines,
|
|
55
|
+
};
|
|
56
|
+
if (mode === "filepicker")
|
|
57
|
+
return projectAXFilepicker(tree, context);
|
|
58
|
+
if (mode === "media")
|
|
59
|
+
return projectAXMedia(tree, context);
|
|
60
|
+
if (mode === "navigation")
|
|
61
|
+
return projectAXNavigation(tree, context);
|
|
62
|
+
if (mode === "people")
|
|
63
|
+
return projectAXPeople(tree, context);
|
|
64
|
+
if (mode === "posts")
|
|
65
|
+
return projectAXPosts(tree, context);
|
|
66
|
+
if (mode === "profile")
|
|
67
|
+
return projectAXProfile(tree, context);
|
|
68
|
+
return projectAXComposer(tree, context);
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const axParserCode: string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { readAX } from "./readers-ax-parser.js";
|
|
2
|
+
import { parseAXNodes, createAXTree } from "./readers-ax-tree.js";
|
|
3
|
+
import { projectAXFilepicker, projectAXMedia, projectAXNavigation, projectAXPeople, projectAXPosts, projectAXProfile, projectAXComposer, } from "./readers-ax-views.js";
|
|
4
|
+
// Explicit serialization closure; the same parser runs in Node and the REPL.
|
|
5
|
+
export const axParserCode = `(function(){const parseAXNodes=${parseAXNodes.toString()};const createAXTree=${createAXTree.toString()};const projectAXFilepicker=${projectAXFilepicker.toString()};const projectAXMedia=${projectAXMedia.toString()};const projectAXNavigation=${projectAXNavigation.toString()};const projectAXPeople=${projectAXPeople.toString()};const projectAXPosts=${projectAXPosts.toString()};const projectAXProfile=${projectAXProfile.toString()};const projectAXComposer=${projectAXComposer.toString()};return ${readAX.toString()};})()`;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function parseAXNodes(state: string): any[];
|
|
2
|
+
export declare function createAXTree(nodes: any[]): {
|
|
3
|
+
nodes: any[];
|
|
4
|
+
descendants: (i: any) => any[];
|
|
5
|
+
evidence: (ns: any) => any;
|
|
6
|
+
navigation: {
|
|
7
|
+
index: any;
|
|
8
|
+
label: any;
|
|
9
|
+
}[];
|
|
10
|
+
roots: any[];
|
|
11
|
+
peopleRoots: any[];
|
|
12
|
+
peopleTimeline: number;
|
|
13
|
+
within: (node: any, root: any) => boolean;
|
|
14
|
+
alerts: {
|
|
15
|
+
index: any;
|
|
16
|
+
label: any;
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
19
|
+
export type AXTree = ReturnType<typeof createAXTree>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export function parseAXNodes(state) {
|
|
2
|
+
const nodes = [], stack = [];
|
|
3
|
+
for (const line of state.split("\n")) {
|
|
4
|
+
const match = line.match(/^(\s*)(\d+) (.*)$/);
|
|
5
|
+
// Multiline post text such as "37 位中文主样本" is not an AX node.
|
|
6
|
+
if (!match ||
|
|
7
|
+
(!match[1] &&
|
|
8
|
+
!/^(?:标准窗口|表单|对话框|sheet|dialog|window|menu bar)(?: |$)/i.test(match[3]))) {
|
|
9
|
+
if (nodes.length &&
|
|
10
|
+
line.trim() &&
|
|
11
|
+
!/^The focused UI|Window:|Wall time:|Output:/.test(line))
|
|
12
|
+
nodes.at(-1).label += "\n" + line;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const depth = match[1].replace(/\t/g, " ").length;
|
|
16
|
+
while (stack.length && nodes[stack.at(-1)].depth >= depth)
|
|
17
|
+
stack.pop();
|
|
18
|
+
// Chrome tab AX uses English role names; native app AX uses the canonical labels below.
|
|
19
|
+
const label = match[3]
|
|
20
|
+
.replace(/^link Description: /, "link ")
|
|
21
|
+
.replace(/^combo box(?: \([^)]*\))? (?:Description: )?/, "组合框 ")
|
|
22
|
+
.replace(/^button(?: \((?![^)]*disabled)[^)]*\))? /, "按钮 ")
|
|
23
|
+
.replace(/^text entry area(?: \(settable\))? Description: /, "文本输入区域 ");
|
|
24
|
+
const node = {
|
|
25
|
+
index: Number(match[2]),
|
|
26
|
+
label,
|
|
27
|
+
depth,
|
|
28
|
+
parent: stack.at(-1) ?? null,
|
|
29
|
+
};
|
|
30
|
+
nodes.push(node);
|
|
31
|
+
stack.push(nodes.length - 1);
|
|
32
|
+
}
|
|
33
|
+
return nodes;
|
|
34
|
+
}
|
|
35
|
+
export function createAXTree(nodes) {
|
|
36
|
+
const descendants = (i) => {
|
|
37
|
+
let end = i + 1;
|
|
38
|
+
while (end < nodes.length && nodes[end].depth > nodes[i].depth)
|
|
39
|
+
end++;
|
|
40
|
+
return nodes.slice(i + 1, end);
|
|
41
|
+
};
|
|
42
|
+
const leaf = (n) => /^(?:文本(?: |$)|text |link |按钮 |弹出式按钮 |切换按钮 |标签 |tab |图像 |文本输入区(?:域)? |文本区域 |文本栏 |组合框 |单选按钮 |复选框 )/.test(n.label) || /^container [\d,.万亿KMB]+ 次(?:观看|查看)$/.test(n.label);
|
|
43
|
+
const evidence = (ns) => ns.filter(leaf).map(({ index, label }) => ({ index, label }));
|
|
44
|
+
const navigation = nodes
|
|
45
|
+
.filter((n) => /^(?:标准窗口|window|HTML 内容) .*URL:/.test(n.label) ||
|
|
46
|
+
/^文本栏 .*地址和搜索栏/.test(n.label) ||
|
|
47
|
+
/^按钮 (?:重新加载|新标签页)$/.test(n.label) ||
|
|
48
|
+
n.label === "窗口" ||
|
|
49
|
+
/, ID: makeKeyAndOrderFront:$/.test(n.label) ||
|
|
50
|
+
/^标签 \((?:selected|settable)/.test(n.label) ||
|
|
51
|
+
/^(?:tab|标签)(?: \([^)]*\))? (?:帖子|回复|转帖|媒体|文章|Posts|Replies|Reposts|Media|Articles)(?:,|$)/.test(n.label) ||
|
|
52
|
+
/^组合框 (?:查询词条|Search)(?:,|$)/.test(n.label) ||
|
|
53
|
+
/^link (?:主页|搜索和发现|通知|个人资料), Value: x\.com\//.test(n.label))
|
|
54
|
+
.map(({ index, label }) => ({ index, label }));
|
|
55
|
+
const roots = [];
|
|
56
|
+
const within = (node, root) => {
|
|
57
|
+
let p = node.parent;
|
|
58
|
+
while (p !== null) {
|
|
59
|
+
if (p === root)
|
|
60
|
+
return true;
|
|
61
|
+
p = nodes[p].parent;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
nodes.forEach((n, i) => {
|
|
66
|
+
// Chrome's window container also contains @handle and every post link.
|
|
67
|
+
if (!/^(?:container |article ).*@\w+/.test(n.label) ||
|
|
68
|
+
/\bURL:/.test(n.label))
|
|
69
|
+
return;
|
|
70
|
+
if (roots.some((root) => within(n, root)))
|
|
71
|
+
return;
|
|
72
|
+
if (descendants(i).some((d) => /^link /.test(d.label) && /x\.com\/\w+\/status\/\d+/.test(d.label)))
|
|
73
|
+
roots.push(i);
|
|
74
|
+
});
|
|
75
|
+
const peopleTimeline = nodes.findIndex((n) => /^container 时间线:(认证关注者|关注者|正在关注)$/.test(n.label));
|
|
76
|
+
const peopleRoots = peopleTimeline < 0
|
|
77
|
+
? []
|
|
78
|
+
: nodes
|
|
79
|
+
.map((n, i) => ({ n, i }))
|
|
80
|
+
.filter(({ n, i }) => within(n, peopleTimeline) &&
|
|
81
|
+
/^按钮 .*@\w+(?: |$)/.test(n.label) &&
|
|
82
|
+
descendants(i).some((d) => /^link @\w+, Value: x\.com\/\w+(?:,|$)/.test(d.label)))
|
|
83
|
+
.map(({ i }) => i);
|
|
84
|
+
const warning = /抱歉.*限制|速度限制|请求异常|访问受限|验证码|验证挑战|登录以继续|captcha|rate limit/i;
|
|
85
|
+
const explicitAlert = (n) => {
|
|
86
|
+
let current = n;
|
|
87
|
+
while (current) {
|
|
88
|
+
if (/^(?:alert|警告|对话框|dialog|status|状态)(?: |$)/i.test(current.label))
|
|
89
|
+
return true;
|
|
90
|
+
current = current.parent === null ? null : nodes[current.parent];
|
|
91
|
+
}
|
|
92
|
+
return /^(?:文本输入区(?:域)?|文本区域|文本栏|按钮) /i.test(n.label);
|
|
93
|
+
};
|
|
94
|
+
const alerts = nodes
|
|
95
|
+
.filter((n, i) => warning.test(n.label) &&
|
|
96
|
+
(![...roots, ...peopleRoots].some((root) => i === root || within(n, root)) ||
|
|
97
|
+
explicitAlert(n)) &&
|
|
98
|
+
!descendants(i).some((d) => warning.test(d.label)))
|
|
99
|
+
.map((n) => ({ index: n.index, label: n.label }));
|
|
100
|
+
return {
|
|
101
|
+
nodes,
|
|
102
|
+
descendants,
|
|
103
|
+
evidence,
|
|
104
|
+
navigation,
|
|
105
|
+
roots,
|
|
106
|
+
peopleRoots,
|
|
107
|
+
peopleTimeline,
|
|
108
|
+
within,
|
|
109
|
+
alerts,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { AXTree } from "./readers-ax-tree.js";
|
|
2
|
+
type AXProjection = {
|
|
3
|
+
mode: string;
|
|
4
|
+
page: string | undefined;
|
|
5
|
+
offset: number;
|
|
6
|
+
limit: number;
|
|
7
|
+
targetId: string;
|
|
8
|
+
targetHandle: string;
|
|
9
|
+
unknown: (reason: string) => any;
|
|
10
|
+
alertLines: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function projectAXFilepicker(tree: AXTree, context: AXProjection): any;
|
|
13
|
+
export declare function projectAXMedia(tree: AXTree, context: AXProjection): any;
|
|
14
|
+
export declare function projectAXNavigation(tree: AXTree, context: AXProjection): {
|
|
15
|
+
mode: string;
|
|
16
|
+
page: string | undefined;
|
|
17
|
+
posts: never[];
|
|
18
|
+
evidence: {
|
|
19
|
+
index: any;
|
|
20
|
+
label: any;
|
|
21
|
+
}[];
|
|
22
|
+
alerts: {
|
|
23
|
+
index: any;
|
|
24
|
+
label: any;
|
|
25
|
+
}[];
|
|
26
|
+
pageComplete: boolean;
|
|
27
|
+
needsStateCheck: boolean;
|
|
28
|
+
missing: string[];
|
|
29
|
+
};
|
|
30
|
+
export declare function projectAXPeople(tree: AXTree, context: AXProjection): {
|
|
31
|
+
mode: string;
|
|
32
|
+
page: string | undefined;
|
|
33
|
+
posts: never[];
|
|
34
|
+
people: {
|
|
35
|
+
handle: any;
|
|
36
|
+
evidence: any;
|
|
37
|
+
}[];
|
|
38
|
+
renderedPeople: number;
|
|
39
|
+
matched: number;
|
|
40
|
+
offset: number;
|
|
41
|
+
moreInDOM: boolean;
|
|
42
|
+
alerts: {
|
|
43
|
+
index: any;
|
|
44
|
+
label: any;
|
|
45
|
+
}[];
|
|
46
|
+
navigation: {
|
|
47
|
+
index: any;
|
|
48
|
+
label: any;
|
|
49
|
+
}[] | undefined;
|
|
50
|
+
missing: string[];
|
|
51
|
+
needsStateCheck: boolean;
|
|
52
|
+
pageComplete: boolean;
|
|
53
|
+
};
|
|
54
|
+
export declare function projectAXPosts(tree: AXTree, context: AXProjection): {
|
|
55
|
+
mode: string;
|
|
56
|
+
page: string | undefined;
|
|
57
|
+
renderedArticles: number;
|
|
58
|
+
matched: number;
|
|
59
|
+
offset: number;
|
|
60
|
+
moreInDOM: boolean;
|
|
61
|
+
pageComplete: boolean;
|
|
62
|
+
needsStateCheck: boolean;
|
|
63
|
+
missing: string[];
|
|
64
|
+
posts: {
|
|
65
|
+
timeEvidence: any;
|
|
66
|
+
text: string;
|
|
67
|
+
evidence: any;
|
|
68
|
+
needsExpansion: any;
|
|
69
|
+
missing: (string | false)[];
|
|
70
|
+
datetimePrecision?: string | undefined;
|
|
71
|
+
id: any;
|
|
72
|
+
author: any;
|
|
73
|
+
primary: any;
|
|
74
|
+
datetime: string | null;
|
|
75
|
+
}[];
|
|
76
|
+
alerts: {
|
|
77
|
+
index: any;
|
|
78
|
+
label: any;
|
|
79
|
+
}[];
|
|
80
|
+
navigation: {
|
|
81
|
+
index: any;
|
|
82
|
+
label: any;
|
|
83
|
+
}[] | undefined;
|
|
84
|
+
};
|
|
85
|
+
export declare function projectAXProfile(tree: AXTree, context: AXProjection): any;
|
|
86
|
+
export declare function projectAXComposer(tree: AXTree, context: AXProjection): any;
|
|
87
|
+
export {};
|