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
|
||||
import uuid
|
||||
import json
|
||||
from fileparse_util import process_document
|
||||
from fileparse_util import process_document, extract_image_caption
|
||||
# import hanlp
|
||||
from itertools import groupby
|
||||
import requests
|
||||
@ -57,7 +57,7 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
import base64
|
||||
from extract_excel_node_relation import get_excel_node_relation
|
||||
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 kg_build.extract_filtertext import get_filtertext_node_relation
|
||||
import os
|
||||
@ -92,7 +92,7 @@ from default_ontology_config import (
|
||||
get_relationships,
|
||||
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()
|
||||
DATA_DIR = "/app/files"
|
||||
# ================== 全局资源容器 ==================
|
||||
@ -102,7 +102,6 @@ _resources: Dict[str, Any] = {
|
||||
"async_driver": None,
|
||||
"embedder": None,
|
||||
}
|
||||
|
||||
# 图谱检索和索引生成
|
||||
try:
|
||||
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)
|
||||
VLM_SEMAPHORE = asyncio.Semaphore(max(1, VLM_CONCURRENCY))
|
||||
|
||||
setup_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -1739,7 +1739,7 @@ async def kg_build(request: KGRequest):
|
||||
except Exception as e:
|
||||
logger.warning(f"保存 kg_build 配置缓存失败: {e}")
|
||||
|
||||
# 后续进程启动逻辑保持不变 ...
|
||||
# 后续进程启动逻辑保持不变 ...https://gitea.zkzdht.com/Ascend/htknow
|
||||
cancel_flag_path = f"/tmp/kg_cancel_{task_id}"
|
||||
if os.path.exists(cancel_flag_path):
|
||||
os.remove(cancel_flag_path)
|
||||
@ -2028,6 +2028,105 @@ class RequestWrapper(BaseModel):
|
||||
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")
|
||||
@ -2133,7 +2232,7 @@ async def split_result(
|
||||
slices = []
|
||||
for ins in contents:
|
||||
slices.append({"content": ins, "positions": []})
|
||||
result_data = {"slices": slices, "images": {}}
|
||||
result_data = {"slices": slices, "images": {},"summary":''}
|
||||
elif suffix in [".xlsx"]:
|
||||
# converter.convert(str(temp_input_path), output_dir=str(DATA_DIR))
|
||||
# temp_base_name = temp_input_path.stem # 如 tmp2bb0l4cn
|
||||
@ -2221,96 +2320,6 @@ class GraphSearchRequest(BaseModel):
|
||||
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 索引创建接口
|
||||
# ========== ========== ========== ========== ========== ==========
|
||||
@ -2921,5 +2930,3 @@ if __name__ == "__main__":
|
||||
|
||||
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()
|
||||
|
||||
# ── 配置(与 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_PASSWORD = os.getenv("NEO4J_PASSWORD", "zdht123@")
|
||||
MINERU_INSTANCES = [
|
||||
# ("http://192.168.0.64:18000/analyze-pdf"),
|
||||
#("http://192.168.0.111:9977/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"}
|
||||
|
||||
|
||||
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 = {
|
||||
"model": "46-qwen3.5-35B",
|
||||
"base_url": "http://192.168.0.46:59800/v1",
|
||||
"model": "46-qwen3.6-35B",
|
||||
"base_url": "http://192.168.2.12:59800/v1",
|
||||
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
|
||||
"temperature": 0.1,
|
||||
"max_tokens": 55096,
|
||||
"max_tokens": 25096,
|
||||
"timeout": 30
|
||||
}
|
||||
|
||||
# ==================== 嵌入模型配置 ====================
|
||||
EMBEDDING_CONFIG = {
|
||||
"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",
|
||||
"base_url_v2" : "http://192.168.0.46:59700/v1",
|
||||
}
|
||||
|
||||
# ==================== Neo4j 图数据库配置 ====================
|
||||
NEO4J_CONFIG = {
|
||||
# 优先读取环境变量,如果没有则使用默认值
|
||||
# 注意:本地开发通常用 localhost,部署时用服务名 neo4j
|
||||
"uri": "neo4j://192.168.0.46:57687",
|
||||
"uri": "neo4j://192.168.2.12:57687",
|
||||
"username": "neo4j",
|
||||
"password": "zdht123@",
|
||||
"database": "neo4j", # 新增 database 配置
|
||||
@ -51,15 +50,15 @@ SEARCH_CONFIG = {
|
||||
"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 = {
|
||||
"indexing_url" : "http://192.168.0.46:59085/neo4j_indexing",
|
||||
"prefix_url" : "http://192.168.0.46:59085",
|
||||
"indexing_url" : "http://192.168.2.12:59085/neo4j_indexing",
|
||||
"prefix_url" : "http://192.168.2.12:59085",
|
||||
}
|
||||
|
||||
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:9978/analyze-pdf",
|
||||
#"http://192.168.0.111:9979/analyze-pdf",
|
||||
@ -67,7 +66,7 @@ API_URLS = [
|
||||
# "http://192.168.0.111:9975/analyze-pdf",
|
||||
]
|
||||
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:9978/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",
|
||||
]
|
||||
|
||||
SHIP_MODEL_NAME = "E:\ZKYNLP\Hjunproject\project0506\kgrag\ship_xinghao.json"
|
||||
TREE_JSON_PATH = r"E:\ZKYNLP\Hjunproject\project0506\kgrag\tree_data.json"
|
||||
root@70964b104c5b:/app# pwd
|
||||
/app
|
||||
root@70964b104c5b:/app# vim config.py
|
||||
root@70964b104c5b:/app# cat config.py
|
||||
# app/model_config.py
|
||||
|
||||
import os
|
||||
|
||||
# ==================== 大模型配置 ====================
|
||||
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",
|
||||
]
|
||||
# ==================== 切片图片处理配置 ====================
|
||||
MAX_SPLIT_IMAGE_INPUT_BYTES = 50 * 1024 * 1024
|
||||
MAX_SPLIT_IMAGE_BYTES = 10 * 1024 * 1024
|
||||
MAX_SPLIT_IMAGE_BASE64_CHARS = (MAX_SPLIT_IMAGE_INPUT_BYTES * 4 // 3) + 4096
|
||||
SPLIT_IMAGE_COMPRESS_THRESHOLD_BYTES = MAX_SPLIT_IMAGE_BYTES
|
||||
SPLIT_IMAGE_MAX_DIMENSION = 4096
|
||||
SPLIT_IMAGE_JPEG_QUALITY = 88
|
||||
OCR_CONCURRENCY = 1
|
||||
VLM_CONCURRENCY = 2
|
||||
|
||||
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)
|
||||
doc_result = asyncio.run(_run_process_doc())
|
||||
if doc_result is not None:
|
||||
content_list, _ = doc_result
|
||||
content_list, images_dict, md_content= doc_result
|
||||
|
||||
else:
|
||||
if not os.path.exists(file_path):
|
||||
|
||||
@ -13,7 +13,6 @@ import json
|
||||
from modelsAPI.model_api import OpenaiAPI
|
||||
from openai import OpenAI
|
||||
from typing import Dict, List, Any
|
||||
from config import TREE_JSON_PATH
|
||||
|
||||
app = FastAPI(title="PDF Upload Service")
|
||||
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 归类,彻底避免环状数据导致的死循环。
|
||||
默认只展开第一艘舰艇的系统/子系统,其它舰艇 children 置空(前端可点击按需加载)。
|
||||
"""
|
||||
|
||||
# 1. 辅助函数:判断标签是否包含关键字
|
||||
def has_label(node, keyword):
|
||||
labels = node.get("labels", [])
|
||||
@ -541,8 +539,8 @@ def fetch_graph_sample1(driver):
|
||||
# 文件名处理方法
|
||||
# =============================
|
||||
|
||||
# BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
# TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
|
||||
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:
|
||||
@ -834,7 +832,7 @@ def get_entity(filename):
|
||||
if __name__ == "__main__":
|
||||
# 阀控蓄电池脉冲式快速充电装置
|
||||
test_list = [
|
||||
"163-06A0015-B01001_发动机-维修手册.pdf",
|
||||
"777-11A1014-B01001_发动机-维修手册.pdf",
|
||||
|
||||
]
|
||||
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 asyncio
|
||||
import logging
|
||||
from typing import Optional, Tuple, Dict
|
||||
from typing import Any, Optional, Tuple, Dict
|
||||
import aiofiles # pip install aiofiles
|
||||
|
||||
from config import SEARCH_DIR,IMAGE_DIR
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -16,7 +16,7 @@ async def find_and_read_content_list(
|
||||
encoding: str = 'utf-8'
|
||||
) -> Tuple[Optional[list], Optional[str]]:
|
||||
"""根据原始文件名查找并读取对应的 _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"
|
||||
logger.info(f"正在查找文件: {target_filename}")
|
||||
|
||||
@ -44,6 +44,93 @@ async def find_and_read_content_list(
|
||||
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:
|
||||
"""从文档数据中提取所有 .jpg 图片的纯文件名。纯 CPU 操作,不需要 async。"""
|
||||
result_set = set()
|
||||
@ -103,10 +190,9 @@ async def encode_images_to_base64(
|
||||
|
||||
async def process_document(
|
||||
input_file_name: str,
|
||||
search_dir: Optional[str] = "/app/mineru_output",
|
||||
local_image_dir: Optional[str] = None,
|
||||
concurrency: int = 32,
|
||||
) -> Optional[Tuple[list, Dict[str, str]]]:
|
||||
) -> Optional[Tuple[list, Dict[str, str], Optional[str]]]:
|
||||
"""
|
||||
异步处理文档:查找 content_list.json,提取图片引用,并发编码为 base64。
|
||||
|
||||
@ -117,9 +203,10 @@ async def process_document(
|
||||
concurrency: 图片编码的并发数上限,默认 32
|
||||
|
||||
Returns:
|
||||
成功: (content_list, images_dict)
|
||||
成功: (content_list, images_dict, md_content)
|
||||
失败: None
|
||||
"""
|
||||
search_dir = SEARCH_DIR
|
||||
content, content_path = await find_and_read_content_list(search_dir, input_file_name)
|
||||
if content is None:
|
||||
logger.error("❌ 未找到指定的 _content_list.json 文件。")
|
||||
@ -128,6 +215,13 @@ async def process_document(
|
||||
logger.info(f"✅ 找到文件: {content_path}")
|
||||
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:
|
||||
local_image_dir = os.path.join(os.path.dirname(content_path), "images")
|
||||
logger.info(f"图片目录: {local_image_dir}")
|
||||
@ -140,20 +234,19 @@ async def process_document(
|
||||
)
|
||||
logger.info(f"成功编码 {len(images_dict)} 张图片")
|
||||
|
||||
return content, images_dict
|
||||
return content, images_dict, md_content
|
||||
|
||||
|
||||
async def main():
|
||||
result = await process_document(
|
||||
input_file_name="163-06A0014-B01001_发动机-维修手册.pdf",
|
||||
search_dir="/app/mineru_output",
|
||||
concurrency=32,
|
||||
)
|
||||
|
||||
if result is None:
|
||||
return 1
|
||||
|
||||
content, images_dict = result
|
||||
content, images_dict, md_content = result
|
||||
print(f"\n=== 处理完成 ===")
|
||||
print(f"文档段落数: {len(content)}")
|
||||
print(f"图片数量: {len(images_dict)}")
|
||||
@ -164,5 +257,12 @@ async def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
exit(exit_code)
|
||||
result = asyncio.run(
|
||||
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
|
||||
from typing import Dict, List, Any, Optional
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Set
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXCLUDED_RESULT_NODE_LABELS = {"维修工作", "操作程序", "操作使用"}
|
||||
_FILTERED_OUT = object()
|
||||
|
||||
|
||||
class SimpleNode:
|
||||
"""
|
||||
@ -537,6 +541,110 @@ def format_entry_nodes_as_results(entry_nodes: dict) -> List[Dict[str, Any]]:
|
||||
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:
|
||||
"""
|
||||
@ -649,8 +757,9 @@ def serialize_graph_for_llm(graph_response: dict) -> str:
|
||||
- results(任意查询返回值:聚合/记录/字符串等)
|
||||
"""
|
||||
data = graph_response.get("data", {})
|
||||
nodes = data.get("nodes", [])
|
||||
results = data.get("results", [])
|
||||
# 这里的过滤只影响 results 字段里的文本展示,不改动接口返回的 data.nodes/data.links。
|
||||
nodes = _filter_result_nodes_for_output(data.get("nodes", []))
|
||||
results = _filter_results_for_output(data.get("results", []))
|
||||
|
||||
output_lines = []
|
||||
|
||||
@ -695,3 +804,4 @@ def serialize_graph_for_llm(graph_response: dict) -> str:
|
||||
return "图谱查询完成,但未返回有效数据。"
|
||||
|
||||
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
|
||||
|
||||
# 模块级单例客户端,避免每次调用都创建新连接
|
||||
_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:
|
||||
|
||||
@staticmethod
|
||||
@ -148,7 +160,79 @@ class OpenaiAPI:
|
||||
extra_body={"chat_template_kwargs": {"enable_thinking": False}}
|
||||
)
|
||||
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
|
||||
async def open_api_chat_async_json(query: str, model: str = None, temperature: float = None) -> str:
|
||||
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 节点的分页大小
|
||||
|
||||
# 全局索引名称(覆盖所有节点标签)
|
||||
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_AUTH = (
|
||||
NEO4J_CONFIG["username"],
|
||||
@ -55,6 +57,8 @@ OLD_INDEX_NAMES: List[str] = [
|
||||
VECTOR_INDEX_NAME,
|
||||
FULLTEXT_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")
|
||||
|
||||
|
||||
# --------- 创建全局混合索引(覆盖所有节点标签) ---------
|
||||
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:
|
||||
"""打印当前 Neo4j 实例的版本和版本类型。"""
|
||||
with driver.session() as session:
|
||||
@ -407,12 +795,13 @@ build_hyrid_indexes = build_hybrid_indexes
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 可以通过命令行参数或环境变量控制是否强制刷新
|
||||
import sys
|
||||
|
||||
result = build_hybrid_indexes(
|
||||
drop_old=True
|
||||
)
|
||||
force_refresh = "--force" in sys.argv or os.getenv("FORCE_REFRESH", "false").lower() == "true"
|
||||
|
||||
result = create_all_indexes(force_refresh=force_refresh)
|
||||
if result["success"]:
|
||||
logger.info("混合索引创建成功")
|
||||
logger.info(f"索引创建成功完成(模式: {result['mode']})")
|
||||
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": {
|
||||
"code": "163",
|
||||
"name": "JZ贱",
|
||||
"777": {
|
||||
"code": "777",
|
||||
"name": "jz坚",
|
||||
"level": 0,
|
||||
"children":{
|
||||
"00": {
|
||||
@ -579,20 +579,20 @@
|
||||
"level": 1,
|
||||
"children": {}
|
||||
},
|
||||
"11": {
|
||||
"code": "11",
|
||||
"12": {
|
||||
"code": "12",
|
||||
"name": "测试、维修试和保障系统",
|
||||
"level": 1,
|
||||
"children": {}
|
||||
}
|
||||
},
|
||||
"06": {
|
||||
"code": "06",
|
||||
"11": {
|
||||
"code": "11",
|
||||
"name": "汽车配件",
|
||||
"level": 1,
|
||||
"children":{
|
||||
"A0":{
|
||||
"code": "A0",
|
||||
"A1":{
|
||||
"code": "A1",
|
||||
"name": "汽车主配件",
|
||||
"level": 2,
|
||||
"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