Title: 🖥️ V8 函数列表 - 后端 Canonical: https://microi.net/doc/v8-engine/v8-server.html Page-Modified: 2026-10-30T18:29:49.000Z Source: published HTML document body; page revision is not a capability verification date. # 🖥️ V8 函数列表 - 后端 服务器端 V8 引擎支持 ES6 语法,集成后端对象和方法 --- ## 📌 介绍 - 服务器端 V8 引擎代码与前端 V8 的编程语言均为 JavaScript 语法 - 服务器端 V8 引擎支持 ES6 语法 - 集成了后端对象、方法,可使用 JS 调用后端方法(非 HTTP) - 服务器端 V8 代码在服务器端执行 - 主要用于表单属性的服务器端 V8 事件、接口引擎、数据源引擎等 ## 接口引擎 V8.ApiEngine 接口引擎详细介绍 (https://microi.net/doc/v8-engine/api-engine.html) 服务器端 V8 事件可以直接调用接口引擎(非 HTTP),接口引擎也可以调用其它接口引擎。传入 V8.DbTrans 时,共享外层事务;不传时由被调用接口引擎管理自己的事务。 StopHttp、允许匿名调用和接口角色限制约束的是外部 HTTP 入口。V8.ApiEngine.Run 属于可信服务端调用,不经过 HTTP 门禁。因此,被其它接口引擎复用的敏感业务仍必须在被调用引擎内部校验当前用户、业务状态和数据范围;能够编辑接口引擎、数据源、Job 或后端事件的账号属于“服务端代码执行”信任边界,只应授予高权限管理员。 同租户嵌套调用未提供身份时,运行时从本次重新验证的 DiyToken 恢复 V8.CurrentUser 的独立副本,无须给 V8.ApiEngine.Run 的业务参数补传用户。 子脚本修改身份副本不会改变共享登录态。跨租户公开调用继续清空原租户身份; 匿名调用不会获得登录身份,可信后台任务仍使用服务端恢复的任务身份。 已有 Controller 的受控身份投影保持兼容,客户端 _CurrentUser 仍被入口覆盖或移除。 此行为须部署包含修复的后端镜像,单独保存接口脚本不能更新运行时。 升级验收应使用普通账号的真实 HTTP 外层接口调用内部引擎,并覆盖匿名、 跨租户、后台任务和会话吊销;离线参数回归不能替代实际 HTTP 验收。 外部客户端推荐直接向动态路由发送 JSON: http ``` POST /apiengine/{ApiEngineKey}?OsClient={OsClient} Content-Type: application/json {"Action":"Bootstrap","Keyword":"客户"} ``` 只有不能立即升级的旧客户端才使用兼容入口,并把接口 Key 放在 JSON Body 中: http ``` POST /api/ApiEngine/Run Content-Type: application/json {"ApiEngineKey":"your_key","Action":"Bootstrap"} ``` 两种入口都会把 JSON Body 恢复到 V8.Param;同名 Query/Form 参数保持既有优先级。接口层只负责 HTTP 路由、参数绑定和可信上下文恢复,不承载 AI、模型路由等业务逻辑。客户端提交的 _CurrentUser、_InvokeType:'Server' 或 _TrustedServerInvocation 不能建立服务端信任,身份和调用类型始终由认证中间件及接口层决定。 嵌套 JSON 的 JObject/JArray 在 Jint 中可能仍是宿主对象,直接调用 Array.isArray(V8.Param.rows) 会与普通 JavaScript 数组不同。需要严格结构校验的业务入口,可先规范化有界业务参数,再执行字段白名单、真实数组、布尔、条数、权限与状态检查: 严格业务白名单需要先区分宿主传输元数据。真实 HTTP 入口会重建 ApiEngineKey / ApiAddress、_InvokeType、_DeviceId、_RouteValues 和 _HttpMethod / _RequestPath / _RequestScheme / _RequestHost / _RequestPathBase / _ContentType / _RawBody;固定业务入口可逐项排除其确实支持的传输字段,并校验固定路由归属。不要整体允许所有下划线字段,不要重新解析 _RawBody 改写业务动作,也不要据这些值授权。已有历史调试器追加的 TestParam1 仅兼容 null/undefined、最多 200 字符的字符串、有限数字或布尔值,并立即丢弃;对象、数组、超长值和其它未知字段仍拒绝。实际 HTTP 验收须同时覆盖普通岗位、管理员、未知字段与越权输入,不能把离线干净参数等同于宿主实际请求。 javascript ``` var input = JSON.parse(JSON.stringify(V8.Param || {})); if (!Array.isArray(input.rows) || input.rows.length > 200) { return { Code: 0, Msg: '请提供不超过200条的真实JSON列表' }; } ``` 这个转换只处理业务 JSON,不能恢复可信身份或事务;继续使用当前 V8.CurrentUser、V8.DbTrans 和原生 V8.WF,拒绝客户端伪造的身份、处理人和流程字段。不要用“含 length 属性”放宽数组规则,也不要把大文件正文或整个可信 V8 对象往返序列化。修改后需要重新保存归属接口或更新应用包,并用真实 HTTP 参数、重放及非法类型验收;仅离线 JavaScript 数组测试不能证明宿主互操作通过。 新增或可修改的前端、微服务、UniApp、MCP 与外部集成必须使用动态路径或引擎配置的唯一 ApiAddress,不得新增 /api/ApiEngine/Run 依赖。这样系统日志/监控、网关限流、访问审计和流量排行才能直接显示真实接口引擎;旧地址只保留在显式 RunLegacy 兼容方法中。 ### 一个接口配置多个兼容路由 sys_apiengine.ApiAddress 是唯一主路由;“多路由”字段 ApiRoutes 可让同一接口继续接收多个历史 Controller/移动端地址,多个路径用英文分号分隔: text ``` ApiAddress: /apiengine/platform-sys-menu ApiRoutes: /api/SysMenu/GetSysMenuModel;/api/SysMenu/GetSysMenuStep ``` 接口缓存会同时按记录 Id、ApiEngineKey、主路由和全部多路由命中同一份代码。路由按完整路径、不区分大小写精确匹配;Query 不属于路由。主路由与多路由不得重复或与其它启用接口冲突,保存和启动缓存遇到冲突会失败关闭。新代码仍应使用稳定 Key 地址,多路由只用于旧客户端和已登记第三方回调兼容。 同地址存在 Controller 时,当前租户的接口引擎优先;只有权威查询确认引擎缺失,才执行原 Controller,两者均不存在返回 404。引擎停用、禁止 HTTP、鉴权失败或执行异常不触发降级,路由存储故障返回 503。包括 /api/License/GetManagementState 在内的地址不再按 Controller 前缀跳过动态解析。此优先级需要更新后端二进制,不能仅修改应用包就视为宿主路由已更新。 若旧租户仅更新后端程序,应用自动升级尚未完成,后端保留 LegacyMobileCompatibilityController 作为临时启动兼容入口。/api/SysUser/Login、续签、Token 登录、退出、公开系统设置、语言包、登录壁纸、域名租户解析、当前用户及菜单读取,在主库确认对应地址与固定接口 Key 均缺失时复用现有可信后端能力,仍执行密码、验证码、DiyToken 和菜单权限校验。管理员登录后可进入应用商城安装或更新平台应用;资源齐备后立即优先使用接口引擎。已禁用、禁止 HTTP、权限拒绝、数据库异常及接口执行失败不会触发兜底。 历史 /api/SysUser/* 地址固定绑定对应会话动作,不能用请求体 Action 改成其它操作,并支持 --OsClient--{OsClient}-- 租户后缀。路径与 Query 同时指定租户时必须一致。兼容 Controller 随后端程序交付,商城 SaaS 应用交付接口源码与多路由;两者需要分别更新。此 Controller 仅用于兼容,未来可能整体删除,新业务继续使用接口引擎。 旧地址 /api/Os/GetDateTimeNow 和 /api/SysLog/AddSysLog 也纳入同一兼容入口,并分别由 SaaS 引擎应用中的 platform-os-legacy-compatibility、platform-client-log 唯一交付。路径及 OS 动作大小写不敏感,支持 getDateTimeNow 和租户路径后缀。时间接口允许匿名 GET/POST,保持 { Code:1, Data:'yyyy/MM/dd HH:mm:ss' };日志接口支持已登录用户 GET/POST,租户和用户由 DiyToken 确定,不能通过 UserId/UserName/Category/Action 伪造平台审计。日志返回成功表示进入现有异步持久化流程,查到日志记录后才代表持久化验收通过。 旧部门树地址 /api/SysDept/GetSysDeptStep 由 SaaS 引擎应用的 platform-sys-dept 通过 ApiRoutes 交付,支持已登录用户 GET/POST、大小写和租户路径后缀。旧 JSON 请求 { "FormEngineKey":"Sys_Dept" } 不必补 Action,返回原有 { Code, Data } 及递归 _Child 结构,排序和组织范围仍使用既有部门逻辑。该历史地址固定为读树,即使传入 Action:"DelSysDept" 也不会删除;用户与租户来自 DiyToken,不信任请求的 _CurrentUser。新客户端使用 /apiengine/platform-sys-dept 并传 { "Action":"GetSysDeptStep" }。 若部门引擎尚未安装,更新后的兼容 Controller 仅为此历史读树地址复用既有 Core 原子;不会为新增、修改、删除部门提供缺引擎兜底。引擎已配置但停用、禁止 HTTP、拒绝权限或执行失败时照常失败。要同时覆盖正常引擎与未完成应用升级的租户,需分别更新后端镜像和“SaaS引擎”应用,不能只保存一份 V8 源码就宣称 Controller 已更新。 javascript ``` // 同步调用 var result = V8.ApiEngine.Run('ApiEngineKey', { Param1: '1' }); // 共享当前事务 var result2 = V8.ApiEngine.Run('ApiEngineKey', { Param2: '1' }, V8.DbTrans); ``` 接口引擎返回值与事务语义: 新代码推荐 return { Code:1, Data:... }。历史 V8.Result = { Code:1, Data:... } 继续兼容,包括脚本末尾或裸 return;;两种写法用于同步和异步引擎都保留结果。 若两种写法同时使用,以明确返回的非 undefined 值优先;失败的 Code=0 不能被 静默改成成功。重定向等响应配置与结果写法独立,迁移不需要把所有旧脚本批量重写。 - 返回 DosResult 或带 Code 的对象:Code === 1 提交,其它值回滚。 - 返回对象但没有 Code:回滚,避免“忘记返回状态”时误提交。 - 返回字符串、数字、数组、布尔值或 null,且脚本未抛异常:默认提交。 - 嵌套调用传入外层事务时,最终提交或回滚由外层调用者决定。 - V8.DbTrans.Commit()、Rollback()、Close() 会被安全代理忽略,不要在脚本中手动管理平台事务。 ### 接口引擎流式响应(SSE / NDJSON) 将接口引擎“响应类型”设为 Stream 后,脚本可通过 V8.Stream 逐段输出。浏览器默认收到 text/event-stream;请求头 Accept: application/x-ndjson(或 Query streamFormat=ndjson)时返回 NDJSON。身份、匿名开关、角色、压力保护、Jint 预算、分布式锁和事务规则与普通接口完全一致。 javascript ``` for (var i = 0; i < 5; i++) { var write = await V8.Stream.WriteAsync( { Index: i, Text: '第 ' + (i + 1) + ' 段' }, 'chunk', 'row-' + i ); if (write.Code !== 1) return write; // 客户端断开或超过配额时停止业务 } return { Code: 1, Data: { Count: 5 } }; ``` V8.Stream.Write(data, eventName?, id?) 是同步写法,WriteAsync 会等待网络背压,更适合循环输出。 事件名只能使用字母开头的 1–64 位字母、数字、点、下划线或连字符;open/done/error/heartbeat 由宿主保留。所有脚本分片都带 Provisional:true,只是暂态进度,不代表数据库已经提交;只有脚本 返回且事务真正提交后,宿主才发送 done 与 Committed:true。失败或回滚发送 error,客户端必须 丢弃依赖未提交事务的暂态结果。 连接断开会触发请求取消;脚本应检查每次写入结果并尽快退出。默认单分片 256 KB、单请求累计 16 MB、心跳 15 秒,可在当前租户 sys_osclients 的 ApiEngineStreamMaxChunkKB / ApiEngineStreamMaxTotalMB / ApiEngineStreamHeartbeatSeconds 调整;宿主仍分别 限制为 4–1024 KB、1–256 MB、5–60 秒。流式接口不能用来绕过文件响应、HDFS、大文件上传、后台任务 或 MQ;需要可靠断点续跑的长任务仍使用后台任务并持久化 Checkpoint。 ### 接口引擎受控 HTTP 响应 将“响应类型”设为 HTTP 后,接口引擎可返回标准协议需要的状态码、Content-Type、正文和安全响应头,不必创建 Controller: 通过 MCP 创建或保存时,分别在 microi_create_engine、microi_save_engine_code 中传 responseType: "HTTP"。工具枚举和目标后端必须同时支持该模式;保存后以真实 HTTP 请求检查状态码、响应头和正文,内部运行成功不能替代协议验收。 javascript ``` return { Code: 1, DataAppend: { HttpResponse: { StatusCode: 302, ContentType: 'text/plain; charset=utf-8', Body: '', Headers: { Location: 'https://identity.example.com/login', 'Cache-Control': 'no-store' } } } }; ``` 普通接口引擎只能设置经过白名单和换行检查的响应头;Location 只允许站内地址、HTTPS 或本机开发地址。Host、Content-Length、Transfer-Encoding 等宿主/逐跳头始终禁止,Set-Cookie 只接受平台可信原子签名结果。状态码范围为 100–599,204/304 不得带正文。 ApiAddress 与 ApiRoutes 都支持完整路径段模板,例如 /sso/{OsClient}/.well-known/openid-configuration 和 /saml/{OsClient}/sp/{ConnectionKey}/metadata。模板值写入 V8.Param._RouteValues,并覆盖同名外部参数;包含 {OsClient} 时租户由路径解析,歧义匹配失败关闭。 普通第三方 HTTP 回调即使包含验签、AES 或服务端隐藏密钥,也应把公开地址放在 Managed 接口引擎的 ApiAddress / ApiRoutes,由接口引擎返回 DataAppend.HttpResponse 并调度 CreateIfMissing 租户 Hook。C# 只扩展接口引擎缺失的最小安全原子,例如畅捷通 V2: javascript ``` var decoded = V8.Method.DecodeChanjetCallbackV2({ EncryptedMessage: V8.Param.encryptMsg }); if (!decoded || Number(decoded.Code) !== 1) { return { Code: 0, DataAppend: { HttpResponse: { StatusCode: 400, ContentType: 'application/json; charset=utf-8', Body: '{"result":"fail"}', Headers: { 'Cache-Control': 'no-store' } } } }; } ``` 该原子只允许 platform-chanjet-callback-v2 调用,租户从当前 V8 执行上下文取得,AES Key 与 AppKey 白名单不会进入脚本;接口引擎不能传入或覆盖租户、密钥。其它回调应沿用同一模式,不要把公开路由和业务流程重新写入 Controller。 ### 接口引擎双向实时通道(SignalR) 适用版本:后端 8.6.1 及以上;通过 MCP 配置时使用开发工具 6.0.4 及以上。 需要浏览器持续发送命令并接收回执时,把接口引擎的 ResponseType 设置为 SignalR, 连接 /api-engine-socket?OsClient={租户}&ApiEngineKey={接口Key}。这是通用传输入口, 不为每个项目新增 Hub,也不把配对、房间、设备权限或业务状态写进 C# Hub。 它要求后端包含 ApiEngineSocketHub、MCP Schema 支持 SignalR,并更新接口引擎应用包的 响应类型选项;仅修改数据库中的枚举文字不能让旧后端获得该能力。 场景 | 配置与入口 | 普通请求/响应 | JSON、String、File、HTML 或 HTTP | 服务端持续输出文本 | Stream,SSE/NDJSON,不是双向连接 | 已提交事件的订阅刷新 | /api-engine-realtime,见下文订阅协议 | 同一连接双向发送命令和回执 | SignalR,/api-engine-socket | 连接在建立时绑定 OsClient + ApiEngineKey,消息不能切换租户或接口。 需要登录的接口仍使用有效 DiyToken;不接受 AccessKey。 匿名配对等场景必须由接口显式开启 AllowAnonymous=1,并自行实现限次配对、权限与租约。 IsEnable、StopHttp、匿名开关和 ApiRole 每次调用仍走接口引擎原有校验; StopHttp=1 同样禁止外部实时调用,不能用 WebSocket 绕过。 客户端调用 connection.invoke('Invoke', requestId, message),服务端注入: javascript ``` V8.Param.Socket.ConnectionId // 宿主生成的连接 ID V8.Param.Socket.Event // Connected / Message / Disconnected V8.Param.Socket.ClientKey // 宿主生成的客户端地址摘要,用于共享限流;不是业务身份 V8.Param.RequestId // 本次消息编号 V8.Param.Message // 客户端业务对象,始终与可信上下文分开 ``` 接口代码应先验证可信通道,避免普通 HTTP 伪造 Socket 参数,亦保证旧节点失败关闭: javascript ``` var trusted = V8.Method.RequireApiEngineSocketContext(); if (trusted.Code !== 1) return trusted; if (V8.Param.Socket.Event !== 'Message') return { Code: 1 }; // 此例只向当前连接回复;实际业务在这里校验当前用户、动作、资源归属和 RequestId。 return { Code: 1, Data: { Accepted: true }, DataAppend: { SocketMessages: [{ ConnectionId: V8.Param.Socket.ConnectionId, Data: { Type: 'Receipt', RequestId: V8.Param.RequestId, At: DateNow('yyyy-MM-dd HH:mm:ss') } }] } }; ``` 成功提交后,宿主把 SocketMessages 投递为 EngineMessage 事件。 接收连接必须属于同一租户与接口的连接组;失败或回滚结果不投递。 调用方只收到 Code/Msg/Data,不会收到内部连接路由或执行堆栈。 接口成功提交、服务器接受命令与展示屏/设备实际完成是三个不同状态; 远程控制应由目标端在动作完成后再发送业务 ACK,超时显示“结果未知”,不能直接显示成功。 传输采用 JSON SignalR 协议,接收帧上限 16KB、业务 Message 上限 12KB、深度 16, 单次最多 16 个目标、投递内容合计 32KB,调用方响应上限 32KB。 每个租户、接口和客户端地址摘要共享连接/消息限流:每分钟 30 次建立、每 10 秒 240 条消息、 最多 64 个活动连接;75 秒未发送业务消息会断开。因此客户端应定期发送由业务接口识别的 心跳消息(例如每 15 秒一次),不能仅依赖 SignalR 协议 Ping 延长业务授权。 Redis 不可用时失败关闭,多节点复用现有 Redis backplane。 配对码只是低熵定位符,不能替代 DiyToken 或赋予管理员身份。展示屏必须主动允许控制, 以浏览器随机高熵秘密证明控制码所有权,服务端只保存摘要;处理控制码冲突、配对猜测限流、 停止授权、离线租约、断线恢复、重复命令和过期回执。权限状态放共享 Redis/数据库, Disconnected 只是尽力通知,节点退出时仍应由租约到期收回授权。 Redis 写入不随关系库事务回滚,不能宣称消息与 Redis/数据库已经实现“恰好一次”; 有持久副作用的业务须增加幂等键、权威版本与 outbox/恢复机制。 部署时代理必须转发 WebSocket Upgrade,所有目标节点必须包含该通道及相同接口配置。 仅使用 WebSocket 且跳过协商时不需要通过不同 HTTP 请求保持协商节点;启用其它传输时需按 SignalR 扩展部署说明 (https://learn.microsoft.com/aspnet/core/signalr/scale) 配置节点路由。 双节点、Redis 故障、滚动升级和真实浏览器两端验收分别记录,编译或单测通过不能替代线上联调。 多端遥控使用双向命令通道:业务脚本维护展示屏与多个独立控制会话的集合, 在共享分布式锁下实施有界 FIFO 和每端 RequestId 去重。撤销展示屏授权时终止全部会话, 单台手机离开只撤销自身;同一画面的命令按顺序执行,执行回执仅返回发起端。 命令入队成功、SignalR 投递成功与展示屏实际执行成功是三个不同状态,不得混为一谈。 ### 接口引擎通用实时事件(SignalR) 接口引擎负责业务命令、权限、事务和权威状态,SignalR 负责把事务成功后的服务端事件低延迟推送给已授权订阅者。该能力不是游戏专用:订单进度、协同编辑、设备状态、审批提醒和多人房间都使用同一个通用 Hub。共享数据库、Redis 或业务状态机仍是事实源,不能把业务完成与否只保存在 Hub、进程内字典或 SignalR 消息中。 写接口成功时返回固定大小写的 DataAppend.RealtimeEvent: javascript ``` return { Code: 1, Data: snapshot, DataAppend: { RealtimeEvent: { EventId: requestId, // 全局稳定;同一次重试保持不变 ChannelKey: 'order_updates', // 业务频道,小写字母/数字/下划线 SubjectId: order.Id, // 频道内资源 Id Version: order.VersionNo, // 非负、单调递增 EventType: 'StatusChanged', Data: { Status: order.Status } // 可选;只放该群组可见的安全投影,最大 32KB } } }; ``` 平台只在外部接口引擎请求执行完成且 Code === 1 后读取该对象;失败或回滚结果不会广播。宿主只读取固定大小写的 DataAppend.RealtimeEvent,重新生成 OccurredAt,并把事件收敛为 EventId/ChannelKey/SubjectId/Version/EventType/Data/OccurredAt。业务返回的其它 DataAppend、Data、私有手牌、用户信息和额外字段都不会进入通用 SignalR 事件。 跨节点发布使用共享 Redis:同一个 OsClient + EventId 先取得短时 Claim,再按 ChannelKey + SubjectId 原子维护单调 latest。低于当前 Version 的事件作为过期事件拒绝广播;同版本但事件指纹不同会判为版本冲突并拒绝;完全相同的事件重放不会推进 latest。只有 SignalR 真实广播成功后,平台才写入 24 小时 EventId 完成标记。若节点在广播前退出,Claim 到期后可重试,不会形成“已经去重但从未广播”的永久窗口;故障恢复可能产生重复通知,所以客户端仍必须去重。Redis 或 SignalR 故障不能反写已经提交的业务结果,客户端通过 HTTP Snapshot 收敛。 通用 Hub 固定契约: 宿主将事件和订阅结果中的 Latest.Data 转为仅含 JSON 标量、字典和数组的独立传输副本,保留上述字段及大小写,不把 JToken/JObject 直接交给 SignalR。Redis backplane 会预序列化已注册的 JSON/MessagePack 协议;即使浏览器只用 JSON,MessagePack 不支持 JToken 也会使整次广播失败。遇到“已连接但无推送”时,必须同时检查订阅授权、真实 RealtimeEvent 帧及 ApiEngineRealtime 日志,不能只凭握手成功判断实时功能通过。此传输修复须随兼容后端部署,保存业务 V8 不能代替后端升级。 项目 | 值 | 协议版本 | 2 | URL | /api-engine-realtime | 订阅方法 | SubscribeChannel | 取消订阅 | UnsubscribeChannel | 客户端事件 | RealtimeEvent | 订阅参数 | { ChannelKey, SubjectId } | 订阅结果 | ProtocolVersion/ChannelKey/SubjectId/Version/Latest/RenewAfterMilliseconds/LeaseExpiresAt | 租约 | 30 秒时隙;客户端按返回值续租 | 事件字段 | EventId/ChannelKey/SubjectId/Version/EventType/Data/OccurredAt | 客户端不能指定任意接口 Key、用户或租户。连接必须使用当前有效的普通登录 Token;Hub 会检查 JWT 有效期、平台活跃 Token 缓存和租户配置的 Token 生命周期。现有 AccessKey 权限模型没有 realtime:subscribe scope,因此平台会直接拒绝 AccessKey 实时连接;在平台正式增加并校验该 scope 前,不得通过放宽 Hub 校验绕过此边界。 Hub 从登录 Token 恢复 OsClient 与 CurrentUser,再按约定调用 realtime_{channel_key}_authorize 接口引擎。例如 order_updates 对应 realtime_order_updates_authorize: javascript ``` // ApiEngineKey: realtime_order_updates_authorize var order = V8.FormEngine.GetFormData('biz_order', { Id: V8.Param.SubjectId, _SelectFields: ['Id', 'OwnerUserId', 'VersionNo'] }); if (!order || order.Code !== 1 || order.Data.OwnerUserId !== V8.CurrentUser.Id) { return { Code: 0, Msg: '您无权订阅该订单' }; } return { Code: 1, Data: { Authorized: true, ChannelKey: V8.Param.ChannelKey, SubjectId: order.Data.Id, Version: order.Data.VersionNo } }; ``` SubscribeChannel 不是连接全生命周期的一次性授权,而是 30 秒时隙租约。每次调用都会重新校验 Token,通过共享 Redis 的 OsClient + UserId 限流,并重新执行授权接口引擎;当前限额是所有标签页和 API 节点合计 10 秒最多 96 次。服务端把通过授权的连接加入当前和下一时隙,只向当前时隙广播,并返回建议续租时间。客户端必须按本次响应的 RenewAfterMilliseconds 串行再次调用 SubscribeChannel,不要写死间隔;停止续租后,连接最迟在后续时隙自然停止收到事件。授权失败会移除该频道租约,Token 失效会清理全部租约并断开连接。Redis 限流不可用时订阅失败关闭,业务仍可使用 HTTP Snapshot。 客户端按 EventId 去重、按 Version 忽略旧事件并检测缺口;发现版本跳跃、重连、续租失败或服务降级时,立即调用业务接口获取按当前用户裁剪的 Snapshot,并保留有界 HTTP 轮询兜底。一个频道群组中的所有订阅者都会收到同一份 Data,因此用户私有手牌、Token、密钥和按用户不同的字段不得放入群组事件。 浏览器使用项目本地打包的 @microsoft/signalr,下面是续租与 Snapshot 降级的最小骨架: javascript ``` const subscription = { ChannelKey: 'order_updates', SubjectId: orderId }; const connection = new signalR.HubConnectionBuilder() .withUrl(apiBase + '/api-engine-realtime', { accessTokenFactory: () => loginToken // 普通登录 Token,不是 AccessKey }) .withAutomaticReconnect() .build(); const seenEventIds = new Set(); let snapshotVersion = 0; let renewTimer; connection.on('RealtimeEvent', async event => { if (seenEventIds.has(event.EventId) || event.Version <= snapshotVersion) return; seenEventIds.add(event.EventId); // 即使 Data 有公共增量,也要在缺口、重连和关键状态变化时回读权威 Snapshot。 const snapshot = await getOrderSnapshot(orderId); snapshotVersion = snapshot.Version; render(snapshot); }); async function renewLease() { clearTimeout(renewTimer); try { const lease = await connection.invoke('SubscribeChannel', subscription); renewTimer = setTimeout( () => void renewLease(), Math.max(1000, lease.RenewAfterMilliseconds) ); } catch (error) { await refreshByHttpSnapshot(); renewTimer = setTimeout(() => void renewLease(), 3000); } } await connection.start(); await renewLease(); ``` 下面的 /game-realtime 是已有五款游戏的向后兼容协议;新业务和完成迁移后的游戏使用上面的通用协议。 ### 受信固定步长规则运行时(后端安装候选) 平台固定 Managed 协议作用域只绑定当前租户与指定接口引擎,并在可信运行入口消费。 此类调用无玩家用户,不要求伪造 DiyToken,也不注入匿名玩家标识;普通 HTTP、普通 嵌套调用、错误租户/引擎以及已消费或释放的作用域仍执行原有身份与写入限制。 参数中的 TrustedPump 等标记不能建立此作用域。后端升级才修复此行为,保存脚本不能替代。 管理员可用既有 Snapshot 查询 FixedStepPump 节点摘要,区分循环、调用失败与过期; 本机观察不是主库提交或授权证明。字段与诊断步骤见系统日志/监控 (https://microi.net/doc/system-engine/system-observability.html)。 需要持续模拟的应用可由现有 API 的通用宿主承载;业务路由、DiyToken、房间成员、主库事务、inbox、checkpoint 和结算仍由 Managed 接口引擎处理。该能力需要正式后端安装,保存 V8 代码或创建任务不会自动安装原子。规则程序集与应用适配属于私有应用发行包,不把业务 Domain 放入平台通用源码,也不增加业务 Controller。 js ``` // 只能由当前租户已审批的精确 tick 接口引擎执行;不是浏览器可直接提交的信封。 var identity = V8.Method.RunPlatformApiRuntime({ RuntimeKey: 'fixed-step-simulation', Action: 'DescribeInvocation', Param: { RegistrationId: trustedRegistrationId } }); if (identity.Code !== 1) return identity; ``` 原子动作包括 DescribeInvocation、CreateInitialState、AdvanceBatch、Project、ObserveOwnedRoom、StopOwnedRoom、DrainOwnedRoom。调用返回标准 Code/Data;实际当前租户与 ApiEngineKey 来自 V8 上下文,不能在 Param 中覆盖。NodeId 为宿主生成,TrustedPump 仅在受信后台调用作用域内成立。StopHttp 不阻止其它脚本 nested Run,因此实际引擎必须核成员或可信后台作用域,不能原样转发玩家信封。 ObserveOwnedRoom 是有限的本机调度提示。当前没有 afterCommit 回调,接口引擎只能在所有持久写入成功、即将 return Code=1 时给出提交前提示;不能宣称提示已经处于提交之后。后续 pump 必须重新取得主库行锁,验证 owner/fence/租约与状态才可推进,原事务回滚不得产生合法写。StopOwnedRoom 立即取消在途请求;终局事务应调用 DrainOwnedRoom(RegistrationId/RoomId/RoomEpoch/AuthorityEpoch),返回 Code=1, Data={Drained:true} 只表示不再安排后续批次,当前请求仍可完成提交。Drain 不延长租约,也不阻止宿主全局停机取消;它不是数据库提交回执。 临时预算或生命周期拒绝返回 Code=0、固定非敏感 Msg,并带 DataAppend={ErrorCode,Retryable:true}。ErrorCode只可能是 KernelBusy、HostMemoryPressure、RoomStillDraining、CoordinatorStopping、StaleLease、ComputeBudgetExceeded。授权、来源审批和未知异常不进入这个列表,也不暴露原异常。Managed应对精确白名单码显式 return {Code:0,Msg:code} 让宿主回滚,再让客户端有界退避并保持原RequestId/指令正文;不要把码当JS异常直接抛出,因为生产V8诊断会追加行列,破坏精确协议值。 固定规则步长为 50ms,单批最多追赶4步、处理64条指令;进程最多2个计算、同租户最多1个,超额快速拒绝。50ms 是目标唤醒间隔,实际延迟和容量须实测。Job 的秒级任务负责恢复扫描与补偿,不能冒充20Hz连续步。未知提交必须用原 RequestId 和原语义重试;同一事务行锁下读取 DB 时间、验证租约/fence/CAS后才持久化并ACK。本机排程只保存有限唤醒提示,不能作为共享权威事实。 单批规则计算预算为 100ms,规则工厂、完整检查点恢复、各步推进和捕获共用该预算。每批仍从主库核验批准来源和撤销状态;这一可信授权等待单独计时,不计作规则计算,但仍消耗原总租约。授权结束后、构造规则前再次检查取消和租约,不能缓存授权、延长租约或给每个阶段重新分配预算。离线注入授权延迟只验证计时边界;实际 HTTP 延迟、冷启动和服务器负载仍需线上验收。 升级后端后,管理员可用既有 microi_query_system_observability 的 Snapshot 读取 FixedStepRuntime.CoreAssemblyFileSha256 和 Registrations[] 的计算预算见证。 ComputeBudgetFailureCount 是当前节点累计次数;LastComputeBudgetFailureUtc、 LastComputeBudgetPhase 及 LastComputeBudgetComputeMilliseconds / LastComputeBudgetAuthorizationMilliseconds / LastComputeBudgetTotalMilliseconds 记录最后一次真实拒绝。旧后端缺少字段表示尚不支持,不能解释为零次失败。 固定阶段值 | 超时检测所在的计算边界 | InputValidation | 信封、指令排序与哈希校验 | RuleFactory | 已批准的固定规则工厂 | RestoreState | 完整恢复与绑定核验 | ApplyCommand | 应用收件箱指令 | AdvanceTick | 确定性规则步推进 | CaptureState | 捕获新检查点 | 这些时间是单调时钟的累计墙上耗时,包含调度或 GC 等影响;计算时间扣除可信授权等待。 阶段值只说明在哪个边界检测到总预算耗尽,不能当作该函数独自耗时或生产根因。 见证只保留于最多 64 个编译部署绑定,不保存房间、玩家、指令、自由异常或秘密; 管理员查询仍按当前租户隔离。节点重启会清零,跨节点需分别读取;它不是持久日志、 审批、数据库提交、健康或商业验收证明。普通错误码、取消、租约和 100ms 预算保持不变。 审批资源由应用商城声明式安装,原生代码另外经过后台发行审批。私有应用编译组合固定 tenant/registration/入口与审批摘要,主库审批须同时匹配已安装版本、V3 Completed来源hash、真实DLL及依赖闭包。运行中撤销、卸载、软删来源或主库不可用均失败关闭。首批提供者限定同租户与MySQL;其它来源/方言不静默降级。没有任意DLL路径、URL或Type的V8/MCP注册入口,原生程序集也不是不可信代码沙箱。 普通租户的编译证明默认 TenantInstallation,保留真实 Installed 回执及旧摘要。官方同租户发布源禁止安装自己,只能由后端发行组合明确固定 OfficialPublishedSnapshot:InstallRecordId 绑定 mic_data_version.Id,原始 Data 的UTF-8 SHA-256另固定于编译回执并参与独立审批摘要。该类别与快照摘要不从Param或自由表字段选择、不新增表字段、不伪造Installed。受信官方身份、当前Published/审批有效指针、Verified包hash/size/HDFS、不可变快照、V3 Completed源码hash及实际DLL依赖须全部匹配;当前状态变化、软删或主库不可用下次调用立即拒绝,旧历史存在不足以授权。跨租户仍拒绝,结构发布不等于后端安装或代码审批。审批表纳入通用强制管理员清单,普通用户即使误授表/菜单权限仍不得改授权状态;V8Limit和ReadPrimary不是写权限。 Project 只返回当前用户绑定席位的安全快照和可见事件;完整checkpoint、未知敌人状态与内部凭据不可广播。通用SignalR只发送公开版本失效通知。源码/离线回归、真实Jint/SQL、后端部署、两节点故障恢复及用户实机体验需要分别验收。 ### 多人游戏实时失效通知(兼容协议) 发牌、出牌、碰杠胡、结算、捕鱼命中等规则必须在接口引擎中执行,并用数据库事务、RequestId、ExpectedVersion、唯一索引和行锁维护权威状态。SignalR 不承载这些业务命令,也不发送手牌;它只在接口引擎成功提交后通知同房玩家“房间版本已变化”,客户端随后重新调用 gateway 的 Snapshot。 写操作成功时,gateway 返回固定大小写的 DataAppend.RealtimeInvalidation。对象只能包含以下六个公开字段: javascript ``` return { Code: 1, Data: snapshot, DataAppend: { RealtimeInvalidation: { EventId: requestId, // 全局稳定,重试时保持不变 AppKey: 'landlord-arena', RoomId: room.Id, Version: room.VersionNo, Command: 'Play', OccurredAt: DateNow('yyyy-MM-dd HH:mm:ss') } } }; ``` 平台只在 ApiEngine.RunAsync 返回、接口引擎事务已经提交后读取该对象;Code !== 1 或回滚结果不会广播。宿主会重新生成 OccurredAt,并丢弃对象中任何额外字段,因此误放入 PrivateHand/UserId/StateJson 也不会进入 SignalR。共享 Redis 按 OsClient + EventId 保存 24 小时去重,同一个 EventId 若对应不同 AppKey/RoomId/Version/Command 会被判为冲突并拒绝广播。多 API 节点通过现有 SignalR Redis backplane 发送;Redis 或 SignalR 短暂故障不能反写已经提交的业务结果。 Hub 固定契约: 项目 | 值 | URL | /game-realtime | 订阅方法 | SubscribeGameRoom | 取消订阅 | UnsubscribeGameRoom | 客户端事件 | GameRoomChanged | 事件字段 | EventId/AppKey/RoomId/Version/Command/OccurredAt | 订阅参数为 { AppKey, GatewayKey, RoomId },其中 GatewayKey 必须符合 app_*_gateway。Hub 不接受客户端传入的 UserId/_CurrentUser/OsClient:它从当前有效登录 Token 恢复用户和租户,再以服务端可信身份调用对应 gateway 的 Command='AuthorizeRealtime'。gateway 必须复用 Snapshot 相同的房间成员校验,并精确回显房间: javascript ``` if (V8.Param.Command === 'AuthorizeRealtime') { // 先按 V8.CurrentUser.Id 查询房间成员;不要信任 V8.Param.UserId var member = getCurrentRoomMember(V8.CurrentUser.Id, V8.Param.RoomId); if (!member) return { Code: 0, Msg: '您不是该房间成员' }; return { Code: 1, Data: { Authorized: true, AppKey: V8.Param.AppKey, RoomId: V8.Param.RoomId, Version: member.VersionNo } }; } ``` 浏览器使用项目本地打包的 @microsoft/signalr,不要依赖运行时 CDN: javascript ``` const connection = new signalR.HubConnectionBuilder() .withUrl(apiBase + '/game-realtime', { accessTokenFactory: () => loginToken }) .withAutomaticReconnect() .build(); const seenEventIds = new Set(); let snapshotVersion = 0; connection.on('GameRoomChanged', async event => { if (seenEventIds.has(event.EventId) || event.Version <= snapshotVersion) return; seenEventIds.add(event.EventId); const snapshot = await runGateway({ Command: 'Snapshot', RoomId: event.RoomId }); snapshotVersion = snapshot.Data.Version; render(snapshot.Data); }); await connection.start(); await connection.invoke('SubscribeGameRoom', { AppKey: 'landlord-arena', GatewayKey: 'app_ddz_gateway', RoomId: roomId }); ``` SignalR 是低延迟提示,不是事实源:通知可能丢失、重复或乱序,客户端必须按 EventId 去重、按 Version 忽略旧通知,并保留约 1.2 秒的 Snapshot 轮询兜底。滚动升级期间旧节点可能暂时没有 Hub,客户端应继续轮询并自动重连,不能因此允许本地发牌或本地判定胜负。 ### 接口嵌套与 Jint 资源预算 V8.ApiEngine.Run 嵌套调用是平台支持的正常编排方式。新版默认允许 32 层、节点硬上限默认 64 层;实际业务可以有 5、10 甚至更多层,但仍应避免循环调用,并让每层保持单一职责。这里的“接口嵌套深度”与 JavaScript 函数递归深度不是同一个概念。 Jint 的 LimitMemory 统计当前执行线程自约束重置后的累计托管分配字节数,不是当前仍存活的对象、进程工作集,也不会预留 2GB 物理内存。因此,一个接口触发“2GB累计分配上限”不等于服务器当时真实占用了 2GB;100 个并发接口也不能据此直接推算为 200GB 实时内存。大量临时对象、重复 JSON 序列化/反序列化、整表加载和数组复制都会快速累加,即使对象随后已被 GC 回收。 旧版中,父引擎执行子接口时,子接口初始化、查询、JSON 和业务对象分配还会被每一层父引擎重复计入,四层编排可能远早于预期触发父层 2GB。新版默认启用嵌套隔离: - 每个接口引擎拥有自己的单层累计分配预算,默认 2048MB、节点硬上限默认 8192MB; - 子接口分配不再重复计入每个父接口的单层预算; - 根调用树仍有独立累计分配总预算,默认 8192MB、节点硬上限默认 32768MB,防止通过无限嵌套绕过整体保护; - 嵌套调用不会重复占用全局和租户并发名额;同一调用树重入同一个 Key 也不会再次抢占自己的 Key 名额,循环调用最终由嵌套深度上限终止。 当前片段的有效预算可从 V8.Limits 查看: javascript ``` console.log(JSON.stringify(V8.Limits)); // TimeoutSeconds, MaxStatements, LimitMemoryMB, // CallTreeLimitMemoryMB, LimitRecursion, NestedApiDepthLimit, // CurrentDepth, IsBackgroundTask, IsolateNestedApiMemory, // ResidentMemoryGuardOnly, UnlimitedRuntime, MemoryAccounting ``` 资源异常会在 DataAppend.V8Limit 返回结构化分类,例如 V8_MEMORY_LIMIT、V8_CALL_TREE_MEMORY_LIMIT、V8_STATEMENTS_LIMIT、V8_RECURSION_LIMIT、V8_TIMEOUT、V8_NESTED_DEPTH_LIMIT 或 V8_EXECUTION_QUEUE_TIMEOUT,同时包含限制值、调用深度和调用路径。排查时应按分类处理,不要把所有异常都归为“服务器内存不足”。 后台任务仍通过接口引擎执行,所以一个未分片的 30 分钟脚本仍会受同一套单片超时、语句和累计分配预算约束。后台任务的总时长可以是数小时,但每片应控制在默认 600 秒以内,在提交本片事务后返回 HasMore + Checkpoint,由 Worker 创建新的执行片段继续;新片会获得新的超时、语句和累计分配预算。 开启分布式锁的可信后台任务使用最长 60 秒的 Redis 租约,并在执行期间按持有者令牌持续续租。接口的长执行预算与锁的单次存活时间分别计算;节点退出后,未续租的锁会在该短租约到期后释放,检查点无需等待原来一小时的接口预算。正常运行的长任务仍保留最多 12 小时(显式更长的执行预算优先)的续租边界。普通 HTTP 调用继续使用原有固定租约;前端伪造后台任务参数不能开启可信续租。 ### 接口引擎的“V8运行限制” 接口引擎使用正向开关 sys_apiengine.V8Limit,表单显示名称为 V8运行限制: - 默认关闭(0):不为该接口设置 Jint 单次执行超时、最大语句数、JavaScript 函数递归和累计分配预算,同时取消 Promise 的固定等待时限; - 打开(1):按 Timeout / MaxStatements / LimitMemory / LimitRecursion 配置执行限制,并在表单中显示这些配置项; - 老字段 sys_apiengine.V8Unlimited 仅作滚动升级兼容,新的表单、MCP 和 Manifest 均使用 V8Limit/v8Limit。 无论开关状态如何,进程/容器常驻内存保护、HTTP 断开与后台任务取消、节点停机取消、执行并发、接口嵌套深度、CLR 类型沙箱、租户权限、SQL/ORM/HTTP/文件限制都始终生效。接口调用的下游接口分别读取自己的 V8Limit,不会继承上游设置。 表后端事件同样使用正向开关 diy_table.V8Limit:字段缺失、null、0/false 都不设置 Jint 单次预算,只有 1/true 才启用限制。旧 diy_table.V8Unlimited 仅在新字段不存在时按反向语义兼容,新版表单、MCP、Manifest 与应用资源统一写 V8Limit/v8Limit。复杂逻辑位于 SubmitBeforeServerV8、SubmitAfterServerV8 或 ServerDataV8 时,仍建议满足以下条件: - 由后台任务承载,避免依赖长时间浏览器连接; - 已评估数据库长事务的锁等待、事务日志/Undo、回滚耗时和连接超时; - 任务具备稳定幂等键,节点故障导致数据库自动回滚后可安全重试; - 控制并发和查询字段,不把“48GB 内存”理解为单任务可以无界分配;常驻内存保护是全进程边界,接近阈值仍会拒绝新执行或有界停机。 ## V8.Notification V8.Notification.Send 向当前 OsClient 的指定用户发送“平台内部”SignalR 提示。它是低延迟提示原语,不负责创建权威日志;业务通知优先调用 msg_event,由接口引擎先在 mic_msg_event_log 原子 claim,再调用本方法。 js ``` var result = V8.Notification.Send({ NotificationId: 'event-123-user-1', EventId: 'event-123', ReceiverUserIds: ['user-1'], Title: '待办提醒', Content: '您有一条新的待办', LinkUrl: '/#/todo/123', Payload: { TodoId: '123' } }); ``` - ReceiverUserId 或 ReceiverUserIds 必传其一,去重后最多 200 个。 - Title 最长 200 字符;Content、序列化后的 Payload 各最多 32 KiB。 - LinkUrl 最长 500 字符,只允许站内路径、锚点或 HTTP/HTTPS。 - 客户端事件固定为 ReceivePlatformNotification。 - 存在当前事务时,推送只在事务提交后进行最多 1.8 秒的有界等待;回滚不推送。 - 实时链路不可用时返回可降级结果,客户端应调用 msg_internal_list 回读持久通知。 完整的策略表、公众号/服务号与小程序区别、幂等和多节点验收见消息通知 (https://microi.net/doc/system-engine/message-notification.html)。 ## 表单引擎 V8.FormEngine 见平台文档:FormEngine 用法 (https://microi.net/doc/v8-engine/form-engine.html)。 后端接口引擎和后端表单 V8 事件在活跃 V8 上下文中调用 FormEngine 时,由服务端写入不可被外部 JSON 构造的可信标记,因此不要求 _SysMenuId。租户边界、平台保护表和脚本自身的业务校验仍然生效。浏览器或其它外部 HTTP 请求不能通过伪造 _InvokeType: 'Server' 获得该信任;_InvokeType 只控制是否触发表单事件,不是身份或授权标记。 这里要区分“进入事件前”和“事件内部”:浏览器调用 AddFormData 仍要先通过目标菜单的 Add 权限,菜单 SqlWhere / SqlJoin 只约束已有记录的查询、修改和删除,不用于拒绝一条尚不存在的新增记录;进入 SubmitBeforeServerV8 / SubmitAfterServerV8 后,事件代码与接口引擎具有相同的服务器 FormEngine/数据库执行能力,可在当前租户内完成跨表事务、复杂 SQL 及归属字段写入。 原生新增管线独立确定实际主键,SubmitBeforeServerV8 的 V8.Form.Id 可能为空;在事件里自行补 GUID 不等于改变实际插入主键。条码、明细或审计等需要主表 Id 的写入,应在 SubmitAfterServerV8 核验 V8.Form.Id 并通过同一 V8.DbTrans 回读主记录后执行。After 仍在事务提交前,附属记录失败应返回 Code=0,主表与附属记录一起回滚。验收同时比较新增响应 Id、主表 Id、附属外键和审计 RowId,避免成功响应掩盖孤立引用。 提交前、提交后事件可能复用同一个 Jint 全局环境。生成公共库时,不要在两段事件里重复声明同名顶层 let/const;使用各事件独立函数作用域,或允许重复初始化的 var。单独编译两段脚本无法覆盖此问题,必须执行真实表单的完整 Before → After 链路并确认事务成功。 前端/外部 HTTP 的菜单授权、历史无 _SysMenuId 推断、TableChild 委托和行级权限规则详见 FormEngine 安全授权 (https://microi.net/doc/v8-engine/form-engine.html#%E5%AE%89%E5%85%A8%E6%8E%88%E6%9D%83%E6%A8%A1%E5%9E%8B)。 平台内部的多层封装也必须保留来源:如果一个已校验管理员的设计器或升级任务在内部再次调用 FormEngine,应传递原管理员上下文,或由服务器构造带 _TrustedServerInvocation 的强类型参数;不要把数据转成裸 JObject 后依赖类型推断。可信标记是服务端实现细节,V8 代码和 HTTP 客户端都不需要、也不能自行设置。 这一规则也适用于 AddDiyField/AddField:它会先读取表定义,再在事务中调用通用 FormEngine 写入 diy_field,最后创建物理列。内部 diy_field 写入必须继承外层已验证的管理员或可信升级上下文;普通客户端不能借动态建字段入口绕过保护表授权。 高精度金额、数量或换算率传入 decimal(p,s) / 有小数位的 number(p,s) 字段时,优先保留十进制字符串,例如 "999999999999.123456"。不要先经过 JavaScript Number()、parseFloat() 或浮点乘除再转回字符串;后端参数转换不能恢复已在客户端或 V8 中丢失的位数。字符串使用英文小数点与无分组的格式,解析不随请求线程语言变化;逗号分组、本地化逗号小数、指数、非数字和越界值会拒绝并保留字段错误上下文。既有 null、空白及历史字符串 "null" 继续绑定空值,类型化 CLR 数值继续兼容。业务入口仍需自行检查合法符号、精度、小数位、范围和权限;界面本地化显示与提交格式应分别处理。数据库 Provider 的真实往返、HTTP 事务和目标租户升级均需单独验收,参数绑定单元测试不代表跨库业务成功。 ## 缓存操作 V8.Cache V8.Cache 是当前租户命名空间内的 Redis 能力。传逻辑 Key 时服务端自动生成 Microi:${V8.OsClient}:{逻辑Key};传完整的当前租户 Key 继续兼容,传入其它租户的 Microi: 前缀会被拒绝。它不暴露 Redis IDatabase、连接管理、服务器扫描或任意连接能力。 过期时间可传秒数,也可传 d.HH:mm:ss 字符串,例如 59 或 0.00:00:59;省略时为永久。常用方法包括 Set/Get/Delete/Del/Remove、KeyExist/Exists、SetIfNotExists、Expire,以及 HashSet/HashGet/HashGetAll/HashGetAllKeys/HashGetAllValues/HashDelete/HashRemove/HashExists/HashLength/HashIncrement。 javascript ``` // 推荐只传逻辑 Key,租户前缀由服务端添加 var cacheKey = 'FormData:baoming'; var cacheValue = JSON.stringify(formData); var result1 = V8.Cache.Set(cacheKey, cacheValue, 59); var result2 = V8.Cache.Get(cacheKey); var result3 = V8.Cache.Remove(cacheKey); V8.Cache.HashSet('Customer:Stats', 'Count', '1'); var count = V8.Cache.HashGet('Customer:Stats', 'Count'); V8.Cache.Expire('Customer:Stats', 3600); // 为整个 Hash Key 设置 TTL // 只在 Key 不存在时写入,并强制使用正数秒 TTL。 var first = V8.Cache.SetIfNotExists('Idempotency:Order:123', 'processing', 60); ``` SetIfNotExists 适合短期去重窗口,但仍没有唯一持有者令牌、续租和仅持有者释放语义。不要用它或“先 KeyExist、再 Set、最后 Remove”实现分布式锁。接口引擎应使用平台的分布式锁配置,Job/Worker 使用带租约和持有者令牌的锁;锁之外还必须使用稳定幂等键、唯一约束或状态机保证副作用只执行一次。 Expire 直接调整 Redis TTL,不会缩短当前源码中已经存在的 String L1 副本。可能进入 L1 的 String/对象应优先用带 TTL 的 Set 重写,或先删除再写入;Hash 不进入 L1,可以直接为整个 Hash Key 设置 TTL。L1/L2 数据流、Pub/Sub 失效、管理接口和源码配置详见分布式缓存(L1/L2) (https://microi.net/doc/system-engine/cache.html)。 菜单、角色和表权限保存会递增 Redis 授权版本并使各节点的短期快照失效。不要把“重启容器”或“清空整个 Redis”当作权限刷新方案。 ## .NET 互操作与异步边界 后端 V8 对部分 .NET 类型开放互操作,但平台能力应优先使用 V8.*:例如用 V8.Method.NewUlid() 生成标识、用 V8.Base64 编解码、用 DateNow() 处理时间。不要依赖全局 System 名称访问任意 CLR 类型;平台还提供了 V8.System 主机监控扩展,两者可能发生名称冲突,且部分危险 CLR 类型会被禁用。 setTimeout 和 System.Threading.Tasks.Task.Run 不能作为“请求返回后可靠执行”的方案。V8Engine.Run 返回后会释放当前 Jint Engine、租户上下文、事务和并发租约,延迟回调可能面对已失效的上下文。请求内异步 API 使用 await;需要脱离请求执行时,使用接口引擎后台任务、Job、MQ 或 outbox,并设计幂等、重试和多节点故障恢复。 后台接口引擎通过 V8.Method.UpdateBackgroundTask 上报真实单位进度: js ``` var taskId = V8.Param._BackgroundTaskId; V8.Method.UpdateBackgroundTask({ _BackgroundTaskId: taskId, Current: committedCount, Total: totalCount, Msg: '已提交第 ' + committedCount + ' 条', Log: '批次 ' + batchNo + ' 已提交' }); ``` Current 必须表示已经提交、重试不会重复的工作量;有自然单位时不要同时自行计算 Progress。Log/AppendLog 会追加到任务详情,不得写入密码、Token 或密钥。总量未知时省略 Total,平台显示不定进度;ETA 由服务端根据真实吞吐采样计算。失败和取消保留最后进度,只有最终 Code=1 才显示 100%。预计超过 10 分钟的接口应分页处理,并通过 Data.BackgroundTask={HasMore:true,Checkpoint,Current,Total,NextDelaySeconds} 让平台持久化检查点后重新入队。 javascript ``` var now = DateNow('yyyy-MM-dd HH:mm:ss'); var id = V8.Method.NewUlid(); // 请求内异步方法(仅在方法本身提供 Async 版本时) var result = await V8.ApiEngine.RunAsync('ApiEngineKey', { Id: id }); ``` ## 常用函数 V8.Method ### 全局日期与函数库 DateNow(format)、DateFormat(date, format)、DateAdd(date, unit, amount, format) 由后端引擎初始化提供,不依赖系统设置表是否已安装。DateAdd 支持 s/m/h/d/w/M/q/y;大写 M 是月,小写 m 是分钟;月末或闰年加减会截到目标月的有效日期。 “系统设置”应用的 mci_global_function 子表可按前端/后端维护具名函数。函数库与原有全局脚本合并后按租户缓存,不会把合并结果写回覆盖用户脚本;保存记录后在真实提交时失效。原有同名自定义函数优先,详见系统全局函数 (https://microi.net/doc/more/sys-config.html#%E7%B3%BB%E7%BB%9F%E5%85%A8%E5%B1%80%E5%87%BD%E6%95%B0)。V8.Method.ValidateGlobalFunction(name, code) 只做语法与单函数声明校验,返回标准 DosResult,不执行代码。 V8.Method 同时包含业务工具、管理员运维能力和平台内部能力。普通业务脚本优先使用下列稳定接口;数据库备份、清空数据库、认证缓存维护等管理方法不能作为普通业务 API 暴露。 展开查看 JavaScript 代码 javascript ``` // 当前 Token 与身份。不要把返回对象直接透传给前端。 var currentTokenObj = V8.Method.GetCurrentToken(token, osClient) // { OsClient:'', CurrentUser:{}, Token:'不包含 Bearer ' } 或 null // 当前用户资料或权限已提交后刷新登录投影;osClient 只是当前租户一致性断言 var refreshLogin = V8.Method.RefreshLoginUser(V8.CurrentUser.Id, V8.OsClient); // 仅兼容历史 microi-init 的 body Token:必须传原始 Token,宿主会重新权威验证 var legacyRefresh = V8.Method.RefreshLoginUser( tokenResult.CurrentUser.Id, V8.OsClient, V8.Param.Token ); var id = V8.Method.NewUlid(); var timestamp = V8.Method.GetTimestamp(); // 当前运行中的 Microi.Core 文件版本,格式为 vX.Y.Z var backendVersion = V8.Method.GetBackendVersion(); // 后端可信 V8 按租户内对象路径签发短期代理地址 var result = V8.Method.GetPrivateFileUrl({ FilePathName: '/microi/file/2023-08-06/xxx.doc' }); // 结构化系统日志;不要记录密码、Token、密钥或完整请求体 V8.Method.AddSysLog({ Type: '接口日志', Title: '同步完成', Content: '记录数:20', Level: 1 }); // microi_database 后端提交后事件专用:事务提交后刷新全部节点的 V8.Dbs var refreshResult = V8.Method.RefreshExtensionDatabases(); // sys_role 后端提交前事件专用:读取服务端唯一的表直连授权策略 var directTablePolicies = V8.Method.GetDirectTableGrantPolicies(); // 接口引擎保存需要再次读取的密码或 Token:密钥永不进入 V8, // 密文只允许同一 OsClient、同一 ApiEngineKey 解密。 var cipher = V8.Method.ProtectApiEngineSecret(secretText); var plainText = V8.Method.UnprotectApiEngineSecret(cipher); // 仅官方消息通知应用的 Managed 接口 wechat_send_tpl_msg 可调用。 // AppId/AppSecret 固定从当前租户 wx_mp 读取,不进入 V8 参数或返回值。 var sendResult = V8.Method.SendWeChatTemplateMessage({ WxMpId: V8.Param.WxMpId, OpenId: V8.Param.OpenId, TemplateId: V8.Param.TemplateId, TemplateData: V8.Param.TemplateData, Url: V8.Param.Url, MiniProgramAppId: V8.Param.MiniProgramAppId, PagePath: V8.Param.PagePath }); ``` GetBackendVersion() 读取当前后端运行程序集的 FileVersion,只返回规范化的 vX.Y.Z,不暴露程序路径、主机名或其它环境信息。官方匿名 Managed 接口 GET /apiengine/platform-service-health?OsClient={OsClient} 使用它返回 Data.Status=Healthy 与 Data.BackendVersion;客户端应以该固定接口判断整个 API 服务是否可用,不能把任一菜单、表单或其它业务接口的失败直接升级为全局离线。 应用包先于后端二进制滚动升级时,接口仍返回 Healthy,但 BackendVersion 可暂时为空; 新二进制上线后会自动补齐真实版本。旧节点的 /api/Diagnostics/health 仅用于客户端 滚动升级兼容,不是新版客户端的主要健康契约。 RefreshExtensionDatabases(osClient?) 绑定当前 V8 租户。存在 V8.DbTrans 时只注册提交后回调:真实事务提交成功才递增共享 Redis 版本,回滚不刷新;没有事务时立即刷新。它适合“数据库扩展”应用的 microi_database.SubmitAfterServerV8,不应暴露成匿名或普通业务接口。 GetDirectTableGrantPolicies() 返回平台统一维护的表直连授权模式和允许操作。它只供角色管理等可信后端表单事件校验,不能替代当前用户、菜单、表和行级权限判断,也不能直接作为匿名业务接口返回。 RefreshLoginUser(userId, osClient?, token?) 是登录身份投影刷新原子,不是按调用方参数任意重载租户缓存的工具。有效租户只来自当前 V8 上下文、已认证 DiyToken,或宿主为可信表事件/升级任务显式建立的“用户 + 租户”作用域;osClient 仅作兼容一致性断言,不一致时失败关闭。 普通用户只能刷新本人,同租户平台超级管理员经主库复核后才可刷新其他用户;访问密钥、匿名/空身份和跨租户调用均拒绝。宿主查询 sys_user、角色权限与读写登录缓存时始终使用同一个规范化租户,避免把一个租户的用户投影写入另一个租户。主写入已经提交时,刷新失败应作为 Warning 返回,不能把已完成业务伪装成失败并诱导客户端重试。 第三参 token 只用于兼容历史 microi-init 把 Token 放在请求体、未发送 Authorization Header 的客户端。它必须是原始 Bearer 凭据;宿主会在 RefreshLoginUser 内重新调用 DiyToken 权威验证,并要求 Token 恢复出的租户和用户与当前 V8 租户、osClient 一致性断言及 userId 完全一致。伪造、失效、跨租户或“管理员 Token 刷新别人”均失败,且不会回退当前 ambient 身份。V8.Method.GetCurrentToken(...) 返回的对象不能作为第三参或认证证明。 ProtectApiEngineSecret(plainText) / UnprotectApiEngineSecret(cipherText) 只允许在后端接口引擎上下文调用。宿主把密文同时绑定当前租户与当前 ApiEngineKey,调用方不能传入 OsClient、密钥或 Purpose,也不会获得派生密钥。适用于远程连接密码、短期刷新 Token 等“业务明确需要再次读取”的接口私有凭据;列表必须继续脱敏,读取动作仍要执行当前用户、行归属和权限校验,禁止把解密结果写日志、审计或返回无权前端。接口引擎改 Key 后旧密文不可解,因此升级已有 Managed 引擎时应保持 Key 稳定。 SendWeChatTemplateMessage(options) 不是普通业务脚本可复用的微信 SDK。宿主只允许消息通知官方应用的 Managed 接口 wechat_send_tpl_msg 调用,并把公众号配置固定为当前租户 wx_mp 记录;参数只能选择公众号、接收人、模板、受限模板字段、HTTP(S) 跳转地址和受限小程序路径,不能传入或读取 AppSecret。租户的模板选择、接收人计算、通知记录与个性化逻辑继续由 V8 接口引擎及其 CreateIfMissing Hook 编排。 ### 平台启动与私有文件可信原子 官网 PC、UniApp 与微服务的运行时启动能力已经迁入“SaaS引擎”官方应用。新客户端使用以下稳定接口引擎地址;历史 Controller 路由只作为旧客户端兼容入口,不应再被新代码引用: 接口引擎 | 鉴权 | 返回边界 | platform-os-client-by-domain | 匿名 | 只返回匹配的 OsClient | platform-sys-config | 匿名 | 只返回浏览器安全系统设置投影,绝不返回 ServerPrivateSettings 或密钥 | platform-lang-bundle | 匿名 | 返回当前租户、指定语言和前缀的词条 | platform-login-wallpapers | 匿名 | 最多返回 200 条启用壁纸的 Id/Name/Category/ImgUrl 投影 | microi-init | 匿名启动;用户/菜单需请求体原始 DiyToken 重验 | 兼容旧 UniApp 的 OsClient/SysConfig/DateTimeNow/CurrentUser/Token/ModuleList 聚合;禁止匿名跨租户读配置,菜单按角色过滤 | platform-current-user | DiyToken;访问密钥只允许自省 | 只返回宿主恢复的 V8.CurrentUser | platform-private-file-url | DiyToken 或具备 file:read 的访问密钥 | 重算租户、菜单、表、行、字段和对象引用后签发短效代理地址 | platform-sys-user-public-info | DiyToken | 分页返回 Id/Name/Avatar 最小公共投影 | platform-private-file-url 不接受“已登录即可签任意路径”。FormField 必须提交菜单、表、记录和字段四元组并命中字段原值;FormFieldDerivedPreview 还必须提交字段保存的原文件,宿主只重算 DWG→_preview.dxf、STEP/STP→_preview.stl 的唯一同目录派生对象;FileManagerObject 要求单个大小写精确对象 Key、能力探针返回的当前租户文件柜菜单、对象实际存在,并且只接受平台超级管理员 DiyToken,访问密钥即使带 file:read 也不能打开文件柜对象。 其中涉及宿主秘密、不可伪造状态或受限匿名投影的能力分别使用 V8.Method.ResolveOsClientByDomain、GetPublicSysConfig、GetLangBundle、GetLoginWallpapers、GetLegacyInitMenuTree 和 GetAuthorizedPrivateFileUrl。这些不是通用 V8 API:宿主会同时校验当前 OsClient 与固定 ApiEngineKey,其它接口引擎、表单事件或直接调用一律失败关闭。GetLegacyInitMenuTree(rawToken, osClient?) 只允许 microi-init 调用,内部再次验证原始 DiyToken、拒绝访问密钥与跨租户请求,并用 SysMenuLogic.GetSysMenuStep 按权威角色权限构树;它不是通用菜单查询器。GetLoginWallpapers 在 Core 内固定查询启用且未删除的壁纸,只投影 Id/Name/Category/ImgUrl 并限制 200 条;它不会、也不要求把 diy_wallpaper.IsAnonymousRead 打开。公共用户目录继续由已鉴权的 Managed 接口使用 FormEngine 做固定投影与有界分页。 platform-sys-config 从 v1.1.0 开始在 V8 内调用 V8.FormEngine.GetSysConfig 读取配置、分类失败、组装响应;新宿主的 ProjectPublicSysConfig(configuration) 只隔离密钥和服务端脚本,GetLoginEncryptionPublicKey() 只读公钥,GetPlatformEdition() 只读发行类型。不能直接返回 V8.SysConfig 或未经投影的读取结果。旧 GetPublicSysConfig 仅保留给旧后端滚动升级和缺引擎兼容入口;新原子链失败不会转调旧门面。该接口唯一归属“SaaS引擎”应用,Microi.Upgrade 只从它投影必要启动接口,不默认安装整套 SaaS 管理界面或其它可选应用。 SaaS 应用的 platform-license-management-state、platform-license-server-nodes 使用旧 License 地址作为 ApiAddress,同时保留稳定 Key 地址。前者在 V8 组装只读授权状态;后者对非主租户管理员返回空节点列表。签名校验、授权主体及节点读取继续由固定引擎、可信 DiyToken 和租户边界保护。普通账号不会因此获得签发、刷新、写授权文件或修改服务器节点的权限。这两个可选接口不进入启动恢复包;需要包含 ReadLicense* 原子的后端,缺少时返回明确兼容提示,不伪造授权成功。 需要鉴权且允许个性化扩展的 Managed 接口会调用 platform-runtime-custom-hook。匿名启动接口(包括登录壁纸和 microi-init)不调用租户 Hook,避免匿名请求触发写表、通知或外呼副作用;microi-init 即使携带有效 Token,也只执行固定兼容聚合,不把 Token、用户投影或菜单交给租户 Hook。该 Hook 以 CreateIfMissing 首次创建,默认只有 return { Code : 1 };,之后归租户维护且官方升级不会覆盖。官方 Managed 接口顶部会明确提示其所属应用;从可信官方源安装、更新或重新安装应用会恢复官方代码,因此个性化逻辑必须写入 Hook,不能直接修改 Managed 接口。Hook 只接收阶段、来源 Key、当前用户 Id 或结果数量等脱敏元数据,不得传入 DiyToken、密码、Secret、原始 SSO 断言或私有文件短链。 ### 系统账号与租户设置可信原子 用户资料、用户偏好、租户开通和非 Secret 系统设置同样采用“Managed 编排 + 最小可信原子 + CreateIfMissing Hook”: Managed 接口 | 官方应用 / Hook | 可信原子边界 | platform-create-tenant | SaaS引擎 / platform-runtime-custom-hook | AuthorizeCurrentUserTenantProvisioning、ProvisionCurrentUserTenant 从可信当前用户派生所有者与密码材料 | platform-user-update-preferences | 系统账号 / platform-user-custom-hook | 固定当前 V8.CurrentUser.Id 和界面偏好白名单 | platform-user-update-profile | 系统账号 / platform-user-custom-hook | PrepareCurrentUserProfileUpdate 固定当前用户并校验租户头像目录 | platform-sys-user-admin | 系统账号 / platform-user-custom-hook | ManageSysUserAdmin 固定租户与身份,复核表权限、角色层级、改密 step-up、内容安全和会话吊销;授权预检返回规范化 ChangesPassword,V8 不自行解析密码字段 | platform-tenant-system-settings | 系统设置 / platform-system-settings-custom-hook | ValidateTenantSystemSettingsOperation 与 GetTenantSystemSettingsSecurityProjection 只允许超级管理员管理非 Secret 值 | 这些可信方法只允许表中指定的固定 ApiEngineKey 调用,并重复校验当前租户、DiyToken 用户和访问密钥会话,不能作为普通 V8 方法复用。租户 Hook 只收到安全最小投影;Before Hook 可以阻断,主写入完成后的 After Hook、登录投影刷新或审计失败只能返回 Warning,不能把已完成结果改成失败并诱导重试。 密码哈希/重置、DiyToken 签发、管理员查看历史密码、Secret/Sensitive Key 加密保存以及 GetRevealChallenge + Reveal 的一次性步进验证继续留在可信 C#。V8 不获得密码材料、SecretCipher、通用解密器或 Reveal 原子。 ### 官方升级资源发布可信授权 get-microi-upgrade-resource 的固定白名单读取保持匿名兼容;只有 Publish/PublishBatch 写入分支调用 V8.Method.AuthorizeOfficialResourcePublish()。该方法不是通用管理员判断 API,只允许同名官方 Managed 接口调用,并同时固定 iTdos 官方租户、拒绝访问密钥会话、验证 DiyToken 身份,再从租户主库复核用户、状态和平台管理员角色仍有效。资源 JSON 校验、SHA256 乐观锁、固定顺序事务行锁、写入、精确选择元数据同步及发布后哈希回读继续由接口引擎编排。 不得用 V8.CurrentUser.Level >= 9999 代替该控制面授权:Level 是登录投影,可能在角色降级后短暂陈旧;也不得把 AuthorizeOfficialResourcePublish 暴露给其它接口引擎、表单事件或租户 Hook。 ### 系统日志/监控可信原子 系统日志、请求归因和进程/主机观测继续由接口引擎编排;C# 只提供接口引擎无法安全完成的当前节点采样、日志读取和安全封禁原子。官方应用使用 mci-system-observability-query 与 mci-system-observability-action,两者都必须保持登录校验,不能开放匿名调用。 javascript ``` // 当前节点请求、接口、来源 IP、进程、主机、日志队列和安全访问快照 var snapshot = V8.Method.GetSystemObservability({ Action: 'Snapshot', WindowMinutes: 5, // 1~15 分钟 Top: 20, // 5~50 项 IncludeHost: true, IncludeDocker: false }); // 只允许平台超级管理员执行;会写安全审计 var blocked = V8.Method.ManageSystemObservability({ Action: 'BlockIp', Ip: '203.0.113.10', BlockMinutes: 30, Reason: '高频异常请求' }); var unblocked = V8.Method.ManageSystemObservability({ Action: 'UnblockIp', Ip: '203.0.113.10' }); ``` GetSystemObservability 支持 Snapshot、Logs、LogTypes、LogStats、Signal、Trace、ApiRank、AppLogs,以及 Memory、MemoryIncidents、MemoryIncident。事故详情须传 32 位十六进制 IncidentId;历史最多返回 50 条摘要,按可信租户隔离。服务端会重新验证当前租户平台管理员身份,限制分页、时间窗口、排行与日志长度,并对应用日志中的密码、Token、Cookie、Secret、ApiKey 等内容脱敏。观测中间件不采集请求体、查询字符串、Authorization 或 Cookie。 内存诊断独立于 V8Limit,默认采集尚未完成的执行身份、父子链、代码哈希及有界分配样本,不对复杂脚本增加执行限制。累计分配和 CLR 分配栈不等于存活堆/RSS,也不保证 JavaScript 行号。必须部署含独立采集器的 API 并挂载持久 logs 卷;Mongo 不可用时先本机留证,恢复后幂等补传。完整数据边界、采集健康、重启恢复和使用步骤见系统日志/监控:内存事故定位 (https://microi.net/doc/system-engine/system-observability.html)。 Snapshot.Scope.CurrentNodeOnly=true 表示数据只属于当前 API 节点;多实例部署必须逐节点或在外部指标系统聚合。页面展示的请求耗时、吞吐、并发和错误率用于定位 CPU 相关热点,不等同于逐请求 CPU 采样,不能据此宣称某个请求独占了精确 CPU 百分比。ManageSystemObservability 仅接受 BlockIp/UnblockIp,拒绝本机、未指定和组播地址,封禁最长 7 天并写入用户行为审计。 ### 身份应用可信原子 SSO 与平台短信登录遵循“接口引擎编排,C# 只补不可伪造的底层原子”。下列方法不是普通业务 API,只允许对应官方 Managed 接口引擎在精确 OsClient + ApiEngineKey 上调用;其它接口引擎、表单事件、匿名请求或直接 HTTP 调用都会失败关闭: 方法 | 唯一用途 | 允许的接口引擎 | CreateFederatedUser | 校验非管理员角色并以专用带盐哈希创建 SSO JIT 用户 | sso_resolve_federated_identity | CreateSsoLoginTicket | 为已解析用户创建短时一次性 SSO 登录票据 | sso_legacy_token_login | CompleteSsoLogin | 原子消费 SSO 票据、重读启用用户并签发 DiyToken | sso_complete_login | RotateSsoClientSecret | 生成只显示一次的客户端 Secret 并持久化验证哈希 | sso_rotate_client_secret | RunSsoProtocol | 执行固定 OIDC/SAML/CAS 协议原子并返回进程内签名 HTTP 契约 | 对应的 24 个 sso_http_* 精确 Key | CreatePlatformSmsProof | 限频并原子消费短信验证码,签发短时证明 | platform_auth_sms_login | CreatePlatformSmsUser | 在短信证明约束下创建带盐哈希用户 | platform_auth_sms_login | CompletePlatformSmsLogin | 原子消费短信证明并签发 DiyToken | platform_auth_sms_login | ResetPlatformSmsPassword({Phone,SmsCode,NewPassword,ConfirmPassword,_Lang}) | 原子消费短信验证码,同时保存密码哈希与 PwdEncode,撤销旧会话 | official_password_reset | RepairPlatformPasswordEncoding({UserIds,DryRun,ConfirmExecution,_Lang}) | 主库复核管理员后预览或修复已确认的历史密码编码不一致;最多 20 个用户 | platform-sys-user-admin 的 RepairPasswordEncoding 动作 | 短信重置与编码修复需要后端 8.7.4 或更高版本,配套 SaaS 引擎 v8.6.13、系统账号 v8.4.13;仅升级应用包不能代替后端能力升级。official_password_reset 由 SaaS 包单独以 Managed 资源交付,V8 直接返回原子结果,不能自行用 DES 写 Pwd 或把密码交给租户 Hook。Pwd 与 PwdEncode 必须作为一组一致更新:旧脚本只更新 Pwd 会造成后台仍可显示遗留密文对应原文,而密码验证按新编码失败。注册、重置与密码登录使用同一原始密码输入,不应在某一环节单独 trim。 编码修复默认 DryRun=true;实际执行需 DryRun=false 与 ConfirmExecution='REPAIR_PASSWORD_ENCODING'。返回仅含用户 Id、分类及是否可修复/已修复,不返回密码材料,也不改变用户状态、账号或权限。仅 State=1 且未删除的用户可登录,历史 State=2 仍是禁用。官网登录的手机号别名只在精确账号不存在且手机号唯一时解析;最终身份仍使用可信认证结果的用户 Id。 前端传入的 _Lang 必须继续传给嵌套登录/重置原子。没有参数时可沿用 lang 请求头,再默认 cn;服务端按语言返回可读错误,语言缓存未预热也不能直接返回 AccountOrPwdError 等内部 Key。V8.TranslateEngine.GetLang(key, lang) 的第二个参数是目标语言。 接口引擎不得把这些方法包装成“通用用户创建/Token 生成”接口,也不得让客户端传入目标 OsClient、任意用户对象、Role Level、Secret 保存位置或回调地址。官方应用分别通过 app.microi.sso 与自动安装的 app.microi.saas-engine 交付编排代码和 ResourcePolicies;应用尚未安装时,普通账号密码登录仍由最小启动内核保证可用。 SSO 的完整应用合同、协议端点和 35 个接口引擎见 SSO 身份联邦 (https://microi.net/doc/more/sso.html)。 ### V8.Method.Upload V8.Method.Upload 使用当前租户文件配置,并执行平台上传上限与租户动态配额。默认上限为:单文件 500 MB、单请求 500 MB、最多 10 个文件、每用户每天 2 GB、每租户每天 20 GB;sys_osclients.DisableFileUpload 默认关闭(即允许上传),只有显式开启才禁止当前租户上传。FileUploadMaxFileMB、FileUploadMaxRequestMB、FileUploadMaxCount、FileUploadDailyUserQuotaMB、FileUploadDailyTenantQuotaMB 可按租户调整;旧 FileUploadEnabled 仅供未升级节点兼容。HTTP 上传会在 Base64 解码前预检体积,日配额用 Redis Lua 原子计数;配额服务不可用时失败关闭。 javascript ``` var uploadResult = V8.Method.Upload({ FilesByteBase64: V8.FilesByteBase64, Limit: true, Preview: false, Path: '/file', OsClient: V8.OsClient }); // 接口引擎生成 JSON、源码或模板时直接上传 UTF-8 文本,避免 Base64 4/3 膨胀。 // 仍受当前租户 HDFS、公有/私有桶、扩展名、配额和 256 MB 文本上限约束。 var textUploadResult = V8.Method.UploadText({ Content: JSON.stringify(packageModel), FileName: 'application-v1_2_3.json', Path: '/microi-store/packages/application/202608', Limit: false, Preview: false, OsClient: V8.OsClient }); ``` UploadText 只接受 Content 和一个安全的 FileName,禁止同时传 FilesByteBase64/FilesByte/Files。它避免字符串先转 Base64 再还原字节产生的额外内存和错误文本编码,但不会替调用方完成内容哈希:商城等可信发布流程仍须对上传结果回读,并核对 UTF-8 字节数与 SHA-256 后才能提交数据库指针。 可信后端可用 V8.Method.GetPrivateFileText({ OsClient: V8.OsClient, FilePathName, Limit: true, MaxBytes }) 读取当前租户私有文本,成功返回 { Code: 1, Data: UTF8字符串 }。MaxBytes 默认 64 MiB, 受 256 MiB 固定上限约束;必须先验证 Code=1 和字符串类型,失败不能改走未经授权的下载地址。 该方法按 UTF8 解码,不适合直接保真转换二进制。需要原字节保真的源码导出应先用 V8.EncryptHelper.Sha256Hex(result.Data) 核对已验证的原字节摘要,再用 V8.Base64.StringToBase64(result.Data) 编码并核对长度;摘要不匹配时使用原字节路径。 最终仍核验将返回的 ZIP 实际条目,不以一次文本读取或另一份对象摘要代替归档验签。 大文本这样避免 CLR 字节数组进入 Jint 的逐字节复制,权限、路径、配额和活动版本约束仍由调用方负责。 应用编译资产和私有源码在当前租户桶内迁移时,可使用 V8.Method.CopyObject({ FilePathName: 'itdos/旧对象', Path: 'itdos/micro-app/app-key/v1.0.0/index.html', Limit: false })。Limit: true 对应私有桶源码,源与目标不能跨桶;两个路径均被收敛到当前租户。V8.Method.ObjectExist({ FilePathName, Limit }) 检查对象,V8.Method.GetObjectSha256({ FilePathName, Limit }) 在服务端流式返回 Data.Sha256(原始字节)、Data.WireSha256(旧版 Base64 文本)和 Data.Size,不把对象字节传入 V8。V8.Method.ListObjects({ Path, Limit, Recursive: true, Marker, MaxKeys }) 按单个应用前缀分页列举,单页上限 1000。历史版本复制前核对已有目标哈希,完成后还须从 CDN 独立回读公有资产;不能因 CopyObject 返回成功就更新商城体验地址。这些管理原子只在可信后端接口引擎中使用,不向匿名调用者开放。 MinIO/S3 复制保留源对象的 MIME 参数和元数据,包括 text/css; charset=utf-8。旧后端使用 MinIO SDK 7 时,带参数 MIME 的复制请求可能因重复 Content-Type 返回签名不匹配;需升级后端复制传输修复,再沿用原请求键回读目标并恢复。修复只还原 SDK 已签名的精确 MIME,继续由存储服务校验签名和执行复制;不能通过移除 charset、放宽签名校验或下载后重新上传来掩盖问题。取消或超时不证明目标未写入,必须先核对目标字节数与哈希。 #### 上传 HTTP 请求的接口引擎扩展 V8.Method.UploadCurrentRequestAsync() 执行当前上传 HTTP 请求的可信文件流操作; V8.Method.IsLegacyUploadCompatibilityEnabled() 读取当前租户“兼容平台旧版本”开关。 两者不接受身份、租户、桶或文件载荷参数,只能在宿主选定的上传接口引擎作用域中使用。 文件字节不进入 Jint,同一请求重复调用复用同一个上传任务,失败也不自动重试。 platform-hdfs-upload 负责返回编排,platform-hdfs-upload-hook 可扩展成功结果。 普通后端脚本上传继续使用 V8.Method.Upload;离开上传请求的脚本不能调用上述请求原子。 接口字段、开关和升级步骤见 旧移动端上传返回兼容 (https://microi.net/doc/more/hdfs.html#%E6%97%A7%E7%A7%BB%E5%8A%A8%E7%AB%AF%E4%B8%8A%E4%BC%A0%E8%BF%94%E5%9B%9E%E5%85%BC%E5%AE%B9)。 #### 私有文件访问与审计 普通 HTTP 上传只允许平台规定的目录并默认按私有文件处理;可信后端 V8 可进行租户内受控文件操作,但不能把 GetPrivateFileByte、对象列举或删除等管理能力直接暴露给普通用户。浏览器访问私有文件时还必须证明菜单、记录、字段与附件绑定关系,详见 文件上传与私有文件 (https://microi.net/doc/more/hdfs.html)。 GetPrivateFileUrl 返回的是后端短期票据代理地址,而不是可泄露的对象存储真实签名地址。后端会分别记录链接签发和实际 GET/HEAD 打开/下载行为;登录用户记录为 Name(Account),转发链接被无身份访问时记录为匿名访问。代理支持 Range 流式响应并对分片请求短时去重,失败时不会退回未经审计的真实签名地址。Limit:false 的公有文件仍可直接走 CDN/公有桶,不记录此类行为日志。 #### 系统日志队列与持久化 系统日志调用会先进入后端真正有界的内存队列,由单一后台消费者按批次写入 MongoDB;请求线程通常不等待 MongoDB。平台固定使用主队列 4096 条、内存重试区 512 条、每批 250 条,安装者无需维护队列容量环境变量。两级内存都满时会同步写持久化 spool 形成回压,禁止用无界内存队列继续堆积;健康信息会公开 Capacity、OverflowPending、EmergencySpooled 和 Dropped,其中 Dropped 必须保持为 0。 每批日志在写 MongoDB 前先写入固定目录 logs/syslog-spool,MongoDB 暂时不可用或服务正常重启时会自动幂等重放。容器部署时应直接把该目录挂载到持久卷,节点标识由平台自动生成,不需要额外环境变量。所有节点共享 MongoDB/Redis,日志按全局 EventId 幂等 upsert,详情停留状态和私有附件票据可跨节点继续读取。 #### 用户行为日志 平台内置用户行为日志还会记录 Category、Action、Source、TargetType、TargetId、SessionId、DurationSeconds、Success、OccurredAt 等结构化字段。用户显示统一采用 Name(Account);密码、Token、Authorization、Secret、ApiKey、连接字符串等敏感内容会在进入队列时脱敏和限长。 ### API 进程内存保护 API 默认启用进程级内存保护。达到软阈值后节点会停止接收普通请求并返回 HTTP 503;连续达到硬阈值后先请求宿主有界停机,宽限期结束仍未释放时以退出码 137 强制结束,由 Docker/Kubernetes/服务管理器重启,避免单个节点耗尽整台宿主机内存。GET /api/Diagnostics/health 同时承担 readiness:内存保护期间返回 503;GET /api/Diagnostics/liveness 只表示进程仍存活。 #### 阈值与度量口径 保护阈值统一依据进程实际驻留内存(Windows Working Set / Linux RSS),不能依据 Linux 下的 PrivateMemorySize64。后者可能包含 .NET GC 预留但尚未占用物理内存的巨大虚拟地址空间,数值甚至会超过宿主机物理内存数倍,只能作为诊断值。健康接口会同时返回 PressureMetric=ResidentSet、WorkingSetMB、PrivateAddressSpaceMB 与 ManagedHeapMB,其中只有驻留内存参与熔断判断。 默认先识别 Linux cgroup v2/v1 容器内存上限;容器未限额时使用宿主机物理内存,其他平台回退到 .NET GC 可用内存。软阈值固定为该有效内存额度的 95%,硬阈值固定为 98%,不再固定封顶为 4096 MB。例如 48 GiB 单节点默认约为 Soft=46694 MB、Hard=48168 MB,RSS 3.94 GB 不会触发保护。平台仍采用安全轮询、连续样本和有界退出策略。 内存保护不增加任何专用环境变量,也不要求在 appsettings.json 中维护一组节点参数。95%/98% 属于平台自动安全边界;后续确需面向用户开放调整时,应进入 SaaS 引擎或系统设置统一管理。 #### 多节点部署与缓存预热 阈值按单个 API 节点的有效内存额度计算。单节点独占宿主机时直接使用默认 95%/98%;多个 API/Worker 或数据库共用同一宿主机时,必须由容器编排层给每个容器设置独立 memory limit,避免所有节点都按整机额度计算造成超卖。生产环境必须配置自动重启和 readiness 摘除;多节点滚动发布时,一个节点进入内存保护不能影响其它节点继续服务。 启动缓存和批量预热也必须自身有界。例如多语言运行时缓存先做有效行数预算检查,只读取租户实际启用的语言列,再按 Id 游标分页;默认每页 500、最多扫描 10000 行、最多保留 5000000 字符、单条 SQL 最长 30 秒。超过行数预算时不会先物化预算上限内的巨大对象图,而是立即拒绝本次重载并保留旧缓存。分页数、最大行数、最大字符数和 SQL 超时统一在主租户 SaaS 引擎的“平台运行配置”中维护,不增加环境变量;提高上限前必须测量单节点峰值内存。禁止使用 SELECT * 后一次性 ToList,再复制为第二份字典;数据库异常时保留旧缓存并失败关闭。 ## V8.Base64 - Base64转换,与System.Convert.ToBase64String(bytes)不同的是V8.Base64若遇异常会直接返回源字符串 javascript ``` var result = V8.Base64.StringToBase64('123456'); var result = V8.Base64.Base64ToString('MTIzNDU2'); ``` ## 图像处理 V8.Image V8.Image 提供跨平台的服务端图片生成、合并和编辑能力。所有方法都以对象形式传参,只处理内存中的 Base64、Data URI 或字节数组,不直接读取本地路径,也不会主动访问 URL。 ### 图片来源与返回值 图片来源支持以下形式: javascript ``` // 顶层 Base64 { FileByteBase64: '' } // 等价字段 { Base64: '' } { DataUrl: 'data:image/png;base64,...' } { Bytes: response.RawBytes } // 单图方法也支持 Image / Source 嵌套,值可以是对象或字符串 { Image: { FileByteBase64: '' } } { Source: '' } ``` 处理成功时,除 GetInfo 外均返回标准 DosResult: javascript ``` { Code: 1, Data: { FileName: 'image.png', ContentType: 'image/png', FileByteBase64: '', Width: 800, Height: 600, Size: 12345, Format: 'png' }, Msg: '' } ``` 每次调用后必须先判断 Code。接口引擎开启“响应文件”后,可以直接返回这个结果,在浏览器中预览或下载图片。 公共输出参数: 参数 | 默认值 | 说明 | OutputFormat / Format | png | 支持 png、jpeg / jpg、webp、bmp;OutputFormat 优先 | Quality | 90 | 编码质量,运行时限制到 1 至 100 | BackgroundColor | 透明;JPEG 为白色 | 画布背景色 | FileName | image.<扩展名> | 输出文件名,扩展名会按真实格式修正 | 兼容公共别名:ImageFormat / OutputType → OutputFormat,Background / BgColor → BackgroundColor,单图方法的 ImageBase64 → FileByteBase64。 ### 方法列表 方法 | 说明 | V8.Image.Create(param) | 生成纯色、渐变、文字或基础图形图片 | V8.Image.Merge(param) | 横向、纵向、网格或覆盖合并图片 | V8.Image.Overlay(param) | 覆盖合并快捷方法,未设置模式时自动使用 overlay | V8.Image.Resize(param) | 调整宽高 | V8.Image.Crop(param) | 裁剪矩形区域 | V8.Image.Rotate(param) | 旋转图片 | V8.Image.Flip(param) | 水平或垂直翻转 | V8.Image.Convert(param) | 转换图片编码格式 | V8.Image.RemoveSolidBackground(param) | 将纯色背景透明化,输出 PNG | V8.Image.Draw(param) | 在已有图片上绘制文字和图形 | V8.Image.Watermark(param) | 添加图片水印 | V8.Image.CreateQRCode(param) | 生成二维码 | V8.Image.GetInfo(param) | 读取宽高、格式、帧数等信息 | RemoveSolidBackground 接收内存图片及 Tolerance、Feather,输入最多 20,000,000 像素。可用 ChromaKeyColor: '#00ff00' 指定抠像色,并以 EdgeConnectedOnly: true 只处理连接画布边缘的背景,保留主体内部同色区域。专门生成的纯绿幕素材还可开启 SuppressGreenSpill: true,清除手臂间封闭绿幕并压低边缘溢绿;主体本身有绿色时不应开启。省略这些参数保持四角估色与全图匹配的兼容行为。该方法是确定性颜色处理,不能替代语义分割。 Create 的专用参数: 参数 | 默认值 | 说明 | Width / Height | 800 / 600 | 新画布宽高 | CanvasWidth / CanvasHeight | 未设置 | 设置后分别覆盖 Width / Height | BackgroundColorEnd | 未设置 | 设置后与 BackgroundColor 形成线性渐变 | GradientDirection | left-to-right | 支持横向、top-to-bottom / vertical、diagonal | Text / TextColor / FontSize / FontFamily | 未设置 / #111827 / 32 / 默认字体 | 在画布中心追加快捷文字 | Elements | 未设置 | 文字、矩形、椭圆、圆形和线段列表 | ### 生成图片并覆盖合并 下面示例先生成大图和小图,再把小图覆盖到大图的指定坐标。覆盖模式按 ZIndex 从小到大绘制,数值更大的图层位于上方;相同 ZIndex 时数组中靠后的图层位于上方。 javascript ``` var baseResult = V8.Image.Create({ Width: 1200, Height: 700, BackgroundColor: '#2563eb', BackgroundColorEnd: '#0f172a', GradientDirection: 'left-to-right', Text: 'Microi', TextColor: '#ffffff', FontSize: 72, FileName: 'poster.png' }); if (baseResult.Code !== 1) return baseResult; var badgeResult = V8.Image.Create({ Width: 240, Height: 120, BackgroundColor: '#f97316', Text: 'NEW', TextColor: '#ffffff', FontSize: 42 }); if (badgeResult.Code !== 1) return badgeResult; var result = V8.Image.Overlay({ CanvasWidth: 1200, CanvasHeight: 700, Images: [ { FileByteBase64: baseResult.Data.FileByteBase64, Width: 1200, Height: 700, Fit: 'fill', ZIndex: 0 }, { FileByteBase64: badgeResult.Data.FileByteBase64, X: 900, Y: 80, Scale: 0.75, Opacity: 0.95, CornerRadius: 16, ZIndex: 10 } ], OutputFormat: 'png', FileName: 'poster-with-badge.png' }); return result; ``` 也可以使用双图简写: javascript ``` return V8.Image.Overlay({ BaseImage: baseResult.Data.FileByteBase64, OverlayImage: badgeResult.Data.FileByteBase64, X: 900, Y: 80, OverlayWidth: 180, OverlayHeight: 90, Opacity: 0.9 }); ``` 主图兼容 BaseImage、BackgroundImage、FirstImage、Base;覆盖图兼容 OverlayImage、ForegroundImage、SecondImage、Overlay。简写结构中的顶层 X、Y、Position、Opacity、OverlayWidth、OverlayHeight、Scale 会应用到覆盖图。 ### 合并模式 javascript ``` // 左右拼接 var horizontal = V8.Image.Merge({ Mode: 'horizontal', Direction: 'ltr', Gap: 20, Padding: 20, Alignment: 'center', Images: [ { FileByteBase64: firstBase64, Height: 320 }, { FileByteBase64: secondBase64, Height: 320 } ] }); // 上下拼接 var vertical = V8.Image.Merge({ Mode: 'vertical', Direction: 'ttb', Gap: 16, Alignment: 'left', Images: [firstBase64, secondBase64] }); // 网格拼接 var grid = V8.Image.Merge({ Mode: 'grid', Columns: 3, Gap: 12, Padding: 12, Images: imageBase64List }); ``` 参数 | 说明 | Mode | horizontal、vertical、grid、overlay | Layout | 优先于 Mode;支持 row、column、canvas、cover,以及 left/right/top/bottom/up/down 方向快捷值 | Direction | ltr、rtl、ttb、btt,也支持 left-to-right 等完整写法 | Images / Layers | 图片或图层数组;数组项可以直接是 Base64 / Data URI 字符串 | CanvasWidth / CanvasHeight | 固定画布尺寸;未设置时按布局自动计算 | Padding / Gap | 内边距 / 图片间距,负数按 0 处理 | Alignment | 横向时控制上下对齐,纵向时控制左右对齐,网格时控制单元格内对齐 | Columns | 网格列数 | 合并兼容别名:MergeType / Type → Mode,Items → Images。 ### 图层参数 参数 | 默认值 | 说明 | Width / Height | 原尺寸 | 只设置一个时按比例计算另一个 | Scale | 1 | 在宽高计算后再次按比例缩放,范围大于 0 且不超过 100 | Fit | contain | 同时设置宽高时支持 contain、cover、fill / stretch、none | X / Y | 未设置 | 覆盖模式绝对坐标;设置其中一个后,另一个默认使用 Padding | Position / Anchor | top-left | 未设置坐标时的锚点;Position 优先 | OffsetX / OffsetY | 0 | 坐标或锚点定位后的偏移 | Opacity | 1 | 透明度,限制到 0 至 1 | Rotation | 0 | 顺时针旋转角度 | ZIndex | 0 | 覆盖顺序,数值越大越靠上 | FlipHorizontal / FlipVertical | false | 翻转当前图层 | CropX / CropY / CropWidth / CropHeight | 原图范围 | 缩放前裁剪源图 | CornerRadius | 0 | 圆角半径 | BorderColor / BorderWidth | 未设置 / 0 | 图层边框 | BlendMode | src-over | 混合模式 | contain 保持完整内容并等比缩放;cover 居中裁剪并填满目标宽高;fill / stretch 强制拉伸;none 使用原尺寸。Scale 在上述计算后继续生效。 常用锚点:top-left、top、top-right、left、center、right、bottom-left、bottom、bottom-right。混合模式支持 src-over、multiply、screen、overlay、darken、lighten、plus / add、src、dst-over。 图层兼容别名:Order → ZIndex、Alpha → Opacity、Rotate → Rotation、Left / Top → X / Y。 ### 其它图片操作 javascript ``` // 缩放:Width、Height 至少设置一个;Pad=true 时保留完整目标画布 var resized = V8.Image.Resize({ Image: sourceBase64, Width: 800, Height: 600, Fit: 'cover', Pad: false, AllowUpscale: true, Alignment: 'center' }); // 裁剪;Clamp=true 时把部分越界区域收缩到图片范围 var cropped = V8.Image.Crop({ Image: sourceBase64, X: 100, Y: 80, Width: 640, Height: 360, Clamp: false }); // 旋转;Expand=false 时保持原画布,边缘可能被裁掉 var rotated = V8.Image.Rotate({ Image: sourceBase64, Degrees: 30, Expand: true }); // 水平、垂直翻转;Horizontal 默认 true,Vertical 默认 false var flipped = V8.Image.Flip({ Image: sourceBase64, Horizontal: true, Vertical: false }); // 格式转换 var converted = V8.Image.Convert({ Image: sourceBase64, OutputFormat: 'webp', Quality: 85, FileName: 'converted.webp' }); // 图片水印 var watermarked = V8.Image.Watermark({ BaseImage: sourceBase64, Watermark: logoBase64, Width: 180, Height: 90, Scale: 1, Position: 'bottom-right', Margin: 24, OffsetX: 0, OffsetY: 0, Opacity: 0.7, Rotation: 0 }); // 二维码;Content 优先于 Text,Size 默认 300 var qr = V8.Image.CreateQRCode({ Content: 'https://microi.net/', Size: 420, FileName: 'qrcode.png' }); // 读取原始图片信息 var info = V8.Image.GetInfo({ Image: sourceBase64 }); // Data: Width、Height、Format、ContentType、Size、FrameCount、 // RepetitionCount、Origin、HasAlpha ``` Watermark 的 BaseImage 也可写为 Image,兼容 Base → BaseImage、Overlay → Watermark。 ### 绘制文字和图形 Create 和 Draw 使用相同的 Elements。Create 在新画布上绘制;Draw 在输入图片上绘制,输出宽高与原图相同。 javascript ``` var result = V8.Image.Draw({ Image: sourceBase64, Elements: [ { Type: 'text', X: 40, Y: 40, Text: 'CONFIDENTIAL', Color: 'rgba(239,68,68,0.75)', FontSize: 36, FontFamily: 'Arial', FontStyle: 'bold-italic', Align: 'left', VerticalAlign: 'top', Rotation: -8 }, { Type: 'round-rect', X: 40, Y: 90, Width: 320, Height: 100, FillColor: '#ffffff88', StrokeColor: '#ef4444', StrokeWidth: 3, CornerRadius: 16, Opacity: 0.9 }, { Type: 'line', X: 40, Y: 220, X2: 360, Y2: 220, StrokeColor: '#ef4444', StrokeWidth: 3 } ] }); ``` 元素类型 | 参数 | text | Text、Color、FontSize、FontFamily、FontStyle、Align、VerticalAlign | rectangle / rect / round-rect | X、Y、Width、Height、填充、描边、圆角 | ellipse / circle | X、Y、Width、Height、填充、描边 | line | X、Y、X2、Y2 或 Width、Height、描边 | 所有元素还支持 Opacity 和 Rotation。单次最多绘制 500 个元素。 ### 颜色、安全与资源限制 颜色支持常用英文颜色名、transparent、#RGB、#RGBA、#RRGGBB、#RRGGBBAA、rgb(...)、rgba(...)。颜色自身的 Alpha 会与 Opacity 相乘。 运行时内置限制:单次最多合并 50 张图;单边不超过 16,384 像素;单张输入或输出画布不超过 25,000,000 像素;单次解码和单次缩放后图层分别不超过 50,000,000 像素;单张输入不超过 25 MB;单次输入总量不超过 100 MB;输出不超过 50 MB。 这些限制是保护上限,不是业务推荐值。匿名接口应增加更严格的数量、尺寸、并发和权限限制。远程图片必须先通过 V8.Http 下载,并对用户可控 URL 做协议、域名和目标地址白名单校验,不能把 URL 或服务器路径直接传给 V8.Image。 FontFamily 是首选字体。运行时会逐个 Unicode 字符验证字形:未传字体、指定字体不存在或某个字体缺少部分字符时,先回退到服务器已安装且包含该字形的字体,再回退到随 Dos.Common 程序集发布的 Noto Sans CJK SC;同一段中英文混排文字可使用多个字体段。因此没有安装任何系统字体的 Linux / 群晖 / 精简容器也能绘制基础拉丁字符、数字和简体中文。如果系统字体与内置字体都不包含某字符,接口会返回带字符及 U+XXXX 码位的明确错误,绝不会生成“口口”缺字方框。内置字体解决可用性,不替代品牌字体、繁体异体字、特殊符号或 Emoji 字体;要求固定字形时仍应在服务器安装业务字体并显式传 FontFamily。 Unicode 非字符(U+FDD0–U+FDEF,以及各 Unicode 平面末尾的 FFFE/FFFF)会在匹配字体前被拒绝,并返回准确码位。macOS 的占位字体不会使这些字符被误判为可绘制文字。 ## 当前用户 V8.CurrentUser - 当前登陆用户信息,包含用户所属角色、组织机构等,包含使用表单引擎对sys_user表新增字段的信息。 - 未登录时访问到的值为{} js ``` var userName = V8.CurrentUser.Name; ``` ## 数据库对象 V8.Db - 数据库访问对象,支持Dos.ORM、SqlSugar切换 - FromSql 只传 SQL 字符串;动态值请使用 .AddInParameter("@p0", value) 链式绑定,不要写 FromSql(sql, value)。 js ``` // 查询多条 var list = V8.Db.FromSql("select Id, Account, Name from sys_user where Status = @p0") .AddInParameter("@p0", 1) .ToArray(); // 执行 insert/update/delete,返回受影响行数 var affected = V8.Db.FromSql("update sys_user set Status = @p0 where Id = @p1") .AddInParameter("@p0", 0) .AddInParameter("@p1", userId) .ExecuteNonQuery(); // 查询单条 var user = V8.Db.FromSql("select Id, Account, Name from sys_user where Id = @p0") .AddInParameter("@p0", userId) .First(); // 查询单个标量 var count = V8.Db.FromSql("select count(1) from sys_user where Status = @p0") .AddInParameter("@p0", 1) .ToScalar(); ``` ## 数据库只读对象 V8.DbRead - 数据库只读对象,用法和V8.Db一样,当数据库未部署读写分离时,此对象与V8.Db对象值一致。 ## 扩展数据库对象 V8.Dbs “数据库管理”中启用的连接按 DbKey 暴露为 V8.Dbs.。当前支持配置的数据库类型为 MySql、SqlServer、Oracle、PostgreSql、DaMeng、KingBase;完整配置、MCP 结构读取和附件迁移说明见扩展数据库与外部数据迁移 (https://microi.net/doc/system-engine/databases.html)。 安装或更新应用商城中的“数据库扩展”后,microi_database 的后端提交后事件会调用 V8.Method.RefreshExtensionDatabases()。事件在事务提交后递增按 OsClient 隔离的 Redis 版本,因此新增、修改、停用或删除连接后,各 API 节点下一次访问 V8.Dbs 即可看到新配置,无需重启;短 TTL 仅作为旧版本节点的兼容兜底。 Oracle 原生日期格式化使用 TO_CHAR(日期值, 'YYYY-MM-DD'),不要交换值与格式的顺序。 ToArray() / ToList() 才会执行并读取查询,因此错误堆栈停在该方法不等于集合转换失败。 若出现 ORA-01722,应核对最终 SQL、参数类型及账号筛选条件,分别验证计数和列表查询, 不要用重复执行掩盖确定性的转换错误。旧版 Oracle Provider 曾错误交换 TO_CHAR 参数, 应升级包含原生 SQL 保留修复的后端;无需修改数据库列或将业务查询改为无条件查询。 js ``` var dataList = V8.Dbs.OracleDB1 .FromSql('SELECT ID, NAME FROM CUSTOMER WHERE STATUS = @p0') .AddInParameter('@p0', 1) .ToArray(); // 不保存到数据库管理:创建仅当前请求使用的临时会话 // 连接串只能来自可信服务端配置,禁止直接传 V8.Param.ConnectionString var tempDb = V8.Dbs.Open( 'SqlServer', 'Server=127.0.0.1,1433;Database=app;User Id=user;Password=***;TrustServerCertificate=True;' ); var tempRows = tempDb.FromSql('SELECT Id, Name FROM Customer WHERE Status = @p0') .AddInParameter('@p0', 1) .ToArray(); // 扩展数据库事务与 V8.DbTrans 完全独立,需要手动管理生命周期 var recordId = V8.Param.Id; var emptyExTrans = V8.Dbs.EmptyEx.BeginTransaction(); try { var count = emptyExTrans .FromSql("delete from diy_extend_test where Id = @p0") .AddInParameter("@p0", recordId) .ExecuteNonQuery(); emptyExTrans.Commit(); return { Code : 1, Data : count }; } catch (error) { emptyExTrans.Rollback(); throw error; } finally { emptyExTrans.Close(); } ``` 新增或修改保存连接后会递增共享 Redis 版本,各节点在下一次访问时立即回源,不需要重启 API。默认兜底 TTL 为 60 秒,需要调整时修改 SaaS 引擎主租户的 ExtensionDatabaseCacheSeconds。连接串、密码和鉴权参数不得出现在日志、前端代码或接口返回中。 ## 数据库事务 V8.DbTrans V8.Db 是主库会话,V8.Db.FromSql 不会自动加入当前接口引擎事务。需要共同提交或回滚的 SQL 必须使用 V8.DbTrans.FromSql;依赖本事务尚未提交的数据时,查询也使用该入口。FormEngine 和嵌套 ApiEngine.Run 显式传入 V8.DbTrans 共享事务。不要读取安全代理的内部事务来手动提交。 - 数据库事务对象,可以像V8.Db一样使用,如: js ``` var array = V8.DbTrans.FromSql('...').ToArray(); ``` 无需在接口引擎中手动调用 V8.DbTrans.Commit() 或 Rollback()。事务生命周期由平台安全代理统一管理: - 返回 DosResult 或带 Code 的对象时,只有 Code === 1 提交。 - 返回对象但没有 Code 时回滚。 - 返回字符串、数字、数组、布尔值或 null,且脚本未异常时提交。 - 复用外层传入的事务时,最终结果由外层调用决定。 - 脚本显式调用平台事务的 Commit/ Rollback/Close 会被忽略。 - 接口引擎示例 javascript ``` //操作第一张表,带事务 var result1 = V8.FormEngine.UptFormData('表名或表Id,不区分大小写', { Id : '',//必传 Age : 20, //要修改的字段,注意字段值不能是{}或[],需要序列化 Sex : '女' }, V8.DbTrans); //操作第二张表,带事务 var result2 = V8.FormEngine.UptFormData('表名或表Id,不区分大小写', { Id : '',//必传 Age : 20, //要修改的字段,注意字段值不能是{}或[],需要序列化 Sex : '女' }, V8.DbTrans); //如果第二张表操作成功 if(result2.Code == 1){ return { Code : 1 };//平台会自动提交事务,因为返回的Code=1 }else{//如果第二张表操作失败 return { Code : 0, Msg : result2.Msg };//平台会自动回滚事务,因为返回的Code=0 } ``` ## V8.MQTT V8.MQTT 是 MQTT 事件接口引擎的只读上下文,配合 V8.EventName 使用。它不是普通接口引擎中的通用发布函数;服务端设备下行使用带租户上下文的 IMicroiMQTT.PublishAsync(osClient, ...)。 V8.EventName | 说明 | StartServer / StopServer | 当前 Broker 启动或正常停止 | Connected / Disconnected | 当前租户设备连接或断开 | Subscribing | 订阅通过租户 Topic ACL 后 | MessageReceived | 收到发布消息;返回 Code != 1 可阻止向订阅者广播 | MessageChanged | Retained Message 发生变化 | V8.MQTT 字段 | 说明 | ClientId、OsClient、Topic | 已由 Broker 校验的设备、租户与规范化 Topic | Payload、PayloadRaw | JSON 自动解析结果与原始 UTF-8 文本 | UserName、UserProperties | 连接用户名与 MQTT v5 User Properties | Qos、Retain | QoS 级别与保留消息标记 | javascript ``` if (V8.EventName === 'MessageReceived') { console.log(V8.MQTT.ClientId + ' -> ' + V8.MQTT.Topic); return { Code: 1, Data: V8.MQTT.Payload }; } ``` 完整的 Broker 配置、SaaS 凭据、Topic 隔离、设备级接口引擎、TLS、数据落库、服务端下行与集群边界见 MQTT 引擎(IoT 物联网) (https://microi.net/doc/system-engine/mqtt-engine.html)。 ## V8.MongoDb 平台兼容镜像基于驱动 3.11.2 的安全修复维护 MongoDB 3.6 及当前服务端的 CRUD、日志路径,保留 BSON、认证和逐项特性版本检查。遇到 wire version 6 ... requires at least 9 时,需要更新包含兼容构建的后端镜像;该错误不表示日志为空或 Token 失效。驱动兼容不会自动更新数据库,也不修复旧服务端自身的缺陷。 ### 介绍 - 本篇介绍如何在接口引擎、后端V8事件中对MongoDB进行相关操作 - 对MongoDB的新增操作会自动生成对应数据库名和表名,因此可自定义分库、分表规则 ### 新增数据 AddFormData *自定义数据库名、表名,不存在时会自动创建 javascript ``` //可以指定固定的Id值 var newId = V8.MongoDb.NewId(); V8.MongoDb.AddFormData({ DbName : '', //数据库名称,如:sys_log_2024 TableName: '', //表名名称,如:log_2024_12 Id : newId, //也可以不指定,会自动生成 _FormData : { Name : '张三', Sex : '男', Age : 18 } }); ``` ### 修改数据 UptFormData javascript ``` V8.MongoDb.UptFormData({ DbName : '', //数据库名称,如:sys_log_2024 TableName: '', //表名名称,如:log_2024_12 Id : '', //数据Id _FormData : { Name : '张三', Sex : '男', Age : 18 } }); ``` ### 按条件批量修改 UptFormDataByWhere javascript ``` var result = V8.MongoDb.UptFormDataByWhere({ DbName: 'diy_chat_' + V8.OsClient.toLowerCase(), TableName: 'chat_' + DateNow('yyyy'), _Where: [ ['FromUserId', '=', V8.Param.PeerUserId], ['ToUserId', '=', V8.CurrentUser.Id], ['IsRead', '=', false] ], _FormData: { IsRead: true } }); ``` _Where 必须是至少包含一个有效条件的参数化数组;空条件、无效字段或操作符会失败关闭,不会退化为全集合修改。_FormData 不得修改 _id 及其子路径。成功返回 MatchedCount 和 ModifiedCount。 ### 删除数据 DelFormData javascript ``` V8.MongoDb.DelFormData({ DbName : '', //数据库名称,如:sys_log_2024 TableName: '', //表名名称,如:log_2024_12 Id : '', //数据Id }); ``` ### 按条件批量删除 DelFormDataByWhere javascript ``` var result = V8.MongoDb.DelFormDataByWhere({ DbName: 'diy_chat_' + V8.OsClient.toLowerCase(), TableName: 'chat_last_contact', _Where: [ ['UserId', '=', V8.CurrentUser.Id], ['ContactUserId', '=', V8.Param.PeerUserId] ] }); ``` 此方法同样强制非空 _Where,成功返回 DeletedCount。删除业务必须把当前用户、权威资源 Id 等边界写入条件,不得仅依赖前端传入的行 Id。 ### 查询数据列表 GetTableData javascript ``` V8.MongoDb.GetTableData({ DbName : '', //数据库名称,如:sys_log_itdos TableName: '', //表名名称,如:log_202412 _Where : [ ['Type', '=', '访问菜单'], ['OR', 'Type', '=', '点击V8按钮'] ], _OrderBy: 'CreateTime', _OrderByType: 'DESC', _PageIndex: 1, _PageSize: 20 }); ``` ### 跨年跨月分页 GetPartitionedTableData 适用于按年分库、按月分集合的海量聊天和日志。接口引擎先校验当前用户与资源权限,再调用有界存储原子;不会把服务器的任意数据库列表开放给客户端。 javascript ``` var result = V8.MongoDb.GetPartitionedTableData({ DatabasePrefix: 'audit_log', TablePrefix: 'event', _Where: [['UserId', '=', V8.CurrentUser.Id]], IndexFields: ['UserId'], PageSize: 30, MaxPartitions: 64, Cursor: V8.Param.Cursor || '' }); // 成功:{ Code:1, Data:{ Rows, HasMore, NextCursor, ScannedPartitions } } ``` 存储命名是 {DatabasePrefix}_{当前租户小写}_{yyyy} 库与 {TablePrefix}_{yyyyMM} 集合,同时兼容无年份旧库、{TablePrefix}_{yyyy_MM} 月集合和 {TablePrefix}_{yyyy} 年集合。年度库中的集合年份必须一致;业务代码固定前缀,不能直接透传用户输入。前缀仅允许小写字母开头的字母、数字、下划线,最多 48 字符。 参数 | 契约 | PageSize | 默认 30,范围 1–100;与普通 GetTableData 的 _PageSize 不同 | MaxPartitions | 单次最多扫描 64 个分区,可设 1–128;全目录最多 200 个租户库、2400 个有效集合 | _Where / _WhereAny | 非空有效条件;_WhereAny 支持 1–4 组条件的 OR,替代 _Where,每一组都要包含权威业务边界 | IndexFields | 可选,最多 3 个互异安全字段;组合这些字段升序与 CreateTime DESC, _id DESC 建索引 | Cursor | 原样回传上次 NextCursor;首次传空,查询条件改变后重置,最大 2048 字符 | CountOnly | 可选,true 返回 { Count };超出分区扫描上限失败,不返回部分总数 | 文档应使用 BSON 日期 CreateTime 和原生 ObjectId _id。普通页按时间、Id 倒序合并,不执行 Count;旧/新分区重复 _id 在页内去重。同一时间多条消息通过 Id 继续,不因跨月、跨年或空月丢失。若 Rows 为空但 HasMore=true,仍要继续使用新游标;目录/扫描超限及存储故障不能作为“没有更早消息”。调用总时限为 20 秒,单集合查询为 10 秒。 游标绑定租户、前缀和查询条件,只代表翻页位置,不能替代权限验证。行内的 _PartitionDb、_PartitionTable 仅用于受信任接口引擎更新实际分区;对外必须显式选择业务字段,不能直接返回内部行对象。CountOnly 计数物理行,迁移时同时保留多个分区中的未读副本可能重复计数,应避免双写或通过权威业务索引计数。 普通 _Where 对原生 _id 的 In / NotIn 支持 1–1000 个合法 ObjectId(数组或逗号分隔字符串);=、!= 和范围比较也转换为原生 ObjectId。非法值会失败,不能忽略过滤条件。可信重放可在 AddFormData 顶层传 OccurredAt 保持原始 CreateTime;未传则使用服务端当前时间,不应信任客户端自报时间。跨月重试仍需稳定业务 Id 与持久分区定位,防止重复落入新集合。 平台聊天的接入及滚动历史规则见消息通知 (https://microi.net/doc/system-engine/message-notification.html#%E8%B7%A8%E6%9C%88%E8%B7%A8%E5%B9%B4%E7%9A%84%E8%81%8A%E5%A4%A9%E5%8E%86%E5%8F%B2)。该接口依赖对应 Core/MongoDB/API 组件与 Managed 运行时版本;未部署时不能用本地测试结论替代线上可用性。 #### 物理审计与索引集合模式 同一 GetPartitionedTableData 原子提供两种明确的高级模式,不能同时启用。它们只解决存储读取,业务权限、维护状态和对外字段仍由接口引擎负责。 模式 | 参数与边界 | PhysicalRows:true | 按物理分区与原生 _id 扫描,保留不同分区的同 Id 副本;PageSize 默认及上限 100,MaxPartitions 默认及上限 8 | 物理扫描范围 | _Where 可省略,只有无条件的全物理扫描才返回 FullPhysicalScope=true;带条件不能证明全租户迁移完成,不支持 _WhereAny 或 CountOnly | 物理扫描游标 | 返回 DirectoryHash、NextCursor、HasMore;后续带回目录哈希和游标。目录或集合身份变化、非法原生 Id 均失败,不能跳过后宣告完成 | CollectionPage:true | 固定当前租户基础库及可信 TableName;IndexFields 为 1–4 个字段,_Where 必须完整等值匹配这些前缀,不支持 _WhereAny | 索引分页 | SortField 是不在索引前缀内的文本字段,按该字段和原生 _id 倒序;PageSize 默认 30、范围 1–100,普通页不计数 | 索引准备与计数 | 维护阶段显式 EnsureIndex:true;普通读取只使用已存在的索引。CountOnly:true 单独计数,准备和计数均不能携带分页游标 | 返回的物理库、集合与内部定位元数据不能直接透传给浏览器。新增迁移应在明确的维护窗口内扫描,正常用户翻历史不能触发全库扫描或临时建大索引。 #### Managed 存储维护准入 适用版本:后端 8.6.4 及以上,并安装消息通知应用 v8.3.25 及以上。全部服务节点必须先完成二进制升级。 V8.Method.ManageManagedStorageMaintenance 与 V8.Method.RequireManagedStorageMaintenance 是可信宿主原子,仅供固定 platform-chat-maintenance 接口使用。前者接受固定 Scope='platform-chat-runtime' 和 Operation=Begin/Drain/Status/End;后者核验排空状态与预期 ExpectedEpoch。普通自建引擎、伪造角色/租户参数、匿名与访问密钥会话均不能获得维护权限。 准入和活动票据由共享存储保存;MongoDB 写入、嵌套调用及正常异步调用继承当前可信上下文。脚本不能通过禁止上下文流动的线程 API 脱离边界,也不能通过普通 MongoDB CRUD 改写宿主控制集合。结束维护需实际排空和最新修订号,不接受客户端自报“已完成”。这些原子不替代业务迁移校验,不应由客户端直接拼参数调用;完整顺序见旧消息定位索引维护 (https://microi.net/doc/system-engine/message-notification.html#%E6%97%A7%E6%B6%88%E6%81%AF%E5%AE%9A%E4%BD%8D%E7%B4%A2%E5%BC%95%E7%9A%84%E7%BB%B4%E6%8A%A4%E7%AA%97%E5%8F%A3)。 ### 查询单条数据 GetFormData javascript ``` V8.MongoDb.GetFormData({ DbName : '', //数据库名称,如:sys_log_2024 TableName: '', //表名名称,如:log_2024_12 Id : '', //数据Id }); ``` 单条查询不存在时,MongoDB 返回 { Code: 1, Data: null };这与 V8.FormEngine.GetFormData 的 Code=2 不同。必须同时检查 Code 与 Data,不能将查询失败、异常、空响应或缺失 Data 误判为不存在并继续新增。消息等幂等写入使用固定业务标识和分区;写入响应未知时只回读同一 _id 并核对载荷摘要,不得换 Id 或月份盲目重发。 GetFormData 和 GetTableData 属于 dynamic 无模式读取。服务端会在 MongoDB 投影阶段过滤 BSON 内部 _t CLR 类型判别字段,避免存量强类型 C# 写入留下的旧类型名称触发反序列化失败。_t 不是 V8 业务契约;业务需要类型标记时应使用自有字段(例如 DocumentType),不得读取、筛选或依赖 _t。强类型 C# MongoDB 模型仍按自身的多态映射处理。 MongoDB 调用使用当前 V8 租户上下文;非主库租户脚本传入其它 OsClient 不能跨租户访问。MongoDB 不参与 V8.DbTrans:批量写方法返回 Code=1 时应视为已提交事实,后续关系库 Hook、SignalR 或外部投递失败不得伪装成“整体未发生”并盲目重试;需要重试时使用稳定业务 Id 实现幂等。 ## V8.Http - 对 RestSharp 的受控封装,支持 GET、POST、PATCH。前后端 V8 使用相同的 PascalCase 对象参数;后端同步方法直接返回,显式 *Async 方法在 Jint 中使用 await,前端浏览器端也需使用 await。 方法 | 主要参数 | 返回值 | V8.Http.Get | GetParam | 响应字符串 | V8.Http.Post | PostParam / PostParamString | 响应字符串 | V8.Http.Patch | PatchParam / PatchParamString | 响应字符串 | GetResponse/PostResponse/PatchResponse | 同上 | 完整响应对象 | GetAsync/PostAsync/PatchAsync | 同步版对应参数 | await 后得到响应字符串 | GetResponseAsync/PostResponseAsync/PatchResponseAsync | 同步版对应参数 | await 后得到完整响应对象 | 完整响应对象包含 Content、Headers、RawBytes、StatusCode、ErrorMessage。Timeout 与兼容参数名 TimeOut 的单位均为秒,默认 600 秒(10 分钟);Headers 与 Header 等效;ParamType 支持 form(默认)、json、xml、binary。GetParam 是 URL 查询参数,GET、POST、PATCH 均可使用。字符串版遇到部分网络错误时可能直接返回错误文本,因此支付、同步、回调等关键集成应使用 *Response 并检查 StatusCode 与 ErrorMessage,不要假定字符串一定是 JSON。 javascript ``` // POST JSON。嵌套对象使用 PostParamString,避免对象转换丢失层级。 var loginText = V8.Http.Post({ Url: 'https://api.example.com/login', PostParamString: JSON.stringify({ User: { Account: 'admin', Pwd: '******' }, OsClient: 'demo' }), ParamType: 'json', Timeout: 10, Headers: { 'X-Trace-Id': V8.Method.NewGuid() } }); var loginResult = JSON.parse(loginText); // GET 查询参数 var listText = V8.Http.Get({ Url: 'https://api.example.com/users', GetParam: { page: 1, size: 20 }, Headers: { Authorization: 'Bearer ' + loginResult.token } }); // PATCH JSON。嵌套对象使用 PatchParamString。 var patchText = V8.Http.Patch({ Url: 'https://api.example.com/users/123', PatchParamString: JSON.stringify({ profile: { name: '新名字' } }), ParamType: 'json', Headers: { Authorization: 'Bearer ' + loginResult.token } }); // PATCH 完整响应 var patchResp = V8.Http.PatchResponse({ Url: 'https://api.example.com/users/123', PatchParam: { Status: 1 }, ParamType: 'json' }); if (patchResp.StatusCode < 200 || patchResp.StatusCode >= 300) { return { Code: 0, Msg: patchResp.ErrorMessage || patchResp.Content }; } // 请求内异步。不要用 setTimeout/Task.Run 把它变成请求外后台任务。 var asyncResp = await V8.Http.GetResponseAsync({ Url: 'https://api.example.com/health', Timeout: 5 }); // XML 请求 var xmlText = V8.Http.Post({ Url: 'https://api.example.com/xml', ParamType: 'xml', PostParamString: '1' }); // 上传文件:键同时作为表单字段名和文件名 var uploadText = V8.Http.Post({ Url: 'https://api.example.com/upload', PostParam: { title: '附件' }, FilesByteBase64: { 'report.pdf': pdfBase64 } }); ``` 接口引擎中必须使用对象参数格式,例如 V8.Http.Get({ Url: url })。不要使用 V8.Http.Get(url);旧的 .NET 同名异步重载可能被 Jint 解析为 Promise。 后端 V8.Http 的严格 SSRF 防护默认关闭,未配置时完全保留历史行为:不限制协议、URL 内嵌凭据、localhost、私网、链路本地或云元数据地址,并继续自动处理重定向。只有在 SaaS 引擎主租户启用 SsrfProtectionEnabled 后,才只允许 HTTP(S),拒绝 URL 内嵌凭据、回环、私网、链路本地、云元数据和其它特殊地址,同时禁止自动跟随 3xx;受控目标可通过 SsrfAllowedHosts 精确放行。白名单匹配主机,不匹配 URL 子串;严格模式下需要跳转时,先用 GetResponse/PostResponse/PatchResponse 检查状态码和 Location,再显式发起下一次请求。租户配置方法见 平台安全总览 (https://microi.net/doc/more/security.html)。 ### 从后端 V8 调用 Microi.AI 后端接口引擎和表单后端事件已注入第一等 V8.AI,无需再通过 HTTP 自请求当前服务器。对象创建时固定绑定当前 V8 执行的 OsClient 与 V8.CurrentUser;脚本传入的用户、租户、Endpoint、ApiKey、NL2SQL 表白名单和服务端授权标记都会被清除: javascript ``` var result = await V8.AI.Chat({ UserChatMsg: String(V8.Param.Question || ''), AiModel: String(V8.Param.AiModel || ''), AiModelId: String(V8.Param.AiModelId || '') }); return result; ``` 可用方法包括 GetLicenseState、UpdateConversationTitle、Chat、ChatStream、RecognizeIntent、NL2SQL、NL2SQLStream、NL2V8 和 NL2V8Stream。UpdateConversationTitle(conversationId, title, source) 只修改当前 V8 租户、当前登录用户自己的对话。匿名接口没有可信用户上下文时返回 Code=1001;NL2SQL 始终重新按当前用户授权,NL2V8/NL2V8Stream 仅平台管理员可调用。 官方 app.microi.ai-engine v6.3.6 使用 Managed platform-ai-runtime 编排标题修改、意图识别、非流式对话、NL2SQL 与非流式 NL2V8。租户个性化逻辑写在 CreateIfMissing platform-ai-custom-hook,该 Hook 只接收来源、阶段和动作,不接收问题、回答、标题、SQL、模型、附件或凭据。浏览器流式请求继续走原生 SSE 路由。 媒体目录和图片持久任务增加以下四个后端原子,由 Managed platform-ai-runtime 按同名 Action 编排。使用前需同时部署包含这些方法的服务端和对应 AI助手应用包;版本号或应用安装成功不能代替实际能力检查。 后端方法 | 用途与返回边界 | await V8.AI.GetMediaModels() | 返回当前登录租户可用的脱敏媒体目录,不接受客户端指定租户或用户,不返回供应商密钥。 | await V8.AI.GenerateMiniMaxImage(param) | 以稳定 RequestId 入队图片任务;Code=2 + Data.TaskId 表示已受理,尚未生成成功。 | await V8.AI.GetMiniMaxImageTask(taskId) | 查询当前租户、当前用户拥有的原任务;不能通过传入别人的 TaskId 越权读取。 | await V8.AI.RecoverMiniMaxImageTask(taskId) | 对已有可恢复结果继续转存;复用原任务与供应商回执,不再次生成图片。 | 外部客户端通过 POST /apiengine/platform-ai-runtime 提交 Action 和白名单业务参数;后端 V8 可直接调用上述方法。租户与用户取自可信 V8 上下文,匿名身份拒绝,伪造的 OsClient、CurrentUser、ApiKey、Endpoint 不会改变实际身份或供应商凭据。模型启用、用户权限、额度和许可仍由既有图片任务运行时检查。 javascript ``` // RequestId 应由调用方保存并复用,不能在每次重试时重新生成。 var result = await V8.ApiEngine.Run('platform-ai-runtime', { Action: 'GenerateMiniMaxImage', RequestId: String(V8.Param.RequestId || ''), Prompt: String(V8.Param.Prompt || ''), Model: String(V8.Param.Model || ''), Count: 1 }); return result; ``` 客户端保存原 RequestId、TaskId 以及所属服务器、租户和账号,异步查询原任务直到明确终态。只有实际图片结果及持久地址回读通过才算交付。超时、断连或空响应属于结果未知,应先复查原任务,不能换 RequestId 盲目重发;缺少可恢复回执时,RecoverMiniMaxImageTask 也不能把未知结果伪装成成功。旧原生 HTTP 路由保留兼容,应用包更新不会替代服务端二进制更新。 后端流式回调会占用本次 Jint 请求,只适合确实需要由同一脚本消费增量的场景;面向页面的打字机效果仍优先让浏览器使用前端 V8.AI.ChatStream。MCP 直接使用专用 microi_chat 获取最终结果,不再绕到 microi_run_engine 包装接口。不要把 Token、完整问题或回答写入日志。完整代码与授权矩阵见 AI 引擎与 Microi.AI 中转站 (https://microi.net/doc/system-engine/ai-engine.html)。 ## V8.Tcp 后端接口引擎和后端表单事件可通过 V8.Tcp 连接 TCP 服务端并发送原始字节,适用于 ESC/POS 网络小票机、串口服务器、PLC 等设备。它是一次性客户端,不提供 TCP 监听、持久 Socket 或 TLS 隧道。 方法 | 说明 | 返回值 | V8.Tcp.Send | 连接、发送、关闭 | DosResult | await V8.Tcp.SendAsync | 当前请求内异步连接、发送、关闭 | Promise | V8.Tcp.SendAndReceive | 发送后读取有界响应,再关闭 | DosResult | await V8.Tcp.SendAndReceiveAsync | 当前请求内异步发送并读取有界响应 | Promise | 所有方法使用同一个对象参数: 参数 | 必填 | 说明 | Host | 是 | 主机名或 IP,不能传 URL | Port | 是 | 1-65535;RAW/JetDirect 打印常用 9100 | Bytes / RawBytes / ByteArray | 四选一 | 0-255 的整数数组 | ByteBase64 / BytesBase64 / Base64 | 四选一 | Base64 编码的原始字节 | Hex | 四选一 | 十六进制;可包含空格、0x、连字符、冒号或逗号 | Text | 四选一 | 按 Encoding 编码后发送 | Encoding | 否 | 默认 utf-8;支持 ascii、gb18030、gbk/gb2312、UTF-16/UTF-32 大小端 | ConnectTimeout | 否 | 连接超时秒数,默认 10,范围 1-120 | SendTimeout | 否 | 发送超时秒数,默认 10,范围 1-120 | ReceiveTimeout | 否 | 收发方法的总接收超时秒数,默认 3,范围 1-120 | MaxReceiveBytes | 否 | 最大响应字节数,默认 65536,最大 1048576 | NoDelay | 否 | 是否禁用 Nagle,默认 true | javascript ``` // ESC/POS 网络小票机。实际地址必须从后端可信配置或受权限保护的设备表读取。 var printResult = V8.Tcp.Send({ Host: '192.168.1.88', Port: 9100, // 初始化、ASCII 小票正文、换行、切纸 Bytes: [ 27, 64, 77, 105, 99, 114, 111, 105, 10, 84, 111, 116, 97, 108, 58, 32, 49, 50, 46, 48, 48, 10, 10, 10, 29, 86, 0 ], ConnectTimeout: 5, SendTimeout: 5 }); if (printResult.Code !== 1) return printResult; return { Code: 1, Data: { BytesSent: printResult.Data.BytesSent, RemoteEndPoint: printResult.Data.RemoteEndPoint } }; ``` 如果打印机需要 GBK/GB18030 文本,可直接发送文本;若同一帧还包含 ESC/POS 控制命令,应在调用前把完整帧编码为一个 Bytes、ByteBase64 或 Hex 载荷: javascript ``` var textResult = await V8.Tcp.SendAsync({ Host: '192.168.1.88', Port: 9100, Text: '吾码小票\n合计:12.00\n\n', Encoding: 'gb18030' }); if (textResult.Code !== 1) return textResult; ``` 需要读取设备响应时: javascript ``` var deviceResult = V8.Tcp.SendAndReceive({ Host: '192.168.1.20', Port: 4001, Hex: '01 03 00 00 00 02 C4 0B', ReceiveTimeout: 3, MaxReceiveBytes: 4096 }); if (deviceResult.Code !== 1) return deviceResult; // Data.RawBytes、Data.ByteBase64、Data.Hex // ReceiveEndReason: RemoteClosed / Timeout / MaxReceiveBytes return deviceResult; ``` Send 成功时 Data 包含 BytesSent 和 RemoteEndPoint。收发方法还返回 BytesReceived、RawBytes、ByteBase64、Hex、ReceiveEndReason 和 Truncated。发送载荷最大 4 MiB。收到部分数据后达到接收超时,会以 Code=1 返回已收到的数据并标记 ReceiveEndReason='Timeout';一个字节也未收到则返回失败。 安全与验收边界: - Host、Port 必须来自 SaaS 可信配置或受权限保护的设备表,并在业务代码中做精确白名单校验;禁止直接透传 V8.Param。匿名接口尤其不能开放任意 TCP 目标。 - TCP 不经过 V8.Http 的 SSRF 防护。生产网络应再用容器网络策略或防火墙只放行打印机/设备网段与端口,且不要记录原始票据、设备口令或响应秘密。 - 每次调用都会新建并关闭连接。Code=1 只证明字节已写入 TCP 连接,不证明设备已执行、更不证明小票已经出纸;正式验收仍需设备状态或实物小票证据。 - 写入结果不确定时不要盲目自动重试:打印不是天然幂等,重试可能产生重复小票。需要可靠打印时使用业务幂等号、设备回执以及 Job/MQ/outbox 编排。 - 在 Docker 中,Host 是从后端容器视角解析;localhost 指容器自身。部署前必须验证容器到设备 IP/端口的实际路由。 ### 设备原始 TCP 入站(API 容器内) V8.Tcp 是出站客户端;它不负责接收设备连接。对于只会配置“服务器 IP/域名 + 端口”、持续发送二进制帧并等待同连接应答的设备,Microi.net.Api 可启用独立的 TCP 入站监听。此监听与现有 HTTP 接口引擎处于同一个进程、同一个 API Docker 容器,但必须使用不同端口:设备填 主机:TCP端口,不填 http://、https:// 或 /apiengine/...。HTTP 端口和现有其它厂商的 HTTP/MQTT 对接保持不变。 无完整规则时默认不监听。先为主租户安装带 sys_osclients.TcpIngressEnable、TcpIngressPort 与 TcpIngressRulesJson 字段的 SaaS 引擎资源包,再由有权限的主租户管理员配置。TcpIngressEnable 为空或 1 表示允许按完整规则监听,0 明确关闭;空规则不监听,坏规则仅使 TCP 进入 Faulted。TcpIngressPort 是容器内监听端口,空值回退 1885;启停和端口均不包含在规则 JSON 中,也不等于 Docker/CLB 对外端口。运行中的 API 每约 30 秒独立回查主库开关:设 0 后下一轮关闭 TCP 监听与会话,HTTP 继续运行;从 0 改回空值/1,以及修改端口、规则、引擎 Key、CIDR,不会热重绑,必须手工重启现有 API 容器。以下是协议为 AA333380 上行、BB666680 下行、CRC16/XMODEM 的窄来源示例;osClient、apiEngineKey 和来源网段都应换成经过核实的值: json ``` { "osClient": "device-tenant", "apiEngineKey": "device-uplink", "allowedCidrs": ["198.51.100.10/32"], "requestHeaderHex": "AA333380", "responseHeaderHex": "BB666680", "tailHex": "FCFF", "lengthOffset": 4, "lengthAdjustment": 4, "correlationOffset": 6, "correlationLength": 10, "maxFrameBytes": 4096, "maxConnections": 100, "idleTimeoutSeconds": 90, "maxQueuedFramesPerConnection": 8, "executionTimeoutSeconds": 5 } ``` 来源 IP 确实无法事先确定时,可由主租户管理员显式承担风险:在上面的完整规则中增加 "allowAnySource": true,并把 allowedCidrs 改为唯一一项 ["0.0.0.0/0"]。两项必须同时存在;缺少布尔开关、混入其它网段、使用 IPv6 或把 /0 写进普通窄来源规则,都会被拒绝。此设置允许任意 IPv4 客户端尝试连接,不是默认推荐,更不是设备鉴权。在接口引擎中按权威设备资料校验设备身份、命令与业务权限,限制重放/重复写入并监控异常连接;若设备协议只有可伪造的设备号和 CRC、没有独立密钥或签名,仍存在冒充上报风险,应记录并尽早收窄网络入口。 监听器按 U16 大端长度字段和帧尾重组拆包/粘包,校验 CRC16/XMODEM,然后把一帧十六进制作为 frameHex 通过固定 _InvokeType=Server 内部调用固定接口引擎。接口引擎负责业务解析、写表、幂等与生成 Data.ResponseFrameHex;监听器校验应答帧结构、CRC 和设备号/命令关联后,才在原 TCP 连接回写原始字节。业务 Code=0 若携带协议定义的合法失败应答,仍可回写;无合法应答则关闭连接,不伪造成功。设备输入不能覆盖租户、引擎 Key、端口或来源策略。启用 TCP 时目标接口引擎必须 IsEnable=1、StopHttp=1、AllowAnonymous=0,不满足则 TCP 局部失败关闭、不监听,而 HTTP API 继续运行;否则旧匿名 HTTP 入口可绕过 TCP 来源限制。CRC 和匿名标志都不是设备身份认证。 上线应分四阶段:先部署并启动新 API 镜像,确认目标租户 sys_config.ServerVersion >= 7.6.16.0 且旧 MqttEnable 空值已迁为 0;新 TCP 包尚未安装时规则为空、不监听,HTTP 继续。再通过商城安装/更新 SaaS 字段并发布配套前端,明确设主租户 TcpIngressEnable=0、规则暂留空,让旧 HTTP 和其它设备通道照常运行;物理迁移不会自动更新旧租户的 UI 元数据,不能先发 NullValue=1 的包/界面。核实目标接口不再有合法 HTTP 调用后,仅对该接口关闭 HTTP(StopHttp=1)与匿名访问(AllowAnonymous=0),并验证旧 URL 不可匿名直调;最后填写完整 TCP 规则、配置端口映射和防火墙,将 TcpIngressEnable 设为 1,重启同一 API 容器并做心跳及业务验收。若目标接口仍被旧 HTTP 设备使用,须先迁移或拆出受控兼容入口,不得直接切换。其它厂商独立的 HTTP/MQTT 接口及端口保持原样,但 API 容器重启期间会短暂中断,需分别回归。 在同一 API Compose service 增加如 "19011:1885/tcp" 的端口映射(对外宿主机 19011 → 容器内默认 1885),保留原有 HTTP 1052 映射;更新包含此能力的 API 镜像并重启。新执行的官方在线一键安装器会在动态 61600+ 宿主端口段末尾追加 MQTT/TCP 映射,离线安装器默认使用 7010/7011;两者都映射至 API 容器内 1883/1885 并尝试开放宿主机防火墙,实际服务是否监听仍受 SaaS 配置控制。已有 Compose 不会自动升级映射;若更改主租户 TcpIngressPort,须同步更改容器目标端口。当前入站监听仅支持 IPv4;优先在 allowedCidrs 和云安全组/防火墙同时限制已核实的来源网段,不能依赖 HTTP 安全白名单。无法限源时仅可采用上文显式全网例外,确认业务身份校验与剩余风险后再开放对应 TCP 端口。经 CLB 时先验证后端实际看到的源 IP,再配置相应来源规则;CLB TCP 监听不得启用当前不支持的 Proxy Protocol v2。该协议的 CRC 只校验传输完整性,不能认证设备身份。 验收要分层记录:TCP 内部状态须为 Listening 且日志明确显示端口已监听(目前没有对外的 TCP 状态 HTTP 端点);从允许网段先探测端口,再做拆包、粘包、连续心跳回环测试,并逐帧核对同连接 ACK 的字节、CRC 和设备号关联;最后由实体设备验证心跳上行、同连接应答和业务快照。配置、引擎安全位或端口冲突失败仅使 TCP 进入 Faulted 并记录失败阶段,HTTP API 保持运行;不能让 HTTP 健康或单纯端口连通误报为 TCP 已回 ACK。只有端口可连或回环通过,也不能宣称设备网络图标已点亮或生产数据已入库。 ## V8.IoT:通过设备现有 TCP 连接下行 V8.IoT.Send 把完整协议帧交给持有设备连接的 API 节点发送,适用于设备主动连接平台、 无法从服务器另开连接访问设备的场景。它与一次性客户端 V8.Tcp.Send 的连接方向不同。 需要配套升级 Core、V8Engine 和 API;只替换 V8Engine DLL 不足以启用此能力。 ### 与 MQTT、TCP 的选择 接入方式 | 使用能力 | 协议与业务的归属 | 设备连接平台 MQTT Broker | 既有 MQTT 引擎 | Topic、认证、ACL 和下行按 MQTT 文档配置 | 平台主动连接设备开放的 TCP 端口 | V8.Tcp | 一次连接内发送/收取原始字节 | 设备主动连接平台原始 TCP 监听 | V8.IoT.Send | 沿已授权的原连接下行,跨 API 节点路由 | 厂家提供 HTTP 云接口 | V8.Http | 接口引擎调用厂家 API | V8.IoT 不包含任何厂商名、制冷/制热操作码、设备业务表或固定设备编号。 原子只处理受权连接、字节帧、去重、有效期与传输状态;厂商适配、命令序列号、 设备权限、回执解析及业务状态机由接口引擎负责。当前公开的 IoT 传输适配器是原始 TCP, 不会自动把 TCP 报文转换成 MQTT 或 HTTP。 ### 可配置的厂商协议 原规则未添加 framing 时,保持历史格式:4 字节帧头、2 字节帧尾、 U16 大端长度、CRC16/XMODEM 大端校验。新协议在既有规则内显式添加 framing: json ``` { "osClient": "", "apiEngineKey": "device-uplink", "allowedCidrs": ["127.0.0.1/32"], "requestHeaderHex": "A1", "responseHeaderHex": "B2", "tailHex": "0D0A", "lengthOffset": 1, "lengthAdjustment": 0, "correlationOffset": 3, "correlationLength": 2, "framing": { "mode": "LengthPrefix", "lengthBytes": 2, "lengthByteOrder": "LittleEndian", "checksum": "Crc16Modbus", "checksumByteOrder": "LittleEndian", "checksumStartOffset": 0 }, "downlink": { "enabled": true, "deviceOffset": 3, "deviceLength": 2, "sendApiEngineKeys": ["device-control-dispatch"] } } ``` 这是另一套匿名示例协议,不能直接覆盖已有设备配置。示例设备地址 0102、 载荷 55 的上报帧为 A10A0001025511F40D0A;CRC 应由接入方按实际协议计算并核验。 所有偏移均从帧首按字节计数,地址可位于长度字段前或后,但不能与帧头、长度或校验区重叠。 配置 | 语义与范围 | framing.mode | LengthPrefix 长度字段、FixedLength 固定帧长、Delimiter 结束符拆帧 | requestHeaderHex / responseHeaderHex | 显式 framing 下各为 0–32 字节;无帧头时明确写空字符串 | tailHex | 0–16 字节;Delimiter 模式必须非空,载荷不得包含未转义的结束符 | lengthOffset / framing.lengthBytes | 长度字段偏移 0–128;宽度 1、2、4 字节;只在 LengthPrefix 模式读取 | framing.lengthByteOrder | BigEndian(默认)或 LittleEndian | lengthAdjustment | 实际总长度 = 长度字段值 + 此值;显式 framing 时允许 -65535–65535 | framing.fixedLength | FixedLength 模式必填正整数,且不超过 maxFrameBytes | framing.checksum | Crc16Xmodem(默认)、Crc16Modbus、None | framing.checksumByteOrder | 校验字段字节序,默认 BigEndian;校验字段紧邻帧尾之前 | framing.checksumStartOffset | 校验计算起点 0–128,终点为校验字段之前,不包含帧尾 | downlink.downlinkDeviceOffset | 下行地址偏移,默认等于上报的 deviceOffset | 上报、普通应答和主动下行共用同一套结构规则,头部可按方向不同;普通应答继续严格校验 correlationOffset/correlationLength,主动下行独立验证目标设备,不能放宽 ACK 关联来发送控制帧。 完整帧最大 65535 字节,仍受连接缓冲与待发队列总预算约束。平台不按未验证的长度分配内存。 一个 API 进程当前只加载主租户的一套监听规则;这不是单端口自动识别任意厂家协议。 不同协议可用相应的受管接入实例/网关,业务接口共用本原子;单实例多监听、TLS、转义解码、 动态地址格式和未列出的校验算法尚未提供。遇到不满足描述的协议应补通用适配器,不能写死厂家分支。 ### 配置与连接授权 默认不启用。在主租户既有 sys_osclients.TcpIngressRulesJson 中添加以下 downlink 节点, 其它帧配置、端口和正常 ACK 关联规则保持原值。修改后重启 API;不增加环境变量或新进程。 json ``` { "downlink": { "enabled": true, "deviceOffset": 6, "deviceLength": 8, "sendApiEngineKeys": ["device-control-dispatch"], "maxPendingCommands": 32, "maxExpireSeconds": 120, "deduplicateSeconds": 86400, "leaseSeconds": 30, "pollIntervalMilliseconds": 500 } } ``` 上例是添加到原规则中的节点片段,不能用它替换完整入站配置。 设备区域必须按实际协议填写;deviceOffset 是从帧首开始的零基字节偏移。 控制接口引擎必须禁止匿名,并在生成报文前按当前用户校验设备行权限和操作权限。 调用 Key 从后端执行上下文读取,不能由 V8.Param 指定;该白名单授予的是可信业务入口。 内部上报接口在确认设备身份与业务绑定后,每次成功处理都需返回 Data.IoTDeviceId,内容为帧内设备地址的规范十六进制,同时继续返回正常的 Data.ResponseFrameHex。宿主校验两者地址一致,且 ACK 写入成功后才登记连接。 协议明确无需平台再次应答的消息,可使用下文的显式 None 模式,在校验处理完成后登记/复核连接。 未绑定设备可继续返回心跳 ACK,但不返回 IoTDeviceId,因而不能接收下行。 已登记连接后续失去授权则关闭;同一物理连接不能切换设备身份。 CRC 和设备号本身不能认证设备,实际凭据或来源限制仍按设备协议处理。 配置 | 默认值 / 范围 | enabled | 必须显式为 true;缺失整个节点或设为 false 不启用 | deviceOffset / deviceLength | 必填;上报偏移 0–128,地址长 1–32 字节,不能覆盖帧头/长度字段;下行偏移可单独指定 | sendApiEngineKeys | 必填;1–32 个允许调用的接口引擎 Key | maxPendingCommands | 32;1–128,单设备待发载荷预算最多 512 KiB | maxExpireSeconds | 120;1–600 秒 | deduplicateSeconds | 86400;600–604800 秒;业务去重仍须持久记录 | leaseSeconds | 30;10–120 秒;连接持有者续租,旧连接不能删除新连接租约 | pollIntervalMilliseconds | 500;100–5000,必须小于租约时长的三分之一 | ### 调用与结果 javascript ``` // 已持久记录指令并提交后,由已授权的派发接口执行。 // 这些变量由当前业务设备记录和厂家协议生成,不能透传任意前端报文。 return V8.IoT.Send({ DeviceId: deviceAddressHex, CommandId: committedCommandId, FrameHex: fullProtocolFrameHex, ExpireSeconds: 30 }); ``` DeviceId 是协议设备地址,不是业务表记录 Id;接口引擎负责映射。 CommandId 必须稳定,支持 1–128 位字母、数字、点、下划线和横线。 FrameHex 必须是无分隔符的完整十六进制帧,经过配置的帧头、长度、尾部、 所选校验算法和设备地址校验。其它参数,包括 OsClient、Host、Port 均拒绝。 当前租户登录用户及可信后台任务身份可使用;匿名上下文、表单事件和未授权 Key 拒绝。 ### 动作、参数、报警和定时配置共用原子 Send 不限制业务命令码、子操作码和载荷长度;开关、温度、提醒、阀门、定时、模式或其它 功能都可由接口引擎生成符合配置的完整帧。新增同协议业务动作不需要再次修改 C#。 平台传输成功、设备命令确认与设备达到目标状态必须分别记录。 控制类型 | 接口引擎/命令目录需要配置 | 开机、关机、模式、继电器 | 业务动作 Key、协议命令/子码、允许状态、用户设备与动作权限 | 温度、流量、时长、阈值 | 字段顺序、U8/U16/U32 等宽度、字节序、单位/倍率、机型范围、枚举/步长 | 报警上报与恢复 | 上行命令、位图/枚举、产生/恢复事件、去重与通知;不能把上报码直接当作控制码 | 报警提示开关、消音、复位 | 厂家明确提供的下行命令、字段、权限与回执;没有协议定义时保持不可用 | 定时、分段参数、数组 | 重复组数、时间/时区、启用类型、流水号所在位置、总帧长 | 设备执行回执 | 期望回执命令、子码、流水号偏移、成功/失败/未知值、是否需要平台再应答 | 命令目录属于租户业务配置/接口引擎,不按开机、设水温或清警等具体业务动作增加厂家专属原子。 前端仅提交受限的动作和参数,后端重新读取受权设备与机型配置, 禁止把前端的命令码、成功码、目标设备、完整十六进制或可执行表达式直接透传。 平台 CommandId 与设备协议流水号不是同一概念。业务 outbox 需保存二者映射, 在同一设备、同一命令及子码范围内关联回执;重复派发复用同一编号及报文。 不同命令的回执可能使用不同命令码、成功值或字段顺序,不能固定为单一位置或一律“0=成功”。 未知结果保留未知;没有回执的命令只能确认传输,不能伪造设备已执行。 参数范围、固件语义或帧长度存在歧义时,默认拒绝生成/派发该动作,待厂家确认后更新可信目录。 读回快照有延迟时,不用列表字段的旧值直接证明温度、开关或报警已改变。 ### 控制回执不需要再次应答时 部分协议中的控制回执本身就是设备对平台命令的确认,不需要平台再发一个 ACK。 在厂家确认这种行为后,可在既有完整 TcpIngressRulesJson 的根节点增加: json ``` { "allowNoResponse": true } ``` 这是节点片段;不能替换完整规则。默认 false,必须是布尔值,修改后重启 API。 上报接口先验证设备绑定、回执命令、子码、流水号和参数长度,并持久记录结果,再返回: javascript ``` return { Code: 1, Data: { IoTDeviceId: authorizedDeviceAddressHex, ResponseMode: 'None' } }; ``` 此返回表示“这条上行消息已按协议处理,不需要写回字节”。它不代表设备执行成功, 不能代替业务记录中的成功/失败/未知状态。ResponseMode:'None' 与 ResponseFrameHex 属性互斥。 普通有应答消息继续返回 Data.ResponseFrameHex,可省略模式或显式设置 ResponseMode:'Reply'。 只有配置允许且结果为数值 Code=1、模式精确为 None、物理帧有效时,宿主才保持原连接而不写任何字节。 空结果、缺失应答、Code=0 的 None、未知模式和损坏帧仍关闭连接;普通失败 ACK 的既有行为保持不变。 需要心跳应答的协议仍必须逐次回心跳 ACK,不能把所有上报都改成 None。 每条成功的免应答消息同样复核 IoTDeviceId;授权撤销或设备身份切换时关闭下行连接。 状态 | 含义与后续动作 | Queued | 已接受并在共享 Redis 中排队,尚未证明写入设备连接 | Sending | 已出队开始发送;进程中断可能停留于此,结果未知,禁止自动重发 | Written | 字节已写入 TCP;仍需设备协议回执确认执行结果 | Unknown | 写入或保存结果失败,可能已有物理副作用,禁止自动重发 | Expired | 排队指令已过期,不再下发 | Offline / QueueFull / Conflict | Code=0,分别表示无有效连接、队列已满、相同编号对应不同内容或调用身份 | Code=1 表示本次命令或同编号的既有命令已被接收,不等于设备执行成功。 相同编号在去重期内重复提交返回既有状态和 Duplicate=true;不会延长过期时间, 不会因设备重连把旧队列转发到新连接。去重期结束后编号可能再次被接受,业务需保留长期唯一约束。 ### 事务、多节点与验收边界 设备动作和 Redis 投递无法跟随数据库事务回滚。控制接口应只写已授权的指令/outbox, 事务提交后由派发任务读取;派发任务使用同一 CommandId 调用本原子,再保存投递状态。 不能在创建业务记录的同一事务里发送设备动作并期待数据库回滚撤销动作。 设备执行结果由上报接口按厂家流水号更新,不由此原子推断。 连接所有权、排队和去重使用既有租户 Redis,队列绑定连接代次,Socket 只留在持有节点。 原 ACK 与主动指令共用串行写通道;Redis 原子抢占后不自动重投,优先避免重复物理副作用。 Redis 故障会拒绝/关闭对应下行连接;共享 Redis 的持久化和故障转移能力由实际部署决定, 不能把本能力描述为跨断电的绝对零丢失或设备“恰好执行一次”。 若重连发生在所有权检查与实际写入之间,仍存在短暂竞态窗口;严格设备侧去重需协议支持。 上线前分别验证旧心跳回归、跨节点路由、队列过期、重连、重复提交、写后状态保存失败, 再验证真实设备回执与动作。离线替身与回环 Socket 测试不能代替真实 Redis/多进程及硬件验收。 ## V8.Header、V8.Param - 目前两者均只支持在接口引擎中使用,用于获取客户端http post请求接口引擎地址发送的报文和Request Payload参数。 ## 微信支付最小原子 V8.WeChat V8.WeChat 是供后端接口引擎调用的微信支付协议原子,不是订单、退款或通知业务 SDK。商户号、订单状态、金额、幂等、事务、日志和通知仍应在接口引擎中编排;只有 HTTP 路由、可信验签、租户恢复、密钥隔离等协议边界才放在最小 C# 网关中。 函数 | 说明 | AesGcmDecrypt(associatedData, nonce, ciphertext, apiV3Key) | 使用 AES-256-GCM 解密微信支付 API v3 回调或平台证书资源,并验证密文尾部认证标签 | GetWeChatSign(privateKeyPem, paramList) | 按参数顺序以换行符拼接待签名串,使用商户 RSA 私钥生成 SHA256-RSA2048 Base64 签名 | GetWeChatAuthorization(mchid, serialNo, privateKeyPem, wxApiAddress, body) | 为 POST 请求生成 WECHATPAY2-SHA256-RSA2048 Authorization 值;wxApiAddress 传参与签名一致的请求路径和 Query | ### AesGcmDecrypt 参数编码 微信支付回调中的四个参数不是全部 Base64。必须保持下列编码边界: 参数 | 正确处理 | apiV3Key | 商户 APIv3 密钥原文,按 UTF-8 转为 32 字节;不要先做 Base64 解码 | nonce | resource.nonce 原文,按 UTF-8 转为 12 字节随机 IV;不是 Base64 | associatedData | 原样使用 resource.associated_data 并按 UTF-8 编码;字段为空时按空字节串处理 | ciphertext | 仅此参数执行 Base64 解码;解码后的完整字节串末尾包含 16 字节 GCM 认证标签,标签必须和密文一起交给解密器 | 以下是接口引擎中的核心调用片段。WeChatPay.ApiV3Key 是示例系统设置 Key,应与当前租户实际配置保持一致: javascript ``` var resource = V8.Param && V8.Param.resource; if (!resource || resource.algorithm != 'AEAD_AES_256_GCM' || !resource.nonce || !resource.ciphertext) { return { Code: 0, Msg: '微信支付回调资源格式错误。' }; } var privateSettings = (V8.SysConfig && V8.SysConfig.ServerPrivateSettings) || {}; var apiV3Key = privateSettings['WeChatPay.ApiV3Key']; if (!apiV3Key) { return { Code: 0, Msg: '当前租户未配置微信支付 APIv3 密钥。' }; } var decryptedText; try { decryptedText = V8.WeChat.AesGcmDecrypt( resource.associated_data || '', resource.nonce, resource.ciphertext, apiV3Key ); } catch (ex) { // 不回传异常、密钥、密文或解密内容;只记录不含秘密的追踪标识。 var traceId = V8.Method.NewUlid(); console.error('微信支付回调资源解密失败,TraceId=' + traceId); return { Code: 0, Msg: '微信支付回调资源解密失败。', DataAppend: { TraceId: traceId } }; } var paymentNotice = JSON.parse(decryptedText); // 从这里开始仍属于接口引擎业务:从数据库重读订单,校验 appid/mchid/out_trade_no/金额, // 以 transaction_id 或通知 Id 做唯一幂等,按状态机更新,并通过 outbox 编排后续副作用。 ``` AesGcmDecrypt 会校验 APIv3 密钥和 nonce 字节长度,并认证密文尾部标签;密文或标签被篡改时会抛出异常。不要把 nonce 改成 Convert.FromBase64String(nonce),也不要为了兼容错误调用而同时接受两种 nonce 编码。该函数只完成资源解密与 GCM 完整性认证,不等于微信支付 HTTP 回调签名验证;正式回调必须先按 Wechatpay-Timestamp、Wechatpay-Nonce、请求原文和 Wechatpay-Signature 完成平台签名验证,再进入业务事务。参见微信支付官方“如何解密回调报文和平台证书” (https://pay.wechatpay.cn/doc/v3/partner/4012082320)。 APIv3 密钥、商户私钥不得写入 V8 源码、接口参数、日志或响应。后端接口引擎从当前租户 V8.SysConfig.ServerPrivateSettings 读取私密设置;浏览器和匿名公开配置不会获得该节点。 ## 加密类 V8.EncryptHelper - Dos.Common加密帮助类 javascript ``` var pwd = V8.EncryptHelper.DESEncode('123456');//DES加密 var pwd = V8.EncryptHelper.DESDecode('JdZe5gWKjZo=');//DES解密 var pwd = V8.EncryptHelper.SHA1('123456'); var pwd = V8.EncryptHelper.SHA256('123456'); var pwd = V8.EncryptHelper.SHA512('123456'); var digest = V8.EncryptHelper.MD5Encrypt('123456');//兼容用不可逆摘要,不是加密 var pwd = V8.EncryptHelper.Sha256Hex('123456'); ``` MD5、SHA1、SHA256 等摘要不能用于新密码存储;登录密码必须使用平台认证流程和带盐的专用密码哈希。 DESEncode/DESDecode 可用于业务明确要求“加密保存且授权后显示明文”的兼容字段,但只能在后端接口引擎/表单事件中处理:列表默认掩码,显示明文使用独立受权动作并审计,响应禁止缓存,不向匿名、访问密钥会话或普通 FormEngine 暴露批量解密。DES 是现有兼容格式;新高价值秘密优先使用带版本的现代认证加密和集中密钥管理。完整分级见平台安全与兼容基线 (https://microi.net/doc/more/security.html)。 接口引擎自身需要持久保存可逆凭据时,不要从已脱敏的 V8.OsClientModel 读取 AuthSecret、DbConn,也不要自行拼接 AES 密钥;使用上文的 V8.Method.ProtectApiEngineSecret/UnprotectApiEngineSecret,由可信宿主完成租户与接口引擎作用域绑定。 ## 强身份验证票据 V8.Method.ConsumeIdentityVerificationTicket 登录后的前端 V8 可通过 V8.Identity.Verify 完成 Passkey、Authenticator TOTP 或严格人脸验证,取得两分钟有效的一次性票据。后端接口引擎必须从数据库重读业务事实、重新计算 ActionHash,再原子消费: javascript ``` var actionHash = V8.EncryptHelper.Sha256Hex(canonicalBusinessCommand); var verified = V8.Method.ConsumeIdentityVerificationTicket({ Ticket: V8.Param.IdentityVerificationTicket, Purpose: 'ApproveSensitiveOperation', ActionHash: actionHash }); if (verified.Code !== 1) return verified; ``` 票据绑定当前 DiyToken 用户、OsClient、用途和操作摘要,只能成功消费一次;访问密钥会话不能使用。它不代替菜单/表权限、业务状态机、事务和幂等。完整配置与前端示例见 Passkey、Authenticator、设备生物识别与严格人脸验证 (https://microi.net/doc/more/identity-verification.html)。 ## V8.TranslateEngine V8.TranslateEngine 绑定当前 V8 执行租户的 SaaS 翻译配置。兼容入口 Translate(text, to, from?) 的 Data 是单个译文字符串;新代码需要批量、HTML、自动检测、候选译文或完整返回信息时使用 TranslateText: 底层实现来自开源 NuGet/类库 Microi.Translate;平台插件只负责把当前租户上下文安全注入 V8,不在闭源 Microi.net 内复制翻译业务逻辑。 javascript ``` var result = V8.TranslateEngine.TranslateText({ SourceTexts: ['你好', '世界'], // 与 SourceText 二选一 FromLang: 'auto', Lang: 'en', Format: 'text', // text | html Alternatives: 2 }); if (result.Code !== 1) return result; var detected = V8.TranslateEngine.Detect({ SourceText: 'Bonjour' }); var languages = V8.TranslateEngine.GetLanguages(); var health = V8.TranslateEngine.Health(); ``` 完整业务方法包括 Translate/TranslateText、Detect、GetLanguages、TranslateFile、Suggest、Health,以及不调用供应商的 GetLang/GetLangData/GetLangCode 词条方法。文件翻译支持 TXT、HTML、ODT、ODP、DOCX、PPTX、XLSX、EPUB、PDF,返回文件名、ContentType、长度和 Base64;接口引擎返回文件时需开启“响应文件”。 脚本传入的 OsClient 会被当前 V8TenantContext 覆盖,且参数中不存在 Endpoint、API Key、Authorization 或 Header。普通租户不会隐式借用主租户翻译凭据。LibreTranslate 的前端设置、API Key 管理、指标和 Web UI 属于运维面,不开放给 V8。完整返回结构、MCP 工具和部署方式见翻译引擎 (https://microi.net/doc/system-engine/translate-engine.html)。 ## V8.OCR V8.OCR 是图片/PDF 文字识别的统一租户网关。Microi API 负责鉴权、租户隔离、文件校验、大小/超时/页数限制和统一结果;OCR 模型由独立的 PaddleX 服务承载,避免在每个 .NET API 节点重复加载模型。 本节保留后端 V8 方法签名和返回协议;方案选型、部署、SaaS 配置、HTTP/MCP 调用、生产模式与故障排查见 OCR 识别引擎 (https://microi.net/doc/system-engine/ocr-engine.html)。 javascript ``` var result = await V8.OCR.Recognize({ FileByteBase64: V8.FilesByteBase64.invoice, FileName: 'invoice.png', UseDocOrientationClassify: true, UseDocUnwarping: true, UseTextlineOrientation: true, TextRecScoreThresh: 0.5, ReturnWordBox: false }); if (result.Code !== 1) { return result; } return { Code: 1, Data: { Text: result.Data.Text, AverageConfidence: result.Data.AverageConfidence, Pages: result.Data.Pages, TraceId: result.Data.TraceId } }; ``` 支持 PDF、PNG、JPEG、BMP、GIF、TIFF、WebP。平台会同时校验扩展名、Base64 和文件魔数;成功结果统一包含 Provider、TraceId、FileType、Text、AverageConfidence、PageCount、ElapsedMilliseconds 和 Pages。每页包含文本区域、置信度和多边形坐标。 也可由已登录客户端调用官方 Managed 接口 POST /apiengine/platform-ocr-recognize,请求体与上例参数相同。请求体中的 OsClient 会被忽略,以验证后的 Token 租户为准。接口不接受 endpoint、API key、任意 Header、代理地址或服务端文件路径;个性化逻辑写入 platform-runtime-custom-hook,文件和识别原文不会传给 Hook。 ### SaaS 引擎配置 管理员在 SaaS引擎 → OCR识别 独立 Tab 配置: 字段 | 说明 | 默认值 | OcrEnabled | 当前租户总开关;未开启时失败关闭 | 0 | OcrProvider | PaddleX(基础服务)或 PaddleXHighStability(KServe) | PaddleX | OcrEndpoint | 服务完整接口地址 | 空 | OcrApiKey | 可选 Bearer 密钥 | 空 | OcrHeadersJson | 可选服务端固定 Header JSON | 空 | OcrTimeoutSeconds | 单次超时,平台硬上限 300 秒 | 60 | OcrMaxFileMB | 单文件上限,平台硬上限 100 MB | 20 | OcrMaxPages | PDF 页数上限,平台硬上限 100 页 | 10 | OcrMinConfidence | 统一结果最低置信度,范围 0-1 | 0 | OcrEndpoint、OcrApiKey、OcrHeadersJson 等 OCR 配置不会进入 V8.OsClientModel,业务脚本只能使用绑定当前租户的 V8.OCR。密码控件用于减少界面误显,但不等同于数据库静态加密;生产环境仍应限制 SaaS 配置表权限、数据库备份权限和审计日志内容。 ### 部署边界 PaddleX 基础服务协议为 POST /ocr,高稳定服务协议为 POST /v2/models/ocr/infer。生产环境应固定 PaddleX、PaddlePaddle 和模型版本,并为 OCR 服务配置访问控制、readiness、请求体/并发限制与 CPU/GPU 资源上限。Microi API 节点保持无状态,可以共同调用同一服务池。 仓库提供固定 PaddleX 3.6.1 + PaddlePaddle 3.2.2 的 linux/amd64 CPU 基线。PaddlePaddle 3.3.0 当前存在 CPU oneDNN PIR 推理兼容问题,因此不要自行替换为 3.3.0。推荐直接拉取吾码杭州公开镜像;镜像发布阶段已经预置默认 OCR 产线模型: bash ``` cd Microi.Server/Microi.OCR/deploy/paddlex docker compose -f compose.cpu.yml pull docker compose -f compose.cpu.yml up -d --no-build docker compose -f compose.cpu.yml ps ``` 默认只映射 127.0.0.1:18080,宿主机运行的 API 将 OcrEndpoint 配成 http://127.0.0.1:18080/ocr。若 API 也在 Docker 中,可将其加入名为 microi-ocr 的外部网络,并配置 http://microi-ocr:8080/ocr。全新的 named volume 会从镜像复制预置模型并在容器重启后复用;healthcheck 的 10 分钟启动宽限用于低配机器首次复制和加载模型,不代表单次识别可运行 10 分钟。本地修改 Dockerfile 后可显式执行 docker compose -f compose.cpu.yml build。 Compose 的 CPU/内存限制是安全基线,应按真实图片尺寸、页数和并发压测后调整。GPU 环境必须按显卡驱动选择官方 CUDA 11.8/12.6 镜像与对应 PaddlePaddle wheel,不应直接复用 CPU Dockerfile 或使用浮动 latest。 当前入口是请求内同步识别。批量、超大文件或长耗时 OCR 必须建立共享数据库/MQ/outbox 任务,以全局任务 Id、唯一约束和状态机保证幂等恢复;进程内队列、static 字典或本地文件不能作为任务完成事实源。 ## V8.Vision V8.Vision 是绑定当前租户的通用视觉原子能力。它负责有界图片解码、宿主已安装 ONNX 流水线的检测/分割、人脸对齐、对象级特征、HNSW 近邻检索、IoU 跟踪和连续帧投票;对象/商品/人员表、匹配状态、AI 回退和业务 Hook 均由接口引擎编排。完整架构、后台资源、模型与人脸治理见视觉引擎 (https://microi.net/doc/system-engine/vision-engine.html)。 javascript ``` var analyzed = await V8.Vision.Analyze({ FileByteBase64: V8.FilesByteBase64['frame'], FileName: 'camera-frame.jpg', PipelineKey: 'general-yolox-dinov2-v1', Mode: 'General', FrameId: 'scale-01-000123', StreamSessionId: 'scale-01-session-42', FrameSequence: 123 }); if (!analyzed || analyzed.Code !== 1) return analyzed; var ranked = await V8.Vision.Search({ QueryEmbeddingBase64: analyzed.Data.Detections[0].EmbeddingBase64, Candidates: [ { Key: 'sample-id', Name: '鲤鱼', EmbeddingBase64: '...' } ], Threshold: 0.82, TopK: 5, EfSearch: 80, IndexKey: 'ready-general-samples' }); var stable = await V8.Vision.Stabilize({ StreamSessionId: 'scale-01-session-42', FrameId: 'scale-01-000123', Key: ranked.Data.Matches.length ? ranked.Data.Matches[0].Key : '__unmatched__', Confidence: ranked.Data.Matches.length ? ranked.Data.Matches[0].Similarity : 0, WindowSize: 5, MinimumVotes: 3 }); ``` 方法 | 返回 | 说明 | Analyze(param) | Promise> | 检测/分割、逐目标嵌入、Face 五点对齐与 TrackId;返回对象框而不暴露模型路径 | Search(param) | Promise> | HNSW Top-K 检索,索引按租户和候选指纹缓存,小集合精确回退 | Stabilize(param) | Promise> | 按租户、StreamSessionId 与 TrackId 执行有界连续帧投票 | Extract(param) | Promise> | 单张 JPEG/PNG/WebP 特征;包含模型、尺寸、质量、耗时、向量和警告 | ExtractBatch(param) | Promise> | 有界帧批次;宿主默认最多 12 帧 | Compare(param) | Promise> | 两个同维向量的余弦相似度与是否达阈值 | CompareBatch(param) | Promise> | 候选集排序、TopK 与命中标记 | GetCapabilities() | DosResult | 模型 Key、版本、任务类型、许可证、就绪状态与硬上限 | 调用方不能传模型文件路径、模型目录、执行提供程序、网络地址或 OsClient。ModelKey/PipelineKey 只能解析到可信宿主模型目录中的单层安全名称,并校验 model.json 与每个 ONNX 文件 SHA-256。执行提供程序由宿主已安装运行时与受控描述符决定,支持 CPU、CUDA、TensorRT、OpenVINO、DirectML 的安全回退;调用方不能覆盖。Face 模式必须使用 FaceDetection + FaceEmbedding 流水线;内置指纹仅保留联调能力,不能用于生产身份判断。 一般业务不要直接把公开向量发到浏览器,而应调用官方 platform-vision-runtime:它从当前租户样本库读取候选,在数据库未命中时先返回 AiPending,再通过持久后台任务调用 Microi.AI。特征向量使用接口引擎绑定的保护能力保存,公开响应和租户 Hook 不包含向量、图片或 AI 提示词。 运行时动作包括 Recognize/Enroll/Result/Recent/Correct。Recent/Result 只可返回 HasImage 和由 V8.Method.GetPrivateFileUrl 生成的短期授权图片地址,禁止返回 InputFilePath 或图片 Base64。Correct 重新校验当前租户的对象与模式,保存原识别结果、修正人和原因;SaveAsSample=true 时按来源请求幂等录入。对于水产、生鲜和蔬菜,返回的冰冻状态、新鲜度与预估价格必须分别携带 Database/AI/Manual 来源,AI 估价不能直接作为门店正式售价或订单结算依据。 ## V8.Office V8.Office 可在接口引擎中生成 Excel、Word、PowerPoint 文件。导出方法返回 DosResult,接口引擎需要开启【响应文件】,并把 Data 转成 Base64 返回。 标准表格导出的下载扩展名为 .xlsx。以前下载为 .xls、实际内容却为 XLSX 的平台文件,Excel 导入入口会在验证真实工作簿结构后兼容读取;真正的 .xls 也继续支持。任意 ZIP、HTML 和损坏文件仍会被拒绝,OnlyOffice 文件回源继续严格校验扩展名与格式。 方法 | 说明 | ExportExcel({...}) | 导出 .xlsx,支持单/多 Sheet、标准表格、高级自由布局、图片、公式、合并、边框、打印和行分组 | ExcelToList({...}) | 解析 Excel 或 CSV;CSV 自动识别 UTF-8/GBK 编码与常见分隔符,SheetIndex 从 0 开始 | ExportWordText({...}) | 旧版纯文本 Word 导出,继续兼容 | ExportWord({...}) | 导出 .docx,支持段落、章节、表格、图片、页眉页脚、页码 | ExportPowerPoint({...}) | 导出 .pptx,支持多页、文本、项目符号、表格、图片、主题、页码 | SendEmail({...}) | 发送 HTML 邮件 | ### 导出 Excel ExportExcel 提供两种可在同一工作簿混用的模式: - 标准表格模式:继续使用 ExcelData + ExcelHeader,适合数据列表、图片列和常规报表,完全兼容旧代码。 - 高级布局模式:使用 ExcelLayout 按 A1 区域写入任意单元格、公式、样式和合并区域,适合审批单、套打表、主子表、多级表头、统计卡片及截图同款复杂版式。 需要控制工作表名称、列宽、行高、冻结窗格、筛选、打印和公共样式时传 ExcelOptions;标准表格中某一列的宽度、隐藏、数字格式或样式仍在 ExcelHeader 对应项中配置。 完整单 Sheet 示例: js ``` var excelResult = V8.Office.ExportExcel({ OsClient: V8.OsClient, ExcelData: dataList, ExcelHeader: [ { Name: 'Name', Label: '姓名', Component: 'Text', Width: 18, MinWidth: 12, MaxWidth: 30, HeaderStyle: { BackgroundColor: '17365D', FontColor: 'FFFFFF' }, Style: { WrapText: true, VerticalAlignment: 'Center' } }, { Name: 'Amount', Label: '金额', Component: 'NumberText', Type: 'decimal', Width: 16, NumberFormat: '#,##0.00', Style: { HorizontalAlignment: 'Right' } }, { Name: 'Remark', Label: '备注', Component: 'Textarea', AutoSize: true, MinWidth: 20, MaxWidth: 50, Style: { WrapText: true } } ], ExcelOptions: { SheetName: '销售明细', DefaultColumnWidth: 14, // 未设置 Width 的列,单位为 Excel 字符宽度 HeaderRowHeight: 30, // 单位:磅(pt) DataRowHeight: 24, // 单位:磅(pt) FreezeHeader: true, FreezeColumns: 1, AutoFilter: true, ShowGridLines: false, HeaderStyle: { FontName: 'Microsoft YaHei', FontSize: 11, Bold: true, HorizontalAlignment: 'Center', VerticalAlignment: 'Center', BorderStyle: 'Thin', BorderColor: 'B7C9D6' }, CellStyle: { FontName: 'Microsoft YaHei', FontSize: 10, VerticalAlignment: 'Center' } } }); if (excelResult.Code !== 1) return excelResult; return { Code: 1, Data: { FileName: '业务数据.xlsx', ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', FileByteBase64: System.Convert.ToBase64String(excelResult.Data) } }; ``` #### ExcelOptions 工作表配置 参数 | 类型 | 说明 | SheetName | string | 单 Sheet 名称;多 Sheet 通常直接使用每项的 SheetName | DefaultColumnWidth | number | 默认列宽,单位为 Excel 字符宽度;有效范围 0.1~255 | DefaultRowHeight | number | 默认行高,单位为磅(pt);最大 409.5 | HeaderRowHeight | number | 表头行高,单位为磅(pt);最大 409.5 | DataRowHeight | number | 数据行高,单位为磅(pt);最大 409.5 | FreezeHeader | boolean | 是否冻结首行 | FreezeRows | number | 冻结顶部行数;传入后优先于 FreezeHeader | FreezeColumns | number | 冻结左侧列数,例如 1 表示冻结首列 | AutoFilter | boolean | 是否为表头及数据区域启用自动筛选 | AutoFilterRange | string | 自定义筛选区域,如 A3:K20;传入后自动启用筛选 | AutoSizeColumns | boolean | 是否自动计算所有列宽;大数据量导出建议使用固定 Width,性能更稳定 | ShowGridLines | boolean | 是否显示工作表网格线 | Zoom | number | 工作表缩放比例,范围 10~400 | PrintOrientation | string | 打印方向:Portrait / Landscape | PaperSize | string | 纸张:A3 / A4 / A5 / Letter / Legal,也可传 NPOI 数字代码 | FitToWidth / FitToHeight | number | 打印时缩放为指定页宽/页高;常用 1 / 0 | PrintArea | string | 打印区域,如 A1:K15 | MarginTop/Right/Bottom/Left | number | 打印页边距,单位为英寸 | CenterHorizontally/Vertically | boolean | 打印时水平/垂直居中 | HeaderText / FooterText | string | 页眉/页脚文字 | ShowPageNumber | boolean | 在页脚右侧显示“第 X 页 / 共 Y 页” | HeaderStyle | object | 全局表头样式,列级 HeaderStyle 可覆盖 | CellStyle | object | 全局数据单元格样式,列级 Style 可覆盖 | #### ExcelHeader 列配置 参数 | 类型 | 说明 | Name | string | 数据字段名,对应 ExcelData 每项的属性 | Label | string | Excel 表头文字 | Component / Config | string / object | 吾码字段组件与配置;图片、下拉、多选等按原组件规则导出 | Type | string | int / decimal 等数值类型会生成 Excel 数值单元格 | Width / ColumnWidth | number | 固定列宽,单位为 Excel 字符宽度;Width 为推荐写法 | AutoSize | boolean | 单列自动宽度;开启后可配合 MinWidth / MaxWidth 限制范围 | MinWidth / MaxWidth | number | 自动或固定列宽的下限/上限 | Hidden | boolean | 是否隐藏该列,数据仍保留在文件中 | HeaderHeight | number | 表头行高候选值;同一 Sheet 取所有列与 HeaderRowHeight 的最大值 | RowHeight | number | 数据行高候选值;同一 Sheet 取所有列与 DataRowHeight 的最大值 | NumberFormat | string | 数字/日期显示格式简写,如 #,##0.00、0.00%、yyyy-mm-dd | HeaderStyle | object | 当前列表头样式 | Style | object | 当前列数据单元格样式 | HeaderStyle / Style 支持以下属性: 参数 | 说明 | FontName、FontSize、FontColor | 字体、字号、字体颜色 | Bold、Italic、Underline | 加粗、斜体、下划线 | BackgroundColor | 背景色 | HorizontalAlignment | Left / Center / Right / Justify 等 | VerticalAlignment | Top / Center / Bottom 等 | WrapText、ShrinkToFit、Rotation | 自动换行、缩小字体填充、文字旋转角度(-90~90) | NumberFormat | 数字/日期格式;也可直接写在 ExcelHeader 项上 | BorderStyle、BorderColor | 统一设置四边边框样式与颜色,例如 Thin、B7C9D6 | BorderTop/Right/Bottom/LeftStyle | 分别设置上/右/下/左边框类型,可用 Thin、Medium、Dashed、Dotted、Double、DashDot 等 | BorderTop/Right/Bottom/LeftColor | 分别设置四条边框颜色;未传时继承 BorderColor | 颜色支持 RRGGBB 或 #RRGGBB。列级样式会继承全局样式并覆盖同名属性。若同时启用 AutoSize 与固定 Width,以自动计算结果为准,再由 MinWidth / MaxWidth 限制。要让 NumberFormat 按数值或日期参与 Excel 计算,源数据应为数值/日期,并给数值列传 Type:'int' 或 Type:'decimal';显示格式不会把普通文本强制转换为数值。没有传任何新配置时,旧导出宽度、行高和样式行为保持不变。 #### ExcelLayout 高级自由布局 高级布局不要求每行具有相同字段。每个 Cells 项通过 Range 指定 A1 单元格或区域;样式作用于整个区域,Value / Formula 写入区域左上角,Merge:true 同时合并该区域。多个样式区域可以重叠,后写入的非空样式属性覆盖先前同名属性,因此可先给 A3:K15 统一画细网格,再给首尾行和左右列覆盖中粗外框。 js ``` var cells = [ // 整张申请表的细网格 { Range: 'A1:H10', Style: { FontName: 'Microsoft YaHei', FontSize: 10, BorderStyle: 'Thin', BorderColor: '7F8C9A', VerticalAlignment: 'Center', WrapText: true }}, // 合并标题 { Range: 'A1:H1', Value: '盘盈亏及报废申请表', Merge: true, Style: { FontSize: 18, Bold: true, BackgroundColor: 'EEF2F7', HorizontalAlignment: 'Center', BorderTopStyle: 'Medium', BorderTopColor: '34495E' }}, // 表头与一行数据 { Range: 'A2', Value: '序号', Style: { Bold: true, HorizontalAlignment: 'Center' }}, { Range: 'B2', Value: '物料编码', Style: { Bold: true, HorizontalAlignment: 'Center' }}, { Range: 'G2', Value: '数量', Style: { Bold: true, HorizontalAlignment: 'Center' }}, { Range: 'H2', Value: '金额', Style: { Bold: true, HorizontalAlignment: 'Center' }}, { Range: 'A3', Value: 1 }, { Range: 'B3', Value: 'V3-MAT-1200' }, { Range: 'G3', Value: 2, DataType: 'Number' }, { Range: 'H3', Formula: 'G3*1200', Style: { NumberFormat: '#,##0.00' }}, // 合计与审批意见 { Range: 'A8:G8', Value: '合计', Merge: true, Style: { Bold: true, HorizontalAlignment: 'Right' }}, { Range: 'H8', Formula: 'SUM(H3:H7)', Style: { Bold: true, NumberFormat: '#,##0.00' }}, { Range: 'A9:D9', Value: '(1) 申请人:张三(已电子签)', Merge: true }, { Range: 'E9:H9', Value: '(2) 主管意见:同意(已电子签)', Merge: true }, { Range: 'A10:H10', Value: '备注:审批完成后原件交财务归档。', Merge: true } ]; var excelResult = V8.Office.ExportExcel({ OsClient: V8.OsClient, ExcelSheets: [{ SheetName: '审批单', ExcelLayout: { Cells: cells, // 也可集中传 MergedRanges: ['A1:H1', 'A8:G8'] Columns: [ { Column: 'A', Width: 9 }, { Column: 'B', Width: 22 }, { Column: 'H', Width: 16 } ], Rows: [{ Row: 1, Height: 42 }, { Row: 2, Height: 32 }], // Excel 原生分组,适合主表/子表明细展开折叠 RowGroups: [{ StartRow: 3, EndRow: 7, Collapsed: false }] }, ExcelOptions: { ShowGridLines: false, FreezeRows: 2, FreezeColumns: 1, AutoFilterRange: 'A2:H8', PrintOrientation: 'Landscape', PaperSize: 'A4', FitToWidth: 1, PrintArea: 'A1:H10', ShowPageNumber: true } }] }); ``` ExcelLayout 参数: 参数 | 说明 | Cells | 单元格/区域列表;每项支持 Range、Value、Formula、DataType、Merge、Style | MergedRanges | 额外合并区域列表,如 ['A1:K1','A10:F10'];重叠且不完全相同的合并区域会明确报错 | Columns | 列配置;用 Column:'A' 或从 1 开始的 Index 定位,支持 Width/Hidden/AutoSize/MinWidth/MaxWidth | Rows | 行配置;Row 从 1 开始,支持 Height/Hidden | RowGroups | Excel 原生行分组;StartRow/EndRow 从 1 开始,Collapsed 控制初始折叠状态 | DataType 可用 String、Number、Boolean、DateTime、Blank;不传时按值自动识别。公式可带或不带开头的 =,生成后会要求 Excel 重新计算。xlsx 限制为最大 1,048,576 行、16,384 列;布局区域越大,创建的单元格和样式越多,应只覆盖实际使用范围。 官方完整示例接口引擎为 export-excel-advanced-demo,一次返回 5 张 Sheet:截图同款盘盈亏报废申请、可折叠主子表、复杂多级合并表头、标准数据表、边框与卡片样式库。接口必须开启【响应文件】;示例地址:/apiengine/export-excel-advanced-demo--OsClient--iTdos--。 如需把接口地址直接发送给客户在线查看,使用配套匿名接口 export-excel-advanced-demo-preview。该接口也必须开启【响应文件】,直接访问会下载 .xlsx;/online-office 通过 fileUrl 接收它的完整地址,平台后端限域读取文件后透明缓存到当前租户公有 HDFS,再把可回源静态地址交给 OnlyOffice。接口引擎仍只响应文件,不需要返回 FileUrl/OnlineOfficePath JSON;响应文件动态路由同时支持 GET/HEAD。 text ``` /apiengine/export-excel-advanced-demo-preview--OsClient--iTdos-- ``` 本地预览链接示例(fileUrl 已 URL 编码): text ``` http://localhost:1988/?OsClient=iTdos#/online-office?fileUrl=https%3A%2F%2Flocalhost%3A7266%2Fapiengine%2Fexport-excel-advanced-demo-preview--OsClient--iTdos--&fileName=%E5%90%BE%E7%A0%81V8%E9%AB%98%E7%BA%A7Excel%E5%A4%9ASheet%E7%A4%BA%E4%BE%8B.xlsx&fileType=xlsx&canEdit=0 ``` 匿名 fileUrl 只允许当前平台 ApiBase 下的 /apiengine/...,且必须显式包含当前 OsClient,禁止把任意外部 URL 交给 OnlyOffice。开发环境中的 localhost/127.0.0.1 仅允许由同端口本地后端读取;文件通过 SHA-256 确定性路径写入当前租户 office-preview 公有目录并共享缓存 10 分钟,限制 50MB、不跟随重定向、校验扩展名和文件头。未登录时即使传 canEdit=1 也强制只读,并隐藏系统菜单、顶部导航和页签;敏感数据不得开放匿名接口。 #### 多 Sheet 外层 ExcelOptions 是全部 Sheet 的默认配置;每个 Sheet 可传自己的 ExcelOptions 覆盖局部属性。这样可以统一字体和默认宽度,同时单独控制每个页签的冻结、筛选和行高。 js ``` var excelResult = V8.Office.ExportExcel({ OsClient: V8.OsClient, ExcelOptions: { DefaultColumnWidth: 14, HeaderRowHeight: 28, HeaderStyle: { Bold: true, BackgroundColor: 'D9EAF7' } }, ExcelSheets: [ { SheetName: '订单', ExcelData: orderList, ExcelHeader: [ { Name: 'OrderNo', Label: '订单号', Component: 'Text', Width: 22 }, { Name: 'Amount', Label: '金额', Component: 'NumberText', Type: 'decimal', Width: 16, NumberFormat: '#,##0.00' } ], ExcelOptions: { FreezeHeader: true, AutoFilter: true } }, { SheetName: '客户', ExcelData: customerList, ExcelHeader: [ { Name: 'Name', Label: '客户名称', Component: 'Text', Width: 24 }, { Name: 'Phone', Label: '联系电话', Component: 'Text', Width: 18 } ], ExcelOptions: { DataRowHeight: 22 } } ] }); if (excelResult.Code !== 1) return excelResult; return { Code: 1, Data: { FileName: '订单与客户.xlsx', ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', FileByteBase64: System.Convert.ToBase64String(excelResult.Data) } }; ``` ExcelSheets 每项可使用 ExcelLayout 高级布局,也可使用 ExcelData + ExcelHeader 标准表格;标准表格还可传 FormEngineKey/TableId/_Where/_OrderBy/_PageSize 等查询参数。同一个工作簿可以混用两种模式。Sheets 是兼容别名,新代码使用 ExcelSheets。Sheet 名称中的非法字符、31 字符上限和重名会自动处理。 图片列说明:ImgUpload.Multiple=1 仍会按最大图片数展开为多列并合并表头;Width 会应用到展开后的每一列,DataRowHeight / RowHeight 控制图片所在行高度。私有文件仍按原规则输出受限值,不会绕过文件权限。自动列宽需要遍历单元格,大批量导出优先显式设置 Width,避免不必要的内存和 CPU 开销。 ### 导出 Word 新代码使用对象参数的 ExportWord;ExportWordText 仅作为旧版纯文本接口继续保留。页面边距、图片宽高单位为厘米,字体大小单位为磅。 js ``` var wordResult = V8.Office.ExportWord({ Title: '月度经营报告', Subtitle: DateNow('yyyy年MM月'), Author: V8.CurrentUser.Name, Subject: '经营分析', Keywords: '经营,月报', Description: '月度经营分析报告', PageSize: 'A4', // A4 | Letter Orientation: 'Portrait', // Portrait | Landscape MarginTop: 2.2, MarginRight: 2.0, MarginBottom: 2.2, MarginLeft: 2.0, FontFamily: 'Microsoft YaHei', FontSize: 10.5, TitleFontSize: 20, SubtitleFontSize: 12, TitleAlignment: 'Center', LineSpacing: 1.25, ParagraphSpacingAfter: 6, HeaderText: '吾码经营中心', FooterText: '内部资料', ShowPageNumber: true, Paragraphs: [ { Text: '本月经营情况总体稳定。', FirstLineIndent: 0.74 }, { Text: '以下数据未经授权不得外传。', Bold: true, FontColor: 'C00000' } ], Sections: [{ Heading: '一、核心指标', HeadingLevel: 1, Content: '本节展示主要经营指标。', Tables: [{ Title: '指标明细', Headers: ['指标', '本月', '同比'], Rows: [['销售额', 1280000, '12.5%'], ['订单数', 860, '8.1%']], ColumnWidths: [4, 4, 4], HeaderBackgroundColor: 'D9EAF7', BorderColor: 'B7C9D6' }] }], Images: [{ FileByteBase64: chartBase64, // 纯 Base64 或 data URI FileName: 'chart.png', ContentType: 'image/png', Width: 15, Height: 8, Alignment: 'Center', Caption: '图 1:趋势分析' }] }); if (wordResult.Code !== 1) return wordResult; return { Code: 1, Data: { FileName: '月度经营报告.docx', ContentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', FileByteBase64: System.Convert.ToBase64String(wordResult.Data) } }; ``` 常用子参数: 对象 | 支持参数 | Paragraphs[] | Text/Alignment/Bold/Italic/Underline/FontFamily/FontSize/FontColor/SpacingBefore/SpacingAfter/LineSpacing/FirstLineIndent/PageBreakBefore | Sections[] | Heading/HeadingLevel/Content/Paragraphs/Tables/Images/PageBreakBefore | Tables[] | Title/Headers/Rows/ColumnWidths/Alignment/HeaderBold/HeaderBackgroundColor/HeaderFontColor/BorderColor/FontSize | Images[] | FileByteBase64/FileName/ContentType/Width/Height/Alignment/Caption | ### 导出 PowerPoint 幻灯片尺寸、图片/表格位置和宽高单位均为英寸;默认画布为 16:9(13.333 × 7.5)。 js ``` var pptResult = V8.Office.ExportPowerPoint({ Title: '季度经营汇报', Author: V8.CurrentUser.Name, Subject: '季度复盘', Keywords: '经营,季度', SlideWidth: 13.333, SlideHeight: 7.5, FontFamily: 'Microsoft YaHei', BackgroundColor: 'FFFFFF', TitleColor: '17365D', TextColor: '222222', TitleFontSize: 28, BodyFontSize: 18, ShowSlideNumber: true, Slides: [ { Layout: 'TitleSlide', Title: '季度经营汇报', Subtitle: DateNow('yyyy-MM-dd') }, { Layout: 'TitleAndContent', Title: '核心结论', Bullets: ['收入保持增长', '重点客户续约稳定'], TextItems: [ { Text: '风险:回款周期延长', Bullet: true, Level: 0, Bold: true, FontColor: 'C00000' } ], Tables: [{ Headers: ['指标', '本期', '目标'], Rows: [['销售额', '128万', '120万']], X: 0.7, Y: 4.0, Width: 11.9, Height: 2.2, HeaderBackgroundColor: '17365D' }] }, { Title: '趋势图', Images: [{ FileByteBase64: chartBase64, FileName: 'trend.png', ContentType: 'image/png', X: 1.2, Y: 1.5, Width: 10.9, Height: 5.2 }] } ] }); if (pptResult.Code !== 1) return pptResult; return { Code: 1, Data: { FileName: '季度经营汇报.pptx', ContentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', FileByteBase64: System.Convert.ToBase64String(pptResult.Data) } }; ``` 对象 | 支持参数 | 顶层 | Title/Author/Subject/Keywords/Company/SlideWidth/SlideHeight/FontFamily/BackgroundColor/TitleColor/TextColor/TitleFontSize/BodyFontSize/ShowSlideNumber/Slides | Slides[] | Layout/Title/Subtitle/Content/Bullets/TextItems/Images/Tables/BackgroundColor/TitleColor/TextColor/TitleFontSize/BodyFontSize | TextItems[] | Text/Level/Bullet/Bold/Italic/FontSize/FontColor/Alignment | Images[] | FileByteBase64/FileName/ContentType/X/Y/Width/Height | Tables[] | Headers/Rows/ColumnWidths/X/Y/Width/Height/HeaderBackgroundColor/HeaderFontColor/CellBackgroundColor/CellFontColor/FontSize | ### 解析 Excel / CSV js ``` var rows = V8.Office.ExcelToList({ FileByteBase64: excelBase64, FileType: 'excel', // CSV 传 csv,或通过 FileName: 'data.csv' 自动判断 SheetIndex: 0, // 从 0 开始 HeaderStartRow: 5, // 其余行号均从 1 开始 HeaderEndRow: 6, DataStartRow: 7, DataEndRow: 1000, Columns: [ { ColumnIndex: 0, Name: 'CustomerName', Label: '客户名称' }, { Column: 'C', Name: 'Phone', Label: '手机号' } ] }); if (rows.Code !== 1) return rows; ``` 参数 | 说明 | FileType/FileName | CSV 传 FileType:'csv' 或带 .csv 后缀的 FileName;Excel 省略即可。 | Encoding/Delimiter | CSV 可选固定编码/分隔符;通常应省略,由服务端自动识别 UTF-8/GBK 与逗号、制表符、分号、竖线。 | SheetIndex | 工作表索引,从 0 开始;省略时读取第一张。 | HeaderStartRow/HeaderEndRow | 表头一基起止行,支持合并单元格和多级表头;省略时保持旧行为:首行为表头。 | DataStartRow/DataEndRow | 数据一基起止行;省略时从表头下一行读到已用区域末尾。 | Columns | 确认后的列映射。ColumnIndex 从 0 开始,也可传 Excel 列字母 Column;Name 为返回对象字段名。 | MaxDataRows/MaxColumns | 解析上限;服务端仍强制不超过平台 50000 行、256 个有效映射列。 | 增强参数全部可省略,旧的首行表头模板继续兼容。传入增强范围后,每行额外返回 _ExcelRow 便于精确报错;数据行是否有效按所有已映射列判断,不再因 A 列为空而丢弃;公式单元格读取计算结果。CSV 解析成功时,DataAppend.FileType/Encoding/Delimiter 返回服务端实际识别结果,便于接口引擎与浏览器元数据交叉复核。 菜单配置【导入接口替换】时,统一弹层会在用户确认后上传原始文件,并同时传 V8.Param._ImportMetaJson、_ImportErrorPolicy 和 _ImportUniqueRulesJson。_ImportMetaJson v2.2 内也包含 ErrorPolicy/UpsertMode/UniqueRules。自定义接口应解析元数据,把相同范围传给 V8.Office.ExcelToList,从服务端原文件重新取数;不能直接信任浏览器预览,也不能退回固定首行表头: js ``` var meta = JSON.parse(V8.Param._ImportMetaJson || '{}'); var errorPolicy = V8.Param._ImportErrorPolicy || meta.ErrorPolicy || 'RollbackAll'; if (errorPolicy !== 'RollbackAll' && errorPolicy !== 'ContinueOnError') { return { Code: 0, Msg: '不支持的导入错误处理策略' }; } var fileMap = V8.FilesByteBase64 || {}; var fileBase64 = Object.values(fileMap)[0]; if (!fileBase64) return { Code: 0, Msg: '请上传 Excel 文件' }; var parsed = V8.Office.ExcelToList({ FileByteBase64: fileBase64, FileType: meta.FileType || V8.Param._ImportFileType, FileName: V8.Param._ImportFileName, SheetIndex: meta.SheetIndex == null ? 0 : meta.SheetIndex, HeaderStartRow: meta.HeaderStartRow, HeaderEndRow: meta.HeaderEndRow, DataStartRow: meta.DataStartRow, DataEndRow: meta.DataEndRow, Columns: meta.Columns || [] }); if (parsed.Code !== 1) return parsed; if (meta.FileType === 'csv' && parsed.DataAppend) { if (meta.Encoding && meta.Encoding !== parsed.DataAppend.Encoding) { return { Code: 0, Msg: 'CSV 编码复核不一致,请重新上传后确认预览。' }; } if (meta.Delimiter && meta.Delimiter.replace('\t', '\\t') !== parsed.DataAppend.Delimiter) { return { Code: 0, Msg: 'CSV 分隔符复核不一致,请重新上传后确认预览。' }; } } // 接下来仍须重做权限、字段、唯一性和状态校验。 // meta.UniqueRules / _ImportUniqueRulesJson 仅用于向用户说明,不能代替服务端权威字段配置。 ``` 自定义接口的 Upsert 规则必须与通用导入一致:每个“单字段唯一”各自判断,全部“组合唯一”字段共同判断;任一完整规则命中同一 Id 则按 Id 修改,全部未命中则新增;不同规则命中不同 Id,或一条规则命中多条数据,必须把该行记为冲突。禁止把多个独立规则错误拼成同一个 AND 条件,也禁止按宽泛唯一条件一次更新多行。 - RollbackAll:首个行错误返回 Code:0,由接口引擎事务整体回滚,禁止手动 Commit/Rollback。 - ContinueOnError:捕获行错误并继续处理下一行,最终返回 Code:1,同时返回 Added/Updated/Failed/Errors。若数据库异常已使当前事务不可继续,应先做整批预校验或改用平台允许的独立幂等行操作,不能声称已继续。 菜单【导入接口替换】与 V8.OpenImportDialog 后台接口引擎都必须遵守以上策略;区别只是前者使用 V8.FilesByteBase64 在服务端重读原文件,后者读取 _ImportRowsJson 的确认后映射行。 #### 保留标准导入、仅增加事务内整批校验 业务只需要增加跨行汇总、额度或并发校验时,不必替换整套导入接口。将模块 ImportV8 配置为 ApiEngine:,标准 /api/FormEngine/ImportDiyTableRow 会继续负责权限、原文件复核、字段映射、唯一规则、进度和写入,并在 RollbackAll 的同一数据库事务内、实际写入前调用指定接口引擎。 校验接口通常只返回 { Code: 1 }。支持安全回填的平台会在 V8.Param.Capabilities.PreflightFixedValuesV1 传入 true;接口确认该能力后,如果能依据当前用户有权访问的权威数据,唯一推导出整批共同缺失的业务字段,可以返回 { Code: 1, Data: { FixedValues: { ProjectId: "..." } } }。平台会再次校验:字段必须是当前表的可导入业务字段,值必须是非空标量,且不得设置 Id、审计、租户等平台字段,也不得覆盖 TableChild 已固定的父记录上下文;通过后该值才会用于本批全部写入。无法唯一推导时应返回 Code != 1,并在 Msg 中说明缺失信息和可操作的解决方法。 接口引擎从 V8.Param.Rows 读取服务端规范化后的整批行,从 V8.Param.FixedValues 读取父表固定值;另有 TableId/TableName/SysMenuId/ImportErrorPolicy/ImportFileName/ImportIdempotencyKey/UniqueRuleCount/Capabilities。此处的 V8.DbTrans 与后续标准写入共用事务,适合先按稳定顺序锁定额度行,再重新汇总并校验。接口以 Code:1 放行(可选返回上述 Data.FixedValues)或以 Code!=1 拒绝,不应自行写入、Commit 或 Rollback。 启用该钩子后只允许 RollbackAll。TableChild 内导入时,平台固定父记录外键覆盖 Excel 同名列,避免把数据写到其它父记录。客户端每次重新选择文件生成新的 _ImportIdempotencyKey,同一 HTTP 重试沿用该键;服务端按租户、表、菜单和用户隔离请求状态,避免重复创建写入任务。它不负责判断“相同文件以一个全新请求再次上传”是否属于业务重复。 ### 发送邮件 SendEmail - 源码实现在 Microi.Server/Microi.Office/MicroiOffice.cs(GitHub (https://github.com/itdos/microi.net/blob/master/Microi.Server/Microi.Office/MicroiOffice.cs) / Gitee (https://gitee.com/ITdos/microi.net/blob/master/Microi.Server/Microi.Office/MicroiOffice.cs))。 js ``` return V8.Office.SendEmail({ SmtpServer : 'smtp.qq.com', SmtpPort : 587, EnableSSL : true, SystemEmail : 'admin@itdos.com', SystemEmailPwd : 'uuzrnazvv*******', EmailSubject : '测试接口引擎发邮件标题', EmailBody : '测试接口引擎发邮件内容,支持html', Receivers : ['123446172@qq.com', '973702@qq.com'] }); ``` ## 邮箱协议 V8.Email 邮箱业务通过接口引擎 mci-email 编排,详见 邮箱系统 (https://microi.net/doc/system-engine/email-engine.html)。 V8.Email 只提供租户凭据加密、IMAP、SMTP 和 MIME 协议原子,不创建第二套 HTTP 接口。 方法 | 用途 | ProtectCredential(value) | 将授权码加密为当前租户密文;没有对 V8 开放的解密方法 | TestConnection(p) | 检测 IMAP / SMTP 的 TLS 连接与账号认证 | ListFolders(p) | 读取目录路径、类型和服务端计数 | Fetch(p) | 分页读取信封,支持 UID 游标、UIDVALIDITY 重置与最新邮件优先 | Inspect(p) | 最多核对 100 个 UID 的存在、已读、星标和删除状态 | GetMessage(p) / GetAttachment(p) | 按需读取正文或一个附件,不加载远程图片 | SetFlags(p) / Move(p) | 修改单封邮件标记或移动到目标目录 | Send(p) | 发送 MIME 邮件,返回 Accepted / Rejected / Unknown 投递状态 | StoreSent(p) | 按 Message-Id 查重并保存远端已发送副本,独立于 SMTP 投递 | 连接参数为 Host, Port, Security, UserName, Credential, Timeout。 Credential 必须为当前租户密文,Security 仅支持 SslOnConnect 或 StartTls。 邮件参数包括 To, Cc, Bcc, Subject, TextBody, HtmlBody, InReplyTo, MessageId, Attachments。 附件为 { FileName, ContentType, FileByteBase64 },最多 10 个,总大小不超过 10MB。 读取时传 Folder, UidValidity, Uid;分页传 AfterUid, Limit(最高 100)。 Recent=true 先读取最新一批而保留历史游标;GetAttachment 另传从 0 开始的 AttachmentIndex。 目录路径不可自行改写,UID 必须与 UIDVALIDITY 一起持久化。 发信前由接口引擎保存稳定草稿 Id、Message-Id 与共享去重状态。 Accepted 代表 SMTP 接受,不代表收件人已经阅读;Unknown 必须先核对远端目录,禁止自动重发。 普通调用优先使用邮箱业务接口,避免遗漏权限、状态机和发送幂等。 ## 系统设置 V8.SysConfig - 后端接口引擎与后端 V8 事件访问当前租户完整系统配置;返回值是独立副本,脚本修改不会写回缓存或数据库。 - sys_config 的全部字段位于根对象;mci_system_setting 的全部启用设置位于独立的 ServerPrivateSettings 节点,Secret 在可信后端按当前租户解密。不存在 PublicSettings 属性。 - V8.FormEngine.GetSysConfig(...) 强制绑定当前 V8 租户,显式传其它 OsClient 也不能跨租户读取。 js ``` var sysTitle = V8.SysConfig.SysTitle; var privateSettings = V8.SysConfig.ServerPrivateSettings || {}; var loginName = privateSettings['Login.Gitee.Name']; var clientSecret = privateSettings['Login.Gitee.ClientSecret']; // clientSecret 只能用于当前后端逻辑,禁止 return 或写日志。 ``` 匿名 FormEngine/GetSysConfig 与前端 V8 使用另一条公开投影:只返回 sys_config 的浏览器安全字段,整个 ServerPrivateSettings 节点永远不进入浏览器。后端 V8 不需要查询 mci_system_setting.SecretCipher 或自行解密,直接按 Key 读取 V8.SysConfig.ServerPrivateSettings;任何 Secret 都不得返回客户端、写日志、写审计或保存到前端可读字段。 ## SaaS引擎信息 V8.OsClientModel / V8.ClientModel - 两者是当前租户 SaaS 配置的独立脱敏副本,脚本修改不会写回服务端运行配置。 - 数据库连接、鉴权密钥以及共享 Redis、对象存储、RabbitMQ、MQTT、Search 的地址、账号和密码不会注入 V8;即使接口错误地 return V8.ClientModel,也不会泄露主库基础设施凭据。 - sys_osclients 自定义业务字段只作为存量兼容;新增公开配置使用当前租户库的 sys_config 实体字段,新增私密业务设置使用 mci_system_setting。后端通过 V8.SysConfig.ServerPrivateSettings 使用私密值与 Secret;这些设置不通过 V8.OsClientModel 暴露。 - 存储类型 HDFS 与公开文件域名可以读取;访问缓存、文件、MQ、MQTT 和 Search 必须使用对应的 V8.* 受控能力,服务端自动添加当前租户命名空间。 js ``` var title = V8.OsClientModel.SysTitle; var storageType = V8.ClientModel.HDFS; // ClientModel 是兼容别名 // 以下字段为 undefined,不再暴露: // V8.ClientModel.DbConn / RedisPwd / MinIOSecretKey / MQPassword / MqttPwd / SearchEngineApiKey ``` 共享基础设施可以复用同一 Redis、对象存储、RabbitMQ Broker、MQTT Broker 和搜索集群,但隔离边界由服务端强制执行:缓存 Key、对象路径、队列、Topic、索引分别绑定当前 OsClient。RabbitMQ、MQTT 和 Search 还必须为子租户配置独立凭据;缺少独立凭据时对应能力失败关闭,不会回退使用主租户账号。 ### 主租户推广授权与公开空库开通原子 SaaS 应用通过以下固定 Key 调用可信原子。它们不能作为任意业务接口的通用跨租户或建库入口: 方法 | 绑定接口 | 参数与结果 | V8.Method.AuthorizeSaasPromotion() | platform-saas-promotion | 重新读取账号、角色、菜单;返回 UserId,CanViewAll,CanConfigure 及当前运行分区 | V8.Method.ReadSaasTenantUsage({TenantIds,Refresh}) | 同上 | 最多20个租户记录 Id;返回固定统计及采集状态、时间,不允许任意 SQL/连接 | V8.Method.CreateSaasReferralCapability(linkId) | 同上 | 从主库核验推荐归属,签发绑定主租户与分区的公开链接票据 | V8.Method.ResumeSaasPublicTrialTask(taskId) | 同上 | 原推荐人或授权管理者恢复失败申请;保留原幂等键、密码哈希和加密数据库检查点,总执行最多三次 | V8.Method.SaasPublicTrialAtom(parameters) | platform-saas-public-trial | Bootstrap / Queue / Progress;校验链接、推荐人、验证码与额度,创建有幂等键的持久任务 | V8.Method.ProvisionPublicSaasTrial({GrantCipher}) | platform-saas-public-trial-worker | 只接受当前后台任务及栅栏令牌,不接受管理员身份、SQL包或连接参数 | 业务员须具有推广菜单权限,只能读取本人推荐的子租户;管理角色可看全局,平台管理员才可修改公开开通配置。 公开开通使用有限能力票据,不授予管理员会话。数据库 DDL 前持久化加密账号检查点,恢复时只接续同一任务, 不接管其它请求的库或旋转既有账号密码。详见 SaaS 推广中心与客户自主开通 (https://microi.net/doc/system-engine/saas-engine.html#saas-%E6%8E%A8%E5%B9%BF%E4%B8%AD%E5%BF%83%E4%B8%8E%E5%AE%A2%E6%88%B7%E8%87%AA%E4%B8%BB%E5%BC%80%E9%80%9A)。 ## 表单数据 V8.Form - 表单提交事件中可访问表单数据,接口引擎中此对象为空。 ## V8.OldForm - 在修改数据时,后端V8事件可访问到V8.OldForm修改前的数据值 原生删除事件中,平台将删除前的数据库记录放入 V8.Form,V8.OldForm 可能为空。删除审计或关联清理应使用 V8.Form;修改事件继续使用 V8.OldForm。删除上下文中的版本字段是数据库当前快照,不能据此声称已校验客户端期望版本。业务要求防止过期删除时,应由受控接口接收期望版本、锁定并校验主库记录,然后在同一事务删除。 ## V8.FormSubmitAction - 表单提交类型:可能的值:Insert Delete Update(string类型) - 注意服务器端V8事件里面没有FormOutAction、FormOutAfterAction,只有FormSubmitAction ## V8.EventName - 后端V8事件名称,在全局V8引擎代码中比较好用,可能的值: js ``` FormSubmitBefore:表单提交前V8事件 FormSubmitAfter:表单提交后V8事件 DataFilter:数据处理V8事件 WFNodeLine:流程节点条件判断V8事件 WFNodeEnd:流程节点结束V8事件 WFNodeStart:流程节点开始V8事件 ``` ## V8.Param - 用于访问前端传入的参数,能访问到url参数、form-data参数、payload-json参数 ## V8.Action - 用于访问在全局服务器V8代码处自定义的方法 ## V8.InvokeType - 访问当前调用类型,可能的值:Server、Client,当访问到的V8.InvokeType为空时,则默认Server - Server:服务器端调用,如在接口引擎中调用接口引擎,在后端V8事件中调用接口引擎 - Client:前端调用,如在前端V8事件中调用接口引擎,在前端提交表单 ## V8.TableModel - 在后端V8事件中,可访问到操作的当前diy_table表的信息 ## V8.OsClient - 访问当前的OsClient值 ## 其它后端能力与支持边界 后端会把多个扩展同时注册为 V8.* 和兼容全局对象。新代码统一使用 V8.*,不要依赖全局别名,避免与 JavaScript、CLR 或其它扩展同名。 能力 | 推荐入口 | 适用范围与安全边界 | 数据源引擎 | V8.DataSourceEngine.Run/RunAsync | 只运行当前租户数据源;动态 SQL、远程连接和返回字段仍需按业务授权 | 模块引擎 | V8.ModuleEngine | 读取当前用户可见模块模型;不能代替 FormEngine 的数据权限 | 工作流引擎 | V8.WFEngine、事件中的 V8.WF | 启动、发送、撤回、取消等动作必须校验当前用户、当前租户、流程状态与表单范围 | 翻译引擎 | V8.TranslateEngine.Translate/TranslateText/Detect/GetLanguages/TranslateFile/Suggest/Health/GetLang... | 使用当前租户的语言与供应商配置,不能传其它租户、endpoint 或密钥;批量/文件受硬上限保护 | OCR | V8.OCR.Recognize | 只使用当前租户服务端配置;调用方不能覆盖 endpoint、密钥、Header 或 OsClient;批量任务需要共享持久状态与幂等 | 文件能力 | V8.HDFS、V8.Method.Upload/GetPrivateFileUrl | 普通业务优先使用受控上传和短期代理;列举、删除、读取私有字节属于可信管理能力 | 消息能力 | V8.MQ.SendMsg | 队列名绑定当前租户;消费、关闭通道等属于 Worker 内部能力,消息必须有全局 EventId 和幂等消费 | 短信 | V8.Sms.Send | 供应商配置必须脱敏,发送接口要有频率、金额/条数、模板和收件人限制 | 爬虫 | V8.Spider | 属于高风险 Worker 能力;必须使用租户目标地址策略、租户/用户会话隔离、并发和运行时限,禁止脚本指定浏览器可执行文件 | 主机监控 | V8.System | CPU、内存、磁盘、网络等运维数据仅供管理员/运维,不应从普通或匿名接口返回 | 任务调度 | V8.Method.SaveScheduleJob、V8.Method.ManageScheduleJob | 仅当前租户超级管理员;保存只允许接口引擎任务。表单先检查 ManageScheduleJob({Action:'Capabilities'}) 的 RuntimeOnly 能力,再直接调用 SaveScheduleJob 并传 RuntimeOnly:true:仅同步 Quartz 并回读状态,由原表单事务写元数据。直接调用可保留业务 ApiEngineKey,避免嵌套 V8.ApiEngine.Run 将同名路由参数改成管理接口 Key;默认保存仍写完整任务。不能用 HTTP 回调本平台或在提交事件中再次写当前表 | 支付、微信、DNS | 对应 V8.* 扩展 | 单独校验签名、幂等键、回调重放、金额与租户凭据,不要返回密钥 | V8.HDFS.DeleteObject 的目录路径必须保留末尾 /,租户根和其它租户绝对路径不能删除。恢复单个零字节空目录标记时显式传 EmptyDirectoryOnly:true、Limit:true/false,并等待异步结果;服务端从可信 V8 作用域或 DiyToken 复核当前租户实际管理员权限,不信任参数中的用户、角色、供应商或密钥。该模式完整核验同桶前缀与大小,只删除精确标记,随后严格回读;未知分页、缺少元数据、非空或回读失败一律拒绝。Code=1 必须同时携带 Data.DeletionMode='EmptyDirectoryMarkerOnly' 和 VerifiedAbsent=true。 HTTP/MCP 使用专用 /api/HDFS/DeleteEmptyDirectoryMarker 路由,旧后端 404 不能降级为普通目录删除。此操作不构成零字节 CAS,也不能证明历史 NoPUT;业务层须先隔离原请求、排除同 key 的活跃写入,并保留原请求键与独立恢复证据。详见分布式存储 (https://microi.net/doc/more/hdfs.html)。 动态建表、动态字段、数据库备份/清空、缓存连接管理等控制面 HTTP API 独立执行管理员门禁,低层方法在 V8 对象上可见不代表可以向普通用户公开。接口引擎标准管理表 sys_apiengine 支持真实菜单/表权限委托,读取和代码增删改按各自动作授权;代码写权限仅授予可信开发角色。该委托不豁免其它控制面、接口运行或 MCP 专用鉴权。 ## 执行上下文与资源限制 ### 公开热点资源的备用地址 V8.Method.GetPublicResourceBackupUrl({FilePathName:'tenant/micro-app/app/index.html'}) 是只读选择原子,返回 {Code:1,Data:{UseBackup,Url}}。只有当前公开应用、完整版本清单、签名副本与 NAS 健康同时匹配才返回地址;不接受 URL、Ready、SHA、流量或凭据,不读取私有文件。旧后端或未晋级时继续原公开地址,不能把 FileServerBak 非空当作副本存在。 历史安装微服务的商城 IsPublic=0 不等同于其已发布编译文件是私有对象。只有原生 InstalledPublicBuild/1 提供者从主库独立读取当前应用、唯一协议 0–2 历史版本、唯一启用服务及完整 dist/ 文件集合,复用安装别名四方一致性校验,并确认匹配且未到期的 InstalledAliasReconciliation/1 三次稳定检查点后,才可把整个公开构建依赖组纳入分流。每个文件必须标记 PublicBuildOnly 或 PrivateSource+PublicBuild,源对象当前公有 ACL、版本、原始 SHA 与同一复制 Run 的目标回执都要核验;单个 JS、私有源码、陈旧检查点、缺失成员或跨租户路径均不准入。首次开放目标前和健康续期复查全组,实际选择时再复查所选源对象;失败继续原授权下载链,不把 IsPublic、LegacyUnverified 或协议号改成公开/V3 成功。该接口只供原生宿主内部调用,不开放新的 HTTP、MCP 参数或可编辑 V8 能力。探测链最后一个 NAS 响应返回后,准备与续期都通过同一个完成门重新读取当前整组权威和公有 ACL,再检查租约并提交 Ready 或健康时间;变化则走现有失败撤权。两种真实访问入口使用同一个 5 秒总预算覆盖绑定、Redis、主库权威、ACL 和 HTML HEAD,原生权威读取最多 4 个并发,超时停止等待但占用槽位直到已启动读取结束,后续 SQL 不再启动;后台任务仍使用宿主原有取消期限。 管理员控制面由系统日志应用的 Managed 接口 mci-file-resource-runtime 调用 ManagePublicResourceOffload;普通引擎不能调用管理原子。支持 Capabilities/Status/List/ProbeBackup/Collect/Prepare/PinHistoricalHotspot/Disable/Enable,除动作外只接受 SourceTenant/AppKey。复制请求入队后由共享后台工作器继续,不通过 MCP/HTTP 长时间传文件;Queued 不代表已复制或分流。实际策略及验收见分布式存储 (https://microi.net/doc/more/hdfs.html)。 常见上下文包括 V8.Param、V8.Header、V8.CurrentUser、V8.OsClient、V8.Form、V8.OldForm、V8.TableModel、V8.TableData、V8.FormSubmitAction、V8.EventName、V8.InvokeType、V8.RowIndex、V8.CacheData、V8.NotSaveField、V8.LineValue、V8.NextNodeId、V8.FilesByteBase64 和 V8.WF。Engine、HttpContext、执行租约等宿主对象属于内部实现,不要保存到静态变量、缓存或延迟回调。 平台的 SecurityGuard、PressureGuard、V8Limits、OrmLimits、StartupLimits 以及 V8 并发门共同保护单次脚本和单节点资源。V8 的默认/最大超时、语句数、单层累计分配、调用树累计分配、JavaScript 递归、接口嵌套深度和并发等待可在 sys_config 的开发配置中查看;接口引擎与表后端事件分别默认 sys_apiengine.V8Limit=0、diy_table.V8Limit=0,不设置 Jint 单次预算,只有打开 V8运行限制 后才应用 Timeout/MaxStatements/LimitMemory/LimitRecursion。进程内并发门不是集群级配额或分布式锁;多节点副作用仍必须依赖 Redis/数据库租约、幂等键、唯一约束、状态机或 outbox/inbox。 脚本应主动控制: - SQL 页大小、字段数、循环次数和返回体大小; - HTTP 超时、响应大小与目标地址; - 图片、Office、ZIP 的文件数、解压体积和托管内存; - MQ/短信/支付等外部副作用的幂等与重试; - 日志内容的脱敏、限长与关联 ID。 完整的部署与安全基线见 平台安全总览 (https://microi.net/doc/more/security.html)。 ### AI 完整能力与过程回调(8.8.1) V8.AI 已绑定当前接口引擎的认证用户和租户,不能由参数替换身份、Endpoint、ApiKey 或授权表。 后端可直接调用,无需请求自己的 HTTP 服务。与前端的完整方法和 29 项图像工具对照见 前端 V8 AI 调用 (https://microi.net/doc/v8-engine/v8-client.html#%E5%B7%A5%E4%BD%9C%E5%8F%B0%E5%85%A8%E9%83%A8%E8%83%BD%E5%8A%9B%E4%B8%8E%E5%AE%9E%E6%97%B6%E8%BF%87%E7%A8%8B)。 能力 | 后端入口 | 边界 | 聊天/意图/数据分析 | Chat、RecognizeIntent、NL2SQL | 数据权限仍由服务端计算 | 流式结果 | ChatStream、NL2SQLStream、NL2V8Stream | 分片回调;NL2V8 / NL2V8Stream 限平台管理员 | 完整执行事件 | ChatEvents(param, onEvent) | 真实 message/agent/receipt 事件 + 最终 DosResult;旧 AI 模块只兼容文本分片 | 模型与图片任务 | GetMediaModels、GenerateMiniMaxImage、GetMiniMaxImageTask、RecoverMiniMaxImageTask | 图片用 Operation 选择生成/消除/去水印等 21 项操作 | 音乐/语音/视频 | GenerateMiniMaxMusic、GetMiniMaxMusicTask、RecoverMiniMaxMusicTask、GenerateMiniMaxSpeech、CreateMiniMaxVideo、GetMiniMaxVideoTask、GetMiniMaxVideoFile、PersistMiniMaxVideoFile | 保留原方法权限、幂等、签名句柄和 HDFS 规则 | 八项精确图像操作 | V8.Image.Resize/Grayscale/RemoveSolidBackground/Crop/Rotate/Flip/Convert/Merge | 不调用模型;参数使用 V8.Image 原子契约 | javascript ``` var events = []; var result = await V8.AI.ChatEvents({ UserChatMsg: '解释这段已授权的内容', AiModel: '当前启用模型' }, async function (event, data) { // 只消费真实过程;限制本地集合大小,不把完整问题、回答或身份写入系统日志。 if (events.length < 100 && event === 'agent') events.push(data); }); return result; // Code=1 才是完整成功;失败/取消可能仍含部分结果。 ``` ChatEvents 的可选第三参数是宿主提供的 .NET CancellationToken,普通 V8 调用省略。 后端回调不自动变成浏览器推送;浏览器使用 V8.AI.ChatStream 或 ChatSignalR。 MCP 使用 microi_ai 的 Capabilities 发现完整动作,选择实时媒体目录后再调用; 推理/变更先给出确认值,生成使用原 RequestId,MCP 返回最终结果或任务回执。 microi_chat 等既有工具继续兼容;MCP 回答不等同于逐 token 流或已执行工具的证明。 ## console - Microi.net.dll从v3.5.1开始支持console往服务器端输出日志 js ``` console.log('日志输出'); console.error('日志输出'); console.warn('日志输出'); console.info('日志输出'); //服务端查看日志 docker logs microi-api ```