Skill: picseeinc/meta-fb-inbox (version 1.0.0)

--- name: meta-fb-inbox description: Check Facebook page inbox messages via Meta Business Suite browser automation. Use when asked to check Facebook messages, reply to FB customers, or manage Facebook page inbox. Supports multiple pages with custom aliases. --- # Meta Facebook Inbox ⚠️ **CRITICAL**: Always use `profile:"openclaw"` (isolated browser) for all browser actions. Never use Chrome relay. Keep the same `targetId` across operations to avoid losing the tab. ⚠️ **CONTEXT Management**: - Snapshot results are large. Do NOT repeat them in thinking. - Extract needed info and process immediately. - **Preferred approach**: Use `snapshot refs:"aria" compact:true` to find element refs, then `act` + `click`/`type` with ref. ## Configuration Before first use, check if `config.json` exists: ``` read file_path:"skills/meta-fb-inbox/config.json" ``` ⚠️ **PATH RULE**: Always use workspace-relative paths starting from `skills/meta-fb-inbox/`. Never use `../` or absolute paths. If missing or empty, help the user run the setup wizard: ```bash cd skills/meta-fb-inbox node scripts/setup.js ``` ### Config Format `config.json` contains a `pages` array. Each page has an `alias` (display name) and `url` (Meta Business Suite inbox URL): ```json { "pages": [ { "alias": "fb fanpage", "url": "https://business.facebook.com/latest/inbox/all/?&asset_id=123456789012345" }, { "alias": "fb fanpage 2", "url": "https://business.facebook.com/latest/inbox/all/?&asset_id=987654321098765" } ] } ``` ### Resolving a Page When the user asks to check messages: - If they specify an alias (e.g. "fb fanpage 2"), look it up in `config.json` → `pages`. - If they don't specify, use the **first** page in the array. - If only one page exists, use it directly. - If multiple pages exist and no alias given, list available aliases and ask which one. ## Enter Facebook Inbox This is the first step for all operations below. 1. **Check and start browser service:** ``` browser action:"status" ``` If `"running": false`, start the browser service: ``` browser action:"start" profile:"openclaw" ``` Wait 2-3 seconds for the service to initialize. 2. **Read config to get the target page URL:** ``` read file_path:"skills/meta-fb-inbox/config.json" ``` Resolve the page alias to a URL (see "Resolving a Page" above). 3. **Open browser:** ``` browser action:"open" profile:"openclaw" targetUrl:"<pageUrl>" ``` Capture `targetId` and `url` from response. 4. **Wait for page load:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"wait","timeMs":3000} ``` 5. **Check login state:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"evaluate","fn":"function(){return window.location.href;}"} ``` - If URL contains `business.facebook.com` and shows the inbox → logged in, proceed. - If redirected to a login page → notify the user they need to log in manually in the OpenClaw browser, then retry. 6. **Proceed with the user's request.** 7. **Clean Up Browser Tabs** (after all operations): a. Open a blank tab: ``` browser action:"open" profile:"openclaw" targetUrl:"about:blank" ``` b. Get all tabs: ``` browser action:"tabs" profile:"openclaw" ``` c. Close all tabs except the newest one (about:blank): ``` browser action:"close" profile:"openclaw" targetId:"<your_targetId>" ``` ## Quick Check (for cron / automated checks) Complete flow: open page → screenshot → report. 1. Read config: `read file_path:"skills/meta-fb-inbox/config.json"` → resolve page URL 2. Start browser if needed: `browser action:"start" profile:"openclaw"` 3. Open page: `browser action:"open" profile:"openclaw" targetUrl:<pageUrl>` → get `targetId` 4. Wait: `browser action:"act" profile:"openclaw" targetId:<targetId> request:{"kind":"wait","timeMs":4000}` 5. Check URL to verify login (see step 5 above) 6. Take snapshot: `browser action:"snapshot" profile:"openclaw" targetId:<targetId> refs:"aria" compact:true` 7. Look for conversation items in the inbox list. Each conversation typically shows: - Customer name - Last message preview - Timestamp - Unread indicator (bold text or dot) 8. Report results in format: `<name> (<time>) <preview> [未讀/已讀]` 9. Clean up tabs. If the snapshot is hard to parse (Meta's DOM is complex), fall back to: ``` browser action:"screenshot" profile:"openclaw" targetId:<targetId> ``` Then describe what you see in the screenshot. ## Check Inbox Messages 1. Follow "Enter Facebook Inbox" steps to get to the inbox page. 2. Take a snapshot or screenshot to see the conversation list. 3. Report conversations with name, time, preview, and read/unread status. ### Get URLs for Conversations (取得對話網址 - Simple!) **Why get URLs?** Having the direct conversation URL lets you jump straight to a chat later without searching for it again. Saves time when replying. **When to get URLs:** After listing chats, get URLs for unread conversations or any conversation you might need to access again. **📝 Simple 3-Step Method:** 1. **Click into the conversation** using the customer name: ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"evaluate","fn":"function() { const containers = document.querySelectorAll('div[style*=\"position: absolute\"]'); for (let container of containers) { const nameEl = container.querySelector('div.x1vvvo52.x1fvot60.xxio538'); if (nameEl && nameEl.textContent.trim() === '<customer_name>') { nameEl.click(); return {clicked: true}; } } return {error: 'not found'}; }"} ``` Replace `<customer_name>` with the actual customer name from your conversation list. 2. **Wait 2 seconds for page to load and URL to update:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"wait","timeMs":2000} ``` 3. **Get the current URL - it's automatically updated!** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"evaluate","fn":"function(){return window.location.href;}"} ``` 4. **Store the URL** alongside the chat info. **✅ Example URL Format:** ``` https://business.facebook.com/latest/inbox/all/?&asset_id=123456789012345&selected_item_id=1234567890&thread_type=FB_MESSAGE ``` Or with optional mailbox_id: ``` https://business.facebook.com/latest/inbox/all/?&asset_id=123456789012345&mailbox_id=123456789012345&selected_item_id=9876543210&thread_type=FB_MESSAGE ``` **🔍 Understanding the URL:** - `asset_id` = Your Facebook page ID (stays the same) - `selected_item_id` = Unique conversation ID (different for each customer) - `thread_type=FB_MESSAGE` = Messenger conversation type - `mailbox_id` = Optional, may appear for some conversations **💾 How to Use Later:** Next time you need to access this conversation, skip all the searching: ``` browser action:"navigate" profile:"openclaw" targetId:"<targetId>" targetUrl:"<saved_conversation_url>" ``` Wait 2 seconds, and you're in the conversation! **💡 Tip:** Get URLs for unread messages during your check routine, store them in a file or memory, and reuse them for instant access. **🔄 Getting Multiple URLs:** If you need URLs for multiple conversations: 1. Get URL for first conversation (steps 1-3 above) 2. Navigate back to inbox list: ``` browser action:"navigate" profile:"openclaw" targetId:"<targetId>" targetUrl:"<pageUrl>" ``` Wait 2 seconds. 3. Repeat for next conversation. **Alternative method (when already in conversation):** If you're already viewing a conversation and just need its URL, skip step 1 and directly run step 3 - the URL is already there! ## View Specific Conversation **⚡ Fast Path:** If you have the conversation URL from "Get URLs for Conversations": ``` browser action:"navigate" profile:"openclaw" targetId:"<targetId>" targetUrl:"<conversation_url>" ``` Wait 2 seconds, then skip to step 3. **Standard Path:** 1. From the inbox list, find and click the conversation (use snapshot + click by ref). 2. Wait for messages to load (2-3 seconds). 3. Take a screenshot to see the message thread. 4. Report the messages with sender, time, and content. ### Extract Messages with Images To programmatically read messages: 1. **Navigate to the conversation** (if not already there). 2. **Inject the read-messages script:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"evaluate","fn":"<contents_of_scripts/read-messages.js>"} ``` Read the script file: ``` read file_path:"skills/meta-fb-inbox/scripts/read-messages.js" ``` And pass its entire contents as the `fn` parameter (wrapped in parentheses). 3. **Parse the response:** The script returns an array of message objects: ```json [ {"text": "你好", "isCustomer": true, "hasImage": false, "imageUrl": null}, {"text": "[Image]", "isCustomer": true, "hasImage": true, "imageUrl": "https://scontent-..."} ] ``` 4. **Download images (if any):** ⚠️ **Default download location**: `~/Downloads` (do NOT clutter the workspace). For each message with `hasImage: true`: ```bash cd ~/Downloads curl -O "<imageUrl>" ``` The downloaded file will use the original filename from the URL. If you want a custom filename: ```bash cd ~/Downloads curl -o "fb-message-$(date +%Y%m%d-%H%M%S).jpg" "<imageUrl>" ``` ## Reply to a Message **⚡ Fast Path (if you have conversation URL):** If you already obtained the conversation URL from "Get URLs for Conversations" section, skip step 1 and go directly: ``` browser action:"navigate" profile:"openclaw" targetId:"<targetId>" targetUrl:"<conversation_url>" ``` Wait 2 seconds, then proceed to step 2 (Take snapshot to find input box). **Standard Path (when you don't have the URL):** 1. **Open the conversation:** Click the customer name in the conversation list: ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"evaluate","fn":"function() { const containers = document.querySelectorAll('div[style*=\"position: absolute\"]'); for (let container of containers) { const nameEl = container.querySelector('div.x1vvvo52.x1fvot60.xxio538'); if (nameEl && nameEl.textContent.trim() === '<customer_name>') { nameEl.click(); return {clicked: true}; } } return {error: 'not found'}; }"} ``` Replace `<customer_name>` with the actual customer name. Wait 2 seconds for the conversation to load. 2. **Take a snapshot to find the input box:** ``` browser action:"snapshot" profile:"openclaw" targetId:"<targetId>" refs:"aria" compact:true ``` Look for `textbox` with `[active]` attribute. 3. **Type the reply message:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"type","ref":"<input_ref>","text":"<your_message>"} ``` After typing, wait 1 second for the send button to appear. 4. **Take another snapshot to find the send button:** ``` browser action:"snapshot" profile:"openclaw" targetId:"<targetId>" refs:"aria" compact:true ``` Look for a new `button` near the textbox. The send button appears after you type text (it replaces the "like" button). 5. **Click the send button:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"click","ref":"<send_button_ref>"} ``` 6. **Verify the message was sent:** Wait 2 seconds, then take a screenshot or check the conversation list to confirm the message appears. ## Manage Labels **⚡ Fast Path:** If you have the conversation URL, navigate directly: ``` browser action:"navigate" profile:"openclaw" targetId:"<targetId>" targetUrl:"<conversation_url>" ``` Wait 2 seconds, then proceed to the label operations. ### Remove a Label 1. **Open the conversation** (if not already open, and you don't have URL). 2. **Take a snapshot to locate the label's remove button:** ``` browser action:"snapshot" profile:"openclaw" targetId:"<targetId>" refs:"aria" compact:true ``` Look for the Labels section in the right sidebar (appears before Notes section): - A `heading` with level=3 (the Labels heading) - Each label appears as a `button` element - Inside each label button, there's a nested `button` with text "clearLabel" - The clearLabel button is the "×" (remove) icon 3. **Click the clearLabel button:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"click","ref":"<clearLabel_ref>"} ``` **Example from snapshot:** ``` - heading [level=3] [ref=eAAA] ← Labels heading - button [ref=e949]: ← Label button (shows label name) - button "clearLabel" [ref=e952] ← Remove button (THIS ONE!) ``` Click the clearLabel button (`ref=e952`) to remove the label. 4. **Wait for the change to apply:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"wait","timeMs":1500} ``` 5. **Verify the label was removed** (optional): Take a screenshot or snapshot to confirm the label is gone. ### Add a Label 1. Open the conversation. 2. Take a snapshot to find the label input combobox: ``` - combobox "label" [ref=eXXX] ``` 3. Type the label name: ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"type","ref":"<combobox_ref>","text":"<label_name>","submit":true} ``` 4. Wait and verify. ## Manage Notes (備註) **⚡ Fast Path:** If you have the conversation URL, navigate directly: ``` browser action:"navigate" profile:"openclaw" targetId:"<targetId>" targetUrl:"<conversation_url>" ``` Wait 2 seconds, then proceed to the note operations. ### Quick Decision Guide (看這裡！) **Question: Do you need to ADD or EDIT a note?** 1. Take a snapshot of the conversation sidebar. 2. Look for the Notes section (usually appears after Labels section). 3. Check what you see: - **ONLY see a "Add note" type button/link (single button, no existing note text)** → Contact has NO note → Use **"Add a New Note"** section below. - **See an "Edit" link + existing note text (usually with timestamp)** → Contact has existing note → Use **"Edit an Existing Note"** section below. **Simple rule:** - No note = Add new (only one button visible) - Has note = Edit existing (edit link + note text visible) **How to identify in snapshot:** - **No note**: Look for pattern like `button [ref=eXXX]` or `link [ref=eXXX]` with NO paragraph text nearby - **Has note**: Look for pattern like `link [ref=eXXX]` (edit) + `link [ref=eYYY]` (delete) + `paragraph [ref=eZZZ]: "note text"` ### Note Structure The Notes section is located below the Labels section in the right sidebar. There are two states: 1. **No note exists**: You will see a single button or link (typically the "add note" action). 2. **Note exists**: You will see: - The note text (in a `paragraph` element) - An Edit link (usually appears before the note text with timestamp) - A Delete link (next to edit link) - Sometimes an "add note" link (for additional notes, but we prefer single-note approach) **Best Practice**: Keep only one note per contact. Use "Edit" to update existing notes rather than adding multiple notes. **In snapshot, look for these patterns:** No note: ``` - heading [level=3] [ref=eXXX] ← Notes heading - button [ref=eYYY] ← Single "add note" button ``` Has note: ``` - heading [level=3] [ref=eXXX] ← Notes heading - link [ref=eYYY] ← "add note" link - text: "X minutes ago ·" ← Timestamp - link [ref=eZZZ] ← Edit link (THIS ONE for editing!) - text: "·" - link [ref=eAAA] ← Delete link - paragraph [ref=eBBB]: "note text" ← Existing note content ``` ### Edit an Existing Note (編輯註記 - Simple Steps for AI) **When to use:** Contact already has a note. You will see an Edit link next to existing note text with timestamp. **Step-by-step:** 1. **Open the conversation** (click customer name in chat list). Wait 2 seconds for it to load. 2. **Take snapshot** to find the Edit button: ``` browser action:"snapshot" profile:"openclaw" targetId:"<targetId>" refs:"aria" compact:true ``` Look for the pattern (Notes section): ``` - heading [level=3] [ref=eAAA] ← Notes heading - link [ref=eBBB] ← "add note" link - text: "4 hours ago ·" ← Timestamp - link [ref=eXXX] ← Edit link (THIS ONE!) - text: "·" - link [ref=eYYY] ← Delete link - paragraph [ref=eZZZ]: "Customer note" ← Current note ``` **Key**: Find the link that appears AFTER the timestamp text and BEFORE the "·" separator. That's your Edit button. 3. **Click Edit button:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"click","ref":"<edit_button_ref>"} ``` 4. **Wait for edit form:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"wait","timeMs":1500} ``` 5. **Take snapshot** to find the textbox: ``` browser action:"snapshot" profile:"openclaw" targetId:"<targetId>" refs:"aria" compact:true ``` Look for: ``` - textbox [ref=eXXX]: "Customer note" ← Current text (contains existing note) - button [ref=eYYY] ← Cancel button - button [ref=eZZZ] ← Delete button - button [disabled] [ref=eAAA] ← Save button (disabled until you change text) ``` **Key**: The textbox will have the current note text. The Save button will show `[disabled]` attribute. 6. **Click textbox to focus:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"click","ref":"<textbox_ref>"} ``` 7. **Select all existing text (Cmd+A):** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"press","key":"Meta+a"} ``` 8. **Type new text (replaces selected text):** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"type","ref":"<textbox_ref>","text":"Customer ID: 12345 ✅"} ``` 9. **Wait for Save button to enable:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"wait","timeMs":500} ``` 10. **Take snapshot** to find enabled Save button: ``` browser action:"snapshot" profile:"openclaw" targetId:"<targetId>" refs:"aria" compact:true ``` Look for: ``` - button [ref=eXXX] [cursor=pointer]: ← Now enabled! (no [disabled] attribute) ``` **Key**: The Save button will now show `[cursor=pointer]` and will NOT have `[disabled]` attribute. 11. **Click Save:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"click","ref":"<save_button_ref>"} ``` 12. **Wait and verify:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"wait","timeMs":2000} ``` Take screenshot to confirm update. **Common mistakes to avoid:** - Don't forget to select all (Cmd+A) before typing — otherwise you append to existing text! - Don't skip the wait after clicking — buttons need time to load. - Always take a fresh snapshot to get new ref numbers. ### Add a New Note (新增註記 - Simple Steps for AI) **When to use:** Contact has NO existing note. You will see ONLY a single button/link (no existing note text). **Step-by-step:** 1. **Open the conversation** (click customer name in chat list). Wait 2 seconds for it to load. 2. **Take snapshot** to find the "add note" button: ``` browser action:"snapshot" profile:"openclaw" targetId:"<targetId>" refs:"aria" compact:true ``` Look for the pattern (Notes section with NO existing note): ``` - heading [level=3] [ref=eAAA] ← Notes heading - button [ref=eXXX] ← Single "add note" button (THIS ONE!) ``` **Key**: You should see ONLY one button/link under the Notes heading, with NO paragraph element containing note text. 3. **Click "add note" button:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"click","ref":"<button_ref>"} ``` 4. **Wait for the textbox:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"wait","timeMs":1500} ``` 5. **Take snapshot** to find the textbox: ``` browser action:"snapshot" profile:"openclaw" targetId:"<targetId>" refs:"aria" compact:true ``` Look for: ``` - textbox [ref=eXXX]: ← Input field (may show "Hidden Label") /placeholder: ... ← Placeholder text - button [ref=eYYY] ← Cancel button - button [disabled] [ref=eZZZ] ← Save button (disabled at first) ``` **Key**: The textbox might be labeled "Hidden Label" or similar. Look for `[disabled]` attribute on the Save button. 6. **Type the note text:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"type","ref":"<textbox_ref>","text":"Customer note text here"} ``` 7. **Wait for Save button to become enabled:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"wait","timeMs":500} ``` 8. **Take snapshot** to find the enabled Save button: ``` browser action:"snapshot" profile:"openclaw" targetId:"<targetId>" refs:"aria" compact:true ``` Look for: ``` - button [ref=eXXX] [cursor=pointer]: ← Now enabled (no [disabled] attribute!) ``` 9. **Click Save button:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"click","ref":"<save_button_ref>"} ``` 10. **Wait and verify:** ``` browser action:"act" profile:"openclaw" targetId:"<targetId>" request:{"kind":"wait","timeMs":2000} ``` Take a screenshot to confirm the note appears in the sidebar. **Common mistakes to avoid:** - Don't skip snapshots — you need the ref numbers! - Don't reuse old ref numbers — they change every snapshot. - Wait after clicking before taking next snapshot. ### Delete a Note 1. Take a snapshot to find the Delete link (appears next to Edit link when a note exists). 2. Click the Delete link (the link that appears AFTER the "·" separator, next to the Edit link). 3. Confirm the deletion if prompted. **In snapshot:** ``` - link [ref=eXXX] ← Edit link - text: "·" - link [ref=eYYY] ← Delete link (THIS ONE!) ``` ## Switch Between Pages If multiple pages are configured: 1. Read config to see all available pages. 2. Navigate to the desired page URL: ``` browser action:"navigate" profile:"openclaw" targetId:"<targetId>" targetUrl:"<newPageUrl>" ``` 3. Wait for the page to load. ## Notes - Meta Business Suite sessions expire periodically; re-login may be required. - Facebook's DOM structure is complex and changes frequently. Prefer screenshots over DOM parsing when snapshots are unreliable. - The inbox URL format: `https://business.facebook.com/latest/inbox/all/?&asset_id=<PAGE_ID>` - Some pages may require specific permissions in Meta Business Suite.

path: _meta.json
content: slug: meta-fb-inbox
version: 1.0.0

path: README.md
content: # Meta Facebook Inbox

Check Facebook page inbox messages via Meta Business Suite browser automation.

## Features

- ✅ Check inbox messages and unread status
- ✅ Reply to customer messages
- ✅ Get direct conversation URLs for fast access
- ✅ Manage labels (tags) for each conversation
- ✅ Manage notes (internal memos) for each contact
- ✅ Extract messages with images
- ✅ Support multiple Facebook pages with custom aliases

## Installation

### 1. Install the skill

If using ClawHub:
```bash
clawhub install meta-fb-inbox
```

Or clone manually:
```bash
git clone <repo> ~/.openclaw/workspace/skills/meta-fb-inbox
```

### 2. Run setup

```bash
cd ~/.openclaw/workspace/skills/meta-fb-inbox
node scripts/setup.js
```

The setup wizard will:
1. Open https://business.facebook.com/ in your browser
2. Guide you to log in and navigate to your inbox
3. Ask you to copy and paste the inbox URL
4. Save the configuration to `config.json`

### 3. Start using

Tell your OpenClaw agent:
- "Check Facebook inbox"
- "Check FB messages"
- "Reply to [customer name] on Facebook"
- "Add note to [customer name] conversation: [note text]"
- "Get conversation URLs for unread messages"

## Manual Configuration

If you prefer to configure manually, create `config.json`:

```json
{
  "pages": [
    {
      "alias": "fb fanpage",
      "url": "https://business.facebook.com/latest/inbox/all/?&asset_id=123456789012345"
    }
  ]
}
```

To find your inbox URL:
1. Visit https://business.facebook.com/
2. Go to Inbox section
3. Select "All messages"
4. Copy the full URL from the browser address bar (should contain `asset_id`)

### Multiple Pages

To manage multiple Facebook pages, add more entries to the `pages` array:

```json
{
  "pages": [
    {
      "alias": "main page",
      "url": "https://business.facebook.com/latest/inbox/all/?&asset_id=111111111111111"
    },
    {
      "alias": "support page",
      "url": "https://business.facebook.com/latest/inbox/all/?&asset_id=222222222222222"
    }
  ]
}
```

When you ask to check messages without specifying an alias, the agent will use the first page in the array.

## Reconfiguration

To change your Facebook page or add more pages:
```bash
cd ~/.openclaw/workspace/skills/meta-fb-inbox
node scripts/setup.js
```

The wizard will detect existing config and ask if you want to add a new page or overwrite.

## Conversation URLs

One powerful feature of this skill is getting direct conversation URLs. When you ask the agent to check messages, it can optionally retrieve the direct URL for each conversation.

With a conversation URL, you can:
- Jump directly to a conversation without searching
- Save frequently-accessed conversations for instant access
- Reduce API calls and improve response time

Example conversation URL:
```
https://business.facebook.com/latest/inbox/all/?&asset_id=123456789012345&selected_item_id=9876543210&thread_type=FB_MESSAGE
```

The agent can store these URLs and reuse them for fast access later.

## Troubleshooting

**"config.json not found"**
- Run `node scripts/setup.js` in the skill directory

**"Session expired" or login required**
- Meta Business Suite sessions expire periodically
- The skill will detect this and guide you through re-login

**Browser control issues**
- Make sure OpenClaw browser service is running
- The skill uses `profile:"openclaw"` (isolated browser)
- Never use Chrome relay for Meta Business Suite

**Conversation not found**
- Make sure the customer name is spelled correctly
- The skill searches by exact text match in the conversation list
- Try using a direct conversation URL if you have it

**Image download location**
- Images are downloaded to `~/Downloads` by default
- Named with timestamp: `fb-message-YYYYMMDD-HHMMSS.jpg`

## Notes

- This skill uses browser automation (no official Facebook API)
- Sessions may expire; re-login required periodically
- Multi-page management is supported
- All operations are performed through the OpenClaw isolated browser
- Labels and Notes are internal to Meta Business Suite (not visible to customers)

## License

MIT


path: scripts/read-messages.js
content: (function() {
  // Find all message bubbles in the conversation
  // Messages are identified by specific background colors:
  // - Blue (rgb(10, 124, 255)) = our replies
  // - Gray (rgb(239, 239, 239) or similar) = customer messages
  
  const allDivs = document.querySelectorAll('div');
  const messages = [];
  const processedImages = new Set();
  
  // First pass: collect text messages
  for (let div of allDivs) {
    const text = div.textContent.trim();
    
    // Skip if text is too short or too long
    if (text.length < 2 || text.length > 1000) continue;
    
    // Check background color
    const bg = window.getComputedStyle(div).backgroundColor;
    const match = bg.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
    if (!match) continue;
    
    const [_, r, g, b] = match.map(Number);
    
    let isCustomer = null;
    
    // Blue background = our message
    if (b > 200 && r < 50 && g > 100 && g < 150) {
      isCustomer = false;
    }
    // Gray/white background = customer message  
    else if (r > 200 && g > 200 && b > 200) {
      isCustomer = true;
    }
    
    // Skip if not a message bubble
    if (isCustomer === null) continue;
    
    // Check if this div has the common message class pattern
    const className = div.className || '';
    if (!className.includes('x1y1aw1k')) continue;
    
    // Avoid duplicate messages (parent containers may also match)
    // Only take messages with relatively few children
    if (div.children.length > 5) continue;
    
    // Check for duplicates
    const isDuplicate = messages.some(m => m.text === text);
    if (isDuplicate) continue;
    
    messages.push({
      text: text,
      isCustomer: isCustomer,
      hasImage: false,
      imageUrl: null,
      bg: bg
    });
  }
  
  // Second pass: find standalone images (photos without text bubbles)
  const images = document.querySelectorAll('img[src*="fbcdn.net"]');
  for (let img of images) {
    // Only process large images (actual message photos, not icons/avatars)
    if (img.width < 100 || img.height < 100) continue;
    if (processedImages.has(img.src)) continue;
    
    // Filter out link preview images (external.*.fna.fbcdn.net)
    // Only keep actual uploaded photos (scontent-*.xx.fbcdn.net)
    if (img.src.includes('external.') && img.src.includes('.fna.fbcdn.net')) {
      continue;
    }
    
    // Images in Facebook Messenger are typically customer messages (left side)
    // unless they're in a blue bubble container
    processedImages.add(img.src);
    
    messages.push({
      text: '[Image]',
      isCustomer: true,  // Assume customer sent it
      hasImage: true,
      imageUrl: img.src,
      bg: 'image'
    });
  }
  
  // Reverse to show chronological order (oldest first)
  return messages.reverse();
})()


path: scripts/list-chats.js
content: (function() {
  // Find chat list container by structural pattern:
  // - Parent contains multiple children with position: absolute
  // - Each child is a conversation item with timestamp and name div
  let chatContainer = null;
  const containers = document.querySelectorAll('div[style*="position: absolute"]');
  
  for (let container of containers) {
    const parent = container.parentElement;
    if (!parent) continue;
    
    const siblings = Array.from(parent.children);
    // Chat list usually has 5-30 items
    if (siblings.length >= 5 && siblings.length <= 30) {
      // Verify it's the chat list by checking for timestamp and name elements
      const hasTimestamp = !!parent.querySelector('abbr.timestamp');
      const hasNameDiv = !!parent.querySelector('div.x1vvvo52.x1fvot60.xxio538');
      
      if (hasTimestamp && hasNameDiv) {
        chatContainer = parent;
        break;
      }
    }
  }
  
  if (!chatContainer) {
    return { error: 'Chat container not found' };
  }
  
  // Extract chat items
  const items = Array.from(chatContainer.children);
  const chats = [];
  
  items.forEach(item => {
    try {
      const text = item.textContent || '';
      
      // Skip if too short or empty
      if (text.length < 10) return;
      
      // Find name element by class pattern
      const nameEl = item.querySelector('div.x1vvvo52.x1fvot60.xxio538');
      const name = nameEl ? nameEl.textContent.trim() : '';
      
      // Find time using timestamp element
      const timeEl = item.querySelector('abbr.timestamp');
      const time = timeEl ? timeEl.textContent.trim() : '';
      
      // Find last message preview
      // Look for divs that contain message content (usually short text with <=3 children)
      const allDivs = item.querySelectorAll('div');
      let lastMsg = '';
      for (let div of allDivs) {
        const divText = div.textContent.trim();
        // Message preview typically 10-300 chars, has few children
        if (divText.length > 10 && divText.length < 300 && div.children.length <= 3) {
          // Skip if it's just the name or time
          if (divText === name || divText === time) continue;
          
          // Clean up: remove name prefix if present
          let cleaned = divText;
          if (cleaned.startsWith(name)) {
            cleaned = cleaned.substring(name.length).trim();
          }
          
          // Take the first suitable message preview
          if (cleaned.length > 5) {
            lastMsg = cleaned.substring(0, 100);
            break;
          }
        }
      }
      
      // Check unread status by font weight
      const nameStyle = nameEl ? window.getComputedStyle(nameEl) : null;
      const unread = nameStyle && (nameStyle.fontWeight === 'bold' || parseInt(nameStyle.fontWeight) >= 600);
      
      if (name) {
        chats.push({
          name: name,
          time: time || '',
          lastMsg: lastMsg || '',
          unread: !!unread
        });
      }
    } catch (e) {
      // Skip items that fail to parse
    }
  });
  
  return chats;
})()


path: scripts/setup.js
content: #!/usr/bin/env node
/**
 * Meta Facebook Inbox Setup Wizard
 * Supports multiple Facebook pages with custom aliases.
 * Run: node setup.js
 */

const fs = require('fs');
const path = require('path');
const readline = require('readline');

const CONFIG_PATH = path.join(__dirname, '..', 'config.json');
const DEFAULT_ALIAS = 'fb fanpage';

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

function question(prompt) {
  return new Promise(resolve => rl.question(prompt, resolve));
}

function loadConfig() {
  if (fs.existsSync(CONFIG_PATH)) {
    return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
  }
  return { pages: [] };
}

function saveConfig(config) {
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
}

function validateUrl(url) {
  return url.startsWith('https://business.facebook.com/') || url.startsWith('https://www.facebook.com/');
}

function showPages(pages) {
  if (pages.length === 0) {
    console.log('  (no pages configured)\n');
    return;
  }
  pages.forEach((p, i) => {
    console.log(`  ${i + 1}. [${p.alias}] ${p.url}`);
  });
  console.log('');
}

async function addPage(config) {
  console.log('\n📋 Instructions:');
  console.log('  1. Open Meta Business Suite in your browser');
  console.log('  2. Go to the Inbox of your Facebook page');
  console.log('  3. Copy the full URL from the address bar');
  console.log('     (e.g. https://business.facebook.com/latest/inbox/all/?&asset_id=123456)\n');

  let url = '';
  while (!url) {
    const input = await question('Paste the Facebook Inbox URL: ');
    const trimmed = input.trim();
    if (!trimmed) {
      console.log('❌ URL cannot be empty.\n');
      continue;
    }
    if (!validateUrl(trimmed)) {
      console.log('❌ URL must start with https://business.facebook.com/ or https://www.facebook.com/\n');
      continue;
    }
    url = trimmed;
  }

  const aliasInput = await question(`Give this page an alias (default: "${DEFAULT_ALIAS}"): `);
  const alias = aliasInput.trim() || DEFAULT_ALIAS;

  // Check for duplicate alias
  const existing = config.pages.find(p => p.alias === alias);
  if (existing) {
    const overwrite = await question(`⚠️  Alias "${alias}" already exists. Overwrite? (y/N): `);
    if (overwrite.toLowerCase() !== 'y') {
      console.log('Skipped.\n');
      return;
    }
    existing.url = url;
  } else {
    config.pages.push({ alias, url });
  }

  saveConfig(config);
  console.log(`\n✅ Page saved: [${alias}] ${url}\n`);
}

async function removePage(config) {
  if (config.pages.length === 0) {
    console.log('No pages to remove.\n');
    return;
  }
  showPages(config.pages);
  const num = await question('Enter page number to remove (or 0 to cancel): ');
  const idx = parseInt(num, 10) - 1;
  if (idx < 0 || idx >= config.pages.length) {
    console.log('Cancelled.\n');
    return;
  }
  const removed = config.pages.splice(idx, 1)[0];
  saveConfig(config);
  console.log(`✅ Removed: [${removed.alias}]\n`);
}

async function main() {
  console.log('\n🔧 Meta Facebook Inbox Setup\n');

  const config = loadConfig();

  console.log('Current pages:');
  showPages(config.pages);

  let running = true;
  while (running) {
    const choice = await question('What would you like to do?\n  1. Add a page\n  2. Remove a page\n  3. Done\n\nChoice (1/2/3): ');
    switch (choice.trim()) {
      case '1':
        await addPage(config);
        break;
      case '2':
        await removePage(config);
        break;
      case '3':
      default:
        running = false;
        break;
    }
  }

  console.log('Current configuration:');
  showPages(config.pages);
  console.log('Setup complete. You can re-run this script anytime to add or remove pages.\n');
  rl.close();
}

main().catch(err => {
  console.error('Error:', err);
  rl.close();
  process.exit(1);
});


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)