openxiangda 1.0.81 → 1.0.83
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/lib/cli.js +190 -87
- package/lib/utils.js +4 -0
- package/openxiangda-skills/SKILL.md +3 -1
- package/openxiangda-skills/references/architecture-design.md +6 -0
- package/openxiangda-skills/references/automation-v3.md +1 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +3 -0
- package/openxiangda-skills/references/workflow-v3.md +1 -1
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +5 -4
- package/package.json +1 -1
- package/templates/openxiangda-react-spa/AGENTS.md +6 -0
- package/templates/openxiangda-react-spa/package.json +3 -0
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +146 -0
- package/templates/openxiangda-react-spa/tsconfig.js-code-nodes.json +19 -0
package/lib/cli.js
CHANGED
|
@@ -128,6 +128,82 @@ OpenXiangda 使用普通用户登录 token,不需要 AK/SK。
|
|
|
128
128
|
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 校验、再构建并上传为快照。`);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
const SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
132
|
+
'--dry-run',
|
|
133
|
+
'--enabled',
|
|
134
|
+
'--force',
|
|
135
|
+
'--help',
|
|
136
|
+
'--include-sourcemaps',
|
|
137
|
+
'--json',
|
|
138
|
+
'--legacy-form-bundle',
|
|
139
|
+
'--no-activate',
|
|
140
|
+
'--no-build',
|
|
141
|
+
'--no-runtime-aliases',
|
|
142
|
+
'--prune',
|
|
143
|
+
'--publish',
|
|
144
|
+
'--published',
|
|
145
|
+
'--redact',
|
|
146
|
+
'--resources',
|
|
147
|
+
'--skip-resources',
|
|
148
|
+
'--strict',
|
|
149
|
+
'--summary',
|
|
150
|
+
'--unpublish',
|
|
151
|
+
'--yes',
|
|
152
|
+
'-h',
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
function parseSubcommandArgs(args) {
|
|
156
|
+
const leadingFlags = [];
|
|
157
|
+
let index = 0;
|
|
158
|
+
while (index < args.length) {
|
|
159
|
+
const item = args[index];
|
|
160
|
+
if (!isFlagToken(item)) break;
|
|
161
|
+
if (item === '--') {
|
|
162
|
+
index += 1;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
leadingFlags.push(item);
|
|
166
|
+
if (hasInlineFlagValue(item) || isSubcommandBooleanFlag(item)) {
|
|
167
|
+
index += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const next = args[index + 1];
|
|
171
|
+
if (next && !isFlagToken(next)) {
|
|
172
|
+
leadingFlags.push(next);
|
|
173
|
+
index += 2;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
index += 1;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
subcommand: args[index],
|
|
180
|
+
rest: [...leadingFlags, ...args.slice(index + 1)],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isFlagToken(value) {
|
|
185
|
+
return typeof value === 'string' && value.startsWith('-');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function hasInlineFlagValue(value) {
|
|
189
|
+
return typeof value === 'string' && value.startsWith('--') && value.includes('=');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isSubcommandBooleanFlag(value) {
|
|
193
|
+
return SUBCOMMAND_BOOLEAN_FLAGS.has(value) || /^--no-/.test(String(value || ''));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function wantsSubcommandHelp(subcommand, flags) {
|
|
197
|
+
return (
|
|
198
|
+
!subcommand ||
|
|
199
|
+
subcommand === 'help' ||
|
|
200
|
+
subcommand === '--help' ||
|
|
201
|
+
subcommand === '-h' ||
|
|
202
|
+
flags.help ||
|
|
203
|
+
flags.h
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
131
207
|
async function update(args) {
|
|
132
208
|
const requestedSubcommand = args[0] && !args[0].startsWith('--') ? args[0] : 'check';
|
|
133
209
|
const parsedArgs = requestedSubcommand === args[0] ? args.slice(1) : args;
|
|
@@ -1303,81 +1379,6 @@ async function form(args) {
|
|
|
1303
1379
|
return;
|
|
1304
1380
|
}
|
|
1305
1381
|
|
|
1306
|
-
if (subcommand === 'executions') {
|
|
1307
|
-
const [automationKey] = positional;
|
|
1308
|
-
if (!automationKey) fail('用法: openxiangda automation executions <automationCode|automationId>');
|
|
1309
|
-
const target = getWorkspaceTarget(config, profileName, flags);
|
|
1310
|
-
const automationId = resolveAutomationId(target.bound, automationKey, flags);
|
|
1311
|
-
const data = await requestWithAuth(
|
|
1312
|
-
config,
|
|
1313
|
-
target.profileName,
|
|
1314
|
-
apiPathWithQuery(
|
|
1315
|
-
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automationId)}/executions`,
|
|
1316
|
-
{
|
|
1317
|
-
status: flags.status,
|
|
1318
|
-
triggerEventType: flags['trigger-type'],
|
|
1319
|
-
startDate: flags['start-date'],
|
|
1320
|
-
endDate: flags['end-date'],
|
|
1321
|
-
page: flags.page,
|
|
1322
|
-
pageSize: flags['page-size'] || flags.limit,
|
|
1323
|
-
}
|
|
1324
|
-
)
|
|
1325
|
-
);
|
|
1326
|
-
if (flags.json) return writeJson(data);
|
|
1327
|
-
printAutomationExecutionList(data);
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
if (subcommand === 'logs') {
|
|
1332
|
-
const [instanceId] = positional;
|
|
1333
|
-
if (!instanceId) fail('用法: openxiangda automation logs <instanceId> [--automation <automationCode|automationId>]');
|
|
1334
|
-
const target = getWorkspaceTarget(config, profileName, flags);
|
|
1335
|
-
const data = await getAutomationExecutionDetailFromCli(
|
|
1336
|
-
config,
|
|
1337
|
-
target,
|
|
1338
|
-
instanceId,
|
|
1339
|
-
flags.automation || flags['automation-code'] || flags['automation-id']
|
|
1340
|
-
);
|
|
1341
|
-
const output = flags.redact ? redactLogValue(data) : data;
|
|
1342
|
-
if (flags.json) return writeJson(output);
|
|
1343
|
-
printAutomationExecutionLogs(output, { summary: Boolean(flags.summary) });
|
|
1344
|
-
return;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
if (subcommand === 'diagnose') {
|
|
1348
|
-
const [automationKey] = positional;
|
|
1349
|
-
if (!automationKey) fail('用法: openxiangda automation diagnose <automationCode|automationId>');
|
|
1350
|
-
const target = getWorkspaceTarget(config, profileName, flags);
|
|
1351
|
-
const automationId = resolveAutomationId(target.bound, automationKey, flags);
|
|
1352
|
-
const records = await requestWithAuth(
|
|
1353
|
-
config,
|
|
1354
|
-
target.profileName,
|
|
1355
|
-
apiPathWithQuery(
|
|
1356
|
-
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automationId)}/executions`,
|
|
1357
|
-
{
|
|
1358
|
-
status: flags.status || 'failed',
|
|
1359
|
-
page: 1,
|
|
1360
|
-
pageSize: flags['page-size'] || flags.limit || 1,
|
|
1361
|
-
}
|
|
1362
|
-
)
|
|
1363
|
-
);
|
|
1364
|
-
const first = records?.data?.[0];
|
|
1365
|
-
if (!first?.id) {
|
|
1366
|
-
if (flags.json) return writeJson({ found: false, message: '未找到失败执行记录' });
|
|
1367
|
-
print('未找到失败执行记录');
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
const detail = await requestWithAuth(
|
|
1371
|
-
config,
|
|
1372
|
-
target.profileName,
|
|
1373
|
-
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automationId)}/executions/${encodeURIComponent(first.id)}`
|
|
1374
|
-
);
|
|
1375
|
-
const output = flags.redact ? redactLogValue(detail) : detail;
|
|
1376
|
-
if (flags.json) return writeJson({ found: true, detail: output, summary: buildAutomationDiagnosis(output) });
|
|
1377
|
-
print(buildAutomationDiagnosis(output));
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
1382
|
if (subcommand === 'publish') {
|
|
1382
1383
|
const [formKey] = positional;
|
|
1383
1384
|
if (!formKey || !flags['bundle-url']) {
|
|
@@ -1625,8 +1626,12 @@ async function menu(args) {
|
|
|
1625
1626
|
}
|
|
1626
1627
|
|
|
1627
1628
|
async function workflow(args) {
|
|
1628
|
-
const
|
|
1629
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
1629
1630
|
const { flags, positional } = parseArgs(rest);
|
|
1631
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
1632
|
+
print('用法: openxiangda workflow list|create|bind|pull|publish|delete|validate [--profile name] [--json]');
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1630
1635
|
const config = loadConfig();
|
|
1631
1636
|
const profileName = flags.profile || config.currentProfile;
|
|
1632
1637
|
|
|
@@ -1798,8 +1803,12 @@ async function workflow(args) {
|
|
|
1798
1803
|
}
|
|
1799
1804
|
|
|
1800
1805
|
async function automation(args) {
|
|
1801
|
-
const
|
|
1806
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
1802
1807
|
const { flags, positional } = parseArgs(rest);
|
|
1808
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
1809
|
+
print('用法: openxiangda automation list|create|bind|pull|executions|logs|diagnose|publish|unpublish|enable|disable|delete|validate|cron-validate [--profile name] [--json]');
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1803
1812
|
const config = loadConfig();
|
|
1804
1813
|
const profileName = flags.profile || config.currentProfile;
|
|
1805
1814
|
|
|
@@ -1911,6 +1920,81 @@ async function automation(args) {
|
|
|
1911
1920
|
return;
|
|
1912
1921
|
}
|
|
1913
1922
|
|
|
1923
|
+
if (subcommand === 'executions') {
|
|
1924
|
+
const [automationKey] = positional;
|
|
1925
|
+
if (!automationKey) fail('用法: openxiangda automation executions <automationCode|automationId>');
|
|
1926
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
1927
|
+
const automationId = resolveAutomationId(target.bound, automationKey, flags);
|
|
1928
|
+
const data = await requestWithAuth(
|
|
1929
|
+
config,
|
|
1930
|
+
target.profileName,
|
|
1931
|
+
apiPathWithQuery(
|
|
1932
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automationId)}/executions`,
|
|
1933
|
+
{
|
|
1934
|
+
status: flags.status,
|
|
1935
|
+
triggerEventType: flags['trigger-type'],
|
|
1936
|
+
startDate: flags['start-date'],
|
|
1937
|
+
endDate: flags['end-date'],
|
|
1938
|
+
page: flags.page,
|
|
1939
|
+
pageSize: flags['page-size'] || flags.limit,
|
|
1940
|
+
}
|
|
1941
|
+
)
|
|
1942
|
+
);
|
|
1943
|
+
if (flags.json) return writeJson(data);
|
|
1944
|
+
printAutomationExecutionList(data);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
if (subcommand === 'logs') {
|
|
1949
|
+
const [instanceId] = positional;
|
|
1950
|
+
if (!instanceId) fail('用法: openxiangda automation logs <instanceId> [--automation <automationCode|automationId>]');
|
|
1951
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
1952
|
+
const data = await getAutomationExecutionDetailFromCli(
|
|
1953
|
+
config,
|
|
1954
|
+
target,
|
|
1955
|
+
instanceId,
|
|
1956
|
+
flags.automation || flags['automation-code'] || flags['automation-id']
|
|
1957
|
+
);
|
|
1958
|
+
const output = flags.redact ? redactLogValue(data) : data;
|
|
1959
|
+
if (flags.json) return writeJson(output);
|
|
1960
|
+
printAutomationExecutionLogs(output, { summary: Boolean(flags.summary) });
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (subcommand === 'diagnose') {
|
|
1965
|
+
const [automationKey] = positional;
|
|
1966
|
+
if (!automationKey) fail('用法: openxiangda automation diagnose <automationCode|automationId>');
|
|
1967
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
1968
|
+
const automationId = resolveAutomationId(target.bound, automationKey, flags);
|
|
1969
|
+
const records = await requestWithAuth(
|
|
1970
|
+
config,
|
|
1971
|
+
target.profileName,
|
|
1972
|
+
apiPathWithQuery(
|
|
1973
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automationId)}/executions`,
|
|
1974
|
+
{
|
|
1975
|
+
status: flags.status || 'failed',
|
|
1976
|
+
page: 1,
|
|
1977
|
+
pageSize: flags['page-size'] || flags.limit || 1,
|
|
1978
|
+
}
|
|
1979
|
+
)
|
|
1980
|
+
);
|
|
1981
|
+
const first = records?.data?.[0];
|
|
1982
|
+
if (!first?.id) {
|
|
1983
|
+
if (flags.json) return writeJson({ found: false, message: '未找到失败执行记录' });
|
|
1984
|
+
print('未找到失败执行记录');
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
const detail = await requestWithAuth(
|
|
1988
|
+
config,
|
|
1989
|
+
target.profileName,
|
|
1990
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automationId)}/executions/${encodeURIComponent(first.id)}`
|
|
1991
|
+
);
|
|
1992
|
+
const output = flags.redact ? redactLogValue(detail) : detail;
|
|
1993
|
+
if (flags.json) return writeJson({ found: true, detail: output, summary: buildAutomationDiagnosis(output) });
|
|
1994
|
+
print(buildAutomationDiagnosis(output));
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1914
1998
|
if (['publish', 'unpublish', 'enable', 'disable'].includes(subcommand)) {
|
|
1915
1999
|
const [automationKey] = positional;
|
|
1916
2000
|
if (!automationKey) {
|
|
@@ -2002,8 +2086,12 @@ async function automation(args) {
|
|
|
2002
2086
|
}
|
|
2003
2087
|
|
|
2004
2088
|
async function dataView(args) {
|
|
2005
|
-
const
|
|
2089
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2006
2090
|
const { flags, positional } = parseArgs(rest);
|
|
2091
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2092
|
+
print('用法: openxiangda data-view list|status|refresh|query|stats <dataViewCode> [--profile name] [--json]');
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2007
2095
|
const config = loadConfig();
|
|
2008
2096
|
const profileName = flags.profile || config.currentProfile;
|
|
2009
2097
|
const target = getWorkspaceTarget(config, profileName, flags);
|
|
@@ -2580,8 +2668,12 @@ async function settings(args) {
|
|
|
2580
2668
|
}
|
|
2581
2669
|
|
|
2582
2670
|
async function resource(args) {
|
|
2583
|
-
const
|
|
2671
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2584
2672
|
const { flags } = parseArgs(rest);
|
|
2673
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2674
|
+
print('用法: openxiangda resource validate|plan|publish|pull|typegen [--profile name] [--json]');
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2585
2677
|
const config = loadConfig();
|
|
2586
2678
|
const profileName = flags.profile || config.currentProfile;
|
|
2587
2679
|
|
|
@@ -2634,8 +2726,12 @@ async function resource(args) {
|
|
|
2634
2726
|
}
|
|
2635
2727
|
|
|
2636
2728
|
async function runtime(args) {
|
|
2637
|
-
const
|
|
2729
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2638
2730
|
const { flags, positional } = parseArgs(rest);
|
|
2731
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2732
|
+
print('用法: openxiangda runtime deploy|releases|activate [--profile name] [--json]');
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2639
2735
|
const config = loadConfig();
|
|
2640
2736
|
const profileName = flags.profile || config.currentProfile;
|
|
2641
2737
|
const target = getWorkspaceTarget(config, profileName, flags);
|
|
@@ -3523,13 +3619,7 @@ function readResourceItemsFromFile(filePath, spec) {
|
|
|
3523
3619
|
__error: '资源项必须是 JSON object',
|
|
3524
3620
|
};
|
|
3525
3621
|
}
|
|
3526
|
-
const code =
|
|
3527
|
-
item.code ||
|
|
3528
|
-
item.resourceCode ||
|
|
3529
|
-
item.methodName ||
|
|
3530
|
-
(spec.key === 'notifications' ? inferNotificationResourceCode(item) : undefined) ||
|
|
3531
|
-
item.formCode ||
|
|
3532
|
-
(values.length === 1 ? defaultCode : undefined);
|
|
3622
|
+
const code = inferResourceCode(item, spec, values.length === 1 ? defaultCode : undefined);
|
|
3533
3623
|
return {
|
|
3534
3624
|
...item,
|
|
3535
3625
|
...(code ? { code } : {}),
|
|
@@ -3540,6 +3630,19 @@ function readResourceItemsFromFile(filePath, spec) {
|
|
|
3540
3630
|
});
|
|
3541
3631
|
}
|
|
3542
3632
|
|
|
3633
|
+
function inferResourceCode(item, spec, defaultCode) {
|
|
3634
|
+
if (item.code || item.resourceCode || item.methodName) {
|
|
3635
|
+
return item.code || item.resourceCode || item.methodName;
|
|
3636
|
+
}
|
|
3637
|
+
if (spec.key === 'notifications') {
|
|
3638
|
+
return inferNotificationResourceCode(item) || defaultCode;
|
|
3639
|
+
}
|
|
3640
|
+
if (spec.key === 'formSettings') {
|
|
3641
|
+
return item.formCode || defaultCode;
|
|
3642
|
+
}
|
|
3643
|
+
return defaultCode;
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3543
3646
|
function inferNotificationResourceCode(item) {
|
|
3544
3647
|
const resourceType = normalizeNotificationResourceType(item);
|
|
3545
3648
|
if (resourceType === 'template') return item.code || item.templateCode;
|
package/lib/utils.js
CHANGED
|
@@ -28,6 +28,10 @@ function parseArgs(argv) {
|
|
|
28
28
|
const positional = [];
|
|
29
29
|
for (let index = 0; index < argv.length; index += 1) {
|
|
30
30
|
const item = argv[index];
|
|
31
|
+
if (item === '-h') {
|
|
32
|
+
flags.h = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
31
35
|
if (!item.startsWith('--')) {
|
|
32
36
|
positional.push(item);
|
|
33
37
|
continue;
|
|
@@ -147,7 +147,9 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
147
147
|
- Run write commands that update `.openxiangda/state.json` sequentially within the same workspace. Read-only commands can run in parallel.
|
|
148
148
|
- JS_CODE is backend-executed workflow/automation logic, not frontend page code. Use it when logic must run after a backend trigger such as fixed cron schedules, form date-field schedules, form submit/update/delete/field-change events, or workflow approval/process events.
|
|
149
149
|
- Use JS_CODE for node-local cross-form data queries, create/update/batch update operations, process termination, platform API calls, external HTTP calls, and backend trigger orchestration. For logic that pages, automations, and workflows should reuse through a stable backend entry, prefer App Function. Do not use JS_CODE for simple UI interactions, ordinary form validation, or display-only page behavior.
|
|
150
|
-
- For workflow/automation JS_CODE nodes, prefer V2 `runtimeMode: "trusted_node"`. AI-authored source must be TypeScript under `
|
|
150
|
+
- For workflow/automation JS_CODE nodes, prefer V2 `runtimeMode: "trusted_node"`. AI-authored source must be TypeScript under `src/js-code-nodes/<scriptCode>/index.ts`, `src/automations/<resourceCode>/index.ts`, or `src/functions/<functionCode>/index.ts`. `pnpm build-js-code --script <scriptCode>` runs TypeScript validation before bundling, and `sourceFile.localPath` should point to the TS source; the CLI builds, uploads, and replaces it with snapshot metadata during validate/create.
|
|
151
|
+
- Form permission group resources must have stable local codes. Use unique `code` values such as `ticket_reporter_view` and `ticket_repairer_view`; do not rely on the form code when one form has multiple groups.
|
|
152
|
+
- Workflow resources can be created as drafts. After resource publish, verify `openxiangda workflow list --profile <name> --json` and run `openxiangda workflow publish <workflowCode> --profile <name>` until `isPublished: true` is visible.
|
|
151
153
|
|
|
152
154
|
## Subskills
|
|
153
155
|
|
|
@@ -73,6 +73,8 @@ Do not insult the person. Criticize the proposal and explain the technical conse
|
|
|
73
73
|
| Live view for unbounded heavy joins | Slow runtime queries | Materialized view with scheduled refresh and indexes |
|
|
74
74
|
| JS_CODE for reusable business service | Logic gets duplicated in graph nodes | App Function for shared backend logic; JS_CODE only for node-local trigger logic |
|
|
75
75
|
| Raw native form controls | Inconsistent with platform runtime and validation | OpenXiangda platform components first, Ant Design wrappers second |
|
|
76
|
+
| Multiple form permission groups without stable local codes | CLI state and platform `resourceCode` cannot reliably distinguish groups on the same form | Give every group a unique `code`, for example `ticket_reporter_view` and `ticket_repairer_view` |
|
|
77
|
+
| Assuming resource publish means workflow is active | Workflow resources may exist as drafts until explicitly published | Verify `workflow list` shows `isPublished: true`; run `workflow publish <workflowCode>` when needed |
|
|
76
78
|
|
|
77
79
|
## Question Gates
|
|
78
80
|
|
|
@@ -188,6 +190,7 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
188
190
|
- App roles use stable local codes.
|
|
189
191
|
- Page permission groups control entry/menu visibility.
|
|
190
192
|
- Form permission groups enforce data access.
|
|
193
|
+
- Form permission group resources use unique stable `code` values. Do not use the same form code for multiple groups.
|
|
191
194
|
- Field permissions hide sensitive fields where backend support exists.
|
|
192
195
|
- Frontend-only button hiding is UX, not security.
|
|
193
196
|
|
|
@@ -207,6 +210,8 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
207
210
|
- Use automation for backend triggers and schedules.
|
|
208
211
|
- Use JS_CODE V2 trusted_node for node-local backend logic.
|
|
209
212
|
- Use App Function for reusable backend services.
|
|
213
|
+
- After publishing workflow resources, verify `workflow list` and record whether `isPublished` is true.
|
|
214
|
+
- In React SPA and classic workspaces, JS_CODE/App Function TypeScript lives under `src/js-code-nodes`, `src/automations`, or `src/functions`; build with `pnpm build-js-code`.
|
|
210
215
|
|
|
211
216
|
## Final Document Template
|
|
212
217
|
|
|
@@ -274,6 +279,7 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
274
279
|
- Performance/query checks:
|
|
275
280
|
- Report freshness checks:
|
|
276
281
|
- Notification/automation checks:
|
|
282
|
+
- Workflow checks: real approval flows show `isPublished: true`.
|
|
277
283
|
|
|
278
284
|
## 11. Open Questions And Confirmed Assumptions
|
|
279
285
|
|
|
@@ -287,7 +287,7 @@ Use JS_CODE V2 when the script is local to one automation graph node.
|
|
|
287
287
|
}
|
|
288
288
|
```
|
|
289
289
|
|
|
290
|
-
Author source in `
|
|
290
|
+
Author source in `src/js-code-nodes/<scriptCode>/index.ts`. AI-authored source must be TypeScript. Build with `pnpm build-js-code --script <scriptCode>`; the command runs TypeScript validation before bundling. During validate/create, the CLI uploads the generated bundle, replaces `sourceFile.localPath` with `{ bucketName, objectName, sha256, ... }`, and the backend verifies sha256 before execution.
|
|
291
291
|
|
|
292
292
|
The backend runs the snapshot in the trusted Node runtime, applies the node timeout (`30000` ms by default), stores execution logs, and writes the returned value to the node output and `variables.node_<nodeId>`. Scripts may use `export default async function (ctx) {}`, `module.exports = async (ctx) => {}`, `require`, `process`, `Buffer`, arbitrary HTTP, and `platform.api` for `/openxiangda-api/v1`.
|
|
293
293
|
|
|
@@ -371,6 +371,8 @@ export default async function (ctx) {
|
|
|
371
371
|
|
|
372
372
|
## 10. Form Permission Group — `src/resources/permissions/form-groups/<formCode>/<groupCode>.json`
|
|
373
373
|
|
|
374
|
+
每个表单权限组必须有稳定、唯一的 `code`。同一张表单通常会有多个查看/提交/管理员权限组,不能把 `formCode` 当成权限组 code,否则本地 state 和平台 `resourceCode` 会把不同组混在一起。
|
|
375
|
+
|
|
374
376
|
```json
|
|
375
377
|
{
|
|
376
378
|
"code": "sales_view",
|
|
@@ -455,3 +457,4 @@ export default async function (ctx) {
|
|
|
455
457
|
- ❌ 用 data view 代替 `linkedForm` 下拉、单表 CRUD 或写回。**data view 是只读;materialized 有刷新延迟,live 要控制查询边界。**
|
|
456
458
|
- ❌ 把跨页面/跨流程复用的后端逻辑都塞进 JS_CODE。**优先 App Function,JS_CODE 只做节点内脚本。**
|
|
457
459
|
- ❌ 在 page 源码里 hardcode `/api/notification-config/*` 或 `/connectors/actions/invoke`。**用 `sdk.notification` / `sdk.connector`。**
|
|
460
|
+
- ❌ 发布 workflow resource 后不检查是否激活。**用 `openxiangda workflow list --profile <name> --json` 确认 `isPublished: true`,需要时再跑 `openxiangda workflow publish <workflowCode>`。**
|
|
@@ -133,7 +133,7 @@ File snapshot:
|
|
|
133
133
|
}
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
AI-authored JS_CODE source must be TypeScript under `
|
|
136
|
+
AI-authored JS_CODE source must be TypeScript under `src/js-code-nodes/<scriptCode>/index.ts`. When validating or creating, the CLI runs `pnpm build-js-code --script <scriptCode>`, which runs TypeScript validation first, bundles to `dist/js-code-nodes/<scriptCode>/index.cjs`, uploads the bundle, and replaces `sourceFile.localPath` with immutable snapshot metadata. The backend verifies snapshot `sha256`, runs it in the trusted Node runtime, applies the node timeout (`30000` ms by default), stores execution logs, and writes the returned value to the node output and `variables.node_<nodeId>`.
|
|
137
137
|
|
|
138
138
|
For reusable backend logic that should be shared by pages, automations, and workflows, prefer an App Function under `src/functions/<functionCode>/index.ts` plus `src/resources/functions/<functionCode>.json`. Workflow graphs that support App Function nodes can call it with:
|
|
139
139
|
|
|
@@ -24,7 +24,7 @@ Create a workflow only when the scenario has **real approval semantics**: approv
|
|
|
24
24
|
- ✅ New AI-authored automations: prefer **code-first** `automation_code_ts` (`src/automations/<code>/index.ts` + `definition.code.json` + `preview.json`).
|
|
25
25
|
- ✅ New AI-authored workflows that don't need canvas editing: prefer **code-first** `src/workflows/<code>/workflow.ts` using `openxiangda/workflow`.
|
|
26
26
|
- ✅ Reusable backend business logic: prefer **App Function** (`src/functions/<functionCode>/index.ts` + `src/resources/functions/<functionCode>.json`), then call it from page `sdk.function.invoke` or automation/workflow `function_call`.
|
|
27
|
-
- ✅ JS_CODE V2 trusted_node: source in TypeScript under `
|
|
27
|
+
- ✅ JS_CODE V2 trusted_node: source in TypeScript under `src/js-code-nodes/<scriptCode>/index.ts`, `src/automations/<resourceCode>/index.ts`, or `src/functions/<functionCode>/index.ts`. Run `pnpm build-js-code --script <code>` (validates with `tsc` first).
|
|
28
28
|
- ✅ Use `trigger_v2` for new automation triggers; CLI fills root `appType` / `formUuid` from active profile when `--form-code` is provided.
|
|
29
29
|
- ✅ Use logical `workflowCode` / `automationCode` locally; live IDs are profile-isolated under `.openxiangda/state.json`.
|
|
30
30
|
- ✅ `ctx.logger.debug/info/warn/error(message, data?)` at every important step — inspect via `automation executions` / `automation logs` / `automation diagnose`.
|
|
@@ -70,9 +70,10 @@ openxiangda form bind customer --form-uuid FORM_XXX --profile dev
|
|
|
70
70
|
4. Publish:
|
|
71
71
|
```bash
|
|
72
72
|
openxiangda workflow publish customer_approval --profile dev
|
|
73
|
+
openxiangda workflow list --profile dev --json
|
|
73
74
|
```
|
|
74
75
|
|
|
75
|
-
Use `workflow pull` to inspect the live definition. Use logical workflow codes locally; never copy a workflow ID from one profile to another.
|
|
76
|
+
Use `workflow pull` to inspect the live definition. Use `workflow list --json` after publishing and confirm the target shows `isPublished: true`; resource publish can create/update a draft without activating it. Use logical workflow codes locally; never copy a workflow ID from one profile to another.
|
|
76
77
|
|
|
77
78
|
## JS_CODE V2
|
|
78
79
|
|
|
@@ -86,7 +87,7 @@ For new AI-authored workflows where users do not need canvas editing, prefer com
|
|
|
86
87
|
|
|
87
88
|
Do not use JS_CODE for simple UI interactions, ordinary form validation, display-only page behavior, or logic that belongs in a normal React code page. For non-trivial backend logic, prefer JS_CODE V2 trusted Node scripts over large inline snippets. AI-authored JS_CODE source must be TypeScript:
|
|
88
89
|
|
|
89
|
-
1. Put source in `
|
|
90
|
+
1. Put source in `src/js-code-nodes/<scriptCode>/index.ts`.
|
|
90
91
|
2. Run `pnpm build-js-code --script <scriptCode>`. This command runs TypeScript validation first and only bundles after `tsc` passes.
|
|
91
92
|
3. In workflow or automation JSON, use:
|
|
92
93
|
```json
|
|
@@ -106,7 +107,7 @@ Do not use JS_CODE for simple UI interactions, ordinary form validation, display
|
|
|
106
107
|
}
|
|
107
108
|
```
|
|
108
109
|
|
|
109
|
-
The CLI requires `sourceFile.localPath` to point to `src/js-code-nodes/<scriptCode>/index.ts`. During validate/create it runs `pnpm build-js-code --script <scriptCode>`, uploads the generated
|
|
110
|
+
The CLI requires `sourceFile.localPath` to point to TypeScript source in `src/js-code-nodes/<scriptCode>/index.ts`, `src/automations/<resourceCode>/index.ts`, or `src/functions/<functionCode>/index.ts`. During validate/create it runs `pnpm build-js-code --script <scriptCode>`, uploads the generated bundle to `/file/js-code-snapshot/upload`, verifies the server snapshot metadata, and replaces it with `{ bucketName, objectName, sha256, ... }`.
|
|
110
111
|
|
|
111
112
|
The backend verifies the uploaded snapshot sha256 before execution, runs it in the trusted Node runtime, applies the node timeout (`30000` ms by default), stores console/runtime logs in the execution record, and writes the returned value to the node output and `variables.node_<nodeId>`.
|
|
112
113
|
|
package/package.json
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
pnpm install
|
|
19
19
|
pnpm dev
|
|
20
20
|
pnpm typecheck
|
|
21
|
+
pnpm typecheck:js-code
|
|
21
22
|
pnpm build
|
|
23
|
+
pnpm build-js-code
|
|
22
24
|
openxiangda workspace publish --profile <name> --form <formCode>
|
|
23
25
|
openxiangda resource plan --profile <name>
|
|
24
26
|
openxiangda resource publish --profile <name>
|
|
@@ -33,6 +35,8 @@ openxiangda resource publish --profile <name>
|
|
|
33
35
|
openxiangda runtime deploy --profile <name>
|
|
34
36
|
```
|
|
35
37
|
|
|
38
|
+
`pnpm build-js-code` 会检查并打包 `src/js-code-nodes/<code>/index.ts`、`src/automations/<code>/index.ts`、`src/functions/<code>/index.ts`,供 JS_CODE V2、代码自动化和 App Function 资源发布使用。
|
|
39
|
+
|
|
36
40
|
`openxiangda runtime deploy` 会构建 `dist/`、上传应用前端包并激活当前版本。不要手工修改 `dist/index.html`。
|
|
37
41
|
|
|
38
42
|
## 应用结构
|
|
@@ -41,6 +45,8 @@ openxiangda runtime deploy --profile <name>
|
|
|
41
45
|
- `src/pages/admin/AdminDashboardPage.tsx`:默认首页。
|
|
42
46
|
- `src/pages/defaults/*`:表单、流程、数据列表、文件预览等默认页。
|
|
43
47
|
- `src/runtime/default-page-overrides.tsx`:整页覆盖默认页的入口。
|
|
48
|
+
- `src/js-code-nodes/*`、`src/automations/*`、`src/functions/*`:后端执行脚本源码。
|
|
49
|
+
- `scripts/build-js-code.mjs`:JS_CODE V2、代码自动化和 App Function 的 TypeScript 构建脚本。
|
|
44
50
|
|
|
45
51
|
## 权限资源
|
|
46
52
|
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
8
8
|
"build": "vite build",
|
|
9
|
+
"build-js-code": "node scripts/build-js-code.mjs",
|
|
10
|
+
"build:js-code": "node scripts/build-js-code.mjs",
|
|
9
11
|
"build:forms": "lowcode-workspace build-forms",
|
|
10
12
|
"sync-schema": "lowcode-workspace sync-schema",
|
|
11
13
|
"publish:all": "lowcode-workspace publish-all",
|
|
12
14
|
"openxiangda:publish": "lowcode-workspace publish-all",
|
|
13
15
|
"typecheck": "tsc -p tsconfig.app.json --noEmit",
|
|
16
|
+
"typecheck:js-code": "tsc -p tsconfig.js-code-nodes.json --noEmit",
|
|
14
17
|
"check": "pnpm typecheck && pnpm build",
|
|
15
18
|
"resources:plan": "openxiangda resource plan",
|
|
16
19
|
"resources:publish": "openxiangda resource publish",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { builtinModules } from "node:module";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { readdir, stat } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { build } from "vite";
|
|
9
|
+
|
|
10
|
+
const rootDir = fileURLToPath(new URL("..", import.meta.url));
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
const sourceKinds = {
|
|
14
|
+
"js-code-nodes": {
|
|
15
|
+
name: "js-code-nodes",
|
|
16
|
+
sourceRoot: path.join(rootDir, "src", "js-code-nodes"),
|
|
17
|
+
outputRoot: path.join(rootDir, "dist", "js-code-nodes"),
|
|
18
|
+
label: "JS_CODE",
|
|
19
|
+
},
|
|
20
|
+
automations: {
|
|
21
|
+
name: "automations",
|
|
22
|
+
sourceRoot: path.join(rootDir, "src", "automations"),
|
|
23
|
+
outputRoot: path.join(rootDir, "dist", "automations"),
|
|
24
|
+
label: "automation code",
|
|
25
|
+
},
|
|
26
|
+
functions: {
|
|
27
|
+
name: "functions",
|
|
28
|
+
sourceRoot: path.join(rootDir, "src", "functions"),
|
|
29
|
+
outputRoot: path.join(rootDir, "dist", "functions"),
|
|
30
|
+
label: "app function",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function readArg(name) {
|
|
35
|
+
const prefix = `--${name}=`;
|
|
36
|
+
const inline = args.find((arg) => arg.startsWith(prefix));
|
|
37
|
+
if (inline) return inline.slice(prefix.length);
|
|
38
|
+
const index = args.indexOf(`--${name}`);
|
|
39
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function listScriptCodes(kind) {
|
|
43
|
+
if (!existsSync(kind.sourceRoot)) return [];
|
|
44
|
+
const entries = await readdir(kind.sourceRoot);
|
|
45
|
+
const result = [];
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const entryDir = path.join(kind.sourceRoot, entry);
|
|
48
|
+
const entryStat = await stat(entryDir);
|
|
49
|
+
if (!entryStat.isDirectory()) continue;
|
|
50
|
+
if (existsSync(path.join(entryDir, "index.ts"))) result.push(entry);
|
|
51
|
+
}
|
|
52
|
+
return result.sort();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function typecheckScripts() {
|
|
56
|
+
const result = spawnSync(
|
|
57
|
+
"pnpm",
|
|
58
|
+
["exec", "tsc", "-p", "tsconfig.js-code-nodes.json", "--noEmit"],
|
|
59
|
+
{
|
|
60
|
+
cwd: rootDir,
|
|
61
|
+
stdio: "inherit",
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
if (result.status !== 0) throw new Error("JS_CODE TypeScript validation failed");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function buildScript(kind, scriptCode) {
|
|
68
|
+
const entry = path.join(kind.sourceRoot, scriptCode, "index.ts");
|
|
69
|
+
if (!existsSync(entry)) throw new Error(`${kind.label} script not found: ${entry}`);
|
|
70
|
+
|
|
71
|
+
const outDir = path.join(kind.outputRoot, scriptCode);
|
|
72
|
+
const external = Array.from(
|
|
73
|
+
new Set([...builtinModules, ...builtinModules.map((name) => `node:${name}`)]),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await build({
|
|
77
|
+
configFile: false,
|
|
78
|
+
root: rootDir,
|
|
79
|
+
logLevel: "warn",
|
|
80
|
+
resolve: {
|
|
81
|
+
alias: {
|
|
82
|
+
"@": path.join(rootDir, "src"),
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
build: {
|
|
86
|
+
ssr: entry,
|
|
87
|
+
outDir,
|
|
88
|
+
emptyOutDir: true,
|
|
89
|
+
target: "node20",
|
|
90
|
+
minify: false,
|
|
91
|
+
sourcemap: true,
|
|
92
|
+
rollupOptions: {
|
|
93
|
+
external,
|
|
94
|
+
output: {
|
|
95
|
+
format: "cjs",
|
|
96
|
+
entryFileNames: "index.cjs",
|
|
97
|
+
inlineDynamicImports: true,
|
|
98
|
+
exports: "auto",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log(`built ${kind.name}/${scriptCode} -> ${path.relative(rootDir, path.join(outDir, "index.cjs"))}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const onlyScript = readArg("script");
|
|
108
|
+
const sourceArg = readArg("source");
|
|
109
|
+
const selectedKind = sourceArg ? sourceKinds[sourceArg] : undefined;
|
|
110
|
+
if (sourceArg && !selectedKind) {
|
|
111
|
+
throw new Error(`unsupported source: ${sourceArg}. Expected js-code-nodes, automations, or functions`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function resolveBuildTargets() {
|
|
115
|
+
if (onlyScript) {
|
|
116
|
+
if (selectedKind) return [{ kind: selectedKind, scriptCode: onlyScript }];
|
|
117
|
+
for (const kind of Object.values(sourceKinds)) {
|
|
118
|
+
if (existsSync(path.join(kind.sourceRoot, onlyScript, "index.ts"))) {
|
|
119
|
+
return [{ kind, scriptCode: onlyScript }];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return [{ kind: sourceKinds["js-code-nodes"], scriptCode: onlyScript }];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const kinds = selectedKind ? [selectedKind] : Object.values(sourceKinds);
|
|
126
|
+
const targets = [];
|
|
127
|
+
for (const kind of kinds) {
|
|
128
|
+
for (const scriptCode of await listScriptCodes(kind)) {
|
|
129
|
+
targets.push({ kind, scriptCode });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return targets;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const targets = await resolveBuildTargets();
|
|
136
|
+
|
|
137
|
+
if (targets.length === 0) {
|
|
138
|
+
console.log("no JS_CODE scripts found under src/js-code-nodes/<scriptCode>/index.ts, src/automations/<scriptCode>/index.ts, or src/functions/<functionCode>/index.ts");
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
typecheckScripts();
|
|
143
|
+
|
|
144
|
+
for (const target of targets) {
|
|
145
|
+
await buildScript(target.kind, target.scriptCode);
|
|
146
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
"baseUrl": ".",
|
|
10
|
+
"paths": {
|
|
11
|
+
"@/*": ["src/*"]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"src/js-code-nodes/**/*.ts",
|
|
16
|
+
"src/automations/**/*.ts",
|
|
17
|
+
"src/functions/**/*.ts"
|
|
18
|
+
]
|
|
19
|
+
}
|