审批处理扩展
要求
适用环境
- 私有部署
- Web
ONES 系统版本
7.x
依赖包版本
{
"devDependencies": {
"@ones/cli-plugin": "1.70.55"
},
"dependencies": {
"@ones-op/bridge": "^1.0.0",
"@ones-op/fetch": "1.70.55",
"@ones-op/node-types": "^1.0.0",
"@ones-op/sdk": "1.70.55",
"react": "~17.0.2",
"react-dom": "~17.0.2"
}
}
能力概述
审批处理扩展允许插件参与审批规则配置、同意或拒绝操作,以及审批流程结果展示。插件可以在这些接入点实现自己的业务处理,例如补充业务确认、调用外部系统、生成业务凭证或展示扩展处理结果。
具体业务形态由插件自行定义。ONES 只提供标准接入位置和调用合同,不限定插件必须实现某一种业务。
一个审批规则最多绑定一个审批处理 Provider。审批状态、审批意见和流程推进仍由 ONES 管理;插件产生的业务状态和结果由插件自行保存和解释。
能力边界
- 只支持接管
approve(同意)和reject(拒绝)。 - 加签、转交和撤销继续执行 ONES 标准流程。
- 单条审批和批量审批使用同一个操作接管插槽。
- 当前版本只提供 Web 插槽,不支持移动端或 H5。
- 插件必须在当前审批页面内完成业务处理并调用宿主提供的
onSubmit。 - 不支持页面跳转恢复、新窗口恢复或插件服务端异步回调自动推进审批。
- 插件不能直接修改 ONES 审批数据,也不能绕过
onSubmit自行调用内部审批接口。
工作原理
审批处理扩展包含三个前端插槽和一个后端校验函数:
| 接入点 | 名称 | 作用 |
|---|---|---|
| 规则配置插槽 | ones:approval:rule-plugin-settings | 在审批规则“更多设置”中配置插件接管范围和业务设置 |
| 操作接管插槽 | ones:approval:operation-takeover | 用户点击同意或拒绝时展示插件界面 |
| 流程展示插槽 | ones:approval:flow-row-extra | 在审批流程最后一列展示插件业务结果 |
| 后端校验函数 | validate | 在 ONES 写入审批意见前校验插件业务是否已经完成 |
完整处理过程如下:
- 管理员在审批规则中选择插件,并通过规则配置插槽保存
enabledActions和settings。 - ONES 将配置随审批规则版本保存。审批单创建后继续使用创建时冻结的规则版本和配置。
- 审批人点击已接管的同意或拒绝时,ONES 不再打开标准意见弹层,而是加载操作接管插槽。
- 插件完成并保存自己的业务结果,然后调用宿主
onSubmit({ comment })。 onSubmit继续执行 ONES 标准审批请求。ONES 后端在写入审批意见前调用插件validate。validate通过后,ONES 保存审批意见并推进流程;失败时阻断当前审批项。- 打开审批详情时,ONES 按流程行加载展示插槽,插件从自己的后端查询并渲染结果。
插件必须先保存业务结果,再调用 onSubmit。因为 onSubmit 执行期间,ONES 后端会立即调用插件的 validate。
配置功能扩展
在 config/plugin.yaml 中声明 approvalProcessHandler。以下配置还声明了两个插件接口和一张实体表,用于实现可运行的 Demo。
当前 Plugin 1.0 CLI 不会自动生成功能扩展配置,需要手动将以下内容加入 config/plugin.yaml。
apis:
- type: addition
methods:
- POST
url: /approval/completion/mark
function: markApprovalCompletion
- type: addition
methods:
- POST
url: /approval/completion/get
function: getApprovalCompletion
extension:
- approvalProcessHandler:
provider: aprvV1D1
funcs:
- name: validate
url: validate
slots:
- name: ones:approval:rule-plugin-settings
entryUrl: modules/approval-rule-settings-v1/index.html
- name: ones:approval:operation-takeover
entryUrl: modules/approval-operation-takeover-v1/index.html
- name: ones:approval:flow-row-extra
entryUrl: modules/approval-flow-row-extra-v1/index.html
config:
displayName: Plugin 1.0 审批 Demo
supportedActions:
- approve
- reject
flowColumnWidth: 120
storage:
entities:
- name: approval_completion_v1
attributes:
approval_uuid:
type: string
length: 64
required: true
node_uuid:
type: string
length: 64
required: true
opinion_uuid:
type: string
length: 64
required: true
action:
type: string
length: 16
required: true
comment:
type: text
required: false
default: ''
result:
type: string
length: 80
required: true
updated_at:
type: string
length: 32
required: true
配置约束
| 字段 | 要求 |
|---|---|
service.app_id | 必须是 8 位字母或数字。使用 ones create 生成的正式 App ID |
provider | 必须是 8 位字母或数字;发布后不要修改 |
funcs[].name | 必须为 validate |
funcs[].url | 必须为插件函数名 validate,不能填写 /approval/validate |
slots[].entryUrl | 必须指向打包后的 modules/<module-id>/index.html |
displayName | 去除首尾空白后不能为空,最多 80 个字符 |
supportedActions | 至少一个值,只能包含不重复的 approve、reject |
flowColumnWidth | 可选,默认 100;设置时必须是 80 到 160 的整数 |
settings 只能保存 JSON object,序列化后不能超过 32 KiB。不要在其中保存密钥、访问令牌或单次审批的运行结果。
validate 由 extension.funcs 注册并由 ONES 后端调用,不需要再在 apis 中声明普通 HTTP 路由。示例中的两个 apis 只供插件前端保存和查询 Demo 业务结果。
下文代码组成一个可运行的 Plugin 1.0 Demo。扩展名称、配置约束、Slot Context 和 validate 合同属于开放能力协议;Demo 的 API 路径、Entity Storage 结构、展示文案、样式和数据解包方式可以按插件业务替换。
公共类型
创建 web/src/approval/types.ts:
export type ApprovalPluginAction = 'approve' | 'reject'
export interface ApprovalRulePluginSettingsValue {
enabledActions: ApprovalPluginAction[]
settings: Record<string, unknown>
}
export interface ApprovalRulePluginSettingsContext {
mode: 'create' | 'edit' | 'copy'
supportedActions: ApprovalPluginAction[]
value: ApprovalRulePluginSettingsValue
onChange(value: ApprovalRulePluginSettingsValue): Promise<void>
onInit(config: { validate?: () => Promise<void> }): void
}
export interface ApprovalOperationTarget {
approvalUUID: string
nodeUUID: string
opinionUUID: string
ruleUUID: string
ruleVersion: number
}
export interface ApprovalOperationTakeoverContext {
action: ApprovalPluginAction
commentRequired: boolean
settings: Record<string, unknown>
approvals: ApprovalOperationTarget[]
onSubmit(value: { comment?: string }): Promise<{
succeededApprovalUUIDs: string[]
failedApprovalUUIDs: string[]
}>
onCancel(): void
}
export type ApprovalFlowRowType =
| 'submit'
| 'opinion'
| 'pending-users'
| 'cc'
| 'execute'
| 'revoke'
export interface ApprovalFlowRowExtraContext {
settings: Record<string, unknown>
approvalUUID: string
row: {
rowType: ApprovalFlowRowType
nodeUUID?: string
opinionUUID?: string
}
}
实现规则配置插槽
创建 web/src/approval/approval-rule-settings.tsx:
import { useExtensionContext } from '@ones-op/sdk'
import React, { useEffect, useRef, useState } from 'react'
import type {
ApprovalPluginAction,
ApprovalRulePluginSettingsContext,
ApprovalRulePluginSettingsValue,
} from './types'
export default function ApprovalRuleSettings() {
const context = useExtensionContext<ApprovalRulePluginSettingsContext>()
const supportedActions = context.supportedActions ?? []
const initialValue = context.value ?? {
enabledActions: supportedActions,
settings: { source: 'plugin-v1-demo' },
}
const [value, setValue] = useState<ApprovalRulePluginSettingsValue>(initialValue)
const valueRef = useRef(value)
useEffect(() => {
valueRef.current = value
}, [value])
useEffect(() => {
context.onInit({
validate: async () => {
if (valueRef.current.enabledActions.length === 0) {
throw new Error('请至少选择一个审批操作')
}
},
})
}, [context])
const updateAction = async (action: ApprovalPluginAction, checked: boolean) => {
const enabledActions = checked
? [...new Set([...value.enabledActions, action])]
: value.enabledActions.filter((item) => item !== action)
if (enabledActions.some((item) => !supportedActions.includes(item))) {
throw new Error('插件不支持该审批操作')
}
const next = {
enabledActions,
settings: {
...value.settings,
source: 'plugin-v1-demo',
},
}
await context.onChange(next)
setValue(next)
}
return (
<section>
<p>当前模式:{context.mode}。请选择需要插件接管的审批操作。</p>
{supportedActions.map((action) => (
<label key={action} style={{ display: 'block' }}>
<input
type="checkbox"
checked={value.enabledActions.includes(action)}
onChange={(event) => void updateAction(action, event.target.checked)}
/>
接管{action === 'approve' ? '同意' : '拒绝'}
</label>
))}
</section>
)
}
Context 字段说明
| 字段 | 说明 |
|---|---|
mode | 当前规则入口:新建 create、编辑 edit 或复制 copy |
supportedActions | Provider 静态声明支持的操作上限 |
value.enabledActions | 当前规则实际启用的接管操作,必须是 supportedActions 的子集 |
value.settings | 当前规则的插件业务设置 |
onChange | 将完整的 enabledActions 和 settings 交给宿主 |
onInit | 注册可选的规则配置校验函数 |
插件应在初始化后调用一次 onInit。宿主会在离开“更多设置”和最终保存规则前调用已注册的 validate。onChange 必须提交完整值,不要只提交变化字段。
mode 用于帮助插件正确区分规则配置的初始化方式:
create:创建新规则,插件应按自己的业务初始化默认配置。edit:编辑已有规则,插件应基于value展示并修改当前配置。copy:复制已有规则,value包含被复制规则的配置;插件可以保留通用设置,并重置不应复用的外部绑定、业务标识或一次性数据。
插件不能只根据 value 是否为空判断入口。否则复制规则可能被误判为编辑,导致新规则错误复用原规则的外部业务绑定;编辑规则也可能被误判为新建并覆盖已有配置。
实现操作接管插槽
当用户点击当前规则已启用接管的“同意”或“拒绝”时,ONES 不打开标准审批意见弹层,而是加载该插槽。插件完成自己的业务后,必须通过宿主 onSubmit 继续执行标准审批。
Context 字段说明
| 字段 | 说明 |
|---|---|
action | 当前操作,只可能是 approve 或 reject |
commentRequired | 当前审批规则是否要求审批意见必填;插件用于控制交互 ,宿主仍会再次校验 |
settings | 创建审批单时冻结的插件规则设置 |
approvals | 本次单条或批量审批目标;单条时数组长度为 1 |
onSubmit | 由宿主提交标准审批,并返回本次成功和失败的审批单 UUID |
onCancel | 取消本次接管操作,不改变审批状态 |
approvals 中每个目标包含审批单、当前节点、当前意见以及规则版本标识。插件通常使用 approvalUUID + opinionUUID + action 关联并持久化自己的业务结果。
推荐按以下顺序处理:
- 校验插件界面的输入。
- 为
approvals中的目标完成并保存插件业务。 - 确认插件后端已经能够通过
validate查询到业务完成状态。 - 整批只调用一次
onSubmit({ comment })。 - 根据返回的成功和失败 UUID 更新插件状态或提示重试。
Demo:调用插件自己的后端
创建 web/src/approval/plugin-api.ts:
import { OPFetch } from '@ones-op/fetch'
import type { ApprovalOperationTarget, ApprovalPluginAction } from './types'
function parseJSON(value: unknown): unknown {
if (typeof value !== 'string') return value
try {
return JSON.parse(value)
} catch {
return value
}
}
function readBody<T>(response: unknown): T {
let value = response
for (let depth = 0; depth < 4; depth += 1) {
value = parseJSON(value)
if (!value || typeof value !== 'object') break
if ('body' in value) {
value = (value as { body?: unknown }).body
} else if ('data' in value) {
value = (value as { data?: unknown }).data
} else {
break
}
}
return parseJSON(value) as T
}
export async function markApprovalCompletions(
action: ApprovalPluginAction,
comment: string,
approvals: ApprovalOperationTarget[],
): Promise<void> {
await OPFetch('/project/api/project/approval/completion/mark', {
method: 'POST',
data: { action, comment, approvals },
})
}
export async function getApprovalCompletion(
approvalUUID: string,
opinionUUID: string,
): Promise<string | null> {
const response = await OPFetch('/project/api/project/approval/completion/get', {
method: 'POST',
data: { approvalUUID, opinionUUID },
})
return readBody<{ value?: string | null }>(response)?.value ?? null
}
Plugin 1.0 网关可能保留 PluginResponse.body,也可能将其序列化,因此 Demo 同时兼容 data、body 和 JSON 字符串包装。该解包逻辑属于参考实现,不是审批处理扩展合同的一部分。
Demo:操作接管界面
创建 web/src/approval/approval-operation-takeover.tsx:
import { useExtensionContext } from '@ones-op/sdk'
import React, { useState } from 'react'
import { markApprovalCompletions } from './plugin-api'
import type { ApprovalOperationTakeoverContext } from './types'
import './approval-operation-takeover.css'
export default function ApprovalOperationTakeover() {
const context = useExtensionContext<ApprovalOperationTakeoverContext>()
const [comment, setComment] = useState('')
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState('')
const actionLabel = context.action === 'reject' ? '拒绝' : '同意'
const submit = async () => {
if (submitting) return
const normalizedComment = comment.trim()
if (context.commentRequired && !normalizedComment) {
setError('请输入审批意见')
return
}
setSubmitting(true)
setError('')
try {
// 必须先保存插件业务结果,供 ONES 后端 validate 查询。
await markApprovalCompletions(context.action, normalizedComment, context.approvals)
const result = await context.onSubmit({
comment: normalizedComment,
})
if (result.failedApprovalUUIDs.length > 0) {
setError(`${result.failedApprovalUUIDs.length} 个审批项处理失败`)
setSubmitting(false)
}
} catch (reason) {
setError(reason instanceof Error ? reason.message : '审批操作失败')
setSubmitting(false)
}
}
return (
<div className="approval-plugin-modal-mask">
<section role="dialog" aria-modal="true">
<h2>Plugin 1.0 审批接管</h2>
<p>
当前操作:{actionLabel},审批单数量:
{context.approvals.length}
</p>
<textarea
value={comment}
disabled={submitting}
onChange={(event) => setComment(event.target.value)}
/>
{error ? <p role="alert">{error}</p> : null}
<button disabled={submitting} onClick={context.onCancel}>
取消
</button>
<button disabled={submitting} onClick={() => void submit()}>
确认并{actionLabel}
</button>
</section>
</div>
)
}
建议为弹窗遮罩设置固定定位和足够的层级:
.approval-plugin-modal-mask {
position: fixed;
z-index: 999;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(20, 32, 44, 0.48);
}
onSubmit 语义
- 单条审批时
approvals.length为1。 - 批量审批只加载一次插槽,
approvals包含本次可接管的全部审批目标。 - 插件完成整批业务后只调用一次
onSubmit。 onSubmit复用 ONES 原有参数校验、防重复提交、提示、批量结果处理和详情刷新。- 全部失败时
onSubmitreject;全部成功或部分成功时 resolve。 - 插件应根据
succeededApprovalUUIDs和failedApprovalUUIDs处理批量结果。 onCancel关闭插件界面,不改变审批状态。
实现流程展示插槽
启用审批处理扩展后,ONES 在审批流程日期后增加扩展列,并为每条当前可见的流程行加载该插槽。插件根据 Context 查询自己的后端并决定当前单元格显示的内容。
Context 字段说明
| 字段 | 说明 |
|---|---|
settings | 创建审批单时冻结的插件规则设置 |
approvalUUID | 当前审批单 UUID |
row.rowType | 当前流程行类型 |
row.nodeUUID | 当前节点 UUID;仅部分行提供 |
row.opinionUUID | 当前审批意见 UUID;真实审批意见行提供 |
Demo:流程扩展列
创建 web/src/approval/approval-flow-row-extra.tsx:
import { useExtensionContext } from '@ones-op/sdk'
import React, { useEffect, useState } from 'react'
import { getApprovalCompletion } from './plugin-api'
import type { ApprovalFlowRowExtraContext } from './types'
export default function ApprovalFlowRowExtra() {
const context = useExtensionContext<ApprovalFlowRowExtraContext>()
const [value, setValue] = useState<string | null>(null)
useEffect(() => {
let active = true
const opinionUUID = context.row?.opinionUUID
if (context.row?.rowType !== 'opinion' || !context.approvalUUID || !opinionUUID) {
setValue(null)
return () => {
active = false
}
}
void getApprovalCompletion(context.approvalUUID, opinionUUID)
.then((result) => {
if (active) setValue(result)
})
.catch((reason) => {
console.error('加载审批扩展结果失败', reason)
if (active) setValue(null)
})
return () => {
active = false
}
}, [context.approvalUUID, context.row?.opinionUUID, context.row?.rowType])
return value ? <span>{value}</span> : null
}
ONES 会为每条可见流程行加载该插槽。并非所有行都有 nodeUUID 或 opinionUUID,插件必须根据 rowType 判断可用字段:
rowType | 可用标识 |
|---|---|
submit | approvalUUID |
opinion | approvalUUID、nodeUUID、opinionUUID |
pending-users | approvalUUID、nodeUUID |
cc | approvalUUID、nodeUUID |
execute | approvalUUID、nodeUUID |
revoke | approvalUUID |
Demo 只在真实意见行展示插件结果。生产插件可以根据自己的业务决定哪些行显示内容,但不能假设所有行都有意见 UUID。
注册三个前端模块
为三个 entryUrl 创建同名源码目录:
web/src/modules/
├── approval-rule-settings-v1/index.tsx
├── approval-operation-takeover-v1/index.tsx
└── approval-flow-row-extra-v1/index.tsx
每个入口使用 OPProvider 向组件提供扩展 Context。规则配置入口示例如下:
import { lifecycle, OPProvider } from '@ones-op/bridge'
import React from 'react'
import ReactDOM from 'react-dom'
import ApprovalRuleSettings from '../../approval/approval-rule-settings'
const root = document.getElementById('ones-mf-root')
if (!root) throw new Error('Plugin module root is missing')
ReactDOM.render(
<OPProvider>
<ApprovalRuleSettings />
</OPProvider>,
root,
)
lifecycle.onDestroy(() => {
ReactDOM.unmountComponentAtNode(root)
})
另外两个入口使用相同结构,分别渲染 ApprovalOperationTakeover 和 ApprovalFlowRowExtra。
实现插件后端
以下 Demo 使用 Entity Storage 保存插件业务结果。实际业务也可以使用插件自己的服务端存储,但 validate 和流程展示必须查询同一权威数据源。
在插件模板生成的 backend/src/index.ts 中保留原有生命周期函数,并添加以下实现:
import type { PluginRequest, PluginResponse } from '@ones-op/node-types'
import { storage } from '@ones-op/sdk/backend'
import { createHash } from 'node:crypto'
type Action = 'approve' | 'reject'
interface Completion {
approval_uuid: string
node_uuid: string
opinion_uuid: string
action: Action
comment: string
result: string
updated_at: string
}
interface ApprovalValidateRequest {
team_uuid?: string
user_uuid?: string
action?: Action
rule?: {
rule_uuid: string
rule_version: number
settings: Record<string, unknown>
}
approval?: {
approval_uuid?: string
node_uuid?: string
opinion_uuid?: string
}
comment?: string
}
const entity = storage.entity<Completion>('approval_completion_v1')
function key(approvalUUID: string, opinionUUID: string): string {
return createHash('sha256').update(approvalUUID).update('\0').update(opinionUUID).digest('hex')
}
function hasPayload(value: unknown): boolean {
if (value == null) return false
if (typeof value === 'string') return value.trim().length > 0
if (value instanceof Uint8Array) return value.length > 0
if (typeof value === 'object') {
return Object.keys(value).length > 0
}
return true
}
function decode<T>(value: unknown): T {
if (typeof value === 'string') return JSON.parse(value)
if (value instanceof Uint8Array) {
return JSON.parse(new TextDecoder().decode(value))
}
return (value ?? {}) as T
}
function requestBody<T>(requestOrBody: PluginRequest | T): T {
const request = requestOrBody as PluginRequest
if (
!requestOrBody ||
typeof requestOrBody !== 'object' ||
!('body' in requestOrBody || 'reqBody' in requestOrBody || 'headers' in requestOrBody)
) {
return requestOrBody as T
}
return decode<T>(hasPayload(request.body) ? request.body : request.reqBody)
}
export async function markApprovalCompletion(request: PluginRequest): Promise<PluginResponse> {
const input = requestBody<{
action: Action
comment?: string
approvals: Array<{
approvalUUID: string
nodeUUID: string
opinionUUID: string
}>
}>(request)
if (!['approve', 'reject'].includes(input.action) || !input.approvals?.length) {
throw new Error('审批接管参数不完整')
}
await Promise.all(
input.approvals.map((target) =>
entity.set(key(target.approvalUUID, target.opinionUUID), {
approval_uuid: target.approvalUUID,
node_uuid: target.nodeUUID,
opinion_uuid: target.opinionUUID,
action: input.action,
comment: input.comment?.trim() ?? '',
result: 'Plugin 1.0 Demo',
updated_at: new Date().toISOString(),
}),
),
)
return {
body: {
markedApprovalUUIDs: input.approvals.map((target) => target.approvalUUID),
},
}
}
export async function getApprovalCompletion(request: PluginRequest): Promise<PluginResponse> {
const input = requestBody<{
approvalUUID?: string
opinionUUID?: string
}>(request)
if (!input.approvalUUID || !input.opinionUUID) {
return { body: { value: null } }
}
const record = await entity.get(key(input.approvalUUID, input.opinionUUID))
return { body: { value: record?.result ?? null } }
}
export async function validate(
request: PluginRequest | ApprovalValidateRequest,
): Promise<PluginResponse> {
const input = requestBody<ApprovalValidateRequest>(request)
const approvalUUID = input.approval?.approval_uuid
const opinionUUID = input.approval?.opinion_uuid
if (!approvalUUID || !opinionUUID || !input.action) {
return {
body: {
error: {
level: 'error',
reason: '插件审批上下文不完整',
},
},
}
}
const record = await entity.get(key(approvalUUID, opinionUUID))
if (!record || record.action !== input.action) {
return {
body: {
error: {
level: 'error',
reason: '请先完成插件要求的审批处理',
},
},
}
}
return { body: {} }
}
validate 请求
ONES 在原审批权限、状态和当前待处理人校验通过后,第一次写入审批意见前调用 validate:
| 字段 | 说明 |
|---|---|
team_uuid | 当前团队 UUID |
user_uuid | 当前审批操作人 UUID |
rule.rule_uuid | 当前审批单使用的审批规则 UUID |
rule.rule_version | 当前审批单创 建时冻结的规则版本 |
rule.settings | 当前审批单创建时冻结的插件规则设置 |
approval.approval_uuid | 当前审批单 UUID |
approval.node_uuid | 当前待处理节点 UUID |
approval.opinion_uuid | 当前操作人的待处理审批意见 UUID |
action | 当前操作:approve 或 reject |
comment | 插件通过 onSubmit 提交的审批意见;未填写时可以不存在 |
{
"team_uuid": "team_12345678",
"user_uuid": "user_12345678",
"rule": {
"rule_uuid": "rule_12345678",
"rule_version": 2,
"settings": {
"source": "plugin-v1-demo"
}
},
"approval": {
"approval_uuid": "approval_12345678",
"node_uuid": "node_12345678",
"opinion_uuid": "opinion_12345678"
},
"action": "approve",
"comment": "同意"
}
校验通过时返回空对象:
{}
校验不通过时返回:
{
"error": {
"reason": "请先完成插件要求的审批处理",
"level": "error"
}
}
reason 必须是非空纯文本,最多 500 个字符;level 当前只能为 error。函数调用失败、超时、非法 JSON 或非法错误结构都会阻断当前审批项。
批量审批时,前端只加载一次插槽并调用一次 onSubmit,ONES 后端仍会逐个审批项调用 validate。因此 validate 必须无副作用、可重复调用并保持高效。
数据存储要求
- 插件业务结果必须保存在插件后端,不能只保存在
localStorage或当前浏览器状态中。 - 建议使用
approval_uuid + opinion_uuid + action作为业务幂等标识。 - Entity Storage Key 必须满足
^[_a-z0-9]{1,64}$。示例使用审批单 UUID 和意见 UUID 的 SHA-256 摘要。 - 不要直接使用包含冒号等字符的 UUID 拼接值作为 Entity Storage Key。
- 流程展示接口需要保证不同账号、浏览器和设备查询到一致结果。
- 生产插件应校验调用者身份和数据查看权限,不要向无权限用户返回插件业务结果。