-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathexport.js
More file actions
242 lines (212 loc) · 9.48 KB
/
export.js
File metadata and controls
242 lines (212 loc) · 9.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
(() => {
// =============== 新增:自动提取 API 前缀 ===============
const getApiBase = () => {
const entry = performance.getEntries().find(e =>
e.name.includes('/console/api') || e.name.includes('/api')
);
if (entry) {
const url = new URL(entry.name, location.origin);
const path = url.pathname;
if (path.includes('/console/api')) {
return url.origin + '/console/api';
} else if (path.includes('/api')) {
return url.origin + '/api';
}
}
// 默认回退(Dify 控制台通常用 /console/api)
return location.origin + '/console/api';
};
// =============== 新增:从Cookie提取指定值(通用方法) ===============
const getCookie = (name) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
};
// =============== 新增:获取Token(新老方式兼容,适配实际Cookie) ===============
const getAuthToken = () => {
// 1. 优先从Cookie获取 csrf_token(实际存在的Cookie键)
let csrfToken = getCookie('csrf_token');
let cookieKey = 'csrf_token';
// 2. 兼容回退:若没找到 csrf_token,再试 x-csrf-token
if (!csrfToken) {
csrfToken = getCookie('x-csrf-token');
cookieKey = 'x-csrf-token';
}
// 3. 找到CSRF Token则返回
if (csrfToken) {
console.log(`✅ 从Cookie获取到${cookieKey}:${csrfToken.substring(0, 20)}...`); // 脱敏输出
return {
type: 'csrf',
token: csrfToken,
cookieKey: cookieKey // 记录实际读取的Cookie键
};
}
// 4. 回退到原有方式获取console_token
const consoleToken = localStorage.console_token || sessionStorage.console_token || window.console_token;
if (consoleToken) {
console.log('✅ 从存储获取到console_token:', consoleToken.substring(0, 20) + '...');
return {
type: 'jwt',
token: consoleToken
};
}
// 5. 所有方式都失败
return null;
};
// 1. 动态加载JSZip库(提供多个备用源解决跨域问题)
const loadJSZip = () => {
return new Promise((resolve, reject) => {
console.log('🔧 正在加载ZIP打包库...');
// 备用CDN列表(按优先级排序)
const jsZipSources = [
'https://cdn.staticfile.org/jszip/3.10.1/jszip.min.js', // 阿里云静态资源CDN(国内节点,稳定性强)
'https://static.cloud.tencent.com/ajax/libs/jszip/3.10.1/jszip.min.js', // 腾讯云静态资源CDN(国内节点,覆盖广)
'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js', // jsDelivr CDN(国际知名开源库CDN)
'https://unpkg.com/jszip@3.10.1/dist/jszip.min.js', // unpkg CDN(专注于npm包分发的CDN)
'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js' // Cloudflare CDN(cdnjs项目,开源库分发)
];
let currentSourceIndex = 0;
const tryLoadSource = () => {
if (currentSourceIndex >= jsZipSources.length) {
reject(new Error('所有ZIP库源都加载失败,请检查网络或手动引入JSZip'));
return;
}
const script = document.createElement('script');
const currentSource = jsZipSources[currentSourceIndex].trim(); // 修复多余空格
console.log(`尝试从源 ${currentSourceIndex + 1}/${jsZipSources.length} 加载: ${currentSource}`);
script.src = currentSource;
script.onload = () => {
if (window.JSZip) {
console.log('✅ ZIP库加载成功');
resolve(window.JSZip);
} else {
console.warn(`❌ 源 ${currentSourceIndex + 1} 加载但未找到JSZip对象,尝试下一个源...`);
currentSourceIndex++;
tryLoadSource();
}
};
script.onerror = () => {
console.warn(`❌ 源 ${currentSourceIndex + 1} 加载失败,尝试下一个源...`);
currentSourceIndex++;
tryLoadSource();
};
setTimeout(() => {
if (!window.JSZip) {
console.warn(`⏰ 源 ${currentSourceIndex + 1} 加载超时,尝试下一个源...`);
script.remove();
currentSourceIndex++;
tryLoadSource();
}
}, 10000);
document.head.appendChild(script);
};
tryLoadSource();
});
};
// 2. 获取动态日期(格式:YYYYMMDD,自动补0)
const getDynamicDate = () => {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}${month}${day}`;
};
// 3. 核心:获取应用YAML并打包成ZIP
const fetchAppsAndZip = async (JSZip) => {
// =============== 关键修改:使用动态 API 前缀 ===============
const API_BASE = getApiBase();
console.log('🌐 检测到 API 基础地址:', API_BASE);
// 获取认证Token(新老方式兼容)
const authInfo = getAuthToken();
const dynamicDate = getDynamicDate();
// 校验Token
if (!authInfo) {
const errorMsg = '❌ 未找到有效的认证Token!\n- 未检测到 csrf_token / x-csrf-token Cookie\n- 未检测到console_token(localStorage/sessionStorage/window)\n请前往GitHub反馈此问题:https://github.com/xxx/xxx/issues';
console.error(errorMsg);
alert(errorMsg); // 弹窗提示,提升用户感知
return;
}
try {
// 构建请求头(适配不同Token类型,遵循通用标准)
const requestHeaders = {};
if (authInfo.type === 'jwt') {
// JWT Token:使用Bearer认证头
requestHeaders['Authorization'] = `Bearer ${authInfo.token}`;
} else if (authInfo.type === 'csrf') {
// CSRF Token:使用通用标准请求头(后端99%兼容这两个)
requestHeaders['X-CSRFToken'] = authInfo.token; // Django/Flask等框架默认认这个
requestHeaders['X-CSRF-Token'] = authInfo.token; // 兼容部分自定义后端
// 可选:添加实际Cookie键作为请求头,兜底兼容
requestHeaders[authInfo.cookieKey] = authInfo.token;
}
// 3.1 获取应用列表(使用完整 URL)
console.log('🔍 正在获取Dify应用列表...');
const appRes = await fetch(`${API_BASE}/apps?page=1&limit=100&name=&is_created_by_me=false`, {
method: 'GET', // 显式指定GET,避免默认OPTIONS预检问题
headers: requestHeaders,
credentials: 'include' // 强制携带Cookie,适配CSRF认证
});
if (!appRes.ok) throw new Error(`应用列表请求失败:${appRes.status} ${appRes.statusText}`);
const appData = await appRes.json();
const apps = appData.data || [];
if (apps.length === 0) {
console.error('❌ 未获取到任何应用数据');
return;
}
console.log(`✅ 共获取到 ${apps.length} 个应用,开始下载YAML并打包...`);
// 3.2 初始化ZIP,批量添加YAML文件
const zip = new JSZip();
const addToZipPromises = apps.map((app, index) => {
return fetch(`${API_BASE}/apps/${app.id}/export?include_secret=false`, {
method: 'GET',
headers: requestHeaders,
credentials: 'include'
})
.then(res => {
if (!res.ok) throw new Error(`应用【${app.name || app.id}】导出失败:${res.status} ${res.statusText}`);
return res.json();
})
.then(exportJson => {
const safeAppName = (app.name || `unknown-app-${app.id}`).replace(/[<>:"/\\|?*]/g, '_'); // 清理非法文件名字符
const yamlFileName = `${safeAppName}.yaml`;
zip.file(yamlFileName, exportJson.data);
console.log(`✅ 已添加到压缩包 (${index + 1}/${apps.length}):${yamlFileName}`);
return true;
})
.catch(err => {
console.error(`❌ 应用【${app.name || app.id}】处理失败:${err.message}`);
return false;
});
});
await Promise.all(addToZipPromises);
console.log('\n⏳ 正在生成ZIP压缩包...');
const zipBlob = await zip.generateAsync({
type: 'blob',
compression: 'STORE' // 不压缩,提升速度(YAML文本压缩率低)
});
const zipFileName = `dify_apps_yaml_${dynamicDate}.zip`;
const downloadUrl = URL.createObjectURL(zipBlob);
const aTag = document.createElement('a');
aTag.href = downloadUrl;
aTag.download = zipFileName;
document.body.appendChild(aTag);
aTag.click();
// 清理资源
document.body.removeChild(aTag);
URL.revokeObjectURL(downloadUrl);
console.log(`\n🎉 压缩包生成完成!已下载:${zipFileName}`);
console.log(`📌 提示:解压后可直接获取每个应用的独立YAML文件`);
} catch (globalErr) {
console.error(`\n❌ 整体流程失败:${globalErr.message}`);
alert(`操作失败:${globalErr.message}\n请检查控制台日志或前往GitHub反馈`);
}
};
// 启动流程
loadJSZip()
.then(JSZip => fetchAppsAndZip(JSZip))
.catch(err => {
console.error(`❌ 初始化失败:${err.message}`);
alert(`初始化失败:${err.message}\n请检查网络或前往GitHub反馈此问题:https://github.com/AuditAIH/dify-workflow-batch-export/issues`);
});
})();