Compare commits

...

No commits in common. "defeng-patch" and "main" have entirely different histories.

35 changed files with 85445 additions and 737164 deletions

203
app.py
View File

@ -40,7 +40,7 @@ from pydantic import BaseModel
from dataprocess import _split_markdown
import uuid
import json
from fileparse_util import process_document
from fileparse_util import process_document, extract_image_caption
# import hanlp
from itertools import groupby
import requests
@ -57,7 +57,7 @@ from concurrent.futures import ThreadPoolExecutor
import base64
from extract_excel_node_relation import get_excel_node_relation
from doc2pdf import Doc2PDF
from chunk_text import get_chunk_bbox, split_text_preserve_sentences, data_replace, chunk_check,merge_short_slices,find_and_read_content_list,process_pdf_file,process_other_file
from chunk_text import get_chunk_bbox, split_text_preserve_sentences, data_replace, chunk_check,merge_short_slices,find_and_read_content_list,process_pdf_file,process_other_file,safe_original_filename,image_base64_to_data_url,extract_image_text,prepare_split_image
from filename_proceess_and_kgquery import get_entity
from kg_build.extract_filtertext import get_filtertext_node_relation
import os
@ -92,7 +92,7 @@ from default_ontology_config import (
get_relationships,
get_relationship_types,
)
from config import NEO4J_CONFIG,URL,API_URLS,API_OTHER_URLS
from config import NEO4J_CONFIG,URL,API_URLS,API_OTHER_URLS,VLM_CONCURRENCY,SEARCH_DIR
converter = Doc2PDF()
DATA_DIR = "/app/files"
# ================== 全局资源容器 ==================
@ -102,7 +102,6 @@ _resources: Dict[str, Any] = {
"async_driver": None,
"embedder": None,
}
# 图谱检索和索引生成
try:
from graph_search.graph_service import GraphService
@ -167,6 +166,7 @@ async def lifespan(app):
app = FastAPI(max_request_size=1024 * 1024 * 10, lifespan=lifespan)
VLM_SEMAPHORE = asyncio.Semaphore(max(1, VLM_CONCURRENCY))
setup_logging()
logger = logging.getLogger(__name__)
@ -1739,7 +1739,7 @@ async def kg_build(request: KGRequest):
except Exception as e:
logger.warning(f"保存 kg_build 配置缓存失败: {e}")
# 后续进程启动逻辑保持不变 ...
# 后续进程启动逻辑保持不变 ...https://gitea.zkzdht.com/Ascend/htknow
cancel_flag_path = f"/tmp/kg_cancel_{task_id}"
if os.path.exists(cancel_flag_path):
os.remove(cancel_flag_path)
@ -2028,6 +2028,105 @@ class RequestWrapper(BaseModel):
pdf_contents: List[PdfContentItem]
class Base64ImageRequest(BaseModel):
content: Optional[str] = Field(
None,
description="Optional text around the image, for example: 图3.1 船舶主发动机组成图images/test.jpg",
)
filename: str = Field(..., description="Original image filename, for example test.png")
image_base64: str = Field(..., description="Base64 image content, with or without data:image/... prefix")
original_filename:Optional[str] = Field(
None,
description="图片 所在pdf文件",
)
async def analyze_image_with_vlm(image_bytes: bytes, suffix: str) -> str:
image_url = image_base64_to_data_url(image_bytes, suffix)
async with VLM_SEMAPHORE:
return await OpenaiAPI.open_api_vl_without_thinking(image_url)
@app.post("/split_image")
async def image_base64(request_data: Base64ImageRequest):
logger.info(f"Received image: {request_data.filename}")
logger.info(f"内容: {request_data.content}")
logger.info(f"pdf文件名: {request_data.original_filename}")
filename = safe_original_filename(request_data.filename)
logger.info(f"filename: {filename}")
suffix = PathLib(filename).suffix.lower()
if suffix not in {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".gif", ".tif", ".tiff"}:
raise HTTPException(status_code=400, detail="Unsupported image type")
image_bytes, processed_suffix = prepare_split_image(request_data.image_base64, suffix)
if not image_bytes:
raise HTTPException(status_code=400, detail="decoded image is empty")
temp_path = None
image_caption = []
vlm_text = ""
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=processed_suffix) as tmp:
tmp.write(image_bytes)
temp_path = PathLib(tmp.name)
ocr_data = await extract_image_text(temp_path)
try:
temp_path.unlink()
except Exception as e:
logger.warning(f"Failed to delete temp image {temp_path}: {e}")
finally:
temp_path = None
if request_data.original_filename:
image_caption = await extract_image_caption(
SEARCH_DIR,
request_data.original_filename,
request_data.filename,
)
caption_text = "\n".join(image_caption).strip()
# image_caption 不为空时使用 caption
# 否则使用原始图片文件名
if caption_text:
caption_text =caption_text+".jpg"
output_filename = caption_text or filename
logger.info(f"image_caption: {caption_text}")
content = (request_data.content or "").strip()
ocr_full_text = ((ocr_data or {}).get("full_text") or "").strip()
# content 和 OCR 都为空时调用 VLM
if not content and not ocr_full_text:
vlm_text = await analyze_image_with_vlm(
image_bytes,
processed_suffix,
)
image_related_content = ocr_full_text+ "\n" +content+ "\n"+vlm_text
# image_content = (
# f"{output_filename}\n"
# f"{image_related_content}"
# )
logger.info(f"output_filename: {output_filename}")
logger.info(f"{image_related_content}")
return JSONResponse(
{
"code": 200,
"message": "ok",
"data": {
"image_content": image_related_content,
"filename": filename,
"jpg_filename": output_filename
},
}
)
finally:
if temp_path and temp_path.exists():
try:
temp_path.unlink()
except Exception as e:
logger.warning(f"Failed to delete temp image {temp_path}: {e}")
@app.post("/split_content_list")
@ -2133,7 +2232,7 @@ async def split_result(
slices = []
for ins in contents:
slices.append({"content": ins, "positions": []})
result_data = {"slices": slices, "images": {}}
result_data = {"slices": slices, "images": {},"summary":''}
elif suffix in [".xlsx"]:
# converter.convert(str(temp_input_path), output_dir=str(DATA_DIR))
# temp_base_name = temp_input_path.stem # 如 tmp2bb0l4cn
@ -2221,96 +2320,6 @@ class GraphSearchRequest(BaseModel):
entry_nodes: Optional[Dict[str, Any]] = None
@app.post("/search_graph——1")
async def search_graph(request: GraphSearchRequest, background_tasks: BackgroundTasks):
"""
========== ========== ========== ========== ========== ==========
纯图谱检索
请求参数
- query: 用户查询文本必填
- top_k: 返回用于展示的路径数量默认10
- graph_timeout: 图谱检索超时时间默认2.0暂未使用
- entry_nodes: 入口节点信息可选不提供则从查询中提取
========== ========== ========== ========== ========== ==========
"""
start_time = time.time()
query = request.query
# ========== 参数验证 ==========
if not query or not query.strip():
raise HTTPException(status_code=400, detail="查询文本不能为空")
query = query.strip()
logger.info(f"[search_graph] 图谱检索请求: query='{query}', top_k={request.top_k}")
try:
# ========== 检查依赖是否可用 ==========
if not GRAPH_AVAILABLE:
raise HTTPException(
status_code=503,
detail="图谱检索功能依赖未安装,请安装: pip install neo4j langchain langchain-community neo4j-graphrag jieba",
)
# ========== 初始化图谱服务复用app.py的neo4j driver ==========
# 注意传入app.py中创建的driver实现连接复用
graph_service = GraphService(driver=driver)
# ========== 执行图谱检索(简单版本:只判断统计/非统计) ==========
# 说明:
# - 直接调用 GraphService.search自动判断统计/非统计查询类型
# - query_type="auto" 会自动识别统计类查询(如"多少"、"比例"等)和明细类查询
result = await graph_service.search(
query=query,
entry_nodes=request.entry_nodes,
top_k=request.top_k,
max_attempts=3,
query_type="auto", # 自动判断统计/非统计
)
# ========== 确保返回格式一致 ==========
if not isinstance(result, dict):
result = {
"code": 200,
"message": "success",
"data": result if isinstance(result, (list, dict)) else ([] if isinstance(result, list) else {}),
"meta": {"elapsed_ms": round((time.time() - start_time) * 1000, 2)},
}
# 确保data存在可以是列表或字典新格式包含 nodes、links、results 的对象)
if "data" not in result:
result["data"] = {}
# 注意:不再强制转换为列表,支持新的对象格式
# 统计结果数量(兼容新旧格式)
if isinstance(result.get("data"), list):
result_count = len(result["data"])
elif isinstance(result.get("data"), dict):
# 新格式:统计 results 数组的长度
result_count = len(result["data"].get("results", []))
else:
result_count = 0
logger.info(f"[search_graph] 图谱检索完成: 返回 {result_count} 条结果")
return result
except HTTPException:
raise
except Exception as e:
elapsed = time.time() - start_time
logger.error(f"[search_graph] 图谱检索失败 (耗时: {elapsed * 1000:.2f}ms): {e}", exc_info=True)
return {
"code": 500,
"message": "图谱检索失败",
"data": [],
"meta": {"elapsed_ms": round(elapsed * 1000, 2), "error": str(e), "query_type": "error"},
}
# ========== ========== ========== ========== ========== ==========
# Neo4j 索引创建接口
# ========== ========== ========== ========== ========== ==========
@ -2921,5 +2930,3 @@ if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=9085)

View File

@ -85,16 +85,16 @@ def _build_ontology_config():
ONTOLOGY, ONTOLOGY_DESC, NODE_TYPES = _build_ontology_config()
# ── 配置(与 app.py 保持一致,按需修改) ────────────────────────────────────
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://localhost:7687")
NEO4J_URI = os.getenv("NEO4J_URI", "bolt://192.168.2.12:57687")
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD", "zdht123@")
MINERU_INSTANCES = [
# ("http://192.168.0.64:18000/analyze-pdf"),
#("http://192.168.0.111:9977/analyze-pdf/"),
# ("http://192.168.0.111:9979/analyze-pdf"),
("http://192.168.1.64:18000/analyze-pdf/"),
("http://192.168.2.12:59988/analyze-pdf/"),
]
PREFIX_URL = os.getenv("PREFIX_URL", "http://192.168.1.64:9085")
PREFIX_URL = os.getenv("PREFIX_URL", "http://192.168.2.12:59085")
SUPPORTED_EXT = {".pdf", ".md", ".docx", ".xlsx"}

File diff suppressed because it is too large Load Diff

250
chunkincreas.py Normal file
View File

@ -0,0 +1,250 @@
import asyncio
import aiohttp
import json
import traceback
from filename_proceess_and_kgquery import get_entity
from config import SHIP_MODEL_NAME
# --- 配置区 ---
BASE_URL = "https://htknow.zkzdht.com/api/v1/knowledge"
ERROR_LOG_FILE = "error_log.txt"
HEADERS = {
"accept": "application/json",
"x-role": "admin",
"x-user-id": "1",
"x-user-name": "testuser"
}
# 全局 Semaphore用于限制并发连接数防止对服务器造成过大压力
# 根据实际情况调整数值
CONCURRENT_LIMIT = 10
semaphore = asyncio.Semaphore(CONCURRENT_LIMIT)
async def log_error(kb_id, file_id, error_msg):
"""
将错误信息追加写入日志文件
注意文件 I/O asyncio 中是阻塞的如果日志量极大可以考虑使用线程池执行器
"""
# 对于少量的日志写入,直接使用 await asyncio.to_thread 或直接同步写入通常足够
# 这里为了简化,直接使用同步写入(在日志量不大时影响较小)
# 如果需要完全异步,可以使用 aiofiles 库
try:
with open(ERROR_LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[ERROR] KnowledgeBaseID: {kb_id}, FileID: {file_id}, Message: {error_msg}\n")
except Exception as e:
print(f"写入错误日志时发生异常: {e}")
async def find_ship_info_by_hull(json_file_path, data):
"""从NER结果中提取舷号再从JSON文件中查找对应的舰船信息"""
try:
# 同步文件读取,如果文件很大或数量很多,可以使用 aiofiles
with open(json_file_path, 'r', encoding='utf-8') as f:
ship_data = json.load(f)
hulls = [
entity['properties']['舷号']
for entity in data.get('entities', [])
if 'properties' in entity and '舷号' in entity['properties']
]
if not hulls:
return None
target_hull = hulls[0]
for item in ship_data:
if str(item.get('hull_number')) == str(target_hull):
return {
"model_name": item.get('model_name'),
"ship_name": item.get('ship_name')
}
return None
except Exception as e:
print(f"查找舰船信息时发生错误: {e}")
return None
def format_entity_text(data):
"""格式化实体文本 (纯计算逻辑,保持同步)"""
type_mapping = {
'舰艇': [('舰艇为', '名称'), (',舷号', '舷号')],
'系统': [(',系统:', '名称')],
'子系统': [(',子系统:', '名称')],
'设备': [(',设备:', '名称')]
}
text_parts = []
for entity in data.get('entities', []):
entity_type = entity.get('type')
properties = entity.get('properties', {})
if entity_type in type_mapping:
entity_str = ""
for prefix, attr_key in type_mapping[entity_type]:
value = str(properties.get(attr_key, ''))
entity_str += f"{prefix}{value}"
text_parts.append(entity_str)
return ''.join(text_parts)
async def get_knowledge_base_files(session, kb_id):
"""
获取指定知识库的文件列表
"""
url = f"{BASE_URL}/knowledge_base/{kb_id}"
try:
async with session.get(url, headers=HEADERS, timeout=120) as response:
response.raise_for_status()
data = await response.json()
if 'files' in data and isinstance(data['files'], list):
return data['files']
else:
print(f"警告: 知识库 {kb_id} 中未找到 'files' 字段。")
return []
except Exception as e:
error_msg = f"获取知识库详情失败: {str(e)} | Traceback: {traceback.format_exc()}"
print(error_msg)
await log_error(kb_id, "N/A", error_msg)
return []
async def get_file_slices(session, file_id):
"""获取文件所有切片"""
url = f"{BASE_URL}/files/{file_id}/slices"
try:
async with session.get(url, headers=HEADERS, timeout=120) as response:
response.raise_for_status()
return await response.json()
except Exception as e:
error_msg = f"获取文件切片失败: {str(e)}"
print(error_msg)
# 错误会在调用处记录
raise Exception(error_msg)
async def update_slices(session, slices_data, file_id):
"""更新切片"""
url = f"{BASE_URL}/files/{file_id}/slices"
headers = {
**HEADERS,
"content-type": "application/json"
}
payload = {"slices": slices_data}
try:
async with session.put(url, headers=headers, json=payload, timeout=120) as response:
response.raise_for_status()
return await response.json()
except Exception as e:
error_msg = f"更新切片失败: {str(e)}"
print(error_msg)
raise Exception(error_msg)
async def process_single_file(session, kb_id, file):
"""
处理单个文件的逻辑用于并发执行
"""
file_id = file.get('id')
filename = file.get('filename', 'Unknown')
print(f" 处理文件: {filename} (ID: {file_id})")
try:
# --- 步骤 A: 提取实体信息 ---
final_result = get_entity(filename)
# 处理舰船型号
xinghao = await find_ship_info_by_hull(SHIP_MODEL_NAME, final_result)
model_name = xinghao.get('model_name', '') if xinghao else ''
# 格式化其他实体文本
other_data = format_entity_text(final_result)
# --- 步骤 B: 获取切片 ---
slices = await get_file_slices(session, file_id)
# --- 步骤 C: 处理切片内容 ---
slices_v2 = []
info_suffix = f"({other_data},型号为{model_name})" if model_name and other_data else ""
for ins in slices:
ins_id = ins.get('id')
content = ins.get('content', '')
# 查找第一行的结尾(换行符位置)
newline_idx = content.find('\n')
# 拼接新内容
if newline_idx == -1:
# 内容中没有换行符,直接在末尾添加
new_content = content + info_suffix
else:
# 在第一行末尾插入
new_content = content[:newline_idx] + info_suffix + content[newline_idx:]
slices_v2.append({"id": ins_id, "content": new_content})
# --- 步骤 D: 更新切片 ---
await update_slices(session, slices_v2, file_id)
print(f" 成功更新文件: {filename}")
except Exception as e:
# 捕获当前文件处理过程中的所有异常
error_msg = f"处理文件失败: {str(e)}"
print(error_msg)
# 记录错误日志
await log_error(kb_id, file_id, error_msg)
# 跳过当前文件
return False
return True
async def process_files_in_knowledge_base(session, kb_id):
"""
处理单个知识库中的所有文件
"""
print(f"\n=== 开始处理知识库 ID: {kb_id} ===")
# 1. 获取文件列表
files = await get_knowledge_base_files(session, kb_id)
if not files:
print(f"知识库 {kb_id} 中没有文件或获取文件列表失败,跳过。")
return
# 2. 并发处理每个文件
# 使用 asyncio.gather 来并发处理所有文件
# 如果文件数量巨大,可以分批处理以避免内存溢出
file_tasks = [
process_single_file(session, kb_id, file)
for file in files
]
# 并发执行所有文件任务
# return_exceptions=True 会捕获任务内部的异常并作为结果返回,而不是中断整个 gather
results = await asyncio.gather(*file_tasks, return_exceptions=True)
# 统计结果
success_count = sum(1 for r in results if r is True)
failed_count = len(results) - success_count
print(f"知识库 {kb_id} 处理完成。成功: {success_count}, 失败: {failed_count}")
async def main():
KNOWLEDGE_BASE_IDS = ['8']
print(f"开始批量处理 {len(KNOWLEDGE_BASE_IDS)} 个知识库...")
# 创建一个全局的 TCP 连接器,复用连接
connector = aiohttp.TCPConnector(limit=CONCURRENT_LIMIT, limit_per_host=5)
async with aiohttp.ClientSession(connector=connector) as session:
# 遍历所有知识库ID并发处理
# 如果知识库数量很多,也可以在这里使用 asyncio.gather
for kb_id in KNOWLEDGE_BASE_IDS:
try:
await process_files_in_knowledge_base(session, kb_id)
except Exception as e:
error_msg = f"处理知识库顶层异常: {str(e)}"
print(error_msg)
await log_error(kb_id, "N/A", error_msg)
continue
print("所有知识库处理完成。")
if __name__ == "__main__":
# 运行异步主函数
asyncio.run(main())

115
config.py
View File

@ -4,27 +4,26 @@ import os
# ==================== 大模型配置 ====================
LLM_CONFIG = {
"model": "46-qwen3.5-35B",
"base_url": "http://192.168.0.46:59800/v1",
"model": "46-qwen3.6-35B",
"base_url": "http://192.168.2.12:59800/v1",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
"temperature": 0.1,
"max_tokens": 55096,
"max_tokens": 25096,
"timeout": 30
}
# ==================== 嵌入模型配置 ====================
EMBEDDING_CONFIG = {
"model": "bge-m3",
"base_url": "http://192.168.0.46:59700/v1/embeddings",
"base_url": "http://192.168.2.12:59700/v1/embeddings",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
"base_url_v2" : "http://192.168.0.46:59700/v1",
}
# ==================== Neo4j 图数据库配置 ====================
NEO4J_CONFIG = {
# 优先读取环境变量,如果没有则使用默认值
# 注意:本地开发通常用 localhost部署时用服务名 neo4j
"uri": "neo4j://192.168.0.46:57687",
"uri": "neo4j://192.168.2.12:57687",
"username": "neo4j",
"password": "zdht123@",
"database": "neo4j", # 新增 database 配置
@ -51,15 +50,15 @@ SEARCH_CONFIG = {
"node_fetch_page" : 10000,
}
INDEXING_ULR = "http://192.168.0.46:59085/neo4j_indexing"
INDEXING_ULR = "http://192.168.2.12:59085/neo4j_indexing"
URL = {
"indexing_url" : "http://192.168.0.46:59085/neo4j_indexing",
"prefix_url" : "http://192.168.0.46:59085",
"indexing_url" : "http://192.168.2.12:59085/neo4j_indexing",
"prefix_url" : "http://192.168.2.12:59085",
}
API_URLS = [
"http://192.168.0.64:59988/analyze-pdf",
"http://192.168.2.12:59988/analyze-pdf",
# "http://192.168.0.111:9977/analyze-pdf",
# "http://192.168.0.111:9978/analyze-pdf",
#"http://192.168.0.111:9979/analyze-pdf",
@ -67,7 +66,7 @@ API_URLS = [
# "http://192.168.0.111:9975/analyze-pdf",
]
API_OTHER_URLS = [
"http://192.168.0.46:59988/analyze-otherfile",
"http://192.168.2.12:59988/analyze-otherfile",
# "http://192.168.0.111:9977/analyze-otherfile",
# "http://192.168.0.111:9978/analyze-otherfile",
#"http://192.168.0.111:9979/analyze-otherfile",
@ -75,88 +74,18 @@ API_OTHER_URLS = [
# "http://192.168.0.111:9975/analyze-otherfile",
]
SHIP_MODEL_NAME = "E:\ZKYNLP\Hjunproject\project0506\kgrag\ship_xinghao.json"
TREE_JSON_PATH = r"E:\ZKYNLP\Hjunproject\project0506\kgrag\tree_data.json"
root@70964b104c5b:/app# pwd
/app
root@70964b104c5b:/app# vim config.py
root@70964b104c5b:/app# cat config.py
# app/model_config.py
import os
# ==================== 大模型配置 ====================
LLM_CONFIG = {
"model": "46-qwen3.5-35B",
"base_url": "http://192.168.0.46:59800/v1",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
"temperature": 0.1,
"max_tokens": 55096,
"timeout": 30
}
# ==================== 嵌入模型配置 ====================
EMBEDDING_CONFIG = {
"model": "bge-m3",
"base_url": "http://192.168.0.46:59700/v1/embeddings",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",
"base_url_v2" : "http://192.168.0.46:59700/v1",
}
# ==================== Neo4j 图数据库配置 ====================
NEO4J_CONFIG = {
# 优先读取环境变量,如果没有则使用默认值
# 注意:本地开发通常用 localhost部署时用服务名 neo4j
"uri": "neo4j://192.168.0.46:57687",
"username": "neo4j",
"password": "zdht123@",
"database": "neo4j", # 新增 database 配置
}
# ==================== 检索与索引配置 ====================
SEARCH_CONFIG = {
"name_property": "名称",
"fulltext_property": "fulltext",
"embedding_property": "embedding",
"search_label": "Searchable",
"vector_index_name": "global_searchable_embedding",
"fulltext_index_name": "global_searchable_content_search",
"fulltext_field_index_name": "global_searchable_fulltext_search",
"global_entity_embedding" : "global_entity_embedding",
"global_entity_content_search": "global_entity_content_search",
"excluded_business_labels": [
"Entity", "Chunk", "Document", "_Bloom_Perspective_", "Searchable"
],
"vector_dimension": 1024,
"jieba_pos_whitelist": ["n", "nr", "ns", "nt", "nz", "vn", "eng"],
"embed_batch_size" : 64,
"embed_thread_num" : 4,
"node_fetch_page" : 10000,
}
INDEXING_ULR = "http://192.168.0.46:59085/neo4j_indexing"
URL = {
"indexing_url" : "http://192.168.0.46:59085/neo4j_indexing",
"prefix_url" : "http://192.168.0.46:59085",
}
API_URLS = [
"http://192.168.0.64:59988/analyze-pdf",
# "http://192.168.0.111:9977/analyze-pdf",
# "http://192.168.0.111:9978/analyze-pdf",
#"http://192.168.0.111:9979/analyze-pdf",
#"http://192.168.0.111:9980/analyze-pdf",
# "http://192.168.0.111:9975/analyze-pdf",
]
API_OTHER_URLS = [
"http://192.168.0.46:59988/analyze-otherfile",
# "http://192.168.0.111:9977/analyze-otherfile",
# "http://192.168.0.111:9978/analyze-otherfile",
#"http://192.168.0.111:9979/analyze-otherfile",
#"http://192.168.0.111:9980/analyze-otherfile",
# "http://192.168.0.111:9975/analyze-otherfile",
]
# ==================== 切片图片处理配置 ====================
MAX_SPLIT_IMAGE_INPUT_BYTES = 50 * 1024 * 1024
MAX_SPLIT_IMAGE_BYTES = 10 * 1024 * 1024
MAX_SPLIT_IMAGE_BASE64_CHARS = (MAX_SPLIT_IMAGE_INPUT_BYTES * 4 // 3) + 4096
SPLIT_IMAGE_COMPRESS_THRESHOLD_BYTES = MAX_SPLIT_IMAGE_BYTES
SPLIT_IMAGE_MAX_DIMENSION = 4096
SPLIT_IMAGE_JPEG_QUALITY = 88
OCR_CONCURRENCY = 1
VLM_CONCURRENCY = 2
SHIP_MODEL_NAME = "/app/ship_xinghao.json"
TREE_JSON_PATH = "/app/tree_data.json"
# ==================== mineru和kgrag目录映射地址 ====================
SEARCH_DIR ="/app/mineru_output"
IMAGE_DIR = "/app/mineru_output/images"

46
databaseconnect.py Normal file
View File

@ -0,0 +1,46 @@
import json
def find_ship_info(json_file_path, target_hull):
"""
从JSON文件中根据舷号查找舰船信息
:param json_file_path: JSON文件路径
:param target_hull: 目标舷号 (字符串或数字)
:return: 包含 model_name ship_name 的字典未找到返回 None
"""
try:
# 1. 打开并读取JSON文件
with open(json_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 2. 遍历列表查找匹配项
# 注意:这里使用 str() 转换是为了兼容 JSON 中 hull_number 可能是数字(163)或字符串("163")的情况
for item in data:
if str(item.get('hull_number')) == str(target_hull):
return {
"model_name": item.get('model_name'),
"ship_name": item.get('ship_name')
}
# 如果遍历结束仍未找到
return None
except FileNotFoundError:
print(f"错误:找不到文件 {json_file_path}")
return None
except json.JSONDecodeError:
print("错误JSON 文件格式不正确")
return None
# --- 使用示例 ---
if __name__ == "__main__":
file_path = "E:\ZKYNLP\Hjunproject\project0506\kgrag\ship_xinghao.json" # 替换为你的实际文件名
search_hull = "163" # 你要查询的舷号
result = find_ship_info(file_path, search_hull)
if result:
print(f"查询成功!")
print(f"型号: {result['model_name']}")
print(f"舰名: {result['ship_name']}")
else:
print(f"未找到舷号为 {search_hull} 的记录")

41
error_log.txt Normal file
View File

@ -0,0 +1,41 @@
[ERROR] KnowledgeBaseID: 1789, FileID: 3974, Message: 处理文件失败: 更新切片失败: HTTPSConnectionPool(host='htknow.zkzdht.com', port=443): Read timed out. (read timeout=10)
[ERROR] KnowledgeBaseID: 1789, FileID: 3962, Message: 处理文件 163-06A0014-B01001_发动机-维修手册.pdf 的切片批次 3 时出错: 更新切片批次失败:
[ERROR] KnowledgeBaseID: 1789, FileID: 3974, Message: 处理文件 163-06A0016-B01003_雷达-维修手册.pdf 的切片批次 3 时出错: 更新切片批次失败:
[ERROR] KnowledgeBaseID: 1789, FileID: 3961, Message: 处理文件 163-06A0016-B01003_雷达-操作使用手册(1).pdf 的切片批次 4 时出错: 更新切片批次失败:
[ERROR] KnowledgeBaseID: 1789, FileID: 3974, Message: 处理文件 163-06A0016-B01003_雷达-维修手册.pdf 的切片批次 4 时出错: 更新切片批次失败: 500, message='Internal Server Error', url='https://htknow.zkzdht.com/api/v1/knowledge/files/3974/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 299, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/299/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 296, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/296/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 319, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/319/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 315, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/315/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 311, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/311/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 307, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/307/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 291, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/291/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 283, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/283/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 293, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/293/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 295, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/295/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 286, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/286/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 290, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/290/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 287, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/287/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 278, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/278/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 284, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/284/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 282, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/282/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 40, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/40/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 275, Message: 处理文件失败: 更新切片失败: 400, message='Bad Request', url='https://htknow.zkzdht.com/api/v1/knowledge/files/275/slices'
[ERROR] KnowledgeBaseID: 8, FileID: 299, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/299/slices
[ERROR] KnowledgeBaseID: 8, FileID: 319, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/319/slices
[ERROR] KnowledgeBaseID: 8, FileID: 315, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/315/slices
[ERROR] KnowledgeBaseID: 8, FileID: 311, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/311/slices
[ERROR] KnowledgeBaseID: 8, FileID: 307, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/307/slices
[ERROR] KnowledgeBaseID: 8, FileID: 296, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/296/slices
[ERROR] KnowledgeBaseID: 8, FileID: 283, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/283/slices
[ERROR] KnowledgeBaseID: 8, FileID: 291, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/291/slices
[ERROR] KnowledgeBaseID: 8, FileID: 293, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/293/slices
[ERROR] KnowledgeBaseID: 8, FileID: 295, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/295/slices
[ERROR] KnowledgeBaseID: 8, FileID: 286, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/286/slices
[ERROR] KnowledgeBaseID: 8, FileID: 287, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/287/slices
[ERROR] KnowledgeBaseID: 8, FileID: 290, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/290/slices
[ERROR] KnowledgeBaseID: 8, FileID: 278, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/278/slices
[ERROR] KnowledgeBaseID: 8, FileID: 284, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/284/slices
[ERROR] KnowledgeBaseID: 8, FileID: 40, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/40/slices
[ERROR] KnowledgeBaseID: 8, FileID: 282, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/282/slices
[ERROR] KnowledgeBaseID: 8, FileID: 275, Message: 处理文件失败: 更新切片失败: 400 Client Error: Bad Request for url: https://htknow.zkzdht.com/api/v1/knowledge/files/275/slices

View File

@ -339,7 +339,7 @@ def process_file_content(
return await process_document(file_name)
doc_result = asyncio.run(_run_process_doc())
if doc_result is not None:
content_list, _ = doc_result
content_list, images_dict, md_content= doc_result
else:
if not os.path.exists(file_path):

View File

@ -13,7 +13,6 @@ import json
from modelsAPI.model_api import OpenaiAPI
from openai import OpenAI
from typing import Dict, List, Any
from config import TREE_JSON_PATH
app = FastAPI(title="PDF Upload Service")
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
@ -89,7 +88,6 @@ def build_tree(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
不再依赖 links 的连线直接根据 labels 归类彻底避免环状数据导致的死循环
默认只展开第一艘舰艇的系统/子系统其它舰艇 children 置空前端可点击按需加载
"""
# 1. 辅助函数:判断标签是否包含关键字
def has_label(node, keyword):
labels = node.get("labels", [])
@ -541,8 +539,8 @@ def fetch_graph_sample1(driver):
# 文件名处理方法
# =============================
# BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
def _load_tree_data() -> Dict:
@ -834,7 +832,7 @@ def get_entity(filename):
if __name__ == "__main__":
# 阀控蓄电池脉冲式快速充电装置
test_list = [
"163-06A0015-B01001_发动机-维修手册.pdf",
"777-11A1014-B01001_发动机-维修手册.pdf",
]
for i in test_list:

View File

@ -1,821 +0,0 @@
"""
PDF 用户上传接口FastAPI
- 处理文件名和其对应关系
"""
from typing import Dict, Tuple
import os
import re
from typing import Dict
from fastapi import FastAPI, HTTPException
import logging
import json
from modelsAPI.model_api import OpenaiAPI
from openai import OpenAI
from typing import Dict, List, Any
app = FastAPI(title="PDF Upload Service")
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
logger = logging.getLogger("filename_neo4j_process")
# =========================
# 图方法
# =========================
def node_to_json(node):
props = dict(node)
# ks_raw = props.get("knowledge_source")
# knowledge_source = json.loads(ks_raw) if ks_raw else []
ks_raw = props.get("knowledge_source")
knowledge_source = []
if ks_raw and isinstance(ks_raw, str):
try:
ks_list = json.loads(ks_raw)
if isinstance(ks_list, list):
for ks in ks_list:
if isinstance(ks, dict):
ks.pop("info", None)
knowledge_source.append(ks)
except (json.JSONDecodeError, TypeError):
# 解析失败或类型错误时,保持 knowledge_source 为空列表
pass
name = props.get("名称")
props.pop("embedding", None)
props.pop("knowledge_source", None)
props.pop("name", None) # 现在安全地移除
props.pop("fulltext", None)
if "last_updated" in props:
props["最后更新时间"] = props.pop("last_updated")
if "created_at" in props:
props["创建时间"] = props.pop("created_at")
return {
"id": node.element_id,
"name": name, # 使用提前保存的值
"labels": list(node.labels),
"knowledge_source": knowledge_source,
"properties": props,
}
def rel_to_json(rel):
props = dict(rel)
props.pop("embedding", None)
props.pop("fact_timeline", None)
return {"id": rel.element_id, "label": rel.type, "source": rel.start_node.element_id, "target": rel.end_node.element_id, "properties": props}
def is_simple_path(path):
"""
判断是否为无环路径simple path
"""
seen = set()
for n in path.nodes:
nid = n.element_id
if nid in seen:
return False
seen.add(nid)
return True
def build_tree(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
"""
按照固定层级构建树舰艇 -> 系统 -> 子系统
不再依赖 links 的连线直接根据 labels 归类彻底避免环状数据导致的死循环
"""
# 1. 辅助函数:判断标签是否包含关键字
def has_label(node, keyword):
labels = node.get("labels", [])
if isinstance(labels, list):
return any(keyword in lbl for lbl in labels)
return False
# 2. 辅助函数:构建标准的树节点格式
def make_node(node_data):
return {
"id": node_data["id"],
"name": node_data.get("name", ""),
"labels": node_data.get("labels", []),
"properties": node_data.get("properties", {}),
"color": node_data.get("color"),
"size": node_data.get("size"),
"children": []
}
# 3. 将节点按层级分类存储到字典中,方便快速查找
# key: id, value: node_dict_with_children
ships = {} # 第一层:舰艇
systems = {} # 第二层:系统(不含子系统)
sub_systems = {} # 第三层:子系统
for node in nodes:
# 注意:判断逻辑要互斥,防止“子系统”被误判为“系统”
if has_label(node, "舰艇"):
ships[node["id"]] = make_node(node)
elif has_label(node, "子系统"):
sub_systems[node["id"]] = make_node(node)
elif has_label(node, "系统"):
systems[node["id"]] = make_node(node)
else:
# 如果有其他无关节点,可以在这里处理或忽略
pass
# 4. 开始组装:利用 links 建立关系(此时 links 只充当“连接器”,不决定层级)
# 因为我们已经确定了层级,所以不会出现循环引用
for link in links:
if link.get("label") != "包含":
continue
src_id = link["source"]
tgt_id = link["target"]
# 逻辑 A: 舰艇 -> 系统
if src_id in ships and tgt_id in systems:
ships[src_id]["children"].append(systems[tgt_id])
# 逻辑 B: 系统 -> 子系统
elif src_id in systems and tgt_id in sub_systems:
systems[src_id]["children"].append(sub_systems[tgt_id])
# 逻辑 C: 舰艇 -> 子系统 (防止有些数据直接连了子系统,虽然不规范但也兼容一下)
elif src_id in ships and tgt_id in sub_systems:
ships[src_id]["children"].append(sub_systems[tgt_id])
# 5. 返回所有舰艇作为根节点列表
return list(ships.values())
def build_tree1(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
"""
nodes + links 构建树结构
- 根节点label 包含 '舰艇' 的节点
- 父子关系links label == '包含' 的边 (source -> target)
- 支持多棵树多个舰艇根节点
"""
# 1. 建立 id -> node 的映射
node_map: Dict[str, Dict] = {n["id"]: n for n in nodes}
# 2. 建立 父id -> [子id列表] 的邻接表(只处理"包含"关系)
children_map: Dict[str, List[str]] = {n["id"]: [] for n in nodes}
parent_map: Dict[str, str] = {} # 子id -> 父id用于识别根节点
for link in links:
if link.get("label") == "包含":
src = link["source"]
tgt = link["target"]
if src in children_map:
children_map[src].append(tgt)
if tgt not in parent_map:
parent_map[tgt] = src
# 3. 递归构建子树
def build_subtree(node_id: str, ancestors: set = None) -> Dict[str, Any]:
if ancestors is None:
ancestors = set()
if node_id in ancestors:
return None
ancestors.add(node_id)
node = node_map[node_id]
subtree = {"id": node_id, "name": node.get("name", ""), "labels": node.get("labels", []), "properties": node.get("properties", {}), "color": node.get("color"), "size": node.get("size"), "children": []}
current_ancestors = ancestors | {node_id}
for child_id in children_map.get(node_id, []):
child = build_subtree(child_id, current_ancestors)
if child is not None:
subtree["children"].append(child)
return subtree
# def build_subtree(node_id: str) -> Dict[str, Any]:
# node = node_map[node_id]
# subtree = {
# "id": node_id,
# "name": node.get("name", ""),
# "labels": node.get("labels", []),
# "properties": node.get("properties", {}),
# "color": node.get("color"),
# "size": node.get("size"),
# "children": []
# }
# for child_id in children_map.get(node_id, []):
# subtree["children"].append(build_subtree(child_id))
# return subtree
# 4. 找出所有根节点label 包含 '舰艇',且没有父节点
roots = [n["id"] for n in nodes if "舰艇" in n.get("labels", []) and n["id"] not in parent_map]
# 5. 为每棵树构建结构,并打上树编号
forest = []
for i, root_id in enumerate(roots):
tree = build_subtree(root_id)
tree["tree_index"] = i # 区分不同树
forest.append(tree)
return forest
def build_graph_from_paths(paths):
"""
核心公共方法
Neo4j 路径结果构建 graphData
"""
node_map: Dict[str, dict] = {}
link_map: Dict[str, dict] = {}
def add_edge(rel):
s = rel.start_node
t = rel.end_node
if s.element_id not in node_map:
node_map[s.element_id] = node_to_json(s)
if t.element_id not in node_map:
node_map[t.element_id] = node_to_json(t)
if rel.element_id not in link_map:
link_map[rel.element_id] = rel_to_json(rel)
for record in paths:
p = record["p"]
if not is_simple_path(p):
continue
for r in p.relationships:
add_edge(r)
return {"nodes": list(node_map.values()), "links": list(link_map.values())}
# def fetch_graph_sample(driver,limit: int = 200):
# """提取采样逻辑为可复用函数"""
# with driver.session() as session:
# result = session.run("""
# MATCH (n)
# WITH n
# ORDER BY elementId(n) ASC
# LIMIT $limit
# MATCH (n)-[rel]->(m)
# RETURN n, rel, m
# """, limit=limit)
# node_map, link_map = {}, {}
# for rec in result:
# n, r, m = rec["n"], rec["rel"], rec["m"]
# node_map[n.element_id] = node_to_json(n)
# node_map[m.element_id] = node_to_json(m)
# link_map[r.element_id] = rel_to_json(r)
# return {
# "nodes": list(node_map.values()),
# "links": list(link_map.values())
# }
# def fetch_graph_sample(driver, limit: int = 200):
# """
# 获取采样数据:
# 1. 选取前 $limit 个种子节点Level 0 或 1取决于业务定义这里定义种子为 Level 0
# 2. 对每个节点向下探索最多 3 跳。
# 3. 每个节点对象中增加 level 字段,表示其距离种子节点的深度。
# """
# with driver.session() as session:
# # Cypher 逻辑:
# # - 选出种子节点 n
# # - 匹配 1-3 跳路径
# # - 返回种子节点 n路径 path以及该路径的长度即层级
# result = session.run("""
# MATCH (n)
# WITH n ORDER BY elementId(n) ASC LIMIT $limit
# OPTIONAL MATCH path = (n)-[*1..3]-(m)
# UNWIND nodes(path) AS x
# WITH n, path, m, COUNT(DISTINCT x) AS nodeCount
# WHERE path IS NULL OR nodeCount = LENGTH(path) + 1
# RETURN n, path, LENGTH(path) AS depth
# """, limit=limit)
# node_map, link_map = {}, {}
# for rec in result:
# seed_node = rec["n"]
# path = rec["path"]
# depth = rec["depth"] or 0 # 如果没有路径,深度为 0
# # 1. 处理种子节点 (Level 0)
# if seed_node.element_id not in node_map:
# node_data = node_to_json(seed_node)
# node_data["level"] = 0 # 种子节点设为 0 层
# node_map[seed_node.element_id] = node_data
# # 2. 如果存在路径,解析路径中的所有节点和关系
# if path:
# # 路径中的节点处理
# # path.nodes 包含了从起始到终点的所有节点
# for i, node in enumerate(path.nodes):
# if node.element_id not in node_map:
# node_data = node_to_json(node)
# # 层级即为该节点在当前路径中的索引
# node_data["level"] = i
# node_map[node.element_id] = node_data
# else:
# # 如果节点已存在,保留最小的 level (即最靠近种子的距离)
# node_map[node.element_id]["level"] = min(node_map[node.element_id].get("level", 3), i)
# # 路径中的关系处理
# for rel in path.relationships:
# if rel.element_id not in link_map:
# link_map[rel.element_id] = rel_to_json(rel)
# return {
# "nodes": list(node_map.values()),
# "links": list(link_map.values())
# }
# def fetch_graph_sample(driver, limit=200, max_depth=3):
# with driver.session() as session:
# result = session.run("""
# MATCH (c)
# WITH c ORDER BY elementId(c) ASC LIMIT $limit
# MATCH path = (c)-[*0..3]-(n)
# WITH n, MIN(length(path)) AS dist
# OPTIONAL MATCH (n)-[r]-(m)
# RETURN n, dist, r, m
# """, limit=limit)
# node_map, link_map = {}, {}
# for rec in result:
# n = rec["n"]
# dist = rec["dist"]
# r = rec["r"]
# m = rec["m"]
# # ---- 节点 ----
# nid = n.element_id
# if nid not in node_map:
# node_data = node_to_json(n)
# node_data["level"] = min(dist, 3) if dist is not None else 0
# node_map[nid] = node_data
# # ---- 边 ----
# if r and m:
# mid = m.element_id
# if mid not in node_map:
# node_map[mid] = node_to_json(m)
# link_map.setdefault(r.element_id, rel_to_json(r))
# return {
# "nodes": list(node_map.values()),
# "links": list(link_map.values())
# }
def fetch_graph_sample(driver):
"""
查询所有舰艇及其关联的系统和子系统
路径结构(舰艇) -> (系统) -> (子系统)
"""
with driver.session() as session:
# =========================
# 统一查询:舰艇 -> 系统 -> 子系统
# =========================
# 逻辑说明:
# 1. MATCH (ship:舰艇):选中所有舰艇
# 2. -[*1..3]->(target):查找深度为 1 到 3 的路径
# - 深度 1舰艇 -> 系统
# - 深度 2舰艇 -> 系统 -> 子系统
# 3. WHERE target:系统 OR target:子系统:确保终点是我们关心的节点类型
# (防止查询出其他无关的深层节点)
query = """
MATCH path = (ship:舰艇)-[*1..3]->(target)
WHERE target:系统 OR target:子系统
RETURN path
"""
result = session.run(query)
# =========================
# 解析 paths
# =========================
all_nodes = {}
all_links = {}
for record in result:
path = record["path"]
# 解析节点
for node in path.nodes:
# 兼容 ID 获取
node_key = node.element_id if hasattr(node, 'element_id') else node.id
if node_key not in all_nodes:
all_nodes[node_key] = node_to_json(node)
# 解析关系
for rel in path.relationships:
rel_key = rel.element_id if hasattr(rel, 'element_id') else rel.id
if rel_key not in all_links:
all_links[rel_key] = rel_to_json(rel)
return {
"nodes": list(all_nodes.values()),
"links": list(all_links.values())
}
def fetch_graph_sample1(driver):
"""
查找路径
1. 优先舰艇 -> 子系统
2. fallback舰艇 -> 系统
注意根据数据库实际结构使用 Label (:舰艇, :子系统) 进行匹配
而不是 WHERE category = '...'
"""
# 定义采样数量
SAMPLE_LIMIT = 400
with driver.session() as session:
# =========================
# 第一阶段:舰艇 -> 子系统
# =========================
# 修改点:直接使用 (:舰艇) 和 (:子系统) 标签匹配,去掉 WHERE 子句
result = session.run(
"""
MATCH (ship:舰艇)
WITH ship LIMIT $limit
MATCH path = (ship)-[*1..3]->(sub:子系统)
RETURN path
""",
limit=SAMPLE_LIMIT,
)
paths = [record["path"] for record in result]
# =========================
# fallback舰艇 -> 系统
# =========================
if not paths:
# 修改点:直接使用 (:舰艇) 和 (:系统) 标签匹配
result = session.run(
"""
MATCH (ship:舰艇)
WITH ship LIMIT $limit
MATCH path = (ship)-[*1..2]->(sys:系统)
RETURN path
""",
limit=SAMPLE_LIMIT,
)
paths = [record["path"] for record in result]
# =========================
# 解析 paths
# =========================
all_nodes = {}
all_links = {}
for path in paths:
# 节点
for node in path.nodes:
# 兼容 Neo4j 5.x (element_id) 和 4.x (id)
node_key = node.element_id if hasattr(node, "element_id") else node.id
if node_key not in all_nodes:
all_nodes[node_key] = node_to_json(node)
# 关系
for rel in path.relationships:
rel_key = rel.element_id if hasattr(rel, "element_id") else rel.id
if rel_key not in all_links:
all_links[rel_key] = rel_to_json(rel)
return {"nodes": list(all_nodes.values()), "links": list(all_links.values())}
# =============================
# 文件名处理方法
# =============================
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
def _load_tree_data() -> Dict:
# 注意:实际运行时请确保 result.json 存在,此处仅为代码结构展示
if not os.path.exists(TREE_JSON_PATH):
return {}
with open(TREE_JSON_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def _resolve_ship_root(data: Dict, ship_code: str) -> Dict:
raw = data.get(ship_code)
if not isinstance(raw, dict):
return {}
# 结构 1直接是节点 {"code","name","children"}
if {"code", "name", "children"}.issubset(raw.keys()):
return raw
# 结构 2包装层 {"101": {...root node...}}
if ship_code in raw and isinstance(raw[ship_code], dict):
node = raw[ship_code]
if {"code", "name", "children"}.issubset(node.keys()):
return node
# 兜底:取第一个 node-like 值
for value in raw.values():
if isinstance(value, dict) and {"code", "name", "children"}.issubset(value.keys()):
return value
return {}
def _longest_common_substring_len(a: str, b: str) -> int:
if not a or not b:
return 0
# 优化:确保 a 是较短的字符串以节省空间(可选),原逻辑保持不动也没问题
dp = [0] * (len(b) + 1)
best = 0
for i in range(1, len(a) + 1):
prev = 0
for j in range(1, len(b) + 1):
temp = dp[j]
if a[i - 1] == b[j - 1]:
dp[j] = prev + 1
if dp[j] > best:
best = dp[j]
else:
dp[j] = 0
prev = temp
return best
def _match_level4_tool(filename_text: str, device_node: Dict) -> Tuple[str, str]:
"""
匹配 Level 4 零件
规则如果文件名与 Level 3 (设备名) 的最大共同字符串长度 >= 与任何 Level 4 (零件名) 的长度
则只匹配到 Level 3返回空
只有当 Level 4 的匹配度严格高于 Level 3 才返回 Level 4 信息
"""
children = device_node.get("children") or {}
# 1. 计算文件名与当前设备 (Level 3) 名称的匹配度
device_name = str(device_node.get("name") or "")
level3_score = _longest_common_substring_len(filename_text, device_name)
best_name = ""
best_code = ""
best_score = 0 # 记录 Level 4 中的最佳得分
# 2. 遍历 Level 4 子节点
for child in children.values():
tool_name = str(child.get("name") or "")
tool_code = str(child.get("code") or "")
score = _longest_common_substring_len(filename_text, tool_name)
# 关键修改:只有当 Level 4 的得分严格大于 Level 3 的得分时,才视为有效匹配
# 并且要比当前找到的其他 Level 4 更好
if score > level3_score and score > best_score:
best_score = score
best_name = tool_name
best_code = tool_code
# 如果没有找到比 Level 3 匹配度更高的 Level 4则返回空
if best_score <= level3_score:
return "", ""
return best_name, best_code
def process_filename(filename: str) -> Dict[str, str]:
name, _ = os.path.splitext(os.path.basename(filename))
parts = re.split(r"[_-]", name)
xian_number = ""
level_1_system_name = ""
level_2_system_name = ""
device_name = ""
tool_name = ""
tool_code = ""
if not parts:
return {}
xian_number_code = parts[0].strip()
level1_code = ""
level2_code = ""
device_code = ""
if len(parts) > 1:
part = parts[1].strip()
# 防止索引越界
if len(part) < 2:
return {}
level1_code = part[:2]
level2_code = part[2:4] if len(part) > 2 else ""
device_code = part[4:] if len(part) > 4 else ""
# 若 parts[2] 存在,则用 parts[2] 作为 device_code支持 122-06A0014-B01001_发动机 格式)
if len(parts) > 2:
device_code_alt = parts[2].strip()
if device_code_alt:
device_code = device_code_alt
data = _load_tree_data()
root = _resolve_ship_root(data, xian_number_code)
if not root:
return {}
xian_number = root.get("name", "")
# 一级
s1 = root.get("children", {}).get(level1_code)
if s1:
level_1_system_name = s1.get("name", "")
# 二级
s2 = s1.get("children", {}).get(level2_code)
if s2:
level_2_system_name = s2.get("name", "")
# 三级设备
device = s2.get("children", {}).get(device_code)
if device:
device_name = device.get("name", "")
# 若存在 level4匹配与文件名最大相同字符串的零件
# 内部已包含逻辑:如果设备名匹配度更高,则不返回零件
tool_name, tool_code = _match_level4_tool(name, device)
return {
"Xian_Number": xian_number,
"Xian_Number_code": xian_number_code,
"Level_1_System_Name": level_1_system_name,
"level1_code": level1_code,
"Level_2_System_Name": level_2_system_name,
"level2_code": level2_code,
"Device_Name": device_name,
"device_code": device_code,
"tool_name": tool_name,
"tool_code": tool_code,
}
def get_entity(filename):
result = process_filename(filename)
entities = []
relationships = []
low_level = ""
if result:
xian_number = result.get("Xian_Number")
xian_number_code = result.get("Xian_Number_code")
level_1_system_name = result.get("Level_1_System_Name")
level1_code = result.get("level1_code")
level_2_system_name = result.get("Level_2_System_Name")
level2_code = result.get("level2_code")
device_name = result.get("Device_Name")
device_code = result.get("device_code")
tool_name = result.get("tool_name")
tool_code = result.get("tool_code")
if xian_number:
entities.append(
{
"type": "舰艇",
"properties": {
"名称": xian_number,
"舷号": xian_number_code,
},
}
)
low_level = xian_number
if level_1_system_name:
entities.append(
{
"type": "系统",
"properties": {
"名称": level_1_system_name,
"系统编码": level1_code,
},
}
)
low_level = level_1_system_name
if xian_number and level_1_system_name:
relationships.append(
{
"type": "包含",
"from_entity": xian_number,
"to_entity": level_1_system_name,
}
)
if level_2_system_name:
entities.append(
{
"type": "子系统",
"properties": {
"名称": level_2_system_name,
"子系统编码": level2_code,
},
}
)
low_level = level_2_system_name
if level_1_system_name and level_2_system_name:
relationships.append(
{
"type": "包含",
"from_entity": level_1_system_name,
"to_entity": level_2_system_name,
}
)
if device_name:
entities.append(
{
"type": "设备",
"properties": {
"名称": device_name,
"设备编码": device_code,
},
}
)
low_level = device_name
if level_2_system_name and device_name:
relationships.append(
{
"type": "包含",
"from_entity": level_2_system_name,
"to_entity": device_name,
}
)
if tool_name:
entities.append(
{
"type": "零件",
"properties": {
"名称": tool_name,
"零件编码": tool_code,
},
}
)
low_level = tool_name
if device_name and tool_name:
relationships.append(
{
"type": "包含",
"from_entity": device_name,
"to_entity": tool_name,
}
)
final_result = {
"entities": entities,
"relationships": relationships,
"low_level": low_level,
}
return final_result
# 示例测试逻辑(非必须,仅供验证)
if __name__ == "__main__":
# 阀控蓄电池脉冲式快速充电装置
# test_list = [
# "163-06A0015-B01001_发动机-维修手册.pdf",
# ]
# for i in test_list:
# result = get_entity(i)
# print(result)
from neo4j import GraphDatabase
NEO4J_URI = "bolt://192.168.0.111:7687"
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD","zdht123@") # 不设默认值,强制要求提供
NEO4J_DATABASE = os.getenv("NEO4J_DATABASE", "neo4j")
driver = GraphDatabase.driver(
NEO4J_URI,
auth=(NEO4J_USER, NEO4J_PASSWORD),
database=NEO4J_DATABASE
)
res = fetch_graph_sample(driver)
print(res)

View File

@ -1,815 +0,0 @@
"""
PDF 用户上传接口FastAPI
- 处理文件名和其对应关系
"""
from typing import Dict, Tuple
import os
import re
from typing import Dict
from fastapi import FastAPI, HTTPException
import logging
import json
from modelsAPI.model_api import OpenaiAPI
from openai import OpenAI
from typing import Dict, List, Any
app = FastAPI(title="PDF Upload Service")
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s")
logger = logging.getLogger("filename_neo4j_process")
# =========================
# 图方法
# =========================
def node_to_json(node):
props = dict(node)
# ks_raw = props.get("knowledge_source")
# knowledge_source = json.loads(ks_raw) if ks_raw else []
ks_raw = props.get("knowledge_source")
knowledge_source = []
if ks_raw and isinstance(ks_raw, str):
try:
ks_list = json.loads(ks_raw)
if isinstance(ks_list, list):
for ks in ks_list:
if isinstance(ks, dict):
ks.pop("info", None)
knowledge_source.append(ks)
except (json.JSONDecodeError, TypeError):
# 解析失败或类型错误时,保持 knowledge_source 为空列表
pass
name = props.get("名称")
props.pop("embedding", None)
props.pop("knowledge_source", None)
props.pop("name", None) # 现在安全地移除
props.pop("fulltext", None)
if "last_updated" in props:
props["最后更新时间"] = props.pop("last_updated")
if "created_at" in props:
props["创建时间"] = props.pop("created_at")
return {
"id": node.element_id,
"name": name, # 使用提前保存的值
"labels": list(node.labels),
"knowledge_source": knowledge_source,
"properties": props,
}
def rel_to_json(rel):
props = dict(rel)
props.pop("embedding", None)
props.pop("fact_timeline", None)
return {"id": rel.element_id, "label": rel.type, "source": rel.start_node.element_id, "target": rel.end_node.element_id, "properties": props}
def is_simple_path(path):
"""
判断是否为无环路径simple path
"""
seen = set()
for n in path.nodes:
nid = n.element_id
if nid in seen:
return False
seen.add(nid)
return True
def build_tree(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
"""
按照固定层级构建树舰艇 -> 系统 -> 子系统
不再依赖 links 的连线直接根据 labels 归类彻底避免环状数据导致的死循环
"""
# 1. 辅助函数:判断标签是否包含关键字
def has_label(node, keyword):
labels = node.get("labels", [])
if isinstance(labels, list):
return any(keyword in lbl for lbl in labels)
return False
# 2. 辅助函数:构建标准的树节点格式
def make_node(node_data):
return {
"id": node_data["id"],
"name": node_data.get("name", ""),
"labels": node_data.get("labels", []),
"properties": node_data.get("properties", {}),
"color": node_data.get("color"),
"size": node_data.get("size"),
"children": []
}
# 3. 将节点按层级分类存储到字典中,方便快速查找
# key: id, value: node_dict_with_children
ships = {} # 第一层:舰艇
systems = {} # 第二层:系统(不含子系统)
sub_systems = {} # 第三层:子系统
for node in nodes:
# 注意:判断逻辑要互斥,防止“子系统”被误判为“系统”
if has_label(node, "舰艇"):
ships[node["id"]] = make_node(node)
elif has_label(node, "子系统"):
sub_systems[node["id"]] = make_node(node)
elif has_label(node, "系统"):
systems[node["id"]] = make_node(node)
else:
# 如果有其他无关节点,可以在这里处理或忽略
pass
# 4. 开始组装:利用 links 建立关系(此时 links 只充当“连接器”,不决定层级)
# 因为我们已经确定了层级,所以不会出现循环引用
for link in links:
if link.get("label") != "包含":
continue
src_id = link["source"]
tgt_id = link["target"]
# 逻辑 A: 舰艇 -> 系统
if src_id in ships and tgt_id in systems:
ships[src_id]["children"].append(systems[tgt_id])
# 逻辑 B: 系统 -> 子系统
elif src_id in systems and tgt_id in sub_systems:
systems[src_id]["children"].append(sub_systems[tgt_id])
# 逻辑 C: 舰艇 -> 子系统 (防止有些数据直接连了子系统,虽然不规范但也兼容一下)
elif src_id in ships and tgt_id in sub_systems:
ships[src_id]["children"].append(sub_systems[tgt_id])
# 5. 返回所有舰艇作为根节点列表
return list(ships.values())
def build_tree1(nodes: List[Dict], links: List[Dict]) -> List[Dict]:
"""
nodes + links 构建树结构
- 根节点label 包含 '舰艇' 的节点
- 父子关系links label == '包含' 的边 (source -> target)
- 支持多棵树多个舰艇根节点
"""
# 1. 建立 id -> node 的映射
node_map: Dict[str, Dict] = {n["id"]: n for n in nodes}
# 2. 建立 父id -> [子id列表] 的邻接表(只处理"包含"关系)
children_map: Dict[str, List[str]] = {n["id"]: [] for n in nodes}
parent_map: Dict[str, str] = {} # 子id -> 父id用于识别根节点
for link in links:
if link.get("label") == "包含":
src = link["source"]
tgt = link["target"]
if src in children_map:
children_map[src].append(tgt)
if tgt not in parent_map:
parent_map[tgt] = src
# 3. 递归构建子树
def build_subtree(node_id: str, ancestors: set = None) -> Dict[str, Any]:
if ancestors is None:
ancestors = set()
if node_id in ancestors:
return None
ancestors.add(node_id)
node = node_map[node_id]
subtree = {"id": node_id, "name": node.get("name", ""), "labels": node.get("labels", []), "properties": node.get("properties", {}), "color": node.get("color"), "size": node.get("size"), "children": []}
current_ancestors = ancestors | {node_id}
for child_id in children_map.get(node_id, []):
child = build_subtree(child_id, current_ancestors)
if child is not None:
subtree["children"].append(child)
return subtree
# def build_subtree(node_id: str) -> Dict[str, Any]:
# node = node_map[node_id]
# subtree = {
# "id": node_id,
# "name": node.get("name", ""),
# "labels": node.get("labels", []),
# "properties": node.get("properties", {}),
# "color": node.get("color"),
# "size": node.get("size"),
# "children": []
# }
# for child_id in children_map.get(node_id, []):
# subtree["children"].append(build_subtree(child_id))
# return subtree
# 4. 找出所有根节点label 包含 '舰艇',且没有父节点
roots = [n["id"] for n in nodes if "舰艇" in n.get("labels", []) and n["id"] not in parent_map]
# 5. 为每棵树构建结构,并打上树编号
forest = []
for i, root_id in enumerate(roots):
tree = build_subtree(root_id)
tree["tree_index"] = i # 区分不同树
forest.append(tree)
return forest
def build_graph_from_paths(paths):
"""
核心公共方法
Neo4j 路径结果构建 graphData
"""
node_map: Dict[str, dict] = {}
link_map: Dict[str, dict] = {}
def add_edge(rel):
s = rel.start_node
t = rel.end_node
if s.element_id not in node_map:
node_map[s.element_id] = node_to_json(s)
if t.element_id not in node_map:
node_map[t.element_id] = node_to_json(t)
if rel.element_id not in link_map:
link_map[rel.element_id] = rel_to_json(rel)
for record in paths:
p = record["p"]
if not is_simple_path(p):
continue
for r in p.relationships:
add_edge(r)
return {"nodes": list(node_map.values()), "links": list(link_map.values())}
# def fetch_graph_sample(driver,limit: int = 200):
# """提取采样逻辑为可复用函数"""
# with driver.session() as session:
# result = session.run("""
# MATCH (n)
# WITH n
# ORDER BY elementId(n) ASC
# LIMIT $limit
# MATCH (n)-[rel]->(m)
# RETURN n, rel, m
# """, limit=limit)
# node_map, link_map = {}, {}
# for rec in result:
# n, r, m = rec["n"], rec["rel"], rec["m"]
# node_map[n.element_id] = node_to_json(n)
# node_map[m.element_id] = node_to_json(m)
# link_map[r.element_id] = rel_to_json(r)
# return {
# "nodes": list(node_map.values()),
# "links": list(link_map.values())
# }
# def fetch_graph_sample(driver, limit: int = 200):
# """
# 获取采样数据:
# 1. 选取前 $limit 个种子节点Level 0 或 1取决于业务定义这里定义种子为 Level 0
# 2. 对每个节点向下探索最多 3 跳。
# 3. 每个节点对象中增加 level 字段,表示其距离种子节点的深度。
# """
# with driver.session() as session:
# # Cypher 逻辑:
# # - 选出种子节点 n
# # - 匹配 1-3 跳路径
# # - 返回种子节点 n路径 path以及该路径的长度即层级
# result = session.run("""
# MATCH (n)
# WITH n ORDER BY elementId(n) ASC LIMIT $limit
# OPTIONAL MATCH path = (n)-[*1..3]-(m)
# UNWIND nodes(path) AS x
# WITH n, path, m, COUNT(DISTINCT x) AS nodeCount
# WHERE path IS NULL OR nodeCount = LENGTH(path) + 1
# RETURN n, path, LENGTH(path) AS depth
# """, limit=limit)
# node_map, link_map = {}, {}
# for rec in result:
# seed_node = rec["n"]
# path = rec["path"]
# depth = rec["depth"] or 0 # 如果没有路径,深度为 0
# # 1. 处理种子节点 (Level 0)
# if seed_node.element_id not in node_map:
# node_data = node_to_json(seed_node)
# node_data["level"] = 0 # 种子节点设为 0 层
# node_map[seed_node.element_id] = node_data
# # 2. 如果存在路径,解析路径中的所有节点和关系
# if path:
# # 路径中的节点处理
# # path.nodes 包含了从起始到终点的所有节点
# for i, node in enumerate(path.nodes):
# if node.element_id not in node_map:
# node_data = node_to_json(node)
# # 层级即为该节点在当前路径中的索引
# node_data["level"] = i
# node_map[node.element_id] = node_data
# else:
# # 如果节点已存在,保留最小的 level (即最靠近种子的距离)
# node_map[node.element_id]["level"] = min(node_map[node.element_id].get("level", 3), i)
# # 路径中的关系处理
# for rel in path.relationships:
# if rel.element_id not in link_map:
# link_map[rel.element_id] = rel_to_json(rel)
# return {
# "nodes": list(node_map.values()),
# "links": list(link_map.values())
# }
# def fetch_graph_sample(driver, limit=200, max_depth=3):
# with driver.session() as session:
# result = session.run("""
# MATCH (c)
# WITH c ORDER BY elementId(c) ASC LIMIT $limit
# MATCH path = (c)-[*0..3]-(n)
# WITH n, MIN(length(path)) AS dist
# OPTIONAL MATCH (n)-[r]-(m)
# RETURN n, dist, r, m
# """, limit=limit)
# node_map, link_map = {}, {}
# for rec in result:
# n = rec["n"]
# dist = rec["dist"]
# r = rec["r"]
# m = rec["m"]
# # ---- 节点 ----
# nid = n.element_id
# if nid not in node_map:
# node_data = node_to_json(n)
# node_data["level"] = min(dist, 3) if dist is not None else 0
# node_map[nid] = node_data
# # ---- 边 ----
# if r and m:
# mid = m.element_id
# if mid not in node_map:
# node_map[mid] = node_to_json(m)
# link_map.setdefault(r.element_id, rel_to_json(r))
# return {
# "nodes": list(node_map.values()),
# "links": list(link_map.values())
# }
def fetch_graph_sample(driver):
"""
查询所有舰艇及其关联的系统和子系统
路径结构(舰艇) -> (系统) -> (子系统)
"""
with driver.session() as session:
# =========================
# 统一查询:舰艇 -> 系统 -> 子系统
# =========================
# 逻辑说明:
# 1. MATCH (ship:舰艇):选中所有舰艇
# 2. -[*1..3]->(target):查找深度为 1 到 3 的路径
# - 深度 1舰艇 -> 系统
# - 深度 2舰艇 -> 系统 -> 子系统
# 3. WHERE target:系统 OR target:子系统:确保终点是我们关心的节点类型
# (防止查询出其他无关的深层节点)
query = """
MATCH path = (ship:舰艇)-[*1..3]->(target)
WHERE target:系统 OR target:子系统
RETURN path
"""
result = session.run(query)
# =========================
# 解析 paths
# =========================
all_nodes = {}
all_links = {}
for record in result:
path = record["path"]
# 解析节点
for node in path.nodes:
# 兼容 ID 获取
node_key = node.element_id if hasattr(node, 'element_id') else node.id
if node_key not in all_nodes:
all_nodes[node_key] = node_to_json(node)
# 解析关系
for rel in path.relationships:
rel_key = rel.element_id if hasattr(rel, 'element_id') else rel.id
if rel_key not in all_links:
all_links[rel_key] = rel_to_json(rel)
return {
"nodes": list(all_nodes.values()),
"links": list(all_links.values())
}
def fetch_graph_sample1(driver):
"""
查找路径
1. 优先舰艇 -> 子系统
2. fallback舰艇 -> 系统
注意根据数据库实际结构使用 Label (:舰艇, :子系统) 进行匹配
而不是 WHERE category = '...'
"""
# 定义采样数量
SAMPLE_LIMIT = 400
with driver.session() as session:
# =========================
# 第一阶段:舰艇 -> 子系统
# =========================
# 修改点:直接使用 (:舰艇) 和 (:子系统) 标签匹配,去掉 WHERE 子句
result = session.run(
"""
MATCH (ship:舰艇)
WITH ship LIMIT $limit
MATCH path = (ship)-[*1..3]->(sub:子系统)
RETURN path
""",
limit=SAMPLE_LIMIT,
)
paths = [record["path"] for record in result]
# =========================
# fallback舰艇 -> 系统
# =========================
if not paths:
# 修改点:直接使用 (:舰艇) 和 (:系统) 标签匹配
result = session.run(
"""
MATCH (ship:舰艇)
WITH ship LIMIT $limit
MATCH path = (ship)-[*1..2]->(sys:系统)
RETURN path
""",
limit=SAMPLE_LIMIT,
)
paths = [record["path"] for record in result]
# =========================
# 解析 paths
# =========================
all_nodes = {}
all_links = {}
for path in paths:
# 节点
for node in path.nodes:
# 兼容 Neo4j 5.x (element_id) 和 4.x (id)
node_key = node.element_id if hasattr(node, "element_id") else node.id
if node_key not in all_nodes:
all_nodes[node_key] = node_to_json(node)
# 关系
for rel in path.relationships:
rel_key = rel.element_id if hasattr(rel, "element_id") else rel.id
if rel_key not in all_links:
all_links[rel_key] = rel_to_json(rel)
return {"nodes": list(all_nodes.values()), "links": list(all_links.values())}
# =============================
# 文件名处理方法
# =============================
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TREE_JSON_PATH = os.path.join(BASE_DIR, "tree_data.json")
def _load_tree_data() -> Dict:
# 注意:实际运行时请确保 result.json 存在,此处仅为代码结构展示
if not os.path.exists(TREE_JSON_PATH):
return {}
with open(TREE_JSON_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def _resolve_ship_root(data: Dict, ship_code: str) -> Dict:
raw = data.get(ship_code)
if not isinstance(raw, dict):
return {}
# 结构 1直接是节点 {"code","name","children"}
if {"code", "name", "children"}.issubset(raw.keys()):
return raw
# 结构 2包装层 {"101": {...root node...}}
if ship_code in raw and isinstance(raw[ship_code], dict):
node = raw[ship_code]
if {"code", "name", "children"}.issubset(node.keys()):
return node
# 兜底:取第一个 node-like 值
for value in raw.values():
if isinstance(value, dict) and {"code", "name", "children"}.issubset(value.keys()):
return value
return {}
def _longest_common_substring_len(a: str, b: str) -> int:
if not a or not b:
return 0
# 优化:确保 a 是较短的字符串以节省空间(可选),原逻辑保持不动也没问题
dp = [0] * (len(b) + 1)
best = 0
for i in range(1, len(a) + 1):
prev = 0
for j in range(1, len(b) + 1):
temp = dp[j]
if a[i - 1] == b[j - 1]:
dp[j] = prev + 1
if dp[j] > best:
best = dp[j]
else:
dp[j] = 0
prev = temp
return best
def _match_level4_tool(filename_text: str, device_node: Dict) -> Tuple[str, str]:
"""
匹配 Level 4 零件
规则如果文件名与 Level 3 (设备名) 的最大共同字符串长度 >= 与任何 Level 4 (零件名) 的长度
则只匹配到 Level 3返回空
只有当 Level 4 的匹配度严格高于 Level 3 才返回 Level 4 信息
"""
children = device_node.get("children") or {}
# 1. 计算文件名与当前设备 (Level 3) 名称的匹配度
device_name = str(device_node.get("name") or "")
level3_score = _longest_common_substring_len(filename_text, device_name)
best_name = ""
best_code = ""
best_score = 0 # 记录 Level 4 中的最佳得分
# 2. 遍历 Level 4 子节点
for child in children.values():
tool_name = str(child.get("name") or "")
tool_code = str(child.get("code") or "")
score = _longest_common_substring_len(filename_text, tool_name)
# 关键修改:只有当 Level 4 的得分严格大于 Level 3 的得分时,才视为有效匹配
# 并且要比当前找到的其他 Level 4 更好
if score > level3_score and score > best_score:
best_score = score
best_name = tool_name
best_code = tool_code
# 如果没有找到比 Level 3 匹配度更高的 Level 4则返回空
if best_score <= level3_score:
return "", ""
return best_name, best_code
def process_filename(filename: str) -> Dict[str, str]:
name, _ = os.path.splitext(os.path.basename(filename))
parts = re.split(r"[_-]", name)
xian_number = ""
level_1_system_name = ""
level_2_system_name = ""
device_name = ""
tool_name = ""
tool_code = ""
if not parts:
return {}
xian_number_code = parts[0].strip()
level1_code = ""
level2_code = ""
device_code = ""
if len(parts) > 1:
part = parts[1].strip()
# 防止索引越界
if len(part) < 2:
return {}
level1_code = part[:2]
level2_code = part[2:4] if len(part) > 2 else ""
device_code = part[4:] if len(part) > 4 else ""
data = _load_tree_data()
root = _resolve_ship_root(data, xian_number_code)
if not root:
return {}
xian_number = root.get("name", "")
# 一级
s1 = root.get("children", {}).get(level1_code)
if s1:
level_1_system_name = s1.get("name", "")
# 二级
s2 = s1.get("children", {}).get(level2_code)
if s2:
level_2_system_name = s2.get("name", "")
# 三级设备
device = s2.get("children", {}).get(device_code)
if device:
device_name = device.get("name", "")
# 若存在 level4匹配与文件名最大相同字符串的零件
# 内部已包含逻辑:如果设备名匹配度更高,则不返回零件
tool_name, tool_code = _match_level4_tool(name, device)
return {
"Xian_Number": xian_number,
"Xian_Number_code": xian_number_code,
"Level_1_System_Name": level_1_system_name,
"level1_code": level1_code,
"Level_2_System_Name": level_2_system_name,
"level2_code": level2_code,
"Device_Name": device_name,
"device_code": device_code,
"tool_name": tool_name,
"tool_code": tool_code,
}
def get_entity(filename):
result = process_filename(filename)
entities = []
relationships = []
low_level = ""
if result:
xian_number = result.get("Xian_Number")
xian_number_code = result.get("Xian_Number_code")
level_1_system_name = result.get("Level_1_System_Name")
level1_code = result.get("level1_code")
level_2_system_name = result.get("Level_2_System_Name")
level2_code = result.get("level2_code")
device_name = result.get("Device_Name")
device_code = result.get("device_code")
tool_name = result.get("tool_name")
tool_code = result.get("tool_code")
if xian_number:
entities.append(
{
"type": "舰艇",
"properties": {
"名称": xian_number,
"舷号": xian_number_code,
},
}
)
low_level = xian_number
if level_1_system_name:
entities.append(
{
"type": "系统",
"properties": {
"名称": level_1_system_name,
"系统编码": level1_code,
},
}
)
low_level = level_1_system_name
if xian_number and level_1_system_name:
relationships.append(
{
"type": "包含",
"from_entity": xian_number,
"to_entity": level_1_system_name,
}
)
if level_2_system_name:
entities.append(
{
"type": "子系统",
"properties": {
"名称": level_2_system_name,
"子系统编码": level2_code,
},
}
)
low_level = level_2_system_name
if level_1_system_name and level_2_system_name:
relationships.append(
{
"type": "包含",
"from_entity": level_1_system_name,
"to_entity": level_2_system_name,
}
)
if device_name:
entities.append(
{
"type": "设备",
"properties": {
"名称": device_name,
"设备编码": device_code,
},
}
)
low_level = device_name
if level_2_system_name and device_name:
relationships.append(
{
"type": "包含",
"from_entity": level_2_system_name,
"to_entity": device_name,
}
)
if tool_name:
entities.append(
{
"type": "零件",
"properties": {
"名称": tool_name,
"零件编码": tool_code,
},
}
)
low_level = tool_name
if device_name and tool_name:
relationships.append(
{
"type": "包含",
"from_entity": device_name,
"to_entity": tool_name,
}
)
final_result = {
"entities": entities,
"relationships": relationships,
"low_level": low_level,
}
return final_result
# 示例测试逻辑(非必须,仅供验证)
if __name__ == "__main__":
# 阀控蓄电池脉冲式快速充电装置
# test_list = [
# "163-06A0015-B01001_发动机-维修手册.pdf",
# ]
# for i in test_list:
# result = get_entity(i)
# print(result)
from neo4j import GraphDatabase
NEO4J_URI = "bolt://192.168.0.111:7687"
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD","zdht123@") # 不设默认值,强制要求提供
NEO4J_DATABASE = os.getenv("NEO4J_DATABASE", "neo4j")
driver = GraphDatabase.driver(
NEO4J_URI,
auth=(NEO4J_USER, NEO4J_PASSWORD),
database=NEO4J_DATABASE
)
res = fetch_graph_sample(driver)
print(res)

View File

@ -3,9 +3,9 @@ import json
import base64
import asyncio
import logging
from typing import Optional, Tuple, Dict
from typing import Any, Optional, Tuple, Dict
import aiofiles # pip install aiofiles
from config import SEARCH_DIR,IMAGE_DIR
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@ -16,7 +16,7 @@ async def find_and_read_content_list(
encoding: str = 'utf-8'
) -> Tuple[Optional[list], Optional[str]]:
"""根据原始文件名查找并读取对应的 _content_list.json 文件。"""
file_name, _ = os.path.splitext(original_filename)
file_name, _ = os.path.splitext(os.path.basename(original_filename))
target_filename = f"{file_name}_content_list.json"
logger.info(f"正在查找文件: {target_filename}")
@ -44,6 +44,93 @@ async def find_and_read_content_list(
return None, None
def find_image_record(content_list: Any, image_filename: str) -> Optional[dict]:
"""Find an image record by the basename stored in ``img_path``."""
if not isinstance(content_list, list) or not image_filename:
return None
target_name = os.path.basename(str(image_filename).strip().replace('\\', '/'))
if not target_name:
return None
for item in content_list:
if not isinstance(item, dict) or item.get('type') != 'image':
continue
img_path = item.get('img_path')
if isinstance(img_path, str):
path_name = os.path.basename(img_path.strip().replace('\\', '/'))
if path_name == target_name:
return item
return None
async def find_image_record_by_pdf(
directory: str,
pdf_filename: str,
image_filename: str,
encoding: str = 'utf-8',
) -> Optional[dict]:
"""Find an image record from the content list matching a PDF filename."""
content_list, _ = await find_and_read_content_list(
directory, pdf_filename, encoding=encoding
)
return find_image_record(content_list, image_filename)
async def extract_image_caption(
search_directory: str,
pdf_filename: str,
image_filename: str,
) -> list[str]:
"""?? PDF ??????????? image_caption?"""
image_record = await find_image_record_by_pdf(
directory=search_directory,
pdf_filename=pdf_filename,
image_filename=image_filename,
)
if not image_record:
return []
image_caption = image_record.get("image_caption", [])
if isinstance(image_caption, str):
return [image_caption] if image_caption.strip() else []
if isinstance(image_caption, list):
return [str(item).strip() for item in image_caption if str(item).strip()]
return []
async def find_and_read_md_file(
directory: str,
original_filename: str,
encoding: str = 'utf-8'
) -> Tuple[Optional[str], Optional[str]]:
"""Find and read the Markdown file matching the uploaded document basename."""
file_name, _ = os.path.splitext(original_filename)
target_filename = f"{file_name}.md"
logger.info(f"正在查找 Markdown 文件: {target_filename}")
def _walk_for_file():
for root, _, files in os.walk(directory):
if target_filename in files:
return os.path.join(root, target_filename)
return None
file_path = await asyncio.to_thread(_walk_for_file)
if file_path is None:
return None, None
try:
async with aiofiles.open(file_path, 'r', encoding=encoding, errors='ignore') as f:
return await f.read(), file_path
except Exception as e:
logger.error(f"读取 Markdown 文件 {file_path} 时发生错误: {e}")
return None, None
def extract_all_jpg_filenames(doc_data: list) -> set:
"""从文档数据中提取所有 .jpg 图片的纯文件名。纯 CPU 操作,不需要 async。"""
result_set = set()
@ -103,10 +190,9 @@ async def encode_images_to_base64(
async def process_document(
input_file_name: str,
search_dir: Optional[str] = "/app/mineru_output",
local_image_dir: Optional[str] = None,
concurrency: int = 32,
) -> Optional[Tuple[list, Dict[str, str]]]:
) -> Optional[Tuple[list, Dict[str, str], Optional[str]]]:
"""
异步处理文档:查找 content_list.json,提取图片引用,并发编码为 base64
@ -117,9 +203,10 @@ async def process_document(
concurrency: 图片编码的并发数上限,默认 32
Returns:
成功: (content_list, images_dict)
成功: (content_list, images_dict, md_content)
失败: None
"""
search_dir = SEARCH_DIR
content, content_path = await find_and_read_content_list(search_dir, input_file_name)
if content is None:
logger.error("❌ 未找到指定的 _content_list.json 文件。")
@ -128,6 +215,13 @@ async def process_document(
logger.info(f"✅ 找到文件: {content_path}")
logger.info(f"内容类型: {type(content).__name__}, 长度: {len(str(content))}")
md_content, md_path = await find_and_read_md_file(search_dir, input_file_name)
if md_content is None:
logger.info("未找到对应的 Markdown 文件。")
else:
logger.info(f"找到 Markdown 文件: {md_path}")
logger.info(f"Markdown 内容长度: {len(md_content)}")
if local_image_dir is None:
local_image_dir = os.path.join(os.path.dirname(content_path), "images")
logger.info(f"图片目录: {local_image_dir}")
@ -140,20 +234,19 @@ async def process_document(
)
logger.info(f"成功编码 {len(images_dict)} 张图片")
return content, images_dict
return content, images_dict, md_content
async def main():
result = await process_document(
input_file_name="163-06A0014-B01001_发动机-维修手册.pdf",
search_dir="/app/mineru_output",
concurrency=32,
)
if result is None:
return 1
content, images_dict = result
content, images_dict, md_content = result
print(f"\n=== 处理完成 ===")
print(f"文档段落数: {len(content)}")
print(f"图片数量: {len(images_dict)}")
@ -164,5 +257,12 @@ async def main():
if __name__ == "__main__":
exit_code = asyncio.run(main())
exit(exit_code)
result = asyncio.run(
extract_image_caption(
r"E:\ZKYNLP\Hjunproject\project0506\kgrag",
"163-06A0014-B01001_发动机-维修手册.pdf",
"23fa41fea5e5b5458d6f7df3f8e7e8033b6665bd23203946646651c3e56c2da7.jpg",
)
)
print(result)

142
generate_summary.py Normal file
View File

@ -0,0 +1,142 @@
"""
文件概述生成工具
wiki_engine 的文档概述能力独立出来
输入markdown 格式的文件内容
输出文件概述SUMMARY + markdown 正文
支持并发
"""
import asyncio
from typing import List, Optional
from config import LLM_CONFIG
from openai import AsyncOpenAI
# ==================== 配置 ====================
_MAX_CONCURRENCY = 8 # 最大并发数
_client: Optional[AsyncOpenAI] = None
_semaphore: Optional[asyncio.Semaphore] = None
def _get_client() -> AsyncOpenAI:
"""获取或创建 AsyncOpenAI 单例客户端"""
global _client
if _client is None:
_client = AsyncOpenAI(api_key=LLM_CONFIG['api_key'], base_url=LLM_CONFIG['base_url'])
return _client
def _get_semaphore() -> asyncio.Semaphore:
"""获取或创建并发信号量"""
global _semaphore
if _semaphore is None:
_semaphore = asyncio.Semaphore(_MAX_CONCURRENCY)
return _semaphore
# ==================== Prompt与 wiki_builder.py 的 WIKI_SUMMARY_PROMPT 一致,仅去掉 extracted_slugs 相关部分) ====================
SUMMARY_PROMPT = """You are a wiki editor. Given the following document content, create a structured wiki summary page in Markdown format.
<document>
<content>
{content}
</content>
</document>
<instructions>
1. The FIRST line of your output MUST be: SUMMARY: {{one sentence, 15-40 words, describing what this document is about for wiki index listing}}
2. Create a concise but useful document-level summary.
3. Include the document's main subject, scope, important procedures, standards, systems, equipment, tables, fields.
4. Do NOT invent facts. Stay grounded in the document content.
5. Write in Chinese.
6. If the content is empty or has no substantive information, output exactly: "SUMMARY: No textual content was extractable from this document." followed by a brief note.
</instructions>
Output the SUMMARY line first, then the Markdown content. Do not include any other preamble."""
async def generate_summary(content: str) -> str:
"""
markdown 内容生成文件概述
Args:
content: markdown 格式的文件内容
Returns:
文件概述SUMMARY + markdown 正文
"""
if not content or not content.strip():
return ""
prompt = SUMMARY_PROMPT.format(content=content)
client = _get_client()
async with _get_semaphore():
try:
response = await client.chat.completions.create(
model=LLM_CONFIG['model'],
messages=[
{"role": "system", "content": "You are a grounded wiki editor. Do not invent facts."},
{"role": "user", "content": prompt},
],
temperature=0.1,
max_tokens=LLM_CONFIG['max_tokens'],
stream=False,
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
return (response.choices[0].message.content or "").strip()
except Exception as exc:
print(f"[generate_summary] 概述生成失败: {exc}")
return ""
async def generate_summaries(contents: List[str]) -> List[str]:
"""
并发生成多个文件的概述
Args:
contents: markdown 格式的文件内容列表
Returns:
文件概述列表顺序与输入一致
"""
return await asyncio.gather(*(generate_summary(c) for c in contents))
if __name__ == "__main__":
# import sys
from pathlib import Path
# if len(sys.argv) > 1:
# with open(sys.argv[1], "r", encoding="utf-8") as f:
# test_content = f.read()
# else:
# test_content = """
# # 船舶动力系统维护规程
# ## 概述
# 本文档详细介绍了船舶动力系统的日常维护和故障处理流程。
# ## 发动机日常检查
# - 润滑油位检查:每日检查发动机润滑油位,保持在标尺正常范围
# - 冷却液位检查:确保冷却系统液位正常,无泄漏
# - 皮带张紧度:检查传动皮带张紧度,过松或过紧均需调整
# ## 冷却系统维护
# 定期清洗热交换器,检查水泵密封性,更换老化管路。
# ## 故障诊断流程
# 1. 现象观察:记录故障现象和发生条件
# 2. 数据采集:收集运行参数和报警信息
# 3. 原因分析:对照标准参数分析故障原因
# 4. 处理方案:制定维修方案并执行
# ## 安全注意事项
# 所有维护操作必须在停机状态下进行,操作人员需佩戴防护装备。
# """
md_path = Path(r"E:\ZKYNLP\Hjunproject\project0506\kgrag\船舶主机燃油泵自动控制系统故障树分析.md")
with open(md_path, "r", encoding="utf-8") as f:
test_content = f.read()
result = asyncio.run(generate_summary(test_content))
print(result)

View File

@ -5,9 +5,13 @@
"""
import logging
from typing import Dict, List, Any, Optional
import re
from typing import Dict, List, Any, Optional, Set
logger = logging.getLogger(__name__)
EXCLUDED_RESULT_NODE_LABELS = {"维修工作", "操作程序", "操作使用"}
_FILTERED_OUT = object()
class SimpleNode:
"""
@ -537,6 +541,110 @@ def format_entry_nodes_as_results(entry_nodes: dict) -> List[Dict[str, Any]]:
return results
def _normalize_label_values(labels: Any) -> Set[str]:
"""将 labels/标签/type 等字段统一成标签集合。"""
if labels is None:
return set()
if isinstance(labels, str):
parts = re.split(r"[,,、/\s]+", labels)
return {part.strip("[]'\" ") for part in parts if part.strip("[]'\" ")}
if isinstance(labels, (list, tuple, set, frozenset)):
return {str(label).strip() for label in labels if str(label).strip()}
return {str(labels).strip()} if str(labels).strip() else set()
def _dict_label_values(data: Dict[str, Any]) -> Set[str]:
label_keys = ("labels", "标签", "label", "类型", "type", "节点类型", "node_type")
labels = set()
for key in label_keys:
if key in data:
labels.update(_normalize_label_values(data.get(key)))
return labels
def _has_excluded_result_label(value: Any) -> bool:
"""判断一个结果对象是否明确属于需要从 results 隐藏的节点类型。"""
labels = set()
if isinstance(value, dict):
labels.update(_dict_label_values(value))
elif hasattr(value, "labels") and (hasattr(value, "element_id") or hasattr(value, "id")):
try:
labels.update(_normalize_label_values(value.labels))
except Exception:
pass
return bool(labels & EXCLUDED_RESULT_NODE_LABELS)
def _mentions_excluded_result_label(text: Any) -> bool:
if text is None:
return False
return any(label in str(text) for label in EXCLUDED_RESULT_NODE_LABELS)
def _is_empty_result_content(value: Any) -> bool:
return value in (None, {}, [])
def _filter_excluded_result_content(value: Any, parent_key: Any = None) -> Any:
"""
只用于 results 字段过滤掉维修工作操作程序操作使用这类节点内容
nodes/links 的原始返回不会经过这个函数
"""
if parent_key is not None and _mentions_excluded_result_label(parent_key):
return _FILTERED_OUT
if _has_excluded_result_label(value):
return _FILTERED_OUT
if isinstance(value, dict):
name_value = value.get("名称", value.get("name"))
if _mentions_excluded_result_label(name_value):
return _FILTERED_OUT
filtered = {}
for key, child in value.items():
if _mentions_excluded_result_label(key):
continue
filtered_child = _filter_excluded_result_content(child, key)
if filtered_child is not _FILTERED_OUT:
filtered[key] = filtered_child
return _FILTERED_OUT if _is_empty_result_content(filtered) else filtered
if isinstance(value, list):
filtered_items = []
for item in value:
filtered_item = _filter_excluded_result_content(item)
if filtered_item is not _FILTERED_OUT and not _is_empty_result_content(filtered_item):
filtered_items.append(filtered_item)
return filtered_items
return value
def _filter_results_for_output(results: Any) -> List[Any]:
filtered = _filter_excluded_result_content(results)
if filtered is _FILTERED_OUT or _is_empty_result_content(filtered):
return []
if isinstance(filtered, list):
return filtered
return [filtered]
def _filter_result_nodes_for_output(nodes: Any) -> List[Dict[str, Any]]:
if not isinstance(nodes, list):
return []
return [
node for node in nodes
if isinstance(node, dict) and not _has_excluded_result_label(node)
]
def _format_value(value: Any) -> str:
"""
@ -649,8 +757,9 @@ def serialize_graph_for_llm(graph_response: dict) -> str:
- results任意查询返回值聚合/记录/字符串等
"""
data = graph_response.get("data", {})
nodes = data.get("nodes", [])
results = data.get("results", [])
# 这里的过滤只影响 results 字段里的文本展示,不改动接口返回的 data.nodes/data.links。
nodes = _filter_result_nodes_for_output(data.get("nodes", []))
results = _filter_results_for_output(data.get("results", []))
output_lines = []
@ -695,3 +804,4 @@ def serialize_graph_for_llm(graph_response: dict) -> str:
return "图谱查询完成,但未返回有效数据。"
return "\n".join(output_lines).rstrip()

View File

@ -0,0 +1,697 @@
"""
结果格式化模块
功能:从 Neo4j 查询结果中提取节点和路径信息
"""
import logging
from typing import Dict, List, Any, Optional
logger = logging.getLogger(__name__)
class SimpleNode:
"""
简单的Node模拟类用于将节点数据包装为Neo4j Node对象格式降级策略
"""
def __init__(self, node_data: dict, labels: List[str], node_id: str):
"""
Args:
node_data: 节点数据字典
labels: 节点标签列表
node_id: 节点ID
"""
self._data = node_data
self._labels = labels
self._node_id = node_id
@property
def element_id(self):
"""返回节点ID"""
return self._node_id
@property
def id(self):
"""返回节点ID兼容旧版本"""
return self._node_id
@property
def labels(self):
"""返回节点标签"""
return self._labels
def get(self, key, default=None):
"""获取节点属性"""
return self._data.get(key, default)
def __iter__(self):
"""使节点可以转换为字典"""
return iter(self._data.items())
def __getitem__(self, key):
"""使节点可以像字典一样访问"""
return self._data[key]
class SimplePath:
"""
简单的Path模拟类用于将节点包装为path结构降级策略
包含nodes和relationships属性其中relationships为空列表
"""
def __init__(self, nodes: List):
"""
Args:
nodes: 节点列表SimpleNode对象列表
"""
self.nodes = nodes
self.relationships = [] # 降级策略中,只有节点,没有关系
# def extract_knowledge_source(knowledge_source_value: Any) -> List[Dict[str, str]]:
# """
# 从节点属性中的 knowledge_source 字段提取信息并格式化为标准格式
#
# Args:
# knowledge_source_value: 节点属性中的 knowledge_source 值(可能是字符串、字典、列表等)
#
# Returns:
# List[Dict]: 格式化后的 knowledge_source 列表,格式为 [{"filename":"","url":"","info":""}]
# """
# if knowledge_source_value is None:
# return [{"filename": "", "url": "", "info": ""}]
#
# result = []
#
# # 如果是字符串尝试解析为JSON或直接使用
# if isinstance(knowledge_source_value, str):
# # 尝试解析JSON字符串
# try:
# import json
# parsed = json.loads(knowledge_source_value)
# if isinstance(parsed, list):
# knowledge_source_value = parsed
# elif isinstance(parsed, dict):
# knowledge_source_value = [parsed]
# else:
# # 如果不是JSON将字符串作为info
# return [{"filename": "", "url": "", "info": knowledge_source_value}]
# except:
# # 解析失败将字符串作为info
# return [{"filename": "", "url": "", "info": knowledge_source_value}]
#
# # 如果是字典,转换为列表
# if isinstance(knowledge_source_value, dict):
# knowledge_source_value = [knowledge_source_value]
#
# # 如果是列表,处理每个元素
# if isinstance(knowledge_source_value, list):
# for item in knowledge_source_value:
# if isinstance(item, dict):
# # 从字典中提取字段
# formatted_item = {
# "filename": str(item.get("filename", item.get("file_name", item.get("file", "")))),
# "url": str(item.get("url", item.get("link", item.get("uri", "")))),
# "info": str(item.get("info", item.get("information", item.get("description", ""))))
# }
# result.append(formatted_item)
# elif isinstance(item, str):
# # 如果是字符串作为info
# result.append({"filename": "", "url": "", "info": item})
# else:
# # 其他类型转换为字符串作为info
# result.append({"filename": "", "url": "", "info": str(item)})
# else:
# # 其他类型转换为字符串作为info
# result.append({"filename": "", "url": "", "info": str(knowledge_source_value)})
#
# # 如果没有结果,返回默认值
# if not result:
# result = [{"filename": "", "url": "", "info": ""}]
#
# return result
def extract_knowledge_source(knowledge_source_value: Any) -> List[Dict[str, str]]:
"""
从节点属性中的 knowledge_source 字段提取信息并格式化为标准格式(不含 info 字段)
Args:
knowledge_source_value: 节点属性中的 knowledge_source 值(可能是字符串、字典、列表等)
Returns:
List[Dict]: 格式化后的 knowledge_source 列表,格式为 [{"filename":"","url":""}]
"""
if knowledge_source_value is None:
return [{"filename": "", "url": ""}]
result = []
# 如果是字符串尝试解析为JSON或直接使用
if isinstance(knowledge_source_value, str):
# 尝试解析JSON字符串
try:
import json
parsed = json.loads(knowledge_source_value)
if isinstance(parsed, list):
knowledge_source_value = parsed
elif isinstance(parsed, dict):
knowledge_source_value = [parsed]
else:
# 如果不是JSON跳过该字符串
return [{"filename": "", "url": ""}]
except:
# 解析失败,跳过该字符串
return [{"filename": "", "url": ""}]
# 如果是字典,转换为列表
if isinstance(knowledge_source_value, dict):
knowledge_source_value = [knowledge_source_value]
# 如果是列表,处理每个元素
if isinstance(knowledge_source_value, list):
for item in knowledge_source_value:
if isinstance(item, dict):
# 从字典中提取字段(不含 info
formatted_item = {
"filename": str(item.get("filename", item.get("file_name", item.get("file", "")))),
"url": str(item.get("url", item.get("link", item.get("uri", ""))))
}
result.append(formatted_item)
elif isinstance(item, str):
# 如果是字符串,跳过
continue
else:
# 其他类型,跳过
continue
else:
# 其他类型,跳过
return [{"filename": "", "url": ""}]
# 如果没有结果,返回默认值
if not result:
result = [{"filename": "", "url": ""}]
return result
def remove_sensitive_fields(data: Any) -> Any:
"""
递归删除数据中的 embedding、fulltext 和 path 字段
参考 test.py 的实现方式:在转换为 dict 后立即 pop 掉敏感字段
Args:
data: 要处理的数据可以是字典、列表、Neo4j对象或基本类型
Returns:
清理后的数据
"""
# 如果是 Neo4j Node 对象,转换为字典并删除敏感字段(参考 test.py
if hasattr(data, 'labels') and (hasattr(data, 'element_id') or hasattr(data, 'id')):
try:
props = dict(data)
props.pop('embedding', None)
props.pop('fulltext', None)
# 递归处理属性中的嵌套结构
return remove_sensitive_fields(props)
except:
return data
# 如果是 Neo4j Relationship 对象,转换为字典并删除敏感字段
if hasattr(data, 'type') and (hasattr(data, 'element_id') or hasattr(data, 'id')):
try:
props = dict(data)
props.pop('embedding', None)
props.pop('fulltext', None)
# 递归处理属性中的嵌套结构
return remove_sensitive_fields(props)
except:
return data
if isinstance(data, dict):
# 创建新字典,排除 embedding、fulltext 和 path
result = {}
for key, value in data.items():
if key not in ['embedding', 'fulltext', 'path']:
# 递归处理嵌套的字典、列表和 Neo4j 对象
result[key] = remove_sensitive_fields(value)
return result
elif isinstance(data, list):
# 递归处理列表中的每个元素
return [remove_sensitive_fields(item) for item in data]
else:
# 基本类型直接返回
return data
def format_results(results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
格式化图谱查询结果,提取所有节点和路径(自动去重)
从 rerank 后的结果字典列表中提取 path 信息,生成 nodes 和 links
Args:
results: rerank 后的结果列表(字典列表,每个字典包含 rerank_score 和 path 等信息)
Returns:
Dict: 包含 nodes 和 links 的字典(已去重,只保留最高分)
"""
if not results:
return {"nodes": [], "links": []}
all_nodes = {} # key: node_id(str), value: node dict
all_links = {} # key: rel_id(str), value: link dict
node_scores = {} # key: node_id(str), value: highest rerank score for the node
def process_path(path, current_score):
"""处理单个 Path 对象"""
if path is None:
return
def process_node(node):
"""处理单个节点的辅助函数"""
if node is None:
return
node_id = str(node.element_id) if hasattr(node, 'element_id') else str(node.id)
if node_id not in all_nodes:
props = dict(node)
props.pop('embedding', None)
props.pop('fulltext', None)
props.pop('切片', None)
# 提取 name 用于外部字段从props中提取后移除避免出现在properties中
node_name = props.pop('name', None) or node.get("名称", f"Node_{node_id}")
# 直接过滤掉 last_updated 和 created_at
props.pop('last_updated', None)
props.pop('created_at', None)
# 提取并格式化 knowledge_source从props中提取后移除原始值
knowledge_source_value = props.pop('knowledge_source', None)
formatted_knowledge_source = extract_knowledge_source(knowledge_source_value)
all_nodes[node_id] = {
"id": node_id,
"name": node_name,
"labels": list(node.labels),
"properties": props,
"knowledge_source": formatted_knowledge_source
}
# 初始化节点的最高分数
if current_score > 0:
node_scores[node_id] = current_score
else:
# 如果节点已存在,比较并保留最高分数
existing_score = node_scores.get(node_id, 0)
if current_score > existing_score:
node_scores[node_id] = current_score
try:
# 处理节点:优先使用 path.nodes如果为空则使用 start/end 或 start_node/end_node
nodes_processed = False
# 首先尝试使用 path.nodes
if hasattr(path, 'nodes'):
try:
# 直接迭代 path.nodes可能是生成器
for node in path.nodes:
process_node(node)
nodes_processed = True
except Exception as e:
logger.debug(f"迭代 path.nodes 时出错: {e}")
# 如果 path.nodes 没有处理任何节点size=0的情况尝试使用 start/end 节点
if not nodes_processed:
start_node = None
end_node = None
# 尝试多种方式获取起始节点(优先使用 start_node其次使用 start
if hasattr(path, 'start_node'):
try:
start_node = path.start_node
except Exception:
pass
elif hasattr(path, 'start'):
try:
start_node = path.start
except Exception:
pass
# 尝试多种方式获取结束节点(优先使用 end_node其次使用 end
if hasattr(path, 'end_node'):
try:
end_node = path.end_node
except Exception:
pass
elif hasattr(path, 'end'):
try:
end_node = path.end
except Exception:
pass
# 处理起始节点
if start_node is not None:
process_node(start_node)
# 处理结束节点(只有当与起始节点不同时才处理,避免重复)
if end_node is not None and (start_node is None or end_node != start_node):
process_node(end_node)
# 处理关系
for rel in path.relationships:
rel_id = str(rel.element_id) if hasattr(rel, 'element_id') else str(rel.id)
if rel_id not in all_links:
rel_props = dict(rel)
rel_props.pop('embedding', None)
rel_props.pop('fulltext', None)
start_node_id = str(rel.start_node.element_id) if hasattr(rel.start_node, 'element_id') else str(rel.start_node.id)
end_node_id = str(rel.end_node.element_id) if hasattr(rel.end_node, 'element_id') else str(rel.end_node.id)
all_links[rel_id] = {
"id": rel_id,
"label": rel.type,
"source": start_node_id,
"target": end_node_id,
"properties": rel_props
}
except Exception as e:
logger.warning(f"处理 Path 对象时出错: {e}", exc_info=True)
for result in results:
try:
# 获取当前记录的rerank_score
current_rerank_score = result.get('rerank_score', 0)
# 查找 path 字段(可能是 path 或 paths
path_value = None
if 'path' in result:
path_value = result['path']
elif 'paths' in result:
path_value = result['paths']
else:
# 尝试从所有值中查找 Path 对象
for key, value in result.items():
if value is not None and hasattr(value, 'nodes') and hasattr(value, 'relationships'):
path_value = value
break
# 处理 path_value
if path_value is not None:
# 处理单个 Path 对象
if hasattr(path_value, 'nodes') and hasattr(path_value, 'relationships'):
process_path(path_value, current_rerank_score)
# 处理路径列表
elif isinstance(path_value, list):
if path_value: # 列表不为空
for path in path_value:
if path is not None and hasattr(path, 'nodes') and hasattr(path, 'relationships'):
process_path(path, current_rerank_score)
except Exception as e:
logger.warning(f"格式化结果记录失败: {e}", exc_info=True)
# 为每个节点添加最高rerank分数到其属性中只保留高分
for node_id, score in node_scores.items():
if node_id in all_nodes:
node = all_nodes[node_id]
if 'properties' not in node:
node['properties'] = {}
# 只保留最高分,使用 分数 作为 key
node['properties']['分数'] = score
# 最终清理:确保所有嵌套数据中的敏感字段都被删除
cleaned_nodes = [remove_sensitive_fields(node) for node in all_nodes.values()]
cleaned_links = [remove_sensitive_fields(link) for link in all_links.values()]
return {
"nodes": cleaned_nodes,
"links": cleaned_links
}
def format_entry_nodes_as_results(entry_nodes: dict) -> List[Dict[str, Any]]:
"""
将入口节点信息格式化为检索结果(降级策略)
将节点作为path节点作为结果
Args:
entry_nodes: 入口节点字典,格式如 {"设备": [{"name": "xxx", "score": 0.9, "node": {...}}]}
Returns:
List[Dict]: 结果列表每个元素是一个结果记录包含path字段用于 graph_retrieval.py 的降级策略)
"""
results = []
if not entry_nodes:
return results
for label, nodes in entry_nodes.items():
if not isinstance(nodes, list) or not nodes:
continue
# 标签列表优先使用entry_nodes中的label如果节点数据中有labels则使用节点数据的labels
label_list = [label] if label else []
for node_info in nodes:
node_name = node_info.get("name") or node_info.get("_name") or (node_info.get("node", {}).get("名称")) or str(node_info.get("node", ""))
node_data = node_info.get("node", {})
node_score = node_info.get("score", 0.8)
# 如果node_data是Neo4j Node对象转换为字典
if hasattr(node_data, 'labels') and (hasattr(node_data, 'element_id') or hasattr(node_data, 'id')):
# 从Neo4j Node对象提取labels
node_labels = list(node_data.labels) if hasattr(node_data, 'labels') else label_list
if node_labels:
label_list = node_labels
# 转换为字典
node_data = dict(node_data)
# 跳过无效节点
if not isinstance(node_data, dict) and not node_name:
continue
# 提取节点ID兜底情况下使用简单的ID格式
if isinstance(node_data, dict):
# 优先使用element_id或idNeo4j原生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()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,19 @@ from neo4j_graphrag.embeddings.base import Embedder
from config import LLM_CONFIG, EMBEDDING_CONFIG
# 模块级单例客户端,避免每次调用都创建新连接
_async_client: AsyncOpenAI = None
def _get_async_client() -> AsyncOpenAI:
"""获取或创建 AsyncOpenAI 单例客户端"""
global _async_client
if _async_client is None:
_async_client = AsyncOpenAI(
api_key=LLM_CONFIG["api_key"],
base_url=LLM_CONFIG["base_url"],
)
return _async_client
class OpenaiAPI:
@staticmethod
@ -148,7 +160,79 @@ class OpenaiAPI:
extra_body={"chat_template_kwargs": {"enable_thinking": False}}
)
return response.choices[0].message.content
@staticmethod
async def open_api_vl_without_thinking(
image_url: str
) -> str:
"""
专门用于从图片中提取设备名称故障现象
自动过滤思考过程仅返回核心结果
参数:
image_url: Base64 格式的图片数据 (data:image/...;base64,...)
model: 模型名称默认为配置中的模型
custom_instruction: 额外的特定指令 (可选)
"""
# 1. 确定模型名称
model = LLM_CONFIG.get("model")
# 2. 构建强约束的 System Prompt
# 核心目标:禁止思考标签,禁止废话,只给结果
system_prompt = (
"你是一个视觉分析专家。你的任务是从图片中识别内容并进行描述。\n"
"【严格约束】\n"
"1. 绝对禁止输出 <think>, <think>, <reasoning> 等任何思考过程标签。\n"
"2. 绝对禁止输出'好的''根据图片''分析如下'等开场白或结束语。\n"
"3. 直接输出最终结论,格式必须严格遵守下面的模板。"
)
# 3. 构建针对性的 User Prompt
default_task = (
"请分析这张图片,描述图片内容,重点关注以下信息\n"
"1. 图片整体阐述了什么主题,描述了哪些内容\n"
"请用专业、客观、简短的语言描述。"
)
final_user_prompt = default_task
# 4. 初始化客户端
client = _get_async_client()
# 5. 构建请求参数
kwargs = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": [
{"type": "text", "text": final_user_prompt},
{"type": "image_url", "image_url": {"url": image_url}}
]
}
],
"temperature": 0.1, # 低温度以保证事实准确性
"stream": False,
"max_tokens": LLM_CONFIG.get("max_tokens"),
}
# 尝试通过参数关闭思考 (取决于后端支持情况)
kwargs["extra_body"] = {"chat_template_kwargs": {"enable_thinking": False}}
try:
print(f"🚀 正在调用 {model} 进行图片内容分析...")
response = await client.chat.completions.create(**kwargs)
raw_content = response.choices[0].message.content or ""
print(f"✅ 分析完成:\n{raw_content}")
return raw_content
except Exception as e:
import traceback
error_msg = f"❌ 图片内容分析失败:{str(e)}\n{traceback.format_exc()}"
print(error_msg)
return f"Error: {str(e)}"
@staticmethod
async def open_api_chat_async_json(query: str, model: str = None, temperature: float = None) -> str:
if model is None:

File diff suppressed because one or more lines are too long

View File

@ -35,6 +35,8 @@ EMBED_MAX_WORKERS = SEARCH_CONFIG["embed_thread_num"] # embedding 批次并行
NODE_FETCH_PAGE = SEARCH_CONFIG["node_fetch_page"] # 单次拉取待生成 embedding 节点的分页大小
# 全局索引名称(覆盖所有节点标签)
GLOBAL_VECTOR_INDEX_NAME = SEARCH_CONFIG["global_entity_embedding"]
GLOBAL_FULLTEXT_INDEX_NAME = SEARCH_CONFIG["global_entity_content_search"]
DEFAULT_URI = NEO4J_CONFIG["uri"]
DEFAULT_AUTH = (
NEO4J_CONFIG["username"],
@ -55,6 +57,8 @@ OLD_INDEX_NAMES: List[str] = [
VECTOR_INDEX_NAME,
FULLTEXT_INDEX_NAME,
FULLTEXT_FIELD_INDEX_NAME,
GLOBAL_VECTOR_INDEX_NAME,
GLOBAL_FULLTEXT_INDEX_NAME,
]
@ -188,6 +192,390 @@ def drop_index_without_constraint(driver):
driver.execute_query(f"drop index {record['name']} if exists")
# --------- 创建全局混合索引(覆盖所有节点标签) ---------
def create_global_indexes(driver, force_refresh=False):
"""
创建覆盖所有节点标签的全局向量索引和全文索引
与按标签创建的索引互补支持跨标签统一检索
Args:
driver: Neo4j driver
force_refresh: True=先删除旧索引再重建False=IF NOT EXISTS 跳过已有索引
"""
if force_refresh:
for index_name in (GLOBAL_VECTOR_INDEX_NAME, GLOBAL_FULLTEXT_INDEX_NAME):
try:
driver.execute_query(f"DROP INDEX {index_name} IF EXISTS")
logger.info(f"强制刷新:已删除全局索引 {index_name}")
except Exception as e:
logger.warning(f"删除全局索引 {index_name} 时出错(可能不存在): {e}")
# 全局向量索引(作用于所有节点的 embedding 属性)
try:
driver.execute_query(f"""
CREATE VECTOR INDEX {GLOBAL_VECTOR_INDEX_NAME} IF NOT EXISTS
FOR (n) ON n.embedding
OPTIONS {{indexConfig: {{
`vector.dimensions`: {vector_dim},
`vector.similarity_function`: 'cosine'
}}}}
""")
logger.info(f"全局向量索引 '{GLOBAL_VECTOR_INDEX_NAME}' 已就绪")
except Exception as e:
logger.error(f"创建全局向量索引失败: {e}")
raise
# 全局全文索引(作用于所有节点的 名称 和 内容 属性)
try:
driver.execute_query(f"""
CREATE FULLTEXT INDEX {GLOBAL_FULLTEXT_INDEX_NAME} IF NOT EXISTS
FOR (n) ON EACH [n.名称, n.内容]
""")
logger.info(f"全局全文索引 '{GLOBAL_FULLTEXT_INDEX_NAME}' 已就绪")
except Exception as e:
logger.error(f"创建全局全文索引失败: {e}")
raise
# --------- 创建向量索引 ---------
def vector_indexing(driver, label, force_refresh=False):
"""
创建向量索引如果节点没有 embedding使用"名称"属性生成 embedding
说明
- 待生成 embedding 的节点通过分页循环全部处理不再受单次 LIMIT 限制
避免节点数超过一万时被静默丢弃
- embedding 计算采用并行批次以缩短耗时
Args:
driver: Neo4j driver
label: 节点标签
force_refresh: 是否强制刷新如果索引已存在force_refresh=False 时会跳过True 时会先删除再创建
"""
# === 分页处理所有缺失 embedding 的节点 ===
# 每轮取一页未生成 embedding 的节点并写回,直到没有剩余节点为止。
# 这样即使待处理节点远超一万也能全部覆盖。
total_generated = 0
while True:
query_no_embedding = f"""
MATCH (n:{label})
WHERE n.embedding IS NULL
AND n.名称 IS NOT NULL
RETURN elementId(n) AS id, n.名称 AS 名称
LIMIT {NODE_FETCH_PAGE}
"""
result_no_embedding = driver.execute_query(query_no_embedding)
nodes_to_process = [
(record["id"], record["名称"]) for record in result_no_embedding.records
]
if not nodes_to_process:
break
logger.info(f"标签 {label} 本轮发现 {len(nodes_to_process)} 个节点需要生成 embedding")
ids, names = zip(*nodes_to_process)
ids = list(ids)
names = list(names)
# 并行计算 embedding
logger.info(f"开始为 {len(names)} 个节点计算 embedding并行批次...")
try:
all_embeddings = _compute_embeddings_parallel(names)
except Exception as e:
logger.error(f"批量嵌入向量计算失败: {e}")
raise
# 转换为 numpy 数组并做 L2 归一化
embeddings = np.array(all_embeddings)
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
norms = np.where(norms == 0, 1, norms) # 避免除零
embeddings = embeddings / norms
# 保存 embedding 到节点
logger.info(f"写入 {len(ids)} 个节点的 embedding...")
upsert_vectors(
driver,
ids=ids,
embedding_property="embedding",
embeddings=embeddings,
)
total_generated += len(ids)
logger.info(f"本轮成功为 {len(ids)} 个节点生成并保存 embedding")
# 不足一页说明已处理完,提前结束
if len(nodes_to_process) < NODE_FETCH_PAGE:
break
if total_generated:
logger.info(f"标签 {label} 共生成 {total_generated} 个节点的 embedding")
# 检查是否有节点存在 embedding包括刚生成的
check_query = f"MATCH (n:{label}) WHERE n.embedding IS NOT NULL RETURN count(n) AS count"
result = driver.execute_query(check_query)
node_count = result.records[0]["count"] if result.records else 0
if node_count == 0:
logger.warning(f"标签 {label} 没有节点包含 embedding 属性,跳过向量索引创建")
return
# 创建向量索引
index_name = f"{label.lower()}_vector"
# 如果强制刷新且索引已存在,先删除它
if force_refresh:
try:
driver.execute_query(f"DROP INDEX {index_name} IF EXISTS")
logger.info(f"强制刷新模式:已删除旧索引 {index_name}")
except Exception as e:
logger.warning(f"删除索引 {index_name} 时出错(可能不存在): {e}")
try:
create_vector_index(
driver,
name=index_name, # 索引的唯一名称
label=label, # 要索引的节点标签
embedding_property="embedding", # 包含嵌入向量值的节点属性键
dimensions=vector_dim, # 向量嵌入维度1024与使用的 bge-m3 嵌入模型一致
similarity_fn="cosine", # 向量相似度函数,可选 "euclidean" 或 "cosine"
)
logger.info(f"成功为标签 {label} 创建向量索引(共 {node_count} 个节点)")
except Exception as e:
# 如果是增量更新模式且索引已存在,这是正常的,只记录警告
if not force_refresh and "already exists" in str(e).lower():
logger.info(f"标签 {label} 的向量索引已存在,跳过创建")
else:
logger.error(f"为标签 {label} 创建向量索引失败: {e}")
raise
# --------- 创建全文索引 ---------
def fulltext_indexing(driver, label, force_refresh=False):
"""
创建全文索引并添加节点属性自动获取所有属性
全文索引 --- id
后续检索分词后根据词反向找到节点
Args:
driver: Neo4j driver
label: 节点标签
force_refresh: 是否强制刷新所有节点的全文索引True=清空后重新生成False=只处理缺失的节点
"""
# 排除内部属性
excluded_props = {"embedding", "fulltext", "elementId", "id"}
# 根据 force_refresh 决定查询条件
if force_refresh:
# 强制刷新:处理所有节点(包括已有 fulltext 的节点)
query = f"""MATCH (n:{label})
WITH n, elementId(n) AS id, properties(n) AS props
RETURN id, props"""
else:
# 增量更新:只处理没有 fulltext 的节点
query = f"""MATCH (n:{label})
WHERE n.fulltext IS NULL
WITH n, elementId(n) AS id, properties(n) AS props
RETURN id, props"""
# 执行查询
records = driver.execute_query(query).records
if not records:
if force_refresh:
logger.info(f"{label} 没有节点需要处理")
else:
logger.info(f"{label} 所有节点皆存在全文索引属性")
return
# 构建文本描述(使用所有属性)
record_list = []
for r in records:
node_id = r["id"]
props = r["props"]
# 过滤掉内部属性
filtered_props = {k: v for k, v in props.items() if k not in excluded_props}
# 使用所有属性构建文本
text = build_text_from_all_properties(filtered_props)
if text:
record_list.append({"id": node_id, "text": text})
if not record_list:
logger.info(f"{label} 没有可索引的文本内容")
return
# 创建全文索引(在写入数据之前创建索引)
try:
create_fulltext_index(
driver,
name=f"{label.lower()}_fulltext", # 索引的唯一名称
label=label, # 要创建索引的节点标签
node_properties=["fulltext"], # 要创建全文索引的节点属性列表
)
except Exception as e:
logger.warning(f"创建全文索引可能已存在(继续处理): {e}")
# 文本分词,作为全文索引属性
logger.info(f"计算 {label} ({len(record_list)}) 的全文索引")
pattern = re.compile(r"[a-zA-Z0-9\u4e00-\u9fa5]+") # 匹配英文字母、数字和中文字符
fulltext_tuple_list = []
for id_, text in [(r["id"], r["text"]) for r in record_list]:
# 使用 jieba 分词
words = jieba.lcut(text)
# 过滤并连接
filtered_words = [
word.strip()
for word in words
if word.strip() and pattern.fullmatch(word.strip())
]
fulltext_value = " ".join(filtered_words)
if fulltext_value:
fulltext_tuple_list.append((id_, fulltext_value))
if not fulltext_tuple_list:
logger.warning(f"{label} 分词后没有有效内容")
return
ids, fulltexts = zip(*fulltext_tuple_list)
ids = list(ids)
fulltexts = list(fulltexts)
# 按 elementId 添加全文索引属性
logger.info(f"写入 {label} ({len(fulltexts)}) 的全文索引")
insert_batch_size = 1000
for i in range(0, len(fulltext_tuple_list), insert_batch_size):
batch_rows = [
{"id": id_, "fulltext": ft}
for id_, ft in zip(
ids[i: i + insert_batch_size],
fulltexts[i: i + insert_batch_size],
)
]
# UNWIND将列表数据展开为多行记录
driver.execute_query(
"UNWIND $rows AS row " # 将传入的 rows 列表展开,每项作为一行数据,命名为 row
"MATCH (n) "
"WHERE elementId(n) = row.id "
"SET n.fulltext = row.fulltext ",
{"rows": batch_rows},
)
def create_all_indexes(neo4j_uri=None, neo4j_username=None, neo4j_password=None, force_refresh=False, driver=None):
"""
创建所有索引的主函数可被外部调用
注意Neo4j 连接配置优先从 .env 文件中读取NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD
如果 .env 文件中没有配置则使用参数中的值如果提供或默认值
Args:
neo4j_uri: Neo4j URI可选优先级低于 .env提供 driver 时忽略
neo4j_username: Neo4j 用户名可选优先级低于 .env提供 driver 时忽略
neo4j_password: Neo4j 密码可选优先级低于 .env提供 driver 时忽略
force_refresh: 是否强制刷新所有索引True=清空重建False=增量更新
driver: 可选的 Neo4j driver 实例提供则复用外部 driver
Returns:
dict: 包含执行结果的字典
"""
use_external_driver = driver is not None
if not use_external_driver:
# 优先从 .env 文件读取配置,如果 .env 中没有则使用参数或默认值
neo4j_uri = os.getenv("NEO4J_URI") or neo4j_uri or "bolt://192.168.0.46:57687"
neo4j_username = os.getenv("NEO4J_USERNAME") or neo4j_username or "neo4j"
neo4j_password = os.getenv("NEO4J_PASSWORD") or neo4j_password or "zdht123@"
logger.info(f"连接到 Neo4j: {neo4j_uri}")
else:
logger.info("使用外部提供的 Neo4j driver")
# 定义内部函数来执行索引创建逻辑(避免代码重复)
def _execute_indexing(driver):
# 1、根据 force_refresh 决定是否清空索引和约束
if force_refresh:
logger.info("强制刷新模式:清空所有约束...")
drop_constraint(driver)
logger.info("强制刷新模式:清空所有索引...")
drop_index_without_constraint(driver)
logger.info("强制刷新模式:清空所有节点的 fulltext 属性...")
driver.execute_query("MATCH (n) WHERE n.fulltext IS NOT NULL REMOVE n.fulltext")
else:
logger.info("增量更新模式:保留现有索引和约束")
# 2、自动获取所有节点标签并创建向量索引
logger.info("=" * 50)
logger.info("开始创建向量索引...")
logger.info("=" * 50)
all_labels = get_all_labels(driver)
logger.info(f"发现 {len(all_labels)} 个节点标签: {all_labels}")
vector_index_errors = []
for label in all_labels:
logger.info(f"处理标签: {label}")
try:
vector_indexing(driver, label, force_refresh=force_refresh)
except Exception as e:
error_msg = f"为标签 {label} 创建向量索引时出错: {e}"
logger.error(error_msg)
vector_index_errors.append(error_msg)
continue
# 3、创建全文索引自动获取所有属性
logger.info("=" * 50)
logger.info("开始创建全文索引...")
logger.info("=" * 50)
fulltext_index_errors = []
for label in all_labels:
logger.info(f"处理标签: {label}")
try:
fulltext_indexing(driver, label, force_refresh=force_refresh)
except Exception as e:
error_msg = f"为标签 {label} 创建全文索引时出错: {e}"
logger.error(error_msg)
fulltext_index_errors.append(error_msg)
continue
logger.info("=" * 50)
logger.info("所有索引创建完成!")
logger.info("=" * 50)
has_errors = len(vector_index_errors) > 0 or len(fulltext_index_errors) > 0
mode_text = "强制刷新" if force_refresh else "增量更新"
return {
"success": not has_errors,
"message": f"{mode_text}模式:索引创建完成" if not has_errors
else f"{mode_text}模式:索引创建完成,但部分标签出现错误",
"mode": "force_refresh" if force_refresh else "incremental",
"labels": all_labels,
"vector_index_errors": vector_index_errors,
"fulltext_index_errors": fulltext_index_errors,
}
try:
if use_external_driver:
# 使用外部 driver直接执行不关闭 driver
return _execute_indexing(driver)
else:
# 使用内部 driver使用上下文管理器自动关闭
with GraphDatabase.driver(neo4j_uri, auth=(neo4j_username, neo4j_password)) as driver:
return _execute_indexing(driver)
except Exception as e:
error_msg = f"创建索引过程中发生错误: {e}"
logger.error(error_msg, exc_info=True)
return {
"success": False,
"message": error_msg,
"mode": "error",
"labels": [],
"vector_index_errors": [],
"fulltext_index_errors": [],
}
def print_neo4j_version(driver) -> None:
"""打印当前 Neo4j 实例的版本和版本类型。"""
with driver.session() as session:
@ -407,12 +795,13 @@ build_hyrid_indexes = build_hybrid_indexes
if __name__ == "__main__":
# 可以通过命令行参数或环境变量控制是否强制刷新
import sys
result = build_hybrid_indexes(
drop_old=True
)
force_refresh = "--force" in sys.argv or os.getenv("FORCE_REFRESH", "false").lower() == "true"
result = create_all_indexes(force_refresh=force_refresh)
if result["success"]:
logger.info("混合索引创建成功")
logger.info(f"索引创建成功完成(模式: {result['mode']}")
else:
logger.error(result["message"])
logger.error(f"索引创建完成但有错误: {result['message']}")

0
reset_textlevel.py Normal file
View File

184
resetlevel.py Normal file
View File

@ -0,0 +1,184 @@
import copy
import json
import re
import sys
from typing import Any
CN_NUM = "\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341\u767e\u5343\u4e07\u3007\u96f6\u4e24"
CHAPTER_PATTERN = re.compile(rf"^\s*\u7b2c\s*([{CN_NUM}\d]+)\s*\u7ae0")
CN_LEVEL1_PATTERN = re.compile(rf"^\s*([{CN_NUM}]+)(?=\s|[\u3001\u3002\uff0e.])")
CASE_LIKE_PATTERN = re.compile(r"^\s*\d+(?:\.|\uff0e)(?!\d|\s|$)")
ARABIC_PATTERN = re.compile(
r"^\s*(\d+(?:\.\d+)*)(?=\s|[\u4e00-\u9fff\u3001\u3002\uff0e]|$|\.(?:\s|$))"
)
def has_valid_text_level(item: dict) -> bool:
return "text_level" in item and item.get("text_level") not in (None, "")
def parse_heading(text: str):
text = text.strip()
if not text:
return None, None
if CASE_LIKE_PATTERN.match(text):
return "case_like", None
if CHAPTER_PATTERN.match(text):
return "level1", None
if CN_LEVEL1_PATTERN.match(text):
return "level1", None
match = ARABIC_PATTERN.match(text)
if match:
return "arabic", match.group(1)
return None, None
def arabic_depth(number: str) -> int:
return number.count(".") + 1
def parent_number(number: str) -> str | None:
parts = number.split(".")
if len(parts) <= 1:
return None
return ".".join(parts[:-1])
def collect_headings(data: list) -> list:
headings = []
for item in data:
if not isinstance(item, dict):
continue
if item.get("type") != "text":
continue
if not has_valid_text_level(item):
continue
kind, number = parse_heading(item.get("text", ""))
if kind in {"level1", "arabic", "case_like"}:
headings.append({"kind": kind, "number": number})
return headings
def is_valid_arabic_hierarchy(numbers: list[str], allow_missing_depth1_parent: bool) -> bool:
seen = set()
has_depth2 = False
has_depth3 = False
for number in numbers:
depth = arabic_depth(number)
if depth >= 2:
has_depth2 = True
if depth >= 3:
has_depth3 = True
parent = parent_number(number)
if parent is not None:
parent_depth = arabic_depth(parent)
parent_is_missing_depth1 = allow_missing_depth1_parent and parent_depth == 1
if parent not in seen and not parent_is_missing_depth1:
return False
seen.add(number)
return has_depth2 and has_depth3
def detect_valid_mode(headings: list):
if not headings:
return None
if any(heading["kind"] == "case_like" for heading in headings):
return None
first = headings[0]
if first["kind"] == "arabic":
numbers = [heading["number"] for heading in headings if heading["kind"] == "arabic"]
if numbers and arabic_depth(numbers[0]) == 1:
if is_valid_arabic_hierarchy(numbers, allow_missing_depth1_parent=False):
return "A"
return None
if first["kind"] == "level1":
numbers = [heading["number"] for heading in headings[1:] if heading["kind"] == "arabic"]
if not numbers:
return None
first_depth = arabic_depth(numbers[0])
if first_depth == 2 and is_valid_arabic_hierarchy(numbers, allow_missing_depth1_parent=True):
return "B"
if first_depth == 1 and is_valid_arabic_hierarchy(numbers, allow_missing_depth1_parent=False):
return "C"
return None
JsonContent = Any
def reset_textlevel(json_content: JsonContent) -> JsonContent:
"""
输入 JSON 内容返回更新 text_level 后的新 JSON 内容
json_content 可以是已经 json.load/json.loads 后的 Python 对象
也可以是 JSON 字符串函数不会修改原始入参
"""
if isinstance(json_content, str):
data = json.loads(json_content)
else:
data = copy.deepcopy(json_content)
if not isinstance(data, list):
return data
headings = collect_headings(data)
mode = detect_valid_mode(headings)
if mode is None:
return data
for item in data:
if not isinstance(item, dict):
continue
if item.get("type") != "text":
continue
if not has_valid_text_level(item):
continue
kind, number = parse_heading(item.get("text", ""))
if kind == "level1":
item["text_level"] = 1
continue
if kind != "arabic":
continue
depth = arabic_depth(number)
item["text_level"] = depth if mode in {"A", "B"} else depth + 1
return data
if __name__ == "__main__":
json_file_path = r'E:\ZKYNLP\Hjunproject\project0506\kgrag\163-06A0014-B01001_发动机-维修手册_content_list.json'
with open(json_file_path, "r", encoding="utf-8") as f:
json_content = json.load(f)
result= reset_textlevel(json_content)
for ins in result[:30]:
print(ins)
# print(json.dumps(result, ensure_ascii=False, indent=2))

View File

@ -1,799 +0,0 @@
{
"163": {
"code": "163",
"name": "JZ贱",
"level": 0,
"children":{
"00": {
"code": "00",
"name": "总体及综合保障",
"level": 1,
"children":{}},
"01": {
"code": "01",
"name": "船体结构",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "船体结构总论",
"level": 2,
"children":{}
},
"10":{
"code": "10",
"name": "主船体壳体结构",
"level": 2,
"children":{}
},
"20":{
"code": "20",
"name": "船体舱壁结构",
"level": 2,
"children":{}
},
"30":{
"code": "30",
"name": "船体甲板及平台结构",
"level": 2,
"children":{}
},
"40":{
"code": "40",
"name": "上层建筑(或甲板室)结构",
"level": 2,
"children":{}
},
"50":{
"code": "50",
"name": "舷台结构",
"level": 2,
"children":{}
},
"60":{
"code": "60",
"name": "专用结构",
"level": 2,
"children":{}
},
"70":{
"code": "70",
"name": "复合材料结构",
"level": 2,
"children":{}
},
"80":{
"code": "80",
"name": "基座结构",
"level": 2,
"children":{}
}
}
},
"02": {
"code": "02",
"name": "推进系统",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "推进系统总论",
"level": 2,
"children":{
"000":{
"code": "000",
"name": "动力系统",
"level": 3,
"children":{}}
}},
"10":{
"code": "10",
"name": "H能源发生系统",
"level": 2,
"children":{}
},
"20":{
"code": "20",
"name": "非H能源发生系统",
"level": 2,
"children":{}
},
"30":{
"code": "30",
"name": "动力机组及推进装置",
"level": 2,
"children":{
}
},
"40":{
"code": "40",
"name": "功率传递系统和推进器",
"level": 2,
"children":{
}
},
"50":{
"code": "50",
"name": "推进保障系统",
"level": 2,
"children":{
"006":{
"code": "006",
"name": "电动高压空气缩机组",
"level": 3,
"children":{}},
"007":{
"code": "007",
"name": "电动中压空气压缩机组",
"level": 3,
"children":{}},
"008":{
"code": "008",
"name": "956柴油机排气消音器",
"level": 3,
"children":{}},
"009":{
"code": "009",
"name": "956柴油机进气消音器",
"level": 3,
"children":{}},
"010":{
"code": "010",
"name": "燃气轮机燃油增压泵组",
"level": 3,
"children":{}}
}
},
"60":{
"code": "60",
"name": "推进监测和控制系统",
"level": 2,
"children":{
}
}
}
},
"03": {
"code": "03",
"name": "电力系统",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "电力系统总论",
"level": 2,
"children":{
"000":{
"code": "000",
"name": "电力系统",
"level": 3,
"children":{}}
}
},
"10":{
"code": "10",
"name": "供电系统",
"level": 2,
"children":{
"003":{
"code": "003",
"name": "发电机",
"level": 3,
"children":{}},
"004":{
"code": "004",
"name": "柴油机",
"level": 3,
"children":{}},
"038":{
"code": "038",
"name": "1250kw柴油发电机组箱装体",
"level": 3,
"children":{}},
"052":{
"code": "052",
"name": "三相无刷同步发电机",
"level": 3,
"children":{}},
"053":{
"code": "053",
"name": "电站分系统",
"level": 3,
"children":{}}
}
},
"20":{
"code": "20",
"name": "配电系统",
"level": 2,
"children":{
"005":{
"code": "005",
"name": "主配电板",
"level": 3,
"children":{}},
"010":{
"code": "010",
"name": "配电中心",
"level": 3,
"children":{}},
"011":{
"code": "011",
"name": "武器配电板",
"level": 3,
"children":{}},
"012":{
"code": "012",
"name": "两舷电源转换控制箱",
"level": 3,
"children":{}},
"015":{
"code": "015",
"name": "事故电力网设备",
"level": 3,
"children":{}},
"022":{
"code": "022",
"name": "JPD1、JPD6型贱用分配电箱",
"level": 3,
"children":{}},
"023":{
"code": "023",
"name": "分配电箱(JDP3)",
"level": 3,
"children":{}},
"025":{
"code": "025",
"name": "轻型变压器",
"level": 3,
"children":{}},
"032":{
"code": "032",
"name": "组合起动器",
"level": 3,
"children":{}},
"033":{
"code": "033",
"name": "两舷电源转换配电装置",
"level": 3,
"children":{}},
"034":{
"code": "034",
"name": "岸电联接箱",
"level": 3,
"children":{}},
"039":{
"code": "039",
"name": "磁力起动器",
"level": 3,
"children":{}},
"047":{
"code": "047",
"name": "磁力起动器(20A)",
"level": 3,
"children":{}},
"054":{
"code": "054",
"name": "配电系统",
"level": 3,
"children":{}},
"055":{
"code": "055",
"name": "岸电箱",
"level": 3,
"children":{}},
"057":{
"code": "057",
"name": "电力接触器箱",
"level": 3,
"children":{}}
}
},
"30":{
"code": "30",
"name": "照明系统",
"level": 2,
"children":{
"030":{
"code": "030",
"name": "灯光管制设备",
"level": 3,
"children":{}},
"031":{
"code": "031",
"name": "低照度照明转换装置",
"level": 3,
"children":{}},
"040":{
"code": "040",
"name": "独立式应急灯集中放电装置",
"level": 3,
"children":{}},
"046":{
"code": "046",
"name": "机库照明开关箱",
"level": 3,
"children":{}},
"049":{
"code": "049",
"name": "彩灯系统",
"level": 3,
"children":{}},
"051":{
"code": "051",
"name": "通道角灯控制装置",
"level": 3,
"children":{}},
"059":{
"code": "059",
"name": "照明系统",
"level": 3,
"children":{}},
"060":{
"code": "060",
"name": "照明变压器",
"level": 3,
"children":{}},
"061":{
"code": "061",
"name": "区域照明接触器箱",
"level": 3,
"children":{}},
"062":{
"code": "062",
"name": "独立式应急灯维护管理装置",
"level": 3,
"children":{}},
"063":{
"code": "063",
"name": "外部照明控制装置",
"level": 3,
"children":{}}
}
},
"40":{
"code": "40",
"name": "电气监测和控制系统",
"level": 2,
"children":{
"002":{
"code": "002",
"name": "电力监控分系统",
"level": 3,
"children":{}},
"017":{
"code": "017",
"name": "电网检测装置",
"level": 3,
"children":{}},
"018":{
"code": "018",
"name": "直流绝缘检测装置",
"level": 3,
"children":{}},
"019":{
"code": "019",
"name": "交流绝缘检测装置",
"level": 3,
"children":{}},
"020":{
"code": "020",
"name": "电网绝缘状态综合显控仪",
"level": 3,
"children":{}},
"045":{
"code": "045",
"name": "电网监测接线箱",
"level": 3,
"children":{}}
}
},
"50":{
"code": "50",
"name": "发电机组保障系统",
"level": 2,
"children":{}
},
"60":{
"code": "60",
"name": "电缆敷设",
"level": 2,
"children":{}
},
"70":{
"code": "70",
"name": "综合电缆系统",
"level": 2,
"children":{
}
},
"80":{
"code": "80",
"name": "电场、磁场防护系统",
"level": 2,
"children":{
"016":{
"code": "016",
"name": "消磁系统",
"level": 3,
"children":{
"0161":{
"code": "0161",
"name": "消磁变流装置",
"level": 4,
"children":{}},
"0162":{
"code": "0162",
"name": "消磁接线箱",
"level": 4,
"children":{}},
"0163":{
"code": "0163",
"name": "消磁配电板",
"level": 4,
"children":{}},
"0164":{
"code": "0164",
"name": "其他",
"level": 4,
"children":{}}
}}
}
},
"90":{
"code": "90",
"name": "专用项目03",
"level": 2,
"children":{
"013":{
"code": "013",
"name": "操舵仪",
"level": 3,
"children":{}},
"014":{
"code": "014",
"name": "舵传令钟",
"level": 3,
"children":{}},
"029":{
"code": "029",
"name": "阴极保护装置",
"level": 3,
"children":{}},
"036":{
"code": "036",
"name": "电工试验板",
"level": 3,
"children":{}}
}
}
}
},
"04": {
"code": "04",
"name": "作战系统",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "作战系统总论",
"level": 2,
"children":{
"000":{
"code": "000",
"name": "作战系统",
"level": 3,
"children":{}}
}
},
"10":{
"code": "10",
"name": "信息感知",
"level": 2,
"children":{
},
"20":{
"code": "20",
"name": "本贱作战指挥控制",
"level": 2,
"children":{
}
},
"30":{
"code": "30",
"name": "编队作战指挥",
"level": 2,
"children":{
"001":{
"code": "001",
"name": "编队作战指挥系统",
"level": 3,
"children":{}},
"002":{
"code": "002",
"name": "HZBJ-2B编队指控设备",
"level": 3,
"children":{}},
"003":{
"code": "003",
"name": "编队模拟训练设备",
"level": 3,
"children":{}},
"004":{
"code": "004",
"name": "编队记录设备",
"level": 3,
"children":{}}
}
},
"40":{
"code": "40",
"name": "武器控制及保障",
"level": 2,
"children":{
}
},
"50":{
"code": "50",
"name": "综合通信",
"level": 2,
"children":{
}
}
}
},
"05": {
"code": "05",
"name": "船舶保障系统",
"level": 1,
"children": {}
},
"06": {
"code": "06",
"name": "航空保障系统",
"level": 1,
"children": {}
},
"07": {
"code": "07",
"name": "特种保障系统",
"level": 1,
"children": {}
},
"08": {
"code": "08",
"name": "船舶装置",
"level": 1,
"children": {}
},
"09": {
"code": "09",
"name": "专用装备/系统",
"level": 1,
"children": {}
},
"10": {
"code": "10",
"name": "训练系统",
"level": 1,
"children": {}
},
"11": {
"code": "11",
"name": "测试、维修试和保障系统",
"level": 1,
"children": {}
}
},
"06": {
"code": "06",
"name": "汽车配件",
"level": 1,
"children":{
"A0":{
"code": "A0",
"name": "汽车主配件",
"level": 2,
"children":{
"B01001":{
"code": "B01001",
"name": "发动机",
"level": 3,
"children":{}},
"B01002":{
"code": "B01002",
"name": "螺旋桨",
"level": 3,
"children":{}},
"B01003":{
"code": "B01003",
"name": "雷达",
"level": 3,
"children":{}},
"B01005":{
"code": "B01005",
"name": "声呐",
"level": 3,
"children":{}},
"014":{
"code": "014",
"name": "螺旋桨",
"level": 3,
"children":{}},
"015":{
"code": "015",
"name": "船舶主发动机",
"level": 3,
"children":{}},
"B01006":{
"code": "B01006",
"name": "马达",
"level": 3,
"children":{}},
"B01007":{
"code": "B01007",
"name": "泵",
"level": 3,
"children":{}},
"B01008":{
"code": "B01008",
"name": "阀门",
"level": 3,
"children":{}},
"B01009":{
"code": "B01009",
"name": "变速箱",
"level": 3,
"children":{}},
"B01010":{
"code": "B01010",
"name": "液压系统",
"level": 3,
"children":{}},
"B01011":{
"code": "B01011",
"name": "发电机",
"level": 3,
"children":{}},
"B01012":{
"code": "B01012",
"name": "空压机",
"level": 3,
"children":{}},
"B01013":{
"code": "B01013",
"name": "冷却系统",
"level": 3,
"children":{}},
"B01014":{
"code": "B01014",
"name": "传动轴",
"level": 3,
"children":{}},
"B01015":{
"code": "B01015",
"name": "制动器",
"level": 3,
"children":{}}
}}
}
}
}
},
"101":{
"code":"101",
"name":"nc贱",
"level":0,
"children":{
"04": {
"code": "04",
"name": "作战系统",
"level": 1,
"children":{
"40":{"code": "40",
"name": "武器控制及保障",
"level": 2,
"children":{
"011":{"code": "011",
"name": "AAAzzz型跟踪雷达",
"level": 3,
"children":{}}}
}
}}
}
},
"122":{
"code": "122",
"name": "122舰",
"level": 0,
"children":{
"06":{
"code": "06",
"name": "设备手册",
"level": 1,
"children":{
"A0014":{
"code": "A0014",
"name": "06A0014",
"level": 2,
"children":{
"B01001":{
"code": "B01001",
"name": "发动机",
"level": 3,
"children":{}},
"B01002":{
"code": "B01002",
"name": "螺旋桨",
"level": 3,
"children":{}},
"B01003":{
"code": "B01003",
"name": "雷达",
"level": 3,
"children":{}},
"B01005":{
"code": "B01005",
"name": "声呐",
"level": 3,
"children":{}},
"B01006":{
"code": "B01006",
"name": "马达",
"level": 3,
"children":{}},
"B01007":{
"code": "B01007",
"name": "泵",
"level": 3,
"children":{}},
"B01008":{
"code": "B01008",
"name": "阀门",
"level": 3,
"children":{}},
"B01009":{
"code": "B01009",
"name": "变速箱",
"level": 3,
"children":{}},
"B01010":{
"code": "B01010",
"name": "液压系统",
"level": 3,
"children":{}},
"B01011":{
"code": "B01011",
"name": "发电机",
"level": 3,
"children":{}},
"B01012":{
"code": "B01012",
"name": "空压机",
"level": 3,
"children":{}},
"B01013":{
"code": "B01013",
"name": "冷却系统",
"level": 3,
"children":{}},
"B01014":{
"code": "B01014",
"name": "传动轴",
"level": 3,
"children":{}},
"B01015":{
"code": "B01015",
"name": "制动器",
"level": 3,
"children":{}}
}
}
}
}
}
}
}

BIN
test.xlsx

Binary file not shown.

View File

@ -1,7 +1,7 @@
{
"163": {
"code": "163",
"name": "JZ贱",
"777": {
"code": "777",
"name": "jz坚",
"level": 0,
"children":{
"00": {
@ -579,20 +579,20 @@
"level": 1,
"children": {}
},
"11": {
"code": "11",
"12": {
"code": "12",
"name": "测试、维修试和保障系统",
"level": 1,
"children": {}
}
},
"06": {
"code": "06",
"11": {
"code": "11",
"name": "汽车配件",
"level": 1,
"children":{
"A0":{
"code": "A0",
"A1":{
"code": "A1",
"name": "汽车主配件",
"level": 2,
"children":{

View File

@ -1,637 +0,0 @@
{
"163": {
"code": "163",
"name": "JZ贱",
"level": 0,
"children":{
"00": {
"code": "00",
"name": "总体及综合保障",
"level": 1,
"children":{}},
"01": {
"code": "01",
"name": "船体结构",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "船体结构总论",
"level": 2,
"children":{}
},
"10":{
"code": "10",
"name": "主船体壳体结构",
"level": 2,
"children":{}
},
"20":{
"code": "20",
"name": "船体舱壁结构",
"level": 2,
"children":{}
},
"30":{
"code": "30",
"name": "船体甲板及平台结构",
"level": 2,
"children":{}
},
"40":{
"code": "40",
"name": "上层建筑(或甲板室)结构",
"level": 2,
"children":{}
},
"50":{
"code": "50",
"name": "舷台结构",
"level": 2,
"children":{}
},
"60":{
"code": "60",
"name": "专用结构",
"level": 2,
"children":{}
},
"70":{
"code": "70",
"name": "复合材料结构",
"level": 2,
"children":{}
},
"80":{
"code": "80",
"name": "基座结构",
"level": 2,
"children":{}
}
}
},
"02": {
"code": "02",
"name": "推进系统",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "推进系统总论",
"level": 2,
"children":{
"000":{
"code": "000",
"name": "动力系统",
"level": 3,
"children":{}}
}},
"10":{
"code": "10",
"name": "H能源发生系统",
"level": 2,
"children":{}
},
"20":{
"code": "20",
"name": "非H能源发生系统",
"level": 2,
"children":{}
},
"30":{
"code": "30",
"name": "动力机组及推进装置",
"level": 2,
"children":{
}
},
"40":{
"code": "40",
"name": "功率传递系统和推进器",
"level": 2,
"children":{
}
},
"50":{
"code": "50",
"name": "推进保障系统",
"level": 2,
"children":{
"006":{
"code": "006",
"name": "电动高压空气缩机组",
"level": 3,
"children":{}},
"007":{
"code": "007",
"name": "电动中压空气压缩机组",
"level": 3,
"children":{}},
"008":{
"code": "008",
"name": "956柴油机排气消音器",
"level": 3,
"children":{}},
"009":{
"code": "009",
"name": "956柴油机进气消音器",
"level": 3,
"children":{}},
"010":{
"code": "010",
"name": "燃气轮机燃油增压泵组",
"level": 3,
"children":{}}
}
},
"60":{
"code": "60",
"name": "推进监测和控制系统",
"level": 2,
"children":{
}
}
}
},
"03": {
"code": "03",
"name": "电力系统",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "电力系统总论",
"level": 2,
"children":{
"000":{
"code": "000",
"name": "电力系统",
"level": 3,
"children":{}}
}
},
"10":{
"code": "10",
"name": "供电系统",
"level": 2,
"children":{
"003":{
"code": "003",
"name": "发电机",
"level": 3,
"children":{}},
"004":{
"code": "004",
"name": "柴油机",
"level": 3,
"children":{}},
"038":{
"code": "038",
"name": "1250kw柴油发电机组箱装体",
"level": 3,
"children":{}},
"052":{
"code": "052",
"name": "三相无刷同步发电机",
"level": 3,
"children":{}},
"053":{
"code": "053",
"name": "电站分系统",
"level": 3,
"children":{}}
}
},
"20":{
"code": "20",
"name": "配电系统",
"level": 2,
"children":{
"005":{
"code": "005",
"name": "主配电板",
"level": 3,
"children":{}},
"010":{
"code": "010",
"name": "配电中心",
"level": 3,
"children":{}},
"011":{
"code": "011",
"name": "武器配电板",
"level": 3,
"children":{}},
"012":{
"code": "012",
"name": "两舷电源转换控制箱",
"level": 3,
"children":{}},
"015":{
"code": "015",
"name": "事故电力网设备",
"level": 3,
"children":{}},
"022":{
"code": "022",
"name": "JPD1、JPD6型贱用分配电箱",
"level": 3,
"children":{}},
"023":{
"code": "023",
"name": "分配电箱(JDP3)",
"level": 3,
"children":{}},
"025":{
"code": "025",
"name": "轻型变压器",
"level": 3,
"children":{}},
"032":{
"code": "032",
"name": "组合起动器",
"level": 3,
"children":{}},
"033":{
"code": "033",
"name": "两舷电源转换配电装置",
"level": 3,
"children":{}},
"034":{
"code": "034",
"name": "岸电联接箱",
"level": 3,
"children":{}},
"039":{
"code": "039",
"name": "磁力起动器",
"level": 3,
"children":{}},
"047":{
"code": "047",
"name": "磁力起动器(20A)",
"level": 3,
"children":{}},
"054":{
"code": "054",
"name": "配电系统",
"level": 3,
"children":{}},
"055":{
"code": "055",
"name": "岸电箱",
"level": 3,
"children":{}},
"057":{
"code": "057",
"name": "电力接触器箱",
"level": 3,
"children":{}}
}
},
"30":{
"code": "30",
"name": "照明系统",
"level": 2,
"children":{
"030":{
"code": "030",
"name": "灯光管制设备",
"level": 3,
"children":{}},
"031":{
"code": "031",
"name": "低照度照明转换装置",
"level": 3,
"children":{}},
"040":{
"code": "040",
"name": "独立式应急灯集中放电装置",
"level": 3,
"children":{}},
"046":{
"code": "046",
"name": "机库照明开关箱",
"level": 3,
"children":{}},
"049":{
"code": "049",
"name": "彩灯系统",
"level": 3,
"children":{}},
"051":{
"code": "051",
"name": "通道角灯控制装置",
"level": 3,
"children":{}},
"059":{
"code": "059",
"name": "照明系统",
"level": 3,
"children":{}},
"060":{
"code": "060",
"name": "照明变压器",
"level": 3,
"children":{}},
"061":{
"code": "061",
"name": "区域照明接触器箱",
"level": 3,
"children":{}},
"062":{
"code": "062",
"name": "独立式应急灯维护管理装置",
"level": 3,
"children":{}},
"063":{
"code": "063",
"name": "外部照明控制装置",
"level": 3,
"children":{}}
}
},
"40":{
"code": "40",
"name": "电气监测和控制系统",
"level": 2,
"children":{
"002":{
"code": "002",
"name": "电力监控分系统",
"level": 3,
"children":{}},
"017":{
"code": "017",
"name": "电网检测装置",
"level": 3,
"children":{}},
"018":{
"code": "018",
"name": "直流绝缘检测装置",
"level": 3,
"children":{}},
"019":{
"code": "019",
"name": "交流绝缘检测装置",
"level": 3,
"children":{}},
"020":{
"code": "020",
"name": "电网绝缘状态综合显控仪",
"level": 3,
"children":{}},
"045":{
"code": "045",
"name": "电网监测接线箱",
"level": 3,
"children":{}}
}
},
"50":{
"code": "50",
"name": "发电机组保障系统",
"level": 2,
"children":{}
},
"60":{
"code": "60",
"name": "电缆敷设",
"level": 2,
"children":{}
},
"70":{
"code": "70",
"name": "综合电缆系统",
"level": 2,
"children":{
}
},
"80":{
"code": "80",
"name": "电场、磁场防护系统",
"level": 2,
"children":{
"016":{
"code": "016",
"name": "消磁系统",
"level": 3,
"children":{
"0161":{
"code": "0161",
"name": "消磁变流装置",
"level": 4,
"children":{}},
"0162":{
"code": "0162",
"name": "消磁接线箱",
"level": 4,
"children":{}},
"0163":{
"code": "0163",
"name": "消磁配电板",
"level": 4,
"children":{}},
"0164":{
"code": "0164",
"name": "其他",
"level": 4,
"children":{}}
}}
}
},
"90":{
"code": "90",
"name": "专用项目03",
"level": 2,
"children":{
"013":{
"code": "013",
"name": "操舵仪",
"level": 3,
"children":{}},
"014":{
"code": "014",
"name": "舵传令钟",
"level": 3,
"children":{}},
"029":{
"code": "029",
"name": "阴极保护装置",
"level": 3,
"children":{}},
"036":{
"code": "036",
"name": "电工试验板",
"level": 3,
"children":{}}
}
}
}
},
"04": {
"code": "04",
"name": "作战系统",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "作战系统总论",
"level": 2,
"children":{
"000":{
"code": "000",
"name": "作战系统",
"level": 3,
"children":{}}
}
},
"10":{
"code": "10",
"name": "信息感知",
"level": 2,
"children":{
},
"20":{
"code": "20",
"name": "本贱作战指挥控制",
"level": 2,
"children":{
}
},
"30":{
"code": "30",
"name": "编队作战指挥",
"level": 2,
"children":{
"001":{
"code": "001",
"name": "编队作战指挥系统",
"level": 3,
"children":{}},
"002":{
"code": "002",
"name": "HZBJ-2B编队指控设备",
"level": 3,
"children":{}},
"003":{
"code": "003",
"name": "编队模拟训练设备",
"level": 3,
"children":{}},
"004":{
"code": "004",
"name": "编队记录设备",
"level": 3,
"children":{}}
}
},
"40":{
"code": "40",
"name": "武器控制及保障",
"level": 2,
"children":{
}
},
"50":{
"code": "50",
"name": "综合通信",
"level": 2,
"children":{
}
}
}
},
"05": {
"code": "05",
"name": "船舶保障系统",
"level": 1,
"children": {}
},
"06": {
"code": "06",
"name": "航空保障系统",
"level": 1,
"children": {}
},
"07": {
"code": "07",
"name": "特种保障系统",
"level": 1,
"children": {}
},
"08": {
"code": "08",
"name": "船舶装置",
"level": 1,
"children": {}
},
"09": {
"code": "09",
"name": "专用装备/系统",
"level": 1,
"children": {}
},
"10": {
"code": "10",
"name": "训练系统",
"level": 1,
"children": {}
},
"11": {
"code": "11",
"name": "测试、维修试和保障系统",
"level": 1,
"children": {}
}
},
"06": {
"code": "06",
"name": "汽车配件",
"level": 1,
"children":{
"A0":{
"code": "A0",
"name": "汽车主配件",
"level": 2,
"children":{
"014":{
"code": "014",
"name": "螺旋桨",
"level": 3,
"children":{}},
"015":{
"code": "015",
"name": "船舶主发动机",
"level": 3,
"children":{}}
}}
}
}
}
},
"101":{
"code":"101",
"name":"nc贱",
"level":0,
"children":{
"04": {
"code": "04",
"name": "作战系统",
"level": 1,
"children":{
"40":{"code": "40",
"name": "武器控制及保障",
"level": 2,
"children":{
"011":{"code": "011",
"name": "AAAzzz型跟踪雷达",
"level": 3,
"children":{}}}
}
}}
}
}
}

View File

@ -1,613 +0,0 @@
{
"163": {
"code": "163",
"name": "JZ贱",
"level": 0,
"children":{
"00": {
"code": "00",
"name": "总体及综合保障",
"level": 1,
"children":{}},
"01": {
"code": "01",
"name": "船体结构",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "船体结构总论",
"level": 2,
"children":{}
},
"10":{
"code": "10",
"name": "主船体壳体结构",
"level": 2,
"children":{}
},
"20":{
"code": "20",
"name": "船体舱壁结构",
"level": 2,
"children":{}
},
"30":{
"code": "30",
"name": "船体甲板及平台结构",
"level": 2,
"children":{}
},
"40":{
"code": "40",
"name": "上层建筑(或甲板室)结构",
"level": 2,
"children":{}
},
"50":{
"code": "50",
"name": "舷台结构",
"level": 2,
"children":{}
},
"60":{
"code": "60",
"name": "专用结构",
"level": 2,
"children":{}
},
"70":{
"code": "70",
"name": "复合材料结构",
"level": 2,
"children":{}
},
"80":{
"code": "80",
"name": "基座结构",
"level": 2,
"children":{}
}
}
},
"02": {
"code": "02",
"name": "推进系统",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "推进系统总论",
"level": 2,
"children":{
"000":{
"code": "000",
"name": "动力系统",
"level": 3,
"children":{}}
}},
"10":{
"code": "10",
"name": "H能源发生系统",
"level": 2,
"children":{}
},
"20":{
"code": "20",
"name": "非H能源发生系统",
"level": 2,
"children":{}
},
"30":{
"code": "30",
"name": "动力机组及推进装置",
"level": 2,
"children":{
}
},
"40":{
"code": "40",
"name": "功率传递系统和推进器",
"level": 2,
"children":{
}
},
"50":{
"code": "50",
"name": "推进保障系统",
"level": 2,
"children":{
"006":{
"code": "006",
"name": "电动高压空气缩机组",
"level": 3,
"children":{}},
"007":{
"code": "007",
"name": "电动中压空气压缩机组",
"level": 3,
"children":{}},
"008":{
"code": "008",
"name": "956柴油机排气消音器",
"level": 3,
"children":{}},
"009":{
"code": "009",
"name": "956柴油机进气消音器",
"level": 3,
"children":{}},
"010":{
"code": "010",
"name": "燃气轮机燃油增压泵组",
"level": 3,
"children":{}}
}
},
"60":{
"code": "60",
"name": "推进监测和控制系统",
"level": 2,
"children":{
}
}
}
},
"03": {
"code": "03",
"name": "电力系统",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "电力系统总论",
"level": 2,
"children":{
"000":{
"code": "000",
"name": "电力系统",
"level": 3,
"children":{}}
}
},
"10":{
"code": "10",
"name": "供电系统",
"level": 2,
"children":{
"003":{
"code": "003",
"name": "发电机",
"level": 3,
"children":{}},
"004":{
"code": "004",
"name": "柴油机",
"level": 3,
"children":{}},
"038":{
"code": "038",
"name": "1250kw柴油发电机组箱装体",
"level": 3,
"children":{}},
"052":{
"code": "052",
"name": "三相无刷同步发电机",
"level": 3,
"children":{}},
"053":{
"code": "053",
"name": "电站分系统",
"level": 3,
"children":{}}
}
},
"20":{
"code": "20",
"name": "配电系统",
"level": 2,
"children":{
"005":{
"code": "005",
"name": "主配电板",
"level": 3,
"children":{}},
"010":{
"code": "010",
"name": "配电中心",
"level": 3,
"children":{}},
"011":{
"code": "011",
"name": "武器配电板",
"level": 3,
"children":{}},
"012":{
"code": "012",
"name": "两舷电源转换控制箱",
"level": 3,
"children":{}},
"015":{
"code": "015",
"name": "事故电力网设备",
"level": 3,
"children":{}},
"022":{
"code": "022",
"name": "JPD1、JPD6型贱用分配电箱",
"level": 3,
"children":{}},
"023":{
"code": "023",
"name": "分配电箱(JDP3)",
"level": 3,
"children":{}},
"025":{
"code": "025",
"name": "轻型变压器",
"level": 3,
"children":{}},
"032":{
"code": "032",
"name": "组合起动器",
"level": 3,
"children":{}},
"033":{
"code": "033",
"name": "两舷电源转换配电装置",
"level": 3,
"children":{}},
"034":{
"code": "034",
"name": "岸电联接箱",
"level": 3,
"children":{}},
"039":{
"code": "039",
"name": "磁力起动器",
"level": 3,
"children":{}},
"047":{
"code": "047",
"name": "磁力起动器(20A)",
"level": 3,
"children":{}},
"054":{
"code": "054",
"name": "配电系统",
"level": 3,
"children":{}},
"055":{
"code": "055",
"name": "岸电箱",
"level": 3,
"children":{}},
"057":{
"code": "057",
"name": "电力接触器箱",
"level": 3,
"children":{}}
}
},
"30":{
"code": "30",
"name": "照明系统",
"level": 2,
"children":{
"030":{
"code": "030",
"name": "灯光管制设备",
"level": 3,
"children":{}},
"031":{
"code": "031",
"name": "低照度照明转换装置",
"level": 3,
"children":{}},
"040":{
"code": "040",
"name": "独立式应急灯集中放电装置",
"level": 3,
"children":{}},
"046":{
"code": "046",
"name": "机库照明开关箱",
"level": 3,
"children":{}},
"049":{
"code": "049",
"name": "彩灯系统",
"level": 3,
"children":{}},
"051":{
"code": "051",
"name": "通道角灯控制装置",
"level": 3,
"children":{}},
"059":{
"code": "059",
"name": "照明系统",
"level": 3,
"children":{}},
"060":{
"code": "060",
"name": "照明变压器",
"level": 3,
"children":{}},
"061":{
"code": "061",
"name": "区域照明接触器箱",
"level": 3,
"children":{}},
"062":{
"code": "062",
"name": "独立式应急灯维护管理装置",
"level": 3,
"children":{}},
"063":{
"code": "063",
"name": "外部照明控制装置",
"level": 3,
"children":{}}
}
},
"40":{
"code": "40",
"name": "电气监测和控制系统",
"level": 2,
"children":{
"002":{
"code": "002",
"name": "电力监控分系统",
"level": 3,
"children":{}},
"017":{
"code": "017",
"name": "电网检测装置",
"level": 3,
"children":{}},
"018":{
"code": "018",
"name": "直流绝缘检测装置",
"level": 3,
"children":{}},
"019":{
"code": "019",
"name": "交流绝缘检测装置",
"level": 3,
"children":{}},
"020":{
"code": "020",
"name": "电网绝缘状态综合显控仪",
"level": 3,
"children":{}},
"045":{
"code": "045",
"name": "电网监测接线箱",
"level": 3,
"children":{}}
}
},
"50":{
"code": "50",
"name": "发电机组保障系统",
"level": 2,
"children":{}
},
"60":{
"code": "60",
"name": "电缆敷设",
"level": 2,
"children":{}
},
"70":{
"code": "70",
"name": "综合电缆系统",
"level": 2,
"children":{
}
},
"80":{
"code": "80",
"name": "电场、磁场防护系统",
"level": 2,
"children":{
"016":{
"code": "016",
"name": "消磁系统",
"level": 3,
"children":{
"0161":{
"code": "0161",
"name": "消磁变流装置",
"level": 4,
"children":{}},
"0162":{
"code": "0162",
"name": "消磁接线箱",
"level": 4,
"children":{}},
"0163":{
"code": "0163",
"name": "消磁配电板",
"level": 4,
"children":{}},
"0164":{
"code": "0164",
"name": "其他",
"level": 4,
"children":{}}
}}
}
},
"90":{
"code": "90",
"name": "专用项目03",
"level": 2,
"children":{
"013":{
"code": "013",
"name": "操舵仪",
"level": 3,
"children":{}},
"014":{
"code": "014",
"name": "舵传令钟",
"level": 3,
"children":{}},
"029":{
"code": "029",
"name": "阴极保护装置",
"level": 3,
"children":{}},
"036":{
"code": "036",
"name": "电工试验板",
"level": 3,
"children":{}}
}
}
}
},
"04": {
"code": "04",
"name": "作战系统",
"level": 1,
"children": {
"00":{
"code": "00",
"name": "作战系统总论",
"level": 2,
"children":{
"000":{
"code": "000",
"name": "作战系统",
"level": 3,
"children":{}}
}
},
"10":{
"code": "10",
"name": "信息感知",
"level": 2,
"children":{
},
"20":{
"code": "20",
"name": "本贱作战指挥控制",
"level": 2,
"children":{
}
},
"30":{
"code": "30",
"name": "编队作战指挥",
"level": 2,
"children":{
"001":{
"code": "001",
"name": "编队作战指挥系统",
"level": 3,
"children":{}},
"002":{
"code": "002",
"name": "HZBJ-2B编队指控设备",
"level": 3,
"children":{}},
"003":{
"code": "003",
"name": "编队模拟训练设备",
"level": 3,
"children":{}},
"004":{
"code": "004",
"name": "编队记录设备",
"level": 3,
"children":{}}
}
},
"40":{
"code": "40",
"name": "武器控制及保障",
"level": 2,
"children":{
}
},
"50":{
"code": "50",
"name": "综合通信",
"level": 2,
"children":{
}
}
}
},
"05": {
"code": "05",
"name": "船舶保障系统",
"level": 1,
"children": {}
},
"06": {
"code": "06",
"name": "航空保障系统",
"level": 1,
"children": {}
},
"07": {
"code": "07",
"name": "特种保障系统",
"level": 1,
"children": {}
},
"08": {
"code": "08",
"name": "船舶装置",
"level": 1,
"children": {}
},
"09": {
"code": "09",
"name": "专用装备/系统",
"level": 1,
"children": {}
},
"10": {
"code": "10",
"name": "训练系统",
"level": 1,
"children": {}
},
"11": {
"code": "11",
"name": "测试、维修试和保障系统",
"level": 1,
"children": {}
}
}
}
},
"101":{
"code":"101",
"name":"nc贱",
"level":0,
"children":{
"04": {
"code": "04",
"name": "作战系统",
"level": 1,
"children":{
"40":{"code": "40",
"name": "武器控制及保障",
"level": 2,
"children":{
"011":{"code": "011",
"name": "AAAzzz型跟踪雷达",
"level": 3,
"children":{}}}
}
}}
}
}
}

File diff suppressed because one or more lines are too long