- 备份 agents-registry.json(与 openclaw.json 同时备份) - 验证 accountId 唯一性,防止覆盖现有配置 - 验证 bindings 重复,防止重复绑定 - 回退 agents-registry.json(失败时恢复) - 继承顶层 allowFrom 配置(安全优先) - 创建共享 skills 目录(符号链接) - 清理符号链接(回退时) - 新增 AUDIT-REPORT.md 审查报告
1255 lines
40 KiB
JavaScript
1255 lines
40 KiB
JavaScript
#!/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`);
|
||
|
||
// 步骤 1:Agent 基础信息
|
||
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> 协调者 ID(specialist 类型时需要)
|
||
--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
|
||
});
|
||
}
|