@nextclaw/x-harness 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -0
- package/README.md +41 -2
- package/cli/main.js +219 -0
- package/dist/api.d.ts +10 -0
- package/dist/api.js +308 -0
- package/dist/boundary.d.ts +2 -0
- package/dist/browser-common.d.ts +4 -0
- package/dist/browser-common.js +29 -0
- package/dist/browser-dom-capabilities.d.ts +4 -0
- package/dist/browser-dom-capabilities.js +110 -0
- package/dist/browser-dom-dependencies.d.ts +16 -0
- package/dist/browser-dom-editing.d.ts +148 -0
- package/dist/browser-dom-editing.js +386 -0
- package/dist/browser-dom-navigation.d.ts +9 -0
- package/dist/browser-dom-navigation.js +165 -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 +2 -0
- package/dist/browser-dom-submission.js +82 -0
- package/dist/browser-dom.d.ts +2 -0
- package/dist/browser-dom.js +103 -0
- package/dist/browser-native.d.ts +1 -0
- package/dist/browser-native.js +215 -0
- package/dist/browser.d.ts +4 -0
- package/dist/browser.js +147 -0
- package/dist/creator.d.ts +29 -0
- package/dist/creator.js +164 -0
- package/dist/growth-batch.d.ts +5 -0
- package/dist/growth-batch.js +168 -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-execution.d.ts +11 -0
- package/dist/growth-execution.js +315 -0
- package/dist/growth-parallel.d.ts +4 -0
- package/dist/growth-parallel.js +301 -0
- package/dist/growth-plan.d.ts +4 -0
- package/dist/growth-plan.js +207 -0
- package/dist/growth-policy.d.ts +9 -0
- package/dist/growth-policy.js +181 -0
- package/dist/growth-publishing.d.ts +5 -0
- package/dist/growth-publishing.js +240 -0
- package/dist/growth-reading.d.ts +4 -0
- package/dist/growth-reading.js +135 -0
- package/dist/growth-recovery.d.ts +6 -0
- package/dist/growth-recovery.js +158 -0
- package/dist/growth-state.d.ts +8 -0
- package/dist/growth-state.js +183 -0
- package/dist/growth-tasks.d.ts +17 -0
- package/dist/growth-tasks.js +101 -0
- package/dist/growth.d.ts +2 -0
- package/dist/growth.js +138 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +22 -0
- package/dist/observation.d.ts +1 -0
- package/dist/observation.js +2 -0
- package/dist/platform-capabilities.d.ts +4 -0
- package/dist/platform-capabilities.js +19 -0
- package/dist/platform-collections.d.ts +7 -0
- package/dist/platform-collections.js +223 -0
- package/dist/platform-dependencies.d.ts +43 -0
- package/dist/platform-diagnostics.d.ts +9 -0
- package/dist/platform-diagnostics.js +51 -0
- package/dist/platform-observation.d.ts +8 -0
- package/dist/platform-observation.js +75 -0
- package/dist/platform-rules.d.ts +17 -0
- package/dist/platform-rules.js +41 -0
- package/dist/platform-targets.d.ts +7 -0
- package/dist/platform-targets.js +129 -0
- package/dist/platform-writing.d.ts +8 -0
- package/dist/platform-writing.js +171 -0
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +60 -0
- package/dist/readers-actions.d.ts +2 -0
- package/dist/readers-actions.js +66 -0
- package/dist/readers-ax-host.d.ts +2 -0
- package/dist/readers-ax-host.js +125 -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-tree.d.ts +19 -0
- package/dist/readers-ax-tree.js +110 -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-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 -0
- package/dist/results.d.ts +6 -0
- package/dist/results.js +233 -0
- package/dist/round-state.d.ts +3 -0
- package/dist/round-state.js +155 -0
- package/dist/store.d.ts +30 -0
- package/dist/store.js +175 -0
- package/dist/types.d.ts +407 -0
- package/generated/observation.d.mts +1 -0
- package/generated/observation.mjs +515 -0
- package/package.json +29 -3
- package/skill/SKILL.md +33 -0
- package/skill/references/api.md +10 -0
- 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,141 @@
|
|
|
1
|
+
// Self-contained, read-only DOM function for cua_repl's documented evaluate.
|
|
2
|
+
export function readPosts({ offset = 0, limit = 5, targetId = "", } = {}) {
|
|
3
|
+
if (!Number.isInteger(offset) ||
|
|
4
|
+
offset < 0 ||
|
|
5
|
+
!Number.isInteger(limit) ||
|
|
6
|
+
limit < 1 ||
|
|
7
|
+
limit > 5)
|
|
8
|
+
throw new Error("offset>=0;limit为1~5");
|
|
9
|
+
if (targetId && !/^\d+$/.test(targetId))
|
|
10
|
+
throw new Error("targetId必须是数字公开ID");
|
|
11
|
+
const visible = (el) => el.getClientRects().length > 0;
|
|
12
|
+
const primaryTime = (a) => Array.from(a.querySelectorAll("time")).find((t) => visible(t) &&
|
|
13
|
+
/\/status\/\d+(?:$|[/?#])/.test(t.closest("a")?.getAttribute("href") ?? "")) ?? null;
|
|
14
|
+
const all = Array.from(document.querySelectorAll("article")).filter(visible);
|
|
15
|
+
const candidates = targetId
|
|
16
|
+
? all.filter((a) => {
|
|
17
|
+
const primary = primaryTime(a)?.closest("a")?.getAttribute("href");
|
|
18
|
+
return (primary && new RegExp(`/status/${targetId}(?:$|[/?#])`).test(primary));
|
|
19
|
+
})
|
|
20
|
+
: all;
|
|
21
|
+
const posts = candidates.slice(offset, offset + limit).map((a) => {
|
|
22
|
+
const time = primaryTime(a);
|
|
23
|
+
const primary = time?.closest("a")?.getAttribute("href") ?? null;
|
|
24
|
+
const links = [
|
|
25
|
+
...new Set(Array.from(a.querySelectorAll("a[href]"))
|
|
26
|
+
.filter(visible)
|
|
27
|
+
.map((l) => l.getAttribute("href"))),
|
|
28
|
+
];
|
|
29
|
+
const buttons = Array.from(a.querySelectorAll("button"))
|
|
30
|
+
.filter(visible)
|
|
31
|
+
.map((b) => ({
|
|
32
|
+
label: b.getAttribute("aria-label") || b.innerText,
|
|
33
|
+
disabled: b.hasAttribute("disabled"),
|
|
34
|
+
}));
|
|
35
|
+
const text = a.innerText;
|
|
36
|
+
const datetime = time?.getAttribute("datetime") ?? null;
|
|
37
|
+
const id = primary?.match(/\/status\/(\d+)/)?.[1] ?? null;
|
|
38
|
+
const author = primary?.match(/\/([^/]+)\/status\//)?.[1] ?? null;
|
|
39
|
+
const images = Array.from(a.querySelectorAll('[data-testid="tweetPhoto"] img'))
|
|
40
|
+
.filter(visible)
|
|
41
|
+
.map((img) => ({
|
|
42
|
+
alt: img.getAttribute("alt"),
|
|
43
|
+
src: img.getAttribute("src"),
|
|
44
|
+
}));
|
|
45
|
+
const media = Array.from(a.querySelectorAll('[data-testid="videoPlayer"],video'))
|
|
46
|
+
.filter(visible)
|
|
47
|
+
.map((el) => ({
|
|
48
|
+
kind: "video",
|
|
49
|
+
label: el.getAttribute("aria-label"),
|
|
50
|
+
poster: el.getAttribute("poster"),
|
|
51
|
+
requiresViewing: true,
|
|
52
|
+
}));
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
author,
|
|
56
|
+
datetime,
|
|
57
|
+
primary,
|
|
58
|
+
text,
|
|
59
|
+
links,
|
|
60
|
+
buttons,
|
|
61
|
+
// View/quote counts may be visible links rather than buttons; collect in this same read.
|
|
62
|
+
metrics: [
|
|
63
|
+
...buttons,
|
|
64
|
+
...Array.from(a.querySelectorAll('a[href$="/analytics"],a[href$="/quotes"]'))
|
|
65
|
+
.filter(visible)
|
|
66
|
+
.map((link) => ({
|
|
67
|
+
label: link.getAttribute("aria-label") ||
|
|
68
|
+
`${link.getAttribute("href").endsWith("/analytics") ? "观看" : "引用"} ${link.innerText}`,
|
|
69
|
+
})),
|
|
70
|
+
],
|
|
71
|
+
images,
|
|
72
|
+
media,
|
|
73
|
+
needsExpansion: /显示更多|Show more/i.test(text),
|
|
74
|
+
missing: [
|
|
75
|
+
!id && "id",
|
|
76
|
+
!author && "author",
|
|
77
|
+
!datetime && "datetime",
|
|
78
|
+
!text && "text",
|
|
79
|
+
].filter(Boolean),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
const versionLinks = Array.from(document.querySelectorAll("a[href]"))
|
|
83
|
+
.filter(visible)
|
|
84
|
+
.filter((a) => /最新版本|查看最新帖子|Latest version|View latest/i.test(a.innerText))
|
|
85
|
+
.slice(0, 5)
|
|
86
|
+
.map((a) => ({ label: a.innerText, href: a.getAttribute("href") }));
|
|
87
|
+
const emptyEvidence = all.length === 0
|
|
88
|
+
? [
|
|
89
|
+
...new Set(Array.from(document.querySelectorAll("main span"))
|
|
90
|
+
.filter(visible)
|
|
91
|
+
.map((e) => e.innerText?.trim())
|
|
92
|
+
.filter((text) => text &&
|
|
93
|
+
text.length <= 400 &&
|
|
94
|
+
!text.includes("\n") &&
|
|
95
|
+
/^(?:没有.*结果|未找到.*结果|No results for)/i.test(text))),
|
|
96
|
+
].slice(0, 2)
|
|
97
|
+
: [];
|
|
98
|
+
return {
|
|
99
|
+
renderedArticles: all.length,
|
|
100
|
+
matched: candidates.length,
|
|
101
|
+
offset,
|
|
102
|
+
moreInDOM: candidates.length > offset + posts.length,
|
|
103
|
+
pageComplete: false,
|
|
104
|
+
needsStateCheck: all.length === 0 && emptyEvidence.length === 0,
|
|
105
|
+
posts,
|
|
106
|
+
versionLinks,
|
|
107
|
+
emptyEvidence,
|
|
108
|
+
alerts: Array.from(document.querySelectorAll('[role="alert"]'))
|
|
109
|
+
.filter(visible)
|
|
110
|
+
.map((e) => e.innerText),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function changes(result, cache, page, fresh = false) {
|
|
114
|
+
const unchangedIds = [];
|
|
115
|
+
const posts = result.posts.filter((post) => {
|
|
116
|
+
if (!post.id)
|
|
117
|
+
return true;
|
|
118
|
+
const key = `${page}|${post.id}`;
|
|
119
|
+
const value = JSON.stringify(post);
|
|
120
|
+
const changed = cache[key] !== value;
|
|
121
|
+
cache[key] = value;
|
|
122
|
+
if (!fresh && !changed)
|
|
123
|
+
unchangedIds.push(post.id);
|
|
124
|
+
return fresh || changed;
|
|
125
|
+
});
|
|
126
|
+
const outputChars = JSON.stringify({ ...result, posts, unchangedIds }).length;
|
|
127
|
+
const optimizationHint = outputChars > 8000
|
|
128
|
+
? posts.length > 1
|
|
129
|
+
? "缩小读取窗口或按已观察ID定向读取;保留完整正文"
|
|
130
|
+
: "单篇证据本身长,保留全文,不截断"
|
|
131
|
+
: null;
|
|
132
|
+
return {
|
|
133
|
+
...result,
|
|
134
|
+
posts,
|
|
135
|
+
unchanged: result.posts.length - posts.length,
|
|
136
|
+
unchangedIds,
|
|
137
|
+
fresh,
|
|
138
|
+
outputChars,
|
|
139
|
+
optimizationHint,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Profile selectors are the visible controls observed in the live X workflow.
|
|
2
|
+
// Missing/ambiguous counters remain unknown; rounded counts carry their precision.
|
|
3
|
+
export function readProfile({ handle } = {}) {
|
|
4
|
+
if (!/^[A-Za-z0-9_]{1,15}$/.test(handle ?? ""))
|
|
5
|
+
throw new Error("需要已观察的X handle");
|
|
6
|
+
const visible = (el) => el.getClientRects().length > 0;
|
|
7
|
+
const main = document.querySelector("main");
|
|
8
|
+
if (!main)
|
|
9
|
+
return {
|
|
10
|
+
handle,
|
|
11
|
+
blue: null,
|
|
12
|
+
relationship: "unknown",
|
|
13
|
+
alerts: ["missing-main"],
|
|
14
|
+
observedAt: new Date().toISOString(),
|
|
15
|
+
};
|
|
16
|
+
const buttons = Array.from(main.querySelectorAll("button")).filter(visible);
|
|
17
|
+
const labels = buttons.map((b) => b.getAttribute("aria-label") || b.innerText);
|
|
18
|
+
const relationship = labels.includes(`正在关注 @${handle}`) ||
|
|
19
|
+
labels.includes(`取消关注 @${handle}`)
|
|
20
|
+
? "following"
|
|
21
|
+
: labels.includes(`回关 @${handle}`)
|
|
22
|
+
? "follows-you"
|
|
23
|
+
: labels.includes(`关注 @${handle}`)
|
|
24
|
+
? "not-following"
|
|
25
|
+
: "unknown";
|
|
26
|
+
const badges = buttons
|
|
27
|
+
.filter((b) => b.getAttribute("aria-label") === "提供已验证账号的详细信息。")
|
|
28
|
+
.map((b) => b.querySelector("svg"))
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
const blue = badges.length === 1
|
|
31
|
+
? [
|
|
32
|
+
getComputedStyle(badges[0]).color,
|
|
33
|
+
getComputedStyle(badges[0]).fill,
|
|
34
|
+
].includes("rgb(29, 155, 240)")
|
|
35
|
+
: null;
|
|
36
|
+
const links = Array.from(main.querySelectorAll("a[href]")).filter(visible);
|
|
37
|
+
const counter = (suffixes) => {
|
|
38
|
+
const matched = links.filter((a) => suffixes.some((s) => a.getAttribute("href")?.toLowerCase() ===
|
|
39
|
+
`/${handle}/${s}`.toLowerCase()));
|
|
40
|
+
const texts = [...new Set(matched.map((a) => a.innerText.trim()))];
|
|
41
|
+
if (texts.length !== 1)
|
|
42
|
+
return { value: null, text: texts, precision: "unknown" };
|
|
43
|
+
const match = texts[0].match(/^([\d,.]+)\s*(万|亿|[KMB])?/i);
|
|
44
|
+
const factors = {
|
|
45
|
+
万: 1e4,
|
|
46
|
+
亿: 1e8,
|
|
47
|
+
K: 1e3,
|
|
48
|
+
M: 1e6,
|
|
49
|
+
B: 1e9,
|
|
50
|
+
};
|
|
51
|
+
const value = match
|
|
52
|
+
? Number(match[1].replaceAll(",", "")) *
|
|
53
|
+
(factors[match[2]?.toUpperCase()] ?? 1)
|
|
54
|
+
: null;
|
|
55
|
+
const hrefs = [...new Set(matched.map((a) => a.getAttribute("href")))];
|
|
56
|
+
return {
|
|
57
|
+
value: Number.isFinite(value) ? value : null,
|
|
58
|
+
text: texts[0],
|
|
59
|
+
precision: match?.[2] ? "rounded" : "exact",
|
|
60
|
+
url: hrefs.length === 1 ? "https://x.com" + hrefs[0] : null,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
handle,
|
|
65
|
+
blue,
|
|
66
|
+
relationship,
|
|
67
|
+
relationshipControls: labels
|
|
68
|
+
.filter((label) => label.includes("@" + handle) || /取消关注|订阅/.test(label))
|
|
69
|
+
.slice(0, 5),
|
|
70
|
+
biography: main.querySelector('[data-testid="UserDescription"]')
|
|
71
|
+
?.innerText ?? null,
|
|
72
|
+
following: counter(["following"]),
|
|
73
|
+
followers: counter(["followers", "verified_followers"]),
|
|
74
|
+
alerts: Array.from(document.querySelectorAll('[role="alert"]'))
|
|
75
|
+
.filter(visible)
|
|
76
|
+
.map((a) => a.innerText),
|
|
77
|
+
observedAt: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Bounded visible person cells for a currently loaded follower/following list.
|
|
81
|
+
export function readPeople({ offset = 0, limit = 5 } = {}) {
|
|
82
|
+
if (!Number.isInteger(offset) ||
|
|
83
|
+
offset < 0 ||
|
|
84
|
+
!Number.isInteger(limit) ||
|
|
85
|
+
limit < 1 ||
|
|
86
|
+
limit > 5)
|
|
87
|
+
throw new Error("人物窗口须为非负offset及1–5 limit");
|
|
88
|
+
const visible = (el) => el.getClientRects().length > 0;
|
|
89
|
+
const cells = Array.from(document.querySelectorAll('main [data-testid="UserCell"]')).filter(visible);
|
|
90
|
+
const people = cells.slice(offset, offset + limit).map((cell) => {
|
|
91
|
+
const links = Array.from(cell.querySelectorAll("a[href]"))
|
|
92
|
+
.filter(visible)
|
|
93
|
+
.map((a) => ({ text: a.innerText, href: a.getAttribute("href") }));
|
|
94
|
+
const buttons = Array.from(cell.querySelectorAll("button"))
|
|
95
|
+
.filter(visible)
|
|
96
|
+
.map((b) => ({
|
|
97
|
+
label: b.getAttribute("aria-label") || b.innerText,
|
|
98
|
+
disabled: b.disabled,
|
|
99
|
+
}));
|
|
100
|
+
return { text: cell.innerText, links, buttons };
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
page: location.href,
|
|
104
|
+
people,
|
|
105
|
+
offset,
|
|
106
|
+
renderedPeople: cells.length,
|
|
107
|
+
moreInDOM: offset + limit < cells.length,
|
|
108
|
+
pageComplete: false,
|
|
109
|
+
needsStateCheck: !people.length,
|
|
110
|
+
missing: people.length ? [] : ["people-cells-not-found"],
|
|
111
|
+
alerts: Array.from(document.querySelectorAll('[role="alert"]'))
|
|
112
|
+
.filter(visible)
|
|
113
|
+
.map((a) => a.innerText),
|
|
114
|
+
observedAt: new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// This ranks observed clues, never a predicted follow-back probability.
|
|
118
|
+
export function screenCandidates(profiles, { minRatio = 0.8 } = {}) {
|
|
119
|
+
if (!Array.isArray(profiles) || !Number.isFinite(minRatio) || minRatio <= 0)
|
|
120
|
+
throw new Error("profiles数组和正minRatio必填");
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
return profiles
|
|
123
|
+
.map((p) => {
|
|
124
|
+
const key = String(p.handle ?? "").toLowerCase();
|
|
125
|
+
const duplicate = seen.has(key);
|
|
126
|
+
seen.add(key);
|
|
127
|
+
const following = p.following?.value, followers = p.followers?.value;
|
|
128
|
+
const ratio = Number.isFinite(following) &&
|
|
129
|
+
Number.isFinite(followers) &&
|
|
130
|
+
followers > 0
|
|
131
|
+
? following / followers
|
|
132
|
+
: null;
|
|
133
|
+
const clues = [];
|
|
134
|
+
if (typeof p.mutualEvidence === "string" && p.mutualEvidence.trim())
|
|
135
|
+
clues.push({ kind: "explicit-mutual", evidence: p.mutualEvidence });
|
|
136
|
+
if (ratio !== null && ratio >= minRatio)
|
|
137
|
+
clues.push({
|
|
138
|
+
kind: "following-ratio",
|
|
139
|
+
ratio,
|
|
140
|
+
following: p.following,
|
|
141
|
+
followers: p.followers,
|
|
142
|
+
evidence: "关注/粉丝比例线索,非回关概率",
|
|
143
|
+
});
|
|
144
|
+
const rejected = [
|
|
145
|
+
duplicate && "duplicate",
|
|
146
|
+
p.blue !== true && "blue-unconfirmed",
|
|
147
|
+
p.relationship !== "not-following" && "relationship-ineligible",
|
|
148
|
+
p.alerts?.length && "page-alert",
|
|
149
|
+
!clues.length && "no-followback-clue",
|
|
150
|
+
!/^[A-Za-z0-9_]{1,15}$/.test(p.handle ?? "") && "invalid-handle",
|
|
151
|
+
].filter(Boolean);
|
|
152
|
+
return { ...p, ratio, clues, eligible: rejected.length === 0, rejected };
|
|
153
|
+
})
|
|
154
|
+
.sort((a, b) => Number(b.eligible) - Number(a.eligible) ||
|
|
155
|
+
Number(b.clues.some((c) => c.kind === "explicit-mutual")) -
|
|
156
|
+
Number(a.clues.some((c) => c.kind === "explicit-mutual")));
|
|
157
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Compatibility exports only. Implementation owners are task-specific modules. */
|
|
2
|
+
export { readPosts, changes } from "./readers-posts.js";
|
|
3
|
+
export { readAX } from "./readers-ax-parser.js";
|
|
4
|
+
export { createAXTabHarness, createAXHarness } from "./readers-ax-host.js";
|
|
5
|
+
export { readProfile, readPeople, screenCandidates, } from "./readers-profile.js";
|
|
6
|
+
export { readComposer, readMediaEditor } from "./readers-composer.js";
|
|
7
|
+
export { readMenu, readNavigation } from "./readers-navigation.js";
|
|
8
|
+
export { attachImage, openReply } from "./readers-actions.js";
|
|
9
|
+
export { setupCode, profileCode, profilesCode, browserCode, axCode, runReaderCLI, } from "./readers-cli.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PostEvidence, ReadResult, ReadMode, Diagnostic, ExecutionDecision } from "./types.js";
|
|
2
|
+
export declare const stableReason: (value: unknown) => string | undefined;
|
|
3
|
+
export declare function diagnostic(value: unknown): Diagnostic | undefined;
|
|
4
|
+
export declare function source(value: unknown, page?: string): PostEvidence;
|
|
5
|
+
export declare function readResult(operation: string, value: unknown, mode: ReadMode): ReadResult;
|
|
6
|
+
export declare function decisions(value: unknown): ExecutionDecision[];
|
package/dist/results.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const object = (value) => value && typeof value === "object" ? value : {};
|
|
2
|
+
const list = (value) => (Array.isArray(value) ? value : []);
|
|
3
|
+
const strings = (value) => list(value).filter((v) => typeof v === "string");
|
|
4
|
+
function pick(value, keys) {
|
|
5
|
+
const input = object(value), out = {};
|
|
6
|
+
for (const key of keys) {
|
|
7
|
+
const v = input[key];
|
|
8
|
+
if (v !== undefined &&
|
|
9
|
+
(v === null || ["string", "number", "boolean"].includes(typeof v)))
|
|
10
|
+
out[key] = v;
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
export const stableReason = (value) => typeof value === "string"
|
|
15
|
+
? /^[a-z][a-z0-9.-]{0,95}$/.test(value)
|
|
16
|
+
? value
|
|
17
|
+
: "harness-boundary-error"
|
|
18
|
+
: undefined;
|
|
19
|
+
export function diagnostic(value) {
|
|
20
|
+
const raw = object(value);
|
|
21
|
+
if (typeof raw.code !== "string")
|
|
22
|
+
return undefined;
|
|
23
|
+
return {
|
|
24
|
+
...pick(raw, ["operation", "category", "next", "evidenceRef"]),
|
|
25
|
+
code: stableReason(raw.code),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function source(value, page) {
|
|
29
|
+
const raw = object(value), out = pick(raw, [
|
|
30
|
+
"id",
|
|
31
|
+
"author",
|
|
32
|
+
"datetime",
|
|
33
|
+
"url",
|
|
34
|
+
"body",
|
|
35
|
+
"needsExpansion",
|
|
36
|
+
]);
|
|
37
|
+
out.id = typeof raw.id === "string" ? raw.id : null;
|
|
38
|
+
if (out.body === undefined && typeof raw.text === "string")
|
|
39
|
+
out.body = raw.text;
|
|
40
|
+
if (!out.url && typeof raw.primary === "string" && page) {
|
|
41
|
+
try {
|
|
42
|
+
out.url = new URL(raw.primary, page).href;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* explicit missing below */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (raw.images !== undefined)
|
|
49
|
+
out.images = list(raw.images).map((v) => pick(v, ["src", "alt"]));
|
|
50
|
+
if (raw.media !== undefined)
|
|
51
|
+
out.media = list(raw.media).map((v) => pick(v, [
|
|
52
|
+
"kind",
|
|
53
|
+
"src",
|
|
54
|
+
"poster",
|
|
55
|
+
"label",
|
|
56
|
+
"duration",
|
|
57
|
+
"requiresViewing",
|
|
58
|
+
]));
|
|
59
|
+
if (raw.links !== undefined)
|
|
60
|
+
out.links = list(raw.links).flatMap((v) => typeof v === "string"
|
|
61
|
+
? [v]
|
|
62
|
+
: typeof object(v).href === "string"
|
|
63
|
+
? [
|
|
64
|
+
pick(v, ["href", "text", "label"]),
|
|
65
|
+
]
|
|
66
|
+
: []);
|
|
67
|
+
if (out.links)
|
|
68
|
+
out.links = out.links.filter((link) => isContentLink(link, out));
|
|
69
|
+
if (strings(raw.missing).length)
|
|
70
|
+
out.missing = strings(raw.missing);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function isContentLink(link, post) {
|
|
74
|
+
const href = typeof link === "string" ? link : link.href;
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(href, post.url ?? "https://x.com");
|
|
77
|
+
if (url.hostname !== "x.com")
|
|
78
|
+
return true;
|
|
79
|
+
if (/\/status\/\d+\/(?:analytics|quotes|retweets|likes|bookmarks)\/?$/.test(url.pathname))
|
|
80
|
+
return false;
|
|
81
|
+
if (post.author &&
|
|
82
|
+
url.pathname.toLowerCase() === `/${post.author}`.toLowerCase())
|
|
83
|
+
return false;
|
|
84
|
+
if (post.url && url.href === new URL(post.url).href)
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* Keep an observed unresolved link; do not infer its destination. */
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function person(value) {
|
|
93
|
+
const raw = object(value), out = pick(raw, [
|
|
94
|
+
"handle",
|
|
95
|
+
"url",
|
|
96
|
+
"blue",
|
|
97
|
+
"relationship",
|
|
98
|
+
"biography",
|
|
99
|
+
]);
|
|
100
|
+
for (const key of ["following", "followers"]) {
|
|
101
|
+
if (raw[key] !== undefined) {
|
|
102
|
+
out[key] =
|
|
103
|
+
raw[key] === null
|
|
104
|
+
? null
|
|
105
|
+
: pick(raw[key], ["value", "raw", "text", "precision", "url"]);
|
|
106
|
+
const counter = object(raw[key]);
|
|
107
|
+
if (out[key] && Array.isArray(counter.text))
|
|
108
|
+
out[key].text = strings(counter.text);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
function feedback(value) {
|
|
114
|
+
const raw = object(value), out = pick(raw, ["id", "pageComplete"]);
|
|
115
|
+
if (raw.metrics !== undefined)
|
|
116
|
+
out.metrics = list(raw.metrics).flatMap((v) => {
|
|
117
|
+
const label = typeof v === "string" ? v : object(v).label, value = object(v).value;
|
|
118
|
+
if (typeof label !== "string" ||
|
|
119
|
+
!/(?:回复|转帖|喜欢|书签|观看|查看|引用|replies|reply|reposts?|retweets?|likes?|bookmarks?|views?|quotes?)/i.test(label))
|
|
120
|
+
return [];
|
|
121
|
+
if (!/\d/.test(label) &&
|
|
122
|
+
typeof value !== "number" &&
|
|
123
|
+
typeof value !== "string")
|
|
124
|
+
return [];
|
|
125
|
+
return [typeof v === "string" ? v : pick(v, ["label", "value"])];
|
|
126
|
+
});
|
|
127
|
+
if (out.metrics)
|
|
128
|
+
out.metrics = [
|
|
129
|
+
...new Map(out.metrics.map((metric) => [JSON.stringify(metric), metric])).values(),
|
|
130
|
+
];
|
|
131
|
+
if (raw.replies !== undefined)
|
|
132
|
+
out.replies = list(raw.replies).map((v) => source(v));
|
|
133
|
+
if (raw.unconfirmedReplyIds !== undefined)
|
|
134
|
+
out.unconfirmedReplyIds = strings(raw.unconfirmedReplyIds);
|
|
135
|
+
if (strings(raw.missing).length)
|
|
136
|
+
out.missing = strings(raw.missing);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
export function readResult(operation, value, mode) {
|
|
140
|
+
const raw = object(value), out = pick(raw, [
|
|
141
|
+
"status",
|
|
142
|
+
"fresh",
|
|
143
|
+
"complete",
|
|
144
|
+
"pageComplete",
|
|
145
|
+
"evidenceRef",
|
|
146
|
+
"reason",
|
|
147
|
+
]);
|
|
148
|
+
if (out.reason)
|
|
149
|
+
out.reason = stableReason(out.reason);
|
|
150
|
+
if (!["observed", "needsDecision", "needsCapability"].includes(out.status))
|
|
151
|
+
out.status = "needsDecision";
|
|
152
|
+
if (out.status !== "observed" && out.complete === undefined)
|
|
153
|
+
out.complete = false;
|
|
154
|
+
if (raw.ids !== undefined)
|
|
155
|
+
out.ids = strings(raw.ids);
|
|
156
|
+
const missing = strings(raw.missing);
|
|
157
|
+
if (list(raw.alerts).length && !missing.includes("platform-alert"))
|
|
158
|
+
missing.push("platform-alert");
|
|
159
|
+
if (missing.length)
|
|
160
|
+
out.missing = missing;
|
|
161
|
+
const d = diagnostic(raw.diagnostic);
|
|
162
|
+
if (d)
|
|
163
|
+
out.diagnostic = d;
|
|
164
|
+
if (operation === "inspectAccount")
|
|
165
|
+
Object.assign(out, pick(raw, ["accountHandle", "accountURL"]));
|
|
166
|
+
if (mode === "receipt")
|
|
167
|
+
return out;
|
|
168
|
+
if (operation === "readPost") {
|
|
169
|
+
const posts = list(raw.posts);
|
|
170
|
+
if (posts.length === 1)
|
|
171
|
+
out.post = source(posts[0], typeof raw.page === "string" ? raw.page : undefined);
|
|
172
|
+
}
|
|
173
|
+
else if (operation === "readPerson" && typeof raw.handle === "string")
|
|
174
|
+
out.person = person(raw);
|
|
175
|
+
else if (operation === "readCreator" && raw.profile && raw.meta) {
|
|
176
|
+
const meta = object(raw.meta);
|
|
177
|
+
const header = typeof meta.headerText === "string" ? meta.headerText : "";
|
|
178
|
+
out.person = person(raw.profile);
|
|
179
|
+
out.creator = {
|
|
180
|
+
...pick(raw, ["scrolls", "observedAt"]),
|
|
181
|
+
...pick(meta, ["totalPostsText"]),
|
|
182
|
+
name: header.split("\n")[0] || null,
|
|
183
|
+
joined: header.match(/\d{4}年\d{1,2}月 加入/)?.[0] ?? null,
|
|
184
|
+
externalLinks: strings(meta.externalLinks).filter((href) => {
|
|
185
|
+
try {
|
|
186
|
+
const host = new URL(href).hostname;
|
|
187
|
+
return !/(^|\.)(?:x\.com|twitter\.com)$/.test(host);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}),
|
|
193
|
+
};
|
|
194
|
+
out.sources = list(raw.sources).map((v) => {
|
|
195
|
+
const row = object(v);
|
|
196
|
+
const post = source(v, typeof raw.page === "string" ? raw.page : undefined);
|
|
197
|
+
post.metrics = [
|
|
198
|
+
...new Set([
|
|
199
|
+
...strings(row.metricDescriptions),
|
|
200
|
+
...list(row.buttons).map((b) => object(b).label),
|
|
201
|
+
...list(row.analytics).map((a) => object(a).label),
|
|
202
|
+
].filter((label) => typeof label === "string" &&
|
|
203
|
+
/(?:回复|转帖|喜欢|书签|观看|查看|replies|reposts?|likes?|bookmarks?|views?)/i.test(label) &&
|
|
204
|
+
/\d/.test(label))),
|
|
205
|
+
];
|
|
206
|
+
Object.assign(post, pick(row, ["quoted", "videoElements"]));
|
|
207
|
+
return post;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else if (raw.sources !== undefined)
|
|
211
|
+
out.sources = list(raw.sources).map((v) => source(v));
|
|
212
|
+
else if (raw.feedback !== undefined)
|
|
213
|
+
out.feedback = list(raw.feedback).map(feedback);
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
export function decisions(value) {
|
|
217
|
+
return list(value).map((v) => {
|
|
218
|
+
const raw = object(v), out = pick(raw, [
|
|
219
|
+
"actionId",
|
|
220
|
+
"kind",
|
|
221
|
+
"status",
|
|
222
|
+
"reason",
|
|
223
|
+
"resumeAt",
|
|
224
|
+
"owner",
|
|
225
|
+
"evidenceRef",
|
|
226
|
+
]);
|
|
227
|
+
if (out.reason)
|
|
228
|
+
out.reason = stableReason(out.reason);
|
|
229
|
+
if (strings(raw.missing).length)
|
|
230
|
+
out.missing = strings(raw.missing);
|
|
231
|
+
return out;
|
|
232
|
+
});
|
|
233
|
+
}
|