From cf1bebf6258e83c6726a6244fff644c8080509f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=9B=E5=85=AE?= Date: Thu, 11 Jun 2026 23:47:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=85=E9=9B=86=E6=88=90?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8F=98=E9=87=8F=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/settings/settings.service.ts | 117 +++++++++++++++++++++++++++++++ src/settings/settings.types.ts | 7 ++ 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4bf49d7..1944b18 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ pnpm prisma:migrate:dev --name init ## API - `GET /health`: dependency configuration and health summary. -- `GET /settings/integration-config`: Jenkins/Gitea/notification/LLM/Database/Redis/Secret Encryption Key configuration status. The response only exposes environment variable names, never secret values. +- `GET /settings/integration-config`: Jenkins/Gitea/notification/LLM/Database/Redis/Secret Encryption Key configuration status and variable help text. The response only exposes environment variable names and safe descriptions, never secret values. - `GET /projects`: configured project list. - `GET /projects/:key`: one project by key. - `GET /deploy-runs`: latest deploy runs. diff --git a/src/settings/settings.service.ts b/src/settings/settings.service.ts index ae8ece8..d859f60 100644 --- a/src/settings/settings.service.ts +++ b/src/settings/settings.service.ts @@ -6,6 +6,7 @@ import { IntegrationConfigState, IntegrationConfigStatus, IntegrationConfigStatusResponse, + IntegrationVariableHelp, } from './settings.types'; type EnvStringKey = { @@ -21,6 +22,7 @@ type IntegrationDefinition = { required: EnvStringKey[]; optional?: EnvKey[]; note?: string; + variableHelp?: Partial>; }; const INTEGRATION_DEFINITIONS: IntegrationDefinition[] = [ @@ -29,6 +31,24 @@ const INTEGRATION_DEFINITIONS: IntegrationDefinition[] = [ name: 'Jenkins', required: ['JENKINS_BASE_URL', 'JENKINS_USERNAME', 'JENKINS_API_TOKEN'], optional: ['JENKINS_AUTO_SYNC_ENABLED', 'JENKINS_AUTO_SYNC_INTERVAL_MS'], + variableHelp: { + JENKINS_BASE_URL: + variableHelp('Jenkins 服务端 API 地址,只能由后端服务访问。', { + docUrl: 'https://www.jenkins.io/doc/book/using/remote-access-api/', + }), + JENKINS_USERNAME: variableHelp('用于调用 Jenkins Remote API 的服务账号。'), + JENKINS_API_TOKEN: variableHelp( + 'Jenkins 服务账号 API token,只能保存在服务端环境变量或后续加密密钥表。', + ), + JENKINS_AUTO_SYNC_ENABLED: variableHelp( + '是否启用轻量自动同步器,自动扫描 queued/running 发布单。', + { example: 'true' }, + ), + JENKINS_AUTO_SYNC_INTERVAL_MS: variableHelp( + '自动同步 Jenkins 状态的轮询间隔,单位毫秒。', + { example: '30000' }, + ), + }, }, { key: 'gitea', @@ -36,6 +56,16 @@ const INTEGRATION_DEFINITIONS: IntegrationDefinition[] = [ required: ['GITEA_BASE_URL', 'GITEA_TOKEN'], optional: ['GITEA_WEBHOOK_SECRET'], note: 'GITEA_WEBHOOK_SECRET 用于开启 webhook 签名校验。', + variableHelp: { + GITEA_BASE_URL: variableHelp('Gitea 服务端 API 地址,用于读取仓库、分支和 tag。'), + GITEA_TOKEN: variableHelp( + 'Gitea API token,只能由后端持有,用于读取 refs 和后续仓库元数据。', + { docUrl: 'https://docs.gitea.com/development/api-usage' }, + ), + GITEA_WEBHOOK_SECRET: variableHelp( + 'Gitea push webhook 签名密钥;配置后后端会校验 webhook 来源。', + ), + }, }, { key: 'notification', @@ -48,27 +78,82 @@ const INTEGRATION_DEFINITIONS: IntegrationDefinition[] = [ 'DEVOPS_PUBLIC_URL', ], note: '当前不强制配置机器人;后续可按 provider 接入企微、飞书或通用 webhook。', + variableHelp: { + NOTIFICATION_PROVIDER: variableHelp( + '通知 provider,可选 wecom、feishu、generic;未配置时通知节点会 skipped。', + { example: 'wecom' }, + ), + WECOM_WEBHOOK_URL: variableHelp( + '企业微信群机器人的服务端 webhook,只能保存在后端。', + { + docUrl: 'https://developer.work.weixin.qq.com/document/path/91770', + }, + ), + FEISHU_WEBHOOK_URL: variableHelp( + '飞书自定义机器人的服务端 webhook,只能保存在后端。', + { + docUrl: + 'https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot', + }, + ), + NOTIFICATION_WEBHOOK_URL: variableHelp( + '内部通知网关或通用 webhook 的服务端地址,适合接自建消息中转服务。', + ), + DEVOPS_PUBLIC_URL: variableHelp('通知消息中跳转回 DevOps 平台的公开访问地址。', { + example: 'https://devops.mrzhan.top', + }), + }, }, { key: 'llm', name: 'LLM Agent', required: ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL'], + variableHelp: { + LLM_BASE_URL: variableHelp('LLM 兼容接口地址,只允许后端代理调用。'), + LLM_API_KEY: variableHelp( + 'LLM API key,只能保存在后端环境变量或后续加密密钥表。', + ), + LLM_MODEL: variableHelp('DevOps Agent 使用的模型名称。'), + }, }, { key: 'database', name: 'MySQL / Prisma', required: ['DATABASE_URL'], optional: ['USE_DATABASE_READS', 'PRISMA_CONNECT_ON_BOOT'], + variableHelp: { + DATABASE_URL: variableHelp( + 'Prisma 连接 MySQL 的服务端连接串,前端和审计日志不得暴露原文。', + { docUrl: 'https://www.prisma.io/docs/orm/reference/connection-urls' }, + ), + USE_DATABASE_READS: variableHelp('是否启用 MySQL 持久化读写路径。', { + example: 'true', + }), + PRISMA_CONNECT_ON_BOOT: variableHelp( + '是否在服务启动时主动连接数据库,用于尽早发现连接问题。', + { example: 'true' }, + ), + }, }, { key: 'redis', name: 'Redis / BullMQ', required: ['REDIS_URL'], + variableHelp: { + REDIS_URL: variableHelp( + 'Redis 连接串,后续用于 BullMQ 队列、通知重试和 Jenkins 日志异步任务。', + ), + }, }, { key: 'secrets', name: '服务端密钥加密', required: ['SECRET_ENCRYPTION_KEY'], + variableHelp: { + SECRET_ENCRYPTION_KEY: variableHelp( + '服务端加密密钥,用于后续加密保存 Jenkins/Gitea/通知/LLM 等敏感配置。', + ), + }, }, ]; @@ -113,6 +198,9 @@ export class SettingsService { missing, ...(definition.optional ? { optional: [...definition.optional] } : {}), ...(definition.note ? { note: definition.note } : {}), + ...(definition.variableHelp + ? { variableHelp: this.pickVariableHelp(definition) } + : {}), }; } @@ -143,6 +231,9 @@ export class SettingsService { provider && !providerWebhook ? `NOTIFICATION_PROVIDER=${provider} 暂不支持;可选 wecom、feishu、generic。` : definition.note, + ...(definition.variableHelp + ? { variableHelp: this.pickVariableHelp(definition, required) } + : {}), }; } @@ -188,4 +279,30 @@ export class SettingsService { return configuredCount > 0 ? 'partial' : 'missing'; } + + private pickVariableHelp( + definition: IntegrationDefinition, + extraKeys: EnvKey[] = [], + ): Record { + const visibleKeys = new Set([ + ...definition.required, + ...(definition.optional ?? []), + ...extraKeys, + ]); + const helpEntries = Object.entries(definition.variableHelp ?? {}).filter( + ([key]) => visibleKeys.has(key as EnvKey), + ); + + return Object.fromEntries(helpEntries); + } +} + +function variableHelp( + description: string, + options: Pick = {}, +): IntegrationVariableHelp { + return { + description, + ...options, + }; } diff --git a/src/settings/settings.types.ts b/src/settings/settings.types.ts index 56009e9..0e1b128 100644 --- a/src/settings/settings.types.ts +++ b/src/settings/settings.types.ts @@ -10,6 +10,12 @@ export type IntegrationConfigKey = export type IntegrationConfigState = 'configured' | 'partial' | 'missing'; +export type IntegrationVariableHelp = { + description: string; + example?: string; + docUrl?: string; +}; + export type IntegrationConfigStatus = { key: IntegrationConfigKey; name: string; @@ -19,6 +25,7 @@ export type IntegrationConfigStatus = { missing: string[]; optional?: string[]; note?: string; + variableHelp?: Record; }; export type IntegrationConfigStatusResponse = {