openxiangda 1.0.26 → 1.0.28
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 -1
- package/openxiangda-skills/references/automation-v3.md +56 -0
- package/openxiangda-skills/references/component-guide.md +9 -9
- package/openxiangda-skills/references/openxiangda-api.md +4 -0
- package/openxiangda-skills/references/style-system.md +121 -114
- package/openxiangda-skills/references/workflow-v3.md +44 -0
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +2 -2
- 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/examples/best-practices/design-style.md +4 -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) {
|
|
@@ -144,7 +144,7 @@ Workflow / automation / permissions:
|
|
|
144
144
|
Platform domain knowledge (read these first when generating forms or pages so the output matches the live platform behavior):
|
|
145
145
|
|
|
146
146
|
- `references/platform-data-model.md` — how the platform persists field values (JSONB, `{label, value}` for option fields, attachment shape, etc.). Use this whenever you write, render, or diagnose form data.
|
|
147
|
-
- `references/style-system.md` — three-layer style architecture, CSS namespace, and
|
|
147
|
+
- `references/style-system.md` — three-layer style architecture, CSS namespace, and flexible Tailwind/CSS guidance. Use this before writing substantial page or form CSS.
|
|
148
148
|
- `references/architecture-patterns.md` — CRUD data flow, `DataManagementList` pattern, recommended directory layout for `src/pages` and `src/forms`. Use this before scaffolding a new page or list view.
|
|
149
149
|
- `references/best-practices.md` — read this before implementing app-level business patterns; it points to `examples/best-practices/` and explains when to use status fields instead of workflow.
|
|
150
150
|
- `references/component-guide.md` — when to pick platform components vs. raw Ant Design vs. custom components, with the rules for option fields. Use this before introducing a new component.
|
|
@@ -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`:
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
1. **平台组件优先**:所有平台组件已经接入了组织架构、文件存储、权限、多端适配等平台能力,重写一遍 = 丢能力 + 后续无法维护。
|
|
10
10
|
2. **基于 antd / antd-mobile 包装**:自定义组件必须站在 antd 之上,保证 PC 和移动端的基础体验一致。
|
|
11
11
|
3. **成熟能力先调研开源方案**:图表、动画、拖拽、虚拟列表、富文本、日历、导入导出、复杂表格等,不要直接手写。先查当前项目依赖、官方文档和维护状态,再决定使用库或轻量封装。
|
|
12
|
-
4.
|
|
13
|
-
5.
|
|
12
|
+
4. **样式优先走变量与语义类**:常规后台体验优先使用 CSS 变量、Tailwind 语义类和 Ant Design token;当业务视觉需要时,可以使用 Tailwind 普通类、任意值、局部 CSS、页面级变量或少量动态 inline style。
|
|
13
|
+
5. **谨慎覆盖组件内部 class**:组件库内部类名(`ant-select-selector` 等)是私有 API,下个版本就可能变;确实需要覆盖时要限定页面作用域,并优先考虑组件 props、`className`、CSS 变量或 `ConfigProvider`。
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -86,7 +86,7 @@ import {
|
|
|
86
86
|
|
|
87
87
|
## 4. 自定义组件开发规范
|
|
88
88
|
|
|
89
|
-
### 4.1
|
|
89
|
+
### 4.1 推荐:基于 antd 包装 + CSS 变量 + 语义类
|
|
90
90
|
|
|
91
91
|
```tsx
|
|
92
92
|
import { Select, SelectProps } from 'antd';
|
|
@@ -115,7 +115,7 @@ export function CustomSelect({ options, className, ...rest }: CustomSelectProps)
|
|
|
115
115
|
- 透传 `...rest`,保留 antd 全部 API。
|
|
116
116
|
- `popupClassName="sy-app-workspace"` 确保弹层应用平台样式作用域。
|
|
117
117
|
- `getPopupContainer` 解决弹层被裁切问题(详见第 7 节常见错误)。
|
|
118
|
-
- 使用 `w-full`、`rounded-form`
|
|
118
|
+
- 使用 `w-full`、`rounded-form` 等语义类作为默认基线;如果组件视觉需要精调,可以继续使用 Tailwind 普通类、任意值或局部 CSS。
|
|
119
119
|
|
|
120
120
|
### 4.2 错误:从头实现选择器
|
|
121
121
|
|
|
@@ -162,7 +162,7 @@ function BadSelect({ options }) {
|
|
|
162
162
|
}
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
>
|
|
165
|
+
> 优先覆盖 `style-system.md` 中定义的 `--sy-color-*`、`--sy-radius-*`、`--sy-spacing-*` 等平台变量。需要页面专属视觉时,可以新增页面级 CSS 变量,建议加业务前缀,避免与平台变量冲突。
|
|
166
166
|
|
|
167
167
|
3. **antd `ConfigProvider` 主题**
|
|
168
168
|
```tsx
|
|
@@ -171,7 +171,7 @@ function BadSelect({ options }) {
|
|
|
171
171
|
</ConfigProvider>
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
-
4.
|
|
174
|
+
4. **谨慎**:直接覆盖组件内部 class(`.ant-select-selector { ... }`)属于私有 API,升级风险较高。确实需要时必须限定在页面根类或 `.sy-app-workspace` 下,并优先考虑 `ConfigProvider`、`className`、组件 props 或 CSS 变量是否能解决。
|
|
175
175
|
|
|
176
176
|
---
|
|
177
177
|
|
|
@@ -207,10 +207,10 @@ export function ActionButton(props) {
|
|
|
207
207
|
| 用 `<input type="file">` 上传 | 用 `AttachmentField` / `ImageField` |
|
|
208
208
|
| 用第三方富文本(TinyMCE 等) | 用 `EditorField` |
|
|
209
209
|
| 自己写列表 + 分页 + 导出 | 用 `DataManagementList` |
|
|
210
|
-
| `style={{ color: '#333', padding: 16 }}`
|
|
210
|
+
| 常规组件大量写 `style={{ color: '#333', padding: 16 }}` | 优先 Tailwind 语义类、普通工具类、CSS 变量或局部 CSS;动态值和少量精调 inline style 可接受 |
|
|
211
211
|
| Select / Date / Modal 弹层错位、被滚动容器裁切 | 加 `getPopupContainer={(trigger) => trigger.parentElement}`,并在弹层 `popupClassName` 上加 `sy-app-workspace` |
|
|
212
212
|
| 不分端,PC 组件直接放移动端 | 按端拆页 + 端内用对应 UI 库 |
|
|
213
|
-
|
|
|
213
|
+
| 无作用域覆盖 `.ant-xxx` 内部 class | 优先用 `className`、CSS 变量或 `ConfigProvider` 主题;必要覆盖时限定到页面命名空间 |
|
|
214
214
|
| 在自定义组件中不透传 `...rest` / `ref` | 透传 props 与 `forwardRef`,保留底层组件全部能力 |
|
|
215
215
|
|
|
216
216
|
---
|
|
@@ -219,6 +219,6 @@ export function ActionButton(props) {
|
|
|
219
219
|
|
|
220
220
|
- [ ] 涉及人员 / 部门 / 文件 / 图片 / 富文本 / 签名 / 地图 / 列表 → 用了平台组件?
|
|
221
221
|
- [ ] 自定义组件 → 基于 antd / antd-mobile 包装并透传 props?
|
|
222
|
-
- [ ] 样式 →
|
|
222
|
+
- [ ] 样式 → 常规区域优先 Tailwind 语义类 / CSS 变量 / ConfigProvider;特殊视觉用 Tailwind 任意值、局部 CSS 或页面级变量且作用域收敛?
|
|
223
223
|
- [ ] 弹层 → 设置了 `getPopupContainer` 与 `sy-app-workspace` 弹层样式作用域?
|
|
224
224
|
- [ ] 多端 → PC 用 antd、Mobile 用 antd-mobile,业务逻辑共享?
|
|
@@ -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.
|