Skill: autogame-17/feishu-post (version 1.1.7)

# Feishu Post (RichText) Skill Send Rich Text (Post) messages to Feishu. This format is distinct from Cards. It supports native rich text elements but is less flexible in layout than cards. It is better for long-form text mixed with images/links. ## Prerequisites - Install `feishu-common` first. - This skill depends on `../feishu-common/index.js` via `utils/feishu-client.js`. ## Features - **Native Emoji Support**: Automatically converts `[微笑]`, `[得意]` etc. to Feishu native emoji tags. - **Markdown-like Parsing**: Supports simple newlines and paragraphs. - **Rich Text**: Uses Feishu's Post content structure. ## Usage ```bash node skills/feishu-post/send.js --target "ou_..." --text-file "temp/msg.md" --title "Optional Title" ``` ## Options - `-t, --target <id>`: Target ID (user `ou_...` or chat `oc_...`). - `-x, --text <text>`: Text content (supports `\n` for newlines and `[emoji]` tags). - `-f, --text-file <path>`: Read content from file. - `--title <text>`: Title of the post. - `--reply-to <id>`: Message ID to reply to. ## Emoji List Supported emojis include: `[微笑]`, `[色]`, `[亲亲]`, `[大哭]`, `[强]`, `[加油]`, and many more. See `emoji-map.js` for the full mapping.

path: README.md
content: # Feishu Post (RichText) Skill

Send Rich Text (Post) messages to Feishu.
This format is distinct from Cards. It supports native rich text elements but is less flexible in layout than cards. It is better for long-form text mixed with images/links.

## Usage

```bash
node skills/feishu-post/send.js --target "ou_..." --text-file "temp/msg.md" --title "Optional Title"
```

## Options
- `-t, --target <id>`: Target ID (user/chat).
- `-f, --text-file <path>`: Markdown content file.
- `--title <text>`: Title of the post.


path: debug_msg.js
content: const { fetchWithAuth } = require('./utils/feishu-client.js');

async function run() {
    const msgId = 'om_x100b5747191850a0b2c501a826f753e';
    console.log('Fetching msg:', msgId);
    try {
        const res = await fetchWithAuth(`https://open.feishu.cn/open-apis/im/v1/messages/${msgId}`, { method: 'GET' });
        const data = await res.json();
        console.log('Current Msg:', JSON.stringify(data, null, 2));
        
        if (data.data && data.data.items && data.data.items[0].parent_id) {
            const parentId = data.data.items[0].parent_id;
            console.log('Fetching Parent:', parentId);
            const res2 = await fetchWithAuth(`https://open.feishu.cn/open-apis/im/v1/messages/${parentId}`, { method: 'GET' });
            const data2 = await res2.json();
            console.log('Parent Msg:', JSON.stringify(data2, null, 2));
        }
    } catch(e) {
        console.error(e);
    }
}
run();


path: emoji-map.js
content: module.exports = {
  // "[色]": "DROOL", // Not in official list
  "[亲亲]": "KISS",
  // "[憨笑]": "LAUGH", // Not in official list
  "[大哭]": "CRY",
  "[鼓掌]": "CLAP",
  "[强]": "THUMBSUP",
  "[害羞]": "SHY",
  "[闭嘴]": "SHUTUP",
  // "[睡]": "SLEEP", // Not in official list
  // "[拥抱]": "HUG", // Not in official list
  "[得意]": "PROUD",
  "[发怒]": "ANGRY",
  // "[惊讶]": "SURPRISED", // Not in official list
  "[委屈]": "WRONGED",
  "[吐]": "VOMIT",
  "[偷笑]": "CHUCKLE",
  // "[可爱]": "CUTE", // Not in official list
  "[白眼]": "ROLL_EYES",
  "[嘘]": "HUSH",
  "[困]": "DROWSY",
  "[惊恐]": "FEAR",
  // "[流汗]": "SWEAT", // Not in official list
  "[大笑]": "BIG_SMILE",
  "[奋斗]": "STRIVE",
  // "[骂人]": "CURSE", // Not in official list
  // "[疑问]": "DOUBT", // Not in official list
  "[晕]": "DIZZY",
  // "[折磨]": "TORMENT", // Not in official list
  "[衰]": "DECAY",
  "[骷髅]": "SKULL",
  "[敲打]": "HAMMER",
  "[再见]": "BYE",
  // "[擦汗]": "WIPE", // Not in official list
  "[抠鼻]": "NOSE_PICK",
  // "[糗大了]": "EMBARRASSED", // Not in official list
  "[坏笑]": "SMIRK",
  // "[左哼哼]": "LEFT_HUM", // Not in official list
  // "[右哼哼]": "RIGHT_HUM", // Not in official list
  "[哈欠]": "YAWN",
  "[鄙视]": "DESPISE",
  "[快哭了]": "TEARS",
  // "[阴险]": "SLY", // Not in official list
  // "[吓]": "SCARE", // Not in official list
  "[可怜]": "PITIFUL",
  // "[菜刀]": "KNIFE", // Not in official list
  // "[西瓜]": "WATERMELON", // Not in official list
  "[啤酒]": "BEER",
  "[篮球]": "BASKETBALL",
  // "[乒乓]": "PINGPONG", // Not in official list
  "[咖啡]": "COFFEE",
  // "[饭]": "RICE", // Not in official list
  // "[猪头]": "PIG", // Not in official list
  "[玫瑰]": "ROSE",
  // "[凋谢]": "WILT", // Not in official list
  // "[示爱]": "LIPS", // Not in official list
  "[爱心]": "HEART",
  "[心碎]": "BROKEN_HEART",
  "[蛋糕]": "CAKE",
  // "[闪电]": "LIGHTNING", // Not in official list
  "[炸弹]": "BOMB",
  "[便便]": "SHIT",
  // "[月亮]": "MOON", // Not in official list
  // "[太阳]": "SUN", // Not in official list
  "[礼物]": "GIFT",
  // "[弱]": "THUMBSDOWN", // Not in official list
  "[握手]": "SHAKE",
  // "[胜利]": "VICTORY", // Not in official list
  "[抱拳]": "SALUTE",
  // "[勾引]": "BECKON", // Not in official list
  // "[拳头]": "FIST", // Not in official list
  // "[差劲]": "POOR", // Not in official list
  // "[爱你]": "LOVE_YOU", // Not in official list
  // "[NO]": "NO", // Not in official list
  "[OK]": "OK",
  // "[Love]": "LOVE", // Not in official list
  "[飞吻]": "BLOW_KISS",
  // "[跳跳]": "JUMP", // Not in official list
  // "[发抖]": "TREMBLE", // Not in official list
  // "[怄火]": "ANNOYED", // Not in official list
  // "[转圈]": "TWIRL", // Not in official list
  // "[磕头]": "KOWTOW", // Not in official list
  // "[回头]": "LOOK_BACK", // Not in official list
  // "[跳绳]": "SKIP", // Not in official list
  "[挥手]": "WAVE",
  // "[激动]": "EXCITED", // Not in official list
  // "[街舞]": "HIPHOP", // Not in official list
  // "[献吻]": "KISS_FLY", // Not in official list
  // "[左太极]": "LEFT_TAIJI", // Not in official list
  // "[右太极]": "RIGHT_TAIJI", // Not in official list
  "[加油]": "JIAYOU",
  "[机智]": "SMART",
  "[耶]": "YEAH",
  "[思考]": "THINKING",
  "[我想静静]": "SILENT",
  "[捂脸]": "FACEPALM",
  "[笑哭]": "JOY",
  "[无语]": "SPEECHLESS",
  "[我看行]": "LOOKS_GOOD",
  "[+1]": "PLUS_ONE",
  "[撒花]": "CELEBRATE",
  "[什么？]": "WHAT",
  "[黑线]": "BLACK_LINE",
  "[灵光一闪]": "IDEA",
  "[震惊]": "SHOCKED",
  "[石化]": "PETRIFIED",
  "[听歌]": "LISTEN_MUSIC",
  "[摸头]": "PAT_HEAD",
  "[皱眉]": "FROWN",
  "[送心]": "SEND_HEART",
};


path: index.js
content: const { sendPost } = require('../feishu-common/index.js');

async function main() {
    const args = require('minimist')(process.argv.slice(2));
    
    // Usage: node skills/feishu-post/index.js --title "Title" --text "Line1\nLine2" --target "open_id"
    // Rich Text Format: 
    // Supports markdown-like input in 'text' argument? 
    // Or simple line breaks.
    // For now, let's keep it simple: Text -> Paragraphs
    
    const title = args.title || 'Notification';
    const text = args.text || '';
    const target = args.target;

    if (!target) {
        console.error('Error: --target is required');
        process.exit(1);
    }

    // Convert newlines to multiple paragraph elements
    const lines = text.split('\\n');
    const content = [
        lines.map(line => ({
            tag: 'text',
            text: line
        }))
    ];

    const postContent = {
        zh_cn: {
            title: title,
            content: content
        }
    };

    try {
        const result = await sendPost(target, postContent);
        console.log(JSON.stringify(result, null, 2));
    } catch (error) {
        console.error('Failed to send post:', error.message);
        process.exit(1);
    }
}

if (require.main === module) {
    main();
}


path: package-lock.json
content: {
  "name": "feishu-post",
  "version": "1.1.6",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "feishu-post",
      "version": "1.1.6",
      "dependencies": {
        "@larksuiteoapi/node-sdk": "^1.58.0",
        "commander": "^14.0.3",
        "dotenv": "^17.2.3"
      }
    },
    "node_modules/@larksuiteoapi/node-sdk": {
      "version": "1.58.0",
      "resolved": "https://registry.npmjs.org/@larksuiteoapi/node-sdk/-/node-sdk-1.58.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "axios": "~1.13.3",
        "lodash.identity": "^3.0.0",
        "lodash.merge": "^4.6.2",
        "lodash.pickby": "^4.6.0",
        "protobufjs": "^7.2.6",
        "qs": "^6.13.0",
        "ws": "^8.16.0"
      }
    },
    "node_modules/@protobufjs/aspromise": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/@protobufjs/base64": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/@protobufjs/codegen": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/@protobufjs/eventemitter": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/@protobufjs/fetch": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "@protobufjs/aspromise": "^1.1.1",
        "@protobufjs/inquire": "^1.1.0"
      }
    },
    "node_modules/@protobufjs/float": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/@protobufjs/inquire": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/@protobufjs/path": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/@protobufjs/pool": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/@protobufjs/utf8": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause"
    },
    "node_modules/@types/node": {
      "version": "25.2.0",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "undici-types": "~7.16.0"
      }
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/axios": {
      "version": "1.13.4",
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "follow-redirects": "^1.15.6",
        "form-data": "^4.0.4",
        "proxy-from-env": "^1.1.0"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/commander": {
      "version": "14.0.3",
      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=20"
      }
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/dotenv": {
      "version": "17.2.3",
      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://dotenvx.com"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/follow-redirects": {
      "version": "1.15.11",
      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/RubenVerborgh"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=4.0"
      },
      "peerDependenciesMeta": {
        "debug": {
          "optional": true
        }
      }
    },
    "node_modules/form-data": {
      "version": "4.0.5",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.2",
        "mime-types": "^2.1.12"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/lodash.identity": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.merge": {
      "version": "4.6.2",
      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.pickby": {
      "version": "4.6.0",
      "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/long": {
      "version": "5.3.2",
      "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0"
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/protobufjs": {
      "version": "7.5.4",
      "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
      "integrity": [REDACTED_SECRET],
      "hasInstallScript": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "@protobufjs/aspromise": "^1.1.2",
        "@protobufjs/base64": "^1.1.2",
        "@protobufjs/codegen": "^2.0.4",
        "@protobufjs/eventemitter": "^1.1.0",
        "@protobufjs/fetch": "^1.1.0",
        "@protobufjs/float": "^1.0.2",
        "@protobufjs/inquire": "^1.1.0",
        "@protobufjs/path": "^1.1.2",
        "@protobufjs/pool": "^1.1.0",
        "@protobufjs/utf8": "^1.1.0",
        "@types/node": ">=13.7.0",
        "long": "^5.0.0"
      },
      "engines": {
        "node": ">=12.0.0"
      }
    },
    "node_modules/proxy-from-env": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/qs": {
      "version": "6.14.1",
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.1.0"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3",
        "side-channel-list": "^1.0.0",
        "side-channel-map": "^1.0.1",
        "side-channel-weakmap": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3",
        "side-channel-map": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/undici-types": {
      "version": "7.16.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/ws": {
      "version": "8.19.0",
      "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=10.0.0"
      },
      "peerDependencies": {
        "bufferutil": "^4.0.1",
        "utf-8-validate": ">=5.0.2"
      },
      "peerDependenciesMeta": {
        "bufferutil": {
          "optional": true
        },
        "utf-8-validate": {
          "optional": true
        }
      }
    }
  }
}


path: package.json
content: {
  "name": "feishu-post",
  "version": "1.1.6",
  "description": "Send rich text messages (Post/RichText) to Feishu users/groups. Supports markdown conversion.",
  "main": "send.js",
  "dependencies": {
    "@larksuiteoapi/node-sdk": "^1.58.0",
    "commander": "^14.0.3",
    "dotenv": "^17.2.3"
  }
}


path: send.js
content: #!/usr/bin/env node
const { fetchWithAuth } = require('./utils/feishu-client.js');
const { parseMarkdownToFeishu } = require('./utils/markdown-parser.js');
const fs = require('fs');
const path = require('path');

// --- Upstream Logic Injection (Simplified) ---
// Ported from upstream src/send.ts (m1heng/clawdbot-feishu)
// Adapted for our lightweight architecture

async function sendPost(options) {
    // Normalize common mis-usage:
    // - Feishu message_id usually starts with "om_". If user passes it as --target,
    //   interpret it as --reply-to to avoid sending an invalid open_id.
    if (options && typeof options.target === 'string' && options.target.startsWith('om_') && !options.replyTo) {
        options.replyTo = options.target;
        delete options.target;
    }

    if (options.content && !options.text) {
        options.text = options.content;
    }
    if (options.markdown && !options.text) {
        options.text = options.markdown;
    }

    let contentText = options.text || '';
    if (options.textFile) {
        try {
            if (!fs.existsSync(options.textFile)) {
                throw new Error(`File not found: ${options.textFile}`);
            }
            contentText = fs.readFileSync(options.textFile, 'utf8');
        } catch (e) {
            throw new Error(`Failed to read message file: ${e.message}`);
        }
    }

    if (!contentText && !options.content) {
        throw new Error('No content provided (use --text or --text-file)');
    }

    // Validate target/replyTo requirements
    const hasReplyTo = !!options.replyTo;
    const hasTarget = typeof options.target === 'string' && options.target.length > 0;
    if (!hasReplyTo && !hasTarget) {
        throw new Error('Missing target (use --target "ou_..." or "oc_..." or provide --reply-to "om_...")');
    }

    // Determine ID Type
    let receiveIdType = 'open_id';
    if (hasTarget) {
        if (options.target.startsWith('oc_')) receiveIdType = 'chat_id';
        else if (options.target.startsWith('ou_')) receiveIdType = 'open_id';
        else if (options.target.includes('@')) receiveIdType = 'email';
        else if (options.target.startsWith('om_')) {
            // Should have been normalized earlier; keep as explicit guard.
            throw new Error('Invalid target "om_...": message_id cannot be used as receive_id. Use --reply-to "om_..."');
        } else {
            throw new Error('Invalid target id. Expected "ou_..." (open_id), "oc_..." (chat_id), or an email address.');
        }
    }

    // Build Payload (RichText)
    // Upstream Logic: Wrap markdown in Post Object
    
    // Split text by newlines to create paragraphs
    // Unescape literal \n if passed from command line
    const rawLines = contentText.replace(/\\n/g, '\n').split(/\r?\n/);
    const contentBody = rawLines.map(line => parseMarkdownToFeishu(line));

    const postContent = {
        zh_cn: {
            title: options.title || '',
            content: contentBody
        }
    };

    const messageBody = {
        receive_id: options.target,
        msg_type: 'post',
        content: JSON.stringify(postContent)
    };

    // Support Reply (New Feature from Upstream)
    let url = `https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`;
    if (options.replyTo) {
        url = `https://open.feishu.cn/open-apis/im/v1/messages/${options.replyTo}/reply`;
        delete messageBody.receive_id; // Reply doesn't need receive_id
    }

    // console.log(`Sending Post to ${options.target}...`);
    
    try {
        const res = await fetchWithAuth(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(messageBody)
        });
        
        const text = await res.text();
        let data;
        
        try {
            data = JSON.parse(text);
        } catch (e) {
             if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}: ${text}`);
             throw new Error(`Invalid JSON response: ${text.slice(0, 200)}...`);
        }
        
        if (!res.ok) {
            // Check if it's a recoverable error (like invalid emoji)
            const isEmojiError = data.code === 230001 && data.msg && data.msg.includes("emoji_type is invalid");
            if (!isEmojiError) {
                 throw new Error(`HTTP ${res.status} ${res.statusText}: ${JSON.stringify(data)}`);
            }
            // If it is an emoji error, flow through to the fallback logic below
        }
        
        if (data.code !== 0) {
            // Hotfix: If invalid emoji detected (230001), fallback to plain text stripping emojis
            if (data.code === 230001 && data.msg && data.msg.includes("emoji_type is invalid")) {
                 console.warn("Detected invalid emoji in Feishu post. Retrying as plain text fallback...");
                 // Fallback strategy: Send as plain text message, which renders emoji codes as text [Code]
                 // This ensures the message gets through even if the specific emoji is not supported.
                 const fallbackBody = {
                     receive_id: messageBody.receive_id,
                     msg_type: 'text',
                     content: JSON.stringify({ text: contentText })
                 };
                 
                 // Reuse URL (it might be a reply URL)
                 if (options.replyTo) {
                      delete fallbackBody.receive_id;
                 }
                 
                 const res2 = await fetchWithAuth(url, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(fallbackBody)
                 });
                 
                 const text2 = await res2.text();
                 const data2 = JSON.parse(text2);
                 if (data2.code === 0) return data2.data;
                 // If fallback fails, throw original error (or new one)
                 throw new Error(`API Error ${data.code}: ${data.msg} (Fallback also failed: ${data2.code} ${data2.msg})`);
            }
            throw new Error(`API Error ${data.code}: ${data.msg}`);
        }
        
        // ... (rest of function)

    } catch (e) {
        console.error(`Send Failed: ${e.message}`);
        throw e;
    }
}

// CLI Wrapper
if (require.main === module) {
    const { program } = require('commander');
    program
        .option('-t, --target <id>', 'Target ID')
        .option('-x, --text <text>', 'Text content')
        .option('-c, --content <text>', 'Content (alias for --text)')
        .option('-m, --markdown <text>', 'Markdown content (alias for --text)')
        .option('-f, --text-file <path>', 'File content')
        .option('--title <text>', 'Title')
        .option('--reply-to <id>', 'Message ID to reply to')
        .parse(process.argv);
    
    const opts = program.opts();
    
    // Safety: if --text contains shell-mangled content (missing chars),
    // try to detect and warn. Also support stdin piping as fallback.
    if (opts.text && !opts.textFile) {
        // Write to temp file to avoid shell escaping issues with special chars
        const tmpPath = path.join('/tmp', `feishu_post_${Date.now()}_${Math.random().toString(36).slice(2)}.txt`);
        fs.writeFileSync(tmpPath, opts.text);
        opts.textFile = tmpPath;
        opts.text = undefined;
        // Clean up after send
        sendPost(opts).then(() => {
            try { fs.unlinkSync(tmpPath); } catch (_) {}
        }).catch((e) => {
            try { fs.unlinkSync(tmpPath); } catch (_) {}
            process.exit(1);
        });
    } else {
        sendPost(opts).catch(() => process.exit(1));
    }
}

module.exports = { sendPost };


path: utils/feishu-client.js
content: function loadFeishuClient() {
  try {
    return require("../../feishu-common/index.js");
  } catch (err) {
    const depErr = new Error(
      "Missing dependency: feishu-common. Install feishu-common skill first, then retry.",
    );
    depErr.cause = err;
    throw depErr;
  }
}

module.exports = loadFeishuClient();


path: utils/markdown-parser.js
content: const emojiMap = require('../emoji-map.js');

function parseMarkdownToFeishu(text) {
    const segments = [];
    
    // Helper to process formatting (Bold/Italic/Code)
    const processFormatting = (str) => {
        const res = [];
        
        // 1. Split by Code: `text`
        const parts = str.split(/(`[^`]+`)/g);
        for (const p of parts) {
            if (p.startsWith('`') && p.endsWith('`') && p.length > 2) {
                 // Feishu Post 'text' tag does not support 'code' style officially.
                 // Reverting to plain text with backticks to ensure visibility.
                 res.push({ tag: 'text', text: p }); 
            } else {
                // 2. Split by Bold: **text**
                const subParts = p.split(/(\*\*[^*]+\*\*)/g);
                for (const sp of subParts) {
                    if (sp.startsWith('**') && sp.endsWith('**') && sp.length > 4) {
                        res.push({ tag: 'text', text: sp.slice(2, -2), style: ['bold'] });
                    } else {
                        // 3. Split by Italic: *text*
                        const subSubParts = sp.split(/(\*[^*]+\*)/g);
                        for (const ssp of subSubParts) {
                            if (ssp.startsWith('*') && ssp.endsWith('*') && ssp.length > 2) {
                                res.push({ tag: 'text', text: ssp.slice(1, -1), style: ['italic'] });
                            } else if (ssp) {
                                res.push({ tag: 'text', text: ssp });
                            }
                        }
                    }
                }
            }
        }
        return res;
    };

    // Regex: Link ([text](url)) OR Emoji ([text])
    const regex = /(\[[^\]]+\]\([^)]+\))|(\[[^\]]+\])/g;
    const parts = text.split(regex);
    
    for (const part of parts) {
        if (!part) continue;
        
        // Handle Link
        if (part.startsWith('[') && part.includes('](') && part.endsWith(')')) {
            const m = part.match(/^\[(.*?)\]\((.*?)\)$/);
            if (m) {
                segments.push({ tag: 'a', text: m[1], href: m[2] });
                continue;
            }
        }
        
        // Handle Emoji
        if (part.startsWith('[') && part.endsWith(']')) {
             if (emojiMap[part]) {
                 segments.push({ tag: 'emotion', emoji_type: emojiMap[part] });
                 continue;
             }
        }
        
        // Handle Text Formatting
        segments.push(...processFormatting(part));
    }
    return segments;
}

module.exports = { parseMarkdownToFeishu };


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