@harness.farm/social-cli 0.1.0 → 0.1.2
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/README.md +213 -0
- package/dist/adapters/base.js +2 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/xiaohongshu.js +314 -0
- package/dist/browser/cdp.js +106 -0
- package/dist/browser/runner.js +75 -0
- package/dist/browser/session.js +38 -0
- package/dist/cli.js +99 -0
- package/dist/output/table.js +43 -0
- package/dist/runner/step-executor.js +142 -0
- package/dist/runner/yaml-runner.js +368 -0
- package/dist/scripts/explore-bili.js +37 -0
- package/dist/scripts/explore-douyin.js +30 -0
- package/dist/scripts/explore-x.js +31 -0
- package/package.json +2 -1
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML Runner — parses adapter YAML and executes commands step by step.
|
|
3
|
+
*
|
|
4
|
+
* YAML schema:
|
|
5
|
+
* platform: string
|
|
6
|
+
* login_url: string
|
|
7
|
+
* login_check:
|
|
8
|
+
* cookie: string # cookie name that indicates logged-in state
|
|
9
|
+
* commands:
|
|
10
|
+
* <name>:
|
|
11
|
+
* args: string[] # positional arg names
|
|
12
|
+
* steps: Step[]
|
|
13
|
+
*
|
|
14
|
+
* Step types:
|
|
15
|
+
* - open: "{{url}}"
|
|
16
|
+
* - click: ".selector"
|
|
17
|
+
* - click: { text: "visible text" }
|
|
18
|
+
* - fill: { selector: ".sel", value: "{{text}}" }
|
|
19
|
+
* - type_rich: { selector: ".sel", value: "{{text}}" } # for contenteditable
|
|
20
|
+
* - wait: 3000
|
|
21
|
+
* - wait: { selector: ".sel" }
|
|
22
|
+
* - eval: "js expression"
|
|
23
|
+
* - capture: { name: varName, eval: "js" }
|
|
24
|
+
* - upload: { selector: ".sel", file: "{{file}}" }
|
|
25
|
+
* - screenshot: path.png
|
|
26
|
+
* - extract: { selector, fields: { key: ".sel" | { selector, attr } } }
|
|
27
|
+
* - return: [ { field, value } ] → builds output table
|
|
28
|
+
* - assert: { eval: "js", message: "error msg" }
|
|
29
|
+
*/
|
|
30
|
+
import fs from 'fs';
|
|
31
|
+
import { parse as parseYaml } from 'yaml';
|
|
32
|
+
import { StepExecutor } from './step-executor.js';
|
|
33
|
+
import { renderTable } from '../output/table.js';
|
|
34
|
+
import { connectTab } from '../browser/cdp.js';
|
|
35
|
+
import { loadSession } from '../browser/session.js';
|
|
36
|
+
// ─── Runner ───────────────────────────────────────────────────────────────────
|
|
37
|
+
export async function runYamlCommand(adapterPath, commandName, argValues, cdpPort = 9222) {
|
|
38
|
+
// 1. Load & parse YAML
|
|
39
|
+
const raw = fs.readFileSync(adapterPath, 'utf-8');
|
|
40
|
+
const adapter = parseYaml(raw);
|
|
41
|
+
const cmdDef = adapter.commands[commandName];
|
|
42
|
+
if (!cmdDef) {
|
|
43
|
+
const available = Object.keys(adapter.commands).join(', ');
|
|
44
|
+
throw new Error(`Unknown command "${commandName}". Available: ${available}`);
|
|
45
|
+
}
|
|
46
|
+
// 2. Build variables map from args
|
|
47
|
+
const vars = {};
|
|
48
|
+
(cmdDef.args ?? []).forEach((name, i) => {
|
|
49
|
+
vars[name] = argValues[i] ?? '';
|
|
50
|
+
});
|
|
51
|
+
// 3. Ensure logged in → resolve tab ws URL
|
|
52
|
+
const wsUrl = await resolveTabWsUrl(adapter, cdpPort);
|
|
53
|
+
const exec = new StepExecutor(wsUrl);
|
|
54
|
+
exec.connect();
|
|
55
|
+
const cdpClient = await connectTab(cdpPort);
|
|
56
|
+
console.log(`✅ 已连接 ${adapter.platform}`);
|
|
57
|
+
console.log(`\n🔍 执行: ${adapter.platform} ${commandName} ${argValues.join(' ')}\n`);
|
|
58
|
+
// 4. Execute steps
|
|
59
|
+
let extractedRows = null;
|
|
60
|
+
let returnRows = null;
|
|
61
|
+
for (const step of cmdDef.steps) {
|
|
62
|
+
const key = Object.keys(step)[0];
|
|
63
|
+
const val = step[key];
|
|
64
|
+
switch (key) {
|
|
65
|
+
case 'open': {
|
|
66
|
+
const url = interpolate(val, vars);
|
|
67
|
+
console.log(` → open ${url}`);
|
|
68
|
+
exec.open(url);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case 'click': {
|
|
72
|
+
if (typeof val === 'string') {
|
|
73
|
+
// plain selector
|
|
74
|
+
const sel = interpolate(val, vars);
|
|
75
|
+
console.log(` → click "${sel}"`);
|
|
76
|
+
throwIfFail(exec.click(sel), `click failed: ${sel}`);
|
|
77
|
+
}
|
|
78
|
+
else if (typeof val === 'object' && val !== null && 'text' in val) {
|
|
79
|
+
const text = interpolate(val.text, vars);
|
|
80
|
+
console.log(` → click text="${text}"`);
|
|
81
|
+
throwIfFail(exec.clickText(text), `text not found: "${text}"`);
|
|
82
|
+
}
|
|
83
|
+
else if (typeof val === 'object' && val !== null && 'selector' in val) {
|
|
84
|
+
const sel = interpolate(val.selector, vars);
|
|
85
|
+
console.log(` → click selector="${sel}"`);
|
|
86
|
+
throwIfFail(exec.click(sel), `click failed: ${sel}`);
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case 'fill': {
|
|
91
|
+
const { selector, value } = val;
|
|
92
|
+
const sel = interpolate(selector, vars);
|
|
93
|
+
const v = interpolate(value, vars);
|
|
94
|
+
console.log(` → fill "${sel}" = "${v.slice(0, 40)}"`);
|
|
95
|
+
throwIfFail(exec.fill(sel, v), `fill failed: ${sel}`);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'type_rich': {
|
|
99
|
+
const { selector, value } = val;
|
|
100
|
+
const sel = interpolate(selector, vars);
|
|
101
|
+
const v = interpolate(value, vars);
|
|
102
|
+
console.log(` → type_rich "${sel}" = "${v.slice(0, 40)}"`);
|
|
103
|
+
throwIfFail(exec.typeContentEditable(sel, v), `type_rich failed: ${sel}`);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'wait': {
|
|
107
|
+
if (typeof val === 'number') {
|
|
108
|
+
console.log(` → wait ${val}ms`);
|
|
109
|
+
exec.wait(val);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const sel = interpolate(val.selector, vars);
|
|
113
|
+
console.log(` → wait selector="${sel}"`);
|
|
114
|
+
exec.wait(sel);
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'eval': {
|
|
119
|
+
const js = interpolate(val, vars);
|
|
120
|
+
console.log(` → eval ...`);
|
|
121
|
+
exec.eval(js);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 'capture': {
|
|
125
|
+
const { name, eval: js } = val;
|
|
126
|
+
const interpolatedJs = interpolate(js, vars);
|
|
127
|
+
console.log(` → capture ${name}`);
|
|
128
|
+
const r = exec.eval(interpolatedJs);
|
|
129
|
+
vars[name] = String(r.value ?? '');
|
|
130
|
+
console.log(` ${name} = ${vars[name].slice(0, 60)}`);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
case 'upload': {
|
|
134
|
+
const { selector, file } = val;
|
|
135
|
+
const sel = interpolate(selector, vars);
|
|
136
|
+
const f = interpolate(file, vars);
|
|
137
|
+
console.log(` → upload "${sel}" ← ${f}`);
|
|
138
|
+
throwIfFail(exec.upload(sel, f), `upload failed: ${sel}`);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case 'screenshot': {
|
|
142
|
+
const p = interpolate(val, vars);
|
|
143
|
+
console.log(` → screenshot → ${p}`);
|
|
144
|
+
exec.screenshot(p);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'extract': {
|
|
148
|
+
const def = val;
|
|
149
|
+
console.log(` → extract "${def.selector}"`);
|
|
150
|
+
extractedRows = runExtract(exec, def, vars);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 'return': {
|
|
154
|
+
returnRows = val.map(r => ({
|
|
155
|
+
field: interpolate(r.field, vars),
|
|
156
|
+
value: interpolate(r.value, vars),
|
|
157
|
+
}));
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'assert': {
|
|
161
|
+
const { eval: js, message } = val;
|
|
162
|
+
const r = exec.eval(interpolate(js, vars));
|
|
163
|
+
if (!r.value)
|
|
164
|
+
throw new Error(message ?? `Assertion failed: ${js}`);
|
|
165
|
+
console.log(` → assert ✅`);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'key': {
|
|
169
|
+
const k = interpolate(val, vars);
|
|
170
|
+
console.log(` → key "${k}"`);
|
|
171
|
+
// For Enter/special keys that need to reach the focused element, use CDPClient directly
|
|
172
|
+
if (k === 'Enter' || k === 'Control+Enter') {
|
|
173
|
+
const modifiers = k.startsWith('Control') ? 2 : 0;
|
|
174
|
+
await cdpClient.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, modifiers });
|
|
175
|
+
await cdpClient.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, modifiers });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
exec.pressKey(k);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'keyboard_insert': {
|
|
183
|
+
// Send text char-by-char via CDP Input.dispatchKeyEvent 'char' events
|
|
184
|
+
// This triggers Draft.js / React input handlers correctly
|
|
185
|
+
const text = interpolate(val, vars);
|
|
186
|
+
console.log(` → keyboard_insert "${text.slice(0, 40)}"`);
|
|
187
|
+
for (const char of text) {
|
|
188
|
+
const code = char.codePointAt(0);
|
|
189
|
+
await cdpClient.send('Input.dispatchKeyEvent', { type: 'keyDown', key: char, windowsVirtualKeyCode: code });
|
|
190
|
+
await cdpClient.send('Input.dispatchKeyEvent', { type: 'char', key: char, text: char });
|
|
191
|
+
await cdpClient.send('Input.dispatchKeyEvent', { type: 'keyUp', key: char, windowsVirtualKeyCode: code });
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case 'insert_text': {
|
|
196
|
+
// CDP Input.insertText — inserts text directly into the focused element,
|
|
197
|
+
// works even inside shadow DOM (unlike keyboard_insert which targets document.activeElement)
|
|
198
|
+
const text = interpolate(val, vars);
|
|
199
|
+
console.log(` → insert_text "${text.slice(0, 40)}"`);
|
|
200
|
+
await cdpClient.send('Input.insertText', { text });
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// 5. Render output
|
|
206
|
+
console.log('');
|
|
207
|
+
if (extractedRows) {
|
|
208
|
+
const fields = Object.keys(extractedRows[0] ?? {});
|
|
209
|
+
const columns = fields.map(k => ({
|
|
210
|
+
key: k,
|
|
211
|
+
header: k,
|
|
212
|
+
width: k === 'index' ? 4 : k === 'link' ? 52 : k === 'title' ? 36 : 24,
|
|
213
|
+
}));
|
|
214
|
+
renderTable(columns, extractedRows);
|
|
215
|
+
}
|
|
216
|
+
else if (returnRows) {
|
|
217
|
+
renderTable([{ key: 'field', header: '字段', width: 12 }, { key: 'value', header: '值', width: 50 }], returnRows);
|
|
218
|
+
}
|
|
219
|
+
cdpClient.close();
|
|
220
|
+
}
|
|
221
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
222
|
+
/** Replace {{expr}} placeholders — supports plain var names and JS expressions */
|
|
223
|
+
function interpolate(template, vars) {
|
|
224
|
+
return template.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
225
|
+
const trimmed = expr.trim();
|
|
226
|
+
// Plain variable name: fast path
|
|
227
|
+
if (/^\w+$/.test(trimmed))
|
|
228
|
+
return vars[trimmed] ?? '';
|
|
229
|
+
// JS expression: inject vars as locals and eval
|
|
230
|
+
try {
|
|
231
|
+
const keys = Object.keys(vars);
|
|
232
|
+
const vals = keys.map(k => vars[k]);
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
234
|
+
return String(new Function(...keys, `return (${trimmed})`)(...vals) ?? '');
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return '';
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function throwIfFail(r, msg) {
|
|
242
|
+
if (!r.ok)
|
|
243
|
+
throw new Error(`${msg}: ${r.error ?? ''}`);
|
|
244
|
+
}
|
|
245
|
+
/** Run an extract step: scrape a list of items with named fields */
|
|
246
|
+
function runExtract(exec, def, vars) {
|
|
247
|
+
const fieldsJson = JSON.stringify(def.fields);
|
|
248
|
+
const js = `(function(){
|
|
249
|
+
var fields = ${fieldsJson};
|
|
250
|
+
var results = [];
|
|
251
|
+
document.querySelectorAll(${JSON.stringify(def.selector)}).forEach(function(item, i) {
|
|
252
|
+
var row = { index: i + 1 };
|
|
253
|
+
Object.keys(fields).forEach(function(key) {
|
|
254
|
+
var spec = fields[key];
|
|
255
|
+
if (typeof spec === 'string') {
|
|
256
|
+
var el = item.querySelector(spec);
|
|
257
|
+
row[key] = el ? el.textContent.trim() : '';
|
|
258
|
+
} else {
|
|
259
|
+
var el2 = item.querySelector(spec.selector);
|
|
260
|
+
row[key] = el2 ? (spec.attr === 'href' ? (el2.href || el2.getAttribute(spec.attr)) : el2.getAttribute(spec.attr)) || el2.textContent.trim() : '';
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
if (Object.values(row).some(function(v){ return v && v !== i + 1; })) results.push(row);
|
|
264
|
+
});
|
|
265
|
+
return JSON.stringify(results);
|
|
266
|
+
})()`;
|
|
267
|
+
const r = exec.eval(js);
|
|
268
|
+
try {
|
|
269
|
+
return JSON.parse(String(r.value ?? '[]'));
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Find the ws URL of the right tab, with full user onboarding if needed */
|
|
276
|
+
async function resolveTabWsUrl(adapter, cdpPort) {
|
|
277
|
+
// Step 1: Check Chrome is running with CDP
|
|
278
|
+
let tabs;
|
|
279
|
+
try {
|
|
280
|
+
const res = await fetch(`http://localhost:${cdpPort}/json`, { signal: AbortSignal.timeout(2000) });
|
|
281
|
+
tabs = (await res.json());
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
console.error(`\n❌ 无法连接到 Chrome CDP (端口 ${cdpPort})`);
|
|
285
|
+
console.error('\n👉 请先启动 Chrome,开启远程调试:\n');
|
|
286
|
+
console.error(` macOS: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome \\`);
|
|
287
|
+
console.error(` --remote-debugging-port=${cdpPort} \\`);
|
|
288
|
+
console.error(` --user-data-dir=$HOME/.cdp-scraper/chrome-profile\n`);
|
|
289
|
+
console.error(` Windows: chrome.exe --remote-debugging-port=${cdpPort} --user-data-dir=%USERPROFILE%\\.cdp-scraper\\chrome-profile\n`);
|
|
290
|
+
console.error(` Linux: google-chrome --remote-debugging-port=${cdpPort} --user-data-dir=~/.cdp-scraper/chrome-profile\n`);
|
|
291
|
+
console.error('启动后重新运行此命令。');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
const pageTabs = tabs.filter(t => t.type === 'page');
|
|
295
|
+
if (pageTabs.length === 0) {
|
|
296
|
+
console.error('❌ Chrome 中没有打开的页面,请至少保持一个标签页打开。');
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
// Step 2: Prefer a tab already on the platform domain
|
|
300
|
+
const domain = new URL(adapter.login_url).hostname.replace('www.', '');
|
|
301
|
+
const existing = pageTabs.find(t => t.url.includes(domain) && !t.url.includes('creator'));
|
|
302
|
+
if (existing)
|
|
303
|
+
return existing.webSocketDebuggerUrl;
|
|
304
|
+
// Step 3: Check saved session cookie
|
|
305
|
+
const cookies = loadSession(adapter.platform);
|
|
306
|
+
const hasCookie = cookies?.some(c => c.name === adapter.login_check.cookie);
|
|
307
|
+
if (hasCookie) {
|
|
308
|
+
const page = pageTabs[0];
|
|
309
|
+
return page.webSocketDebuggerUrl;
|
|
310
|
+
}
|
|
311
|
+
// Step 4: Not logged in — guide user through login
|
|
312
|
+
console.log(`\n🔑 需要登录 ${adapter.platform}`);
|
|
313
|
+
console.log('─'.repeat(50));
|
|
314
|
+
// Open the platform login page in the first tab via agent-browser
|
|
315
|
+
const firstTab = pageTabs[0].webSocketDebuggerUrl;
|
|
316
|
+
console.log(`\n 正在打开登录页: ${adapter.login_url}`);
|
|
317
|
+
console.log(` (使用 agent-browser 连接到: ${firstTab})\n`);
|
|
318
|
+
const { execSync } = await import('child_process');
|
|
319
|
+
try {
|
|
320
|
+
execSync(`agent-browser connect '${firstTab}'`, {
|
|
321
|
+
env: { ...process.env, AGENT_BROWSER_JSON: '1' },
|
|
322
|
+
timeout: 5000,
|
|
323
|
+
encoding: 'utf8',
|
|
324
|
+
});
|
|
325
|
+
execSync(`agent-browser open '${adapter.login_url}'`, {
|
|
326
|
+
env: { ...process.env, AGENT_BROWSER_JSON: '1' },
|
|
327
|
+
timeout: 10000,
|
|
328
|
+
encoding: 'utf8',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
console.log(` ⚠️ 无法自动打开页面,请手动在 Chrome 中访问: ${adapter.login_url}`);
|
|
333
|
+
}
|
|
334
|
+
console.log(`👀 请在 Chrome 中完成登录 ${adapter.platform},然后按 Enter...`);
|
|
335
|
+
await waitForEnter();
|
|
336
|
+
// Step 5: Verify login by checking for the cookie
|
|
337
|
+
const resTabs = await fetch(`http://localhost:${cdpPort}/json`);
|
|
338
|
+
const freshTabs = (await resTabs.json());
|
|
339
|
+
const freshPage = freshTabs.filter(t => t.type === 'page')[0];
|
|
340
|
+
if (!freshPage) {
|
|
341
|
+
console.error('❌ Chrome 中没有打开的页面');
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
// Save session from the browser
|
|
345
|
+
const { connectTab } = await import('../browser/cdp.js');
|
|
346
|
+
const { captureSession } = await import('../browser/session.js');
|
|
347
|
+
const client = await connectTab(cdpPort);
|
|
348
|
+
const saved = await captureSession(client, adapter.platform);
|
|
349
|
+
client.close();
|
|
350
|
+
const ok = saved.some(c => c.name === adapter.login_check.cookie);
|
|
351
|
+
if (!ok) {
|
|
352
|
+
console.error(`❌ 未检测到登录 cookie (${adapter.login_check.cookie}),请确认已登录后重试`);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
console.log(`✅ 登录成功,session 已保存\n`);
|
|
356
|
+
return freshPage.webSocketDebuggerUrl;
|
|
357
|
+
}
|
|
358
|
+
function waitForEnter() {
|
|
359
|
+
return new Promise((resolve) => {
|
|
360
|
+
process.stdin.setRawMode?.(false);
|
|
361
|
+
process.stdin.resume();
|
|
362
|
+
process.stdout.write(' > ');
|
|
363
|
+
process.stdin.once('data', () => {
|
|
364
|
+
process.stdin.pause();
|
|
365
|
+
resolve();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { connectTab } from '../browser/cdp.js';
|
|
2
|
+
const client = await connectTab(9222);
|
|
3
|
+
await client.send('Page.bringToFront');
|
|
4
|
+
await new Promise(r => setTimeout(r, 300));
|
|
5
|
+
// Deep focus into bili-comments shadow DOM
|
|
6
|
+
const focused = await client.eval(`(function(){
|
|
7
|
+
var host = document.querySelector("bili-comments");
|
|
8
|
+
if(!host) return "no bili-comments";
|
|
9
|
+
function deepFocus(root){
|
|
10
|
+
var sr = root.shadowRoot;
|
|
11
|
+
if(!sr) return null;
|
|
12
|
+
var editor = sr.querySelector(".brt-editor");
|
|
13
|
+
if(editor) { editor.click(); editor.focus(); return "ok"; }
|
|
14
|
+
var els = sr.querySelectorAll("*");
|
|
15
|
+
for(var i=0; i<els.length; i++){
|
|
16
|
+
var r = deepFocus(els[i]);
|
|
17
|
+
if(r) return r;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return deepFocus(host) || "not found";
|
|
22
|
+
})()`);
|
|
23
|
+
console.log('focus result:', focused);
|
|
24
|
+
await new Promise(r => setTimeout(r, 500));
|
|
25
|
+
// Try Input.insertText
|
|
26
|
+
await client.send('Input.insertText', { text: 'hello shadow insertText' });
|
|
27
|
+
await new Promise(r => setTimeout(r, 500));
|
|
28
|
+
const content = await client.eval(`(function(){
|
|
29
|
+
var ed = document.querySelector("bili-comments")
|
|
30
|
+
.shadowRoot.querySelector("bili-comments-header-renderer")
|
|
31
|
+
.shadowRoot.querySelector("bili-comment-box")
|
|
32
|
+
.shadowRoot.querySelector("bili-comment-rich-textarea")
|
|
33
|
+
.shadowRoot.querySelector(".brt-editor");
|
|
34
|
+
return ed ? JSON.stringify({ text: ed.textContent.trim(), html: ed.innerHTML.slice(0,100) }) : "not found";
|
|
35
|
+
})()`);
|
|
36
|
+
console.log('editor content:', content);
|
|
37
|
+
client.close();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { connectTab } from '../browser/cdp.js';
|
|
2
|
+
const client = await connectTab(9222);
|
|
3
|
+
await client.send('Page.bringToFront');
|
|
4
|
+
await new Promise(r => setTimeout(r, 200));
|
|
5
|
+
// Get follow button (JbfEzak6) coordinates
|
|
6
|
+
const coords = await client.eval(`(function(){
|
|
7
|
+
var el = document.querySelector('.JbfEzak6');
|
|
8
|
+
if(!el) return 'null';
|
|
9
|
+
var r = el.getBoundingClientRect();
|
|
10
|
+
return JSON.stringify({ x: Math.round(r.x + r.width/2), y: Math.round(r.y + r.height/2) });
|
|
11
|
+
})()`);
|
|
12
|
+
console.log('follow btn coords:', coords);
|
|
13
|
+
// check current state by checking parent avatar area
|
|
14
|
+
const beforeInfo = await client.eval(`JSON.stringify({
|
|
15
|
+
JbfEzak6Count: document.querySelectorAll('.JbfEzak6').length,
|
|
16
|
+
hasFollowedState: document.body.innerHTML.includes('已关注')
|
|
17
|
+
})`);
|
|
18
|
+
console.log('before:', JSON.parse(beforeInfo));
|
|
19
|
+
const { x, y } = JSON.parse(coords);
|
|
20
|
+
await client.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y });
|
|
21
|
+
await client.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
|
22
|
+
await client.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
|
23
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
24
|
+
const afterInfo = await client.eval(`JSON.stringify({
|
|
25
|
+
JbfEzak6Count: document.querySelectorAll('.JbfEzak6').length,
|
|
26
|
+
hasFollowedState: document.body.innerHTML.includes('已关注'),
|
|
27
|
+
bodySnippet: [...document.querySelectorAll('[class*=follow]')].map(function(el){ return el.className.slice(0,60) + ':' + el.textContent.trim().slice(0,20); }).join(' | ')
|
|
28
|
+
})`);
|
|
29
|
+
console.log('after:', JSON.parse(afterInfo));
|
|
30
|
+
client.close();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { newTab } from '../browser/cdp.js';
|
|
2
|
+
const client = await newTab(9222);
|
|
3
|
+
await client.navigate('https://x.com/search?q=AI+law&src=typed_query&f=top', 4000);
|
|
4
|
+
const info = await client.eval(`JSON.stringify({
|
|
5
|
+
url: location.href,
|
|
6
|
+
tweetItems: (() => {
|
|
7
|
+
var tweets = [...document.querySelectorAll('[data-testid="tweet"]')];
|
|
8
|
+
return tweets.slice(0,3).map(function(t){
|
|
9
|
+
return {
|
|
10
|
+
text: t.querySelector('[data-testid="tweetText"]')?.textContent?.trim()?.slice(0,60) || '',
|
|
11
|
+
user: t.querySelector('[data-testid="User-Name"]')?.textContent?.trim()?.slice(0,30) || '',
|
|
12
|
+
link: (() => {
|
|
13
|
+
var a = [...t.querySelectorAll('a')].find(function(a){ return /\\/status\\//.test(a.href); });
|
|
14
|
+
return a ? a.href : '';
|
|
15
|
+
})(),
|
|
16
|
+
time: t.querySelector('time')?.getAttribute('datetime') || ''
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
})()
|
|
20
|
+
})`);
|
|
21
|
+
const d = JSON.parse(info);
|
|
22
|
+
console.log('URL:', d.url);
|
|
23
|
+
console.log('\n搜索结果结构:');
|
|
24
|
+
d.tweetItems.forEach((t, i) => {
|
|
25
|
+
console.log(`\n[${i + 1}]`);
|
|
26
|
+
console.log(' text:', t.text);
|
|
27
|
+
console.log(' user:', t.user);
|
|
28
|
+
console.log(' link:', t.link);
|
|
29
|
+
console.log(' time:', t.time);
|
|
30
|
+
});
|
|
31
|
+
client.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness.farm/social-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "CDP-based social media automation CLI — X, 小红书, 抖音, B站, Temu",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"adapters"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
+
"prepublishOnly": "tsc",
|
|
17
18
|
"dev": "tsx src/cli.ts",
|
|
18
19
|
"build": "tsc",
|
|
19
20
|
"x": "tsx src/cli.ts x",
|