@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,166 @@
1
+ import type { DOMDependencies } from "./browser-dom-dependencies.js";
2
+ export declare class DOMEditing {
3
+ #private;
4
+ dismissMenu: (command: any) => Promise<{
5
+ completed: boolean;
6
+ loc?: undefined;
7
+ submittedEditor?: undefined;
8
+ } | {
9
+ completed: boolean;
10
+ loc: any;
11
+ submittedEditor: any;
12
+ }>;
13
+ openControl: (command: any) => Promise<{
14
+ completed: boolean;
15
+ loc?: undefined;
16
+ submittedEditor?: undefined;
17
+ } | {
18
+ completed: boolean;
19
+ loc: any;
20
+ submittedEditor: any;
21
+ }>;
22
+ openOwnPostMenu: (command: any) => Promise<{
23
+ completed: boolean;
24
+ loc?: undefined;
25
+ submittedEditor?: undefined;
26
+ } | {
27
+ completed: boolean;
28
+ loc: any;
29
+ submittedEditor: any;
30
+ }>;
31
+ submitEditedPost: (command: any) => Promise<{
32
+ completed: boolean;
33
+ loc?: undefined;
34
+ submittedEditor?: undefined;
35
+ } | {
36
+ completed: boolean;
37
+ loc: any;
38
+ submittedEditor: any;
39
+ }>;
40
+ expandPost: (command: any) => Promise<{
41
+ completed: boolean;
42
+ loc?: undefined;
43
+ submittedEditor?: undefined;
44
+ } | {
45
+ completed: boolean;
46
+ loc: any;
47
+ submittedEditor: any;
48
+ }>;
49
+ nextPosts: (command: any) => Promise<{
50
+ completed: boolean;
51
+ loc?: undefined;
52
+ submittedEditor?: undefined;
53
+ } | {
54
+ completed: boolean;
55
+ loc: any;
56
+ submittedEditor: any;
57
+ }>;
58
+ clearPreparedText: (command: any) => Promise<{
59
+ completed: boolean;
60
+ value: Promise<any>;
61
+ loc?: undefined;
62
+ submittedEditor?: undefined;
63
+ } | {
64
+ completed: boolean;
65
+ loc: any;
66
+ submittedEditor: any;
67
+ value?: undefined;
68
+ }>;
69
+ closePreparedReply: (command: any) => Promise<{
70
+ completed: boolean;
71
+ value: Promise<any>;
72
+ loc?: undefined;
73
+ submittedEditor?: undefined;
74
+ } | {
75
+ completed: boolean;
76
+ loc: any;
77
+ submittedEditor: any;
78
+ value?: undefined;
79
+ }>;
80
+ closePreparedMedia: (command: any) => Promise<{
81
+ completed: boolean;
82
+ value: Promise<any>;
83
+ loc?: undefined;
84
+ submittedEditor?: undefined;
85
+ } | {
86
+ completed: boolean;
87
+ loc: any;
88
+ submittedEditor: any;
89
+ value?: undefined;
90
+ }>;
91
+ removePreparedMedia: (command: any) => Promise<{
92
+ completed: boolean;
93
+ value: Promise<any>;
94
+ loc?: undefined;
95
+ submittedEditor?: undefined;
96
+ } | {
97
+ completed: boolean;
98
+ loc: any;
99
+ submittedEditor: any;
100
+ value?: undefined;
101
+ }>;
102
+ attachAssets: (command: any) => Promise<{
103
+ completed: boolean;
104
+ value: {
105
+ assets: {
106
+ path: any;
107
+ alt: any;
108
+ }[];
109
+ mediaEvidence: any;
110
+ composer: any;
111
+ };
112
+ loc?: undefined;
113
+ submittedEditor?: undefined;
114
+ } | {
115
+ completed: boolean;
116
+ loc: any;
117
+ submittedEditor: any;
118
+ value?: undefined;
119
+ }>;
120
+ follow: (command: any) => Promise<{
121
+ completed: boolean;
122
+ loc: any;
123
+ submittedEditor: any;
124
+ }>;
125
+ openReply: (command: any) => Promise<{
126
+ completed: boolean;
127
+ loc: any;
128
+ submittedEditor: any;
129
+ }>;
130
+ fillComposer: (command: any) => Promise<{
131
+ completed: boolean;
132
+ loc?: undefined;
133
+ submittedEditor?: undefined;
134
+ } | {
135
+ completed: boolean;
136
+ loc: any;
137
+ submittedEditor: any;
138
+ }>;
139
+ submitPost: (command: any) => Promise<{
140
+ completed: boolean;
141
+ loc: any;
142
+ submittedEditor: any;
143
+ }>;
144
+ submitReply: (command: any) => Promise<{
145
+ completed: boolean;
146
+ loc: any;
147
+ submittedEditor: any;
148
+ }>;
149
+ constructor(dependencies: Pick<DOMDependencies, "session" | "unique" | "locator" | "tab" | "observe">);
150
+ private handleDismissMenu;
151
+ private handleOpenControl;
152
+ private handleOpenOwnPostMenu;
153
+ private handleSubmitEditedPost;
154
+ private handleExpandPost;
155
+ private handleNextPosts;
156
+ private handleClearPreparedText;
157
+ private handleClosePreparedReply;
158
+ private handleClosePreparedMedia;
159
+ private handleRemovePreparedMedia;
160
+ private handleAttachAssets;
161
+ private handleFollow;
162
+ private handleOpenReply;
163
+ private handleFillComposer;
164
+ private handleSubmitPost;
165
+ }
166
+ export declare function createDOMEditing(...args: ConstructorParameters<typeof DOMEditing>): DOMEditing;
@@ -0,0 +1,418 @@
1
+ import { attachImage } from "./readers-actions.js";
2
+ export class DOMEditing {
3
+ #session;
4
+ #unique;
5
+ #locator;
6
+ #tab;
7
+ #observe;
8
+ dismissMenu;
9
+ openControl;
10
+ openOwnPostMenu;
11
+ submitEditedPost;
12
+ expandPost;
13
+ nextPosts;
14
+ clearPreparedText;
15
+ closePreparedReply;
16
+ closePreparedMedia;
17
+ removePreparedMedia;
18
+ attachAssets;
19
+ follow;
20
+ openReply;
21
+ fillComposer;
22
+ submitPost;
23
+ submitReply;
24
+ constructor(dependencies) {
25
+ const { session, unique, locator, tab, observe } = dependencies;
26
+ this.#session = session;
27
+ this.#unique = unique;
28
+ this.#locator = locator;
29
+ this.#tab = tab;
30
+ this.#observe = observe;
31
+ this.dismissMenu = this.handleDismissMenu.bind(this);
32
+ this.openControl = this.handleOpenControl.bind(this);
33
+ this.openOwnPostMenu = this.handleOpenOwnPostMenu.bind(this);
34
+ this.submitEditedPost = this.handleSubmitEditedPost.bind(this);
35
+ this.expandPost = this.handleExpandPost.bind(this);
36
+ this.nextPosts = this.handleNextPosts.bind(this);
37
+ this.clearPreparedText = this.handleClearPreparedText.bind(this);
38
+ this.closePreparedReply = this.handleClosePreparedReply.bind(this);
39
+ this.closePreparedMedia = this.handleClosePreparedMedia.bind(this);
40
+ this.removePreparedMedia = this.handleRemovePreparedMedia.bind(this);
41
+ this.attachAssets = this.handleAttachAssets.bind(this);
42
+ this.follow = this.handleFollow.bind(this);
43
+ this.openReply = this.handleOpenReply.bind(this);
44
+ this.fillComposer = this.handleFillComposer.bind(this);
45
+ this.submitPost = this.handleSubmitPost.bind(this);
46
+ this.submitReply = this.handleSubmitPost.bind(this);
47
+ }
48
+ async handleDismissMenu(command) {
49
+ const { query, result } = this.#session.current;
50
+ let loc, submittedEditor;
51
+ if (query.view !== "menu" ||
52
+ result.needsStateCheck ||
53
+ !result.items?.length)
54
+ throw new Error("关闭菜单需已有菜单证据");
55
+ const item = result.items[0];
56
+ this.#session.current = null;
57
+ await (await this.#unique(this.#locator(item.role, item.label))).press("Escape");
58
+ return { completed: true };
59
+ return { completed: false, loc, submittedEditor };
60
+ }
61
+ async handleOpenControl(command) {
62
+ const { query, result } = this.#session.current;
63
+ let loc, submittedEditor;
64
+ if (!["navigation", "menu"].includes(query.view) || result.needsStateCheck)
65
+ throw new Error("导航控件须先唯一观察");
66
+ const matches = (result.controls ?? result.items ?? []).filter((c) => c.role === command.role &&
67
+ c.label === command.label &&
68
+ c.href === command.href &&
69
+ !c.disabled);
70
+ if (matches.length !== 1)
71
+ throw new Error("导航控件未唯一观察");
72
+ this.#session.current = null;
73
+ await (await this.#unique(command.role === "text"
74
+ ? this.#tab.playwright.getByText(command.label, { exact: true })
75
+ : this.#locator(command.role, command.label))).click();
76
+ if (command.href?.startsWith("/"))
77
+ await this.#tab.playwright.waitForURL(`https://x.com${command.href}`, {
78
+ timeoutMs: 5000,
79
+ });
80
+ return { completed: true };
81
+ return { completed: false, loc, submittedEditor };
82
+ }
83
+ async handleOpenOwnPostMenu(command) {
84
+ const { query, result } = this.#session.current;
85
+ let loc, submittedEditor;
86
+ const post = result.posts?.[0];
87
+ if (query.view !== "posts" ||
88
+ result.needsStateCheck ||
89
+ result.posts?.length !== 1 ||
90
+ post.id !== command.postId ||
91
+ post.author?.toLowerCase() !== result.accountHandle?.toLowerCase() ||
92
+ post.primary?.match(/\/status\/(\d+)$/)?.[1] !== command.postId)
93
+ throw new Error("编辑需唯一本人原帖");
94
+ const more = post.buttons.filter((b) => /^(更多|More)$/.test(b.label) && !b.disabled);
95
+ if (more.length !== 1)
96
+ throw new Error("原帖更多按钮未唯一观察");
97
+ const article = this.#locator("article").filter({
98
+ has: this.#tab.playwright.locator(`a[href=${JSON.stringify(post.primary)}]`),
99
+ visible: true,
100
+ });
101
+ this.#session.editBinding = {
102
+ postId: post.id,
103
+ accountHandle: result.accountHandle,
104
+ body: post.body,
105
+ };
106
+ this.#session.current = null;
107
+ await (await this.#unique((await this.#unique(article)).getByRole("button", {
108
+ name: more[0].label,
109
+ exact: true,
110
+ }))).click();
111
+ return { completed: true };
112
+ return { completed: false, loc, submittedEditor };
113
+ }
114
+ async handleSubmitEditedPost(command) {
115
+ const { query, result } = this.#session.current;
116
+ let loc, submittedEditor;
117
+ if (query.view !== "composer" ||
118
+ result.needsStateCheck ||
119
+ result.alerts?.length ||
120
+ !this.#session.editBinding ||
121
+ this.#session.editBinding.postId !== command.postId ||
122
+ result.accountHandle !== this.#session.editBinding.accountHandle ||
123
+ !command.evidenceRef ||
124
+ !command.approvedText?.trim() ||
125
+ result.editorText !== command.approvedText ||
126
+ result.editors?.length !== 1)
127
+ throw new Error("编辑提交缺本人原帖绑定或精确审稿证据");
128
+ const updates = result.buttons.filter((b) => /^(更新|Update)$/.test(b.label) && !b.disabled);
129
+ if (updates.length !== 1)
130
+ throw new Error("编辑更新按钮未唯一观察");
131
+ const update = await this.#unique(this.#locator("button", updates[0].label));
132
+ await command.beforeSubmit?.();
133
+ this.#session.current = null;
134
+ this.#session.editBinding = null;
135
+ if (command.activation === "keyboard")
136
+ await update.press("Enter");
137
+ else
138
+ await update.click();
139
+ await this.#tab.playwright.waitForLoadState({
140
+ state: "networkidle",
141
+ timeoutMs: 8000,
142
+ });
143
+ return { completed: true };
144
+ return { completed: false, loc, submittedEditor };
145
+ }
146
+ async handleExpandPost(command) {
147
+ const { query, result } = this.#session.current;
148
+ let loc, submittedEditor;
149
+ if (query.view !== "posts" ||
150
+ result.posts?.length !== 1 ||
151
+ result.posts[0].id !== command.postId)
152
+ throw new Error("展开需唯一原帖");
153
+ const primary = result.posts[0].primary;
154
+ const article = this.#locator("article").filter({
155
+ has: this.#tab.playwright.locator(`a[href=${JSON.stringify(primary)}]`),
156
+ visible: true,
157
+ });
158
+ const control = (await this.#unique(article)).getByText(/^(显示更多|Show more)$/i);
159
+ this.#session.current = null;
160
+ await (await this.#unique(control)).click();
161
+ return { completed: true };
162
+ return { completed: false, loc, submittedEditor };
163
+ }
164
+ async handleNextPosts(command) {
165
+ const { query, result } = this.#session.current;
166
+ let loc, submittedEditor;
167
+ if (query.view !== "posts" || result.needsStateCheck)
168
+ throw new Error("翻页须先观察原列表");
169
+ const primary = result.posts?.at(-1)?.primary;
170
+ if (!primary)
171
+ throw new Error("翻页须有已观察原帖链接");
172
+ const anchor = await this.#unique(this.#tab.playwright
173
+ .locator(`a[href=${JSON.stringify(primary)}]`)
174
+ .filter({ visible: true }));
175
+ this.#session.current = null;
176
+ await anchor.press("PageDown");
177
+ return { completed: true };
178
+ return { completed: false, loc, submittedEditor };
179
+ }
180
+ async handleClearPreparedText(command) {
181
+ const { query, result } = this.#session.current;
182
+ let loc, submittedEditor;
183
+ if (query.view !== "composer" ||
184
+ result.needsStateCheck ||
185
+ !command.evidenceRef ||
186
+ typeof command.text !== "string" ||
187
+ result.editorText?.replace(/\n{3,}/g, "\n\n") !==
188
+ command.text.replace(/\n{3,}/g, "\n\n") ||
189
+ result.editors?.length !== 1)
190
+ throw new Error("仅恢复本次已知稿件准备");
191
+ const editor = await this.#unique(this.#locator("textbox", result.editors[0].label));
192
+ await editor.press("Meta+A");
193
+ await editor.press("Backspace");
194
+ return { completed: true, value: this.#observe({ view: "composer" }) };
195
+ return { completed: false, loc, submittedEditor };
196
+ }
197
+ async handleClosePreparedReply(command) {
198
+ const { query, result } = this.#session.current;
199
+ let loc, submittedEditor;
200
+ if (query.view !== "composer" ||
201
+ result.needsStateCheck ||
202
+ result.editorText?.trim() !== "" ||
203
+ !command.evidenceRef)
204
+ throw new Error("仅关闭有准备失败证据的空回复");
205
+ await (await this.#unique(this.#locator("button", "关闭"))).click();
206
+ this.#session.replyBinding = null;
207
+ return {
208
+ completed: true,
209
+ value: this.#observe({ view: "posts", limit: 5 }),
210
+ };
211
+ return { completed: false, loc, submittedEditor };
212
+ }
213
+ async handleClosePreparedMedia(command) {
214
+ const { query, result } = this.#session.current;
215
+ let loc, submittedEditor;
216
+ if (query.view !== "media" ||
217
+ result.needsStateCheck ||
218
+ !command.evidenceRef)
219
+ throw new Error("媒体恢复须有准备失败证据");
220
+ const backs = result.controls.filter((b) => b.role === "button" && /^(返回|Back)$/i.test(b.label) && !b.disabled);
221
+ if (backs.length !== 1)
222
+ throw new Error("媒体返回入口不唯一");
223
+ await (await this.#unique(this.#locator("button", backs[0].label))).click();
224
+ await this.#tab.playwright
225
+ .getByRole("textbox", { name: /^(帖子文本|Post text)$/i })
226
+ .waitFor({ state: "visible", timeoutMs: 5000 });
227
+ return { completed: true, value: this.#observe({ view: "composer" }) };
228
+ return { completed: false, loc, submittedEditor };
229
+ }
230
+ async handleRemovePreparedMedia(command) {
231
+ const { query, result } = this.#session.current;
232
+ let loc, submittedEditor;
233
+ if (query.view !== "composer" ||
234
+ result.needsStateCheck ||
235
+ result.editorText?.trim() !== "" ||
236
+ !command.evidenceRef)
237
+ throw new Error("仅恢复有来源证据的空正文媒体准备");
238
+ const removes = result.buttons.filter((b) => /^(移除媒体|Remove media)$/i.test(b.label) && !b.disabled);
239
+ if (removes.length !== 1)
240
+ throw new Error("待恢复媒体不唯一");
241
+ await (await this.#unique(this.#locator("button", removes[0].label))).click();
242
+ return { completed: true, value: this.#observe({ view: "composer" }) };
243
+ return { completed: false, loc, submittedEditor };
244
+ }
245
+ async handleAttachAssets(command) {
246
+ const { query, result } = this.#session.current;
247
+ let loc, submittedEditor;
248
+ if (query.view !== "composer" ||
249
+ result.needsStateCheck ||
250
+ command.assets?.length !== 1)
251
+ throw new Error("仅上传已观察编辑器的一张带ALT图片");
252
+ const asset = command.assets[0];
253
+ if (!asset.alt?.trim())
254
+ throw new Error("图片须有完整ALT");
255
+ this.#session.current = null;
256
+ await attachImage(this.#tab, { path: asset.path });
257
+ await this.#tab.playwright
258
+ .getByRole("button", {
259
+ name: /^(?:编辑|Edit)(?:图片|照片|媒体| image| photo| media)?(?: \d+)?$/i,
260
+ })
261
+ .waitFor({ state: "visible", timeoutMs: 5000 });
262
+ const composer = await this.#observe({ view: "composer" });
263
+ const edits = composer.buttons.filter((b) => /^(?:编辑|Edit)(?:图片|照片|媒体| image| photo| media)?(?: \d+)?$/i.test(b.label) && !b.disabled);
264
+ if (edits.length !== 1)
265
+ throw new Error("图片编辑入口未唯一观察");
266
+ await (await this.#unique(this.#locator("button", edits[0].label))).click();
267
+ await this.#tab.playwright
268
+ .getByRole("tab", {
269
+ name: /^(编辑图片描述|Edit image description)$/i,
270
+ })
271
+ .waitFor({ state: "visible", timeoutMs: 5000 });
272
+ let media = await this.#observe({ view: "media" });
273
+ const tabControl = media.controls.find((c) => c.role === "tab" &&
274
+ /^(编辑图片描述|Edit image description)$/i.test(c.label));
275
+ if (!tabControl)
276
+ throw new Error("ALT页签未观察");
277
+ await (await this.#unique(this.#locator("tab", tabControl.label))).click();
278
+ media = await this.#observe({ view: "media" });
279
+ if (media.needsStateCheck || media.fields.length !== 1)
280
+ throw new Error("ALT输入未唯一观察");
281
+ const field = media.fields[0];
282
+ const input = field.label
283
+ ? this.#tab.playwright.getByLabel(field.label, { exact: true })
284
+ : field.placeholder
285
+ ? this.#tab.playwright.getByPlaceholder(field.placeholder, {
286
+ exact: true,
287
+ })
288
+ : this.#locator("textbox");
289
+ await (await this.#unique(input)).fill(asset.alt);
290
+ media = await this.#observe({ view: "media" });
291
+ if (media.fields[0]?.value !== asset.alt)
292
+ throw new Error("ALT全文不匹配");
293
+ const save = media.controls.filter((c) => c.role === "button" && /^(保存|Save)$/i.test(c.label) && !c.disabled);
294
+ if (save.length !== 1)
295
+ throw new Error("ALT保存入口不唯一");
296
+ await (await this.#unique(this.#locator("button", save[0].label))).click();
297
+ const after = await this.#observe({ view: "composer" });
298
+ if (after.needsStateCheck || !after.images?.length)
299
+ throw new Error("上传图片未回到编辑器");
300
+ return {
301
+ completed: true,
302
+ value: {
303
+ assets: [{ path: asset.path, alt: asset.alt }],
304
+ mediaEvidence: media,
305
+ composer: after,
306
+ },
307
+ };
308
+ return { completed: false, loc, submittedEditor };
309
+ }
310
+ async handleFollow(command) {
311
+ const { query, result } = this.#session.current;
312
+ let loc, submittedEditor;
313
+ if (query.view !== "person" ||
314
+ query.handle !== command.handle ||
315
+ result.needsStateCheck)
316
+ throw new Error("人物身份未绑定");
317
+ const name = result.relationship === "follows-you"
318
+ ? `回关 @${command.handle}`
319
+ : `关注 @${command.handle}`;
320
+ loc = await this.#unique(this.#locator("button", name));
321
+ return { completed: false, loc, submittedEditor };
322
+ }
323
+ async handleOpenReply(command) {
324
+ const { query, result } = this.#session.current;
325
+ let loc, submittedEditor;
326
+ this.#session.replyBinding = null;
327
+ if (query.targetId !== command.postId || result.posts?.length !== 1)
328
+ throw new Error("回复主帖未唯一绑定");
329
+ const names = result.posts[0].buttons.filter((b) => /回复|Reply/i.test(b.label));
330
+ if (names.length !== 1)
331
+ throw new Error("回复按钮未唯一观察");
332
+ const primary = result.posts[0].primary;
333
+ if (!primary || primary.match(/\/status\/(\d+)$/)?.[1] !== command.postId)
334
+ throw new Error("回复主帖永久链接未绑定");
335
+ const article = this.#locator("article").filter({
336
+ has: this.#tab.playwright.locator(`a[href=${JSON.stringify(primary)}]`),
337
+ visible: true,
338
+ });
339
+ loc = await this.#unique((await this.#unique(article)).getByRole("button", {
340
+ name: names[0].label,
341
+ exact: true,
342
+ }));
343
+ return { completed: false, loc, submittedEditor };
344
+ }
345
+ async handleFillComposer(command) {
346
+ const { query, result } = this.#session.current;
347
+ let loc, submittedEditor;
348
+ if (query.view !== "composer" || result.editors?.length !== 1)
349
+ throw new Error("编辑器未唯一观察");
350
+ loc = await this.#unique(this.#locator("textbox", result.editors[0].label));
351
+ this.#session.current = null;
352
+ await loc.fill(command.text);
353
+ const submits = result.buttons.filter((b) => /^(回复|Reply|发帖|发布|Post|更新|Update)$/i.test(b.label));
354
+ if (submits.length !== 1)
355
+ throw new Error("编辑后提交入口不唯一");
356
+ await this.#locator("button", submits[0].label)
357
+ .and(this.#tab.playwright.locator(':not([disabled]):not([aria-disabled="true"])'))
358
+ .waitFor({ state: "visible", timeoutMs: 5000 });
359
+ return { completed: true };
360
+ return { completed: false, loc, submittedEditor };
361
+ }
362
+ async handleSubmitPost(command) {
363
+ const { query, result } = this.#session.current;
364
+ let loc, submittedEditor;
365
+ if (query.view !== "composer")
366
+ throw new Error("未观察编辑器");
367
+ const pattern = command.operation === "submitReply"
368
+ ? /^(回复|Reply)$/i
369
+ : /^(发帖|发布|Post)$/i;
370
+ const buttons = result.buttons?.filter((b) => pattern.test(b.label) && !b.disabled) ?? [];
371
+ if (buttons.length !== 1)
372
+ throw new Error("提交按钮未唯一观察");
373
+ loc = await this.#unique(this.#locator("button", buttons[0].label));
374
+ if (command.activation !== undefined &&
375
+ !["click", "keyboard"].includes(command.activation))
376
+ throw new Error("提交激活方式无效");
377
+ if (command.activation === "keyboard" && typeof loc.press !== "function")
378
+ throw new Error("缺语义按钮键盘提交能力");
379
+ if (command.operation === "submitReply") {
380
+ if (result.editors?.length !== 1 || !result.editorText?.trim())
381
+ throw new Error("回复提交缺唯一完整编辑器");
382
+ submittedEditor = this.#locator("textbox", result.editors[0].label).filter({
383
+ hasText: new RegExp(
384
+ // Full text and actor are checked by platform.compose before this act.
385
+ // Use a short observed anchor for the retained-text wait: host locator
386
+ // bridges can reject a multi-kilobyte hasText regex on long posts.
387
+ result.editorText
388
+ .trim()
389
+ .split(/\r?\n/)[0]
390
+ .slice(0, 64)
391
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
392
+ });
393
+ await this.#unique(submittedEditor);
394
+ }
395
+ else {
396
+ if (result.editors?.length !== 1 || !result.editorText?.trim())
397
+ throw new Error("发帖提交缺唯一完整编辑器");
398
+ // Publication with media may stay pending after the click. Do not let
399
+ // receipt navigation interrupt it while the submitted text is retained.
400
+ submittedEditor = this.#locator("textbox", result.editors[0].label).filter({
401
+ hasText: new RegExp(
402
+ // Full text and actor are checked by platform.compose before this act.
403
+ // Use a short observed anchor for the retained-text wait: host locator
404
+ // bridges can reject a multi-kilobyte hasText regex on long posts.
405
+ result.editorText
406
+ .trim()
407
+ .split(/\r?\n/)[0]
408
+ .slice(0, 64)
409
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
410
+ });
411
+ await this.#unique(submittedEditor);
412
+ }
413
+ return { completed: false, loc, submittedEditor };
414
+ }
415
+ }
416
+ export function createDOMEditing(...args) {
417
+ return new DOMEditing(...args);
418
+ }
@@ -0,0 +1,18 @@
1
+ import type { DOMDependencies } from "./browser-dom-dependencies.js";
2
+ export declare class DOMNavigation {
3
+ #private;
4
+ searchSources: (command: any) => Promise<void>;
5
+ waitForFolder: (command: any) => Promise<void>;
6
+ waitForPosts: (command: any) => Promise<void>;
7
+ openObserved: (command: any) => Promise<void>;
8
+ refreshPerson: (command: any) => Promise<void>;
9
+ refreshPosts: (command: any) => Promise<void>;
10
+ constructor(dependencies: Pick<DOMDependencies, "session" | "tab" | "observe">);
11
+ private handleSearchSources;
12
+ private handleWaitForFolder;
13
+ private handleWaitForPosts;
14
+ private handleOpenObserved;
15
+ private handleRefreshPerson;
16
+ private handleRefreshPosts;
17
+ }
18
+ export declare function createDOMNavigation(...args: ConstructorParameters<typeof DOMNavigation>): DOMNavigation;