openxiangda 1.0.155 → 1.0.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/lib/cli.js +176 -1
- package/lib/open-api.js +256 -0
- package/lib/skills.js +16 -2
- package/openxiangda-skills/SKILL.md +7 -3
- package/openxiangda-skills/references/openxiangda-api.md +2 -2
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/troubleshooting.md +12 -0
- package/openxiangda-skills/skills/openxiangda-open-api/SKILL.md +57 -0
- package/openxiangda-skills/skills/openxiangda-open-api/agents/openai.yaml +4 -0
- package/openxiangda-skills/skills/openxiangda-open-api/references/backend-authentication.md +63 -0
- package/openxiangda-skills/skills/openxiangda-open-api/references/dingtalk-api.openapi.json +15653 -0
- package/package.json +6 -3
- package/packages/sdk/dist/components/index.cjs +2 -1
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.mjs +2 -1
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/index.cjs +54 -15
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.mjs +54 -15
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +54 -15
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.mjs +54 -15
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +2 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +2 -0
- package/templates/openxiangda-react-spa/AGENTS.md +2 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +2 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +2 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
OpenXiangda is a lightweight CLI and skill package for private low-code platforms.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Normal OpenXiangda app development uses platform-user login tokens through `/openxiangda-api/v1`; it does not use AK/SK. External backend and third-party integrations use the separate `openxiangda-open-api` skill, `/dingtalk-api/v1.0`, and a platform-managed AK/SK credential.
|
|
6
6
|
|
|
7
7
|
Private platform routing is fixed: backend APIs are under `/service`, platform management is under `/platform`, and app runtime access is under `/view`. Passing a root domain such as `https://yida.wisejob.cn/` to the CLI is supported; OpenXiangda stores the API base as `https://yida.wisejob.cn/service`.
|
|
8
8
|
|
|
@@ -119,13 +119,18 @@ openxiangda organization department-list --profile dev --json
|
|
|
119
119
|
openxiangda organization account-list --profile dev --keyword alice --json
|
|
120
120
|
openxiangda permission snapshot --form-codes customer,orders --json
|
|
121
121
|
openxiangda permission audit --json
|
|
122
|
+
|
|
123
|
+
# external backend Open API contract and one-time credential management:
|
|
124
|
+
openxiangda open-api spec list --search submitFormData --json
|
|
125
|
+
openxiangda open-api spec describe /dingtalk-api/v1.0/forms/submitFormData --method post --json
|
|
126
|
+
openxiangda open-api credential list --profile dev --json
|
|
122
127
|
```
|
|
123
128
|
|
|
124
129
|
调用和测试类命令会检查 JSON envelope。HTTP 200 但 `code: "PUBLIC_GRANT_DENIED"`、`success: false` 或其他字符串业务错误码会被当作失败。
|
|
125
130
|
|
|
126
131
|
角色资源里的 `apiPermissionCodes` 依赖平台端已存在的 API 权限点。比如校区管理员需要维护应用角色时,manifest 应声明 `app:role:manage`;如果 `resource publish` 提示缺少这个权限点,说明当前平台租户的 `api_permissions` seed 不完整,通常是平台版本未包含 seed、服务启动 seed 没跑成功,或租户创建在 seed 之后。处理方式是升级/重启平台并补齐权限点 seed,或由平台管理员补入 `app:role:manage`、`app:page-permission-group:manage`、`app:form-permission-group:manage` 等应用级权限点;不要为了通过发布删除 manifest 里的权限声明。
|
|
127
132
|
|
|
128
|
-
Codex skills are installed separately from login/profile state. Run `openxiangda skill install` after installing or linking the CLI. It installs the `openxiangda` root skill plus the
|
|
133
|
+
Codex skills are installed separately from login/profile state. Run `openxiangda skill install` after installing or linking the CLI. It installs the `openxiangda` root skill plus the 9 top-level subskills into `${CODEX_HOME:-~/.codex}/skills` by default:
|
|
129
134
|
|
|
130
135
|
- `openxiangda`
|
|
131
136
|
- `openxiangda-core`
|
|
@@ -136,6 +141,7 @@ Codex skills are installed separately from login/profile state. Run `openxiangda
|
|
|
136
141
|
- `openxiangda-workflow-automation`
|
|
137
142
|
- `openxiangda-permission-settings`
|
|
138
143
|
- `openxiangda-inspect`
|
|
144
|
+
- `openxiangda-open-api`
|
|
139
145
|
|
|
140
146
|
Use `openxiangda skill install --dest <skills-dir>` to target a different Codex skills directory. If a same-name skill exists but was not installed by OpenXiangda, the command refuses to overwrite it unless `--force` is provided. Restart Codex after installing skills.
|
|
141
147
|
|
package/lib/cli.js
CHANGED
|
@@ -37,6 +37,13 @@ const {
|
|
|
37
37
|
renderResourceExplain,
|
|
38
38
|
} = require('./design-gates');
|
|
39
39
|
const { buildDesignReview, renderDesignReview } = require('./design-review');
|
|
40
|
+
const {
|
|
41
|
+
describeOpenApiOperation,
|
|
42
|
+
listOpenApiOperations,
|
|
43
|
+
listOpenApiTags,
|
|
44
|
+
loadOpenApiDocument,
|
|
45
|
+
redactOpenApiSecret,
|
|
46
|
+
} = require('./open-api');
|
|
40
47
|
const {
|
|
41
48
|
appendBypassLog,
|
|
42
49
|
archiveSddChange,
|
|
@@ -83,6 +90,7 @@ async function main(argv) {
|
|
|
83
90
|
if (command === 'platform') return platform(rest);
|
|
84
91
|
if (command === 'doctor') return doctor(rest);
|
|
85
92
|
if (command === 'contract') return contract(rest);
|
|
93
|
+
if (command === 'open-api') return openApi(rest);
|
|
86
94
|
if (command === 'design') return design(rest);
|
|
87
95
|
if (command === 'sdd') return sdd(rest);
|
|
88
96
|
if (command === 'workspace') return workspace(rest);
|
|
@@ -126,6 +134,8 @@ Usage:
|
|
|
126
134
|
openxiangda auth status|refresh|logout [--profile name]
|
|
127
135
|
openxiangda doctor [--profile name] [--app-type APP_XXX] [--release] [--json]
|
|
128
136
|
openxiangda contract list|explain <contract-name> [--json]
|
|
137
|
+
openxiangda open-api spec tags|list|describe [operationId|path] [--method post] [--tag name] [--search text] [--json]
|
|
138
|
+
openxiangda open-api credential list|get|create|update|rotate-secret [id] [--profile name] [--show-secret] [--yes] [--json]
|
|
129
139
|
openxiangda design gates|template|review [--topic code] [--json]
|
|
130
140
|
openxiangda sdd init|migrate|propose|approve|status|context|verify|sync|archive [change] [--json]
|
|
131
141
|
openxiangda env [--profile name]
|
|
@@ -186,7 +196,7 @@ Usage:
|
|
|
186
196
|
openxiangda skill status [--agent codex|claude|qoder|dual] [--dest <skills-dir>] [--json]
|
|
187
197
|
openxiangda skill bootstrap [<dir>] [--force] [--dry-run] [--json]
|
|
188
198
|
|
|
189
|
-
OpenXiangda
|
|
199
|
+
OpenXiangda 应用开发使用普通用户登录 token,不需要 AK/SK;外部后端集成使用 open-api skill 与 AK/SK。
|
|
190
200
|
表单页、流程表单页和代码页的主链路是 sy-lowcode-app-workspace + openxiangda workspace publish。
|
|
191
201
|
新 React SPA 公开访问使用 public-access 命令和 src/resources/public-access;settings public-access 仅用于旧表单公开设置兼容/修复。
|
|
192
202
|
JS_CODE V2 使用 trusted_node;AI 源码必须写在 src/js-code-nodes/<scriptCode>/index.ts,代码自动化源码写在 src/automations/<resourceCode>/index.ts。definition-json 中的 sourceFile.localPath 会在 validate/create/publish 时先 TS 校验、再构建并上传为快照。`);
|
|
@@ -211,6 +221,7 @@ const SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
|
211
221
|
'--release',
|
|
212
222
|
'--resources',
|
|
213
223
|
'--skip-resources',
|
|
224
|
+
'--show-secret',
|
|
214
225
|
'--strict',
|
|
215
226
|
'--sdd-bypass',
|
|
216
227
|
'--summary',
|
|
@@ -1049,6 +1060,168 @@ async function contract(args) {
|
|
|
1049
1060
|
fail('用法: openxiangda contract list|explain <contract-name> [--json]');
|
|
1050
1061
|
}
|
|
1051
1062
|
|
|
1063
|
+
async function openApi(args) {
|
|
1064
|
+
const [section, action, ...rest] = args;
|
|
1065
|
+
const { flags, positional } = parseArgs(rest);
|
|
1066
|
+
const usage = [
|
|
1067
|
+
'用法:',
|
|
1068
|
+
' openxiangda open-api spec tags',
|
|
1069
|
+
' openxiangda open-api spec list [--tag name] [--search text] [--json]',
|
|
1070
|
+
' openxiangda open-api spec describe <operationId|path> [--method post] [--json]',
|
|
1071
|
+
' openxiangda open-api credential list [--profile name] [--json]',
|
|
1072
|
+
' openxiangda open-api credential get <id> [--profile name] [--show-secret] [--json]',
|
|
1073
|
+
' openxiangda open-api credential create --name <name> [--description text] [--permissions a,b] --yes [--show-secret] [--json]',
|
|
1074
|
+
' openxiangda open-api credential update <id> [--name text] [--description text] [--permissions a,b] [--json]',
|
|
1075
|
+
' openxiangda open-api credential rotate-secret <id> --yes --show-secret [--json]',
|
|
1076
|
+
].join('\n');
|
|
1077
|
+
if (
|
|
1078
|
+
!section ||
|
|
1079
|
+
section === 'help' ||
|
|
1080
|
+
section === '--help' ||
|
|
1081
|
+
section === '-h' ||
|
|
1082
|
+
!action ||
|
|
1083
|
+
action === 'help' ||
|
|
1084
|
+
action === '--help' ||
|
|
1085
|
+
action === '-h' ||
|
|
1086
|
+
flags.help ||
|
|
1087
|
+
flags.h
|
|
1088
|
+
) {
|
|
1089
|
+
print(usage);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (section === 'spec') {
|
|
1094
|
+
const document = loadOpenApiDocument();
|
|
1095
|
+
if (action === 'tags') {
|
|
1096
|
+
const tags = listOpenApiTags(document);
|
|
1097
|
+
if (flags.json) return writeJson({ tags });
|
|
1098
|
+
print(tags.map(item => `${item.name}\t${item.description}`).join('\n'));
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (action === 'list') {
|
|
1102
|
+
const operations = listOpenApiOperations(document, {
|
|
1103
|
+
tag: flags.tag,
|
|
1104
|
+
search: flags.search,
|
|
1105
|
+
});
|
|
1106
|
+
if (flags.json) return writeJson({ operations, total: operations.length });
|
|
1107
|
+
print(
|
|
1108
|
+
operations
|
|
1109
|
+
.map(item => `${item.method}\t${item.path}\t${item.summary}\t${item.operationId}`)
|
|
1110
|
+
.join('\n')
|
|
1111
|
+
);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (action === 'describe') {
|
|
1115
|
+
const selector = positional[0];
|
|
1116
|
+
if (!selector) fail('open-api spec describe 缺少 operationId 或 path');
|
|
1117
|
+
const operation = describeOpenApiOperation(document, selector, flags.method);
|
|
1118
|
+
if (flags.json) return writeJson(operation);
|
|
1119
|
+
print(
|
|
1120
|
+
[
|
|
1121
|
+
`${operation.method} ${operation.path}`,
|
|
1122
|
+
`${operation.summary} (${operation.operationId})`,
|
|
1123
|
+
'',
|
|
1124
|
+
operation.description,
|
|
1125
|
+
'',
|
|
1126
|
+
`Authentication: ${operation.authenticated ? Object.keys(operation.securitySchemes).join(', ') : 'none'}`,
|
|
1127
|
+
'',
|
|
1128
|
+
'Parameters:',
|
|
1129
|
+
JSON.stringify(operation.parameters, null, 2),
|
|
1130
|
+
'',
|
|
1131
|
+
'Request body:',
|
|
1132
|
+
JSON.stringify(operation.requestBody, null, 2),
|
|
1133
|
+
'',
|
|
1134
|
+
'Responses:',
|
|
1135
|
+
JSON.stringify(operation.responses, null, 2),
|
|
1136
|
+
'',
|
|
1137
|
+
'Source:',
|
|
1138
|
+
JSON.stringify(operation.source, null, 2),
|
|
1139
|
+
].join('\n')
|
|
1140
|
+
);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
fail(usage);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (section === 'credential') {
|
|
1147
|
+
const config = loadConfig();
|
|
1148
|
+
const profileName = flags.profile || config.currentProfile;
|
|
1149
|
+
const id = positional[0];
|
|
1150
|
+
try {
|
|
1151
|
+
let data;
|
|
1152
|
+
if (action === 'list') {
|
|
1153
|
+
data = await requestWithAuth(config, profileName, '/api/dingtalk-apps');
|
|
1154
|
+
} else if (action === 'get') {
|
|
1155
|
+
if (!id) fail('open-api credential get 缺少凭证 id');
|
|
1156
|
+
data = await requestWithAuth(
|
|
1157
|
+
config,
|
|
1158
|
+
profileName,
|
|
1159
|
+
`/api/dingtalk-apps/${encodeURIComponent(id)}`
|
|
1160
|
+
);
|
|
1161
|
+
} else if (action === 'create') {
|
|
1162
|
+
if (!flags.yes) {
|
|
1163
|
+
fail('创建开放 API 凭证会写入平台。用户确认后请追加 --yes。');
|
|
1164
|
+
}
|
|
1165
|
+
if (!flags.name) fail('open-api credential create 缺少 --name');
|
|
1166
|
+
data = await requestWithAuth(config, profileName, '/api/dingtalk-apps', {
|
|
1167
|
+
method: 'POST',
|
|
1168
|
+
body: compactObject({
|
|
1169
|
+
name: flags.name,
|
|
1170
|
+
description: flags.description,
|
|
1171
|
+
permissions:
|
|
1172
|
+
flags.permissions === undefined ? undefined : splitList(flags.permissions),
|
|
1173
|
+
}),
|
|
1174
|
+
});
|
|
1175
|
+
} else if (action === 'update') {
|
|
1176
|
+
if (!id) fail('open-api credential update 缺少凭证 id');
|
|
1177
|
+
const body = compactObject({
|
|
1178
|
+
name: flags.name,
|
|
1179
|
+
description: flags.description,
|
|
1180
|
+
permissions:
|
|
1181
|
+
flags.permissions === undefined ? undefined : splitList(flags.permissions),
|
|
1182
|
+
});
|
|
1183
|
+
if (Object.keys(body).length === 0) {
|
|
1184
|
+
fail('open-api credential update 至少需要 --name、--description 或 --permissions');
|
|
1185
|
+
}
|
|
1186
|
+
data = await requestWithAuth(
|
|
1187
|
+
config,
|
|
1188
|
+
profileName,
|
|
1189
|
+
`/api/dingtalk-apps/${encodeURIComponent(id)}`,
|
|
1190
|
+
{ method: 'POST', body }
|
|
1191
|
+
);
|
|
1192
|
+
} else if (action === 'rotate-secret') {
|
|
1193
|
+
if (!id) fail('open-api credential rotate-secret 缺少凭证 id');
|
|
1194
|
+
if (!flags.yes || !flags['show-secret']) {
|
|
1195
|
+
fail('重置 SK 需要显式确认并安全接收新密钥,请同时追加 --yes --show-secret。');
|
|
1196
|
+
}
|
|
1197
|
+
data = await requestWithAuth(
|
|
1198
|
+
config,
|
|
1199
|
+
profileName,
|
|
1200
|
+
`/api/dingtalk-apps/${encodeURIComponent(id)}/regenerate-secret`,
|
|
1201
|
+
{ method: 'POST', body: {} }
|
|
1202
|
+
);
|
|
1203
|
+
} else {
|
|
1204
|
+
fail(usage);
|
|
1205
|
+
}
|
|
1206
|
+
const output = redactOpenApiSecret(data, Boolean(flags['show-secret']));
|
|
1207
|
+
return outputDirectResult(output, flags);
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
if (/平台管理员|无权限|forbidden/i.test(String(error.message || ''))) {
|
|
1210
|
+
fail(`开放 API 凭证管理需要平台管理员权限: ${error.message}`);
|
|
1211
|
+
}
|
|
1212
|
+
throw error;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
fail(usage);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function compactObject(value) {
|
|
1220
|
+
return Object.fromEntries(
|
|
1221
|
+
Object.entries(value).filter(([, item]) => item !== undefined)
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1052
1225
|
async function platform(args) {
|
|
1053
1226
|
const [subcommand, ...rest] = args;
|
|
1054
1227
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -5415,6 +5588,8 @@ async function commands(args) {
|
|
|
5415
5588
|
'auth status|refresh|logout',
|
|
5416
5589
|
'doctor [--profile name] [--app-type APP_XXX] [--release]',
|
|
5417
5590
|
'contract list|explain <contract-name>',
|
|
5591
|
+
'open-api spec tags|list|describe [operationId|path]',
|
|
5592
|
+
'open-api credential list|get|create|update|rotate-secret [id]',
|
|
5418
5593
|
'design gates|template|review [--topic code]',
|
|
5419
5594
|
'sdd init|migrate|propose|approve|status|context|verify|sync|archive',
|
|
5420
5595
|
'env',
|
package/lib/open-api.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const OPEN_API_SPEC_FILE = path.join(
|
|
6
|
+
__dirname,
|
|
7
|
+
'..',
|
|
8
|
+
'openxiangda-skills',
|
|
9
|
+
'skills',
|
|
10
|
+
'openxiangda-open-api',
|
|
11
|
+
'references',
|
|
12
|
+
'dingtalk-api.openapi.json'
|
|
13
|
+
);
|
|
14
|
+
const HTTP_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch']);
|
|
15
|
+
|
|
16
|
+
function loadOpenApiDocument(file = OPEN_API_SPEC_FILE) {
|
|
17
|
+
const document = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
18
|
+
validateOpenApiDocument(document);
|
|
19
|
+
return document;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function validateOpenApiDocument(document) {
|
|
23
|
+
if (!document || typeof document !== 'object') {
|
|
24
|
+
throw new Error('DingTalk OpenAPI document must be a JSON object');
|
|
25
|
+
}
|
|
26
|
+
if (document.openapi !== '3.0.1') {
|
|
27
|
+
throw new Error(`Unsupported DingTalk OpenAPI version: ${document.openapi || 'missing'}`);
|
|
28
|
+
}
|
|
29
|
+
const operations = collectOperations(document);
|
|
30
|
+
if (operations.length === 0) {
|
|
31
|
+
throw new Error('DingTalk OpenAPI document contains no operations');
|
|
32
|
+
}
|
|
33
|
+
const securitySchemes = document.components?.securitySchemes || {};
|
|
34
|
+
for (const item of operations) {
|
|
35
|
+
const operation = item.operation;
|
|
36
|
+
for (const field of ['operationId', 'summary', 'description']) {
|
|
37
|
+
if (!String(operation[field] || '').trim()) {
|
|
38
|
+
throw new Error(`${item.method.toUpperCase()} ${item.path} is missing ${field}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(operation.tags) || operation.tags.length === 0) {
|
|
42
|
+
throw new Error(`${item.method.toUpperCase()} ${item.path} is missing tags`);
|
|
43
|
+
}
|
|
44
|
+
if (!operation.responses || Object.keys(operation.responses).length === 0) {
|
|
45
|
+
throw new Error(`${item.method.toUpperCase()} ${item.path} is missing responses`);
|
|
46
|
+
}
|
|
47
|
+
for (const requirement of operation.security || []) {
|
|
48
|
+
for (const name of Object.keys(requirement)) {
|
|
49
|
+
if (!securitySchemes[name]) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`${item.method.toUpperCase()} ${item.path} references unknown security scheme ${name}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return document;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function collectOperations(document) {
|
|
61
|
+
const operations = [];
|
|
62
|
+
for (const [routePath, pathItem] of Object.entries(document.paths || {})) {
|
|
63
|
+
for (const [method, operation] of Object.entries(pathItem || {})) {
|
|
64
|
+
if (!HTTP_METHODS.has(method)) continue;
|
|
65
|
+
operations.push({
|
|
66
|
+
method,
|
|
67
|
+
path: routePath,
|
|
68
|
+
operation,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return operations.sort((left, right) =>
|
|
73
|
+
`${left.path} ${left.method}`.localeCompare(`${right.path} ${right.method}`)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function listOpenApiTags(document) {
|
|
78
|
+
const declared = new Map(
|
|
79
|
+
(document.tags || []).map(item => [item.name, item.description || ''])
|
|
80
|
+
);
|
|
81
|
+
for (const { operation } of collectOperations(document)) {
|
|
82
|
+
for (const tag of operation.tags || []) {
|
|
83
|
+
if (!declared.has(tag)) declared.set(tag, '');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...declared.entries()]
|
|
87
|
+
.map(([name, description]) => ({ name, description }))
|
|
88
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function listOpenApiOperations(document, options = {}) {
|
|
92
|
+
const tag = String(options.tag || '').trim().toLowerCase();
|
|
93
|
+
const search = String(options.search || '').trim().toLowerCase();
|
|
94
|
+
return collectOperations(document)
|
|
95
|
+
.filter(({ operation }) => {
|
|
96
|
+
if (!tag) return true;
|
|
97
|
+
return (operation.tags || []).some(item => String(item).toLowerCase() === tag);
|
|
98
|
+
})
|
|
99
|
+
.filter(item => {
|
|
100
|
+
if (!search) return true;
|
|
101
|
+
return [
|
|
102
|
+
item.method,
|
|
103
|
+
item.path,
|
|
104
|
+
item.operation.operationId,
|
|
105
|
+
item.operation.summary,
|
|
106
|
+
item.operation.description,
|
|
107
|
+
...(item.operation.tags || []),
|
|
108
|
+
]
|
|
109
|
+
.join('\n')
|
|
110
|
+
.toLowerCase()
|
|
111
|
+
.includes(search);
|
|
112
|
+
})
|
|
113
|
+
.map(item => ({
|
|
114
|
+
method: item.method.toUpperCase(),
|
|
115
|
+
path: item.path,
|
|
116
|
+
operationId: item.operation.operationId,
|
|
117
|
+
summary: item.operation.summary,
|
|
118
|
+
tags: item.operation.tags || [],
|
|
119
|
+
authenticated: (item.operation.security || []).length > 0,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function describeOpenApiOperation(document, selector, method) {
|
|
124
|
+
const normalizedSelector = String(selector || '').trim();
|
|
125
|
+
if (!normalizedSelector) {
|
|
126
|
+
throw new Error('Missing operationId or path');
|
|
127
|
+
}
|
|
128
|
+
const normalizedMethod = String(method || '').trim().toLowerCase();
|
|
129
|
+
let matches = collectOperations(document).filter(item =>
|
|
130
|
+
item.operation.operationId === normalizedSelector || item.path === normalizedSelector
|
|
131
|
+
);
|
|
132
|
+
if (normalizedMethod) {
|
|
133
|
+
matches = matches.filter(item => item.method === normalizedMethod);
|
|
134
|
+
}
|
|
135
|
+
if (matches.length === 0) {
|
|
136
|
+
throw new Error(`Open API operation not found: ${normalizedSelector}`);
|
|
137
|
+
}
|
|
138
|
+
if (matches.length > 1) {
|
|
139
|
+
const methods = matches.map(item => item.method.toUpperCase()).join(', ');
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Path ${normalizedSelector} has multiple methods (${methods}); pass --method`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { path: routePath, method: operationMethod, operation } = matches[0];
|
|
146
|
+
const security = operation.security || [];
|
|
147
|
+
const securitySchemeNames = [
|
|
148
|
+
...new Set(security.flatMap(requirement => Object.keys(requirement))),
|
|
149
|
+
];
|
|
150
|
+
const securitySchemes = Object.fromEntries(
|
|
151
|
+
securitySchemeNames.map(name => [
|
|
152
|
+
name,
|
|
153
|
+
resolveNode(document.components?.securitySchemes?.[name], document),
|
|
154
|
+
])
|
|
155
|
+
);
|
|
156
|
+
return {
|
|
157
|
+
method: operationMethod.toUpperCase(),
|
|
158
|
+
path: routePath,
|
|
159
|
+
operationId: operation.operationId,
|
|
160
|
+
summary: operation.summary,
|
|
161
|
+
description: operation.description,
|
|
162
|
+
tags: operation.tags || [],
|
|
163
|
+
authenticated: security.length > 0,
|
|
164
|
+
security,
|
|
165
|
+
securitySchemes,
|
|
166
|
+
parameters: resolveNode(operation.parameters || [], document),
|
|
167
|
+
requestBody: resolveNode(operation.requestBody || null, document),
|
|
168
|
+
responses: resolveNode(operation.responses || {}, document),
|
|
169
|
+
source: operation['x-code-source'] || null,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolveNode(value, document, seen = new Set()) {
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
return value.map(item => resolveNode(item, document, seen));
|
|
176
|
+
}
|
|
177
|
+
if (!value || typeof value !== 'object') return value;
|
|
178
|
+
|
|
179
|
+
if (value.$ref) {
|
|
180
|
+
const reference = value.$ref;
|
|
181
|
+
if (seen.has(reference)) return { $ref: reference };
|
|
182
|
+
const target = resolveJsonPointer(document, reference);
|
|
183
|
+
if (!target) return { ...value };
|
|
184
|
+
const nextSeen = new Set(seen);
|
|
185
|
+
nextSeen.add(reference);
|
|
186
|
+
return resolveNode(target, document, nextSeen);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const resolved = {};
|
|
190
|
+
for (const [key, item] of Object.entries(value)) {
|
|
191
|
+
if (key === 'allOf') continue;
|
|
192
|
+
resolved[key] = resolveNode(item, document, seen);
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(value.allOf)) return resolved;
|
|
195
|
+
|
|
196
|
+
const merged = {};
|
|
197
|
+
for (const item of value.allOf) {
|
|
198
|
+
mergeSchema(merged, resolveNode(item, document, seen));
|
|
199
|
+
}
|
|
200
|
+
mergeSchema(merged, resolved);
|
|
201
|
+
return merged;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resolveJsonPointer(document, reference) {
|
|
205
|
+
if (!String(reference).startsWith('#/')) return null;
|
|
206
|
+
return reference
|
|
207
|
+
.slice(2)
|
|
208
|
+
.split('/')
|
|
209
|
+
.map(part => part.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
210
|
+
.reduce((current, part) => current?.[part], document);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function mergeSchema(target, source) {
|
|
214
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) return target;
|
|
215
|
+
for (const [key, value] of Object.entries(source)) {
|
|
216
|
+
if (key === 'properties' && value && typeof value === 'object') {
|
|
217
|
+
target.properties = { ...(target.properties || {}), ...value };
|
|
218
|
+
} else if (key === 'required' && Array.isArray(value)) {
|
|
219
|
+
target.required = [...new Set([...(target.required || []), ...value])];
|
|
220
|
+
} else {
|
|
221
|
+
target[key] = value;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return target;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function redactOpenApiSecret(value, showSecret = false) {
|
|
228
|
+
if (showSecret || value === null || value === undefined) return value;
|
|
229
|
+
if (Array.isArray(value)) {
|
|
230
|
+
return value.map(item => redactOpenApiSecret(item, false));
|
|
231
|
+
}
|
|
232
|
+
if (typeof value !== 'object') return value;
|
|
233
|
+
const result = {};
|
|
234
|
+
for (const [key, item] of Object.entries(value)) {
|
|
235
|
+
result[key] = /^(appSecret|secret)$/i.test(key)
|
|
236
|
+
? '***redacted***'
|
|
237
|
+
: redactOpenApiSecret(item, false);
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sha256File(file) {
|
|
243
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = {
|
|
247
|
+
OPEN_API_SPEC_FILE,
|
|
248
|
+
collectOperations,
|
|
249
|
+
describeOpenApiOperation,
|
|
250
|
+
listOpenApiOperations,
|
|
251
|
+
listOpenApiTags,
|
|
252
|
+
loadOpenApiDocument,
|
|
253
|
+
redactOpenApiSecret,
|
|
254
|
+
sha256File,
|
|
255
|
+
validateOpenApiDocument,
|
|
256
|
+
};
|
package/lib/skills.js
CHANGED
|
@@ -73,6 +73,15 @@ const SKILL_SPECS = [
|
|
|
73
73
|
sourceRelativePath: 'openxiangda-skills/skills/openxiangda-inspect',
|
|
74
74
|
type: 'subskill',
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
name: 'openxiangda-open-api',
|
|
78
|
+
displayName: 'OpenXiangda Open API',
|
|
79
|
+
shortDescription: '外部后端开放接口契约与 AK/SK 管理。',
|
|
80
|
+
defaultPrompt:
|
|
81
|
+
'Use $openxiangda-open-api to implement a secure backend integration with the private low-code platform Open API.',
|
|
82
|
+
sourceRelativePath: 'openxiangda-skills/skills/openxiangda-open-api',
|
|
83
|
+
type: 'subskill',
|
|
84
|
+
},
|
|
76
85
|
];
|
|
77
86
|
|
|
78
87
|
function getDefaultCodexSkillsDir(env = process.env) {
|
|
@@ -323,6 +332,10 @@ function copyRootSkill(stagingDir) {
|
|
|
323
332
|
function copySubskill(spec, stagingDir) {
|
|
324
333
|
const sourceDir = path.join(ROOT_DIR, spec.sourceRelativePath);
|
|
325
334
|
fs.mkdirSync(stagingDir, { recursive: true });
|
|
335
|
+
fs.cpSync(sourceDir, stagingDir, {
|
|
336
|
+
recursive: true,
|
|
337
|
+
dereference: false,
|
|
338
|
+
});
|
|
326
339
|
const skillMarkdown = fs
|
|
327
340
|
.readFileSync(path.join(sourceDir, 'SKILL.md'), 'utf8')
|
|
328
341
|
.replace(/\.\.\/\.\.\/references\//g, 'references/');
|
|
@@ -337,9 +350,10 @@ function writeAgentMetadata(spec, skillDir) {
|
|
|
337
350
|
const agentsDir = path.join(skillDir, 'agents');
|
|
338
351
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
339
352
|
const defaultPrompt =
|
|
340
|
-
spec.
|
|
353
|
+
spec.defaultPrompt ||
|
|
354
|
+
(spec.name === 'openxiangda'
|
|
341
355
|
? '使用 $openxiangda 处理私有化低代码平台的登录、发布和诊断任务。'
|
|
342
|
-
: `使用 $${spec.name} 处理对应的 OpenXiangda
|
|
356
|
+
: `使用 $${spec.name} 处理对应的 OpenXiangda 低代码平台任务。`);
|
|
343
357
|
const content = [
|
|
344
358
|
'interface:',
|
|
345
359
|
` display_name: ${yamlQuote(spec.displayName)}`,
|
|
@@ -5,7 +5,7 @@ description: "Use OpenXiangda for any private low-code platform or sy-lowcode-ap
|
|
|
5
5
|
|
|
6
6
|
# OpenXiangda
|
|
7
7
|
|
|
8
|
-
OpenXiangda is a lightweight bridge between an AI coding tool and a private low-code platform.
|
|
8
|
+
OpenXiangda is a lightweight bridge between an AI coding tool and a private low-code platform. Normal app development uses a platform domain, a normal-user login, and `/openxiangda-api/v1`; it does not use AK/SK. External backend and third-party integrations are a separate track that uses the `openxiangda-open-api` skill and `/dingtalk-api/v1.0`.
|
|
9
9
|
|
|
10
10
|
OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` publishes form/page bundles with `openxiangda workspace publish`. Phase 6 `react-spa` workspaces are standard React/Vite apps: publish resources with `openxiangda resource publish`, then publish the frontend dist with `openxiangda runtime deploy`. Do not send React SPA routes through old `isRenderNav`, app-shell, or workbench page parameters.
|
|
11
11
|
|
|
@@ -37,6 +37,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
37
37
|
| 登录 / 切换平台 / profile / token / whoami | `openxiangda-core` | `openxiangda env --profile <name>` / `openxiangda auth status` |
|
|
38
38
|
| 多表只读联表 / 固定口径统计 / 强实时复杂查询 / 看板指标 | `openxiangda-form` (data view) | declare `src/resources/data-views/<code>.json` with `storageMode` → `resource publish` |
|
|
39
39
|
| 调外部 / 第三方 API / 钉钉 / 自建系统 | `openxiangda-page` (connector) | declare `src/resources/connectors/<code>.json` → `sdk.connector.invoke` |
|
|
40
|
+
| 外部后端 / 三方系统调用享搭 / AK/SK / 开放接口 | `openxiangda-open-api` | `openxiangda open-api spec list --search <keyword> --json` → describe the selected operation |
|
|
40
41
|
|
|
41
42
|
### Hard rules — always
|
|
42
43
|
|
|
@@ -68,7 +69,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
68
69
|
### Hard rules — never
|
|
69
70
|
|
|
70
71
|
- ❌ `pnpm publish:all` / `pnpm publish:oss` / `pnpm register` / `lowcode-workspace publish-*` directly — they are workspace internals and miss the profile token injection. Use `openxiangda workspace publish ...` instead.
|
|
71
|
-
- ❌ Asking the user
|
|
72
|
+
- ❌ Asking the user to paste `AK` / `SK` / `appKey` / `appSecret` into chat. Normal OpenXiangda flows use the user token; authorized external backend work uses `open-api credential` for a direct secure handoff.
|
|
72
73
|
- ❌ Searching the platform for a similar app name when `.openxiangda/state.json` has no binding — create a new app with `workspace init --app-name`.
|
|
73
74
|
- ❌ Using `openxiangda form create` / `form publish` / `page publish` as the normal page generation path. They are low-level repair commands.
|
|
74
75
|
- ❌ Implementing an architecture-class request before the user confirms the design. This includes creating files, mutating platform resources, publishing runtime, sending notifications, or calling live write/delete endpoints.
|
|
@@ -195,13 +196,16 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
195
196
|
- `skills/openxiangda-workflow-automation/SKILL.md`: workflow v3 definitions, automation triggers, publishing, enabling, and profile-isolated IDs.
|
|
196
197
|
- `skills/openxiangda-permission-settings/SKILL.md`: app roles, page permission groups, form permission groups, and settings-oriented guidance.
|
|
197
198
|
- `skills/openxiangda-inspect/SKILL.md`: read-only app and resource diagnosis using snapshots and profile-local resource IDs.
|
|
199
|
+
- `skills/openxiangda-open-api/SKILL.md`: external backend integration through the complete `/dingtalk-api/v1.0` contract and one-time API credential management.
|
|
198
200
|
|
|
199
201
|
## API Namespace
|
|
200
202
|
|
|
201
|
-
OpenXiangda
|
|
203
|
+
OpenXiangda CLI and low-code app development use `/openxiangda-api/v1`. External backend and third-party integrations use `/dingtalk-api/v1.0` through `openxiangda-open-api`; do not switch normal workspace workflows to AK/SK.
|
|
202
204
|
|
|
203
205
|
On private deployments, the public API URL is normally `<platform-domain>/service/openxiangda-api/v1`.
|
|
204
206
|
|
|
207
|
+
For external backend integrations, the public base is normally `<platform-domain>/service/dingtalk-api/v1.0`. Use `openxiangda open-api spec describe` for exact request and response fields.
|
|
208
|
+
|
|
205
209
|
For endpoint details, read `references/openxiangda-api.md` only when you need request or response fields.
|
|
206
210
|
|
|
207
211
|
## References Index
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Base path: `/openxiangda-api/v1`
|
|
4
4
|
|
|
5
|
-
OpenXiangda
|
|
5
|
+
OpenXiangda CLI and low-code app workflows use `/openxiangda-api/v1`. External backend and third-party integrations use the separate `openxiangda-open-api` skill and `/dingtalk-api/v1.0`; do not switch workspace publishing, page, form, workflow, or automation work to AK/SK.
|
|
6
6
|
|
|
7
7
|
Standard private deployment public paths:
|
|
8
8
|
|
|
@@ -16,7 +16,7 @@ Authentication:
|
|
|
16
16
|
|
|
17
17
|
- Normal CLI calls use `Authorization: Bearer <accessToken>`.
|
|
18
18
|
- Refresh calls use the `refreshToken` request body.
|
|
19
|
-
-
|
|
19
|
+
- `/dingtalk-api/v1.0` AK/SK authentication is not part of this user-token API contract. Read the `openxiangda-open-api` skill for external backend integration.
|
|
20
20
|
|
|
21
21
|
## Auth
|
|
22
22
|
|
|
@@ -151,7 +151,7 @@ const api = usePageFormRuntimeApi()
|
|
|
151
151
|
</FormProvider>
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
Use `createPageFormRuntimeApi(sdk)` when hooks are unavailable. Both APIs preserve JSON envelopes
|
|
154
|
+
Use `createPageFormRuntimeApi(sdk)` when hooks are unavailable. Both APIs preserve JSON envelopes, route `responseType: "blob"` through `sdk.transport.download`, and normalize ticket content URLs such as `/file/download-by-ticket/*` with the current PageSdk `servicePrefix`. They do not rewrite `/view/*` page URLs or absolute storage URLs. Never forward every form request to `sdk.request`, and never prepend `/service` in application code.
|
|
155
155
|
|
|
156
156
|
Shareable file preview ticket:
|
|
157
157
|
|
|
@@ -32,6 +32,18 @@
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
+
## 问题:附件下载打开站点根 `/file/` 并返回 404
|
|
36
|
+
|
|
37
|
+
**症状**:附件下载 ticket 创建成功,但浏览器打开 `https://host/file/...`,实际平台文件接口位于 `https://host/service/file/...`。
|
|
38
|
+
|
|
39
|
+
**原因**:应用手写了 PageSdk 到 `FormProvider` 的不完整适配,或使用了未归一化的 ticket payload,绕过了当前 runtime 的 `servicePrefix`。
|
|
40
|
+
|
|
41
|
+
**解决方案**:升级 OpenXiangda,并让 `FormProvider.config.api` 来自 `usePageFormRuntimeApi()` 或 `createPageFormRuntimeApi(sdk)`。SDK 会把 ticket payload 中的 `/file/*` 内容地址补成当前 `servicePrefix`,同时保留 `/view/*` 页面地址和绝对 OSS URL。业务代码不要直接 `window.open(ticket.downloadUrl)` 后再自行猜测前缀。
|
|
42
|
+
|
|
43
|
+
**验证方式**:点击下载后地址应为 `/service/file/download-by-ticket/:ticket` 或服务端返回的绝对存储 URL,不应是站点根 `/file/*`。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
35
47
|
## 问题:antd/antd-mobile 弹层样式丢失
|
|
36
48
|
|
|
37
49
|
**症状**:Select / Dropdown / Popup / Modal 等组件的弹出层没有样式,直接跑到屏幕外,或呈现为无样式的裸 HTML。
|