- 实现三阶段动作链:前置钩子(BeforeActionsJson)、主链(ActionsJson)、后置钩子(AfterActionsJson) - 扩展自定义函数支持HTTP请求类型,添加相关字段配置 - 重构异常处理机制,引入语义错误码字符串替代数字码 - 优化数据库表名前缀,统一使用b_前缀提升命名规范性 - 实现菜单路由名称自动派生,支持Code到Name的转换 - 重构错误响应信封,code字段改为语义错误码字符串格式 - 添加ImportJobProcessorService后台服务支持 - 优化Swagger文档默认URL重定向逻辑 - 修复IM模块列缺失的数据库补丁机制
470 lines
23 KiB
Markdown
470 lines
23 KiB
Markdown
# Scripting 模块设计 · 浏览器可编程 C# 脚本函数(MediatR 全开放)
|
||
|
||
- **状态**:已批准(Approved)
|
||
- **日期**:2026-07-06
|
||
- **作者**:brainstorming session
|
||
- **影响范围**:`build-flow.api`(新建 `modules/scripting/`,改造 `workflow` 按钮链,删除旧 CustomFunction)、`build-flow.web`(新建 `views/scripts/`)
|
||
|
||
---
|
||
|
||
## 一、目标与现状差距
|
||
|
||
把现有的"自定义函数"能力从 `workflow` 模块解耦,独立成 `scripting` 模块,实现**浏览器里写 C# 脚本 → 通过 MediatR 调用系统任意内部方法**,作为"函数即服务"被 HTTP 按名调用、按钮动作链触发。
|
||
|
||
现状痛点(已通过代码探索确认):
|
||
|
||
| 维度 | 现状 | 痛点 |
|
||
|------|------|------|
|
||
| 位置 | 耦合在 `workflow/Application/BuildFlow/CustomFunctions/`,唯一调用方是按钮动作链 | 无法跨模块复用 |
|
||
| 脚本能力 | `CustomFunctionScriptGlobals` 只注入 `Variables` + `InputJson` | **只能纯计算,不能调用任何系统内部方法** |
|
||
| 编辑器 | 前端只是个 `Textarea`(`create-form.vue:44-48`) | 无 C# 语法高亮/补全 |
|
||
| 编译 | 每次执行都重新 `CSharpScript.EvaluateAsync`(`CustomFunctionExecutionService.cs:81`) | 无编译缓存,性能差 |
|
||
| 可编辑性 | 无 Update 端点(搜索 `UpdateCustomFunction` 为空) | 函数创建后**不可修改** |
|
||
| 测试 | `TestCustomFunctionCommandHandler` 走正式执行服务,要求 `Published`(`CustomFunctionExecutionService.cs:36-39`) | **草稿不能测试** |
|
||
| 其他 | 无版本历史、无执行日志查看页、无 Validator、沙箱仅靠隐式引用裁剪 | 体验和安全都差 |
|
||
|
||
---
|
||
|
||
## 二、核心决策(已与用户确认)
|
||
|
||
| # | 决策点 | 选择 | 理由 |
|
||
|---|--------|------|------|
|
||
| 1 | 脚本能调用什么 | **MediatR 全开放** | 最大灵活性,复用所有现有 CQRS 能力 |
|
||
| 2 | 与现有 CustomFunction 关系 | **独立新模块 + 迁移** | 架构最干净,表重命名保数据,旧代码删除 |
|
||
| 3 | 脚本执行身份 | **触发者身份** | 最小权限,零额外权限代码,复用现有权限拦截 |
|
||
| 4 | 前端编辑器 | **Monaco + 智能提示面板** | 性价比最高,C# 高亮 + 可调用方法/Command 面板 |
|
||
| 5 | 调用入口 | **HTTP 按名调用 + 按钮动作链触发** | "函数即服务"的核心 + 兼容现有低代码使用者 |
|
||
| 6 | 安全沙箱 | **程序集白名单** | 用 Roslyn `WithReferences` + `WithImports` 裁剪,best-effort |
|
||
| 7 | 元数据来源 | **反射自动扫描** MediatR request 类型 | 零维护,自动追随代码变更 |
|
||
| 8 | 迁移策略 | **保数据重命名表**(`b_funcs` → `funcs`) | 保留生产数据,仅升级能力 |
|
||
|
||
---
|
||
|
||
## 三、模块架构
|
||
|
||
### 3.1 目录结构
|
||
|
||
```
|
||
build-flow.api/src/modules/scripting/
|
||
├── Domain/
|
||
│ ├── Entities/
|
||
│ │ ├── ScriptFunction.cs (从 CustomFunction 迁移)
|
||
│ │ └── ScriptExecutionLog.cs
|
||
│ └── Enums/
|
||
│ ├── ScriptStatus.cs (Draft=0/Published=1/Disabled=2)
|
||
│ └── ScriptExecutionStatus.cs (Success=0/Failed=1/Timeout=2)
|
||
├── Application/
|
||
│ ├── Commands/
|
||
│ │ ├── CreateScriptCommand.cs + Handler
|
||
│ │ ├── UpdateScriptCommand.cs + Handler ← 新增能力
|
||
│ │ ├── DeleteScriptCommand.cs + Handler ← 新增能力
|
||
│ │ ├── PublishScriptCommand.cs + Handler
|
||
│ │ ├── DisableScriptCommand.cs + Handler
|
||
│ │ ├── TestScriptCommand.cs + Handler ← 草稿可测
|
||
│ │ └── InvokeScriptCommand.cs + Handler ← HTTP 按名调用
|
||
│ ├── Queries/
|
||
│ │ ├── GetScriptListQuery.cs + Handler
|
||
│ │ ├── GetScriptDetailQuery.cs + Handler ← 新增
|
||
│ │ ├── GetScriptExecutionLogsQuery.cs + Handler ← 新增
|
||
│ │ └── GetScriptMetaQuery.cs + Handler ← 反射扫描元数据
|
||
│ ├── Host/
|
||
│ │ ├── ScriptGlobals.cs ← 注入 ISender + ICurrentUserContext + Ct + Logger
|
||
│ │ └── ScriptLogger.cs ← 受控日志,写入 ScriptExecutionLog.LogLines
|
||
│ ├── Services/
|
||
│ │ ├── IScriptExecutionService.cs (ExecuteByIdAsync / ExecuteByNameAsync)
|
||
│ │ ├── ScriptExecutionService.cs (Roslyn + 沙箱 + 编译缓存)
|
||
│ │ └── ScriptTypeRegistry.cs ← 启动反射扫描 MediatR 类型
|
||
│ ├── DTOs/ (ScriptDto / ExecutionResultDto / ScriptTypeMetaDto)
|
||
│ └── ScriptingApplicationModule.cs
|
||
└── Infrastructure/
|
||
├── Persistence/
|
||
│ ├── ScriptingDbContext.cs
|
||
│ └── Configurations/
|
||
│ ├── ScriptFunctionConfiguration.cs
|
||
│ └── ScriptExecutionLogConfiguration.cs
|
||
└── ScriptingInfrastructureModule.cs
|
||
```
|
||
|
||
### 3.2 模块依赖(ABP `[DependsOn]`)
|
||
|
||
```
|
||
BuildFlowHostModule
|
||
├── WorkflowApplicationModule
|
||
│ └── ScriptingApplicationModule ← 新增依赖(workflow 用 IScriptExecutionService)
|
||
│ └── PlatformApplicationModule (复用 ICurrentUserContext、用户查询等)
|
||
├── ScriptingInfrastructureModule ← 新增
|
||
└── ...(其他模块不变)
|
||
```
|
||
|
||
工作流模块通过 `[DependsOn(typeof(ScriptingApplicationModule))]` 引入脚本能力,注入 `IScriptExecutionService`。脚本执行服务反过来通过 MediatR `ISender` 调用任意模块的 Command/Query Handler,**不会产生循环引用**(MediatR 是松耦合的运行时分发,不是编译期依赖)。
|
||
|
||
---
|
||
|
||
## 四、脚本运行时(核心)
|
||
|
||
### 4.1 ScriptGlobals —— 用户脚本可见的宿主对象
|
||
|
||
```csharp
|
||
public sealed class ScriptGlobals
|
||
{
|
||
public IReadOnlyDictionary<string, object?> Variables { get; } // 输入变量(已转原生类型)
|
||
public string InputJson { get; } // 原始输入 JSON 兜底
|
||
public ISender Mediator { get; } // ← MediatR 全开放入口
|
||
public ICurrentUserContext CurrentUser { get; } // 当前用户
|
||
public CancellationToken CancellationToken { get; } // 必须传给 Mediator.Send
|
||
public ScriptLogger Logger { get; } // 受控日志
|
||
public IServiceProvider Services { get; } // 逃生舱(按需 Resolve<T>)
|
||
}
|
||
```
|
||
|
||
**用户脚本示例**(HTTP 调用场景):
|
||
|
||
```csharp
|
||
using BuildFlow.Platform.Application.Users.Queries;
|
||
|
||
var q = new GetUserQuery { Id = (Guid)Variables["userId"] };
|
||
var user = await Mediator.Send(q, CancellationToken);
|
||
Logger.Info($"查询到用户 {user.Name}");
|
||
|
||
return new { name = user.Name, level = user.Level };
|
||
```
|
||
|
||
### 4.2 触发者身份的工作原理(无需额外权限代码)
|
||
|
||
这是本设计最关键的安全特性:
|
||
|
||
- `CurrentUserContext`(`BuildFlow.Host/Services/Users/CurrentUserContext.cs`)基于 `IHttpContextAccessor`,从 JWT claims 解析用户
|
||
- 脚本里 `await Mediator.Send(handler)` 执行时,handler 内部注入的 `ICurrentUserContext` 仍指向**触发 HTTP 请求的同一个用户**(因为整个调用链在同一个 HTTP 请求 Scope 内)
|
||
- 该用户没有 `user.delete` 权限 → handler 内的权限拦截自然拒绝(抛 `UnauthorizedException`)
|
||
- **零额外权限代码**,完整复用现有所有 Command/Query Handler 的权限拦截逻辑
|
||
|
||
这意味着:脚本不可能"提权"。普通用户触发的脚本,永远只能做该用户本身能做的事。
|
||
|
||
### 4.3 变量类型升级(修现状痛点)
|
||
|
||
现状 `Variables` 的值是 `JsonElement`,脚本里要写:
|
||
|
||
```csharp
|
||
// 现状(冗长)
|
||
var amount = ((System.Text.Json.JsonElement)Variables["amount"]).GetDecimal();
|
||
```
|
||
|
||
新版 `ParseVariables` 在解析 inputJson 时把 `JsonElement` 转换为 .NET 原生类型:
|
||
|
||
| JSON 类型 | .NET 类型 |
|
||
|-----------|-----------|
|
||
| 整数 | `long` |
|
||
| 浮点 | `double` |
|
||
| 数字(字符串形式为 decimal 时) | `decimal` |
|
||
| 布尔 | `bool` |
|
||
| 字符串 | `string`(尝试解析为 DateTime/Guid,成功则用原生类型) |
|
||
| 日期字符串 | `DateTime` |
|
||
| Guid 字符串 | `Guid` |
|
||
| 对象/数组 | 保留 `JsonElement`(兜底) |
|
||
| null | `null` |
|
||
|
||
效果:脚本里写 `(decimal)Variables["amount"]` 即可。
|
||
|
||
### 4.4 编译缓存(修现状痛点)
|
||
|
||
现状每次 `CSharpScript.EvaluateAsync` 重新编译,几十到几百 ms。
|
||
|
||
新版用 `CSharpScript.Create(code, options, globalsType)` 创建 `Script<object>` 对象,调用 `.CreateDelegate()` 得到可复用委托,按 `code` 的 SHA256 哈希缓存到 `ConcurrentDictionary<string, ScriptDelegate>`。
|
||
|
||
- globals 类型固定 `ScriptGlobals`,所以委托可复用
|
||
- 每次执行只 new 一个 ScriptGlobals 实例传给委托
|
||
- 脚本更新(草稿编辑、发布)后 code 哈希变化,自然走新缓存项
|
||
- 缓存上限配置(默认 500 条,简单的 LRU 淘汰)
|
||
|
||
---
|
||
|
||
## 五、安全沙箱(程序集白名单)
|
||
|
||
### 5.1 程序集白名单
|
||
|
||
`ScriptOptions.WithReferences(...)` **显式**列出允许引用的程序集。
|
||
|
||
**允许**(脚本编译时可见):
|
||
- `System.Private.CoreLib`(`typeof(object).Assembly`)
|
||
- `System.Linq`(`typeof(Enumerable).Assembly`)
|
||
- `System.Text.Json`(`typeof(JsonElement).Assembly`)
|
||
- `System.Collections`
|
||
- `MediatR`(`typeof(ISender).Assembly`)
|
||
- 所有 `BuildFlow.*.Application` 程序集(脚本要 `new GetUserQuery()`)
|
||
- 所有 `BuildFlow.*.Domain` 程序集(DTO/枚举类型)
|
||
- `BuildFlow.Shared` 程序集
|
||
|
||
**禁止**(关键安全约束):
|
||
- ❌ 所有 `*.Infrastructure` 程序集 → 脚本**不能直接 new DbContext** 绕过业务规则
|
||
- ❌ `System.Reflection.*` → 防反射探索
|
||
- ❌ `System.IO.*` / `System.IO.FileSystem` → 禁文件访问
|
||
- ❌ `System.Net.Http.*` / `System.Net.Sockets` → 禁直接网络(必须走业务方法)
|
||
- ❌ `System.Diagnostics.Process` → 禁启进程
|
||
|
||
### 5.2 using 白名单
|
||
|
||
`ScriptOptions.WithImports(...)` 显式列允许的命名空间:
|
||
|
||
```
|
||
System, System.Linq, System.Text.Json, System.Collections.Generic,
|
||
System.Threading, System.Threading.Tasks,
|
||
MediatR,
|
||
BuildFlow.Shared.Domain.Common,
|
||
BuildFlow.Platform.Application.*, BuildFlow.Application.* (按实际 Application 命名空间)
|
||
```
|
||
|
||
### 5.3 软约束:代码黑名单扫描
|
||
|
||
`System.Private.CoreLib` 通过 `typeof(object).Assembly` 仍隐式可达,理论上能 `typeof(object).Assembly.GetType("System.Reflection.Assembly")` 反射探索。这是 Roslyn scripting 的固有限制。
|
||
|
||
**缓解**:编译前对脚本源码做正则扫描,命中以下模式直接拒绝编译(返回 `BusinessException("脚本包含禁用 API")`):
|
||
|
||
- `System.Reflection`
|
||
- `System.IO.File` / `System.IO.Directory`
|
||
- `System.Net.Http` / `System.Net.Sockets`
|
||
- `System.Diagnostics.Process`
|
||
- `typeof(` 后跟 `.Assembly`(限制 typeof 仅用于已知类型)
|
||
- `AppDomain` / `Assembly.Load`
|
||
|
||
> **⚠ 重要标注**:这是 **best-effort 软约束**,不是强安全边界。强隔离需上进程级方案(本期不做,见第十一章 YAGNI)。
|
||
|
||
---
|
||
|
||
## 六、调用入口
|
||
|
||
### 6.1 API 端点清单(全部挂在 `/api/scripts`)
|
||
|
||
**管理类**(前端管理页用):
|
||
|
||
| Method | Path | 权限码 | 说明 |
|
||
|--------|------|--------|------|
|
||
| GET | `/api/scripts` | `scripts.read` | 分页列表 |
|
||
| GET | `/api/scripts/{id}` | `scripts.read` | 详情 |
|
||
| POST | `/api/scripts` | `scripts.create` | 创建 |
|
||
| PUT | `/api/scripts/{id}` | `scripts.update` | 更新(**新增**,现状没有) |
|
||
| DELETE | `/api/scripts/{id}` | `scripts.delete` | 删除(**新增**) |
|
||
| POST | `/api/scripts/{id}/publish` | `scripts.publish` | 发布 |
|
||
| POST | `/api/scripts/{id}/disable` | `scripts.disable` | 禁用 |
|
||
| POST | `/api/scripts/{id}/test` | `scripts.test` | 测试(**草稿可测**) |
|
||
| GET | `/api/scripts/{id}/logs` | `scripts.read` | 执行日志(**新增**) |
|
||
|
||
**元数据类**(Monaco 面板用):
|
||
|
||
| Method | Path | 权限码 | 说明 |
|
||
|--------|------|--------|------|
|
||
| GET | `/api/scripts/meta/types` | `scripts.read` | 反射扫描的所有可用 Command/Query |
|
||
| GET | `/api/scripts/meta/assemblies` | `scripts.read` | 可用程序集清单(调试用) |
|
||
|
||
**调用类**(核心):
|
||
|
||
| Method | Path | 权限码 | 说明 |
|
||
|--------|------|--------|------|
|
||
| POST | `/api/scripts/{name}/invoke` | `scripts.run` | 按名调用已发布脚本 |
|
||
|
||
`/invoke` 请求/响应:
|
||
|
||
```json
|
||
// POST /api/scripts/get-user-detail/invoke
|
||
Request: { "input": { "userId": "..." } }
|
||
Response: { "status": "Success", "result": {...}, "durationMs": 123 }
|
||
```
|
||
|
||
### 6.2 按钮动作链改造
|
||
|
||
`BuildFlowButtonActionExecutor`(`workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs`)改造点:
|
||
|
||
- 依赖从 `ICustomFunctionExecutionService` 改为 `IScriptExecutionService`
|
||
- `runFunction` action 的 `target`(GUID)保持不变 → 调 `ExecuteByIdAsync`
|
||
- Scope 校验(`FormDefinitionId` 必须匹配按钮的表单,`EnsureFunctionScopeAsync`)逻辑迁移过来,**不变**
|
||
- `BuildFunctionInputJson`(`$input.xxx` 占位符映射)逻辑迁移过来,**不变**
|
||
|
||
### 6.3 ScriptTypeRegistry —— 反射扫描元数据
|
||
|
||
启动时(`ScriptingInfrastructureModule` 或 hosted service 初始化):
|
||
|
||
1. 遍历 `AppDomain.CurrentDomain.GetAssemblies()` 中所有 `BuildFlow.*` 程序集
|
||
2. 找出所有实现 `IBaseRequest`(MediatR)的类型
|
||
3. 提取每个类型:`FullName`、`Namespace`、所有公开属性(名+类型+XML 注释)、所属程序集、是 Command 还是 Query(按返回类型推断)
|
||
4. 缓存到 `IMemoryCache`,key=`script-types`,TTL=10 分钟(首次加载后基本不变)
|
||
5. `GET /api/scripts/meta/types?keyword=GetUser&kind=Query&page=1` 支持分页+搜索
|
||
|
||
前端 Monaco 用这个端点填充"方法面板"。
|
||
|
||
---
|
||
|
||
## 七、数据模型与迁移
|
||
|
||
### 7.1 表迁移(EF Core Migration)
|
||
|
||
**RENAME 保数据**:
|
||
|
||
- `b_funcs` → `funcs`
|
||
- `b_func_execution_logs` → `func_execution_logs`
|
||
|
||
字段调整:
|
||
|
||
- `funcs` 新增列:`LogLines`(text, 可空)— 测试时收集 Logger 输出
|
||
- 其他列保持不变(`Id, FormDefinitionId, Name, DisplayName, Description, Code, InputSchema, OutputSchema, Status, Version, TimeoutMs, 审计字段`)
|
||
|
||
枚举迁移:
|
||
|
||
- `BuildFlowMetadataStatus` → `ScriptStatus`(值映射 0/1/2 不变)
|
||
- `CustomFunctionExecutionStatus` → `ScriptExecutionStatus`(值不变)
|
||
|
||
### 7.2 SeedData 迁移
|
||
|
||
旧权限码删除(来自 `SeedData.cs:149-153`):
|
||
|
||
- `buildflow:function:read/create/publish/disable/test`
|
||
|
||
新权限码(添加到 admin 角色):
|
||
|
||
- `scripts.read / create / update / publish / disable / delete / test / run`
|
||
|
||
菜单迁移(来自 `SeedData.cs:242`):
|
||
|
||
- 旧 `buildflow.functions` → `/buildflow/custom-functions/index`
|
||
- 新 `scripts` → `/scripts/index`
|
||
|
||
---
|
||
|
||
## 八、前端(build-flow.web)
|
||
|
||
### 8.1 新增依赖
|
||
|
||
`apps/web-antdv-next/package.json` 增加:
|
||
|
||
- `monaco-editor`(C# 语言支持)
|
||
- `@guolao/vue-monaco-editor`(Vue 3 封装,与 vben 兼容)
|
||
|
||
### 8.2 页面结构
|
||
|
||
`apps/web-antdv-next/src/views/scripts/`:
|
||
|
||
```
|
||
index.vue 列表页(VxeGrid,复刻现有 custom-functions/index.vue)
|
||
data.ts 列定义
|
||
detail.vue 详情+编辑页(替换 create-form.vue)
|
||
modules/
|
||
script-editor.vue Monaco 编辑器组件(C# 语法高亮+补全)
|
||
method-panel.vue 方法面板( Queries/Commands Tab + 搜索 + 点击插入 )
|
||
test-modal.vue 测试弹窗(schema 表单输入 + 控制台输出 + JSON 结果)
|
||
log-drawer.vue 执行日志抽屉
|
||
route-context.ts 路由 meta 处理
|
||
```
|
||
|
||
### 8.3 编辑器布局(detail.vue)
|
||
|
||
```
|
||
┌────────────────────────────┬─────────────────┐
|
||
│ Monaco C# 编辑器 │ 方法面板 │
|
||
│ │ 🔍 搜索... │
|
||
│ using System; │ │
|
||
│ using MediatR; │ 📦 Queries │
|
||
│ │ • GetUser │
|
||
│ var q = new GetUserQuery │ • ListUsers │
|
||
│ { │ 📦 Commands │
|
||
│ Id = (Guid)Vars["uid"] │ • UpdateUser │
|
||
│ }; │ • DeleteUser │
|
||
│ var u = await Mediator │ │
|
||
│ .Send(q, CancellationToken)│ 点击 → 插入代码 │
|
||
│ return u.Name; │ │
|
||
├────────────────────────────┴─────────────────┤
|
||
│ [测试运行] [保存草稿] [发布] [查看日志] [禁用] │
|
||
└──────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 8.4 测试运行改进(修现状痛点)
|
||
|
||
- 现状草稿不能测试 → **改为草稿/已发布都能测试**
|
||
- 测试时跳过 `Status == Published` 校验,直接用脚本代码运行
|
||
- 输入区:基于 `InputSchema` 动态生成表单(schema-form),不再手填 JSON
|
||
- 结果区:美化 JSON 显示 + Logger 控制台输出区
|
||
- 仍写一条 `ScriptExecutionLog`,`FunctionName` 标注 `[TEST]`
|
||
|
||
### 8.5 API 封装
|
||
|
||
`apps/web-antdv-next/src/api/core/scripts.ts`(新建文件,从 workflow.ts 剥离):
|
||
|
||
- 9 个管理 API + 2 个元数据 API + 1 个 invoke API
|
||
- TypeScript 类型定义(`ScriptsApi.Script`, `ScriptsApi.ExecutionResult`, `ScriptsApi.TypeMeta` 等)
|
||
|
||
---
|
||
|
||
## 九、边缘场景处理
|
||
|
||
| # | 场景 | 处理 |
|
||
|---|------|------|
|
||
| E1 | 死循环 / 死递归 | `TimeoutMs` 兜底(默认 5s,最大 30s),超时强 cancel |
|
||
| E2 | 脚本调脚本(递归 invoke) | `AsyncLocal<int>` 调用深度计数器,超过 5 抛 `BusinessException("脚本调用深度超限")` |
|
||
| E3 | 编译开销 | 见 4.4 编译缓存(按 code 哈希缓存委托,LRU 500 条) |
|
||
| E4 | `await` 不传 CancellationToken | globals 暴露 `CancellationToken`,文档强制传;端点透传超时 token |
|
||
| E5 | 共享 DbContext 并发 | 保持 Scoped 生命周期(每 HTTP 请求一 Scope),与现状一致 |
|
||
| E6 | 直接绕过 MediatR 写库 | **白名单不含 Infrastructure 程序集**,强制走 MediatR 业务路径 |
|
||
| E7 | 反射绕过 | 见 5.3 代码黑名单扫描(best-effort 软约束) |
|
||
| E8 | 超大输入/输出 | inputJson 最大 1MB(端点配置);result 序列化超 1MB 截断存日志 |
|
||
| E9 | 测试模式权限 | 测试入口要求 `scripts.test`(管理员级),仍以触发者身份执行 |
|
||
| E10 | 事务边界 | 每条 `Mediator.Send` 独立事务(与现状一致);脚本内多写操作部分失败需用户自己补偿,文档警告 |
|
||
|
||
---
|
||
|
||
## 十、实现任务拆解(subagent-driven-development)
|
||
|
||
每个 task 边界清晰、上下文自洽,实现后 git commit,下一个 subagent 看到的是已落地代码。每个 task 走 **implementer → spec reviewer → code quality reviewer** 三段式。
|
||
|
||
| # | Task | 模型 | 关键产出 |
|
||
|---|------|------|---------|
|
||
| T1 | 新建 scripting 模块骨架 | cheap | 3 个 csproj(Domain/Application/Infrastructure)+ ABP Module 类 + 挂到 Host `[DependsOn]` |
|
||
| T2 | 迁移 Domain 实体 + EF 配置 + 表重命名迁移 | standard | ScriptFunction/ScriptExecutionLog 实体、ScriptingDbContext、`b_funcs→funcs` 迁移文件 |
|
||
| T3 | ScriptGlobals + ScriptExecutionService + 沙箱 | standard | globals 注入 ISender、变量类型升级、程序集白名单、编译缓存、深度计数器 |
|
||
| T4 | ScriptTypeRegistry 反射扫描 + 元数据端点 | standard | 启动扫描 + `GET /api/scripts/meta/types` |
|
||
| T5 | CQRS Commands/Queries + 管理端点 | standard | Create/Update/Delete/Publish/Disable/Test/Invoke/List/Detail/Logs 全套 |
|
||
| T6 | 按钮动作链改造指向 IScriptExecutionService | standard | `BuildFlowButtonActionExecutor` 改依赖、Scope 校验迁移 |
|
||
| T7 | 删除旧 CustomFunction 代码 + 权限/菜单 SeedData 迁移 | cheap | 删 `workflow/.../CustomFunctions/`、删 `b_func*` 旧配置、SeedData 替换权限码 |
|
||
| T8 | 前端 Monaco + 方法面板 + 编辑/测试/日志页 | standard | scripts/ 视图全集、scripts.ts API、test 支持草稿 |
|
||
| T9 | 集成测试 + 端到端验证 | standard | 后端 `ScriptExecutionTests`(含 MediatR 调用、沙箱拦截、超时、深度)、按钮链回归 |
|
||
|
||
---
|
||
|
||
## 十一、不做的事(YAGNI)
|
||
|
||
明确排除(避免范围蔓延):
|
||
|
||
- ❌ 进程级隔离 / 容器沙箱(best-effort 软沙箱已够本期)
|
||
- ❌ 脚本断点调试 / step debugging(Monaco + 控制台输出足够)
|
||
- ❌ 脚本级分布式事务(每 Send 独立事务)
|
||
- ❌ 函数级权限码 `scripts.run.{name}`(统一 `scripts.run`,按需后期加)
|
||
- ❌ 脚本版本历史快照(Version 自增保留,但不存历史 Code 快照)
|
||
- ❌ Webhook 调用入口 / 后台 SDK 调用(仅 HTTP + 按钮链两个入口)
|
||
|
||
---
|
||
|
||
## 十二、实现后产出清单
|
||
|
||
- `build-flow.api/src/modules/scripting/` 全新三层模块
|
||
- EF Core 迁移:表重命名 + 新增列
|
||
- `BuildFlowButtonActionExecutor` 改造
|
||
- 删除 `workflow/Application/BuildFlow/CustomFunctions/` 全部
|
||
- `apps/web-antdv-next/src/views/scripts/` 全新视图
|
||
- `apps/web-antdv-next/src/api/core/scripts.ts` 新 API 文件
|
||
- 后端集成测试 `ScriptExecutionTests.cs`
|
||
- SeedData 权限码/菜单更新
|
||
- 本设计文档(已就位)
|
||
|
||
---
|
||
|
||
## 十三、验收标准
|
||
|
||
实现完成后,以下场景必须可演示:
|
||
|
||
1. **浏览器写脚本调系统方法**:在前端 Monaco 编辑器里写一段 C# 脚本,调用 `GetUserQuery`,测试运行成功,返回用户数据。
|
||
2. **HTTP 按名调用**:通过 `POST /api/scripts/{name}/invoke` 调用一个已发布脚本,返回正确结果。
|
||
3. **触发者权限生效**:普通用户触发调用 `DeleteUserCommand` 的脚本,被权限拦截拒绝(403)。
|
||
4. **沙箱拦截**:脚本里写 `System.IO.File.ReadAllText("/etc/passwd")` 编译失败,返回"脚本包含禁用 API"。
|
||
5. **草稿可测**:草稿状态脚本能测试运行(修复现状痛点)。
|
||
6. **可编辑**:已创建的脚本能 Update 修改 Code/Schema(修复现状痛点)。
|
||
7. **按钮链回归**:现有 BuildFlow 按钮动作链(`runFunction`)仍能正常触发脚本,行为不变。
|
||
8. **编译缓存**:同一脚本第二次执行明显快于第一次(编译命中缓存)。
|
||
9. **方法面板**:前端 Monaco 右侧面板能列出系统所有 Command/Query,点击插入代码片段。
|
||
10. **执行日志**:每次执行(含测试)都写入 `func_execution_logs`,前端能查看。
|