@nextclaw/x-harness 0.2.66 → 0.2.68
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/api.d.ts +3 -1
- package/dist/api.js +14 -3
- package/dist/browser-dom-capabilities.js +6 -1
- package/dist/browser-dom-navigation.d.ts +2 -0
- package/dist/browser-dom-navigation.js +7 -0
- package/dist/browser-native.js +7 -1
- package/dist/growth-execution.d.ts +1 -0
- package/dist/growth-execution.js +19 -2
- package/dist/growth-recovery.js +3 -2
- package/dist/growth-tasks.d.ts +1 -0
- package/dist/growth-tasks.js +12 -1
- package/dist/growth.d.ts +1 -0
- package/dist/growth.js +1 -0
- package/dist/platform-collections.d.ts +2 -0
- package/dist/platform-collections.js +142 -0
- package/dist/platform-dependencies.d.ts +2 -0
- package/dist/platform-rules.js +7 -1
- package/dist/platform-writing.js +4 -3
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +5 -1
- package/dist/publication-time.d.ts +5 -0
- package/dist/publication-time.js +18 -0
- package/dist/reply-discovery.d.ts +12 -0
- package/dist/reply-discovery.js +167 -0
- package/dist/results.js +33 -0
- package/dist/types.d.ts +42 -0
- package/package.json +2 -2
- package/skill/references/batch-discovery.md +2 -0
- package/skill/references/reads.md +10 -7
- package/skill/references/receipt-recovery.md +7 -0
- package/skill/references/recovery.md +1 -1
package/SOURCE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Corresponding source
|
|
2
2
|
|
|
3
|
-
Version 0.2.
|
|
3
|
+
Version 0.2.68, commit 9d121f1f8b24f7c1082d8f17a3f6ec473d45d534.
|
|
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/9d121f1f8b24f7c1082d8f17a3f6ec473d45d534
|
|
6
6
|
|
|
7
7
|
Clone this revision, run npm ci, npm run bootstrap and npm run release:build.
|
|
8
8
|
|
package/dist/api.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export declare class BusinessHarness implements XGrowthHarness {
|
|
|
22
22
|
results: never[];
|
|
23
23
|
reason: string;
|
|
24
24
|
} | {
|
|
25
|
-
status: "
|
|
25
|
+
status: "verified" | "needsDecision";
|
|
26
26
|
results: ActionResult[];
|
|
27
27
|
reason?: undefined;
|
|
28
28
|
}>;
|
|
@@ -81,7 +81,9 @@ export declare class BusinessHarness implements XGrowthHarness {
|
|
|
81
81
|
evidenceRef?: undefined;
|
|
82
82
|
diagnostic?: undefined;
|
|
83
83
|
}>;
|
|
84
|
+
discoverReplyCandidates(args: import("./types.js").ReplyDiscoveryOptions): Promise<import("./types.js").ReplyDiscoveryResult>;
|
|
84
85
|
searchSources(args: import("./types.js").SearchOptions): Promise<ReadResult>;
|
|
86
|
+
readExploreRecommendations(args?: import("./types.js").ExploreRecommendationOptions): Promise<ReadResult>;
|
|
85
87
|
readPost({ target, mode, includeContext, includeThread, resolveQuote, }: {
|
|
86
88
|
includeContext?: boolean;
|
|
87
89
|
includeThread?: boolean;
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { ReplyDiscovery } from "./reply-discovery.js";
|
|
1
2
|
import { sameHandle, samePublishedText } from "./platform-rules.js";
|
|
3
|
+
import { publicationTime } from "./publication-time.js";
|
|
2
4
|
import { FollowbackAudit } from "./followback-audit.js";
|
|
3
5
|
import { IncomingReactions } from "./incoming-reactions.js";
|
|
4
6
|
import { createGrowthHarness } from "./growth.js";
|
|
@@ -32,7 +34,10 @@ export class BusinessHarness {
|
|
|
32
34
|
this.readPost = this.readPost.bind(this);
|
|
33
35
|
this.readIncomingConversations = this.readIncomingConversations.bind(this);
|
|
34
36
|
this.readIncomingReplies = this.readIncomingReplies.bind(this);
|
|
37
|
+
this.discoverReplyCandidates = this.discoverReplyCandidates.bind(this);
|
|
35
38
|
this.searchSources = this.searchSources.bind(this);
|
|
39
|
+
this.readExploreRecommendations =
|
|
40
|
+
this.readExploreRecommendations.bind(this);
|
|
36
41
|
this.readBookmarks = this.readBookmarks.bind(this);
|
|
37
42
|
this.readCreator = this.readCreator.bind(this);
|
|
38
43
|
const { store, platform } = dependencies;
|
|
@@ -366,7 +371,7 @@ export class BusinessHarness {
|
|
|
366
371
|
const phrases = pending
|
|
367
372
|
.map((a) => '"' + a.text.slice(0, 80).replace(/["\\]/g, " ") + '"')
|
|
368
373
|
.join(" OR ");
|
|
369
|
-
const query = `from:${pending[0].accountHandle} (${phrases})
|
|
374
|
+
const query = `from:${pending[0].accountHandle} (${phrases}) filter:replies`;
|
|
370
375
|
let found = await this.searchSources({
|
|
371
376
|
query,
|
|
372
377
|
limit: 5,
|
|
@@ -391,8 +396,8 @@ export class BusinessHarness {
|
|
|
391
396
|
p.url?.toLowerCase() ===
|
|
392
397
|
`https://x.com/${action.accountHandle}/status/${p.id}`.toLowerCase() &&
|
|
393
398
|
!(action.beforeIds ?? []).includes(p.id) &&
|
|
394
|
-
|
|
395
|
-
|
|
399
|
+
publicationTime(p) >= Date.parse(action.intentAt) &&
|
|
400
|
+
publicationTime(p) <= this.#clock() + 5000);
|
|
396
401
|
if (matches.length !== 1)
|
|
397
402
|
continue;
|
|
398
403
|
let bound = false;
|
|
@@ -596,9 +601,15 @@ export class BusinessHarness {
|
|
|
596
601
|
pageComplete: false,
|
|
597
602
|
};
|
|
598
603
|
}
|
|
604
|
+
discoverReplyCandidates(args) {
|
|
605
|
+
return new ReplyDiscovery((request) => this.searchSources(request), this.#store, this.#clock).run(args);
|
|
606
|
+
}
|
|
599
607
|
searchSources(args) {
|
|
600
608
|
return this.read("searchSources", args, args.mode);
|
|
601
609
|
}
|
|
610
|
+
readExploreRecommendations(args = {}) {
|
|
611
|
+
return this.read("readExploreRecommendations", args, args.mode);
|
|
612
|
+
}
|
|
602
613
|
readPost({ target, mode, includeContext, includeThread, resolveQuote, }) {
|
|
603
614
|
return this.read("readPost", { ...target, includeContext, includeThread, resolveQuote }, mode);
|
|
604
615
|
}
|
|
@@ -45,7 +45,12 @@ export class DOMCapabilities {
|
|
|
45
45
|
if (operation === "readCreator") {
|
|
46
46
|
return this.creatorCapabilities(base, operation);
|
|
47
47
|
}
|
|
48
|
-
if ([
|
|
48
|
+
if ([
|
|
49
|
+
"readBookmarks",
|
|
50
|
+
"searchSources",
|
|
51
|
+
"readExploreRecommendations",
|
|
52
|
+
"readFeedback",
|
|
53
|
+
].includes(operation)) {
|
|
49
54
|
return this.collectionCapabilities(base, operation);
|
|
50
55
|
}
|
|
51
56
|
if (operation !== "read")
|
|
@@ -3,6 +3,7 @@ export declare class DOMNavigation {
|
|
|
3
3
|
#private;
|
|
4
4
|
openQuote: (command: any) => Promise<any>;
|
|
5
5
|
searchSources: (command: any) => Promise<void>;
|
|
6
|
+
openExploreRecommendations: (command: any) => Promise<void>;
|
|
6
7
|
waitForFolder: (command: any) => Promise<void>;
|
|
7
8
|
waitForPosts: (command: any) => Promise<void>;
|
|
8
9
|
openObserved: (command: any) => Promise<void>;
|
|
@@ -11,6 +12,7 @@ export declare class DOMNavigation {
|
|
|
11
12
|
constructor(dependencies: Pick<DOMDependencies, "session" | "tab" | "observe">);
|
|
12
13
|
private handleOpenQuote;
|
|
13
14
|
private handleSearchSources;
|
|
15
|
+
private handleOpenExploreRecommendations;
|
|
14
16
|
private handleWaitForFolder;
|
|
15
17
|
private handleWaitForPosts;
|
|
16
18
|
private handleOpenObserved;
|
|
@@ -5,6 +5,7 @@ export class DOMNavigation {
|
|
|
5
5
|
#observe;
|
|
6
6
|
openQuote;
|
|
7
7
|
searchSources;
|
|
8
|
+
openExploreRecommendations;
|
|
8
9
|
waitForFolder;
|
|
9
10
|
waitForPosts;
|
|
10
11
|
openObserved;
|
|
@@ -17,6 +18,8 @@ export class DOMNavigation {
|
|
|
17
18
|
this.#observe = observe;
|
|
18
19
|
this.openQuote = this.handleOpenQuote.bind(this);
|
|
19
20
|
this.searchSources = this.handleSearchSources.bind(this);
|
|
21
|
+
this.openExploreRecommendations =
|
|
22
|
+
this.handleOpenExploreRecommendations.bind(this);
|
|
20
23
|
this.waitForFolder = this.handleWaitForFolder.bind(this);
|
|
21
24
|
this.waitForPosts = this.handleWaitForPosts.bind(this);
|
|
22
25
|
this.openObserved = this.handleOpenObserved.bind(this);
|
|
@@ -70,6 +73,10 @@ export class DOMNavigation {
|
|
|
70
73
|
await this.#tab.goto(destination);
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
76
|
+
async handleOpenExploreRecommendations() {
|
|
77
|
+
this.#session.current = null;
|
|
78
|
+
await this.#tab.goto("https://x.com/explore");
|
|
79
|
+
}
|
|
73
80
|
async handleWaitForFolder(command) {
|
|
74
81
|
if (command.selected) {
|
|
75
82
|
await this.#tab.playwright
|
package/dist/browser-native.js
CHANGED
|
@@ -170,7 +170,13 @@ export class NativeXBrowser {
|
|
|
170
170
|
capabilities(operation) {
|
|
171
171
|
if (operation === "readPeople")
|
|
172
172
|
return ["native-people-blue-evidence"];
|
|
173
|
-
if ([
|
|
173
|
+
if ([
|
|
174
|
+
"readBookmarks",
|
|
175
|
+
"searchSources",
|
|
176
|
+
"readExploreRecommendations",
|
|
177
|
+
"readFeedback",
|
|
178
|
+
"likePost",
|
|
179
|
+
].includes(operation))
|
|
174
180
|
return [`native.${operation}`];
|
|
175
181
|
const names = operation === "read"
|
|
176
182
|
? ["getAXState"]
|
|
@@ -12,6 +12,7 @@ export declare class GrowthExecution {
|
|
|
12
12
|
private checkExecutionIdentity;
|
|
13
13
|
private actionIsActionable;
|
|
14
14
|
private admitAction;
|
|
15
|
+
private readSubmissionWithURLContinuation;
|
|
15
16
|
private handleExecute;
|
|
16
17
|
}
|
|
17
18
|
export declare function createGrowthExecution(...args: ConstructorParameters<typeof GrowthExecution>): GrowthExecution;
|
package/dist/growth-execution.js
CHANGED
|
@@ -176,7 +176,7 @@ export class GrowthExecution {
|
|
|
176
176
|
if (action.kind !== "follow") {
|
|
177
177
|
let baseline;
|
|
178
178
|
try {
|
|
179
|
-
baseline = await this
|
|
179
|
+
baseline = await this.readSubmissionWithURLContinuation({ ...action, intentAt: this.#stamp() }, { phase: "baseline" });
|
|
180
180
|
}
|
|
181
181
|
catch (error) {
|
|
182
182
|
return {
|
|
@@ -202,6 +202,23 @@ export class GrowthExecution {
|
|
|
202
202
|
}
|
|
203
203
|
return { beforeIds };
|
|
204
204
|
}
|
|
205
|
+
async readSubmissionWithURLContinuation(action, context) {
|
|
206
|
+
try {
|
|
207
|
+
return await this.#platform.findSubmission(action, context);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
const preparedBaseline = context?.phase === "baseline";
|
|
211
|
+
const observedReceipt = !context &&
|
|
212
|
+
action.observedPublicURL &&
|
|
213
|
+
this.#evidenceExists(action.observedPublicEvidenceRef);
|
|
214
|
+
if (action.kind !== "reply" ||
|
|
215
|
+
!(preparedBaseline || observedReceipt) ||
|
|
216
|
+
!/Timed out waiting for URL/.test(String(error?.message ?? "")))
|
|
217
|
+
throw error;
|
|
218
|
+
// Continue only this read; the existing admission and receipt gates stay intact.
|
|
219
|
+
return this.#platform.findSubmission(action, context);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
205
222
|
async handleExecute(snapshot, action) {
|
|
206
223
|
const executor = this.#executor;
|
|
207
224
|
const recovering = ["intent", "unknown"].includes(action.status);
|
|
@@ -215,7 +232,7 @@ export class GrowthExecution {
|
|
|
215
232
|
if (recovering) {
|
|
216
233
|
let observation;
|
|
217
234
|
try {
|
|
218
|
-
observation = await this
|
|
235
|
+
observation = await this.readSubmissionWithURLContinuation(action);
|
|
219
236
|
}
|
|
220
237
|
catch (error) {
|
|
221
238
|
observation = {
|
package/dist/growth-recovery.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publicationTime } from "./publication-time.js";
|
|
1
2
|
import { xURL, matchesReviewedAssets } from "./platform-rules.js";
|
|
2
3
|
import { matchesRenderedLinks } from "./source-link-recovery.js";
|
|
3
4
|
import { same } from "./growth-constants.js";
|
|
@@ -126,8 +127,8 @@ export class GrowthRecovery {
|
|
|
126
127
|
matchesRenderedLinks(action.text, p, sourceLinks, this.#evidenceExists)) &&
|
|
127
128
|
!p.needsExpansion &&
|
|
128
129
|
!(action.beforeIds ?? []).includes(p.id) &&
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
publicationTime(p) >= Date.parse(action.intentAt) &&
|
|
131
|
+
publicationTime(p) <= this.#clock() + 5000 &&
|
|
131
132
|
matchesReviewedAssets(p, action.assets));
|
|
132
133
|
});
|
|
133
134
|
Object.assign(observation, {
|
package/dist/growth-tasks.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare class GrowthTasks {
|
|
|
10
10
|
readPerson(args: any): Promise<any>;
|
|
11
11
|
readBookmarks(args: any): Promise<any>;
|
|
12
12
|
searchSources(args: any): Promise<any>;
|
|
13
|
+
readExploreRecommendations(args?: any): Promise<any>;
|
|
13
14
|
readFeedback(args: any): Promise<any>;
|
|
14
15
|
runGrowthRound(args: any): Promise<any>;
|
|
15
16
|
resumeGrowthRound(args: any): Promise<any>;
|
package/dist/growth-tasks.js
CHANGED
|
@@ -19,6 +19,8 @@ export class GrowthTasks {
|
|
|
19
19
|
this.runGrowthRound = this.runGrowthRound.bind(this);
|
|
20
20
|
this.readFeedback = this.readFeedback.bind(this);
|
|
21
21
|
this.searchSources = this.searchSources.bind(this);
|
|
22
|
+
this.readExploreRecommendations =
|
|
23
|
+
this.readExploreRecommendations.bind(this);
|
|
22
24
|
this.readBookmarks = this.readBookmarks.bind(this);
|
|
23
25
|
this.readPerson = this.readPerson.bind(this);
|
|
24
26
|
this.readPeople = this.readPeople.bind(this);
|
|
@@ -40,7 +42,13 @@ export class GrowthTasks {
|
|
|
40
42
|
}
|
|
41
43
|
preflight({ operation, input = {} }) {
|
|
42
44
|
const executor = this.#executor;
|
|
43
|
-
if ([
|
|
45
|
+
if ([
|
|
46
|
+
"readPeople",
|
|
47
|
+
"readBookmarks",
|
|
48
|
+
"searchSources",
|
|
49
|
+
"readExploreRecommendations",
|
|
50
|
+
"readFeedback",
|
|
51
|
+
].includes(operation))
|
|
44
52
|
return this.#platform.preflight({ operation, input });
|
|
45
53
|
if (["inspectRound", "verifySubmission"].includes(operation))
|
|
46
54
|
return this.#capability("read", input);
|
|
@@ -110,6 +118,9 @@ export class GrowthTasks {
|
|
|
110
118
|
searchSources(args) {
|
|
111
119
|
return this.#readTask("searchSources", args);
|
|
112
120
|
}
|
|
121
|
+
readExploreRecommendations(args = {}) {
|
|
122
|
+
return this.#readTask("readExploreRecommendations", args);
|
|
123
|
+
}
|
|
113
124
|
readFeedback(args) {
|
|
114
125
|
return this.#readTask("readFeedback", args);
|
|
115
126
|
}
|
package/dist/growth.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare class GrowthHarness {
|
|
|
13
13
|
readPerson: (args: any) => Promise<any>;
|
|
14
14
|
readBookmarks: (args: any) => Promise<any>;
|
|
15
15
|
searchSources: (args: any) => Promise<any>;
|
|
16
|
+
readExploreRecommendations: (args?: any) => Promise<any>;
|
|
16
17
|
readFeedback: (args: any) => Promise<any>;
|
|
17
18
|
runGrowthRound: (args: any) => Promise<any>;
|
|
18
19
|
resumeGrowthRound: (args: any) => Promise<any>;
|
package/dist/growth.js
CHANGED
|
@@ -4,6 +4,7 @@ export declare class PlatformCollections {
|
|
|
4
4
|
collectSources: ({ processedIds, maxPages, limit }?: any) => Promise<any>;
|
|
5
5
|
readCreator: (args: import("./types.js").CreatorOptions) => Promise<any>;
|
|
6
6
|
readBookmarks: ({ folder, ...window }?: any) => Promise<any>;
|
|
7
|
+
readExploreRecommendations: (args?: import("./types.js").ExploreRecommendationOptions) => Promise<any>;
|
|
7
8
|
searchSources: ({ query, limit, maxPages, processedIds, }?: any) => Promise<any>;
|
|
8
9
|
constructor(dependencies: Pick<PlatformDependencies, "ready" | "observed" | "browser" | "choose">);
|
|
9
10
|
private handleCollectSources;
|
|
@@ -15,5 +16,6 @@ export declare class PlatformCollections {
|
|
|
15
16
|
private bookmarkDiscovery;
|
|
16
17
|
private waitForBookmarkLabel;
|
|
17
18
|
private handleSearchSources;
|
|
19
|
+
private handleReadExploreRecommendations;
|
|
18
20
|
}
|
|
19
21
|
export declare function createPlatformCollections(...args: ConstructorParameters<typeof PlatformCollections>): PlatformCollections;
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { xURL, source } from "./platform-rules.js";
|
|
2
|
+
const exploreNavigationEvidence = (nav) => ({
|
|
3
|
+
page: nav.page,
|
|
4
|
+
headings: (nav.headings ?? []).slice(0, 5),
|
|
5
|
+
tabs: (nav.controls ?? [])
|
|
6
|
+
.filter((control) => control.role === "tab")
|
|
7
|
+
.slice(0, 8)
|
|
8
|
+
.map((control) => ({
|
|
9
|
+
label: control.label,
|
|
10
|
+
selected: control.selected === true,
|
|
11
|
+
})),
|
|
12
|
+
});
|
|
2
13
|
export class PlatformCollections {
|
|
3
14
|
#ready;
|
|
4
15
|
#observed;
|
|
@@ -7,6 +18,7 @@ export class PlatformCollections {
|
|
|
7
18
|
collectSources;
|
|
8
19
|
readCreator;
|
|
9
20
|
readBookmarks;
|
|
21
|
+
readExploreRecommendations;
|
|
10
22
|
searchSources;
|
|
11
23
|
constructor(dependencies) {
|
|
12
24
|
const { ready, observed, browser, choose } = dependencies;
|
|
@@ -18,6 +30,8 @@ export class PlatformCollections {
|
|
|
18
30
|
this.readCreator = this.handleReadCreator.bind(this);
|
|
19
31
|
this.readBookmarks = this.handleReadBookmarks.bind(this);
|
|
20
32
|
this.searchSources = this.handleSearchSources.bind(this);
|
|
33
|
+
this.readExploreRecommendations =
|
|
34
|
+
this.handleReadExploreRecommendations.bind(this);
|
|
21
35
|
}
|
|
22
36
|
async handleCollectSources({ processedIds = [], maxPages = 3, limit = 1, } = {}) {
|
|
23
37
|
if (!Array.isArray(processedIds) ||
|
|
@@ -439,6 +453,134 @@ export class PlatformCollections {
|
|
|
439
453
|
};
|
|
440
454
|
return { ...value, query };
|
|
441
455
|
}
|
|
456
|
+
async handleReadExploreRecommendations({ limit = 3, maxPages = 1, processedIds = [], } = {}) {
|
|
457
|
+
await this.#browser.act({ operation: "openExploreRecommendations" });
|
|
458
|
+
let nav = await this.#ready({ view: "navigation" });
|
|
459
|
+
const page = (value) => {
|
|
460
|
+
try {
|
|
461
|
+
const url = xURL(value);
|
|
462
|
+
return url.pathname.replace(/\/+$/, "") === "/explore" ? url : null;
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
const entryPage = page(nav.page);
|
|
469
|
+
if (!entryPage)
|
|
470
|
+
return {
|
|
471
|
+
status: "needsDecision",
|
|
472
|
+
fresh: true,
|
|
473
|
+
complete: false,
|
|
474
|
+
page: nav.page,
|
|
475
|
+
missing: ["explore-page-unconfirmed"],
|
|
476
|
+
sources: [],
|
|
477
|
+
navigationEvidence: exploreNavigationEvidence(nav),
|
|
478
|
+
};
|
|
479
|
+
if (!nav.alerts?.length && nav.needsStateCheck) {
|
|
480
|
+
try {
|
|
481
|
+
await this.#browser.act({ operation: "waitForPosts" });
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
if (!/timed out|timeout/i.test(error.message))
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
487
|
+
nav = await this.#ready({ view: "navigation" });
|
|
488
|
+
}
|
|
489
|
+
if (nav.alerts?.length)
|
|
490
|
+
return {
|
|
491
|
+
status: "needsDecision",
|
|
492
|
+
fresh: true,
|
|
493
|
+
complete: false,
|
|
494
|
+
page: nav.page,
|
|
495
|
+
missing: ["platform-alert"],
|
|
496
|
+
sources: [],
|
|
497
|
+
navigationEvidence: exploreNavigationEvidence(nav),
|
|
498
|
+
};
|
|
499
|
+
if (nav.needsStateCheck) {
|
|
500
|
+
const diagnosticRead = await this.handleCollectSources({
|
|
501
|
+
limit,
|
|
502
|
+
maxPages,
|
|
503
|
+
processedIds,
|
|
504
|
+
});
|
|
505
|
+
const sameExplorePage = page(diagnosticRead.page);
|
|
506
|
+
return {
|
|
507
|
+
...diagnosticRead,
|
|
508
|
+
status: "needsDecision",
|
|
509
|
+
complete: false,
|
|
510
|
+
missing: [
|
|
511
|
+
...new Set([
|
|
512
|
+
"explore-navigation-unconfirmed",
|
|
513
|
+
...(!sameExplorePage ? ["explore-page-changed-during-read"] : []),
|
|
514
|
+
...(diagnosticRead.missing ?? []),
|
|
515
|
+
]),
|
|
516
|
+
],
|
|
517
|
+
sources: sameExplorePage ? (diagnosticRead.sources ?? []) : [],
|
|
518
|
+
navigationEvidence: exploreNavigationEvidence(nav),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
const recommended = (nav.controls ?? []).filter((control) => control.role === "tab" &&
|
|
522
|
+
["为你推荐", "For you"].includes(control.label.split("\n")[0]));
|
|
523
|
+
if (recommended.length !== 1)
|
|
524
|
+
return {
|
|
525
|
+
status: "needsDecision",
|
|
526
|
+
fresh: true,
|
|
527
|
+
complete: false,
|
|
528
|
+
page: entryPage.href,
|
|
529
|
+
missing: ["explore-recommended-tab-unconfirmed"],
|
|
530
|
+
sources: [],
|
|
531
|
+
navigationEvidence: exploreNavigationEvidence(nav),
|
|
532
|
+
};
|
|
533
|
+
const expectedLabel = recommended[0].label.split("\n")[0];
|
|
534
|
+
if (!recommended[0].selected &&
|
|
535
|
+
!(await this.#choose(nav, (control) => control.role === "tab" &&
|
|
536
|
+
control.label.split("\n")[0] === expectedLabel)))
|
|
537
|
+
return {
|
|
538
|
+
status: "needsDecision",
|
|
539
|
+
fresh: true,
|
|
540
|
+
complete: false,
|
|
541
|
+
page: entryPage.href,
|
|
542
|
+
missing: ["explore-recommended-tab-selection-unavailable"],
|
|
543
|
+
sources: [],
|
|
544
|
+
navigationEvidence: exploreNavigationEvidence(nav),
|
|
545
|
+
};
|
|
546
|
+
nav = await this.#ready({ view: "navigation" });
|
|
547
|
+
const selected = (nav.controls ?? []).filter((control) => control.role === "tab" &&
|
|
548
|
+
control.label.split("\n")[0] === expectedLabel &&
|
|
549
|
+
control.selected);
|
|
550
|
+
if (!page(nav.page) ||
|
|
551
|
+
nav.needsStateCheck ||
|
|
552
|
+
nav.alerts?.length ||
|
|
553
|
+
selected.length !== 1)
|
|
554
|
+
return {
|
|
555
|
+
status: "needsDecision",
|
|
556
|
+
fresh: true,
|
|
557
|
+
complete: false,
|
|
558
|
+
page: nav.page,
|
|
559
|
+
missing: ["explore-recommended-tab-selection-unconfirmed"],
|
|
560
|
+
sources: [],
|
|
561
|
+
navigationEvidence: exploreNavigationEvidence(nav),
|
|
562
|
+
};
|
|
563
|
+
const value = await this.handleCollectSources({
|
|
564
|
+
limit,
|
|
565
|
+
maxPages,
|
|
566
|
+
processedIds,
|
|
567
|
+
});
|
|
568
|
+
if (!page(value.page))
|
|
569
|
+
return {
|
|
570
|
+
...value,
|
|
571
|
+
status: "needsDecision",
|
|
572
|
+
complete: false,
|
|
573
|
+
missing: ["explore-page-changed-during-read"],
|
|
574
|
+
};
|
|
575
|
+
return {
|
|
576
|
+
...value,
|
|
577
|
+
sourceOrigin: {
|
|
578
|
+
kind: "explore-recommendations",
|
|
579
|
+
page: entryPage.href,
|
|
580
|
+
selectedTab: expectedLabel,
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
}
|
|
442
584
|
}
|
|
443
585
|
export function createPlatformCollections(...args) {
|
|
444
586
|
return new PlatformCollections(...args);
|
|
@@ -19,6 +19,7 @@ export interface PlatformDependencies {
|
|
|
19
19
|
media: any;
|
|
20
20
|
links: any;
|
|
21
21
|
needsExpansion: any;
|
|
22
|
+
metrics: string[];
|
|
22
23
|
missing: any[];
|
|
23
24
|
};
|
|
24
25
|
collectSources: ({ processedIds, maxPages, limit }?: any) => Promise<any>;
|
|
@@ -28,6 +29,7 @@ export interface PlatformDependencies {
|
|
|
28
29
|
readAccount: () => Promise<any>;
|
|
29
30
|
readBookmarks: ({ folder, ...window }?: any) => Promise<any>;
|
|
30
31
|
searchSources: ({ query, limit, maxPages, processedIds, }?: any) => Promise<any>;
|
|
32
|
+
readExploreRecommendations: (args?: import("./types.js").ExploreRecommendationOptions) => Promise<any>;
|
|
31
33
|
readFeedback: ({ targets, replyLimit }?: any) => Promise<any>;
|
|
32
34
|
readPerson: (target: any) => Promise<any>;
|
|
33
35
|
readPost: (target: any) => Promise<any>;
|
package/dist/platform-rules.js
CHANGED
|
@@ -34,7 +34,13 @@ export const source = (post, page) => ({
|
|
|
34
34
|
images: post.images ?? [],
|
|
35
35
|
media: post.media ?? [],
|
|
36
36
|
links: post.links ?? [],
|
|
37
|
-
metrics: [
|
|
37
|
+
metrics: [
|
|
38
|
+
...new Set([...(post.buttons ?? []), ...(post.metrics ?? [])]
|
|
39
|
+
.map((metric) => typeof metric === "string" ? metric : metric?.label)
|
|
40
|
+
.filter((label) => typeof label === "string" &&
|
|
41
|
+
/(?:回复|转帖|喜欢|书签|观看|查看|引用|replies|reply|reposts?|retweets?|likes?|bookmarks?|views?|quotes?)/i.test(label) &&
|
|
42
|
+
/\d/.test(label))),
|
|
43
|
+
],
|
|
38
44
|
needsExpansion: post.needsExpansion,
|
|
39
45
|
missing: [
|
|
40
46
|
...(post.missing ?? []),
|
package/dist/platform-writing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publicationTime } from "./publication-time.js";
|
|
1
2
|
import { xURL, sameHandle, sameText, samePublishedText, matchesReviewedAssets, } from "./platform-rules.js";
|
|
2
3
|
export class PlatformWriting {
|
|
3
4
|
#observed;
|
|
@@ -204,9 +205,9 @@ export class PlatformWriting {
|
|
|
204
205
|
/^\d+$/.test(post.id ?? "") &&
|
|
205
206
|
matchesReviewedAssets(post, action.assets) &&
|
|
206
207
|
!(action.beforeIds ?? []).includes(post.id) &&
|
|
207
|
-
Number.isFinite(
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
Number.isFinite(publicationTime(post)) &&
|
|
209
|
+
publicationTime(post) >= Date.parse(action.intentAt) &&
|
|
210
|
+
publicationTime(post) <= this.#clock() + 5000 &&
|
|
210
211
|
!post.needsExpansion);
|
|
211
212
|
}
|
|
212
213
|
async readQuotedReceipt(value, action, candidate) {
|
package/dist/platform.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createPlatformCollections } from "./platform-collections.js";
|
|
1
2
|
export { xURL } from "./platform-rules.js";
|
|
2
3
|
export declare class XPlatform {
|
|
3
4
|
#private;
|
|
@@ -8,6 +9,7 @@ export declare class XPlatform {
|
|
|
8
9
|
readAccount: () => Promise<any>;
|
|
9
10
|
readBookmarks: ({ folder, ...window }?: any) => Promise<any>;
|
|
10
11
|
searchSources: ({ query, limit, maxPages, processedIds, }?: any) => Promise<any>;
|
|
12
|
+
readExploreRecommendations: ReturnType<typeof createPlatformCollections>["readExploreRecommendations"];
|
|
11
13
|
readFeedback: ({ targets, replyLimit }?: any) => Promise<any>;
|
|
12
14
|
readPerson: (target: any) => Promise<any>;
|
|
13
15
|
readPost: (target: any) => Promise<any>;
|
package/dist/platform.js
CHANGED
|
@@ -23,6 +23,7 @@ export class XPlatform {
|
|
|
23
23
|
#readCreator;
|
|
24
24
|
#readBookmarks;
|
|
25
25
|
#searchSources;
|
|
26
|
+
#readExploreRecommendations;
|
|
26
27
|
#createPlatformDiagnosticsResult;
|
|
27
28
|
#followStage;
|
|
28
29
|
#captureProof;
|
|
@@ -41,6 +42,7 @@ export class XPlatform {
|
|
|
41
42
|
readAccount;
|
|
42
43
|
readBookmarks;
|
|
43
44
|
searchSources;
|
|
45
|
+
readExploreRecommendations;
|
|
44
46
|
readFeedback;
|
|
45
47
|
readPerson;
|
|
46
48
|
readPost;
|
|
@@ -79,11 +81,12 @@ export class XPlatform {
|
|
|
79
81
|
browser: browser,
|
|
80
82
|
choose: this.#choose,
|
|
81
83
|
});
|
|
82
|
-
const { collectSources, readCreator, readBookmarks, searchSources } = this.#createPlatformCollectionsResult;
|
|
84
|
+
const { collectSources, readCreator, readBookmarks, searchSources, readExploreRecommendations, } = this.#createPlatformCollectionsResult;
|
|
83
85
|
this.#collectSources = collectSources;
|
|
84
86
|
this.#readCreator = readCreator;
|
|
85
87
|
this.#readBookmarks = readBookmarks;
|
|
86
88
|
this.#searchSources = searchSources;
|
|
89
|
+
this.#readExploreRecommendations = readExploreRecommendations;
|
|
87
90
|
this.#createPlatformDiagnosticsResult = createPlatformDiagnostics({
|
|
88
91
|
clock: this.#clock,
|
|
89
92
|
});
|
|
@@ -116,6 +119,7 @@ export class XPlatform {
|
|
|
116
119
|
this.readAccount = this.#readAccount;
|
|
117
120
|
this.readBookmarks = this.#readBookmarks;
|
|
118
121
|
this.searchSources = this.#searchSources;
|
|
122
|
+
this.readExploreRecommendations = this.#readExploreRecommendations;
|
|
119
123
|
this.readFeedback = this.#readFeedback;
|
|
120
124
|
this.readPerson = this.#readPerson;
|
|
121
125
|
this.readPeople = this.#createPlatformTargetsResult.readPeople;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Refine an observed second-resolution timestamp only with its matching public X ID. */
|
|
2
|
+
export function publicationTime(post) {
|
|
3
|
+
const rendered = Date.parse(post.datetime ?? "");
|
|
4
|
+
if (!Number.isFinite(rendered))
|
|
5
|
+
return NaN;
|
|
6
|
+
if (!/^[1-9]\d{17,19}$/.test(post.id ?? ""))
|
|
7
|
+
return rendered;
|
|
8
|
+
const id = BigInt(post.id);
|
|
9
|
+
if (id > 9223372036854775807n)
|
|
10
|
+
return NaN;
|
|
11
|
+
// Twitter's published Snowflake layout: 41 timestamp bits, 22 lower bits.
|
|
12
|
+
const decoded = Number((id >> 22n) + 1288834974657n);
|
|
13
|
+
if (Math.floor(decoded / 1000) !== Math.floor(rendered / 1000))
|
|
14
|
+
return NaN;
|
|
15
|
+
if (rendered % 1000 !== 0 && rendered !== decoded)
|
|
16
|
+
return NaN;
|
|
17
|
+
return decoded;
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RoundStore } from "./store.js";
|
|
2
|
+
import type { ReadResult, ReplyDiscoveryOptions, ReplyDiscoveryResult, SearchOptions } from "./types.js";
|
|
3
|
+
/** Read-only discovery; shortlisted evidence still requires semantic review. */
|
|
4
|
+
export declare class ReplyDiscovery {
|
|
5
|
+
private search;
|
|
6
|
+
private store;
|
|
7
|
+
private clock;
|
|
8
|
+
constructor(search: (args: SearchOptions) => Promise<ReadResult>, store: RoundStore, clock: () => number);
|
|
9
|
+
private exclusion;
|
|
10
|
+
private rejection;
|
|
11
|
+
run(args: ReplyDiscoveryOptions): Promise<ReplyDiscoveryResult>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Read-only discovery; shortlisted evidence still requires semantic review. */
|
|
2
|
+
export class ReplyDiscovery {
|
|
3
|
+
search;
|
|
4
|
+
store;
|
|
5
|
+
clock;
|
|
6
|
+
constructor(search, store, clock) {
|
|
7
|
+
this.search = search;
|
|
8
|
+
this.store = store;
|
|
9
|
+
this.clock = clock;
|
|
10
|
+
}
|
|
11
|
+
exclusion(state, handles) {
|
|
12
|
+
const authors = new Set(handles.map((h) => h.replace(/^@/, "").toLowerCase()));
|
|
13
|
+
const ids = new Set();
|
|
14
|
+
if (state.accountHandle)
|
|
15
|
+
authors.add(state.accountHandle.toLowerCase());
|
|
16
|
+
for (const a of [
|
|
17
|
+
...(state.actions ?? []),
|
|
18
|
+
...(state.replyAuthorHistory?.entries ?? []),
|
|
19
|
+
]) {
|
|
20
|
+
if (a.kind !== "reply" || a.status === "skipped")
|
|
21
|
+
continue;
|
|
22
|
+
const author = a.author ?? a.conversation?.recipientHandle;
|
|
23
|
+
if (author)
|
|
24
|
+
authors.add(author.toLowerCase());
|
|
25
|
+
if (a.postId)
|
|
26
|
+
ids.add(a.postId);
|
|
27
|
+
}
|
|
28
|
+
return { authors, ids };
|
|
29
|
+
}
|
|
30
|
+
rejection(p, args, now) {
|
|
31
|
+
if (!p.id ||
|
|
32
|
+
!p.author ||
|
|
33
|
+
!p.url ||
|
|
34
|
+
!p.body?.trim() ||
|
|
35
|
+
p.needsExpansion ||
|
|
36
|
+
p.missing?.length)
|
|
37
|
+
return "incomplete-source";
|
|
38
|
+
const time = Date.parse(p.datetime ?? "");
|
|
39
|
+
if (!Number.isFinite(time) ||
|
|
40
|
+
time > now ||
|
|
41
|
+
now - time > (args.maxAgeHours ?? 12) * 3600000)
|
|
42
|
+
return "source-time";
|
|
43
|
+
if ((args.chineseOnly ?? true) && !/[\u3400-\u9fff]/u.test(p.body))
|
|
44
|
+
return "source-language";
|
|
45
|
+
const raw = p.metrics?.find((m) => /喜欢|点赞|likes?\b/i.test(m));
|
|
46
|
+
const match = raw?.match(/([\d,]+(?:\.\d+)?)\s*([万千KM])?/i);
|
|
47
|
+
const multiplier = {
|
|
48
|
+
万: 10000,
|
|
49
|
+
千: 1000,
|
|
50
|
+
K: 1000,
|
|
51
|
+
M: 1000000,
|
|
52
|
+
};
|
|
53
|
+
const likes = match
|
|
54
|
+
? Number(match[1].replaceAll(",", "")) *
|
|
55
|
+
(multiplier[(match[2] ?? "").toUpperCase()] ?? 1)
|
|
56
|
+
: NaN;
|
|
57
|
+
if (!Number.isFinite(likes) || likes < (args.minLikes ?? 20))
|
|
58
|
+
return "source-likes";
|
|
59
|
+
}
|
|
60
|
+
async run(args) {
|
|
61
|
+
const target = args.target ?? 20, maxWindows = args.maxWindows ?? 12;
|
|
62
|
+
if (!args.queries?.length ||
|
|
63
|
+
args.queries.length > 10 ||
|
|
64
|
+
args.queries.some((q) => !q.trim()) ||
|
|
65
|
+
!Number.isInteger(target) ||
|
|
66
|
+
target < 1 ||
|
|
67
|
+
target > 50 ||
|
|
68
|
+
!Number.isInteger(maxWindows) ||
|
|
69
|
+
maxWindows < 1 ||
|
|
70
|
+
maxWindows > 30 ||
|
|
71
|
+
!Number.isFinite(args.minLikes ?? 20) ||
|
|
72
|
+
(args.minLikes ?? 20) < 0 ||
|
|
73
|
+
!Number.isFinite(args.maxAgeHours ?? 12) ||
|
|
74
|
+
(args.maxAgeHours ?? 12) <= 0) {
|
|
75
|
+
return {
|
|
76
|
+
status: "needsDecision",
|
|
77
|
+
reason: "discovery-options-invalid",
|
|
78
|
+
candidates: [],
|
|
79
|
+
examinedIds: [],
|
|
80
|
+
windows: [],
|
|
81
|
+
rejections: {},
|
|
82
|
+
targetReached: false,
|
|
83
|
+
pageComplete: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const examined = new Set(args.processedIds ?? []), authors = new Set();
|
|
87
|
+
const candidates = [], windows = [], rejections = {};
|
|
88
|
+
let reason;
|
|
89
|
+
for (let i = 0; i < maxWindows && candidates.length < target; i++) {
|
|
90
|
+
const now = this.clock(), hour = new Date(now + 8 * 3600000).getUTCHours();
|
|
91
|
+
if (hour < 7) {
|
|
92
|
+
reason = "night-platform-boundary";
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const excluded = this.exclusion((await this.store.load()).state, args.excludedHandles ?? []);
|
|
96
|
+
const query = args.queries[i % args.queries.length];
|
|
97
|
+
const request = {
|
|
98
|
+
query,
|
|
99
|
+
processedIds: [...examined, ...excluded.ids],
|
|
100
|
+
limit: 5,
|
|
101
|
+
maxPages: 3,
|
|
102
|
+
mode: "decision",
|
|
103
|
+
};
|
|
104
|
+
let read = await this.search(request);
|
|
105
|
+
if ((read.reason === "source-page-unconfirmed" ||
|
|
106
|
+
read.missing?.includes("source-page-unconfirmed")) &&
|
|
107
|
+
new Date(this.clock() + 8 * 3600000).getUTCHours() >= 7)
|
|
108
|
+
read = await this.search(request);
|
|
109
|
+
windows.push({
|
|
110
|
+
query,
|
|
111
|
+
evidenceRef: read.evidenceRef,
|
|
112
|
+
status: read.status,
|
|
113
|
+
missing: read.missing ?? [],
|
|
114
|
+
observed: read.sources?.length ?? 0,
|
|
115
|
+
});
|
|
116
|
+
if (read.status === "needsCapability" ||
|
|
117
|
+
read.missing?.includes("platform-alert") ||
|
|
118
|
+
read.reason === "night-platform-boundary") {
|
|
119
|
+
reason = read.reason ?? "platform-alert";
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
for (const post of read.sources ?? []) {
|
|
123
|
+
if (post.id && examined.has(post.id))
|
|
124
|
+
continue;
|
|
125
|
+
if (post.id)
|
|
126
|
+
examined.add(post.id);
|
|
127
|
+
const author = post.author?.toLowerCase();
|
|
128
|
+
const rejection = author && (excluded.authors.has(author) || authors.has(author))
|
|
129
|
+
? "author-history-or-duplicate"
|
|
130
|
+
: post.id && excluded.ids.has(post.id)
|
|
131
|
+
? "target-history"
|
|
132
|
+
: this.rejection(post, args, this.clock());
|
|
133
|
+
if (rejection || !read.evidenceRef) {
|
|
134
|
+
const key = rejection ?? "source-proof-missing";
|
|
135
|
+
rejections[key] = (rejections[key] ?? 0) + 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
candidates.push({
|
|
139
|
+
post,
|
|
140
|
+
evidenceRef: read.evidenceRef,
|
|
141
|
+
query,
|
|
142
|
+
requiresReview: true,
|
|
143
|
+
});
|
|
144
|
+
authors.add(author);
|
|
145
|
+
if (candidates.length === target)
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const result = {
|
|
150
|
+
status: candidates.length >= target ? "observed" : "needsDecision",
|
|
151
|
+
reason: reason ??
|
|
152
|
+
(candidates.length < target ? "bounded-candidate-shortage" : undefined),
|
|
153
|
+
candidates,
|
|
154
|
+
examinedIds: [...examined],
|
|
155
|
+
windows,
|
|
156
|
+
rejections,
|
|
157
|
+
targetReached: candidates.length >= target,
|
|
158
|
+
pageComplete: false,
|
|
159
|
+
};
|
|
160
|
+
result.evidenceRef = await this.store.recordProof({
|
|
161
|
+
operation: "discoverReplyCandidates",
|
|
162
|
+
capturedAt: new Date(this.clock()).toISOString(),
|
|
163
|
+
result,
|
|
164
|
+
});
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/results.js
CHANGED
|
@@ -90,6 +90,17 @@ export function source(value, page) {
|
|
|
90
90
|
: []);
|
|
91
91
|
if (out.links)
|
|
92
92
|
out.links = out.links.filter((link) => isContentLink(link, out));
|
|
93
|
+
if (raw.metrics !== undefined) {
|
|
94
|
+
const metrics = list(raw.metrics).flatMap((value) => {
|
|
95
|
+
if (typeof value === "string")
|
|
96
|
+
return [value];
|
|
97
|
+
const label = object(value).label;
|
|
98
|
+
return typeof label === "string" ? [label] : [];
|
|
99
|
+
});
|
|
100
|
+
out.metrics = [
|
|
101
|
+
...new Set(metrics.filter((label) => /(?:回复|转帖|喜欢|书签|观看|查看|引用|replies|reply|reposts?|retweets?|likes?|bookmarks?|views?|quotes?)/i.test(label) && /\d/.test(label))),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
93
104
|
if (strings(raw.missing).length)
|
|
94
105
|
out.missing = strings(raw.missing);
|
|
95
106
|
return out;
|
|
@@ -200,6 +211,28 @@ export function readResult(operation, value, mode) {
|
|
|
200
211
|
out.complete = false;
|
|
201
212
|
if (raw.ids !== undefined)
|
|
202
213
|
out.ids = strings(raw.ids);
|
|
214
|
+
if (operation === "readExploreRecommendations") {
|
|
215
|
+
const origin = object(raw.sourceOrigin);
|
|
216
|
+
try {
|
|
217
|
+
const page = new URL(String(origin.page));
|
|
218
|
+
if (origin.kind === "explore-recommendations" &&
|
|
219
|
+
page.protocol === "https:" &&
|
|
220
|
+
page.hostname === "x.com" &&
|
|
221
|
+
!page.username &&
|
|
222
|
+
!page.password &&
|
|
223
|
+
!page.port &&
|
|
224
|
+
page.pathname.replace(/\/+$/, "") === "/explore" &&
|
|
225
|
+
["为你推荐", "For you"].includes(String(origin.selectedTab)))
|
|
226
|
+
out.sourceOrigin = {
|
|
227
|
+
kind: "explore-recommendations",
|
|
228
|
+
page: page.href,
|
|
229
|
+
selectedTab: String(origin.selectedTab),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
/* Unverified provenance is omitted at the public business boundary. */
|
|
234
|
+
}
|
|
235
|
+
}
|
|
203
236
|
const missing = strings(raw.missing);
|
|
204
237
|
if (list(raw.alerts).length && !missing.includes("platform-alert"))
|
|
205
238
|
missing.push("platform-alert");
|
package/dist/types.d.ts
CHANGED
|
@@ -93,6 +93,11 @@ export interface Diagnostic {
|
|
|
93
93
|
next?: "inspect-local-proof" | "restore-proof-store";
|
|
94
94
|
evidenceRef?: EvidenceRef;
|
|
95
95
|
}
|
|
96
|
+
export interface SourceOriginEvidence {
|
|
97
|
+
kind: "explore-recommendations";
|
|
98
|
+
page: string;
|
|
99
|
+
selectedTab: "为你推荐" | "For you";
|
|
100
|
+
}
|
|
96
101
|
export interface ReadResult {
|
|
97
102
|
status: "observed" | "needsDecision" | "needsCapability";
|
|
98
103
|
fresh?: boolean;
|
|
@@ -124,6 +129,7 @@ export interface ReadResult {
|
|
|
124
129
|
scopeComplete?: boolean;
|
|
125
130
|
};
|
|
126
131
|
sources?: PostEvidence[];
|
|
132
|
+
sourceOrigin?: SourceOriginEvidence;
|
|
127
133
|
post?: PostEvidence;
|
|
128
134
|
context?: PostEvidence[];
|
|
129
135
|
threadPosts?: PostEvidence[];
|
|
@@ -253,6 +259,40 @@ export interface BookmarkOptions extends WindowOptions {
|
|
|
253
259
|
export interface SearchOptions extends WindowOptions {
|
|
254
260
|
query: string;
|
|
255
261
|
}
|
|
262
|
+
export interface ReplyDiscoveryOptions {
|
|
263
|
+
queries: string[];
|
|
264
|
+
target?: number;
|
|
265
|
+
maxWindows?: number;
|
|
266
|
+
processedIds?: string[];
|
|
267
|
+
excludedHandles?: string[];
|
|
268
|
+
minLikes?: number;
|
|
269
|
+
maxAgeHours?: number;
|
|
270
|
+
chineseOnly?: boolean;
|
|
271
|
+
}
|
|
272
|
+
export interface ReplyDiscoveryResult {
|
|
273
|
+
status: "observed" | "needsDecision";
|
|
274
|
+
reason?: string;
|
|
275
|
+
candidates: {
|
|
276
|
+
post: PostEvidence;
|
|
277
|
+
evidenceRef: EvidenceRef;
|
|
278
|
+
query: string;
|
|
279
|
+
requiresReview: true;
|
|
280
|
+
}[];
|
|
281
|
+
examinedIds: string[];
|
|
282
|
+
windows: {
|
|
283
|
+
query: string;
|
|
284
|
+
evidenceRef?: EvidenceRef;
|
|
285
|
+
status: ReadResult["status"];
|
|
286
|
+
missing: string[];
|
|
287
|
+
observed: number;
|
|
288
|
+
}[];
|
|
289
|
+
rejections: Record<string, number>;
|
|
290
|
+
targetReached: boolean;
|
|
291
|
+
pageComplete: false;
|
|
292
|
+
evidenceRef?: EvidenceRef;
|
|
293
|
+
}
|
|
294
|
+
export interface ExploreRecommendationOptions extends WindowOptions {
|
|
295
|
+
}
|
|
256
296
|
export interface CreatorOptions {
|
|
257
297
|
handle: string;
|
|
258
298
|
target?: number;
|
|
@@ -562,7 +602,9 @@ export interface XGrowthHarness {
|
|
|
562
602
|
readPeople(args: PeopleOptions): Promise<ReadResult>;
|
|
563
603
|
readFollowbackAudit(args: FollowbackAuditOptions): Promise<FollowbackAuditResult>;
|
|
564
604
|
readBookmarks(args: BookmarkOptions): Promise<ReadResult>;
|
|
605
|
+
discoverReplyCandidates(args: ReplyDiscoveryOptions): Promise<ReplyDiscoveryResult>;
|
|
565
606
|
searchSources(args: SearchOptions): Promise<ReadResult>;
|
|
607
|
+
readExploreRecommendations(args?: ExploreRecommendationOptions): Promise<ReadResult>;
|
|
566
608
|
readIncomingReplies(args?: IncomingReplyOptions): Promise<ReadResult>;
|
|
567
609
|
readIncomingConversations(args?: IncomingConversationOptions): Promise<IncomingConversationResult>;
|
|
568
610
|
readPost(args: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/x-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.68",
|
|
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": "9d121f1f8b24f7c1082d8f17a3f6ec473d45d534"
|
|
46
46
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
# Batch discovery
|
|
2
|
+
For a batch of active replies, first use `x.discoverReplyCandidates({queries:["互关 min_faves:20", "串门 min_faves:20"],target:20,processedIds})`. One call owns serial paginated searches, original-ledger target/author exclusion, deduplication and deterministic age/likes/language/body gates. It performs no writes. Save/reuse `examinedIds` on the original actor; full candidate text, original per-window proofs and the combined proof stay available locally. `requiresReview:true` means relevance, human identity and suitable text still need review. Review the batch, then use existing `replyBatch`; do not rebuild per-person discovery or infer approval from metadata. Partial shortages include rejection totals and window warnings; `pageComplete:false` never claims all of X was read. Night boundary stops further reads. Missing metrics remain unknown. Repair a concrete missing capability at its original owner, validate it and resume the task; no implementation reading for normal calls.
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
| readCreator | handle | target (10;1–50), maxScrolls, navigate, expand, mode |
|
|
7
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
|
+
| readExploreRecommendations | none | processedIds, limit (1–5), maxPages (1–10), mode |
|
|
9
10
|
| readPost | target: {postId,url,evidenceRef} | includeContext, resolveQuote, mode |
|
|
10
11
|
| readPerson | target: {handle,url,evidenceRef} | mode |
|
|
11
12
|
| readPeople | target: {url,evidenceRef} | offset, limit (1–5), advance, mode |
|
|
@@ -13,20 +14,22 @@
|
|
|
13
14
|
| inspectAccount | none | mode |
|
|
14
15
|
| inspectRound | taskId (original batch ID) | none |
|
|
15
16
|
|
|
16
|
-
Targets
|
|
17
|
+
Targets need proof. Native `readCreator` requires a selected profile (`navigate:false`); relative time is unknown. Stop on route changes.
|
|
17
18
|
|
|
18
|
-
Bookmarks resume:true continues the bound folder; rendered tail ≠ full proof.
|
|
19
|
+
Bookmarks `resume:true` continues the bound folder; rendered tail ≠ full proof.
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
`pageComplete:false`: finite scope. Preserve full text; view media before describing.
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
Incoming replies: ledger only; carry processedIds; skip settled/intent/unknown. Read parents with `readPost(includeContext:true)`; deduplicate.
|
|
23
24
|
|
|
24
|
-
Incoming: `readIncomingConversations({targets?,...window})`
|
|
25
|
+
Incoming conversations: `readIncomingConversations({targets?,...window})` discovers/reads ≤5 chains in one call. Targets need proof; returns nodes, ancestors, confirmed edges, unknown parents and attempts. `rootCandidateId` may not be root. Missing context blocks review; don't infer edges or clearance.
|
|
25
26
|
|
|
26
27
|
`resolveQuote:true` follows one observed card and returns `post.quoteSource` only after exact destination/body/author/time binding.
|
|
27
28
|
|
|
28
|
-
`readPeople`:
|
|
29
|
+
`readPeople`: selected followers/following; `offset` reuses cells, `advance` scrolls. Returns bio, blue, relationship; dedup. Unknowns need profiles; Native unsupported.
|
|
29
30
|
|
|
30
31
|
Discovery: names only.
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
Count `sourceOrigin` only with confirmed `/explore` “For you” tab; exclude trends/search. Preserve visible counts; absent stays unknown.
|
|
34
|
+
|
|
35
|
+
Batch replies: [automated discovery](batch-discovery.md).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Observed reply receipt recovery
|
|
2
|
+
|
|
3
|
+
If the root reports an original unknown but a fresh proof or public search exposes its actual own-account permalink, keep that original action. Never derive a receipt ID from a timestamp or guess URLs. Read the observed candidate with root readPost and its original evidenceRef; confirm the actual author, reviewed text, new ID and time at/after intent. A visible parent in context alone is not the final exact-parent verification.
|
|
4
|
+
|
|
5
|
+
The original ledger already supports observedPublicURL and observedPublicEvidenceRef. Use the existing diagnostics createFileRoundStore transaction to attach only these two fields to that same unknown/intent reply, preserving other actions, executors and counts. Both fields must come from actual observed proof, not a claimed success. Then call root verifySubmission({taskId,actionId}); its normal fresh author/text/time/assets/exact-parent gates settle the original action. A rejected/ambiguous hint remains unknown. Do not manually mark verified, create a replacement action, resend or bypass alerts.
|
|
6
|
+
|
|
7
|
+
Prepared reply baseline URL timeouts get one internal read-only continuation. A second timeout, unrelated failure, alerts or unconfirmed content stops before intent. Reusing this root behavior is preferable to model-level per-parent rewarming. This does not grant new platform access or override project budgets/windows.
|
|
@@ -4,7 +4,7 @@ Load on failure/unknown only. Keep operation/status/missing/code/proof/error; do
|
|
|
4
4
|
|
|
5
5
|
| Signal | Next action and exit condition |
|
|
6
6
|
| --- | --- |
|
|
7
|
-
| unknown submission | `await x.verifySubmission({taskId,actionId})`, same IDs; no
|
|
7
|
+
| unknown submission | `await x.verifySubmission({taskId,actionId})`, same IDs; no resend. [Observed link](receipt-recovery.md). |
|
|
8
8
|
| Existing public post needs recovery | `await x.verifyPublishedPost({taskId,draftPath,publicURL,evidenceRef})`; observed URL/proof. sourceLink/sourceLinks: [link proof](source-link-recovery.md). |
|
|
9
9
|
| Follow needs independent recovery | `await x.verifyFollow({handle,url,evidenceRef,mode:'decision'})`; honor returned verified. Normal followBatch already verifies. |
|
|
10
10
|
| needsDecision | Resolve returned missing review/proof in original action, resume same ID. Current AI performs internal review under project contract. |
|