跳到主要内容

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_jsindex.ts服务端自动化、表单写入、审批服务调用、集成逻辑
页面client_pagesrc/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.runtimeexecutionId、traceId、assetId读取执行元数据
context.loggerinfo、warn、error、debug写入 Studio 运行日志
context.httprequest、requestJson调用外部 HTTP API
context.formlist、find、create、update、trash查询和写入表单记录
context.formSchemaget、fields、field查询表单和字段元数据
context.fileget、resolve、create解析或创建文件资产
context.approvalsubmitRecord、listTasks、decideTask处理审批运行时数据
context.organizationget、list、tree查询组织数据
context.departmentget、list、tree查询部门数据
context.membercurrent、get、list查询成员数据
context.iamcurrentRoleSummary、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,
};
}

确认真实成员字段时可以临时保留日志;字段确认后应删除临时日志。