@nextclaw/x-harness 0.0.0-stage → 0.1.1

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.
Files changed (110) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +41 -2
  3. package/cli/main.js +219 -0
  4. package/dist/api.d.ts +10 -0
  5. package/dist/api.js +308 -0
  6. package/dist/boundary.d.ts +2 -0
  7. package/dist/browser-common.d.ts +4 -0
  8. package/dist/browser-common.js +29 -0
  9. package/dist/browser-dom-capabilities.d.ts +4 -0
  10. package/dist/browser-dom-capabilities.js +110 -0
  11. package/dist/browser-dom-dependencies.d.ts +16 -0
  12. package/dist/browser-dom-editing.d.ts +148 -0
  13. package/dist/browser-dom-editing.js +386 -0
  14. package/dist/browser-dom-navigation.d.ts +9 -0
  15. package/dist/browser-dom-navigation.js +165 -0
  16. package/dist/browser-dom-network.d.ts +1 -0
  17. package/dist/browser-dom-network.js +128 -0
  18. package/dist/browser-dom-submission.d.ts +2 -0
  19. package/dist/browser-dom-submission.js +82 -0
  20. package/dist/browser-dom.d.ts +2 -0
  21. package/dist/browser-dom.js +103 -0
  22. package/dist/browser-native.d.ts +1 -0
  23. package/dist/browser-native.js +215 -0
  24. package/dist/browser.d.ts +4 -0
  25. package/dist/browser.js +147 -0
  26. package/dist/creator.d.ts +29 -0
  27. package/dist/creator.js +164 -0
  28. package/dist/growth-batch.d.ts +5 -0
  29. package/dist/growth-batch.js +168 -0
  30. package/dist/growth-constants.d.ts +5 -0
  31. package/dist/growth-constants.js +13 -0
  32. package/dist/growth-dependencies.d.ts +33 -0
  33. package/dist/growth-execution.d.ts +11 -0
  34. package/dist/growth-execution.js +315 -0
  35. package/dist/growth-parallel.d.ts +4 -0
  36. package/dist/growth-parallel.js +301 -0
  37. package/dist/growth-plan.d.ts +4 -0
  38. package/dist/growth-plan.js +207 -0
  39. package/dist/growth-policy.d.ts +9 -0
  40. package/dist/growth-policy.js +181 -0
  41. package/dist/growth-publishing.d.ts +5 -0
  42. package/dist/growth-publishing.js +240 -0
  43. package/dist/growth-reading.d.ts +4 -0
  44. package/dist/growth-reading.js +135 -0
  45. package/dist/growth-recovery.d.ts +6 -0
  46. package/dist/growth-recovery.js +158 -0
  47. package/dist/growth-state.d.ts +8 -0
  48. package/dist/growth-state.js +183 -0
  49. package/dist/growth-tasks.d.ts +17 -0
  50. package/dist/growth-tasks.js +101 -0
  51. package/dist/growth.d.ts +2 -0
  52. package/dist/growth.js +138 -0
  53. package/dist/index.d.ts +4 -0
  54. package/dist/index.js +22 -0
  55. package/dist/observation.d.ts +1 -0
  56. package/dist/observation.js +2 -0
  57. package/dist/platform-capabilities.d.ts +4 -0
  58. package/dist/platform-capabilities.js +19 -0
  59. package/dist/platform-collections.d.ts +7 -0
  60. package/dist/platform-collections.js +223 -0
  61. package/dist/platform-dependencies.d.ts +43 -0
  62. package/dist/platform-diagnostics.d.ts +9 -0
  63. package/dist/platform-diagnostics.js +51 -0
  64. package/dist/platform-observation.d.ts +8 -0
  65. package/dist/platform-observation.js +75 -0
  66. package/dist/platform-rules.d.ts +17 -0
  67. package/dist/platform-rules.js +41 -0
  68. package/dist/platform-targets.d.ts +7 -0
  69. package/dist/platform-targets.js +129 -0
  70. package/dist/platform-writing.d.ts +8 -0
  71. package/dist/platform-writing.js +171 -0
  72. package/dist/platform.d.ts +2 -0
  73. package/dist/platform.js +60 -0
  74. package/dist/readers-actions.d.ts +2 -0
  75. package/dist/readers-actions.js +66 -0
  76. package/dist/readers-ax-host.d.ts +2 -0
  77. package/dist/readers-ax-host.js +125 -0
  78. package/dist/readers-ax-parser.d.ts +1 -0
  79. package/dist/readers-ax-parser.js +69 -0
  80. package/dist/readers-ax-program.d.ts +1 -0
  81. package/dist/readers-ax-tree.d.ts +19 -0
  82. package/dist/readers-ax-tree.js +110 -0
  83. package/dist/readers-ax-views.d.ts +87 -0
  84. package/dist/readers-ax-views.js +247 -0
  85. package/dist/readers-cli.d.ts +6 -0
  86. package/dist/readers-composer.d.ts +2 -0
  87. package/dist/readers-composer.js +131 -0
  88. package/dist/readers-navigation.d.ts +2 -0
  89. package/dist/readers-navigation.js +66 -0
  90. package/dist/readers-posts.d.ts +2 -0
  91. package/dist/readers-posts.js +141 -0
  92. package/dist/readers-profile.d.ts +3 -0
  93. package/dist/readers-profile.js +157 -0
  94. package/dist/readers.d.ts +9 -0
  95. package/dist/results.d.ts +6 -0
  96. package/dist/results.js +233 -0
  97. package/dist/round-state.d.ts +3 -0
  98. package/dist/round-state.js +155 -0
  99. package/dist/store.d.ts +30 -0
  100. package/dist/store.js +175 -0
  101. package/dist/types.d.ts +407 -0
  102. package/generated/observation.d.mts +1 -0
  103. package/generated/observation.mjs +515 -0
  104. package/package.json +29 -3
  105. package/skill/SKILL.md +33 -0
  106. package/skill/references/api.md +10 -0
  107. package/skill/references/reads.md +23 -0
  108. package/skill/references/recovery.md +25 -0
  109. package/skill/references/setup.md +23 -0
  110. package/skill/references/writes.md +51 -0
@@ -0,0 +1,171 @@
1
+ import { xURL, sameHandle, sameText, samePublishedText, } from "./platform-rules.js";
2
+ export function createPlatformWriting(dependencies) {
3
+ const { observed, browser, followStage, readPerson, open, clock, readPost } = dependencies;
4
+ async function compose(action) {
5
+ let value = await observed({ view: "composer" });
6
+ if (value.needsStateCheck ||
7
+ value.alerts?.length ||
8
+ typeof value.editorText !== "string" ||
9
+ value.editorText.trim() !== "" ||
10
+ (action.kind === "reply" && value.replyToId !== action.postId))
11
+ throw new Error("编辑器不为空或回复上下文未绑定");
12
+ if (action.assets?.length)
13
+ await browser.act({ operation: "attachAssets", assets: action.assets });
14
+ await browser.act({ operation: "fillComposer", text: action.text });
15
+ value = await observed({ view: "composer" });
16
+ if (!sameText(value.editorText, action.text) ||
17
+ value.needsStateCheck ||
18
+ value.alerts?.length ||
19
+ (action.kind === "reply" && value.replyToId !== action.postId) ||
20
+ (action.accountHandle &&
21
+ !sameHandle(value.accountHandle, action.accountHandle)))
22
+ throw new Error(`提交前账号/正文/目标未核实 ${JSON.stringify({ textMatches: sameText(value.editorText, action.text), needsStateCheck: value.needsStateCheck, alerts: value.alerts, accountHandle: value.accountHandle, replyToId: value.replyToId })}`);
23
+ return browser.act({
24
+ operation: action.kind === "reply" ? "submitReply" : "submitPost",
25
+ beforeSubmit: action.beforeSubmit,
26
+ ...(action.activation ? { activation: action.activation } : {}),
27
+ });
28
+ }
29
+ async function findSubmission(action) {
30
+ if (action.kind === "follow") {
31
+ let value = await followStage(action, "verify-read", () => readPerson(action));
32
+ if (!value.needsStateCheck) {
33
+ const absent = browser.capabilities?.("refreshPerson") ?? [
34
+ "refreshPerson",
35
+ ];
36
+ if (absent.length)
37
+ return {
38
+ ...value,
39
+ verified: false,
40
+ missing: absent,
41
+ needsStateCheck: true,
42
+ };
43
+ await followStage(action, "verify-refresh", () => browser.act({
44
+ operation: "refreshPerson",
45
+ handle: action.handle,
46
+ url: action.url,
47
+ evidenceRef: action.evidenceRef,
48
+ }));
49
+ value = await followStage(action, "verify-after-refresh", () => readPerson(action));
50
+ }
51
+ return {
52
+ ...value,
53
+ verified: !value.needsStateCheck &&
54
+ !value.alerts?.length &&
55
+ value.relationship === "following" &&
56
+ (!action.accountHandle ||
57
+ sameHandle(value.accountHandle, action.accountHandle)),
58
+ publicId: null,
59
+ };
60
+ }
61
+ // A bounded window cannot prove absence. Reply receipts use the observed target conversation.
62
+ const observedReceipt = ["reply", "original"].includes(action.kind) &&
63
+ action.observedPublicURL &&
64
+ action.observedPublicEvidenceRef;
65
+ const receiptPath = observedReceipt
66
+ ? xURL(action.observedPublicURL).pathname.split("/")
67
+ : [];
68
+ if (observedReceipt &&
69
+ (receiptPath.length !== 4 ||
70
+ !sameHandle(receiptPath[1], action.accountHandle) ||
71
+ receiptPath[2] !== "status" ||
72
+ !/^[0-9]+$/.test(receiptPath[3])))
73
+ throw new Error("已观察回复回执须属于原账号");
74
+ const receiptTarget = observedReceipt
75
+ ? {
76
+ url: action.observedPublicURL,
77
+ evidenceRef: action.observedPublicEvidenceRef,
78
+ }
79
+ : action.kind === "reply" && action.url && action.evidenceRef
80
+ ? { url: action.url, evidenceRef: action.evidenceRef }
81
+ : { url: action.accountURL, evidenceRef: action.accountEvidenceRef };
82
+ let value = await open(receiptTarget, { view: "posts", limit: 5 });
83
+ if (!(action.kind === "original" && observedReceipt) &&
84
+ !value.needsStateCheck &&
85
+ !(browser.capabilities?.("refreshPosts") ?? ["refreshPosts"]).length) {
86
+ await browser.act({
87
+ operation: "refreshPosts",
88
+ url: receiptTarget.url,
89
+ evidenceRef: receiptTarget.evidenceRef,
90
+ });
91
+ value = await observed({ view: "posts", limit: 5 });
92
+ }
93
+ const matchesContent = (post) => sameHandle(post.author, action.accountHandle) &&
94
+ samePublishedText(post.body, action.text) &&
95
+ /^\d+$/.test(post.id ?? "") &&
96
+ (!action.assets?.length ||
97
+ action.assets.every((asset) => post.images?.some((image) => image.alt === asset.alt))) &&
98
+ !(action.beforeIds ?? []).includes(post.id) &&
99
+ Number.isFinite(Date.parse(post.datetime)) &&
100
+ Date.parse(post.datetime) >= Date.parse(action.intentAt) &&
101
+ Date.parse(post.datetime) <= clock() + 5000 &&
102
+ !post.needsExpansion;
103
+ let matches = (value.posts ?? []).filter(matchesContent);
104
+ if (action.kind === "reply" &&
105
+ matches.length === 1 &&
106
+ matches[0].replyToId == null &&
107
+ matches[0].primary) {
108
+ // The candidate URL comes from this fresh public observation, not from a guessed ID path.
109
+ const candidate = matches[0];
110
+ const url = xURL(new URL(candidate.primary, xURL(value.page).origin).href);
111
+ if (url.pathname.toLowerCase() ===
112
+ `/${action.accountHandle}/status/${candidate.id}`.toLowerCase()) {
113
+ value = await open({
114
+ url: url.href,
115
+ evidenceRef: {
116
+ page: value.page,
117
+ observedAt: value.observedAt,
118
+ publicId: candidate.id,
119
+ primary: candidate.primary,
120
+ },
121
+ }, {
122
+ view: "posts",
123
+ targetId: candidate.id,
124
+ replyTargetId: action.postId,
125
+ });
126
+ matches = (value.posts ?? []).filter(matchesContent);
127
+ }
128
+ }
129
+ if (action.kind === "reply")
130
+ matches = matches.filter((post) => post.replyToId === action.postId);
131
+ return {
132
+ ...value,
133
+ verified: !value.needsStateCheck && !value.alerts?.length && matches.length === 1,
134
+ publicId: matches.length === 1 ? matches[0].id : null,
135
+ missing: matches.length === 1 ? [] : ["unique-public-submission-unconfirmed"],
136
+ };
137
+ }
138
+ async function submitFollow(action) {
139
+ const value = await followStage(action, "submit-preflight-read", () => readPerson(action));
140
+ if (value.needsStateCheck ||
141
+ value.alerts?.length ||
142
+ !["not-following", "follows-you"].includes(value.relationship) ||
143
+ (action.accountHandle &&
144
+ !sameHandle(value.accountHandle, action.accountHandle)))
145
+ throw new Error("fresh账号/人物关系不允许关注");
146
+ await followStage(action, "submit-guard", () => action.beforeSubmit?.());
147
+ // A returned click is not a receipt from X; durable success still requires fresh verification.
148
+ const clicked = await followStage(action, "click", () => browser.act({ operation: "follow", handle: action.handle }));
149
+ try {
150
+ await followStage(action, "click-ui-observation", () => observed({ view: "person", handle: action.handle }));
151
+ }
152
+ catch {
153
+ /* The click may have committed; retain diagnostic failure and use the original fresh receipt path. */
154
+ }
155
+ return clicked;
156
+ }
157
+ async function submitPost(action) {
158
+ await open({ url: action.composeURL, evidenceRef: action.composeEvidenceRef }, { view: "composer" });
159
+ return compose(action);
160
+ }
161
+ async function submitReply(action) {
162
+ const value = await readPost(action);
163
+ if (value.needsStateCheck ||
164
+ value.alerts?.length ||
165
+ !(value.posts ?? []).some((p) => p.id === action.postId))
166
+ throw new Error("回复主帖身份未核实");
167
+ await browser.act({ operation: "openReply", postId: action.postId });
168
+ return compose(action);
169
+ }
170
+ return { compose, findSubmission, submitFollow, submitPost, submitReply };
171
+ }
@@ -0,0 +1,2 @@
1
+ export { xURL } from "./platform-rules.js";
2
+ export declare function createXPlatform(browser: any, { clock }?: any): any;
@@ -0,0 +1,60 @@
1
+ import { createPlatformObservation } from "./platform-observation.js";
2
+ import { createPlatformTargets } from "./platform-targets.js";
3
+ import { createPlatformCollections } from "./platform-collections.js";
4
+ import { createPlatformDiagnostics } from "./platform-diagnostics.js";
5
+ import { createPlatformWriting } from "./platform-writing.js";
6
+ import { createPlatformCapabilities } from "./platform-capabilities.js";
7
+ export { xURL } from "./platform-rules.js";
8
+ export function createXPlatform(browser, { clock = () => Date.now() } = {}) {
9
+ const createPlatformObservationResult = createPlatformObservation({
10
+ browser,
11
+ clock,
12
+ });
13
+ const { missing, observed, open, ready, choose } = createPlatformObservationResult;
14
+ const createPlatformTargetsResult = createPlatformTargets({
15
+ observed,
16
+ open,
17
+ browser,
18
+ ready,
19
+ });
20
+ const { readAccount, readFeedback, readPerson, readPost } = createPlatformTargetsResult;
21
+ const createPlatformCollectionsResult = createPlatformCollections({
22
+ ready,
23
+ observed,
24
+ browser,
25
+ choose,
26
+ });
27
+ const { collectSources, readCreator, readBookmarks, searchSources } = createPlatformCollectionsResult;
28
+ const createPlatformDiagnosticsResult = createPlatformDiagnostics({ clock });
29
+ const { followStage, captureProof } = createPlatformDiagnosticsResult;
30
+ const createPlatformWritingResult = createPlatformWriting({
31
+ observed,
32
+ browser,
33
+ followStage,
34
+ readPerson,
35
+ open,
36
+ clock,
37
+ readPost,
38
+ });
39
+ const { compose, findSubmission, submitFollow, submitPost, submitReply } = createPlatformWritingResult;
40
+ const createPlatformCapabilitiesResult = createPlatformCapabilities({
41
+ missing,
42
+ browser,
43
+ });
44
+ const { preflight } = createPlatformCapabilitiesResult;
45
+ return {
46
+ preflight,
47
+ readCreator,
48
+ readAccount,
49
+ readBookmarks,
50
+ searchSources,
51
+ readFeedback,
52
+ readPerson,
53
+ readPost,
54
+ findSubmission,
55
+ submitFollow,
56
+ submitPost,
57
+ submitReply,
58
+ captureProof,
59
+ };
60
+ }
@@ -0,0 +1,2 @@
1
+ export declare function attachImage(tab: any, { path }?: any): Promise<any>;
2
+ export declare function openReply(tab: any, { postId }?: any): Promise<any>;
@@ -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,2 @@
1
+ export declare function createAXTabHarness(tab: any): any;
2
+ export declare function createAXHarness(app: any): any;
@@ -0,0 +1,125 @@
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 function createAXHarness(app) {
15
+ let controls = new Map(), readable = false, busy = false;
16
+ const requireMethods = (methods) => {
17
+ const missing = methods.filter((name) => typeof app?.[name] !== "function");
18
+ if (missing.length) {
19
+ controls.clear();
20
+ readable = false;
21
+ throw new Error(`harness能力不足:${missing.join("/")};先补原入口并验证,未执行操作`);
22
+ }
23
+ };
24
+ const indexes = (value) => {
25
+ if (!value || typeof value !== "object")
26
+ return;
27
+ if (Number.isInteger(value.index) && typeof value.label === "string")
28
+ controls.set(value.index, value.label);
29
+ for (const child of Object.values(value))
30
+ indexes(child);
31
+ };
32
+ const observe = async (options = {}) => {
33
+ controls.clear();
34
+ readable = false;
35
+ readAX("", options);
36
+ requireMethods(["getAXState"]);
37
+ const result = readAX(await app.getAXState({ emit: false, disableDiffing: true }), options);
38
+ indexes(result);
39
+ readable = true;
40
+ return result;
41
+ };
42
+ return {
43
+ async observe(options = {}) {
44
+ if (busy)
45
+ throw new Error("AX操作进行中,禁止并发读取");
46
+ busy = true;
47
+ try {
48
+ return await observe(options);
49
+ }
50
+ finally {
51
+ busy = false;
52
+ }
53
+ },
54
+ async act({ index, operation = "click", value, read = {}, } = {}) {
55
+ if (busy)
56
+ throw new Error("AX操作进行中,禁止并发写入");
57
+ if (!readable || !controls.has(index))
58
+ throw new Error("必须先observe;index须来自最新定向证据");
59
+ if (!["click", "fill", "paste", "pressKey", "scroll", "secondary"].includes(operation))
60
+ throw new Error("未封装的AX操作,先补能力并验证");
61
+ // Validate before any side effect; a malformed follow-up read must never submit.
62
+ readAX("", read);
63
+ if (["fill", "paste", "pressKey", "secondary"].includes(operation) &&
64
+ typeof value !== "string")
65
+ throw new Error("AX操作value须为字符串");
66
+ if (["fill", "paste", "pressKey"].includes(operation) &&
67
+ !/^(?:文本输入区(?:域)?|文本区域|文本栏|组合框) /.test(controls.get(index)))
68
+ throw new Error("键盘操作须定位最新证据中的编辑控件");
69
+ if (operation === "secondary" &&
70
+ !controls.get(index).includes(`Secondary Actions: ${value}`))
71
+ throw new Error("未观察到该辅助动作");
72
+ if (operation === "scroll" &&
73
+ !["up", "down", "left", "right"].includes(value))
74
+ throw new Error("无效滚动方向");
75
+ const methods = operation === "fill"
76
+ ? ["click", "pressKey", "paste"]
77
+ : ["paste", "pressKey"].includes(operation)
78
+ ? ["click", operation]
79
+ : [
80
+ operation === "secondary"
81
+ ? "performSecondaryAction"
82
+ : operation,
83
+ ];
84
+ requireMethods([...methods, "getAXState"]);
85
+ busy = true;
86
+ controls.clear();
87
+ readable = false;
88
+ try {
89
+ if (operation === "click")
90
+ await app.click(index);
91
+ else if (["fill", "paste", "pressKey"].includes(operation)) {
92
+ await app.click(index);
93
+ if (operation === "fill") {
94
+ await app.pressKey("super+a");
95
+ await app.paste(value);
96
+ }
97
+ else
98
+ await app[operation](value);
99
+ }
100
+ else if (operation === "scroll")
101
+ await app.scroll(index, value, 1);
102
+ else
103
+ await app.performSecondaryAction(index, value);
104
+ return await observe(read);
105
+ }
106
+ finally {
107
+ busy = false;
108
+ }
109
+ },
110
+ async screenshot() {
111
+ if (busy)
112
+ throw new Error("AX操作进行中,禁止并发截图");
113
+ requireMethods(["getScreenshot"]);
114
+ busy = true;
115
+ controls.clear();
116
+ readable = false;
117
+ try {
118
+ return await app.getScreenshot({ emit: false });
119
+ }
120
+ finally {
121
+ busy = false;
122
+ }
123
+ },
124
+ };
125
+ }
@@ -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,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,110 @@
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
+ /^标签 \((?:selected|settable)/.test(n.label) ||
50
+ /^(?:tab|标签)(?: \([^)]*\))? (?:帖子|回复|转帖|媒体|文章|Posts|Replies|Reposts|Media|Articles)(?:,|$)/.test(n.label) ||
51
+ /^组合框 (?:查询词条|Search)(?:,|$)/.test(n.label) ||
52
+ /^link (?:主页|搜索和发现|通知|个人资料), Value: x\.com\//.test(n.label))
53
+ .map(({ index, label }) => ({ index, label }));
54
+ const roots = [];
55
+ const within = (node, root) => {
56
+ let p = node.parent;
57
+ while (p !== null) {
58
+ if (p === root)
59
+ return true;
60
+ p = nodes[p].parent;
61
+ }
62
+ return false;
63
+ };
64
+ nodes.forEach((n, i) => {
65
+ // Chrome's window container also contains @handle and every post link.
66
+ if (!/^(?:container |article ).*@\w+/.test(n.label) ||
67
+ /\bURL:/.test(n.label))
68
+ return;
69
+ if (roots.some((root) => within(n, root)))
70
+ return;
71
+ if (descendants(i).some((d) => /^link /.test(d.label) && /x\.com\/\w+\/status\/\d+/.test(d.label)))
72
+ roots.push(i);
73
+ });
74
+ const peopleTimeline = nodes.findIndex((n) => /^container 时间线:(认证关注者|关注者|正在关注)$/.test(n.label));
75
+ const peopleRoots = peopleTimeline < 0
76
+ ? []
77
+ : nodes
78
+ .map((n, i) => ({ n, i }))
79
+ .filter(({ n, i }) => within(n, peopleTimeline) &&
80
+ /^按钮 .*@\w+(?: |$)/.test(n.label) &&
81
+ descendants(i).some((d) => /^link @\w+, Value: x\.com\/\w+(?:,|$)/.test(d.label)))
82
+ .map(({ i }) => i);
83
+ const warning = /抱歉.*限制|速度限制|请求异常|访问受限|验证码|验证挑战|登录以继续|captcha|rate limit/i;
84
+ const explicitAlert = (n) => {
85
+ let current = n;
86
+ while (current) {
87
+ if (/^(?:alert|警告|对话框|dialog|status|状态)(?: |$)/i.test(current.label))
88
+ return true;
89
+ current = current.parent === null ? null : nodes[current.parent];
90
+ }
91
+ return /^(?:文本输入区(?:域)?|文本区域|文本栏|按钮) /i.test(n.label);
92
+ };
93
+ const alerts = nodes
94
+ .filter((n, i) => warning.test(n.label) &&
95
+ (![...roots, ...peopleRoots].some((root) => i === root || within(n, root)) ||
96
+ explicitAlert(n)) &&
97
+ !descendants(i).some((d) => warning.test(d.label)))
98
+ .map((n) => ({ index: n.index, label: n.label }));
99
+ return {
100
+ nodes,
101
+ descendants,
102
+ evidence,
103
+ navigation,
104
+ roots,
105
+ peopleRoots,
106
+ peopleTimeline,
107
+ within,
108
+ alerts,
109
+ };
110
+ }