diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..93c69e4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,137 @@ +# 更新日志 + +## v5.1 - 2026-03-14 + +### ✨ 新增功能 + +#### `/config` 命令增强 +- ✅ `add` - 添加存储桶配置 + ```bash + /config add <名称> + 示例:/config add mybucket xxxxxx yyyyyy my-bucket z0 https://cdn.example.com + ``` +- ✅ `remove` - 删除存储桶配置(带引用检查) + ```bash + /config remove <名称> + ``` +- ✅ `list` - 查看所有存储桶(已有) +- ✅ `set` - 修改配置项(已有) + +**保护机制:** +- ⚠️ 不能删除 `default` 存储桶 +- ⚠️ 删除前检查是否有上传配置引用该存储桶 + +--- + +#### `/path` 命令增强 +- ✅ `add` - 添加预设路径(已有) + ```bash + /path add <名称> <路径> + 示例:/path add backup /backup/ + ``` +- ✅ `remove` - 删除预设路径(带引用检查) + ```bash + /path remove <名称> + ``` +- ✅ `list` - 查看所有预设路径(已有) + +**保护机制:** +- ⚠️ 删除前检查是否有上传配置引用该路径 + +--- + +#### `/profile` 命令增强 +- ✅ `add` - 添加上传配置模板(已有) + ```bash + /profile add <名称> <存储桶> [路径键名] + 示例:/profile add IPA 上传 default ipa + ``` +- ✅ `remove` - 删除上传配置模板(已有) + ```bash + /profile remove <名称> + ``` +- ✅ `list` - 查看所有上传配置模板(已有) + +**验证机制:** +- ✅ 添加时验证存储桶是否存在 +- ✅ 添加时验证路径键名是否存在(如果提供) + +--- + +### 📖 文档更新 + +#### 帮助卡片全面升级 +- 详细说明了所有命令的用法 +- 添加了完整的示例 +- 增加了注意事项说明 +- 优化了卡片布局和可读性 + +#### README.md 更新 +- 补充了完整的命令表格 +- 添加了每个命令的详细示例 +- 分类整理了上传命令、存储桶配置、预设路径、上传配置模板 + +--- + +### 🛡️ 安全增强 + +1. **引用检查** - 删除存储桶/路径前自动检查是否被上传配置引用 +2. **默认保护** - 禁止删除 default 存储桶 +3. **参数验证** - 添加存储桶时验证区域代码 +4. **存在性检查** - 添加上传配置时验证存储桶和路径是否存在 + +--- + +### 📝 使用示例 + +#### 完整配置流程 +```bash +# 1. 添加存储桶 +/config add production xxxxxx yyyyyy prod-bucket z0 https://cdn.example.com + +# 2. 添加预设路径 +/path add app /app/ +/path add backup /backup/ + +# 3. 创建上传配置 +/profile add 生产环境上传 production app +/profile add 备份上传 production backup + +# 4. 查看配置 +/config list +/path list +/profile list + +# 5. 开始上传 +/upload +``` + +#### 删除配置 +```bash +# 删除上传配置 +/profile remove 备份上传 + +# 删除预设路径 +/path remove backup + +# 删除存储桶(需确保没有被引用) +/config remove production +``` + +--- + +### 🐛 已知问题 + +无 + +--- + +### 📦 升级方式 + +```bash +# 重启服务 +pm2 restart qiniu-bot + +# 查看日志 +pm2 logs qiniu-bot +``` diff --git a/README.md b/README.md index 886149d..4f7dbe2 100644 --- a/README.md +++ b/README.md @@ -197,16 +197,57 @@ NODE_ENV=production ### 常用命令 +#### 📤 上传命令 + | 命令 | 说明 | |------|------| -| `/upload` | 开始上传流程 | -| `/config list` | 查看存储桶配置 | -| `/path list` | 查看预设路径 | +| `/upload` 或 `/u` | 开始上传流程 | +| `/help` 或 `/qh` | 查看详细帮助 | + +#### ⚙️ 存储桶配置 (`/config` 或 `/qc`) + +| 命令 | 说明 | +|------|------| +| `/config list` | 查看所有存储桶配置 | +| `/config add <名称> ` | 添加存储桶配置 | +| `/config remove <名称>` | 删除存储桶配置 | +| `/config set <键路径> <值>` | 修改配置项 | + +**示例:** +```bash +/config add mybucket xxxxxx yyyyyy my-bucket z0 https://cdn.example.com +/config remove mybucket +``` + +#### 📁 预设路径 (`/path`) + +| 命令 | 说明 | +|------|------| +| `/path list` | 查看所有预设路径 | | `/path add <名称> <路径>` | 添加预设路径 | -| `/profile list` | 查看上传配置模板 | -| `/profile add <名称> <桶> [路径]` | 添加上传配置 | -| `/profile remove <名称>` | 删除上传配置 | -| `/help` | 查看详细帮助 | +| `/path remove <名称>` | 删除预设路径 | + +**示例:** +```bash +/path add backup /backup/ +/path add ipa /ipa/gamehall_jinxian2.ipa +/path remove backup +``` + +#### 📤 上传配置模板 (`/profile`) + +| 命令 | 说明 | +|------|------| +| `/profile list` | 查看所有上传配置模板 | +| `/profile add <名称> <存储桶> [路径键名]` | 添加上传配置模板 | +| `/profile remove <名称>` | 删除上传配置模板 | + +**示例:** +```bash +/profile add 默认上传 default +/profile add IPA 上传 default ipa +/profile remove IPA 上传 +``` ### 使用流程 diff --git a/src/index.js b/src/index.js index aadb116..d6e214f 100644 --- a/src/index.js +++ b/src/index.js @@ -530,10 +530,76 @@ async function handleConfigCommandV2(message, content, feishu, uploader) { const args = text.replace(/^\/(config|qc)\s*/i, '').trim().split(/\s+/); const subCommand = args[0]; + const configPath = path.join(process.cwd(), 'config', 'qiniu-config.json'); + try { + const fullConfig = loadFullConfig(); + if (subCommand === 'list' || !subCommand) { const configData = await uploader.listConfig(); await feishu.sendCard(chatId, createBucketsListCard(configData)); + } else if (subCommand === 'add') { + // /config add <名称> + if (args.length < 7) { + throw new Error('用法:/config add <名称> \n示例:/config add mybucket xxxxxx yyyyyy my-bucket z0 https://cdn.example.com'); + } + const name = args[1]; + const accessKey = args[2]; + const secretKey = args[3]; + const bucket = args[4]; + const region = args[5]; + const domain = args[6]; + + // 验证区域代码 + const validRegions = ['z0', 'z1', 'z2', 'na0', 'as0']; + if (!validRegions.includes(region)) { + throw new Error(`无效的区域代码,可用:${validRegions.join(', ')}`); + } + + fullConfig.buckets[name] = { + accessKey, + secretKey, + bucket, + region, + domain + }; + + fs.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2)); + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `✅ 已添加存储桶配置:**${name}**\n存储桶:${bucket}\n区域:${region}\n域名:${domain}` } + }); + } else if (subCommand === 'remove' || subCommand === 'del') { + if (args.length < 2) { + throw new Error('用法:/config remove <名称>'); + } + const name = args[1]; + + if (!fullConfig.buckets[name]) { + throw new Error(`存储桶 "${name}" 不存在,可用:${Object.keys(fullConfig.buckets).join(', ')}`); + } + + // 不允许删除 default 存储桶 + if (name === 'default') { + throw new Error('不能删除 default 存储桶'); + } + + // 检查是否有上传配置引用此存储桶 + const profiles = fullConfig.uploadProfiles || {}; + const referencedBy = Object.entries(profiles) + .filter(([_, config]) => config.bucket === name) + .map(([name, _]) => name); + + if (referencedBy.length > 0) { + throw new Error(`无法删除:存储桶 "${name}" 被以下上传配置引用:${referencedBy.join(', ')}\n请先删除或修改这些上传配置`); + } + + delete fullConfig.buckets[name]; + fs.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2)); + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `✅ 已删除存储桶配置:**${name}**` } + }); } else if (subCommand === 'set') { const [keyPath, value] = args.slice(1); await uploader.setConfigValue(keyPath, value); @@ -541,11 +607,13 @@ async function handleConfigCommandV2(message, content, feishu, uploader) { msg_type: 'text', content: { text: `✅ 已设置 ${keyPath} = ${value}` } }); + } else { + throw new Error(`未知命令:${subCommand}\n可用命令:list, add, remove, set`); } } catch (error) { await feishu.sendMessage(chatId, { msg_type: 'text', - content: { text: `❌ 配置失败:${error.message}` } + content: { text: `❌ 配置管理失败:${error.message}` } }); } } @@ -561,18 +629,51 @@ async function handlePathCommandV2(message, content, feishu) { try { const fullConfig = loadFullConfig(); - if (subCommand === 'list') { + if (subCommand === 'list' || !subCommand) { const paths = fullConfig.uploadPaths || {}; await feishu.sendCard(chatId, createPathsListCard(paths)); } else if (subCommand === 'add') { + if (args.length < 3) { + throw new Error('用法:/path add <名称> <路径>\n示例:/path add backup /backup/'); + } const name = args[1]; const pathValue = args[2]; + + fullConfig.uploadPaths = fullConfig.uploadPaths || {}; fullConfig.uploadPaths[name] = pathValue; fs.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2)); await feishu.sendMessage(chatId, { msg_type: 'text', content: { text: `✅ 已添加预设路径:**${name}** → ${pathValue}` } }); + } else if (subCommand === 'remove' || subCommand === 'del') { + if (args.length < 2) { + throw new Error('用法:/path remove <名称>'); + } + const name = args[1]; + + if (!fullConfig.uploadPaths || !fullConfig.uploadPaths[name]) { + throw new Error(`预设路径 "${name}" 不存在,可用:${Object.keys(fullConfig.uploadPaths || {}).join(', ') || '无'}`); + } + + // 检查是否有上传配置引用此路径 + const profiles = fullConfig.uploadProfiles || {}; + const referencedBy = Object.entries(profiles) + .filter(([_, config]) => config.path === name) + .map(([pname, _]) => pname); + + if (referencedBy.length > 0) { + throw new Error(`无法删除:路径 "${name}" 被以下上传配置引用:${referencedBy.join(', ')}\n请先删除或修改这些上传配置`); + } + + delete fullConfig.uploadPaths[name]; + fs.writeFileSync(configPath, JSON.stringify(fullConfig, null, 2)); + await feishu.sendMessage(chatId, { + msg_type: 'text', + content: { text: `✅ 已删除预设路径:**${name}**` } + }); + } else { + throw new Error(`未知命令:${subCommand}\n可用命令:list, add, remove`); } } catch (error) { await feishu.sendMessage(chatId, { @@ -679,7 +780,7 @@ async function handleHelpCommandV2(chatId, feishu) { tag: 'div', text: { tag: 'lark_md', - content: '**📤 上传方式**\n\n**方式 1:选择配置 → 发送文件**\n1️⃣ 发送 /upload\n2️⃣ 选择上传配置\n3️⃣ 发送文件\n4️⃣ 确认上传\n\n**方式 2:发送文件 → 选择配置**\n1️⃣ 直接发送文件\n2️⃣ 选择上传配置\n3️⃣ 确认上传' + content: '**📤 上传方式**\n\n**方式 1:选择配置 → 发送文件**\n1️⃣ 发送 `/upload`\n2️⃣ 选择上传配置\n3️⃣ 发送文件\n4️⃣ 确认上传\n\n**方式 2:发送文件 → 选择配置**\n1️⃣ 直接发送文件\n2️⃣ 选择上传配置\n3️⃣ 确认上传' } }, { @@ -689,7 +790,7 @@ async function handleHelpCommandV2(chatId, feishu) { tag: 'div', text: { tag: 'lark_md', - content: '**⚙️ 配置命令**\n\n• /config list - 查看存储桶\n• /profile list - 查看上传配置\n• /profile add <名称> <桶> [路径] - 添加配置\n• /profile remove <名称> - 删除配置\n\n**📁 路径命令**\n\n• /path list - 查看预设路径\n• /path add <名称> <路径> - 添加路径' + content: '**⚙️ 存储桶配置 (/config)**\n\n• `/config list` - 查看所有存储桶配置\n• `/config add <名称> ` - 添加存储桶\n• `/config remove <名称>` - 删除存储桶\n• `/config set <键路径> <值>` - 修改配置项\n\n**示例:**\n`/config add mybucket xxxxxx yyyyyy my-bucket z0 https://cdn.example.com`' } }, { @@ -699,7 +800,27 @@ async function handleHelpCommandV2(chatId, feishu) { tag: 'div', text: { tag: 'lark_md', - content: '**💡 示例**\n\n`/profile add IPA 上传 default ipa`\n`/path add backup /backup/`\n\n**提示:** 上传同名文件会自动覆盖' + content: '**📁 预设路径 (/path)**\n\n• `/path list` - 查看所有预设路径\n• `/path add <名称> <路径>` - 添加预设路径\n• `/path remove <名称>` - 删除预设路径\n\n**示例:**\n`/path add backup /backup/`\n`/path add ipa /ipa/gamehall_jinxian2.ipa`' + } + }, + { + tag: 'hr' + }, + { + tag: 'div', + text: { + tag: 'lark_md', + content: '**📤 上传配置 (/profile)**\n\n• `/profile list` - 查看所有上传配置模板\n• `/profile add <名称> <存储桶> [路径键名]` - 添加上传配置\n• `/profile remove <名称>` - 删除上传配置\n\n**示例:**\n`/profile add 默认上传 default`\n`/profile add IPA 上传 default ipa`' + } + }, + { + tag: 'hr' + }, + { + tag: 'div', + text: { + tag: 'lark_md', + content: '**💡 快捷命令**\n\n• `/upload` 或 `/u` - 开始上传\n• `/help` 或 `/qh` - 显示帮助\n• `/config` 或 `/qc` - 配置管理\n\n**⚠️ 注意事项**\n• 删除存储桶/路径前需确保没有被上传配置引用\n• 不能删除 default 存储桶\n• 上传同名文件会自动覆盖' } }, {