API 中心(ApiConfig / ApiGroup)定义编写指南¶
目标:Agent 直接生成/修改 workspace API 中心定义(分组 + 接口),使 magic-api 可被第三方/前台调用。知识以本文为准。不写原理;不依赖外链。
iScript:写 responseScript 时,先读 iscript-usage(含 GraalVM 差异) → api.md;本文只定路由/分组与落盘。
术语:实体类 cn.myapps.core.common.model.api.ApiConfig、cn.myapps.core.common.model.api.group.ApiGroup;产品「API 中心 / 高级工具-API中心」;运行入口 MagicApiController。是软件级 自定义 HTTP 接口(iScript 写响应),**不是**平台内置 /api/runtime /api/designtime,**不是**外部接口 .extinterface(那是脚本里调出站 HTTP/WebService/JAR 的封装),**不是**企业域 Open API Secret。
依赖:挂在**软件(Application)下。设计态新建 API **必须先有 ApiGroup(校验「请选择API分组!」)。parentId/apiGroup = 分组 id。
产出物一览¶
| 部分 | 落盘 | 形态 |
|---|---|---|
| API 分组 | api/{分组名}.apigroup/{分组名}.apigroup |
目录型;JAXB 根 ApiGroup;id 为根属性 |
| API | api/{分组名}.apigroup/{API名}.api |
单文件;JAXB 根 Api;id 为根属性 |
# 推荐(设计态现行 / 与 Widget 分组同构)
storage/workspace/{软件名}.application/api/{分组名}.apigroup/{分组名}.apigroup
storage/workspace/{软件名}.application/api/{分组名}.apigroup/{API名}.api
# 历史/样例常见:分组元数据为扁平单文件,API 直接躺在 api/ 下(仍可被后缀扫描载入)
storage/workspace/{软件名}.application/api/{分组名}.apigroup
storage/workspace/{软件名}.application/api/{API名}.api
# 极旧:无分组(apiGroup 空、parentId=软件 id)
storage/workspace/{软件名}.application/api/{API名}.api
路径相对 storage/workspace。
常量(ModelSuffix) |
值 |
|---|---|
API_PATH_SUFFIX / API_FILE_SUFFIX |
api |
APIGROUP_PATH_SUFFIX / APIGROUP_FILE_SUFFIX |
apigroup |
APIGROUP_FILE_GROUP |
/api |
DAO 按后缀 .api / .apigroup 递归扫描**应用树,扁平与目录型都能载入。运行匹配靠 **HTTP 方法 + Ant 路径模板,不靠文件路径。
新建最低配置:
- 若不存在分组:先建
{name}.apigroup/+ 同名元数据(见 §7) - 写
{名}.api:id、name、parentId=分组 id、apiGroup=分组 id、status、requestType、requestUrl、encode=UTF-8、responseType、非空倾向responseScript requestUrl以/开头;可含{pathVar};与调用路径(去掉/magic-api/{软件名}后)一致- 同软件内 API
name不重名(设计态按 parent 列表校验「名称已经存在!」)
约定:
- JAXB;根元素
Api/ApiGroup(PascalCase);id在 根属性 responseScript用 CDATA- 文件名 =
name+.api;分组同理name+.apigroup;目录型时「目录名 = 元数据文件名」 name勿含/%\(落盘替换为=47/=37/=92)parentId:有分组时 =apiGroup(分组 id);无分组历史样例 = 软件 idapplicationid:基类常**无**落盘;靠路径归属 / API save 时 set- **勿**放到
module/下;永远在应用级api/ requestType小写:get/post/put/deletetags:字符串;UI 多标签用;拼接;可空串
1. 心智模型¶
软件 Application
└─ api/
├─ {分组}.apigroup/ ← ApiGroup(目录型推荐)
│ ├─ {分组}.apigroup ← 分组元数据
│ └─ {名}.api ← ApiConfig
├─ {分组}.apigroup ← 历史扁平分组文件
└─ {名}.api ← 历史扁平 / 无分组
外部/前台 HTTP
{method} {context}/magic-api/{软件名}{requestUrl}
│
▼ MagicApiController.execute
按软件名→applicationId;findByRequestUrl(method, path)
├─ status≠public 且无登录用户 → 401
├─ AntPathMatcher 抽 {pathVar} → ParamsTable
├─ body → params._content
└─ 跑 responseScript(空 Document + WebUser)
└─ 按 responseType 写出 json/xml/raw/binary
| 概念 | 说明 |
|---|---|
| 分组 ApiGroup | 设计器左侧/筛选;物理目录容器;无业务路由字段 |
requestUrl |
自定义路径模板,相对 magic-api/{软件名};**不含**软件名前缀 |
requestType |
HTTP 方法;与 request.getMethod() **忽略大小写**比较 |
status |
生命周期标签;鉴权只特殊对待 public(免登录),其它一律要登录用户 |
responseScript |
接口正文 iScript;返回值按 responseType 序列化 |
responseType |
json / xml / raw / binary |
encode |
设计器有字段;现行运行端写响应**固定 UTF-8**,生成写 UTF-8 即可 |
与相近能力区分:
| 对象 | 用途 |
|---|---|
.api / MagicApi |
**入站**自定义接口(本文) |
.extinterface |
**出站**接口定义,生成脚本片段给 iScript 调用外部系统 |
/api/runtime/* /api/designtime/* |
平台内置 REST,不是 workspace 文件 |
| 域 Open API Secret | 第三方调平台开放 API 的令牌密钥,不是 .api |
2. ApiConfig 属性 → .api XML¶
属性表¶
| 属性 | XML | 类型 | 默认/样例 | 说明 |
|---|---|---|---|---|
id |
根属性 | string | 须生成 | __ + 短 UUID |
name |
子元素 | string | — | 必填;文件名;同 parent 下唯一 |
parentId |
子元素 | string | — | = 分组 id(设计态 create/update 强制 setParentId(apiGroup)) |
applicationid |
常不落盘 | string | API 侧 = 软件 id | |
description/remark |
CDATA | string | 基类;样例常省 | |
status |
子元素 | string | published |
见 §3 状态表 |
apiGroup |
子元素 | string | — | = 分组 id;设计态空则拒存 |
encode |
子元素 | string | UTF-8 |
UI 仅此选项;运行常忽略 |
requestType |
子元素 | string | get |
get/post/put/delete |
requestUrl |
子元素 | string | — | 必填语义;如 /hello、/departments/{departmentId} |
tags |
子元素 | string | 可空 | ; 分隔多标签 |
responseType |
子元素 | string | json |
json/xml/raw/binary |
responseScript |
CDATA | string | — | 响应 iScript |
id 生成¶
统一规则:__ + 短 UUID。示例:__MpEzTToulqZtNEFisw6。须全局/库内不冲突(.api / .apigroup 同)。
路径计算(保存/搬家)¶
ApiConfig.getPath():
- 若
apiGroup非空:解析分组 →apiGroup.getPath() + "/" + name + ".api"
即{软件}.application/api/{分组名}.apigroup/{API名}.api - 否则:
application.getPath() + "/" + name + ".api"(落在api/组下时由父路径决定)
ApiGroup 目录型 uri = getPath() + "/" + name + ".apigroup"(与 WidgetGroup/Module 同类)。
3. 常量表¶
status¶
| 值 | UI | 运行鉴权 |
|---|---|---|
published |
发布(默认) | 要登录 |
public |
公开 | 免登录(user==null 仍执行) |
development |
开发 | 要登录 |
test |
测试 | 要登录 |
abnormal |
异常 | 要登录 |
maintain |
维护 | 要登录 |
abandoned |
废弃 | 要登录 |
除 public 外,**全部**走「无用户 → 401 Not Permission」。abandoned/development 等**不会**被运行端自动拦截;想禁用不产生匹配则应删接口或改 requestUrl,勿假设状态会禁用路由。
requestType¶
| 值 | HTTP |
|---|---|
get |
GET |
post |
POST |
put |
PUT |
delete |
DELETE |
须小写写入 XML;匹配时 equalsIgnoreCase。
responseType¶
| 值 | 脚本应 return | 写出 |
|---|---|---|
json |
Map/Bean/Collection/JSONArray 等对象 |
application/json;经 ObjectMapper→JSONObject/JSONArray |
xml |
对象 | application/xml;对象→JSON→Staxon JSON→XML,外包类简单名标签 |
raw |
字符串 | 原文追加(无强制 Content-Type 分支) |
binary |
java.io.File(或整体 result 为 File) |
application/x-download 附件下载 |
设计器提示摘要:
- json:
return HashMap/ 业务对象 - xml:
returnVO/对象 - raw:
return "字符串" - binary:
return new java.io.File("...")
encode¶
仅 UTF-8。生成固定写它。
4. 运行时调用与匹配¶
URL¶
例:软件名 admintools,requestType=get,requestUrl=/departments/{departmentId}
GET /obpm/magic-api/admintools/departments/xxx
GET {runtimeContext}/magic-api/admintools/departments/xxx
控制器剥前缀逻辑(两套):
- URI 以
/obpm/magic-api/开头 → 去掉该前缀再去掉appName长度,余下为匹配 path - 否则:去掉
contextPath + "/magic-api/" + appName,余下为匹配 path
余下 path 通常仍带前导 /,须与 requestUrl 一致(含 /)。
appName = 软件 name(目录 {name}.application 的 name),不是 id。
匹配算法(findByRequestUrl)¶
- 列出该软件全部
.api method与requestType忽略大小写相等AntPathMatcher.match(requestUrl模板, 实际path)为真 → 取第一个 命中
因此:同方法下更泛的模板可能抢先;勿在两接口间写互相覆盖的模板;若冲突,依赖列表顺序(排序后遍历)不可控,应改路径。
路径变量:extractUriTemplateVariables → 写入 ParamsTable(可用 getParameter("departmentId"))。
Query string:由框架 getParams() 进 ParamsTable;不是 requestUrl 模板的一部分。UI placeholder 里的 ?age={age} 勿当真——查询参数直接用请求 query,不必写进 requestUrl。
Body:@RequestBody String content → params.setParameter("_content", content)。POST/PUT JSON 体用 getParameter("_content") 再 JSONObject.fromObject(...)。
脚本上下文¶
IRunner.initBSFManager(
new Document(), // 空文档,无表单 CURDOC 业务态
params, // 含 path 变量、query、_content
webUser, // public 时可为 null
empty ValidateMessage list
)
ScriptLabel(..., TYPE.API.RESPONSE="API:RESPONSE", apiId, apiName)
常用取参:
| 写法 | 含义 |
|---|---|
getParameter("x") |
ParamsTable |
CONTEXT.getParameter("x") |
同上常见写法 |
getParameter("_content") |
原始 body 字符串 |
getParamsTable().getHttpRequest() |
原生 request(日志等) |
getWebUser() / getApplication() / getDomainid() |
会话与应用(public 时 user 可能空,慎用) |
responseScript 返回 null → 不写响应体(仍 200 空)。异常 → printStackTrace 后向 writer 写 e.getMessage()。
5. XML 骨架¶
ApiGroup¶
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ApiGroup id="__GROUP_UUID">
<name>部门模块</name>
<parentId>SOFTWARE_UUID</parentId>
</ApiGroup>
路径:api/部门模块.apigroup/部门模块.apigroup
Api(JSON GET + path 变量)¶
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Api id="__API_UUID">
<name>根据id获取部门</name>
<parentId>__GROUP_UUID</parentId>
<status>published</status>
<apiGroup>__GROUP_UUID</apiGroup>
<encode>UTF-8</encode>
<requestType>get</requestType>
<requestUrl>/departments/{departmentId}</requestUrl>
<tags></tags>
<responseType>json</responseType>
<responseScript><![CDATA[(function(){
var departmentId = getParameter("departmentId");
// …查库/调工具类…
return resultMap;
})()]]></responseScript>
</Api>
Api(POST + body → JSON)¶
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Api id="__API_UUID">
<name>卡片保存</name>
<parentId>__GROUP_UUID</parentId>
<status>published</status>
<apiGroup>__GROUP_UUID</apiGroup>
<encode>UTF-8</encode>
<requestType>post</requestType>
<requestUrl>/create_card</requestUrl>
<tags></tags>
<responseType>json</responseType>
<responseScript><![CDATA[(function () {
var content = getParameter("_content");
var Apijson = new (Java.type('java.util.HashMap'))();
if (content == null || content === "") {
Apijson.put("errcode", 400);
Apijson.put("errmsg", "参数不能为空");
return Apijson;
}
var requestJson = (Java.type('net.sf.json.JSONObject')).fromObject(content);
// …业务…
Apijson.put("errcode", 200);
Apijson.put("errmsg", "ok");
return Apijson;
})()]]></responseScript>
</Api>
Api(public + raw)¶
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Api id="__API_UUID">
<name>消息接收</name>
<parentId>__GROUP_UUID</parentId>
<status>public</status>
<apiGroup>__GROUP_UUID</apiGroup>
<encode>UTF-8</encode>
<requestType>get</requestType>
<requestUrl>/MsgReceived</requestUrl>
<tags></tags>
<responseType>raw</responseType>
<responseScript><![CDATA[(function(){
var echo = getParameter("echostr");
return echo;
})()]]></responseScript>
</Api>
Api(binary)¶
...
<responseType>binary</responseType>
<responseScript><![CDATA[(function(){
var file = new Packages.java.io.File("D://test.txt");
return file;
})()]]></responseScript>
binary 须返回存在的 File;走下载头 Content-Disposition: attachment。
6. 设计态 / 运行态 HTTP(定位用)¶
设计态基路径(典型):${designerContext}/api/designtime/applications
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /{appId}/apis |
列表;query:name、apiGroupId、requestType、status、pageNo、linesPerPage |
| GET | /{appId}/apis/{apiId} |
详情 |
| POST | /{appId}/apis |
新建;parentId=apiGroup;空分组 →「请选择API分组!」;重名 →「名称已经存在!」 |
| PUT | /{appId}/apis/{apiId} |
更新;回写字段见控制器;uri 置 null 以重算路径 |
| DELETE | /{appId}/apis |
body:id JSON 数组 |
分组:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /{appId}/apigroups |
列表;parent 实为软件 id |
| GET | /{appId}/apigroups/{apiGroupId} |
详情 |
| POST | /{appId}/apigroups |
新建;parentId=applicationId |
| PUT | /{appId}/apigroups/{apiGroupId} |
主要改 name |
| DELETE | /{appId}/apigroups |
body:id 数组 |
运行态:
| 方法 | 路径 | 说明 |
|---|---|---|
ANY(由各 .api 的 requestType 决定) |
{runtimeContext}/magic-api/{软件名}/** |
MagicApiController;无独立 CRUD |
设计器 UI 展示完整前缀:/magic-api/{{软件名}} + 用户填的 requestUrl。
7. ApiGroup — 本文内自足¶
实体:ApiGroup。根 ApiGroup。无额外业务字段,仅 id/name/parentId(+ 基类可空描述)。
getFileGroup() = /api;getPath()(parent 为 Application 时)=
目录型 uri(APIGROUP 在「目录型后缀」列表):
骨架¶
见 §5。parentId = 软件 id。
注意¶
- 改分组
name= 改目录名;其下.api的getPath()依赖分组名,**勿随意改名**除非同步磁盘文件。 - 删除分组前应先迁走/删除子 API,否则遗留文件。
- 设计态校验:同软件下分组
name不重名(「名称已经存在!」)。 - 存量可能仅有扁平
xxx.apigroup文件且 API 平铺在api/:载入仍可;新建优先目录型,与admintools/demo-basic一致。
8. 与外部接口 / 平台 API 的边界¶
| 能力 | 载体 | 本文是否管 |
|---|---|---|
| 自定义入站 HTTP | .api + magic-api |
是 |
| 脚本调外部 REST/WS/JAR | .extinterface(extinterface/) |
否 |
| 设计器/运行内置资源 CRUD | /api/designtime /api/runtime |
否 |
| 第三方调平台标准开放接口 | 域 Open API Secret + 平台 REST | 否 |
| 表单/视图业务数据 API | Document/View 控制器 | 否 |
.extinterface 落盘组常为应用级 extinterface/;字段含 type(restful/webservice/jar)、requestUrl、exampleCode/jsCode 等,没有 responseScript 入站执行模型。
9. 检查清单(生成后自检)¶
- 已有
.apigroup,且 API 的apiGroup=parentId=该分组 id - 文件在
api/{分组}.apigroup/{name}.api(推荐) - 根元素
Api/ApiGroup(PascalCase,非 camelCase) -
requestType小写;requestUrl以/开头 - 调用 URL =
/magic-api/{软件名}+requestUrl(软件名而非 id) - 免登录需求 →
status=public;否则用published等且调用带会话 -
responseType与 return 类型匹配(raw→String,binary→File,json/xml→对象) - POST/PUT 读 body 用
_content - path 变量名与
{var}一致,并用getParameter - 同方法下无互相覆盖的 Ant 模板
- 同 parent 无同名 API;name 无
/\% -
encode=UTF-8;responseScript为 CDATA
10. 典型生成顺序¶
- 确认软件目录
{app}.application/存在(记下软件 name 与 id) - 建分组
api/{G}.apigroup/{G}.apigroup(parentId=软件 id) - 定 HTTP 方法、
requestUrl、鉴权status、responseType - 写
.api到分组目录;脚本内完成取参与返回 - 用对应 METHOD 调
{runtime}/magic-api/{软件名}{requestUrl}验证 - 非
public时带登录 cookie/token;public可匿名
附录 A:后缀常量(ModelSuffix)¶
| 常量 | 值 |
|---|---|
| API_PATH_SUFFIX / API_FILE_SUFFIX | api |
| APIGROUP_PATH_SUFFIX / APIGROUP_FILE_SUFFIX | apigroup |
| APIGROUP_FILE_GROUP | /api |
类映射:ModelSuffixEx → .api→ApiConfig,.apigroup→ApiGroup。
附录 B:设计器控制器类名¶
| 类 | 职责 |
|---|---|
ApiDesignTimeController |
API CRUD |
ApiGroupDesignTimeController |
分组 CRUD |
ApiDesignTimeServiceImpl |
query / findByRequestUrl |
FileSystemApiDAO |
过滤 name/requestType/requestUrl/status/apiGroupId |
MagicApiController |
运行态执行 |
包路径摘要:cn.myapps.designtime.api…、cn.myapps.core.common.model.api…、cn.myapps.runtime.common.controller.MagicApiController。
附录 C:常见失败¶
| 现象 | 原因 |
|---|---|
| 设计器保存报请选择API分组 | 缺 apiGroup |
| 名称已经存在 | 同 parent 下 name 重复 |
| 404 Api Not Found | 软件名错;或 method/path 无匹配;或软件未扫到该 .api |
| 401 Not Permission | status≠public 且未登录 |
| raw 响应怪 / ClassCast | responseType=raw 但 return 非 String |
| binary 失败 | 未 return File,或文件不存在/SecurityFile 不可读 |
| json 空串字段 | 运行端把 null 序列化成 "" |
| POST 取不到体 | 未读 _content,或客户端未传 body |
| path 变量空 | requestUrl 未写 {名},或实际 URL 段与模板不一致 |
| 改组名后路径乱 | 磁盘目录未随 name 搬家 |
| 与另一 API 行为错乱 | Ant 模板冲突,先匹配到错误接口 |
| 根标签解析失败 | 写成 <api>/<ApiConfig>;须 <Api> / <ApiGroup> |
附录 D:脚本最小模式速查¶
// GET + path var → JSON
(function(){
var id = getParameter("id");
var m = new (Java.type('java.util.HashMap'))();
m.put("id", id);
return m;
})()
// POST JSON body
(function(){
var content = getParameter("_content");
var jo = (Java.type('net.sf.json.JSONObject')).fromObject(content);
var m = new (Java.type('java.util.HashMap'))();
m.put("echo", jo.get("x"));
return m;
})()
可 #include "baselibrary" / #include "sysfunction" 等软件宏库(与其它 iScript 一致),按目标软件已有 repository 使用。