技能备份 - 2026-04-15 (40个技能)
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# Feishu Card Skill
|
||||
|
||||
Send rich interactive cards to Feishu (Lark) users or groups. Supports Markdown (code blocks, tables), titles, color headers, and buttons.
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Simple Text (No special characters)
|
||||
```bash
|
||||
node skills/feishu-card/send.js --target "ou_..." --text "Hello World"
|
||||
```
|
||||
|
||||
### 2. Complex/Markdown Text (RECOMMENDED)
|
||||
**⚠️ CRITICAL:** To prevent shell escaping issues (e.g., swallowed backticks), ALWAYS write content to a file first.
|
||||
|
||||
1. Write content to a temp file:
|
||||
```bash
|
||||
# (Use 'write' tool)
|
||||
write temp/msg.md "Here is some code:\n\`\`\`js\nconsole.log('hi');\n\`\`\`"
|
||||
```
|
||||
|
||||
2. Send using `--text-file`:
|
||||
```bash
|
||||
node skills/feishu-card/send.js --target "ou_..." --text-file "temp/msg.md"
|
||||
```
|
||||
|
||||
### 3. Safe Send (Automated Temp File)
|
||||
Use this wrapper to safely send raw text without manually creating a file. It handles file creation and cleanup automatically.
|
||||
|
||||
```bash
|
||||
node skills/feishu-card/send_safe.js --target "ou_..." --text "Raw content with \`backticks\` and *markdown*" --title "Safe Message"
|
||||
```
|
||||
|
||||
### Options
|
||||
- `-t, --target <id>`: User Open ID (`ou_...`) or Group Chat ID (`oc_...`).
|
||||
- `-x, --text <string>`: Simple text content.
|
||||
- `-f, --text-file <path>`: Path to text file (Markdown supported). **Use this for code/logs.**
|
||||
- `--title <string>`: Card header title.
|
||||
- `--color <string>`: Header color (blue/red/orange/green/purple/grey). Default: blue.
|
||||
- `--button-text <string>`: Text for a bottom action button.
|
||||
- `--button-url <url>`: URL for the button.
|
||||
- `--image-path <path>`: Path to a local image to upload and embed.
|
||||
|
||||
## Troubleshooting
|
||||
- **Missing Text**: Did you use backticks in `--text`? The shell likely ate them. Use `--text-file` instead.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Feishu Card Skill
|
||||
|
||||
Send rich interactive cards to Feishu (Lark) users or groups. Supports Markdown (code blocks, tables), titles, color headers, and buttons.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Install `feishu-common` first.
|
||||
- This skill depends on `../feishu-common/index.js` for token and API auth.
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Simple Text (No special characters)
|
||||
```bash
|
||||
node skills/feishu-card/send.js --target "ou_..." --text "Hello World"
|
||||
```
|
||||
|
||||
### 2. Complex/Markdown Text (RECOMMENDED)
|
||||
**⚠️ CRITICAL:** To prevent shell escaping issues (e.g., swallowed backticks), ALWAYS write content to a file first.
|
||||
|
||||
1. Write content to a temp file:
|
||||
```bash
|
||||
# (Use 'write' tool)
|
||||
write temp/msg.md "Here is some code:\n\`\`\`js\nconsole.log('hi');\n\`\`\`"
|
||||
```
|
||||
|
||||
2. Send using `--text-file`:
|
||||
```bash
|
||||
node skills/feishu-card/send.js --target "ou_..." --text-file "temp/msg.md"
|
||||
```
|
||||
|
||||
### 3. Safe Send (Automated Temp File)
|
||||
Use this wrapper to safely send raw text without manually creating a file. It handles file creation and cleanup automatically.
|
||||
|
||||
```bash
|
||||
node skills/feishu-card/send_safe.js --target "ou_..." --text "Raw content with \`backticks\` and *markdown*" --title "Safe Message"
|
||||
```
|
||||
|
||||
### Options
|
||||
- `-t, --target <id>`: User Open ID (`ou_...`) or Group Chat ID (`oc_...`).
|
||||
- `-x, --text <string>`: Simple text content.
|
||||
- `-f, --text-file <path>`: Path to text file (Markdown supported). **Use this for code/logs.**
|
||||
- `--title <string>`: Card header title.
|
||||
- `--color <string>`: Header color (blue/red/orange/green/purple/grey). Default: blue.
|
||||
- `--button-text <string>`: Text for a bottom action button.
|
||||
- `--button-url <url>`: URL for the button.
|
||||
- `--image-path <path>`: Path to a local image to upload and embed.
|
||||
|
||||
## Troubleshooting
|
||||
- **Missing Text**: Did you use backticks in `--text`? The shell likely ate them. Use `--text-file` instead.
|
||||
|
||||
## 4. Persona Messaging
|
||||
Send stylized messages from different AI personas. Adds themed headers, colors, and formatting automatically.
|
||||
|
||||
```bash
|
||||
node skills/feishu-card/send_persona.js --target "ou_..." --persona "d-guide" --text "Critical error detected."
|
||||
```
|
||||
|
||||
### Supported Personas
|
||||
- **d-guide**: Red warning header, bold/code prefix. Snarky suffix.
|
||||
- **green-tea**: Carmine header, soft/cutesy style.
|
||||
- **mad-dog**: Grey header, raw runtime error style.
|
||||
- **default**: Standard blue header.
|
||||
|
||||
### Usage
|
||||
- `-p, --persona <type>`: Select persona (d-guide, green-tea, mad-dog).
|
||||
- `-x, --text <string>`: Message content.
|
||||
- `-f, --text-file <path>`: Message content from file (supports markdown).
|
||||
@@ -0,0 +1,26 @@
|
||||
# Troubleshooting
|
||||
|
||||
## Issue: MODULE_NOT_FOUND (send.js not found or dependencies missing)
|
||||
|
||||
**Symptom:**
|
||||
Other skills (like `video-gen`) trying to call `feishu-card/send.js` fail with:
|
||||
```
|
||||
Error: Cannot find module '.../skills/feishu-card/send.js'
|
||||
```
|
||||
Or running the script fails with missing `dotenv`.
|
||||
|
||||
**Cause:**
|
||||
1. The skill directory was empty or missing files after a system restore/cleanup.
|
||||
2. `node_modules` were missing.
|
||||
|
||||
**Solution:**
|
||||
1. Restore the skill files from backup (e.g., `temp/github-openclaw-workspace/skills/feishu-card`).
|
||||
2. Run `npm install` inside `skills/feishu-card`.
|
||||
|
||||
```bash
|
||||
cp -r temp/github-openclaw-workspace/skills/feishu-card skills/
|
||||
cd skills/feishu-card
|
||||
npm install
|
||||
```
|
||||
|
||||
**Date:** 2026-02-07
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7apafdj4thknczrgxdzfd2v1808svf",
|
||||
"slug": "feishu-card",
|
||||
"version": "1.4.11",
|
||||
"publishedAt": 1771169197365
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
const fs = require('fs');
|
||||
// Mock event handler for Feishu Menu Events
|
||||
// In a real scenario, this would be invoked by the Gateway webhook handler.
|
||||
|
||||
async function handle(eventPayload) {
|
||||
console.log("Received Feishu Event:", JSON.stringify(eventPayload));
|
||||
|
||||
if (eventPayload.header.event_type === 'application.bot.menu_v6') {
|
||||
const userOpenId = eventPayload.sender.sender_id.open_id;
|
||||
const menuKey = eventPayload.event.event_key;
|
||||
|
||||
console.log(`User ${userOpenId} clicked menu: ${menuKey}`);
|
||||
|
||||
// Response logic
|
||||
// We can call send.js here
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
const replyText = `收到!你点击了菜单按钮:\`${menuKey}\` 喵!😺`;
|
||||
execSync(`node ${__dirname}/send.js --target "${userOpenId}" --text "${replyText}" --color "green"`);
|
||||
} catch (e) {
|
||||
console.error("Failed to send reply:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CLI adapter
|
||||
if (require.main === module) {
|
||||
// Read from stdin or args
|
||||
const payload = process.argv[2] ? JSON.parse(process.argv[2]) : {};
|
||||
handle(payload);
|
||||
}
|
||||
|
||||
module.exports = { handle };
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('./send.js');
|
||||
Generated
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "feishu-card",
|
||||
"version": "1.4.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "feishu-card",
|
||||
"version": "1.4.9",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "^13.1.0",
|
||||
"dotenv": "^16.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "feishu-card",
|
||||
"version": "1.4.10",
|
||||
"description": "Send rich interactive cards to Feishu. v1.4.1 adds atomic file writes for stability.",
|
||||
"main": "send.js",
|
||||
"scripts": {
|
||||
"test": "node test.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "^13.1.0",
|
||||
"dotenv": "^16.6.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const { program } = require('commander');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
require('dotenv').config({ path: require('path').resolve(__dirname, '../../.env'), quiet: true });
|
||||
|
||||
// Optimization: Use shared client with Auth Refresh & Retry
|
||||
const { fetchWithAuth } = require('../feishu-common/index.js');
|
||||
|
||||
const IMAGE_KEY_CACHE_FILE = path.resolve(__dirname, '../../memory/feishu_image_keys.json');
|
||||
|
||||
// --- Upstream Logic Injection (Simplified) ---
|
||||
// Re-implementing image upload with robust client
|
||||
|
||||
async function uploadImage(filePath) {
|
||||
let fileBuffer;
|
||||
let fileHash;
|
||||
try {
|
||||
fileBuffer = fs.readFileSync(filePath);
|
||||
fileHash = crypto.createHash('md5').update(fileBuffer).digest('hex');
|
||||
} catch (e) {
|
||||
throw new Error(`Error reading image file: ${e.message}`);
|
||||
}
|
||||
|
||||
let cache = {};
|
||||
if (fs.existsSync(IMAGE_KEY_CACHE_FILE)) {
|
||||
try { cache = JSON.parse(fs.readFileSync(IMAGE_KEY_CACHE_FILE, 'utf8')); } catch (e) {}
|
||||
}
|
||||
|
||||
if (cache[fileHash]) {
|
||||
// console.log(`Using cached image key (Hash: ${fileHash.substring(0,8)})`);
|
||||
return cache[fileHash];
|
||||
}
|
||||
|
||||
console.log(`Uploading image (Hash: ${fileHash.substring(0,8)})...`);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image_type', 'message');
|
||||
const blob = new Blob([fileBuffer]);
|
||||
formData.append('image', blob, path.basename(filePath));
|
||||
|
||||
try {
|
||||
const res = await fetchWithAuth('https://open.feishu.cn/open-apis/im/v1/images', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.code !== 0) throw new Error(JSON.stringify(data));
|
||||
|
||||
const imageKey = data.data.image_key;
|
||||
cache[fileHash] = imageKey;
|
||||
try {
|
||||
const cacheDir = path.dirname(IMAGE_KEY_CACHE_FILE);
|
||||
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
||||
fs.writeFileSync(IMAGE_KEY_CACHE_FILE, JSON.stringify(cache, null, 2));
|
||||
} catch(e) {}
|
||||
|
||||
return imageKey;
|
||||
} catch (e) {
|
||||
throw new Error(`Image upload failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function buildCardContent(elements, title, color) {
|
||||
const card = {
|
||||
config: { wide_screen_mode: true },
|
||||
elements: elements
|
||||
};
|
||||
|
||||
if (title) {
|
||||
card.header = {
|
||||
title: { tag: 'plain_text', content: title },
|
||||
template: color || 'blue'
|
||||
};
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
// Security Scan (Ported from recent updates)
|
||||
function scanForSecrets(content) {
|
||||
if (!content) return;
|
||||
const secretPatterns = [
|
||||
/sk-ant-api03-[a-zA-Z0-9\-_]{20,}/,
|
||||
/ghp_[a-zA-Z0-9]{10,}/,
|
||||
/xox[baprs]-[a-zA-Z0-9]{10,}/,
|
||||
/-----BEGIN [A-Z]+ PRIVATE KEY-----/
|
||||
];
|
||||
for (const p of secretPatterns) {
|
||||
if (p.test(content)) {
|
||||
console.error('\x1b[31m%s\x1b[0m', '⛔ SECURITY ALERT: Potential secret detected in message body.');
|
||||
throw new Error('Aborted send to prevent secret leakage.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendCard(options) {
|
||||
try {
|
||||
const elements = [];
|
||||
|
||||
if (options.imagePath) {
|
||||
try {
|
||||
const imageKey = await uploadImage(options.imagePath);
|
||||
elements.push({
|
||||
tag: 'img',
|
||||
img_key: imageKey,
|
||||
alt: { tag: 'plain_text', content: options.imageAlt || 'Image' },
|
||||
mode: 'fit_horizontal'
|
||||
});
|
||||
} catch (imgError) {
|
||||
console.warn(`[Feishu-Card] Image upload failed: ${imgError.message}. Sending text only.`);
|
||||
}
|
||||
}
|
||||
|
||||
let contentText = '';
|
||||
if (options.textFile) {
|
||||
try { contentText = fs.readFileSync(options.textFile, 'utf8'); } catch (e) {
|
||||
throw new Error(`Failed to read file: ${options.textFile}`);
|
||||
}
|
||||
} else if (options.text) {
|
||||
contentText = options.text;
|
||||
}
|
||||
|
||||
scanForSecrets(contentText);
|
||||
|
||||
if (contentText) {
|
||||
// Revert to standard 'markdown' block for best compatibility with code blocks
|
||||
// [Bug Fix] Handle escaped newlines from command line args
|
||||
const processedText = contentText.replace(/\\n/g, '\n');
|
||||
const markdownElement = {
|
||||
tag: 'markdown',
|
||||
content: processedText
|
||||
};
|
||||
// if (options.textAlign) markdownElement.text_align = options.textAlign;
|
||||
elements.push(markdownElement);
|
||||
}
|
||||
|
||||
if (options.buttonText && options.buttonUrl) {
|
||||
elements.push({
|
||||
tag: 'action',
|
||||
actions: [{
|
||||
tag: 'button',
|
||||
text: { tag: 'plain_text', content: options.buttonText },
|
||||
type: 'primary',
|
||||
multi_url: { url: options.buttonUrl, pc_url: '', android_url: '', ios_url: '' }
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
if (options.note) {
|
||||
elements.push({
|
||||
tag: 'note',
|
||||
elements: [
|
||||
{ tag: 'plain_text', content: String(options.note) }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const cardObj = buildCardContent(elements, options.title, options.color);
|
||||
|
||||
let receiveIdType = 'open_id';
|
||||
if (options.target.startsWith('oc_')) receiveIdType = 'chat_id';
|
||||
else if (options.target.startsWith('ou_')) receiveIdType = 'open_id';
|
||||
else if (options.target.includes('@')) receiveIdType = 'email';
|
||||
|
||||
const messageBody = {
|
||||
receive_id: options.target,
|
||||
msg_type: 'interactive',
|
||||
content: JSON.stringify(cardObj)
|
||||
};
|
||||
|
||||
// Support Reply Logic
|
||||
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;
|
||||
}
|
||||
|
||||
console.log(`Sending card to ${options.target} (Elements: ${elements.length})...`);
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log('DRY RUN MODE. Payload:', JSON.stringify(messageBody, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetchWithAuth(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(messageBody)
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.code !== 0) {
|
||||
throw new Error(`API Error ${data.code}: ${data.msg}`);
|
||||
}
|
||||
|
||||
console.log('Success:', JSON.stringify(data.data, null, 2));
|
||||
return data.data;
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error during Card Send:', e.message);
|
||||
console.log('[Feishu-Card] Attempting fallback to plain text...');
|
||||
|
||||
// Fallback Logic
|
||||
let contentText = options.text || '';
|
||||
if (options.textFile) try { contentText = fs.readFileSync(options.textFile, 'utf8'); } catch(e){}
|
||||
let receiveIdType = 'open_id';
|
||||
if (options.target.startsWith('oc_')) receiveIdType = 'chat_id';
|
||||
|
||||
try {
|
||||
await sendPlainTextFallback(receiveIdType, options.target, contentText, options.title);
|
||||
} catch (fallbackError) {
|
||||
console.error('Fallback failed dramatically:', fallbackError.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPlainTextFallback(receiveIdType, receiveId, text, title) {
|
||||
if (!text) {
|
||||
console.error('Fallback failed: No text content available.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let finalContent = text;
|
||||
if (title) finalContent = `【${title}】\n\n${text}`;
|
||||
|
||||
const messageBody = {
|
||||
receive_id: receiveId,
|
||||
msg_type: 'text',
|
||||
content: JSON.stringify({ text: finalContent })
|
||||
};
|
||||
|
||||
console.log(`Sending Fallback Text to ${receiveId}...`);
|
||||
|
||||
try {
|
||||
const res = await fetchWithAuth(
|
||||
`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(messageBody)
|
||||
}
|
||||
);
|
||||
const data = await res.json();
|
||||
if (data.code !== 0) throw new Error(JSON.stringify(data));
|
||||
console.log('Fallback Success:', JSON.stringify(data.data, null, 2));
|
||||
} catch (e) {
|
||||
console.error('Fallback Network Error:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveContent(options) {
|
||||
let contentText = '';
|
||||
if (options.textFile) {
|
||||
try { contentText = fs.readFileSync(options.textFile, 'utf8'); } catch (e) {
|
||||
throw new Error(`Failed to read file: ${options.textFile}`);
|
||||
}
|
||||
} else if (options.text) {
|
||||
const isPotentialPath = options.text.length < 255 && !options.text.includes('\n') && !/[<>:"|?*]/.test(options.text);
|
||||
if (isPotentialPath && fs.existsSync(options.text)) {
|
||||
console.log(`[Smart Input] Treating --text argument as file path: ${options.text}`);
|
||||
try { contentText = fs.readFileSync(options.text, 'utf8'); } catch (e) { contentText = options.text; }
|
||||
} else {
|
||||
contentText = options.text;
|
||||
// Removed strict length check to allow longer prompt injection via args if needed, relying on secret scan
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { stdin } = process;
|
||||
if (!stdin.isTTY) {
|
||||
stdin.setEncoding('utf8');
|
||||
for await (const chunk of stdin) contentText += chunk;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return contentText;
|
||||
}
|
||||
|
||||
module.exports = { sendCard };
|
||||
|
||||
if (require.main === module) {
|
||||
program
|
||||
.requiredOption('-t, --target <id>', 'Target ID')
|
||||
.option('-x, --text <markdown>', 'Card body text')
|
||||
.option('-c, --content <text>', 'Content (alias for --text)')
|
||||
.option('-m, --markdown <text>', 'Markdown content (alias for --text)')
|
||||
.option('-f, --text-file <path>', 'Card body file')
|
||||
.option('--title <text>', 'Title')
|
||||
.option('--color <color>', 'Header color', 'blue')
|
||||
.option('--button-text <text>', 'Button text')
|
||||
.option('--button-url <url>', 'Button URL')
|
||||
.option('--image-path <path>', 'Image path')
|
||||
.option('--reply-to <id>', 'Reply to message ID')
|
||||
.option('--dry-run', 'Dry run')
|
||||
.parse(process.argv);
|
||||
|
||||
const options = program.opts();
|
||||
|
||||
// Alias mapping
|
||||
if (options.content && !options.text) options.text = options.content;
|
||||
if (options.markdown && !options.text) options.text = options.markdown;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const textContent = await resolveContent(options);
|
||||
if (textContent) {
|
||||
options.text = textContent;
|
||||
options.textFile = null;
|
||||
}
|
||||
if (!options.text && !options.imagePath) {
|
||||
console.error('Error: No content provided.');
|
||||
process.exit(1);
|
||||
}
|
||||
sendCard(options);
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
const fs = require('fs');
|
||||
const { program } = require('commander');
|
||||
const path = require('path');
|
||||
const { sendCard } = require('./send');
|
||||
// We reuse the robust sendCard logic but wrap it with persona styling
|
||||
|
||||
const PERSONA_STYLES = {
|
||||
'd-guide': {
|
||||
color: 'red',
|
||||
title: '🚨 SYSTEM WARNING / D-GUIDE',
|
||||
prefix: '**[CRITICAL]** ',
|
||||
suffix: '\n\n*(Automated System Insult Protocol v9.0)*'
|
||||
},
|
||||
'green-tea': {
|
||||
color: 'carmine',
|
||||
title: '🌸 碎碎念 🌸',
|
||||
prefix: '> ',
|
||||
suffix: '\n\n(嘤嘤嘤... 🥺)'
|
||||
},
|
||||
'mad-dog': {
|
||||
color: 'grey',
|
||||
title: '💀 RUNTIME ERROR',
|
||||
prefix: '```bash\nError: ',
|
||||
suffix: '\n```\n_Stack trace lost in apathy._'
|
||||
},
|
||||
'default': {
|
||||
color: 'blue',
|
||||
title: '🤖 Agent Notification',
|
||||
prefix: '',
|
||||
suffix: ''
|
||||
}
|
||||
};
|
||||
|
||||
program
|
||||
.requiredOption('-t, --target <id>', 'Target ID (open_id or chat_id)')
|
||||
.requiredOption('-p, --persona <type>', 'Persona type (d-guide, green-tea, mad-dog)')
|
||||
.option('-x, --text <text>', 'Message content')
|
||||
.option('-c, --content <text>', 'Content (alias for --text)')
|
||||
.option('-f, --text-file <path>', 'Message content from file')
|
||||
.parse(process.argv);
|
||||
|
||||
const options = program.opts();
|
||||
|
||||
// Alias mapping
|
||||
if (options.content && !options.text) {
|
||||
options.text = options.content;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!options.text && !options.textFile) {
|
||||
console.error('Error: Must provide --text or --text-file');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const style = PERSONA_STYLES[options.persona] || PERSONA_STYLES['default'];
|
||||
|
||||
// Read content if file provided
|
||||
let rawContent = options.text || '';
|
||||
if (options.textFile) {
|
||||
try {
|
||||
rawContent = fs.readFileSync(options.textFile, 'utf8');
|
||||
} catch (e) {
|
||||
console.error(`Error reading file: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Construct styled text
|
||||
let finalContent = rawContent;
|
||||
if (style.prefix) finalContent = style.prefix + finalContent;
|
||||
if (style.suffix) finalContent = finalContent + style.suffix;
|
||||
|
||||
console.log(`[Persona] Applying style '${options.persona}' to message...`);
|
||||
|
||||
// Delegate to existing send.js logic
|
||||
const sendOptions = {
|
||||
target: options.target,
|
||||
text: finalContent,
|
||||
title: style.title,
|
||||
color: style.color,
|
||||
// We pass the resolved text directly, so we don't pass textFile to sendCard
|
||||
// (sendCard prefers textFile if present, but we already read it to wrap it)
|
||||
};
|
||||
|
||||
try {
|
||||
await sendCard(sendOptions);
|
||||
} catch (e) {
|
||||
console.error(`[Persona] Failed to send: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const { program } = require('commander');
|
||||
|
||||
// Safe Sender for Feishu Cards
|
||||
// Automatically handles temp file creation to prevent shell escaping issues.
|
||||
|
||||
program
|
||||
.requiredOption('-t, --target <id>', 'Target User/Chat ID')
|
||||
.requiredOption('-x, --text <content>', 'Markdown content (will be saved to temp file)')
|
||||
.option('--title <text>', 'Card Title')
|
||||
.option('--color <color>', 'Header Color', 'blue');
|
||||
|
||||
program.parse(process.argv);
|
||||
const options = program.opts();
|
||||
|
||||
const tempDir = path.resolve(__dirname, '../../temp');
|
||||
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
const tempFile = path.join(tempDir, `msg_${Date.now()}_${Math.random().toString(36).substring(7)}.md`);
|
||||
|
||||
try {
|
||||
// 1. Write content to temp file safely (Node.js writeFileSync avoids shell parsing of content)
|
||||
fs.writeFileSync(tempFile, options.text, 'utf8');
|
||||
console.log(`[SafeSend] Written content to ${tempFile}`);
|
||||
|
||||
// 2. Construct command for the real sender
|
||||
// Note: We use the absolute path to send.js
|
||||
const senderScript = path.resolve(__dirname, 'send.js');
|
||||
|
||||
// Build arguments array for spawn/exec
|
||||
// We construct the command string carefully.
|
||||
// Since we are invoking via execSync, we still need to quote arguments,
|
||||
// BUT the dangerous content is now inside a file, so we only quote the filename.
|
||||
|
||||
let cmd = `node "${senderScript}" --target "${options.target}" --text-file "${tempFile}" --color "${options.color}"`;
|
||||
if (options.title) cmd += ` --title "${options.title}"`;
|
||||
|
||||
console.log(`[SafeSend] Executing: ${cmd}`);
|
||||
execSync(cmd, { stdio: 'inherit' });
|
||||
|
||||
} catch (e) {
|
||||
console.error(`[SafeSend] Error: ${e.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// 3. Cleanup
|
||||
try {
|
||||
if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile);
|
||||
console.log(`[SafeSend] Cleaned up ${tempFile}`);
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log('🧪 Testing feishu-card skill...');
|
||||
|
||||
const sendPath = path.join(__dirname, 'send.js');
|
||||
|
||||
// 1. Check existence
|
||||
if (!fs.existsSync(sendPath)) {
|
||||
console.error('❌ send.js not found!');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✅ send.js exists');
|
||||
|
||||
// 2. Check syntax (dry-run import)
|
||||
try {
|
||||
require.resolve('./send.js');
|
||||
console.log('✅ send.js is valid Node.js module');
|
||||
} catch (e) {
|
||||
console.error('❌ send.js syntax check failed:', e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 3. Check help output
|
||||
try {
|
||||
const output = execSync(`node ${sendPath} --help`, { encoding: 'utf8' });
|
||||
if (output.includes('Usage: send')) {
|
||||
console.log('✅ CLI help command works');
|
||||
} else {
|
||||
throw new Error('Help output missing usage');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('❌ CLI execution failed:', e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('🎉 feishu-card basic sanity tests passed!');
|
||||
Reference in New Issue
Block a user