ocr-based-qwen/worker.js
2025-01-11 12:05:59 +08:00

773 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
// 处理 CORS 预检请求
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
// 处理 POST 请求
if (request.method === 'POST' && url.pathname === '/recognize') {
try {
const { token, imageId } = await request.json();
if (!token || !imageId) {
return new Response(JSON.stringify({ error: 'Missing token or imageId' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// 调用 QwenLM API
const response = await fetch('https://chat.qwenlm.ai/api/chat/completions', {
method: 'POST',
headers: {
'accept': '*/*',
'authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
stream: false,
model: 'qwen-vl-max-latest',
messages: [
{
role: 'user',
content: [
{ type: 'text', text: '请严格只返回图片中的内容,不要添加任何解释、描述或多余的文字' },
{ type: 'image', image: imageId }, // 使用上传后的图片 ID
],
},
],
session_id: '1',
chat_id: '2',
id: '3',
}),
});
const data = await response.json();
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
// 返回前端界面
return new Response(getHTML(), {
headers: { 'Content-Type': 'text/html' },
});
}
function getHTML() {
return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能图片识别</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: rgba(255, 255, 255, 0.95);
padding: 2.5rem;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
width: 90%;
max-width: 800px;
transition: all 0.3s ease;
}
h1 {
color: #2c3e50;
margin-bottom: 1.5rem;
font-size: 1.8rem;
text-align: center;
}
.upload-area {
border: 2px dashed #8e9eab;
border-radius: 12px;
padding: 2rem;
text-align: center;
transition: all 0.3s ease;
margin-bottom: 1.5rem;
cursor: pointer;
position: relative;
overflow: hidden;
}
.upload-area:hover {
border-color: #3498db;
background: rgba(52, 152, 219, 0.05);
}
.upload-area.dragover {
border-color: #3498db;
background: rgba(52, 152, 219, 0.1);
transform: scale(1.02);
}
.upload-area i {
font-size: 2rem;
color: #8e9eab;
margin-bottom: 1rem;
}
.upload-text {
color: #7f8c8d;
font-size: 0.9rem;
}
#tokens {
width: 100%;
padding: 0.8rem;
border: 1px solid #dcdde1;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
resize: none;
}
.result-container {
margin-top: 1.5rem;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
}
.result-container.show {
opacity: 1;
transform: translateY(0);
}
.result {
background: #f8f9fa;
padding: 1.2rem;
border-radius: 8px;
color: #2c3e50;
font-size: 1rem;
line-height: 1.6;
white-space: pre-wrap;
}
.loading {
display: none;
text-align: center;
margin: 1rem 0;
}
.loading::after {
content: '';
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid #3498db;
border-radius: 50%;
border-top-color: transparent;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.preview-image {
max-width: 100%;
max-height: 200px;
margin: 1rem 0;
border-radius: 8px;
display: none;
}
/* 侧边栏样式 */
.sidebar {
position: fixed;
right: -300px;
top: 0;
width: 300px;
height: 100vh;
background: white;
box-shadow: -5px 0 15px rgba(0, 0, 0, 0.1);
transition: right 0.3s ease;
padding: 20px;
z-index: 1000;
}
.sidebar.open {
right: 0;
}
.sidebar-toggle {
position: fixed;
right: 20px;
top: 20px;
background: #3498db;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
z-index: 1001;
}
.token-list {
margin-top: 20px;
}
.token-item {
background: #f8f9fa;
padding: 10px;
margin-bottom: 10px;
border-radius: 5px;
cursor: pointer;
word-break: break-all;
}
.token-item:hover {
background: #e9ecef;
}
#tokenInput {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #dcdde1;
border-radius: 5px;
}
.save-btn {
background: #3498db;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
width: 100%;
}
/* 历史记录样式 */
.history-container {
margin-top: 2rem;
border-top: 1px solid #eee;
padding-top: 1rem;
}
.history-title {
color: #2c3e50;
font-size: 1.2rem;
margin-bottom: 1rem;
}
.history-item {
display: flex;
align-items: flex-start;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 1rem;
}
.history-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
margin-right: 1rem;
}
.history-content {
flex: 1;
}
.history-text {
color: #2c3e50;
font-size: 0.9rem;
line-height: 1.4;
}
.history-time {
color: #7f8c8d;
font-size: 0.8rem;
margin-top: 0.5rem;
}
.no-history {
text-align: center;
color: #7f8c8d;
padding: 1rem;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
z-index: 2000;
cursor: pointer;
}
.modal-content {
max-width: 90%;
max-height: 90vh;
margin: auto;
display: block;
position: relative;
top: 50%;
transform: translateY(-50%);
}
/* 修改侧边栏样式 */
.sidebar {
position: fixed;
right: -300px; /* 改为右侧边栏 */
top: 0;
width: 300px;
height: 100vh;
background: white;
box-shadow: -5px 0 15px rgba(0, 0, 0, 0.1);
transition: right 0.3s ease;
padding: 20px;
z-index: 1000;
}
/* 添加左侧边栏样式 */
.history-sidebar {
position: fixed;
left: -300px;
top: 0;
width: 300px;
height: 100vh;
background: white;
box-shadow: 5px 0 15px rgba(0, 0, 0, 0.1);
transition: left 0.3s ease;
padding: 20px;
z-index: 1000;
overflow-y: auto;
}
.history-sidebar.open {
left: 0;
}
.history-toggle {
position: fixed;
left: 20px;
top: 20px;
background: #3498db;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
z-index: 1001;
}
/* 添加复制按钮样式 */
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.copy-btn {
background: #3498db;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.3s ease;
}
.copy-btn:hover {
background: #2980b9;
}
.copy-btn.copied {
background: #27ae60;
}
</style>
</head>
<body>
<button class="sidebar-toggle" id="sidebarToggle">⚙️ Token设置</button>
<div class="sidebar" id="sidebar">
<h2>Token 管理</h2>
<textarea id="tokenInput" placeholder="输入Token多个Token请用英文逗号分隔" rows="4"></textarea>
<button class="save-btn" id="saveTokens">保存</button>
<div class="token-list" id="tokenList"></div>
</div>
<div class="container">
<h1>智能图片识别</h1>
<div class="upload-area" id="uploadArea">
<i>📸</i>
<div class="upload-text">
拖拽图片到这里,或点击上传<br>
支持复制粘贴图片
</div>
<img id="previewImage" class="preview-image">
</div>
<div class="loading" id="loading"></div>
<div class="result-container" id="resultContainer">
<div class="result-header">
<span>识别结果</span>
<button class="copy-btn" id="copyBtn">复制结果</button>
</div>
<div class="result" id="result"></div>
</div>
<button class="history-toggle" id="historyToggle">📋 识别历史</button>
<div class="history-sidebar" id="historySidebar">
<h2>识别历史</h2>
<div id="historyList"></div>
</div>
</div>
<div id="imageModal" class="modal">
<img class="modal-content" id="modalImage">
</div>
<script>
// 首先定义类
function HistoryManager() {
this.maxHistory = 10;
}
// 添加原型方法
HistoryManager.prototype.getHistoryKey = function(token) {
return 'imageRecognition_history_' + token;
};
HistoryManager.prototype.loadHistory = function(token) {
const history = localStorage.getItem(this.getHistoryKey(token));
return history ? JSON.parse(history) : [];
};
HistoryManager.prototype.saveHistory = function(token, history) {
localStorage.setItem(this.getHistoryKey(token), JSON.stringify(history));
};
HistoryManager.prototype.addHistory = function(token, imageData, result) {
const history = this.loadHistory(token);
const newRecord = {
image: imageData,
result: result,
timestamp: new Date().toISOString()
};
history.unshift(newRecord);
if (history.length > this.maxHistory) {
history.pop();
}
this.saveHistory(token, history);
this.displayHistory(token);
};
HistoryManager.prototype.displayHistory = function(token) {
const history = this.loadHistory(token);
if (history.length === 0) {
historyList.innerHTML = '<div class="no-history">暂无识别历史</div>';
return;
}
var html = '';
for (var i = 0; i < history.length; i++) {
var record = history[i];
html += '<div class="history-item">';
html += '<img src="' + record.image + '" class="history-image" alt="历史图片" onclick="showFullImage(this.src)">';
html += '<div class="history-content">';
html += '<div class="history-text">' + record.result + '</div>';
html += '<div class="history-time">' + new Date(record.timestamp).toLocaleString() + '</div>';
html += '</div></div>';
}
historyList.innerHTML = html;
};
// 在 HistoryManager 类定义后添加 TokenManager 类
function TokenManager() {
this.currentIndex = 0;
}
TokenManager.prototype.getNextToken = function(tokens) {
if (!tokens || tokens.length === 0) return null;
// 轮询获取下一个token
const token = tokens[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % tokens.length;
return token;
};
// 初始化变量
const uploadArea = document.getElementById('uploadArea');
const tokensInput = document.getElementById('tokenInput');
const resultDiv = document.getElementById('result');
const resultContainer = document.getElementById('resultContainer');
const loading = document.getElementById('loading');
const previewImage = document.getElementById('previewImage');
const historyList = document.getElementById('historyList');
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebarToggle');
const tokenInput = document.getElementById('tokenInput');
const saveTokensBtn = document.getElementById('saveTokens');
const tokenList = document.getElementById('tokenList');
const historySidebar = document.getElementById('historySidebar');
const historyToggle = document.getElementById('historyToggle');
let currentToken = '';
let tokens = [];
const historyManager = new HistoryManager();
const tokenManager = new TokenManager();
// 从localStorage加载保存的tokens
function loadTokens() {
const savedTokens = localStorage.getItem('imageRecognitionTokens');
if (savedTokens) {
tokens = savedTokens.split(',');
updateTokenList();
if (tokens.length > 0) {
currentToken = tokens[0];
}
}
}
// 修改 updateTokenList 函数
function updateTokenList() {
tokenList.innerHTML = '';
tokens.forEach(function(token, index) {
var truncatedToken = token.slice(0, 10) + '...' + token.slice(-10);
var div = document.createElement('div');
div.className = 'token-item';
div.textContent = 'Token ' + (index + 1) + ': ' + truncatedToken;
div.addEventListener('click', function() {
currentToken = token;
historyManager.displayHistory(currentToken);
});
tokenList.appendChild(div);
});
tokenInput.value = tokens.join(',');
}
// 保存tokens
saveTokensBtn.addEventListener('click', () => {
const inputTokens = tokenInput.value.split(',').map(t => t.trim()).filter(t => t);
if (inputTokens.length > 0) {
tokens = inputTokens;
localStorage.setItem('imageRecognitionTokens', tokens.join(','));
updateTokenList();
currentToken = tokens[0];
alert('Tokens已保存');
} else {
alert('请至少输入一个有效的Token');
}
});
// 侧边栏开关
sidebarToggle.addEventListener('click', () => {
sidebar.classList.toggle('open');
});
// 处理文件上传和识别
async function processImage(file) {
// 使用 TokenManager 获取下一个可用的 token
const nextToken = tokenManager.getNextToken(tokens);
if (!nextToken) {
alert('请先设置Token');
sidebar.classList.add('open');
return;
}
currentToken = nextToken;
// 显示图片预览
const reader = new FileReader();
let imageData;
reader.onload = (e) => {
imageData = e.target.result;
previewImage.src = imageData;
previewImage.style.display = 'block';
};
reader.readAsDataURL(file);
// 显示加载动画
loading.style.display = 'block';
resultContainer.classList.remove('show');
try {
// 上传文件
const formData = new FormData();
formData.append('file', file);
const uploadResponse = await fetch('https://chat.qwenlm.ai/api/v1/files/', {
method: 'POST',
headers: {
'accept': 'application/json',
'authorization': 'Bearer ' + currentToken,
},
body: formData,
});
const uploadData = await uploadResponse.json();
if (!uploadData.id) throw new Error('文件上传失败');
// 识别图片
const recognizeResponse = await fetch('/recognize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: currentToken,
imageId: uploadData.id
}),
});
const recognizeData = await recognizeResponse.json();
// 提取并显示识别结果
const result = recognizeData.choices[0]?.message?.content || '识别失败';
resultDiv.textContent = result;
resultContainer.classList.add('show');
copyBtn.textContent = '复制结果';
copyBtn.classList.remove('copied');
// 添加到历史记录
historyManager.addHistory(currentToken, imageData, result);
} catch (error) {
resultDiv.textContent = '处理失败: ' + error.message;
resultContainer.classList.add('show');
copyBtn.textContent = '复制结果';
copyBtn.classList.remove('copied');
} finally {
loading.style.display = 'none';
}
}
// 文件拖放处理
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file && file.type.startsWith('image/')) {
processImage(file);
}
});
// 点击上传
uploadArea.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) processImage(file);
};
input.click();
});
// 粘贴处理
document.addEventListener('paste', (e) => {
const file = e.clipboardData.files[0];
if (file && file.type.startsWith('image/')) {
processImage(file);
}
});
// 初始化
loadTokens();
if (currentToken) {
historyManager.displayHistory(currentToken);
}
const modal = document.getElementById('imageModal');
const modalImg = document.getElementById('modalImage');
function showFullImage(src) {
modal.style.display = "block";
modalImg.src = src;
}
// 点击模态框关闭
modal.onclick = function() {
modal.style.display = "none";
}
// ESC 键关闭模态框
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modal.style.display === 'block') {
modal.style.display = 'none';
}
});
// 左侧历史记录边栏开关
historyToggle.addEventListener('click', () => {
historySidebar.classList.toggle('open');
});
const copyBtn = document.getElementById('copyBtn');
// 复制结果功能
copyBtn.addEventListener('click', async () => {
const result = resultDiv.textContent;
try {
await navigator.clipboard.writeText(result);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制结果';
copyBtn.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('复制失败:', err);
}
});
</script>
</body>
</html>
`;
}