@nextclaw/x-harness 0.1.0 → 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 (94) hide show
  1. package/README.md +2 -2
  2. package/cli/main.js +23 -11
  3. package/dist/api.js +53 -1
  4. package/dist/browser-dom-capabilities.d.ts +4 -0
  5. package/dist/browser-dom-capabilities.js +110 -0
  6. package/dist/browser-dom-dependencies.d.ts +16 -0
  7. package/dist/browser-dom-editing.d.ts +148 -0
  8. package/dist/browser-dom-editing.js +386 -0
  9. package/dist/browser-dom-navigation.d.ts +9 -0
  10. package/dist/browser-dom-navigation.js +165 -0
  11. package/dist/browser-dom-network.d.ts +1 -0
  12. package/dist/browser-dom-network.js +128 -0
  13. package/dist/browser-dom-submission.d.ts +2 -0
  14. package/dist/browser-dom-submission.js +82 -0
  15. package/dist/browser-dom.d.ts +1 -1
  16. package/dist/browser-dom.js +51 -802
  17. package/dist/browser-native.js +32 -28
  18. package/dist/browser.js +6 -2
  19. package/dist/creator.js +1 -1
  20. package/dist/growth-batch.d.ts +5 -0
  21. package/dist/growth-batch.js +168 -0
  22. package/dist/growth-constants.d.ts +5 -0
  23. package/dist/growth-constants.js +13 -0
  24. package/dist/growth-dependencies.d.ts +33 -0
  25. package/dist/growth-execution.d.ts +11 -0
  26. package/dist/growth-execution.js +315 -0
  27. package/dist/growth-parallel.d.ts +4 -0
  28. package/dist/growth-parallel.js +301 -0
  29. package/dist/growth-plan.d.ts +4 -0
  30. package/dist/growth-plan.js +207 -0
  31. package/dist/growth-policy.d.ts +9 -0
  32. package/dist/growth-policy.js +181 -0
  33. package/dist/growth-publishing.d.ts +5 -0
  34. package/dist/growth-publishing.js +240 -0
  35. package/dist/growth-reading.d.ts +4 -0
  36. package/dist/growth-reading.js +135 -0
  37. package/dist/growth-recovery.d.ts +6 -0
  38. package/dist/growth-recovery.js +158 -0
  39. package/dist/growth-state.d.ts +8 -0
  40. package/dist/growth-state.js +183 -0
  41. package/dist/growth-tasks.d.ts +17 -0
  42. package/dist/growth-tasks.js +101 -0
  43. package/dist/growth.d.ts +1 -0
  44. package/dist/growth.js +122 -1902
  45. package/dist/platform-capabilities.d.ts +4 -0
  46. package/dist/platform-capabilities.js +19 -0
  47. package/dist/platform-collections.d.ts +7 -0
  48. package/dist/platform-collections.js +223 -0
  49. package/dist/platform-dependencies.d.ts +43 -0
  50. package/dist/platform-diagnostics.d.ts +9 -0
  51. package/dist/platform-diagnostics.js +51 -0
  52. package/dist/platform-observation.d.ts +8 -0
  53. package/dist/platform-observation.js +75 -0
  54. package/dist/platform-rules.d.ts +17 -0
  55. package/dist/platform-rules.js +41 -0
  56. package/dist/platform-targets.d.ts +7 -0
  57. package/dist/platform-targets.js +129 -0
  58. package/dist/platform-writing.d.ts +8 -0
  59. package/dist/platform-writing.js +171 -0
  60. package/dist/platform.d.ts +1 -1
  61. package/dist/platform.js +56 -677
  62. package/dist/readers-actions.d.ts +2 -0
  63. package/dist/readers-actions.js +66 -0
  64. package/dist/readers-ax-host.d.ts +2 -0
  65. package/dist/readers-ax-host.js +125 -0
  66. package/dist/readers-ax-parser.d.ts +1 -0
  67. package/dist/readers-ax-parser.js +69 -0
  68. package/dist/readers-ax-program.d.ts +1 -0
  69. package/dist/readers-ax-tree.d.ts +19 -0
  70. package/dist/readers-ax-tree.js +110 -0
  71. package/dist/readers-ax-views.d.ts +87 -0
  72. package/dist/readers-ax-views.js +247 -0
  73. package/dist/readers-cli.d.ts +6 -0
  74. package/dist/readers-composer.d.ts +2 -0
  75. package/dist/readers-composer.js +131 -0
  76. package/dist/readers-navigation.d.ts +2 -0
  77. package/dist/readers-navigation.js +66 -0
  78. package/dist/readers-posts.d.ts +2 -0
  79. package/dist/readers-posts.js +141 -0
  80. package/dist/readers-profile.d.ts +3 -0
  81. package/dist/readers-profile.js +157 -0
  82. package/dist/readers.d.ts +9 -20
  83. package/dist/results.js +1 -1
  84. package/dist/round-state.js +2 -2
  85. package/dist/types.d.ts +10 -0
  86. package/generated/observation.mjs +2 -1
  87. package/package.json +1 -1
  88. package/skill/SKILL.md +17 -36
  89. package/skill/references/api.md +7 -34
  90. package/skill/references/reads.md +23 -0
  91. package/skill/references/recovery.md +25 -0
  92. package/skill/references/setup.md +23 -0
  93. package/skill/references/writes.md +51 -0
  94. package/dist/readers.js +0 -1045
@@ -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;
@@ -0,0 +1,141 @@
1
+ // Self-contained, read-only DOM function for cua_repl's documented evaluate.
2
+ export function readPosts({ offset = 0, limit = 5, targetId = "", } = {}) {
3
+ if (!Number.isInteger(offset) ||
4
+ offset < 0 ||
5
+ !Number.isInteger(limit) ||
6
+ limit < 1 ||
7
+ limit > 5)
8
+ throw new Error("offset>=0;limit为1~5");
9
+ if (targetId && !/^\d+$/.test(targetId))
10
+ throw new Error("targetId必须是数字公开ID");
11
+ const visible = (el) => el.getClientRects().length > 0;
12
+ const primaryTime = (a) => Array.from(a.querySelectorAll("time")).find((t) => visible(t) &&
13
+ /\/status\/\d+(?:$|[/?#])/.test(t.closest("a")?.getAttribute("href") ?? "")) ?? null;
14
+ const all = Array.from(document.querySelectorAll("article")).filter(visible);
15
+ const candidates = targetId
16
+ ? all.filter((a) => {
17
+ const primary = primaryTime(a)?.closest("a")?.getAttribute("href");
18
+ return (primary && new RegExp(`/status/${targetId}(?:$|[/?#])`).test(primary));
19
+ })
20
+ : all;
21
+ const posts = candidates.slice(offset, offset + limit).map((a) => {
22
+ const time = primaryTime(a);
23
+ const primary = time?.closest("a")?.getAttribute("href") ?? null;
24
+ const links = [
25
+ ...new Set(Array.from(a.querySelectorAll("a[href]"))
26
+ .filter(visible)
27
+ .map((l) => l.getAttribute("href"))),
28
+ ];
29
+ const buttons = Array.from(a.querySelectorAll("button"))
30
+ .filter(visible)
31
+ .map((b) => ({
32
+ label: b.getAttribute("aria-label") || b.innerText,
33
+ disabled: b.hasAttribute("disabled"),
34
+ }));
35
+ const text = a.innerText;
36
+ const datetime = time?.getAttribute("datetime") ?? null;
37
+ const id = primary?.match(/\/status\/(\d+)/)?.[1] ?? null;
38
+ const author = primary?.match(/\/([^/]+)\/status\//)?.[1] ?? null;
39
+ const images = Array.from(a.querySelectorAll('[data-testid="tweetPhoto"] img'))
40
+ .filter(visible)
41
+ .map((img) => ({
42
+ alt: img.getAttribute("alt"),
43
+ src: img.getAttribute("src"),
44
+ }));
45
+ const media = Array.from(a.querySelectorAll('[data-testid="videoPlayer"],video'))
46
+ .filter(visible)
47
+ .map((el) => ({
48
+ kind: "video",
49
+ label: el.getAttribute("aria-label"),
50
+ poster: el.getAttribute("poster"),
51
+ requiresViewing: true,
52
+ }));
53
+ return {
54
+ id,
55
+ author,
56
+ datetime,
57
+ primary,
58
+ text,
59
+ links,
60
+ buttons,
61
+ // View/quote counts may be visible links rather than buttons; collect in this same read.
62
+ metrics: [
63
+ ...buttons,
64
+ ...Array.from(a.querySelectorAll('a[href$="/analytics"],a[href$="/quotes"]'))
65
+ .filter(visible)
66
+ .map((link) => ({
67
+ label: link.getAttribute("aria-label") ||
68
+ `${link.getAttribute("href").endsWith("/analytics") ? "观看" : "引用"} ${link.innerText}`,
69
+ })),
70
+ ],
71
+ images,
72
+ media,
73
+ needsExpansion: /显示更多|Show more/i.test(text),
74
+ missing: [
75
+ !id && "id",
76
+ !author && "author",
77
+ !datetime && "datetime",
78
+ !text && "text",
79
+ ].filter(Boolean),
80
+ };
81
+ });
82
+ const versionLinks = Array.from(document.querySelectorAll("a[href]"))
83
+ .filter(visible)
84
+ .filter((a) => /最新版本|查看最新帖子|Latest version|View latest/i.test(a.innerText))
85
+ .slice(0, 5)
86
+ .map((a) => ({ label: a.innerText, href: a.getAttribute("href") }));
87
+ const emptyEvidence = all.length === 0
88
+ ? [
89
+ ...new Set(Array.from(document.querySelectorAll("main span"))
90
+ .filter(visible)
91
+ .map((e) => e.innerText?.trim())
92
+ .filter((text) => text &&
93
+ text.length <= 400 &&
94
+ !text.includes("\n") &&
95
+ /^(?:没有.*结果|未找到.*结果|No results for)/i.test(text))),
96
+ ].slice(0, 2)
97
+ : [];
98
+ return {
99
+ renderedArticles: all.length,
100
+ matched: candidates.length,
101
+ offset,
102
+ moreInDOM: candidates.length > offset + posts.length,
103
+ pageComplete: false,
104
+ needsStateCheck: all.length === 0 && emptyEvidence.length === 0,
105
+ posts,
106
+ versionLinks,
107
+ emptyEvidence,
108
+ alerts: Array.from(document.querySelectorAll('[role="alert"]'))
109
+ .filter(visible)
110
+ .map((e) => e.innerText),
111
+ };
112
+ }
113
+ export function changes(result, cache, page, fresh = false) {
114
+ const unchangedIds = [];
115
+ const posts = result.posts.filter((post) => {
116
+ if (!post.id)
117
+ return true;
118
+ const key = `${page}|${post.id}`;
119
+ const value = JSON.stringify(post);
120
+ const changed = cache[key] !== value;
121
+ cache[key] = value;
122
+ if (!fresh && !changed)
123
+ unchangedIds.push(post.id);
124
+ return fresh || changed;
125
+ });
126
+ const outputChars = JSON.stringify({ ...result, posts, unchangedIds }).length;
127
+ const optimizationHint = outputChars > 8000
128
+ ? posts.length > 1
129
+ ? "缩小读取窗口或按已观察ID定向读取;保留完整正文"
130
+ : "单篇证据本身长,保留全文,不截断"
131
+ : null;
132
+ return {
133
+ ...result,
134
+ posts,
135
+ unchanged: result.posts.length - posts.length,
136
+ unchangedIds,
137
+ fresh,
138
+ outputChars,
139
+ optimizationHint,
140
+ };
141
+ }
@@ -0,0 +1,3 @@
1
+ export declare function readProfile({ handle }?: any): any;
2
+ export declare function readPeople({ offset, limit }?: any): any;
3
+ export declare function screenCandidates(profiles: any, { minRatio }?: any): any;