From b8ab077b57ce234fbd68363479dc5f24f17bc88b Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Mon, 19 May 2025 17:41:39 +0800 Subject: [PATCH 01/28] improve ui --- .../workflow/node/ConditionNode.tsx | 53 +++- .../workflow/node/ConditionNodeConfigForm.tsx | 250 ++++++++++++++++++ ui/src/domain/workflow.ts | 47 +++- ui/src/stores/workflow/index.ts | 4 +- 4 files changed, 346 insertions(+), 8 deletions(-) create mode 100644 ui/src/components/workflow/node/ConditionNodeConfigForm.tsx diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index 56639692..63aa1aa1 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -1,9 +1,14 @@ -import { memo } from "react"; +import { memo, useRef, useState } from "react"; import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; import { Button, Card, Popover } from "antd"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; +import ConditionNodeConfigForm, { ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; +import { WorkflowNodeConfigForCondition } from "@/domain/workflow"; +import { produce } from "immer"; +import { useWorkflowStore } from "@/stores/workflow"; +import { useZustandShallowSelector } from "@/hooks"; export type ConditionNodeProps = SharedNodeProps & { branchId: string; @@ -11,7 +16,37 @@ export type ConditionNodeProps = SharedNodeProps & { }; const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => { - // TODO: 条件分支 + const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); + + const [formPending, setFormPending] = useState(false); + const formRef = useRef(null); + + const [drawerOpen, setDrawerOpen] = useState(false); + + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForCondition; + + const handleDrawerConfirm = async () => { + setFormPending(true); + try { + await formRef.current!.validateFields(); + } catch (err) { + setFormPending(false); + throw err; + } + + try { + const newValues = getFormValues(); + const newNode = produce(node, (draft) => { + draft.config = { + ...newValues, + }; + draft.validated = true; + }); + await updateNode(newNode); + } finally { + setFormPending(false); + } + }; return ( <> @@ -30,7 +65,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP } placement="rightTop" > - + setDrawerOpen(true)}>
+ + setDrawerOpen(open)} + getFormValues={() => formRef.current!.getFieldsValue()} + > + + @@ -47,3 +93,4 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP }; export default memo(ConditionNode); + diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx new file mode 100644 index 00000000..4eca1617 --- /dev/null +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -0,0 +1,250 @@ +import { forwardRef, memo, useEffect, useImperativeHandle, useState } from "react"; +import { Button, Card, Form, Input, Select, Space, Radio } from "antd"; +import { PlusOutlined, DeleteOutlined } from "@ant-design/icons"; + +import { + WorkflowNodeConfigForCondition, + Expr, + WorkflowNodeIOValueSelector, + ComparisonOperator, + LogicalOperator, + isConstExpr, + isVarExpr, + WorkflowNode, +} from "@/domain/workflow"; +import { FormInstance } from "antd"; +import { useZustandShallowSelector } from "@/hooks"; +import { useWorkflowStore } from "@/stores/workflow"; + +// 表单内部使用的扁平结构 - 修改后只保留必要字段 +interface ConditionItem { + leftSelector: WorkflowNodeIOValueSelector; + operator: ComparisonOperator; + rightValue: string; +} + +type ConditionNodeConfigFormFieldValues = { + conditions: ConditionItem[]; + logicalOperator: LogicalOperator; +}; + +export type ConditionNodeConfigFormProps = { + className?: string; + style?: React.CSSProperties; + disabled?: boolean; + initialValues?: Partial; + onValuesChange?: (values: WorkflowNodeConfigForCondition) => void; + availableSelectors?: WorkflowNodeIOValueSelector[]; + nodeId: string; +}; + +export type ConditionNodeConfigFormInstance = { + getFieldsValue: () => ReturnType["getFieldsValue"]>; + resetFields: FormInstance["resetFields"]; + validateFields: FormInstance["validateFields"]; +}; + +// 初始表单值 +const initFormModel = (): ConditionNodeConfigFormFieldValues => { + return { + conditions: [ + { + leftSelector: undefined as unknown as WorkflowNodeIOValueSelector, + operator: "==", + rightValue: "", + }, + ], + logicalOperator: "and", + }; +}; + +// 将表单值转换为表达式结构 +const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { + // 创建单个条件的表达式 + const createComparisonExpr = (condition: ConditionItem): Expr => { + const left: Expr = { type: "var", selector: condition.leftSelector }; + const right: Expr = { type: "const", value: condition.rightValue || "" }; + + return { + type: "compare", + op: condition.operator, + left, + right, + }; + }; + + // 如果只有一个条件,直接返回比较表达式 + if (values.conditions.length === 1) { + return createComparisonExpr(values.conditions[0]); + } + + // 多个条件,通过逻辑运算符连接 + let expr: Expr = createComparisonExpr(values.conditions[0]); + + for (let i = 1; i < values.conditions.length; i++) { + expr = { + type: "logical", + op: values.logicalOperator, + left: expr, + right: createComparisonExpr(values.conditions[i]), + }; + } + + return expr; +}; + +// 递归提取表达式中的条件项 +const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { + if (!expr) return initFormModel(); + + const conditions: ConditionItem[] = []; + let logicalOp: LogicalOperator = "and"; + + const extractComparisons = (expr: Expr): void => { + if (expr.type === "compare") { + // 确保左侧是变量,右侧是常量 + if (isVarExpr(expr.left) && isConstExpr(expr.right)) { + conditions.push({ + leftSelector: expr.left.selector, + operator: expr.op, + rightValue: String(expr.right.value), + }); + } + } else if (expr.type === "logical") { + logicalOp = expr.op; + extractComparisons(expr.left); + extractComparisons(expr.right); + } + }; + + extractComparisons(expr); + + return { + conditions: conditions.length > 0 ? conditions : initFormModel().conditions, + logicalOperator: logicalOp, + }; +}; + +const ConditionNodeConfigForm = forwardRef( + ({ className, style, disabled, initialValues, onValuesChange, nodeId }, ref) => { + const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); + + const [form] = Form.useForm(); + const [formModel, setFormModel] = useState(initFormModel()); + + const [previousNodes, setPreviousNodes] = useState([]); + useEffect(() => { + const previousNodes = getWorkflowOuptutBeforeId(nodeId); + setPreviousNodes(previousNodes); + }, [nodeId]); + + // 初始化表单值 + useEffect(() => { + if (initialValues?.expression) { + const formValues = expressionToForm(initialValues.expression); + form.setFieldsValue(formValues); + setFormModel(formValues); + } + }, [form, initialValues]); + + // 公开表单方法 + useImperativeHandle( + ref, + () => ({ + getFieldsValue: form.getFieldsValue, + resetFields: form.resetFields, + validateFields: form.validateFields, + }), + [form] + ); + + // 表单值变更处理 + const handleFormChange = (changedValues: any, values: ConditionNodeConfigFormFieldValues) => { + setFormModel(values); + + // 转换为表达式结构并通知父组件 + const expression = formToExpression(values); + onValuesChange?.({ expression }); + }; + + return ( +
+ + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + 1 ? + + + )} + + + {formModel.conditions && formModel.conditions.length > 1 && ( + + + 满足所有条件 (AND) + 满足任一条件 (OR) + + + )} +
+ ); + } +); + +export default memo(ConditionNodeConfigForm); + diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 06226425..762d57ab 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -165,6 +165,10 @@ export type WorkflowNodeConfigForNotify = { providerConfig?: Record; }; +export type WorkflowNodeConfigForCondition = { + expression: Expr; +}; + export type WorkflowNodeConfigForBranch = never; export type WorkflowNodeConfigForEnd = never; @@ -185,6 +189,32 @@ export type WorkflowNodeIOValueSelector = { // #endregion +// #region Condition expression + +type Value = string | number | boolean; + +export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!="; + +export type LogicalOperator = "and" | "or" | "not"; + +export type ConstExpr = { type: "const"; value: Value }; +export type VarExpr = { type: "var"; selector: WorkflowNodeIOValueSelector }; +export type CompareExpr = { type: "compare"; op: ComparisonOperator; left: Expr; right: Expr }; +export type LogicalExpr = { type: "logical"; op: LogicalOperator; left: Expr; right: Expr }; +export type NotExpr = { type: "not"; expr: Expr }; + +export type Expr = ConstExpr | VarExpr | CompareExpr | LogicalExpr | NotExpr; + +export const isConstExpr = (expr: Expr): expr is ConstExpr => { + return expr.type === "const"; +}; + +export const isVarExpr = (expr: Expr): expr is VarExpr => { + return expr.type === "var"; +}; + +// #endregion + const isBranchLike = (node: WorkflowNode) => { return node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.ExecuteResultBranch; }; @@ -433,7 +463,17 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd }); }; -export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: string): WorkflowNode[] => { +const typeEqual = (a: WorkflowNodeIO, t: string) => { + if (t === "all") { + return true; + } + if (a.type === t) { + return true; + } + return false; +}; + +export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: string = "all"): WorkflowNode[] => { // 某个分支的节点,不应该能获取到相邻分支上节点的输出 const outputs: WorkflowNode[] = []; @@ -445,10 +485,10 @@ export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: return true; } - if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => io.type === type)) { + if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => typeEqual(io, type))) { output.push({ ...current, - outputs: current.outputs.filter((io) => io.type === type), + outputs: current.outputs.filter((io) => typeEqual(io, type)), }); } @@ -501,3 +541,4 @@ export const isAllNodesValidated = (node: WorkflowNode): boolean => { return true; }; + diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts index 8057fda5..d20fec16 100644 --- a/ui/src/stores/workflow/index.ts +++ b/ui/src/stores/workflow/index.ts @@ -32,7 +32,7 @@ export type WorkflowState = { addBranch: (branchId: string) => void; removeBranch: (branchId: string, index: number) => void; - getWorkflowOuptutBeforeId: (nodeId: string, type: string) => WorkflowNode[]; + getWorkflowOuptutBeforeId: (nodeId: string, type?: string) => WorkflowNode[]; }; export const useWorkflowStore = create((set, get) => ({ @@ -243,7 +243,7 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - getWorkflowOuptutBeforeId: (nodeId: string, type: string) => { + getWorkflowOuptutBeforeId: (nodeId: string, type: string = "all") => { return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, type); }, })); From 05d43f38cead084f61ed6a6468716579bc6550f2 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Mon, 19 May 2025 18:15:04 +0800 Subject: [PATCH 02/28] improve previous variables --- .../workflow/node/ConditionNodeConfigForm.tsx | 21 +++++++++++------ ui/src/domain/workflow.ts | 23 +++++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx index 4eca1617..c83a4b68 100644 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -183,13 +183,20 @@ const ConditionNodeConfigForm = forwardRef {/* 左侧变量选择器 */} - + {/* 操作符 */} diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 762d57ab..6cccf32e 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -187,6 +187,29 @@ export type WorkflowNodeIOValueSelector = { name: string; }; +export const workflowNodeIOOptions = (node: WorkflowNode, io: WorkflowNodeIO) => { + switch (io.type) { + case "certificate": + return [ + { + label: "是否有效", + value: "valid", + }, + { + label: "剩余有效天数", + value: "valid", + }, + ]; + default: + return [ + { + label: `${node.name} - ${io.label}`, + value: `${node.id}#${io.name}`, + }, + ]; + } +}; + // #endregion // #region Condition expression From 6f054ee5946604324fdabd232fbd0bfc58cd0994 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Mon, 19 May 2025 18:16:21 +0800 Subject: [PATCH 03/28] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ab2fb7b..7584fdd1 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ 做个人产品或者在中小企业里负责运维的同学,会遇到要管理多个域名的情况,需要给域名申请证书。但是手动申请证书有以下缺点: -- 😱 麻烦:申请证书并部署到服务的流程虽不复杂,但也挺麻烦的,犹其是你有多个域名需要维护的时候。 +- 😱 麻烦:申请证书并部署到服务的流程虽不复杂,但也挺麻烦的,尤其是你有多个域名需要维护的时候。 - 😭 易忘:另外当前免费证书的有效期只有 90 天,这就要求你定期的操作,增加了工作量的同时,你也很容易忘掉续期,从而导致网站访问不了。 Certimate 就是为了解决上述问题而产生的,它具有以下优势: From 1e67e9333ec9a7d997e8dbe1f28af42121fc13fd Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Mon, 19 May 2025 21:59:37 +0800 Subject: [PATCH 04/28] condition render --- .../workflow/node/ConditionNode.tsx | 44 +++++++++++++-- .../workflow/node/ConditionNodeConfigForm.tsx | 56 ++----------------- ui/src/domain/workflow.ts | 55 +++++++++++------- 3 files changed, 80 insertions(+), 75 deletions(-) diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index 63aa1aa1..69dc101d 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -4,8 +4,8 @@ import { Button, Card, Popover } from "antd"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; -import ConditionNodeConfigForm, { ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; -import { WorkflowNodeConfigForCondition } from "@/domain/workflow"; +import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; +import { Expr, WorkflowNodeConfigForCondition } from "@/domain/workflow"; import { produce } from "immer"; import { useWorkflowStore } from "@/stores/workflow"; import { useZustandShallowSelector } from "@/hooks"; @@ -23,7 +23,42 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForCondition; + const getFormValues = () => formRef.current!.getFieldsValue() as ConditionNodeConfigFormFieldValues; + + // 将表单值转换为表达式结构 + const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { + // 创建单个条件的表达式 + const createComparisonExpr = (condition: ConditionItem): Expr => { + const left: Expr = { type: "var", selector: condition.leftSelector }; + const right: Expr = { type: "const", value: condition.rightValue || "" }; + + return { + type: "compare", + op: condition.operator, + left, + right, + }; + }; + + // 如果只有一个条件,直接返回比较表达式 + if (values.conditions.length === 1) { + return createComparisonExpr(values.conditions[0]); + } + + // 多个条件,通过逻辑运算符连接 + let expr: Expr = createComparisonExpr(values.conditions[0]); + + for (let i = 1; i < values.conditions.length; i++) { + expr = { + type: "logical", + op: values.logicalOperator, + left: expr, + right: createComparisonExpr(values.conditions[i]), + }; + } + + return expr; + }; const handleDrawerConfirm = async () => { setFormPending(true); @@ -36,9 +71,10 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP try { const newValues = getFormValues(); + const expression = formToExpression(newValues); const newNode = produce(node, (draft) => { draft.config = { - ...newValues, + expression, }; draft.validated = true; }); diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx index c83a4b68..52f2c3e2 100644 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -11,19 +11,20 @@ import { isConstExpr, isVarExpr, WorkflowNode, + workflowNodeIOOptions, } from "@/domain/workflow"; import { FormInstance } from "antd"; import { useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; // 表单内部使用的扁平结构 - 修改后只保留必要字段 -interface ConditionItem { +export interface ConditionItem { leftSelector: WorkflowNodeIOValueSelector; operator: ComparisonOperator; rightValue: string; } -type ConditionNodeConfigFormFieldValues = { +export type ConditionNodeConfigFormFieldValues = { conditions: ConditionItem[]; logicalOperator: LogicalOperator; }; @@ -58,41 +59,6 @@ const initFormModel = (): ConditionNodeConfigFormFieldValues => { }; }; -// 将表单值转换为表达式结构 -const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { - // 创建单个条件的表达式 - const createComparisonExpr = (condition: ConditionItem): Expr => { - const left: Expr = { type: "var", selector: condition.leftSelector }; - const right: Expr = { type: "const", value: condition.rightValue || "" }; - - return { - type: "compare", - op: condition.operator, - left, - right, - }; - }; - - // 如果只有一个条件,直接返回比较表达式 - if (values.conditions.length === 1) { - return createComparisonExpr(values.conditions[0]); - } - - // 多个条件,通过逻辑运算符连接 - let expr: Expr = createComparisonExpr(values.conditions[0]); - - for (let i = 1; i < values.conditions.length; i++) { - expr = { - type: "logical", - op: values.logicalOperator, - left: expr, - right: createComparisonExpr(values.conditions[i]), - }; - } - - return expr; -}; - // 递归提取表达式中的条件项 const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { if (!expr) return initFormModel(); @@ -159,12 +125,8 @@ const ConditionNodeConfigForm = forwardRef { + const handleFormChange = (_: undefined, values: ConditionNodeConfigFormFieldValues) => { setFormModel(values); - - // 转换为表达式结构并通知父组件 - const expression = formToExpression(values); - onValuesChange?.({ expression }); }; return ( @@ -186,15 +148,7 @@ const ConditionNodeConfigForm = forwardRef { - return { - label: item.name, - options: item.outputs?.map((output) => { - return { - label: `${item.name} - ${output.label}`, - value: `${item.id}#${output.name}`, - }; - }), - }; + return workflowNodeIOOptions(item); })} > diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 6cccf32e..d6354e14 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -187,27 +187,42 @@ export type WorkflowNodeIOValueSelector = { name: string; }; -export const workflowNodeIOOptions = (node: WorkflowNode, io: WorkflowNodeIO) => { - switch (io.type) { - case "certificate": - return [ - { - label: "是否有效", - value: "valid", - }, - { - label: "剩余有效天数", - value: "valid", - }, - ]; - default: - return [ - { - label: `${node.name} - ${io.label}`, - value: `${node.id}#${io.name}`, - }, - ]; +type WorkflowNodeIOOptions = { + label: string; + value: string; +}; + +export const workflowNodeIOOptions = (node: WorkflowNode) => { + const rs = { + label: node.name, + options: Array(), + }; + + if (node.outputs) { + for (const output of node.outputs) { + switch (output.type) { + case "certificate": + rs.options.push({ + label: `${node.name} - ${output.label} - 是否有效`, + value: `${node.id}#${output.name}.validated`, + }); + + rs.options.push({ + label: `${node.name} - ${output.label} - 剩余天数`, + value: `${node.id}#${output.name}.daysLeft`, + }); + break; + default: + rs.options.push({ + label: `${node.name} - ${output.label}`, + value: `${node.id}#${output.name}`, + }); + break; + } + } } + + return rs; }; // #endregion From 6353f0139be8f1aeb6501f9597ffa40af055b1d8 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Tue, 20 May 2025 11:01:04 +0800 Subject: [PATCH 05/28] improve variable types --- .../workflow/node/ConditionNode.tsx | 12 +- .../workflow/node/ConditionNodeConfigForm.tsx | 163 +++++++++++++++--- ui/src/domain/workflow.ts | 11 +- 3 files changed, 160 insertions(+), 26 deletions(-) diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index 69dc101d..60a35c26 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -5,7 +5,7 @@ import { Button, Card, Popover } from "antd"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; -import { Expr, WorkflowNodeConfigForCondition } from "@/domain/workflow"; +import { Expr, WorkflowNodeConfigForCondition, WorkflowNodeIoValueType } from "@/domain/workflow"; import { produce } from "immer"; import { useWorkflowStore } from "@/stores/workflow"; import { useZustandShallowSelector } from "@/hooks"; @@ -29,7 +29,15 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { // 创建单个条件的表达式 const createComparisonExpr = (condition: ConditionItem): Expr => { - const left: Expr = { type: "var", selector: condition.leftSelector }; + const selectors = condition.leftSelector.split("#"); + const left: Expr = { + type: "var", + selector: { + id: selectors[0], + name: selectors[1], + type: selectors[2] as WorkflowNodeIoValueType, + }, + }; const right: Expr = { type: "const", value: condition.rightValue || "" }; return { diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx index 52f2c3e2..b041ea7c 100644 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -1,5 +1,5 @@ import { forwardRef, memo, useEffect, useImperativeHandle, useState } from "react"; -import { Button, Card, Form, Input, Select, Space, Radio } from "antd"; +import { Button, Card, Form, Input, Select, Space, Radio, DatePicker } from "antd"; import { PlusOutlined, DeleteOutlined } from "@ant-design/icons"; import { @@ -12,6 +12,7 @@ import { isVarExpr, WorkflowNode, workflowNodeIOOptions, + WorkflowNodeIoValueType, } from "@/domain/workflow"; import { FormInstance } from "antd"; import { useZustandShallowSelector } from "@/hooks"; @@ -19,7 +20,7 @@ import { useWorkflowStore } from "@/stores/workflow"; // 表单内部使用的扁平结构 - 修改后只保留必要字段 export interface ConditionItem { - leftSelector: WorkflowNodeIOValueSelector; + leftSelector: string; operator: ComparisonOperator; rightValue: string; } @@ -50,7 +51,7 @@ const initFormModel = (): ConditionNodeConfigFormFieldValues => { return { conditions: [ { - leftSelector: undefined as unknown as WorkflowNodeIOValueSelector, + leftSelector: "", operator: "==", rightValue: "", }, @@ -71,7 +72,7 @@ const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { // 确保左侧是变量,右侧是常量 if (isVarExpr(expr.left) && isConstExpr(expr.right)) { conditions.push({ - leftSelector: expr.left.selector, + leftSelector: `${expr.left.selector.id}#${expr.left.selector.name}#${expr.left.selector.type}`, operator: expr.op, rightValue: String(expr.right.value), }); @@ -91,6 +92,38 @@ const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { }; }; +// 根据变量类型获取适当的操作符选项 +const getOperatorsByType = (type: string): { value: ComparisonOperator; label: string }[] => { + switch (type) { + case "number": + case "string": + return [ + { value: "==", label: "等于 (==)" }, + { value: "!=", label: "不等于 (!=)" }, + { value: ">", label: "大于 (>)" }, + { value: ">=", label: "大于等于 (>=)" }, + { value: "<", label: "小于 (<)" }, + { value: "<=", label: "小于等于 (<=)" }, + ]; + case "boolean": + return [{ value: "is", label: "为" }]; + default: + return []; + } +}; + +// 从选择器字符串中提取变量类型 +const getVariableTypeFromSelector = (selector: string): string => { + if (!selector) return "string"; + + // 假设选择器格式为 "id#name#type" + const parts = selector.split("#"); + if (parts.length >= 3) { + return parts[2].toLowerCase() || "string"; + } + return "string"; +}; + const ConditionNodeConfigForm = forwardRef( ({ className, style, disabled, initialValues, onValuesChange, nodeId }, ref) => { const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); @@ -127,6 +160,12 @@ const ConditionNodeConfigForm = forwardRef { setFormModel(values); + + if (onValuesChange) { + // 将表单值转换为表达式 + const expression = formToExpression(values); + onValuesChange({ expression }); + } }; return ( @@ -141,7 +180,6 @@ const ConditionNodeConfigForm = forwardRef 1 ? @@ -266,10 +284,10 @@ const ConditionNodeConfigForm = forwardRef {formModel.conditions && formModel.conditions.length > 1 && ( - + - 满足所有条件 (AND) - 满足任一条件 (OR) + {t(`${prefix}.logical_operator.and`)} + {t(`${prefix}.logical_operator.or`)} )} diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 7b53e6e4..a0288838 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -790,6 +790,26 @@ "workflow_node.branch.label": "Parallel branch", "workflow_node.condition.label": "Branch", + "workflow_node.condition.form.variable.placeholder": "Please select variable", + "workflow_node.condition.form.variable.errmsg": "Please select variable", + "workflow_node.condition.form.operator.errmsg": "Please select operator", + "workflow_node.condition.form.value.errmsg": "Please enter value", + "workflow_node.condition.form.value.string.placeholder": "Please enter value", + "workflow_node.condition.form.value.number.placeholder": "Please enter value", + "workflow_node.condition.form.value.boolean.placeholder": "Please select value", + "workflow_node.condition.form.value.boolean.true": "True", + "workflow_node.condition.form.value.boolean.false": "False", + "workflow_node.condition.form.add_condition.button": "Add condition", + "workflow_node.condition.form.logical_operator.label": "Logical operator", + "workflow_node.condition.form.logical_operator.and": "Meet all conditions (AND)", + "workflow_node.condition.form.logical_operator.or": "Meet any condition (OR)", + "workflow_node.condition.form.comparison.equal": "Equal", + "workflow_node.condition.form.comparison.not_equal": "Not equal", + "workflow_node.condition.form.comparison.greater_than": "Greater than", + "workflow_node.condition.form.comparison.greater_than_or_equal": "Greater than or equal", + "workflow_node.condition.form.comparison.less_than": "Less than", + "workflow_node.condition.form.comparison.less_than_or_equal": "Less than or equal", + "workflow_node.condition.form.comparison.is": "Is", "workflow_node.execute_result_branch.label": "Execution result branch", @@ -797,3 +817,4 @@ "workflow_node.execute_failure.label": "If the previous node failed ..." } + diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 89cbfc11..9381aa71 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -789,6 +789,26 @@ "workflow_node.branch.label": "并行分支", "workflow_node.condition.label": "分支", + "workflow_node.condition.form.variable.placeholder": "选择变量", + "workflow_node.condition.form.variable.errmsg": "请选择变量", + "workflow_node.condition.form.operator.errmsg": "请选择操作符", + "workflow_node.condition.form.value.errmsg": "请输入值", + "workflow_node.condition.form.value.string.placeholder": "输入值", + "workflow_node.condition.form.value.number.placeholder": "输入数值", + "workflow_node.condition.form.value.boolean.placeholder": "选择值", + "workflow_node.condition.form.value.boolean.true": "是", + "workflow_node.condition.form.value.boolean.false": "否", + "workflow_node.condition.form.add_condition.button": "添加条件", + "workflow_node.condition.form.logical_operator.label": "条件逻辑", + "workflow_node.condition.form.logical_operator.and": "满足所有条件 (AND)", + "workflow_node.condition.form.logical_operator.or": "满足任一条件 (OR)", + "workflow_node.condition.form.comparison.equal": "等于", + "workflow_node.condition.form.comparison.not_equal": "不等于", + "workflow_node.condition.form.comparison.greater_than": "大于", + "workflow_node.condition.form.comparison.greater_than_or_equal": "大于等于", + "workflow_node.condition.form.comparison.less_than": "小于", + "workflow_node.condition.form.comparison.less_than_or_equal": "小于等于", + "workflow_node.condition.form.comparison.is": "为", "workflow_node.execute_result_branch.label": "执行结果分支", @@ -796,3 +816,4 @@ "workflow_node.execute_failure.label": "若前序节点执行失败…" } + From 97d692910baa5077488072909201ac613f41d3d4 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Tue, 20 May 2025 18:09:42 +0800 Subject: [PATCH 07/28] expression evaluate --- internal/domain/expr.go | 262 ++++++++++++++++++ internal/domain/workflow.go | 21 ++ internal/workflow/dispatcher/invoker.go | 5 + .../workflow/node-processor/apply_node.go | 11 + .../workflow/node-processor/condition_node.go | 30 +- internal/workflow/node-processor/context.go | 126 +++++++++ .../workflow/node-processor/deploy_node.go | 2 + .../node-processor/execute_failure_node.go | 2 + .../node-processor/execute_success_node.go | 2 + .../workflow/node-processor/notify_node.go | 2 + internal/workflow/node-processor/processor.go | 16 ++ .../workflow/node-processor/start_node.go | 2 + .../workflow/node-processor/upload_node.go | 8 + .../workflow/node/ConditionNode.tsx | 25 +- ui/src/domain/workflow.ts | 2 +- 15 files changed, 511 insertions(+), 5 deletions(-) create mode 100644 internal/domain/expr.go create mode 100644 internal/workflow/node-processor/context.go diff --git a/internal/domain/expr.go b/internal/domain/expr.go new file mode 100644 index 00000000..3b312642 --- /dev/null +++ b/internal/domain/expr.go @@ -0,0 +1,262 @@ +package domain + +import ( + "encoding/json" + "fmt" +) + +type Value any + +type ( + ComparisonOperator string + LogicalOperator string +) + +const ( + GreaterThan ComparisonOperator = ">" + LessThan ComparisonOperator = "<" + GreaterOrEqual ComparisonOperator = ">=" + LessOrEqual ComparisonOperator = "<=" + Equal ComparisonOperator = "==" + NotEqual ComparisonOperator = "!=" + Is ComparisonOperator = "is" + + And LogicalOperator = "and" + Or LogicalOperator = "or" + Not LogicalOperator = "not" +) + +type Expr interface { + GetType() string + Eval(variables map[string]map[string]any) (any, error) +} + +type ConstExpr struct { + Type string `json:"type"` + Value Value `json:"value"` +} + +func (c ConstExpr) GetType() string { return c.Type } + +type VarExpr struct { + Type string `json:"type"` + Selector WorkflowNodeIOValueSelector `json:"selector"` +} + +func (v VarExpr) GetType() string { return v.Type } + +func (v VarExpr) Eval(variables map[string]map[string]any) (any, error) { + if v.Selector.Id == "" { + return nil, fmt.Errorf("node id is empty") + } + if v.Selector.Name == "" { + return nil, fmt.Errorf("name is empty") + } + + if _, ok := variables[v.Selector.Id]; !ok { + return nil, fmt.Errorf("node %s not found", v.Selector.Id) + } + + if _, ok := variables[v.Selector.Id][v.Selector.Name]; !ok { + return nil, fmt.Errorf("variable %s not found in node %s", v.Selector.Name, v.Selector.NodeId) + } + + return variables[v.Selector.Id][v.Selector.Name], nil +} + +type CompareExpr struct { + Type string `json:"type"` // compare + Op ComparisonOperator `json:"op"` + Left Expr `json:"left"` + Right Expr `json:"right"` +} + +func (c CompareExpr) GetType() string { return c.Type } + +func (c CompareExpr) Eval(variables map[string]map[string]any) (any, error) { + left, err := c.Left.Eval(variables) + if err != nil { + return nil, err + } + right, err := c.Right.Eval(variables) + if err != nil { + return nil, err + } + + switch c.Op { + case GreaterThan: + return left.(float64) > right.(float64), nil + case LessThan: + return left.(float64) < right.(float64), nil + case GreaterOrEqual: + return left.(float64) >= right.(float64), nil + case LessOrEqual: + return left.(float64) <= right.(float64), nil + case Equal: + return left == right, nil + case NotEqual: + return left != right, nil + case Is: + return left == right, nil + default: + return nil, fmt.Errorf("unknown operator: %s", c.Op) + } +} + +type LogicalExpr struct { + Type string `json:"type"` // logical + Op LogicalOperator `json:"op"` + Left Expr `json:"left"` + Right Expr `json:"right"` +} + +func (l LogicalExpr) GetType() string { return l.Type } + +func (l LogicalExpr) Eval(variables map[string]map[string]any) (any, error) { + left, err := l.Left.Eval(variables) + if err != nil { + return nil, err + } + right, err := l.Right.Eval(variables) + if err != nil { + return nil, err + } + + switch l.Op { + case And: + return left.(bool) && right.(bool), nil + case Or: + return left.(bool) || right.(bool), nil + default: + return nil, fmt.Errorf("unknown operator: %s", l.Op) + } +} + +type NotExpr struct { + Type string `json:"type"` // not + Expr Expr `json:"expr"` +} + +func (n NotExpr) GetType() string { return n.Type } + +func (n NotExpr) Eval(variables map[string]map[string]any) (any, error) { + inner, err := n.Expr.Eval(variables) + if err != nil { + return nil, err + } + return !inner.(bool), nil +} + +type rawExpr struct { + Type string `json:"type"` +} + +func MarshalExpr(e Expr) ([]byte, error) { + return json.Marshal(e) +} + +func UnmarshalExpr(data []byte) (Expr, error) { + var typ rawExpr + if err := json.Unmarshal(data, &typ); err != nil { + return nil, err + } + + switch typ.Type { + case "const": + var e ConstExpr + if err := json.Unmarshal(data, &e); err != nil { + return nil, err + } + return e, nil + case "var": + var e VarExpr + if err := json.Unmarshal(data, &e); err != nil { + return nil, err + } + return e, nil + case "compare": + var e CompareExprRaw + if err := json.Unmarshal(data, &e); err != nil { + return nil, err + } + return e.ToCompareExpr() + case "logical": + var e LogicalExprRaw + if err := json.Unmarshal(data, &e); err != nil { + return nil, err + } + return e.ToLogicalExpr() + case "not": + var e NotExprRaw + if err := json.Unmarshal(data, &e); err != nil { + return nil, err + } + return e.ToNotExpr() + default: + return nil, fmt.Errorf("unknown expr type: %s", typ.Type) + } +} + +type CompareExprRaw struct { + Type string `json:"type"` + Op ComparisonOperator `json:"op"` + Left json.RawMessage `json:"left"` + Right json.RawMessage `json:"right"` +} + +func (r CompareExprRaw) ToCompareExpr() (CompareExpr, error) { + leftExpr, err := UnmarshalExpr(r.Left) + if err != nil { + return CompareExpr{}, err + } + rightExpr, err := UnmarshalExpr(r.Right) + if err != nil { + return CompareExpr{}, err + } + return CompareExpr{ + Type: r.Type, + Op: r.Op, + Left: leftExpr, + Right: rightExpr, + }, nil +} + +type LogicalExprRaw struct { + Type string `json:"type"` + Op LogicalOperator `json:"op"` + Left json.RawMessage `json:"left"` + Right json.RawMessage `json:"right"` +} + +func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) { + left, err := UnmarshalExpr(r.Left) + if err != nil { + return LogicalExpr{}, err + } + right, err := UnmarshalExpr(r.Right) + if err != nil { + return LogicalExpr{}, err + } + return LogicalExpr{ + Type: r.Type, + Op: r.Op, + Left: left, + Right: right, + }, nil +} + +type NotExprRaw struct { + Type string `json:"type"` + Expr json.RawMessage `json:"expr"` +} + +func (r NotExprRaw) ToNotExpr() (NotExpr, error) { + inner, err := UnmarshalExpr(r.Expr) + if err != nil { + return NotExpr{}, err + } + return NotExpr{ + Type: r.Type, + Expr: inner, + }, nil +} diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 6f3cccea..e1e72354 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -81,6 +81,10 @@ type WorkflowNodeConfigForApply struct { SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30) } +type WorkflowNodeConfigForCondition struct { + Expression Expr `json:"expression"` // 条件表达式 +} + type WorkflowNodeConfigForUpload struct { Certificate string `json:"certificate"` PrivateKey string `json:"privateKey"` @@ -104,6 +108,22 @@ type WorkflowNodeConfigForNotify struct { Message string `json:"message"` // 通知内容 } +func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { + raw := maputil.GetString(n.Config, "expression") + if raw == "" { + return WorkflowNodeConfigForCondition{} + } + + expr, err := UnmarshalExpr([]byte(raw)) + if err != nil { + return WorkflowNodeConfigForCondition{} + } + + return WorkflowNodeConfigForCondition{ + Expression: expr, + } +} + func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { skipBeforeExpiryDays := maputil.GetInt32(n.Config, "skipBeforeExpiryDays") if skipBeforeExpiryDays == 0 { @@ -171,6 +191,7 @@ type WorkflowNodeIO struct { type WorkflowNodeIOValueSelector struct { Id string `json:"id"` Name string `json:"name"` + Type string `json:"type"` } const WorkflowNodeIONameCertificate string = "certificate" diff --git a/internal/workflow/dispatcher/invoker.go b/internal/workflow/dispatcher/invoker.go index c644b26b..a4de08e7 100644 --- a/internal/workflow/dispatcher/invoker.go +++ b/internal/workflow/dispatcher/invoker.go @@ -101,6 +101,11 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow processor.GetLogger().Error(procErr.Error()) break } + + nodeOutputs := processor.GetOutputs() + if len(nodeOutputs) > 0 { + ctx = nodes.AddNodeOutput(ctx, current.Id, nodeOutputs) + } } break diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 97b7575d..468f553b 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -16,6 +16,7 @@ import ( type applyNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer certRepo certificateRepository outputRepo workflowOutputRepository @@ -25,6 +26,7 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode { return &applyNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), certRepo: repository.NewCertificateRepository(), outputRepo: repository.NewWorkflowOutputRepository(), @@ -71,6 +73,7 @@ func (n *applyNode) Process(ctx context.Context) error { n.logger.Warn("failed to parse certificate, may be the CA responded error") return err } + certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeWorkflow, Certificate: applyResult.CertificateFullChain, @@ -96,6 +99,10 @@ func (n *applyNode) Process(ctx context.Context) error { return err } + // 添加中间结果 + n.outputs["certificate.validated"] = true + n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24) + n.logger.Info("apply completed") return nil @@ -139,6 +146,10 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if expirationTime > renewalInterval { + + n.outputs["certificate.validated"] = true + n.outputs["certificate.daysLeft"] = int(expirationTime.Hours() / 24) + return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) } } diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index 2bac55fa..f8ed228b 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -2,6 +2,7 @@ package nodeprocessor import ( "context" + "errors" "github.com/usual2970/certimate/internal/domain" ) @@ -9,16 +10,43 @@ import ( type conditionNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer } func NewConditionNode(node *domain.WorkflowNode) *conditionNode { return &conditionNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), } } func (n *conditionNode) Process(ctx context.Context) error { - // 此类型节点不需要执行任何操作,直接返回 + n.logger.Info("enter condition node: " + n.node.Name) + + nodeConfig := n.node.GetConfigForCondition() + if nodeConfig.Expression == nil { + return nil + } return nil } + +func (n *conditionNode) eval(ctx context.Context, expression domain.Expr) (any, error) { + switch expr:=expression.(type) { + case domain.CompareExpr: + left,err:= n.eval(ctx, expr.Left) + if err != nil { + return nil, err + } + right,err:= n.eval(ctx, expr.Right) + if err != nil { + return nil, err + } + + case domain.LogicalExpr: + case domain.NotExpr: + case domain.VarExpr: + case domain.ConstExpr: + } + return false, errors.New("unknown expression type") +} diff --git a/internal/workflow/node-processor/context.go b/internal/workflow/node-processor/context.go new file mode 100644 index 00000000..adceacf6 --- /dev/null +++ b/internal/workflow/node-processor/context.go @@ -0,0 +1,126 @@ +package nodeprocessor + +import ( + "context" + "sync" +) + +// 定义上下文键类型,避免键冲突 +type workflowContextKey string + +const ( + nodeOutputsKey workflowContextKey = "node_outputs" +) + +// 带互斥锁的节点输出容器 +type nodeOutputsContainer struct { + sync.RWMutex + outputs map[string]map[string]any +} + +// 创建新的并发安全的节点输出容器 +func newNodeOutputsContainer() *nodeOutputsContainer { + return &nodeOutputsContainer{ + outputs: make(map[string]map[string]any), + } +} + +// 添加节点输出到上下文 +func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context { + container := getNodeOutputsContainer(ctx) + if container == nil { + container = newNodeOutputsContainer() + } + + container.Lock() + defer container.Unlock() + + // 创建输出的深拷贝以避免后续修改 + outputCopy := make(map[string]any, len(output)) + for k, v := range output { + outputCopy[k] = v + } + + container.outputs[nodeId] = outputCopy + return context.WithValue(ctx, nodeOutputsKey, container) +} + +// 从上下文获取节点输出 +func GetNodeOutput(ctx context.Context, nodeId string) map[string]any { + container := getNodeOutputsContainer(ctx) + if container == nil { + return nil + } + + container.RLock() + defer container.RUnlock() + + output, exists := container.outputs[nodeId] + if !exists { + return nil + } + + outputCopy := make(map[string]any, len(output)) + for k, v := range output { + outputCopy[k] = v + } + + return outputCopy +} + +// 获取特定节点的特定输出项 +func GetNodeOutputValue(ctx context.Context, nodeId string, key string) (any, bool) { + output := GetNodeOutput(ctx, nodeId) + if output == nil { + return nil, false + } + + value, exists := output[key] + return value, exists +} + +// 获取所有节点输出 +func GetNodeOutputs(ctx context.Context) map[string]map[string]any { + container := getNodeOutputsContainer(ctx) + if container == nil { + return nil + } + + container.RLock() + defer container.RUnlock() + + // 创建所有输出的深拷贝 + allOutputs := make(map[string]map[string]any, len(container.outputs)) + for nodeId, output := range container.outputs { + nodeCopy := make(map[string]any, len(output)) + for k, v := range output { + nodeCopy[k] = v + } + allOutputs[nodeId] = nodeCopy + } + + return allOutputs +} + +// 获取节点输出容器 +func getNodeOutputsContainer(ctx context.Context) *nodeOutputsContainer { + value := ctx.Value(nodeOutputsKey) + if value == nil { + return nil + } + return value.(*nodeOutputsContainer) +} + +// 检查节点是否有输出 +func HasNodeOutput(ctx context.Context, nodeId string) bool { + container := getNodeOutputsContainer(ctx) + if container == nil { + return false + } + + container.RLock() + defer container.RUnlock() + + _, exists := container.outputs[nodeId] + return exists +} diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index d60a5a7a..3819b4a2 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -15,6 +15,7 @@ import ( type deployNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer certRepo certificateRepository outputRepo workflowOutputRepository @@ -24,6 +25,7 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode { return &deployNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), certRepo: repository.NewCertificateRepository(), outputRepo: repository.NewWorkflowOutputRepository(), diff --git a/internal/workflow/node-processor/execute_failure_node.go b/internal/workflow/node-processor/execute_failure_node.go index 59f6a5bd..d3f61e30 100644 --- a/internal/workflow/node-processor/execute_failure_node.go +++ b/internal/workflow/node-processor/execute_failure_node.go @@ -9,12 +9,14 @@ import ( type executeFailureNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer } func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode { return &executeFailureNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), } } diff --git a/internal/workflow/node-processor/execute_success_node.go b/internal/workflow/node-processor/execute_success_node.go index e5b65860..46a74482 100644 --- a/internal/workflow/node-processor/execute_success_node.go +++ b/internal/workflow/node-processor/execute_success_node.go @@ -9,12 +9,14 @@ import ( type executeSuccessNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer } func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode { return &executeSuccessNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), } } diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index 1840938b..8f336931 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -12,6 +12,7 @@ import ( type notifyNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer settingsRepo settingsRepository } @@ -20,6 +21,7 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { return ¬ifyNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), settingsRepo: repository.NewSettingsRepository(), } diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index 4523b13a..eb7bc155 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -14,6 +14,8 @@ type NodeProcessor interface { SetLogger(*slog.Logger) Process(ctx context.Context) error + + GetOutputs() map[string]any } type nodeProcessor struct { @@ -32,6 +34,20 @@ func (n *nodeProcessor) SetLogger(logger *slog.Logger) { n.logger = logger } +type nodeOutputer struct { + outputs map[string]any +} + +func newNodeOutputer() *nodeOutputer { + return &nodeOutputer{ + outputs: make(map[string]any), + } +} + +func (n *nodeOutputer) GetOutputs() map[string]any { + return n.outputs +} + type certificateRepository interface { GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error) } diff --git a/internal/workflow/node-processor/start_node.go b/internal/workflow/node-processor/start_node.go index 5bbc1c09..30dee424 100644 --- a/internal/workflow/node-processor/start_node.go +++ b/internal/workflow/node-processor/start_node.go @@ -9,12 +9,14 @@ import ( type startNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer } func NewStartNode(node *domain.WorkflowNode) *startNode { return &startNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), } } diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 2da19eed..7fbb1515 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/repository" @@ -12,6 +13,7 @@ import ( type uploadNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer certRepo certificateRepository outputRepo workflowOutputRepository @@ -21,6 +23,7 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode { return &uploadNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), certRepo: repository.NewCertificateRepository(), outputRepo: repository.NewWorkflowOutputRepository(), @@ -66,6 +69,9 @@ func (n *uploadNode) Process(ctx context.Context) error { return err } + n.outputs["certificate.validated"] = true + n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24) + n.logger.Info("upload completed") return nil @@ -85,6 +91,8 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, n.node.Id) if lastCertificate != nil { + n.outputs["certificate.validated"] = true + n.outputs["certificate.daysLeft"] = int(time.Until(lastCertificate.ExpireAt).Hours() / 24) return true, "the certificate has already been uploaded" } } diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index 43b32e60..d3f1defc 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -5,7 +5,7 @@ import { Button, Card, Popover } from "antd"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; -import { Expr, WorkflowNodeIoValueType } from "@/domain/workflow"; +import { Expr, WorkflowNodeIoValueType, Value } from "@/domain/workflow"; import { produce } from "immer"; import { useWorkflowStore } from "@/stores/workflow"; import { useZustandShallowSelector } from "@/hooks"; @@ -30,15 +30,34 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP // 创建单个条件的表达式 const createComparisonExpr = (condition: ConditionItem): Expr => { const selectors = condition.leftSelector.split("#"); + const t = selectors[2] as WorkflowNodeIoValueType; const left: Expr = { type: "var", selector: { id: selectors[0], name: selectors[1], - type: selectors[2] as WorkflowNodeIoValueType, + type: t, }, }; - const right: Expr = { type: "const", value: condition.rightValue || "" }; + + let value: Value = condition.rightValue; + switch (t) { + case "boolean": + if (value === "true") { + value = true; + } else if (value === "false") { + value = false; + } + break; + case "number": + value = parseInt(value as string); + break; + case "string": + value = value as string; + break; + } + + const right: Expr = { type: "const", value: value }; return { type: "compare", diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 792b7d45..9cd12287 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -232,7 +232,7 @@ export const workflowNodeIOOptions = (node: WorkflowNode) => { // #region Condition expression -type Value = string | number | boolean; +export type Value = string | number | boolean; export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!=" | "is"; From faad7cb6d791ff59a2def22bdefb9c2422d62f49 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Tue, 20 May 2025 22:54:41 +0800 Subject: [PATCH 08/28] improve condition evaluate --- internal/domain/expr.go | 300 ++++++++++++++++-- internal/domain/expr_test.go | 127 ++++++++ internal/domain/workflow.go | 7 +- internal/workflow/dispatcher/invoker.go | 11 +- .../workflow/node-processor/condition_node.go | 35 +- .../workflow/node/ConditionNode.tsx | 2 +- .../workflow/node/ConditionNodeConfigForm.tsx | 1 + ui/src/domain/workflow.ts | 2 +- 8 files changed, 440 insertions(+), 45 deletions(-) create mode 100644 internal/domain/expr_test.go diff --git a/internal/domain/expr.go b/internal/domain/expr.go index 3b312642..4791ba7d 100644 --- a/internal/domain/expr.go +++ b/internal/domain/expr.go @@ -26,18 +26,276 @@ const ( Not LogicalOperator = "not" ) +type EvalResult struct { + Type string + Value any +} + +func (e *EvalResult) GetFloat64() (float64, error) { + if e.Type != "number" { + return 0, fmt.Errorf("type mismatch: %s", e.Type) + } + switch v := e.Value.(type) { + case int: + return float64(v), nil + case float64: + return v, nil + default: + return 0, fmt.Errorf("unsupported type: %T", v) + } +} + +func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + switch e.Type { + case "number": + + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: "boolean", + Value: left > right, + }, nil + case "string": + return &EvalResult{ + Type: "boolean", + Value: e.Value.(string) > other.Value.(string), + }, nil + + default: + return nil, fmt.Errorf("unsupported type: %s", e.Type) + } +} + +func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + switch e.Type { + case "number": + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + return &EvalResult{ + Type: "boolean", + Value: left >= right, + }, nil + case "string": + return &EvalResult{ + Type: "boolean", + Value: e.Value.(string) >= other.Value.(string), + }, nil + + default: + return nil, fmt.Errorf("unsupported type: %s", e.Type) + } +} + +func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + switch e.Type { + case "number": + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + return &EvalResult{ + Type: "boolean", + Value: left < right, + }, nil + case "string": + return &EvalResult{ + Type: "boolean", + Value: e.Value.(string) < other.Value.(string), + }, nil + + default: + return nil, fmt.Errorf("unsupported type: %s", e.Type) + } +} + +func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + switch e.Type { + case "number": + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + return &EvalResult{ + Type: "boolean", + Value: left <= right, + }, nil + case "string": + return &EvalResult{ + Type: "boolean", + Value: e.Value.(string) <= other.Value.(string), + }, nil + + default: + return nil, fmt.Errorf("unsupported type: %s", e.Type) + } +} + +func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + switch e.Type { + case "number": + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + return &EvalResult{ + Type: "boolean", + Value: left == right, + }, nil + case "string": + return &EvalResult{ + Type: "boolean", + Value: e.Value.(string) == other.Value.(string), + }, nil + + default: + return nil, fmt.Errorf("unsupported type: %s", e.Type) + } +} + +func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + switch e.Type { + case "number": + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + return &EvalResult{ + Type: "boolean", + Value: left != right, + }, nil + case "string": + return &EvalResult{ + Type: "boolean", + Value: e.Value.(string) != other.Value.(string), + }, nil + + default: + return nil, fmt.Errorf("unsupported type: %s", e.Type) + } +} + +func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + switch e.Type { + case "boolean": + return &EvalResult{ + Type: "boolean", + Value: e.Value.(bool) && other.Value.(bool), + }, nil + default: + return nil, fmt.Errorf("unsupported type: %s", e.Type) + } +} + +func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + switch e.Type { + case "boolean": + return &EvalResult{ + Type: "boolean", + Value: e.Value.(bool) || other.Value.(bool), + }, nil + default: + return nil, fmt.Errorf("unsupported type: %s", e.Type) + } +} + +func (e *EvalResult) Not() (*EvalResult, error) { + if e.Type != "boolean" { + return nil, fmt.Errorf("type mismatch: %s", e.Type) + } + return &EvalResult{ + Type: "boolean", + Value: !e.Value.(bool), + }, nil +} + +func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + switch e.Type { + case "boolean": + return &EvalResult{ + Type: "boolean", + Value: e.Value.(bool) == other.Value.(bool), + }, nil + default: + return nil, fmt.Errorf("unsupported type: %s", e.Type) + } +} + type Expr interface { GetType() string - Eval(variables map[string]map[string]any) (any, error) + Eval(variables map[string]map[string]any) (*EvalResult, error) } type ConstExpr struct { - Type string `json:"type"` - Value Value `json:"value"` + Type string `json:"type"` + Value Value `json:"value"` + ValueType string `json:"valueType"` } func (c ConstExpr) GetType() string { return c.Type } +func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { + return &EvalResult{ + Type: c.ValueType, + Value: c.Value, + }, nil +} + type VarExpr struct { Type string `json:"type"` Selector WorkflowNodeIOValueSelector `json:"selector"` @@ -45,7 +303,7 @@ type VarExpr struct { func (v VarExpr) GetType() string { return v.Type } -func (v VarExpr) Eval(variables map[string]map[string]any) (any, error) { +func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { if v.Selector.Id == "" { return nil, fmt.Errorf("node id is empty") } @@ -58,10 +316,12 @@ func (v VarExpr) Eval(variables map[string]map[string]any) (any, error) { } if _, ok := variables[v.Selector.Id][v.Selector.Name]; !ok { - return nil, fmt.Errorf("variable %s not found in node %s", v.Selector.Name, v.Selector.NodeId) + return nil, fmt.Errorf("variable %s not found in node %s", v.Selector.Name, v.Selector.Id) } - - return variables[v.Selector.Id][v.Selector.Name], nil + return &EvalResult{ + Type: v.Selector.Type, + Value: variables[v.Selector.Id][v.Selector.Name], + }, nil } type CompareExpr struct { @@ -73,7 +333,7 @@ type CompareExpr struct { func (c CompareExpr) GetType() string { return c.Type } -func (c CompareExpr) Eval(variables map[string]map[string]any) (any, error) { +func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { left, err := c.Left.Eval(variables) if err != nil { return nil, err @@ -85,19 +345,19 @@ func (c CompareExpr) Eval(variables map[string]map[string]any) (any, error) { switch c.Op { case GreaterThan: - return left.(float64) > right.(float64), nil + return left.GreaterThan(right) case LessThan: - return left.(float64) < right.(float64), nil + return left.LessThan(right) case GreaterOrEqual: - return left.(float64) >= right.(float64), nil + return left.GreaterOrEqual(right) case LessOrEqual: - return left.(float64) <= right.(float64), nil + return left.LessOrEqual(right) case Equal: - return left == right, nil + return left.Equal(right) case NotEqual: - return left != right, nil + return left.NotEqual(right) case Is: - return left == right, nil + return left.Is(right) default: return nil, fmt.Errorf("unknown operator: %s", c.Op) } @@ -112,7 +372,7 @@ type LogicalExpr struct { func (l LogicalExpr) GetType() string { return l.Type } -func (l LogicalExpr) Eval(variables map[string]map[string]any) (any, error) { +func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { left, err := l.Left.Eval(variables) if err != nil { return nil, err @@ -124,9 +384,9 @@ func (l LogicalExpr) Eval(variables map[string]map[string]any) (any, error) { switch l.Op { case And: - return left.(bool) && right.(bool), nil + return left.And(right) case Or: - return left.(bool) || right.(bool), nil + return left.Or(right) default: return nil, fmt.Errorf("unknown operator: %s", l.Op) } @@ -139,12 +399,12 @@ type NotExpr struct { func (n NotExpr) GetType() string { return n.Type } -func (n NotExpr) Eval(variables map[string]map[string]any) (any, error) { +func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { inner, err := n.Expr.Eval(variables) if err != nil { return nil, err } - return !inner.(bool), nil + return inner.Not() } type rawExpr struct { diff --git a/internal/domain/expr_test.go b/internal/domain/expr_test.go new file mode 100644 index 00000000..f0a34504 --- /dev/null +++ b/internal/domain/expr_test.go @@ -0,0 +1,127 @@ +package domain + +import ( + "testing" +) + +func TestLogicalEval(t *testing.T) { + // 测试逻辑表达式 and + logicalExpr := LogicalExpr{ + Left: ConstExpr{ + Type: "const", + Value: true, + ValueType: "boolean", + }, + Op: And, + Right: ConstExpr{ + Type: "const", + Value: true, + ValueType: "boolean", + }, + } + result, err := logicalExpr.Eval(nil) + if err != nil { + t.Errorf("failed to evaluate logical expression: %v", err) + } + if result.Value != true { + t.Errorf("expected true, got %v", result) + } + + // 测试逻辑表达式 or + orExpr := LogicalExpr{ + Left: ConstExpr{ + Type: "const", + Value: true, + ValueType: "boolean", + }, + Op: Or, + Right: ConstExpr{ + Type: "const", + Value: true, + ValueType: "boolean", + }, + } + result, err = orExpr.Eval(nil) + if err != nil { + t.Errorf("failed to evaluate logical expression: %v", err) + } + if result.Value != true { + t.Errorf("expected true, got %v", result) + } +} + +func TestUnmarshalExpr(t *testing.T) { + type args struct { + data []byte + } + tests := []struct { + name string + args args + want Expr + wantErr bool + }{ + { + name: "test1", + args: args{ + data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validated","type":"boolean"},"type":"var"},"op":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"compare"},"op":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"op":"==","right":{"type":"const","value":2,"valueType":"number"},"type":"compare"},"type":"logical"}`), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := UnmarshalExpr(tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalExpr() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got == nil { + t.Errorf("UnmarshalExpr() got = nil, want %v", tt.want) + return + } + }) + } +} + +func TestExpr_Eval(t *testing.T) { + type args struct { + variables map[string]map[string]any + data []byte + } + tests := []struct { + name string + args args + want *EvalResult + wantErr bool + }{ + { + name: "test1", + args: args{ + variables: map[string]map[string]any{ + "ODnYSOXB6HQP2_vz6JcZE": { + "certificate.validated": true, + "certificate.daysLeft": 2, + }, + }, + data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validated","type":"boolean"},"type":"var"},"op":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"compare"},"op":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"op":"==","right":{"type":"const","value":2,"valueType":"number"},"type":"compare"},"type":"logical"}`), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := UnmarshalExpr(tt.args.data) + if err != nil { + t.Errorf("UnmarshalExpr() error = %v", err) + return + } + got, err := c.Eval(tt.args.variables) + t.Log("got:", got) + if (err != nil) != tt.wantErr { + t.Errorf("ConstExpr.Eval() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got.Value != true { + t.Errorf("ConstExpr.Eval() got = %v, want %v", got.Value, true) + } + }) + } +} diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index e1e72354..63237221 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -1,6 +1,7 @@ package domain import ( + "encoding/json" "time" maputil "github.com/usual2970/certimate/internal/pkg/utils/map" @@ -109,11 +110,13 @@ type WorkflowNodeConfigForNotify struct { } func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { - raw := maputil.GetString(n.Config, "expression") - if raw == "" { + expression := n.Config["expression"] + if expression == nil { return WorkflowNodeConfigForCondition{} } + raw, _ := json.Marshal(expression) + expr, err := UnmarshalExpr([]byte(raw)) if err != nil { return WorkflowNodeConfigForCondition{} diff --git a/internal/workflow/dispatcher/invoker.go b/internal/workflow/dispatcher/invoker.go index a4de08e7..b6e4a4db 100644 --- a/internal/workflow/dispatcher/invoker.go +++ b/internal/workflow/dispatcher/invoker.go @@ -98,7 +98,9 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow procErr = processor.Process(ctx) if procErr != nil { - processor.GetLogger().Error(procErr.Error()) + if current.Type != domain.WorkflowNodeTypeCondition { + processor.GetLogger().Error(procErr.Error()) + } break } @@ -110,9 +112,12 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow break } - // TODO: 优化可读性 - if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch { + if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition { + current = nil + procErr = nil + return nil + } else if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch { return procErr } else if procErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure) diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index f8ed228b..d90811d9 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -26,27 +26,26 @@ func (n *conditionNode) Process(ctx context.Context) error { nodeConfig := n.node.GetConfigForCondition() if nodeConfig.Expression == nil { + n.logger.Info("no condition found, continue to next node") return nil } + + rs, err := n.eval(ctx, nodeConfig.Expression) + if err != nil { + n.logger.Warn("failed to eval expression: " + err.Error()) + return err + } + + if rs.Value == false { + n.logger.Info("condition not met, skip this branch") + return errors.New("condition not met") + } + + n.logger.Info("condition met, continue to next node") return nil } -func (n *conditionNode) eval(ctx context.Context, expression domain.Expr) (any, error) { - switch expr:=expression.(type) { - case domain.CompareExpr: - left,err:= n.eval(ctx, expr.Left) - if err != nil { - return nil, err - } - right,err:= n.eval(ctx, expr.Right) - if err != nil { - return nil, err - } - - case domain.LogicalExpr: - case domain.NotExpr: - case domain.VarExpr: - case domain.ConstExpr: - } - return false, errors.New("unknown expression type") +func (n *conditionNode) eval(ctx context.Context, expression domain.Expr) (*domain.EvalResult, error) { + variables := GetNodeOutputs(ctx) + return expression.Eval(variables) } diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index d3f1defc..7b2cb554 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -57,7 +57,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP break; } - const right: Expr = { type: "const", value: value }; + const right: Expr = { type: "const", value: value, valueType: t }; return { type: "compare", diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx index f2d08253..e040dc78 100644 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -318,6 +318,7 @@ const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { const right: Expr = { type: "const", value: rightValue, + valueType: type, }; return { diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 9cd12287..05c936a7 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -238,7 +238,7 @@ export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!=" | "is"; export type LogicalOperator = "and" | "or" | "not"; -export type ConstExpr = { type: "const"; value: Value }; +export type ConstExpr = { type: "const"; value: Value; valueType: WorkflowNodeIoValueType }; export type VarExpr = { type: "var"; selector: WorkflowNodeIOValueSelector }; export type CompareExpr = { type: "compare"; op: ComparisonOperator; left: Expr; right: Expr }; export type LogicalExpr = { type: "logical"; op: LogicalOperator; left: Expr; right: Expr }; From 993ca36755ad8af5a4a6181d252beb9071882314 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Wed, 21 May 2025 13:48:54 +0800 Subject: [PATCH 09/28] add certificate mornitoring node --- internal/domain/workflow.go | 23 +++ .../workflow/node-processor/inspect_node.go | 159 ++++++++++++++++++ .../node-processor/inspect_node_test.go | 39 +++++ internal/workflow/node-processor/processor.go | 2 + .../components/workflow/WorkflowElement.tsx | 4 + ui/src/components/workflow/node/AddNode.tsx | 2 + .../workflow/node/ConditionNode.tsx | 1 - .../workflow/node/ConditionNodeConfigForm.tsx | 1 - .../components/workflow/node/InspectNode.tsx | 90 ++++++++++ .../workflow/node/InspectNodeConfigForm.tsx | 85 ++++++++++ ui/src/domain/workflow.ts | 20 ++- .../i18n/locales/en/nls.workflow.nodes.json | 7 +- .../i18n/locales/zh/nls.workflow.nodes.json | 7 +- 13 files changed, 435 insertions(+), 5 deletions(-) create mode 100644 internal/workflow/node-processor/inspect_node.go create mode 100644 internal/workflow/node-processor/inspect_node_test.go create mode 100644 ui/src/components/workflow/node/InspectNode.tsx create mode 100644 ui/src/components/workflow/node/InspectNodeConfigForm.tsx diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 63237221..c1d2db86 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -38,6 +38,7 @@ const ( WorkflowNodeTypeExecuteResultBranch = WorkflowNodeType("execute_result_branch") WorkflowNodeTypeExecuteSuccess = WorkflowNodeType("execute_success") WorkflowNodeTypeExecuteFailure = WorkflowNodeType("execute_failure") + WorkflowNodeTypeInspect = WorkflowNodeType("inspect") ) type WorkflowTriggerType string @@ -86,6 +87,11 @@ type WorkflowNodeConfigForCondition struct { Expression Expr `json:"expression"` // 条件表达式 } +type WorkflowNodeConfigForInspect struct { + Domain string `json:"domain"` // 域名 + Port string `json:"port"` // 端口 +} + type WorkflowNodeConfigForUpload struct { Certificate string `json:"certificate"` PrivateKey string `json:"privateKey"` @@ -127,6 +133,23 @@ func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { } } +func (n *WorkflowNode) GetConfigForInspect() WorkflowNodeConfigForInspect { + domain := maputil.GetString(n.Config, "domain") + if domain == "" { + return WorkflowNodeConfigForInspect{} + } + + port := maputil.GetString(n.Config, "port") + if port == "" { + port = "443" + } + + return WorkflowNodeConfigForInspect{ + Domain: domain, + Port: port, + } +} + func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { skipBeforeExpiryDays := maputil.GetInt32(n.Config, "skipBeforeExpiryDays") if skipBeforeExpiryDays == 0 { diff --git a/internal/workflow/node-processor/inspect_node.go b/internal/workflow/node-processor/inspect_node.go new file mode 100644 index 00000000..6c6bea6a --- /dev/null +++ b/internal/workflow/node-processor/inspect_node.go @@ -0,0 +1,159 @@ +package nodeprocessor + +import ( + "context" + "crypto/tls" + "fmt" + "math" + "net" + "time" + + "github.com/usual2970/certimate/internal/domain" +) + +type inspectNode struct { + node *domain.WorkflowNode + *nodeProcessor + *nodeOutputer +} + +func NewInspectNode(node *domain.WorkflowNode) *inspectNode { + return &inspectNode{ + node: node, + nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), + } +} + +func (n *inspectNode) Process(ctx context.Context) error { + n.logger.Info("enter inspect website certificate node ...") + + nodeConfig := n.node.GetConfigForInspect() + + err := n.inspect(ctx, nodeConfig) + if err != nil { + n.logger.Warn("inspect website certificate failed: " + err.Error()) + return err + } + + return nil +} + +func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNodeConfigForInspect) error { + // 定义重试参数 + maxRetries := 3 + retryInterval := 2 * time.Second + + var cert *tls.Certificate + var lastError error + + domainWithPort := nodeConfig.Domain + ":" + nodeConfig.Port + + for attempt := 0; attempt < maxRetries; attempt++ { + if attempt > 0 { + n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, domainWithPort)) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(retryInterval): + // Wait for retry interval + } + } + + dialer := &net.Dialer{ + Timeout: 10 * time.Second, + } + + conn, err := tls.DialWithDialer(dialer, "tcp", domainWithPort, &tls.Config{ + InsecureSkipVerify: true, // Allow self-signed certificates + }) + if err != nil { + lastError = fmt.Errorf("failed to connect to %s: %w", domainWithPort, err) + n.logger.Warn(fmt.Sprintf("Connection attempt #%d failed: %s", attempt+1, lastError.Error())) + continue + } + + // Get certificate information + certInfo := conn.ConnectionState().PeerCertificates[0] + conn.Close() + + // Certificate information retrieved successfully + cert = &tls.Certificate{ + Certificate: [][]byte{certInfo.Raw}, + Leaf: certInfo, + } + lastError = nil + n.logger.Info(fmt.Sprintf("Successfully retrieved certificate information for %s", domainWithPort)) + break + } + + if lastError != nil { + return fmt.Errorf("failed to retrieve certificate after %d attempts: %w", maxRetries, lastError) + } + + certInfo := cert.Leaf + now := time.Now() + + isValid := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore) + + // Check domain matching + domainMatch := false + if len(certInfo.DNSNames) > 0 { + for _, dnsName := range certInfo.DNSNames { + if matchDomain(nodeConfig.Domain, dnsName) { + domainMatch = true + break + } + } + } else if matchDomain(nodeConfig.Domain, certInfo.Subject.CommonName) { + domainMatch = true + } + + isValid = isValid && domainMatch + + daysRemaining := math.Floor(certInfo.NotAfter.Sub(now).Hours() / 24) + + // Set node outputs + outputs := map[string]any{ + "certificate.validated": isValid, + "certificate.daysLeft": daysRemaining, + } + n.setOutputs(outputs) + + return nil +} + +func (n *inspectNode) setOutputs(outputs map[string]any) { + n.outputs = outputs +} + +func matchDomain(requestDomain, certDomain string) bool { + if requestDomain == certDomain { + return true + } + + if len(certDomain) > 2 && certDomain[0] == '*' && certDomain[1] == '.' { + + wildcardSuffix := certDomain[1:] + requestDomainLen := len(requestDomain) + suffixLen := len(wildcardSuffix) + + if requestDomainLen > suffixLen && requestDomain[requestDomainLen-suffixLen:] == wildcardSuffix { + remainingPart := requestDomain[:requestDomainLen-suffixLen] + if len(remainingPart) > 0 && !contains(remainingPart, '.') { + return true + } + } + } + + return false +} + +func contains(s string, c byte) bool { + for i := 0; i < len(s); i++ { + if s[i] == c { + return true + } + } + return false +} diff --git a/internal/workflow/node-processor/inspect_node_test.go b/internal/workflow/node-processor/inspect_node_test.go new file mode 100644 index 00000000..5cb826c1 --- /dev/null +++ b/internal/workflow/node-processor/inspect_node_test.go @@ -0,0 +1,39 @@ +package nodeprocessor + +import ( + "context" + "testing" + + "github.com/usual2970/certimate/internal/domain" +) + +func Test_inspectWebsiteCertificateNode_inspect(t *testing.T) { + type args struct { + ctx context.Context + nodeConfig domain.WorkflowNodeConfigForInspect + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "test1", + args: args{ + ctx: context.Background(), + nodeConfig: domain.WorkflowNodeConfigForInspect{ + Domain: "baidu.com", + Port: "443", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := NewInspectNode(&domain.WorkflowNode{}) + if err := n.inspect(tt.args.ctx, tt.args.nodeConfig); (err != nil) != tt.wantErr { + t.Errorf("inspectWebsiteCertificateNode.inspect() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index eb7bc155..4cdfe76f 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -86,6 +86,8 @@ func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) { return NewExecuteSuccessNode(node), nil case domain.WorkflowNodeTypeExecuteFailure: return NewExecuteFailureNode(node), nil + case domain.WorkflowNodeTypeInspect: + return NewInspectNode(node), nil } return nil, fmt.Errorf("supported node type: %s", string(node.Type)) diff --git a/ui/src/components/workflow/WorkflowElement.tsx b/ui/src/components/workflow/WorkflowElement.tsx index 3aa70ff3..d36029df 100644 --- a/ui/src/components/workflow/WorkflowElement.tsx +++ b/ui/src/components/workflow/WorkflowElement.tsx @@ -12,6 +12,7 @@ import ExecuteResultNode from "./node/ExecuteResultNode"; import NotifyNode from "./node/NotifyNode"; import StartNode from "./node/StartNode"; import UploadNode from "./node/UploadNode"; +import InspectNode from "./node/InspectNode"; export type WorkflowElementProps = { node: WorkflowNode; @@ -31,6 +32,9 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem case WorkflowNodeType.Upload: return ; + + case WorkflowNodeType.Inspect: + return ; case WorkflowNodeType.Deploy: return ; diff --git a/ui/src/components/workflow/node/AddNode.tsx b/ui/src/components/workflow/node/AddNode.tsx index fb697e19..bf4c5be2 100644 --- a/ui/src/components/workflow/node/AddNode.tsx +++ b/ui/src/components/workflow/node/AddNode.tsx @@ -7,6 +7,7 @@ import { SendOutlined as SendOutlinedIcon, SisternodeOutlined as SisternodeOutlinedIcon, SolutionOutlined as SolutionOutlinedIcon, + MonitorOutlined as MonitorOutlinedIcon, } from "@ant-design/icons"; import { Dropdown } from "antd"; @@ -27,6 +28,7 @@ const AddNode = ({ node, disabled }: AddNodeProps) => { return [ [WorkflowNodeType.Apply, "workflow_node.apply.label", ], [WorkflowNodeType.Upload, "workflow_node.upload.label", ], + [WorkflowNodeType.Inspect, "workflow_node.inspect.label", ], [WorkflowNodeType.Deploy, "workflow_node.deploy.label", ], [WorkflowNodeType.Notify, "workflow_node.notify.label", ], [WorkflowNodeType.Branch, "workflow_node.branch.label", ], diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index 7b2cb554..417db4af 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -156,4 +156,3 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP }; export default memo(ConditionNode); - diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx index e040dc78..81022a28 100644 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -350,4 +350,3 @@ const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { }; export default memo(ConditionNodeConfigForm); - diff --git a/ui/src/components/workflow/node/InspectNode.tsx b/ui/src/components/workflow/node/InspectNode.tsx new file mode 100644 index 00000000..fa4324e2 --- /dev/null +++ b/ui/src/components/workflow/node/InspectNode.tsx @@ -0,0 +1,90 @@ +import { memo, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Flex, Typography } from "antd"; +import { produce } from "immer"; + +import { type WorkflowNodeConfigForInspect, WorkflowNodeType } from "@/domain/workflow"; +import { useZustandShallowSelector } from "@/hooks"; +import { useWorkflowStore } from "@/stores/workflow"; + +import SharedNode, { type SharedNodeProps } from "./_SharedNode"; +import InspectNodeConfigForm, { type InspectNodeConfigFormInstance } from "./InspectNodeConfigForm"; + +export type InspectNodeProps = SharedNodeProps; + +const InspectNode = ({ node, disabled }: InspectNodeProps) => { + if (node.type !== WorkflowNodeType.Inspect) { + console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Inspect}`); + } + + const { t } = useTranslation(); + + const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); + + const formRef = useRef(null); + const [formPending, setFormPending] = useState(false); + + const [drawerOpen, setDrawerOpen] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForInspect; + + const wrappedEl = useMemo(() => { + if (node.type !== WorkflowNodeType.Inspect) { + console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Inspect}`); + } + + if (!node.validated) { + return {t("workflow_node.action.configure_node")}; + } + + const config = (node.config as WorkflowNodeConfigForInspect) ?? {}; + return ( + + {config.domain ?? ""} + + ); + }, [node]); + + const handleDrawerConfirm = async () => { + setFormPending(true); + try { + await formRef.current!.validateFields(); + } catch (err) { + setFormPending(false); + throw err; + } + + try { + const newValues = getFormValues(); + const newNode = produce(node, (draft) => { + draft.config = { + ...newValues, + }; + draft.validated = true; + }); + await updateNode(newNode); + } finally { + setFormPending(false); + } + }; + + return ( + <> + setDrawerOpen(true)}> + {wrappedEl} + + + setDrawerOpen(open)} + getFormValues={() => formRef.current!.getFieldsValue()} + > + + + + ); +}; + +export default memo(InspectNode); diff --git a/ui/src/components/workflow/node/InspectNodeConfigForm.tsx b/ui/src/components/workflow/node/InspectNodeConfigForm.tsx new file mode 100644 index 00000000..ea9573e5 --- /dev/null +++ b/ui/src/components/workflow/node/InspectNodeConfigForm.tsx @@ -0,0 +1,85 @@ +import { forwardRef, memo, useImperativeHandle } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type WorkflowNodeConfigForInspect } from "@/domain/workflow"; +import { useAntdForm } from "@/hooks"; + +import { validDomainName, validPortNumber } from "@/utils/validators"; + +type InspectNodeConfigFormFieldValues = Partial; + +export type InspectNodeConfigFormProps = { + className?: string; + style?: React.CSSProperties; + disabled?: boolean; + initialValues?: InspectNodeConfigFormFieldValues; + onValuesChange?: (values: InspectNodeConfigFormFieldValues) => void; +}; + +export type InspectNodeConfigFormInstance = { + getFieldsValue: () => ReturnType["getFieldsValue"]>; + resetFields: FormInstance["resetFields"]; + validateFields: FormInstance["validateFields"]; +}; + +const initFormModel = (): InspectNodeConfigFormFieldValues => { + return { + domain: "", + port: "443", + }; +}; + +const InspectNodeConfigForm = forwardRef( + ({ className, style, disabled, initialValues, onValuesChange }, ref) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + domain: z.string().refine((val) => validDomainName(val), { + message: t("workflow_node.inspect.form.domain.placeholder"), + }), + port: z.string().refine((val) => validPortNumber(val), { + message: t("workflow_node.inspect.form.port.placeholder"), + }), + }); + const formRule = createSchemaFieldRule(formSchema); + const { form: formInst, formProps } = useAntdForm({ + name: "workflowNodeInspectConfigForm", + initialValues: initialValues ?? initFormModel(), + }); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values as InspectNodeConfigFormFieldValues); + }; + + useImperativeHandle(ref, () => { + return { + getFieldsValue: () => { + return formInst.getFieldsValue(true); + }, + resetFields: (fields) => { + return formInst.resetFields(fields as (keyof InspectNodeConfigFormFieldValues)[]); + }, + validateFields: (nameList, config) => { + return formInst.validateFields(nameList, config); + }, + } as InspectNodeConfigFormInstance; + }); + + return ( +
+ + + + + + + +
+ ); + } +); + +export default memo(InspectNodeConfigForm); diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 05c936a7..6b951b49 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -31,6 +31,7 @@ export enum WorkflowNodeType { End = "end", Apply = "apply", Upload = "upload", + Inspect = "inspect", Deploy = "deploy", Notify = "notify", Branch = "branch", @@ -46,6 +47,7 @@ const workflowNodeTypeDefaultNames: Map = new Map([ [WorkflowNodeType.End, i18n.t("workflow_node.end.label")], [WorkflowNodeType.Apply, i18n.t("workflow_node.apply.label")], [WorkflowNodeType.Upload, i18n.t("workflow_node.upload.label")], + [WorkflowNodeType.Inspect, i18n.t("workflow_node.inspect.label")], [WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.label")], [WorkflowNodeType.Notify, i18n.t("workflow_node.notify.label")], [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")], @@ -95,6 +97,17 @@ const workflowNodeTypeDefaultOutputs: Map = }, ], ], + [ + WorkflowNodeType.Inspect, + [ + { + name: "certificate", + type: "certificate", + required: true, + label: "证书", + }, + ], + ], [WorkflowNodeType.Deploy, []], [WorkflowNodeType.Notify, []], ]); @@ -145,6 +158,11 @@ export type WorkflowNodeConfigForUpload = { privateKey: string; }; +export type WorkflowNodeConfigForInspect = { + domain: string; + port: string; +}; + export type WorkflowNodeConfigForDeploy = { certificate: string; provider: string; @@ -313,6 +331,7 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {} case WorkflowNodeType.Apply: case WorkflowNodeType.Upload: case WorkflowNodeType.Deploy: + case WorkflowNodeType.Inspect: { node.inputs = workflowNodeTypeDefaultInputs.get(nodeType); node.outputs = workflowNodeTypeDefaultOutputs.get(nodeType); @@ -582,4 +601,3 @@ export const isAllNodesValidated = (node: WorkflowNode): boolean => { return true; }; - diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index a0288838..80cba031 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -753,6 +753,12 @@ "workflow_node.upload.form.private_key.label": "Private key (PEM format)", "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", + "workflow_node.inspect.label": "Inspect certificate", + "workflow_node.inspect.form.domain.label": "Domain", + "workflow_node.inspect.form.domain.placeholder": "Please enter domain name", + "workflow_node.inspect.form.port.label": "Port", + "workflow_node.inspect.form.port.placeholder": "Please enter port", + "workflow_node.notify.label": "Notification", "workflow_node.notify.form.subject.label": "Subject", "workflow_node.notify.form.subject.placeholder": "Please enter subject", @@ -817,4 +823,3 @@ "workflow_node.execute_failure.label": "If the previous node failed ..." } - diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 9381aa71..4fce90b7 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -752,6 +752,12 @@ "workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)", "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", + "workflow_node.inspect.label": "检查网站证书", + "workflow_node.inspect.form.domain.label": "域名", + "workflow_node.inspect.form.domain.placeholder": "请输入要检查的网站域名", + "workflow_node.inspect.form.port.label": "端口号", + "workflow_node.inspect.form.port.placeholder": "请输入要检查的端口号", + "workflow_node.notify.label": "推送通知", "workflow_node.notify.form.subject.label": "通知主题", "workflow_node.notify.form.subject.placeholder": "请输入通知主题", @@ -816,4 +822,3 @@ "workflow_node.execute_failure.label": "若前序节点执行失败…" } - From 75326b1dddc82a6d45f3757b7a3b60a393f0c88f Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Wed, 21 May 2025 15:59:02 +0800 Subject: [PATCH 10/28] refactor code --- internal/domain/expr.go | 79 ++++++++++++++++++++----------------- internal/domain/workflow.go | 6 +-- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/internal/domain/expr.go b/internal/domain/expr.go index 4791ba7d..9d1a744e 100644 --- a/internal/domain/expr.go +++ b/internal/domain/expr.go @@ -10,6 +10,7 @@ type Value any type ( ComparisonOperator string LogicalOperator string + ValueType string ) const ( @@ -24,15 +25,19 @@ const ( And LogicalOperator = "and" Or LogicalOperator = "or" Not LogicalOperator = "not" + + Number ValueType = "number" + String ValueType = "string" + Boolean ValueType = "boolean" ) type EvalResult struct { - Type string + Type ValueType Value any } func (e *EvalResult) GetFloat64() (float64, error) { - if e.Type != "number" { + if e.Type != Number { return 0, fmt.Errorf("type mismatch: %s", e.Type) } switch v := e.Value.(type) { @@ -50,7 +55,7 @@ func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { - case "number": + case Number: left, err := e.GetFloat64() if err != nil { @@ -62,12 +67,12 @@ func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { } return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: left > right, }, nil - case "string": + case String: return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: e.Value.(string) > other.Value.(string), }, nil @@ -81,7 +86,7 @@ func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { - case "number": + case Number: left, err := e.GetFloat64() if err != nil { return nil, err @@ -91,12 +96,12 @@ func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) { return nil, err } return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: left >= right, }, nil - case "string": + case String: return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: e.Value.(string) >= other.Value.(string), }, nil @@ -110,7 +115,7 @@ func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { - case "number": + case Number: left, err := e.GetFloat64() if err != nil { return nil, err @@ -120,12 +125,12 @@ func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) { return nil, err } return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: left < right, }, nil - case "string": + case String: return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: e.Value.(string) < other.Value.(string), }, nil @@ -139,7 +144,7 @@ func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { - case "number": + case Number: left, err := e.GetFloat64() if err != nil { return nil, err @@ -149,12 +154,12 @@ func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) { return nil, err } return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: left <= right, }, nil - case "string": + case String: return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: e.Value.(string) <= other.Value.(string), }, nil @@ -168,7 +173,7 @@ func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { - case "number": + case Number: left, err := e.GetFloat64() if err != nil { return nil, err @@ -178,12 +183,12 @@ func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) { return nil, err } return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: left == right, }, nil - case "string": + case String: return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: e.Value.(string) == other.Value.(string), }, nil @@ -197,7 +202,7 @@ func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { - case "number": + case Number: left, err := e.GetFloat64() if err != nil { return nil, err @@ -207,12 +212,12 @@ func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) { return nil, err } return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: left != right, }, nil - case "string": + case String: return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: e.Value.(string) != other.Value.(string), }, nil @@ -226,9 +231,9 @@ func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { - case "boolean": + case Boolean: return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: e.Value.(bool) && other.Value.(bool), }, nil default: @@ -241,9 +246,9 @@ func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { - case "boolean": + case Boolean: return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: e.Value.(bool) || other.Value.(bool), }, nil default: @@ -252,11 +257,11 @@ func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) { } func (e *EvalResult) Not() (*EvalResult, error) { - if e.Type != "boolean" { + if e.Type != Boolean { return nil, fmt.Errorf("type mismatch: %s", e.Type) } return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: !e.Value.(bool), }, nil } @@ -266,9 +271,9 @@ func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } switch e.Type { - case "boolean": + case Boolean: return &EvalResult{ - Type: "boolean", + Type: Boolean, Value: e.Value.(bool) == other.Value.(bool), }, nil default: @@ -282,9 +287,9 @@ type Expr interface { } type ConstExpr struct { - Type string `json:"type"` - Value Value `json:"value"` - ValueType string `json:"valueType"` + Type string `json:"type"` + Value Value `json:"value"` + ValueType ValueType `json:"valueType"` } func (c ConstExpr) GetType() string { return c.Type } diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 5213eff4..8f6522a5 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -210,9 +210,9 @@ type WorkflowNodeIO struct { } type WorkflowNodeIOValueSelector struct { - Id string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` + Id string `json:"id"` + Name string `json:"name"` + Type ValueType `json:"type"` } const WorkflowNodeIONameCertificate string = "certificate" From 9cdc59b2726ce5701bd01e25e36f1d0d30ea8b82 Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Thu, 22 May 2025 17:09:14 +0800 Subject: [PATCH 11/28] refactor code --- internal/domain/expr.go | 126 +++++++++---- internal/domain/workflow.go | 13 +- .../workflow/node-processor/apply_node.go | 10 +- internal/workflow/node-processor/const.go | 6 + .../workflow/node-processor/inspect_node.go | 176 +++++++++++------- .../workflow/node-processor/upload_node.go | 8 +- .../workflow/node/ConditionNode.tsx | 27 +-- .../workflow/node/ConditionNodeConfigForm.tsx | 26 +-- .../components/workflow/node/InspectNode.tsx | 2 +- .../workflow/node/InspectNodeConfigForm.tsx | 22 ++- ui/src/domain/workflow.ts | 52 ++++-- ui/src/i18n/locales/en/nls.workflow.json | 6 +- .../i18n/locales/en/nls.workflow.nodes.json | 4 + ui/src/i18n/locales/zh/nls.workflow.json | 6 +- .../i18n/locales/zh/nls.workflow.nodes.json | 4 + 15 files changed, 312 insertions(+), 176 deletions(-) create mode 100644 internal/workflow/node-processor/const.go diff --git a/internal/domain/expr.go b/internal/domain/expr.go index 9d1a744e..01730e3d 100644 --- a/internal/domain/expr.go +++ b/internal/domain/expr.go @@ -3,6 +3,7 @@ package domain import ( "encoding/json" "fmt" + "strconv" ) type Value any @@ -11,6 +12,7 @@ type ( ComparisonOperator string LogicalOperator string ValueType string + ExprType string ) const ( @@ -29,6 +31,12 @@ const ( Number ValueType = "number" String ValueType = "string" Boolean ValueType = "boolean" + + ConstExprType ExprType = "const" + VarExprType ExprType = "var" + CompareExprType ExprType = "compare" + LogicalExprType ExprType = "logical" + NotExprType ExprType = "not" ) type EvalResult struct { @@ -40,14 +48,40 @@ func (e *EvalResult) GetFloat64() (float64, error) { if e.Type != Number { return 0, fmt.Errorf("type mismatch: %s", e.Type) } - switch v := e.Value.(type) { - case int: - return float64(v), nil - case float64: - return v, nil - default: - return 0, fmt.Errorf("unsupported type: %T", v) + + stringValue, ok := e.Value.(string) + if !ok { + return 0, fmt.Errorf("value is not a string: %v", e.Value) } + + floatValue, err := strconv.ParseFloat(stringValue, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse float64: %v", err) + } + return floatValue, nil +} + +func (e *EvalResult) GetBool() (bool, error) { + if e.Type != Boolean { + return false, fmt.Errorf("type mismatch: %s", e.Type) + } + + strValue, ok := e.Value.(string) + if ok { + if strValue == "true" { + return true, nil + } else if strValue == "false" { + return false, nil + } + return false, fmt.Errorf("value is not a boolean: %v", e.Value) + } + + boolValue, ok := e.Value.(bool) + if !ok { + return false, fmt.Errorf("value is not a boolean: %v", e.Value) + } + + return boolValue, nil } func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { @@ -232,9 +266,17 @@ func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) { } switch e.Type { case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + right, err := other.GetBool() + if err != nil { + return nil, err + } return &EvalResult{ Type: Boolean, - Value: e.Value.(bool) && other.Value.(bool), + Value: left && right, }, nil default: return nil, fmt.Errorf("unsupported type: %s", e.Type) @@ -247,9 +289,17 @@ func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) { } switch e.Type { case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + right, err := other.GetBool() + if err != nil { + return nil, err + } return &EvalResult{ Type: Boolean, - Value: e.Value.(bool) || other.Value.(bool), + Value: left || right, }, nil default: return nil, fmt.Errorf("unsupported type: %s", e.Type) @@ -260,9 +310,13 @@ func (e *EvalResult) Not() (*EvalResult, error) { if e.Type != Boolean { return nil, fmt.Errorf("type mismatch: %s", e.Type) } + boolValue, err := e.GetBool() + if err != nil { + return nil, err + } return &EvalResult{ Type: Boolean, - Value: !e.Value.(bool), + Value: !boolValue, }, nil } @@ -272,9 +326,17 @@ func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) { } switch e.Type { case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + right, err := other.GetBool() + if err != nil { + return nil, err + } return &EvalResult{ Type: Boolean, - Value: e.Value.(bool) == other.Value.(bool), + Value: left == right, }, nil default: return nil, fmt.Errorf("unsupported type: %s", e.Type) @@ -282,17 +344,17 @@ func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) { } type Expr interface { - GetType() string + GetType() ExprType Eval(variables map[string]map[string]any) (*EvalResult, error) } type ConstExpr struct { - Type string `json:"type"` + Type ExprType `json:"type"` Value Value `json:"value"` ValueType ValueType `json:"valueType"` } -func (c ConstExpr) GetType() string { return c.Type } +func (c ConstExpr) GetType() ExprType { return c.Type } func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { return &EvalResult{ @@ -302,11 +364,11 @@ func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error } type VarExpr struct { - Type string `json:"type"` + Type ExprType `json:"type"` Selector WorkflowNodeIOValueSelector `json:"selector"` } -func (v VarExpr) GetType() string { return v.Type } +func (v VarExpr) GetType() ExprType { return v.Type } func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { if v.Selector.Id == "" { @@ -330,13 +392,13 @@ func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) } type CompareExpr struct { - Type string `json:"type"` // compare + Type ExprType `json:"type"` // compare Op ComparisonOperator `json:"op"` Left Expr `json:"left"` Right Expr `json:"right"` } -func (c CompareExpr) GetType() string { return c.Type } +func (c CompareExpr) GetType() ExprType { return c.Type } func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { left, err := c.Left.Eval(variables) @@ -369,13 +431,13 @@ func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, err } type LogicalExpr struct { - Type string `json:"type"` // logical + Type ExprType `json:"type"` // logical Op LogicalOperator `json:"op"` Left Expr `json:"left"` Right Expr `json:"right"` } -func (l LogicalExpr) GetType() string { return l.Type } +func (l LogicalExpr) GetType() ExprType { return l.Type } func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { left, err := l.Left.Eval(variables) @@ -398,11 +460,11 @@ func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, err } type NotExpr struct { - Type string `json:"type"` // not - Expr Expr `json:"expr"` + Type ExprType `json:"type"` // not + Expr Expr `json:"expr"` } -func (n NotExpr) GetType() string { return n.Type } +func (n NotExpr) GetType() ExprType { return n.Type } func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { inner, err := n.Expr.Eval(variables) @@ -413,7 +475,7 @@ func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) } type rawExpr struct { - Type string `json:"type"` + Type ExprType `json:"type"` } func MarshalExpr(e Expr) ([]byte, error) { @@ -427,31 +489,31 @@ func UnmarshalExpr(data []byte) (Expr, error) { } switch typ.Type { - case "const": + case ConstExprType: var e ConstExpr if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e, nil - case "var": + case VarExprType: var e VarExpr if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e, nil - case "compare": + case CompareExprType: var e CompareExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e.ToCompareExpr() - case "logical": + case LogicalExprType: var e LogicalExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e.ToLogicalExpr() - case "not": + case NotExprType: var e NotExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err @@ -463,7 +525,7 @@ func UnmarshalExpr(data []byte) (Expr, error) { } type CompareExprRaw struct { - Type string `json:"type"` + Type ExprType `json:"type"` Op ComparisonOperator `json:"op"` Left json.RawMessage `json:"left"` Right json.RawMessage `json:"right"` @@ -487,7 +549,7 @@ func (r CompareExprRaw) ToCompareExpr() (CompareExpr, error) { } type LogicalExprRaw struct { - Type string `json:"type"` + Type ExprType `json:"type"` Op LogicalOperator `json:"op"` Left json.RawMessage `json:"left"` Right json.RawMessage `json:"right"` @@ -511,7 +573,7 @@ func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) { } type NotExprRaw struct { - Type string `json:"type"` + Type ExprType `json:"type"` Expr json.RawMessage `json:"expr"` } diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 8f6522a5..afa379a8 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -88,8 +88,10 @@ type WorkflowNodeConfigForCondition struct { } type WorkflowNodeConfigForInspect struct { + Host string `json:"host"` // 主机 Domain string `json:"domain"` // 域名 Port string `json:"port"` // 端口 + Path string `json:"path"` // 路径 } type WorkflowNodeConfigForUpload struct { @@ -134,9 +136,14 @@ func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { } func (n *WorkflowNode) GetConfigForInspect() WorkflowNodeConfigForInspect { + host := maputil.GetString(n.Config, "host") + if host == "" { + return WorkflowNodeConfigForInspect{} + } + domain := maputil.GetString(n.Config, "domain") if domain == "" { - return WorkflowNodeConfigForInspect{} + domain = host } port := maputil.GetString(n.Config, "port") @@ -144,9 +151,13 @@ func (n *WorkflowNode) GetConfigForInspect() WorkflowNodeConfigForInspect { port = "443" } + path := maputil.GetString(n.Config, "path") + return WorkflowNodeConfigForInspect{ Domain: domain, Port: port, + Host: host, + Path: path, } } diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 7ace68ef..e5cc7274 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -100,8 +100,8 @@ func (n *applyNode) Process(ctx context.Context) error { } // 添加中间结果 - n.outputs["certificate.validated"] = true - n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24) + n.outputs[outputCertificateValidatedKey] = "true" + n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24)) n.logger.Info("apply completed") @@ -146,9 +146,9 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if expirationTime > renewalInterval { - - n.outputs["certificate.validated"] = true - n.outputs["certificate.daysLeft"] = int(expirationTime.Hours() / 24) + + n.outputs[outputCertificateValidatedKey] = "true" + n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(expirationTime.Hours()/24)) return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) } diff --git a/internal/workflow/node-processor/const.go b/internal/workflow/node-processor/const.go new file mode 100644 index 00000000..c1af01c9 --- /dev/null +++ b/internal/workflow/node-processor/const.go @@ -0,0 +1,6 @@ +package nodeprocessor + +const ( + outputCertificateValidatedKey = "certificate.validated" + outputCertificateDaysLeftKey = "certificate.daysLeft" +) diff --git a/internal/workflow/node-processor/inspect_node.go b/internal/workflow/node-processor/inspect_node.go index 6c6bea6a..a8661f37 100644 --- a/internal/workflow/node-processor/inspect_node.go +++ b/internal/workflow/node-processor/inspect_node.go @@ -3,9 +3,12 @@ package nodeprocessor import ( "context" "crypto/tls" + "crypto/x509" "fmt" "math" "net" + "net/http" + "strings" "time" "github.com/usual2970/certimate/internal/domain" @@ -26,13 +29,13 @@ func NewInspectNode(node *domain.WorkflowNode) *inspectNode { } func (n *inspectNode) Process(ctx context.Context) error { - n.logger.Info("enter inspect website certificate node ...") + n.logger.Info("entering inspect certificate node...") nodeConfig := n.node.GetConfigForInspect() err := n.inspect(ctx, nodeConfig) if err != nil { - n.logger.Warn("inspect website certificate failed: " + err.Error()) + n.logger.Warn("inspect certificate failed: " + err.Error()) return err } @@ -40,18 +43,35 @@ func (n *inspectNode) Process(ctx context.Context) error { } func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNodeConfigForInspect) error { - // 定义重试参数 maxRetries := 3 retryInterval := 2 * time.Second - var cert *tls.Certificate var lastError error + var certInfo *x509.Certificate - domainWithPort := nodeConfig.Domain + ":" + nodeConfig.Port + host := nodeConfig.Host + + port := nodeConfig.Port + if port == "" { + port = "443" + } + + domain := nodeConfig.Domain + if domain == "" { + domain = host + } + + path := nodeConfig.Path + if path != "" && !strings.HasPrefix(path, "/") { + path = "/" + path + } + + targetAddr := fmt.Sprintf("%s:%s", host, port) + n.logger.Info(fmt.Sprintf("Inspecting certificate at %s (validating domain: %s)", targetAddr, domain)) for attempt := 0; attempt < maxRetries; attempt++ { if attempt > 0 { - n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, domainWithPort)) + n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, targetAddr)) select { case <-ctx.Done(): return ctx.Err() @@ -60,30 +80,65 @@ func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNod } } - dialer := &net.Dialer{ - Timeout: 10 * time.Second, + transport := &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + }).DialContext, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: domain, // Set SNI to domain for proper certificate selection + }, + ForceAttemptHTTP2: false, + DisableKeepAlives: true, } - conn, err := tls.DialWithDialer(dialer, "tcp", domainWithPort, &tls.Config{ - InsecureSkipVerify: true, // Allow self-signed certificates - }) + client := &http.Client{ + Transport: transport, + Timeout: 15 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + scheme := "https" + urlStr := fmt.Sprintf("%s://%s", scheme, targetAddr) + if path != "" { + urlStr = urlStr + path + } + + req, err := http.NewRequestWithContext(ctx, "HEAD", urlStr, nil) if err != nil { - lastError = fmt.Errorf("failed to connect to %s: %w", domainWithPort, err) + lastError = fmt.Errorf("failed to create HTTP request: %w", err) + n.logger.Warn(fmt.Sprintf("Request creation attempt #%d failed: %s", attempt+1, lastError.Error())) + continue + } + + if domain != host { + req.Host = domain + } + + req.Header.Set("User-Agent", "CertificateValidator/1.0") + req.Header.Set("Accept", "*/*") + + resp, err := client.Do(req) + if err != nil { + lastError = fmt.Errorf("HTTP request failed: %w", err) n.logger.Warn(fmt.Sprintf("Connection attempt #%d failed: %s", attempt+1, lastError.Error())) continue } - // Get certificate information - certInfo := conn.ConnectionState().PeerCertificates[0] - conn.Close() - - // Certificate information retrieved successfully - cert = &tls.Certificate{ - Certificate: [][]byte{certInfo.Raw}, - Leaf: certInfo, + if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { + resp.Body.Close() + lastError = fmt.Errorf("no TLS certificates received in HTTP response") + n.logger.Warn(fmt.Sprintf("Certificate retrieval attempt #%d failed: %s", attempt+1, lastError.Error())) + continue } + + certInfo = resp.TLS.PeerCertificates[0] + resp.Body.Close() + lastError = nil - n.logger.Info(fmt.Sprintf("Successfully retrieved certificate information for %s", domainWithPort)) + n.logger.Info(fmt.Sprintf("Successfully retrieved certificate from %s", targetAddr)) break } @@ -91,69 +146,46 @@ func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNod return fmt.Errorf("failed to retrieve certificate after %d attempts: %w", maxRetries, lastError) } - certInfo := cert.Leaf - now := time.Now() - - isValid := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore) - - // Check domain matching - domainMatch := false - if len(certInfo.DNSNames) > 0 { - for _, dnsName := range certInfo.DNSNames { - if matchDomain(nodeConfig.Domain, dnsName) { - domainMatch = true - break - } + if certInfo == nil { + outputs := map[string]any{ + outputCertificateValidatedKey: "false", + outputCertificateDaysLeftKey: "0", } - } else if matchDomain(nodeConfig.Domain, certInfo.Subject.CommonName) { - domainMatch = true + n.setOutputs(outputs) + return nil } - isValid = isValid && domainMatch + now := time.Now() + + isValidTime := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore) + + domainMatch := true + if err := certInfo.VerifyHostname(domain); err != nil { + domainMatch = false + } + + isValid := isValidTime && domainMatch daysRemaining := math.Floor(certInfo.NotAfter.Sub(now).Hours() / 24) - // Set node outputs - outputs := map[string]any{ - "certificate.validated": isValid, - "certificate.daysLeft": daysRemaining, + isValidStr := "false" + if isValid { + isValidStr = "true" } + + outputs := map[string]any{ + outputCertificateValidatedKey: isValidStr, + outputCertificateDaysLeftKey: fmt.Sprintf("%d", int(daysRemaining)), + } + n.setOutputs(outputs) + n.logger.Info(fmt.Sprintf("Certificate inspection completed - Target: %s, Domain: %s, Valid: %s, Days Remaining: %d", + targetAddr, domain, isValidStr, int(daysRemaining))) + return nil } func (n *inspectNode) setOutputs(outputs map[string]any) { n.outputs = outputs } - -func matchDomain(requestDomain, certDomain string) bool { - if requestDomain == certDomain { - return true - } - - if len(certDomain) > 2 && certDomain[0] == '*' && certDomain[1] == '.' { - - wildcardSuffix := certDomain[1:] - requestDomainLen := len(requestDomain) - suffixLen := len(wildcardSuffix) - - if requestDomainLen > suffixLen && requestDomain[requestDomainLen-suffixLen:] == wildcardSuffix { - remainingPart := requestDomain[:requestDomainLen-suffixLen] - if len(remainingPart) > 0 && !contains(remainingPart, '.') { - return true - } - } - } - - return false -} - -func contains(s string, c byte) bool { - for i := 0; i < len(s); i++ { - if s[i] == c { - return true - } - } - return false -} diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 7fbb1515..ab86807e 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -69,8 +69,8 @@ func (n *uploadNode) Process(ctx context.Context) error { return err } - n.outputs["certificate.validated"] = true - n.outputs["certificate.daysLeft"] = int(time.Until(certificate.ExpireAt).Hours() / 24) + n.outputs[outputCertificateValidatedKey] = "true" + n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24)) n.logger.Info("upload completed") @@ -91,8 +91,8 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, n.node.Id) if lastCertificate != nil { - n.outputs["certificate.validated"] = true - n.outputs["certificate.daysLeft"] = int(time.Until(lastCertificate.ExpireAt).Hours() / 24) + n.outputs[outputCertificateValidatedKey] = "true" + n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(lastCertificate.ExpireAt).Hours()/24)) return true, "the certificate has already been uploaded" } } diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index 417db4af..bcd58c77 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -5,7 +5,7 @@ import { Button, Card, Popover } from "antd"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; -import { Expr, WorkflowNodeIoValueType, Value } from "@/domain/workflow"; +import { Expr, WorkflowNodeIoValueType, ExprType } from "@/domain/workflow"; import { produce } from "immer"; import { useWorkflowStore } from "@/stores/workflow"; import { useZustandShallowSelector } from "@/hooks"; @@ -32,7 +32,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP const selectors = condition.leftSelector.split("#"); const t = selectors[2] as WorkflowNodeIoValueType; const left: Expr = { - type: "var", + type: ExprType.Var, selector: { id: selectors[0], name: selectors[1], @@ -40,27 +40,10 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP }, }; - let value: Value = condition.rightValue; - switch (t) { - case "boolean": - if (value === "true") { - value = true; - } else if (value === "false") { - value = false; - } - break; - case "number": - value = parseInt(value as string); - break; - case "string": - value = value as string; - break; - } - - const right: Expr = { type: "const", value: value, valueType: t }; + const right: Expr = { type: ExprType.Const, value: condition.rightValue, valueType: t }; return { - type: "compare", + type: ExprType.Compare, op: condition.operator, left, right, @@ -77,7 +60,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP for (let i = 1; i < values.conditions.length; i++) { expr = { - type: "logical", + type: ExprType.Logical, op: values.logicalOperator, left: expr, right: createComparisonExpr(values.conditions[i]), diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx index 81022a28..9cbb56cc 100644 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -14,6 +14,7 @@ import { WorkflowNode, workflowNodeIOOptions, WorkflowNodeIoValueType, + ExprType, } from "@/domain/workflow"; import { FormInstance } from "antd"; import { useZustandShallowSelector } from "@/hooks"; @@ -58,7 +59,7 @@ const initFormModel = (): ConditionNodeConfigFormFieldValues => { rightValue: "", }, ], - logicalOperator: "and", + logicalOperator: LogicalOperator.And, }; }; @@ -67,10 +68,10 @@ const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { if (!expr) return initFormModel(); const conditions: ConditionItem[] = []; - let logicalOp: LogicalOperator = "and"; + let logicalOp: LogicalOperator = LogicalOperator.And; const extractComparisons = (expr: Expr): void => { - if (expr.type === "compare") { + if (expr.type === ExprType.Compare) { // 确保左侧是变量,右侧是常量 if (isVarExpr(expr.left) && isConstExpr(expr.right)) { conditions.push({ @@ -79,7 +80,7 @@ const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { rightValue: String(expr.right.value), }); } - } else if (expr.type === "logical") { + } else if (expr.type === ExprType.Logical) { logicalOp = expr.op; extractComparisons(expr.left); extractComparisons(expr.right); @@ -304,25 +305,18 @@ const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { const type = typeStr as WorkflowNodeIoValueType; const left: Expr = { - type: "var", + type: ExprType.Var, selector: { id, name, type }, }; - let rightValue: any = condition.rightValue; - if (type === "number") { - rightValue = Number(condition.rightValue); - } else if (type === "boolean") { - rightValue = condition.rightValue === "true"; - } - const right: Expr = { - type: "const", - value: rightValue, + type: ExprType.Const, + value: condition.rightValue, valueType: type, }; return { - type: "compare", + type: ExprType.Compare, op: condition.operator, left, right, @@ -339,7 +333,7 @@ const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { for (let i = 1; i < values.conditions.length; i++) { expr = { - type: "logical", + type: ExprType.Logical, op: values.logicalOperator, left: expr, right: createComparisonExpr(values.conditions[i]), diff --git a/ui/src/components/workflow/node/InspectNode.tsx b/ui/src/components/workflow/node/InspectNode.tsx index fa4324e2..0d038894 100644 --- a/ui/src/components/workflow/node/InspectNode.tsx +++ b/ui/src/components/workflow/node/InspectNode.tsx @@ -39,7 +39,7 @@ const InspectNode = ({ node, disabled }: InspectNodeProps) => { const config = (node.config as WorkflowNodeConfigForInspect) ?? {}; return ( - {config.domain ?? ""} + {config.host ?? ""} ); }, [node]); diff --git a/ui/src/components/workflow/node/InspectNodeConfigForm.tsx b/ui/src/components/workflow/node/InspectNodeConfigForm.tsx index ea9573e5..2d7d83b0 100644 --- a/ui/src/components/workflow/node/InspectNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/InspectNodeConfigForm.tsx @@ -7,7 +7,7 @@ import { z } from "zod"; import { type WorkflowNodeConfigForInspect } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; -import { validDomainName, validPortNumber } from "@/utils/validators"; +import { validDomainName, validIPv4Address, validPortNumber } from "@/utils/validators"; type InspectNodeConfigFormFieldValues = Partial; @@ -29,6 +29,8 @@ const initFormModel = (): InspectNodeConfigFormFieldValues => { return { domain: "", port: "443", + path: "", + host: "", }; }; @@ -37,12 +39,14 @@ const InspectNodeConfigForm = forwardRef validDomainName(val), { - message: t("workflow_node.inspect.form.domain.placeholder"), + host: z.string().refine((val) => validIPv4Address(val) || validDomainName(val), { + message: t("workflow_node.inspect.form.host.placeholder"), }), + domain: z.string().optional(), port: z.string().refine((val) => validPortNumber(val), { message: t("workflow_node.inspect.form.port.placeholder"), }), + path: z.string().optional(), }); const formRule = createSchemaFieldRule(formSchema); const { form: formInst, formProps } = useAntdForm({ @@ -70,13 +74,21 @@ const InspectNodeConfigForm = forwardRef - - + + + + + + + + + + ); } diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 6b951b49..5a3e9821 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -67,7 +67,7 @@ const workflowNodeTypeDefaultInputs: Map = n name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.certificate.label"), }, ], ], @@ -82,7 +82,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.certificate.label"), }, ], ], @@ -93,7 +93,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.certificate.label"), }, ], ], @@ -104,7 +104,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.certificate.label"), }, ], ], @@ -161,6 +161,8 @@ export type WorkflowNodeConfigForUpload = { export type WorkflowNodeConfigForInspect = { domain: string; port: string; + host: string; + path: string; }; export type WorkflowNodeConfigForDeploy = { @@ -200,14 +202,20 @@ export type WorkflowNodeIO = { valueSelector?: WorkflowNodeIOValueSelector; }; +export const VALUE_TYPES = Object.freeze({ + STRING: "string", + NUMBER: "number", + BOOLEAN: "boolean", +} as const); + +export type WorkflowNodeIoValueType = (typeof VALUE_TYPES)[keyof typeof VALUE_TYPES]; + export type WorkflowNodeIOValueSelector = { id: string; name: string; type: WorkflowNodeIoValueType; }; -export type WorkflowNodeIoValueType = "string" | "number" | "boolean"; - type WorkflowNodeIOOptions = { label: string; value: string; @@ -224,12 +232,12 @@ export const workflowNodeIOOptions = (node: WorkflowNode) => { switch (output.type) { case "certificate": rs.options.push({ - label: `${node.name} - ${output.label} - 是否有效`, + label: `${node.name} - ${output.label} - ${i18n.t("workflow.variables.is_validated.label")}`, value: `${node.id}#${output.name}.validated#boolean`, }); rs.options.push({ - label: `${node.name} - ${output.label} - 剩余天数`, + label: `${node.name} - ${output.label} - ${i18n.t("workflow.variables.days_left.label")}`, value: `${node.id}#${output.name}.daysLeft#number`, }); break; @@ -254,22 +262,34 @@ export type Value = string | number | boolean; export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!=" | "is"; -export type LogicalOperator = "and" | "or" | "not"; +export enum LogicalOperator { + And = "and", + Or = "or", + Not = "not", +} -export type ConstExpr = { type: "const"; value: Value; valueType: WorkflowNodeIoValueType }; -export type VarExpr = { type: "var"; selector: WorkflowNodeIOValueSelector }; -export type CompareExpr = { type: "compare"; op: ComparisonOperator; left: Expr; right: Expr }; -export type LogicalExpr = { type: "logical"; op: LogicalOperator; left: Expr; right: Expr }; -export type NotExpr = { type: "not"; expr: Expr }; +export enum ExprType { + Const = "const", + Var = "var", + Compare = "compare", + Logical = "logical", + Not = "not", +} + +export type ConstExpr = { type: ExprType.Const; value: string; valueType: WorkflowNodeIoValueType }; +export type VarExpr = { type: ExprType.Var; selector: WorkflowNodeIOValueSelector }; +export type CompareExpr = { type: ExprType.Compare; op: ComparisonOperator; left: Expr; right: Expr }; +export type LogicalExpr = { type: ExprType.Logical; op: LogicalOperator; left: Expr; right: Expr }; +export type NotExpr = { type: ExprType.Not; expr: Expr }; export type Expr = ConstExpr | VarExpr | CompareExpr | LogicalExpr | NotExpr; export const isConstExpr = (expr: Expr): expr is ConstExpr => { - return expr.type === "const"; + return expr.type === ExprType.Const; }; export const isVarExpr = (expr: Expr): expr is VarExpr => { - return expr.type === "var"; + return expr.type === ExprType.Var; }; // #endregion diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index b4f9d7e6..cdf722a0 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -53,5 +53,9 @@ "workflow.detail.orchestration.action.run": "Run", "workflow.detail.orchestration.action.run.confirm": "You have unreleased changes. Do you really want to run this workflow based on the latest released version?", "workflow.detail.orchestration.action.run.prompt": "Running... Please check the history later", - "workflow.detail.runs.tab": "History runs" + "workflow.detail.runs.tab": "History runs", + + "workflow.variables.is_validated.label": "Is valid", + "workflow.variables.days_left.label": "Days left", + "workflow.variables.certificate.label": "Certificate" } diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 2555c36e..b70e38de 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -806,6 +806,10 @@ "workflow_node.inspect.form.domain.placeholder": "Please enter domain name", "workflow_node.inspect.form.port.label": "Port", "workflow_node.inspect.form.port.placeholder": "Please enter port", + "workflow_node.inspect.form.host.label": "Host", + "workflow_node.inspect.form.host.placeholder": "Please enter host", + "workflow_node.inspect.form.path.label": "Path", + "workflow_node.inspect.form.path.placeholder": "Please enter path", "workflow_node.notify.label": "Notification", "workflow_node.notify.form.subject.label": "Subject", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 46cdc228..e86e796a 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -53,5 +53,9 @@ "workflow.detail.orchestration.action.run": "执行", "workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?", "workflow.detail.orchestration.action.run.prompt": "执行中……请稍后查看执行历史", - "workflow.detail.runs.tab": "执行历史" + "workflow.detail.runs.tab": "执行历史", + + "workflow.variables.is_validated.label": "是否有效", + "workflow.variables.days_left.label": "剩余天数", + "workflow.variables.certificate.label": "证书" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 206daeeb..722568fe 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -805,6 +805,10 @@ "workflow_node.inspect.form.domain.placeholder": "请输入要检查的网站域名", "workflow_node.inspect.form.port.label": "端口号", "workflow_node.inspect.form.port.placeholder": "请输入要检查的端口号", + "workflow_node.inspect.form.host.label": "Host", + "workflow_node.inspect.form.host.placeholder": "请输入 Host", + "workflow_node.inspect.form.path.label": "Path", + "workflow_node.inspect.form.path.placeholder": "请输入 Path", "workflow_node.notify.label": "推送通知", "workflow_node.notify.form.subject.label": "通知主题", From b8b94dfd772bb92b67c3af0cb3a6432119bb5e92 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 27 May 2025 17:28:06 +0800 Subject: [PATCH 12/28] feat: support configuring huaweicloud enterprise project id --- internal/deployer/providers.go | 42 ++++++++++--------- internal/domain/access.go | 5 ++- .../huaweicloud-cdn/huaweicloud_cdn.go | 11 +++-- .../huaweicloud-elb/huaweicloud_elb.go | 12 ++++-- .../huaweicloud-scm/huaweicloud_scm.go | 7 +++- .../huaweicloud-waf/huaweicloud_waf.go | 35 ++++++++++------ .../huaweicloud-elb/huaweicloud_elb.go | 11 +++-- .../huaweicloud-scm/huaweicloud_scm.go | 18 ++++---- .../huaweicloud-waf/huaweicloud_waf.go | 11 +++-- internal/pkg/utils/type/cast.go | 17 ++++++++ .../access/AccessFormHuaweiCloudConfig.tsx | 20 +++++++-- ui/src/domain/access.ts | 1 + ui/src/i18n/locales/en/nls.access.json | 3 ++ ui/src/i18n/locales/zh/nls.access.json | 3 ++ 14 files changed, 137 insertions(+), 59 deletions(-) diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 7f9bae91..1760ee24 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -676,40 +676,44 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer switch options.Provider { case domain.DeploymentProviderTypeHuaweiCloudCDN: deployer, err := pHuaweiCloudCDN.NewDeployer(&pHuaweiCloudCDN.DeployerConfig{ - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, - Region: maputil.GetString(options.ProviderServiceConfig, "region"), - Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + EnterpriseProjectId: access.EnterpriseProjectId, + Region: maputil.GetString(options.ProviderServiceConfig, "region"), + Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) return deployer, err case domain.DeploymentProviderTypeHuaweiCloudELB: deployer, err := pHuaweiCloudELB.NewDeployer(&pHuaweiCloudELB.DeployerConfig{ - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, - Region: maputil.GetString(options.ProviderServiceConfig, "region"), - ResourceType: pHuaweiCloudELB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), - CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"), - LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"), - ListenerId: maputil.GetString(options.ProviderServiceConfig, "listenerId"), + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + EnterpriseProjectId: access.EnterpriseProjectId, + Region: maputil.GetString(options.ProviderServiceConfig, "region"), + ResourceType: pHuaweiCloudELB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), + CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"), + LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"), + ListenerId: maputil.GetString(options.ProviderServiceConfig, "listenerId"), }) return deployer, err case domain.DeploymentProviderTypeHuaweiCloudSCM: deployer, err := pHuaweiCloudSCM.NewDeployer(&pHuaweiCloudSCM.DeployerConfig{ - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + EnterpriseProjectId: access.EnterpriseProjectId, }) return deployer, err case domain.DeploymentProviderTypeHuaweiCloudWAF: deployer, err := pHuaweiCloudWAF.NewDeployer(&pHuaweiCloudWAF.DeployerConfig{ - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, - Region: maputil.GetString(options.ProviderServiceConfig, "region"), - ResourceType: pHuaweiCloudWAF.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), - CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"), - Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + EnterpriseProjectId: access.EnterpriseProjectId, + Region: maputil.GetString(options.ProviderServiceConfig, "region"), + ResourceType: pHuaweiCloudWAF.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), + CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"), + Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) return deployer, err diff --git a/internal/domain/access.go b/internal/domain/access.go index e31bb1a0..d77de3fe 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -199,8 +199,9 @@ type AccessConfigForHetzner struct { } type AccessConfigForHuaweiCloud struct { - AccessKeyId string `json:"accessKeyId"` - SecretAccessKey string `json:"secretAccessKey"` + AccessKeyId string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` } type AccessConfigForJDCloud struct { diff --git a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go index d33dafff..3a8122ca 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go @@ -21,6 +21,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` // 加速域名(不支持泛域名)。 @@ -51,8 +53,9 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { } uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - SecretAccessKey: config.SecretAccessKey, + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + EnterpriseProjectId: config.EnterpriseProjectId, }) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) @@ -88,7 +91,8 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE // 查询加速域名配置 // REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html showDomainFullConfigReq := &hccdnmodel.ShowDomainFullConfigRequest{ - DomainName: d.config.Domain, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + DomainName: d.config.Domain, } showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq) d.logger.Debug("sdk request 'cdn.ShowDomainFullConfig'", slog.Any("request", showDomainFullConfigReq), slog.Any("response", showDomainFullConfigResp)) @@ -107,6 +111,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE updateDomainMultiCertificatesReqBodyContent.CertName = typeutil.ToPtr(upres.CertName) updateDomainMultiCertificatesReqBodyContent = assign(updateDomainMultiCertificatesReqBodyContent, showDomainFullConfigResp.Configs) updateDomainMultiCertificatesReq := &hccdnmodel.UpdateDomainMultiCertificatesRequest{ + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), Body: &hccdnmodel.UpdateDomainMultiCertificatesRequestBody{ Https: updateDomainMultiCertificatesReqBodyContent, }, diff --git a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go index 23ec4a92..92c62c9a 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go @@ -27,6 +27,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` // 部署资源类型。 @@ -62,9 +64,10 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { } uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - SecretAccessKey: config.SecretAccessKey, - Region: config.Region, + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + EnterpriseProjectId: config.EnterpriseProjectId, + Region: config.Region, }) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) @@ -172,6 +175,9 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, certPEM str Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"}, LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id}, } + if d.config.EnterpriseProjectId != "" { + listListenersReq.EnterpriseProjectId = typeutil.ToPtr([]string{d.config.EnterpriseProjectId}) + } listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) d.logger.Debug("sdk request 'elb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp)) if err != nil { diff --git a/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go b/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go index c8c208ad..c1afb5d8 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go @@ -15,6 +15,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` } type DeployerProvider struct { @@ -31,8 +33,9 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { } uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - SecretAccessKey: config.SecretAccessKey, + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + EnterpriseProjectId: config.EnterpriseProjectId, }) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) diff --git a/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go b/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go index 8fe96ee0..04c1c30e 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go @@ -27,6 +27,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` // 部署资源类型。 @@ -59,9 +61,10 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { } uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - SecretAccessKey: config.SecretAccessKey, - Region: config.Region, + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + EnterpriseProjectId: config.EnterpriseProjectId, + Region: config.Region, }) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) @@ -126,7 +129,8 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri // 查询证书 // REF: https://support.huaweicloud.com/api-waf/ShowCertificate.html showCertificateReq := &hcwafmodel.ShowCertificateRequest{ - CertificateId: d.config.CertificateId, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + CertificateId: d.config.CertificateId, } showCertificateResp, err := d.sdkClient.ShowCertificate(showCertificateReq) d.logger.Debug("sdk request 'waf.ShowCertificate'", slog.Any("request", showCertificateReq), slog.Any("response", showCertificateResp)) @@ -137,7 +141,8 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri // 更新证书 // REF: https://support.huaweicloud.com/api-waf/UpdateCertificate.html updateCertificateReq := &hcwafmodel.UpdateCertificateRequest{ - CertificateId: d.config.CertificateId, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + CertificateId: d.config.CertificateId, Body: &hcwafmodel.UpdateCertificateRequestBody{ Name: *showCertificateResp.Name, Content: typeutil.ToPtr(certPEM), @@ -179,9 +184,10 @@ func (d *DeployerProvider) deployToCloudServer(ctx context.Context, certPEM stri } listHostReq := &hcwafmodel.ListHostRequest{ - Hostname: typeutil.ToPtr(strings.TrimPrefix(d.config.Domain, "*")), - Page: typeutil.ToPtr(listHostPage), - Pagesize: typeutil.ToPtr(listHostPageSize), + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + Hostname: typeutil.ToPtr(strings.TrimPrefix(d.config.Domain, "*")), + Page: typeutil.ToPtr(listHostPage), + Pagesize: typeutil.ToPtr(listHostPageSize), } listHostResp, err := d.sdkClient.ListHost(listHostReq) d.logger.Debug("sdk request 'waf.ListHost'", slog.Any("request", listHostReq), slog.Any("response", listHostResp)) @@ -211,7 +217,8 @@ func (d *DeployerProvider) deployToCloudServer(ctx context.Context, certPEM stri // 更新云模式防护域名的配置 // REF: https://support.huaweicloud.com/api-waf/UpdateHost.html updateHostReq := &hcwafmodel.UpdateHostRequest{ - InstanceId: hostId, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + InstanceId: hostId, Body: &hcwafmodel.UpdateHostRequestBody{ Certificateid: typeutil.ToPtr(upres.CertId), Certificatename: typeutil.ToPtr(upres.CertName), @@ -252,9 +259,10 @@ func (d *DeployerProvider) deployToPremiumHost(ctx context.Context, certPEM stri } listPremiumHostReq := &hcwafmodel.ListPremiumHostRequest{ - Hostname: typeutil.ToPtr(strings.TrimPrefix(d.config.Domain, "*")), - Page: typeutil.ToPtr(fmt.Sprintf("%d", listPremiumHostPage)), - Pagesize: typeutil.ToPtr(fmt.Sprintf("%d", listPremiumHostPageSize)), + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + Hostname: typeutil.ToPtr(strings.TrimPrefix(d.config.Domain, "*")), + Page: typeutil.ToPtr(fmt.Sprintf("%d", listPremiumHostPage)), + Pagesize: typeutil.ToPtr(fmt.Sprintf("%d", listPremiumHostPageSize)), } listPremiumHostResp, err := d.sdkClient.ListPremiumHost(listPremiumHostReq) d.logger.Debug("sdk request 'waf.ListPremiumHost'", slog.Any("request", listPremiumHostReq), slog.Any("response", listPremiumHostResp)) @@ -284,7 +292,8 @@ func (d *DeployerProvider) deployToPremiumHost(ctx context.Context, certPEM stri // 修改独享模式域名配置 // REF: https://support.huaweicloud.com/api-waf/UpdatePremiumHost.html updatePremiumHostReq := &hcwafmodel.UpdatePremiumHostRequest{ - HostId: hostId, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + HostId: hostId, Body: &hcwafmodel.UpdatePremiumHostRequestBody{ Certificateid: typeutil.ToPtr(upres.CertId), Certificatename: typeutil.ToPtr(upres.CertName), diff --git a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go index 9369144e..b205e97e 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go @@ -26,6 +26,8 @@ type UploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` } @@ -141,10 +143,11 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE createCertificateReq := &hcelbmodel.CreateCertificateRequest{ Body: &hcelbmodel.CreateCertificateRequestBody{ Certificate: &hcelbmodel.CreateCertificateOption{ - ProjectId: typeutil.ToPtr(projectId), - Name: typeutil.ToPtr(certName), - Certificate: typeutil.ToPtr(certPEM), - PrivateKey: typeutil.ToPtr(privkeyPEM), + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), + ProjectId: typeutil.ToPtr(projectId), + Name: typeutil.ToPtr(certName), + Certificate: typeutil.ToPtr(certPEM), + PrivateKey: typeutil.ToPtr(privkeyPEM), }, }, } diff --git a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go index f8435733..9f47442e 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go @@ -21,6 +21,8 @@ type UploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` } @@ -79,10 +81,11 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } listCertificatesReq := &hcscmmodel.ListCertificatesRequest{ - Limit: typeutil.ToPtr(listCertificatesLimit), - Offset: typeutil.ToPtr(listCertificatesOffset), - SortDir: typeutil.ToPtr("DESC"), - SortKey: typeutil.ToPtr("certExpiredTime"), + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), + Limit: typeutil.ToPtr(listCertificatesLimit), + Offset: typeutil.ToPtr(listCertificatesOffset), + SortDir: typeutil.ToPtr("DESC"), + SortKey: typeutil.ToPtr("certExpiredTime"), } listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq) u.logger.Debug("sdk request 'scm.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) @@ -142,9 +145,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // REF: https://support.huaweicloud.com/api-ccm/ImportCertificate.html importCertificateReq := &hcscmmodel.ImportCertificateRequest{ Body: &hcscmmodel.ImportCertificateRequestBody{ - Name: certName, - Certificate: certPEM, - PrivateKey: privkeyPEM, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), + Name: certName, + Certificate: certPEM, + PrivateKey: privkeyPEM, }, } importCertificateResp, err := u.sdkClient.ImportCertificate(importCertificateReq) diff --git a/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go b/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go index d0c61775..a1cbe1df 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go @@ -26,6 +26,8 @@ type UploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` } @@ -84,8 +86,9 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } listCertificatesReq := &hcwafmodel.ListCertificatesRequest{ - Page: typeutil.ToPtr(listCertificatesPage), - Pagesize: typeutil.ToPtr(listCertificatesPageSize), + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), + Page: typeutil.ToPtr(listCertificatesPage), + Pagesize: typeutil.ToPtr(listCertificatesPageSize), } listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq) u.logger.Debug("sdk request 'waf.ShowCertificate'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) @@ -96,7 +99,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE if listCertificatesResp.Items != nil { for _, certItem := range *listCertificatesResp.Items { showCertificateReq := &hcwafmodel.ShowCertificateRequest{ - CertificateId: certItem.Id, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), + CertificateId: certItem.Id, } showCertificateResp, err := u.sdkClient.ShowCertificate(showCertificateReq) u.logger.Debug("sdk request 'waf.ShowCertificate'", slog.Any("request", showCertificateReq), slog.Any("response", showCertificateResp)) @@ -141,6 +145,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // 创建证书 // REF: https://support.huaweicloud.com/api-waf/CreateCertificate.html createCertificateReq := &hcwafmodel.CreateCertificateRequest{ + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), Body: &hcwafmodel.CreateCertificateRequestBody{ Name: certName, Content: certPEM, diff --git a/internal/pkg/utils/type/cast.go b/internal/pkg/utils/type/cast.go index 684e262e..1acd4765 100644 --- a/internal/pkg/utils/type/cast.go +++ b/internal/pkg/utils/type/cast.go @@ -1,5 +1,7 @@ package typeutil +import "reflect" + // 将对象转换为指针。 // // 入参: @@ -11,6 +13,21 @@ func ToPtr[T any](v T) (p *T) { return &v } +// 将非零值的对象转换为指针。 +// 与 [ToPtr] 不同的是,如果对象的值为零值,则返回 nil。 +// +// 入参: +// - 待转换的对象。 +// +// 出参: +// - 返回对象的指针。 +func ToPtrOrZeroNil[T any](v T) (p *T) { + if !reflect.ValueOf(v).IsZero() { + return &v + } + return nil +} + // 将指针转换为对象。 // // 入参: diff --git a/ui/src/components/access/AccessFormHuaweiCloudConfig.tsx b/ui/src/components/access/AccessFormHuaweiCloudConfig.tsx index f1d56ff0..c460f473 100644 --- a/ui/src/components/access/AccessFormHuaweiCloudConfig.tsx +++ b/ui/src/components/access/AccessFormHuaweiCloudConfig.tsx @@ -28,14 +28,19 @@ const AccessFormHuaweiCloudConfig = ({ form: formInst, formName, disabled, initi const formSchema = z.object({ accessKeyId: z .string() + .trim() .min(1, t("access.form.huaweicloud_access_key_id.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })) - .trim(), + .max(64, t("common.errmsg.string_max", { max: 64 })), secretAccessKey: z .string() + .trim() .min(1, t("access.form.huaweicloud_secret_access_key.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })), + enterpriseProjectId: z + .string() + .trim() .max(64, t("common.errmsg.string_max", { max: 64 })) - .trim(), + .nullish(), }); const formRule = createSchemaFieldRule(formSchema); @@ -69,6 +74,15 @@ const AccessFormHuaweiCloudConfig = ({ form: formInst, formName, disabled, initi > + + } + > + + ); }; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 69979aac..51398e7f 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -264,6 +264,7 @@ export type AccessConfigForHetzner = { export type AccessConfigForHuaweiCloud = { accessKeyId: string; secretAccessKey: string; + enterpriseProjectId?: string; }; export type AccessConfigForJDCloud = { diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index bf453f1d..13c9c5eb 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -252,6 +252,9 @@ "access.form.huaweicloud_secret_access_key.label": "Huawei Cloud SecretAccessKey", "access.form.huaweicloud_secret_access_key.placeholder": "Please enter Huawei Cloud SecretAccessKey", "access.form.huaweicloud_secret_access_key.tooltip": "For more information, see https://support.huaweicloud.com/intl/en-us/usermanual-ca/ca_01_0003.html", + "access.form.huaweicloud_enterprise_project_id.label": "Huawei Cloud enterprise project ID (Optional)", + "access.form.huaweicloud_enterprise_project_id.placeholder": "Please enter Huawei Cloud enterprise project ID", + "access.form.huaweicloud_enterprise_project_id.tooltip": "For more information, see https://support.huaweicloud.com/intl/en-us/usermanual-em/em_03_0000.html", "access.form.jdcloud_access_key_id.label": "JD Cloud AccessKeyId", "access.form.jdcloud_access_key_id.placeholder": "Please enter JD Cloud AccessKeyId", "access.form.jdcloud_access_key_id.tooltip": "For more information, see https://docs.jdcloud.com/en/account-management/accesskey-management", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index fb51668f..43305dda 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -252,6 +252,9 @@ "access.form.huaweicloud_secret_access_key.label": "华为云 SecretAccessKey", "access.form.huaweicloud_secret_access_key.placeholder": "请输入华为云 SecretAccessKey", "access.form.huaweicloud_secret_access_key.tooltip": "这是什么?请参阅 https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html", + "access.form.huaweicloud_enterprise_project_id.label": "华为云企业项目 ID(可选)", + "access.form.huaweicloud_enterprise_project_id.placeholder": "请输入华为云企业项目 ID", + "access.form.huaweicloud_enterprise_project_id.tooltip": "这是什么?请参阅 https://support.huaweicloud.com/usermanual-em/zh-cn_topic_0126101490.html", "access.form.jdcloud_access_key_id.label": "京东云 AccessKeyId", "access.form.jdcloud_access_key_id.placeholder": "请输入京东云 AccessKeyId", "access.form.jdcloud_access_key_id.tooltip": "这是什么?请参阅 https://docs.jdcloud.com/cn/account-management/accesskey-management", From df1f216b5bb8308f571782d1acba65a2c71617da Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 27 May 2025 21:04:25 +0800 Subject: [PATCH 13/28] feat: support configuring aliyun resource group id --- internal/deployer/providers.go | 14 ++++++ internal/domain/access.go | 1 + .../providers/aliyun-alb/aliyun_alb.go | 9 ++-- .../providers/aliyun-apigw/aliyun_apigw.go | 21 +++++---- .../aliyun-cas-deploy/aliyun_cas_deploy.go | 43 +++++++++++++------ .../providers/aliyun-cas/aliyun_cas.go | 3 ++ .../providers/aliyun-cdn/aliyun_cdn.go | 2 + .../providers/aliyun-clb/aliyun_clb.go | 9 ++-- .../providers/aliyun-dcdn/aliyun_dcdn.go | 2 + .../providers/aliyun-ddos/aliyun_ddos.go | 9 ++-- .../providers/aliyun-esa/aliyun_esa.go | 9 ++-- .../deployer/providers/aliyun-fc/aliyun_fc.go | 7 ++- .../deployer/providers/aliyun-ga/aliyun_ga.go | 7 ++- .../providers/aliyun-live/aliyun_live.go | 4 +- .../providers/aliyun-nlb/aliyun_nlb.go | 14 +++--- .../providers/aliyun-oss/aliyun_oss.go | 2 + .../providers/aliyun-vod/aliyun_vod.go | 6 ++- .../providers/aliyun-waf/aliyun_waf.go | 27 +++++++----- .../providers/aliyun-cas/aliyun_cas.go | 23 +++++----- .../providers/aliyun-slb/aliyun_slb.go | 9 +++- internal/pkg/utils/type/cast.go | 7 +-- .../access/AccessFormAliyunConfig.tsx | 27 ++++++++++-- .../access/AccessFormHuaweiCloudConfig.tsx | 6 +-- ui/src/domain/access.ts | 1 + ui/src/i18n/locales/en/nls.access.json | 3 ++ ui/src/i18n/locales/zh/nls.access.json | 3 ++ 26 files changed, 186 insertions(+), 82 deletions(-) diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 1760ee24..e67c29e0 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -157,6 +157,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunALB.NewDeployer(&pAliyunALB.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ResourceType: pAliyunALB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"), @@ -169,6 +170,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunAPIGW.NewDeployer(&pAliyunAPIGW.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ServiceType: pAliyunAPIGW.ServiceType(maputil.GetString(options.ProviderServiceConfig, "serviceType")), GatewayId: maputil.GetString(options.ProviderServiceConfig, "gatewayId"), @@ -181,6 +183,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunCAS.NewDeployer(&pAliyunCAS.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), }) return deployer, err @@ -189,6 +192,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunCASDeploy.NewDeployer(&pAliyunCASDeploy.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ResourceIds: sliceutil.Filter(strings.Split(maputil.GetString(options.ProviderServiceConfig, "resourceIds"), ";"), func(s string) bool { return s != "" }), ContactIds: sliceutil.Filter(strings.Split(maputil.GetString(options.ProviderServiceConfig, "contactIds"), ";"), func(s string) bool { return s != "" }), @@ -199,6 +203,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunCDN.NewDeployer(&pAliyunCDN.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) return deployer, err @@ -207,6 +212,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunCLB.NewDeployer(&pAliyunCLB.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ResourceType: pAliyunCLB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"), @@ -219,6 +225,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunDCDN.NewDeployer(&pAliyunDCDN.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) return deployer, err @@ -227,6 +234,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunDDoS.NewDeployer(&pAliyunDDoS.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) @@ -245,6 +253,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunFC.NewDeployer(&pAliyunFC.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ServiceVersion: maputil.GetOrDefaultString(options.ProviderServiceConfig, "serviceVersion", "3.0"), Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), @@ -255,6 +264,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunGA.NewDeployer(&pAliyunGA.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, ResourceType: pAliyunGA.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), AcceleratorId: maputil.GetString(options.ProviderServiceConfig, "acceleratorId"), ListenerId: maputil.GetString(options.ProviderServiceConfig, "listenerId"), @@ -275,6 +285,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunNLB.NewDeployer(&pAliyunNLB.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ResourceType: pAliyunNLB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"), @@ -286,6 +297,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunOSS.NewDeployer(&pAliyunOSS.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), Bucket: maputil.GetString(options.ProviderServiceConfig, "bucket"), Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), @@ -296,6 +308,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunVOD.NewDeployer(&pAliyunVOD.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) @@ -305,6 +318,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunWAF.NewDeployer(&pAliyunWAF.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ServiceVersion: maputil.GetOrDefaultString(options.ProviderServiceConfig, "serviceVersion", "3.0"), InstanceId: maputil.GetString(options.ProviderServiceConfig, "instanceId"), diff --git a/internal/domain/access.go b/internal/domain/access.go index d77de3fe..5c96420e 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -38,6 +38,7 @@ type AccessConfigForACMEHttpReq struct { type AccessConfigForAliyun struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` + ResourceGroupId string `json:"resourceGroupId,omitempty"` } type AccessConfigForAWS struct { diff --git a/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go b/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go index 35b4997c..fec66c0e 100644 --- a/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go +++ b/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go @@ -25,6 +25,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 部署资源类型。 @@ -64,7 +66,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk clients: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -423,7 +425,7 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients // 接入点一览 https://api.aliyun.com/product/Alb var albEndpoint string switch region { - case "cn-hangzhou-finance": + case "", "cn-hangzhou-finance": albEndpoint = "alb.cn-hangzhou.aliyuncs.com" default: albEndpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region) @@ -463,7 +465,7 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients }, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 ALB 服务的 @@ -479,6 +481,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go b/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go index d74c7c27..f215e701 100644 --- a/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go +++ b/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go @@ -16,6 +16,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/uploader" uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" ) type DeployerConfig struct { @@ -23,6 +24,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 服务类型。 @@ -61,7 +64,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk clients: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -149,10 +152,11 @@ func (d *DeployerProvider) deployToCloudNative(ctx context.Context, certPEM stri } listDomainsReq := &aliapig.ListDomainsRequest{ - GatewayId: tea.String(d.config.GatewayId), - NameLike: tea.String(d.config.Domain), - PageNumber: tea.Int32(listDomainsPageNumber), - PageSize: tea.Int32(listDomainsPageSize), + ResourceGroupId: typeutil.ToPtrOrZeroNil(d.config.ResourceGroupId), + GatewayId: tea.String(d.config.GatewayId), + NameLike: tea.String(d.config.Domain), + PageNumber: tea.Int32(listDomainsPageNumber), + PageSize: tea.Int32(listDomainsPageSize), } listDomainsResp, err := d.sdkClients.CloudNativeAPIGateway.ListDomains(listDomainsReq) d.logger.Debug("sdk request 'apig.ListDomains'", slog.Any("request", listDomainsReq), slog.Any("response", listDomainsResp)) @@ -223,7 +227,7 @@ func (d *DeployerProvider) deployToCloudNative(ctx context.Context, certPEM stri func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients, error) { // 接入点一览 https://api.aliyun.com/product/APIG - cloudNativeAPIGEndpoint := fmt.Sprintf("apig.%s.aliyuncs.com", region) + cloudNativeAPIGEndpoint := strings.ReplaceAll(fmt.Sprintf("apig.%s.aliyuncs.com", region), "..", ".") cloudNativeAPIGConfig := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), @@ -235,7 +239,7 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients } // 接入点一览 https://api.aliyun.com/product/CloudAPI - traditionalAPIGEndpoint := fmt.Sprintf("apigateway.%s.aliyuncs.com", region) + traditionalAPIGEndpoint := strings.ReplaceAll(fmt.Sprintf("apigateway.%s.aliyuncs.com", region), "..", ".") traditionalAPIGConfig := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), @@ -252,7 +256,7 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients }, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 APIGateway 服务的 @@ -268,6 +272,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go b/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go index 077dea5c..569edaf3 100644 --- a/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go +++ b/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go @@ -22,6 +22,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 阿里云云产品资源 ID 数组。 @@ -50,11 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - AccessKeySecret: config.AccessKeySecret, - Region: config.Region, - }) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -94,9 +92,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE if len(contactIds) == 0 { // 获取联系人列表 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listcontact - listContactReq := &alicas.ListContactRequest{} - listContactReq.ShowSize = tea.Int32(1) - listContactReq.CurrentPage = tea.Int32(1) + listContactReq := &alicas.ListContactRequest{ + ShowSize: tea.Int32(1), + CurrentPage: tea.Int32(1), + } listContactResp, err := d.sdkClient.ListContact(listContactReq) d.logger.Debug("sdk request 'cas.ListContact'", slog.Any("request", listContactReq), slog.Any("response", listContactResp)) if err != nil { @@ -157,14 +156,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE } func createSdkClient(accessKeyId, accessKeySecret, region string) (*alicas.Client, error) { - if region == "" { - region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州 - } - // 接入点一览 https://api.aliyun.com/product/cas var endpoint string switch region { - case "cn-hangzhou": + case "", "cn-hangzhou": endpoint = "cas.aliyuncs.com" default: endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region) @@ -183,3 +178,25 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alicas.Clien return client, nil } + +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { + casRegion := region + if casRegion != "" { + // 阿里云 CAS 服务接入点是独立于其他服务的 + // 国内版固定接入点:华东一杭州 + // 国际版固定接入点:亚太东南一新加坡 + if !strings.HasPrefix(casRegion, "cn-") { + casRegion = "ap-southeast-1" + } else { + casRegion = "cn-hangzhou" + } + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: accessKeyId, + AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, + Region: casRegion, + }) + return uploader, err +} diff --git a/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go b/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go index 56681e57..73d2d77b 100644 --- a/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go +++ b/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go @@ -15,6 +15,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` } @@ -35,6 +37,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, + ResourceGroupId: config.ResourceGroupId, Region: config.Region, }) if err != nil { diff --git a/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go b/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go index ce5f9fd8..5fa6eedf 100644 --- a/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go +++ b/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go @@ -19,6 +19,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } diff --git a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go index 34c3a49e..ec35a190 100644 --- a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go +++ b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go @@ -20,6 +20,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 部署资源类型。 @@ -54,7 +56,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -283,7 +285,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien // 接入点一览 https://api.aliyun.com/product/Slb var endpoint string switch region { - case + case "", "cn-hangzhou", "cn-hangzhou-finance", "cn-shanghai-finance-1", @@ -307,10 +309,11 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien return client, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: region, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go b/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go index 4eb411fd..f27f4ab9 100644 --- a/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go +++ b/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go @@ -19,6 +19,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } diff --git a/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go index d1cb5b61..83d5d602 100644 --- a/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go +++ b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go @@ -22,6 +22,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 网站域名(支持泛域名)。 @@ -47,7 +49,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -104,7 +106,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliddos.Clie config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), - Endpoint: tea.String(fmt.Sprintf("ddoscoo.%s.aliyuncs.com", region)), + Endpoint: tea.String(strings.ReplaceAll(fmt.Sprintf("ddoscoo.%s.aliyuncs.com", region), "..", ".")), } client, err := aliddos.NewClient(config) @@ -115,7 +117,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliddos.Clie return client, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 Anti-DDoS 服务的 @@ -131,6 +133,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go b/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go index 1f29756f..74d8344b 100644 --- a/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go +++ b/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go @@ -22,6 +22,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 阿里云 ESA 站点 ID。 @@ -47,7 +49,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -105,7 +107,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliesa.Clien config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), - Endpoint: tea.String(fmt.Sprintf("esa.%s.aliyuncs.com", region)), + Endpoint: tea.String(strings.ReplaceAll(fmt.Sprintf("esa.%s.aliyuncs.com", region), "..", ".")), } client, err := aliesa.NewClient(config) @@ -116,7 +118,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliesa.Clien return client, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 ESA 服务的 @@ -132,6 +134,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go index 426aa3a6..c1e8c5a0 100644 --- a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go +++ b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "strings" "time" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" @@ -19,6 +20,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 服务版本。 @@ -150,6 +153,8 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients // 接入点一览 https://api.aliyun.com/product/FC-Open var fc2Endpoint string switch region { + case "": + fc2Endpoint = "fc.aliyuncs.com" case "cn-hangzhou-finance": fc2Endpoint = fmt.Sprintf("%s.fc.aliyuncs.com", region) default: @@ -167,7 +172,7 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients } // 接入点一览 https://api.aliyun.com/product/FC-Open - fc3Endpoint := fmt.Sprintf("fcv3.%s.aliyuncs.com", region) + fc3Endpoint := strings.ReplaceAll(fmt.Sprintf("fcv3.%s.aliyuncs.com", region), "..", ".") fc3Config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), diff --git a/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go b/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go index f69660a8..c7385863 100644 --- a/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go +++ b/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go @@ -22,6 +22,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 部署资源类型。 ResourceType ResourceType `json:"resourceType"` // 全球加速实例 ID。 @@ -53,7 +55,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -312,10 +314,11 @@ func createSdkClient(accessKeyId, accessKeySecret string) (*aliga.Client, error) return client, nil } -func createSslUploader(accessKeyId, accessKeySecret string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId string) (uploader.Uploader, error) { uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: "cn-hangzhou", }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go b/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go index 354c9601..0481f3bf 100644 --- a/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go +++ b/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go @@ -19,6 +19,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 直播流域名(支持泛域名)。 @@ -86,7 +88,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alilive.Clie // 接入点一览 https://api.aliyun.com/product/live var endpoint string switch region { - case + case "", "cn-qingdao", "cn-beijing", "cn-shanghai", diff --git a/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go b/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go index 58015f3d..e4e80db9 100644 --- a/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go +++ b/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go @@ -21,6 +21,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 部署资源类型。 @@ -52,7 +54,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -224,12 +226,7 @@ func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudL func createSdkClient(accessKeyId, accessKeySecret, region string) (*alinlb.Client, error) { // 接入点一览 https://api.aliyun.com/product/Nlb - var endpoint string - switch region { - default: - endpoint = fmt.Sprintf("nlb.%s.aliyuncs.com", region) - } - + endpoint := strings.ReplaceAll(fmt.Sprintf("nlb.%s.aliyuncs.com", region), "..", ".") config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), @@ -244,7 +241,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alinlb.Clien return client, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 NLB 服务的 @@ -260,6 +257,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go b/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go index 474fe5b3..d810c0f9 100644 --- a/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go +++ b/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go @@ -16,6 +16,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 存储桶名。 diff --git a/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go b/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go index 48e52c26..b340e0a3 100644 --- a/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go +++ b/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "strings" "time" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" @@ -18,6 +19,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 点播加速域名(不支持泛域名)。 @@ -80,8 +83,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE func createSdkClient(accessKeyId, accessKeySecret, region string) (*alivod.Client, error) { // 接入点一览 https://api.aliyun.com/product/vod - endpoint := fmt.Sprintf("vod.%s.aliyuncs.com", region) - + endpoint := strings.ReplaceAll(fmt.Sprintf("vod.%s.aliyuncs.com", region), "..", ".") config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), diff --git a/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go b/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go index 26dbd008..c8ec310a 100644 --- a/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go +++ b/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go @@ -15,6 +15,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/uploader" uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas" sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" ) type DeployerConfig struct { @@ -22,6 +23,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 服务版本。 @@ -51,7 +54,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -107,8 +110,9 @@ func (d *DeployerProvider) deployToWAF3(ctx context.Context, certPEM string, pri // 查询默认 SSL/TLS 设置 // REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describedefaulthttps describeDefaultHttpsReq := &aliwaf.DescribeDefaultHttpsRequest{ - InstanceId: tea.String(d.config.InstanceId), - RegionId: tea.String(d.config.Region), + ResourceManagerResourceGroupId: typeutil.ToPtrOrZeroNil(d.config.ResourceGroupId), + InstanceId: tea.String(d.config.InstanceId), + RegionId: tea.String(d.config.Region), } describeDefaultHttpsResp, err := d.sdkClient.DescribeDefaultHttps(describeDefaultHttpsReq) d.logger.Debug("sdk request 'waf.DescribeDefaultHttps'", slog.Any("request", describeDefaultHttpsReq), slog.Any("response", describeDefaultHttpsResp)) @@ -119,11 +123,12 @@ func (d *DeployerProvider) deployToWAF3(ctx context.Context, certPEM string, pri // 修改默认 SSL/TLS 设置 // REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifydefaulthttps modifyDefaultHttpsReq := &aliwaf.ModifyDefaultHttpsRequest{ - InstanceId: tea.String(d.config.InstanceId), - RegionId: tea.String(d.config.Region), - CertId: tea.String(upres.CertId), - TLSVersion: tea.String("tlsv1"), - EnableTLSv3: tea.Bool(false), + ResourceManagerResourceGroupId: typeutil.ToPtrOrZeroNil(d.config.ResourceGroupId), + InstanceId: tea.String(d.config.InstanceId), + RegionId: tea.String(d.config.Region), + CertId: tea.String(upres.CertId), + TLSVersion: tea.String("tlsv1"), + EnableTLSv3: tea.Bool(false), } if describeDefaultHttpsResp.Body != nil && describeDefaultHttpsResp.Body.DefaultHttps != nil { modifyDefaultHttpsReq.TLSVersion = describeDefaultHttpsResp.Body.DefaultHttps.TLSVersion @@ -172,10 +177,11 @@ func (d *DeployerProvider) deployToWAF3(ctx context.Context, certPEM string, pri func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliwaf.Client, error) { // 接入点一览:https://api.aliyun.com/product/waf-openapi + endpoint := strings.ReplaceAll(fmt.Sprintf("wafopenapi.%s.aliyuncs.com", region), "..", ".") config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), - Endpoint: tea.String(fmt.Sprintf("wafopenapi.%s.aliyuncs.com", region)), + Endpoint: tea.String(endpoint), } client, err := aliwaf.NewClient(config) @@ -186,7 +192,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliwaf.Clien return client, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 WAF 服务的 @@ -202,6 +208,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go b/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go index 9d7be223..f5f490a3 100644 --- a/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go +++ b/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go @@ -13,6 +13,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/uploader" certutil "github.com/usual2970/certimate/internal/pkg/utils/cert" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" ) type UploaderConfig struct { @@ -20,6 +21,8 @@ type UploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` } @@ -78,9 +81,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } listUserCertificateOrderReq := &alicas.ListUserCertificateOrderRequest{ - CurrentPage: tea.Int64(listUserCertificateOrderPage), - ShowSize: tea.Int64(listUserCertificateOrderLimit), - OrderType: tea.String("CERT"), + ResourceGroupId: typeutil.ToPtrOrZeroNil(u.config.ResourceGroupId), + CurrentPage: tea.Int64(listUserCertificateOrderPage), + ShowSize: tea.Int64(listUserCertificateOrderLimit), + OrderType: tea.String("CERT"), } listUserCertificateOrderResp, err := u.sdkClient.ListUserCertificateOrder(listUserCertificateOrderReq) u.logger.Debug("sdk request 'cas.ListUserCertificateOrder'", slog.Any("request", listUserCertificateOrderReq), slog.Any("response", listUserCertificateOrderResp)) @@ -143,9 +147,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // 上传新证书 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate uploadUserCertificateReq := &alicas.UploadUserCertificateRequest{ - Name: tea.String(certName), - Cert: tea.String(certPEM), - Key: tea.String(privkeyPEM), + ResourceGroupId: typeutil.ToPtrOrZeroNil(u.config.ResourceGroupId), + Name: tea.String(certName), + Cert: tea.String(certPEM), + Key: tea.String(privkeyPEM), } uploadUserCertificateResp, err := u.sdkClient.UploadUserCertificate(uploadUserCertificateReq) u.logger.Debug("sdk request 'cas.UploadUserCertificate'", slog.Any("request", uploadUserCertificateReq), slog.Any("response", uploadUserCertificateResp)) @@ -176,14 +181,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } func createSdkClient(accessKeyId, accessKeySecret, region string) (*alicas.Client, error) { - if region == "" { - region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州 - } - // 接入点一览 https://api.aliyun.com/product/cas var endpoint string switch region { - case "cn-hangzhou": + case "", "cn-hangzhou": endpoint = "cas.aliyuncs.com" default: endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region) diff --git a/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go b/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go index cc1544c1..933bc51e 100644 --- a/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go +++ b/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go @@ -16,6 +16,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/uploader" certutil "github.com/usual2970/certimate/internal/pkg/utils/cert" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" ) type UploaderConfig struct { @@ -23,6 +24,8 @@ type UploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` } @@ -71,7 +74,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // 查询证书列表,避免重复上传 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeservercertificates describeServerCertificatesReq := &alislb.DescribeServerCertificatesRequest{ - RegionId: tea.String(u.config.Region), + ResourceGroupId: typeutil.ToPtrOrZeroNil(u.config.ResourceGroupId), + RegionId: tea.String(u.config.Region), } describeServerCertificatesResp, err := u.sdkClient.DescribeServerCertificates(describeServerCertificatesReq) u.logger.Debug("sdk request 'slb.DescribeServerCertificates'", slog.Any("request", describeServerCertificatesReq), slog.Any("response", describeServerCertificatesResp)) @@ -110,6 +114,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // 上传新证书 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-uploadservercertificate uploadServerCertificateReq := &alislb.UploadServerCertificateRequest{ + ResourceGroupId: typeutil.ToPtrOrZeroNil(u.config.ResourceGroupId), RegionId: tea.String(u.config.Region), ServerCertificateName: tea.String(certName), ServerCertificate: tea.String(certPEM), @@ -132,7 +137,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien // 接入点一览 https://api.aliyun.com/product/Slb var endpoint string switch region { - case + case "", "cn-hangzhou", "cn-hangzhou-finance", "cn-shanghai-finance-1", diff --git a/internal/pkg/utils/type/cast.go b/internal/pkg/utils/type/cast.go index 1acd4765..77eb9dad 100644 --- a/internal/pkg/utils/type/cast.go +++ b/internal/pkg/utils/type/cast.go @@ -22,10 +22,11 @@ func ToPtr[T any](v T) (p *T) { // 出参: // - 返回对象的指针。 func ToPtrOrZeroNil[T any](v T) (p *T) { - if !reflect.ValueOf(v).IsZero() { - return &v + if reflect.ValueOf(v).IsZero() { + return nil } - return nil + + return &v } // 将指针转换为对象。 diff --git a/ui/src/components/access/AccessFormAliyunConfig.tsx b/ui/src/components/access/AccessFormAliyunConfig.tsx index b3c0fbd0..4904b072 100644 --- a/ui/src/components/access/AccessFormAliyunConfig.tsx +++ b/ui/src/components/access/AccessFormAliyunConfig.tsx @@ -28,14 +28,15 @@ const AccessFormAliyunConfig = ({ form: formInst, formName, disabled, initialVal const formSchema = z.object({ accessKeyId: z .string() + .trim() .min(1, t("access.form.aliyun_access_key_id.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })) - .trim(), + .max(64, t("common.errmsg.string_max", { max: 64 })), accessKeySecret: z .string() + .trim() .min(1, t("access.form.aliyun_access_key_secret.placeholder")) - .max(64, t("common.errmsg.string_max", { max: 64 })) - .trim(), + .max(64, t("common.errmsg.string_max", { max: 64 })), + resourceGroupId: z.string().nullish(), }); const formRule = createSchemaFieldRule(formSchema); @@ -69,6 +70,24 @@ const AccessFormAliyunConfig = ({ form: formInst, formName, disabled, initialVal >
+ + } + > + + + + } + > + + ); }; diff --git a/ui/src/components/access/AccessFormHuaweiCloudConfig.tsx b/ui/src/components/access/AccessFormHuaweiCloudConfig.tsx index c460f473..ca83febd 100644 --- a/ui/src/components/access/AccessFormHuaweiCloudConfig.tsx +++ b/ui/src/components/access/AccessFormHuaweiCloudConfig.tsx @@ -36,11 +36,7 @@ const AccessFormHuaweiCloudConfig = ({ form: formInst, formName, disabled, initi .trim() .min(1, t("access.form.huaweicloud_secret_access_key.placeholder")) .max(64, t("common.errmsg.string_max", { max: 64 })), - enterpriseProjectId: z - .string() - .trim() - .max(64, t("common.errmsg.string_max", { max: 64 })) - .nullish(), + enterpriseProjectId: z.string().nullish(), }); const formRule = createSchemaFieldRule(formSchema); diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 51398e7f..fe9f12e3 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -101,6 +101,7 @@ export type AccessConfigForACMEHttpReq = { export type AccessConfigForAliyun = { accessKeyId: string; accessKeySecret: string; + resourceGroupId?: string; }; export type AccessConfigForAWS = { diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 13c9c5eb..1e570779 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -69,6 +69,9 @@ "access.form.aliyun_access_key_secret.label": "Aliyun AccessKeySecret", "access.form.aliyun_access_key_secret.placeholder": "Please enter Aliyun AccessKeySecret", "access.form.aliyun_access_key_secret.tooltip": "For more information, see https://www.alibabacloud.com/help/en/acr/create-and-obtain-an-accesskey-pair", + "access.form.aliyun_resource_group_id.label": "Aliyun resource group ID (Optional)", + "access.form.aliyun_resource_group_id.placeholder": "Please enter Aliyun resource group ID", + "access.form.aliyun_resource_group_id.tooltip": "For more information, see https://www.alibabacloud.com/help/en/resource-management/product-overview", "access.form.aws_access_key_id.label": "AWS AccessKeyId", "access.form.aws_access_key_id.placeholder": "Please enter AWS AccessKeyId", "access.form.aws_access_key_id.tooltip": "For more information, see https://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/id_credentials_access-keys.html", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 43305dda..7e5abf8e 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -69,6 +69,9 @@ "access.form.aliyun_access_key_secret.label": "阿里云 AccessKeySecret", "access.form.aliyun_access_key_secret.placeholder": "请输入阿里云 AccessKeySecret", "access.form.aliyun_access_key_secret.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair", + "access.form.aliyun_resource_group_id.label": "阿里云资源组 ID(可选)", + "access.form.aliyun_resource_group_id.placeholder": "请输入阿里云资源组 ID", + "access.form.aliyun_resource_group_id.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/resource-management/resource-group/product-overview", "access.form.aws_access_key_id.label": "AWS AccessKeyId", "access.form.aws_access_key_id.placeholder": "请输入 AWS AccessKeyId", "access.form.aws_access_key_id.tooltip": "这是什么?请参阅 https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/id_credentials_access-keys.html", From ddb46f9dda6d48a7aea3c4cb8b8952da619aa672 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 28 May 2025 10:17:33 +0800 Subject: [PATCH 14/28] refactor: clean code --- internal/domain/workflow.go | 8 ++++---- .../providers/aliyun-cas-deploy/aliyun_cas_deploy.go | 2 +- .../deployer/providers/baotawaf-site/baotawaf_site.go | 2 +- internal/pkg/core/deployer/providers/local/local.go | 2 +- internal/pkg/core/deployer/providers/ssh/ssh.go | 8 ++++---- internal/pkg/core/deployer/providers/webhook/webhook.go | 2 +- internal/pkg/core/notifier/providers/bark/bark.go | 2 +- internal/pkg/core/notifier/providers/webhook/webhook.go | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 6a96dd81..10925b72 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -68,17 +68,17 @@ type WorkflowNodeConfigForApply struct { Provider string `json:"provider"` // DNS 提供商 ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 - CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值将使用全局配置) + CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值时使用全局配置) CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置 KeyAlgorithm string `json:"keyAlgorithm"` // 证书算法 Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔 DnsPropagationWait int32 `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间,等同于 lego 的 `--dns-propagation-wait` 参数 - DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播检查超时时间(零值取决于提供商的默认值) - DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS 解析记录 TTL(零值取决于提供商的默认值) + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播检查超时时间(零值时使用提供商的默认值) + DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS 解析记录 TTL(零值时使用提供商的默认值) DisableFollowCNAME bool `json:"disableFollowCNAME,omitempty"` // 是否关闭 CNAME 跟随 DisableARI bool `json:"disableARI,omitempty"` // 是否关闭 ARI - SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30) + SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值时默认值 30) } type WorkflowNodeConfigForUpload struct { diff --git a/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go b/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go index 569edaf3..5acdb50e 100644 --- a/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go +++ b/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go @@ -29,7 +29,7 @@ type DeployerConfig struct { // 阿里云云产品资源 ID 数组。 ResourceIds []string `json:"resourceIds"` // 阿里云云联系人 ID 数组。 - // 零值时默认使用账号下第一个联系人。 + // 零值时使用账号下第一个联系人。 ContactIds []string `json:"contactIds"` } diff --git a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go index 435f7a69..945d5a48 100644 --- a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go +++ b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go @@ -23,7 +23,7 @@ type DeployerConfig struct { // 网站名称。 SiteName string `json:"siteName"` // 网站 SSL 端口。 - // 零值时默认为 443。 + // 零值时默认值 443。 SitePort int32 `json:"sitePort,omitempty"` } diff --git a/internal/pkg/core/deployer/providers/local/local.go b/internal/pkg/core/deployer/providers/local/local.go index a71ad9d3..8b05d95b 100644 --- a/internal/pkg/core/deployer/providers/local/local.go +++ b/internal/pkg/core/deployer/providers/local/local.go @@ -15,7 +15,7 @@ import ( type DeployerConfig struct { // Shell 执行环境。 - // 零值时默认根据操作系统决定。 + // 零值时根据操作系统决定。 ShellEnv ShellEnvType `json:"shellEnv,omitempty"` // 前置命令。 PreCommand string `json:"preCommand,omitempty"` diff --git a/internal/pkg/core/deployer/providers/ssh/ssh.go b/internal/pkg/core/deployer/providers/ssh/ssh.go index ae6e459f..96447cfb 100644 --- a/internal/pkg/core/deployer/providers/ssh/ssh.go +++ b/internal/pkg/core/deployer/providers/ssh/ssh.go @@ -19,10 +19,10 @@ import ( type JumpServerConfig struct { // SSH 主机。 - // 零值时默认为 "localhost"。 + // 零值时默认值 "localhost"。 SshHost string `json:"sshHost,omitempty"` // SSH 端口。 - // 零值时默认为 22。 + // 零值时默认值 22。 SshPort int32 `json:"sshPort,omitempty"` // SSH 登录用户名。 SshUsername string `json:"sshUsername,omitempty"` @@ -36,10 +36,10 @@ type JumpServerConfig struct { type DeployerConfig struct { // SSH 主机。 - // 零值时默认为 "localhost"。 + // 零值时默认值 "localhost"。 SshHost string `json:"sshHost,omitempty"` // SSH 端口。 - // 零值时默认为 22。 + // 零值时默认值 22。 SshPort int32 `json:"sshPort,omitempty"` // SSH 登录用户名。 SshUsername string `json:"sshUsername,omitempty"` diff --git a/internal/pkg/core/deployer/providers/webhook/webhook.go b/internal/pkg/core/deployer/providers/webhook/webhook.go index 49b07b47..b25c129a 100644 --- a/internal/pkg/core/deployer/providers/webhook/webhook.go +++ b/internal/pkg/core/deployer/providers/webhook/webhook.go @@ -23,7 +23,7 @@ type DeployerConfig struct { // Webhook 回调数据(application/json 或 application/x-www-form-urlencoded 格式)。 WebhookData string `json:"webhookData,omitempty"` // 请求谓词。 - // 零值时默认为 "POST"。 + // 零值时默认值 "POST"。 Method string `json:"method,omitempty"` // 请求标头。 Headers map[string]string `json:"headers,omitempty"` diff --git a/internal/pkg/core/notifier/providers/bark/bark.go b/internal/pkg/core/notifier/providers/bark/bark.go index ec0d44f3..fb3298ec 100644 --- a/internal/pkg/core/notifier/providers/bark/bark.go +++ b/internal/pkg/core/notifier/providers/bark/bark.go @@ -12,7 +12,7 @@ import ( type NotifierConfig struct { // Bark 服务地址。 - // 零值时默认使用官方服务器。 + // 零值时使用官方服务器。 ServerUrl string `json:"serverUrl"` // Bark 设备密钥。 DeviceKey string `json:"deviceKey"` diff --git a/internal/pkg/core/notifier/providers/webhook/webhook.go b/internal/pkg/core/notifier/providers/webhook/webhook.go index 8850ea73..acc0caab 100644 --- a/internal/pkg/core/notifier/providers/webhook/webhook.go +++ b/internal/pkg/core/notifier/providers/webhook/webhook.go @@ -22,7 +22,7 @@ type NotifierConfig struct { // Webhook 回调数据(application/json 或 application/x-www-form-urlencoded 格式)。 WebhookData string `json:"webhookData,omitempty"` // 请求谓词。 - // 零值时默认为 "POST"。 + // 零值时默认值 "POST"。 Method string `json:"method,omitempty"` // 请求标头。 Headers map[string]string `json:"headers,omitempty"` From 829fa29cf11d31ceb2b7155075e51298b11711ba Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 28 May 2025 10:46:02 +0800 Subject: [PATCH 15/28] feat: add user-agent http header for thirdparty sdks --- .../notifier/providers/discordbot/discordbot.go | 3 ++- .../pkg/core/notifier/providers/gotify/gotify.go | 3 ++- .../notifier/providers/mattermost/mattermost.go | 4 +++- .../core/notifier/providers/pushover/pushover.go | 1 + .../core/notifier/providers/pushplus/pushplus.go | 1 + .../notifier/providers/serverchan/serverchan.go | 1 + .../core/notifier/providers/slackbot/slackbot.go | 3 ++- .../notifier/providers/telegrambot/telegrambot.go | 1 + .../core/notifier/providers/wecombot/wecombot.go | 1 + internal/pkg/sdk3rd/1panel/client.go | 5 ++--- internal/pkg/sdk3rd/baishan/client.go | 3 ++- internal/pkg/sdk3rd/btpanel/client.go | 8 ++++---- internal/pkg/sdk3rd/btwaf/client.go | 8 ++++---- internal/pkg/sdk3rd/bunny/client.go | 1 + internal/pkg/sdk3rd/cachefly/client.go | 2 +- internal/pkg/sdk3rd/cdnfly/client.go | 5 +++-- internal/pkg/sdk3rd/dcloud/unicloud/client.go | 3 ++- internal/pkg/sdk3rd/dnsla/client.go | 3 ++- internal/pkg/sdk3rd/flexcdn/client.go | 1 + internal/pkg/sdk3rd/gname/client.go | 12 ++++++------ internal/pkg/sdk3rd/goedge/client.go | 1 + internal/pkg/sdk3rd/lecdn/v3/client/client.go | 3 ++- internal/pkg/sdk3rd/lecdn/v3/master/client.go | 3 ++- internal/pkg/sdk3rd/netlify/client.go | 3 ++- internal/pkg/sdk3rd/rainyun/client.go | 3 ++- internal/pkg/sdk3rd/ratpanel/client.go | 1 + internal/pkg/sdk3rd/safeline/client.go | 6 +++--- internal/pkg/sdk3rd/upyun/console/client.go | 1 + internal/pkg/sdk3rd/wangsu/cdnpro/api.go | 4 ++-- internal/pkg/sdk3rd/wangsu/openapi/client.go | 15 ++++++++------- 30 files changed, 66 insertions(+), 43 deletions(-) diff --git a/internal/pkg/core/notifier/providers/discordbot/discordbot.go b/internal/pkg/core/notifier/providers/discordbot/discordbot.go index 3ed0cab7..20e7d304 100644 --- a/internal/pkg/core/notifier/providers/discordbot/discordbot.go +++ b/internal/pkg/core/notifier/providers/discordbot/discordbot.go @@ -52,8 +52,9 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s // REF: https://discord.com/developers/docs/resources/message#create-message req := n.httpClient.R(). SetContext(ctx). - SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bot "+n.config.BotToken). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "content": subject + "\n" + message, }) diff --git a/internal/pkg/core/notifier/providers/gotify/gotify.go b/internal/pkg/core/notifier/providers/gotify/gotify.go index c82cd5a5..aa7d36a0 100644 --- a/internal/pkg/core/notifier/providers/gotify/gotify.go +++ b/internal/pkg/core/notifier/providers/gotify/gotify.go @@ -57,8 +57,9 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s // REF: https://gotify.net/api-docs#/message/createMessage req := n.httpClient.R(). SetContext(ctx). - SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bearer "+n.config.Token). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "title": subject, "message": message, diff --git a/internal/pkg/core/notifier/providers/mattermost/mattermost.go b/internal/pkg/core/notifier/providers/mattermost/mattermost.go index 81283f7c..70c6effe 100644 --- a/internal/pkg/core/notifier/providers/mattermost/mattermost.go +++ b/internal/pkg/core/notifier/providers/mattermost/mattermost.go @@ -60,6 +60,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s loginReq := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "login_id": n.config.Username, "password": n.config.Password, @@ -76,8 +77,9 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s // REF: https://developers.mattermost.com/api-documentation/#/operations/CreatePost postReq := n.httpClient.R(). SetContext(ctx). - SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bearer "+loginResp.Header().Get("Token")). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "channel_id": n.config.ChannelId, "props": map[string]interface{}{ diff --git a/internal/pkg/core/notifier/providers/pushover/pushover.go b/internal/pkg/core/notifier/providers/pushover/pushover.go index 827a45d6..48238608 100644 --- a/internal/pkg/core/notifier/providers/pushover/pushover.go +++ b/internal/pkg/core/notifier/providers/pushover/pushover.go @@ -53,6 +53,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s req := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "title": subject, "message": message, diff --git a/internal/pkg/core/notifier/providers/pushplus/pushplus.go b/internal/pkg/core/notifier/providers/pushplus/pushplus.go index 79a27d49..025e1620 100644 --- a/internal/pkg/core/notifier/providers/pushplus/pushplus.go +++ b/internal/pkg/core/notifier/providers/pushplus/pushplus.go @@ -52,6 +52,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s req := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "title": subject, "content": message, diff --git a/internal/pkg/core/notifier/providers/serverchan/serverchan.go b/internal/pkg/core/notifier/providers/serverchan/serverchan.go index d1897ab4..0eb9bc24 100644 --- a/internal/pkg/core/notifier/providers/serverchan/serverchan.go +++ b/internal/pkg/core/notifier/providers/serverchan/serverchan.go @@ -51,6 +51,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s req := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "text": subject, "desp": message, diff --git a/internal/pkg/core/notifier/providers/slackbot/slackbot.go b/internal/pkg/core/notifier/providers/slackbot/slackbot.go index 7b16ad25..a453f8f1 100644 --- a/internal/pkg/core/notifier/providers/slackbot/slackbot.go +++ b/internal/pkg/core/notifier/providers/slackbot/slackbot.go @@ -52,8 +52,9 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s // REF: https://docs.slack.dev/messaging/sending-and-scheduling-messages#publishing req := n.httpClient.R(). SetContext(ctx). - SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bearer "+n.config.BotToken). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "token": n.config.BotToken, "channel": n.config.ChannelId, diff --git a/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go b/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go index 39e1f705..ef99c66b 100644 --- a/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go +++ b/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go @@ -53,6 +53,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s req := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "chat_id": n.config.ChatId, "text": subject + "\n" + message, diff --git a/internal/pkg/core/notifier/providers/wecombot/wecombot.go b/internal/pkg/core/notifier/providers/wecombot/wecombot.go index 8f51a70a..d6f86ef5 100644 --- a/internal/pkg/core/notifier/providers/wecombot/wecombot.go +++ b/internal/pkg/core/notifier/providers/wecombot/wecombot.go @@ -51,6 +51,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s req := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "msgtype": "text", "text": map[string]string{ diff --git a/internal/pkg/sdk3rd/1panel/client.go b/internal/pkg/sdk3rd/1panel/client.go index 3fe549a0..8090340e 100644 --- a/internal/pkg/sdk3rd/1panel/client.go +++ b/internal/pkg/sdk3rd/1panel/client.go @@ -14,8 +14,6 @@ import ( ) type Client struct { - apiKey string - client *resty.Client } @@ -25,7 +23,8 @@ func NewClient(serverUrl, apiVersion, apiKey string) *Client { } client := resty.New(). - SetBaseURL(strings.TrimRight(serverUrl, "/") + "/api/" + apiVersion). + SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api/"+apiVersion). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { timestamp := fmt.Sprintf("%d", time.Now().Unix()) tokenMd5 := md5.Sum([]byte("1panel" + apiKey + timestamp)) diff --git a/internal/pkg/sdk3rd/baishan/client.go b/internal/pkg/sdk3rd/baishan/client.go index b3e428ee..7922096e 100644 --- a/internal/pkg/sdk3rd/baishan/client.go +++ b/internal/pkg/sdk3rd/baishan/client.go @@ -19,7 +19,8 @@ type Client struct { func NewClient(apiToken string) *Client { client := resty.New(). SetBaseURL("https://cdn.api.baishan.com"). - SetHeader("token", apiToken) + SetHeader("User-Agent", "certimate"). + SetHeader("Token", apiToken) return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/btpanel/client.go b/internal/pkg/sdk3rd/btpanel/client.go index aafee04f..7faa46c0 100644 --- a/internal/pkg/sdk3rd/btpanel/client.go +++ b/internal/pkg/sdk3rd/btpanel/client.go @@ -21,7 +21,9 @@ type Client struct { func NewClient(serverUrl, apiKey string) *Client { client := resty.New(). - SetBaseURL(strings.TrimRight(serverUrl, "/")) + SetBaseURL(strings.TrimRight(serverUrl, "/")). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetHeader("User-Agent", "certimate") return &Client{ apiKey: apiKey, @@ -77,9 +79,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, data["request_time"] = fmt.Sprintf("%d", timestamp) data["request_token"] = c.generateSignature(fmt.Sprintf("%d", timestamp)) - req := c.client.R(). - SetHeader("Content-Type", "application/x-www-form-urlencoded"). - SetFormData(data) + req := c.client.R().SetFormData(data) resp, err := req.Post(path) if err != nil { return resp, fmt.Errorf("baota api error: failed to send request: %w", err) diff --git a/internal/pkg/sdk3rd/btwaf/client.go b/internal/pkg/sdk3rd/btwaf/client.go index 083db0c1..4bf76b16 100644 --- a/internal/pkg/sdk3rd/btwaf/client.go +++ b/internal/pkg/sdk3rd/btwaf/client.go @@ -19,7 +19,9 @@ type Client struct { func NewClient(serverUrl, apiKey string) *Client { client := resty.New(). - SetBaseURL(strings.TrimRight(serverUrl, "/") + "/api"). + SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api"). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { timestamp := fmt.Sprintf("%d", time.Now().Unix()) keyMd5 := md5.Sum([]byte(apiKey)) @@ -48,9 +50,7 @@ func (c *Client) WithTLSConfig(config *tls.Config) *Client { } func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, error) { - req := c.client.R(). - SetHeader("Content-Type", "application/json"). - SetBody(params) + req := c.client.R().SetBody(params) resp, err := req.Post(path) if err != nil { return resp, fmt.Errorf("baota api error: failed to send request: %w", err) diff --git a/internal/pkg/sdk3rd/bunny/client.go b/internal/pkg/sdk3rd/bunny/client.go index 8d50e1fc..1efa2236 100644 --- a/internal/pkg/sdk3rd/bunny/client.go +++ b/internal/pkg/sdk3rd/bunny/client.go @@ -17,6 +17,7 @@ type Client struct { func NewClient(apiToken string) *Client { client := resty.New(). SetBaseURL("https://api.bunny.net"). + SetHeader("User-Agent", "certimate"). SetHeader("AccessKey", apiToken) return &Client{ diff --git a/internal/pkg/sdk3rd/cachefly/client.go b/internal/pkg/sdk3rd/cachefly/client.go index 342e329d..cf29e833 100644 --- a/internal/pkg/sdk3rd/cachefly/client.go +++ b/internal/pkg/sdk3rd/cachefly/client.go @@ -17,7 +17,7 @@ type Client struct { func NewClient(apiToken string) *Client { client := resty.New(). SetBaseURL("https://api.cachefly.com/api/2.5"). - SetHeader("x-cf-authorization", "Bearer "+apiToken) + SetHeader("X-CF-Authorization", "Bearer "+apiToken) return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/cdnfly/client.go b/internal/pkg/sdk3rd/cdnfly/client.go index 2dabf6fd..6026d246 100644 --- a/internal/pkg/sdk3rd/cdnfly/client.go +++ b/internal/pkg/sdk3rd/cdnfly/client.go @@ -18,8 +18,9 @@ type Client struct { func NewClient(serverUrl, apiKey, apiSecret string) *Client { client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). - SetHeader("api-key", apiKey). - SetHeader("api-secret", apiSecret) + SetHeader("User-Agent", "certimate"). + SetHeader("API-Key", apiKey). + SetHeader("API-Secret", apiSecret) return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/dcloud/unicloud/client.go b/internal/pkg/sdk3rd/dcloud/unicloud/client.go index 1e0f3728..8db4a792 100644 --- a/internal/pkg/sdk3rd/dcloud/unicloud/client.go +++ b/internal/pkg/sdk3rd/dcloud/unicloud/client.go @@ -51,6 +51,7 @@ func NewClient(username, password string) *Client { client.serverlessClient = resty.New() client.apiClient = resty.New(). SetBaseURL("https://unicloud-api.dcloud.net.cn/unicloud/api"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.apiUserToken != "" { req.Header.Set("Token", client.apiUserToken) @@ -173,9 +174,9 @@ func (c *Client) invokeServerless(endpoint, clientSecret, appId, spaceId, target sign := c.generateSignature(payload, clientSecret) req := c.serverlessClient.R(). + SetHeader("Content-Type", "application/json"). SetHeader("Origin", "https://unicloud.dcloud.net.cn"). SetHeader("Referer", "https://unicloud.dcloud.net.cn"). - SetHeader("Content-Type", "application/json"). SetHeader("X-Client-Info", string(clientInfoJsonb)). SetHeader("X-Client-Token", c.serverlessJwtToken). SetHeader("X-Serverless-Sign", sign). diff --git a/internal/pkg/sdk3rd/dnsla/client.go b/internal/pkg/sdk3rd/dnsla/client.go index d9a86fc5..accd36d9 100644 --- a/internal/pkg/sdk3rd/dnsla/client.go +++ b/internal/pkg/sdk3rd/dnsla/client.go @@ -17,7 +17,8 @@ type Client struct { func NewClient(apiId, apiSecret string) *Client { client := resty.New(). SetBaseURL("https://api.dns.la/api"). - SetBasicAuth(apiId, apiSecret) + SetBasicAuth(apiId, apiSecret). + SetHeader("User-Agent", "certimate") return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/flexcdn/client.go b/internal/pkg/sdk3rd/flexcdn/client.go index b478ffac..0844ffa5 100644 --- a/internal/pkg/sdk3rd/flexcdn/client.go +++ b/internal/pkg/sdk3rd/flexcdn/client.go @@ -32,6 +32,7 @@ func NewClient(serverUrl, apiRole, accessKeyId, accessKey string) *Client { } client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("X-Cloud-Access-Token", client.accessToken) diff --git a/internal/pkg/sdk3rd/gname/client.go b/internal/pkg/sdk3rd/gname/client.go index ef00e699..843785a5 100644 --- a/internal/pkg/sdk3rd/gname/client.go +++ b/internal/pkg/sdk3rd/gname/client.go @@ -20,7 +20,10 @@ type Client struct { } func NewClient(appId, appKey string) *Client { - client := resty.New() + client := resty.New(). + SetBaseURL("http://api.gname.com"). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetHeader("User-Agent", "certimate") return &Client{ appId: appId, @@ -74,11 +77,8 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, data["gntime"] = fmt.Sprintf("%d", time.Now().Unix()) data["gntoken"] = c.generateSignature(data) - url := "http://api.gname.com" + path - req := c.client.R(). - SetHeader("Content-Type", "application/x-www-form-urlencoded"). - SetFormData(data) - resp, err := req.Post(url) + req := c.client.R().SetFormData(data) + resp, err := req.Post(path) if err != nil { return resp, fmt.Errorf("gname api error: failed to send request: %w", err) } else if resp.IsError() { diff --git a/internal/pkg/sdk3rd/goedge/client.go b/internal/pkg/sdk3rd/goedge/client.go index 3cd4900a..bc87734a 100644 --- a/internal/pkg/sdk3rd/goedge/client.go +++ b/internal/pkg/sdk3rd/goedge/client.go @@ -32,6 +32,7 @@ func NewClient(serverUrl, apiRole, accessKeyId, accessKey string) *Client { } client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("X-Edge-Access-Token", client.accessToken) diff --git a/internal/pkg/sdk3rd/lecdn/v3/client/client.go b/internal/pkg/sdk3rd/lecdn/v3/client/client.go index 3fa822eb..4af04d4f 100644 --- a/internal/pkg/sdk3rd/lecdn/v3/client/client.go +++ b/internal/pkg/sdk3rd/lecdn/v3/client/client.go @@ -28,7 +28,8 @@ func NewClient(serverUrl, username, password string) *Client { password: password, } client.client = resty.New(). - SetBaseURL(strings.TrimRight(serverUrl, "/") + "/prod-api"). + SetBaseURL(strings.TrimRight(serverUrl, "/")+"/prod-api"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("Authorization", "Bearer "+client.accessToken) diff --git a/internal/pkg/sdk3rd/lecdn/v3/master/client.go b/internal/pkg/sdk3rd/lecdn/v3/master/client.go index ee1abaca..dc033634 100644 --- a/internal/pkg/sdk3rd/lecdn/v3/master/client.go +++ b/internal/pkg/sdk3rd/lecdn/v3/master/client.go @@ -28,7 +28,8 @@ func NewClient(serverUrl, username, password string) *Client { password: password, } client.client = resty.New(). - SetBaseURL(strings.TrimRight(serverUrl, "/") + "/prod-api"). + SetBaseURL(strings.TrimRight(serverUrl, "/")+"/prod-api"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("Authorization", "Bearer "+client.accessToken) diff --git a/internal/pkg/sdk3rd/netlify/client.go b/internal/pkg/sdk3rd/netlify/client.go index d270e35e..bf3f4ad6 100644 --- a/internal/pkg/sdk3rd/netlify/client.go +++ b/internal/pkg/sdk3rd/netlify/client.go @@ -17,7 +17,8 @@ type Client struct { func NewClient(apiToken string) *Client { client := resty.New(). SetBaseURL("https://api.netlify.com/api/v1"). - SetHeader("Authorization", "Bearer "+apiToken) + SetHeader("Authorization", "Bearer "+apiToken). + SetHeader("User-Agent", "certimate") return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/rainyun/client.go b/internal/pkg/sdk3rd/rainyun/client.go index 80113f0d..cf9e1895 100644 --- a/internal/pkg/sdk3rd/rainyun/client.go +++ b/internal/pkg/sdk3rd/rainyun/client.go @@ -17,7 +17,8 @@ type Client struct { func NewClient(apiKey string) *Client { client := resty.New(). SetBaseURL("https://api.v2.rainyun.com"). - SetHeader("x-api-key", apiKey) + SetHeader("User-Agent", "certimate"). + SetHeader("X-API-Key", apiKey) return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/ratpanel/client.go b/internal/pkg/sdk3rd/ratpanel/client.go index f1d20359..e1abb6c5 100644 --- a/internal/pkg/sdk3rd/ratpanel/client.go +++ b/internal/pkg/sdk3rd/ratpanel/client.go @@ -25,6 +25,7 @@ func NewClient(serverUrl string, accessTokenId int32, accessToken string) *Clien SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { var body []byte var err error diff --git a/internal/pkg/sdk3rd/safeline/client.go b/internal/pkg/sdk3rd/safeline/client.go index 93d884a3..05ee6d1a 100644 --- a/internal/pkg/sdk3rd/safeline/client.go +++ b/internal/pkg/sdk3rd/safeline/client.go @@ -17,6 +17,8 @@ type Client struct { func NewClient(serverUrl, apiToken string) *Client { client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetHeader("X-SLCE-API-TOKEN", apiToken) return &Client{ @@ -35,9 +37,7 @@ func (c *Client) WithTLSConfig(config *tls.Config) *Client { } func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, error) { - req := c.client.R(). - SetHeader("Content-Type", "application/json"). - SetBody(params) + req := c.client.R().SetBody(params) resp, err := req.Post(path) if err != nil { return resp, fmt.Errorf("safeline api error: failed to send request: %w", err) diff --git a/internal/pkg/sdk3rd/upyun/console/client.go b/internal/pkg/sdk3rd/upyun/console/client.go index b207549e..e9202d91 100644 --- a/internal/pkg/sdk3rd/upyun/console/client.go +++ b/internal/pkg/sdk3rd/upyun/console/client.go @@ -26,6 +26,7 @@ func NewClient(username, password string) *Client { } client.client = resty.New(). SetBaseURL("https://console.upyun.com"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.loginCookie != "" { req.Header.Set("Cookie", client.loginCookie) diff --git a/internal/pkg/sdk3rd/wangsu/cdnpro/api.go b/internal/pkg/sdk3rd/wangsu/cdnpro/api.go index c6f8da04..c45e6921 100644 --- a/internal/pkg/sdk3rd/wangsu/cdnpro/api.go +++ b/internal/pkg/sdk3rd/wangsu/cdnpro/api.go @@ -11,7 +11,7 @@ import ( func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { resp := &CreateCertificateResponse{} rres, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/certificates", req, resp, func(r *resty.Request) { - r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp)) + r.SetHeader("X-CNC-Timestamp", fmt.Sprintf("%d", req.Timestamp)) }) if err != nil { return resp, err @@ -28,7 +28,7 @@ func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateR resp := &UpdateCertificateResponse{} rres, err := c.client.SendRequestWithResult(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId)), req, resp, func(r *resty.Request) { - r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp)) + r.SetHeader("X-CNC-Timestamp", fmt.Sprintf("%d", req.Timestamp)) }) if err != nil { return resp, err diff --git a/internal/pkg/sdk3rd/wangsu/openapi/client.go b/internal/pkg/sdk3rd/wangsu/openapi/client.go index a8f4f2af..09723032 100644 --- a/internal/pkg/sdk3rd/wangsu/openapi/client.go +++ b/internal/pkg/sdk3rd/wangsu/openapi/client.go @@ -30,9 +30,10 @@ type Result interface { func NewClient(accessKey, secretKey string) *Client { client := resty.New(). SetBaseURL("https://open.chinanetcenter.com"). - SetHeader("Host", "open.chinanetcenter.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). + SetHeader("Host", "open.chinanetcenter.com"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { // Step 1: Get request method method := req.Method @@ -85,7 +86,7 @@ func NewClient(accessKey, secretKey string) *Client { // Step 6: Get timestamp var reqtime time.Time - timestampString := req.Header.Get("x-cnc-timestamp") + timestampString := req.Header.Get("X-CNC-Timestamp") if timestampString == "" { reqtime = time.Now().UTC() timestampString = fmt.Sprintf("%d", reqtime.Unix()) @@ -111,9 +112,9 @@ func NewClient(accessKey, secretKey string) *Client { signHex := strings.ToLower(hex.EncodeToString(sign)) // Step 9: Add headers to request - req.Header.Set("x-cnc-accesskey", accessKey) - req.Header.Set("x-cnc-timestamp", timestampString) - req.Header.Set("x-cnc-auth-method", "AKSK") + req.Header.Set("X-CNC-AccessKey", accessKey) + req.Header.Set("X-CNC-Timestamp", timestampString) + req.Header.Set("X-CNC-Auth-Method", "AKSK") req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", SignAlgorithmHeader, accessKey, signedHeaders, signHex)) req.Header.Set("Date", reqtime.Format("Mon, 02 Jan 2006 15:04:05 GMT")) @@ -173,7 +174,7 @@ func (c *Client) SendRequestWithResult(method string, path string, params interf if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &result) - result.SetRequestId(resp.Header().Get("x-cnc-request-id")) + result.SetRequestId(resp.Header().Get("X-CNC-Request-Id")) } return resp, err } @@ -185,6 +186,6 @@ func (c *Client) SendRequestWithResult(method string, path string, params interf } } - result.SetRequestId(resp.Header().Get("x-cnc-request-id")) + result.SetRequestId(resp.Header().Get("X-CNC-Request-Id")) return resp, nil } From 0e8ebaa8851dee89c1420709c5c9a40470a1f851 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 28 May 2025 14:51:18 +0800 Subject: [PATCH 16/28] fix: #732 --- internal/domain/access.go | 2 +- .../core/deployer/providers/baotawaf-site/baotawaf_site_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/domain/access.go b/internal/domain/access.go index e31bb1a0..3910ffc3 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -384,7 +384,7 @@ type AccessConfigForWeComBot struct { type AccessConfigForWestcn struct { Username string `json:"username"` - ApiPassword string `json:"password"` + ApiPassword string `json:"apiPassword"` } type AccessConfigForZeroSSL struct { diff --git a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site_test.go b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site_test.go index 6bead4b5..e9b4b836 100644 --- a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site_test.go +++ b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site_test.go @@ -39,7 +39,7 @@ Shell command to run this test: --CERTIMATE_DEPLOYER_BAOTAWAFSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CERTIMATE_DEPLOYER_BAOTAWAFSITE_SERVERURL="http://127.0.0.1:8888" \ --CERTIMATE_DEPLOYER_BAOTAWAFSITE_APIKEY="your-api-key" \ - --CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITENAME="your-site-name"\ + --CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITENAME="your-site-name" \ --CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITEPORT=443 */ func TestDeploy(t *testing.T) { From daf22b7f153a2a92d6b060e643f982f0213e78c7 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 28 May 2025 16:44:11 +0800 Subject: [PATCH 17/28] feat: initialize aliyun fc ssl protocol --- internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go index c1e8c5a0..d86998d0 100644 --- a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go +++ b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go @@ -111,6 +111,9 @@ func (d *DeployerProvider) deployToFC3(ctx context.Context, certPEM string, priv TlsConfig: getCustomDomainResp.Body.TlsConfig, }, } + if tea.StringValue(updateCustomDomainReq.Body.Protocol) == "HTTP" { + updateCustomDomainReq.Body.Protocol = tea.String("HTTP,HTTPS") + } updateCustomDomainResp, err := d.sdkClients.FC3.UpdateCustomDomain(tea.String(d.config.Domain), updateCustomDomainReq) d.logger.Debug("sdk request 'fc.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp)) if err != nil { @@ -140,6 +143,9 @@ func (d *DeployerProvider) deployToFC2(ctx context.Context, certPEM string, priv Protocol: getCustomDomainResp.Body.Protocol, TlsConfig: getCustomDomainResp.Body.TlsConfig, } + if tea.StringValue(updateCustomDomainReq.Protocol) == "HTTP" { + updateCustomDomainReq.Protocol = tea.String("HTTP,HTTPS") + } updateCustomDomainResp, err := d.sdkClients.FC2.UpdateCustomDomain(tea.String(d.config.Domain), updateCustomDomainReq) d.logger.Debug("sdk request 'fc.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp)) if err != nil { From 605de595b1bc7d893b236c3f031a5b0d321190ed Mon Sep 17 00:00:00 2001 From: "Yoan.liu" Date: Wed, 28 May 2025 20:34:18 +0800 Subject: [PATCH 18/28] remove upx compression --- .github/workflows/release.yml | 11 ++++++----- .goreleaser.yml | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59247aec..12475bc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,11 +23,11 @@ jobs: uses: actions/setup-go@v5 with: go-version-file: "go.mod" - - - name: Install upx (optional) - run: | - sudo apt-get update - sudo apt-get install -y upx + + # - name: Install upx (optional) + # run: | + # sudo apt-get update + # sudo apt-get install -y upx - name: Build WebUI run: | @@ -49,3 +49,4 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + diff --git a/.goreleaser.yml b/.goreleaser.yml index d65550fd..65ce8d48 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -30,8 +30,8 @@ builds: - goos: darwin goarch: arm -upx: - - enabled: true +# upx: +# - enabled: true release: draft: true From 3a829ad53ba37d0fd7e4284cdaef55ebdf628b8b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 28 May 2025 21:05:56 +0800 Subject: [PATCH 19/28] refactor: workflow monitor(aka inspect) node --- internal/domain/workflow.go | 98 ++++----- .../workflow/node-processor/apply_node.go | 8 +- .../workflow/node-processor/deploy_node.go | 7 +- .../workflow/node-processor/inspect_node.go | 191 ------------------ .../node-processor/inspect_node_test.go | 39 ---- .../workflow/node-processor/monitor_node.go | 164 +++++++++++++++ .../node-processor/monitor_node_test.go | 28 +++ .../workflow/node-processor/notify_node.go | 9 +- internal/workflow/node-processor/processor.go | 10 +- .../workflow/node-processor/upload_node.go | 6 +- .../components/access/AccessFormSSHConfig.tsx | 2 +- ui/src/components/access/AccessSelect.tsx | 4 +- .../provider/ACMEDns01ProviderPicker.tsx | 2 +- .../provider/ACMEDns01ProviderSelect.tsx | 2 +- .../provider/AccessProviderPicker.tsx | 4 +- .../provider/AccessProviderSelect.tsx | 4 +- .../components/provider/CAProviderSelect.tsx | 2 +- .../provider/DeploymentProviderPicker.tsx | 2 +- .../provider/DeploymentProviderSelect.tsx | 2 +- .../provider/NotificationProviderSelect.tsx | 2 +- .../components/workflow/WorkflowElement.tsx | 11 +- ui/src/components/workflow/node/AddNode.tsx | 4 +- .../workflow/node/ConditionNode.tsx | 13 +- .../components/workflow/node/DeployNode.tsx | 2 +- .../workflow/node/InspectNodeConfigForm.tsx | 97 --------- .../node/{InspectNode.tsx => MonitorNode.tsx} | 28 +-- .../workflow/node/MonitorNodeConfigForm.tsx | 115 +++++++++++ .../components/workflow/node/NotifyNode.tsx | 2 +- .../components/workflow/node/UnknownNode.tsx | 45 +++++ ui/src/domain/provider.ts | 16 +- ui/src/domain/workflow.ts | 42 ++-- .../i18n/locales/en/nls.workflow.nodes.json | 55 +++-- ui/src/i18n/locales/zh/nls.access.json | 2 +- .../i18n/locales/zh/nls.workflow.nodes.json | 53 +++-- ui/src/pages/accesses/AccessList.tsx | 2 +- 35 files changed, 557 insertions(+), 516 deletions(-) delete mode 100644 internal/workflow/node-processor/inspect_node.go delete mode 100644 internal/workflow/node-processor/inspect_node_test.go create mode 100644 internal/workflow/node-processor/monitor_node.go create mode 100644 internal/workflow/node-processor/monitor_node_test.go delete mode 100644 ui/src/components/workflow/node/InspectNodeConfigForm.tsx rename ui/src/components/workflow/node/{InspectNode.tsx => MonitorNode.tsx} (72%) create mode 100644 ui/src/components/workflow/node/MonitorNodeConfigForm.tsx create mode 100644 ui/src/components/workflow/node/UnknownNode.tsx diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index afa379a8..7d7355c5 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -31,6 +31,7 @@ const ( WorkflowNodeTypeEnd = WorkflowNodeType("end") WorkflowNodeTypeApply = WorkflowNodeType("apply") WorkflowNodeTypeUpload = WorkflowNodeType("upload") + WorkflowNodeTypeMonitor = WorkflowNodeType("monitor") WorkflowNodeTypeDeploy = WorkflowNodeType("deploy") WorkflowNodeTypeNotify = WorkflowNodeType("notify") WorkflowNodeTypeBranch = WorkflowNodeType("branch") @@ -38,7 +39,6 @@ const ( WorkflowNodeTypeExecuteResultBranch = WorkflowNodeType("execute_result_branch") WorkflowNodeTypeExecuteSuccess = WorkflowNodeType("execute_success") WorkflowNodeTypeExecuteFailure = WorkflowNodeType("execute_failure") - WorkflowNodeTypeInspect = WorkflowNodeType("inspect") ) type WorkflowTriggerType string @@ -83,21 +83,17 @@ type WorkflowNodeConfigForApply struct { SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30) } -type WorkflowNodeConfigForCondition struct { - Expression Expr `json:"expression"` // 条件表达式 -} - -type WorkflowNodeConfigForInspect struct { - Host string `json:"host"` // 主机 - Domain string `json:"domain"` // 域名 - Port string `json:"port"` // 端口 - Path string `json:"path"` // 路径 -} - type WorkflowNodeConfigForUpload struct { - Certificate string `json:"certificate"` - PrivateKey string `json:"privateKey"` - Domains string `json:"domains"` + Certificate string `json:"certificate"` // 证书 PEM 内容 + PrivateKey string `json:"privateKey"` // 私钥 PEM 内容 + Domains string `json:"domains,omitempty"` +} + +type WorkflowNodeConfigForMonitor struct { + Host string `json:"host"` // 主机地址 + Port int32 `json:"port,omitempty"` // 端口(零值时默认值 443) + Domain string `json:"domain,omitempty"` // 域名(零值时默认值 [Host]) + RequestPath string `json:"requestPath,omitempty"` // 请求路径 } type WorkflowNodeConfigForDeploy struct { @@ -117,48 +113,8 @@ type WorkflowNodeConfigForNotify struct { Message string `json:"message"` // 通知内容 } -func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { - expression := n.Config["expression"] - if expression == nil { - return WorkflowNodeConfigForCondition{} - } - - raw, _ := json.Marshal(expression) - - expr, err := UnmarshalExpr([]byte(raw)) - if err != nil { - return WorkflowNodeConfigForCondition{} - } - - return WorkflowNodeConfigForCondition{ - Expression: expr, - } -} - -func (n *WorkflowNode) GetConfigForInspect() WorkflowNodeConfigForInspect { - host := maputil.GetString(n.Config, "host") - if host == "" { - return WorkflowNodeConfigForInspect{} - } - - domain := maputil.GetString(n.Config, "domain") - if domain == "" { - domain = host - } - - port := maputil.GetString(n.Config, "port") - if port == "" { - port = "443" - } - - path := maputil.GetString(n.Config, "path") - - return WorkflowNodeConfigForInspect{ - Domain: domain, - Port: port, - Host: host, - Path: path, - } +type WorkflowNodeConfigForCondition struct { + Expression Expr `json:"expression"` // 条件表达式 } func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { @@ -190,6 +146,16 @@ func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload { } } +func (n *WorkflowNode) GetConfigForMonitor() WorkflowNodeConfigForMonitor { + host := maputil.GetString(n.Config, "host") + return WorkflowNodeConfigForMonitor{ + Host: host, + Port: maputil.GetOrDefaultInt32(n.Config, "port", 443), + Domain: maputil.GetOrDefaultString(n.Config, "domain", host), + RequestPath: maputil.GetString(n.Config, "path"), + } +} + func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy { return WorkflowNodeConfigForDeploy{ Certificate: maputil.GetString(n.Config, "certificate"), @@ -211,6 +177,24 @@ func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify { } } +func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { + expression := n.Config["expression"] + if expression == nil { + return WorkflowNodeConfigForCondition{} + } + + raw, _ := json.Marshal(expression) + + expr, err := UnmarshalExpr([]byte(raw)) + if err != nil { + return WorkflowNodeConfigForCondition{} + } + + return WorkflowNodeConfigForCondition{ + Expression: expr, + } +} + type WorkflowNodeIO struct { Label string `json:"label"` Name string `json:"name"` diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index e663e3fc..321d9fc8 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -34,7 +34,7 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode { } func (n *applyNode) Process(ctx context.Context) error { - n.logger.Info("ready to apply ...") + n.logger.Info("ready to obtain certificiate ...") // 查询上次执行结果 lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) @@ -63,7 +63,7 @@ func (n *applyNode) Process(ctx context.Context) error { // 申请证书 applyResult, err := applicant.Apply(ctx) if err != nil { - n.logger.Warn("failed to apply") + n.logger.Warn("failed to obtain certificiate") return err } @@ -112,7 +112,7 @@ func (n *applyNode) Process(ctx context.Context) error { n.outputs[outputCertificateValidatedKey] = "true" n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24)) - n.logger.Info("apply completed") + n.logger.Info("application completed") return nil } @@ -156,7 +156,7 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo if expirationTime > renewalInterval { n.outputs[outputCertificateValidatedKey] = "true" n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(expirationTime.Hours()/24)) - return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) + return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) } } } diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 3819b4a2..f0ded21d 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -33,7 +33,7 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode { } func (n *deployNode) Process(ctx context.Context) error { - n.logger.Info("ready to deploy ...") + n.logger.Info("ready to deploy certificate ...") // 查询上次执行结果 lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) @@ -78,7 +78,7 @@ func (n *deployNode) Process(ctx context.Context) error { // 部署证书 if err := deployer.Deploy(ctx); err != nil { - n.logger.Warn("failed to deploy") + n.logger.Warn("failed to deploy certificate") return err } @@ -95,8 +95,7 @@ func (n *deployNode) Process(ctx context.Context) error { return err } - n.logger.Info("deploy completed") - + n.logger.Info("deployment completed") return nil } diff --git a/internal/workflow/node-processor/inspect_node.go b/internal/workflow/node-processor/inspect_node.go deleted file mode 100644 index a8661f37..00000000 --- a/internal/workflow/node-processor/inspect_node.go +++ /dev/null @@ -1,191 +0,0 @@ -package nodeprocessor - -import ( - "context" - "crypto/tls" - "crypto/x509" - "fmt" - "math" - "net" - "net/http" - "strings" - "time" - - "github.com/usual2970/certimate/internal/domain" -) - -type inspectNode struct { - node *domain.WorkflowNode - *nodeProcessor - *nodeOutputer -} - -func NewInspectNode(node *domain.WorkflowNode) *inspectNode { - return &inspectNode{ - node: node, - nodeProcessor: newNodeProcessor(node), - nodeOutputer: newNodeOutputer(), - } -} - -func (n *inspectNode) Process(ctx context.Context) error { - n.logger.Info("entering inspect certificate node...") - - nodeConfig := n.node.GetConfigForInspect() - - err := n.inspect(ctx, nodeConfig) - if err != nil { - n.logger.Warn("inspect certificate failed: " + err.Error()) - return err - } - - return nil -} - -func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNodeConfigForInspect) error { - maxRetries := 3 - retryInterval := 2 * time.Second - - var lastError error - var certInfo *x509.Certificate - - host := nodeConfig.Host - - port := nodeConfig.Port - if port == "" { - port = "443" - } - - domain := nodeConfig.Domain - if domain == "" { - domain = host - } - - path := nodeConfig.Path - if path != "" && !strings.HasPrefix(path, "/") { - path = "/" + path - } - - targetAddr := fmt.Sprintf("%s:%s", host, port) - n.logger.Info(fmt.Sprintf("Inspecting certificate at %s (validating domain: %s)", targetAddr, domain)) - - for attempt := 0; attempt < maxRetries; attempt++ { - if attempt > 0 { - n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, targetAddr)) - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(retryInterval): - // Wait for retry interval - } - } - - transport := &http.Transport{ - DialContext: (&net.Dialer{ - Timeout: 10 * time.Second, - }).DialContext, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - ServerName: domain, // Set SNI to domain for proper certificate selection - }, - ForceAttemptHTTP2: false, - DisableKeepAlives: true, - } - - client := &http.Client{ - Transport: transport, - Timeout: 15 * time.Second, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - } - - scheme := "https" - urlStr := fmt.Sprintf("%s://%s", scheme, targetAddr) - if path != "" { - urlStr = urlStr + path - } - - req, err := http.NewRequestWithContext(ctx, "HEAD", urlStr, nil) - if err != nil { - lastError = fmt.Errorf("failed to create HTTP request: %w", err) - n.logger.Warn(fmt.Sprintf("Request creation attempt #%d failed: %s", attempt+1, lastError.Error())) - continue - } - - if domain != host { - req.Host = domain - } - - req.Header.Set("User-Agent", "CertificateValidator/1.0") - req.Header.Set("Accept", "*/*") - - resp, err := client.Do(req) - if err != nil { - lastError = fmt.Errorf("HTTP request failed: %w", err) - n.logger.Warn(fmt.Sprintf("Connection attempt #%d failed: %s", attempt+1, lastError.Error())) - continue - } - - if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { - resp.Body.Close() - lastError = fmt.Errorf("no TLS certificates received in HTTP response") - n.logger.Warn(fmt.Sprintf("Certificate retrieval attempt #%d failed: %s", attempt+1, lastError.Error())) - continue - } - - certInfo = resp.TLS.PeerCertificates[0] - resp.Body.Close() - - lastError = nil - n.logger.Info(fmt.Sprintf("Successfully retrieved certificate from %s", targetAddr)) - break - } - - if lastError != nil { - return fmt.Errorf("failed to retrieve certificate after %d attempts: %w", maxRetries, lastError) - } - - if certInfo == nil { - outputs := map[string]any{ - outputCertificateValidatedKey: "false", - outputCertificateDaysLeftKey: "0", - } - n.setOutputs(outputs) - return nil - } - - now := time.Now() - - isValidTime := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore) - - domainMatch := true - if err := certInfo.VerifyHostname(domain); err != nil { - domainMatch = false - } - - isValid := isValidTime && domainMatch - - daysRemaining := math.Floor(certInfo.NotAfter.Sub(now).Hours() / 24) - - isValidStr := "false" - if isValid { - isValidStr = "true" - } - - outputs := map[string]any{ - outputCertificateValidatedKey: isValidStr, - outputCertificateDaysLeftKey: fmt.Sprintf("%d", int(daysRemaining)), - } - - n.setOutputs(outputs) - - n.logger.Info(fmt.Sprintf("Certificate inspection completed - Target: %s, Domain: %s, Valid: %s, Days Remaining: %d", - targetAddr, domain, isValidStr, int(daysRemaining))) - - return nil -} - -func (n *inspectNode) setOutputs(outputs map[string]any) { - n.outputs = outputs -} diff --git a/internal/workflow/node-processor/inspect_node_test.go b/internal/workflow/node-processor/inspect_node_test.go deleted file mode 100644 index 5cb826c1..00000000 --- a/internal/workflow/node-processor/inspect_node_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package nodeprocessor - -import ( - "context" - "testing" - - "github.com/usual2970/certimate/internal/domain" -) - -func Test_inspectWebsiteCertificateNode_inspect(t *testing.T) { - type args struct { - ctx context.Context - nodeConfig domain.WorkflowNodeConfigForInspect - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "test1", - args: args{ - ctx: context.Background(), - nodeConfig: domain.WorkflowNodeConfigForInspect{ - Domain: "baidu.com", - Port: "443", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - n := NewInspectNode(&domain.WorkflowNode{}) - if err := n.inspect(tt.args.ctx, tt.args.nodeConfig); (err != nil) != tt.wantErr { - t.Errorf("inspectWebsiteCertificateNode.inspect() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/internal/workflow/node-processor/monitor_node.go b/internal/workflow/node-processor/monitor_node.go new file mode 100644 index 00000000..f8c1adae --- /dev/null +++ b/internal/workflow/node-processor/monitor_node.go @@ -0,0 +1,164 @@ +package nodeprocessor + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "math" + "net" + "net/http" + "strconv" + "strings" + "time" + + "github.com/usual2970/certimate/internal/domain" +) + +type monitorNode struct { + node *domain.WorkflowNode + *nodeProcessor + *nodeOutputer +} + +func NewMonitorNode(node *domain.WorkflowNode) *monitorNode { + return &monitorNode{ + node: node, + nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), + } +} + +func (n *monitorNode) Process(ctx context.Context) error { + n.logger.Info("ready to monitor certificate ...") + + nodeConfig := n.node.GetConfigForMonitor() + + targetAddr := fmt.Sprintf("%s:%d", nodeConfig.Host, nodeConfig.Port) + if nodeConfig.Port == 0 { + targetAddr = fmt.Sprintf("%s:443", nodeConfig.Host) + } + + targetDomain := nodeConfig.Domain + if targetDomain == "" { + targetDomain = nodeConfig.Host + } + + n.logger.Info(fmt.Sprintf("retrieving certificate at %s (domain: %s)", targetAddr, targetDomain)) + + const MAX_ATTEMPTS = 3 + const RETRY_INTERVAL = 2 * time.Second + var cert *x509.Certificate + var err error + for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ { + if attempt > 0 { + n.logger.Info(fmt.Sprintf("retry %d time(s) ...", attempt, targetAddr)) + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(RETRY_INTERVAL): + } + } + + cert, err = n.tryRetrieveCert(ctx, targetAddr, targetDomain, nodeConfig.RequestPath) + if err == nil { + break + } + } + + if err != nil { + n.logger.Warn("failed to monitor certificate") + return err + } else { + if cert == nil { + n.logger.Warn("no ssl certificates retrieved in http response") + + outputs := map[string]any{ + outputCertificateValidatedKey: strconv.FormatBool(false), + outputCertificateDaysLeftKey: strconv.FormatInt(0, 10), + } + n.setOutputs(outputs) + } else { + n.logger.Info(fmt.Sprintf("ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')", + cert.SerialNumber, cert.Subject.String(), cert.Issuer.String(), + cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339), + strings.Join(cert.DNSNames, ";")), + ) + + now := time.Now() + isCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore) + isCertHostMatched := true + if err := cert.VerifyHostname(targetDomain); err != nil { + isCertHostMatched = false + } + + validated := isCertPeriodValid && isCertHostMatched + daysLeft := int(math.Floor(cert.NotAfter.Sub(now).Hours() / 24)) + outputs := map[string]any{ + outputCertificateValidatedKey: strconv.FormatBool(validated), + outputCertificateDaysLeftKey: strconv.FormatInt(int64(daysLeft), 10), + } + n.setOutputs(outputs) + + if validated { + n.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft)) + } else { + n.logger.Warn(fmt.Sprintf("the certificate is invalid", validated)) + } + } + } + + n.logger.Info("monitoring completed") + return nil +} + +func (n *monitorNode) tryRetrieveCert(ctx context.Context, addr, domain, requestPath string) (_cert *x509.Certificate, _err error) { + transport := &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + }).DialContext, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + ForceAttemptHTTP2: false, + DisableKeepAlives: true, + Proxy: http.ProxyFromEnvironment, + } + + client := &http.Client{ + Transport: transport, + Timeout: 15 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + url := fmt.Sprintf("https://%s/%s", addr, strings.TrimLeft(requestPath, "/")) + req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) + if err != nil { + _err = fmt.Errorf("failed to create http request: %w", err) + n.logger.Warn(fmt.Sprintf("failed to create http request: %w", err)) + return nil, _err + } + + req.Header.Set("User-Agent", "certimate") + resp, err := client.Do(req) + if err != nil { + _err = fmt.Errorf("failed to send http request: %w", err) + n.logger.Warn(fmt.Sprintf("failed to send http request: %w", err)) + return nil, _err + } + defer resp.Body.Close() + + if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { + return nil, _err + } + + _cert = resp.TLS.PeerCertificates[0] + return _cert, nil +} + +func (n *monitorNode) setOutputs(outputs map[string]any) { + n.outputs = outputs +} diff --git a/internal/workflow/node-processor/monitor_node_test.go b/internal/workflow/node-processor/monitor_node_test.go new file mode 100644 index 00000000..1cc0c876 --- /dev/null +++ b/internal/workflow/node-processor/monitor_node_test.go @@ -0,0 +1,28 @@ +package nodeprocessor_test + +import ( + "context" + "log/slog" + "testing" + + "github.com/usual2970/certimate/internal/domain" + nodeprocessor "github.com/usual2970/certimate/internal/workflow/node-processor" +) + +func Test_MonitorNode(t *testing.T) { + t.Run("Monitor", func(t *testing.T) { + node := nodeprocessor.NewMonitorNode(&domain.WorkflowNode{ + Id: "test", + Type: domain.WorkflowNodeTypeMonitor, + Name: "test", + Config: map[string]any{ + "host": "baidu.com", + "port": 443, + }, + }) + node.SetLogger(slog.Default()) + if err := node.Process(context.Background()); err != nil { + t.Errorf("err: %+v", err) + } + }) +} diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index 8f336931..f084cb4f 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -28,7 +28,7 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { } func (n *notifyNode) Process(ctx context.Context) error { - n.logger.Info("ready to notify ...") + n.logger.Info("ready to send notification ...") nodeConfig := n.node.GetConfigForNotify() @@ -51,11 +51,11 @@ func (n *notifyNode) Process(ctx context.Context) error { // 发送通知 if err := notify.SendToChannel(nodeConfig.Subject, nodeConfig.Message, nodeConfig.Channel, channelConfig); err != nil { - n.logger.Warn("failed to notify", slog.String("channel", nodeConfig.Channel)) + n.logger.Warn("failed to send notification", slog.String("channel", nodeConfig.Channel)) return err } - n.logger.Info("notify completed") + n.logger.Info("notification completed") return nil } @@ -73,9 +73,10 @@ func (n *notifyNode) Process(ctx context.Context) error { // 推送通知 if err := deployer.Notify(ctx); err != nil { - n.logger.Warn("failed to notify") + n.logger.Warn("failed to send notification") return err } + n.logger.Info("notification completed") return nil } diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index 24de76d1..d375883f 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -74,25 +74,25 @@ func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) { switch node.Type { case domain.WorkflowNodeTypeStart: return NewStartNode(node), nil - case domain.WorkflowNodeTypeCondition: - return NewConditionNode(node), nil case domain.WorkflowNodeTypeApply: return NewApplyNode(node), nil case domain.WorkflowNodeTypeUpload: return NewUploadNode(node), nil + case domain.WorkflowNodeTypeMonitor: + return NewMonitorNode(node), nil case domain.WorkflowNodeTypeDeploy: return NewDeployNode(node), nil case domain.WorkflowNodeTypeNotify: return NewNotifyNode(node), nil + case domain.WorkflowNodeTypeCondition: + return NewConditionNode(node), nil case domain.WorkflowNodeTypeExecuteSuccess: return NewExecuteSuccessNode(node), nil case domain.WorkflowNodeTypeExecuteFailure: return NewExecuteFailureNode(node), nil - case domain.WorkflowNodeTypeInspect: - return NewInspectNode(node), nil } - return nil, fmt.Errorf("supported node type: %s", string(node.Type)) + return nil, fmt.Errorf("unsupported node type: %s", string(node.Type)) } func getContextWorkflowId(ctx context.Context) string { diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 6a59ca74..8e59b009 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -31,7 +31,7 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode { } func (n *uploadNode) Process(ctx context.Context) error { - n.logger.Info("ready to upload ...") + n.logger.Info("ready to upload certiticate ...") nodeConfig := n.node.GetConfigForUpload() @@ -43,7 +43,7 @@ func (n *uploadNode) Process(ctx context.Context) error { // 检测是否可以跳过本次执行 if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable { - n.logger.Info(fmt.Sprintf("skip this upload, because %s", reason)) + n.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason)) return nil } else if reason != "" { n.logger.Info(fmt.Sprintf("re-upload, because %s", reason)) @@ -72,7 +72,7 @@ func (n *uploadNode) Process(ctx context.Context) error { n.outputs[outputCertificateValidatedKey] = "true" n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24)) - n.logger.Info("upload completed") + n.logger.Info("uploading completed") return nil } diff --git a/ui/src/components/access/AccessFormSSHConfig.tsx b/ui/src/components/access/AccessFormSSHConfig.tsx index 32d1b8bc..c964b1b3 100644 --- a/ui/src/components/access/AccessFormSSHConfig.tsx +++ b/ui/src/components/access/AccessFormSSHConfig.tsx @@ -120,7 +120,7 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
- +
diff --git a/ui/src/components/access/AccessSelect.tsx b/ui/src/components/access/AccessSelect.tsx index 7c112bbe..0a570699 100644 --- a/ui/src/components/access/AccessSelect.tsx +++ b/ui/src/components/access/AccessSelect.tsx @@ -37,7 +37,7 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { if (!access) { return ( - + {key} @@ -48,7 +48,7 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { const provider = accessProvidersMap.get(access.provider); return ( - + {access.name} diff --git a/ui/src/components/provider/ACMEDns01ProviderPicker.tsx b/ui/src/components/provider/ACMEDns01ProviderPicker.tsx index 0f20b296..5a5be8ca 100644 --- a/ui/src/components/provider/ACMEDns01ProviderPicker.tsx +++ b/ui/src/components/provider/ACMEDns01ProviderPicker.tsx @@ -67,7 +67,7 @@ const ACMEDns01ProviderPicker = ({ className, style, autoFocus, filter, placehol }} > - + {t(provider.name)}
diff --git a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx index b03adf7b..e2408eeb 100644 --- a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx +++ b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx @@ -32,7 +32,7 @@ const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectPr const provider = acmeDns01ProvidersMap.get(key); return ( - + {t(provider?.name ?? "")} diff --git a/ui/src/components/provider/AccessProviderPicker.tsx b/ui/src/components/provider/AccessProviderPicker.tsx index 002d2519..507a95c8 100644 --- a/ui/src/components/provider/AccessProviderPicker.tsx +++ b/ui/src/components/provider/AccessProviderPicker.tsx @@ -86,12 +86,12 @@ const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder }} > - +
{t(provider.name)} -
+
{t("access.props.provider.builtin")} diff --git a/ui/src/components/provider/AccessProviderSelect.tsx b/ui/src/components/provider/AccessProviderSelect.tsx index 37f1626d..bf4ff6e7 100644 --- a/ui/src/components/provider/AccessProviderSelect.tsx +++ b/ui/src/components/provider/AccessProviderSelect.tsx @@ -49,12 +49,12 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid return (
- + {t(provider.name)} -
+
{t("access.props.provider.builtin")} diff --git a/ui/src/components/provider/CAProviderSelect.tsx b/ui/src/components/provider/CAProviderSelect.tsx index 15d31230..e5477c21 100644 --- a/ui/src/components/provider/CAProviderSelect.tsx +++ b/ui/src/components/provider/CAProviderSelect.tsx @@ -48,7 +48,7 @@ const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => { const provider = caProvidersMap.get(key); return ( - + {t(provider?.name ?? "")} diff --git a/ui/src/components/provider/DeploymentProviderPicker.tsx b/ui/src/components/provider/DeploymentProviderPicker.tsx index b1bcd6fe..bb569acd 100644 --- a/ui/src/components/provider/DeploymentProviderPicker.tsx +++ b/ui/src/components/provider/DeploymentProviderPicker.tsx @@ -104,7 +104,7 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, filter, placeho > - + {t(provider.name)} diff --git a/ui/src/components/provider/DeploymentProviderSelect.tsx b/ui/src/components/provider/DeploymentProviderSelect.tsx index 0b38cedf..89173243 100644 --- a/ui/src/components/provider/DeploymentProviderSelect.tsx +++ b/ui/src/components/provider/DeploymentProviderSelect.tsx @@ -32,7 +32,7 @@ const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelect const provider = deploymentProvidersMap.get(key); return ( - + {t(provider?.name ?? "")} diff --git a/ui/src/components/provider/NotificationProviderSelect.tsx b/ui/src/components/provider/NotificationProviderSelect.tsx index 98a1005c..f30a8f6f 100644 --- a/ui/src/components/provider/NotificationProviderSelect.tsx +++ b/ui/src/components/provider/NotificationProviderSelect.tsx @@ -32,7 +32,7 @@ const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSe const provider = notificationProvidersMap.get(key); return ( - + {t(provider?.name ?? "")} diff --git a/ui/src/components/workflow/WorkflowElement.tsx b/ui/src/components/workflow/WorkflowElement.tsx index d36029df..86720f6d 100644 --- a/ui/src/components/workflow/WorkflowElement.tsx +++ b/ui/src/components/workflow/WorkflowElement.tsx @@ -9,10 +9,11 @@ import DeployNode from "./node/DeployNode"; import EndNode from "./node/EndNode"; import ExecuteResultBranchNode from "./node/ExecuteResultBranchNode"; import ExecuteResultNode from "./node/ExecuteResultNode"; +import MonitorNode from "./node/MonitorNode"; import NotifyNode from "./node/NotifyNode"; import StartNode from "./node/StartNode"; +import UnknownNode from "./node/UnknownNode"; import UploadNode from "./node/UploadNode"; -import InspectNode from "./node/InspectNode"; export type WorkflowElementProps = { node: WorkflowNode; @@ -32,9 +33,9 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem case WorkflowNodeType.Upload: return ; - - case WorkflowNodeType.Inspect: - return ; + + case WorkflowNodeType.Monitor: + return ; case WorkflowNodeType.Deploy: return ; @@ -60,7 +61,7 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem default: console.warn(`[certimate] unsupported workflow node type: ${node.type}`); - return <>; + return ; } }, [node, disabled, branchId, branchIndex]); diff --git a/ui/src/components/workflow/node/AddNode.tsx b/ui/src/components/workflow/node/AddNode.tsx index bf4c5be2..86a45134 100644 --- a/ui/src/components/workflow/node/AddNode.tsx +++ b/ui/src/components/workflow/node/AddNode.tsx @@ -3,11 +3,11 @@ import { useTranslation } from "react-i18next"; import { CloudUploadOutlined as CloudUploadOutlinedIcon, DeploymentUnitOutlined as DeploymentUnitOutlinedIcon, + MonitorOutlined as MonitorOutlinedIcon, PlusOutlined as PlusOutlinedIcon, SendOutlined as SendOutlinedIcon, SisternodeOutlined as SisternodeOutlinedIcon, SolutionOutlined as SolutionOutlinedIcon, - MonitorOutlined as MonitorOutlinedIcon, } from "@ant-design/icons"; import { Dropdown } from "antd"; @@ -28,7 +28,7 @@ const AddNode = ({ node, disabled }: AddNodeProps) => { return [ [WorkflowNodeType.Apply, "workflow_node.apply.label", ], [WorkflowNodeType.Upload, "workflow_node.upload.label", ], - [WorkflowNodeType.Inspect, "workflow_node.inspect.label", ], + [WorkflowNodeType.Monitor, "workflow_node.monitor.label", ], [WorkflowNodeType.Deploy, "workflow_node.deploy.label", ], [WorkflowNodeType.Notify, "workflow_node.notify.label", ], [WorkflowNodeType.Branch, "workflow_node.branch.label", ], diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index bcd58c77..bc5b5918 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -1,14 +1,17 @@ import { memo, useRef, useState } from "react"; import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; import { Button, Card, Popover } from "antd"; +import { produce } from "immer"; + +import type { Expr, WorkflowNodeIoValueType } from "@/domain/workflow"; +import { ExprType } from "@/domain/workflow"; +import { useZustandShallowSelector } from "@/hooks"; +import { useWorkflowStore } from "@/stores/workflow"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; -import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; -import { Expr, WorkflowNodeIoValueType, ExprType } from "@/domain/workflow"; -import { produce } from "immer"; -import { useWorkflowStore } from "@/stores/workflow"; -import { useZustandShallowSelector } from "@/hooks"; +import type { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; +import ConditionNodeConfigForm from "./ConditionNodeConfigForm"; export type ConditionNodeProps = SharedNodeProps & { branchId: string; diff --git a/ui/src/components/workflow/node/DeployNode.tsx b/ui/src/components/workflow/node/DeployNode.tsx index 92eb2890..b04516fb 100644 --- a/ui/src/components/workflow/node/DeployNode.tsx +++ b/ui/src/components/workflow/node/DeployNode.tsx @@ -46,7 +46,7 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => { const provider = deploymentProvidersMap.get(config.provider); return ( - + {t(provider?.name ?? "")} ); diff --git a/ui/src/components/workflow/node/InspectNodeConfigForm.tsx b/ui/src/components/workflow/node/InspectNodeConfigForm.tsx deleted file mode 100644 index 2d7d83b0..00000000 --- a/ui/src/components/workflow/node/InspectNodeConfigForm.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { forwardRef, memo, useImperativeHandle } from "react"; -import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; -import { createSchemaFieldRule } from "antd-zod"; -import { z } from "zod"; - -import { type WorkflowNodeConfigForInspect } from "@/domain/workflow"; -import { useAntdForm } from "@/hooks"; - -import { validDomainName, validIPv4Address, validPortNumber } from "@/utils/validators"; - -type InspectNodeConfigFormFieldValues = Partial; - -export type InspectNodeConfigFormProps = { - className?: string; - style?: React.CSSProperties; - disabled?: boolean; - initialValues?: InspectNodeConfigFormFieldValues; - onValuesChange?: (values: InspectNodeConfigFormFieldValues) => void; -}; - -export type InspectNodeConfigFormInstance = { - getFieldsValue: () => ReturnType["getFieldsValue"]>; - resetFields: FormInstance["resetFields"]; - validateFields: FormInstance["validateFields"]; -}; - -const initFormModel = (): InspectNodeConfigFormFieldValues => { - return { - domain: "", - port: "443", - path: "", - host: "", - }; -}; - -const InspectNodeConfigForm = forwardRef( - ({ className, style, disabled, initialValues, onValuesChange }, ref) => { - const { t } = useTranslation(); - - const formSchema = z.object({ - host: z.string().refine((val) => validIPv4Address(val) || validDomainName(val), { - message: t("workflow_node.inspect.form.host.placeholder"), - }), - domain: z.string().optional(), - port: z.string().refine((val) => validPortNumber(val), { - message: t("workflow_node.inspect.form.port.placeholder"), - }), - path: z.string().optional(), - }); - const formRule = createSchemaFieldRule(formSchema); - const { form: formInst, formProps } = useAntdForm({ - name: "workflowNodeInspectConfigForm", - initialValues: initialValues ?? initFormModel(), - }); - - const handleFormChange = (_: unknown, values: z.infer) => { - onValuesChange?.(values as InspectNodeConfigFormFieldValues); - }; - - useImperativeHandle(ref, () => { - return { - getFieldsValue: () => { - return formInst.getFieldsValue(true); - }, - resetFields: (fields) => { - return formInst.resetFields(fields as (keyof InspectNodeConfigFormFieldValues)[]); - }, - validateFields: (nameList, config) => { - return formInst.validateFields(nameList, config); - }, - } as InspectNodeConfigFormInstance; - }); - - return ( -
- - - - - - - - - - - - - - - -
- ); - } -); - -export default memo(InspectNodeConfigForm); diff --git a/ui/src/components/workflow/node/InspectNode.tsx b/ui/src/components/workflow/node/MonitorNode.tsx similarity index 72% rename from ui/src/components/workflow/node/InspectNode.tsx rename to ui/src/components/workflow/node/MonitorNode.tsx index 0d038894..68feb842 100644 --- a/ui/src/components/workflow/node/InspectNode.tsx +++ b/ui/src/components/workflow/node/MonitorNode.tsx @@ -3,43 +3,43 @@ import { useTranslation } from "react-i18next"; import { Flex, Typography } from "antd"; import { produce } from "immer"; -import { type WorkflowNodeConfigForInspect, WorkflowNodeType } from "@/domain/workflow"; +import { type WorkflowNodeConfigForMonitor, WorkflowNodeType } from "@/domain/workflow"; import { useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; -import InspectNodeConfigForm, { type InspectNodeConfigFormInstance } from "./InspectNodeConfigForm"; +import MonitorNodeConfigForm, { type MonitorNodeConfigFormInstance } from "./MonitorNodeConfigForm"; -export type InspectNodeProps = SharedNodeProps; +export type MonitorNodeProps = SharedNodeProps; -const InspectNode = ({ node, disabled }: InspectNodeProps) => { - if (node.type !== WorkflowNodeType.Inspect) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Inspect}`); +const MonitorNode = ({ node, disabled }: MonitorNodeProps) => { + if (node.type !== WorkflowNodeType.Monitor) { + console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Monitor}`); } const { t } = useTranslation(); const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); - const formRef = useRef(null); + const formRef = useRef(null); const [formPending, setFormPending] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForInspect; + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor; const wrappedEl = useMemo(() => { - if (node.type !== WorkflowNodeType.Inspect) { - console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Inspect}`); + if (node.type !== WorkflowNodeType.Monitor) { + console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Monitor}`); } if (!node.validated) { return {t("workflow_node.action.configure_node")}; } - const config = (node.config as WorkflowNodeConfigForInspect) ?? {}; + const config = (node.config as WorkflowNodeConfigForMonitor) ?? {}; return ( - {config.host ?? ""} + {config.domain || config.host || ""} ); }, [node]); @@ -81,10 +81,10 @@ const InspectNode = ({ node, disabled }: InspectNodeProps) => { onOpenChange={(open) => setDrawerOpen(open)} getFormValues={() => formRef.current!.getFieldsValue()} > - + ); }; -export default memo(InspectNode); +export default memo(MonitorNode); diff --git a/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx b/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx new file mode 100644 index 00000000..883124f9 --- /dev/null +++ b/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx @@ -0,0 +1,115 @@ +import { forwardRef, memo, useImperativeHandle } from "react"; +import { useTranslation } from "react-i18next"; +import { Alert, Form, type FormInstance, Input, InputNumber } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type WorkflowNodeConfigForMonitor } from "@/domain/workflow"; +import { useAntdForm } from "@/hooks"; +import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators"; + +type MonitorNodeConfigFormFieldValues = Partial; + +export type MonitorNodeConfigFormProps = { + className?: string; + style?: React.CSSProperties; + disabled?: boolean; + initialValues?: MonitorNodeConfigFormFieldValues; + onValuesChange?: (values: MonitorNodeConfigFormFieldValues) => void; +}; + +export type MonitorNodeConfigFormInstance = { + getFieldsValue: () => ReturnType["getFieldsValue"]>; + resetFields: FormInstance["resetFields"]; + validateFields: FormInstance["validateFields"]; +}; + +const initFormModel = (): MonitorNodeConfigFormFieldValues => { + return { + host: "", + port: 443, + requestPath: "/", + }; +}; + +const MonitorNodeConfigForm = forwardRef( + ({ className, style, disabled, initialValues, onValuesChange }, ref) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + host: z.string().refine((v) => { + return validDomainName(v) || validIPv4Address(v) || validIPv6Address(v); + }, t("common.errmsg.host_invalid")), + port: z.preprocess( + (v) => Number(v), + z + .number() + .int(t("workflow_node.monitor.form.port.placeholder")) + .refine((v) => validPortNumber(v), t("common.errmsg.port_invalid")) + ), + domain: z + .string() + .nullish() + .refine((v) => { + if (!v) return true; + return validDomainName(v); + }, t("common.errmsg.domain_invalid")), + requestPath: z.string().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + const { form: formInst, formProps } = useAntdForm({ + name: "workflowNodeMonitorConfigForm", + initialValues: initialValues ?? initFormModel(), + }); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values as MonitorNodeConfigFormFieldValues); + }; + + useImperativeHandle(ref, () => { + return { + getFieldsValue: () => { + return formInst.getFieldsValue(true); + }, + resetFields: (fields) => { + return formInst.resetFields(fields as (keyof MonitorNodeConfigFormFieldValues)[]); + }, + validateFields: (nameList, config) => { + return formInst.validateFields(nameList, config); + }, + } as MonitorNodeConfigFormInstance; + }); + + return ( +
+ + } /> + + +
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + +
+ ); + } +); + +export default memo(MonitorNodeConfigForm); diff --git a/ui/src/components/workflow/node/NotifyNode.tsx b/ui/src/components/workflow/node/NotifyNode.tsx index 16132539..da48552d 100644 --- a/ui/src/components/workflow/node/NotifyNode.tsx +++ b/ui/src/components/workflow/node/NotifyNode.tsx @@ -43,7 +43,7 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => { const provider = notificationProvidersMap.get(config.provider); return ( - + {t(channel?.name ?? provider?.name ?? " ")} {config.subject ?? ""} diff --git a/ui/src/components/workflow/node/UnknownNode.tsx b/ui/src/components/workflow/node/UnknownNode.tsx new file mode 100644 index 00000000..7cb64aae --- /dev/null +++ b/ui/src/components/workflow/node/UnknownNode.tsx @@ -0,0 +1,45 @@ +import { memo } from "react"; +import { CloseCircleOutlined as CloseCircleOutlinedIcon } from "@ant-design/icons"; +import { Alert, Button, Card } from "antd"; + +import { useZustandShallowSelector } from "@/hooks"; +import { useWorkflowStore } from "@/stores/workflow"; + +import { type SharedNodeProps } from "./_SharedNode"; +import AddNode from "./AddNode"; + +export type MonitorNodeProps = SharedNodeProps; + +const UnknownNode = ({ node, disabled }: MonitorNodeProps) => { + const { removeNode } = useWorkflowStore(useZustandShallowSelector(["removeNode"])); + + const handleClickRemove = () => { + removeNode(node.id); + }; + + return ( + <> + +
+ +
+ INVALID NODE +
+ PLEASE REMOVE +
+
+ } + /> +
+ + + + + ); +}; + +export default memo(UnknownNode); diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 594674f1..bb550691 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -553,6 +553,14 @@ export const deploymentProvidersMap: Map [ type, diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 5a3e9821..4dea7f64 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -31,7 +31,7 @@ export enum WorkflowNodeType { End = "end", Apply = "apply", Upload = "upload", - Inspect = "inspect", + Monitor = "monitor", Deploy = "deploy", Notify = "notify", Branch = "branch", @@ -43,23 +43,25 @@ export enum WorkflowNodeType { } const workflowNodeTypeDefaultNames: Map = new Map([ - [WorkflowNodeType.Start, i18n.t("workflow_node.start.label")], - [WorkflowNodeType.End, i18n.t("workflow_node.end.label")], - [WorkflowNodeType.Apply, i18n.t("workflow_node.apply.label")], - [WorkflowNodeType.Upload, i18n.t("workflow_node.upload.label")], - [WorkflowNodeType.Inspect, i18n.t("workflow_node.inspect.label")], - [WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.label")], - [WorkflowNodeType.Notify, i18n.t("workflow_node.notify.label")], - [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")], - [WorkflowNodeType.Condition, i18n.t("workflow_node.condition.label")], - [WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.label")], - [WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.label")], - [WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.label")], - [WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")], + [WorkflowNodeType.Start, i18n.t("workflow_node.start.default_name")], + [WorkflowNodeType.End, i18n.t("workflow_node.end.default_name")], + [WorkflowNodeType.Apply, i18n.t("workflow_node.apply.default_name")], + [WorkflowNodeType.Upload, i18n.t("workflow_node.upload.default_name")], + [WorkflowNodeType.Monitor, i18n.t("workflow_node.monitor.default_name")], + [WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.default_name")], + [WorkflowNodeType.Notify, i18n.t("workflow_node.notify.default_name")], + [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.default_name")], + [WorkflowNodeType.Condition, i18n.t("workflow_node.condition.default_name")], + [WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.default_name")], + [WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.default_name")], + [WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.default_name")], + [WorkflowNodeType.Custom, i18n.t("workflow_node.custom.default_name")], ]); const workflowNodeTypeDefaultInputs: Map = new Map([ [WorkflowNodeType.Apply, []], + [WorkflowNodeType.Upload, []], + [WorkflowNodeType.Monitor, []], [ WorkflowNodeType.Deploy, [ @@ -98,7 +100,7 @@ const workflowNodeTypeDefaultOutputs: Map = ], ], [ - WorkflowNodeType.Inspect, + WorkflowNodeType.Monitor, [ { name: "certificate", @@ -158,11 +160,11 @@ export type WorkflowNodeConfigForUpload = { privateKey: string; }; -export type WorkflowNodeConfigForInspect = { - domain: string; - port: string; +export type WorkflowNodeConfigForMonitor = { host: string; - path: string; + port: number; + domain?: string; + requestPath?: string; }; export type WorkflowNodeConfigForDeploy = { @@ -351,7 +353,7 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {} case WorkflowNodeType.Apply: case WorkflowNodeType.Upload: case WorkflowNodeType.Deploy: - case WorkflowNodeType.Inspect: + case WorkflowNodeType.Monitor: { node.inputs = workflowNodeTypeDefaultInputs.get(nodeType); node.outputs = workflowNodeTypeDefaultOutputs.get(nodeType); diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index edd703f2..5b6c870c 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -10,6 +10,7 @@ "workflow_node.unsaved_changes.confirm": "You have unsaved changes. Do you really want to close the panel and drop those changes?", "workflow_node.start.label": "Start", + "workflow_node.start.default_name": "Start", "workflow_node.start.form.trigger.label": "Trigger", "workflow_node.start.form.trigger.placeholder": "Please select trigger", "workflow_node.start.form.trigger.tooltip": "Auto: Time triggered based on cron expression.
Manual: Manually triggered.", @@ -22,7 +23,8 @@ "workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:", "workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times. Don't always set it to midnight every day to avoid spikes in traffic.

Reference links:
1. Let’s Encrypt rate limits
2. Why should my Let’s Encrypt (ACME) client run at a random time?", - "workflow_node.apply.label": "Application", + "workflow_node.apply.label": "Obtain certificate", + "workflow_node.apply.default_name": "Application", "workflow_node.apply.form.domains.label": "Domains", "workflow_node.apply.form.domains.placeholder": "Please enter domains (separated by semicolons)", "workflow_node.apply.form.domains.tooltip": "Wildcard domain: *.example.com", @@ -97,7 +99,17 @@ "workflow_node.apply.form.skip_before_expiry_days.unit": "days", "workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the issued certificate, otherwise the certificate may never be renewed.", - "workflow_node.deploy.label": "Deployment", + "workflow_node.upload.label": "Upload certificate", + "workflow_node.upload.default_name": "Uploading", + "workflow_node.upload.form.domains.label": "Domains", + "workflow_node.upload.form.domains.placholder": "Please select certificate file", + "workflow_node.upload.form.certificate.label": "Certificate (PEM format)", + "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----", + "workflow_node.upload.form.private_key.label": "Private key (PEM format)", + "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", + + "workflow_node.deploy.label": "Deploy certificate", + "workflow_node.deploy.default_name": "Deployment", "workflow_node.deploy.form.provider.label": "Deploy target", "workflow_node.deploy.form.provider.placeholder": "Please select deploy target", "workflow_node.deploy.form.provider.search.placeholder": "Search deploy target ...", @@ -805,25 +817,20 @@ "workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "skip", "workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "not skip", - "workflow_node.upload.label": "Upload", - "workflow_node.upload.form.domains.label": "Domains", - "workflow_node.upload.form.domains.placholder": "Please select certificate file", - "workflow_node.upload.form.certificate.label": "Certificate (PEM format)", - "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----", - "workflow_node.upload.form.private_key.label": "Private key (PEM format)", - "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", + "workflow_node.monitor.label": "Monitor certificate", + "workflow_node.monitor.default_name": "Monitoring", + "workflow_node.monitor.form.guide": "Tips: Certimate will send a HEAD request to the target address to obtain the certificate. Please ensure that the address is accessible through HTTPS protocol.", + "workflow_node.monitor.form.host.label": "Host", + "workflow_node.monitor.form.host.placeholder": "Please enter host", + "workflow_node.monitor.form.port.label": "Port", + "workflow_node.monitor.form.port.placeholder": "Please enter port", + "workflow_node.monitor.form.domain.label": "Domain (Optional)", + "workflow_node.monitor.form.domain.placeholder": "Please enter domain name", + "workflow_node.monitor.form.request_path.label": "Request path (Optional)", + "workflow_node.monitor.form.request_path.placeholder": "Please enter request path", - "workflow_node.inspect.label": "Inspect certificate", - "workflow_node.inspect.form.domain.label": "Domain", - "workflow_node.inspect.form.domain.placeholder": "Please enter domain name", - "workflow_node.inspect.form.port.label": "Port", - "workflow_node.inspect.form.port.placeholder": "Please enter port", - "workflow_node.inspect.form.host.label": "Host", - "workflow_node.inspect.form.host.placeholder": "Please enter host", - "workflow_node.inspect.form.path.label": "Path", - "workflow_node.inspect.form.path.placeholder": "Please enter path", - - "workflow_node.notify.label": "Notification", + "workflow_node.notify.label": "Send notification", + "workflow_node.notify.default_name": "Notification", "workflow_node.notify.form.subject.label": "Subject", "workflow_node.notify.form.subject.placeholder": "Please enter subject", "workflow_node.notify.form.message.label": "Message", @@ -862,10 +869,13 @@ "workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string", "workflow_node.end.label": "End", + "workflow_node.end.default_name": "End", "workflow_node.branch.label": "Parallel branch", + "workflow_node.branch.default_name": "Parallel", "workflow_node.condition.label": "Branch", + "workflow_node.condition.default_name": "Branch", "workflow_node.condition.form.variable.placeholder": "Please select variable", "workflow_node.condition.form.variable.errmsg": "Please select variable", "workflow_node.condition.form.operator.errmsg": "Please select operator", @@ -888,8 +898,11 @@ "workflow_node.condition.form.comparison.is": "Is", "workflow_node.execute_result_branch.label": "Execution result branch", + "workflow_node.execute_result_branch.default_name": "Execution result branch", "workflow_node.execute_success.label": "If the previous node succeeded ...", + "workflow_node.execute_success.default_name": "If the previous node succeeded ...", - "workflow_node.execute_failure.label": "If the previous node failed ..." + "workflow_node.execute_failure.label": "If the previous node failed ...", + "workflow_node.execute_failure.default_name": "If the previous node failed ..." } diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index fb51668f..5f28a950 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -28,7 +28,7 @@ "access.form.name.placeholder": "请输入授权名称", "access.form.provider.label": "提供商", "access.form.provider.placeholder": "请选择提供商", - "access.form.provider.tooltip": "提供商分为两种类型:
【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。
【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。

该字段保存后不可修改。", + "access.form.provider.tooltip": "提供商分为两种类型:
【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理你的域名解析记录。
【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。

该字段保存后不可修改。", "access.form.provider.search.placeholder": "搜索提供商……", "access.form.certificate_authority.label": "证书颁发机构", "access.form.certificate_authority.placeholder": "请选择证书颁发机构", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index ef61e5a5..0d7ce68c 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -10,6 +10,7 @@ "workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?", "workflow_node.start.label": "开始", + "workflow_node.start.default_name": "开始", "workflow_node.start.form.trigger.label": "触发方式", "workflow_node.start.form.trigger.placeholder": "请选择触发方式", "workflow_node.start.form.trigger.tooltip": "自动触发:基于 Cron 表达式定时触发。
手动触发:手动点击执行触发。", @@ -22,7 +23,8 @@ "workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:", "workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。

参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?", - "workflow_node.apply.label": "申请证书", + "workflow_node.apply.label": "申请签发证书", + "workflow_node.apply.default_name": "申请", "workflow_node.apply.form.domains.label": "域名", "workflow_node.apply.form.domains.placeholder": "请输入域名(多个值请用半角分号隔开)", "workflow_node.apply.form.domains.tooltip": "泛域名表示形式为:*.example.com", @@ -96,7 +98,17 @@ "workflow_node.apply.form.skip_before_expiry_days.unit": "天", "workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。", - "workflow_node.deploy.label": "部署证书", + "workflow_node.upload.label": "上传自有证书", + "workflow_node.upload.default_name": "上传", + "workflow_node.upload.form.domains.label": "域名", + "workflow_node.upload.form.domains.placeholder": "上传证书文件后显示", + "workflow_node.upload.form.certificate.label": "证书文件(PEM 格式)", + "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----", + "workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)", + "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", + + "workflow_node.deploy.label": "部署证书到 ...", + "workflow_node.deploy.default_name": "部署", "workflow_node.deploy.form.provider.label": "部署目标", "workflow_node.deploy.form.provider.placeholder": "请选择部署目标", "workflow_node.deploy.form.provider.search.placeholder": "搜索部署目标……", @@ -804,25 +816,20 @@ "workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过", "workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过", - "workflow_node.upload.label": "上传证书", - "workflow_node.upload.form.domains.label": "域名", - "workflow_node.upload.form.domains.placeholder": "上传证书文件后显示", - "workflow_node.upload.form.certificate.label": "证书文件(PEM 格式)", - "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----", - "workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)", - "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", - - "workflow_node.inspect.label": "检查网站证书", - "workflow_node.inspect.form.domain.label": "域名", - "workflow_node.inspect.form.domain.placeholder": "请输入要检查的网站域名", - "workflow_node.inspect.form.port.label": "端口号", - "workflow_node.inspect.form.port.placeholder": "请输入要检查的端口号", - "workflow_node.inspect.form.host.label": "Host", - "workflow_node.inspect.form.host.placeholder": "请输入 Host", - "workflow_node.inspect.form.path.label": "Path", - "workflow_node.inspect.form.path.placeholder": "请输入 Path", + "workflow_node.monitor.label": "监控网站证书", + "workflow_node.monitor.default_name": "监控", + "workflow_node.monitor.form.guide": "小贴士:Certimate 将向目标地址发送一个 HEAD 请求来获取相应的域名证书,请确保该地址可通过 HTTPS 协议访问。", + "workflow_node.monitor.form.host.label": "主机地址", + "workflow_node.monitor.form.host.placeholder": "请输入主机地址(可以是域名或 IP)", + "workflow_node.monitor.form.port.label": "主机端口", + "workflow_node.monitor.form.port.placeholder": "请输入主机端口", + "workflow_node.monitor.form.domain.label": "域名(可选)", + "workflow_node.monitor.form.domain.placeholder": "请输入域名(仅当主机地址为 IP 时可选)", + "workflow_node.monitor.form.request_path.label": "请求路径(可选)", + "workflow_node.monitor.form.request_path.placeholder": "请输入请求路径", "workflow_node.notify.label": "推送通知", + "workflow_node.notify.default_name": "通知", "workflow_node.notify.form.subject.label": "通知主题", "workflow_node.notify.form.subject.placeholder": "请输入通知主题", "workflow_node.notify.form.message.label": "通知内容", @@ -861,10 +868,13 @@ "workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串", "workflow_node.end.label": "结束", + "workflow_node.end.default_name": "结束", "workflow_node.branch.label": "并行分支", + "workflow_node.branch.default_name": "并行", "workflow_node.condition.label": "分支", + "workflow_node.condition.default_name": "分支", "workflow_node.condition.form.variable.placeholder": "选择变量", "workflow_node.condition.form.variable.errmsg": "请选择变量", "workflow_node.condition.form.operator.errmsg": "请选择操作符", @@ -887,8 +897,11 @@ "workflow_node.condition.form.comparison.is": "为", "workflow_node.execute_result_branch.label": "执行结果分支", + "workflow_node.execute_result_branch.default_name": "执行结果分支", "workflow_node.execute_success.label": "若前序节点执行成功…", + "workflow_node.execute_success.default_name": "若前序节点执行成功…", - "workflow_node.execute_failure.label": "若前序节点执行失败…" + "workflow_node.execute_failure.label": "若前序节点执行失败…", + "workflow_node.execute_failure.default_name": "若前序节点执行失败…" } diff --git a/ui/src/pages/accesses/AccessList.tsx b/ui/src/pages/accesses/AccessList.tsx index f815812e..a99dd588 100644 --- a/ui/src/pages/accesses/AccessList.tsx +++ b/ui/src/pages/accesses/AccessList.tsx @@ -56,7 +56,7 @@ const AccessList = () => { render: (_, record) => { return ( - + {t(accessProvidersMap.get(record.provider)?.name ?? "")} ); From efdeacf01a9d751807b52f0f8a77209ebf6459fa Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 28 May 2025 21:39:02 +0800 Subject: [PATCH 20/28] feat: add preset webhook template for serverchan3 --- .../access/AccessFormWebhookConfig.tsx | 23 ++++++++++++++++--- ui/src/i18n/locales/en/nls.access.json | 3 ++- ui/src/i18n/locales/zh/nls.access.json | 3 ++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/ui/src/components/access/AccessFormWebhookConfig.tsx b/ui/src/components/access/AccessFormWebhookConfig.tsx index d79f848d..7ceec29a 100644 --- a/ui/src/components/access/AccessFormWebhookConfig.tsx +++ b/ui/src/components/access/AccessFormWebhookConfig.tsx @@ -224,7 +224,24 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa ); break; - case "serverchan": + case "serverchan3": + formInst.setFieldValue("url", "https://.push.ft07.com/send/.send"); + formInst.setFieldValue("method", "POST"); + formInst.setFieldValue("headers", "Content-Type: application/json"); + formInst.setFieldValue( + "defaultDataForNotification", + JSON.stringify( + { + title: "${SUBJECT}", + desp: "${MESSAGE}", + }, + null, + 2 + ) + ); + break; + + case "serverchanturbo": formInst.setFieldValue("url", "https://sctapi.ftqq.com/.send"); formInst.setFieldValue("method", "POST"); formInst.setFieldValue("headers", "Content-Type: application/json"); @@ -329,9 +346,9 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
({ + items: ["bark", "ntfy", "gotify", "pushover", "pushplus", "serverchan3", "serverchanturbo", "common"].map((key) => ({ key, - label: t(`access.form.webhook_preset_data.option.${key}.label`), + label: , onClick: () => handlePresetDataForNotificationClick(key), })), }} diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 1e570779..59bee417 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -465,7 +465,8 @@ "access.form.webhook_preset_data.option.ntfy.label": "ntfy", "access.form.webhook_preset_data.option.pushover.label": "Pushover", "access.form.webhook_preset_data.option.pushplus.label": "PushPlus", - "access.form.webhook_preset_data.option.serverchan.label": "ServerChan", + "access.form.webhook_preset_data.option.serverchan3.label": "ServerChan3", + "access.form.webhook_preset_data.option.serverchanturbo.label": "ServerChanTurbo", "access.form.webhook_preset_data.option.common.label": "General template", "access.form.wecombot_webhook_url.label": "WeCom bot Webhook URL", "access.form.wecombot_webhook_url.placeholder": "Please enter WeCom bot Webhook URL", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 7e5abf8e..3c94a882 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -465,7 +465,8 @@ "access.form.webhook_preset_data.option.ntfy.label": "ntfy", "access.form.webhook_preset_data.option.pushover.label": "Pushover", "access.form.webhook_preset_data.option.pushplus.label": "PushPlus 推送加", - "access.form.webhook_preset_data.option.serverchan.label": "Server 酱", + "access.form.webhook_preset_data.option.serverchan3.label": "Server 酱 3", + "access.form.webhook_preset_data.option.serverchanturbo.label": "Server酱 Turbo", "access.form.webhook_preset_data.option.common.label": "通用模板", "access.form.wecombot_webhook_url.label": "企业微信群机器人 Webhook 地址", "access.form.wecombot_webhook_url.placeholder": "请输入企业微信群机器人 Webhook 地址", From e73e2739c165f8b42d49b1c1affeb620bfc8d5fa Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 28 May 2025 22:21:41 +0800 Subject: [PATCH 21/28] feat: use discard handler as default providers logger --- .../core/deployer/providers/1panel-console/1panel_console.go | 2 +- internal/pkg/core/deployer/providers/1panel-site/1panel_site.go | 2 +- internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go | 2 +- .../pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go | 2 +- .../deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go | 2 +- internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go | 2 +- internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go | 2 +- internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go | 2 +- internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go | 2 +- internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go | 2 +- internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go | 2 +- internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go | 2 +- internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go | 2 +- internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go | 2 +- internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go | 2 +- internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go | 2 +- internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go | 2 +- internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go | 2 +- internal/pkg/core/deployer/providers/aws-acm/aws_acm.go | 2 +- .../core/deployer/providers/aws-cloudfront/aws_cloudfront.go | 2 +- .../core/deployer/providers/azure-keyvault/azure_keyvault.go | 2 +- .../deployer/providers/baiducloud-appblb/baiducloud_appblb.go | 2 +- .../core/deployer/providers/baiducloud-blb/baiducloud_blb.go | 2 +- .../core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go | 2 +- .../core/deployer/providers/baiducloud-cert/baiducloud_cert.go | 2 +- internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go | 2 +- .../deployer/providers/baotapanel-console/baotapanel_console.go | 2 +- .../core/deployer/providers/baotapanel-site/baotapanel_site.go | 2 +- .../deployer/providers/baotawaf-console/baotawaf_console.go | 2 +- .../pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go | 2 +- internal/pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go | 2 +- .../pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go | 2 +- internal/pkg/core/deployer/providers/cachefly/cachefly.go | 2 +- internal/pkg/core/deployer/providers/cdnfly/cdnfly.go | 2 +- .../pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go | 2 +- .../deployer/providers/edgio-applications/edgio_applications.go | 2 +- internal/pkg/core/deployer/providers/flexcdn/flexcdn.go | 2 +- internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go | 2 +- internal/pkg/core/deployer/providers/goedge/goedge.go | 2 +- .../core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go | 2 +- .../core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go | 2 +- .../core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go | 2 +- .../core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go | 2 +- internal/pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go | 2 +- internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go | 2 +- .../pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go | 2 +- internal/pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go | 2 +- internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go | 2 +- internal/pkg/core/deployer/providers/lecdn/lecdn.go | 2 +- internal/pkg/core/deployer/providers/local/local.go | 2 +- .../pkg/core/deployer/providers/netlify-site/netlify_site.go | 2 +- internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go | 2 +- internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go | 2 +- internal/pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go | 2 +- .../pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go | 2 +- .../deployer/providers/ratpanel-console/ratpanel_console.go | 2 +- .../pkg/core/deployer/providers/ratpanel-site/ratpanel_site.go | 2 +- internal/pkg/core/deployer/providers/safeline/safeline.go | 2 +- internal/pkg/core/deployer/providers/ssh/ssh.go | 2 +- .../deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go | 2 +- .../deployer/providers/tencentcloud-clb/tencentcloud_clb.go | 2 +- .../deployer/providers/tencentcloud-cos/tencentcloud_cos.go | 2 +- .../deployer/providers/tencentcloud-css/tencentcloud_css.go | 2 +- .../deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go | 2 +- .../core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go | 2 +- .../deployer/providers/tencentcloud-scf/tencentcloud_scf.go | 2 +- .../tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go | 2 +- .../deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go | 2 +- .../deployer/providers/tencentcloud-vod/tencentcloud_vod.go | 2 +- .../deployer/providers/tencentcloud-waf/tencentcloud_waf.go | 2 +- internal/pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go | 2 +- internal/pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go | 2 +- .../deployer/providers/unicloud-webhost/unicloud_webhost.go | 2 +- internal/pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go | 2 +- .../core/deployer/providers/volcengine-alb/volcengine_alb.go | 2 +- .../core/deployer/providers/volcengine-cdn/volcengine_cdn.go | 2 +- .../providers/volcengine-certcenter/volcengine_certcenter.go | 2 +- .../core/deployer/providers/volcengine-clb/volcengine_clb.go | 2 +- .../core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go | 2 +- .../deployer/providers/volcengine-imagex/volcengine_imagex.go | 2 +- .../core/deployer/providers/volcengine-live/volcengine_live.go | 2 +- .../core/deployer/providers/volcengine-tos/volcengine_tos.go | 2 +- internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go | 2 +- .../pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go | 2 +- .../deployer/providers/wangsu-certificate/wangsu_certificate.go | 2 +- internal/pkg/core/deployer/providers/webhook/webhook.go | 2 +- internal/pkg/core/notifier/providers/bark/bark.go | 2 +- internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go | 2 +- internal/pkg/core/notifier/providers/discordbot/discordbot.go | 2 +- internal/pkg/core/notifier/providers/email/email.go | 2 +- internal/pkg/core/notifier/providers/gotify/gotify.go | 2 +- internal/pkg/core/notifier/providers/larkbot/larkbot.go | 2 +- internal/pkg/core/notifier/providers/mattermost/mattermost.go | 2 +- internal/pkg/core/notifier/providers/pushover/pushover.go | 2 +- internal/pkg/core/notifier/providers/pushplus/pushplus.go | 2 +- internal/pkg/core/notifier/providers/serverchan/serverchan.go | 2 +- internal/pkg/core/notifier/providers/slackbot/slackbot.go | 2 +- internal/pkg/core/notifier/providers/telegrambot/telegrambot.go | 2 +- internal/pkg/core/notifier/providers/webhook/webhook.go | 2 +- internal/pkg/core/notifier/providers/wecombot/wecombot.go | 2 +- internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go | 2 +- internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go | 2 +- internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go | 2 +- internal/pkg/core/uploader/providers/aws-acm/aws_acm.go | 2 +- .../core/uploader/providers/azure-keyvault/azure_keyvault.go | 2 +- .../core/uploader/providers/baiducloud-cert/baiducloud_cert.go | 2 +- .../pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go | 2 +- internal/pkg/core/uploader/providers/dogecloud/dogecloud.go | 2 +- internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go | 2 +- .../core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go | 2 +- .../core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go | 2 +- .../core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go | 2 +- internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go | 2 +- .../pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go | 2 +- .../uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go | 2 +- .../uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go | 2 +- internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go | 2 +- internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go | 2 +- .../core/uploader/providers/volcengine-cdn/volcengine_cdn.go | 2 +- .../providers/volcengine-certcenter/volcengine_certcenter.go | 2 +- .../core/uploader/providers/volcengine-live/volcengine_live.go | 2 +- .../uploader/providers/wangsu-certificate/wangsu_certificate.go | 2 +- 122 files changed, 122 insertions(+), 122 deletions(-) diff --git a/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go b/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go index cdeb8af5..b1df4153 100644 --- a/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go +++ b/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go @@ -53,7 +53,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go b/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go index 07d124a3..0f721c3f 100644 --- a/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go +++ b/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go @@ -74,7 +74,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go b/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go index fec66c0e..0f22091a 100644 --- a/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go +++ b/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go @@ -81,7 +81,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go b/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go index f215e701..12f0f3d7 100644 --- a/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go +++ b/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go @@ -79,7 +79,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go b/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go index 5acdb50e..cfcdaa18 100644 --- a/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go +++ b/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go @@ -67,7 +67,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go b/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go index 73d2d77b..f1cc8811 100644 --- a/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go +++ b/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go @@ -53,7 +53,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go b/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go index 5fa6eedf..96dd211f 100644 --- a/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go +++ b/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go @@ -52,7 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go index ec35a190..1722e4fd 100644 --- a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go +++ b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go @@ -71,7 +71,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go b/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go index f27f4ab9..a5109163 100644 --- a/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go +++ b/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go @@ -52,7 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go index 83d5d602..f0bd3476 100644 --- a/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go +++ b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go @@ -64,7 +64,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go b/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go index 74d8344b..e4906fb5 100644 --- a/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go +++ b/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go @@ -64,7 +64,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go index d86998d0..1ff046c3 100644 --- a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go +++ b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go @@ -63,7 +63,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go b/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go index c7385863..6ea13077 100644 --- a/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go +++ b/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go @@ -70,7 +70,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go b/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go index 0481f3bf..0fab9485 100644 --- a/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go +++ b/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go @@ -54,7 +54,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go b/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go index e4e80db9..dd83f514 100644 --- a/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go +++ b/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go @@ -69,7 +69,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go b/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go index d810c0f9..6a698cf0 100644 --- a/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go +++ b/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go @@ -53,7 +53,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go b/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go index b340e0a3..ab02fa89 100644 --- a/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go +++ b/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go @@ -54,7 +54,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go b/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go index c8ec310a..cb3c70e9 100644 --- a/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go +++ b/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go @@ -69,7 +69,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go b/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go index a9e90b60..0c9c5d57 100644 --- a/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go +++ b/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go @@ -66,7 +66,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront.go b/internal/pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront.go index 0808a4fb..7ec17044 100644 --- a/internal/pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront.go +++ b/internal/pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront.go @@ -66,7 +66,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go b/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go index b8f8df99..1331bbf6 100644 --- a/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go +++ b/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go @@ -76,7 +76,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go index 3318135f..3bb965ca 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go +++ b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go @@ -74,7 +74,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go index a16ea102..0490b9ad 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go +++ b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go @@ -74,7 +74,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go b/internal/pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go index 7ef78fb1..ccd11f9b 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go +++ b/internal/pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go @@ -48,7 +48,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go b/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go index 200d34ec..f2295593 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go +++ b/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go @@ -47,7 +47,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go b/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go index e3efa6e4..b056b076 100644 --- a/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go +++ b/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go @@ -51,7 +51,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baotapanel-console/baotapanel_console.go b/internal/pkg/core/deployer/providers/baotapanel-console/baotapanel_console.go index 5709f82d..403b96e8 100644 --- a/internal/pkg/core/deployer/providers/baotapanel-console/baotapanel_console.go +++ b/internal/pkg/core/deployer/providers/baotapanel-console/baotapanel_console.go @@ -50,7 +50,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go b/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go index d6ee1533..78fc3e96 100644 --- a/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go +++ b/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go @@ -55,7 +55,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baotawaf-console/baotawaf_console.go b/internal/pkg/core/deployer/providers/baotawaf-console/baotawaf_console.go index 482ca8e4..dbdbf811 100644 --- a/internal/pkg/core/deployer/providers/baotawaf-console/baotawaf_console.go +++ b/internal/pkg/core/deployer/providers/baotawaf-console/baotawaf_console.go @@ -48,7 +48,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go index 945d5a48..24eabb41 100644 --- a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go +++ b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go @@ -54,7 +54,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go b/internal/pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go index e2bfd696..cdc39baa 100644 --- a/internal/pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go +++ b/internal/pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go @@ -41,7 +41,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go b/internal/pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go index e659c9a1..a11bbaf7 100644 --- a/internal/pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go +++ b/internal/pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go @@ -59,7 +59,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/cachefly/cachefly.go b/internal/pkg/core/deployer/providers/cachefly/cachefly.go index 21cb4dd0..fa1cce13 100644 --- a/internal/pkg/core/deployer/providers/cachefly/cachefly.go +++ b/internal/pkg/core/deployer/providers/cachefly/cachefly.go @@ -42,7 +42,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/cdnfly/cdnfly.go b/internal/pkg/core/deployer/providers/cdnfly/cdnfly.go index 1ced8caf..25fb6a54 100644 --- a/internal/pkg/core/deployer/providers/cdnfly/cdnfly.go +++ b/internal/pkg/core/deployer/providers/cdnfly/cdnfly.go @@ -60,7 +60,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go b/internal/pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go index efcf4b7c..9401285f 100644 --- a/internal/pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go +++ b/internal/pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go @@ -55,7 +55,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go index 195c202e..a4a60c98 100644 --- a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go +++ b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go @@ -48,7 +48,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go b/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go index 8b692e90..3c957071 100644 --- a/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go +++ b/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go @@ -61,7 +61,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go index 780f91a7..0d652df9 100644 --- a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go +++ b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go @@ -69,7 +69,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/goedge/goedge.go b/internal/pkg/core/deployer/providers/goedge/goedge.go index 25caeb01..0b7ff2b5 100644 --- a/internal/pkg/core/deployer/providers/goedge/goedge.go +++ b/internal/pkg/core/deployer/providers/goedge/goedge.go @@ -61,7 +61,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go index 3a8122ca..cbdff322 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go @@ -71,7 +71,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go index 92c62c9a..52cbcab5 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go @@ -83,7 +83,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go b/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go index c1afb5d8..0ba5816a 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go @@ -50,7 +50,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go b/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go index 04c1c30e..8afb2049 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go @@ -80,7 +80,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go b/internal/pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go index ca42126e..0f8a048d 100644 --- a/internal/pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go +++ b/internal/pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go @@ -76,7 +76,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go b/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go index 10ccf19d..7da0000b 100644 --- a/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go +++ b/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go @@ -60,7 +60,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go b/internal/pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go index 24e5bc7a..666ce101 100644 --- a/internal/pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go +++ b/internal/pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go @@ -48,7 +48,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go b/internal/pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go index 6f61625d..19e5e286 100644 --- a/internal/pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go +++ b/internal/pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go @@ -51,7 +51,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go b/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go index de2e62be..e51bfcd8 100644 --- a/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go +++ b/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go @@ -52,7 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/lecdn/lecdn.go b/internal/pkg/core/deployer/providers/lecdn/lecdn.go index c85f6558..4d9f4302 100644 --- a/internal/pkg/core/deployer/providers/lecdn/lecdn.go +++ b/internal/pkg/core/deployer/providers/lecdn/lecdn.go @@ -73,7 +73,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/local/local.go b/internal/pkg/core/deployer/providers/local/local.go index 8b05d95b..0b71da8a 100644 --- a/internal/pkg/core/deployer/providers/local/local.go +++ b/internal/pkg/core/deployer/providers/local/local.go @@ -67,7 +67,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go b/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go index 908b78c3..3b2072d7 100644 --- a/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go +++ b/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go @@ -45,7 +45,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go b/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go index 349c3a16..e4238ccf 100644 --- a/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go +++ b/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go @@ -57,7 +57,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go b/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go index 573eeb94..8491ecc3 100644 --- a/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go +++ b/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go @@ -57,7 +57,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go b/internal/pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go index db8d899e..ec6cfc4b 100644 --- a/internal/pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go +++ b/internal/pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go @@ -57,7 +57,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go index 0b003bee..99321f82 100644 --- a/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go +++ b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go @@ -58,7 +58,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/ratpanel-console/ratpanel_console.go b/internal/pkg/core/deployer/providers/ratpanel-console/ratpanel_console.go index 651ae0ac..85e7f530 100644 --- a/internal/pkg/core/deployer/providers/ratpanel-console/ratpanel_console.go +++ b/internal/pkg/core/deployer/providers/ratpanel-console/ratpanel_console.go @@ -50,7 +50,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/ratpanel-site/ratpanel_site.go b/internal/pkg/core/deployer/providers/ratpanel-site/ratpanel_site.go index 8d605b3d..7e30daf6 100644 --- a/internal/pkg/core/deployer/providers/ratpanel-site/ratpanel_site.go +++ b/internal/pkg/core/deployer/providers/ratpanel-site/ratpanel_site.go @@ -52,7 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/safeline/safeline.go b/internal/pkg/core/deployer/providers/safeline/safeline.go index f1b6b039..253a8754 100644 --- a/internal/pkg/core/deployer/providers/safeline/safeline.go +++ b/internal/pkg/core/deployer/providers/safeline/safeline.go @@ -53,7 +53,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/ssh/ssh.go b/internal/pkg/core/deployer/providers/ssh/ssh.go index 96447cfb..a52c355e 100644 --- a/internal/pkg/core/deployer/providers/ssh/ssh.go +++ b/internal/pkg/core/deployer/providers/ssh/ssh.go @@ -103,7 +103,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go b/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go index a92e4eb1..1df67032 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go @@ -70,7 +70,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go b/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go index 2f7c0f22..5455e236 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go @@ -79,7 +79,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go b/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go index 99ee9b2f..2aa6b2d0 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go @@ -66,7 +66,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go b/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go index 7de626d9..a9056719 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go @@ -60,7 +60,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go b/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go index 88840f4a..d1ba3ce4 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go @@ -69,7 +69,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go b/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go index 919339bb..138fb84a 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go @@ -69,7 +69,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf.go b/internal/pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf.go index bc8d8696..a0967f4e 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf.go @@ -62,7 +62,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go b/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go index 585000d9..5b4dd8d3 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go @@ -66,7 +66,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go b/internal/pkg/core/deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go index 5fbdb7c6..09ac14cd 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go @@ -47,7 +47,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod.go b/internal/pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod.go index 1b8553c5..b7c2a3ad 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod.go @@ -62,7 +62,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf.go b/internal/pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf.go index 18380289..1c8e7272 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf.go @@ -67,7 +67,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go b/internal/pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go index 5f90b943..532efb85 100644 --- a/internal/pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go +++ b/internal/pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go @@ -65,7 +65,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go b/internal/pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go index 5564e6a8..42a51cb4 100644 --- a/internal/pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go +++ b/internal/pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go @@ -67,7 +67,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost.go b/internal/pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost.go index e24708bd..82946bf1 100644 --- a/internal/pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost.go +++ b/internal/pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost.go @@ -52,7 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go b/internal/pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go index 4c9987a3..2fbe52b8 100644 --- a/internal/pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go +++ b/internal/pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go @@ -60,7 +60,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go b/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go index b17ae729..e4d76ab1 100644 --- a/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go +++ b/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go @@ -74,7 +74,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn.go b/internal/pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn.go index e9b2c325..e67e8885 100644 --- a/internal/pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn.go +++ b/internal/pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn.go @@ -59,7 +59,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go b/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go index 3989a000..8bb40d5b 100644 --- a/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go +++ b/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go @@ -50,7 +50,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go b/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go index 3b6a37bf..bc2dc9e0 100644 --- a/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go +++ b/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go @@ -70,7 +70,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go b/internal/pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go index 707ccde3..82021205 100644 --- a/internal/pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go +++ b/internal/pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go @@ -64,7 +64,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex.go b/internal/pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex.go index 2f419752..a7c974b4 100644 --- a/internal/pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex.go +++ b/internal/pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex.go @@ -65,7 +65,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-live/volcengine_live.go b/internal/pkg/core/deployer/providers/volcengine-live/volcengine_live.go index 46c0b9dc..3195d810 100644 --- a/internal/pkg/core/deployer/providers/volcengine-live/volcengine_live.go +++ b/internal/pkg/core/deployer/providers/volcengine-live/volcengine_live.go @@ -60,7 +60,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-tos/volcengine_tos.go b/internal/pkg/core/deployer/providers/volcengine-tos/volcengine_tos.go index 365d95f2..674106e1 100644 --- a/internal/pkg/core/deployer/providers/volcengine-tos/volcengine_tos.go +++ b/internal/pkg/core/deployer/providers/volcengine-tos/volcengine_tos.go @@ -64,7 +64,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go b/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go index 43c65de2..f889b996 100644 --- a/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go +++ b/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go @@ -59,7 +59,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go index 4d5f2e10..0780f80d 100644 --- a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go +++ b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go @@ -68,7 +68,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go b/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go index 3f691489..51fa7076 100644 --- a/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go +++ b/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go @@ -61,7 +61,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/webhook/webhook.go b/internal/pkg/core/deployer/providers/webhook/webhook.go index b25c129a..7ad6d6b0 100644 --- a/internal/pkg/core/deployer/providers/webhook/webhook.go +++ b/internal/pkg/core/deployer/providers/webhook/webhook.go @@ -61,7 +61,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/notifier/providers/bark/bark.go b/internal/pkg/core/notifier/providers/bark/bark.go index fb3298ec..f82fd180 100644 --- a/internal/pkg/core/notifier/providers/bark/bark.go +++ b/internal/pkg/core/notifier/providers/bark/bark.go @@ -42,7 +42,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go b/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go index d6d8b096..d13380a6 100644 --- a/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go +++ b/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go @@ -38,7 +38,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/discordbot/discordbot.go b/internal/pkg/core/notifier/providers/discordbot/discordbot.go index 20e7d304..e4b15aae 100644 --- a/internal/pkg/core/notifier/providers/discordbot/discordbot.go +++ b/internal/pkg/core/notifier/providers/discordbot/discordbot.go @@ -41,7 +41,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/email/email.go b/internal/pkg/core/notifier/providers/email/email.go index 69d39012..b2802c7f 100644 --- a/internal/pkg/core/notifier/providers/email/email.go +++ b/internal/pkg/core/notifier/providers/email/email.go @@ -50,7 +50,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/gotify/gotify.go b/internal/pkg/core/notifier/providers/gotify/gotify.go index aa7d36a0..0e96e9f7 100644 --- a/internal/pkg/core/notifier/providers/gotify/gotify.go +++ b/internal/pkg/core/notifier/providers/gotify/gotify.go @@ -44,7 +44,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/larkbot/larkbot.go b/internal/pkg/core/notifier/providers/larkbot/larkbot.go index 7d3e8a55..01bfeb1c 100644 --- a/internal/pkg/core/notifier/providers/larkbot/larkbot.go +++ b/internal/pkg/core/notifier/providers/larkbot/larkbot.go @@ -35,7 +35,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/mattermost/mattermost.go b/internal/pkg/core/notifier/providers/mattermost/mattermost.go index 70c6effe..b725b961 100644 --- a/internal/pkg/core/notifier/providers/mattermost/mattermost.go +++ b/internal/pkg/core/notifier/providers/mattermost/mattermost.go @@ -46,7 +46,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/pushover/pushover.go b/internal/pkg/core/notifier/providers/pushover/pushover.go index 48238608..7367688b 100644 --- a/internal/pkg/core/notifier/providers/pushover/pushover.go +++ b/internal/pkg/core/notifier/providers/pushover/pushover.go @@ -41,7 +41,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/pushplus/pushplus.go b/internal/pkg/core/notifier/providers/pushplus/pushplus.go index 025e1620..a2e3de0e 100644 --- a/internal/pkg/core/notifier/providers/pushplus/pushplus.go +++ b/internal/pkg/core/notifier/providers/pushplus/pushplus.go @@ -40,7 +40,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/serverchan/serverchan.go b/internal/pkg/core/notifier/providers/serverchan/serverchan.go index 0eb9bc24..5ee42785 100644 --- a/internal/pkg/core/notifier/providers/serverchan/serverchan.go +++ b/internal/pkg/core/notifier/providers/serverchan/serverchan.go @@ -39,7 +39,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/slackbot/slackbot.go b/internal/pkg/core/notifier/providers/slackbot/slackbot.go index a453f8f1..221235fc 100644 --- a/internal/pkg/core/notifier/providers/slackbot/slackbot.go +++ b/internal/pkg/core/notifier/providers/slackbot/slackbot.go @@ -41,7 +41,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go b/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go index ef99c66b..31463a0c 100644 --- a/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go +++ b/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go @@ -41,7 +41,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/webhook/webhook.go b/internal/pkg/core/notifier/providers/webhook/webhook.go index acc0caab..507cc812 100644 --- a/internal/pkg/core/notifier/providers/webhook/webhook.go +++ b/internal/pkg/core/notifier/providers/webhook/webhook.go @@ -60,7 +60,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/notifier/providers/wecombot/wecombot.go b/internal/pkg/core/notifier/providers/wecombot/wecombot.go index d6f86ef5..c6ad2daa 100644 --- a/internal/pkg/core/notifier/providers/wecombot/wecombot.go +++ b/internal/pkg/core/notifier/providers/wecombot/wecombot.go @@ -39,7 +39,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } diff --git a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go index ca3c7303..bc36e565 100644 --- a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go +++ b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go @@ -52,7 +52,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go b/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go index f5f490a3..ecf4c8cf 100644 --- a/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go +++ b/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go @@ -54,7 +54,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go b/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go index 933bc51e..b331df05 100644 --- a/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go +++ b/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go @@ -57,7 +57,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go b/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go index 05cc70e3..32b0a2e4 100644 --- a/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go +++ b/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go @@ -51,7 +51,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go b/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go index 5ac68d69..3d0ec5b5 100644 --- a/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go +++ b/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go @@ -58,7 +58,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go index 727aa03f..c7ae8304 100644 --- a/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go +++ b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go @@ -46,7 +46,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go b/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go index 1235893c..e54b01c6 100644 --- a/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go +++ b/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go @@ -49,7 +49,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go b/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go index ca98fc90..aa76f621 100644 --- a/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go +++ b/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go @@ -44,7 +44,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go b/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go index 5987136e..276baff8 100644 --- a/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go +++ b/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go @@ -46,7 +46,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go index b205e97e..5858a9c0 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go @@ -59,7 +59,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go index 9f47442e..acba2b65 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go @@ -54,7 +54,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go b/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go index a1cbe1df..809ccddd 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go @@ -59,7 +59,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go b/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go index b26755a6..91ea632e 100644 --- a/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go +++ b/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go @@ -52,7 +52,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go index 8dc2fefe..07343371 100644 --- a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go +++ b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go @@ -48,7 +48,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go index cb493110..4ea70ba0 100644 --- a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go +++ b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go @@ -44,7 +44,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go b/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go index 59067de4..5a96a951 100644 --- a/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go +++ b/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go @@ -46,7 +46,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go index 90eb1683..90b40754 100644 --- a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go +++ b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go @@ -56,7 +56,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go b/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go index 7a8bd3a0..1669e56b 100644 --- a/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go +++ b/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go @@ -44,7 +44,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go b/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go index b529e84a..59fca8f9 100644 --- a/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go +++ b/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go @@ -50,7 +50,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go b/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go index 99511ebf..fd5ce670 100644 --- a/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go +++ b/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go @@ -49,7 +49,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go b/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go index de5ec27d..11948f04 100644 --- a/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go +++ b/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go @@ -47,7 +47,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } diff --git a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go index b512be09..e401810a 100644 --- a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go +++ b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go @@ -50,7 +50,7 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } From f0af36b59eb8a6595a8c660f9af7ee00323aa014 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 28 May 2025 22:43:18 +0800 Subject: [PATCH 22/28] refactor: clean code --- internal/pkg/core/deployer/deployer.go | 2 +- internal/pkg/core/notifier/notifier.go | 2 +- internal/pkg/core/notifier/providers/bark/bark.go | 2 +- .../notifier/providers/dingtalkbot/dingtalkbot.go | 2 +- .../notifier/providers/discordbot/discordbot.go | 2 +- .../pkg/core/notifier/providers/email/email.go | 8 ++++---- .../pkg/core/notifier/providers/gotify/gotify.go | 2 +- .../pkg/core/notifier/providers/larkbot/larkbot.go | 2 +- .../notifier/providers/mattermost/mattermost.go | 2 +- .../core/notifier/providers/pushover/pushover.go | 2 +- .../core/notifier/providers/pushplus/pushplus.go | 2 +- .../notifier/providers/serverchan/serverchan.go | 2 +- .../core/notifier/providers/slackbot/slackbot.go | 2 +- .../notifier/providers/telegrambot/telegrambot.go | 2 +- .../pkg/core/notifier/providers/webhook/webhook.go | 2 +- .../core/notifier/providers/wecombot/wecombot.go | 2 +- .../uploader/providers/1panel-ssl/1panel_ssl.go | 4 ++-- .../uploader/providers/aliyun-cas/aliyun_cas.go | 2 +- .../uploader/providers/aliyun-slb/aliyun_slb.go | 2 +- .../pkg/core/uploader/providers/aws-acm/aws_acm.go | 2 +- .../providers/azure-keyvault/azure_keyvault.go | 2 +- .../providers/baiducloud-cert/baiducloud_cert.go | 2 +- .../providers/byteplus-cdn/byteplus_cdn.go | 2 +- .../core/uploader/providers/dogecloud/dogecloud.go | 2 +- .../core/uploader/providers/gcore-cdn/gcore_cdn.go | 2 +- .../providers/huaweicloud-elb/huaweicloud_elb.go | 2 +- .../providers/huaweicloud-scm/huaweicloud_scm.go | 2 +- .../providers/huaweicloud-waf/huaweicloud_waf.go | 2 +- .../uploader/providers/jdcloud-ssl/jdcloud_ssl.go | 2 +- .../providers/qiniu-sslcert/qiniu_sslcert.go | 2 +- .../rainyun-sslcenter/rainyun_sslcenter.go | 4 ++-- .../providers/tencentcloud-ssl/tencentcloud_ssl.go | 2 +- .../uploader/providers/ucloud-ussl/ucloud_ussl.go | 4 ++-- .../core/uploader/providers/upyun-ssl/upyun_ssl.go | 2 +- .../providers/volcengine-cdn/volcengine_cdn.go | 2 +- .../volcengine-certcenter/volcengine_certcenter.go | 2 +- .../providers/volcengine-live/volcengine_live.go | 2 +- .../wangsu-certificate/wangsu_certificate.go | 2 +- internal/pkg/core/uploader/uploader.go | 2 +- internal/pkg/sdk3rd/qiniu/auth.go | 4 ++-- internal/pkg/utils/cert/converter.go | 10 +++++----- internal/pkg/utils/cert/extractor.go | 6 +++--- internal/pkg/utils/cert/parser.go | 14 +++++++------- 43 files changed, 62 insertions(+), 62 deletions(-) diff --git a/internal/pkg/core/deployer/deployer.go b/internal/pkg/core/deployer/deployer.go index 67ce7ef7..85a4e156 100644 --- a/internal/pkg/core/deployer/deployer.go +++ b/internal/pkg/core/deployer/deployer.go @@ -20,7 +20,7 @@ type Deployer interface { // 出参: // - res:部署结果。 // - err: 错误。 - Deploy(ctx context.Context, certPEM string, privkeyPEM string) (res *DeployResult, err error) + Deploy(ctx context.Context, certPEM string, privkeyPEM string) (_res *DeployResult, _err error) } // 表示证书部署结果的数据结构。 diff --git a/internal/pkg/core/notifier/notifier.go b/internal/pkg/core/notifier/notifier.go index 876b5d48..f04084aa 100644 --- a/internal/pkg/core/notifier/notifier.go +++ b/internal/pkg/core/notifier/notifier.go @@ -19,7 +19,7 @@ type Notifier interface { // 出参: // - res:发送结果。 // - err: 错误。 - Notify(ctx context.Context, subject string, message string) (res *NotifyResult, err error) + Notify(ctx context.Context, subject string, message string) (_res *NotifyResult, _err error) } // 表示通知发送结果的数据结构。 diff --git a/internal/pkg/core/notifier/providers/bark/bark.go b/internal/pkg/core/notifier/providers/bark/bark.go index f82fd180..805a72b0 100644 --- a/internal/pkg/core/notifier/providers/bark/bark.go +++ b/internal/pkg/core/notifier/providers/bark/bark.go @@ -49,7 +49,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { const defaultServerURL = "https://api.day.app/" serverUrl := defaultServerURL if n.config.ServerUrl != "" { diff --git a/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go b/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go index d13380a6..81358ef4 100644 --- a/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go +++ b/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go @@ -45,7 +45,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { webhookUrl, err := url.Parse(n.config.WebhookUrl) if err != nil { return nil, fmt.Errorf("dingtalk api error: invalid webhook url: %w", err) diff --git a/internal/pkg/core/notifier/providers/discordbot/discordbot.go b/internal/pkg/core/notifier/providers/discordbot/discordbot.go index e4b15aae..704e7c79 100644 --- a/internal/pkg/core/notifier/providers/discordbot/discordbot.go +++ b/internal/pkg/core/notifier/providers/discordbot/discordbot.go @@ -48,7 +48,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://discord.com/developers/docs/resources/message#create-message req := n.httpClient.R(). SetContext(ctx). diff --git a/internal/pkg/core/notifier/providers/email/email.go b/internal/pkg/core/notifier/providers/email/email.go index b2802c7f..c8405554 100644 --- a/internal/pkg/core/notifier/providers/email/email.go +++ b/internal/pkg/core/notifier/providers/email/email.go @@ -57,7 +57,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { var smtpAuth smtp.Auth if n.config.Username != "" || n.config.Password != "" { smtpAuth = smtp.PlainAuth("", n.config.Username, n.config.Password, n.config.SmtpHost) @@ -76,10 +76,11 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s var yak *mailyak.MailYak if n.config.SmtpTls { - yak, err = mailyak.NewWithTLS(smtpAddr, smtpAuth, newTlsConfig()) + yakWithTls, err := mailyak.NewWithTLS(smtpAddr, smtpAuth, newTlsConfig()) if err != nil { return nil, err } + yak = yakWithTls } else { yak = mailyak.New(smtpAddr, smtpAuth) } @@ -89,8 +90,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s yak.Subject(subject) yak.Plain().Set(message) - err = yak.Send() - if err != nil { + if err := yak.Send(); err != nil { return nil, err } diff --git a/internal/pkg/core/notifier/providers/gotify/gotify.go b/internal/pkg/core/notifier/providers/gotify/gotify.go index 0e96e9f7..75d8737b 100644 --- a/internal/pkg/core/notifier/providers/gotify/gotify.go +++ b/internal/pkg/core/notifier/providers/gotify/gotify.go @@ -51,7 +51,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { serverUrl := strings.TrimRight(n.config.ServerUrl, "/") // REF: https://gotify.net/api-docs#/message/createMessage diff --git a/internal/pkg/core/notifier/providers/larkbot/larkbot.go b/internal/pkg/core/notifier/providers/larkbot/larkbot.go index 01bfeb1c..8c5022c0 100644 --- a/internal/pkg/core/notifier/providers/larkbot/larkbot.go +++ b/internal/pkg/core/notifier/providers/larkbot/larkbot.go @@ -42,7 +42,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { bot := lark.NewNotificationBot(n.config.WebhookUrl) content := lark.NewPostBuilder(). Title(subject). diff --git a/internal/pkg/core/notifier/providers/mattermost/mattermost.go b/internal/pkg/core/notifier/providers/mattermost/mattermost.go index b725b961..de72d192 100644 --- a/internal/pkg/core/notifier/providers/mattermost/mattermost.go +++ b/internal/pkg/core/notifier/providers/mattermost/mattermost.go @@ -53,7 +53,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { serverUrl := strings.TrimRight(n.config.ServerUrl, "/") // REF: https://developers.mattermost.com/api-documentation/#/operations/Login diff --git a/internal/pkg/core/notifier/providers/pushover/pushover.go b/internal/pkg/core/notifier/providers/pushover/pushover.go index 7367688b..aedf8d3a 100644 --- a/internal/pkg/core/notifier/providers/pushover/pushover.go +++ b/internal/pkg/core/notifier/providers/pushover/pushover.go @@ -48,7 +48,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://pushover.net/api req := n.httpClient.R(). SetContext(ctx). diff --git a/internal/pkg/core/notifier/providers/pushplus/pushplus.go b/internal/pkg/core/notifier/providers/pushplus/pushplus.go index a2e3de0e..9f565ce5 100644 --- a/internal/pkg/core/notifier/providers/pushplus/pushplus.go +++ b/internal/pkg/core/notifier/providers/pushplus/pushplus.go @@ -47,7 +47,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://pushplus.plus/doc/guide/api.html#%E4%B8%80%E3%80%81%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF%E6%8E%A5%E5%8F%A3 req := n.httpClient.R(). SetContext(ctx). diff --git a/internal/pkg/core/notifier/providers/serverchan/serverchan.go b/internal/pkg/core/notifier/providers/serverchan/serverchan.go index 5ee42785..ea6adf2b 100644 --- a/internal/pkg/core/notifier/providers/serverchan/serverchan.go +++ b/internal/pkg/core/notifier/providers/serverchan/serverchan.go @@ -46,7 +46,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://sct.ftqq.com/ req := n.httpClient.R(). SetContext(ctx). diff --git a/internal/pkg/core/notifier/providers/slackbot/slackbot.go b/internal/pkg/core/notifier/providers/slackbot/slackbot.go index 221235fc..92db106c 100644 --- a/internal/pkg/core/notifier/providers/slackbot/slackbot.go +++ b/internal/pkg/core/notifier/providers/slackbot/slackbot.go @@ -48,7 +48,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://docs.slack.dev/messaging/sending-and-scheduling-messages#publishing req := n.httpClient.R(). SetContext(ctx). diff --git a/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go b/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go index 31463a0c..80d03a21 100644 --- a/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go +++ b/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go @@ -48,7 +48,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://core.telegram.org/bots/api#sendmessage req := n.httpClient.R(). SetContext(ctx). diff --git a/internal/pkg/core/notifier/providers/webhook/webhook.go b/internal/pkg/core/notifier/providers/webhook/webhook.go index 507cc812..523f7b4d 100644 --- a/internal/pkg/core/notifier/providers/webhook/webhook.go +++ b/internal/pkg/core/notifier/providers/webhook/webhook.go @@ -67,7 +67,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // 处理 Webhook URL webhookUrl, err := url.Parse(n.config.WebhookUrl) if err != nil { diff --git a/internal/pkg/core/notifier/providers/wecombot/wecombot.go b/internal/pkg/core/notifier/providers/wecombot/wecombot.go index c6ad2daa..93b03c4d 100644 --- a/internal/pkg/core/notifier/providers/wecombot/wecombot.go +++ b/internal/pkg/core/notifier/providers/wecombot/wecombot.go @@ -46,7 +46,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://developer.work.weixin.qq.com/document/path/91770 req := n.httpClient.R(). SetContext(ctx). diff --git a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go index bc36e565..7391129d 100644 --- a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go +++ b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go @@ -59,7 +59,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 遍历证书列表,避免重复上传 if res, err := u.getCertIfExists(ctx, certPEM, privkeyPEM); err != nil { return nil, err @@ -94,7 +94,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } } -func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { searchWebsiteSSLPageNumber := int32(1) searchWebsiteSSLPageSize := int32(100) for { diff --git a/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go b/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go index ecf4c8cf..ea0968eb 100644 --- a/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go +++ b/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go @@ -61,7 +61,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go b/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go index b331df05..dac9c7bd 100644 --- a/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go +++ b/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go @@ -64,7 +64,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go b/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go index 32b0a2e4..f68ebadc 100644 --- a/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go +++ b/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go @@ -58,7 +58,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go b/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go index 3d0ec5b5..eb67fd2f 100644 --- a/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go +++ b/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go @@ -65,7 +65,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go index c7ae8304..b0fca821 100644 --- a/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go +++ b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go @@ -53,7 +53,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go b/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go index e54b01c6..a654db31 100644 --- a/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go +++ b/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go @@ -56,7 +56,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go b/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go index aa76f621..1b0b963f 100644 --- a/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go +++ b/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go @@ -51,7 +51,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 生成新证书名(需符合多吉云命名规则) var certId, certName string certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) diff --git a/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go b/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go index 276baff8..f3127602 100644 --- a/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go +++ b/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go @@ -53,7 +53,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 生成新证书名(需符合 Gcore 命名规则) var certId, certName string certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) diff --git a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go index 5858a9c0..d429c259 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go @@ -66,7 +66,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go index acba2b65..4e35562e 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go @@ -61,7 +61,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go b/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go index 809ccddd..789876ba 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go @@ -66,7 +66,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go b/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go index 91ea632e..44ed7f29 100644 --- a/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go +++ b/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go @@ -59,7 +59,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go index 07343371..99a1a0b5 100644 --- a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go +++ b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go @@ -55,7 +55,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go index 4ea70ba0..613fc7a9 100644 --- a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go +++ b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go @@ -51,7 +51,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { if res, err := u.getCertIfExists(ctx, certPEM); err != nil { return nil, err } else if res != nil { @@ -80,7 +80,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } } -func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go b/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go index 5a96a951..db4e92f4 100644 --- a/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go +++ b/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go @@ -53,7 +53,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 上传新证书 // REF: https://cloud.tencent.com/document/product/400/41665 uploadCertificateReq := tcssl.NewUploadCertificateRequest() diff --git a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go index 90b40754..acfbb214 100644 --- a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go +++ b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go @@ -63,7 +63,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 生成新证书名(需符合优刻得命名规则) var certId, certName string certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) @@ -111,7 +111,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE }, nil } -func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go b/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go index 1669e56b..6b45e130 100644 --- a/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go +++ b/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go @@ -51,7 +51,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 上传证书 uploadHttpsCertificateReq := &upyunsdk.UploadHttpsCertificateRequest{ Certificate: certPEM, diff --git a/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go b/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go index 59fca8f9..00ac07ae 100644 --- a/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go +++ b/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go @@ -57,7 +57,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go b/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go index fd5ce670..9accc17d 100644 --- a/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go +++ b/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go @@ -56,7 +56,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 上传证书 // REF: https://www.volcengine.com/docs/6638/1365580 importCertificateReq := &veccsdk.ImportCertificateInput{ diff --git a/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go b/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go index 11948f04..d758fbb4 100644 --- a/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go +++ b/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go @@ -54,7 +54,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go index e401810a..59eb4ca2 100644 --- a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go +++ b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go @@ -57,7 +57,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/uploader.go b/internal/pkg/core/uploader/uploader.go index 34d2813a..0a1681a7 100644 --- a/internal/pkg/core/uploader/uploader.go +++ b/internal/pkg/core/uploader/uploader.go @@ -21,7 +21,7 @@ type Uploader interface { // 出参: // - res:上传结果。 // - err: 错误。 - Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *UploadResult, err error) + Upload(ctx context.Context, certPEM string, privkeyPEM string) (_res *UploadResult, _err error) } // 表示证书上传结果的数据结构,包含上传后的证书 ID、名称和其他数据。 diff --git a/internal/pkg/sdk3rd/qiniu/auth.go b/internal/pkg/sdk3rd/qiniu/auth.go index 7f053f56..6df13752 100644 --- a/internal/pkg/sdk3rd/qiniu/auth.go +++ b/internal/pkg/sdk3rd/qiniu/auth.go @@ -18,10 +18,10 @@ func newTransport(mac *auth.Credentials, tr http.RoundTripper) *transport { return &transport{tr, mac} } -func (t *transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { +func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { token, err := t.mac.SignRequestV2(req) if err != nil { - return + return nil, err } req.Header.Set("Authorization", "Qiniu "+token) diff --git a/internal/pkg/utils/cert/converter.go b/internal/pkg/utils/cert/converter.go index b726e86d..0d7e4c53 100644 --- a/internal/pkg/utils/cert/converter.go +++ b/internal/pkg/utils/cert/converter.go @@ -16,7 +16,7 @@ import ( // 出参: // - certPEM: 证书 PEM 内容。 // - err: 错误。 -func ConvertCertificateToPEM(cert *x509.Certificate) (certPEM string, err error) { +func ConvertCertificateToPEM(cert *x509.Certificate) (_certPEM string, _err error) { if cert == nil { return "", errors.New("`cert` is nil") } @@ -37,14 +37,14 @@ func ConvertCertificateToPEM(cert *x509.Certificate) (certPEM string, err error) // 出参: // - privkeyPEM: 私钥 PEM 内容。 // - err: 错误。 -func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (privkeyPEM string, err error) { +func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (_privkeyPEM string, _err error) { if privkey == nil { return "", errors.New("`privkey` is nil") } - data, err := x509.MarshalECPrivateKey(privkey) - if err != nil { - return "", fmt.Errorf("failed to marshal EC private key: %w", err) + data, _err := x509.MarshalECPrivateKey(privkey) + if _err != nil { + return "", fmt.Errorf("failed to marshal EC private key: %w", _err) } block := &pem.Block{ diff --git a/internal/pkg/utils/cert/extractor.go b/internal/pkg/utils/cert/extractor.go index 94d0a8da..a4077d37 100644 --- a/internal/pkg/utils/cert/extractor.go +++ b/internal/pkg/utils/cert/extractor.go @@ -14,7 +14,7 @@ import ( // - serverCertPEM: 服务器证书的 PEM 内容。 // - intermediaCertPEM: 中间证书的 PEM 内容。 // - err: 错误。 -func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, intermediaCertPEM string, err error) { +func ExtractCertificatesFromPEM(certPEM string) (_serverCertPEM string, _intermediaCertPEM string, _err error) { pemBlocks := make([]*pem.Block, 0) pemData := []byte(certPEM) for { @@ -27,8 +27,8 @@ func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, intermedi pemData = rest } - serverCertPEM = "" - intermediaCertPEM = "" + serverCertPEM := "" + intermediaCertPEM := "" if len(pemBlocks) == 0 { return "", "", errors.New("failed to decode PEM block") diff --git a/internal/pkg/utils/cert/parser.go b/internal/pkg/utils/cert/parser.go index eb743f78..3ecb8639 100644 --- a/internal/pkg/utils/cert/parser.go +++ b/internal/pkg/utils/cert/parser.go @@ -21,7 +21,7 @@ import ( // 出参: // - cert: x509.Certificate 对象。 // - err: 错误。 -func ParseCertificateFromPEM(certPEM string) (cert *x509.Certificate, err error) { +func ParseCertificateFromPEM(certPEM string) (_cert *x509.Certificate, _err error) { pemData := []byte(certPEM) block, _ := pem.Decode(pemData) @@ -29,7 +29,7 @@ func ParseCertificateFromPEM(certPEM string) (cert *x509.Certificate, err error) return nil, errors.New("failed to decode PEM block") } - cert, err = x509.ParseCertificate(block.Bytes) + cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse certificate: %w", err) } @@ -45,7 +45,7 @@ func ParseCertificateFromPEM(certPEM string) (cert *x509.Certificate, err error) // 出参: // - privkey: crypto.PrivateKey 对象,可能是 rsa.PrivateKey、ecdsa.PrivateKey 或 ed25519.PrivateKey。 // - err: 错误。 -func ParsePrivateKeyFromPEM(privkeyPEM string) (privkey crypto.PrivateKey, err error) { +func ParsePrivateKeyFromPEM(privkeyPEM string) (_privkey crypto.PrivateKey, _err error) { pemData := []byte(privkeyPEM) return certcrypto.ParsePEMPrivateKey(pemData) } @@ -58,7 +58,7 @@ func ParsePrivateKeyFromPEM(privkeyPEM string) (privkey crypto.PrivateKey, err e // 出参: // - privkey: ecdsa.PrivateKey 对象。 // - err: 错误。 -func ParseECPrivateKeyFromPEM(privkeyPEM string) (privkey *ecdsa.PrivateKey, err error) { +func ParseECPrivateKeyFromPEM(privkeyPEM string) (_privkey *ecdsa.PrivateKey, _err error) { pemData := []byte(privkeyPEM) block, _ := pem.Decode(pemData) @@ -66,7 +66,7 @@ func ParseECPrivateKeyFromPEM(privkeyPEM string) (privkey *ecdsa.PrivateKey, err return nil, errors.New("failed to decode PEM block") } - privkey, err = x509.ParseECPrivateKey(block.Bytes) + privkey, err := x509.ParseECPrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse private key: %w", err) } @@ -82,7 +82,7 @@ func ParseECPrivateKeyFromPEM(privkeyPEM string) (privkey *ecdsa.PrivateKey, err // 出参: // - privkey: rsa.PrivateKey 对象。 // - err: 错误。 -func ParsePKCS1PrivateKeyFromPEM(privkeyPEM string) (privkey *rsa.PrivateKey, err error) { +func ParsePKCS1PrivateKeyFromPEM(privkeyPEM string) (_privkey *rsa.PrivateKey, _err error) { pemData := []byte(privkeyPEM) block, _ := pem.Decode(pemData) @@ -90,7 +90,7 @@ func ParsePKCS1PrivateKeyFromPEM(privkeyPEM string) (privkey *rsa.PrivateKey, er return nil, errors.New("failed to decode PEM block") } - privkey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + privkey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse private key: %w", err) } From 599cf17c9e1f3f03bc60dcd6d463ae82a28c5a94 Mon Sep 17 00:00:00 2001 From: tailor Date: Thu, 29 May 2025 11:13:44 +0800 Subject: [PATCH 23/28] fix: wangsu get certificate list api error --- internal/pkg/sdk3rd/wangsu/certificate/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/sdk3rd/wangsu/certificate/api.go b/internal/pkg/sdk3rd/wangsu/certificate/api.go index 037fb6e7..22172d4e 100644 --- a/internal/pkg/sdk3rd/wangsu/certificate/api.go +++ b/internal/pkg/sdk3rd/wangsu/certificate/api.go @@ -8,7 +8,7 @@ import ( func (c *Client) ListCertificates() (*ListCertificatesResponse, error) { resp := &ListCertificatesResponse{} - _, err := c.client.SendRequestWithResult(http.MethodGet, "/api/certificate", nil, resp) + _, err := c.client.SendRequestWithResult(http.MethodGet, "/api/ssl/certificate", nil, resp) if err != nil { return resp, err } From 28811c46d821a4c17b94e5cb67f3ee2681a02210 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 31 May 2025 16:29:17 +0800 Subject: [PATCH 24/28] fix: #746 --- .../core/deployer/providers/baotawaf-site/baotawaf_site.go | 2 +- internal/pkg/sdk3rd/btwaf/models.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go index 435f7a69..20572ca5 100644 --- a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go +++ b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go @@ -116,7 +116,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE SiteId: siteId, Type: typeutil.ToPtr("openCert"), Server: &btsdk.SiteServerInfo{ - ListenSSLPort: typeutil.ToPtr(d.config.SitePort), + ListenSSLPorts: typeutil.ToPtr([]int32{d.config.SitePort}), SSL: &btsdk.SiteServerSSLInfo{ IsSSL: typeutil.ToPtr(int32(1)), FullChain: typeutil.ToPtr(certPEM), diff --git a/internal/pkg/sdk3rd/btwaf/models.go b/internal/pkg/sdk3rd/btwaf/models.go index 16290e88..6217e1a5 100644 --- a/internal/pkg/sdk3rd/btwaf/models.go +++ b/internal/pkg/sdk3rd/btwaf/models.go @@ -37,8 +37,8 @@ type GetSiteListResponse struct { } type SiteServerInfo struct { - ListenSSLPort *int32 `json:"listen_ssl_port,omitempty"` - SSL *SiteServerSSLInfo `json:"ssl,omitempty"` + ListenSSLPorts *[]int32 `json:"listen_ssl_port,omitempty"` + SSL *SiteServerSSLInfo `json:"ssl,omitempty"` } type SiteServerSSLInfo struct { From 6731c465e7004f0fa0ac8b58cb37ded5e9eb9293 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 28 May 2025 23:30:38 +0800 Subject: [PATCH 25/28] refactor: workflow condition node refactor: workflow condition node --- internal/applicant/applicant.go | 38 +- internal/deployer/deployer.go | 12 +- internal/domain/{ => expr}/expr.go | 413 ++++++++++-------- internal/domain/{ => expr}/expr_test.go | 30 +- internal/domain/workflow.go | 14 +- internal/notify/notifier.go | 12 +- .../lego-providers/powerdns/powerdns.go | 1 + .../deployer/providers/proxmoxve/proxmoxve.go | 12 +- .../wangsu-certificate/wangsu_certificate.go | 2 +- internal/pkg/utils/http/transport.go | 33 ++ .../workflow/node-processor/apply_node.go | 18 +- .../workflow/node-processor/condition_node.go | 21 +- internal/workflow/node-processor/const.go | 4 +- internal/workflow/node-processor/context.go | 4 +- .../workflow/node-processor/deploy_node.go | 5 +- .../workflow/node-processor/monitor_node.go | 77 ++-- .../workflow/node-processor/notify_node.go | 14 +- .../workflow/node-processor/upload_node.go | 19 +- main.go | 9 +- ui/src/components/MultipleInput.tsx | 40 +- ui/src/components/MultipleSplitValueInput.tsx | 6 +- .../components/access/AccessFormSSHConfig.tsx | 2 +- ui/src/components/access/AccessSelect.tsx | 10 +- .../provider/ACMEDns01ProviderSelect.tsx | 12 +- .../provider/AccessProviderSelect.tsx | 12 +- .../components/provider/CAProviderSelect.tsx | 12 +- .../provider/DeploymentProviderSelect.tsx | 12 +- .../provider/NotificationProviderSelect.tsx | 12 +- .../components/workflow/WorkflowRunDetail.tsx | 2 +- ui/src/components/workflow/node/AddNode.tsx | 9 +- ui/src/components/workflow/node/ApplyNode.tsx | 4 +- .../workflow/node/ApplyNodeConfigForm.tsx | 6 +- .../workflow/node/ConditionNode.tsx | 101 ++--- .../workflow/node/ConditionNodeConfigForm.tsx | 348 ++------------- ...onditionNodeConfigFormExpressionEditor.tsx | 400 +++++++++++++++++ .../components/workflow/node/DeployNode.tsx | 6 +- .../workflow/node/DeployNodeConfigForm.tsx | 51 ++- ...loyNodeConfigFormAliyunCASDeployConfig.tsx | 6 +- ...ployNodeConfigFormBaotaPanelSiteConfig.tsx | 4 +- ...eConfigFormTencentCloudSSLDeployConfig.tsx | 4 +- ...loyNodeConfigFormUniCloudWebHostConfig.tsx | 6 +- .../DeployNodeConfigFormWangsuCDNConfig.tsx | 4 +- .../components/workflow/node/MonitorNode.tsx | 4 +- .../components/workflow/node/NotifyNode.tsx | 4 +- ui/src/components/workflow/node/StartNode.tsx | 4 +- .../components/workflow/node/UploadNode.tsx | 4 +- .../components/workflow/node/_SharedNode.tsx | 21 +- ui/src/domain/workflow.ts | 145 ++---- ui/src/domain/workflowExpr.ts | 1 + ui/src/i18n/locales/en/index.ts | 4 +- ui/src/i18n/locales/en/nls.workflow.json | 6 +- .../i18n/locales/en/nls.workflow.nodes.json | 45 +- ui/src/i18n/locales/en/nls.workflow.vars.json | 6 + ui/src/i18n/locales/zh/index.ts | 4 +- ui/src/i18n/locales/zh/nls.workflow.json | 6 +- .../i18n/locales/zh/nls.workflow.nodes.json | 53 +-- ui/src/i18n/locales/zh/nls.workflow.vars.json | 6 + ui/src/pages/workflows/WorkflowDetail.tsx | 2 +- ui/src/stores/workflow/index.ts | 6 +- 59 files changed, 1140 insertions(+), 988 deletions(-) rename internal/domain/{ => expr}/expr.go (69%) rename internal/domain/{ => expr}/expr_test.go (66%) create mode 100644 internal/pkg/utils/http/transport.go create mode 100644 ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx create mode 100644 ui/src/domain/workflowExpr.ts create mode 100644 ui/src/i18n/locales/en/nls.workflow.vars.json create mode 100644 ui/src/i18n/locales/zh/nls.workflow.vars.json diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index f1200094..d361cf83 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -53,35 +53,35 @@ func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, err return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeApply)) } - nodeConfig := config.Node.GetConfigForApply() + nodeCfg := config.Node.GetConfigForApply() options := &applicantProviderOptions{ - Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }), - ContactEmail: nodeConfig.ContactEmail, - Provider: domain.ACMEDns01ProviderType(nodeConfig.Provider), + Domains: sliceutil.Filter(strings.Split(nodeCfg.Domains, ";"), func(s string) bool { return s != "" }), + ContactEmail: nodeCfg.ContactEmail, + Provider: domain.ACMEDns01ProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), - ProviderServiceConfig: nodeConfig.ProviderConfig, - CAProvider: domain.CAProviderType(nodeConfig.CAProvider), + ProviderServiceConfig: nodeCfg.ProviderConfig, + CAProvider: domain.CAProviderType(nodeCfg.CAProvider), CAProviderAccessConfig: make(map[string]any), - CAProviderServiceConfig: nodeConfig.CAProviderConfig, - KeyAlgorithm: nodeConfig.KeyAlgorithm, - Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }), - DnsPropagationWait: nodeConfig.DnsPropagationWait, - DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, - DnsTTL: nodeConfig.DnsTTL, - DisableFollowCNAME: nodeConfig.DisableFollowCNAME, + CAProviderServiceConfig: nodeCfg.CAProviderConfig, + KeyAlgorithm: nodeCfg.KeyAlgorithm, + Nameservers: sliceutil.Filter(strings.Split(nodeCfg.Nameservers, ";"), func(s string) bool { return s != "" }), + DnsPropagationWait: nodeCfg.DnsPropagationWait, + DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout, + DnsTTL: nodeCfg.DnsTTL, + DisableFollowCNAME: nodeCfg.DisableFollowCNAME, } accessRepo := repository.NewAccessRepository() - if nodeConfig.ProviderAccessId != "" { - if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + if nodeCfg.ProviderAccessId != "" { + if access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { options.ProviderAccessConfig = access.Config } } - if nodeConfig.CAProviderAccessId != "" { - if access, err := accessRepo.GetById(context.Background(), nodeConfig.CAProviderAccessId); err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.CAProviderAccessId, err) + if nodeCfg.CAProviderAccessId != "" { + if access, err := accessRepo.GetById(context.Background(), nodeCfg.CAProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.CAProviderAccessId, err) } else { options.CAProviderAccessId = access.Id options.CAProviderAccessConfig = access.Config diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index e4a28746..c73120ba 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -29,18 +29,18 @@ func NewWithWorkflowNode(config DeployerWithWorkflowNodeConfig) (Deployer, error return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeDeploy)) } - nodeConfig := config.Node.GetConfigForDeploy() + nodeCfg := config.Node.GetConfigForDeploy() options := &deployerProviderOptions{ - Provider: domain.DeploymentProviderType(nodeConfig.Provider), + Provider: domain.DeploymentProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), - ProviderServiceConfig: nodeConfig.ProviderConfig, + ProviderServiceConfig: nodeCfg.ProviderConfig, } accessRepo := repository.NewAccessRepository() - if nodeConfig.ProviderAccessId != "" { - access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId) + if nodeCfg.ProviderAccessId != "" { + access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId) if err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { options.ProviderAccessConfig = access.Config } diff --git a/internal/domain/expr.go b/internal/domain/expr/expr.go similarity index 69% rename from internal/domain/expr.go rename to internal/domain/expr/expr.go index 01730e3d..755a876c 100644 --- a/internal/domain/expr.go +++ b/internal/domain/expr/expr.go @@ -1,4 +1,4 @@ -package domain +package expr import ( "encoding/json" @@ -6,41 +6,38 @@ import ( "strconv" ) -type Value any - type ( - ComparisonOperator string - LogicalOperator string - ValueType string - ExprType string + ExprType string + ExprComparisonOperator string + ExprLogicalOperator string + ExprValueType string ) const ( - GreaterThan ComparisonOperator = ">" - LessThan ComparisonOperator = "<" - GreaterOrEqual ComparisonOperator = ">=" - LessOrEqual ComparisonOperator = "<=" - Equal ComparisonOperator = "==" - NotEqual ComparisonOperator = "!=" - Is ComparisonOperator = "is" + GreaterThan ExprComparisonOperator = "gt" + GreaterOrEqual ExprComparisonOperator = "gte" + LessThan ExprComparisonOperator = "lt" + LessOrEqual ExprComparisonOperator = "lte" + Equal ExprComparisonOperator = "eq" + NotEqual ExprComparisonOperator = "neq" - And LogicalOperator = "and" - Or LogicalOperator = "or" - Not LogicalOperator = "not" + And ExprLogicalOperator = "and" + Or ExprLogicalOperator = "or" + Not ExprLogicalOperator = "not" - Number ValueType = "number" - String ValueType = "string" - Boolean ValueType = "boolean" + Number ExprValueType = "number" + String ExprValueType = "string" + Boolean ExprValueType = "boolean" - ConstExprType ExprType = "const" - VarExprType ExprType = "var" - CompareExprType ExprType = "compare" - LogicalExprType ExprType = "logical" - NotExprType ExprType = "not" + ConstantExprType ExprType = "const" + VariantExprType ExprType = "var" + ComparisonExprType ExprType = "comparison" + LogicalExprType ExprType = "logical" + NotExprType ExprType = "not" ) type EvalResult struct { - Type ValueType + Type ExprValueType Value any } @@ -88,13 +85,20 @@ func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } - switch e.Type { - case Number: + switch e.Type { + case String: + return &EvalResult{ + Type: Boolean, + Value: e.Value.(string) > other.Value.(string), + }, nil + + case Number: left, err := e.GetFloat64() if err != nil { return nil, err } + right, err := other.GetFloat64() if err != nil { return nil, err @@ -104,14 +108,9 @@ func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { Type: Boolean, Value: left > right, }, nil - case String: - return &EvalResult{ - Type: Boolean, - Value: e.Value.(string) > other.Value.(string), - }, nil default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -119,28 +118,32 @@ func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { - case Number: - left, err := e.GetFloat64() - if err != nil { - return nil, err - } - right, err := other.GetFloat64() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left >= right, - }, nil case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) >= other.Value.(string), }, nil + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left >= right, + }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -148,28 +151,32 @@ func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { - case Number: - left, err := e.GetFloat64() - if err != nil { - return nil, err - } - right, err := other.GetFloat64() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left < right, - }, nil case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) < other.Value.(string), }, nil + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left < right, + }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -177,28 +184,32 @@ func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { - case Number: - left, err := e.GetFloat64() - if err != nil { - return nil, err - } - right, err := other.GetFloat64() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left <= right, - }, nil case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) <= other.Value.(string), }, nil + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left <= right, + }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -206,28 +217,48 @@ func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { - case Number: - left, err := e.GetFloat64() - if err != nil { - return nil, err - } - right, err := other.GetFloat64() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left == right, - }, nil case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) == other.Value.(string), }, nil + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left == right, + }, nil + + case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + + right, err := other.GetBool() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left == right, + }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -235,28 +266,48 @@ func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { - case Number: - left, err := e.GetFloat64() - if err != nil { - return nil, err - } - right, err := other.GetFloat64() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left != right, - }, nil case String: return &EvalResult{ Type: Boolean, Value: e.Value.(string) != other.Value.(string), }, nil + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left != right, + }, nil + + case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + + right, err := other.GetBool() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left != right, + }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -264,22 +315,26 @@ func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { case Boolean: left, err := e.GetBool() if err != nil { return nil, err } + right, err := other.GetBool() if err != nil { return nil, err } + return &EvalResult{ Type: Boolean, Value: left && right, }, nil + default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -287,22 +342,25 @@ func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) { if e.Type != other.Type { return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) } + switch e.Type { case Boolean: left, err := e.GetBool() if err != nil { return nil, err } + right, err := other.GetBool() if err != nil { return nil, err } + return &EvalResult{ Type: Boolean, Value: left || right, }, nil default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) + return nil, fmt.Errorf("unsupported value type: %s", e.Type) } } @@ -310,67 +368,52 @@ func (e *EvalResult) Not() (*EvalResult, error) { if e.Type != Boolean { return nil, fmt.Errorf("type mismatch: %s", e.Type) } + boolValue, err := e.GetBool() if err != nil { return nil, err } + return &EvalResult{ Type: Boolean, Value: !boolValue, }, nil } -func (e *EvalResult) Is(other *EvalResult) (*EvalResult, error) { - if e.Type != other.Type { - return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) - } - switch e.Type { - case Boolean: - left, err := e.GetBool() - if err != nil { - return nil, err - } - right, err := other.GetBool() - if err != nil { - return nil, err - } - return &EvalResult{ - Type: Boolean, - Value: left == right, - }, nil - default: - return nil, fmt.Errorf("unsupported type: %s", e.Type) - } -} - type Expr interface { GetType() ExprType Eval(variables map[string]map[string]any) (*EvalResult, error) } -type ConstExpr struct { - Type ExprType `json:"type"` - Value Value `json:"value"` - ValueType ValueType `json:"valueType"` +type ExprValueSelector struct { + Id string `json:"id"` + Name string `json:"name"` + Type ExprValueType `json:"type"` } -func (c ConstExpr) GetType() ExprType { return c.Type } +type ConstantExpr struct { + Type ExprType `json:"type"` + Value string `json:"value"` + ValueType ExprValueType `json:"valueType"` +} -func (c ConstExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { +func (c ConstantExpr) GetType() ExprType { return c.Type } + +func (c ConstantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { return &EvalResult{ Type: c.ValueType, Value: c.Value, }, nil } -type VarExpr struct { - Type ExprType `json:"type"` - Selector WorkflowNodeIOValueSelector `json:"selector"` +type VariantExpr struct { + Type ExprType `json:"type"` + Selector ExprValueSelector `json:"selector"` } -func (v VarExpr) GetType() ExprType { return v.Type } +func (v VariantExpr) GetType() ExprType { return v.Type } -func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { +func (v VariantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { if v.Selector.Id == "" { return nil, fmt.Errorf("node id is empty") } @@ -391,16 +434,16 @@ func (v VarExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) }, nil } -type CompareExpr struct { - Type ExprType `json:"type"` // compare - Op ComparisonOperator `json:"op"` - Left Expr `json:"left"` - Right Expr `json:"right"` +type ComparisonExpr struct { + Type ExprType `json:"type"` // compare + Operator ExprComparisonOperator `json:"operator"` + Left Expr `json:"left"` + Right Expr `json:"right"` } -func (c CompareExpr) GetType() ExprType { return c.Type } +func (c ComparisonExpr) GetType() ExprType { return c.Type } -func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { +func (c ComparisonExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { left, err := c.Left.Eval(variables) if err != nil { return nil, err @@ -410,7 +453,7 @@ func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, err return nil, err } - switch c.Op { + switch c.Operator { case GreaterThan: return left.GreaterThan(right) case LessThan: @@ -423,18 +466,16 @@ func (c CompareExpr) Eval(variables map[string]map[string]any) (*EvalResult, err return left.Equal(right) case NotEqual: return left.NotEqual(right) - case Is: - return left.Is(right) default: - return nil, fmt.Errorf("unknown operator: %s", c.Op) + return nil, fmt.Errorf("unknown expression operator: %s", c.Operator) } } type LogicalExpr struct { - Type ExprType `json:"type"` // logical - Op LogicalOperator `json:"op"` - Left Expr `json:"left"` - Right Expr `json:"right"` + Type ExprType `json:"type"` // logical + Operator ExprLogicalOperator `json:"operator"` + Left Expr `json:"left"` + Right Expr `json:"right"` } func (l LogicalExpr) GetType() ExprType { return l.Type } @@ -449,13 +490,13 @@ func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, err return nil, err } - switch l.Op { + switch l.Operator { case And: return left.And(right) case Or: return left.Or(right) default: - return nil, fmt.Errorf("unknown operator: %s", l.Op) + return nil, fmt.Errorf("unknown expression operator: %s", l.Operator) } } @@ -489,24 +530,24 @@ func UnmarshalExpr(data []byte) (Expr, error) { } switch typ.Type { - case ConstExprType: - var e ConstExpr + case ConstantExprType: + var e ConstantExpr if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e, nil - case VarExprType: - var e VarExpr + case VariantExprType: + var e VariantExpr if err := json.Unmarshal(data, &e); err != nil { return nil, err } return e, nil - case CompareExprType: - var e CompareExprRaw + case ComparisonExprType: + var e ComparisonExprRaw if err := json.Unmarshal(data, &e); err != nil { return nil, err } - return e.ToCompareExpr() + return e.ToComparisonExpr() case LogicalExprType: var e LogicalExprRaw if err := json.Unmarshal(data, &e); err != nil { @@ -520,39 +561,39 @@ func UnmarshalExpr(data []byte) (Expr, error) { } return e.ToNotExpr() default: - return nil, fmt.Errorf("unknown expr type: %s", typ.Type) + return nil, fmt.Errorf("unknown expression type: %s", typ.Type) } } -type CompareExprRaw struct { - Type ExprType `json:"type"` - Op ComparisonOperator `json:"op"` - Left json.RawMessage `json:"left"` - Right json.RawMessage `json:"right"` +type ComparisonExprRaw struct { + Type ExprType `json:"type"` + Operator ExprComparisonOperator `json:"operator"` + Left json.RawMessage `json:"left"` + Right json.RawMessage `json:"right"` } -func (r CompareExprRaw) ToCompareExpr() (CompareExpr, error) { +func (r ComparisonExprRaw) ToComparisonExpr() (ComparisonExpr, error) { leftExpr, err := UnmarshalExpr(r.Left) if err != nil { - return CompareExpr{}, err + return ComparisonExpr{}, err } rightExpr, err := UnmarshalExpr(r.Right) if err != nil { - return CompareExpr{}, err + return ComparisonExpr{}, err } - return CompareExpr{ - Type: r.Type, - Op: r.Op, - Left: leftExpr, - Right: rightExpr, + return ComparisonExpr{ + Type: r.Type, + Operator: r.Operator, + Left: leftExpr, + Right: rightExpr, }, nil } type LogicalExprRaw struct { - Type ExprType `json:"type"` - Op LogicalOperator `json:"op"` - Left json.RawMessage `json:"left"` - Right json.RawMessage `json:"right"` + Type ExprType `json:"type"` + Operator ExprLogicalOperator `json:"operator"` + Left json.RawMessage `json:"left"` + Right json.RawMessage `json:"right"` } func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) { @@ -565,10 +606,10 @@ func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) { return LogicalExpr{}, err } return LogicalExpr{ - Type: r.Type, - Op: r.Op, - Left: left, - Right: right, + Type: r.Type, + Operator: r.Operator, + Left: left, + Right: right, }, nil } diff --git a/internal/domain/expr_test.go b/internal/domain/expr/expr_test.go similarity index 66% rename from internal/domain/expr_test.go rename to internal/domain/expr/expr_test.go index f0a34504..fb76d98c 100644 --- a/internal/domain/expr_test.go +++ b/internal/domain/expr/expr_test.go @@ -1,4 +1,4 @@ -package domain +package expr import ( "testing" @@ -7,15 +7,15 @@ import ( func TestLogicalEval(t *testing.T) { // 测试逻辑表达式 and logicalExpr := LogicalExpr{ - Left: ConstExpr{ + Left: ConstantExpr{ Type: "const", - Value: true, + Value: "true", ValueType: "boolean", }, - Op: And, - Right: ConstExpr{ + Operator: And, + Right: ConstantExpr{ Type: "const", - Value: true, + Value: "true", ValueType: "boolean", }, } @@ -29,15 +29,15 @@ func TestLogicalEval(t *testing.T) { // 测试逻辑表达式 or orExpr := LogicalExpr{ - Left: ConstExpr{ + Left: ConstantExpr{ Type: "const", - Value: true, + Value: "true", ValueType: "boolean", }, - Op: Or, - Right: ConstExpr{ + Operator: Or, + Right: ConstantExpr{ Type: "const", - Value: true, + Value: "true", ValueType: "boolean", }, } @@ -63,7 +63,7 @@ func TestUnmarshalExpr(t *testing.T) { { name: "test1", args: args{ - data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validated","type":"boolean"},"type":"var"},"op":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"compare"},"op":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"op":"==","right":{"type":"const","value":2,"valueType":"number"},"type":"compare"},"type":"logical"}`), + data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`), }, }, } @@ -98,11 +98,11 @@ func TestExpr_Eval(t *testing.T) { args: args{ variables: map[string]map[string]any{ "ODnYSOXB6HQP2_vz6JcZE": { - "certificate.validated": true, - "certificate.daysLeft": 2, + "certificate.validity": true, + "certificate.daysLeft": 2, }, }, - data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validated","type":"boolean"},"type":"var"},"op":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"compare"},"op":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"op":"==","right":{"type":"const","value":2,"valueType":"number"},"type":"compare"},"type":"logical"}`), + data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`), }, }, } diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 7d7355c5..02f8b671 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -4,6 +4,7 @@ import ( "encoding/json" "time" + "github.com/usual2970/certimate/internal/domain/expr" maputil "github.com/usual2970/certimate/internal/pkg/utils/map" ) @@ -114,7 +115,7 @@ type WorkflowNodeConfigForNotify struct { } type WorkflowNodeConfigForCondition struct { - Expression Expr `json:"expression"` // 条件表达式 + Expression expr.Expr `json:"expression"` // 条件表达式 } func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { @@ -183,9 +184,8 @@ func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { return WorkflowNodeConfigForCondition{} } - raw, _ := json.Marshal(expression) - - expr, err := UnmarshalExpr([]byte(raw)) + exprRaw, _ := json.Marshal(expression) + expr, err := expr.UnmarshalExpr([]byte(exprRaw)) if err != nil { return WorkflowNodeConfigForCondition{} } @@ -204,10 +204,6 @@ type WorkflowNodeIO struct { ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"` } -type WorkflowNodeIOValueSelector struct { - Id string `json:"id"` - Name string `json:"name"` - Type ValueType `json:"type"` -} +type WorkflowNodeIOValueSelector = expr.ExprValueSelector const WorkflowNodeIONameCertificate string = "certificate" diff --git a/internal/notify/notifier.go b/internal/notify/notifier.go index ee3fbd2f..5e957841 100644 --- a/internal/notify/notifier.go +++ b/internal/notify/notifier.go @@ -29,18 +29,18 @@ func NewWithWorkflowNode(config NotifierWithWorkflowNodeConfig) (Notifier, error return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeNotify)) } - nodeConfig := config.Node.GetConfigForNotify() + nodeCfg := config.Node.GetConfigForNotify() options := ¬ifierProviderOptions{ - Provider: domain.NotificationProviderType(nodeConfig.Provider), + Provider: domain.NotificationProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), - ProviderServiceConfig: nodeConfig.ProviderConfig, + ProviderServiceConfig: nodeCfg.ProviderConfig, } accessRepo := repository.NewAccessRepository() - if nodeConfig.ProviderAccessId != "" { - access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId) + if nodeCfg.ProviderAccessId != "" { + access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId) if err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { options.ProviderAccessConfig = access.Config } diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go index b34516d4..7c87536c 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go @@ -29,6 +29,7 @@ func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, providerConfig.APIKey = config.ApiKey if config.AllowInsecureConnections { providerConfig.HTTPClient.Transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, diff --git a/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go b/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go index 349c3a16..0295c7e2 100644 --- a/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go +++ b/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go @@ -13,6 +13,7 @@ import ( "github.com/luthermonson/go-proxmox" "github.com/usual2970/certimate/internal/pkg/core/deployer" + httputil "github.com/usual2970/certimate/internal/pkg/utils/http" ) type DeployerConfig struct { @@ -101,15 +102,16 @@ func createSdkClient(serverUrl, apiToken, apiTokenSecret string, skipTlsVerify b } httpClient := &http.Client{ - Transport: http.DefaultTransport, + Transport: httputil.NewDefaultTransport(), Timeout: http.DefaultClient.Timeout, } if skipTlsVerify { - httpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, + transport := httputil.NewDefaultTransport() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} } + transport.TLSClientConfig.InsecureSkipVerify = true + httpClient.Transport = transport } client := proxmox.NewClient( strings.TrimRight(serverUrl, "/")+"/api2/json", diff --git a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go index b512be09..38ddbd46 100644 --- a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go +++ b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go @@ -65,7 +65,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } // 查询证书列表,避免重复上传 - // REF: https://www.wangsu.com/document/api-doc/26426 + // REF: https://www.wangsu.com/document/api-doc/22675?productCode=certificatemanagement listCertificatesResp, err := u.sdkClient.ListCertificates() u.logger.Debug("sdk request 'certificatemanagement.ListCertificates'", slog.Any("response", listCertificatesResp)) if err != nil { diff --git a/internal/pkg/utils/http/transport.go b/internal/pkg/utils/http/transport.go new file mode 100644 index 00000000..ff8c8804 --- /dev/null +++ b/internal/pkg/utils/http/transport.go @@ -0,0 +1,33 @@ +package httputil + +import ( + "net" + "net/http" + "time" +) + +// 创建并返回一个 [http.DefaultTransport] 对象副本。 +// +// 出参: +// - transport: [http.DefaultTransport] 对象副本。 +func NewDefaultTransport() *http.Transport { + if http.DefaultTransport != nil { + if t, ok := http.DefaultTransport.(*http.Transport); ok { + return t.Clone() + } + } + + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 321d9fc8..8616fbd9 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -3,6 +3,7 @@ package nodeprocessor import ( "context" "fmt" + "strconv" "time" "golang.org/x/exp/maps" @@ -108,15 +109,15 @@ func (n *applyNode) Process(ctx context.Context) error { } } - // 添加中间结果 - n.outputs[outputCertificateValidatedKey] = "true" - n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24)) + // 记录中间结果 + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10) n.logger.Info("application completed") return nil } -func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { +func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 currentNodeConfig := n.node.GetConfigForApply() @@ -154,9 +155,12 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if expirationTime > renewalInterval { - n.outputs[outputCertificateValidatedKey] = "true" - n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(expirationTime.Hours()/24)) - return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) + daysLeft := int(expirationTime.Hours() / 24) + // TODO: 优化此处逻辑,[checkCanSkip] 方法不应该修改中间结果,违背单一职责 + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10) + + return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", daysLeft, currentNodeConfig.SkipBeforeExpiryDays) } } } diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index d90811d9..d9e8126d 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -3,8 +3,10 @@ package nodeprocessor import ( "context" "errors" + "fmt" "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/domain/expr" ) type conditionNode struct { @@ -22,30 +24,29 @@ func NewConditionNode(node *domain.WorkflowNode) *conditionNode { } func (n *conditionNode) Process(ctx context.Context) error { - n.logger.Info("enter condition node: " + n.node.Name) - - nodeConfig := n.node.GetConfigForCondition() - if nodeConfig.Expression == nil { - n.logger.Info("no condition found, continue to next node") + nodeCfg := n.node.GetConfigForCondition() + if nodeCfg.Expression == nil { + n.logger.Info("without any conditions, enter this branch") return nil } - rs, err := n.eval(ctx, nodeConfig.Expression) + rs, err := n.evalExpr(ctx, nodeCfg.Expression) if err != nil { - n.logger.Warn("failed to eval expression: " + err.Error()) + n.logger.Warn(fmt.Sprintf("failed to eval condition expression: %w", err)) return err } if rs.Value == false { n.logger.Info("condition not met, skip this branch") - return errors.New("condition not met") + return errors.New("condition not met") // TODO: 错误处理 + } else { + n.logger.Info("condition met, enter this branch") } - n.logger.Info("condition met, continue to next node") return nil } -func (n *conditionNode) eval(ctx context.Context, expression domain.Expr) (*domain.EvalResult, error) { +func (n *conditionNode) evalExpr(ctx context.Context, expression expr.Expr) (*expr.EvalResult, error) { variables := GetNodeOutputs(ctx) return expression.Eval(variables) } diff --git a/internal/workflow/node-processor/const.go b/internal/workflow/node-processor/const.go index c1af01c9..62d2d56b 100644 --- a/internal/workflow/node-processor/const.go +++ b/internal/workflow/node-processor/const.go @@ -1,6 +1,6 @@ package nodeprocessor const ( - outputCertificateValidatedKey = "certificate.validated" - outputCertificateDaysLeftKey = "certificate.daysLeft" + outputKeyForCertificateValidity = "certificate.validity" + outputKeyForCertificateDaysLeft = "certificate.daysLeft" ) diff --git a/internal/workflow/node-processor/context.go b/internal/workflow/node-processor/context.go index adceacf6..96c40487 100644 --- a/internal/workflow/node-processor/context.go +++ b/internal/workflow/node-processor/context.go @@ -35,7 +35,8 @@ func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) co container.Lock() defer container.Unlock() - // 创建输出的深拷贝以避免后续修改 + // 创建输出的深拷贝 + // TODO: 暂时使用浅拷贝,等后续值类型扩充后修改 outputCopy := make(map[string]any, len(output)) for k, v := range output { outputCopy[k] = v @@ -90,6 +91,7 @@ func GetNodeOutputs(ctx context.Context) map[string]map[string]any { defer container.RUnlock() // 创建所有输出的深拷贝 + // TODO: 暂时使用浅拷贝,等后续值类型扩充后修改 allOutputs := make(map[string]map[string]any, len(container.outputs)) for nodeId, output := range container.outputs { nodeCopy := make(map[string]any, len(output)) diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index f0ded21d..f89f4a1f 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -42,8 +42,9 @@ func (n *deployNode) Process(ctx context.Context) error { } // 获取前序节点输出证书 + const DELIMITER = "#" previousNodeOutputCertificateSource := n.node.GetConfigForDeploy().Certificate - previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, "#") + previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, DELIMITER) if len(previousNodeOutputCertificateSourceSlice) != 2 { n.logger.Warn("invalid certificate source", slog.String("certificate.source", previousNodeOutputCertificateSource)) return fmt.Errorf("invalid certificate source: %s", previousNodeOutputCertificateSource) @@ -99,7 +100,7 @@ func (n *deployNode) Process(ctx context.Context) error { return nil } -func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { +func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 currentNodeConfig := n.node.GetConfigForDeploy() diff --git a/internal/workflow/node-processor/monitor_node.go b/internal/workflow/node-processor/monitor_node.go index f8c1adae..4b875f26 100644 --- a/internal/workflow/node-processor/monitor_node.go +++ b/internal/workflow/node-processor/monitor_node.go @@ -6,13 +6,13 @@ import ( "crypto/x509" "fmt" "math" - "net" "net/http" "strconv" "strings" "time" "github.com/usual2970/certimate/internal/domain" + httputil "github.com/usual2970/certimate/internal/pkg/utils/http" ) type monitorNode struct { @@ -32,23 +32,23 @@ func NewMonitorNode(node *domain.WorkflowNode) *monitorNode { func (n *monitorNode) Process(ctx context.Context) error { n.logger.Info("ready to monitor certificate ...") - nodeConfig := n.node.GetConfigForMonitor() + nodeCfg := n.node.GetConfigForMonitor() - targetAddr := fmt.Sprintf("%s:%d", nodeConfig.Host, nodeConfig.Port) - if nodeConfig.Port == 0 { - targetAddr = fmt.Sprintf("%s:443", nodeConfig.Host) + targetAddr := fmt.Sprintf("%s:%d", nodeCfg.Host, nodeCfg.Port) + if nodeCfg.Port == 0 { + targetAddr = fmt.Sprintf("%s:443", nodeCfg.Host) } - targetDomain := nodeConfig.Domain + targetDomain := nodeCfg.Domain if targetDomain == "" { - targetDomain = nodeConfig.Host + targetDomain = nodeCfg.Host } n.logger.Info(fmt.Sprintf("retrieving certificate at %s (domain: %s)", targetAddr, targetDomain)) const MAX_ATTEMPTS = 3 const RETRY_INTERVAL = 2 * time.Second - var cert *x509.Certificate + var certs []*x509.Certificate var err error for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ { if attempt > 0 { @@ -61,7 +61,7 @@ func (n *monitorNode) Process(ctx context.Context) error { } } - cert, err = n.tryRetrieveCert(ctx, targetAddr, targetDomain, nodeConfig.RequestPath) + certs, err = n.tryRetrievePeerCertificates(ctx, targetAddr, targetDomain, nodeCfg.RequestPath) if err == nil { break } @@ -71,15 +71,13 @@ func (n *monitorNode) Process(ctx context.Context) error { n.logger.Warn("failed to monitor certificate") return err } else { - if cert == nil { + if len(certs) == 0 { n.logger.Warn("no ssl certificates retrieved in http response") - outputs := map[string]any{ - outputCertificateValidatedKey: strconv.FormatBool(false), - outputCertificateDaysLeftKey: strconv.FormatInt(0, 10), - } - n.setOutputs(outputs) + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(false) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(0, 10) } else { + cert := certs[0] // 只取证书链中的第一个证书,即服务器证书 n.logger.Info(fmt.Sprintf("ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')", cert.SerialNumber, cert.Subject.String(), cert.Issuer.String(), cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339), @@ -95,11 +93,8 @@ func (n *monitorNode) Process(ctx context.Context) error { validated := isCertPeriodValid && isCertHostMatched daysLeft := int(math.Floor(cert.NotAfter.Sub(now).Hours() / 24)) - outputs := map[string]any{ - outputCertificateValidatedKey: strconv.FormatBool(validated), - outputCertificateDaysLeftKey: strconv.FormatInt(int64(daysLeft), 10), - } - n.setOutputs(outputs) + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(validated) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10) if validated { n.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft)) @@ -113,52 +108,40 @@ func (n *monitorNode) Process(ctx context.Context) error { return nil } -func (n *monitorNode) tryRetrieveCert(ctx context.Context, addr, domain, requestPath string) (_cert *x509.Certificate, _err error) { - transport := &http.Transport{ - DialContext: (&net.Dialer{ - Timeout: 10 * time.Second, - }).DialContext, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - ForceAttemptHTTP2: false, - DisableKeepAlives: true, - Proxy: http.ProxyFromEnvironment, +func (n *monitorNode) tryRetrievePeerCertificates(ctx context.Context, addr, domain, requestPath string) ([]*x509.Certificate, error) { + transport := httputil.NewDefaultTransport() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} } + transport.TLSClientConfig.InsecureSkipVerify = true client := &http.Client{ - Transport: transport, - Timeout: 15 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, + Timeout: 30 * time.Second, + Transport: transport, } url := fmt.Sprintf("https://%s/%s", addr, strings.TrimLeft(requestPath, "/")) req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) if err != nil { - _err = fmt.Errorf("failed to create http request: %w", err) - n.logger.Warn(fmt.Sprintf("failed to create http request: %w", err)) - return nil, _err + err = fmt.Errorf("failed to create http request: %w", err) + n.logger.Warn(err.Error()) + return nil, err } req.Header.Set("User-Agent", "certimate") resp, err := client.Do(req) if err != nil { - _err = fmt.Errorf("failed to send http request: %w", err) - n.logger.Warn(fmt.Sprintf("failed to send http request: %w", err)) - return nil, _err + err = fmt.Errorf("failed to send http request: %w", err) + n.logger.Warn(err.Error()) + return nil, err } defer resp.Body.Close() if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { - return nil, _err + return make([]*x509.Certificate, 0), nil } - - _cert = resp.TLS.PeerCertificates[0] - return _cert, nil -} - -func (n *monitorNode) setOutputs(outputs map[string]any) { - n.outputs = outputs + return resp.TLS.PeerCertificates, nil } diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go index f084cb4f..dabfd034 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -30,9 +30,9 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { func (n *notifyNode) Process(ctx context.Context) error { n.logger.Info("ready to send notification ...") - nodeConfig := n.node.GetConfigForNotify() + nodeCfg := n.node.GetConfigForNotify() - if nodeConfig.Provider == "" { + if nodeCfg.Provider == "" { // Deprecated: v0.4.x 将废弃 // 兼容旧版本的通知渠道 n.logger.Warn("WARNING! you are using the notification channel from global settings, which will be deprecated in the future") @@ -44,14 +44,14 @@ func (n *notifyNode) Process(ctx context.Context) error { } // 获取通知渠道 - channelConfig, err := settings.GetNotifyChannelConfig(nodeConfig.Channel) + channelConfig, err := settings.GetNotifyChannelConfig(nodeCfg.Channel) if err != nil { return err } // 发送通知 - if err := notify.SendToChannel(nodeConfig.Subject, nodeConfig.Message, nodeConfig.Channel, channelConfig); err != nil { - n.logger.Warn("failed to send notification", slog.String("channel", nodeConfig.Channel)) + if err := notify.SendToChannel(nodeCfg.Subject, nodeCfg.Message, nodeCfg.Channel, channelConfig); err != nil { + n.logger.Warn("failed to send notification", slog.String("channel", nodeCfg.Channel)) return err } @@ -63,8 +63,8 @@ func (n *notifyNode) Process(ctx context.Context) error { deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{ Node: n.node, Logger: n.logger, - Subject: nodeConfig.Subject, - Message: nodeConfig.Message, + Subject: nodeCfg.Subject, + Message: nodeCfg.Message, }) if err != nil { n.logger.Warn("failed to create notifier provider") diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 8e59b009..9431d31a 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -3,6 +3,7 @@ package nodeprocessor import ( "context" "fmt" + "strconv" "strings" "time" @@ -33,7 +34,7 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode { func (n *uploadNode) Process(ctx context.Context) error { n.logger.Info("ready to upload certiticate ...") - nodeConfig := n.node.GetConfigForUpload() + nodeCfg := n.node.GetConfigForUpload() // 查询上次执行结果 lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) @@ -53,7 +54,7 @@ func (n *uploadNode) Process(ctx context.Context) error { certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeUpload, } - certificate.PopulateFromPEM(nodeConfig.Certificate, nodeConfig.PrivateKey) + certificate.PopulateFromPEM(nodeCfg.Certificate, nodeCfg.PrivateKey) // 保存执行结果 output := &domain.WorkflowOutput{ @@ -69,15 +70,15 @@ func (n *uploadNode) Process(ctx context.Context) error { return err } - n.outputs[outputCertificateValidatedKey] = "true" - n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24)) + // 记录中间结果 + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10) n.logger.Info("uploading completed") - return nil } -func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (skip bool, reason string) { +func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) { if lastOutput != nil && lastOutput.Succeeded { // 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致 currentNodeConfig := n.node.GetConfigForUpload() @@ -91,8 +92,10 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId) if lastCertificate != nil { - n.outputs[outputCertificateValidatedKey] = "true" - n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(lastCertificate.ExpireAt).Hours()/24)) + daysLeft := int(time.Until(lastCertificate.ExpireAt).Hours() / 24) + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(daysLeft > 0) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10) + return true, "the certificate has already been uploaded" } } diff --git a/main.go b/main.go index 76f7f1c0..18e88bed 100644 --- a/main.go +++ b/main.go @@ -26,9 +26,7 @@ func main() { app := app.GetApp().(*pocketbase.PocketBase) var flagHttp string - var flagDir string flag.StringVar(&flagHttp, "http", "127.0.0.1:8090", "HTTP server address") - flag.StringVar(&flagDir, "dir", "/pb_data/database", "Pocketbase data directory") if len(os.Args) < 2 { slog.Error("[CERTIMATE] missing exec args") os.Exit(1) @@ -59,14 +57,17 @@ func main() { Priority: 999, }) + app.OnServe().BindFunc(func(e *core.ServeEvent) error { + slog.Info("[CERTIMATE] Visit the website: http://" + flagHttp) + return e.Next() + }) + app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error { routes.Unregister() slog.Info("[CERTIMATE] Exit!") return e.Next() }) - slog.Info("[CERTIMATE] Visit the website: http://" + flagHttp) - if err := app.Start(); err != nil { slog.Error("[CERTIMATE] Start failed.", "err", err) } diff --git a/ui/src/components/MultipleInput.tsx b/ui/src/components/MultipleInput.tsx index d28db745..d8be12fa 100644 --- a/ui/src/components/MultipleInput.tsx +++ b/ui/src/components/MultipleInput.tsx @@ -152,10 +152,10 @@ const MultipleInput = ({ value={element} onBlur={() => handleInputBlur(index)} onChange={(val) => handleChange(index, val)} - onClickAdd={() => handleClickAdd(index)} - onClickDown={() => handleClickDown(index)} - onClickUp={() => handleClickUp(index)} - onClickRemove={() => handleClickRemove(index)} + onEntryAdd={() => handleClickAdd(index)} + onEntryDown={() => handleClickDown(index)} + onEntryUp={() => handleClickUp(index)} + onEntryRemove={() => handleClickRemove(index)} /> ); })} @@ -174,10 +174,10 @@ type MultipleInputItemProps = Omit< defaultValue?: string; value?: string; onChange?: (value: string) => void; - onClickAdd?: () => void; - onClickDown?: () => void; - onClickUp?: () => void; - onClickRemove?: () => void; + onEntryAdd?: () => void; + onEntryDown?: () => void; + onEntryUp?: () => void; + onEntryRemove?: () => void; }; type MultipleInputItemInstance = { @@ -197,10 +197,10 @@ const MultipleInputItem = forwardRef { if (!showSortButton) return null; - return diff --git a/ui/src/components/access/AccessSelect.tsx b/ui/src/components/access/AccessSelect.tsx index 0a570699..01f30249 100644 --- a/ui/src/components/access/AccessSelect.tsx +++ b/ui/src/components/access/AccessSelect.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type AccessModel } from "@/domain/access"; import { accessProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type AccessTypeSelectProps = Omit< }; const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { + const { token: themeToken } = theme.useToken(); + const { accesses, loadedAtOnce, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses"])); useEffect(() => { fetchAccesses(); @@ -65,12 +67,12 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { const value = inputValue.toLowerCase(); return option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (label) { + labelRender={({ value }) => { + if (value != null) { return renderOption(value as string); } - return {props.placeholder}; + return {props.placeholder}; }} loading={!loadedAtOnce} options={options} diff --git a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx index e2408eeb..227bfcdd 100644 --- a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx +++ b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type ACMEDns01Provider, acmeDns01ProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type ACMEDns01ProviderSelectProps = Omit< const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(acmeDns01ProvidersMap.values()); @@ -49,12 +51,12 @@ const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectPr const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/AccessProviderSelect.tsx b/ui/src/components/provider/AccessProviderSelect.tsx index bf4ff6e7..055b3ddc 100644 --- a/ui/src/components/provider/AccessProviderSelect.tsx +++ b/ui/src/components/provider/AccessProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Tag, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Tag, Typography, theme } from "antd"; import Show from "@/components/Show"; import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider"; @@ -16,6 +16,8 @@ export type AccessProviderSelectProps = Omit< const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProviderSelectProps = { showOptionTags: true }) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(accessProvidersMap.values()); @@ -84,12 +86,12 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/CAProviderSelect.tsx b/ui/src/components/provider/CAProviderSelect.tsx index e5477c21..d1fdbba9 100644 --- a/ui/src/components/provider/CAProviderSelect.tsx +++ b/ui/src/components/provider/CAProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type CAProvider, caProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type CAProviderSelectProps = Omit< const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(caProvidersMap.values()); @@ -65,12 +67,12 @@ const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => { const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder || t("provider.default_ca_provider.label")}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/DeploymentProviderSelect.tsx b/ui/src/components/provider/DeploymentProviderSelect.tsx index 89173243..07fa4577 100644 --- a/ui/src/components/provider/DeploymentProviderSelect.tsx +++ b/ui/src/components/provider/DeploymentProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type DeploymentProvider, deploymentProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type DeploymentProviderSelectProps = Omit< const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(deploymentProvidersMap.values()); @@ -49,12 +51,12 @@ const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelect const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/NotificationProviderSelect.tsx b/ui/src/components/provider/NotificationProviderSelect.tsx index f30a8f6f..8b0dd353 100644 --- a/ui/src/components/provider/NotificationProviderSelect.tsx +++ b/ui/src/components/provider/NotificationProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type NotificationProvider, notificationProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type NotificationProviderSelectProps = Omit< const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(notificationProvidersMap.values()); @@ -49,12 +51,12 @@ const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSe const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/workflow/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx index 2d421880..746adb4c 100644 --- a/ui/src/components/workflow/WorkflowRunDetail.tsx +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -36,7 +36,7 @@ import { ClientResponseError } from "pocketbase"; import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import Show from "@/components/Show"; import { type CertificateModel } from "@/domain/certificate"; -import type { WorkflowLogModel } from "@/domain/workflowLog"; +import { type WorkflowLogModel } from "@/domain/workflowLog"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; import { useBrowserTheme } from "@/hooks"; import { listByWorkflowRunId as listCertificatesByWorkflowRunId } from "@/repository/certificate"; diff --git a/ui/src/components/workflow/node/AddNode.tsx b/ui/src/components/workflow/node/AddNode.tsx index 86a45134..207ec7c7 100644 --- a/ui/src/components/workflow/node/AddNode.tsx +++ b/ui/src/components/workflow/node/AddNode.tsx @@ -35,7 +35,14 @@ const AddNode = ({ node, disabled }: AddNodeProps) => { [WorkflowNodeType.ExecuteResultBranch, "workflow_node.execute_result_branch.label", ], ] .filter(([type]) => { - if (node.type !== WorkflowNodeType.Apply && node.type !== WorkflowNodeType.Deploy && node.type !== WorkflowNodeType.Notify) { + const hasExecuteResult = [ + WorkflowNodeType.Apply, + WorkflowNodeType.Upload, + WorkflowNodeType.Monitor, + WorkflowNodeType.Deploy, + WorkflowNodeType.Notify, + ].includes(node.type); + if (!hasExecuteResult) { return type !== WorkflowNodeType.ExecuteResultBranch; } diff --git a/ui/src/components/workflow/node/ApplyNode.tsx b/ui/src/components/workflow/node/ApplyNode.tsx index c250fd89..ff0d64bf 100644 --- a/ui/src/components/workflow/node/ApplyNode.tsx +++ b/ui/src/components/workflow/node/ApplyNode.tsx @@ -38,9 +38,9 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForApply; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForApply; const handleDrawerConfirm = async () => { setFormPending(true); @@ -74,12 +74,12 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx index 7faa148e..ae56efc3 100644 --- a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx @@ -56,7 +56,7 @@ export type ApplyNodeConfigFormInstance = { validateFields: FormInstance["validateFields"]; }; -const MULTIPLE_INPUT_DELIMITER = ";"; +const MULTIPLE_INPUT_SEPARATOR = ";"; const initFormModel = (): ApplyNodeConfigFormFieldValues => { return { @@ -76,7 +76,7 @@ const ApplyNodeConfigForm = forwardRef { if (!v) return false; return String(v) - .split(MULTIPLE_INPUT_DELIMITER) + .split(MULTIPLE_INPUT_SEPARATOR) .every((e) => validDomainName(e, { allowWildcard: true })); }, t("common.errmsg.domain_invalid")), contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email(t("common.errmsg.email_invalid")), @@ -106,7 +106,7 @@ const ApplyNodeConfigForm = forwardRef { if (!v) return true; return String(v) - .split(MULTIPLE_INPUT_DELIMITER) + .split(MULTIPLE_INPUT_SEPARATOR) .every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e)); }, t("common.errmsg.host_invalid")), dnsPropagationWait: z.preprocess( diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index bc5b5918..2c8b3d81 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -1,17 +1,14 @@ import { memo, useRef, useState } from "react"; -import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; +import { FilterFilled as FilterFilledIcon, FilterOutlined as FilterOutlinedIcon, MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; import { Button, Card, Popover } from "antd"; import { produce } from "immer"; -import type { Expr, WorkflowNodeIoValueType } from "@/domain/workflow"; -import { ExprType } from "@/domain/workflow"; import { useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; -import type { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; -import ConditionNodeConfigForm from "./ConditionNodeConfigForm"; +import ConditionNodeConfigForm, { type ConditionNodeConfigFormFieldValues, type ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; export type ConditionNodeProps = SharedNodeProps & { branchId: string; @@ -23,55 +20,9 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP const [formPending, setFormPending] = useState(false); const formRef = useRef(null); - - const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as ConditionNodeConfigFormFieldValues; - // 将表单值转换为表达式结构 - const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { - // 创建单个条件的表达式 - const createComparisonExpr = (condition: ConditionItem): Expr => { - const selectors = condition.leftSelector.split("#"); - const t = selectors[2] as WorkflowNodeIoValueType; - const left: Expr = { - type: ExprType.Var, - selector: { - id: selectors[0], - name: selectors[1], - type: t, - }, - }; - - const right: Expr = { type: ExprType.Const, value: condition.rightValue, valueType: t }; - - return { - type: ExprType.Compare, - op: condition.operator, - left, - right, - }; - }; - - // 如果只有一个条件,直接返回比较表达式 - if (values.conditions.length === 1) { - return createComparisonExpr(values.conditions[0]); - } - - // 多个条件,通过逻辑运算符连接 - let expr: Expr = createComparisonExpr(values.conditions[0]); - - for (let i = 1; i < values.conditions.length; i++) { - expr = { - type: ExprType.Logical, - op: values.logicalOperator, - left: expr, - right: createComparisonExpr(values.conditions[i]), - }; - } - - return expr; - }; + const [drawerOpen, setDrawerOpen] = useState(false); const handleDrawerConfirm = async () => { setFormPending(true); @@ -84,10 +35,9 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP try { const newValues = getFormValues(); - const expression = formToExpression(newValues); const newNode = produce(node, (draft) => { draft.config = { - expression, + ...newValues, }; draft.validated = true; }); @@ -100,7 +50,7 @@ const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeP return ( <> setDrawerOpen(true)}>
- +
e.stopPropagation()}> + +
setDrawerOpen(true)}> + {node.config?.expression ? ( +
+
- - setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} - > - -
+ setDrawerOpen(open)} + > + + + ); diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx index 9cbb56cc..3cd92d7b 100644 --- a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -1,36 +1,16 @@ -import { forwardRef, memo, useEffect, useImperativeHandle, useState } from "react"; -import { Button, Card, Form, Input, Select, Radio } from "antd"; -import { PlusOutlined, DeleteOutlined } from "@ant-design/icons"; -import i18n from "@/i18n"; - -import { - WorkflowNodeConfigForCondition, - Expr, - WorkflowNodeIOValueSelector, - ComparisonOperator, - LogicalOperator, - isConstExpr, - isVarExpr, - WorkflowNode, - workflowNodeIOOptions, - WorkflowNodeIoValueType, - ExprType, -} from "@/domain/workflow"; -import { FormInstance } from "antd"; -import { useZustandShallowSelector } from "@/hooks"; -import { useWorkflowStore } from "@/stores/workflow"; +import { forwardRef, memo, useImperativeHandle, useRef } from "react"; import { useTranslation } from "react-i18next"; +import { Form, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; -// 表单内部使用的扁平结构 - 修改后只保留必要字段 -export interface ConditionItem { - leftSelector: string; - operator: ComparisonOperator; - rightValue: string; -} +import { type Expr, type WorkflowNodeConfigForCondition } from "@/domain/workflow"; +import { useAntdForm } from "@/hooks"; + +import ConditionNodeConfigFormExpressionEditor, { type ConditionNodeConfigFormExpressionEditorInstance } from "./ConditionNodeConfigFormExpressionEditor"; export type ConditionNodeConfigFormFieldValues = { - conditions: ConditionItem[]; - logicalOperator: LogicalOperator; + expression?: Expr | undefined; }; export type ConditionNodeConfigFormProps = { @@ -38,9 +18,8 @@ export type ConditionNodeConfigFormProps = { style?: React.CSSProperties; disabled?: boolean; initialValues?: Partial; - onValuesChange?: (values: WorkflowNodeConfigForCondition) => void; - availableSelectors?: WorkflowNodeIOValueSelector[]; nodeId: string; + onValuesChange?: (values: WorkflowNodeConfigForCondition) => void; }; export type ConditionNodeConfigFormInstance = { @@ -49,298 +28,49 @@ export type ConditionNodeConfigFormInstance = { validateFields: FormInstance["validateFields"]; }; -// 初始表单值 const initFormModel = (): ConditionNodeConfigFormFieldValues => { - return { - conditions: [ - { - leftSelector: "", - operator: "==", - rightValue: "", - }, - ], - logicalOperator: LogicalOperator.And, - }; -}; - -// 递归提取表达式中的条件项 -const expressionToForm = (expr?: Expr): ConditionNodeConfigFormFieldValues => { - if (!expr) return initFormModel(); - - const conditions: ConditionItem[] = []; - let logicalOp: LogicalOperator = LogicalOperator.And; - - const extractComparisons = (expr: Expr): void => { - if (expr.type === ExprType.Compare) { - // 确保左侧是变量,右侧是常量 - if (isVarExpr(expr.left) && isConstExpr(expr.right)) { - conditions.push({ - leftSelector: `${expr.left.selector.id}#${expr.left.selector.name}#${expr.left.selector.type}`, - operator: expr.op, - rightValue: String(expr.right.value), - }); - } - } else if (expr.type === ExprType.Logical) { - logicalOp = expr.op; - extractComparisons(expr.left); - extractComparisons(expr.right); - } - }; - - extractComparisons(expr); - - return { - conditions: conditions.length > 0 ? conditions : initFormModel().conditions, - logicalOperator: logicalOp, - }; -}; - -// 根据变量类型获取适当的操作符选项 -const getOperatorsByType = (type: string): { value: ComparisonOperator; label: string }[] => { - switch (type) { - case "number": - case "string": - return [ - { value: "==", label: i18n.t("workflow_node.condition.form.comparison.equal") }, - { value: "!=", label: i18n.t("workflow_node.condition.form.comparison.not_equal") }, - { value: ">", label: i18n.t("workflow_node.condition.form.comparison.greater_than") }, - { value: ">=", label: i18n.t("workflow_node.condition.form.comparison.greater_than_or_equal") }, - { value: "<", label: i18n.t("workflow_node.condition.form.comparison.less_than") }, - { value: "<=", label: i18n.t("workflow_node.condition.form.comparison.less_than_or_equal") }, - ]; - case "boolean": - return [{ value: "is", label: i18n.t("workflow_node.condition.form.comparison.is") }]; - default: - return []; - } -}; - -// 从选择器字符串中提取变量类型 -const getVariableTypeFromSelector = (selector: string): string => { - if (!selector) return "string"; - - // 假设选择器格式为 "id#name#type" - const parts = selector.split("#"); - if (parts.length >= 3) { - return parts[2].toLowerCase() || "string"; - } - return "string"; + return {}; }; const ConditionNodeConfigForm = forwardRef( - ({ className, style, disabled, initialValues, onValuesChange, nodeId }, ref) => { + ({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => { const { t } = useTranslation(); - const prefix = "workflow_node.condition.form"; - const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); + const formSchema = z.object({ + expression: z.any().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + const { form: formInst, formProps } = useAntdForm({ + name: "workflowNodeConditionConfigForm", + initialValues: initialValues ?? initFormModel(), + }); - const [form] = Form.useForm(); - const [formModel, setFormModel] = useState(initFormModel()); + const editorRef = useRef(null); - const [previousNodes, setPreviousNodes] = useState([]); - useEffect(() => { - const previousNodes = getWorkflowOuptutBeforeId(nodeId); - setPreviousNodes(previousNodes); - }, [nodeId]); - - // 初始化表单值 - useEffect(() => { - if (initialValues?.expression) { - const formValues = expressionToForm(initialValues.expression); - form.setFieldsValue(formValues); - setFormModel(formValues); - } - }, [form, initialValues]); - - // 公开表单方法 - useImperativeHandle( - ref, - () => ({ - getFieldsValue: form.getFieldsValue, - resetFields: form.resetFields, - validateFields: form.validateFields, - }), - [form] - ); - - // 表单值变更处理 - const handleFormChange = (_: undefined, values: ConditionNodeConfigFormFieldValues) => { - setFormModel(values); - - if (onValuesChange) { - // 将表单值转换为表达式 - const expression = formToExpression(values); - onValuesChange({ expression }); - } + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); }; + useImperativeHandle(ref, () => { + return { + getFieldsValue: formInst.getFieldsValue, + resetFields: formInst.resetFields, + validateFields: (nameList, config) => { + const t1 = formInst.validateFields(nameList, config); + const t2 = editorRef.current!.validate(); + return Promise.all([t1, t2]).then(() => t1); + }, + } as ConditionNodeConfigFormInstance; + }); + return ( -
- - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - 1 ? - - - )} - - - {formModel.conditions && formModel.conditions.length > 1 && ( - - - {t(`${prefix}.logical_operator.and`)} - {t(`${prefix}.logical_operator.or`)} - - - )} + + + +
); } ); -// 表单值转换为表达式结构 (需要添加) -const formToExpression = (values: ConditionNodeConfigFormFieldValues): Expr => { - const createComparisonExpr = (condition: ConditionItem): Expr => { - const [id, name, typeStr] = condition.leftSelector.split("#"); - - const type = typeStr as WorkflowNodeIoValueType; - - const left: Expr = { - type: ExprType.Var, - selector: { id, name, type }, - }; - - const right: Expr = { - type: ExprType.Const, - value: condition.rightValue, - valueType: type, - }; - - return { - type: ExprType.Compare, - op: condition.operator, - left, - right, - }; - }; - - // 如果只有一个条件,直接返回比较表达式 - if (values.conditions.length === 1) { - return createComparisonExpr(values.conditions[0]); - } - - // 多个条件,通过逻辑运算符连接 - let expr: Expr = createComparisonExpr(values.conditions[0]); - - for (let i = 1; i < values.conditions.length; i++) { - expr = { - type: ExprType.Logical, - op: values.logicalOperator, - left: expr, - right: createComparisonExpr(values.conditions[i]), - }; - } - - return expr; -}; - export default memo(ConditionNodeConfigForm); diff --git a/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx b/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx new file mode 100644 index 00000000..1696d46c --- /dev/null +++ b/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx @@ -0,0 +1,400 @@ +import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CloseOutlined as CloseOutlinedIcon, PlusOutlined } from "@ant-design/icons"; +import { useControllableValue } from "ahooks"; +import { Button, Form, Input, Radio, Select, theme } from "antd"; + +import Show from "@/components/Show"; +import type { Expr, ExprComparisonOperator, ExprLogicalOperator, ExprValue, ExprValueSelector, ExprValueType } from "@/domain/workflow"; +import { ExprType } from "@/domain/workflow"; +import { useAntdFormName, useZustandShallowSelector } from "@/hooks"; +import { useWorkflowStore } from "@/stores/workflow"; + +export type ConditionNodeConfigFormExpressionEditorProps = { + className?: string; + style?: React.CSSProperties; + defaultValue?: Expr; + disabled?: boolean; + nodeId: string; + value?: Expr; + onChange?: (value: Expr) => void; +}; + +export type ConditionNodeConfigFormExpressionEditorInstance = { + validate: () => Promise; +}; + +// 表单内部使用的扁平结构 +type ConditionItem = { + // 选择器,格式为 "${nodeId}#${outputName}#${valueType}" + // 将 [ExprValueSelector] 转为字符串形式,以便于结构化存储。 + leftSelector?: string; + // 比较运算符。 + operator?: ExprComparisonOperator; + // 值。 + // 将 [ExprValue] 转为字符串形式,以便于结构化存储。 + rightValue?: string; +}; + +type ConditionFormValues = { + conditions: ConditionItem[]; + logicalOperator: ExprLogicalOperator; +}; + +const initFormModel = (): ConditionFormValues => { + return { + conditions: [{}], + logicalOperator: "and", + }; +}; + +const exprToFormValues = (expr?: Expr): ConditionFormValues => { + if (!expr) return initFormModel(); + + const conditions: ConditionItem[] = []; + let logicalOp: ExprLogicalOperator = "and"; + + const extractExpr = (expr: Expr): void => { + if (expr.type === ExprType.Comparison) { + if (expr.left.type == ExprType.Variant && expr.right.type == ExprType.Constant) { + conditions.push({ + leftSelector: expr.left.selector?.id != null ? `${expr.left.selector.id}#${expr.left.selector.name}#${expr.left.selector.type}` : undefined, + operator: expr.operator != null ? expr.operator : undefined, + rightValue: expr.right?.value != null ? String(expr.right.value) : undefined, + }); + } else { + console.warn("[certimate] invalid comparison expression: left must be a variant and right must be a constant", expr); + } + } else if (expr.type === ExprType.Logical) { + logicalOp = expr.operator || "and"; + extractExpr(expr.left); + extractExpr(expr.right); + } + }; + + extractExpr(expr); + + return { + conditions: conditions, + logicalOperator: logicalOp, + }; +}; + +const formValuesToExpr = (values: ConditionFormValues): Expr | undefined => { + const wrapExpr = (condition: ConditionItem): Expr => { + const [id, name, type] = (condition.leftSelector?.split("#") ?? ["", "", ""]) as [string, string, ExprValueType]; + const valid = !!id && !!name && !!type; + + const left: Expr = { + type: ExprType.Variant, + selector: valid + ? { + id: id, + name: name, + type: type, + } + : ({} as ExprValueSelector), + }; + + const right: Expr = { + type: ExprType.Constant, + value: condition.rightValue!, + valueType: type, + }; + + return { + type: ExprType.Comparison, + operator: condition.operator!, + left, + right, + }; + }; + + if (values.conditions.length === 0) { + return undefined; + } + + // 只有一个条件时,直接返回比较表达式 + if (values.conditions.length === 1) { + const { leftSelector, operator, rightValue } = values.conditions[0]; + if (!leftSelector || !operator || !rightValue) { + return undefined; + } + return wrapExpr(values.conditions[0]); + } + + // 多个条件时,通过逻辑运算符连接 + let expr: Expr = wrapExpr(values.conditions[0]); + for (let i = 1; i < values.conditions.length; i++) { + expr = { + type: ExprType.Logical, + operator: values.logicalOperator, + left: expr, + right: wrapExpr(values.conditions[i]), + }; + } + return expr; +}; + +const ConditionNodeConfigFormExpressionEditor = forwardRef( + ({ className, style, disabled, nodeId, ...props }, ref) => { + const { t } = useTranslation(); + + const { token: themeToken } = theme.useToken(); + + const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); + + const [value, setValue] = useControllableValue(props, { + valuePropName: "value", + defaultValuePropName: "defaultValue", + trigger: "onChange", + }); + + const [formInst] = Form.useForm(); + const formName = useAntdFormName({ form: formInst, name: "workflowNodeConditionConfigFormExpressionEditorForm" }); + const [formModel, setFormModel] = useState(initFormModel()); + + useEffect(() => { + if (value) { + const formValues = exprToFormValues(value); + formInst.setFieldsValue(formValues); + setFormModel(formValues); + } else { + formInst.resetFields(); + setFormModel(initFormModel()); + } + }, [value]); + + const ciSelectorCandidates = useMemo(() => { + const previousNodes = getWorkflowOuptutBeforeId(nodeId); + return previousNodes + .map((node) => { + const group = { + label: node.name, + options: Array<{ label: string; value: string }>(), + }; + + for (const output of node.outputs ?? []) { + switch (output.type) { + case "certificate": + group.options.push({ + label: `${output.label} - ${t("workflow.variables.selector.validity.label")}`, + value: `${node.id}#${output.name}.validity#boolean`, + }); + group.options.push({ + label: `${output.label} - ${t("workflow.variables.selector.days_left.label")}`, + value: `${node.id}#${output.name}.daysLeft#number`, + }); + break; + + default: + group.options.push({ + label: `${output.label}`, + value: `${node.id}#${output.name}#${output.type}`, + }); + console.warn("[certimate] invalid workflow output type in condition expressions", output); + break; + } + } + + return group; + }) + .filter((item) => item.options.length > 0); + }, [nodeId]); + + const getValueTypeBySelector = (selector: string): ExprValueType | undefined => { + if (!selector) return; + + const parts = selector.split("#"); + if (parts.length >= 3) { + return parts[2].toLowerCase() as ExprValueType; + } + }; + + const getOperatorsBySelector = (selector: string): { value: ExprComparisonOperator; label: string }[] => { + const valueType = getValueTypeBySelector(selector); + return getOperatorsByValueType(valueType!); + }; + + const getOperatorsByValueType = (valueType: ExprValue): { value: ExprComparisonOperator; label: string }[] => { + switch (valueType) { + case "number": + return [ + { value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.label") }, + { value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.label") }, + { value: "gt", label: t("workflow_node.condition.form.expression.operator.option.gt.label") }, + { value: "gte", label: t("workflow_node.condition.form.expression.operator.option.gte.label") }, + { value: "lt", label: t("workflow_node.condition.form.expression.operator.option.lt.label") }, + { value: "lte", label: t("workflow_node.condition.form.expression.operator.option.lte.label") }, + ]; + + case "string": + return [ + { value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.label") }, + { value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.label") }, + ]; + + case "boolean": + return [ + { value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.alias_is_label") }, + { value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.alias_not_label") }, + ]; + + default: + return []; + } + }; + + const handleFormChange = (_: undefined, values: ConditionFormValues) => { + setValue(formValuesToExpr(values)); + }; + + useImperativeHandle(ref, () => { + return { + validate: async () => { + await formInst.validateFields(); + }, + } as ConditionNodeConfigFormExpressionEditorInstance; + }); + + return ( +
+ 1}> + + + {t("workflow_node.condition.form.expression.logical_operator.option.and.label")} + {t("workflow_node.condition.form.expression.logical_operator.option.or.label")} + + + + + + {(fields, { add, remove }) => ( +
+ {fields.map(({ key, name: index, ...rest }) => ( +
+ {/* 左:变量选择器 */} + + + + ); + }} + + + {/* 右:输入控件,根据变量类型决定组件 */} + { + return prevValues.conditions?.[index]?.leftSelector !== currentValues.conditions?.[index]?.leftSelector; + }} + > + {({ getFieldValue }) => { + const leftSelector = getFieldValue(["conditions", index, "leftSelector"]); + const valueType = getValueTypeBySelector(leftSelector); + + return ( + + {valueType === "string" ? ( + + ) : valueType === "number" ? ( + + ) : valueType === "boolean" ? ( + + ) : ( + + )} + + ); + }} + + +
+ ))} + + + + +
+ )} +
+
+ ); + } +); + +export default ConditionNodeConfigFormExpressionEditor; diff --git a/ui/src/components/workflow/node/DeployNode.tsx b/ui/src/components/workflow/node/DeployNode.tsx index b04516fb..f495c040 100644 --- a/ui/src/components/workflow/node/DeployNode.tsx +++ b/ui/src/components/workflow/node/DeployNode.tsx @@ -24,10 +24,10 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForDeploy; const [drawerOpen, setDrawerOpen] = useState(false); const [drawerFooterShow, setDrawerFooterShow] = useState(true); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForDeploy; useEffect(() => { setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider); @@ -86,8 +86,9 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => { { setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider); setDrawerOpen(open); }} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 0443327e..33fefcf0 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -1,7 +1,7 @@ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons"; -import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography } from "antd"; +import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography, theme } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -11,7 +11,7 @@ import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPi import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx"; import Show from "@/components/Show"; import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider"; -import { type WorkflowNode, type WorkflowNodeConfigForDeploy } from "@/domain/workflow"; +import { type WorkflowNodeConfigForDeploy, WorkflowNodeType } from "@/domain/workflow"; import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; @@ -125,14 +125,9 @@ const DeployNodeConfigForm = forwardRef { const { t } = useTranslation(); - const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); + const { token: themeToken } = theme.useToken(); - // TODO: 优化此处逻辑 - const [previousNodes, setPreviousNodes] = useState([]); - useEffect(() => { - const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate"); - setPreviousNodes(previousNodes); - }, [nodeId]); + const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); const formSchema = z.object({ certificate: z @@ -170,6 +165,24 @@ const DeployNodeConfigForm = forwardRef { + const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate"); + return previousNodes + .filter((node) => node.type === WorkflowNodeType.Apply || node.type === WorkflowNodeType.Upload) + .map((item) => { + return { + label: item.name, + options: (item.outputs ?? [])?.map((output) => { + return { + label: output.label, + value: `${item.id}#${output.name}`, + }; + }), + }; + }) + .filter((group) => group.options.length > 0); + }, [nodeId]); + const [nestedFormInst] = Form.useForm(); const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeDeployConfigFormProviderConfigForm" }); const nestedFormEl = useMemo(() => { @@ -487,17 +500,15 @@ const DeployNodeConfigForm = forwardRef} > ({ diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx index d64e6eba..36d663b5 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx @@ -18,7 +18,7 @@ export type DeployNodeConfigFormWangsuCDNConfigProps = { onValuesChange?: (values: DeployNodeConfigFormWangsuCDNConfigFieldValues) => void; }; -const MULTIPLE_INPUT_DELIMITER = ";"; +const MULTIPLE_INPUT_SEPARATOR = ";"; const initFormModel = (): DeployNodeConfigFormWangsuCDNConfigFieldValues => { return { @@ -42,7 +42,7 @@ const DeployNodeConfigFormWangsuCDNConfig = ({ .refine((v) => { if (!v) return false; return String(v) - .split(MULTIPLE_INPUT_DELIMITER) + .split(MULTIPLE_INPUT_SEPARATOR) .every((e) => validDomainName(e)); }, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")), }); diff --git a/ui/src/components/workflow/node/MonitorNode.tsx b/ui/src/components/workflow/node/MonitorNode.tsx index 68feb842..39fb159e 100644 --- a/ui/src/components/workflow/node/MonitorNode.tsx +++ b/ui/src/components/workflow/node/MonitorNode.tsx @@ -23,9 +23,9 @@ const MonitorNode = ({ node, disabled }: MonitorNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Monitor) { @@ -74,12 +74,12 @@ const MonitorNode = ({ node, disabled }: MonitorNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/NotifyNode.tsx b/ui/src/components/workflow/node/NotifyNode.tsx index da48552d..89326b50 100644 --- a/ui/src/components/workflow/node/NotifyNode.tsx +++ b/ui/src/components/workflow/node/NotifyNode.tsx @@ -25,9 +25,9 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Notify) { @@ -82,12 +82,12 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/StartNode.tsx b/ui/src/components/workflow/node/StartNode.tsx index 900793fa..4b920bd9 100644 --- a/ui/src/components/workflow/node/StartNode.tsx +++ b/ui/src/components/workflow/node/StartNode.tsx @@ -23,9 +23,9 @@ const StartNode = ({ node, disabled }: StartNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Start) { @@ -83,12 +83,12 @@ const StartNode = ({ node, disabled }: StartNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/UploadNode.tsx b/ui/src/components/workflow/node/UploadNode.tsx index 0197a8c4..6936f147 100644 --- a/ui/src/components/workflow/node/UploadNode.tsx +++ b/ui/src/components/workflow/node/UploadNode.tsx @@ -23,9 +23,9 @@ const UploadNode = ({ node, disabled }: UploadNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Upload) { @@ -74,12 +74,12 @@ const UploadNode = ({ node, disabled }: UploadNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/_SharedNode.tsx b/ui/src/components/workflow/node/_SharedNode.tsx index 44c894ef..72f4b967 100644 --- a/ui/src/components/workflow/node/_SharedNode.tsx +++ b/ui/src/components/workflow/node/_SharedNode.tsx @@ -33,7 +33,7 @@ const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitlePr const handleBlur = (e: React.FocusEvent) => { const oldName = node.name; - const newName = e.target.innerText.trim().substring(0, 64) || oldName; + const newName = e.target.innerText.replaceAll("\r", "").replaceAll("\n", "").trim().substring(0, 64) || oldName; if (oldName === newName) { return; } @@ -45,9 +45,16 @@ const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitlePr ); }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.code === "Enter") { + e.preventDefault(); + e.currentTarget.blur(); + } + }; + return (
-
+
{node.name}
@@ -91,7 +98,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, const handleRenameConfirm = async () => { const oldName = node.name; - const newName = nameRef.current?.trim()?.substring(0, 64) || oldName; + const newName = nameRef.current?.replaceAll("\r", "")?.replaceAll("\n", "").trim()?.substring(0, 64) || oldName; if (oldName === newName) { return; } @@ -195,7 +202,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, }; // #endregion -// #region Wrapper +// #region Block type SharedNodeBlockProps = SharedNodeProps & { children: React.ReactNode; onClick?: (e: React.MouseEvent) => void; @@ -245,7 +252,7 @@ type SharedNodeEditDrawerProps = SharedNodeProps & { pending?: boolean; onOpenChange?: (open: boolean) => void; onConfirm: () => void | Promise; - getFormValues: () => NonNullable; + getConfigNewValues: () => NonNullable; // 用于获取节点配置的新值,以便在抽屉关闭前进行对比,决定是否提示保存 }; const SharedNodeConfigDrawer = ({ @@ -256,7 +263,7 @@ const SharedNodeConfigDrawer = ({ loading, pending, onConfirm, - getFormValues, + getConfigNewValues, ...props }: SharedNodeEditDrawerProps) => { const { t } = useTranslation(); @@ -284,7 +291,7 @@ const SharedNodeConfigDrawer = ({ if (pending) return; const oldValues = JSON.parse(JSON.stringify(node.config ?? {})); - const newValues = JSON.parse(JSON.stringify(getFormValues())); + const newValues = JSON.parse(JSON.stringify(getConfigNewValues())); const changed = !isEqual(oldValues, {}) && !isEqual(oldValues, newValues); const { promise, resolve, reject } = Promise.withResolvers(); diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 4dea7f64..d3b9f822 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -69,7 +69,7 @@ const workflowNodeTypeDefaultInputs: Map = n name: "certificate", type: "certificate", required: true, - label: i18n.t("workflow.variables.certificate.label"), + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -84,7 +84,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: i18n.t("workflow.variables.certificate.label"), + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -95,7 +95,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: i18n.t("workflow.variables.certificate.label"), + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -106,7 +106,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: i18n.t("workflow.variables.certificate.label"), + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -188,7 +188,7 @@ export type WorkflowNodeConfigForNotify = { }; export type WorkflowNodeConfigForCondition = { - expression: Expr; + expression?: Expr; }; export type WorkflowNodeConfigForBranch = never; @@ -204,96 +204,35 @@ export type WorkflowNodeIO = { valueSelector?: WorkflowNodeIOValueSelector; }; -export const VALUE_TYPES = Object.freeze({ - STRING: "string", - NUMBER: "number", - BOOLEAN: "boolean", -} as const); - -export type WorkflowNodeIoValueType = (typeof VALUE_TYPES)[keyof typeof VALUE_TYPES]; - -export type WorkflowNodeIOValueSelector = { - id: string; - name: string; - type: WorkflowNodeIoValueType; -}; - -type WorkflowNodeIOOptions = { - label: string; - value: string; -}; - -export const workflowNodeIOOptions = (node: WorkflowNode) => { - const rs = { - label: node.name, - options: Array(), - }; - - if (node.outputs) { - for (const output of node.outputs) { - switch (output.type) { - case "certificate": - rs.options.push({ - label: `${node.name} - ${output.label} - ${i18n.t("workflow.variables.is_validated.label")}`, - value: `${node.id}#${output.name}.validated#boolean`, - }); - - rs.options.push({ - label: `${node.name} - ${output.label} - ${i18n.t("workflow.variables.days_left.label")}`, - value: `${node.id}#${output.name}.daysLeft#number`, - }); - break; - default: - rs.options.push({ - label: `${node.name} - ${output.label}`, - value: `${node.id}#${output.name}#${output.type}`, - }); - break; - } - } - } - - return rs; -}; - +export type WorkflowNodeIOValueSelector = ExprValueSelector; // #endregion -// #region Condition expression - -export type Value = string | number | boolean; - -export type ComparisonOperator = ">" | "<" | ">=" | "<=" | "==" | "!=" | "is"; - -export enum LogicalOperator { - And = "and", - Or = "or", - Not = "not", -} - +// #region Expression export enum ExprType { - Const = "const", - Var = "var", - Compare = "compare", + Constant = "const", + Variant = "var", + Comparison = "comparison", Logical = "logical", Not = "not", } -export type ConstExpr = { type: ExprType.Const; value: string; valueType: WorkflowNodeIoValueType }; -export type VarExpr = { type: ExprType.Var; selector: WorkflowNodeIOValueSelector }; -export type CompareExpr = { type: ExprType.Compare; op: ComparisonOperator; left: Expr; right: Expr }; -export type LogicalExpr = { type: ExprType.Logical; op: LogicalOperator; left: Expr; right: Expr }; +export type ExprValue = string | number | boolean; +export type ExprValueType = "string" | "number" | "boolean"; +export type ExprValueSelector = { + id: string; + name: string; + type: ExprValueType; +}; + +export type ExprComparisonOperator = "gt" | "gte" | "lt" | "lte" | "eq" | "neq"; +export type ExprLogicalOperator = "and" | "or" | "not"; + +export type ConstantExpr = { type: ExprType.Constant; value: string; valueType: ExprValueType }; +export type VariantExpr = { type: ExprType.Variant; selector: ExprValueSelector }; +export type ComparisonExpr = { type: ExprType.Comparison; operator: ExprComparisonOperator; left: Expr; right: Expr }; +export type LogicalExpr = { type: ExprType.Logical; operator: ExprLogicalOperator; left: Expr; right: Expr }; export type NotExpr = { type: ExprType.Not; expr: Expr }; - -export type Expr = ConstExpr | VarExpr | CompareExpr | LogicalExpr | NotExpr; - -export const isConstExpr = (expr: Expr): expr is ConstExpr => { - return expr.type === ExprType.Const; -}; - -export const isVarExpr = (expr: Expr): expr is VarExpr => { - return expr.type === ExprType.Var; -}; - +export type Expr = ConstantExpr | VariantExpr | ComparisonExpr | LogicalExpr | NotExpr; // #endregion const isBranchLike = (node: WorkflowNode) => { @@ -352,8 +291,8 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {} switch (nodeType) { case WorkflowNodeType.Apply: case WorkflowNodeType.Upload: - case WorkflowNodeType.Deploy: case WorkflowNodeType.Monitor: + case WorkflowNodeType.Deploy: { node.inputs = workflowNodeTypeDefaultInputs.get(nodeType); node.outputs = workflowNodeTypeDefaultOutputs.get(nodeType); @@ -545,20 +484,24 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd }); }; -const typeEqual = (a: WorkflowNodeIO, t: string) => { - if (t === "all") { - return true; - } - if (a.type === t) { - return true; - } - return false; -}; - -export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: string = "all"): WorkflowNode[] => { +export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, typeFilter?: string | string[]): WorkflowNode[] => { // 某个分支的节点,不应该能获取到相邻分支上节点的输出 const outputs: WorkflowNode[] = []; + const filter = (io: WorkflowNodeIO) => { + if (typeFilter == null) { + return true; + } + + if (Array.isArray(typeFilter) && typeFilter.includes(io.type)) { + return true; + } else if (io.type === typeFilter) { + return true; + } + + return false; + }; + const traverse = (current: WorkflowNode, output: WorkflowNode[]) => { if (!current) { return false; @@ -567,10 +510,10 @@ export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: return true; } - if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => typeEqual(io, type))) { + if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => filter(io))) { output.push({ ...current, - outputs: current.outputs.filter((io) => typeEqual(io, type)), + outputs: current.outputs.filter((io) => filter(io)), }); } diff --git a/ui/src/domain/workflowExpr.ts b/ui/src/domain/workflowExpr.ts new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/ui/src/domain/workflowExpr.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/i18n/locales/en/index.ts b/ui/src/i18n/locales/en/index.ts index f038efc7..4eaeced5 100644 --- a/ui/src/i18n/locales/en/index.ts +++ b/ui/src/i18n/locales/en/index.ts @@ -8,6 +8,7 @@ import nlsSettings from "./nls.settings.json"; import nlsWorkflow from "./nls.workflow.json"; import nlsWorkflowNodes from "./nls.workflow.nodes.json"; import nlsWorkflowRuns from "./nls.workflow.runs.json"; +import nlsWorkflowVars from "./nls.workflow.vars.json"; export default Object.freeze({ ...nlsCommon, @@ -16,8 +17,9 @@ export default Object.freeze({ ...nlsSettings, ...nlsProvider, ...nlsAccess, + ...nlsCertificate, ...nlsWorkflow, ...nlsWorkflowNodes, ...nlsWorkflowRuns, - ...nlsCertificate, + ...nlsWorkflowVars, }); diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index cdf722a0..b4f9d7e6 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -53,9 +53,5 @@ "workflow.detail.orchestration.action.run": "Run", "workflow.detail.orchestration.action.run.confirm": "You have unreleased changes. Do you really want to run this workflow based on the latest released version?", "workflow.detail.orchestration.action.run.prompt": "Running... Please check the history later", - "workflow.detail.runs.tab": "History runs", - - "workflow.variables.is_validated.label": "Is valid", - "workflow.variables.days_left.label": "Days left", - "workflow.variables.certificate.label": "Certificate" + "workflow.detail.runs.tab": "History runs" } diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 5b6c870c..626e9b68 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -871,31 +871,32 @@ "workflow_node.end.label": "End", "workflow_node.end.default_name": "End", - "workflow_node.branch.label": "Parallel branch", - "workflow_node.branch.default_name": "Parallel", + "workflow_node.branch.label": "Parallel/Conditional branch", + "workflow_node.branch.default_name": "Branch", "workflow_node.condition.label": "Branch", "workflow_node.condition.default_name": "Branch", - "workflow_node.condition.form.variable.placeholder": "Please select variable", - "workflow_node.condition.form.variable.errmsg": "Please select variable", - "workflow_node.condition.form.operator.errmsg": "Please select operator", - "workflow_node.condition.form.value.errmsg": "Please enter value", - "workflow_node.condition.form.value.string.placeholder": "Please enter value", - "workflow_node.condition.form.value.number.placeholder": "Please enter value", - "workflow_node.condition.form.value.boolean.placeholder": "Please select value", - "workflow_node.condition.form.value.boolean.true": "True", - "workflow_node.condition.form.value.boolean.false": "False", - "workflow_node.condition.form.add_condition.button": "Add condition", - "workflow_node.condition.form.logical_operator.label": "Logical operator", - "workflow_node.condition.form.logical_operator.and": "Meet all conditions (AND)", - "workflow_node.condition.form.logical_operator.or": "Meet any condition (OR)", - "workflow_node.condition.form.comparison.equal": "Equal", - "workflow_node.condition.form.comparison.not_equal": "Not equal", - "workflow_node.condition.form.comparison.greater_than": "Greater than", - "workflow_node.condition.form.comparison.greater_than_or_equal": "Greater than or equal", - "workflow_node.condition.form.comparison.less_than": "Less than", - "workflow_node.condition.form.comparison.less_than_or_equal": "Less than or equal", - "workflow_node.condition.form.comparison.is": "Is", + "workflow_node.condition.form.expression.label": "Conditions to enter the branch", + "workflow_node.condition.form.expression.logical_operator.errmsg": "Please select logical operator of conditions", + "workflow_node.condition.form.expression.logical_operator.option.and.label": "Meeting all of the conditions (AND)", + "workflow_node.condition.form.expression.logical_operator.option.or.label": "Meeting any of the conditions (OR)", + "workflow_node.condition.form.expression.variable.placeholder": "Please select", + "workflow_node.condition.form.expression.variable.errmsg": "Please select variable", + "workflow_node.condition.form.expression.operator.placeholder": "Please select", + "workflow_node.condition.form.expression.operator.errmsg": "Please select operator", + "workflow_node.condition.form.expression.operator.option.eq.label": "equal to", + "workflow_node.condition.form.expression.operator.option.eq.alias_is_label": "is", + "workflow_node.condition.form.expression.operator.option.neq.label": "not equal to", + "workflow_node.condition.form.expression.operator.option.neq.alias_not_label": "is not", + "workflow_node.condition.form.expression.operator.option.gt.label": "greater than", + "workflow_node.condition.form.expression.operator.option.gte.label": "greater than or equal to", + "workflow_node.condition.form.expression.operator.option.lt.label": "less than", + "workflow_node.condition.form.expression.operator.option.lte.label": "less than or equal to", + "workflow_node.condition.form.expression.value.placeholder": "Please enter", + "workflow_node.condition.form.expression.value.errmsg": "Please enter value", + "workflow_node.condition.form.expression.value.option.true.label": "True", + "workflow_node.condition.form.expression.value.option.false.label": "False", + "workflow_node.condition.form.expression.add_condition.button": "Add condition", "workflow_node.execute_result_branch.label": "Execution result branch", "workflow_node.execute_result_branch.default_name": "Execution result branch", diff --git a/ui/src/i18n/locales/en/nls.workflow.vars.json b/ui/src/i18n/locales/en/nls.workflow.vars.json new file mode 100644 index 00000000..a96d8ba5 --- /dev/null +++ b/ui/src/i18n/locales/en/nls.workflow.vars.json @@ -0,0 +1,6 @@ +{ + "workflow.variables.type.certificate.label": "Certificate", + + "workflow.variables.selector.validity.label": "Validity", + "workflow.variables.selector.days_left.label": "Days left" +} diff --git a/ui/src/i18n/locales/zh/index.ts b/ui/src/i18n/locales/zh/index.ts index f038efc7..4eaeced5 100644 --- a/ui/src/i18n/locales/zh/index.ts +++ b/ui/src/i18n/locales/zh/index.ts @@ -8,6 +8,7 @@ import nlsSettings from "./nls.settings.json"; import nlsWorkflow from "./nls.workflow.json"; import nlsWorkflowNodes from "./nls.workflow.nodes.json"; import nlsWorkflowRuns from "./nls.workflow.runs.json"; +import nlsWorkflowVars from "./nls.workflow.vars.json"; export default Object.freeze({ ...nlsCommon, @@ -16,8 +17,9 @@ export default Object.freeze({ ...nlsSettings, ...nlsProvider, ...nlsAccess, + ...nlsCertificate, ...nlsWorkflow, ...nlsWorkflowNodes, ...nlsWorkflowRuns, - ...nlsCertificate, + ...nlsWorkflowVars, }); diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index e86e796a..46cdc228 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -53,9 +53,5 @@ "workflow.detail.orchestration.action.run": "执行", "workflow.detail.orchestration.action.run.confirm": "你有尚未发布的更改。确定要以最近一次发布的版本继续执行吗?", "workflow.detail.orchestration.action.run.prompt": "执行中……请稍后查看执行历史", - "workflow.detail.runs.tab": "执行历史", - - "workflow.variables.is_validated.label": "是否有效", - "workflow.variables.days_left.label": "剩余天数", - "workflow.variables.certificate.label": "证书" + "workflow.detail.runs.tab": "执行历史" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 0d7ce68c..7710e386 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -3,7 +3,7 @@ "workflow_node.branch.add_node": "添加节点", "workflow_node.action.rename_node": "重命名", "workflow_node.action.remove_node": "删除节点", - "workflow_node.action.add_branch": "添加并行分支", + "workflow_node.action.add_branch": "添加分支", "workflow_node.action.rename_branch": "重命名", "workflow_node.action.remove_branch": "删除分支", @@ -707,7 +707,7 @@ "workflow_node.deploy.form.ucloud_us3_domain.label": "优刻得 US3 自定义域名", "workflow_node.deploy.form.ucloud_us3_domain.placeholder": "请输入优刻得 US3 自定义域名", "workflow_node.deploy.form.ucloud_us3_domain.tooltip": "这是什么?请参阅 https://console.ucloud.cn/ufile", - "workflow_node.deploy.form.unicloud_webhost.guide": "小贴士:由于 uniCloud 未提供相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇 uniCloud 接口变更,请到 GitHub 发起 Issue 告知。", + "workflow_node.deploy.form.unicloud_webhost.guide": "小贴士:由于 uniCloud 未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇 uniCloud 接口变更,请到 GitHub 发起 Issue 告知。", "workflow_node.deploy.form.unicloud_webhost_space_provider.label": "uniCloud 服务空间提供商", "workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder": "请选择 uniCloud 服务空间提供商", "workflow_node.deploy.form.unicloud_webhost_space_provider.option.aliyun.label": "阿里云", @@ -717,11 +717,11 @@ "workflow_node.deploy.form.unicloud_webhost_space_id.tooltip": "这是什么?请参阅 https://doc.dcloud.net.cn/uniCloud/concepts/space.html", "workflow_node.deploy.form.unicloud_webhost_domain.label": "uniCloud 前端网页托管网站域名", "workflow_node.deploy.form.unicloud_webhost_domain.placeholder": "请输入 uniCloud 前端网页托管网站域名", - "workflow_node.deploy.form.upyun_cdn.guide": "小贴士:由于又拍云未提供相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", + "workflow_node.deploy.form.upyun_cdn.guide": "小贴士:由于又拍云未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", "workflow_node.deploy.form.upyun_cdn_domain.label": "又拍云 CDN 加速域名", "workflow_node.deploy.form.upyun_cdn_domain.placeholder": "请输入又拍云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.upyun_cdn_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/cdn/", - "workflow_node.deploy.form.upyun_file.guide": "小贴士:由于又拍云未提供相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", + "workflow_node.deploy.form.upyun_file.guide": "小贴士:由于又拍云未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", "workflow_node.deploy.form.upyun_file_domain.label": "又拍云云存储加速域名", "workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储加速域名", "workflow_node.deploy.form.upyun_file_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/file/", @@ -870,31 +870,32 @@ "workflow_node.end.label": "结束", "workflow_node.end.default_name": "结束", - "workflow_node.branch.label": "并行分支", - "workflow_node.branch.default_name": "并行", + "workflow_node.branch.label": "并行/条件分支", + "workflow_node.branch.default_name": "分支", "workflow_node.condition.label": "分支", "workflow_node.condition.default_name": "分支", - "workflow_node.condition.form.variable.placeholder": "选择变量", - "workflow_node.condition.form.variable.errmsg": "请选择变量", - "workflow_node.condition.form.operator.errmsg": "请选择操作符", - "workflow_node.condition.form.value.errmsg": "请输入值", - "workflow_node.condition.form.value.string.placeholder": "输入值", - "workflow_node.condition.form.value.number.placeholder": "输入数值", - "workflow_node.condition.form.value.boolean.placeholder": "选择值", - "workflow_node.condition.form.value.boolean.true": "是", - "workflow_node.condition.form.value.boolean.false": "否", - "workflow_node.condition.form.add_condition.button": "添加条件", - "workflow_node.condition.form.logical_operator.label": "条件逻辑", - "workflow_node.condition.form.logical_operator.and": "满足所有条件 (AND)", - "workflow_node.condition.form.logical_operator.or": "满足任一条件 (OR)", - "workflow_node.condition.form.comparison.equal": "等于", - "workflow_node.condition.form.comparison.not_equal": "不等于", - "workflow_node.condition.form.comparison.greater_than": "大于", - "workflow_node.condition.form.comparison.greater_than_or_equal": "大于等于", - "workflow_node.condition.form.comparison.less_than": "小于", - "workflow_node.condition.form.comparison.less_than_or_equal": "小于等于", - "workflow_node.condition.form.comparison.is": "为", + "workflow_node.condition.form.expression.label": "分支进入条件", + "workflow_node.condition.form.expression.logical_operator.errmsg": "请选择条件组合方式", + "workflow_node.condition.form.expression.logical_operator.option.and.label": "满足以下所有条件 (AND)", + "workflow_node.condition.form.expression.logical_operator.option.or.label": "满足以下任一条件 (OR)", + "workflow_node.condition.form.expression.variable.placeholder": "请选择", + "workflow_node.condition.form.expression.variable.errmsg": "请选择变量", + "workflow_node.condition.form.expression.operator.placeholder": "请选择", + "workflow_node.condition.form.expression.operator.errmsg": "请选择运算符", + "workflow_node.condition.form.expression.operator.option.eq.label": "等于", + "workflow_node.condition.form.expression.operator.option.eq.alias_is_label": "为", + "workflow_node.condition.form.expression.operator.option.neq.label": "不等于", + "workflow_node.condition.form.expression.operator.option.neq.alias_not_label": "不为", + "workflow_node.condition.form.expression.operator.option.gt.label": "大于", + "workflow_node.condition.form.expression.operator.option.gte.label": "大于等于", + "workflow_node.condition.form.expression.operator.option.lt.label": "小于", + "workflow_node.condition.form.expression.operator.option.lte.label": "小于等于", + "workflow_node.condition.form.expression.value.placeholder": "请输入", + "workflow_node.condition.form.expression.value.errmsg": "请输入值", + "workflow_node.condition.form.expression.value.option.true.label": "真", + "workflow_node.condition.form.expression.value.option.false.label": "假", + "workflow_node.condition.form.expression.add_condition.button": "添加条件", "workflow_node.execute_result_branch.label": "执行结果分支", "workflow_node.execute_result_branch.default_name": "执行结果分支", diff --git a/ui/src/i18n/locales/zh/nls.workflow.vars.json b/ui/src/i18n/locales/zh/nls.workflow.vars.json new file mode 100644 index 00000000..eddfc585 --- /dev/null +++ b/ui/src/i18n/locales/zh/nls.workflow.vars.json @@ -0,0 +1,6 @@ +{ + "workflow.variables.type.certificate.label": "证书", + + "workflow.variables.selector.validity.label": "有效性", + "workflow.variables.selector.days_left.label": "剩余天数" +} diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 832269d0..91e8d746 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -265,7 +265,7 @@ const WorkflowDetail = () => { body: { position: "relative", height: "100%", - padding: 0, + padding: initialized ? 0 : undefined, }, }} loading={!initialized} diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts index d20fec16..67bc25f9 100644 --- a/ui/src/stores/workflow/index.ts +++ b/ui/src/stores/workflow/index.ts @@ -32,7 +32,7 @@ export type WorkflowState = { addBranch: (branchId: string) => void; removeBranch: (branchId: string, index: number) => void; - getWorkflowOuptutBeforeId: (nodeId: string, type?: string) => WorkflowNode[]; + getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => WorkflowNode[]; }; export const useWorkflowStore = create((set, get) => ({ @@ -243,7 +243,7 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - getWorkflowOuptutBeforeId: (nodeId: string, type: string = "all") => { - return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, type); + getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => { + return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, typeFilter); }, })); From f885b49daf176a56c55845b7554323eac1411c6e Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sun, 1 Jun 2025 22:59:24 +0800 Subject: [PATCH 26/28] feat: add certtest workflow template --- ui/public/imgs/workflow/tpl-blank.png | Bin 5262 -> 9218 bytes ui/public/imgs/workflow/tpl-certtest.png | Bin 0 -> 20833 bytes ui/public/imgs/workflow/tpl-standard.png | Bin 12858 -> 20723 bytes ui/src/domain/workflow.ts | 154 ++++++++++++++++-- ui/src/domain/workflowExpr.ts | 1 - ui/src/i18n/locales/en/nls.workflow.json | 2 + .../i18n/locales/en/nls.workflow.nodes.json | 2 + ui/src/i18n/locales/zh/nls.workflow.json | 4 +- .../i18n/locales/zh/nls.workflow.nodes.json | 10 +- ui/src/pages/workflows/WorkflowNew.tsx | 34 +++- 10 files changed, 184 insertions(+), 23 deletions(-) create mode 100644 ui/public/imgs/workflow/tpl-certtest.png delete mode 100644 ui/src/domain/workflowExpr.ts diff --git a/ui/public/imgs/workflow/tpl-blank.png b/ui/public/imgs/workflow/tpl-blank.png index 8f683ce6fcb0f5e62d469d415c6277fccf7e0770..ee6568a9804f982e74f4f9a5ee78fd08640a272e 100644 GIT binary patch literal 9218 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 F^iR&aMN9wy literal 5262 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 diff --git a/ui/public/imgs/workflow/tpl-certtest.png b/ui/public/imgs/workflow/tpl-certtest.png new file mode 100644 index 0000000000000000000000000000000000000000..da7bfada55b7d0ff800363c414c9bca88b1d3e67 GIT binary patch literal 20833 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 b9_+v7zW#+&{u|Z5CGc+v{BI?&JN*9uO%#IC literal 0 HcmV?d00001 diff --git a/ui/public/imgs/workflow/tpl-standard.png b/ui/public/imgs/workflow/tpl-standard.png index 46698a875c3b835a18b4fb9b059d405c905bd181..c14238fff2bf3948763f87a1e111f7b9ae0468f9 100644 GIT binary patch literal 20723 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 AivR!s literal 12858 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¨?LE_}115n{4ET98mJ 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 = new Map([ [WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.default_name")], [WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.default_name")], [WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.default_name")], - [WorkflowNodeType.Custom, i18n.t("workflow_node.custom.default_name")], ]); const workflowNodeTypeDefaultInputs: Map = new Map([ @@ -240,25 +239,153 @@ const isBranchLike = (node: WorkflowNode) => { }; type InitWorkflowOptions = { - template?: "standard"; + template?: "standard" | "certtest"; }; export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => { const root = newNode(WorkflowNodeType.Start, {}) as WorkflowNode; root.config = { trigger: WORKFLOW_TRIGGERS.MANUAL }; - if (options.template === "standard") { - let current = root; - current.next = newNode(WorkflowNodeType.Apply, {}); + switch (options.template) { + case "standard": + { + let current = root; - current = current.next; - current.next = newNode(WorkflowNodeType.Deploy, {}); + const applyNode = newNode(WorkflowNodeType.Apply); + current.next = applyNode; - current = current.next; - current.next = newNode(WorkflowNodeType.ExecuteResultBranch, {}); + current = current.next; + current.next = newNode(WorkflowNodeType.ExecuteResultBranch); - current = current.next!.branches![1]; - current.next = newNode(WorkflowNodeType.Notify, {}); + current = current.next!.branches![1]; + current.next = newNode(WorkflowNodeType.Notify, { + nodeConfig: { + subject: "[Certimate] Workflow Failure Alert!", + message: "Your workflow run for the certificate application has failed. Please check the details.", + } as WorkflowNodeConfigForNotify, + }); + + current = applyNode.next!.branches![0]; + current.next = newNode(WorkflowNodeType.Deploy, { + nodeConfig: { + certificate: `${applyNode.id}#certificate`, + skipOnLastSucceeded: true, + } as WorkflowNodeConfigForDeploy, + }); + + current = current.next; + current.next = newNode(WorkflowNodeType.ExecuteResultBranch); + + current = current.next!.branches![1]; + current.next = newNode(WorkflowNodeType.Notify, { + nodeConfig: { + subject: "[Certimate] Workflow Failure Alert!", + message: "Your workflow run for the certificate deployment has failed. Please check the details.", + } as WorkflowNodeConfigForNotify, + }); + } + break; + + case "certtest": + { + let current = root; + + const monitorNode = newNode(WorkflowNodeType.Monitor); + current.next = monitorNode; + + current = current.next; + current.next = newNode(WorkflowNodeType.ExecuteResultBranch); + + current = current.next!.branches![1]; + current.next = newNode(WorkflowNodeType.Notify, { + nodeConfig: { + subject: "[Certimate] Workflow Failure Alert!", + message: "Your workflow run for the certificate monitoring has failed. Please check the details.", + } as WorkflowNodeConfigForNotify, + }); + + current = monitorNode.next!.branches![0]; + const branchNode = newNode(WorkflowNodeType.Branch); + current.next = branchNode; + + current = branchNode.branches![0]; + current.name = i18n.t("workflow_node.condition.default_name.template_certtest_on_expire_soon"); + current.config = { + expression: { + left: { + left: { + selector: { + id: monitorNode.id, + name: "certificate.validity", + type: "boolean", + }, + type: "var", + }, + operator: "eq", + right: { + type: "const", + value: "true", + valueType: "boolean", + }, + type: "comparison", + }, + operator: "and", + right: { + left: { + selector: { + id: monitorNode.id, + name: "certificate.daysLeft", + type: "number", + }, + type: "var", + }, + operator: "lte", + right: { + type: "const", + value: "30", + valueType: "number", + }, + type: "comparison", + }, + type: "logical", + }, + } as WorkflowNodeConfigForCondition; + current.next = newNode(WorkflowNodeType.Notify, { + nodeConfig: { + subject: "[Certimate] Certificate Expiry Alert!", + message: "The certificate will expire soon. Please pay attention to your website.", + } as WorkflowNodeConfigForNotify, + }); + + current = branchNode.branches![1]; + current.name = i18n.t("workflow_node.condition.default_name.template_certtest_on_expired"); + current.config = { + expression: { + left: { + selector: { + id: monitorNode.id, + name: "certificate.validity", + type: "boolean", + }, + type: "var", + }, + operator: "eq", + right: { + type: "const", + value: "false", + valueType: "boolean", + }, + type: "comparison", + }, + } as WorkflowNodeConfigForCondition; + current.next = newNode(WorkflowNodeType.Notify, { + nodeConfig: { + subject: "[Certimate] Certificate Expiry Alert!", + message: "The certificate has already expired. Please pay attention to your website.", + } as WorkflowNodeConfigForNotify, + }); + } + break; } return { @@ -275,6 +402,8 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel = }; type NewNodeOptions = { + nodeName?: string; + nodeConfig?: Record; branchIndex?: number; }; @@ -284,8 +413,9 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {} const node: WorkflowNode = { id: nanoid(), - name: nodeName, + name: options.nodeName ?? nodeName, type: nodeType, + config: options.nodeConfig, }; switch (nodeType) { diff --git a/ui/src/domain/workflowExpr.ts b/ui/src/domain/workflowExpr.ts deleted file mode 100644 index 5f282702..00000000 --- a/ui/src/domain/workflowExpr.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index b4f9d7e6..b086e25f 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -30,6 +30,8 @@ "workflow.new.templates.title": "Choose a Workflow Template", "workflow.new.templates.template.standard.title": "Standard template", "workflow.new.templates.template.standard.description": "A standard operating procedure that includes application, deployment, and notification steps.", + "workflow.new.templates.template.certtest.title": "Monitoring template", + "workflow.new.templates.template.certtest.description": "A monitoring operating procedure that includes monitoring, and notification steps.", "workflow.new.templates.template.blank.title": "Blank template", "workflow.new.templates.template.blank.description": "Customize all the contents of the workflow from the beginning.", "workflow.new.modal.title": "Create workflow", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 626e9b68..a22cd8c4 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -876,6 +876,8 @@ "workflow_node.condition.label": "Branch", "workflow_node.condition.default_name": "Branch", + "workflow_node.condition.default_name.template_certtest_on_expire_soon": "If the certificate will expire soon ...", + "workflow_node.condition.default_name.template_certtest_on_expired": "If the certificate has expired ...", "workflow_node.condition.form.expression.label": "Conditions to enter the branch", "workflow_node.condition.form.expression.logical_operator.errmsg": "Please select logical operator of conditions", "workflow_node.condition.form.expression.logical_operator.option.and.label": "Meeting all of the conditions (AND)", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 46cdc228..9ff12aac 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -29,7 +29,9 @@ "workflow.new.subtitle": "使用工作流来申请证书、部署上传和发送通知", "workflow.new.templates.title": "选择工作流模板", "workflow.new.templates.template.standard.title": "标准模板", - "workflow.new.templates.template.standard.description": "一个包含申请 + 部署 + 通知步骤的标准工作流程。", + "workflow.new.templates.template.standard.description": "一个包含证书申请 + 证书部署 + 消息通知步骤的工作流程。", + "workflow.new.templates.template.certtest.title": "监控模板", + "workflow.new.templates.template.certtest.description": "一个包含证书监控 + 消息通知步骤的工作流程。", "workflow.new.templates.template.blank.title": "空白模板", "workflow.new.templates.template.blank.description": "从零开始自定义工作流的任务内容。", "workflow.new.modal.title": "新建工作流", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 7710e386..b568d2ca 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -875,6 +875,8 @@ "workflow_node.condition.label": "分支", "workflow_node.condition.default_name": "分支", + "workflow_node.condition.default_name.template_certtest_on_expire_soon": "若网站证书即将到期 ...", + "workflow_node.condition.default_name.template_certtest_on_expired": "若网站证书已到期 ...", "workflow_node.condition.form.expression.label": "分支进入条件", "workflow_node.condition.form.expression.logical_operator.errmsg": "请选择条件组合方式", "workflow_node.condition.form.expression.logical_operator.option.and.label": "满足以下所有条件 (AND)", @@ -900,9 +902,9 @@ "workflow_node.execute_result_branch.label": "执行结果分支", "workflow_node.execute_result_branch.default_name": "执行结果分支", - "workflow_node.execute_success.label": "若前序节点执行成功…", - "workflow_node.execute_success.default_name": "若前序节点执行成功…", + "workflow_node.execute_success.label": "若上一节点执行成功…", + "workflow_node.execute_success.default_name": "若上一节点执行成功…", - "workflow_node.execute_failure.label": "若前序节点执行失败…", - "workflow_node.execute_failure.default_name": "若前序节点执行失败…" + "workflow_node.execute_failure.label": "若上一节点执行失败…", + "workflow_node.execute_failure.default_name": "若上一节点执行失败…" } diff --git a/ui/src/pages/workflows/WorkflowNew.tsx b/ui/src/pages/workflows/WorkflowNew.tsx index 5f6af27b..9877dcc2 100644 --- a/ui/src/pages/workflows/WorkflowNew.tsx +++ b/ui/src/pages/workflows/WorkflowNew.tsx @@ -12,9 +12,10 @@ import { useAntdForm } from "@/hooks"; import { save as saveWorkflow } from "@/repository/workflow"; import { getErrMsg } from "@/utils/error"; -const TEMPLATE_KEY_BLANK = "blank" as const; const TEMPLATE_KEY_STANDARD = "standard" as const; -type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_STANDARD; +const TEMPLATE_KEY_CERTTEST = "monitor" as const; +const TEMPLATE_KEY_BLANK = "blank" as const; +type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_CERTTEST | typeof TEMPLATE_KEY_STANDARD; const WorkflowNew = () => { const navigate = useNavigate(); @@ -27,8 +28,8 @@ const WorkflowNew = () => { xs: { flex: "100%" }, md: { flex: "100%" }, lg: { flex: "50%" }, - xl: { flex: "50%" }, - xxl: { flex: "50%" }, + xl: { flex: "33.3333%" }, + xxl: { flex: "33.3333%" }, }; const [templateSelectKey, setTemplateSelectKey] = useState(); @@ -64,6 +65,10 @@ const WorkflowNew = () => { workflow = initWorkflow({ template: "standard" }); break; + case TEMPLATE_KEY_CERTTEST: + workflow = initWorkflow({ template: "certtest" }); + break; + default: throw "Invalid state: `templateSelectKey`"; } @@ -116,7 +121,7 @@ const WorkflowNew = () => {
-
+
{t("workflow.new.templates.title")}
@@ -139,6 +144,25 @@ const WorkflowNew = () => {
+ + + } + hoverable + onClick={() => handleTemplateClick(TEMPLATE_KEY_CERTTEST)} + > +
+ + +
+
+ + Date: Mon, 2 Jun 2025 23:06:18 +0800 Subject: [PATCH 27/28] feat(ui): duplicate workflow node --- .../node-processor/execute_failure_node.go | 1 - .../node-processor/execute_success_node.go | 1 - .../workflow/node-processor/start_node.go | 2 +- .../workflow/node/ExecuteResultNode.tsx | 18 +- .../components/workflow/node/UnknownNode.tsx | 2 +- .../components/workflow/node/_SharedNode.tsx | 41 ++++- ui/src/domain/workflow.ts | 167 +++++++++++++----- .../i18n/locales/en/nls.workflow.nodes.json | 8 +- .../i18n/locales/zh/nls.workflow.nodes.json | 8 +- ui/src/stores/workflow/index.ts | 52 +++++- 10 files changed, 223 insertions(+), 77 deletions(-) diff --git a/internal/workflow/node-processor/execute_failure_node.go b/internal/workflow/node-processor/execute_failure_node.go index d3f61e30..40be18ed 100644 --- a/internal/workflow/node-processor/execute_failure_node.go +++ b/internal/workflow/node-processor/execute_failure_node.go @@ -22,7 +22,6 @@ func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode { func (n *executeFailureNode) Process(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回 - n.logger.Info("the previous node execution was failed") return nil } diff --git a/internal/workflow/node-processor/execute_success_node.go b/internal/workflow/node-processor/execute_success_node.go index 46a74482..2cd78ff3 100644 --- a/internal/workflow/node-processor/execute_success_node.go +++ b/internal/workflow/node-processor/execute_success_node.go @@ -22,7 +22,6 @@ func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode { func (n *executeSuccessNode) Process(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回 - n.logger.Info("the previous node execution was succeeded") return nil } diff --git a/internal/workflow/node-processor/start_node.go b/internal/workflow/node-processor/start_node.go index 30dee424..bdfea1b7 100644 --- a/internal/workflow/node-processor/start_node.go +++ b/internal/workflow/node-processor/start_node.go @@ -22,7 +22,7 @@ func NewStartNode(node *domain.WorkflowNode) *startNode { func (n *startNode) Process(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回 - n.logger.Info("ready to start ...") + n.logger.Info("workflow is started") return nil } diff --git a/ui/src/components/workflow/node/ExecuteResultNode.tsx b/ui/src/components/workflow/node/ExecuteResultNode.tsx index 69a0949c..ce991d95 100644 --- a/ui/src/components/workflow/node/ExecuteResultNode.tsx +++ b/ui/src/components/workflow/node/ExecuteResultNode.tsx @@ -1,5 +1,4 @@ import { memo } from "react"; -import { useTranslation } from "react-i18next"; import { CheckCircleOutlined as CheckCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon, @@ -17,8 +16,6 @@ export type ConditionNodeProps = SharedNodeProps & { }; const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => { - const { t } = useTranslation(); - const { token: themeToken } = theme.useToken(); return ( @@ -42,16 +39,15 @@ const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionN
{node.type === WorkflowNodeType.ExecuteSuccess ? ( - <> - -
{t("workflow_node.execute_success.label")}
- + ) : ( - <> - -
{t("workflow_node.execute_failure.label")}
- + )} +
diff --git a/ui/src/components/workflow/node/UnknownNode.tsx b/ui/src/components/workflow/node/UnknownNode.tsx index 7cb64aae..2586c6e2 100644 --- a/ui/src/components/workflow/node/UnknownNode.tsx +++ b/ui/src/components/workflow/node/UnknownNode.tsx @@ -14,7 +14,7 @@ const UnknownNode = ({ node, disabled }: MonitorNodeProps) => { const { removeNode } = useWorkflowStore(useZustandShallowSelector(["removeNode"])); const handleClickRemove = () => { - removeNode(node.id); + removeNode(node); }; return ( diff --git a/ui/src/components/workflow/node/_SharedNode.tsx b/ui/src/components/workflow/node/_SharedNode.tsx index 72f4b967..5fe6fc3e 100644 --- a/ui/src/components/workflow/node/_SharedNode.tsx +++ b/ui/src/components/workflow/node/_SharedNode.tsx @@ -5,6 +5,7 @@ import { EllipsisOutlined as EllipsisOutlinedIcon, FormOutlined as FormOutlinedIcon, MoreOutlined as MoreOutlinedIcon, + SnippetsOutlined as SnippetsOutlinedIcon, } from "@ant-design/icons"; import { useControllableValue } from "ahooks"; import { Button, Card, Drawer, Dropdown, Input, type InputRef, type MenuProps, Modal, Popover, Space } from "antd"; @@ -82,14 +83,27 @@ const isNodeBranchLike = (node: WorkflowNode) => { ); }; -const isNodeReadOnly = (node: WorkflowNode) => { +const isNodeUnduplicatable = (node: WorkflowNode) => { + return ( + node.type === WorkflowNodeType.Start || + node.type === WorkflowNodeType.End || + node.type === WorkflowNodeType.Branch || + node.type === WorkflowNodeType.ExecuteResultBranch || + node.type === WorkflowNodeType.ExecuteSuccess || + node.type === WorkflowNodeType.ExecuteFailure + ); +}; + +const isNodeUnremovable = (node: WorkflowNode) => { return node.type === WorkflowNodeType.Start || node.type === WorkflowNodeType.End; }; const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, afterUpdate, afterDelete }: SharedNodeMenuProps) => { const { t } = useTranslation(); - const { updateNode, removeNode, removeBranch } = useWorkflowStore(useZustandShallowSelector(["updateNode", "removeNode", "removeBranch"])); + const { duplicateNode, updateNode, removeNode, duplicateBranch, removeBranch } = useWorkflowStore( + useZustandShallowSelector(["duplicateNode", "updateNode", "removeNode", "duplicateBranch", "removeBranch"]) + ); const [modalApi, ModelContextHolder] = Modal.useModal(); @@ -112,11 +126,19 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, afterUpdate?.(); }; - const handleDeleteClick = async () => { + const handleDuplicateClick = async () => { + if (isNodeBranchLike(node)) { + await duplicateBranch(branchId!, branchIndex!); + } else { + await duplicateNode(node); + } + }; + + const handleRemoveClick = async () => { if (isNodeBranchLike(node)) { await removeBranch(branchId!, branchIndex!); } else { - await removeNode(node.id); + await removeNode(node); } afterDelete?.(); @@ -155,16 +177,23 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, setTimeout(() => nameInputRef.current?.focus(), 1); }, }, + { + key: "duplicate", + disabled: disabled || isNodeUnduplicatable(node), + label: isNodeBranchLike(node) ? t("workflow_node.action.duplicate_branch") : t("workflow_node.action.duplicate_node"), + icon: , + onClick: handleDuplicateClick, + }, { type: "divider", }, { key: "remove", - disabled: disabled || isNodeReadOnly(node), + disabled: disabled || isNodeUnremovable(node), label: isNodeBranchLike(node) ? t("workflow_node.action.remove_branch") : t("workflow_node.action.remove_node"), icon: , danger: true, - onClick: handleDeleteClick, + onClick: handleRemoveClick, }, ] satisfies MenuProps["items"]; diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 9a41a393..0a71749b 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import { produce } from "immer"; +import { Immer, produce } from "immer"; import { nanoid } from "nanoid"; import i18n from "@/i18n"; @@ -234,7 +234,7 @@ export type NotExpr = { type: ExprType.Not; expr: Expr }; export type Expr = ConstantExpr | VariantExpr | ComparisonExpr | LogicalExpr | NotExpr; // #endregion -const isBranchLike = (node: WorkflowNode) => { +const isBranchNode = (node: WorkflowNode) => { return node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.ExecuteResultBranch; }; @@ -458,8 +458,75 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {} return node; }; -export const updateNode = (node: WorkflowNode, targetNode: WorkflowNode) => { - return produce(node, (draft) => { +export const cloneNode = (sourceNode: WorkflowNode): WorkflowNode => { + const { produce } = new Immer({ autoFreeze: false }); + const deepClone = (node: WorkflowNode): WorkflowNode => { + return produce(node, (draft) => { + draft.id = nanoid(); + + if (draft.next) { + draft.next = cloneNode(draft.next); + } + + if (draft.branches) { + draft.branches = draft.branches.map((branch) => cloneNode(branch)); + } + + return draft; + }); + }; + + const copyNode = produce(sourceNode, (draft) => { + draft.name = `${draft.name}-copy`; + }); + return deepClone(copyNode); +}; + +export const addNode = (root: WorkflowNode, targetNode: WorkflowNode, previousNodeId: string) => { + if (isBranchNode(targetNode)) { + throw new Error("Cannot add a branch node directly. Use `addBranch` instead."); + } + + return produce(root, (draft) => { + let current = draft; + while (current) { + if (current.id === previousNodeId && !isBranchNode(targetNode)) { + targetNode.next = current.next; + current.next = targetNode; + break; + } else if (current.id === previousNodeId && isBranchNode(targetNode)) { + targetNode.branches![0].next = current.next; + current.next = targetNode; + break; + } + + if (isBranchNode(current)) { + current.branches ??= []; + current.branches = current.branches.map((branch) => addNode(branch, targetNode, previousNodeId)); + } + + current = current.next as WorkflowNode; + } + + return draft; + }); +}; + +export const duplicateNode = (root: WorkflowNode, targetNode: WorkflowNode) => { + if (isBranchNode(targetNode)) { + throw new Error("Cannot duplicate a branch node directly. Use `duplicateBranch` instead."); + } + + const copiedNode = cloneNode(targetNode); + return addNode(root, copiedNode, targetNode.id); +}; + +export const updateNode = (root: WorkflowNode, targetNode: WorkflowNode) => { + if (isBranchNode(targetNode)) { + throw new Error("Cannot update a branch node directly. Use `updateBranch` instead."); + } + + return produce(root, (draft) => { let current = draft; while (current) { if (current.id === targetNode.id) { @@ -476,7 +543,7 @@ export const updateNode = (node: WorkflowNode, targetNode: WorkflowNode) => { break; } - if (isBranchLike(current)) { + if (isBranchNode(current)) { current.branches ??= []; current.branches = current.branches.map((branch) => updateNode(branch, targetNode)); } @@ -488,23 +555,18 @@ export const updateNode = (node: WorkflowNode, targetNode: WorkflowNode) => { }); }; -export const addNode = (node: WorkflowNode, previousNodeId: string, targetNode: WorkflowNode) => { - return produce(node, (draft) => { +export const removeNode = (root: WorkflowNode, targetNodeId: string) => { + return produce(root, (draft) => { let current = draft; while (current) { - if (current.id === previousNodeId && !isBranchLike(targetNode)) { - targetNode.next = current.next; - current.next = targetNode; - break; - } else if (current.id === previousNodeId && isBranchLike(targetNode)) { - targetNode.branches![0].next = current.next; - current.next = targetNode; + if (current.next?.id === targetNodeId) { + current.next = current.next.next; break; } - if (isBranchLike(current)) { + if (isBranchNode(current)) { current.branches ??= []; - current.branches = current.branches.map((branch) => addNode(branch, previousNodeId, targetNode)); + current.branches = current.branches.map((branch) => removeNode(branch, targetNodeId)); } current = current.next as WorkflowNode; @@ -514,8 +576,8 @@ export const addNode = (node: WorkflowNode, previousNodeId: string, targetNode: }); }; -export const addBranch = (node: WorkflowNode, branchNodeId: string) => { - return produce(node, (draft) => { +export const addBranch = (root: WorkflowNode, branchNodeId: string) => { + return produce(root, (draft) => { let current = draft; while (current) { if (current.id === branchNodeId) { @@ -532,7 +594,7 @@ export const addBranch = (node: WorkflowNode, branchNodeId: string) => { break; } - if (isBranchLike(current)) { + if (isBranchNode(current)) { current.branches ??= []; current.branches = current.branches.map((branch) => addBranch(branch, branchNodeId)); } @@ -544,29 +606,8 @@ export const addBranch = (node: WorkflowNode, branchNodeId: string) => { }); }; -export const removeNode = (node: WorkflowNode, targetNodeId: string) => { - return produce(node, (draft) => { - let current = draft; - while (current) { - if (current.next?.id === targetNodeId) { - current.next = current.next.next; - break; - } - - if (isBranchLike(current)) { - current.branches ??= []; - current.branches = current.branches.map((branch) => removeNode(branch, targetNodeId)); - } - - current = current.next as WorkflowNode; - } - - return draft; - }); -}; - -export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchIndex: number) => { - return produce(node, (draft) => { +export const duplicateBranch = (root: WorkflowNode, branchNodeId: string, branchIndex: number) => { + return produce(root, (draft) => { let current = draft; let last: WorkflowNode | undefined = { id: "", @@ -576,7 +617,41 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd }; while (current && last) { if (current.id === branchNodeId) { - if (!isBranchLike(current)) { + if (!isBranchNode(current)) { + return draft; + } + + current.branches ??= []; + current.branches.splice(branchIndex + 1, 0, cloneNode(current.branches[branchIndex])); + + break; + } + + if (isBranchNode(current)) { + current.branches ??= []; + current.branches = current.branches.map((branch) => duplicateBranch(branch, branchNodeId, branchIndex)); + } + + current = current.next as WorkflowNode; + last = last.next; + } + + return draft; + }); +}; + +export const removeBranch = (root: WorkflowNode, branchNodeId: string, branchIndex: number) => { + return produce(root, (draft) => { + let current = draft; + let last: WorkflowNode | undefined = { + id: "", + name: "", + type: WorkflowNodeType.Start, + next: draft, + }; + while (current && last) { + if (current.id === branchNodeId) { + if (!isBranchNode(current)) { return draft; } @@ -601,7 +676,7 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd break; } - if (isBranchLike(current)) { + if (isBranchNode(current)) { current.branches ??= []; current.branches = current.branches.map((branch) => removeBranch(branch, branchNodeId, branchIndex)); } @@ -647,7 +722,7 @@ export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, typeFi }); } - if (isBranchLike(current)) { + if (isBranchNode(current)) { let currentLength = output.length; const latestOutput = output.length > 0 ? output[output.length - 1] : null; for (const branch of current.branches!) { @@ -679,7 +754,7 @@ export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, typeFi export const isAllNodesValidated = (node: WorkflowNode): boolean => { let current = node as typeof node | undefined; while (current) { - if (isBranchLike(current)) { + if (isBranchNode(current)) { for (const branch of current.branches!) { if (!isAllNodesValidated(branch)) { return false; diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index a22cd8c4..0c44f107 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -2,9 +2,11 @@ "workflow_node.action.configure_node": "Configure node", "workflow_node.action.add_node": "Add node", "workflow_node.action.rename_node": "Rename node", + "workflow_node.action.duplicate_node": "Duplicate node", "workflow_node.action.remove_node": "Delete node", "workflow_node.action.add_branch": "Add branch", "workflow_node.action.rename_branch": "Rename branch", + "workflow_node.action.duplicate_branch": "Duplicate branch", "workflow_node.action.remove_branch": "Delete branch", "workflow_node.unsaved_changes.confirm": "You have unsaved changes. Do you really want to close the panel and drop those changes?", @@ -901,11 +903,11 @@ "workflow_node.condition.form.expression.add_condition.button": "Add condition", "workflow_node.execute_result_branch.label": "Execution result branch", - "workflow_node.execute_result_branch.default_name": "Execution result branch", + "workflow_node.execute_result_branch.default_name": "Branch", "workflow_node.execute_success.label": "If the previous node succeeded ...", - "workflow_node.execute_success.default_name": "If the previous node succeeded ...", + "workflow_node.execute_success.default_name": "On Succeeded", "workflow_node.execute_failure.label": "If the previous node failed ...", - "workflow_node.execute_failure.default_name": "If the previous node failed ..." + "workflow_node.execute_failure.default_name": "On Failed" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index b568d2ca..9f244ef2 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -2,9 +2,11 @@ "workflow_node.action.configure_node": "配置节点", "workflow_node.branch.add_node": "添加节点", "workflow_node.action.rename_node": "重命名", + "workflow_node.action.duplicate_node": "复制节点", "workflow_node.action.remove_node": "删除节点", "workflow_node.action.add_branch": "添加分支", "workflow_node.action.rename_branch": "重命名", + "workflow_node.action.duplicate_branch": "复制分支", "workflow_node.action.remove_branch": "删除分支", "workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?", @@ -900,11 +902,11 @@ "workflow_node.condition.form.expression.add_condition.button": "添加条件", "workflow_node.execute_result_branch.label": "执行结果分支", - "workflow_node.execute_result_branch.default_name": "执行结果分支", + "workflow_node.execute_result_branch.default_name": "分支", "workflow_node.execute_success.label": "若上一节点执行成功…", - "workflow_node.execute_success.default_name": "若上一节点执行成功…", + "workflow_node.execute_success.default_name": "执行成功", "workflow_node.execute_failure.label": "若上一节点执行失败…", - "workflow_node.execute_failure.default_name": "若上一节点执行失败…" + "workflow_node.execute_failure.default_name": "执行失败" } diff --git a/ui/src/stores/workflow/index.ts b/ui/src/stores/workflow/index.ts index 67bc25f9..7a086708 100644 --- a/ui/src/stores/workflow/index.ts +++ b/ui/src/stores/workflow/index.ts @@ -7,6 +7,8 @@ import { type WorkflowNodeConfigForStart, addBranch, addNode, + duplicateBranch, + duplicateNode, getOutputBeforeNodeId, removeBranch, removeNode, @@ -26,10 +28,12 @@ export type WorkflowState = { destroy(): void; addNode: (node: WorkflowNode, previousNodeId: string) => void; + duplicateNode: (node: WorkflowNode) => void; updateNode: (node: WorkflowNode) => void; - removeNode: (nodeId: string) => void; + removeNode: (node: WorkflowNode) => void; addBranch: (branchId: string) => void; + duplicateBranch: (branchId: string, index: number) => void; removeBranch: (branchId: string, index: number) => void; getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => WorkflowNode[]; @@ -146,7 +150,27 @@ export const useWorkflowStore = create((set, get) => ({ addNode: async (node: WorkflowNode, previousNodeId: string) => { if (!get().initialized) throw "Workflow not initialized yet"; - const root = addNode(get().workflow.draft!, previousNodeId, node); + const root = addNode(get().workflow.draft!, node, previousNodeId); + const resp = await saveWorkflow({ + id: get().workflow.id!, + draft: root, + hasDraft: true, + }); + + set((state: WorkflowState) => { + return { + workflow: produce(state.workflow, (draft) => { + draft.draft = resp.draft; + draft.hasDraft = resp.hasDraft; + }), + }; + }); + }, + + duplicateNode: async (node: WorkflowNode) => { + if (!get().initialized) throw "Workflow not initialized yet"; + + const root = duplicateNode(get().workflow.draft!, node); const resp = await saveWorkflow({ id: get().workflow.id!, draft: root, @@ -183,10 +207,10 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - removeNode: async (nodeId: string) => { + removeNode: async (node: WorkflowNode) => { if (!get().initialized) throw "Workflow not initialized yet"; - const root = removeNode(get().workflow.draft!, nodeId); + const root = removeNode(get().workflow.draft!, node.id); const resp = await saveWorkflow({ id: get().workflow.id!, draft: root, @@ -223,6 +247,26 @@ export const useWorkflowStore = create((set, get) => ({ }); }, + duplicateBranch: async (branchId: string, index: number) => { + if (!get().initialized) throw "Workflow not initialized yet"; + + const root = duplicateBranch(get().workflow.draft!, branchId, index); + const resp = await saveWorkflow({ + id: get().workflow.id!, + draft: root, + hasDraft: true, + }); + + set((state: WorkflowState) => { + return { + workflow: produce(state.workflow, (draft) => { + draft.draft = resp.draft; + draft.hasDraft = resp.hasDraft; + }), + }; + }); + }, + removeBranch: async (branchId: string, index: number) => { if (!get().initialized) throw "Workflow not initialized yet"; From e6cf4d3e0718380bd23a46f745f7a1cc4563ea67 Mon Sep 17 00:00:00 2001 From: RHQYZ Date: Mon, 2 Jun 2025 23:10:34 +0800 Subject: [PATCH 28/28] bump version to v0.3.15 --- ui/src/domain/version.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/domain/version.ts b/ui/src/domain/version.ts index b7741526..9fbf6cfe 100644 --- a/ui/src/domain/version.ts +++ b/ui/src/domain/version.ts @@ -1 +1 @@ -export const version = "v0.3.14"; +export const version = "v0.3.15";