Soln Studio
Soln Studio 用于构建租户侧脚本和自定义页面。脚本在服务端运行,可以使用 StudioScriptContext。页面资产在浏览器运行,也可以声明同资产内的 server actions,用于需要成员资料、表单 API、文件、审批或外部 HTTP 的服务端逻辑。
在 Studio 中定义变量时,应该让变量靠近拥有数据的运行时:
- 脚本资产从
payload和context定义变量 - 页面资产在
src/App.tsx中定义浏览器状态 - 页面 server action 从 action
payload和context定义服务端变量 - 表单字段值通过字段 API 名在
record.data和写入data中读写
资产类型
| 类型 | Runtime | 入口 | 用途 |
|---|---|---|---|
| 脚本 | server_js | index.ts | 服务端自动化、表单写入、审批服务调用、集成逻辑 |
| 页面 | client_page | src/App.tsx | 自定义用户页面、看板、按钮、调用 server action 的页面 |
新建页面资产默认使用标准目录:
soln.extension.json
src
├── App.tsx
└── server
└── main.ts
脚本资产默认模板
脚本资产默认生成 index.ts:
export default async function main(
payload: StudioScriptRuntimeContextPayload,
context: StudioScriptContext
) {
const created = await context.form.create({
formApiName: 'orders',
title: 'SO-1001',
data: {
orderNo: 'SO-1001',
amount: 1200,
},
});
return {
ok: true,
recordId: created.id,
};
}
payload 始终使用 soln.runtime-context/v1。调用方传入的业务值只放在 payload.input;来源、表单、记录、草稿、选择项、当前子表行、上一步结果和运行元数据继续使用规范的顶层字段。
定义变量
脚本收到明确入参结构时,先定义输入类型:
type CreateOrderInput = {
orderNo?: unknown;
amount?: unknown;
customerId?: unknown;
};
export default async function main(
payload: StudioScriptRuntimeContextPayload,
context: StudioScriptContext
) {
const input = payload.input as CreateOrderInput;
const orderNo = String(input.orderNo || '').trim();
const amount = Number(input.amount);
const customerId = String(input.customerId || '').trim();
if (!orderNo || !Number.isFinite(amount)) {
throw new Error('ORDER_INPUT_INVALID');
}
return context.form.create({
formApiName: 'orders',
title: orderNo,
data: {
orderNo,
amount,
customer: customerId || null,
},
});
}
变量来自已有记录时,通过 record.data 里的字段 API 名读取:
const record = await context.form.find({
formApiName: 'orders',
condition: {
logic: 'and',
children: [
{ field: 'recordId', operator: 'eq', value: 'record-id' },
],
},
});
const data = record?.data || {};
const orderNo = String(data.orderNo || '').trim();
const amount = Number(data.amount || 0);
脚本需要根据字段 type 或 dataType 决定如何转换值时,使用 context.formSchema.field 或 context.formSchema.fields。
内置方法
服务端脚本和 server action 都会收到 StudioScriptContext。当前 runtime 已支持这些内置 client:
| Client | 常用方法 | 用途 |
|---|---|---|
context.runtime | executionId、traceId、assetId | 读取执行元数据 |
context.logger | info、warn、error、debug | 写入 Studio 运行日志 |
context.http | request、requestJson | 调用外部 HTTP API |
context.form | list、find、create、update、trash | 查询和写入表单记录 |
context.formSchema | get、fields、field | 查询表单和字段元数据 |
context.file | get、resolve、create | 解析或创建文件资产 |
context.approval | submitRecord、listTasks、decideTask | 处理审批运行时数据 |
context.organization | get、list、tree | 查询组织数据 |
context.department | get、list、tree | 查询部门数据 |
context.member | current、get、list | 查询成员数据 |
context.iam | currentRoleSummary、memberRoleSummary | 查询运行时角色摘要 |
完整方法签名见 Soln SDK。
页面资产清单
标准页面资产包含 soln.extension.json:
{
"entryFilePath": "src/App.tsx",
"serverActions": [
{
"name": "main",
"entryFilePath": "src/server/main.ts"
}
]
}
entryFilePath 指向浏览器页面入口。serverActions 声明同一个页面资产里的服务端 TypeScript 文件。
只有被 serverActions 声明的文件会按服务端 action 执行。其他 .ts 文件仍然是普通页面 helper。
页面入口
src/App.tsx 是浏览器页面:
export default function App() {
return (
<main
style={{
minHeight: '100vh',
display: 'grid',
placeItems: 'center',
padding: 24,
boxSizing: 'border-box',
fontFamily: 'Arial, sans-serif',
}}
>
Soln Studio Page
</main>
);
}
本地导入
Studio 资产会按受控的资产内模块编译。同一个资产内的文件使用相对路径导入:
import { formatMoney } from './shared/money';
import { OrderCard } from './client/components/OrderCard';
页面 client 文件可以导入 client 和 shared 文件,但不能直接导入 server action 文件。调用 server action 必须通过 @soln/studio。server action 可以通过相对路径导入 server 和 shared helper。
页面 client 文件也可以导入项目运行时内置的 Ant Design 6:
import { Button, Card, Table } from 'antd';
Ant Design 组件使用 antd 根包导入。其他 npm 包仍不可用,除非平台明确加入内置包。
构建 Studio 页面 UI 时,表单、表格、卡片、按钮、弹窗、抽屉、标签页、提示、空状态、加载状态和通用布局控件优先使用 Ant Design 6 组件。
内置依赖与编译失败
项目当前将 Ant Design 锁定为 6.5.2。依赖声明中的 ^6.5.2 不代表 npm 最新版本,实际安装版本由 Runtime 发布使用的锁文件确定。
antd 仅供页面客户端使用,服务端脚本和 server action 不可导入。组件必须从根包导入;不支持 antd/es/*、antd/lib/* 或 antd/dist/reset.css 等子路径。
根包导入若报 Cannot find module 'antd',且 require stack 指向 compiler-core.js,说明已部署的 Runtime 无法解析其内置依赖。Runtime 镜像必须把 antd 安装为生产依赖,并重新构建、升级;仅更新前端不能解决。本地完整仓库编译成功也不能证明租户容器具备该依赖。
这是阻塞性的编译错误。应保留用户要求的组件页面并说明需要修复 Runtime,不要静默改成原生元素后宣称依赖问题已修复。用户要求或同意时才采用替代 UI。Runtime 镜像构建会编译包含 Card、Table、Tag 的页面进行检查;已有安装仍需升级到包含修复的 Runtime 镜像。
按职责组织页面和脚本
优先使用多个职责清晰的文件,不要把整个功能都放进入口文件。修改前先读取资产目录结构,复用已有的合适模块。
- 页面:
src/App.tsx主要负责页面组合。独立 UI 区块放在src/client/components/,数据和状态 hooks 放在src/client/hooks/,服务端动作及业务服务放在src/server/,可供浏览器使用的类型和纯函数放在src/shared/。 - **脚本:**已配置的入口主要负责流程编排;校验、业务规则、外部 API 适配和可复用逻辑随复杂度增长拆到本地模块。
- 使用含义明确的英文文件名和相对路径导入。每个文件保持单一职责,接近 500–600 行时优先拆分。这是可维护性建议,不是编译器硬限制;简单、内聚的功能不必机械拆成许多小文件。
- 客户端不能导入服务端模块。共享模块必须适用于所有引用它的运行环境;页面通过
callAction调用已声明的服务端动作。 - 相关文件与导入关系一起修改,按当前绑定编译完整资产并检查全部诊断,再分别保存和发布。文件写入成功不等于编译或发布成功。
Server Actions
Server action 在 soln.extension.json 中声明,运行时可使用和脚本资产相同的 StudioScriptContext。
export default async function main(
payload: StudioScriptRuntimeContextPayload,
context: StudioScriptContext
) {
const orderNo = String(payload.input.orderNo || '').trim();
return {
ok: true,
orderNo,
executionId: context.runtime.executionId,
};
}
页面中调用 action:
import { callAction } from '@soln/studio';
async function runAction() {
const result = await callAction<
{ orderNo: string },
{ ok: boolean; orderNo: string }
>('main', {
orderNo: 'SO-1001',
});
console.log(result);
}
页面不会直接拿到用户 token。iframe 会发消息给宿主页面,由宿主页面使用当前登录态调用 server action。
页面运行上下文和主题也通过同一个内置 SDK 读取:
import { getRuntimeContext, getTheme } from '@soln/studio';
const context = getRuntimeContext();
const theme = getTheme();
const isDark = theme?.resolvedScheme === 'dark';
当 Studio 页面由表单查看页或编辑页的交互按钮打开时,当前宿主记录从运行上下文读取:
import { getRuntimeContext } from '@soln/studio';
const context = getRuntimeContext();
const recordId = context?.record?.id;
const draftData = context?.draft?.data || {};
const formApiName = context?.form?.apiName;
const selectedRecords = context?.selection?.records || [];
通过表单按钮打开页面时,当前表单值使用 context.draft.data。新建页的 context.record 为 null,查看页和编辑页也不会预加载其 data;Server action 需要持久化值时,应调用 context.form.get({ recordId: payload.record.id })。选中记录或子表行使用 context.selection.records、context.selection.childRows,当前子表行使用 context.child.row。除非页面明确是通过 URL 导航打开,否则不要从 URL query 参数读取当前记录。
预览与运行
在 Debug 面板可以编译、预览或运行当前资产。
- 脚本资产点击 运行 会执行
index.ts - 页面资产打开普通页面文件时,点击 运行 会预览页面
- 页面资产当前打开的文件如果声明在
serverActions中,点击 运行 会单独执行这个 action,并显示日志、错误和returnValue
例如,打开 src/server/main.ts 后点击 运行,只会执行 main action。打开 src/App.tsx 后点击 运行,会预览页面。
外部 HTTP 与当前成员
Server action 可以读取当前成员资料,并调用外部 HTTP:
export default async function createLoginUrl(
payload: StudioScriptRuntimeContextPayload,
context: StudioScriptContext
) {
const member = await context.member.current();
const data = (member?.data || {}) as Record<string, unknown>;
const phone = String(data.phone || '').trim();
const response = await context.http.request({
url: 'https://example.com/session',
method: 'POST',
json: { phone },
timeoutMs: 10000,
expect: 'json',
});
return {
ok: response.ok,
data: response.bodyJson,
};
}
确认真实成员字段时可以临时保留日志;字段确认后应删除临时日志。