@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.
Files changed (113) hide show
  1. package/LICENSE +231 -6
  2. package/README.md +17 -3
  3. package/SOURCE.md +9 -0
  4. package/cli/main.js +232 -188
  5. package/dist/api.d.ts +56 -2
  6. package/dist/api.js +178 -75
  7. package/dist/boundary.js +1 -0
  8. package/dist/browser-common.d.ts +11 -0
  9. package/dist/browser-common.js +34 -18
  10. package/dist/browser-dom-capabilities.d.ts +9 -0
  11. package/dist/browser-dom-capabilities.js +131 -0
  12. package/dist/browser-dom-dependencies.d.ts +16 -0
  13. package/dist/browser-dom-dependencies.js +1 -0
  14. package/dist/browser-dom-editing.d.ts +166 -0
  15. package/dist/browser-dom-editing.js +418 -0
  16. package/dist/browser-dom-navigation.d.ts +18 -0
  17. package/dist/browser-dom-navigation.js +181 -0
  18. package/dist/browser-dom-network.d.ts +1 -0
  19. package/dist/browser-dom-network.js +128 -0
  20. package/dist/browser-dom-submission.d.ts +7 -0
  21. package/dist/browser-dom-submission.js +101 -0
  22. package/dist/browser-dom.d.ts +14 -2
  23. package/dist/browser-dom.js +120 -824
  24. package/dist/browser-native.d.ts +14 -1
  25. package/dist/browser-native.js +180 -143
  26. package/dist/browser.js +6 -2
  27. package/dist/creator-native.d.ts +3 -0
  28. package/dist/creator-native.js +113 -0
  29. package/dist/creator.js +1 -1
  30. package/dist/diagnostics.d.ts +11 -0
  31. package/dist/diagnostics.js +10 -0
  32. package/dist/growth-batch.d.ts +8 -0
  33. package/dist/growth-batch.js +190 -0
  34. package/dist/growth-constants.d.ts +5 -0
  35. package/dist/growth-constants.js +13 -0
  36. package/dist/growth-dependencies.d.ts +33 -0
  37. package/dist/growth-dependencies.js +1 -0
  38. package/dist/growth-execution.d.ts +17 -0
  39. package/dist/growth-execution.js +346 -0
  40. package/dist/growth-parallel.d.ts +8 -0
  41. package/dist/growth-parallel.js +332 -0
  42. package/dist/growth-plan.d.ts +9 -0
  43. package/dist/growth-plan.js +229 -0
  44. package/dist/growth-policy.d.ts +18 -0
  45. package/dist/growth-policy.js +208 -0
  46. package/dist/growth-publishing.d.ts +10 -0
  47. package/dist/growth-publishing.js +266 -0
  48. package/dist/growth-reading.d.ts +8 -0
  49. package/dist/growth-reading.js +157 -0
  50. package/dist/growth-recovery.d.ts +9 -0
  51. package/dist/growth-recovery.js +187 -0
  52. package/dist/growth-state.d.ts +16 -0
  53. package/dist/growth-state.js +220 -0
  54. package/dist/growth-tasks.d.ts +20 -0
  55. package/dist/growth-tasks.js +164 -0
  56. package/dist/growth.d.ts +26 -1
  57. package/dist/growth.js +211 -1908
  58. package/dist/platform-capabilities.d.ts +8 -0
  59. package/dist/platform-capabilities.js +29 -0
  60. package/dist/platform-collections.d.ts +14 -0
  61. package/dist/platform-collections.js +250 -0
  62. package/dist/platform-dependencies.d.ts +43 -0
  63. package/dist/platform-dependencies.js +1 -0
  64. package/dist/platform-diagnostics.d.ts +14 -0
  65. package/dist/platform-diagnostics.js +63 -0
  66. package/dist/platform-observation.d.ts +16 -0
  67. package/dist/platform-observation.js +96 -0
  68. package/dist/platform-rules.d.ts +17 -0
  69. package/dist/platform-rules.js +41 -0
  70. package/dist/platform-targets.d.ts +14 -0
  71. package/dist/platform-targets.js +154 -0
  72. package/dist/platform-writing.d.ts +16 -0
  73. package/dist/platform-writing.js +202 -0
  74. package/dist/platform.d.ts +23 -2
  75. package/dist/platform.js +125 -678
  76. package/dist/readers-actions.d.ts +2 -0
  77. package/dist/readers-actions.js +66 -0
  78. package/dist/readers-ax-host.d.ts +12 -0
  79. package/dist/readers-ax-host.js +134 -0
  80. package/dist/readers-ax-parser.d.ts +1 -0
  81. package/dist/readers-ax-parser.js +69 -0
  82. package/dist/readers-ax-program.d.ts +1 -0
  83. package/dist/readers-ax-program.js +5 -0
  84. package/dist/readers-ax-tree.d.ts +19 -0
  85. package/dist/readers-ax-tree.js +111 -0
  86. package/dist/readers-ax-views.d.ts +87 -0
  87. package/dist/readers-ax-views.js +247 -0
  88. package/dist/readers-cli.d.ts +6 -0
  89. package/dist/readers-cli.js +79 -0
  90. package/dist/readers-composer.d.ts +2 -0
  91. package/dist/readers-composer.js +131 -0
  92. package/dist/readers-navigation.d.ts +2 -0
  93. package/dist/readers-navigation.js +66 -0
  94. package/dist/readers-posts.d.ts +2 -0
  95. package/dist/readers-posts.js +141 -0
  96. package/dist/readers-profile.d.ts +3 -0
  97. package/dist/readers-profile.js +157 -0
  98. package/dist/readers.d.ts +9 -20
  99. package/dist/readers.js +9 -1045
  100. package/dist/results.js +1 -1
  101. package/dist/round-state.js +20 -3
  102. package/dist/store.d.ts +22 -4
  103. package/dist/store.js +173 -151
  104. package/dist/types.d.ts +10 -0
  105. package/dist/types.js +1 -0
  106. package/generated/observation.mjs +3 -2
  107. package/package.json +23 -9
  108. package/skill/SKILL.md +17 -36
  109. package/skill/references/api.md +7 -34
  110. package/skill/references/reads.md +23 -0
  111. package/skill/references/recovery.md +25 -0
  112. package/skill/references/setup.md +23 -0
  113. package/skill/references/writes.md +51 -0
@@ -0,0 +1,247 @@
1
+ export function projectAXFilepicker(tree, context) {
2
+ const { nodes, descendants, evidence, navigation, roots, peopleRoots, peopleTimeline, within, alerts, } = tree;
3
+ const { mode, page, offset, limit, targetId, targetHandle, unknown, alertLines, } = context;
4
+ const dialog = nodes.findIndex((n) => /^(?:对话框|sheet|表单|dialog)(?: |$)/.test(n.label));
5
+ if (dialog < 0)
6
+ return unknown("filepicker-dialog-not-found");
7
+ const items = evidence(descendants(dialog)).filter((n) => /^(?:按钮 (?:\(disabled\) )?(?:打开|Open|前往|Go|取消|Cancel)(?:,|$)|文本栏 |组合框 )/.test(n.label) && !/URL:/.test(n.label));
8
+ const ready = items.some((n) => /^按钮 (?:打开|Open|前往|Go)(?:,|$)/.test(n.label)) || items.some((n) => /^(?:文本栏|组合框) /.test(n.label));
9
+ return {
10
+ mode,
11
+ evidence: items,
12
+ alerts: alertLines,
13
+ pageComplete: false,
14
+ needsStateCheck: !ready,
15
+ missing: ready ? [] : ["filepicker-controls-not-found"],
16
+ };
17
+ }
18
+ export function projectAXMedia(tree, context) {
19
+ const { nodes, descendants, evidence, navigation, roots, peopleRoots, peopleTimeline, within, alerts, } = tree;
20
+ const { mode, page, offset, limit, targetId, targetHandle, unknown, alertLines, } = context;
21
+ if (!/x\.com\/compose\/post\/media(?:[?#]|$)/.test(page ?? ""))
22
+ return unknown("media-route-not-found");
23
+ const web = nodes.findIndex((n) => /^(?:HTML 内容|AXWebArea) .*URL: (?:https?:\/\/)?x\.com\/compose\/post\/media(?:,|$)/.test(n.label));
24
+ if (web < 0)
25
+ return unknown("media-content-not-found");
26
+ const items = evidence(descendants(web));
27
+ return {
28
+ mode,
29
+ page,
30
+ evidence: items,
31
+ alerts: alertLines,
32
+ pageComplete: false,
33
+ needsStateCheck: !items.some((n) => /^按钮 保存(?:,|$)/.test(n.label)),
34
+ missing: [],
35
+ };
36
+ }
37
+ export function projectAXNavigation(tree, context) {
38
+ const { nodes, descendants, evidence, navigation, roots, peopleRoots, peopleTimeline, within, alerts, } = tree;
39
+ const { mode, page, offset, limit, targetId, targetHandle, unknown, alertLines, } = context;
40
+ return {
41
+ mode,
42
+ page,
43
+ posts: [],
44
+ evidence: navigation,
45
+ alerts,
46
+ pageComplete: false,
47
+ needsStateCheck: !navigation.length,
48
+ missing: navigation.length ? [] : ["navigation-not-found"],
49
+ };
50
+ }
51
+ export function projectAXPeople(tree, context) {
52
+ const { nodes, descendants, evidence, navigation, roots, peopleRoots, peopleTimeline, within, alerts, } = tree;
53
+ const { mode, page, offset, limit, targetId, targetHandle, unknown, alertLines, } = context;
54
+ const all = peopleRoots.map((i) => ({
55
+ handle: nodes[i].label.match(/@(\w+)/)[1],
56
+ evidence: evidence(descendants(i)),
57
+ }));
58
+ const matches = targetHandle
59
+ ? all.filter((p) => p.handle.toLowerCase() === targetHandle.toLowerCase())
60
+ : all;
61
+ const missing = peopleTimeline < 0
62
+ ? ["people-timeline-not-found"]
63
+ : !matches.length
64
+ ? ["person-not-found"]
65
+ : targetHandle && matches.length > 1
66
+ ? ["person-ambiguous"]
67
+ : [];
68
+ return {
69
+ mode,
70
+ page,
71
+ posts: [],
72
+ people: missing.length ? [] : matches.slice(offset, offset + limit),
73
+ renderedPeople: all.length,
74
+ matched: matches.length,
75
+ offset,
76
+ moreInDOM: matches.length > offset + limit,
77
+ alerts,
78
+ navigation: missing.length ? navigation : undefined,
79
+ missing,
80
+ needsStateCheck: !!missing.length,
81
+ pageComplete: false,
82
+ };
83
+ }
84
+ export function projectAXPosts(tree, context) {
85
+ const { nodes, descendants, evidence, navigation, roots, peopleRoots, peopleTimeline, within, alerts, } = tree;
86
+ const { mode, page, offset, limit, targetId, targetHandle, unknown, alertLines, } = context;
87
+ const all = roots.map((i) => {
88
+ const postNodes = descendants(i);
89
+ const items = evidence(postNodes);
90
+ const authorHandle = nodes[i].label.match(/@(\w+)/)?.[1];
91
+ const primary = items.find((n) => /^link /.test(n.label) &&
92
+ n.label
93
+ .match(/x\.com\/(\w+)\/status\/\d+(?:[?#][^\s,]*)?(?:,|$)/)?.[1]
94
+ ?.toLowerCase() === authorHandle?.toLowerCase());
95
+ const edited = postNodes.filter((n) => /^link 这是该帖子的最新版本。/.test(n.label) &&
96
+ n.label
97
+ .match(/x\.com\/(\w+)\/status\/\d+\/history(?:,|$)/)?.[1]
98
+ ?.toLowerCase() === authorHandle?.toLowerCase() &&
99
+ !postNodes.some((q) => /^(?:container |article )引用(?: |$)/.test(q.label) &&
100
+ within(n, nodes.indexOf(q))));
101
+ const source = primary ?? (edited.length === 1 ? edited[0] : null);
102
+ const match = source?.label.match(/x\.com\/(\w+)\/status\/(\d+)/);
103
+ const text = postNodes
104
+ .filter((n) => {
105
+ if (!/^(?:文本 |text )/.test(n.label))
106
+ return false;
107
+ let parent = n.parent;
108
+ while (parent !== null && parent !== i) {
109
+ if (/^(?:按钮|button)(?: |$)/.test(nodes[parent].label))
110
+ return false;
111
+ parent = nodes[parent].parent;
112
+ }
113
+ return true;
114
+ })
115
+ .map((n) => n.label.replace(/^(?:文本 |text )/, ""))
116
+ .join("\n");
117
+ const absolute = source?.label.match(/link (上午|下午)?(\d{1,2}):(\d{2}) · (\d{4})年(\d{1,2})月(\d{1,2})日/);
118
+ let datetime = null;
119
+ if (absolute) {
120
+ let hour = Number(absolute[2]);
121
+ if (absolute[1])
122
+ hour = (hour % 12) + (absolute[1] === "下午" ? 12 : 0);
123
+ if (hour < 24 && Number(absolute[3]) < 60)
124
+ datetime = new Date(Date.UTC(Number(absolute[4]), Number(absolute[5]) - 1, Number(absolute[6]), hour - 8, Number(absolute[3]))).toISOString();
125
+ }
126
+ return {
127
+ id: match?.[2] ?? null,
128
+ author: match?.[1] ?? null,
129
+ primary: source?.label ?? null,
130
+ datetime,
131
+ ...(datetime ? { datetimePrecision: "minute" } : {}),
132
+ timeEvidence: source?.label ?? null,
133
+ text,
134
+ evidence: items,
135
+ needsExpansion: /显示更多|Show more/i.test(text) ||
136
+ items.some((n) => /显示更多|Show more/i.test(n.label)),
137
+ missing: [
138
+ !match && "id",
139
+ !text && "text",
140
+ !datetime && "datetime",
141
+ ].filter(Boolean),
142
+ };
143
+ });
144
+ const candidates = targetId ? all.filter((p) => p.id === targetId) : all;
145
+ return {
146
+ mode,
147
+ page,
148
+ renderedArticles: all.length,
149
+ matched: candidates.length,
150
+ offset,
151
+ moreInDOM: candidates.length > offset + limit,
152
+ pageComplete: false,
153
+ needsStateCheck: candidates.length === 0,
154
+ missing: candidates.length ? [] : ["post-not-found"],
155
+ posts: candidates.slice(offset, offset + limit),
156
+ alerts,
157
+ navigation: candidates.length ? undefined : navigation,
158
+ };
159
+ }
160
+ export function projectAXProfile(tree, context) {
161
+ const { nodes, descendants, evidence, navigation, roots, peopleRoots, peopleTimeline, within, alerts, } = tree;
162
+ const { mode, page, offset, limit, targetId, targetHandle, unknown, alertLines, } = context;
163
+ const start = nodes.findIndex((n) => n.label === "container 主页时间线");
164
+ if (start < 0)
165
+ return { ...unknown("profile-not-found"), page, alerts, navigation };
166
+ const main = descendants(start), end = main.findIndex((n) => /container 个人资料时间线/.test(n.label));
167
+ if (end < 0)
168
+ return {
169
+ ...unknown("profile-boundary-not-found"),
170
+ page,
171
+ alerts,
172
+ navigation,
173
+ };
174
+ return {
175
+ mode,
176
+ page,
177
+ posts: [],
178
+ evidence: evidence(main.slice(0, end)),
179
+ alerts,
180
+ pageComplete: false,
181
+ needsStateCheck: false,
182
+ };
183
+ }
184
+ export function projectAXComposer(tree, context) {
185
+ const { nodes, descendants, evidence, navigation, roots, peopleRoots, peopleTimeline, within, alerts, } = tree;
186
+ const { mode, page, offset, limit, targetId, targetHandle, unknown, alertLines, } = context;
187
+ const dialog = nodes.findIndex((n) => /^(?:对话框|dialog)(?: |$)/.test(n.label));
188
+ if (dialog < 0) {
189
+ const web = nodes.findIndex((n) => /^(?:HTML 内容|AXWebArea) .*URL: (?:https?:\/\/)?x\.com\/compose\/post(?:,|$)/.test(n.label));
190
+ if (/x\.com\/compose\/post(?:[?#]|$)/.test(page ?? "") && web >= 0) {
191
+ const items = evidence(descendants(web));
192
+ const editors = items.filter((n) => /^(?:文本输入区(?:域)?|文本区域) .*帖子文本/.test(n.label));
193
+ const submits = items.filter((n) => /^按钮 (?:\(disabled\) )?(?:发帖|Post)(?:,|$)/i.test(n.label));
194
+ if (editors.length === 1 && submits.length === 1)
195
+ return {
196
+ mode,
197
+ page,
198
+ posts: [],
199
+ evidence: items,
200
+ alerts,
201
+ pageComplete: false,
202
+ needsStateCheck: false,
203
+ };
204
+ }
205
+ const editors = nodes.filter((n) => /^(?:文本输入区(?:域)?|文本区域|文本栏) /.test(n.label));
206
+ const submits = nodes.filter((n) => /^按钮 (?:回复|Reply)(?:,|$)/i.test(n.label));
207
+ if (/x\.com\/\w+\/status\/\d+(?:[?#]|$)/.test(page ?? "") &&
208
+ editors.length === 1 &&
209
+ submits.length === 1) {
210
+ return {
211
+ mode,
212
+ page,
213
+ inlineComposer: true,
214
+ posts: [],
215
+ evidence: evidence([...editors, ...submits]),
216
+ alerts,
217
+ pageComplete: false,
218
+ needsStateCheck: false,
219
+ };
220
+ }
221
+ return {
222
+ ...unknown("composer-dialog-not-found"),
223
+ page,
224
+ alerts,
225
+ navigation,
226
+ };
227
+ }
228
+ const items = evidence(descendants(dialog));
229
+ if (!items.some((n) => /^(?:文本输入区(?:域)?|文本区域|文本栏) /.test(n.label)) ||
230
+ !items.some((n) => /^按钮 (?:发帖|发布|回复|Post|Reply)(?:,|$)/i.test(n.label))) {
231
+ return {
232
+ ...unknown("composer-boundary-unconfirmed"),
233
+ page,
234
+ alerts,
235
+ navigation,
236
+ };
237
+ }
238
+ return {
239
+ mode,
240
+ page,
241
+ posts: [],
242
+ evidence: items,
243
+ alerts,
244
+ pageComplete: false,
245
+ needsStateCheck: false,
246
+ };
247
+ }
@@ -0,0 +1,6 @@
1
+ export declare function setupCode(): any;
2
+ export declare function profileCode({ tab, handle }?: any): any;
3
+ export declare function profilesCode(entries: any): any;
4
+ export declare function browserCode({ handle, offset, limit, targetId, fresh, }?: any): any;
5
+ export declare function axCode({ handle, mode, offset, limit, targetId, targetHandle, fresh, }?: any): any;
6
+ export declare function runReaderCLI(): void;
@@ -0,0 +1,79 @@
1
+ import { axParserCode } from "./readers-ax-program.js";
2
+ import { readPosts, changes } from "./readers-posts.js";
3
+ import { readProfile, screenCandidates } from "./readers-profile.js";
4
+ import { readAX } from "./readers-ax-parser.js";
5
+ import { createAXHarness } from "./readers-ax-host.js";
6
+ export function setupCode() {
7
+ return `var xReadPosts = ${readPosts.toString()};\nvar xReadChanges = ${changes.toString()};\nvar xReadProfile = ${readProfile.toString()};\nvar xScreenCandidates = ${screenCandidates.toString()};\nvar xReadAX = ${axParserCode};\nvar xCreateAXHarness = (function(readAX) { return ${createAXHarness.toString()}; })(xReadAX);\nvar xAXSessions = {};\nvar xReadCache = {};\n`;
8
+ }
9
+ export function profileCode({ tab = "xtab", handle } = {}) {
10
+ return profilesCode([{ tab, handle }]);
11
+ }
12
+ export function profilesCode(entries) {
13
+ if (!Array.isArray(entries) || !entries.length || entries.length > 5)
14
+ throw new Error("一次读取1~5位");
15
+ for (const { tab, handle } of entries) {
16
+ if (!/^[A-Za-z_$][\w$]*$/.test(tab ?? ""))
17
+ throw new Error("tab必须是已有Tab变量名");
18
+ if (!/^[A-Za-z0-9_]{1,15}$/.test(handle ?? ""))
19
+ throw new Error("需要已观察的X handle");
20
+ }
21
+ const inputs = entries
22
+ .map(({ tab, handle }) => `{tab:${tab},handle:${JSON.stringify(handle)}}`)
23
+ .join(",");
24
+ return `var xProfiles = [];\nfor (var entry of [${inputs}]) {\n var page = new URL(await entry.tab.url());\n if (page.origin !== 'https://x.com' || page.pathname.toLowerCase() !== '/' + entry.handle.toLowerCase()) { xProfiles.push({handle:entry.handle,blue:null,relationship:'unknown',alerts:['profile-url-mismatch']}); continue; }\n xProfiles.push(await entry.tab.playwright.evaluate(xReadProfile, {handle:entry.handle}));\n}\nnodeRepl.write(xScreenCandidates(xProfiles));\n`;
25
+ }
26
+ export function browserCode({ handle = "xtab", offset = 0, limit = 5, targetId = "", fresh = false, } = {}) {
27
+ if (!/^[A-Za-z_$][\w$]*$/.test(handle))
28
+ throw new Error("handle必须是当前REPL已有Tab变量名");
29
+ if (targetId && !/^\d+$/.test(targetId))
30
+ throw new Error("targetId必须是已观察的数字公开ID");
31
+ if (!Number.isInteger(offset) ||
32
+ offset < 0 ||
33
+ !Number.isInteger(limit) ||
34
+ limit < 1 ||
35
+ limit > 5)
36
+ throw new Error("无效读取窗口");
37
+ return `var xReadResult = await ${handle}.playwright.evaluate(xReadPosts, ${JSON.stringify({ offset, limit, targetId })});\nnodeRepl.write(xReadChanges(xReadResult, xReadCache, await ${handle}.url(), ${fresh}));\n`;
38
+ }
39
+ export function axCode({ handle = "chrome", mode = "posts", offset = 0, limit = 1, targetId = "", targetHandle = "", fresh = false, } = {}) {
40
+ if (!/^[A-Za-z_$][\w$]*$/.test(handle))
41
+ throw new Error("handle必须是已有原生App/Tab变量名");
42
+ // Share validation with the pure parser, without requiring a browser.
43
+ readAX("", { mode, offset, limit, targetId, targetHandle });
44
+ return `if (xAXSessions.${handle}?.app !== ${handle}) xAXSessions.${handle} = {app:${handle}, harness:xCreateAXHarness(${handle})};\nvar xAXResult = await xAXSessions.${handle}.harness.observe(${JSON.stringify({ mode, offset, limit, targetId, targetHandle })});\nnodeRepl.write(xReadChanges(xAXResult, xReadCache, xAXResult.page ?? 'unknown', ${fresh}));\n`;
45
+ }
46
+ export function runReaderCLI() {
47
+ try {
48
+ const args = process.argv.slice(2);
49
+ const value = (flag, fallback) => args.includes(flag) ? args[args.indexOf(flag) + 1] : fallback;
50
+ process.stdout.write(args.includes("--setup")
51
+ ? setupCode()
52
+ : args.includes("--ax")
53
+ ? axCode({
54
+ handle: value("--handle", "chrome"),
55
+ mode: value("--mode", "posts"),
56
+ offset: Number(value("--offset", 0)),
57
+ limit: Number(value("--limit", 1)),
58
+ targetId: value("--id", ""),
59
+ targetHandle: value("--person", ""),
60
+ fresh: args.includes("--fresh"),
61
+ })
62
+ : args.includes("--profile")
63
+ ? profileCode({
64
+ tab: value("--handle", "xtab"),
65
+ handle: value("--profile", ""),
66
+ })
67
+ : browserCode({
68
+ handle: value("--handle", "xtab"),
69
+ offset: Number(value("--offset", 0)),
70
+ limit: Number(value("--limit", 5)),
71
+ targetId: value("--id", ""),
72
+ fresh: args.includes("--fresh"),
73
+ }));
74
+ }
75
+ catch (error) {
76
+ console.error(error.message);
77
+ process.exitCode = 1;
78
+ }
79
+ }
@@ -0,0 +1,2 @@
1
+ export declare function readComposer(): any;
2
+ export declare function readMediaEditor(): any;
@@ -0,0 +1,131 @@
1
+ export function readComposer() {
2
+ const visible = (el) => el.getClientRects().length > 0 && !el.closest('[aria-hidden="true"]');
3
+ const dialogs = Array.from(document.querySelectorAll('[role="dialog"]')).filter(visible);
4
+ const root = dialogs.length === 1
5
+ ? dialogs[0]
6
+ : dialogs.length === 0
7
+ ? document.querySelector("main")
8
+ : null;
9
+ if (location.pathname.startsWith("/compose/post/unsent/")) {
10
+ const text = root?.innerText ?? "";
11
+ return {
12
+ page: location.href,
13
+ unsent: true,
14
+ text: text.slice(0, 8000),
15
+ truncated: text.length > 8000,
16
+ controls: root
17
+ ? Array.from(root.querySelectorAll('button,a,[role="tab"]'))
18
+ .filter(visible)
19
+ .slice(0, 30)
20
+ .map((el) => ({
21
+ label: el.getAttribute("aria-label") || el.innerText,
22
+ href: el.getAttribute("href"),
23
+ }))
24
+ : [],
25
+ needsStateCheck: !root || text.length > 8000,
26
+ };
27
+ }
28
+ const navigation = Array.from(document.querySelectorAll('a[href="/compose/post"],button[data-testid="SideNav_AccountSwitcher_Button"]'))
29
+ .filter(visible)
30
+ .map((el) => ({
31
+ role: el.tagName === "A" ? "link" : "button",
32
+ label: el.getAttribute("aria-label") || el.innerText,
33
+ }));
34
+ if (Array.from(document.querySelectorAll('[role="textbox"],textarea,[contenteditable="true"]')).filter(visible).length !== 1)
35
+ return {
36
+ page: location.href,
37
+ navigation,
38
+ dialog: root &&
39
+ (dialogs.length === 1 || location.pathname === "/compose/post/media")
40
+ ? {
41
+ buttons: Array.from(root.querySelectorAll('button,[role="tab"]'))
42
+ .filter(visible)
43
+ .map((el) => ({
44
+ role: el.getAttribute("role") || "button",
45
+ label: el.getAttribute("aria-label") || el.innerText,
46
+ })),
47
+ fields: Array.from(root.querySelectorAll("textarea,input"))
48
+ .filter(visible)
49
+ .map((el) => ({
50
+ label: el.getAttribute("aria-label"),
51
+ placeholder: el.getAttribute("placeholder"),
52
+ value: el.value,
53
+ })),
54
+ }
55
+ : null,
56
+ needsStateCheck: true,
57
+ missing: ["composer-editor-not-found"],
58
+ editorCandidates: Array.from(document.querySelectorAll('[role="textbox"],textarea,[contenteditable="true"]'))
59
+ .filter(visible)
60
+ .map((el) => ({
61
+ label: el.getAttribute("aria-label"),
62
+ testId: el.getAttribute("data-testid"),
63
+ tag: el.tagName,
64
+ })),
65
+ };
66
+ const editors = Array.from(document.querySelectorAll('[role="textbox"],textarea,[contenteditable="true"]')).filter(visible);
67
+ let scope = editors[0];
68
+ while (scope.parentElement &&
69
+ !Array.from(scope.querySelectorAll("button")).some((el) => visible(el) &&
70
+ /^(发帖|回复|更新)$/.test(el.getAttribute("aria-label") || el.innerText)))
71
+ scope = scope.parentElement;
72
+ if (scope === document.body || scope === document.documentElement)
73
+ return {
74
+ page: location.href,
75
+ needsStateCheck: true,
76
+ missing: ["composer-submit-scope-not-found"],
77
+ };
78
+ return {
79
+ page: location.href,
80
+ navigation,
81
+ editors: editors.map((el) => ({
82
+ label: el.getAttribute("aria-label"),
83
+ testId: el.getAttribute("data-testid"),
84
+ text: el.innerText,
85
+ })),
86
+ buttons: Array.from(scope.querySelectorAll("button"))
87
+ .filter(visible)
88
+ .map((el) => ({
89
+ label: el.getAttribute("aria-label") || el.innerText,
90
+ disabled: el.disabled,
91
+ testId: el.getAttribute("data-testid"),
92
+ ariaDisabled: el.getAttribute("aria-disabled"),
93
+ ariaBusy: el.getAttribute("aria-busy"),
94
+ })),
95
+ images: Array.from(scope.querySelectorAll("img"))
96
+ .filter(visible)
97
+ .map((el) => ({ alt: el.alt })),
98
+ needsStateCheck: false,
99
+ };
100
+ }
101
+ export function readMediaEditor() {
102
+ if (location.pathname !== "/compose/post/media")
103
+ return { needsStateCheck: true, missing: ["media-route-not-found"] };
104
+ const visible = (el) => el.getClientRects().length > 0 && !el.closest('[aria-hidden="true"]');
105
+ const controls = Array.from(document.querySelectorAll('button,[role="tab"]'))
106
+ .filter(visible)
107
+ .map((el) => ({
108
+ role: el.getAttribute("role") || "button",
109
+ label: el.getAttribute("aria-label") || el.innerText,
110
+ disabled: el.disabled === true,
111
+ }));
112
+ const fields = Array.from(document.querySelectorAll('textarea,input,[role="textbox"]'))
113
+ .filter(visible)
114
+ .map((el) => ({
115
+ label: el.getAttribute("aria-label"),
116
+ placeholder: el.getAttribute("placeholder"),
117
+ value: el.value ?? el.innerText,
118
+ }));
119
+ const ready = controls.some((c) => c.role === "button" && c.label === "保存") &&
120
+ controls.some((c) => c.role === "tab" && c.label === "编辑图片描述");
121
+ return {
122
+ page: location.href,
123
+ controls: controls.slice(0, 20),
124
+ fields: fields.slice(0, 5),
125
+ needsStateCheck: !ready || controls.length > 20 || fields.length > 5,
126
+ missing: ready ? [] : ["media-editor-controls-not-ready"],
127
+ alerts: Array.from(document.querySelectorAll('[role="alert"]'))
128
+ .filter(visible)
129
+ .map((el) => el.innerText),
130
+ };
131
+ }
@@ -0,0 +1,2 @@
1
+ export declare function readMenu(): any;
2
+ export declare function readNavigation({ targetLabel }?: any): any;
@@ -0,0 +1,66 @@
1
+ export function readMenu() {
2
+ const visible = (el) => el.getClientRects().length > 0;
3
+ const roots = Array.from(document.querySelectorAll('[role="menu"],[role="dialog"],[data-testid="Dropdown"]')).filter(visible);
4
+ const items = [
5
+ ...new Set(roots.flatMap((root) => Array.from(root.querySelectorAll('a[href],button,[role="menuitem"]')))),
6
+ ].filter(visible);
7
+ return {
8
+ items: items.map((el) => ({
9
+ role: el.getAttribute("role") || (el.tagName === "A" ? "link" : "button"),
10
+ label: el.getAttribute("aria-label") || el.innerText,
11
+ href: el.getAttribute("href"),
12
+ })),
13
+ needsStateCheck: !items.length,
14
+ missing: items.length ? [] : ["menu-not-found"],
15
+ };
16
+ }
17
+ // Bounded visible navigation only. Never include article links as navigation controls.
18
+ export function readNavigation({ targetLabel } = {}) {
19
+ const visible = (el) => el.getClientRects().length > 0 && !el.closest('[aria-hidden="true"]');
20
+ const primary = document.querySelector('[data-testid="primaryColumn"]') ??
21
+ document.querySelector("main");
22
+ const nodes = [
23
+ ...new Set([
24
+ ...Array.from(document.querySelectorAll("nav a[href],nav button")).filter((el) => !el.closest("main")),
25
+ ...Array.from(primary?.querySelectorAll('a[href],button,[role="button"],[role="tab"]') ?? []),
26
+ ...Array.from(document.querySelectorAll('[role="menu"] a[href], [role="menu"] button, [data-testid="Dropdown"] a[href], [data-testid="Dropdown"] button')),
27
+ ]),
28
+ ].filter((el) => visible(el) && !el.closest("article"));
29
+ const controls = nodes.map((el) => ({
30
+ role: el.getAttribute("role") || (el.tagName === "A" ? "link" : "button"),
31
+ label: el.getAttribute("aria-label") || el.innerText.trim(),
32
+ href: el.getAttribute("href"),
33
+ selected: el.getAttribute("aria-selected") === "true",
34
+ disabled: el.disabled === true,
35
+ }));
36
+ // Some folder cards are plain clickable text, with no ARIA role or href.
37
+ // Only the explicitly requested, visible exact label can be promoted to a semantic control.
38
+ if (targetLabel &&
39
+ !controls.some((c) => c.label.split("\n")[0] === targetLabel)) {
40
+ const labels = Array.from(document.querySelectorAll("span,div")).filter((el) => visible(el) &&
41
+ !el.closest("article") &&
42
+ el.innerText.trim() === targetLabel &&
43
+ !Array.from(el.children).some((child) => child.innerText?.trim() === targetLabel));
44
+ controls.push(...labels.map((el) => ({
45
+ role: "text",
46
+ label: el.innerText.trim(),
47
+ href: null,
48
+ disabled: false,
49
+ source: "exact-visible-folder-label",
50
+ })));
51
+ }
52
+ return {
53
+ page: location.href,
54
+ controls: controls.slice(0, 40),
55
+ headings: Array.from(primary?.querySelectorAll('h1,h2,[role="heading"]') ?? [])
56
+ .filter(visible)
57
+ .map((el) => el.innerText.trim())
58
+ .slice(0, 10),
59
+ needsStateCheck: !controls.length || controls.length > 40,
60
+ missing: controls.length > 40
61
+ ? ["navigation-window-overflow"]
62
+ : controls.length
63
+ ? []
64
+ : ["navigation-not-found"],
65
+ };
66
+ }
@@ -0,0 +1,2 @@
1
+ export declare function readPosts({ offset, limit, targetId, }?: any): any;
2
+ export declare function changes(result: any, cache: any, page: any, fresh?: any): any;