openxiangda 1.0.156 → 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 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
- It uses normal platform user login tokens through `/openxiangda-api/v1`; it does not use AK/SK.
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 8 top-level subskills into `${CODEX_HOME:-~/.codex}/skills` by default:
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 使用普通用户登录 token,不需要 AK/SK。
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',
@@ -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.name === 'openxiangda'
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. No AK/SK. The user provides a platform domain, logs in as a normal platform user, and the CLI calls `/openxiangda-api/v1` with that user's token.
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 for `AK` / `SK` / `appKey` / `appSecret`. The whole flow is normal-user token only.
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 uses `/openxiangda-api/v1`. Old `/dingtalk-api/v1.0` APIs are compatibility only and should not be used for new OpenXiangda workflows.
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 APIs use `/openxiangda-api/v1`. Legacy `/dingtalk-api/v1.0` exists only for compatibility and should not be used for new AI workflow or automation work.
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
- - Legacy `/dingtalk-api/v1.0` AK/SK authentication is not part of OpenXiangda.
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