@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,19 +1,29 @@
1
- export function createPlatformCapabilities(dependencies) {
2
- const { missing, browser } = dependencies;
3
- function preflight({ operation, input = {} }) {
4
- const absent = missing(operation);
1
+ export class PlatformCapabilities {
2
+ #missing;
3
+ #browser;
4
+ preflight;
5
+ constructor(dependencies) {
6
+ const { missing, browser } = dependencies;
7
+ this.#missing = missing;
8
+ this.#browser = browser;
9
+ this.preflight = this.handlePreflight.bind(this);
10
+ }
11
+ handlePreflight({ operation, input = {} }) {
12
+ const absent = this.#missing(operation);
5
13
  if (operation === "readCreator" &&
6
- typeof browser.collectCreator !== "function")
14
+ typeof this.#browser.collectCreator !== "function")
7
15
  absent.push("browser.collectCreator");
8
16
  if (input.kind === "follow" || operation === "submitFollow")
9
- absent.push(...(browser.capabilities?.("refreshPerson") ?? ["refreshPerson"]));
17
+ absent.push(...(this.#browser.capabilities?.("refreshPerson") ?? ["refreshPerson"]));
10
18
  if (operation !== "read" && input.assets?.length)
11
- absent.push(...(browser.capabilities?.("attachAssets") ?? ["attachAssets"]));
19
+ absent.push(...(this.#browser.capabilities?.("attachAssets") ?? ["attachAssets"]));
12
20
  return {
13
21
  covered: absent.length === 0,
14
22
  missing: [...new Set(absent)],
15
23
  owner: "scripts/x-harness-browser.mjs",
16
24
  };
17
25
  }
18
- return { preflight };
26
+ }
27
+ export function createPlatformCapabilities(...args) {
28
+ return new PlatformCapabilities(...args);
19
29
  }
@@ -1,7 +1,14 @@
1
1
  import type { PlatformDependencies } from "./platform-dependencies.js";
2
- export declare function createPlatformCollections(dependencies: Pick<PlatformDependencies, "ready" | "observed" | "browser" | "choose">): {
3
- collectSources: ({ processedIds, maxPages, limit, }?: any) => Promise<any>;
2
+ export declare class PlatformCollections {
3
+ #private;
4
+ collectSources: ({ processedIds, maxPages, limit }?: any) => Promise<any>;
4
5
  readCreator: (args: import("./types.js").CreatorOptions) => Promise<any>;
5
6
  readBookmarks: ({ folder, ...window }?: any) => Promise<any>;
6
7
  searchSources: ({ query, limit, maxPages, processedIds, }?: any) => Promise<any>;
7
- };
8
+ constructor(dependencies: Pick<PlatformDependencies, "ready" | "observed" | "browser" | "choose">);
9
+ private handleCollectSources;
10
+ private handleReadCreator;
11
+ private handleReadBookmarks;
12
+ private handleSearchSources;
13
+ }
14
+ export declare function createPlatformCollections(...args: ConstructorParameters<typeof PlatformCollections>): PlatformCollections;
@@ -1,7 +1,25 @@
1
1
  import { xURL, source } from "./platform-rules.js";
2
- export function createPlatformCollections(dependencies) {
3
- const { ready, observed, browser, choose } = dependencies;
4
- async function collectSources({ processedIds = [], maxPages = 3, limit = 1, } = {}) {
2
+ export class PlatformCollections {
3
+ #ready;
4
+ #observed;
5
+ #browser;
6
+ #choose;
7
+ collectSources;
8
+ readCreator;
9
+ readBookmarks;
10
+ searchSources;
11
+ constructor(dependencies) {
12
+ const { ready, observed, browser, choose } = dependencies;
13
+ this.#ready = ready;
14
+ this.#observed = observed;
15
+ this.#browser = browser;
16
+ this.#choose = choose;
17
+ this.collectSources = this.handleCollectSources.bind(this);
18
+ this.readCreator = this.handleReadCreator.bind(this);
19
+ this.readBookmarks = this.handleReadBookmarks.bind(this);
20
+ this.searchSources = this.handleSearchSources.bind(this);
21
+ }
22
+ async handleCollectSources({ processedIds = [], maxPages = 3, limit = 1, } = {}) {
5
23
  if (!Array.isArray(processedIds) ||
6
24
  !Number.isInteger(maxPages) ||
7
25
  maxPages < 1 ||
@@ -13,7 +31,7 @@ export function createPlatformCollections(dependencies) {
13
31
  const seen = new Set(processedIds), sources = [], missing = [];
14
32
  let last, previousIds = "", offset = 0, firstDOMId;
15
33
  for (let page = 0; page < maxPages && sources.length < limit; page++) {
16
- last = await ready({ view: "posts", offset, limit: 5 });
34
+ last = await this.#ready({ view: "posts", offset, limit: 5 });
17
35
  if (page === 0)
18
36
  firstDOMId = last.posts?.[0]?.id;
19
37
  if (last.needsStateCheck || last.alerts?.length)
@@ -39,7 +57,7 @@ export function createPlatformCollections(dependencies) {
39
57
  seen.add(candidate.id);
40
58
  let post = candidate;
41
59
  if (candidate.needsExpansion) {
42
- const target = await observed({
60
+ const target = await this.#observed({
43
61
  view: "posts",
44
62
  targetId: candidate.id,
45
63
  limit: 1,
@@ -48,8 +66,11 @@ export function createPlatformCollections(dependencies) {
48
66
  missing.push("expansion-target-unconfirmed");
49
67
  continue;
50
68
  }
51
- await browser.act({ operation: "expandPost", postId: candidate.id });
52
- const expanded = await ready({
69
+ await this.#browser.act({
70
+ operation: "expandPost",
71
+ postId: candidate.id,
72
+ });
73
+ const expanded = await this.#ready({
53
74
  view: "posts",
54
75
  targetId: candidate.id,
55
76
  limit: 1,
@@ -77,12 +98,12 @@ export function createPlatformCollections(dependencies) {
77
98
  }
78
99
  if (sources.length < limit && page + 1 < maxPages) {
79
100
  // Expansion narrowed the semantic window; restore the list before scrolling.
80
- last = await observed({ view: "posts", offset, limit: 5 });
101
+ last = await this.#observed({ view: "posts", offset, limit: 5 });
81
102
  if (last.moreInDOM)
82
103
  offset += 5;
83
104
  else {
84
- await browser.act({ operation: "nextPosts" });
85
- const advanced = await ready({ view: "posts", limit: 5 });
105
+ await this.#browser.act({ operation: "nextPosts" });
106
+ const advanced = await this.#ready({ view: "posts", limit: 5 });
86
107
  // Retained DOM prefixes include a partial final window too; skip
87
108
  // exactly the consumed cards, while virtualized replacements restart.
88
109
  offset =
@@ -106,14 +127,14 @@ export function createPlatformCollections(dependencies) {
106
127
  : [...new Set([...missing, "bounded-source-window-exhausted"])],
107
128
  };
108
129
  }
109
- async function readCreator(args) {
110
- if (typeof browser.collectCreator !== "function")
130
+ async handleReadCreator(args) {
131
+ if (typeof this.#browser.collectCreator !== "function")
111
132
  return {
112
133
  status: "needsCapability",
113
134
  complete: false,
114
135
  missing: ["browser.collectCreator"],
115
136
  };
116
- const value = await browser.collectCreator(args);
137
+ const value = await this.#browser.collectCreator(args);
117
138
  const absent = [
118
139
  ...new Set([
119
140
  ...(value.missing ?? []),
@@ -130,19 +151,19 @@ export function createPlatformCollections(dependencies) {
130
151
  missing: absent,
131
152
  };
132
153
  }
133
- async function readBookmarks({ folder, ...window } = {}) {
154
+ async handleReadBookmarks({ folder, ...window } = {}) {
134
155
  if (typeof folder !== "string" || !folder.trim())
135
156
  throw new Error("需指定收藏文件夹;不以全部书签冒充命名文件夹");
136
- let nav = await ready({ view: "navigation", targetLabel: folder });
157
+ let nav = await this.#ready({ view: "navigation", targetLabel: folder });
137
158
  const bookmark = (c) => /^(书签|收藏|Bookmarks)$/i.test(c.label);
138
- if (!(await choose(nav, bookmark, { activateSelected: true }))) {
139
- if (await choose(nav, (c) => /^(更多菜单项|More menu items)$/i.test(c.label))) {
140
- nav = await ready({ view: "menu" });
141
- if (!(await choose(nav, bookmark))) {
159
+ if (!(await this.#choose(nav, bookmark, { activateSelected: true }))) {
160
+ if (await this.#choose(nav, (c) => /^(更多菜单项|More menu items)$/i.test(c.label))) {
161
+ nav = await this.#ready({ view: "menu" });
162
+ if (!(await this.#choose(nav, bookmark))) {
142
163
  if (nav.items?.length)
143
- await browser.act({ operation: "dismissMenu" });
144
- nav = await ready({ view: "navigation", targetLabel: folder });
145
- if (!(await choose(nav, (c) => /^(历史|History)$/i.test(c.label))))
164
+ await this.#browser.act({ operation: "dismissMenu" });
165
+ nav = await this.#ready({ view: "navigation", targetLabel: folder });
166
+ if (!(await this.#choose(nav, (c) => /^(历史|History)$/i.test(c.label))))
146
167
  return {
147
168
  status: "needsCapability",
148
169
  owner: "scripts/x-harness-browser.mjs",
@@ -150,34 +171,34 @@ export function createPlatformCollections(dependencies) {
150
171
  };
151
172
  }
152
173
  }
153
- else if (!(await choose(nav, (c) => /^(历史|History)$/i.test(c.label))))
174
+ else if (!(await this.#choose(nav, (c) => /^(历史|History)$/i.test(c.label))))
154
175
  return {
155
176
  status: "needsCapability",
156
177
  owner: "scripts/x-harness-browser.mjs",
157
178
  missing: ["bookmarks-entry-not-observed"],
158
179
  };
159
180
  }
160
- nav = await ready({ view: "navigation", targetLabel: folder });
181
+ nav = await this.#ready({ view: "navigation", targetLabel: folder });
161
182
  const tabs = (nav.controls ?? []).filter((c) => c.role === "tab" && bookmark(c));
162
183
  if (tabs.length === 1 &&
163
184
  (!tabs[0].selected ||
164
185
  !(nav.controls ?? []).some((c) => c.label.split("\n")[0] === folder))) {
165
- await choose(nav, (c) => c === tabs[0], {
186
+ await this.#choose(nav, (c) => c === tabs[0], {
166
187
  activateSelected: true,
167
188
  });
168
- nav = await ready({ view: "navigation", targetLabel: folder });
189
+ nav = await this.#ready({ view: "navigation", targetLabel: folder });
169
190
  }
170
191
  if (!(nav.controls ?? []).some((c) => c.label.split("\n")[0] === folder)) {
171
192
  try {
172
- await browser.act({ operation: "waitForFolder", folder });
193
+ await this.#browser.act({ operation: "waitForFolder", folder });
173
194
  }
174
195
  catch (error) {
175
196
  if (!/timed out|timeout/i.test(error.message))
176
197
  throw error;
177
198
  }
178
- nav = await ready({ view: "navigation", targetLabel: folder });
199
+ nav = await this.#ready({ view: "navigation", targetLabel: folder });
179
200
  }
180
- if (!(await choose(nav, (c) => c.label.split("\n")[0] === folder)))
201
+ if (!(await this.#choose(nav, (c) => c.label.split("\n")[0] === folder)))
181
202
  return {
182
203
  status: "needsCapability",
183
204
  owner: "scripts/x-harness-browser.mjs",
@@ -186,7 +207,7 @@ export function createPlatformCollections(dependencies) {
186
207
  folder,
187
208
  controls: nav.controls,
188
209
  };
189
- const selected = await ready({ view: "navigation" });
210
+ const selected = await this.#ready({ view: "navigation" });
190
211
  if (selected.page === nav.page && !selected.headings?.includes(folder))
191
212
  return {
192
213
  status: "needsDecision",
@@ -195,7 +216,7 @@ export function createPlatformCollections(dependencies) {
195
216
  folder,
196
217
  page: selected.page,
197
218
  };
198
- const value = await collectSources(window);
219
+ const value = await this.handleCollectSources(window);
199
220
  return {
200
221
  ...value,
201
222
  folder,
@@ -206,9 +227,13 @@ export function createPlatformCollections(dependencies) {
206
227
  },
207
228
  };
208
229
  }
209
- async function searchSources({ query, limit = 3, maxPages = 1, processedIds = [], } = {}) {
210
- await browser.act({ operation: "searchSources", query });
211
- const value = await collectSources({ limit, maxPages, processedIds });
230
+ async handleSearchSources({ query, limit = 3, maxPages = 1, processedIds = [], } = {}) {
231
+ await this.#browser.act({ operation: "searchSources", query });
232
+ const value = await this.handleCollectSources({
233
+ limit,
234
+ maxPages,
235
+ processedIds,
236
+ });
212
237
  if (!value.page ||
213
238
  xURL(value.page).pathname !== "/search" ||
214
239
  xURL(value.page).searchParams.get("q") !== query)
@@ -219,5 +244,7 @@ export function createPlatformCollections(dependencies) {
219
244
  };
220
245
  return { ...value, query };
221
246
  }
222
- return { collectSources, readCreator, readBookmarks, searchSources };
247
+ }
248
+ export function createPlatformCollections(...args) {
249
+ return new PlatformCollections(...args);
223
250
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,9 +1,14 @@
1
1
  import type { PlatformDependencies } from "./platform-dependencies.js";
2
- export declare function createPlatformDiagnostics(dependencies: Pick<PlatformDependencies, "clock">): {
2
+ export declare class PlatformDiagnostics {
3
+ #private;
3
4
  followStage: (action: any, stage: any, run: any) => Promise<any>;
4
5
  captureProof: ({ observation }: any) => {
5
6
  followDiagnostics?: any[] | undefined;
6
7
  capturedAt: string;
7
8
  observation: any;
8
9
  };
9
- };
10
+ constructor(dependencies: Pick<PlatformDependencies, "clock">);
11
+ private handleFollowStage;
12
+ private handleCaptureProof;
13
+ }
14
+ export declare function createPlatformDiagnostics(...args: ConstructorParameters<typeof PlatformDiagnostics>): PlatformDiagnostics;
@@ -1,23 +1,31 @@
1
- export function createPlatformDiagnostics(dependencies) {
2
- const { clock } = dependencies;
3
- // Bounded semantic diagnostics travel with existing proof captures; no page dump or second state store.
4
- const followDiagnostics = [];
5
- async function followStage(action, stage, run) {
6
- const started = clock();
1
+ export class PlatformDiagnostics {
2
+ #clock;
3
+ #followDiagnostics;
4
+ followStage;
5
+ captureProof;
6
+ constructor(dependencies) {
7
+ const { clock } = dependencies;
8
+ this.#clock = clock;
9
+ this.#followDiagnostics = [];
10
+ this.followStage = this.handleFollowStage.bind(this);
11
+ this.captureProof = this.handleCaptureProof.bind(this);
12
+ }
13
+ async handleFollowStage(action, stage, run) {
14
+ const started = this.#clock();
7
15
  const event = {
8
16
  actionId: action.id ?? null,
9
17
  handle: action.handle,
10
18
  stage,
11
19
  startedAt: new Date(started).toISOString(),
12
20
  };
13
- followDiagnostics.push(event);
14
- if (followDiagnostics.length > 40)
15
- followDiagnostics.shift();
21
+ this.#followDiagnostics.push(event);
22
+ if (this.#followDiagnostics.length > 40)
23
+ this.#followDiagnostics.shift();
16
24
  try {
17
25
  const value = await run();
18
26
  Object.assign(event, {
19
27
  status: "returned",
20
- durationMs: clock() - started,
28
+ durationMs: this.#clock() - started,
21
29
  });
22
30
  if (value && typeof value === "object")
23
31
  Object.assign(event, {
@@ -34,18 +42,22 @@ export function createPlatformDiagnostics(dependencies) {
34
42
  catch (error) {
35
43
  Object.assign(event, {
36
44
  status: "error",
37
- durationMs: clock() - started,
45
+ durationMs: this.#clock() - started,
38
46
  error: String(error.message ?? error).slice(0, 500),
39
47
  });
40
48
  throw error;
41
49
  }
42
50
  }
43
- const captureProof = ({ observation }) => ({
44
- capturedAt: new Date(clock()).toISOString(),
45
- observation,
46
- ...(followDiagnostics.length
47
- ? { followDiagnostics: structuredClone(followDiagnostics) }
48
- : {}),
49
- });
50
- return { followStage, captureProof };
51
+ handleCaptureProof({ observation }) {
52
+ return {
53
+ capturedAt: new Date(this.#clock()).toISOString(),
54
+ observation,
55
+ ...(this.#followDiagnostics.length
56
+ ? { followDiagnostics: structuredClone(this.#followDiagnostics) }
57
+ : {}),
58
+ };
59
+ }
60
+ }
61
+ export function createPlatformDiagnostics(...args) {
62
+ return new PlatformDiagnostics(...args);
51
63
  }
@@ -1,8 +1,16 @@
1
1
  import type { PlatformDependencies } from "./platform-dependencies.js";
2
- export declare function createPlatformObservation(dependencies: Pick<PlatformDependencies, "browser" | "clock">): {
2
+ export declare class PlatformObservation {
3
+ #private;
3
4
  missing: (operation: any) => any[];
4
5
  observed: (query: any) => Promise<any>;
5
6
  open: (target: any, query: any) => Promise<any>;
6
7
  ready: (query: any) => Promise<any>;
7
8
  choose: (value: any, pattern: any, { activateSelected }?: any) => Promise<any>;
8
- };
9
+ constructor(dependencies: Pick<PlatformDependencies, "browser" | "clock">);
10
+ private handleObserved;
11
+ private handleOpen;
12
+ private handleReady;
13
+ private handleChoose;
14
+ private handleMissing;
15
+ }
16
+ export declare function createPlatformObservation(...args: ConstructorParameters<typeof PlatformObservation>): PlatformObservation;
@@ -1,26 +1,37 @@
1
1
  import { xURL } from "./platform-rules.js";
2
- export function createPlatformObservation(dependencies) {
3
- const { browser, clock } = dependencies;
4
- const methods = ["observe", "act"];
5
- const missing = (operation) => [
6
- ...methods.filter((name) => typeof browser?.[name] !== "function"),
7
- ...(typeof browser?.capabilities === "function"
8
- ? browser.capabilities(operation)
9
- : ["capabilities"]),
10
- ];
11
- async function observed(query) {
12
- const value = await browser.observe(query);
2
+ export class PlatformObservation {
3
+ #browser;
4
+ #clock;
5
+ #methods;
6
+ missing;
7
+ observed;
8
+ open;
9
+ ready;
10
+ choose;
11
+ constructor(dependencies) {
12
+ const { browser, clock } = dependencies;
13
+ this.#browser = browser;
14
+ this.#clock = clock;
15
+ this.#methods = ["observe", "act"];
16
+ this.missing = this.handleMissing.bind(this);
17
+ this.observed = this.handleObserved.bind(this);
18
+ this.open = this.handleOpen.bind(this);
19
+ this.ready = this.handleReady.bind(this);
20
+ this.choose = this.handleChoose.bind(this);
21
+ }
22
+ async handleObserved(query) {
23
+ const value = await this.#browser.observe(query);
13
24
  return {
14
25
  ...value,
15
- observedAt: new Date(clock()).toISOString(),
26
+ observedAt: new Date(this.#clock()).toISOString(),
16
27
  fresh: true,
17
28
  };
18
29
  }
19
- async function open(target, query) {
30
+ async handleOpen(target, query) {
20
31
  const url = xURL(target.url);
21
32
  if (!target.evidenceRef)
22
33
  throw new Error("目标URL缺已观察证据");
23
- let value = await observed(query);
34
+ let value = await this.handleObserved(query);
24
35
  let atTarget = false;
25
36
  try {
26
37
  atTarget = xURL(value.page).href === url.href;
@@ -29,14 +40,14 @@ export function createPlatformObservation(dependencies) {
29
40
  /* Blank/non-X pages require observed navigation. */
30
41
  }
31
42
  if (!atTarget) {
32
- await browser.act({
43
+ await this.#browser.act({
33
44
  operation: "openObserved",
34
45
  url: url.href,
35
46
  evidenceRef: target.evidenceRef,
36
47
  view: query.view,
37
48
  handle: query.handle,
38
49
  });
39
- value = await ready(query);
50
+ value = await this.handleReady(query);
40
51
  }
41
52
  if (!value.page || xURL(value.page).href !== url.href)
42
53
  return {
@@ -46,30 +57,40 @@ export function createPlatformObservation(dependencies) {
46
57
  };
47
58
  return value;
48
59
  }
49
- async function ready(query) {
50
- let value = await observed(query);
60
+ async handleReady(query) {
61
+ let value = await this.handleObserved(query);
51
62
  if (value.needsStateCheck &&
52
63
  !value.alerts?.length &&
53
64
  query.view === "posts") {
54
65
  try {
55
- await browser.act({ operation: "waitForPosts" });
66
+ await this.#browser.act({ operation: "waitForPosts" });
56
67
  }
57
68
  catch (error) {
58
69
  if (!/timed out|timeout/i.test(error.message))
59
70
  throw error;
60
71
  }
61
- value = await observed(query);
72
+ value = await this.handleObserved(query);
62
73
  }
63
74
  return value;
64
75
  }
65
- async function choose(value, pattern, { activateSelected = false } = {}) {
76
+ async handleChoose(value, pattern, { activateSelected = false } = {}) {
66
77
  const controls = (value.controls ?? value.items ?? []).filter((c) => !c.disabled && pattern(c));
67
78
  if (controls.length !== 1)
68
79
  return false;
69
80
  if (controls[0].selected && !activateSelected)
70
81
  return true;
71
- await browser.act({ operation: "openControl", ...controls[0] });
82
+ await this.#browser.act({ operation: "openControl", ...controls[0] });
72
83
  return true;
73
84
  }
74
- return { missing, observed, open, ready, choose };
85
+ handleMissing(operation) {
86
+ return [
87
+ ...this.#methods.filter((name) => typeof this.#browser?.[name] !== "function"),
88
+ ...(typeof this.#browser?.capabilities === "function"
89
+ ? this.#browser.capabilities(operation)
90
+ : ["capabilities"]),
91
+ ];
92
+ }
93
+ }
94
+ export function createPlatformObservation(...args) {
95
+ return new PlatformObservation(...args);
75
96
  }
@@ -1,7 +1,14 @@
1
1
  import type { PlatformDependencies } from "./platform-dependencies.js";
2
- export declare function createPlatformTargets(dependencies: Pick<PlatformDependencies, "observed" | "open" | "browser" | "ready">): {
2
+ export declare class PlatformTargets {
3
+ #private;
3
4
  readAccount: () => Promise<any>;
4
- readFeedback: ({ targets, replyLimit, }?: any) => Promise<any>;
5
+ readFeedback: ({ targets, replyLimit }?: any) => Promise<any>;
5
6
  readPerson: (target: any) => Promise<any>;
6
7
  readPost: (target: any) => Promise<any>;
7
- };
8
+ constructor(dependencies: Pick<PlatformDependencies, "observed" | "open" | "browser" | "ready">);
9
+ private handleReadFeedback;
10
+ private handleReadPerson;
11
+ private handleReadPost;
12
+ private handleReadAccount;
13
+ }
14
+ export declare function createPlatformTargets(...args: ConstructorParameters<typeof PlatformTargets>): PlatformTargets;
@@ -1,15 +1,32 @@
1
1
  import { xURL, sameHandle, handlePattern, source } from "./platform-rules.js";
2
- export function createPlatformTargets(dependencies) {
3
- const { observed, open, browser, ready } = dependencies;
4
- const readAccount = () => observed({ view: "account" });
5
- async function readFeedback({ targets, replyLimit = 3, } = {}) {
2
+ export class PlatformTargets {
3
+ #observed;
4
+ #open;
5
+ #browser;
6
+ #ready;
7
+ readAccount;
8
+ readFeedback;
9
+ readPerson;
10
+ readPost;
11
+ constructor(dependencies) {
12
+ const { observed, open, browser, ready } = dependencies;
13
+ this.#observed = observed;
14
+ this.#open = open;
15
+ this.#browser = browser;
16
+ this.#ready = ready;
17
+ this.readAccount = this.handleReadAccount.bind(this);
18
+ this.readFeedback = this.handleReadFeedback.bind(this);
19
+ this.readPerson = this.handleReadPerson.bind(this);
20
+ this.readPost = this.handleReadPost.bind(this);
21
+ }
22
+ async handleReadFeedback({ targets, replyLimit = 3, } = {}) {
6
23
  if (!Array.isArray(targets) || targets.length < 1 || targets.length > 5)
7
24
  throw new Error("反馈窗口须为1–5个已发布ID");
8
25
  if (!Number.isInteger(replyLimit) || replyLimit < 0 || replyLimit > 3)
9
26
  throw new Error("回复核验窗口须为0–3");
10
27
  const feedback = [];
11
28
  for (const target of targets) {
12
- const value = await readPost(target), post = value.posts?.find((p) => p.id === target.postId);
29
+ const value = await this.handleReadPost(target), post = value.posts?.find((p) => p.id === target.postId);
13
30
  if (value.needsStateCheck || value.alerts?.length || !post) {
14
31
  feedback.push({
15
32
  id: target.postId,
@@ -17,7 +34,7 @@ export function createPlatformTargets(dependencies) {
17
34
  });
18
35
  continue;
19
36
  }
20
- const thread = await observed({ view: "posts", limit: 5 });
37
+ const thread = await this.#observed({ view: "posts", limit: 5 });
21
38
  const replies = [], unconfirmedReplyIds = [];
22
39
  const targetIndex = (thread.posts ?? []).findIndex((p) => p.id === post.id);
23
40
  const candidates = targetIndex < 0
@@ -27,7 +44,7 @@ export function createPlatformTargets(dependencies) {
27
44
  if (!candidate.primary || !candidate.id)
28
45
  continue;
29
46
  const url = new URL(candidate.primary, thread.page).href;
30
- const detail = await open({
47
+ const detail = await this.#open({
31
48
  url,
32
49
  evidenceRef: {
33
50
  page: thread.page,
@@ -68,13 +85,13 @@ export function createPlatformTargets(dependencies) {
68
85
  pageComplete: false,
69
86
  };
70
87
  }
71
- async function readPerson(target) {
88
+ async handleReadPerson(target) {
72
89
  if (!handlePattern.test(target.handle ?? ""))
73
90
  throw new Error("无效已观察handle");
74
91
  if (xURL(target.url).pathname.toLowerCase() !==
75
92
  `/${target.handle}`.toLowerCase())
76
93
  throw new Error("人物URL与handle不一致");
77
- const value = await open(target, {
94
+ const value = await this.#open(target, {
78
95
  view: "person",
79
96
  handle: target.handle,
80
97
  });
@@ -86,11 +103,11 @@ export function createPlatformTargets(dependencies) {
86
103
  missing: ["person-identity-mismatch"],
87
104
  };
88
105
  }
89
- async function readPost(target) {
106
+ async handleReadPost(target) {
90
107
  if (!/^\d+$/.test(target.postId ?? "") ||
91
108
  xURL(target.url).pathname.match(/\/status\/(\d+)$/)?.[1] !== target.postId)
92
109
  throw new Error("帖子URL与公开ID不一致");
93
- let value = await open(target, {
110
+ let value = await this.#open(target, {
94
111
  view: "posts",
95
112
  targetId: target.postId,
96
113
  });
@@ -101,8 +118,11 @@ export function createPlatformTargets(dependencies) {
101
118
  !value.alerts?.length &&
102
119
  posts.length === 1 &&
103
120
  posts[0].needsExpansion) {
104
- await browser.act({ operation: "expandPost", postId: target.postId });
105
- value = await ready({ view: "posts", targetId: target.postId });
121
+ await this.#browser.act({
122
+ operation: "expandPost",
123
+ postId: target.postId,
124
+ });
125
+ value = await this.#ready({ view: "posts", targetId: target.postId });
106
126
  posts = (value.posts ?? []).filter((p) => p.id === target.postId);
107
127
  }
108
128
  const missing = [...(value.missing ?? [])];
@@ -125,5 +145,10 @@ export function createPlatformTargets(dependencies) {
125
145
  missing,
126
146
  };
127
147
  }
128
- return { readAccount, readFeedback, readPerson, readPost };
148
+ handleReadAccount() {
149
+ return this.#observed({ view: "account" });
150
+ }
151
+ }
152
+ export function createPlatformTargets(...args) {
153
+ return new PlatformTargets(...args);
129
154
  }
@@ -1,8 +1,16 @@
1
1
  import type { PlatformDependencies } from "./platform-dependencies.js";
2
- export declare function createPlatformWriting(dependencies: Pick<PlatformDependencies, "observed" | "browser" | "followStage" | "readPerson" | "open" | "clock" | "readPost">): {
2
+ export declare class PlatformWriting {
3
+ #private;
3
4
  compose: (action: any) => Promise<any>;
4
5
  findSubmission: (action: any) => Promise<any>;
5
6
  submitFollow: (action: any) => Promise<any>;
6
7
  submitPost: (action: any) => Promise<any>;
7
8
  submitReply: (action: any) => Promise<any>;
8
- };
9
+ constructor(dependencies: Pick<PlatformDependencies, "observed" | "browser" | "followStage" | "readPerson" | "open" | "clock" | "readPost">);
10
+ private handleCompose;
11
+ private handleFindSubmission;
12
+ private handleSubmitFollow;
13
+ private handleSubmitPost;
14
+ private handleSubmitReply;
15
+ }
16
+ export declare function createPlatformWriting(...args: ConstructorParameters<typeof PlatformWriting>): PlatformWriting;