feat: 发布成功/失败飞书&Lark 群卡片通知
- internal/lark:飞书自定义机器人交互卡片(wide_screen_mode + 彩色头部 green/red/yellow + lark_md 字段区 + 查看构建详情按钮),支持签名加签 (timestamp+sign HMAC-SHA256,密钥 enc:v1: 加密存储) - 双引擎收口:原生模拟引擎与 Jenkins 真实引擎运行结束均推送卡片; 应用/分支/构建(#N 或 runId)/状态/时间;Jenkins 构建附 externalURL 按钮直达构建详情页 - Webhook 优先级:流水线「通知 Webhook」> 全局设置(需启用) - 系统设置新增飞书通知配置区(启用开关/Webhook/签名密钥只写/ 发送测试卡片);POST /api/settings/test-lark - E2E 已验证:本地模拟机器人收到完整卡片 JSON,测试卡片正常
This commit is contained in:
1 parent
6e97b55015
commit
d9d1dd5e99
10 files changed
+329
-9
No files matched your search
@@ -153,4 +153,5 @@ export const apiClient = {
|
||||
batchParse: (b: { text: string; refType?: string; fixedRef?: string }) =>
|
||||
api<any>('/api/batch/parse', { method: 'POST', body: JSON.stringify(b) }),
|
||||
testLLM: () => api<any>('/api/settings/test-llm', { method: 'POST' }),
|
||||
testLark: () => api<any>('/api/settings/test-lark', { method: 'POST' }),
|
||||
}
|
||||
@@ -67,6 +67,7 @@ export default function SystemSettings() {
|
||||
const [theme, setTheme] = useState<ThemeConfig>(defaultTheme);
|
||||
const [notifications, setNotifications] = useState<NotificationConfig>(defaultNotifications);
|
||||
const [llm, setLlm] = useState({ enabled: false, endpoint: '', model: '', apiKey: '' });
|
||||
const [lark, setLark] = useState({ enabled: false, webhook: '', secret: '' });
|
||||
|
||||
const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: apiClient.getSettings });
|
||||
|
||||
@@ -82,8 +83,37 @@ export default function SystemSettings() {
|
||||
model: settings.llmModel || '',
|
||||
apiKey: '', // 只写:回显永远留空
|
||||
}));
|
||||
setLark((prev) => ({
|
||||
...prev,
|
||||
enabled: !!settings.larkEnabled,
|
||||
webhook: settings.larkWebhook || '',
|
||||
secret: '',
|
||||
}));
|
||||
}, [settings]);
|
||||
|
||||
const saveLarkMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const body: any = { larkEnabled: lark.enabled, larkWebhook: lark.webhook };
|
||||
if (lark.secret) body.larkSecret = lark.secret;
|
||||
return apiClient.updateSettings(body);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('飞书通知配置已保存');
|
||||
setLark((p) => ({ ...p, secret: '' }));
|
||||
queryClient.invalidateQueries({ queryKey: ['settings'] });
|
||||
},
|
||||
onError: (err: Error) => toast.error('保存失败', { description: err.message }),
|
||||
});
|
||||
|
||||
const testLarkMutation = useMutation({
|
||||
mutationFn: () => apiClient.testLark(),
|
||||
onSuccess: (res: any) => {
|
||||
if (res.success) toast.success('测试卡片已发送', { description: '请到飞书群查看' });
|
||||
else toast.error('发送失败', { description: res.message });
|
||||
},
|
||||
onError: (err: Error) => toast.error('测试失败', { description: err.message }),
|
||||
});
|
||||
|
||||
const saveLlmMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const body: any = { llmEnabled: llm.enabled, llmEndpoint: llm.endpoint, llmModel: llm.model };
|
||||
@@ -384,6 +414,44 @@ export default function SystemSettings() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 飞书 / Lark 通知 */}
|
||||
<div className="rounded-lg border overflow-hidden mt-4" 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)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] font-semibold" style={{ color: 'var(--text-primary)' }}>飞书 / Lark 群通知</span>
|
||||
<span className="text-[10px] px-1.5 py-[1px] rounded" style={{ background: settings?.larkEnabled && settings?.larkWebhook ? '#dafbe1' : '#f6f8fa', color: settings?.larkEnabled && settings?.larkWebhook ? '#1a7f37' : '#6e7781' }}>
|
||||
{settings?.larkEnabled && settings?.larkWebhook ? '已启用' : '未启用'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 space-y-4">
|
||||
<p className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>
|
||||
发布成功/失败后向群推送结果卡片(应用 / 分支 / 构建 / 状态 / 时间 + 「查看构建详情」按钮跳转 Jenkins 构建页)。
|
||||
流水线单独配置的「通知 Webhook」优先生效。
|
||||
</p>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={lark.enabled} onChange={(e) => setLark({ ...lark, enabled: e.target.checked })} />
|
||||
<span className="text-[12px]" style={{ color: 'var(--text-primary)' }}>启用全局群通知</span>
|
||||
</label>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>群机器人 Webhook URL</label>
|
||||
<input value={lark.webhook} onChange={(e) => setLark({ ...lark, webhook: e.target.value })} placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/xxxx" className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[12px]" style={{ color: 'var(--text-secondary)' }}>签名密钥(可选,机器人开启签名校验时填写)</label>
|
||||
<input type="password" value={lark.secret} onChange={(e) => setLark({ ...lark, secret: e.target.value })} placeholder={settings?.larkSecretSet ? '已配置,留空不修改' : '无需签名可留空'} className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={() => saveLarkMutation.mutate()} disabled={saveLarkMutation.isPending} className="px-3 py-1 text-[12px] rounded-md text-white disabled:opacity-60" style={{ background: 'var(--accent-blue)' }}>
|
||||
{saveLarkMutation.isPending ? '保存中…' : '保存通知配置'}
|
||||
</button>
|
||||
<button onClick={() => testLarkMutation.mutate()} disabled={testLarkMutation.isPending} className="px-3 py-1 text-[12px] rounded-md border disabled:opacity-60" style={{ borderColor: 'var(--border-color-light)', color: 'var(--text-secondary)' }}>
|
||||
{testLarkMutation.isPending ? '发送中…' : '发送测试卡片(需先保存)'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user