Compare commits
No commits in common. "defeng-patch" and "main" have entirely different histories.
defeng-pat
...
main
203
app.py
203
app.py
@ -40,7 +40,7 @@ from pydantic import BaseModel
|
|||||||
from dataprocess import _split_markdown
|
from dataprocess import _split_markdown
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
from fileparse_util import process_document
|
from fileparse_util import process_document, extract_image_caption
|
||||||
# import hanlp
|
# import hanlp
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
import requests
|
import requests
|
||||||
@ -57,7 +57,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
import base64
|
import base64
|
||||||
from extract_excel_node_relation import get_excel_node_relation
|
from extract_excel_node_relation import get_excel_node_relation
|
||||||
from doc2pdf import Doc2PDF
|
from doc2pdf import Doc2PDF
|
||||||
from chunk_text import get_chunk_bbox, split_text_preserve_sentences, data_replace, chunk_check,merge_short_slices,find_and_read_content_list,process_pdf_file,process_other_file
|
from chunk_text import get_chunk_bbox, split_text_preserve_sentences, data_replace, chunk_check,merge_short_slices,find_and_read_content_list,process_pdf_file,process_other_file,safe_original_filename,image_base64_to_data_url,extract_image_text,prepare_split_image
|
||||||
from filename_proceess_and_kgquery import get_entity
|
from filename_proceess_and_kgquery import get_entity
|
||||||
from kg_build.extract_filtertext import get_filtertext_node_relation
|
from kg_build.extract_filtertext import get_filtertext_node_relation
|
||||||
import os
|
import os
|
||||||
@ -92,7 +92,7 @@ from default_ontology_config import (
|
|||||||
get_relationships,
|
get_relationships,
|
||||||
get_relationship_types,
|
get_relationship_types,
|
||||||
)
|
)
|
||||||
from config import NEO4J_CONFIG,URL,API_URLS,API_OTHER_URLS
|
from config import NEO4J_CONFIG,URL,API_URLS,API_OTHER_URLS,VLM_CONCURRENCY,SEARCH_DIR
|
||||||
converter = Doc2PDF()
|
converter = Doc2PDF()
|
||||||
DATA_DIR = "/app/files"
|
DATA_DIR = "/app/files"
|
||||||
# ================== 全局资源容器 ==================
|
# ================== 全局资源容器 ==================
|
||||||
@ -102,7 +102,6 @@ _resources: Dict[str, Any] = {
|
|||||||
"async_driver": None,
|
"async_driver": None,
|
||||||
"embedder": None,
|
"embedder": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 图谱检索和索引生成
|
# 图谱检索和索引生成
|
||||||
try:
|
try:
|
||||||
from graph_search.graph_service import GraphService
|
from graph_search.graph_service import GraphService
|
||||||
@ -167,6 +166,7 @@ async def lifespan(app):
|
|||||||
|
|
||||||
|
|
||||||
app = FastAPI(max_request_size=1024 * 1024 * 10, lifespan=lifespan)
|
app = FastAPI(max_request_size=1024 * 1024 * 10, lifespan=lifespan)
|
||||||
|
VLM_SEMAPHORE = asyncio.Semaphore(max(1, VLM_CONCURRENCY))
|
||||||
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -1739,7 +1739,7 @@ async def kg_build(request: KGRequest):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"保存 kg_build 配置缓存失败: {e}")
|
logger.warning(f"保存 kg_build 配置缓存失败: {e}")
|
||||||
|
|
||||||
# 后续进程启动逻辑保持不变 ...
|
# 后续进程启动逻辑保持不变 ...https://gitea.zkzdht.com/Ascend/htknow
|
||||||
cancel_flag_path = f"/tmp/kg_cancel_{task_id}"
|
cancel_flag_path = f"/tmp/kg_cancel_{task_id}"
|
||||||
if os.path.exists(cancel_flag_path):
|
if os.path.exists(cancel_flag_path):
|
||||||
os.remove(cancel_flag_path)
|
os.remove(cancel_flag_path)
|
||||||
@ -2028,6 +2028,105 @@ class RequestWrapper(BaseModel):
|
|||||||
pdf_contents: List[PdfContentItem]
|
pdf_contents: List[PdfContentItem]
|
||||||
|
|
||||||
|
|
||||||
|
class Base64ImageRequest(BaseModel):
|
||||||
|
content: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="Optional text around the image, for example: 图3.1 船舶主发动机组成图(images/test.jpg)",
|
||||||
|
)
|
||||||
|
filename: str = Field(..., description="Original image filename, for example test.png")
|
||||||
|
image_base64: str = Field(..., description="Base64 image content, with or without data:image/... prefix")
|
||||||
|
original_filename:Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="图片 所在pdf文件",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def analyze_image_with_vlm(image_bytes: bytes, suffix: str) -> str:
|
||||||
|
image_url = image_base64_to_data_url(image_bytes, suffix)
|
||||||
|
async with VLM_SEMAPHORE:
|
||||||
|
return await OpenaiAPI.open_api_vl_without_thinking(image_url)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/split_image")
|
||||||
|
async def image_base64(request_data: Base64ImageRequest):
|
||||||
|
logger.info(f"Received image: {request_data.filename}")
|
||||||
|
logger.info(f"内容: {request_data.content}")
|
||||||
|
logger.info(f"pdf文件名: {request_data.original_filename}")
|
||||||
|
filename = safe_original_filename(request_data.filename)
|
||||||
|
logger.info(f"filename: {filename}")
|
||||||
|
suffix = PathLib(filename).suffix.lower()
|
||||||
|
if suffix not in {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".gif", ".tif", ".tiff"}:
|
||||||
|
raise HTTPException(status_code=400, detail="Unsupported image type")
|
||||||
|
|
||||||
|
image_bytes, processed_suffix = prepare_split_image(request_data.image_base64, suffix)
|
||||||
|
if not image_bytes:
|
||||||
|
raise HTTPException(status_code=400, detail="decoded image is empty")
|
||||||
|
|
||||||
|
temp_path = None
|
||||||
|
image_caption = []
|
||||||
|
vlm_text = ""
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=processed_suffix) as tmp:
|
||||||
|
tmp.write(image_bytes)
|
||||||
|
temp_path = PathLib(tmp.name)
|
||||||
|
|
||||||
|
ocr_data = await extract_image_text(temp_path)
|
||||||
|
try:
|
||||||
|
temp_path.unlink()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to delete temp image {temp_path}: {e}")
|
||||||
|
finally:
|
||||||
|
temp_path = None
|
||||||
|
if request_data.original_filename:
|
||||||
|
image_caption = await extract_image_caption(
|
||||||
|
SEARCH_DIR,
|
||||||
|
request_data.original_filename,
|
||||||
|
request_data.filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
caption_text = "\n".join(image_caption).strip()
|
||||||
|
|
||||||
|
# image_caption 不为空时使用 caption,
|
||||||
|
# 否则使用原始图片文件名
|
||||||
|
if caption_text:
|
||||||
|
caption_text =caption_text+".jpg"
|
||||||
|
output_filename = caption_text or filename
|
||||||
|
logger.info(f"image_caption: {caption_text}")
|
||||||
|
content = (request_data.content or "").strip()
|
||||||
|
ocr_full_text = ((ocr_data or {}).get("full_text") or "").strip()
|
||||||
|
|
||||||
|
# content 和 OCR 都为空时调用 VLM
|
||||||
|
if not content and not ocr_full_text:
|
||||||
|
vlm_text = await analyze_image_with_vlm(
|
||||||
|
image_bytes,
|
||||||
|
processed_suffix,
|
||||||
|
)
|
||||||
|
|
||||||
|
image_related_content = ocr_full_text+ "\n" +content+ "\n"+vlm_text
|
||||||
|
|
||||||
|
# image_content = (
|
||||||
|
# f"{output_filename}\n"
|
||||||
|
# f"{image_related_content}"
|
||||||
|
# )
|
||||||
|
logger.info(f"output_filename: {output_filename}")
|
||||||
|
logger.info(f"{image_related_content}")
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "ok",
|
||||||
|
"data": {
|
||||||
|
"image_content": image_related_content,
|
||||||
|
"filename": filename,
|
||||||
|
"jpg_filename": output_filename
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if temp_path and temp_path.exists():
|
||||||
|
try:
|
||||||
|
temp_path.unlink()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to delete temp image {temp_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/split_content_list")
|
@app.post("/split_content_list")
|
||||||
@ -2133,7 +2232,7 @@ async def split_result(
|
|||||||
slices = []
|
slices = []
|
||||||
for ins in contents:
|
for ins in contents:
|
||||||
slices.append({"content": ins, "positions": []})
|
slices.append({"content": ins, "positions": []})
|
||||||
result_data = {"slices": slices, "images": {}}
|
result_data = {"slices": slices, "images": {},"summary":''}
|
||||||
elif suffix in [".xlsx"]:
|
elif suffix in [".xlsx"]:
|
||||||
# converter.convert(str(temp_input_path), output_dir=str(DATA_DIR))
|
# converter.convert(str(temp_input_path), output_dir=str(DATA_DIR))
|
||||||
# temp_base_name = temp_input_path.stem # 如 tmp2bb0l4cn
|
# temp_base_name = temp_input_path.stem # 如 tmp2bb0l4cn
|
||||||
@ -2221,96 +2320,6 @@ class GraphSearchRequest(BaseModel):
|
|||||||
entry_nodes: Optional[Dict[str, Any]] = None
|
entry_nodes: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/search_graph——1")
|
|
||||||
async def search_graph(request: GraphSearchRequest, background_tasks: BackgroundTasks):
|
|
||||||
"""
|
|
||||||
========== ========== ========== ========== ========== ==========
|
|
||||||
纯图谱检索
|
|
||||||
|
|
||||||
请求参数:
|
|
||||||
- query: 用户查询文本(必填)
|
|
||||||
- top_k: 返回用于展示的路径数量(默认10)
|
|
||||||
- graph_timeout: 图谱检索超时时间,秒(默认2.0,暂未使用)
|
|
||||||
- entry_nodes: 入口节点信息(可选,不提供则从查询中提取)
|
|
||||||
|
|
||||||
========== ========== ========== ========== ========== ==========
|
|
||||||
"""
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
query = request.query
|
|
||||||
|
|
||||||
# ========== 参数验证 ==========
|
|
||||||
if not query or not query.strip():
|
|
||||||
raise HTTPException(status_code=400, detail="查询文本不能为空")
|
|
||||||
|
|
||||||
query = query.strip()
|
|
||||||
|
|
||||||
logger.info(f"[search_graph] 图谱检索请求: query='{query}', top_k={request.top_k}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# ========== 检查依赖是否可用 ==========
|
|
||||||
if not GRAPH_AVAILABLE:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=503,
|
|
||||||
detail="图谱检索功能依赖未安装,请安装: pip install neo4j langchain langchain-community neo4j-graphrag jieba",
|
|
||||||
)
|
|
||||||
|
|
||||||
# ========== 初始化图谱服务(复用app.py的neo4j driver) ==========
|
|
||||||
# 注意:传入app.py中创建的driver,实现连接复用
|
|
||||||
graph_service = GraphService(driver=driver)
|
|
||||||
|
|
||||||
# ========== 执行图谱检索(简单版本:只判断统计/非统计) ==========
|
|
||||||
# 说明:
|
|
||||||
# - 直接调用 GraphService.search,自动判断统计/非统计查询类型
|
|
||||||
# - query_type="auto" 会自动识别统计类查询(如"多少"、"比例"等)和明细类查询
|
|
||||||
result = await graph_service.search(
|
|
||||||
query=query,
|
|
||||||
entry_nodes=request.entry_nodes,
|
|
||||||
top_k=request.top_k,
|
|
||||||
max_attempts=3,
|
|
||||||
query_type="auto", # 自动判断统计/非统计
|
|
||||||
)
|
|
||||||
|
|
||||||
# ========== 确保返回格式一致 ==========
|
|
||||||
if not isinstance(result, dict):
|
|
||||||
result = {
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"data": result if isinstance(result, (list, dict)) else ([] if isinstance(result, list) else {}),
|
|
||||||
"meta": {"elapsed_ms": round((time.time() - start_time) * 1000, 2)},
|
|
||||||
}
|
|
||||||
|
|
||||||
# 确保data存在,可以是列表或字典(新格式:包含 nodes、links、results 的对象)
|
|
||||||
if "data" not in result:
|
|
||||||
result["data"] = {}
|
|
||||||
# 注意:不再强制转换为列表,支持新的对象格式
|
|
||||||
|
|
||||||
# 统计结果数量(兼容新旧格式)
|
|
||||||
if isinstance(result.get("data"), list):
|
|
||||||
result_count = len(result["data"])
|
|
||||||
elif isinstance(result.get("data"), dict):
|
|
||||||
# 新格式:统计 results 数组的长度
|
|
||||||
result_count = len(result["data"].get("results", []))
|
|
||||||
else:
|
|
||||||
result_count = 0
|
|
||||||
|
|
||||||
logger.info(f"[search_graph] 图谱检索完成: 返回 {result_count} 条结果")
|
|
||||||
return result
|
|
||||||
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
logger.error(f"[search_graph] 图谱检索失败 (耗时: {elapsed * 1000:.2f}ms): {e}", exc_info=True)
|
|
||||||
return {
|
|
||||||
"code": 500,
|
|
||||||
"message": "图谱检索失败",
|
|
||||||
"data": [],
|
|
||||||
"meta": {"elapsed_ms": round(elapsed * 1000, 2), "error": str(e), "query_type": "error"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ========== ========== ========== ========== ========== ==========
|
# ========== ========== ========== ========== ========== ==========
|
||||||
# Neo4j 索引创建接口
|
# Neo4j 索引创建接口
|
||||||
# ========== ========== ========== ========== ========== ==========
|
# ========== ========== ========== ========== ========== ==========
|
||||||
@ -2921,5 +2930,3 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=9085)
|
uvicorn.run(app, host="0.0.0.0", port=9085)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -85,16 +85,16 @@ def _build_ontology_config():
|
|||||||
ONTOLOGY, ONTOLOGY_DESC, NODE_TYPES = _build_ontology_config()
|
ONTOLOGY, ONTOLOGY_DESC, NODE_TYPES = _build_ontology_config()
|
||||||
|
|
||||||
# ── 配置(与 app.py 保持一致,按需修改) ────────────────────────────────────
|
# ── 配置(与 app.py 保持一致,按需修改) ────────────────────────────────────
|
||||||
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
|
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://192.168.2.12:57687")
|
||||||
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
|
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
|
||||||
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "zdht123@")
|
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "zdht123@")
|
||||||
MINERU_INSTANCES = [
|
MINERU_INSTANCES = [
|
||||||
# ("http://192.168.0.64:18000/analyze-pdf"),
|
# ("http://192.168.0.64:18000/analyze-pdf"),
|
||||||
#("http://192.168.0.111:9977/analyze-pdf/"),
|
#("http://192.168.0.111:9977/analyze-pdf/"),
|
||||||
# ("http://192.168.0.111:9979/analyze-pdf"),
|
# ("http://192.168.0.111:9979/analyze-pdf"),
|
||||||
("http://192.168.1.64:18000/analyze-pdf/"),
|
("http://192.168.2.12:59988/analyze-pdf/"),
|
||||||
]
|
]
|
||||||
PREFIX_URL = os.getenv("PREFIX_URL", "http://192.168.1.64:9085")
|
PREFIX_URL = os.getenv("PREFIX_URL", "http://192.168.2.12:59085")
|
||||||
|
|
||||||
SUPPORTED_EXT = {".pdf", ".md", ".docx", ".xlsx"}
|
SUPPORTED_EXT = {".pdf", ".md", ".docx", ".xlsx"}
|
||||||
|
|
||||||
|
|||||||
928
chunk_text.py
928
chunk_text.py
File diff suppressed because it is too large
Load Diff
250
chunkincreas.py
Normal file
250
chunkincreas.py
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
from filename_proceess_and_kgquery import get_entity
|
||||||
|
from config import SHIP_MODEL_NAME
|
||||||
|
|
||||||
|
# --- 配置区 ---
|
||||||
|
BASE_URL = "https://htknow.zkzdht.com/api/v1/knowledge"
|
||||||
|
ERROR_LOG_FILE = "error_log.txt"
|
||||||
|
|
||||||
|
HEADERS = {
|
||||||
|
"accept": "application/json",
|
||||||
|
"x-role": "admin",
|
||||||
|
"x-user-id": "1",
|
||||||
|
"x-user-name": "testuser"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 全局 Semaphore,用于限制并发连接数,防止对服务器造成过大压力
|
||||||
|
# 根据实际情况调整数值
|
||||||
|
CONCURRENT_LIMIT = 10
|
||||||
|
semaphore = asyncio.Semaphore(CONCURRENT_LIMIT)
|
||||||
|
|
||||||
|
async def log_error(kb_id, file_id, error_msg):
|
||||||
|
"""
|
||||||
|
将错误信息追加写入日志文件
|
||||||
|
注意:文件 I/O 在 asyncio 中是阻塞的,如果日志量极大,可以考虑使用线程池执行器
|
||||||
|
"""
|
||||||
|
# 对于少量的日志写入,直接使用 await asyncio.to_thread 或直接同步写入通常足够
|
||||||
|
# 这里为了简化,直接使用同步写入(在日志量不大时影响较小)
|
||||||
|
# 如果需要完全异步,可以使用 aiofiles 库
|
||||||
|
try:
|
||||||
|
with open(ERROR_LOG_FILE, "a", encoding="utf-8") as f:
|
||||||
|
f.write(f"[ERROR] KnowledgeBaseID: {kb_id}, FileID: {file_id}, Message: {error_msg}\n")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"写入错误日志时发生异常: {e}")
|
||||||
|
|
||||||
|
async def find_ship_info_by_hull(json_file_path, data):
|
||||||
|
"""从NER结果中提取舷号,再从JSON文件中查找对应的舰船信息"""
|
||||||
|
try:
|
||||||
|
# 同步文件读取,如果文件很大或数量很多,可以使用 aiofiles
|
||||||
|
with open(json_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
ship_data = json.load(f)
|
||||||
|
|
||||||
|
hulls = [
|
||||||
|
entity['properties']['舷号']
|
||||||
|
for entity in data.get('entities', [])
|
||||||
|
if 'properties' in entity and '舷号' in entity['properties']
|
||||||
|
]
|
||||||
|
if not hulls:
|
||||||
|
return None
|
||||||
|
|
||||||
|
target_hull = hulls[0]
|
||||||
|
|
||||||
|
for item in ship_data:
|
||||||
|
if str(item.get('hull_number')) == str(target_hull):
|
||||||
|
return {
|
||||||
|
"model_name": item.get('model_name'),
|
||||||
|
"ship_name": item.get('ship_name')
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"查找舰船信息时发生错误: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def format_entity_text(data):
|
||||||
|
"""格式化实体文本 (纯计算逻辑,保持同步)"""
|
||||||
|
type_mapping = {
|
||||||
|
'舰艇': [('舰艇为', '名称'), (',舷号', '舷号')],
|
||||||
|
'系统': [(',系统:', '名称')],
|
||||||
|
'子系统': [(',子系统:', '名称')],
|
||||||
|
'设备': [(',设备:', '名称')]
|
||||||
|
}
|
||||||
|
|
||||||
|
text_parts = []
|
||||||
|
for entity in data.get('entities', []):
|
||||||
|
entity_type = entity.get('type')
|
||||||
|
properties = entity.get('properties', {})
|
||||||
|
|
||||||
|
if entity_type in type_mapping:
|
||||||
|
entity_str = ""
|
||||||
|
for prefix, attr_key in type_mapping[entity_type]:
|
||||||
|
value = str(properties.get(attr_key, ''))
|
||||||
|
entity_str += f"{prefix}{value}"
|
||||||
|
text_parts.append(entity_str)
|
||||||
|
|
||||||
|
return ''.join(text_parts)
|
||||||
|
|
||||||
|
async def get_knowledge_base_files(session, kb_id):
|
||||||
|
"""
|
||||||
|
获取指定知识库的文件列表
|
||||||
|
"""
|
||||||
|
url = f"{BASE_URL}/knowledge_base/{kb_id}"
|
||||||
|
try:
|
||||||
|
async with session.get(url, headers=HEADERS, timeout=120) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
if 'files' in data and isinstance(data['files'], list):
|
||||||
|
return data['files']
|
||||||
|
else:
|
||||||
|
print(f"警告: 知识库 {kb_id} 中未找到 'files' 字段。")
|
||||||
|
return []
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"获取知识库详情失败: {str(e)} | Traceback: {traceback.format_exc()}"
|
||||||
|
print(error_msg)
|
||||||
|
await log_error(kb_id, "N/A", error_msg)
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def get_file_slices(session, file_id):
|
||||||
|
"""获取文件所有切片"""
|
||||||
|
url = f"{BASE_URL}/files/{file_id}/slices"
|
||||||
|
try:
|
||||||
|
async with session.get(url, headers=HEADERS, timeout=120) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
return await response.json()
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"获取文件切片失败: {str(e)}"
|
||||||
|
print(error_msg)
|
||||||
|
# 错误会在调用处记录
|
||||||
|
raise Exception(error_msg)
|
||||||
|
|
||||||
|
async def update_slices(session, slices_data, file_id):
|
||||||
|
"""更新切片"""
|
||||||
|
url = f"{BASE_URL}/files/{file_id}/slices"
|
||||||
|
headers = {
|
||||||
|
**HEADERS,
|
||||||
|
"content-type": "application/json"
|
||||||
|
}
|
||||||
|
payload = {"slices": slices_data}
|
||||||
|
try:
|
||||||
|
async with session.put(url, headers=headers, json=payload, timeout=120) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
return await response.json()
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"更新切片失败: {str(e)}"
|
||||||
|
print(error_msg)
|
||||||
|
raise Exception(error_msg)
|
||||||
|
|
||||||
|
async def process_single_file(session, kb_id, file):
|
||||||
|
"""
|
||||||
|
处理单个文件的逻辑,用于并发执行
|
||||||
|
"""
|
||||||
|
file_id = file.get('id')
|
||||||
|
filename = file.get('filename', 'Unknown')
|
||||||
|
|
||||||
|
print(f" 处理文件: {filename} (ID: {file_id})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- 步骤 A: 提取实体信息 ---
|
||||||
|
final_result = get_entity(filename)
|
||||||
|
|
||||||
|
# 处理舰船型号
|
||||||
|
xinghao = await find_ship_info_by_hull(SHIP_MODEL_NAME, final_result)
|
||||||
|
model_name = xinghao.get('model_name', '') if xinghao else ''
|
||||||
|
|
||||||
|
# 格式化其他实体文本
|
||||||
|
other_data = format_entity_text(final_result)
|
||||||
|
|
||||||
|
# --- 步骤 B: 获取切片 ---
|
||||||
|
slices = await get_file_slices(session, file_id)
|
||||||
|
|
||||||
|
# --- 步骤 C: 处理切片内容 ---
|
||||||
|
slices_v2 = []
|
||||||
|
info_suffix = f"({other_data},型号为{model_name})" if model_name and other_data else ""
|
||||||
|
for ins in slices:
|
||||||
|
ins_id = ins.get('id')
|
||||||
|
content = ins.get('content', '')
|
||||||
|
|
||||||
|
# 查找第一行的结尾(换行符位置)
|
||||||
|
newline_idx = content.find('\n')
|
||||||
|
|
||||||
|
# 拼接新内容
|
||||||
|
if newline_idx == -1:
|
||||||
|
# 内容中没有换行符,直接在末尾添加
|
||||||
|
new_content = content + info_suffix
|
||||||
|
else:
|
||||||
|
# 在第一行末尾插入
|
||||||
|
new_content = content[:newline_idx] + info_suffix + content[newline_idx:]
|
||||||
|
|
||||||
|
slices_v2.append({"id": ins_id, "content": new_content})
|
||||||
|
|
||||||
|
# --- 步骤 D: 更新切片 ---
|
||||||
|
await update_slices(session, slices_v2, file_id)
|
||||||
|
print(f" 成功更新文件: {filename}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 捕获当前文件处理过程中的所有异常
|
||||||
|
error_msg = f"处理文件失败: {str(e)}"
|
||||||
|
print(error_msg)
|
||||||
|
# 记录错误日志
|
||||||
|
await log_error(kb_id, file_id, error_msg)
|
||||||
|
# 跳过当前文件
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def process_files_in_knowledge_base(session, kb_id):
|
||||||
|
"""
|
||||||
|
处理单个知识库中的所有文件
|
||||||
|
"""
|
||||||
|
print(f"\n=== 开始处理知识库 ID: {kb_id} ===")
|
||||||
|
|
||||||
|
# 1. 获取文件列表
|
||||||
|
files = await get_knowledge_base_files(session, kb_id)
|
||||||
|
if not files:
|
||||||
|
print(f"知识库 {kb_id} 中没有文件或获取文件列表失败,跳过。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. 并发处理每个文件
|
||||||
|
# 使用 asyncio.gather 来并发处理所有文件
|
||||||
|
# 如果文件数量巨大,可以分批处理以避免内存溢出
|
||||||
|
file_tasks = [
|
||||||
|
process_single_file(session, kb_id, file)
|
||||||
|
for file in files
|
||||||
|
]
|
||||||
|
|
||||||
|
# 并发执行所有文件任务
|
||||||
|
# return_exceptions=True 会捕获任务内部的异常并作为结果返回,而不是中断整个 gather
|
||||||
|
results = await asyncio.gather(*file_tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
# 统计结果
|
||||||
|
success_count = sum(1 for r in results if r is True)
|
||||||
|
failed_count = len(results) - success_count
|
||||||
|
print(f"知识库 {kb_id} 处理完成。成功: {success_count}, 失败: {failed_count}")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
KNOWLEDGE_BASE_IDS = ['8']
|
||||||
|
print(f"开始批量处理 {len(KNOWLEDGE_BASE_IDS)} 个知识库...")
|
||||||
|
|
||||||
|
# 创建一个全局的 TCP 连接器,复用连接
|
||||||
|
connector = aiohttp.TCPConnector(limit=CONCURRENT_LIMIT, limit_per_host=5)
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(connector=connector) as session:
|
||||||
|
# 遍历所有知识库ID,并发处理
|
||||||
|
# 如果知识库数量很多,也可以在这里使用 asyncio.gather
|
||||||
|
for kb_id in KNOWLEDGE_BASE_IDS:
|
||||||
|
try:
|
||||||
|
await process_files_in_knowledge_base(session, kb_id)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"处理知识库顶层异常: {str(e)}"
|
||||||
|
print(error_msg)
|
||||||
|
await log_error(kb_id, "N/A", error_msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("所有知识库处理完成。")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 运行异步主函数
|
||||||
|
asyncio.run(main())
|
||||||
117
config.py
117
config.py
@ -4,27 +4,26 @@ import os
|
|||||||
|
|
||||||
# ==================== 大模型配置 ====================
|
# ==================== 大模型配置 ====================
|
||||||
LLM_CONFIG = {
|
LLM_CONFIG = {
|
||||||
"model": "46-qwen3.5-35B",
|
"model": "46-qwen3.6-35B",
|
||||||
"base_url": "http://192.168.0.46:59800/v1",
|
"base_url": "http://192.168.2.12:59800/v1",
|
||||||
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
|
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
|
||||||
"temperature": 0.1,
|
"temperature": 0.1,
|
||||||
"max_tokens": 55096,
|
"max_tokens": 25096,
|
||||||
"timeout": 30
|
"timeout": 30
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==================== 嵌入模型配置 ====================
|
# ==================== 嵌入模型配置 ====================
|
||||||
EMBEDDING_CONFIG = {
|
EMBEDDING_CONFIG = {
|
||||||
"model": "bge-m3",
|
"model": "bge-m3",
|
||||||
"base_url": "http://192.168.0.46:59700/v1/embeddings",
|
"base_url": "http://192.168.2.12:59700/v1/embeddings",
|
||||||
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
|
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
|
||||||
"base_url_v2" : "http://192.168.0.46:59700/v1",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==================== Neo4j 图数据库配置 ====================
|
# ==================== Neo4j 图数据库配置 ====================
|
||||||
NEO4J_CONFIG = {
|
NEO4J_CONFIG = {
|
||||||
# 优先读取环境变量,如果没有则使用默认值
|
# 优先读取环境变量,如果没有则使用默认值
|
||||||
# 注意:本地开发通常用 localhost,部署时用服务名 neo4j
|
# 注意:本地开发通常用 localhost,部署时用服务名 neo4j
|
||||||
"uri": "neo4j://192.168.0.46:57687",
|
"uri": "neo4j://192.168.2.12:57687",
|
||||||
"username": "neo4j",
|
"username": "neo4j",
|
||||||
"password": "zdht123@",
|
"password": "zdht123@",
|
||||||
"database": "neo4j", # 新增 database 配置
|
"database": "neo4j", # 新增 database 配置
|
||||||
@ -51,15 +50,15 @@ SEARCH_CONFIG = {
|
|||||||
"node_fetch_page" : 10000,
|
"node_fetch_page" : 10000,
|
||||||
}
|
}
|
||||||
|
|
||||||
INDEXING_ULR = "http://192.168.0.46:59085/neo4j_indexing"
|
INDEXING_ULR = "http://192.168.2.12:59085/neo4j_indexing"
|
||||||
|
|
||||||
URL = {
|
URL = {
|
||||||
"indexing_url" : "http://192.168.0.46:59085/neo4j_indexing",
|
"indexing_url" : "http://192.168.2.12:59085/neo4j_indexing",
|
||||||
"prefix_url" : "http://192.168.0.46:59085",
|
"prefix_url" : "http://192.168.2.12:59085",
|
||||||
}
|
}
|
||||||
|
|
||||||
API_URLS = [
|
API_URLS = [
|
||||||
"http://192.168.0.64:59988/analyze-pdf",
|
"http://192.168.2.12:59988/analyze-pdf",
|
||||||
# "http://192.168.0.111:9977/analyze-pdf",
|
# "http://192.168.0.111:9977/analyze-pdf",
|
||||||
# "http://192.168.0.111:9978/analyze-pdf",
|
# "http://192.168.0.111:9978/analyze-pdf",
|
||||||
#"http://192.168.0.111:9979/analyze-pdf",
|
#"http://192.168.0.111:9979/analyze-pdf",
|
||||||
@ -67,7 +66,7 @@ API_URLS = [
|
|||||||
# "http://192.168.0.111:9975/analyze-pdf",
|
# "http://192.168.0.111:9975/analyze-pdf",
|
||||||
]
|
]
|
||||||
API_OTHER_URLS = [
|
API_OTHER_URLS = [
|
||||||
"http://192.168.0.46:59988/analyze-otherfile",
|
"http://192.168.2.12:59988/analyze-otherfile",
|
||||||
# "http://192.168.0.111:9977/analyze-otherfile",
|
# "http://192.168.0.111:9977/analyze-otherfile",
|
||||||
# "http://192.168.0.111:9978/analyze-otherfile",
|
# "http://192.168.0.111:9978/analyze-otherfile",
|
||||||
#"http://192.168.0.111:9979/analyze-otherfile",
|
#"http://192.168.0.111:9979/analyze-otherfile",
|
||||||
@ -75,88 +74,18 @@ API_OTHER_URLS = [
|
|||||||
# "http://192.168.0.111:9975/analyze-otherfile",
|
# "http://192.168.0.111:9975/analyze-otherfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
SHIP_MODEL_NAME = "E:\ZKYNLP\Hjunproject\project0506\kgrag\ship_xinghao.json"
|
# ==================== 切片图片处理配置 ====================
|
||||||
TREE_JSON_PATH = r"E:\ZKYNLP\Hjunproject\project0506\kgrag\tree_data.json"
|
MAX_SPLIT_IMAGE_INPUT_BYTES = 50 * 1024 * 1024
|
||||||
root@70964b104c5b:/app# pwd
|
MAX_SPLIT_IMAGE_BYTES = 10 * 1024 * 1024
|
||||||
/app
|
MAX_SPLIT_IMAGE_BASE64_CHARS = (MAX_SPLIT_IMAGE_INPUT_BYTES * 4 // 3) + 4096
|
||||||
root@70964b104c5b:/app# vim config.py
|
SPLIT_IMAGE_COMPRESS_THRESHOLD_BYTES = MAX_SPLIT_IMAGE_BYTES
|
||||||
root@70964b104c5b:/app# cat config.py
|
SPLIT_IMAGE_MAX_DIMENSION = 4096
|
||||||
# app/model_config.py
|
SPLIT_IMAGE_JPEG_QUALITY = 88
|
||||||
|
OCR_CONCURRENCY = 1
|
||||||
import os
|
VLM_CONCURRENCY = 2
|
||||||
|
|
||||||
# ==================== 大模型配置 ====================
|
|
||||||
LLM_CONFIG = {
|
|
||||||
"model": "46-qwen3.5-35B",
|
|
||||||
"base_url": "http://192.168.0.46:59800/v1",
|
|
||||||
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
|
|
||||||
"temperature": 0.1,
|
|
||||||
"max_tokens": 55096,
|
|
||||||
"timeout": 30
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================== 嵌入模型配置 ====================
|
|
||||||
EMBEDDING_CONFIG = {
|
|
||||||
"model": "bge-m3",
|
|
||||||
"base_url": "http://192.168.0.46:59700/v1/embeddings",
|
|
||||||
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
|
|
||||||
"base_url_v2" : "http://192.168.0.46:59700/v1",
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================== Neo4j 图数据库配置 ====================
|
|
||||||
NEO4J_CONFIG = {
|
|
||||||
# 优先读取环境变量,如果没有则使用默认值
|
|
||||||
# 注意:本地开发通常用 localhost,部署时用服务名 neo4j
|
|
||||||
"uri": "neo4j://192.168.0.46:57687",
|
|
||||||
"username": "neo4j",
|
|
||||||
"password": "zdht123@",
|
|
||||||
"database": "neo4j", # 新增 database 配置
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================== 检索与索引配置 ====================
|
|
||||||
SEARCH_CONFIG = {
|
|
||||||
"name_property": "名称",
|
|
||||||
"fulltext_property": "fulltext",
|
|
||||||
"embedding_property": "embedding",
|
|
||||||
"search_label": "Searchable",
|
|
||||||
"vector_index_name": "global_searchable_embedding",
|
|
||||||
"fulltext_index_name": "global_searchable_content_search",
|
|
||||||
"fulltext_field_index_name": "global_searchable_fulltext_search",
|
|
||||||
"global_entity_embedding" : "global_entity_embedding",
|
|
||||||
"global_entity_content_search": "global_entity_content_search",
|
|
||||||
"excluded_business_labels": [
|
|
||||||
"Entity", "Chunk", "Document", "_Bloom_Perspective_", "Searchable"
|
|
||||||
],
|
|
||||||
"vector_dimension": 1024,
|
|
||||||
"jieba_pos_whitelist": ["n", "nr", "ns", "nt", "nz", "vn", "eng"],
|
|
||||||
"embed_batch_size" : 64,
|
|
||||||
"embed_thread_num" : 4,
|
|
||||||
"node_fetch_page" : 10000,
|
|
||||||
}
|
|
||||||
|
|
||||||
INDEXING_ULR = "http://192.168.0.46:59085/neo4j_indexing"
|
|
||||||
|
|
||||||
URL = {
|
|
||||||
"indexing_url" : "http://192.168.0.46:59085/neo4j_indexing",
|
|
||||||
"prefix_url" : "http://192.168.0.46:59085",
|
|
||||||
}
|
|
||||||
|
|
||||||
API_URLS = [
|
|
||||||
"http://192.168.0.64:59988/analyze-pdf",
|
|
||||||
# "http://192.168.0.111:9977/analyze-pdf",
|
|
||||||
# "http://192.168.0.111:9978/analyze-pdf",
|
|
||||||
#"http://192.168.0.111:9979/analyze-pdf",
|
|
||||||
#"http://192.168.0.111:9980/analyze-pdf",
|
|
||||||
# "http://192.168.0.111:9975/analyze-pdf",
|
|
||||||
]
|
|
||||||
API_OTHER_URLS = [
|
|
||||||
"http://192.168.0.46:59988/analyze-otherfile",
|
|
||||||
# "http://192.168.0.111:9977/analyze-otherfile",
|
|
||||||
# "http://192.168.0.111:9978/analyze-otherfile",
|
|
||||||
#"http://192.168.0.111:9979/analyze-otherfile",
|
|
||||||
#"http://192.168.0.111:9980/analyze-otherfile",
|
|
||||||
# "http://192.168.0.111:9975/analyze-otherfile",
|
|
||||||
]
|
|
||||||
|
|
||||||
SHIP_MODEL_NAME = "/app/ship_xinghao.json"
|
SHIP_MODEL_NAME = "/app/ship_xinghao.json"
|
||||||
TREE_JSON_PATH = "/app/tree_data.json"
|
TREE_JSON_PATH = "/app/tree_data.json"
|
||||||
|
# ==================== mineru和kgrag目录映射地址 ====================
|
||||||
|
SEARCH_DIR ="/app/mineru_output"
|
||||||
|
IMAGE_DIR = "/app/mineru_output/images"
|
||||||
|
|||||||
46
databaseconnect.py
Normal file
46
databaseconnect.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
def find_ship_info(json_file_path, target_hull):
|
||||||
|
"""
|
||||||
|
从JSON文件中根据舷号查找舰船信息
|
||||||
|
:param json_file_path: JSON文件路径
|
||||||
|
:param target_hull: 目标舷号 (字符串或数字)
|
||||||
|
:return: 包含 model_name 和 ship_name 的字典,未找到返回 None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1. 打开并读取JSON文件
|
||||||
|
with open(json_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# 2. 遍历列表查找匹配项
|
||||||
|
# 注意:这里使用 str() 转换是为了兼容 JSON 中 hull_number 可能是数字(163)或字符串("163")的情况
|
||||||
|
for item in data:
|
||||||
|
if str(item.get('hull_number')) == str(target_hull):
|
||||||
|
return {
|
||||||
|
"model_name": item.get('model_name'),
|
||||||
|
"ship_name": item.get('ship_name')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 如果遍历结束仍未找到
|
||||||
|
return None
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"错误:找不到文件 {json_file_path}")
|
||||||
|
return None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("错误:JSON 文件格式不正确")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# --- 使用示例 ---
|
||||||
|
if __name__ == "__main__":
|
||||||
|
file_path = "E:\ZKYNLP\Hjunproject\project0506\kgrag\ship_xinghao.json" # 替换为你的实际文件名
|
||||||
|
search_hull = "163" # 你要查询的舷号
|
||||||
|
|
||||||
|
result = find_ship_info(file_path, search_hull)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
print(f"查询成功!")
|
||||||
|
print(f"型号: {result['model_name']}")
|
||||||
|
print(f"舰名: {result['ship_name']}")
|
||||||
|
else:
|
||||||
|
print(f"未找到舷号为 {search_hull} 的记录")
|
||||||
41
error_log.txt
Normal file
41
error_log.txt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
[ERROR] KnowledgeBaseID: 1789, FileID: 3974, Message: 处理文件失败: 更新切片失败: HTTPSConnectionPool(host='htknow.zkzdht.com', port=443): Read timed out. (read timeout=10)
|
||||||
|
[ERROR] KnowledgeBaseID: 1789, FileID: 3962, Message: 处理文件 163-06A0014-B01001_发动机-维修手册.pdf 的切片批次 3 时出错: 更新切片批次失败:
|
||||||
|
[ERROR] KnowledgeBaseID: 1789, FileID: 3974, Message: 处理文件 163-06A0016-B01003_雷达-维修手册.pdf 的切片批次 3 时出错: 更新切片批次失败:
|
||||||
|
[ERROR] KnowledgeBaseID: 1789, FileID: 3961, Message: 处理文件 163-06A0016-B01003_雷达-操作使用手册(1).pdf 的切片批次 4 时出错: 更新切片批次失败:
|
||||||
|
[ERROR] KnowledgeBaseID: 1789, FileID: 3974, Message: 处理文件 163-06A0016-B01003_雷达-维修手册.pdf 的切片批次 4 时出错: 更新切片批次失败: 500, message='Internal Server Error', url='https://htknow.zkzdht.com/api/v1/knowledge/files/3974/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 299, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/299/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 296, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/296/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 319, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/319/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 315, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/315/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 311, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/311/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 307, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/307/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 291, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/291/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 283, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/283/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 293, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/293/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 295, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/295/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 286, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/286/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 290, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/290/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 287, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/287/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 278, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/278/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 284, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/284/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 282, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/282/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 40, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/40/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 275, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/275/slices'
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 299, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/299/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 319, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/319/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 315, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/315/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 311, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/311/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 307, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/307/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 296, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/296/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 283, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/283/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 291, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/291/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 293, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/293/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 295, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/295/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 286, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/286/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 287, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/287/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 290, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/290/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 278, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/278/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 284, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/284/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 40, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/40/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 282, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/282/slices
|
||||||
|
[ERROR] KnowledgeBaseID: 8, FileID: 275, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/275/slices
|
||||||
@ -339,7 +339,7 @@ def process_file_content(
|
|||||||
return await process_document(file_name)
|
return await process_document(file_name)
|
||||||
doc_result = asyncio.run(_run_process_doc())
|
doc_result = asyncio.run(_run_process_doc())
|
||||||
if doc_result is not None:
|
if doc_result is not None:
|
||||||
content_list, _ = doc_result
|
content_list, images_dict, md_content= doc_result
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import json
|
|||||||
from modelsAPI.model_api import OpenaiAPI
|
from modelsAPI.model_api import OpenaiAPI
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from typing import Dict, List, Any
|
from typing import Dict, List, Any
|
||||||
from config import TREE_JSON_PATH
|
|
||||||
|
|
||||||
app = FastAPI(title="PDF Upload Service")
|
app = FastAPI(title="PDF Upload Service")
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
|
||||||
@ -89,7 +88,6 @@ def build_tree(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
|
|||||||
不再依赖 links 的连线,直接根据 labels 归类,彻底避免环状数据导致的死循环。
|
不再依赖 links 的连线,直接根据 labels 归类,彻底避免环状数据导致的死循环。
|
||||||
默认只展开第一艘舰艇的系统/子系统,其它舰艇 children 置空(前端可点击按需加载)。
|
默认只展开第一艘舰艇的系统/子系统,其它舰艇 children 置空(前端可点击按需加载)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 1. 辅助函数:判断标签是否包含关键字
|
# 1. 辅助函数:判断标签是否包含关键字
|
||||||
def has_label(node, keyword):
|
def has_label(node, keyword):
|
||||||
labels = node.get("labels", [])
|
labels = node.get("labels", [])
|
||||||
@ -541,8 +539,8 @@ def fetch_graph_sample1(driver):
|
|||||||
# 文件名处理方法
|
# 文件名处理方法
|
||||||
# =============================
|
# =============================
|
||||||
|
|
||||||
# BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
# TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
|
TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
|
||||||
|
|
||||||
|
|
||||||
def _load_tree_data() -> Dict:
|
def _load_tree_data() -> Dict:
|
||||||
@ -834,7 +832,7 @@ def get_entity(filename):
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# 阀控蓄电池脉冲式快速充电装置
|
# 阀控蓄电池脉冲式快速充电装置
|
||||||
test_list = [
|
test_list = [
|
||||||
"163-06A0015-B01001_发动机-维修手册.pdf",
|
"777-11A1014-B01001_发动机-维修手册.pdf",
|
||||||
|
|
||||||
]
|
]
|
||||||
for i in test_list:
|
for i in test_list:
|
||||||
|
|||||||
@ -1,821 +0,0 @@
|
|||||||
"""
|
|
||||||
PDF 用户上传接口(FastAPI)
|
|
||||||
- 处理文件名和其对应关系
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Dict, Tuple
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from typing import Dict
|
|
||||||
from fastapi import FastAPI, HTTPException
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from modelsAPI.model_api import OpenaiAPI
|
|
||||||
from openai import OpenAI
|
|
||||||
from typing import Dict, List, Any
|
|
||||||
|
|
||||||
app = FastAPI(title="PDF Upload Service")
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
|
|
||||||
logger = logging.getLogger("filename_neo4j_process")
|
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# 图方法
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
|
|
||||||
def node_to_json(node):
|
|
||||||
props = dict(node)
|
|
||||||
# ks_raw = props.get("knowledge_source")
|
|
||||||
# knowledge_source = json.loads(ks_raw) if ks_raw else []
|
|
||||||
|
|
||||||
ks_raw = props.get("knowledge_source")
|
|
||||||
knowledge_source = []
|
|
||||||
|
|
||||||
if ks_raw and isinstance(ks_raw, str):
|
|
||||||
try:
|
|
||||||
ks_list = json.loads(ks_raw)
|
|
||||||
if isinstance(ks_list, list):
|
|
||||||
for ks in ks_list:
|
|
||||||
if isinstance(ks, dict):
|
|
||||||
ks.pop("info", None)
|
|
||||||
knowledge_source.append(ks)
|
|
||||||
except (json.JSONDecodeError, TypeError):
|
|
||||||
# 解析失败或类型错误时,保持 knowledge_source 为空列表
|
|
||||||
pass
|
|
||||||
|
|
||||||
name = props.get("名称")
|
|
||||||
props.pop("embedding", None)
|
|
||||||
props.pop("knowledge_source", None)
|
|
||||||
props.pop("name", None) # 现在安全地移除
|
|
||||||
props.pop("fulltext", None)
|
|
||||||
if "last_updated" in props:
|
|
||||||
props["最后更新时间"] = props.pop("last_updated")
|
|
||||||
|
|
||||||
if "created_at" in props:
|
|
||||||
props["创建时间"] = props.pop("created_at")
|
|
||||||
return {
|
|
||||||
"id": node.element_id,
|
|
||||||
"name": name, # 使用提前保存的值
|
|
||||||
"labels": list(node.labels),
|
|
||||||
"knowledge_source": knowledge_source,
|
|
||||||
"properties": props,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def rel_to_json(rel):
|
|
||||||
props = dict(rel)
|
|
||||||
props.pop("embedding", None)
|
|
||||||
props.pop("fact_timeline", None)
|
|
||||||
return {"id": rel.element_id, "label": rel.type, "source": rel.start_node.element_id, "target": rel.end_node.element_id, "properties": props}
|
|
||||||
|
|
||||||
|
|
||||||
def is_simple_path(path):
|
|
||||||
"""
|
|
||||||
判断是否为无环路径(simple path)
|
|
||||||
"""
|
|
||||||
seen = set()
|
|
||||||
for n in path.nodes:
|
|
||||||
nid = n.element_id
|
|
||||||
if nid in seen:
|
|
||||||
return False
|
|
||||||
seen.add(nid)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def build_tree(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
|
|
||||||
"""
|
|
||||||
按照固定层级构建树:舰艇 -> 系统 -> 子系统
|
|
||||||
不再依赖 links 的连线,直接根据 labels 归类,彻底避免环状数据导致的死循环。
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 1. 辅助函数:判断标签是否包含关键字
|
|
||||||
def has_label(node, keyword):
|
|
||||||
labels = node.get("labels", [])
|
|
||||||
if isinstance(labels, list):
|
|
||||||
return any(keyword in lbl for lbl in labels)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 2. 辅助函数:构建标准的树节点格式
|
|
||||||
def make_node(node_data):
|
|
||||||
return {
|
|
||||||
"id": node_data["id"],
|
|
||||||
"name": node_data.get("name", ""),
|
|
||||||
"labels": node_data.get("labels", []),
|
|
||||||
|
|
||||||
"properties": node_data.get("properties", {}),
|
|
||||||
"color": node_data.get("color"),
|
|
||||||
"size": node_data.get("size"),
|
|
||||||
"children": []
|
|
||||||
}
|
|
||||||
|
|
||||||
# 3. 将节点按层级分类存储到字典中,方便快速查找
|
|
||||||
# key: id, value: node_dict_with_children
|
|
||||||
ships = {} # 第一层:舰艇
|
|
||||||
systems = {} # 第二层:系统(不含子系统)
|
|
||||||
sub_systems = {} # 第三层:子系统
|
|
||||||
|
|
||||||
for node in nodes:
|
|
||||||
# 注意:判断逻辑要互斥,防止“子系统”被误判为“系统”
|
|
||||||
if has_label(node, "舰艇"):
|
|
||||||
ships[node["id"]] = make_node(node)
|
|
||||||
elif has_label(node, "子系统"):
|
|
||||||
sub_systems[node["id"]] = make_node(node)
|
|
||||||
elif has_label(node, "系统"):
|
|
||||||
systems[node["id"]] = make_node(node)
|
|
||||||
else:
|
|
||||||
# 如果有其他无关节点,可以在这里处理或忽略
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 4. 开始组装:利用 links 建立关系(此时 links 只充当“连接器”,不决定层级)
|
|
||||||
# 因为我们已经确定了层级,所以不会出现循环引用
|
|
||||||
|
|
||||||
for link in links:
|
|
||||||
if link.get("label") != "包含":
|
|
||||||
continue
|
|
||||||
|
|
||||||
src_id = link["source"]
|
|
||||||
tgt_id = link["target"]
|
|
||||||
|
|
||||||
# 逻辑 A: 舰艇 -> 系统
|
|
||||||
if src_id in ships and tgt_id in systems:
|
|
||||||
ships[src_id]["children"].append(systems[tgt_id])
|
|
||||||
|
|
||||||
# 逻辑 B: 系统 -> 子系统
|
|
||||||
elif src_id in systems and tgt_id in sub_systems:
|
|
||||||
systems[src_id]["children"].append(sub_systems[tgt_id])
|
|
||||||
|
|
||||||
# 逻辑 C: 舰艇 -> 子系统 (防止有些数据直接连了子系统,虽然不规范但也兼容一下)
|
|
||||||
elif src_id in ships and tgt_id in sub_systems:
|
|
||||||
ships[src_id]["children"].append(sub_systems[tgt_id])
|
|
||||||
|
|
||||||
# 5. 返回所有舰艇作为根节点列表
|
|
||||||
return list(ships.values())
|
|
||||||
def build_tree1(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
|
|
||||||
"""
|
|
||||||
从 nodes + links 构建树结构。
|
|
||||||
- 根节点:label 包含 '舰艇' 的节点
|
|
||||||
- 父子关系:links 中 label == '包含' 的边 (source -> target)
|
|
||||||
- 支持多棵树(多个舰艇根节点)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 1. 建立 id -> node 的映射
|
|
||||||
node_map: Dict[str, Dict] = {n["id"]: n for n in nodes}
|
|
||||||
|
|
||||||
# 2. 建立 父id -> [子id列表] 的邻接表(只处理"包含"关系)
|
|
||||||
children_map: Dict[str, List[str]] = {n["id"]: [] for n in nodes}
|
|
||||||
parent_map: Dict[str, str] = {} # 子id -> 父id,用于识别根节点
|
|
||||||
|
|
||||||
for link in links:
|
|
||||||
if link.get("label") == "包含":
|
|
||||||
src = link["source"]
|
|
||||||
tgt = link["target"]
|
|
||||||
if src in children_map:
|
|
||||||
children_map[src].append(tgt)
|
|
||||||
if tgt not in parent_map:
|
|
||||||
parent_map[tgt] = src
|
|
||||||
|
|
||||||
# 3. 递归构建子树
|
|
||||||
def build_subtree(node_id: str, ancestors: set = None) -> Dict[str, Any]:
|
|
||||||
if ancestors is None:
|
|
||||||
ancestors = set()
|
|
||||||
if node_id in ancestors:
|
|
||||||
return None
|
|
||||||
ancestors.add(node_id)
|
|
||||||
node = node_map[node_id]
|
|
||||||
subtree = {"id": node_id, "name": node.get("name", ""), "labels": node.get("labels", []), "properties": node.get("properties", {}), "color": node.get("color"), "size": node.get("size"), "children": []}
|
|
||||||
current_ancestors = ancestors | {node_id}
|
|
||||||
for child_id in children_map.get(node_id, []):
|
|
||||||
child = build_subtree(child_id, current_ancestors)
|
|
||||||
if child is not None:
|
|
||||||
subtree["children"].append(child)
|
|
||||||
return subtree
|
|
||||||
|
|
||||||
# def build_subtree(node_id: str) -> Dict[str, Any]:
|
|
||||||
# node = node_map[node_id]
|
|
||||||
# subtree = {
|
|
||||||
# "id": node_id,
|
|
||||||
# "name": node.get("name", ""),
|
|
||||||
# "labels": node.get("labels", []),
|
|
||||||
# "properties": node.get("properties", {}),
|
|
||||||
# "color": node.get("color"),
|
|
||||||
# "size": node.get("size"),
|
|
||||||
# "children": []
|
|
||||||
# }
|
|
||||||
# for child_id in children_map.get(node_id, []):
|
|
||||||
# subtree["children"].append(build_subtree(child_id))
|
|
||||||
# return subtree
|
|
||||||
|
|
||||||
# 4. 找出所有根节点:label 包含 '舰艇',且没有父节点
|
|
||||||
roots = [n["id"] for n in nodes if "舰艇" in n.get("labels", []) and n["id"] not in parent_map]
|
|
||||||
|
|
||||||
# 5. 为每棵树构建结构,并打上树编号
|
|
||||||
forest = []
|
|
||||||
for i, root_id in enumerate(roots):
|
|
||||||
tree = build_subtree(root_id)
|
|
||||||
tree["tree_index"] = i # 区分不同树
|
|
||||||
forest.append(tree)
|
|
||||||
|
|
||||||
return forest
|
|
||||||
|
|
||||||
|
|
||||||
def build_graph_from_paths(paths):
|
|
||||||
"""
|
|
||||||
核心公共方法
|
|
||||||
从 Neo4j 路径结果构建 graphData
|
|
||||||
"""
|
|
||||||
node_map: Dict[str, dict] = {}
|
|
||||||
link_map: Dict[str, dict] = {}
|
|
||||||
|
|
||||||
def add_edge(rel):
|
|
||||||
s = rel.start_node
|
|
||||||
t = rel.end_node
|
|
||||||
|
|
||||||
if s.element_id not in node_map:
|
|
||||||
node_map[s.element_id] = node_to_json(s)
|
|
||||||
|
|
||||||
if t.element_id not in node_map:
|
|
||||||
node_map[t.element_id] = node_to_json(t)
|
|
||||||
|
|
||||||
if rel.element_id not in link_map:
|
|
||||||
link_map[rel.element_id] = rel_to_json(rel)
|
|
||||||
|
|
||||||
for record in paths:
|
|
||||||
p = record["p"]
|
|
||||||
|
|
||||||
if not is_simple_path(p):
|
|
||||||
continue
|
|
||||||
|
|
||||||
for r in p.relationships:
|
|
||||||
add_edge(r)
|
|
||||||
|
|
||||||
return {"nodes": list(node_map.values()), "links": list(link_map.values())}
|
|
||||||
|
|
||||||
|
|
||||||
# def fetch_graph_sample(driver,limit: int = 200):
|
|
||||||
# """提取采样逻辑为可复用函数"""
|
|
||||||
# with driver.session() as session:
|
|
||||||
# result = session.run("""
|
|
||||||
# MATCH (n)
|
|
||||||
# WITH n
|
|
||||||
# ORDER BY elementId(n) ASC
|
|
||||||
# LIMIT $limit
|
|
||||||
# MATCH (n)-[rel]->(m)
|
|
||||||
# RETURN n, rel, m
|
|
||||||
# """, limit=limit)
|
|
||||||
|
|
||||||
# node_map, link_map = {}, {}
|
|
||||||
# for rec in result:
|
|
||||||
# n, r, m = rec["n"], rec["rel"], rec["m"]
|
|
||||||
# node_map[n.element_id] = node_to_json(n)
|
|
||||||
# node_map[m.element_id] = node_to_json(m)
|
|
||||||
# link_map[r.element_id] = rel_to_json(r)
|
|
||||||
|
|
||||||
# return {
|
|
||||||
# "nodes": list(node_map.values()),
|
|
||||||
# "links": list(link_map.values())
|
|
||||||
# }
|
|
||||||
|
|
||||||
# def fetch_graph_sample(driver, limit: int = 200):
|
|
||||||
# """
|
|
||||||
# 获取采样数据:
|
|
||||||
# 1. 选取前 $limit 个种子节点(Level 0 或 1,取决于业务定义,这里定义种子为 Level 0)。
|
|
||||||
# 2. 对每个节点向下探索最多 3 跳。
|
|
||||||
# 3. 每个节点对象中增加 level 字段,表示其距离种子节点的深度。
|
|
||||||
# """
|
|
||||||
# with driver.session() as session:
|
|
||||||
# # Cypher 逻辑:
|
|
||||||
# # - 选出种子节点 n
|
|
||||||
# # - 匹配 1-3 跳路径
|
|
||||||
# # - 返回种子节点 n,路径 path,以及该路径的长度(即层级)
|
|
||||||
# result = session.run("""
|
|
||||||
# MATCH (n)
|
|
||||||
# WITH n ORDER BY elementId(n) ASC LIMIT $limit
|
|
||||||
# OPTIONAL MATCH path = (n)-[*1..3]-(m)
|
|
||||||
# UNWIND nodes(path) AS x
|
|
||||||
# WITH n, path, m, COUNT(DISTINCT x) AS nodeCount
|
|
||||||
# WHERE path IS NULL OR nodeCount = LENGTH(path) + 1
|
|
||||||
# RETURN n, path, LENGTH(path) AS depth
|
|
||||||
# """, limit=limit)
|
|
||||||
|
|
||||||
# node_map, link_map = {}, {}
|
|
||||||
|
|
||||||
# for rec in result:
|
|
||||||
# seed_node = rec["n"]
|
|
||||||
# path = rec["path"]
|
|
||||||
# depth = rec["depth"] or 0 # 如果没有路径,深度为 0
|
|
||||||
|
|
||||||
# # 1. 处理种子节点 (Level 0)
|
|
||||||
# if seed_node.element_id not in node_map:
|
|
||||||
# node_data = node_to_json(seed_node)
|
|
||||||
# node_data["level"] = 0 # 种子节点设为 0 层
|
|
||||||
# node_map[seed_node.element_id] = node_data
|
|
||||||
|
|
||||||
# # 2. 如果存在路径,解析路径中的所有节点和关系
|
|
||||||
# if path:
|
|
||||||
# # 路径中的节点处理
|
|
||||||
# # path.nodes 包含了从起始到终点的所有节点
|
|
||||||
# for i, node in enumerate(path.nodes):
|
|
||||||
# if node.element_id not in node_map:
|
|
||||||
# node_data = node_to_json(node)
|
|
||||||
# # 层级即为该节点在当前路径中的索引
|
|
||||||
# node_data["level"] = i
|
|
||||||
# node_map[node.element_id] = node_data
|
|
||||||
# else:
|
|
||||||
# # 如果节点已存在,保留最小的 level (即最靠近种子的距离)
|
|
||||||
# node_map[node.element_id]["level"] = min(node_map[node.element_id].get("level", 3), i)
|
|
||||||
|
|
||||||
# # 路径中的关系处理
|
|
||||||
# for rel in path.relationships:
|
|
||||||
# if rel.element_id not in link_map:
|
|
||||||
# link_map[rel.element_id] = rel_to_json(rel)
|
|
||||||
|
|
||||||
# return {
|
|
||||||
# "nodes": list(node_map.values()),
|
|
||||||
# "links": list(link_map.values())
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
# def fetch_graph_sample(driver, limit=200, max_depth=3):
|
|
||||||
# with driver.session() as session:
|
|
||||||
# result = session.run("""
|
|
||||||
# MATCH (c)
|
|
||||||
# WITH c ORDER BY elementId(c) ASC LIMIT $limit
|
|
||||||
|
|
||||||
# MATCH path = (c)-[*0..3]-(n)
|
|
||||||
# WITH n, MIN(length(path)) AS dist
|
|
||||||
# OPTIONAL MATCH (n)-[r]-(m)
|
|
||||||
|
|
||||||
# RETURN n, dist, r, m
|
|
||||||
# """, limit=limit)
|
|
||||||
|
|
||||||
# node_map, link_map = {}, {}
|
|
||||||
|
|
||||||
# for rec in result:
|
|
||||||
# n = rec["n"]
|
|
||||||
# dist = rec["dist"]
|
|
||||||
# r = rec["r"]
|
|
||||||
# m = rec["m"]
|
|
||||||
|
|
||||||
# # ---- 节点 ----
|
|
||||||
# nid = n.element_id
|
|
||||||
# if nid not in node_map:
|
|
||||||
# node_data = node_to_json(n)
|
|
||||||
# node_data["level"] = min(dist, 3) if dist is not None else 0
|
|
||||||
# node_map[nid] = node_data
|
|
||||||
|
|
||||||
# # ---- 边 ----
|
|
||||||
# if r and m:
|
|
||||||
# mid = m.element_id
|
|
||||||
# if mid not in node_map:
|
|
||||||
# node_map[mid] = node_to_json(m)
|
|
||||||
|
|
||||||
# link_map.setdefault(r.element_id, rel_to_json(r))
|
|
||||||
|
|
||||||
# return {
|
|
||||||
# "nodes": list(node_map.values()),
|
|
||||||
# "links": list(link_map.values())
|
|
||||||
# }
|
|
||||||
|
|
||||||
def fetch_graph_sample(driver):
|
|
||||||
"""
|
|
||||||
查询所有舰艇及其关联的系统和子系统
|
|
||||||
路径结构:(舰艇) -> (系统) -> (子系统)
|
|
||||||
"""
|
|
||||||
|
|
||||||
with driver.session() as session:
|
|
||||||
# =========================
|
|
||||||
# 统一查询:舰艇 -> 系统 -> 子系统
|
|
||||||
# =========================
|
|
||||||
# 逻辑说明:
|
|
||||||
# 1. MATCH (ship:舰艇):选中所有舰艇
|
|
||||||
# 2. -[*1..3]->(target):查找深度为 1 到 3 的路径
|
|
||||||
# - 深度 1:舰艇 -> 系统
|
|
||||||
# - 深度 2:舰艇 -> 系统 -> 子系统
|
|
||||||
# 3. WHERE target:系统 OR target:子系统:确保终点是我们关心的节点类型
|
|
||||||
# (防止查询出其他无关的深层节点)
|
|
||||||
|
|
||||||
query = """
|
|
||||||
MATCH path = (ship:舰艇)-[*1..3]->(target)
|
|
||||||
WHERE target:系统 OR target:子系统
|
|
||||||
RETURN path
|
|
||||||
"""
|
|
||||||
|
|
||||||
result = session.run(query)
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# 解析 paths
|
|
||||||
# =========================
|
|
||||||
all_nodes = {}
|
|
||||||
all_links = {}
|
|
||||||
|
|
||||||
for record in result:
|
|
||||||
path = record["path"]
|
|
||||||
|
|
||||||
# 解析节点
|
|
||||||
for node in path.nodes:
|
|
||||||
# 兼容 ID 获取
|
|
||||||
node_key = node.element_id if hasattr(node, 'element_id') else node.id
|
|
||||||
if node_key not in all_nodes:
|
|
||||||
all_nodes[node_key] = node_to_json(node)
|
|
||||||
|
|
||||||
# 解析关系
|
|
||||||
for rel in path.relationships:
|
|
||||||
rel_key = rel.element_id if hasattr(rel, 'element_id') else rel.id
|
|
||||||
if rel_key not in all_links:
|
|
||||||
all_links[rel_key] = rel_to_json(rel)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"nodes": list(all_nodes.values()),
|
|
||||||
"links": list(all_links.values())
|
|
||||||
}
|
|
||||||
def fetch_graph_sample1(driver):
|
|
||||||
"""
|
|
||||||
查找路径:
|
|
||||||
1. 优先:舰艇 -> 子系统
|
|
||||||
2. fallback:舰艇 -> 系统
|
|
||||||
|
|
||||||
注意:根据数据库实际结构,使用 Label (:舰艇, :子系统) 进行匹配,
|
|
||||||
而不是 WHERE category = '...'
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 定义采样数量
|
|
||||||
SAMPLE_LIMIT = 400
|
|
||||||
|
|
||||||
with driver.session() as session:
|
|
||||||
# =========================
|
|
||||||
# 第一阶段:舰艇 -> 子系统
|
|
||||||
# =========================
|
|
||||||
# 修改点:直接使用 (:舰艇) 和 (:子系统) 标签匹配,去掉 WHERE 子句
|
|
||||||
result = session.run(
|
|
||||||
"""
|
|
||||||
MATCH (ship:舰艇)
|
|
||||||
WITH ship LIMIT $limit
|
|
||||||
|
|
||||||
MATCH path = (ship)-[*1..3]->(sub:子系统)
|
|
||||||
RETURN path
|
|
||||||
""",
|
|
||||||
limit=SAMPLE_LIMIT,
|
|
||||||
)
|
|
||||||
|
|
||||||
paths = [record["path"] for record in result]
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# fallback:舰艇 -> 系统
|
|
||||||
# =========================
|
|
||||||
if not paths:
|
|
||||||
# 修改点:直接使用 (:舰艇) 和 (:系统) 标签匹配
|
|
||||||
result = session.run(
|
|
||||||
"""
|
|
||||||
MATCH (ship:舰艇)
|
|
||||||
WITH ship LIMIT $limit
|
|
||||||
|
|
||||||
MATCH path = (ship)-[*1..2]->(sys:系统)
|
|
||||||
RETURN path
|
|
||||||
""",
|
|
||||||
limit=SAMPLE_LIMIT,
|
|
||||||
)
|
|
||||||
|
|
||||||
paths = [record["path"] for record in result]
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# 解析 paths
|
|
||||||
# =========================
|
|
||||||
all_nodes = {}
|
|
||||||
all_links = {}
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
# 节点
|
|
||||||
for node in path.nodes:
|
|
||||||
# 兼容 Neo4j 5.x (element_id) 和 4.x (id)
|
|
||||||
node_key = node.element_id if hasattr(node, "element_id") else node.id
|
|
||||||
if node_key not in all_nodes:
|
|
||||||
all_nodes[node_key] = node_to_json(node)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
for rel in path.relationships:
|
|
||||||
rel_key = rel.element_id if hasattr(rel, "element_id") else rel.id
|
|
||||||
if rel_key not in all_links:
|
|
||||||
all_links[rel_key] = rel_to_json(rel)
|
|
||||||
|
|
||||||
return {"nodes": list(all_nodes.values()), "links": list(all_links.values())}
|
|
||||||
|
|
||||||
|
|
||||||
# =============================
|
|
||||||
# 文件名处理方法
|
|
||||||
# =============================
|
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
|
|
||||||
|
|
||||||
|
|
||||||
def _load_tree_data() -> Dict:
|
|
||||||
# 注意:实际运行时请确保 result.json 存在,此处仅为代码结构展示
|
|
||||||
if not os.path.exists(TREE_JSON_PATH):
|
|
||||||
return {}
|
|
||||||
with open(TREE_JSON_PATH, "r", encoding="utf-8") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_ship_root(data: Dict, ship_code: str) -> Dict:
|
|
||||||
raw = data.get(ship_code)
|
|
||||||
if not isinstance(raw, dict):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# 结构 1:直接是节点 {"code","name","children"}
|
|
||||||
if {"code", "name", "children"}.issubset(raw.keys()):
|
|
||||||
return raw
|
|
||||||
|
|
||||||
# 结构 2:包装层 {"101": {...root node...}}
|
|
||||||
if ship_code in raw and isinstance(raw[ship_code], dict):
|
|
||||||
node = raw[ship_code]
|
|
||||||
if {"code", "name", "children"}.issubset(node.keys()):
|
|
||||||
return node
|
|
||||||
|
|
||||||
# 兜底:取第一个 node-like 值
|
|
||||||
for value in raw.values():
|
|
||||||
if isinstance(value, dict) and {"code", "name", "children"}.issubset(value.keys()):
|
|
||||||
return value
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def _longest_common_substring_len(a: str, b: str) -> int:
|
|
||||||
if not a or not b:
|
|
||||||
return 0
|
|
||||||
# 优化:确保 a 是较短的字符串以节省空间(可选),原逻辑保持不动也没问题
|
|
||||||
dp = [0] * (len(b) + 1)
|
|
||||||
best = 0
|
|
||||||
for i in range(1, len(a) + 1):
|
|
||||||
prev = 0
|
|
||||||
for j in range(1, len(b) + 1):
|
|
||||||
temp = dp[j]
|
|
||||||
if a[i - 1] == b[j - 1]:
|
|
||||||
dp[j] = prev + 1
|
|
||||||
if dp[j] > best:
|
|
||||||
best = dp[j]
|
|
||||||
else:
|
|
||||||
dp[j] = 0
|
|
||||||
prev = temp
|
|
||||||
return best
|
|
||||||
|
|
||||||
|
|
||||||
def _match_level4_tool(filename_text: str, device_node: Dict) -> Tuple[str, str]:
|
|
||||||
"""
|
|
||||||
匹配 Level 4 零件。
|
|
||||||
规则:如果文件名与 Level 3 (设备名) 的最大共同字符串长度 >= 与任何 Level 4 (零件名) 的长度,
|
|
||||||
则只匹配到 Level 3,返回空。
|
|
||||||
只有当 Level 4 的匹配度严格高于 Level 3 时,才返回 Level 4 信息。
|
|
||||||
"""
|
|
||||||
children = device_node.get("children") or {}
|
|
||||||
|
|
||||||
# 1. 计算文件名与当前设备 (Level 3) 名称的匹配度
|
|
||||||
device_name = str(device_node.get("name") or "")
|
|
||||||
level3_score = _longest_common_substring_len(filename_text, device_name)
|
|
||||||
|
|
||||||
best_name = ""
|
|
||||||
best_code = ""
|
|
||||||
best_score = 0 # 记录 Level 4 中的最佳得分
|
|
||||||
|
|
||||||
# 2. 遍历 Level 4 子节点
|
|
||||||
for child in children.values():
|
|
||||||
tool_name = str(child.get("name") or "")
|
|
||||||
tool_code = str(child.get("code") or "")
|
|
||||||
|
|
||||||
score = _longest_common_substring_len(filename_text, tool_name)
|
|
||||||
|
|
||||||
# 关键修改:只有当 Level 4 的得分严格大于 Level 3 的得分时,才视为有效匹配
|
|
||||||
# 并且要比当前找到的其他 Level 4 更好
|
|
||||||
if score > level3_score and score > best_score:
|
|
||||||
best_score = score
|
|
||||||
best_name = tool_name
|
|
||||||
best_code = tool_code
|
|
||||||
|
|
||||||
# 如果没有找到比 Level 3 匹配度更高的 Level 4,则返回空
|
|
||||||
if best_score <= level3_score:
|
|
||||||
return "", ""
|
|
||||||
|
|
||||||
return best_name, best_code
|
|
||||||
|
|
||||||
|
|
||||||
def process_filename(filename: str) -> Dict[str, str]:
|
|
||||||
name, _ = os.path.splitext(os.path.basename(filename))
|
|
||||||
parts = re.split(r"[_-]", name)
|
|
||||||
|
|
||||||
xian_number = ""
|
|
||||||
level_1_system_name = ""
|
|
||||||
level_2_system_name = ""
|
|
||||||
device_name = ""
|
|
||||||
tool_name = ""
|
|
||||||
tool_code = ""
|
|
||||||
|
|
||||||
if not parts:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
xian_number_code = parts[0].strip()
|
|
||||||
level1_code = ""
|
|
||||||
level2_code = ""
|
|
||||||
device_code = ""
|
|
||||||
|
|
||||||
if len(parts) > 1:
|
|
||||||
part = parts[1].strip()
|
|
||||||
# 防止索引越界
|
|
||||||
if len(part) < 2:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
level1_code = part[:2]
|
|
||||||
level2_code = part[2:4] if len(part) > 2 else ""
|
|
||||||
device_code = part[4:] if len(part) > 4 else ""
|
|
||||||
# 若 parts[2] 存在,则用 parts[2] 作为 device_code(支持 122-06A0014-B01001_发动机 格式)
|
|
||||||
if len(parts) > 2:
|
|
||||||
device_code_alt = parts[2].strip()
|
|
||||||
if device_code_alt:
|
|
||||||
device_code = device_code_alt
|
|
||||||
|
|
||||||
data = _load_tree_data()
|
|
||||||
root = _resolve_ship_root(data, xian_number_code)
|
|
||||||
if not root:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
xian_number = root.get("name", "")
|
|
||||||
|
|
||||||
# 一级
|
|
||||||
s1 = root.get("children", {}).get(level1_code)
|
|
||||||
if s1:
|
|
||||||
level_1_system_name = s1.get("name", "")
|
|
||||||
|
|
||||||
# 二级
|
|
||||||
s2 = s1.get("children", {}).get(level2_code)
|
|
||||||
if s2:
|
|
||||||
level_2_system_name = s2.get("name", "")
|
|
||||||
|
|
||||||
# 三级设备
|
|
||||||
device = s2.get("children", {}).get(device_code)
|
|
||||||
if device:
|
|
||||||
device_name = device.get("name", "")
|
|
||||||
# 若存在 level4,匹配与文件名最大相同字符串的零件
|
|
||||||
# 内部已包含逻辑:如果设备名匹配度更高,则不返回零件
|
|
||||||
tool_name, tool_code = _match_level4_tool(name, device)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"Xian_Number": xian_number,
|
|
||||||
"Xian_Number_code": xian_number_code,
|
|
||||||
"Level_1_System_Name": level_1_system_name,
|
|
||||||
"level1_code": level1_code,
|
|
||||||
"Level_2_System_Name": level_2_system_name,
|
|
||||||
"level2_code": level2_code,
|
|
||||||
"Device_Name": device_name,
|
|
||||||
"device_code": device_code,
|
|
||||||
"tool_name": tool_name,
|
|
||||||
"tool_code": tool_code,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_entity(filename):
|
|
||||||
result = process_filename(filename)
|
|
||||||
entities = []
|
|
||||||
relationships = []
|
|
||||||
low_level = ""
|
|
||||||
|
|
||||||
if result:
|
|
||||||
xian_number = result.get("Xian_Number")
|
|
||||||
xian_number_code = result.get("Xian_Number_code")
|
|
||||||
level_1_system_name = result.get("Level_1_System_Name")
|
|
||||||
level1_code = result.get("level1_code")
|
|
||||||
level_2_system_name = result.get("Level_2_System_Name")
|
|
||||||
level2_code = result.get("level2_code")
|
|
||||||
device_name = result.get("Device_Name")
|
|
||||||
device_code = result.get("device_code")
|
|
||||||
tool_name = result.get("tool_name")
|
|
||||||
tool_code = result.get("tool_code")
|
|
||||||
|
|
||||||
if xian_number:
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"type": "舰艇",
|
|
||||||
"properties": {
|
|
||||||
"名称": xian_number,
|
|
||||||
"舷号": xian_number_code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
low_level = xian_number
|
|
||||||
|
|
||||||
if level_1_system_name:
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"type": "系统",
|
|
||||||
"properties": {
|
|
||||||
"名称": level_1_system_name,
|
|
||||||
"系统编码": level1_code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
low_level = level_1_system_name
|
|
||||||
|
|
||||||
if xian_number and level_1_system_name:
|
|
||||||
relationships.append(
|
|
||||||
{
|
|
||||||
"type": "包含",
|
|
||||||
"from_entity": xian_number,
|
|
||||||
"to_entity": level_1_system_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if level_2_system_name:
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"type": "子系统",
|
|
||||||
"properties": {
|
|
||||||
"名称": level_2_system_name,
|
|
||||||
"子系统编码": level2_code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
low_level = level_2_system_name
|
|
||||||
|
|
||||||
if level_1_system_name and level_2_system_name:
|
|
||||||
relationships.append(
|
|
||||||
{
|
|
||||||
"type": "包含",
|
|
||||||
"from_entity": level_1_system_name,
|
|
||||||
"to_entity": level_2_system_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if device_name:
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"type": "设备",
|
|
||||||
"properties": {
|
|
||||||
"名称": device_name,
|
|
||||||
"设备编码": device_code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
low_level = device_name
|
|
||||||
|
|
||||||
if level_2_system_name and device_name:
|
|
||||||
relationships.append(
|
|
||||||
{
|
|
||||||
"type": "包含",
|
|
||||||
"from_entity": level_2_system_name,
|
|
||||||
"to_entity": device_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if tool_name:
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"type": "零件",
|
|
||||||
"properties": {
|
|
||||||
"名称": tool_name,
|
|
||||||
"零件编码": tool_code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
low_level = tool_name
|
|
||||||
|
|
||||||
if device_name and tool_name:
|
|
||||||
relationships.append(
|
|
||||||
{
|
|
||||||
"type": "包含",
|
|
||||||
"from_entity": device_name,
|
|
||||||
"to_entity": tool_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
final_result = {
|
|
||||||
"entities": entities,
|
|
||||||
"relationships": relationships,
|
|
||||||
"low_level": low_level,
|
|
||||||
}
|
|
||||||
|
|
||||||
return final_result
|
|
||||||
|
|
||||||
|
|
||||||
# 示例测试逻辑(非必须,仅供验证)
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# 阀控蓄电池脉冲式快速充电装置
|
|
||||||
# test_list = [
|
|
||||||
# "163-06A0015-B01001_发动机-维修手册.pdf",
|
|
||||||
|
|
||||||
# ]
|
|
||||||
# for i in test_list:
|
|
||||||
# result = get_entity(i)
|
|
||||||
# print(result)
|
|
||||||
from neo4j import GraphDatabase
|
|
||||||
|
|
||||||
NEO4J_URI = "bolt://192.168.0.111:7687"
|
|
||||||
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
|
|
||||||
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD","zdht123@") # 不设默认值,强制要求提供
|
|
||||||
NEO4J_DATABASE = os.getenv("NEO4J_DATABASE", "neo4j")
|
|
||||||
driver = GraphDatabase.driver(
|
|
||||||
NEO4J_URI,
|
|
||||||
auth=(NEO4J_USER, NEO4J_PASSWORD),
|
|
||||||
database=NEO4J_DATABASE
|
|
||||||
)
|
|
||||||
res = fetch_graph_sample(driver)
|
|
||||||
print(res)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,815 +0,0 @@
|
|||||||
"""
|
|
||||||
PDF 用户上传接口(FastAPI)
|
|
||||||
- 处理文件名和其对应关系
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Dict, Tuple
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from typing import Dict
|
|
||||||
from fastapi import FastAPI, HTTPException
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from modelsAPI.model_api import OpenaiAPI
|
|
||||||
from openai import OpenAI
|
|
||||||
from typing import Dict, List, Any
|
|
||||||
|
|
||||||
app = FastAPI(title="PDF Upload Service")
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
|
|
||||||
logger = logging.getLogger("filename_neo4j_process")
|
|
||||||
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# 图方法
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
|
|
||||||
def node_to_json(node):
|
|
||||||
props = dict(node)
|
|
||||||
# ks_raw = props.get("knowledge_source")
|
|
||||||
# knowledge_source = json.loads(ks_raw) if ks_raw else []
|
|
||||||
|
|
||||||
ks_raw = props.get("knowledge_source")
|
|
||||||
knowledge_source = []
|
|
||||||
|
|
||||||
if ks_raw and isinstance(ks_raw, str):
|
|
||||||
try:
|
|
||||||
ks_list = json.loads(ks_raw)
|
|
||||||
if isinstance(ks_list, list):
|
|
||||||
for ks in ks_list:
|
|
||||||
if isinstance(ks, dict):
|
|
||||||
ks.pop("info", None)
|
|
||||||
knowledge_source.append(ks)
|
|
||||||
except (json.JSONDecodeError, TypeError):
|
|
||||||
# 解析失败或类型错误时,保持 knowledge_source 为空列表
|
|
||||||
pass
|
|
||||||
|
|
||||||
name = props.get("名称")
|
|
||||||
props.pop("embedding", None)
|
|
||||||
props.pop("knowledge_source", None)
|
|
||||||
props.pop("name", None) # 现在安全地移除
|
|
||||||
props.pop("fulltext", None)
|
|
||||||
if "last_updated" in props:
|
|
||||||
props["最后更新时间"] = props.pop("last_updated")
|
|
||||||
|
|
||||||
if "created_at" in props:
|
|
||||||
props["创建时间"] = props.pop("created_at")
|
|
||||||
return {
|
|
||||||
"id": node.element_id,
|
|
||||||
"name": name, # 使用提前保存的值
|
|
||||||
"labels": list(node.labels),
|
|
||||||
"knowledge_source": knowledge_source,
|
|
||||||
"properties": props,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def rel_to_json(rel):
|
|
||||||
props = dict(rel)
|
|
||||||
props.pop("embedding", None)
|
|
||||||
props.pop("fact_timeline", None)
|
|
||||||
return {"id": rel.element_id, "label": rel.type, "source": rel.start_node.element_id, "target": rel.end_node.element_id, "properties": props}
|
|
||||||
|
|
||||||
|
|
||||||
def is_simple_path(path):
|
|
||||||
"""
|
|
||||||
判断是否为无环路径(simple path)
|
|
||||||
"""
|
|
||||||
seen = set()
|
|
||||||
for n in path.nodes:
|
|
||||||
nid = n.element_id
|
|
||||||
if nid in seen:
|
|
||||||
return False
|
|
||||||
seen.add(nid)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def build_tree(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
|
|
||||||
"""
|
|
||||||
按照固定层级构建树:舰艇 -> 系统 -> 子系统
|
|
||||||
不再依赖 links 的连线,直接根据 labels 归类,彻底避免环状数据导致的死循环。
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 1. 辅助函数:判断标签是否包含关键字
|
|
||||||
def has_label(node, keyword):
|
|
||||||
labels = node.get("labels", [])
|
|
||||||
if isinstance(labels, list):
|
|
||||||
return any(keyword in lbl for lbl in labels)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 2. 辅助函数:构建标准的树节点格式
|
|
||||||
def make_node(node_data):
|
|
||||||
return {
|
|
||||||
"id": node_data["id"],
|
|
||||||
"name": node_data.get("name", ""),
|
|
||||||
"labels": node_data.get("labels", []),
|
|
||||||
|
|
||||||
"properties": node_data.get("properties", {}),
|
|
||||||
"color": node_data.get("color"),
|
|
||||||
"size": node_data.get("size"),
|
|
||||||
"children": []
|
|
||||||
}
|
|
||||||
|
|
||||||
# 3. 将节点按层级分类存储到字典中,方便快速查找
|
|
||||||
# key: id, value: node_dict_with_children
|
|
||||||
ships = {} # 第一层:舰艇
|
|
||||||
systems = {} # 第二层:系统(不含子系统)
|
|
||||||
sub_systems = {} # 第三层:子系统
|
|
||||||
|
|
||||||
for node in nodes:
|
|
||||||
# 注意:判断逻辑要互斥,防止“子系统”被误判为“系统”
|
|
||||||
if has_label(node, "舰艇"):
|
|
||||||
ships[node["id"]] = make_node(node)
|
|
||||||
elif has_label(node, "子系统"):
|
|
||||||
sub_systems[node["id"]] = make_node(node)
|
|
||||||
elif has_label(node, "系统"):
|
|
||||||
systems[node["id"]] = make_node(node)
|
|
||||||
else:
|
|
||||||
# 如果有其他无关节点,可以在这里处理或忽略
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 4. 开始组装:利用 links 建立关系(此时 links 只充当“连接器”,不决定层级)
|
|
||||||
# 因为我们已经确定了层级,所以不会出现循环引用
|
|
||||||
|
|
||||||
for link in links:
|
|
||||||
if link.get("label") != "包含":
|
|
||||||
continue
|
|
||||||
|
|
||||||
src_id = link["source"]
|
|
||||||
tgt_id = link["target"]
|
|
||||||
|
|
||||||
# 逻辑 A: 舰艇 -> 系统
|
|
||||||
if src_id in ships and tgt_id in systems:
|
|
||||||
ships[src_id]["children"].append(systems[tgt_id])
|
|
||||||
|
|
||||||
# 逻辑 B: 系统 -> 子系统
|
|
||||||
elif src_id in systems and tgt_id in sub_systems:
|
|
||||||
systems[src_id]["children"].append(sub_systems[tgt_id])
|
|
||||||
|
|
||||||
# 逻辑 C: 舰艇 -> 子系统 (防止有些数据直接连了子系统,虽然不规范但也兼容一下)
|
|
||||||
elif src_id in ships and tgt_id in sub_systems:
|
|
||||||
ships[src_id]["children"].append(sub_systems[tgt_id])
|
|
||||||
|
|
||||||
# 5. 返回所有舰艇作为根节点列表
|
|
||||||
return list(ships.values())
|
|
||||||
def build_tree1(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
|
|
||||||
"""
|
|
||||||
从 nodes + links 构建树结构。
|
|
||||||
- 根节点:label 包含 '舰艇' 的节点
|
|
||||||
- 父子关系:links 中 label == '包含' 的边 (source -> target)
|
|
||||||
- 支持多棵树(多个舰艇根节点)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 1. 建立 id -> node 的映射
|
|
||||||
node_map: Dict[str, Dict] = {n["id"]: n for n in nodes}
|
|
||||||
|
|
||||||
# 2. 建立 父id -> [子id列表] 的邻接表(只处理"包含"关系)
|
|
||||||
children_map: Dict[str, List[str]] = {n["id"]: [] for n in nodes}
|
|
||||||
parent_map: Dict[str, str] = {} # 子id -> 父id,用于识别根节点
|
|
||||||
|
|
||||||
for link in links:
|
|
||||||
if link.get("label") == "包含":
|
|
||||||
src = link["source"]
|
|
||||||
tgt = link["target"]
|
|
||||||
if src in children_map:
|
|
||||||
children_map[src].append(tgt)
|
|
||||||
if tgt not in parent_map:
|
|
||||||
parent_map[tgt] = src
|
|
||||||
|
|
||||||
# 3. 递归构建子树
|
|
||||||
def build_subtree(node_id: str, ancestors: set = None) -> Dict[str, Any]:
|
|
||||||
if ancestors is None:
|
|
||||||
ancestors = set()
|
|
||||||
if node_id in ancestors:
|
|
||||||
return None
|
|
||||||
ancestors.add(node_id)
|
|
||||||
node = node_map[node_id]
|
|
||||||
subtree = {"id": node_id, "name": node.get("name", ""), "labels": node.get("labels", []), "properties": node.get("properties", {}), "color": node.get("color"), "size": node.get("size"), "children": []}
|
|
||||||
current_ancestors = ancestors | {node_id}
|
|
||||||
for child_id in children_map.get(node_id, []):
|
|
||||||
child = build_subtree(child_id, current_ancestors)
|
|
||||||
if child is not None:
|
|
||||||
subtree["children"].append(child)
|
|
||||||
return subtree
|
|
||||||
|
|
||||||
# def build_subtree(node_id: str) -> Dict[str, Any]:
|
|
||||||
# node = node_map[node_id]
|
|
||||||
# subtree = {
|
|
||||||
# "id": node_id,
|
|
||||||
# "name": node.get("name", ""),
|
|
||||||
# "labels": node.get("labels", []),
|
|
||||||
# "properties": node.get("properties", {}),
|
|
||||||
# "color": node.get("color"),
|
|
||||||
# "size": node.get("size"),
|
|
||||||
# "children": []
|
|
||||||
# }
|
|
||||||
# for child_id in children_map.get(node_id, []):
|
|
||||||
# subtree["children"].append(build_subtree(child_id))
|
|
||||||
# return subtree
|
|
||||||
|
|
||||||
# 4. 找出所有根节点:label 包含 '舰艇',且没有父节点
|
|
||||||
roots = [n["id"] for n in nodes if "舰艇" in n.get("labels", []) and n["id"] not in parent_map]
|
|
||||||
|
|
||||||
# 5. 为每棵树构建结构,并打上树编号
|
|
||||||
forest = []
|
|
||||||
for i, root_id in enumerate(roots):
|
|
||||||
tree = build_subtree(root_id)
|
|
||||||
tree["tree_index"] = i # 区分不同树
|
|
||||||
forest.append(tree)
|
|
||||||
|
|
||||||
return forest
|
|
||||||
|
|
||||||
|
|
||||||
def build_graph_from_paths(paths):
|
|
||||||
"""
|
|
||||||
核心公共方法
|
|
||||||
从 Neo4j 路径结果构建 graphData
|
|
||||||
"""
|
|
||||||
node_map: Dict[str, dict] = {}
|
|
||||||
link_map: Dict[str, dict] = {}
|
|
||||||
|
|
||||||
def add_edge(rel):
|
|
||||||
s = rel.start_node
|
|
||||||
t = rel.end_node
|
|
||||||
|
|
||||||
if s.element_id not in node_map:
|
|
||||||
node_map[s.element_id] = node_to_json(s)
|
|
||||||
|
|
||||||
if t.element_id not in node_map:
|
|
||||||
node_map[t.element_id] = node_to_json(t)
|
|
||||||
|
|
||||||
if rel.element_id not in link_map:
|
|
||||||
link_map[rel.element_id] = rel_to_json(rel)
|
|
||||||
|
|
||||||
for record in paths:
|
|
||||||
p = record["p"]
|
|
||||||
|
|
||||||
if not is_simple_path(p):
|
|
||||||
continue
|
|
||||||
|
|
||||||
for r in p.relationships:
|
|
||||||
add_edge(r)
|
|
||||||
|
|
||||||
return {"nodes": list(node_map.values()), "links": list(link_map.values())}
|
|
||||||
|
|
||||||
|
|
||||||
# def fetch_graph_sample(driver,limit: int = 200):
|
|
||||||
# """提取采样逻辑为可复用函数"""
|
|
||||||
# with driver.session() as session:
|
|
||||||
# result = session.run("""
|
|
||||||
# MATCH (n)
|
|
||||||
# WITH n
|
|
||||||
# ORDER BY elementId(n) ASC
|
|
||||||
# LIMIT $limit
|
|
||||||
# MATCH (n)-[rel]->(m)
|
|
||||||
# RETURN n, rel, m
|
|
||||||
# """, limit=limit)
|
|
||||||
|
|
||||||
# node_map, link_map = {}, {}
|
|
||||||
# for rec in result:
|
|
||||||
# n, r, m = rec["n"], rec["rel"], rec["m"]
|
|
||||||
# node_map[n.element_id] = node_to_json(n)
|
|
||||||
# node_map[m.element_id] = node_to_json(m)
|
|
||||||
# link_map[r.element_id] = rel_to_json(r)
|
|
||||||
|
|
||||||
# return {
|
|
||||||
# "nodes": list(node_map.values()),
|
|
||||||
# "links": list(link_map.values())
|
|
||||||
# }
|
|
||||||
|
|
||||||
# def fetch_graph_sample(driver, limit: int = 200):
|
|
||||||
# """
|
|
||||||
# 获取采样数据:
|
|
||||||
# 1. 选取前 $limit 个种子节点(Level 0 或 1,取决于业务定义,这里定义种子为 Level 0)。
|
|
||||||
# 2. 对每个节点向下探索最多 3 跳。
|
|
||||||
# 3. 每个节点对象中增加 level 字段,表示其距离种子节点的深度。
|
|
||||||
# """
|
|
||||||
# with driver.session() as session:
|
|
||||||
# # Cypher 逻辑:
|
|
||||||
# # - 选出种子节点 n
|
|
||||||
# # - 匹配 1-3 跳路径
|
|
||||||
# # - 返回种子节点 n,路径 path,以及该路径的长度(即层级)
|
|
||||||
# result = session.run("""
|
|
||||||
# MATCH (n)
|
|
||||||
# WITH n ORDER BY elementId(n) ASC LIMIT $limit
|
|
||||||
# OPTIONAL MATCH path = (n)-[*1..3]-(m)
|
|
||||||
# UNWIND nodes(path) AS x
|
|
||||||
# WITH n, path, m, COUNT(DISTINCT x) AS nodeCount
|
|
||||||
# WHERE path IS NULL OR nodeCount = LENGTH(path) + 1
|
|
||||||
# RETURN n, path, LENGTH(path) AS depth
|
|
||||||
# """, limit=limit)
|
|
||||||
|
|
||||||
# node_map, link_map = {}, {}
|
|
||||||
|
|
||||||
# for rec in result:
|
|
||||||
# seed_node = rec["n"]
|
|
||||||
# path = rec["path"]
|
|
||||||
# depth = rec["depth"] or 0 # 如果没有路径,深度为 0
|
|
||||||
|
|
||||||
# # 1. 处理种子节点 (Level 0)
|
|
||||||
# if seed_node.element_id not in node_map:
|
|
||||||
# node_data = node_to_json(seed_node)
|
|
||||||
# node_data["level"] = 0 # 种子节点设为 0 层
|
|
||||||
# node_map[seed_node.element_id] = node_data
|
|
||||||
|
|
||||||
# # 2. 如果存在路径,解析路径中的所有节点和关系
|
|
||||||
# if path:
|
|
||||||
# # 路径中的节点处理
|
|
||||||
# # path.nodes 包含了从起始到终点的所有节点
|
|
||||||
# for i, node in enumerate(path.nodes):
|
|
||||||
# if node.element_id not in node_map:
|
|
||||||
# node_data = node_to_json(node)
|
|
||||||
# # 层级即为该节点在当前路径中的索引
|
|
||||||
# node_data["level"] = i
|
|
||||||
# node_map[node.element_id] = node_data
|
|
||||||
# else:
|
|
||||||
# # 如果节点已存在,保留最小的 level (即最靠近种子的距离)
|
|
||||||
# node_map[node.element_id]["level"] = min(node_map[node.element_id].get("level", 3), i)
|
|
||||||
|
|
||||||
# # 路径中的关系处理
|
|
||||||
# for rel in path.relationships:
|
|
||||||
# if rel.element_id not in link_map:
|
|
||||||
# link_map[rel.element_id] = rel_to_json(rel)
|
|
||||||
|
|
||||||
# return {
|
|
||||||
# "nodes": list(node_map.values()),
|
|
||||||
# "links": list(link_map.values())
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
# def fetch_graph_sample(driver, limit=200, max_depth=3):
|
|
||||||
# with driver.session() as session:
|
|
||||||
# result = session.run("""
|
|
||||||
# MATCH (c)
|
|
||||||
# WITH c ORDER BY elementId(c) ASC LIMIT $limit
|
|
||||||
|
|
||||||
# MATCH path = (c)-[*0..3]-(n)
|
|
||||||
# WITH n, MIN(length(path)) AS dist
|
|
||||||
# OPTIONAL MATCH (n)-[r]-(m)
|
|
||||||
|
|
||||||
# RETURN n, dist, r, m
|
|
||||||
# """, limit=limit)
|
|
||||||
|
|
||||||
# node_map, link_map = {}, {}
|
|
||||||
|
|
||||||
# for rec in result:
|
|
||||||
# n = rec["n"]
|
|
||||||
# dist = rec["dist"]
|
|
||||||
# r = rec["r"]
|
|
||||||
# m = rec["m"]
|
|
||||||
|
|
||||||
# # ---- 节点 ----
|
|
||||||
# nid = n.element_id
|
|
||||||
# if nid not in node_map:
|
|
||||||
# node_data = node_to_json(n)
|
|
||||||
# node_data["level"] = min(dist, 3) if dist is not None else 0
|
|
||||||
# node_map[nid] = node_data
|
|
||||||
|
|
||||||
# # ---- 边 ----
|
|
||||||
# if r and m:
|
|
||||||
# mid = m.element_id
|
|
||||||
# if mid not in node_map:
|
|
||||||
# node_map[mid] = node_to_json(m)
|
|
||||||
|
|
||||||
# link_map.setdefault(r.element_id, rel_to_json(r))
|
|
||||||
|
|
||||||
# return {
|
|
||||||
# "nodes": list(node_map.values()),
|
|
||||||
# "links": list(link_map.values())
|
|
||||||
# }
|
|
||||||
|
|
||||||
def fetch_graph_sample(driver):
|
|
||||||
"""
|
|
||||||
查询所有舰艇及其关联的系统和子系统
|
|
||||||
路径结构:(舰艇) -> (系统) -> (子系统)
|
|
||||||
"""
|
|
||||||
|
|
||||||
with driver.session() as session:
|
|
||||||
# =========================
|
|
||||||
# 统一查询:舰艇 -> 系统 -> 子系统
|
|
||||||
# =========================
|
|
||||||
# 逻辑说明:
|
|
||||||
# 1. MATCH (ship:舰艇):选中所有舰艇
|
|
||||||
# 2. -[*1..3]->(target):查找深度为 1 到 3 的路径
|
|
||||||
# - 深度 1:舰艇 -> 系统
|
|
||||||
# - 深度 2:舰艇 -> 系统 -> 子系统
|
|
||||||
# 3. WHERE target:系统 OR target:子系统:确保终点是我们关心的节点类型
|
|
||||||
# (防止查询出其他无关的深层节点)
|
|
||||||
|
|
||||||
query = """
|
|
||||||
MATCH path = (ship:舰艇)-[*1..3]->(target)
|
|
||||||
WHERE target:系统 OR target:子系统
|
|
||||||
RETURN path
|
|
||||||
"""
|
|
||||||
|
|
||||||
result = session.run(query)
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# 解析 paths
|
|
||||||
# =========================
|
|
||||||
all_nodes = {}
|
|
||||||
all_links = {}
|
|
||||||
|
|
||||||
for record in result:
|
|
||||||
path = record["path"]
|
|
||||||
|
|
||||||
# 解析节点
|
|
||||||
for node in path.nodes:
|
|
||||||
# 兼容 ID 获取
|
|
||||||
node_key = node.element_id if hasattr(node, 'element_id') else node.id
|
|
||||||
if node_key not in all_nodes:
|
|
||||||
all_nodes[node_key] = node_to_json(node)
|
|
||||||
|
|
||||||
# 解析关系
|
|
||||||
for rel in path.relationships:
|
|
||||||
rel_key = rel.element_id if hasattr(rel, 'element_id') else rel.id
|
|
||||||
if rel_key not in all_links:
|
|
||||||
all_links[rel_key] = rel_to_json(rel)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"nodes": list(all_nodes.values()),
|
|
||||||
"links": list(all_links.values())
|
|
||||||
}
|
|
||||||
def fetch_graph_sample1(driver):
|
|
||||||
"""
|
|
||||||
查找路径:
|
|
||||||
1. 优先:舰艇 -> 子系统
|
|
||||||
2. fallback:舰艇 -> 系统
|
|
||||||
|
|
||||||
注意:根据数据库实际结构,使用 Label (:舰艇, :子系统) 进行匹配,
|
|
||||||
而不是 WHERE category = '...'
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 定义采样数量
|
|
||||||
SAMPLE_LIMIT = 400
|
|
||||||
|
|
||||||
with driver.session() as session:
|
|
||||||
# =========================
|
|
||||||
# 第一阶段:舰艇 -> 子系统
|
|
||||||
# =========================
|
|
||||||
# 修改点:直接使用 (:舰艇) 和 (:子系统) 标签匹配,去掉 WHERE 子句
|
|
||||||
result = session.run(
|
|
||||||
"""
|
|
||||||
MATCH (ship:舰艇)
|
|
||||||
WITH ship LIMIT $limit
|
|
||||||
|
|
||||||
MATCH path = (ship)-[*1..3]->(sub:子系统)
|
|
||||||
RETURN path
|
|
||||||
""",
|
|
||||||
limit=SAMPLE_LIMIT,
|
|
||||||
)
|
|
||||||
|
|
||||||
paths = [record["path"] for record in result]
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# fallback:舰艇 -> 系统
|
|
||||||
# =========================
|
|
||||||
if not paths:
|
|
||||||
# 修改点:直接使用 (:舰艇) 和 (:系统) 标签匹配
|
|
||||||
result = session.run(
|
|
||||||
"""
|
|
||||||
MATCH (ship:舰艇)
|
|
||||||
WITH ship LIMIT $limit
|
|
||||||
|
|
||||||
MATCH path = (ship)-[*1..2]->(sys:系统)
|
|
||||||
RETURN path
|
|
||||||
""",
|
|
||||||
limit=SAMPLE_LIMIT,
|
|
||||||
)
|
|
||||||
|
|
||||||
paths = [record["path"] for record in result]
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# 解析 paths
|
|
||||||
# =========================
|
|
||||||
all_nodes = {}
|
|
||||||
all_links = {}
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
# 节点
|
|
||||||
for node in path.nodes:
|
|
||||||
# 兼容 Neo4j 5.x (element_id) 和 4.x (id)
|
|
||||||
node_key = node.element_id if hasattr(node, "element_id") else node.id
|
|
||||||
if node_key not in all_nodes:
|
|
||||||
all_nodes[node_key] = node_to_json(node)
|
|
||||||
|
|
||||||
# 关系
|
|
||||||
for rel in path.relationships:
|
|
||||||
rel_key = rel.element_id if hasattr(rel, "element_id") else rel.id
|
|
||||||
if rel_key not in all_links:
|
|
||||||
all_links[rel_key] = rel_to_json(rel)
|
|
||||||
|
|
||||||
return {"nodes": list(all_nodes.values()), "links": list(all_links.values())}
|
|
||||||
|
|
||||||
|
|
||||||
# =============================
|
|
||||||
# 文件名处理方法
|
|
||||||
# =============================
|
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
|
|
||||||
|
|
||||||
|
|
||||||
def _load_tree_data() -> Dict:
|
|
||||||
# 注意:实际运行时请确保 result.json 存在,此处仅为代码结构展示
|
|
||||||
if not os.path.exists(TREE_JSON_PATH):
|
|
||||||
return {}
|
|
||||||
with open(TREE_JSON_PATH, "r", encoding="utf-8") as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_ship_root(data: Dict, ship_code: str) -> Dict:
|
|
||||||
raw = data.get(ship_code)
|
|
||||||
if not isinstance(raw, dict):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# 结构 1:直接是节点 {"code","name","children"}
|
|
||||||
if {"code", "name", "children"}.issubset(raw.keys()):
|
|
||||||
return raw
|
|
||||||
|
|
||||||
# 结构 2:包装层 {"101": {...root node...}}
|
|
||||||
if ship_code in raw and isinstance(raw[ship_code], dict):
|
|
||||||
node = raw[ship_code]
|
|
||||||
if {"code", "name", "children"}.issubset(node.keys()):
|
|
||||||
return node
|
|
||||||
|
|
||||||
# 兜底:取第一个 node-like 值
|
|
||||||
for value in raw.values():
|
|
||||||
if isinstance(value, dict) and {"code", "name", "children"}.issubset(value.keys()):
|
|
||||||
return value
|
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def _longest_common_substring_len(a: str, b: str) -> int:
|
|
||||||
if not a or not b:
|
|
||||||
return 0
|
|
||||||
# 优化:确保 a 是较短的字符串以节省空间(可选),原逻辑保持不动也没问题
|
|
||||||
dp = [0] * (len(b) + 1)
|
|
||||||
best = 0
|
|
||||||
for i in range(1, len(a) + 1):
|
|
||||||
prev = 0
|
|
||||||
for j in range(1, len(b) + 1):
|
|
||||||
temp = dp[j]
|
|
||||||
if a[i - 1] == b[j - 1]:
|
|
||||||
dp[j] = prev + 1
|
|
||||||
if dp[j] > best:
|
|
||||||
best = dp[j]
|
|
||||||
else:
|
|
||||||
dp[j] = 0
|
|
||||||
prev = temp
|
|
||||||
return best
|
|
||||||
|
|
||||||
|
|
||||||
def _match_level4_tool(filename_text: str, device_node: Dict) -> Tuple[str, str]:
|
|
||||||
"""
|
|
||||||
匹配 Level 4 零件。
|
|
||||||
规则:如果文件名与 Level 3 (设备名) 的最大共同字符串长度 >= 与任何 Level 4 (零件名) 的长度,
|
|
||||||
则只匹配到 Level 3,返回空。
|
|
||||||
只有当 Level 4 的匹配度严格高于 Level 3 时,才返回 Level 4 信息。
|
|
||||||
"""
|
|
||||||
children = device_node.get("children") or {}
|
|
||||||
|
|
||||||
# 1. 计算文件名与当前设备 (Level 3) 名称的匹配度
|
|
||||||
device_name = str(device_node.get("name") or "")
|
|
||||||
level3_score = _longest_common_substring_len(filename_text, device_name)
|
|
||||||
|
|
||||||
best_name = ""
|
|
||||||
best_code = ""
|
|
||||||
best_score = 0 # 记录 Level 4 中的最佳得分
|
|
||||||
|
|
||||||
# 2. 遍历 Level 4 子节点
|
|
||||||
for child in children.values():
|
|
||||||
tool_name = str(child.get("name") or "")
|
|
||||||
tool_code = str(child.get("code") or "")
|
|
||||||
|
|
||||||
score = _longest_common_substring_len(filename_text, tool_name)
|
|
||||||
|
|
||||||
# 关键修改:只有当 Level 4 的得分严格大于 Level 3 的得分时,才视为有效匹配
|
|
||||||
# 并且要比当前找到的其他 Level 4 更好
|
|
||||||
if score > level3_score and score > best_score:
|
|
||||||
best_score = score
|
|
||||||
best_name = tool_name
|
|
||||||
best_code = tool_code
|
|
||||||
|
|
||||||
# 如果没有找到比 Level 3 匹配度更高的 Level 4,则返回空
|
|
||||||
if best_score <= level3_score:
|
|
||||||
return "", ""
|
|
||||||
|
|
||||||
return best_name, best_code
|
|
||||||
|
|
||||||
|
|
||||||
def process_filename(filename: str) -> Dict[str, str]:
|
|
||||||
name, _ = os.path.splitext(os.path.basename(filename))
|
|
||||||
parts = re.split(r"[_-]", name)
|
|
||||||
|
|
||||||
xian_number = ""
|
|
||||||
level_1_system_name = ""
|
|
||||||
level_2_system_name = ""
|
|
||||||
device_name = ""
|
|
||||||
tool_name = ""
|
|
||||||
tool_code = ""
|
|
||||||
|
|
||||||
if not parts:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
xian_number_code = parts[0].strip()
|
|
||||||
level1_code = ""
|
|
||||||
level2_code = ""
|
|
||||||
device_code = ""
|
|
||||||
|
|
||||||
if len(parts) > 1:
|
|
||||||
part = parts[1].strip()
|
|
||||||
# 防止索引越界
|
|
||||||
if len(part) < 2:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
level1_code = part[:2]
|
|
||||||
level2_code = part[2:4] if len(part) > 2 else ""
|
|
||||||
device_code = part[4:] if len(part) > 4 else ""
|
|
||||||
|
|
||||||
data = _load_tree_data()
|
|
||||||
root = _resolve_ship_root(data, xian_number_code)
|
|
||||||
if not root:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
xian_number = root.get("name", "")
|
|
||||||
|
|
||||||
# 一级
|
|
||||||
s1 = root.get("children", {}).get(level1_code)
|
|
||||||
if s1:
|
|
||||||
level_1_system_name = s1.get("name", "")
|
|
||||||
|
|
||||||
# 二级
|
|
||||||
s2 = s1.get("children", {}).get(level2_code)
|
|
||||||
if s2:
|
|
||||||
level_2_system_name = s2.get("name", "")
|
|
||||||
|
|
||||||
# 三级设备
|
|
||||||
device = s2.get("children", {}).get(device_code)
|
|
||||||
if device:
|
|
||||||
device_name = device.get("name", "")
|
|
||||||
# 若存在 level4,匹配与文件名最大相同字符串的零件
|
|
||||||
# 内部已包含逻辑:如果设备名匹配度更高,则不返回零件
|
|
||||||
tool_name, tool_code = _match_level4_tool(name, device)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"Xian_Number": xian_number,
|
|
||||||
"Xian_Number_code": xian_number_code,
|
|
||||||
"Level_1_System_Name": level_1_system_name,
|
|
||||||
"level1_code": level1_code,
|
|
||||||
"Level_2_System_Name": level_2_system_name,
|
|
||||||
"level2_code": level2_code,
|
|
||||||
"Device_Name": device_name,
|
|
||||||
"device_code": device_code,
|
|
||||||
"tool_name": tool_name,
|
|
||||||
"tool_code": tool_code,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_entity(filename):
|
|
||||||
result = process_filename(filename)
|
|
||||||
entities = []
|
|
||||||
relationships = []
|
|
||||||
low_level = ""
|
|
||||||
|
|
||||||
if result:
|
|
||||||
xian_number = result.get("Xian_Number")
|
|
||||||
xian_number_code = result.get("Xian_Number_code")
|
|
||||||
level_1_system_name = result.get("Level_1_System_Name")
|
|
||||||
level1_code = result.get("level1_code")
|
|
||||||
level_2_system_name = result.get("Level_2_System_Name")
|
|
||||||
level2_code = result.get("level2_code")
|
|
||||||
device_name = result.get("Device_Name")
|
|
||||||
device_code = result.get("device_code")
|
|
||||||
tool_name = result.get("tool_name")
|
|
||||||
tool_code = result.get("tool_code")
|
|
||||||
|
|
||||||
if xian_number:
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"type": "舰艇",
|
|
||||||
"properties": {
|
|
||||||
"名称": xian_number,
|
|
||||||
"舷号": xian_number_code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
low_level = xian_number
|
|
||||||
|
|
||||||
if level_1_system_name:
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"type": "系统",
|
|
||||||
"properties": {
|
|
||||||
"名称": level_1_system_name,
|
|
||||||
"系统编码": level1_code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
low_level = level_1_system_name
|
|
||||||
|
|
||||||
if xian_number and level_1_system_name:
|
|
||||||
relationships.append(
|
|
||||||
{
|
|
||||||
"type": "包含",
|
|
||||||
"from_entity": xian_number,
|
|
||||||
"to_entity": level_1_system_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if level_2_system_name:
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"type": "子系统",
|
|
||||||
"properties": {
|
|
||||||
"名称": level_2_system_name,
|
|
||||||
"子系统编码": level2_code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
low_level = level_2_system_name
|
|
||||||
|
|
||||||
if level_1_system_name and level_2_system_name:
|
|
||||||
relationships.append(
|
|
||||||
{
|
|
||||||
"type": "包含",
|
|
||||||
"from_entity": level_1_system_name,
|
|
||||||
"to_entity": level_2_system_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if device_name:
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"type": "设备",
|
|
||||||
"properties": {
|
|
||||||
"名称": device_name,
|
|
||||||
"设备编码": device_code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
low_level = device_name
|
|
||||||
|
|
||||||
if level_2_system_name and device_name:
|
|
||||||
relationships.append(
|
|
||||||
{
|
|
||||||
"type": "包含",
|
|
||||||
"from_entity": level_2_system_name,
|
|
||||||
"to_entity": device_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if tool_name:
|
|
||||||
entities.append(
|
|
||||||
{
|
|
||||||
"type": "零件",
|
|
||||||
"properties": {
|
|
||||||
"名称": tool_name,
|
|
||||||
"零件编码": tool_code,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
low_level = tool_name
|
|
||||||
|
|
||||||
if device_name and tool_name:
|
|
||||||
relationships.append(
|
|
||||||
{
|
|
||||||
"type": "包含",
|
|
||||||
"from_entity": device_name,
|
|
||||||
"to_entity": tool_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
final_result = {
|
|
||||||
"entities": entities,
|
|
||||||
"relationships": relationships,
|
|
||||||
"low_level": low_level,
|
|
||||||
}
|
|
||||||
|
|
||||||
return final_result
|
|
||||||
|
|
||||||
|
|
||||||
# 示例测试逻辑(非必须,仅供验证)
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# 阀控蓄电池脉冲式快速充电装置
|
|
||||||
# test_list = [
|
|
||||||
# "163-06A0015-B01001_发动机-维修手册.pdf",
|
|
||||||
|
|
||||||
# ]
|
|
||||||
# for i in test_list:
|
|
||||||
# result = get_entity(i)
|
|
||||||
# print(result)
|
|
||||||
from neo4j import GraphDatabase
|
|
||||||
|
|
||||||
NEO4J_URI = "bolt://192.168.0.111:7687"
|
|
||||||
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
|
|
||||||
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD","zdht123@") # 不设默认值,强制要求提供
|
|
||||||
NEO4J_DATABASE = os.getenv("NEO4J_DATABASE", "neo4j")
|
|
||||||
driver = GraphDatabase.driver(
|
|
||||||
NEO4J_URI,
|
|
||||||
auth=(NEO4J_USER, NEO4J_PASSWORD),
|
|
||||||
database=NEO4J_DATABASE
|
|
||||||
)
|
|
||||||
res = fetch_graph_sample(driver)
|
|
||||||
print(res)
|
|
||||||
|
|
||||||
|
|
||||||
@ -3,9 +3,9 @@ import json
|
|||||||
import base64
|
import base64
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, Tuple, Dict
|
from typing import Any, Optional, Tuple, Dict
|
||||||
import aiofiles # pip install aiofiles
|
import aiofiles # pip install aiofiles
|
||||||
|
from config import SEARCH_DIR,IMAGE_DIR
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ async def find_and_read_content_list(
|
|||||||
encoding: str = 'utf-8'
|
encoding: str = 'utf-8'
|
||||||
) -> Tuple[Optional[list], Optional[str]]:
|
) -> Tuple[Optional[list], Optional[str]]:
|
||||||
"""根据原始文件名查找并读取对应的 _content_list.json 文件。"""
|
"""根据原始文件名查找并读取对应的 _content_list.json 文件。"""
|
||||||
file_name, _ = os.path.splitext(original_filename)
|
file_name, _ = os.path.splitext(os.path.basename(original_filename))
|
||||||
target_filename = f"{file_name}_content_list.json"
|
target_filename = f"{file_name}_content_list.json"
|
||||||
logger.info(f"正在查找文件: {target_filename}")
|
logger.info(f"正在查找文件: {target_filename}")
|
||||||
|
|
||||||
@ -44,6 +44,93 @@ async def find_and_read_content_list(
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def find_image_record(content_list: Any, image_filename: str) -> Optional[dict]:
|
||||||
|
"""Find an image record by the basename stored in ``img_path``."""
|
||||||
|
if not isinstance(content_list, list) or not image_filename:
|
||||||
|
return None
|
||||||
|
|
||||||
|
target_name = os.path.basename(str(image_filename).strip().replace('\\', '/'))
|
||||||
|
if not target_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for item in content_list:
|
||||||
|
if not isinstance(item, dict) or item.get('type') != 'image':
|
||||||
|
continue
|
||||||
|
|
||||||
|
img_path = item.get('img_path')
|
||||||
|
if isinstance(img_path, str):
|
||||||
|
path_name = os.path.basename(img_path.strip().replace('\\', '/'))
|
||||||
|
if path_name == target_name:
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def find_image_record_by_pdf(
|
||||||
|
directory: str,
|
||||||
|
pdf_filename: str,
|
||||||
|
image_filename: str,
|
||||||
|
encoding: str = 'utf-8',
|
||||||
|
) -> Optional[dict]:
|
||||||
|
"""Find an image record from the content list matching a PDF filename."""
|
||||||
|
content_list, _ = await find_and_read_content_list(
|
||||||
|
directory, pdf_filename, encoding=encoding
|
||||||
|
)
|
||||||
|
return find_image_record(content_list, image_filename)
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_image_caption(
|
||||||
|
search_directory: str,
|
||||||
|
pdf_filename: str,
|
||||||
|
image_filename: str,
|
||||||
|
) -> list[str]:
|
||||||
|
"""?? PDF ??????????? image_caption?"""
|
||||||
|
image_record = await find_image_record_by_pdf(
|
||||||
|
directory=search_directory,
|
||||||
|
pdf_filename=pdf_filename,
|
||||||
|
image_filename=image_filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not image_record:
|
||||||
|
return []
|
||||||
|
|
||||||
|
image_caption = image_record.get("image_caption", [])
|
||||||
|
if isinstance(image_caption, str):
|
||||||
|
return [image_caption] if image_caption.strip() else []
|
||||||
|
if isinstance(image_caption, list):
|
||||||
|
return [str(item).strip() for item in image_caption if str(item).strip()]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def find_and_read_md_file(
|
||||||
|
directory: str,
|
||||||
|
original_filename: str,
|
||||||
|
encoding: str = 'utf-8'
|
||||||
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
"""Find and read the Markdown file matching the uploaded document basename."""
|
||||||
|
file_name, _ = os.path.splitext(original_filename)
|
||||||
|
target_filename = f"{file_name}.md"
|
||||||
|
logger.info(f"正在查找 Markdown 文件: {target_filename}")
|
||||||
|
|
||||||
|
def _walk_for_file():
|
||||||
|
for root, _, files in os.walk(directory):
|
||||||
|
if target_filename in files:
|
||||||
|
return os.path.join(root, target_filename)
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_path = await asyncio.to_thread(_walk_for_file)
|
||||||
|
if file_path is None:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiofiles.open(file_path, 'r', encoding=encoding, errors='ignore') as f:
|
||||||
|
return await f.read(), file_path
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"读取 Markdown 文件 {file_path} 时发生错误: {e}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def extract_all_jpg_filenames(doc_data: list) -> set:
|
def extract_all_jpg_filenames(doc_data: list) -> set:
|
||||||
"""从文档数据中提取所有 .jpg 图片的纯文件名。纯 CPU 操作,不需要 async。"""
|
"""从文档数据中提取所有 .jpg 图片的纯文件名。纯 CPU 操作,不需要 async。"""
|
||||||
result_set = set()
|
result_set = set()
|
||||||
@ -103,10 +190,9 @@ async def encode_images_to_base64(
|
|||||||
|
|
||||||
async def process_document(
|
async def process_document(
|
||||||
input_file_name: str,
|
input_file_name: str,
|
||||||
search_dir: Optional[str] = "/app/mineru_output",
|
|
||||||
local_image_dir: Optional[str] = None,
|
local_image_dir: Optional[str] = None,
|
||||||
concurrency: int = 32,
|
concurrency: int = 32,
|
||||||
) -> Optional[Tuple[list, Dict[str, str]]]:
|
) -> Optional[Tuple[list, Dict[str, str], Optional[str]]]:
|
||||||
"""
|
"""
|
||||||
异步处理文档:查找 content_list.json,提取图片引用,并发编码为 base64。
|
异步处理文档:查找 content_list.json,提取图片引用,并发编码为 base64。
|
||||||
|
|
||||||
@ -117,9 +203,10 @@ async def process_document(
|
|||||||
concurrency: 图片编码的并发数上限,默认 32
|
concurrency: 图片编码的并发数上限,默认 32
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
成功: (content_list, images_dict)
|
成功: (content_list, images_dict, md_content)
|
||||||
失败: None
|
失败: None
|
||||||
"""
|
"""
|
||||||
|
search_dir = SEARCH_DIR
|
||||||
content, content_path = await find_and_read_content_list(search_dir, input_file_name)
|
content, content_path = await find_and_read_content_list(search_dir, input_file_name)
|
||||||
if content is None:
|
if content is None:
|
||||||
logger.error("❌ 未找到指定的 _content_list.json 文件。")
|
logger.error("❌ 未找到指定的 _content_list.json 文件。")
|
||||||
@ -128,6 +215,13 @@ async def process_document(
|
|||||||
logger.info(f"✅ 找到文件: {content_path}")
|
logger.info(f"✅ 找到文件: {content_path}")
|
||||||
logger.info(f"内容类型: {type(content).__name__}, 长度: {len(str(content))}")
|
logger.info(f"内容类型: {type(content).__name__}, 长度: {len(str(content))}")
|
||||||
|
|
||||||
|
md_content, md_path = await find_and_read_md_file(search_dir, input_file_name)
|
||||||
|
if md_content is None:
|
||||||
|
logger.info("未找到对应的 Markdown 文件。")
|
||||||
|
else:
|
||||||
|
logger.info(f"找到 Markdown 文件: {md_path}")
|
||||||
|
logger.info(f"Markdown 内容长度: {len(md_content)}")
|
||||||
|
|
||||||
if local_image_dir is None:
|
if local_image_dir is None:
|
||||||
local_image_dir = os.path.join(os.path.dirname(content_path), "images")
|
local_image_dir = os.path.join(os.path.dirname(content_path), "images")
|
||||||
logger.info(f"图片目录: {local_image_dir}")
|
logger.info(f"图片目录: {local_image_dir}")
|
||||||
@ -140,20 +234,19 @@ async def process_document(
|
|||||||
)
|
)
|
||||||
logger.info(f"成功编码 {len(images_dict)} 张图片")
|
logger.info(f"成功编码 {len(images_dict)} 张图片")
|
||||||
|
|
||||||
return content, images_dict
|
return content, images_dict, md_content
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
result = await process_document(
|
result = await process_document(
|
||||||
input_file_name="163-06A0014-B01001_发动机-维修手册.pdf",
|
input_file_name="163-06A0014-B01001_发动机-维修手册.pdf",
|
||||||
search_dir="/app/mineru_output",
|
|
||||||
concurrency=32,
|
concurrency=32,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
content, images_dict = result
|
content, images_dict, md_content = result
|
||||||
print(f"\n=== 处理完成 ===")
|
print(f"\n=== 处理完成 ===")
|
||||||
print(f"文档段落数: {len(content)}")
|
print(f"文档段落数: {len(content)}")
|
||||||
print(f"图片数量: {len(images_dict)}")
|
print(f"图片数量: {len(images_dict)}")
|
||||||
@ -164,5 +257,12 @@ async def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
exit_code = asyncio.run(main())
|
result = asyncio.run(
|
||||||
exit(exit_code)
|
extract_image_caption(
|
||||||
|
r"E:\ZKYNLP\Hjunproject\project0506\kgrag",
|
||||||
|
"163-06A0014-B01001_发动机-维修手册.pdf",
|
||||||
|
"23fa41fea5e5b5458d6f7df3f8e7e8033b6665bd23203946646651c3e56c2da7.jpg",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
|||||||
142
generate_summary.py
Normal file
142
generate_summary.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
"""
|
||||||
|
文件概述生成工具
|
||||||
|
把 wiki_engine 的文档概述能力独立出来
|
||||||
|
输入:markdown 格式的文件内容
|
||||||
|
输出:文件概述(SUMMARY 行 + markdown 正文)
|
||||||
|
支持并发
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from typing import List, Optional
|
||||||
|
from config import LLM_CONFIG
|
||||||
|
from openai import AsyncOpenAI
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 配置 ====================
|
||||||
|
_MAX_CONCURRENCY = 8 # 最大并发数
|
||||||
|
|
||||||
|
_client: Optional[AsyncOpenAI] = None
|
||||||
|
_semaphore: Optional[asyncio.Semaphore] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client() -> AsyncOpenAI:
|
||||||
|
"""获取或创建 AsyncOpenAI 单例客户端"""
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
_client = AsyncOpenAI(api_key=LLM_CONFIG['api_key'], base_url=LLM_CONFIG['base_url'])
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
def _get_semaphore() -> asyncio.Semaphore:
|
||||||
|
"""获取或创建并发信号量"""
|
||||||
|
global _semaphore
|
||||||
|
if _semaphore is None:
|
||||||
|
_semaphore = asyncio.Semaphore(_MAX_CONCURRENCY)
|
||||||
|
return _semaphore
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== Prompt(与 wiki_builder.py 的 WIKI_SUMMARY_PROMPT 一致,仅去掉 extracted_slugs 相关部分) ====================
|
||||||
|
SUMMARY_PROMPT = """You are a wiki editor. Given the following document content, create a structured wiki summary page in Markdown format.
|
||||||
|
|
||||||
|
<document>
|
||||||
|
<content>
|
||||||
|
{content}
|
||||||
|
</content>
|
||||||
|
</document>
|
||||||
|
|
||||||
|
<instructions>
|
||||||
|
1. The FIRST line of your output MUST be: SUMMARY: {{one sentence, 15-40 words, describing what this document is about for wiki index listing}}
|
||||||
|
2. Create a concise but useful document-level summary.
|
||||||
|
3. Include the document's main subject, scope, important procedures, standards, systems, equipment, tables, fields.
|
||||||
|
4. Do NOT invent facts. Stay grounded in the document content.
|
||||||
|
5. Write in Chinese.
|
||||||
|
6. If the content is empty or has no substantive information, output exactly: "SUMMARY: No textual content was extractable from this document." followed by a brief note.
|
||||||
|
</instructions>
|
||||||
|
|
||||||
|
Output the SUMMARY line first, then the Markdown content. Do not include any other preamble."""
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_summary(content: str) -> str:
|
||||||
|
"""
|
||||||
|
从 markdown 内容生成文件概述
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: markdown 格式的文件内容
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
文件概述(SUMMARY 行 + markdown 正文)
|
||||||
|
"""
|
||||||
|
if not content or not content.strip():
|
||||||
|
return ""
|
||||||
|
|
||||||
|
prompt = SUMMARY_PROMPT.format(content=content)
|
||||||
|
client = _get_client()
|
||||||
|
|
||||||
|
async with _get_semaphore():
|
||||||
|
try:
|
||||||
|
response = await client.chat.completions.create(
|
||||||
|
model=LLM_CONFIG['model'],
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "You are a grounded wiki editor. Do not invent facts."},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
temperature=0.1,
|
||||||
|
max_tokens=LLM_CONFIG['max_tokens'],
|
||||||
|
stream=False,
|
||||||
|
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
|
||||||
|
)
|
||||||
|
return (response.choices[0].message.content or "").strip()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[generate_summary] 概述生成失败: {exc}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_summaries(contents: List[str]) -> List[str]:
|
||||||
|
"""
|
||||||
|
并发生成多个文件的概述
|
||||||
|
|
||||||
|
Args:
|
||||||
|
contents: markdown 格式的文件内容列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
文件概述列表,顺序与输入一致
|
||||||
|
"""
|
||||||
|
return await asyncio.gather(*(generate_summary(c) for c in contents))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# if len(sys.argv) > 1:
|
||||||
|
# with open(sys.argv[1], "r", encoding="utf-8") as f:
|
||||||
|
# test_content = f.read()
|
||||||
|
# else:
|
||||||
|
# test_content = """
|
||||||
|
# # 船舶动力系统维护规程
|
||||||
|
|
||||||
|
# ## 概述
|
||||||
|
# 本文档详细介绍了船舶动力系统的日常维护和故障处理流程。
|
||||||
|
|
||||||
|
# ## 发动机日常检查
|
||||||
|
# - 润滑油位检查:每日检查发动机润滑油位,保持在标尺正常范围
|
||||||
|
# - 冷却液位检查:确保冷却系统液位正常,无泄漏
|
||||||
|
# - 皮带张紧度:检查传动皮带张紧度,过松或过紧均需调整
|
||||||
|
|
||||||
|
# ## 冷却系统维护
|
||||||
|
# 定期清洗热交换器,检查水泵密封性,更换老化管路。
|
||||||
|
|
||||||
|
# ## 故障诊断流程
|
||||||
|
# 1. 现象观察:记录故障现象和发生条件
|
||||||
|
# 2. 数据采集:收集运行参数和报警信息
|
||||||
|
# 3. 原因分析:对照标准参数分析故障原因
|
||||||
|
# 4. 处理方案:制定维修方案并执行
|
||||||
|
|
||||||
|
# ## 安全注意事项
|
||||||
|
# 所有维护操作必须在停机状态下进行,操作人员需佩戴防护装备。
|
||||||
|
# """
|
||||||
|
md_path = Path(r"E:\ZKYNLP\Hjunproject\project0506\kgrag\船舶主机燃油泵自动控制系统故障树分析.md")
|
||||||
|
with open(md_path, "r", encoding="utf-8") as f:
|
||||||
|
test_content = f.read()
|
||||||
|
|
||||||
|
result = asyncio.run(generate_summary(test_content))
|
||||||
|
print(result)
|
||||||
@ -5,9 +5,13 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Any, Optional
|
import re
|
||||||
|
from typing import Dict, List, Any, Optional, Set
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
EXCLUDED_RESULT_NODE_LABELS = {"维修工作", "操作程序", "操作使用"}
|
||||||
|
_FILTERED_OUT = object()
|
||||||
|
|
||||||
|
|
||||||
class SimpleNode:
|
class SimpleNode:
|
||||||
"""
|
"""
|
||||||
@ -537,6 +541,110 @@ def format_entry_nodes_as_results(entry_nodes: dict) -> List[Dict[str, Any]]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_label_values(labels: Any) -> Set[str]:
|
||||||
|
"""将 labels/标签/type 等字段统一成标签集合。"""
|
||||||
|
if labels is None:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
if isinstance(labels, str):
|
||||||
|
parts = re.split(r"[,,、/\s]+", labels)
|
||||||
|
return {part.strip("[]'\" ") for part in parts if part.strip("[]'\" ")}
|
||||||
|
|
||||||
|
if isinstance(labels, (list, tuple, set, frozenset)):
|
||||||
|
return {str(label).strip() for label in labels if str(label).strip()}
|
||||||
|
|
||||||
|
return {str(labels).strip()} if str(labels).strip() else set()
|
||||||
|
|
||||||
|
|
||||||
|
def _dict_label_values(data: Dict[str, Any]) -> Set[str]:
|
||||||
|
label_keys = ("labels", "标签", "label", "类型", "type", "节点类型", "node_type")
|
||||||
|
labels = set()
|
||||||
|
for key in label_keys:
|
||||||
|
if key in data:
|
||||||
|
labels.update(_normalize_label_values(data.get(key)))
|
||||||
|
return labels
|
||||||
|
|
||||||
|
|
||||||
|
def _has_excluded_result_label(value: Any) -> bool:
|
||||||
|
"""判断一个结果对象是否明确属于需要从 results 隐藏的节点类型。"""
|
||||||
|
labels = set()
|
||||||
|
|
||||||
|
if isinstance(value, dict):
|
||||||
|
labels.update(_dict_label_values(value))
|
||||||
|
elif hasattr(value, "labels") and (hasattr(value, "element_id") or hasattr(value, "id")):
|
||||||
|
try:
|
||||||
|
labels.update(_normalize_label_values(value.labels))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return bool(labels & EXCLUDED_RESULT_NODE_LABELS)
|
||||||
|
|
||||||
|
|
||||||
|
def _mentions_excluded_result_label(text: Any) -> bool:
|
||||||
|
if text is None:
|
||||||
|
return False
|
||||||
|
return any(label in str(text) for label in EXCLUDED_RESULT_NODE_LABELS)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_empty_result_content(value: Any) -> bool:
|
||||||
|
return value in (None, {}, [])
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_excluded_result_content(value: Any, parent_key: Any = None) -> Any:
|
||||||
|
"""
|
||||||
|
只用于 results 字段:过滤掉维修工作、操作程序、操作使用这类节点内容。
|
||||||
|
nodes/links 的原始返回不会经过这个函数。
|
||||||
|
"""
|
||||||
|
if parent_key is not None and _mentions_excluded_result_label(parent_key):
|
||||||
|
return _FILTERED_OUT
|
||||||
|
|
||||||
|
if _has_excluded_result_label(value):
|
||||||
|
return _FILTERED_OUT
|
||||||
|
|
||||||
|
if isinstance(value, dict):
|
||||||
|
name_value = value.get("名称", value.get("name"))
|
||||||
|
if _mentions_excluded_result_label(name_value):
|
||||||
|
return _FILTERED_OUT
|
||||||
|
|
||||||
|
filtered = {}
|
||||||
|
for key, child in value.items():
|
||||||
|
if _mentions_excluded_result_label(key):
|
||||||
|
continue
|
||||||
|
filtered_child = _filter_excluded_result_content(child, key)
|
||||||
|
if filtered_child is not _FILTERED_OUT:
|
||||||
|
filtered[key] = filtered_child
|
||||||
|
|
||||||
|
return _FILTERED_OUT if _is_empty_result_content(filtered) else filtered
|
||||||
|
|
||||||
|
if isinstance(value, list):
|
||||||
|
filtered_items = []
|
||||||
|
for item in value:
|
||||||
|
filtered_item = _filter_excluded_result_content(item)
|
||||||
|
if filtered_item is not _FILTERED_OUT and not _is_empty_result_content(filtered_item):
|
||||||
|
filtered_items.append(filtered_item)
|
||||||
|
return filtered_items
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_results_for_output(results: Any) -> List[Any]:
|
||||||
|
filtered = _filter_excluded_result_content(results)
|
||||||
|
if filtered is _FILTERED_OUT or _is_empty_result_content(filtered):
|
||||||
|
return []
|
||||||
|
if isinstance(filtered, list):
|
||||||
|
return filtered
|
||||||
|
return [filtered]
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_result_nodes_for_output(nodes: Any) -> List[Dict[str, Any]]:
|
||||||
|
if not isinstance(nodes, list):
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
node for node in nodes
|
||||||
|
if isinstance(node, dict) and not _has_excluded_result_label(node)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _format_value(value: Any) -> str:
|
def _format_value(value: Any) -> str:
|
||||||
"""
|
"""
|
||||||
@ -649,8 +757,9 @@ def serialize_graph_for_llm(graph_response: dict) -> str:
|
|||||||
- results(任意查询返回值:聚合/记录/字符串等)
|
- results(任意查询返回值:聚合/记录/字符串等)
|
||||||
"""
|
"""
|
||||||
data = graph_response.get("data", {})
|
data = graph_response.get("data", {})
|
||||||
nodes = data.get("nodes", [])
|
# 这里的过滤只影响 results 字段里的文本展示,不改动接口返回的 data.nodes/data.links。
|
||||||
results = data.get("results", [])
|
nodes = _filter_result_nodes_for_output(data.get("nodes", []))
|
||||||
|
results = _filter_results_for_output(data.get("results", []))
|
||||||
|
|
||||||
output_lines = []
|
output_lines = []
|
||||||
|
|
||||||
@ -695,3 +804,4 @@ def serialize_graph_for_llm(graph_response: dict) -> str:
|
|||||||
return "图谱查询完成,但未返回有效数据。"
|
return "图谱查询完成,但未返回有效数据。"
|
||||||
|
|
||||||
return "\n".join(output_lines).rstrip()
|
return "\n".join(output_lines).rstrip()
|
||||||
|
|
||||||
|
|||||||
697
graph_search/result_formatter.py_0713
Normal file
697
graph_search/result_formatter.py_0713
Normal file
@ -0,0 +1,697 @@
|
|||||||
|
"""
|
||||||
|
结果格式化模块
|
||||||
|
|
||||||
|
功能:从 Neo4j 查询结果中提取节点和路径信息
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleNode:
|
||||||
|
"""
|
||||||
|
简单的Node模拟类,用于将节点数据包装为Neo4j Node对象格式(降级策略)
|
||||||
|
"""
|
||||||
|
def __init__(self, node_data: dict, labels: List[str], node_id: str):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
node_data: 节点数据字典
|
||||||
|
labels: 节点标签列表
|
||||||
|
node_id: 节点ID
|
||||||
|
"""
|
||||||
|
self._data = node_data
|
||||||
|
self._labels = labels
|
||||||
|
self._node_id = node_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def element_id(self):
|
||||||
|
"""返回节点ID"""
|
||||||
|
return self._node_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
"""返回节点ID(兼容旧版本)"""
|
||||||
|
return self._node_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def labels(self):
|
||||||
|
"""返回节点标签"""
|
||||||
|
return self._labels
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
"""获取节点属性"""
|
||||||
|
return self._data.get(key, default)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""使节点可以转换为字典"""
|
||||||
|
return iter(self._data.items())
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
"""使节点可以像字典一样访问"""
|
||||||
|
return self._data[key]
|
||||||
|
|
||||||
|
|
||||||
|
class SimplePath:
|
||||||
|
"""
|
||||||
|
简单的Path模拟类,用于将节点包装为path结构(降级策略)
|
||||||
|
包含nodes和relationships属性,其中relationships为空列表
|
||||||
|
"""
|
||||||
|
def __init__(self, nodes: List):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
nodes: 节点列表(SimpleNode对象列表)
|
||||||
|
"""
|
||||||
|
self.nodes = nodes
|
||||||
|
self.relationships = [] # 降级策略中,只有节点,没有关系
|
||||||
|
|
||||||
|
|
||||||
|
# def extract_knowledge_source(knowledge_source_value: Any) -> List[Dict[str, str]]:
|
||||||
|
# """
|
||||||
|
# 从节点属性中的 knowledge_source 字段提取信息并格式化为标准格式
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# knowledge_source_value: 节点属性中的 knowledge_source 值(可能是字符串、字典、列表等)
|
||||||
|
#
|
||||||
|
# Returns:
|
||||||
|
# List[Dict]: 格式化后的 knowledge_source 列表,格式为 [{"filename":"","url":"","info":""}]
|
||||||
|
# """
|
||||||
|
# if knowledge_source_value is None:
|
||||||
|
# return [{"filename": "", "url": "", "info": ""}]
|
||||||
|
#
|
||||||
|
# result = []
|
||||||
|
#
|
||||||
|
# # 如果是字符串,尝试解析为JSON或直接使用
|
||||||
|
# if isinstance(knowledge_source_value, str):
|
||||||
|
# # 尝试解析JSON字符串
|
||||||
|
# try:
|
||||||
|
# import json
|
||||||
|
# parsed = json.loads(knowledge_source_value)
|
||||||
|
# if isinstance(parsed, list):
|
||||||
|
# knowledge_source_value = parsed
|
||||||
|
# elif isinstance(parsed, dict):
|
||||||
|
# knowledge_source_value = [parsed]
|
||||||
|
# else:
|
||||||
|
# # 如果不是JSON,将字符串作为info
|
||||||
|
# return [{"filename": "", "url": "", "info": knowledge_source_value}]
|
||||||
|
# except:
|
||||||
|
# # 解析失败,将字符串作为info
|
||||||
|
# return [{"filename": "", "url": "", "info": knowledge_source_value}]
|
||||||
|
#
|
||||||
|
# # 如果是字典,转换为列表
|
||||||
|
# if isinstance(knowledge_source_value, dict):
|
||||||
|
# knowledge_source_value = [knowledge_source_value]
|
||||||
|
#
|
||||||
|
# # 如果是列表,处理每个元素
|
||||||
|
# if isinstance(knowledge_source_value, list):
|
||||||
|
# for item in knowledge_source_value:
|
||||||
|
# if isinstance(item, dict):
|
||||||
|
# # 从字典中提取字段
|
||||||
|
# formatted_item = {
|
||||||
|
# "filename": str(item.get("filename", item.get("file_name", item.get("file", "")))),
|
||||||
|
# "url": str(item.get("url", item.get("link", item.get("uri", "")))),
|
||||||
|
# "info": str(item.get("info", item.get("information", item.get("description", ""))))
|
||||||
|
# }
|
||||||
|
# result.append(formatted_item)
|
||||||
|
# elif isinstance(item, str):
|
||||||
|
# # 如果是字符串,作为info
|
||||||
|
# result.append({"filename": "", "url": "", "info": item})
|
||||||
|
# else:
|
||||||
|
# # 其他类型,转换为字符串作为info
|
||||||
|
# result.append({"filename": "", "url": "", "info": str(item)})
|
||||||
|
# else:
|
||||||
|
# # 其他类型,转换为字符串作为info
|
||||||
|
# result.append({"filename": "", "url": "", "info": str(knowledge_source_value)})
|
||||||
|
#
|
||||||
|
# # 如果没有结果,返回默认值
|
||||||
|
# if not result:
|
||||||
|
# result = [{"filename": "", "url": "", "info": ""}]
|
||||||
|
#
|
||||||
|
# return result
|
||||||
|
def extract_knowledge_source(knowledge_source_value: Any) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
从节点属性中的 knowledge_source 字段提取信息并格式化为标准格式(不含 info 字段)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
knowledge_source_value: 节点属性中的 knowledge_source 值(可能是字符串、字典、列表等)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict]: 格式化后的 knowledge_source 列表,格式为 [{"filename":"","url":""}]
|
||||||
|
"""
|
||||||
|
if knowledge_source_value is None:
|
||||||
|
return [{"filename": "", "url": ""}]
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
# 如果是字符串,尝试解析为JSON或直接使用
|
||||||
|
if isinstance(knowledge_source_value, str):
|
||||||
|
# 尝试解析JSON字符串
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
parsed = json.loads(knowledge_source_value)
|
||||||
|
if isinstance(parsed, list):
|
||||||
|
knowledge_source_value = parsed
|
||||||
|
elif isinstance(parsed, dict):
|
||||||
|
knowledge_source_value = [parsed]
|
||||||
|
else:
|
||||||
|
# 如果不是JSON,跳过该字符串
|
||||||
|
return [{"filename": "", "url": ""}]
|
||||||
|
except:
|
||||||
|
# 解析失败,跳过该字符串
|
||||||
|
return [{"filename": "", "url": ""}]
|
||||||
|
|
||||||
|
# 如果是字典,转换为列表
|
||||||
|
if isinstance(knowledge_source_value, dict):
|
||||||
|
knowledge_source_value = [knowledge_source_value]
|
||||||
|
|
||||||
|
# 如果是列表,处理每个元素
|
||||||
|
if isinstance(knowledge_source_value, list):
|
||||||
|
for item in knowledge_source_value:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
# 从字典中提取字段(不含 info)
|
||||||
|
formatted_item = {
|
||||||
|
"filename": str(item.get("filename", item.get("file_name", item.get("file", "")))),
|
||||||
|
"url": str(item.get("url", item.get("link", item.get("uri", ""))))
|
||||||
|
}
|
||||||
|
result.append(formatted_item)
|
||||||
|
elif isinstance(item, str):
|
||||||
|
# 如果是字符串,跳过
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# 其他类型,跳过
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# 其他类型,跳过
|
||||||
|
return [{"filename": "", "url": ""}]
|
||||||
|
|
||||||
|
# 如果没有结果,返回默认值
|
||||||
|
if not result:
|
||||||
|
result = [{"filename": "", "url": ""}]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def remove_sensitive_fields(data: Any) -> Any:
|
||||||
|
"""
|
||||||
|
递归删除数据中的 embedding、fulltext 和 path 字段
|
||||||
|
参考 test.py 的实现方式:在转换为 dict 后立即 pop 掉敏感字段
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: 要处理的数据(可以是字典、列表、Neo4j对象或基本类型)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
清理后的数据
|
||||||
|
"""
|
||||||
|
# 如果是 Neo4j Node 对象,转换为字典并删除敏感字段(参考 test.py)
|
||||||
|
if hasattr(data, 'labels') and (hasattr(data, 'element_id') or hasattr(data, 'id')):
|
||||||
|
try:
|
||||||
|
props = dict(data)
|
||||||
|
props.pop('embedding', None)
|
||||||
|
props.pop('fulltext', None)
|
||||||
|
# 递归处理属性中的嵌套结构
|
||||||
|
return remove_sensitive_fields(props)
|
||||||
|
except:
|
||||||
|
return data
|
||||||
|
|
||||||
|
# 如果是 Neo4j Relationship 对象,转换为字典并删除敏感字段
|
||||||
|
if hasattr(data, 'type') and (hasattr(data, 'element_id') or hasattr(data, 'id')):
|
||||||
|
try:
|
||||||
|
props = dict(data)
|
||||||
|
props.pop('embedding', None)
|
||||||
|
props.pop('fulltext', None)
|
||||||
|
# 递归处理属性中的嵌套结构
|
||||||
|
return remove_sensitive_fields(props)
|
||||||
|
except:
|
||||||
|
return data
|
||||||
|
|
||||||
|
if isinstance(data, dict):
|
||||||
|
# 创建新字典,排除 embedding、fulltext 和 path
|
||||||
|
result = {}
|
||||||
|
for key, value in data.items():
|
||||||
|
if key not in ['embedding', 'fulltext', 'path']:
|
||||||
|
# 递归处理嵌套的字典、列表和 Neo4j 对象
|
||||||
|
result[key] = remove_sensitive_fields(value)
|
||||||
|
return result
|
||||||
|
elif isinstance(data, list):
|
||||||
|
# 递归处理列表中的每个元素
|
||||||
|
return [remove_sensitive_fields(item) for item in data]
|
||||||
|
else:
|
||||||
|
# 基本类型直接返回
|
||||||
|
return data
|
||||||
|
|
||||||
|
def format_results(results: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
格式化图谱查询结果,提取所有节点和路径(自动去重)
|
||||||
|
|
||||||
|
从 rerank 后的结果字典列表中提取 path 信息,生成 nodes 和 links
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results: rerank 后的结果列表(字典列表,每个字典包含 rerank_score 和 path 等信息)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 包含 nodes 和 links 的字典(已去重,只保留最高分)
|
||||||
|
"""
|
||||||
|
if not results:
|
||||||
|
return {"nodes": [], "links": []}
|
||||||
|
|
||||||
|
all_nodes = {} # key: node_id(str), value: node dict
|
||||||
|
all_links = {} # key: rel_id(str), value: link dict
|
||||||
|
node_scores = {} # key: node_id(str), value: highest rerank score for the node
|
||||||
|
|
||||||
|
def process_path(path, current_score):
|
||||||
|
"""处理单个 Path 对象"""
|
||||||
|
if path is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def process_node(node):
|
||||||
|
"""处理单个节点的辅助函数"""
|
||||||
|
if node is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
node_id = str(node.element_id) if hasattr(node, 'element_id') else str(node.id)
|
||||||
|
|
||||||
|
if node_id not in all_nodes:
|
||||||
|
props = dict(node)
|
||||||
|
props.pop('embedding', None)
|
||||||
|
props.pop('fulltext', None)
|
||||||
|
props.pop('切片', None)
|
||||||
|
|
||||||
|
# 提取 name 用于外部字段(从props中提取后移除,避免出现在properties中)
|
||||||
|
node_name = props.pop('name', None) or node.get("名称", f"Node_{node_id}")
|
||||||
|
|
||||||
|
# 直接过滤掉 last_updated 和 created_at
|
||||||
|
props.pop('last_updated', None)
|
||||||
|
props.pop('created_at', None)
|
||||||
|
|
||||||
|
# 提取并格式化 knowledge_source(从props中提取后移除原始值)
|
||||||
|
knowledge_source_value = props.pop('knowledge_source', None)
|
||||||
|
formatted_knowledge_source = extract_knowledge_source(knowledge_source_value)
|
||||||
|
|
||||||
|
all_nodes[node_id] = {
|
||||||
|
"id": node_id,
|
||||||
|
"name": node_name,
|
||||||
|
"labels": list(node.labels),
|
||||||
|
"properties": props,
|
||||||
|
"knowledge_source": formatted_knowledge_source
|
||||||
|
}
|
||||||
|
# 初始化节点的最高分数
|
||||||
|
if current_score > 0:
|
||||||
|
node_scores[node_id] = current_score
|
||||||
|
else:
|
||||||
|
# 如果节点已存在,比较并保留最高分数
|
||||||
|
existing_score = node_scores.get(node_id, 0)
|
||||||
|
if current_score > existing_score:
|
||||||
|
node_scores[node_id] = current_score
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 处理节点:优先使用 path.nodes,如果为空则使用 start/end 或 start_node/end_node
|
||||||
|
nodes_processed = False
|
||||||
|
|
||||||
|
# 首先尝试使用 path.nodes
|
||||||
|
if hasattr(path, 'nodes'):
|
||||||
|
try:
|
||||||
|
# 直接迭代 path.nodes(可能是生成器)
|
||||||
|
for node in path.nodes:
|
||||||
|
process_node(node)
|
||||||
|
nodes_processed = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"迭代 path.nodes 时出错: {e}")
|
||||||
|
|
||||||
|
# 如果 path.nodes 没有处理任何节点(size=0的情况),尝试使用 start/end 节点
|
||||||
|
if not nodes_processed:
|
||||||
|
start_node = None
|
||||||
|
end_node = None
|
||||||
|
|
||||||
|
# 尝试多种方式获取起始节点(优先使用 start_node,其次使用 start)
|
||||||
|
if hasattr(path, 'start_node'):
|
||||||
|
try:
|
||||||
|
start_node = path.start_node
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif hasattr(path, 'start'):
|
||||||
|
try:
|
||||||
|
start_node = path.start
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 尝试多种方式获取结束节点(优先使用 end_node,其次使用 end)
|
||||||
|
if hasattr(path, 'end_node'):
|
||||||
|
try:
|
||||||
|
end_node = path.end_node
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif hasattr(path, 'end'):
|
||||||
|
try:
|
||||||
|
end_node = path.end
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 处理起始节点
|
||||||
|
if start_node is not None:
|
||||||
|
process_node(start_node)
|
||||||
|
|
||||||
|
# 处理结束节点(只有当与起始节点不同时才处理,避免重复)
|
||||||
|
if end_node is not None and (start_node is None or end_node != start_node):
|
||||||
|
process_node(end_node)
|
||||||
|
|
||||||
|
# 处理关系
|
||||||
|
for rel in path.relationships:
|
||||||
|
rel_id = str(rel.element_id) if hasattr(rel, 'element_id') else str(rel.id)
|
||||||
|
|
||||||
|
if rel_id not in all_links:
|
||||||
|
rel_props = dict(rel)
|
||||||
|
rel_props.pop('embedding', None)
|
||||||
|
rel_props.pop('fulltext', None)
|
||||||
|
|
||||||
|
start_node_id = str(rel.start_node.element_id) if hasattr(rel.start_node, 'element_id') else str(rel.start_node.id)
|
||||||
|
end_node_id = str(rel.end_node.element_id) if hasattr(rel.end_node, 'element_id') else str(rel.end_node.id)
|
||||||
|
|
||||||
|
all_links[rel_id] = {
|
||||||
|
"id": rel_id,
|
||||||
|
"label": rel.type,
|
||||||
|
"source": start_node_id,
|
||||||
|
"target": end_node_id,
|
||||||
|
"properties": rel_props
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"处理 Path 对象时出错: {e}", exc_info=True)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
try:
|
||||||
|
# 获取当前记录的rerank_score
|
||||||
|
current_rerank_score = result.get('rerank_score', 0)
|
||||||
|
|
||||||
|
# 查找 path 字段(可能是 path 或 paths)
|
||||||
|
path_value = None
|
||||||
|
if 'path' in result:
|
||||||
|
path_value = result['path']
|
||||||
|
elif 'paths' in result:
|
||||||
|
path_value = result['paths']
|
||||||
|
else:
|
||||||
|
# 尝试从所有值中查找 Path 对象
|
||||||
|
for key, value in result.items():
|
||||||
|
if value is not None and hasattr(value, 'nodes') and hasattr(value, 'relationships'):
|
||||||
|
path_value = value
|
||||||
|
break
|
||||||
|
|
||||||
|
# 处理 path_value
|
||||||
|
if path_value is not None:
|
||||||
|
# 处理单个 Path 对象
|
||||||
|
if hasattr(path_value, 'nodes') and hasattr(path_value, 'relationships'):
|
||||||
|
process_path(path_value, current_rerank_score)
|
||||||
|
# 处理路径列表
|
||||||
|
elif isinstance(path_value, list):
|
||||||
|
if path_value: # 列表不为空
|
||||||
|
for path in path_value:
|
||||||
|
if path is not None and hasattr(path, 'nodes') and hasattr(path, 'relationships'):
|
||||||
|
process_path(path, current_rerank_score)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"格式化结果记录失败: {e}", exc_info=True)
|
||||||
|
|
||||||
|
# 为每个节点添加最高rerank分数到其属性中(只保留高分)
|
||||||
|
for node_id, score in node_scores.items():
|
||||||
|
if node_id in all_nodes:
|
||||||
|
node = all_nodes[node_id]
|
||||||
|
if 'properties' not in node:
|
||||||
|
node['properties'] = {}
|
||||||
|
# 只保留最高分,使用 分数 作为 key
|
||||||
|
node['properties']['分数'] = score
|
||||||
|
|
||||||
|
# 最终清理:确保所有嵌套数据中的敏感字段都被删除
|
||||||
|
cleaned_nodes = [remove_sensitive_fields(node) for node in all_nodes.values()]
|
||||||
|
cleaned_links = [remove_sensitive_fields(link) for link in all_links.values()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"nodes": cleaned_nodes,
|
||||||
|
"links": cleaned_links
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_entry_nodes_as_results(entry_nodes: dict) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
将入口节点信息格式化为检索结果(降级策略)
|
||||||
|
将节点作为path,节点作为结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entry_nodes: 入口节点字典,格式如 {"设备": [{"name": "xxx", "score": 0.9, "node": {...}}]}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict]: 结果列表,每个元素是一个结果记录,包含path字段(用于 graph_retrieval.py 的降级策略)
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
if not entry_nodes:
|
||||||
|
return results
|
||||||
|
|
||||||
|
for label, nodes in entry_nodes.items():
|
||||||
|
if not isinstance(nodes, list) or not nodes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 标签列表(优先使用entry_nodes中的label,如果节点数据中有labels则使用节点数据的labels)
|
||||||
|
label_list = [label] if label else []
|
||||||
|
|
||||||
|
for node_info in nodes:
|
||||||
|
node_name = node_info.get("name") or node_info.get("_name") or (node_info.get("node", {}).get("名称")) or str(node_info.get("node", ""))
|
||||||
|
node_data = node_info.get("node", {})
|
||||||
|
node_score = node_info.get("score", 0.8)
|
||||||
|
|
||||||
|
# 如果node_data是Neo4j Node对象,转换为字典
|
||||||
|
if hasattr(node_data, 'labels') and (hasattr(node_data, 'element_id') or hasattr(node_data, 'id')):
|
||||||
|
# 从Neo4j Node对象提取labels
|
||||||
|
node_labels = list(node_data.labels) if hasattr(node_data, 'labels') else label_list
|
||||||
|
if node_labels:
|
||||||
|
label_list = node_labels
|
||||||
|
# 转换为字典
|
||||||
|
node_data = dict(node_data)
|
||||||
|
|
||||||
|
# 跳过无效节点
|
||||||
|
if not isinstance(node_data, dict) and not node_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 提取节点ID(兜底情况下使用简单的ID格式)
|
||||||
|
if isinstance(node_data, dict):
|
||||||
|
# 优先使用element_id或id(Neo4j原生ID)
|
||||||
|
if node_data.get("element_id"):
|
||||||
|
node_id = str(node_data.get("element_id"))
|
||||||
|
elif node_data.get("id"):
|
||||||
|
node_id = str(node_data.get("id"))
|
||||||
|
else:
|
||||||
|
# 兜底情况:使用节点的名称属性作为简单ID
|
||||||
|
# 优先使用中文属性名"名称",其次使用"name"
|
||||||
|
node_id = node_data.get("名称") or node_data.get("name") or node_name
|
||||||
|
if node_id:
|
||||||
|
node_id = str(node_id)
|
||||||
|
else:
|
||||||
|
# 如果都没有,生成一个简单的ID
|
||||||
|
node_id = f"{label}_{len(results)}"
|
||||||
|
else:
|
||||||
|
node_id = str(node_name) if node_name else f"{label}_{len(results)}"
|
||||||
|
|
||||||
|
# 提取节点属性(排除内部字段和指定字段)
|
||||||
|
node_properties = {}
|
||||||
|
if isinstance(node_data, dict):
|
||||||
|
for key, value in node_data.items():
|
||||||
|
if key not in ["element_id", "id", "labels", "embedding", "fulltext", "name", "切片"]:
|
||||||
|
node_properties[key] = value
|
||||||
|
|
||||||
|
# 如果节点数据中有labels,使用它
|
||||||
|
if "labels" in node_data and node_data["labels"]:
|
||||||
|
if isinstance(node_data["labels"], list):
|
||||||
|
label_list = node_data["labels"]
|
||||||
|
elif hasattr(node_data["labels"], '__iter__'):
|
||||||
|
label_list = list(node_data["labels"])
|
||||||
|
|
||||||
|
# 如果没有属性,跳过
|
||||||
|
if not node_properties:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 直接过滤掉 last_updated 和 created_at
|
||||||
|
node_properties.pop('last_updated', None)
|
||||||
|
node_properties.pop('created_at', None)
|
||||||
|
|
||||||
|
# 使用remove_sensitive_fields递归删除embedding和fulltext字段(确保嵌套结构也被清理)
|
||||||
|
node_properties = remove_sensitive_fields(node_properties)
|
||||||
|
|
||||||
|
# 提取并格式化 knowledge_source
|
||||||
|
knowledge_source_value = node_properties.get('knowledge_source')
|
||||||
|
formatted_knowledge_source = extract_knowledge_source(knowledge_source_value)
|
||||||
|
|
||||||
|
# 创建SimpleNode对象(knowledge_source 会在 format_results 的 process_node 中再次处理)
|
||||||
|
simple_node = SimpleNode(
|
||||||
|
node_data=node_properties,
|
||||||
|
labels=label_list,
|
||||||
|
node_id=node_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建SimplePath对象(只包含一个节点)
|
||||||
|
simple_path = SimplePath(nodes=[simple_node])
|
||||||
|
|
||||||
|
# 构建结果字典
|
||||||
|
# 包含path字段(节点作为path),同时将节点属性作为result字段
|
||||||
|
result = {
|
||||||
|
"path": simple_path,
|
||||||
|
"result": node_properties # 节点作为结果(已删除embedding和fulltext)
|
||||||
|
}
|
||||||
|
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _format_value(value: Any) -> str:
|
||||||
|
"""
|
||||||
|
将任意值转为简洁字符串,自适应处理:
|
||||||
|
- 数字或字符串:直接使用
|
||||||
|
- 字典:检查是否有 result 字段,分解 result 字段,去除 knowledge_source
|
||||||
|
- 列表:递归处理每个元素
|
||||||
|
"""
|
||||||
|
# 如果直接是数字或字符串,直接返回
|
||||||
|
if isinstance(value, (str, int, float)):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
# 如果是字典,检查是否有 result 字段
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
# 如果字典中有 result 字段,优先处理 result 字段的内容
|
||||||
|
if 'result' in value:
|
||||||
|
result_value = value['result']
|
||||||
|
# 处理 result 字段(去除 knowledge_source 和指定字段)
|
||||||
|
if isinstance(result_value, dict):
|
||||||
|
# 复制字典,去除 knowledge_source、last_updated、created_at 和指定字段
|
||||||
|
skip_keys = {'knowledge_source', 'name', '切片', 'last_updated', 'created_at'}
|
||||||
|
cleaned_result = {k: v for k, v in result_value.items() if k not in skip_keys}
|
||||||
|
return _format_value(cleaned_result)
|
||||||
|
elif isinstance(result_value, list):
|
||||||
|
# 如果是列表,递归处理每个元素(去除 knowledge_source)
|
||||||
|
if len(result_value) == 0:
|
||||||
|
return "[]"
|
||||||
|
# 如果是简单列表,直接连接
|
||||||
|
if all(isinstance(x, (str, int, float)) for x in result_value[:3]):
|
||||||
|
return ", ".join(str(x) for x in result_value[:10]) + ("..." if len(result_value) > 10 else "")
|
||||||
|
# 复杂列表,递归处理每个元素
|
||||||
|
cleaned_items = []
|
||||||
|
for item in result_value[:10]:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
# 去除 knowledge_source、last_updated、created_at 和指定字段
|
||||||
|
skip_keys = {'knowledge_source', 'name', '切片', 'last_updated', 'created_at'}
|
||||||
|
cleaned_item = {k: v for k, v in item.items() if k not in skip_keys}
|
||||||
|
cleaned_items.append(_format_value(cleaned_item))
|
||||||
|
else:
|
||||||
|
cleaned_items.append(_format_value(item))
|
||||||
|
result_str = "; ".join(cleaned_items)
|
||||||
|
if len(result_value) > 10:
|
||||||
|
result_str += f" ...(共 {len(result_value)} 条)"
|
||||||
|
return result_str
|
||||||
|
else:
|
||||||
|
# result 字段是其他类型,直接格式化
|
||||||
|
return _format_value(result_value)
|
||||||
|
else:
|
||||||
|
# 没有 result 字段,正常处理字典,去除 knowledge_source、last_updated、created_at 和指定字段
|
||||||
|
skip_keys = {'knowledge_source', 'name', '切片', 'last_updated', 'created_at'}
|
||||||
|
cleaned_dict = {k: v for k, v in value.items() if k not in skip_keys}
|
||||||
|
# 展平字典为 key: value 形式,避免嵌套 JSON
|
||||||
|
parts = []
|
||||||
|
for k, v in cleaned_dict.items():
|
||||||
|
if not isinstance(v, (dict, list)) or len(str(v)) < 100: # 避免大对象
|
||||||
|
parts.append(f"{k}: {_format_value(v)}")
|
||||||
|
return "; ".join(parts) if parts else "{...}"
|
||||||
|
|
||||||
|
# 如果是列表
|
||||||
|
elif isinstance(value, list):
|
||||||
|
if len(value) == 0:
|
||||||
|
return "[]"
|
||||||
|
# 如果是简单列表(如 [1,2,3] 或 ["a","b"])
|
||||||
|
if all(isinstance(x, (str, int, float)) for x in value[:3]):
|
||||||
|
return ", ".join(str(x) for x in value[:10]) + ("..." if len(value) > 10 else "")
|
||||||
|
else:
|
||||||
|
# 复杂对象列表,检查是否有 {'result': {...}} 格式的元素
|
||||||
|
# 检查前几个元素,判断是否都是 {'result': {...}} 格式
|
||||||
|
has_result_format = False
|
||||||
|
if value and isinstance(value[0], dict) and 'result' in value[0]:
|
||||||
|
# 检查是否所有元素都是 {'result': {...}} 格式
|
||||||
|
has_result_format = all(
|
||||||
|
isinstance(item, dict) and 'result' in item and isinstance(item.get('result'), dict)
|
||||||
|
for item in value[:3] # 只检查前3个元素来判断模式
|
||||||
|
)
|
||||||
|
|
||||||
|
formatted_items = []
|
||||||
|
for item in value[:10]: # 最多处理10个元素
|
||||||
|
if has_result_format and isinstance(item, dict) and 'result' in item:
|
||||||
|
# 如果是 {'result': {...}} 格式,分解 result 字段并去除 knowledge_source 和指定字段
|
||||||
|
result_value = item['result']
|
||||||
|
if isinstance(result_value, dict):
|
||||||
|
# 去除 knowledge_source、last_updated、created_at 和指定字段
|
||||||
|
skip_keys = {'knowledge_source', 'name', '切片', 'last_updated', 'created_at'}
|
||||||
|
cleaned_result = {k: v for k, v in result_value.items() if k not in skip_keys}
|
||||||
|
formatted_items.append(_format_value(cleaned_result))
|
||||||
|
else:
|
||||||
|
formatted_items.append(_format_value(result_value))
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
# 如果不是 {'result': {...}} 格式,去除字典中的 knowledge_source、last_updated、created_at 和指定字段
|
||||||
|
skip_keys = {'knowledge_source', 'name', '切片', 'last_updated', 'created_at'}
|
||||||
|
cleaned_item = {k: v for k, v in item.items() if k not in skip_keys}
|
||||||
|
formatted_items.append(_format_value(cleaned_item))
|
||||||
|
else:
|
||||||
|
# 其他类型,正常处理
|
||||||
|
formatted_items.append(_format_value(item))
|
||||||
|
|
||||||
|
result = "; ".join(formatted_items)
|
||||||
|
if len(value) > 10:
|
||||||
|
result += f" ...(共 {len(value)} 条)"
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return str(value)[:200] # 截断超长内容
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_graph_for_llm(graph_response: dict) -> str:
|
||||||
|
"""
|
||||||
|
通用图谱结果序列化器,支持:
|
||||||
|
- nodes(实体详情)
|
||||||
|
- results(任意查询返回值:聚合/记录/字符串等)
|
||||||
|
"""
|
||||||
|
data = graph_response.get("data", {})
|
||||||
|
nodes = data.get("nodes", [])
|
||||||
|
results = data.get("results", [])
|
||||||
|
|
||||||
|
output_lines = []
|
||||||
|
|
||||||
|
# ========== 1. 处理 results(核心查询返回值)==========
|
||||||
|
if results:
|
||||||
|
output_lines.append("【图谱查询直接结果】")
|
||||||
|
if len(results) == 1 and not isinstance(results[0], (dict, list)):
|
||||||
|
# 单值结果(如 count, max, 字符串)
|
||||||
|
output_lines.append(f" {results[0]}")
|
||||||
|
else:
|
||||||
|
# 多条记录或复杂结构
|
||||||
|
for i, res in enumerate(results): # 最多展示10条
|
||||||
|
formatted = _format_value(res)
|
||||||
|
output_lines.append(f" [{i + 1}] {formatted}")
|
||||||
|
|
||||||
|
output_lines.append("")
|
||||||
|
|
||||||
|
# ========== 2. 处理 nodes(完整实体信息)==========
|
||||||
|
if nodes:
|
||||||
|
output_lines.append("【相关图谱实体详情】")
|
||||||
|
for node in nodes: # 防止过长,最多15个节点
|
||||||
|
name = node.get("name", "Unnamed")
|
||||||
|
labels = node.get("labels", [])
|
||||||
|
props = node.get("properties", {})
|
||||||
|
|
||||||
|
output_lines.append(f" - 名称: {name}")
|
||||||
|
if labels:
|
||||||
|
output_lines.append(f" 标签: {', '.join(labels)}")
|
||||||
|
|
||||||
|
# 过滤掉低价值字段(如 分数, 内部 id, name, 切片, last_updated, created_at)
|
||||||
|
skip_props = {"分数", "id", "name", "切片", "last_updated", "created_at", "最后更新时间", "创建时间"}
|
||||||
|
for k, v in props.items():
|
||||||
|
if k not in skip_props and v not in (None, ""):
|
||||||
|
output_lines.append(f" {k}: {v}")
|
||||||
|
|
||||||
|
# 不再显示手册来源(knowledge_source),因为用户要求在 results 字段中不显示
|
||||||
|
|
||||||
|
output_lines.append("") # 节点间空行
|
||||||
|
|
||||||
|
# ========== 返回结果 ==========
|
||||||
|
if not output_lines:
|
||||||
|
return "图谱查询完成,但未返回有效数据。"
|
||||||
|
|
||||||
|
return "\n".join(output_lines).rstrip()
|
||||||
123137
huizong_node_rela.json
123137
huizong_node_rela.json
File diff suppressed because one or more lines are too long
BIN
kg_build_0409/test/122-06A0014-B01001_发动机-维修手册.docx
Normal file
BIN
kg_build_0409/test/122-06A0014-B01001_发动机-维修手册.docx
Normal file
Binary file not shown.
BIN
kg_build_0409/test/122-06A0014-B01001_发动机-维修手册.pdf
Normal file
BIN
kg_build_0409/test/122-06A0014-B01001_发动机-维修手册.pdf
Normal file
Binary file not shown.
9517
kg_build_0409/test/122-06A0014-B01001_发动机-维修手册_content_list.json
Normal file
9517
kg_build_0409/test/122-06A0014-B01001_发动机-维修手册_content_list.json
Normal file
File diff suppressed because one or more lines are too long
BIN
kg_build_0409/test/163-06A0015-B01001_船舶主发动机原理图.pdf
Normal file
BIN
kg_build_0409/test/163-06A0015-B01001_船舶主发动机原理图.pdf
Normal file
Binary file not shown.
133
kg_build_0409/test/过滤文本抽取结果.json
Normal file
133
kg_build_0409/test/过滤文本抽取结果.json
Normal file
File diff suppressed because one or more lines are too long
257
kg_build_0409/test/过滤文本抽取结果_0318.json
Normal file
257
kg_build_0409/test/过滤文本抽取结果_0318.json
Normal file
File diff suppressed because one or more lines are too long
568715
kg_snapshot.json
568715
kg_snapshot.json
File diff suppressed because one or more lines are too long
@ -15,7 +15,19 @@ from neo4j_graphrag.embeddings.base import Embedder
|
|||||||
|
|
||||||
from config import LLM_CONFIG, EMBEDDING_CONFIG
|
from config import LLM_CONFIG, EMBEDDING_CONFIG
|
||||||
|
|
||||||
|
# 模块级单例客户端,避免每次调用都创建新连接
|
||||||
|
_async_client: AsyncOpenAI = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_async_client() -> AsyncOpenAI:
|
||||||
|
"""获取或创建 AsyncOpenAI 单例客户端"""
|
||||||
|
global _async_client
|
||||||
|
if _async_client is None:
|
||||||
|
_async_client = AsyncOpenAI(
|
||||||
|
api_key=LLM_CONFIG["api_key"],
|
||||||
|
base_url=LLM_CONFIG["base_url"],
|
||||||
|
)
|
||||||
|
return _async_client
|
||||||
class OpenaiAPI:
|
class OpenaiAPI:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -148,7 +160,79 @@ class OpenaiAPI:
|
|||||||
extra_body={"chat_template_kwargs": {"enable_thinking": False}}
|
extra_body={"chat_template_kwargs": {"enable_thinking": False}}
|
||||||
)
|
)
|
||||||
return response.choices[0].message.content
|
return response.choices[0].message.content
|
||||||
|
@staticmethod
|
||||||
|
async def open_api_vl_without_thinking(
|
||||||
|
image_url: str
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
专门用于从图片中提取【设备名称】和【故障现象】。
|
||||||
|
自动过滤思考过程,仅返回核心结果。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
image_url: Base64 格式的图片数据 (data:image/...;base64,...)
|
||||||
|
model: 模型名称,默认为配置中的模型
|
||||||
|
custom_instruction: 额外的特定指令 (可选)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 1. 确定模型名称
|
||||||
|
model = LLM_CONFIG.get("model")
|
||||||
|
|
||||||
|
# 2. 构建强约束的 System Prompt
|
||||||
|
# 核心目标:禁止思考标签,禁止废话,只给结果
|
||||||
|
system_prompt = (
|
||||||
|
"你是一个视觉分析专家。你的任务是从图片中识别内容并进行描述。\n"
|
||||||
|
"【严格约束】\n"
|
||||||
|
"1. 绝对禁止输出 <think>, <think>, <reasoning> 等任何思考过程标签。\n"
|
||||||
|
"2. 绝对禁止输出'好的'、'根据图片'、'分析如下'等开场白或结束语。\n"
|
||||||
|
"3. 直接输出最终结论,格式必须严格遵守下面的模板。"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 构建针对性的 User Prompt
|
||||||
|
default_task = (
|
||||||
|
"请分析这张图片,描述图片内容,重点关注以下信息\n"
|
||||||
|
"1. 图片整体阐述了什么主题,描述了哪些内容\n"
|
||||||
|
"请用专业、客观、简短的语言描述。"
|
||||||
|
)
|
||||||
|
|
||||||
|
final_user_prompt = default_task
|
||||||
|
|
||||||
|
# 4. 初始化客户端
|
||||||
|
client = _get_async_client()
|
||||||
|
|
||||||
|
# 5. 构建请求参数
|
||||||
|
kwargs = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "text", "text": final_user_prompt},
|
||||||
|
{"type": "image_url", "image_url": {"url": image_url}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"temperature": 0.1, # 低温度以保证事实准确性
|
||||||
|
"stream": False,
|
||||||
|
"max_tokens": LLM_CONFIG.get("max_tokens"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 尝试通过参数关闭思考 (取决于后端支持情况)
|
||||||
|
kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"🚀 正在调用 {model} 进行图片内容分析...")
|
||||||
|
response = await client.chat.completions.create(**kwargs)
|
||||||
|
raw_content = response.choices[0].message.content or ""
|
||||||
|
|
||||||
|
print(f"✅ 分析完成:\n{raw_content}")
|
||||||
|
return raw_content
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_msg = f"❌ 图片内容分析失败:{str(e)}\n{traceback.format_exc()}"
|
||||||
|
print(error_msg)
|
||||||
|
return f"Error: {str(e)}"
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def open_api_chat_async_json(query: str, model: str = None, temperature: float = None) -> str:
|
async def open_api_chat_async_json(query: str, model: str = None, temperature: float = None) -> str:
|
||||||
if model is None:
|
if model is None:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -35,6 +35,8 @@ EMBED_MAX_WORKERS = SEARCH_CONFIG["embed_thread_num"] # embedding 批次并行
|
|||||||
NODE_FETCH_PAGE = SEARCH_CONFIG["node_fetch_page"] # 单次拉取待生成 embedding 节点的分页大小
|
NODE_FETCH_PAGE = SEARCH_CONFIG["node_fetch_page"] # 单次拉取待生成 embedding 节点的分页大小
|
||||||
|
|
||||||
# 全局索引名称(覆盖所有节点标签)
|
# 全局索引名称(覆盖所有节点标签)
|
||||||
|
GLOBAL_VECTOR_INDEX_NAME = SEARCH_CONFIG["global_entity_embedding"]
|
||||||
|
GLOBAL_FULLTEXT_INDEX_NAME = SEARCH_CONFIG["global_entity_content_search"]
|
||||||
DEFAULT_URI = NEO4J_CONFIG["uri"]
|
DEFAULT_URI = NEO4J_CONFIG["uri"]
|
||||||
DEFAULT_AUTH = (
|
DEFAULT_AUTH = (
|
||||||
NEO4J_CONFIG["username"],
|
NEO4J_CONFIG["username"],
|
||||||
@ -55,6 +57,8 @@ OLD_INDEX_NAMES: List[str] = [
|
|||||||
VECTOR_INDEX_NAME,
|
VECTOR_INDEX_NAME,
|
||||||
FULLTEXT_INDEX_NAME,
|
FULLTEXT_INDEX_NAME,
|
||||||
FULLTEXT_FIELD_INDEX_NAME,
|
FULLTEXT_FIELD_INDEX_NAME,
|
||||||
|
GLOBAL_VECTOR_INDEX_NAME,
|
||||||
|
GLOBAL_FULLTEXT_INDEX_NAME,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -188,6 +192,390 @@ def drop_index_without_constraint(driver):
|
|||||||
driver.execute_query(f"drop index {record['name']} if exists")
|
driver.execute_query(f"drop index {record['name']} if exists")
|
||||||
|
|
||||||
|
|
||||||
|
# --------- 创建全局混合索引(覆盖所有节点标签) ---------
|
||||||
|
def create_global_indexes(driver, force_refresh=False):
|
||||||
|
"""
|
||||||
|
创建覆盖所有节点标签的全局向量索引和全文索引。
|
||||||
|
与按标签创建的索引互补,支持跨标签统一检索。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
driver: Neo4j driver
|
||||||
|
force_refresh: True=先删除旧索引再重建,False=IF NOT EXISTS 跳过已有索引
|
||||||
|
"""
|
||||||
|
if force_refresh:
|
||||||
|
for index_name in (GLOBAL_VECTOR_INDEX_NAME, GLOBAL_FULLTEXT_INDEX_NAME):
|
||||||
|
try:
|
||||||
|
driver.execute_query(f"DROP INDEX {index_name} IF EXISTS")
|
||||||
|
logger.info(f"强制刷新:已删除全局索引 {index_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"删除全局索引 {index_name} 时出错(可能不存在): {e}")
|
||||||
|
|
||||||
|
# 全局向量索引(作用于所有节点的 embedding 属性)
|
||||||
|
try:
|
||||||
|
driver.execute_query(f"""
|
||||||
|
CREATE VECTOR INDEX {GLOBAL_VECTOR_INDEX_NAME} IF NOT EXISTS
|
||||||
|
FOR (n) ON n.embedding
|
||||||
|
OPTIONS {{indexConfig: {{
|
||||||
|
`vector.dimensions`: {vector_dim},
|
||||||
|
`vector.similarity_function`: 'cosine'
|
||||||
|
}}}}
|
||||||
|
""")
|
||||||
|
logger.info(f"全局向量索引 '{GLOBAL_VECTOR_INDEX_NAME}' 已就绪")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建全局向量索引失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 全局全文索引(作用于所有节点的 名称 和 内容 属性)
|
||||||
|
try:
|
||||||
|
driver.execute_query(f"""
|
||||||
|
CREATE FULLTEXT INDEX {GLOBAL_FULLTEXT_INDEX_NAME} IF NOT EXISTS
|
||||||
|
FOR (n) ON EACH [n.名称, n.内容]
|
||||||
|
""")
|
||||||
|
logger.info(f"全局全文索引 '{GLOBAL_FULLTEXT_INDEX_NAME}' 已就绪")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建全局全文索引失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# --------- 创建向量索引 ---------
|
||||||
|
def vector_indexing(driver, label, force_refresh=False):
|
||||||
|
"""
|
||||||
|
创建向量索引(如果节点没有 embedding,使用"名称"属性生成 embedding)
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 待生成 embedding 的节点通过分页循环全部处理,不再受单次 LIMIT 限制,
|
||||||
|
避免节点数超过一万时被静默丢弃。
|
||||||
|
- embedding 计算采用并行批次以缩短耗时。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
driver: Neo4j driver
|
||||||
|
label: 节点标签
|
||||||
|
force_refresh: 是否强制刷新(如果索引已存在,force_refresh=False 时会跳过,True 时会先删除再创建)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# === 分页处理所有缺失 embedding 的节点 ===
|
||||||
|
# 每轮取一页未生成 embedding 的节点并写回,直到没有剩余节点为止。
|
||||||
|
# 这样即使待处理节点远超一万也能全部覆盖。
|
||||||
|
total_generated = 0
|
||||||
|
while True:
|
||||||
|
query_no_embedding = f"""
|
||||||
|
MATCH (n:{label})
|
||||||
|
WHERE n.embedding IS NULL
|
||||||
|
AND n.名称 IS NOT NULL
|
||||||
|
RETURN elementId(n) AS id, n.名称 AS 名称
|
||||||
|
LIMIT {NODE_FETCH_PAGE}
|
||||||
|
"""
|
||||||
|
result_no_embedding = driver.execute_query(query_no_embedding)
|
||||||
|
nodes_to_process = [
|
||||||
|
(record["id"], record["名称"]) for record in result_no_embedding.records
|
||||||
|
]
|
||||||
|
|
||||||
|
if not nodes_to_process:
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info(f"标签 {label} 本轮发现 {len(nodes_to_process)} 个节点需要生成 embedding")
|
||||||
|
|
||||||
|
ids, names = zip(*nodes_to_process)
|
||||||
|
ids = list(ids)
|
||||||
|
names = list(names)
|
||||||
|
|
||||||
|
# 并行计算 embedding
|
||||||
|
logger.info(f"开始为 {len(names)} 个节点计算 embedding(并行批次)...")
|
||||||
|
try:
|
||||||
|
all_embeddings = _compute_embeddings_parallel(names)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"批量嵌入向量计算失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# 转换为 numpy 数组并做 L2 归一化
|
||||||
|
embeddings = np.array(all_embeddings)
|
||||||
|
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
|
||||||
|
norms = np.where(norms == 0, 1, norms) # 避免除零
|
||||||
|
embeddings = embeddings / norms
|
||||||
|
|
||||||
|
# 保存 embedding 到节点
|
||||||
|
logger.info(f"写入 {len(ids)} 个节点的 embedding...")
|
||||||
|
upsert_vectors(
|
||||||
|
driver,
|
||||||
|
ids=ids,
|
||||||
|
embedding_property="embedding",
|
||||||
|
embeddings=embeddings,
|
||||||
|
)
|
||||||
|
total_generated += len(ids)
|
||||||
|
logger.info(f"本轮成功为 {len(ids)} 个节点生成并保存 embedding")
|
||||||
|
|
||||||
|
# 不足一页说明已处理完,提前结束
|
||||||
|
if len(nodes_to_process) < NODE_FETCH_PAGE:
|
||||||
|
break
|
||||||
|
|
||||||
|
if total_generated:
|
||||||
|
logger.info(f"标签 {label} 共生成 {total_generated} 个节点的 embedding")
|
||||||
|
|
||||||
|
# 检查是否有节点存在 embedding(包括刚生成的)
|
||||||
|
check_query = f"MATCH (n:{label}) WHERE n.embedding IS NOT NULL RETURN count(n) AS count"
|
||||||
|
result = driver.execute_query(check_query)
|
||||||
|
node_count = result.records[0]["count"] if result.records else 0
|
||||||
|
|
||||||
|
if node_count == 0:
|
||||||
|
logger.warning(f"标签 {label} 没有节点包含 embedding 属性,跳过向量索引创建")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建向量索引
|
||||||
|
index_name = f"{label.lower()}_vector"
|
||||||
|
|
||||||
|
# 如果强制刷新且索引已存在,先删除它
|
||||||
|
if force_refresh:
|
||||||
|
try:
|
||||||
|
driver.execute_query(f"DROP INDEX {index_name} IF EXISTS")
|
||||||
|
logger.info(f"强制刷新模式:已删除旧索引 {index_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"删除索引 {index_name} 时出错(可能不存在): {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
create_vector_index(
|
||||||
|
driver,
|
||||||
|
name=index_name, # 索引的唯一名称
|
||||||
|
label=label, # 要索引的节点标签
|
||||||
|
embedding_property="embedding", # 包含嵌入向量值的节点属性键
|
||||||
|
dimensions=vector_dim, # 向量嵌入维度,1024与使用的 bge-m3 嵌入模型一致
|
||||||
|
similarity_fn="cosine", # 向量相似度函数,可选 "euclidean" 或 "cosine"
|
||||||
|
)
|
||||||
|
logger.info(f"成功为标签 {label} 创建向量索引(共 {node_count} 个节点)")
|
||||||
|
except Exception as e:
|
||||||
|
# 如果是增量更新模式且索引已存在,这是正常的,只记录警告
|
||||||
|
if not force_refresh and "already exists" in str(e).lower():
|
||||||
|
logger.info(f"标签 {label} 的向量索引已存在,跳过创建")
|
||||||
|
else:
|
||||||
|
logger.error(f"为标签 {label} 创建向量索引失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# --------- 创建全文索引 ---------
|
||||||
|
def fulltext_indexing(driver, label, force_refresh=False):
|
||||||
|
"""
|
||||||
|
创建全文索引,并添加节点属性(自动获取所有属性)
|
||||||
|
|
||||||
|
全文索引:词 ---》 id
|
||||||
|
后续检索,分词后,根据词反向找到节点
|
||||||
|
|
||||||
|
Args:
|
||||||
|
driver: Neo4j driver
|
||||||
|
label: 节点标签
|
||||||
|
force_refresh: 是否强制刷新所有节点的全文索引(True=清空后重新生成,False=只处理缺失的节点)
|
||||||
|
"""
|
||||||
|
# 排除内部属性
|
||||||
|
excluded_props = {"embedding", "fulltext", "elementId", "id"}
|
||||||
|
|
||||||
|
# 根据 force_refresh 决定查询条件
|
||||||
|
if force_refresh:
|
||||||
|
# 强制刷新:处理所有节点(包括已有 fulltext 的节点)
|
||||||
|
query = f"""MATCH (n:{label})
|
||||||
|
WITH n, elementId(n) AS id, properties(n) AS props
|
||||||
|
RETURN id, props"""
|
||||||
|
else:
|
||||||
|
# 增量更新:只处理没有 fulltext 的节点
|
||||||
|
query = f"""MATCH (n:{label})
|
||||||
|
WHERE n.fulltext IS NULL
|
||||||
|
WITH n, elementId(n) AS id, properties(n) AS props
|
||||||
|
RETURN id, props"""
|
||||||
|
|
||||||
|
# 执行查询
|
||||||
|
records = driver.execute_query(query).records
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
if force_refresh:
|
||||||
|
logger.info(f"{label} 没有节点需要处理")
|
||||||
|
else:
|
||||||
|
logger.info(f"{label} 所有节点皆存在全文索引属性")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 构建文本描述(使用所有属性)
|
||||||
|
record_list = []
|
||||||
|
for r in records:
|
||||||
|
node_id = r["id"]
|
||||||
|
props = r["props"]
|
||||||
|
# 过滤掉内部属性
|
||||||
|
filtered_props = {k: v for k, v in props.items() if k not in excluded_props}
|
||||||
|
# 使用所有属性构建文本
|
||||||
|
text = build_text_from_all_properties(filtered_props)
|
||||||
|
if text:
|
||||||
|
record_list.append({"id": node_id, "text": text})
|
||||||
|
|
||||||
|
if not record_list:
|
||||||
|
logger.info(f"{label} 没有可索引的文本内容")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建全文索引(在写入数据之前创建索引)
|
||||||
|
try:
|
||||||
|
create_fulltext_index(
|
||||||
|
driver,
|
||||||
|
name=f"{label.lower()}_fulltext", # 索引的唯一名称
|
||||||
|
label=label, # 要创建索引的节点标签
|
||||||
|
node_properties=["fulltext"], # 要创建全文索引的节点属性列表
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"创建全文索引可能已存在(继续处理): {e}")
|
||||||
|
|
||||||
|
# 文本分词,作为全文索引属性
|
||||||
|
logger.info(f"计算 {label} ({len(record_list)}) 的全文索引")
|
||||||
|
pattern = re.compile(r"[a-zA-Z0-9\u4e00-\u9fa5]+") # 匹配英文字母、数字和中文字符
|
||||||
|
fulltext_tuple_list = []
|
||||||
|
for id_, text in [(r["id"], r["text"]) for r in record_list]:
|
||||||
|
# 使用 jieba 分词
|
||||||
|
words = jieba.lcut(text)
|
||||||
|
# 过滤并连接
|
||||||
|
filtered_words = [
|
||||||
|
word.strip()
|
||||||
|
for word in words
|
||||||
|
if word.strip() and pattern.fullmatch(word.strip())
|
||||||
|
]
|
||||||
|
fulltext_value = " ".join(filtered_words)
|
||||||
|
if fulltext_value:
|
||||||
|
fulltext_tuple_list.append((id_, fulltext_value))
|
||||||
|
|
||||||
|
if not fulltext_tuple_list:
|
||||||
|
logger.warning(f"{label} 分词后没有有效内容")
|
||||||
|
return
|
||||||
|
|
||||||
|
ids, fulltexts = zip(*fulltext_tuple_list)
|
||||||
|
ids = list(ids)
|
||||||
|
fulltexts = list(fulltexts)
|
||||||
|
|
||||||
|
# 按 elementId 添加全文索引属性
|
||||||
|
logger.info(f"写入 {label} ({len(fulltexts)}) 的全文索引")
|
||||||
|
insert_batch_size = 1000
|
||||||
|
for i in range(0, len(fulltext_tuple_list), insert_batch_size):
|
||||||
|
batch_rows = [
|
||||||
|
{"id": id_, "fulltext": ft}
|
||||||
|
for id_, ft in zip(
|
||||||
|
ids[i: i + insert_batch_size],
|
||||||
|
fulltexts[i: i + insert_batch_size],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# UNWIND:将列表数据展开为多行记录
|
||||||
|
driver.execute_query(
|
||||||
|
"UNWIND $rows AS row " # 将传入的 rows 列表展开,每项作为一行数据,命名为 row
|
||||||
|
"MATCH (n) "
|
||||||
|
"WHERE elementId(n) = row.id "
|
||||||
|
"SET n.fulltext = row.fulltext ",
|
||||||
|
{"rows": batch_rows},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_all_indexes(neo4j_uri=None, neo4j_username=None, neo4j_password=None, force_refresh=False, driver=None):
|
||||||
|
"""
|
||||||
|
创建所有索引的主函数(可被外部调用)
|
||||||
|
|
||||||
|
注意:Neo4j 连接配置优先从 .env 文件中读取(NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD)
|
||||||
|
如果 .env 文件中没有配置,则使用参数中的值(如果提供)或默认值
|
||||||
|
|
||||||
|
Args:
|
||||||
|
neo4j_uri: Neo4j URI(可选,优先级低于 .env,提供 driver 时忽略)
|
||||||
|
neo4j_username: Neo4j 用户名(可选,优先级低于 .env,提供 driver 时忽略)
|
||||||
|
neo4j_password: Neo4j 密码(可选,优先级低于 .env,提供 driver 时忽略)
|
||||||
|
force_refresh: 是否强制刷新所有索引(True=清空重建,False=增量更新)
|
||||||
|
driver: 可选的 Neo4j driver 实例,提供则复用外部 driver
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含执行结果的字典
|
||||||
|
"""
|
||||||
|
use_external_driver = driver is not None
|
||||||
|
|
||||||
|
if not use_external_driver:
|
||||||
|
# 优先从 .env 文件读取配置,如果 .env 中没有则使用参数或默认值
|
||||||
|
neo4j_uri = os.getenv("NEO4J_URI") or neo4j_uri or "bolt://192.168.0.46:57687"
|
||||||
|
neo4j_username = os.getenv("NEO4J_USERNAME") or neo4j_username or "neo4j"
|
||||||
|
neo4j_password = os.getenv("NEO4J_PASSWORD") or neo4j_password or "zdht123@"
|
||||||
|
logger.info(f"连接到 Neo4j: {neo4j_uri}")
|
||||||
|
else:
|
||||||
|
logger.info("使用外部提供的 Neo4j driver")
|
||||||
|
|
||||||
|
# 定义内部函数来执行索引创建逻辑(避免代码重复)
|
||||||
|
def _execute_indexing(driver):
|
||||||
|
# 1、根据 force_refresh 决定是否清空索引和约束
|
||||||
|
if force_refresh:
|
||||||
|
logger.info("强制刷新模式:清空所有约束...")
|
||||||
|
drop_constraint(driver)
|
||||||
|
logger.info("强制刷新模式:清空所有索引...")
|
||||||
|
drop_index_without_constraint(driver)
|
||||||
|
logger.info("强制刷新模式:清空所有节点的 fulltext 属性...")
|
||||||
|
driver.execute_query("MATCH (n) WHERE n.fulltext IS NOT NULL REMOVE n.fulltext")
|
||||||
|
else:
|
||||||
|
logger.info("增量更新模式:保留现有索引和约束")
|
||||||
|
|
||||||
|
# 2、自动获取所有节点标签并创建向量索引
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info("开始创建向量索引...")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
|
||||||
|
all_labels = get_all_labels(driver)
|
||||||
|
logger.info(f"发现 {len(all_labels)} 个节点标签: {all_labels}")
|
||||||
|
|
||||||
|
vector_index_errors = []
|
||||||
|
for label in all_labels:
|
||||||
|
logger.info(f"处理标签: {label}")
|
||||||
|
try:
|
||||||
|
vector_indexing(driver, label, force_refresh=force_refresh)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"为标签 {label} 创建向量索引时出错: {e}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
vector_index_errors.append(error_msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3、创建全文索引(自动获取所有属性)
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info("开始创建全文索引...")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
|
||||||
|
fulltext_index_errors = []
|
||||||
|
for label in all_labels:
|
||||||
|
logger.info(f"处理标签: {label}")
|
||||||
|
try:
|
||||||
|
fulltext_indexing(driver, label, force_refresh=force_refresh)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"为标签 {label} 创建全文索引时出错: {e}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
fulltext_index_errors.append(error_msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info("所有索引创建完成!")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
|
||||||
|
has_errors = len(vector_index_errors) > 0 or len(fulltext_index_errors) > 0
|
||||||
|
mode_text = "强制刷新" if force_refresh else "增量更新"
|
||||||
|
return {
|
||||||
|
"success": not has_errors,
|
||||||
|
"message": f"{mode_text}模式:索引创建完成" if not has_errors
|
||||||
|
else f"{mode_text}模式:索引创建完成,但部分标签出现错误",
|
||||||
|
"mode": "force_refresh" if force_refresh else "incremental",
|
||||||
|
"labels": all_labels,
|
||||||
|
"vector_index_errors": vector_index_errors,
|
||||||
|
"fulltext_index_errors": fulltext_index_errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if use_external_driver:
|
||||||
|
# 使用外部 driver,直接执行,不关闭 driver
|
||||||
|
return _execute_indexing(driver)
|
||||||
|
else:
|
||||||
|
# 使用内部 driver,使用上下文管理器自动关闭
|
||||||
|
with GraphDatabase.driver(neo4j_uri, auth=(neo4j_username, neo4j_password)) as driver:
|
||||||
|
return _execute_indexing(driver)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"创建索引过程中发生错误: {e}"
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": error_msg,
|
||||||
|
"mode": "error",
|
||||||
|
"labels": [],
|
||||||
|
"vector_index_errors": [],
|
||||||
|
"fulltext_index_errors": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def print_neo4j_version(driver) -> None:
|
def print_neo4j_version(driver) -> None:
|
||||||
"""打印当前 Neo4j 实例的版本和版本类型。"""
|
"""打印当前 Neo4j 实例的版本和版本类型。"""
|
||||||
with driver.session() as session:
|
with driver.session() as session:
|
||||||
@ -407,12 +795,13 @@ build_hyrid_indexes = build_hybrid_indexes
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# 可以通过命令行参数或环境变量控制是否强制刷新
|
||||||
|
import sys
|
||||||
|
|
||||||
result = build_hybrid_indexes(
|
force_refresh = "--force" in sys.argv or os.getenv("FORCE_REFRESH", "false").lower() == "true"
|
||||||
drop_old=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
result = create_all_indexes(force_refresh=force_refresh)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
logger.info("混合索引创建成功")
|
logger.info(f"索引创建成功完成(模式: {result['mode']})")
|
||||||
else:
|
else:
|
||||||
logger.error(result["message"])
|
logger.error(f"索引创建完成但有错误: {result['message']}")
|
||||||
|
|||||||
Binary file not shown.
0
reset_textlevel.py
Normal file
0
reset_textlevel.py
Normal file
184
resetlevel.py
Normal file
184
resetlevel.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import copy
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
CN_NUM = "\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u3007\u96f6\u4e24"
|
||||||
|
|
||||||
|
CHAPTER_PATTERN = re.compile(rf"^\s*\u7b2c\s*([{CN_NUM}\d]+)\s*\u7ae0")
|
||||||
|
CN_LEVEL1_PATTERN = re.compile(rf"^\s*([{CN_NUM}]+)(?=\s|[\u3001\u3002\uff0e.])")
|
||||||
|
|
||||||
|
CASE_LIKE_PATTERN = re.compile(r"^\s*\d+(?:\.|\uff0e)(?!\d|\s|$)")
|
||||||
|
ARABIC_PATTERN = re.compile(
|
||||||
|
r"^\s*(\d+(?:\.\d+)*)(?=\s|[\u4e00-\u9fff\u3001\u3002\uff0e]|$|\.(?:\s|$))"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_valid_text_level(item: dict) -> bool:
|
||||||
|
return "text_level" in item and item.get("text_level") not in (None, "")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_heading(text: str):
|
||||||
|
text = text.strip()
|
||||||
|
if not text:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
if CASE_LIKE_PATTERN.match(text):
|
||||||
|
return "case_like", None
|
||||||
|
|
||||||
|
if CHAPTER_PATTERN.match(text):
|
||||||
|
return "level1", None
|
||||||
|
|
||||||
|
if CN_LEVEL1_PATTERN.match(text):
|
||||||
|
return "level1", None
|
||||||
|
|
||||||
|
match = ARABIC_PATTERN.match(text)
|
||||||
|
if match:
|
||||||
|
return "arabic", match.group(1)
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def arabic_depth(number: str) -> int:
|
||||||
|
return number.count(".") + 1
|
||||||
|
|
||||||
|
|
||||||
|
def parent_number(number: str) -> str | None:
|
||||||
|
parts = number.split(".")
|
||||||
|
if len(parts) <= 1:
|
||||||
|
return None
|
||||||
|
return ".".join(parts[:-1])
|
||||||
|
|
||||||
|
|
||||||
|
def collect_headings(data: list) -> list:
|
||||||
|
headings = []
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
if item.get("type") != "text":
|
||||||
|
continue
|
||||||
|
if not has_valid_text_level(item):
|
||||||
|
continue
|
||||||
|
|
||||||
|
kind, number = parse_heading(item.get("text", ""))
|
||||||
|
if kind in {"level1", "arabic", "case_like"}:
|
||||||
|
headings.append({"kind": kind, "number": number})
|
||||||
|
|
||||||
|
return headings
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_arabic_hierarchy(numbers: list[str], allow_missing_depth1_parent: bool) -> bool:
|
||||||
|
seen = set()
|
||||||
|
has_depth2 = False
|
||||||
|
has_depth3 = False
|
||||||
|
|
||||||
|
for number in numbers:
|
||||||
|
depth = arabic_depth(number)
|
||||||
|
if depth >= 2:
|
||||||
|
has_depth2 = True
|
||||||
|
if depth >= 3:
|
||||||
|
has_depth3 = True
|
||||||
|
|
||||||
|
parent = parent_number(number)
|
||||||
|
if parent is not None:
|
||||||
|
parent_depth = arabic_depth(parent)
|
||||||
|
parent_is_missing_depth1 = allow_missing_depth1_parent and parent_depth == 1
|
||||||
|
if parent not in seen and not parent_is_missing_depth1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
seen.add(number)
|
||||||
|
|
||||||
|
return has_depth2 and has_depth3
|
||||||
|
|
||||||
|
|
||||||
|
def detect_valid_mode(headings: list):
|
||||||
|
if not headings:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if any(heading["kind"] == "case_like" for heading in headings):
|
||||||
|
return None
|
||||||
|
|
||||||
|
first = headings[0]
|
||||||
|
|
||||||
|
if first["kind"] == "arabic":
|
||||||
|
numbers = [heading["number"] for heading in headings if heading["kind"] == "arabic"]
|
||||||
|
if numbers and arabic_depth(numbers[0]) == 1:
|
||||||
|
if is_valid_arabic_hierarchy(numbers, allow_missing_depth1_parent=False):
|
||||||
|
return "A"
|
||||||
|
return None
|
||||||
|
|
||||||
|
if first["kind"] == "level1":
|
||||||
|
numbers = [heading["number"] for heading in headings[1:] if heading["kind"] == "arabic"]
|
||||||
|
if not numbers:
|
||||||
|
return None
|
||||||
|
|
||||||
|
first_depth = arabic_depth(numbers[0])
|
||||||
|
if first_depth == 2 and is_valid_arabic_hierarchy(numbers, allow_missing_depth1_parent=True):
|
||||||
|
return "B"
|
||||||
|
|
||||||
|
if first_depth == 1 and is_valid_arabic_hierarchy(numbers, allow_missing_depth1_parent=False):
|
||||||
|
return "C"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
JsonContent = Any
|
||||||
|
|
||||||
|
|
||||||
|
def reset_textlevel(json_content: JsonContent) -> JsonContent:
|
||||||
|
"""
|
||||||
|
输入 JSON 内容,返回更新 text_level 后的新 JSON 内容。
|
||||||
|
|
||||||
|
json_content 可以是已经 json.load/json.loads 后的 Python 对象,
|
||||||
|
也可以是 JSON 字符串。函数不会修改原始入参。
|
||||||
|
"""
|
||||||
|
if isinstance(json_content, str):
|
||||||
|
data = json.loads(json_content)
|
||||||
|
else:
|
||||||
|
data = copy.deepcopy(json_content)
|
||||||
|
|
||||||
|
if not isinstance(data, list):
|
||||||
|
return data
|
||||||
|
|
||||||
|
headings = collect_headings(data)
|
||||||
|
mode = detect_valid_mode(headings)
|
||||||
|
|
||||||
|
if mode is None:
|
||||||
|
return data
|
||||||
|
|
||||||
|
for item in data:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
if item.get("type") != "text":
|
||||||
|
continue
|
||||||
|
if not has_valid_text_level(item):
|
||||||
|
continue
|
||||||
|
|
||||||
|
kind, number = parse_heading(item.get("text", ""))
|
||||||
|
if kind == "level1":
|
||||||
|
item["text_level"] = 1
|
||||||
|
continue
|
||||||
|
if kind != "arabic":
|
||||||
|
continue
|
||||||
|
|
||||||
|
depth = arabic_depth(number)
|
||||||
|
item["text_level"] = depth if mode in {"A", "B"} else depth + 1
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
json_file_path = r'E:\ZKYNLP\Hjunproject\project0506\kgrag\163-06A0014-B01001_发动机-维修手册_content_list.json'
|
||||||
|
with open(json_file_path, "r", encoding="utf-8") as f:
|
||||||
|
json_content = json.load(f)
|
||||||
|
result= reset_textlevel(json_content)
|
||||||
|
|
||||||
|
for ins in result[:30]:
|
||||||
|
print(ins)
|
||||||
|
# print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
799
result.json
799
result.json
@ -1,799 +0,0 @@
|
|||||||
{
|
|
||||||
"163": {
|
|
||||||
"code": "163",
|
|
||||||
"name": "JZ贱",
|
|
||||||
"level": 0,
|
|
||||||
"children":{
|
|
||||||
"00": {
|
|
||||||
"code": "00",
|
|
||||||
"name": "总体及综合保障",
|
|
||||||
"level": 1,
|
|
||||||
"children":{}},
|
|
||||||
"01": {
|
|
||||||
"code": "01",
|
|
||||||
"name": "船体结构",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "船体结构总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "主船体壳体结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "船体舱壁结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "船体甲板及平台结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "上层建筑(或甲板室)结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "舷台结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"60":{
|
|
||||||
"code": "60",
|
|
||||||
"name": "专用结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"70":{
|
|
||||||
"code": "70",
|
|
||||||
"name": "复合材料结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"80":{
|
|
||||||
"code": "80",
|
|
||||||
"name": "基座结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"02": {
|
|
||||||
"code": "02",
|
|
||||||
"name": "推进系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "推进系统总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"000":{
|
|
||||||
"code": "000",
|
|
||||||
"name": "动力系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "H能源发生系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "非H能源发生系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "动力机组及推进装置",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "功率传递系统和推进器",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "推进保障系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"006":{
|
|
||||||
"code": "006",
|
|
||||||
"name": "电动高压空气缩机组",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"007":{
|
|
||||||
"code": "007",
|
|
||||||
"name": "电动中压空气压缩机组",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"008":{
|
|
||||||
"code": "008",
|
|
||||||
"name": "956柴油机排气消音器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"009":{
|
|
||||||
"code": "009",
|
|
||||||
"name": "956柴油机进气消音器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"010":{
|
|
||||||
"code": "010",
|
|
||||||
"name": "燃气轮机燃油增压泵组",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"60":{
|
|
||||||
"code": "60",
|
|
||||||
"name": "推进监测和控制系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"03": {
|
|
||||||
"code": "03",
|
|
||||||
"name": "电力系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "电力系统总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"000":{
|
|
||||||
"code": "000",
|
|
||||||
"name": "电力系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "供电系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"003":{
|
|
||||||
"code": "003",
|
|
||||||
"name": "发电机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"004":{
|
|
||||||
"code": "004",
|
|
||||||
"name": "柴油机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"038":{
|
|
||||||
"code": "038",
|
|
||||||
"name": "1250kw柴油发电机组箱装体",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"052":{
|
|
||||||
"code": "052",
|
|
||||||
"name": "三相无刷同步发电机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"053":{
|
|
||||||
"code": "053",
|
|
||||||
"name": "电站分系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "配电系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"005":{
|
|
||||||
"code": "005",
|
|
||||||
"name": "主配电板",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"010":{
|
|
||||||
"code": "010",
|
|
||||||
"name": "配电中心",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"011":{
|
|
||||||
"code": "011",
|
|
||||||
"name": "武器配电板",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"012":{
|
|
||||||
"code": "012",
|
|
||||||
"name": "两舷电源转换控制箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"015":{
|
|
||||||
"code": "015",
|
|
||||||
"name": "事故电力网设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"022":{
|
|
||||||
"code": "022",
|
|
||||||
"name": "JPD1、JPD6型贱用分配电箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"023":{
|
|
||||||
"code": "023",
|
|
||||||
"name": "分配电箱(JDP3)",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"025":{
|
|
||||||
"code": "025",
|
|
||||||
"name": "轻型变压器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"032":{
|
|
||||||
"code": "032",
|
|
||||||
"name": "组合起动器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"033":{
|
|
||||||
"code": "033",
|
|
||||||
"name": "两舷电源转换配电装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"034":{
|
|
||||||
"code": "034",
|
|
||||||
"name": "岸电联接箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"039":{
|
|
||||||
"code": "039",
|
|
||||||
"name": "磁力起动器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"047":{
|
|
||||||
"code": "047",
|
|
||||||
"name": "磁力起动器(20A)",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"054":{
|
|
||||||
"code": "054",
|
|
||||||
"name": "配电系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"055":{
|
|
||||||
"code": "055",
|
|
||||||
"name": "岸电箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"057":{
|
|
||||||
"code": "057",
|
|
||||||
"name": "电力接触器箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "照明系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"030":{
|
|
||||||
"code": "030",
|
|
||||||
"name": "灯光管制设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"031":{
|
|
||||||
"code": "031",
|
|
||||||
"name": "低照度照明转换装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"040":{
|
|
||||||
"code": "040",
|
|
||||||
"name": "独立式应急灯集中放电装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"046":{
|
|
||||||
"code": "046",
|
|
||||||
"name": "机库照明开关箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"049":{
|
|
||||||
"code": "049",
|
|
||||||
"name": "彩灯系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"051":{
|
|
||||||
"code": "051",
|
|
||||||
"name": "通道角灯控制装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"059":{
|
|
||||||
"code": "059",
|
|
||||||
"name": "照明系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"060":{
|
|
||||||
"code": "060",
|
|
||||||
"name": "照明变压器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"061":{
|
|
||||||
"code": "061",
|
|
||||||
"name": "区域照明接触器箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"062":{
|
|
||||||
"code": "062",
|
|
||||||
"name": "独立式应急灯维护管理装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"063":{
|
|
||||||
"code": "063",
|
|
||||||
"name": "外部照明控制装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "电气监测和控制系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"002":{
|
|
||||||
"code": "002",
|
|
||||||
"name": "电力监控分系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"017":{
|
|
||||||
"code": "017",
|
|
||||||
"name": "电网检测装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"018":{
|
|
||||||
"code": "018",
|
|
||||||
"name": "直流绝缘检测装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"019":{
|
|
||||||
"code": "019",
|
|
||||||
"name": "交流绝缘检测装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"020":{
|
|
||||||
"code": "020",
|
|
||||||
"name": "电网绝缘状态综合显控仪",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"045":{
|
|
||||||
"code": "045",
|
|
||||||
"name": "电网监测接线箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "发电机组保障系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"60":{
|
|
||||||
"code": "60",
|
|
||||||
"name": "电缆敷设",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"70":{
|
|
||||||
"code": "70",
|
|
||||||
"name": "综合电缆系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"80":{
|
|
||||||
"code": "80",
|
|
||||||
"name": "电场、磁场防护系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"016":{
|
|
||||||
"code": "016",
|
|
||||||
"name": "消磁系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{
|
|
||||||
"0161":{
|
|
||||||
"code": "0161",
|
|
||||||
"name": "消磁变流装置",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}},
|
|
||||||
"0162":{
|
|
||||||
"code": "0162",
|
|
||||||
"name": "消磁接线箱",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}},
|
|
||||||
"0163":{
|
|
||||||
"code": "0163",
|
|
||||||
"name": "消磁配电板",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}},
|
|
||||||
"0164":{
|
|
||||||
"code": "0164",
|
|
||||||
"name": "其他",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"90":{
|
|
||||||
"code": "90",
|
|
||||||
"name": "专用项目03",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"013":{
|
|
||||||
"code": "013",
|
|
||||||
"name": "操舵仪",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"014":{
|
|
||||||
"code": "014",
|
|
||||||
"name": "舵传令钟",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"029":{
|
|
||||||
"code": "029",
|
|
||||||
"name": "阴极保护装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"036":{
|
|
||||||
"code": "036",
|
|
||||||
"name": "电工试验板",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"04": {
|
|
||||||
"code": "04",
|
|
||||||
"name": "作战系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "作战系统总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"000":{
|
|
||||||
"code": "000",
|
|
||||||
"name": "作战系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "信息感知",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "本贱作战指挥控制",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "编队作战指挥",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"001":{
|
|
||||||
"code": "001",
|
|
||||||
"name": "编队作战指挥系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"002":{
|
|
||||||
"code": "002",
|
|
||||||
"name": "HZBJ-2B编队指控设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"003":{
|
|
||||||
"code": "003",
|
|
||||||
"name": "编队模拟训练设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"004":{
|
|
||||||
"code": "004",
|
|
||||||
"name": "编队记录设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "武器控制及保障",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "综合通信",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"05": {
|
|
||||||
"code": "05",
|
|
||||||
"name": "船舶保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"06": {
|
|
||||||
"code": "06",
|
|
||||||
"name": "航空保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"07": {
|
|
||||||
"code": "07",
|
|
||||||
"name": "特种保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"08": {
|
|
||||||
"code": "08",
|
|
||||||
"name": "船舶装置",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"09": {
|
|
||||||
"code": "09",
|
|
||||||
"name": "专用装备/系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"10": {
|
|
||||||
"code": "10",
|
|
||||||
"name": "训练系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"11": {
|
|
||||||
"code": "11",
|
|
||||||
"name": "测试、维修试和保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"06": {
|
|
||||||
"code": "06",
|
|
||||||
"name": "汽车配件",
|
|
||||||
"level": 1,
|
|
||||||
"children":{
|
|
||||||
"A0":{
|
|
||||||
"code": "A0",
|
|
||||||
"name": "汽车主配件",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"B01001":{
|
|
||||||
"code": "B01001",
|
|
||||||
"name": "发动机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01002":{
|
|
||||||
"code": "B01002",
|
|
||||||
"name": "螺旋桨",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01003":{
|
|
||||||
"code": "B01003",
|
|
||||||
"name": "雷达",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01005":{
|
|
||||||
"code": "B01005",
|
|
||||||
"name": "声呐",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"014":{
|
|
||||||
"code": "014",
|
|
||||||
"name": "螺旋桨",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"015":{
|
|
||||||
"code": "015",
|
|
||||||
"name": "船舶主发动机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01006":{
|
|
||||||
"code": "B01006",
|
|
||||||
"name": "马达",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01007":{
|
|
||||||
"code": "B01007",
|
|
||||||
"name": "泵",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01008":{
|
|
||||||
"code": "B01008",
|
|
||||||
"name": "阀门",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01009":{
|
|
||||||
"code": "B01009",
|
|
||||||
"name": "变速箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01010":{
|
|
||||||
"code": "B01010",
|
|
||||||
"name": "液压系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01011":{
|
|
||||||
"code": "B01011",
|
|
||||||
"name": "发电机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01012":{
|
|
||||||
"code": "B01012",
|
|
||||||
"name": "空压机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01013":{
|
|
||||||
"code": "B01013",
|
|
||||||
"name": "冷却系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01014":{
|
|
||||||
"code": "B01014",
|
|
||||||
"name": "传动轴",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01015":{
|
|
||||||
"code": "B01015",
|
|
||||||
"name": "制动器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"101":{
|
|
||||||
"code":"101",
|
|
||||||
"name":"nc贱",
|
|
||||||
"level":0,
|
|
||||||
"children":{
|
|
||||||
"04": {
|
|
||||||
"code": "04",
|
|
||||||
"name": "作战系统",
|
|
||||||
"level": 1,
|
|
||||||
"children":{
|
|
||||||
"40":{"code": "40",
|
|
||||||
"name": "武器控制及保障",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"011":{"code": "011",
|
|
||||||
"name": "AAAzzz型跟踪雷达",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"122":{
|
|
||||||
"code": "122",
|
|
||||||
"name": "122舰",
|
|
||||||
"level": 0,
|
|
||||||
"children":{
|
|
||||||
"06":{
|
|
||||||
"code": "06",
|
|
||||||
"name": "设备手册",
|
|
||||||
"level": 1,
|
|
||||||
"children":{
|
|
||||||
"A0014":{
|
|
||||||
"code": "A0014",
|
|
||||||
"name": "06A0014",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"B01001":{
|
|
||||||
"code": "B01001",
|
|
||||||
"name": "发动机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01002":{
|
|
||||||
"code": "B01002",
|
|
||||||
"name": "螺旋桨",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01003":{
|
|
||||||
"code": "B01003",
|
|
||||||
"name": "雷达",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01005":{
|
|
||||||
"code": "B01005",
|
|
||||||
"name": "声呐",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01006":{
|
|
||||||
"code": "B01006",
|
|
||||||
"name": "马达",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01007":{
|
|
||||||
"code": "B01007",
|
|
||||||
"name": "泵",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01008":{
|
|
||||||
"code": "B01008",
|
|
||||||
"name": "阀门",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01009":{
|
|
||||||
"code": "B01009",
|
|
||||||
"name": "变速箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01010":{
|
|
||||||
"code": "B01010",
|
|
||||||
"name": "液压系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01011":{
|
|
||||||
"code": "B01011",
|
|
||||||
"name": "发电机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01012":{
|
|
||||||
"code": "B01012",
|
|
||||||
"name": "空压机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01013":{
|
|
||||||
"code": "B01013",
|
|
||||||
"name": "冷却系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01014":{
|
|
||||||
"code": "B01014",
|
|
||||||
"name": "传动轴",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"B01015":{
|
|
||||||
"code": "B01015",
|
|
||||||
"name": "制动器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"163": {
|
"777": {
|
||||||
"code": "163",
|
"code": "777",
|
||||||
"name": "JZ贱",
|
"name": "jz坚",
|
||||||
"level": 0,
|
"level": 0,
|
||||||
"children":{
|
"children":{
|
||||||
"00": {
|
"00": {
|
||||||
@ -579,20 +579,20 @@
|
|||||||
"level": 1,
|
"level": 1,
|
||||||
"children": {}
|
"children": {}
|
||||||
},
|
},
|
||||||
"11": {
|
"12": {
|
||||||
"code": "11",
|
"code": "12",
|
||||||
"name": "测试、维修试和保障系统",
|
"name": "测试、维修试和保障系统",
|
||||||
"level": 1,
|
"level": 1,
|
||||||
"children": {}
|
"children": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"06": {
|
"11": {
|
||||||
"code": "06",
|
"code": "11",
|
||||||
"name": "汽车配件",
|
"name": "汽车配件",
|
||||||
"level": 1,
|
"level": 1,
|
||||||
"children":{
|
"children":{
|
||||||
"A0":{
|
"A1":{
|
||||||
"code": "A0",
|
"code": "A1",
|
||||||
"name": "汽车主配件",
|
"name": "汽车主配件",
|
||||||
"level": 2,
|
"level": 2,
|
||||||
"children":{
|
"children":{
|
||||||
|
|||||||
@ -1,637 +0,0 @@
|
|||||||
{
|
|
||||||
"163": {
|
|
||||||
"code": "163",
|
|
||||||
"name": "JZ贱",
|
|
||||||
"level": 0,
|
|
||||||
"children":{
|
|
||||||
"00": {
|
|
||||||
"code": "00",
|
|
||||||
"name": "总体及综合保障",
|
|
||||||
"level": 1,
|
|
||||||
"children":{}},
|
|
||||||
"01": {
|
|
||||||
"code": "01",
|
|
||||||
"name": "船体结构",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "船体结构总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "主船体壳体结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "船体舱壁结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "船体甲板及平台结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "上层建筑(或甲板室)结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "舷台结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"60":{
|
|
||||||
"code": "60",
|
|
||||||
"name": "专用结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"70":{
|
|
||||||
"code": "70",
|
|
||||||
"name": "复合材料结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"80":{
|
|
||||||
"code": "80",
|
|
||||||
"name": "基座结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"02": {
|
|
||||||
"code": "02",
|
|
||||||
"name": "推进系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "推进系统总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"000":{
|
|
||||||
"code": "000",
|
|
||||||
"name": "动力系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "H能源发生系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "非H能源发生系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "动力机组及推进装置",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "功率传递系统和推进器",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "推进保障系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"006":{
|
|
||||||
"code": "006",
|
|
||||||
"name": "电动高压空气缩机组",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"007":{
|
|
||||||
"code": "007",
|
|
||||||
"name": "电动中压空气压缩机组",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"008":{
|
|
||||||
"code": "008",
|
|
||||||
"name": "956柴油机排气消音器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"009":{
|
|
||||||
"code": "009",
|
|
||||||
"name": "956柴油机进气消音器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"010":{
|
|
||||||
"code": "010",
|
|
||||||
"name": "燃气轮机燃油增压泵组",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"60":{
|
|
||||||
"code": "60",
|
|
||||||
"name": "推进监测和控制系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"03": {
|
|
||||||
"code": "03",
|
|
||||||
"name": "电力系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "电力系统总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"000":{
|
|
||||||
"code": "000",
|
|
||||||
"name": "电力系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "供电系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"003":{
|
|
||||||
"code": "003",
|
|
||||||
"name": "发电机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"004":{
|
|
||||||
"code": "004",
|
|
||||||
"name": "柴油机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"038":{
|
|
||||||
"code": "038",
|
|
||||||
"name": "1250kw柴油发电机组箱装体",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"052":{
|
|
||||||
"code": "052",
|
|
||||||
"name": "三相无刷同步发电机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"053":{
|
|
||||||
"code": "053",
|
|
||||||
"name": "电站分系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "配电系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"005":{
|
|
||||||
"code": "005",
|
|
||||||
"name": "主配电板",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"010":{
|
|
||||||
"code": "010",
|
|
||||||
"name": "配电中心",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"011":{
|
|
||||||
"code": "011",
|
|
||||||
"name": "武器配电板",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"012":{
|
|
||||||
"code": "012",
|
|
||||||
"name": "两舷电源转换控制箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"015":{
|
|
||||||
"code": "015",
|
|
||||||
"name": "事故电力网设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"022":{
|
|
||||||
"code": "022",
|
|
||||||
"name": "JPD1、JPD6型贱用分配电箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"023":{
|
|
||||||
"code": "023",
|
|
||||||
"name": "分配电箱(JDP3)",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"025":{
|
|
||||||
"code": "025",
|
|
||||||
"name": "轻型变压器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"032":{
|
|
||||||
"code": "032",
|
|
||||||
"name": "组合起动器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"033":{
|
|
||||||
"code": "033",
|
|
||||||
"name": "两舷电源转换配电装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"034":{
|
|
||||||
"code": "034",
|
|
||||||
"name": "岸电联接箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"039":{
|
|
||||||
"code": "039",
|
|
||||||
"name": "磁力起动器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"047":{
|
|
||||||
"code": "047",
|
|
||||||
"name": "磁力起动器(20A)",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"054":{
|
|
||||||
"code": "054",
|
|
||||||
"name": "配电系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"055":{
|
|
||||||
"code": "055",
|
|
||||||
"name": "岸电箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"057":{
|
|
||||||
"code": "057",
|
|
||||||
"name": "电力接触器箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "照明系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"030":{
|
|
||||||
"code": "030",
|
|
||||||
"name": "灯光管制设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"031":{
|
|
||||||
"code": "031",
|
|
||||||
"name": "低照度照明转换装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"040":{
|
|
||||||
"code": "040",
|
|
||||||
"name": "独立式应急灯集中放电装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"046":{
|
|
||||||
"code": "046",
|
|
||||||
"name": "机库照明开关箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"049":{
|
|
||||||
"code": "049",
|
|
||||||
"name": "彩灯系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"051":{
|
|
||||||
"code": "051",
|
|
||||||
"name": "通道角灯控制装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"059":{
|
|
||||||
"code": "059",
|
|
||||||
"name": "照明系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"060":{
|
|
||||||
"code": "060",
|
|
||||||
"name": "照明变压器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"061":{
|
|
||||||
"code": "061",
|
|
||||||
"name": "区域照明接触器箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"062":{
|
|
||||||
"code": "062",
|
|
||||||
"name": "独立式应急灯维护管理装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"063":{
|
|
||||||
"code": "063",
|
|
||||||
"name": "外部照明控制装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "电气监测和控制系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"002":{
|
|
||||||
"code": "002",
|
|
||||||
"name": "电力监控分系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"017":{
|
|
||||||
"code": "017",
|
|
||||||
"name": "电网检测装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"018":{
|
|
||||||
"code": "018",
|
|
||||||
"name": "直流绝缘检测装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"019":{
|
|
||||||
"code": "019",
|
|
||||||
"name": "交流绝缘检测装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"020":{
|
|
||||||
"code": "020",
|
|
||||||
"name": "电网绝缘状态综合显控仪",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"045":{
|
|
||||||
"code": "045",
|
|
||||||
"name": "电网监测接线箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "发电机组保障系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"60":{
|
|
||||||
"code": "60",
|
|
||||||
"name": "电缆敷设",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"70":{
|
|
||||||
"code": "70",
|
|
||||||
"name": "综合电缆系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"80":{
|
|
||||||
"code": "80",
|
|
||||||
"name": "电场、磁场防护系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"016":{
|
|
||||||
"code": "016",
|
|
||||||
"name": "消磁系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{
|
|
||||||
"0161":{
|
|
||||||
"code": "0161",
|
|
||||||
"name": "消磁变流装置",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}},
|
|
||||||
"0162":{
|
|
||||||
"code": "0162",
|
|
||||||
"name": "消磁接线箱",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}},
|
|
||||||
"0163":{
|
|
||||||
"code": "0163",
|
|
||||||
"name": "消磁配电板",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}},
|
|
||||||
"0164":{
|
|
||||||
"code": "0164",
|
|
||||||
"name": "其他",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"90":{
|
|
||||||
"code": "90",
|
|
||||||
"name": "专用项目03",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"013":{
|
|
||||||
"code": "013",
|
|
||||||
"name": "操舵仪",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"014":{
|
|
||||||
"code": "014",
|
|
||||||
"name": "舵传令钟",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"029":{
|
|
||||||
"code": "029",
|
|
||||||
"name": "阴极保护装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"036":{
|
|
||||||
"code": "036",
|
|
||||||
"name": "电工试验板",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"04": {
|
|
||||||
"code": "04",
|
|
||||||
"name": "作战系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "作战系统总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"000":{
|
|
||||||
"code": "000",
|
|
||||||
"name": "作战系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "信息感知",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "本贱作战指挥控制",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "编队作战指挥",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"001":{
|
|
||||||
"code": "001",
|
|
||||||
"name": "编队作战指挥系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"002":{
|
|
||||||
"code": "002",
|
|
||||||
"name": "HZBJ-2B编队指控设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"003":{
|
|
||||||
"code": "003",
|
|
||||||
"name": "编队模拟训练设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"004":{
|
|
||||||
"code": "004",
|
|
||||||
"name": "编队记录设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "武器控制及保障",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "综合通信",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"05": {
|
|
||||||
"code": "05",
|
|
||||||
"name": "船舶保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"06": {
|
|
||||||
"code": "06",
|
|
||||||
"name": "航空保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"07": {
|
|
||||||
"code": "07",
|
|
||||||
"name": "特种保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"08": {
|
|
||||||
"code": "08",
|
|
||||||
"name": "船舶装置",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"09": {
|
|
||||||
"code": "09",
|
|
||||||
"name": "专用装备/系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"10": {
|
|
||||||
"code": "10",
|
|
||||||
"name": "训练系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"11": {
|
|
||||||
"code": "11",
|
|
||||||
"name": "测试、维修试和保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"06": {
|
|
||||||
"code": "06",
|
|
||||||
"name": "汽车配件",
|
|
||||||
"level": 1,
|
|
||||||
"children":{
|
|
||||||
"A0":{
|
|
||||||
"code": "A0",
|
|
||||||
"name": "汽车主配件",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"014":{
|
|
||||||
"code": "014",
|
|
||||||
"name": "螺旋桨",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"015":{
|
|
||||||
"code": "015",
|
|
||||||
"name": "船舶主发动机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"101":{
|
|
||||||
"code":"101",
|
|
||||||
"name":"nc贱",
|
|
||||||
"level":0,
|
|
||||||
"children":{
|
|
||||||
"04": {
|
|
||||||
"code": "04",
|
|
||||||
"name": "作战系统",
|
|
||||||
"level": 1,
|
|
||||||
"children":{
|
|
||||||
"40":{"code": "40",
|
|
||||||
"name": "武器控制及保障",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"011":{"code": "011",
|
|
||||||
"name": "AAAzzz型跟踪雷达",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
613
tree_data_1.json
613
tree_data_1.json
@ -1,613 +0,0 @@
|
|||||||
{
|
|
||||||
"163": {
|
|
||||||
"code": "163",
|
|
||||||
"name": "JZ贱",
|
|
||||||
"level": 0,
|
|
||||||
"children":{
|
|
||||||
"00": {
|
|
||||||
"code": "00",
|
|
||||||
"name": "总体及综合保障",
|
|
||||||
"level": 1,
|
|
||||||
"children":{}},
|
|
||||||
"01": {
|
|
||||||
"code": "01",
|
|
||||||
"name": "船体结构",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "船体结构总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "主船体壳体结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "船体舱壁结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "船体甲板及平台结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "上层建筑(或甲板室)结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "舷台结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"60":{
|
|
||||||
"code": "60",
|
|
||||||
"name": "专用结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"70":{
|
|
||||||
"code": "70",
|
|
||||||
"name": "复合材料结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"80":{
|
|
||||||
"code": "80",
|
|
||||||
"name": "基座结构",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"02": {
|
|
||||||
"code": "02",
|
|
||||||
"name": "推进系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "推进系统总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"000":{
|
|
||||||
"code": "000",
|
|
||||||
"name": "动力系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "H能源发生系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "非H能源发生系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "动力机组及推进装置",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "功率传递系统和推进器",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "推进保障系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"006":{
|
|
||||||
"code": "006",
|
|
||||||
"name": "电动高压空气缩机组",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"007":{
|
|
||||||
"code": "007",
|
|
||||||
"name": "电动中压空气压缩机组",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"008":{
|
|
||||||
"code": "008",
|
|
||||||
"name": "956柴油机排气消音器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"009":{
|
|
||||||
"code": "009",
|
|
||||||
"name": "956柴油机进气消音器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"010":{
|
|
||||||
"code": "010",
|
|
||||||
"name": "燃气轮机燃油增压泵组",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"60":{
|
|
||||||
"code": "60",
|
|
||||||
"name": "推进监测和控制系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"03": {
|
|
||||||
"code": "03",
|
|
||||||
"name": "电力系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "电力系统总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"000":{
|
|
||||||
"code": "000",
|
|
||||||
"name": "电力系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "供电系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"003":{
|
|
||||||
"code": "003",
|
|
||||||
"name": "发电机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"004":{
|
|
||||||
"code": "004",
|
|
||||||
"name": "柴油机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"038":{
|
|
||||||
"code": "038",
|
|
||||||
"name": "1250kw柴油发电机组箱装体",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"052":{
|
|
||||||
"code": "052",
|
|
||||||
"name": "三相无刷同步发电机",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"053":{
|
|
||||||
"code": "053",
|
|
||||||
"name": "电站分系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "配电系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"005":{
|
|
||||||
"code": "005",
|
|
||||||
"name": "主配电板",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"010":{
|
|
||||||
"code": "010",
|
|
||||||
"name": "配电中心",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"011":{
|
|
||||||
"code": "011",
|
|
||||||
"name": "武器配电板",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"012":{
|
|
||||||
"code": "012",
|
|
||||||
"name": "两舷电源转换控制箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"015":{
|
|
||||||
"code": "015",
|
|
||||||
"name": "事故电力网设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"022":{
|
|
||||||
"code": "022",
|
|
||||||
"name": "JPD1、JPD6型贱用分配电箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"023":{
|
|
||||||
"code": "023",
|
|
||||||
"name": "分配电箱(JDP3)",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"025":{
|
|
||||||
"code": "025",
|
|
||||||
"name": "轻型变压器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"032":{
|
|
||||||
"code": "032",
|
|
||||||
"name": "组合起动器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"033":{
|
|
||||||
"code": "033",
|
|
||||||
"name": "两舷电源转换配电装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"034":{
|
|
||||||
"code": "034",
|
|
||||||
"name": "岸电联接箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"039":{
|
|
||||||
"code": "039",
|
|
||||||
"name": "磁力起动器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"047":{
|
|
||||||
"code": "047",
|
|
||||||
"name": "磁力起动器(20A)",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"054":{
|
|
||||||
"code": "054",
|
|
||||||
"name": "配电系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"055":{
|
|
||||||
"code": "055",
|
|
||||||
"name": "岸电箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"057":{
|
|
||||||
"code": "057",
|
|
||||||
"name": "电力接触器箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "照明系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"030":{
|
|
||||||
"code": "030",
|
|
||||||
"name": "灯光管制设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"031":{
|
|
||||||
"code": "031",
|
|
||||||
"name": "低照度照明转换装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"040":{
|
|
||||||
"code": "040",
|
|
||||||
"name": "独立式应急灯集中放电装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"046":{
|
|
||||||
"code": "046",
|
|
||||||
"name": "机库照明开关箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"049":{
|
|
||||||
"code": "049",
|
|
||||||
"name": "彩灯系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"051":{
|
|
||||||
"code": "051",
|
|
||||||
"name": "通道角灯控制装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"059":{
|
|
||||||
"code": "059",
|
|
||||||
"name": "照明系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"060":{
|
|
||||||
"code": "060",
|
|
||||||
"name": "照明变压器",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"061":{
|
|
||||||
"code": "061",
|
|
||||||
"name": "区域照明接触器箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"062":{
|
|
||||||
"code": "062",
|
|
||||||
"name": "独立式应急灯维护管理装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"063":{
|
|
||||||
"code": "063",
|
|
||||||
"name": "外部照明控制装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "电气监测和控制系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"002":{
|
|
||||||
"code": "002",
|
|
||||||
"name": "电力监控分系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"017":{
|
|
||||||
"code": "017",
|
|
||||||
"name": "电网检测装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"018":{
|
|
||||||
"code": "018",
|
|
||||||
"name": "直流绝缘检测装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"019":{
|
|
||||||
"code": "019",
|
|
||||||
"name": "交流绝缘检测装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"020":{
|
|
||||||
"code": "020",
|
|
||||||
"name": "电网绝缘状态综合显控仪",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"045":{
|
|
||||||
"code": "045",
|
|
||||||
"name": "电网监测接线箱",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "发电机组保障系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"60":{
|
|
||||||
"code": "60",
|
|
||||||
"name": "电缆敷设",
|
|
||||||
"level": 2,
|
|
||||||
"children":{}
|
|
||||||
},
|
|
||||||
"70":{
|
|
||||||
"code": "70",
|
|
||||||
"name": "综合电缆系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"80":{
|
|
||||||
"code": "80",
|
|
||||||
"name": "电场、磁场防护系统",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"016":{
|
|
||||||
"code": "016",
|
|
||||||
"name": "消磁系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{
|
|
||||||
"0161":{
|
|
||||||
"code": "0161",
|
|
||||||
"name": "消磁变流装置",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}},
|
|
||||||
"0162":{
|
|
||||||
"code": "0162",
|
|
||||||
"name": "消磁接线箱",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}},
|
|
||||||
"0163":{
|
|
||||||
"code": "0163",
|
|
||||||
"name": "消磁配电板",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}},
|
|
||||||
"0164":{
|
|
||||||
"code": "0164",
|
|
||||||
"name": "其他",
|
|
||||||
"level": 4,
|
|
||||||
"children":{}}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"90":{
|
|
||||||
"code": "90",
|
|
||||||
"name": "专用项目03",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"013":{
|
|
||||||
"code": "013",
|
|
||||||
"name": "操舵仪",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"014":{
|
|
||||||
"code": "014",
|
|
||||||
"name": "舵传令钟",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"029":{
|
|
||||||
"code": "029",
|
|
||||||
"name": "阴极保护装置",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"036":{
|
|
||||||
"code": "036",
|
|
||||||
"name": "电工试验板",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"04": {
|
|
||||||
"code": "04",
|
|
||||||
"name": "作战系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {
|
|
||||||
"00":{
|
|
||||||
"code": "00",
|
|
||||||
"name": "作战系统总论",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"000":{
|
|
||||||
"code": "000",
|
|
||||||
"name": "作战系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"10":{
|
|
||||||
"code": "10",
|
|
||||||
"name": "信息感知",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
},
|
|
||||||
"20":{
|
|
||||||
"code": "20",
|
|
||||||
"name": "本贱作战指挥控制",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"30":{
|
|
||||||
"code": "30",
|
|
||||||
"name": "编队作战指挥",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"001":{
|
|
||||||
"code": "001",
|
|
||||||
"name": "编队作战指挥系统",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"002":{
|
|
||||||
"code": "002",
|
|
||||||
"name": "HZBJ-2B编队指控设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"003":{
|
|
||||||
"code": "003",
|
|
||||||
"name": "编队模拟训练设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}},
|
|
||||||
"004":{
|
|
||||||
"code": "004",
|
|
||||||
"name": "编队记录设备",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"40":{
|
|
||||||
"code": "40",
|
|
||||||
"name": "武器控制及保障",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"50":{
|
|
||||||
"code": "50",
|
|
||||||
"name": "综合通信",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"05": {
|
|
||||||
"code": "05",
|
|
||||||
"name": "船舶保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"06": {
|
|
||||||
"code": "06",
|
|
||||||
"name": "航空保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"07": {
|
|
||||||
"code": "07",
|
|
||||||
"name": "特种保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"08": {
|
|
||||||
"code": "08",
|
|
||||||
"name": "船舶装置",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"09": {
|
|
||||||
"code": "09",
|
|
||||||
"name": "专用装备/系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"10": {
|
|
||||||
"code": "10",
|
|
||||||
"name": "训练系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
},
|
|
||||||
"11": {
|
|
||||||
"code": "11",
|
|
||||||
"name": "测试、维修试和保障系统",
|
|
||||||
"level": 1,
|
|
||||||
"children": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"101":{
|
|
||||||
"code":"101",
|
|
||||||
"name":"nc贱",
|
|
||||||
"level":0,
|
|
||||||
"children":{
|
|
||||||
"04": {
|
|
||||||
"code": "04",
|
|
||||||
"name": "作战系统",
|
|
||||||
"level": 1,
|
|
||||||
"children":{
|
|
||||||
"40":{"code": "40",
|
|
||||||
"name": "武器控制及保障",
|
|
||||||
"level": 2,
|
|
||||||
"children":{
|
|
||||||
"011":{"code": "011",
|
|
||||||
"name": "AAAzzz型跟踪雷达",
|
|
||||||
"level": 3,
|
|
||||||
"children":{}}}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
408
wxproject.json
408
wxproject.json
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user