kingkont 0.20.29 → 0.20.30
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/lib/providers.js +75 -13
- package/lib/r2-upload.js +93 -0
- package/package.json +1 -1
package/lib/providers.js
CHANGED
|
@@ -1007,9 +1007,82 @@ async function audioViaChatium(s, body) {
|
|
|
1007
1007
|
}
|
|
1008
1008
|
|
|
1009
1009
|
// =============================================================================
|
|
1010
|
-
// PUBLIC API: file upload (Chatium-storage с fallback на KIE).
|
|
1010
|
+
// PUBLIC API: file upload (Chatium-storage с fallback на R2 и KIE).
|
|
1011
1011
|
// =============================================================================
|
|
1012
1012
|
|
|
1013
|
+
const r2Upload = require('./r2-upload');
|
|
1014
|
+
|
|
1015
|
+
// Подъём chatium-uploads до 5 секунд, дальше race-win на R2.
|
|
1016
|
+
// Юзер: «текущий вариант не нужно убивать. просто если видим что 5 секунд
|
|
1017
|
+
// не грузится референс в fs.chatium.ru — отправляем через cloudflare».
|
|
1018
|
+
const CHATIUM_UPLOAD_TIMEOUT_MS = 5000;
|
|
1019
|
+
|
|
1020
|
+
async function _chatiumUploadOnly({ buffer, filename, mime, s }) {
|
|
1021
|
+
const urlReq = chatiumBase(s) + CHATIUM_PATHS.uploadUrl;
|
|
1022
|
+
const ru = await fetch(urlReq, { headers: { 'Authorization': `Bearer ${s.chatium.token}` } });
|
|
1023
|
+
if (!ru.ok) throw new Error('KingKont upload-url: ' + (await ru.text()).slice(0, 200));
|
|
1024
|
+
const { uploadUrl } = await ru.json();
|
|
1025
|
+
if (!uploadUrl) throw new Error('KingKont не вернул uploadUrl');
|
|
1026
|
+
logCall('POST', 'Chatium', uploadUrl, `file=${filename} size=${buffer.length}`);
|
|
1027
|
+
const fd = new FormData();
|
|
1028
|
+
fd.append('Filedata', new Blob([buffer], { type: mime }), filename);
|
|
1029
|
+
const ru2 = await fetch(uploadUrl, { method: 'POST', body: fd });
|
|
1030
|
+
const hash = (await ru2.text()).trim();
|
|
1031
|
+
if (!ru2.ok || !hash) throw new Error(`Chatium upload HTTP ${ru2.status}: ${hash.slice(0, 200)}`);
|
|
1032
|
+
return { url: `${CHATIUM_CDN}/${hash}`, fileName: filename, size: buffer.length, hash, provider: 'kingkont' };
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Race: chatium upload против 5s таймера. Если chatium успел — берём его
|
|
1036
|
+
// (закэшируется, экономия для следующих вызовов). Если тайм-аут — стартуем
|
|
1037
|
+
// R2 upload и возвращаем его URL. Chatium upload в фоне продолжается —
|
|
1038
|
+
// его результат всё равно сохранится в chatium-storage, просто мы для
|
|
1039
|
+
// ЭТОЙ генерации использовали R2.
|
|
1040
|
+
async function _uploadViaChatiumWithR2Fallback({ buffer, filename, mime, s }) {
|
|
1041
|
+
const chatiumP = _chatiumUploadOnly({ buffer, filename, mime, s })
|
|
1042
|
+
.catch(e => { console.warn('[uploadFile] chatium failed:', e?.message || e); throw e; });
|
|
1043
|
+
|
|
1044
|
+
const TIMEOUT_SENTINEL = Symbol('timeout');
|
|
1045
|
+
const timerP = new Promise(resolve => setTimeout(resolve, CHATIUM_UPLOAD_TIMEOUT_MS, TIMEOUT_SENTINEL));
|
|
1046
|
+
|
|
1047
|
+
// Race: побеждает либо chatium-result, либо TIMEOUT_SENTINEL.
|
|
1048
|
+
// Catch внутри chatiumP подавляет, но Promise.race увидит resolved/rejected
|
|
1049
|
+
// тогда — обернём отдельный wrapper, чтобы reject не «выиграл» race.
|
|
1050
|
+
const chatiumSafe = chatiumP.then(
|
|
1051
|
+
r => ({ ok: true, result: r }),
|
|
1052
|
+
e => ({ ok: false, error: e }),
|
|
1053
|
+
);
|
|
1054
|
+
const winner = await Promise.race([chatiumSafe, timerP]);
|
|
1055
|
+
|
|
1056
|
+
if (winner !== TIMEOUT_SENTINEL) {
|
|
1057
|
+
if (winner.ok) {
|
|
1058
|
+
console.log('[uploadFile] chatium won race in <5s');
|
|
1059
|
+
return winner.result;
|
|
1060
|
+
}
|
|
1061
|
+
// Chatium быстро упал. Падаем на R2 сразу.
|
|
1062
|
+
console.warn('[uploadFile] chatium fast-failed → R2');
|
|
1063
|
+
} else {
|
|
1064
|
+
console.warn(`[uploadFile] chatium медленно (>${CHATIUM_UPLOAD_TIMEOUT_MS}ms) → R2`);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Fallback на R2.
|
|
1068
|
+
if (!r2Upload.isConfigured()) {
|
|
1069
|
+
// R2 не настроен — ждём chatium докрутится (последний шанс).
|
|
1070
|
+
console.warn('[uploadFile] R2 not configured, waiting chatium...');
|
|
1071
|
+
const r = await chatiumSafe;
|
|
1072
|
+
if (r.ok) return r.result;
|
|
1073
|
+
throw r.error;
|
|
1074
|
+
}
|
|
1075
|
+
const r2 = await r2Upload.uploadToR2(buffer, mime, _extFromFilename(filename));
|
|
1076
|
+
return {
|
|
1077
|
+
url: r2.url, fileName: filename, size: buffer.length, hash: r2.key, provider: 'cloudflare-r2',
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function _extFromFilename(name) {
|
|
1082
|
+
const m = String(name || '').match(/\.([a-zA-Z0-9]+)$/);
|
|
1083
|
+
return m ? m[1].toLowerCase() : null;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1013
1086
|
/**
|
|
1014
1087
|
* @returns {Promise<{ url, fileName, size, hash?: string, provider }>}
|
|
1015
1088
|
*/
|
|
@@ -1020,18 +1093,7 @@ async function uploadFile({ buffer, filename = 'upload.bin', mime = 'application
|
|
|
1020
1093
|
}
|
|
1021
1094
|
|
|
1022
1095
|
if (s.useChatium && s.chatium?.token && s.chatium?.base) {
|
|
1023
|
-
|
|
1024
|
-
const ru = await fetch(urlReq, { headers: { 'Authorization': `Bearer ${s.chatium.token}` } });
|
|
1025
|
-
if (!ru.ok) throw new Error('KingKont upload-url: ' + (await ru.text()).slice(0, 200));
|
|
1026
|
-
const { uploadUrl } = await ru.json();
|
|
1027
|
-
if (!uploadUrl) throw new Error('KingKont не вернул uploadUrl');
|
|
1028
|
-
logCall('POST', 'Chatium', uploadUrl, `file=${filename} size=${buffer.length}`);
|
|
1029
|
-
const fd = new FormData();
|
|
1030
|
-
fd.append('Filedata', new Blob([buffer], { type: mime }), filename);
|
|
1031
|
-
const ru2 = await fetch(uploadUrl, { method: 'POST', body: fd });
|
|
1032
|
-
const hash = (await ru2.text()).trim();
|
|
1033
|
-
if (!ru2.ok || !hash) throw new Error(`Chatium upload HTTP ${ru2.status}: ${hash.slice(0, 200)}`);
|
|
1034
|
-
return { url: `${CHATIUM_CDN}/${hash}`, fileName: filename, size: buffer.length, hash, provider: 'kingkont' };
|
|
1096
|
+
return await _uploadViaChatiumWithR2Fallback({ buffer, filename, mime, s });
|
|
1035
1097
|
}
|
|
1036
1098
|
|
|
1037
1099
|
if (!s.useKie) throw new Error('Войдите в KingKont или KIE для загрузки файлов.');
|
package/lib/r2-upload.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// lib/r2-upload.js — загрузка файла в Cloudflare R2 через Cloudflare API (Bearer).
|
|
2
|
+
//
|
|
3
|
+
// Используется как fallback когда chatium-storage медленно / недоступно
|
|
4
|
+
// (см. uploadFile в providers.js → race с 5s timeout).
|
|
5
|
+
//
|
|
6
|
+
// Credentials хардкодим (юзер: «дам наш ключ, не надо чтобы каждый
|
|
7
|
+
// настраивал»). Если в будущем нужно будет дать юзерам кастомизировать —
|
|
8
|
+
// перенесём в settings.json.
|
|
9
|
+
//
|
|
10
|
+
// Способ auth: Cloudflare Account API Token (cfat_...) → Bearer header.
|
|
11
|
+
// Это проще чем S3-protocol SigV4 (нет хитрой подписи), и token-based
|
|
12
|
+
// auth тривиальнее ротировать в случае утечки.
|
|
13
|
+
//
|
|
14
|
+
// API endpoint:
|
|
15
|
+
// PUT https://api.cloudflare.com/client/v4/accounts/<account>/r2/buckets/<bucket>/objects/<key>
|
|
16
|
+
// Authorization: Bearer <token>
|
|
17
|
+
// Content-Type: <mime>
|
|
18
|
+
// body: <bytes>
|
|
19
|
+
// → 200 OK { success:true, result:{key, size, etag} }
|
|
20
|
+
//
|
|
21
|
+
// Public read через managed-domain (pub-<bucket-id>.r2.dev), включён через
|
|
22
|
+
// API (PUT /r2/buckets/<b>/domains/managed { enabled:true }).
|
|
23
|
+
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
|
|
26
|
+
const R2 = {
|
|
27
|
+
accountId: 'dd50e50ba5d89ce42553cfb2f5d6ee40',
|
|
28
|
+
apiToken: 'cfat_8HndHS0KMqecVBsjpM5Se2VbLQIvSw2ipCDnhtDm1502679f',
|
|
29
|
+
bucket: 'kingkont',
|
|
30
|
+
publicBase: 'https://pub-cd4114af9f7d44c9bf8c9442bc7dddc2.r2.dev',
|
|
31
|
+
};
|
|
32
|
+
// Env override (для dev / тестов / ротации без релиза).
|
|
33
|
+
if (process.env.R2_ACCOUNT_ID) R2.accountId = process.env.R2_ACCOUNT_ID;
|
|
34
|
+
if (process.env.R2_API_TOKEN) R2.apiToken = process.env.R2_API_TOKEN;
|
|
35
|
+
if (process.env.R2_BUCKET) R2.bucket = process.env.R2_BUCKET;
|
|
36
|
+
if (process.env.R2_PUBLIC_BASE) R2.publicBase = process.env.R2_PUBLIC_BASE;
|
|
37
|
+
|
|
38
|
+
function isConfigured() {
|
|
39
|
+
return !!(R2.accountId && R2.apiToken && R2.bucket && R2.publicBase);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Залить файл в R2 и вернуть публичную URL'ку.
|
|
44
|
+
* @param {Buffer} buffer
|
|
45
|
+
* @param {string} mime
|
|
46
|
+
* @param {string} [ext]
|
|
47
|
+
* @returns {Promise<{ url: string, key: string, size: number }>}
|
|
48
|
+
*/
|
|
49
|
+
async function uploadToR2(buffer, mime = 'application/octet-stream', ext) {
|
|
50
|
+
if (!isConfigured()) throw new Error('R2 не настроен');
|
|
51
|
+
if (!buffer || !buffer.length) throw new Error('пустой файл');
|
|
52
|
+
|
|
53
|
+
const finalExt = (ext || _extFromMime(mime) || 'bin').replace(/[^a-z0-9]/gi, '').slice(0, 8) || 'bin';
|
|
54
|
+
// UUID-based — юзер просил без content-hash дедупа.
|
|
55
|
+
const key = `${crypto.randomUUID()}.${finalExt}`;
|
|
56
|
+
|
|
57
|
+
const url = `https://api.cloudflare.com/client/v4/accounts/${R2.accountId}/r2/buckets/${R2.bucket}/objects/${encodeURIComponent(key)}`;
|
|
58
|
+
|
|
59
|
+
const t0 = Date.now();
|
|
60
|
+
const r = await fetch(url, {
|
|
61
|
+
method: 'PUT',
|
|
62
|
+
headers: {
|
|
63
|
+
'Authorization': `Bearer ${R2.apiToken}`,
|
|
64
|
+
'Content-Type': mime,
|
|
65
|
+
},
|
|
66
|
+
body: buffer,
|
|
67
|
+
});
|
|
68
|
+
if (!r.ok) {
|
|
69
|
+
const text = await r.text().catch(() => '');
|
|
70
|
+
throw new Error(`R2 PUT ${r.status}: ${text.slice(0, 300)}`);
|
|
71
|
+
}
|
|
72
|
+
// CF API возвращает {success, result}. Проверяем флаг для надёжности.
|
|
73
|
+
const data = await r.json().catch(() => ({}));
|
|
74
|
+
if (data && data.success === false) {
|
|
75
|
+
throw new Error(`R2 PUT не success: ${JSON.stringify(data.errors || data).slice(0, 300)}`);
|
|
76
|
+
}
|
|
77
|
+
const publicUrl = `${R2.publicBase.replace(/\/$/, '')}/${key}`;
|
|
78
|
+
console.log(`[r2-upload] OK ${key} (${buffer.length}B, ${mime}) in ${Date.now() - t0}ms → ${publicUrl}`);
|
|
79
|
+
return { url: publicUrl, key, size: buffer.length };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _extFromMime(mime) {
|
|
83
|
+
return ({
|
|
84
|
+
'image/jpeg': 'jpg', 'image/jpg': 'jpg',
|
|
85
|
+
'image/png': 'png', 'image/webp': 'webp', 'image/gif': 'gif', 'image/avif': 'avif',
|
|
86
|
+
'video/mp4': 'mp4', 'video/quicktime': 'mov', 'video/webm': 'webm',
|
|
87
|
+
'audio/mpeg': 'mp3', 'audio/wav': 'wav', 'audio/mp4': 'm4a', 'audio/x-m4a': 'm4a',
|
|
88
|
+
'text/plain': 'txt', 'text/markdown': 'md',
|
|
89
|
+
'application/octet-stream': 'bin',
|
|
90
|
+
})[mime] || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { uploadToR2, isConfigured };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kingkont",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.30",
|
|
4
4
|
"description": "KingKont \u00b7 Chatium \u2014 \u043d\u043e\u0434-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0441\u0446\u0435\u043d \u0441 AI-\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0435\u0439 (\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438/\u0432\u0438\u0434\u0435\u043e/\u0433\u043e\u043b\u043e\u0441/SFX/\u043c\u0443\u0437\u044b\u043a\u0430/\u0442\u0435\u043a\u0441\u0442)",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|