refactor: Webhook 移入系统设置「消息通知」Tab,取消独立菜单

- WebhookConfig 页面转为可嵌入卡片组件 WebhookManager
  (标准设置卡样式,逻辑不变),渲染于飞书通知卡下方
- 移除侧边栏「Webhook」菜单项、/webhooks 路由与页面文件
  (旧地址自动回退流水线页)
- 使用指南/接口文档同步更新 Webhook 配置入口描述
This commit is contained in:
Logan.Li committed 2026-10-27 14:53:00 +08:00
1 parent ed88a18e03
commit 695c1c2357
6 files changed
+22 -23

No files matched your search

+1 -1
View File
@@ -85,7 +85,7 @@ curl -X POST http://localhost:9527/api/ai/release \
| `/api/build-nodes/:id/disk-test` | POST | dd 磁盘性能实测(256MB,本机 exec 或 SSH 远程;返回读写 MB/s) | | `/api/build-nodes/:id/disk-test` | POST | dd 磁盘性能实测(256MB,本机 exec 或 SSH 远程;返回读写 MB/s) |
| `/api/tools` | GET/PUT | 工具清单与安装策略 | | `/api/tools` | GET/PUT | 工具清单与安装策略 |
| `/api/tools/:id/check` | POST | 真实执行版本探测命令 | | `/api/tools/:id/check` | POST | 真实执行版本探测命令 |
| `/api/webhooks` | CRUD + `/:id/test` | 出站 Webhook(HMAC-SHA256 签名发送) | | `/api/webhooks` | CRUD + `/:id/test` | 出站 Webhook(HMAC-SHA256 签名发送,配置入口:系统设置→消息通知) |
| `/api/hooks/pipeline/:id?token=` | POST | 入站触发流水线(免鉴权,token 校验) | | `/api/hooks/pipeline/:id?token=` | POST | 入站触发流水线(免鉴权,token 校验) |
## Jenkins ## Jenkins
+4 -2
View File
@@ -89,13 +89,15 @@ cex-agent-admin
- 「检测」真实执行版本探测命令(如 `java -version`);「安装」是保守策略——只检测不自动下载 - 「检测」真实执行版本探测命令(如 `java -version`);「安装」是保守策略——只检测不自动下载
- `deploy/Dockerfile.buildnode` 一键构建 Ubuntu 构建节点镜像 - `deploy/Dockerfile.buildnode` 一键构建 Ubuntu 构建节点镜像
## 飞书 / Lark 通知 ## 飞书 / Lark 通知与 Webhook
**是什么**:发布成功/失败自动向群推送结果卡片(应用/分支/构建/状态/时间 + 「查看构建详情」按钮直达 Jenkins)。 **是什么**:发布成功/失败自动向群推送结果卡片(应用/分支/构建/状态/时间 + 「查看构建详情」按钮直达 Jenkins)。
**怎么用**:系统设置 → 消息通知 → 填群机器人 Webhook(和可选的签名密钥)→「发送测试卡片」验证。 **怎么用**:系统设置 → 消息通知 → 填群机器人 Webhook(和可选的签名密钥)→「发送测试卡片」验证。
**技巧**:单条流水线可在「基本信息」里单独填通知 Webhook,优先于全局配置。 **技巧**:
- 单条流水线可在「基本信息」里单独填通知 Webhook,优先于全局配置
- 通用 Webhook 回调(发给第三方系统,HMAC-SHA256 签名)也在系统设置 → 消息通知里配置
## 用户与 LDAP ## 用户与 LDAP
+1 -6
View File
@@ -17,7 +17,6 @@ import ToolConfig from '@/pages/ToolConfig';
import JenkinsImport from '@/pages/JenkinsImport'; import JenkinsImport from '@/pages/JenkinsImport';
import BatchRelease from '@/pages/BatchRelease'; import BatchRelease from '@/pages/BatchRelease';
import ReleaseRecords from '@/pages/ReleaseRecords'; import ReleaseRecords from '@/pages/ReleaseRecords';
import WebhookConfig from '@/pages/WebhookConfig';
import GitOpsDeploy from '@/pages/GitOpsDeploy'; import GitOpsDeploy from '@/pages/GitOpsDeploy';
import PluginManager from '@/pages/PluginManager'; import PluginManager from '@/pages/PluginManager';
import SystemSettings from '@/pages/SystemSettings'; import SystemSettings from '@/pages/SystemSettings';
@@ -27,7 +26,7 @@ import UserManage from '@/pages/UserManage';
import Docs from '@/pages/Docs'; import Docs from '@/pages/Docs';
import { apiClient, auth } from '@/lib/api'; import { apiClient, auth } from '@/lib/api';
export type ModuleType = 'pipelines' | 'gitops' | 'batch-release' | 'release-records' | 'environments' | 'credentials' | 'build-nodes' | 'tools' | 'jenkins-import' | 'webhooks' | 'plugins' | 'users' | 'docs' | 'settings'; export type ModuleType = 'pipelines' | 'gitops' | 'batch-release' | 'release-records' | 'environments' | 'credentials' | 'build-nodes' | 'tools' | 'jenkins-import' | 'plugins' | 'users' | 'docs' | 'settings';
export type EditorTab = '基本信息' | '流程配置' | '触发设置' | '变量和缓存' | '发布'; export type EditorTab = '基本信息' | '流程配置' | '触发设置' | '变量和缓存' | '发布';
// 路由 ↔ 模块映射(用户可直接访问 /release、/pipelines/1 等) // 路由 ↔ 模块映射(用户可直接访问 /release、/pipelines/1 等)
@@ -42,7 +41,6 @@ const ROUTE_TO_MODULE: Record<string, ModuleType> = {
'/build-nodes': 'build-nodes', '/build-nodes': 'build-nodes',
'/tools': 'tools', '/tools': 'tools',
'/jenkins-import': 'jenkins-import', '/jenkins-import': 'jenkins-import',
'/webhooks': 'webhooks',
'/plugins': 'plugins', '/plugins': 'plugins',
'/users': 'users', '/users': 'users',
'/docs': 'docs', '/docs': 'docs',
@@ -59,7 +57,6 @@ const MODULE_TO_ROUTE: Record<ModuleType, string> = {
'build-nodes': '/build-nodes', 'build-nodes': '/build-nodes',
tools: '/tools', tools: '/tools',
'jenkins-import': '/jenkins-import', 'jenkins-import': '/jenkins-import',
webhooks: '/webhooks',
plugins: '/plugins', plugins: '/plugins',
users: '/users', users: '/users',
docs: '/docs', docs: '/docs',
@@ -230,8 +227,6 @@ export default function App() {
return <BatchRelease onViewRecords={() => goToModule('release-records')} />; return <BatchRelease onViewRecords={() => goToModule('release-records')} />;
case 'release-records': case 'release-records':
return <ReleaseRecords />; return <ReleaseRecords />;
case 'webhooks':
return <WebhookConfig />;
case 'gitops': case 'gitops':
return <GitOpsDeploy />; return <GitOpsDeploy />;
case 'plugins': case 'plugins':
+1 -3
View File
@@ -10,7 +10,6 @@ import {
Rocket, Rocket,
Zap, Zap,
History, History,
Webhook,
CloudSync, CloudSync,
Puzzle, Puzzle,
Settings, Settings,
@@ -24,7 +23,7 @@ import { useQuery } from '@tanstack/react-query';
import { apiClient } from '@/lib/api'; import { apiClient } from '@/lib/api';
import { useTheme } from '@/providers/theme'; import { useTheme } from '@/providers/theme';
type ModuleType = 'pipelines' | 'gitops' | 'batch-release' | 'release-records' | 'environments' | 'credentials' | 'build-nodes' | 'tools' | 'jenkins-import' | 'webhooks' | 'plugins' | 'users' | 'docs' | 'settings'; type ModuleType = 'pipelines' | 'gitops' | 'batch-release' | 'release-records' | 'environments' | 'credentials' | 'build-nodes' | 'tools' | 'jenkins-import' | 'plugins' | 'users' | 'docs' | 'settings';
interface SidebarProps { interface SidebarProps {
activeModule: ModuleType; activeModule: ModuleType;
@@ -56,7 +55,6 @@ const menuGroups: { label: string; items: { id: ModuleType; label: string; icon:
label: '集成', label: '集成',
items: [ items: [
{ id: 'jenkins-import', label: 'Jenkins导入', icon: Download, badge: null }, { id: 'jenkins-import', label: 'Jenkins导入', icon: Download, badge: null },
{ id: 'webhooks', label: 'Webhook', icon: Webhook, badge: null },
{ id: 'tools', label: '工具配置', icon: Wrench, badge: null }, { id: 'tools', label: '工具配置', icon: Wrench, badge: null },
], ],
}, },
@@ -49,7 +49,7 @@ const formatTime = (v: string | null | undefined) => {
const emptyForm = { name: '', url: '', secret: '', events: [] as string[] }; const emptyForm = { name: '', url: '', secret: '', events: [] as string[] };
export default function WebhookConfig() { export default function WebhookManager() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null); const [editingId, setEditingId] = useState<number | null>(null);
@@ -137,14 +137,13 @@ export default function WebhookConfig() {
}; };
return ( return (
<div className="h-full flex flex-col" style={{ background: 'var(--canvas-bg)' }}> <div className="rounded-lg border overflow-hidden" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
{/* Header */} {/* Card Header */}
<div className="px-5 py-3 border-b flex items-center justify-between flex-shrink-0" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}> <div className="px-5 py-3 border-b flex items-center justify-between" style={{ borderColor: 'var(--border-color)', background: 'var(--panel-bg-secondary)' }}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<h1 className="text-[15px] font-semibold" style={{ color: 'var(--text-primary)' }}>Webhook 配置</h1> <Webhook className="w-4 h-4" style={{ color: 'var(--accent-blue)' }} />
<span className="text-[11px] px-2 py-[2px] rounded-full font-medium" style={{ background: 'var(--panel-bg-secondary)', color: 'var(--text-tertiary)' }}> <span className="text-[13px] font-semibold" style={{ color: 'var(--text-primary)' }}>Webhook 通知</span>
{webhooks.length} <span className="text-[10px]" style={{ color: 'var(--text-tertiary)' }}>发布事件回调第三方(HMAC-SHA256 签名)</span>
</span>
</div> </div>
<button <button
onClick={() => { setShowForm(true); setEditingId(null); setEditingSecretSet(false); setForm(emptyForm); }} onClick={() => { setShowForm(true); setEditingId(null); setEditingSecretSet(false); setForm(emptyForm); }}
@@ -155,8 +154,8 @@ export default function WebhookConfig() {
</button> </button>
</div> </div>
{/* Content */} {/* Body */}
<div className="flex-1 overflow-y-auto custom-scrollbar p-5"> <div className="p-5">
{showForm && ( {showForm && (
<div className="rounded-lg border p-4 mb-4" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}> <div className="rounded-lg border p-4 mb-4" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
<h3 className="text-[14px] font-semibold mb-3" style={{ color: 'var(--text-primary)' }}>{editingId != null ? '编辑 Webhook' : '新建 Webhook'}</h3> <h3 className="text-[14px] font-semibold mb-3" style={{ color: 'var(--text-primary)' }}>{editingId != null ? '编辑 Webhook' : '新建 Webhook'}</h3>
+5
View File
@@ -7,6 +7,7 @@ import { toast } from 'sonner';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '@/lib/api'; import { apiClient } from '@/lib/api';
import { useTheme, type ThemeMode } from '@/providers/theme'; import { useTheme, type ThemeMode } from '@/providers/theme';
import WebhookManager from '@/components/settings/WebhookManager';
interface ThemeConfig { interface ThemeConfig {
mode: 'light' | 'dark' | 'system'; mode: 'light' | 'dark' | 'system';
@@ -638,6 +639,10 @@ export default function SystemSettings() {
</div> </div>
</div> </div>
</div> </div>
<div className="mt-4">
<WebhookManager />
</div>
</> </>
)} )}