feat: 展示集成变量说明与流程详情

This commit is contained in:
湛兮
2026-06-11 23:52:01 +08:00
parent 88836590cd
commit 2daf93ab24
6 changed files with 982 additions and 131 deletions
+180 -10
View File
@@ -13,6 +13,7 @@ import type {
IntegrationConfigKey,
IntegrationConfigState,
IntegrationConfigStatus,
IntegrationVariableHelp,
IntegrationStatus,
JenkinsSyncSummary,
PipelineStep,
@@ -28,6 +29,13 @@ import { http } from './http'
type DataSource = 'backend'
type BackendHealthStatus = 'ok' | 'not_configured' | 'unavailable'
type BackendRunStatus = 'pending' | 'queued' | 'running' | 'success' | 'failed' | 'canceled'
type BackendVariableHelpValue =
| string
| {
description?: unknown
example?: unknown
docUrl?: unknown
}
interface BackendHealth {
status: BackendHealthStatus
@@ -99,6 +107,31 @@ interface BackendProcessDefinition {
nodeIds: string[]
}
interface BackendIntegrationConfigItem {
key: IntegrationConfigKey
name: string
status: IntegrationConfigState
required?: string[]
configured?: string[]
missing?: string[]
optional?: string[]
note?: string
variableHelp?: Record<string, BackendVariableHelpValue>
variableDescriptions?: Record<string, unknown>
variables?: Array<{
name?: unknown
key?: unknown
description?: unknown
example?: unknown
docUrl?: unknown
}>
}
interface BackendIntegrationConfigStatus {
checkedAt?: string
integrations?: BackendIntegrationConfigItem[]
}
let lastDataSource: DataSource = 'backend'
const integrationConfigKeys: IntegrationConfigKey[] = [
'jenkins',
@@ -283,6 +316,14 @@ export const devopsApi = {
return fetchIntegrationConfigStatus()
},
async getReleaseProcessXml(): Promise<string> {
const { data } = await http.get<BackendProcessDefinition>(
'/process-definitions/release',
)
lastDataSource = 'backend'
return data.xml
},
async createAgentInvocation(
payload: CreateAgentInvocationPayload,
): Promise<AgentInvocation> {
@@ -300,13 +341,6 @@ export const devopsApi = {
return toSystemSettings(health, integrationConfig)
},
async getReleaseProcessXml(): Promise<string> {
const { data } = await http.get<BackendProcessDefinition>(
'/process-definitions/release',
)
lastDataSource = 'backend'
return data.xml
},
}
async function fetchHealth(): Promise<BackendHealth> {
@@ -325,7 +359,9 @@ async function fetchRuns(): Promise<BackendDeployRun[]> {
}
async function fetchIntegrationConfigStatus(): Promise<IntegrationConfigStatus> {
const { data } = await http.get<IntegrationConfigStatus>('/settings/integration-config')
const { data } = await http.get<BackendIntegrationConfigStatus>(
'/settings/integration-config',
)
return sanitizeIntegrationConfigStatus(data)
}
@@ -358,7 +394,7 @@ function toIntegrationStatus(
}
function sanitizeIntegrationConfigStatus(
payload: IntegrationConfigStatus,
payload: BackendIntegrationConfigStatus,
): IntegrationConfigStatus {
const integrations = Array.isArray(payload?.integrations) ? payload.integrations : []
@@ -373,6 +409,8 @@ function sanitizeIntegrationConfigStatus(
const required = sanitizeVariableNames(item.required)
const configured = sanitizeVariableNames(item.configured)
const missing = sanitizeVariableNames(item.missing)
const optional = sanitizeVariableNames(item.optional)
const variableHelp = sanitizeVariableHelp(item)
return {
key: item.key,
@@ -381,8 +419,9 @@ function sanitizeIntegrationConfigStatus(
required,
configured,
missing,
optional: sanitizeVariableNames(item.optional),
optional,
note: sanitizeIntegrationNote(item.note),
...(Object.keys(variableHelp).length > 0 ? { variableHelp } : {}),
}
}),
}
@@ -423,6 +462,137 @@ function sanitizeIntegrationNote(note?: string): string | undefined {
.replace(/key=[^&\s]+/g, 'key=[已隐藏]')
}
function sanitizeVariableHelp(
item: BackendIntegrationConfigItem,
): Record<string, IntegrationVariableHelp> {
const help: Record<string, IntegrationVariableHelp> = {}
collectVariableHelpMap(help, item.variableHelp)
collectVariableDescriptionMap(help, item.variableDescriptions)
collectVariableHelpArray(help, item.variables)
return help
}
function collectVariableHelpMap(
target: Record<string, IntegrationVariableHelp>,
value?: Record<string, BackendVariableHelpValue>,
) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return
}
Object.entries(value).forEach(([rawName, rawHelp]) => {
const name = sanitizeVariableName(rawName)
if (!name) {
return
}
const help =
typeof rawHelp === 'string'
? sanitizeVariableHelpValue({ description: rawHelp })
: sanitizeVariableHelpValue(rawHelp)
if (help) {
target[name] = help
}
})
}
function collectVariableDescriptionMap(
target: Record<string, IntegrationVariableHelp>,
value?: Record<string, unknown>,
) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return
}
Object.entries(value).forEach(([rawName, rawDescription]) => {
const name = sanitizeVariableName(rawName)
const description = sanitizeVariableDescription(rawDescription)
if (name && description) {
target[name] = { description }
}
})
}
function collectVariableHelpArray(
target: Record<string, IntegrationVariableHelp>,
value?: BackendIntegrationConfigItem['variables'],
) {
if (!Array.isArray(value)) {
return
}
value.forEach((item) => {
const name = sanitizeVariableName(item.name ?? item.key)
const help = sanitizeVariableHelpValue(item)
if (name && help) {
target[name] = help
}
})
}
function sanitizeVariableHelpValue(
value?: {
description?: unknown
example?: unknown
docUrl?: unknown
},
): IntegrationVariableHelp | undefined {
const description = sanitizeVariableDescription(value?.description)
if (!description) {
return undefined
}
return {
description,
...(typeof value?.example === 'string' && value.example.trim()
? { example: sanitizeIntegrationNote(value.example.trim()) ?? value.example.trim() }
: {}),
...(typeof value?.docUrl === 'string' && isSafeDocUrl(value.docUrl)
? { docUrl: value.docUrl.trim() }
: {}),
}
}
function sanitizeVariableDescription(value: unknown): string | undefined {
if (typeof value !== 'string' || !value.trim()) {
return undefined
}
return sanitizeIntegrationNote(value.trim())
}
function sanitizeVariableName(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined
}
const name = value.trim()
return /^[A-Z][A-Z0-9_]*$/.test(name) ? name : undefined
}
function isSafeDocUrl(value: string): boolean {
try {
const url = new URL(value)
const normalized = url.toString().toLowerCase()
return (
url.protocol === 'https:' &&
!url.searchParams.has('key') &&
!normalized.includes('/cgi-bin/webhook/') &&
!normalized.includes('/open-apis/bot/') &&
!normalized.includes('/webhook/')
)
} catch {
return false
}
}
function safeText(value: unknown, fallback: string): string {
return typeof value === 'string' && value.trim() ? value : fallback
}