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 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) {
@@ -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 the no-hardcoded-color rule. Use this before writing any page or form CSS.
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. **样式走变量与语义类**:使用 CSS 变量、Tailwind 语义类,**禁止**在 JSX 中硬编码颜色、间距、字号、圆角。
13
- 5. **不覆盖组件内部 class**:组件库内部类名(`ant-select-selector` 等)是私有 API,下个版本就可能变。
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 正确:基于 antd 包装 + CSS 变量 + 语义类
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` 等语义类,不写 `style={{ width: '100%' }}`。
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
- > 覆盖时只能使用 `style-system.md` 中定义的 `--sy-color-*`、`--sy-radius-*`、`--sy-spacing-*` 等变量名,自创变量不会生效。
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. **禁止**:直接覆盖组件内部 class(`.ant-select-selector { ... }`)—— fragile,升级即坏。
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 }}` 硬编码 | Tailwind 语义类或 CSS 变量 |
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
- | 覆盖 `.ant-xxx` 内部 class | `className`、CSS 变量或 `ConfigProvider` 主题 |
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
- - [ ] 样式 → 全部走 Tailwind 语义类 / CSS 变量 / ConfigProvider,无硬编码?
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.