@nextclaw/x-harness 0.2.52 → 0.2.54
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/SOURCE.md +2 -2
- package/dist/browser-dom-editing.js +1 -1
- package/dist/browser-dom-submission.d.ts +1 -0
- package/dist/browser-dom-submission.js +33 -2
- package/dist/platform-collections.d.ts +2 -0
- package/dist/platform-collections.js +61 -17
- package/dist/readers-posts.js +11 -0
- package/dist/results.js +8 -0
- package/dist/types.d.ts +9 -0
- package/generated/observation.mjs +11 -0
- package/package.json +2 -2
- package/skill/references/reads.md +7 -5
package/SOURCE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Corresponding source
|
|
2
2
|
|
|
3
|
-
Version 0.2.
|
|
3
|
+
Version 0.2.54, commit a9b2bb0d7399463a8dc259e433e3a2bd31dc9bc7.
|
|
4
4
|
|
|
5
|
-
Complete source, build scripts and dependencies: https://github.com/Peiiii/x-harness/tree/
|
|
5
|
+
Complete source, build scripts and dependencies: https://github.com/Peiiii/x-harness/tree/a9b2bb0d7399463a8dc259e433e3a2bd31dc9bc7
|
|
6
6
|
|
|
7
7
|
Clone this revision, run npm ci, npm run bootstrap and npm run release:build.
|
|
8
8
|
|
|
@@ -198,7 +198,7 @@ export class DOMEditing {
|
|
|
198
198
|
.locator(`a[href=${JSON.stringify(primary)}]`)
|
|
199
199
|
.filter({ visible: true }));
|
|
200
200
|
this.#session.current = null;
|
|
201
|
-
await anchor.press("
|
|
201
|
+
await anchor.press("PageDown");
|
|
202
202
|
return { completed: true };
|
|
203
203
|
return { completed: false, loc, submittedEditor };
|
|
204
204
|
}
|
|
@@ -3,5 +3,6 @@ export declare class DOMSubmission {
|
|
|
3
3
|
#private;
|
|
4
4
|
constructor(dependencies: Pick<DOMDependencies, "submitAX" | "session" | "tab" | "observe" | "unique" | "locator">);
|
|
5
5
|
finishDOMAction(command: any, prepared: any, query: any, result: any): Promise<void>;
|
|
6
|
+
private waitSubmittedEditor;
|
|
6
7
|
}
|
|
7
8
|
export declare function createDOMSubmission(...args: ConstructorParameters<typeof DOMSubmission>): (command: any, prepared: any, query: any, result: any) => Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { xURL } from "./platform-rules.js";
|
|
1
|
+
import { xURL, sameHandle, sameText } from "./platform-rules.js";
|
|
2
2
|
import { valueOf } from "./browser-common.js";
|
|
3
3
|
export class DOMSubmission {
|
|
4
4
|
#submitAX;
|
|
@@ -29,6 +29,7 @@ export class DOMSubmission {
|
|
|
29
29
|
: null;
|
|
30
30
|
let nativeSubmitIndex;
|
|
31
31
|
if (this.#submitAX &&
|
|
32
|
+
command.activation !== "keyboard" &&
|
|
32
33
|
["submitPost", "submitReply"].includes(command.operation)) {
|
|
33
34
|
const native = await this.#submitAX.observe({ mode: "composer" });
|
|
34
35
|
const editors = native.evidence?.filter((e) => /^(?:文本输入区(?:域)?|文本区域|文本栏) /.test(e.label)) ?? [];
|
|
@@ -88,7 +89,7 @@ export class DOMSubmission {
|
|
|
88
89
|
// Inline replies keep the editor mounted. Wait for this submitted text to clear before
|
|
89
90
|
// receipt navigation/reload can interrupt the actual submission request.
|
|
90
91
|
if (submittedEditor)
|
|
91
|
-
await
|
|
92
|
+
await this.waitSubmittedEditor(submittedEditor, result);
|
|
92
93
|
if (["submitPost", "submitReply"].includes(command.operation)) {
|
|
93
94
|
try {
|
|
94
95
|
await this.#tab.playwright.waitForLoadState({
|
|
@@ -104,6 +105,36 @@ export class DOMSubmission {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
}
|
|
108
|
+
async waitSubmittedEditor(editor, submitted) {
|
|
109
|
+
const deadline = Date.now() + 10000;
|
|
110
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
111
|
+
const remaining = deadline - Date.now();
|
|
112
|
+
if (remaining <= 0)
|
|
113
|
+
throw Error("submitted-editor-clear-timeout");
|
|
114
|
+
try {
|
|
115
|
+
await editor.waitFor({ state: "hidden", timeoutMs: remaining });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (!/timed out|timeout|deadline exceeded/i.test(error.message) ||
|
|
120
|
+
attempt === 3 ||
|
|
121
|
+
Date.now() >= deadline)
|
|
122
|
+
throw error;
|
|
123
|
+
// Resume only the wait, never the submission or a navigation that could interrupt it.
|
|
124
|
+
const current = await this.#observe({ view: "composer" });
|
|
125
|
+
if (current.alerts?.length ||
|
|
126
|
+
!current.accountHandle ||
|
|
127
|
+
!submitted.accountHandle ||
|
|
128
|
+
!sameHandle(current.accountHandle, submitted.accountHandle) ||
|
|
129
|
+
xURL(current.page).href !== xURL(submitted.page).href ||
|
|
130
|
+
(typeof current.editorText === "string" &&
|
|
131
|
+
current.editorText.trim() &&
|
|
132
|
+
(!sameText(current.editorText, submitted.editorText) ||
|
|
133
|
+
current.replyToId !== submitted.replyToId)))
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
107
138
|
}
|
|
108
139
|
export function createDOMSubmission(...args) {
|
|
109
140
|
return new DOMSubmission(...args).finishDOMAction;
|
|
@@ -7,8 +7,10 @@ export declare class PlatformCollections {
|
|
|
7
7
|
searchSources: ({ query, limit, maxPages, processedIds, }?: any) => Promise<any>;
|
|
8
8
|
constructor(dependencies: Pick<PlatformDependencies, "ready" | "observed" | "browser" | "choose">);
|
|
9
9
|
private handleCollectSources;
|
|
10
|
+
private advanceSourceWindow;
|
|
10
11
|
private handleReadCreator;
|
|
11
12
|
private handleReadBookmarks;
|
|
13
|
+
private resumeBookmarks;
|
|
12
14
|
private confirmBookmarkFolder;
|
|
13
15
|
private bookmarkDiscovery;
|
|
14
16
|
private waitForBookmarkLabel;
|
|
@@ -117,28 +117,19 @@ export class PlatformCollections {
|
|
|
117
117
|
if (last.moreInDOM)
|
|
118
118
|
offset += 5;
|
|
119
119
|
else {
|
|
120
|
-
await this
|
|
121
|
-
|
|
122
|
-
if (scrolled.needsStateCheck || scrolled.alerts?.length)
|
|
120
|
+
const advanced = await this.advanceSourceWindow(last, seen);
|
|
121
|
+
if (advanced.needsStateCheck || advanced.alerts?.length)
|
|
123
122
|
return {
|
|
124
|
-
...
|
|
123
|
+
...advanced,
|
|
125
124
|
sources,
|
|
126
125
|
complete: false,
|
|
127
126
|
missing: ["source-page-unconfirmed"],
|
|
128
127
|
};
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
operation: "waitForPosts",
|
|
134
|
-
excludeIds: [...seen],
|
|
135
|
-
});
|
|
128
|
+
if (advanced.paginationBoundary) {
|
|
129
|
+
last = advanced;
|
|
130
|
+
missing.push("stable-rendered-tail");
|
|
131
|
+
break;
|
|
136
132
|
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
if (!/timed out|timeout/i.test(error.message))
|
|
139
|
-
throw error;
|
|
140
|
-
}
|
|
141
|
-
const advanced = await this.#ready({ view: "posts", limit: 5 });
|
|
142
133
|
// Retained DOM prefixes include a partial final window too; skip
|
|
143
134
|
// exactly the consumed cards, while virtualized replacements restart.
|
|
144
135
|
offset =
|
|
@@ -157,11 +148,49 @@ export class PlatformCollections {
|
|
|
157
148
|
sources,
|
|
158
149
|
complete: sources.length >= limit,
|
|
159
150
|
pageComplete: false,
|
|
151
|
+
...(last?.paginationBoundary
|
|
152
|
+
? { paginationBoundary: last.paginationBoundary }
|
|
153
|
+
: {}),
|
|
160
154
|
missing: sources.length >= limit
|
|
161
155
|
? []
|
|
162
156
|
: [...new Set([...missing, "bounded-source-window-exhausted"])],
|
|
163
157
|
};
|
|
164
158
|
}
|
|
159
|
+
async advanceSourceWindow(previous, seen) {
|
|
160
|
+
const renderedIds = previous.renderedIds ?? [...seen];
|
|
161
|
+
await this.#browser.act({ operation: "nextPosts" });
|
|
162
|
+
let advanced = await this.#ready({ view: "posts", limit: 5 });
|
|
163
|
+
if (advanced.needsStateCheck || advanced.alerts?.length)
|
|
164
|
+
return advanced;
|
|
165
|
+
let timedOut = false;
|
|
166
|
+
try {
|
|
167
|
+
// Wait for a newly rendered card, even if that source was processed on an
|
|
168
|
+
// earlier run. Global processed IDs do not describe the current DOM window.
|
|
169
|
+
await this.#browser.act({
|
|
170
|
+
operation: "waitForPosts",
|
|
171
|
+
excludeIds: renderedIds,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (!/timed out|timeout/i.test(error.message))
|
|
176
|
+
throw error;
|
|
177
|
+
timedOut = true;
|
|
178
|
+
}
|
|
179
|
+
advanced = await this.#ready({ view: "posts", limit: 5 });
|
|
180
|
+
if (timedOut &&
|
|
181
|
+
advanced.atRenderedTail &&
|
|
182
|
+
!advanced.loading &&
|
|
183
|
+
advanced.firstRenderedId === previous.firstRenderedId &&
|
|
184
|
+
advanced.renderedArticles === previous.renderedArticles)
|
|
185
|
+
advanced.paginationBoundary = {
|
|
186
|
+
kind: "stable-rendered-tail",
|
|
187
|
+
lastId: advanced.renderedIds?.at(-1),
|
|
188
|
+
renderedCount: advanced.renderedArticles,
|
|
189
|
+
waitMs: 5000,
|
|
190
|
+
scopeComplete: false,
|
|
191
|
+
};
|
|
192
|
+
return advanced;
|
|
193
|
+
}
|
|
165
194
|
async handleReadCreator(args) {
|
|
166
195
|
if (typeof this.#browser.collectCreator !== "function")
|
|
167
196
|
return {
|
|
@@ -186,7 +215,7 @@ export class PlatformCollections {
|
|
|
186
215
|
missing: absent,
|
|
187
216
|
};
|
|
188
217
|
}
|
|
189
|
-
async handleReadBookmarks({ folder, discoverOnly = false, ...window } = {}) {
|
|
218
|
+
async handleReadBookmarks({ folder, discoverOnly = false, resume = false, ...window } = {}) {
|
|
190
219
|
if (!discoverOnly && (typeof folder !== "string" || !folder.trim()))
|
|
191
220
|
throw new Error("需指定收藏文件夹;不以全部书签冒充命名文件夹");
|
|
192
221
|
let nav = await this.#ready({
|
|
@@ -194,6 +223,10 @@ export class PlatformCollections {
|
|
|
194
223
|
targetLabel: folder,
|
|
195
224
|
bookmarkNavigation: true,
|
|
196
225
|
});
|
|
226
|
+
if (resume && discoverOnly)
|
|
227
|
+
throw Error("bookmark-resume-discovery-conflict");
|
|
228
|
+
if (resume)
|
|
229
|
+
return this.resumeBookmarks(folder, window, nav.page);
|
|
197
230
|
if (discoverOnly && nav.bookmarkFolders?.length)
|
|
198
231
|
return this.bookmarkDiscovery(nav);
|
|
199
232
|
const bookmark = (c) => /^(书签|收藏|Bookmarks)$/i.test(c.label);
|
|
@@ -308,6 +341,17 @@ export class PlatformCollections {
|
|
|
308
341
|
},
|
|
309
342
|
};
|
|
310
343
|
}
|
|
344
|
+
async resumeBookmarks(folder, window, entryPage) {
|
|
345
|
+
const selected = await this.confirmBookmarkFolder(folder);
|
|
346
|
+
if (selected.status)
|
|
347
|
+
return selected;
|
|
348
|
+
const value = await this.handleCollectSources(window);
|
|
349
|
+
return {
|
|
350
|
+
...value,
|
|
351
|
+
folder,
|
|
352
|
+
folderEvidence: { entryPage, selectedPage: selected.page, name: folder },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
311
355
|
async confirmBookmarkFolder(folder) {
|
|
312
356
|
let selected = await this.#ready({ view: "navigation" });
|
|
313
357
|
if (!selected.headings?.includes(folder)) {
|
package/dist/readers-posts.js
CHANGED
|
@@ -102,6 +102,17 @@ export function readPosts({ offset = 0, limit = 5, targetId = "", } = {}) {
|
|
|
102
102
|
?.getAttribute("href")
|
|
103
103
|
?.match(/\/status\/(\d+)/)?.[1] ?? null,
|
|
104
104
|
renderedArticles: all.length,
|
|
105
|
+
renderedIds: all
|
|
106
|
+
.map((a) => primaryTime(a)
|
|
107
|
+
?.closest("a")
|
|
108
|
+
?.getAttribute("href")
|
|
109
|
+
?.match(/\/status\/(\d+)/)?.[1])
|
|
110
|
+
.filter(Boolean),
|
|
111
|
+
atRenderedTail: Boolean(document.scrollingElement) &&
|
|
112
|
+
document.scrollingElement.scrollTop +
|
|
113
|
+
document.scrollingElement.clientHeight >=
|
|
114
|
+
document.scrollingElement.scrollHeight - 2,
|
|
115
|
+
loading: Array.from(document.querySelectorAll('[role="progressbar"]')).some(visible),
|
|
105
116
|
matched: candidates.length,
|
|
106
117
|
offset,
|
|
107
118
|
moreInDOM: candidates.length > offset + posts.length,
|
package/dist/results.js
CHANGED
|
@@ -162,6 +162,14 @@ export function readResult(operation, value, mode) {
|
|
|
162
162
|
"moreInDOM",
|
|
163
163
|
]);
|
|
164
164
|
if (operation === "readBookmarks") {
|
|
165
|
+
if (raw.paginationBoundary)
|
|
166
|
+
out.paginationBoundary = pick(raw.paginationBoundary, [
|
|
167
|
+
"kind",
|
|
168
|
+
"lastId",
|
|
169
|
+
"renderedCount",
|
|
170
|
+
"waitMs",
|
|
171
|
+
"scopeComplete",
|
|
172
|
+
]);
|
|
165
173
|
if (raw.folders !== undefined)
|
|
166
174
|
out.folders = strings(raw.folders);
|
|
167
175
|
if (typeof raw.folder === "string")
|
package/dist/types.d.ts
CHANGED
|
@@ -116,6 +116,13 @@ export interface ReadResult {
|
|
|
116
116
|
selectedPage?: string;
|
|
117
117
|
name?: string;
|
|
118
118
|
};
|
|
119
|
+
paginationBoundary?: {
|
|
120
|
+
kind?: string;
|
|
121
|
+
lastId?: string;
|
|
122
|
+
renderedCount?: number;
|
|
123
|
+
waitMs?: number;
|
|
124
|
+
scopeComplete?: boolean;
|
|
125
|
+
};
|
|
119
126
|
sources?: PostEvidence[];
|
|
120
127
|
post?: PostEvidence;
|
|
121
128
|
context?: PostEvidence[];
|
|
@@ -198,6 +205,8 @@ export interface IncomingConversationResult {
|
|
|
198
205
|
}
|
|
199
206
|
export interface BookmarkOptions extends WindowOptions {
|
|
200
207
|
folder?: string;
|
|
208
|
+
/** Continue the currently observed named-folder window without reselecting it. */
|
|
209
|
+
resume?: boolean;
|
|
201
210
|
/** Discover names only; never read all bookmarks as a named folder. */
|
|
202
211
|
discoverOnly?: boolean;
|
|
203
212
|
}
|
|
@@ -103,6 +103,17 @@ export const readXObservation = (query) => {
|
|
|
103
103
|
?.getAttribute("href")
|
|
104
104
|
?.match(/\/status\/(\d+)/)?.[1] ?? null,
|
|
105
105
|
renderedArticles: all.length,
|
|
106
|
+
renderedIds: all
|
|
107
|
+
.map((a) => primaryTime(a)
|
|
108
|
+
?.closest("a")
|
|
109
|
+
?.getAttribute("href")
|
|
110
|
+
?.match(/\/status\/(\d+)/)?.[1])
|
|
111
|
+
.filter(Boolean),
|
|
112
|
+
atRenderedTail: Boolean(document.scrollingElement) &&
|
|
113
|
+
document.scrollingElement.scrollTop +
|
|
114
|
+
document.scrollingElement.clientHeight >=
|
|
115
|
+
document.scrollingElement.scrollHeight - 2,
|
|
116
|
+
loading: Array.from(document.querySelectorAll('[role="progressbar"]')).some(visible),
|
|
106
117
|
matched: candidates.length,
|
|
107
118
|
offset,
|
|
108
119
|
moreInDOM: candidates.length > offset + posts.length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/x-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.54",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed X task harness with compact business results and explicit diagnostics.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "a9b2bb0d7399463a8dc259e433e3a2bd31dc9bc7"
|
|
46
46
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
| --- | --- | --- |
|
|
5
5
|
| readIncomingReplies | none | processedIds, limit (5), maxPages (3), mode |
|
|
6
6
|
| readCreator | handle | target (10;1–50), maxScrolls, navigate, expand, mode |
|
|
7
|
-
| readBookmarks | folder (observed name), or discoverOnly:true | processedIds, limit (1–5), maxPages (1–10), mode |
|
|
7
|
+
| readBookmarks | folder (observed name), or discoverOnly:true | processedIds, limit (1–5), maxPages (1–10), resume, mode |
|
|
8
8
|
| searchSources | query | processedIds, limit (1–5), maxPages (1–10), mode |
|
|
9
9
|
| readPost | target: {postId,url,evidenceRef} | includeContext, resolveQuote, mode |
|
|
10
10
|
| readPerson | target: {handle,url,evidenceRef} | mode |
|
|
@@ -13,16 +13,18 @@
|
|
|
13
13
|
| inspectAccount | none | mode |
|
|
14
14
|
| inspectRound | taskId (original batch ID) | none |
|
|
15
15
|
|
|
16
|
-
Targets
|
|
16
|
+
Targets require proof. Native creator: selected profile, navigate:false; relative time unknown; stop on page change.
|
|
17
|
+
|
|
18
|
+
Bookmarks resume:true continues the bound folder; rendered tail ≠ full proof.
|
|
17
19
|
|
|
18
20
|
observed ≠ field completeness; pageComplete:false = scope. View media before describing; preserve target text; keep other reads local.
|
|
19
21
|
|
|
20
22
|
`readIncomingReplies`: ledger account; skip settled/intent/unknown and processedIds across dates. readPost(includeContext:true) for ancestors; missing context blocks review. Dedup publicly.
|
|
21
23
|
|
|
22
|
-
Incoming
|
|
24
|
+
Incoming: `readIncomingConversations({targets?,...window})` combines discovery/context (≤5); no preceding `readIncomingReplies`. Preflight/source failures remain visible. Returns ancestorPath, nodes, confirmed edges, unknown parents and attempts; rootCandidateId is only the earliest loaded ancestor. Missing context blocks review; never guess roots/edges or inbox clearance. Use `readIncomingReplies` only when the candidate list alone is the goal; it excludes settled/intent/unknown + cross-date processedIds.
|
|
23
25
|
|
|
24
26
|
`resolveQuote:true` follows one observed card and returns `post.quoteSource` only after exact destination/body/author/time binding.
|
|
25
27
|
|
|
26
|
-
`readPeople`: observed follower/following
|
|
28
|
+
`readPeople`: observed follower/following; offset reuses cells; advance scrolls a fresh unique handle. Full bio/blue/relationship; dedup; unknowns need profiles. No Native.
|
|
27
29
|
|
|
28
|
-
Discovery
|
|
30
|
+
Discovery: names only.
|