feat: capture binary files for browser inbox

This commit is contained in:
2026-06-29 10:12:26 +08:00
parent 8970333d13
commit 7b4db61a9c
8 changed files with 43 additions and 15 deletions
+2 -1
View File
@@ -50,7 +50,8 @@
fileName: info.fileName || '',
fileMime: info.fileMime || '',
fileSize: info.fileSize || 0,
fileText: info.fileText || ''
fileText: info.fileText || '',
fileDataBase64: info.fileDataBase64 || ''
});
}
+1 -1
View File
@@ -37,7 +37,7 @@
</section>
<section class="file-picker">
<label for="file-input">Text file</label>
<label for="file-input">File</label>
<input id="file-input" type="file">
</section>
+25 -5
View File
@@ -10,6 +10,24 @@
var pendingCountEl = document.getElementById('pending-count');
var statusDotEl = document.getElementById('status-dot');
var MAX_FILE_TEXT_LENGTH = 2 * 1024 * 1024;
var MAX_FILE_BYTES = 8 * 1024 * 1024;
function arrayBufferToBase64(buffer) {
var bytes = new Uint8Array(buffer);
var chunkSize = 0x8000;
var binary = '';
for (var i = 0; i < bytes.length; i += chunkSize) {
var chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
function readOptionalText(file) {
if (file.size > MAX_FILE_TEXT_LENGTH) return Promise.resolve('');
if (file.type && file.type.indexOf('text/') !== 0 && file.type !== 'application/json') return Promise.resolve('');
return file.text().catch(function () { return ''; });
}
function setStatus(text) {
statusEl.textContent = text;
@@ -75,19 +93,21 @@
setStatus('Choose a text file first');
return;
}
if (file.size > MAX_FILE_TEXT_LENGTH) {
setStatus('File is too large for text capture');
if (file.size > MAX_FILE_BYTES) {
setStatus('File is too large for browser capture');
return;
}
setStatus('Reading file...');
file.text().then(function (content) {
Promise.all([file.arrayBuffer(), readOptionalText(file)]).then(function (results) {
var content = results[1] || '';
send({
type: 'verstak.capture',
kind: 'file',
fileName: file.name,
fileMime: file.type || 'text/plain',
fileMime: file.type || '',
fileSize: file.size,
fileText: content
fileText: content,
fileDataBase64: arrayBufferToBase64(results[0])
});
}).catch(function (err) {
setStatus(err && err.message ? err.message : String(err));
+6 -2
View File
@@ -4,6 +4,7 @@
var CAPTURE_SCHEMA_VERSION = 1;
var DEFAULT_RECEIVER_URL = 'http://127.0.0.1:47731/api/browser-inbox/v1/captures';
var MAX_FILE_TEXT_LENGTH = 2 * 1024 * 1024;
var MAX_FILE_BYTES = 8 * 1024 * 1024;
function nowIso() {
return new Date().toISOString();
@@ -73,7 +74,8 @@
name: cleanString(input.fileName || '', 255),
mime: cleanString(input.fileMime || '', 128),
size: Number.isFinite(fileSize) && fileSize >= 0 ? fileSize : fileText.length,
text: fileText
text: fileText,
dataBase64: cleanString(input.fileDataBase64 || '', Math.ceil(MAX_FILE_BYTES * 4 / 3) + 4)
};
}
if (input.context) payload.context = input.context;
@@ -90,7 +92,8 @@
if (payload.kind === 'selection' && (!payload.selection || !payload.selection.text)) throw new Error('selection.text is required');
if (payload.kind === 'link' && (!payload.link || !payload.link.url)) throw new Error('link.url is required');
if (payload.kind === 'file' && (!payload.file || !payload.file.name)) throw new Error('file.name is required');
if (payload.kind === 'file' && (!payload.file || !payload.file.text)) throw new Error('file.text is required');
if (payload.kind === 'file' && Number(payload.file.size || 0) > MAX_FILE_BYTES) throw new Error('file.size exceeds limit');
if (payload.kind === 'file' && (!payload.file || (!payload.file.text && !payload.file.dataBase64))) throw new Error('file.text or file.dataBase64 is required');
return true;
}
@@ -98,6 +101,7 @@
CAPTURE_SCHEMA_VERSION: CAPTURE_SCHEMA_VERSION,
DEFAULT_RECEIVER_URL: DEFAULT_RECEIVER_URL,
MAX_FILE_TEXT_LENGTH: MAX_FILE_TEXT_LENGTH,
MAX_FILE_BYTES: MAX_FILE_BYTES,
buildCapture: buildCapture,
validateCapture: validateCapture
};