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 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.tsdefinition-json 中的 sourceFile.localPath 会在 validate/create 时先 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 或 definitionFile`);
2808
+ if (!item.definitionJson && !item.definitionFile && !item.workflowFile) {
2809
+ errors.push(`${label}: 缺少 definitionJson、definitionFileworkflowFile`);
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 definitionJson = await resolveManifestJson(
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 = await resolveManifestJson(
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
- 'viewJson',
3427
- 'viewFile',
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 = await resolveManifestJson(
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
- 'viewJson',
3500
- 'viewFile',
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 matched = normalized.match(/src\/js-code-nodes\/([^/]+)\/index\.tsx?$/);
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', 'js-code-nodes', resolvedScriptCode, 'index.cjs');
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`.