diff --git a/.gitignore b/.gitignore index 6ba8e95..48fb7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ __pycache__/ bbit_ai/test/milvus/milvus_docs/__MACOSX/ bbit_ai/test/milvus/milvus_docs/ +bbit_ai/test/ocr/PP-OCRv5_server_det_infer/ +bbit_ai/test/ocr/PP-OCRv5_server_rec_infer/ +vue/vue.tar +bbit_ai/test/ocr/ +bbit_ai/ce-pybackend.tar diff --git a/bbit_ai/.idea/app.iml b/bbit_ai/.idea/app.iml index 80e35b9..6d713b3 100644 --- a/bbit_ai/.idea/app.iml +++ b/bbit_ai/.idea/app.iml @@ -1,7 +1,7 @@ - + diff --git a/bbit_ai/.idea/misc.xml b/bbit_ai/.idea/misc.xml index c28477b..5756918 100644 --- a/bbit_ai/.idea/misc.xml +++ b/bbit_ai/.idea/misc.xml @@ -1,7 +1,9 @@ - \ No newline at end of file diff --git a/bbit_ai/app/agent/ticketAgent.py b/bbit_ai/app/agent/ticketAgent.py new file mode 100644 index 0000000..1e9e992 --- /dev/null +++ b/bbit_ai/app/agent/ticketAgent.py @@ -0,0 +1,60 @@ +from typing import Optional +from pydantic import BaseModel, Field +from langchain_core.messages import HumanMessage + +from config.llm import * +from langchain.prompts import PromptTemplate + +from langchain.prompts import PromptTemplate +from langchain.schema import HumanMessage +import os +import base64 +from langchain.prompts import PromptTemplate +from langchain_openai import ChatOpenAI +from langchain_core.output_parsers import JsonOutputParser +from langchain.schema import HumanMessage +from pydantic import BaseModel, Field + +from pydantic import BaseModel, Field +import json + +import re +import json +import requests +import cv2 +import numpy as np +import requests + +class CocoonSample(BaseModel): + moisture_content: float = Field( + ..., + description="茧的含水量,单位为百分比(%),浮点数" + ) + cocoon_weight: float = Field( + ..., + description="下足茧的重量,单位为克,可带小数" + ) + defective_pupa_count: int = Field( + ..., + description="非好蛹粒数,即不合格蛹的数量,整数" + ) + fresh_shell_weight: float = Field( + ..., + description="鲜壳重量,单位为克,可带小数" + ) + sample_count: int = Field( + ..., + description="小样粒数,用于检测的茧粒数,整数" + ) + net_weight_total: float = Field( + ..., + description="所有样品的净重合计,单位为克,浮点数" + ) + evaluator: Optional[str] = Field( + None, + description="仪评人姓名,可能为空" + ) + reviewer: Optional[str] = Field( + None, + description="复核人员姓名,可能为空" + ) diff --git a/bbit_ai/app/app.py b/bbit_ai/app/app.py index 9a961e2..a7225af 100644 --- a/bbit_ai/app/app.py +++ b/bbit_ai/app/app.py @@ -1,18 +1,22 @@ from fastapi import FastAPI -from routers.Chat import chatRouter -from routers.Report import reportRouter -from routers.Datasource import reportDataRouter from fastapi.middleware.cors import CORSMiddleware -from routers.Knowledge import knowledgeRouter -from routers.Service import serviceRouter + from routers.Bot import botRouter +from routers.Chat import chatRouter +from routers.Datasource import reportDataRouter +from routers.F8 import f8Router +from routers.Knowledge import knowledgeRouter +from routers.Report import reportRouter +from routers.Service import serviceRouter +from routers.Vision import visionRouter + app = FastAPI(title="BBIT_AI") origins = [ "http://localhost:8090", # Vite dev 默认端口 "http://127.0.0.1:5173", "http://s1.ronsunny.cn:8089", - "*" # ⚠️ 生产环境不要用 + "*", # ⚠️ 生产环境不要用 ] app.add_middleware( @@ -22,9 +26,13 @@ app.add_middleware( allow_methods=["*"], # 必须包含 OPTIONS、GET 等 allow_headers=["*"], ) -app.include_router(chatRouter, prefix="/api/llm", tags=["chat"]) -app.include_router(reportRouter, prefix="/api/llm", tags=["chat"]) -app.include_router(knowledgeRouter, prefix="/api/llm", tags=["chat"]) -app.include_router(reportDataRouter, prefix="/api/llm", tags=["chat"]) -app.include_router(serviceRouter, prefix="/api/llm", tags=["chat"]) -app.include_router(botRouter, prefix="/api/llm", tags=["chat"]) \ No newline at end of file +app.include_router(chatRouter, prefix="/api/llm", tags=["llm"]) +app.include_router(reportRouter, prefix="/api/llm", tags=["llm"]) +app.include_router(knowledgeRouter, prefix="/api/llm", tags=["llm"]) +app.include_router(reportDataRouter, prefix="/api/llm", tags=["llm"]) +app.include_router(serviceRouter, prefix="/api/llm", tags=["llm"]) +app.include_router(botRouter, prefix="/api/llm", tags=["llm"]) +app.include_router(visionRouter, prefix="/api/llm", tags=["llm"]) + + +app.include_router(f8Router, prefix="/api/f8", tags=["f8"]) diff --git a/bbit_ai/app/config/app.py b/bbit_ai/app/config/app.py new file mode 100644 index 0000000..4f7de61 --- /dev/null +++ b/bbit_ai/app/config/app.py @@ -0,0 +1,3 @@ +SERVER_PATH_OSS = "s1.ronsunny.cn" + +F8_SERVER_USER_ID = "da33efb9-776a-443b-b1ec-dbbbf08793d7" diff --git a/bbit_ai/app/config/llm.py b/bbit_ai/app/config/llm.py index 11ff5ca..d2e8e6c 100644 --- a/bbit_ai/app/config/llm.py +++ b/bbit_ai/app/config/llm.py @@ -6,6 +6,8 @@ from langchain_openai import ChatOpenAI from openai import OpenAI import os from langchain_openai import OpenAIEmbeddings +from langchain_community.embeddings import DashScopeEmbeddings + # 通义千文Key tongyiKey = "sk-9464b2498c184982a9fe9d2c2e725ab5" # DeepSeekKey @@ -18,13 +20,16 @@ llmThink = ChatOpenAI( api_key=tongyiKey, base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", model="qwen-max", - stream = False ) -from langchain_community.embeddings import DashScopeEmbeddings -embeddings = DashScopeEmbeddings( +llmEmbeddings = DashScopeEmbeddings( model="text-embedding-v3", dashscope_api_key= tongyiKey, ) +llmVision = ChatOpenAI( + api_key=tongyiKey, + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", + model="qwen-vl-plus", +) # from langchain_deepseek import ChatDeepSeek # llm = ChatDeepSeek( diff --git a/bbit_ai/app/config/milvus.py b/bbit_ai/app/config/milvus.py index f5ab578..d057e4a 100644 --- a/bbit_ai/app/config/milvus.py +++ b/bbit_ai/app/config/milvus.py @@ -1,10 +1,10 @@ from langchain_milvus import BM25BuiltInFunction, Milvus -from config.llm import embeddings +from config.llm import llmEmbeddings URI = "http://10.10.10.9:19530" knVectorstore = Milvus( - embedding_function=embeddings, + embedding_function=llmEmbeddings, connection_args={"uri": URI, "token": "root:Milvus", "db_name": "bbit_ai_lab"}, collection_name="knowledge", index_params={"index_type": "FLAT", "metric_type": "L2"}, @@ -19,7 +19,7 @@ knVectorstore = Milvus( drop_old=False, # set to True if seeking to drop the collection with that name if it exists ) memVectorstore = Milvus( - embedding_function=embeddings, + embedding_function=llmEmbeddings, connection_args={"uri": URI, "token": "root:Milvus", "db_name": "bbit_ai_lab"}, collection_name="memory", index_params={"index_type": "FLAT", "metric_type": "L2"}, diff --git a/bbit_ai/app/config/minIO.py b/bbit_ai/app/config/minIO.py new file mode 100644 index 0000000..21e018a --- /dev/null +++ b/bbit_ai/app/config/minIO.py @@ -0,0 +1,27 @@ +from datetime import timedelta + +from minio import Minio + +# MinIO 客户端初始化 +minio_client = Minio( + "s1.ronsunny.cn:9000", + access_key="minioadmin", + secret_key="minioadmin", + secure=False, +) + + +def push_file(bucket_name, object_name, file_bytes, contents, content_type): + minio_client.put_object( + bucket_name, + object_name, + file_bytes, + length=len(contents), + content_type=content_type, + ) + + +def get_temp_url(bucket_name, object_name): + return minio_client.presigned_get_object( + bucket_name, object_name, expires=timedelta(seconds=3600) + ) diff --git a/bbit_ai/app/db/postgres.py b/bbit_ai/app/db/postgres.py index 4640389..314f825 100644 --- a/bbit_ai/app/db/postgres.py +++ b/bbit_ai/app/db/postgres.py @@ -1,8 +1,9 @@ -from langchain_postgres import PostgresChatMessageHistory -from config.pgDb import pg_pool -from config.ssDb import mssql_pool -from typing import List, Dict import json +from typing import List, Dict + +from langchain_postgres import PostgresChatMessageHistory + +from config.pgDb import pg_pool # ————————————————————————————————————————————————————AI角色——————————————————————————————— @@ -13,8 +14,7 @@ def get_ai_personality(ai_id: str): with pg_pool.getConn() as conn: with conn.cursor() as cur: cur.execute( - "SELECT ai_personality FROM ai_chat_profiles WHERE id = %s", - (ai_id,) + "SELECT ai_personality FROM ai_chat_profiles WHERE id = %s", (ai_id,) ) row = cur.fetchone() if row: @@ -27,8 +27,7 @@ def get_description(ai_id: str): with pg_pool.getConn() as conn: with conn.cursor() as cur: cur.execute( - "SELECT description FROM ai_chat_profiles WHERE id = %s", - (ai_id,) + "SELECT description FROM ai_chat_profiles WHERE id = %s", (ai_id,) ) row = cur.fetchone() if row: @@ -40,8 +39,7 @@ def get_description(ai_id: str): def get_ai_available_kn_bases(ai_id: str) -> List[str]: with pg_pool.getConn() as conn: result = conn.execute( - "SELECT available_kn_bases FROM ai_chat_profiles WHERE id = %s", - (ai_id,) + "SELECT available_kn_bases FROM ai_chat_profiles WHERE id = %s", (ai_id,) ) return result.fetchone()[0] @@ -50,10 +48,7 @@ def get_all_ai_bot(user_id: str, module: str) -> List[Dict]: with pg_pool.getConn() as conn: with conn.cursor() as cur: # 查询用户角色 - cur.execute( - "SELECT roles FROM users WHERE id = %s", - (user_id,) - ) + cur.execute("SELECT roles FROM users WHERE id = %s", (user_id,)) role_row = cur.fetchone() if not role_row: return [] # 用户不存在 @@ -69,27 +64,37 @@ def get_all_ai_bot(user_id: str, module: str) -> List[Dict]: AND is_active = TRUE AND available_roles::jsonb ?| %s """, - (module, user_roles) + (module, user_roles), ) rows = cur.fetchall() result = [] for row in rows: # row 索引对应 SELECT 字段顺序 - id_, title, description, welcome_words, ai_personality, available_report_tables, available_kn_bases = row + ( + id_, + title, + description, + welcome_words, + ai_personality, + available_report_tables, + available_kn_bases, + ) = row # 解析 JSON roles_json = ai_personality if ai_personality else {} - result.append({ - "id": id_, - "title": title, - "description": description, - "welcome_words": welcome_words, - "name": roles_json.get("名字", ""), - "role": roles_json.get("性格", ""), - "service": roles_json.get("业务", ""), - "available_report_tables": available_report_tables, - "available_kn_bases": available_kn_bases - }) + result.append( + { + "id": id_, + "title": title, + "description": description, + "welcome_words": welcome_words, + "name": roles_json.get("名字", ""), + "role": roles_json.get("性格", ""), + "service": roles_json.get("业务", ""), + "available_report_tables": available_report_tables, + "available_kn_bases": available_kn_bases, + } + ) return result @@ -98,9 +103,7 @@ def get_all_ai_bot(user_id: str, module: str) -> List[Dict]: def insert_message(session_id: str, isAI: bool, content: str): with pg_pool.getConn() as conn: history = PostgresChatMessageHistory( - database_name, - session_id, - sync_connection=conn + database_name, session_id, sync_connection=conn ) if isAI: history.add_ai_message(content) @@ -112,15 +115,10 @@ def get_history(session_id: str): simplified = [] with pg_pool.getConn() as conn: history = PostgresChatMessageHistory( - database_name, - session_id, - sync_connection=conn + database_name, session_id, sync_connection=conn ) for msg in history.messages: - simplified.append({ - "type": msg.type, - "content": msg.content - }) + simplified.append({"type": msg.type, "content": msg.content}) return simplified @@ -129,28 +127,33 @@ def get_history_with_time(session_id: str, number: int): with pg_pool.getConn() as conn: with conn.cursor() as cur: cur.execute( - f"SELECT message, created_at FROM ai_chat_history WHERE session_id = '{session_id}' ORDER BY created_at DESC LIMIT {number}") + f"SELECT message, created_at FROM ai_chat_history WHERE session_id = '{session_id}' ORDER BY created_at DESC LIMIT {number}" + ) rows = cur.fetchall() simplified = [] for row in rows: msg_dict = row[0] - simplified.append({ - "type": msg_dict.get("type"), - "created_at": row[1].isoformat(), - "content": msg_dict.get("data", {}).get("content") - }) + simplified.append( + { + "type": msg_dict.get("type"), + "created_at": row[1].isoformat(), + "content": msg_dict.get("data", {}).get("content"), + } + ) return simplified # ————————————————————————————————————————————————————会话——————————————————————————————— -def insert_session(user_id: str, ai_id: str, session_id: str, session_title: str, available_module): +def insert_session( + user_id: str, ai_id: str, session_id: str, session_title: str, available_module +): with pg_pool.getConn() as coon: with coon.cursor() as cur: cur.execute( "INSERT INTO ai_chat_sessions (id ,user_id, ai_id, title, available_module, created_at, updated_at) VALUES (%s, %s, %s, %s,%s, NOW(), NOW())", - (session_id, user_id, ai_id, session_title, available_module) + (session_id, user_id, ai_id, session_title, available_module), ) coon.commit() @@ -160,7 +163,7 @@ def update_session_updated_at(session_id: str): with conn.cursor() as cur: cur.execute( "UPDATE ai_chat_sessions SET updated_at = NOW() WHERE id = %s", - (session_id,) + (session_id,), ) conn.commit() @@ -173,16 +176,12 @@ def get_sessions(user_id: str, available_module: str): "FROM ai_chat_sessions " "WHERE user_id = %s AND available_module = %s " "ORDER BY updated_at DESC", - (user_id, available_module) + (user_id, available_module), ) sessions = cur.fetchall() return [ - { - "id": row[0], - "title": row[1], - "updated_at": row[2] - } + {"id": row[0], "title": row[1], "updated_at": row[2]} for row in sessions ] @@ -193,16 +192,10 @@ def get_reports(user_id: str): with conn.cursor() as cur: cur.execute( "SELECT id, title FROM ai_reports WHERE created_by = %s AND is_masked = TRUE ORDER BY created_at DESC", - (user_id,) + (user_id,), ) reports = cur.fetchall() - return [ - { - "id": row[0], - "title": row[1] - } - for row in reports - ] + return [{"id": row[0], "title": row[1]} for row in reports] def save_report(id: str, user_id: str, title: str, sql: str): @@ -210,7 +203,7 @@ def save_report(id: str, user_id: str, title: str, sql: str): with conn.cursor() as cur: cur.execute( "INSERT INTO ai_reports (id, title, sql, created_at, created_by , is_masked) VALUES (%s, %s, %s, NOW(), %s, FALSE) RETURNING id", - (id, title, sql, user_id) + (id, title, sql, user_id), ) report_id = cur.fetchone()[0] conn.commit() @@ -222,7 +215,7 @@ def maked_report(report_id: str, title: str): with conn.cursor() as cur: cur.execute( "UPDATE ai_reports SET title = %s, is_masked = TRUE WHERE id = %s", - (title, report_id) + (title, report_id), ) conn.commit() @@ -230,10 +223,7 @@ def maked_report(report_id: str, title: str): def getSQL(reportId: str): with pg_pool.getConn() as conn: with conn.cursor() as cur: - cur.execute( - "SELECT sql FROM ai_reports WHERE id = %s", - (reportId,) - ) + cur.execute("SELECT sql FROM ai_reports WHERE id = %s", (reportId,)) row = cur.fetchone() if row: return row[0] @@ -247,7 +237,7 @@ def get_available_tables_str(aiId: str): # 1. 先取 AI 可用的数据库表 cur.execute( "SELECT available_report_tables FROM ai_chat_profiles WHERE id = %s", - (aiId,) + (aiId,), ) role_row = cur.fetchone() if not role_row: @@ -258,7 +248,7 @@ def get_available_tables_str(aiId: str): return "无数据库表可用" # 2. 构造 IN 查询占位符 - placeholders = ','.join(['%s'] * len(available_tables)) + placeholders = ",".join(["%s"] * len(available_tables)) sql_query = f""" SELECT id, name, description FROM ai_reports_tables @@ -272,7 +262,7 @@ def get_available_tables_str(aiId: str): for table in tableIds: cur.execute( "SELECT name, type, description FROM ai_reports_fields WHERE table_id = %s AND is_active = TRUE", - (table[0],) + (table[0],), ) columns = cur.fetchall() result += f"{table[1]}:{table[2]}\n" @@ -291,8 +281,15 @@ def get_available_tables(): cursor.execute( "SELECT id, name, description,is_active FROM ai_reports_tables", ) - return [{"id": row[0], "name": row[1], "description": row[2], "is_active": row[3]} for row in - cursor.fetchall()] + return [ + { + "id": row[0], + "name": row[1], + "description": row[2], + "is_active": row[3], + } + for row in cursor.fetchall() + ] # 新增表 @@ -305,7 +302,7 @@ def add_table(name, description, user_id): VALUES (%s, %s, %s) RETURNING id """, - (name, description, user_id) + (name, description, user_id), ) new_id = cursor.fetchone()[0] # 取返回的 id return new_id @@ -319,8 +316,16 @@ def get_fields_by_table_id(table_id): "SELECT id, name, type, description, is_active FROM ai_reports_fields WHERE table_id = %s", (table_id,), ) - return [{"id": row[0], "name": row[1], "type": row[2], "description": row[3], "is_active": row[4]} for row - in cursor.fetchall()] + return [ + { + "id": row[0], + "name": row[1], + "type": row[2], + "description": row[3], + "is_active": row[4], + } + for row in cursor.fetchall() + ] # 新增字段 @@ -329,18 +334,26 @@ def add_field(name, type, description, is_active, table_id, user_id): with conn.cursor() as cursor: cursor.execute( "INSERT INTO ai_reports_fields (name,type,description, is_active, create_by, table_id) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id", - (name, type, description, is_active, user_id, table_id) + (name, type, description, is_active, user_id, table_id), ) new_id = cursor.fetchone()[0] # 取返回的 id return new_id # 新增报表智能体 -def insert_bot(title: str, description: str, welcome_words: str, ai_personality: str, available_module: str, - available_report_tables: str, available_kn_bases: str, user_id: str): +def insert_bot( + title: str, + description: str, + welcome_words: str, + ai_personality: str, + available_module: str, + available_report_tables: str, + available_kn_bases: str, + user_id: str, +): with pg_pool.getConn() as conn: with conn.cursor() as cursor: - available_roles = json.dumps(['user']) + available_roles = json.dumps(["user"]) cursor.execute( """ INSERT INTO ai_chat_profiles @@ -348,19 +361,38 @@ def insert_bot(title: str, description: str, welcome_words: str, ai_personality: VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, now()) RETURNING id """, - (available_module, available_roles, title, description, welcome_words, ai_personality, - available_report_tables, available_kn_bases, user_id) + ( + available_module, + available_roles, + title, + description, + welcome_words, + ai_personality, + available_report_tables, + available_kn_bases, + user_id, + ), ) report_id = cursor.fetchone()[0] return report_id # 更新报表智能体 -def update_bot(id: str, title: str, description: str, welcome_words: str, ai_personality: str, available_module: str, - available_report_tables: str, available_kn_bases: str, user_id: str): +def update_bot( + id: str, + title: str, + description: str, + welcome_words: str, + ai_personality: str, + available_module: str, + available_report_tables: str, + available_kn_bases: str, + user_id: str, +): with pg_pool.getConn() as conn: with conn.cursor() as cursor: - cursor.execute(""" + cursor.execute( + """ UPDATE ai_chat_profiles SET title = %s, description = %s, @@ -373,9 +405,18 @@ def update_bot(id: str, title: str, description: str, welcome_words: str, ai_per updated_by = %s WHERE id = %s """, - (title, description, ai_personality, welcome_words, available_report_tables, - available_kn_bases, available_module, user_id, id) - ) + ( + title, + description, + ai_personality, + welcome_words, + available_report_tables, + available_kn_bases, + available_module, + user_id, + id, + ), + ) # ————————————————————————————————————————————————————知识库——————————————————————————————— @@ -388,11 +429,18 @@ def get_available_knowledge_bases(available_module: str): FROM ai_knowledge WHERE available_module::jsonb @> %s::jsonb """, - (f'["{available_module}"]',) + (f'["{available_module}"]',), ) - return [{"id": row[0], "name": row[1], "description": row[2], "is_active": row[3]} for row in - cursor.fetchall()] + return [ + { + "id": row[0], + "name": row[1], + "description": row[2], + "is_active": row[3], + } + for row in cursor.fetchall() + ] def add_knowledge_base(name, description, user_id): @@ -404,6 +452,108 @@ def add_knowledge_base(name, description, user_id): VALUES (%s, %s, %s, now()) RETURNING id """, - (name, description, user_id) + (name, description, user_id), ) new_id = cursor.fetchone()[0] # 取返回的 id + return new_id + + +# ————————————————————————————————————————————————————仪评指标联识别——————————————————————————————— +from config.minIO import get_temp_url +import utils.MyUtils as MyUtils + + +def get_ticket_image_list(user_id): + with pg_pool.getConn() as conn: + with conn.cursor() as cursor: + cursor.execute( + """ + SELECT created_at, file_name, resolution, size, name, + moisture_content, cocoon_weight, defective_pupa_count, + fresh_shell_weight, sample_count, barcode, oss, + net_weight_total, evaluator, reviewer,id + FROM ticket_images + WHERE created_by = %s + """, + (user_id,), + ) + rows = cursor.fetchall() + result = [] + for row in rows: + result.append( + { + "created_at": MyUtils.format_datetime(row[0]), + "file_name": row[1], + "resolution": row[2], + "size": round(row[3], 2), + "name": row[4], + "moisture_content": row[5], + "cocoon_weight": row[6], + "defective_pupa_count": row[7], + "fresh_shell_weight": row[8], + "sample_count": row[9], + "barcode": row[10], + # "oss_url": f"http://{SERVER_PATH_OSS}:9000/image-ticket/{row[11]}", + "oss_url": get_temp_url("image-ticket", row[11]), + "net_weight_total": row[12], + "evaluator": row[13], + "reviewer": row[14], + "id": row[15], + } + ) + return result + + +def insert_ticket_image( + created_by, + file_name, + resolution, + size, + name, + moisture_content, + cocoon_weight, + defective_pupa_count, + fresh_shell_weight, + sample_count, + barcode, + oss, + net_weight_total, + evaluator, + reviewer, +): + with pg_pool.getConn() as conn: + with conn.cursor() as cursor: + cursor.execute( + """ + INSERT INTO ticket_images ( + created_by, file_name, resolution, size, name, + moisture_content, cocoon_weight, defective_pupa_count, + fresh_shell_weight, sample_count, barcode, oss, + net_weight_total, evaluator, reviewer, created_at + ) + VALUES (%s, %s, %s, %s, %s, + %s, %s, %s, %s, %s, + %s, %s, %s, %s, %s, NOW()) + RETURNING id + """, + ( + created_by, + file_name, + resolution, + size, + name, + moisture_content, + cocoon_weight, + defective_pupa_count, + fresh_shell_weight, + sample_count, + barcode, + oss, + net_weight_total, + evaluator, + reviewer, + ), + ) + new_id = cursor.fetchone()[0] + conn.commit() + return new_id diff --git a/bbit_ai/app/llm/ticketLLM.py b/bbit_ai/app/llm/ticketLLM.py new file mode 100644 index 0000000..5c52bc9 --- /dev/null +++ b/bbit_ai/app/llm/ticketLLM.py @@ -0,0 +1,152 @@ +import json +import re + +from langchain.schema import HumanMessage + +from config.llm import * + + +def get_ticket_response(image_url: str): + # 构建 prompt + prompt_text = f""" +你是一位专业的图像分析AI。你的任务是仔细分析提供的图片内容,并按JSON格式输出结果。 + +## JSON输出结构及字段说明: +# 蚕茧检测信息数据模型(ImageDescription) + +该模型用于描述蚕茧检测信息,每条记录包含以下字段: + +## 1. 含水率 (`moisture_content`) +- **类型**:浮点数 +- **描述**:茧的含水量,单位为百分比(%) + +## 2. 下足茧重量 (`cocoon_weight`) +- **类型**:浮点数 +- **描述**:下足茧的重量,单位为克(可带小数) + +## 3. 非好蛹粒数 (`defective_pupa_count`) +- **类型**:整数 +- **描述**:不合格蛹的数量,即非好蛹的个数 + +## 4. 鲜壳量 (`fresh_shell_weight`) +- **类型**:浮点数 +- **描述**:鲜壳重量,单位为克(可带小数) + +## 5. 小样粒数 (`sample_count`) +- **类型**:整数 +- **描述**:检测使用的小样数量,即用于检测的茧粒数 + +## 6. 净重合计 (`net_weight_total`) +- **类型**:浮点数 +- **描述**:所有样品的净重总和,单位为克 + +## 7. 仪评人姓名 (`evaluator`) +- **类型**:字符串 +- **描述**:进行仪器检测的人员姓名,可能为空 + +## 8. 复核人员姓名 (`reviewer`) +- **类型**:字符串 +- **描述**:复核人员姓名,可能为空 + +--- + +### 注意事项 +- 所有字段都是必填的(required),在 JSON 实例中必须提供值 +- 浮点数字段可以包含小数,整数字段只能是整数 +- `evaluator` 和 `reviewer` 可以为空字符串,但字段必须存在 + +最后,只输出严格的 JSON 格式,不要包含其他文字、markdown等内容。 +""" + messages = [ + HumanMessage( + content=[ + {"type": "text", "text": prompt_text}, + {"type": "image_url", "image_url": {"url": image_url}}, + ] + ) + ] + # 直接调用模型 + llmRes = llmVision.invoke(messages).content + # 去掉 ```json 和 ``` 包裹 + cleaned = re.sub(r"^```json\s*|\s*```$", "", llmRes.strip()) + # 解析 JSON + jsonRes = json.loads(cleaned) + jsonRes["barcode"] = decode_barcode(image_url) + return jsonRes + + +import os +import base64 +import tempfile +import requests +from pyzxing import BarCodeReader +from PIL import Image +from io import BytesIO +from fastapi import UploadFile + + +def decode_barcode(input_data) -> str: + """ + 自动识别输入类型并解析条码: + - URL 字符串 + - Base64 字符串 + - UploadFile / bytes / 文件对象 + 返回第一个条码的内容,解析失败返回空字符串 + """ + # ---------------- 输入处理 ---------------- + img = None + + # URL + if isinstance(input_data, str) and ( + input_data.startswith("http://") or input_data.startswith("https://") + ): + response = requests.get(input_data) + response.raise_for_status() + img = Image.open(BytesIO(response.content)) + + # Base64 字符串 + elif isinstance(input_data, str): + # 过滤 data URI 前缀 + if "," in input_data: + input_data = input_data.split(",")[1] + try: + img_data = base64.b64decode(input_data) + img = Image.open(BytesIO(img_data)) + except Exception: + raise ValueError("无法解析 Base64 字符串") + + # UploadFile / bytes / 文件对象 + elif isinstance(input_data, UploadFile): + content = input_data.file.read() + input_data.file.seek(0) + img = Image.open(BytesIO(content)) + elif isinstance(input_data, bytes): + img = Image.open(BytesIO(input_data)) + elif hasattr(input_data, "read"): # 文件对象 + content = input_data.read() + if hasattr(input_data, "seek"): + input_data.seek(0) + img = Image.open(BytesIO(content)) + else: + raise ValueError("不支持的输入类型") + + # ---------------- 临时文件处理 ---------------- + tmp_fd, tmp_path = tempfile.mkstemp(suffix=".png") + try: + with open(tmp_path, "wb") as f: + img.save(f, format="PNG") + + # ---------------- 调用 pyzxing ---------------- + reader = BarCodeReader() + result = reader.decode(tmp_path) + + if result: + parsed = result[0].get("parsed", "") + if isinstance(parsed, bytes): + parsed = parsed.decode("utf-8") + return parsed + return "" + finally: + os.close(tmp_fd) + if os.path.exists(tmp_path): + os.remove(tmp_path) diff --git a/bbit_ai/app/models/F8ImageRequest.py b/bbit_ai/app/models/F8ImageRequest.py new file mode 100644 index 0000000..aa6bede --- /dev/null +++ b/bbit_ai/app/models/F8ImageRequest.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class F8ImageRequest(BaseModel): + title: str + image: str diff --git a/bbit_ai/app/models/ImageRequest.py b/bbit_ai/app/models/ImageRequest.py new file mode 100644 index 0000000..d1d8fa0 --- /dev/null +++ b/bbit_ai/app/models/ImageRequest.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class ImageRequest(BaseModel): + image: str diff --git a/bbit_ai/app/routers/F8.py b/bbit_ai/app/routers/F8.py new file mode 100644 index 0000000..b62da27 --- /dev/null +++ b/bbit_ai/app/routers/F8.py @@ -0,0 +1,30 @@ +import base64 + +from fastapi import APIRouter + +from config.app import F8_SERVER_USER_ID +from models.BaseResponse import BaseResponse +from models.F8ImageRequest import F8ImageRequest +from service.vision import process_ticket_image +from utils import MyUtils + +f8Router = APIRouter() + + +@f8Router.post("/createTicketImageTask") +async def cocoonTicket(data: F8ImageRequest): + input_data = data.image + if "," in input_data: + input_data = input_data.split(",")[1] + try: + img_bytes = base64.b64decode(input_data) + json_data = await MyUtils.async_task( + process_ticket_image, + img_bytes, + f"{data.title}.jpg", + data.title, + F8_SERVER_USER_ID, + ) + return BaseResponse(data=json_data) + except Exception as e: + return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None) diff --git a/bbit_ai/app/routers/Vision.py b/bbit_ai/app/routers/Vision.py new file mode 100644 index 0000000..5d18d81 --- /dev/null +++ b/bbit_ai/app/routers/Vision.py @@ -0,0 +1,51 @@ +from uuid import UUID + +from fastapi import APIRouter, File, Form, Depends + +import db.postgres as pg +from config.security import get_user_id_from_token +from llm.ticketLLM import * +from models.BaseResponse import BaseResponse +from models.ImageRequest import ImageRequest +from service.vision import process_ticket_image +from utils import MyUtils + +visionRouter = APIRouter() + + +# 测试接口,已经可以实现 +@visionRouter.post("/cocoonTicket") +def cocoonTicket(data: ImageRequest, user_id: UUID = Depends(get_user_id_from_token)): + if not user_id: + return {"error": "userId is required"} + try: + json = get_ticket_response(data.image) + return BaseResponse(data=json) + except: + return BaseResponse(status=False, message="unknown error", data=None) + + +@visionRouter.post("/createTicketImageTask") +async def createTicketImageTask( + file: UploadFile = File(...), + projectName: str = Form(...), + user_id: UUID = Depends(get_user_id_from_token), +): + if not user_id: + return {"error": "userId is required"} + try: + contents = await file.read() + json_data = await MyUtils.async_task( + process_ticket_image, contents, file.filename, projectName, user_id + ) + return BaseResponse(data=json_data) + except Exception as e: + print(str(e)) + return BaseResponse(status=False, message=f"解析失败: {str(e)}", data=None) + + +@visionRouter.get("/getTicketImageList") +def cocoonTicket(user_id: UUID = Depends(get_user_id_from_token)): + if not user_id: + return {"error": "userId is required"} + return BaseResponse(data=pg.get_ticket_image_list(user_id)) diff --git a/bbit_ai/app/service/vision.py b/bbit_ai/app/service/vision.py new file mode 100644 index 0000000..abed7aa --- /dev/null +++ b/bbit_ai/app/service/vision.py @@ -0,0 +1,60 @@ +import uuid +from uuid import UUID + +import config.minIO as minIO +import db.postgres as pg +from config.minIO import minio_client +from llm.ticketLLM import * + + +def process_ticket_image( + img_bytes: bytes, + file_name: str = None, + project_name: str = None, + user_id: UUID = None, +): + """ + 处理票据图片的核心逻辑,供不同接口调用 + """ + + # 上传到 OSS,使用 UUID 做对象名 + object_name = str(uuid.uuid4()) + file_bytes = BytesIO(img_bytes) + bucket_name = "image-ticket" + if not minio_client.bucket_exists(bucket_name): + minio_client.make_bucket(bucket_name) + + minIO.push_file(bucket_name, object_name, file_bytes, img_bytes, "image/jpeg") + oss_url = minIO.get_temp_url(bucket_name, object_name) + + # 调用分析方法获取 JSON + json_data = get_ticket_response(oss_url) + # 解析条码 + barcode = decode_barcode(BytesIO(img_bytes)) + json_data["barcode"] = barcode + + # 获取图片分辨率和大小 + img = Image.open(BytesIO(img_bytes)) + resolution = f"{img.width}x{img.height}" + size_kb = len(img_bytes) / 1024 + + # 插入数据库 + pg.insert_ticket_image( + created_by=user_id, + file_name=file_name, + resolution=resolution, + size=size_kb, + name=project_name if project_name else object_name[:8], + moisture_content=json_data.get("moisture_content"), + cocoon_weight=json_data.get("cocoon_weight"), + defective_pupa_count=json_data.get("defective_pupa_count"), + fresh_shell_weight=json_data.get("fresh_shell_weight"), + sample_count=json_data.get("sample_count"), + barcode=barcode, + oss=object_name, + net_weight_total=json_data.get("net_weight_total"), + evaluator=json_data.get("evaluator"), + reviewer=json_data.get("reviewer"), + ) + + return json_data diff --git a/bbit_ai/app/utils/MyUtils.py b/bbit_ai/app/utils/MyUtils.py index a8c2b0a..27ac1f7 100644 --- a/bbit_ai/app/utils/MyUtils.py +++ b/bbit_ai/app/utils/MyUtils.py @@ -1,6 +1,27 @@ import threading + # 后台操作 def async_db_task(func, *args, **kwargs): """将数据库操作放到后台线程执行""" - threading.Thread(target=func, args=args, kwargs=kwargs, daemon=True).start() \ No newline at end of file + threading.Thread(target=func, args=args, kwargs=kwargs, daemon=True).start() + + +import asyncio + + +async def async_task(func, *args, **kwargs): + return await asyncio.to_thread(func, *args, **kwargs) + + +from datetime import datetime + +import pytz + + +def format_datetime(dt: datetime, tz="Asia/Shanghai"): + if dt.tzinfo is None: + dt = pytz.UTC.localize(dt) + tz_obj = pytz.timezone(tz) + dt = dt.astimezone(tz_obj) + return dt.strftime("%Y-%m-%d %H:%M:%S") diff --git a/bbit_ai/Dockerfile b/bbit_ai/docker/Dockerfile similarity index 72% rename from bbit_ai/Dockerfile rename to bbit_ai/docker/Dockerfile index ca69512..24a0f6e 100644 --- a/bbit_ai/Dockerfile +++ b/bbit_ai/docker/Dockerfile @@ -1,13 +1,8 @@ # 使用官方 Python 镜像 FROM python:3.10-slim -# 设置工作目录 WORKDIR /app -# 复制依赖文件 -COPY requirements.txt . -# 更新系统源,安装 PostgreSQL 和 ODBC 依赖,以及微软 SQL Server 驱动 -# 安装基础依赖和 Microsoft ODBC 驱动依赖 RUN apt-get update && \ apt-get install -y --no-install-recommends \ libpq5 \ @@ -23,14 +18,25 @@ RUN apt-get update && \ ACCEPT_EULA=Y apt-get install -y msodbcsql18 && \ rm -rf /var/lib/apt/lists/* - +COPY docker/requirements.txt . # 安装 Python 依赖 RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt +# 复制并解压 JRE +COPY docker/OpenJDK17U-jre_x64_linux_hotspot_17.0.16_8.tar.gz /opt/ +RUN tar -xzf /opt/OpenJDK17U-jre_x64_linux_hotspot_17.0.16_8.tar.gz -C /opt/ && \ + rm /opt/OpenJDK17U-jre_x64_linux_hotspot_17.0.16_8.tar.gz + +# 配置 Java 环境 +ENV JAVA_HOME=/opt/jdk-17.0.16+8-jre +ENV PATH="$JAVA_HOME/bin:$PATH" + + # 复制项目代码 COPY app/ . EXPOSE 13011 # 启动命令(使用 uvicorn 启动 FastAPI) -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "13011"] +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "13011", "--workers", "4"] + diff --git a/bbit_ai/docker/OpenJDK17U-jre_x64_linux_hotspot_17.0.16_8.tar.gz b/bbit_ai/docker/OpenJDK17U-jre_x64_linux_hotspot_17.0.16_8.tar.gz new file mode 100644 index 0000000..915161c Binary files /dev/null and b/bbit_ai/docker/OpenJDK17U-jre_x64_linux_hotspot_17.0.16_8.tar.gz differ diff --git a/bbit_ai/requirements.txt b/bbit_ai/docker/requirements.txt similarity index 84% rename from bbit_ai/requirements.txt rename to bbit_ai/docker/requirements.txt index fb9d133..beca369 100644 --- a/bbit_ai/requirements.txt +++ b/bbit_ai/docker/requirements.txt @@ -16,3 +16,7 @@ typing_extensions==4.15.0 uvicorn[standard] pyodbc==5.2.0 dashscope==1.24.2 +minio==7.2.16 +pyzxing==1.1.1 +Pillow==11.3.0 +python-multipart==0.0.20 diff --git a/bbit_ai/test/vision/Vision.py b/bbit_ai/test/vision/Vision.py new file mode 100644 index 0000000..d0e9cb1 --- /dev/null +++ b/bbit_ai/test/vision/Vision.py @@ -0,0 +1,56 @@ +import os +import base64 +from langchain.prompts import PromptTemplate +from langchain_openai import ChatOpenAI +from langchain.output_parsers import JsonOutputParser +from langchain.schema import HumanMessage +from pydantic import BaseModel, Field + +# 定义你想要的结构化输出 +class ImageDescription(BaseModel): + objects: list[str] = Field(description="图片里出现的主要物体") + scene: str = Field(description="场景描述") + mood: str = Field(description="整体氛围") + +parser = JsonOutputParser(pydantic_object=ImageDescription) + +prompt = PromptTemplate( + template="""你是一个图像分析助手。请根据输入的图片内容和文字说明, +输出符合下列 JSON schema 的结果: +{ + "moisture_content": 12.5, + "cocoon_weight": 15.2, + "defective_pupa_count": 3, + "fresh_shell_weight": 8.7, + "sample_count": 50, + "net_weight_total": 760, + "evaluator": "张三", + "reviewer": "李四", + "barcode": "123456789012" +} + +输入内容: +{query} +""", + input_variables=["query"], + partial_variables={"format_instructions": parser.get_format_instructions()}, +) + +def get_response(base64_image: str, system_text: str = "请分析这张图片并输出JSON结果"): + messages = [ + HumanMessage(content=[ + {"type": "text", "text": system_text}, + # {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}} # 本地图片转成base64 + {"type": "image_url", "image_url": {"url": {base64_image}}} # 本地图片转成base64 + ]) + ] + + # 串起来:prompt -> llm -> parser + chain = prompt | llm | parser + response = chain.invoke({"query": messages}) + print(response) + +if __name__ == "__main__": + with open("test.jpg", "rb") as f: + base64_image = base64.b64encode(f.read()).decode("utf-8") + get_response(base64_image) diff --git a/vue/apps/web-antd/src/api/cv/index.ts b/vue/apps/web-antd/src/api/cv/index.ts index 20530e9..442809a 100644 --- a/vue/apps/web-antd/src/api/cv/index.ts +++ b/vue/apps/web-antd/src/api/cv/index.ts @@ -1,2 +1,3 @@ export * from './iva'; export * from './sca'; +export * from './ticket'; diff --git a/vue/apps/web-antd/src/api/cv/ticket.ts b/vue/apps/web-antd/src/api/cv/ticket.ts new file mode 100644 index 0000000..6ce0dd6 --- /dev/null +++ b/vue/apps/web-antd/src/api/cv/ticket.ts @@ -0,0 +1,19 @@ +import { pyRequestClient } from '#/api/request'; + +/** + * 获取已分析的图片列表 + */ +export async function refreshTicketImageList() { + return pyRequestClient.get('/llm/getTicketImageList'); +} + +/** + * 上传图片分析任务 + */ +export async function createTicketImageTask(formData: FormData) { + return pyRequestClient.post('/llm/createTicketImageTask', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); +} diff --git a/vue/apps/web-antd/src/router/routes/modules/cv.ts b/vue/apps/web-antd/src/router/routes/modules/cv.ts index 00d0841..961c7ff 100644 --- a/vue/apps/web-antd/src/router/routes/modules/cv.ts +++ b/vue/apps/web-antd/src/router/routes/modules/cv.ts @@ -8,7 +8,7 @@ const routes: RouteRecordRaw[] = [ meta: { icon: 'ic:round-remove-red-eye', authority: ['iva', 'sca', 'ysa'], - keepAlive: false, + keepAlive: true, order: 2, title: $t('计算机视觉'), }, @@ -48,6 +48,17 @@ const routes: RouteRecordRaw[] = [ }, component: () => import('#/views/cv/ysa/index.vue'), }, + { + name: 'TICKET', + path: '/cv/ticket', + meta: { + authority: ['ticket'], + icon: 'mdi:ticket-confirmation', + title: $t('仪评指标联分析'), + keepAlive: true, + }, + component: () => import('#/views/cv/ticket/index.vue'), + }, { name: 'CVAT', path: '/cv/cvat', diff --git a/vue/apps/web-antd/src/views/cv/iva/CreateVideoTaskModal.vue b/vue/apps/web-antd/src/views/cv/iva/CreateVideoTaskModal.vue index 05c1a1f..5829384 100644 --- a/vue/apps/web-antd/src/views/cv/iva/CreateVideoTaskModal.vue +++ b/vue/apps/web-antd/src/views/cv/iva/CreateVideoTaskModal.vue @@ -27,6 +27,7 @@ const [Modal, modalApi] = useVbenModal({ onConfirm() { if (!projectName.value || !selectedFile.value) { message.warning('请填写项目名并选择视频文件'); + return; // 阻止继续执行 } uploadFile(); }, diff --git a/vue/apps/web-antd/src/views/cv/sca/CreateYSATaskModal.vue b/vue/apps/web-antd/src/views/cv/sca/CreateYSATaskModal.vue index 67e1b4a..dff3b61 100644 --- a/vue/apps/web-antd/src/views/cv/sca/CreateYSATaskModal.vue +++ b/vue/apps/web-antd/src/views/cv/sca/CreateYSATaskModal.vue @@ -1,19 +1,10 @@ diff --git a/vue/apps/web-antd/src/views/cv/ticket/index.vue b/vue/apps/web-antd/src/views/cv/ticket/index.vue new file mode 100644 index 0000000..6542503 --- /dev/null +++ b/vue/apps/web-antd/src/views/cv/ticket/index.vue @@ -0,0 +1,248 @@ + + + diff --git a/vue/apps/web-antd/src/views/dashboard/workspace/index.vue b/vue/apps/web-antd/src/views/dashboard/workspace/index.vue index b6e3d35..ac99097 100644 --- a/vue/apps/web-antd/src/views/dashboard/workspace/index.vue +++ b/vue/apps/web-antd/src/views/dashboard/workspace/index.vue @@ -2,97 +2,105 @@ import type { WorkbenchProjectItem, WorkbenchQuickNavItem, -} from "@vben/common-ui"; +} from '@vben/common-ui'; -import { useRouter } from "vue-router"; -import { WorkbenchHeader, WorkbenchQuickNav } from "@vben/common-ui"; -import { preferences } from "@vben/preferences"; -import { useUserStore } from "@vben/stores"; -import { openWindow } from "@vben/utils"; +import { useRouter } from 'vue-router'; + +import { WorkbenchHeader, WorkbenchQuickNav } from '@vben/common-ui'; +import { preferences } from '@vben/preferences'; +import { useUserStore } from '@vben/stores'; +import { openWindow } from '@vben/utils'; const userStore = useUserStore(); // 同样,这里的 url 也可以使用以 http 开头的外部链接 const cv: WorkbenchQuickNavItem[] = [ { - color: "#3fb27f", + color: '#3fb27f', authority: ['iva'], - icon: "mdi:video", - title: "视频智能分析", - url: "/cv/iva" + icon: 'mdi:video', + title: '视频智能分析', + url: '/cv/iva', }, { - color: "#3fb27f", + color: '#3fb27f', authority: ['sca'], - icon: "mdi:ice-cream", - title: "蚕茧仪评分析", - url: "/cv/sca" + icon: 'mdi:ice-cream', + title: '蚕茧仪评分析', + url: '/cv/sca', }, { - color: "#3fb27f", + color: '#3fb27f', authority: ['ysa'], - icon: "mdi:waveform", - title: "催青阶段分析", - url: "/cv/ysa" + icon: 'mdi:waveform', + title: '催青阶段分析', + url: '/cv/ysa', }, { - color: "#3fb27f", - icon: "ion:bar-chart-outline", - title: "标注平台入口", + color: '#3fb27f', + authority: ['ticket'], + icon: 'mdi:ticket-confirmation', + title: '仪评指标联分析', + url: '/cv/ticket', + }, + { + color: '#3fb27f', + icon: 'ion:bar-chart-outline', + title: '标注平台入口', authority: ['user'], - url: "http://171.212.101.199:13013/" + url: 'http://171.212.101.199:13013/', }, ]; const llm: WorkbenchQuickNavItem[] = [ { - color: "#1fdaca", + color: '#1fdaca', authority: ['bot'], - icon: "mdi:face-agent", - title: "通用智能体", - url: "/llm/bot" + icon: 'mdi:face-agent', + title: '通用智能体', + url: '/llm/bot', }, { - color: "#1fdaca", + color: '#1fdaca', authority: ['report'], icon: 'mdi:set-center', - title: "智农观数阁", - url: "/llm/report/report-chat" + title: '智农观数阁', + url: '/llm/report/report-chat', }, { - color: "#1fdaca", + color: '#1fdaca', authority: ['service'], icon: 'mdi:android-head', - title: "灵思智服阁", - url: "/llm/service/service-chat" + title: '灵思智服阁', + url: '/llm/service/service-chat', }, ]; const common: WorkbenchQuickNavItem[] = [ { - color: "#bf0c2c", + color: '#bf0c2c', authority: ['remote'], - icon: "carbon:workspace", - title: "设备远程控制", - url: "/remote" + icon: 'carbon:workspace', + title: '设备远程控制', + url: '/remote', }, { - color: "#bf0c2c", + color: '#bf0c2c', icon: 'ion:grid-outline', - title: "RAGFlow", + title: 'RAGFlow', authority: ['user'], - url: "/out/rag" + url: '/out/rag', }, ]; const router = useRouter(); // 这是一个示例方法,实际项目中需要根据实际情况进行调整 function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) { - if (nav.url?.startsWith("http")) { + if (nav.url?.startsWith('http')) { openWindow(nav.url); return; } - if (nav.url?.startsWith("/")) { + if (nav.url?.startsWith('/')) { router.push(nav.url).catch((error) => { - console.error("Navigation failed:", error); + console.error('Navigation failed:', error); }); } else { console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`); @@ -101,10 +109,10 @@ function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) { function getGreeting() { const hour = new Date().getHours(); - if (hour < 6) return "凌晨好"; - if (hour < 12) return "早安"; - if (hour < 18) return "下午好"; - return "晚上好"; + if (hour < 6) return '凌晨好'; + if (hour < 12) return '早安'; + if (hour < 18) return '下午好'; + return '晚上好'; }