diff --git a/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptEndpoint.cs
new file mode 100644
index 0000000..ff83987
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptEndpoint.cs
@@ -0,0 +1,32 @@
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 创建脚本端点。POST /scripts。需 scripts.create 权限。
+public class CreateScriptEndpoint(IMediator mediator)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/scripts");
+ Permissions("scripts.create");
+ }
+
+ public override async Task HandleAsync(CreateScriptRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new CreateScriptCommand(
+ req.Name,
+ req.DisplayName,
+ req.Description,
+ req.Code,
+ req.InputSchema,
+ req.OutputSchema,
+ req.TimeoutMs,
+ req.FormDefinitionId),
+ ct);
+ await Send.ResponseAsync(result, 201, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptRequest.cs
new file mode 100644
index 0000000..f24d881
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/CreateScriptRequest.cs
@@ -0,0 +1,12 @@
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 创建脚本请求。仅承载 C# 脚本字段。
+public sealed record CreateScriptRequest(
+ string Name,
+ string DisplayName,
+ string? Description,
+ string Code,
+ string? InputSchema,
+ string? OutputSchema,
+ int TimeoutMs,
+ Guid? FormDefinitionId = null);
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptEndpoint.cs
new file mode 100644
index 0000000..7eb024c
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptEndpoint.cs
@@ -0,0 +1,21 @@
+using MediatR;
+using Scripting.Application.Commands;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 删除脚本端点(软删除)。DELETE /scripts/{id}。需 scripts.delete 权限。
+public class DeleteScriptEndpoint(IMediator mediator)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Delete("/scripts/{id}");
+ Permissions("scripts.delete");
+ }
+
+ public override async Task HandleAsync(DeleteScriptRequest req, CancellationToken ct)
+ {
+ await mediator.Send(new DeleteScriptCommand(req.Id), ct);
+ await Send.OkAsync(true, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptRequest.cs
new file mode 100644
index 0000000..73cbde0
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/DeleteScriptRequest.cs
@@ -0,0 +1,4 @@
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 删除脚本请求。Id 由路由 {id} 绑定。
+public sealed record DeleteScriptRequest(Guid Id);
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptEndpoint.cs
new file mode 100644
index 0000000..7685954
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptEndpoint.cs
@@ -0,0 +1,22 @@
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 禁用脚本端点。POST /scripts/{id}/disable。需 scripts.disable 权限。
+public class DisableScriptEndpoint(IMediator mediator)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/scripts/{id}/disable");
+ Permissions("scripts.disable");
+ }
+
+ public override async Task HandleAsync(DisableScriptRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new DisableScriptCommand(req.Id), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptRequest.cs
new file mode 100644
index 0000000..6dc302a
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/DisableScriptRequest.cs
@@ -0,0 +1,4 @@
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 禁用脚本请求。Id 由路由 {id} 绑定。
+public sealed record DisableScriptRequest(Guid Id);
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailEndpoint.cs
new file mode 100644
index 0000000..b3cc9e9
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailEndpoint.cs
@@ -0,0 +1,22 @@
+using MediatR;
+using Scripting.Application.DTOs;
+using Scripting.Application.Queries;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 脚本详情端点。GET /scripts/{id}。需 scripts.read 权限。Id 由路由 {id} 按名绑定到请求。
+public class GetScriptDetailEndpoint(IMediator mediator)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Get("/scripts/{id}");
+ Permissions("scripts.read");
+ }
+
+ public override async Task HandleAsync(GetScriptDetailRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new GetScriptDetailQuery(req.Id), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailRequest.cs
new file mode 100644
index 0000000..ed596ab
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptDetailRequest.cs
@@ -0,0 +1,4 @@
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 脚本详情查询请求。Id 由路由 {id} 绑定。
+public sealed record GetScriptDetailRequest(Guid Id);
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsEndpoint.cs
new file mode 100644
index 0000000..ad12ffd
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsEndpoint.cs
@@ -0,0 +1,29 @@
+using MediatR;
+using Scripting.Application.Common;
+using Scripting.Application.DTOs;
+using Scripting.Application.Queries;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 脚本执行日志查询端点。GET /scripts/{id}/logs。需 scripts.read 权限。
+public class GetScriptExecutionLogsEndpoint(IMediator mediator)
+ : Endpoint>
+{
+ public override void Configure()
+ {
+ Get("/scripts/{id}/logs");
+ Permissions("scripts.read");
+ }
+
+ public override async Task HandleAsync(GetScriptExecutionLogsRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new GetScriptExecutionLogsQuery(
+ req.Id,
+ req.PageIndex,
+ req.PageSize,
+ req.Status),
+ ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsRequest.cs
new file mode 100644
index 0000000..8f9cc3a
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptExecutionLogsRequest.cs
@@ -0,0 +1,12 @@
+using Scripting.Domain.Enums;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 脚本执行日志查询请求。Id(脚本 Id)由路由 {id} 绑定。
+public sealed class GetScriptExecutionLogsRequest
+{
+ public Guid Id { get; set; }
+ public int PageIndex { get; set; } = 1;
+ public int PageSize { get; set; } = 20;
+ public ScriptExecutionStatus? Status { get; set; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListEndpoint.cs
new file mode 100644
index 0000000..c426a4c
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListEndpoint.cs
@@ -0,0 +1,30 @@
+using MediatR;
+using Scripting.Application.Common;
+using Scripting.Application.DTOs;
+using Scripting.Application.Queries;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 脚本分页查询端点。GET /scripts。需 scripts.read 权限。
+public class GetScriptListEndpoint(IMediator mediator)
+ : Endpoint>
+{
+ public override void Configure()
+ {
+ Get("/scripts");
+ Permissions("scripts.read");
+ }
+
+ public override async Task HandleAsync(GetScriptListRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new GetScriptListQuery(
+ req.PageIndex,
+ req.PageSize,
+ req.Status,
+ req.Keyword,
+ req.FormDefinitionId),
+ ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListRequest.cs
new file mode 100644
index 0000000..e561b25
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/GetScriptListRequest.cs
@@ -0,0 +1,13 @@
+using Scripting.Domain.Enums;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 脚本分页查询请求。支持按状态、表单作用域、关键字筛选。
+public sealed class GetScriptListRequest
+{
+ public int PageIndex { get; set; } = 1;
+ public int PageSize { get; set; } = 20;
+ public ScriptStatus? Status { get; set; }
+ public string? Keyword { get; set; }
+ public Guid? FormDefinitionId { get; set; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptEndpoint.cs
new file mode 100644
index 0000000..616e337
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptEndpoint.cs
@@ -0,0 +1,33 @@
+using System.Text.Json;
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+///
+/// 调用(按名执行)脚本端点。POST /scripts/{name}/invoke。需 scripts.run 权限。
+/// 请求体 { "input": { ... } };input 可为任意 JSON 值,端点序列化为字符串后委托执行服务。
+///
+public class InvokeScriptEndpoint(IMediator mediator)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/scripts/{name}/invoke");
+ Permissions("scripts.run");
+ }
+
+ public override async Task HandleAsync(InvokeScriptRequest req, CancellationToken ct)
+ {
+ // Input 可省略(默认 {}),序列化为紧凑 JSON 字符串交给执行服务。
+ var inputJson = req.Input is null
+ ? "{}"
+ : JsonSerializer.Serialize(req.Input.Value);
+
+ var result = await mediator.Send(
+ new InvokeScriptCommand(req.Name, inputJson),
+ ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptRequest.cs
new file mode 100644
index 0000000..4276ace
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/InvokeScriptRequest.cs
@@ -0,0 +1,15 @@
+using System.Text.Json;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+///
+/// 调用脚本请求。Name 由路由 {name} 绑定;Input 为任意 JSON 对象,端点将其序列化为字符串传给执行服务。
+///
+public sealed class InvokeScriptRequest
+{
+ /// 由路由 {name} 绑定的脚本英文名。
+ public string Name { get; set; } = string.Empty;
+
+ /// 调用输入,可为任意 JSON 值(对象/数组/标量)。
+ public JsonElement? Input { get; set; }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptEndpoint.cs
new file mode 100644
index 0000000..0b1ee6a
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptEndpoint.cs
@@ -0,0 +1,22 @@
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 发布脚本端点。POST /scripts/{id}/publish。需 scripts.publish 权限。
+public class PublishScriptEndpoint(IMediator mediator)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/scripts/{id}/publish");
+ Permissions("scripts.publish");
+ }
+
+ public override async Task HandleAsync(PublishScriptRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new PublishScriptCommand(req.Id), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptRequest.cs
new file mode 100644
index 0000000..76a7b5a
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/PublishScriptRequest.cs
@@ -0,0 +1,4 @@
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 发布脚本请求。Id 由路由 {id} 绑定。
+public sealed record PublishScriptRequest(Guid Id);
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/TestScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/TestScriptEndpoint.cs
new file mode 100644
index 0000000..7783899
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/TestScriptEndpoint.cs
@@ -0,0 +1,22 @@
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 测试执行脚本端点。POST /scripts/{id}/test。需 scripts.test 权限。
+public class TestScriptEndpoint(IMediator mediator)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Post("/scripts/{id}/test");
+ Permissions("scripts.test");
+ }
+
+ public override async Task HandleAsync(TestScriptRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(new TestScriptCommand(req.Id, req.InputJson), ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/TestScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/TestScriptRequest.cs
new file mode 100644
index 0000000..1ff03d6
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/TestScriptRequest.cs
@@ -0,0 +1,4 @@
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 测试执行脚本请求。Id 由路由 {id} 绑定,InputJson 为请求体。
+public sealed record TestScriptRequest(Guid Id, string InputJson);
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptEndpoint.cs b/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptEndpoint.cs
new file mode 100644
index 0000000..293512d
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptEndpoint.cs
@@ -0,0 +1,31 @@
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 更新脚本端点。PUT /scripts/{id}。需 scripts.update 权限。Id 由路由 {id} 绑定。
+public class UpdateScriptEndpoint(IMediator mediator)
+ : Endpoint
+{
+ public override void Configure()
+ {
+ Put("/scripts/{id}");
+ Permissions("scripts.update");
+ }
+
+ public override async Task HandleAsync(UpdateScriptRequest req, CancellationToken ct)
+ {
+ var result = await mediator.Send(
+ new UpdateScriptCommand(
+ req.Id,
+ req.DisplayName,
+ req.Description,
+ req.Code,
+ req.InputSchema,
+ req.OutputSchema,
+ req.TimeoutMs),
+ ct);
+ await Send.OkAsync(result, ct);
+ }
+}
diff --git a/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptRequest.cs b/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptRequest.cs
new file mode 100644
index 0000000..407ab1c
--- /dev/null
+++ b/src/BuildFlow.Host/Endpoints/Scripting/UpdateScriptRequest.cs
@@ -0,0 +1,11 @@
+namespace BuildFlow.Host.Endpoints.Scripting;
+
+/// 更新脚本请求。Id 由路由 {id} 绑定;Name/FormDefinitionId 不可改。
+public sealed record UpdateScriptRequest(
+ Guid Id,
+ string DisplayName,
+ string? Description,
+ string Code,
+ string? InputSchema,
+ string? OutputSchema,
+ int TimeoutMs);
diff --git a/src/modules/scripting/Application/Commands/CreateScriptCommand.cs b/src/modules/scripting/Application/Commands/CreateScriptCommand.cs
new file mode 100644
index 0000000..b9f9574
--- /dev/null
+++ b/src/modules/scripting/Application/Commands/CreateScriptCommand.cs
@@ -0,0 +1,18 @@
+using MediatR;
+using Scripting.Application.DTOs;
+
+namespace Scripting.Application.Commands;
+
+///
+/// 创建脚本函数命令。新建脚本默认是草稿(Draft),必须发布后才能被动作链/HTTP invoke 执行。
+/// Name 为稳定英文标识(创建后不可改),供按钮引用与按名调用。
+///
+public sealed record CreateScriptCommand(
+ string Name,
+ string DisplayName,
+ string? Description,
+ string Code,
+ string? InputSchema,
+ string? OutputSchema,
+ int TimeoutMs,
+ Guid? FormDefinitionId = null) : IRequest;
diff --git a/src/modules/scripting/Application/Commands/DeleteScriptCommand.cs b/src/modules/scripting/Application/Commands/DeleteScriptCommand.cs
new file mode 100644
index 0000000..222f72d
--- /dev/null
+++ b/src/modules/scripting/Application/Commands/DeleteScriptCommand.cs
@@ -0,0 +1,6 @@
+using MediatR;
+
+namespace Scripting.Application.Commands;
+
+/// 删除脚本函数命令(软删除)。Published 状态必须先 Disable 才能删除。
+public sealed record DeleteScriptCommand(Guid Id) : IRequest;
diff --git a/src/modules/scripting/Application/Commands/DisableScriptCommand.cs b/src/modules/scripting/Application/Commands/DisableScriptCommand.cs
new file mode 100644
index 0000000..96d8ad5
--- /dev/null
+++ b/src/modules/scripting/Application/Commands/DisableScriptCommand.cs
@@ -0,0 +1,7 @@
+using MediatR;
+using Scripting.Application.DTOs;
+
+namespace Scripting.Application.Commands;
+
+/// 禁用脚本函数命令。禁用后动作链将拒绝执行该脚本,HTTP invoke 返回 400。
+public sealed record DisableScriptCommand(Guid Id) : IRequest;
diff --git a/src/modules/scripting/Application/Commands/InvokeScriptCommand.cs b/src/modules/scripting/Application/Commands/InvokeScriptCommand.cs
new file mode 100644
index 0000000..aa6aab6
--- /dev/null
+++ b/src/modules/scripting/Application/Commands/InvokeScriptCommand.cs
@@ -0,0 +1,7 @@
+using MediatR;
+using Scripting.Application.DTOs;
+
+namespace Scripting.Application.Commands;
+
+/// 按 Name 调用已发布脚本命令(HTTP /scripts/{name}/invoke)。
+public sealed record InvokeScriptCommand(string Name, string InputJson) : IRequest;
diff --git a/src/modules/scripting/Application/Commands/PublishScriptCommand.cs b/src/modules/scripting/Application/Commands/PublishScriptCommand.cs
new file mode 100644
index 0000000..570a7fa
--- /dev/null
+++ b/src/modules/scripting/Application/Commands/PublishScriptCommand.cs
@@ -0,0 +1,7 @@
+using MediatR;
+using Scripting.Application.DTOs;
+
+namespace Scripting.Application.Commands;
+
+/// 发布脚本函数命令。发布将状态置为 Published 并递增 Version,便于后续审计变更。
+public sealed record PublishScriptCommand(Guid Id) : IRequest;
diff --git a/src/modules/scripting/Application/Commands/ScriptMapper.cs b/src/modules/scripting/Application/Commands/ScriptMapper.cs
new file mode 100644
index 0000000..51557f6
--- /dev/null
+++ b/src/modules/scripting/Application/Commands/ScriptMapper.cs
@@ -0,0 +1,33 @@
+using Scripting.Application.DTOs;
+using Scripting.Domain.Entities;
+
+namespace Scripting.Application.Commands;
+
+/// 脚本 DTO 映射器,集中保持命令返回结构一致。注意执行日志的字段重命名(FunctionId→ScriptId)。
+internal static class ScriptMapper
+{
+ public static ScriptDto ToDto(ScriptFunction e) => new(
+ e.Id,
+ e.FormDefinitionId,
+ e.Name,
+ e.DisplayName,
+ e.Description,
+ e.Code,
+ e.InputSchema,
+ e.OutputSchema,
+ e.Status,
+ e.Version,
+ e.TimeoutMs);
+
+ public static ScriptExecutionLogDto ToLogDto(ScriptExecutionLog e) => new(
+ e.Id,
+ e.FunctionId,
+ e.FunctionName,
+ e.Status,
+ e.InputJson,
+ e.ResultJson,
+ e.ErrorMessage,
+ e.DurationMs,
+ e.CreatedAt,
+ e.LogLines);
+}
diff --git a/src/modules/scripting/Application/Commands/TestScriptCommand.cs b/src/modules/scripting/Application/Commands/TestScriptCommand.cs
new file mode 100644
index 0000000..2634a78
--- /dev/null
+++ b/src/modules/scripting/Application/Commands/TestScriptCommand.cs
@@ -0,0 +1,7 @@
+using MediatR;
+using Scripting.Application.DTOs;
+
+namespace Scripting.Application.Commands;
+
+/// 测试执行脚本命令。不校验 Published 状态,允许草稿直接测,仍写日志(FunctionName 标 [TEST])。
+public sealed record TestScriptCommand(Guid Id, string InputJson) : IRequest;
diff --git a/src/modules/scripting/Application/Commands/UpdateScriptCommand.cs b/src/modules/scripting/Application/Commands/UpdateScriptCommand.cs
new file mode 100644
index 0000000..a666944
--- /dev/null
+++ b/src/modules/scripting/Application/Commands/UpdateScriptCommand.cs
@@ -0,0 +1,17 @@
+using MediatR;
+using Scripting.Application.DTOs;
+
+namespace Scripting.Application.Commands;
+
+///
+/// 更新脚本函数命令。仅允许编辑 DisplayName/Description/Code/InputSchema/OutputSchema/TimeoutMs。
+/// Name 与 FormDefinitionId 创建后即固化,不可改。Disabled 状态禁止编辑;Published 编辑保持已发布但 Version++。
+///
+public sealed record UpdateScriptCommand(
+ Guid Id,
+ string DisplayName,
+ string? Description,
+ string Code,
+ string? InputSchema,
+ string? OutputSchema,
+ int TimeoutMs) : IRequest;
diff --git a/src/modules/scripting/Application/Common/PagedResult.cs b/src/modules/scripting/Application/Common/PagedResult.cs
new file mode 100644
index 0000000..c37803e
--- /dev/null
+++ b/src/modules/scripting/Application/Common/PagedResult.cs
@@ -0,0 +1,13 @@
+namespace Scripting.Application.Common;
+
+///
+/// 通用分页结果包装。脚本模块本地副本,避免对 Workflow 模块的跨模块依赖。
+/// 字段形状与 Workflow.Application.Common.PagedResult 完全一致。
+///
+public class PagedResult
+{
+ public List Items { get; set; } = [];
+ public int Total { get; set; }
+ public int PageIndex { get; set; }
+ public int PageSize { get; set; }
+}
diff --git a/src/modules/scripting/Application/DTOs/ScriptDto.cs b/src/modules/scripting/Application/DTOs/ScriptDto.cs
new file mode 100644
index 0000000..310b234
--- /dev/null
+++ b/src/modules/scripting/Application/DTOs/ScriptDto.cs
@@ -0,0 +1,20 @@
+using Scripting.Domain.Enums;
+
+namespace Scripting.Application.DTOs;
+
+///
+/// 脚本函数 DTO,用于脚本管理页面和按钮动作选择器。
+/// 仅承载 C# 脚本字段(HTTP 类型函数已按 §11 YAGNI 移除)。
+///
+public sealed record ScriptDto(
+ Guid Id,
+ Guid? FormDefinitionId,
+ string Name,
+ string DisplayName,
+ string? Description,
+ string Code,
+ string? InputSchema,
+ string? OutputSchema,
+ ScriptStatus Status,
+ int Version,
+ int TimeoutMs);
diff --git a/src/modules/scripting/Application/DTOs/ScriptExecutionLogDto.cs b/src/modules/scripting/Application/DTOs/ScriptExecutionLogDto.cs
new file mode 100644
index 0000000..ee5f702
--- /dev/null
+++ b/src/modules/scripting/Application/DTOs/ScriptExecutionLogDto.cs
@@ -0,0 +1,19 @@
+using Scripting.Domain.Enums;
+
+namespace Scripting.Application.DTOs;
+
+///
+/// 脚本执行日志 DTO。注意:底层实体字段名仍是 FunctionId/FunctionName(T2 迁移保留),
+/// 此处 DTO 使用 ScriptId/ScriptName 以提升对外清晰度,由映射器做字段重命名。
+///
+public sealed record ScriptExecutionLogDto(
+ Guid Id,
+ Guid ScriptId,
+ string ScriptName,
+ ScriptExecutionStatus Status,
+ string InputJson,
+ string? ResultJson,
+ string? ErrorMessage,
+ long DurationMs,
+ DateTime CreatedAt,
+ string? LogLines);
diff --git a/src/modules/scripting/Application/Handlers/CreateScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/CreateScriptCommandHandler.cs
new file mode 100644
index 0000000..a1c97fe
--- /dev/null
+++ b/src/modules/scripting/Application/Handlers/CreateScriptCommandHandler.cs
@@ -0,0 +1,41 @@
+using BuildFlow.Shared.Domain.Exceptions;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+using Scripting.Domain.Entities;
+using Scripting.Domain.Enums;
+using Scripting.Infrastructure.Persistence;
+
+namespace Scripting.Application.Handlers;
+
+/// 创建脚本函数处理器。校验 Name 唯一,保证按钮动作引用稳定;TimeoutMs 默认 5000ms。
+public class CreateScriptCommandHandler(ScriptingDbContext db)
+ : IRequestHandler
+{
+ public async Task Handle(CreateScriptCommand request, CancellationToken ct)
+ {
+ if (await db.Scripts.AnyAsync(x => x.Name == request.Name, ct))
+ {
+ throw new BusinessException($"脚本 {request.Name} 已存在");
+ }
+
+ var entity = new ScriptFunction
+ {
+ Id = Guid.NewGuid(),
+ FormDefinitionId = request.FormDefinitionId,
+ Name = request.Name,
+ DisplayName = request.DisplayName,
+ Description = request.Description,
+ Code = request.Code,
+ InputSchema = request.InputSchema,
+ OutputSchema = request.OutputSchema,
+ Status = ScriptStatus.Draft,
+ Version = 1,
+ TimeoutMs = request.TimeoutMs <= 0 ? 5000 : request.TimeoutMs,
+ };
+ db.Scripts.Add(entity);
+ await db.SaveChangesAsync(ct);
+ return ScriptMapper.ToDto(entity);
+ }
+}
diff --git a/src/modules/scripting/Application/Handlers/DeleteScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/DeleteScriptCommandHandler.cs
new file mode 100644
index 0000000..6a8f4c3
--- /dev/null
+++ b/src/modules/scripting/Application/Handlers/DeleteScriptCommandHandler.cs
@@ -0,0 +1,27 @@
+using BuildFlow.Shared.Domain.Exceptions;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Scripting.Application.Commands;
+using Scripting.Domain.Enums;
+using Scripting.Infrastructure.Persistence;
+
+namespace Scripting.Application.Handlers;
+
+/// 删除脚本函数处理器(软删除)。Published 状态必须先 Disable 才能删除。
+public class DeleteScriptCommandHandler(ScriptingDbContext db)
+ : IRequestHandler
+{
+ public async Task Handle(DeleteScriptCommand request, CancellationToken ct)
+ {
+ var entity = await db.Scripts.FindAsync([request.Id], ct)
+ ?? throw new BusinessException($"脚本 {request.Id} 不存在");
+
+ if (entity.Status == ScriptStatus.Published)
+ {
+ throw new BusinessException("已发布的脚本不能删除,请先禁用");
+ }
+
+ entity.IsDeleted = true;
+ await db.SaveChangesAsync(ct);
+ }
+}
diff --git a/src/modules/scripting/Application/Handlers/DisableScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/DisableScriptCommandHandler.cs
new file mode 100644
index 0000000..d624f06
--- /dev/null
+++ b/src/modules/scripting/Application/Handlers/DisableScriptCommandHandler.cs
@@ -0,0 +1,23 @@
+using BuildFlow.Shared.Domain.Exceptions;
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+using Scripting.Domain.Enums;
+using Scripting.Infrastructure.Persistence;
+
+namespace Scripting.Application.Handlers;
+
+/// 禁用脚本函数处理器。置 Disabled 后动作链与 HTTP invoke 都将拒绝执行。
+public class DisableScriptCommandHandler(ScriptingDbContext db)
+ : IRequestHandler
+{
+ public async Task Handle(DisableScriptCommand request, CancellationToken ct)
+ {
+ var entity = await db.Scripts.FindAsync([request.Id], ct)
+ ?? throw new BusinessException($"脚本 {request.Id} 不存在");
+
+ entity.Status = ScriptStatus.Disabled;
+ await db.SaveChangesAsync(ct);
+ return ScriptMapper.ToDto(entity);
+ }
+}
diff --git a/src/modules/scripting/Application/Handlers/GetScriptDetailQueryHandler.cs b/src/modules/scripting/Application/Handlers/GetScriptDetailQueryHandler.cs
new file mode 100644
index 0000000..b966e2c
--- /dev/null
+++ b/src/modules/scripting/Application/Handlers/GetScriptDetailQueryHandler.cs
@@ -0,0 +1,20 @@
+using BuildFlow.Shared.Domain.Exceptions;
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+using Scripting.Application.Queries;
+using Scripting.Infrastructure.Persistence;
+
+namespace Scripting.Application.Handlers;
+
+/// 脚本详情查询处理器:FindAsync 取实体,缺失抛 BusinessException。
+public sealed class GetScriptDetailQueryHandler(ScriptingDbContext db)
+ : IRequestHandler
+{
+ public async Task Handle(GetScriptDetailQuery request, CancellationToken ct)
+ {
+ var entity = await db.Scripts.FindAsync([request.Id], ct)
+ ?? throw new BusinessException($"脚本 {request.Id} 不存在");
+ return ScriptMapper.ToDto(entity);
+ }
+}
diff --git a/src/modules/scripting/Application/Handlers/GetScriptExecutionLogsQueryHandler.cs b/src/modules/scripting/Application/Handlers/GetScriptExecutionLogsQueryHandler.cs
new file mode 100644
index 0000000..02fdca7
--- /dev/null
+++ b/src/modules/scripting/Application/Handlers/GetScriptExecutionLogsQueryHandler.cs
@@ -0,0 +1,52 @@
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Scripting.Application.Common;
+using Scripting.Application.DTOs;
+using Scripting.Application.Queries;
+using Scripting.Infrastructure.Persistence;
+
+namespace Scripting.Application.Handlers;
+
+/// 脚本执行日志查询处理器:按 FunctionId 过滤,可选状态过滤,按 CreatedAt 倒序分页。
+public sealed class GetScriptExecutionLogsQueryHandler(ScriptingDbContext db)
+ : IRequestHandler>
+{
+ public async Task> Handle(
+ GetScriptExecutionLogsQuery request,
+ CancellationToken ct)
+ {
+ var query = db.ScriptExecutionLogs
+ .Where(x => x.FunctionId == request.ScriptId);
+
+ if (request.Status.HasValue)
+ {
+ query = query.Where(x => x.Status == request.Status.Value);
+ }
+
+ var total = await query.CountAsync(ct);
+ var items = await query
+ .OrderByDescending(x => x.CreatedAt)
+ .Skip((request.PageIndex - 1) * request.PageSize)
+ .Take(request.PageSize)
+ .Select(x => new ScriptExecutionLogDto(
+ x.Id,
+ x.FunctionId,
+ x.FunctionName,
+ x.Status,
+ x.InputJson,
+ x.ResultJson,
+ x.ErrorMessage,
+ x.DurationMs,
+ x.CreatedAt,
+ x.LogLines))
+ .ToListAsync(ct);
+
+ return new PagedResult
+ {
+ Items = items,
+ Total = total,
+ PageIndex = request.PageIndex,
+ PageSize = request.PageSize,
+ };
+ }
+}
diff --git a/src/modules/scripting/Application/Handlers/GetScriptListQueryHandler.cs b/src/modules/scripting/Application/Handlers/GetScriptListQueryHandler.cs
new file mode 100644
index 0000000..7cab3e4
--- /dev/null
+++ b/src/modules/scripting/Application/Handlers/GetScriptListQueryHandler.cs
@@ -0,0 +1,64 @@
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Scripting.Application.Common;
+using Scripting.Application.DTOs;
+using Scripting.Application.Queries;
+using Scripting.Infrastructure.Persistence;
+
+namespace Scripting.Application.Handlers;
+
+/// 脚本列表查询处理器:过滤 + 按 UpdatedAt 倒序分页,投影到 ScriptDto。
+public sealed class GetScriptListQueryHandler(ScriptingDbContext db)
+ : IRequestHandler>
+{
+ public async Task> Handle(
+ GetScriptListQuery request,
+ CancellationToken ct)
+ {
+ var query = db.Scripts.AsQueryable();
+
+ if (request.Status.HasValue)
+ {
+ query = query.Where(x => x.Status == request.Status.Value);
+ }
+
+ if (request.FormDefinitionId.HasValue)
+ {
+ query = query.Where(x => x.FormDefinitionId == request.FormDefinitionId.Value);
+ }
+
+ if (!string.IsNullOrWhiteSpace(request.Keyword))
+ {
+ query = query.Where(x =>
+ x.Name.Contains(request.Keyword) ||
+ x.DisplayName.Contains(request.Keyword));
+ }
+
+ var total = await query.CountAsync(ct);
+ var items = await query
+ .OrderByDescending(x => x.UpdatedAt)
+ .Skip((request.PageIndex - 1) * request.PageSize)
+ .Take(request.PageSize)
+ .Select(x => new ScriptDto(
+ x.Id,
+ x.FormDefinitionId,
+ x.Name,
+ x.DisplayName,
+ x.Description,
+ x.Code,
+ x.InputSchema,
+ x.OutputSchema,
+ x.Status,
+ x.Version,
+ x.TimeoutMs))
+ .ToListAsync(ct);
+
+ return new PagedResult
+ {
+ Items = items,
+ Total = total,
+ PageIndex = request.PageIndex,
+ PageSize = request.PageSize,
+ };
+ }
+}
diff --git a/src/modules/scripting/Application/Handlers/InvokeScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/InvokeScriptCommandHandler.cs
new file mode 100644
index 0000000..3fc5237
--- /dev/null
+++ b/src/modules/scripting/Application/Handlers/InvokeScriptCommandHandler.cs
@@ -0,0 +1,14 @@
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+using Scripting.Application.Services;
+
+namespace Scripting.Application.Handlers;
+
+/// 按 Name 调用脚本处理器,委托执行服务(仅 Published 脚本可调用)。
+public class InvokeScriptCommandHandler(IScriptExecutionService executionService)
+ : IRequestHandler
+{
+ public Task Handle(InvokeScriptCommand request, CancellationToken ct)
+ => executionService.ExecuteByNameAsync(request.Name, request.InputJson, ct);
+}
diff --git a/src/modules/scripting/Application/Handlers/PublishScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/PublishScriptCommandHandler.cs
new file mode 100644
index 0000000..4e3ac0f
--- /dev/null
+++ b/src/modules/scripting/Application/Handlers/PublishScriptCommandHandler.cs
@@ -0,0 +1,24 @@
+using BuildFlow.Shared.Domain.Exceptions;
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+using Scripting.Domain.Enums;
+using Scripting.Infrastructure.Persistence;
+
+namespace Scripting.Application.Handlers;
+
+/// 发布脚本函数处理器。置 Published 并 Version++,便于后续审计变更。
+public class PublishScriptCommandHandler(ScriptingDbContext db)
+ : IRequestHandler
+{
+ public async Task Handle(PublishScriptCommand request, CancellationToken ct)
+ {
+ var entity = await db.Scripts.FindAsync([request.Id], ct)
+ ?? throw new BusinessException($"脚本 {request.Id} 不存在");
+
+ entity.Status = ScriptStatus.Published;
+ entity.Version++;
+ await db.SaveChangesAsync(ct);
+ return ScriptMapper.ToDto(entity);
+ }
+}
diff --git a/src/modules/scripting/Application/Handlers/TestScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/TestScriptCommandHandler.cs
new file mode 100644
index 0000000..d66fce9
--- /dev/null
+++ b/src/modules/scripting/Application/Handlers/TestScriptCommandHandler.cs
@@ -0,0 +1,14 @@
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+using Scripting.Application.Services;
+
+namespace Scripting.Application.Handlers;
+
+/// 测试执行脚本处理器,委托执行服务以保证测试与运行时一致。
+public class TestScriptCommandHandler(IScriptExecutionService executionService)
+ : IRequestHandler
+{
+ public Task Handle(TestScriptCommand request, CancellationToken ct)
+ => executionService.ExecuteTestAsync(request.Id, request.InputJson, ct);
+}
diff --git a/src/modules/scripting/Application/Handlers/UpdateScriptCommandHandler.cs b/src/modules/scripting/Application/Handlers/UpdateScriptCommandHandler.cs
new file mode 100644
index 0000000..4b333c4
--- /dev/null
+++ b/src/modules/scripting/Application/Handlers/UpdateScriptCommandHandler.cs
@@ -0,0 +1,42 @@
+using BuildFlow.Shared.Domain.Exceptions;
+using MediatR;
+using Scripting.Application.Commands;
+using Scripting.Application.DTOs;
+using Scripting.Domain.Enums;
+using Scripting.Infrastructure.Persistence;
+
+namespace Scripting.Application.Handlers;
+
+///
+/// 更新脚本函数处理器。Disabled 禁止编辑;Published 编辑保持已发布但 Version++(调用方可据此感知变更)。
+///
+public class UpdateScriptCommandHandler(ScriptingDbContext db)
+ : IRequestHandler
+{
+ public async Task Handle(UpdateScriptCommand request, CancellationToken ct)
+ {
+ var entity = await db.Scripts.FindAsync([request.Id], ct)
+ ?? throw new BusinessException($"脚本 {request.Id} 不存在");
+
+ if (entity.Status == ScriptStatus.Disabled)
+ {
+ throw new BusinessException("已禁用的脚本不能编辑");
+ }
+
+ entity.DisplayName = request.DisplayName;
+ entity.Description = request.Description;
+ entity.Code = request.Code;
+ entity.InputSchema = request.InputSchema;
+ entity.OutputSchema = request.OutputSchema;
+ entity.TimeoutMs = request.TimeoutMs <= 0 ? 5000 : request.TimeoutMs;
+
+ // 已发布脚本被编辑后保持已发布,但递增版本号,便于调用方/审计感知变更。
+ if (entity.Status == ScriptStatus.Published)
+ {
+ entity.Version++;
+ }
+
+ await db.SaveChangesAsync(ct);
+ return ScriptMapper.ToDto(entity);
+ }
+}
diff --git a/src/modules/scripting/Application/Queries/GetScriptDetailQuery.cs b/src/modules/scripting/Application/Queries/GetScriptDetailQuery.cs
new file mode 100644
index 0000000..638caa8
--- /dev/null
+++ b/src/modules/scripting/Application/Queries/GetScriptDetailQuery.cs
@@ -0,0 +1,7 @@
+using MediatR;
+using Scripting.Application.DTOs;
+
+namespace Scripting.Application.Queries;
+
+/// 按 Id 查询脚本函数详情。
+public sealed record GetScriptDetailQuery(Guid Id) : IRequest;
diff --git a/src/modules/scripting/Application/Queries/GetScriptExecutionLogsQuery.cs b/src/modules/scripting/Application/Queries/GetScriptExecutionLogsQuery.cs
new file mode 100644
index 0000000..8f126b0
--- /dev/null
+++ b/src/modules/scripting/Application/Queries/GetScriptExecutionLogsQuery.cs
@@ -0,0 +1,13 @@
+using MediatR;
+using Scripting.Application.Common;
+using Scripting.Application.DTOs;
+using Scripting.Domain.Enums;
+
+namespace Scripting.Application.Queries;
+
+/// 分页查询某脚本的执行日志。注意底层实体字段是 FunctionId(映射自 ScriptId)。
+public sealed record GetScriptExecutionLogsQuery(
+ Guid ScriptId,
+ int PageIndex,
+ int PageSize,
+ ScriptExecutionStatus? Status = null) : IRequest>;
diff --git a/src/modules/scripting/Application/Queries/GetScriptListQuery.cs b/src/modules/scripting/Application/Queries/GetScriptListQuery.cs
new file mode 100644
index 0000000..de45305
--- /dev/null
+++ b/src/modules/scripting/Application/Queries/GetScriptListQuery.cs
@@ -0,0 +1,14 @@
+using MediatR;
+using Scripting.Application.Common;
+using Scripting.Application.DTOs;
+using Scripting.Domain.Enums;
+
+namespace Scripting.Application.Queries;
+
+/// 分页查询脚本函数。支持按状态、表单作用域、关键字(Name/DisplayName)筛选。
+public sealed record GetScriptListQuery(
+ int PageIndex,
+ int PageSize,
+ ScriptStatus? Status,
+ string? Keyword,
+ Guid? FormDefinitionId = null) : IRequest>;