图片识别

This commit is contained in:
李鹏宇
2026-06-12 16:39:20 +08:00
parent 46294d43a9
commit 3f8dc84241
4 changed files with 21 additions and 39 deletions
+3 -3
View File
@@ -249,10 +249,10 @@ class AiChatController(http.Controller):
ocr_error = ''
if provider and provider.ocr_enabled:
from ..models.ocr_provider import get_ocr_instance
ocr = get_ocr_instance(provider)
ocr, init_err = get_ocr_instance(provider)
if ocr is None:
ocr_error = 'OCR 实例创建失败,请检查 SecretId/SecretKey 是否完整'
_logger.error('[AI-OCR] get_ocr_instance 返回 None')
ocr_error = init_err or 'OCR 实例创建失败'
_logger.error('[AI-OCR] get_ocr_instance 失败: %s', ocr_error)
else:
result = ocr.smart_recognize(raw_bytes, mimetype)
if result.get('success'):
+16 -9
View File
@@ -146,21 +146,28 @@ def get_ocr_instance(provider):
"""从 AiProvider 记录中创建 OCR 实例。
:param provider: ai.provider 记录
:return: TencentOCR 实例或 None
:return: (TencentOCR 实例 | None, str 错误原因)
"""
if not provider.ocr_enabled:
return None
if not provider.ocr_secret_id or not provider.ocr_secret_key:
_logger.warning('OCR 已启用但 SecretId/SecretKey 未配置 (provider=%s)', provider.name)
return None
return None, 'OCR 未启用'
# 逐项校验,给出精确的错误信息
if not provider.ocr_secret_id:
return None, '未配置 SecretId(请到服务商配置 → OCR配置 填写)'
if not provider.ocr_secret_key:
return None, '未配置 SecretKey(请到服务商配置 → OCR配置 填写)'
if not HAS_TENCENT_SDK:
return None
return None, 'tencentcloud-sdk-python 未安装(pip install tencentcloud-sdk-python)'
try:
return TencentOCR(
secret_id=provider.ocr_secret_id,
secret_key=provider.ocr_secret_key,
region=provider.ocr_region or 'ap-guangzhou',
)
), ''
except ValueError as ve:
_logger.warning('[AI-OCR] 参数错误: %s', ve)
return None, str(ve)
except Exception as e:
_logger.exception('创建 OCR 实例失败')
return None
_logger.exception('[AI-OCR] 创建 OCR 实例异常')
return None, f'初始化异常: {e}'
@@ -35,17 +35,7 @@ export class AiChatAction extends Component {
.replace(/【附件:[^】]+】[\s\S]*?--- 附件内容结束 ---/g, '')
.replace(/\[附件[::]\s*[^\]]+\]\n?/g, '')
.trim() || safe;
// URL → 精美卡片(还原 escapeHtml 把 & 变回 &,保证 href 正确)
display = display.replace(
/(https?:\/\/[^\s<>"'\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]+)/g,
(url) => {
const rawUrl = url.replace(/&amp;/g, '&');
let domain = '';
try { domain = new URL(rawUrl).hostname; } catch (_) { domain = rawUrl.substring(0, 50); }
const displayUrl = rawUrl.length > 60 ? rawUrl.substring(0, 57) + '...' : rawUrl;
return `<a class="o_ai_link_card" href="${rawUrl}" target="_blank" rel="noopener noreferrer" title="${rawUrl}"><span class="o_ai_link_icon"><i class="fa fa-external-link"></i></span><span class="o_ai_link_domain">${this._escapeHtml(domain)}</span><span class="o_ai_link_url">${this._escapeHtml(displayUrl)}</span></a>`;
}
);
// 不做额外处理,URL 保持纯文本显示
return display.replace(/\n/g, '<br>');
}
+1 -16
View File
@@ -235,22 +235,7 @@ function renderContent(text) {
.replace(/【附件:[^】]+】[\s\S]*?--- 附件内容结束 ---/g, '')
.replace(/\[附件[::]\s*[^\]]+\]\n?/g, '')
.trim() || safe; // 若 strip 后为空,回退到 safe 继续处理 URL
// URL 渲染为精美卡片(先还原 escapeHtml 把 &amp; 变回 &,保证 href 正确)
display = display.replace(
/(https?:\/\/[^\s<>"'\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]+)/g,
(url) => {
// 还原 HTML 实体(escapeHtml 会把 & 变成 &amp;,URL 参数中的 & 需要还原)
const rawUrl = url.replace(/&amp;/g, '&');
let domain = '';
try {
domain = new URL(rawUrl).hostname;
} catch (_) {
domain = rawUrl.substring(0, 50);
}
const displayUrl = rawUrl.length > 60 ? rawUrl.substring(0, 57) + '...' : rawUrl;
return `<a class="o_ai_link_card" href="${rawUrl}" target="_blank" rel="noopener noreferrer" title="${rawUrl}"><span class="o_ai_link_icon"><i class="fa fa-external-link"></i></span><span class="o_ai_link_domain">${escapeHtml(domain)}</span><span class="o_ai_link_url">${escapeHtml(displayUrl)}</span></a>`;
}
);
// 不做额外处理,URL 保持纯文本显示
// 换行转 <br>
display = display.replace(/\n/g, '<br>');
return display;