n8n-nodes-kingsoft-airscript 2.0.7 → 2.0.8
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.
|
@@ -7,136 +7,165 @@ const API_CONFIG = {
|
|
|
7
7
|
ENDPOINTS: {
|
|
8
8
|
SYNC_TASK: '/api/v3/ide/file/:file_id/script/:script_id/sync_task',
|
|
9
9
|
ASYNC_TASK: '/api/v3/ide/file/:file_id/script/:script_id/task',
|
|
10
|
-
GET_TASK: '/api/v3/script/task'
|
|
11
|
-
}
|
|
10
|
+
GET_TASK: '/api/v3/script/task',
|
|
11
|
+
},
|
|
12
12
|
};
|
|
13
13
|
const 创建并发闸门 = (上限) => {
|
|
14
|
-
let
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
|
|
14
|
+
let 当前运行数 = 0;
|
|
15
|
+
const 等待队列 = [];
|
|
16
|
+
const 获取资源 = async () => {
|
|
17
|
+
if (当前运行数 < 上限) {
|
|
18
|
+
当前运行数++;
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
-
await new Promise(resolve =>
|
|
22
|
-
|
|
21
|
+
await new Promise((resolve) => 等待队列.push(resolve));
|
|
22
|
+
当前运行数++;
|
|
23
23
|
};
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
24
|
+
const 释放资源 = () => {
|
|
25
|
+
当前运行数--;
|
|
26
|
+
const 下一个任务 = 等待队列.shift();
|
|
27
|
+
if (下一个任务)
|
|
28
|
+
下一个任务();
|
|
29
29
|
};
|
|
30
|
-
return { acquire
|
|
30
|
+
return { acquire: 获取资源, release: 释放资源 };
|
|
31
31
|
};
|
|
32
32
|
class KingsoftAirscript {
|
|
33
33
|
constructor() {
|
|
34
34
|
this.description = {
|
|
35
|
-
displayName: 'Kingsoft AirScript 连接器',
|
|
35
|
+
displayName: 'Kingsoft AirScript 连接器 v3',
|
|
36
36
|
name: 'kingsoftAirscript',
|
|
37
37
|
icon: 'file:kingsoftAirscript.svg',
|
|
38
38
|
group: ['transform'],
|
|
39
39
|
version: 3,
|
|
40
40
|
subtitle: '={{$parameter["operation"]}}',
|
|
41
|
-
description: '连接并执行 Kingsoft AirScript
|
|
41
|
+
description: '连接并执行 Kingsoft AirScript 脚本(V3:默认只执行一次 + 内部分批并发 + 汇总输出,杜绝重复写入)。',
|
|
42
42
|
defaults: { name: 'Kingsoft AirScript' },
|
|
43
43
|
inputs: ['main'],
|
|
44
44
|
outputs: ['main'],
|
|
45
45
|
credentials: [{ name: 'kingsoftAirscriptApi', required: true }],
|
|
46
46
|
hints: [
|
|
47
47
|
{
|
|
48
|
-
message:
|
|
48
|
+
message: 'V3 默认对 runScript 只执行一次。大数据请开启「自动分批写入」,让节点内部按 data 分批并发执行。',
|
|
49
49
|
type: 'info',
|
|
50
50
|
location: 'inputPane',
|
|
51
|
-
whenToDisplay: 'always'
|
|
51
|
+
whenToDisplay: 'always',
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
|
-
message:
|
|
54
|
+
message: '全局最大并发数设置过高可能导致系统资源消耗过大,请根据服务器性能适当调整。',
|
|
55
55
|
type: 'warning',
|
|
56
56
|
location: 'inputPane',
|
|
57
|
-
whenToDisplay: 'always'
|
|
57
|
+
whenToDisplay: 'always',
|
|
58
58
|
},
|
|
59
|
-
{
|
|
60
|
-
message: "处理大量数据时,建议启用自动分批写入功能,避免内存溢出。",
|
|
61
|
-
type: 'info',
|
|
62
|
-
location: 'inputPane',
|
|
63
|
-
whenToDisplay: 'always'
|
|
64
|
-
}
|
|
65
59
|
],
|
|
66
60
|
properties: [
|
|
67
61
|
{
|
|
68
|
-
displayName: '📖 金山 AirScript 连接器 v3',
|
|
62
|
+
displayName: '📖 金山 AirScript 连接器 v3(推荐):默认只执行一次(避免重复写入),支持 argv 内数组字段分批并发执行,并可汇总输出。',
|
|
69
63
|
name: 'notice',
|
|
70
64
|
type: 'notice',
|
|
71
65
|
default: '',
|
|
72
|
-
description: '
|
|
66
|
+
description: 'V3:默认只执行一次(避免重复写入),支持 argv 内数组字段分批并发执行,并可汇总输出。',
|
|
73
67
|
},
|
|
74
68
|
{
|
|
75
|
-
displayName: '
|
|
76
|
-
name: '
|
|
69
|
+
displayName: '操作',
|
|
70
|
+
name: 'operation',
|
|
77
71
|
type: 'options',
|
|
78
|
-
|
|
72
|
+
noDataExpression: true,
|
|
79
73
|
options: [
|
|
80
74
|
{
|
|
81
|
-
name: '
|
|
82
|
-
value: '
|
|
83
|
-
|
|
75
|
+
name: '同步执行脚本 (Sync)',
|
|
76
|
+
value: 'runScriptSync',
|
|
77
|
+
action: '同步执行脚本并等待结果',
|
|
78
|
+
description: '适用于需要立即获取执行结果的场景,执行时间较短时推荐'
|
|
84
79
|
},
|
|
85
80
|
{
|
|
86
|
-
name: '
|
|
87
|
-
value: '
|
|
88
|
-
|
|
81
|
+
name: '异步执行脚本 (Async)',
|
|
82
|
+
value: 'runScriptAsync',
|
|
83
|
+
action: '异步执行脚本并返回 Task id',
|
|
84
|
+
description: '适用于长时间运行的脚本,避免 n8n 工作流超时'
|
|
89
85
|
},
|
|
90
86
|
{
|
|
91
|
-
name: '
|
|
92
|
-
value: '
|
|
93
|
-
|
|
87
|
+
name: '获取任务状态 (Get Status)',
|
|
88
|
+
value: 'getTaskStatus',
|
|
89
|
+
action: '根据 Task id 查询任务状态',
|
|
90
|
+
description: '用于查询异步执行任务的状态和结果'
|
|
94
91
|
},
|
|
95
92
|
],
|
|
96
|
-
|
|
97
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'] } },
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
displayName: '目标数组字段路径',
|
|
101
|
-
name: 'targetArrayField',
|
|
102
|
-
type: 'string',
|
|
103
|
-
default: 'data',
|
|
104
|
-
hint: '例如:data 或 写表行列表',
|
|
105
|
-
description: '当选择「追加输入项到 argv 数组字段」时,指定 argv 中的目标数组字段。例如设置为 "data",则所有输入项会被聚合到 argv.data 数组中。',
|
|
106
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], payloadMode: ['appendItemsToArrayField'] } },
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
displayName: '项目数据字段路径',
|
|
110
|
-
name: 'itemDataPath',
|
|
111
|
-
type: 'string',
|
|
112
|
-
default: '',
|
|
113
|
-
hint: '留空表示使用整个 $json',
|
|
114
|
-
description: '当选择「从输入项直接作为 argv」或「追加输入项到 argv 数组字段」时,指定从输入项中提取数据的字段路径。例如设置为 "payload",则会使用 $JSON.payload 作为数据来源。',
|
|
115
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], payloadMode: ['argvFromItem', 'appendItemsToArrayField'] } },
|
|
93
|
+
default: 'runScriptSync',
|
|
116
94
|
},
|
|
117
95
|
{
|
|
118
|
-
displayName: '
|
|
119
|
-
name: '
|
|
96
|
+
displayName: '执行模式(V3)',
|
|
97
|
+
name: 'runModeV3',
|
|
120
98
|
type: 'options',
|
|
121
99
|
noDataExpression: true,
|
|
100
|
+
default: 'uiOnce',
|
|
101
|
+
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'] } },
|
|
122
102
|
options: [
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
103
|
+
{
|
|
104
|
+
name: '执行一次:仅使用 UI Argv(推荐,最安全)',
|
|
105
|
+
value: 'uiOnce',
|
|
106
|
+
description: '无论输入多少 items,本节点仅执行一次。适合 UI Argv 本身已包含 data 数组的批量写入。\n✅ 适用场景:金山文档中已配置好数据结构的场景,避免重复执行',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: '执行一次:聚合输入 Items 到数组字段(默认 Data,可改)',
|
|
110
|
+
value: 'aggregateOnce',
|
|
111
|
+
description: '把所有输入 items 聚合成数组,写入到 Argv 的指定字段(默认 data),然后仅执行一次。\n✅ 适用场景:将 n8n 工作流中的多个数据项合并后批量处理',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
displayName: '聚合配置(Aggregate)',
|
|
117
|
+
name: 'aggregate',
|
|
118
|
+
type: 'collection',
|
|
119
|
+
placeholder: '添加聚合配置',
|
|
120
|
+
default: {},
|
|
121
|
+
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], runModeV3: ['aggregateOnce'] } },
|
|
122
|
+
options: [
|
|
123
|
+
{
|
|
124
|
+
displayName: '目标数组字段路径(相对 Argv)',
|
|
125
|
+
name: 'targetArrayField',
|
|
126
|
+
type: 'string',
|
|
127
|
+
default: 'data',
|
|
128
|
+
description: '例:data 或 写表行列表 或 data.rows(支持点路径)\n⚠️ 这个字段名需要与 AirScript 脚本中期望接收数据的字段名一致',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
displayName: '从输入项提取数据路径(相对 $Json)',
|
|
132
|
+
name: 'itemDataPath',
|
|
133
|
+
type: 'string',
|
|
134
|
+
default: '',
|
|
135
|
+
hint: '留空表示使用整个 $json',
|
|
136
|
+
description: '聚合时,每个输入 item 从哪个路径取值作为数组元素。\n💡 示例:data.rows 表示从每个输入项的 data.rows 字段提取数据',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
displayName: '注入策略',
|
|
140
|
+
name: 'injectPolicy',
|
|
141
|
+
type: 'options',
|
|
142
|
+
default: 'overwrite',
|
|
143
|
+
options: [
|
|
144
|
+
{ name: '强制覆盖目标字段(推荐,最确定)', value: 'overwrite', description: '无论目标字段是否存在,都强制覆盖' },
|
|
145
|
+
{ name: '若目标字段不存在才注入', value: 'ifMissing', description: '仅在目标字段不存在时注入,避免意外覆盖' },
|
|
146
|
+
{ name: '追加到目标字段(数组 Concat)', value: 'append', description: '将数据追加到现有数组末尾' },
|
|
147
|
+
],
|
|
148
|
+
description: '决定聚合数组如何写入 Argv。\n🔒 推荐使用"强制覆盖",行为最明确',
|
|
149
|
+
},
|
|
126
150
|
],
|
|
127
|
-
default: 'runScriptSync',
|
|
128
151
|
},
|
|
129
152
|
{
|
|
130
153
|
displayName: 'ID 输入模式',
|
|
131
154
|
name: 'idInputMode',
|
|
132
155
|
type: 'options',
|
|
133
156
|
noDataExpression: true,
|
|
134
|
-
displayOptions: {
|
|
135
|
-
show: { operation: ['runScriptSync', 'runScriptAsync'] },
|
|
136
|
-
},
|
|
157
|
+
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'] } },
|
|
137
158
|
options: [
|
|
138
|
-
{
|
|
139
|
-
|
|
159
|
+
{
|
|
160
|
+
name: '通过 Webhook 链接输入',
|
|
161
|
+
value: 'url',
|
|
162
|
+
description: '粘贴完整链接(推荐)\n✅ 从金山文档脚本页面直接复制链接,自动解析 ID,避免手动输入错误'
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: '手动输入 ID',
|
|
166
|
+
value: 'manual',
|
|
167
|
+
description: '分别填写 File ID 与 Script ID\n⚠️ 需要确保 ID 准确无误,建议优先使用链接方式'
|
|
168
|
+
},
|
|
140
169
|
],
|
|
141
170
|
default: 'url',
|
|
142
171
|
},
|
|
@@ -146,15 +175,10 @@ class KingsoftAirscript {
|
|
|
146
175
|
type: 'string',
|
|
147
176
|
default: '',
|
|
148
177
|
required: true,
|
|
149
|
-
hint: '
|
|
150
|
-
displayOptions: {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
idInputMode: ['url'],
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
placeholder: '在此粘贴从金山文档复制的完整链接',
|
|
157
|
-
description: '推荐!最简单的方式。',
|
|
178
|
+
hint: '示例:https://www.kdocs.cn/api/v3/ide/file/xxx/script/yyy/sync_task',
|
|
179
|
+
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], idInputMode: ['url'] } },
|
|
180
|
+
placeholder: '粘贴从金山文档复制的完整链接',
|
|
181
|
+
description: '将从链接中自动解析 File ID 与 Script ID。\n💡 获取方式:在金山文档中打开脚本,点击"复制链接"按钮',
|
|
158
182
|
},
|
|
159
183
|
{
|
|
160
184
|
displayName: '文件 ID (File ID)',
|
|
@@ -162,14 +186,8 @@ class KingsoftAirscript {
|
|
|
162
186
|
type: 'string',
|
|
163
187
|
default: '',
|
|
164
188
|
required: true,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
show: {
|
|
168
|
-
operation: ['runScriptSync', 'runScriptAsync'],
|
|
169
|
-
idInputMode: ['manual'],
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
description: '脚本所在文档的 ID。',
|
|
189
|
+
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], idInputMode: ['manual'] } },
|
|
190
|
+
description: '脚本所在文件的 ID。\n🔍 可在金山文档的 URL 中找到 file_id 参数',
|
|
173
191
|
},
|
|
174
192
|
{
|
|
175
193
|
displayName: '脚本 ID (Script ID)',
|
|
@@ -177,161 +195,79 @@ class KingsoftAirscript {
|
|
|
177
195
|
type: 'string',
|
|
178
196
|
default: '',
|
|
179
197
|
required: true,
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
show: {
|
|
183
|
-
operation: ['runScriptSync', 'runScriptAsync'],
|
|
184
|
-
idInputMode: ['manual'],
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
description: '要执行的脚本的 ID。',
|
|
198
|
+
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], idInputMode: ['manual'] } },
|
|
199
|
+
description: '脚本 ID。\n🔍 可在金山文档的 URL 中找到 script_id 参数',
|
|
188
200
|
},
|
|
189
201
|
{
|
|
190
|
-
displayName: '
|
|
202
|
+
displayName: '可选上下文参数(Context)',
|
|
191
203
|
name: 'contextParameters',
|
|
192
204
|
type: 'collection',
|
|
193
205
|
placeholder: '添加可选参数',
|
|
194
206
|
default: {},
|
|
195
|
-
description: '设置可选的 Context 参数,会附加到请求中',
|
|
196
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'] } },
|
|
197
|
-
options: [
|
|
198
|
-
{ displayName: '表名 (Sheet Name)', name: 'sheetName', type: 'string', default: '', description: '对应 Context.sheet_name' },
|
|
199
|
-
{ displayName: '范围 (Range)', name: 'range', type: 'string', default: '', description: '对应 Context.range, 例如 "$B$156"' },
|
|
200
|
-
{ displayName: '链接来源 (Link From)', name: 'linkFrom', type: 'string', default: '', description: '对应 Context.link_from' },
|
|
201
|
-
{ displayName: '视图名称 (DB Active View)', name: 'dbActiveView', type: 'string', default: '', description: '对应 Context.db_active_view' },
|
|
202
|
-
{ displayName: '选区 (DB Selection)', name: 'dbSelection', type: 'string', default: '', description: '对应 Context.db_selection' },
|
|
203
|
-
],
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
displayName: '参数模板',
|
|
207
|
-
name: 'argvTemplate',
|
|
208
|
-
type: 'options',
|
|
209
|
-
noDataExpression: true,
|
|
210
207
|
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'] } },
|
|
211
208
|
options: [
|
|
212
|
-
{
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
{
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
209
|
+
{
|
|
210
|
+
displayName: '表名 (Sheet Name)',
|
|
211
|
+
name: 'sheetName',
|
|
212
|
+
type: 'string',
|
|
213
|
+
default: '',
|
|
214
|
+
description: '对应 Context.sheet_name\n💡 指定脚本操作的目标表格名称'
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
displayName: '范围 (Range)',
|
|
218
|
+
name: 'range',
|
|
219
|
+
type: 'string',
|
|
220
|
+
default: '',
|
|
221
|
+
description: '对应 Context.range\n💡 指定操作范围,如 A1:B10'
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
displayName: '链接来源 (Link From)',
|
|
225
|
+
name: 'linkFrom',
|
|
226
|
+
type: 'string',
|
|
227
|
+
default: '',
|
|
228
|
+
description: '对应 Context.link_from\n💡 用于跨表链接操作时指定来源'
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
displayName: '视图名称 (DB Active View)',
|
|
232
|
+
name: 'dbActiveView',
|
|
233
|
+
type: 'string',
|
|
234
|
+
default: '',
|
|
235
|
+
description: '对应 Context.db_active_view\n💡 指定数据库视图名称'
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
displayName: '选区 (DB Selection)',
|
|
239
|
+
name: 'dbSelection',
|
|
240
|
+
type: 'string',
|
|
241
|
+
default: '',
|
|
242
|
+
description: '对应 Context.db_selection\n💡 指定数据库选区范围'
|
|
243
|
+
},
|
|
223
244
|
],
|
|
224
|
-
default: 'custom',
|
|
225
|
-
description: '选择预设模板后,会显示对应的 Argv JSON 输入框(可编辑)',
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
displayName: '脚本参数 (Argv)',
|
|
229
|
-
name: 'argv_custom',
|
|
230
|
-
type: 'json',
|
|
231
|
-
default: '{\n "message": "Hello from n8n!"\n}',
|
|
232
|
-
required: true,
|
|
233
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], argvTemplate: ['custom'] } },
|
|
234
|
-
description: '自定义 JSON 参数。 数据输入模式说明: - argvFromUI:完全使用此模板作为 argv - argvFromItem:此模板被忽略,使用输入项的 $JSON - appendItemsToArrayField:此模板作为基础,输入项会被聚合成数组添加到指定字段',
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
displayName: '脚本参数 (Argv)',
|
|
238
|
-
name: 'argv_basicInput',
|
|
239
|
-
type: 'json',
|
|
240
|
-
default: `{\n "sheet_name": "客户表",\n "覆盖模式": false,\n "data": [\n {\n "客户名称": "张三",\n "联系方式": "13800138000",\n "邮箱": "zhangsan@example.com"\n }\n ]\n}`,
|
|
241
|
-
required: true,
|
|
242
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], argvTemplate: ['basicInput'] } },
|
|
243
|
-
description: '基本数据录入模板参数(可编辑)',
|
|
244
245
|
},
|
|
245
246
|
{
|
|
246
|
-
displayName: '
|
|
247
|
-
name: '
|
|
248
|
-
type: '
|
|
249
|
-
default:
|
|
250
|
-
required: true,
|
|
251
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], argvTemplate: ['attachment'] } },
|
|
252
|
-
description: '处理附件字段模板参数(可编辑)',
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
displayName: '脚本参数 (Argv)',
|
|
256
|
-
name: 'argv_cascade',
|
|
257
|
-
type: 'json',
|
|
258
|
-
default: `{\n "sheet_name": "订单表",\n "覆盖模式": true,\n "唯一键": "订单ID",\n "data": [\n {\n "订单ID": "O001",\n "客户": "张三",\n "地区": "广东省",\n "城市": "深圳市",\n "产品": "XXX沙发"\n }\n ]\n}`,
|
|
259
|
-
required: true,
|
|
260
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], argvTemplate: ['cascade'] } },
|
|
261
|
-
description: '处理级联字段模板参数(可编辑)',
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
displayName: '脚本参数 (Argv)',
|
|
265
|
-
name: 'argv_migrate',
|
|
266
|
-
type: 'json',
|
|
267
|
-
default: `{\n "源表名称": "旧客户表",\n "目标表名称": "新客户表",\n "映射关系": {\n "旧ID": "客户ID",\n "姓名": "客户名称",\n "电话": "联系方式"\n },\n "过滤条件": "状态 = '活跃'"\n}`,
|
|
268
|
-
required: true,
|
|
269
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], argvTemplate: ['migrate'] } },
|
|
270
|
-
description: '批量迁移数据模板参数(可编辑)',
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
displayName: '脚本参数 (Argv)',
|
|
274
|
-
name: 'argv_formula',
|
|
275
|
-
type: 'json',
|
|
276
|
-
default: `{\n "sheet_name": "销售表",\n "字段配置": [\n {\n "字段名称": "销售额",\n "字段类型": "公式",\n "表达式": "单价 * 数量"\n }\n ]\n}`,
|
|
277
|
-
required: true,
|
|
278
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], argvTemplate: ['formula'] } },
|
|
279
|
-
description: '设置公式字段模板参数(可编辑)',
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
displayName: '脚本参数 (Argv)',
|
|
283
|
-
name: 'argv_setLinkProperties',
|
|
284
|
-
type: 'json',
|
|
285
|
-
default: `{\n "sheet_name": "订单表",\n "关联字段": "客户ID",\n "目标表": "客户表",\n "显示字段": ["客户名称", "联系方式"]\n}`,
|
|
286
|
-
required: true,
|
|
287
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], argvTemplate: ['setLinkProperties'] } },
|
|
288
|
-
description: '设置关联属性模板参数(可编辑)',
|
|
289
|
-
},
|
|
290
|
-
{
|
|
291
|
-
displayName: '脚本参数 (Argv)',
|
|
292
|
-
name: 'argv_setCascadeOptions',
|
|
293
|
-
type: 'json',
|
|
294
|
-
default: `{\n "sheet_name": "地区表",\n "级联字段": "地区",\n "选项结构": {\n "广东省": ["深圳市", "广州市", "东莞市"],\n "浙江省": ["杭州市", "宁波市", "温州市"]\n }\n}`,
|
|
295
|
-
required: true,
|
|
296
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], argvTemplate: ['setCascadeOptions'] } },
|
|
297
|
-
description: '设置级联选项模板参数(可编辑)',
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
displayName: '脚本参数 (Argv)',
|
|
301
|
-
name: 'argv_n8nExpression',
|
|
302
|
-
type: 'json',
|
|
303
|
-
default: `{\n "sheet_name": "动态表",\n "覆盖模式": true,\n "唯一键": "ID",\n "data": [\n {\n "ID": "={{ $json.id }}",\n "名称": "={{ $json.name }}",\n "值": "={{ $json.value }}"\n }\n ]\n}`,
|
|
304
|
-
required: true,
|
|
305
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'], argvTemplate: ['n8nExpression'] } },
|
|
306
|
-
description: '使用 N8N 表达式模板参数(可编辑)',
|
|
307
|
-
},
|
|
308
|
-
{
|
|
309
|
-
displayName: '脚本参数 (Argv)',
|
|
310
|
-
name: 'argv_compositeKey',
|
|
311
|
-
type: 'json',
|
|
312
|
-
default: `{\n "sheet_name": "库存表",\n "覆盖模式": true,\n "唯一键": ["产品ID", "仓库ID"],\n "data": [\n {\n "产品ID": "P001",\n "仓库ID": "W001",\n "库存数量": 100,\n "最后更新": "2024-01-01"\n }\n ]\n}`,
|
|
247
|
+
displayName: '任务 ID (Task ID)',
|
|
248
|
+
name: 'taskId',
|
|
249
|
+
type: 'string',
|
|
250
|
+
default: '',
|
|
313
251
|
required: true,
|
|
314
|
-
displayOptions: { show: { operation: ['
|
|
315
|
-
description: '
|
|
252
|
+
displayOptions: { show: { operation: ['getTaskStatus'] } },
|
|
253
|
+
description: '异步执行返回的 task_id。',
|
|
316
254
|
},
|
|
317
255
|
{
|
|
318
|
-
displayName: '
|
|
319
|
-
name: '
|
|
256
|
+
displayName: '脚本参数(Argv JSON)',
|
|
257
|
+
name: 'argv',
|
|
320
258
|
type: 'json',
|
|
321
|
-
default:
|
|
259
|
+
default: '{\n "sheet_name": "示例表",\n "data": []\n}',
|
|
322
260
|
required: true,
|
|
323
|
-
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync']
|
|
324
|
-
description: '
|
|
261
|
+
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'] } },
|
|
262
|
+
description: '最终会作为 Context.argv 发送给 AirScript。V3 不提供多模板,避免困惑。',
|
|
325
263
|
},
|
|
326
264
|
{
|
|
327
|
-
displayName: '
|
|
328
|
-
name: '
|
|
329
|
-
type: '
|
|
265
|
+
displayName: 'Argv 示例(可复制)常用示例(复制到「脚本参数 Argv JSON」即可): 1)执行一次(UI Argv 自带 data 数组) { "sheet_name": "示例表", "data": [] } 2)执行一次(用表达式聚合输入 items) { "sheet_name": "示例表", "data": "={{ $input.all().map(i => i.JSON) }}" } 3)聚合模式(推荐) - 执行模式:aggregateOnce - 目标数组字段路径:data - itemDataPath 留空(使用整个 $JSON)',
|
|
266
|
+
name: 'argvExamples',
|
|
267
|
+
type: 'notice',
|
|
330
268
|
default: '',
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
displayOptions: { show: { operation: ['getTaskStatus'] } },
|
|
334
|
-
description: '从异步执行脚本操作中获取到的任务 ID',
|
|
269
|
+
displayOptions: { show: { operation: ['runScriptSync', 'runScriptAsync'] } },
|
|
270
|
+
description: '常用示例(复制到「脚本参数 Argv JSON」即可): 1)执行一次(UI Argv 自带 data 数组) { "sheet_name": "示例表", "data": [] } 2)执行一次(用表达式聚合输入 items) { "sheet_name": "示例表", "data": "={{ $input.all().map(i => i.JSON) }}" } 3)聚合模式(推荐) - 执行模式:aggregateOnce - 目标数组字段路径:data - itemDataPath 留空(使用整个 $JSON)'
|
|
335
271
|
},
|
|
336
272
|
{
|
|
337
273
|
displayName: '高级选项',
|
|
@@ -339,14 +275,14 @@ class KingsoftAirscript {
|
|
|
339
275
|
type: 'collection',
|
|
340
276
|
placeholder: '添加高级选项',
|
|
341
277
|
default: {},
|
|
342
|
-
description: '
|
|
278
|
+
description: '超时/重试/分批/调试/并发等配置。\n💡 根据具体需求选择相应的配置项',
|
|
343
279
|
options: [
|
|
344
280
|
{
|
|
345
|
-
displayName: '
|
|
281
|
+
displayName: '并行执行(主要用于 getTaskStatus)',
|
|
346
282
|
name: 'a_parallelExecution',
|
|
347
283
|
type: 'boolean',
|
|
348
284
|
default: false,
|
|
349
|
-
description: 'Whether
|
|
285
|
+
description: 'Whether 开启后将并行处理多个输入项(对 runScript 默认只跑一次影响不大)',
|
|
350
286
|
},
|
|
351
287
|
{
|
|
352
288
|
displayName: '查询任务超时(秒)',
|
|
@@ -366,49 +302,48 @@ class KingsoftAirscript {
|
|
|
366
302
|
description: '异步等待完成模式下,最多等待的总时长(秒)。',
|
|
367
303
|
},
|
|
368
304
|
{
|
|
369
|
-
displayName: '
|
|
305
|
+
displayName: '等待执行完成(异步轮询)',
|
|
370
306
|
name: 'c_waitForCompletion',
|
|
371
307
|
type: 'boolean',
|
|
372
308
|
default: false,
|
|
373
|
-
description: 'Whether enabled, 异步执行时节点将轮询直到任务完成',
|
|
374
309
|
displayOptions: { show: { '/operation': ['runScriptAsync'] } },
|
|
310
|
+
description: 'Whether开启后:异步执行会轮询直到 finished/failed 或超时。',
|
|
375
311
|
},
|
|
376
312
|
{
|
|
377
313
|
displayName: '调试模式(输出 _调试 字段)',
|
|
378
314
|
name: 'w_debug',
|
|
379
315
|
type: 'boolean',
|
|
380
316
|
default: false,
|
|
381
|
-
description: 'Whether
|
|
317
|
+
description: 'Whether 开启后输出包含 `_调试` 字段,包含执行摘要、重试与分批信息',
|
|
382
318
|
},
|
|
383
319
|
{
|
|
384
320
|
displayName: '分批输出模式',
|
|
385
321
|
name: 'v_chunkOutputMode',
|
|
386
322
|
type: 'options',
|
|
387
|
-
default: '
|
|
323
|
+
default: 'merge',
|
|
388
324
|
options: [
|
|
389
|
-
{ name: '
|
|
390
|
-
{ name: '
|
|
325
|
+
{ name: '逐批输出(Split)', value: 'split' },
|
|
326
|
+
{ name: '汇总为一个 Item(merge,推荐)', value: 'merge' },
|
|
391
327
|
],
|
|
392
328
|
displayOptions: { show: { '/options/r_autoChunkEnabled': [true] } },
|
|
393
|
-
description: '
|
|
329
|
+
description: '分批执行时的输出策略。',
|
|
394
330
|
},
|
|
395
331
|
{
|
|
396
332
|
displayName: '分批字段路径(相对 Argv)',
|
|
397
333
|
name: 's_chunkFieldPath',
|
|
398
334
|
type: 'string',
|
|
399
|
-
default: '
|
|
400
|
-
|
|
401
|
-
description: '例:写表行列表 或 data.写表行列表(从 argv 根开始)',
|
|
335
|
+
default: 'data',
|
|
336
|
+
description: '默认 data。也可填 写表行列表 / rows / data.rows 等(从 argv 根开始)。',
|
|
402
337
|
displayOptions: { show: { '/options/r_autoChunkEnabled': [true] } },
|
|
403
338
|
},
|
|
404
339
|
{
|
|
405
|
-
displayName: '
|
|
340
|
+
displayName: '分批最大并发数(同一执行内)',
|
|
406
341
|
name: 'u_chunkMaxConcurrency',
|
|
407
342
|
type: 'number',
|
|
408
343
|
default: 3,
|
|
409
344
|
typeOptions: { minValue: 1, maxValue: 20 },
|
|
410
345
|
displayOptions: { show: { '/options/r_autoChunkEnabled': [true] } },
|
|
411
|
-
description: '
|
|
346
|
+
description: '分批时,同时执行的批次数上限。',
|
|
412
347
|
},
|
|
413
348
|
{
|
|
414
349
|
displayName: '轮询抖动(毫秒)',
|
|
@@ -416,7 +351,7 @@ class KingsoftAirscript {
|
|
|
416
351
|
type: 'number',
|
|
417
352
|
default: 300,
|
|
418
353
|
typeOptions: { minValue: 0 },
|
|
419
|
-
description: '
|
|
354
|
+
description: '轮询间隔叠加随机抖动,避免集中刷接口。',
|
|
420
355
|
},
|
|
421
356
|
{
|
|
422
357
|
displayName: '轮询间隔 (秒)',
|
|
@@ -428,23 +363,22 @@ class KingsoftAirscript {
|
|
|
428
363
|
description: '异步等待完成模式下的轮询间隔(秒)。',
|
|
429
364
|
},
|
|
430
365
|
{
|
|
431
|
-
displayName: '
|
|
366
|
+
displayName: '每批条数(Chunk Size)',
|
|
432
367
|
name: 't_chunkSize',
|
|
433
368
|
type: 'number',
|
|
434
369
|
default: 200,
|
|
435
|
-
hint: '处理大量数据时建议减小此值',
|
|
436
370
|
typeOptions: { minValue: 1 },
|
|
437
371
|
displayOptions: { show: { '/options/r_autoChunkEnabled': [true] } },
|
|
438
|
-
description: '
|
|
372
|
+
description: '分批时每批数组元素数量(可自定义)。',
|
|
439
373
|
},
|
|
440
374
|
{
|
|
441
|
-
displayName: '
|
|
375
|
+
displayName: '批处理数量(并行 Item 批大小)',
|
|
442
376
|
name: 'e_batchSize',
|
|
443
377
|
type: 'number',
|
|
444
378
|
default: 10,
|
|
445
379
|
typeOptions: { minValue: 1 },
|
|
446
380
|
displayOptions: { show: { '/options/a_parallelExecution': [true] } },
|
|
447
|
-
description: '
|
|
381
|
+
description: '并行执行开启时,每批并行处理的输入 item 数量。',
|
|
448
382
|
},
|
|
449
383
|
{
|
|
450
384
|
displayName: '请求超时(秒)',
|
|
@@ -455,27 +389,26 @@ class KingsoftAirscript {
|
|
|
455
389
|
description: '调用 sync_task/task 的 HTTP 超时(秒)。',
|
|
456
390
|
},
|
|
457
391
|
{
|
|
458
|
-
displayName: '
|
|
392
|
+
displayName: '全局最大并发数(HTTP 请求总闸门)',
|
|
459
393
|
name: 'x_globalMaxConcurrency',
|
|
460
394
|
type: 'number',
|
|
461
395
|
default: 5,
|
|
462
|
-
hint: '建议根据服务器性能设置,默认 5',
|
|
463
396
|
typeOptions: { minValue: 1, maxValue: 50 },
|
|
464
|
-
description: '
|
|
397
|
+
description: '防止并发乘法爆炸(例如:分批并发 × 并行执行)。',
|
|
465
398
|
},
|
|
466
399
|
{
|
|
467
400
|
displayName: '失败判定:Finished 但 Error 不为空算失败',
|
|
468
401
|
name: 'n_failOnScriptError',
|
|
469
402
|
type: 'boolean',
|
|
470
403
|
default: true,
|
|
471
|
-
description: 'Whether
|
|
404
|
+
description: 'Whether 开启后:status=finished 但 error/error_details.msg 不为空也视为失败',
|
|
472
405
|
},
|
|
473
406
|
{
|
|
474
407
|
displayName: '输出格式',
|
|
475
408
|
name: 'f_outputFormat',
|
|
476
409
|
type: 'options',
|
|
477
410
|
default: 'fullResponse',
|
|
478
|
-
displayOptions: { show: { '/operation': ['runScriptSync', 'runScriptAsync'] } },
|
|
411
|
+
displayOptions: { show: { '/operation': ['runScriptSync', 'runScriptAsync', 'getTaskStatus'] } },
|
|
479
412
|
options: [
|
|
480
413
|
{ name: '完整响应', value: 'fullResponse' },
|
|
481
414
|
{ name: '仅结果', value: 'resultOnly' },
|
|
@@ -484,12 +417,43 @@ class KingsoftAirscript {
|
|
|
484
417
|
],
|
|
485
418
|
},
|
|
486
419
|
{
|
|
487
|
-
displayName: '
|
|
420
|
+
displayName: '危险:每个输入项执行一次(慎用)',
|
|
421
|
+
name: 'y_dangerEachItemEnabled',
|
|
422
|
+
type: 'boolean',
|
|
423
|
+
default: false,
|
|
424
|
+
description: 'Whether to execute AirScript once per input item (n items = n calls, easily causing duplicate writes)\n⚠️ 强烈警告:启用此选项可能导致数据重复写入,请确保业务逻辑需要多次执行!',
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
displayName: '危险模式:合并 UI Argv(不推荐)',
|
|
428
|
+
name: 'ab_eachItemMergeUiArgv',
|
|
429
|
+
type: 'boolean',
|
|
430
|
+
default: false,
|
|
431
|
+
displayOptions: { show: { '/options/y_dangerEachItemEnabled': [true] } },
|
|
432
|
+
description: 'Whether默认关闭:避免 UI 中 data 数组被每项重复执行导致成倍写入。\n⚠️ 启用后,每个输入项都会包含 UI Argv 中的数据,可能导致数据重复',
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
displayName: '危险模式:每项 Argv 来源路径(相对 $Json)',
|
|
436
|
+
name: 'aa_eachItemDataPath',
|
|
437
|
+
type: 'string',
|
|
438
|
+
default: '',
|
|
439
|
+
displayOptions: { show: { '/options/y_dangerEachItemEnabled': [true] } },
|
|
440
|
+
description: '留空表示使用整个 $JSON 作为 argv。\n💡 示例:data.rows 表示从每个输入项的 data.rows 字段提取数据',
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
displayName: '危险模式:确认多次执行',
|
|
444
|
+
name: 'z_confirmDangerEachItem',
|
|
445
|
+
type: 'boolean',
|
|
446
|
+
default: false,
|
|
447
|
+
displayOptions: { show: { '/options/y_dangerEachItemEnabled': [true] } },
|
|
448
|
+
description: 'Whether必须勾选才允许执行(用于防止误触)。\n🔒 安全机制:防止误触危险模式,确保用户明确知道风险',
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
displayName: '智能解析 Result(若 Result 是 JSON 字符串)',
|
|
488
452
|
name: 'g_parseResultString',
|
|
489
453
|
type: 'boolean',
|
|
490
454
|
default: false,
|
|
491
|
-
|
|
492
|
-
|
|
455
|
+
displayOptions: { show: { '/operation': ['runScriptSync', 'runScriptAsync', 'getTaskStatus'] } },
|
|
456
|
+
description: 'Whether 开启后:若 data.result 是 JSON 字符串,将自动 JSON.parse',
|
|
493
457
|
},
|
|
494
458
|
{
|
|
495
459
|
displayName: '重试初始等待(毫秒)',
|
|
@@ -512,8 +476,7 @@ class KingsoftAirscript {
|
|
|
512
476
|
name: 'r_autoChunkEnabled',
|
|
513
477
|
type: 'boolean',
|
|
514
478
|
default: false,
|
|
515
|
-
|
|
516
|
-
description: 'Whether enabled, 会按配置的字段路径分割 argv 中的数组,并分块运行脚本',
|
|
479
|
+
description: 'Whether 开启后:按字段路径拆分 argv 内数组,分批并发执行脚本',
|
|
517
480
|
},
|
|
518
481
|
{
|
|
519
482
|
displayName: '最大重试次数',
|
|
@@ -528,7 +491,7 @@ class KingsoftAirscript {
|
|
|
528
491
|
name: 'm_honorRetryAfter',
|
|
529
492
|
type: 'boolean',
|
|
530
493
|
default: true,
|
|
531
|
-
description: 'Whether
|
|
494
|
+
description: 'Whether收到 429 且响应头带 Retry-After 时,按指示等待后重试。',
|
|
532
495
|
},
|
|
533
496
|
],
|
|
534
497
|
},
|
|
@@ -538,608 +501,679 @@ class KingsoftAirscript {
|
|
|
538
501
|
}
|
|
539
502
|
async execute() {
|
|
540
503
|
const 输入数据项列表 = this.getInputData();
|
|
504
|
+
const 节点 = this.getNode();
|
|
541
505
|
const 凭证 = (await this.getCredentials('kingsoftAirscriptApi'));
|
|
542
|
-
const
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
506
|
+
const 选项模块 = (() => {
|
|
507
|
+
const 读取高级选项 = (索引) => {
|
|
508
|
+
const raw = this.getNodeParameter('options', 索引, {});
|
|
509
|
+
const 取数 = (k, d) => {
|
|
510
|
+
const v = raw[k];
|
|
511
|
+
const n = typeof v === 'number' ? v : Number(v);
|
|
512
|
+
return Number.isFinite(n) ? n : d;
|
|
513
|
+
};
|
|
514
|
+
const 取布尔 = (k, d) => {
|
|
515
|
+
const v = raw[k];
|
|
516
|
+
return typeof v === 'boolean' ? v : d;
|
|
517
|
+
};
|
|
518
|
+
const 取字符串 = (k, d) => {
|
|
519
|
+
const v = raw[k];
|
|
520
|
+
return typeof v === 'string' ? v : d;
|
|
521
|
+
};
|
|
522
|
+
return {
|
|
523
|
+
a_parallelExecution: 取布尔('a_parallelExecution', false),
|
|
524
|
+
e_batchSize: 取数('e_batchSize', 10),
|
|
525
|
+
b_timeoutSeconds: 取数('b_timeoutSeconds', 300),
|
|
526
|
+
c_waitForCompletion: 取布尔('c_waitForCompletion', false),
|
|
527
|
+
d_pollIntervalSeconds: 取数('d_pollIntervalSeconds', 5),
|
|
528
|
+
o_pollJitterMs: 取数('o_pollJitterMs', 300),
|
|
529
|
+
f_outputFormat: 取字符串('f_outputFormat', 'fullResponse'),
|
|
530
|
+
g_parseResultString: 取布尔('g_parseResultString', false),
|
|
531
|
+
h_requestTimeoutSeconds: 取数('h_requestTimeoutSeconds', 120),
|
|
532
|
+
i_statusTimeoutSeconds: 取数('i_statusTimeoutSeconds', 30),
|
|
533
|
+
j_maxRetries: 取数('j_maxRetries', 2),
|
|
534
|
+
k_retryInitialDelayMs: 取数('k_retryInitialDelayMs', 800),
|
|
535
|
+
l_retryBackoffFactor: 取数('l_retryBackoffFactor', 2),
|
|
536
|
+
m_honorRetryAfter: 取布尔('m_honorRetryAfter', true),
|
|
537
|
+
n_failOnScriptError: 取布尔('n_failOnScriptError', true),
|
|
538
|
+
r_autoChunkEnabled: 取布尔('r_autoChunkEnabled', false),
|
|
539
|
+
s_chunkFieldPath: 取字符串('s_chunkFieldPath', 'data'),
|
|
540
|
+
t_chunkSize: 取数('t_chunkSize', 200),
|
|
541
|
+
u_chunkMaxConcurrency: 取数('u_chunkMaxConcurrency', 3),
|
|
542
|
+
v_chunkOutputMode: 取字符串('v_chunkOutputMode', 'merge'),
|
|
543
|
+
w_debug: 取布尔('w_debug', false),
|
|
544
|
+
x_globalMaxConcurrency: 取数('x_globalMaxConcurrency', 5),
|
|
545
|
+
y_dangerEachItemEnabled: 取布尔('y_dangerEachItemEnabled', false),
|
|
546
|
+
z_confirmDangerEachItem: 取布尔('z_confirmDangerEachItem', false),
|
|
547
|
+
aa_eachItemDataPath: 取字符串('aa_eachItemDataPath', ''),
|
|
548
|
+
ab_eachItemMergeUiArgv: 取布尔('ab_eachItemMergeUiArgv', false),
|
|
549
|
+
};
|
|
552
550
|
};
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
551
|
+
const 创建重试配置 = (opt) => ({
|
|
552
|
+
最大重试次数: opt.j_maxRetries,
|
|
553
|
+
初始等待毫秒: opt.k_retryInitialDelayMs,
|
|
554
|
+
退避倍数: opt.l_retryBackoffFactor,
|
|
555
|
+
尊重RetryAfter: opt.m_honorRetryAfter,
|
|
556
|
+
});
|
|
557
|
+
return { 读取高级选项, 创建重试配置 };
|
|
558
|
+
})();
|
|
559
|
+
const 工具模块 = (() => {
|
|
560
|
+
const 等待毫秒 = async (ms) => {
|
|
561
|
+
if (ms <= 0)
|
|
562
|
+
return;
|
|
563
|
+
await (0, n8n_workflow_1.sleep)(ms);
|
|
556
564
|
};
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
e_batchSize: 取数('e_batchSize', 10),
|
|
563
|
-
f_outputFormat: 取字符串('f_outputFormat', 'fullResponse'),
|
|
564
|
-
g_parseResultString: 取布尔('g_parseResultString', false),
|
|
565
|
-
x_globalMaxConcurrency: 取数('x_globalMaxConcurrency', 5),
|
|
566
|
-
h_requestTimeoutSeconds: 取数('h_requestTimeoutSeconds', 120),
|
|
567
|
-
i_statusTimeoutSeconds: 取数('i_statusTimeoutSeconds', 30),
|
|
568
|
-
j_maxRetries: 取数('j_maxRetries', 2),
|
|
569
|
-
k_retryInitialDelayMs: 取数('k_retryInitialDelayMs', 800),
|
|
570
|
-
l_retryBackoffFactor: 取数('l_retryBackoffFactor', 2),
|
|
571
|
-
m_honorRetryAfter: 取布尔('m_honorRetryAfter', true),
|
|
572
|
-
n_failOnScriptError: 取布尔('n_failOnScriptError', true),
|
|
573
|
-
o_pollJitterMs: 取数('o_pollJitterMs', 300),
|
|
574
|
-
r_autoChunkEnabled: 取布尔('r_autoChunkEnabled', false),
|
|
575
|
-
s_chunkFieldPath: 取字符串('s_chunkFieldPath', '写表行列表'),
|
|
576
|
-
t_chunkSize: 取数('t_chunkSize', 200),
|
|
577
|
-
u_chunkMaxConcurrency: 取数('u_chunkMaxConcurrency', 3),
|
|
578
|
-
v_chunkOutputMode: 取字符串('v_chunkOutputMode', 'split'),
|
|
579
|
-
w_debug: 取布尔('w_debug', false),
|
|
565
|
+
const 脱敏ID = (id) => {
|
|
566
|
+
const s = (id !== null && id !== void 0 ? id : '').toString();
|
|
567
|
+
if (s.length <= 10)
|
|
568
|
+
return s;
|
|
569
|
+
return `${s.slice(0, 4)}***${s.slice(-4)}`;
|
|
580
570
|
};
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
};
|
|
594
|
-
const 脱敏ID = (id) => {
|
|
595
|
-
const s = (id !== null && id !== void 0 ? id : '').toString();
|
|
596
|
-
if (s.length <= 10)
|
|
597
|
-
return s;
|
|
598
|
-
return `${s.slice(0, 4)}***${s.slice(-4)}`;
|
|
599
|
-
};
|
|
600
|
-
const 解析RetryAfter毫秒 = (v) => {
|
|
601
|
-
if (v === null || v === undefined)
|
|
602
|
-
return null;
|
|
603
|
-
const s = String(v).trim();
|
|
604
|
-
if (/^\d+$/.test(s))
|
|
605
|
-
return Number(s) * 1000;
|
|
606
|
-
const t = Date.parse(s);
|
|
607
|
-
if (Number.isFinite(t)) {
|
|
608
|
-
const diff = t - Date.now();
|
|
609
|
-
return diff > 0 ? diff : 0;
|
|
610
|
-
}
|
|
611
|
-
return null;
|
|
612
|
-
};
|
|
613
|
-
const 提取Http错误 = (e) => { var _a; return (_a = e) !== null && _a !== void 0 ? _a : {}; };
|
|
614
|
-
const 是IDataObject = (v) => {
|
|
615
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
616
|
-
};
|
|
617
|
-
const getStatusCode = (err) => {
|
|
618
|
-
var _a, _b, _c, _d;
|
|
619
|
-
return (_c = (_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : err.statusCode) !== null && _c !== void 0 ? _c : (_d = err.response) === null || _d === void 0 ? void 0 : _d.statusCode;
|
|
620
|
-
};
|
|
621
|
-
const getBody = (err) => {
|
|
622
|
-
var _a, _b, _c, _d;
|
|
623
|
-
return ((_c = (_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data) !== null && _b !== void 0 ? _b : err.body) !== null && _c !== void 0 ? _c : (_d = err.response) === null || _d === void 0 ? void 0 : _d.body);
|
|
624
|
-
};
|
|
625
|
-
const getHeaders = (err) => {
|
|
626
|
-
var _a, _b, _c, _d;
|
|
627
|
-
return (_c = (_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.headers) !== null && _b !== void 0 ? _b : err.headers) !== null && _c !== void 0 ? _c : (_d = err.response) === null || _d === void 0 ? void 0 : _d.headers;
|
|
628
|
-
};
|
|
629
|
-
const 提取错误消息 = (err) => {
|
|
630
|
-
const data = getBody(err);
|
|
631
|
-
if (是IDataObject(data)) {
|
|
632
|
-
const errorDetails = data.error_details;
|
|
633
|
-
if (是IDataObject(errorDetails)) {
|
|
634
|
-
const msg = errorDetails.msg;
|
|
635
|
-
if (typeof msg === 'string' && msg.trim())
|
|
636
|
-
return msg.trim();
|
|
571
|
+
const 解析RetryAfter毫秒 = (v) => {
|
|
572
|
+
if (v === null || v === undefined)
|
|
573
|
+
return null;
|
|
574
|
+
const s = String(v).trim();
|
|
575
|
+
if (/^\d+$/.test(s))
|
|
576
|
+
return Number(s) * 1000;
|
|
577
|
+
const t = Date.parse(s);
|
|
578
|
+
if (Number.isFinite(t)) {
|
|
579
|
+
const diff = t - Date.now();
|
|
580
|
+
return diff > 0 ? diff : 0;
|
|
637
581
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return (['ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNABORTED'].includes(String(错误码 !== null && 错误码 !== void 0 ? 错误码 : '')) ||
|
|
651
|
-
[408, 425, 429, 500, 502, 503, 504].includes(Number(状态码 !== null && 状态码 !== void 0 ? 状态码 : 0)));
|
|
652
|
-
};
|
|
653
|
-
const 带重试 = async (执行函数, 配置, 调试) => {
|
|
654
|
-
var _a, _b;
|
|
655
|
-
let 尝试 = 0;
|
|
656
|
-
while (true) {
|
|
657
|
-
try {
|
|
658
|
-
const res = await 执行函数();
|
|
659
|
-
if (调试)
|
|
660
|
-
调试.重试次数 = 尝试;
|
|
661
|
-
return res;
|
|
662
|
-
}
|
|
663
|
-
catch (e) {
|
|
664
|
-
const err = 提取Http错误(e);
|
|
665
|
-
const 状态码 = getStatusCode(err);
|
|
666
|
-
if (!判断是否可重试(e) || 尝试 >= 配置.最大重试次数) {
|
|
667
|
-
if (调试)
|
|
668
|
-
调试.重试次数 = 尝试;
|
|
669
|
-
throw e;
|
|
670
|
-
}
|
|
671
|
-
let 等待 = Math.round(配置.初始等待毫秒 * Math.pow(配置.退避倍数, 尝试) + Math.random() * 200);
|
|
672
|
-
if (配置.尊重RetryAfter && 状态码 === 429) {
|
|
673
|
-
const headers = (_a = getHeaders(err)) !== null && _a !== void 0 ? _a : {};
|
|
674
|
-
const ra = (_b = headers['retry-after']) !== null && _b !== void 0 ? _b : headers['Retry-After'];
|
|
675
|
-
const raMs = 解析RetryAfter毫秒(ra);
|
|
676
|
-
if (raMs !== null)
|
|
677
|
-
等待 = Math.max(等待, raMs);
|
|
582
|
+
return null;
|
|
583
|
+
};
|
|
584
|
+
const 是IDataObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
585
|
+
const 并发执行 = async (任务函数列表, 并发数) => {
|
|
586
|
+
const 结果 = new Array(任务函数列表.length);
|
|
587
|
+
let 下一个索引 = 0;
|
|
588
|
+
const 工人 = async () => {
|
|
589
|
+
while (true) {
|
|
590
|
+
const i = 下一个索引++;
|
|
591
|
+
if (i >= 任务函数列表.length)
|
|
592
|
+
break;
|
|
593
|
+
结果[i] = await 任务函数列表[i]();
|
|
678
594
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
};
|
|
686
|
-
const 并发执行 = async (任务函数列表, 并发数) => {
|
|
687
|
-
const 结果 = new Array(任务函数列表.length);
|
|
688
|
-
let 下一个索引 = 0;
|
|
689
|
-
const 工人 = async () => {
|
|
690
|
-
while (true) {
|
|
691
|
-
const i = 下一个索引++;
|
|
692
|
-
if (i >= 任务函数列表.length)
|
|
693
|
-
break;
|
|
694
|
-
结果[i] = await 任务函数列表[i]();
|
|
695
|
-
}
|
|
595
|
+
};
|
|
596
|
+
const 工人数量 = Math.min(Math.max(1, 并发数), 任务函数列表.length);
|
|
597
|
+
await Promise.all(Array.from({ length: 工人数量 }, () => 工人()));
|
|
598
|
+
return 结果;
|
|
696
599
|
};
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
return 结果;
|
|
700
|
-
};
|
|
701
|
-
const 取路径值 = (obj, path) => {
|
|
702
|
-
if (!obj || !path)
|
|
703
|
-
return undefined;
|
|
704
|
-
const keys = String(path).split('.').filter(Boolean);
|
|
705
|
-
let cur = obj;
|
|
706
|
-
for (const k of keys) {
|
|
707
|
-
if (cur === null || cur === undefined || typeof cur !== 'object')
|
|
600
|
+
const 取路径值 = (obj, path) => {
|
|
601
|
+
if (!obj || !path)
|
|
708
602
|
return undefined;
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (keys.length === 0)
|
|
716
|
-
return obj;
|
|
717
|
-
const 递归 = (cur, idx) => {
|
|
718
|
-
const k = keys[idx];
|
|
719
|
-
const isLast = idx === keys.length - 1;
|
|
720
|
-
if (Array.isArray(cur) && !isLast) {
|
|
721
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `路径中间节点不能是数组:${keys.slice(0, idx + 1).join('.')} 是数组,无法在其下创建子字段`);
|
|
603
|
+
const keys = String(path).split('.').filter(Boolean);
|
|
604
|
+
let cur = obj;
|
|
605
|
+
for (const k of keys) {
|
|
606
|
+
if (cur === null || cur === undefined || typeof cur !== 'object')
|
|
607
|
+
return undefined;
|
|
608
|
+
cur = cur[k];
|
|
722
609
|
}
|
|
723
|
-
|
|
724
|
-
const clone = { ...base };
|
|
725
|
-
if (isLast)
|
|
726
|
-
clone[k] = value;
|
|
727
|
-
else
|
|
728
|
-
clone[k] = 递归(base[k], idx + 1);
|
|
729
|
-
return clone;
|
|
610
|
+
return cur;
|
|
730
611
|
};
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const msg = details.msg;
|
|
741
|
-
if (typeof msg === 'string' && msg.trim())
|
|
742
|
-
return msg.trim();
|
|
743
|
-
}
|
|
744
|
-
return '';
|
|
745
|
-
};
|
|
746
|
-
const 校验脚本是否失败 = (响应, itemIndex, 开启) => {
|
|
747
|
-
if (!开启)
|
|
748
|
-
return;
|
|
749
|
-
const status = typeof 响应.status === 'string' ? 响应.status : '';
|
|
750
|
-
const errorMsg = 提取脚本错误消息(响应);
|
|
751
|
-
if (status === 'finished' && errorMsg) {
|
|
752
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `脚本执行失败:${errorMsg}`, { itemIndex });
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
const 提取TaskId = (响应) => {
|
|
756
|
-
var _a, _b;
|
|
757
|
-
const fromData = (_a = 响应.data) === null || _a === void 0 ? void 0 : _a.task_id;
|
|
758
|
-
const fromTop = 响应.task_id;
|
|
759
|
-
return String((_b = fromData !== null && fromData !== void 0 ? fromData : fromTop) !== null && _b !== void 0 ? _b : '').trim();
|
|
760
|
-
};
|
|
761
|
-
const 解析执行信息 = (itemIndex, inputData) => {
|
|
762
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
763
|
-
const id输入模式 = this.getNodeParameter('idInputMode', itemIndex, 'url');
|
|
764
|
-
let 文件ID = '';
|
|
765
|
-
let 脚本ID = '';
|
|
766
|
-
if (id输入模式 === 'url') {
|
|
767
|
-
const webhook链接 = this.getNodeParameter('webhookUrl', itemIndex, '');
|
|
768
|
-
try {
|
|
769
|
-
const url对象 = new URL(webhook链接);
|
|
770
|
-
const 路径分段 = url对象.pathname.split('/');
|
|
771
|
-
const 文件ID索引 = 路径分段.indexOf('file');
|
|
772
|
-
const 脚本ID索引 = 路径分段.indexOf('script');
|
|
773
|
-
if (文件ID索引 !== -1 && 脚本ID索引 !== -1 && 路径分段.length > Math.max(文件ID索引, 脚本ID索引) + 1) {
|
|
774
|
-
文件ID = 路径分段[文件ID索引 + 1];
|
|
775
|
-
脚本ID = 路径分段[脚本ID索引 + 1];
|
|
612
|
+
const 以不可变方式写入路径 = (obj, path, value) => {
|
|
613
|
+
const keys = String(path).split('.').filter(Boolean);
|
|
614
|
+
if (keys.length === 0)
|
|
615
|
+
return obj;
|
|
616
|
+
const 递归 = (cur, idx) => {
|
|
617
|
+
const k = keys[idx];
|
|
618
|
+
const isLast = idx === keys.length - 1;
|
|
619
|
+
if (Array.isArray(cur) && !isLast) {
|
|
620
|
+
throw new n8n_workflow_1.NodeOperationError(节点, `路径中间节点不能是数组:${keys.slice(0, idx + 1).join('.')} 是数组,无法在其下创建子字段`);
|
|
776
621
|
}
|
|
777
|
-
|
|
778
|
-
|
|
622
|
+
const base = (cur && typeof cur === 'object' && !Array.isArray(cur)) ? cur : {};
|
|
623
|
+
const clone = { ...base };
|
|
624
|
+
if (isLast)
|
|
625
|
+
clone[k] = value;
|
|
626
|
+
else
|
|
627
|
+
clone[k] = 递归(base[k], idx + 1);
|
|
628
|
+
return clone;
|
|
629
|
+
};
|
|
630
|
+
return 递归(obj, 0);
|
|
631
|
+
};
|
|
632
|
+
const 第一层智能反序列化 = (argv参数) => {
|
|
633
|
+
for (const key in argv参数) {
|
|
634
|
+
if (!Object.prototype.hasOwnProperty.call(argv参数, key))
|
|
635
|
+
continue;
|
|
636
|
+
const value = argv参数[key];
|
|
637
|
+
if (typeof value !== 'string')
|
|
638
|
+
continue;
|
|
639
|
+
const trimmed = value.trim();
|
|
640
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
641
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
642
|
+
try {
|
|
643
|
+
argv参数[key] = JSON.parse(trimmed);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
}
|
|
779
647
|
}
|
|
780
648
|
}
|
|
781
|
-
|
|
782
|
-
|
|
649
|
+
};
|
|
650
|
+
return {
|
|
651
|
+
等待毫秒,
|
|
652
|
+
脱敏ID,
|
|
653
|
+
解析RetryAfter毫秒,
|
|
654
|
+
是IDataObject,
|
|
655
|
+
并发执行,
|
|
656
|
+
取路径值,
|
|
657
|
+
以不可变方式写入路径,
|
|
658
|
+
第一层智能反序列化,
|
|
659
|
+
};
|
|
660
|
+
})();
|
|
661
|
+
const 错误模块 = (() => {
|
|
662
|
+
const 提取Http错误 = (e) => { var _a; return (_a = e) !== null && _a !== void 0 ? _a : {}; };
|
|
663
|
+
const 获取状态码 = (err) => {
|
|
664
|
+
var _a, _b, _c, _d;
|
|
665
|
+
return (_c = (_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : err.statusCode) !== null && _c !== void 0 ? _c : (_d = err.response) === null || _d === void 0 ? void 0 : _d.statusCode;
|
|
666
|
+
};
|
|
667
|
+
const 获取响应体 = (err) => {
|
|
668
|
+
var _a, _b, _c, _d;
|
|
669
|
+
return ((_c = (_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data) !== null && _b !== void 0 ? _b : err.body) !== null && _c !== void 0 ? _c : (_d = err.response) === null || _d === void 0 ? void 0 : _d.body);
|
|
670
|
+
};
|
|
671
|
+
const 获取响应头 = (err) => {
|
|
672
|
+
var _a, _b, _c, _d;
|
|
673
|
+
return (_c = (_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.headers) !== null && _b !== void 0 ? _b : err.headers) !== null && _c !== void 0 ? _c : (_d = err.response) === null || _d === void 0 ? void 0 : _d.headers;
|
|
674
|
+
};
|
|
675
|
+
const 提取错误消息 = (err) => {
|
|
676
|
+
const 响应体数据 = 获取响应体(err);
|
|
677
|
+
if (工具模块.是IDataObject(响应体数据)) {
|
|
678
|
+
const 错误详情 = 响应体数据.error_details;
|
|
679
|
+
if (工具模块.是IDataObject(错误详情)) {
|
|
680
|
+
const 错误消息 = 错误详情.msg;
|
|
681
|
+
if (typeof 错误消息 === 'string' && 错误消息.trim())
|
|
682
|
+
return 错误消息.trim();
|
|
683
|
+
}
|
|
684
|
+
const 错误信息 = 响应体数据.error;
|
|
685
|
+
if (typeof 错误信息 === 'string' && 错误信息.trim())
|
|
686
|
+
return 错误信息.trim();
|
|
783
687
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const 可选上下文参数 = this.getNodeParameter('contextParameters', itemIndex, {});
|
|
795
|
-
let 最终_sheet_name = '';
|
|
796
|
-
let 最终_range = '';
|
|
797
|
-
let 最终_link_from = '';
|
|
798
|
-
let 最终_db_active_view = '';
|
|
799
|
-
let 最终_db_selection = '';
|
|
800
|
-
const 模板到字段名 = {
|
|
801
|
-
custom: 'argv_custom',
|
|
802
|
-
basicInput: 'argv_basicInput',
|
|
803
|
-
attachment: 'argv_attachment',
|
|
804
|
-
cascade: 'argv_cascade',
|
|
805
|
-
migrate: 'argv_migrate',
|
|
806
|
-
formula: 'argv_formula',
|
|
807
|
-
setLinkProperties: 'argv_setLinkProperties',
|
|
808
|
-
setCascadeOptions: 'argv_setCascadeOptions',
|
|
809
|
-
n8nExpression: 'argv_n8nExpression',
|
|
810
|
-
compositeKey: 'argv_compositeKey',
|
|
811
|
-
createSheet: 'argv_createSheet',
|
|
688
|
+
if (typeof err.message === 'string' && err.message.trim())
|
|
689
|
+
return err.message.trim();
|
|
690
|
+
return '未知错误';
|
|
691
|
+
};
|
|
692
|
+
const 判断是否可重试 = (e) => {
|
|
693
|
+
const 错误 = 提取Http错误(e);
|
|
694
|
+
const 错误码 = 错误.code;
|
|
695
|
+
const 状态码 = 获取状态码(错误);
|
|
696
|
+
return (['ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNABORTED'].includes(String(错误码 !== null && 错误码 !== void 0 ? 错误码 : '')) ||
|
|
697
|
+
[408, 425, 429, 500, 502, 503, 504].includes(Number(状态码 !== null && 状态码 !== void 0 ? 状态码 : 0)));
|
|
812
698
|
};
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
699
|
+
return { 提取Http错误, 获取状态码, 获取响应体, 获取响应头, 提取错误消息, 判断是否可重试 };
|
|
700
|
+
})();
|
|
701
|
+
const 重试模块 = (() => {
|
|
702
|
+
const 带重试 = async (执行函数, 配置, 调试) => {
|
|
703
|
+
var _a, _b;
|
|
704
|
+
let 尝试次数 = 0;
|
|
705
|
+
while (true) {
|
|
818
706
|
try {
|
|
819
|
-
|
|
707
|
+
const 执行结果 = await 执行函数();
|
|
708
|
+
if (调试)
|
|
709
|
+
调试.重试次数 = 尝试次数;
|
|
710
|
+
return 执行结果;
|
|
820
711
|
}
|
|
821
|
-
catch {
|
|
822
|
-
|
|
712
|
+
catch (错误) {
|
|
713
|
+
const 实际错误 = 错误模块.提取Http错误(错误);
|
|
714
|
+
const 状态码 = 错误模块.获取状态码(实际错误);
|
|
715
|
+
if (!错误模块.判断是否可重试(错误) || 尝试次数 >= 配置.最大重试次数) {
|
|
716
|
+
if (调试)
|
|
717
|
+
调试.重试次数 = 尝试次数;
|
|
718
|
+
throw 错误;
|
|
719
|
+
}
|
|
720
|
+
let 等待毫秒数 = Math.round(配置.初始等待毫秒 * Math.pow(配置.退避倍数, 尝试次数) + Math.random() * 200);
|
|
721
|
+
if (配置.尊重RetryAfter && 状态码 === 429) {
|
|
722
|
+
const 响应头 = (_a = 错误模块.获取响应头(实际错误)) !== null && _a !== void 0 ? _a : {};
|
|
723
|
+
const 重试后 = (_b = 响应头['retry-after']) !== null && _b !== void 0 ? _b : 响应头['Retry-After'];
|
|
724
|
+
const 重试后毫秒 = 工具模块.解析RetryAfter毫秒(重试后);
|
|
725
|
+
if (重试后毫秒 !== null)
|
|
726
|
+
等待毫秒数 = Math.max(等待毫秒数, 重试后毫秒);
|
|
727
|
+
}
|
|
728
|
+
if (调试)
|
|
729
|
+
调试.最后等待毫秒 = 等待毫秒数;
|
|
730
|
+
await 工具模块.等待毫秒(等待毫秒数);
|
|
731
|
+
尝试次数++;
|
|
823
732
|
}
|
|
824
733
|
}
|
|
825
|
-
|
|
826
|
-
|
|
734
|
+
};
|
|
735
|
+
return { 带重试 };
|
|
736
|
+
})();
|
|
737
|
+
const 脚本模块 = (() => {
|
|
738
|
+
const 提取脚本错误消息 = (响应) => {
|
|
739
|
+
var _a;
|
|
740
|
+
const error = String((_a = 响应.error) !== null && _a !== void 0 ? _a : '').trim();
|
|
741
|
+
if (error)
|
|
742
|
+
return error;
|
|
743
|
+
const details = 响应.error_details;
|
|
744
|
+
if (details && typeof details === 'object' && !Array.isArray(details)) {
|
|
745
|
+
const msg = details.msg;
|
|
746
|
+
if (typeof msg === 'string' && msg.trim())
|
|
747
|
+
return msg.trim();
|
|
748
|
+
}
|
|
749
|
+
return '';
|
|
750
|
+
};
|
|
751
|
+
const 校验脚本是否失败 = (响应, itemIndex, 开启) => {
|
|
752
|
+
if (!开启)
|
|
753
|
+
return;
|
|
754
|
+
const status = typeof 响应.status === 'string' ? 响应.status : '';
|
|
755
|
+
const errorMsg = 提取脚本错误消息(响应);
|
|
756
|
+
if (status === 'finished' && errorMsg) {
|
|
757
|
+
throw new n8n_workflow_1.NodeOperationError(节点, `脚本执行失败:${errorMsg}`, { itemIndex });
|
|
827
758
|
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
759
|
+
};
|
|
760
|
+
const 提取TaskId = (响应) => {
|
|
761
|
+
var _a, _b;
|
|
762
|
+
const fromData = (_a = 响应.data) === null || _a === void 0 ? void 0 : _a.task_id;
|
|
763
|
+
const fromTop = 响应.task_id;
|
|
764
|
+
return String((_b = fromData !== null && fromData !== void 0 ? fromData : fromTop) !== null && _b !== void 0 ? _b : '').trim();
|
|
765
|
+
};
|
|
766
|
+
return { 校验脚本是否失败, 提取TaskId };
|
|
767
|
+
})();
|
|
768
|
+
const 参数模块 = (() => {
|
|
769
|
+
const 解析文件脚本ID = (itemIndex) => {
|
|
770
|
+
const id输入模式 = this.getNodeParameter('idInputMode', itemIndex, 'url');
|
|
771
|
+
let 文件ID = '';
|
|
772
|
+
let 脚本ID = '';
|
|
773
|
+
if (id输入模式 === 'url') {
|
|
774
|
+
const webhook链接 = this.getNodeParameter('webhookUrl', itemIndex, '');
|
|
775
|
+
try {
|
|
776
|
+
const url对象 = new URL(webhook链接);
|
|
777
|
+
const 路径分段 = url对象.pathname.split('/');
|
|
778
|
+
const 文件ID索引 = 路径分段.indexOf('file');
|
|
779
|
+
const 脚本ID索引 = 路径分段.indexOf('script');
|
|
780
|
+
if (文件ID索引 !== -1 &&
|
|
781
|
+
脚本ID索引 !== -1 &&
|
|
782
|
+
路径分段.length > Math.max(文件ID索引, 脚本ID索引) + 1) {
|
|
783
|
+
文件ID = 路径分段[文件ID索引 + 1];
|
|
784
|
+
脚本ID = 路径分段[脚本ID索引 + 1];
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Webhook 链接格式不正确,无法解析出 File ID 和 Script ID。', { itemIndex });
|
|
840
788
|
}
|
|
841
789
|
}
|
|
842
|
-
|
|
843
|
-
|
|
790
|
+
catch {
|
|
791
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Webhook 链接格式不正确,无法解析出 File ID 和 Script ID。', { itemIndex });
|
|
844
792
|
}
|
|
845
793
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
794
|
+
else {
|
|
795
|
+
文件ID = this.getNodeParameter('fileId', itemIndex, '');
|
|
796
|
+
脚本ID = this.getNodeParameter('scriptId', itemIndex, '');
|
|
797
|
+
}
|
|
798
|
+
if (!文件ID || !脚本ID) {
|
|
799
|
+
throw new n8n_workflow_1.NodeOperationError(节点, '未能获取到有效的 File ID 和 Script ID。', { itemIndex });
|
|
800
|
+
}
|
|
801
|
+
return { 文件ID, 脚本ID };
|
|
802
|
+
};
|
|
803
|
+
const 读取UIArgv = (itemIndex) => {
|
|
804
|
+
const raw = this.getNodeParameter('argv', itemIndex, {});
|
|
805
|
+
if (typeof raw === 'string') {
|
|
855
806
|
try {
|
|
856
|
-
|
|
807
|
+
return JSON.parse(raw);
|
|
857
808
|
}
|
|
858
809
|
catch {
|
|
859
|
-
|
|
810
|
+
return { value: raw };
|
|
860
811
|
}
|
|
861
812
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
813
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw))
|
|
814
|
+
return raw;
|
|
815
|
+
return {};
|
|
816
|
+
};
|
|
817
|
+
const 读取执行模式V3 = (itemIndex) => {
|
|
818
|
+
return this.getNodeParameter('runModeV3', itemIndex, 'uiOnce');
|
|
819
|
+
};
|
|
820
|
+
const 取输入项数据 = (item, 路径) => {
|
|
821
|
+
if (!(item === null || item === void 0 ? void 0 : item.json))
|
|
822
|
+
return undefined;
|
|
823
|
+
if (!路径)
|
|
824
|
+
return item.json;
|
|
825
|
+
return 工具模块.取路径值(item.json, 路径);
|
|
826
|
+
};
|
|
827
|
+
const 合并Context = (itemIndex, argv参数, 调试字段) => {
|
|
828
|
+
const 可选上下文参数 = this.getNodeParameter('contextParameters', itemIndex, {});
|
|
829
|
+
const 规范化文本 = (v) => {
|
|
830
|
+
if (v === null || v === undefined)
|
|
831
|
+
return '';
|
|
832
|
+
if (typeof v === 'string')
|
|
833
|
+
return v.trim();
|
|
834
|
+
return String(v).trim();
|
|
835
|
+
};
|
|
836
|
+
const 从Argv取 = (snakeKey, camelKey) => {
|
|
837
|
+
var _a;
|
|
838
|
+
const v = (_a = argv参数[snakeKey]) !== null && _a !== void 0 ? _a : argv参数[camelKey];
|
|
839
|
+
return 规范化文本(v);
|
|
840
|
+
};
|
|
841
|
+
const 合并一个字段 = (参数) => {
|
|
842
|
+
const argv值 = 从Argv取(参数.snakeKey, 参数.camelKey);
|
|
843
|
+
const context值 = 规范化文本(可选上下文参数[参数.contextKey]);
|
|
844
|
+
if (argv值 && context值 && argv值 !== context值) {
|
|
845
|
+
if (调试字段) {
|
|
846
|
+
if (!调试字段.Context合并冲突)
|
|
847
|
+
调试字段.Context合并冲突 = [];
|
|
848
|
+
调试字段.Context合并冲突.push({
|
|
849
|
+
字段: 参数.字段中文名,
|
|
850
|
+
argv: argv值,
|
|
851
|
+
contextParameters: context值,
|
|
852
|
+
最终采用: 'argv优先',
|
|
853
|
+
});
|
|
854
|
+
}
|
|
873
855
|
}
|
|
874
|
-
|
|
875
|
-
|
|
856
|
+
const 最终值 = argv值 || context值;
|
|
857
|
+
if (最终值) {
|
|
858
|
+
argv参数[参数.snakeKey] = 最终值;
|
|
876
859
|
}
|
|
860
|
+
return 最终值;
|
|
861
|
+
};
|
|
862
|
+
const 最终_sheet_name = 合并一个字段({ 字段中文名: '表名', snakeKey: 'sheet_name', camelKey: 'sheetName', contextKey: 'sheetName' });
|
|
863
|
+
const 最终_range = 合并一个字段({ 字段中文名: '范围', snakeKey: 'range', camelKey: 'range', contextKey: 'range' });
|
|
864
|
+
const 最终_link_from = 合并一个字段({ 字段中文名: '链接来源', snakeKey: 'link_from', camelKey: 'linkFrom', contextKey: 'linkFrom' });
|
|
865
|
+
const 最终_db_active_view = 合并一个字段({ 字段中文名: '视图名称', snakeKey: 'db_active_view', camelKey: 'dbActiveView', contextKey: 'dbActiveView' });
|
|
866
|
+
const 最终_db_selection = 合并一个字段({ 字段中文名: '选区', snakeKey: 'db_selection', camelKey: 'dbSelection', contextKey: 'dbSelection' });
|
|
867
|
+
const 上下文 = { argv: argv参数 };
|
|
868
|
+
if (最终_sheet_name)
|
|
869
|
+
上下文.sheet_name = 最终_sheet_name;
|
|
870
|
+
if (最终_range)
|
|
871
|
+
上下文.range = 最终_range;
|
|
872
|
+
if (最终_link_from)
|
|
873
|
+
上下文.link_from = 最终_link_from;
|
|
874
|
+
if (最终_db_active_view)
|
|
875
|
+
上下文.db_active_view = 最终_db_active_view;
|
|
876
|
+
if (最终_db_selection)
|
|
877
|
+
上下文.db_selection = 最终_db_selection;
|
|
878
|
+
return 上下文;
|
|
879
|
+
};
|
|
880
|
+
const 聚合输入项到数组 = (itemIndex, 目标路径, 提取路径, 策略, uiArgv) => {
|
|
881
|
+
const 聚合数组 = [];
|
|
882
|
+
for (const item of 输入数据项列表) {
|
|
883
|
+
const v = 取输入项数据(item, 提取路径);
|
|
884
|
+
if (v !== undefined)
|
|
885
|
+
聚合数组.push(v);
|
|
877
886
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
if (
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
if (typeof value !== 'string')
|
|
888
|
-
continue;
|
|
889
|
-
const trimmed = value.trim();
|
|
890
|
-
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
891
|
-
try {
|
|
892
|
-
argv参数[key] = JSON.parse(trimmed);
|
|
893
|
-
}
|
|
894
|
-
catch (e) {
|
|
895
|
-
void e;
|
|
887
|
+
const 当前值 = 工具模块.取路径值(uiArgv, 目标路径);
|
|
888
|
+
if (策略 === 'ifMissing') {
|
|
889
|
+
if (当前值 !== undefined)
|
|
890
|
+
return uiArgv;
|
|
891
|
+
return 工具模块.以不可变方式写入路径(uiArgv, 目标路径, 聚合数组);
|
|
892
|
+
}
|
|
893
|
+
if (策略 === 'append') {
|
|
894
|
+
if (Array.isArray(当前值)) {
|
|
895
|
+
return 工具模块.以不可变方式写入路径(uiArgv, 目标路径, [...当前值, ...聚合数组]);
|
|
896
896
|
}
|
|
897
|
+
return 工具模块.以不可变方式写入路径(uiArgv, 目标路径, 聚合数组);
|
|
897
898
|
}
|
|
898
|
-
|
|
899
|
-
const 从Argv提取 = (snakeKey, camelKey) => {
|
|
900
|
-
if (argv参数[snakeKey] !== undefined)
|
|
901
|
-
return argv参数[snakeKey];
|
|
902
|
-
if (argv参数[camelKey] !== undefined)
|
|
903
|
-
return argv参数[camelKey];
|
|
904
|
-
return undefined;
|
|
899
|
+
return 工具模块.以不可变方式写入路径(uiArgv, 目标路径, 聚合数组);
|
|
905
900
|
};
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if (最终_db_selection)
|
|
920
|
-
argv参数.db_selection = 最终_db_selection;
|
|
921
|
-
const 上下文 = { argv: argv参数 };
|
|
922
|
-
if (最终_sheet_name)
|
|
923
|
-
上下文.sheet_name = 最终_sheet_name;
|
|
924
|
-
if (最终_range)
|
|
925
|
-
上下文.range = 最终_range;
|
|
926
|
-
if (最终_link_from)
|
|
927
|
-
上下文.link_from = 最终_link_from;
|
|
928
|
-
if (最终_db_active_view)
|
|
929
|
-
上下文.db_active_view = 最终_db_active_view;
|
|
930
|
-
if (最终_db_selection)
|
|
931
|
-
上下文.db_selection = 最终_db_selection;
|
|
932
|
-
return { 文件ID, 脚本ID, argv参数, 上下文 };
|
|
933
|
-
};
|
|
934
|
-
const 调用脚本接口 = async (参数) => {
|
|
935
|
-
var _a, _b;
|
|
936
|
-
const endpoint = 参数.类型 === 'sync' ? API_CONFIG.ENDPOINTS.SYNC_TASK : API_CONFIG.ENDPOINTS.ASYNC_TASK;
|
|
937
|
-
const 请求URL = `${API_CONFIG.BASE_URL}${endpoint}`
|
|
938
|
-
.replace(':file_id', 参数.文件ID)
|
|
939
|
-
.replace(':script_id', 参数.脚本ID);
|
|
940
|
-
const t0 = Date.now();
|
|
941
|
-
const 调试统计 = {};
|
|
942
|
-
const options = {
|
|
943
|
-
method: 'POST',
|
|
944
|
-
url: 请求URL,
|
|
945
|
-
headers: {
|
|
946
|
-
'Content-Type': 'application/json',
|
|
947
|
-
'AirScript-Token': 凭证.apiToken,
|
|
948
|
-
},
|
|
949
|
-
body: { Context: 参数.上下文 },
|
|
950
|
-
json: true,
|
|
951
|
-
timeout: 参数.请求超时毫秒,
|
|
901
|
+
const 构建Argv_V3安全模式 = (itemIndex) => {
|
|
902
|
+
var _a, _b, _c;
|
|
903
|
+
const 执行模式 = 读取执行模式V3(itemIndex);
|
|
904
|
+
const uiArgv = 读取UIArgv(itemIndex);
|
|
905
|
+
工具模块.第一层智能反序列化(uiArgv);
|
|
906
|
+
if (执行模式 === 'uiOnce') {
|
|
907
|
+
return uiArgv;
|
|
908
|
+
}
|
|
909
|
+
const 聚合配置 = this.getNodeParameter('aggregate', itemIndex, {});
|
|
910
|
+
const 目标数组字段路径 = String((_a = 聚合配置.targetArrayField) !== null && _a !== void 0 ? _a : 'data');
|
|
911
|
+
const 项目数据路径 = String((_b = 聚合配置.itemDataPath) !== null && _b !== void 0 ? _b : '');
|
|
912
|
+
const 注入 = ((_c = 聚合配置.injectPolicy) !== null && _c !== void 0 ? _c : 'overwrite');
|
|
913
|
+
return 聚合输入项到数组(itemIndex, 目标数组字段路径, 项目数据路径, 注入, uiArgv);
|
|
952
914
|
};
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
915
|
+
const 构建Argv_危险每项模式 = (itemIndex, opt) => {
|
|
916
|
+
const 每项路径 = opt.aa_eachItemDataPath;
|
|
917
|
+
const item = 输入数据项列表[itemIndex];
|
|
918
|
+
const 每项原始 = 取输入项数据(item, 每项路径);
|
|
919
|
+
let 每项Argv = {};
|
|
920
|
+
if (每项原始 && typeof 每项原始 === 'object' && !Array.isArray(每项原始)) {
|
|
921
|
+
每项Argv = { ...每项原始 };
|
|
957
922
|
}
|
|
958
|
-
|
|
959
|
-
|
|
923
|
+
else if (每项原始 !== undefined) {
|
|
924
|
+
每项Argv = { value: 每项原始 };
|
|
960
925
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
if (!参数.taskId || !参数.taskId.trim()) {
|
|
975
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'taskId 不能为空', { itemIndex: (_a = 参数.itemIndex) !== null && _a !== void 0 ? _a : 0 });
|
|
976
|
-
}
|
|
977
|
-
const 请求URL = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.GET_TASK}?task_id=${encodeURIComponent(参数.taskId)}`;
|
|
978
|
-
const t0 = Date.now();
|
|
979
|
-
const 调试统计 = {};
|
|
980
|
-
const options = {
|
|
981
|
-
method: 'GET',
|
|
982
|
-
url: 请求URL,
|
|
983
|
-
headers: { 'Content-Type': 'application/json', 'AirScript-Token': 凭证.apiToken },
|
|
984
|
-
json: true,
|
|
985
|
-
timeout: 参数.状态超时毫秒,
|
|
926
|
+
工具模块.第一层智能反序列化(每项Argv);
|
|
927
|
+
if (!opt.ab_eachItemMergeUiArgv)
|
|
928
|
+
return 每项Argv;
|
|
929
|
+
const uiArgv = 读取UIArgv(0);
|
|
930
|
+
工具模块.第一层智能反序列化(uiArgv);
|
|
931
|
+
const 数据字段 = opt.s_chunkFieldPath || 'data';
|
|
932
|
+
const uiData = 工具模块.取路径值(uiArgv, 数据字段);
|
|
933
|
+
if (输入数据项列表.length > 1 && Array.isArray(uiData) && uiData.length > 0) {
|
|
934
|
+
throw new n8n_workflow_1.NodeOperationError(节点, `危险模式下你开启了「合并 UI Argv」,且 UI Argv 的 ${数据字段} 是一个长度为 ${uiData.length} 的数组。\n` +
|
|
935
|
+
`这会导致重复写入(例如:10 个输入项 × data 10 条 => 100 条)。\n` +
|
|
936
|
+
`建议关闭「合并 UI Argv」,或关闭危险模式,或改用「执行一次」。`, { itemIndex: 0 });
|
|
937
|
+
}
|
|
938
|
+
return { ...uiArgv, ...每项Argv };
|
|
986
939
|
};
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
940
|
+
return {
|
|
941
|
+
解析文件脚本ID,
|
|
942
|
+
读取执行模式V3,
|
|
943
|
+
构建Argv_V3安全模式,
|
|
944
|
+
构建Argv_危险每项模式,
|
|
945
|
+
合并Context,
|
|
946
|
+
};
|
|
947
|
+
})();
|
|
948
|
+
const 网络模块 = (() => {
|
|
949
|
+
const 调用脚本接口 = async (参数) => {
|
|
950
|
+
var _a, _b;
|
|
951
|
+
const 接口端点 = 参数.类型 === 'sync' ? API_CONFIG.ENDPOINTS.SYNC_TASK : API_CONFIG.ENDPOINTS.ASYNC_TASK;
|
|
952
|
+
const 请求URL = `${API_CONFIG.BASE_URL}${接口端点}`
|
|
953
|
+
.replace(':file_id', 参数.文件ID)
|
|
954
|
+
.replace(':script_id', 参数.脚本ID);
|
|
955
|
+
const 开始时间 = Date.now();
|
|
956
|
+
const 调试统计 = {};
|
|
957
|
+
const 请求选项 = {
|
|
958
|
+
method: 'POST',
|
|
959
|
+
url: 请求URL,
|
|
960
|
+
headers: {
|
|
961
|
+
'Content-Type': 'application/json',
|
|
962
|
+
'AirScript-Token': 凭证.apiToken,
|
|
963
|
+
},
|
|
964
|
+
body: { Context: 参数.上下文 },
|
|
965
|
+
json: true,
|
|
966
|
+
timeout: 参数.请求超时毫秒,
|
|
967
|
+
};
|
|
968
|
+
const 响应结果 = (await 重试模块.带重试(async () => {
|
|
969
|
+
await 参数.并发闸门.acquire();
|
|
970
|
+
try {
|
|
971
|
+
return (await this.helpers.httpRequest(请求选项));
|
|
972
|
+
}
|
|
973
|
+
finally {
|
|
974
|
+
参数.并发闸门.release();
|
|
975
|
+
}
|
|
976
|
+
}, 参数.重试, 调试统计));
|
|
977
|
+
if (参数.调试) {
|
|
978
|
+
参数.调试['执行脚本'] = {
|
|
979
|
+
url: `/api/v3/ide/file/${工具模块.脱敏ID(参数.文件ID)}/script/${工具模块.脱敏ID(参数.脚本ID)}/${参数.类型 === 'sync' ? 'sync_task' : 'task'}`,
|
|
980
|
+
耗时毫秒: Date.now() - 开始时间,
|
|
981
|
+
重试次数: (_a = 调试统计.重试次数) !== null && _a !== void 0 ? _a : 0,
|
|
982
|
+
最后等待毫秒: (_b = 调试统计.最后等待毫秒) !== null && _b !== void 0 ? _b : null,
|
|
983
|
+
};
|
|
991
984
|
}
|
|
992
|
-
|
|
993
|
-
|
|
985
|
+
return 响应结果;
|
|
986
|
+
};
|
|
987
|
+
const 获取任务状态 = async (参数) => {
|
|
988
|
+
var _a, _b, _c;
|
|
989
|
+
if (!参数.taskId || !参数.taskId.trim()) {
|
|
990
|
+
throw new n8n_workflow_1.NodeOperationError(节点, 'taskId 不能为空', { itemIndex: (_a = 参数.itemIndex) !== null && _a !== void 0 ? _a : 0 });
|
|
994
991
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
992
|
+
const 请求URL = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.GET_TASK}?task_id=${encodeURIComponent(参数.taskId)}`;
|
|
993
|
+
const 开始时间 = Date.now();
|
|
994
|
+
const 调试统计 = {};
|
|
995
|
+
const 请求选项 = {
|
|
996
|
+
method: 'GET',
|
|
997
|
+
url: 请求URL,
|
|
998
|
+
headers: { 'Content-Type': 'application/json', 'AirScript-Token': 凭证.apiToken },
|
|
999
|
+
json: true,
|
|
1000
|
+
timeout: 参数.状态超时毫秒,
|
|
1002
1001
|
};
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1002
|
+
const 响应结果 = (await 重试模块.带重试(async () => {
|
|
1003
|
+
await 参数.并发闸门.acquire();
|
|
1004
|
+
try {
|
|
1005
|
+
return (await this.helpers.httpRequest(请求选项));
|
|
1006
|
+
}
|
|
1007
|
+
finally {
|
|
1008
|
+
参数.并发闸门.release();
|
|
1009
|
+
}
|
|
1010
|
+
}, 参数.重试, 调试统计));
|
|
1011
|
+
if (参数.调试) {
|
|
1012
|
+
参数.调试['获取任务状态'] = {
|
|
1013
|
+
task_id: 工具模块.脱敏ID(参数.taskId),
|
|
1014
|
+
耗时毫秒: Date.now() - 开始时间,
|
|
1015
|
+
重试次数: (_b = 调试统计.重试次数) !== null && _b !== void 0 ? _b : 0,
|
|
1016
|
+
最后等待毫秒: (_c = 调试统计.最后等待毫秒) !== null && _c !== void 0 ? _c : null,
|
|
1017
|
+
};
|
|
1014
1018
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
if (状态 === 'finished') {
|
|
1027
|
-
校验脚本是否失败(状态响应, 参数.itemIndex, 参数.finished但error算失败);
|
|
1019
|
+
return 响应结果;
|
|
1020
|
+
};
|
|
1021
|
+
const 等待异步任务完成 = async (参数) => {
|
|
1022
|
+
var _a, _b;
|
|
1023
|
+
const 截止时间 = Date.now() + 参数.总等待超时毫秒;
|
|
1024
|
+
let 首次 = true;
|
|
1025
|
+
let 轮询次数 = 0;
|
|
1026
|
+
while (Date.now() < 截止时间) {
|
|
1027
|
+
if (!首次) {
|
|
1028
|
+
const 抖动时间 = 参数.轮询抖动毫秒 > 0 ? Math.floor(Math.random() * 参数.轮询抖动毫秒) : 0;
|
|
1029
|
+
await 工具模块.等待毫秒(参数.轮询间隔毫秒 + 抖动时间);
|
|
1028
1030
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1031
|
+
首次 = false;
|
|
1032
|
+
const 状态响应 = await 获取任务状态({
|
|
1033
|
+
taskId: 参数.taskId,
|
|
1034
|
+
状态超时毫秒: 参数.状态超时毫秒,
|
|
1035
|
+
重试: 参数.重试,
|
|
1036
|
+
调试: 参数.调试,
|
|
1037
|
+
并发闸门: 参数.并发闸门,
|
|
1038
|
+
});
|
|
1039
|
+
const 任务状态 = typeof 状态响应.status === 'string' ? 状态响应.status.toLowerCase() : '';
|
|
1040
|
+
const 终端状态列表 = ['finished', 'failed', 'canceled', 'cancelled', 'error'];
|
|
1041
|
+
if (终端状态列表.includes(任务状态)) {
|
|
1042
|
+
if (任务状态 === 'finished') {
|
|
1043
|
+
脚本模块.校验脚本是否失败(状态响应, 参数.itemIndex, 参数.finished但error算失败);
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
const 错误信息 = `任务执行${任务状态}:` +
|
|
1047
|
+
(String((_b = (_a = 状态响应.error) !== null && _a !== void 0 ? _a : 状态响应.message) !== null && _b !== void 0 ? _b : '未知错误'));
|
|
1048
|
+
throw new n8n_workflow_1.NodeOperationError(节点, 错误信息, { itemIndex: 参数.itemIndex });
|
|
1049
|
+
}
|
|
1050
|
+
if (参数.调试)
|
|
1051
|
+
参数.调试['轮询次数'] = (轮询次数 + 1);
|
|
1052
|
+
return 状态响应;
|
|
1032
1053
|
}
|
|
1033
|
-
|
|
1034
|
-
参数.调试['轮询次数'] = (轮询次数 + 1);
|
|
1035
|
-
return 状态响应;
|
|
1054
|
+
轮询次数++;
|
|
1036
1055
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1056
|
+
if (参数.调试)
|
|
1057
|
+
参数.调试['轮询次数'] = 轮询次数;
|
|
1058
|
+
throw new n8n_workflow_1.NodeOperationError(节点, `异步任务等待超时(${Math.round(参数.总等待超时毫秒 / 1000)}秒)`, { itemIndex: 参数.itemIndex });
|
|
1059
|
+
};
|
|
1060
|
+
return { 调用脚本接口, 获取任务状态, 等待异步任务完成 };
|
|
1061
|
+
})();
|
|
1062
|
+
const 输出模块 = (() => {
|
|
1063
|
+
const 格式化输出 = (响应数据, 索引, opt, 附加, operation) => {
|
|
1064
|
+
var _a, _b, _c, _d, _e;
|
|
1065
|
+
const 数据内容 = ((_a = 响应数据.data) !== null && _a !== void 0 ? _a : undefined);
|
|
1066
|
+
if (opt.g_parseResultString && 数据内容 && typeof 数据内容.result === 'string') {
|
|
1067
|
+
try {
|
|
1068
|
+
数据内容.result = JSON.parse(数据内容.result);
|
|
1069
|
+
}
|
|
1070
|
+
catch {
|
|
1071
|
+
数据内容.result = 数据内容.result.replace(/\\n/g, '\n');
|
|
1072
|
+
}
|
|
1051
1073
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1074
|
+
const 元数据 = { operation: operation || 'unknown' };
|
|
1075
|
+
const 数据中的任务ID = 数据内容 === null || 数据内容 === void 0 ? void 0 : 数据内容.task_id;
|
|
1076
|
+
const 响应中的任务ID = 响应数据.task_id;
|
|
1077
|
+
if (数据中的任务ID)
|
|
1078
|
+
元数据.task_id = 数据中的任务ID;
|
|
1079
|
+
else if (响应中的任务ID)
|
|
1080
|
+
元数据.task_id = 响应中的任务ID;
|
|
1081
|
+
const 批次索引 = (_b = 附加 === null || 附加 === void 0 ? void 0 : 附加.批次索引) !== null && _b !== void 0 ? _b : 附加 === null || 附加 === void 0 ? void 0 : 附加.batchIndex;
|
|
1082
|
+
if (批次索引 !== undefined)
|
|
1083
|
+
元数据.batchIndex = 批次索引;
|
|
1084
|
+
const 输出列表 = [];
|
|
1085
|
+
if (opt.f_outputFormat === 'resultOnly') {
|
|
1086
|
+
输出列表.push({
|
|
1087
|
+
json: {
|
|
1088
|
+
...(附加 !== null && 附加 !== void 0 ? 附加 : {}),
|
|
1089
|
+
_meta: 元数据,
|
|
1090
|
+
result: (_c = 数据内容 === null || 数据内容 === void 0 ? void 0 : 数据内容.result) !== null && _c !== void 0 ? _c : null,
|
|
1091
|
+
status: ((_d = 响应数据.status) !== null && _d !== void 0 ? _d : null),
|
|
1092
|
+
error: ((_e = 响应数据.error) !== null && _e !== void 0 ? _e : ''),
|
|
1093
|
+
},
|
|
1094
|
+
pairedItem: { item: 索引 },
|
|
1095
|
+
});
|
|
1096
|
+
return 输出列表;
|
|
1054
1097
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
_meta.sheet_name = sheetNameFromArgv;
|
|
1066
|
-
const taskIdFromData = data === null || data === void 0 ? void 0 : data.task_id;
|
|
1067
|
-
const taskIdFromResponse = 响应数据.task_id;
|
|
1068
|
-
if (taskIdFromData)
|
|
1069
|
-
_meta.task_id = taskIdFromData;
|
|
1070
|
-
else if (taskIdFromResponse)
|
|
1071
|
-
_meta.task_id = taskIdFromResponse;
|
|
1072
|
-
const batchIndex = (_b = 附加 === null || 附加 === void 0 ? void 0 : 附加.批次索引) !== null && _b !== void 0 ? _b : 附加 === null || 附加 === void 0 ? void 0 : 附加.batchIndex;
|
|
1073
|
-
if (batchIndex !== undefined)
|
|
1074
|
-
_meta.batchIndex = batchIndex;
|
|
1075
|
-
const out = [];
|
|
1076
|
-
if (opt.f_outputFormat === 'resultOnly') {
|
|
1077
|
-
out.push({
|
|
1078
|
-
json: {
|
|
1079
|
-
...(附加 !== null && 附加 !== void 0 ? 附加 : {}),
|
|
1080
|
-
_meta,
|
|
1081
|
-
result: (_c = data === null || data === void 0 ? void 0 : data.result) !== null && _c !== void 0 ? _c : null,
|
|
1082
|
-
status: ((_d = 响应数据.status) !== null && _d !== void 0 ? _d : null),
|
|
1083
|
-
error: ((_e = 响应数据.error) !== null && _e !== void 0 ? _e : ''),
|
|
1084
|
-
},
|
|
1085
|
-
pairedItem: { item: 索引 },
|
|
1086
|
-
});
|
|
1087
|
-
return out;
|
|
1088
|
-
}
|
|
1089
|
-
if (opt.f_outputFormat === 'splitLogs') {
|
|
1090
|
-
const logs = data === null || data === void 0 ? void 0 : data.logs;
|
|
1091
|
-
if (Array.isArray(logs)) {
|
|
1092
|
-
for (const 日志条目 of logs) {
|
|
1093
|
-
if (日志条目 && typeof 日志条目 === 'object') {
|
|
1094
|
-
out.push({ json: { ...(附加 !== null && 附加 !== void 0 ? 附加 : {}), _meta, ...日志条目 }, pairedItem: { item: 索引 } });
|
|
1098
|
+
if (opt.f_outputFormat === 'splitLogs') {
|
|
1099
|
+
const 日志列表 = 数据内容 === null || 数据内容 === void 0 ? void 0 : 数据内容.logs;
|
|
1100
|
+
if (Array.isArray(日志列表) && 日志列表.length > 0) {
|
|
1101
|
+
for (const 日志条目 of 日志列表) {
|
|
1102
|
+
if (日志条目 && typeof 日志条目 === 'object') {
|
|
1103
|
+
输出列表.push({
|
|
1104
|
+
json: { ...(附加 !== null && 附加 !== void 0 ? 附加 : {}), _meta: 元数据, ...日志条目 },
|
|
1105
|
+
pairedItem: { item: 索引 },
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1095
1108
|
}
|
|
1109
|
+
return 输出列表;
|
|
1096
1110
|
}
|
|
1111
|
+
return [
|
|
1112
|
+
{
|
|
1113
|
+
json: { ...(附加 !== null && 附加 !== void 0 ? 附加 : {}), _meta: 元数据, logs: [] },
|
|
1114
|
+
pairedItem: { item: 索引 },
|
|
1115
|
+
},
|
|
1116
|
+
];
|
|
1097
1117
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1118
|
+
if (opt.f_outputFormat === 'splitErrors') {
|
|
1119
|
+
const 日志列表 = 数据内容 === null || 数据内容 === void 0 ? void 0 : 数据内容.logs;
|
|
1120
|
+
if (Array.isArray(日志列表) && 日志列表.length > 0) {
|
|
1121
|
+
for (const 日志条目 of 日志列表) {
|
|
1122
|
+
if (日志条目 && typeof 日志条目 === 'object') {
|
|
1123
|
+
const 日志级别 = 日志条目.level;
|
|
1124
|
+
if (日志级别 === 'error') {
|
|
1125
|
+
输出列表.push({
|
|
1126
|
+
json: { ...(附加 !== null && 附加 !== void 0 ? 附加 : {}), _meta: 元数据, ...日志条目 },
|
|
1127
|
+
pairedItem: { item: 索引 },
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1108
1131
|
}
|
|
1132
|
+
return 输出列表.length > 0
|
|
1133
|
+
? 输出列表
|
|
1134
|
+
: [
|
|
1135
|
+
{
|
|
1136
|
+
json: { ...(附加 !== null && 附加 !== void 0 ? 附加 : {}), _meta: 元数据, errors: [] },
|
|
1137
|
+
pairedItem: { item: 索引 },
|
|
1138
|
+
},
|
|
1139
|
+
];
|
|
1109
1140
|
}
|
|
1141
|
+
return [
|
|
1142
|
+
{
|
|
1143
|
+
json: { ...(附加 !== null && 附加 !== void 0 ? 附加 : {}), _meta: 元数据, errors: [] },
|
|
1144
|
+
pairedItem: { item: 索引 },
|
|
1145
|
+
},
|
|
1146
|
+
];
|
|
1110
1147
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const 处理单个项目 = async (索引, 并发闸门) => {
|
|
1117
|
-
const 操作类型 = this.getNodeParameter('operation', 索引, 'runScriptSync');
|
|
1118
|
-
const opt = 读取高级选项(索引);
|
|
1119
|
-
const 重试 = {
|
|
1120
|
-
最大重试次数: opt.j_maxRetries,
|
|
1121
|
-
初始等待毫秒: opt.k_retryInitialDelayMs,
|
|
1122
|
-
退避倍数: opt.l_retryBackoffFactor,
|
|
1123
|
-
尊重RetryAfter: opt.m_honorRetryAfter,
|
|
1148
|
+
输出列表.push({
|
|
1149
|
+
json: { ...(附加 !== null && 附加 !== void 0 ? 附加 : {}), _meta: 元数据, ...响应数据 },
|
|
1150
|
+
pairedItem: { item: 索引 },
|
|
1151
|
+
});
|
|
1152
|
+
return 输出列表;
|
|
1124
1153
|
};
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1154
|
+
return { 格式化输出 };
|
|
1155
|
+
})();
|
|
1156
|
+
const 调度模块 = (() => {
|
|
1157
|
+
const opt0 = 选项模块.读取高级选项(0);
|
|
1158
|
+
const 全局并发闸门 = 创建并发闸门(Math.max(1, opt0.x_globalMaxConcurrency));
|
|
1159
|
+
const operation0 = this.getNodeParameter('operation', 0, 'runScriptSync');
|
|
1160
|
+
let indicesToRun = [];
|
|
1161
|
+
if (operation0 === 'getTaskStatus') {
|
|
1162
|
+
indicesToRun = Array.from({ length: 输入数据项列表.length }, (_, i) => i);
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
indicesToRun = [0];
|
|
1166
|
+
if (opt0.y_dangerEachItemEnabled) {
|
|
1167
|
+
indicesToRun = Array.from({ length: 输入数据项列表.length }, (_, i) => i);
|
|
1168
|
+
if (indicesToRun.length > 1 && !opt0.z_confirmDangerEachItem) {
|
|
1169
|
+
throw new n8n_workflow_1.NodeOperationError(节点, `你开启了「危险:每个输入项执行一次」。当前输入共有 ${indicesToRun.length} 项,将会调用 AirScript ${indicesToRun.length} 次,极易造成重复写入。\n` +
|
|
1170
|
+
`如确实需要,请在高级选项中勾选「危险模式:确认多次执行」。否则请关闭危险模式(推荐)。`, { itemIndex: 0 });
|
|
1171
|
+
}
|
|
1130
1172
|
}
|
|
1131
|
-
const 状态响应 = await 获取任务状态({
|
|
1132
|
-
taskId,
|
|
1133
|
-
状态超时毫秒: opt.i_statusTimeoutSeconds * 1000,
|
|
1134
|
-
重试,
|
|
1135
|
-
调试: 调试字段,
|
|
1136
|
-
并发闸门,
|
|
1137
|
-
});
|
|
1138
|
-
校验脚本是否失败(状态响应, 索引, opt.n_failOnScriptError);
|
|
1139
|
-
const 附加 = opt.w_debug ? { _调试: 调试字段 } : undefined;
|
|
1140
|
-
return 格式化输出(状态响应, 索引, opt, 附加, 操作类型);
|
|
1141
1173
|
}
|
|
1142
|
-
|
|
1174
|
+
return { opt0, 全局并发闸门, indicesToRun };
|
|
1175
|
+
})();
|
|
1176
|
+
const 业务模块 = (() => {
|
|
1143
1177
|
const 组装批次失败响应 = (错误信息, 堆栈) => ({
|
|
1144
1178
|
status: 'finished',
|
|
1145
1179
|
error: 错误信息,
|
|
@@ -1151,167 +1185,223 @@ class KingsoftAirscript {
|
|
|
1151
1185
|
},
|
|
1152
1186
|
data: { result: null, logs: [] },
|
|
1153
1187
|
});
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1188
|
+
const 处理单个项目 = async (索引, 并发闸门) => {
|
|
1189
|
+
var _a;
|
|
1190
|
+
const 操作类型 = this.getNodeParameter('operation', 索引, 'runScriptSync');
|
|
1191
|
+
const opt = 选项模块.读取高级选项(索引);
|
|
1192
|
+
const 重试配置 = 选项模块.创建重试配置(opt);
|
|
1193
|
+
const 调试字段 = opt.w_debug ? {} : undefined;
|
|
1194
|
+
if (opt.w_debug && 调试字段) {
|
|
1195
|
+
调试字段['执行摘要'] = {
|
|
1196
|
+
operation: 操作类型,
|
|
1197
|
+
输入item数量: 输入数据项列表.length,
|
|
1198
|
+
危险每项执行: opt.y_dangerEachItemEnabled,
|
|
1199
|
+
分批启用: opt.r_autoChunkEnabled,
|
|
1200
|
+
分批字段路径: opt.s_chunkFieldPath,
|
|
1201
|
+
每批条数: opt.t_chunkSize,
|
|
1202
|
+
批次并发: opt.u_chunkMaxConcurrency,
|
|
1203
|
+
分批输出模式: opt.v_chunkOutputMode,
|
|
1204
|
+
};
|
|
1168
1205
|
}
|
|
1169
|
-
if (操作类型 === '
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
脚本ID,
|
|
1174
|
-
上下文: 上下文入参,
|
|
1175
|
-
请求超时毫秒: opt.h_requestTimeoutSeconds * 1000,
|
|
1176
|
-
重试,
|
|
1177
|
-
调试: 调试对象,
|
|
1178
|
-
并发闸门,
|
|
1179
|
-
});
|
|
1180
|
-
if (!opt.c_waitForCompletion)
|
|
1181
|
-
return 初始响应;
|
|
1182
|
-
const taskId = 提取TaskId(初始响应);
|
|
1183
|
-
if (!taskId) {
|
|
1184
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), '异步执行返回的响应中未找到 task_id(data.task_id 或 task_id)。', { itemIndex: 索引 });
|
|
1206
|
+
if (操作类型 === 'getTaskStatus') {
|
|
1207
|
+
const taskId = this.getNodeParameter('taskId', 索引, '');
|
|
1208
|
+
if (!taskId || !taskId.trim()) {
|
|
1209
|
+
throw new n8n_workflow_1.NodeOperationError(节点, 'Task ID 不能为空', { itemIndex: 索引 });
|
|
1185
1210
|
}
|
|
1186
|
-
|
|
1187
|
-
itemIndex: 索引,
|
|
1211
|
+
const 状态响应 = await 网络模块.获取任务状态({
|
|
1188
1212
|
taskId,
|
|
1189
|
-
轮询间隔毫秒: opt.d_pollIntervalSeconds * 1000,
|
|
1190
|
-
轮询抖动毫秒: opt.o_pollJitterMs,
|
|
1191
|
-
总等待超时毫秒: opt.b_timeoutSeconds * 1000,
|
|
1192
1213
|
状态超时毫秒: opt.i_statusTimeoutSeconds * 1000,
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
调试: 调试对象,
|
|
1214
|
+
重试: 重试配置,
|
|
1215
|
+
调试: 调试字段,
|
|
1196
1216
|
并发闸门,
|
|
1217
|
+
itemIndex: 索引,
|
|
1197
1218
|
});
|
|
1198
|
-
|
|
1199
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `未知的操作类型: ${操作类型}`, { itemIndex: 索引 });
|
|
1200
|
-
};
|
|
1201
|
-
if (opt.r_autoChunkEnabled) {
|
|
1202
|
-
const 数组路径 = opt.s_chunkFieldPath;
|
|
1203
|
-
const 目标数组 = 取路径值(argv参数, 数组路径);
|
|
1204
|
-
if (!Array.isArray(目标数组) || 目标数组.length === 0) {
|
|
1205
|
-
const 响应 = await 执行一次(上下文, 调试字段);
|
|
1219
|
+
脚本模块.校验脚本是否失败(状态响应, 索引, opt.n_failOnScriptError);
|
|
1206
1220
|
const 附加 = opt.w_debug ? { _调试: 调试字段 } : undefined;
|
|
1207
|
-
return
|
|
1221
|
+
return 输出模块.格式化输出(状态响应, 索引, opt, 附加, 操作类型);
|
|
1208
1222
|
}
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1223
|
+
const { 文件ID, 脚本ID } = 参数模块.解析文件脚本ID(索引);
|
|
1224
|
+
let argv参数;
|
|
1225
|
+
if (opt.y_dangerEachItemEnabled) {
|
|
1226
|
+
argv参数 = 参数模块.构建Argv_危险每项模式(索引, opt);
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
argv参数 = 参数模块.构建Argv_V3安全模式(0);
|
|
1230
|
+
}
|
|
1231
|
+
const 上下文 = 参数模块.合并Context(索引, argv参数, 调试字段);
|
|
1232
|
+
const 执行一次 = async (上下文入参, 调试对象) => {
|
|
1233
|
+
if (操作类型 === 'runScriptSync') {
|
|
1234
|
+
const 响应 = await 网络模块.调用脚本接口({
|
|
1235
|
+
类型: 'sync',
|
|
1236
|
+
文件ID,
|
|
1237
|
+
脚本ID,
|
|
1238
|
+
上下文: 上下文入参,
|
|
1239
|
+
请求超时毫秒: opt.h_requestTimeoutSeconds * 1000,
|
|
1240
|
+
重试: 重试配置,
|
|
1241
|
+
调试: 调试对象,
|
|
1242
|
+
并发闸门,
|
|
1243
|
+
});
|
|
1244
|
+
脚本模块.校验脚本是否失败(响应, 索引, opt.n_failOnScriptError);
|
|
1245
|
+
return 响应;
|
|
1246
|
+
}
|
|
1247
|
+
if (操作类型 === 'runScriptAsync') {
|
|
1248
|
+
const 初始响应 = await 网络模块.调用脚本接口({
|
|
1249
|
+
类型: 'async',
|
|
1250
|
+
文件ID,
|
|
1251
|
+
脚本ID,
|
|
1252
|
+
上下文: 上下文入参,
|
|
1253
|
+
请求超时毫秒: opt.h_requestTimeoutSeconds * 1000,
|
|
1254
|
+
重试: 重试配置,
|
|
1255
|
+
调试: 调试对象,
|
|
1256
|
+
并发闸门,
|
|
1257
|
+
});
|
|
1258
|
+
if (!opt.c_waitForCompletion)
|
|
1259
|
+
return 初始响应;
|
|
1260
|
+
const taskId = 脚本模块.提取TaskId(初始响应);
|
|
1261
|
+
if (!taskId) {
|
|
1262
|
+
throw new n8n_workflow_1.NodeOperationError(节点, '异步执行返回的响应中未找到 task_id(data.task_id 或 task_id)。', { itemIndex: 索引 });
|
|
1227
1263
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1264
|
+
return await 网络模块.等待异步任务完成({
|
|
1265
|
+
itemIndex: 索引,
|
|
1266
|
+
taskId,
|
|
1267
|
+
轮询间隔毫秒: opt.d_pollIntervalSeconds * 1000,
|
|
1268
|
+
轮询抖动毫秒: opt.o_pollJitterMs,
|
|
1269
|
+
总等待超时毫秒: opt.b_timeoutSeconds * 1000,
|
|
1270
|
+
状态超时毫秒: opt.i_statusTimeoutSeconds * 1000,
|
|
1271
|
+
重试: 重试配置,
|
|
1272
|
+
finished但error算失败: opt.n_failOnScriptError,
|
|
1273
|
+
调试: 调试对象,
|
|
1274
|
+
并发闸门,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
throw new n8n_workflow_1.NodeOperationError(节点, `未知的操作类型: ${操作类型}`, { itemIndex: 索引 });
|
|
1278
|
+
};
|
|
1279
|
+
if (opt.r_autoChunkEnabled) {
|
|
1280
|
+
let 数组路径 = opt.s_chunkFieldPath || 'data';
|
|
1281
|
+
let 目标数组 = 工具模块.取路径值(argv参数, 数组路径);
|
|
1282
|
+
const 执行模式 = this.getNodeParameter('runModeV3', 0, 'uiOnce');
|
|
1283
|
+
if ((!Array.isArray(目标数组) || 目标数组.length === 0) && 执行模式 === 'aggregateOnce') {
|
|
1284
|
+
const 聚合配置 = this.getNodeParameter('aggregate', 0, {});
|
|
1285
|
+
const 聚合字段路径 = String((_a = 聚合配置.targetArrayField) !== null && _a !== void 0 ? _a : 'data');
|
|
1286
|
+
if (聚合字段路径 && 聚合字段路径 !== 数组路径) {
|
|
1287
|
+
const 备选数组 = 工具模块.取路径值(argv参数, 聚合字段路径);
|
|
1288
|
+
if (Array.isArray(备选数组) && 备选数组.length > 0) {
|
|
1289
|
+
const 原数组路径 = 数组路径;
|
|
1290
|
+
数组路径 = 聚合字段路径;
|
|
1291
|
+
目标数组 = 备选数组;
|
|
1292
|
+
if (opt.w_debug && 调试字段) {
|
|
1293
|
+
调试字段['分批字段路径自动切换'] = {
|
|
1294
|
+
从: 原数组路径,
|
|
1295
|
+
到: 数组路径,
|
|
1296
|
+
原因: '原分批字段未找到数组,已回退到聚合字段',
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1237
1300
|
}
|
|
1238
|
-
}
|
|
1301
|
+
}
|
|
1302
|
+
if (!Array.isArray(目标数组) || 目标数组.length === 0) {
|
|
1303
|
+
const 响应 = await 执行一次(上下文, 调试字段);
|
|
1304
|
+
const 附加 = opt.w_debug ? { _调试: 调试字段 } : undefined;
|
|
1305
|
+
return 输出模块.格式化输出(响应, 索引, opt, 附加, 操作类型);
|
|
1306
|
+
}
|
|
1307
|
+
const 批次大小 = Math.max(1, opt.t_chunkSize);
|
|
1308
|
+
const 批次并发 = Math.max(1, opt.u_chunkMaxConcurrency);
|
|
1309
|
+
const 任务列表 = [];
|
|
1310
|
+
for (let i = 0; i < 目标数组.length; i += 批次大小) {
|
|
1311
|
+
const 批次 = 目标数组.slice(i, i + 批次大小);
|
|
1312
|
+
const 批次索引 = Math.floor(i / 批次大小);
|
|
1313
|
+
任务列表.push(async () => {
|
|
1314
|
+
try {
|
|
1315
|
+
const 批次调试对象 = opt.w_debug ? {} : undefined;
|
|
1316
|
+
const 批次argv = 工具模块.以不可变方式写入路径(argv参数, 数组路径, 批次);
|
|
1317
|
+
const 批次上下文 = { ...上下文, argv: 批次argv };
|
|
1318
|
+
const 响应 = await 执行一次(批次上下文, 批次调试对象);
|
|
1319
|
+
const 批次附加 = opt.w_debug
|
|
1320
|
+
? { 批次索引, _调试: { ...(批次调试对象 !== null && 批次调试对象 !== void 0 ? 批次调试对象 : {}) } }
|
|
1321
|
+
: { 批次索引 };
|
|
1322
|
+
return 输出模块.格式化输出(响应, 索引, opt, 批次附加, 操作类型);
|
|
1323
|
+
}
|
|
1324
|
+
catch (错误) {
|
|
1325
|
+
if (!this.continueOnFail())
|
|
1326
|
+
throw 错误;
|
|
1327
|
+
const err = 错误 instanceof Error ? 错误 : new Error(String(错误));
|
|
1328
|
+
const 错误对象 = 组装批次失败响应(err.message, err.stack);
|
|
1329
|
+
const 错误附加 = opt.w_debug
|
|
1330
|
+
? { 批次索引, _调试: { 错误: err.message } }
|
|
1331
|
+
: { 批次索引, 错误: err.message };
|
|
1332
|
+
return 输出模块.格式化输出(错误对象, 索引, opt, 错误附加, 操作类型);
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
const 批次结果 = await 工具模块.并发执行(任务列表, 批次并发);
|
|
1337
|
+
if (opt.v_chunkOutputMode === 'merge') {
|
|
1338
|
+
const 批次输出 = 批次结果.flat();
|
|
1339
|
+
const 批次输出列表 = 批次输出.map((x) => x.json);
|
|
1340
|
+
const taskIds = 批次输出
|
|
1341
|
+
.map((x) => x.json)
|
|
1342
|
+
.filter((json) => typeof json === 'object' && json !== null)
|
|
1343
|
+
.map((json) => {
|
|
1344
|
+
var _a, _b, _c;
|
|
1345
|
+
const fromData = (_a = json.data) === null || _a === void 0 ? void 0 : _a.task_id;
|
|
1346
|
+
const fromTop = json.task_id;
|
|
1347
|
+
const fromMeta = (_b = json._meta) === null || _b === void 0 ? void 0 : _b.task_id;
|
|
1348
|
+
return (_c = fromData !== null && fromData !== void 0 ? fromData : fromTop) !== null && _c !== void 0 ? _c : fromMeta;
|
|
1349
|
+
})
|
|
1350
|
+
.filter((id) => typeof id === 'string' && id.length > 0);
|
|
1351
|
+
const 批次数 = Math.ceil(目标数组.length / 批次大小);
|
|
1352
|
+
const 分批汇总 = {
|
|
1353
|
+
字段路径: 数组路径,
|
|
1354
|
+
总数据条数: 目标数组.length,
|
|
1355
|
+
每批条数: 批次大小,
|
|
1356
|
+
批次数,
|
|
1357
|
+
并发数: 批次并发,
|
|
1358
|
+
};
|
|
1359
|
+
const 合并结果 = {
|
|
1360
|
+
分批汇总,
|
|
1361
|
+
批次输出列表,
|
|
1362
|
+
...(taskIds.length === 1 ? { task_id: taskIds[0] } : taskIds.length > 1 ? { task_ids: taskIds } : {}),
|
|
1363
|
+
...(opt.w_debug ? { _调试: 调试字段 } : {}),
|
|
1364
|
+
};
|
|
1365
|
+
return [{ json: 合并结果, pairedItem: { item: 索引 } }];
|
|
1366
|
+
}
|
|
1367
|
+
return 批次结果.flat();
|
|
1239
1368
|
}
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
var _a, _b, _c;
|
|
1249
|
-
const fromData = (_a = json.data) === null || _a === void 0 ? void 0 : _a.task_id;
|
|
1250
|
-
const fromTop = json.task_id;
|
|
1251
|
-
const fromMeta = (_b = json._meta) === null || _b === void 0 ? void 0 : _b.task_id;
|
|
1252
|
-
return (_c = fromData !== null && fromData !== void 0 ? fromData : fromTop) !== null && _c !== void 0 ? _c : fromMeta;
|
|
1253
|
-
})
|
|
1254
|
-
.filter((id) => typeof id === 'string' && id.length > 0);
|
|
1255
|
-
const 批次数 = Math.ceil(目标数组.length / 批次大小);
|
|
1256
|
-
const 分批汇总 = {
|
|
1257
|
-
批次数,
|
|
1258
|
-
每批条数: Array.from({ length: 批次数 }, (_, i) => {
|
|
1259
|
-
const start = i * 批次大小;
|
|
1260
|
-
const end = Math.min(start + 批次大小, 目标数组.length);
|
|
1261
|
-
return end - start;
|
|
1262
|
-
}),
|
|
1263
|
-
字段路径: 数组路径,
|
|
1264
|
-
总数据条数: 目标数组.length,
|
|
1265
|
-
并发数,
|
|
1266
|
-
};
|
|
1267
|
-
const 合并结果 = {
|
|
1268
|
-
分批汇总,
|
|
1269
|
-
批次输出列表,
|
|
1270
|
-
...(taskIds.length === 1 ? { task_id: taskIds[0] } : taskIds.length > 1 ? { task_ids: taskIds } : {}),
|
|
1271
|
-
...(opt.w_debug ? { _调试: 调试字段 } : {}),
|
|
1272
|
-
};
|
|
1273
|
-
return [{ json: 合并结果, pairedItem: { item: 索引 } }];
|
|
1369
|
+
const 响应 = await 执行一次(上下文, 调试字段);
|
|
1370
|
+
const 附加 = opt.w_debug ? { _调试: 调试字段 } : undefined;
|
|
1371
|
+
return 输出模块.格式化输出(响应, 索引, opt, 附加, 操作类型);
|
|
1372
|
+
};
|
|
1373
|
+
const 安全执行单个项目 = async (itemIndex, 并发闸门) => {
|
|
1374
|
+
var _a, _b, _c;
|
|
1375
|
+
try {
|
|
1376
|
+
return await 处理单个项目(itemIndex, 并发闸门);
|
|
1274
1377
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
throw e;
|
|
1289
|
-
const err = 提取Http错误(e);
|
|
1290
|
-
return [
|
|
1291
|
-
{
|
|
1292
|
-
json: {
|
|
1293
|
-
error: 提取错误消息(err),
|
|
1294
|
-
code: (_a = err.code) !== null && _a !== void 0 ? _a : null,
|
|
1295
|
-
statusCode: (_b = getStatusCode(err)) !== null && _b !== void 0 ? _b : null,
|
|
1296
|
-
details: (_c = getBody(err)) !== null && _c !== void 0 ? _c : null,
|
|
1378
|
+
catch (e) {
|
|
1379
|
+
if (!this.continueOnFail())
|
|
1380
|
+
throw e;
|
|
1381
|
+
const 实际错误 = 错误模块.提取Http错误(e);
|
|
1382
|
+
return [
|
|
1383
|
+
{
|
|
1384
|
+
json: {
|
|
1385
|
+
error: 错误模块.提取错误消息(实际错误),
|
|
1386
|
+
code: (_a = 实际错误.code) !== null && _a !== void 0 ? _a : null,
|
|
1387
|
+
statusCode: (_b = 错误模块.获取状态码(实际错误)) !== null && _b !== void 0 ? _b : null,
|
|
1388
|
+
details: (_c = 错误模块.获取响应体(实际错误)) !== null && _c !== void 0 ? _c : null,
|
|
1389
|
+
},
|
|
1390
|
+
pairedItem: { item: itemIndex },
|
|
1297
1391
|
},
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1302
|
-
};
|
|
1303
|
-
const operation0 = this.getNodeParameter('operation', 0, 'runScriptSync');
|
|
1304
|
-
const payloadMode0 = this.getNodeParameter('payloadMode', 0, 'argvFromUI');
|
|
1305
|
-
const appendOnce = (operation0 === 'runScriptSync' || operation0 === 'runScriptAsync')
|
|
1306
|
-
&& payloadMode0 === 'appendItemsToArrayField';
|
|
1307
|
-
const indicesToRun = appendOnce ? [0] : Array.from({ length: 输入数据项列表.length }, (_, i) => i);
|
|
1392
|
+
];
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
return { 安全执行单个项目 };
|
|
1396
|
+
})();
|
|
1308
1397
|
const 汇总输出 = [];
|
|
1398
|
+
const { opt0, 全局并发闸门, indicesToRun } = 调度模块;
|
|
1309
1399
|
if (opt0.a_parallelExecution) {
|
|
1310
1400
|
const 批处理数量 = Math.max(1, opt0.e_batchSize);
|
|
1311
1401
|
for (let i = 0; i < indicesToRun.length; i += 批处理数量) {
|
|
1312
1402
|
const slice = indicesToRun.slice(i, i + 批处理数量);
|
|
1313
|
-
const 任务函数列表 = slice.map((idx) => () =>
|
|
1314
|
-
const 批次结果 = await
|
|
1403
|
+
const 任务函数列表 = slice.map((idx) => () => 业务模块.安全执行单个项目(idx, 全局并发闸门));
|
|
1404
|
+
const 批次结果 = await 工具模块.并发执行(任务函数列表, slice.length);
|
|
1315
1405
|
for (const 单项结果 of 批次结果) {
|
|
1316
1406
|
汇总输出.push(...单项结果);
|
|
1317
1407
|
}
|
|
@@ -1319,7 +1409,7 @@ class KingsoftAirscript {
|
|
|
1319
1409
|
}
|
|
1320
1410
|
else {
|
|
1321
1411
|
for (const idx of indicesToRun) {
|
|
1322
|
-
const 单项结果 = await
|
|
1412
|
+
const 单项结果 = await 业务模块.安全执行单个项目(idx, 全局并发闸门);
|
|
1323
1413
|
汇总输出.push(...单项结果);
|
|
1324
1414
|
}
|
|
1325
1415
|
}
|