first commit
This commit is contained in:
139
SKILL.md
Normal file
139
SKILL.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
invocations:
|
||||
- words:
|
||||
- 添加飞书机器人
|
||||
- 配置飞书机器人
|
||||
- 新增飞书账户
|
||||
- 添加机器人账户
|
||||
- feishu bot
|
||||
- 飞书多账户
|
||||
description: 交互式添加新的飞书机器人账户并绑定 Agent
|
||||
---
|
||||
|
||||
# feishu-bot-manager
|
||||
|
||||
飞书多账户机器人配置管理 Skill。
|
||||
|
||||
## 路由绑定方案
|
||||
|
||||
### 方案 1:账户级绑定
|
||||
该飞书账户的所有消息 → 指定 Agent
|
||||
|
||||
**适用场景**:一个机器人专门服务一个 Agent。比如创建一个"销售机器人",它的所有消息都由"销售 Agent"处理。
|
||||
|
||||
**生成的绑定**:
|
||||
```json
|
||||
{ "agentId": "recruiter", "match": { "channel": "feishu", "accountId": "bot-sales" } }
|
||||
```
|
||||
|
||||
### 方案 2:群聊级绑定
|
||||
特定群聊的消息 → 指定 Agent
|
||||
|
||||
**适用场景**:把 Agent 绑定到特定群聊。多个机器人在群里,但不同群聊分配给不同 Agent。
|
||||
|
||||
**生成的绑定**:
|
||||
```json
|
||||
{ "agentId": "recruiter", "match": { "channel": "feishu", "peer": { "kind": "group", "id": "oc_xxx" } } }
|
||||
```
|
||||
|
||||
**注意**:群聊级绑定优先级更高,会覆盖账户级绑定!
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 交互模式(通过对话)
|
||||
|
||||
直接说:"添加飞书机器人"
|
||||
|
||||
我会询问:
|
||||
1. App ID 和 App Secret
|
||||
2. 账户信息(账户 ID、机器人名称)
|
||||
3. **选择路由绑定方案**(账户级/群聊级)
|
||||
4. 选择绑定的 Agent
|
||||
5. 群聊 ID(如果选群聊级绑定)
|
||||
6. 预览确认后执行
|
||||
|
||||
### 命令行调用
|
||||
|
||||
```bash
|
||||
# 账户级绑定 - 该机器人所有消息都由指定 Agent 处理
|
||||
openclaw skills run feishu-bot-manager -- \
|
||||
--app-id cli_xxx \
|
||||
--app-secret yyy \
|
||||
--account-id bot-sales \
|
||||
--agent-id recruiter \
|
||||
--routing-mode account
|
||||
|
||||
# 群聊级绑定 - 特定群聊的消息由指定 Agent 处理
|
||||
openclaw skills run feishu-bot-manager -- \
|
||||
--app-id cli_xxx \
|
||||
--app-secret yyy \
|
||||
--account-id bot-sales \
|
||||
--agent-id recruiter \
|
||||
--chat-id oc_xxx \
|
||||
--routing-mode group
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| --app-id | ✅ | 飞书 App ID (cli_xxx) |
|
||||
| --app-secret | ✅ | 飞书 App Secret |
|
||||
| --account-id | ❌ | 账户标识,默认自动生成 |
|
||||
| --bot-name | ❌ | 机器人名称,默认 "Feishu Bot" |
|
||||
| --dm-policy | ❌ | DM 策略: open/pairing/allowlist,默认 open |
|
||||
| --agent-id | ❌ | 要绑定的 Agent ID |
|
||||
| --chat-id | ❌ | 群聊 ID (oc_xxx),群聊绑定时需要 |
|
||||
| --routing-mode | ❌ | 路由模式: account/group,默认 account |
|
||||
|
||||
## 配置结构示例
|
||||
|
||||
添加新机器人后,配置会变成这样(保留现有配置):
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"feishu": {
|
||||
"enabled": true,
|
||||
"appId": "cli_现有", // ← 保留不动
|
||||
"appSecret": "现有Secret", // ← 保留不动
|
||||
"dmPolicy": "open",
|
||||
"accounts": { // ← 新添加
|
||||
"bot-new": {
|
||||
"appId": "cli_xxx",
|
||||
"appSecret": "yyy",
|
||||
"botName": "新机器人",
|
||||
"dmPolicy": "open",
|
||||
"allowFrom": ["*"],
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"bindings": [
|
||||
{ // ← 新添加
|
||||
"agentId": "recruiter",
|
||||
"match": {
|
||||
"channel": "feishu",
|
||||
"accountId": "bot-new" // 或 "peer": { "kind": "group", "id": "oc_xxx" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 流程
|
||||
|
||||
1. 检查并备份现有配置
|
||||
2. 添加新账户到 `channels.feishu.accounts`
|
||||
3. 根据选择的路由模式添加 binding
|
||||
4. 设置 `session.dmScope` 为 `per-account-channel-peer`
|
||||
5. 重启 Gateway
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **保留现有配置**:现有 `appId/appSecret` 完全不动
|
||||
- **自动备份**:修改前自动备份 openclaw.json
|
||||
- **dmScope 设置**:自动设置会话绑定颗粒度
|
||||
- **重启 Gateway**:重启后约 10-30 秒恢复服务
|
||||
- **恢复方法**:如出问题可用备份文件手动恢复
|
||||
252
index.js
Normal file
252
index.js
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* feishu-bot-manager
|
||||
* 飞书多账户机器人配置管理 Skill
|
||||
*
|
||||
* 支持的路由绑定方案:
|
||||
* 1. 账户级绑定 - 该飞书账户的所有消息路由到指定 Agent
|
||||
* 2. 群聊级绑定 - 特定群聊的消息路由到指定 Agent
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// 配置路径
|
||||
const CONFIG_PATH = path.join(process.env.HOME, '.openclaw', 'openclaw.json');
|
||||
const BACKUP_DIR = path.join(process.env.HOME, '.openclaw', 'backups');
|
||||
|
||||
// 颜色输出
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m',
|
||||
gray: '\x1b[90m',
|
||||
bold: '\x1b[1m'
|
||||
};
|
||||
|
||||
const log = {
|
||||
info: (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`),
|
||||
success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
|
||||
warning: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
|
||||
error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
|
||||
step: (num, total, msg) => console.log(`\n${colors.cyan}[${num}/${total}]${colors.reset} ${msg}`),
|
||||
preview: (msg) => console.log(`${colors.gray}${msg}${colors.reset}`),
|
||||
bold: (msg) => console.log(`${colors.bold}${msg}${colors.reset}`)
|
||||
};
|
||||
|
||||
// 读取配置
|
||||
function loadConfig() {
|
||||
try {
|
||||
const content = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch (err) {
|
||||
log.error(`读取配置失败: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
function saveConfig(config) {
|
||||
try {
|
||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
||||
return true;
|
||||
} catch (err) {
|
||||
log.error(`保存配置失败: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建备份
|
||||
function createBackup() {
|
||||
try {
|
||||
if (!fs.existsSync(BACKUP_DIR)) {
|
||||
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
||||
}
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupPath = path.join(BACKUP_DIR, `openclaw.json.${timestamp}`);
|
||||
fs.copyFileSync(CONFIG_PATH, backupPath);
|
||||
return backupPath;
|
||||
} catch (err) {
|
||||
log.error(`创建备份失败: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示路由方案说明
|
||||
function showRoutingOptions() {
|
||||
console.log(`
|
||||
${colors.bold}📋 路由绑定方案${colors.reset}
|
||||
|
||||
${colors.bold}方案 1:账户级绑定${colors.reset}
|
||||
该飞书账户的所有消息 → 指定 Agent
|
||||
适用:一个机器人专门服务一个 Agent
|
||||
|
||||
${colors.bold}方案 2:群聊级绑定${colors.reset}
|
||||
特定群聊的消息 → 指定 Agent
|
||||
适用:把 Agent 绑定到特定群聊
|
||||
|
||||
${colors.yellow}提示:群聊级绑定优先级更高,会覆盖账户级绑定!${colors.reset}
|
||||
`);
|
||||
}
|
||||
|
||||
// 快速模式
|
||||
async function quickMode(options) {
|
||||
console.log(`\n${colors.cyan}🤖 飞书机器人配置助手${colors.reset}\n`);
|
||||
|
||||
const {
|
||||
appid,
|
||||
appsecret,
|
||||
accountid,
|
||||
botname,
|
||||
agentid,
|
||||
chatid,
|
||||
routingmode
|
||||
} = options;
|
||||
|
||||
if (!appid || !appsecret) {
|
||||
log.error('需要提供 --app-id 和 --app-secret');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
const accountId = accountid || `bot-${Date.now()}`;
|
||||
|
||||
// 创建备份
|
||||
const backupPath = createBackup();
|
||||
log.success(`配置已备份: ${path.basename(backupPath)}`);
|
||||
|
||||
// 确保 channels.feishu.accounts 存在
|
||||
if (!config.channels) config.channels = {};
|
||||
if (!config.channels.feishu) config.channels.feishu = { enabled: true };
|
||||
if (!config.channels.feishu.accounts) config.channels.feishu.accounts = {};
|
||||
|
||||
// 添加账户
|
||||
config.channels.feishu.accounts[accountId] = {
|
||||
appId: appid,
|
||||
appSecret: appsecret,
|
||||
botName: botname || 'Feishu Bot',
|
||||
dmPolicy: options.dmpolicy || 'open',
|
||||
allowFrom: ['*'],
|
||||
enabled: true
|
||||
};
|
||||
|
||||
// 添加绑定
|
||||
if (!config.bindings) config.bindings = [];
|
||||
|
||||
const mode = routingmode || 'account';
|
||||
|
||||
if (mode === 'account' && agentid) {
|
||||
// 账户级绑定:该账户所有消息路由到指定 Agent
|
||||
config.bindings.push({
|
||||
agentId: agentid,
|
||||
match: {
|
||||
channel: 'feishu',
|
||||
accountId: accountId
|
||||
}
|
||||
});
|
||||
log.success(`已添加账户级绑定: ${agentid} ← ${accountId}`);
|
||||
} else if (mode === 'group' && agentid && chatid) {
|
||||
// 群聊级绑定:特定群聊消息路由到指定 Agent
|
||||
config.bindings.push({
|
||||
agentId: agentid,
|
||||
match: {
|
||||
channel: 'feishu',
|
||||
peer: { kind: 'group', id: chatid }
|
||||
}
|
||||
});
|
||||
log.success(`已添加群聊级绑定: ${agentid} ← ${chatid}`);
|
||||
}
|
||||
|
||||
saveConfig(config);
|
||||
log.success('配置已更新');
|
||||
|
||||
// 设置会话绑定颗粒度
|
||||
log.info('设置会话绑定颗粒度...');
|
||||
try {
|
||||
execSync('openclaw config set session.dmScope "per-account-channel-peer"', { stdio: 'pipe' });
|
||||
log.success('会话绑定颗粒度已设置');
|
||||
} catch (err) {
|
||||
log.warning('设置 dmScope 失败,请手动执行:');
|
||||
console.log(' openclaw config set session.dmScope "per-account-channel-peer"');
|
||||
}
|
||||
|
||||
// 重启
|
||||
log.warning('正在重启 Gateway...');
|
||||
try {
|
||||
execSync('openclaw gateway restart', { stdio: 'inherit' });
|
||||
log.success('Gateway 重启完成');
|
||||
} catch (err) {
|
||||
log.error(`重启失败: ${err.message}`);
|
||||
log.info('请手动执行: openclaw gateway restart');
|
||||
}
|
||||
|
||||
// 完成提示
|
||||
console.log('\n' + '─'.repeat(50));
|
||||
log.success('配置完成!');
|
||||
console.log('\n配置摘要:');
|
||||
console.log(` 账户 ID: ${accountId}`);
|
||||
console.log(` 路由模式: ${mode}`);
|
||||
if (agentid) console.log(` Agent: ${agentid}`);
|
||||
if (chatid) console.log(` 群聊: ${chatid}`);
|
||||
console.log('\n如配置有误,可从备份恢复:');
|
||||
console.log(` cp ${backupPath} ${CONFIG_PATH}`);
|
||||
console.log('─'.repeat(50) + '\n');
|
||||
}
|
||||
|
||||
// 处理命令行参数
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const options = {};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg.startsWith('--')) {
|
||||
const key = arg.slice(2).replace(/-/g, '');
|
||||
const value = args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : 'true';
|
||||
options[key] = value;
|
||||
if (value !== 'true') i++;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// 主入口
|
||||
const options = parseArgs();
|
||||
|
||||
if (options.help || options.h) {
|
||||
showRoutingOptions();
|
||||
console.log(`
|
||||
${colors.bold}用法:${colors.reset}
|
||||
node index.js [选项]
|
||||
|
||||
${colors.bold}选项:${colors.reset}
|
||||
--app-id <id> 飞书 App ID (必填)
|
||||
--app-secret <secret> 飞书 App Secret (必填)
|
||||
--account-id <id> 账户标识 (可选, 默认自动生成)
|
||||
--bot-name <name> 机器人名称 (可选)
|
||||
--dm-policy <policy> DM 策略: open/pairing/allowlist (默认: open)
|
||||
--agent-id <id> 要绑定的 Agent ID (可选)
|
||||
--chat-id <id> 群聊 ID oc_xxx (群聊绑定时需要)
|
||||
--routing-mode <mode> 路由模式: account/group (默认: account)
|
||||
--help, -h 显示帮助
|
||||
|
||||
${colors.bold}示例:${colors.reset}
|
||||
# 账户级绑定 - 该机器人所有消息都由指定 Agent 处理
|
||||
node index.js --app-id cli_xxx --app-secret yyy --agent-id recruiter --routing-mode account
|
||||
|
||||
# 群聊级绑定 - 特定群聊的消息由指定 Agent 处理
|
||||
node index.js --app-id cli_xxx --app-secret yyy --agent-id recruiter --chat-id oc_xxx --routing-mode group
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (options.appid) {
|
||||
quickMode(options);
|
||||
} else {
|
||||
log.error('请提供 --app-id 和 --app-secret,或使用 --help 查看帮助');
|
||||
process.exit(1);
|
||||
}
|
||||
68
lib/validator.js
Normal file
68
lib/validator.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 配置验证工具
|
||||
*/
|
||||
|
||||
// 验证 App ID 格式
|
||||
function validateAppId(appId) {
|
||||
return /^cli_[a-zA-Z0-9]+$/.test(appId);
|
||||
}
|
||||
|
||||
// 验证账户 ID 格式
|
||||
function validateAccountId(id) {
|
||||
return /^[a-z0-9-]+$/.test(id);
|
||||
}
|
||||
|
||||
// 验证群聊 ID 格式
|
||||
function validateChatId(id) {
|
||||
return /^oc_[a-zA-Z0-9]+$/.test(id);
|
||||
}
|
||||
|
||||
// 验证用户 ID 格式
|
||||
function validateUserId(id) {
|
||||
return /^ou_[a-zA-Z0-9]+$/.test(id);
|
||||
}
|
||||
|
||||
// 验证完整配置
|
||||
function validateConfig(config) {
|
||||
const errors = [];
|
||||
|
||||
if (!config.channels?.feishu) {
|
||||
errors.push('缺少 channels.feishu 配置');
|
||||
return errors;
|
||||
}
|
||||
|
||||
const accounts = config.channels.feishu.accounts || {};
|
||||
|
||||
for (const [key, acc] of Object.entries(accounts)) {
|
||||
if (!validateAccountId(key)) {
|
||||
errors.push(`账户 ID "${key}" 格式无效`);
|
||||
}
|
||||
if (!validateAppId(acc.appId)) {
|
||||
errors.push(`[${key}] App ID 格式无效: ${acc.appId}`);
|
||||
}
|
||||
if (!acc.appSecret) {
|
||||
errors.push(`[${key}] App Secret 不能为空`);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 bindings
|
||||
const bindings = config.bindings || [];
|
||||
for (const binding of bindings) {
|
||||
if (!binding.agentId) {
|
||||
errors.push('存在缺少 agentId 的 binding');
|
||||
}
|
||||
if (!binding.match?.peer?.id) {
|
||||
errors.push('存在缺少 peer.id 的 binding');
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId,
|
||||
validateAccountId,
|
||||
validateChatId,
|
||||
validateUserId,
|
||||
validateConfig
|
||||
};
|
||||
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "feishu-bot-manager",
|
||||
"version": "1.0.0",
|
||||
"description": "飞书多账户机器人配置管理",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"readline": "^1.3.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user