Compare commits

..

No commits in common. "3b30c425ab3d3d9e7b0fa2e67749dfa674d347dc" and "aacdce569ed7fa711b9b925b002e46a161fafb8d" have entirely different histories.

20 changed files with 17 additions and 1417 deletions

Binary file not shown.

4
app.py
View File

@ -27,10 +27,6 @@ def create_app():
def index():
return render_template('index.html')
@app.route('/ocr')
def ocr():
return render_template('ocr.html')
# 自动创建数据库表
with app.app_context():
print("🔧 正在检查并创建数据库表...")

View File

@ -5,7 +5,6 @@ import requests
import io
import threading
import time
import base64
from flask import Blueprint, request, jsonify, session, current_app
from urllib.parse import quote
from config import Config
@ -178,8 +177,6 @@ def generate():
# 获取模型及对应的消耗积分
model_value = data.get('model')
is_chat_model = "gemini" in model_value.lower() or "gpt" in model_value.lower()
model_dict = SystemDict.query.filter_by(dict_type='ai_model', value=model_value).first()
cost = model_dict.cost if model_dict else 1
@ -215,63 +212,6 @@ def generate():
if model == "nano-banana-2" and size:
payload["image_size"] = size
# 如果是聊天模型,重新构建 payload
if is_chat_model:
messages = data.get('messages')
# 核心修复:将图片 URL 转换为 Base64解决第三方接口禁止 9000 端口访问的问题
def url_to_base64(url):
if not url or not url.startswith('http'):
return url
if ':9000' not in url:
return url
try:
# 尝试通过 S3 客户端直接读取(更安全,避开网络回环问题)
filename = url.split('/')[-1]
from urllib.parse import unquote
filename = unquote(filename)
resp = s3_client.get_object(Bucket=Config.MINIO["bucket"], Key=filename)
content = resp['Body'].read()
mime_type = resp.get('ContentType', 'image/jpeg')
encoded_string = base64.b64encode(content).decode('utf-8')
return f"data:{mime_type};base64,{encoded_string}"
except Exception as e:
print(f"⚠️ Base64 转换失败: {e}")
return url
if not messages:
# 兼容性处理:如果没有 messages构建一个简单的
messages = [
{"role": "system", "content": data.get('system_prompt', "You are a helpful assistant.")},
{"role": "user", "content": [
{"type": "text", "text": prompt}
]}
]
for img_url in input_img_urls:
messages[1]["content"].append({"type": "image_url", "image_url": {"url": url_to_base64(img_url)}})
else:
# 递归处理传入的 messages
for msg in messages:
content = msg.get('content')
if isinstance(content, list):
for item in content:
if isinstance(item, dict) and item.get('type') == 'image_url':
img_info = item.get('image_url')
if img_info and 'url' in img_info:
img_info['url'] = url_to_base64(img_info['url'])
payload = {
"model": model,
"messages": messages,
"stream": False
}
target_api = Config.CHAT_API
if not mode == 'key':
# 使用生图默认的试用/优质 Key
api_key = Config.PREMIUM_KEY if is_premium else Config.TRIAL_KEY
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
resp = requests.post(target_api, json=payload, headers=headers, timeout=300)
@ -284,30 +224,6 @@ def generate():
return jsonify({"error": resp.text}), resp.status_code
api_result = resp.json()
if is_chat_model:
# 聊天模型返回的是文本
content = api_result['choices'][0]['message']['content']
# 核心修复如果是验光单解读OCR不存入生图历史记录避免污染生图历史
if prompt != "解读验光单":
new_record = GenerationRecord(
user_id=session.get('user_id'),
prompt=prompt,
model=model,
image_urls=json.dumps([{"type": "text", "content": content}])
)
db.session.add(new_record)
db.session.commit()
system_logger.info(f"用户生成文本成功", phone=user.phone, model=model, record_id=new_record.id)
else:
system_logger.info(f"用户解析验光单成功", phone=user.phone, model=model)
return jsonify({
"data": [{"content": content, "type": "text"}],
"message": "解析成功!"
})
raw_urls = [item['url'] for item in api_result.get('data', [])]
# 立即写入数据库(先存原始 URL
@ -408,8 +324,7 @@ def get_history():
pagination = GenerationRecord.query.filter(
GenerationRecord.user_id == user_id,
GenerationRecord.created_at >= ninety_days_ago,
GenerationRecord.prompt != "解读验光单" # 过滤掉验光单助手的操作记录
GenerationRecord.created_at >= ninety_days_ago
).order_by(GenerationRecord.created_at.desc())\
.paginate(page=page, per_page=per_page, error_out=False)

View File

@ -173,7 +173,6 @@ def get_menu():
# 菜单定义库:名称, 图标, 链接, 所需权限
all_menus = [
{"name": "创作工作台", "icon": "wand-2", "url": "/", "perm": None},
{"name": "验光单助手", "icon": "scan-eye", "url": "/ocr", "perm": None},
{"name": "购买积分", "icon": "shopping-cart", "url": "/buy", "perm": None},
{"name": "权限管理中心", "icon": "shield-check", "url": "/rbac", "perm": "manage_rbac"},
{"name": "系统字典管理", "icon": "book-open", "url": "/dicts", "perm": "manage_dicts"},

View File

@ -1,26 +0,0 @@
// config.js
const AppConfig = {
API_URL: 'https://ai.comfly.chat/v1/chat/completions',
//MODEL: 'gemini-3-pro-preview', // 你要求的模型
//MODEL: 'gemini-2.5-pro',
MODEL: 'gemini-3-flash-preview',
// 这里填入第一步生成的“加密Key”
ENCRYPTED_KEY: "MARAXis7BiwzBDwiLQIjLHNRWwwbWQAKDSFyAiQEYx8iIy8QUHNRAQkoJhMdCRRRWAwJ",
// 解密函数(必须与加密算法对应)
getDecryptedKey: function() {
const salt = "ComflyChatSecret2025"; // 必须与加密时的 Salt 一致
try {
const raw = atob(this.ENCRYPTED_KEY);
let result = "";
for (let i = 0; i < raw.length; i++) {
const charCode = raw.charCodeAt(i) ^ salt.charCodeAt(i % salt.length);
result += String.fromCharCode(charCode);
}
return result;
} catch (e) {
console.error("Key 解密失败");
return "";
}
}
};

View File

@ -27,7 +27,6 @@ class Config:
# AI API 配置
AI_API = "https://ai.t8star.cn/v1/images/generations"
CHAT_API = "https://ai.comfly.chat/v1/chat/completions"
# 试用模式配置
TRIAL_API = "https://ai.comfly.chat/v1/images/generations"

View File

@ -82,49 +82,3 @@
[2026-01-12 23:09:28] INFO - 用户登录尝试
[2026-01-12 23:09:28] INFO - 用户登录成功
[2026-01-12 23:18:01] INFO - 用户生成图片成功
[2026-01-13 22:27:07] INFO - 用户生成图片成功
[2026-01-13 22:44:18] INFO - 积分预扣除 (普通试用)
[2026-01-13 22:44:21] WARNING - API 报错,积分已退还
[2026-01-13 22:46:45] INFO - 积分预扣除 (普通试用)
[2026-01-13 22:47:04] INFO - 用户生成文本成功
[2026-01-13 22:50:18] INFO - 积分预扣除 (普通试用)
[2026-01-13 22:50:36] INFO - 用户生成文本成功
[2026-01-13 22:57:55] INFO - 积分预扣除 (普通试用)
[2026-01-13 22:58:07] INFO - 用户生成文本成功
[2026-01-13 23:02:42] INFO - 积分预扣除 (普通试用)
[2026-01-13 23:02:55] INFO - 用户生成文本成功
[2026-01-13 23:08:26] INFO - 积分预扣除 (普通试用)
[2026-01-13 23:08:43] INFO - 用户生成文本成功
[2026-01-13 23:09:40] INFO - 用户登录尝试
[2026-01-13 23:09:40] INFO - 用户登录成功
[2026-01-13 23:11:23] INFO - 积分预扣除 (普通试用)
[2026-01-13 23:11:41] INFO - 用户生成文本成功
[2026-01-13 23:13:29] INFO - 用户登录尝试
[2026-01-13 23:13:30] INFO - 用户登录成功
[2026-01-13 23:14:33] INFO - 用户生成图片成功
[2026-01-13 23:17:57] INFO - 用户生成图片成功
[2026-01-13 23:18:03] INFO - 用户生成图片成功
[2026-01-13 23:18:15] INFO - 用户生成图片成功
[2026-01-13 23:20:10] INFO - 用户生成图片成功
[2026-01-13 23:20:11] INFO - 用户生成图片成功
[2026-01-13 23:20:11] INFO - 用户生成图片成功
[2026-01-13 23:20:14] INFO - 用户生成图片成功
[2026-01-13 23:21:19] INFO - 用户生成图片成功
[2026-01-13 23:21:21] INFO - 用户生成图片成功
[2026-01-13 23:21:21] INFO - 用户生成图片成功
[2026-01-13 23:21:22] INFO - 用户生成图片成功
[2026-01-13 23:24:14] INFO - 用户生成图片成功
[2026-01-13 23:24:15] INFO - 用户生成图片成功
[2026-01-13 23:24:16] INFO - 用户生成图片成功
[2026-01-13 23:24:17] INFO - 用户生成图片成功
[2026-01-13 23:30:04] INFO - 用户生成图片成功
[2026-01-13 23:30:04] INFO - 用户生成图片成功
[2026-01-13 23:30:06] INFO - 用户生成图片成功
[2026-01-13 23:48:04] INFO - 用户生成图片成功
[2026-01-13 23:48:05] INFO - 用户生成图片成功
[2026-01-13 23:48:07] INFO - 用户生成图片成功
[2026-01-13 23:52:15] INFO - 用户生成图片成功
[2026-01-13 23:52:15] INFO - 用户生成图片成功
[2026-01-13 23:52:22] INFO - 用户生成图片成功
[2026-01-13 23:59:01] INFO - 用户登录尝试
[2026-01-13 23:59:02] INFO - 用户登录成功

415
main.js
View File

@ -1,415 +0,0 @@
// main.js
// 状态管理
let imageList = [];
let finalMarkdown = "";
let extractedData = null;
let statusInterval = null; // 用于动态文字切换
// DOM 元素引用
const els = {
dropArea: document.getElementById('drop-area'),
fileInput: document.getElementById('file-input'),
placeholder: document.getElementById('upload-placeholder'),
previewGrid: document.getElementById('preview-grid'),
controlBar: document.getElementById('control-bar'),
imgCount: document.getElementById('img-count'),
clearBtn: document.getElementById('clear-btn'),
analyzeBtn: document.getElementById('analyze-btn'),
btnText: document.getElementById('btn-text'),
btnSpinner: document.getElementById('btn-spinner'),
// 结果区域视图
placeholderView: document.getElementById('placeholder-view'), // 初始欢迎页
loadingView: document.getElementById('loading-view'), // 加载动画页
loadingStatus: document.getElementById('loading-status-text'), // 动态提示文字
resultContent: document.getElementById('result-content'), // 结果显示页
resultScroll: document.getElementById('result-scroll'), // 滚动容器
copyBtn: document.getElementById('copy-btn')
};
// === 事件监听 ===
els.dropArea.addEventListener('click', (e) => {
if (e.target !== els.clearBtn && imageList.length < 2) els.fileInput.click();
});
els.fileInput.addEventListener('change', e => handleFiles(e.target.files));
document.addEventListener('paste', e => {
const items = e.clipboardData.items;
const files = [];
for (let item of items) {
if (item.type.indexOf('image') !== -1) files.push(item.getAsFile());
}
if (files.length > 0) handleFiles(files);
});
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(name => {
els.dropArea.addEventListener(name, e => { e.preventDefault(); e.stopPropagation(); });
});
els.dropArea.addEventListener('drop', e => handleFiles(e.dataTransfer.files));
els.clearBtn.addEventListener('click', (e) => {
e.stopPropagation();
resetImages();
});
// === 复制按钮逻辑 ===
els.copyBtn.addEventListener('click', async () => {
let textToCopy = "";
if (!extractedData) {
textToCopy = els.resultContent.innerText;
showCopyFeedback("⚠️ 未识别到数据,已复制全文");
} else {
textToCopy = generateCopyString(extractedData);
}
const success = await copyToClipboard(textToCopy);
if (success) {
showCopyFeedback(extractedData ? "✅ 格式化数据已复制" : "✅ 已复制全文");
} else {
prompt("浏览器禁止自动复制,请手动复制以下内容:", textToCopy);
}
});
// === 兼容性极强的复制函数 ===
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.warn("现代复制API失败尝试传统方法:", err);
try {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '0';
textarea.setAttribute('readonly', '');
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textarea);
return successful;
} catch (e) {
return false;
}
}
}
function showCopyFeedback(msg) {
const originalText = "📄 复制结果";
els.copyBtn.innerText = msg;
setTimeout(() => {
els.copyBtn.innerText = originalText;
}, 2000);
}
// === 高度定制的复制逻辑 ===
function generateCopyString(data) {
const r = data.right;
const l = data.left;
const tPd = data.total_pd;
const blocks = [];
const fmtDeg = (val) => {
if (!val || val === "未标明" || val.trim() === "") return null;
const num = parseFloat(val);
if (isNaN(num)) return null;
const deg = Math.round(num * 100);
if (deg === 0) return null;
if (deg < 0) return Math.abs(deg).toString();
if (deg > 0) return "+" + deg.toString();
return null;
};
const fmtCyl = (val) => fmtDeg(val);
const fmtRaw = (val) => (!val || val === "未标明" || val.trim() === "") ? null : val.trim();
const fmtPD = (val) => {
if (!val || val === "未标明" || val.trim() === "") return null;
const num = parseFloat(val);
if (isNaN(num)) return val.trim();
return num.toString();
};
const rS = fmtDeg(r.s), lS = fmtDeg(l.s);
const rC = fmtCyl(r.c), lC = fmtCyl(l.c);
const rA = rC ? fmtRaw(r.a) : null, lA = lC ? fmtRaw(l.a) : null;
const rAdd = fmtDeg(r.add), lAdd = fmtDeg(l.add);
const rPH = fmtRaw(r.ph), lPH = fmtRaw(l.ph);
const rPd = fmtPD(r.pd), lPd = fmtPD(l.pd), totalPd = fmtPD(tPd);
if (!rS && !rC && !lS && !lC) return "平光";
const isDoubleSame = (rS === lS) && (rC === lC) && (rA === lA);
if (isDoubleSame) {
let block = `双眼${rS || ""}`;
if (rC) block += `散光${rC}`;
if (rA) block += `轴位${rA}`;
blocks.push(block);
} else {
if (rS || rC) {
let block = `右眼${rS || ""}`;
if (rC) block += `散光${rC}`;
if (rA) block += `轴位${rA}`;
blocks.push(block);
}
if (lS || lC) {
let block = `左眼${lS || ""}`;
if (lC) block += `散光${lC}`;
if (lA) block += `轴位${lA}`;
blocks.push(block);
}
}
if (rAdd && lAdd && rAdd === lAdd) blocks.push(`ADD${rAdd}`);
else {
if (rAdd) blocks.push(`ADD${rAdd}`);
if (lAdd) blocks.push(`ADD${lAdd}`);
}
if (rPH) blocks.push(`瞳高右眼${rPH}`);
if (lPH) blocks.push(`瞳高左眼${lPH}`);
if (totalPd) blocks.push(`瞳距${totalPd}`);
else {
if (rPd) blocks.push(`右眼瞳距${rPd}`);
if (lPd) blocks.push(`左眼瞳距${lPd}`);
}
return blocks.join(' ');
}
// === 核心逻辑函数 ===
async function handleFiles(files) {
if (imageList.length >= 2) {
alert("最多只能上传 2 张图片,请先清空后再试。");
return;
}
const remainingSlots = 2 - imageList.length;
const filesProcess = Array.from(files).slice(0, remainingSlots);
for (let file of filesProcess) {
if (!file.type.startsWith('image/')) continue;
try {
const base64 = await readFileAsBase64(file);
const compressed = await compressImage(base64, 1024, 0.8);
imageList.push(compressed);
} catch (err) {
console.error("图片处理失败", err);
}
}
updatePreviewUI();
}
function resetImages() {
imageList = [];
updatePreviewUI();
els.fileInput.value = '';
}
function updatePreviewUI() {
const count = imageList.length;
if (count === 0) {
els.placeholder.classList.remove('hidden');
els.previewGrid.classList.add('hidden');
els.controlBar.classList.add('hidden');
} else {
els.placeholder.classList.add('hidden');
els.previewGrid.classList.remove('hidden');
els.controlBar.classList.remove('hidden');
}
els.previewGrid.innerHTML = '';
els.previewGrid.className = `w-full h-full grid gap-2 ${count > 1 ? 'grid-cols-2' : 'grid-cols-1'}`;
imageList.forEach((imgSrc) => {
const div = document.createElement('div');
div.className = "relative rounded-lg overflow-hidden border border-slate-200 bg-slate-100 flex items-center justify-center h-full max-h-[400px]";
div.innerHTML = `<img src="${imgSrc}" class="max-w-full max-h-full object-contain">`;
els.previewGrid.appendChild(div);
});
els.imgCount.innerText = `${count}/2`;
}
function readFileAsBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function compressImage(base64Str, maxDim, quality) {
return new Promise((resolve) => {
const img = new Image();
img.src = base64Str;
img.onload = () => {
let w = img.width, h = img.height;
if (w > maxDim || h > maxDim) {
if (w > h) { h = Math.round(h * maxDim / w); w = maxDim; }
else { w = Math.round(w * maxDim / h); h = maxDim; }
}
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
resolve(canvas.toDataURL('image/jpeg', quality));
};
});
}
// === 核心:设置加载状态 UI ===
function setLoading(isLoading) {
els.analyzeBtn.disabled = isLoading;
// 清除之前的定时器
if (statusInterval) {
clearInterval(statusInterval);
statusInterval = null;
}
if (isLoading) {
els.btnText.innerText = "正在分析...";
els.btnSpinner.classList.remove('hidden');
if (els.placeholderView) els.placeholderView.classList.add('hidden');
if (els.resultContent) els.resultContent.classList.add('hidden');
if (els.loadingView) els.loadingView.classList.remove('hidden');
const statuses = [
"正在上传并扫描验光单...",
"AI 正在识别视觉参数...",
"正在校对球镜与柱镜数值...",
"正在进行深度光学分析...",
"正在生成专业解读报告...",
"正在整理结果排版..."
];
let i = 0;
if (els.loadingStatus) {
els.loadingStatus.innerText = statuses[0];
statusInterval = setInterval(() => {
if (i < statuses.length - 1) {
i++;
els.loadingStatus.innerText = statuses[i];
} else {
// 播完了还没返回?进入“深度思考”省略号动画模式
clearInterval(statusInterval);
let dots = 0;
const baseText = statuses[statuses.length - 1];
statusInterval = setInterval(() => {
dots = (dots + 1) % 4;
els.loadingStatus.innerText = baseText + ".".repeat(dots);
}, 500);
}
}, 3000); // 3秒切换一次更真实
}
} else {
els.btnText.innerText = "开始智能解读";
els.btnSpinner.classList.add('hidden');
if (els.loadingView) els.loadingView.classList.add('hidden');
if (els.resultContent) els.resultContent.classList.remove('hidden');
}
}
// === API 调用逻辑 ===
els.analyzeBtn.addEventListener('click', async () => {
if (imageList.length === 0) return alert('请至少上传 1 张验光单');
const apiKey = AppConfig.getDecryptedKey();
if (!apiKey) return alert("配置错误:无法读取 API Key");
setLoading(true);
finalMarkdown = "";
extractedData = null;
els.resultContent.innerHTML = "";
try {
const userContent = AppPrompts.generateUserPayload(imageList);
const response = await fetch(AppConfig.API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model: AppConfig.MODEL,
stream: true,
messages: [
{ role: "system", content: AppPrompts.systemMessage },
{ role: "user", content: userContent }
]
})
});
if (!response.ok) throw new Error(`API 请求失败: ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let hasStartedStreaming = false;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
try {
const json = JSON.parse(line.substring(6));
const content = json.choices[0].delta?.content || "";
if (content && !hasStartedStreaming) {
hasStartedStreaming = true;
// 数据回来的一瞬间,立即停止加载动画和文字
if (els.loadingView) els.loadingView.classList.add('hidden');
els.resultContent.classList.remove('hidden');
if (statusInterval) clearInterval(statusInterval);
}
finalMarkdown += content;
// 实时解析 Markdown (过滤 JSON 块)
const displayMarkdown = finalMarkdown.replace(/```json[\s\S]*```/, '');
els.resultContent.innerHTML = marked.parse(displayMarkdown);
// 自动滚动到底部
if (els.resultScroll) {
els.resultScroll.scrollTop = els.resultScroll.scrollHeight;
}
} catch (e) {}
}
}
}
// 结束后提取 JSON 用于复制功能
const jsonMatch = finalMarkdown.match(/```json\n([\s\S]*?)\n```/);
if (jsonMatch && jsonMatch[1]) {
try {
extractedData = JSON.parse(jsonMatch[1]);
} catch (e) {}
}
els.copyBtn.disabled = false;
els.copyBtn.classList.remove('opacity-50', 'cursor-not-allowed');
} catch (err) {
els.resultContent.innerHTML = `<div class="p-4 bg-red-50 text-red-500 rounded-lg"><strong>出错了:</strong> ${err.message}</div>`;
if (els.loadingView) els.loadingView.classList.add('hidden');
els.resultContent.classList.remove('hidden');
} finally {
setLoading(false);
}
});

View File

@ -1,103 +0,0 @@
// prompts.js
const AppPrompts = {
systemMessage: `你是一个专业的眼科验光师助手,服务对象是眼镜店客服人员。
1. **基本原则**
- 使用简体中文
- 核心任务精准提取配镜数据S, C, A, PD
- **全能模式**你需要同时具备处理电脑验光单手写处方光学十字图的能力
2. ** 核心逻辑一视觉符号与笔迹语义完整保留严禁简化**
请仔细观察图像中的手写标记它们决定了数据的取舍
* **标记 A下划线/圈选 (Row Selection)** **针对电脑验光单**
* **现象**在打印数据列表 AVE **某一行数字下方**画了横线或圈出了某一行
* **含义**验光师认为该行数据最准**强制放弃**底部的 AVE平均值
* **操作**直接提取**被标记行** CYL (柱镜) AX (轴位)即使 AVE 行有数据也以被标记的行为准
* **标记 B删除线 (Strikethrough/Cancellation)**
* **现象**线条直接**贯穿/覆盖**了数字常见于散光/轴位
* **操作**该项数据归零如散光变 0.00
* **标记 C手写数值 (Handwritten Override)**
* **现象**旁边写了新的数字 -0.75
* **操作**手写数值优先级最高直接覆盖打印数值
3. ** 核心逻辑二光学十字换算完整保留严禁简化**
若图像中出现手画的十字交叉图请严格执行
* **定球镜 (S)**取十字线上两个数值中代数较大更偏正/更不负的数值作为 S
* **定轴位 (A)**
- 找到标有角度数值 90°, 180°的那根线
- S 在该线上A = 该角度
- S 不在该线上A = 该角度 ± 90°
* **定柱镜 (C)**C = (十字线上另一个数值) - S
4. **数据提取优先级序列**
请按以下顺序确定最终数值
1. **手写修正的具体数值** (最高)
2. **被下划线/圈选的特定打印行** (次高)
3. **打印的 AVE (平均值)** (普通)
4. **光学十字推算值** (仅在有十字图时生效)
5. ** 智能诊断与客服话术生成功能升级**
在精准提取数据后请检查是否命中以下规则
**执行要求**若命中请在输出结果的第三部分直接生成一段**客服可以直接复制发送给客户的回复话术**
*(若同时命中多条请将话术自然融合不要机械分点)*
* **场景 A非标准步长安抚数值不能被 0.25 整除 -0.12, -0.37**
* **触发条件**提取的 S C 结尾不是 .00, .25, .50, .75
* **话术策略**安抚客户不用担心奇怪的数字解释这是电脑原始数据承诺会按标准0.25调整
* **话术示例**仅供参考
"亲,单子上显示的 -0.37 是电脑验光的原始参考值哈。我们配镜时会按照国际标准度数(比如 0.25 或 0.50),您看您这边有没有验光师的手写单子呢,这样配出来的眼镜佩戴舒适哦~"
* **场景 B低度数/防蓝光推荐双眼度数 < 0.25**
* **触发条件**双眼 S C 绝对值均 < 0.25 0.00, 0.12, 0.25, 0.37
* **话术策略**恭喜客户视力底子好 -> 建议不配度数 -> 强烈推荐 0 度防蓝光保护视力
* **话术示例**仅供参考
"您的视力底子非常好数据看都只是极其微小的生理波动不到 25 这通常是不需要配度数的
如果您平时看手机电脑多特别推荐您配一副0度防蓝光眼镜既能阻隔辐射保护这么好的视力平时戴着也很好看呢"
* **场景 CPD (瞳距) 缺失**
* **触发条件**手写区无 PD 且全图未扫描到 PD
* **话术策略**温柔追问数据
* **话术示例**"亲单子上没看到瞳距PD数据哦您之前有测过吗或者手边有旧眼镜我们可以帮您测一下~"
6. **输出结构**
请严格按照以下顺序输出
- **第一部分分析摘要**简述数据来源依据下划线提取了第3行数据...
- **第二部分Markdown 表格**包含眼别, 球镜(S), 柱镜(C), 轴位(A), ADD, 瞳高(PH), 瞳距(PD)
- **第三部分建议客服回复的话术**
* **关键**请在此处输出 Point 5 中生成的**针对性话术**
* **格式**不要写规则A触发直接写建议您这样回复客户xxxxxxxx
* **融合要求**如果同时有非标准步长低度数请生成一段包含这两点意思的完整话术
- **第四部分JSON 数据**必须在最后
7. **JSON 输出格式**
\`\`\`json
{
"right": { "s": "", "c": "", "a": "", "add": "", "ph": "", "pd": "" },
"left": { "s": "", "c": "", "a": "", "add": "", "ph": "", "pd": "" },
"total_pd": ""
}
\`\`\`
`,
generateUserPayload: function(imageArray) {
return [
{
type: "text",
text: `请解读这张验光单。
**执行步骤**
1. **精准提取**严格执行核心逻辑一视觉符号核心逻辑二光学十字确保数据绝对准确
2. **话术生成重要**
- 如果遇到 -0.12/-0.37 这种数请生成一段话术让客服告诉客户这是电脑原始值镜片的国际标准是0.25一档可以询问客户有没有手写验光单
- 如果遇到低度数< 0.25请生成一段话术让客服推荐0度防蓝光眼镜
- **最终输出**请在第三部分直接给出一代**客服可以复制发给客户的文字**语气要亲切自然
3. **PD 检查**寻找打印的瞳距值
请输出最终结果`
},
...imageArray.map(imgBase64 => ({
type: "image_url",
image_url: { url: imgBase64, detail: "high" }
}))
];
}
};

File diff suppressed because one or more lines are too long

View File

@ -1,26 +0,0 @@
// config.js
const AppConfig = {
API_URL: 'https://ai.comfly.chat/v1/chat/completions',
//MODEL: 'gemini-3-pro-preview', // 你要求的模型
//MODEL: 'gemini-2.5-pro',
MODEL: 'gemini-3-flash-preview',
// 这里填入第一步生成的“加密Key”
ENCRYPTED_KEY: "MARAXis7BiwzBDwiLQIjLHNRWwwbWQAKDSFyAiQEYx8iIy8QUHNRAQkoJhMdCRRRWAwJ",
// 解密函数(必须与加密算法对应)
getDecryptedKey: function() {
const salt = "ComflyChatSecret2025"; // 必须与加密时的 Salt 一致
try {
const raw = atob(this.ENCRYPTED_KEY);
let result = "";
for (let i = 0; i < raw.length; i++) {
const charCode = raw.charCodeAt(i) ^ salt.charCodeAt(i % salt.length);
result += String.fromCharCode(charCode);
}
return result;
} catch (e) {
console.error("Key 解密失败");
return "";
}
}
};

File diff suppressed because one or more lines are too long

View File

@ -466,8 +466,8 @@ document.getElementById('submitBtn').onclick = async () => {
const startTask = async (index) => {
const slot = document.createElement('div');
slot.className = 'image-frame relative bg-white/50 animate-pulse min-h-[200px] flex items-center justify-center rounded-[2.5rem] border border-slate-100 shadow-sm';
slot.innerHTML = `<div class="text-slate-400 text-[10px] font-bold italic">AI 构思中...</div>`;
slot.className = 'image-frame relative bg-white/50 animate-pulse min-h-[300px] flex items-center justify-center rounded-[2.5rem] border border-slate-100 shadow-sm';
slot.innerHTML = `<div class="text-slate-400 text-xs font-bold italic">正在创作第 ${index + 1}...</div>`;
grid.appendChild(slot);
try {
@ -493,12 +493,10 @@ document.getElementById('submitBtn').onclick = async () => {
const imgUrl = res.data[0].url;
currentGeneratedUrls.push(imgUrl);
slot.className = 'image-frame group relative animate-in zoom-in-95 duration-700 flex flex-col items-center justify-center overflow-hidden bg-white shadow-2xl transition-all hover:shadow-indigo-100/50';
slot.className = 'image-frame group relative animate-in zoom-in-95 duration-700';
slot.innerHTML = `
<div class="w-full h-full flex items-center justify-center bg-slate-50/20 p-2">
<img src="${imgUrl}" class="max-w-full max-h-[60vh] md:max-h-[70vh] object-contain rounded-2xl shadow-sm transition-transform duration-500 group-hover:scale-[1.01]">
</div>
<div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-slate-900/40 backdrop-blur-[2px] rounded-[2.5rem]">
<img src="${imgUrl}" class="w-full h-auto rounded-[2.5rem] object-contain shadow-xl">
<div class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black/20 rounded-[2.5rem]">
<button onclick="downloadImage('${imgUrl}')" class="bg-white/90 p-4 rounded-full text-indigo-600 shadow-xl hover:scale-110 transition-transform">
<i data-lucide="download-cloud" class="w-6 h-6"></i>
</button>

View File

@ -1,383 +0,0 @@
// ocr.js
lucide.createIcons();
async function checkAuth() {
const r = await fetch('/api/auth/me');
const d = await r.json();
const profile = document.getElementById('userProfile');
const entry = document.getElementById('loginEntryBtn');
const submitBtn = document.getElementById('submitBtn');
if(d.logged_in) {
if(profile) profile.classList.remove('hidden');
if(entry) entry.classList.add('hidden');
submitBtn.disabled = false;
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
const phoneDisp = document.getElementById('userPhoneDisplay');
if(phoneDisp) phoneDisp.innerText = d.phone;
// 处理积分显示
const pointsBadge = document.getElementById('pointsBadge');
const pointsDisplay = document.getElementById('pointsDisplay');
if(pointsBadge && pointsDisplay) {
pointsBadge.classList.remove('hidden');
pointsDisplay.innerText = d.points;
}
const headerPoints = document.getElementById('headerPoints');
if(headerPoints) headerPoints.innerText = d.points;
} else {
if(profile) profile.classList.add('hidden');
if(entry) entry.classList.remove('hidden');
submitBtn.disabled = true;
submitBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
lucide.createIcons();
}
let uploadedFiles = [];
const FIXED_MODEL = 'gemini-3-flash-preview';
function updateCostPreview() {
const costPreview = document.getElementById('costPreview');
const isPremium = document.getElementById('isPremium')?.checked || false;
// 默认消耗 1 积分,优质模式 X2
let cost = 1;
if(isPremium) cost *= 2;
costPreview.innerText = `本次解析将消耗 ${cost} 积分`;
costPreview.classList.remove('hidden');
}
function renderImagePreviews() {
const prev = document.getElementById('imagePreview');
if(!prev) return;
prev.innerHTML = '';
uploadedFiles.forEach((file, index) => {
const d = document.createElement('div');
d.className = 'relative w-24 h-24 rounded-2xl overflow-hidden flex-shrink-0 border-2 border-white shadow-md group';
d.innerHTML = `
<div class="w-full h-full bg-slate-100 animate-pulse flex items-center justify-center">
<i data-lucide="image" class="w-5 h-5 text-slate-300"></i>
</div>
<div class="absolute bottom-1 left-1 bg-black/60 text-white text-[8px] px-1.5 py-0.5 rounded-md font-black backdrop-blur-sm z-10">图${index + 1}</div>
<button onclick="removeUploadedFile(${index})" class="absolute top-1 right-1 bg-rose-500 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center z-20">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
`;
prev.appendChild(d);
const reader = new FileReader();
reader.onload = (ev) => {
d.innerHTML = `
<img src="${ev.target.result}" class="w-full h-full object-cover">
<div class="absolute bottom-1 left-1 bg-black/60 text-white text-[8px] px-1.5 py-0.5 rounded-md font-black backdrop-blur-sm z-10">图${index + 1}</div>
<button onclick="removeUploadedFile(${index})" class="absolute top-1 right-1 bg-rose-500 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center z-20">
<i data-lucide="x" class="w-3 h-3"></i>
</button>
`;
lucide.createIcons();
};
reader.readAsDataURL(file);
});
}
function removeUploadedFile(index) {
uploadedFiles.splice(index, 1);
renderImagePreviews();
}
function handleNewFiles(files) {
const newFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
if (newFiles.length === 0) return;
if (uploadedFiles.length + newFiles.length > 3) {
showToast('最多只能上传 3 张照片', 'warning');
const remaining = 3 - uploadedFiles.length;
if (remaining > 0) {
uploadedFiles = uploadedFiles.concat(newFiles.slice(0, remaining));
}
} else {
uploadedFiles = uploadedFiles.concat(newFiles);
}
renderImagePreviews();
}
document.getElementById('fileInput').onchange = (e) => {
handleNewFiles(e.target.files);
e.target.value = '';
};
// 粘贴上传逻辑
document.addEventListener('paste', (e) => {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
const files = [];
for (let item of items) {
if (item.type.indexOf('image') !== -1) {
files.push(item.getAsFile());
}
}
if (files.length > 0) {
handleNewFiles(files);
}
});
// === 高度定制的复制逻辑 ===
function generateCopyString(data) {
const r = data.right;
const l = data.left;
const tPd = data.total_pd;
const blocks = [];
const fmtDeg = (val) => {
if (!val || val === "未标明" || val.trim() === "") return null;
const num = parseFloat(val);
if (isNaN(num)) return null;
const deg = Math.round(num * 100);
if (deg === 0) return null;
if (deg < 0) return Math.abs(deg).toString();
if (deg > 0) return "+" + deg.toString();
return null;
};
const fmtCyl = (val) => fmtDeg(val);
const fmtRaw = (val) => (!val || val === "未标明" || val.trim() === "") ? null : val.trim();
const fmtPD = (val) => {
if (!val || val === "未标明" || val.trim() === "") return null;
const num = parseFloat(val);
if (isNaN(num)) return val.trim();
return num.toString();
};
const rS = fmtDeg(r.s), lS = fmtDeg(l.s);
const rC = fmtCyl(r.c), lC = fmtCyl(l.c);
const rA = rC ? fmtRaw(r.a) : null, lA = lC ? fmtRaw(l.a) : null;
const rAdd = fmtDeg(r.add), lAdd = fmtDeg(l.add);
const rPH = fmtRaw(r.ph), lPH = fmtRaw(l.ph);
const rPd = fmtPD(r.pd), lPd = fmtPD(l.pd), totalPd = fmtPD(tPd);
if (!rS && !rC && !lS && !lC) return "平光";
const isDoubleSame = (rS === lS) && (rC === lC) && (rA === lA);
if (isDoubleSame) {
let block = `双眼${rS || ""}`;
if (rC) block += `散光${rC}`;
if (rA) block += `轴位${rA}`;
blocks.push(block);
} else {
if (rS || rC) {
let block = `右眼${rS || ""}`;
if (rC) block += `散光${rC}`;
if (rA) block += `轴位${rA}`;
blocks.push(block);
}
if (lS || lC) {
let block = `左眼${lS || ""}`;
if (lC) block += `散光${lC}`;
if (lA) block += `轴位${lA}`;
blocks.push(block);
}
}
if (rAdd && lAdd && rAdd === lAdd) blocks.push(`ADD${rAdd}`);
else {
if (rAdd) blocks.push(`ADD${rAdd}`);
if (lAdd) blocks.push(`ADD${lAdd}`);
}
if (rPH) blocks.push(`瞳高右眼${rPH}`);
if (lPH) blocks.push(`瞳高左眼${lPH}`);
if (totalPd) blocks.push(`瞳距${totalPd}`);
else {
if (rPd) blocks.push(`右眼瞳距${rPd}`);
if (lPd) blocks.push(`左眼瞳距${lPd}`);
}
return blocks.join(' ');
}
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.warn("现代复制API失败尝试传统方法:", err);
try {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '0';
textarea.setAttribute('readonly', '');
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textarea);
return successful;
} catch (e) {
return false;
}
}
}
const dropZone = document.getElementById('dropZone');
if (dropZone) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(name => {
dropZone.addEventListener(name, (e) => {
e.preventDefault();
e.stopPropagation();
}, false);
});
dropZone.addEventListener('drop', (e) => {
handleNewFiles(e.dataTransfer.files);
}, false);
}
document.getElementById('isPremium').onchange = () => updateCostPreview();
document.getElementById('submitBtn').onclick = async () => {
const btn = document.getElementById('submitBtn');
const authCheck = await fetch('/api/auth/me');
const authData = await authCheck.json();
if(!authData.logged_in) return showToast('请先登录', 'warning');
if(uploadedFiles.length === 0) return showToast('请上传验光单照片', 'warning');
const isPremium = document.getElementById('isPremium')?.checked || false;
const cost = isPremium ? 2 : 1;
if(authData.points < cost) return showToast('积分不足', 'warning');
btn.disabled = true;
const btnText = btn.querySelector('span');
btnText.innerText = "AI 正在深度解读中...";
document.getElementById('statusInfo').classList.remove('hidden');
document.getElementById('placeholder').classList.add('hidden');
document.getElementById('finalWrapper').classList.remove('hidden');
const textResult = document.getElementById('textResult');
textResult.innerHTML = '<div class="flex flex-col items-center justify-center p-20 gap-4"><i data-lucide="loader-2" class="w-10 h-10 animate-spin text-indigo-500"></i><span class="text-xs font-bold text-slate-400 animate-pulse">正在提取光学数据...</span></div>';
lucide.createIcons();
try {
let image_urls = [];
const uploadData = new FormData();
for(let f of uploadedFiles) uploadData.append('images', f);
const upR = await fetch('/api/upload', { method: 'POST', body: uploadData });
const upRes = await upR.json();
if(upRes.error) throw new Error(upRes.error);
image_urls = upRes.urls;
const messages = [
{ role: "system", content: AppPrompts.systemMessage },
{
role: "user",
content: [
{ type: "text", text: "请解读这张验光单。" }
]
}
];
image_urls.forEach(url => {
messages[1].content.push({ type: "image_url", image_url: { url: url } });
});
const r = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
mode: 'trial',
is_premium: isPremium,
prompt: "解读验光单",
model: FIXED_MODEL,
messages: messages,
image_urls: image_urls
})
});
const res = await r.json();
if(res.error) throw new Error(res.error);
const content = res.data[0].content;
// 分离正文和 JSON 数据
let displayMarkdown = content;
let jsonData = null;
const jsonMatch = content.match(/```json\n?([\s\S]*?)\n?```/);
if (jsonMatch && jsonMatch[1]) {
try {
jsonData = JSON.parse(jsonMatch[1].trim());
// 实时解析 Markdown (过滤 JSON 块)
displayMarkdown = content.replace(/```json[\s\S]*?```/g, '').trim();
} catch (e) {
console.error("JSON 解析失败", e);
}
}
// 渲染 Markdown
marked.setOptions({
gfm: true,
breaks: true,
tables: true,
headerIds: true,
mangle: false
});
textResult.innerHTML = marked.parse(displayMarkdown);
// 处理复制按钮逻辑
const actionBtn = document.getElementById('resultActions');
const copyBtn = document.getElementById('copyJsonBtn');
if (jsonData && actionBtn && copyBtn) {
actionBtn.classList.remove('hidden');
copyBtn.onclick = async () => {
const copyString = generateCopyString(jsonData);
const success = await copyToClipboard(copyString);
if (success) {
showToast('解析数据已成功复制', 'success');
} else {
showToast('复制失败,请手动选择', 'error');
}
};
}
checkAuth();
} catch (e) {
showToast('解读失败: ' + e.message, 'error');
textResult.innerHTML = `<div class="p-10 text-rose-500 font-bold text-center">解读失败: ${e.message}</div>`;
} finally {
btn.disabled = false;
btnText.innerText = "开始解读验光单";
document.getElementById('statusInfo').classList.add('hidden');
}
};
async function init() {
checkAuth();
updateCostPreview();
}
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
if(!container) {
// 如果在 iframe 中,尝试调用父页面的 toast
if(window.parent && window.parent.showToast) {
window.parent.showToast(message, type);
return;
}
alert(message);
return;
}
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const colors = { success: 'text-emerald-500', error: 'text-rose-500', warning: 'text-amber-500', info: 'text-indigo-500' };
const icons = { success: 'check-circle', error: 'x-circle', warning: 'alert-triangle', info: 'info' };
toast.innerHTML = `<i data-lucide="${icons[type]}" class="w-5 h-5 ${colors[type]} flex-shrink-0"></i><span class="text-sm font-medium text-slate-700">${message}</span>`;
container.appendChild(toast);
lucide.createIcons();
setTimeout(() => { toast.style.animation = 'slideOutRight 0.3s ease-in'; setTimeout(() => toast.remove(), 300); }, 3000);
}
init();

View File

@ -1,103 +0,0 @@
// prompts.js
const AppPrompts = {
systemMessage: `你是一个专业的眼科验光师助手,服务对象是眼镜店客服人员。
1. **基本原则**
- 使用简体中文
- 核心任务精准提取配镜数据S, C, A, PD
- **全能模式**你需要同时具备处理电脑验光单手写处方光学十字图的能力
2. ** 核心逻辑一视觉符号与笔迹语义完整保留严禁简化**
请仔细观察图像中的手写标记它们决定了数据的取舍
* **标记 A下划线/圈选 (Row Selection)** **针对电脑验光单**
* **现象**在打印数据列表 AVE **某一行数字下方**画了横线或圈出了某一行
* **含义**验光师认为该行数据最准**强制放弃**底部的 AVE平均值
* **操作**直接提取**被标记行** CYL (柱镜) AX (轴位)即使 AVE 行有数据也以被标记的行为准
* **标记 B删除线 (Strikethrough/Cancellation)**
* **现象**线条直接**贯穿/覆盖**了数字常见于散光/轴位
* **操作**该项数据归零如散光变 0.00
* **标记 C手写数值 (Handwritten Override)**
* **现象**旁边写了新的数字 -0.75
* **操作**手写数值优先级最高直接覆盖打印数值
3. ** 核心逻辑二光学十字换算完整保留严禁简化**
若图像中出现手画的十字交叉图请严格执行
* **定球镜 (S)**取十字线上两个数值中代数较大更偏正/更不负的数值作为 S
* **定轴位 (A)**
- 找到标有角度数值 90°, 180°的那根线
- S 在该线上A = 该角度
- S 不在该线上A = 该角度 ± 90°
* **定柱镜 (C)**C = (十字线上另一个数值) - S
4. **数据提取优先级序列**
请按以下顺序确定最终数值
1. **手写修正的具体数值** (最高)
2. **被下划线/圈选的特定打印行** (次高)
3. **打印的 AVE (平均值)** (普通)
4. **光学十字推算值** (仅在有十字图时生效)
5. ** 智能诊断与客服话术生成功能升级**
在精准提取数据后请检查是否命中以下规则
**执行要求**若命中请在输出结果的第三部分直接生成一段**客服可以直接复制发送给客户的回复话术**
*(若同时命中多条请将话术自然融合不要机械分点)*
* **场景 A非标准步长安抚数值不能被 0.25 整除 -0.12, -0.37**
* **触发条件**提取的 S C 结尾不是 .00, .25, .50, .75
* **话术策略**安抚客户不用担心奇怪的数字解释这是电脑原始数据承诺会按标准0.25调整
* **话术示例**仅供参考
"亲,单子上显示的 -0.37 是电脑验光的原始参考值哈。我们配镜时会按照国际标准度数(比如 0.25 或 0.50),您看您这边有没有验光师的手写单子呢,这样配出来的眼镜佩戴舒适哦~"
* **场景 B低度数/防蓝光推荐双眼度数 < 0.25**
* **触发条件**双眼 S C 绝对值均 < 0.25 0.00, 0.12, 0.25, 0.37
* **话术策略**恭喜客户视力底子好 -> 建议不配度数 -> 强烈推荐 0 度防蓝光保护视力
* **话术示例**仅供参考
"您的视力底子非常好数据看都只是极其微小的生理波动不到 25 这通常是不需要配度数的
如果您平时看手机电脑多特别推荐您配一副0度防蓝光眼镜既能阻隔辐射保护这么好的视力平时戴着也很好看呢"
* **场景 CPD (瞳距) 缺失**
* **触发条件**手写区无 PD 且全图未扫描到 PD
* **话术策略**温柔追问数据
* **话术示例**"亲单子上没看到瞳距PD数据哦您之前有测过吗或者手边有旧眼镜我们可以帮您测一下~"
6. **输出结构**
请严格按照以下顺序输出
- **第一部分分析摘要**简述数据来源依据下划线提取了第3行数据...
- **第二部分Markdown 表格**包含眼别, 球镜(S), 柱镜(C), 轴位(A), ADD, 瞳高(PH), 瞳距(PD)
- **第三部分建议客服回复的话术**
* **关键**请在此处输出 Point 5 中生成的**针对性话术**
* **格式**不要写规则A触发直接写建议您这样回复客户xxxxxxxx
* **融合要求**如果同时有非标准步长低度数请生成一段包含这两点意思的完整话术
- **第四部分JSON 数据**必须在最后
7. **JSON 输出格式**
\`\`\`json
{
"right": { "s": "", "c": "", "a": "", "add": "", "ph": "", "pd": "" },
"left": { "s": "", "c": "", "a": "", "add": "", "ph": "", "pd": "" },
"total_pd": ""
}
\`\`\`
`,
generateUserPayload: function(imageArray) {
return [
{
type: "text",
text: `请解读这张验光单。
**执行步骤**
1. **精准提取**严格执行核心逻辑一视觉符号核心逻辑二光学十字确保数据绝对准确
2. **话术生成重要**
- 如果遇到 -0.12/-0.37 这种数请生成一段话术让客服告诉客户这是电脑原始值镜片的国际标准是0.25一档可以询问客户有没有手写验光单
- 如果遇到低度数< 0.25请生成一段话术让客服推荐0度防蓝光眼镜
- **最终输出**请在第三部分直接给出一代**客服可以复制发给客户的文字**语气要亲切自然
3. **PD 检查**寻找打印的瞳距值
请输出最终结果`
},
...imageArray.map(imgBase64 => ({
type: "image_url",
image_url: { url: imgBase64, detail: "high" }
}))
];
}
};

View File

@ -4,10 +4,10 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}AI 视界{% endblock %}</title>
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<script src="{{ url_for('static', filename='js/lucide.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/Sortable.min.js') }}"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.staticfile.org/Sortable/1.15.0/Sortable.min.js"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block head %}{% endblock %}
</head>
@ -25,17 +25,6 @@
<div id="dynamicMenuList" class="flex-1 w-full px-2 space-y-4"></div>
<div id="globalUserProfile" class="flex flex-col items-center gap-4 mb-4">
<!-- 联系方式 -->
<div class="relative group flex justify-center mb-2">
<div class="w-10 h-10 bg-slate-800 rounded-xl flex items-center justify-center text-slate-400 border border-slate-700 hover:text-indigo-400 transition-colors cursor-help">
<i data-lucide="message-circle" class="w-5 h-5"></i>
</div>
<div class="absolute left-full ml-4 px-3 py-2 bg-slate-800 text-white text-[10px] font-black rounded-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50 shadow-xl border border-slate-700">
联系客服 QQ: 240241002
<div class="absolute right-full top-1/2 -translate-y-1/2 border-8 border-transparent border-r-slate-800"></div>
</div>
</div>
<div class="w-10 h-10 bg-slate-800 rounded-xl flex items-center justify-center text-slate-400 border border-slate-700">
<i data-lucide="user" class="w-5 h-5"></i>
</div>

View File

@ -143,38 +143,31 @@
</div>
</div>
<div class="flex-1 flex flex-col p-8 relative overflow-hidden min-h-0">
<div class="flex-1 flex items-center justify-center p-8 relative">
<div id="statusInfo" class="absolute top-8 left-1/2 -translate-x-1/2 hidden z-20">
<div class="bg-white/90 backdrop-blur-xl border border-indigo-100 px-6 py-3 rounded-2xl shadow-xl flex items-center gap-3">
<div class="w-2 h-2 bg-indigo-500 rounded-full animate-bounce"></div>
<span class="text-xs font-black text-slate-600 uppercase tracking-widest">AI 正在处理您的请求...</span>
</div>
</div>
<div id="resultCanvas" class="flex-1 w-full flex flex-col relative overflow-hidden">
<!-- 初始占位状态 -->
<div id="placeholder" class="flex-1 flex flex-col items-center justify-center text-center max-w-lg mx-auto">
<div id="resultCanvas" class="w-full h-full flex items-center justify-center">
<div id="placeholder" class="text-center max-w-lg">
<div class="w-48 h-48 bg-white rounded-[4.5rem] shadow-2xl flex items-center justify-center rotate-6 mx-auto mb-12 border border-slate-100">
<i data-lucide="glasses" class="w-20 h-20 text-indigo-500"></i>
</div>
<h2 class="text-4xl font-black text-slate-900 mb-8">视界进化 · 艺术呈现</h2>
<p class="text-slate-500 text-lg font-medium">在左侧完成设定,开启 AI 试戴体验</p>
</div>
<div id="finalWrapper" class="hidden flex-1 w-full flex flex-col overflow-hidden">
<!-- 滚动区域 -->
<div class="flex-1 w-full overflow-y-auto custom-scrollbar px-8 py-10">
<div id="imageGrid" class="grid grid-cols-1 md:grid-cols-2 gap-10 w-full max-w-6xl mx-auto items-start"></div>
</div>
<!-- 固定底部操作栏 -->
<div id="resultActions" class="w-full flex-shrink-0 bg-slate-50/80 backdrop-blur-sm border-t border-slate-200/60 py-6 flex items-center justify-center gap-8 z-10">
<div id="finalWrapper" class="hidden w-full h-full flex flex-col items-center justify-center py-6">
<div id="imageGrid" class="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-4xl p-4"></div>
<div class="mt-14 flex items-center gap-8">
<!-- 全部下载按钮 -->
<button id="downloadAllBtn" class="bg-indigo-600 text-white px-10 py-4 rounded-3xl text-sm font-bold shadow-xl shadow-indigo-100 hover:bg-indigo-700 hover:scale-105 active:scale-95 transition-all flex items-center gap-3">
<button id="downloadAllBtn" class="bg-indigo-600 text-white px-8 py-4 rounded-[1.8rem] text-sm font-bold shadow-xl shadow-indigo-200 hover:bg-indigo-700 hover:scale-105 active:scale-95 transition-all flex items-center gap-3">
<i data-lucide="download" class="w-5 h-5"></i>
<span>全部下载</span>
</button>
<!-- 重新生成按钮 -->
<button id="regenBtn" class="w-16 h-16 bg-white border border-slate-100 rounded-3xl flex items-center justify-center text-slate-400 shadow-xl hover:text-indigo-600 transition-all hover:scale-110 active:scale-95 group">
<button id="regenBtn" class="w-16 h-16 bg-white border border-slate-100 rounded-[1.8rem] flex items-center justify-center text-slate-400 shadow-xl hover:text-indigo-600 transition-all hover:scale-110 active:scale-95 group">
<i data-lucide="refresh-cw" class="w-6 h-6 group-hover:rotate-180 transition-transform duration-500"></i>
</button>
</div>

View File

@ -1,173 +0,0 @@
{% extends "base.html" %}
{% block title %}验光单助手 - AI 视界{% endblock %}
{% block content %}
<!-- 中间AI 功能设定区 -->
<aside class="w-80 lg:w-[340px] flex-shrink-0 glass-sidebar flex flex-col z-30 border-r border-slate-200/60 shadow-xl bg-white/50 backdrop-blur-xl">
<div class="p-6 pb-2">
<h2 class="text-xl font-black text-slate-900 tracking-tight">验光单助手</h2>
<p class="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-1">智能数据提取配置</p>
<!-- 免责声明提示 - 增强版 -->
<div class="mt-6 p-4 bg-indigo-50/50 border border-indigo-100/50 rounded-2xl relative overflow-hidden group">
<div class="absolute top-0 right-0 -mt-2 -mr-2 w-16 h-16 bg-indigo-500/5 rounded-full blur-2xl group-hover:bg-indigo-500/10 transition-colors"></div>
<div class="flex items-center gap-2.5 mb-2">
<div class="w-6 h-6 bg-white rounded-lg flex items-center justify-center shadow-sm border border-indigo-50">
<i data-lucide="shield-check" class="w-3.5 h-3.5 text-indigo-500"></i>
</div>
<span class="text-xs font-black text-indigo-900 uppercase tracking-wider">使用必读</span>
</div>
<div class="space-y-2">
<p class="text-[11px] text-slate-600 leading-relaxed font-medium">
本助手支持单次上传多张单据(如:电脑验光+综合验光),但请确保单据均属于<span class="text-indigo-600 font-bold underline">同一个人</span>
</p>
<div class="p-2 bg-white/60 rounded-xl border border-indigo-100/20 backdrop-blur-sm">
<p class="text-[11px] text-indigo-600 leading-relaxed font-bold flex items-start gap-1.5">
<i data-lucide="alert-triangle" class="w-3 h-3 mt-0.5 flex-shrink-0"></i>
<span>因手写单据模糊或光影识别误差,<span class="underline decoration-indigo-300 underline-offset-2">请务必以人工核对后的数据为准</span></span>
</p>
</div>
<p class="text-[10px] text-slate-400 font-bold italic">
* 平台仅提供 AI 技术支持参考,不承担医疗诊断建议。
</p>
</div>
</div>
</div>
<div class="flex-1 overflow-y-auto px-6 py-4 space-y-6 custom-scrollbar">
<section class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="w-6 h-6 rounded-full bg-indigo-600 text-[10px] text-white flex items-center justify-center font-bold">01</span>
<h3 class="text-sm font-bold text-slate-800">状态信息</h3>
</div>
<!-- 积分显示 -->
<div id="pointsBadge" class="hidden px-2 py-0.5 bg-amber-50 text-amber-600 border border-amber-100 rounded-lg text-[10px] font-black uppercase">
可用积分: <span id="pointsDisplay">0</span>
</div>
</div>
<div id="premiumToggle" class="flex items-center justify-between bg-amber-50/50 border border-amber-100/50 p-3 rounded-2xl animate-in fade-in duration-500">
<div class="flex items-center gap-2">
<div class="w-7 h-7 bg-amber-100 text-amber-600 rounded-lg flex items-center justify-center">
<i data-lucide="sparkles" class="w-4 h-4"></i>
</div>
<div>
<div class="text-[10px] font-black text-amber-700 uppercase tracking-tight">高级解析模式</div>
<div class="text-[8px] text-amber-500 font-bold">积分消耗 X2</div>
</div>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="isPremium" class="sr-only peer">
<div class="w-9 h-5 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-amber-500"></div>
</label>
</div>
</section>
<section class="space-y-4 pb-2">
<div class="flex items-center gap-3">
<span class="w-6 h-6 rounded-full bg-indigo-600 text-[10px] text-white flex items-center justify-center font-bold">02</span>
<h3 class="text-sm font-bold text-slate-800">验光单照片</h3>
</div>
<div id="dropZone" class="relative group">
<div class="border-2 border-dashed border-slate-100 rounded-3xl p-6 text-center bg-white/30 hover:border-indigo-200 transition-all cursor-pointer">
<i data-lucide="image-plus" class="w-6 h-6 mx-auto mb-2 text-slate-300"></i>
<p class="text-[10px] text-slate-400 font-bold">点击、拖拽或粘贴照片最多3张</p>
<p class="text-[8px] text-indigo-400 font-black mt-1 uppercase tracking-tighter">* 仅支持同一人的多张单据合拍或分传</p>
</div>
<input id="fileInput" type="file" multiple class="absolute inset-0 opacity-0 cursor-pointer">
</div>
<div id="imagePreview" class="flex flex-wrap gap-3 py-1"></div>
</section>
</div>
<div class="p-6 bg-white/95 border-t border-slate-100">
<button id="submitBtn" class="w-full btn-primary py-4 rounded-2xl shadow-lg hover:scale-[1.02] active:scale-[0.98] gap-2 flex items-center justify-center">
<i data-lucide="wand-2" class="w-5 h-5"></i>
<span class="text-base font-bold tracking-widest">开始解读验光单</span>
</button>
<p id="costPreview" class="text-center text-[10px] text-amber-600 font-bold mt-2 hidden"></p>
<p id="loginHint" class="text-center text-[10px] text-slate-400 mt-2 hidden">
<i data-lucide="lock" class="w-2.5 h-2.5 inline"></i> 请先登录以使用生成功能
</p>
</div>
</aside>
<!-- 右侧:主展示 -->
<main class="flex-1 relative flex flex-col bg-slate-50 overflow-hidden">
<div class="h-24 flex items-center justify-between px-12 relative z-10">
<div class="flex items-center gap-3">
<div class="w-2.5 h-2.5 bg-indigo-500 rounded-full animate-ping"></div>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-widest text-xs">引擎就绪</span>
</div>
<div class="flex items-center gap-6">
<div id="userProfile" class="hidden flex items-center gap-3 bg-white/80 backdrop-blur-md px-4 py-2 rounded-2xl border border-white shadow-sm hover:bg-white transition-all">
<div class="w-8 h-8 bg-indigo-100 rounded-xl flex items-center justify-center text-indigo-600">
<i data-lucide="user" class="w-4 h-4"></i>
</div>
<div class="flex flex-col">
<span id="userPhoneDisplay" class="text-xs font-bold text-slate-600">--</span>
<span class="text-[9px] font-black text-amber-600 uppercase">余额: <span id="headerPoints">0</span> 积分</span>
</div>
</div>
<a id="loginEntryBtn" href="/login" class="bg-indigo-600 text-white px-6 py-2.5 rounded-2xl text-xs font-bold shadow-lg shadow-indigo-200 hover:bg-indigo-700 transition-all">
立即登录
</a>
</div>
</div>
<div class="flex-1 flex items-center justify-center p-8 relative overflow-hidden">
<div id="statusInfo" class="absolute top-8 left-1/2 -translate-x-1/2 hidden z-20">
<div class="bg-white/90 backdrop-blur-xl border border-indigo-100 px-6 py-3 rounded-2xl shadow-xl flex items-center gap-3">
<div class="w-2 h-2 bg-indigo-500 rounded-full animate-bounce"></div>
<span class="text-xs font-black text-slate-600 uppercase tracking-widest">AI 正在处理您的请求...</span>
</div>
</div>
<div id="resultCanvas" class="w-full h-full flex flex-col items-center justify-center">
<!-- 初始占位状态 -->
<div id="placeholder" class="text-center max-w-lg">
<div class="w-48 h-48 bg-white rounded-[4.5rem] shadow-2xl flex items-center justify-center rotate-6 mx-auto mb-12 border border-slate-100">
<i data-lucide="scan-eye" class="w-20 h-20 text-indigo-500"></i>
</div>
<h2 class="text-4xl font-black text-slate-900 mb-8">精准解析 · 专业建议</h2>
<p class="text-slate-500 text-lg font-medium">上传验光单照片,获取 AI 深度解读</p>
</div>
<!-- 结果展示状态:使用 flex 布局确保按钮区域固定在底部 -->
<div id="finalWrapper" class="hidden w-full h-full flex flex-col items-center">
<!-- 滚动的内容区域 -->
<div class="flex-1 w-full overflow-y-auto custom-scrollbar px-4 py-6 flex flex-col items-center">
<div id="textResult" class="w-full max-w-4xl p-10 bg-white rounded-[2.5rem] shadow-2xl border border-slate-100 prose prose-indigo max-w-none mb-6"></div>
</div>
<!-- 固定的操作栏区域 -->
<div id="resultActions" class="w-full flex-shrink-0 bg-slate-50/80 backdrop-blur-sm border-t border-slate-200/60 py-6 flex flex-col items-center gap-3 z-10">
<div class="flex items-center gap-4">
<button id="copyJsonBtn" class="bg-slate-900 text-white px-10 py-3.5 rounded-2xl text-xs font-bold shadow-xl hover:bg-slate-800 transition-all flex items-center gap-2.5 active:scale-[0.98]">
<i data-lucide="copy" class="w-4 h-4"></i>
复制解析数据
</button>
<button onclick="location.reload()" class="bg-white border border-slate-200 text-slate-600 px-10 py-3.5 rounded-2xl text-xs font-bold hover:bg-slate-50 transition-all flex items-center gap-2.5 active:scale-[0.98]">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
重新解析
</button>
</div>
<p class="text-[10px] text-slate-400 font-medium">
* AI 识别结果仅供参考,请务必以人工核对后的数据为准
</p>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="{{ url_for('static', filename='js/config.js') }}"></script>
<script src="{{ url_for('static', filename='js/prompts.js') }}"></script>
<script src="{{ url_for('static', filename='js/ocr.js') }}"></script>
{% endblock %}