Skill: autogame-17/feishu-voice-assistant (version 1.0.0)

--- name: feishu-voice-assistant description: Sends voice messages (audio) to Feishu chats using Duby TTS. tags: [feishu, voice, tts, audio] --- # Feishu Voice Assistant Generate speech from text using Duby AI and send it as a native voice message (audio) to Feishu. ## Usage ### Send a Voice Message ```bash node skills/feishu-voice-assistant/index.js --text "Hello, this is a voice message!" --target "$TARGET_USER_ID" ``` ### Options - `--text`: The text to convert to speech. - `--target`: The Feishu user ID (`ou_...`) or chat ID (`oc_...`). - `--voice`: (Optional) Duby Voice ID. Default is Xinduo. ## Dependencies - `duby`: For TTS generation. - `feishu-common`: For API authentication. - `form-data`: For file uploads. ## Configuration Requires `DUBY_API_KEY` and Feishu credentials in `.env`.

path: index.js
content: #!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { program } = require('commander');
const { getToken, fetchWithRetry } = require('../feishu-common/index.js');
const duby = require('../duby/index.js');
const { Blob } = require('buffer'); // Use native Blob for FormData

// Ensure .env is loaded
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });

async function uploadAudio(token, filePath) {
    const fileName = path.basename(filePath);
    const fileBuffer = fs.readFileSync(filePath);
    const blob = new Blob([fileBuffer]);
    
    const formData = new FormData();
    formData.append('file_type', 'stream'); // Generic stream for audio files
    formData.append('file_name', fileName);
    formData.append('file', blob, fileName);
    formData.append('duration', '10000'); // Optional duration in ms

    try {
        const response = await fetchWithRetry('https://open.feishu.cn/open-apis/im/v1/files', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${token}`
            },
            body: formData
        });
        
        const data = await response.json();
        if (data.code !== 0) {
            throw new Error(`Upload failed: ${data.msg} (Code: ${data.code})`);
        }
        return data.data.file_key;
    } catch (error) {
        throw new Error(`Upload error: ${error.message}`);
    }
}

async function sendAudioMessage(target, fileKey) {
    const token = await getToken();
    
    // Determine receive_id_type
    const receiveIdType = target.startsWith('oc_') ? 'chat_id' : 'open_id';
    
    const body = {
        receive_id: target,
        msg_type: 'audio',
        content: JSON.stringify({ file_key: fileKey })
    };

    try {
        const response = await fetchWithRetry(
            `https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=${receiveIdType}`,
            {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${token}`,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(body)
            }
        );

        const data = await response.json();
        if (data.code !== 0) {
            throw new Error(`Send failed: ${data.msg} (Code: ${data.code})`);
        }
        return data.data;
    } catch (error) {
        throw new Error(`Send error: ${error.message}`);
    }
}

async function main() {
    program
        .option('--text <text>', 'Text to speak')
        .option('--target <id>', 'Target User ID or Chat ID')
        .option('--voice <id>', 'Voice ID (optional)')
        .parse(process.argv);

    const options = program.opts();

    if (!options.text || !options.target) {
        console.error('Usage: node index.js --text "Hello" --target "ou_xxx" [--voice "id"]');
        process.exit(1);
    }

    try {
        console.log(`🎤 Generating audio for: "${options.text}"...`);
        
        // Step 1: Generate TTS
        // duby.duby_tts returns "MEDIA:/path/to/file.mp3" string
        // We need to parse this string to get the actual path.
        const mediaPath = await duby.duby_tts({ 
            text: options.text, 
            voice_id: options.voice 
        });

        if (!mediaPath || !mediaPath.startsWith('MEDIA:')) {
            throw new Error(`Invalid TTS response: ${mediaPath}`);
        }

        const filePath = mediaPath.replace('MEDIA:', '').trim();
        const absolutePath = path.resolve(process.cwd(), filePath);

        if (!fs.existsSync(absolutePath)) {
            throw new Error(`Generated audio file not found at: ${absolutePath}`);
        }

        console.log(`📤 Uploading audio: ${filePath}...`);
        
        // Step 2: Upload to Feishu
        const token = await getToken();
        const fileKey = await uploadAudio(token, absolutePath);
        
        console.log(`📨 Sending audio message to ${options.target}...`);

        // Step 3: Send Message
        await sendAudioMessage(options.target, fileKey);

        console.log('✅ Voice message sent successfully!');
        
        // Cleanup temp file
        // fs.unlinkSync(absolutePath); // Optional: keep for cache or delete
    } catch (error) {
        console.error(`❌ Error: ${error.message}`);
        process.exit(1);
    }
}

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

module.exports = { main };


path: package-lock.json
content: {
  "name": "feishu-voice-assistant",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "feishu-voice-assistant",
      "version": "1.0.0",
      "dependencies": {
        "commander": "^9.0.0",
        "dotenv": "^16.0.0",
        "form-data": "^4.0.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/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/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": "9.5.0",
      "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": "^12.20.0 || >=14"
      }
    },
    "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": "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"
      }
    },
    "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/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/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"
      }
    }
  }
}


path: package.json
content: {
  "name": "feishu-voice-assistant",
  "version": "1.0.0",
  "description": "Sends voice messages (audio) to Feishu using Duby TTS",
  "main": "index.js",
  "dependencies": {
    "commander": "^9.0.0",
    "form-data": "^4.0.0",
    "dotenv": "^16.0.0"
  },
  "scripts": {
    "test": "node index.js --help"
  }
}


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