kingkont 0.20.30 → 0.20.32
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 +10 -0
- package/lib/r2-upload.js +26 -66
- package/package.json +1 -1
- package/settings.html +19 -0
package/lib/providers.js
CHANGED
|
@@ -1092,6 +1092,16 @@ async function uploadFile({ buffer, filename = 'upload.bin', mime = 'application
|
|
|
1092
1092
|
throw new Error(`файл слишком большой (${(buffer.length/1024/1024).toFixed(1)} MB), лимит 50MB`);
|
|
1093
1093
|
}
|
|
1094
1094
|
|
|
1095
|
+
// Явный override: «Использовать Cloudflare для загрузки» — каждый файл сразу
|
|
1096
|
+
// идёт в R2, без chatium-race. Чекбокс в settings.html, юзер сам решает.
|
|
1097
|
+
if (s.useCloudflareR2 && r2Upload.isConfigured()) {
|
|
1098
|
+
console.log('[uploadFile] useCloudflareR2=true → R2 directly');
|
|
1099
|
+
const r2 = await r2Upload.uploadToR2(buffer, mime, _extFromFilename(filename));
|
|
1100
|
+
return {
|
|
1101
|
+
url: r2.url, fileName: filename, size: buffer.length, hash: r2.key, provider: 'cloudflare-r2',
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1095
1105
|
if (s.useChatium && s.chatium?.token && s.chatium?.base) {
|
|
1096
1106
|
return await _uploadViaChatiumWithR2Fallback({ buffer, filename, mime, s });
|
|
1097
1107
|
}
|
package/lib/r2-upload.js
CHANGED
|
@@ -1,93 +1,53 @@
|
|
|
1
|
-
// lib/r2-upload.js — загрузка файла в
|
|
1
|
+
// lib/r2-upload.js — загрузка файла в R2 через Cloudflare Worker upload-proxy.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Worker задеплоен в `cf-worker/` (см. README там). Он принимает POST,
|
|
4
|
+
// складывает body в R2-бакет `kingkont`, отвечает {url, key, size}.
|
|
5
|
+
// Public-domain бакета: pub-cd4114af9f7d44c9bf8c9442bc7dddc2.r2.dev — file
|
|
6
|
+
// доступен по {url}.
|
|
5
7
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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 }).
|
|
8
|
+
// Раньше тут была прямая запись через Cloudflare API с Account API token.
|
|
9
|
+
// Токен был закоммичен в публичный npm-пакет → CF auto-revoked secret
|
|
10
|
+
// scanner'ом за час. Worker-подход решает проблему: URL Worker'а публичный
|
|
11
|
+
// и не секретный, R2-credentials живут только в R2-binding на стороне CF.
|
|
23
12
|
|
|
24
|
-
const
|
|
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;
|
|
13
|
+
const WORKER_URL = (process.env.R2_WORKER_URL || 'https://kingkont-r2-upload.timur-dd5.workers.dev').replace(/\/$/, '');
|
|
37
14
|
|
|
38
15
|
function isConfigured() {
|
|
39
|
-
return !!
|
|
16
|
+
return !!WORKER_URL && WORKER_URL !== '<NOT_CONFIGURED>';
|
|
40
17
|
}
|
|
41
18
|
|
|
42
19
|
/**
|
|
43
|
-
* Залить файл в R2 и вернуть публичную URL'ку.
|
|
20
|
+
* Залить файл в R2 через Worker и вернуть публичную URL'ку.
|
|
44
21
|
* @param {Buffer} buffer
|
|
45
22
|
* @param {string} mime
|
|
46
23
|
* @param {string} [ext]
|
|
47
24
|
* @returns {Promise<{ url: string, key: string, size: number }>}
|
|
48
25
|
*/
|
|
49
26
|
async function uploadToR2(buffer, mime = 'application/octet-stream', ext) {
|
|
50
|
-
if (!isConfigured()) throw new Error('R2 не настроен');
|
|
27
|
+
if (!isConfigured()) throw new Error('R2 Worker URL не настроен');
|
|
51
28
|
if (!buffer || !buffer.length) throw new Error('пустой файл');
|
|
52
29
|
|
|
53
|
-
const
|
|
54
|
-
|
|
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)}`;
|
|
30
|
+
const headers = { 'Content-Type': mime };
|
|
31
|
+
if (ext) headers['X-Ext'] = String(ext).replace(/[^a-z0-9]/gi, '').slice(0, 8);
|
|
58
32
|
|
|
59
33
|
const t0 = Date.now();
|
|
60
|
-
const r = await fetch(
|
|
61
|
-
method: '
|
|
62
|
-
headers
|
|
63
|
-
'Authorization': `Bearer ${R2.apiToken}`,
|
|
64
|
-
'Content-Type': mime,
|
|
65
|
-
},
|
|
34
|
+
const r = await fetch(`${WORKER_URL}/upload`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers,
|
|
66
37
|
body: buffer,
|
|
67
38
|
});
|
|
39
|
+
|
|
68
40
|
if (!r.ok) {
|
|
69
41
|
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)}`);
|
|
42
|
+
throw new Error(`R2 worker PUT ${r.status}: ${text.slice(0, 300)}`);
|
|
76
43
|
}
|
|
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
44
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'text/plain': 'txt', 'text/markdown': 'md',
|
|
89
|
-
'application/octet-stream': 'bin',
|
|
90
|
-
})[mime] || null;
|
|
45
|
+
const data = await r.json();
|
|
46
|
+
if (!data || !data.url) {
|
|
47
|
+
throw new Error(`R2 worker вернул без url: ${JSON.stringify(data).slice(0, 200)}`);
|
|
48
|
+
}
|
|
49
|
+
console.log(`[r2-upload] OK ${data.key} (${data.size}B, ${mime}) in ${Date.now() - t0}ms → ${data.url}`);
|
|
50
|
+
return { url: data.url, key: data.key, size: data.size };
|
|
91
51
|
}
|
|
92
52
|
|
|
93
53
|
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.32",
|
|
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": {
|
package/settings.html
CHANGED
|
@@ -169,6 +169,23 @@
|
|
|
169
169
|
</div>
|
|
170
170
|
</div>
|
|
171
171
|
|
|
172
|
+
<h2>Загрузка референсов</h2>
|
|
173
|
+
<div class="card">
|
|
174
|
+
<label class="use-toggle" for="useCloudflareR2">
|
|
175
|
+
<input type="checkbox" id="useCloudflareR2" />
|
|
176
|
+
<span class="label-text">
|
|
177
|
+
Использовать Cloudflare для загрузки
|
|
178
|
+
<span class="sub">— upload референсов идёт в R2-бакет KingKont вместо chatium-storage</span>
|
|
179
|
+
</span>
|
|
180
|
+
</label>
|
|
181
|
+
<div class="hint">
|
|
182
|
+
По умолчанию (выключено) — upload идёт в <code>fs.chatium.ru</code>, и
|
|
183
|
+
если он медленнее 5 секунд, автоматически уходит в Cloudflare R2.
|
|
184
|
+
Если включить — chatium-storage не используется вообще, каждый
|
|
185
|
+
файл сразу идёт в R2 (<code>pub-cd4114…r2.dev</code>).
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
172
189
|
<h2>Для Claude</h2>
|
|
173
190
|
<div class="card">
|
|
174
191
|
<div style="font-size:13px; color:var(--text); margin-bottom:8px;">
|
|
@@ -217,6 +234,7 @@ async function loadSettings() {
|
|
|
217
234
|
$('useOpenrouter').checked = useDefault(s.useOpenrouter);
|
|
218
235
|
$('useElevenlabs').checked = useDefault(s.useElevenlabs);
|
|
219
236
|
$('useKie').checked = useDefault(s.useKie);
|
|
237
|
+
$('useCloudflareR2').checked = useDefault(s.useCloudflareR2);
|
|
220
238
|
|
|
221
239
|
// Применяем visual-disabled к блокам с выключенным toggle
|
|
222
240
|
syncProviderVisuals();
|
|
@@ -246,6 +264,7 @@ $('save').addEventListener('click', async () => {
|
|
|
246
264
|
useOpenrouter: $('useOpenrouter').checked,
|
|
247
265
|
useElevenlabs: $('useElevenlabs').checked,
|
|
248
266
|
useKie: $('useKie').checked,
|
|
267
|
+
useCloudflareR2: $('useCloudflareR2').checked,
|
|
249
268
|
});
|
|
250
269
|
const saved = $('saved');
|
|
251
270
|
saved.classList.add('show');
|