@nextclaw/x-harness 0.1.1 → 0.2.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 (80) hide show
  1. package/LICENSE +231 -6
  2. package/README.md +15 -1
  3. package/SOURCE.md +9 -0
  4. package/cli/main.js +232 -200
  5. package/dist/api.d.ts +56 -2
  6. package/dist/api.js +130 -79
  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 +7 -2
  11. package/dist/browser-dom-capabilities.js +123 -102
  12. package/dist/browser-dom-dependencies.js +1 -0
  13. package/dist/browser-dom-editing.d.ts +20 -2
  14. package/dist/browser-dom-editing.js +148 -116
  15. package/dist/browser-dom-navigation.d.ts +11 -2
  16. package/dist/browser-dom-navigation.js +66 -50
  17. package/dist/browser-dom-submission.d.ts +6 -1
  18. package/dist/browser-dom-submission.js +37 -18
  19. package/dist/browser-dom.d.ts +13 -1
  20. package/dist/browser-dom.js +113 -66
  21. package/dist/browser-native.d.ts +14 -1
  22. package/dist/browser-native.js +161 -128
  23. package/dist/creator-native.d.ts +3 -0
  24. package/dist/creator-native.js +113 -0
  25. package/dist/diagnostics.d.ts +11 -0
  26. package/dist/diagnostics.js +10 -0
  27. package/dist/growth-batch.d.ts +6 -3
  28. package/dist/growth-batch.js +157 -135
  29. package/dist/growth-dependencies.js +1 -0
  30. package/dist/growth-execution.d.ts +8 -2
  31. package/dist/growth-execution.js +68 -37
  32. package/dist/growth-parallel.d.ts +6 -2
  33. package/dist/growth-parallel.js +78 -47
  34. package/dist/growth-plan.d.ts +8 -3
  35. package/dist/growth-plan.js +45 -23
  36. package/dist/growth-policy.d.ts +11 -2
  37. package/dist/growth-policy.js +80 -53
  38. package/dist/growth-publishing.d.ts +7 -2
  39. package/dist/growth-publishing.js +72 -46
  40. package/dist/growth-reading.d.ts +6 -2
  41. package/dist/growth-reading.js +47 -25
  42. package/dist/growth-recovery.d.ts +7 -4
  43. package/dist/growth-recovery.js +177 -148
  44. package/dist/growth-state.d.ts +10 -2
  45. package/dist/growth-state.js +67 -30
  46. package/dist/growth-tasks.d.ts +19 -16
  47. package/dist/growth-tasks.js +153 -90
  48. package/dist/growth.d.ts +25 -1
  49. package/dist/growth.js +207 -124
  50. package/dist/platform-capabilities.d.ts +6 -2
  51. package/dist/platform-capabilities.js +18 -8
  52. package/dist/platform-collections.d.ts +10 -3
  53. package/dist/platform-collections.js +62 -35
  54. package/dist/platform-dependencies.js +1 -0
  55. package/dist/platform-diagnostics.d.ts +7 -2
  56. package/dist/platform-diagnostics.js +31 -19
  57. package/dist/platform-observation.d.ts +10 -2
  58. package/dist/platform-observation.js +44 -23
  59. package/dist/platform-targets.d.ts +10 -3
  60. package/dist/platform-targets.js +39 -14
  61. package/dist/platform-writing.d.ts +10 -2
  62. package/dist/platform-writing.js +63 -32
  63. package/dist/platform.d.ts +22 -1
  64. package/dist/platform.js +120 -52
  65. package/dist/readers-ax-host.d.ts +11 -1
  66. package/dist/readers-ax-host.js +108 -99
  67. package/dist/readers-ax-program.js +5 -0
  68. package/dist/readers-ax-tree.js +1 -0
  69. package/dist/readers-cli.js +79 -0
  70. package/dist/readers.js +9 -0
  71. package/dist/round-state.js +18 -1
  72. package/dist/store.d.ts +22 -4
  73. package/dist/store.js +173 -151
  74. package/dist/types.js +1 -0
  75. package/generated/observation.mjs +1 -1
  76. package/package.json +23 -9
  77. package/skill/SKILL.md +2 -2
  78. package/skill/references/reads.md +1 -1
  79. package/skill/references/recovery.md +4 -4
  80. package/skill/references/setup.md +1 -1
@@ -1,8 +1,34 @@
1
1
  import { xURL, sameHandle, sameText, samePublishedText, } from "./platform-rules.js";
2
- export function createPlatformWriting(dependencies) {
3
- const { observed, browser, followStage, readPerson, open, clock, readPost } = dependencies;
4
- async function compose(action) {
5
- let value = await observed({ view: "composer" });
2
+ export class PlatformWriting {
3
+ #observed;
4
+ #browser;
5
+ #followStage;
6
+ #readPerson;
7
+ #open;
8
+ #clock;
9
+ #readPost;
10
+ compose;
11
+ findSubmission;
12
+ submitFollow;
13
+ submitPost;
14
+ submitReply;
15
+ constructor(dependencies) {
16
+ const { observed, browser, followStage, readPerson, open, clock, readPost, } = dependencies;
17
+ this.#observed = observed;
18
+ this.#browser = browser;
19
+ this.#followStage = followStage;
20
+ this.#readPerson = readPerson;
21
+ this.#open = open;
22
+ this.#clock = clock;
23
+ this.#readPost = readPost;
24
+ this.compose = this.handleCompose.bind(this);
25
+ this.findSubmission = this.handleFindSubmission.bind(this);
26
+ this.submitFollow = this.handleSubmitFollow.bind(this);
27
+ this.submitPost = this.handleSubmitPost.bind(this);
28
+ this.submitReply = this.handleSubmitReply.bind(this);
29
+ }
30
+ async handleCompose(action) {
31
+ let value = await this.#observed({ view: "composer" });
6
32
  if (value.needsStateCheck ||
7
33
  value.alerts?.length ||
8
34
  typeof value.editorText !== "string" ||
@@ -10,9 +36,12 @@ export function createPlatformWriting(dependencies) {
10
36
  (action.kind === "reply" && value.replyToId !== action.postId))
11
37
  throw new Error("编辑器不为空或回复上下文未绑定");
12
38
  if (action.assets?.length)
13
- await browser.act({ operation: "attachAssets", assets: action.assets });
14
- await browser.act({ operation: "fillComposer", text: action.text });
15
- value = await observed({ view: "composer" });
39
+ await this.#browser.act({
40
+ operation: "attachAssets",
41
+ assets: action.assets,
42
+ });
43
+ await this.#browser.act({ operation: "fillComposer", text: action.text });
44
+ value = await this.#observed({ view: "composer" });
16
45
  if (!sameText(value.editorText, action.text) ||
17
46
  value.needsStateCheck ||
18
47
  value.alerts?.length ||
@@ -20,17 +49,17 @@ export function createPlatformWriting(dependencies) {
20
49
  (action.accountHandle &&
21
50
  !sameHandle(value.accountHandle, action.accountHandle)))
22
51
  throw new Error(`提交前账号/正文/目标未核实 ${JSON.stringify({ textMatches: sameText(value.editorText, action.text), needsStateCheck: value.needsStateCheck, alerts: value.alerts, accountHandle: value.accountHandle, replyToId: value.replyToId })}`);
23
- return browser.act({
52
+ return this.#browser.act({
24
53
  operation: action.kind === "reply" ? "submitReply" : "submitPost",
25
54
  beforeSubmit: action.beforeSubmit,
26
55
  ...(action.activation ? { activation: action.activation } : {}),
27
56
  });
28
57
  }
29
- async function findSubmission(action) {
58
+ async handleFindSubmission(action) {
30
59
  if (action.kind === "follow") {
31
- let value = await followStage(action, "verify-read", () => readPerson(action));
60
+ let value = await this.#followStage(action, "verify-read", () => this.#readPerson(action));
32
61
  if (!value.needsStateCheck) {
33
- const absent = browser.capabilities?.("refreshPerson") ?? [
62
+ const absent = this.#browser.capabilities?.("refreshPerson") ?? [
34
63
  "refreshPerson",
35
64
  ];
36
65
  if (absent.length)
@@ -40,13 +69,13 @@ export function createPlatformWriting(dependencies) {
40
69
  missing: absent,
41
70
  needsStateCheck: true,
42
71
  };
43
- await followStage(action, "verify-refresh", () => browser.act({
72
+ await this.#followStage(action, "verify-refresh", () => this.#browser.act({
44
73
  operation: "refreshPerson",
45
74
  handle: action.handle,
46
75
  url: action.url,
47
76
  evidenceRef: action.evidenceRef,
48
77
  }));
49
- value = await followStage(action, "verify-after-refresh", () => readPerson(action));
78
+ value = await this.#followStage(action, "verify-after-refresh", () => this.#readPerson(action));
50
79
  }
51
80
  return {
52
81
  ...value,
@@ -79,16 +108,16 @@ export function createPlatformWriting(dependencies) {
79
108
  : action.kind === "reply" && action.url && action.evidenceRef
80
109
  ? { url: action.url, evidenceRef: action.evidenceRef }
81
110
  : { url: action.accountURL, evidenceRef: action.accountEvidenceRef };
82
- let value = await open(receiptTarget, { view: "posts", limit: 5 });
111
+ let value = await this.#open(receiptTarget, { view: "posts", limit: 5 });
83
112
  if (!(action.kind === "original" && observedReceipt) &&
84
113
  !value.needsStateCheck &&
85
- !(browser.capabilities?.("refreshPosts") ?? ["refreshPosts"]).length) {
86
- await browser.act({
114
+ !(this.#browser.capabilities?.("refreshPosts") ?? ["refreshPosts"]).length) {
115
+ await this.#browser.act({
87
116
  operation: "refreshPosts",
88
117
  url: receiptTarget.url,
89
118
  evidenceRef: receiptTarget.evidenceRef,
90
119
  });
91
- value = await observed({ view: "posts", limit: 5 });
120
+ value = await this.#observed({ view: "posts", limit: 5 });
92
121
  }
93
122
  const matchesContent = (post) => sameHandle(post.author, action.accountHandle) &&
94
123
  samePublishedText(post.body, action.text) &&
@@ -98,7 +127,7 @@ export function createPlatformWriting(dependencies) {
98
127
  !(action.beforeIds ?? []).includes(post.id) &&
99
128
  Number.isFinite(Date.parse(post.datetime)) &&
100
129
  Date.parse(post.datetime) >= Date.parse(action.intentAt) &&
101
- Date.parse(post.datetime) <= clock() + 5000 &&
130
+ Date.parse(post.datetime) <= this.#clock() + 5000 &&
102
131
  !post.needsExpansion;
103
132
  let matches = (value.posts ?? []).filter(matchesContent);
104
133
  if (action.kind === "reply" &&
@@ -110,7 +139,7 @@ export function createPlatformWriting(dependencies) {
110
139
  const url = xURL(new URL(candidate.primary, xURL(value.page).origin).href);
111
140
  if (url.pathname.toLowerCase() ===
112
141
  `/${action.accountHandle}/status/${candidate.id}`.toLowerCase()) {
113
- value = await open({
142
+ value = await this.#open({
114
143
  url: url.href,
115
144
  evidenceRef: {
116
145
  page: value.page,
@@ -135,37 +164,39 @@ export function createPlatformWriting(dependencies) {
135
164
  missing: matches.length === 1 ? [] : ["unique-public-submission-unconfirmed"],
136
165
  };
137
166
  }
138
- async function submitFollow(action) {
139
- const value = await followStage(action, "submit-preflight-read", () => readPerson(action));
167
+ async handleSubmitFollow(action) {
168
+ const value = await this.#followStage(action, "submit-preflight-read", () => this.#readPerson(action));
140
169
  if (value.needsStateCheck ||
141
170
  value.alerts?.length ||
142
171
  !["not-following", "follows-you"].includes(value.relationship) ||
143
172
  (action.accountHandle &&
144
173
  !sameHandle(value.accountHandle, action.accountHandle)))
145
174
  throw new Error("fresh账号/人物关系不允许关注");
146
- await followStage(action, "submit-guard", () => action.beforeSubmit?.());
175
+ await this.#followStage(action, "submit-guard", () => action.beforeSubmit?.());
147
176
  // A returned click is not a receipt from X; durable success still requires fresh verification.
148
- const clicked = await followStage(action, "click", () => browser.act({ operation: "follow", handle: action.handle }));
177
+ const clicked = await this.#followStage(action, "click", () => this.#browser.act({ operation: "follow", handle: action.handle }));
149
178
  try {
150
- await followStage(action, "click-ui-observation", () => observed({ view: "person", handle: action.handle }));
179
+ await this.#followStage(action, "click-ui-observation", () => this.#observed({ view: "person", handle: action.handle }));
151
180
  }
152
181
  catch {
153
182
  /* The click may have committed; retain diagnostic failure and use the original fresh receipt path. */
154
183
  }
155
184
  return clicked;
156
185
  }
157
- async function submitPost(action) {
158
- await open({ url: action.composeURL, evidenceRef: action.composeEvidenceRef }, { view: "composer" });
159
- return compose(action);
186
+ async handleSubmitPost(action) {
187
+ await this.#open({ url: action.composeURL, evidenceRef: action.composeEvidenceRef }, { view: "composer" });
188
+ return this.handleCompose(action);
160
189
  }
161
- async function submitReply(action) {
162
- const value = await readPost(action);
190
+ async handleSubmitReply(action) {
191
+ const value = await this.#readPost(action);
163
192
  if (value.needsStateCheck ||
164
193
  value.alerts?.length ||
165
194
  !(value.posts ?? []).some((p) => p.id === action.postId))
166
195
  throw new Error("回复主帖身份未核实");
167
- await browser.act({ operation: "openReply", postId: action.postId });
168
- return compose(action);
196
+ await this.#browser.act({ operation: "openReply", postId: action.postId });
197
+ return this.handleCompose(action);
169
198
  }
170
- return { compose, findSubmission, submitFollow, submitPost, submitReply };
199
+ }
200
+ export function createPlatformWriting(...args) {
201
+ return new PlatformWriting(...args);
171
202
  }
@@ -1,2 +1,23 @@
1
1
  export { xURL } from "./platform-rules.js";
2
- export declare function createXPlatform(browser: any, { clock }?: any): any;
2
+ export declare class XPlatform {
3
+ #private;
4
+ preflight: ({ operation, input }: any) => any;
5
+ readCreator: (args: import("./types.js").CreatorOptions) => Promise<any>;
6
+ readAccount: () => Promise<any>;
7
+ readBookmarks: ({ folder, ...window }?: any) => Promise<any>;
8
+ searchSources: ({ query, limit, maxPages, processedIds, }?: any) => Promise<any>;
9
+ readFeedback: ({ targets, replyLimit }?: any) => Promise<any>;
10
+ readPerson: (target: any) => Promise<any>;
11
+ readPost: (target: any) => Promise<any>;
12
+ findSubmission: (action: any) => Promise<any>;
13
+ submitFollow: (action: any) => Promise<any>;
14
+ submitPost: (action: any) => Promise<any>;
15
+ submitReply: (action: any) => Promise<any>;
16
+ captureProof: ({ observation }: any) => {
17
+ followDiagnostics?: any[] | undefined;
18
+ capturedAt: string;
19
+ observation: any;
20
+ };
21
+ constructor(browser: any, { clock }?: any);
22
+ }
23
+ export declare function createXPlatform(...args: ConstructorParameters<typeof XPlatform>): any;
package/dist/platform.js CHANGED
@@ -5,56 +5,124 @@ import { createPlatformDiagnostics } from "./platform-diagnostics.js";
5
5
  import { createPlatformWriting } from "./platform-writing.js";
6
6
  import { createPlatformCapabilities } from "./platform-capabilities.js";
7
7
  export { xURL } from "./platform-rules.js";
8
- export function createXPlatform(browser, { clock = () => Date.now() } = {}) {
9
- const createPlatformObservationResult = createPlatformObservation({
10
- browser,
11
- clock,
12
- });
13
- const { missing, observed, open, ready, choose } = createPlatformObservationResult;
14
- const createPlatformTargetsResult = createPlatformTargets({
15
- observed,
16
- open,
17
- browser,
18
- ready,
19
- });
20
- const { readAccount, readFeedback, readPerson, readPost } = createPlatformTargetsResult;
21
- const createPlatformCollectionsResult = createPlatformCollections({
22
- ready,
23
- observed,
24
- browser,
25
- choose,
26
- });
27
- const { collectSources, readCreator, readBookmarks, searchSources } = createPlatformCollectionsResult;
28
- const createPlatformDiagnosticsResult = createPlatformDiagnostics({ clock });
29
- const { followStage, captureProof } = createPlatformDiagnosticsResult;
30
- const createPlatformWritingResult = createPlatformWriting({
31
- observed,
32
- browser,
33
- followStage,
34
- readPerson,
35
- open,
36
- clock,
37
- readPost,
38
- });
39
- const { compose, findSubmission, submitFollow, submitPost, submitReply } = createPlatformWritingResult;
40
- const createPlatformCapabilitiesResult = createPlatformCapabilities({
41
- missing,
42
- browser,
43
- });
44
- const { preflight } = createPlatformCapabilitiesResult;
45
- return {
46
- preflight,
47
- readCreator,
48
- readAccount,
49
- readBookmarks,
50
- searchSources,
51
- readFeedback,
52
- readPerson,
53
- readPost,
54
- findSubmission,
55
- submitFollow,
56
- submitPost,
57
- submitReply,
58
- captureProof,
59
- };
8
+ export class XPlatform {
9
+ #clock;
10
+ #createPlatformObservationResult;
11
+ #missing;
12
+ #observed;
13
+ #open;
14
+ #ready;
15
+ #choose;
16
+ #createPlatformTargetsResult;
17
+ #readAccount;
18
+ #readFeedback;
19
+ #readPerson;
20
+ #readPost;
21
+ #createPlatformCollectionsResult;
22
+ #collectSources;
23
+ #readCreator;
24
+ #readBookmarks;
25
+ #searchSources;
26
+ #createPlatformDiagnosticsResult;
27
+ #followStage;
28
+ #captureProof;
29
+ #createPlatformWritingResult;
30
+ #compose;
31
+ #findSubmission;
32
+ #submitFollow;
33
+ #submitPost;
34
+ #submitReply;
35
+ #createPlatformCapabilitiesResult;
36
+ #preflight;
37
+ preflight;
38
+ readCreator;
39
+ readAccount;
40
+ readBookmarks;
41
+ searchSources;
42
+ readFeedback;
43
+ readPerson;
44
+ readPost;
45
+ findSubmission;
46
+ submitFollow;
47
+ submitPost;
48
+ submitReply;
49
+ captureProof;
50
+ constructor(browser, { clock = () => Date.now() } = {}) {
51
+ this.#clock = clock;
52
+ this.#createPlatformObservationResult = createPlatformObservation({
53
+ browser: browser,
54
+ clock: this.#clock,
55
+ });
56
+ const { missing, observed, open, ready, choose } = this.#createPlatformObservationResult;
57
+ this.#missing = missing;
58
+ this.#observed = observed;
59
+ this.#open = open;
60
+ this.#ready = ready;
61
+ this.#choose = choose;
62
+ this.#createPlatformTargetsResult = createPlatformTargets({
63
+ observed: this.#observed,
64
+ open: this.#open,
65
+ browser: browser,
66
+ ready: this.#ready,
67
+ });
68
+ const { readAccount, readFeedback, readPerson, readPost } = this.#createPlatformTargetsResult;
69
+ this.#readAccount = readAccount;
70
+ this.#readFeedback = readFeedback;
71
+ this.#readPerson = readPerson;
72
+ this.#readPost = readPost;
73
+ this.#createPlatformCollectionsResult = createPlatformCollections({
74
+ ready: this.#ready,
75
+ observed: this.#observed,
76
+ browser: browser,
77
+ choose: this.#choose,
78
+ });
79
+ const { collectSources, readCreator, readBookmarks, searchSources } = this.#createPlatformCollectionsResult;
80
+ this.#collectSources = collectSources;
81
+ this.#readCreator = readCreator;
82
+ this.#readBookmarks = readBookmarks;
83
+ this.#searchSources = searchSources;
84
+ this.#createPlatformDiagnosticsResult = createPlatformDiagnostics({
85
+ clock: this.#clock,
86
+ });
87
+ const { followStage, captureProof } = this.#createPlatformDiagnosticsResult;
88
+ this.#followStage = followStage;
89
+ this.#captureProof = captureProof;
90
+ this.#createPlatformWritingResult = createPlatformWriting({
91
+ observed: this.#observed,
92
+ browser: browser,
93
+ followStage: this.#followStage,
94
+ readPerson: this.#readPerson,
95
+ open: this.#open,
96
+ clock: this.#clock,
97
+ readPost: this.#readPost,
98
+ });
99
+ const { compose, findSubmission, submitFollow, submitPost, submitReply } = this.#createPlatformWritingResult;
100
+ this.#compose = compose;
101
+ this.#findSubmission = findSubmission;
102
+ this.#submitFollow = submitFollow;
103
+ this.#submitPost = submitPost;
104
+ this.#submitReply = submitReply;
105
+ this.#createPlatformCapabilitiesResult = createPlatformCapabilities({
106
+ missing: this.#missing,
107
+ browser: browser,
108
+ });
109
+ const { preflight } = this.#createPlatformCapabilitiesResult;
110
+ this.#preflight = preflight;
111
+ this.preflight = this.#preflight;
112
+ this.readCreator = this.#readCreator;
113
+ this.readAccount = this.#readAccount;
114
+ this.readBookmarks = this.#readBookmarks;
115
+ this.searchSources = this.#searchSources;
116
+ this.readFeedback = this.#readFeedback;
117
+ this.readPerson = this.#readPerson;
118
+ this.readPost = this.#readPost;
119
+ this.findSubmission = this.#findSubmission;
120
+ this.submitFollow = this.#submitFollow;
121
+ this.submitPost = this.#submitPost;
122
+ this.submitReply = this.#submitReply;
123
+ this.captureProof = this.#captureProof;
124
+ }
125
+ }
126
+ export function createXPlatform(...args) {
127
+ return new XPlatform(...args);
60
128
  }
@@ -1,2 +1,12 @@
1
1
  export declare function createAXTabHarness(tab: any): any;
2
- export declare function createAXHarness(app: any): any;
2
+ export declare class AXHarness {
3
+ #private;
4
+ constructor(app: any);
5
+ private requireMethods;
6
+ private indexes;
7
+ private handleObserve;
8
+ observe(options?: any): Promise<any>;
9
+ act({ index, operation, value, read, }?: any): Promise<any>;
10
+ screenshot(): Promise<any>;
11
+ }
12
+ export declare function createAXHarness(...args: ConstructorParameters<typeof AXHarness>): any;
@@ -11,115 +11,124 @@ export function createAXTabHarness(tab) {
11
11
  getScreenshot: (options) => tab.getScreenshot(options),
12
12
  });
13
13
  }
14
- export function createAXHarness(app) {
15
- let controls = new Map(), readable = false, busy = false;
16
- const requireMethods = (methods) => {
17
- const missing = methods.filter((name) => typeof app?.[name] !== "function");
14
+ export class AXHarness {
15
+ #app;
16
+ #controls;
17
+ #readable;
18
+ #busy;
19
+ constructor(app) {
20
+ this.screenshot = this.screenshot.bind(this);
21
+ this.act = this.act.bind(this);
22
+ this.observe = this.observe.bind(this);
23
+ this.#app = app;
24
+ this.#controls = new Map();
25
+ this.#readable = false;
26
+ this.#busy = false;
27
+ }
28
+ requireMethods(methods) {
29
+ const missing = methods.filter((name) => typeof this.#app?.[name] !== "function");
18
30
  if (missing.length) {
19
- controls.clear();
20
- readable = false;
31
+ this.#controls.clear();
32
+ this.#readable = false;
21
33
  throw new Error(`harness能力不足:${missing.join("/")};先补原入口并验证,未执行操作`);
22
34
  }
23
- };
24
- const indexes = (value) => {
35
+ }
36
+ indexes(value) {
25
37
  if (!value || typeof value !== "object")
26
38
  return;
27
39
  if (Number.isInteger(value.index) && typeof value.label === "string")
28
- controls.set(value.index, value.label);
40
+ this.#controls.set(value.index, value.label);
29
41
  for (const child of Object.values(value))
30
- indexes(child);
31
- };
32
- const observe = async (options = {}) => {
33
- controls.clear();
34
- readable = false;
42
+ this.indexes(child);
43
+ }
44
+ async handleObserve(options = {}) {
45
+ this.#controls.clear();
46
+ this.#readable = false;
35
47
  readAX("", options);
36
- requireMethods(["getAXState"]);
37
- const result = readAX(await app.getAXState({ emit: false, disableDiffing: true }), options);
38
- indexes(result);
39
- readable = true;
48
+ this.requireMethods(["getAXState"]);
49
+ const result = readAX(await this.#app.getAXState({ emit: false, disableDiffing: true }), options);
50
+ this.indexes(result);
51
+ this.#readable = true;
40
52
  return result;
41
- };
42
- return {
43
- async observe(options = {}) {
44
- if (busy)
45
- throw new Error("AX操作进行中,禁止并发读取");
46
- busy = true;
47
- try {
48
- return await observe(options);
49
- }
50
- finally {
51
- busy = false;
52
- }
53
- },
54
- async act({ index, operation = "click", value, read = {}, } = {}) {
55
- if (busy)
56
- throw new Error("AX操作进行中,禁止并发写入");
57
- if (!readable || !controls.has(index))
58
- throw new Error("必须先observe;index须来自最新定向证据");
59
- if (!["click", "fill", "paste", "pressKey", "scroll", "secondary"].includes(operation))
60
- throw new Error("未封装的AX操作,先补能力并验证");
61
- // Validate before any side effect; a malformed follow-up read must never submit.
62
- readAX("", read);
63
- if (["fill", "paste", "pressKey", "secondary"].includes(operation) &&
64
- typeof value !== "string")
65
- throw new Error("AX操作value须为字符串");
66
- if (["fill", "paste", "pressKey"].includes(operation) &&
67
- !/^(?:文本输入区(?:域)?|文本区域|文本栏|组合框) /.test(controls.get(index)))
68
- throw new Error("键盘操作须定位最新证据中的编辑控件");
69
- if (operation === "secondary" &&
70
- !controls.get(index).includes(`Secondary Actions: ${value}`))
71
- throw new Error("未观察到该辅助动作");
72
- if (operation === "scroll" &&
73
- !["up", "down", "left", "right"].includes(value))
74
- throw new Error("无效滚动方向");
75
- const methods = operation === "fill"
76
- ? ["click", "pressKey", "paste"]
77
- : ["paste", "pressKey"].includes(operation)
78
- ? ["click", operation]
79
- : [
80
- operation === "secondary"
81
- ? "performSecondaryAction"
82
- : operation,
83
- ];
84
- requireMethods([...methods, "getAXState"]);
85
- busy = true;
86
- controls.clear();
87
- readable = false;
88
- try {
89
- if (operation === "click")
90
- await app.click(index);
91
- else if (["fill", "paste", "pressKey"].includes(operation)) {
92
- await app.click(index);
93
- if (operation === "fill") {
94
- await app.pressKey("super+a");
95
- await app.paste(value);
96
- }
97
- else
98
- await app[operation](value);
53
+ }
54
+ async observe(options = {}) {
55
+ if (this.#busy)
56
+ throw new Error("AX操作进行中,禁止并发读取");
57
+ this.#busy = true;
58
+ try {
59
+ return await this.handleObserve(options);
60
+ }
61
+ finally {
62
+ this.#busy = false;
63
+ }
64
+ }
65
+ async act({ index, operation = "click", value, read = {}, } = {}) {
66
+ if (this.#busy)
67
+ throw new Error("AX操作进行中,禁止并发写入");
68
+ if (!this.#readable || !this.#controls.has(index))
69
+ throw new Error("必须先observe;index须来自最新定向证据");
70
+ if (!["click", "fill", "paste", "pressKey", "scroll", "secondary"].includes(operation))
71
+ throw new Error("未封装的AX操作,先补能力并验证");
72
+ // Validate before any side effect; a malformed follow-up read must never submit.
73
+ readAX("", read);
74
+ if (["fill", "paste", "pressKey", "secondary"].includes(operation) &&
75
+ typeof value !== "string")
76
+ throw new Error("AX操作value须为字符串");
77
+ if (["fill", "paste", "pressKey"].includes(operation) &&
78
+ !/^(?:文本输入区(?:域)?|文本区域|文本栏|组合框) /.test(this.#controls.get(index)))
79
+ throw new Error("键盘操作须定位最新证据中的编辑控件");
80
+ if (operation === "secondary" &&
81
+ !this.#controls.get(index).includes(`Secondary Actions: ${value}`))
82
+ throw new Error("未观察到该辅助动作");
83
+ if (operation === "scroll" &&
84
+ !["up", "down", "left", "right"].includes(value))
85
+ throw new Error("无效滚动方向");
86
+ const methods = operation === "fill"
87
+ ? ["click", "pressKey", "paste"]
88
+ : ["paste", "pressKey"].includes(operation)
89
+ ? ["click", operation]
90
+ : [operation === "secondary" ? "performSecondaryAction" : operation];
91
+ this.requireMethods([...methods, "getAXState"]);
92
+ this.#busy = true;
93
+ this.#controls.clear();
94
+ this.#readable = false;
95
+ try {
96
+ if (operation === "click")
97
+ await this.#app.click(index);
98
+ else if (["fill", "paste", "pressKey"].includes(operation)) {
99
+ await this.#app.click(index);
100
+ if (operation === "fill") {
101
+ await this.#app.pressKey("super+a");
102
+ await this.#app.paste(value);
99
103
  }
100
- else if (operation === "scroll")
101
- await app.scroll(index, value, 1);
102
104
  else
103
- await app.performSecondaryAction(index, value);
104
- return await observe(read);
105
- }
106
- finally {
107
- busy = false;
108
- }
109
- },
110
- async screenshot() {
111
- if (busy)
112
- throw new Error("AX操作进行中,禁止并发截图");
113
- requireMethods(["getScreenshot"]);
114
- busy = true;
115
- controls.clear();
116
- readable = false;
117
- try {
118
- return await app.getScreenshot({ emit: false });
105
+ await this.#app[operation](value);
119
106
  }
120
- finally {
121
- busy = false;
122
- }
123
- },
124
- };
107
+ else if (operation === "scroll")
108
+ await this.#app.scroll(index, value, 1);
109
+ else
110
+ await this.#app.performSecondaryAction(index, value);
111
+ return await this.handleObserve(read);
112
+ }
113
+ finally {
114
+ this.#busy = false;
115
+ }
116
+ }
117
+ async screenshot() {
118
+ if (this.#busy)
119
+ throw new Error("AX操作进行中,禁止并发截图");
120
+ this.requireMethods(["getScreenshot"]);
121
+ this.#busy = true;
122
+ this.#controls.clear();
123
+ this.#readable = false;
124
+ try {
125
+ return await this.#app.getScreenshot({ emit: false });
126
+ }
127
+ finally {
128
+ this.#busy = false;
129
+ }
130
+ }
131
+ }
132
+ export function createAXHarness(...args) {
133
+ return new AXHarness(...args);
125
134
  }
@@ -0,0 +1,5 @@
1
+ import { readAX } from "./readers-ax-parser.js";
2
+ import { parseAXNodes, createAXTree } from "./readers-ax-tree.js";
3
+ import { projectAXFilepicker, projectAXMedia, projectAXNavigation, projectAXPeople, projectAXPosts, projectAXProfile, projectAXComposer, } from "./readers-ax-views.js";
4
+ // Explicit serialization closure; the same parser runs in Node and the REPL.
5
+ export const axParserCode = `(function(){const parseAXNodes=${parseAXNodes.toString()};const createAXTree=${createAXTree.toString()};const projectAXFilepicker=${projectAXFilepicker.toString()};const projectAXMedia=${projectAXMedia.toString()};const projectAXNavigation=${projectAXNavigation.toString()};const projectAXPeople=${projectAXPeople.toString()};const projectAXPosts=${projectAXPosts.toString()};const projectAXProfile=${projectAXProfile.toString()};const projectAXComposer=${projectAXComposer.toString()};return ${readAX.toString()};})()`;
@@ -46,6 +46,7 @@ export function createAXTree(nodes) {
46
46
  /^文本栏 .*地址和搜索栏/.test(n.label) ||
47
47
  /^按钮 (?:重新加载|新标签页)$/.test(n.label) ||
48
48
  n.label === "窗口" ||
49
+ /, ID: makeKeyAndOrderFront:$/.test(n.label) ||
49
50
  /^标签 \((?:selected|settable)/.test(n.label) ||
50
51
  /^(?:tab|标签)(?: \([^)]*\))? (?:帖子|回复|转帖|媒体|文章|Posts|Replies|Reposts|Media|Articles)(?:,|$)/.test(n.label) ||
51
52
  /^组合框 (?:查询词条|Search)(?:,|$)/.test(n.label) ||