Add clipboard paste functionality and documentation for X Articles and regular posts
- Implemented `paste-from-clipboard.ts` script to send real paste keystrokes across macOS, Linux, and Windows. - Created detailed guides for publishing articles to X Articles and posting regular content, including usage examples and troubleshooting tips. - Added support for Markdown formatting and image handling in X Articles. - Documented manual workflows for image pasting and browser interactions using Playwright MCP.
This commit is contained in:
parent
da920bb830
commit
e3d2f5d03f
|
|
@ -6,7 +6,7 @@
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "0.4.0"
|
"version": "0.4.1"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
19
README.md
19
README.md
|
|
@ -34,6 +34,25 @@ Run the following command in Claude Code:
|
||||||
/plugin install content-skills@baoyu-skills
|
/plugin install content-skills@baoyu-skills
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Option 3: Ask the Agent**
|
||||||
|
|
||||||
|
Simply tell Claude Code:
|
||||||
|
|
||||||
|
> Please install Skills from github.com/JimLiu/baoyu-skills
|
||||||
|
|
||||||
|
## Update Skills
|
||||||
|
|
||||||
|
To update skills to the latest version:
|
||||||
|
|
||||||
|
1. Run `/plugin` in Claude Code
|
||||||
|
2. Switch to **Marketplaces** tab (use arrow keys or Tab)
|
||||||
|
3. Select **baoyu-skills**
|
||||||
|
4. Choose **Update marketplace**
|
||||||
|
|
||||||
|
You can also **Enable auto-update** to get the latest versions automatically.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Available Skills
|
## Available Skills
|
||||||
|
|
||||||
### gemini-web
|
### gemini-web
|
||||||
|
|
|
||||||
19
README.zh.md
19
README.zh.md
|
|
@ -34,6 +34,25 @@
|
||||||
/plugin install content-skills@baoyu-skills
|
/plugin install content-skills@baoyu-skills
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**方式三:告诉 Agent**
|
||||||
|
|
||||||
|
直接告诉 Claude Code:
|
||||||
|
|
||||||
|
> 请帮我安装 github.com/JimLiu/baoyu-skills 中的 Skills
|
||||||
|
|
||||||
|
## 更新技能
|
||||||
|
|
||||||
|
更新技能到最新版本:
|
||||||
|
|
||||||
|
1. 在 Claude Code 中运行 `/plugin`
|
||||||
|
2. 切换到 **Marketplaces** 标签页(使用方向键或 Tab)
|
||||||
|
3. 选择 **baoyu-skills**
|
||||||
|
4. 选择 **Update marketplace**
|
||||||
|
|
||||||
|
也可以选择 **Enable auto-update** 启用自动更新,每次启动时自动获取最新版本。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 可用技能
|
## 可用技能
|
||||||
|
|
||||||
### gemini-web
|
### gemini-web
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
|
|
@ -7,25 +7,45 @@ description: Post content to WeChat Official Account (微信公众号). Supports
|
||||||
|
|
||||||
Post content to WeChat Official Account using Chrome CDP automation.
|
Post content to WeChat Official Account using Chrome CDP automation.
|
||||||
|
|
||||||
|
## Script Directory
|
||||||
|
|
||||||
|
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
|
||||||
|
|
||||||
|
**Agent Execution Instructions**:
|
||||||
|
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
||||||
|
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
||||||
|
3. Replace all `${SKILL_DIR}` in this document with the actual path
|
||||||
|
|
||||||
|
**Script Reference**:
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `scripts/wechat-browser.ts` | Image-text posts (图文) |
|
||||||
|
| `scripts/wechat-article.ts` | Full article posting (文章) |
|
||||||
|
| `scripts/md-to-wechat.ts` | Markdown → WeChat HTML conversion |
|
||||||
|
| `scripts/copy-to-clipboard.ts` | Copy content to clipboard |
|
||||||
|
| `scripts/paste-from-clipboard.ts` | Send real paste keystroke |
|
||||||
|
|
||||||
## Quick Usage
|
## Quick Usage
|
||||||
|
|
||||||
### Image-Text (图文) - Multiple images with title/content
|
### Image-Text (图文) - Multiple images with title/content
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From markdown file and image directory
|
# From markdown file and image directory
|
||||||
npx -y bun ./scripts/wechat-browser.ts --markdown article.md --images ./images/
|
npx -y bun ${SKILL_DIR}/scripts/wechat-browser.ts --markdown article.md --images ./images/
|
||||||
|
|
||||||
# With explicit parameters
|
# With explicit parameters
|
||||||
npx -y bun ./scripts/wechat-browser.ts --title "标题" --content "内容" --image img1.png --image img2.png --submit
|
npx -y bun ${SKILL_DIR}/scripts/wechat-browser.ts --title "标题" --content "内容" --image img1.png --image img2.png --submit
|
||||||
```
|
```
|
||||||
|
|
||||||
### Article (文章) - Full markdown with formatting
|
### Article (文章) - Full markdown with formatting
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Post markdown article
|
# Post markdown article
|
||||||
npx -y bun ./scripts/wechat-article.ts --markdown article.md --theme grace
|
npx -y bun ${SKILL_DIR}/scripts/wechat-article.ts --markdown article.md --theme grace
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Note**: `${SKILL_DIR}` represents this skill's installation directory. Agent replaces with actual path at runtime.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- **Image-Text Posting**: See `references/image-text-posting.md` for detailed image-text posting guide
|
- **Image-Text Posting**: See `references/image-text-posting.md` for detailed image-text posting guide
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
|
function printUsage(exitCode = 0): never {
|
||||||
|
console.log(`Send real paste keystroke (Cmd+V / Ctrl+V) to the frontmost application
|
||||||
|
|
||||||
|
This bypasses CDP's synthetic events which websites can detect and ignore.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
npx -y bun paste-from-clipboard.ts [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--retries <n> Number of retry attempts (default: 3)
|
||||||
|
--delay <ms> Delay between retries in ms (default: 500)
|
||||||
|
--app <name> Target application to activate first (macOS only)
|
||||||
|
--help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Simple paste
|
||||||
|
npx -y bun paste-from-clipboard.ts
|
||||||
|
|
||||||
|
# Paste to Chrome with retries
|
||||||
|
npx -y bun paste-from-clipboard.ts --app "Google Chrome" --retries 5
|
||||||
|
|
||||||
|
# Quick paste with shorter delay
|
||||||
|
npx -y bun paste-from-clipboard.ts --delay 200
|
||||||
|
`);
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleepSync(ms: number): void {
|
||||||
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateApp(appName: string): boolean {
|
||||||
|
if (process.platform !== 'darwin') return false;
|
||||||
|
|
||||||
|
// Activate and wait for app to be frontmost
|
||||||
|
const script = `
|
||||||
|
tell application "${appName}"
|
||||||
|
activate
|
||||||
|
delay 0.5
|
||||||
|
end tell
|
||||||
|
|
||||||
|
-- Verify app is frontmost
|
||||||
|
tell application "System Events"
|
||||||
|
set frontApp to name of first application process whose frontmost is true
|
||||||
|
if frontApp is not "${appName}" then
|
||||||
|
tell application "${appName}" to activate
|
||||||
|
delay 0.3
|
||||||
|
end if
|
||||||
|
end tell
|
||||||
|
`;
|
||||||
|
const result = spawnSync('osascript', ['-e', script], { stdio: 'pipe' });
|
||||||
|
return result.status === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteMac(retries: number, delayMs: number, targetApp?: string): boolean {
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
// Build script that activates app (if specified) and sends keystroke in one atomic operation
|
||||||
|
const script = targetApp
|
||||||
|
? `
|
||||||
|
tell application "${targetApp}"
|
||||||
|
activate
|
||||||
|
end tell
|
||||||
|
delay 0.3
|
||||||
|
tell application "System Events"
|
||||||
|
keystroke "v" using command down
|
||||||
|
end tell
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
tell application "System Events"
|
||||||
|
keystroke "v" using command down
|
||||||
|
end tell
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = spawnSync('osascript', ['-e', script], { stdio: 'pipe' });
|
||||||
|
if (result.status === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stderr = result.stderr?.toString().trim();
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`[paste] osascript error: ${stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < retries - 1) {
|
||||||
|
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
|
||||||
|
sleepSync(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteLinux(retries: number, delayMs: number): boolean {
|
||||||
|
// Try xdotool first (X11), then ydotool (Wayland)
|
||||||
|
const tools = [
|
||||||
|
{ cmd: 'xdotool', args: ['key', 'ctrl+v'] },
|
||||||
|
{ cmd: 'ydotool', args: ['key', '29:1', '47:1', '47:0', '29:0'] }, // Ctrl down, V down, V up, Ctrl up
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const tool of tools) {
|
||||||
|
const which = spawnSync('which', [tool.cmd], { stdio: 'pipe' });
|
||||||
|
if (which.status !== 0) continue;
|
||||||
|
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
const result = spawnSync(tool.cmd, tool.args, { stdio: 'pipe' });
|
||||||
|
if (result.status === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (i < retries - 1) {
|
||||||
|
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
|
||||||
|
sleepSync(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[paste] No supported tool found. Install xdotool (X11) or ydotool (Wayland).');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteWindows(retries: number, delayMs: number): boolean {
|
||||||
|
const ps = `
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
[System.Windows.Forms.SendKeys]::SendWait("^v")
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', ps], { stdio: 'pipe' });
|
||||||
|
if (result.status === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (i < retries - 1) {
|
||||||
|
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
|
||||||
|
sleepSync(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function paste(retries: number, delayMs: number, targetApp?: string): boolean {
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'darwin':
|
||||||
|
return pasteMac(retries, delayMs, targetApp);
|
||||||
|
case 'linux':
|
||||||
|
return pasteLinux(retries, delayMs);
|
||||||
|
case 'win32':
|
||||||
|
return pasteWindows(retries, delayMs);
|
||||||
|
default:
|
||||||
|
console.error(`[paste] Unsupported platform: ${process.platform}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
let retries = 3;
|
||||||
|
let delayMs = 500;
|
||||||
|
let targetApp: string | undefined;
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i] ?? '';
|
||||||
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
printUsage(0);
|
||||||
|
}
|
||||||
|
if (arg === '--retries' && args[i + 1]) {
|
||||||
|
retries = parseInt(args[++i]!, 10) || 3;
|
||||||
|
} else if (arg === '--delay' && args[i + 1]) {
|
||||||
|
delayMs = parseInt(args[++i]!, 10) || 500;
|
||||||
|
} else if (arg === '--app' && args[i + 1]) {
|
||||||
|
targetApp = args[++i];
|
||||||
|
} else if (arg.startsWith('-')) {
|
||||||
|
console.error(`Unknown option: ${arg}`);
|
||||||
|
printUsage(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetApp) {
|
||||||
|
console.log(`[paste] Target app: ${targetApp}`);
|
||||||
|
}
|
||||||
|
console.log(`[paste] Sending paste keystroke (retries=${retries}, delay=${delayMs}ms)...`);
|
||||||
|
const success = paste(retries, delayMs, targetApp);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
console.log('[paste] Paste keystroke sent successfully');
|
||||||
|
} else {
|
||||||
|
console.error('[paste] Failed to send paste keystroke');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
|
|
@ -7,26 +7,23 @@ description: Post content and articles to X (Twitter). Supports regular posts wi
|
||||||
|
|
||||||
Post content, images, and long-form articles to X using real Chrome browser (bypasses anti-bot detection).
|
Post content, images, and long-form articles to X using real Chrome browser (bypasses anti-bot detection).
|
||||||
|
|
||||||
## Features
|
## Script Directory
|
||||||
|
|
||||||
- **Regular Posts**: Text + up to 4 images
|
**Important**: All scripts are located in the `scripts/` subdirectory of this skill.
|
||||||
- **X Articles**: Publish Markdown files with rich formatting and images (requires X Premium)
|
|
||||||
|
|
||||||
## Usage
|
**Agent Execution Instructions**:
|
||||||
|
1. Determine this SKILL.md file's directory path as `SKILL_DIR`
|
||||||
|
2. Script path = `${SKILL_DIR}/scripts/<script-name>.ts`
|
||||||
|
3. Replace all `${SKILL_DIR}` in this document with the actual path
|
||||||
|
|
||||||
```bash
|
**Script Reference**:
|
||||||
# Post text only
|
| Script | Purpose |
|
||||||
/baoyu-post-to-x "Your post content here"
|
|--------|---------|
|
||||||
|
| `scripts/x-browser.ts` | Regular posts (text + images) |
|
||||||
# Post with image
|
| `scripts/x-article.ts` | Long-form article publishing (Markdown) |
|
||||||
/baoyu-post-to-x "Your post content" --image /path/to/image.png
|
| `scripts/md-to-html.ts` | Markdown → HTML conversion |
|
||||||
|
| `scripts/copy-to-clipboard.ts` | Copy content to clipboard |
|
||||||
# Post with multiple images (up to 4)
|
| `scripts/paste-from-clipboard.ts` | Send real paste keystroke |
|
||||||
/baoyu-post-to-x "Your post content" --image img1.png --image img2.png
|
|
||||||
|
|
||||||
# Actually submit the post
|
|
||||||
/baoyu-post-to-x "Your post content" --submit
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
|
@ -34,58 +31,28 @@ Post content, images, and long-form articles to X using real Chrome browser (byp
|
||||||
- `bun` installed (for running scripts)
|
- `bun` installed (for running scripts)
|
||||||
- First run: log in to X in the opened browser window
|
- First run: log in to X in the opened browser window
|
||||||
|
|
||||||
## Quick Start (Recommended)
|
## References
|
||||||
|
|
||||||
Use the `x-browser.ts` script directly:
|
- **Regular Posts**: See `references/regular-posts.md` for manual workflow, troubleshooting, and technical details
|
||||||
|
- **X Articles**: See `references/articles.md` for long-form article publishing guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regular Posts
|
||||||
|
|
||||||
|
Text + up to 4 images.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Preview mode (doesn't post)
|
# Preview mode (doesn't post)
|
||||||
npx -y bun ./scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
|
npx -y bun ${SKILL_DIR}/scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
|
||||||
|
|
||||||
# Actually post
|
# Actually post
|
||||||
npx -y bun ./scripts/x-browser.ts "Hello!" --image ./photo.png --submit
|
npx -y bun ${SKILL_DIR}/scripts/x-browser.ts "Hello!" --image ./photo.png --submit
|
||||||
```
|
```
|
||||||
|
|
||||||
The script:
|
> **Note**: `${SKILL_DIR}` represents this skill's installation directory. Agent replaces with actual path at runtime.
|
||||||
1. Launches real Chrome with anti-detection disabled
|
|
||||||
2. Uses persistent profile (only need to log in once)
|
|
||||||
3. Types text and pastes images via CDP
|
|
||||||
4. Waits 30s for preview (or posts immediately with `--submit`)
|
|
||||||
|
|
||||||
## Manual Workflow
|
|
||||||
|
|
||||||
If you prefer step-by-step control:
|
|
||||||
|
|
||||||
### Step 1: Copy Image to Clipboard
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx -y bun ./scripts/copy-to-clipboard.ts image /path/to/image.png
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Use Playwright MCP (if Chrome session available)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Navigate
|
|
||||||
mcp__playwright__browser_navigate url="https://x.com/compose/post"
|
|
||||||
|
|
||||||
# Get element refs
|
|
||||||
mcp__playwright__browser_snapshot
|
|
||||||
|
|
||||||
# Type text
|
|
||||||
mcp__playwright__browser_click element="editor" ref="<ref>"
|
|
||||||
mcp__playwright__browser_type element="editor" ref="<ref>" text="Your content"
|
|
||||||
|
|
||||||
# Paste image (after copying to clipboard)
|
|
||||||
mcp__playwright__browser_press_key key="Meta+v" # macOS
|
|
||||||
# or
|
|
||||||
mcp__playwright__browser_press_key key="Control+v" # Windows/Linux
|
|
||||||
|
|
||||||
# Screenshot to verify
|
|
||||||
mcp__playwright__browser_take_screenshot filename="preview.png"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Parameters
|
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
| Parameter | Description |
|
| Parameter | Description |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| `<text>` | Post content (positional argument) |
|
| `<text>` | Post content (positional argument) |
|
||||||
|
|
@ -93,42 +60,40 @@ mcp__playwright__browser_take_screenshot filename="preview.png"
|
||||||
| `--submit` | Actually post (default: preview only) |
|
| `--submit` | Actually post (default: preview only) |
|
||||||
| `--profile <dir>` | Custom Chrome profile directory |
|
| `--profile <dir>` | Custom Chrome profile directory |
|
||||||
|
|
||||||
## Image Support
|
---
|
||||||
|
|
||||||
- Formats: PNG, JPEG, GIF, WebP
|
## X Articles
|
||||||
- Max 4 images per post
|
|
||||||
- Images copied to system clipboard, then pasted via keyboard shortcut
|
|
||||||
|
|
||||||
## Example Session
|
Long-form Markdown articles (requires X Premium).
|
||||||
|
|
||||||
```
|
```bash
|
||||||
User: /baoyu-post-to-x "Hello from Claude!" --image ./screenshot.png
|
# Preview mode
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md
|
||||||
|
|
||||||
Claude:
|
# With cover image
|
||||||
1. Runs: npx -y bun ./scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
|
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
|
||||||
2. Chrome opens with X compose page
|
|
||||||
3. Text is typed into editor
|
# Publish
|
||||||
4. Image is copied to clipboard and pasted
|
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --submit
|
||||||
5. Browser stays open 30s for preview
|
|
||||||
6. Reports: "Post composed. Use --submit to post."
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
**Parameters**:
|
||||||
|
| Parameter | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `<markdown>` | Markdown file path (positional argument) |
|
||||||
|
| `--cover <path>` | Cover image path |
|
||||||
|
| `--title <text>` | Override article title |
|
||||||
|
| `--submit` | Actually publish (default: preview only) |
|
||||||
|
|
||||||
- **Chrome not found**: Set `X_BROWSER_CHROME_PATH` environment variable
|
**Frontmatter** (optional):
|
||||||
- **Not logged in**: First run opens Chrome - log in manually, cookies are saved
|
```yaml
|
||||||
- **Image paste fails**: Verify clipboard script: `npx -y bun ./scripts/copy-to-clipboard.ts image <path>`
|
---
|
||||||
- **Rate limited**: Wait a few minutes before retrying
|
title: My Article Title
|
||||||
|
cover_image: /path/to/cover.jpg
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
## How It Works
|
---
|
||||||
|
|
||||||
The `x-browser.ts` script uses Chrome DevTools Protocol (CDP) to:
|
|
||||||
1. Launch real Chrome (not Playwright) with `--disable-blink-features=AutomationControlled`
|
|
||||||
2. Use persistent profile directory for saved login sessions
|
|
||||||
3. Interact with X via CDP commands (Runtime.evaluate, Input.dispatchKeyEvent)
|
|
||||||
4. Paste images from system clipboard
|
|
||||||
|
|
||||||
This approach bypasses X's anti-automation detection that blocks Playwright/Puppeteer.
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|
@ -136,204 +101,3 @@ This approach bypasses X's anti-automation detection that blocks Playwright/Pupp
|
||||||
- Always preview before using `--submit`
|
- Always preview before using `--submit`
|
||||||
- Browser closes automatically after operation
|
- Browser closes automatically after operation
|
||||||
- Supports macOS, Linux, and Windows
|
- Supports macOS, Linux, and Windows
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# X Articles (Long-form Publishing)
|
|
||||||
|
|
||||||
Publish Markdown articles to X Articles editor with rich text formatting and images.
|
|
||||||
|
|
||||||
## X Article Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Publish markdown article (preview mode)
|
|
||||||
/baoyu-post-to-x article /path/to/article.md
|
|
||||||
|
|
||||||
# With custom cover image
|
|
||||||
/baoyu-post-to-x article article.md --cover ./hero.png
|
|
||||||
|
|
||||||
# With custom title
|
|
||||||
/baoyu-post-to-x article article.md --title "My Custom Title"
|
|
||||||
|
|
||||||
# Actually publish (not just draft)
|
|
||||||
/baoyu-post-to-x article article.md --submit
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prerequisites for Articles
|
|
||||||
|
|
||||||
- X Premium subscription (required for Articles)
|
|
||||||
- Google Chrome installed
|
|
||||||
- `bun` installed
|
|
||||||
|
|
||||||
## Article Script
|
|
||||||
|
|
||||||
Use `x-article.ts` directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx -y bun ./scripts/x-article.ts article.md
|
|
||||||
npx -y bun ./scripts/x-article.ts article.md --cover ./cover.jpg
|
|
||||||
npx -y bun ./scripts/x-article.ts article.md --submit
|
|
||||||
```
|
|
||||||
|
|
||||||
## Markdown Format
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
title: My Article Title
|
|
||||||
cover_image: /path/to/cover.jpg
|
|
||||||
---
|
|
||||||
|
|
||||||
# Title (becomes article title)
|
|
||||||
|
|
||||||
Regular paragraph text with **bold** and *italic*.
|
|
||||||
|
|
||||||
## Section Header
|
|
||||||
|
|
||||||
More content here.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- List item 1
|
|
||||||
- List item 2
|
|
||||||
|
|
||||||
1. Numbered item
|
|
||||||
2. Another item
|
|
||||||
|
|
||||||
> Blockquote text
|
|
||||||
|
|
||||||
[Link text](https://example.com)
|
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
Code blocks become blockquotes (X doesn't support code)
|
|
||||||
\`\`\`
|
|
||||||
```
|
|
||||||
|
|
||||||
## Frontmatter Fields
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| `title` | Article title (or uses first H1) |
|
|
||||||
| `cover_image` | Cover image path or URL |
|
|
||||||
| `cover` | Alias for cover_image |
|
|
||||||
| `image` | Alias for cover_image |
|
|
||||||
|
|
||||||
## Image Handling
|
|
||||||
|
|
||||||
1. **Cover Image**: First image or `cover_image` from frontmatter
|
|
||||||
2. **Remote Images**: Automatically downloaded to temp directory
|
|
||||||
3. **Placeholders**: Images in content use `[[IMAGE_PLACEHOLDER_N]]` format
|
|
||||||
4. **Insertion**: Placeholders are found, selected, and replaced with actual images
|
|
||||||
|
|
||||||
## Markdown to HTML Script
|
|
||||||
|
|
||||||
Convert markdown and inspect structure:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Get JSON with all metadata
|
|
||||||
npx -y bun ./scripts/md-to-html.ts article.md
|
|
||||||
|
|
||||||
# Output HTML only
|
|
||||||
npx -y bun ./scripts/md-to-html.ts article.md --html-only
|
|
||||||
|
|
||||||
# Save HTML to file
|
|
||||||
npx -y bun ./scripts/md-to-html.ts article.md --save-html /tmp/article.html
|
|
||||||
```
|
|
||||||
|
|
||||||
JSON output:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"title": "Article Title",
|
|
||||||
"coverImage": "/path/to/cover.jpg",
|
|
||||||
"contentImages": [
|
|
||||||
{
|
|
||||||
"placeholder": "[[IMAGE_PLACEHOLDER_1]]",
|
|
||||||
"localPath": "/tmp/x-article-images/img.png",
|
|
||||||
"blockIndex": 5
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"html": "<p>Content...</p>",
|
|
||||||
"totalBlocks": 20
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Supported Formatting
|
|
||||||
|
|
||||||
| Markdown | HTML Output |
|
|
||||||
|----------|-------------|
|
|
||||||
| `# H1` | Title only (not in body) |
|
|
||||||
| `## H2` - `###### H6` | `<h2>` |
|
|
||||||
| `**bold**` | `<strong>` |
|
|
||||||
| `*italic*` | `<em>` |
|
|
||||||
| `[text](url)` | `<a href>` |
|
|
||||||
| `> quote` | `<blockquote>` |
|
|
||||||
| `` `code` `` | `<code>` |
|
|
||||||
| ```` ``` ```` | `<blockquote>` (X limitation) |
|
|
||||||
| `- item` | `<ul><li>` |
|
|
||||||
| `1. item` | `<ol><li>` |
|
|
||||||
| `` | Image placeholder |
|
|
||||||
|
|
||||||
## Article Workflow
|
|
||||||
|
|
||||||
1. **Parse Markdown**: Extract title, cover, content images, generate HTML
|
|
||||||
2. **Launch Chrome**: Real browser with CDP, persistent login
|
|
||||||
3. **Navigate**: Open `x.com/compose/articles`
|
|
||||||
4. **Create Article**: Click create button if on list page
|
|
||||||
5. **Upload Cover**: Use file input for cover image
|
|
||||||
6. **Fill Title**: Type title into title field
|
|
||||||
7. **Paste Content**: Copy HTML to clipboard, paste into editor
|
|
||||||
8. **Insert Images**: For each placeholder (reverse order):
|
|
||||||
- Find placeholder text in editor
|
|
||||||
- Select the placeholder
|
|
||||||
- Copy image to clipboard
|
|
||||||
- Paste to replace selection
|
|
||||||
9. **Review**: Browser stays open for 60s preview
|
|
||||||
10. **Publish**: Only with `--submit` flag
|
|
||||||
|
|
||||||
## Article Example Session
|
|
||||||
|
|
||||||
```
|
|
||||||
User: /baoyu-post-to-x article ./blog/my-post.md --cover ./thumbnail.png
|
|
||||||
|
|
||||||
Claude:
|
|
||||||
1. Parses markdown: title="My Post", 3 content images
|
|
||||||
2. Launches Chrome with CDP
|
|
||||||
3. Navigates to x.com/compose/articles
|
|
||||||
4. Clicks create button
|
|
||||||
5. Uploads thumbnail.png as cover
|
|
||||||
6. Fills title "My Post"
|
|
||||||
7. Pastes HTML content
|
|
||||||
8. Inserts 3 images at placeholder positions
|
|
||||||
9. Reports: "Article composed. Review and use --submit to publish."
|
|
||||||
```
|
|
||||||
|
|
||||||
## Article Troubleshooting
|
|
||||||
|
|
||||||
- **No create button**: Ensure X Premium subscription is active
|
|
||||||
- **Cover upload fails**: Check file path and format (PNG, JPEG)
|
|
||||||
- **Images not inserting**: Verify placeholders exist in pasted content
|
|
||||||
- **Content not pasting**: Check HTML clipboard: `npx -y bun ./scripts/copy-to-clipboard.ts html --file /tmp/test.html`
|
|
||||||
|
|
||||||
## How Article Publishing Works
|
|
||||||
|
|
||||||
1. `md-to-html.ts` converts Markdown to HTML:
|
|
||||||
- Extracts frontmatter (title, cover)
|
|
||||||
- Converts markdown to HTML
|
|
||||||
- Replaces images with unique placeholders
|
|
||||||
- Downloads remote images locally
|
|
||||||
- Returns structured JSON
|
|
||||||
|
|
||||||
2. `x-article.ts` publishes via CDP:
|
|
||||||
- Launches real Chrome (bypasses detection)
|
|
||||||
- Uses persistent profile (saved login)
|
|
||||||
- Navigates and fills editor via DOM manipulation
|
|
||||||
- Pastes HTML from system clipboard
|
|
||||||
- Finds/selects/replaces each image placeholder
|
|
||||||
|
|
||||||
## Scripts Reference
|
|
||||||
|
|
||||||
| Script | Purpose |
|
|
||||||
|--------|---------|
|
|
||||||
| `x-browser.ts` | Regular posts (text + images) |
|
|
||||||
| `x-article.ts` | Article publishing (Markdown) |
|
|
||||||
| `md-to-html.ts` | Markdown → HTML conversion |
|
|
||||||
| `copy-to-clipboard.ts` | Copy image/HTML to clipboard |
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
# X Articles - Detailed Guide
|
||||||
|
|
||||||
|
Publish Markdown articles to X Articles editor with rich text formatting and images.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- X Premium subscription (required for Articles)
|
||||||
|
- Google Chrome installed
|
||||||
|
- `bun` installed
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Publish markdown article (preview mode)
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md
|
||||||
|
|
||||||
|
# With custom cover image
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --cover ./cover.jpg
|
||||||
|
|
||||||
|
# Actually publish
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/x-article.ts article.md --submit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Markdown Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
title: My Article Title
|
||||||
|
cover_image: /path/to/cover.jpg
|
||||||
|
---
|
||||||
|
|
||||||
|
# Title (becomes article title)
|
||||||
|
|
||||||
|
Regular paragraph text with **bold** and *italic*.
|
||||||
|
|
||||||
|
## Section Header
|
||||||
|
|
||||||
|
More content here.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- List item 1
|
||||||
|
- List item 2
|
||||||
|
|
||||||
|
1. Numbered item
|
||||||
|
2. Another item
|
||||||
|
|
||||||
|
> Blockquote text
|
||||||
|
|
||||||
|
[Link text](https://example.com)
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
Code blocks become blockquotes (X doesn't support code)
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontmatter Fields
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `title` | Article title (or uses first H1) |
|
||||||
|
| `cover_image` | Cover image path or URL |
|
||||||
|
| `cover` | Alias for cover_image |
|
||||||
|
| `image` | Alias for cover_image |
|
||||||
|
|
||||||
|
## Image Handling
|
||||||
|
|
||||||
|
1. **Cover Image**: First image or `cover_image` from frontmatter
|
||||||
|
2. **Remote Images**: Automatically downloaded to temp directory
|
||||||
|
3. **Placeholders**: Images in content use `[[IMAGE_PLACEHOLDER_N]]` format
|
||||||
|
4. **Insertion**: Placeholders are found, selected, and replaced with actual images
|
||||||
|
|
||||||
|
## Markdown to HTML Script
|
||||||
|
|
||||||
|
Convert markdown and inspect structure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get JSON with all metadata
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/md-to-html.ts article.md
|
||||||
|
|
||||||
|
# Output HTML only
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/md-to-html.ts article.md --html-only
|
||||||
|
|
||||||
|
# Save HTML to file
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/md-to-html.ts article.md --save-html /tmp/article.html
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON output:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "Article Title",
|
||||||
|
"coverImage": "/path/to/cover.jpg",
|
||||||
|
"contentImages": [
|
||||||
|
{
|
||||||
|
"placeholder": "[[IMAGE_PLACEHOLDER_1]]",
|
||||||
|
"localPath": "/tmp/x-article-images/img.png",
|
||||||
|
"blockIndex": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"html": "<p>Content...</p>",
|
||||||
|
"totalBlocks": 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Formatting
|
||||||
|
|
||||||
|
| Markdown | HTML Output |
|
||||||
|
|----------|-------------|
|
||||||
|
| `# H1` | Title only (not in body) |
|
||||||
|
| `## H2` - `###### H6` | `<h2>` |
|
||||||
|
| `**bold**` | `<strong>` |
|
||||||
|
| `*italic*` | `<em>` |
|
||||||
|
| `[text](url)` | `<a href>` |
|
||||||
|
| `> quote` | `<blockquote>` |
|
||||||
|
| `` `code` `` | `<code>` |
|
||||||
|
| ```` ``` ```` | `<blockquote>` (X limitation) |
|
||||||
|
| `- item` | `<ul><li>` |
|
||||||
|
| `1. item` | `<ol><li>` |
|
||||||
|
| `` | Image placeholder |
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Parse Markdown**: Extract title, cover, content images, generate HTML
|
||||||
|
2. **Launch Chrome**: Real browser with CDP, persistent login
|
||||||
|
3. **Navigate**: Open `x.com/compose/articles`
|
||||||
|
4. **Create Article**: Click create button if on list page
|
||||||
|
5. **Upload Cover**: Use file input for cover image
|
||||||
|
6. **Fill Title**: Type title into title field
|
||||||
|
7. **Paste Content**: Copy HTML to clipboard, paste into editor
|
||||||
|
8. **Insert Images**: For each placeholder (reverse order):
|
||||||
|
- Find placeholder text in editor
|
||||||
|
- Select the placeholder
|
||||||
|
- Copy image to clipboard
|
||||||
|
- Paste to replace selection
|
||||||
|
9. **Review**: Browser stays open for 60s preview
|
||||||
|
10. **Publish**: Only with `--submit` flag
|
||||||
|
|
||||||
|
## Example Session
|
||||||
|
|
||||||
|
```
|
||||||
|
User: /post-to-x article ./blog/my-post.md --cover ./thumbnail.png
|
||||||
|
|
||||||
|
Claude:
|
||||||
|
1. Parses markdown: title="My Post", 3 content images
|
||||||
|
2. Launches Chrome with CDP
|
||||||
|
3. Navigates to x.com/compose/articles
|
||||||
|
4. Clicks create button
|
||||||
|
5. Uploads thumbnail.png as cover
|
||||||
|
6. Fills title "My Post"
|
||||||
|
7. Pastes HTML content
|
||||||
|
8. Inserts 3 images at placeholder positions
|
||||||
|
9. Reports: "Article composed. Review and use --submit to publish."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **No create button**: Ensure X Premium subscription is active
|
||||||
|
- **Cover upload fails**: Check file path and format (PNG, JPEG)
|
||||||
|
- **Images not inserting**: Verify placeholders exist in pasted content
|
||||||
|
- **Content not pasting**: Check HTML clipboard: `npx -y bun ${SKILL_DIR}/scripts/copy-to-clipboard.ts html --file /tmp/test.html`
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. `md-to-html.ts` converts Markdown to HTML:
|
||||||
|
- Extracts frontmatter (title, cover)
|
||||||
|
- Converts markdown to HTML
|
||||||
|
- Replaces images with unique placeholders
|
||||||
|
- Downloads remote images locally
|
||||||
|
- Returns structured JSON
|
||||||
|
|
||||||
|
2. `x-article.ts` publishes via CDP:
|
||||||
|
- Launches real Chrome (bypasses detection)
|
||||||
|
- Uses persistent profile (saved login)
|
||||||
|
- Navigates and fills editor via DOM manipulation
|
||||||
|
- Pastes HTML from system clipboard
|
||||||
|
- Finds/selects/replaces each image placeholder
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Regular Posts - Detailed Guide
|
||||||
|
|
||||||
|
Detailed documentation for posting text and images to X.
|
||||||
|
|
||||||
|
## Manual Workflow
|
||||||
|
|
||||||
|
If you prefer step-by-step control:
|
||||||
|
|
||||||
|
### Step 1: Copy Image to Clipboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/copy-to-clipboard.ts image /path/to/image.png
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Paste from Clipboard
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple paste to frontmost app
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/paste-from-clipboard.ts
|
||||||
|
|
||||||
|
# Paste to Chrome with retries
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/paste-from-clipboard.ts --app "Google Chrome" --retries 5
|
||||||
|
|
||||||
|
# Quick paste with shorter delay
|
||||||
|
npx -y bun ${SKILL_DIR}/scripts/paste-from-clipboard.ts --delay 200
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Use Playwright MCP (if Chrome session available)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate
|
||||||
|
mcp__playwright__browser_navigate url="https://x.com/compose/post"
|
||||||
|
|
||||||
|
# Get element refs
|
||||||
|
mcp__playwright__browser_snapshot
|
||||||
|
|
||||||
|
# Type text
|
||||||
|
mcp__playwright__browser_click element="editor" ref="<ref>"
|
||||||
|
mcp__playwright__browser_type element="editor" ref="<ref>" text="Your content"
|
||||||
|
|
||||||
|
# Paste image (after copying to clipboard)
|
||||||
|
mcp__playwright__browser_press_key key="Meta+v" # macOS
|
||||||
|
# or
|
||||||
|
mcp__playwright__browser_press_key key="Control+v" # Windows/Linux
|
||||||
|
|
||||||
|
# Screenshot to verify
|
||||||
|
mcp__playwright__browser_take_screenshot filename="preview.png"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image Support
|
||||||
|
|
||||||
|
- Formats: PNG, JPEG, GIF, WebP
|
||||||
|
- Max 4 images per post
|
||||||
|
- Images copied to system clipboard, then pasted via keyboard shortcut
|
||||||
|
|
||||||
|
## Example Session
|
||||||
|
|
||||||
|
```
|
||||||
|
User: /post-to-x "Hello from Claude!" --image ./screenshot.png
|
||||||
|
|
||||||
|
Claude:
|
||||||
|
1. Runs: npx -y bun ${SKILL_DIR}/scripts/x-browser.ts "Hello from Claude!" --image ./screenshot.png
|
||||||
|
2. Chrome opens with X compose page
|
||||||
|
3. Text is typed into editor
|
||||||
|
4. Image is copied to clipboard and pasted
|
||||||
|
5. Browser stays open 30s for preview
|
||||||
|
6. Reports: "Post composed. Use --submit to post."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Chrome not found**: Set `X_BROWSER_CHROME_PATH` environment variable
|
||||||
|
- **Not logged in**: First run opens Chrome - log in manually, cookies are saved
|
||||||
|
- **Image paste fails**:
|
||||||
|
- Verify clipboard script: `npx -y bun ${SKILL_DIR}/scripts/copy-to-clipboard.ts image <path>`
|
||||||
|
- On macOS, grant "Accessibility" permission to Terminal/iTerm in System Settings > Privacy & Security > Accessibility
|
||||||
|
- Keep Chrome window visible and in front during paste operations
|
||||||
|
- **osascript permission denied**: Grant Terminal accessibility permissions in System Preferences
|
||||||
|
- **Rate limited**: Wait a few minutes before retrying
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The `x-browser.ts` script uses Chrome DevTools Protocol (CDP) to:
|
||||||
|
1. Launch real Chrome (not Playwright) with `--disable-blink-features=AutomationControlled`
|
||||||
|
2. Use persistent profile directory for saved login sessions
|
||||||
|
3. Interact with X via CDP commands (Runtime.evaluate, Input.dispatchKeyEvent)
|
||||||
|
4. **Paste images using osascript** (macOS): Sends real Cmd+V keystroke to Chrome, bypassing CDP's synthetic events that X can detect
|
||||||
|
|
||||||
|
This approach bypasses X's anti-automation detection that blocks Playwright/Puppeteer.
|
||||||
|
|
||||||
|
### Image Paste Mechanism (macOS)
|
||||||
|
|
||||||
|
CDP's `Input.dispatchKeyEvent` sends "synthetic" keyboard events that websites can detect. X ignores synthetic paste events for security. The solution:
|
||||||
|
|
||||||
|
1. Copy image to system clipboard via Swift/AppKit (`copy-to-clipboard.ts`)
|
||||||
|
2. Bring Chrome to front via `osascript`
|
||||||
|
3. Send real Cmd+V keystroke via `osascript` and System Events
|
||||||
|
4. Wait for upload to complete
|
||||||
|
|
||||||
|
This requires Terminal to have "Accessibility" permission in System Settings.
|
||||||
|
|
@ -303,6 +303,7 @@ export async function parseMarkdown(
|
||||||
// Resolve image paths (download remote, resolve relative)
|
// Resolve image paths (download remote, resolve relative)
|
||||||
const contentImages: ImageInfo[] = [];
|
const contentImages: ImageInfo[] = [];
|
||||||
let isFirstImage = true;
|
let isFirstImage = true;
|
||||||
|
let coverPlaceholder: string | null = null;
|
||||||
|
|
||||||
for (let i = 0; i < images.length; i++) {
|
for (let i = 0; i < images.length; i++) {
|
||||||
const img = images[i]!;
|
const img = images[i]!;
|
||||||
|
|
@ -311,6 +312,7 @@ export async function parseMarkdown(
|
||||||
// First image becomes cover if no cover specified
|
// First image becomes cover if no cover specified
|
||||||
if (isFirstImage && !coverImagePath) {
|
if (isFirstImage && !coverImagePath) {
|
||||||
coverImagePath = localPath;
|
coverImagePath = localPath;
|
||||||
|
coverPlaceholder = `[[IMAGE_PLACEHOLDER_${i + 1}]]`;
|
||||||
isFirstImage = false;
|
isFirstImage = false;
|
||||||
// Don't add to contentImages, it's the cover
|
// Don't add to contentImages, it's the cover
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -325,6 +327,13 @@ export async function parseMarkdown(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove cover placeholder from HTML if first image was used as cover
|
||||||
|
let finalHtml = html;
|
||||||
|
if (coverPlaceholder) {
|
||||||
|
// Remove the placeholder and its containing <p> tag
|
||||||
|
finalHtml = finalHtml.replace(new RegExp(`<p>${coverPlaceholder.replace(/[[\]]/g, '\\$&')}</p>\\n?`, 'g'), '');
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve cover image path
|
// Resolve cover image path
|
||||||
let resolvedCoverImage: string | null = null;
|
let resolvedCoverImage: string | null = null;
|
||||||
if (coverImagePath) {
|
if (coverImagePath) {
|
||||||
|
|
@ -335,7 +344,7 @@ export async function parseMarkdown(
|
||||||
title,
|
title,
|
||||||
coverImage: resolvedCoverImage,
|
coverImage: resolvedCoverImage,
|
||||||
contentImages,
|
contentImages,
|
||||||
html,
|
html: finalHtml,
|
||||||
totalBlocks,
|
totalBlocks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
|
function printUsage(exitCode = 0): never {
|
||||||
|
console.log(`Send real paste keystroke (Cmd+V / Ctrl+V) to the frontmost application
|
||||||
|
|
||||||
|
This bypasses CDP's synthetic events which websites can detect and ignore.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
npx -y bun paste-from-clipboard.ts [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--retries <n> Number of retry attempts (default: 3)
|
||||||
|
--delay <ms> Delay between retries in ms (default: 500)
|
||||||
|
--app <name> Target application to activate first (macOS only)
|
||||||
|
--help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Simple paste
|
||||||
|
npx -y bun paste-from-clipboard.ts
|
||||||
|
|
||||||
|
# Paste to Chrome with retries
|
||||||
|
npx -y bun paste-from-clipboard.ts --app "Google Chrome" --retries 5
|
||||||
|
|
||||||
|
# Quick paste with shorter delay
|
||||||
|
npx -y bun paste-from-clipboard.ts --delay 200
|
||||||
|
`);
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleepSync(ms: number): void {
|
||||||
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateApp(appName: string): boolean {
|
||||||
|
if (process.platform !== 'darwin') return false;
|
||||||
|
|
||||||
|
// Activate and wait for app to be frontmost
|
||||||
|
const script = `
|
||||||
|
tell application "${appName}"
|
||||||
|
activate
|
||||||
|
delay 0.5
|
||||||
|
end tell
|
||||||
|
|
||||||
|
-- Verify app is frontmost
|
||||||
|
tell application "System Events"
|
||||||
|
set frontApp to name of first application process whose frontmost is true
|
||||||
|
if frontApp is not "${appName}" then
|
||||||
|
tell application "${appName}" to activate
|
||||||
|
delay 0.3
|
||||||
|
end if
|
||||||
|
end tell
|
||||||
|
`;
|
||||||
|
const result = spawnSync('osascript', ['-e', script], { stdio: 'pipe' });
|
||||||
|
return result.status === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteMac(retries: number, delayMs: number, targetApp?: string): boolean {
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
// Build script that activates app (if specified) and sends keystroke in one atomic operation
|
||||||
|
const script = targetApp
|
||||||
|
? `
|
||||||
|
tell application "${targetApp}"
|
||||||
|
activate
|
||||||
|
end tell
|
||||||
|
delay 0.3
|
||||||
|
tell application "System Events"
|
||||||
|
keystroke "v" using command down
|
||||||
|
end tell
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
tell application "System Events"
|
||||||
|
keystroke "v" using command down
|
||||||
|
end tell
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = spawnSync('osascript', ['-e', script], { stdio: 'pipe' });
|
||||||
|
if (result.status === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stderr = result.stderr?.toString().trim();
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`[paste] osascript error: ${stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < retries - 1) {
|
||||||
|
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
|
||||||
|
sleepSync(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteLinux(retries: number, delayMs: number): boolean {
|
||||||
|
// Try xdotool first (X11), then ydotool (Wayland)
|
||||||
|
const tools = [
|
||||||
|
{ cmd: 'xdotool', args: ['key', 'ctrl+v'] },
|
||||||
|
{ cmd: 'ydotool', args: ['key', '29:1', '47:1', '47:0', '29:0'] }, // Ctrl down, V down, V up, Ctrl up
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const tool of tools) {
|
||||||
|
const which = spawnSync('which', [tool.cmd], { stdio: 'pipe' });
|
||||||
|
if (which.status !== 0) continue;
|
||||||
|
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
const result = spawnSync(tool.cmd, tool.args, { stdio: 'pipe' });
|
||||||
|
if (result.status === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (i < retries - 1) {
|
||||||
|
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
|
||||||
|
sleepSync(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[paste] No supported tool found. Install xdotool (X11) or ydotool (Wayland).');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteWindows(retries: number, delayMs: number): boolean {
|
||||||
|
const ps = `
|
||||||
|
Add-Type -AssemblyName System.Windows.Forms
|
||||||
|
[System.Windows.Forms.SendKeys]::SendWait("^v")
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', ps], { stdio: 'pipe' });
|
||||||
|
if (result.status === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (i < retries - 1) {
|
||||||
|
console.error(`[paste] Attempt ${i + 1}/${retries} failed, retrying in ${delayMs}ms...`);
|
||||||
|
sleepSync(delayMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function paste(retries: number, delayMs: number, targetApp?: string): boolean {
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'darwin':
|
||||||
|
return pasteMac(retries, delayMs, targetApp);
|
||||||
|
case 'linux':
|
||||||
|
return pasteLinux(retries, delayMs);
|
||||||
|
case 'win32':
|
||||||
|
return pasteWindows(retries, delayMs);
|
||||||
|
default:
|
||||||
|
console.error(`[paste] Unsupported platform: ${process.platform}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
let retries = 3;
|
||||||
|
let delayMs = 500;
|
||||||
|
let targetApp: string | undefined;
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const arg = args[i] ?? '';
|
||||||
|
if (arg === '--help' || arg === '-h') {
|
||||||
|
printUsage(0);
|
||||||
|
}
|
||||||
|
if (arg === '--retries' && args[i + 1]) {
|
||||||
|
retries = parseInt(args[++i]!, 10) || 3;
|
||||||
|
} else if (arg === '--delay' && args[i + 1]) {
|
||||||
|
delayMs = parseInt(args[++i]!, 10) || 500;
|
||||||
|
} else if (arg === '--app' && args[i + 1]) {
|
||||||
|
targetApp = args[++i];
|
||||||
|
} else if (arg.startsWith('-')) {
|
||||||
|
console.error(`Unknown option: ${arg}`);
|
||||||
|
printUsage(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetApp) {
|
||||||
|
console.log(`[paste] Target app: ${targetApp}`);
|
||||||
|
}
|
||||||
|
console.log(`[paste] Sending paste keystroke (retries=${retries}, delay=${delayMs}ms)...`);
|
||||||
|
const success = paste(retries, delayMs, targetApp);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
console.log('[paste] Paste keystroke sent successfully');
|
||||||
|
} else {
|
||||||
|
console.error('[paste] Failed to send paste keystroke');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
|
|
@ -141,44 +141,30 @@ class CdpConnection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScriptDir(): string {
|
||||||
|
return path.dirname(new URL(import.meta.url).pathname);
|
||||||
|
}
|
||||||
|
|
||||||
function copyImageToClipboard(imagePath: string): boolean {
|
function copyImageToClipboard(imagePath: string): boolean {
|
||||||
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
|
||||||
const copyScript = path.join(scriptDir, 'copy-to-clipboard.ts');
|
|
||||||
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
|
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
|
||||||
return result.status === 0;
|
return result.status === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyHtmlToClipboard(htmlPath: string): boolean {
|
function copyHtmlToClipboard(htmlPath: string): boolean {
|
||||||
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
|
||||||
const copyScript = path.join(scriptDir, 'copy-to-clipboard.ts');
|
|
||||||
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'html', '--file', htmlPath], { stdio: 'inherit' });
|
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'html', '--file', htmlPath], { stdio: 'inherit' });
|
||||||
return result.status === 0;
|
return result.status === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendRealPasteKeystroke(retries = 3): boolean {
|
function pasteFromClipboard(targetApp?: string, retries = 3, delayMs = 500): boolean {
|
||||||
if (process.platform !== 'darwin') {
|
const pasteScript = path.join(getScriptDir(), 'paste-from-clipboard.ts');
|
||||||
console.log('[x-article] Real keystroke only supported on macOS');
|
const args = ['npx', '-y', 'bun', pasteScript, '--retries', String(retries), '--delay', String(delayMs)];
|
||||||
return false;
|
if (targetApp) {
|
||||||
|
args.push('--app', targetApp);
|
||||||
}
|
}
|
||||||
|
const result = spawnSync(args[0]!, args.slice(1), { stdio: 'inherit' });
|
||||||
// Use osascript to send Cmd+V to frontmost application (Chrome)
|
return result.status === 0;
|
||||||
const script = `
|
|
||||||
tell application "System Events"
|
|
||||||
keystroke "v" using command down
|
|
||||||
end tell
|
|
||||||
`;
|
|
||||||
|
|
||||||
for (let i = 0; i < retries; i++) {
|
|
||||||
const result = spawnSync('osascript', ['-e', script], { stdio: 'pipe' });
|
|
||||||
if (result.status === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Wait a bit before retry
|
|
||||||
if (i < retries - 1) {
|
|
||||||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArticleOptions {
|
interface ArticleOptions {
|
||||||
|
|
@ -300,11 +286,6 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
};
|
};
|
||||||
|
|
||||||
const pasteFromClipboard = async (): Promise<void> => {
|
|
||||||
const modifiers = process.platform === 'darwin' ? 4 : 2; // Meta or Ctrl
|
|
||||||
await pressKey('v', modifiers);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if we're on the articles list page (has Write button)
|
// Check if we're on the articles list page (has Write button)
|
||||||
console.log('[x-article] Looking for Write button...');
|
console.log('[x-article] Looking for Write button...');
|
||||||
const writeButtonFound = await waitForElement('[data-testid="empty_state_button_text"]', 10_000);
|
const writeButtonFound = await waitForElement('[data-testid="empty_state_button_text"]', 10_000);
|
||||||
|
|
@ -538,7 +519,7 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
|
|
||||||
// Wait for scroll animation
|
// Wait for scroll animation
|
||||||
await sleep(300);
|
await sleep(500);
|
||||||
|
|
||||||
if (!placeholderFound.result.value) {
|
if (!placeholderFound.result.value) {
|
||||||
console.warn(`[x-article] Placeholder not found in DOM: ${img.placeholder}`);
|
console.warn(`[x-article] Placeholder not found in DOM: ${img.placeholder}`);
|
||||||
|
|
@ -553,25 +534,26 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sleep(300);
|
// Wait for clipboard to be fully ready
|
||||||
|
await sleep(800);
|
||||||
|
|
||||||
// Delete placeholder by pressing Enter (placeholder is already selected)
|
// Delete placeholder by pressing Enter (placeholder is already selected)
|
||||||
console.log(`[x-article] Deleting placeholder with Enter...`);
|
console.log(`[x-article] Deleting placeholder with Enter...`);
|
||||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, { sessionId });
|
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, { sessionId });
|
||||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, { sessionId });
|
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13 }, { sessionId });
|
||||||
await sleep(200);
|
await sleep(300);
|
||||||
|
|
||||||
// Paste image
|
// Paste image using paste script (activates Chrome, sends real keystroke)
|
||||||
console.log(`[x-article] Pasting image...`);
|
console.log(`[x-article] Pasting image...`);
|
||||||
if (sendRealPasteKeystroke()) {
|
if (pasteFromClipboard('Google Chrome', 5, 800)) {
|
||||||
console.log(`[x-article] Image pasted: ${path.basename(img.localPath)}`);
|
console.log(`[x-article] Image pasted: ${path.basename(img.localPath)}`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[x-article] Failed to paste image`);
|
console.warn(`[x-article] Failed to paste image after retries`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for image to upload
|
// Wait for image to upload
|
||||||
console.log(`[x-article] Waiting for upload...`);
|
console.log(`[x-article] Waiting for upload...`);
|
||||||
await sleep(4000);
|
await sleep(5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[x-article] All images processed.');
|
console.log('[x-article] All images processed.');
|
||||||
|
|
@ -633,17 +615,15 @@ export async function publishArticle(options: ArticleOptions): Promise<void> {
|
||||||
console.log('[x-article] Article published!');
|
console.log('[x-article] Article published!');
|
||||||
} else {
|
} else {
|
||||||
console.log('[x-article] Article composed (draft mode).');
|
console.log('[x-article] Article composed (draft mode).');
|
||||||
console.log('[x-article] Browser will stay open for 60 seconds for review...');
|
console.log('[x-article] Browser remains open for manual review.');
|
||||||
await sleep(60_000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
// Disconnect CDP but keep browser open
|
||||||
if (cdp) {
|
if (cdp) {
|
||||||
try { await cdp.send('Browser.close', {}, { timeoutMs: 5_000 }); } catch {}
|
|
||||||
cdp.close();
|
cdp.close();
|
||||||
}
|
}
|
||||||
setTimeout(() => { if (!chrome.killed) try { chrome.kill('SIGKILL'); } catch {} }, 2_000).unref?.();
|
// Don't kill Chrome - let user review and close manually
|
||||||
try { chrome.kill('SIGTERM'); } catch {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,26 @@ import process from 'node:process';
|
||||||
|
|
||||||
const X_COMPOSE_URL = 'https://x.com/compose/post';
|
const X_COMPOSE_URL = 'https://x.com/compose/post';
|
||||||
|
|
||||||
|
function getScriptDir(): string {
|
||||||
|
return path.dirname(new URL(import.meta.url).pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyImageToClipboard(imagePath: string): boolean {
|
||||||
|
const copyScript = path.join(getScriptDir(), 'copy-to-clipboard.ts');
|
||||||
|
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
|
||||||
|
return result.status === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pasteFromClipboard(targetApp?: string, retries = 3, delayMs = 500): boolean {
|
||||||
|
const pasteScript = path.join(getScriptDir(), 'paste-from-clipboard.ts');
|
||||||
|
const args = ['npx', '-y', 'bun', pasteScript, '--retries', String(retries), '--delay', String(delayMs)];
|
||||||
|
if (targetApp) {
|
||||||
|
args.push('--app', targetApp);
|
||||||
|
}
|
||||||
|
const result = spawnSync(args[0]!, args.slice(1), { stdio: 'inherit' });
|
||||||
|
return result.status === 0;
|
||||||
|
}
|
||||||
|
|
||||||
function sleep(ms: number): Promise<void> {
|
function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
@ -274,37 +294,46 @@ export async function postToX(options: XBrowserOptions): Promise<void> {
|
||||||
|
|
||||||
console.log(`[x-browser] Pasting image: ${imagePath}`);
|
console.log(`[x-browser] Pasting image: ${imagePath}`);
|
||||||
|
|
||||||
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
if (!copyImageToClipboard(imagePath)) {
|
||||||
const copyScript = path.join(scriptDir, 'copy-to-clipboard.ts');
|
|
||||||
|
|
||||||
const result = spawnSync('npx', ['-y', 'bun', copyScript, 'image', imagePath], { stdio: 'inherit' });
|
|
||||||
if (result.status !== 0) {
|
|
||||||
console.warn(`[x-browser] Failed to copy image to clipboard: ${imagePath}`);
|
console.warn(`[x-browser] Failed to copy image to clipboard: ${imagePath}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for clipboard to be ready
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
// Focus the editor
|
||||||
await cdp.send('Runtime.evaluate', {
|
await cdp.send('Runtime.evaluate', {
|
||||||
expression: `document.querySelector('[data-testid="tweetTextarea_0"]')?.focus()`,
|
expression: `document.querySelector('[data-testid="tweetTextarea_0"]')?.focus()`,
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
const modifiers = process.platform === 'darwin' ? 4 : 2;
|
// Use paste script (handles platform differences, activates Chrome)
|
||||||
await cdp.send('Input.dispatchKeyEvent', {
|
console.log('[x-browser] Pasting from clipboard...');
|
||||||
type: 'keyDown',
|
const pasteSuccess = pasteFromClipboard('Google Chrome', 5, 500);
|
||||||
key: 'v',
|
|
||||||
code: 'KeyV',
|
if (!pasteSuccess) {
|
||||||
modifiers,
|
// Fallback to CDP (may not work for images on X)
|
||||||
windowsVirtualKeyCode: 86,
|
console.log('[x-browser] Paste script failed, trying CDP fallback...');
|
||||||
}, { sessionId });
|
const modifiers = process.platform === 'darwin' ? 4 : 2;
|
||||||
await cdp.send('Input.dispatchKeyEvent', {
|
await cdp.send('Input.dispatchKeyEvent', {
|
||||||
type: 'keyUp',
|
type: 'keyDown',
|
||||||
key: 'v',
|
key: 'v',
|
||||||
code: 'KeyV',
|
code: 'KeyV',
|
||||||
modifiers,
|
modifiers,
|
||||||
windowsVirtualKeyCode: 86,
|
windowsVirtualKeyCode: 86,
|
||||||
}, { sessionId });
|
}, { sessionId });
|
||||||
|
await cdp.send('Input.dispatchKeyEvent', {
|
||||||
|
type: 'keyUp',
|
||||||
|
key: 'v',
|
||||||
|
code: 'KeyV',
|
||||||
|
modifiers,
|
||||||
|
windowsVirtualKeyCode: 86,
|
||||||
|
}, { sessionId });
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[x-browser] Waiting for image upload...');
|
console.log('[x-browser] Waiting for image upload...');
|
||||||
await sleep(3000);
|
await sleep(4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (submit) {
|
if (submit) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue