build-flow.api/docs/superpowers/specs/2026-07-06-scripting-module-design.md
xiangning b0cf3949d0 feat(workflow): 扩展按钮定义功能并优化错误处理
- 实现三阶段动作链:前置钩子(BeforeActionsJson)、主链(ActionsJson)、后置钩子(AfterActionsJson)
- 扩展自定义函数支持HTTP请求类型,添加相关字段配置
- 重构异常处理机制,引入语义错误码字符串替代数字码
- 优化数据库表名前缀,统一使用b_前缀提升命名规范性
- 实现菜单路由名称自动派生,支持Code到Name的转换
- 重构错误响应信封,code字段改为语义错误码字符串格式
- 添加ImportJobProcessorService后台服务支持
- 优化Swagger文档默认URL重定向逻辑
- 修复IM模块列缺失的数据库补丁机制
2026-07-06 17:27:32 +08:00

470 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`,前端能查看。