persona-core 1.0.0
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/dist/db/client.d.ts +55 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +157 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/index.d.ts +7 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +7 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/repositories/executionRepository.d.ts +15 -0
- package/dist/db/repositories/executionRepository.d.ts.map +1 -0
- package/dist/db/repositories/executionRepository.js +41 -0
- package/dist/db/repositories/executionRepository.js.map +1 -0
- package/dist/db/repositories/index.d.ts +10 -0
- package/dist/db/repositories/index.d.ts.map +1 -0
- package/dist/db/repositories/index.js +10 -0
- package/dist/db/repositories/index.js.map +1 -0
- package/dist/db/repositories/nodeRepository.d.ts +15 -0
- package/dist/db/repositories/nodeRepository.d.ts.map +1 -0
- package/dist/db/repositories/nodeRepository.js +61 -0
- package/dist/db/repositories/nodeRepository.js.map +1 -0
- package/dist/db/repositories/personaRepository.d.ts +13 -0
- package/dist/db/repositories/personaRepository.d.ts.map +1 -0
- package/dist/db/repositories/personaRepository.js +42 -0
- package/dist/db/repositories/personaRepository.js.map +1 -0
- package/dist/db/repositories/planResultRepository.d.ts +13 -0
- package/dist/db/repositories/planResultRepository.d.ts.map +1 -0
- package/dist/db/repositories/planResultRepository.js +30 -0
- package/dist/db/repositories/planResultRepository.js.map +1 -0
- package/dist/db/repositories/scheduleRepository.d.ts +27 -0
- package/dist/db/repositories/scheduleRepository.d.ts.map +1 -0
- package/dist/db/repositories/scheduleRepository.js +187 -0
- package/dist/db/repositories/scheduleRepository.js.map +1 -0
- package/dist/db/repositories/types.d.ts +132 -0
- package/dist/db/repositories/types.d.ts.map +1 -0
- package/dist/db/repositories/types.js +5 -0
- package/dist/db/repositories/types.js.map +1 -0
- package/dist/db/schema.d.ts +742 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +85 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/capabilities.d.ts +56 -0
- package/dist/llm/capabilities.d.ts.map +1 -0
- package/dist/llm/capabilities.js +305 -0
- package/dist/llm/capabilities.js.map +1 -0
- package/dist/llm/index.d.ts +7 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +10 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/interfaces.d.ts +249 -0
- package/dist/llm/interfaces.d.ts.map +1 -0
- package/dist/llm/interfaces.js +5 -0
- package/dist/llm/interfaces.js.map +1 -0
- package/dist/llm/providers/anthropic-compatible.d.ts +48 -0
- package/dist/llm/providers/anthropic-compatible.d.ts.map +1 -0
- package/dist/llm/providers/anthropic-compatible.js +163 -0
- package/dist/llm/providers/anthropic-compatible.js.map +1 -0
- package/dist/llm/providers/index.d.ts +14 -0
- package/dist/llm/providers/index.d.ts.map +1 -0
- package/dist/llm/providers/index.js +12 -0
- package/dist/llm/providers/index.js.map +1 -0
- package/dist/llm/providers/openai-compatible.d.ts +59 -0
- package/dist/llm/providers/openai-compatible.d.ts.map +1 -0
- package/dist/llm/providers/openai-compatible.js +207 -0
- package/dist/llm/providers/openai-compatible.js.map +1 -0
- package/dist/services/actionService.d.ts +132 -0
- package/dist/services/actionService.d.ts.map +1 -0
- package/dist/services/actionService.js +971 -0
- package/dist/services/actionService.js.map +1 -0
- package/dist/services/branchService.d.ts +19 -0
- package/dist/services/branchService.d.ts.map +1 -0
- package/dist/services/branchService.js +50 -0
- package/dist/services/branchService.js.map +1 -0
- package/dist/services/expressionEvaluator.d.ts +16 -0
- package/dist/services/expressionEvaluator.d.ts.map +1 -0
- package/dist/services/expressionEvaluator.js +70 -0
- package/dist/services/expressionEvaluator.js.map +1 -0
- package/dist/services/factory.d.ts +43 -0
- package/dist/services/factory.d.ts.map +1 -0
- package/dist/services/factory.js +30 -0
- package/dist/services/factory.js.map +1 -0
- package/dist/services/index.d.ts +15 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +17 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/interfaces.d.ts +117 -0
- package/dist/services/interfaces.d.ts.map +1 -0
- package/dist/services/interfaces.js +5 -0
- package/dist/services/interfaces.js.map +1 -0
- package/dist/services/loaders/fileLoader.d.ts +56 -0
- package/dist/services/loaders/fileLoader.d.ts.map +1 -0
- package/dist/services/loaders/fileLoader.js +161 -0
- package/dist/services/loaders/fileLoader.js.map +1 -0
- package/dist/services/loaders/index.d.ts +6 -0
- package/dist/services/loaders/index.d.ts.map +1 -0
- package/dist/services/loaders/index.js +6 -0
- package/dist/services/loaders/index.js.map +1 -0
- package/dist/services/loaders/transformers.d.ts +32 -0
- package/dist/services/loaders/transformers.d.ts.map +1 -0
- package/dist/services/loaders/transformers.js +78 -0
- package/dist/services/loaders/transformers.js.map +1 -0
- package/dist/services/mockAction.d.ts +65 -0
- package/dist/services/mockAction.d.ts.map +1 -0
- package/dist/services/mockAction.js +153 -0
- package/dist/services/mockAction.js.map +1 -0
- package/dist/services/mockBranch.d.ts +50 -0
- package/dist/services/mockBranch.d.ts.map +1 -0
- package/dist/services/mockBranch.js +75 -0
- package/dist/services/mockBranch.js.map +1 -0
- package/dist/services/mockThinking.d.ts +68 -0
- package/dist/services/mockThinking.d.ts.map +1 -0
- package/dist/services/mockThinking.js +89 -0
- package/dist/services/mockThinking.js.map +1 -0
- package/dist/services/thinkingService.d.ts +15 -0
- package/dist/services/thinkingService.d.ts.map +1 -0
- package/dist/services/thinkingService.js +117 -0
- package/dist/services/thinkingService.js.map +1 -0
- package/dist/temporal/activities/actionActivities.d.ts +15 -0
- package/dist/temporal/activities/actionActivities.d.ts.map +1 -0
- package/dist/temporal/activities/actionActivities.js +140 -0
- package/dist/temporal/activities/actionActivities.js.map +1 -0
- package/dist/temporal/activities/branchActivities.d.ts +13 -0
- package/dist/temporal/activities/branchActivities.d.ts.map +1 -0
- package/dist/temporal/activities/branchActivities.js +26 -0
- package/dist/temporal/activities/branchActivities.js.map +1 -0
- package/dist/temporal/activities/dbActivities.d.ts +14 -0
- package/dist/temporal/activities/dbActivities.d.ts.map +1 -0
- package/dist/temporal/activities/dbActivities.js +84 -0
- package/dist/temporal/activities/dbActivities.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +9 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +9 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/thinkingActivities.d.ts +17 -0
- package/dist/temporal/activities/thinkingActivities.d.ts.map +1 -0
- package/dist/temporal/activities/thinkingActivities.js +145 -0
- package/dist/temporal/activities/thinkingActivities.js.map +1 -0
- package/dist/temporal/activities/types.d.ts +100 -0
- package/dist/temporal/activities/types.d.ts.map +1 -0
- package/dist/temporal/activities/types.js +5 -0
- package/dist/temporal/activities/types.js.map +1 -0
- package/dist/temporal/client.d.ts +43 -0
- package/dist/temporal/client.d.ts.map +1 -0
- package/dist/temporal/client.js +75 -0
- package/dist/temporal/client.js.map +1 -0
- package/dist/temporal/index.d.ts +10 -0
- package/dist/temporal/index.d.ts.map +1 -0
- package/dist/temporal/index.js +12 -0
- package/dist/temporal/index.js.map +1 -0
- package/dist/temporal/personaCoreClient.d.ts +199 -0
- package/dist/temporal/personaCoreClient.d.ts.map +1 -0
- package/dist/temporal/personaCoreClient.js +233 -0
- package/dist/temporal/personaCoreClient.js.map +1 -0
- package/dist/temporal/personaWorker.d.ts +141 -0
- package/dist/temporal/personaWorker.d.ts.map +1 -0
- package/dist/temporal/personaWorker.js +93 -0
- package/dist/temporal/personaWorker.js.map +1 -0
- package/dist/temporal/worker.d.ts +66 -0
- package/dist/temporal/worker.d.ts.map +1 -0
- package/dist/temporal/worker.js +109 -0
- package/dist/temporal/worker.js.map +1 -0
- package/dist/temporal/workflows/index.d.ts +5 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +5 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/scheduleWorkflow.d.ts +31 -0
- package/dist/temporal/workflows/scheduleWorkflow.d.ts.map +1 -0
- package/dist/temporal/workflows/scheduleWorkflow.js +256 -0
- package/dist/temporal/workflows/scheduleWorkflow.js.map +1 -0
- package/dist/types/common.d.ts +81 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +5 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/nodes.d.ts +496 -0
- package/dist/types/nodes.d.ts.map +1 -0
- package/dist/types/nodes.js +5 -0
- package/dist/types/nodes.js.map +1 -0
- package/dist/types/persona.d.ts +59 -0
- package/dist/types/persona.d.ts.map +1 -0
- package/dist/types/persona.js +36 -0
- package/dist/types/persona.js.map +1 -0
- package/dist/types/schedule.d.ts +143 -0
- package/dist/types/schedule.d.ts.map +1 -0
- package/dist/types/schedule.js +155 -0
- package/dist/types/schedule.js.map +1 -0
- package/dist/utils/dateUtils.d.ts +31 -0
- package/dist/utils/dateUtils.d.ts.map +1 -0
- package/dist/utils/dateUtils.js +53 -0
- package/dist/utils/dateUtils.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/inputResolver.d.ts +43 -0
- package/dist/utils/inputResolver.d.ts.map +1 -0
- package/dist/utils/inputResolver.js +137 -0
- package/dist/utils/inputResolver.js.map +1 -0
- package/dist/utils/sharedDataUtils.d.ts +36 -0
- package/dist/utils/sharedDataUtils.d.ts.map +1 -0
- package/dist/utils/sharedDataUtils.js +84 -0
- package/dist/utils/sharedDataUtils.js.map +1 -0
- package/dist/utils/typeGuards.d.ts +33 -0
- package/dist/utils/typeGuards.d.ts.map +1 -0
- package/dist/utils/typeGuards.js +50 -0
- package/dist/utils/typeGuards.js.map +1 -0
- package/docs/add-llm-provider.md +353 -0
- package/docs/external/deepseek-v32.md +28 -0
- package/docs/quick-start.md +849 -0
- package/docs/suite-guide.md +148 -0
- package/docs/usage-guide.md +1487 -0
- package/package.json +80 -0
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一的 ActionService 实现
|
|
3
|
+
* 通过注入 ContentGenerationCapability 支持不同的 LLM 提供者
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs/promises';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import nunjucks from 'nunjucks';
|
|
8
|
+
import { FileLoader } from './loaders/index.js';
|
|
9
|
+
export class DefaultActionService {
|
|
10
|
+
contentGeneration;
|
|
11
|
+
defaultOptions;
|
|
12
|
+
asyncTaskContexts = new Map();
|
|
13
|
+
fileConfig;
|
|
14
|
+
fileLoader;
|
|
15
|
+
nunjucksEnv;
|
|
16
|
+
constructor(
|
|
17
|
+
/** LLM 内容生成能力(用于 llm_call 类型) */
|
|
18
|
+
contentGeneration, defaultOptions,
|
|
19
|
+
/** 文件操作配置 */
|
|
20
|
+
fileConfig) {
|
|
21
|
+
this.contentGeneration = contentGeneration;
|
|
22
|
+
this.defaultOptions = defaultOptions;
|
|
23
|
+
this.fileConfig = {
|
|
24
|
+
baseDir: fileConfig?.baseDir ?? process.cwd(),
|
|
25
|
+
allowedExtensions: fileConfig?.allowedExtensions ?? [],
|
|
26
|
+
maxFileSize: fileConfig?.maxFileSize ?? 10 * 1024 * 1024, // 10MB
|
|
27
|
+
};
|
|
28
|
+
this.fileLoader = new FileLoader(this.fileConfig.baseDir, this.fileConfig.maxFileSize);
|
|
29
|
+
// 初始化 Nunjucks 环境
|
|
30
|
+
this.nunjucksEnv = new nunjucks.Environment(null, {
|
|
31
|
+
autoescape: false, // JSON 模板不需要 HTML 转义
|
|
32
|
+
});
|
|
33
|
+
this.registerNunjucksFilters();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 注册 Nunjucks 内置过滤器
|
|
37
|
+
* 替换原有的 Loader 和 Transformer 系统
|
|
38
|
+
*/
|
|
39
|
+
registerNunjucksFilters() {
|
|
40
|
+
const self = this;
|
|
41
|
+
// 异步过滤器:读取文件或下载远程图片
|
|
42
|
+
// 用法: {{ imagePath | file | base64 | dataUri }}
|
|
43
|
+
// 支持:
|
|
44
|
+
// - 本地文件路径:读取文件内容
|
|
45
|
+
// - 远程 URL (http/https):下载图片内容(解决防盗链问题)
|
|
46
|
+
// - data URI:直接返回
|
|
47
|
+
this.nunjucksEnv.addFilter('file', function (filePath, callback) {
|
|
48
|
+
// 检测常见错误:传入对象而不是字符串
|
|
49
|
+
if (filePath && typeof filePath === 'object') {
|
|
50
|
+
const obj = filePath;
|
|
51
|
+
const hasImageUrl = 'imageUrl' in obj || 'image_url' in obj || 'url' in obj;
|
|
52
|
+
if (hasImageUrl) {
|
|
53
|
+
callback(new Error(`file filter received an object instead of a string. ` +
|
|
54
|
+
`This usually happens when image_search results are passed directly. ` +
|
|
55
|
+
`Solution: Use path property in input definition, e.g., ` +
|
|
56
|
+
`{ type: 'reference', fieldName: 'images', path: '[*].imageUrl' }`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
callback(new Error(`file filter requires a string, got object: ${JSON.stringify(filePath).slice(0, 100)}`));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
63
|
+
callback(null, filePath);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// 如果已经是 data URI,直接返回
|
|
67
|
+
if (filePath.startsWith('data:')) {
|
|
68
|
+
callback(null, filePath);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// 如果是远程 URL,下载图片(解决防盗链问题)
|
|
72
|
+
if (filePath.startsWith('http://') || filePath.startsWith('https://')) {
|
|
73
|
+
self.downloadRemoteImage(filePath)
|
|
74
|
+
.then(result => {
|
|
75
|
+
if (result === null) {
|
|
76
|
+
// 下载失败,返回空字符串标记,模板中会过滤掉
|
|
77
|
+
console.warn(`[ActionService] Skipping inaccessible image: ${filePath}`);
|
|
78
|
+
callback(null, ''); // 空字符串会被模板过滤掉
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
callback(null, result);
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.catch(err => callback(err));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// 本地文件
|
|
88
|
+
self.loadFileForTemplate(filePath)
|
|
89
|
+
.then(result => callback(null, result))
|
|
90
|
+
.catch(err => callback(err));
|
|
91
|
+
}, true); // true = 异步过滤器
|
|
92
|
+
// 同步过滤器:base64 编码
|
|
93
|
+
this.nunjucksEnv.addFilter('base64', function (input) {
|
|
94
|
+
// 如果是 file 过滤器的输出对象 { buffer, mimeType }
|
|
95
|
+
if (input && typeof input === 'object' && 'buffer' in input) {
|
|
96
|
+
const obj = input;
|
|
97
|
+
return {
|
|
98
|
+
base64: obj.buffer.toString('base64'),
|
|
99
|
+
mimeType: obj.mimeType,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// 如果是 Buffer
|
|
103
|
+
if (Buffer.isBuffer(input)) {
|
|
104
|
+
return input.toString('base64');
|
|
105
|
+
}
|
|
106
|
+
// 如果是字符串(URL 或已处理的值),直接返回
|
|
107
|
+
if (typeof input === 'string') {
|
|
108
|
+
return input;
|
|
109
|
+
}
|
|
110
|
+
return input;
|
|
111
|
+
});
|
|
112
|
+
// 同步过滤器:生成 data URI
|
|
113
|
+
this.nunjucksEnv.addFilter('dataUri', function (input) {
|
|
114
|
+
// 如果是 base64 过滤器的输出对象 { base64, mimeType }
|
|
115
|
+
if (input && typeof input === 'object' && 'base64' in input) {
|
|
116
|
+
const obj = input;
|
|
117
|
+
return `data:${obj.mimeType};base64,${obj.base64}`;
|
|
118
|
+
}
|
|
119
|
+
// 如果是字符串(URL),直接返回
|
|
120
|
+
if (typeof input === 'string') {
|
|
121
|
+
return input;
|
|
122
|
+
}
|
|
123
|
+
return input;
|
|
124
|
+
});
|
|
125
|
+
// 异步过滤器:批量下载图片并转换为 data URI
|
|
126
|
+
// 用法: {{ referenceImages | fetchImages }}
|
|
127
|
+
// 输入: URL 字符串数组
|
|
128
|
+
// 输出: data URI 字符串数组(过滤掉无法下载的图片,最多 3 张)
|
|
129
|
+
const MAX_REFERENCE_IMAGES = 3;
|
|
130
|
+
this.nunjucksEnv.addFilter('fetchImages', function (urls, callback) {
|
|
131
|
+
if (!Array.isArray(urls)) {
|
|
132
|
+
callback(null, []);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// 限制最多处理 MAX_REFERENCE_IMAGES 张图片,避免超时
|
|
136
|
+
const limitedUrls = urls.slice(0, MAX_REFERENCE_IMAGES);
|
|
137
|
+
if (urls.length > MAX_REFERENCE_IMAGES) {
|
|
138
|
+
console.log(`[ActionService] fetchImages: limiting to ${MAX_REFERENCE_IMAGES} images (received ${urls.length})`);
|
|
139
|
+
}
|
|
140
|
+
const downloadPromises = limitedUrls.map(async (url) => {
|
|
141
|
+
if (typeof url !== 'string' || !url) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
// data URI 直接返回
|
|
145
|
+
if (url.startsWith('data:')) {
|
|
146
|
+
return url;
|
|
147
|
+
}
|
|
148
|
+
// 远程 URL 尝试下载
|
|
149
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
150
|
+
const result = await self.downloadRemoteImage(url);
|
|
151
|
+
if (result) {
|
|
152
|
+
const base64 = result.buffer.toString('base64');
|
|
153
|
+
return `data:${result.mimeType};base64,${base64}`;
|
|
154
|
+
}
|
|
155
|
+
return null; // 下载失败
|
|
156
|
+
}
|
|
157
|
+
// 本地文件
|
|
158
|
+
try {
|
|
159
|
+
const result = await self.loadFileForTemplate(url);
|
|
160
|
+
const base64 = result.buffer.toString('base64');
|
|
161
|
+
return `data:${result.mimeType};base64,${base64}`;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
console.warn(`[ActionService] Failed to load local file: ${url}`);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
Promise.all(downloadPromises)
|
|
169
|
+
.then(results => {
|
|
170
|
+
// 过滤掉 null(下载失败的图片)
|
|
171
|
+
const validUrls = results.filter((r) => r !== null);
|
|
172
|
+
console.log(`[ActionService] fetchImages: ${validUrls.length}/${limitedUrls.length} images successfully loaded`);
|
|
173
|
+
callback(null, validUrls);
|
|
174
|
+
})
|
|
175
|
+
.catch(err => callback(err));
|
|
176
|
+
}, true); // true = 异步过滤器
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 加载文件(供 file 过滤器使用)
|
|
180
|
+
*/
|
|
181
|
+
async loadFileForTemplate(filePath) {
|
|
182
|
+
const result = await this.fileLoader.load(filePath, { binary: true });
|
|
183
|
+
return {
|
|
184
|
+
buffer: Buffer.isBuffer(result.value) ? result.value : Buffer.from(result.value),
|
|
185
|
+
mimeType: result.mimeType,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 下载远程图片(解决防盗链问题)
|
|
190
|
+
* 通过本地代理下载,绕过 referer 检查
|
|
191
|
+
* 如果下载失败,返回 null 而不是抛出错误
|
|
192
|
+
*/
|
|
193
|
+
async downloadRemoteImage(url) {
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(url, {
|
|
196
|
+
headers: {
|
|
197
|
+
// 模拟浏览器请求,绕过简单的防盗链检查
|
|
198
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
199
|
+
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
|
200
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
201
|
+
'Referer': new URL(url).origin + '/',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
console.warn(`[ActionService] Failed to download image (${response.status}): ${url}`);
|
|
206
|
+
return null; // 返回 null,让调用方决定如何处理
|
|
207
|
+
}
|
|
208
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
209
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
210
|
+
// 从 Content-Type 获取 MIME 类型,或根据 URL 猜测
|
|
211
|
+
let mimeType = response.headers.get('content-type') || 'image/jpeg';
|
|
212
|
+
// 移除 charset 等额外信息
|
|
213
|
+
if (mimeType.includes(';')) {
|
|
214
|
+
mimeType = mimeType.split(';')[0].trim();
|
|
215
|
+
}
|
|
216
|
+
return { buffer, mimeType };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
console.warn(`[ActionService] Failed to download remote image: ${message}`);
|
|
221
|
+
return null; // 网络错误时也返回 null
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 使用 Nunjucks 渲染模板
|
|
226
|
+
*/
|
|
227
|
+
async renderTemplate(template, inputs) {
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
this.nunjucksEnv.renderString(template, inputs, (err, result) => {
|
|
230
|
+
if (err) {
|
|
231
|
+
reject(new Error(`Template rendering error: ${err.message}`));
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
resolve(result || '');
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async executeSync(actionType, context) {
|
|
240
|
+
switch (actionType.executionType) {
|
|
241
|
+
case 'llm_call':
|
|
242
|
+
return this.executeLLMCall(actionType, context);
|
|
243
|
+
case 'api_call':
|
|
244
|
+
return this.executeApiCall(actionType, context);
|
|
245
|
+
case 'http_request':
|
|
246
|
+
return this.executeHttpRequest(actionType, context);
|
|
247
|
+
case 'file_read':
|
|
248
|
+
case 'file_write':
|
|
249
|
+
return this.executeFileOperation(actionType, context);
|
|
250
|
+
case 'database_query':
|
|
251
|
+
return this.executeDatabaseQuery(actionType, context);
|
|
252
|
+
case 'shell_command':
|
|
253
|
+
return this.executeShellCommand(actionType, context);
|
|
254
|
+
default:
|
|
255
|
+
// 回退到 LLM 生成
|
|
256
|
+
return this.executeLLMCall(actionType, context);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async executeLLMCall(actionType, context) {
|
|
260
|
+
const result = await this.contentGeneration.generate(actionType.name, actionType.description, context.resolvedInputs, actionType.outputFields, this.defaultOptions);
|
|
261
|
+
return {
|
|
262
|
+
outputs: result.outputs,
|
|
263
|
+
reasoningContent: result.reasoningContent,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async executeApiCall(actionType, context) {
|
|
267
|
+
const apiConfig = actionType.apiConfig;
|
|
268
|
+
// 如果没有 apiConfig,回退到 LLM 生成(向后兼容)
|
|
269
|
+
if (!apiConfig) {
|
|
270
|
+
return this.executeLLMCall(actionType, context);
|
|
271
|
+
}
|
|
272
|
+
// 解析 URL 中的模板变量
|
|
273
|
+
const url = this.resolveUrlTemplate(apiConfig.url, context.resolvedInputs);
|
|
274
|
+
// 构建请求头(异步,支持 Template 表达式)
|
|
275
|
+
const headers = await this.buildHeaders(apiConfig, context.resolvedInputs);
|
|
276
|
+
// 构建请求体(异步,支持 Loader 表达式)
|
|
277
|
+
const body = await this.buildRequestBody(apiConfig, context.resolvedInputs);
|
|
278
|
+
// 构建 cookie 头
|
|
279
|
+
if (apiConfig.cookies && Object.keys(apiConfig.cookies).length > 0) {
|
|
280
|
+
const cookieHeader = Object.entries(apiConfig.cookies)
|
|
281
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
282
|
+
.join('; ');
|
|
283
|
+
headers['Cookie'] = cookieHeader;
|
|
284
|
+
}
|
|
285
|
+
// 创建 AbortController 用于超时控制
|
|
286
|
+
const controller = new AbortController();
|
|
287
|
+
const timeoutMs = apiConfig.timeoutMs ?? 30000;
|
|
288
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
289
|
+
try {
|
|
290
|
+
const response = await fetch(url, {
|
|
291
|
+
method: apiConfig.method ?? 'GET',
|
|
292
|
+
headers,
|
|
293
|
+
body,
|
|
294
|
+
signal: controller.signal,
|
|
295
|
+
redirect: apiConfig.followRedirects === false ? 'manual' : 'follow',
|
|
296
|
+
});
|
|
297
|
+
clearTimeout(timeoutId);
|
|
298
|
+
// 处理响应
|
|
299
|
+
const outputs = await this.processApiResponse(response, apiConfig, context.resolvedInputs);
|
|
300
|
+
return { outputs };
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
clearTimeout(timeoutId);
|
|
304
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
305
|
+
throw new Error(`API call timeout after ${timeoutMs}ms: ${url}`);
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 解析 URL 中的模板变量
|
|
312
|
+
* 支持 ${variableName} 格式
|
|
313
|
+
*/
|
|
314
|
+
resolveUrlTemplate(urlTemplate, inputs) {
|
|
315
|
+
return urlTemplate.replace(/\$\{(\w+)\}/g, (_match, varName) => {
|
|
316
|
+
const value = inputs[varName];
|
|
317
|
+
if (value === undefined || value === null) {
|
|
318
|
+
throw new Error(`Missing URL template variable: ${varName}`);
|
|
319
|
+
}
|
|
320
|
+
return encodeURIComponent(String(value));
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* 构建请求头(异步版本,支持 Nunjucks 模板)
|
|
325
|
+
*/
|
|
326
|
+
async buildHeaders(apiConfig, inputs) {
|
|
327
|
+
const headers = {};
|
|
328
|
+
if (apiConfig.headers) {
|
|
329
|
+
const { type, values, template, base } = apiConfig.headers;
|
|
330
|
+
// 1. 先添加 base headers(优先级最低)
|
|
331
|
+
if (base) {
|
|
332
|
+
Object.assign(headers, base);
|
|
333
|
+
}
|
|
334
|
+
// 2. 根据 type 处理
|
|
335
|
+
if (type === 'static') {
|
|
336
|
+
// 静态模式:直接使用 values
|
|
337
|
+
if (values) {
|
|
338
|
+
Object.assign(headers, values);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else if (type === 'template') {
|
|
342
|
+
// 模板模式:渲染 Nunjucks 模板
|
|
343
|
+
if (template) {
|
|
344
|
+
const renderedHeaders = await this.renderTemplate(template, inputs);
|
|
345
|
+
try {
|
|
346
|
+
const parsedHeaders = JSON.parse(renderedHeaders);
|
|
347
|
+
if (typeof parsedHeaders === 'object' && parsedHeaders !== null) {
|
|
348
|
+
Object.assign(headers, parsedHeaders);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
throw new Error('headers.template must render to a JSON object');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (error instanceof SyntaxError) {
|
|
356
|
+
throw new Error(`headers.template must render to valid JSON: ${error.message}`);
|
|
357
|
+
}
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// 如果有请求体且未指定 Content-Type,根据 contentType 设置
|
|
364
|
+
if (apiConfig.body) {
|
|
365
|
+
if (!headers['Content-Type']) {
|
|
366
|
+
switch (apiConfig.body.contentType) {
|
|
367
|
+
case 'json':
|
|
368
|
+
case 'template':
|
|
369
|
+
headers['Content-Type'] = 'application/json';
|
|
370
|
+
break;
|
|
371
|
+
case 'form':
|
|
372
|
+
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
373
|
+
break;
|
|
374
|
+
// raw 类型由用户自行设置 Content-Type
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return headers;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* 构建请求体(异步版本,支持 Loader 表达式)
|
|
382
|
+
*/
|
|
383
|
+
async buildRequestBody(apiConfig, inputs) {
|
|
384
|
+
if (!apiConfig.body) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
const { contentType, rawField, template, includeFields, excludeFields } = apiConfig.body;
|
|
388
|
+
// raw 模式:使用指定字段的原始值
|
|
389
|
+
if (contentType === 'raw') {
|
|
390
|
+
if (!rawField) {
|
|
391
|
+
throw new Error('rawField is required when contentType is "raw"');
|
|
392
|
+
}
|
|
393
|
+
const value = inputs[rawField];
|
|
394
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
395
|
+
}
|
|
396
|
+
// template 模式:使用 Nunjucks 模板引擎
|
|
397
|
+
if (contentType === 'template') {
|
|
398
|
+
if (!template) {
|
|
399
|
+
throw new Error('template is required when contentType is "template"');
|
|
400
|
+
}
|
|
401
|
+
return this.renderTemplate(template, inputs);
|
|
402
|
+
}
|
|
403
|
+
// 确定要包含的字段
|
|
404
|
+
let bodyData = {};
|
|
405
|
+
if (includeFields && includeFields.length > 0) {
|
|
406
|
+
// 只包含指定字段
|
|
407
|
+
for (const field of includeFields) {
|
|
408
|
+
if (field in inputs) {
|
|
409
|
+
bodyData[field] = inputs[field];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
// 包含所有输入字段(排除以 _ 开头的内部字段)
|
|
415
|
+
bodyData = { ...inputs };
|
|
416
|
+
for (const key of Object.keys(bodyData)) {
|
|
417
|
+
if (key.startsWith('_')) {
|
|
418
|
+
delete bodyData[key];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// 排除指定字段
|
|
423
|
+
if (excludeFields && excludeFields.length > 0) {
|
|
424
|
+
for (const field of excludeFields) {
|
|
425
|
+
delete bodyData[field];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// 根据 contentType 序列化
|
|
429
|
+
if (contentType === 'json') {
|
|
430
|
+
return JSON.stringify(bodyData);
|
|
431
|
+
}
|
|
432
|
+
else if (contentType === 'form') {
|
|
433
|
+
const params = new URLSearchParams();
|
|
434
|
+
for (const [key, value] of Object.entries(bodyData)) {
|
|
435
|
+
params.append(key, String(value));
|
|
436
|
+
}
|
|
437
|
+
return params.toString();
|
|
438
|
+
}
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* 处理 API 响应
|
|
443
|
+
*/
|
|
444
|
+
async processApiResponse(response, apiConfig, inputs) {
|
|
445
|
+
const responseType = apiConfig.response?.type ?? 'json';
|
|
446
|
+
let responseData;
|
|
447
|
+
switch (responseType) {
|
|
448
|
+
case 'json':
|
|
449
|
+
responseData = await response.json();
|
|
450
|
+
break;
|
|
451
|
+
case 'text':
|
|
452
|
+
responseData = await response.text();
|
|
453
|
+
break;
|
|
454
|
+
case 'binary':
|
|
455
|
+
responseData = await response.arrayBuffer();
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
// 检查 API 是否返回了错误响应
|
|
459
|
+
if (responseData && typeof responseData === 'object' && 'error' in responseData) {
|
|
460
|
+
const errorResponse = responseData;
|
|
461
|
+
const errorMessage = errorResponse.error?.message || JSON.stringify(errorResponse.error);
|
|
462
|
+
throw new Error(`API returned error: ${errorMessage}`);
|
|
463
|
+
}
|
|
464
|
+
// 基础输出
|
|
465
|
+
const outputs = {
|
|
466
|
+
_status: response.status,
|
|
467
|
+
_statusText: response.statusText,
|
|
468
|
+
_headers: Object.fromEntries(response.headers.entries()),
|
|
469
|
+
};
|
|
470
|
+
// 检查是否配置了 saveToFile
|
|
471
|
+
const saveToFileConfig = apiConfig.response?.saveToFile;
|
|
472
|
+
if (saveToFileConfig?.enabled) {
|
|
473
|
+
const saveResult = await this.saveResponseDataToFile(responseData, saveToFileConfig, inputs);
|
|
474
|
+
// 返回文件路径和相关信息,不返回原始数据
|
|
475
|
+
const outputField = saveToFileConfig.outputField ?? 'filePath';
|
|
476
|
+
outputs[outputField] = saveResult.filePath;
|
|
477
|
+
outputs.bytesWritten = saveResult.bytesWritten;
|
|
478
|
+
outputs.success = true;
|
|
479
|
+
return outputs;
|
|
480
|
+
}
|
|
481
|
+
// 如果配置了字段提取
|
|
482
|
+
if (apiConfig.response?.extract && typeof responseData === 'object' && responseData !== null) {
|
|
483
|
+
for (const [outputField, jsonPath] of Object.entries(apiConfig.response.extract)) {
|
|
484
|
+
outputs[outputField] = this.extractByPath(responseData, jsonPath);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
// 没有配置提取时,直接返回响应数据
|
|
489
|
+
outputs.response = responseData;
|
|
490
|
+
}
|
|
491
|
+
return outputs;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* 将响应数据保存到文件
|
|
495
|
+
* 用于处理大型响应(如图片、视频),避免超出 Temporal payload 限制
|
|
496
|
+
*/
|
|
497
|
+
async saveResponseDataToFile(responseData, config, inputs) {
|
|
498
|
+
// 获取保存路径
|
|
499
|
+
const targetPath = inputs[config.pathField];
|
|
500
|
+
if (!targetPath) {
|
|
501
|
+
throw new Error(`saveToFile.pathField "${config.pathField}" not found in inputs`);
|
|
502
|
+
}
|
|
503
|
+
// 解析并验证文件路径
|
|
504
|
+
const fullPath = this.resolveFilePath(targetPath);
|
|
505
|
+
// 提取要保存的数据
|
|
506
|
+
let dataToSave = responseData;
|
|
507
|
+
if (config.dataPath) {
|
|
508
|
+
dataToSave = this.extractByPath(responseData, config.dataPath);
|
|
509
|
+
if (dataToSave === undefined) {
|
|
510
|
+
throw new Error(`Could not extract data from response using path: ${config.dataPath}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// 确定数据格式并转换为 Buffer
|
|
514
|
+
const buffer = this.convertToBuffer(dataToSave, config.dataFormat);
|
|
515
|
+
// 确保目录存在
|
|
516
|
+
const dir = path.dirname(fullPath);
|
|
517
|
+
await fs.mkdir(dir, { recursive: true });
|
|
518
|
+
// 写入文件
|
|
519
|
+
await fs.writeFile(fullPath, buffer);
|
|
520
|
+
return {
|
|
521
|
+
filePath: fullPath,
|
|
522
|
+
bytesWritten: buffer.length,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* 将数据转换为 Buffer
|
|
527
|
+
* 支持多种数据格式
|
|
528
|
+
*/
|
|
529
|
+
convertToBuffer(data, format = 'auto') {
|
|
530
|
+
// 预处理:如果数据是对象且包含 url 字段(如 { url: "data:..." }),提取 url
|
|
531
|
+
let processedData = data;
|
|
532
|
+
if (typeof data === 'object' && data !== null && 'url' in data) {
|
|
533
|
+
processedData = data.url;
|
|
534
|
+
}
|
|
535
|
+
// 自动检测格式
|
|
536
|
+
if (format === 'auto') {
|
|
537
|
+
if (typeof processedData === 'string') {
|
|
538
|
+
// 检查是否是 base64 data URI
|
|
539
|
+
if (processedData.startsWith('data:')) {
|
|
540
|
+
format = 'base64';
|
|
541
|
+
}
|
|
542
|
+
else if (this.looksLikeBase64(processedData)) {
|
|
543
|
+
format = 'base64';
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
format = 'text';
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else if (processedData instanceof ArrayBuffer || Buffer.isBuffer(processedData)) {
|
|
550
|
+
format = 'binary';
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
format = 'json';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
switch (format) {
|
|
557
|
+
case 'base64': {
|
|
558
|
+
// 处理对象包装的情况(如 { url: "data:..." })
|
|
559
|
+
let strData;
|
|
560
|
+
if (typeof processedData === 'string') {
|
|
561
|
+
strData = processedData;
|
|
562
|
+
}
|
|
563
|
+
else if (typeof processedData === 'object' && processedData !== null) {
|
|
564
|
+
// 尝试从对象中提取字符串
|
|
565
|
+
const obj = processedData;
|
|
566
|
+
if (typeof obj.url === 'string') {
|
|
567
|
+
strData = obj.url;
|
|
568
|
+
}
|
|
569
|
+
else if (typeof obj.image_url === 'string') {
|
|
570
|
+
strData = obj.image_url;
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
throw new Error('base64 format requires string data or object with url/image_url field');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
throw new Error('base64 format requires string data');
|
|
578
|
+
}
|
|
579
|
+
// 处理 data URI 格式
|
|
580
|
+
if (strData.startsWith('data:')) {
|
|
581
|
+
const base64Data = strData.split(',')[1];
|
|
582
|
+
if (!base64Data) {
|
|
583
|
+
throw new Error('Invalid data URI format');
|
|
584
|
+
}
|
|
585
|
+
return Buffer.from(base64Data, 'base64');
|
|
586
|
+
}
|
|
587
|
+
return Buffer.from(strData, 'base64');
|
|
588
|
+
}
|
|
589
|
+
case 'binary': {
|
|
590
|
+
if (processedData instanceof ArrayBuffer) {
|
|
591
|
+
return Buffer.from(processedData);
|
|
592
|
+
}
|
|
593
|
+
if (Buffer.isBuffer(processedData)) {
|
|
594
|
+
return processedData;
|
|
595
|
+
}
|
|
596
|
+
throw new Error('binary format requires ArrayBuffer or Buffer data');
|
|
597
|
+
}
|
|
598
|
+
case 'text': {
|
|
599
|
+
if (typeof processedData === 'string') {
|
|
600
|
+
return Buffer.from(processedData, 'utf-8');
|
|
601
|
+
}
|
|
602
|
+
return Buffer.from(String(processedData), 'utf-8');
|
|
603
|
+
}
|
|
604
|
+
case 'json': {
|
|
605
|
+
return Buffer.from(JSON.stringify(processedData, null, 2), 'utf-8');
|
|
606
|
+
}
|
|
607
|
+
default:
|
|
608
|
+
throw new Error(`Unknown data format: ${format}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* 检查字符串是否看起来像 base64 编码
|
|
613
|
+
*/
|
|
614
|
+
looksLikeBase64(str) {
|
|
615
|
+
// 简单检查:长度较长且只包含 base64 字符
|
|
616
|
+
if (str.length < 100)
|
|
617
|
+
return false;
|
|
618
|
+
const base64Regex = /^[A-Za-z0-9+/=]+$/;
|
|
619
|
+
return base64Regex.test(str.slice(0, 100));
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* 从对象中按路径提取值
|
|
623
|
+
* 支持点号分隔路径和数组索引,如:
|
|
624
|
+
* - "data.results"
|
|
625
|
+
* - "meta.total"
|
|
626
|
+
* - "choices.0.message" (数字作为数组索引)
|
|
627
|
+
* - "items[0].name" (方括号数组索引)
|
|
628
|
+
*/
|
|
629
|
+
extractByPath(obj, path) {
|
|
630
|
+
// 将 [0] 格式转换为 .0 格式,统一处理
|
|
631
|
+
const normalizedPath = path.replace(/\[(\d+)\]/g, '.$1');
|
|
632
|
+
const parts = normalizedPath.split('.');
|
|
633
|
+
let current = obj;
|
|
634
|
+
for (const part of parts) {
|
|
635
|
+
if (current === null || current === undefined) {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
if (typeof current !== 'object') {
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
// 检查是否是数组索引
|
|
642
|
+
const index = parseInt(part, 10);
|
|
643
|
+
if (!isNaN(index) && Array.isArray(current)) {
|
|
644
|
+
current = current[index];
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
current = current[part];
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return current;
|
|
651
|
+
}
|
|
652
|
+
async executeHttpRequest(_actionType, context) {
|
|
653
|
+
// HTTP 请求逻辑
|
|
654
|
+
const url = context.resolvedInputs.url;
|
|
655
|
+
const method = context.resolvedInputs.method ?? 'GET';
|
|
656
|
+
const body = context.resolvedInputs.body;
|
|
657
|
+
const response = await fetch(url, {
|
|
658
|
+
method,
|
|
659
|
+
headers: { 'Content-Type': 'application/json' },
|
|
660
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
661
|
+
});
|
|
662
|
+
const data = await response.json();
|
|
663
|
+
return { outputs: { response: data, status: response.status } };
|
|
664
|
+
}
|
|
665
|
+
async executeFileOperation(actionType, context) {
|
|
666
|
+
const { resolvedInputs } = context;
|
|
667
|
+
// 获取文件路径
|
|
668
|
+
const filePath = resolvedInputs.filePath;
|
|
669
|
+
const filename = resolvedInputs.filename;
|
|
670
|
+
const targetPath = filePath ?? filename;
|
|
671
|
+
if (!targetPath) {
|
|
672
|
+
throw new Error('File operation requires filePath or filename input');
|
|
673
|
+
}
|
|
674
|
+
// 构建完整路径并验证安全性
|
|
675
|
+
const fullPath = this.resolveFilePath(targetPath);
|
|
676
|
+
if (actionType.executionType === 'file_write') {
|
|
677
|
+
return this.executeFileWrite(fullPath, resolvedInputs);
|
|
678
|
+
}
|
|
679
|
+
else if (actionType.executionType === 'file_read') {
|
|
680
|
+
return this.executeFileRead(fullPath, resolvedInputs);
|
|
681
|
+
}
|
|
682
|
+
throw new Error(`Unknown file operation: ${actionType.executionType}`);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* 解析并验证文件路径
|
|
686
|
+
*/
|
|
687
|
+
resolveFilePath(targetPath) {
|
|
688
|
+
// 如果是绝对路径,直接使用;否则相对于 baseDir
|
|
689
|
+
const fullPath = path.isAbsolute(targetPath)
|
|
690
|
+
? targetPath
|
|
691
|
+
: path.resolve(this.fileConfig.baseDir, targetPath);
|
|
692
|
+
// 安全检查:确保路径在 baseDir 内(防止路径遍历攻击)
|
|
693
|
+
const normalizedPath = path.normalize(fullPath);
|
|
694
|
+
const normalizedBase = path.normalize(this.fileConfig.baseDir);
|
|
695
|
+
if (!normalizedPath.startsWith(normalizedBase)) {
|
|
696
|
+
throw new Error(`File path must be within base directory: ${this.fileConfig.baseDir}`);
|
|
697
|
+
}
|
|
698
|
+
// 检查文件扩展名
|
|
699
|
+
if (this.fileConfig.allowedExtensions && this.fileConfig.allowedExtensions.length > 0) {
|
|
700
|
+
const ext = path.extname(normalizedPath).toLowerCase();
|
|
701
|
+
if (!this.fileConfig.allowedExtensions.includes(ext)) {
|
|
702
|
+
throw new Error(`File extension not allowed: ${ext}. Allowed: ${this.fileConfig.allowedExtensions.join(', ')}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return normalizedPath;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* 执行文件写入
|
|
709
|
+
*/
|
|
710
|
+
async executeFileWrite(fullPath, inputs) {
|
|
711
|
+
const content = inputs.content;
|
|
712
|
+
const data = inputs.data;
|
|
713
|
+
// 确定要写入的内容
|
|
714
|
+
let writeContent;
|
|
715
|
+
if (typeof content === 'string') {
|
|
716
|
+
writeContent = content;
|
|
717
|
+
}
|
|
718
|
+
else if (data !== undefined) {
|
|
719
|
+
writeContent = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
throw new Error('File write requires content or data input');
|
|
723
|
+
}
|
|
724
|
+
// 检查内容大小
|
|
725
|
+
const contentSize = Buffer.byteLength(writeContent, 'utf8');
|
|
726
|
+
if (contentSize > this.fileConfig.maxFileSize) {
|
|
727
|
+
throw new Error(`Content size (${contentSize} bytes) exceeds maximum allowed (${this.fileConfig.maxFileSize} bytes)`);
|
|
728
|
+
}
|
|
729
|
+
// 确保目录存在
|
|
730
|
+
const dir = path.dirname(fullPath);
|
|
731
|
+
await fs.mkdir(dir, { recursive: true });
|
|
732
|
+
// 写入文件
|
|
733
|
+
const encoding = inputs.encoding ?? 'utf8';
|
|
734
|
+
const append = inputs.append === true;
|
|
735
|
+
if (append) {
|
|
736
|
+
await fs.appendFile(fullPath, writeContent, { encoding });
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
await fs.writeFile(fullPath, writeContent, { encoding });
|
|
740
|
+
}
|
|
741
|
+
// 获取文件信息
|
|
742
|
+
const stats = await fs.stat(fullPath);
|
|
743
|
+
return {
|
|
744
|
+
outputs: {
|
|
745
|
+
success: true,
|
|
746
|
+
filePath: fullPath,
|
|
747
|
+
bytesWritten: contentSize,
|
|
748
|
+
fileSize: stats.size,
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* 执行文件读取
|
|
754
|
+
*/
|
|
755
|
+
async executeFileRead(fullPath, inputs) {
|
|
756
|
+
// 检查文件是否存在
|
|
757
|
+
try {
|
|
758
|
+
await fs.access(fullPath);
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
throw new Error(`File not found: ${fullPath}`);
|
|
762
|
+
}
|
|
763
|
+
// 获取文件信息
|
|
764
|
+
const stats = await fs.stat(fullPath);
|
|
765
|
+
// 检查文件大小
|
|
766
|
+
if (stats.size > this.fileConfig.maxFileSize) {
|
|
767
|
+
throw new Error(`File size (${stats.size} bytes) exceeds maximum allowed (${this.fileConfig.maxFileSize} bytes)`);
|
|
768
|
+
}
|
|
769
|
+
// 读取文件
|
|
770
|
+
const encoding = inputs.encoding ?? 'utf8';
|
|
771
|
+
const content = await fs.readFile(fullPath, { encoding });
|
|
772
|
+
// 尝试解析 JSON(如果请求)
|
|
773
|
+
let parsedData = undefined;
|
|
774
|
+
if (inputs.parseJson === true) {
|
|
775
|
+
try {
|
|
776
|
+
parsedData = JSON.parse(content);
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
// 解析失败,保持 undefined
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
outputs: {
|
|
784
|
+
success: true,
|
|
785
|
+
filePath: fullPath,
|
|
786
|
+
content,
|
|
787
|
+
data: parsedData,
|
|
788
|
+
fileSize: stats.size,
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async executeDatabaseQuery(_actionType, _context) {
|
|
793
|
+
// 数据库查询逻辑(需要注入数据库连接)
|
|
794
|
+
// TODO: 注入数据库连接并实现查询逻辑
|
|
795
|
+
return { outputs: { rows: [] } };
|
|
796
|
+
}
|
|
797
|
+
async executeShellCommand(_actionType, _context) {
|
|
798
|
+
// Shell 命令逻辑(需要根据实际环境实现)
|
|
799
|
+
// TODO: 根据安全策略实现 shell 命令执行
|
|
800
|
+
return { outputs: { stdout: '', stderr: '', exitCode: 0 } };
|
|
801
|
+
}
|
|
802
|
+
async submitAsyncTask(actionType, context) {
|
|
803
|
+
const asyncMode = actionType.asyncMode;
|
|
804
|
+
// 验证异步配置
|
|
805
|
+
if (!asyncMode?.statusCheck?.url) {
|
|
806
|
+
throw new Error(`Async action "${actionType.id}" requires asyncMode.statusCheck.url configuration. ` +
|
|
807
|
+
`Please configure the status check URL for polling task status.`);
|
|
808
|
+
}
|
|
809
|
+
// 使用 apiConfig 提交任务
|
|
810
|
+
if (!actionType.apiConfig) {
|
|
811
|
+
throw new Error(`Async action "${actionType.id}" requires apiConfig for task submission`);
|
|
812
|
+
}
|
|
813
|
+
// 调用现有的 executeApiCall 提交任务
|
|
814
|
+
const submitResult = await this.executeApiCall(actionType, context);
|
|
815
|
+
// 从响应中提取 taskId
|
|
816
|
+
const taskIdPath = asyncMode.taskIdPath ?? 'taskId';
|
|
817
|
+
const taskId = this.extractByPath(submitResult.outputs.response ?? submitResult.outputs, taskIdPath);
|
|
818
|
+
if (!taskId || typeof taskId !== 'string') {
|
|
819
|
+
throw new Error(`Failed to extract taskId from response using path "${taskIdPath}". ` +
|
|
820
|
+
`Response: ${JSON.stringify(submitResult.outputs)}`);
|
|
821
|
+
}
|
|
822
|
+
// 保存任务上下文
|
|
823
|
+
// 从新结构中提取 headers 用于后续请求
|
|
824
|
+
const headersConfig = actionType.apiConfig.headers;
|
|
825
|
+
let resolvedHeaders;
|
|
826
|
+
if (headersConfig) {
|
|
827
|
+
// 合并 base 和 values/template 渲染结果
|
|
828
|
+
// 由于 checkTaskStatus/getTaskResult 需要同步使用,这里只保存静态值
|
|
829
|
+
resolvedHeaders = { ...headersConfig.base };
|
|
830
|
+
if (headersConfig.type === 'static' && headersConfig.values) {
|
|
831
|
+
Object.assign(resolvedHeaders, headersConfig.values);
|
|
832
|
+
}
|
|
833
|
+
// 注意:template 类型需要动态渲染,这里暂时使用 base
|
|
834
|
+
// 如果 statusCheck/resultFetch 需要动态 headers,应在各自配置中定义
|
|
835
|
+
}
|
|
836
|
+
this.asyncTaskContexts.set(taskId, {
|
|
837
|
+
taskId,
|
|
838
|
+
asyncMode,
|
|
839
|
+
headers: resolvedHeaders,
|
|
840
|
+
inputs: context.resolvedInputs,
|
|
841
|
+
});
|
|
842
|
+
return taskId;
|
|
843
|
+
}
|
|
844
|
+
async checkTaskStatus(taskId) {
|
|
845
|
+
const ctx = this.asyncTaskContexts.get(taskId);
|
|
846
|
+
if (!ctx) {
|
|
847
|
+
throw new Error(`Task context not found for taskId: ${taskId}. The task may not have been submitted through this service instance.`);
|
|
848
|
+
}
|
|
849
|
+
const statusCheck = ctx.asyncMode.statusCheck;
|
|
850
|
+
// 解析 URL 模板
|
|
851
|
+
const url = this.resolveAsyncUrlTemplate(statusCheck.url, taskId, ctx.inputs);
|
|
852
|
+
// 合并请求头
|
|
853
|
+
const headers = {
|
|
854
|
+
...ctx.headers,
|
|
855
|
+
...statusCheck.headers,
|
|
856
|
+
};
|
|
857
|
+
// 发起状态检查请求
|
|
858
|
+
const response = await fetch(url, {
|
|
859
|
+
method: statusCheck.method ?? 'GET',
|
|
860
|
+
headers,
|
|
861
|
+
});
|
|
862
|
+
if (!response.ok) {
|
|
863
|
+
throw new Error(`Status check failed: ${response.status} ${response.statusText}`);
|
|
864
|
+
}
|
|
865
|
+
const data = await response.json();
|
|
866
|
+
// 保存响应用于后续结果提取
|
|
867
|
+
ctx.lastStatusResponse = data;
|
|
868
|
+
// 提取状态信息
|
|
869
|
+
const extract = statusCheck.extract;
|
|
870
|
+
const statusValue = this.extractByPath(data, extract.statusPath ?? 'status');
|
|
871
|
+
// 规范化状态值比较
|
|
872
|
+
const completedValues = this.normalizeStatusValues(extract.completedValues ?? 'completed');
|
|
873
|
+
const failedValues = this.normalizeStatusValues(extract.failedValues ?? 'failed');
|
|
874
|
+
const statusStr = String(statusValue).toLowerCase();
|
|
875
|
+
const completed = completedValues.some(v => v.toLowerCase() === statusStr);
|
|
876
|
+
const failed = failedValues.some(v => v.toLowerCase() === statusStr);
|
|
877
|
+
// 提取错误信息
|
|
878
|
+
let error;
|
|
879
|
+
if (failed && extract.errorPath) {
|
|
880
|
+
const errorValue = this.extractByPath(data, extract.errorPath);
|
|
881
|
+
error = errorValue ? String(errorValue) : undefined;
|
|
882
|
+
}
|
|
883
|
+
// 提取进度
|
|
884
|
+
let progress;
|
|
885
|
+
if (extract.progressPath) {
|
|
886
|
+
const progressValue = this.extractByPath(data, extract.progressPath);
|
|
887
|
+
if (typeof progressValue === 'number') {
|
|
888
|
+
progress = progressValue;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return { completed, failed, error, progress };
|
|
892
|
+
}
|
|
893
|
+
async getTaskResult(taskId) {
|
|
894
|
+
const ctx = this.asyncTaskContexts.get(taskId);
|
|
895
|
+
if (!ctx) {
|
|
896
|
+
throw new Error(`Task context not found for taskId: ${taskId}. The task may not have been submitted through this service instance.`);
|
|
897
|
+
}
|
|
898
|
+
const resultFetch = ctx.asyncMode.resultFetch;
|
|
899
|
+
// 情况 1: 有独立的结果获取 URL
|
|
900
|
+
if (resultFetch?.url) {
|
|
901
|
+
const url = this.resolveAsyncUrlTemplate(resultFetch.url, taskId, ctx.inputs);
|
|
902
|
+
const headers = {
|
|
903
|
+
...ctx.headers,
|
|
904
|
+
...resultFetch.headers,
|
|
905
|
+
};
|
|
906
|
+
const response = await fetch(url, {
|
|
907
|
+
method: resultFetch.method ?? 'GET',
|
|
908
|
+
headers,
|
|
909
|
+
});
|
|
910
|
+
if (!response.ok) {
|
|
911
|
+
throw new Error(`Result fetch failed: ${response.status} ${response.statusText}`);
|
|
912
|
+
}
|
|
913
|
+
const data = await response.json();
|
|
914
|
+
// 应用结果提取映射
|
|
915
|
+
if (resultFetch.extract) {
|
|
916
|
+
return { outputs: this.extractMultipleFields(data, resultFetch.extract) };
|
|
917
|
+
}
|
|
918
|
+
return { outputs: data };
|
|
919
|
+
}
|
|
920
|
+
// 情况 2: 从最后一次状态检查响应中提取结果
|
|
921
|
+
if (ctx.lastStatusResponse) {
|
|
922
|
+
const extractConfig = resultFetch?.extractFromStatus;
|
|
923
|
+
if (extractConfig) {
|
|
924
|
+
return { outputs: this.extractMultipleFields(ctx.lastStatusResponse, extractConfig) };
|
|
925
|
+
}
|
|
926
|
+
// 使用整个状态响应作为结果
|
|
927
|
+
return { outputs: ctx.lastStatusResponse };
|
|
928
|
+
}
|
|
929
|
+
throw new Error('No result available: neither result URL nor status response found');
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* 解析异步 URL 模板
|
|
933
|
+
* 支持 ${taskId} 和 ${fieldName} 变量
|
|
934
|
+
*/
|
|
935
|
+
resolveAsyncUrlTemplate(urlTemplate, taskId, inputs) {
|
|
936
|
+
return urlTemplate.replace(/\$\{(\w+)\}/g, (_match, varName) => {
|
|
937
|
+
if (varName === 'taskId') {
|
|
938
|
+
return encodeURIComponent(taskId);
|
|
939
|
+
}
|
|
940
|
+
const value = inputs[varName];
|
|
941
|
+
if (value === undefined || value === null) {
|
|
942
|
+
throw new Error(`Missing URL template variable: ${varName}`);
|
|
943
|
+
}
|
|
944
|
+
return encodeURIComponent(String(value));
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* 规范化状态值为数组
|
|
949
|
+
*/
|
|
950
|
+
normalizeStatusValues(values) {
|
|
951
|
+
return Array.isArray(values) ? values : [values];
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* 从对象中提取多个字段
|
|
955
|
+
*/
|
|
956
|
+
extractMultipleFields(data, extract) {
|
|
957
|
+
const result = {};
|
|
958
|
+
for (const [outputField, jsonPath] of Object.entries(extract)) {
|
|
959
|
+
result[outputField] = this.extractByPath(data, jsonPath);
|
|
960
|
+
}
|
|
961
|
+
return result;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* 清理已完成的异步任务上下文
|
|
965
|
+
* 防止内存泄漏
|
|
966
|
+
*/
|
|
967
|
+
cleanupAsyncTaskContext(taskId) {
|
|
968
|
+
this.asyncTaskContexts.delete(taskId);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
//# sourceMappingURL=actionService.js.map
|