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