openxiangda 1.0.25 → 1.0.27
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 +16 -0
- package/lib/cli.js +324 -17
- package/openxiangda-skills/SKILL.md +1 -0
- package/openxiangda-skills/references/automation-v3.md +56 -0
- package/openxiangda-skills/references/openxiangda-api.md +4 -0
- package/openxiangda-skills/references/pages/publish-flow.md +7 -0
- package/openxiangda-skills/references/workflow-v3.md +44 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +2 -0
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +4 -0
- package/package.json +6 -1
- package/packages/sdk/dist/workflow/index.cjs +230 -0
- package/packages/sdk/dist/workflow/index.cjs.map +1 -0
- package/packages/sdk/dist/workflow/index.d.mts +61 -0
- package/packages/sdk/dist/workflow/index.d.ts +61 -0
- package/packages/sdk/dist/workflow/index.mjs +209 -0
- package/packages/sdk/dist/workflow/index.mjs.map +1 -0
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +56 -16
- package/templates/sy-lowcode-app-workspace/tsconfig.js-code-nodes.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,8 @@ openxiangda workspace publish --profile dev
|
|
|
25
25
|
openxiangda menu list --profile dev
|
|
26
26
|
openxiangda workflow list --profile dev
|
|
27
27
|
openxiangda automation list --profile dev
|
|
28
|
+
openxiangda automation executions daily_ticket_digest --profile dev
|
|
29
|
+
openxiangda automation diagnose daily_ticket_digest --profile dev
|
|
28
30
|
openxiangda permission role-list --profile dev
|
|
29
31
|
openxiangda settings get customer --profile dev
|
|
30
32
|
openxiangda resource validate --profile dev
|
|
@@ -78,6 +80,20 @@ openxiangda workspace bind --profile dev --app-type APP_XXXX
|
|
|
78
80
|
|
|
79
81
|
工程化资源放在工作区 `src/resources/` 下,由 `openxiangda resource validate|plan|publish|pull` 管理。`workspace publish` 会先构建并注册 workspace 表单/页面,再执行非破坏性资源 upsert,这样菜单、权限组、流程和表单设置可以解析最新的 profile-local ID。需要删除平台中 manifest 未声明的资源时,显式传 `--prune`。连接器页面运行时通过 `sdk.connector.invoke()` / `sdk.connector.call("connector.api")` 调用平台运行时接口,第三方密钥只保存在后端连接器配置中。
|
|
80
82
|
|
|
83
|
+
AI-authored automation can use code-first resources:
|
|
84
|
+
|
|
85
|
+
- Source: `src/automations/<resourceCode>/index.ts`
|
|
86
|
+
- Definition: `src/resources/automations/<resourceCode>/definition.code.json`
|
|
87
|
+
- Preview: `src/resources/automations/<resourceCode>/preview.json`
|
|
88
|
+
- Logs: `openxiangda automation executions|logs|diagnose`
|
|
89
|
+
|
|
90
|
+
AI-authored workflows can use compile-time SDK resources:
|
|
91
|
+
|
|
92
|
+
- Source: `src/workflows/<resourceCode>/workflow.ts`
|
|
93
|
+
- Manifest: `src/resources/workflows/<resourceCode>/workflow.json`
|
|
94
|
+
- Compiled graph: `definition.v3.json`
|
|
95
|
+
- Read-only preview: `preview.json`
|
|
96
|
+
|
|
81
97
|
Resource and connector manifest guide: [docs/openxiangda-resources-and-connectors.md](docs/openxiangda-resources-and-connectors.md).
|
|
82
98
|
|
|
83
99
|
Skill migration plan: [docs/skill-refactor-plan.md](docs/skill-refactor-plan.md).
|
package/lib/cli.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const os = require('os');
|
|
4
5
|
const { spawnSync } = require('child_process');
|
|
6
|
+
const { pathToFileURL } = require('url');
|
|
7
|
+
const esbuild = require('esbuild');
|
|
5
8
|
const {
|
|
6
9
|
CONFIG_FILE,
|
|
7
10
|
PROJECT_STATE_FILE,
|
|
@@ -94,6 +97,9 @@ Usage:
|
|
|
94
97
|
openxiangda automation list [--profile name] [--json]
|
|
95
98
|
openxiangda automation create <automationCode> --name <text> --trigger-json <file> --definition-json <file>
|
|
96
99
|
openxiangda automation bind <automationCode> --automation-id <id>
|
|
100
|
+
openxiangda automation executions <automationCode|automationId> [--status failed] [--json]
|
|
101
|
+
openxiangda automation logs <instanceId> [--automation <automationCode|automationId>] [--summary] [--redact] [--json]
|
|
102
|
+
openxiangda automation diagnose <automationCode|automationId> [--redact] [--json]
|
|
97
103
|
openxiangda automation publish|enable|disable <automationCode|automationId>
|
|
98
104
|
openxiangda permission role-list|role-create|role-bind
|
|
99
105
|
openxiangda permission page-group-list|page-group-create|page-group-bind
|
|
@@ -106,7 +112,7 @@ Usage:
|
|
|
106
112
|
|
|
107
113
|
OpenXiangda 使用普通用户登录 token,不需要 AK/SK。
|
|
108
114
|
表单页、流程表单页和代码页的主链路是 sy-lowcode-app-workspace + openxiangda workspace publish。
|
|
109
|
-
JS_CODE V2 使用 trusted_node;AI 源码必须写在 src/js-code-nodes/<scriptCode>/index.ts
|
|
115
|
+
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 校验、再构建并上传为快照。`);
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
async function update(args) {
|
|
@@ -840,6 +846,81 @@ async function form(args) {
|
|
|
840
846
|
return;
|
|
841
847
|
}
|
|
842
848
|
|
|
849
|
+
if (subcommand === 'executions') {
|
|
850
|
+
const [automationKey] = positional;
|
|
851
|
+
if (!automationKey) fail('用法: openxiangda automation executions <automationCode|automationId>');
|
|
852
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
853
|
+
const automationId = resolveAutomationId(target.bound, automationKey, flags);
|
|
854
|
+
const data = await requestWithAuth(
|
|
855
|
+
config,
|
|
856
|
+
target.profileName,
|
|
857
|
+
apiPathWithQuery(
|
|
858
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automationId)}/executions`,
|
|
859
|
+
{
|
|
860
|
+
status: flags.status,
|
|
861
|
+
triggerEventType: flags['trigger-type'],
|
|
862
|
+
startDate: flags['start-date'],
|
|
863
|
+
endDate: flags['end-date'],
|
|
864
|
+
page: flags.page,
|
|
865
|
+
pageSize: flags['page-size'] || flags.limit,
|
|
866
|
+
}
|
|
867
|
+
)
|
|
868
|
+
);
|
|
869
|
+
if (flags.json) return writeJson(data);
|
|
870
|
+
printAutomationExecutionList(data);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (subcommand === 'logs') {
|
|
875
|
+
const [instanceId] = positional;
|
|
876
|
+
if (!instanceId) fail('用法: openxiangda automation logs <instanceId> [--automation <automationCode|automationId>]');
|
|
877
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
878
|
+
const data = await getAutomationExecutionDetailFromCli(
|
|
879
|
+
config,
|
|
880
|
+
target,
|
|
881
|
+
instanceId,
|
|
882
|
+
flags.automation || flags['automation-code'] || flags['automation-id']
|
|
883
|
+
);
|
|
884
|
+
const output = flags.redact ? redactLogValue(data) : data;
|
|
885
|
+
if (flags.json) return writeJson(output);
|
|
886
|
+
printAutomationExecutionLogs(output, { summary: Boolean(flags.summary) });
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (subcommand === 'diagnose') {
|
|
891
|
+
const [automationKey] = positional;
|
|
892
|
+
if (!automationKey) fail('用法: openxiangda automation diagnose <automationCode|automationId>');
|
|
893
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
894
|
+
const automationId = resolveAutomationId(target.bound, automationKey, flags);
|
|
895
|
+
const records = await requestWithAuth(
|
|
896
|
+
config,
|
|
897
|
+
target.profileName,
|
|
898
|
+
apiPathWithQuery(
|
|
899
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automationId)}/executions`,
|
|
900
|
+
{
|
|
901
|
+
status: flags.status || 'failed',
|
|
902
|
+
page: 1,
|
|
903
|
+
pageSize: flags['page-size'] || flags.limit || 1,
|
|
904
|
+
}
|
|
905
|
+
)
|
|
906
|
+
);
|
|
907
|
+
const first = records?.data?.[0];
|
|
908
|
+
if (!first?.id) {
|
|
909
|
+
if (flags.json) return writeJson({ found: false, message: '未找到失败执行记录' });
|
|
910
|
+
print('未找到失败执行记录');
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const detail = await requestWithAuth(
|
|
914
|
+
config,
|
|
915
|
+
target.profileName,
|
|
916
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automationId)}/executions/${encodeURIComponent(first.id)}`
|
|
917
|
+
);
|
|
918
|
+
const output = flags.redact ? redactLogValue(detail) : detail;
|
|
919
|
+
if (flags.json) return writeJson({ found: true, detail: output, summary: buildAutomationDiagnosis(output) });
|
|
920
|
+
print(buildAutomationDiagnosis(output));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
843
924
|
if (subcommand === 'publish') {
|
|
844
925
|
const [formKey] = positional;
|
|
845
926
|
if (!formKey || !flags['bundle-url']) {
|
|
@@ -1460,7 +1541,7 @@ async function automation(args) {
|
|
|
1460
1541
|
return;
|
|
1461
1542
|
}
|
|
1462
1543
|
|
|
1463
|
-
fail('用法: openxiangda automation list|create|bind|pull|publish|unpublish|enable|disable|delete|validate|cron-validate');
|
|
1544
|
+
fail('用法: openxiangda automation list|create|bind|pull|executions|logs|diagnose|publish|unpublish|enable|disable|delete|validate|cron-validate');
|
|
1464
1545
|
}
|
|
1465
1546
|
|
|
1466
1547
|
async function permission(args) {
|
|
@@ -2724,8 +2805,8 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
2724
2805
|
if (kind === 'menus' && !item.name) errors.push(`${label}: 缺少 name`);
|
|
2725
2806
|
if (kind === 'workflows') {
|
|
2726
2807
|
if (!item.formCode && !item.formUuid) errors.push(`${label}: 缺少 formCode 或 formUuid`);
|
|
2727
|
-
if (!item.definitionJson && !item.definitionFile) {
|
|
2728
|
-
errors.push(`${label}: 缺少 definitionJson 或
|
|
2808
|
+
if (!item.definitionJson && !item.definitionFile && !item.workflowFile) {
|
|
2809
|
+
errors.push(`${label}: 缺少 definitionJson、definitionFile 或 workflowFile`);
|
|
2729
2810
|
}
|
|
2730
2811
|
}
|
|
2731
2812
|
if (kind === 'automations') {
|
|
@@ -2852,6 +2933,158 @@ function resourceLabel(kind, item) {
|
|
|
2852
2933
|
return `${kind}:${item.code || item.__source || item.__index || '?'}`;
|
|
2853
2934
|
}
|
|
2854
2935
|
|
|
2936
|
+
async function getAutomationExecutionDetailFromCli(config, target, instanceId, automationKey) {
|
|
2937
|
+
const automationIds = [];
|
|
2938
|
+
if (automationKey) {
|
|
2939
|
+
automationIds.push(resolveAutomationId(target.bound, automationKey, {}));
|
|
2940
|
+
} else {
|
|
2941
|
+
for (const entry of Object.values(target.bound.resources?.automations || {})) {
|
|
2942
|
+
if (entry?.automationId) automationIds.push(entry.automationId);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
const uniqueAutomationIds = Array.from(new Set(automationIds.filter(Boolean)));
|
|
2946
|
+
if (uniqueAutomationIds.length === 0) {
|
|
2947
|
+
fail('无法定位自动化,请传入 --automation <automationCode|automationId> 或先绑定本地自动化资源');
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
let lastError;
|
|
2951
|
+
for (const automationId of uniqueAutomationIds) {
|
|
2952
|
+
try {
|
|
2953
|
+
return await requestWithAuth(
|
|
2954
|
+
config,
|
|
2955
|
+
target.profileName,
|
|
2956
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/automations/${encodeURIComponent(automationId)}/executions/${encodeURIComponent(instanceId)}`
|
|
2957
|
+
);
|
|
2958
|
+
} catch (error) {
|
|
2959
|
+
lastError = error;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
throw lastError || new Error(`执行记录不存在: ${instanceId}`);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
function printAutomationExecutionList(result) {
|
|
2966
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
2967
|
+
if (rows.length === 0) {
|
|
2968
|
+
print('暂无执行记录');
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
for (const item of rows) {
|
|
2972
|
+
print(
|
|
2973
|
+
[
|
|
2974
|
+
item.id,
|
|
2975
|
+
item.status,
|
|
2976
|
+
item.triggerEventType,
|
|
2977
|
+
`${item.executionDuration ?? 0}ms`,
|
|
2978
|
+
item.startedAt || item.createdAt || '',
|
|
2979
|
+
item.errorMessage ? `error=${item.errorMessage}` : '',
|
|
2980
|
+
]
|
|
2981
|
+
.filter(Boolean)
|
|
2982
|
+
.join(' ')
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2985
|
+
if (result?.totalCount !== undefined) {
|
|
2986
|
+
print(`共 ${result.totalCount} 条,page=${result.page || 1}, pageSize=${result.pageSize || rows.length}`);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
function printAutomationExecutionLogs(detail, options = {}) {
|
|
2991
|
+
if (options.summary) {
|
|
2992
|
+
print(buildAutomationDiagnosis(detail));
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
print(`执行记录: ${detail.id}`);
|
|
2996
|
+
print(`状态: ${detail.status}`);
|
|
2997
|
+
print(`自动化: ${detail.definitionName || detail.definitionId}`);
|
|
2998
|
+
print(`触发: ${detail.triggerEventType || ''} ${detail.triggerEventId || ''}`.trim());
|
|
2999
|
+
print(`耗时: ${detail.executionDuration ?? 0}ms`);
|
|
3000
|
+
if (detail.errorMessage) print(`错误: ${detail.errorMessage}`);
|
|
3001
|
+
if (detail.triggerEventData !== undefined) {
|
|
3002
|
+
print('\ntriggerEventData:');
|
|
3003
|
+
print(JSON.stringify(detail.triggerEventData, null, 2));
|
|
3004
|
+
}
|
|
3005
|
+
if (detail.result !== undefined && detail.result !== null) {
|
|
3006
|
+
print('\nresult:');
|
|
3007
|
+
print(typeof detail.result === 'string' ? detail.result : JSON.stringify(detail.result, null, 2));
|
|
3008
|
+
}
|
|
3009
|
+
print('\nlogs:');
|
|
3010
|
+
const executionLogs = Array.isArray(detail.nodeExecutionLogs) ? detail.nodeExecutionLogs : [];
|
|
3011
|
+
for (const entry of executionLogs) {
|
|
3012
|
+
if (entry?.logs && Array.isArray(entry.logs)) {
|
|
3013
|
+
print(`# ${entry.nodeLabel || entry.nodeId || entry.nodeType || 'node'} (${entry.success ? 'success' : 'failed'})`);
|
|
3014
|
+
for (const log of entry.logs) print(formatAutomationLogLine(log));
|
|
3015
|
+
} else {
|
|
3016
|
+
print(formatAutomationLogLine(entry));
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
function formatAutomationLogLine(log) {
|
|
3022
|
+
if (typeof log === 'string') return log;
|
|
3023
|
+
if (!log || typeof log !== 'object') return String(log);
|
|
3024
|
+
const prefix = [log.timestamp, log.level, log.stepKey ? `step=${log.stepKey}` : '']
|
|
3025
|
+
.filter(Boolean)
|
|
3026
|
+
.join(' ');
|
|
3027
|
+
const message = log.message || JSON.stringify(log);
|
|
3028
|
+
const data = log.data !== undefined ? ` ${JSON.stringify(log.data)}` : '';
|
|
3029
|
+
return `${prefix ? `[${prefix}] ` : ''}${message}${data}`;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
function buildAutomationDiagnosis(detail) {
|
|
3033
|
+
const lines = [];
|
|
3034
|
+
lines.push(`执行记录: ${detail.id}`);
|
|
3035
|
+
lines.push(`状态: ${detail.status}`);
|
|
3036
|
+
lines.push(`自动化: ${detail.definitionName || detail.definitionId}`);
|
|
3037
|
+
if (detail.errorMessage) lines.push(`错误: ${detail.errorMessage}`);
|
|
3038
|
+
lines.push(`耗时: ${detail.executionDuration ?? 0}ms`);
|
|
3039
|
+
const importantLogs = flattenAutomationLogs(detail.nodeExecutionLogs).filter(log => {
|
|
3040
|
+
if (typeof log === 'string') return /ERROR|WARN|异常|失败|超时/i.test(log);
|
|
3041
|
+
const level = String(log?.level || '').toUpperCase();
|
|
3042
|
+
return ['ERROR', 'WARN', 'CONSOLE ERROR', 'CONSOLE WARN'].includes(level);
|
|
3043
|
+
});
|
|
3044
|
+
if (importantLogs.length > 0) {
|
|
3045
|
+
lines.push('\n关键日志:');
|
|
3046
|
+
for (const log of importantLogs) lines.push(`- ${formatAutomationLogLine(log)}`);
|
|
3047
|
+
}
|
|
3048
|
+
if (detail.triggerEventData !== undefined) {
|
|
3049
|
+
lines.push('\n触发数据:');
|
|
3050
|
+
lines.push(JSON.stringify(detail.triggerEventData, null, 2));
|
|
3051
|
+
}
|
|
3052
|
+
if (detail.result !== undefined && detail.result !== null) {
|
|
3053
|
+
lines.push('\n返回结果:');
|
|
3054
|
+
lines.push(typeof detail.result === 'string' ? detail.result : JSON.stringify(detail.result, null, 2));
|
|
3055
|
+
}
|
|
3056
|
+
return lines.join('\n');
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
function flattenAutomationLogs(nodeExecutionLogs) {
|
|
3060
|
+
const result = [];
|
|
3061
|
+
for (const entry of Array.isArray(nodeExecutionLogs) ? nodeExecutionLogs : []) {
|
|
3062
|
+
if (Array.isArray(entry?.logs)) result.push(...entry.logs);
|
|
3063
|
+
else result.push(entry);
|
|
3064
|
+
}
|
|
3065
|
+
return result;
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
function redactLogValue(value) {
|
|
3069
|
+
if (typeof value === 'string') {
|
|
3070
|
+
return value
|
|
3071
|
+
.replace(/(access[_-]?token|refresh[_-]?token|authorization|password|secret|key)(["':= ]+)([^"',\s}]+)/gi, '$1$2***')
|
|
3072
|
+
.replace(/1[3-9]\d{9}/g, '1**********')
|
|
3073
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '***@***');
|
|
3074
|
+
}
|
|
3075
|
+
if (Array.isArray(value)) return value.map(redactLogValue);
|
|
3076
|
+
if (value && typeof value === 'object') {
|
|
3077
|
+
const next = {};
|
|
3078
|
+
for (const [key, item] of Object.entries(value)) {
|
|
3079
|
+
next[key] = /token|authorization|password|secret|credential/i.test(key)
|
|
3080
|
+
? '***'
|
|
3081
|
+
: redactLogValue(item);
|
|
3082
|
+
}
|
|
3083
|
+
return next;
|
|
3084
|
+
}
|
|
3085
|
+
return value;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
2855
3088
|
async function publishResourcesForWorkspace(config, profileName, options = {}) {
|
|
2856
3089
|
const manifest = loadWorkspaceResources();
|
|
2857
3090
|
const validation = validateWorkspaceResources(manifest);
|
|
@@ -3412,21 +3645,32 @@ async function publishNotificationResources(config, target, notifications, resul
|
|
|
3412
3645
|
async function publishWorkflowResources(config, target, workflows, result) {
|
|
3413
3646
|
for (const workflowItem of workflows) {
|
|
3414
3647
|
const existing = await findExistingWorkflow(config, target, workflowItem.code);
|
|
3415
|
-
const
|
|
3648
|
+
const compiledWorkflow = await compileWorkflowSourceFile(workflowItem);
|
|
3649
|
+
const definitionJson = compiledWorkflow?.definitionJson || await resolveManifestJson(
|
|
3416
3650
|
config,
|
|
3417
3651
|
target.profileName,
|
|
3418
3652
|
workflowItem,
|
|
3419
3653
|
'definitionJson',
|
|
3420
3654
|
'definitionFile'
|
|
3421
3655
|
);
|
|
3422
|
-
const viewJson =
|
|
3656
|
+
const viewJson =
|
|
3657
|
+
compiledWorkflow?.previewJson ??
|
|
3658
|
+
(await resolveManifestJson(
|
|
3659
|
+
config,
|
|
3660
|
+
target.profileName,
|
|
3661
|
+
workflowItem,
|
|
3662
|
+
'viewJson',
|
|
3663
|
+
'viewFile',
|
|
3664
|
+
true
|
|
3665
|
+
)) ??
|
|
3666
|
+
(await resolveManifestJson(
|
|
3423
3667
|
config,
|
|
3424
3668
|
target.profileName,
|
|
3425
3669
|
workflowItem,
|
|
3426
|
-
'
|
|
3427
|
-
'
|
|
3670
|
+
'previewJson',
|
|
3671
|
+
'previewFile',
|
|
3428
3672
|
true
|
|
3429
|
-
);
|
|
3673
|
+
));
|
|
3430
3674
|
const formUuid = resolveManifestFormUuid(target.bound, workflowItem);
|
|
3431
3675
|
const body = {
|
|
3432
3676
|
resourceCode: workflowItem.code,
|
|
@@ -3492,14 +3736,23 @@ async function publishAutomationResources(config, target, automations, result) {
|
|
|
3492
3736
|
'definitionJson',
|
|
3493
3737
|
'definitionFile'
|
|
3494
3738
|
);
|
|
3495
|
-
const viewJson =
|
|
3739
|
+
const viewJson =
|
|
3740
|
+
(await resolveManifestJson(
|
|
3741
|
+
config,
|
|
3742
|
+
target.profileName,
|
|
3743
|
+
automationItem,
|
|
3744
|
+
'viewJson',
|
|
3745
|
+
'viewFile',
|
|
3746
|
+
true
|
|
3747
|
+
)) ??
|
|
3748
|
+
(await resolveManifestJson(
|
|
3496
3749
|
config,
|
|
3497
3750
|
target.profileName,
|
|
3498
3751
|
automationItem,
|
|
3499
|
-
'
|
|
3500
|
-
'
|
|
3752
|
+
'previewJson',
|
|
3753
|
+
'previewFile',
|
|
3501
3754
|
true
|
|
3502
|
-
);
|
|
3755
|
+
));
|
|
3503
3756
|
const automationPayload = resolveAutomationManifestPayload(target, automationItem);
|
|
3504
3757
|
const body = {
|
|
3505
3758
|
resourceCode: automationItem.code,
|
|
@@ -4379,6 +4632,57 @@ async function resolveManifestJson(config, profileName, item, objectKey, fileKey
|
|
|
4379
4632
|
return value;
|
|
4380
4633
|
}
|
|
4381
4634
|
|
|
4635
|
+
async function compileWorkflowSourceFile(workflowItem) {
|
|
4636
|
+
if (!workflowItem.workflowFile) return undefined;
|
|
4637
|
+
const sourcePath = path.resolve(workflowItem.__dir || process.cwd(), workflowItem.workflowFile);
|
|
4638
|
+
if (!fs.existsSync(sourcePath)) {
|
|
4639
|
+
fail(`${resourceLabel('workflow', workflowItem)} workflowFile 不存在: ${sourcePath}`);
|
|
4640
|
+
}
|
|
4641
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openxiangda-workflow-'));
|
|
4642
|
+
const outfile = path.join(tmpDir, `${workflowItem.code || 'workflow'}.mjs`);
|
|
4643
|
+
try {
|
|
4644
|
+
await esbuild.build({
|
|
4645
|
+
entryPoints: [sourcePath],
|
|
4646
|
+
outfile,
|
|
4647
|
+
bundle: true,
|
|
4648
|
+
platform: 'node',
|
|
4649
|
+
format: 'esm',
|
|
4650
|
+
target: ['node18'],
|
|
4651
|
+
sourcemap: false,
|
|
4652
|
+
logLevel: 'silent',
|
|
4653
|
+
});
|
|
4654
|
+
const mod = await import(`${pathToFileURL(outfile).href}?t=${Date.now()}`);
|
|
4655
|
+
const exported = mod.default || mod.workflow || mod.definition;
|
|
4656
|
+
if (!exported) {
|
|
4657
|
+
fail(`${resourceLabel('workflow', workflowItem)} workflowFile 必须 default export defineWorkflow(...)`);
|
|
4658
|
+
}
|
|
4659
|
+
const compiled =
|
|
4660
|
+
typeof exported.compile === 'function'
|
|
4661
|
+
? exported.compile()
|
|
4662
|
+
: typeof exported === 'function'
|
|
4663
|
+
? exported()
|
|
4664
|
+
: exported;
|
|
4665
|
+
if (!compiled?.definitionJson || !compiled?.previewJson) {
|
|
4666
|
+
fail(`${resourceLabel('workflow', workflowItem)} workflowFile 编译结果必须包含 definitionJson 和 previewJson`);
|
|
4667
|
+
}
|
|
4668
|
+
if (workflowItem.definitionFile) {
|
|
4669
|
+
writeCompiledResourceFile(workflowItem, workflowItem.definitionFile, compiled.definitionJson);
|
|
4670
|
+
}
|
|
4671
|
+
if (workflowItem.previewFile) {
|
|
4672
|
+
writeCompiledResourceFile(workflowItem, workflowItem.previewFile, compiled.previewJson);
|
|
4673
|
+
}
|
|
4674
|
+
return compiled;
|
|
4675
|
+
} finally {
|
|
4676
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4680
|
+
function writeCompiledResourceFile(item, relativeFile, value) {
|
|
4681
|
+
const filePath = path.resolve(item.__dir || process.cwd(), relativeFile);
|
|
4682
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
4683
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
4684
|
+
}
|
|
4685
|
+
|
|
4382
4686
|
function resolveManifestFormUuid(bound, item, options = {}) {
|
|
4383
4687
|
const formKey = item.formCode || item.form || (options.fallbackToCode ? item.code : undefined);
|
|
4384
4688
|
if (formKey) {
|
|
@@ -4889,23 +5193,26 @@ function resolveJsCodeBundlePath(localPath, scriptCode) {
|
|
|
4889
5193
|
}
|
|
4890
5194
|
|
|
4891
5195
|
const normalized = localPath.replace(/\\/g, '/');
|
|
4892
|
-
const
|
|
5196
|
+
const jsCodeMatched = normalized.match(/src\/js-code-nodes\/([^/]+)\/index\.tsx?$/);
|
|
5197
|
+
const automationMatched = normalized.match(/src\/automations\/([^/]+)\/index\.tsx?$/);
|
|
5198
|
+
const matched = jsCodeMatched || automationMatched;
|
|
4893
5199
|
const resolvedScriptCode = scriptCode || matched?.[1];
|
|
4894
5200
|
if (!resolvedScriptCode) {
|
|
4895
5201
|
fail(
|
|
4896
|
-
`无法从 sourceFile.localPath 推断 scriptCode,请使用 src/js-code-nodes/<scriptCode>/index.ts: ${localPath}`
|
|
5202
|
+
`无法从 sourceFile.localPath 推断 scriptCode,请使用 src/js-code-nodes/<scriptCode>/index.ts 或 src/automations/<scriptCode>/index.ts: ${localPath}`
|
|
4897
5203
|
);
|
|
4898
5204
|
}
|
|
5205
|
+
const sourceKind = automationMatched ? 'automations' : 'js-code-nodes';
|
|
4899
5206
|
|
|
4900
5207
|
const workspaceRoot = findWorkspaceRoot(localPath);
|
|
4901
|
-
const result = spawnSync('pnpm', ['build-js-code', '--script', resolvedScriptCode], {
|
|
5208
|
+
const result = spawnSync('pnpm', ['build-js-code', '--script', resolvedScriptCode, '--source', sourceKind], {
|
|
4902
5209
|
cwd: workspaceRoot,
|
|
4903
5210
|
stdio: 'inherit',
|
|
4904
5211
|
});
|
|
4905
5212
|
if (result.status !== 0) {
|
|
4906
5213
|
fail(`JS_CODE bundle构建失败: ${resolvedScriptCode}`);
|
|
4907
5214
|
}
|
|
4908
|
-
return path.join(workspaceRoot, 'dist',
|
|
5215
|
+
return path.join(workspaceRoot, 'dist', sourceKind, resolvedScriptCode, 'index.cjs');
|
|
4909
5216
|
}
|
|
4910
5217
|
|
|
4911
5218
|
function findWorkspaceRoot(startPath) {
|
|
@@ -79,6 +79,7 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
79
79
|
- If there is no `sy-lowcode-app-workspace`, create one with `openxiangda workspace init <dir>` before writing forms, pages, or JS_CODE nodes.
|
|
80
80
|
- Never treat `openxiangda form create` as page generation. It only creates a low-level platform form shell for diagnostics or for workspace publish internals. The source of user-facing pages is `sy-lowcode-app-workspace`.
|
|
81
81
|
- Publish normal form pages, workflow form pages, and custom code pages through `openxiangda workspace publish --profile <name>` from the app workspace.
|
|
82
|
+
- For live platform publishing, do not call `lowcode-workspace publish-all`, `pnpm publish:all`, or legacy project scripts such as `scripts/openxiangda-publish.mjs` directly. They are workspace internals and may miss the normal-user token/profile injection. Use `openxiangda workspace publish ...` so `OPENXIANGDA_PROFILE`, `OPENXIANGDA_BASE_URL`, `OPENXIANGDA_ACCESS_TOKEN`, and `OPENXIANGDA_APP_TYPE` are injected consistently.
|
|
82
83
|
- For routine AI edits, avoid reflexive full publish. First run `openxiangda workspace publish --profile <name> --changed --dry-run`, then use `--changed`, `--page <pageCode>`, `--form <formCode>`, or `--only pages/a,forms/b`. Use full publish only when broad shared/config changes intentionally affect many modules.
|
|
83
84
|
- Never store token data in the project directory. User tokens live in `~/.openxiangda/profiles.json`; project state lives in `.openxiangda/state.json` and stores only IDs and mappings.
|
|
84
85
|
- Use logical resource codes in local files. Platform-specific IDs such as `formUuid`, `pageId`, `workflowId`, and `automationId` must be isolated by profile.
|
|
@@ -62,6 +62,54 @@ Use CLI flags such as `--form-code customer` so the CLI can fill `formUuid` from
|
|
|
62
62
|
|
|
63
63
|
## Definition JSON
|
|
64
64
|
|
|
65
|
+
AI-authored automations should prefer code-first definitions when the logic is mainly backend orchestration and does not need a visual editable canvas.
|
|
66
|
+
|
|
67
|
+
Code automation definition:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"kind": "automation_code_ts",
|
|
72
|
+
"version": "code_v1",
|
|
73
|
+
"runtimeMode": "trusted_node",
|
|
74
|
+
"sourceType": "file_snapshot",
|
|
75
|
+
"scriptCode": "daily_ticket_digest",
|
|
76
|
+
"sourceFile": {
|
|
77
|
+
"localPath": "src/automations/daily_ticket_digest/index.ts"
|
|
78
|
+
},
|
|
79
|
+
"timeoutMs": 30000
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Author source in `src/automations/<resourceCode>/index.ts`. During validate/create/publish, the CLI runs `pnpm build-js-code --script <resourceCode> --source automations`, uploads `dist/automations/<resourceCode>/index.cjs`, and replaces `sourceFile.localPath` with immutable snapshot metadata.
|
|
84
|
+
|
|
85
|
+
Pair it with a structured preview file:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"kind": "automation_code_preview",
|
|
90
|
+
"version": "preview_v1",
|
|
91
|
+
"steps": [
|
|
92
|
+
{ "id": "load", "type": "data_read", "label": "查询待处理数据" },
|
|
93
|
+
{ "id": "notify", "type": "notification", "label": "发送提醒" }
|
|
94
|
+
],
|
|
95
|
+
"edges": [{ "source": "load", "target": "notify" }]
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Resource manifest shape:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"code": "daily_ticket_digest",
|
|
104
|
+
"name": "每日工单摘要",
|
|
105
|
+
"triggerConfig": { "type": "scheduled", "enabled": true },
|
|
106
|
+
"definitionFile": "definition.code.json",
|
|
107
|
+
"previewFile": "preview.json",
|
|
108
|
+
"publish": true,
|
|
109
|
+
"enable": true
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
65
113
|
Minimum shape:
|
|
66
114
|
|
|
67
115
|
```json
|
|
@@ -123,6 +171,14 @@ The backend runs the snapshot in the trusted Node runtime, applies the node time
|
|
|
123
171
|
|
|
124
172
|
Runtime context includes `ctx.triggerEvent`, `ctx.formData`, `ctx.workflowData`, `ctx.operator`, `ctx.app`, `ctx.variables`, and `ctx.node`. Data/process bridge methods include `ctx.methods.queryOneData`, `queryManyData`, `updateOneData`, `updateDataByFormInstanceId`, `updateManyData`, `createOneData`, `terminateProcess`, and `getAllParentDepartments`.
|
|
125
173
|
|
|
174
|
+
Code automation also exposes `ctx.logger.debug/info/warn/error(message, data?)`. AI-authored code should log input parsing, query conditions, external calls, writes, branch decisions, and caught errors. Logs are stored as full raw execution data by default. Use CLI diagnosis commands:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
openxiangda automation executions daily_ticket_digest --status failed --profile dev
|
|
178
|
+
openxiangda automation logs <instanceId> --automation daily_ticket_digest --profile dev
|
|
179
|
+
openxiangda automation diagnose daily_ticket_digest --profile dev
|
|
180
|
+
```
|
|
181
|
+
|
|
126
182
|
Notification bridge methods include `ctx.notification.sendByType`, `batchSendByType`, `findConfig`, and `previewTemplate`. For custom business messages, create `src/resources/notifications/` first and use its `notificationType`; do not call legacy `/api/notification-config/*` endpoints directly.
|
|
127
183
|
|
|
128
184
|
Example `src/js-code-nodes/scheduled_reconcile/index.ts`:
|
|
@@ -358,6 +358,10 @@ Requires Bearer token. Lists versions in the same automation group.
|
|
|
358
358
|
|
|
359
359
|
Requires Bearer token. Lists execution records for diagnosis.
|
|
360
360
|
|
|
361
|
+
### GET `/apps/:appType/automations/:automationId/executions/:instanceId`
|
|
362
|
+
|
|
363
|
+
Requires Bearer token. Returns one execution record with raw `nodeExecutionLogs`, `triggerEventData`, `executionContext`, result, and error details for AI diagnosis.
|
|
364
|
+
|
|
361
365
|
### GET `/apps/:appType/roles`
|
|
362
366
|
|
|
363
367
|
Requires Bearer token. Lists app roles visible to the current user.
|
|
@@ -6,6 +6,13 @@ Preferred publish command:
|
|
|
6
6
|
openxiangda workspace publish --profile <name>
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
Do not run `lowcode-workspace publish-all`, `pnpm publish:all`, or legacy
|
|
10
|
+
project scripts such as `scripts/openxiangda-publish.mjs` directly for live
|
|
11
|
+
publishing. Those commands are workspace internals. They may skip OpenXiangda's
|
|
12
|
+
normal-user token/profile injection and fall back to old `APP_KEY` /
|
|
13
|
+
`APP_SECRET` behavior, or publish with a buildId that the platform menu does not
|
|
14
|
+
activate. Use `openxiangda workspace publish ...` as the external entry.
|
|
15
|
+
|
|
9
16
|
For day-to-day AI edits, prefer a targeted publish:
|
|
10
17
|
|
|
11
18
|
```bash
|
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
Workflow definitions are JSON objects saved by `openxiangda workflow create`.
|
|
4
4
|
|
|
5
|
+
AI-authored workflows should prefer compile-time TypeScript SDK when the user does not need canvas editing. The SDK compiles local `workflow.ts` into the current v3 graph JSON, so the backend still uses the existing workflow engine for approval tasks, copy tasks, callback waits, branch advancement, operation logs, and process completion events.
|
|
6
|
+
|
|
7
|
+
Example `src/workflows/expense_approval/workflow.ts`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { defineWorkflow } from "openxiangda/workflow";
|
|
11
|
+
|
|
12
|
+
export default defineWorkflow({
|
|
13
|
+
build(flow) {
|
|
14
|
+
const start = flow.start();
|
|
15
|
+
const approve = flow.approval("manager_approval", {
|
|
16
|
+
label: "主管审批",
|
|
17
|
+
approverType: "ext_target_approval",
|
|
18
|
+
approvals: ["USER_MANAGER"],
|
|
19
|
+
approvalNames: ["主管"],
|
|
20
|
+
multiApprove: "or",
|
|
21
|
+
});
|
|
22
|
+
const copy = flow.copy("copy_finance", {
|
|
23
|
+
label: "抄送财务",
|
|
24
|
+
approverType: "ext_target_approval",
|
|
25
|
+
approvals: ["USER_FINANCE"],
|
|
26
|
+
approvalNames: ["财务"],
|
|
27
|
+
});
|
|
28
|
+
const end = flow.end();
|
|
29
|
+
flow.sequence(start, approve, copy, end);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Resource manifest:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"code": "expense_approval",
|
|
39
|
+
"formCode": "expense",
|
|
40
|
+
"workflowFile": "../../../workflows/expense_approval/workflow.ts",
|
|
41
|
+
"definitionFile": "definition.v3.json",
|
|
42
|
+
"previewFile": "preview.json",
|
|
43
|
+
"publish": true
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
During `openxiangda resource publish`, the CLI compiles `workflowFile`, writes `definitionFile` and `previewFile` when configured, sends v3 `definitionJson` to the backend, and stores the preview in `viewJson` for read-only frontend display.
|
|
48
|
+
|
|
5
49
|
Minimum shape:
|
|
6
50
|
|
|
7
51
|
```json
|
|
@@ -154,6 +154,8 @@ openxiangda workspace publish --profile dev --only pages/dashboard,forms/custome
|
|
|
154
154
|
|
|
155
155
|
Do not run a full publish automatically after a single page or form edit. Full publish is for intentional broad changes, shared/config changes that affect many modules, or cache repair. Targeted publish skips resources by default; pass `--resources` or run `openxiangda resource publish` when resource manifests changed.
|
|
156
156
|
|
|
157
|
+
Do not call `lowcode-workspace publish-all`, `pnpm publish:all`, or legacy project scripts such as `scripts/openxiangda-publish.mjs` directly for live publishing. They are internal workspace commands and may not receive the normal-user access token from the selected profile. The external entry is always `openxiangda workspace publish ...`.
|
|
158
|
+
|
|
157
159
|
- `OPENXIANGDA_PROFILE`
|
|
158
160
|
- `OPENXIANGDA_BASE_URL`
|
|
159
161
|
- `OPENXIANGDA_ACCESS_TOKEN`
|
|
@@ -52,6 +52,10 @@ Use `workflow pull` to inspect the live definition. Use logical workflow codes l
|
|
|
52
52
|
|
|
53
53
|
JS_CODE is the backend execution escape hatch for workflow and automation. Use it when the logic must run on the server after a backend trigger, such as a fixed cron schedule, a form date-field schedule, a form submit/update/delete/field-change event, or a workflow approval/process event. It is appropriate for cross-form data queries, create/update/batch update operations, process termination, platform API calls, external HTTP calls, and complex orchestration that the frontend cannot handle reliably.
|
|
54
54
|
|
|
55
|
+
For new AI-authored automations, prefer code-first `automation_code_ts` resources instead of visual v3 graph definitions. Put the source in `src/automations/<resourceCode>/index.ts`, define `definition.code.json` with `kind: "automation_code_ts"`, and provide `preview.json` for read-only frontend display. Use `ctx.logger.debug/info/warn/error(message, data?)` at every important step; OpenXiangda can inspect logs with `automation executions`, `automation logs`, and `automation diagnose`.
|
|
56
|
+
|
|
57
|
+
For new AI-authored workflows where users do not need canvas editing, prefer compile-time `workflow.ts` using `openxiangda/workflow`. The CLI compiles it to v3 `definitionJson` and `preview.json`; the backend still runs the normal workflow engine for approval tasks, copy tasks, callback waits, branch advancement, and process records.
|
|
58
|
+
|
|
55
59
|
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:
|
|
56
60
|
|
|
57
61
|
1. Put source in `sy-lowcode-app-workspace/src/js-code-nodes/<scriptCode>/index.ts`.
|