瀏覽代碼

Merge branch 'dev' into dev_sgsc_xth

xgo 3 周之前
父節點
當前提交
faa913c762
共有 63 個文件被更改,包括 7117 次插入4062 次删除
  1. 11 1
      Dockerfile
  2. 5 7
      core/base/__init__.py
  3. 101 0
      core/construction_review/component/ai_review_engine.py
  4. 94 0
      core/construction_review/component/desensitize/README.md
  5. 24 0
      core/construction_review/component/desensitize/__init__.py
  6. 292 0
      core/construction_review/component/desensitize/dict_manager.py
  7. 293 0
      core/construction_review/component/desensitize/engine.py
  8. 267 0
      core/construction_review/component/desensitize/model_client.py
  9. 39 0
      core/construction_review/component/desensitize/processors/base_processor.py
  10. 155 0
      core/construction_review/component/desensitize/processors/biz_processor.py
  11. 138 0
      core/construction_review/component/desensitize/processors/financial_processor.py
  12. 169 0
      core/construction_review/component/desensitize/processors/geo_processor.py
  13. 117 0
      core/construction_review/component/desensitize/processors/pii_processor.py
  14. 236 0
      core/construction_review/component/desensitize/remapper.py
  15. 268 0
      core/construction_review/component/desensitize/validator.py
  16. 124 69
      core/construction_review/component/doc_worker/classification/chunk_classifier.py
  17. 1 0
      core/construction_review/component/doc_worker/classification/hierarchy_classifier.py
  18. 172 173
      core/construction_review/component/doc_worker/config/StandardCategoryTable.csv
  19. 0 17
      core/construction_review/component/doc_worker/docx_worker/__init__.py
  20. 0 118
      core/construction_review/component/doc_worker/docx_worker/cli.py
  21. 0 110
      core/construction_review/component/doc_worker/docx_worker/full_text_extractor.py
  22. 0 106
      core/construction_review/component/doc_worker/docx_worker/pipeline.py
  23. 0 327
      core/construction_review/component/doc_worker/docx_worker/text_splitter.py
  24. 0 123
      core/construction_review/component/doc_worker/docx_worker/toc_extractor.py
  25. 0 1
      core/construction_review/component/doc_worker/docx_worker/命令
  26. 1 1
      core/construction_review/component/doc_worker/utils/text_split_support.py
  27. 0 10
      core/construction_review/component/doc_worker/命令
  28. 56 78
      core/construction_review/component/document_processor.py
  29. 72 32
      core/construction_review/component/reviewers/completeness_reviewer.py
  30. 0 537
      core/construction_review/component/reviewers/reference_basis_reviewer.py.bak
  31. 34 2
      core/construction_review/component/reviewers/timeliness_basis_reviewer.py
  32. 487 0
      core/construction_review/component/reviewers/timeliness_content_reviewer.py
  33. 22 1
      core/construction_review/component/reviewers/utils/inter_tool.py
  34. 0 2330
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2.py
  35. 66 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/__init__.py
  36. 227 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/category_loaders.py
  37. 207 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/chunks_converter.py
  38. 155 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/config.py
  39. 791 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/content_classifier.py
  40. 157 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/embedding_client.py
  41. 146 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/json_utils.py
  42. 352 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/main_classifier.py
  43. 71 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/models.py
  44. 360 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/prompt.py
  45. 132 0
      core/construction_review/component/reviewers/utils/llm_content_classifier_v2/text_split_utils.py
  46. 1 0
      core/construction_review/component/reviewers/utils/reference_matcher.py
  47. 1 0
      core/construction_review/workflows/ai_review_workflow.py
  48. 25 0
      core/construction_review/workflows/core_functions/ai_review_core_fun.py
  49. 1 1
      foundation/ai/agent/generate/model_generate.py
  50. 16 0
      foundation/infrastructure/config/config.py
  51. 145 0
      problem.json
  52. 二進制
      requirements.txt
  53. 3 0
      server/app.py
  54. 141 0
      test_content_timeliness.py
  55. 0 1
      tests/test_pr
  56. 145 0
      utils_test/Completeness_Test/2026年3月23日-bug/fc38b3526e408a787d0fdc75e024eb3d-1774245354.json
  57. 267 0
      utils_test/Redis/redis_sentinel_test_2.py
  58. 358 0
      views/construction_review/desensitize_api.py
  59. 171 7
      views/construction_review/file_upload.py
  60. 1 6
      views/construction_review/review_results.py
  61. 0 1
      views/construction_review/task_control.py
  62. 0 1
      views/construction_write/content_completion.py
  63. 0 2
      views/construction_write/outline_views.py

+ 11 - 1
Dockerfile

@@ -1,6 +1,10 @@
 FROM python:3.12-slim
 
-# 安装 OpenCV 系统依赖(更完整的列表)
+# 替换为阿里云 apt 源(Debian 12 使用 DEB822 格式)
+RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources && \
+    sed -i 's|security.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
+
+# 安装 OpenCV 系统依赖及 LibreOffice(docx/doc 转 PDF)
 RUN apt-get update && apt-get install -y \
     # OpenCV 核心依赖
     libgl1 \
@@ -26,6 +30,12 @@ RUN apt-get update && apt-get install -y \
     # 其他可能需要的库
     libfontconfig1 \
     libfreetype6 \
+    # LibreOffice(用于 docx/doc 转 PDF)
+    libreoffice-writer \
+    libreoffice-core \
+    # 中文字体(PDF 转换中文支持)
+    fonts-wqy-zenhei \
+    --no-install-recommends \
     && rm -rf /var/lib/apt/lists/*
 
 ENV DEBIAN_FRONTEND=noninteractive \

+ 5 - 7
core/base/__init__.py

@@ -1,27 +1,27 @@
 """
 文档分类切分库
-支持PDF和Word文档的目录提取、智能分类和文本切分
+支持PDF文档的目录提取、智能分类和文本切分
 
 主要功能:
-1. 提取PDF/Word文档的目录结构
+1. 提取PDF文档的目录结构
 2. 识别和校验目录的层级关系
 3. 基于二级目录关键词匹配对一级目录进行智能分类
 4. 按目录层级和字符数智能切分文本
 5. 保存分类结果到多种格式
 
 使用示例(当前推荐直接使用业务层封装的 DocumentProcessor,而不是底层分类器类)。
+
+注意: DOCX/DOC 文件应在上传层转换为 PDF,本模块不再直接处理 DOCX
 """
 
-__version__ = "2.0.0"
+__version__ = "2.1.0"
 __author__ = "Your Name"
 
 
 from core.construction_review.component.doc_worker.interfaces import TOCExtractor, TextSplitter
 from core.construction_review.component.doc_worker.classification.hierarchy_classifier import HierarchyClassifier
 from core.construction_review.component.doc_worker.pdf_worker.toc_extractor import PdfTOCExtractor
-from core.construction_review.component.doc_worker.docx_worker.toc_extractor import DocxTOCExtractor
 from core.construction_review.component.doc_worker.pdf_worker.text_splitter import PdfTextSplitter
-from core.construction_review.component.doc_worker.docx_worker.text_splitter import DocxTextSplitter
 
 
 __all__ = [
@@ -29,8 +29,6 @@ __all__ = [
     'TextSplitter',
     'HierarchyClassifier',
     'PdfTOCExtractor',
-    'DocxTOCExtractor',
     'PdfTextSplitter',
-    'DocxTextSplitter',
 ]
 

+ 101 - 0
core/construction_review/component/ai_review_engine.py

@@ -1129,6 +1129,107 @@ class AIReviewEngine(BaseReviewer):
                 }
             }
         
+    async def timeliness_content_reviewer(self, review_data: Dict[str, Any], trace_id: str,
+                                state: dict = None, stage_name: str = None) -> Dict[str, Any]:
+        """
+        执行三级分类内容时效性审查:检查tertiary_classification_details中引用的规范是否过时
+
+        Args:
+            review_data: 待审查数据,包含tertiary_classification_details
+            trace_id: 追踪ID
+            state: 状态字典
+            stage_name: 阶段名称
+
+        Returns:
+            审查结果字典,包含内容时效性审查结果
+        """
+        start_time = time.time()
+        try:
+            logger.info(f"开始三级分类内容时效性审查,trace_id: {trace_id}")
+
+            # 提取三级分类详情
+            tertiary_details = review_data.get('tertiary_classification_details', [])
+            max_concurrent = review_data.get('max_concurrent', 4)
+
+            if not tertiary_details:
+                logger.warning("三级分类详情为空,将跳过内容时效性审查")
+                return {
+                    "timeliness_content_review_results": {
+                        "review_results": [],
+                        "total_items": 0,
+                        "issue_items": 0,
+                        "execution_time": time.time() - start_time,
+                        "error_message": None,
+                        "message": "未找到三级分类详情,跳过内容时效性审查"
+                    }
+                }
+
+            logger.info(f"提取到 {len(tertiary_details)} 个三级分类详情")
+
+            # 调用内容时效性审查
+            try:
+                # 使用信号量控制并发
+                async with self.semaphore:
+                    # 从state中获取progress_manager和callback_task_id
+                    progress_manager = state.get('progress_manager') if state else None
+                    callback_task_id = state.get('callback_task_id') if state else None
+
+                    # 调用内容时效性审查器
+                    from core.construction_review.component.reviewers.timeliness_content_reviewer import ContentTimelinessReviewer
+                    async with ContentTimelinessReviewer(max_concurrent=max_concurrent) as reviewer:
+                        timeliness_content_results = await reviewer.review_tertiary_content(
+                            tertiary_details=tertiary_details,
+                            collection_name="first_bfp_collection_status",
+                            progress_manager=progress_manager,
+                            callback_task_id=callback_task_id
+                        )
+
+                    logger.info(f"内容时效性审查完成,发现问题数量: {len(timeliness_content_results)}")
+
+                    # 统计审查结果
+                    total_items = len(timeliness_content_results)
+                    issue_items = sum(1 for item in timeliness_content_results if item.get('exist_issue', False))
+
+                    logger.info(f"审查统计 - 总规范引用: {total_items}, 问题项: {issue_items}")
+
+            except Exception as e:
+                logger.error(f"内容时效性审查失败: {str(e)}")
+                return {
+                    "timeliness_content_review_results": {
+                        "review_results": [],
+                        "total_items": 0,
+                        "issue_items": 0,
+                        "execution_time": time.time() - start_time,
+                        "error_message": f"内容时效性审查失败: {str(e)}"
+                    }
+                }
+
+            # 返回完整结果
+            return {
+                "timeliness_content_review_results": {
+                    "review_results": timeliness_content_results,
+                    "total_items": total_items,
+                    "issue_items": issue_items,
+                    "execution_time": time.time() - start_time,
+                    "error_message": None
+                }
+            }
+
+        except Exception as e:
+            execution_time = time.time() - start_time
+            error_msg = f"内容时效性审查失败: {str(e)}"
+            logger.error(error_msg, exc_info=True)
+
+            return {
+                "timeliness_content_review_results": {
+                    "review_results": [],
+                    "total_items": 0,
+                    "issue_items": 0,
+                    "execution_time": execution_time,
+                    "error_message": error_msg
+                }
+            }
+
     async def timeliness_basis_reviewer(self, review_data: Dict[str, Any], trace_id: str,
                                 state: dict = None, stage_name: str = None) -> Dict[str, Any]:
         """

+ 94 - 0
core/construction_review/component/desensitize/README.md

@@ -0,0 +1,94 @@
+# 施工方案数据脱敏模块
+
+基于 `wlast.md` (v2.3) 文档实现的数据脱敏系统。
+
+## 目录结构
+
+```
+desensitize/
+├── __init__.py              # 模块导出
+├── engine.py                # 脱敏引擎核心
+├── validator.py             # 黑白名单校验器
+├── dict_manager.py          # 脱敏字典管理器
+├── model_client.py          # 本地大模型客户端
+├── remapper.py              # 审查结果逆向映射器
+├── processors/              # 四维度处理器
+│   ├── base_processor.py    # 处理器基类
+│   ├── pii_processor.py     # PII脱敏(姓名/手机/身份证)
+│   ├── geo_processor.py     # 地理坐标脱敏(桩号/地名/高程)
+│   ├── biz_processor.py     # 商业标识脱敏(企业名/项目名)
+│   └── financial_processor.py # 财务数据脱敏(金额/单价)
+└── README.md                # 本文件
+```
+
+## 四维度脱敏规则
+
+| 维度 | 脱敏对象 | 脱敏策略 | 保留逻辑 |
+|------|---------|---------|---------|
+| PII | 姓名、电话、身份证、证书编号 | 占位符/掩码替换 | 角色关系与排班逻辑 |
+| 地理坐标 | 桩号、地名、高程、桥隧名称 | 相对化处理/序列化 | 相对长度与工程实体序列 |
+| 商业标识 | 项目名、企业名、合同编号 | 泛化替换 | 组织角色架构 |
+| 财务数据 | 金额、单价、工程量 | 全量掩码 | 表格科目结构 |
+
+## 两阶段部署策略
+
+### 阶段一(测试)
+- 配置:`DEPLOY_PHASE = test`
+- 使用外部大模型 API 测试验证
+- 仅用于测试数据集,严禁生产文档
+
+### 阶段二(生产)
+- 配置:`DEPLOY_PHASE = local`
+- 方案A:`DESENSITIZE_SCHEME = scheme_a`(规则引擎主导 + 轻量本地模型兜底)
+- 方案B:`DESENSITIZE_SCHEME = scheme_b`(本地大模型全量语义驱动)
+
+## API 接口
+
+| 接口 | 路径 | 功能 |
+|------|------|------|
+| 文档脱敏 | `POST /desensitize/document` | 四维度脱敏处理 |
+| 脱敏校验 | `POST /desensitize/validate` | 黑白名单校验 |
+| 结果还原 | `POST /desensitize/remap` | 审查结果逆向映射 |
+| 字典查询 | `GET /desensitize/dict/{task_id}` | 查询字典元信息 |
+| 文本脱敏 | `POST /desensitize/text` | 简化版文本脱敏 |
+
+## 配置说明
+
+在 `config/config.ini` 中添加以下配置:
+
+```ini
+[desensitize]
+DEPLOY_PHASE = test              # 部署阶段: test/local
+DESENSITIZE_SCHEME = scheme_a    # 本地方案: scheme_a/scheme_b
+DEFAULT_LEVEL = standard         # 默认脱敏级别
+DICT_STORAGE_PATH = ./data/desensitize_dicts
+ENABLE_VALIDATION = true
+VALIDATION_FAILURE_ACTION = block
+```
+
+## 安全约束
+
+1. **外部调用前置脱敏原则**:脱敏是调用任何外部大模型 API 的强制前提
+2. **本地化优先原则**:所有脱敏动作在企业内部本地服务器完成
+3. **可逆映射原则**:本地保留脱敏字典,支持结果还原
+4. **纵深防御原则**:规则引擎 + 本地 AI 模型 + 黑白名单校验三层防护
+
+## 示例
+
+**原始文本**:
+```
+映雪特大桥(D1K86+279.91~D1K87+279.91)跨越云雾河,全长1000m。
+项目经理张三(电话:13812345678)负责现场安全管理,安全费用预算 ¥1,250,000.00 元。
+```
+
+**脱敏后**:
+```
+[1号特大桥](K0+000~K1+000)跨越[1号河流],全长1000m。
+[项目经理A](电话:[手机号脱敏])负责现场安全管理,安全费用预算 [金额脱敏] 元。
+```
+
+## 运行测试
+
+```bash
+python test_desensitize.py
+```

+ 24 - 0
core/construction_review/component/desensitize/__init__.py

@@ -0,0 +1,24 @@
+"""
+施工方案数据脱敏模块
+
+提供四维度脱敏能力:PII、地理坐标、商业标识、财务数据
+支持外部API测试和本地部署两阶段切换
+"""
+
+from .engine import LocalDesensitizationEngine, DesensitizedResult, EntityRegistry
+from .dict_manager import DictManager
+from .validator import BlackWhiteListChecker, ValidationResult
+from .remapper import ResultRemapper, RemapResult
+from .model_client import DesensitizeModelClient
+
+__all__ = [
+    "LocalDesensitizationEngine",
+    "DesensitizedResult",
+    "EntityRegistry",
+    "DictManager",
+    "BlackWhiteListChecker",
+    "ValidationResult",
+    "ResultRemapper",
+    "RemapResult",
+    "DesensitizeModelClient",
+]

+ 292 - 0
core/construction_review/component/desensitize/dict_manager.py

@@ -0,0 +1,292 @@
+"""
+脱敏字典管理器
+
+负责脱敏字典的加密存储、读取和生命周期管理。
+本地保留脱敏字典,支持将云端审查结果还原为真实工程术语。
+
+根据 wlast.md 文档第6.3.1节和附录A设计
+"""
+
+import json
+import hashlib
+import logging
+from typing import Dict, Any, Optional
+from datetime import datetime
+from pathlib import Path
+
+try:
+    from cryptography.fernet import Fernet
+    from cryptography.hazmat.primitives import hashes
+    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
+    import base64
+    CRYPTO_AVAILABLE = True
+except ImportError:
+    CRYPTO_AVAILABLE = False
+
+from foundation.infrastructure.config.config import config_handler
+from foundation.infrastructure.cache.redis_connection import RedisConnectionFactory
+
+logger = logging.getLogger(__name__)
+
+
+class DictManager:
+    """脱敏字典管理器
+
+    功能:
+    1. 脱敏字典的加密存储(本地文件 + Redis缓存)
+    2. 脱敏字典的读取和解密
+    3. 脱敏字典的生命周期管理(自动清理)
+    4. 多版本备份支持
+    """
+
+    # Redis 键前缀
+    REDIS_KEY_PREFIX = "desensitize:dict:"
+    REDIS_TTL = 7 * 24 * 3600  # 7天过期
+
+    def __init__(self):
+        """初始化字典管理器"""
+        self.cfg = config_handler.get_section("desensitize")
+        self.storage_path = Path(self.cfg.get("DICT_STORAGE_PATH", "./data/desensitize_dicts"))
+        self.storage_path.mkdir(parents=True, exist_ok=True)
+
+        # 初始化加密(如果配置了密钥)
+        self.encryption_key = self._get_encryption_key()
+        self.cipher = None
+        if self.encryption_key and CRYPTO_AVAILABLE:
+            self.cipher = Fernet(self.encryption_key)
+        elif self.encryption_key and not CRYPTO_AVAILABLE:
+            logger.warning("[DictManager] cryptography 库未安装,加密功能不可用")
+
+    def _get_encryption_key(self) -> Optional[bytes]:
+        """获取加密密钥"""
+        key_str = self.cfg.get("DICT_ENCRYPTION_KEY", "")
+
+        # 如果配置了 KMS 引用或环境变量
+        if key_str.startswith("${") and key_str.endswith("}"):
+            env_var = key_str[2:-1]
+            import os
+            key_str = os.environ.get(env_var, "")
+
+        if not key_str:
+            # 使用默认密钥(仅开发环境)
+            logger.warning("[DictManager] 使用默认加密密钥,生产环境请配置安全密钥")
+            key_str = "LQAgentPlatform-Desensitize-Default-Key-32bytes"
+
+        # 生成 Fernet 密钥
+        if CRYPTO_AVAILABLE:
+            kdf = PBKDF2HMAC(
+                algorithm=hashes.SHA256(),
+                length=32,
+                salt=b"lqagentplatform_desensitize_salt",
+                iterations=100000,
+            )
+            key = base64.urlsafe_b64encode(kdf.derive(key_str.encode()))
+            return key
+        return None
+
+    async def save(self, task_id: str, mapping_dict: Dict[str, Any]) -> str:
+        """保存脱敏字典
+
+        Args:
+            task_id: 任务唯一标识
+            mapping_dict: 脱敏映射字典
+
+        Returns:
+            dict_hash: 字典哈希值(用于验证完整性)
+        """
+        # 1. 构造完整字典
+        full_dict = {
+            "version": "1.0",
+            "created_at": datetime.now().isoformat(),
+            "task_id": task_id,
+            "mappings": mapping_dict,
+        }
+
+        # 2. 计算哈希
+        dict_content = json.dumps(full_dict, sort_keys=True, ensure_ascii=False)
+        dict_hash = hashlib.sha256(dict_content.encode()).hexdigest()[:16]
+        full_dict["dict_hash"] = dict_hash
+
+        # 3. 加密(如果可用)
+        if self.cipher:
+            encrypted_content = self.cipher.encrypt(dict_content.encode())
+            storage_content = encrypted_content
+            storage_format = "encrypted"
+        else:
+            storage_content = dict_content.encode()
+            storage_format = "plain"
+
+        # 4. 本地文件存储
+        file_path = self.storage_path / f"{task_id}.dict"
+        try:
+            with open(file_path, "wb") as f:
+                f.write(storage_content)
+            logger.info(f"[DictManager] 脱敏字典已保存: {file_path}")
+        except Exception as e:
+            logger.error(f"[DictManager] 本地存储失败: {e}")
+            raise
+
+        # 5. Redis 缓存(便于快速访问)
+        try:
+            redis_conn = await RedisConnectionFactory.get_connection()
+            redis_key = f"{self.REDIS_KEY_PREFIX}{task_id}"
+            await redis_conn.setex(
+                redis_key,
+                self.REDIS_TTL,
+                json.dumps({
+                    "format": storage_format,
+                    "hash": dict_hash,
+                    "content": base64.b64encode(storage_content).decode() if isinstance(storage_content, bytes) else storage_content
+                })
+            )
+        except Exception as e:
+            logger.warning(f"[DictManager] Redis 缓存失败: {e}")
+
+        return dict_hash
+
+    async def load(self, task_id: str) -> Optional[Dict[str, Any]]:
+        """加载脱敏字典
+
+        Args:
+            task_id: 任务唯一标识
+
+        Returns:
+            脱敏字典,如果不存在则返回 None
+        """
+        # 1. 尝试从 Redis 加载
+        try:
+            redis_conn = await RedisConnectionFactory.get_connection()
+            redis_key = f"{self.REDIS_KEY_PREFIX}{task_id}"
+            cached = await redis_conn.get(redis_key)
+            if cached:
+                data = json.loads(cached)
+                storage_content = base64.b64decode(data["content"])
+
+                if data["format"] == "encrypted" and self.cipher:
+                    decrypted = self.cipher.decrypt(storage_content)
+                    return json.loads(decrypted.decode())
+                else:
+                    return json.loads(storage_content.decode())
+        except Exception as e:
+            logger.warning(f"[DictManager] Redis 加载失败: {e}")
+
+        # 2. 从本地文件加载
+        file_path = self.storage_path / f"{task_id}.dict"
+        if not file_path.exists():
+            logger.error(f"[DictManager] 字典文件不存在: {file_path}")
+            return None
+
+        try:
+            with open(file_path, "rb") as f:
+                storage_content = f.read()
+
+            # 解密
+            if self.cipher:
+                try:
+                    decrypted = self.cipher.decrypt(storage_content)
+                    return json.loads(decrypted.decode())
+                except Exception as e:
+                    logger.error(f"[DictManager] 解密失败: {e}")
+                    # 尝试作为明文解析
+                    return json.loads(storage_content.decode())
+            else:
+                return json.loads(storage_content.decode())
+
+        except Exception as e:
+            logger.error(f"[DictManager] 文件加载失败: {e}")
+            return None
+
+    async def get_mapping(self, task_id: str) -> Optional[Dict[str, Any]]:
+        """获取映射关系(简化接口)"""
+        full_dict = await self.load(task_id)
+        if full_dict:
+            return full_dict.get("mappings", {})
+        return None
+
+    async def delete(self, task_id: str) -> bool:
+        """删除脱敏字典"""
+        success = True
+
+        # 删除本地文件
+        file_path = self.storage_path / f"{task_id}.dict"
+        try:
+            if file_path.exists():
+                file_path.unlink()
+                logger.info(f"[DictManager] 本地字典已删除: {file_path}")
+        except Exception as e:
+            logger.error(f"[DictManager] 本地删除失败: {e}")
+            success = False
+
+        # 删除 Redis 缓存
+        try:
+            redis_conn = await RedisConnectionFactory.get_connection()
+            redis_key = f"{self.REDIS_KEY_PREFIX}{task_id}"
+            await redis_conn.delete(redis_key)
+        except Exception as e:
+            logger.warning(f"[DictManager] Redis 删除失败: {e}")
+
+        return success
+
+    async def exists(self, task_id: str) -> bool:
+        """检查字典是否存在"""
+        file_path = self.storage_path / f"{task_id}.dict"
+        return file_path.exists()
+
+    def get_dict_metadata(self, task_id: str) -> Optional[Dict[str, Any]]:
+        """获取字典元信息(不加载完整内容)"""
+        file_path = self.storage_path / f"{task_id}.dict"
+        if not file_path.exists():
+            return None
+
+        try:
+            stat = file_path.stat()
+            return {
+                "task_id": task_id,
+                "file_path": str(file_path),
+                "file_size": stat.st_size,
+                "modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(),
+                "exists": True
+            }
+        except Exception as e:
+            logger.error(f"[DictManager] 获取元信息失败: {e}")
+            return None
+
+    async def cleanup_expired(self, days: int = 7) -> int:
+        """清理过期字典
+
+        Args:
+            days: 过期天数
+
+        Returns:
+            清理的字典数量
+        """
+        import time
+        cutoff_time = time.time() - (days * 24 * 3600)
+        cleaned = 0
+
+        for dict_file in self.storage_path.glob("*.dict"):
+            try:
+                stat = dict_file.stat()
+                if stat.st_mtime < cutoff_time:
+                    dict_file.unlink()
+                    cleaned += 1
+                    logger.info(f"[DictManager] 清理过期字典: {dict_file.name}")
+            except Exception as e:
+                logger.warning(f"[DictManager] 清理失败 {dict_file}: {e}")
+
+        return cleaned
+
+    async def create_backup(self, task_id: str) -> Optional[str]:
+        """创建字典备份"""
+        file_path = self.storage_path / f"{task_id}.dict"
+        if not file_path.exists():
+            return None
+
+        backup_path = self.storage_path / f"{task_id}.dict.backup"
+        try:
+            import shutil
+            shutil.copy2(file_path, backup_path)
+            return str(backup_path)
+        except Exception as e:
+            logger.error(f"[DictManager] 备份失败: {e}")
+            return None

+ 293 - 0
core/construction_review/component/desensitize/engine.py

@@ -0,0 +1,293 @@
+"""
+脱敏引擎核心
+
+LocalDesensitizationEngine: 本地脱敏引擎主入口
+协调四维度处理器、模型客户端和字典管理器
+"""
+
+import re
+import logging
+from dataclasses import dataclass, field
+from typing import Optional, Dict, Any, List
+from pathlib import Path
+
+from .processors.pii_processor import PIIDesensitizer
+from .processors.geo_processor import GeoDesensitizer
+from .processors.biz_processor import BizDesensitizer
+from .processors.financial_processor import FinancialDesensitizer
+from .dict_manager import DictManager
+from .validator import BlackWhiteListChecker
+from .model_client import DesensitizeModelClient
+from foundation.infrastructure.config.config import config_handler
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class DesensitizedResult:
+    """脱敏处理结果"""
+    content: str  # 脱敏后的文本内容
+    task_id: str  # 任务ID
+    dict_hash: str  # 脱敏字典哈希
+    statistics: Dict[str, int] = field(default_factory=dict)  # 统计信息
+    is_valid: bool = True  # 是否通过黑白名单校验
+    violations: List[Dict] = field(default_factory=list)  # 违规项列表
+
+
+class EntityRegistry:
+    """实体注册表 - 跨维度追踪实体映射关系"""
+
+    def __init__(self):
+        self.persons: Dict[str, str] = {}  # 姓名 -> 角色占位符
+        self.entities: Dict[str, str] = {}  # 实体名 -> 序列化占位符
+        self.companies: Dict[str, str] = {}  # 公司名 -> 标准角色
+        self.coordinates: Dict[str, Any] = {  # 坐标映射
+            "base_point": None,  # 基准点(最小桩号)
+            "mappings": {}  # 原始 -> 相对
+        }
+        self.amounts: List[str] = []  # 金额列表
+
+        # 计数器用于生成序列号
+        self._person_counter = 0
+        self._entity_counter = 0
+
+    def register_person(self, name: str, role: str = "人员") -> str:
+        """注册人员,返回角色占位符"""
+        if name in self.persons:
+            return self.persons[name]
+
+        self._person_counter += 1
+        placeholder = f"[{role}{self._get_suffix(self._person_counter)}]"
+        self.persons[name] = placeholder
+        return placeholder
+
+    def register_entity(self, name: str, entity_type: str = "实体") -> str:
+        """注册工程实体(桥/隧/河等),返回序列化占位符"""
+        if name in self.entities:
+            return self.entities[name]
+
+        self._entity_counter += 1
+        type_map = {
+            "桥": "号特大桥",
+            "隧道": "号隧道",
+            "河": "号河流",
+            "涵洞": "号涵洞",
+            "立交": "号立交"
+        }
+        suffix = type_map.get(entity_type, f"号{entity_type}")
+        placeholder = f"[{self._entity_counter}{suffix}]"
+        self.entities[name] = placeholder
+        return placeholder
+
+    def register_company(self, name: str, company_type: str = "单位") -> str:
+        """注册企业/单位,返回标准角色词汇"""
+        if name in self.companies:
+            return self.companies[name]
+
+        type_map = {
+            "建设": "建设单位",
+            "总包": "总承包单位",
+            "监理": "监理单位",
+            "设计": "设计单位",
+            "劳务": "劳务分包单位",
+            "数字化": "数字化实施方"
+        }
+        placeholder = f"[{type_map.get(company_type, company_type)}]"
+        self.companies[name] = placeholder
+        return placeholder
+
+    def set_coordinate_base(self, base_point: str):
+        """设置坐标相对化基准点"""
+        self.coordinates["base_point"] = base_point
+        self.coordinates["relative_zero"] = "K0+000"
+
+    def register_coordinate(self, original: str, relative: str):
+        """注册坐标映射关系"""
+        self.coordinates["mappings"][original] = relative
+
+    def export_mapping(self) -> Dict[str, Any]:
+        """导出完整映射字典"""
+        return {
+            "version": "1.0",
+            "persons": [
+                {"original": k, "masked": v, "role": v[1:-1].rstrip("ABCDEFGHIJ")}
+                for k, v in self.persons.items()
+            ],
+            "entities": [
+                {"original": k, "masked": v}
+                for k, v in self.entities.items()
+            ],
+            "companies": [
+                {"original": k, "masked": v}
+                for k, v in self.companies.items()
+            ],
+            "coordinates": self.coordinates,
+            "amount_count": len(self.amounts)
+        }
+
+    def get_statistics(self) -> Dict[str, int]:
+        """获取统计信息"""
+        return {
+            "pii_count": len(self.persons) + len(self.amounts),
+            "geo_count": len(self.entities) + len(self.coordinates.get("mappings", {})),
+            "biz_count": len(self.companies),
+            "financial_count": len(self.amounts)
+        }
+
+    @staticmethod
+    def _get_suffix(n: int) -> str:
+        """获取序号后缀 A, B, C..."""
+        return chr(ord('A') + n - 1) if n <= 26 else f"{n}"
+
+
+class LocalDesensitizationEngine:
+    """本地脱敏引擎主入口
+
+    支持两阶段配置:
+    - DEPLOY_PHASE=test: 使用外部API测试(仅测试数据集)
+    - DEPLOY_PHASE=local: 使用本地模型(生产环境)
+
+    支持两方案切换:
+    - scheme_a: 规则引擎主导 + 模型语义兜底
+    - scheme_b: 模型全量语义驱动 + 规则后校验
+    """
+
+    def __init__(self):
+        self.cfg = config_handler.get_section("desensitize")
+        self.deploy_phase = self.cfg.get("DEPLOY_PHASE", "test")
+        self.scheme = self.cfg.get("DESENSITIZE_SCHEME", "scheme_a")
+        self.level = self.cfg.get("DEFAULT_LEVEL", "standard")
+
+        # 初始化处理器
+        self.pii_processor = PIIDesensitizer()
+        self.geo_processor = GeoDesensitizer()
+        self.biz_processor = BizDesensitizer()
+        self.financial_processor = FinancialDesensitizer()
+
+        # 初始化模型客户端(可选,scheme_b或scheme_a补充阶段使用)
+        self.model_client: Optional[DesensitizeModelClient] = None
+        if self._need_model():
+            self.model_client = DesensitizeModelClient()
+
+        # 初始化校验器和管理器
+        self.validator = BlackWhiteListChecker()
+        self.dict_manager = DictManager()
+
+        logger.info(f"[DesensitizeEngine] Initialized: phase={self.deploy_phase}, "
+                   f"scheme={self.scheme}, level={self.level}")
+
+    def _need_model(self) -> bool:
+        """判断当前配置是否需要调用大模型"""
+        if self.deploy_phase == "test":
+            return True  # 测试阶段必须调用模型
+        if self.scheme == "scheme_b":
+            return True  # 方案B需要模型全量推理
+        return False  # scheme_a 可能不需要(纯规则引擎)
+
+    async def process(self, content: str, task_id: str) -> DesensitizedResult:
+        """执行完整脱敏流程
+
+        Args:
+            content: 原始文档内容
+            task_id: 任务唯一标识
+
+        Returns:
+            DesensitizedResult: 脱敏结果
+        """
+        logger.info(f"[DesensitizeEngine] Processing task {task_id}, "
+                   f"content_length={len(content)}")
+
+        # 创建实体注册表(跨维度共享)
+        registry = EntityRegistry()
+
+        try:
+            if self.scheme == "scheme_a":
+                result_content = await self._process_scheme_a(content, registry)
+            else:  # scheme_b
+                result_content = await self._process_scheme_b(content, registry)
+
+            # 黑白名单最终校验(强制,不可跳过)
+            validation = self.validator.validate(result_content)
+
+            if not validation.is_valid:
+                logger.error(f"[DesensitizeEngine] Validation failed for {task_id}: "
+                           f"{len(validation.violations)} violations")
+                # 根据配置决定是阻断还是仅告警
+                if self.cfg.get("VALIDATION_FAILURE_ACTION", "block") == "block":
+                    return DesensitizedResult(
+                        content=result_content,
+                        task_id=task_id,
+                        dict_hash="",
+                        statistics=registry.get_statistics(),
+                        is_valid=False,
+                        violations=validation.violations
+                    )
+
+            # 保存脱敏字典
+            mapping_dict = registry.export_mapping()
+            dict_hash = await self.dict_manager.save(task_id, mapping_dict)
+
+            return DesensitizedResult(
+                content=result_content,
+                task_id=task_id,
+                dict_hash=dict_hash,
+                statistics=registry.get_statistics(),
+                is_valid=True,
+                violations=[]
+            )
+
+        except Exception as e:
+            logger.exception(f"[DesensitizeEngine] Processing failed for {task_id}")
+            raise
+
+    async def _process_scheme_a(self, content: str, registry: EntityRegistry) -> str:
+        """方案A:规则引擎主导 + 模型语义兜底"""
+        # Step 1: 规则引擎全量扫描
+        content = self.pii_processor.process(content, registry)
+        content = self.geo_processor.process(content, registry)
+        content = self.biz_processor.process(content, registry)
+        content = self.financial_processor.process(content, registry)
+
+        # Step 2: 模型补充识别(如果配置了模型客户端)
+        if self.model_client and self.model_client.selective:
+            # 提取低置信度片段(这里简化处理,实际可基于规则置信度)
+            low_confidence_segments = self._extract_uncertain_segments(content)
+            if low_confidence_segments:
+                logger.debug(f"[SchemeA] Model supplement for {len(low_confidence_segments)} segments")
+                # 可选:调用模型对低置信度片段做补充识别
+                # content = await self.model_client.desensitize_text(content, self.level)
+
+        return content
+
+    async def _process_scheme_b(self, content: str, registry: EntityRegistry) -> str:
+        """方案B:模型全量语义驱动 + 规则后校验"""
+        if not self.model_client:
+            raise RuntimeError("Scheme B requires model client but none available")
+
+        # Step 1: 模型全量语义脱敏
+        content = await self.model_client.desensitize_text(content, self.level)
+
+        # Step 2: 规则引擎补充校验(精确格式兜底)
+        # 确保结构化字段(如精确桩号、手机号格式)完全脱敏
+        content = self.pii_processor.process(content, registry, strict_mode=True)
+        content = self.geo_processor.process(content, registry, strict_mode=True)
+
+        return content
+
+    def _extract_uncertain_segments(self, content: str) -> List[str]:
+        """提取可能未脱敏的低置信度片段(用于模型补充识别)"""
+        # 简化实现:检查是否还有潜在的敏感模式残留
+        uncertain = []
+
+        # 检查是否有可能是人名但未被处理的片段(如"负责人:XXX"后的XXX)
+        name_patterns = [
+            r'负责人[::]\s*([\u4e00-\u9fa5]{2,4})',
+            r'项目经理[::]\s*([\u4e00-\u9fa5]{2,4})',
+            r'安全员[::]\s*([\u4e00-\u9fa5]{2,4})',
+        ]
+
+        for pattern in name_patterns:
+            matches = re.findall(pattern, content)
+            uncertain.extend(matches)
+
+        return uncertain

+ 267 - 0
core/construction_review/component/desensitize/model_client.py

@@ -0,0 +1,267 @@
+"""
+本地大模型脱敏客户端
+
+支持两阶段配置:
+- 阶段一(测试):使用外部API测试候选模型
+- 阶段二(生产):使用本地部署模型(Qwen3-8B/30B)
+
+根据 wlast.md 文档第3.2节和8.3节设计
+"""
+
+import re
+import logging
+from typing import Optional, Dict, Any, List
+from openai import AsyncOpenAI
+
+from foundation.infrastructure.config.config import config_handler
+
+logger = logging.getLogger(__name__)
+
+
+class DesensitizeModelClient:
+    """本地大模型脱敏客户端(仅限内网 Qwen3-8B / Qwen3-30B)
+
+    ⚠️ 安全约束:
+      - 生产阶段 SERVER_URL 必须为内网地址(192.168.x.x / 10.x.x.x / 172.x.x.x)
+      - 禁止配置任何外部公网大模型 API 地址
+      - 脱敏处理必须在调用外部审查 API 之前完成
+
+    阶段配置:
+      - DEPLOY_PHASE=test: 使用外部API测试(仅测试数据集)
+      - DEPLOY_PHASE=local: 使用本地部署模型(生产环境)
+    """
+
+    # 脱敏 Prompt 模板(根据 wlast.md 文档附录B)
+    DESENSITIZE_PROMPT_TEMPLATE = """你是专业的施工方案数据脱敏专家,请对以下内容进行{level}级别脱敏:
+
+【脱敏规则】
+1. PII(个人身份信息):
+   - 姓名 → [项目经理A]、[安全员B] 等角色占位符
+   - 手机号 → [手机号脱敏]
+   - 身份证号 → [证件号脱敏]
+   - 证书编号 → [证件号脱敏]
+   - 邮箱 → [邮箱脱敏]
+
+2. 地理坐标(工程相对化):
+   - 绝对桩号(如D1K86+279.91)→ 转换为相对桩号(K0+000为起点)
+   - 桥隧名称(如映雪特大桥)→ [1号特大桥] 等序列化命名
+   - 高程数据(海拔1850.5m)→ [中山区] 等地形描述
+   - 行政地名(四川省成都市)→ [某地区]
+
+3. 企业标识:
+   - 公司名称 → [建设单位]、[总承包单位]、[监理单位] 等标准角色
+   - 项目名称 → [某工程项目]
+   - 合同编号 → [合同编号脱敏]
+
+4. 财务数据:
+   - 金额(¥1,250,000.00)→ [金额脱敏]元
+   - 设备租赁单价 → [单价脱敏]/单位时间
+   - 材料单价 → [单价脱敏]/单位
+   - 工程量数值 → [数量脱敏]+单位
+
+【要求】
+- 保持原文结构、段落格式和工程逻辑
+- 保留技术参数(如混凝土标号、钢筋规格)
+- 保留工艺工序描述
+- 保留安全规范条款
+- 保留组织架构关系(仅替换名称为占位符)
+
+请直接返回脱敏后的完整文本:"""
+
+    def __init__(self):
+        """初始化模型客户端"""
+        self._load_config()
+        self.client = AsyncOpenAI(
+            base_url=self.server_url,
+            api_key=self.api_key
+        )
+
+        logger.info(f"[DesensitizeModelClient] 初始化完成: "
+                   f"phase={self.deploy_phase}, model={self.model}, "
+                   f"scheme={self.scheme if self.deploy_phase == 'local' else 'N/A'}")
+
+    def _load_config(self):
+        """加载配置"""
+        base_cfg = config_handler.get_section("desensitize")
+        self.deploy_phase = base_cfg.get("DEPLOY_PHASE", "test")
+
+        if self.deploy_phase == "test":
+            # 阶段一:外部 API 测试验证
+            self._load_test_config()
+        else:
+            # 阶段二:本地部署生产模式
+            self._load_local_config()
+
+    def _load_test_config(self):
+        """加载测试阶段配置"""
+        test_cfg = config_handler.get_section("desensitize_model_test")
+        test_model = test_cfg.get("TEST_MODEL", "gemma3_12b")
+        cfg = config_handler.get_section(f"desensitize_model_test_{test_model}")
+
+        self.server_url = cfg.get("SERVER_URL", "")
+        self.api_key = cfg.get("API_KEY", "dummy")
+        self.model = cfg.get("MODEL_NAME", "")
+        self.max_tokens = int(cfg.get("MAX_TOKENS", "4096"))
+        self.temperature = float(cfg.get("TEMPERATURE", "0.1"))
+
+        # 测试阶段特有配置
+        self.selective = False
+        self.confidence_threshold = 0.0
+        self.scheme = "test"
+
+        # 打印警告
+        logger.warning(
+            f"[脱敏] 当前为测试阶段(DEPLOY_PHASE=test),"
+            f"使用外部模型 {self.model},请勿传入真实生产文档"
+        )
+
+        # 检查 API KEY 是否为环境变量引用
+        if self.api_key.startswith("${") and self.api_key.endswith("}"):
+            env_var = self.api_key[2:-1]
+            import os
+            self.api_key = os.environ.get(env_var, "")
+
+    def _load_local_config(self):
+        """加载本地生产配置"""
+        base_cfg = config_handler.get_section("desensitize")
+        self.scheme = base_cfg.get("DESENSITIZE_SCHEME", "scheme_a")
+
+        cfg = config_handler.get_section(f"desensitize_model_{self.scheme}")
+        self.server_url = cfg.get("SERVER_URL", "")
+        self.api_key = cfg.get("API_KEY", "dummy")
+        self.model = cfg.get("MODEL_NAME", "")
+        self.max_tokens = int(cfg.get("MAX_TOKENS", "2048"))
+        self.temperature = float(cfg.get("TEMPERATURE", "0.1"))
+
+        # 本地特有配置
+        self.selective = cfg.get("SELECTIVE_INFERENCE", "false").lower() == "true"
+        self.confidence_threshold = float(cfg.get("CONFIDENCE_THRESHOLD", "0.7"))
+        self.max_input_length = int(cfg.get("MAX_INPUT_LENGTH", "8192"))
+
+        # 生产阶段:强制校验必须为内网地址
+        if not self._is_internal_ip(self.server_url):
+            raise ValueError(
+                f"[脱敏] 生产阶段只允许内网地址,拒绝外网地址: {self.server_url}\n"
+                f"请将 DEPLOY_PHASE 改为 test,或将 SERVER_URL 改为内网地址"
+            )
+
+    def _is_internal_ip(self, url: str) -> bool:
+        """检查是否为内网地址"""
+        internal_prefixes = [
+            "http://192.168.",
+            "http://10.",
+            "http://172.16.",
+            "http://172.17.",
+            "http://172.18.",
+            "http://172.19.",
+            "http://172.20.",
+            "http://172.21.",
+            "http://172.22.",
+            "http://172.23.",
+            "http://172.24.",
+            "http://172.25.",
+            "http://172.26.",
+            "http://172.27.",
+            "http://172.28.",
+            "http://172.29.",
+            "http://172.30.",
+            "http://172.31.",
+            "https://192.168.",
+            "https://10.",
+        ]
+        return any(url.startswith(p) for p in internal_prefixes)
+
+    async def desensitize_text(self, text: str, level: str = "standard") -> str:
+        """使用大模型进行智能脱敏
+
+        Args:
+            text: 待脱敏文本
+            level: 脱敏级别 (minimal/standard/strict)
+
+        Returns:
+            脱敏后的文本
+        """
+        # 截断过长的输入
+        if len(text) > self.max_input_length:
+            logger.warning(f"[DesensitizeModelClient] 输入文本过长({len(text)}), 截断至{self.max_input_length}")
+            text = text[:self.max_input_length]
+
+        system_prompt = self.DESENSITIZE_PROMPT_TEMPLATE.format(level=level)
+
+        try:
+            response = await self.client.chat.completions.create(
+                model=self.model,
+                messages=[
+                    {"role": "system", "content": system_prompt},
+                    {"role": "user", "content": text}
+                ],
+                temperature=self.temperature,
+                max_tokens=self.max_tokens
+            )
+
+            result = response.choices[0].message.content
+
+            # 清理可能的代码块标记
+            result = self._clean_code_blocks(result)
+
+            logger.debug(f"[DesensitizeModelClient] 脱敏完成: {len(text)} -> {len(result)} chars")
+            return result
+
+        except Exception as e:
+            logger.error(f"[DesensitizeModelClient] 模型调用失败: {e}")
+            raise
+
+    async def desensitize_chunks(self, chunks: List[str], level: str = "standard") -> List[str]:
+        """批量脱敏多个文本块
+
+        Args:
+            chunks: 文本块列表
+            level: 脱敏级别
+
+        Returns:
+            脱敏后的文本块列表
+        """
+        results = []
+        for i, chunk in enumerate(chunks):
+            try:
+                desensitized = await self.desensitize_text(chunk, level)
+                results.append(desensitized)
+                logger.debug(f"[DesensitizeModelClient] 块{i+1}/{len(chunks)}脱敏完成")
+            except Exception as e:
+                logger.error(f"[DesensitizeModelClient] 块{i+1}脱敏失败: {e}")
+                # 失败时保留原文(或可根据策略阻断)
+                results.append(chunk)
+        return results
+
+    def _clean_code_blocks(self, text: str) -> str:
+        """清理代码块标记"""
+        # 移除 markdown 代码块标记
+        text = re.sub(r'^```\w*\n?', '', text)
+        text = re.sub(r'\n?```$', '', text)
+        return text.strip()
+
+    async def check_confidence(self, text: str) -> Dict[str, Any]:
+        """检查文本脱敏置信度(用于方案A的选择性推理)
+
+        返回置信度分数和可能需要人工检查的片段
+        """
+        # 简单启发式检查
+        risk_patterns = [
+            (r'\b1[3-9]\d{9}\b', 'phone'),
+            (r'\b\d{17}[\dXx]\b', 'id_card'),
+            (r'[\u4e00-\u9fa5]{2,4}(?:集团|有限公司|局)', 'company'),
+        ]
+
+        risks = []
+        for pattern, risk_type in risk_patterns:
+            if re.search(pattern, text):
+                risks.append(risk_type)
+
+        confidence = 1.0 - (len(risks) * 0.3)  # 简单线性计算
+        confidence = max(0.0, min(1.0, confidence))
+
+        return {
+            "confidence": confidence,
+            "risks": risks,
+            "needs_review": confidence < self.confidence_threshold
+        }

+ 39 - 0
core/construction_review/component/desensitize/processors/base_processor.py

@@ -0,0 +1,39 @@
+"""
+脱敏处理器基类
+"""
+
+import re
+from abc import ABC, abstractmethod
+from typing import Any, Dict
+
+
+class BaseDesensitizer(ABC):
+    """脱敏处理器基类"""
+
+    def __init__(self):
+        self.patterns: Dict[str, re.Pattern] = {}
+        self._compile_patterns()
+
+    @abstractmethod
+    def _compile_patterns(self):
+        """编译正则表达式模式"""
+        pass
+
+    @abstractmethod
+    def process(self, content: str, registry: Any, strict_mode: bool = False) -> str:
+        """处理内容并返回脱敏后的文本
+
+        Args:
+            content: 输入文本
+            registry: 实体注册表
+            strict_mode: 严格模式(用于方案B的后校验阶段)
+
+        Returns:
+            脱敏后的文本
+        """
+        pass
+
+    def _replace_with_placeholder(self, content: str, pattern: re.Pattern,
+                                   placeholder: str, registry: Any = None) -> str:
+        """通用替换方法"""
+        return pattern.sub(placeholder, content)

+ 155 - 0
core/construction_review/component/desensitize/processors/biz_processor.py

@@ -0,0 +1,155 @@
+"""
+商业标识脱敏处理器
+
+处理对象:项目名称、建设单位、总承包单位、监理单位、劳务分包单位等
+执行策略:彻底泛化,替换为标准工程角色词汇
+"""
+
+import re
+from typing import Any
+from .base_processor import BaseDesensitizer
+
+
+class BizDesensitizer(BaseDesensitizer):
+    """商业标识脱敏处理器"""
+
+    def _compile_patterns(self):
+        """编译商业标识相关正则表达式"""
+        # 项目标识(如"都四山地轨道交通项目DSZH标")
+        self.patterns["project"] = re.compile(
+            r'([\u4e00-\u9fa5A-Z0-9\-]{5,30}(?:项目|标段|工程))'
+        )
+
+        # 建设单位(含集团、局、有限公司等)
+        self.patterns["constructor"] = re.compile(
+            r'([\u4e00-\u9fa5]{2,6}(?:集团|局|有限公司))'
+        )
+
+        # 总承包单位
+        self.patterns["contractor"] = re.compile(
+            r'(总承包单位[::]?\s*[\u4e00-\u9fa5]{2,10}(?:集团|局|公司)?)'
+        )
+
+        # 监理单位
+        self.patterns["supervisor"] = re.compile(
+            r'([\u4e00-\u9fa5]{2,8}(?:监理|咨询)(?:公司|单位))'
+        )
+
+        # 设计单位
+        self.patterns["designer"] = re.compile(
+            r'([\u4e00-\u9fa5]{2,8}(?:设计|勘察)(?:院|所|公司))'
+        )
+
+        # 劳务分包单位
+        self.patterns["subcontractor"] = re.compile(
+            r'([\u4e00-\u9fa5]{2,10}(?:劳务|分包)(?:公司|单位))'
+        )
+
+        # 数字化/科技公司
+        self.patterns["tech_company"] = re.compile(
+            r'([\u4e00-\u9fa5]{2,8}(?:数字|科技|信息|软件)(?:公司|科技))'
+        )
+
+        # 合同编号
+        self.patterns["contract_no"] = re.compile(
+            r'(合同编号?[::]?\s*[A-Z0-9\-]{5,20})'
+        )
+
+        # 统一社会信用代码/组织机构代码
+        self.patterns["credit_code"] = re.compile(
+            r'([A-Z0-9]{18})'  # 18位统一社会信用代码
+        )
+
+    def process(self, content: str, registry: Any, strict_mode: bool = False) -> str:
+        """执行商业标识脱敏"""
+        # 1. 总承包单位泛化(必须在项目标识之前处理,防止"工程"被项目模式破坏)
+        def replace_contractor(match):
+            return "[总承包单位]"
+
+        content = self.patterns["contractor"].sub(replace_contractor, content)
+
+        # 补充匹配:行首或换行后的"总承包单位"(没有冒号的情况)
+        content = re.sub(
+            r'([,。;\n]|^)(总承包单位)[::\s]*([\u4e00-\u9fa5]{2,10}(?:集团|局|公司))',
+            r'\1[\2]',
+            content
+        )
+
+        # 2. 项目标识泛化(必须在总承包单位之后处理)
+        def replace_project(match):
+            original = match.group(1)
+            placeholder = "[某工程项目]"
+            registry.register_company(original, "项目")
+            return placeholder
+
+        content = self.patterns["project"].sub(replace_project, content)
+
+        # 3. 建设单位泛化
+        def replace_constructor(match):
+            original = match.group(1)
+            placeholder = registry.register_company(original, "建设")
+            return placeholder
+
+        content = self.patterns["constructor"].sub(replace_constructor, content)
+
+        # 4. 监理单位泛化
+        def replace_supervisor(match):
+            original = match.group(1)
+            placeholder = registry.register_company(original, "监理")
+            return placeholder
+
+        content = self.patterns["supervisor"].sub(replace_supervisor, content)
+
+        # 5. 设计单位泛化
+        def replace_designer(match):
+            original = match.group(1)
+            placeholder = registry.register_company(original, "设计")
+            return placeholder
+
+        content = self.patterns["designer"].sub(replace_designer, content)
+
+        # 6. 劳务分包单位泛化
+        def replace_subcontractor(match):
+            original = match.group(1)
+            placeholder = registry.register_company(original, "劳务")
+            return placeholder
+
+        content = self.patterns["subcontractor"].sub(replace_subcontractor, content)
+
+        # 7. 数字化/科技公司泛化
+        def replace_tech(match):
+            original = match.group(1)
+            placeholder = registry.register_company(original, "数字化")
+            return placeholder
+
+        content = self.patterns["tech_company"].sub(replace_tech, content)
+
+        # 8. 合同编号掩码
+        content = self.patterns["contract_no"].sub("[合同编号脱敏]", content)
+
+        # 9. 统一社会信用代码掩码
+        content = self.patterns["credit_code"].sub("[信用代码脱敏]", content)
+
+        return content
+
+    def extract_companies(self, content: str) -> list:
+        """提取所有企业/单位名称"""
+        companies = []
+
+        patterns = [
+            ("project", self.patterns["project"]),
+            ("constructor", self.patterns["constructor"]),
+            ("supervisor", self.patterns["supervisor"]),
+            ("designer", self.patterns["designer"]),
+            ("subcontractor", self.patterns["subcontractor"]),
+        ]
+
+        for company_type, pattern in patterns:
+            for match in pattern.finditer(content):
+                companies.append({
+                    "name": match.group(1),
+                    "type": company_type,
+                    "position": match.span()
+                })
+
+        return companies

+ 138 - 0
core/construction_review/component/desensitize/processors/financial_processor.py

@@ -0,0 +1,138 @@
+"""
+财务与成本敏感数据脱敏处理器
+
+处理对象:金额、设备租赁单价、材料单价、工程量数值等
+执行策略:全量数值掩码,保留表格科目结构
+"""
+
+import re
+from typing import Any
+from .base_processor import BaseDesensitizer
+
+
+class FinancialDesensitizer(BaseDesensitizer):
+    """财务数据脱敏处理器"""
+
+    def _compile_patterns(self):
+        """编译财务数据相关正则表达式"""
+        # 设备租赁单价(必须在通用金额之前匹配,防止金额模式先匹配)
+        self.patterns["equipment_price"] = re.compile(
+            r'(塔吊|起重机|挖掘机|装载机|压路机|混凝土搅拌站|钢筋加工设备)[\s\u4e00-\u9fa5]*'
+            r'\d{1,6}(?:\.\d{1,2})?\s*(?:元|万元)?\s*/\s*(?:月|天|台班|小时)'
+        )
+
+        # 材料单价
+        self.patterns["material_price"] = re.compile(
+            r'(钢筋|混凝土|水泥|砂石|模板|脚手架|防水材料|保温材料)[\s\u4e00-\u9fa5]*'
+            r'\d{1,5}(?:\.\d{1,2})?\s*(?:元|万元)?\s*/\s*(?:吨|立方米|平米|米|套)'
+        )
+
+        # 货币金额(含人民币符号、千分位)
+        # 匹配:¥1,250,000.00、¥ 50000、50000元、50,000.00元
+        self.patterns["amount_rmb"] = re.compile(
+            r'¥\s*\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?'
+        )
+        # 金额后带单位,添加负向后顾 (?<!\d) 确保前面不是数字,避免部分匹配
+        self.patterns["amount_yuan"] = re.compile(
+            r'(?<!\d)\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?\s*(?:元|万元|亿元)'
+        )
+
+        # 工程量数值(保留单位,掩码数值)
+        self.patterns["quantity"] = re.compile(
+            r'(\d{1,6}(?:\.\d{1,2})?)\s*(立方米|m³|平米|㎡|米|m|吨|t|个|套|根)'
+        )
+
+        # 预算/费用合计
+        self.patterns["total"] = re.compile(
+            r'(总计|合计|总价|预算金额)[::\s]*'
+            r'[¥]?\s*\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?'
+        )
+
+        # 纯数字大金额(5位以上数字,可能是金额)
+        self.patterns["large_number"] = re.compile(
+            r'\b(\d{5,}(?:,\d{3})*(?:\.\d{1,2})?)\b'
+        )
+
+        # 百分比(通常与费用比例相关,保留)
+        self.patterns["percentage"] = re.compile(
+            r'(\d{1,3}(?:\.\d{1,2})?)\s*%'
+        )
+
+    def process(self, content: str, registry: Any, strict_mode: bool = False) -> str:
+        """执行财务数据脱敏
+
+        保留表格的表头、项目分类及科目结构
+        仅掩码具体数值,确保AI能审查"是否编制了资金投入计划"
+        """
+        # 1. 设备租赁单价掩码(保留设备类型和单位)- 必须在通用金额之前
+        def replace_equipment_price(match):
+            equipment = match.group(1)
+            registry.amounts.append(match.group(0))  # 记录金额用于统计
+            return f"{equipment} [单价脱敏]/单位时间"
+
+        content = self.patterns["equipment_price"].sub(replace_equipment_price, content)
+
+        # 2. 材料单价掩码(保留材料名称和单位)- 必须在通用金额之前
+        def replace_material_price(match):
+            material = match.group(1)
+            registry.amounts.append(match.group(0))  # 记录金额用于统计
+            return f"{material} [单价脱敏]/单位"
+
+        content = self.patterns["material_price"].sub(replace_material_price, content)
+
+        # 3. 人民币金额掩码
+        for match in self.patterns["amount_rmb"].finditer(content):
+            registry.amounts.append(match.group(0))  # 记录金额用于统计
+        content = self.patterns["amount_rmb"].sub("¥[金额脱敏]", content)
+
+        # 4. 元单位金额掩码
+        def replace_amount_yuan(match):
+            text = match.group(0)
+            registry.amounts.append(text)  # 记录金额用于统计
+            # 保留单位(元/万元/亿元)
+            if "万元" in text:
+                return "[金额脱敏]万元"
+            elif "亿元" in text:
+                return "[金额脱敏]亿元"
+            else:
+                return "[金额脱敏]元"
+
+        content = self.patterns["amount_yuan"].sub(replace_amount_yuan, content)
+
+        # 5. 工程量数值掩码(保留单位)
+        def replace_quantity(match):
+            unit = match.group(2)
+            registry.amounts.append(match.group(1))  # 记录数值用于统计
+            return f"[数量脱敏]{unit}"
+
+        content = self.patterns["quantity"].sub(replace_quantity, content)
+
+        # 6. 预算合计掩码
+        for match in self.patterns["total"].finditer(content):
+            registry.amounts.append(match.group(0))  # 记录金额用于统计
+        content = self.patterns["total"].sub(r"\1:¥[金额脱敏]", content)
+
+        # 7. 严格模式下,大金额数字也掩码
+        if strict_mode:
+            for match in self.patterns["large_number"].finditer(content):
+                registry.amounts.append(match.group(0))  # 记录金额用于统计
+            content = self.patterns["large_number"].sub("[数值脱敏]", content)
+
+        # 8. 百分比保留(通常是费率比例,不涉及具体金额)
+        # content = self.patterns["percentage"].sub(r"[比例脱敏]%", content)
+
+        return content
+
+    def extract_amounts(self, content: str) -> list:
+        """提取所有金额数值(用于统计)"""
+        amounts = []
+
+        for pattern_name in ["amount_rmb", "amount_yuan"]:
+            for match in self.patterns[pattern_name].finditer(content):
+                amounts.append({
+                    "value": match.group(0),
+                    "type": pattern_name,
+                    "position": match.span()
+                })
+
+        return amounts

+ 169 - 0
core/construction_review/component/desensitize/processors/geo_processor.py

@@ -0,0 +1,169 @@
+"""
+地理坐标与基建数据脱敏处理器
+
+处理对象:桩号、高程、桥隧名称、地质数据、行政地名等
+执行策略:相对化处理(隐去绝对位置,保留物理逻辑)
+"""
+
+import re
+from typing import Any, Tuple, Optional
+from .base_processor import BaseDesensitizer
+
+
+class GeoDesensitizer(BaseDesensitizer):
+    """地理坐标脱敏处理器"""
+
+    def _compile_patterns(self):
+        """编译地理坐标相关正则表达式"""
+        # 绝对桩号格式:D1K86+279.91, K0+500, DK15+230.5 等
+        self.patterns["coordinate"] = re.compile(
+            r'([A-Z]?\d?[Kk])(\d{2,5})\+(\d{2,6}(?:\.\d+)?)'
+        )
+
+        # 实体名称(桥、隧道、涵洞、河流等)
+        self.patterns["bridge"] = re.compile(
+            r'([\u4e00-\u9fa5]{2,8}(?:特大桥|大桥))'
+        )
+        self.patterns["tunnel"] = re.compile(
+            r'([\u4e00-\u9fa5]{2,8}隧道)'
+        )
+        self.patterns["culvert"] = re.compile(
+            r'([\u4e00-\u9fa5]{2,6}涵洞)'
+        )
+        self.patterns["river"] = re.compile(
+            r'([\u4e00-\u9fa5]{2,6}(?:河|江))'
+        )
+
+        # 高程数据
+        self.patterns["elevation"] = re.compile(
+            r'海拔\s*(\d{3,4}(?:\.\d+)?)\s*[m米]'
+        )
+
+        # 地质描述(保留等级,泛化描述)
+        self.patterns["geology"] = re.compile(
+            r'(V|IV|III|II|I)[级阶]围岩'
+        )
+
+        # 行政地名(省市区县)
+        self.patterns["location"] = re.compile(
+            r'(四川省|成都市|北京市|上海市|重庆市|天津市|[\u4e00-\u9fa5]{2,6}(?:省|市|区|县))'
+        )
+
+    def process(self, content: str, registry: Any, strict_mode: bool = False) -> str:
+        """执行地理坐标脱敏"""
+        # 1. 计算坐标基准点(最小桩号)
+        base_point = self._find_base_point(content)
+        if base_point:
+            registry.set_coordinate_base(self._format_coordinate(base_point))
+
+        # 2. 桩号相对化处理
+        def replace_coordinate(match):
+            prefix = match.group(1)  # 如 D1K, K
+            km = int(match.group(2))  # 公里数
+            meter = match.group(3)  # 米数
+
+            original = match.group(0)
+
+            if base_point:
+                # 计算相对桩号
+                relative_km, relative_meter = self._calculate_relative(
+                    (prefix, km, float(meter) if '.' in meter else int(meter)),
+                    base_point
+                )
+                relative = f"K{relative_km}+{int(relative_meter):03d}"
+                registry.register_coordinate(original, relative)
+                return relative
+            else:
+                # 无法确定基准,泛化处理
+                registry.register_coordinate(original, f"[桩号脱敏]")
+                return "[桩号脱敏]"
+
+        content = self.patterns["coordinate"].sub(replace_coordinate, content)
+
+        # 3. 工程实体序列化处理
+        content = self.patterns["bridge"].sub(
+            lambda m: registry.register_entity(m.group(1), "桥"), content
+        )
+        content = self.patterns["tunnel"].sub(
+            lambda m: registry.register_entity(m.group(1), "隧道"), content
+        )
+        content = self.patterns["culvert"].sub(
+            lambda m: registry.register_entity(m.group(1), "涵洞"), content
+        )
+        content = self.patterns["river"].sub(
+            lambda m: registry.register_entity(m.group(1), "河"), content
+        )
+
+        # 4. 高程数据泛化
+        def replace_elevation(match):
+            elevation = float(match.group(1))
+            # 根据海拔范围泛化地形描述
+            if elevation > 3000:
+                return "[高原高山区]"
+            elif elevation > 1500:
+                return "[中山区]"
+            elif elevation > 500:
+                return "[低山丘陵区]"
+            else:
+                return "[平原区]"
+
+        content = self.patterns["elevation"].sub(replace_elevation, content)
+
+        # 5. 地质等级保留(V级围岩等)
+        # 地质等级属于技术参数,保留但泛化描述
+        def replace_geology(match):
+            level = match.group(1)
+            return f"[{level}级围岩区]"
+
+        if strict_mode:
+            content = self.patterns["geology"].sub(replace_geology, content)
+
+        # 6. 行政地名泛化
+        content = self.patterns["location"].sub("[某地区]", content)
+
+        return content
+
+    def _find_base_point(self, content: str) -> Optional[Tuple[str, int, float]]:
+        """从文本中提取最小桩号作为相对化基准点"""
+        coordinates = []
+
+        for match in self.patterns["coordinate"].finditer(content):
+            prefix = match.group(1)
+            km = int(match.group(2))
+            meter_str = match.group(3)
+            meter = float(meter_str) if '.' in meter_str else int(meter_str)
+            coordinates.append((prefix, km, meter))
+
+        if not coordinates:
+            return None
+
+        # 按公里数+米数排序,取最小值
+        coordinates.sort(key=lambda x: (x[1], x[2]))
+        return coordinates[0]
+
+    def _format_coordinate(self, coord: Tuple[str, int, float]) -> str:
+        """格式化坐标"""
+        prefix, km, meter = coord
+        if isinstance(meter, float):
+            return f"{prefix}{km}+{meter:.2f}"
+        return f"{prefix}{km}+{meter:03d}"
+
+    def _calculate_relative(self, coord: Tuple[str, int, float],
+                           base: Tuple[str, int, float]) -> Tuple[int, float]:
+        """计算相对桩号
+
+        Args:
+            coord: 当前坐标 (prefix, km, meter)
+            base: 基准坐标 (prefix, km, meter)
+
+        Returns:
+            (相对公里数, 相对米数)
+        """
+        coord_total_meters = coord[1] * 1000 + coord[2]
+        base_total_meters = base[1] * 1000 + base[2]
+
+        relative_meters = coord_total_meters - base_total_meters
+        relative_km = int(relative_meters // 1000)
+        relative_meter = int(relative_meters % 1000)
+
+        return relative_km, relative_meter

+ 117 - 0
core/construction_review/component/desensitize/processors/pii_processor.py

@@ -0,0 +1,117 @@
+"""
+PII (Personally Identifiable Information) 脱敏处理器
+
+处理对象:姓名、手机号、身份证号、证书编号、邮箱等个人身份信息
+"""
+
+import re
+from typing import Any, Optional
+from .base_processor import BaseDesensitizer
+
+
+class PIIDesensitizer(BaseDesensitizer):
+    """PII 脱敏处理器"""
+
+    def _compile_patterns(self):
+        """编译 PII 相关正则表达式"""
+        # 手机号:1[3-9]开头的11位数字
+        self.patterns["phone"] = re.compile(r'\b1[3-9]\d{9}\b')
+
+        # 身份证号:18位(15位已淘汰,暂不处理)
+        self.patterns["id_card"] = re.compile(r'\b\d{17}[\dXx]\b')
+
+        # 邮箱
+        self.patterns["email"] = re.compile(
+            r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
+        )
+
+        # 证书编号(特种作业资格证等):省份简称+字母+数字
+        # 如:京A202401001
+        self.patterns["cert"] = re.compile(
+            r'[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤川青藏琼宁][A-Z]\d{6,12}'
+        )
+
+        # 人名识别(中文姓名,2-4个字,位于特定上下文)
+        # 如"项目经理张三"、"负责人:李四"
+        self.patterns["person_with_role"] = re.compile(
+            r'(项目经理|负责人|安全员|技术负责人|班组长|施工员|质检员)[::\s]*([\u4e00-\u9fa5]{2,4})'
+        )
+
+        # 独立人名(更宽松,结合上下文判断)
+        # 位于句首或特定连接词后
+        self.patterns["person_standalone"] = re.compile(
+            r'(?:^|[,。;\n])([\u4e00-\u9fa5]{2,3})(?:负责|主持|编制|审核|审批)'
+        )
+
+        # 审批签字行:姓名 + 日期
+        self.patterns["signature"] = re.compile(
+            r'([\u4e00-\u9fa5]{2,4})\s*20\d{2}[年./]\d{1,2}[月./]\d{1,2}[日]?'
+        )
+
+    def process(self, content: str, registry: Any, strict_mode: bool = False) -> str:
+        """执行 PII 脱敏
+
+        Args:
+            content: 输入文本
+            registry: 实体注册表,用于维护人名映射一致性
+            strict_mode: 严格模式,确保所有匹配项都被处理
+        """
+        # 1. 手机号脱敏
+        content = self.patterns["phone"].sub('[手机号脱敏]', content)
+
+        # 2. 身份证号脱敏
+        content = self.patterns["id_card"].sub('[证件号脱敏]', content)
+
+        # 3. 证书编号脱敏
+        content = self.patterns["cert"].sub('[证件号脱敏]', content)
+
+        # 4. 邮箱脱敏
+        content = self.patterns["email"].sub('[邮箱脱敏]', content)
+
+        # 5. 带角色的人名脱敏(如"项目经理张三")
+        def replace_person_with_role(match):
+            role = match.group(1)
+            name = match.group(2)
+            # 映射角色到标准占位符类型
+            role_map = {
+                '项目经理': '项目经理',
+                '负责人': '负责人',
+                '安全员': '安全员',
+                '技术负责人': '技术负责人',
+                '班组长': '班组长',
+                '施工员': '施工员',
+                '质检员': '质检员'
+            }
+            placeholder = registry.register_person(name, role_map.get(role, '人员'))
+            return f"{role}:{placeholder}" if ':' in match.group(0) else f"{role}{placeholder}"
+
+        content = self.patterns["person_with_role"].sub(replace_person_with_role, content)
+
+        # 6. 审批签字脱敏
+        def replace_signature(match):
+            return '[已审核]'
+        content = self.patterns["signature"].sub(replace_signature, content)
+
+        # 7. 独立人名脱敏(严格模式下更激进)
+        if strict_mode:
+            def replace_standalone_person(match):
+                name = match.group(1)
+                placeholder = registry.register_person(name, "人员")
+                return match.group(0).replace(name, placeholder)
+            content = self.patterns["person_standalone"].sub(replace_standalone_person, content)
+
+        return content
+
+    def extract_persons(self, content: str) -> list:
+        """提取文本中的所有人名(用于预分析)"""
+        persons = []
+
+        # 提取带角色的人名
+        for match in self.patterns["person_with_role"].finditer(content):
+            persons.append({
+                "name": match.group(2),
+                "role": match.group(1),
+                "position": match.span()
+            })
+
+        return persons

+ 236 - 0
core/construction_review/component/desensitize/remapper.py

@@ -0,0 +1,236 @@
+"""
+审查结果逆向映射器
+
+将云端审查意见中的泛化占位符还原为真实工程术语,生成最终审查报告。
+
+根据 wlast.md 文档第6.3.3节设计
+"""
+
+import re
+import logging
+from typing import Dict, Any, Optional, Tuple
+from dataclasses import dataclass
+
+from .dict_manager import DictManager
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class RemapResult:
+    """映射结果"""
+    original_response: str  # 原始响应(脱敏状态)
+    remapped_response: str  # 还原后的响应(含真实术语)
+    mapping_summary: Dict[str, int]  # 映射统计
+    errors: list  # 错误信息
+
+
+class ResultRemapper:
+    """审查结果逆向映射器
+
+    功能:
+    1. 实体逆向替换(人名、企业名、工程实体)
+    2. 坐标逆向转换(相对桩号 -> 绝对桩号)
+    3. 保持审查意见语义不变
+
+    示例:
+      输入: "[项目经理A] 在 [1号特大桥] K0+500 处发现安全隐患"
+      输出: "张三 在 映雪特大桥 D1K86+779.91 处发现安全隐患"
+    """
+
+    def __init__(self):
+        """初始化映射器"""
+        self.dict_manager = DictManager()
+
+    async def remap(self, cloud_response: str, task_id: str,
+                    remap_coordinate: bool = True) -> RemapResult:
+        """将云端审查意见中的泛化词汇映射回真实工程术语
+
+        Args:
+            cloud_response: 云端审查返回的文本
+            task_id: 文档脱敏时返回的任务ID
+            remap_coordinate: 是否还原相对桩号
+
+        Returns:
+            RemapResult: 映射结果
+        """
+        errors = []
+
+        # 1. 加载脱敏字典
+        full_dict = await self.dict_manager.load(task_id)
+        if not full_dict:
+            error_msg = f"找不到脱敏字典: {task_id}"
+            logger.error(f"[ResultRemapper] {error_msg}")
+            return RemapResult(
+                original_response=cloud_response,
+                remapped_response=cloud_response,
+                mapping_summary={},
+                errors=[error_msg]
+            )
+
+        mapping_dict = full_dict.get("mappings", {})
+        result = cloud_response
+
+        # 2. 统计信息
+        summary = {
+            "person_mapped": 0,
+            "entity_mapped": 0,
+            "company_mapped": 0,
+            "coordinate_mapped": 0,
+            "total_replacements": 0
+        }
+
+        # 3. 人员逆向替换
+        persons = mapping_dict.get("persons", [])
+        for person in persons:
+            original = person.get("masked", "")
+            real_name = person.get("original", "")
+            if original and original in result:
+                count = result.count(original)
+                result = result.replace(original, real_name)
+                summary["person_mapped"] += count
+                summary["total_replacements"] += count
+                logger.debug(f"[ResultRemapper] 人员映射: {original} -> {real_name}")
+
+        # 4. 工程实体逆向替换
+        entities = mapping_dict.get("entities", [])
+        for entity in entities:
+            original = entity.get("masked", "")
+            real_name = entity.get("original", "")
+            if original and original in result:
+                count = result.count(original)
+                result = result.replace(original, real_name)
+                summary["entity_mapped"] += count
+                summary["total_replacements"] += count
+                logger.debug(f"[ResultRemapper] 实体映射: {original} -> {real_name}")
+
+        # 5. 企业/单位逆向替换
+        companies = mapping_dict.get("companies", [])
+        for company in companies:
+            original = company.get("masked", "")
+            real_name = company.get("original", "")
+            if original and original in result:
+                count = result.count(original)
+                result = result.replace(original, real_name)
+                summary["company_mapped"] += count
+                summary["total_replacements"] += count
+                logger.debug(f"[ResultRemapper] 企业映射: {original} -> {real_name}")
+
+        # 6. 坐标逆向转换
+        if remap_coordinate:
+            coord_info = mapping_dict.get("coordinates", {})
+            base_point = coord_info.get("base_point")
+            mappings = coord_info.get("mappings", {})
+
+            if base_point and mappings:
+                result, coord_count = self._remap_coordinates(result, mappings, base_point)
+                summary["coordinate_mapped"] = coord_count
+                summary["total_replacements"] += coord_count
+
+        # 7. 项目名称还原(如果有)
+        project_info = mapping_dict.get("project_name", {})
+        if project_info:
+            masked = project_info.get("masked", "")
+            original = project_info.get("original", "")
+            if masked and masked in result:
+                count = result.count(masked)
+                result = result.replace(masked, original)
+                summary["total_replacements"] += count
+
+        logger.info(f"[ResultRemapper] 映射完成: {summary}")
+
+        return RemapResult(
+            original_response=cloud_response,
+            remapped_response=result,
+            mapping_summary=summary,
+            errors=errors
+        )
+
+    def _remap_coordinates(self, text: str, mappings: Dict[str, str],
+                           base_point: str) -> Tuple[str, int]:
+        """将相对桩号还原为绝对桩号
+
+        Args:
+            text: 待处理的文本
+            mappings: 原始->相对的映射字典
+            base_point: 基准点
+
+        Returns:
+            (处理后的文本, 替换次数)
+        """
+        count = 0
+        result = text
+
+        # 匹配相对桩号格式 K{n}+{m}
+        relative_pattern = re.compile(r'K(\d+)\+(\d{3})')
+
+        def replace_coordinate(match):
+            nonlocal count
+            km = int(match.group(1))
+            meter = int(match.group(2))
+
+            # 查找对应的绝对坐标
+            for original, relative in mappings.items():
+                if relative == match.group(0):
+                    count += 1
+                    return original
+
+            # 如果没有找到精确匹配,尝试计算
+            # 这里简化处理,实际可能需要更复杂的坐标解析
+            return match.group(0)
+
+        result = relative_pattern.sub(replace_coordinate, result)
+        return result, count
+
+    async def remap_batch(self, responses: list, task_id: str,
+                          remap_coordinate: bool = True) -> list:
+        """批量映射多个响应
+
+        Args:
+            responses: 响应列表,每项为 (response_id, response_text)
+            task_id: 任务ID
+            remap_coordinate: 是否还原坐标
+
+        Returns:
+            映射后的结果列表
+        """
+        results = []
+        for response_id, response_text in responses:
+            try:
+                remap_result = await self.remap(response_text, task_id, remap_coordinate)
+                results.append({
+                    "response_id": response_id,
+                    "original": response_text,
+                    "remapped": remap_result.remapped_response,
+                    "summary": remap_result.mapping_summary
+                })
+            except Exception as e:
+                logger.error(f"[ResultRemapper] 批量映射失败 {response_id}: {e}")
+                results.append({
+                    "response_id": response_id,
+                    "original": response_text,
+                    "remapped": response_text,
+                    "error": str(e)
+                })
+        return results
+
+    def extract_masked_entities(self, text: str) -> Dict[str, list]:
+        """从文本中提取所有占位符实体
+
+        用于检查是否还有未映射的内容
+        """
+        patterns = {
+            "person": r'\[(项目经理|安全员|技术负责人|负责人|班组长|施工员|质检员|人员)[ABCDEFGHIJ]\]',
+            "entity": r'\[(\d+号)(特大桥|隧道|涵洞|河流|立交)\]',
+            "company": r'\[(建设单位|总承包单位|监理单位|设计单位|劳务分包单位|数字化实施方|某工程项目)\]',
+            "coordinate": r'K\d+\+\d{3}',
+            "masked": r'\[(手机号|证件号|邮箱|金额|单价|数量|桩号|合同编号|信用代码)脱敏\]',
+        }
+
+        result = {}
+        for entity_type, pattern in patterns.items():
+            matches = re.findall(pattern, text)
+            if matches:
+                result[entity_type] = matches
+
+        return result

+ 268 - 0
core/construction_review/component/desensitize/validator.py

@@ -0,0 +1,268 @@
+"""
+黑白名单校验器
+
+在文件发往云端前进行最终正则校验,任一黑名单模式命中则拦截上传并告警,确保零漏脱。
+"""
+
+import re
+import logging
+from dataclasses import dataclass, field
+from typing import List, Dict, Any, Optional
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class ValidationResult:
+    """校验结果"""
+    is_valid: bool  # 是否通过校验
+    violations: List[Dict[str, Any]] = field(default_factory=list)  # 违规项列表
+    whitelist_matches: int = 0  # 白名单匹配数
+    blacklist_matches: int = 0  # 黑名单匹配数
+
+
+class BlackWhiteListChecker:
+    """黑白名单校验器 - 上传前最终安全门
+
+    核心约束:
+    - 黑名单:绝对不允许出现的模式
+    - 白名单:脱敏后允许的合规格式(用于统计和辅助判断)
+
+    根据 wlast.md 文档第6.3.2节设计
+    """
+
+    # 黑名单:绝对不允许出现的模式(原始敏感数据格式)
+    BLACKLIST_PATTERNS = [
+        # 手机号
+        (r'\b1[3-9]\d{9}\b', 'phone', 'critical'),
+        # 身份证号
+        (r'\b\d{17}[\dXx]\b', 'id_card', 'critical'),
+        # 企业名称(含集团、局、有限公司等)
+        (r'[\u4e00-\u9fa5]{2,10}(集团|有限公司|局|处)', 'company', 'high'),
+        # 精确绝对桩号(含小数点精确值)
+        (r'[A-Z]?\d?[Kk]\d{2,5}\+\d{3,6}\.\d+', 'coordinate', 'high'),
+        # 证书编号
+        (r'[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤川青藏琼宁][A-Z]\d{6,12}', 'cert', 'high'),
+        # 邮箱
+        (r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', 'email', 'medium'),
+        # 统一社会信用代码
+        (r'[A-Z0-9]{18}', 'credit_code', 'medium'),
+    ]
+
+    # 白名单:脱敏后允许的合规格式
+    WHITELIST_PATTERNS = [
+        # 脱敏标记
+        (r'\[手机号脱敏\]', 'masked_phone'),
+        (r'\[证件号脱敏\]', 'masked_id'),
+        (r'\[邮箱脱敏\]', 'masked_email'),
+        (r'\[金额脱敏\]', 'masked_amount'),
+        (r'\[单价脱敏\]', 'masked_price'),
+        (r'\[数量脱敏\]', 'masked_quantity'),
+        (r'\[桩号脱敏\]', 'masked_coordinate'),
+        (r'\[合同编号脱敏\]', 'masked_contract'),
+        (r'\[信用代码脱敏\]', 'masked_credit'),
+        # 角色占位符
+        (r'\[项目经理[ABCDEFGHIJ]\]', 'role_pm'),
+        (r'\[安全员[ABCDEFGHIJ]\]', 'role_safety'),
+        (r'\[技术负责人[ABCDEFGHIJ]\]', 'role_tech'),
+        (r'\[负责人[ABCDEFGHIJ]\]', 'role_leader'),
+        (r'\[班组长[ABCDEFGHIJ]\]', 'role_foreman'),
+        (r'\[施工员[ABCDEFGHIJ]\]', 'role_worker'),
+        (r'\[质检员[ABCDEFGHIJ]\]', 'role_qc'),
+        (r'\[人员[ABCDEFGHIJ]\]', 'role_person'),
+        # 单位/角色词汇
+        (r'\[建设单位\]', 'company_constructor'),
+        (r'\[总承包单位\]', 'company_contractor'),
+        (r'\[监理单位\]', 'company_supervisor'),
+        (r'\[设计单位\]', 'company_designer'),
+        (r'\[劳务分包单位\]', 'company_subcontractor'),
+        (r'\[数字化实施方\]', 'company_tech'),
+        (r'\[某工程项目\]', 'project_masked'),
+        # 工程实体占位符
+        (r'\[\d+号特大桥\]', 'entity_bridge'),
+        (r'\[\d+号隧道\]', 'entity_tunnel'),
+        (r'\[\d+号涵洞\]', 'entity_culvert'),
+        (r'\[\d+号河流\]', 'entity_river'),
+        (r'\[\d+号立交\]', 'entity_interchange'),
+        # 相对桩号(标准化格式 K{数字}+{数字})
+        (r'K\d+\+\d{3}', 'relative_coordinate'),
+        # 地形描述
+        (r'\[高原高山区\]', 'terrain_high'),
+        (r'\[中山区\]', 'terrain_mid'),
+        (r'\[低山丘陵区\]', 'terrain_low'),
+        (r'\[平原区\]', 'terrain_plain'),
+        (r'\[某地区\]', 'location_masked'),
+        # 地质等级
+        (r'\[[IV]+级围岩区\]', 'geology_level'),
+        # 审批标记
+        (r'\[已审核\]', 'signature_masked'),
+    ]
+
+    def __init__(self):
+        """初始化校验器,编译正则表达式"""
+        self.blacklist = [
+            (re.compile(pattern), pattern_type, severity)
+            for pattern, pattern_type, severity in self.BLACKLIST_PATTERNS
+        ]
+        self.whitelist = [
+            (re.compile(pattern), pattern_type)
+            for pattern, pattern_type in self.WHITELIST_PATTERNS
+        ]
+
+    def validate(self, content: str, check_level: str = "strict") -> ValidationResult:
+        """执行黑白名单校验
+
+        Args:
+            content: 待校验的文本内容
+            check_level: 校验级别 ('strict' 严格 / 'normal' 普通)
+
+        Returns:
+            ValidationResult: 校验结果
+        """
+        violations = []
+        whitelist_matches = 0
+        blacklist_matches = 0
+
+        # 1. 黑名单校验(查找原始敏感数据残留)
+        for pattern, pattern_type, severity in self.blacklist:
+            matches = pattern.findall(content)
+            if matches:
+                blacklist_matches += len(matches)
+                # 去重,限制报告数量
+                unique_matches = list(dict.fromkeys(matches))[:5]
+
+                for match in unique_matches:
+                    # 查找位置
+                    positions = self._find_positions(content, pattern, match)
+
+                    violations.append({
+                        "type": pattern_type,
+                        "pattern": str(pattern.pattern),
+                        "match": match,
+                        "positions": positions,
+                        "severity": severity,
+                        "suggestion": self._get_suggestion(pattern_type),
+                        "check": "blacklist"
+                    })
+
+        # 2. 白名单统计(用于辅助判断脱敏覆盖率)
+        for pattern, pattern_type in self.whitelist:
+            matches = pattern.findall(content)
+            whitelist_matches += len(matches)
+
+        # 3. 严格模式额外检查
+        if check_level == "strict":
+            # 检查是否有疑似中文姓名残留(2-4字人名,前面有职务词)
+            strict_violations = self._strict_mode_check(content)
+            violations.extend(strict_violations)
+            blacklist_matches += len(strict_violations)
+
+        is_valid = len(violations) == 0
+
+        if not is_valid:
+            logger.warning(
+                f"[BlackWhiteListChecker] 校验失败: {len(violations)} 个违规项, "
+                f"黑名单匹配: {blacklist_matches}, 白名单匹配: {whitelist_matches}"
+            )
+            for v in violations[:3]:  # 只记录前3个
+                logger.warning(f"  - {v['type']}: {v['match'][:50]}...")
+
+        return ValidationResult(
+            is_valid=is_valid,
+            violations=violations,
+            whitelist_matches=whitelist_matches,
+            blacklist_matches=blacklist_matches
+        )
+
+    def _find_positions(self, content: str, pattern: re.Pattern,
+                        match_text: str) -> List[Dict[str, Any]]:
+        """查找匹配文本在内容中的位置"""
+        positions = []
+        for m in pattern.finditer(content):
+            if m.group(0) == match_text:
+                # 计算行号和列号
+                line_num = content[:m.start()].count('\n') + 1
+                line_start = content.rfind('\n', 0, m.start()) + 1
+                col_num = m.start() - line_start + 1
+
+                positions.append({
+                    "start": m.start(),
+                    "end": m.end(),
+                    "line": line_num,
+                    "column": col_num
+                })
+                if len(positions) >= 3:  # 限制位置数量
+                    break
+        return positions
+
+    def _get_suggestion(self, pattern_type: str) -> str:
+        """根据违规类型返回建议"""
+        suggestions = {
+            'phone': '替换为 [手机号脱敏]',
+            'id_card': '替换为 [证件号脱敏]',
+            'company': '替换为 [建设单位] 等标准角色词汇',
+            'coordinate': '转换为相对桩号格式 K{n}+{m}',
+            'cert': '替换为 [证件号脱敏]',
+            'email': '替换为 [邮箱脱敏]',
+            'credit_code': '替换为 [信用代码脱敏]',
+            'person_name': '替换为 [项目经理A] 等角色占位符',
+        }
+        return suggestions.get(pattern_type, '检查并脱敏处理')
+
+    def _strict_mode_check(self, content: str) -> List[Dict[str, Any]]:
+        """严格模式额外检查"""
+        violations = []
+
+        # 检查疑似中文姓名(带职务前缀)
+        person_pattern = re.compile(
+            r'(项目经理|负责人|安全员|技术负责人|班组长|施工员|质检员)[::\s]*'
+            r'([\u4e00-\u9fa5]{2,4})'
+        )
+
+        for match in person_pattern.finditer(content):
+            name = match.group(2)
+            # 简单启发式:排除常见非人名词
+            non_person_words = ['要求', '规定', '标准', '规范', '措施', '方案',
+                               '计划', '制度', '管理', '控制', '保证', '确保']
+            if name not in non_person_words:
+                line_num = content[:match.start()].count('\n') + 1
+
+                violations.append({
+                    "type": "person_name",
+                    "pattern": str(person_pattern.pattern),
+                    "match": match.group(0),
+                    "positions": [{"line": line_num, "column": match.start()}],
+                    "severity": "high",
+                    "suggestion": f"将'{name}'替换为[项目经理A]等角色占位符",
+                    "check": "strict_mode"
+                })
+
+        return violations
+
+    def quick_check(self, content: str) -> bool:
+        """快速检查,仅返回是否通过(用于高频场景)"""
+        for pattern, _, _ in self.blacklist:
+            if pattern.search(content):
+                return False
+        return True
+
+    def get_check_summary(self, content: str) -> Dict[str, Any]:
+        """获取校验摘要统计"""
+        result = self.validate(content)
+
+        # 按类型统计
+        severity_count = {"critical": 0, "high": 0, "medium": 0, "low": 0}
+        type_count = {}
+
+        for v in result.violations:
+            severity_count[v["severity"]] = severity_count.get(v["severity"], 0) + 1
+            type_count[v["type"]] = type_count.get(v["type"], 0) + 1
+
+        return {
+            "is_valid": result.is_valid,
+            "total_violations": len(result.violations),
+            "severity_distribution": severity_count,
+            "type_distribution": type_count,
+            "whitelist_matches": result.whitelist_matches,
+            "coverage_estimate": min(100, result.whitelist_matches * 5)  # 粗略估计
+        }

+ 124 - 69
core/construction_review/component/doc_worker/classification/chunk_classifier.py

@@ -8,14 +8,18 @@ from __future__ import annotations
 
 import asyncio
 import csv
+import json
+import re
 from collections import OrderedDict
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 
+from foundation.infrastructure.config.config import config_handler
+from foundation.observability.logger.loggering import review_logger as logger
+from foundation.ai.agent.generate.model_generate import generate_model_client
+
 from ..config.provider import default_config_provider
-from ..utils.llm_client import LLMClient
 from ..utils.prompt_loader import PromptLoader
-from foundation.observability.logger.loggering import review_logger as logger
 
 # 延迟导入新的三级分类器(避免循环导入)
 _LLM_CONTENT_CLASSIFIER = None
@@ -33,30 +37,50 @@ def _get_llm_content_classifier():
     return _LLM_CONTENT_CLASSIFIER
 
 
+def _extract_json(text: str) -> Optional[Dict[str, Any]]:
+    """从字符串中提取第一个有效 JSON 对象"""
+    for pattern in [r"```json\s*(\{.*?})\s*```", r"```\s*(\{.*?})\s*```"]:
+        m = re.search(pattern, text, re.DOTALL)
+        if m:
+            try:
+                return json.loads(m.group(1))
+            except json.JSONDecodeError:
+                pass
+    try:
+        for candidate in re.findall(r"(\{.*?\})", text, re.DOTALL):
+            try:
+                return json.loads(candidate)
+            except json.JSONDecodeError:
+                pass
+    except Exception:
+        pass
+    return None
+
+
 class ChunkClassifier:
     """内容块分类器(二级和三级分类)"""
 
     def __init__(self):
         """初始化分类器"""
         self._cfg = default_config_provider
-        
-        # 初始化LLM客户端和提示词加载器
-        self.llm_client = LLMClient(config_provider=self._cfg)
+        self._concurrency = int(config_handler.get("llm_keywords", "CONCURRENT_WORKERS", "20"))
+
+        # 初始化提示词加载器
         self.prompt_loader = PromptLoader()
-        
+
         # 加载CSV分类标准
         self._load_classification_standards()
 
     def _load_classification_standards(self):
         """从CSV文件加载二级和三级分类标准"""
         csv_file = Path(__file__).parent.parent / "config" / "StandardCategoryTable.csv"
-        
+
         if not csv_file.exists():
             raise FileNotFoundError(f"分类标准CSV文件不存在: {csv_file}")
-        
+
         # 结构: {first_code: {second_code: {second_cn, second_focus, third_items: [{third_code, third_cn, third_focus}]}}}
         self.classification_tree: Dict[str, Dict[str, Any]] = {}
-        
+
         with csv_file.open("r", encoding="utf-8-sig") as f:
             reader = csv.DictReader(f)
             for row in reader:
@@ -68,14 +92,14 @@ class ChunkClassifier:
                 third_code = (row.get("third_code") or "").strip()
                 third_cn = (row.get("third_name") or "").strip()
                 third_focus = (row.get("third_focus") or "").strip()
-                
+
                 if not first_code or not second_code:
                     continue
-                
+
                 # 初始化一级类别
                 if first_code not in self.classification_tree:
                     self.classification_tree[first_code] = {}
-                
+
                 # 初始化二级类别
                 if second_code not in self.classification_tree[first_code]:
                     self.classification_tree[first_code][second_code] = {
@@ -83,7 +107,7 @@ class ChunkClassifier:
                         "second_focus": second_focus,
                         "third_items": []
                     }
-                
+
                 # 添加三级类别(如果存在)
                 if third_code and third_cn:
                     self.classification_tree[first_code][second_code]["third_items"].append({
@@ -95,102 +119,143 @@ class ChunkClassifier:
     def _build_secondary_standards(self, first_category_code: str) -> tuple[str, dict]:
         """
         构建二级分类标准文本
-        
+
         返回:
             (标准文本, 索引映射字典)
         """
         if first_category_code not in self.classification_tree:
             return "(无二级分类标准)", {}
-        
+
         standards_lines = ["    0. 非标准项 - 不符合以下任何类别"]
         index_mapping = {0: ("非标准项", "non_standard")}
-        
+
         for idx, (second_code, second_data) in enumerate(self.classification_tree[first_category_code].items(), 1):
             second_cn = second_data["second_cn"]
             second_focus = second_data["second_focus"]
-            
+
             # 保存索引映射
             index_mapping[idx] = (second_cn, second_code)
-            
+
             if second_focus and second_focus != "NULL":
                 standards_lines.append(f"    {idx}. {second_cn} - 关注点:{second_focus}")
             else:
                 standards_lines.append(f"    {idx}. {second_cn}")
-        
+
         return "\n".join(standards_lines) if standards_lines else "(无二级分类标准)", index_mapping
 
     def _build_tertiary_standards(self, first_category_code: str, second_category_code: str) -> tuple[str, dict]:
         """
         构建三级分类标准文本
-        
+
         返回:
             (标准文本, 索引映射字典)
         """
         if first_category_code not in self.classification_tree:
             return "(无三级分类标准)", {}
-        
+
         if second_category_code not in self.classification_tree[first_category_code]:
             return "(无三级分类标准)", {}
-        
+
         third_items = self.classification_tree[first_category_code][second_category_code]["third_items"]
-        
+
         if not third_items:
             return "(无三级分类标准)", {}
-        
+
         standards_lines = ["    0. 非标准项 - 不符合以下任何类别"]
         index_mapping = {0: ("非标准项", "non_standard")}
-        
+
         for idx, third_item in enumerate(third_items, 1):
             third_cn = third_item["third_cn"]
             third_code = third_item["third_code"]
             third_focus = third_item["third_focus"]
-            
+
             # 保存索引映射
             index_mapping[idx] = (third_cn, third_code)
-            
+
             if third_focus and third_focus != "NULL":
                 standards_lines.append(f"    {idx}. {third_cn} - 关注点:{third_focus}")
             else:
                 standards_lines.append(f"    {idx}. {third_cn}")
-        
+
         return "\n".join(standards_lines), index_mapping
 
+    async def _call_llm_once(self, system_prompt: str, user_prompt: str) -> Optional[Dict[str, Any]]:
+        """
+        单次异步 LLM 调用(使用统一的 GenerateModelClient)
+
+        失败返回 None,由调用方决定处理逻辑
+        """
+        try:
+            content = await generate_model_client.get_model_generate_invoke(
+                trace_id="chunk_classifier",
+                system_prompt=system_prompt,
+                user_prompt=user_prompt,
+                model_name="qwen3_5_122b_a10b",  # 使用 122B 大模型提升分类准确性
+            )
+            result = _extract_json(content)
+            return result if result is not None else {"raw_content": content}
+        except Exception as e:
+            logger.error(f"[ChunkClassifier] LLM 调用失败: {e}")
+            return None
+
+    async def _batch_call_llm(
+        self,
+        requests: List[tuple],  # [(system_prompt, user_prompt), ...]
+    ) -> List[Optional[Dict[str, Any]]]:
+        """
+        并发批量调用 LLM(带信号量控制)
+
+        参数:
+            requests: 请求列表,每个元素是 (system_prompt, user_prompt) 元组
+
+        返回:
+            结果列表,与输入请求一一对应
+        """
+        semaphore = asyncio.Semaphore(self._concurrency)
+
+        async def bounded_call(system_prompt: str, user_prompt: str):
+            async with semaphore:
+                return await self._call_llm_once(system_prompt, user_prompt)
+
+        tasks = [bounded_call(sp, up) for sp, up in requests]
+        return list(await asyncio.gather(*tasks))
+
     async def classify_chunks_secondary_async(self, chunks: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
         """
         异步对chunks进行二级分类
-        
+
         参数:
             chunks: 已完成一级分类的chunk列表
-            
+
         返回:
             添加了二级分类字段的chunk列表
         """
         logger.info(f"正在对 {len(chunks)} 个内容块进行二级分类...")
-        
+
         # 准备LLM请求
         llm_requests = []
         valid_chunks = []
         index_mappings = []  # 保存每个请求对应的索引映射
-        
+
         for chunk in chunks:
             first_category_code = chunk.get("chapter_classification", "")
             chunk_title = chunk.get("section_label", "")
             hierarchy_path = " -> ".join(chunk.get("hierarchy_path", []))
             content = chunk.get("review_chunk_content", "")
             content_preview = content[:300] if content else ""
-            
+
             # 获取一级分类的中文名称
             first_category_cn = self._get_first_category_cn(first_category_code)
-            
+
             # 构建二级分类标准(返回标准文本和索引映射)
             secondary_standards, index_mapping = self._build_secondary_standards(first_category_code)
-            
+
             if secondary_standards == "(无二级分类标准)":
                 # 如果没有二级分类标准,跳过
                 chunk["secondary_category_cn"] = "无"
                 chunk["secondary_category_code"] = "none"
                 continue
-            
+
             # 渲染提示词
             prompt = self.prompt_loader.render(
                 "chunk_secondary_classification",
@@ -200,28 +265,23 @@ class ChunkClassifier:
                 content_preview=content_preview,
                 secondary_standards=secondary_standards
             )
-            
-            messages = [
-                {"role": "system", "content": prompt["system"]},
-                {"role": "user", "content": prompt["user"]}
-            ]
-            
-            llm_requests.append(messages)
+
+            llm_requests.append((prompt["system"], prompt["user"]))
             valid_chunks.append(chunk)
             index_mappings.append(index_mapping)
-        
+
         if not llm_requests:
             logger.info("所有内容块都没有二级分类标准,跳过二级分类")
             return chunks
-        
+
         # 批量异步调用LLM API
-        llm_results = await self.llm_client.batch_call_async(llm_requests)
-        
+        llm_results = await self._batch_call_llm(llm_requests)
+
         # 处理分类结果
         for chunk, llm_result, index_mapping in zip(valid_chunks, llm_results, index_mappings):
             if llm_result and isinstance(llm_result, dict):
                 category_index = llm_result.get("category_index")
-                
+
                 # 验证索引并映射到类别
                 if isinstance(category_index, int) and category_index in index_mapping:
                     secondary_cn, secondary_code = index_mapping[category_index]
@@ -235,7 +295,7 @@ class ChunkClassifier:
             else:
                 chunk["secondary_category_cn"] = "非标准项"
                 chunk["secondary_category_code"] = "non_standard"
-        
+
         logger.info("二级分类完成!")
         return chunks
 
@@ -317,12 +377,12 @@ class ChunkClassifier:
         每个chunk只能属于一个三级分类
         """
         logger.info(f"正在对 {len(chunks)} 个内容块进行三级分类...")
-        
+
         # 准备LLM请求
         llm_requests = []
         valid_chunks = []
         index_mappings = []  # 保存每个请求对应的索引映射
-        
+
         for chunk in chunks:
             first_category_code = chunk.get("chapter_classification", "")
             second_category_code = chunk.get("secondary_category_code", "")
@@ -330,19 +390,19 @@ class ChunkClassifier:
             chunk_title = chunk.get("section_label", "")
             content = chunk.get("review_chunk_content", "")
             content_preview = content[:300] if content else ""
-            
+
             # 获取一级分类的中文名称
             first_category_cn = self._get_first_category_cn(first_category_code)
-            
+
             # 构建三级分类标准(返回标准文本和索引映射)
             tertiary_standards, index_mapping = self._build_tertiary_standards(first_category_code, second_category_code)
-            
+
             if tertiary_standards == "(无三级分类标准)":
                 # 如果没有三级分类标准,跳过
                 chunk["tertiary_category_cn"] = "无"
                 chunk["tertiary_category_code"] = "none"
                 continue
-            
+
             # 渲染提示词
             prompt = self.prompt_loader.render(
                 "chunk_tertiary_classification",
@@ -352,28 +412,23 @@ class ChunkClassifier:
                 content_preview=content_preview,
                 tertiary_standards=tertiary_standards
             )
-            
-            messages = [
-                {"role": "system", "content": prompt["system"]},
-                {"role": "user", "content": prompt["user"]}
-            ]
-            
-            llm_requests.append(messages)
+
+            llm_requests.append((prompt["system"], prompt["user"]))
             valid_chunks.append(chunk)
             index_mappings.append(index_mapping)
-        
+
         if not llm_requests:
             logger.info("所有内容块都没有三级分类标准,跳过三级分类")
             return chunks
-        
+
         # 批量异步调用LLM API
-        llm_results = await self.llm_client.batch_call_async(llm_requests)
-        
+        llm_results = await self._batch_call_llm(llm_requests)
+
         # 处理分类结果
         for chunk, llm_result, index_mapping in zip(valid_chunks, llm_results, index_mappings):
             if llm_result and isinstance(llm_result, dict):
                 category_index = llm_result.get("category_index")
-                
+
                 # 验证索引并映射到类别
                 if isinstance(category_index, int) and category_index in index_mapping:
                     tertiary_cn, tertiary_code = index_mapping[category_index]
@@ -387,7 +442,7 @@ class ChunkClassifier:
             else:
                 chunk["tertiary_category_cn"] = "非标准项"
                 chunk["tertiary_category_code"] = "non_standard"
-        
+
         logger.info("三级分类完成!")
         return chunks
 
@@ -435,4 +490,4 @@ class ChunkClassifier:
                 classifier_config=classifier_config
             ))
         except RuntimeError:
-            raise RuntimeError("请使用 await classify_chunks_tertiary_async")
+            raise RuntimeError("请使用 await classify_chunks_tertiary_async")

+ 1 - 0
core/construction_review/component/doc_worker/classification/hierarchy_classifier.py

@@ -65,6 +65,7 @@ class HierarchyClassifier(IHierarchyClassifier):
                 trace_id="hierarchy_classifier",
                 system_prompt=system_prompt,
                 user_prompt=user_prompt,
+                model_name="qwen3_5_122b_a10b",  # 使用 122B 大模型提升分类准确性
             )
             result = _extract_json(content)
             return result if result is not None else {"raw_content": content}

+ 172 - 173
core/construction_review/component/doc_worker/config/StandardCategoryTable.csv

@@ -1,173 +1,172 @@
-first_code,first_name,second_code,second_name,second_focus,third_code,third_name,third_focus,keywords
-basis,编制依据,LawsAndRegulations,法律法规,NULL,NationalLawsAndRegulations,国家政府发布的法律法规与规章制度,国家级、法律、法规、规章、强制力、普遍适用、基础框架、顶层设计、行业准则、合规性、统一标准、权威性、强制性条文、基本要求。,国家法律;法规;规章;强制性条文;国务院令;住房城乡建设部;中华人民共和国
-basis,编制依据,LawsAndRegulations,法律法规,NULL,ProvincialLawsAndRegulationsOfProjectLocation,工程所在地省级政府发布的法律法规与规章制度,地方性、区域性、细化补充、因地制宜、执行细则、地方特色、适应性要求、属地管理、动态调整、配套政策、本地化实施。,省级;地方法规;省政府;地方规章;属地管理;四川省;省人民政府
-basis,编制依据,StandardsAndSpecifications,标准规范,NULL,IndustryStandards,行业标准,需符合国家/行业强制或推荐性标准(如GB/T、JTG等)、时效性强(需跟踪最新版)、覆盖全生命周期(设计→施工→运维)、是定义工程项目的最低技术要求、质量验收准则、安全红线。,GB/T;JTG;CJJ;行业标准;国家标准;推荐性标准;GB 5;TB;HJ;DL
-basis,编制依据,StandardsAndSpecifications,标准规范,NULL,TechnicalRegulations,技术规程,操作流程标准化、工艺参数、量化风险管控、节点设备使用、规范施工验收细则、人员资质要求、应急预案模板、数字化交付标准、BIM协同规则、绿色施工指标、强制性条款需100%执行(如安全操作规范)、包含可视化元素(图表、流程图、三维模型指引)、与BIM技术深度融合(4D进度模拟、5D成本控制)。,Q/CR;技术规程;操作规程;工艺规程;施工规范;企业标准
-basis,编制依据,DocumentSystems,文件制度,NULL,SichuanRoadAndBridgeDocumentSystemsAndmanagementProcedures,四川路桥下发的文件制度和管理程序文件,需包含集团级BIM实施标准、EPC总承包管理模式细则、强制要求下属单位接入集团统一的数字化管理平台(如PM系统)、设置科技创新成果转化的量化考核指标。,四川公路桥梁建设集团;四川公路桥梁;SCQJ;SCQJT;四川路桥总公司
-basis,编制依据,DocumentSystems,文件制度,NULL,RoadAndBridgeGroupDocumentSystemsAndmanagementProcedures,路桥集团下发的文件制度和管理程序文件,区域化管理细则、属地化政策适配、项目分级管控、应急响应机制、分包商信用评价、农民工工资支付、保障绿色施工区域、标准智慧工地建设指南、隐蔽工程验收流程、工程变更索赔指引。,路桥集团;路桥股份;四川路桥集团有限公司;四川路桥集团;四川路桥集团超危大;川路桥〔;路桥〔;集团下发;集团公司文件
-basis,编制依据,DocumentSystems,文件制度,NULL,BridgeCompanyDocumentSystemsAndmanagementProcedures,桥梁公司下发的文件制度和管理程序文件,专业技术标准、工艺工法创新、特种设备管理、试验检测规程、安全生产责任制、班前安全教育工程、质量三检制技术交底、标准化竣工资料归档、规范创优工程培育计划。,桥梁公司;桥梁分公司;桥梁工程公司;四川路桥桥梁公司;公司下发;公司管理制度
-basis,编制依据,DocumentSystems,文件制度,NULL,ConstructionUnitDocumentSystemsAndmanagementProcedures,建设单位下发的文件制度和管理程序文件,项目合同履约、工程款支付管理、设计变更审批、竣工验收标准、运营移交协议、保修期责任划分、参建单位考核评价、档案管理实施细则、信息沟通机制、争议解决程序。,建设单位;业主;甲方文件;建设方;建设单位下发;业主要求
-basis,编制依据,CompilationPrinciples,编制原则,NULL,NationalPoliciesStandardsAndDesignDocument,国家方针、政策、标准和设计文件,需动态更新(如新版《公路工程技术标准》实施后同步调整)、涉及多部门联合审查(发改委、住建部、生态环境部)。,国家方针;政策;设计文件;国家政策;设计规范;设计图纸;施工图
-basis,编制依据,CompilationPrinciples,编制原则,NULL,BasicConstructionProcedures,基本建设程序,法定程序刚性、四阶段闭环管理、审批链条完整性、阶段成果验收、逆程序风险管控、数字化流程跟踪。,基本建设程序;建设程序;立项;可研;审批手续;报批;批准
-basis,编制依据,CompilationPrinciples,编制原则,NULL,ProjectFunctionImplementation,工程项目功能实现,需求匹配度、全功能交付、使用效能保障、用户需求反演、系统集成测试、缺陷责任期追溯。,功能实现;使用功能;工程功能;满足功能;功能要求;使用要求
-basis,编制依据,CompilationPrinciples,编制原则,NULL,ContractPerformance,合同履约,契约精神、权利义务对等、支付节点刚性、变更索赔闭环、信用评价联动、争议解决机制,合同;履约;合同条款;合同要求;履行合同;合同约定;合同文件
-basis,编制依据,CompilationPrinciples,编制原则,NULL,ConstructionForceConcentration,施工力量集中,资源集约化、专业化、班组机械配置标准化、劳动力调度、算法工序穿插优化、进度风险预警,施工力量;资源集中;人员集中;机械集中;劳动力集中;人员集结
-basis,编制依据,CompilationPrinciples,编制原则,NULL,ProcessControl,工序控制,工序逻辑链、工艺标准化、交接检验制度化、关键线路动态监测、平行检验机制、隐蔽工程追溯,工序控制;工艺控制;工序衔接;施工工序;工序质量;过程管控
-basis,编制依据,CompilationScope,编制范围,NULL,ProjectCoverage,填写完整涵盖本方案包含的所有工程,项目范围完整性、子项划分颗粒度、工程量清单闭合性、专业界面划分、变更管理阈值、风险识别矩阵价、值工程分析、可施工性评审、全生命周期覆盖、涉密工程隔离。,编制范围;工程范围;施工范围;工程内容;本方案涵盖;方案范围
-basis,编制依据,CompilationScope,编制范围,NULL,ConstructionTechnology,部分工程可简要说明采取的施工工艺,工艺标准化体系、工法创新等级、质量控制关键点、机械化作业率、绿色施工技术、BIM协同设计、装配式构件应用、智能监测覆盖率、特殊环境适应性、非遗工艺传承。,施工工艺;施工方法;简要说明;施工技术;采用工艺
-overview,工程概况,DesignSummary,设计概况,关注名称类、具体数值类、量化单位类、技术标准需引用国家或行业规范、关注标准号数字类、各类年限、等级数值、量化单位。,ProjectIntroduction,工程简介,工程名称、工程类型(如住宅、桥梁、隧道)、建设规模(如建筑面积、长度、高度)、工程地址、投资额、工程性质(新建/改建/扩建)、设计单位、设计依据(如合同编号)、工程范围(如施工边界坐标)。,工程名称;工程概况;工程简介;工程性质;建设规模;工程类型;项目名称
-overview,工程概况,DesignSummary,设计概况,关注名称类、具体数值类、量化单位类、技术标准需引用国家或行业规范、关注标准号数字类、各类年限、等级数值、量化单位。,MainTechnicalStandards,主要技术标准,技术规范编号(如GB50021-2001《岩土工程勘察规范》)、设计使用年限、荷载标准(如活荷载、恒荷载)、抗震设防烈度、防火等级、环保标准(如绿色建筑认证)、材料标准(如混凝土强度等级C30)、施工工艺标准。,技术标准;设计使用年限;荷载等级;抗震设防;防火等级;混凝土强度;技术规范编号
-overview,工程概况,GeologyWeather,工程地质与水文气象,关注地名类、具体水位数值类、量化单位类、关注记录降雨量、水位、气温等数值量化单位类。,HydrologicalConditions,水文状况,地下水位(历史最高水位、当前水位)、含水层类型(孔隙水、裂隙水、承压水)、隔水层厚度、渗透系数(K值)、给水度、水质腐蚀性(如pH值、氯离子含量)、补径排条件(补给源、径流方向)、水文地质参数(如越流系数)、地下水动态监测数据。,水文;地下水;水位;地下水位;含水层;渗透系数;水文地质;补径排
-overview,工程概况,GeologyWeather,工程地质与水文气象,关注地名类、具体水位数值类、量化单位类、关注记录降雨量、水位、气温等数值量化单位类。,ClimaticConditions,气候条件,气候类型(如亚热带季风气候)、年平均气温、极端气温、年降水量、降雨强度(如小时最大降雨量)、蒸发量、湿度、主导风向、风速、冰冻期、台风频率、气象数据来源(如当地气象站)。,气候;气温;降水量;降雨;风速;气象;年降水;极端气温;蒸发量;冰冻期
-overview,工程概况,Surroundings,周边环境,周边环境需分析地形地貌名词、建筑分布名词及交通状况道路名词、相关量化单位、相关数字数值、各类专业性名词、建筑物名词、米或毫米等单位类。,PositionalRelationship,位置关系,相邻建(构)筑物(如住宅楼、桥梁)、距离(米)、方位角、山体坡度(°)、边坡稳定性(如安全系数)、河谷宽度、深基坑深度(米)、道路等级(如城市主干道)、高压电电压(kV)、地下管线类型(给水、燃气、电缆)、埋深(米)、保护距离。,位置关系;周边环境;相邻建筑;距离;管线;高压线;河道;构筑物;方位
-overview,工程概况,Surroundings,周边环境,周边环境需分析地形地貌名词、建筑分布名词及交通状况道路名词、相关量化单位、相关数字数值、各类专业性名词、建筑物名词、米或毫米等单位类。,StructuralDimensions,结构尺寸,建筑物高度/层数、山体海拔、边坡坡比(如1:1.5)、河谷断面尺寸、深基坑支护结构(如桩径、墙厚)、道路宽度(米)、高压电塔高度、地下管线段径(如DN100)。,建筑高度;层数;坡比;宽度;断面尺寸;坡度;桩径;墙厚;海拔
-overview,工程概况,LayoutPlan,施工平面及立面布置,场地名词、数值类、量化单位类、名称类、数值类、量化单位类。,TemporaryFacilityLocation,临时设施位置,拌和站坐标(如X、Y)、钢筋加工场距工程距离(米)、材料堆码区域面积(㎡)、临时占地红线、与工程最近点距离、场地利用率(%)。,拌和站;钢筋加工;材料堆放;临时设施;临时占地;材料堆场
-overview,工程概况,LayoutPlan,施工平面及立面布置,场地名词、数值类、量化单位类、名称类、数值类、量化单位类。,ConstructionWorkPlatform,施工作业平台与便道参数,作业平台尺寸(长×宽、米)、地面形式(混凝土硬化、砂石铺垫)、施工便道长度(米)、宽度(米)、路面形式(沥青、碎石)、最小弯曲半径(米)、坡度(%)、承载力(kPa)。,施工平台;作业平台;施工便道;便道长度;平台尺寸;通道宽度
-overview,工程概况,LayoutPlan,施工平面及立面布置,场地名词、数值类、量化单位类、名称类、数值类、量化单位类。,TemporaryWaterAndElectricityArrangement,临时水电布置,临时用水源(市政管网、地下水井)、管径(如DN100)、管线布置图(走向、节点距离)、供水压力(MPa)、变压器容量(kVA)、配电箱位置、线路走向(架空/埋地)、敷设方式(直埋、穿管)、电缆规格(如YJV22)。,临时用水;临时用电;供水管径;配电箱;水电布置;临时电缆
-overview,工程概况,RequirementsTech,施工要求和技术保证条件,名称类、日期类。名称类、量化单位类、数值类。,DurationTarget,工期目标,开工日期(年月日)、竣工日期、总工期(天)、关键节点工期(如基础完工期)、进度计划(甘特图编号)、工期保证措施(如资源调配)。,工期;开工日期;竣工日期;总工期;工期目标;关键节点;工期保证
-overview,工程概况,RequirementsTech,施工要求和技术保证条件,名称类、日期类。名称类、量化单位类、数值类。,QualityTarget,质量目标,质量目标(如合格率100%、鲁班奖)、合同条款编号、业主具体要求(如绿色施工认证)。,质量目标;合格率;质量标准;鲁班奖;优质工程;质量等级
-overview,工程概况,RequirementsTech,施工要求和技术保证条件,名称类、日期类。名称类、量化单位类、数值类。,SecurityGoals,安全目标,安全目标(如零死亡事故、隐患整改率)、合同条款编号、业主具体要求(如绿色施工认证)。,安全目标;零伤亡;安全事故;安全指标;安全生产目标
-overview,工程概况,RequirementsTech,施工要求和技术保证条件,名称类、日期类。名称类、量化单位类、数值类。,EnvironmentalGoals,环境目标,环境目标(如扬尘控制、噪声限值)、合同条款编号、业主具体要求(如绿色施工认证)。,环境目标;扬尘控制;噪声限值;绿色施工指标;文明施工目标
-overview,工程概况,RiskLevel,风险辨别与分级,危害隐患性词汇类、法规名称类、标准编号类。风险等级相关专业性词汇、属于、标准编号或其它编号、部门名称类、数值类、量化单位类。名称类、数值类。,DangerSource,危险源,地质灾害(地面沉降、滑坡)、水文风险(管涌、流砂)、施工风险(坍塌、触电)、环境风险(污染、火灾)、机械伤害(塔吊倾覆)、法律法规依据(如《安全生产法》)。,危险源;风险源;危害因素;安全隐患;事故隐患;危险因素;风险点
-overview,工程概况,RiskLevel,风险辨别与分级,危害隐患性词汇类、法规名称类、标准编号类。风险等级相关专业性词汇、属于、标准编号或其它编号、部门名称类、数值类、量化单位类。名称类、数值类。,ClassificationAndResponseMeasures,分级与应对措施,风险等级(重大、较大、一般)、分级标准(如LEC法)、应对措施(监测、支护、疏散)、应急预案编号、责任部门、监控频率。,风险等级;重大风险;较大风险;一般风险;应对措施;LEC;风险分级;风险评估
-overview,工程概况,Stakeholders,参建各方责任主体单位,名称类、数值类。,UnitType,单位类型,建设单位(业主)、设计单位、监理单位、施工单位(总包)、监控单位(监测机构)、专业分包单位(如桩基分包)、统一社会信用代码、项目负责人。,建设单位;设计单位;监理单位;施工单位;参建单位;总承包;社会信用代码
-plan,施工计划,Schedule,施工进度计划,工序作业时间分析、关键工程节点安排、施工进度计划横道图、进度控制点、里程碑事件、工序搭接关系、工期延误风险、进度调整机制、施工流水节拍、网络计划技术(如双代号网络图),ProcessOperationTimeAnalysis,工序作业时间分析,需明确各工序的持续时间、逻辑关系及资源需求、是进度计划的基础;,工序持续时间;工序时间分析;作业时间;持续时间;时间分析
-plan,施工计划,Schedule,施工进度计划,工序作业时间分析、关键工程节点安排、施工进度计划横道图、进度控制点、里程碑事件、工序搭接关系、工期延误风险、进度调整机制、施工流水节拍、网络计划技术(如双代号网络图),KeyProjectNodeArrangement,关键工程(工序)节点安排,主要工程(工序)节点的起止时间和持续时间、聚焦影响总工期的关键工序(如基础浇筑、主体封顶)、是进度控制的核心;,关键节点;里程碑;关键工序;主要节点;节点工期;关键线路
-plan,施工计划,Schedule,施工进度计划,工序作业时间分析、关键工程节点安排、施工进度计划横道图、进度控制点、里程碑事件、工序搭接关系、工期延误风险、进度调整机制、施工流水节拍、网络计划技术(如双代号网络图),ConstructionScheduleGanttChart,施工进度计划横道图等,直观展示进度安排的标准工具、需包含主要工序名称、起始时间、截止时间、持续时间、时间横道、责任人等信息;,横道图;进度横道;施工进度计划;甘特图;进度安排;时间计划
-plan,施工计划,Materials,施工材料计划,名称类、规格类、数值类、数值单位类,ListOfConstructionMeasuresAndMaterials,施工措施材料清单,排除主题工程材料、施工措施材料应包含如临时支撑结构材料、辅助施工材料、非主体工程的挡防措施、作业平台处理、模板配置、人员上下通道、安全防护措施和安全防护用品等、详细列出材料名称、规格、数量、重量及来源(如厂家、经销商)、是材料计划的核心输出,措施材料;临时支撑材料;辅助材料;安全防护材料;模板;脚手架材料
-plan,施工计划,Equipment,施工设备计划,设备名称类、规格类、数值类、数值单位类、时间日期类,MainConstructionMachineryAndEquipment,主要施工机械设备,列出关键设备(如起重吊装设备、混凝土浇筑设备、张拉压浆设备、人员升降设备、钻孔设备、隧道专用设备、监测监控设备、质量检查验收设备等)、明确其设备名称、规格(如额定功率)、数量及来源(自有或租赁);,施工机械;机械设备;起重机;泵车;钻机;吊装设备;设备清单;主要机械
-plan,施工计划,Workforce,劳动力计划,工种名称类、时间日期类、数值类、数值单位类,WorkforceAllocationPlan,劳动力配置计划,明确工种投入(如木工、钢筋工等)情况、按施工阶段(如基础、主体、装饰)列出各工种(如模板工、混凝土工)的投入数量、确保劳动力与进度匹配;,劳动力;工种投入;工人配置;班组;劳动力计划;人员配置计划
-plan,施工计划,Workforce,劳动力计划,工种名称类、时间日期类、数值类、数值单位类,StageLaborDemand,阶段劳动力需求,明确周/旬/月的劳动力峰值及低谷、优化人员调度;,劳动力峰值;阶段用工;月劳动力;劳动力需求;用工高峰
-plan,施工计划,SafetyCost,安全生产费用使用计划,名称类、金额类、货币数值类、货币单位类、不能将项目总的安全生产费用列入,CategoryOfSafetyProductionExpenses,安全生产费用类别,符合《企业安全生产费用提取和使用管理办法》(财资〔2022〕136号)及地方规定(如广东省水利厅2025年办法)、如安全防护设施、应急救援等;,安全费用类别;安全生产费用;安全投入;安全经费;财资〔2022〕136号
-plan,施工计划,SafetyCost,安全生产费用使用计划,名称类、金额类、货币数值类、货币单位类、不能将项目总的安全生产费用列入,SecurityFeeName,安全费用名称,具体(如“施工现场临时用电系统改造”“应急救援器材采购”)、避免模糊表述;,安全费用名称;安全防护费;应急救援费;临时用电改造;安全器材采购
-plan,施工计划,SafetyCost,安全生产费用使用计划,名称类、金额类、货币数值类、货币单位类、不能将项目总的安全生产费用列入,SingleInvestmentAmount,单项投入金额,明确每项费用的具体数值(如“临时防护栏杆采购:5万元”)、确保费用可量化;,单项金额;费用金额;万元;单项投入;单项安全费
-plan,施工计划,SafetyCost,安全生产费用使用计划,名称类、金额类、货币数值类、货币单位类、不能将项目总的安全生产费用列入,TotalSafetyProductionExpenses,安全生产费用总额,根据工程规模、风险等级计算、确保足额投入,安全费用总额;总金额;安全投入合计;安全费总计
-technology,施工工艺技术,MethodsOverview,主要施工方法概述,工艺名称类、施工专业词汇类、规格类、数值类、数值单位类,ConstructionTechnologySelection,施工工艺选择,需明确工程采用的核心工艺(如“现浇混凝土框架工艺”“装配式构件安装工艺”)、是施工方法概述的基础;,工艺选择;施工工艺;核心工艺;施工方法选择;工法
-technology,施工工艺技术,MethodsOverview,主要施工方法概述,工艺名称类、施工专业词汇类、规格类、数值类、数值单位类,MainConstructionMethods,主要施工方法,需概括各分部分项工程的关键做法(如“基础采用旋挖钻孔灌注桩施工”“主体采用铝模板体系施工”);,施工方法;主要施工方法;关键做法;灌注桩;现浇;装配式;施工工法
-technology,施工工艺技术,MethodsOverview,主要施工方法概述,工艺名称类、施工专业词汇类、规格类、数值类、数值单位类,TemplateConfigurationQuantity,模板配置数量,需根据施工进度和构件尺寸计算(如“柱模板配置20套”“梁模板配置15套”)、是模板管理的关键指标;,模板配置;模板数量;模板套数;模板配备
-technology,施工工艺技术,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,MaterialType,材料类型,需明确主要材料的类别(如“钢筋”“混凝土”“防水卷材”)、是技术参数的基础;,材料类型;钢筋;混凝土;防水材料;钢材;主要材料
-technology,施工工艺技术,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,MaterialSpecifications,材料规格,需细化材料的尺寸、型号(如“钢筋HRB400EΦ16”“混凝土C30P6”)、直接影响工程质量;,材料规格;型号;HRB400;C30;规格型号;材料尺寸
-technology,施工工艺技术,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,DeviceName,设备名称,需列出关键设备的全称(如“挖掘机”“塔式起重机”“混凝土泵车”)、是设备管理的核心;,设备名称;挖掘机;起重机;塔吊;泵车;钻机
-technology,施工工艺技术,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,DeviceModel,设备型号,需明确设备的规格型号(如“徐工XE200挖掘机”“中联重科TC6013塔式起重机”)、用于设备的采购和维护;,设备型号;型号规格;机械型号;设备规格
-technology,施工工艺技术,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,EquipmentManufacturingTime,设备出厂时间,需包含设备的出厂时间,出厂时间;出厂日期;生产日期;出厂年份
-technology,施工工艺技术,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,EquipmentPerformanceParameters,设备性能参数,需包含设备的额定功率、工作效率等(如“塔式起重机最大起重量8t”“混凝土泵车输送量60m³/h”)、是设备选型的依据;,性能参数;额定功率;最大起重量;输送量;工作效率
-technology,施工工艺技术,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,EquipmentWeight,设备自重,需记录设备的自身重量(如“塔式起重机自重50t”)、用于基础设计和运输规划。,设备自重;自重;重量;整机重量
-technology,施工工艺技术,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,MeasurementAndStakeout,测量放样,需明确测量的基准点、控制网设置(如“建立施工平面控制网”“放出建筑物轴线”)、是施工定位的关键;,测量放样;控制网;轴线;基准点;放线;测量基准;坐标
-technology,施工工艺技术,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,TemporaryWaterAndElectricityConsumption,临时水电用量,需计算施工期间的用水、用电量(如“临时用水管径DN100”“临时用电容量500kW”)、用于临时设施的设计;,临时用水量;临时用电量;用水量;用电量;水电用量
-technology,施工工艺技术,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,TheSiteIsFlat,场地平整,需明确平整的范围、标高(如“平整场地至设计标高±0.000”“压实度达到90%”)、是施工场地准备的基础;,场地平整;整平场地;标高;压实度;平整
-technology,施工工艺技术,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,Staffing,人员配置,需列出各岗位的人员数量(如“项目经理1名”“施工员2名”“钢筋工10名”)、是劳动力管理的核心;,人员配置;岗位人员;项目经理;施工员;人员配备;人员分工
-technology,施工工艺技术,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,EquipmentEntry,设备进场,需明确设备的进场时间、运输方式(如“塔式起重机进场时间2026年3月1日”“采用平板车运输”)、是设备准备的关键;,设备进场;进场时间;进场方式;进场日期;机械进场
-technology,施工工艺技术,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,SafetyProtectionFacilities,安全防护措施,需列出现场的安全设施(如“安全网”“防护栏杆”“消防栓”)、是安全保障的基础;,安全防护;安全网;防护栏杆;消防设施;安全设施;防护措施
-technology,施工工艺技术,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,PersonnelAccess,人员上下通道,需明确通道的形式、位置(如“楼梯间通道”“脚手架斜道”)、是人员通行的安全保障。,人员通道;上下通道;楼梯通道;斜道;人员上下;通道布置
-technology,施工工艺技术,Process,工艺流程,工序专业名称类、工程名称类、数值类、数值单位类,ConstructionProcess,施工工序,需列出工程的主要工序(如“地基处理→基础浇筑→主体结构→装饰装修”)、是工艺流程的核心;,施工工序;主要工序;工序流程;施工顺序;工艺步骤
-technology,施工工艺技术,Process,工艺流程,工序专业名称类、工程名称类、数值类、数值单位类,ProcessSequence,工艺顺序,需明确工序的先后逻辑(如“先绑扎钢筋后支模板”“先浇筑混凝土后养护”)、是流程执行的关键;,工艺顺序;施工顺序;先后顺序;工序逻辑;工序衔接关系
-technology,施工工艺技术,Process,工艺流程,工序专业名称类、工程名称类、数值类、数值单位类,ProcessFlowDiagram,工艺流程框图,需用图形展示工序的衔接(如“地基处理流程图”“主体结构施工流程图”)、是流程可视化的工具;,流程图;工艺流程图;施工流程框图;工序框图
-technology,施工工艺技术,Operations,施工方法及操作要求,施工流程名称类、数值类、数值单位类,ConstructionProcessOperations,施工工序描述操作,需详细描述各工序的操作步骤(如“钢筋绑扎操作流程”“模板安装操作步骤”)、是操作指导的核心;,操作步骤;操作流程;施工步骤;操作方法;操作要求
-technology,施工工艺技术,Operations,施工方法及操作要求,施工流程名称类、数值类、数值单位类,ConstructionPoints,施工要点,需明确工序的关键要求(如“钢筋绑扎需保证间距均匀”“模板安装需保证垂直度”)、是质量控制的关键;,施工要点;关键要求;质量关键;工艺要点;控制要点
-technology,施工工艺技术,Operations,施工方法及操作要求,施工流程名称类、数值类、数值单位类,FAQPrevention,常见问题及预防,需列出工序的常见问题及预防措施(如“预防混凝土蜂窝麻面:控制混凝土坍落度”“预防模板漏浆:密封模板缝隙”)、是风险防控的重点 ,常见问题;质量通病;预防措施;防治措施;常见缺陷;预防对策
-technology,施工工艺技术,Operations,施工方法及操作要求,施工流程名称类、数值类、数值单位类,ProblemSolvingMeasures,问题处理措施,需明确问题的解决方法(如“混凝土蜂窝处理:剔除松散部分、用高一等级混凝土填补”“模板漏浆处理:用海绵条密封缝隙”)、是问题解决的指南;,问题处理;处理措施;整改措施;修复方法;缺陷处理
-technology,施工工艺技术,Inspection,检查要求,材料进场检验、构配件抽查、工序检查内容、工序检查标准、隐蔽工程验收,MaterialInspectionUponArrival,材料进场质量检验,需明确材料的检验项目(如“钢筋的屈服强度检验”“混凝土的抗压强度检验”)、是材料质量控制的基础;,材料进场检验;进场检验;三证一检;复检;材料验收;进场质量检验
-technology,施工工艺技术,Inspection,检查要求,材料进场检验、构配件抽查、工序检查内容、工序检查标准、隐蔽工程验收,RandomInspectionOfIncomingComponents,构配件进场质量抽查,需明确构配件的抽查比例(如“构配件抽查比例为10%”“每批抽查5件”)、是构配件质量控制的手段;,构配件抽查;进场抽检;抽查比例;构件抽检
-technology,施工工艺技术,Inspection,检查要求,材料进场检验、构配件抽查、工序检查内容、工序检查标准、隐蔽工程验收,ProcessInspectionContent,工序检查内容,需列出各工序的检查项目(如“钢筋绑扎的检查内容:间距、数量、锚固长度”“模板安装的检查内容:垂直度、平整度、支撑稳定性”)、是工序检查的核心;,工序检查;检查内容;检查项目;工序检验;检查清单
-technology,施工工艺技术,Inspection,检查要求,材料进场检验、构配件抽查、工序检查内容、工序检查标准、隐蔽工程验收,ProcessInspectionStandards,工序检查标准,需明确检查的合格标准(如“钢筋间距允许偏差±10mm”“模板垂直度允许偏差5mm”)、是工序验收的依据;,检查标准;验收标准;允许偏差;检查合格;偏差限值
-safety,安全保证措施,SafetySystem,安全保证体系,流程体系类名词、标准文书类、标标准编号编码数字类,SafetyProductionAssuranceSystemFrameworkDiagram,安全生产保证体系框图,安全保证体系的视觉化呈现、需明确体系的核心要素(如组织机构、制度流程、资源保障)及逻辑关系、是公司标准体系的具象化载体;,安全保证体系;安全体系框图;安全管理体系框图;安全组织体系
-safety,安全保证措施,SafetySystem,安全保证体系,流程体系类名词、标准文书类、标标准编号编码数字类,CompanyStandardSystemReference,公司标准体系引用,强调安全保证体系需承接公司现有标准(如《公司安全生产管理办法》《公司安全技术规程》)、确保体系的一致性与延续性;,公司标准体系;公司安全管理办法;公司安全技术规程;标准体系引用
-safety,安全保证措施,Organization,组织保证措施,名词类、人名类、岗位名称类、制度名词类,SafetymanagementOrganization,安全管理组织机构,基于项目经理为组长的安全工作领导小组、关注岗位组织架构名称类、部门名称类、关系结构类名词;,安全管理组织;安全领导小组;安全管理机构;安全管理组织机构
-safety,安全保证措施,Organization,组织保证措施,名词类、人名类、岗位名称类、制度名词类,PersonnelSafetyResponsibilities,人员安全职责,关注岗位名称类、人名类、责任制度名词类、岗位职责名词类、安全制度名词类;,安全职责;人员安全责任;岗位安全;安全责任制
-safety,安全保证措施,TechMeasures,技术保证措施,施工专业名词类、工序名称类 、施工设备名称类、施工材料名称类、施工场地名称类、岗位名称类,OverallSecurityMeasures,总体安全措施,包含保证施工过程中主要工序的人员、材料、机械设备安全所采取的技术措施、以及材料运输、吊装、施工作业区域的临边、临空、洞口安全防护设施、安全母绳布置、人员上下(横向)通道布置等、是针对项目整体的安全技术规划(如“施工现场临时用电总体方案”“高空作业总体防护措施”)、需覆盖所有施工环节;,总体安全措施;临边防护;洞口防护;安全母绳;安全防护设施布置
-safety,安全保证措施,TechMeasures,技术保证措施,施工专业名词类、工序名称类 、施工设备名称类、施工材料名称类、施工场地名称类、岗位名称类,SafetyAssuranceMeasuresForKeyProcesses,主要工序安全措施,是针对关键工序的具体安全要求(如“深基坑开挖支护措施”“模板安装拆除安全规范”)、需明确每一步操作的安全要点;,主要工序安全;关键工序安全;工序安全措施;专项安全措施
-safety,安全保证措施,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,MonitoringOrganization,监测组织机构,监测监控的责任主体、需明确监测人员的资质(如注册安全工程师、监测技术员)及职责(如数据采集、分析、报告)、确保监测工作的专业性;,监测机构;监测人员;监测责任;监测组织机构;监测负责人
-safety,安全保证措施,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,MonitoringRange,监测范围,需覆盖施工区域内的所有风险点(如深基坑周边、高支模体系、临时用电线路)、避免遗漏;,监测范围;监测区域;监测覆盖范围;监测对象
-safety,安全保证措施,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,MonitoringItems,监测项目,需明确监测的具体内容(如深基坑的水平位移、高支模的立杆轴力、临时用电的电压电流)、是监测的核心;,监测项目;监测内容;水平位移;沉降监测;监测指标
-safety,安全保证措施,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,MonitoringPointSettings,监测点设置,需根据风险点的分布确定(如深基坑每10米设置一个位移监测点)、需符合《建筑基坑支护技术规程》(JGJ 120-2012)等行业标准;,监测点;测点布置;监测布点;测点位置
-safety,安全保证措施,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,MonitoringInstrumentsAndEquipment,监测仪器设备,需明确仪器的名称(如全站仪、测斜仪、应力传感器)、型号(如徕卡TS60全站仪)及精度(如0.5秒级)、确保数据的准确性;,监测仪器;全站仪;测斜仪;应力传感器;监测设备;仪器型号
-safety,安全保证措施,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,MonitoringMethods,监测方法,需明确数据采集的方式(如人工读数、自动采集)、及数据处理方法(如统计分析、趋势预测)、是监测的关键环节;,监测方法;数据采集;人工读数;自动采集;数据处理
-safety,安全保证措施,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,MonitoringFrequency,监测频率,需明确监测频率、(如深基坑每天一次、高支模每周两次)。,监测频率;监测周期;每天一次;每周两次;监测时间间隔
-safety,安全保证措施,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,WarningValuesAndControlValues,预警值及控制值,需根据设计文件及行业标准确定(如深基坑水平位移预警值为30mm、控制值为50mm)、是判断风险的重要依据;,预警值;控制值;报警值;监测阈值;预警指标;预警控制
-safety,安全保证措施,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,InformationFeedbackMechanism,信息反馈机制,监测数据的传递流程(如监测人员→项目安全部→项目经理→公司总部)、需明确反馈的时间要求(如实时反馈、每日汇总)、确保风险及时处理。,信息反馈;监测报告;数据反馈;反馈流程;监测信息传递
-safety,安全保证措施,Emergency,应急处置措施,事故名称类、救援器材类、机构名称类、数字类、数值单位类。,EmergencyProcedures,应急处置程序,应采用公司标准应急处理程序图(附件16)、应急响应的步骤流程(如“事故报告→现场警戒→人员疏散→救援实施→善后处理”)、需明确每一步的责任部门及时间要求、确保响应及时;,应急程序;应急流程;应急响应;应急处置程序;处置步骤
-safety,安全保证措施,Emergency,应急处置措施,事故名称类、救援器材类、机构名称类、数字类、数值单位类。,EmergencyMeasures,应急处置措施,应根据方案实施过程潜在的危险源、判断出可能造成的伤害类型、制定出有针对性的救援措施、保证在事故发生后伤者能得到有效和及时的救治、如触电、有毒有害气体中毒、高处坠落、物体打击、施工现场及驻地火灾等事故、针对不同类型事故的具体处理方法(如“火灾事故使用干粉灭火器扑救”“坍塌事故使用千斤顶支撑”)、需明确操作要点(如灭火器的使用方法、千斤顶的支撑位置)、确保救援有效;,应急措施;救援措施;应急处置;紧急处置;事故救援;触电;中毒;坍塌
-safety,安全保证措施,Emergency,应急处置措施,事故名称类、救援器材类、机构名称类、数字类、数值单位类。,EmergencySuppliesAndEquipmentSupport,应急物资及设备保障,应根据事故的不同、以表格的形式说明救援的物品名称、规格型号、单位、数量、监管人、联系电话等内容、应急处置的物质基础、需明确物资的名称(如灭火器、急救箱、千斤顶)、数量(如每100平方米配备2个灭火器)、存放位置(如施工现场入口处)及维护要求(如每月检查一次灭火器压力)、确保物资随时可用;,应急物资;应急设备;救援器材;灭火器;急救箱;应急保障
-safety,安全保证措施,Emergency,应急处置措施,事故名称类、救援器材类、机构名称类、数字类、数值单位类。,TrafficmanagementAndMedicalRescue,交通疏导与医疗救援,应以表格的形式明确施工工点附近的医疗救援机构名称、联系电话、距离等、并附应急救援线路图;,医疗救援;交通疏导;救援线路;医院联系;急救电话;应急救援路线
-safety,安全保证措施,Emergency,应急处置措施,事故名称类、救援器材类、机构名称类、数字类、数值单位类。,PostDisposal,后期处置,包括善后处理、调查与评估、恢复生产等三个方面、事故后的恢复工作、需明确善后处理(如伤亡人员家属安抚、财产损失统计)、事故调查(如原因分析、责任认定评估)及整改措施(如完善安全制度、加强培训)、避免事故重复发生;,后期处置;善后处理;事故调查;恢复生产;善后工作;事故评估
-quality,质量保证措施,QualitySystem,质量保证体系,组织机构名称类、岗位名称类、岗位职责词汇类。,QualityAssuranceSystemFramework,质量管理组织机构,应引用公司标准体系框图、质量体系的视觉化呈现、需明确体系的核心要素(如组织机构、制度流程、资源保障)及逻辑关系、是体系落地的框架基础;,质量保证体系;质量体系框图;质量管理体系;质量管理组织机构
-quality,质量保证措施,QualitySystem,质量保证体系,组织机构名称类、岗位名称类、岗位职责词汇类。,QualitymanagementOrganization,人员职责,基于项目经理为组长的工作领导小组、小组中包括项目经理、项目总工、质量总监、工程部门、质检部门、专业分包单位(协作队伍)项目负责人和项目技术负责人等、需明确层级(如公司级、项目级、班组级)及组成部门(如质量部、工程部、技术部)、形成“横向到边、纵向到底”的管理网络;,质量管理组织;质量领导小组;质检人员;质量总监;质量体系组织
-quality,质量保证措施,QualitySystem,质量保证体系,组织机构名称类、岗位名称类、岗位职责词汇类。,PersonnelResponsibilities,质量保证体系框图,需细化每个岗位的质量责任(如项目经理的“第一责任人”职责、质量员的“现场监督”职责)、避免职责模糊导致的管理漏洞;,质量职责;质量责任制;岗位质量责任;质量保证体系框图
-quality,质量保证措施,QualityGoals,质量目标,目标标准词汇类、合同条款类、具体工程名称类、量化数值类、数值单位类。,DecompositionOfQualityObjectives,质量目标分解,根据施工合同和业主要求填写、需将总目标拆解为分部(基础、主体、装饰)、分项工程的具体目标(如“主体结构混凝土强度合格率100%”)、是目标落地的关键;,质量目标分解;分项质量;质量指标;质量目标分项
-quality,质量保证措施,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,OverallPlanForEngineeringExcellence,工程创优总体计划,需明确创优的阶段目标(如“基础工程创优”“主体工程创优”)及关键节点、是创优工作的路线图;,创优计划;精品工程;优质工程;工程创优;创优目标
-quality,质量保证措施,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,TechnicalPreparation,技术准备,需涵盖BIM技术应用、施工方案优化等、为创优提供技术支撑;,技术准备;BIM技术;新技术应用;技术支撑;技术创新
-quality,质量保证措施,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,ProcessControl,过程控制,需聚焦关键工序(如“大体积混凝土浇筑”“钢结构安装”)、打造精品工序;,过程控制;关键工序精品;精品工序;质量控制过程;工程创优过程
-quality,质量保证措施,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,DetailedTreatment,细部处理,需优化节点做法(如“墙面抹灰阴阳角顺直”“防水卷材搭接严密”)、提升工程观感质量;,细部处理;节点优化;节点做法;细部质量;细部工艺
-quality,质量保证措施,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,NewTechnologyPromotion,新技术推广,需应用“四新技术”(新技术、新材料、新工艺、新设备)、提升创优的技术含量;,四新技术;新技术推广;新工艺;新材料;新设备;技术创新应用
-quality,质量保证措施,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,PreparationOfApplicationMaterials,申报资料编制,需整理创优所需的资料(如工程质量报告、技术创新成果)、是创优申报的核心材料;,申报资料;创优申报;工程质量报告;申报材料
-quality,质量保证措施,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,EngineeringDataArchiving,工程资料归档,需确保资料真实、完整、符合创优评审要求。,工程资料归档;档案管理;竣工资料;资料归档
-quality,质量保证措施,QualityControl,质量控制程序与具体措施,原材料进场检验(三证一检)、实体工程质量验收(分项/分部工程验收)、质量通病防治(墙面空鼓/屋面渗漏)、季节性施工质量控制(冬期混凝土保温/雨期防水)、工序质量控制点、质量检查程序(自检/互检/专检)、质量问题整改(闭环管理),RawMaterialInspection,原材料进场检验,需执行“三证一检”(合格证、质检报告、生产许可证+进场复检)、确保材料质量;,原材料进场;三证一检;材料检验;复检报告;进场材料质量
-quality,质量保证措施,QualityControl,质量控制程序与具体措施,原材料进场检验(三证一检)、实体工程质量验收(分项/分部工程验收)、质量通病防治(墙面空鼓/屋面渗漏)、季节性施工质量控制(冬期混凝土保温/雨期防水)、工序质量控制点、质量检查程序(自检/互检/专检)、质量问题整改(闭环管理),PhysicalProjectQualityAcceptance,实体工程质量验收,需按分项(如“钢筋绑扎”)、分部工程(如“基础工程”)进行验收、符合规范要求;,实体验收;分项验收;分部验收;实体工程验收;工程质量验收
-quality,质量保证措施,QualityControl,质量控制程序与具体措施,原材料进场检验(三证一检)、实体工程质量验收(分项/分部工程验收)、质量通病防治(墙面空鼓/屋面渗漏)、季节性施工质量控制(冬期混凝土保温/雨期防水)、工序质量控制点、质量检查程序(自检/互检/专检)、质量问题整改(闭环管理),PreventionAndControlOfCommonQualityDefectsInProcesses,工序质量通病防治,需针对常见问题(如“墙面空鼓”“屋面渗漏”)制定专项措施(如“抹灰前基层凿毛”“防水附加层施工”)、减少质量缺陷;,质量通病;空鼓;渗漏;裂缝;蜂窝麻面;防治措施;通病防治
-quality,质量保证措施,QualityControl,质量控制程序与具体措施,原材料进场检验(三证一检)、实体工程质量验收(分项/分部工程验收)、质量通病防治(墙面空鼓/屋面渗漏)、季节性施工质量控制(冬期混凝土保温/雨期防水)、工序质量控制点、质量检查程序(自检/互检/专检)、质量问题整改(闭环管理),SeasonalConstructionQualityAssuranceMeasures,季节性施工质量保证措施,需针对冬期(混凝土保温)、雨期(防水加强)、高温(混凝土保湿)制定专项措施、确保施工质量;,季节性施工;冬期施工;雨期施工;高温施工;夏季施工;冬季混凝土
-environment,环境保证措施,EnvSystem,环境保证体系,环境保证体系框图、公司标准体系引用,BlockDiagramOfEnvironmentalAssuranceSystem,环境保证体系框图,环境保证体系的视觉化呈现、需明确体系的核心要素(如组织机构、制度流程、资源保障)及逻辑关系、是公司标准体系的具象化载体;,环境保证体系;环境管理体系框图;环境保证体系框图
-environment,环境保证措施,EnvSystem,环境保证体系,环境保证体系框图、公司标准体系引用,CompanyStandardSystemReference,公司标准体系引用,应引用公司标准体系框图、强调环境保证体系需承接公司现有标准(如《公司环境管理体系手册》《公司环境保护管理办法》)、确保体系的一致性与延续性;,环境管理体系;环境保护管理办法;公司环境标准;环境体系引用
-environment,环境保证措施,EnvOrg,环境保护组织机构,环境保护组织架构、管理人员姓名、管理人员职务、管理人员职责、环境管理岗位责任、责任考核机制、环境管理职责分工、环境管理人员资质、环境管理沟通机制,EnvironmentalAssuranceSystemFramework,环境保护组织架构,包含管理人员姓名、职务、职责、环境管理的责任主体、基于项目经理为组长的工作领导小组、小组中包括项目经理、项目副经理、项目总工、工程部门、质检部门、安全环保部门、专业分包单位(协作队伍)项目负责人和项目技术负责人等、需明确机构的层级(如公司级、项目级、班组级)及组成部门(如环境部、工程部、技术部)、形成“横向到边、纵向到底”的管理网络;,环境保护组织;环境管理机构;环境管理组织架构;环境领导小组
-environment,环境保证措施,EnvOrg,环境保护组织机构,环境保护组织架构、管理人员姓名、管理人员职务、管理人员职责、环境管理岗位责任、责任考核机制、环境管理职责分工、环境管理人员资质、环境管理沟通机制,EnvironmentalmanagementJobResponsibilities,环境管理岗位责任,需明确各岗位的环境责任(如项目经理的“环境第一责任人”职责、环境专员的“现场巡查”职责)、是组织保证的基石;,环境职责;环保岗位;环境管理责任;环保责任
-environment,环境保证措施,EnvOrg,环境保护组织机构,环境保护组织架构、管理人员姓名、管理人员职务、管理人员职责、环境管理岗位责任、责任考核机制、环境管理职责分工、环境管理人员资质、环境管理沟通机制,ResponsibilityAssessmentMechanism,责任考核机制,对环境职责履行情况的评价方式(如月度考核、年度评优)、需与环境绩效挂钩(如奖金发放、晋升晋级)、强化责任意识。,考核机制;责任考核;环境考核;月度考核;绩效考核;考核评价
-environment,环境保证措施,EnvProtection,环境保护及文明施工措施,办公生活区环境卫生管理、生活垃圾分类处置、卫生防疫措施、饮用水安全保障、施工区域水土流失防治、截排水系统设计、边坡防护工程、沉沙池设置、噪声排放监测、低噪声设备选型、隔音屏障设置、施工时间管控、污水沉淀处理、油污隔离措施、废水检测达标、扬尘控制、废气净化处理、清洁能源使用、裸土覆盖、车辆冲洗设施。,EnvironmentalSanitationGuaranteeMeasuresForOfficeAndLivingAreas,办公生活区环境卫生保证措施,需明确责任分工(如保洁人员配置、卫生区域划分)及管理流程(如每日清扫、每周检查)、确保环境整洁;,环境卫生;生活区;办公区;生活垃圾;卫生防疫;饮用水
-environment,环境保证措施,EnvProtection,环境保护及文明施工措施,办公生活区环境卫生管理、生活垃圾分类处置、卫生防疫措施、饮用水安全保障、施工区域水土流失防治、截排水系统设计、边坡防护工程、沉沙池设置、噪声排放监测、低噪声设备选型、隔音屏障设置、施工时间管控、污水沉淀处理、油污隔离措施、废水检测达标、扬尘控制、废气净化处理、清洁能源使用、裸土覆盖、车辆冲洗设施。,SoilAndWaterConservationMeasuresInTheConstructionArea,施工区域水土流失防治措施,需通过“截(截水沟)、排(排水沟)、拦(拦挡坝)、护(边坡防护)”综合措施、减少雨水对裸露土壤的冲刷;,水土流失;边坡防护;截排水;沉沙池;水土保持;拦挡
-environment,环境保证措施,EnvProtection,环境保护及文明施工措施,办公生活区环境卫生管理、生活垃圾分类处置、卫生防疫措施、饮用水安全保障、施工区域水土流失防治、截排水系统设计、边坡防护工程、沉沙池设置、噪声排放监测、低噪声设备选型、隔音屏障设置、施工时间管控、污水沉淀处理、油污隔离措施、废水检测达标、扬尘控制、废气净化处理、清洁能源使用、裸土覆盖、车辆冲洗设施。,NoiseEmissionMonitoring,噪声排放监测,需按照《建筑施工场界环境噪声排放标准》(GB 12523-2011)要求、在施工现场边界设置监测点、每日监测1次、记录等效声级(Leq)和最大声级(Lmax);,噪声监测;噪声排放;等效声级;噪音控制;GB 12523;声级
-environment,环境保证措施,EnvProtection,环境保护及文明施工措施,办公生活区环境卫生管理、生活垃圾分类处置、卫生防疫措施、饮用水安全保障、施工区域水土流失防治、截排水系统设计、边坡防护工程、沉沙池设置、噪声排放监测、低噪声设备选型、隔音屏障设置、施工时间管控、污水沉淀处理、油污隔离措施、废水检测达标、扬尘控制、废气净化处理、清洁能源使用、裸土覆盖、车辆冲洗设施。,WaterPollutionPreventionAndControlMeasures,水污染防治措施,需在搅拌机、运输车清洗处设置沉淀池、施工废水经沉淀后回用(如洒水降尘)、避免直接排入市政管网;,水污染;废水处理;污水沉淀;沉淀池;废水排放;污水处理
-environment,环境保证措施,EnvProtection,环境保护及文明施工措施,办公生活区环境卫生管理、生活垃圾分类处置、卫生防疫措施、饮用水安全保障、施工区域水土流失防治、截排水系统设计、边坡防护工程、沉沙池设置、噪声排放监测、低噪声设备选型、隔音屏障设置、施工时间管控、污水沉淀处理、油污隔离措施、废水检测达标、扬尘控制、废气净化处理、清洁能源使用、裸土覆盖、车辆冲洗设施。,AirPollutionPreventionAndControlMeasures,大气污染防治措施,需采取“洒水降尘、裸土覆盖、车辆冲洗、道路硬化”等措施、确保施工现场目测扬尘高度小于1.5m(土方作业阶段)或0.5m(结构施工阶段)。,扬尘;大气污染;尘土;车辆冲洗;裸土覆盖;废气排放;扬尘控制
-management,施工管理及作业人员配备与分工,Managers,施工管理人员,施工管理人员名单、岗位职责清单、管理职责分解、管理权限划分、管理流程衔接。,ConstructionmanagementPersonnelList,施工管理人员名单,需以表格形式明确项目管理人员(如项目经理、项目书记、项目总工、项目副经理、质量总监、安全总监、各职能部门、主管技术员、测量员、质检员、以及专业分包单位(协作队伍)项目负责人和项目技术负责人等)的姓名、岗位及联系方式、是人员管理的基础台账;,管理人员名单;项目经理;项目总工;施工管理人员;人员信息表
-management,施工管理及作业人员配备与分工,Managers,施工管理人员,施工管理人员名单、岗位职责清单、管理职责分解、管理权限划分、管理流程衔接。,JobResponsibilitiesList,岗位职责清单,需细化每个管理岗位的职责(如项目经理的“项目全面管理”职责、技术负责人的“技术方案审核”职责)、避免职责模糊导致的管理漏洞;,岗位职责;职责清单;管理岗位职责;岗位分工;职责分解
-management,施工管理及作业人员配备与分工,SafetyStaff,专职安全生产管理人员,专职安全生产管理人员名单、安全生产考核合格证书、证书编号、证书有效期、安全岗位职责、安全责任追究。,ListOfFullTimeSafetyProductionmanagementPersonnel,专职安全生产管理人员名单,需以表格形式明确专职安全员(如项目安全总监、专职安全员)的姓名、岗位及联系方式、是安全管理的核心台账;,专职安全员;专职安全管理人员;安全员名单;安全管理人员名单
-management,施工管理及作业人员配备与分工,SafetyStaff,专职安全生产管理人员,专职安全生产管理人员名单、安全生产考核合格证书、证书编号、证书有效期、安全岗位职责、安全责任追究。,SafetyProductionQualificationCertificate,安全生产考核合格证书,需明确证书类型(如“建筑施工企业专职安全生产管理人员证书”)、编号及有效期、是上岗的必备资质;,安全生产考核合格证;安全证书;证书编号;证书有效期;安全资质
-management,施工管理及作业人员配备与分工,SafetyStaff,专职安全生产管理人员,专职安全生产管理人员名单、安全生产考核合格证书、证书编号、证书有效期、安全岗位职责、安全责任追究。,SafetyProductionmanagementJobResponsibilities,安全生产管理岗位职责,需细化专职安全员的职责(如“现场安全检查”“隐患整改监督”“安全培训实施”)、确保安全管理工作落地;,安全生产管理职责;专职安全员职责;安全管理岗位职责
-management,施工管理及作业人员配备与分工,SpecialWorkers,特种作业人员,特种作业人员名单、特种作业操作资格证书、证书编号、证书有效期、特种作业工种、岗位职责、证书延期复核、违章作业记录。,ListOfSecialOperationsPersonnel,特种作业人员名单,需以表格形式明确特种作业人员(如建筑电工、建筑架子工、建筑起重机械司机等)的姓名、工种及联系方式、是特种作业管理的基础台账;,特种作业人员;特种作业;电工;架子工;起重机司机;焊工;特种人员名单
-management,施工管理及作业人员配备与分工,SpecialWorkers,特种作业人员,特种作业人员名单、特种作业操作资格证书、证书编号、证书有效期、特种作业工种、岗位职责、证书延期复核、违章作业记录。,SpecialOperationsQualificationCertificate,特种作业操作资格证书,需明确证书类型(如“建筑施工特种作业操作资格证书”)、编号及有效期、是上岗的必备资质;,特种作业资格证;操作资格证书;特种证书;上岗证
-management,施工管理及作业人员配备与分工,SpecialWorkers,特种作业人员,特种作业人员名单、特种作业操作资格证书、证书编号、证书有效期、特种作业工种、岗位职责、证书延期复核、违章作业记录。,SpecialOperationsJobResponsibilities,特种作业岗位职责,需明确作业人员从事的具体工种(如“塔式起重机司机”“高处作业吊篮安装拆卸工”)、细化特种作业人员的职责是工种管理的关键;,特种作业职责;工种职责;特种岗位职责;作业人员职责
-management,施工管理及作业人员配备与分工,OtherWorkers,其它作业人员,专业分包单位管理人员数量、不同工种作业人员数量、作业人员台账、工种分类统计。,NumberOfmanagementPersonnelInProfessionalSubcontractingUnits,专业分包单位管理人员数量,需明确分包单位(如劳务分包、专业分包)的管理人员(如分包项目经理、技术负责人)数量、是分包管理的基础;,分包管理人员;专业分包;劳务分包;分包单位人员
-management,施工管理及作业人员配备与分工,OtherWorkers,其它作业人员,专业分包单位管理人员数量、不同工种作业人员数量、作业人员台账、工种分类统计。,NumberOfWorkersInDifferentJobCategories,不同工种作业人员数量,需以表格形式明确各工种(如木工、钢筋工、混凝土工、砌筑工等)的作业人员数量、是劳动力调配的依据;,工种数量;作业人员数量;不同工种;工种统计;人员统计
-management,施工管理及作业人员配备与分工,OtherWorkers,其它作业人员,专业分包单位管理人员数量、不同工种作业人员数量、作业人员台账、工种分类统计。,WorkersLlog,作业人员台账,需记录作业人员的姓名、工种、身份证号、联系方式等信息、是人员管理的重要档案;,作业人员台账;工人信息;人员档案;实名制;人员登记
-acceptance,验收要求,Standards,验收标准,国家验收规范(如《公路工程质量检验评定标准》JTG F80/1-2017、《桥梁工程施工质量验收标准》GB 50205-2020)、行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2020)、企业管理办法(如《四川路桥施工验收管理办法》《路桥集团专项施工方案验收条件》)、操作规程(如《桥梁施工安全操作规程》),NationalStandardsSpecificationsAndOperatingProcedures,国家标准、规范、操作规程,是验收的基础依据、需明确具体规范名称(如JTG F80/1-2017)、避免使用“国家规范”等泛化表述;,国家标准;操作规程;JTG F80;GB 50205;验收规范;国家规范
-acceptance,验收要求,Standards,验收标准,国家验收规范(如《公路工程质量检验评定标准》JTG F80/1-2017、《桥梁工程施工质量验收标准》GB 50205-2020)、行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2021)、企业管理办法(如《四川路桥施工验收管理办法》《路桥集团专项施工方案验收条件》)、操作规程(如《桥梁施工安全操作规程》),IndustryStandardOperatingProcedures,行业标准、规范、操作规程,需指向具体行业的内部文件(如行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2020)、体现行业管理要求;,行业标准;行业规范;行业操作规程;JTG_T 3650;行业内部文件
-acceptance,验收要求,Standards,验收标准,国家验收规范(如《公路工程质量检验评定标准》JTG F80/1-2017、《桥梁工程施工质量验收标准》GB 50205-2020)、行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2022)、企业管理办法(如《四川路桥施工验收管理办法》《路桥集团专项施工方案验收条件》)、操作规程(如《桥梁施工安全操作规程》),SichuanRoadAndBridgemanagementRegulations,四川路桥的管理办法,需关联企业管理办法(如《四川路桥施工验收管理办法》)、体现企业特色和企业管理要求;,四川路桥施工验收;四川路桥管理办法;川路桥管理;四川路桥验收
-acceptance,验收要求,Standards,验收标准,国家验收规范(如《公路工程质量检验评定标准》JTG F80/1-2017、《桥梁工程施工质量验收标准》GB 50205-2020)、行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2023)、企业管理办法(如《四川路桥施工验收管理办法》《路桥集团专项施工方案验收条件》)、操作规程(如《桥梁施工安全操作规程》),managementRegulationsOfLuqiaoGroup,路桥集团的管理办法,需关联集团管理办法(如《路桥集团专项施工方案验收条件》)、体现集团特色和集团管理要求;,路桥集团专项施工方案验收;路桥集团管理办法;路桥集团验收条件;四川路桥集团验收;四川路桥集团管理办法;四川路桥集团专项施工方案
-acceptance,验收要求,Standards,验收标准,国家验收规范(如《公路工程质量检验评定标准》JTG F80/1-2017、《桥梁工程施工质量验收标准》GB 50205-2020)、行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2024)、企业管理办法(如《四川路桥施工验收管理办法》《路桥集团专项施工方案验收条件》)、操作规程(如《桥梁施工安全操作规程》),managementRegulationsOfBridgeCompany,桥梁公司的管理办法,需关联桥梁施工管理办法(如《桥梁施工安全操作规程》)、体现桥梁施工重点 focus:和桥梁施工标准管理要求;,桥梁公司管理办法;桥梁施工管理;桥梁安全操作规程;桥梁公司验收;四川路桥桥梁公司管理办法;四川路桥桥梁公司验收
-acceptance,验收要求,Procedure,验收程序,进场验收(材料/设备进场检验)、过程验收(工序/隐蔽工程验收)、阶段验收(基础/主体/装饰阶段验收)、完工验收(工程竣工预验收),OnsiteAcceptance,进场验收,需明确验收对象(如“钢筋进场验收”“塔式起重机进场验收”)、是质量控制的第一道防线;,进场验收;材料进场;设备进场验收;进场检验
-acceptance,验收要求,Procedure,验收程序,进场验收(材料/设备进场检验)、过程验收(工序/隐蔽工程验收)、阶段验收(基础/主体/装饰阶段验收)、完工验收(工程竣工预验收),ProcessAcceptance,过程验收,需关联施工工序(如“混凝土浇筑过程验收”“钢筋绑扎过程验收”)、强调动态管控;,过程验收;施工过程验收;工序验收;隐蔽工程验收
-acceptance,验收要求,Procedure,验收程序,进场验收(材料/设备进场检验)、过程验收(工序/隐蔽工程验收)、阶段验收(基础/主体/装饰阶段验收)、完工验收(工程竣工预验收),StageAcceptance,阶段验收,需对应工程阶段(如“基础工程阶段验收”“主体结构阶段验收”)、是阶段性成果确认的关键;,阶段验收;基础工程验收;主体验收;结构验收;阶段性验收
-acceptance,验收要求,Procedure,验收程序,进场验收(材料/设备进场检验)、过程验收(工序/隐蔽工程验收)、阶段验收(基础/主体/装饰阶段验收)、完工验收(工程竣工预验收),CompletionAcceptance,完工验收,需明确验收内容(如“工程竣工预验收”“专项施工方案完工验收”)、是竣工验收的前提。,完工验收;竣工验收;竣工预验收;完工后验收
-acceptance,验收要求,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),SafetyProductionConditionAcceptance,安全生产条件验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、需细化具体内容(如“安全防护设施验收”包括“安全网张挂验收”“防护栏杆安装验收”)、避免“安全生产验收”等泛化表述;,安全生产条件验收;安全验收;安全防护验收;临时用电验收
-acceptance,验收要求,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),ResourceAllocationAcceptance,资源配置验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、需关联资源类型(如“人员配置验收”包括“特种作业人员资质验收”“管理人员到位验收”)、体现资源的针对性;,资源配置验收;人员配置验收;设备配置验收;资源验收
-acceptance,验收要求,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),ConstructionProcessAcceptance,施工工艺验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、需明确工艺环节(如“模板安装工艺验收”包括“模板垂直度验收”“模板拼接缝验收”)、强调工艺的标准化;,施工工艺验收;模板安装验收;工艺验收;施工技术验收
-acceptance,验收要求,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),AcceptanceOfMechanicalEquipment,机械设备验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、需指向具体设备(如“塔式起重机验收”包括“设备型号验收”“安全装置验收”)、确保设备符合施工要求。,机械设备验收;设备验收;起重机验收;机械验收
-acceptance,验收要求,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),TemporarySupportStructure,临时支撑结构验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、专项施工方案及审批记录、技术交底记录、构配件质量证明文件(合格证/检测报告)、地基承载力报告、搭设过程检查记录、荷载试验报告(高支模/大跨度)、验收记录表(含实测数据/影像资料)、整改复查记录。,临时支撑验收;脚手架验收;满堂支架验收;支架验收;临时结构验收
-acceptance,验收要求,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),PersonnelOperationPlatform,人员操作平台验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、架体材质(如钢管无裂纹、弯曲、型钢无开焊);架体构造(立杆间距、剪刀撑设置、连墙件固定);稳定性(移动式平台刹车装置、落地式平台基础坚实度);荷载限制(平台荷载不超过设计值、悬挂限载标志);防护栏杆(高度≥1.2m、竖向栏杆间距≤1.5m、底部设挡脚板);平台铺板(满铺、固定、无空隙);登高扶梯(防滑、固定、与平台连接牢固);安全网(平台周边设置密目网或安全平网)。,操作平台验收;人员平台;高空作业平台验收;施工平台验收
-acceptance,验收要求,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),SafetyProtectionFacilities,安全防护措施验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、需结合场景需求(如建筑施工中的基坑临边防护、电梯井防护门)、功能定位(如预防事故的防护栏杆、减少事故影响的安全网)、技术要求(如材质、构造、固定方式)。其核心逻辑是“隔离危险、承接冲击、提醒注意”,安全防护验收;防护设施验收;安全网验收;防护栏杆验收
-acceptance,验收要求,Timing,验收时间,专项施工方案验收时间(如《专项施工方案验收条件一览表》预估时间)、验收时间调整(根据实际进度调整验收时间)、验收条件触发时间(具备验收条件后15日内组织验收),AcceptanceTimeOfSpecialConstructionPlan,专项施工方案验收时间,需关联具体表格(如“《专项施工方案验收条件一览表》预估时间”)、体现时间的可追溯性;,验收时间;专项方案验收时间;验收时间表;预估验收时间
-acceptance,验收要求,Timing,验收时间,专项施工方案验收时间(如《专项施工方案验收条件一览表》预估时间)、验收时间调整(根据实际进度调整验收时间)、验收条件触发时间(具备验收条件后16日内组织验收),AcceptanceTimeAdjustment,验收时间调整,需明确调整依据(如“根据施工进度调整验收时间”)、避免“时间调整”等泛化表述;,验收时间调整;进度调整验收;时间调整说明
-acceptance,验收要求,Timing,验收时间,专项施工方案验收时间(如《专项施工方案验收条件一览表》预估时间)、验收时间调整(根据实际进度调整验收时间)、验收条件触发时间(具备验收条件后17日内组织验收),AcceptanceConditionTriggerTime,验收条件触发时间,需明确时间节点(如“具备验收条件后15日内组织验收”)、强调时效性。,验收条件触发;15日内;具备验收条件;组织验收时间
-acceptance,验收要求,Personnel,验收人员,建设单位验收人员(如建设单位项目负责人、建设单位技术负责人)、设计单位验收人员(如设计单位项目负责人、设计单位专业工程师)、施工单位验收人员(如施工单位项目经理、施工单位技术负责人)、监理单位验收人员(如总监理工程师、专业监理工程师)、监测单位验收人员(如监测项目负责人、监测技术员),AcceptancePersonnelOfTheConstructionUnit,建设单位验收人员,需明确具体角色(如“建设单位项目负责人”)、避免“建设单位人员”等泛化表述;由施工作业班组在施工过程中自行对照方案自检、施工完成后由方案编制负责人、项目经理、项目副经理、项目技术负责人、安全环保处、工程处、机料处、合同处、专业分包单位(协作队伍)项目负责人和项目技术负责人等部门人员参加方案验收。,建设单位验收人员;业主验收;建设单位项目负责人;甲方验收
-acceptance,验收要求,Personnel,验收人员,建设单位验收人员(如建设单位项目负责人、建设单位技术负责人)、设计单位验收人员(如设计单位项目负责人、设计单位专业工程师)、施工单位验收人员(如施工单位项目经理、施工单位技术负责人)、监理单位验收人员(如总监理工程师、专业监理工程师)、监测单位验收人员(如监测项目负责人、监测技术员),DesignUnitAcceptancePersonnel,设计单位验收人员,需明确验收人员姓名、关联专业(如“设计单位专业工程师”)、体现设计的专业性;由施工作业班组在施工过程中自行对照方案自检、施工完成后由方案编制负责人、项目经理、项目副经理、项目技术负责人、安全环保处、工程处、机料处、合同处、专业分包单位(协作队伍)项目负责人和项目技术负责人等部门人员参加方案验收。,设计单位验收;设计单位人员;设计师验收;设计验收
-acceptance,验收要求,Personnel,验收人员,建设单位验收人员(如建设单位项目负责人、建设单位技术负责人)、设计单位验收人员(如设计单位项目负责人、设计单位专业工程师)、施工单位验收人员(如施工单位项目经理、施工单位技术负责人)、监理单位验收人员(如总监理工程师、专业监理工程师)、监测单位验收人员(如监测项目负责人、监测技术员),ConstructionUnitAcceptancePersonnel,施工单位验收人员,需明确验收人员姓名、指向管理岗位(如“施工单位项目经理”“施工单位技术负责人”)、强调施工单位的主体责任;由施工作业班组在施工过程中自行对照方案自检、施工完成后由方案编制负责人、项目经理、项目副经理、项目技术负责人、安全环保处、工程处、机料处、合同处、专业分包单位(协作队伍)项目负责人和项目技术负责人等部门人员参加方案验收。,施工单位验收;施工方验收;施工单位项目经理;施工验收人员
-acceptance,验收要求,Personnel,验收人员,建设单位验收人员(如建设单位项目负责人、建设单位技术负责人)、设计单位验收人员(如设计单位项目负责人、设计单位专业工程师)、施工单位验收人员(如施工单位项目经理、施工单位技术负责人)、监理单位验收人员(如总监理工程师、专业监理工程师)、监测单位验收人员(如监测项目负责人、监测技术员),InspectionPersonnelOfTheSupervisionUnit,监理单位验收人员,需明确验收人员姓名、监理角色(如“总监理工程师”“专业监理工程师”)、体现监理的监督职责;由施工作业班组在施工过程中自行对照方案自检、施工完成后由方案编制负责人、项目经理、项目副经理、项目技术负责人、安全环保处、工程处、机料处、合同处、专业分包单位(协作队伍)项目负责人和项目技术负责人等部门人员参加方案验收。,监理单位验收;总监理工程师;监理人员;监理验收
-acceptance,验收要求,Personnel,验收人员,建设单位验收人员(如建设单位项目负责人、建设单位技术负责人)、设计单位验收人员(如设计单位项目负责人、设计单位专业工程师)、施工单位验收人员(如施工单位项目经理、施工单位技术负责人)、监理单位验收人员(如总监理工程师、专业监理工程师)、监测单位验收人员(如监测项目负责人、监测技术员),MonitoringUnitAcceptancePersonnel,监测单位验收人员,需明确验收人员姓名、关联监测内容(如“监测项目负责人”“监测技术员”)、确保监测数据的准确性;由施工作业班组在施工过程中自行对照方案自检、施工完成后由方案编制负责人、项目经理、项目副经理、项目技术负责人、安全环保处、工程处、机料处、合同处、专业分包单位(协作队伍)项目负责人和项目技术负责人等部门人员参加方案验收。,监测单位验收;监测人员;监测项目负责人;监测验收
-other,其它资料,Calculations,计算书,编制依据、工程简况、方案简述、设计参数、主要工况计算、局部计算、结论及建议、应力分析结果、变形分析结果、反力分析结果、屈曲分析结果,ContentRequirements,内容要求,专项施工方案中包含承重结构、重要临时设施、设备选型、吊绳吊具受力计算;地基承载力等工作内容编制的专项计算书、内容应包含编制依据、工程简况、方案简述、设计参数、主要工况计算、局部计算、结论及建议。,计算书;编制依据;设计参数;主要工况;计算内容;结构计算
-other,其它资料,Calculations,计算书,编制依据、工程简况、方案简述、设计参数、主要工况计算、局部计算、结论及建议、应力分析结果、变形分析结果、反力分析结果、屈曲分析结果,CalculationOfMainWorkingConditions,主要工况计算,需针对关键施工工况(如“盖梁浇筑工况”“桩基础施工工况”)、包含“本工况描述”“应力/变形/反力/屈曲分析结果”、是计算书的核心内容;,主要工况计算;工况分析;应力分析;变形分析;反力分析;屈曲分析
-other,其它资料,Calculations,计算书,编制依据、工程简况、方案简述、设计参数、主要工况计算、局部计算、结论及建议、应力分析结果、变形分析结果、反力分析结果、屈曲分析结果,LocalCalculation,局部计算,需对于受力集中、结构复杂的局部重要节点进行细部分析(如“钢管桩与横梁连接节点”“模板支撑体系节点”)、确保结构安全;,局部计算;节点计算;细部计算;局部受力;复杂节点
-other,其它资料,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,OverallLayoutPlan,总体平面布置图,需展示项目整体布局(如“施工便道”“材料堆放区”“临时设施”)、是施工部署的可视化基础;,总体平面布置图;总平面图;施工布局;总体布置
-other,其它资料,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,ConstructionSiteLayoutPlan,施工工点平面布置图,需围绕“空间规划”“功能实现”“安全文明”三大核心、覆盖从边界界定到具体设施的全流程要素。,施工工点平面布置图;工点布置;场地布置图;施工场地平面
-other,其它资料,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,LongitudinalElevationLayoutOfSupportingStructure,支撑结构纵立面布置图,需明确支撑体系(如“钢管桩支架”“满堂脚手架”)的纵向布置(如“桩长”“间距”“标高”)、是结构安全的关键依据;,支撑结构纵立面;纵立面布置;桩长;纵断面图;支撑纵立面
-other,其它资料,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,CrossSectionalLayoutDiagram,支横断面布置图,需围绕“断面类型”“组成要素”“尺寸参数”“坡度设置”“附属设施”五大维度、覆盖道路、桥梁等工程的通用及专业要素。,横断面图;断面布置图;横断面布置;截面图
-other,其它资料,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,FloorPlan,平面布置图,需围绕“空间布局”“功能分区”“施工支持”三大核心、覆盖从区域划分到具体设施的全流程要素。,平面布置图;功能分区;施工区域划分;平面图
-other,其它资料,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,DetailedStructuralDiagram,细部构造图,需细化关键节点(如“模板拼接节点”“支撑体系连接节点”)、标注尺寸、材料及工艺要求、指导现场施工;,细部构造图;节点详图;构造详图;节点图
-other,其它资料,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,FormworkLayoutDrawing,模板布置图,需明确模板的平面位置(如“柱模板”“梁模板”)、尺寸及支撑方式、确保模板安装符合设计要求。,模板布置图;模板平面图;模板位置
-other,其它资料,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,TemplateConstructionDiagram,模板构造图,需覆盖模板体系组成、构造细节、支撑系统、节点处理及精度控制等全流程要素。,模板构造图;模板体系图;支撑系统图;模板结构图
-other,其它资料,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,ConstructionScheduleNetworkDiagram,施工进度计划网络图,需用节点表示工序逻辑关系(如“桩基础施工→承台施工→盖梁施工”)、是进度控制的核心工具;,网络图;施工进度网络图;工序网络;双代号网络图
-other,其它资料,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,ConstructionScheduleGanttChart,施工进度计划横道图,围绕“时间维度”“任务要素”“进度关系”“调整控制”四大核心、覆盖从计划编制到动态监控的全流程。,施工进度横道图;横道图附表;进度计划横道;甘特图附表
-other,其它资料,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,HazardAnalysisAndResponseMeasuresTable,危险源分析和应对措施表,需识别施工中的危险源(如“高处坠落”“物体打击”)、制定针对性应对措施(如“设置防护栏杆”“佩戴安全带”)、是安全保障的关键文档;,危险源分析表;应对措施表;危险源表;风险分析表
-other,其它资料,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,ScannedCopyOfTheCertificateOfFulltimSafetymanagementPersonnel,专职安全管理人员证件扫描件,需包含“安全生产考核合格证书”“证书编号”“有效期”、确保管理人员资质符合要求;,安全证件扫描;专职安全考核证;安全证书扫描件
-other,其它资料,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,ScannedCopyOfSpecialOperationsPersonnelsCertificate,特种作业人员证件扫描件,需围绕“信息真实性”“管理规范性”“使用便捷性”三大核心、覆盖证件内容、“证书编号”、“有效期”、法规标准四大维度,特种作业证件扫描;特种证书扫描件;操作证扫描
-other,其它资料,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,ScannedCopyOfProfessionalSubcontractorsQualifications,专业分包单位资质扫描件,需包含“营业执照”“资质证书”“安全生产许可证”、确保分包单位具备施工能力。,分包资质扫描;营业执照扫描;资质证书扫描;分包证件
-other,其它资料,Team,编制及审核人员情况,专项施工方案验收条件一览表、编制人员信息、复核人员信息、审核人员信息、审批人员信息、姓名、职务、职称,ListOfAcceptanceConditionsForSpecialConstructionSchemes,专项施工方案验收条件一览表,需明确验收的前提条件(如“计算书完成”“设计图审核通过”)、是验收的流程依据;,验收条件一览表;专项施工方案验收条件;验收前提条件
-other,其它资料,Team,编制及审核人员情况,专项施工方案验收条件一览表、编制人员信息、复核人员信息、审核人员信息、审批人员信息、姓名、职务、职称,PreparePersonnelInformation,编制人员信息,需包含“姓名”“职务”“职称”(如“张三 技术员 助理工程师”)、确保编制人员具备专业能力;,编制人员;编制人信息;方案编制者;编制人
-other,其它资料,Team,编制及审核人员情况,专项施工方案验收条件一览表、编制人员信息、复核人员信息、审核人员信息、审批人员信息、姓名、职务、职称,ReviewerInformation,审核人员信息,需包含“姓名”“职务”“职称”(如“李四 项目技术负责人 工程师”)、确保审核流程的严谨性;,审核人员;复核人员;审核信息;审核人;复核人
-other,其它资料,Team,编制及审核人员情况,专项施工方案验收条件一览表、编制人员信息、复核人员信息、审核人员信息、审批人员信息、姓名、职务、职称,ApprovalPersonnelInformation,审批人员信息,需包含“姓名”“职务”“职称”(如“王五 项目经理 高级工程师”)、确保方案符合项目整体要求,审批人员;批准人;审批信息;审批签字;项目经理审批
+first_seq,first_code,first_name,second_seq,second_code,second_name,second_focus,third_seq,third_code,third_name,third_focus,keywords
+1,basis,编制依据,1,LawsAndRegulations,法律法规,NULL,1,NationalLawsAndRegulations,国家政府发布的法律法规与规章制度,国家级、法律、法规、规章、强制力、普遍适用、基础框架、顶层设计、行业准则、合规性、统一标准、权威性、强制性条文、基本要求。,国家法律;法规;规章;强制性条文;国务院令;住房城乡建设部;中华人民共和国
+1,basis,编制依据,1,LawsAndRegulations,法律法规,NULL,2,ProvincialLawsAndRegulationsOfProjectLocation,工程所在地省级政府发布的法律法规与规章制度,地方性、区域性、细化补充、因地制宜、执行细则、地方特色、适应性要求、属地管理、动态调整、配套政策、本地化实施。,省级;地方法规;省政府;地方规章;属地管理;四川省;省人民政府
+1,basis,编制依据,2,StandardsAndSpecifications,标准规范,NULL,1,IndustryStandards,行业标准,需符合国家/行业强制或推荐性标准(如GB/T、JTG等)、时效性强(需跟踪最新版)、覆盖全生命周期(设计→施工→运维)、是定义工程项目的最低技术要求、质量验收准则、安全红线。,GB/T;JTG;CJJ;行业标准;国家标准;推荐性标准;GB 5;TB;HJ;DL
+1,basis,编制依据,2,StandardsAndSpecifications,标准规范,NULL,2,TechnicalRegulations,技术规程,操作流程标准化、工艺参数、量化风险管控、节点设备使用、规范施工验收细则、人员资质要求、应急预案模板、数字化交付标准、BIM协同规则、绿色施工指标、强制性条款需100%执行(如安全操作规范)、包含可视化元素(图表、流程图、三维模型指引)、与BIM技术深度融合(4D进度模拟、5D成本控制)。,Q/CR;技术规程;操作规程;工艺规程;施工规范;企业标准
+1,basis,编制依据,3,DocumentSystems,文件制度,NULL,1,SichuanRoadAndBridgeDocumentSystemsAndmanagementProcedures,四川路桥下发的文件制度和管理程序文件,需包含集团级BIM实施标准、EPC总承包管理模式细则、强制要求下属单位接入集团统一的数字化管理平台(如PM系统)、设置科技创新成果转化的量化考核指标。,四川公路桥梁建设集团;四川公路桥梁;SCQJ;SCQJT;四川路桥总公司
+1,basis,编制依据,3,DocumentSystems,文件制度,NULL,2,RoadAndBridgeGroupDocumentSystemsAndmanagementProcedures,路桥集团下发的文件制度和管理程序文件,区域化管理细则、属地化政策适配、项目分级管控、应急响应机制、分包商信用评价、农民工工资支付、保障绿色施工区域、标准智慧工地建设指南、隐蔽工程验收流程、工程变更索赔指引。,路桥集团;路桥股份;四川路桥集团有限公司;四川路桥集团;四川路桥集团超危大;川路桥〔;路桥〔;集团下发;集团公司文件
+1,basis,编制依据,3,DocumentSystems,文件制度,NULL,3,BridgeCompanyDocumentSystemsAndmanagementProcedures,桥梁公司下发的文件制度和管理程序文件,专业技术标准、工艺工法创新、特种设备管理、试验检测规程、安全生产责任制、班前安全教育工程、质量三检制技术交底、标准化竣工资料归档、规范创优工程培育计划。,桥梁公司;桥梁分公司;桥梁工程公司;四川路桥桥梁公司;公司下发;公司管理制度
+1,basis,编制依据,3,DocumentSystems,文件制度,NULL,4,ConstructionUnitDocumentSystemsAndmanagementProcedures,建设单位下发的文件制度和管理程序文件,项目合同履约、工程款支付管理、设计变更审批、竣工验收标准、运营移交协议、保修期责任划分、参建单位考核评价、档案管理实施细则、信息沟通机制、争议解决程序。,建设单位;业主;甲方文件;建设方;建设单位下发;业主要求
+1,basis,编制依据,4,CompilationPrinciples,编制原则,NULL,1,NationalPoliciesStandardsAndDesignDocument,国家方针、政策、标准和设计文件,需动态更新(如新版《公路工程技术标准》实施后同步调整)、涉及多部门联合审查(发改委、住建部、生态环境部)。,国家方针;政策;设计文件;国家政策;设计规范;设计图纸;施工图
+1,basis,编制依据,4,CompilationPrinciples,编制原则,NULL,2,BasicConstructionProcedures,基本建设程序,法定程序刚性、四阶段闭环管理、审批链条完整性、阶段成果验收、逆程序风险管控、数字化流程跟踪。,基本建设程序;建设程序;立项;可研;审批手续;报批;批准
+1,basis,编制依据,4,CompilationPrinciples,编制原则,NULL,3,ProjectFunctionImplementation,工程项目功能实现,需求匹配度、全功能交付、使用效能保障、用户需求反演、系统集成测试、缺陷责任期追溯。,功能实现;使用功能;工程功能;满足功能;功能要求;使用要求
+1,basis,编制依据,4,CompilationPrinciples,编制原则,NULL,4,ContractPerformance,合同履约,契约精神、权利义务对等、支付节点刚性、变更索赔闭环、信用评价联动、争议解决机制,合同;履约;合同条款;合同要求;履行合同;合同约定;合同文件
+1,basis,编制依据,4,CompilationPrinciples,编制原则,NULL,5,ConstructionForceConcentration,施工力量集中,资源集约化、专业化、班组机械配置标准化、劳动力调度、算法工序穿插优化、进度风险预警,施工力量;资源集中;人员集中;机械集中;劳动力集中;人员集结
+1,basis,编制依据,4,CompilationPrinciples,编制原则,NULL,6,ProcessControl,工序控制,工序逻辑链、工艺标准化、交接检验制度化、关键线路动态监测、平行检验机制、隐蔽工程追溯,工序控制;工艺控制;工序衔接;施工工序;工序质量;过程管控
+1,basis,编制依据,5,CompilationScope,编制范围,NULL,1,ProjectCoverage,填写完整涵盖本方案包含的所有工程,项目范围完整性、子项划分颗粒度、工程量清单闭合性、专业界面划分、变更管理阈值、风险识别矩阵价、值工程分析、可施工性评审、全生命周期覆盖、涉密工程隔离。,编制范围;工程范围;施工范围;工程内容;本方案涵盖;方案范围
+1,basis,编制依据,5,CompilationScope,编制范围,NULL,2,ConstructionTechnology,部分工程可简要说明采取的施工工艺,工艺标准化体系、工法创新等级、质量控制关键点、机械化作业率、绿色施工技术、BIM协同设计、装配式构件应用、智能监测覆盖率、特殊环境适应性、非遗工艺传承。,施工工艺;施工方法;简要说明;施工技术;采用工艺
+2,overview,工程概况,1,DesignSummary,设计概况,关注名称类、具体数值类、量化单位类、技术标准需引用国家或行业规范、关注标准号数字类、各类年限、等级数值、量化单位。,1,ProjectIntroduction,工程简介,工程名称、工程类型(如住宅、桥梁、隧道)、建设规模(如建筑面积、长度、高度)、工程地址、投资额、工程性质(新建/改建/扩建)、设计单位、设计依据(如合同编号)、工程范围(如施工边界坐标)。,工程名称;工程概况;工程简介;工程性质;建设规模;工程类型;项目名称
+2,overview,工程概况,1,DesignSummary,设计概况,关注名称类、具体数值类、量化单位类、技术标准需引用国家或行业规范、关注标准号数字类、各类年限、等级数值、量化单位。,2,MainTechnicalStandards,主要技术标准,技术规范编号(如GB50021-2001《岩土工程勘察规范》)、设计使用年限、荷载标准(如活荷载、恒荷载)、抗震设防烈度、防火等级、环保标准(如绿色建筑认证)、材料标准(如混凝土强度等级C30)、施工工艺标准。,技术标准;设计使用年限;荷载等级;抗震设防;防火等级;混凝土强度;技术规范编号
+2,overview,工程概况,2,GeologyWeather,工程地质与水文气象,关注地名类、具体水位数值类、量化单位类、关注记录降雨量、水位、气温等数值量化单位类。,1,HydrologicalConditions,水文状况,地下水位(历史最高水位、当前水位)、含水层类型(孔隙水、裂隙水、承压水)、隔水层厚度、渗透系数(K值)、给水度、水质腐蚀性(如pH值、氯离子含量)、补径排条件(补给源、径流方向)、水文地质参数(如越流系数)、地下水动态监测数据。,水文;地下水;水位;地下水位;含水层;渗透系数;水文地质;补径排
+2,overview,工程概况,2,GeologyWeather,工程地质与水文气象,关注地名类、具体水位数值类、量化单位类、关注记录降雨量、水位、气温等数值量化单位类。,2,ClimaticConditions,气候条件,气候类型(如亚热带季风气候)、年平均气温、极端气温、年降水量、降雨强度(如小时最大降雨量)、蒸发量、湿度、主导风向、风速、冰冻期、台风频率、气象数据来源(如当地气象站)。,气候;气温;降水量;降雨;风速;气象;年降水;极端气温;蒸发量;冰冻期
+2,overview,工程概况,3,Surroundings,周边环境,周边环境需分析地形地貌名词、建筑分布名词及交通状况道路名词、相关量化单位、相关数字数值、各类专业性名词、建筑物名词、米或毫米等单位类。,1,PositionalRelationship,位置关系,相邻建(构)筑物(如住宅楼、桥梁)、距离(米)、方位角、山体坡度(°)、边坡稳定性(如安全系数)、河谷宽度、深基坑深度(米)、道路等级(如城市主干道)、高压电电压(kV)、地下管线类型(给水、燃气、电缆)、埋深(米)、保护距离。,位置关系;周边环境;相邻建筑;距离;管线;高压线;河道;构筑物;方位
+2,overview,工程概况,3,Surroundings,周边环境,周边环境需分析地形地貌名词、建筑分布名词及交通状况道路名词、相关量化单位、相关数字数值、各类专业性名词、建筑物名词、米或毫米等单位类。,2,StructuralDimensions,结构尺寸,建筑物高度/层数、山体海拔、边坡坡比(如1:1.5)、河谷断面尺寸、深基坑支护结构(如桩径、墙厚)、道路宽度(米)、高压电塔高度、地下管线段径(如DN100)。,建筑高度;层数;坡比;宽度;断面尺寸;坡度;桩径;墙厚;海拔
+2,overview,工程概况,4,LayoutPlan,施工平面及立面布置,场地名词、数值类、量化单位类、名称类、数值类、量化单位类。,1,TemporaryFacilityLocation,临时设施位置,拌和站坐标(如X、Y)、钢筋加工场距工程距离(米)、材料堆码区域面积(㎡)、临时占地红线、与工程最近点距离、场地利用率(%)。,拌和站;钢筋加工;材料堆放;临时设施;临时占地;材料堆场
+2,overview,工程概况,4,LayoutPlan,施工平面及立面布置,场地名词、数值类、量化单位类、名称类、数值类、量化单位类。,2,ConstructionWorkPlatform,施工作业平台与便道参数,作业平台尺寸(长×宽、米)、地面形式(混凝土硬化、砂石铺垫)、施工便道长度(米)、宽度(米)、路面形式(沥青、碎石)、最小弯曲半径(米)、坡度(%)、承载力(kPa)。,施工平台;作业平台;施工便道;便道长度;平台尺寸;通道宽度
+2,overview,工程概况,4,LayoutPlan,施工平面及立面布置,场地名词、数值类、量化单位类、名称类、数值类、量化单位类。,3,TemporaryWaterAndElectricityArrangement,临时水电布置,临时用水源(市政管网、地下水井)、管径(如DN100)、管线布置图(走向、节点距离)、供水压力(MPa)、变压器容量(kVA)、配电箱位置、线路走向(架空/埋地)、敷设方式(直埋、穿管)、电缆规格(如YJV22)。,临时用水;临时用电;供水管径;配电箱;水电布置;临时电缆
+2,overview,工程概况,5,RequirementsTech,施工要求和技术保证条件,名称类、日期类。名称类、量化单位类、数值类。,1,DurationTarget,工期目标,开工日期(年月日)、竣工日期、总工期(天)、关键节点工期(如基础完工期)、进度计划(甘特图编号)、工期保证措施(如资源调配)。,工期;开工日期;竣工日期;总工期;工期目标;关键节点;工期保证
+2,overview,工程概况,5,RequirementsTech,施工要求和技术保证条件,名称类、日期类。名称类、量化单位类、数值类。,2,QualityTarget,质量目标,质量目标(如合格率100%、鲁班奖)、合同条款编号、业主具体要求(如绿色施工认证)。,质量目标;合格率;质量标准;鲁班奖;优质工程;质量等级
+2,overview,工程概况,5,RequirementsTech,施工要求和技术保证条件,名称类、日期类。名称类、量化单位类、数值类。,3,SecurityGoals,安全目标,安全目标(如零死亡事故、隐患整改率)、合同条款编号、业主具体要求(如绿色施工认证)。,安全目标;零伤亡;安全事故;安全指标;安全生产目标
+2,overview,工程概况,5,RequirementsTech,施工要求和技术保证条件,名称类、日期类。名称类、量化单位类、数值类。,4,EnvironmentalGoals,环境目标,环境目标(如扬尘控制、噪声限值)、合同条款编号、业主具体要求(如绿色施工认证)。,环境目标;扬尘控制;噪声限值;绿色施工指标;文明施工目标
+2,overview,工程概况,6,RiskLevel,风险辨别与分级,危害隐患性词汇类、法规名称类、标准编号类。风险等级相关专业性词汇、属于、标准编号或其它编号、部门名称类、数值类、量化单位类。名称类、数值类。,1,DangerSource,危险源,地质灾害(地面沉降、滑坡)、水文风险(管涌、流砂)、施工风险(坍塌、触电)、环境风险(污染、火灾)、机械伤害(塔吊倾覆)、法律法规依据(如《安全生产法》)。,危险源;风险源;危害因素;安全隐患;事故隐患;危险因素;风险点
+2,overview,工程概况,6,RiskLevel,风险辨别与分级,危害隐患性词汇类、法规名称类、标准编号类。风险等级相关专业性词汇、属于、标准编号或其它编号、部门名称类、数值类、量化单位类。名称类、数值类。,2,ClassificationAndResponseMeasures,分级与应对措施,风险等级(重大、较大、一般)、分级标准(如LEC法)、应对措施(监测、支护、疏散)、应急预案编号、责任部门、监控频率。,风险等级;重大风险;较大风险;一般风险;应对措施;LEC;风险分级;风险评估
+2,overview,工程概况,7,Stakeholders,参建各方责任主体单位,名称类、数值类。,1,UnitType,单位类型,建设单位(业主)、设计单位、监理单位、施工单位(总包)、监控单位(监测机构)、专业分包单位(如桩基分包)、统一社会信用代码、项目负责人。,建设单位;设计单位;监理单位;施工单位;参建单位;总承包;社会信用代码
+3,plan,施工计划,1,Schedule,施工进度计划,工序作业时间分析、关键工程节点安排、施工进度计划横道图、进度控制点、里程碑事件、工序搭接关系、工期延误风险、进度调整机制、施工流水节拍、网络计划技术(如双代号网络图),1,ProcessOperationTimeAnalysis,工序作业时间分析,需明确各工序的持续时间、逻辑关系及资源需求、是进度计划的基础;,工序持续时间;工序时间分析;作业时间;持续时间;时间分析
+3,plan,施工计划,1,Schedule,施工进度计划,工序作业时间分析、关键工程节点安排、施工进度计划横道图、进度控制点、里程碑事件、工序搭接关系、工期延误风险、进度调整机制、施工流水节拍、网络计划技术(如双代号网络图),2,KeyProjectNodeArrangement,关键工程(工序)节点安排,主要工程(工序)节点的起止时间和持续时间、聚焦影响总工期的关键工序(如基础浇筑、主体封顶)、是进度控制的核心;,关键节点;里程碑;关键工序;主要节点;节点工期;关键线路
+3,plan,施工计划,1,Schedule,施工进度计划,工序作业时间分析、关键工程节点安排、施工进度计划横道图、进度控制点、里程碑事件、工序搭接关系、工期延误风险、进度调整机制、施工流水节拍、网络计划技术(如双代号网络图),3,ConstructionScheduleGanttChart,施工进度计划横道图等,直观展示进度安排的标准工具、需包含主要工序名称、起始时间、截止时间、持续时间、时间横道、责任人等信息;,横道图;进度横道;施工进度计划;甘特图;进度安排;时间计划
+3,plan,施工计划,2,Materials,施工材料计划,名称类、规格类、数值类、数值单位类,1,ListOfConstructionMeasuresAndMaterials,施工措施材料清单,排除主题工程材料、施工措施材料应包含如临时支撑结构材料、辅助施工材料、非主体工程的挡防措施、作业平台处理、模板配置、人员上下通道、安全防护措施和安全防护用品等、详细列出材料名称、规格、数量、重量及来源(如厂家、经销商)、是材料计划的核心输出,措施材料;临时支撑材料;辅助材料;安全防护材料;模板;脚手架材料
+3,plan,施工计划,3,Equipment,施工设备计划,设备名称类、规格类、数值类、数值单位类、时间日期类,1,MainConstructionMachineryAndEquipment,主要施工机械设备,列出关键设备(如起重吊装设备、混凝土浇筑设备、张拉压浆设备、人员升降设备、钻孔设备、隧道专用设备、监测监控设备、质量检查验收设备等)、明确其设备名称、规格(如额定功率)、数量及来源(自有或租赁);,施工机械;机械设备;起重机;泵车;钻机;吊装设备;设备清单;主要机械
+3,plan,施工计划,4,Workforce,劳动力计划,工种名称类、时间日期类、数值类、数值单位类,1,WorkforceAllocationPlan,劳动力配置计划,明确工种投入(如木工、钢筋工等)情况、按施工阶段(如基础、主体、装饰)列出各工种(如模板工、混凝土工)的投入数量、确保劳动力与进度匹配;,劳动力;工种投入;工人配置;班组;劳动力计划;人员配置计划
+3,plan,施工计划,4,Workforce,劳动力计划,工种名称类、时间日期类、数值类、数值单位类,2,StageLaborDemand,阶段劳动力需求,明确周/旬/月的劳动力峰值及低谷、优化人员调度;,劳动力峰值;阶段用工;月劳动力;劳动力需求;用工高峰
+3,plan,施工计划,5,SafetyCost,安全生产费用使用计划,名称类、金额类、货币数值类、货币单位类、不能将项目总的安全生产费用列入,1,CategoryOfSafetyProductionExpenses,安全生产费用类别,符合《企业安全生产费用提取和使用管理办法》(财资〔2022〕136号)及地方规定(如广东省水利厅2025年办法)、如安全防护设施、应急救援等;,安全费用类别;安全生产费用;安全投入;安全经费;财资〔2022〕136号
+3,plan,施工计划,5,SafetyCost,安全生产费用使用计划,名称类、金额类、货币数值类、货币单位类、不能将项目总的安全生产费用列入,2,SecurityFeeName,安全费用名称,具体(如“施工现场临时用电系统改造”“应急救援器材采购”)、避免模糊表述;,安全费用名称;安全防护费;应急救援费;临时用电改造;安全器材采购
+3,plan,施工计划,5,SafetyCost,安全生产费用使用计划,名称类、金额类、货币数值类、货币单位类、不能将项目总的安全生产费用列入,3,SingleInvestmentAmount,单项投入金额,明确每项费用的具体数值(如“临时防护栏杆采购:5万元”)、确保费用可量化;,单项金额;费用金额;万元;单项投入;单项安全费
+3,plan,施工计划,5,SafetyCost,安全生产费用使用计划,名称类、金额类、货币数值类、货币单位类、不能将项目总的安全生产费用列入,4,TotalSafetyProductionExpenses,安全生产费用总额,根据工程规模、风险等级计算、确保足额投入,安全费用总额;总金额;安全投入合计;安全费总计
+4,technology,施工工艺技术,1,MethodsOverview,主要施工方法概述,工艺名称类、施工专业词汇类、规格类、数值类、数值单位类,1,ConstructionTechnologySelection,施工工艺选择,需明确工程采用的核心工艺(如“现浇混凝土框架工艺”“装配式构件安装工艺”)、是施工方法概述的基础;,工艺选择;施工工艺;核心工艺;施工方法选择;工法
+4,technology,施工工艺技术,1,MethodsOverview,主要施工方法概述,工艺名称类、施工专业词汇类、规格类、数值类、数值单位类,2,MainConstructionMethods,主要施工方法,需概括各分部分项工程的关键做法(如“基础采用旋挖钻孔灌注桩施工”“主体采用铝模板体系施工”);,施工方法;主要施工方法;关键做法;灌注桩;现浇;装配式;施工工法
+4,technology,施工工艺技术,1,MethodsOverview,主要施工方法概述,工艺名称类、施工专业词汇类、规格类、数值类、数值单位类,3,TemplateConfigurationQuantity,模板配置数量,需根据施工进度和构件尺寸计算(如“柱模板配置20套”“梁模板配置15套”)、是模板管理的关键指标;,模板配置;模板数量;模板套数;模板配备
+4,technology,施工工艺技术,2,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,1,MaterialType,材料类型,需明确主要材料的类别(如“钢筋”“混凝土”“防水卷材”)、是技术参数的基础;,材料类型;钢筋;混凝土;防水材料;钢材;主要材料
+4,technology,施工工艺技术,2,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,2,MaterialSpecifications,材料规格,需细化材料的尺寸、型号(如“钢筋HRB400EΦ16”“混凝土C30P6”)、直接影响工程质量;,材料规格;型号;HRB400;C30;规格型号;材料尺寸
+4,technology,施工工艺技术,2,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,3,DeviceName,设备名称,需列出关键设备的全称(如“挖掘机”“塔式起重机”“混凝土泵车”)、是设备管理的核心;,设备名称;挖掘机;起重机;塔吊;泵车;钻机
+4,technology,施工工艺技术,2,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,4,DeviceModel,设备型号,需明确设备的规格型号(如“徐工XE200挖掘机”“中联重科TC6013塔式起重机”)、用于设备的采购和维护;,设备型号;型号规格;机械型号;设备规格
+4,technology,施工工艺技术,2,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,5,EquipmentManufacturingTime,设备出厂时间,需包含设备的出厂时间,出厂时间;出厂日期;生产日期;出厂年份
+4,technology,施工工艺技术,2,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,6,EquipmentPerformanceParameters,设备性能参数,需包含设备的额定功率、工作效率等(如“塔式起重机最大起重量8t”“混凝土泵车输送量60m³/h”)、是设备选型的依据;,性能参数;额定功率;最大起重量;输送量;工作效率
+4,technology,施工工艺技术,2,TechParams,技术参数,工程材料类名词、规格类、数值类、数值单位类、时间日期类、重量单位类,7,EquipmentWeight,设备自重,需记录设备的自身重量(如“塔式起重机自重50t”)、用于基础设计和运输规划。,设备自重;自重;重量;整机重量
+4,technology,施工工艺技术,3,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,1,MeasurementAndStakeout,测量放样,需明确测量的基准点、控制网设置(如“建立施工平面控制网”“放出建筑物轴线”)、是施工定位的关键;,测量放样;控制网;轴线;基准点;放线;测量基准;坐标
+4,technology,施工工艺技术,3,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,2,TemporaryWaterAndElectricityConsumption,临时水电用量,需计算施工期间的用水、用电量(如“临时用水管径DN100”“临时用电容量500kW”)、用于临时设施的设计;,临时用水量;临时用电量;用水量;用电量;水电用量
+4,technology,施工工艺技术,3,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,3,TheSiteIsFlat,场地平整,需明确平整的范围、标高(如“平整场地至设计标高±0.000”“压实度达到90%”)、是施工场地准备的基础;,场地平整;整平场地;标高;压实度;平整
+4,technology,施工工艺技术,3,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,4,Staffing,人员配置,需列出各岗位的人员数量(如“项目经理1名”“施工员2名”“钢筋工10名”)、是劳动力管理的核心;,人员配置;岗位人员;项目经理;施工员;人员配备;人员分工
+4,technology,施工工艺技术,3,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,5,SafetyProtectionFacilities,安全防护措施,需列出现场的安全设施(如“安全网”“防护栏杆”“消防栓”)、是安全保障的基础;,安全防护;安全网;防护栏杆;消防设施;安全设施;防护措施
+4,technology,施工工艺技术,3,PrepWork,施工准备,名称类、数值类、规格类、数值单位类、岗位名称类、时间日期类、工程设备类,6,PersonnelAccess,人员上下通道,需明确通道的形式、位置(如“楼梯间通道”“脚手架斜道”)、是人员通行的安全保障。,人员通道;上下通道;楼梯通道;斜道;人员上下;通道布置
+4,technology,施工工艺技术,4,Process,工艺流程,工序专业名称类、工程名称类、数值类、数值单位类,1,ConstructionProcess,施工工序,需列出工程的主要工序(如“地基处理→基础浇筑→主体结构→装饰装修”)、是工艺流程的核心;,施工工序;主要工序;工序流程;施工顺序;工艺步骤
+4,technology,施工工艺技术,4,Process,工艺流程,工序专业名称类、工程名称类、数值类、数值单位类,2,ProcessSequence,工艺顺序,需明确工序的先后逻辑(如“先绑扎钢筋后支模板”“先浇筑混凝土后养护”)、是流程执行的关键;,工艺顺序;施工顺序;先后顺序;工序逻辑;工序衔接关系
+4,technology,施工工艺技术,4,Process,工艺流程,工序专业名称类、工程名称类、数值类、数值单位类,3,ProcessFlowDiagram,工艺流程框图,需用图形展示工序的衔接(如“地基处理流程图”“主体结构施工流程图”)、是流程可视化的工具;,流程图;工艺流程图;施工流程框图;工序框图
+4,technology,施工工艺技术,5,Operations,施工方法及操作要求,施工流程名称类、数值类、数值单位类,1,ConstructionProcessOperations,施工工序描述操作,需详细描述各工序的操作步骤(如“钢筋绑扎操作流程”“模板安装操作步骤”)、是操作指导的核心;,操作步骤;操作流程;施工步骤;操作方法;操作要求
+4,technology,施工工艺技术,5,Operations,施工方法及操作要求,施工流程名称类、数值类、数值单位类,2,ConstructionPoints,施工要点,需明确工序的关键要求(如“钢筋绑扎需保证间距均匀”“模板安装需保证垂直度”)、是质量控制的关键;,施工要点;关键要求;质量关键;工艺要点;控制要点
+4,technology,施工工艺技术,5,Operations,施工方法及操作要求,施工流程名称类、数值类、数值单位类,3,FAQPrevention,常见问题及预防,需列出工序的常见问题及预防措施(如“预防混凝土蜂窝麻面:控制混凝土坍落度”“预防模板漏浆:密封模板缝隙”)、是风险防控的重点 ,常见问题;质量通病;预防措施;防治措施;常见缺陷;预防对策
+4,technology,施工工艺技术,5,Operations,施工方法及操作要求,施工流程名称类、数值类、数值单位类,4,ProblemSolvingMeasures,问题处理措施,需明确问题的解决方法(如“混凝土蜂窝处理:剔除松散部分、用高一等级混凝土填补”“模板漏浆处理:用海绵条密封缝隙”)、是问题解决的指南;,问题处理;处理措施;整改措施;修复方法;缺陷处理
+4,technology,施工工艺技术,6,Inspection,检查要求,材料进场检验、构配件抽查、工序检查内容、工序检查标准、隐蔽工程验收,1,MaterialInspectionUponArrival,材料进场质量检验,需明确材料的检验项目(如“钢筋的屈服强度检验”“混凝土的抗压强度检验”)、是材料质量控制的基础;,材料进场检验;进场检验;三证一检;复检;材料验收;进场质量检验
+4,technology,施工工艺技术,6,Inspection,检查要求,材料进场检验、构配件抽查、工序检查内容、工序检查标准、隐蔽工程验收,2,RandomInspectionOfIncomingComponents,构配件进场质量抽查,需明确构配件的抽查比例(如“构配件抽查比例为10%”“每批抽查5件”)、是构配件质量控制的手段;,构配件抽查;进场抽检;抽查比例;构件抽检
+4,technology,施工工艺技术,6,Inspection,检查要求,材料进场检验、构配件抽查、工序检查内容、工序检查标准、隐蔽工程验收,3,ProcessInspectionContent,工序检查内容,需列出各工序的检查项目(如“钢筋绑扎的检查内容:间距、数量、锚固长度”“模板安装的检查内容:垂直度、平整度、支撑稳定性”)、是工序检查的核心;,工序检查;检查内容;检查项目;工序检验;检查清单
+4,technology,施工工艺技术,6,Inspection,检查要求,材料进场检验、构配件抽查、工序检查内容、工序检查标准、隐蔽工程验收,4,ProcessInspectionStandards,工序检查标准,需明确检查的合格标准(如“钢筋间距允许偏差±10mm”“模板垂直度允许偏差5mm”)、是工序验收的依据;,检查标准;验收标准;允许偏差;检查合格;偏差限值
+5,safety,安全保证措施,1,SafetySystem,安全保证体系,流程体系类名词、标准文书类、标标准编号编码数字类,1,SafetyProductionAssuranceSystemFrameworkDiagram,安全生产保证体系框图,安全保证体系的视觉化呈现、需明确体系的核心要素(如组织机构、制度流程、资源保障)及逻辑关系、是公司标准体系的具象化载体;,安全保证体系;安全体系框图;安全管理体系框图;安全组织体系
+5,safety,安全保证措施,1,SafetySystem,安全保证体系,流程体系类名词、标准文书类、标标准编号编码数字类,2,CompanyStandardSystemReference,公司标准体系引用,强调安全保证体系需承接公司现有标准(如《公司安全生产管理办法》《公司安全技术规程》)、确保体系的一致性与延续性;,公司标准体系;公司安全管理办法;公司安全技术规程;标准体系引用
+5,safety,安全保证措施,2,Organization,组织保证措施,名词类、人名类、岗位名称类、制度名词类,1,SafetymanagementOrganization,安全管理组织机构,基于项目经理为组长的安全工作领导小组、关注岗位组织架构名称类、部门名称类、关系结构类名词;,安全管理组织;安全领导小组;安全管理机构;安全管理组织机构
+5,safety,安全保证措施,2,Organization,组织保证措施,名词类、人名类、岗位名称类、制度名词类,2,PersonnelSafetyResponsibilities,人员安全职责,关注岗位名称类、人名类、责任制度名词类、岗位职责名词类、安全制度名词类;,安全职责;人员安全责任;岗位安全;安全责任制
+5,safety,安全保证措施,3,TechMeasures,技术保证措施,施工专业名词类、工序名称类 、施工设备名称类、施工材料名称类、施工场地名称类、岗位名称类,1,OverallSecurityMeasures,总体安全措施,包含保证施工过程中主要工序的人员、材料、机械设备安全所采取的技术措施、以及材料运输、吊装、施工作业区域的临边、临空、洞口安全防护设施、安全母绳布置、人员上下(横向)通道布置等、是针对项目整体的安全技术规划(如“施工现场临时用电总体方案”“高空作业总体防护措施”)、需覆盖所有施工环节;,总体安全措施;临边防护;洞口防护;安全母绳;安全防护设施布置
+5,safety,安全保证措施,3,TechMeasures,技术保证措施,施工专业名词类、工序名称类 、施工设备名称类、施工材料名称类、施工场地名称类、岗位名称类,2,SafetyAssuranceMeasuresForKeyProcesses,主要工序安全措施,是针对关键工序的具体安全要求(如“深基坑开挖支护措施”“模板安装拆除安全规范”)、需明确每一步操作的安全要点;,主要工序安全;关键工序安全;工序安全措施;专项安全措施
+5,safety,安全保证措施,4,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,1,MonitoringOrganization,监测组织机构,监测监控的责任主体、需明确监测人员的资质(如注册安全工程师、监测技术员)及职责(如数据采集、分析、报告)、确保监测工作的专业性;,监测机构;监测人员;监测责任;监测组织机构;监测负责人
+5,safety,安全保证措施,4,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,2,MonitoringRange,监测范围,需覆盖施工区域内的所有风险点(如深基坑周边、高支模体系、临时用电线路)、避免遗漏;,监测范围;监测区域;监测覆盖范围;监测对象
+5,safety,安全保证措施,4,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,3,MonitoringItems,监测项目,需明确监测的具体内容(如深基坑的水平位移、高支模的立杆轴力、临时用电的电压电流)、是监测的核心;,监测项目;监测内容;水平位移;沉降监测;监测指标
+5,safety,安全保证措施,4,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,4,MonitoringPointSettings,监测点设置,需根据风险点的分布确定(如深基坑每10米设置一个位移监测点)、需符合《建筑基坑支护技术规程》(JGJ 120-2012)等行业标准;,监测点;测点布置;监测布点;测点位置
+5,safety,安全保证措施,4,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,5,MonitoringInstrumentsAndEquipment,监测仪器设备,需明确仪器的名称(如全站仪、测斜仪、应力传感器)、型号(如徕卡TS60全站仪)及精度(如0.5秒级)、确保数据的准确性;,监测仪器;全站仪;测斜仪;应力传感器;监测设备;仪器型号
+5,safety,安全保证措施,4,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,6,MonitoringMethods,监测方法,需明确数据采集的方式(如人工读数、自动采集)、及数据处理方法(如统计分析、趋势预测)、是监测的关键环节;,监测方法;数据采集;人工读数;自动采集;数据处理
+5,safety,安全保证措施,4,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,7,MonitoringFrequency,监测频率,需明确监测频率、(如深基坑每天一次、高支模每周两次)。,监测频率;监测周期;每天一次;每周两次;监测时间间隔
+5,safety,安全保证措施,4,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,8,WarningValuesAndControlValues,预警值及控制值,需根据设计文件及行业标准确定(如深基坑水平位移预警值为30mm、控制值为50mm)、是判断风险的重要依据;,预警值;控制值;报警值;监测阈值;预警指标;预警控制
+5,safety,安全保证措施,4,Monitoring,监测监控措施,组织机构名称类、施工区域名称类、监测专业名词类、施工场地名称类、设备名称类、数值类、数值单位类、岗位名称类,9,InformationFeedbackMechanism,信息反馈机制,监测数据的传递流程(如监测人员→项目安全部→项目经理→公司总部)、需明确反馈的时间要求(如实时反馈、每日汇总)、确保风险及时处理。,信息反馈;监测报告;数据反馈;反馈流程;监测信息传递
+5,safety,安全保证措施,5,Emergency,应急处置措施,事故名称类、救援器材类、机构名称类、数字类、数值单位类。,1,EmergencyProcedures,应急处置程序,应采用公司标准应急处理程序图(附件16)、应急响应的步骤流程(如“事故报告→现场警戒→人员疏散→救援实施→善后处理”)、需明确每一步的责任部门及时间要求、确保响应及时;,应急程序;应急流程;应急响应;应急处置程序;处置步骤
+5,safety,安全保证措施,5,Emergency,应急处置措施,事故名称类、救援器材类、机构名称类、数字类、数值单位类。,2,EmergencyMeasures,应急处置措施,应根据方案实施过程潜在的危险源、判断出可能造成的伤害类型、制定出有针对性的救援措施、保证在事故发生后伤者能得到有效和及时的救治、如触电、有毒有害气体中毒、高处坠落、物体打击、施工现场及驻地火灾等事故、针对不同类型事故的具体处理方法(如“火灾事故使用干粉灭火器扑救”“坍塌事故使用千斤顶支撑”)、需明确操作要点(如灭火器的使用方法、千斤顶的支撑位置)、确保救援有效;,应急措施;救援措施;应急处置;紧急处置;事故救援;触电;中毒;坍塌
+5,safety,安全保证措施,5,Emergency,应急处置措施,事故名称类、救援器材类、机构名称类、数字类、数值单位类。,3,EmergencySuppliesAndEquipmentSupport,应急物资及设备保障,应根据事故的不同、以表格的形式说明救援的物品名称、规格型号、单位、数量、监管人、联系电话等内容、应急处置的物质基础、需明确物资的名称(如灭火器、急救箱、千斤顶)、数量(如每100平方米配备2个灭火器)、存放位置(如施工现场入口处)及维护要求(如每月检查一次灭火器压力)、确保物资随时可用;,应急物资;应急设备;救援器材;灭火器;急救箱;应急保障
+5,safety,安全保证措施,5,Emergency,应急处置措施,事故名称类、救援器材类、机构名称类、数字类、数值单位类。,4,TrafficmanagementAndMedicalRescue,交通疏导与医疗救援,应以表格的形式明确施工工点附近的医疗救援机构名称、联系电话、距离等、并附应急救援线路图;,医疗救援;交通疏导;救援线路;医院联系;急救电话;应急救援路线
+5,safety,安全保证措施,5,Emergency,应急处置措施,事故名称类、救援器材类、机构名称类、数字类、数值单位类。,5,PostDisposal,后期处置,包括善后处理、调查与评估、恢复生产等三个方面、事故后的恢复工作、需明确善后处理(如伤亡人员家属安抚、财产损失统计)、事故调查(如原因分析、责任认定评估)及整改措施(如完善安全制度、加强培训)、避免事故重复发生;,后期处置;善后处理;事故调查;恢复生产;善后工作;事故评估
+6,quality,质量保证措施,1,QualitySystem,质量保证体系,组织机构名称类、岗位名称类、岗位职责词汇类。,1,QualityAssuranceSystemFramework,质量管理组织机构,应引用公司标准体系框图、质量体系的视觉化呈现、需明确体系的核心要素(如组织机构、制度流程、资源保障)及逻辑关系、是体系落地的框架基础;,质量保证体系;质量体系框图;质量管理体系;质量管理组织机构
+6,quality,质量保证措施,1,QualitySystem,质量保证体系,组织机构名称类、岗位名称类、岗位职责词汇类。,2,QualitymanagementOrganization,人员职责,基于项目经理为组长的工作领导小组、小组中包括项目经理、项目总工、质量总监、工程部门、质检部门、专业分包单位(协作队伍)项目负责人和项目技术负责人等、需明确层级(如公司级、项目级、班组级)及组成部门(如质量部、工程部、技术部)、形成“横向到边、纵向到底”的管理网络;,质量管理组织;质量领导小组;质检人员;质量总监;质量体系组织
+6,quality,质量保证措施,1,QualitySystem,质量保证体系,组织机构名称类、岗位名称类、岗位职责词汇类。,3,PersonnelResponsibilities,质量保证体系框图,需细化每个岗位的质量责任(如项目经理的“第一责任人”职责、质量员的“现场监督”职责)、避免职责模糊导致的管理漏洞;,质量职责;质量责任制;岗位质量责任;质量保证体系框图
+6,quality,质量保证措施,2,QualityGoals,质量目标,目标标准词汇类、合同条款类、具体工程名称类、量化数值类、数值单位类。,1,DecompositionOfQualityObjectives,质量目标分解,根据施工合同和业主要求填写、需将总目标拆解为分部(基础、主体、装饰)、分项工程的具体目标(如“主体结构混凝土强度合格率100%”)、是目标落地的关键;,质量目标分解;分项质量;质量指标;质量目标分项
+6,quality,质量保证措施,3,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,1,OverallPlanForEngineeringExcellence,工程创优总体计划,需明确创优的阶段目标(如“基础工程创优”“主体工程创优”)及关键节点、是创优工作的路线图;,创优计划;精品工程;优质工程;工程创优;创优目标
+6,quality,质量保证措施,3,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,2,TechnicalPreparation,技术准备,需涵盖BIM技术应用、施工方案优化等、为创优提供技术支撑;,技术准备;BIM技术;新技术应用;技术支撑;技术创新
+6,quality,质量保证措施,3,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,3,ProcessControl,过程控制,需聚焦关键工序(如“大体积混凝土浇筑”“钢结构安装”)、打造精品工序;,过程控制;关键工序精品;精品工序;质量控制过程;工程创优过程
+6,quality,质量保证措施,3,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,4,DetailedTreatment,细部处理,需优化节点做法(如“墙面抹灰阴阳角顺直”“防水卷材搭接严密”)、提升工程观感质量;,细部处理;节点优化;节点做法;细部质量;细部工艺
+6,quality,质量保证措施,3,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,5,NewTechnologyPromotion,新技术推广,需应用“四新技术”(新技术、新材料、新工艺、新设备)、提升创优的技术含量;,四新技术;新技术推广;新工艺;新材料;新设备;技术创新应用
+6,quality,质量保证措施,3,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,6,PreparationOfApplicationMaterials,申报资料编制,需整理创优所需的资料(如工程质量报告、技术创新成果)、是创优申报的核心材料;,申报资料;创优申报;工程质量报告;申报材料
+6,quality,质量保证措施,3,Excellence,工程创优规划,工程创优总体计划、技术准备(BIM/新技术应用)、过程控制(关键工序精品打造)、细部处理(节点优化)、精品工程创建、新技术推广(四新技术)、申报资料编制、工程资料归档、创优考核机制,7,EngineeringDataArchiving,工程资料归档,需确保资料真实、完整、符合创优评审要求。,工程资料归档;档案管理;竣工资料;资料归档
+6,quality,质量保证措施,4,QualityControl,质量控制程序与具体措施,原材料检查验收(三证一检)、实体工程质量验收(分项/分部工程验收)、质量通病防治(墙面空鼓/屋面渗漏)、季节性施工质量控制(冬期混凝土保温/雨期防水)、工序质量控制点、质量检查程序(自检/互检/专检)、质量问题整改(闭环管理),1,RawMaterialInspection,原材料检查验收,需执行“三证一检”(合格证、质检报告、生产许可证+进场复检)、确保材料质量;,原材料进场;三证一检;材料检验;复检报告;进场材料质量
+6,quality,质量保证措施,4,QualityControl,质量控制程序与具体措施,原材料检查验收(三证一检)、实体工程质量验收(分项/分部工程验收)、质量通病防治(墙面空鼓/屋面渗漏)、季节性施工质量控制(冬期混凝土保温/雨期防水)、工序质量控制点、质量检查程序(自检/互检/专检)、质量问题整改(闭环管理),2,PhysicalProjectQualityAcceptance,实体工程质量验收,需按分项(如“钢筋绑扎”)、分部工程(如“基础工程”)进行验收、符合规范要求;,实体验收;分项验收;分部验收;实体工程验收;工程质量验收
+6,quality,质量保证措施,4,QualityControl,质量控制程序与具体措施,原材料检查验收(三证一检)、实体工程质量验收(分项/分部工程验收)、质量通病防治(墙面空鼓/屋面渗漏)、季节性施工质量控制(冬期混凝土保温/雨期防水)、工序质量控制点、质量检查程序(自检/互检/专检)、质量问题整改(闭环管理),3,PreventionAndControlOfCommonQualityDefectsInProcesses,工序质量通病防治,需针对常见问题(如“墙面空鼓”“屋面渗漏”)制定专项措施(如“抹灰前基层凿毛”“防水附加层施工”)、减少质量缺陷;,质量通病;空鼓;渗漏;裂缝;蜂窝麻面;防治措施;通病防治
+6,quality,质量保证措施,4,QualityControl,质量控制程序与具体措施,原材料检查验收(三证一检)、实体工程质量验收(分项/分部工程验收)、质量通病防治(墙面空鼓/屋面渗漏)、季节性施工质量控制(冬期混凝土保温/雨期防水)、工序质量控制点、质量检查程序(自检/互检/专检)、质量问题整改(闭环管理),4,SeasonalConstructionQualityAssuranceMeasures,季节性施工质量保证措施,需针对冬期(混凝土保温)、雨期(防水加强)、高温(混凝土保湿)制定专项措施、确保施工质量;,季节性施工;冬期施工;雨期施工;高温施工;夏季施工;冬季混凝土
+7,environment,环境保证措施,1,EnvSystem,环境保证体系,环境保证体系框图、公司标准体系引用,1,BlockDiagramOfEnvironmentalAssuranceSystem,环境保证体系框图,环境保证体系的视觉化呈现、需明确体系的核心要素(如组织机构、制度流程、资源保障)及逻辑关系、是公司标准体系的具象化载体;,环境保证体系;环境管理体系框图;环境保证体系框图
+7,environment,环境保证措施,1,EnvSystem,环境保证体系,环境保证体系框图、公司标准体系引用,2,CompanyStandardSystemReference,公司标准体系引用,应引用公司标准体系框图、强调环境保证体系需承接公司现有标准(如《公司环境管理体系手册》《公司环境保护管理办法》)、确保体系的一致性与延续性;,环境管理体系;环境保护管理办法;公司环境标准;环境体系引用
+7,environment,环境保证措施,2,EnvOrg,环境保护组织机构,环境保护组织架构、管理人员姓名、管理人员职务、管理人员职责、环境管理岗位责任、责任考核机制、环境管理职责分工、环境管理人员资质、环境管理沟通机制,1,EnvironmentalAssuranceSystemFramework,环境保护组织架构,包含管理人员姓名、职务、职责、环境管理的责任主体、基于项目经理为组长的工作领导小组、小组中包括项目经理、项目副经理、项目总工、工程部门、质检部门、安全环保部门、专业分包单位(协作队伍)项目负责人和项目技术负责人等、需明确机构的层级(如公司级、项目级、班组级)及组成部门(如环境部、工程部、技术部)、形成“横向到边、纵向到底”的管理网络;,环境保护组织;环境管理机构;环境管理组织架构;环境领导小组
+7,environment,环境保证措施,2,EnvOrg,环境保护组织机构,环境保护组织架构、管理人员姓名、管理人员职务、管理人员职责、环境管理岗位责任、责任考核机制、环境管理职责分工、环境管理人员资质、环境管理沟通机制,2,EnvironmentalmanagementJobResponsibilities,环境管理岗位责任,需明确各岗位的环境责任(如项目经理的“环境第一责任人”职责、环境专员的“现场巡查”职责)、是组织保证的基石;,环境职责;环保岗位;环境管理责任;环保责任
+7,environment,环境保证措施,2,EnvOrg,环境保护组织机构,环境保护组织架构、管理人员姓名、管理人员职务、管理人员职责、环境管理岗位责任、责任考核机制、环境管理职责分工、环境管理人员资质、环境管理沟通机制,3,ResponsibilityAssessmentMechanism,责任考核机制,对环境职责履行情况的评价方式(如月度考核、年度评优)、需与环境绩效挂钩(如奖金发放、晋升晋级)、强化责任意识。,考核机制;责任考核;环境考核;月度考核;绩效考核;考核评价
+7,environment,环境保证措施,3,EnvProtection,环境保护及文明施工措施,办公生活区环境卫生管理、生活垃圾分类处置、卫生防疫措施、饮用水安全保障、施工区域水土流失防治、截排水系统设计、边坡防护工程、沉沙池设置、噪声排放监测、低噪声设备选型、隔音屏障设置、施工时间管控、污水沉淀处理、油污隔离措施、废水检测达标、扬尘控制、废气净化处理、清洁能源使用、裸土覆盖、车辆冲洗设施。,1,EnvironmentalSanitationGuaranteeMeasuresForOfficeAndLivingAreas,办公生活区环境卫生保证措施,需明确责任分工(如保洁人员配置、卫生区域划分)及管理流程(如每日清扫、每周检查)、确保环境整洁;,环境卫生;生活区;办公区;生活垃圾;卫生防疫;饮用水
+7,environment,环境保证措施,3,EnvProtection,环境保护及文明施工措施,办公生活区环境卫生管理、生活垃圾分类处置、卫生防疫措施、饮用水安全保障、施工区域水土流失防治、截排水系统设计、边坡防护工程、沉沙池设置、噪声排放监测、低噪声设备选型、隔音屏障设置、施工时间管控、污水沉淀处理、油污隔离措施、废水检测达标、扬尘控制、废气净化处理、清洁能源使用、裸土覆盖、车辆冲洗设施。,2,SoilAndWaterConservationMeasuresInTheConstructionArea,施工区域水土流失防治措施,需通过“截(截水沟)、排(排水沟)、拦(拦挡坝)、护(边坡防护)”综合措施、减少雨水对裸露土壤的冲刷;,水土流失;边坡防护;截排水;沉沙池;水土保持;拦挡
+7,environment,环境保证措施,3,EnvProtection,环境保护及文明施工措施,办公生活区环境卫生管理、生活垃圾分类处置、卫生防疫措施、饮用水安全保障、施工区域水土流失防治、截排水系统设计、边坡防护工程、沉沙池设置、噪声排放监测、低噪声设备选型、隔音屏障设置、施工时间管控、污水沉淀处理、油污隔离措施、废水检测达标、扬尘控制、废气净化处理、清洁能源使用、裸土覆盖、车辆冲洗设施。,3,NoiseEmissionMonitoring,噪声排放监测,需按照《建筑施工场界环境噪声排放标准》(GB 12523-2011)要求、在施工现场边界设置监测点、每日监测1次、记录等效声级(Leq)和最大声级(Lmax);,噪声监测;噪声排放;等效声级;噪音控制;GB 12523;声级
+7,environment,环境保证措施,3,EnvProtection,环境保护及文明施工措施,办公生活区环境卫生管理、生活垃圾分类处置、卫生防疫措施、饮用水安全保障、施工区域水土流失防治、截排水系统设计、边坡防护工程、沉沙池设置、噪声排放监测、低噪声设备选型、隔音屏障设置、施工时间管控、污水沉淀处理、油污隔离措施、废水检测达标、扬尘控制、废气净化处理、清洁能源使用、裸土覆盖、车辆冲洗设施。,4,WaterPollutionPreventionAndControlMeasures,水污染防治措施,需在搅拌机、运输车清洗处设置沉淀池、施工废水经沉淀后回用(如洒水降尘)、避免直接排入市政管网;,水污染;废水处理;污水沉淀;沉淀池;废水排放;污水处理
+7,environment,环境保证措施,3,EnvProtection,环境保护及文明施工措施,办公生活区环境卫生管理、生活垃圾分类处置、卫生防疫措施、饮用水安全保障、施工区域水土流失防治、截排水系统设计、边坡防护工程、沉沙池设置、噪声排放监测、低噪声设备选型、隔音屏障设置、施工时间管控、污水沉淀处理、油污隔离措施、废水检测达标、扬尘控制、废气净化处理、清洁能源使用、裸土覆盖、车辆冲洗设施。,5,AirPollutionPreventionAndControlMeasures,大气污染防治措施,需采取“洒水降尘、裸土覆盖、车辆冲洗、道路硬化”等措施、确保施工现场目测扬尘高度小于1.5m(土方作业阶段)或0.5m(结构施工阶段)。,扬尘;大气污染;尘土;车辆冲洗;裸土覆盖;废气排放;扬尘控制
+8,management,施工管理及作业人员配备与分工,1,Managers,施工管理人员,施工管理人员名单、岗位职责清单、管理职责分解、管理权限划分、管理流程衔接。,1,ConstructionmanagementPersonnelList,施工管理人员名单,需以表格形式明确项目管理人员(如项目经理、项目书记、项目总工、项目副经理、质量总监、安全总监、各职能部门、主管技术员、测量员、质检员、以及专业分包单位(协作队伍)项目负责人和项目技术负责人等)的姓名、岗位及联系方式、是人员管理的基础台账;,管理人员名单;项目经理;项目总工;施工管理人员;人员信息表
+8,management,施工管理及作业人员配备与分工,1,Managers,施工管理人员,施工管理人员名单、岗位职责清单、管理职责分解、管理权限划分、管理流程衔接。,2,JobResponsibilitiesList,岗位职责清单,需细化每个管理岗位的职责(如项目经理的“项目全面管理”职责、技术负责人的“技术方案审核”职责)、避免职责模糊导致的管理漏洞;,岗位职责;职责清单;管理岗位职责;岗位分工;职责分解
+8,management,施工管理及作业人员配备与分工,2,SafetyStaff,专职安全生产管理人员,专职安全生产管理人员名单、安全生产考核合格证书、证书编号、证书有效期、安全岗位职责、安全责任追究。,1,ListOfFullTimeSafetyProductionmanagementPersonnel,专职安全生产管理人员名单,需以表格形式明确专职安全员(如项目安全总监、专职安全员)的姓名、岗位及联系方式、是安全管理的核心台账;,专职安全员;专职安全管理人员;安全员名单;安全管理人员名单
+8,management,施工管理及作业人员配备与分工,2,SafetyStaff,专职安全生产管理人员,专职安全生产管理人员名单、安全生产考核合格证书、证书编号、证书有效期、安全岗位职责、安全责任追究。,2,SafetyProductionQualificationCertificate,安全生产考核合格证书,需明确证书类型(如“建筑施工企业专职安全生产管理人员证书”)、编号及有效期、是上岗的必备资质;,安全生产考核合格证;安全证书;证书编号;证书有效期;安全资质
+8,management,施工管理及作业人员配备与分工,2,SafetyStaff,专职安全生产管理人员,专职安全生产管理人员名单、安全生产考核合格证书、证书编号、证书有效期、安全岗位职责、安全责任追究。,3,SafetyProductionmanagementJobResponsibilities,安全生产管理岗位职责,需细化专职安全员的职责(如“现场安全检查”“隐患整改监督”“安全培训实施”)、确保安全管理工作落地;,安全生产管理职责;专职安全员职责;安全管理岗位职责
+8,management,施工管理及作业人员配备与分工,3,SpecialWorkers,特种作业人员,特种作业人员名单、特种作业操作资格证书、证书编号、证书有效期、特种作业工种、岗位职责、证书延期复核、违章作业记录。,1,ListOfSecialOperationsPersonnel,特种作业人员名单,需以表格形式明确特种作业人员(如建筑电工、建筑架子工、建筑起重机械司机等)的姓名、工种及联系方式、是特种作业管理的基础台账;,特种作业人员;特种作业;电工;架子工;起重机司机;焊工;特种人员名单
+8,management,施工管理及作业人员配备与分工,3,SpecialWorkers,特种作业人员,特种作业人员名单、特种作业操作资格证书、证书编号、证书有效期、特种作业工种、岗位职责、证书延期复核、违章作业记录。,2,SpecialOperationsQualificationCertificate,特种作业操作资格证书,需明确证书类型(如“建筑施工特种作业操作资格证书”)、编号及有效期、是上岗的必备资质;,特种作业资格证;操作资格证书;特种证书;上岗证
+8,management,施工管理及作业人员配备与分工,3,SpecialWorkers,特种作业人员,特种作业人员名单、特种作业操作资格证书、证书编号、证书有效期、特种作业工种、岗位职责、证书延期复核、违章作业记录。,3,SpecialOperationsJobResponsibilities,特种作业岗位职责,需明确作业人员从事的具体工种(如“塔式起重机司机”“高处作业吊篮安装拆卸工”)、细化特种作业人员的职责是工种管理的关键;,特种作业职责;工种职责;特种岗位职责;作业人员职责
+8,management,施工管理及作业人员配备与分工,4,OtherWorkers,其它作业人员,专业分包单位管理人员数量、不同工种作业人员数量、作业人员台账、工种分类统计。,1,NumberOfmanagementPersonnelInProfessionalSubcontractingUnits,专业分包单位管理人员数量,需明确分包单位(如劳务分包、专业分包)的管理人员(如分包项目经理、技术负责人)数量、是分包管理的基础;,分包管理人员;专业分包;劳务分包;分包单位人员
+8,management,施工管理及作业人员配备与分工,4,OtherWorkers,其它作业人员,专业分包单位管理人员数量、不同工种作业人员数量、作业人员台账、工种分类统计。,2,NumberOfWorkersInDifferentJobCategories,不同工种作业人员数量,需以表格形式明确各工种(如木工、钢筋工、混凝土工、砌筑工等)的作业人员数量、是劳动力调配的依据;,工种数量;作业人员数量;不同工种;工种统计;人员统计
+8,management,施工管理及作业人员配备与分工,4,OtherWorkers,其它作业人员,专业分包单位管理人员数量、不同工种作业人员数量、作业人员台账、工种分类统计。,3,WorkersLlog,作业人员台账,需记录作业人员的姓名、工种、身份证号、联系方式等信息、是人员管理的重要档案;,作业人员台账;工人信息;人员档案;实名制;人员登记
+9,acceptance,验收要求,1,Standards,验收标准,国家验收规范(如《公路工程质量检验评定标准》JTG F80/1-2017、《桥梁工程施工质量验收标准》GB 50205-2020)、行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2020)、企业管理办法(如《四川路桥施工验收管理办法》《路桥集团专项施工方案验收条件》)、操作规程(如《桥梁施工安全操作规程》),1,NationalStandardsSpecificationsAndOperatingProcedures,国家标准、规范、操作规程,是验收的基础依据、需明确具体规范名称(如JTG F80/1-2017)、避免使用“国家规范”等泛化表述;,国家标准;操作规程;JTG F80;GB 50205;验收规范;国家规范
+9,acceptance,验收要求,1,Standards,验收标准,国家验收规范(如《公路工程质量检验评定标准》JTG F80/1-2017、《桥梁工程施工质量验收标准》GB 50205-2020)、行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2021)、企业管理办法(如《四川路桥施工验收管理办法》《路桥集团专项施工方案验收条件》)、操作规程(如《桥梁施工安全操作规程》),2,IndustryStandardOperatingProcedures,行业标准、规范、操作规程,需指向具体行业的内部文件(如行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2020)、体现行业管理要求;,行业标准;行业规范;行业操作规程;JTG_T 3650;行业内部文件
+9,acceptance,验收要求,1,Standards,验收标准,国家验收规范(如《公路工程质量检验评定标准》JTG F80/1-2017、《桥梁工程施工质量验收标准》GB 50205-2020)、行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2022)、企业管理办法(如《四川路桥施工验收管理办法》《路桥集团专项施工方案验收条件》)、操作规程(如《桥梁施工安全操作规程》),3,SichuanRoadAndBridgemanagementRegulations,四川路桥的管理办法,需关联企业管理办法(如《四川路桥施工验收管理办法》)、体现企业特色和企业管理要求;,四川路桥施工验收;四川路桥管理办法;川路桥管理;四川路桥验收
+9,acceptance,验收要求,1,Standards,验收标准,国家验收规范(如《公路工程质量检验评定标准》JTG F80/1-2017、《桥梁工程施工质量验收标准》GB 50205-2020)、行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2023)、企业管理办法(如《四川路桥施工验收管理办法》《路桥集团专项施工方案验收条件》)、操作规程(如《桥梁施工安全操作规程》),4,managementRegulationsOfLuqiaoGroup,路桥集团的管理办法,需关联集团管理办法(如《路桥集团专项施工方案验收条件》)、体现集团特色和集团管理要求;,路桥集团专项施工方案验收;路桥集团管理办法;路桥集团验收条件;四川路桥集团验收;四川路桥集团管理办法;四川路桥集团专项施工方案
+9,acceptance,验收要求,1,Standards,验收标准,国家验收规范(如《公路工程质量检验评定标准》JTG F80/1-2017、《桥梁工程施工质量验收标准》GB 50205-2020)、行业标准(如《公路桥涵施工技术规范》JTG_T 3650-2024)、企业管理办法(如《四川路桥施工验收管理办法》《路桥集团专项施工方案验收条件》)、操作规程(如《桥梁施工安全操作规程》),5,managementRegulationsOfBridgeCompany,桥梁公司的管理办法,需关联桥梁施工管理办法(如《桥梁施工安全操作规程》)、体现桥梁施工重点 focus:和桥梁施工标准管理要求;,桥梁公司管理办法;桥梁施工管理;桥梁安全操作规程;桥梁公司验收;四川路桥桥梁公司管理办法;四川路桥桥梁公司验收
+9,acceptance,验收要求,2,Procedure,验收程序,进场验收(材料/设备进场检验)、过程验收(工序/隐蔽工程验收)、阶段验收(基础/主体/装饰阶段验收)、完工验收(工程竣工预验收),1,OnsiteAcceptance,进场验收,需明确验收对象(如“钢筋进场验收”“塔式起重机进场验收”)、是质量控制的第一道防线;,进场验收;材料进场;设备进场验收;进场检验
+9,acceptance,验收要求,2,Procedure,验收程序,进场验收(材料/设备进场检验)、过程验收(工序/隐蔽工程验收)、阶段验收(基础/主体/装饰阶段验收)、完工验收(工程竣工预验收),2,ProcessAcceptance,过程验收,需关联施工工序(如“混凝土浇筑过程验收”“钢筋绑扎过程验收”)、强调动态管控;,过程验收;施工过程验收;工序验收;隐蔽工程验收
+9,acceptance,验收要求,2,Procedure,验收程序,进场验收(材料/设备进场检验)、过程验收(工序/隐蔽工程验收)、阶段验收(基础/主体/装饰阶段验收)、完工验收(工程竣工预验收),3,StageAcceptance,阶段验收,需对应工程阶段(如“基础工程阶段验收”“主体结构阶段验收”)、是阶段性成果确认的关键;,阶段验收;基础工程验收;主体验收;结构验收;阶段性验收
+9,acceptance,验收要求,2,Procedure,验收程序,进场验收(材料/设备进场检验)、过程验收(工序/隐蔽工程验收)、阶段验收(基础/主体/装饰阶段验收)、完工验收(工程竣工预验收),4,CompletionAcceptance,完工验收,需明确验收内容(如“工程竣工预验收”“专项施工方案完工验收”)、是竣工验收的前提。,完工验收;竣工验收;竣工预验收;完工后验收
+9,acceptance,验收要求,3,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),1,SafetyProductionConditionAcceptance,安全生产条件验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、需细化具体内容(如“安全防护设施验收”包括“安全网张挂验收”“防护栏杆安装验收”)、避免“安全生产验收”等泛化表述;,安全生产条件验收;安全验收;安全防护验收;临时用电验收
+9,acceptance,验收要求,3,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),2,ResourceAllocationAcceptance,资源配置验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、需关联资源类型(如“人员配置验收”包括“特种作业人员资质验收”“管理人员到位验收”)、体现资源的针对性;,资源配置验收;人员配置验收;设备配置验收;资源验收
+9,acceptance,验收要求,3,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),3,ConstructionProcessAcceptance,施工工艺验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、需明确工艺环节(如“模板安装工艺验收”包括“模板垂直度验收”“模板拼接缝验收”)、强调工艺的标准化;,施工工艺验收;模板安装验收;工艺验收;施工技术验收
+9,acceptance,验收要求,3,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),4,AcceptanceOfMechanicalEquipment,机械设备验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、需指向具体设备(如“塔式起重机验收”包括“设备型号验收”“安全装置验收”)、确保设备符合施工要求。,机械设备验收;设备验收;起重机验收;机械验收
+9,acceptance,验收要求,3,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),5,TemporarySupportStructure,临时支撑结构验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、专项施工方案及审批记录、技术交底记录、构配件质量证明文件(合格证/检测报告)、地基承载力报告、搭设过程检查记录、荷载试验报告(高支模/大跨度)、验收记录表(含实测数据/影像资料)、整改复查记录。,临时支撑验收;脚手架验收;满堂支架验收;支架验收;临时结构验收
+9,acceptance,验收要求,3,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),6,PersonnelOperationPlatform,人员操作平台验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、架体材质(如钢管无裂纹、弯曲、型钢无开焊);架体构造(立杆间距、剪刀撑设置、连墙件固定);稳定性(移动式平台刹车装置、落地式平台基础坚实度);荷载限制(平台荷载不超过设计值、悬挂限载标志);防护栏杆(高度≥1.2m、竖向栏杆间距≤1.5m、底部设挡脚板);平台铺板(满铺、固定、无空隙);登高扶梯(防滑、固定、与平台连接牢固);安全网(平台周边设置密目网或安全平网)。,操作平台验收;人员平台;高空作业平台验收;施工平台验收
+9,acceptance,验收要求,3,Content,验收内容,安全生产条件验收(安全防护设施验收、临时用电验收)、资源配置验收(人员配置验收、设备配置验收)、施工工艺验收(模板安装工艺验收、混凝土浇筑工艺验收)、机械设备验收(塔式起重机验收、混凝土泵车验收)、临时支撑结构验收(脚手架验收、满堂支架验收)、人员操作平台验收(高空作业平台验收、操作脚手架验收)、安全防护设施验收(安全网验收、防护栏杆验收),7,SafetyProtectionFacilities,安全防护措施验收,验收表格中要明确项目的具体验收标准、验收标准应尽量量化到具体参数或标准、需结合场景需求(如建筑施工中的基坑临边防护、电梯井防护门)、功能定位(如预防事故的防护栏杆、减少事故影响的安全网)、技术要求(如材质、构造、固定方式)。其核心逻辑是“隔离危险、承接冲击、提醒注意”,安全防护验收;防护设施验收;安全网验收;防护栏杆验收
+9,acceptance,验收要求,4,Timing,验收时间,专项施工方案验收时间(如《专项施工方案验收条件一览表》预估时间)、验收时间调整(根据实际进度调整验收时间)、验收条件触发时间(具备验收条件后15日内组织验收),1,AcceptanceTimeOfSpecialConstructionPlan,专项施工方案验收时间,需关联具体表格(如“《专项施工方案验收条件一览表》预估时间”)、体现时间的可追溯性;,验收时间;专项方案验收时间;验收时间表;预估验收时间
+9,acceptance,验收要求,4,Timing,验收时间,专项施工方案验收时间(如《专项施工方案验收条件一览表》预估时间)、验收时间调整(根据实际进度调整验收时间)、验收条件触发时间(具备验收条件后16日内组织验收),2,AcceptanceTimeAdjustment,验收时间调整,需明确调整依据(如“根据施工进度调整验收时间”)、避免“时间调整”等泛化表述;,验收时间调整;进度调整验收;时间调整说明
+9,acceptance,验收要求,4,Timing,验收时间,专项施工方案验收时间(如《专项施工方案验收条件一览表》预估时间)、验收时间调整(根据实际进度调整验收时间)、验收条件触发时间(具备验收条件后17日内组织验收),3,AcceptanceConditionTriggerTime,验收条件触发时间,需明确时间节点(如“具备验收条件后15日内组织验收”)、强调时效性。,验收条件触发;15日内;具备验收条件;组织验收时间
+9,acceptance,验收要求,5,Personnel,验收人员,建设单位验收人员(如建设单位项目负责人、建设单位技术负责人)、设计单位验收人员(如设计单位项目负责人、设计单位专业工程师)、施工单位验收人员(如施工单位项目经理、施工单位技术负责人)、监理单位验收人员(如总监理工程师、专业监理工程师)、监测单位验收人员(如监测项目负责人、监测技术员),1,AcceptancePersonnelOfTheConstructionUnit,建设单位验收人员,需明确具体角色(如“建设单位项目负责人”)、避免“建设单位人员”等泛化表述;由施工作业班组在施工过程中自行对照方案自检、施工完成后由方案编制负责人、项目经理、项目副经理、项目技术负责人、安全环保处、工程处、机料处、合同处、专业分包单位(协作队伍)项目负责人和项目技术负责人等部门人员参加方案验收。,建设单位验收人员;业主验收;建设单位项目负责人;甲方验收
+9,acceptance,验收要求,5,Personnel,验收人员,建设单位验收人员(如建设单位项目负责人、建设单位技术负责人)、设计单位验收人员(如设计单位项目负责人、设计单位专业工程师)、施工单位验收人员(如施工单位项目经理、施工单位技术负责人)、监理单位验收人员(如总监理工程师、专业监理工程师)、监测单位验收人员(如监测项目负责人、监测技术员),2,DesignUnitAcceptancePersonnel,设计单位验收人员,需明确验收人员姓名、关联专业(如“设计单位专业工程师”)、体现设计的专业性;由施工作业班组在施工过程中自行对照方案自检、施工完成后由方案编制负责人、项目经理、项目副经理、项目技术负责人、安全环保处、工程处、机料处、合同处、专业分包单位(协作队伍)项目负责人和项目技术负责人等部门人员参加方案验收。,设计单位验收;设计单位人员;设计师验收;设计验收
+9,acceptance,验收要求,5,Personnel,验收人员,建设单位验收人员(如建设单位项目负责人、建设单位技术负责人)、设计单位验收人员(如设计单位项目负责人、设计单位专业工程师)、施工单位验收人员(如施工单位项目经理、施工单位技术负责人)、监理单位验收人员(如总监理工程师、专业监理工程师)、监测单位验收人员(如监测项目负责人、监测技术员),3,ConstructionUnitAcceptancePersonnel,施工单位验收人员,需明确验收人员姓名、指向管理岗位(如“施工单位项目经理”“施工单位技术负责人”)、强调施工单位的主体责任;由施工作业班组在施工过程中自行对照方案自检、施工完成后由方案编制负责人、项目经理、项目副经理、项目技术负责人、安全环保处、工程处、机料处、合同处、专业分包单位(协作队伍)项目负责人和项目技术负责人等部门人员参加方案验收。,施工单位验收;施工方验收;施工单位项目经理;施工验收人员
+9,acceptance,验收要求,5,Personnel,验收人员,建设单位验收人员(如建设单位项目负责人、建设单位技术负责人)、设计单位验收人员(如设计单位项目负责人、设计单位专业工程师)、施工单位验收人员(如施工单位项目经理、施工单位技术负责人)、监理单位验收人员(如总监理工程师、专业监理工程师)、监测单位验收人员(如监测项目负责人、监测技术员),4,InspectionPersonnelOfTheSupervisionUnit,监理单位验收人员,需明确验收人员姓名、监理角色(如“总监理工程师”“专业监理工程师”)、体现监理的监督职责;由施工作业班组在施工过程中自行对照方案自检、施工完成后由方案编制负责人、项目经理、项目副经理、项目技术负责人、安全环保处、工程处、机料处、合同处、专业分包单位(协作队伍)项目负责人和项目技术负责人等部门人员参加方案验收。,监理单位验收;总监理工程师;监理人员;监理验收
+9,acceptance,验收要求,5,Personnel,验收人员,建设单位验收人员(如建设单位项目负责人、建设单位技术负责人)、设计单位验收人员(如设计单位项目负责人、设计单位专业工程师)、施工单位验收人员(如施工单位项目经理、施工单位技术负责人)、监理单位验收人员(如总监理工程师、专业监理工程师)、监测单位验收人员(如监测项目负责人、监测技术员),5,MonitoringUnitAcceptancePersonnel,监测单位验收人员,需明确验收人员姓名、关联监测内容(如“监测项目负责人”“监测技术员”)、确保监测数据的准确性;由施工作业班组在施工过程中自行对照方案自检、施工完成后由方案编制负责人、项目经理、项目副经理、项目技术负责人、安全环保处、工程处、机料处、合同处、专业分包单位(协作队伍)项目负责人和项目技术负责人等部门人员参加方案验收。,监测单位验收;监测人员;监测项目负责人;监测验收
+10,other,其它资料,1,Calculations,计算书,编制依据、工程简况、方案简述、设计参数、主要工况计算、局部计算、结论及建议、应力分析结果、变形分析结果、反力分析结果、屈曲分析结果,1,ContentRequirements,内容要求,专项施工方案中包含承重结构、重要临时设施、设备选型、吊绳吊具受力计算;地基承载力等工作内容编制的专项计算书、内容应包含编制依据、工程简况、方案简述、设计参数、主要工况计算、局部计算、结论及建议。,计算书;编制依据;设计参数;主要工况;计算内容;结构计算
+10,other,其它资料,1,Calculations,计算书,编制依据、工程简况、方案简述、设计参数、主要工况计算、局部计算、结论及建议、应力分析结果、变形分析结果、反力分析结果、屈曲分析结果,2,CalculationOfMainWorkingConditions,主要工况计算,需针对关键施工工况(如“盖梁浇筑工况”“桩基础施工工况”)、包含“本工况描述”“应力/变形/反力/屈曲分析结果”、是计算书的核心内容;,主要工况计算;工况分析;应力分析;变形分析;反力分析;屈曲分析
+10,other,其它资料,1,Calculations,计算书,编制依据、工程简况、方案简述、设计参数、主要工况计算、局部计算、结论及建议、应力分析结果、变形分析结果、反力分析结果、屈曲分析结果,3,LocalCalculation,局部计算,需对于受力集中、结构复杂的局部重要节点进行细部分析(如“钢管桩与横梁连接节点”“模板支撑体系节点”)、确保结构安全;,局部计算;节点计算;细部计算;局部受力;复杂节点
+10,other,其它资料,2,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,1,OverallLayoutPlan,总体平面布置图,需展示项目整体布局(如“施工便道”“材料堆放区”“临时设施”)、是施工部署的可视化基础;,总体平面布置图;总平面图;施工布局;总体布置
+10,other,其它资料,2,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,2,ConstructionSiteLayoutPlan,施工工点平面布置图,需围绕“空间规划”“功能实现”“安全文明”三大核心、覆盖从边界界定到具体设施的全流程要素。,施工工点平面布置图;工点布置;场地布置图;施工场地平面
+10,other,其它资料,2,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,3,LongitudinalElevationLayoutOfSupportingStructure,支撑结构纵立面布置图,需明确支撑体系(如“钢管桩支架”“满堂脚手架”)的纵向布置(如“桩长”“间距”“标高”)、是结构安全的关键依据;,支撑结构纵立面;纵立面布置;桩长;纵断面图;支撑纵立面
+10,other,其它资料,2,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,4,CrossSectionalLayoutDiagram,支横断面布置图,需围绕“断面类型”“组成要素”“尺寸参数”“坡度设置”“附属设施”五大维度、覆盖道路、桥梁等工程的通用及专业要素。,横断面图;断面布置图;横断面布置;截面图
+10,other,其它资料,2,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,5,FloorPlan,平面布置图,需围绕“空间布局”“功能分区”“施工支持”三大核心、覆盖从区域划分到具体设施的全流程要素。,平面布置图;功能分区;施工区域划分;平面图
+10,other,其它资料,2,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,6,DetailedStructuralDiagram,细部构造图,需细化关键节点(如“模板拼接节点”“支撑体系连接节点”)、标注尺寸、材料及工艺要求、指导现场施工;,细部构造图;节点详图;构造详图;节点图
+10,other,其它资料,2,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,7,FormworkLayoutDrawing,模板布置图,需明确模板的平面位置(如“柱模板”“梁模板”)、尺寸及支撑方式、确保模板安装符合设计要求。,模板布置图;模板平面图;模板位置
+10,other,其它资料,2,Drawings,相关施工图纸,工程专业名词类、设备设施类、量化数值类、数值单位类、结构组件名称类、施工区域名称类,8,TemplateConstructionDiagram,模板构造图,需覆盖模板体系组成、构造细节、支撑系统、节点处理及精度控制等全流程要素。,模板构造图;模板体系图;支撑系统图;模板结构图
+10,other,其它资料,3,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,1,ConstructionScheduleNetworkDiagram,施工进度计划网络图,需用节点表示工序逻辑关系(如“桩基础施工→承台施工→盖梁施工”)、是进度控制的核心工具;,网络图;施工进度网络图;工序网络;双代号网络图
+10,other,其它资料,3,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,2,ConstructionScheduleGanttChart,施工进度计划横道图,围绕“时间维度”“任务要素”“进度关系”“调整控制”四大核心、覆盖从计划编制到动态监控的全流程。,施工进度横道图;横道图附表;进度计划横道;甘特图附表
+10,other,其它资料,3,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,3,HazardAnalysisAndResponseMeasuresTable,危险源分析和应对措施表,需识别施工中的危险源(如“高处坠落”“物体打击”)、制定针对性应对措施(如“设置防护栏杆”“佩戴安全带”)、是安全保障的关键文档;,危险源分析表;应对措施表;危险源表;风险分析表
+10,other,其它资料,3,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,4,ScannedCopyOfTheCertificateOfFulltimSafetymanagementPersonnel,专职安全管理人员证件扫描件,需包含“安全生产考核合格证书”“证书编号”“有效期”、确保管理人员资质符合要求;,安全证件扫描;专职安全考核证;安全证书扫描件
+10,other,其它资料,3,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,5,ScannedCopyOfSpecialOperationsPersonnelsCertificate,特种作业人员证件扫描件,需围绕“信息真实性”“管理规范性”“使用便捷性”三大核心、覆盖证件内容、“证书编号”、“有效期”、法规标准四大维度,特种作业证件扫描;特种证书扫描件;操作证扫描
+10,other,其它资料,3,Tables,附图附表,工序名称类、施工危险因素词汇类、安全设备设施类、资质证照名称类、标准文书类、数值类、时间时期类、数值单位类,6,ScannedCopyOfProfessionalSubcontractorsQualifications,专业分包单位资质扫描件,需包含“营业执照”“资质证书”“安全生产许可证”、确保分包单位具备施工能力。,分包资质扫描;营业执照扫描;资质证书扫描;分包证件
+10,other,其它资料,4,Team,编制及审核人员情况,专项施工方案验收条件一览表、编制人员信息、复核人员信息、审核人员信息、审批人员信息、姓名、职务、职称,1,ListOfAcceptanceConditionsForSpecialConstructionSchemes,专项施工方案验收条件一览表,需明确验收的前提条件(如“计算书完成”“设计图审核通过”)、是验收的流程依据;,验收条件一览表;专项施工方案验收条件;验收前提条件
+10,other,其它资料,4,Team,编制及审核人员情况,专项施工方案验收条件一览表、编制人员信息、复核人员信息、审核人员信息、审批人员信息、姓名、职务、职称,2,PreparePersonnelInformation,编制人员信息,需包含“姓名”“职务”“职称”(如“张三 技术员 助理工程师”)、确保编制人员具备专业能力;,编制人员;编制人信息;方案编制者;编制人
+10,other,其它资料,4,Team,编制及审核人员情况,专项施工方案验收条件一览表、编制人员信息、复核人员信息、审核人员信息、审批人员信息、姓名、职务、职称,3,ReviewerInformation,审核人员信息,需包含“姓名”“职务”“职称”(如“李四 项目技术负责人 工程师”)、确保审核流程的严谨性;,审核人员;复核人员;审核信息;审核人;复核人
+10,other,其它资料,4,Team,编制及审核人员情况,专项施工方案验收条件一览表、编制人员信息、复核人员信息、审核人员信息、审批人员信息、姓名、职务、职称,4,ApprovalPersonnelInformation,审批人员信息,需包含“姓名”“职务”“职称”(如“王五 项目经理 高级工程师”)、确保方案符合项目整体要求,审批人员;批准人;审批信息;审批签字;项目经理审批

+ 0 - 17
core/construction_review/component/doc_worker/docx_worker/__init__.py

@@ -1,17 +0,0 @@
-"""
-DOCX 文档处理模块
-
-提供 DOCX 文件的目录提取、全文提取、文本切分等功能。
-"""
-
-from .pipeline import DocxPipeline
-from .toc_extractor import DocxTOCExtractor
-from .full_text_extractor import DocxFullTextExtractor
-from .text_splitter import DocxTextSplitter
-
-__all__ = [
-    "DocxPipeline",
-    "DocxTOCExtractor",
-    "DocxFullTextExtractor",
-    "DocxTextSplitter",
-]

+ 0 - 118
core/construction_review/component/doc_worker/docx_worker/cli.py

@@ -1,118 +0,0 @@
-"""
-DOCX 处理命令行接口
-
-用法示例:
-  python -m file_parse.docx_worker.cli input.docx
-  python -m file_parse.docx_worker.cli input.docx -l 1 --max-size 3000 --min-size 50
-  python -m file_parse.docx_worker.cli input.docx -o ./output
-"""
-
-import argparse
-import json
-import sys
-from datetime import datetime
-from pathlib import Path
-
-from ..interfaces import DocumentSource
-from .pipeline import DocxPipeline
-
-
-def main():
-    parser = argparse.ArgumentParser(description="DOCX 文档处理工具")
-    parser.add_argument("docx_path", help="输入 DOCX 文件路径")
-    parser.add_argument(
-        "-l", "--level",
-        type=int,
-        help="目标层级(默认从配置读取)"
-    )
-    parser.add_argument(
-        "--max-size",
-        type=int,
-        help="最大块大小(默认从配置读取)"
-    )
-    parser.add_argument(
-        "--min-size",
-        type=int,
-        help="最小块大小(默认从配置读取)"
-    )
-    parser.add_argument(
-        "-o", "--output",
-        help="输出目录(默认为 ./output)"
-    )
-    
-    args = parser.parse_args()
-
-    # 检查文件是否存在
-    docx_path = Path(args.docx_path)
-    if not docx_path.exists():
-        print(f"错误:文件不存在 -> {docx_path}", file=sys.stderr)
-        sys.exit(1)
-
-    # 创建输出目录
-    output_dir = Path(args.output) if args.output else Path("./output")
-    output_dir.mkdir(parents=True, exist_ok=True)
-
-    # 创建文档源
-    source = DocumentSource(path=docx_path, file_type="docx")
-
-    # 运行处理流程
-    try:
-        pipeline = DocxPipeline()
-        result = pipeline.run(
-            source,
-            target_level=args.level,
-            max_chunk_size=args.max_size,
-            min_chunk_size=args.min_size,
-        )
-    except Exception as e:
-        print(f"处理失败:{e}", file=sys.stderr)
-        import traceback
-        traceback.print_exc()
-        sys.exit(1)
-
-    # 生成输出文件名
-    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-    base_name = docx_path.stem
-    output_file = output_dir / f"{base_name}_完整结果_{timestamp}.json"
-
-    # 构建完整输出结构
-    output_data = {
-        "source_file": str(docx_path.absolute()),
-        "process_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
-        "toc_summary": {
-            "total_items": result["toc_info"]["toc_count"],
-            "toc_pages": result["toc_info"]["toc_pages"],
-        },
-        "complete_toc_list": [
-            {
-                "index": i + 1,
-                "title": item["title"],
-                "page": item["page"],
-                "level": item["level"],
-                "original": item["original"],
-            }
-            for i, item in enumerate(result["toc_info"]["toc_items"])
-        ],
-        "classification_summary": {
-            "target_level": result["meta"]["target_level"],
-            "total_count": result["classification"]["total_count"],
-            "categories": result["classification"].get("category_stats", {}),
-        },
-        "classified_items": result["classification"]["items"],
-        "chunks": result["chunks"],
-        "meta": result["meta"],
-    }
-
-    # 写入文件
-    with output_file.open("w", encoding="utf-8") as f:
-        json.dump(output_data, f, ensure_ascii=False, indent=2)
-
-    print(f"\n处理完成!")
-    print(f"  输出文件: {output_file}")
-    print(f"  目录项数: {result['toc_info']['toc_count']}")
-    print(f"  分类项数: {result['classification']['total_count']}")
-    print(f"  文本块数: {len(result['chunks'])}")
-
-
-if __name__ == "__main__":
-    main()

+ 0 - 110
core/construction_review/component/doc_worker/docx_worker/full_text_extractor.py

@@ -1,110 +0,0 @@
-"""
-DOCX 全文提取实现
-
-提取 DOCX 文档的全文内容,按段落组织,模拟分页。
-"""
-
-from __future__ import annotations
-
-import re
-from io import BytesIO
-from pathlib import Path
-from typing import Any, Dict, List
-
-from docx import Document
-
-from ..interfaces import FullTextExtractor, DocumentSource
-
-
-class DocxFullTextExtractor(FullTextExtractor):
-    """DOCX 全文提取器"""
-
-    def __init__(self, paragraphs_per_page: int = 30):
-        """
-        初始化
-        
-        Args:
-            paragraphs_per_page: 每页段落数(用于模拟分页)
-        """
-        self.paragraphs_per_page = paragraphs_per_page
-
-    def extract_full_text(self, source: DocumentSource) -> List[Dict[str, Any]]:
-        """
-        提取 DOCX 文档的全文内容
-        
-        返回结构:
-        [
-            {
-                "page_num": int,
-                "text": str,
-                "start_pos": int,
-                "end_pos": int,
-                "source_file": str,
-            },
-            ...
-        ]
-        """
-        # 加载文档
-        if source.path:
-            doc = Document(source.path)
-            source_file = str(source.path)
-        elif source.content:
-            doc = Document(BytesIO(source.content))
-            source_file = "bytes_stream"
-        else:
-            raise ValueError("DocumentSource 必须提供 path 或 content")
-
-        # 按照文档中的实际顺序提取段落和表格
-        # 创建段落和表格的元素到对象的映射
-        para_map = {para._element: para for para in doc.paragraphs}
-        table_map = {table._element: table for table in doc.tables}
-        
-        # 按照文档中的顺序遍历所有元素
-        all_elements = []
-        for element in doc.element.body:
-            if element in para_map:
-                # 段落元素
-                para = para_map[element]
-                text = para.text
-                # 过滤目录行:标题\t页码(页码部分支持带修饰符号)
-                # 匹配从开头开始,包含制表符且末尾有数字的模式(目录行特征)
-                if text and not re.match(r"^.+\t+.*?\d+.*?\s*$", text):
-                    all_elements.append(text)
-            elif element in table_map:
-                # 表格元素
-                table = table_map[element]
-                table_text = self._extract_table_text(table)
-                all_elements.append(table_text)
-
-        # 模拟分页:每 N 个元素作为一页
-        pages_content = []
-        current_pos = 0
-        
-        # 正则表达式:匹配 [表格开始]...任意内容...[表格结束] 模式
-        table_placeholder_pattern = re.compile(
-            r'\n?\[表格开始\]\n.*?\n\[表格结束\]\n?',
-            re.DOTALL
-        )
-        
-        for page_num in range(0, len(all_elements), self.paragraphs_per_page):
-            page_elements = all_elements[page_num:page_num + self.paragraphs_per_page]
-            page_text = "\n".join(page_elements)
-            
-            # 将任何可能存在的 [表格开始]...表格内容...[表格结束] 替换为占位符
-            page_text = table_placeholder_pattern.sub('\n<表格></表格>\n', page_text)
-            
-            pages_content.append({
-                "page_num": page_num // self.paragraphs_per_page + 1,
-                "text": page_text,
-                "start_pos": current_pos,
-                "end_pos": current_pos + len(page_text),
-                "source_file": source_file,
-            })
-            
-            current_pos += len(page_text)
-
-        return pages_content
-
-    def _extract_table_text(self, table) -> str:
-        """提取表格占位符,不提取实际内容"""
-        return "\n<表格></表格>\n"

+ 0 - 106
core/construction_review/component/doc_worker/docx_worker/pipeline.py

@@ -1,106 +0,0 @@
-"""
-DOCX 文档处理流程
-
-整合目录提取、分类、全文提取、文本切分等步骤。
-"""
-
-from __future__ import annotations
-
-from pathlib import Path
-from typing import Any, Dict, Optional
-
-from ..interfaces import DocumentPipeline, DocumentSource
-from ..config.provider import default_config_provider
-from ..classification.hierarchy_classifier import HierarchyClassifier
-
-from .toc_extractor import DocxTOCExtractor
-from .full_text_extractor import DocxFullTextExtractor
-from .text_splitter import DocxTextSplitter
-
-
-class DocxPipeline(DocumentPipeline):
-    """DOCX 文档处理流水线"""
-
-    def __init__(self):
-        self._cfg = default_config_provider
-        self._toc_extractor = DocxTOCExtractor()
-        self._full_text_extractor = DocxFullTextExtractor(
-            paragraphs_per_page=int(self._cfg.get("toc_extraction.paragraphs_per_page", 30))
-        )
-        self._text_splitter = DocxTextSplitter()
-        self._classifier = HierarchyClassifier()
-
-    def run(
-        self,
-        source: DocumentSource,
-        target_level: Optional[int] = None,
-        max_chunk_size: Optional[int] = None,
-        min_chunk_size: Optional[int] = None,
-    ) -> Dict[str, Any]:
-        """
-        运行完整流程
-        
-        返回:
-        {
-            "toc_info": {...},
-            "classification": {...},
-            "chunks": [...],
-            "meta": {...},
-        }
-        """
-        # 从配置获取默认值
-        if target_level is None:
-            target_level = int(self._cfg.get("text_splitting.target_level", 1))
-        if max_chunk_size is None:
-            max_chunk_size = int(self._cfg.get("text_splitting.max_chunk_size", 3000))
-        if min_chunk_size is None:
-            min_chunk_size = int(self._cfg.get("text_splitting.min_chunk_size", 50))
-
-        print(f"开始处理 DOCX 文档...")
-        print(f"  目标层级: {target_level}")
-        print(f"  最大块大小: {max_chunk_size}")
-        print(f"  最小块大小: {min_chunk_size}")
-
-        # 步骤1: 提取目录
-        print("\n步骤1: 提取目录...")
-        toc_info = self._toc_extractor.extract_toc(source)
-        print(f"  提取到 {toc_info['toc_count']} 个目录项")
-
-        # 步骤2: 分类目录项
-        print("\n步骤2: 分类目录项...")
-        classification = self._classifier.classify(toc_info["toc_items"], target_level)
-        print(f"  分类完成,共 {classification['total_count']} 个目标层级项")
-
-        # 步骤3: 提取全文
-        print("\n步骤3: 提取全文...")
-        pages_content = self._full_text_extractor.extract_full_text(source)
-        print(f"  提取到 {len(pages_content)} 页内容")
-
-        # 步骤4: 切分文本
-        print("\n步骤4: 切分文本...")
-        chunks = self._text_splitter.split_by_hierarchy(
-            classification["items"],
-            pages_content,
-            toc_info,
-            target_level,
-            max_chunk_size,
-            min_chunk_size,
-        )
-        print(f"  切分完成,共 {len(chunks)} 个块")
-
-        # 填充文件名
-        file_name = Path(source.path).name if source.path else "unknown.docx"
-        for chunk in chunks:
-            chunk["file_name"] = file_name
-
-        return {
-            "toc_info": toc_info,
-            "classification": classification,
-            "chunks": chunks,
-            "meta": {
-                "target_level": target_level,
-                "max_chunk_size": max_chunk_size,
-                "min_chunk_size": min_chunk_size,
-                "file_type": "docx",
-            },
-        }

+ 0 - 327
core/construction_review/component/doc_worker/docx_worker/text_splitter.py

@@ -1,327 +0,0 @@
-"""
-DOCX 文本切分实现
-
-复刻 PDF 处理的切分逻辑:
-1. 跳过目录页,只在正文中定位章节标题
-2. 按最低目录层级进行切分,形成章节块
-3. 对超过最大字符数的块按段落-句子进行再次切分,保持语义完整性
-"""
-
-from __future__ import annotations
-
-from typing import Any, Dict, List
-
-from ..config.provider import default_config_provider
-from ..interfaces import TextSplitter
-from ..utils.title_matcher import TitleMatcher
-from ..utils.text_split_support import HierarchicalChunkMixin
-
-
-class DocxTextSplitter(TextSplitter, HierarchicalChunkMixin):
-    """按目录层级对 DOCX 正文进行智能分块的实现"""
-
-    def __init__(self) -> None:
-        self._cfg = default_config_provider
-        self._title_matcher = TitleMatcher()
-
-    def split_by_hierarchy(
-        self,
-        classification_items: List[Dict[str, Any]],
-        pages_content: List[Dict[str, Any]],
-        toc_info: Dict[str, Any],
-        target_level: int,
-        max_chunk_size: int,
-        min_chunk_size: int,
-    ) -> List[Dict[str, Any]]:
-        """
-        按目录层级和字符数智能切分文本
-        
-        逻辑与 PDF 处理完全一致
-        """
-        toc_pages = toc_info.get("toc_pages", []) or []
-        all_toc_items = toc_info.get("toc_items", [])
-        
-        # 使用完整全文
-        full_text = "".join(p.get("text", "") for p in pages_content)
-
-        print(f"  正在定位{len(classification_items)}个已分类的标题...")
-        print(f"  目录所在页: {toc_pages}")
-
-        # 步骤1: 在正文中定位已分类的标题(跳过目录页)
-        located = self._title_matcher.find_title_positions(
-            classification_items, full_text, pages_content, toc_pages
-        )
-        
-        # 只保留成功定位的标题
-        found_titles = [t for t in located if t["found"]]
-        if not found_titles:
-            print(f"  错误: 未能在正文中定位任何标题")
-            return []
-
-        print(f"  成功定位 {len(found_titles)}/{len(classification_items)} 个标题")
-        
-        # 按位置排序
-        found_titles.sort(key=lambda x: x["position"])
-
-        # 步骤2: 构建一级目录标题到分类信息的映射
-        chapter_classification_map: Dict[str, Dict[str, Any]] = {}
-        for item in classification_items:
-            if item.get("level") == 1:
-                chapter_title = item.get("title", "")
-                chapter_classification_map[chapter_title] = {
-                    "category": item.get("category", ""),
-                    "category_code": item.get("category_code", "other"),
-                    "page": item.get("page", ""),
-                    "level": item.get("level", 1),
-                }
-
-        # 步骤3: 为每个找到的标题构建完整的层级路径
-        for title_info in found_titles:
-            hierarchy_path = self._build_hierarchy_path(
-                title_info["title"], all_toc_items, target_level
-            )
-            title_info["hierarchy_path"] = hierarchy_path
-
-        # 步骤4: 按目录层级处理每个标题块
-        all_chunks: List[Dict[str, Any]] = []
-        
-        for i, title_info in enumerate(found_titles):
-            start_pos = title_info["position"]
-            
-            # 确定正文块的结束位置(下一个同级标题的位置)
-            if i + 1 < len(found_titles):
-                end_pos = found_titles[i + 1]["position"]
-            else:
-                end_pos = len(full_text)
-            
-            # 提取正文块
-            content_block = full_text[start_pos:end_pos]
-            
-            # 在正文块中查找子标题(按最低层级切分)
-            sub_chunks = self._split_by_sub_titles(
-                content_block,
-                all_toc_items,
-                title_info,
-                target_level,
-                max_chunk_size,
-                min_chunk_size,
-            )
-            
-            # 为每个子块添加元数据
-            for j, sub_chunk in enumerate(sub_chunks, 1):
-                chunk_data = self._build_chunk_metadata(
-                    sub_chunk, title_info, start_pos, pages_content, i, j, chapter_classification_map
-                )
-                all_chunks.append(chunk_data)
-
-        # 步骤4: 生成最终的chunk_id和serial_number
-        final_chunks = self._finalize_chunk_ids(all_chunks)
-
-        print(f"  初始切分: {len(all_chunks)} 个块")
-        print(f"  最终块数: {len(final_chunks)} 个块")
-
-        return final_chunks
-
-    def _split_by_sub_titles(
-        self,
-        content_block: str,
-        all_toc_items: List[Dict[str, Any]],
-        parent_title_info: Dict[str, Any],
-        target_level: int,
-        max_chunk_size: int,
-        min_chunk_size: int,
-    ) -> List[Dict[str, Any]]:
-        """
-        在正文块中按子标题进行切分(按照toc_items的顺序和层级关系)
-        
-        核心逻辑:
-        1. 查找所有层级的子标题(不限于直接子标题)
-        2. 按位置排序后,两个相邻子标题之间的内容作为一个块
-        3. 只有当块超过 max_chunk_size 时才按句子切分
-        """
-        # 找到父标题在toc_items中的位置
-        parent_title = parent_title_info["title"]
-        parent_idx = -1
-        parent_level = target_level
-        
-        for idx, toc_item in enumerate(all_toc_items):
-            if toc_item["title"] == parent_title:
-                parent_idx = idx
-                parent_level = toc_item.get("level", target_level)
-                break
-
-        if parent_idx < 0:
-            # 如果找不到父标题,将整个正文块作为一个块
-            if len(content_block) > max_chunk_size:
-                return self._split_large_chunk(content_block, max_chunk_size, parent_title, [])
-            else:
-                return [
-                    {
-                        "content": content_block,
-                        "relative_start": 0,
-                        "sub_title": "",
-                        "hierarchy_path": parent_title_info.get("hierarchy_path", [parent_title]),
-                    }
-                ]
-
-        # 找到下一个同级或更高级标题的位置(确定父标题的范围)
-        next_sibling_idx = len(all_toc_items)
-        for idx in range(parent_idx + 1, len(all_toc_items)):
-            item = all_toc_items[idx]
-            if item.get("level", 1) <= parent_level:
-                next_sibling_idx = idx
-                break
-
-        # 查找所有子标题(所有 level > parent_level 的标题)
-        # 这是关键:不限于直接子标题,而是所有更深层级的标题
-        all_sub_titles = []
-        fuzzy_threshold = float(self._cfg.get("text_splitting.fuzzy_threshold", 0.8))
-
-        for idx in range(parent_idx + 1, next_sibling_idx):
-            toc_item = all_toc_items[idx]
-            item_level = toc_item.get("level", 1)
-            
-            # 查找所有更深层级的子标题
-            if item_level > parent_level:
-                # 在正文块中查找这个子标题
-                pos = self._find_title_in_block(
-                    toc_item["title"], content_block, fuzzy_threshold
-                )
-                if pos >= 0:
-                    # 调试:显示找到的标题及其周围内容
-                    context_start = max(0, pos - 20)
-                    context_end = min(len(content_block), pos + len(toc_item["title"]) + 50)
-                    context = content_block[context_start:context_end].replace("\n", " ")
-                    print(f"        找到子标题: {toc_item['title']} (level={item_level}), 位置={pos}, 上下文: ...{context}...")
-                    
-                    all_sub_titles.append(
-                        {
-                            "title": toc_item["title"],
-                            "level": toc_item["level"],
-                            "position": pos,
-                            "toc_index": idx,
-                            "toc_item": toc_item,
-                        }
-                    )
-
-        # 按位置排序
-        all_sub_titles.sort(key=lambda x: x["position"])
-
-        # 如果没有找到任何子标题,将整个正文块作为一个块
-        if not all_sub_titles:
-            if len(content_block) > max_chunk_size:
-                return self._split_large_chunk(
-                    content_block, max_chunk_size, parent_title, 
-                    parent_title_info.get("hierarchy_path", [parent_title])
-                )
-            else:
-                return [
-                    {
-                        "content": content_block,
-                        "relative_start": 0,
-                        "sub_title": "",
-                        "hierarchy_path": parent_title_info.get("hierarchy_path", [parent_title]),
-                    }
-                ]
-
-        # 找到直接子标题(parent_level + 1)和所有更深层级的标题
-        direct_child_level = parent_level + 1
-        direct_child_titles = [sub for sub in all_sub_titles if sub["level"] == direct_child_level]
-        
-        # 找到最低层级(用于判断哪些是最底层的标题)
-        max_level = max(sub["level"] for sub in all_sub_titles) if all_sub_titles else parent_level
-        
-        print(f"      父标题: {parent_title}, 找到 {len(all_sub_titles)} 个子标题, 直接子标题数: {len(direct_child_titles)}, 最低层级: {max_level}")
-
-        # 如果没有直接子标题,但有更深层级的标题,使用最低层级标题切分(保持向后兼容)
-        if not direct_child_titles and all_sub_titles:
-            lowest_level_titles = [sub for sub in all_sub_titles if sub["level"] == max_level]
-            print(f"      没有直接子标题,使用最低层级标题切分: {len(lowest_level_titles)} 个")
-            direct_child_titles = lowest_level_titles
-
-        # 按直接子标题切分(如果存在)
-        chunks = []
-        if direct_child_titles:
-            for i, sub_title in enumerate(direct_child_titles):
-                start_pos = sub_title["position"]
-
-                # 确定结束位置(下一个同级或更高级标题的位置)
-                # 在 all_sub_titles 中查找下一个位置大于当前标题,且 level <= direct_child_level 的标题
-                end_pos = len(content_block)
-                for next_sub in all_sub_titles:
-                    if next_sub["position"] > start_pos and next_sub["level"] <= direct_child_level:
-                        end_pos = next_sub["position"]
-                        break
-
-                chunk_content = content_block[start_pos:end_pos]
-                
-                # 调试信息
-                content_preview = chunk_content[:100].replace("\n", " ")
-                print(f"        切分块 {i+1}: {sub_title['title']} (level={sub_title['level']}), 位置: {start_pos}-{end_pos}, 长度: {len(chunk_content)}, 预览: {content_preview}...")
-
-                # 检查子标题是否有实际正文内容
-                title_len = len(sub_title["title"])
-                content_after_title = chunk_content[title_len:].strip()
-
-                if not content_after_title or len(content_after_title) < 10:
-                    print(f"        跳过(内容不足)")
-                    continue
-
-                # 构建层级路径
-                hierarchy_path = self._build_hierarchy_path_for_subtitle(
-                    sub_title["toc_item"], all_toc_items, parent_title_info
-                )
-
-                # 只有当块超过 max_chunk_size 时才按句子切分
-                if len(chunk_content) > max_chunk_size:
-                    print(f"        块过大,按句子切分")
-                    split_chunks = self._split_large_chunk(
-                        chunk_content, max_chunk_size, sub_title["title"], hierarchy_path
-                    )
-                    for split_chunk in split_chunks:
-                        split_chunk["relative_start"] = start_pos + split_chunk["relative_start"]
-                        split_chunk["sub_title"] = sub_title["title"]
-                        if "hierarchy_path" not in split_chunk:
-                            split_chunk["hierarchy_path"] = hierarchy_path
-                        chunks.append(split_chunk)
-                else:
-                    # 直接作为一个块
-                    chunks.append(
-                        {
-                            "content": chunk_content,
-                            "relative_start": start_pos,
-                            "sub_title": sub_title["title"],
-                            "hierarchy_path": hierarchy_path,
-                        }
-                    )
-
-        # 如果所有子标题都没有正文内容,返回整个正文块
-        if not chunks:
-            if len(content_block) > max_chunk_size:
-                return self._split_large_chunk(
-                    content_block, max_chunk_size, parent_title,
-                    parent_title_info.get("hierarchy_path", [parent_title])
-                )
-            else:
-                return [
-                    {
-                        "content": content_block,
-                        "relative_start": 0,
-                        "sub_title": "",
-                        "hierarchy_path": parent_title_info.get("hierarchy_path", [parent_title]),
-                    }
-                ]
-
-        return chunks
-
-    def _find_title_in_block(self, title: str, block: str, fuzzy_threshold: float) -> int:
-        """在文本块中查找标题位置(简化版)"""
-        # 直接使用 TitleMatcher 的方法
-        return self._title_matcher._find_title_in_text(title, block, fuzzy_threshold)
-
-    def _get_page_from_pos(self, pos: int, pages_content: List[Dict[str, Any]]) -> int:
-        """根据位置获取页码"""
-        for page in pages_content:
-            if page["start_pos"] <= pos < page["end_pos"]:
-                return int(page["page_num"])
-        return 1

+ 0 - 123
core/construction_review/component/doc_worker/docx_worker/toc_extractor.py

@@ -1,123 +0,0 @@
-"""
-DOCX 目录提取实现
-
-参考 docx_toc_detector.py 的逻辑,识别目录行(标题 + 制表符 + 页码)。
-"""
-
-from __future__ import annotations
-
-import re
-from pathlib import Path
-from typing import Any, Dict, List
-
-from docx import Document
-
-from ..interfaces import TOCExtractor, DocumentSource
-from ..utils.toc_level_identifier import TOCLevelIdentifier
-from ..utils.toc_pattern_matcher import TOCPatternMatcher
-
-
-class DocxTOCExtractor(TOCExtractor):
-    """DOCX 目录提取器"""
-
-    # 目录行模式:标题 + 制表符 + 页码(页码部分支持带修饰符号,如 ‐ 19 ‐)
-    TOC_PATTERN = re.compile(r"^(?P<title>.+?)\t+(?P<page>.*?\d+.*?)\s*$")
-
-    def __init__(self) -> None:
-        """初始化 DOCX 目录提取器"""
-        self._level_identifier = TOCLevelIdentifier()
-        self._page_extractor = TOCPatternMatcher()
-
-    def extract_toc(self, source: DocumentSource) -> Dict[str, Any]:
-        """
-        提取 DOCX 文档的目录信息
-        
-        返回结构:
-        {
-            "toc_items": [{"title": str, "page": int, "level": int, "original": str}, ...],
-            "toc_count": int,
-            "toc_pages": List[int],
-        }
-        """
-        # 加载文档
-        if source.path:
-            doc = Document(source.path)
-        elif source.content:
-            from io import BytesIO
-            doc = Document(BytesIO(source.content))
-        else:
-            raise ValueError("DocumentSource 必须提供 path 或 content")
-
-        # 提取目录行
-        toc_items = []
-        toc_pages_set = set()
-        
-        for para in doc.paragraphs:
-            text = para.text.strip()
-            if "\t" not in text:
-                continue
-            
-            match = self.TOC_PATTERN.match(text)
-            if match:
-                title = match.group("title").strip()
-                page_raw = match.group("page").strip()
-                
-                # 从可能带有修饰符号的页码中提取纯数字
-                page_num_str = self._page_extractor.extract_page_number(page_raw)
-                try:
-                    page = int(page_num_str)
-                except ValueError:
-                    # 如果无法转换为整数,跳过该项
-                    continue
-                
-                # 先不设置层级,后续统一识别
-                toc_items.append({
-                    "title": title,
-                    "page": page,
-                    "original": text,
-                })
-                
-                toc_pages_set.add(page)
-
-        # 估算目录所在页(假设目录在前几页)
-        if toc_items:
-            # 目录页通常是目录项中最小页码之前的页
-            min_content_page = min(item["page"] for item in toc_items)
-            toc_pages = list(range(1, min(min_content_page, 10)))
-        else:
-            toc_pages = []
-
-        # 使用 TOCLevelIdentifier 识别层级(与 doc_worker 保持一致)
-        toc_items = self._level_identifier.identify_levels(toc_items)
-
-        return {
-            "toc_items": toc_items,
-            "toc_count": len(toc_items),
-            "toc_pages": toc_pages,
-        }
-
-    def _detect_level(self, title: str) -> int:
-        """
-        根据标题格式检测层级(已废弃,保留仅用于向后兼容)
-        
-        注意:此方法已不再使用,现在使用 TOCLevelIdentifier 统一识别层级。
-        保留此方法仅用于向后兼容和测试。
-        """
-        # 章节格式
-        if re.match(r"^第[一二三四五六七八九十\d]+章", title):
-            return 1
-        
-        # 中文编号 + 右括号
-        if re.match(r"^[一二三四五六七八九十]+[))]", title):
-            return 2
-        
-        # 数字 + 顿号/句号
-        if re.match(r"^\d+[、..]", title):
-            return 3
-        
-        # 括号数字
-        if re.match(r"^[\((]\d+[\))]", title):
-            return 4
-        
-        # 默认 level 2
-        return 2

+ 0 - 1
core/construction_review/component/doc_worker/docx_worker/命令

@@ -1 +0,0 @@
-python -m file_parse.docx_worker.cli ".\路桥\47_四川川交路桥有限责任公司会理至禄劝(四川境)高速公路项目土建项目ZCB1-3合同段项目经理部.docx" -l 1 --max-size 3000 --min-size 50 -o ./output

+ 1 - 1
core/construction_review/component/doc_worker/utils/text_split_support.py

@@ -114,7 +114,7 @@ class HierarchicalChunkMixin:
     """
     分级目录切分的通用工具 Mixin。
 
-    把原先 `PdfTextSplitter` / `DocxTextSplitter` 中完全相同的
+    把原先 `PdfTextSplitter` 中完全相同的
     chunk 元数据构造、层级路径、编号提取等方法抽到这里,
     便于多种 worker 复用。
     """

+ 0 - 10
core/construction_review/component/doc_worker/命令

@@ -1,10 +0,0 @@
-python -m file_parse.docx_worker.cli ".\路桥\47_四川川交路桥有限责任公司会理至禄劝(四川境)高速公路项目土建项目ZCB1-3合同段项目经理部.docx" -l 1 --max-size 3000 --min-size 50 -o ./output
-python -m core.construction_review.component.doc_worker.pdf_worker.cli "E:\LLM\dev_v1\files\7a88f0d5-9d82-43bf-b2b1-c2924d67477e.pdf" -l 1 --max-size 3000 --min-size 50 -o ./output
-
-
-
-python -m file_parse.pdf_worker.cli "Z:\施工方案及编制依据案例库(第一阶段)1205\施工方案文档列表\44_四川公路桥梁建设集团有限公司镇巴(川陕界)至广安高速公路通广段C合同段C4项目经理部.pdf" -l 1 --max-size 3000 --min-size 50 -o ./output
-
-
-
-python -m doc_worker.pdf_worker.cli "data\44_四川公路桥梁建设集团有限公司镇巴(川陕界)至广安高速公路通广段C合同段C4项目经理部.pdf" -l 1 --max-size 3000 --min-size 50 -o ./output

+ 56 - 78
core/construction_review/component/document_processor.py

@@ -5,9 +5,11 @@
 
 重构说明:
 1. 使用类级别共享ChunkClassifier实例,避免重复创建LLM客户端
-2. 统一PDF/DOCX处理流程,消除代码重复
+2. 统一PDF处理流程,消除代码重复
 3. 移除splits冗余数据,统一使用chunks
 4. 完善异常处理,记录完整堆栈信息
+
+注意: DOCX/DOC 文件应在上传层转换为 PDF,本模块不再直接处理 DOCX
 """
 
 import io
@@ -31,9 +33,6 @@ try:
     from .doc_worker.pdf_worker.hybrid_extractor import HybridFullTextExtractor
     from .doc_worker.pdf_worker.text_splitter import PdfTextSplitter
     from .doc_worker.pdf_worker.classifier import PdfHierarchyClassifier
-    from .doc_worker.docx_worker.toc_extractor import DocxTOCExtractor
-    from .doc_worker.docx_worker.full_text_extractor import DocxFullTextExtractor
-    from .doc_worker.docx_worker.text_splitter import DocxTextSplitter
     from .doc_worker.classification.hierarchy_classifier import HierarchyClassifier as DocxHierarchyClassifier
     from .doc_worker.classification.chunk_classifier import ChunkClassifier
     from .doc_worker.config.provider import default_config_provider
@@ -43,9 +42,6 @@ except ImportError:
     from core.construction_review.component.doc_worker.pdf_worker.hybrid_extractor import HybridFullTextExtractor
     from core.construction_review.component.doc_worker.pdf_worker.text_splitter import PdfTextSplitter
     from core.construction_review.component.doc_worker.pdf_worker.classifier import PdfHierarchyClassifier
-    from core.construction_review.component.doc_worker.docx_worker.toc_extractor import DocxTOCExtractor
-    from core.construction_review.component.doc_worker.docx_worker.full_text_extractor import DocxFullTextExtractor
-    from core.construction_review.component.doc_worker.docx_worker.text_splitter import DocxTextSplitter
     from core.construction_review.component.doc_worker.classification.hierarchy_classifier import HierarchyClassifier as DocxHierarchyClassifier
     from core.construction_review.component.doc_worker.classification.chunk_classifier import ChunkClassifier
     from core.construction_review.component.doc_worker.config.provider import default_config_provider
@@ -158,7 +154,7 @@ class DocumentProcessor:
     _shared_chunk_classifier: Optional[ChunkClassifier] = None
 
     def __init__(self, progress_manager=None, callback_task_id: str = None, progress_state: dict = None):
-        self.supported_types = ['pdf', 'docx']
+        self.supported_types = ['pdf']  # DOCX/DOC 应在上传层转换为 PDF
         self.config = default_config_provider
         # SSE 进度推送(由 DocumentWorkflow 注入)
         self._progress_manager = progress_manager
@@ -166,24 +162,54 @@ class DocumentProcessor:
         # 与心跳协程共享的状态字典,更新后心跳自动反映新阶段
         self._progress_state = progress_state
 
-        # 初始化各类型文档的处理组件
+        # 初始化PDF文档的处理组件
         self._components: Dict[str, DocumentComponents] = {
             'pdf': DocumentComponents(
                 toc_extractor=PdfTOCExtractor(),
                 classifier=PdfHierarchyClassifier(),
                 fulltext_extractor=HybridFullTextExtractor(),
                 text_splitter=PdfTextSplitter()
-            ),
-            'docx': DocumentComponents(
-                toc_extractor=DocxTOCExtractor(),
-                classifier=DocxHierarchyClassifier(),
-                fulltext_extractor=DocxFullTextExtractor(
-                    paragraphs_per_page=int(self.config.get("toc_extraction.paragraphs_per_page", 30))
-                ),
-                text_splitter=DocxTextSplitter()
             )
         }
 
+        # 加载标准分类表并创建序号映射
+        self._load_category_seq_mappings()
+
+    def _load_category_seq_mappings(self):
+        """加载标准分类表CSV,创建code到seq的映射"""
+        self._first_seq_map: Dict[str, int] = {}  # first_code -> first_seq
+        self._second_seq_map: Dict[str, int] = {}  # second_code -> second_seq
+
+        try:
+            import csv
+            csv_path = Path(__file__).parent / 'doc_worker' / 'config' / 'StandardCategoryTable.csv'
+            if not csv_path.exists():
+                logger.warning(f"标准分类表不存在: {csv_path}")
+                return
+
+            with open(csv_path, 'r', encoding='utf-8-sig') as f:
+                reader = csv.DictReader(f)
+                for row in reader:
+                    first_code = row.get('first_code', '').strip()
+                    second_code = row.get('second_code', '').strip()
+                    try:
+                        first_seq = int(row.get('first_seq', 0) or 0)
+                    except (ValueError, TypeError):
+                        first_seq = 0
+                    try:
+                        second_seq = int(row.get('second_seq', 0) or 0)
+                    except (ValueError, TypeError):
+                        second_seq = 0
+
+                    if first_code and first_code not in self._first_seq_map:
+                        self._first_seq_map[first_code] = first_seq
+                    if second_code and second_code not in self._second_seq_map:
+                        self._second_seq_map[second_code] = second_seq
+
+            logger.debug(f"加载分类序号映射: 一级 {len(self._first_seq_map)} 个, 二级 {len(self._second_seq_map)} 个")
+        except Exception as e:
+            logger.warning(f"加载分类序号映射失败: {e}")
+
     @classmethod
     def _get_chunk_classifier(cls) -> ChunkClassifier:
         """获取共享的ChunkClassifier实例"""
@@ -456,10 +482,6 @@ class DocumentProcessor:
             }
         }
 
-        # DOCX额外保留full_text字段
-        if file_type == 'docx':
-            result['full_text'] = ''.join([page.get('text', '') for page in pages_content])
-
         return result
 
     async def _fallback_processing(self, file_content: bytes, file_type: str) -> Dict[str, Any]:
@@ -468,15 +490,12 @@ class DocumentProcessor:
 
         Args:
             file_content: 文件内容
-            file_type: 文件类型(pdf/docx
+            file_type: 文件类型(仅支持 pdf)
 
         Returns:
             Dict: 基础处理结果
         """
-        if file_type == 'pdf':
-            return await self._fallback_pdf_processing(file_content)
-        else:
-            return await self._fallback_docx_processing(file_content)
+        return await self._fallback_pdf_processing(file_content)
 
     async def _fallback_pdf_processing(self, file_content: bytes) -> Dict[str, Any]:
         """PDF基础处理模式(当智能处理失败时使用)"""
@@ -533,46 +552,6 @@ class DocumentProcessor:
             logger.error(f"基础PDF处理失败: {str(e)}", exc_info=True)
             raise
 
-    async def _fallback_docx_processing(self, file_content: bytes) -> Dict[str, Any]:
-        """DOCX基础处理模式(当智能处理失败时使用)"""
-        try:
-            from docx import Document
-            from io import BytesIO
-
-            logger.info("使用基础DOCX处理模式(内存模式)")
-            doc = Document(BytesIO(file_content))
-            full_text = '\n'.join([paragraph.text for paragraph in doc.paragraphs])
-
-            # 简单分块,并过滤空内容
-            chunks = []
-            chunk_size = 1000
-            chunk_index = 1
-            for i in range(0, len(full_text), chunk_size):
-                chunk_text = full_text[i:i+chunk_size].strip()
-                if chunk_text:
-                    chunks.append({
-                        'chunk_id': f'chunk_{chunk_index}',
-                        'content': chunk_text,
-                        'metadata': {'chunk_index': chunk_index}
-                    })
-                    chunk_index += 1
-
-            logger.info(f"基础处理完成,有效分块数量: {len(chunks)}")
-
-            return {
-                'document_type': 'docx',
-                'total_chunks': len(chunks),
-                'full_text': full_text,
-                'chunks': chunks,
-                'metadata': {
-                    'paragraphs_count': len(doc.paragraphs),
-                    'word_count': len(full_text.split())
-                }
-            }
-        except Exception as e:
-            logger.error(f"基础DOCX处理失败: {str(e)}", exc_info=True)
-            raise
-
     def structure_content(self, raw_content: Dict[str, Any]) -> Dict[str, Any]:
         """结构化处理,适配doc_worker返回的格式"""
         try:
@@ -589,6 +568,12 @@ class DocumentProcessor:
                     if content:
                         metadata = chunk.get('metadata', {})
                         element_tag = metadata.get('element_tag', {})
+                        chapter_classification = metadata.get('chapter_classification', '')
+                        secondary_category_code = metadata.get('secondary_category_code', '')
+
+                        # 获取序号
+                        first_seq = self._first_seq_map.get(chapter_classification, 0)
+                        second_seq = self._second_seq_map.get(secondary_category_code, 0)
 
                         chunks.append({
                             'chunk_id': metadata.get('chunk_id', ''),
@@ -596,9 +581,11 @@ class DocumentProcessor:
                             'content': content,
                             'section_label': metadata.get('section_label', ''),
                             'project_plan_type': metadata.get('project_plan_type', ''),
-                            'chapter_classification': metadata.get('chapter_classification', ''),
+                            'chapter_classification': chapter_classification,
+                            'first_seq': first_seq,
                             'secondary_category_cn': metadata.get('secondary_category_cn', ''),
-                            'secondary_category_code': metadata.get('secondary_category_code', ''),
+                            'secondary_category_code': secondary_category_code,
+                            'second_seq': second_seq,
                             'tertiary_category_cn': metadata.get('tertiary_category_cn', ''),
                             'tertiary_category_code': metadata.get('tertiary_category_code', ''),
                             # 三级分类详情列表(包含该二级分类下的所有三级分类)
@@ -625,17 +612,8 @@ class DocumentProcessor:
                                 'original_content': content[:100] + '...' if len(content) > 100 else content
                             })
                 else:
-                    # DOCX基础处理
-                    all_chunks = raw_content.get('chunks', [])
+                    # 基础处理结果为空
                     chunks = []
-                    for chunk in all_chunks:
-                        content = chunk.get('content', '').strip()
-                        if content:
-                            chunks.append({
-                                'chunk_id': chunk.get('chunk_id', f'chunk_{len(chunks)+1}'),
-                                'content': content,
-                                'metadata': chunk.get('metadata', {})
-                            })
 
             # 构建返回结果
             result = {

+ 72 - 32
core/construction_review/component/reviewers/completeness_reviewer.py

@@ -27,6 +27,9 @@ class TertiaryItem:
     second_cn: str
     third_cn: str
     third_focus: str
+    first_seq: int = 0
+    second_seq: int = 0
+    third_seq: int = 0
 
 
 @dataclass
@@ -36,6 +39,8 @@ class SecondaryItem:
     second_code: str
     first_cn: str
     second_cn: str
+    first_seq: int = 0
+    second_seq: int = 0
 
 
 @dataclass
@@ -100,6 +105,20 @@ class TertiarySpecLoader:
                 third_cn = str(row.get('third_name', '')).strip()
                 third_focus = str(row.get('third_focus', '')).strip()
 
+                # 读取序号字段
+                try:
+                    first_seq = int(row.get('first_seq', 0) or 0)
+                except (ValueError, TypeError):
+                    first_seq = 0
+                try:
+                    second_seq = int(row.get('second_seq', 0) or 0)
+                except (ValueError, TypeError):
+                    second_seq = 0
+                try:
+                    third_seq = int(row.get('third_seq', 0) or 0)
+                except (ValueError, TypeError):
+                    third_seq = 0
+
                 # 动态构建一级分类名称映射
                 if first_code and first_cn and first_code not in self.first_names:
                     self.first_names[first_code] = first_cn
@@ -113,7 +132,10 @@ class TertiarySpecLoader:
                     first_cn=first_cn or self.first_names.get(first_code, first_code),
                     second_cn=second_cn,
                     third_cn=third_cn,
-                    third_focus=third_focus
+                    third_focus=third_focus,
+                    first_seq=first_seq,
+                    second_seq=second_seq,
+                    third_seq=third_seq
                 )
 
                 # 存储二级项
@@ -123,7 +145,9 @@ class TertiarySpecLoader:
                         first_code=first_code,
                         second_code=second_code,
                         first_cn=first_cn or self.first_names.get(first_code, first_code),
-                        second_cn=second_cn
+                        second_cn=second_cn,
+                        first_seq=first_seq,
+                        second_seq=second_seq
                     )
         
         except Exception as e:
@@ -383,13 +407,19 @@ class LightweightCompletenessChecker:
         extra_second = actual_second_keys - required_second
 
         # 一级缺失详情
-        missing_first_details = [
-            {
+        missing_first_details = []
+        for c in sorted(missing_first):
+            # 从任意该一级下的二级获取 first_seq
+            first_seq = 0
+            for (fc, sc), item in self.secondary_specs.items():
+                if fc == c:
+                    first_seq = item.first_seq
+                    break
+            missing_first_details.append({
                 "first_code": c,
-                "first_name": self.spec_loader.first_names.get(c, c)
-            }
-            for c in sorted(missing_first)
-        ]
+                "first_name": self.spec_loader.first_names.get(c, c),
+                "first_seq": first_seq
+            })
 
         # 二级缺失详情
         missing_second_details = []
@@ -398,8 +428,10 @@ class LightweightCompletenessChecker:
             missing_second_details.append({
                 "first_code": cat1,
                 "first_name": item.first_cn if item else self.spec_loader.first_names.get(cat1, cat1),
+                "first_seq": item.first_seq if item else 0,
                 "secondary_code": cat2,
-                "secondary_name": item.second_cn if item else "未知"
+                "secondary_name": item.second_cn if item else "未知",
+                "second_seq": item.second_seq if item else 0
             })
 
         # 二级多余详情(目录有但标准无)
@@ -409,8 +441,10 @@ class LightweightCompletenessChecker:
             extra_second_details.append({
                 "first_code": cat1,
                 "first_name": self.spec_loader.first_names.get(cat1, cat1),
+                "first_seq": item.first_seq if item else 0,
                 "secondary_code": cat2,
                 "secondary_name": item.second_cn if item else "未知",
+                "second_seq": item.second_seq if item else 0,
                 "outline_title": outline_secondary.get((cat1, cat2), "")
             })
 
@@ -480,10 +514,13 @@ class LightweightCompletenessChecker:
                 missing_details.append({
                     "first_code": cat1,
                     "first_name": item.first_cn,
+                    "first_seq": item.first_seq,
                     "secondary_code": cat2,
                     "secondary_name": item.second_cn,
+                    "second_seq": item.second_seq,
                     "tertiary_code": cat3,
                     "tertiary_name": item.third_cn,
+                    "third_seq": item.third_seq,
                     "focus": item.third_focus
                 })
         
@@ -508,8 +545,10 @@ class LightweightCompletenessChecker:
             secondary_stats_list.append({
                 "first_code": cat1,
                 "first_name": item.first_cn if item else self.spec_loader.first_names.get(cat1, cat1),
+                "first_seq": item.first_seq if item else 0,
                 "secondary_code": cat2,
                 "secondary_name": item.second_cn if item else "未知",
+                "second_seq": item.second_seq if item else 0,
                 "total_tertiary": stats["total"],
                 "present": stats["present"],
                 "missing": stats["missing"],
@@ -631,6 +670,12 @@ class LightweightCompletenessChecker:
 
         for first_code in sorted(required_first):
             first_name = self.spec_loader.first_names.get(first_code, first_code)
+            # 获取一级序号
+            first_seq = 0
+            for (fc, sc), item in self.secondary_specs.items():
+                if fc == first_code:
+                    first_seq = item.first_seq
+                    break
 
             # ── 一级缺失 ──────────────────────────────────────────────
             if first_code not in actual_first:
@@ -643,6 +688,7 @@ class LightweightCompletenessChecker:
                         f"根据规范要求,文档必须包含'{first_name}'一级章节,"
                         f"当前正文中未发现该章节任何内容"
                     ),
+                    "first_seq": first_seq,
                 })
                 continue
 
@@ -653,6 +699,7 @@ class LightweightCompletenessChecker:
             for (cat1, cat2) in required_second:
                 sec_item = self.secondary_specs.get((cat1, cat2))
                 second_name = sec_item.second_cn if sec_item else cat2
+                second_seq = sec_item.second_seq if sec_item else 0
 
                 # ── 二级缺失 ──────────────────────────────────────────
                 if (cat1, cat2) not in actual_secondary:
@@ -667,6 +714,8 @@ class LightweightCompletenessChecker:
                             f"根据规范要求,'{first_name}'下应包含'{second_name}'二级章节,"
                             f"当前正文中未发现该章节内容"
                         ),
+                        "first_seq": first_seq,
+                        "second_seq": second_seq,
                     })
                     continue
 
@@ -685,29 +734,20 @@ class LightweightCompletenessChecker:
                 if not missing_t_items:
                     continue
 
-                n = len(missing_t_items)
-
-                # 缺失名称列表(最多展示 5 条)
-                missing_labels = [
-                    f"{i + 1}.{t.third_cn}" for i, t in enumerate(missing_t_items[:5])
-                ]
-                if n > 5:
-                    missing_labels.append(f"等共{n}项")
-                missing_str = "、".join(missing_labels)
-
-                recommendations.append({
-                    "level": "三级",
-                    "issue_point": (
-                        f"【三级内容缺失】{first_name} > {second_name} 缺少{n}个三级要点:{missing_str}"
-                    ),
-                    "location": f"{first_name} > {second_name}",
-                    "suggestion": (
-                        f"请补充'{second_name}'以下{n}个要点内容:{missing_str}"
-                    ),
-                    "reason": (
-                        f"'{second_name}'下缺失以下{n}个规范要求的内容要点:{missing_str}"
-                    ),
-                })
+                # 为每个缺失的三级项创建单独的 recommendation
+                for t_item in missing_t_items:
+                    recommendations.append({
+                        "level": "三级",
+                        "issue_point": (
+                            f"【三级内容缺失】{first_name} > {second_name} > '{t_item.third_cn}'"
+                        ),
+                        "location": f"{first_name} > {second_name}",
+                        "suggestion": f"请补充'{second_name}'下的'{t_item.third_cn}'内容",
+                        "reason": f"'{second_name}'下缺失规范要求的'{t_item.third_cn}'内容要点",
+                        "first_seq": first_seq,
+                        "second_seq": second_seq,
+                        "third_seq": t_item.third_seq,
+                    })
 
         # ── 一致性审查:目录有列但正文无内容 ─────────────────────────────
         if outline_result:

+ 0 - 537
core/construction_review/component/reviewers/reference_basis_reviewer.py.bak

@@ -1,537 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-import json
-import time
-import yaml
-from typing import Any, Dict, List, Optional
-from functools import partial
-
-from langchain_milvus import Milvus, BM25BuiltInFunction
-from foundation.infrastructure.config.config import config_handler
-from foundation.ai.models.model_handler import model_handler as mh
-from core.construction_review.component.reviewers.utils.directory_extraction import BasisItem, BasisItems
-from core.construction_review.component.reviewers.utils.inter_tool import InterTool
-from core.construction_review.component.reviewers.utils.prompt_loader import PromptLoader
-from core.construction_review.component.reviewers.utils.punctuation_checker import check_punctuation
-from core.construction_review.component.reviewers.utils.punctuation_result_processor import process_punctuation_results
-from core.construction_review.component.reviewers.utils.reference_matcher import match_reference_files
-from foundation.observability.logger.loggering import review_logger as logger
-from langchain_core.prompts import ChatPromptTemplate
-from foundation.ai.agent.generate.model_generate import generate_model_client
-
-class BasisSearchEngine:
-    """编制依据向量搜索引擎"""
-
-    # 类级别的缓存,避免重复创建 Milvus 实例
-    _vectorstore_cache = {}
-
-    def __init__(self):
-        self.emdmodel = None
-        self.host = None
-        self.port = None
-        self.user = None
-        self.password = None
-        self._initialize()
-
-    def _initialize(self):
-        """初始化搜索引擎"""
-        try:
-            # 连接配置
-            self.host = config_handler.get('milvus', 'MILVUS_HOST', 'localhost')
-            self.port = int(config_handler.get('milvus', 'MILVUS_PORT', '19530'))
-            self.user = config_handler.get('milvus', 'MILVUS_USER')
-            self.password = config_handler.get('milvus', 'MILVUS_PASSWORD')
-
-            # 初始化嵌入模型
-            self.emdmodel = mh._get_lq_qwen3_8b_emd()
-            logger.info("嵌入模型初始化成功")
-
-        except Exception as e:
-            logger.error(f" BasisSearchEngine 初始化失败: {e}")
-
-    def _get_vectorstore(self, collection_name: str):
-        """获取或创建 Milvus vectorstore 实例(使用缓存)"""
-        cache_key = f"{self.host}:{self.port}:{collection_name}"
-
-        if cache_key not in BasisSearchEngine._vectorstore_cache:
-            connection_args = {
-                "uri": f"http://{self.host}:{self.port}",
-                "user": self.user,
-                "db_name": "lq_db"
-            }
-            if self.password:
-                connection_args["password"] = self.password
-
-            # 抑制 AsyncMilvusClient 的警告日志
-            import logging
-            original_level = logging.getLogger('pymilvus').level
-            logging.getLogger('pymilvus').setLevel(logging.ERROR)
-
-            try:
-                vectorstore = Milvus(
-                    embedding_function=self.emdmodel,
-                    collection_name=collection_name,
-                    connection_args=connection_args,
-                    consistency_level="Strong",
-                    builtin_function=BM25BuiltInFunction(),
-                    vector_field=["dense", "sparse"]
-                )
-                BasisSearchEngine._vectorstore_cache[cache_key] = vectorstore
-                logger.info(f"创建并缓存 Milvus 连接: {cache_key}")
-            finally:
-                logging.getLogger('pymilvus').setLevel(original_level)
-
-        return BasisSearchEngine._vectorstore_cache[cache_key]
-
-    def hybrid_search(self, collection_name: str, query_text: str,
-                     top_k: int = 3, ranker_type: str = "weighted",
-                     dense_weight: float = 0.7, sparse_weight: float = 0.3):
-        try:
-            # 使用缓存的 vectorstore
-            vectorstore = self._get_vectorstore(collection_name)
-
-            # 执行混合搜索
-            if ranker_type == "weighted":
-                results = vectorstore.similarity_search(
-                    query=query_text,
-                    k=top_k,
-                    ranker_type="weighted",
-                    ranker_params={"weights": [dense_weight, sparse_weight]}
-                )
-            else:  # rrf
-                results = vectorstore.similarity_search(
-                    query=query_text,
-                    k=top_k,
-                    ranker_type="rrf",
-                    ranker_params={"k": 60}
-                )
-
-            # 格式化结果,保持与其他搜索方法一致
-            formatted_results = []
-            for doc in results:
-                formatted_results.append({
-                    'id': doc.metadata.get('pk', 0),
-                    'text_content': doc.page_content,
-                    'metadata': doc.metadata,
-                    'distance': 0.0,
-                    'similarity': 1.0
-                })
-
-            return formatted_results
-
-        except Exception as e:
-            # 回退到传统的向量搜索
-            logger.error(f" 搜索失败: {e}")
-
-class StandardizedResponseProcessor:
-    """标准化响应处理器 - 统一为outline_reviewer.py格式"""
-
-    def __init__(self):
-        self.inter_tool = InterTool()
-
-    def process_llm_response(self, response_text: str, check_name: str, chapter_code: str,check_item_code:str) -> List[Dict[str, Any]]:
-        """
-        处理LLM响应,返回标准格式
-
-        Args:
-            response_text: LLM原始响应文本
-            check_name: 检查项名称
-            chapter_code: 章节代码
-            check_item_code: 检查项代码
-
-        Returns:
-            List[Dict]: 标准格式的审查结果列表
-        """
-        if not self.inter_tool:
-            logger.warning("InterTool未初始化,返回空结果")
-            return []
-
-        try:
-            # 使用inter_tool提取JSON数据
-            json_data = self.inter_tool._extract_json_data(response_text)
-            parsed_result = []
-
-            if json_data and isinstance(json_data, list):
-                for item in json_data:
-                    parsed_result.append(self.inter_tool._create_issue_item(item, check_name, chapter_code,check_item_code))
-            elif json_data and isinstance(json_data, dict):
-                parsed_result.append(self.inter_tool._create_issue_item(json_data, check_name, chapter_code,check_item_code))
-
-            return parsed_result
-
-        except Exception as e:
-            logger.error(f"处理LLM响应失败: {str(e)}")
-            # 返回一个错误条目
-            return [{
-                "check_item": check_name,
-                "chapter_code": "basis",
-                "check_item_code": f"basis_{check_name}",
-                "check_result": {"error": str(e)},
-                "exist_issue": True,
-                "risk_info": {"risk_level": "medium"}
-            }]
-
-
-class MessageBuilder:
-    """消息构建工具类"""
-
-    def __init__(self, prompt_loader_instance=None):
-        self.prompt_loader = prompt_loader_instance
-        
-    def get_prompt_template(self):
-        with open("core/construction_review/component/reviewers/prompt/reference_basis_reviewer.yaml", "r", encoding="utf-8") as f:
-            data = yaml.safe_load(f)
-        return ChatPromptTemplate.from_messages([
-                ("system", data["reference_basis_reviewer"]["system_prompt"]),
-                ("user", data["reference_basis_reviewer"]["user_prompt_template"])
-            ])
-    
-class LLMReviewClient:
-    """LLM审查客户端"""
-
-    def __init__(self):
-        """初始化LLM审查客户端,使用通用模型底座"""
-        self.model_client = generate_model_client
-
-    async def review_basis(self, Message: str, trace_id: str = None) -> str:
-        try:
-            logger.info(f" 模型调用准备阶段: trace_id={trace_id}")
-
-            # 使用通用模型底座调用
-            messages = Message.format_messages() if hasattr(Message, 'format_messages') else Message
-            response = await self.model_client.get_model_generate_invoke(
-                trace_id=trace_id or "ref_basis_review",
-                messages=messages if isinstance(messages, list) else None,
-                prompt=messages if isinstance(messages, str) else None,
-                model_name="qwen3_30b"
-            )
-            return response
-
-        except Exception as e:
-            logger.error(f" 模型调用准备阶段失败: {e}")
-            # 返回空JSON数组字符串以防解析崩溃
-            return "[]"
-
-
-class BasisReviewService:
-    """编制依据审查服务核心类"""
-
-    def __init__(self, max_concurrent: int = 4):
-        self.search_engine = BasisSearchEngine()
-        self.llm_client = LLMReviewClient()
-        self.response_processor = StandardizedResponseProcessor()
-        fresh_prompt_loader = PromptLoader()
-        self.message_builder = MessageBuilder(fresh_prompt_loader)
-        self.max_concurrent = max_concurrent
-        self._semaphore = None
-
-    async def __aenter__(self):
-        """异步上下文管理器入口"""
-        if self._semaphore is None:
-            self._semaphore = asyncio.Semaphore(self.max_concurrent)
-        return self
-
-    async def __aexit__(self, exc_type, exc_val, exc_tb):
-        """异步上下文管理器出口"""
-        return False
-
-    async def review_batch(
-        self,
-        basis_items: List[str],
-        collection_name: str = "first_bfp_collection_status",
-        filters: Optional[Dict[str, Any]] = None,
-        min_score: float = 0.3,
-        top_k_each: int = 3,
-    ) -> List[Dict[str, Any]]:
-        """异步批次审查(通常3条)"""
-        basis_items = [x for x in (basis_items or []) if isinstance(x, str) and x.strip()]
-        if not basis_items:
-            return []
-
-        async with self._semaphore:
-            try:
-                # 第一步:搜索编制依据并通过match_reference_files过滤
-                search_tasks = []
-                for basis in basis_items:
-                    task = asyncio.create_task(
-                        self._async_search_basis(basis, collection_name, top_k_each)
-                    )
-                    search_tasks.append(task)
-
-                # 等待所有搜索完成
-                search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
-
-                grouped_candidates = []
-                for i, result in enumerate(search_results):
-                    if isinstance(result, Exception):
-                        logger.error(f"搜索失败 '{basis_items[i]}': {result}")
-                        grouped_candidates.append([])
-                    else:
-                        # result 是 List[dict],需要遍历
-                        texts = [item["text_content"] for item in result if "text_content" in item]
-                        grouped_candidates.append(texts)
-                
-                # 获取match_reference_files的结果并过滤
-                match_result = await match_reference_files(reference_text=grouped_candidates, review_text=basis_items)
-                # 解析JSON并过滤:same_name_current和exact_match_info都是""的项过滤掉
-                try:
-                    match_data = json.loads(match_result)
-                    # 提取items字段(match_reference_files返回{items: [...]}格式)
-                    items = match_data.get('items', match_data) if isinstance(match_data, dict) else match_data
-                    filtered_data = [item for item in items if not (item.get('same_name_current') == "" and item.get('exact_match_info') == "")]
-                    # 从过滤后的数据中提取review_item用于后续检查
-                    filtered_basis_items = [item.get('review_item') for item in filtered_data if item.get('review_item')]
-                    basis_items_to_check = filtered_basis_items if filtered_basis_items else []
-                    logger.info(f"过滤后参与检查的编制依据: {len(basis_items_to_check)}/{len(basis_items)}")
-                except (json.JSONDecodeError, TypeError) as e:
-                    logger.warning(f"过滤match_reference_files结果时出错: {e}")
-                    # 如果解析失败,使用原始结果
-                    basis_items_to_check = []
-                
-                # 如果没有过滤出数据,直接返回空结果
-                if not basis_items_to_check:
-                    logger.info(f"过滤后没有符合条件的编制依据,跳过后续检查")
-                    return []
-                
-                # 第二步:调用标点符号检查器
-                checker_result = await check_punctuation(basis_items_to_check)
-                print(checker_result)
-                
-                # 第三步:调用结果处理器,生成详细的问题分析报告
-                processor_result = await process_punctuation_results(checker_result)
-                print("\n【第二步】问题分析报告输出:")
-                print(processor_result)
-                
-                # 第四步:转换为标准格式
-                standardized_result = self.response_processor.process_llm_response(
-                    processor_result, 
-                    "reference_check", 
-                    "basis",
-                    "basis_reference_check"
-                )
-
-                # 统计问题数量
-                issue_count = sum(1 for item in standardized_result if item.get('exist_issue', False))
-                logger.info(f"编制依据批次审查完成:总计 {len(basis_items_to_check)} 项,发现问题 {issue_count} 项")
-
-                return standardized_result
-
-            except Exception as e:
-                logger.error(f" 批次处理失败: {e}")
-                return [{
-                    "check_item": "reference_check",
-                    "chapter_code": "basis",
-                    "check_item_code": "basis_reference_check",
-                    "check_result": {"error": str(e), "basis_items": basis_items},
-                    "exist_issue": True,
-                    "risk_info": {"risk_level": "high"}
-                }]
-
-    async def _async_search_basis(
-        self,
-        basis: str,
-        collection_name: str,
-        top_k_each: int
-    ) -> List[dict]:
-        """异步搜索单个编制依据(Hybrid Search)"""
-        try:
-            loop = asyncio.get_running_loop()
-            func = partial(
-                self.search_engine.hybrid_search,
-                collection_name=collection_name,
-                query_text=basis,
-                top_k=top_k_each,
-                ranker_type="weighted",
-                dense_weight=0.3,
-                sparse_weight=0.7
-            )
-            retrieved = await loop.run_in_executor(None, func)
-            logger.info(f" 搜索 '{basis}' -> 找到 {len(retrieved or [])} 个结果")
-            return retrieved or []
-        except Exception as e:
-            logger.error(f" 搜索失败 '{basis}': {e}")
-            return []
-
-    async def review_all(self, basis_items: BasisItems, collection_name: str = "first_bfp_collection_status",
-                        progress_manager=None, callback_task_id: str = None) -> List[List[Dict[str, Any]]]:
-        """异步批量审查所有编制依据(BasisItems 入参)"""
-        if not basis_items or not getattr(basis_items, "items", None):
-            return []
-        
-        items = [item.raw for item in basis_items.items if getattr(item, "raw", None)]
-        if not items:
-            return []
-
-        start_time = time.time()
-        total_batches = (len(items) + 2) // 3  # 计算总批次数
-        
-        # 发送开始审查的SSE推送(使用独立命名空间,避免与主流程进度冲突)
-        if progress_manager and callback_task_id:
-            try:
-                await progress_manager.update_stage_progress(
-                    callback_task_id=callback_task_id,
-                    stage_name="编制依据审查-子任务",  # 独立命名空间
-                    status="processing",
-                    message=f"开始编制依据审查,共{len(items)}项编制依据",
-                    overall_task_status="processing",
-                    event_type="processing"
-                    # 不设置 current,避免覆盖主流程进度
-                )
-            except Exception as e:
-                logger.error(f"SSE推送开始消息失败: {e}")
-
-        # 分批处理
-        batches = []
-        for i in range(0, len(items), 3):
-            batch = items[i:i + 3]
-            batches.append(batch)
-
-        # 异步并发执行所有批次,使用回调处理SSE推送
-        async def process_batch_with_callback(batch_index: int, batch: List[str]) -> List[Dict[str, Any]]:
-            """处理单个批次并执行SSE回调"""
-            try:
-                # 执行单个批次审查
-                result = await self.review_batch(batch, collection_name)
-
-                # 统计当前批次结果
-                batch_standard_count = 0
-                for item in result:
-                    if isinstance(item, dict) and item.get('is_standard', False):
-                        batch_standard_count += 1
-
-                # 立即推送当前批次完成的SSE消息(使用独立命名空间)
-                logger.info(f"批次{batch_index + 1}完成,准备推送SSE")
-                if progress_manager and callback_task_id:
-                    try:
-                        await progress_manager.update_stage_progress(
-                            callback_task_id=callback_task_id,
-                            stage_name=f"编制依据审查-子任务-批次{batch_index + 1}",  # 独立命名空间
-                            status="processing",
-                            message=f"完成第{batch_index + 1}/{total_batches}批次编制依据审查,{len(batch)}项,其中{batch_standard_count}项为标准",
-                            overall_task_status="processing",
-                            event_type="processing",
-                            issues=result  # 推送该批次的审查结果
-                            # 不设置 current,避免覆盖主流程进度
-                        )
-                        logger.info(f"批次{batch_index + 1} SSE推送成功")
-                    except Exception as e:
-                        logger.error(f"SSE推送批次{batch_index + 1}结果失败: {e}")
-
-                return result
-
-            except Exception as e:
-                logger.error(f" 批次 {batch_index} 处理失败: {e}")
-                error_result = [{"name": name, "is_standard": False, "status": "", "meg": f"批次处理失败: {str(e)}"}
-                                for name in batch]
-
-                # 即使失败也要推送结果(使用独立命名空间)
-                if progress_manager and callback_task_id:
-                    try:
-                        await progress_manager.update_stage_progress(
-                            callback_task_id=callback_task_id,
-                            stage_name=f"编制依据审查-子任务-批次{batch_index + 1}",  # 独立命名空间
-                            status="processing",
-                            message=f"第{batch_index + 1}/{total_batches}批次处理失败",
-                            overall_task_status="processing",
-                            event_type="processing",
-                            issues=error_result
-                            # 不设置 current,避免覆盖主流程进度
-                        )
-                    except Exception as push_e:
-                        logger.error(f"SSE推送失败批次{batch_index + 1}结果失败: {push_e}")
-
-                return error_result
-
-        # 创建所有批次的异步任务
-        batch_tasks = []
-        for i, batch in enumerate(batches):
-            task = process_batch_with_callback(i, batch)
-            batch_tasks.append(task)
-
-        # 并发执行所有批次
-        logger.info(f"开始并发执行{total_batches}个批次编制依据审查")
-        processed_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
-
-        # 处理异常结果并统计
-        total_items = 0
-        issue_items = 0
-        successful_batches = 0
-
-        # 重新构建结果列表,过滤异常
-        final_results = []
-        for i, result in enumerate(processed_results):
-            if isinstance(result, Exception):
-                logger.error(f" 批次 {i} 返回异常: {result}")
-                error_batch = batches[i] if i < len(batches) else []
-                error_result = [{
-                    "check_item": "reference_check",
-                    "chapter_code": "basis",
-                    "check_item_code": "basis_reference_check",
-                    "check_result": {"error": str(result), "basis_items": error_batch},
-                    "exist_issue": True,
-                    "risk_info": {"risk_level": "high"}
-                }]
-                final_results.append(error_result)
-            else:
-                final_results.append(result)
-                successful_batches += 1
-
-                # 过滤空批次结果,避免出现 []
-        final_results = [res for res in final_results if res]
-
-        # 统计总结果
-        for result in final_results:
-            for item in result:
-                total_items += 1
-                if isinstance(item, dict) and item.get('exist_issue', False):
-                    issue_items += 1
-
-        logger.info(f"并发执行完成,成功批次: {successful_batches}/{total_batches}")
-
-
-        # 发送完成审查的SSE推送(使用独立命名空间,不设置current避免覆盖主流程进度)
-        elapsed_time = time.time() - start_time
-        if progress_manager and callback_task_id:
-            try:
-                await progress_manager.update_stage_progress(
-                    callback_task_id=callback_task_id,
-                    stage_name="编制依据审查-子任务",  # 独立命名空间
-                    status="processing",
-                    message=f"编制依据审查完成,共{total_items}项,发现问题{issue_items}项,耗时{elapsed_time:.2f}秒",
-                    overall_task_status="processing",
-                    event_type="processing"
-                    # 不设置 current,避免覆盖主流程进度
-                )
-            except Exception as e:
-                logger.error(f"SSE推送完成消息失败: {e}")
-
-        logger.info(f" 异步审查完成,耗时: {elapsed_time:.4f} 秒")
-        logger.info(f" 总编制依据: {total_items}, 问题项: {issue_items}, 成功批次: {successful_batches}/{total_batches}")
-        print("final_results:\n")
-        print(final_results)    
-        return final_results
-
-
-# 便捷函数
-async def review_basis_batch_async(basis_items: List[str], max_concurrent: int = 4) -> List[Dict[str, Any]]:
-    """异步批次审查便捷函数"""
-    async with BasisReviewService(max_concurrent=max_concurrent) as service:
-        return await service.review_batch(basis_items)
-
-
-async def review_all_basis_async(basis_items: BasisItems, max_concurrent: int = 4) -> List[List[Dict[str, Any]]]:
-    """异步全部审查便捷函数(BasisItems 入参)"""
-    async with BasisReviewService(max_concurrent=max_concurrent) as service:
-        return await service.review_all(basis_items)
-
-if __name__ == "__main__":
-    # 简单测试
-    test_basis_items = BasisItems(items=[
-        BasisItem(title="坠落防护水平生命线装置", suffix="GB 38454", raw="《坠落防护水平生命线装置》GB 38454"),
-        BasisItem(title="电力高处作业防坠器", suffix="DL/T 1147", raw="《电力高处作业防坠器》DL/T 1147"),
-        BasisItem(title="坠落防护挂点装置", suffix="GB 30862", raw="《坠落防护挂点装置》GB 30862"),
-        BasisItem(title="混凝土结构设计规范", suffix="GB 50010-2010", raw="《混凝土结构设计规范》GB 50010-2010"),
-        BasisItem(title="建筑施工组织设计规范", suffix="GB/T 50502-2015", raw="《建筑施工组织设计规范》GB/T 50502-2015"),
-    ])
-    result = asyncio.run(review_all_basis_async(test_basis_items))

+ 34 - 2
core/construction_review/component/reviewers/timeliness_basis_reviewer.py

@@ -224,12 +224,44 @@ class BasisReviewService:
                 
                 # 获取match_reference_files的结果并过滤
                 match_result = await match_reference_files(reference_text=grouped_candidates, review_text=basis_items)
-                # 解析JSON并过滤:same_name_current和exact_match_info都是""的项过滤掉
+
+                # 记录完整的匹配结果用于调试
+                logger.info(f"批次 match_reference_files 原始结果: {match_result[:500]}...")
+
+                # 解析JSON并过滤:保留有相关信息的项
                 try:
                     match_data = json.loads(match_result)
                     # 提取items字段(match_reference_files返回{items: [...]}格式)
                     items = match_data.get('items', match_data) if isinstance(match_data, dict) else match_data
-                    filtered_data = [item for item in items if item.get('exact_match_info') != ""]
+
+                    logger.info(f"解析到 {len(items)} 个匹配项")
+                    for idx, item in enumerate(items):
+                        logger.info(f"  项{idx}: review_item={item.get('review_item', 'unknown')}, "
+                                  f"has_related_file={item.get('has_related_file')}, "
+                                  f"exact_match_info={item.get('exact_match_info')}, "
+                                  f"same_name_current={item.get('same_name_current')}")
+
+                    # 放宽过滤条件:只要有相关文件信息就进行审查
+                    filtered_data = [
+                        item for item in items
+                        if item.get('has_related_file') or
+                           item.get('exact_match_info') or
+                           item.get('same_name_current')
+                    ]
+
+                    logger.info(f"过滤后保留 {len(filtered_data)} 个项")
+
+                    # 记录被过滤掉的项目用于调试
+                    skipped_items = [
+                        item for item in items
+                        if not (item.get('has_related_file') or
+                               item.get('exact_match_info') or
+                               item.get('same_name_current'))
+                    ]
+                    if skipped_items:
+                        logger.warning(f"跳过了 {len(skipped_items)} 个无参考信息的编制依据: "
+                                     f"{[item.get('review_item', 'unknown') for item in skipped_items]}")
+
                     # 如果没有过滤出数据,直接返回空结果
                     if not filtered_data:
                         logger.info(f"过滤后没有符合条件的编制依据,跳过后续检查")

+ 487 - 0
core/construction_review/component/reviewers/timeliness_content_reviewer.py

@@ -0,0 +1,487 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+三级分类内容时效性审查模块
+
+功能:从三级分类详情的content字段中提取规范引用,并进行时效性审查。
+主要用于检测文本内容中引用的规范是否过时(如JTG B01-2011应更新为JTG B01-2014)。
+"""
+
+import re
+import json
+import asyncio
+from typing import Any, Dict, List, Optional, Tuple
+from dataclasses import dataclass, field
+from functools import partial
+
+from foundation.observability.logger.loggering import review_logger as logger
+from core.construction_review.component.reviewers.utils.reference_matcher import match_reference_files
+from core.construction_review.component.reviewers.utils.timeliness_determiner import determine_timeliness_issue
+from core.construction_review.component.reviewers.timeliness_basis_reviewer import BasisSearchEngine, StandardizedResponseProcessor
+
+
+@dataclass
+class StandardReference:
+    """规范引用数据类"""
+    original_text: str           # 原始文本,如"《公路工程技术标准》(JTG B01-2011)"
+    name: str                    # 规范名称,如"公路工程技术标准"
+    number: str                  # 规范编号,如"JTG B01-2011"
+    context: str                 # 上下文内容
+    location_info: Dict[str, Any] = field(default_factory=dict)  # 位置信息
+
+
+@dataclass
+class ContentTimelinessResult:
+    """内容时效性审查结果"""
+    reference: StandardReference
+    has_issue: bool
+    issue_type: str              # 问题类型
+    suggestion: str
+    reason: str
+    risk_level: str              # 无风险 / 高风险
+
+
+class StandardExtractor:
+    """规范引用提取器"""
+
+    # 规范编号正则模式(匹配类似 GB 50010-2010、JTG B01-2014、GB/T 50502-2020 等格式)
+    STANDARD_NUMBER_PATTERNS = [
+        # 中国国家标准:GB 50010-2010、GB/T 50502-2020
+        r'GB(?:/T)?\s*\d{4,5}(?:\.\d+)?\s*-\s*\d{4}',
+        # 中国行业标准:JTG B01-2014、JTG D60-2015、JTG/T 3650-2020
+        r'[A-Z]{2,3}(?:/T)?\s*[A-Z]?\s*\d{2,4}(?:\.\d+)?\s*-\s*\d{4}',
+        # 地方标准:DB11/T 1234-2020
+        r'DB\d{2}(?:/T)?\s*\d{4,5}\s*-\s*\d{4}',
+        # 团体标准:T/CECS 123-2020
+        r'T/\w+\s*\d{3,5}\s*-\s*\d{4}',
+    ]
+
+    # 规范名称与编号组合的正则模式
+    STANDARD_FULL_PATTERN = re.compile(
+        r'《([^《》]+)》\s*[((]([^))]+)[))]',
+        re.MULTILINE
+    )
+
+    # 仅规范编号模式
+    STANDARD_NUMBER_ONLY_PATTERN = re.compile(
+        r'(' + '|'.join(STANDARD_NUMBER_PATTERNS) + r')',
+        re.MULTILINE | re.IGNORECASE
+    )
+
+    def __init__(self):
+        self.extracted_cache: Dict[str, List[StandardReference]] = {}
+
+    def extract_from_content(self, content: str, location_info: Optional[Dict] = None) -> List[StandardReference]:
+        """
+        从内容文本中提取规范引用
+
+        Args:
+            content: 内容文本(包含行号标记如 <80>)
+            location_info: 位置信息(如三级分类代码、行号范围等)
+
+        Returns:
+            List[StandardReference]: 提取的规范引用列表
+        """
+        if not content:
+            return []
+
+        # 使用缓存
+        cache_key = hash(content)
+        if cache_key in self.extracted_cache:
+            return self.extracted_cache[cache_key]
+
+        references = []
+
+        # 1. 提取完整格式:《名称》(编号)
+        full_matches = self.STANDARD_FULL_PATTERN.findall(content)
+        for name, number in full_matches:
+            # 验证编号是否符合规范格式
+            if self._is_valid_standard_number(number):
+                original = f"《{name}》({number})"
+                # 查找该引用在原文中的位置
+                context = self._extract_context(content, original)
+                ref = StandardReference(
+                    original_text=original,
+                    name=name.strip(),
+                    number=number.strip(),
+                    context=context,
+                    location_info=location_info or {}
+                )
+                references.append(ref)
+
+        # 2. 提取孤立的规范编号(用于补充)
+        number_matches = self.STANDARD_NUMBER_ONLY_PATTERN.findall(content)
+        for match in number_matches:
+            number = match if isinstance(match, str) else match[0]
+            # 检查是否已包含在完整格式中
+            if not any(number in ref.number for ref in references):
+                # 尝试提取该编号附近的上下文作为名称
+                name = self._infer_name_from_context(content, number)
+                original = f"《{name}》({number})" if name else number
+                ref = StandardReference(
+                    original_text=original,
+                    name=name or "",
+                    number=number.strip(),
+                    context=self._extract_context(content, number),
+                    location_info=location_info or {}
+                )
+                references.append(ref)
+
+        # 去重(基于original_text)
+        seen = set()
+        unique_refs = []
+        for ref in references:
+            if ref.original_text not in seen:
+                seen.add(ref.original_text)
+                unique_refs.append(ref)
+
+        self.extracted_cache[cache_key] = unique_refs
+        return unique_refs
+
+    def _is_valid_standard_number(self, number: str) -> bool:
+        """验证是否为有效的规范编号"""
+        number = number.strip().upper()
+        # 检查是否匹配任一规范编号模式
+        for pattern in self.STANDARD_NUMBER_PATTERNS:
+            if re.match(pattern, number, re.IGNORECASE):
+                return True
+        return False
+
+    def _extract_context(self, content: str, target: str, window: int = 50) -> str:
+        """提取目标文本的上下文"""
+        idx = content.find(target)
+        if idx == -1:
+            return ""
+        start = max(0, idx - window)
+        end = min(len(content), idx + len(target) + window)
+        return content[start:end].strip()
+
+    def _infer_name_from_context(self, content: str, number: str) -> str:
+        """从上下文推断规范名称"""
+        # 查找编号附近的《名称》格式
+        pattern = re.compile(r'《([^《》]{3,50})》[^《》]{0,30}' + re.escape(number))
+        match = pattern.search(content)
+        if match:
+            return match.group(1)
+        return ""
+
+
+class ContentTimelinessReviewer:
+    """三级分类内容时效性审查器"""
+
+    def __init__(self, max_concurrent: int = 4):
+        self.extractor = StandardExtractor()
+        self.search_engine = BasisSearchEngine()
+        self.response_processor = StandardizedResponseProcessor()
+        self.max_concurrent = max_concurrent
+        self._semaphore = None
+
+    async def __aenter__(self):
+        """异步上下文管理器入口"""
+        if self._semaphore is None:
+            self._semaphore = asyncio.Semaphore(self.max_concurrent)
+        return self
+
+    async def __aexit__(self, exc_type, exc_val, exc_tb):
+        """异步上下文管理器出口"""
+        return False
+
+    async def review_tertiary_content(
+        self,
+        tertiary_details: List[Dict[str, Any]],
+        collection_name: str = "first_bfp_collection_status",
+        progress_manager=None,
+        callback_task_id: str = None
+    ) -> List[Dict[str, Any]]:
+        """
+        审查三级分类内容中的规范时效性
+
+        Args:
+            tertiary_details: 三级分类详情列表,每项包含content字段
+            collection_name: Milvus集合名称
+            progress_manager: 进度管理器(可选,用于SSE推送)
+            callback_task_id: 回调任务ID(可选)
+
+        Returns:
+            List[Dict]: 标准化的审查结果列表
+        """
+        if not tertiary_details:
+            return []
+
+        # 1. 从所有三级分类内容中提取规范引用
+        all_references = []
+        reference_to_location = {}  # 用于追踪引用来源
+
+        for detail in tertiary_details:
+            content = detail.get("content", "")
+            if not content:
+                continue
+
+            location_info = {
+                "third_category_name": detail.get("third_category_name", ""),
+                "third_category_code": detail.get("third_category_code", ""),
+                "start_line": detail.get("start_line", 0),
+                "end_line": detail.get("end_line", 0),
+            }
+
+            refs = self.extractor.extract_from_content(content, location_info)
+            for ref in refs:
+                all_references.append(ref)
+                # 记录引用来源(用于后续结果关联)
+                if ref.original_text not in reference_to_location:
+                    reference_to_location[ref.original_text] = []
+                reference_to_location[ref.original_text].append(location_info)
+
+        if not all_references:
+            logger.info("未从三级分类内容中提取到规范引用")
+            return []
+
+        logger.info(f"从三级分类内容中提取到 {len(all_references)} 个规范引用")
+
+        # 2. 对提取的规范进行时效性审查
+        all_issues = []
+
+        # 分批处理(每批3个)
+        batch_size = 3
+        ref_texts = [ref.original_text for ref in all_references]
+        total_batches = (len(ref_texts) + batch_size - 1) // batch_size
+
+        for i in range(0, len(ref_texts), batch_size):
+            batch_refs = all_references[i:i + batch_size]
+            batch_texts = [ref.original_text for ref in batch_refs]
+            batch_num = i // batch_size + 1
+
+            try:
+                async with self._semaphore:
+                    # 搜索参考规范
+                    search_tasks = []
+                    for ref in batch_refs:
+                        task = asyncio.create_task(
+                            self._async_search_standard(ref.number, collection_name)
+                        )
+                        search_tasks.append(task)
+
+                    search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
+
+                    # 构建参考文本列表
+                    grouped_candidates = []
+                    for j, result in enumerate(search_results):
+                        if isinstance(result, Exception):
+                            logger.error(f"搜索失败 '{batch_refs[j].original_text}': {result}")
+                            grouped_candidates.append([])
+                        else:
+                            texts = [item.get("text_content", "") for item in result if item]
+                            grouped_candidates.append(texts)
+
+                    # 匹配参考文件
+                    match_result = await match_reference_files(
+                        reference_text=grouped_candidates,
+                        review_text=batch_texts
+                    )
+
+                    # 记录完整的匹配结果用于调试
+                    logger.info(f"批次{batch_num} match_reference_files 原始结果: {match_result[:500]}...")
+
+                    # 过滤:保留有相关信息的项进行审查
+                    # 条件:has_related_file为true 或 exact_match_info不为空 或 same_name_current不为空
+                    try:
+                        match_data = json.loads(match_result)
+                        items = match_data.get('items', match_data) if isinstance(match_data, dict) else match_data
+
+                        logger.info(f"批次{batch_num} 解析到 {len(items)} 个匹配项")
+                        for idx, item in enumerate(items):
+                            logger.info(f"  项{idx}: review_item={item.get('review_item', 'unknown')}, "
+                                      f"has_related_file={item.get('has_related_file')}, "
+                                      f"exact_match_info={item.get('exact_match_info')}, "
+                                      f"same_name_current={item.get('same_name_current')}")
+
+                        # 放宽过滤条件:只要有相关文件信息就进行审查
+                        filtered_data = [
+                            item for item in items
+                            if item.get('has_related_file') or
+                               item.get('exact_match_info') or
+                               item.get('same_name_current')
+                        ]
+
+                        logger.info(f"批次{batch_num} 过滤后保留 {len(filtered_data)} 个项")
+
+                        # 记录被过滤掉的项目用于调试
+                        skipped_items = [
+                            item for item in items
+                            if not (item.get('has_related_file') or
+                                   item.get('exact_match_info') or
+                                   item.get('same_name_current'))
+                        ]
+                        if skipped_items:
+                            logger.warning(f"批次{batch_num} 跳过了 {len(skipped_items)} 个无参考信息的项: "
+                                         f"{[item.get('review_item', 'unknown') for item in skipped_items]}")
+
+                        if not filtered_data:
+                            logger.info(f"批次{batch_num}: 没有符合审查条件的规范引用")
+                            continue
+
+                        # 重新构建JSON
+                        if isinstance(match_data, dict) and 'items' in match_data:
+                            match_result = json.dumps({"items": filtered_data}, ensure_ascii=False)
+                        else:
+                            match_result = json.dumps(filtered_data, ensure_ascii=False)
+
+                        # 判定时效性问题
+                        llm_out = await determine_timeliness_issue(match_result)
+
+                        # 处理响应
+                        standardized_result = self.response_processor.process_llm_response(
+                            llm_out,
+                            "content_timeliness_check",
+                            "content",
+                            "content_timeliness_check"
+                        )
+
+                        # 3. 增强结果:添加位置信息
+                        for item in standardized_result:
+                            review_item = item.get("check_result", {}).get("location", "")
+                            if review_item in reference_to_location:
+                                locations = reference_to_location[review_item]
+                                # 添加位置信息到结果
+                                item["location_info"] = locations
+                                # 添加三级分类上下文
+                                contexts = []
+                                for loc in locations:
+                                    ctx = f"[{loc.get('third_category_name', '')}] 第{loc.get('start_line', 0)}-{loc.get('end_line', 0)}行"
+                                    contexts.append(ctx)
+                                item["content_context"] = "; ".join(contexts)
+
+                                # 更新location字段为更详细的描述
+                                if contexts:
+                                    item["check_result"]["location"] = f"{review_item}(出现在:{item['content_context']})"
+
+                        all_issues.extend(standardized_result)
+
+                        # SSE推送(如果提供了progress_manager)
+                        if progress_manager and callback_task_id:
+                            try:
+                                await progress_manager.update_stage_progress(
+                                    callback_task_id=callback_task_id,
+                                    stage_name=f"内容时效性审查-批次{batch_num}",
+                                    status="processing",
+                                    message=f"完成第{batch_num}/{total_batches}批次内容时效性审查,{len(batch_refs)}项",
+                                    overall_task_status="processing",
+                                    event_type="processing",
+                                    issues=standardized_result
+                                )
+                            except Exception as e:
+                                logger.error(f"SSE推送失败: {e}")
+
+                    except (json.JSONDecodeError, TypeError) as e:
+                        logger.warning(f"处理匹配结果时出错: {e}")
+                        continue
+
+            except Exception as e:
+                logger.error(f"批次 {batch_num} 处理失败: {e}")
+                error_result = {
+                    "check_item": "content_timeliness_check",
+                    "chapter_code": "content",
+                    "check_item_code": "content_timeliness_check",
+                    "check_result": {"error": str(e), "batch_num": batch_num},
+                    "exist_issue": True,
+                    "risk_info": {"risk_level": "medium"}
+                }
+                all_issues.append(error_result)
+
+        # 统计结果
+        issue_count = sum(1 for item in all_issues if item.get("exist_issue", False))
+        logger.info(f"内容时效性审查完成:总计 {len(all_references)} 项引用,发现问题 {issue_count} 项")
+
+        return all_issues
+
+    async def _async_search_standard(
+        self,
+        standard_number: str,
+        collection_name: str,
+        top_k: int = 3
+    ) -> List[dict]:
+        """异步搜索单个规范"""
+        try:
+            loop = asyncio.get_running_loop()
+            func = partial(
+                self.search_engine.hybrid_search,
+                collection_name=collection_name,
+                query_text=standard_number,
+                top_k=top_k,
+                ranker_type="weighted",
+                dense_weight=0.3,
+                sparse_weight=0.7
+            )
+            retrieved = await loop.run_in_executor(None, func)
+            logger.debug(f"搜索 '{standard_number}' -> 找到 {len(retrieved or [])} 个结果")
+            return retrieved or []
+        except Exception as e:
+            logger.error(f"搜索失败 '{standard_number}': {e}")
+            return []
+
+
+# ===== 便捷函数 =====
+
+async def review_tertiary_content_timeliness(
+    tertiary_details: List[Dict[str, Any]],
+    collection_name: str = "first_bfp_collection_status",
+    max_concurrent: int = 4,
+    progress_manager=None,
+    callback_task_id: str = None
+) -> List[Dict[str, Any]]:
+    """
+    审查三级分类内容时效性的便捷函数
+
+    Args:
+        tertiary_details: 三级分类详情列表
+        collection_name: Milvus集合名称
+        max_concurrent: 最大并发数
+        progress_manager: 进度管理器(可选)
+        callback_task_id: 回调任务ID(可选)
+
+    Returns:
+        List[Dict]: 标准化的审查结果列表
+    """
+    async with ContentTimelinessReviewer(max_concurrent=max_concurrent) as reviewer:
+        return await reviewer.review_tertiary_content(
+            tertiary_details=tertiary_details,
+            collection_name=collection_name,
+            progress_manager=progress_manager,
+            callback_task_id=callback_task_id
+        )
+
+
+# ===== 测试代码 =====
+if __name__ == "__main__":
+    # 测试数据
+    test_tertiary_details = [
+        {
+            "third_category_name": "国家方针、政策、标准和设计文件",
+            "third_category_code": "NationalPoliciesStandardsAndDesignDocument",
+            "start_line": 80,
+            "end_line": 82,
+            "content": "<80> 国家方针、政策、标准和设计文件\n<81> 《公路工程技术标准》(JTG B01-2011)\n<82> 《公路桥涵设计通用规范》(JTG D60-2015)"
+        },
+        {
+            "third_category_name": "施工技术标准",
+            "third_category_code": "ConstructionTechnicalStandards",
+            "start_line": 100,
+            "end_line": 102,
+            "content": "<100> 施工技术标准\n<101> 《公路桥涵施工技术规范》(JTG/T 3650-2020)\n<102> 《混凝土结构设计规范》(GB 50010-2010)"
+        }
+    ]
+
+    print(f"测试 {len(test_tertiary_details)} 个三级分类内容...")
+
+    # 测试提取器
+    extractor = StandardExtractor()
+    for detail in test_tertiary_details:
+        refs = extractor.extract_from_content(detail["content"])
+        print(f"\n从 '{detail['third_category_name']}' 提取到 {len(refs)} 个规范引用:")
+        for ref in refs:
+            print(f"  - {ref.original_text}")
+
+    # 测试完整审查流程(需要Milvus连接)
+    # result = asyncio.run(review_tertiary_content_timeliness(test_tertiary_details))
+    # print("\n审查结果:")
+    # print(json.dumps(result, ensure_ascii=False, indent=2))

+ 22 - 1
core/construction_review/component/reviewers/utils/inter_tool.py

@@ -298,7 +298,7 @@ class InterTool:
                 reference_data = check_result.get('reference_basis_review_results', {})
                 batch_results = reference_data.get('review_results', [])
                 logger.debug(f"🔍 [DEBUG] 处理规范性审查结果,批次数: {len(batch_results)}")
-                
+
                 for batch in batch_results:
                     if isinstance(batch, list):
                         for item in batch:
@@ -323,6 +323,27 @@ class InterTool:
                 logger.info(f"🔍 规范性审查结果处理完成,添加 {len(review_lists)} 个问题项")
                 continue
 
+            # 🔧 特殊处理:timeliness_content_reviewer 的返回格式
+            if check_key == 'timeliness_content_reviewer' and isinstance(check_result, dict):
+                content_timeliness_data = check_result.get('timeliness_content_review_results', {})
+                batch_results = content_timeliness_data.get('review_results', [])
+                logger.debug(f"🔍 [DEBUG] 处理内容时效性审查结果,问题数: {len(batch_results)}")
+
+                for item in batch_results:
+                    if isinstance(item, dict):
+                        review_lists.append({
+                            "check_item": item.get('check_item', 'content_timeliness_check'),
+                            "chapter_code": item.get('chapter_code', chapter_code),
+                            "check_item_code": item.get('check_item_code', f"{chapter_code}_content_timeliness_check"),
+                            "check_result": item.get('check_result', item),
+                            "exist_issue": item.get('exist_issue', False),
+                            "risk_info": item.get('risk_info', {"risk_level": "low"}),
+                            "location_info": item.get('location_info', []),
+                            "content_context": item.get('content_context', '')
+                        })
+                logger.info(f"🔍 内容时效性审查结果处理完成,添加 {len(batch_results)} 个问题项")
+                continue
+
             # 🔧 类型安全检查:支持字典和 base_reviewer.ReviewResult 对象
             is_dict = isinstance(check_result, dict)
             is_review_result = hasattr(check_result, 'details') and hasattr(check_result, 'success')

+ 0 - 2330
core/construction_review/component/reviewers/utils/llm_content_classifier_v2.py

@@ -1,2330 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-"""
-LLM 内容三级分类识别模块
-
-根据 StandardCategoryTable.csv 的标准,让模型识别文档中的三级分类内容,
-输出 JSON 格式包含:三级分类名称、起止行号、原文内容
-
-特点:
-- 行级细粒度分类:返回每个三级分类的起止行号和原文内容
-- 多分类支持:一个段落可包含多个三级分类
-- 全局行号:维护全局连续行号,便于跨段落定位
-- Embedding 优化:相似度 >= 阈值时跳过 LLM,降低 API 成本
-- 分块处理:长段落自动分块,结果合并
-- 统一配置管理:从 config.ini 读取模型配置
-
-使用方式:
-1. 作为模块导入使用:
-   from llm_content_classifier_v2 import LLMContentClassifier, classify_chunks
-   result = await classify_chunks(chunks)
-
-2. 独立运行测试:
-   python llm_content_classifier_v2.py
-"""
-
-import asyncio
-import json
-import re
-import csv
-import time
-import math
-from pathlib import Path
-from typing import Dict, List, Optional, Tuple, Any
-from dataclasses import dataclass, field
-from openai import AsyncOpenAI
-
-# 导入统一配置处理器
-from foundation.infrastructure.config.config import config_handler
-from foundation.observability.logger.loggering import review_logger as logger
-
-
-# ==================== 配置类 ====================
-
-def _get_llm_config_from_ini(model_type: str) -> Tuple[str, str, str]:
-    """
-    从 config.ini 获取 LLM 配置
-
-    Args:
-        model_type: 模型类型(如 qwen3_5_122b_a10b)
-
-    Returns:
-        Tuple[str, str, str]: (api_key, base_url, model_id)
-    """
-    try:
-        # 尝试读取 DashScope 格式配置
-        base_url = config_handler.get(model_type, "DASHSCOPE_SERVER_URL", "")
-        model_id = config_handler.get(model_type, "DASHSCOPE_MODEL_ID", "")
-        api_key = config_handler.get(model_type, "DASHSCOPE_API_KEY", "")
-
-        # 如果没有 DashScope 配置,尝试读取其他格式
-        if not base_url:
-            # 尝试 QWEN_SERVER_URL 格式
-            base_url = config_handler.get(model_type, f"{model_type.upper()}_SERVER_URL", "")
-            model_id = config_handler.get(model_type, f"{model_type.upper()}_MODEL_ID", "")
-            api_key = config_handler.get(model_type, f"{model_type.upper()}_API_KEY", "")
-
-        return api_key, base_url, model_id
-    except Exception:
-        return "", "", ""
-
-
-def _get_embedding_config_from_ini(embedding_model_type: str) -> Tuple[str, str, str]:
-    """
-    从 config.ini 获取 Embedding 模型配置
-
-    Args:
-        embedding_model_type: Embedding 模型类型
-
-    Returns:
-        Tuple[str, str, str]: (api_key, base_url, model_id)
-    """
-    try:
-        # 本地 Embedding 模型
-        if embedding_model_type == "lq_qwen3_8b_emd":
-            base_url = config_handler.get("lq_qwen3_8b_emd", "LQ_EMBEDDING_SERVER_URL", "")
-            model_id = config_handler.get("lq_qwen3_8b_emd", "LQ_EMBEDDING_MODEL_ID", "Qwen3-Embedding-8B")
-            api_key = config_handler.get("lq_qwen3_8b_emd", "LQ_EMBEDDING_API_KEY", "dummy")
-            return api_key, base_url, model_id
-
-        # 硅基流动 Embedding 模型
-        elif embedding_model_type == "siliconflow_embed":
-            base_url = config_handler.get("siliconflow_embed", "SLCF_EMBED_SERVER_URL", "")
-            model_id = config_handler.get("siliconflow_embed", "SLCF_EMBED_MODEL_ID", "Qwen/Qwen3-Embedding-8B")
-            api_key = config_handler.get("siliconflow_embed", "SLCF_EMBED_API_KEY", "")
-            return api_key, base_url, model_id
-
-        return "", "", ""
-    except Exception:
-        return "", "", ""
-
-
-@dataclass
-class ClassifierConfig:
-    """分类器配置(从 config.ini 加载)"""
-
-    # LLM API 配置(从 config.ini 加载)
-    api_key: str = ""
-    base_url: str = ""
-    model: str = ""
-
-    # 并发控制
-    max_concurrent_requests: int = 10
-    max_retries: int = 3
-    retry_delay: int = 1
-
-    # Embedding 配置(从 config.ini 加载)
-    embedding_api_key: str = ""
-    embedding_base_url: str = ""
-    embedding_model: str = ""
-    embedding_similarity_threshold: float = 0.9
-
-    # 路径配置
-    category_table_path: str = ""
-    second_category_path: str = ""
-    output_path: str = ""
-
-    def __post_init__(self):
-        """从 config.ini 加载配置"""
-        # 加载 LLM 配置
-        llm_model_type = config_handler.get("model", "COMPLETENESS_REVIEW_MODEL_TYPE", "qwen3_5_122b_a10b")
-        api_key, base_url, model_id = _get_llm_config_from_ini(llm_model_type)
-
-        # 设置 LLM 配置(如果从 config.ini 读取成功)
-        if api_key:
-            self.api_key = api_key
-        if base_url:
-            self.base_url = base_url
-        if model_id:
-            self.model = model_id
-
-        # 加载 Embedding 配置
-        embedding_model_type = config_handler.get("model", "EMBEDDING_MODEL_TYPE", "lq_qwen3_8b_emd")
-        emb_api_key, emb_base_url, emb_model_id = _get_embedding_config_from_ini(embedding_model_type)
-
-        if emb_api_key:
-            self.embedding_api_key = emb_api_key
-        if emb_base_url:
-            self.embedding_base_url = emb_base_url
-        if emb_model_id:
-            self.embedding_model = emb_model_id
-
-        # 初始化默认路径
-        if not self.category_table_path:
-            self.category_table_path = str(
-                Path(__file__).parent.parent.parent / "doc_worker" / "config" / "StandardCategoryTable.csv"
-            )
-        if not self.second_category_path:
-            self.second_category_path = str(
-                Path(__file__).parent.parent.parent / "doc_worker" / "config" / "construction_plan_standards.csv"
-            )
-        if not self.output_path:
-            # 项目根目录下的 temp/construction_review/llm_content_classifier_v2
-            project_root = Path(__file__).parent.parent.parent.parent.parent.parent
-            self.output_path = str(project_root / "temp" / "construction_review" / "llm_content_classifier_v2")
-
-
-# 默认配置实例(从 config.ini 加载,用于独立运行测试)
-DEFAULT_CONFIG = ClassifierConfig()
-
-# 向后兼容的全局变量(供独立运行测试使用,从 config.ini 加载)
-API_KEY = DEFAULT_CONFIG.api_key
-MAX_CONCURRENT_REQUESTS = DEFAULT_CONFIG.max_concurrent_requests
-MAX_RETRIES = DEFAULT_CONFIG.max_retries
-RETRY_DELAY = DEFAULT_CONFIG.retry_delay
-BASE_URL = DEFAULT_CONFIG.base_url
-MODEL = DEFAULT_CONFIG.model
-EMBEDDING_API_KEY = DEFAULT_CONFIG.embedding_api_key
-EMBEDDING_BASE_URL = DEFAULT_CONFIG.embedding_base_url
-EMBEDDING_MODEL = DEFAULT_CONFIG.embedding_model
-EMBEDDING_SIMILARITY_THRESHOLD = DEFAULT_CONFIG.embedding_similarity_threshold
-CATEGORY_TABLE_PATH = Path(DEFAULT_CONFIG.category_table_path)
-SECOND_CATEGORY_PATH = Path(DEFAULT_CONFIG.second_category_path)
-
-
-# ==================== 数据模型 ====================
-
-@dataclass
-class CategoryStandard:
-    """标准分类定义"""
-    first_code: str
-    first_name: str
-    second_code: str
-    second_name: str
-    second_focus: str  # 二级分类关注点
-    third_code: str
-    third_name: str
-    third_focus: str
-    keywords: str = ""
-
-
-@dataclass
-class SecondCategoryStandard:
-    """二级分类标准定义(来自construction_plan_standards.csv)"""
-    first_name: str  # 一级分类中文名
-    second_name: str  # 二级分类中文名
-    second_raw_content: str  # 二级分类详细描述
-
-
-@dataclass
-class ClassifiedContent:
-    """分类结果"""
-    third_category_name: str  # 三级分类名称
-    third_category_code: str  # 三级分类代码
-    start_line: int
-    end_line: int
-    content: str  # 原文内容
-
-
-@dataclass
-class SectionContent:
-    """二级标题内容"""
-    section_key: str  # 如 "第一章->一"
-    section_name: str  # 如 "一)编制依据"
-    lines: List[str]  # 原始行列表
-    numbered_content: str  # 带行号的内容
-    category_standards: List[CategoryStandard] = field(default_factory=list)  # 该二级分类下的三级标准
-    line_number_map: List[int] = field(default_factory=list)  # 每行对应的全局行号(如果有)
-    chunk_ranges: List[Tuple[str, int, int]] = field(default_factory=list)  # [(chunk_id, global_start, global_end), ...]
-
-
-@dataclass
-class ClassificationResult:
-    """分类结果"""
-    model: str
-    section_key: str
-    section_name: str
-    classified_contents: List[ClassifiedContent]
-    latency: float
-    raw_response: str = ""
-    error: Optional[str] = None
-    total_lines: int = 0  # 该section的总行数
-    classified_lines: int = 0  # 已分类的行数
-    coverage_rate: float = 0.0  # 分类率(已分类行数/总行数)
-
-
-# ==================== 二级分类关键词映射 ====================
-# 用于将文档中的二级标题名称映射到 StandardCategoryTable.csv 中的标准名称
-# 格式: { CSV标准名称: [可能的文档名称列表] }
-SECONDARY_CATEGORY_KEYWORDS = {
-    # 编制依据 (basis)
-    "法律法规": ["法律法规", "法律", "法规"],
-    "标准规范": ["标准规范", "标准", "规范", "技术标准"],
-    "文件制度": ["文件制度", "制度文件", "管理文件"],
-    "编制原则": ["编制原则", "原则"],
-    "编制范围": ["编制范围", "范围", "工程范围"],
-
-    # 工程概况 (overview)
-    "设计概况": ["设计概况", "工程简介", "工程概况", "概况"],
-    "工程地质与水文气象": ["工程地质与水文气象", "地质", "水文", "气象", "工程地质", "水文气象", "地质与水文"],
-    "周边环境": ["周边环境", "环境", "周围环境"],
-    "施工平面及立面布置": ["施工平面及立面布置", "平面布置", "立面布置", "施工平面", "平面及立面"],
-    "施工要求和技术保证条件": ["施工要求和技术保证条件", "施工要求", "技术保证", "保证条件"],
-    "风险辨识与分级": ["风险辨识与分级", "风险辨识", "风险分级", "风险", "风险等级"],
-    "参建各方责任主体单位": ["参建各方责任主体单位", "参建单位", "责任主体", "参建各方"],
-
-    # 施工计划 (plan)
-    "施工进度计划": ["施工进度计划", "进度计划", "进度", "工期计划"],
-    "施工材料计划": ["施工材料计划", "材料计划", "材料"],
-    "施工设备计划": ["施工设备计划", "设备计划", "机械设备", "设备"],
-    "劳动力计划": ["劳动力计划", "劳动力", "人员计划", "用工计划"],
-    "安全生产费用使用计划": ["安全生产费用使用计划", "安全费用", "安全费", "安全生产费用"],
-
-    # 施工工艺技术 (technology)
-    "主要施工方法概述": ["主要施工方法概述", "施工方法概述", "方法概述", "施工方法"],
-    "技术参数": ["技术参数", "参数", "技术指标"],
-    "工艺流程": ["工艺流程", "流程", "施工流程"],
-    "施工准备": ["施工准备", "准备", "准备工作"],
-    "施工方法及操作要求": ["施工方法及操作要求", "施工方案及操作要求", "操作要求", "施工方案", "施工方法", "方法及操作"],
-    "检查要求": ["检查要求", "检查", "验收要求", "检查验收"],
-
-    # 安全保证措施 (safety)
-    "安全保证体系": ["安全保证体系", "安全体系", "安全管理体系"],
-    "组织保证措施": ["组织保证措施", "组织措施", "组织保证"],
-    "技术保证措施": ["技术保证措施", "技术保障措施", "技术措施", "保障措施", "技术保障", "安全防护措施", "安全防护"],
-    "监测监控措施": ["监测监控措施", "监测措施", "监控措施", "监测监控"],
-    "应急处置措施": ["应急处置措施", "应急预案", "应急措施", "应急处置"],
-
-    # 质量保证措施 (quality)
-    "质量保证体系": ["质量保证体系", "质量体系", "质量管理体系"],
-    "质量目标": ["质量目标", "质量指标"],
-    "工程创优规划": ["工程创优规划", "创优规划", "创优计划", "创优"],
-    "质量控制程序与具体措施": ["质量控制程序与具体措施", "质量控制", "质量措施", "质量控制措施"],
-
-    # 环境保证措施 (environment)
-    "环境保证体系": ["环境保证体系", "环境体系", "环境管理体系"],
-    "环境保护组织机构": ["环境保护组织机构", "环保组织", "环境组织"],
-    "环境保护及文明施工措施": ["环境保护及文明施工措施", "环保措施", "文明施工", "环境保护", "环境措施"],
-
-    # 施工管理及作业人员配备与分工 (management)
-    "施工管理人员": ["施工管理人员", "管理人员", "管理人员配备"],
-    "专职安全生产管理人员": ["专职安全生产管理人员", "专职安全员", "安全管理人员", "安全员", "特种作业人员", "特种工"],
-    "其他作业人员": ["其他作业人员", "其他人员", "作业人员"],
-
-    # 验收要求 (acceptance)
-    "验收标准": ["验收标准", "验收规范", "标准"],
-    "验收程序": ["验收程序", "验收流程", "程序"],
-    "验收内容": ["验收内容", "验收项目"],
-    "验收时间": ["验收时间", "验收日期"],
-    "验收人员": ["验收人员", "验收参与人员"],
-
-    # 其他资料 (other)
-    "计算书": ["计算书", "计算", "验算"],
-    "相关施工图纸": ["相关施工图纸", "施工图纸", "图纸"],
-    "附图附表": ["附图附表", "附图", "附表"],
-    "编制及审核人员情况": ["编制及审核人员情况", "编制人员", "审核人员"],
-}
-
-
-# ==================== 标准分类加载器 ====================
-
-class CategoryStandardLoader:
-    """加载 StandardCategoryTable.csv"""
-
-    def __init__(self, csv_path: Path):
-        self.csv_path = csv_path
-        self.standards: List[CategoryStandard] = []
-        self._load()
-
-    def _load(self):
-        """加载CSV文件"""
-        with open(self.csv_path, 'r', encoding='utf-8-sig') as f:  # utf-8-sig处理BOM
-            reader = csv.DictReader(f)
-            for row in reader:
-                self.standards.append(CategoryStandard(
-                    first_code=row.get('first_code', ''),
-                    first_name=row.get('first_name', ''),
-                    second_code=row.get('second_code', ''),
-                    second_name=row.get('second_name', ''),
-                    second_focus=row.get('second_focus', ''),
-                    third_code=row.get('third_code', ''),
-                    third_name=row.get('third_name', ''),
-                    third_focus=row.get('third_focus', ''),
-                    keywords=row.get('keywords', '')
-                ))
-
-    def get_standards_by_second_code(self, second_code: str) -> List[CategoryStandard]:
-        """根据二级分类代码获取对应的三级分类标准"""
-        return [s for s in self.standards if s.second_code == second_code]
-
-    def _find_standard_name_by_keyword(self, second_name: str) -> Optional[str]:
-        """
-        通过关键词映射查找标准二级分类名称
-
-        Args:
-            second_name: 文档中的二级标题名称
-
-        Returns:
-            匹配到的标准名称,未匹配返回None
-        """
-        cleaned_name = second_name.strip().lower()
-
-        # 遍历映射表进行匹配
-        for standard_name, keywords in SECONDARY_CATEGORY_KEYWORDS.items():
-            for keyword in keywords:
-                # 宽容匹配:关键词在标题中,或标题在关键词中
-                if keyword.lower() in cleaned_name or cleaned_name in keyword.lower():
-                    return standard_name
-
-        return None
-
-    def get_standards_by_second_name(self, second_name: str) -> List[CategoryStandard]:
-        """
-        根据二级分类名称获取对应的三级分类标准(支持模糊匹配)
-
-        匹配优先级:
-        1. 完全匹配 CSV 中的标准名称
-        2. 包含关系匹配(标准名包含标题名,或标题名包含标准名)
-        3. 关键词映射匹配(通过 SECONDARY_CATEGORY_KEYWORDS)
-
-        Args:
-            second_name: 二级标题名称
-
-        Returns:
-            匹配到的三级分类标准列表
-        """
-        cleaned_name = second_name.strip()
-
-        # 1. 先尝试完全匹配
-        exact = [s for s in self.standards if s.second_name == cleaned_name]
-        if exact:
-            return exact
-
-        # 2. 包含关系匹配(取第一个命中的 second_name,再返回同名的全部行)
-        for s in self.standards:
-            if s.second_name in cleaned_name or cleaned_name in s.second_name:
-                matched_name = s.second_name
-                return [st for st in self.standards if st.second_name == matched_name]
-
-        # 3. 使用关键词映射进行模糊匹配
-        matched_standard_name = self._find_standard_name_by_keyword(cleaned_name)
-        if matched_standard_name:
-            return [s for s in self.standards if s.second_name == matched_standard_name]
-
-        return []
-
-
-class SecondCategoryStandardLoader:
-    """加载 construction_plan_standards.csv(二级分类标准)"""
-
-    def __init__(self, csv_path: Path):
-        self.csv_path = csv_path
-        self.standards: List[SecondCategoryStandard] = []
-        self._load()
-
-    def _load(self):
-        """加载CSV文件"""
-        with open(self.csv_path, 'r', encoding='utf-8-sig') as f:  # utf-8-sig处理BOM
-            reader = csv.DictReader(f)
-            for row in reader:
-                self.standards.append(SecondCategoryStandard(
-                    first_name=row.get('first_name', '').strip(),
-                    second_name=row.get('second_name', '').strip(),
-                    second_raw_content=row.get('second_raw_content', '').strip()
-                ))
-
-    def get_standard_by_second_name(self, second_name: str) -> Optional[SecondCategoryStandard]:
-        """根据二级分类名称获取标准定义(支持模糊匹配)"""
-        # 清理待匹配的名称
-        cleaned_name = second_name.strip().lower()
-
-        # 1. 先尝试完全匹配或包含关系匹配
-        for std in self.standards:
-            # 完全匹配
-            if std.second_name.lower() == cleaned_name:
-                return std
-            # 包含关系匹配
-            if std.second_name.lower() in cleaned_name or cleaned_name in std.second_name.lower():
-                return std
-
-        # 2. 使用关键词映射进行模糊匹配
-        matched_standard_name = None
-        for standard_name, keywords in SECONDARY_CATEGORY_KEYWORDS.items():
-            for keyword in keywords:
-                if keyword.lower() in cleaned_name or cleaned_name in keyword.lower():
-                    matched_standard_name = standard_name
-                    break
-            if matched_standard_name:
-                break
-
-        if matched_standard_name:
-            # 在standards中查找匹配的标准
-            for std in self.standards:
-                if std.second_name == matched_standard_name:
-                    return std
-
-        return None
-
-
-# ==================== Embedding 客户端 ====================
-
-class EmbeddingClient:
-    """Embedding模型客户端,用于计算文本相似度"""
-
-    def __init__(self):
-        self.client = AsyncOpenAI(
-            api_key=EMBEDDING_API_KEY,
-            base_url=EMBEDDING_BASE_URL
-        )
-        self.model = EMBEDDING_MODEL
-
-    async def get_embedding(self, text: str) -> Optional[List[float]]:
-        """获取文本的embedding向量"""
-        try:
-            response = await self.client.embeddings.create(
-                model=self.model,
-                input=text
-            )
-            if response.data and len(response.data) > 0:
-                return response.data[0].embedding
-            return None
-        except Exception as e:
-            logger.error(f"Embedding API调用失败: {e}")
-            return None
-
-    async def get_embeddings_batch(self, texts: List[str]) -> List[Optional[List[float]]]:
-        """批量获取文本的embedding向量"""
-        try:
-            response = await self.client.embeddings.create(
-                model=self.model,
-                input=texts
-            )
-            results = []
-            for item in response.data:
-                results.append(item.embedding)
-            return results
-        except Exception as e:
-            logger.error(f"Embedding API批量调用失败: {e}")
-            return [None] * len(texts)
-
-    def cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
-        """计算两个向量的余弦相似度"""
-        if not vec1 or not vec2 or len(vec1) != len(vec2):
-            return 0.0
-
-        dot_product = sum(a * b for a, b in zip(vec1, vec2))
-        norm1 = math.sqrt(sum(a * a for a in vec1))
-        norm2 = math.sqrt(sum(b * b for b in vec2))
-
-        if norm1 == 0 or norm2 == 0:
-            return 0.0
-
-        return dot_product / (norm1 * norm2)
-
-    def _clean_section_name(self, section_name: str) -> str:
-        """清理section名称,去除序号等前缀
-
-        例如:
-        - "一)编制依据" -> "编制依据"
-        - "二) 技术保证措施" -> "技术保证措施"
-        - "1. 施工计划" -> "施工计划"
-        - "(1) 工艺流程" -> "工艺流程"
-        """
-        cleaned = section_name.strip()
-
-        # 去除开头的序号模式:
-        # 1. 中文数字+)或中文数字+、 如 "一)"、"二、"
-        # 2. 阿拉伯数字+. 或阿拉伯数字+)如 "1.", "2)"
-        # 3. 括号数字如 "(1)", "(一)"
-        patterns = [
-            r'^[一二三四五六七八九十百千]+[)\\)、\\.\\s]+',  # 中文数字+标点
-            r'^\\d+[\\.\\)\\)、\\s]+',  # 阿拉伯数字+标点
-            r'^[((]\\d+[))][\\s\\.]*',  # 括号数字
-            r'^[((][一二三四五六七八九十][))][\\s\\.]*',  # 括号中文数字
-        ]
-
-        for pattern in patterns:
-            cleaned = re.sub(pattern, '', cleaned)
-
-        return cleaned.strip()
-
-    async def check_similarity(
-        self,
-        section_name: str,
-        section_content: str,
-        second_category_name: str,
-        second_category_raw_content: str = ""
-    ) -> Tuple[bool, float]:
-        """
-        检查待审查内容与二级分类标准的相似度
-
-        比较:
-        - 左侧: section的实际内容(待审查的施工方案内容)
-        - 右侧: second_raw_content(来自construction_plan_standards.csv的标准定义)
-
-        返回: (is_similar, similarity_score)
-        - is_similar: 是否相似(相似度 > 阈值 或标题完全匹配)
-        - similarity_score: 相似度分数 (0-1)
-        """
-        # 步骤1: 先判断标题是否匹配
-        # 清理文本进行比较(去除序号等前缀)
-        cleaned_section_name = self._clean_section_name(section_name).lower()
-        cleaned_second_name = second_category_name.strip().lower()
-
-        # 标题直接相等检查(清理后的)
-        if cleaned_section_name == cleaned_second_name:
-            # 标题匹配,继续用embedding比较内容相似度
-            pass
-        else:
-            # 标题不匹配,检查是否包含关系
-            if cleaned_second_name in cleaned_section_name or cleaned_section_name in cleaned_second_name:
-                # 要求包含的部分至少4个字符,避免短词误判
-                if len(cleaned_second_name) >= 4 or len(cleaned_section_name) >= 4:
-                    # 标题部分匹配,继续用embedding比较内容
-                    pass
-                else:
-                    # 标题不匹配且太短,直接返回不相似
-                    return False, 0.0
-            else:
-                # 标题完全不匹配,直接返回不相似
-                return False, 0.0
-
-        # 步骤2: 使用embedding计算内容相似度
-        # 左侧: section的实际内容(待审查的施工方案实际内容)
-        # 右侧: second_raw_content(该second_name的标准定义)
-        section_text = section_content[:800]  # 取前800字符的实际内容
-        category_text = second_category_raw_content[:800] if second_category_raw_content else second_category_name
-
-        # 获取embedding
-        embeddings = await self.get_embeddings_batch([section_text, category_text])
-
-        if embeddings[0] is None or embeddings[1] is None:
-            # embedding获取失败,保守起见返回不相似
-            return False, 0.0
-
-        # 计算相似度
-        similarity = self.cosine_similarity(embeddings[0], embeddings[1])
-
-        # 判断结果
-        is_similar = similarity >= EMBEDDING_SIMILARITY_THRESHOLD
-
-        return is_similar, similarity
-
-
-# ==================== LLM 客户端 ====================
-
-class ContentClassifierClient:
-    """LLM 内容分类客户端"""
-
-    def __init__(self, model: str, semaphore: asyncio.Semaphore, embedding_client: Optional[EmbeddingClient] = None, second_category_loader: Optional[SecondCategoryStandardLoader] = None):
-        self.model = model
-        self.semaphore = semaphore
-        self.client = AsyncOpenAI(
-            api_key=API_KEY,
-            base_url=BASE_URL
-        )
-        self.embedding_client = embedding_client
-        self.second_category_loader = second_category_loader
-
-    async def classify_content(self, section: SectionContent) -> ClassificationResult:
-        """对内容进行三级分类识别(带并发控制和自动修复,支持长内容分块处理)"""
-        start_time = time.time()
-
-        # 步骤1: 使用Embedding模型检查二级分类与内容的相似度
-        if self.embedding_client and self.second_category_loader and section.category_standards:
-            # 从construction_plan_standards.csv中查找对应的标准二级分类
-            # 使用section_name进行匹配
-            std_second_category = self.second_category_loader.get_standard_by_second_name(section.section_name)
-
-            if std_second_category:
-                # 找到了对应的标准二级分类,进行相似度检查
-                # 检查section内容与标准的second_raw_content的一致性
-                section_text = '\n'.join(section.lines)
-                is_similar, similarity = await self.embedding_client.check_similarity(
-                    section_name=section.section_name,
-                    section_content=section_text,
-                    second_category_name=std_second_category.second_name,
-                    second_category_raw_content=std_second_category.second_raw_content
-                )
-
-                if is_similar:
-                    logger.debug(f"[{section.section_name}] 相似度检查通过 ({similarity:.3f} >= {EMBEDDING_SIMILARITY_THRESHOLD}),跳过LLM分类,默认包含所有三级分类")
-                    # 生成默认分类结果:包含所有三级分类
-                    all_contents = self._generate_default_classification(section)
-                    total_lines, classified_lines, coverage_rate = self._calculate_coverage_rate(section, all_contents)
-                    latency = time.time() - start_time
-                    return ClassificationResult(
-                        model=self.model,
-                        section_key=section.section_key,
-                        section_name=section.section_name,
-                        classified_contents=all_contents,
-                        latency=latency,
-                        raw_response=f"[Embedding相似度跳过] similarity={similarity:.3f}",
-                        error=None,
-                        total_lines=total_lines,
-                        classified_lines=classified_lines,
-                        coverage_rate=coverage_rate
-                    )
-                else:
-                    logger.debug(f"[{section.section_name}] 相似度检查未通过 ({similarity:.3f} < {EMBEDDING_SIMILARITY_THRESHOLD}),继续LLM分类")
-            else:
-                logger.debug(f"[{section.section_name}] 未在construction_plan_standards.csv中找到对应标准,继续LLM分类")
-
-        # 如果内容过长,分块处理
-        MAX_LINES_PER_CHUNK = 150  # 每个块最多150行
-        total_lines = len(section.lines)
-
-        if total_lines <= MAX_LINES_PER_CHUNK:
-            # 内容不长,直接处理
-            result = await self._classify_single_chunk(section, start_time)
-            # 补充验证:关键字扫描 + LLM二次确认,补充遗漏的分类
-            if not result.error and result.classified_contents is not None:
-                supplement = await self._detect_and_supplement(section, result.classified_contents)
-                if supplement:
-                    merged = self._merge_classified_contents(result.classified_contents + supplement, section)
-                    total_l, classified_l, coverage_r = self._calculate_coverage_rate(section, merged)
-                    return ClassificationResult(
-                        model=result.model,
-                        section_key=result.section_key,
-                        section_name=result.section_name,
-                        classified_contents=merged,
-                        latency=result.latency,
-                        raw_response=result.raw_response,
-                        error=result.error,
-                        total_lines=total_l,
-                        classified_lines=classified_l,
-                        coverage_rate=coverage_r
-                    )
-            return result
-
-        # 内容过长,无重叠分块处理
-        # 不使用 overlap:有重叠时边界行被两块各看一次反而容易两头都不认领,
-        # 无重叠时每行只属于唯一一块,prompt 里的"必须分类每一行"约束更有效。
-        logger.debug(f"[{section.section_name}] 内容较长({total_lines}行),分块处理...")
-        all_contents = []
-        chunk_size = MAX_LINES_PER_CHUNK
-
-        chunk_start = 0
-        while chunk_start < total_lines:
-            chunk_end = min(chunk_start + chunk_size, total_lines)
-            chunk_section = self._create_chunk_section(section, chunk_start, chunk_end)
-
-            chunk_result = await self._classify_single_chunk(chunk_section, 0, is_chunk=True)
-
-            if chunk_result.error:
-                logger.error(f"[{section.section_name}] 块 {chunk_start+1}-{chunk_end} 处理失败: {chunk_result.error[:50]}")
-            else:
-                logger.debug(f"[{section.section_name}] 块 {chunk_start+1}-{chunk_end} 成功: {len(chunk_result.classified_contents)} 个分类")
-                all_contents.extend(chunk_result.classified_contents)
-
-            # 无重叠:下一块从当前块末尾紧接开始
-            chunk_start = chunk_end
-
-        # 所有块处理完成后,再次聚合所有内容(解决分块导致的同一分类分散问题)
-        if all_contents:
-            all_contents = self._merge_classified_contents(all_contents, section)
-
-        # 补充验证:关键字扫描 + LLM二次确认,补充遗漏的分类
-        supplement = await self._detect_and_supplement(section, all_contents)
-        if supplement:
-            all_contents = self._merge_classified_contents(all_contents + supplement, section)
-
-        # 计算分类率
-        total_lines, classified_lines, coverage_rate = self._calculate_coverage_rate(section, all_contents)
-
-        latency = time.time() - start_time
-
-        return ClassificationResult(
-            model=self.model,
-            section_key=section.section_key,
-            section_name=section.section_name,
-            classified_contents=all_contents,
-            latency=latency,
-            raw_response="",
-            error=None if all_contents else "所有块处理失败",
-            total_lines=total_lines,
-            classified_lines=classified_lines,
-            coverage_rate=coverage_rate
-        )
-
-    def _calculate_coverage_rate(self, section: SectionContent, contents: List[ClassifiedContent]) -> tuple:
-        """计算分类率(已分类行数/总行数)"""
-        total_lines = len(section.lines)
-        if total_lines == 0 or not contents:
-            return total_lines, 0, 0.0
-
-        # 使用集合记录已分类的行号(避免重复计数)
-        classified_line_set = set()
-
-        for content in contents:
-            if section.line_number_map:
-                # 如果有全局行号映射,找出起止行号对应的索引
-                start_idx = -1
-                end_idx = -1
-                for idx, global_line in enumerate(section.line_number_map):
-                    if global_line == content.start_line:
-                        start_idx = idx
-                    if global_line == content.end_line:
-                        end_idx = idx
-                        break
-
-                if start_idx != -1 and end_idx != -1:
-                    for i in range(start_idx, end_idx + 1):
-                        if i < len(section.line_number_map):
-                            classified_line_set.add(section.line_number_map[i])
-            else:
-                # 没有全局行号,直接使用起止行号
-                for line_num in range(content.start_line, content.end_line + 1):
-                    classified_line_set.add(line_num)
-
-        classified_lines = len(classified_line_set)
-        coverage_rate = (classified_lines / total_lines) * 100 if total_lines > 0 else 0.0
-
-        return total_lines, classified_lines, coverage_rate
-
-    def _generate_default_classification(self, section: SectionContent) -> List[ClassifiedContent]:
-        """
-        生成默认的分类结果(当embedding相似度检查通过时使用)
-        默认包含所有三级分类,覆盖整个section内容
-        """
-        if not section.category_standards:
-            return []
-
-        # 获取全局行号范围
-        if section.line_number_map:
-            start_line = section.line_number_map[0]
-            end_line = section.line_number_map[-1]
-        else:
-            start_line = 1
-            end_line = len(section.lines)
-
-        # 为每个三级分类创建一个条目,覆盖全部内容
-        default_contents = []
-        for std in section.category_standards:
-            # 提取该分类对应的内容
-            content = self._extract_content_by_line_numbers(section, start_line, end_line)
-            default_contents.append(ClassifiedContent(
-                third_category_name=std.third_name,
-                third_category_code=std.third_code,
-                start_line=start_line,
-                end_line=end_line,
-                content=content
-            ))
-
-        return default_contents
-
-    def _create_chunk_section(self, section: SectionContent, start_idx: int, end_idx: int) -> SectionContent:
-        """从section创建子块"""
-        chunk_lines = section.lines[start_idx:end_idx]
-        chunk_line_map = section.line_number_map[start_idx:end_idx] if section.line_number_map else list(range(start_idx + 1, end_idx + 1))
-
-        # 生成带行号的内容
-        numbered_content = '\n'.join([f"<{chunk_line_map[i]}> {line}" for i, line in enumerate(chunk_lines)])
-
-        return SectionContent(
-            section_key=f"{section.section_key}_chunk_{start_idx}_{end_idx}",
-            section_name=section.section_name,
-            lines=chunk_lines,
-            numbered_content=numbered_content,
-            category_standards=section.category_standards,
-            line_number_map=chunk_line_map
-        )
-
-    async def _classify_single_chunk(self, section: SectionContent, start_time: float, is_chunk: bool = False) -> ClassificationResult:
-        """处理单个块"""
-        prompt = self._build_prompt(section, is_chunk=is_chunk)
-
-        try:
-            async with self.semaphore:
-                response = await self._call_api(prompt)
-
-            classified_contents, parse_error = await self._parse_with_fix(response, section, prompt)
-
-            if not is_chunk:
-                latency = time.time() - start_time
-                # 计算分类率
-                total_lines, classified_lines, coverage_rate = self._calculate_coverage_rate(section, classified_contents)
-                return ClassificationResult(
-                    model=self.model,
-                    section_key=section.section_key,
-                    section_name=section.section_name,
-                    classified_contents=classified_contents,
-                    latency=latency,
-                    raw_response=response[:1000],
-                    error=parse_error,
-                    total_lines=total_lines,
-                    classified_lines=classified_lines,
-                    coverage_rate=coverage_rate
-                )
-            else:
-                return ClassificationResult(
-                    model=self.model,
-                    section_key=section.section_key,
-                    section_name=section.section_name,
-                    classified_contents=classified_contents,
-                    latency=0,
-                    raw_response="",
-                    error=parse_error
-                )
-        except Exception as e:
-            if not is_chunk:
-                latency = time.time() - start_time
-                return ClassificationResult(
-                    model=self.model,
-                    section_key=section.section_key,
-                    section_name=section.section_name,
-                    classified_contents=[],
-                    latency=latency,
-                    error=str(e)
-                )
-            else:
-                return ClassificationResult(
-                    model=self.model,
-                    section_key=section.section_key,
-                    section_name=section.section_name,
-                    classified_contents=[],
-                    latency=0,
-                    error=str(e)
-                )
-
-    async def _parse_with_fix(self, response: str, section: SectionContent, original_prompt: str = "") -> tuple:
-        """解析响应,失败时让模型修复(最多3次重试)
-
-        返回: (contents, error_msg)
-        - contents: 分类结果列表(可能为空,表示模型判定无匹配内容)
-        - error_msg: 错误信息,None表示成功(包括空结果),非None表示解析失败
-        """
-        # 第一次尝试解析
-        contents, parse_success = self._parse_response(response, section)
-
-        # 解析成功(包括空结果,表示模型判定内容不符合任何分类标准)
-        if parse_success:
-            if not contents:
-                logger.debug(f"[{section.section_name}] 模型判定无匹配内容,记录为未分类")
-            return contents, None
-
-        # 解析失败(JSON格式错误),尝试让模型修复(最多3次)
-        logger.warning(f"[{section.section_name}] JSON解析失败,请求模型修复...")
-        logger.debug(f"[{section.section_name}] 原始响应前200字符: {response[:200]}...")
-
-        original_response = response
-
-        for attempt in range(3):
-            fix_prompt = self._build_fix_prompt(original_response)
-
-            try:
-                async with self.semaphore:
-                    fixed_response = await self._call_api(fix_prompt)
-
-                # 尝试解析修复后的输出
-                contents, parse_success = self._parse_response(fixed_response, section)
-                if parse_success:
-                    logger.debug(f"[{section.section_name}] 模型修复成功(第{attempt+1}次)")
-                    if not contents:
-                        logger.debug(f"[{section.section_name}] 修复后模型判定无匹配内容,记录为未分类")
-                    return contents, None
-                else:
-                    logger.debug(f"[{section.section_name}] 第{attempt+1}次修复失败,继续重试...")
-                    original_response = fixed_response
-            except Exception as e:
-                return [], f"请求模型修复失败: {str(e)}"
-
-        logger.error(f"[{section.section_name}] 模型修复3次后仍无法解析JSON")
-        return [], "模型修复3次后仍无法解析JSON"
-
-    def _build_fix_prompt(self, original_response: str) -> str:
-        """构建JSON修复提示词"""
-        return f"""你之前的输出存在JSON格式错误,请修复以下内容为正确的JSON格式。
-
-## 修复要求
-1. 严格保持原始数据的完整性和内容,不要修改任何业务数据
-2. 只修复JSON语法错误(如缺少逗号、括号不匹配、引号问题等)
-3. 确保输出的是合法的JSON格式
-4. 【重要】category_index 必须是数字索引(0, 1, 2...),禁止输出文本名称或代码
-5. 输出必须严格符合以下结构:
-{{
-    "classified_contents_list": [
-        {{
-            "category_index": 数字索引号,
-            "start_line": 数字,
-            "end_line": 数字
-        }}
-    ]
-}}
-
-## 原始输出(需要修复的内容)
-```
-{original_response[:6000]}
-```
-
-注意:
-- 只输出JSON,不要任何解释文字
-- 如果原始内容被截断,修复已提供的部分即可
-- category_index 只能是数字,如 0(非标准项)、1、2、3..."""
-
-    def _build_prompt(self, section: SectionContent, is_chunk: bool = False) -> str:
-        """构建分类提示词(优化版)- 使用数字索引映射,避免模型输出复杂代码"""
-
-        # 获取二级分类信息
-        second_code = ""
-        second_name = section.section_name
-        first_code = ""
-        first_name = ""
-
-        if section.category_standards:
-            first_code = section.category_standards[0].first_code
-            first_name = section.category_standards[0].first_name
-            second_code = section.category_standards[0].second_code
-
-        # 构建三级分类标准描述(使用数字索引,模型只需输出索引号)
-        standards_desc = []
-        for i, std in enumerate(section.category_standards, 1):
-            # 完整显示 third_focus,这是最重要的分类依据!
-            focus_content = std.third_focus if std.third_focus else "(无具体关注要点)"
-            standards_desc.append(
-                f"{i}. {std.third_name}\n"
-                f"   【识别要点】{focus_content}"
-            )
-
-        # 添加非标准项作为兜底分类(索引0)
-        standards_desc.insert(0, "0. 非标准项\n   【识别要点】仅当内容完全不符合以上任何分类时使用,如页眉页脚、纯表格分隔线、无关的广告语等")
-
-        standards_text = '\n\n'.join(standards_desc) if standards_desc else "无具体标准,请根据内容自行判断"
-
-        # 构建索引映射表(用于后处理转换)
-        index_mapping_lines = []
-        index_mapping_lines.append("0 -> 非标准项 (no_standard)")
-        for i, std in enumerate(section.category_standards, 1):
-            index_mapping_lines.append(f"{i} -> {std.third_name} ({std.third_code})")
-        index_mapping_text = "\n".join(index_mapping_lines)
-
-        # 计算内容长度和分段提示
-        content_length = len(section.numbered_content)
-        max_content_length = 12000  # 增加内容长度限制
-        content_to_use = section.numbered_content[:max_content_length]
-        is_truncated = len(section.numbered_content) > max_content_length
-
-        if is_chunk and section.line_number_map:
-            chunk_hint = (
-                f"\n【注意】这是文档的一个分块(行号 {section.line_number_map[0]}~{section.line_number_map[-1]}),"
-                f"请对有实质内容的行进行分类,空行和纯符号行无需单独输出。\n"
-            )
-        elif is_chunk:
-            chunk_hint = "\n【注意】这是文档的一个分块,请对有实质内容的行进行分类。\n"
-        else:
-            chunk_hint = ""
-        truncation_hint = f"\n【提示】内容较长已截断,当前显示前{max_content_length}字符,请对显示的内容进行完整分类。\n" if is_truncated else ""
-
-        return f"""你是一个专业的施工方案文档分析专家。请根据给定的三级分类标准,识别文档内容中属于各个三级分类的部分。{chunk_hint}{truncation_hint}
-
-## 当前文档位置
-- 一级分类: {first_name} ({first_code})
-- 二级分类: {second_name} ({second_code})
-
-## 三级分类标准(共{len(section.category_standards)}个,必须在此范围内分类)
-
-{standards_text}
-
----
-
-## 文档内容(每行以<行号>开头,共{len(section.lines)}行)
-```
-{content_to_use}
-```
-
----
-
-## 分类任务指南
-
-### 核心原则(按优先级排序)
-1. **优先匹配标准分类**:首先判断内容是否符合上述任何一个三级分类标准
-2. **关键词匹配**:内容中出现与分类名称相关的关键词时,应归类到该分类
-3. **语义相关**:即使没有精确关键词,只要语义相关,也应归类
-4. **非标准项谨慎使用**:只有当内容完全不符合任何标准分类时,才使用"非标准项"
-
-### 分类示例
-- 看到"验收内容"、"验收标准"、"验收程序"等内容 → 归类到对应的三级分类
-- 看到"检验方法"、"检查内容"等 → 可能属于"检查要求"或"验收内容"
-- 看到"材料"、"钢筋"、"混凝土"等 → 关注上下文判断所属三级分类
-
-### 行号处理规则
-- **必须合并连续行**:连续多行属于同一分类时,合并为一个条目(start_line为起始,end_line为结束)
-- **禁止逐行输出**:不要为每一行单独创建条目
-- **允许重复分类**:同一行内容可以同时属于多个三级分类
-
-### 多主体句拆分规则(重要)
-- 当一行内容同时提及多个不同主体或类别时,**必须为每个主体单独输出一条分类条目,行号相同**
-- 示例:`"3、有关勘察、设计和监测单位项目技术负责人"` 同时涉及设计单位和监测单位,应输出:
-  - `{{"third_category_code": "DesignUnitXxx", "start_line": N, "end_line": N}}`
-  - `{{"third_category_code": "MonitoringUnitXxx", "start_line": N, "end_line": N}}`
-- 示例:`"总承包单位和分包单位技术负责人"` 同时涉及施工单位,应归入施工单位对应分类
-- 凡是"A、B和C单位"句式,需逐一判断每个主体能否对应某个三级分类
-
-### 自查清单
-- [ ] 是否优先使用了标准分类而非"非标准项"?
-- [ ] 连续相同分类的行是否已合并?
-- [ ] 分类名称是否与标准列表完全一致?
-- [ ] 包含多个主体的行是否已拆分为多条输出?
-
-## 索引映射表(用于后处理转换,你只需输出索引号)
-{index_mapping_text}
-
-## 输出格式(严格JSON,不要任何其他文字)
-```{{
-    "classified_contents_list": [
-        {{
-            "category_index": 数字索引号,
-            "start_line": 起始行号,
-            "end_line": 结束行号
-        }}
-    ]
-}}
-```
-
-## 强制约束
-1. **category_index 必须是数字**(0, 1, 2, 3...),对应上述索引映射表
-2. 0 表示非标准项,1-{len(section.category_standards)} 对应各个三级分类
-3. **禁止输出文本名称或代码**,只输出数字索引
-4. 行号范围: {section.line_number_map[0] if section.line_number_map else 1} - {section.line_number_map[-1] if section.line_number_map else len(section.lines)}
-5. 只输出JSON,禁止任何解释文字"""
-
-    async def _call_api(self, prompt: str) -> str:
-        """调用API(带指数退避重试)"""
-        system_prompt = """你是专业的施工方案文档分析专家。你的任务是:
-1. 仔细阅读文档内容,理解每行的语义
-2. 将内容归类到给定的三级分类标准中
-3. 【重要】优先使用标准分类,只有完全不符合时才使用索引0(非标准项)
-4. 【重要】连续相同分类的多行必须合并为一个条目
-5. 【重要】当一行同时提及多个主体或类别(如"勘察、设计和监测单位"),必须为每个主体单独输出一条条目,行号相同
-6. 【重要】输出格式:category_index必须是纯数字(0,1,2...),禁止输出文本名称或代码
-7. 必须在给定的三级分类标准范围内分类,禁止创造新的分类
-8. 只输出JSON格式结果,不要任何解释文字"""
-
-        kwargs = {
-            "model": self.model,
-            "messages": [
-                {"role": "system", "content": system_prompt},
-                {"role": "user", "content": prompt}
-            ],
-            "temperature": 0.1,  # 降低温度提高分类准确性
-            "max_tokens": 8000   # 增加输出空间
-        }
-
-        # qwen3.5 系列模型默认开启思考模式,需要显式关闭
-        # qwen3 系列模型不需要 enable_thinking 参数
-        if "qwen3.5" in self.model:
-            kwargs["extra_body"] = {"enable_thinking": False}
-
-        # 指数退避重试
-        max_retries = 5
-        base_delay = 2  # 基础延迟2秒
-
-        for attempt in range(max_retries):
-            try:
-                response = await self.client.chat.completions.create(**kwargs)
-                return response.choices[0].message.content or ""
-            except Exception as e:
-                error_str = str(e)
-                # 检查是否是429限流错误
-                if "429" in error_str or "rate limit" in error_str.lower():
-                    if attempt < max_retries - 1:
-                        # 指数退避: 2^attempt * (1 + random)
-                        delay = base_delay * (2 ** attempt) + (hash(prompt) % 1000) / 1000
-                        logger.warning(f"API限流(429),等待 {delay:.1f}s 后重试 ({attempt + 1}/{max_retries})...")
-                        await asyncio.sleep(delay)
-                        continue
-                # 其他错误或重试次数用完,抛出异常
-                raise
-
-        return ""
-
-    def _parse_response(self, response: str, section: SectionContent) -> tuple:
-        """解析响应(增强版,处理各种JSON格式问题)
-
-        返回: (contents, parse_success)
-        - contents: 分类结果列表
-        - parse_success: True表示JSON解析成功(包括空结果),False表示解析失败
-        """
-        if not response or not response.strip():
-            return [], False  # 空响应视为解析失败
-
-        response = response.strip()
-
-        # 尝试多种方式提取JSON
-        json_str = None
-
-        # 方法1: 从代码块中提取
-        code_block_match = re.search(r'```(?:json)?\s*([\s\S]*?)```', response)
-        if code_block_match:
-            json_str = code_block_match.group(1).strip()
-
-        # 方法2: 优先查找JSON数组(模型经常直接输出数组格式)
-        if not json_str:
-            # 使用非贪婪匹配找到第一个完整的数组
-            array_match = re.search(r'\[[\s\S]*?\]', response)
-            if array_match:
-                potential_array = array_match.group(0)
-                # 验证是否是有效的JSON数组
-                try:
-                    parsed = json.loads(potential_array)
-                    if isinstance(parsed, list):
-                        json_str = potential_array
-                except:
-                    pass
-
-        # 方法3: 查找JSON对象
-        if not json_str:
-            json_match = re.search(r'\{[\s\S]*\}', response)
-            if json_match:
-                json_str = json_match.group(0)
-
-        if not json_str:
-            return [], False  # 未找到JSON结构,解析失败
-
-        # 处理模型直接输出数组的情况(包装成对象格式)
-        if json_str.strip().startswith('['):
-            try:
-                # 验证是有效的JSON数组
-                array_data = json.loads(json_str)
-                if isinstance(array_data, list):
-                    # 包装成期望的格式
-                    json_str = json.dumps({"classified_contents": array_data})
-            except:
-                pass  # 不是有效数组,继续后续处理
-
-        # 先尝试直接解析,如果成功则不需要修复
-        try:
-            json.loads(json_str)
-            # JSON 有效,直接使用
-        except json.JSONDecodeError:
-            # JSON 无效,尝试修复
-            json_str = self._fix_json(json_str)
-
-        try:
-            data = json.loads(json_str)
-            # 处理数组格式
-            if isinstance(data, list):
-                data = {"classified_contents": data}
-            contents = []
-            # 支持两种键名: classified_contents 或 classified_contents_list
-            items = data.get("classified_contents", []) or data.get("classified_contents_list", [])
-
-            # 构建索引映射表:索引 -> (third_name, third_code)
-            index_mapping = {0: ("非标准项", "no_standard")}
-            if section.category_standards:
-                for i, std in enumerate(section.category_standards, 1):
-                    index_mapping[i] = (std.third_name, std.third_code)
-
-            for item in items:
-                start_line = item.get("start_line", 0)
-                end_line = item.get("end_line", 0)
-
-                # 优先使用 category_index 进行映射
-                category_index = item.get("category_index")
-                if category_index is not None:
-                    # 通过索引映射获取标准名称和代码
-                    idx = int(category_index) if isinstance(category_index, (int, float, str)) else 0
-                    category_name, category_code = index_mapping.get(idx, ("非标准项", "no_standard"))
-                else:
-                    # 兼容旧格式:直接读取 third_category_code 和 third_category_name
-                    category_code = item.get("third_category_code", "")
-                    category_name = item.get("third_category_name", "")
-
-                    # 清理分类名称格式:移除末尾的代码部分
-                    if category_name and " (" in category_name and category_name.endswith(")"):
-                        category_name = re.sub(r'\s*\([^)]+\)\s*$', '', category_name).strip()
-
-                    # 验证分类代码是否在有效列表中
-                    valid_codes = set(v[1] for v in index_mapping.values())
-                    if category_code not in valid_codes:
-                        logger.warning(f"发现非标准分类 '{category_name}' ({category_code}),强制归为非标准项")
-                        category_code = "no_standard"
-                        category_name = "非标准项"
-
-                # 根据行号从section中提取原文
-                content = self._extract_content_by_line_numbers(section, start_line, end_line)
-                contents.append(ClassifiedContent(
-                    third_category_name=category_name,
-                    third_category_code=category_code,
-                    start_line=start_line,
-                    end_line=end_line,
-                    content=content
-                ))
-            # 聚合同一分类下相邻的内容
-            contents = self._merge_classified_contents(contents, section)
-            return contents, True  # 解析成功(可能为空结果)
-        except Exception as e:
-            # 尝试更激进的修复
-            try:
-                fixed = self._aggressive_json_fix(json_str)
-                data = json.loads(fixed)
-                # 处理数组格式
-                if isinstance(data, list):
-                    data = {"classified_contents": data}
-                contents = []
-                # 支持两种键名: classified_contents 或 classified_contents_list
-                items = data.get("classified_contents", []) or data.get("classified_contents_list", [])
-
-                # 构建索引映射表:索引 -> (third_name, third_code)
-                index_mapping = {0: ("非标准项", "no_standard")}
-                if section.category_standards:
-                    for i, std in enumerate(section.category_standards, 1):
-                        index_mapping[i] = (std.third_name, std.third_code)
-
-                for item in items:
-                    start_line = item.get("start_line", 0)
-                    end_line = item.get("end_line", 0)
-
-                    # 优先使用 category_index 进行映射
-                    category_index = item.get("category_index")
-                    if category_index is not None:
-                        idx = int(category_index) if isinstance(category_index, (int, float, str)) else 0
-                        category_name, category_code = index_mapping.get(idx, ("非标准项", "no_standard"))
-                    else:
-                        # 兼容旧格式
-                        category_code = item.get("third_category_code", "")
-                        category_name = item.get("third_category_name", "")
-                        valid_codes = set(v[1] for v in index_mapping.values())
-                        if category_code not in valid_codes:
-                            logger.warning(f"发现非标准分类 '{category_name}' ({category_code}),强制归为非标准项")
-                            category_code = "no_standard"
-                            category_name = "非标准项"
-
-                    # 根据行号从section中提取原文
-                    content = self._extract_content_by_line_numbers(section, start_line, end_line)
-                    contents.append(ClassifiedContent(
-                        third_category_name=category_name,
-                        third_category_code=category_code,
-                        start_line=start_line,
-                        end_line=end_line,
-                        content=content
-                    ))
-                # 聚合同一分类下相邻的内容
-                contents = self._merge_classified_contents(contents, section)
-                return contents, True  # 解析成功(可能为空结果)
-            except Exception as e2:
-                logger.error(f"解析JSON失败: {e}, 二次修复也失败: {e2}")
-                logger.debug(f"原始响应前500字符: {response[:500]}...")
-                logger.debug(f"提取的JSON前300字符: {json_str[:300]}...")
-                return [], False  # 解析失败
-
-    def _merge_classified_contents(self, contents: List[ClassifiedContent], section: SectionContent) -> List[ClassifiedContent]:
-        """将同一分类下的内容按区间合并(只有连续或重叠的区间才合并)"""
-        if not contents:
-            return contents
-
-        # 按分类代码分组
-        groups: Dict[str, List[ClassifiedContent]] = {}
-        for content in contents:
-            key = content.third_category_code
-            if key not in groups:
-                groups[key] = []
-            groups[key].append(content)
-
-        merged_contents = []
-
-        for category_code, group_contents in groups.items():
-            # 按起始行号排序
-            group_contents.sort(key=lambda x: x.start_line)
-
-            # 合并连续或重叠的区间
-            merged_ranges = []
-            for content in group_contents:
-                if not merged_ranges:
-                    # 第一个区间
-                    merged_ranges.append({
-                        'start': content.start_line,
-                        'end': content.end_line
-                    })
-                else:
-                    last_range = merged_ranges[-1]
-                    # 检查是否连续或重叠(允许3行的间隔也算连续)
-                    if content.start_line <= last_range['end'] + 3:
-                        # 扩展当前区间
-                        last_range['end'] = max(last_range['end'], content.end_line)
-                    else:
-                        # 不连续,新建区间
-                        merged_ranges.append({
-                            'start': content.start_line,
-                            'end': content.end_line
-                        })
-
-            # 为每个合并后的区间创建条目
-            for range_info in merged_ranges:
-                merged_content = self._extract_content_by_line_numbers(
-                    section, range_info['start'], range_info['end']
-                )
-                merged_contents.append(ClassifiedContent(
-                    third_category_name=group_contents[0].third_category_name,
-                    third_category_code=category_code,
-                    start_line=range_info['start'],
-                    end_line=range_info['end'],
-                    content=merged_content
-                ))
-
-        # 按起始行号排序最终结果
-        merged_contents.sort(key=lambda x: x.start_line)
-        return merged_contents
-
-    def _extract_content_by_line_numbers(self, section: SectionContent, start_line: int, end_line: int) -> str:
-        """根据全局行号从section中提取原文内容"""
-        if not section.line_number_map:
-            # 如果没有行号映射,使用相对索引
-            start_idx = max(0, start_line - 1)
-            end_idx = min(len(section.lines), end_line)
-            return '\n'.join(section.lines[start_idx:end_idx])
-
-        # 找到全局行号对应的索引
-        start_idx = -1
-        end_idx = -1
-
-        for idx, global_line_num in enumerate(section.line_number_map):
-            if global_line_num == start_line:
-                start_idx = idx
-            if global_line_num == end_line:
-                end_idx = idx
-                break
-
-        # 如果没找到精确匹配,使用近似值
-        if start_idx == -1:
-            for idx, global_line_num in enumerate(section.line_number_map):
-                if global_line_num >= start_line:
-                    start_idx = idx
-                    break
-        if end_idx == -1:
-            for idx in range(len(section.line_number_map) - 1, -1, -1):
-                if section.line_number_map[idx] <= end_line:
-                    end_idx = idx
-                    break
-
-        if start_idx == -1:
-            start_idx = 0
-        if end_idx == -1:
-            end_idx = len(section.lines) - 1
-
-        # 确保索引有效
-        start_idx = max(0, min(start_idx, len(section.lines) - 1))
-        end_idx = max(0, min(end_idx, len(section.lines) - 1))
-
-        if start_idx > end_idx:
-            start_idx, end_idx = end_idx, start_idx
-
-        # 添加行号标记返回
-        lines_with_numbers = []
-        for i in range(start_idx, end_idx + 1):
-            global_line = section.line_number_map[i] if i < len(section.line_number_map) else (i + 1)
-            lines_with_numbers.append(f"<{global_line}> {section.lines[i]}")
-
-        return '\n'.join(lines_with_numbers)
-
-    async def _call_supplement_verification(
-        self,
-        section: SectionContent,
-        std: CategoryStandard,
-        hit_lines: List[int],
-        matched_kws: List[str],
-        is_table: bool = False
-    ) -> bool:
-        """针对单个候选遗漏分类发起补充验证LLM调用,返回是否存在。"""
-        start = min(hit_lines)
-        end = max(hit_lines)
-        chunk_text = self._extract_content_by_line_numbers(section, start, end)
-
-        if is_table:
-            trigger = "该内容块包含表格,表格中多列信息混排,以下分类在主分类阶段未被识别,需确认是否存在于表格中"
-        else:
-            trigger = f"以下关键字在文档中被检测到:{'、'.join(matched_kws)}(出现于第 {hit_lines} 行)"
-
-        prompt = f"""你是一个施工方案内容分类专家。
-
-【组织层级说明】
-本项目的组织层级如下,判断时请严格区分:
-- 四川路桥(总公司)= 四川公路桥梁建设集团有限公司,文件通常以"四川公路桥梁"开头或含"SCQJ"
-- 路桥集团(子公司)= 四川路桥集团有限公司,文件中出现"四川路桥集团"即属于路桥集团(子公司),而非总公司
-- 桥梁公司(子公司)= 四川路桥桥梁公司,文件中出现"四川路桥桥梁公司"或"桥梁公司"即属于桥梁公司(子公司)
-
-【待审查内容】(第 {start}~{end} 行)
-{chunk_text}
-
-【待确认的分类】
-分类名称:{std.third_name}
-识别说明:{std.third_focus}
-
-【触发原因】
-{trigger}
-
-【问题】
-上述文档内容中,是否包含"{std.third_name}"相关的实质内容?
-
-请仅回答"存在"或"不存在":"""
-
-        try:
-            kwargs = {
-                "model": self.model,
-                "messages": [
-                    {"role": "system", "content": '你是施工方案内容审查专家,请根据提供的内容作出判断,只回答"存在"或"不存在",不要任何其他文字。'},
-                    {"role": "user", "content": prompt}
-                ],
-                "temperature": 0.0,
-                "max_tokens": 10
-            }
-            if "qwen3.5" in self.model:
-                kwargs["extra_body"] = {"enable_thinking": False}
-            response = await self.client.chat.completions.create(**kwargs)
-            resp = response.choices[0].message.content or ""
-            if "不存在" in resp:
-                return False
-            if "存在" in resp:
-                return True
-            # 格式异常,保守返回 True
-            logger.warning(f"supplement_verify 格式异常: {resp[:50]}")
-            return True
-        except Exception as e:
-            logger.warning(f"supplement_verify 调用失败: {e}")
-            return True
-
-    async def _detect_and_supplement(
-        self,
-        section: SectionContent,
-        llm_results: List[ClassifiedContent]
-    ) -> List[ClassifiedContent]:
-        """扫描整个 section,补充 LLM 遗漏的三级分类。
-
-        扫描范围:当前二级分类下的所有行(不跨二级分类,由 section.category_standards 保证)。
-        触发条件:该二级分类下某个三级标准未出现在 LLM 结果中。
-        注意:同一行内容可同时属于多个三级分类,不限制"已覆盖行"。
-        """
-        if not section.category_standards or not section.lines:
-            return []
-
-        # 已命中的有效分类(排除 no_standard)
-        found_codes = {c.third_category_code for c in llm_results if c.third_category_code != 'no_standard'}
-
-        # 判断整个 section 是否含表格特征
-        full_text = ' '.join(section.lines)
-        is_table = (
-            any(kw in full_text for kw in ['序号', '作业活动', '风险源', '防范措施'])
-            or full_text.count('|') > 5
-        )
-
-        supplemented = []
-        for std in section.category_standards:
-            if std.third_code in found_codes or not std.keywords:
-                continue
-
-            keywords = [k.strip() for k in std.keywords.split(';') if k.strip()]
-
-            if is_table:
-                # 表格路径:整个 section 行范围提交 LLM 验证
-                if not section.line_number_map:
-                    continue
-                hit_lines = [section.line_number_map[0], section.line_number_map[-1]]
-                confirmed = await self._call_supplement_verification(section, std, hit_lines, [], is_table=True)
-            else:
-                # 普通路径:扫描整个 section 所有行的关键字
-                hit_lines, matched_kws = [], []
-                for i, line_text in enumerate(section.lines):
-                    line_num = section.line_number_map[i] if section.line_number_map else (i + 1)
-                    for kw in keywords:
-                        if kw in line_text and line_num not in hit_lines:
-                            hit_lines.append(line_num)
-                            if kw not in matched_kws:
-                                matched_kws.append(kw)
-                if not hit_lines:
-                    continue
-                confirmed = await self._call_supplement_verification(section, std, hit_lines, matched_kws)
-
-            if confirmed:
-                start, end = min(hit_lines), max(hit_lines)
-                content = self._extract_content_by_line_numbers(section, start, end)
-                supplemented.append(ClassifiedContent(
-                    third_category_name=std.third_name,
-                    third_category_code=std.third_code,
-                    start_line=start,
-                    end_line=end,
-                    content=content
-                ))
-
-        return supplemented
-
-    def _fix_json(self, json_str: str) -> str:
-        """修复常见的JSON格式问题"""
-        # 去除尾部多余的逗号
-        json_str = re.sub(r',(\s*[}\]])', r'\1', json_str)
-
-        # 确保 JSON 结构闭合
-        json_str = self._ensure_json_closed(json_str)
-
-        # 替换单引号为双引号(但要小心内容中的单引号)
-        # 使用更精确的方法:先尝试解析,失败再替换
-        try:
-            json.loads(json_str)
-            return json_str
-        except:
-            # 尝试替换单引号
-            json_str = json_str.replace("'", '"')
-
-        return json_str
-
-    def _truncate_to_valid_json(self, json_str: str) -> str:
-        """将截断的JSON截断到最后一个完整对象的位置,并保留数组结构"""
-        # 找到 "classified_contents" 数组的开始
-        array_start = json_str.find('"classified_contents"')
-        if array_start == -1:
-            return json_str
-
-        # 找到数组的 '['
-        bracket_start = json_str.find('[', array_start)
-        if bracket_start == -1:
-            return json_str
-
-        # 遍历数组,找到最后一个完整的对象
-        brace_count = 0
-        bracket_count = 1  # 已经进入数组,所以是1
-        in_string = False
-        escape_next = False
-        last_valid_obj_end = 0
-        i = bracket_start + 1
-
-        while i < len(json_str):
-            char = json_str[i]
-
-            if escape_next:
-                escape_next = False
-                i += 1
-                continue
-
-            if char == '\\':
-                escape_next = True
-                i += 1
-                continue
-
-            if char == '"' and not escape_next:
-                in_string = not in_string
-                i += 1
-                continue
-
-            if not in_string:
-                if char == '{':
-                    brace_count += 1
-                elif char == '}':
-                    brace_count -= 1
-                    if brace_count == 0:
-                        # 找到一个完整的对象
-                        last_valid_obj_end = i
-                elif char == '[':
-                    bracket_count += 1
-                elif char == ']':
-                    bracket_count -= 1
-                    if bracket_count == 0:
-                        # 数组正常闭合,不需要截断
-                        return json_str
-
-            i += 1
-
-        if last_valid_obj_end > 0:
-            # 截断到最后一个完整对象的位置,并关闭数组
-            return json_str[:last_valid_obj_end + 1] + ']'
-
-        return json_str
-
-    def _ensure_json_closed(self, json_str: str) -> str:
-        """确保JSON结构闭合"""
-        # 计算未闭合的括号
-        brace_count = 0
-        bracket_count = 0
-        in_string = False
-        escape_next = False
-
-        for char in json_str:
-            if escape_next:
-                escape_next = False
-                continue
-            if char == '\\':
-                escape_next = True
-                continue
-            if char == '"' and not escape_next:
-                in_string = not in_string
-                continue
-            if not in_string:
-                if char == '{':
-                    brace_count += 1
-                elif char == '}':
-                    brace_count -= 1
-                elif char == '[':
-                    bracket_count += 1
-                elif char == ']':
-                    bracket_count -= 1
-
-        # 添加闭合括号
-        result = json_str
-        # 先去掉尾部可能的逗号
-        result = result.rstrip().rstrip(',').rstrip()
-
-        # 关闭对象
-        while brace_count > 0:
-            result += '}'
-            brace_count -= 1
-
-        # 关闭数组
-        while bracket_count > 0:
-            result += ']'
-            bracket_count -= 1
-
-        return result
-
-    def _aggressive_json_fix(self, json_str: str) -> str:
-        """激进的JSON修复,用于处理复杂情况"""
-        # 首先尝试截断到最后一个完整对象
-        json_str = self._truncate_to_valid_json(json_str)
-        # 然后确保结构闭合
-        json_str = self._ensure_json_closed(json_str)
-        return json_str
-
-
-# ==================== Chunks 转换器(用于集成) ====================
-
-class ChunksConverter:
-    """chunks 格式与 SectionContent 格式的转换器"""
-
-    def __init__(self, category_loader: 'CategoryStandardLoader'):
-        self.category_loader = category_loader
-
-    def chunks_to_sections(self, chunks: List[Dict[str, Any]]) -> List[SectionContent]:
-        """
-        将 chunks 列表转换为 SectionContent 列表
-
-        分组策略:
-        1. 优先按 section_label 分组(更精确的文档结构)
-        2. 如果 section_label 相同,再按一级分类分组
-        3. 从 section_label 提取二级分类名称用于匹配三级标准
-
-        Args:
-            chunks: 文档分块列表,每个 chunk 需包含:
-                - chapter_classification: 一级分类代码
-                - secondary_category_code: 二级分类代码(可能为 none)
-                - secondary_category_cn: 二级分类中文名
-                - review_chunk_content 或 content: 内容文本
-                - section_label: 章节标签(如 "第一章编制依据->一、法律法规")
-
-        Returns:
-            List[SectionContent]: 二级标题段落列表
-        """
-        # 按 section_label 分组(更精确)
-        # section_label 格式: "第一章编制依据->一、法律法规"
-        section_groups: Dict[str, List[Dict]] = {}
-
-        for chunk in chunks:
-            # 获取分类信息
-            section_label = chunk.get("section_label", "") or chunk.get("chapter", "")
-            first_code = chunk.get("chapter_classification", "") or chunk.get("first_code", "")
-            second_code = chunk.get("secondary_category_code", "") or chunk.get("second_code", "")
-            second_cn = chunk.get("secondary_category_cn", "") or chunk.get("second_name", "")
-
-            # 分组策略:每个二级分类独立分组,禁止合并不同二级分类
-            # 优先使用 section_label,其次使用 secondary_category_code
-            if section_label and "->" in section_label:
-                # 有明确的章节标签,使用它作为分组键
-                group_key = section_label
-            elif second_code and second_code not in ("none", "None", ""):
-                # 有二级分类代码,按二级分类独立分组(关键:不再合并到一级分类下)
-                group_key = f"{first_code}->{second_code}"
-            elif section_label:
-                group_key = section_label
-            else:
-                # 完全没有分类信息,使用唯一键避免合并
-                group_key = f"unknown_{first_code}_{id(chunk)}"
-
-            if group_key not in section_groups:
-                section_groups[group_key] = []
-            section_groups[group_key].append(chunk)
-
-        # 为每个分组创建 SectionContent
-        section_contents = []
-        all_lines = []  # 全局行号追踪
-
-        for group_key, group_chunks in section_groups.items():
-            if not group_chunks:
-                continue
-
-            # 合并该分组的所有内容,同时记录每个原始 chunk 的行范围
-            section_lines = []
-            chunk_line_counts: List[Tuple[str, int]] = []  # (chunk_id, line_count)
-            for chunk in group_chunks:
-                content = chunk.get("review_chunk_content", "") or chunk.get("content", "") or chunk.get("original_content", "")
-                if content:
-                    lines = content.split('\n')
-                    n = len(lines)
-                    chunk_id = chunk.get("chunk_id") or chunk.get("id") or str(id(chunk))
-                    chunk_line_counts.append((chunk_id, n))
-                    section_lines.extend(lines)
-                    all_lines.extend(lines)
-                else:
-                    chunk_id = chunk.get("chunk_id") or chunk.get("id") or str(id(chunk))
-                    chunk_line_counts.append((chunk_id, 0))
-
-            if not section_lines:
-                continue
-
-            # 获取一级分类代码
-            first_code = group_chunks[0].get("chapter_classification", "") or group_chunks[0].get("first_code", "")
-
-            # 获取二级分类名称和代码
-            second_code = group_chunks[0].get("secondary_category_code", "") or group_chunks[0].get("second_code", "")
-            second_cn = group_chunks[0].get("secondary_category_cn", "") or group_chunks[0].get("second_name", "")
-
-            # 从 section_label 提取二级分类名称(优先)
-            section_label = group_chunks[0].get("section_label", "") or group_chunks[0].get("chapter", "")
-            if "->" in section_label:
-                parts = section_label.split("->")
-                if len(parts) >= 2:
-                    extracted = parts[1].strip()
-                    # 去除序号前缀(如 "一、" "二、")
-                    cleaned = re.sub(r'^[一二三四五六七八九十]+[、)\s]+', '', extracted).strip()
-                    if cleaned:
-                        second_cn = cleaned
-                        # 尝试根据提取的名称匹配二级分类代码
-                        matched_standards = self.category_loader.get_standards_by_second_name(cleaned)
-                        if matched_standards:
-                            second_code = matched_standards[0].second_code
-
-            # 构建带行号的内容
-            start_line = len(all_lines) - len(section_lines) + 1
-            line_number_map = list(range(start_line, len(all_lines) + 1))
-            numbered_lines = []
-            for i, line in enumerate(section_lines):
-                numbered_lines.append(f"<{line_number_map[i]}> {line}")
-            numbered_content = '\n'.join(numbered_lines)
-
-            # 计算每个原始 chunk 在全局行号中的范围
-            chunk_ranges: List[Tuple[str, int, int]] = []
-            current_global = start_line
-            for chunk_id, n_lines in chunk_line_counts:
-                if n_lines > 0:
-                    chunk_ranges.append((chunk_id, current_global, current_global + n_lines - 1))
-                    current_global += n_lines
-
-            # 获取三级分类标准
-            category_standards = self.category_loader.get_standards_by_second_code(second_code)
-            if not category_standards:
-                category_standards = self.category_loader.get_standards_by_second_name(second_cn)
-
-            # 构建 section_key(使用 group_key 保留 section_label 信息,粒度更细)
-            section_key = group_key
-
-            section_contents.append(SectionContent(
-                section_key=section_key,
-                section_name=second_cn or second_code,
-                lines=section_lines,
-                numbered_content=numbered_content,
-                category_standards=category_standards,
-                line_number_map=line_number_map,
-                chunk_ranges=chunk_ranges
-            ))
-
-        return section_contents
-
-    def classification_result_to_chunks(
-        self,
-        result: ClassificationResult,
-        original_chunks: List[Dict[str, Any]],
-        first_code: str,
-        second_code: str
-    ) -> List[Dict[str, Any]]:
-        """
-        将 ClassificationResult 转换回 chunks 格式
-
-        将行级分类结果展开,为每个三级分类创建对应的 chunk 条目
-
-        Args:
-            result: 分类结果
-            original_chunks: 原始 chunks(用于保留其他字段)
-            first_code: 一级分类代码
-            second_code: 二级分类代码
-
-        Returns:
-            List[Dict]: 更新后的 chunks 列表
-        """
-        updated_chunks = []
-
-        # 收集所有三级分类信息,过滤掉非标准项(no_standard)
-        tertiary_classifications = []
-        for content in result.classified_contents:
-            # 跳过非标准项,不纳入三级分类统计
-            if content.third_category_code == "no_standard":
-                continue
-            tertiary_classifications.append({
-                "third_category_name": content.third_category_name,
-                "third_category_code": content.third_category_code,
-                "start_line": content.start_line,
-                "end_line": content.end_line,
-                "content": content.content
-            })
-
-        # 更新原始 chunks
-        for chunk in original_chunks:
-            updated_chunk = dict(chunk)
-            updated_chunk["first_code"] = first_code
-            updated_chunk["second_code"] = second_code
-
-            # 添加三级分类详情列表
-            updated_chunk["tertiary_classification_details"] = tertiary_classifications
-
-            # 如果有三级分类结果,设置第一个作为主要分类(向后兼容)
-            if tertiary_classifications:
-                updated_chunk["tertiary_category_code"] = tertiary_classifications[0]["third_category_code"]
-                updated_chunk["tertiary_category_cn"] = tertiary_classifications[0]["third_category_name"]
-
-            updated_chunks.append(updated_chunk)
-
-        return updated_chunks
-
-
-# ==================== 主入口类 ====================
-
-class LLMContentClassifier:
-    """
-    LLM 内容三级分类器(主入口类)
-
-    封装完整的分类流程,提供简洁的接口供外部调用
-    """
-
-    def __init__(self, config: Optional[ClassifierConfig] = None):
-        """
-        初始化分类器
-
-        Args:
-            config: 配置对象,如果为 None 则使用默认配置
-        """
-        self.config = config or ClassifierConfig()
-
-        # 加载标准分类
-        self.category_loader = CategoryStandardLoader(Path(self.config.category_table_path))
-
-        # 加载二级分类标准(如果存在)
-        self.second_category_loader = None
-        if Path(self.config.second_category_path).exists():
-            self.second_category_loader = SecondCategoryStandardLoader(Path(self.config.second_category_path))
-
-        # 创建转换器
-        self.converter = ChunksConverter(self.category_loader)
-
-        # 并发控制信号量
-        self.semaphore = asyncio.Semaphore(self.config.max_concurrent_requests)
-
-        # Embedding 客户端(可选)
-        self.embedding_client = None
-        if self.config.embedding_base_url:
-            self.embedding_client = self._create_embedding_client()
-
-    def _create_embedding_client(self) -> 'EmbeddingClient':
-        """创建 Embedding 客户端"""
-        client = EmbeddingClient()
-        # 使用配置覆盖默认值
-        client.client = AsyncOpenAI(
-            api_key=self.config.embedding_api_key,
-            base_url=self.config.embedding_base_url
-        )
-        client.model = self.config.embedding_model
-        return client
-
-    async def classify_chunks(
-        self,
-        chunks: List[Dict[str, Any]],
-        progress_callback: Optional[callable] = None
-    ) -> List[Dict[str, Any]]:
-        """
-        对 chunks 进行三级分类
-
-        Args:
-            chunks: 文档分块列表,每个 chunk 需包含:
-                - chapter_classification: 一级分类代码
-                - secondary_category_code: 二级分类代码
-                - secondary_category_cn: 二级分类中文名
-                - review_chunk_content 或 content: 内容文本
-            progress_callback: 进度回调函数 (completed, total, section_name, success) -> None,支持 async
-
-        Returns:
-            List[Dict]: 更新后的 chunks 列表,每个 chunk 新增字段:
-                - tertiary_category_code: 三级分类代码
-                - tertiary_category_cn: 三级分类名称
-                - tertiary_classification_details: 行级分类详情列表
-        """
-        logger.info(f"正在对 {len(chunks)} 个内容块进行三级分类...")
-
-        # 步骤1: 将 chunks 转换为 SectionContent 列表
-        sections = self.converter.chunks_to_sections(chunks)
-        logger.info(f"按二级标题分组后得到 {len(sections)} 个段落")
-
-        if not sections:
-            logger.info("没有有效的段落需要分类")
-            return chunks
-
-        # 步骤2: 创建分类客户端
-        classifier = ContentClassifierClient(
-            model=self.config.model,
-            semaphore=self.semaphore,
-            embedding_client=self.embedding_client,
-            second_category_loader=self.second_category_loader
-        )
-
-        # 步骤3: 并发分类所有段落
-        results_map: Dict[str, ClassificationResult] = {}
-
-        async def classify_with_progress(section: SectionContent, idx: int, total: int):
-            result = await classifier.classify_content(section)
-            results_map[section.section_key] = result
-
-            if progress_callback:
-                ret = progress_callback(idx + 1, total, section.section_name, not result.error)
-                if asyncio.iscoroutine(ret):
-                    await ret
-            else:
-                status = "成功" if not result.error else f"失败: {result.error[:30]}"
-                logger.debug(f"[{idx + 1}/{total}] {section.section_name}: {status}")
-
-            return result
-
-        tasks = [
-            classify_with_progress(section, idx, len(sections))
-            for idx, section in enumerate(sections)
-        ]
-        await asyncio.gather(*tasks)
-
-        # 步骤4: 将分类结果转换回 chunks 格式,按 chunk_ranges 过滤确保每个 chunk 只拿自己行范围内的详情
-        updated_chunks = []
-
-        # 建立 chunk_id -> (section_key, g_start, g_end) 映射,来自 sections 的 chunk_ranges
-        chunk_range_map: Dict[str, Tuple[str, int, int]] = {}
-        for section in sections:
-            for (cid, g_start, g_end) in section.chunk_ranges:
-                chunk_range_map[cid] = (section.section_key, g_start, g_end)
-
-        # 为每个原始 chunk 单独分配其行范围内的分类详情
-        for chunk in chunks:
-            updated_chunk = dict(chunk)
-            first_code = chunk.get("chapter_classification", "") or chunk.get("first_code", "")
-            second_code = chunk.get("secondary_category_code", "") or chunk.get("second_code", "")
-
-            # 从 chunk_range_map 获取该 chunk 的行范围(同时拿到正确的 section_key)
-            chunk_id = chunk.get("chunk_id") or chunk.get("id") or str(id(chunk))
-            range_info = chunk_range_map.get(chunk_id)
-
-            if range_info:
-                # 优先使用 chunk_range_map 中记录的 section_key(经过名称匹配的正确 key)
-                section_key = range_info[0]
-            else:
-                # 降级:从 chunk 字段重建(可能在 second_code="none" 时查不到)
-                section_key = f"{first_code}->{second_code}"
-
-            result = results_map.get(section_key)
-
-            if result:
-                updated_chunk["first_code"] = first_code
-                updated_chunk["second_code"] = second_code
-
-                # 收集全部有效三级分类(非 no_standard)
-                all_tertiary = [
-                    {
-                        "third_category_name": c.third_category_name,
-                        "third_category_code": c.third_category_code,
-                        "start_line": c.start_line,
-                        "end_line": c.end_line,
-                        "content": c.content
-                    }
-                    for c in result.classified_contents
-                    if c.third_category_code != "no_standard"
-                ]
-
-                if range_info:
-                    # 过滤:只保留与该 chunk 行范围有交集的详情
-                    _, g_start, g_end = range_info
-                    filtered = [
-                        t for t in all_tertiary
-                        if t["start_line"] <= g_end and t["end_line"] >= g_start
-                    ]
-                else:
-                    # 无法定位行范围(可能是单 chunk 分组),保留全部
-                    filtered = all_tertiary
-
-                # 去重:按 (third_category_code, start_line, end_line) 三元组去重
-                seen = set()
-                deduped = []
-                for t in filtered:
-                    key = (t["third_category_code"], t["start_line"], t["end_line"])
-                    if key not in seen:
-                        seen.add(key)
-                        deduped.append(t)
-                updated_chunk["tertiary_classification_details"] = deduped
-
-                # 向后兼容:设置第一个三级分类为主分类
-                tertiary_details = updated_chunk["tertiary_classification_details"]
-                if tertiary_details:
-                    updated_chunk["tertiary_category_code"] = tertiary_details[0]["third_category_code"]
-                    updated_chunk["tertiary_category_cn"] = tertiary_details[0]["third_category_name"]
-
-            updated_chunks.append(updated_chunk)
-
-        logger.info(f"三级分类完成!共处理 {len(updated_chunks)} 个 chunks")
-        return updated_chunks
-
-
-# ==================== 便捷函数 ====================
-
-async def classify_chunks(
-    chunks: List[Dict[str, Any]],
-    config: Optional[ClassifierConfig] = None,
-    progress_callback: Optional[callable] = None
-) -> List[Dict[str, Any]]:
-    """
-    对 chunks 进行三级分类的便捷函数
-
-    Args:
-        chunks: 文档分块列表
-        config: 配置对象(可选)
-        progress_callback: 进度回调函数
-
-    Returns:
-        List[Dict]: 更新后的 chunks 列表
-
-    使用示例:
-        from llm_content_classifier_v2 import classify_chunks
-
-        # 使用默认配置
-        updated_chunks = await classify_chunks(chunks)
-
-        # 使用自定义配置
-        config = ClassifierConfig(
-            model="qwen3.5-122b-a10b",
-            embedding_similarity_threshold=0.85
-        )
-        updated_chunks = await classify_chunks(chunks, config=config)
-    """
-    classifier = LLMContentClassifier(config)
-    return await classifier.classify_chunks(chunks, progress_callback)
-
-
-def classify_chunks_sync(
-    chunks: List[Dict[str, Any]],
-    config: Optional[ClassifierConfig] = None
-) -> List[Dict[str, Any]]:
-    """
-    同步版本的分类函数(阻塞调用)
-
-    Args:
-        chunks: 文档分块列表
-        config: 配置对象(可选)
-
-    Returns:
-        List[Dict]: 更新后的 chunks 列表
-    """
-    try:
-        loop = asyncio.get_running_loop()
-    except RuntimeError:
-        # 没有运行中的事件循环
-        return asyncio.run(classify_chunks(chunks, config))
-
-    # 已有事件循环,创建任务
-    import concurrent.futures
-    with concurrent.futures.ThreadPoolExecutor() as executor:
-        future = executor.submit(
-            asyncio.run,
-            classify_chunks(chunks, config)
-        )
-        return future.result()
-
-
-# ==================== 文本切块工具 ====================
-
-def _is_markdown_table_line(line: str) -> bool:
-    """判断一行是否为 Markdown 表格行(以 | 开头且以 | 结尾)"""
-    stripped = line.strip()
-    return stripped.startswith('|') and stripped.endswith('|') and len(stripped) >= 3
-
-
-def _split_text_lines_with_overlap(
-    lines: List[str],
-    max_chars: int,
-    overlap_chars: int
-) -> List[List[str]]:
-    """
-    将文本行列表按字符数切分,相邻 chunk 之间保留重叠。
-
-    - 普通行(<= max_chars):积累到超限时 flush,下一个 chunk 以末尾若干行作重叠头。
-    - 超长行(> max_chars):先 flush 当前积累,再对该行做字符级滑窗切分,
-      每片段 max_chars 字符,步长 max_chars - overlap_chars(即相邻片段重叠 overlap_chars)。
-    """
-    if not lines:
-        return []
-
-    chunks: List[List[str]] = []
-    current_lines: List[str] = []
-    current_chars: int = 0
-
-    def _flush():
-        """保存当前 chunk,并以末尾若干行作为下一个 chunk 的重叠起始。"""
-        nonlocal current_lines, current_chars
-        if not current_lines:
-            return
-        chunks.append(list(current_lines))
-        overlap_lines: List[str] = []
-        overlap_len: int = 0
-        for prev in reversed(current_lines):
-            overlap_lines.insert(0, prev)
-            overlap_len += len(prev)
-            if overlap_len >= overlap_chars:
-                break
-        current_lines = overlap_lines
-        current_chars = overlap_len
-
-    for line in lines:
-        line_chars = len(line)
-
-        if line_chars > max_chars:
-            # 超长行:先 flush,再对该行做字符级滑窗切分
-            _flush()
-            step = max_chars - overlap_chars  # 滑动步长
-            start = 0
-            while start < line_chars:
-                piece = line[start: start + max_chars]
-                chunks.append([piece])
-                start += step
-            # 以最后一片段末尾的 overlap_chars 个字符作重叠起始
-            last_piece = line[max(0, line_chars - overlap_chars):]
-            current_lines = [last_piece]
-            current_chars = len(last_piece)
-        else:
-            # 普通行:加入后超限则先 flush
-            if current_chars + line_chars > max_chars and current_lines:
-                _flush()
-            current_lines.append(line)
-            current_chars += line_chars
-
-    if current_lines:
-        chunks.append(current_lines)
-
-    return chunks
-
-
-def split_section_into_chunks(
-    lines: List[str],
-    max_chars: int = 600,
-    overlap_chars: int = 30
-) -> List[Dict[str, Any]]:
-    """
-    将二级分类下的行列表切分为 chunks。
-
-    规则:
-    - Markdown 表格(以 | 开头且以 | 结尾的连续行)作为独立 chunk,不切断、不与其他内容合并、无重叠。
-    - 普通文本按 max_chars 字符数切分,相邻 chunk 之间有 overlap_chars 字符的重叠。
-    - 单行超过 max_chars 时做字符级滑窗切分,相邻片段之间同样保留 overlap_chars 重叠。
-
-    Args:
-        lines:         行列表(不含行号标记)
-        max_chars:     每个文本 chunk 的最大字符数,默认 600
-        overlap_chars: 相邻文本 chunk 的重叠字符数,默认 30
-
-    Returns:
-        List[Dict]: 每个元素包含:
-            - 'type':  'text' 或 'table'
-            - 'lines': 该 chunk 对应的行列表
-    """
-    if not lines:
-        return []
-
-    # Step 1:将行序列分割为交替的 table_segment / text_segment
-    segments: List[Tuple[str, List[str]]] = []
-    i = 0
-    while i < len(lines):
-        if _is_markdown_table_line(lines[i]):
-            table_lines: List[str] = []
-            while i < len(lines) and _is_markdown_table_line(lines[i]):
-                table_lines.append(lines[i])
-                i += 1
-            segments.append(('table', table_lines))
-        else:
-            text_lines: List[str] = []
-            while i < len(lines) and not _is_markdown_table_line(lines[i]):
-                text_lines.append(lines[i])
-                i += 1
-            segments.append(('text', text_lines))
-
-    # Step 2:表格段整体输出;文本段按字符数切分并加重叠
-    result: List[Dict[str, Any]] = []
-    for seg_type, seg_lines in segments:
-        if seg_type == 'table':
-            result.append({'type': 'table', 'lines': seg_lines})
-        else:
-            for chunk_lines in _split_text_lines_with_overlap(seg_lines, max_chars, overlap_chars):
-                result.append({'type': 'text', 'lines': chunk_lines})
-
-    return result
-
-
-# ==================== 快速测试入口 ====================
-
-if __name__ == "__main__":
-    import io
-    import sys
-    from datetime import datetime
-
-    # 修复 Windows 终端 UTF-8 输出
-    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
-
-    TEST_JSON_PATH = Path(r"temp\construction_review\final_result\4148f6019f89e061b15679666f646893-1773993108.json")
-    OUTPUT_DIR = Path(r"temp\construction_review\llm_content_classifier_v2")
-    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
-
-    def _sep(title: str = "", width: int = 70):
-        print(f"\n{'=' * width}\n  {title}\n{'=' * width}" if title else "─" * width)
-
-    def _load_chunks_from_json(json_path: Path) -> List[Dict[str, Any]]:
-        with open(json_path, encoding="utf-8") as f:
-            data = json.load(f)
-        if "document_result" in data:
-            return data["document_result"]["structured_content"]["chunks"]
-        return data["data"]["document_result"]["structured_content"]["chunks"]
-
-    # ── 加载数据 ──────────────────────────────────────────────
-    _sep("加载测试数据")
-    if not TEST_JSON_PATH.exists():
-        print(f"[ERROR] 文件不存在: {TEST_JSON_PATH}")
-        sys.exit(1)
-
-    raw_chunks = _load_chunks_from_json(TEST_JSON_PATH)
-    print(f"原始 chunks 数: {len(raw_chunks)}")
-
-    # ── 运行完整分类流程 ───────────────────────────────────────
-    _sep("运行三级分类(LLMContentClassifier)")
-    config = ClassifierConfig()
-    print(f"模型: {config.model}")
-    print(f"Embedding 模型: {config.embedding_model}")
-    print(f"相似度阈值: {config.embedding_similarity_threshold}")
-
-    classifier = LLMContentClassifier(config)
-    updated_chunks = asyncio.run(classifier.classify_chunks(raw_chunks))
-
-    # ── 保存结果 ──────────────────────────────────────────────
-    _sep("保存结果")
-    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
-    result_file = OUTPUT_DIR / f"result_{ts}.json"
-    with open(result_file, "w", encoding="utf-8") as f:
-        json.dump(updated_chunks, f, ensure_ascii=False, indent=2)
-    print(f"完整结果已保存: {result_file}")
-
-    # ── 控制台汇总展示 ────────────────────────────────────────
-    _sep("分类结果汇总")
-
-    # 按 section_label 聚合三级分类详情
-    section_map: Dict[str, List[Dict]] = {}
-    for chunk in updated_chunks:
-        label = chunk.get("section_label") or chunk.get("chunk_id", "unknown")
-        details = chunk.get("tertiary_classification_details", [])
-        if label not in section_map:
-            section_map[label] = []
-        for d in details:
-            key = d["third_category_code"]
-            if not any(x["third_category_code"] == key for x in section_map[label]):
-                section_map[label].append(d)
-
-    total_third = 0
-    for label, details in section_map.items():
-        print(f"\n[{label}]  三级分类数={len(details)}")
-        for d in details:
-            line_range = f"L{d.get('start_line', '?')}-{d.get('end_line', '?')}"
-            preview = (d.get("content") or "")[:50].replace("\n", " ")
-            print(f"  ├ {d['third_category_name']}({d['third_category_code']})  {line_range}  {preview}...")
-        total_third += len(details)
-
-    _sep()
-    print(f"处理 chunks: {len(updated_chunks)}  |  识别三级分类: {total_third}  |  结果目录: {OUTPUT_DIR}")

+ 66 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/__init__.py

@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+LLM 内容三级分类识别模块 v2
+
+重构后的模块化版本,向后兼容原有接口。
+"""
+
+from .models import CategoryStandard, SecondCategoryStandard, ClassifiedContent, SectionContent, ClassificationResult
+from .config import ClassifierConfig, DEFAULT_CONFIG, API_KEY, MAX_CONCURRENT_REQUESTS, MAX_RETRIES, RETRY_DELAY, BASE_URL, MODEL, EMBEDDING_API_KEY, EMBEDDING_BASE_URL, EMBEDDING_MODEL, EMBEDDING_SIMILARITY_THRESHOLD, CATEGORY_TABLE_PATH, SECOND_CATEGORY_PATH
+from .category_loaders import SECONDARY_CATEGORY_KEYWORDS, CategoryStandardLoader, SecondCategoryStandardLoader
+from .embedding_client import EmbeddingClient
+from .content_classifier import ContentClassifierClient
+from .chunks_converter import ChunksConverter
+from .main_classifier import LLMContentClassifier, classify_chunks, classify_chunks_sync
+from .text_split_utils import split_section_into_chunks
+from .prompt import (
+    CLASSIFY_SYSTEM_PROMPT,
+    SUPPLEMENT_VERIFY_SYSTEM_PROMPT,
+    build_classify_prompt,
+    build_fix_prompt,
+    build_supplement_verify_prompt,
+)
+
+__all__ = [
+    # 提示词
+    "CLASSIFY_SYSTEM_PROMPT",
+    "SUPPLEMENT_VERIFY_SYSTEM_PROMPT",
+    "build_classify_prompt",
+    "build_fix_prompt",
+    "build_supplement_verify_prompt",
+    # 数据模型
+    "CategoryStandard",
+    "SecondCategoryStandard",
+    "ClassifiedContent",
+    "SectionContent",
+    "ClassificationResult",
+    # 配置
+    "ClassifierConfig",
+    "DEFAULT_CONFIG",
+    "API_KEY",
+    "MAX_CONCURRENT_REQUESTS",
+    "MAX_RETRIES",
+    "RETRY_DELAY",
+    "BASE_URL",
+    "MODEL",
+    "EMBEDDING_API_KEY",
+    "EMBEDDING_BASE_URL",
+    "EMBEDDING_MODEL",
+    "EMBEDDING_SIMILARITY_THRESHOLD",
+    "CATEGORY_TABLE_PATH",
+    "SECOND_CATEGORY_PATH",
+    # 加载器
+    "SECONDARY_CATEGORY_KEYWORDS",
+    "CategoryStandardLoader",
+    "SecondCategoryStandardLoader",
+    # 客户端
+    "EmbeddingClient",
+    "ContentClassifierClient",
+    "ChunksConverter",
+    "LLMContentClassifier",
+    # 便捷函数
+    "classify_chunks",
+    "classify_chunks_sync",
+    "split_section_into_chunks",
+]

+ 227 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/category_loaders.py

@@ -0,0 +1,227 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+CSV加载器 + 关键词映射
+"""
+
+import csv
+from pathlib import Path
+from typing import Dict, List, Optional
+
+from .models import CategoryStandard, SecondCategoryStandard
+
+
+# ==================== 二级分类关键词映射 ====================
+# 用于将文档中的二级标题名称映射到 StandardCategoryTable.csv 中的标准名称
+# 格式: { CSV标准名称: [可能的文档名称列表] }
+SECONDARY_CATEGORY_KEYWORDS = {
+    # 编制依据 (basis)
+    "法律法规": ["法律法规", "法律", "法规"],
+    "标准规范": ["标准规范", "标准", "规范", "技术标准"],
+    "文件制度": ["文件制度", "制度文件", "管理文件"],
+    "编制原则": ["编制原则", "原则"],
+    "编制范围": ["编制范围", "范围", "工程范围"],
+
+    # 工程概况 (overview)
+    "设计概况": ["设计概况", "工程简介", "工程概况", "概况"],
+    "工程地质与水文气象": ["工程地质与水文气象", "地质", "水文", "气象", "工程地质", "水文气象", "地质与水文"],
+    "周边环境": ["周边环境", "环境", "周围环境"],
+    "施工平面及立面布置": ["施工平面及立面布置", "平面布置", "立面布置", "施工平面", "平面及立面"],
+    "施工要求和技术保证条件": ["施工要求和技术保证条件", "施工要求", "技术保证", "保证条件"],
+    "风险辨识与分级": ["风险辨识与分级", "风险辨识", "风险分级", "风险", "风险等级"],
+    "参建各方责任主体单位": ["参建各方责任主体单位", "参建单位", "责任主体", "参建各方"],
+
+    # 施工计划 (plan)
+    "施工进度计划": ["施工进度计划", "进度计划", "进度", "工期计划"],
+    "施工材料计划": ["施工材料计划", "材料计划", "材料"],
+    "施工设备计划": ["施工设备计划", "设备计划", "机械设备", "设备"],
+    "劳动力计划": ["劳动力计划", "劳动力", "人员计划", "用工计划"],
+    "安全生产费用使用计划": ["安全生产费用使用计划", "安全费用", "安全费", "安全生产费用"],
+
+    # 施工工艺技术 (technology)
+    "主要施工方法概述": ["主要施工方法概述", "施工方法概述", "方法概述", "施工方法"],
+    "技术参数": ["技术参数", "参数", "技术指标"],
+    "工艺流程": ["工艺流程", "流程", "施工流程"],
+    "施工准备": ["施工准备", "准备", "准备工作"],
+    "施工方法及操作要求": ["施工方法及操作要求", "施工方案及操作要求", "操作要求", "施工方案", "施工方法", "方法及操作"],
+    "检查要求": ["检查要求", "检查", "验收要求", "检查验收"],
+
+    # 安全保证措施 (safety)
+    "安全保证体系": ["安全保证体系", "安全体系", "安全管理体系"],
+    "组织保证措施": ["组织保证措施", "组织措施", "组织保证"],
+    "技术保证措施": ["技术保证措施", "技术保障措施", "技术措施", "保障措施", "技术保障", "安全防护措施", "安全防护"],
+    "监测监控措施": ["监测监控措施", "监测措施", "监控措施", "监测监控"],
+    "应急处置措施": ["应急处置措施", "应急预案", "应急措施", "应急处置"],
+
+    # 质量保证措施 (quality)
+    "质量保证体系": ["质量保证体系", "质量体系", "质量管理体系"],
+    "质量目标": ["质量目标", "质量指标"],
+    "工程创优规划": ["工程创优规划", "创优规划", "创优计划", "创优"],
+    "质量控制程序与具体措施": ["质量控制程序与具体措施", "质量控制", "质量措施", "质量控制措施"],
+
+    # 环境保证措施 (environment)
+    "环境保证体系": ["环境保证体系", "环境体系", "环境管理体系"],
+    "环境保护组织机构": ["环境保护组织机构", "环保组织", "环境组织"],
+    "环境保护及文明施工措施": ["环境保护及文明施工措施", "环保措施", "文明施工", "环境保护", "环境措施"],
+
+    # 施工管理及作业人员配备与分工 (management)
+    "施工管理人员": ["施工管理人员", "管理人员", "管理人员配备"],
+    "专职安全生产管理人员": ["专职安全生产管理人员", "专职安全员", "安全管理人员", "安全员", "特种作业人员", "特种工"],
+    "其他作业人员": ["其他作业人员", "其他人员", "作业人员"],
+
+    # 验收要求 (acceptance)
+    "验收标准": ["验收标准", "验收规范", "标准"],
+    "验收程序": ["验收程序", "验收流程", "程序"],
+    "验收内容": ["验收内容", "验收项目"],
+    "验收时间": ["验收时间", "验收日期"],
+    "验收人员": ["验收人员", "验收参与人员"],
+
+    # 其他资料 (other)
+    "计算书": ["计算书", "计算", "验算"],
+    "相关施工图纸": ["相关施工图纸", "施工图纸", "图纸"],
+    "附图附表": ["附图附表", "附图", "附表"],
+    "编制及审核人员情况": ["编制及审核人员情况", "编制人员", "审核人员"],
+}
+
+
+# ==================== 标准分类加载器 ====================
+
+class CategoryStandardLoader:
+    """加载 StandardCategoryTable.csv"""
+
+    def __init__(self, csv_path: Path):
+        self.csv_path = csv_path
+        self.standards: List[CategoryStandard] = []
+        self._load()
+
+    def _load(self):
+        """加载CSV文件"""
+        with open(self.csv_path, 'r', encoding='utf-8-sig') as f:  # utf-8-sig处理BOM
+            reader = csv.DictReader(f)
+            for row in reader:
+                self.standards.append(CategoryStandard(
+                    first_code=row.get('first_code', ''),
+                    first_name=row.get('first_name', ''),
+                    first_seq=int(row.get('first_seq', '0') or 0),
+                    second_code=row.get('second_code', ''),
+                    second_name=row.get('second_name', ''),
+                    second_seq=int(row.get('second_seq', '0') or 0),
+                    second_focus=row.get('second_focus', ''),
+                    third_code=row.get('third_code', ''),
+                    third_name=row.get('third_name', ''),
+                    third_seq=int(row.get('third_seq', '0') or 0),
+                    third_focus=row.get('third_focus', ''),
+                    keywords=row.get('keywords', '')
+                ))
+
+    def get_standards_by_second_code(self, second_code: str) -> List[CategoryStandard]:
+        """根据二级分类代码获取对应的三级分类标准"""
+        return [s for s in self.standards if s.second_code == second_code]
+
+    def _find_standard_name_by_keyword(self, second_name: str) -> Optional[str]:
+        """
+        通过关键词映射查找标准二级分类名称
+
+        Args:
+            second_name: 文档中的二级标题名称
+
+        Returns:
+            匹配到的标准名称,未匹配返回None
+        """
+        cleaned_name = second_name.strip().lower()
+
+        # 遍历映射表进行匹配
+        for standard_name, keywords in SECONDARY_CATEGORY_KEYWORDS.items():
+            for keyword in keywords:
+                # 宽容匹配:关键词在标题中,或标题在关键词中
+                if keyword.lower() in cleaned_name or cleaned_name in keyword.lower():
+                    return standard_name
+
+        return None
+
+    def get_standards_by_second_name(self, second_name: str) -> List[CategoryStandard]:
+        """
+        根据二级分类名称获取对应的三级分类标准(支持模糊匹配)
+
+        匹配优先级:
+        1. 完全匹配 CSV 中的标准名称
+        2. 包含关系匹配(标准名包含标题名,或标题名包含标准名)
+        3. 关键词映射匹配(通过 SECONDARY_CATEGORY_KEYWORDS)
+
+        Args:
+            second_name: 二级标题名称
+
+        Returns:
+            匹配到的三级分类标准列表
+        """
+        cleaned_name = second_name.strip()
+
+        # 1. 先尝试完全匹配
+        exact = [s for s in self.standards if s.second_name == cleaned_name]
+        if exact:
+            return exact
+
+        # 2. 包含关系匹配(取第一个命中的 second_name,再返回同名的全部行)
+        for s in self.standards:
+            if s.second_name in cleaned_name or cleaned_name in s.second_name:
+                matched_name = s.second_name
+                return [st for st in self.standards if st.second_name == matched_name]
+
+        # 3. 使用关键词映射进行模糊匹配
+        matched_standard_name = self._find_standard_name_by_keyword(cleaned_name)
+        if matched_standard_name:
+            return [s for s in self.standards if s.second_name == matched_standard_name]
+
+        return []
+
+
+class SecondCategoryStandardLoader:
+    """加载 construction_plan_standards.csv(二级分类标准)"""
+
+    def __init__(self, csv_path: Path):
+        self.csv_path = csv_path
+        self.standards: List[SecondCategoryStandard] = []
+        self._load()
+
+    def _load(self):
+        """加载CSV文件"""
+        with open(self.csv_path, 'r', encoding='utf-8-sig') as f:  # utf-8-sig处理BOM
+            reader = csv.DictReader(f)
+            for row in reader:
+                self.standards.append(SecondCategoryStandard(
+                    first_name=row.get('first_name', '').strip(),
+                    second_name=row.get('second_name', '').strip(),
+                    second_raw_content=row.get('second_raw_content', '').strip()
+                ))
+
+    def get_standard_by_second_name(self, second_name: str) -> Optional[SecondCategoryStandard]:
+        """根据二级分类名称获取标准定义(支持模糊匹配)"""
+        # 清理待匹配的名称
+        cleaned_name = second_name.strip().lower()
+
+        # 1. 先尝试完全匹配或包含关系匹配
+        for std in self.standards:
+            # 完全匹配
+            if std.second_name.lower() == cleaned_name:
+                return std
+            # 包含关系匹配
+            if std.second_name.lower() in cleaned_name or cleaned_name in std.second_name.lower():
+                return std
+
+        # 2. 使用关键词映射进行模糊匹配
+        matched_standard_name = None
+        for standard_name, keywords in SECONDARY_CATEGORY_KEYWORDS.items():
+            for keyword in keywords:
+                if keyword.lower() in cleaned_name or cleaned_name in keyword.lower():
+                    matched_standard_name = standard_name
+                    break
+            if matched_standard_name:
+                break
+
+        if matched_standard_name:
+            # 在standards中查找匹配的标准
+            for std in self.standards:
+                if std.second_name == matched_standard_name:
+                    return std
+
+        return None

+ 207 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/chunks_converter.py

@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ChunksConverter:chunks 格式与 SectionContent 格式的转换器
+"""
+
+import re
+from typing import Any, Dict, List, Tuple
+
+from .models import ClassificationResult, SectionContent
+from .category_loaders import CategoryStandardLoader
+
+
+class ChunksConverter:
+    """chunks 格式与 SectionContent 格式的转换器"""
+
+    def __init__(self, category_loader: CategoryStandardLoader):
+        self.category_loader = category_loader
+
+    def chunks_to_sections(self, chunks: List[Dict[str, Any]]) -> List[SectionContent]:
+        """
+        将 chunks 列表转换为 SectionContent 列表
+
+        分组策略:
+        1. 优先按 section_label 分组(更精确的文档结构)
+        2. 如果 section_label 相同,再按一级分类分组
+        3. 从 section_label 提取二级分类名称用于匹配三级标准
+
+        Args:
+            chunks: 文档分块列表,每个 chunk 需包含:
+                - chapter_classification: 一级分类代码
+                - secondary_category_code: 二级分类代码(可能为 none)
+                - secondary_category_cn: 二级分类中文名
+                - review_chunk_content 或 content: 内容文本
+                - section_label: 章节标签(如 "第一章编制依据->一、法律法规")
+
+        Returns:
+            List[SectionContent]: 二级标题段落列表
+        """
+        # 按 section_label 分组(更精确)
+        # section_label 格式: "第一章编制依据->一、法律法规"
+        section_groups: Dict[str, List[Dict]] = {}
+
+        for chunk in chunks:
+            # 获取分类信息
+            section_label = chunk.get("section_label", "") or chunk.get("chapter", "")
+            first_code = chunk.get("chapter_classification", "") or chunk.get("first_code", "")
+            second_code = chunk.get("secondary_category_code", "") or chunk.get("second_code", "")
+            second_cn = chunk.get("secondary_category_cn", "") or chunk.get("second_name", "")
+
+            # 分组策略:每个二级分类独立分组,禁止合并不同二级分类
+            # 优先使用 section_label,其次使用 secondary_category_code
+            if section_label and "->" in section_label:
+                # 有明确的章节标签,使用它作为分组键
+                group_key = section_label
+            elif second_code and second_code not in ("none", "None", ""):
+                # 有二级分类代码,按二级分类独立分组(关键:不再合并到一级分类下)
+                group_key = f"{first_code}->{second_code}"
+            elif section_label:
+                group_key = section_label
+            else:
+                # 完全没有分类信息,使用唯一键避免合并
+                group_key = f"unknown_{first_code}_{id(chunk)}"
+
+            if group_key not in section_groups:
+                section_groups[group_key] = []
+            section_groups[group_key].append(chunk)
+
+        # 为每个分组创建 SectionContent
+        section_contents = []
+        all_lines = []  # 全局行号追踪
+
+        for group_key, group_chunks in section_groups.items():
+            if not group_chunks:
+                continue
+
+            # 合并该分组的所有内容,同时记录每个原始 chunk 的行范围
+            section_lines = []
+            chunk_line_counts: List[Tuple[str, int]] = []  # (chunk_id, line_count)
+            for chunk in group_chunks:
+                content = chunk.get("review_chunk_content", "") or chunk.get("content", "") or chunk.get("original_content", "")
+                if content:
+                    lines = content.split('\n')
+                    n = len(lines)
+                    chunk_id = chunk.get("chunk_id") or chunk.get("id") or str(id(chunk))
+                    chunk_line_counts.append((chunk_id, n))
+                    section_lines.extend(lines)
+                    all_lines.extend(lines)
+                else:
+                    chunk_id = chunk.get("chunk_id") or chunk.get("id") or str(id(chunk))
+                    chunk_line_counts.append((chunk_id, 0))
+
+            if not section_lines:
+                continue
+
+            # 获取一级分类代码
+            first_code = group_chunks[0].get("chapter_classification", "") or group_chunks[0].get("first_code", "")
+
+            # 获取二级分类名称和代码
+            second_code = group_chunks[0].get("secondary_category_code", "") or group_chunks[0].get("second_code", "")
+            second_cn = group_chunks[0].get("secondary_category_cn", "") or group_chunks[0].get("second_name", "")
+
+            # 从 section_label 提取二级分类名称(优先)
+            section_label = group_chunks[0].get("section_label", "") or group_chunks[0].get("chapter", "")
+            if "->" in section_label:
+                parts = section_label.split("->")
+                if len(parts) >= 2:
+                    extracted = parts[1].strip()
+                    # 去除序号前缀(如 "一、" "二、")
+                    cleaned = re.sub(r'^[一二三四五六七八九十]+[、)\s]+', '', extracted).strip()
+                    if cleaned:
+                        second_cn = cleaned
+                        # 尝试根据提取的名称匹配二级分类代码
+                        matched_standards = self.category_loader.get_standards_by_second_name(cleaned)
+                        if matched_standards:
+                            second_code = matched_standards[0].second_code
+
+            # 构建带行号的内容
+            start_line = len(all_lines) - len(section_lines) + 1
+            line_number_map = list(range(start_line, len(all_lines) + 1))
+            numbered_lines = []
+            for i, line in enumerate(section_lines):
+                numbered_lines.append(f"<{line_number_map[i]}> {line}")
+            numbered_content = '\n'.join(numbered_lines)
+
+            # 计算每个原始 chunk 在全局行号中的范围
+            chunk_ranges: List[Tuple[str, int, int]] = []
+            current_global = start_line
+            for chunk_id, n_lines in chunk_line_counts:
+                if n_lines > 0:
+                    chunk_ranges.append((chunk_id, current_global, current_global + n_lines - 1))
+                    current_global += n_lines
+
+            # 获取三级分类标准
+            category_standards = self.category_loader.get_standards_by_second_code(second_code)
+            if not category_standards:
+                category_standards = self.category_loader.get_standards_by_second_name(second_cn)
+
+            # 构建 section_key(使用 group_key 保留 section_label 信息,粒度更细)
+            section_key = group_key
+
+            section_contents.append(SectionContent(
+                section_key=section_key,
+                section_name=second_cn or second_code,
+                lines=section_lines,
+                numbered_content=numbered_content,
+                category_standards=category_standards,
+                line_number_map=line_number_map,
+                chunk_ranges=chunk_ranges
+            ))
+
+        return section_contents
+
+    def classification_result_to_chunks(
+        self,
+        result: ClassificationResult,
+        original_chunks: List[Dict[str, Any]],
+        first_code: str,
+        second_code: str
+    ) -> List[Dict[str, Any]]:
+        """
+        将 ClassificationResult 转换回 chunks 格式
+
+        将行级分类结果展开,为每个三级分类创建对应的 chunk 条目
+
+        Args:
+            result: 分类结果
+            original_chunks: 原始 chunks(用于保留其他字段)
+            first_code: 一级分类代码
+            second_code: 二级分类代码
+
+        Returns:
+            List[Dict]: 更新后的 chunks 列表
+        """
+        updated_chunks = []
+
+        # 收集所有三级分类信息,过滤掉非标准项(no_standard)
+        tertiary_classifications = []
+        for content in result.classified_contents:
+            # 跳过非标准项,不纳入三级分类统计
+            if content.third_category_code == "no_standard":
+                continue
+            tertiary_classifications.append({
+                "third_category_name": content.third_category_name,
+                "third_category_code": content.third_category_code,
+                "start_line": content.start_line,
+                "end_line": content.end_line,
+                "content": content.content
+            })
+
+        # 更新原始 chunks
+        for chunk in original_chunks:
+            updated_chunk = dict(chunk)
+            updated_chunk["first_code"] = first_code
+            updated_chunk["second_code"] = second_code
+
+            # 添加三级分类详情列表
+            updated_chunk["tertiary_classification_details"] = tertiary_classifications
+
+            # 如果有三级分类结果,设置第一个作为主要分类(向后兼容)
+            if tertiary_classifications:
+                updated_chunk["tertiary_category_code"] = tertiary_classifications[0]["third_category_code"]
+                updated_chunk["tertiary_category_cn"] = tertiary_classifications[0]["third_category_name"]
+
+            updated_chunks.append(updated_chunk)
+
+        return updated_chunks

+ 155 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/config.py

@@ -0,0 +1,155 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+配置类与全局变量
+"""
+
+from pathlib import Path
+from typing import Tuple
+from dataclasses import dataclass
+
+from foundation.infrastructure.config.config import config_handler
+
+
+def _get_llm_config_from_ini(model_type: str) -> Tuple[str, str, str]:
+    """
+    从 config.ini 获取 LLM 配置
+
+    Args:
+        model_type: 模型类型(如 qwen3_5_122b_a10b)
+
+    Returns:
+        Tuple[str, str, str]: (api_key, base_url, model_id)
+    """
+    try:
+        # 尝试读取 DashScope 格式配置
+        base_url = config_handler.get(model_type, "DASHSCOPE_SERVER_URL", "")
+        model_id = config_handler.get(model_type, "DASHSCOPE_MODEL_ID", "")
+        api_key = config_handler.get(model_type, "DASHSCOPE_API_KEY", "")
+
+        # 如果没有 DashScope 配置,尝试读取其他格式
+        if not base_url:
+            # 尝试 QWEN_SERVER_URL 格式
+            base_url = config_handler.get(model_type, f"{model_type.upper()}_SERVER_URL", "")
+            model_id = config_handler.get(model_type, f"{model_type.upper()}_MODEL_ID", "")
+            api_key = config_handler.get(model_type, f"{model_type.upper()}_API_KEY", "")
+
+        return api_key, base_url, model_id
+    except Exception:
+        return "", "", ""
+
+
+def _get_embedding_config_from_ini(embedding_model_type: str) -> Tuple[str, str, str]:
+    """
+    从 config.ini 获取 Embedding 模型配置
+
+    Args:
+        embedding_model_type: Embedding 模型类型
+
+    Returns:
+        Tuple[str, str, str]: (api_key, base_url, model_id)
+    """
+    try:
+        # 本地 Embedding 模型
+        if embedding_model_type == "lq_qwen3_8b_emd":
+            base_url = config_handler.get("lq_qwen3_8b_emd", "LQ_EMBEDDING_SERVER_URL", "")
+            model_id = config_handler.get("lq_qwen3_8b_emd", "LQ_EMBEDDING_MODEL_ID", "Qwen3-Embedding-8B")
+            api_key = config_handler.get("lq_qwen3_8b_emd", "LQ_EMBEDDING_API_KEY", "dummy")
+            return api_key, base_url, model_id
+
+        # 硅基流动 Embedding 模型
+        elif embedding_model_type == "siliconflow_embed":
+            base_url = config_handler.get("siliconflow_embed", "SLCF_EMBED_SERVER_URL", "")
+            model_id = config_handler.get("siliconflow_embed", "SLCF_EMBED_MODEL_ID", "Qwen/Qwen3-Embedding-8B")
+            api_key = config_handler.get("siliconflow_embed", "SLCF_EMBED_API_KEY", "")
+            return api_key, base_url, model_id
+
+        return "", "", ""
+    except Exception:
+        return "", "", ""
+
+
+@dataclass
+class ClassifierConfig:
+    """分类器配置(从 config.ini 加载)"""
+
+    # LLM API 配置(从 config.ini 加载)
+    api_key: str = ""
+    base_url: str = ""
+    model: str = ""
+
+    # 并发控制
+    max_concurrent_requests: int = 10
+    max_retries: int = 3
+    retry_delay: int = 1
+
+    # Embedding 配置(从 config.ini 加载)
+    embedding_api_key: str = ""
+    embedding_base_url: str = ""
+    embedding_model: str = ""
+    embedding_similarity_threshold: float = 0.9
+
+    # 路径配置
+    category_table_path: str = ""
+    second_category_path: str = ""
+    output_path: str = ""
+
+    def __post_init__(self):
+        """从 config.ini 加载配置"""
+        # 加载 LLM 配置
+        llm_model_type = config_handler.get("model", "COMPLETENESS_REVIEW_MODEL_TYPE", "qwen3_5_122b_a10b")
+        api_key, base_url, model_id = _get_llm_config_from_ini(llm_model_type)
+
+        # 设置 LLM 配置(如果从 config.ini 读取成功)
+        if api_key:
+            self.api_key = api_key
+        if base_url:
+            self.base_url = base_url
+        if model_id:
+            self.model = model_id
+
+        # 加载 Embedding 配置
+        embedding_model_type = config_handler.get("model", "EMBEDDING_MODEL_TYPE", "lq_qwen3_8b_emd")
+        emb_api_key, emb_base_url, emb_model_id = _get_embedding_config_from_ini(embedding_model_type)
+
+        if emb_api_key:
+            self.embedding_api_key = emb_api_key
+        if emb_base_url:
+            self.embedding_base_url = emb_base_url
+        if emb_model_id:
+            self.embedding_model = emb_model_id
+
+        # 初始化默认路径
+        # 注意:本文件位于 reviewers/utils/llm_content_classifier_v2/config.py
+        # parent.parent.parent.parent = component/
+        if not self.category_table_path:
+            self.category_table_path = str(
+                Path(__file__).parent.parent.parent.parent / "doc_worker" / "config" / "StandardCategoryTable.csv"
+            )
+        if not self.second_category_path:
+            self.second_category_path = str(
+                Path(__file__).parent.parent.parent.parent / "doc_worker" / "config" / "construction_plan_standards.csv"
+            )
+        if not self.output_path:
+            # 项目根目录下的 temp/construction_review/llm_content_classifier_v2
+            # 从 reviewers/utils/llm_content_classifier_v2/ 向上 7 层到项目根目录
+            project_root = Path(__file__).parent.parent.parent.parent.parent.parent.parent
+            self.output_path = str(project_root / "temp" / "construction_review" / "llm_content_classifier_v2")
+
+
+# 默认配置实例(从 config.ini 加载,用于独立运行测试)
+DEFAULT_CONFIG = ClassifierConfig()
+
+# 向后兼容的全局变量(供独立运行测试使用,从 config.ini 加载)
+API_KEY = DEFAULT_CONFIG.api_key
+MAX_CONCURRENT_REQUESTS = DEFAULT_CONFIG.max_concurrent_requests
+MAX_RETRIES = DEFAULT_CONFIG.max_retries
+RETRY_DELAY = DEFAULT_CONFIG.retry_delay
+BASE_URL = DEFAULT_CONFIG.base_url
+MODEL = DEFAULT_CONFIG.model
+EMBEDDING_API_KEY = DEFAULT_CONFIG.embedding_api_key
+EMBEDDING_BASE_URL = DEFAULT_CONFIG.embedding_base_url
+EMBEDDING_MODEL = DEFAULT_CONFIG.embedding_model
+EMBEDDING_SIMILARITY_THRESHOLD = DEFAULT_CONFIG.embedding_similarity_threshold
+CATEGORY_TABLE_PATH = Path(DEFAULT_CONFIG.category_table_path)
+SECOND_CATEGORY_PATH = Path(DEFAULT_CONFIG.second_category_path)

+ 791 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/content_classifier.py

@@ -0,0 +1,791 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+ContentClassifierClient 核心分类逻辑
+"""
+
+import asyncio
+import json
+import re
+import time
+from typing import Dict, List, Optional, Tuple
+
+from openai import AsyncOpenAI
+
+from .models import CategoryStandard, ClassifiedContent, ClassificationResult, SectionContent
+from .config import API_KEY, BASE_URL
+from .embedding_client import EmbeddingClient
+from .category_loaders import SecondCategoryStandardLoader
+from .json_utils import _fix_json, _aggressive_json_fix
+from .prompt import (
+    CLASSIFY_SYSTEM_PROMPT,
+    SUPPLEMENT_VERIFY_SYSTEM_PROMPT,
+    build_classify_prompt,
+    build_fix_prompt,
+    build_supplement_verify_prompt,
+)
+from foundation.observability.logger.loggering import review_logger as logger
+
+
+class ContentClassifierClient:
+    """LLM 内容分类客户端"""
+
+    def __init__(self, model: str, semaphore: asyncio.Semaphore, embedding_client: Optional[EmbeddingClient] = None, second_category_loader: Optional[SecondCategoryStandardLoader] = None):
+        self.model = model
+        self.semaphore = semaphore
+        self.client = AsyncOpenAI(
+            api_key=API_KEY,
+            base_url=BASE_URL
+        )
+        self.embedding_client = embedding_client
+        self.second_category_loader = second_category_loader
+
+    async def classify_content(self, section: SectionContent) -> ClassificationResult:
+        """对内容进行三级分类识别(带并发控制和自动修复,支持长内容分块处理)"""
+        start_time = time.time()
+
+        # 步骤1: 使用Embedding模型检查二级分类与内容的相似度
+        if self.embedding_client and self.second_category_loader and section.category_standards:
+            # 从construction_plan_standards.csv中查找对应的标准二级分类
+            # 使用section_name进行匹配
+            std_second_category = self.second_category_loader.get_standard_by_second_name(section.section_name)
+
+            if std_second_category:
+                # 找到了对应的标准二级分类,进行相似度检查
+                # 检查section内容与标准的second_raw_content的一致性
+                section_text = '\n'.join(section.lines)
+                is_similar, similarity = await self.embedding_client.check_similarity(
+                    section_name=section.section_name,
+                    section_content=section_text,
+                    second_category_name=std_second_category.second_name,
+                    second_category_raw_content=std_second_category.second_raw_content
+                )
+
+                if is_similar:
+                    from .config import EMBEDDING_SIMILARITY_THRESHOLD
+                    logger.debug(f"[{section.section_name}] 相似度检查通过 ({similarity:.3f} >= {EMBEDDING_SIMILARITY_THRESHOLD}),跳过LLM分类,默认包含所有三级分类")
+                    # 生成默认分类结果:包含所有三级分类
+                    all_contents = self._generate_default_classification(section)
+                    total_lines, classified_lines, coverage_rate = self._calculate_coverage_rate(section, all_contents)
+                    latency = time.time() - start_time
+                    return ClassificationResult(
+                        model=self.model,
+                        section_key=section.section_key,
+                        section_name=section.section_name,
+                        classified_contents=all_contents,
+                        latency=latency,
+                        raw_response=f"[Embedding相似度跳过] similarity={similarity:.3f}",
+                        error=None,
+                        total_lines=total_lines,
+                        classified_lines=classified_lines,
+                        coverage_rate=coverage_rate
+                    )
+                else:
+                    logger.debug(f"[{section.section_name}] 相似度检查未通过 ({similarity:.3f} < ?),继续LLM分类")
+            else:
+                logger.debug(f"[{section.section_name}] 未在construction_plan_standards.csv中找到对应标准,继续LLM分类")
+
+        # 如果内容过长,分块处理
+        MAX_LINES_PER_CHUNK = 150  # 每个块最多150行
+        total_lines = len(section.lines)
+
+        if total_lines <= MAX_LINES_PER_CHUNK:
+            # 内容不长,直接处理
+            result = await self._classify_single_chunk(section, start_time)
+            # 补充验证:关键字扫描 + LLM二次确认,补充遗漏的分类
+            if not result.error and result.classified_contents is not None:
+                supplement = await self._detect_and_supplement(section, result.classified_contents)
+                if supplement:
+                    merged = self._merge_classified_contents(result.classified_contents + supplement, section)
+                    total_l, classified_l, coverage_r = self._calculate_coverage_rate(section, merged)
+                    return ClassificationResult(
+                        model=result.model,
+                        section_key=result.section_key,
+                        section_name=result.section_name,
+                        classified_contents=merged,
+                        latency=result.latency,
+                        raw_response=result.raw_response,
+                        error=result.error,
+                        total_lines=total_l,
+                        classified_lines=classified_l,
+                        coverage_rate=coverage_r
+                    )
+            return result
+
+        # 内容过长,无重叠分块处理
+        logger.debug(f"[{section.section_name}] 内容较长({total_lines}行),分块处理...")
+        all_contents = []
+        chunk_size = MAX_LINES_PER_CHUNK
+
+        chunk_start = 0
+        while chunk_start < total_lines:
+            chunk_end = min(chunk_start + chunk_size, total_lines)
+            chunk_section = self._create_chunk_section(section, chunk_start, chunk_end)
+
+            chunk_result = await self._classify_single_chunk(chunk_section, 0, is_chunk=True)
+
+            if chunk_result.error:
+                logger.error(f"[{section.section_name}] 块 {chunk_start+1}-{chunk_end} 处理失败: {chunk_result.error[:50]}")
+            else:
+                logger.debug(f"[{section.section_name}] 块 {chunk_start+1}-{chunk_end} 成功: {len(chunk_result.classified_contents)} 个分类")
+                all_contents.extend(chunk_result.classified_contents)
+
+            # 无重叠:下一块从当前块末尾紧接开始
+            chunk_start = chunk_end
+
+        # 所有块处理完成后,再次聚合所有内容(解决分块导致的同一分类分散问题)
+        if all_contents:
+            all_contents = self._merge_classified_contents(all_contents, section)
+
+        # 补充验证:关键字扫描 + LLM二次确认,补充遗漏的分类
+        supplement = await self._detect_and_supplement(section, all_contents)
+        if supplement:
+            all_contents = self._merge_classified_contents(all_contents + supplement, section)
+
+        # 计算分类率
+        total_lines, classified_lines, coverage_rate = self._calculate_coverage_rate(section, all_contents)
+
+        latency = time.time() - start_time
+
+        return ClassificationResult(
+            model=self.model,
+            section_key=section.section_key,
+            section_name=section.section_name,
+            classified_contents=all_contents,
+            latency=latency,
+            raw_response="",
+            error=None if all_contents else "所有块处理失败",
+            total_lines=total_lines,
+            classified_lines=classified_lines,
+            coverage_rate=coverage_rate
+        )
+
+    def _calculate_coverage_rate(self, section: SectionContent, contents: List[ClassifiedContent]) -> tuple:
+        """计算分类率(已分类行数/总行数)"""
+        total_lines = len(section.lines)
+        if total_lines == 0 or not contents:
+            return total_lines, 0, 0.0
+
+        # 使用集合记录已分类的行号(避免重复计数)
+        classified_line_set = set()
+
+        for content in contents:
+            if section.line_number_map:
+                # 如果有全局行号映射,找出起止行号对应的索引
+                start_idx = -1
+                end_idx = -1
+                for idx, global_line in enumerate(section.line_number_map):
+                    if global_line == content.start_line:
+                        start_idx = idx
+                    if global_line == content.end_line:
+                        end_idx = idx
+                        break
+
+                if start_idx != -1 and end_idx != -1:
+                    for i in range(start_idx, end_idx + 1):
+                        if i < len(section.line_number_map):
+                            classified_line_set.add(section.line_number_map[i])
+            else:
+                # 没有全局行号,直接使用起止行号
+                for line_num in range(content.start_line, content.end_line + 1):
+                    classified_line_set.add(line_num)
+
+        classified_lines = len(classified_line_set)
+        coverage_rate = (classified_lines / total_lines) * 100 if total_lines > 0 else 0.0
+
+        return total_lines, classified_lines, coverage_rate
+
+    def _generate_default_classification(self, section: SectionContent) -> List[ClassifiedContent]:
+        """
+        生成默认的分类结果(当embedding相似度检查通过时使用)
+        默认包含所有三级分类,覆盖整个section内容
+        """
+        if not section.category_standards:
+            return []
+
+        # 获取全局行号范围
+        if section.line_number_map:
+            start_line = section.line_number_map[0]
+            end_line = section.line_number_map[-1]
+        else:
+            start_line = 1
+            end_line = len(section.lines)
+
+        # 为每个三级分类创建一个条目,覆盖全部内容
+        default_contents = []
+        for std in section.category_standards:
+            # 提取该分类对应的内容
+            content = self._extract_content_by_line_numbers(section, start_line, end_line)
+            default_contents.append(ClassifiedContent(
+                third_category_name=std.third_name,
+                third_category_code=std.third_code,
+                third_seq=std.third_seq,
+                start_line=start_line,
+                end_line=end_line,
+                content=content
+            ))
+
+        return default_contents
+
+    def _create_chunk_section(self, section: SectionContent, start_idx: int, end_idx: int) -> SectionContent:
+        """从section创建子块"""
+        chunk_lines = section.lines[start_idx:end_idx]
+        chunk_line_map = section.line_number_map[start_idx:end_idx] if section.line_number_map else list(range(start_idx + 1, end_idx + 1))
+
+        # 生成带行号的内容
+        numbered_content = '\n'.join([f"<{chunk_line_map[i]}> {line}" for i, line in enumerate(chunk_lines)])
+
+        return SectionContent(
+            section_key=f"{section.section_key}_chunk_{start_idx}_{end_idx}",
+            section_name=section.section_name,
+            lines=chunk_lines,
+            numbered_content=numbered_content,
+            category_standards=section.category_standards,
+            line_number_map=chunk_line_map
+        )
+
+    async def _classify_single_chunk(self, section: SectionContent, start_time: float, is_chunk: bool = False) -> ClassificationResult:
+        """处理单个块"""
+        prompt = self._build_prompt(section, is_chunk=is_chunk)
+
+        try:
+            async with self.semaphore:
+                response = await self._call_api(prompt)
+
+            classified_contents, parse_error = await self._parse_with_fix(response, section, prompt)
+
+            if not is_chunk:
+                latency = time.time() - start_time
+                # 计算分类率
+                total_lines, classified_lines, coverage_rate = self._calculate_coverage_rate(section, classified_contents)
+                return ClassificationResult(
+                    model=self.model,
+                    section_key=section.section_key,
+                    section_name=section.section_name,
+                    classified_contents=classified_contents,
+                    latency=latency,
+                    raw_response=response[:1000],
+                    error=parse_error,
+                    total_lines=total_lines,
+                    classified_lines=classified_lines,
+                    coverage_rate=coverage_rate
+                )
+            else:
+                return ClassificationResult(
+                    model=self.model,
+                    section_key=section.section_key,
+                    section_name=section.section_name,
+                    classified_contents=classified_contents,
+                    latency=0,
+                    raw_response="",
+                    error=parse_error
+                )
+        except Exception as e:
+            if not is_chunk:
+                latency = time.time() - start_time
+                return ClassificationResult(
+                    model=self.model,
+                    section_key=section.section_key,
+                    section_name=section.section_name,
+                    classified_contents=[],
+                    latency=latency,
+                    error=str(e)
+                )
+            else:
+                return ClassificationResult(
+                    model=self.model,
+                    section_key=section.section_key,
+                    section_name=section.section_name,
+                    classified_contents=[],
+                    latency=0,
+                    error=str(e)
+                )
+
+    async def _parse_with_fix(self, response: str, section: SectionContent, original_prompt: str = "") -> tuple:
+        """解析响应,失败时让模型修复(最多3次重试)
+
+        返回: (contents, error_msg)
+        - contents: 分类结果列表(可能为空,表示模型判定无匹配内容)
+        - error_msg: 错误信息,None表示成功(包括空结果),非None表示解析失败
+        """
+        # 第一次尝试解析
+        contents, parse_success = self._parse_response(response, section)
+
+        # 解析成功(包括空结果,表示模型判定内容不符合任何分类标准)
+        if parse_success:
+            if not contents:
+                logger.debug(f"[{section.section_name}] 模型判定无匹配内容,记录为未分类")
+            return contents, None
+
+        # 解析失败(JSON格式错误),尝试让模型修复(最多3次)
+        logger.warning(f"[{section.section_name}] JSON解析失败,请求模型修复...")
+        logger.debug(f"[{section.section_name}] 原始响应前200字符: {response[:200]}...")
+
+        original_response = response
+
+        for attempt in range(3):
+            fix_prompt = self._build_fix_prompt(original_response)
+
+            try:
+                async with self.semaphore:
+                    fixed_response = await self._call_api(fix_prompt)
+
+                # 尝试解析修复后的输出
+                contents, parse_success = self._parse_response(fixed_response, section)
+                if parse_success:
+                    logger.debug(f"[{section.section_name}] 模型修复成功(第{attempt+1}次)")
+                    if not contents:
+                        logger.debug(f"[{section.section_name}] 修复后模型判定无匹配内容,记录为未分类")
+                    return contents, None
+                else:
+                    logger.debug(f"[{section.section_name}] 第{attempt+1}次修复失败,继续重试...")
+                    original_response = fixed_response
+            except Exception as e:
+                return [], f"请求模型修复失败: {str(e)}"
+
+        logger.error(f"[{section.section_name}] 模型修复3次后仍无法解析JSON")
+        return [], "模型修复3次后仍无法解析JSON"
+
+    def _build_fix_prompt(self, original_response: str) -> str:
+        """构建JSON修复提示词(委托给 prompt.py 中的 build_fix_prompt)"""
+        return build_fix_prompt(original_response)
+
+    def _build_prompt(self, section: SectionContent, is_chunk: bool = False) -> str:
+        """构建分类提示词(委托给 prompt.py 中的 build_classify_prompt)"""
+        return build_classify_prompt(section, is_chunk)
+
+    async def _call_api(self, prompt: str) -> str:
+        """调用API(带指数退避重试)"""
+        system_prompt = CLASSIFY_SYSTEM_PROMPT
+
+        kwargs = {
+            "model": self.model,
+            "messages": [
+                {"role": "system", "content": system_prompt},
+                {"role": "user", "content": prompt}
+            ],
+            "temperature": 0.1,  # 降低温度提高分类准确性
+            "max_tokens": 8000   # 增加输出空间
+        }
+
+        # qwen3.5 系列模型默认开启思考模式,需要显式关闭
+        # qwen3 系列模型不需要 enable_thinking 参数
+        if "qwen3.5" in self.model:
+            kwargs["extra_body"] = {"enable_thinking": False}
+
+        # 指数退避重试
+        max_retries = 5
+        base_delay = 2  # 基础延迟2秒
+
+        for attempt in range(max_retries):
+            try:
+                response = await self.client.chat.completions.create(**kwargs)
+                return response.choices[0].message.content or ""
+            except Exception as e:
+                error_str = str(e)
+                # 检查是否是429限流错误
+                if "429" in error_str or "rate limit" in error_str.lower():
+                    if attempt < max_retries - 1:
+                        # 指数退避: 2^attempt * (1 + random)
+                        delay = base_delay * (2 ** attempt) + (hash(prompt) % 1000) / 1000
+                        logger.warning(f"API限流(429),等待 {delay:.1f}s 后重试 ({attempt + 1}/{max_retries})...")
+                        await asyncio.sleep(delay)
+                        continue
+                # 其他错误或重试次数用完,抛出异常
+                raise
+
+        return ""
+
+    def _parse_response(self, response: str, section: SectionContent) -> tuple:
+        """解析响应(增强版,处理各种JSON格式问题)
+
+        返回: (contents, parse_success)
+        - contents: 分类结果列表
+        - parse_success: True表示JSON解析成功(包括空结果),False表示解析失败
+        """
+        if not response or not response.strip():
+            return [], False  # 空响应视为解析失败
+
+        response = response.strip()
+
+        # 尝试多种方式提取JSON
+        json_str = None
+
+        # 方法1: 从代码块中提取
+        code_block_match = re.search(r'```(?:json)?\s*([\s\S]*?)```', response)
+        if code_block_match:
+            json_str = code_block_match.group(1).strip()
+
+        # 方法2: 优先查找JSON数组(模型经常直接输出数组格式)
+        if not json_str:
+            # 使用非贪婪匹配找到第一个完整的数组
+            array_match = re.search(r'\[[\s\S]*?\]', response)
+            if array_match:
+                potential_array = array_match.group(0)
+                # 验证是否是有效的JSON数组
+                try:
+                    parsed = json.loads(potential_array)
+                    if isinstance(parsed, list):
+                        json_str = potential_array
+                except Exception:
+                    pass
+
+        # 方法3: 查找JSON对象
+        if not json_str:
+            json_match = re.search(r'\{[\s\S]*\}', response)
+            if json_match:
+                json_str = json_match.group(0)
+
+        if not json_str:
+            return [], False  # 未找到JSON结构,解析失败
+
+        # 处理模型直接输出数组的情况(包装成对象格式)
+        if json_str.strip().startswith('['):
+            try:
+                # 验证是有效的JSON数组
+                array_data = json.loads(json_str)
+                if isinstance(array_data, list):
+                    # 包装成期望的格式
+                    json_str = json.dumps({"classified_contents": array_data})
+            except Exception:
+                pass  # 不是有效数组,继续后续处理
+
+        # 先尝试直接解析,如果成功则不需要修复
+        try:
+            json.loads(json_str)
+            # JSON 有效,直接使用
+        except json.JSONDecodeError:
+            # JSON 无效,尝试修复
+            json_str = self._fix_json(json_str)
+
+        try:
+            data = json.loads(json_str)
+            # 处理数组格式
+            if isinstance(data, list):
+                data = {"classified_contents": data}
+            contents = []
+            # 支持两种键名: classified_contents 或 classified_contents_list
+            items = data.get("classified_contents", []) or data.get("classified_contents_list", [])
+
+            # 构建索引映射表:索引 -> (third_name, third_code, third_seq)
+            index_mapping = {0: ("非标准项", "no_standard", 0)}
+            if section.category_standards:
+                for i, std in enumerate(section.category_standards, 1):
+                    index_mapping[i] = (std.third_name, std.third_code, std.third_seq)
+
+            for item in items:
+                start_line = item.get("start_line", 0)
+                end_line = item.get("end_line", 0)
+
+                # 优先使用 category_index 进行映射
+                category_index = item.get("category_index")
+                if category_index is not None:
+                    # 通过索引映射获取标准名称、代码和序号
+                    idx = int(category_index) if isinstance(category_index, (int, float, str)) else 0
+                    category_name, category_code, category_seq = index_mapping.get(idx, ("非标准项", "no_standard", 0))
+                else:
+                    # 兼容旧格式:直接读取 third_category_code 和 third_category_name
+                    category_code = item.get("third_category_code", "")
+                    category_name = item.get("third_category_name", "")
+
+                    # 清理分类名称格式:移除末尾的代码部分
+                    if category_name and " (" in category_name and category_name.endswith(")"):
+                        category_name = re.sub(r'\s*\([^)]+\)\s*$', '', category_name).strip()
+
+                    # 验证分类代码是否在有效列表中
+                    valid_codes = set(v[1] for v in index_mapping.values())
+                    if category_code not in valid_codes:
+                        logger.warning(f"发现非标准分类 '{category_name}' ({category_code}),强制归为非标准项")
+                        category_code = "no_standard"
+                        category_name = "非标准项"
+
+                # 根据行号从section中提取原文
+                content = self._extract_content_by_line_numbers(section, start_line, end_line)
+                contents.append(ClassifiedContent(
+                    third_category_name=category_name,
+                    third_category_code=category_code,
+                    third_seq=category_seq,
+                    start_line=start_line,
+                    end_line=end_line,
+                    content=content
+                ))
+            # 聚合同一分类下相邻的内容
+            contents = self._merge_classified_contents(contents, section)
+            return contents, True  # 解析成功(可能为空结果)
+        except Exception as e:
+            # 尝试更激进的修复
+            try:
+                fixed = self._aggressive_json_fix(json_str)
+                data = json.loads(fixed)
+                # 处理数组格式
+                if isinstance(data, list):
+                    data = {"classified_contents": data}
+                contents = []
+                # 支持两种键名: classified_contents 或 classified_contents_list
+                items = data.get("classified_contents", []) or data.get("classified_contents_list", [])
+
+                # 构建索引映射表:索引 -> (third_name, third_code)
+                index_mapping = {0: ("非标准项", "no_standard")}
+                if section.category_standards:
+                    for i, std in enumerate(section.category_standards, 1):
+                        index_mapping[i] = (std.third_name, std.third_code)
+
+                for item in items:
+                    start_line = item.get("start_line", 0)
+                    end_line = item.get("end_line", 0)
+
+                    # 优先使用 category_index 进行映射
+                    category_index = item.get("category_index")
+                    if category_index is not None:
+                        idx = int(category_index) if isinstance(category_index, (int, float, str)) else 0
+                        category_name, category_code = index_mapping.get(idx, ("非标准项", "no_standard"))
+                    else:
+                        # 兼容旧格式
+                        category_code = item.get("third_category_code", "")
+                        category_name = item.get("third_category_name", "")
+                        valid_codes = set(v[1] for v in index_mapping.values())
+                        if category_code not in valid_codes:
+                            logger.warning(f"发现非标准分类 '{category_name}' ({category_code}),强制归为非标准项")
+                            category_code = "no_standard"
+                            category_name = "非标准项"
+
+                    # 根据行号从section中提取原文
+                    content = self._extract_content_by_line_numbers(section, start_line, end_line)
+                    contents.append(ClassifiedContent(
+                        third_category_name=category_name,
+                        third_category_code=category_code,
+                        third_seq=0,
+                        start_line=start_line,
+                        end_line=end_line,
+                        content=content
+                    ))
+                # 聚合同一分类下相邻的内容
+                contents = self._merge_classified_contents(contents, section)
+                return contents, True  # 解析成功(可能为空结果)
+            except Exception as e2:
+                logger.error(f"解析JSON失败: {e}, 二次修复也失败: {e2}")
+                logger.debug(f"原始响应前500字符: {response[:500]}...")
+                logger.debug(f"提取的JSON前300字符: {json_str[:300]}...")
+                return [], False  # 解析失败
+
+    def _merge_classified_contents(self, contents: List[ClassifiedContent], section: SectionContent) -> List[ClassifiedContent]:
+        """将同一分类下的内容按区间合并(只有连续或重叠的区间才合并)"""
+        if not contents:
+            return contents
+
+        # 按分类代码分组
+        groups: Dict[str, List[ClassifiedContent]] = {}
+        for content in contents:
+            key = content.third_category_code
+            if key not in groups:
+                groups[key] = []
+            groups[key].append(content)
+
+        merged_contents = []
+
+        for category_code, group_contents in groups.items():
+            # 按起始行号排序
+            group_contents.sort(key=lambda x: x.start_line)
+
+            # 合并连续或重叠的区间
+            merged_ranges = []
+            for content in group_contents:
+                if not merged_ranges:
+                    # 第一个区间
+                    merged_ranges.append({
+                        'start': content.start_line,
+                        'end': content.end_line
+                    })
+                else:
+                    last_range = merged_ranges[-1]
+                    # 检查是否连续或重叠(允许3行的间隔也算连续)
+                    if content.start_line <= last_range['end'] + 3:
+                        # 扩展当前区间
+                        last_range['end'] = max(last_range['end'], content.end_line)
+                    else:
+                        # 不连续,新建区间
+                        merged_ranges.append({
+                            'start': content.start_line,
+                            'end': content.end_line
+                        })
+
+            # 为每个合并后的区间创建条目
+            for range_info in merged_ranges:
+                merged_content = self._extract_content_by_line_numbers(
+                    section, range_info['start'], range_info['end']
+                )
+                merged_contents.append(ClassifiedContent(
+                    third_category_name=group_contents[0].third_category_name,
+                    third_category_code=category_code,
+                    third_seq=group_contents[0].third_seq,
+                    start_line=range_info['start'],
+                    end_line=range_info['end'],
+                    content=merged_content
+                ))
+
+        # 按起始行号排序最终结果
+        merged_contents.sort(key=lambda x: x.start_line)
+        return merged_contents
+
+    def _extract_content_by_line_numbers(self, section: SectionContent, start_line: int, end_line: int) -> str:
+        """根据全局行号从section中提取原文内容"""
+        if not section.line_number_map:
+            # 如果没有行号映射,使用相对索引
+            start_idx = max(0, start_line - 1)
+            end_idx = min(len(section.lines), end_line)
+            return '\n'.join(section.lines[start_idx:end_idx])
+
+        # 找到全局行号对应的索引
+        start_idx = -1
+        end_idx = -1
+
+        for idx, global_line_num in enumerate(section.line_number_map):
+            if global_line_num == start_line:
+                start_idx = idx
+            if global_line_num == end_line:
+                end_idx = idx
+                break
+
+        # 如果没找到精确匹配,使用近似值
+        if start_idx == -1:
+            for idx, global_line_num in enumerate(section.line_number_map):
+                if global_line_num >= start_line:
+                    start_idx = idx
+                    break
+        if end_idx == -1:
+            for idx in range(len(section.line_number_map) - 1, -1, -1):
+                if section.line_number_map[idx] <= end_line:
+                    end_idx = idx
+                    break
+
+        if start_idx == -1:
+            start_idx = 0
+        if end_idx == -1:
+            end_idx = len(section.lines) - 1
+
+        # 确保索引有效
+        start_idx = max(0, min(start_idx, len(section.lines) - 1))
+        end_idx = max(0, min(end_idx, len(section.lines) - 1))
+
+        if start_idx > end_idx:
+            start_idx, end_idx = end_idx, start_idx
+
+        # 添加行号标记返回
+        lines_with_numbers = []
+        for i in range(start_idx, end_idx + 1):
+            global_line = section.line_number_map[i] if i < len(section.line_number_map) else (i + 1)
+            lines_with_numbers.append(f"<{global_line}> {section.lines[i]}")
+
+        return '\n'.join(lines_with_numbers)
+
+    async def _call_supplement_verification(
+        self,
+        section: SectionContent,
+        std: CategoryStandard,
+        hit_lines: List[int],
+        matched_kws: List[str],
+        is_table: bool = False
+    ) -> bool:
+        """针对单个候选遗漏分类发起补充验证LLM调用,返回是否存在。"""
+        start = min(hit_lines)
+        end = max(hit_lines)
+        chunk_text = self._extract_content_by_line_numbers(section, start, end)
+
+        prompt = build_supplement_verify_prompt(std, chunk_text, start, end, hit_lines, matched_kws, is_table)
+
+        try:
+            kwargs = {
+                "model": self.model,
+                "messages": [
+                    {"role": "system", "content": SUPPLEMENT_VERIFY_SYSTEM_PROMPT},
+                    {"role": "user", "content": prompt}
+                ],
+                "temperature": 0.0,
+                "max_tokens": 10
+            }
+            if "qwen3.5" in self.model:
+                kwargs["extra_body"] = {"enable_thinking": False}
+            response = await self.client.chat.completions.create(**kwargs)
+            resp = response.choices[0].message.content or ""
+            if "不存在" in resp:
+                return False
+            if "存在" in resp:
+                return True
+            # 格式异常,保守返回 True
+            logger.warning(f"supplement_verify 格式异常: {resp[:50]}")
+            return True
+        except Exception as e:
+            logger.warning(f"supplement_verify 调用失败: {e}")
+            return True
+
+    async def _detect_and_supplement(
+        self,
+        section: SectionContent,
+        llm_results: List[ClassifiedContent]
+    ) -> List[ClassifiedContent]:
+        """扫描整个 section,补充 LLM 遗漏的三级分类。
+
+        扫描范围:当前二级分类下的所有行(不跨二级分类,由 section.category_standards 保证)。
+        触发条件:该二级分类下某个三级标准未出现在 LLM 结果中。
+        注意:同一行内容可同时属于多个三级分类,不限制"已覆盖行"。
+        """
+        if not section.category_standards or not section.lines:
+            return []
+
+        # 已命中的有效分类(排除 no_standard)
+        found_codes = {c.third_category_code for c in llm_results if c.third_category_code != 'no_standard'}
+
+        # 判断整个 section 是否含表格特征
+        full_text = ' '.join(section.lines)
+        is_table = (
+            any(kw in full_text for kw in ['序号', '作业活动', '风险源', '防范措施'])
+            or full_text.count('|') > 5
+        )
+
+        supplemented = []
+        for std in section.category_standards:
+            if std.third_code in found_codes or not std.keywords:
+                continue
+
+            keywords = [k.strip() for k in std.keywords.split(';') if k.strip()]
+
+            if is_table:
+                # 表格路径:整个 section 行范围提交 LLM 验证
+                if not section.line_number_map:
+                    continue
+                hit_lines = [section.line_number_map[0], section.line_number_map[-1]]
+                confirmed = await self._call_supplement_verification(section, std, hit_lines, [], is_table=True)
+            else:
+                # 普通路径:扫描整个 section 所有行的关键字
+                hit_lines, matched_kws = [], []
+                for i, line_text in enumerate(section.lines):
+                    line_num = section.line_number_map[i] if section.line_number_map else (i + 1)
+                    for kw in keywords:
+                        if kw in line_text and line_num not in hit_lines:
+                            hit_lines.append(line_num)
+                            if kw not in matched_kws:
+                                matched_kws.append(kw)
+                if not hit_lines:
+                    continue
+                confirmed = await self._call_supplement_verification(section, std, hit_lines, matched_kws)
+
+            if confirmed:
+                start, end = min(hit_lines), max(hit_lines)
+                content = self._extract_content_by_line_numbers(section, start, end)
+                supplemented.append(ClassifiedContent(
+                    third_category_name=std.third_name,
+                    third_category_code=std.third_code,
+                    third_seq=std.third_seq,
+                    start_line=start,
+                    end_line=end,
+                    content=content
+                ))
+
+        return supplemented
+
+
+    def _fix_json(self, json_str: str) -> str:
+        return _fix_json(json_str)
+
+    def _aggressive_json_fix(self, json_str: str) -> str:
+        return _aggressive_json_fix(json_str)

+ 157 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/embedding_client.py

@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+Embedding 客户端
+"""
+
+import math
+import re
+from typing import List, Optional, Tuple
+
+from openai import AsyncOpenAI
+
+from .config import EMBEDDING_API_KEY, EMBEDDING_BASE_URL, EMBEDDING_MODEL, EMBEDDING_SIMILARITY_THRESHOLD
+from foundation.observability.logger.loggering import review_logger as logger
+
+
+class EmbeddingClient:
+    """Embedding模型客户端,用于计算文本相似度"""
+
+    def __init__(self):
+        self.client = AsyncOpenAI(
+            api_key=EMBEDDING_API_KEY,
+            base_url=EMBEDDING_BASE_URL
+        )
+        self.model = EMBEDDING_MODEL
+
+    async def get_embedding(self, text: str) -> Optional[List[float]]:
+        """获取文本的embedding向量"""
+        try:
+            response = await self.client.embeddings.create(
+                model=self.model,
+                input=text
+            )
+            if response.data and len(response.data) > 0:
+                return response.data[0].embedding
+            return None
+        except Exception as e:
+            logger.error(f"Embedding API调用失败: {e}")
+            return None
+
+    async def get_embeddings_batch(self, texts: List[str]) -> List[Optional[List[float]]]:
+        """批量获取文本的embedding向量"""
+        try:
+            response = await self.client.embeddings.create(
+                model=self.model,
+                input=texts
+            )
+            results = []
+            for item in response.data:
+                results.append(item.embedding)
+            return results
+        except Exception as e:
+            logger.error(f"Embedding API批量调用失败: {e}")
+            return [None] * len(texts)
+
+    def cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
+        """计算两个向量的余弦相似度"""
+        if not vec1 or not vec2 or len(vec1) != len(vec2):
+            return 0.0
+
+        dot_product = sum(a * b for a, b in zip(vec1, vec2))
+        norm1 = math.sqrt(sum(a * a for a in vec1))
+        norm2 = math.sqrt(sum(b * b for b in vec2))
+
+        if norm1 == 0 or norm2 == 0:
+            return 0.0
+
+        return dot_product / (norm1 * norm2)
+
+    def _clean_section_name(self, section_name: str) -> str:
+        """清理section名称,去除序号等前缀
+
+        例如:
+        - "一)编制依据" -> "编制依据"
+        - "二) 技术保证措施" -> "技术保证措施"
+        - "1. 施工计划" -> "施工计划"
+        - "(1) 工艺流程" -> "工艺流程"
+        """
+        cleaned = section_name.strip()
+
+        # 去除开头的序号模式:
+        # 1. 中文数字+)或中文数字+、 如 "一)"、"二、"
+        # 2. 阿拉伯数字+. 或阿拉伯数字+)如 "1.", "2)"
+        # 3. 括号数字如 "(1)", "(一)"
+        patterns = [
+            r'^[一二三四五六七八九十百千]+[)\\)、\\.\\s]+',  # 中文数字+标点
+            r'^\\d+[\\.\\)\\)、\\s]+',  # 阿拉伯数字+标点
+            r'^[((]\\d+[))][\\s\\.]*',  # 括号数字
+            r'^[((][一二三四五六七八九十][))][\\s\\.]*',  # 括号中文数字
+        ]
+
+        for pattern in patterns:
+            cleaned = re.sub(pattern, '', cleaned)
+
+        return cleaned.strip()
+
+    async def check_similarity(
+        self,
+        section_name: str,
+        section_content: str,
+        second_category_name: str,
+        second_category_raw_content: str = ""
+    ) -> Tuple[bool, float]:
+        """
+        检查待审查内容与二级分类标准的相似度
+
+        比较:
+        - 左侧: section的实际内容(待审查的施工方案内容)
+        - 右侧: second_raw_content(来自construction_plan_standards.csv的标准定义)
+
+        返回: (is_similar, similarity_score)
+        - is_similar: 是否相似(相似度 > 阈值 或标题完全匹配)
+        - similarity_score: 相似度分数 (0-1)
+        """
+        # 步骤1: 先判断标题是否匹配
+        # 清理文本进行比较(去除序号等前缀)
+        cleaned_section_name = self._clean_section_name(section_name).lower()
+        cleaned_second_name = second_category_name.strip().lower()
+
+        # 标题直接相等检查(清理后的)
+        if cleaned_section_name == cleaned_second_name:
+            # 标题匹配,继续用embedding比较内容相似度
+            pass
+        else:
+            # 标题不匹配,检查是否包含关系
+            if cleaned_second_name in cleaned_section_name or cleaned_section_name in cleaned_second_name:
+                # 要求包含的部分至少4个字符,避免短词误判
+                if len(cleaned_second_name) >= 4 or len(cleaned_section_name) >= 4:
+                    # 标题部分匹配,继续用embedding比较内容
+                    pass
+                else:
+                    # 标题不匹配且太短,直接返回不相似
+                    return False, 0.0
+            else:
+                # 标题完全不匹配,直接返回不相似
+                return False, 0.0
+
+        # 步骤2: 使用embedding计算内容相似度
+        # 左侧: section的实际内容(待审查的施工方案实际内容)
+        # 右侧: second_raw_content(该second_name的标准定义)
+        section_text = section_content[:800]  # 取前800字符的实际内容
+        category_text = second_category_raw_content[:800] if second_category_raw_content else second_category_name
+
+        # 获取embedding
+        embeddings = await self.get_embeddings_batch([section_text, category_text])
+
+        if embeddings[0] is None or embeddings[1] is None:
+            # embedding获取失败,保守起见返回不相似
+            return False, 0.0
+
+        # 计算相似度
+        similarity = self.cosine_similarity(embeddings[0], embeddings[1])
+
+        # 判断结果
+        is_similar = similarity >= EMBEDDING_SIMILARITY_THRESHOLD
+
+        return is_similar, similarity

+ 146 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/json_utils.py

@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+JSON 修复工具函数
+"""
+
+import json
+import re
+
+
+def _fix_json(json_str: str) -> str:
+    """修复常见的JSON格式问题"""
+    # 去除尾部多余的逗号
+    json_str = re.sub(r',(\s*[}\]])', r'\1', json_str)
+
+    # 确保 JSON 结构闭合
+    json_str = _ensure_json_closed(json_str)
+
+    # 替换单引号为双引号(但要小心内容中的单引号)
+    # 使用更精确的方法:先尝试解析,失败再替换
+    try:
+        json.loads(json_str)
+        return json_str
+    except Exception:
+        # 尝试替换单引号
+        json_str = json_str.replace("'", '"')
+
+    return json_str
+
+
+def _truncate_to_valid_json(json_str: str) -> str:
+    """将截断的JSON截断到最后一个完整对象的位置,并保留数组结构"""
+    # 找到 "classified_contents" 数组的开始
+    array_start = json_str.find('"classified_contents"')
+    if array_start == -1:
+        return json_str
+
+    # 找到数组的 '['
+    bracket_start = json_str.find('[', array_start)
+    if bracket_start == -1:
+        return json_str
+
+    # 遍历数组,找到最后一个完整的对象
+    brace_count = 0
+    bracket_count = 1  # 已经进入数组,所以是1
+    in_string = False
+    escape_next = False
+    last_valid_obj_end = 0
+    i = bracket_start + 1
+
+    while i < len(json_str):
+        char = json_str[i]
+
+        if escape_next:
+            escape_next = False
+            i += 1
+            continue
+
+        if char == '\\':
+            escape_next = True
+            i += 1
+            continue
+
+        if char == '"' and not escape_next:
+            in_string = not in_string
+            i += 1
+            continue
+
+        if not in_string:
+            if char == '{':
+                brace_count += 1
+            elif char == '}':
+                brace_count -= 1
+                if brace_count == 0:
+                    # 找到一个完整的对象
+                    last_valid_obj_end = i
+            elif char == '[':
+                bracket_count += 1
+            elif char == ']':
+                bracket_count -= 1
+                if bracket_count == 0:
+                    # 数组正常闭合,不需要截断
+                    return json_str
+
+        i += 1
+
+    if last_valid_obj_end > 0:
+        # 截断到最后一个完整对象的位置,并关闭数组
+        return json_str[:last_valid_obj_end + 1] + ']'
+
+    return json_str
+
+
+def _ensure_json_closed(json_str: str) -> str:
+    """确保JSON结构闭合"""
+    # 计算未闭合的括号
+    brace_count = 0
+    bracket_count = 0
+    in_string = False
+    escape_next = False
+
+    for char in json_str:
+        if escape_next:
+            escape_next = False
+            continue
+        if char == '\\':
+            escape_next = True
+            continue
+        if char == '"' and not escape_next:
+            in_string = not in_string
+            continue
+        if not in_string:
+            if char == '{':
+                brace_count += 1
+            elif char == '}':
+                brace_count -= 1
+            elif char == '[':
+                bracket_count += 1
+            elif char == ']':
+                bracket_count -= 1
+
+    # 添加闭合括号
+    result = json_str
+    # 先去掉尾部可能的逗号
+    result = result.rstrip().rstrip(',').rstrip()
+
+    # 关闭对象
+    while brace_count > 0:
+        result += '}'
+        brace_count -= 1
+
+    # 关闭数组
+    while bracket_count > 0:
+        result += ']'
+        bracket_count -= 1
+
+    return result
+
+
+def _aggressive_json_fix(json_str: str) -> str:
+    """激进的JSON修复,用于处理复杂情况"""
+    # 首先尝试截断到最后一个完整对象
+    json_str = _truncate_to_valid_json(json_str)
+    # 然后确保结构闭合
+    json_str = _ensure_json_closed(json_str)
+    return json_str

+ 352 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/main_classifier.py

@@ -0,0 +1,352 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+LLMContentClassifier 主入口类 + 便捷函数
+"""
+
+import asyncio
+import json
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple
+
+from openai import AsyncOpenAI
+
+from .models import ClassificationResult, SectionContent
+from .config import ClassifierConfig
+from .category_loaders import CategoryStandardLoader, SecondCategoryStandardLoader
+from .embedding_client import EmbeddingClient
+from .content_classifier import ContentClassifierClient
+from .chunks_converter import ChunksConverter
+from foundation.observability.logger.loggering import review_logger as logger
+
+
+class LLMContentClassifier:
+    """
+    LLM 内容三级分类器(主入口类)
+
+    封装完整的分类流程,提供简洁的接口供外部调用
+    """
+
+    def __init__(self, config: Optional[ClassifierConfig] = None):
+        """
+        初始化分类器
+
+        Args:
+            config: 配置对象,如果为 None 则使用默认配置
+        """
+        self.config = config or ClassifierConfig()
+
+        # 加载标准分类
+        self.category_loader = CategoryStandardLoader(Path(self.config.category_table_path))
+
+        # 加载二级分类标准(如果存在)
+        self.second_category_loader = None
+        if Path(self.config.second_category_path).exists():
+            self.second_category_loader = SecondCategoryStandardLoader(Path(self.config.second_category_path))
+
+        # 创建转换器
+        self.converter = ChunksConverter(self.category_loader)
+
+        # 并发控制信号量
+        self.semaphore = asyncio.Semaphore(self.config.max_concurrent_requests)
+
+        # Embedding 客户端(可选)
+        self.embedding_client = None
+        if self.config.embedding_base_url:
+            self.embedding_client = self._create_embedding_client()
+
+    def _create_embedding_client(self) -> EmbeddingClient:
+        """创建 Embedding 客户端"""
+        client = EmbeddingClient()
+        # 使用配置覆盖默认值
+        client.client = AsyncOpenAI(
+            api_key=self.config.embedding_api_key,
+            base_url=self.config.embedding_base_url
+        )
+        client.model = self.config.embedding_model
+        return client
+
+    async def classify_chunks(
+        self,
+        chunks: List[Dict[str, Any]],
+        progress_callback: Optional[callable] = None
+    ) -> List[Dict[str, Any]]:
+        """
+        对 chunks 进行三级分类
+
+        Args:
+            chunks: 文档分块列表,每个 chunk 需包含:
+                - chapter_classification: 一级分类代码
+                - secondary_category_code: 二级分类代码
+                - secondary_category_cn: 二级分类中文名
+                - review_chunk_content 或 content: 内容文本
+            progress_callback: 进度回调函数 (completed, total, section_name, success) -> None,支持 async
+
+        Returns:
+            List[Dict]: 更新后的 chunks 列表,每个 chunk 新增字段:
+                - tertiary_category_code: 三级分类代码
+                - tertiary_category_cn: 三级分类名称
+                - tertiary_classification_details: 行级分类详情列表
+        """
+        logger.info(f"正在对 {len(chunks)} 个内容块进行三级分类...")
+
+        # 步骤1: 将 chunks 转换为 SectionContent 列表
+        sections = self.converter.chunks_to_sections(chunks)
+        logger.info(f"按二级标题分组后得到 {len(sections)} 个段落")
+
+        if not sections:
+            logger.info("没有有效的段落需要分类")
+            return chunks
+
+        # 步骤2: 创建分类客户端
+        classifier = ContentClassifierClient(
+            model=self.config.model,
+            semaphore=self.semaphore,
+            embedding_client=self.embedding_client,
+            second_category_loader=self.second_category_loader
+        )
+
+        # 步骤3: 并发分类所有段落
+        results_map: Dict[str, ClassificationResult] = {}
+
+        async def classify_with_progress(section: SectionContent, idx: int, total: int):
+            result = await classifier.classify_content(section)
+            results_map[section.section_key] = result
+
+            if progress_callback:
+                ret = progress_callback(idx + 1, total, section.section_name, not result.error)
+                if asyncio.iscoroutine(ret):
+                    await ret
+            else:
+                status = "成功" if not result.error else f"失败: {result.error[:30]}"
+                logger.debug(f"[{idx + 1}/{total}] {section.section_name}: {status}")
+
+            return result
+
+        tasks = [
+            classify_with_progress(section, idx, len(sections))
+            for idx, section in enumerate(sections)
+        ]
+        await asyncio.gather(*tasks)
+
+        # 步骤4: 将分类结果转换回 chunks 格式,按 chunk_ranges 过滤确保每个 chunk 只拿自己行范围内的详情
+        updated_chunks = []
+
+        # 建立 chunk_id -> (section_key, g_start, g_end) 映射,来自 sections 的 chunk_ranges
+        chunk_range_map: Dict[str, Tuple[str, int, int]] = {}
+        for section in sections:
+            for (cid, g_start, g_end) in section.chunk_ranges:
+                chunk_range_map[cid] = (section.section_key, g_start, g_end)
+
+        # 为每个原始 chunk 单独分配其行范围内的分类详情
+        for chunk in chunks:
+            updated_chunk = dict(chunk)
+            first_code = chunk.get("chapter_classification", "") or chunk.get("first_code", "")
+            second_code = chunk.get("secondary_category_code", "") or chunk.get("second_code", "")
+
+            # 从 chunk_range_map 获取该 chunk 的行范围(同时拿到正确的 section_key)
+            chunk_id = chunk.get("chunk_id") or chunk.get("id") or str(id(chunk))
+            range_info = chunk_range_map.get(chunk_id)
+
+            if range_info:
+                # 优先使用 chunk_range_map 中记录的 section_key(经过名称匹配的正确 key)
+                section_key = range_info[0]
+            else:
+                # 降级:从 chunk 字段重建(可能在 second_code="none" 时查不到)
+                section_key = f"{first_code}->{second_code}"
+
+            result = results_map.get(section_key)
+
+            if result:
+                updated_chunk["first_code"] = first_code
+                updated_chunk["second_code"] = second_code
+
+                # 收集全部有效三级分类(非 no_standard)
+                all_tertiary = [
+                    {
+                        "third_category_name": c.third_category_name,
+                        "third_category_code": c.third_category_code,
+                        "third_seq": c.third_seq,
+                        "start_line": c.start_line,
+                        "end_line": c.end_line,
+                        "content": c.content
+                    }
+                    for c in result.classified_contents
+                    if c.third_category_code != "no_standard"
+                ]
+
+                if range_info:
+                    # 过滤:只保留与该 chunk 行范围有交集的详情
+                    _, g_start, g_end = range_info
+                    filtered = [
+                        t for t in all_tertiary
+                        if t["start_line"] <= g_end and t["end_line"] >= g_start
+                    ]
+                else:
+                    # 无法定位行范围(可能是单 chunk 分组),保留全部
+                    filtered = all_tertiary
+
+                # 去重:按 (third_category_code, start_line, end_line) 三元组去重
+                seen = set()
+                deduped = []
+                for t in filtered:
+                    key = (t["third_category_code"], t["start_line"], t["end_line"])
+                    if key not in seen:
+                        seen.add(key)
+                        deduped.append(t)
+                updated_chunk["tertiary_classification_details"] = deduped
+
+                # 向后兼容:设置第一个三级分类为主分类
+                tertiary_details = updated_chunk["tertiary_classification_details"]
+                if tertiary_details:
+                    updated_chunk["tertiary_category_code"] = tertiary_details[0]["third_category_code"]
+                    updated_chunk["tertiary_category_cn"] = tertiary_details[0]["third_category_name"]
+
+            updated_chunks.append(updated_chunk)
+
+        logger.info(f"三级分类完成!共处理 {len(updated_chunks)} 个 chunks")
+        return updated_chunks
+
+
+# ==================== 便捷函数 ====================
+
+async def classify_chunks(
+    chunks: List[Dict[str, Any]],
+    config: Optional[ClassifierConfig] = None,
+    progress_callback: Optional[callable] = None
+) -> List[Dict[str, Any]]:
+    """
+    对 chunks 进行三级分类的便捷函数
+
+    Args:
+        chunks: 文档分块列表
+        config: 配置对象(可选)
+        progress_callback: 进度回调函数
+
+    Returns:
+        List[Dict]: 更新后的 chunks 列表
+
+    使用示例:
+        from llm_content_classifier_v2 import classify_chunks
+
+        # 使用默认配置
+        updated_chunks = await classify_chunks(chunks)
+
+        # 使用自定义配置
+        config = ClassifierConfig(
+            model="qwen3.5-122b-a10b",
+            embedding_similarity_threshold=0.85
+        )
+        updated_chunks = await classify_chunks(chunks, config=config)
+    """
+    classifier = LLMContentClassifier(config)
+    return await classifier.classify_chunks(chunks, progress_callback)
+
+
+def classify_chunks_sync(
+    chunks: List[Dict[str, Any]],
+    config: Optional[ClassifierConfig] = None
+) -> List[Dict[str, Any]]:
+    """
+    同步版本的分类函数(阻塞调用)
+
+    Args:
+        chunks: 文档分块列表
+        config: 配置对象(可选)
+
+    Returns:
+        List[Dict]: 更新后的 chunks 列表
+    """
+    try:
+        loop = asyncio.get_running_loop()
+    except RuntimeError:
+        # 没有运行中的事件循环
+        return asyncio.run(classify_chunks(chunks, config))
+
+    # 已有事件循环,创建任务
+    import concurrent.futures
+    with concurrent.futures.ThreadPoolExecutor() as executor:
+        future = executor.submit(
+            asyncio.run,
+            classify_chunks(chunks, config)
+        )
+        return future.result()
+
+
+# ==================== 快速测试入口 ====================
+
+if __name__ == "__main__":
+    import io
+    import sys
+    from datetime import datetime
+
+    # 修复 Windows 终端 UTF-8 输出
+    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
+
+    TEST_JSON_PATH = Path(r"temp\construction_review\final_result\4148f6019f89e061b15679666f646893-1773993108.json")
+    OUTPUT_DIR = Path(r"temp\construction_review\llm_content_classifier_v2")
+    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
+
+    def _sep(title: str = "", width: int = 70):
+        print(f"\n{'=' * width}\n  {title}\n{'=' * width}" if title else "─" * width)
+
+    def _load_chunks_from_json(json_path: Path) -> List[Dict[str, Any]]:
+        with open(json_path, encoding="utf-8") as f:
+            data = json.load(f)
+        if "document_result" in data:
+            return data["document_result"]["structured_content"]["chunks"]
+        return data["data"]["document_result"]["structured_content"]["chunks"]
+
+    # ── 加载数据 ──────────────────────────────────────────────
+    _sep("加载测试数据")
+    if not TEST_JSON_PATH.exists():
+        print(f"[ERROR] 文件不存在: {TEST_JSON_PATH}")
+        sys.exit(1)
+
+    raw_chunks = _load_chunks_from_json(TEST_JSON_PATH)
+    print(f"原始 chunks 数: {len(raw_chunks)}")
+
+    # ── 运行完整分类流程 ───────────────────────────────────────
+    _sep("运行三级分类(LLMContentClassifier)")
+    config = ClassifierConfig()
+    print(f"模型: {config.model}")
+    print(f"Embedding 模型: {config.embedding_model}")
+    print(f"相似度阈值: {config.embedding_similarity_threshold}")
+
+    classifier = LLMContentClassifier(config)
+    updated_chunks = asyncio.run(classifier.classify_chunks(raw_chunks))
+
+    # ── 保存结果 ──────────────────────────────────────────────
+    _sep("保存结果")
+    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
+    result_file = OUTPUT_DIR / f"result_{ts}.json"
+    with open(result_file, "w", encoding="utf-8") as f:
+        json.dump(updated_chunks, f, ensure_ascii=False, indent=2)
+    print(f"完整结果已保存: {result_file}")
+
+    # ── 控制台汇总展示 ────────────────────────────────────────
+    _sep("分类结果汇总")
+
+    # 按 section_label 聚合三级分类详情
+    section_map: Dict[str, List[Dict]] = {}
+    for chunk in updated_chunks:
+        label = chunk.get("section_label") or chunk.get("chunk_id", "unknown")
+        details = chunk.get("tertiary_classification_details", [])
+        if label not in section_map:
+            section_map[label] = []
+        for d in details:
+            key = d["third_category_code"]
+            if not any(x["third_category_code"] == key for x in section_map[label]):
+                section_map[label].append(d)
+
+    total_third = 0
+    for label, details in section_map.items():
+        print(f"\n[{label}]  三级分类数={len(details)}")
+        for d in details:
+            line_range = f"L{d.get('start_line', '?')}-{d.get('end_line', '?')}"
+            preview = (d.get("content") or "")[:50].replace("\n", " ")
+            print(f"  ├ {d['third_category_name']}({d['third_category_code']})  {line_range}  {preview}...")
+        total_third += len(details)
+
+    _sep()
+    print(f"处理 chunks: {len(updated_chunks)}  |  识别三级分类: {total_third}  |  结果目录: {OUTPUT_DIR}")

+ 71 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/models.py

@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+数据模型定义
+"""
+
+from typing import List, Optional, Tuple
+from dataclasses import dataclass, field
+
+
+@dataclass
+class CategoryStandard:
+    """标准分类定义"""
+    first_code: str
+    first_name: str
+    first_seq: int  # 一级序号
+    second_code: str
+    second_name: str
+    second_seq: int  # 二级序号
+    second_focus: str  # 二级分类关注点
+    third_code: str
+    third_name: str
+    third_seq: int  # 三级序号
+    third_focus: str
+    keywords: str = ""
+
+
+@dataclass
+class SecondCategoryStandard:
+    """二级分类标准定义(来自construction_plan_standards.csv)"""
+    first_name: str  # 一级分类中文名
+    second_name: str  # 二级分类中文名
+    second_raw_content: str  # 二级分类详细描述
+
+
+@dataclass
+class ClassifiedContent:
+    """分类结果"""
+    third_category_name: str  # 三级分类名称
+    third_category_code: str  # 三级分类代码
+    third_seq: int  # 三级序号
+    start_line: int
+    end_line: int
+    content: str  # 原文内容
+
+
+@dataclass
+class SectionContent:
+    """二级标题内容"""
+    section_key: str  # 如 "第一章->一"
+    section_name: str  # 如 "一)编制依据"
+    lines: List[str]  # 原始行列表
+    numbered_content: str  # 带行号的内容
+    category_standards: List[CategoryStandard] = field(default_factory=list)  # 该二级分类下的三级标准
+    line_number_map: List[int] = field(default_factory=list)  # 每行对应的全局行号(如果有)
+    chunk_ranges: List[Tuple[str, int, int]] = field(default_factory=list)  # [(chunk_id, global_start, global_end), ...]
+
+
+@dataclass
+class ClassificationResult:
+    """分类结果"""
+    model: str
+    section_key: str
+    section_name: str
+    classified_contents: List[ClassifiedContent]
+    latency: float
+    raw_response: str = ""
+    error: Optional[str] = None
+    total_lines: int = 0  # 该section的总行数
+    classified_lines: int = 0  # 已分类的行数
+    coverage_rate: float = 0.0  # 分类率(已分类行数/总行数)

+ 360 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/prompt.py

@@ -0,0 +1,360 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+提示词模板集中管理
+
+所有对外 LLM 调用的 system_prompt 和 user_prompt 均在此定义。
+
+================================================================================
+使用场景总览
+================================================================================
+
+1. 主分类调用 (CLASSIFY_SYSTEM_PROMPT + build_classify_prompt)
+   - 调用位置: ContentClassifierClient.classify_content() → _classify_single_chunk() → _call_api()
+   - 触发时机: 对二级分类下的文档内容进行三级分类识别时
+   - 输入: SectionContent(包含二级分类标准、带行号的文档内容)
+   - 输出: JSON 格式的分类结果列表(category_index 为数字索引)
+   - 特点:
+     * 支持长内容分块处理(超过150行自动切分)
+     * 使用数字索引避免模型输出复杂代码字符串
+     * 包含详细的分类指南和示例
+
+2. JSON 修复调用 (build_fix_prompt)
+   - 调用位置: ContentClassifierClient._parse_with_fix()
+   - 触发时机: 主分类返回的 JSON 解析失败时(最多重试3次)
+   - 输入: 原始模型输出(格式错误的 JSON 字符串)
+   - 输出: 修复后的合法 JSON
+   - 特点: 严格保持业务数据完整性,仅修复语法错误
+
+3. 补充验证调用 (SUPPLEMENT_VERIFY_SYSTEM_PROMPT + build_supplement_verify_prompt)
+   - 调用位置: ContentClassifierClient._detect_and_supplement() → _call_supplement_verification()
+   - 触发时机:
+     * 主分类完成后,扫描发现某些三级分类可能遗漏时
+     * 通过 keywords 字段匹配到未覆盖行时
+     * 表格内容需要二次确认时
+   - 输入: 待确认的 CategoryStandard + 相关文本片段
+   - 输出: "存在" 或 "不存在"
+   - 特点: 轻量级验证,max_tokens=10,temperature=0.0
+
+================================================================================
+调用流程图
+================================================================================
+
+classify_content()
+    ├── Embedding 相似度检查(可选跳过)
+    ├── _classify_single_chunk()
+    │       ├── build_classify_prompt() ──→ LLM 主分类
+    │       └── _parse_with_fix()
+    │               └── build_fix_prompt() ──→ LLM 修复 JSON(失败时)
+    ├── _detect_and_supplement()
+    │       └── build_supplement_verify_prompt() ──→ LLM 确认遗漏(逐个标准)
+    └── 合并结果并返回
+"""
+
+from typing import List
+
+from .models import CategoryStandard, SectionContent
+
+
+# ================================================================================
+# 主分类调用
+# ================================================================================
+
+# 使用场景:ContentClassifierClient._call_api() 中的 system 消息
+# 作用:定义模型角色为"施工方案文档分析专家",约束输出格式和行为
+CLASSIFY_SYSTEM_PROMPT = """你是专业的施工方案文档分析专家。你的任务是:
+1. 仔细阅读文档内容,理解每行的语义
+2. 将内容归类到给定的三级分类标准中
+3. 【重要】优先使用标准分类,只有完全不符合时才使用索引0(非标准项)
+4. 【重要】连续相同分类的多行必须合并为一个条目
+5. 【重要】当一行同时提及多个主体或类别(如"勘察、设计和监测单位"),必须为每个主体单独输出一条条目,行号相同
+6. 【重要】输出格式:category_index必须是纯数字(0,1,2...),禁止输出文本名称或代码
+7. 必须在给定的三级分类标准范围内分类,禁止创造新的分类
+8. 只输出JSON格式结果,不要任何解释文字"""
+
+
+def build_classify_prompt(section: SectionContent, is_chunk: bool = False) -> str:
+    """
+    构建主分类的 user prompt。
+
+    使用场景:
+        ContentClassifierClient._classify_single_chunk() 中调用,
+        将 SectionContent 转换为 LLM 可理解的分类任务描述。
+
+    参数说明:
+        section: 包含二级分类标准、文档内容和行号映射的段落对象
+        is_chunk: 是否为分块处理(长文档会被切分成多个 chunk 依次处理)
+
+    输出格式:
+        完整的 user prompt 字符串,包含:
+        - 当前文档位置(一级/二级分类信息)
+        - 三级分类标准列表(带数字索引)
+        - 带行号的文档内容
+        - 分类任务指南(核心原则、示例、行号规则、多主体拆分规则)
+        - 索引映射表(供后处理转换使用)
+        - 强制约束(category_index 必须是数字)
+
+    注意事项:
+        - 内容超过 12000 字符会自动截断,并添加截断提示
+        - 分块处理时会添加 chunk_hint 提示模型当前是文档的一部分
+        - 使用数字索引(1-N)而非分类代码,避免模型输出错误
+    """
+    # 获取二级分类信息
+    second_code = ""
+    second_name = section.section_name
+    first_code = ""
+    first_name = ""
+
+    if section.category_standards:
+        first_code = section.category_standards[0].first_code
+        first_name = section.category_standards[0].first_name
+        second_code = section.category_standards[0].second_code
+
+    # 构建三级分类标准描述(使用数字索引,模型只需输出索引号)
+    standards_desc = []
+    for i, std in enumerate(section.category_standards, 1):
+        focus_content = std.third_focus if std.third_focus else "(无具体关注要点)"
+        standards_desc.append(
+            f"{i}. {std.third_name}\n"
+            f"   【识别要点】{focus_content}"
+        )
+
+    # 添加非标准项作为兜底分类(索引0)
+    standards_desc.insert(0, "0. 非标准项\n   【识别要点】仅当内容完全不符合以上任何分类标准时使用,如页眉页脚、纯表格分隔线、无关的广告语等")
+
+    standards_text = '\n\n'.join(standards_desc) if standards_desc else "无具体标准,请根据内容自行判断"
+
+    # 构建索引映射表(用于后处理转换)
+    index_mapping_lines = ["0 -> 非标准项 (no_standard)"]
+    for i, std in enumerate(section.category_standards, 1):
+        index_mapping_lines.append(f"{i} -> {std.third_name} ({std.third_code})")
+    index_mapping_text = "\n".join(index_mapping_lines)
+
+    # 计算内容长度和分段提示
+    max_content_length = 12000
+    content_to_use = section.numbered_content[:max_content_length]
+    is_truncated = len(section.numbered_content) > max_content_length
+
+    if is_chunk and section.line_number_map:
+        chunk_hint = (
+            f"\n【注意】这是文档的一个分块(行号 {section.line_number_map[0]}~{section.line_number_map[-1]}),"
+            f"请对有实质内容的行进行分类,空行和纯符号行无需单独输出。\n"
+        )
+    elif is_chunk:
+        chunk_hint = "\n【注意】这是文档的一个分块,请对有实质内容的行进行分类。\n"
+    else:
+        chunk_hint = ""
+
+    truncation_hint = (
+        f"\n【提示】内容较长已截断,当前显示前{max_content_length}字符,请对显示的内容进行完整分类。\n"
+        if is_truncated else ""
+    )
+
+    line_start = section.line_number_map[0] if section.line_number_map else 1
+    line_end = section.line_number_map[-1] if section.line_number_map else len(section.lines)
+
+    return f"""你是一个专业的施工方案文档分析专家。请根据给定的三级分类标准,识别文档内容中属于各个三级分类的部分。{chunk_hint}{truncation_hint}
+
+## 当前文档位置
+- 一级分类: {first_name} ({first_code})
+- 二级分类: {second_name} ({second_code})
+
+## 三级分类标准(共{len(section.category_standards)}个,必须在此范围内分类)
+
+{standards_text}
+
+---
+
+## 文档内容(每行以<行号>开头,共{len(section.lines)}行)
+```
+{content_to_use}
+```
+
+---
+
+## 分类任务指南
+
+### 核心原则(按优先级排序)
+1. **优先匹配标准分类**:首先判断内容是否符合上述任何一个三级分类标准
+2. **关键词匹配**:内容中出现与分类名称相关的关键词时,应归类到该分类
+3. **语义相关**:即使没有精确关键词,只要语义相关,也应归类
+4. **非标准项谨慎使用**:只有当内容完全不符合任何标准分类时,才使用"非标准项"
+
+### 分类示例
+- 看到"验收内容"、"验收标准"、"验收程序"等内容 → 归类到对应的三级分类
+- 看到"检验方法"、"检查内容"等 → 可能属于"检查要求"或"验收内容"
+- 看到"材料"、"钢筋"、"混凝土"等 → 关注上下文判断所属三级分类
+
+### 行号处理规则
+- **必须合并连续行**:连续多行属于同一分类时,合并为一个条目(start_line为起始,end_line为结束)
+- **禁止逐行输出**:不要为每一行单独创建条目
+- **允许重复分类**:同一行内容可以同时属于多个三级分类
+
+### 多主体句拆分规则(重要)
+- 当一行内容同时提及多个不同主体或类别时,**必须为每个主体单独输出一条分类条目,行号相同**
+- 示例:`"3、有关勘察、设计和监测单位项目技术负责人"` 同时涉及设计单位和监测单位,应输出:
+  - `{{"third_category_code": "DesignUnitXxx", "start_line": N, "end_line": N}}`
+  - `{{"third_category_code": "MonitoringUnitXxx", "start_line": N, "end_line": N}}`
+- 示例:`"总承包单位和分包单位技术负责人"` 同时涉及施工单位,应归入施工单位对应分类
+- 凡是"A、B和C单位"句式,需逐一判断每个主体能否对应某个三级分类
+
+### 自查清单
+- [ ] 是否优先使用了标准分类而非"非标准项"?
+- [ ] 连续相同分类的行是否已合并?
+- [ ] 分类名称是否与标准列表完全一致?
+- [ ] 包含多个主体的行是否已拆分为多条输出?
+
+## 索引映射表(用于后处理转换,你只需输出索引号)
+{index_mapping_text}
+
+## 输出格式(严格JSON,不要任何其他文字)
+```{{
+    "classified_contents_list": [
+        {{
+            "category_index": 数字索引号,
+            "start_line": 起始行号,
+            "end_line": 结束行号
+        }}
+    ]
+}}
+```
+
+## 强制约束
+1. **category_index 必须是数字**(0, 1, 2, 3...),对应上述索引映射表
+2. 0 表示非标准项,1-{len(section.category_standards)} 对应各个三级分类
+3. **禁止输出文本名称或代码**,只输出数字索引
+4. 行号范围: {line_start} - {line_end}
+5. 只输出JSON,禁止任何解释文字"""
+
+
+# ================================================================================
+# JSON 修复调用
+# ================================================================================
+
+def build_fix_prompt(original_response: str) -> str:
+    """
+    构建 JSON 格式修复的 user prompt。
+
+    使用场景:
+        ContentClassifierClient._parse_with_fix() 中调用,
+        当主分类返回的 JSON 解析失败时,请求模型修复格式错误。
+
+    参数说明:
+        original_response: 原始模型输出,包含 JSON 格式错误(如缺少逗号、
+                          括号不匹配、引号问题等)
+
+    修复策略:
+        1. 严格保持原始数据的完整性和内容,不修改业务数据
+        2. 仅修复 JSON 语法错误(逗号、括号、引号等)
+        3. 确保输出合法的 JSON 格式
+        4. 强制 category_index 为数字索引
+
+    注意事项:
+        - 最多截取前 6000 字符进行修复(避免超出上下文限制)
+        - 如果原始内容被截断,修复已提供的部分即可
+        - 这是自动重试机制的一部分,最多重试 3 次
+    """
+    return f"""你之前的输出存在JSON格式错误,请修复以下内容为正确的JSON格式。
+
+## 修复要求
+1. 严格保持原始数据的完整性和内容,不要修改任何业务数据
+2. 只修复JSON语法错误(如缺少逗号、括号不匹配、引号问题等)
+3. 确保输出的是合法的JSON格式
+4. 【重要】category_index 必须是数字索引(0, 1, 2...),禁止输出文本名称或代码
+5. 输出必须严格符合以下结构:
+{{
+    "classified_contents_list": [
+        {{
+            "category_index": 数字索引号,
+            "start_line": 数字,
+            "end_line": 数字
+        }}
+    ]
+}}
+
+## 原始输出(需要修复的内容)
+```
+{original_response[:6000]}
+```
+
+注意:
+- 只输出JSON,不要任何解释文字
+- 如果原始内容被截断,修复已提供的部分即可
+- category_index 只能是数字,如 0(非标准项)、1、2、3..."""
+
+
+# ================================================================================
+# 补充验证调用
+# ================================================================================
+
+# 使用场景:ContentClassifierClient._call_supplement_verification() 中的 system 消息
+# 作用:定义模型角色为"内容审查专家",约束输出为二值判断(存在/不存在)
+SUPPLEMENT_VERIFY_SYSTEM_PROMPT = '你是施工方案内容审查专家,请根据提供的内容作出判断,只回答"存在"或"不存在",不要任何其他文字。'
+
+
+def build_supplement_verify_prompt(
+    std: CategoryStandard,
+    chunk_text: str,
+    start: int,
+    end: int,
+    hit_lines: List[int],
+    matched_kws: List[str],
+    is_table: bool = False
+) -> str:
+    """
+    构建补充验证的 user prompt。
+
+    使用场景:
+        ContentClassifierClient._detect_and_supplement() → _call_supplement_verification()
+        在主分类完成后,对某些可能遗漏的三级分类进行二次确认。
+
+    触发条件:
+        1. 普通路径:扫描 section 全文,发现某些三级分类的 keywords 出现在
+           未被 LLM 分类覆盖的行中
+        2. 表格路径:section 包含表格特征(含"序号/作业活动/风险源/防范措施"
+           或 | 符号较多),某些分类可能隐藏在表格列中
+
+    参数说明:
+        std: 待确认的三级分类标准(包含 third_name, third_focus, keywords)
+        chunk_text: 待审查的文本片段(根据 hit_lines 提取)
+        start: 文本片段起始行号
+        end: 文本片段结束行号
+        hit_lines: 匹配到 keyword 的行号列表
+        matched_kws: 匹配到的关键词列表
+        is_table: 是否为表格路径(影响 trigger 说明文字)
+
+    输出格式:
+        模型应只回答"存在"或"不存在"
+
+    调用特点:
+        - 轻量级调用:max_tokens=10, temperature=0.0
+        - 逐个标准独立调用(非批量)
+        - 包含组织层级说明(区分总公司/子公司/桥梁公司)
+    """
+    if is_table:
+        trigger = "该内容块包含表格,表格中多列信息混排,以下分类在主分类阶段未被识别,需确认是否存在于表格中"
+    else:
+        trigger = f"以下关键字在文档中被检测到:{'、'.join(matched_kws)}(出现于第 {hit_lines} 行)"
+
+    return f"""你是一个施工方案内容分类专家。
+
+【组织层级说明】
+本项目的组织层级如下,判断时请严格区分:
+- 四川路桥(总公司)= 四川公路桥梁建设集团有限公司,文件通常以"四川公路桥梁"开头或含"SCQJ"
+- 路桥集团(子公司)= 四川路桥集团有限公司,文件中出现"四川路桥集团"即属于路桥集团(子公司),而非总公司
+- 桥梁公司(子公司)= 四川路桥桥梁公司,文件中出现"四川路桥桥梁公司"或"桥梁公司"即属于桥梁公司(子公司)
+
+【待审查内容】(第 {start}~{end} 行)
+{chunk_text}
+
+【待确认的分类】
+分类名称:{std.third_name}
+识别说明:{std.third_focus}
+
+【触发原因】
+{trigger}
+
+【问题】
+上述文档内容中,是否包含"{std.third_name}"相关的实质内容?
+
+请仅回答"存在"或"不存在":"""

+ 132 - 0
core/construction_review/component/reviewers/utils/llm_content_classifier_v2/text_split_utils.py

@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+文本切块工具函数
+"""
+
+from typing import Any, Dict, List, Tuple
+
+
+def _is_markdown_table_line(line: str) -> bool:
+    """判断一行是否为 Markdown 表格行(以 | 开头且以 | 结尾)"""
+    stripped = line.strip()
+    return stripped.startswith('|') and stripped.endswith('|') and len(stripped) >= 3
+
+
+def _split_text_lines_with_overlap(
+    lines: List[str],
+    max_chars: int,
+    overlap_chars: int
+) -> List[List[str]]:
+    """
+    将文本行列表按字符数切分,相邻 chunk 之间保留重叠。
+
+    - 普通行(<= max_chars):积累到超限时 flush,下一个 chunk 以末尾若干行作重叠头。
+    - 超长行(> max_chars):先 flush 当前积累,再对该行做字符级滑窗切分,
+      每片段 max_chars 字符,步长 max_chars - overlap_chars(即相邻片段重叠 overlap_chars)。
+    """
+    if not lines:
+        return []
+
+    chunks: List[List[str]] = []
+    current_lines: List[str] = []
+    current_chars: int = 0
+
+    def _flush():
+        """保存当前 chunk,并以末尾若干行作为下一个 chunk 的重叠起始。"""
+        nonlocal current_lines, current_chars
+        if not current_lines:
+            return
+        chunks.append(list(current_lines))
+        overlap_lines: List[str] = []
+        overlap_len: int = 0
+        for prev in reversed(current_lines):
+            overlap_lines.insert(0, prev)
+            overlap_len += len(prev)
+            if overlap_len >= overlap_chars:
+                break
+        current_lines = overlap_lines
+        current_chars = overlap_len
+
+    for line in lines:
+        line_chars = len(line)
+
+        if line_chars > max_chars:
+            # 超长行:先 flush,再对该行做字符级滑窗切分
+            _flush()
+            step = max_chars - overlap_chars  # 滑动步长
+            start = 0
+            while start < line_chars:
+                piece = line[start: start + max_chars]
+                chunks.append([piece])
+                start += step
+            # 以最后一片段末尾的 overlap_chars 个字符作重叠起始
+            last_piece = line[max(0, line_chars - overlap_chars):]
+            current_lines = [last_piece]
+            current_chars = len(last_piece)
+        else:
+            # 普通行:加入后超限则先 flush
+            if current_chars + line_chars > max_chars and current_lines:
+                _flush()
+            current_lines.append(line)
+            current_chars += line_chars
+
+    if current_lines:
+        chunks.append(current_lines)
+
+    return chunks
+
+
+def split_section_into_chunks(
+    lines: List[str],
+    max_chars: int = 600,
+    overlap_chars: int = 30
+) -> List[Dict[str, Any]]:
+    """
+    将二级分类下的行列表切分为 chunks。
+
+    规则:
+    - Markdown 表格(以 | 开头且以 | 结尾的连续行)作为独立 chunk,不切断、不与其他内容合并、无重叠。
+    - 普通文本按 max_chars 字符数切分,相邻 chunk 之间有 overlap_chars 字符的重叠。
+    - 单行超过 max_chars 时做字符级滑窗切分,相邻片段之间同样保留 overlap_chars 重叠。
+
+    Args:
+        lines:         行列表(不含行号标记)
+        max_chars:     每个文本 chunk 的最大字符数,默认 600
+        overlap_chars: 相邻文本 chunk 的重叠字符数,默认 30
+
+    Returns:
+        List[Dict]: 每个元素包含:
+            - 'type':  'text' 或 'table'
+            - 'lines': 该 chunk 对应的行列表
+    """
+    if not lines:
+        return []
+
+    # Step 1:将行序列分割为交替的 table_segment / text_segment
+    segments: List[Tuple[str, List[str]]] = []
+    i = 0
+    while i < len(lines):
+        if _is_markdown_table_line(lines[i]):
+            table_lines: List[str] = []
+            while i < len(lines) and _is_markdown_table_line(lines[i]):
+                table_lines.append(lines[i])
+                i += 1
+            segments.append(('table', table_lines))
+        else:
+            text_lines: List[str] = []
+            while i < len(lines) and not _is_markdown_table_line(lines[i]):
+                text_lines.append(lines[i])
+                i += 1
+            segments.append(('text', text_lines))
+
+    # Step 2:表格段整体输出;文本段按字符数切分并加重叠
+    result: List[Dict[str, Any]] = []
+    for seg_type, seg_lines in segments:
+        if seg_type == 'table':
+            result.append({'type': 'table', 'lines': seg_lines})
+        else:
+            for chunk_lines in _split_text_lines_with_overlap(seg_lines, max_chars, overlap_chars):
+                result.append({'type': 'text', 'lines': chunk_lines})
+
+    return result

+ 1 - 0
core/construction_review/component/reviewers/utils/reference_matcher.py

@@ -8,6 +8,7 @@ import re
 from typing import List, Optional, Tuple
 from dataclasses import dataclass
 
+from core.construction_review.component.reviewers.utils.reference_number_generator import generate_reference_number, validate_reference_number
 from pydantic import BaseModel, Field, ValidationError
 from langchain_core.prompts import ChatPromptTemplate
 from langchain_core.output_parsers import PydanticOutputParser, StrOutputParser

+ 1 - 0
core/construction_review/workflows/ai_review_workflow.py

@@ -293,6 +293,7 @@ class AIReviewWorkflow:
                 'semantic_logic_check': 'check_semantic_logic',
                 'completeness_check': 'check_completeness',
                 'timeliness_check': 'timeliness_basis_reviewer',
+                'timeliness_content_check': 'timeliness_content_reviewer',
                 'reference_check': 'reference_basis_reviewer',
                 'sensitive_check': 'check_sensitive',
                 'non_parameter_compliance_check': 'check_non_parameter_compliance',

+ 25 - 0
core/construction_review/workflows/core_functions/ai_review_core_fun.py

@@ -546,6 +546,31 @@ class AIReviewCoreFun:
                 is_sse_push=True
             )
 
+        # timeliness_content_reviewer:三级分类内容时效性审查(逐块处理)
+        elif func_name == "timeliness_content_reviewer" and not is_complete_field:
+            # 从chunk中获取三级分类详情
+            tertiary_details = chunk.get("tertiary_classification_details", [])
+            review_data = {
+                "tertiary_classification_details": tertiary_details,  # 三级分类详情
+                "max_concurrent": 4
+            }
+            raw_result = await method(
+                review_data=review_data,
+                trace_id=trace_id,
+                state=state,
+                stage_name=stage_name
+            )
+            # 基础审查方法,放入 basic_compliance
+            return UnitReviewResult(
+                unit_index=chunk_index,
+                unit_content=chunk,
+                basic_compliance={func_name: raw_result},
+                technical_compliance={},
+                rag_enhanced={},
+                overall_risk=self._calculate_single_result_risk(raw_result),
+                is_sse_push=True
+            )
+
         else:
             # 处理 check_completeness 但 is_complete_field=False 的情况
             if func_name == "check_completeness" and not is_complete_field:

+ 1 - 1
foundation/ai/agent/generate/model_generate.py

@@ -303,4 +303,4 @@ class GenerateModelClient:
             logger.error(f"[模型流式调用] 异常 trace_id: {trace_id}, 耗时: {elapsed_time:.2f}s, 错误: {type(e).__name__}: {str(e)}")
             raise
 
-generate_model_client = GenerateModelClient(default_timeout=15, max_retries=2, backoff_factor=0.5)
+generate_model_client = GenerateModelClient(default_timeout=60, max_retries=10, backoff_factor=0.5)

+ 16 - 0
foundation/infrastructure/config/config.py

@@ -24,6 +24,22 @@ class ConfigHandler:
             value = default
         return value
 
+    def get_section(self, section):
+        """获取整个配置段的字典
+
+        Args:
+            section: 配置段名称
+
+        Returns:
+            该段所有配置的字典,如果不存在则返回空字典
+        """
+        try:
+            if self.config.has_section(section):
+                return dict(self.config.items(section))
+        except Exception:
+            pass
+        return {}
+
 
 
 # 全局配置实例

File diff suppressed because it is too large
+ 145 - 0
problem.json


二進制
requirements.txt


+ 3 - 0
server/app.py

@@ -35,6 +35,7 @@ from views.construction_review.file_upload import file_upload_router
 from views.construction_review.review_results import review_results_router
 from views.construction_review.launch_review import launch_review_router
 from views.construction_review.task_control import task_control_router
+from views.construction_review.desensitize_api import desensitize_router
 
 # 导入施工方案编写路由
 from views.construction_write.outline_views import outline_router
@@ -103,6 +104,7 @@ class RouteManager:
         self.app.include_router(review_results_router)
         self.app.include_router(launch_review_router)
         self.app.include_router(task_control_router)  # 任务控制路由
+        self.app.include_router(desensitize_router)   # 数据脱敏路由
 
         # 施工方案编写路由
         self.app.include_router(outline_router)
@@ -540,6 +542,7 @@ def create_app() -> FastAPI:
     app.include_router(file_upload_router)
     app.include_router(review_results_router)
     app.include_router(launch_review_router)
+    app.include_router(desensitize_router)  # 数据脱敏路由
 
     # 施工方案编写路由
     app.include_router(outline_router)

+ 141 - 0
test_content_timeliness.py

@@ -0,0 +1,141 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+测试内容时效性审查是否正确处理 JTG B01-2011 的情况
+"""
+
+import json
+import asyncio
+from core.construction_review.component.reviewers.timeliness_content_reviewer import (
+    StandardExtractor, ContentTimelinessReviewer
+)
+
+# 测试数据 - 模拟 problem.json 中的情况
+test_tertiary_details = [
+    {
+        "third_category_name": "国家方针、政策、标准和设计文件",
+        "third_category_code": "NationalPoliciesStandardsAndDesignDocument",
+        "start_line": 80,
+        "end_line": 82,
+        "content": """<80> 国家方针、政策、标准和设计文件
+<81> 《公路工程技术标准》(JTG B01-2011)
+<82> 《公路桥涵设计通用规范》(JTG D60-2015)"""
+    }
+]
+
+# 测试提取器
+def test_extractor():
+    print("=" * 60)
+    print("测试规范提取器")
+    print("=" * 60)
+
+    extractor = StandardExtractor()
+
+    for detail in test_tertiary_details:
+        refs = extractor.extract_from_content(detail["content"])
+        print(f"\n从 '{detail['third_category_name']}' 提取到 {len(refs)} 个规范引用:")
+        for ref in refs:
+            print(f"  - 原始文本: {ref.original_text}")
+            print(f"    名称: {ref.name}")
+            print(f"    编号: {ref.number}")
+            print(f"    上下文: {ref.context[:100]}...")
+
+    return refs
+
+# 测试过滤逻辑
+def test_filter_logic():
+    print("\n" + "=" * 60)
+    print("测试过滤逻辑")
+    print("=" * 60)
+
+    # 模拟 match_reference_files 返回的数据
+    mock_match_result = [
+        {
+            "review_item": "《公路工程技术标准》(JTG B01-2011)",
+            "has_related_file": True,
+            "has_exact_match": False,
+            "exact_match_info": "",
+            "same_name_current": "《公路工程技术标准》(JTG B01-2014)状态为现行"
+        },
+        {
+            "review_item": "《公路桥涵设计通用规范》(JTG D60-2015)",
+            "has_related_file": True,
+            "has_exact_match": True,
+            "exact_match_info": "《公路桥涵设计通用规范》(JTG D60-2015)状态为现行",
+            "same_name_current": ""
+        }
+    ]
+
+    print("\n模拟 match_reference_files 返回数据:")
+    for idx, item in enumerate(mock_match_result):
+        print(f"\n  项{idx}:")
+        print(f"    review_item: {item['review_item']}")
+        print(f"    has_related_file: {item['has_related_file']}")
+        print(f"    has_exact_match: {item['has_exact_match']}")
+        print(f"    exact_match_info: {item['exact_match_info']}")
+        print(f"    same_name_current: {item['same_name_current']}")
+
+    # 测试旧过滤逻辑(只保留 exact_match_info 不为空的)
+    old_filtered = [item for item in mock_match_result if item.get('exact_match_info')]
+    print(f"\n旧过滤逻辑(只保留 exact_match_info 不为空的): {len(old_filtered)} 个项")
+    for item in old_filtered:
+        print(f"  - {item['review_item']}")
+
+    # 测试新过滤逻辑(保留有相关信息的)
+    new_filtered = [
+        item for item in mock_match_result
+        if item.get('has_related_file') or
+           item.get('exact_match_info') or
+           item.get('same_name_current')
+    ]
+    print(f"\n新过滤逻辑(保留有相关信息的): {len(new_filtered)} 个项")
+    for item in new_filtered:
+        print(f"  - {item['review_item']}")
+
+    # 分析差异
+    missed = [item for item in mock_match_result if item not in old_filtered]
+    if missed:
+        print(f"\n[警告] 旧逻辑漏检的项:")
+        for item in missed:
+            print(f"  - {item['review_item']}")
+            print(f"    has_related_file: {item['has_related_file']}")
+            print(f"    same_name_current: {item['same_name_current']}")
+
+# 完整测试
+async def test_full_review():
+    print("\n" + "=" * 60)
+    print("完整审查测试(需要 Milvus 连接)")
+    print("=" * 60)
+
+    try:
+        async with ContentTimelinessReviewer(max_concurrent=4) as reviewer:
+            results = await reviewer.review_tertiary_content(
+                tertiary_details=test_tertiary_details,
+                collection_name="first_bfp_collection_status"
+            )
+
+            print(f"\n审查完成,共 {len(results)} 个结果:")
+            for idx, result in enumerate(results):
+                print(f"\n  结果{idx}:")
+                print(f"    check_item: {result.get('check_item')}")
+                print(f"    exist_issue: {result.get('exist_issue')}")
+                print(f"    risk_info: {result.get('risk_info')}")
+                check_result = result.get('check_result', {})
+                print(f"    issue_point: {check_result.get('issue_point')}")
+                print(f"    suggestion: {check_result.get('suggestion')}")
+                print(f"    reason: {check_result.get('reason')}")
+
+    except Exception as e:
+        print(f"测试失败: {e}")
+        import traceback
+        traceback.print_exc()
+
+if __name__ == "__main__":
+    # 测试提取器
+    refs = test_extractor()
+
+    # 测试过滤逻辑
+    test_filter_logic()
+
+    # 完整测试(可选)
+    # asyncio.run(test_full_review())

+ 0 - 1
tests/test_pr

@@ -1 +0,0 @@
-  pass1

File diff suppressed because it is too large
+ 145 - 0
utils_test/Completeness_Test/2026年3月23日-bug/fc38b3526e408a787d0fdc75e024eb3d-1774245354.json


+ 267 - 0
utils_test/Redis/redis_sentinel_test_2.py

@@ -0,0 +1,267 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import redis
+from redis.sentinel import Sentinel, SentinelConnectionPool
+from redis.connection import Connection, ConnectionPool
+import time
+import sys
+
+# ============================================
+# 关键配置:地址映射表
+# ============================================
+SENTINEL_EXTERNAL_IP='192.168.92.96'
+# Sentinel 端口(所有节点共用)
+SENTINEL_EXTERNAL_PORT = 30768  # Sentinel 服务的 NodePort
+
+# Redis 数据端口 - 每个节点需要独立的 NodePort
+# 这样可以根据 master 主机名动态连接到对应的端口
+REDIS_DATA_MAPPING = {
+    # 内部主机名 -> (外部 IP, 外部端口)
+    'redis-node-0.redis-headless.redis.svc.cluster.local': ('192.168.92.96', 32259),
+    'redis-node-1.redis-headless.redis.svc.cluster.local': ('192.168.92.96', 32260),
+    'redis-node-2.redis-headless.redis.svc.cluster.local': ('192.168.92.96', 32261),
+}
+
+# 旧的映射表(备用)
+ADDRESS_MAPPING = REDIS_DATA_MAPPING
+NODE_PORT_MAPPING = REDIS_DATA_MAPPING
+EXTERNAL_MAPPING = REDIS_DATA_MAPPING
+
+# ============================================
+# 自定义 Connection 类:拦截并转换地址
+# ============================================
+class ExternalRedisConnection(Connection):
+    """自定义连接类,自动转换内部地址到外部地址"""
+
+    def __init__(self, host='localhost', port=6379, **kwargs):
+        # 转换地址
+        original_host = host
+        original_port = port
+
+        if host in EXTERNAL_MAPPING:
+            new_host, new_port = EXTERNAL_MAPPING[host]
+            print(f"  [地址转换] {original_host}:{original_port} -> {new_host}:{new_port}")
+            host = new_host
+            port = new_port
+        else:
+            print(f"  [地址保持] {host}:{port} (未在映射表中)")
+
+        super().__init__(host=host, port=port, **kwargs)
+
+
+class ExternalConnectionPool(ConnectionPool):
+    """自定义连接池,使用地址转换连接"""
+    connection_class = ExternalRedisConnection
+
+
+class ExternalSentinelManagedConnectionPool(SentinelConnectionPool):
+    """Sentinel 管理的连接池,支持地址转换"""
+    connection_class = ExternalRedisConnection
+
+    def __init__(self, service_name, sentinel_manager, **kwargs):
+        # 保存参数
+        self.service_name = service_name
+        self.sentinel_manager = sentinel_manager
+        self._kwargs = kwargs
+        super().__init__(service_name, sentinel_manager, **kwargs)
+
+    def get_master_address(self):
+        """获取 Master 地址并转换"""
+        master_addr = super().get_master_address()
+        if master_addr:
+            host, port = master_addr
+            if host in EXTERNAL_MAPPING:
+                new_addr = EXTERNAL_MAPPING[host]
+                print(f"  [Master 转换] {host}:{port} -> {new_addr[0]}:{new_addr[1]}")
+                return new_addr
+        return master_addr
+
+    def rotate_slaves(self):
+        """轮询 Slave 地址并转换"""
+        slaves = super().rotate_slaves()
+        converted_slaves = []
+        for host, port in slaves:
+            if host in EXTERNAL_MAPPING:
+                new_host, new_port = EXTERNAL_MAPPING[host]
+                print(f"  [Slave 转换] {host}:{port} -> {new_host}:{new_port}")
+                converted_slaves.append((new_host, new_port))
+            else:
+                converted_slaves.append((host, port))
+        return converted_slaves or [self.get_master_address()]
+
+
+# ============================================
+# Redis 配置
+# ============================================
+REDIS_CONFIG = {
+    'sentinels': [
+        (SENTINEL_EXTERNAL_IP, SENTINEL_EXTERNAL_PORT),  # 哨兵 NodePort
+    ],
+    'master_name': 'lqmaster',
+    'password': 'Lq123456!',
+    'socket_timeout': 5,
+    'socket_connect_timeout': 5
+}
+
+
+def get_master_external_address(sentinel):
+    """获取 master 的外部可访问地址"""
+    master_host, master_port = sentinel.discover_master(REDIS_CONFIG['master_name'])
+    print(f"Sentinel 返回的 Master: {master_host}:{master_port}")
+
+    # 转换到外部地址
+    if master_host in REDIS_DATA_MAPPING:
+        ext_host, ext_port = REDIS_DATA_MAPPING[master_host]
+        print(f"转换后的外部地址:{ext_host}:{ext_port}")
+        return ext_host, ext_port
+    else:
+        print(f"警告:{master_host} 不在映射表中")
+        # 尝试从主机名提取节点名
+        if 'redis-node-0' in master_host:
+            return REDIS_DATA_MAPPING.get('redis-node-0.redis-headless.redis.svc.cluster.local', (None, None))
+        elif 'redis-node-1' in master_host:
+            return REDIS_DATA_MAPPING.get('redis-node-1.redis-headless.redis.svc.cluster.local', (None, None))
+        elif 'redis-node-2' in master_host:
+            return REDIS_DATA_MAPPING.get('redis-node-2.redis-headless.redis.svc.cluster.local', (None, None))
+        return None, None
+
+
+def simple_test_with_conversion():
+    """简化测试:动态获取 master 地址并连接"""
+    print("\n" + "="*50)
+    print("简化测试:动态主从切换支持")
+    print("="*50)
+
+    try:
+        # 1. 连接 Sentinel
+        sentinel = Sentinel(
+            REDIS_CONFIG['sentinels'],
+            sentinel_kwargs={
+                'password': REDIS_CONFIG['password'],
+                'socket_timeout': REDIS_CONFIG['socket_timeout']
+            }
+        )
+
+        # 2. 获取 master 的外部地址
+        ext_host, ext_port = get_master_external_address(sentinel)
+        if not ext_host or not ext_port:
+            print("无法获取 master 外部地址!")
+            return None
+
+        # 3. 打印所有节点信息
+        print("\nSentinel 管理的所有节点:")
+        try:
+            slaves = sentinel.discover_slaves(REDIS_CONFIG['master_name'])
+            for slave in slaves:
+                print(f"  从节点:{slave[0]}:{slave[1]}")
+        except Exception as e:
+            print(f"  无法获取从节点列表:{e}")
+
+        # 4. 直接连接到 master(使用转换后的地址)
+        print(f"\n直接连接 Master: {ext_host}:{ext_port}")
+        master_client = redis.Redis(
+            host=ext_host,
+            port=ext_port,
+            password=REDIS_CONFIG['password'],
+            socket_timeout=REDIS_CONFIG['socket_timeout'],
+            decode_responses=False
+        )
+
+        # 5. 测试连接
+        ping_result = master_client.ping()
+        print(f"✓ 连接成功!PING: {ping_result}")
+
+        # 6. 读写测试
+        test_key = b"test:external:conversion"
+        test_value = f"timestamp_{int(time.time())}".encode()
+
+        master_client.set(test_key, test_value)
+        print(f"✓ 写入成功:{test_key.decode()} = {test_value.decode()}")
+
+        read_value = master_client.get(test_key)
+        print(f"✓ 读取成功:{test_key.decode()} = {read_value.decode() if read_value else None}")
+
+        # 7. 清理
+        master_client.delete(test_key)
+        print(f"✓ 清理成功")
+
+        return master_client
+
+    except Exception as e:
+        print(f"✗ 测试失败:{e}")
+        import traceback
+        traceback.print_exc()
+        return None
+
+
+def test_with_proxy_command():
+    """使用 redis-py 的代理功能(另一种方案)"""
+    print("\n" + "="*50)
+    print("方案 2:使用连接参数转换")
+    print("="*50)
+
+    # 这种方案在创建连接时动态替换地址
+    # 适用于需要保持 Sentinel 高可用特性的场景
+
+    try:
+        sentinel = Sentinel(REDIS_CONFIG['sentinels'])
+
+        # 获取当前 Master
+        master_host, master_port = sentinel.discover_master(REDIS_CONFIG['master_name'])
+        print(f"原始 Master: {master_host}:{master_port}")
+
+        # 转换
+        if master_host in EXTERNAL_MAPPING:
+            new_host, new_port = EXTERNAL_MAPPING[master_host]
+        else:
+            new_host, new_port = master_host, master_port
+
+        # 创建直连客户端(不通过 Sentinel 管理,适合短期连接)
+        client = redis.Redis(
+            host=new_host,
+            port=new_port,
+            password=REDIS_CONFIG['password'],
+            decode_responses=True
+        )
+
+        # 测试
+        client.set("test:proxy", "works")
+        value = client.get("test:proxy")
+        print(f"✓ 代理测试成功:{value}")
+        client.delete("test:proxy")
+
+        return client
+
+    except Exception as e:
+        print(f"✗ 失败:{e}")
+        return None
+
+
+def main():
+    """主函数"""
+    print("\n")
+    print("="*60)
+    print("Redis Sentinel 外部访问测试(地址转换版)")
+    print("="*60)
+    print(f"哨兵地址:{REDIS_CONFIG['sentinels']}")
+    print(f"Master 名称:{REDIS_CONFIG['master_name']}")
+    print(f"地址映射表:")
+    for internal, external in EXTERNAL_MAPPING.items():
+        print(f"  {internal} -> {external[0]}:{external[1]}")
+    print("="*60)
+
+    # 方案 1:简化测试(推荐)
+    client = simple_test_with_conversion()
+
+    if not client:
+        print("\n简化测试失败,尝试其他方案...")
+        # 可以在这里添加其他测试方案
+
+    print("\n" + "="*60)
+    print("测试完成!")
+    print("="*60)
+
+
+if __name__ == "__main__":
+    main()

+ 358 - 0
views/construction_review/desensitize_api.py

@@ -0,0 +1,358 @@
+"""
+脱敏模块 API 接口
+
+提供文档脱敏、校验、结果还原等功能的 REST API 接口
+
+根据 wlast.md 文档第7节设计
+"""
+
+import uuid
+from datetime import datetime
+from typing import Optional
+from pydantic import BaseModel, Field
+from fastapi import APIRouter, HTTPException, UploadFile, File, Form
+from fastapi.responses import JSONResponse
+
+from foundation.observability.logger.loggering import review_logger as logger
+from core.construction_review.component.desensitize import (
+    BlackWhiteListChecker,
+    ValidationResult,
+    DictManager,
+)
+from core.construction_review.component.desensitize.remapper import ResultRemapper
+
+desensitize_router = APIRouter(prefix="/desensitize", tags=["数据脱敏"])
+
+# 初始化组件
+validator = BlackWhiteListChecker()
+dict_manager = DictManager()
+remapper = ResultRemapper()
+
+
+# ============ 请求/响应模型 ============
+
+class DesensitizeLevel:
+    """脱敏级别枚举"""
+    MINIMAL = "minimal"    # 最小脱敏:仅PII
+    STANDARD = "standard"  # 标准脱敏:PII + 地理坐标 + 商业标识
+    STRICT = "strict"      # 严格脱敏:全四维度
+
+
+class DesensitizeModelType:
+    """脱敏模型类型枚举"""
+    RULE = "rule"              # 规则引擎
+    QWEN3_5_35B = "qwen3_5_35b"  # Qwen3.5-35B本地推理
+
+
+class ValidateCheckLevel:
+    """校验级别枚举"""
+    STRICT = "strict"
+    NORMAL = "normal"
+
+
+class DesensitizeDocumentRequest(BaseModel):
+    """文档脱敏请求模型"""
+    user_id: str = Field(..., description="用户唯一标识")
+    project_id: str = Field(..., description="项目唯一标识")
+    desensitize_level: str = Field(default="standard", description="脱敏级别: minimal/standard/strict")
+    model_type: str = Field(default="rule", description="脱敏处理模型: rule/qwen3_5_35b")
+
+
+class DesensitizeDocumentResponse(BaseModel):
+    """文档脱敏响应模型"""
+    code: int = Field(default=200, description="状态码")
+    message: str = Field(default="success", description="状态消息")
+    data: dict = Field(default_factory=dict, description="响应数据")
+
+
+class ValidateRequest(BaseModel):
+    """脱敏校验请求模型"""
+    content: str = Field(..., description="待校验的文本内容")
+    check_level: str = Field(default="strict", description="校验级别: strict/normal")
+
+
+class ValidateResponse(BaseModel):
+    """脱敏校验响应模型"""
+    code: int = Field(default=200, description="状态码")
+    message: str = Field(default="success", description="状态消息")
+    data: dict = Field(default_factory=dict, description="响应数据")
+
+
+class RemapRequest(BaseModel):
+    """结果还原请求模型"""
+    task_id: str = Field(..., description="文档脱敏时返回的任务ID")
+    cloud_response: str = Field(..., description="云端审查返回的文本")
+    remap_coordinate: bool = Field(default=True, description="是否还原相对桩号")
+
+
+class RemapResponse(BaseModel):
+    """结果还原响应模型"""
+    code: int = Field(default=200, description="状态码")
+    message: str = Field(default="success", description="状态消息")
+    data: dict = Field(default_factory=dict, description="响应数据")
+
+
+class DictInfoResponse(BaseModel):
+    """字典信息响应模型"""
+    code: int = Field(default=200, description="状态码")
+    message: str = Field(default="success", description="状态消息")
+    data: dict = Field(default_factory=dict, description="响应数据")
+
+
+# ============ API 接口 ============
+
+@desensitize_router.post("/document", response_model=DesensitizeDocumentResponse)
+async def desensitize_document(
+    user_id: str = Form(..., description="用户唯一标识"),
+    project_id: str = Form(..., description="项目唯一标识"),
+    document: UploadFile = File(..., description="PDF/Word格式施工方案"),
+    desensitize_level: str = Form(default="standard", description="脱敏级别: minimal/standard/strict"),
+    model_type: str = Form(default="rule", description="脱敏处理模型: rule/qwen3_5_35b")
+):
+    """
+    文档脱敏接口
+
+    对施工方案文档进行四维度脱敏处理,生成脱敏字典并本地加密存储
+
+    - **desensitize_level**: minimal(仅PII) / standard(标准) / strict(严格)
+    - **model_type**: rule(规则引擎) / qwen3_5_35b(本地大模型)
+    """
+    try:
+        # 生成任务ID
+        task_id = f"des-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:6]}"
+
+        logger.info(f"[DesensitizeAPI] 文档脱敏请求: task_id={task_id}, "
+                   f"user_id={user_id}, level={desensitize_level}, model={model_type}")
+
+        # 读取文档内容
+        content_bytes = await document.read()
+        content = content_bytes.decode('utf-8', errors='ignore')
+
+        if not content:
+            raise HTTPException(status_code=400, detail="文档内容为空或无法解析")
+
+        # 注:脱敏功能暂时禁用,直接返回原始内容
+        # TODO: 如需启用脱敏,取消下面注释并删除直接返回的代码
+        # result: DesensitizedResult = await desensitize_engine.process(content, task_id)
+        # if not result.is_valid:
+        #     return DesensitizeDocumentResponse(...)
+
+        # 直接返回原始内容(脱敏已禁用)
+        preview_length = min(500, len(content))
+        return DesensitizeDocumentResponse(
+            code=200,
+            message="文档处理成功(脱敏功能已禁用)",
+            data={
+                "task_id": task_id,
+                "status": "completed (desensitization disabled)",
+                "desensitize_level": desensitize_level,
+                "model_type": model_type,
+                "output": {
+                    "content_preview": content[:preview_length] + "..." if len(content) > preview_length else content,
+                    "content_length": len(content),
+                    "dict_hash": ""
+                },
+                "statistics": {
+                    "pii_count": 0,
+                    "geo_count": 0,
+                    "biz_count": 0,
+                    "financial_count": 0
+                }
+            }
+        )
+
+    except Exception as e:
+        logger.exception(f"[DesensitizeAPI] 文档脱敏失败: {e}")
+        raise HTTPException(status_code=500, detail=f"脱敏处理失败: {str(e)}")
+
+
+@desensitize_router.post("/validate", response_model=ValidateResponse)
+async def validate_desensitized(request: ValidateRequest):
+    """
+    脱敏校验接口
+
+    黑白名单校验,检测脱敏是否完整,返回违规项列表
+
+    - **check_level**: strict(严格) / normal(普通)
+    """
+    try:
+        logger.info(f"[DesensitizeAPI] 校验请求: check_level={request.check_level}")
+
+        result: ValidationResult = validator.validate(request.content, request.check_level)
+
+        # 构造违规项响应
+        violations = []
+        for v in result.violations[:20]:  # 限制返回数量
+            violations.append({
+                "type": v.get("type"),
+                "match": v.get("match"),
+                "severity": v.get("severity"),
+                "suggestion": v.get("suggestion"),
+                "position": v.get("positions", [{}])[0] if v.get("positions") else {}
+            })
+
+        return ValidateResponse(
+            code=200,
+            message="校验完成" if result.is_valid else f"发现 {len(result.violations)} 个违规项",
+            data={
+                "is_valid": result.is_valid,
+                "check_level": request.check_level,
+                "violations": violations,
+                "summary": {
+                    "total_violations": len(result.violations),
+                    "whitelist_matches": result.whitelist_matches,
+                    "blacklist_matches": result.blacklist_matches
+                }
+            }
+        )
+
+    except Exception as e:
+        logger.exception(f"[DesensitizeAPI] 校验失败: {e}")
+        raise HTTPException(status_code=500, detail=f"校验失败: {str(e)}")
+
+
+@desensitize_router.post("/remap", response_model=RemapResponse)
+async def remap_result(request: RemapRequest):
+    """
+    结果还原接口
+
+    将云端审查意见中的泛化占位符还原为真实工程术语,生成最终审查报告
+
+    示例转换:
+    - "[项目经理A]在[1号特大桥]K0+500处发现安全隐患"
+    - "张三在映雪特大桥D1K86+779.91处发现安全隐患"
+    """
+    try:
+        logger.info(f"[DesensitizeAPI] 结果还原请求: task_id={request.task_id}")
+
+        # 检查字典是否存在
+        if not await dict_manager.exists(request.task_id):
+            raise HTTPException(status_code=404, detail=f"找不到脱敏字典: {request.task_id}")
+
+        # 执行映射
+        remap_result = await remapper.remap(
+            cloud_response=request.cloud_response,
+            task_id=request.task_id,
+            remap_coordinate=request.remap_coordinate
+        )
+
+        if remap_result.errors:
+            logger.warning(f"[DesensitizeAPI] 映射警告: {remap_result.errors}")
+
+        return RemapResponse(
+            code=200,
+            message="映射还原成功",
+            data={
+                "task_id": request.task_id,
+                "original_response": remap_result.original_response,
+                "remapped_response": remap_result.remapped_response,
+                "mapping_summary": remap_result.mapping_summary
+            }
+        )
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.exception(f"[DesensitizeAPI] 结果还原失败: {e}")
+        raise HTTPException(status_code=500, detail=f"结果还原失败: {str(e)}")
+
+
+@desensitize_router.get("/dict/{task_id}", response_model=DictInfoResponse)
+async def get_dict_info(task_id: str):
+    """
+    字典查询接口
+
+    查询脱敏字典元信息(不包含敏感映射内容)
+    """
+    try:
+        metadata = dict_manager.get_dict_metadata(task_id)
+
+        if not metadata:
+            raise HTTPException(status_code=404, detail=f"找不到脱敏字典: {task_id}")
+
+        return DictInfoResponse(
+            code=200,
+            message="查询成功",
+            data={
+                "task_id": task_id,
+                "metadata": {
+                    "file_path": metadata.get("file_path"),
+                    "file_size": metadata.get("file_size"),
+                    "modified_at": metadata.get("modified_at")
+                }
+            }
+        )
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.exception(f"[DesensitizeAPI] 字典查询失败: {e}")
+        raise HTTPException(status_code=500, detail=f"查询失败: {str(e)}")
+
+
+@desensitize_router.delete("/dict/{task_id}")
+async def delete_dict(task_id: str):
+    """
+    删除脱敏字典接口
+
+    手动删除指定任务的脱敏字典(通常由自动清理任务处理)
+    """
+    try:
+        success = await dict_manager.delete(task_id)
+
+        if success:
+            return JSONResponse(
+                status_code=200,
+                content={
+                    "code": 200,
+                    "message": f"字典 {task_id} 已删除",
+                    "data": {"task_id": task_id}
+                }
+            )
+        else:
+            raise HTTPException(status_code=500, detail="删除失败")
+
+    except Exception as e:
+        logger.exception(f"[DesensitizeAPI] 字典删除失败: {e}")
+        raise HTTPException(status_code=500, detail=f"删除失败: {str(e)}")
+
+
+@desensitize_router.post("/text")
+async def desensitize_text(
+    content: str = Form(..., description="待脱敏文本内容"),
+    level: str = Form(default="standard", description="脱敏级别")
+):
+    """
+    文本脱敏接口(简化版)
+
+    直接对输入文本进行脱敏,不存储字典(适用于简单场景)
+    """
+    try:
+        # 注:脱敏功能暂时禁用,直接返回原始内容
+        # TODO: 如需启用脱敏,取消下面注释
+        # task_id = f"text-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:6]}"
+        # result = await desensitize_engine.process(content, task_id)
+        # await dict_manager.delete(task_id)
+
+        return JSONResponse(
+            status_code=200,
+            content={
+                "code": 200,
+                "message": "文本处理成功(脱敏功能已禁用)",
+                "data": {
+                    "original_length": len(content),
+                    "desensitized_length": len(content),
+                    "desensitized_content": content,  # 返回原始内容
+                    "statistics": {
+                        "pii_count": 0,
+                        "geo_count": 0,
+                        "biz_count": 0,
+                        "financial_count": 0
+                    }
+                }
+            }
+        )
+
+    except Exception as e:
+        logger.exception(f"[DesensitizeAPI] 文本脱敏失败: {e}")
+        raise HTTPException(status_code=500, detail=f"文本脱敏失败: {str(e)}")

+ 171 - 7
views/construction_review/file_upload.py

@@ -4,22 +4,171 @@
 """
 import ast
 import traceback
-import uuid
 import time
+import tempfile
+import subprocess
+import os
 from datetime import datetime
+from pathlib import Path
 
-from pydantic import BaseModel, Field
-from typing import Optional,List
+from pydantic import BaseModel
+from typing import List
 from foundation.utils import md5
 from foundation.infrastructure.config import config_handler
 from .schemas.error_schemas import FileUploadErrors
 from core.base.workflow_manager import WorkflowManager
 from foundation.observability.logger.loggering import review_logger as logger
 from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Request
-from core.base.redis_duplicate_checker import RedisDuplicateChecker
 from foundation.infrastructure.tracing import TraceContext, auto_trace
 
 
+def _find_soffice_path() -> str:
+    """
+    查找 LibreOffice soffice 可执行文件路径
+
+    Returns:
+        str: soffice 可执行文件路径
+
+    Raises:
+        FileNotFoundError: 未找到 LibreOffice
+    """
+    import platform
+
+    # Linux/Docker 环境:直接使用 soffice
+    if platform.system() != 'Windows':
+        return 'soffice'
+
+    # Windows 环境:检测常见安装路径
+    possible_paths = [
+        r"C:\Program Files\LibreOffice\program\soffice.exe",
+        r"C:\Program Files (x86)\LibreOffice\program\soffice.exe",
+    ]
+
+    for path in possible_paths:
+        if os.path.exists(path):
+            logger.info(f"找到 LibreOffice: {path}")
+            return path
+
+    raise FileNotFoundError(
+        "LibreOffice 未安装。请从 https://www.libreoffice.org/download/ 下载安装,"
+        "或确保 soffice.exe 在 PATH 中"
+    )
+
+
+def convert_docx_to_pdf(docx_content: bytes, filename: str) -> tuple[bytes, str]:
+    """
+    将 docx/doc 文件内容转换为 PDF
+
+    Windows 开发环境: 优先使用 docx2pdf (Microsoft Word COM),回退到 LibreOffice
+    Linux/Docker 生产环境: 使用 LibreOffice (soffice)
+
+    Args:
+        docx_content: docx/doc 文件的二进制内容
+        filename: 原始文件名
+
+    Returns:
+        tuple[bytes, str]: (PDF 文件内容, 原始文件名)
+
+    Raises:
+        Exception: 转换失败时抛出异常
+    """
+    import platform
+
+    # Windows 环境:优先尝试 docx2pdf (Microsoft Word COM)
+    if platform.system() == 'Windows':
+        try:
+            from docx2pdf import convert
+            return _convert_via_docx2pdf(docx_content, filename, convert)
+        except ImportError:
+            logger.info("docx2pdf 未安装,使用 LibreOffice")
+        except Exception as e:
+            logger.warning(f"docx2pdf 转换失败,回退到 LibreOffice: {str(e)}")
+
+    # Linux/Docker 或 Windows 回退:使用 LibreOffice
+    return _convert_via_libreoffice(docx_content, filename)
+
+
+def _convert_via_docx2pdf(docx_content: bytes, filename: str, convert_func) -> tuple[bytes, str]:
+    """使用 docx2pdf (Microsoft Word COM) 转换,返回 PDF 内容和原始文件名"""
+    with tempfile.TemporaryDirectory() as temp_dir:
+        temp_dir_path = Path(temp_dir)
+
+        # 保存原始文件
+        original_ext = Path(filename).suffix.lower()
+        temp_input = temp_dir_path / f"input{original_ext}"
+        temp_output = temp_dir_path / "output.pdf"
+        temp_input.write_bytes(docx_content)
+
+        logger.info(f"使用 Microsoft Word 转换 {filename} 为 PDF...")
+
+        convert_func(str(temp_input), str(temp_output))
+
+        if not temp_output.exists():
+            raise Exception("转换后未找到 PDF 文件")
+
+        pdf_content = temp_output.read_bytes()
+
+        logger.info(f"成功转换 {filename} 为 PDF, 大小: {len(pdf_content) / 1024:.2f} KB")
+
+        return pdf_content, filename  # 返回原始文件名
+
+
+def _convert_via_libreoffice(docx_content: bytes, filename: str) -> tuple[bytes, str]:
+    """使用 LibreOffice (soffice) 转换,返回 PDF 内容和原始文件名"""
+    # 创建临时目录
+    with tempfile.TemporaryDirectory() as temp_dir:
+        temp_dir_path = Path(temp_dir)
+
+        # 保存原始文件到临时目录
+        original_ext = Path(filename).suffix.lower()
+        base_name = Path(filename).stem
+        temp_input = temp_dir_path / f"input{original_ext}"
+        temp_input.write_bytes(docx_content)
+
+        logger.info(f"使用 LibreOffice 转换 {filename} 为 PDF...")
+
+        # 查找 LibreOffice 路径
+        try:
+            soffice_path = _find_soffice_path()
+        except FileNotFoundError as e:
+            logger.error(str(e))
+            raise Exception(str(e))
+
+        # 使用 LibreOffice 转换
+        try:
+            result = subprocess.run(
+                [
+                    soffice_path, '--headless', '--convert-to', 'pdf',
+                    '--outdir', str(temp_dir_path),
+                    str(temp_input)
+                ],
+                capture_output=True,
+                text=True,
+                timeout=120  # 2分钟超时
+            )
+
+            if result.returncode != 0:
+                logger.error(f"LibreOffice 转换失败: {result.stderr}")
+                raise Exception(f"LibreOffice 转换失败: {result.stderr}")
+
+            # 查找生成的 PDF 文件
+            pdf_files = list(temp_dir_path.glob("*.pdf"))
+            if not pdf_files:
+                raise Exception("转换后未找到 PDF 文件")
+
+            pdf_file = pdf_files[0]
+            pdf_content = pdf_file.read_bytes()
+
+            logger.info(f"成功转换 {filename} 为 PDF, 大小: {len(pdf_content) / 1024:.2f} KB")
+
+            return pdf_content, filename  # 返回原始文件名
+
+        except subprocess.TimeoutExpired:
+            raise Exception("LibreOffice 转换超时")
+        except FileNotFoundError:
+            raise Exception("LibreOffice 未安装或 soffice 命令不可用")
+
+
 file_upload_router = APIRouter(prefix="/sgsc", tags=["前端接口"])
 uploaded_files = {}
 # 初始化工作流管理器
@@ -136,7 +285,7 @@ async def file_upload(
         if user is None or user not in valid_users:
             raise FileUploadErrors.invalid_user()
 
-        # 生成文件MD5ID
+        # 生成文件MD5ID(基于原始文件内容,用于重复检测)
         file_id = md5.md5_id(content)
 
 
@@ -153,10 +302,25 @@ async def file_upload(
 
         # 确定文件类型
         file_extension = file[0].filename.split('.')[-1].lower() if '.' in file[0].filename else ''
+        original_filename = file[0].filename  # 保存原始文件名
+
         if content.startswith(b'%PDF'):
             file_type = 'pdf'
         elif content.startswith(b'PK\x03\x04') and file_extension in ['docx', 'doc']:
-            file_type = 'docx'
+            # 检测到 docx/doc 文件,转换为 PDF
+            logger.info(f"检测到 {file_extension} 文件,正在转换为 PDF...")
+            try:
+                pdf_content, _ = convert_docx_to_pdf(content, original_filename)
+                # 更新文件内容(文件名和 MD5 保持不变,用于重复检测)
+                content = pdf_content
+                file_type = 'pdf'  # 标记为 PDF 类型,后续流程按 PDF 处理
+                file_size = len(pdf_content)
+                file_size_mb = round(file_size / (1024 * 1024), 2)
+                # 注意:file_id 保持不变(基于原始 docx 内容),用于重复文件检测
+                logger.info(f"文件已转换为 PDF,大小: {file_size_mb} MB")
+            except Exception as convert_error:
+                logger.error(f"docx 转 PDF 失败: {str(convert_error)}")
+                raise FileUploadErrors.internal_error(f"文档转换失败: {str(convert_error)}")
         else:
             file_type = 'unknown'
 
@@ -172,7 +336,7 @@ async def file_upload(
                 'user_id': user,
                 'file_type': file_type,
                 'callback_task_id': callback_task_id,
-                "file_name": file[0].filename,
+                "file_name": original_filename,  # 保持原始文件名(docx 转 PDF 后仍显示原始文件名)
                 "file_size": file_size_mb,
                 'updated_at': created_at
             }

+ 1 - 6
views/construction_review/review_results.py

@@ -3,14 +3,9 @@
 模拟风险统计、总结报告和问题条文返回
 """
 
-import random
-import os
-import json
-from datetime import datetime
 from fastapi import APIRouter, HTTPException, Query
 from pydantic import BaseModel
-from typing import Optional, Dict, Any
-from .schemas.error_schemas import ReviewResultsErrors
+from typing import Dict, Any
 from foundation.observability.cachefiles import cache, CacheBaseDir
 
 

+ 0 - 1
views/construction_review/task_control.py

@@ -3,7 +3,6 @@
 提供任务终止、查询等控制功能
 """
 
-import asyncio
 from typing import List, Optional, Dict, Any
 from pydantic import BaseModel, Field
 from fastapi import APIRouter, HTTPException, Query

+ 0 - 1
views/construction_write/content_completion.py

@@ -6,7 +6,6 @@ API URL: https://dashscope.aliyuncs.com/compatible-mode/v1
 模型:qwen3-30b-a3b-instruct-2507
 """
 
-import os
 import uuid
 import json
 import time

+ 0 - 2
views/construction_write/outline_views.py

@@ -10,7 +10,6 @@
 - POST /sgbx/context_generate: SSE 流式上下文生成 (新增)
 """
 
-import os
 import uuid
 import json
 import time
@@ -26,7 +25,6 @@ from foundation.infrastructure.config.config import config_handler
 from core.base.workflow_manager import WorkflowManager
 from core.base.sse_manager import unified_sse_manager
 from core.base.progress_manager import ProgressManager
-from redis import asyncio as redis_async  # 新增这行
 from redis.asyncio import Redis as AsyncRedis
 
 # 创建路由

Some files were not shown because too many files changed in this diff