@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.
- package/LICENSE +231 -6
- package/README.md +17 -3
- package/SOURCE.md +9 -0
- package/cli/main.js +232 -188
- package/dist/api.d.ts +56 -2
- package/dist/api.js +178 -75
- package/dist/boundary.js +1 -0
- package/dist/browser-common.d.ts +11 -0
- package/dist/browser-common.js +34 -18
- package/dist/browser-dom-capabilities.d.ts +9 -0
- package/dist/browser-dom-capabilities.js +131 -0
- package/dist/browser-dom-dependencies.d.ts +16 -0
- package/dist/browser-dom-dependencies.js +1 -0
- package/dist/browser-dom-editing.d.ts +166 -0
- package/dist/browser-dom-editing.js +418 -0
- package/dist/browser-dom-navigation.d.ts +18 -0
- package/dist/browser-dom-navigation.js +181 -0
- package/dist/browser-dom-network.d.ts +1 -0
- package/dist/browser-dom-network.js +128 -0
- package/dist/browser-dom-submission.d.ts +7 -0
- package/dist/browser-dom-submission.js +101 -0
- package/dist/browser-dom.d.ts +14 -2
- package/dist/browser-dom.js +120 -824
- package/dist/browser-native.d.ts +14 -1
- package/dist/browser-native.js +180 -143
- package/dist/browser.js +6 -2
- package/dist/creator-native.d.ts +3 -0
- package/dist/creator-native.js +113 -0
- package/dist/creator.js +1 -1
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.js +10 -0
- package/dist/growth-batch.d.ts +8 -0
- package/dist/growth-batch.js +190 -0
- package/dist/growth-constants.d.ts +5 -0
- package/dist/growth-constants.js +13 -0
- package/dist/growth-dependencies.d.ts +33 -0
- package/dist/growth-dependencies.js +1 -0
- package/dist/growth-execution.d.ts +17 -0
- package/dist/growth-execution.js +346 -0
- package/dist/growth-parallel.d.ts +8 -0
- package/dist/growth-parallel.js +332 -0
- package/dist/growth-plan.d.ts +9 -0
- package/dist/growth-plan.js +229 -0
- package/dist/growth-policy.d.ts +18 -0
- package/dist/growth-policy.js +208 -0
- package/dist/growth-publishing.d.ts +10 -0
- package/dist/growth-publishing.js +266 -0
- package/dist/growth-reading.d.ts +8 -0
- package/dist/growth-reading.js +157 -0
- package/dist/growth-recovery.d.ts +9 -0
- package/dist/growth-recovery.js +187 -0
- package/dist/growth-state.d.ts +16 -0
- package/dist/growth-state.js +220 -0
- package/dist/growth-tasks.d.ts +20 -0
- package/dist/growth-tasks.js +164 -0
- package/dist/growth.d.ts +26 -1
- package/dist/growth.js +211 -1908
- package/dist/platform-capabilities.d.ts +8 -0
- package/dist/platform-capabilities.js +29 -0
- package/dist/platform-collections.d.ts +14 -0
- package/dist/platform-collections.js +250 -0
- package/dist/platform-dependencies.d.ts +43 -0
- package/dist/platform-dependencies.js +1 -0
- package/dist/platform-diagnostics.d.ts +14 -0
- package/dist/platform-diagnostics.js +63 -0
- package/dist/platform-observation.d.ts +16 -0
- package/dist/platform-observation.js +96 -0
- package/dist/platform-rules.d.ts +17 -0
- package/dist/platform-rules.js +41 -0
- package/dist/platform-targets.d.ts +14 -0
- package/dist/platform-targets.js +154 -0
- package/dist/platform-writing.d.ts +16 -0
- package/dist/platform-writing.js +202 -0
- package/dist/platform.d.ts +23 -2
- package/dist/platform.js +125 -678
- package/dist/readers-actions.d.ts +2 -0
- package/dist/readers-actions.js +66 -0
- package/dist/readers-ax-host.d.ts +12 -0
- package/dist/readers-ax-host.js +134 -0
- package/dist/readers-ax-parser.d.ts +1 -0
- package/dist/readers-ax-parser.js +69 -0
- package/dist/readers-ax-program.d.ts +1 -0
- package/dist/readers-ax-program.js +5 -0
- package/dist/readers-ax-tree.d.ts +19 -0
- package/dist/readers-ax-tree.js +111 -0
- package/dist/readers-ax-views.d.ts +87 -0
- package/dist/readers-ax-views.js +247 -0
- package/dist/readers-cli.d.ts +6 -0
- package/dist/readers-cli.js +79 -0
- package/dist/readers-composer.d.ts +2 -0
- package/dist/readers-composer.js +131 -0
- package/dist/readers-navigation.d.ts +2 -0
- package/dist/readers-navigation.js +66 -0
- package/dist/readers-posts.d.ts +2 -0
- package/dist/readers-posts.js +141 -0
- package/dist/readers-profile.d.ts +3 -0
- package/dist/readers-profile.js +157 -0
- package/dist/readers.d.ts +9 -20
- package/dist/readers.js +9 -1045
- package/dist/results.js +1 -1
- package/dist/round-state.js +20 -3
- package/dist/store.d.ts +22 -4
- package/dist/store.js +173 -151
- package/dist/types.d.ts +10 -0
- package/dist/types.js +1 -0
- package/generated/observation.mjs +3 -2
- package/package.json +23 -9
- package/skill/SKILL.md +17 -36
- package/skill/references/api.md +7 -34
- package/skill/references/reads.md +23 -0
- package/skill/references/recovery.md +25 -0
- package/skill/references/setup.md +23 -0
- package/skill/references/writes.md +51 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PlatformDependencies } from "./platform-dependencies.js";
|
|
2
|
+
export declare class PlatformCapabilities {
|
|
3
|
+
#private;
|
|
4
|
+
preflight: ({ operation, input }: any) => any;
|
|
5
|
+
constructor(dependencies: Pick<PlatformDependencies, "missing" | "browser">);
|
|
6
|
+
private handlePreflight;
|
|
7
|
+
}
|
|
8
|
+
export declare function createPlatformCapabilities(...args: ConstructorParameters<typeof PlatformCapabilities>): PlatformCapabilities;
|
|
@@ -0,0 +1,29 @@
|
|
|
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);
|
|
13
|
+
if (operation === "readCreator" &&
|
|
14
|
+
typeof this.#browser.collectCreator !== "function")
|
|
15
|
+
absent.push("browser.collectCreator");
|
|
16
|
+
if (input.kind === "follow" || operation === "submitFollow")
|
|
17
|
+
absent.push(...(this.#browser.capabilities?.("refreshPerson") ?? ["refreshPerson"]));
|
|
18
|
+
if (operation !== "read" && input.assets?.length)
|
|
19
|
+
absent.push(...(this.#browser.capabilities?.("attachAssets") ?? ["attachAssets"]));
|
|
20
|
+
return {
|
|
21
|
+
covered: absent.length === 0,
|
|
22
|
+
missing: [...new Set(absent)],
|
|
23
|
+
owner: "scripts/x-harness-browser.mjs",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function createPlatformCapabilities(...args) {
|
|
28
|
+
return new PlatformCapabilities(...args);
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PlatformDependencies } from "./platform-dependencies.js";
|
|
2
|
+
export declare class PlatformCollections {
|
|
3
|
+
#private;
|
|
4
|
+
collectSources: ({ processedIds, maxPages, limit }?: any) => Promise<any>;
|
|
5
|
+
readCreator: (args: import("./types.js").CreatorOptions) => Promise<any>;
|
|
6
|
+
readBookmarks: ({ folder, ...window }?: any) => Promise<any>;
|
|
7
|
+
searchSources: ({ query, limit, maxPages, processedIds, }?: any) => Promise<any>;
|
|
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;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { xURL, source } from "./platform-rules.js";
|
|
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, } = {}) {
|
|
23
|
+
if (!Array.isArray(processedIds) ||
|
|
24
|
+
!Number.isInteger(maxPages) ||
|
|
25
|
+
maxPages < 1 ||
|
|
26
|
+
maxPages > 10 ||
|
|
27
|
+
!Number.isInteger(limit) ||
|
|
28
|
+
limit < 1 ||
|
|
29
|
+
limit > 5)
|
|
30
|
+
throw new Error("来源窗口须为1–5,页面上限1–10及已处理ID数组");
|
|
31
|
+
const seen = new Set(processedIds), sources = [], missing = [];
|
|
32
|
+
let last, previousIds = "", offset = 0, firstDOMId;
|
|
33
|
+
for (let page = 0; page < maxPages && sources.length < limit; page++) {
|
|
34
|
+
last = await this.#ready({ view: "posts", offset, limit: 5 });
|
|
35
|
+
if (page === 0)
|
|
36
|
+
firstDOMId = last.posts?.[0]?.id;
|
|
37
|
+
if (last.needsStateCheck || last.alerts?.length)
|
|
38
|
+
return {
|
|
39
|
+
...last,
|
|
40
|
+
sources,
|
|
41
|
+
complete: false,
|
|
42
|
+
missing: ["source-page-unconfirmed"],
|
|
43
|
+
};
|
|
44
|
+
if (offset && !last.posts?.length) {
|
|
45
|
+
missing.push("source-window-not-yet-rendered");
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
const ids = (last.posts ?? []).map((p) => p.id).join(",");
|
|
49
|
+
if (page && ids === previousIds) {
|
|
50
|
+
missing.push("pagination-did-not-advance");
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
previousIds = ids;
|
|
54
|
+
for (const candidate of last.posts ?? []) {
|
|
55
|
+
if (!candidate.id || seen.has(candidate.id))
|
|
56
|
+
continue;
|
|
57
|
+
seen.add(candidate.id);
|
|
58
|
+
let post = candidate;
|
|
59
|
+
if (candidate.needsExpansion) {
|
|
60
|
+
const target = await this.#observed({
|
|
61
|
+
view: "posts",
|
|
62
|
+
targetId: candidate.id,
|
|
63
|
+
limit: 1,
|
|
64
|
+
});
|
|
65
|
+
if (target.posts?.length !== 1) {
|
|
66
|
+
missing.push("expansion-target-unconfirmed");
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
await this.#browser.act({
|
|
70
|
+
operation: "expandPost",
|
|
71
|
+
postId: candidate.id,
|
|
72
|
+
});
|
|
73
|
+
const expanded = await this.#ready({
|
|
74
|
+
view: "posts",
|
|
75
|
+
targetId: candidate.id,
|
|
76
|
+
limit: 1,
|
|
77
|
+
});
|
|
78
|
+
if (expanded.needsStateCheck || expanded.alerts?.length)
|
|
79
|
+
return {
|
|
80
|
+
...expanded,
|
|
81
|
+
sources,
|
|
82
|
+
complete: false,
|
|
83
|
+
missing: ["expanded-source-unconfirmed"],
|
|
84
|
+
};
|
|
85
|
+
post = expanded.posts?.[0];
|
|
86
|
+
}
|
|
87
|
+
if (!post ||
|
|
88
|
+
post.needsExpansion ||
|
|
89
|
+
!post.body ||
|
|
90
|
+
!post.author ||
|
|
91
|
+
!Number.isFinite(Date.parse(post.datetime))) {
|
|
92
|
+
missing.push(`incomplete-source:${candidate.id}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
sources.push(source(post, last.page));
|
|
96
|
+
if (sources.length >= limit)
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
if (sources.length < limit && page + 1 < maxPages) {
|
|
100
|
+
// Expansion narrowed the semantic window; restore the list before scrolling.
|
|
101
|
+
last = await this.#observed({ view: "posts", offset, limit: 5 });
|
|
102
|
+
if (last.moreInDOM)
|
|
103
|
+
offset += 5;
|
|
104
|
+
else {
|
|
105
|
+
await this.#browser.act({ operation: "nextPosts" });
|
|
106
|
+
const advanced = await this.#ready({ view: "posts", limit: 5 });
|
|
107
|
+
// Retained DOM prefixes include a partial final window too; skip
|
|
108
|
+
// exactly the consumed cards, while virtualized replacements restart.
|
|
109
|
+
offset =
|
|
110
|
+
firstDOMId && advanced.posts?.[0]?.id === firstDOMId
|
|
111
|
+
? offset + (last.posts?.length ?? 0)
|
|
112
|
+
: 0;
|
|
113
|
+
firstDOMId = advanced.posts?.[0]?.id;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
page: last?.page,
|
|
119
|
+
accountHandle: last?.accountHandle,
|
|
120
|
+
observedAt: last?.observedAt,
|
|
121
|
+
fresh: true,
|
|
122
|
+
sources,
|
|
123
|
+
complete: sources.length >= limit,
|
|
124
|
+
pageComplete: false,
|
|
125
|
+
missing: sources.length >= limit
|
|
126
|
+
? []
|
|
127
|
+
: [...new Set([...missing, "bounded-source-window-exhausted"])],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async handleReadCreator(args) {
|
|
131
|
+
if (typeof this.#browser.collectCreator !== "function")
|
|
132
|
+
return {
|
|
133
|
+
status: "needsCapability",
|
|
134
|
+
complete: false,
|
|
135
|
+
missing: ["browser.collectCreator"],
|
|
136
|
+
};
|
|
137
|
+
const value = await this.#browser.collectCreator(args);
|
|
138
|
+
const absent = [
|
|
139
|
+
...new Set([
|
|
140
|
+
...(value.missing ?? []),
|
|
141
|
+
...(value.profile?.alerts ?? []),
|
|
142
|
+
...(value.meta?.missing ?? []),
|
|
143
|
+
]),
|
|
144
|
+
];
|
|
145
|
+
return {
|
|
146
|
+
...value,
|
|
147
|
+
sources: value.posts,
|
|
148
|
+
page: `https://x.com/${args.handle}`,
|
|
149
|
+
pageComplete: false,
|
|
150
|
+
complete: value.complete === true && !absent.length && !value.alerts?.length,
|
|
151
|
+
missing: absent,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async handleReadBookmarks({ folder, ...window } = {}) {
|
|
155
|
+
if (typeof folder !== "string" || !folder.trim())
|
|
156
|
+
throw new Error("需指定收藏文件夹;不以全部书签冒充命名文件夹");
|
|
157
|
+
let nav = await this.#ready({ view: "navigation", targetLabel: folder });
|
|
158
|
+
const bookmark = (c) => /^(书签|收藏|Bookmarks)$/i.test(c.label);
|
|
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))) {
|
|
163
|
+
if (nav.items?.length)
|
|
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))))
|
|
167
|
+
return {
|
|
168
|
+
status: "needsCapability",
|
|
169
|
+
owner: "scripts/x-harness-browser.mjs",
|
|
170
|
+
missing: ["bookmarks-entry-not-observed"],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (!(await this.#choose(nav, (c) => /^(历史|History)$/i.test(c.label))))
|
|
175
|
+
return {
|
|
176
|
+
status: "needsCapability",
|
|
177
|
+
owner: "scripts/x-harness-browser.mjs",
|
|
178
|
+
missing: ["bookmarks-entry-not-observed"],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
nav = await this.#ready({ view: "navigation", targetLabel: folder });
|
|
182
|
+
const tabs = (nav.controls ?? []).filter((c) => c.role === "tab" && bookmark(c));
|
|
183
|
+
if (tabs.length === 1 &&
|
|
184
|
+
(!tabs[0].selected ||
|
|
185
|
+
!(nav.controls ?? []).some((c) => c.label.split("\n")[0] === folder))) {
|
|
186
|
+
await this.#choose(nav, (c) => c === tabs[0], {
|
|
187
|
+
activateSelected: true,
|
|
188
|
+
});
|
|
189
|
+
nav = await this.#ready({ view: "navigation", targetLabel: folder });
|
|
190
|
+
}
|
|
191
|
+
if (!(nav.controls ?? []).some((c) => c.label.split("\n")[0] === folder)) {
|
|
192
|
+
try {
|
|
193
|
+
await this.#browser.act({ operation: "waitForFolder", folder });
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (!/timed out|timeout/i.test(error.message))
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
nav = await this.#ready({ view: "navigation", targetLabel: folder });
|
|
200
|
+
}
|
|
201
|
+
if (!(await this.#choose(nav, (c) => c.label.split("\n")[0] === folder)))
|
|
202
|
+
return {
|
|
203
|
+
status: "needsCapability",
|
|
204
|
+
owner: "scripts/x-harness-browser.mjs",
|
|
205
|
+
page: nav.page,
|
|
206
|
+
missing: ["named-bookmark-folder-not-observed"],
|
|
207
|
+
folder,
|
|
208
|
+
controls: nav.controls,
|
|
209
|
+
};
|
|
210
|
+
const selected = await this.#ready({ view: "navigation" });
|
|
211
|
+
if (selected.page === nav.page && !selected.headings?.includes(folder))
|
|
212
|
+
return {
|
|
213
|
+
status: "needsDecision",
|
|
214
|
+
complete: false,
|
|
215
|
+
missing: ["folder-selection-unconfirmed"],
|
|
216
|
+
folder,
|
|
217
|
+
page: selected.page,
|
|
218
|
+
};
|
|
219
|
+
const value = await this.handleCollectSources(window);
|
|
220
|
+
return {
|
|
221
|
+
...value,
|
|
222
|
+
folder,
|
|
223
|
+
folderEvidence: {
|
|
224
|
+
entryPage: nav.page,
|
|
225
|
+
selectedPage: selected.page,
|
|
226
|
+
name: folder,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
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
|
+
});
|
|
237
|
+
if (!value.page ||
|
|
238
|
+
xURL(value.page).pathname !== "/search" ||
|
|
239
|
+
xURL(value.page).searchParams.get("q") !== query)
|
|
240
|
+
return {
|
|
241
|
+
...value,
|
|
242
|
+
complete: false,
|
|
243
|
+
missing: ["search-query-unconfirmed"],
|
|
244
|
+
};
|
|
245
|
+
return { ...value, query };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
export function createPlatformCollections(...args) {
|
|
249
|
+
return new PlatformCollections(...args);
|
|
250
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CreatorOptions } from "./types.js";
|
|
2
|
+
import type { BoundaryRecord } from "./boundary.js";
|
|
3
|
+
export interface PlatformDependencies {
|
|
4
|
+
browser: BoundaryRecord;
|
|
5
|
+
clock: () => number;
|
|
6
|
+
followStage: (action: any, stage: any, run: any) => Promise<any>;
|
|
7
|
+
missing: (operation: any) => any[];
|
|
8
|
+
observed: (query: any) => Promise<any>;
|
|
9
|
+
open: (target: any, query: any) => Promise<any>;
|
|
10
|
+
ready: (query: any) => Promise<any>;
|
|
11
|
+
choose: (value: any, pattern: any, { activateSelected }?: any) => Promise<any>;
|
|
12
|
+
source: (post: any, page: any) => {
|
|
13
|
+
id: any;
|
|
14
|
+
author: any;
|
|
15
|
+
datetime: any;
|
|
16
|
+
url: string | null;
|
|
17
|
+
body: any;
|
|
18
|
+
images: any;
|
|
19
|
+
media: any;
|
|
20
|
+
links: any;
|
|
21
|
+
needsExpansion: any;
|
|
22
|
+
missing: any[];
|
|
23
|
+
};
|
|
24
|
+
collectSources: ({ processedIds, maxPages, limit }?: any) => Promise<any>;
|
|
25
|
+
compose: (action: any) => Promise<any>;
|
|
26
|
+
preflight: ({ operation, input }: any) => any;
|
|
27
|
+
readCreator: (args: CreatorOptions) => Promise<any>;
|
|
28
|
+
readAccount: () => Promise<any>;
|
|
29
|
+
readBookmarks: ({ folder, ...window }?: any) => Promise<any>;
|
|
30
|
+
searchSources: ({ query, limit, maxPages, processedIds, }?: any) => Promise<any>;
|
|
31
|
+
readFeedback: ({ targets, replyLimit }?: any) => Promise<any>;
|
|
32
|
+
readPerson: (target: any) => Promise<any>;
|
|
33
|
+
readPost: (target: any) => Promise<any>;
|
|
34
|
+
findSubmission: (action: any) => Promise<any>;
|
|
35
|
+
submitFollow: (action: any) => Promise<any>;
|
|
36
|
+
submitPost: (action: any) => Promise<any>;
|
|
37
|
+
submitReply: (action: any) => Promise<any>;
|
|
38
|
+
captureProof: ({ observation }: any) => {
|
|
39
|
+
followDiagnostics?: any[] | undefined;
|
|
40
|
+
capturedAt: string;
|
|
41
|
+
observation: any;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PlatformDependencies } from "./platform-dependencies.js";
|
|
2
|
+
export declare class PlatformDiagnostics {
|
|
3
|
+
#private;
|
|
4
|
+
followStage: (action: any, stage: any, run: any) => Promise<any>;
|
|
5
|
+
captureProof: ({ observation }: any) => {
|
|
6
|
+
followDiagnostics?: any[] | undefined;
|
|
7
|
+
capturedAt: string;
|
|
8
|
+
observation: any;
|
|
9
|
+
};
|
|
10
|
+
constructor(dependencies: Pick<PlatformDependencies, "clock">);
|
|
11
|
+
private handleFollowStage;
|
|
12
|
+
private handleCaptureProof;
|
|
13
|
+
}
|
|
14
|
+
export declare function createPlatformDiagnostics(...args: ConstructorParameters<typeof PlatformDiagnostics>): PlatformDiagnostics;
|
|
@@ -0,0 +1,63 @@
|
|
|
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();
|
|
15
|
+
const event = {
|
|
16
|
+
actionId: action.id ?? null,
|
|
17
|
+
handle: action.handle,
|
|
18
|
+
stage,
|
|
19
|
+
startedAt: new Date(started).toISOString(),
|
|
20
|
+
};
|
|
21
|
+
this.#followDiagnostics.push(event);
|
|
22
|
+
if (this.#followDiagnostics.length > 40)
|
|
23
|
+
this.#followDiagnostics.shift();
|
|
24
|
+
try {
|
|
25
|
+
const value = await run();
|
|
26
|
+
Object.assign(event, {
|
|
27
|
+
status: "returned",
|
|
28
|
+
durationMs: this.#clock() - started,
|
|
29
|
+
});
|
|
30
|
+
if (value && typeof value === "object")
|
|
31
|
+
Object.assign(event, {
|
|
32
|
+
page: value.page,
|
|
33
|
+
accountHandle: value.accountHandle,
|
|
34
|
+
relationship: value.relationship,
|
|
35
|
+
relationshipControls: value.relationshipControls,
|
|
36
|
+
needsStateCheck: value.needsStateCheck,
|
|
37
|
+
alerts: value.alerts,
|
|
38
|
+
fresh: value.fresh,
|
|
39
|
+
});
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
Object.assign(event, {
|
|
44
|
+
status: "error",
|
|
45
|
+
durationMs: this.#clock() - started,
|
|
46
|
+
error: String(error.message ?? error).slice(0, 500),
|
|
47
|
+
});
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
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);
|
|
63
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PlatformDependencies } from "./platform-dependencies.js";
|
|
2
|
+
export declare class PlatformObservation {
|
|
3
|
+
#private;
|
|
4
|
+
missing: (operation: any) => any[];
|
|
5
|
+
observed: (query: any) => Promise<any>;
|
|
6
|
+
open: (target: any, query: any) => Promise<any>;
|
|
7
|
+
ready: (query: any) => Promise<any>;
|
|
8
|
+
choose: (value: any, pattern: any, { activateSelected }?: any) => Promise<any>;
|
|
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;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { xURL } from "./platform-rules.js";
|
|
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);
|
|
24
|
+
return {
|
|
25
|
+
...value,
|
|
26
|
+
observedAt: new Date(this.#clock()).toISOString(),
|
|
27
|
+
fresh: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async handleOpen(target, query) {
|
|
31
|
+
const url = xURL(target.url);
|
|
32
|
+
if (!target.evidenceRef)
|
|
33
|
+
throw new Error("目标URL缺已观察证据");
|
|
34
|
+
let value = await this.handleObserved(query);
|
|
35
|
+
let atTarget = false;
|
|
36
|
+
try {
|
|
37
|
+
atTarget = xURL(value.page).href === url.href;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* Blank/non-X pages require observed navigation. */
|
|
41
|
+
}
|
|
42
|
+
if (!atTarget) {
|
|
43
|
+
await this.#browser.act({
|
|
44
|
+
operation: "openObserved",
|
|
45
|
+
url: url.href,
|
|
46
|
+
evidenceRef: target.evidenceRef,
|
|
47
|
+
view: query.view,
|
|
48
|
+
handle: query.handle,
|
|
49
|
+
});
|
|
50
|
+
value = await this.handleReady(query);
|
|
51
|
+
}
|
|
52
|
+
if (!value.page || xURL(value.page).href !== url.href)
|
|
53
|
+
return {
|
|
54
|
+
...value,
|
|
55
|
+
needsStateCheck: true,
|
|
56
|
+
missing: ["target-page-mismatch"],
|
|
57
|
+
};
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
async handleReady(query) {
|
|
61
|
+
let value = await this.handleObserved(query);
|
|
62
|
+
if (value.needsStateCheck &&
|
|
63
|
+
!value.alerts?.length &&
|
|
64
|
+
query.view === "posts") {
|
|
65
|
+
try {
|
|
66
|
+
await this.#browser.act({ operation: "waitForPosts" });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (!/timed out|timeout/i.test(error.message))
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
value = await this.handleObserved(query);
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
async handleChoose(value, pattern, { activateSelected = false } = {}) {
|
|
77
|
+
const controls = (value.controls ?? value.items ?? []).filter((c) => !c.disabled && pattern(c));
|
|
78
|
+
if (controls.length !== 1)
|
|
79
|
+
return false;
|
|
80
|
+
if (controls[0].selected && !activateSelected)
|
|
81
|
+
return true;
|
|
82
|
+
await this.#browser.act({ operation: "openControl", ...controls[0] });
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
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);
|
|
96
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const handlePattern: RegExp;
|
|
2
|
+
export declare function xURL(value: any): any;
|
|
3
|
+
export declare const sameHandle: (a: any, b: any) => boolean;
|
|
4
|
+
export declare const sameText: (a: any, b: any) => boolean;
|
|
5
|
+
export declare const samePublishedText: (a: any, b: any) => boolean;
|
|
6
|
+
export declare const source: (post: any, page: any) => {
|
|
7
|
+
id: any;
|
|
8
|
+
author: any;
|
|
9
|
+
datetime: any;
|
|
10
|
+
url: string | null;
|
|
11
|
+
body: any;
|
|
12
|
+
images: any;
|
|
13
|
+
media: any;
|
|
14
|
+
links: any;
|
|
15
|
+
needsExpansion: any;
|
|
16
|
+
missing: any[];
|
|
17
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const handlePattern = /^[A-Za-z0-9_]{1,15}$/;
|
|
2
|
+
export function xURL(value) {
|
|
3
|
+
const url = new URL(/^x\.com\//.test(value ?? "") ? `https://${value}` : value);
|
|
4
|
+
if (url.protocol !== "https:" ||
|
|
5
|
+
url.hostname !== "x.com" ||
|
|
6
|
+
url.username ||
|
|
7
|
+
url.password ||
|
|
8
|
+
url.port)
|
|
9
|
+
throw new Error("只接受已观察的https://x.com目标");
|
|
10
|
+
return url;
|
|
11
|
+
}
|
|
12
|
+
export const sameHandle = (a, b) => typeof a === "string" &&
|
|
13
|
+
typeof b === "string" &&
|
|
14
|
+
a.toLowerCase() === b.toLowerCase();
|
|
15
|
+
export const sameText = (a, b) => typeof a === "string" &&
|
|
16
|
+
typeof b === "string" &&
|
|
17
|
+
a.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n") ===
|
|
18
|
+
b.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n");
|
|
19
|
+
export const samePublishedText = (a, b) => {
|
|
20
|
+
const rendered = (value) => typeof value === "string"
|
|
21
|
+
? value
|
|
22
|
+
.replace(/(https?:\/\/[^\s…]+)…(?=\s|$)/g, "$1")
|
|
23
|
+
.replace(/(https?:\/\/[A-Za-z0-9.-]+(?::\d+)?)\/(?=[\s`]|$)/g, "$1")
|
|
24
|
+
: value;
|
|
25
|
+
return sameText(rendered(a), rendered(b));
|
|
26
|
+
};
|
|
27
|
+
export const source = (post, page) => ({
|
|
28
|
+
id: post.id,
|
|
29
|
+
author: post.author,
|
|
30
|
+
datetime: post.datetime,
|
|
31
|
+
url: post.primary ? new URL(post.primary, page).href : null,
|
|
32
|
+
body: post.body,
|
|
33
|
+
images: post.images ?? [],
|
|
34
|
+
media: post.media ?? [],
|
|
35
|
+
links: post.links ?? [],
|
|
36
|
+
needsExpansion: post.needsExpansion,
|
|
37
|
+
missing: [
|
|
38
|
+
...(post.missing ?? []),
|
|
39
|
+
...(!post.body ? ["exact-body-unconfirmed"] : []),
|
|
40
|
+
],
|
|
41
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PlatformDependencies } from "./platform-dependencies.js";
|
|
2
|
+
export declare class PlatformTargets {
|
|
3
|
+
#private;
|
|
4
|
+
readAccount: () => Promise<any>;
|
|
5
|
+
readFeedback: ({ targets, replyLimit }?: any) => Promise<any>;
|
|
6
|
+
readPerson: (target: any) => Promise<any>;
|
|
7
|
+
readPost: (target: any) => Promise<any>;
|
|
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;
|