From dd08a2aa8912f6c5c2800c229843a293f8d62264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Liu=20=E5=AE=9D=E7=8E=89?= Date: Fri, 13 Feb 2026 20:27:31 -0600 Subject: [PATCH] feat(baoyu-post-to-wechat): add pre-flight environment check script --- skills/baoyu-post-to-wechat/SKILL.md | 24 ++ .../scripts/check-permissions.ts | 251 ++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 skills/baoyu-post-to-wechat/scripts/check-permissions.ts diff --git a/skills/baoyu-post-to-wechat/SKILL.md b/skills/baoyu-post-to-wechat/SKILL.md index cd761a2..6e56515 100644 --- a/skills/baoyu-post-to-wechat/SKILL.md +++ b/skills/baoyu-post-to-wechat/SKILL.md @@ -18,6 +18,7 @@ description: Posts content to WeChat Official Account (微信公众号) via API | `scripts/wechat-browser.ts` | Image-text posts (图文) | | `scripts/wechat-article.ts` | Article posting via browser (文章) | | `scripts/wechat-api.ts` | Article posting via API (文章) | +| `scripts/check-permissions.ts` | Verify environment & permissions | ## Preferences (EXTEND.md) @@ -76,6 +77,29 @@ chrome_profile_path: /path/to/chrome/profile 3. EXTEND.md 4. Skill defaults +## Pre-flight Check (Optional) + +Before first use, suggest running the environment check. User can skip if they prefer. + +```bash +npx -y bun ${SKILL_DIR}/scripts/check-permissions.ts +``` + +Checks: Chrome, profile isolation, Bun, Accessibility, clipboard, paste keystroke, API credentials, Chrome conflicts. + +**If any check fails**, provide fix guidance per item: + +| Check | Fix | +|-------|-----| +| Chrome | Install Chrome or set `WECHAT_BROWSER_CHROME_PATH` env var | +| Profile dir | Ensure `~/.local/share/wechat-browser-profile` is writable | +| Bun runtime | `curl -fsSL https://bun.sh/install \| bash` | +| Accessibility (macOS) | System Settings → Privacy & Security → Accessibility → enable terminal app | +| Clipboard copy | Ensure Swift/AppKit available (macOS Xcode CLI tools: `xcode-select --install`) | +| Paste keystroke (macOS) | Same as Accessibility fix above | +| Paste keystroke (Linux) | Install `xdotool` (X11) or `ydotool` (Wayland) | +| API credentials | Follow guided setup in Step 5, or manually set in `.baoyu-skills/.env` | + ## Image-Text Posting (图文) For short posts with multiple images (up to 9): diff --git a/skills/baoyu-post-to-wechat/scripts/check-permissions.ts b/skills/baoyu-post-to-wechat/scripts/check-permissions.ts new file mode 100644 index 0000000..e8689ca --- /dev/null +++ b/skills/baoyu-post-to-wechat/scripts/check-permissions.ts @@ -0,0 +1,251 @@ +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import process from 'node:process'; +import { findChromeExecutable, getDefaultProfileDir } from './cdp.ts'; + +interface CheckResult { + name: string; + ok: boolean; + detail: string; +} + +const results: CheckResult[] = []; + +function log(label: string, ok: boolean, detail: string): void { + results.push({ name: label, ok, detail }); + const icon = ok ? '✅' : '❌'; + console.log(`${icon} ${label}: ${detail}`); +} + +function warn(label: string, detail: string): void { + results.push({ name: label, ok: true, detail }); + console.log(`⚠️ ${label}: ${detail}`); +} + +async function checkChrome(): Promise { + const chromePath = findChromeExecutable(); + if (chromePath) { + log('Chrome', true, chromePath); + } else { + log('Chrome', false, 'Not found. Set WECHAT_BROWSER_CHROME_PATH env var or install Chrome.'); + } +} + +async function checkProfileIsolation(): Promise { + const profileDir = getDefaultProfileDir(); + const userChromeDir = process.platform === 'darwin' + ? path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome') + : process.platform === 'win32' + ? path.join(os.homedir(), 'AppData', 'Local', 'Google', 'Chrome', 'User Data') + : path.join(os.homedir(), '.config', 'google-chrome'); + + const isIsolated = !profileDir.startsWith(userChromeDir); + log('Profile isolation', isIsolated, `Skill profile: ${profileDir}`); + + if (isIsolated) { + const exists = fs.existsSync(profileDir); + if (exists) { + log('Profile dir', true, 'Exists and accessible'); + } else { + try { + fs.mkdirSync(profileDir, { recursive: true }); + log('Profile dir', true, 'Created successfully'); + } catch (e) { + log('Profile dir', false, `Cannot create: ${e instanceof Error ? e.message : String(e)}`); + } + } + } +} + +async function checkAccessibility(): Promise { + if (process.platform !== 'darwin') { + log('Accessibility', true, `Skipped (not macOS, platform: ${process.platform})`); + return; + } + + const result = spawnSync('osascript', ['-e', ` + tell application "System Events" + set frontApp to name of first application process whose frontmost is true + return frontApp + end tell + `], { stdio: 'pipe', timeout: 10_000 }); + + if (result.status === 0) { + const app = result.stdout?.toString().trim(); + log('Accessibility (System Events)', true, `Frontmost app: ${app}`); + } else { + const stderr = result.stderr?.toString().trim() || ''; + if (stderr.includes('not allowed assistive access') || stderr.includes('1002')) { + log('Accessibility (System Events)', false, + 'Denied. Grant access: System Settings → Privacy & Security → Accessibility → enable your terminal app'); + } else { + log('Accessibility (System Events)', false, `Failed: ${stderr}`); + } + } +} + +async function checkClipboardCopy(): Promise { + if (process.platform !== 'darwin') { + log('Clipboard copy (image)', true, `Skipped (not macOS)`); + return; + } + + const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'wechat-check-')); + try { + const testPng = path.join(tmpDir, 'test.png'); + const swiftSrc = `import AppKit +import Foundation +let size = NSSize(width: 2, height: 2) +let image = NSImage(size: size) +image.lockFocus() +NSColor.red.set() +NSBezierPath.fill(NSRect(origin: .zero, size: size)) +image.unlockFocus() +guard let tiff = image.tiffRepresentation, + let rep = NSBitmapImageRep(data: tiff), + let png = rep.representation(using: .png, properties: [:]) else { + FileHandle.standardError.write("Failed to create test PNG\\n".data(using: .utf8)!) + exit(1) +} +try png.write(to: URL(fileURLWithPath: CommandLine.arguments[1])) +`; + const genScript = path.join(tmpDir, 'gen.swift'); + await writeFile(genScript, swiftSrc, 'utf8'); + const genResult = spawnSync('swift', [genScript, testPng], { stdio: 'pipe', timeout: 30_000 }); + if (genResult.status !== 0) { + log('Clipboard copy (image)', false, `Cannot create test image: ${genResult.stderr?.toString().trim()}`); + return; + } + + const clipSrc = `import AppKit +import Foundation +guard let image = NSImage(contentsOfFile: CommandLine.arguments[1]) else { + FileHandle.standardError.write("Failed to load image\\n".data(using: .utf8)!) + exit(1) +} +let pb = NSPasteboard.general +pb.clearContents() +if !pb.writeObjects([image]) { + FileHandle.standardError.write("Failed to write to clipboard\\n".data(using: .utf8)!) + exit(1) +} +`; + const clipScript = path.join(tmpDir, 'clip.swift'); + await writeFile(clipScript, clipSrc, 'utf8'); + const clipResult = spawnSync('swift', [clipScript, testPng], { stdio: 'pipe', timeout: 30_000 }); + if (clipResult.status === 0) { + log('Clipboard copy (image)', true, 'Can copy image to clipboard via Swift/AppKit'); + } else { + log('Clipboard copy (image)', false, `Failed: ${clipResult.stderr?.toString().trim()}`); + } + } finally { + await rm(tmpDir, { recursive: true, force: true }); + } +} + +async function checkPasteKeystroke(): Promise { + if (process.platform === 'darwin') { + const result = spawnSync('osascript', ['-e', ` + tell application "System Events" + set canSend to true + return canSend + end tell + `], { stdio: 'pipe', timeout: 10_000 }); + + if (result.status === 0) { + log('Paste keystroke (osascript)', true, 'System Events can send keystrokes'); + } else { + const stderr = result.stderr?.toString().trim() || ''; + log('Paste keystroke (osascript)', false, `Cannot send keystrokes: ${stderr}`); + } + } else if (process.platform === 'linux') { + const xdotool = spawnSync('which', ['xdotool'], { stdio: 'pipe' }); + const ydotool = spawnSync('which', ['ydotool'], { stdio: 'pipe' }); + if (xdotool.status === 0) { + log('Paste keystroke', true, 'xdotool available (X11)'); + } else if (ydotool.status === 0) { + log('Paste keystroke', true, 'ydotool available (Wayland)'); + } else { + log('Paste keystroke', false, 'No tool found. Install xdotool (X11) or ydotool (Wayland).'); + } + } else if (process.platform === 'win32') { + log('Paste keystroke', true, 'Windows uses PowerShell SendKeys (built-in)'); + } +} + +async function checkBun(): Promise { + const result = spawnSync('npx', ['-y', 'bun', '--version'], { stdio: 'pipe', timeout: 30_000 }); + if (result.status === 0) { + log('Bun runtime', true, `v${result.stdout?.toString().trim()}`); + } else { + log('Bun runtime', false, 'Cannot run bun. Install: curl -fsSL https://bun.sh/install | bash'); + } +} + +async function checkApiCredentials(): Promise { + const cwd = process.cwd(); + const projectEnv = path.join(cwd, '.baoyu-skills', '.env'); + const userEnv = path.join(os.homedir(), '.baoyu-skills', '.env'); + + let found = false; + for (const envPath of [projectEnv, userEnv]) { + if (fs.existsSync(envPath)) { + const content = fs.readFileSync(envPath, 'utf8'); + if (content.includes('WECHAT_APP_ID')) { + log('API credentials', true, `Found in ${envPath}`); + found = true; + break; + } + } + } + + if (!found) { + warn('API credentials', 'Not found. Required for API publishing method. Run the skill to set up via guided flow.'); + } +} + +async function checkRunningChromeConflict(): Promise { + if (process.platform !== 'darwin') return; + + const result = spawnSync('pgrep', ['-f', 'Google Chrome'], { stdio: 'pipe' }); + const pids = result.stdout?.toString().trim().split('\n').filter(Boolean) || []; + + if (pids.length > 0) { + warn('Running Chrome instances', `${pids.length} Chrome process(es) detected. The skill uses --user-data-dir for isolation, so this is safe.`); + } else { + log('Running Chrome instances', true, 'No existing Chrome processes'); + } +} + +async function main(): Promise { + console.log('=== baoyu-post-to-wechat: Permission & Environment Check ===\n'); + + await checkChrome(); + await checkProfileIsolation(); + await checkBun(); + await checkAccessibility(); + await checkClipboardCopy(); + await checkPasteKeystroke(); + await checkApiCredentials(); + await checkRunningChromeConflict(); + + console.log('\n--- Summary ---'); + const failed = results.filter((r) => !r.ok); + if (failed.length === 0) { + console.log('All checks passed. Ready to post to WeChat.'); + } else { + console.log(`${failed.length} issue(s) found:`); + for (const f of failed) { + console.log(` ❌ ${f.name}: ${f.detail}`); + } + process.exit(1); + } +} + +await main().catch((err) => { + console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); +});