@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,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 {};
@@ -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,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;