仪评指标联分析模块

This commit is contained in:
BBIT-Kai
2025-09-24 13:59:00 +08:00
parent 0ab82b00d6
commit d2775f60a7
28 changed files with 1106 additions and 181 deletions
+5
View File
@@ -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
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$/app" />
<orderEntry type="jdk" jdkName="bbit_ai_lab" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
+3 -1
View File
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12" />
<option name="enabledOnReformat" value="true" />
<option name="enabledOnSave" value="true" />
<option name="sdkName" value="bbit_ai_lab" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="bbit_ai_lab" project-jdk-type="Python SDK" />
</project>
+60
View File
@@ -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="复核人员姓名,可能为空"
)
+20 -12
View File
@@ -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"])
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"])
+3
View File
@@ -0,0 +1,3 @@
SERVER_PATH_OSS = "s1.ronsunny.cn"
F8_SERVER_USER_ID = "da33efb9-776a-443b-b1ec-dbbbf08793d7"
+8 -3
View File
@@ -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(
+3 -3
View File
@@ -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"},
+27
View File
@@ -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)
)
+240 -90
View File
@@ -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
+152
View File
@@ -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)
+6
View File
@@ -0,0 +1,6 @@
from pydantic import BaseModel
class F8ImageRequest(BaseModel):
title: str
image: str
+5
View File
@@ -0,0 +1,5 @@
from pydantic import BaseModel
class ImageRequest(BaseModel):
image: str
+30
View File
@@ -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)
+51
View File
@@ -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))
+60
View File
@@ -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
+22 -1
View File
@@ -1,6 +1,27 @@
import threading
# 后台操作
def async_db_task(func, *args, **kwargs):
"""将数据库操作放到后台线程执行"""
threading.Thread(target=func, args=args, kwargs=kwargs, daemon=True).start()
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")
@@ -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"]
@@ -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
+56
View File
@@ -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)
+1
View File
@@ -1,2 +1,3 @@
export * from './iva';
export * from './sca';
export * from './ticket';
+19
View File
@@ -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',
},
});
}
@@ -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',
@@ -27,6 +27,7 @@ const [Modal, modalApi] = useVbenModal({
onConfirm() {
if (!projectName.value || !selectedFile.value) {
message.warning('请填写项目名并选择视频文件');
return; // 阻止继续执行
}
uploadFile();
},
@@ -1,19 +1,10 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, Input, message } from 'ant-design-vue';
import { createVideoTask } from '#/api/cv/iva';
const projectName = ref('');
const year = ref<null | number>(null);
const month = ref<null | number>(null);
const day = ref<null | number>(null);
const hours = ref<null | number>(null);
const minutes = ref<null | number>(null);
const seconds = ref<null | number>(null);
const fileName = ref('');
const selectedFile = ref<File | null>(null);
const fileInputRef = ref<HTMLInputElement | null>(null);
@@ -27,6 +18,7 @@ const [Modal, modalApi] = useVbenModal({
onConfirm() {
if (!projectName.value || !selectedFile.value) {
message.warning('请填写项目名并选择蚕茧图片');
return;
}
uploadFile();
},
@@ -41,12 +33,6 @@ async function uploadFile() {
modalApi.close();
// 清空表单
projectName.value = '';
year.value = null;
month.value = null;
day.value = null;
hours.value = null;
minutes.value = null;
seconds.value = null;
fileName.value = '';
selectedFile.value = null;
});
@@ -60,7 +46,7 @@ function handleFileChange(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
selectedFile.value = files[0];
fileName.value = files[0]!.name;
fileName.value = files[0].name;
}
}
</script>
@@ -0,0 +1,248 @@
<script setup lang="ts">
import { onDeactivated, onMounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Form, Input, message } from 'ant-design-vue';
import * as api from '#/api';
const list = ref<any[]>([]);
const error = ref<null | string>(null);
const selectedItem = ref<any>(null);
async function loadList() {
error.value = null;
list.value = (await api.refreshTicketImageList()) || [];
}
function createTask() {
modalApi.open();
}
async function selectItem(item: any) {
selectedItem.value = item;
refreshLineChart();
}
onMounted(() => {
loadList();
});
const showInfoStr = ref<Record<string, number | string>>({});
const showInfoStr2 = ref<Record<string, number | string>>({});
function refreshLineChart() {
const data = selectedItem.value;
showInfoStr.value = {
项目名: data.name,
上传时间: data.created_at,
文件名: data.file_name,
文件大小: `${data.size} MB`,
分辨率: data.resolution,
};
showInfoStr2.value = {
含水量: `${data.moisture_content} %`,
下足茧重: `${data.cocoon_weight} g`,
'非蛹/僵蚕': `${data.defective_pupa_count}`,
鲜壳量: `${data.fresh_shell_weight} g`,
小样数: `${data.sample_count}`,
净重合计: `${data.net_weight_total} kg`,
仪评人: data.evaluator,
复核人: data.reviewer,
条码: data.barcode,
};
}
onDeactivated(() => {
// 离开路由时清理状态
selectedItem.value = null;
showInfoStr.value = {};
});
const projectName = ref('');
const fileName = ref('');
const selectedFile = ref<File | null>(null);
const fileInputRef = ref<HTMLInputElement | null>(null);
const [Modal, modalApi] = useVbenModal({
title: '新建仪评指标联分析任务',
class: 'w-[600px]',
onCancel() {
modalApi.close();
},
onConfirm() {
if (!selectedFile.value) {
message.warning('请选择指标联图片');
return;
}
uploadFile();
},
});
async function uploadFile() {
if (!selectedFile.value) {
message.warning('请选择图片');
return;
}
// 先关闭弹窗
modalApi.close();
try {
const formData = new FormData();
formData.append('file', selectedFile.value);
formData.append('projectName', projectName.value);
await api.createTicketImageTask(formData);
// 接口完成后再触发事件
message.success('分析任务完成');
loadList();
// 清空表单
selectedFile.value = null;
projectName.value = '';
fileName.value = '';
} catch {
message.error('上传失败');
}
}
function selectFile() {
fileInputRef.value?.click();
}
function handleFileChange(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
selectedFile.value = files[0];
fileName.value = files[0].name;
}
}
</script>
<template>
<div class="flex h-[90dvh] w-full flex-col">
<Modal>
<Form layout="vertical">
<Form.Item label="任务名称">
<Input v-model:value="projectName" placeholder="可为空,将取随机值" />
</Form.Item>
<Form.Item label="仪评指标联图片" required>
<div
@click="selectFile"
style="
padding: 16px;
text-align: center;
cursor: pointer;
border: 1px dashed #d9d9d9;
"
>
{{ fileName || '点击选择文件' }}
<input
type="file"
accept="image/*"
ref="fileInputRef"
@change="handleFileChange"
style="display: none"
/>
</div>
</Form.Item>
</Form>
</Modal>
<BaseModal />
<div class="flex h-full w-full bg-gray-50">
<!-- 左侧筛选 + 列表 -->
<div class="flex w-64 flex-col border-r bg-white p-4">
<!-- 按钮组 -->
<div class="mb-4 flex justify-between space-x-2">
<Button type="primary" @click="createTask" class="flex-1">
新建任务
</Button>
</div>
<!-- 列表 -->
<div class="flex-1 space-y-2 overflow-auto">
<div
v-for="item in list"
:key="item.v_id"
@click="selectItem(item)"
class="cursor-pointer rounded border p-3 hover:bg-gray-100"
:class="{ 'bg-gray-100': item.id === selectedItem?.id }"
>
<div class="text-base font-medium">{{ item.name }}</div>
<div class="text-sm text-gray-400">{{ item.created_at }}</div>
</div>
</div>
</div>
<!-- 右侧Tab 内容区 -->
<div class="flex flex-1 flex-col overflow-hidden p-6">
<div
v-if="!selectedItem"
class="flex h-full items-center justify-center text-gray-400"
>
请先选择左侧列表中的分析任务
</div>
<template v-else>
<div class="flex h-full flex-col gap-4">
<!-- 主内容区域左右结构 -->
<div class="flex flex-1 gap-4">
<!-- 左侧 -->
<div class="flex w-72 flex-col gap-4">
<!-- 视频基础信息展示 -->
<div
class="w-full rounded border bg-white p-4"
id="video_base_info"
>
<div
v-for="(value, key) in showInfoStr"
:key="key"
class="mb-2 flex text-sm text-gray-700"
>
<div class="w-28 font-medium text-gray-900">
{{ key }}
</div>
<div class="flex-1 break-all text-gray-600">
{{ value || '—' }}
</div>
</div>
</div>
<!-- 空白卡片 -->
<div class="flex-1 rounded border bg-white p-4">
<div
v-for="(value, key) in showInfoStr2"
:key="key"
class="mb-2 flex text-sm text-gray-700"
>
<div class="w-32 font-medium text-gray-900">
{{ key }}
</div>
<div class="flex-1 break-all text-gray-600">
{{ value || '—' }}
</div>
</div>
</div>
</div>
<!-- 右侧 -->
<div class="flex flex-1 flex-col gap-4">
<!-- 左右两个图片显示 -->
<!-- 左图 -->
<div
class="flex h-full w-full items-center justify-center rounded border bg-white p-4"
>
<img
:src="selectedItem?.oss_url"
alt="左图"
class="h-[80dvh] w-full object-contain"
/>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
@@ -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 '晚上好';
}
</script>