Skill: autogame-17/feishu-whiteboard (version 1.0.0)

# Feishu Whiteboard Skill Allows creating and manipulating Feishu Whiteboards programmatically. ## Configuration Requires `FEISHU_APP_ID` and `FEISHU_APP_SECRET` in environment or `config.json`. Scopes required: `board:whiteboard:node:create` ## Usage ### Create a Board ```bash node skills/feishu-whiteboard/create.js "My Architecture Diagram" ``` Output: JSON containing `whiteboard_id`. ### Add Nodes (Demo) ```bash node skills/feishu-whiteboard/draw.js <whiteboard_id> demo ``` Adds a rectangle and a circle connected by a line. ### Programmatic Usage ```javascript const { createWhiteboard } = require('./create'); const { addNodes, createShape, createConnector } = require('./draw'); const board = await createWhiteboard("System Design"); const nodes = [ createShape("web", "rect", 0, 0, 200, 100, "Web Server"), createShape("db", "cylinder", 0, 300, 100, 100, "Database"), createConnector("link1", "web", "db") ]; await addNodes(board.whiteboard_id, nodes); ``` ## Troubleshooting If you encounter `404 page not found`, it usually means the Whiteboard API is not enabled for your tenant or the endpoint URL has changed. The current implementation uses `/open-apis/board/v1/whiteboards`.

path: README.md
content: # Feishu Whiteboard Skill

Allows creating and manipulating Feishu Whiteboards programmatically.

## Configuration
Requires `FEISHU_APP_ID` and `FEISHU_APP_SECRET` in environment or `config.json`.
Scopes required: `board:whiteboard:node:create`

## Usage

### Create a Board
```bash
node skills/feishu-whiteboard/create.js "My Architecture Diagram"
```
Output: JSON containing `whiteboard_id`.

### Add Nodes (Demo)
```bash
node skills/feishu-whiteboard/draw.js <whiteboard_id> demo
```
Adds a rectangle and a circle connected by a line.

### Programmatic Usage

```javascript
const { createWhiteboard } = require('./create');
const { addNodes, createShape, createConnector } = require('./draw');

const board = await createWhiteboard("System Design");
const nodes = [
  createShape("web", "rect", 0, 0, 200, 100, "Web Server"),
  createShape("db", "cylinder", 0, 300, 100, 100, "Database"),
  createConnector("link1", "web", "db")
];
await addNodes(board.whiteboard_id, nodes);
```

## Troubleshooting
If you encounter `404 page not found`, it usually means the Whiteboard API is not enabled for your tenant or the endpoint URL has changed. The current implementation uses `/open-apis/board/v1/whiteboards`.


path: _meta.json
content: ownerId: kn7apafdj4thknczrgxdzfd2v1808svf
slug: feishu-whiteboard
version: 1.0.0
publishedAt: 1770317626935

path: api.js
content: const fs = require('fs');
const path = require('path');
require('dotenv').config({ quiet: true });

// Unified Token Cache (Shared with feishu-doc, feishu-card, etc.)
const TOKEN_CACHE_FILE = path.resolve(__dirname, '../../memory/feishu_token.json');

let tokenCache = {
  token: null,
  expireTime: 0
};

function loadConfig() {
  const configPath = path.join(__dirname, 'config.json');
  let config = {};
  if (fs.existsSync(configPath)) {
    try {
      config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
    } catch (e) {
      // Ignore
    }
  }
  
  return {
    app_id: process.env.FEISHU_APP_ID || config.app_id,
    app_secret: process.env.FEISHU_APP_SECRET || config.app_secret
  };
}

async function getTenantAccessToken(forceRefresh = false) {
  const now = Math.floor(Date.now() / 1000);

  // Try to load from disk first
  if (!forceRefresh && !tokenCache.token && fs.existsSync(TOKEN_CACHE_FILE)) {
    try {
      const saved = JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, 'utf8'));
      // Handle both 'expire' (standard) and 'expireTime' (legacy)
      const expiry = saved.expire || saved.expireTime;
      if (saved.token && expiry > now) {
        tokenCache.[REDACTED_SECRET];
        tokenCache.expireTime = expiry; 
      }
    } catch (e) {
      // Ignore corrupted cache
    }
  }

  // Force Refresh: Delete memory cache and file cache
  if (forceRefresh) {
    tokenCache.token = null;
    tokenCache.expireTime = 0;
  }

  if (tokenCache.token && tokenCache.expireTime > now) {
    return tokenCache.token;
  }

  const config = loadConfig();
  if (!config.app_id || !config.app_secret) {
    throw new Error("Missing app_id or app_secret.");
  }

  const response = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      "app_id": config.app_id,
      "app_secret": config.app_secret
    })
  });

  const data = await response.json();
  if (data.code !== 0) {
    throw new Error(`Failed to get tenant_access_token: ${data.msg}`);
  }

  tokenCache.[REDACTED_SECRET];
  tokenCache.expireTime = now + data.expire - 60;

  try {
    const cacheDir = path.dirname(TOKEN_CACHE_FILE);
    if (!fs.existsSync(cacheDir)) {
      fs.mkdirSync(cacheDir, { recursive: true });
    }
    fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify({
         [REDACTED_SECRET],
         expire: tokenCache.expireTime
    }, null, 2));
  } catch (e) {
    console.error("Failed to save token cache:", e.message);
  }

  return tokenCache.token;
}

async function apiRequest(method, endpoint, body = null) {
    let token = await getTenantAccessToken();
    const url = `https://open.feishu.cn${endpoint}`;
    
    const headers = {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json; charset=utf-8'
    };

    let response = await fetch(url, {
        method,
        headers,
        body: body ? JSON.stringify(body) : null
    });

    // Handle token expiry (code 99991663 or similar auth errors)
    // Simple check: if 401, retry once
    if (response.status === 401) {
         token = await getTenantAccessToken(true);
         headers['Authorization'] = `Bearer ${token}`;
         response = await fetch(url, {
            method,
            headers,
            body: body ? JSON.stringify(body) : null
        });
    }

    const rawText = await response.text();
    let data;
    try {
        data = JSON.parse(rawText);
    } catch (e) {
        throw new Error(`Failed to parse API response: ${rawText.substring(0, 200)}...`);
    }

    if (data.code !== 0) {
        throw new Error(`API Error [${endpoint}]: ${data.msg} (Code: ${data.code})`);
    }
    return data.data;
}

module.exports = {
    getTenantAccessToken,
    apiRequest
};


path: create.js
content: const { apiRequest } = require('./api');

async function createWhiteboard(title = "Untitled Whiteboard") {
    try {
        console.log(`Creating whiteboard: ${title}...`);
        const result = await apiRequest('POST', '/open-apis/board/v1/whiteboards', {
            title: title
        });
        console.log("Whiteboard created successfully!");
        console.log(JSON.stringify(result, null, 2));
        return result;
    } catch (error) {
        console.error("Failed to create whiteboard:", error.message);
        process.exit(1);
    }
}

if (require.main === module) {
    const title = process.argv[2] || `New Board ${new Date().toISOString()}`;
    createWhiteboard(title);
}

module.exports = { createWhiteboard };


path: create_dashboard.js
content: const { createWhiteboard } = require('./create');
const { addNodes, createShape, createConnector } = require('./draw');

async function main() {
    try {
        // 1. Create Board
        console.log("Creating Dashboard Concept Board...");
        const board = await createWhiteboard("🤖 AI Agent Dashboard (Concept)");
        const boardId = board.whiteboard_id;
        console.log(`Board Created: ${boardId}`);

        // 2. Define Nodes
        const nodes = [];
        
        // Center: Live2D Anchor
        nodes.push(createShape("center_avatar", "circle", 0, 0, 200, 200, "Live2D\nAvatar\n(Center)", { r: 255, g: 200, b: 200, a: 1 }));

        // Left: Work Bench
        nodes.push(createShape("left_work", "rect", -400, 0, 250, 400, "🛠️ Workbench\n- Skills\n- Terminal\n- Tasks", { r: 200, g: 200, b: 255, a: 1 }));

        // Right: Life Area
        nodes.push(createShape("right_life", "rect", 400, 0, 250, 400, "🧠 Life & Memory\n- Diary\n- Friends\n- Soul.md", { r: 200, g: 255, b: 200, a: 1 }));

        // Bottom: Assets (Background/Back)
        nodes.push(createShape("bottom_assets", "rect", 0, 400, 600, 200, "📦 Asset Library\n- Images\n- Stickers\n- Voice", { r: 255, g: 255, b: 200, a: 1 }));

        // Connectors
        nodes.push(createConnector("conn_work", "center_avatar", "left_work"));
        nodes.push(createConnector("conn_life", "center_avatar", "right_life"));
        nodes.push(createConnector("conn_asset", "center_avatar", "bottom_assets"));

        // 3. Draw
        await addNodes(boardId, nodes);
        
        console.log(`SUCCESS: ${board.url}`);
    } catch (e) {
        console.error("Error:", e.message);
    }
}

main();


path: create_dashboard_real.js
content: const { addNodes } = require('./draw');

// Correct Schema Helper based on Reverse Engineering
function createRealShape(id, shapeType, x, y, width, height, text, fillColor) {
    return {
        id: id,
        type: "composite_shape",
        composite_shape: {
            type: shapeType // rect, round_rect, circle, diamond
        },
        x: x,
        y: y,
        width: width,
        height: height,
        style: {
            fill_color: fillColor || "#ffffff",
            fill_opacity: 100,
            border_color: "#000000",
            border_width: "regular" // or "narrow"
        },
        text: {
            text: text,
            font_size: 14,
            horizontal_align: "center",
            vertical_align: "mid"
        }
    };
}

function createRealConnector(id, startId, endId, startSide="right", endSide="left") {
    // Simplified connector logic - Feishu connectors link shapes by ID, not just coordinates if supported?
    // Actually the read output showed connectors linking positions. 
    // Linking by ID usually requires "start_object_id" and "end_object_id" which IS in my original draw.js schema.
    // Let's stick to the ID linking, hoping it works with the correct endpoint. 
    // If not, we might need to calculate coordinates. 
    // Wait, read output shows: "start": { "position": { "x": ... } } 
    // But docs say object linking is possible. Let's try object linking first.
    return {
        id: id,
        type: "connector",
        connector: {
            start: { id: startId, side: startSide }, // Hypothetical, need to check if ID binding works
            end: { id: endId, side: endSide }
        }
    };
}

// Fallback: Coordinate Connector
function createCoordinateConnector(id, x1, y1, x2, y2) {
    return {
        id: id,
        type: "connector",
        connector: {
            start: { position: { x: x1, y: y1 } },
            end: { position: { x: x2, y: y2 }, arrow_style: "triangle_arrow" },
            shape: "curved"
        }
    };
}

async function main() {
    const boardId = "N3UOwBzjNhgPyIbgH7vcWznSnRc"; // Fan Maowei's Board
    const startX = 3000;
    const startY = 7000;

    const nodes = [];

    // 1. Center: Avatar (Circle)
    nodes.push(createRealShape("node_center", "circle", startX, startY, 200, 200, "Live2D Avatar\n(Interaction)", "#ffccc7")); // Red-ish

    // 2. Left: Work (Rect)
    nodes.push(createRealShape("node_left", "rect", startX - 400, startY, 200, 400, "Workbench\n(Skills/CLI)", "#bae7ff")); // Blue-ish

    // 3. Right: Life (Rect)
    nodes.push(createRealShape("node_right", "rect", startX + 400, startY, 200, 400, "Life & Memory\n(Soul)", "#d9f7be")); // Green-ish

    // 4. Bottom: Assets (Rect)
    nodes.push(createRealShape("node_bottom", "rect", startX, startY + 400, 600, 150, "Asset Library\n(Media)", "#efdbff")); // Purple-ish

    // 5. Connectors (Using Coordinates for safety since I know the layout)
    // Left -> Center
    nodes.push(createCoordinateConnector("conn_1", startX - 200, startY + 100, startX, startY + 100));
    // Center -> Right
    nodes.push(createCoordinateConnector("conn_2", startX + 200, startY + 100, startX + 400, startY + 100));
    // Center -> Bottom
    nodes.push(createCoordinateConnector("conn_3", startX + 100, startY + 200, startX + 100, startY + 400));

    try {
        await addNodes(boardId, nodes);
        console.log("Dashboard drawn successfully!");
    } catch (e) {
        console.error("Failed:", e.message);
    }
}

main();


path: create_wiki_board.js
content: const { apiRequest } = require('./api');

async function createWikiBoard(spaceId, parentToken, title) {
    try {
        console.log(`Creating Wiki Board in Space ${spaceId} under ${parentToken}...`);
        const res = await apiRequest('POST', `/open-apis/wiki/v2/spaces/${spaceId}/nodes`, {
            obj_type: "whiteboard",
            node_type: "origin",
            parent_node_token: parentToken,
            title: title
        });
        console.log("Success:", JSON.stringify(res, null, 2));
    } catch (e) {
        console.error("Create Failed:", e.message);
    }
}

if (require.main === module) {
    createWikiBoard("7586511345940450525", "M4lmwbQ5giQMHIkPveDcvT1JnJe", "SoulBadge Architecture Diagram");
}


path: debug_draw.js
content: const { addNodes } = require('./draw');

async function main() {
    const boardId = "N3UOwBzjNhgPyIbgH7vcWznSnRc"; 
    
    // Exact clone of a valid node from read_nodes.js output
    const debugNode = {
      "id": "debug_node_001",
      "type": "composite_shape",
      "composite_shape": {
        "type": "rect"
      },
      "x": 3000,
      "y": 7000,
      "width": 200,
      "height": 100,
      "angle": 0,
      "style": {
        "border_color": "#1890ff",
        "border_color_type": 1,
        "border_opacity": 100,
        "border_style": "solid",
        "border_width": "narrow",
        "fill_color": "#bae7ff",
        "fill_color_type": 1,
        "fill_opacity": 100
      },
      "text": {
        "text": "Debug Node",
        "font_size": 14,
        "font_weight": "regular",
        "horizontal_align": "center",
        "vertical_align": "mid",
        "text_color": "#1f2329",
        "text_color_type": 0
      }
    };

    try {
        await addNodes(boardId, [debugNode]);
        console.log("Debug node added successfully!");
    } catch (e) {
        console.error("Debug failed:", e.message);
    }
}

main();


path: debug_minimal.js
content: const { addNodes } = require('./draw');

// Minimalist Node (Stripped down to absolute basics)
function createMinimalNode(text, x, y) {
    return {
        "type": "composite_shape",
        "composite_shape": { "type": "rect" },
        "x": x,
        "y": y,
        "width": 100,
        "height": 50,
        // Omit style and text details if possible, or use defaults
        "text": { "text": text }
    };
}

async function main() {
    const boardId = "N3UOwBzjNhgPyIbgH7vcWznSnRc";
    const nodes = [ createMinimalNode("Minimal Test", 2500, 8500) ];
    try {
        await addNodes(boardId, nodes);
        console.log("Minimal Success");
    } catch (e) {
        console.error("Minimal Fail:", e.message);
    }
}
main();


path: debug_schema.js
content: const { apiRequest } = require('./api');

async function testAdd(boardId) {
    const node = {
        type: "composite_shape",
        composite_shape: { type: "rect" },
        x: 2000,
        y: 8000, // Safe distance
        width: 200,
        height: 100,
        style: {
            fill_color: "#ffffff",
            fill_color_type: 1, // Color type: 1 (Custom?)
            border_color: "#000000",
            border_color_type: 1,
            border_width: "regular"
        },
        text: {
            text: "Write Test",
            font_size: 14,
            horizontal_align: "center",
            vertical_align: "mid",
            text_color: "#000000",
            text_color_type: 0 // Color type: 0
        }
    };

    try {
        console.log(`Adding node to ${boardId}...`);
        const res = await apiRequest('POST', `/open-apis/board/v1/whiteboards/${boardId}/nodes`, {
            nodes: [node]
        });
        console.log("Success:", JSON.stringify(res));
    } catch (e) {
        console.error("Fail:", e.message);
    }
}

if (require.main === module) {
    testAdd(process.argv[2]);
}


path: draw.js
content: const { apiRequest } = require('./api');

async function addNodes(whiteboardId, nodes) {
    if (!whiteboardId) {
        throw new Error("Whiteboard ID is required");
    }
    
    try {
        console.log(`Adding ${nodes.length} nodes to board ${whiteboardId}...`);
        const result = await apiRequest('POST', `/open-apis/board/v1/whiteboards/${whiteboardId}/nodes`, {
            nodes: nodes
        });
        console.log("Nodes added successfully!");
        return result;
    } catch (error) {
        console.error("Failed to add nodes:", error.message);
        throw error;
    }
}

// Helper to create a shape node
function createShape(id, shapeType, x, y, width, height, text = "", color = null) {
    const node = {
        id: id,
        type: "shape",
        shape_type: shapeType, // rect, circle, ellipse, etc.
        x: x,
        y: y,
        width: width,
        height: height,
        style: {}
    };
    
    if (color) {
        node.style.fill_color = color;
    }
    
    if (text) {
        node.text = {
            content: text
        };
    }
    
    return node;
}

// Helper to create a connector
function createConnector(id, startId, endId) {
    return {
        id: id,
        type: "connector",
        start_object_id: startId,
        end_object_id: endId
    };
}

if (require.main === module) {
    const args = process.argv.slice(2);
    if (args.length < 1) {
        console.log("Usage: node draw.js <whiteboard_id> [demo]");
        process.exit(1);
    }

    const boardId = args[0];
    const mode = args[1];

    if (mode === 'demo') {
        const nodes = [
            createShape("node1", "rect", 100, 100, 150, 80, "Service A"),
            createShape("node2", "circle", 400, 100, 100, 100, "DB", "#ffcc00"),
            createConnector("conn1", "node1", "node2")
        ];
        addNodes(boardId, nodes).catch(() => process.exit(1));
    } else {
        console.log("Please provide 'demo' as second argument to run the demo.");
    }
}

module.exports = { addNodes, createShape, createConnector };


path: draw_new_board.js
content: const { addNodes } = require('./draw');

// Standard Node
function createNode(text, x, y, color) {
    return {
        "type": "composite_shape",
        "composite_shape": { "type": "round_rect" },
        "x": x,
        "y": y,
        "width": 160,
        "height": 80,
        "style": { "fill_color": color, "fill_opacity": 100 },
        "text": { "text": text, "font_size": 14, "horizontal_align": "center" }
    };
}

function createArrow(x1, y1, x2, y2) {
    return {
        "type": "connector",
        "connector": {
            "start": { "position": { "x": x1, "y": y1 } },
            "end": { "position": { "x": x2, "y": y2 }, "arrow_style": "triangle_arrow" }
        }
    };
}

async function main() {
    const boardId = "Qxqpwl88FhdtPtbitIjc015en7e"; // New Board ID
    
    // Start fresh at Origin
    const startX = 0; 
    const startY = 0; 
    const gap = 300;

    const nodes = [
        createNode("📱 App", startX, startY, "#bae7ff"),
        createNode("☁️ Cloud", startX + gap, startY, "#d9f7be"),
        createNode("📛 Badge", startX + gap * 2, startY, "#efdbff"),
        
        createArrow(startX + 160, startY + 40, startX + gap, startY + 40),
        createArrow(startX + gap + 160, startY + 40, startX + gap * 2, startY + 40)
    ];

    try {
        console.log(`Drawing on NEW Board ${boardId}...`);
        await addNodes(boardId, nodes);
        console.log("Success!");
    } catch (e) {
        console.error("Fail:", e.message);
    }
}

main();


path: draw_relocate.js
content: const { addNodes } = require('./draw');

// Minimal Node (Proven Working)
function createNode(text, x, y, color) {
    return {
        "type": "composite_shape",
        "composite_shape": { "type": "round_rect" },
        "x": x,
        "y": y,
        "width": 160,
        "height": 80,
        "style": { "fill_color": color, "fill_opacity": 100 },
        "text": { "text": text, "font_size": 14, "horizontal_align": "center" }
    };
}

function createArrow(x1, y1, x2, y2) {
    return {
        "type": "connector",
        "connector": {
            "start": { "position": { "x": x1, "y": y1 } },
            "end": { "position": { "x": x2, "y": y2 }, "arrow_style": "triangle_arrow" }
        }
    };
}

async function main() {
    const boardId = "N3UOwBzjNhgPyIbgH7vcWznSnRc";
    
    // Target Coordinates (Near the massive object o8:3 at 26720, 83320)
    // We will place it slightly to the right and top
    const startX = 26800; 
    const startY = 83400; 
    const gap = 300;

    const nodes = [
        createNode("📱 App", startX, startY, "#bae7ff"),
        createNode("☁️ Cloud", startX + gap, startY, "#d9f7be"),
        createNode("📛 Badge", startX + gap * 2, startY, "#efdbff"),
        
        createArrow(startX + 160, startY + 40, startX + gap, startY + 40),
        createArrow(startX + gap + 160, startY + 40, startX + gap * 2, startY + 40)
    ];

    try {
        console.log(`Relocating to Giant's Shoulder (${startX}, ${startY})...`);
        await addNodes(boardId, nodes);
        console.log("Success!");
    } catch (e) {
        console.error("Fail:", e.message);
    }
}

main();


path: draw_soulbadge.js
content: const { addNodes } = require('./draw');

// STRICTLY Minimal Schema (Proven Working)
function createNode(text, x, y, color) {
    return {
        "type": "composite_shape",
        "composite_shape": {
            "type": "round_rect"
        },
        "x": x,
        "y": y,
        "width": 160,
        "height": 80,
        "style": {
            "fill_color": color,
            "fill_opacity": 100
        },
        "text": {
            "text": text,
            "font_size": 14,
            "font_weight": "regular",
            "horizontal_align": "center",
            "vertical_align": "mid"
        }
    };
}

function createArrow(startX, startY, endX, endY) {
    return {
        "type": "connector",
        "connector": {
            "start": { "position": { "x": startX, "y": startY } },
            "end": { "position": { "x": endX, "y": endY }, "arrow_style": "triangle_arrow" }
        }
    };
}

async function main() {
    const boardId = "N3UOwBzjNhgPyIbgH7vcWznSnRc";
    
    // Absolute Origin (0,0)
    const startX = 0; 
    const startY = 0;
    const gap = 300;

    const nodes = [
        createNode("📱 App", startX, startY, "#bae7ff"),
        createNode("☁️ Cloud", startX + gap, startY, "#d9f7be"),
        createNode("📛 Badge", startX + gap * 2, startY, "#efdbff"),
        
        createArrow(startX + 160, startY + 40, startX + gap, startY + 40),
        createArrow(startX + gap + 160, startY + 40, startX + gap * 2, startY + 40)
    ];

    try {
        console.log(`Drawing at (0,0)...`);
        await addNodes(boardId, nodes);
        console.log("Success!");
    } catch (e) {
        console.error("Failed:", e.message);
    }
}

main();


path: draw_soulbadge_full.js
content: const { addNodes } = require('./draw');

function createNode(id, text, x, y, color) {
    return {
        "id": id,
        "type": "composite_shape",
        "composite_shape": { "type": "round_rect" },
        "x": x,
        "y": y,
        "width": 180,
        "height": 90,
        "style": { "fill_color": color, "fill_opacity": 100 },
        "text": { "text": text, "font_size": 14, "horizontal_align": "center" }
    };
}

function createArrow(startId, endId, text="") {
    return {
        "type": "connector",
        "connector": {
            "start": { "id": startId, "side": "right" },
            "end": { "id": endId, "side": "left" },
            "arrow_style": "triangle_arrow",
            "caption": { "content": text }
        }
    };
}

// Special downward arrow for parallel flows
function createDownArrow(startId, endId) {
    return {
        "type": "connector",
        "connector": {
            "start": { "id": startId, "side": "bottom" },
            "end": { "id": endId, "side": "top" },
            "arrow_style": "triangle_arrow"
        }
    };
}

async function main() {
    const boardId = "Qxqpwl88FhdtPtbitIjc015en7e";
    const nodes = [];
    
    // Colors
    const C_USER = "#ffffff"; // White
    const C_APP = "#bae7ff";  // Blue
    const C_CLOUD = "#d9f7be"; // Green
    const C_BADGE = "#efdbff"; // Purple

    // --- Phase 1: Activation ---
    // (0,0)
    nodes.push(createNode("n1", "📦 用户: 撕开激活码\n下载 App", 0, 0, C_USER));
    nodes.push(createNode("n2", "📱 App: 输入激活码\n认证成功", 300, 0, C_APP));
    
    // --- Phase 2: Creation ---
    nodes.push(createNode("n3", "🎨 User: 创建角色\n(捏脸/设定)", 600, 0, C_USER));
    nodes.push(createNode("n4", "☁️ Cloud: 工作流生成\n(图/视/音)", 900, 0, C_CLOUD));
    
    // --- Phase 3: Binding (Hardware Branch) ---
    // Start a new row for Hardware parallel actions
    nodes.push(createNode("n5", "📛 Badge: 开机 & 配网\n(连接WiFi)", 300, 300, C_BADGE));
    nodes.push(createNode("n6", "📛 Badge: 生成信任码\n(Trust Code)", 600, 300, C_BADGE));
    
    // Back to App Flow
    nodes.push(createNode("n7", "📱 App: 输入信任码\n(需同WiFi)", 900, 300, C_APP));
    
    // --- Phase 4: Injection ---
    nodes.push(createNode("n8", "📤 App: 上传“灵魂包”\n(Inject Soul)", 1200, 150, C_APP)); // Merge point
    nodes.push(createNode("n9", "🔄 Badge: 转圈圈\n(接收/加载)", 1500, 150, C_BADGE));
    
    // --- Phase 5: Live ---
    nodes.push(createNode("n10", "✨ Badge: 待机展示\n(周期性动作)", 1800, 150, C_BADGE));
    nodes.push(createNode("n11", "👆 User: 触屏互动\n(激活反馈)", 2100, 150, C_USER));

    // --- Connectors ---
    // Flow 1
    nodes.push(createArrow("n1", "n2"));
    nodes.push(createArrow("n2", "n3"));
    nodes.push(createArrow("n3", "n4"));
    
    // Flow 2 (Badge Setup)
    nodes.push(createArrow("n5", "n6"));
    nodes.push(createArrow("n6", "n7", "显示码"));
    
    // Merge
    nodes.push(createArrow("n4", "n8", "资源就绪"));
    nodes.push(createArrow("n7", "n8", "绑定成功"));
    
    // Final Flow
    nodes.push(createArrow("n8", "n9"));
    nodes.push(createArrow("n9", "n10"));
    nodes.push(createArrow("n10", "n11"));

    try {
        console.log("Drawing Full User Story...");
        await addNodes(boardId, nodes);
        console.log("Success!");
    } catch (e) {
        console.error("Fail:", e.message);
    }
}

main();


path: draw_soulbadge_nodes_only.js
content: const { addNodes } = require('./draw');

// Minimal Node
function createNode(text, x, y) {
    return {
        "type": "composite_shape",
        "composite_shape": { "type": "round_rect" },
        "x": x,
        "y": y,
        "width": 180,
        "height": 90,
        "style": { "fill_color": "#ffffff", "fill_opacity": 100 }, // Default white
        "text": { "text": text }
    };
}

async function main() {
    const boardId = "Qxqpwl88FhdtPtbitIjc015en7e";
    const nodes = [];
    
    // Just draw the nodes first without connectors to isolate the error
    // Phase 1
    nodes.push(createNode("n1: Unbox", 0, 0));
    nodes.push(createNode("n2: App Auth", 300, 0));
    nodes.push(createNode("n3: Create Role", 600, 0));
    nodes.push(createNode("n4: Cloud Gen", 900, 0));
    
    // Phase 3
    nodes.push(createNode("n5: Badge WiFi", 300, 300));
    nodes.push(createNode("n6: Trust Code", 600, 300));
    nodes.push(createNode("n7: App Bind", 900, 300));
    
    // Phase 4
    nodes.push(createNode("n8: Upload", 1200, 150));
    nodes.push(createNode("n9: Loading", 1500, 150));
    nodes.push(createNode("n10: Idle", 1800, 150));
    nodes.push(createNode("n11: Touch", 2100, 150));

    try {
        console.log("Drawing Nodes Only...");
        await addNodes(boardId, nodes);
        console.log("Success!");
    } catch (e) {
        console.error("Fail:", e.message);
    }
}

main();


path: draw_soulbadge_safe.js
content: const { addNodes } = require('./draw');

// Fallback to Coordinate-based Arrows if ID linking fails (Code 2890002 often means invalid connection)
function createNode(id, text, x, y, color) {
    return {
        "id": id,
        "type": "composite_shape",
        "composite_shape": { "type": "round_rect" },
        "x": x,
        "y": y,
        "width": 180,
        "height": 90,
        "style": { "fill_color": color, "fill_opacity": 100 },
        "text": { "text": text, "font_size": 14, "horizontal_align": "center" }
    };
}

function createCoordArrow(x1, y1, x2, y2) {
    return {
        "type": "connector",
        "connector": {
            "start": { "position": { "x": x1, "y": y1 } },
            "end": { "position": { "x": x2, "y": y2 }, "arrow_style": "triangle_arrow" },
            "shape": "elbow"
        }
    };
}

async function main() {
    const boardId = "Qxqpwl88FhdtPtbitIjc015en7e";
    const nodes = [];
    
    // Colors
    const C_USER = "#ffffff"; 
    const C_APP = "#bae7ff";  
    const C_CLOUD = "#d9f7be"; 
    const C_BADGE = "#efdbff"; 

    // Define positions
    // Node center is (x + width/2, y + height/2)
    // w=180, h=90. Center offset (+90, +45).
    
    const p1 = {x: 0, y: 0};
    const p2 = {x: 300, y: 0};
    const p3 = {x: 600, y: 0};
    const p4 = {x: 900, y: 0};
    
    const p5 = {x: 300, y: 300};
    const p6 = {x: 600, y: 300};
    const p7 = {x: 900, y: 300};
    
    const p8 = {x: 1200, y: 150};
    const p9 = {x: 1500, y: 150};
    const p10 = {x: 1800, y: 150};
    const p11 = {x: 2100, y: 150};

    // Nodes
    nodes.push(createNode("n1", "📦 用户: 撕开激活码\n下载 App", p1.x, p1.y, C_USER));
    nodes.push(createNode("n2", "📱 App: 输入激活码\n认证成功", p2.x, p2.y, C_APP));
    nodes.push(createNode("n3", "🎨 User: 创建角色\n(捏脸/设定)", p3.x, p3.y, C_USER));
    nodes.push(createNode("n4", "☁️ Cloud: 工作流生成\n(图/视/音)", p4.x, p4.y, C_CLOUD));
    
    nodes.push(createNode("n5", "📛 Badge: 开机 & 配网\n(连接WiFi)", p5.x, p5.y, C_BADGE));
    nodes.push(createNode("n6", "📛 Badge: 生成信任码\n(Trust Code)", p6.x, p6.y, C_BADGE));
    nodes.push(createNode("n7", "📱 App: 输入信任码\n(需同WiFi)", p7.x, p7.y, C_APP));
    
    nodes.push(createNode("n8", "📤 App: 上传“灵魂包”\n(Inject Soul)", p8.x, p8.y, C_APP));
    nodes.push(createNode("n9", "🔄 Badge: 转圈圈\n(接收/加载)", p9.x, p9.y, C_BADGE));
    nodes.push(createNode("n10", "✨ Badge: 待机展示\n(周期性动作)", p10.x, p10.y, C_BADGE));
    nodes.push(createNode("n11", "👆 User: 触屏互动\n(激活反馈)", p11.x, p11.y, C_USER));

    // Connectors (Center-to-Center approximation or Edge-to-Edge)
    // Left-Right: (x + 180, y + 45) -> (next_x, next_y + 45)
    
    const h = 45; // half height
    const w = 180; // width
    
    nodes.push(createCoordArrow(p1.x + w, p1.y + h, p2.x, p2.y + h));
    nodes.push(createCoordArrow(p2.x + w, p2.y + h, p3.x, p3.y + h));
    nodes.push(createCoordArrow(p3.x + w, p3.y + h, p4.x, p4.y + h));
    
    nodes.push(createCoordArrow(p5.x + w, p5.y + h, p6.x, p6.y + h));
    nodes.push(createCoordArrow(p6.x + w, p6.y + h, p7.x, p7.y + h));
    
    // Merge: n4 -> n8 (Diagonal down)
    nodes.push(createCoordArrow(p4.x + w, p4.y + h, p8.x, p8.y + h));
    // Merge: n7 -> n8 (Diagonal up)
    nodes.push(createCoordArrow(p7.x + w, p7.y + h, p8.x, p8.y + h));
    
    nodes.push(createCoordArrow(p8.x + w, p8.y + h, p9.x, p9.y + h));
    nodes.push(createCoordArrow(p9.x + w, p9.y + h, p10.x, p10.y + h));
    nodes.push(createCoordArrow(p10.x + w, p10.y + h, p11.x, p11.y + h));

    try {
        console.log("Drawing Full User Story (Safe Mode)...");
        await addNodes(boardId, nodes);
        console.log("Success!");
    } catch (e) {
        console.error("Fail:", e.message);
    }
}

main();


path: draw_soulbadge_v2.js
content: const { addNodes } = require('./draw');

// Rebuilding functionality layer by layer
function createWorkingNode(text, x, y, color) {
    return {
        "type": "composite_shape",
        "composite_shape": { "type": "round_rect" },
        "x": x,
        "y": y,
        "width": 160,
        "height": 80,
        // Style seems to be the breaker. Let's add it back carefully.
        "style": {
            "fill_color": color,
            "fill_opacity": 100 // Required if color is set
            // Omit border_color_type etc. for now, let defaults handle it
        },
        "text": { 
            "text": text,
            "font_size": 14,
            "horizontal_align": "center" 
        }
    };
}

function createWorkingArrow(x1, y1, x2, y2) {
    return {
        "type": "connector",
        "connector": {
            "start": { "position": { "x": x1, "y": y1 } },
            "end": { "position": { "x": x2, "y": y2 } }
        }
    };
}

async function main() {
    const boardId = "N3UOwBzjNhgPyIbgH7vcWznSnRc";
    const startX = 2000;
    const startY = 8500; // New row
    const gap = 250;

    const nodes = [
        createWorkingNode("📱 App", startX, startY, "#bae7ff"),
        createWorkingNode("☁️ Cloud", startX + gap, startY, "#d9f7be"),
        createWorkingNode("📛 Badge", startX + gap * 2, startY, "#efdbff"),
        
        createWorkingArrow(startX + 160, startY + 40, startX + gap, startY + 40),
        createWorkingArrow(startX + gap + 160, startY + 40, startX + gap * 2, startY + 40)
    ];

    try {
        console.log("Attempting V2 Draw...");
        await addNodes(boardId, nodes);
        console.log("V2 Success!");
    } catch (e) {
        console.error("V2 Fail:", e.message);
    }
}

main();


path: draw_visible.js
content: const { addNodes } = require('./draw');

function createWorkingNode(text, x, y, color) {
    return {
        "type": "composite_shape",
        "composite_shape": { "type": "round_rect" },
        "x": x,
        "y": y,
        "width": 140,
        "height": 80,
        "style": {
            "fill_color": color,
            "fill_opacity": 100
        },
        "text": { 
            "text": text,
            "font_size": 14,
            "horizontal_align": "center" 
        }
    };
}

function createWorkingArrow(x1, y1, x2, y2) {
    return {
        "type": "connector",
        "connector": {
            "start": { "position": { "x": x1, "y": y1 } },
            "end": { "position": { "x": x2, "y": y2 }, "arrow_style": "triangle_arrow" }
        }
    };
}

async function main() {
    const boardId = "N3UOwBzjNhgPyIbgH7vcWznSnRc";
    
    // Target: Directly below the existing diagram (y ~ 7300-7500)
    const startX = 1250; 
    const startY = 7800; // Visible area
    const gap = 200;

    const nodes = [
        createWorkingNode("📱 App", startX, startY, "#bae7ff"),
        createWorkingNode("☁️ Cloud", startX + gap, startY, "#d9f7be"),
        createWorkingNode("📛 Badge", startX + gap * 2, startY, "#efdbff"),
        
        createWorkingArrow(startX + 140, startY + 40, startX + gap, startY + 40),
        createWorkingArrow(startX + gap + 140, startY + 40, startX + gap * 2, startY + 40)
    ];

    try {
        console.log("Drawing in Visible Area (y=7800)...");
        await addNodes(boardId, nodes);
        console.log("Success!");
    } catch (e) {
        console.error("Fail:", e.message);
    }
}

main();


path: get_node_by_obj.js
content: const { apiRequest } = require('./api');

async function getNodeByObj(token) {
    try {
        console.log(`Getting Wiki Node for Doc ${token}...`);
        const res = await apiRequest('GET', `/open-apis/wiki/v2/spaces/get_node?obj_type=docx&obj_token=${token}`);
        console.log(JSON.stringify(res, null, 2));
    } catch (e) {
        console.error("Error:", e.message);
    }
}

if (require.main === module) {
    getNodeByObj(process.argv[2]);
}


path: get_wiki.js
content: const { apiRequest } = require('./api');

async function getWikiNode(token) {
    try {
        console.log(`Fetching Wiki Node ${token}...`);
        // /open-apis/wiki/v2/nodes/:token
        const res = await apiRequest('GET', `/open-apis/wiki/v2/nodes/${token}`);
        console.log(JSON.stringify(res, null, 2));
    } catch (e) {
        console.error("Error:", e.message);
    }
}

if (require.main === module) {
    getWikiNode(process.argv[2]);
}


path: package-lock.json
content: {
  "name": "feishu-whiteboard",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "feishu-whiteboard",
      "version": "1.0.0",
      "dependencies": {
        "dotenv": "^16.0.0"
      }
    },
    "node_modules/dotenv": {
      "version": "16.6.1",
      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://dotenvx.com"
      }
    }
  }
}


path: package.json
content: {
  "name": "feishu-whiteboard",
  "version": "1.0.0",
  "description": "Feishu Whiteboard Skill",
  "main": "create.js",
  "dependencies": {
    "dotenv": "^16.0.0"
  }
}


path: probe_wiki.js
content: const { apiRequest } = require('./api');

async function probeWiki(token) {
    try {
        console.log(`Probing Wiki Token ${token}...`);
        // Try GET node directly
        // Note: Real endpoint is GET /open-apis/wiki/v2/spaces/get_node with query params
        const res = await apiRequest('GET', `/open-apis/wiki/v2/spaces/get_node?[REDACTED_SECRET]`);
        console.log("Result:", JSON.stringify(res, null, 2));
    } catch (e) {
        console.error("Probe Failed:", e.message);
    }
}

if (require.main === module) {
    probeWiki(process.argv[2]);
}


path: read_nodes.js
content: const { apiRequest } = require('./api');

async function listNodes(whiteboardId) {
    if (!whiteboardId) {
        throw new Error("Whiteboard ID is required");
    }
    
    try {
        console.log(`Listing nodes from board ${whiteboardId}...`);
        const result = await apiRequest('GET', `/open-apis/board/v1/whiteboards/${whiteboardId}/nodes`);
        console.log("Nodes fetched successfully!");
        return result;
    } catch (error) {
        console.error("Failed to list nodes:", error.message);
        throw error;
    }
}

if (require.main === module) {
    const args = process.argv.slice(2);
    if (args.length < 1) {
        console.log("Usage: node read_nodes.js <whiteboard_id>");
        process.exit(1);
    }

    const boardId = args[0];
    listNodes(boardId)
        .then(data => console.log(JSON.stringify(data, null, 2)))
        .catch(() => process.exit(1));
}

module.exports = { listNodes };


path: relocate.js
content: const { apiRequest } = require('./api');

async function updateNode(boardId, nodeId, x, y) {
    try {
        console.log(`Moving node ${nodeId} to (${x}, ${y})...`);
        const res = await apiRequest('PUT', `/open-apis/board/v1/whiteboards/${boardId}/nodes/${nodeId}`, {
            node: {
                x: x,
                y: y
            }
        });
        console.log(`Moved ${nodeId}: Success`);
    } catch (e) {
        console.error(`Failed to move ${nodeId}:`, e.message);
    }
}

async function main() {
    const boardId = "N3UOwBzjNhgPyIbgH7vcWznSnRc";
    
    // IDs from the previous read output
    // App: o2:40 (2000, 8500) -> Move to (1250, 7800)
    // Cloud: o2:41 (2250, 8500) -> Move to (1550, 7800)
    // Badge: o2:42 (2500, 8500) -> Move to (1850, 7800)
    // Arrows need to be deleted/redrawn or moved. Connectors usually auto-update if linked by ID, but mine were coord-based.
    // If they were coord-based, moving the shapes won't move the arrows' endpoints if they aren't bound.
    // My previous script used coordinate connectors. So I need to delete the old arrows and draw new ones, or just leave them as debris for now and focus on showing the nodes.
    
    // Let's just move the shapes first so he can SEE them.
    
    await updateNode(boardId, "o2:40", 1250, 7800);
    await updateNode(boardId, "o2:41", 1550, 7800);
    await updateNode(boardId, "o2:42", 1850, 7800);
}

main();


path: test.js
content: const { createWhiteboard } = require('./create');
const { addNodes, createShape, createConnector } = require('./draw');

async function runTest() {
    try {
        console.log("=== Starting Feishu Whiteboard Test ===");
        
        // 1. Create Board
        const boardData = await createWhiteboard(`Test Board ${new Date().toLocaleTimeString()}`);
        if (!boardData || !boardData.whiteboard_id) {
            throw new Error("Failed to get whiteboard_id from creation response");
        }
        const boardId = boardData.whiteboard_id;
        console.log(`Board Created: ${boardId}`);

        // 2. Prepare Nodes
        // Note: Using random IDs to ensure uniqueness if needed, though simple strings usually work per board
        const nodes = [
            createShape("shape_1", "rect", 100, 100, 200, 100, "Frontend", "#e6f7ff"),
            createShape("shape_2", "rect", 500, 100, 200, 100, "Backend", "#fff7e6"),
            createConnector("conn_1", "shape_1", "shape_2")
        ];

        // 3. Add Nodes
        await addNodes(boardId, nodes);
        console.log("=== Test Complete: Success ===");
        console.log(`URL: https://open.feishu.cn/board/${boardId}`); // Guessing URL format
        
    } catch (e) {
        console.error("Test Failed:", e);
        process.exit(1);
    }
}

runTest();


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