Compare commits
No commits in common. "main" and "defeng-patch" have entirely different histories.
main
...
defeng-pat
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, extract_image_caption
|
||||
from fileparse_util import process_document
|
||||
# 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,safe_original_filename,image_base64_to_data_url,extract_image_text,prepare_split_image
|
||||
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 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,VLM_CONCURRENCY,SEARCH_DIR
|
||||
from config import NEO4J_CONFIG,URL,API_URLS,API_OTHER_URLS
|
||||
converter = Doc2PDF()
|
||||
DATA_DIR = "/app/files"
|
||||
# ================== 全局资源容器 ==================
|
||||
@ -102,6 +102,7 @@ _resources: Dict[str, Any] = {
|
||||
"async_driver": None,
|
||||
"embedder": None,
|
||||
}
|
||||
|
||||
# 图谱检索和索引生成
|
||||
try:
|
||||
from graph_search.graph_service import GraphService
|
||||
@ -166,7 +167,6 @@ 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,105 +2028,6 @@ 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")
|
||||
@ -2232,7 +2133,7 @@ async def split_result(
|
||||
slices = []
|
||||
for ins in contents:
|
||||
slices.append({"content": ins, "positions": []})
|
||||
result_data = {"slices": slices, "images": {},"summary":''}
|
||||
result_data = {"slices": slices, "images": {}}
|
||||
elif suffix in [".xlsx"]:
|
||||
# converter.convert(str(temp_input_path), output_dir=str(DATA_DIR))
|
||||
# temp_base_name = temp_input_path.stem # 如 tmp2bb0l4cn
|
||||
@ -2320,6 +2221,96 @@ 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 索引创建接口
|
||||
# ========== ========== ========== ========== ========== ==========
|
||||
@ -2930,3 +2921,5 @@ 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://192.168.2.12:57687")
|
||||
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
|
||||
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.2.12:59988/analyze-pdf/"),
|
||||
("http://192.168.1.64:18000/analyze-pdf/"),
|
||||
]
|
||||
PREFIX_URL = os.getenv("PREFIX_URL", "http://192.168.2.12:59085")
|
||||
PREFIX_URL = os.getenv("PREFIX_URL", "http://192.168.1.64:9085")
|
||||
|
||||
SUPPORTED_EXT = {".pdf", ".md", ".docx", ".xlsx"}
|
||||
|
||||
|
||||
914
chunk_text.py
914
chunk_text.py
File diff suppressed because it is too large
Load Diff
250
chunkincreas.py
250
chunkincreas.py
@ -1,250 +0,0 @@
|
||||
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())
|
||||
115
config.py
115
config.py
@ -4,26 +4,27 @@ import os
|
||||
|
||||
# ==================== 大模型配置 ====================
|
||||
LLM_CONFIG = {
|
||||
"model": "46-qwen3.6-35B",
|
||||
"base_url": "http://192.168.2.12:59800/v1",
|
||||
"model": "46-qwen3.5-35B",
|
||||
"base_url": "http://192.168.0.46:59800/v1",
|
||||
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
|
||||
"temperature": 0.1,
|
||||
"max_tokens": 25096,
|
||||
"max_tokens": 55096,
|
||||
"timeout": 30
|
||||
}
|
||||
|
||||
# ==================== 嵌入模型配置 ====================
|
||||
EMBEDDING_CONFIG = {
|
||||
"model": "bge-m3",
|
||||
"base_url": "http://192.168.2.12:59700/v1/embeddings",
|
||||
"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.2.12:57687",
|
||||
"uri": "neo4j://192.168.0.46:57687",
|
||||
"username": "neo4j",
|
||||
"password": "zdht123@",
|
||||
"database": "neo4j", # 新增 database 配置
|
||||
@ -50,15 +51,15 @@ SEARCH_CONFIG = {
|
||||
"node_fetch_page" : 10000,
|
||||
}
|
||||
|
||||
INDEXING_ULR = "http://192.168.2.12:59085/neo4j_indexing"
|
||||
INDEXING_ULR = "http://192.168.0.46:59085/neo4j_indexing"
|
||||
|
||||
URL = {
|
||||
"indexing_url" : "http://192.168.2.12:59085/neo4j_indexing",
|
||||
"prefix_url" : "http://192.168.2.12:59085",
|
||||
"indexing_url" : "http://192.168.0.46:59085/neo4j_indexing",
|
||||
"prefix_url" : "http://192.168.0.46:59085",
|
||||
}
|
||||
|
||||
API_URLS = [
|
||||
"http://192.168.2.12:59988/analyze-pdf",
|
||||
"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",
|
||||
@ -66,7 +67,7 @@ API_URLS = [
|
||||
# "http://192.168.0.111:9975/analyze-pdf",
|
||||
]
|
||||
API_OTHER_URLS = [
|
||||
"http://192.168.2.12:59988/analyze-otherfile",
|
||||
"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",
|
||||
@ -74,18 +75,88 @@ API_OTHER_URLS = [
|
||||
# "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 = "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",
|
||||
]
|
||||
|
||||
SHIP_MODEL_NAME = "/app/ship_xinghao.json"
|
||||
TREE_JSON_PATH = "/app/tree_data.json"
|
||||
# ==================== mineru和kgrag目录映射地址 ====================
|
||||
SEARCH_DIR ="/app/mineru_output"
|
||||
IMAGE_DIR = "/app/mineru_output/images"
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
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} 的记录")
|
||||
@ -1,41 +0,0 @@
|
||||
[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, images_dict, md_content= doc_result
|
||||
content_list, _ = doc_result
|
||||
|
||||
else:
|
||||
if not os.path.exists(file_path):
|
||||
|
||||
@ -13,6 +13,7 @@ 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")
|
||||
@ -88,6 +89,7 @@ def build_tree(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
|
||||
不再依赖 links 的连线,直接根据 labels 归类,彻底避免环状数据导致的死循环。
|
||||
默认只展开第一艘舰艇的系统/子系统,其它舰艇 children 置空(前端可点击按需加载)。
|
||||
"""
|
||||
|
||||
# 1. 辅助函数:判断标签是否包含关键字
|
||||
def has_label(node, keyword):
|
||||
labels = node.get("labels", [])
|
||||
@ -539,8 +541,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:
|
||||
@ -832,7 +834,7 @@ def get_entity(filename):
|
||||
if __name__ == "__main__":
|
||||
# 阀控蓄电池脉冲式快速充电装置
|
||||
test_list = [
|
||||
"777-11A1014-B01001_发动机-维修手册.pdf",
|
||||
"163-06A0015-B01001_发动机-维修手册.pdf",
|
||||
|
||||
]
|
||||
for i in test_list:
|
||||
|
||||
821
filename_proceess_and_kgquery_0417.py
Normal file
821
filename_proceess_and_kgquery_0417.py
Normal file
@ -0,0 +1,821 @@
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
|
||||
815
filename_proceess_and_kgquery_hdf.py
Normal file
815
filename_proceess_and_kgquery_hdf.py
Normal file
@ -0,0 +1,815 @@
|
||||
"""
|
||||
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 Any, Optional, Tuple, Dict
|
||||
from typing import 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(os.path.basename(original_filename))
|
||||
file_name, _ = os.path.splitext(original_filename)
|
||||
target_filename = f"{file_name}_content_list.json"
|
||||
logger.info(f"正在查找文件: {target_filename}")
|
||||
|
||||
@ -44,93 +44,6 @@ 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()
|
||||
@ -190,9 +103,10 @@ 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[str]]]:
|
||||
) -> Optional[Tuple[list, Dict[str, str]]]:
|
||||
"""
|
||||
异步处理文档:查找 content_list.json,提取图片引用,并发编码为 base64。
|
||||
|
||||
@ -203,10 +117,9 @@ async def process_document(
|
||||
concurrency: 图片编码的并发数上限,默认 32
|
||||
|
||||
Returns:
|
||||
成功: (content_list, images_dict, md_content)
|
||||
成功: (content_list, images_dict)
|
||||
失败: 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 文件。")
|
||||
@ -215,13 +128,6 @@ 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}")
|
||||
@ -234,19 +140,20 @@ async def process_document(
|
||||
)
|
||||
logger.info(f"成功编码 {len(images_dict)} 张图片")
|
||||
|
||||
return content, images_dict, md_content
|
||||
return content, images_dict
|
||||
|
||||
|
||||
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, md_content = result
|
||||
content, images_dict = result
|
||||
print(f"\n=== 处理完成 ===")
|
||||
print(f"文档段落数: {len(content)}")
|
||||
print(f"图片数量: {len(images_dict)}")
|
||||
@ -257,12 +164,5 @@ async def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = asyncio.run(
|
||||
extract_image_caption(
|
||||
r"E:\ZKYNLP\Hjunproject\project0506\kgrag",
|
||||
"163-06A0014-B01001_发动机-维修手册.pdf",
|
||||
"23fa41fea5e5b5458d6f7df3f8e7e8033b6665bd23203946646651c3e56c2da7.jpg",
|
||||
)
|
||||
)
|
||||
print(result)
|
||||
|
||||
exit_code = asyncio.run(main())
|
||||
exit(exit_code)
|
||||
|
||||
@ -1,142 +0,0 @@
|
||||
"""
|
||||
文件概述生成工具
|
||||
把 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,13 +5,9 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Set
|
||||
from typing import Dict, List, Any, Optional
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXCLUDED_RESULT_NODE_LABELS = {"维修工作", "操作程序", "操作使用"}
|
||||
_FILTERED_OUT = object()
|
||||
|
||||
|
||||
class SimpleNode:
|
||||
"""
|
||||
@ -541,110 +537,6 @@ 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:
|
||||
"""
|
||||
@ -757,9 +649,8 @@ def serialize_graph_for_llm(graph_response: dict) -> str:
|
||||
- results(任意查询返回值:聚合/记录/字符串等)
|
||||
"""
|
||||
data = graph_response.get("data", {})
|
||||
# 这里的过滤只影响 results 字段里的文本展示,不改动接口返回的 data.nodes/data.links。
|
||||
nodes = _filter_result_nodes_for_output(data.get("nodes", []))
|
||||
results = _filter_results_for_output(data.get("results", []))
|
||||
nodes = data.get("nodes", [])
|
||||
results = data.get("results", [])
|
||||
|
||||
output_lines = []
|
||||
|
||||
@ -804,4 +695,3 @@ def serialize_graph_for_llm(graph_response: dict) -> str:
|
||||
return "图谱查询完成,但未返回有效数据。"
|
||||
|
||||
return "\n".join(output_lines).rstrip()
|
||||
|
||||
|
||||
@ -1,697 +0,0 @@
|
||||
"""
|
||||
结果格式化模块
|
||||
|
||||
功能:从 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()
|
||||
123093
huizong_node_rela.json
123093
huizong_node_rela.json
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
568701
kg_snapshot.json
568701
kg_snapshot.json
File diff suppressed because one or more lines are too long
@ -15,19 +15,7 @@ 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
|
||||
@ -160,79 +148,7 @@ 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:
|
||||
|
||||
113394
neo4j_export_deduplicated.json
Normal file
113394
neo4j_export_deduplicated.json
Normal file
File diff suppressed because one or more lines are too long
@ -35,8 +35,6 @@ 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"],
|
||||
@ -57,8 +55,6 @@ OLD_INDEX_NAMES: List[str] = [
|
||||
VECTOR_INDEX_NAME,
|
||||
FULLTEXT_INDEX_NAME,
|
||||
FULLTEXT_FIELD_INDEX_NAME,
|
||||
GLOBAL_VECTOR_INDEX_NAME,
|
||||
GLOBAL_FULLTEXT_INDEX_NAME,
|
||||
]
|
||||
|
||||
|
||||
@ -192,390 +188,6 @@ 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:
|
||||
@ -795,13 +407,12 @@ build_hyrid_indexes = build_hybrid_indexes
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 可以通过命令行参数或环境变量控制是否强制刷新
|
||||
import sys
|
||||
|
||||
force_refresh = "--force" in sys.argv or os.getenv("FORCE_REFRESH", "false").lower() == "true"
|
||||
result = build_hybrid_indexes(
|
||||
drop_old=True
|
||||
)
|
||||
|
||||
result = create_all_indexes(force_refresh=force_refresh)
|
||||
if result["success"]:
|
||||
logger.info(f"索引创建成功完成(模式: {result['mode']})")
|
||||
logger.info("混合索引创建成功")
|
||||
else:
|
||||
logger.error(f"索引创建完成但有错误: {result['message']}")
|
||||
logger.error(result["message"])
|
||||
BIN
pdfs/AI模型基准测试对比_2025年12月.pdf
Normal file
BIN
pdfs/AI模型基准测试对比_2025年12月.pdf
Normal file
Binary file not shown.
184
resetlevel.py
184
resetlevel.py
@ -1,184 +0,0 @@
|
||||
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
Normal file
799
result.json
Normal file
@ -0,0 +1,799 @@
|
||||
{
|
||||
"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 @@
|
||||
{
|
||||
"777": {
|
||||
"code": "777",
|
||||
"name": "jz坚",
|
||||
"163": {
|
||||
"code": "163",
|
||||
"name": "JZ贱",
|
||||
"level": 0,
|
||||
"children":{
|
||||
"00": {
|
||||
@ -579,20 +579,20 @@
|
||||
"level": 1,
|
||||
"children": {}
|
||||
},
|
||||
"12": {
|
||||
"code": "12",
|
||||
"11": {
|
||||
"code": "11",
|
||||
"name": "测试、维修试和保障系统",
|
||||
"level": 1,
|
||||
"children": {}
|
||||
}
|
||||
},
|
||||
"11": {
|
||||
"code": "11",
|
||||
"06": {
|
||||
"code": "06",
|
||||
"name": "汽车配件",
|
||||
"level": 1,
|
||||
"children":{
|
||||
"A1":{
|
||||
"code": "A1",
|
||||
"A0":{
|
||||
"code": "A0",
|
||||
"name": "汽车主配件",
|
||||
"level": 2,
|
||||
"children":{
|
||||
|
||||
637
tree_data_0413.json
Normal file
637
tree_data_0413.json
Normal file
@ -0,0 +1,637 @@
|
||||
{
|
||||
"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
Normal file
613
tree_data_1.json
Normal file
@ -0,0 +1,613 @@
|
||||
{
|
||||
"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
Normal file
408
wxproject.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user