@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.
Files changed (110) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +41 -2
  3. package/cli/main.js +219 -0
  4. package/dist/api.d.ts +10 -0
  5. package/dist/api.js +308 -0
  6. package/dist/boundary.d.ts +2 -0
  7. package/dist/browser-common.d.ts +4 -0
  8. package/dist/browser-common.js +29 -0
  9. package/dist/browser-dom-capabilities.d.ts +4 -0
  10. package/dist/browser-dom-capabilities.js +110 -0
  11. package/dist/browser-dom-dependencies.d.ts +16 -0
  12. package/dist/browser-dom-editing.d.ts +148 -0
  13. package/dist/browser-dom-editing.js +386 -0
  14. package/dist/browser-dom-navigation.d.ts +9 -0
  15. package/dist/browser-dom-navigation.js +165 -0
  16. package/dist/browser-dom-network.d.ts +1 -0
  17. package/dist/browser-dom-network.js +128 -0
  18. package/dist/browser-dom-submission.d.ts +2 -0
  19. package/dist/browser-dom-submission.js +82 -0
  20. package/dist/browser-dom.d.ts +2 -0
  21. package/dist/browser-dom.js +103 -0
  22. package/dist/browser-native.d.ts +1 -0
  23. package/dist/browser-native.js +215 -0
  24. package/dist/browser.d.ts +4 -0
  25. package/dist/browser.js +147 -0
  26. package/dist/creator.d.ts +29 -0
  27. package/dist/creator.js +164 -0
  28. package/dist/growth-batch.d.ts +5 -0
  29. package/dist/growth-batch.js +168 -0
  30. package/dist/growth-constants.d.ts +5 -0
  31. package/dist/growth-constants.js +13 -0
  32. package/dist/growth-dependencies.d.ts +33 -0
  33. package/dist/growth-execution.d.ts +11 -0
  34. package/dist/growth-execution.js +315 -0
  35. package/dist/growth-parallel.d.ts +4 -0
  36. package/dist/growth-parallel.js +301 -0
  37. package/dist/growth-plan.d.ts +4 -0
  38. package/dist/growth-plan.js +207 -0
  39. package/dist/growth-policy.d.ts +9 -0
  40. package/dist/growth-policy.js +181 -0
  41. package/dist/growth-publishing.d.ts +5 -0
  42. package/dist/growth-publishing.js +240 -0
  43. package/dist/growth-reading.d.ts +4 -0
  44. package/dist/growth-reading.js +135 -0
  45. package/dist/growth-recovery.d.ts +6 -0
  46. package/dist/growth-recovery.js +158 -0
  47. package/dist/growth-state.d.ts +8 -0
  48. package/dist/growth-state.js +183 -0
  49. package/dist/growth-tasks.d.ts +17 -0
  50. package/dist/growth-tasks.js +101 -0
  51. package/dist/growth.d.ts +2 -0
  52. package/dist/growth.js +138 -0
  53. package/dist/index.d.ts +4 -0
  54. package/dist/index.js +22 -0
  55. package/dist/observation.d.ts +1 -0
  56. package/dist/observation.js +2 -0
  57. package/dist/platform-capabilities.d.ts +4 -0
  58. package/dist/platform-capabilities.js +19 -0
  59. package/dist/platform-collections.d.ts +7 -0
  60. package/dist/platform-collections.js +223 -0
  61. package/dist/platform-dependencies.d.ts +43 -0
  62. package/dist/platform-diagnostics.d.ts +9 -0
  63. package/dist/platform-diagnostics.js +51 -0
  64. package/dist/platform-observation.d.ts +8 -0
  65. package/dist/platform-observation.js +75 -0
  66. package/dist/platform-rules.d.ts +17 -0
  67. package/dist/platform-rules.js +41 -0
  68. package/dist/platform-targets.d.ts +7 -0
  69. package/dist/platform-targets.js +129 -0
  70. package/dist/platform-writing.d.ts +8 -0
  71. package/dist/platform-writing.js +171 -0
  72. package/dist/platform.d.ts +2 -0
  73. package/dist/platform.js +60 -0
  74. package/dist/readers-actions.d.ts +2 -0
  75. package/dist/readers-actions.js +66 -0
  76. package/dist/readers-ax-host.d.ts +2 -0
  77. package/dist/readers-ax-host.js +125 -0
  78. package/dist/readers-ax-parser.d.ts +1 -0
  79. package/dist/readers-ax-parser.js +69 -0
  80. package/dist/readers-ax-program.d.ts +1 -0
  81. package/dist/readers-ax-tree.d.ts +19 -0
  82. package/dist/readers-ax-tree.js +110 -0
  83. package/dist/readers-ax-views.d.ts +87 -0
  84. package/dist/readers-ax-views.js +247 -0
  85. package/dist/readers-cli.d.ts +6 -0
  86. package/dist/readers-composer.d.ts +2 -0
  87. package/dist/readers-composer.js +131 -0
  88. package/dist/readers-navigation.d.ts +2 -0
  89. package/dist/readers-navigation.js +66 -0
  90. package/dist/readers-posts.d.ts +2 -0
  91. package/dist/readers-posts.js +141 -0
  92. package/dist/readers-profile.d.ts +3 -0
  93. package/dist/readers-profile.js +157 -0
  94. package/dist/readers.d.ts +9 -0
  95. package/dist/results.d.ts +6 -0
  96. package/dist/results.js +233 -0
  97. package/dist/round-state.d.ts +3 -0
  98. package/dist/round-state.js +155 -0
  99. package/dist/store.d.ts +30 -0
  100. package/dist/store.js +175 -0
  101. package/dist/types.d.ts +407 -0
  102. package/generated/observation.d.mts +1 -0
  103. package/generated/observation.mjs +515 -0
  104. package/package.json +29 -3
  105. package/skill/SKILL.md +33 -0
  106. package/skill/references/api.md +10 -0
  107. package/skill/references/reads.md +23 -0
  108. package/skill/references/recovery.md +25 -0
  109. package/skill/references/setup.md +23 -0
  110. 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,3 @@
1
+ export declare function readProfile({ handle }?: any): any;
2
+ export declare function readPeople({ offset, limit }?: any): any;
3
+ export declare function screenCandidates(profiles: any, { minRatio }?: any): any;
@@ -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[];
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export declare function checkRound(state: any, { now, evidenceExists }?: any): any;
2
+ export declare function readRound(markdown: any): any;
3
+ export declare function runRoundCLI(): void;