Files
openclaw 14cc4311d8 fix: 完善备份、验证和技能共享机制
- 备份 agents-registry.json(与 openclaw.json 同时备份)
- 验证 accountId 唯一性,防止覆盖现有配置
- 验证 bindings 重复,防止重复绑定
- 回退 agents-registry.json(失败时恢复)
- 继承顶层 allowFrom 配置(安全优先)
- 创建共享 skills 目录(符号链接)
- 清理符号链接(回退时)
- 新增 AUDIT-REPORT.md 审查报告
2026-03-17 22:59:54 +08:00

1255 lines
40 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* agent-creator-with-binding
* 创建新 Agent 并配置飞书机器人绑定(整合版)
*
* 支持的路由绑定方案:
* 1. 账户级绑定 - 该飞书账户的所有消息路由到指定 Agent
* 2. 群聊级绑定 - 特定群聊的消息路由到指定 Agent
*/
const fs = require('fs');
const path = require('path');
const readline = require('readline');
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}`)
};
// 创建 readline 接口
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// promisified question
function question(query) {
return new Promise(resolve => rl.question(query, resolve));
}
// 读取配置
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);
log.success(`配置已备份:${path.basename(backupPath)}`);
// 额外备份 agents-registry.json如果存在
const registryPath = path.join(path.dirname(CONFIG_PATH), 'agents-registry.json');
if (fs.existsSync(registryPath)) {
const registryBackup = path.join(BACKUP_DIR, `agents-registry.json.${timestamp}`);
fs.copyFileSync(registryPath, registryBackup);
log.success(`Agent 注册表已备份:${path.basename(registryBackup)}`);
}
return backupPath;
} catch (err) {
log.error(`创建备份失败:${err.message}`);
return null;
}
}
// 验证 accountId 唯一性
function validateAccountIdUnique(config, accountId) {
const existingAccounts = config.channels?.feishu?.accounts || {};
if (existingAccounts[accountId]) {
return {
valid: false,
error: `账户 ID '${accountId}' 已存在,请使用不同的 ID`
};
}
return { valid: true };
}
// 验证 binding 不重复
function validateBindingUnique(config, agentId, accountId, chatId, bindingMode) {
const bindings = config.bindings || [];
for (const binding of bindings) {
if (binding.agentId !== agentId) {
if (bindingMode === 'account' && binding.match?.accountId === accountId) {
return {
valid: false,
error: `账户 '${accountId}' 已绑定到 agent '${binding.agentId}'`
};
}
if (bindingMode === 'group' && binding.match?.peer?.id === chatId) {
return {
valid: false,
error: `群聊 '${chatId}' 已绑定到 agent '${binding.agentId}'`
};
}
}
}
return { valid: true };
}
// 回退 Agent 注册表
function rollbackRegistry(backupPath) {
if (!backupPath) return false;
const registryBackup = backupPath.replace('openclaw.json', 'agents-registry.json');
const registryPath = path.join(path.dirname(CONFIG_PATH), 'agents-registry.json');
if (fs.existsSync(registryBackup)) {
try {
fs.copyFileSync(registryBackup, registryPath);
log.success('Agent 注册表已回退');
return true;
} catch (err) {
log.error(`回退注册表失败:${err.message}`);
}
}
return false;
}
// 回退配置
function rollback(backupPath) {
if (!backupPath || !fs.existsSync(backupPath)) {
log.error('备份文件不存在,无法回退');
return false;
}
try {
log.warning('正在回退配置...');
fs.copyFileSync(backupPath, CONFIG_PATH);
log.success('配置已回退');
return true;
} catch (err) {
log.error(`回退失败:${err.message}`);
return false;
}
}
// 清理已创建的 Agent回退用
function cleanupAgent(agentId) {
try {
const workspacePath = `/home/admin/.openclaw/workspace-${agentId}`;
const agentPath = `/home/admin/.openclaw/agents/${agentId}`;
// 先删除符号链接(如果有)
const skillsPath = path.join(workspacePath, 'skills');
if (fs.existsSync(skillsPath)) {
try {
const stats = fs.lstatSync(skillsPath);
if (stats.isSymbolicLink()) {
fs.unlinkSync(skillsPath);
log.warning(`已删除 skills 符号链接`);
}
} catch (e) {
// 忽略符号链接删除错误
}
}
if (fs.existsSync(workspacePath)) {
fs.rmSync(workspacePath, { recursive: true, force: true });
log.warning(`已删除工作空间:${workspacePath}`);
}
if (fs.existsSync(agentPath)) {
fs.rmSync(agentPath, { recursive: true, force: true });
log.warning(`已删除 Agent 目录:${agentPath}`);
}
return true;
} catch (err) {
log.error(`清理 Agent 失败:${err.message}`);
return false;
}
}
// 验证函数
function validateAgentId(id) {
return /^[a-z0-9-]+$/.test(id);
}
function validateAppId(appId) {
return /^cli_[a-zA-Z0-9]+$/.test(appId);
}
function validateAccountId(id) {
return /^[a-z0-9-]+$/.test(id);
}
function validateChatId(id) {
return /^oc_[a-zA-Z0-9]+$/.test(id);
}
// 创建 Agent
function createAgent(agentId) {
log.step(1, 6, `创建 Agent: ${agentId}`);
try {
execSync(`openclaw agents add ${agentId}`, { stdio: 'inherit' });
log.success(`Agent ${agentId} 创建成功`);
// 创建 skills 符号链接(共享技能)
setupSharedSkills(agentId);
return true;
} catch (err) {
log.error(`创建 Agent 失败:${err.message}`);
return false;
}
}
// 设置共享 Skills符号链接
function setupSharedSkills(agentId) {
const workspacePath = `/home/admin/.openclaw/workspace-${agentId}`;
const skillsPath = path.join(workspacePath, 'skills');
const sharedSkillsPath = path.join(path.dirname(CONFIG_PATH), 'skills');
try {
// 如果 skills 目录不存在,创建符号链接
if (!fs.existsSync(skillsPath)) {
// 确保共享 skills 目录存在
if (!fs.existsSync(sharedSkillsPath)) {
fs.mkdirSync(sharedSkillsPath, { recursive: true });
log.success(`创建共享 skills 目录:${sharedSkillsPath}`);
}
// 创建符号链接
fs.symlinkSync(sharedSkillsPath, skillsPath, 'dir');
log.success(`已创建 skills 符号链接 → ${sharedSkillsPath}`);
}
} catch (err) {
log.warning(`创建 skills 符号链接失败:${err.message}`);
log.warning('Agent 将使用独立 skills 目录');
}
}
// 生成 Agent 人设文件
function generateAgentFiles(options) {
log.step(2, 6, '生成 Agent 人设文件');
const workspacePath = `/home/admin/.openclaw/workspace-${options.agentId}`;
const templatesPath = path.join(__dirname, 'templates');
try {
// 读取模板
const soulTemplate = fs.readFileSync(path.join(templatesPath, 'soul.md.template'), 'utf8');
const agentsTemplate = fs.readFileSync(path.join(templatesPath, 'agents.md.template'), 'utf8');
// 生成个性化内容
const soulContent = generateSoulContent(options);
const agentsContent = generateAgentsContent(options);
// 写入文件
fs.writeFileSync(path.join(workspacePath, 'SOUL.md'), soulContent);
fs.writeFileSync(path.join(workspacePath, 'AGENTS.md'), agentsContent);
log.success(`已生成 SOUL.md 和 AGENTS.md`);
return true;
} catch (err) {
log.error(`生成人设文件失败:${err.message}`);
log.warning('Agent 已创建,但人设文件未生成,可手动创建');
return false;
}
}
// 生成 SOUL.md 内容
function generateSoulContent(options) {
const { agentName, description, bindingMode, agentType, coordinator } = options;
const displayType = agentType === 'coordinator' ? '协调者 Agent'
: agentType === 'specialist' ? '专职 Agent'
: bindingMode === 'none' ? '后台 Agent' : '前台 Agent';
// 根据职责生成 Skills 推荐
const recommendedSkills = getRecommendedSkills(options);
// 协调者专属内容
const coordinatorSection = agentType === 'coordinator' ? `
## 多 Agent 协作
你是**协调者**,负责:
1. 与用户交流,理解需求
2. 将需求分解为子任务
3. 分发给专职 agent
4. 整合结果,交付给用户
**重要:** 读取 \`/home/admin/.openclaw/agents-registry.json\` 了解所有可用 agent
` : '';
// 专职 agent 专属内容
const specialistSection = agentType === 'specialist' ? `
## 多 Agent 协作
你是**专职 Agent**,负责:
1. 接收协调者 (\`${coordinator || 'game-director'}\`) 分配的任务
2. 执行专业领域的工作
3. 将结果返回给协调者
**重要:**
- 主要与 \`${coordinator || 'game-director'}\` 协作
- 读取 \`/home/admin/.openclaw/agents-registry.json\` 了解其他 agent
` : '';
return `# SOUL.md - ${agentName}
## 身份
- **名称:** ${agentName}
- **职责:** ${description || '帮助用户完成任务'}
- **类型:** ${displayType}${coordinatorSection}${specialistSection}
## 行为准则
1. 遵循 OpenClaw 规范
2. 保护用户隐私
3. 及时响应用户需求
4. 在职责范围内提供专业帮助
5. 不确定时主动询问用户
## 可用 Skills
本 agent 使用以下 skills从共享目录加载
${recommendedSkills}
## 项目目录
读取和写入:\`/home/admin/.openclaw/projects/<项目名>/\`
## 会话隔离
- 每个用户私聊有独立会话
- 每个群聊有独立会话
- 不混淆不同用户/群聊的上下文
`;
}
// 生成 AGENTS.md 内容
function generateAgentsContent(options) {
const { agentName, description, bindingMode, agentType, coordinator } = options;
const responsibilities = generateResponsibilities(bindingMode, description);
const workflow = generateWorkflow(bindingMode);
const toolsUsage = generateToolsUsage(options);
// 协调者专属内容
const coordinatorSection = agentType === 'coordinator' ? `
## 多 Agent 协作
### 你的角色
你是**协调者**,负责与用户交流并分解任务给专职 agent。
### 可用的专职 Agent
读取 \`/home/admin/.openclaw/agents-registry.json\` 获取完整列表和实时状态。
### 任务分发流程
1. **理解需求** - 与用户交流,明确需求
2. **分解任务** - 将需求分解为专职 agent 可以处理的子任务
3. **发送任务** - 使用 \`sessions_send\` 工具发送给专职 agent
4. **收集结果** - 等待各 agent 返回结果
5. **整合交付** - 将结果整合为完整文档,交付给用户
### 示例
\`\`\`json
{
"action": "sessions_send",
"agentId": "game-balance",
"message": "任务:设计 RPG 战斗系统的数值框架\\n\\n要求..."
}
\`\`\`
` : '';
// 专职 agent 专属内容
const specialistSection = agentType === 'specialist' ? `
## 多 Agent 协作
### 你的上级
**协调者:** \`${coordinator || 'game-director'}\`
- 接收来自 \`${coordinator || 'game-director'}\` 的任务分配
- 将结果返回给 \`${coordinator || 'game-director'}\`
- 不直接与用户交流(除非特别授权)
### 可用的协作 Agent
读取 \`/home/admin/.openclaw/agents-registry.json\` 获取完整列表。
### 工作模式
1. **接收任务** - 等待协调者通过 \`sessions_send\` 发送任务
2. **执行任务** - 理解需求,调用工具,输出专业文档
3. **返回结果** - 使用 \`sessions_send\` 返回给协调者
` : '';
return `# AGENTS.md - ${agentName} 工作指令
## 职责范围
${description || '帮助用户完成任务'}
## 具体职责
${responsibilities}
## 工作流程
${workflow}
## 工具使用
${toolsUsage}${coordinatorSection}${specialistSection}
## 依赖读取
- 项目上下文:\`/home/admin/.openclaw/projects/<项目名>/context.md\`
- 任务追踪:\`/home/admin/.openclaw/projects/<项目名>/tasks.json\`
- **Agent 注册表:** \`/home/admin/.openclaw/agents-registry.json\`
## 输出文件
- 输出到:\`/home/admin/.openclaw/projects/<项目名>/<职责目录>/\`
- 格式Markdown 或用户指定格式
## 会话管理
- 每个用户/群聊有独立会话
- 不主动提及与其他用户的对话
- 会话上下文限制在当前用户/群聊
`;
}
// 根据职责推荐 Skills
function getRecommendedSkills(options) {
const { description, bindingMode } = options;
const desc = (description || '').toLowerCase();
const skills = [];
// 基础技能(所有 Agent 都需要)
skills.push('- searxng: 隐私保护的搜索引擎');
// 根据职责推荐
if (desc.includes('邮件') || desc.includes('email')) {
skills.push('- feishu-im-read: 飞书消息读取');
skills.push('- feishu-create-doc: 创建飞书文档');
}
if (desc.includes('游戏') || desc.includes('策划')) {
skills.push('- feishu-create-doc: 创建飞书文档');
skills.push('- feishu-bitable: 多维表格管理');
}
if (desc.includes('数据') || desc.includes('处理')) {
skills.push('- feishu-bitable: 多维表格管理');
}
if (desc.includes('客服') || desc.includes('支持')) {
skills.push('- feishu-im-read: 飞书消息读取');
}
// 前台 Agent 需要 IM 技能
if (bindingMode !== 'none') {
skills.push('- feishu-im-read: 飞书消息读取');
}
// 去重
const uniqueSkills = [...new Set(skills)];
return uniqueSkills.join('\n');
}
// 生成具体职责
function generateResponsibilities(bindingMode, description) {
const responsibilities = {
none: `1. 接收其他 Agent 分配的任务
2. 执行专业领域的工作
3. 返回处理结果
4. 保持高效和准确`,
account: `1. 响应用户的飞书消息
2. 处理用户请求
3. 提供专业领域的帮助
4. 主动询问不清楚的需求`,
group: `1. 响应群聊中的消息
2. 协助群组讨论
3. 提供专业领域的建议
4. 促进团队协作`
};
return responsibilities[bindingMode] || responsibilities.account;
}
// 生成工作流程
function generateWorkflow(bindingMode) {
const workflows = {
none: `1. 接收其他 Agent 的任务分配
2. 分析任务需求和上下文
3. 调用相应工具或技能
4. 执行专业处理
5. 返回结果给调用方`,
account: `1. 接收用户的飞书消息
2. 分析用户意图和需求
3. 调用相应工具或技能
4. 提供解决方案或执行操作
5. 确认用户满意度`,
group: `1. 监听群聊消息
2. 识别需要响应的消息
3. 分析群组成员需求
4. 提供协助或建议
5. 促进讨论进展`
};
return workflows[bindingMode] || workflows.account;
}
// 生成工具使用说明
function generateToolsUsage(options) {
const { bindingMode, description } = options;
const desc = (description || '').toLowerCase();
let usage = `- 使用飞书工具时,遵循 feishu-* 技能规范
- 需要外部 API 时,先确认用户授权
- 访问项目目录时使用绝对路径`;
if (desc.includes('邮件') || desc.includes('email')) {
usage += `
- 处理邮件时注意隐私保护
- 不自动发送邮件,需用户确认`;
}
if (desc.includes('数据') || desc.includes('处理')) {
usage += `
- 处理数据时注意格式验证
- 重要数据操作前备份`;
}
if (bindingMode === 'none') {
usage += `
- 只响应授权 Agent 的调用
- 不直接处理用户消息`;
}
return usage;
}
// 配置飞书账户(整合 feishu-agent-binding 功能)
function configureFeishuAccount(options) {
if (!options.newBot) return true;
log.step(4, 6, '配置飞书机器人账户');
const config = loadConfig();
// 确保 channels.feishu 结构存在
if (!config.channels) config.channels = {};
if (!config.channels.feishu) config.channels.feishu = { enabled: true };
if (!config.channels.feishu.accounts) config.channels.feishu.accounts = {};
// ★ 重要:顶层配置必须设置 dmPolicy 和 allowFrom
// 原因:当 accountId === "default" 时getLarkAccount 不会合并 accounts.default 的覆盖值
// 所以顶层 channels.feishu 必须有 dmPolicy 和 allowFrom确保 default 账户正确继承
// 如果是第一个账户或者是 default 账户,设置顶层配置
const isDefaultBot = options.accountId === 'default' || options.accountId.startsWith('bot-');
const existingAccounts = Object.keys(config.channels.feishu.accounts);
const isFirstAccount = existingAccounts.length === 0;
if (isFirstAccount || isDefaultBot) {
// 设置顶层 dmPolicy 和 allowFrom默认 allowlist白名单用户
config.channels.feishu.dmPolicy = options.dmPolicy || 'allowlist';
config.channels.feishu.allowFrom = options.allowFrom || ['*'];
config.channels.feishu.groupPolicy = options.groupPolicy || 'open';
config.channels.feishu.groupAllowFrom = options.groupAllowFrom || [];
log.success(`已设置顶层飞书配置dmPolicy=${config.channels.feishu.dmPolicy}`);
}
// 添加账户级配置
// ★ 重要:如果没有显式设置 allowFrom继承顶层配置而不是 ['*']
const inheritedAllowFrom = config.channels.feishu.allowFrom || ['*'];
const inheritedGroupAllowFrom = config.channels.feishu.groupAllowFrom || [];
config.channels.feishu.accounts[options.accountId] = {
appId: options.appId,
appSecret: options.appSecret,
botName: options.botName || `${options.agentName}机器人`,
enabled: true,
domain: 'feishu',
connectionMode: 'websocket',
requireMention: true,
streaming: true,
// 账户级配置可以覆盖顶层设置
dmPolicy: options.dmPolicy || config.channels.feishu.dmPolicy || 'allowlist',
allowFrom: options.allowFrom || inheritedAllowFrom,
groupPolicy: options.groupPolicy || config.channels.feishu.groupPolicy || 'open',
groupAllowFrom: options.groupAllowFrom || inheritedGroupAllowFrom
};
if (saveConfig(config)) {
log.success(`已添加飞书账户:${options.accountId}`);
return true;
}
return false;
}
// 更新共享 Agent 注册表
function updateAgentsRegistry(options) {
const registryPath = path.join(process.env.HOME, '.openclaw', 'agents-registry.json');
let registry = { version: 1, agents: [] };
try {
if (fs.existsSync(registryPath)) {
registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
}
// 检查 agent 是否已存在
const existingIndex = registry.agents.findIndex(a => a.id === options.agentId);
const agentEntry = {
id: options.agentId,
name: options.agentName || options.agentId,
description: options.description || `${options.agentName || options.agentId} agent`,
workspace: `/home/admin/.openclaw/workspace-${options.agentId}/`,
type: options.agentType || (options.bindingMode === 'none' ? 'backend' : 'general'),
coordinator: options.coordinator || null,
capabilities: options.capabilities ? options.capabilities.split(',').map(s => s.trim()) : [],
binding: options.bindingMode !== 'none' ? {
channel: 'feishu',
accountId: options.accountId,
...(options.bindingMode === 'group' ? { peer: { kind: 'group', id: options.chatId } } : {})
} : null,
createdAt: existingIndex >= 0 ? registry.agents[existingIndex].createdAt : new Date().toISOString()
};
if (existingIndex >= 0) {
registry.agents[existingIndex] = agentEntry;
log.success(`已更新 Agent 注册表:${options.agentId}`);
} else {
registry.agents.push(agentEntry);
log.success(`已添加 Agent 到注册表:${options.agentId}`);
}
registry.updatedAt = new Date().toISOString();
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2), 'utf8');
} catch (err) {
log.error(`更新 Agent 注册表失败:${err.message}`);
log.warning('Agent 已创建,但注册表未更新');
}
}
// 更新配置(添加 agent + bindings
function updateConfig(options) {
log.step(3, 6, '更新配置');
const config = loadConfig();
// 0. 验证accountId 唯一性
if (options.newBot || options.accountId) {
const accountIdValidation = validateAccountIdUnique(config, options.accountId);
if (!accountIdValidation.valid) {
log.error(accountIdValidation.error);
return false;
}
}
// 0b. 验证binding 不重复
if (options.bindingMode !== 'none') {
const bindingValidation = validateBindingUnique(
config,
options.agentId,
options.accountId,
options.chatId,
options.bindingMode
);
if (!bindingValidation.valid) {
log.error(bindingValidation.error);
return false;
}
}
// 1. 添加 agent 到 agents.list
if (!config.agents) config.agents = {};
if (!config.agents.list) config.agents.list = [];
const agentExists = config.agents.list.find(a => a.id === options.agentId);
if (!agentExists) {
config.agents.list.push({
id: options.agentId,
name: options.agentName || options.agentId,
workspace: `/home/admin/.openclaw/workspace-${options.agentId}`
});
log.success(`已添加 agent 到 agents.list`);
} else {
log.warning(`Agent ${options.agentId} 已存在`);
}
// 1b. 更新共享 Agent 注册表agents-registry.json
updateAgentsRegistry(options);
// 2. 添加 bindings 规则
if (!config.bindings) config.bindings = [];
if (options.bindingMode !== 'none') {
const binding = {
agentId: options.agentId,
match: {
channel: 'feishu'
}
};
if (options.bindingMode === 'account') {
binding.match.accountId = options.accountId;
log.success(`已添加账户级绑定:${options.agentId}${options.accountId}`);
} else if (options.bindingMode === 'group') {
binding.match.peer = {
kind: 'group',
id: options.chatId
};
log.success(`已添加群聊级绑定:${options.agentId}${options.chatId}`);
}
config.bindings.push(binding);
}
// 3. 配置会话隔离
if (!config.session) config.session = {};
config.session.dmScope = 'per-account-channel-peer';
log.success(`已配置会话隔离per-account-channel-peer`);
// 4. 启用 agent 间调用
if (!config.tools) config.tools = {};
if (!config.tools.agentToAgent) config.tools.agentToAgent = {};
config.tools.agentToAgent.enabled = true;
if (!config.tools.agentToAgent.allow) {
config.tools.agentToAgent.allow = [];
}
if (!config.tools.agentToAgent.allow.includes(options.agentId)) {
config.tools.agentToAgent.allow.push(options.agentId);
}
log.success(`已启用 agent 间调用`);
if (saveConfig(config)) {
log.success('配置已更新');
return true;
}
return false;
}
// 显示预览
function showPreview(options) {
log.step(5, 6, '预览配置');
console.log(`
${colors.bold}即将执行以下配置变更:${colors.reset}
${colors.bold}=== Agent 配置 ===${colors.reset}
新增 agent:
id: ${options.agentId}
name: ${options.agentName || options.agentId}
workspace: /home/admin/.openclaw/workspace-${options.agentId}
${colors.bold}=== 绑定配置 ===${colors.reset}
绑定模式:${options.bindingMode}
${options.bindingMode === 'account' ? `飞书账户:${options.accountId}` : ''}
${options.bindingMode === 'group' ? `群聊 ID: ${options.chatId}` : ''}
${options.newBot ? `新机器人:${options.botName || options.agentName}机器人` : ''}
${options.newBot ? `App ID: ${options.appId}` : ''}
${colors.bold}=== 系统配置 ===${colors.reset}
- 会话隔离per-account-channel-peer
- agent 间调用:已启用
- 工作空间目录:将自动创建
${colors.yellow}即将执行的操作:${colors.reset}
1. 创建 Agent 工作空间
2. 备份当前配置
3. 更新 openclaw.json添加 agent + bindings
${options.newBot ? '4. 配置飞书机器人账户channels.feishu.accounts' : ''}
${options.newBot ? '5. ' : '4. '}重启 Gateway
`);
}
// 重启 Gateway
function restartGateway() {
log.step(6, 6, '重启 Gateway');
try {
execSync('openclaw gateway restart', { stdio: 'inherit' });
log.success('Gateway 重启完成');
return true;
} catch (err) {
log.error(`重启失败:${err.message}`);
log.info('请手动执行openclaw gateway restart');
return false;
}
}
// 显示路由方案说明
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 interactiveMode() {
console.log(`\n${colors.cyan}🤖 Agent 创建 + 飞书绑定助手${colors.reset}\n`);
// 步骤 1Agent 基础信息
log.step(1, 6, 'Agent 基础信息');
const agentId = await question('Agent ID (如 email-assistant): ');
if (!validateAgentId(agentId)) {
log.error('Agent ID 格式无效,只能使用小写字母、数字和连字符');
rl.close();
process.exit(1);
}
const agentName = await question(`Agent 名称(默认:${agentId}: `) || agentId;
const description = await question('Agent 描述(可选): ');
// 步骤 2选择绑定模式
log.step(2, 6, '选择绑定模式');
showRoutingOptions();
const modeChoice = await question('选择A/B/C: ');
const bindingMode = modeChoice.toLowerCase() === 'a' ? 'account'
: modeChoice.toLowerCase() === 'b' ? 'group' : 'none';
// 步骤 2b选择 Agent 类型(多 Agent 协作)
log.step(2.5, 6, '选择 Agent 类型');
console.log('\nAgent 类型说明:');
console.log(' - general: 通用 Agent独立工作默认');
console.log(' - coordinator: 协调者,负责分解任务给其他 agent');
console.log(' - specialist: 专职 agent接收协调者分配的任务');
const typeChoice = await question('选择 Agent 类型general/coordinator/specialist默认 general: ') || 'general';
const agentType = typeChoice.toLowerCase();
let coordinator = null;
if (agentType === 'specialist') {
coordinator = await question('你的协调者 Agent ID如 game-director: ');
}
// 步骤 3根据绑定模式收集信息
let options = { agentId, agentName, description, bindingMode, agentType, coordinator };
if (bindingMode === 'account') {
log.step(3, 6, '配置飞书机器人');
const newBotChoice = await question('是否需要新飞书机器人?(y/n): ');
options.newBot = newBotChoice.toLowerCase() === 'y';
if (options.newBot) {
const appId = await question('App ID (cli_xxx): ');
if (!validateAppId(appId)) {
log.error('App ID 格式无效');
rl.close();
process.exit(1);
}
options.appId = appId;
const appSecret = await question('App Secret: ');
options.appSecret = appSecret;
options.accountId = await question(`账户 ID默认bot-${agentId}: `) || `bot-${agentId}`;
options.botName = await question(`机器人名称(默认:${agentName}机器人): `) || `${agentName}机器人`;
// DM 策略
console.log('\nDM 策略说明:');
console.log(' - allowlist: 仅允许白名单用户私聊(推荐,安全)');
console.log(' - open: 允许所有用户私聊');
console.log(' - pairing: 需要配对码才能访问');
const dmPolicy = await question(`DM 策略allowlist/open/pairing默认 allowlist: `) || 'allowlist';
options.dmPolicy = dmPolicy;
// 白名单用户(仅当 allowlist 时需要)
if (dmPolicy === 'allowlist') {
console.log('\n白名单用户说明');
console.log(' - 输入用户 open_id格式ou_xxx');
console.log(' - 多个用户用逗号分隔');
console.log(' - 留空则允许所有人(不推荐)');
const allowFromInput = await question('白名单用户(逗号分隔,默认:*: ') || '*';
options.allowFrom = allowFromInput.split(',').map(s => s.trim()).filter(Boolean);
}
} else {
// 列出飞书账户供选择
log.info('可用飞书账户:');
try {
const config = loadConfig();
const accounts = config.channels?.feishu?.accounts || {};
const accountIds = Object.keys(accounts);
if (accountIds.length === 0) {
log.warning('没有找到现有飞书账户,将创建新账户');
options.newBot = true;
const appId = await question('App ID (cli_xxx): ');
options.appId = appId;
const appSecret = await question('App Secret: ');
options.appSecret = appSecret;
options.accountId = await question(`账户 ID默认bot-${agentId}: `) || `bot-${agentId}`;
options.botName = await question(`机器人名称(默认:${agentName}机器人): `) || `${agentName}机器人`;
options.dmPolicy = 'allowlist';
options.allowFrom = ['*'];
} else {
accountIds.forEach((id, index) => {
console.log(` ${index + 1}. ${id} (${accounts[id].botName || '未命名'})`);
});
const choice = await question(`选择账户1-${accountIds.length} 或输入账户 ID: `);
const choiceNum = parseInt(choice);
if (!isNaN(choiceNum) && choiceNum >= 1 && choiceNum <= accountIds.length) {
options.accountId = accountIds[choiceNum - 1];
} else {
options.accountId = choice;
}
options.newBot = false;
}
} catch (err) {
log.error(`读取账户列表失败:${err.message}`);
options.accountId = await question('现有账户 ID: ');
options.newBot = false;
}
}
} else if (bindingMode === 'group') {
log.step(3, 6, '配置群聊绑定');
options.accountId = await question('使用哪个飞书账户?: ');
const chatId = await question('群聊 ID (oc_xxx): ');
if (!validateChatId(chatId)) {
log.error('群聊 ID 格式无效');
rl.close();
process.exit(1);
}
options.chatId = chatId;
options.newBot = false;
} else {
options.newBot = false;
}
// 步骤 4会话隔离配置
log.step(4, 6, '会话隔离配置');
console.log('推荐配置per-account-channel-peer每个用户/群聊在每个机器人有独立会话)');
await question('按 Enter 继续: ');
// 步骤 5预览配置
showPreview(options);
// 步骤 6用户确认
const confirm = await question('确认执行?(y/n): ');
if (confirm.toLowerCase() !== 'y') {
log.info('已取消');
rl.close();
process.exit(0);
}
rl.close();
// 执行配置
console.log('');
// 步骤 1先备份在任何修改之前
const backupPath = createBackup();
if (!backupPath) {
log.error('备份失败,无法继续');
process.exit(1);
}
// 步骤 2创建 Agent
const agentCreated = createAgent(agentId);
if (!agentCreated) {
log.error('创建 Agent 失败,正在回退...');
rollback(backupPath);
process.exit(1);
}
// 步骤 2.5:生成 Agent 人设文件
const filesGenerated = generateAgentFiles({
agentId,
agentName,
description,
bindingMode
});
if (!filesGenerated) {
log.warning('人设文件生成失败,但 Agent 已创建');
// 不回退,继续执行
}
// 步骤 3更新配置包含验证
const configUpdated = updateConfig(options);
if (!configUpdated) {
log.error('更新配置失败(验证未通过),正在回退...');
cleanupAgent(agentId);
rollback(backupPath);
rollbackRegistry(backupPath);
process.exit(1);
}
// 步骤 4配置飞书账户如需要
if (options.newBot) {
const feishuConfigured = configureFeishuAccount(options);
if (!feishuConfigured) {
log.error('配置飞书账户失败,正在回退...');
cleanupAgent(agentId);
rollback(backupPath);
rollbackRegistry(backupPath);
log.warning('openclaw.json 已回退,但飞书侧配置可能需要手动清理');
process.exit(1);
}
}
// 步骤 5重启 Gateway
const gatewayRestarted = restartGateway();
if (!gatewayRestarted) {
log.error('Gateway 重启失败');
log.warning('配置已保存,但 Gateway 未重启');
log.info('请手动执行openclaw gateway restart');
// 不回退,因为配置是正确的
}
// 完成提示
console.log(`\n${'─'.repeat(60)}`);
log.success('配置完成!');
console.log(`\n验证命令openclaw agents list --bindings`);
console.log(`\n如配置有误,可从备份恢复:`);
console.log(` cp ${backupPath} ${CONFIG_PATH}`);
console.log(` openclaw gateway restart`);
console.log(`${'─'.repeat(60)}\n`);
}
// 快速模式(命令行)
function quickMode(options) {
if (!options.agentid) {
log.error('需要提供 --agent-id');
showRoutingOptions();
console.log(`
${colors.bold}用法:${colors.reset}
openclaw skills run agent-creator-with-binding [选项]
${colors.bold}选项:${colors.reset}
--agent-id <id> Agent 唯一标识(必填)
--agent-name <name> Agent 显示名称
--binding-mode <mode> 绑定模式account/group/none
--new-bot 是否创建新飞书机器人
--app-id <id> 飞书 App ID新机器人时需要
--app-secret <secret> 飞书 App Secret新机器人时需要
--account-id <id> 飞书账户 ID
--chat-id <id> 群聊 ID群聊绑定时需要
--bot-name <name> 机器人名称
--dm-policy <policy> DM 策略allowlist/open/pairing默认 allowlist
--allow-from <users> 白名单用户逗号分隔ou_xxx,ou_yyy
--group-policy <policy> 群聊策略open/allowlist/disabled默认 open
--agent-type <type> Agent 类型coordinator/specialist/general默认 general
--coordinator <agent-id> 协调者 IDspecialist 类型时需要)
--skip-confirm 跳过确认
--skip-restart 跳过重启
${colors.bold}示例:${colors.reset}
# 账户级绑定(新机器人)
openclaw skills run agent-creator-with-binding -- \\
--agent-id email-assistant \\
--agent-name "邮件助手" \\
--binding-mode account \\
--new-bot \\
--app-id cli_xxx \\
--app-secret yyy
# 账户级绑定(现有机器人)
openclaw skills run agent-creator-with-binding -- \\
--agent-id email-assistant \\
--binding-mode account \\
--account-id bot-existing
# 群聊级绑定
openclaw skills run agent-creator-with-binding -- \\
--agent-id project-assistant \\
--binding-mode group \\
--account-id bot-main \\
--chat-id oc_xxx
# 不绑定(后台 Agent
openclaw skills run agent-creator-with-binding -- \\
--agent-id data-processor \\
--binding-mode none
`);
process.exit(1);
}
console.log(`\n${colors.cyan}🤖 Agent 创建 + 飞书绑定助手${colors.reset}\n`);
const agentOptions = {
agentId: options.agentid,
agentName: options.agentname || options.agentid,
description: options.description,
bindingMode: options.bindingmode || 'account',
agentType: options.agenttype || 'general',
coordinator: options.coordinator,
newBot: options.newbot === 'true',
appId: options.appid,
appSecret: options.appsecret,
accountId: options.accountid || `bot-${options.agentid}`,
botName: options.botname,
dmPolicy: options.dmpolicy || 'allowlist',
allowFrom: options.allowfrom ? options.allowfrom.split(',').map(s => s.trim()) : undefined,
groupPolicy: options.grouppolicy || 'open',
groupAllowFrom: options.groupallowfrom ? options.groupallowfrom.split(',').map(s => s.trim()) : undefined,
chatId: options.chatid
};
// 验证
if (!validateAgentId(agentOptions.agentId)) {
log.error('Agent ID 格式无效');
process.exit(1);
}
if (agentOptions.bindingMode === 'account' && agentOptions.newBot) {
if (!agentOptions.appId || !agentOptions.appSecret) {
log.error('新机器人需要提供 --app-id 和 --app-secret');
process.exit(1);
}
if (!validateAppId(agentOptions.appId)) {
log.error('App ID 格式无效');
process.exit(1);
}
}
if (agentOptions.bindingMode === 'group' && !agentOptions.chatId) {
log.error('群聊绑定需要提供 --chat-id');
process.exit(1);
}
// 预览
if (!options.skipconfirm) {
showPreview(agentOptions);
console.log('\n');
}
// 执行
console.log('');
// 步骤 1先备份在任何修改之前
const backupPath = createBackup();
if (!backupPath) {
log.error('备份失败,无法继续');
process.exit(1);
}
// 步骤 2创建 Agent
const agentCreated = createAgent(agentOptions.agentId);
if (!agentCreated) {
log.error('创建 Agent 失败,正在回退...');
rollback(backupPath);
process.exit(1);
}
// 步骤 2.5:生成 Agent 人设文件
const filesGenerated = generateAgentFiles(agentOptions);
if (!filesGenerated) {
log.warning('人设文件生成失败,但 Agent 已创建');
// 不回退,继续执行
}
// 步骤 3更新配置
const configUpdated = updateConfig(agentOptions);
if (!configUpdated) {
log.error('更新配置失败,正在回退...');
cleanupAgent(agentOptions.agentId);
rollback(backupPath);
process.exit(1);
}
// 步骤 4配置飞书账户如需要
if (agentOptions.newBot) {
const feishuConfigured = configureFeishuAccount(agentOptions);
if (!feishuConfigured) {
log.error('配置飞书账户失败,正在回退...');
cleanupAgent(agentOptions.agentId);
rollback(backupPath);
log.warning('openclaw.json 已回退,但飞书侧配置可能需要手动清理');
process.exit(1);
}
}
// 步骤 5重启 Gateway
if (!options.skiprestart) {
const gatewayRestarted = restartGateway();
if (!gatewayRestarted) {
log.error('Gateway 重启失败');
log.warning('配置已保存,但 Gateway 未重启');
log.info('请手动执行openclaw gateway restart');
}
}
// 完成提示
console.log(`\n${'─'.repeat(60)}`);
log.success('配置完成!');
console.log(`\n验证命令openclaw agents list --bindings`);
if (backupPath) {
console.log(`\n如配置有误,可从备份恢复:`);
console.log(` cp ${backupPath} ${CONFIG_PATH}`);
}
console.log(`${'─'.repeat(60)}\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 (Object.keys(options).length > 0) {
quickMode(options);
} else {
interactiveMode().then(() => {
// Done
});
}