Initial commit

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xxy 2026-06-05 18:41:06 +08:00
commit 43f3e0b746
29 changed files with 4772 additions and 0 deletions

21
.env.example Normal file
View File

@ -0,0 +1,21 @@
# 复制为 .env 并按实际环境修改
# 远程 MySQL章节内容入库目标
DATABASE_URL=mysql+pymysql://root:Beidas0ft@192.168.4.177:3306/eval_report?charset=utf8mb4
DB_AUTO_CREATE_TABLES=true
# 远程文档解析服务(上传文档 → Markdown
FILE_PARSE_API_URL=http://192.168.4.194:8000/convert
FILE_PARSE_FIELD_NAME=file
FILE_PARSE_ENGINE=auto
FILE_PARSE_HTTP_TIMEOUT_SEC=600
# LLM可选为每个目录生成"声明"。留空则使用确定性兜底模板。
LLM_API_BASE=http://192.168.4.197:8086/v1
LLM_API_KEY=sk-99999999991234
LLM_MODEL_NAME=Qwen3.6-27B
DECLARATION_USE_LLM=true
# 服务监听
HOST=0.0.0.0
PORT=8100

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
# Python
__pycache__/
*.py[cod]
.venv/
venv/
*.egg-info/
# 环境与日志
.env
logs/
# IDE
.idea/
.vscode/

98
README.md Normal file
View File

@ -0,0 +1,98 @@
# 报告模板管理模块
上传一个文档,自动完成:
1. **远程解析**:调用 `http://192.168.4.194:8000/convert`(表单字段 `file` + `engine=auto`)将文档转换为 Markdown。
2. **抽取目录**:从 Markdown 中识别章节标题层级(目录)。
3. **生成声明**:为每个目录(章节)生成一段"章节声明"(撰写指引),存入模板。
4. **脱敏入库**:按标题拆分正文,对每个章节正文**脱敏**(去掉精确数字/金额/日期/百分比等),再按远程 MySQL `report_section_references` 表格式写入,得到可复用的模板化范文。
解析、目录抽取、正文拆分逻辑参考 `eval_report/routers/template.py``routers/reference.py`
## 目录结构
```
config.py 全局配置DB / 解析服务 / LLM
main.py FastAPI 入口
database/ 连接、ORM 模型、建表
models.py report_templates / report_template_sections / report_section_references
schemas/template.py 接口出入参
services/
file_parse_client.py 调用远程 /convert → Markdown
section_extractor.py 目录抽取 + 正文按标题拆分(共用同一遍历)
desensitize_service.py 章节正文脱敏(去精确数字等)
declaration_service.py 为每个目录生成"声明"LLM 可选 + 兜底模板)
llm_client.py OpenAI 兼容 Chat 客户端(可选)
routers/template.py 上传/列表/详情/删除
```
## 配置
复制 `.env.example``.env` 并修改:
- `DATABASE_URL`:远程 MySQL章节内容入库目标
- `FILE_PARSE_API_URL`:远程文档解析服务(默认 `http://192.168.4.194:8000/convert`,文件字段 `FILE_PARSE_FIELD_NAME=file`,引擎 `FILE_PARSE_ENGINE=auto`)。
- `LLM_*`:可选。配置后用 LLM 生成更贴合的章节声明;留空则使用确定性兜底模板。
启动时会按需在远程库中创建本模块用到的三张表(`DB_AUTO_CREATE_TABLES=true`,已存在则跳过)。
## 运行
```bash
pip install -r requirements.txt
python main.py
# 或
uvicorn main:app --host 0.0.0.0 --port 8100
```
打开 `http://localhost:8100/docs` 查看接口文档。
## 主要接口
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/templates/upload` | 上传文档,解析为模板(目录+声明)并将章节内容入库 |
| GET | `/templates` | 模板列表 |
| GET | `/templates/{id}` | 模板详情(含目录与各章节声明) |
| DELETE | `/templates/{id}` | 删除模板 |
| GET | `/health` | 健康检查 |
### 上传示例
```bash
curl -X POST "http://localhost:8100/templates/upload" \
-F "file=@/path/to/报告.docx"
```
返回包含:模板信息(每个目录的 `sectionDeclaration` 即声明)、入库章节数与各章节摘要。
## 日志
启动即初始化日志系统(`log/logger.py`),输出到控制台(强制 UTF-8避免 Windows 中文乱码)并写入 `logs/`
| 文件 | 内容 |
| --- | --- |
| `logs/app.log` | 全量日志(按大小轮转) |
| `logs/error.log` | WARNING 及以上 |
| `logs/upload.log` | 上传/解析/入库链路(`routers.template``services.*` |
- 每个 HTTP 请求会记录方法、路径、状态码、耗时,并在响应头返回 `X-Request-ID`
- uvicorn 的 access/error 日志也统一汇入上述文件。
- 可在 `.env` 调整:`LOG_LEVEL``LOG_DIR``LOG_TO_CONSOLE``LOG_MAX_BYTES``LOG_BACKUP_COUNT``LOG_HTTP_ACCESS`
## 数据落点
- `report_templates`:一条模板记录。
- `report_template_sections`:每个目录一条,`section_prompt` 字段存放该目录的**声明**。
- `report_section_references`:每个章节一条,存放该章节**脱敏后的正文内容**(与远程库现有格式一致)。
### 脱敏规则
`services/desensitize_service.py`
- 阿拉伯数字串(含小数/千分位/全角)→ 占位符(默认 `X``总投资10.5亿元``总投资X亿元``85.3%``X%``2020年3月``X年X月`
- 标题行(`#` 开头)整行保留,不动章节编号与标题。
- 行首枚举序号(`1``2` 等)保留,仅脱敏正文数字。
- 表格分隔行保留;数据格数字默认脱敏(`DESENSITIZE_MASK_TABLE_NUMBERS`)。
- 中文数字(一二三…)默认保留(多为序数/层级)。
- 可在 `.env` 调整:`DESENSITIZE_ENABLED``DESENSITIZE_PLACEHOLDER``DESENSITIZE_MASK_TABLE_NUMBERS`

92
config.py Normal file
View File

@ -0,0 +1,92 @@
"""
config.py
报告模板管理模块的全局配置可通过 .env 或环境变量覆盖
"""
from __future__ import annotations
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# 应用基本信息
APP_TITLE: str = "报告模板管理模块 API"
APP_VERSION: str = "0.1.0"
APP_DESCRIPTION: str = "上传文档 → 远程解析为 Markdown → 拆解目录/章节 → 入库远程 MySQL"
# 服务监听
HOST: str = "0.0.0.0"
PORT: int = 8100
RELOAD: bool = False
CORS_ORIGINS: list[str] = ["*"]
# 日志
LOG_LEVEL: str = "INFO" # DEBUG / INFO / WARNING / ERROR
LOG_DIR: str = "logs" # 日志目录(相对启动目录或绝对路径)
LOG_TO_CONSOLE: bool = True # 是否同时输出到控制台
LOG_MAX_BYTES: int = 10 * 1024 * 1024 # 单文件最大字节数(轮转)
LOG_BACKUP_COUNT: int = 7 # 轮转保留份数
LOG_HTTP_ACCESS: bool = True # 是否记录每个 HTTP 请求
# 远程 MySQLmysql+pymysql://用户:密码@主机:端口/库名?charset=utf8mb4
DATABASE_URL: str = (
"mysql+pymysql://root:Beidas0ft@192.168.4.177:3306/eval_report?charset=utf8mb4"
)
DB_POOL_SIZE: int = 10
DB_MAX_OVERFLOW: int = 20
DB_POOL_TIMEOUT: int = 60
DB_POOL_PRE_PING: bool = True
# 启动时自动建表(仅创建本模块用到的表,已存在则跳过)
DB_AUTO_CREATE_TABLES: bool = True
# 远程文档解析服务:上传文件 → Markdown
FILE_PARSE_API_URL: str = "http://192.168.4.194:8000/convert"
FILE_PARSE_FIELD_NAME: str = "file"
# 解析引擎(随 multipart 一起提交的表单字段 engine
FILE_PARSE_ENGINE: str = "auto"
FILE_PARSE_HTTP_TIMEOUT_SEC: int = 600
FILE_PARSE_RETRY_COUNT: int = 3
FILE_PARSE_RETRY_BACKOFF_SEC: float = 15.0
# 章节正文:是否包含其下级小节内容(章/节聚合整棵子树正文,避免父章节正文为空)
SECTION_CONTENT_INCLUDE_SUBSECTIONS: bool = True
# 单章节正文入库字节上限MySQL TEXT 列上限 65535 字节,留余量防止截断到半个字符)
SECTION_CONTENT_MAX_BYTES: int = 60000
# 章节内容脱敏:入库前过滤精确数据(数字/金额/日期/百分比等)
DESENSITIZE_ENABLED: bool = True
DESENSITIZE_PLACEHOLDER: str = "X" # 数字脱敏后的占位符
# 是否把表格中的数字也脱敏(表格通常是精确数据,默认开启)
DESENSITIZE_MASK_TABLE_NUMBERS: bool = True
# LLM可选为每个目录生成"声明"。未配置时使用确定性兜底模板。
LLM_API_BASE: str = ""
LLM_API_KEY: str = ""
LLM_MODEL_NAME: str = ""
LLM_HTTP_TIMEOUT_SEC: int = 120
# 关闭思考模型的思维链输出vLLM/Qwen3 等chat_template_kwargs.enable_thinking=false
# 既避免"思考过程"混入正文,又减少 token、降低截断与耗时。
LLM_DISABLE_THINKING: bool = True
# 是否调用 LLM 生成章节声明(关闭则始终使用兜底模板)
DECLARATION_USE_LLM: bool = True
# 上传模版时:用 LLM 匹配默认提示词 / 为无匹配章节生成提示词(复刻 eval_report
TEMPLATE_UPLOAD_LLM_PROMPT_MAPPING: bool = True
# LLM 提示词匹配并发:把未匹配章节分批并行调用,缩短整体耗时。
# 多卡 A100 + 连续批处理vLLM/TGITP 或多副本)下,提高并发在飞请求数即可打满 GPU
# - 调小 BATCH_SIZE请求更多更短确保批次数 ≥ 线程数,单请求尾延迟更低
# - 调大 MAX_WORKERS同时在飞的序列更多填满推理服务的批decode 吞吐接近峰值
# - 调小 MAX_TOKENS每序列 KV 缓存预留更少,调度器可纳入更多并发序列
# 2×A100并发目标约 16较单卡的 8 翻倍BATCH_SIZE=2 保证常见规模也能跑满 16 路。
TEMPLATE_UPLOAD_LLM_BATCH_SIZE: int = 2 # 每批未匹配章节数量
TEMPLATE_UPLOAD_LLM_MAX_WORKERS: int = 16 # 并行线程数上限(在飞请求数)
TEMPLATE_UPLOAD_LLM_MAX_TOKENS: int = 2048 # 单批最大输出 token
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
settings = Settings()

7
database/__init__.py Normal file
View File

@ -0,0 +1,7 @@
"""database package连接、模型与依赖注入。"""
from database.core import SessionLocal, engine
from database.dependencies import get_db
from database.init_db import init_database
__all__ = ["engine", "SessionLocal", "get_db", "init_database"]

33
database/core.py Normal file
View File

@ -0,0 +1,33 @@
"""
database/core.py
SQLAlchemy 引擎与 Session 工厂同步引擎连接远程 MySQL
"""
from __future__ import annotations
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from config import settings
engine = create_engine(
settings.DATABASE_URL,
pool_size=settings.DB_POOL_SIZE,
max_overflow=settings.DB_MAX_OVERFLOW,
pool_timeout=settings.DB_POOL_TIMEOUT,
pool_pre_ping=settings.DB_POOL_PRE_PING,
pool_recycle=3600,
connect_args={
"charset": "utf8mb4",
"use_unicode": True,
"init_command": "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
},
echo=False,
)
SessionLocal = sessionmaker(
bind=engine,
autocommit=False,
autoflush=False,
expire_on_commit=False,
)

20
database/dependencies.py Normal file
View File

@ -0,0 +1,20 @@
"""
database/dependencies.py
FastAPI 依赖注入获取数据库 Session
"""
from __future__ import annotations
from typing import Generator
from sqlalchemy.orm import Session
from database.core import SessionLocal
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()

52
database/init_db.py Normal file
View File

@ -0,0 +1,52 @@
"""
database/init_db.py
按需建表仅创建本模块用到的三张表已存在则跳过checkfirst=True
"""
from __future__ import annotations
import logging
from sqlalchemy import inspect, text
from database.core import engine
from database.models import Base
logger = logging.getLogger(__name__)
def _ensure_reference_template_id_column() -> None:
"""为已存在的 report_section_references 表补充 template_id 字段(幂等)。
create_all(checkfirst=True) 只建缺失的表不会给已存在的表加列
因此这里对历史表做一次轻量级 ALTER仅在缺列时执行
"""
insp = inspect(engine)
if "report_section_references" not in insp.get_table_names():
return
columns = {c["name"] for c in insp.get_columns("report_section_references")}
if "template_id" in columns:
return
with engine.begin() as conn:
conn.execute(
text(
"ALTER TABLE report_section_references "
"ADD COLUMN template_id VARCHAR(64) NULL"
)
)
conn.execute(
text(
"ALTER TABLE report_section_references "
"ADD INDEX ix_report_section_references_template_id (template_id)"
)
)
logger.info("init_database: report_section_references.template_id 字段已补充")
def init_database() -> None:
"""在远程 MySQL 中创建本模块所需表(若不存在)。"""
Base.metadata.create_all(bind=engine, checkfirst=True)
_ensure_reference_template_id_column()
logger.info("init_database: report_templates / report_template_sections / report_section_references 已就绪")

68
database/models.py Normal file
View File

@ -0,0 +1,68 @@
"""
database/models.py
ORM 模型与远程 MySQLeval_report 现有表结构一致
- report_templates 模板
- report_template_sections 模板章节目录 + 声明
- report_section_references 章节参考范文章节内容入库目标
"""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class ReportTemplate(Base):
__tablename__ = "report_templates"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
class ReportTemplateSection(Base):
__tablename__ = "report_template_sections"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
template_id: Mapped[str] = mapped_column(
ForeignKey("report_templates.id", ondelete="CASCADE"), nullable=False
)
section_key: Mapped[str] = mapped_column(String(64), nullable=False)
section_title: Mapped[str] = mapped_column(String(255), nullable=False)
# 本模块语义section_prompt 即为该目录生成的"声明"
section_prompt: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
section_output_contract: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
section_order: Mapped[int] = mapped_column(Integer, default=0)
examples: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
class ReportSectionReference(Base):
"""章节参考范文(章节内容入库目标,格式与远程 MySQL 现有表一致)。"""
__tablename__ = "report_section_references"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
# 关联模板(与 report_template_sections.template_id 一致);历史数据可能为空
template_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("report_templates.id", ondelete="CASCADE"), nullable=True, index=True
)
source_file: Mapped[str] = mapped_column(String(255), nullable=False)
section_key: Mapped[str] = mapped_column(String(64), nullable=False)
section_title: Mapped[str] = mapped_column(String(255), nullable=False)
section_order: Mapped[int] = mapped_column(Integer, default=0)
content: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)

5
log/__init__.py Normal file
View File

@ -0,0 +1,5 @@
"""日志包:统一日志配置。"""
from log.logger import configure_logging, get_logger
__all__ = ["configure_logging", "get_logger"]

142
log/logger.py Normal file
View File

@ -0,0 +1,142 @@
"""
log/logger.py
统一日志配置
- 控制台输出强制 UTF-8修复 Windows 控制台中文乱码
- logs/app.log 全量日志按大小轮转
- logs/error.log WARNING 及以上
- logs/upload.log 上传/解析/入库链路routers.templateservices.*
- 接管 uvicorn access/error 日志统一落盘
幂等重复调用只配置一次
"""
from __future__ import annotations
import logging
import sys
from logging.handlers import RotatingFileHandler
from pathlib import Path
_CONFIGURED = False
_FORMAT = "%(asctime)s | %(levelname)-7s | %(name)s | %(message)s"
_DATEFMT = "%Y-%m-%d %H:%M:%S"
# 上传/解析/入库链路相关的 logger 前缀(额外汇总到 upload.log
_UPLOAD_PREFIXES = (
"routers.template",
"services.file_parse_client",
"services.section_extractor",
"services.declaration_service",
"services.llm_client",
)
# 交由 root 统一处理的第三方/框架 logger
_DELEGATED_LOGGERS = ("uvicorn", "uvicorn.error", "uvicorn.access")
class _PrefixFilter(logging.Filter):
def __init__(self, prefixes: tuple[str, ...]) -> None:
super().__init__()
self.prefixes = prefixes
def filter(self, record: logging.LogRecord) -> bool:
name = str(record.name or "")
return any(name == p or name.startswith(p + ".") for p in self.prefixes)
def _force_utf8_stream(stream):
"""让控制台以 UTF-8 输出,避免 Windows GBK 控制台中文乱码。"""
reconfigure = getattr(stream, "reconfigure", None)
if callable(reconfigure):
try:
reconfigure(encoding="utf-8", errors="replace")
except (ValueError, OSError):
pass
return stream
def configure_logging(
*,
log_dir: str | Path | None = None,
level: str | int | None = None,
to_console: bool | None = None,
max_bytes: int | None = None,
backup_count: int | None = None,
) -> Path:
"""配置全局日志。返回 app.log 路径。"""
global _CONFIGURED
# 延迟导入,避免与 config 形成循环依赖问题
from config import settings
log_dir = log_dir if log_dir is not None else settings.LOG_DIR
level = level if level is not None else settings.LOG_LEVEL
to_console = to_console if to_console is not None else settings.LOG_TO_CONSOLE
max_bytes = max_bytes if max_bytes is not None else settings.LOG_MAX_BYTES
backup_count = backup_count if backup_count is not None else settings.LOG_BACKUP_COUNT
if isinstance(level, str):
level = getattr(logging, level.strip().upper(), logging.INFO)
target_dir = Path(log_dir).resolve()
target_dir.mkdir(parents=True, exist_ok=True)
app_log_path = target_dir / "app.log"
if _CONFIGURED:
return app_log_path
formatter = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
def _rotating(name: str, *, backups: int | None = None) -> RotatingFileHandler:
h = RotatingFileHandler(
target_dir / name,
maxBytes=max_bytes,
backupCount=backups if backups is not None else backup_count,
encoding="utf-8",
)
h.setFormatter(formatter)
return h
# 全量日志
app_handler = _rotating("app.log")
app_handler.setLevel(level)
# 错误日志WARNING+
error_handler = _rotating("error.log")
error_handler.setLevel(logging.WARNING)
# 上传/解析链路日志
upload_handler = _rotating("upload.log", backups=max(backup_count, 10))
upload_handler.setLevel(level)
upload_handler.addFilter(_PrefixFilter(_UPLOAD_PREFIXES))
handlers: list[logging.Handler] = [app_handler, error_handler, upload_handler]
if to_console:
console_handler = logging.StreamHandler(_force_utf8_stream(sys.stdout))
console_handler.setLevel(level)
console_handler.setFormatter(formatter)
handlers.append(console_handler)
root = logging.getLogger()
root.setLevel(level)
root.handlers.clear()
for h in handlers:
root.addHandler(h)
# 让 uvicorn 的日志走 root 统一落盘
for name in _DELEGATED_LOGGERS:
lg = logging.getLogger(name)
lg.handlers.clear()
lg.propagate = True
lg.setLevel(level)
_CONFIGURED = True
logging.getLogger(__name__).info("日志系统已初始化 | dir=%s | level=%s", target_dir, logging.getLevelName(level))
return app_log_path
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)

101
main.py Normal file
View File

@ -0,0 +1,101 @@
"""
main.py
报告模板管理模块 FastAPI 应用入口
启动
uvicorn main:app --host 0.0.0.0 --port 8100
python main.py
"""
from __future__ import annotations
import time
import uuid
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from config import settings
from database import init_database
from log import configure_logging, get_logger
from routers import template
# 在创建应用前完成日志配置
configure_logging()
logger = get_logger("app")
access_logger = get_logger("app.access")
@asynccontextmanager
async def lifespan(_app: FastAPI):
logger.info("应用启动 | %s v%s", settings.APP_TITLE, settings.APP_VERSION)
if settings.DB_AUTO_CREATE_TABLES:
try:
init_database()
except Exception as e: # noqa: BLE001
logger.warning("启动建表失败(不影响已存在表的使用): %s", e)
yield
logger.info("应用关闭")
app = FastAPI(
title=settings.APP_TITLE,
version=settings.APP_VERSION,
description=settings.APP_DESCRIPTION,
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def log_requests(request: Request, call_next):
if not settings.LOG_HTTP_ACCESS:
return await call_next(request)
req_id = uuid.uuid4().hex[:8]
start = time.perf_counter()
client = request.client.host if request.client else "-"
access_logger.info("→ [%s] %s %s | client=%s", req_id, request.method, request.url.path, client)
try:
response = await call_next(request)
except Exception:
cost = (time.perf_counter() - start) * 1000
access_logger.exception("✗ [%s] %s %s | %.1fms | 未处理异常", req_id, request.method, request.url.path, cost)
raise
cost = (time.perf_counter() - start) * 1000
access_logger.info(
"← [%s] %s %s | %s | %.1fms",
req_id, request.method, request.url.path, response.status_code, cost,
)
response.headers["X-Request-ID"] = req_id
return response
app.include_router(template.router)
@app.get("/health", tags=["健康检查"])
def health() -> dict:
return {"status": "ok", "version": settings.APP_VERSION}
if __name__ == "__main__":
import uvicorn
# log_config=None沿用本模块 configure_logging() 的配置,避免被 uvicorn 覆盖
uvicorn.run(
"main:app",
host=settings.HOST,
port=settings.PORT,
reload=settings.RELOAD,
log_config=None,
)

0
prompts/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,877 @@
"""Section output contract prompt variables."""
SECTION_OUTPUT_CONTRACTS: dict[str, str] = {'1': '按章节标题自然组织内容,围绕证据包先事实后结论,缺失项写“待补充”。',
'1.1': '必须按以下顺序输出,不得缺项、不得改名:\n'
'1) 项目名称:...\n'
'2) 建设单位:...\n'
'3) 建设地点:...\n'
'4) 建设类型:...\n'
'5) 起止时间:...\n'
'6) 建设内容:...\n'
'7) 建设投资:...\n'
'8) 占地面积:...\n'
'规则:内容仅可来自证据包;缺失项写“待补充”;严禁复用示例中的事实数据。',
'1.2': '必须严格按以下固定结构输出(纯文本编号体),不得缺项、不得增项、不得使用“###”等 Markdown 标题语法:\n'
'项目决策要点\n'
'1.2.1项目背景\n'
'1...\n'
'(要求:先用 24 句完整书面语概括动因与结论,再视需要附表)\n'
'2...\n'
'(要求:同上)\n'
'3...\n'
'要求第3条背景优先写“政策/标准/环保与质量升级”类动因,并给出可由证据包定位支撑的结论,但正文中不要输出“【证据依据:...】”标签)\n'
'综合上述因素,...\n'
'\n'
'1.2.2预期目标\n'
'项目实施后,...\n'
'\n'
'写作质量规则(必须遵守):\n'
'1) '
'必须完全按上述行序与段落结构输出只允许出现「项目决策要点」「1.2.1项目背景」「1」「2」「3」「综合上述因素...」「1.2.2预期目标」这些结构标识;不得输出额外小标题、不得输出项目之外的说明段。\n'
'2) 每条背景必须是连续自然段(可多段),禁止把证据包里的原始换行表直接粘贴成“多列对不齐”的纯文本块。\n'
'3) 若需引用对比表、物料平衡表等,必须使用 Markdown 表格含表头分隔行表内数字与证据包一致可注明表号如表1\n'
'4) 第3条背景请检索证据包中国VI、汽油标准、环评、排放、清洁生产等相关表述结论必须可由证据包定位到文档名或段落支撑但正文中不要输出“【证据依据...】”标签。\n'
'5) 「预期目标」必须写成一段或多段完整书面语(不要用“- 规模目标/质量目标/效益目标:”三行结构)。若证据包已出现装置规模、烷基化油产量/产能(万吨/年、辛烷值、国VI、收入、利润、IRR '
'等任一可核对信息,必须在该段落中明确写出对应数字/结论;不得在证据已含关键数字时仍全部写“待补充”。\n'
'6) 证据不足时,对应句子写“待补充”,不得编造数字。\n'
'7) 严禁复用【章节示例】中的项目名、金额与结论。',
'1.3': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'第一行固定为标题:"1.3 项目实施情况"\n'
'第二段:仅用一段连续文字,按时间顺序写项目实施关键节点,覆盖可研/初设批复、开工、中交、投产试运行、竣工验收等信息;时间与事件要一一对应,可用分号分隔,禁止拆成条目。\n'
'第三段:仅用一段连续文字写投资执行对比,至少包含批复可研估算、批复初设概算、竣工决算;并计算与表述节余金额及比例(若证据不足则对应项写“待补充”),金额与口径仅可使用证据包。\n'
'第四段固定写法:"项目建设工作程序见附表1。"(无证据冲突时必须保留原句)。\n'
'写作约束:正文不得使用“项目实施关键节点”“建设与投资执行情况”等标签式小标题,不得编造时间、金额或比例。\n'
'【禁止输出表格】本节禁止输出任何 Markdown 表格含附表1在内“见附表1”仅为文字引用附表在报告末尾统一输出。',
'1.4': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'第一行固定为标题:"1.4 项目运行情况"\n'
'第二段:仅用一段连续文字写项目运行情况,需包含投产后运行状态、分阶段连续运行时长、停工原因(如有)、加工负荷与烷基化油产量等关键事实;按时间顺序组织,禁止拆成条目。\n'
'第三段:仅用一段连续文字写经营与财务表现,至少包含营业收入、总成本费用、利润总额等指标,并给出经营结论(如盈利能力判断);结论必须由证据支撑。\n'
'财务口径强约束:本节优先且原则上仅可使用投产后已实现的实际值(如某年实际营业收入/成本/利润);不得使用“预测值、后预测、测算值、年均值(生产期均值)”替代实际值。”。\n'
'写作约束:正文不得使用“运行负荷与产量”“经营表现/财务表现”“总体运行结论”等标签式小标题;涉及时间、负荷、产量、金额等数据时,仅可使用证据包口径,证据不足处写“待补充”,不得编造;禁止在同一段内重复抄写相同句子或同一年份财务数据。',
'2': '按章节标题自然组织内容,围绕证据包先事实后结论,缺失项写“待补充”。',
'2.1': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节2.1.1、2.1.2、2.1.3、2.1.4、2.1.5、2.1.6、2.1.7。',
'2.1.1': '必须严格按以下格式与顺序输出,不得缺项、不得改名、不得调换段落顺序。\n'
'正文必须贴近以下版式组织,使用连续自然段表达,不得再写“事实依据/评价判断/问题与建议”等标签:\n'
'1先写一段原料来源及与可研一致性的判断并以“原料数量及组成对比见下表”引出表1。\n'
'2随后固定输出表题“表1原料数量及组成对比表”并紧跟表格。\n'
'3表1后单独一行输出“注1.……”;如果有注释就输出,无明确注释时不写。\n'
'4表1后写一段对可研报告、初步设计、实际生产的原料数量与组成进行对比并给出结论。\n'
'5再写一段全厂或装置负荷、原油加工量、装置加工量等实际运行情况。\n'
'6然后写一句“实际生产原料组成与性质与可研报告基本一致满足装置进料要求详见下表。”或同义表达引出表2。\n'
'7随后固定输出表题“表2原料性质对比表醚后碳四并紧跟表格。\n'
'8表2后写一段分析原料组成、性质、烷烯比等变化及其对生产的影响。\n'
'9最后单独写“后评价认为……”总结性结论结论必须回扣前文和表格数据。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表不得用安评/工艺包比选表替代。\n'
'2表1必须使用要素管理中对应的“原料数量及组成对比表”表题固定为“表1原料数量及组成对比表”。\n'
'3表2必须使用要素管理中对应的“原料性质对比表醚后碳四表题固定为“表2原料性质对比表醚后碳四”。\n'
'4若要素管理中存在上述表格则优先直出其表头、分组表头、行项目和单元格内容不得改列名、不得合并为其他样式、不得替换成其他表。\n'
'5表1字段含义必须覆盖序号、原料名称、规格、可研报告数量万吨/占比(%))、初步设计(数量(万吨)/占比(%))、实际生产(数量(万吨)/占比(%))、备注;须保留合计行。\n'
'6表2字段含义必须覆盖序号、名称、可研报告、初步设计、实际生产、备注行至少包含“密度kg/m3”“硫含量ppm”“氮含量ppm”其余行按要素管理表格直出。\n'
'\n'
'【禁止】\n'
'不得使用“表2.6-1”“原料选择加氢工艺技术对比”等安评/工艺包比选表作为本节主体;不得出现与本节无关的附录标题;不得把表格改写成列表、条目或非表格文本;证据不足处写“待补充”,不得编造。',
'2.1.2': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。',
'2.1.2.1': '本节必须按“产品方案评价”目的组织内容,针对全厂性炼油项目或部分化工类项目,通过对项目投产后市场对产品种类、规格、标准等方面需求的实际情况与前期工作确定的产品方案进行对比,评价前期工作确定的产品方案是否与市场实际需求相适应,评价主要产品是否为高效厚利产品以及对项目成败的影响情况。\n'
'\n'
'必须严格按以下格式与顺序输出,不得缺项、不得改名、不得调换顺序:\n'
'1先写一段总述说明本节是通过产品种类、规格、标准、产量及市场实际需求的前后对比评价产品方案适应性及其对项目成败的影响。\n'
'2再写一段对项目产品前后对比情况进行分析如产品方案与实际需求相差较大必须分析原因。\n'
'3随后固定写一句“项目产品方案对比表见表2-3。”\n'
'4紧接着固定输出表题“表2-3 产品方案对比表”,并紧跟表格。\n'
'5表格后不输出任何模板性注释如“注.表中内容可根据项目实际需要进行增减”等套话),直接进入后评价结论。\n'
'6最后单独写“后评价认为。”并基于前文与表格数据补全评价结论。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表不得改写成列表或段落。\n'
'2优先使用要素管理中对应“产品方案对比表”的结构化表若存在对应表格须直出其表头、分组表头、行项目和单元格内容不得改列名、不得替换成其他表。\n'
'3表题固定为“表2-3 产品方案对比表”。\n'
'4表格字段含义必须覆盖序号、产品、可研报告规格、可研报告数量万吨/年)、实际生产规格、实际生产数量(万吨/年)、备注。\n'
'5表内行项目可包括但不限于汽油、航空煤油、柴油、XX化工品、XX润滑油、XXX、轻油产品率%、综合商品率,%、柴汽比;具体行项目按要素管理中的表格直出,可根据项目实际需要增减。\n'
'\n'
'【写作约束】\n'
'正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”等标签;不得编造市场需求、产品规格、产量、比率或效益判断;证据不足处写“待补充”。',
'2.1.2.2': '本节必须按“产品市场评价”目的组织内容,围绕项目产品市场需求、销售渠道、产品流向、市场风险及产品结构改善情况展开分析,通过对可研报告预期与实际生产情况进行对比,评价前期工作对产品市场的预测是否合理。\n'
'\n'
'必须严格按以下格式与顺序输出,不得缺项、不得改名、不得调换顺序:\n'
'1第一段写可研报告对产品市场的判断依据需说明市场供需现状、预测、供需平衡或标准升级等因素如何支撑项目产品需求。\n'
'2第二段写可研报告对产品消化路径、厂内平衡、销售渠道、市场风险的预测可结合汽油调和、统一销售、内部消化等实际口径展开。\n'
'3第三段写实际生产情况需说明实际产品调入去向、销售方式、销售渠道并与可研预期进行对比。\n'
'4随后固定写一句“前期工作预测的主要产品产量、流向与实际生产产品产量及流向对比见下表。”\n'
'5紧接着固定输出表题“表2-4 ××年项目主要产品流向状况”,并紧跟表格;若为多年数据,应按每年分别列表;投产时间较短时可按季度或几个月列表。\n'
'6表格后即表格最后一行之后单独一行固定输出“注指装置投产到后评价时点按每年列表投产时间短的也可以是季或几个月。”——注必须在表格外面严禁将“注”写入表格的任何单元格包括备注列\n'
'7最后单独写“后评价认为……”总结性结论必须明确判断产品流向、销售渠道、市场风险、产品结构改善等方面与可研预测的一致性或差异。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表不得改写成列表或段落。\n'
'2优先使用要素管理中对应“表2-4 ××年项目主要产品流向状况”或“主要产品流向状况”的结构化表;若存在对应表格,须直出其表头、分组表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表题固定为“表2-4 ××年项目主要产品流向状况”;其中“××年”应替换为要素管理表对应年份。若存在多个年份,应逐年分别输出对应表题和表格。\n'
'4表格字段含义必须覆盖产品名称、规格、实际产量、销量、产品实际流向、可研报告产品流向、备注。\n'
'5表内行项目按要素管理中的表格直出可包含“×××”“小计”等行“小计/合计”行应放在表格末尾。\n'
'6严禁将“注”或注释性文字写入表格行或任何单元格中所有注释必须在表格 Markdown 结束后另起一行输出。\n'
'\n'
'【写作约束】\n'
'正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”等标签;不得编造市场需求、销量、流向、销售渠道、市场风险或产品结构改善情况;证据不足处写“待补充”。',
'2.1.3': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。',
'2.1.3.1': '本节必须按“总加工方案评价”目的组织内容,通过实际情况与前期工作的对比,评价整体规模、单线规模、产品方案是否一致;如存在较大差异,必须分析变动原因及其合理性,并结合项目实际运行情况,对总加工方案的合理性、适应性作出评价。\n'
'\n'
'必须严格按以下要求输出:\n'
'1先写前期工作确定的总体加工方案包括整体规模、单线规模、主要产品方案等核心内容。\n'
'2再写项目实际建设和运行情况与前期工作逐项对比说明一致项与差异项。\n'
'3如整体规模、单线规模、产品方案存在较大变化必须写明变化内容、形成原因及是否合理不得只写结论不写依据。\n'
'4最后单独给出总结性评价明确判断总加工方案是否合理、是否适应实际运行需要以及相关调整是否合理。\n'
'\n'
'【写作约束】\n'
'正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”等标签;可使用连续自然段表达;证据不足处写“待补充”,不得编造规模、负荷、产品方案或变动原因。',
'2.1.3.2': '本节必须按“建设规模及工艺技术方案评价”目的组织内容,通过实际情况与前期工作的对比,评价建设规模、装置规模、运行负荷与工艺技术方案是否与可研一致;如存在较大差异,必须分析原因及合理性,并结合实际运行情况,对工艺技术方案的先进性、适应性、可靠性和环保性能作出评价。\n'
'\n'
'必须严格按以下格式与顺序输出,不得缺项、不得改名、不得调换顺序:\n'
'1先固定输出“1烷基化装置工艺技术方案”作为小标题小标题下先用一段完整文字写可研报告对不同烷基化工艺的比选过程、比选维度、拟选工艺及最终技术供应商随后必须使用“123…”逐条列出可研推荐工艺的先进性与适应性条目内容可覆盖辛烷值、选择性、酸耗、反应器与传热方式、安全环保、可靠性、运行费用等方面。\n'
'2再固定输出“2废酸再生单元工艺技术方案”作为小标题小标题下先用一段完整文字写可研报告对不同废酸再生工艺的比选过程、比选对象、拟选工艺及最终技术供应商随后必须使用“12…”逐条列出推荐工艺的主要特点条目内容可覆盖流程简洁性、运行成本、尾气排放、二次污染、操作弹性、工艺适用性等方面。\n'
'3最后直接单独写“后评价认为……”不得再输出“3后评价结论”或其他总结性小标题。结论必须综合评价烷基化与废酸再生工艺选用是否合理适用、技术是否先进可靠、环保性能是否良好以及前期工作确定的装置规模和原料条件是否在实际运行中得到验证。\n'
'\n'
'【结构硬约束】\n'
'本节正文仅允许出现上述两个编号小标题与最后“后评价认为:……”结论,不得再新增“建设规模及装置规模对比”“配套单元及附属单元”等其他编号小标题;如需说明规模、负荷、原料来源等内容,只能写入“后评价认为:……”段内,不得单列成新标题。\n'
'\n'
'【写作约束】\n'
'正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”等标签;不得编造装置规模、单元规模、运行负荷、技术供应商、工艺优缺点或调整方向;证据不足处写“待补充”。',
'2.1.3.3': '本节必须按“主要设备方案评价”目的组织内容,通过对可研报告、初步设计与实际运行情况的对比,评价主要设备的选型、材质、结构形式及优化调整是否合理适用,是否满足装置长周期平稳运行需要。\n'
'\n'
'必须严格按以下格式与顺序输出,不得缺项、不得改名、不得调换顺序:\n'
'1固定输出“1反应器”作为小标题写明反应器是否采用技术专利商专利设备、设备材质、结构形式、专利内件或关键设计特点并说明其在传质效率、混合效果、安全生产可靠性等方面的作用。\n'
'2固定输出“2冷剂压缩机”作为小标题写明压缩机类型、国产化或供货来源、设计制造成熟度、运行平稳性及选型合理性。\n'
'3固定输出“3塔类”作为小标题写明主要塔器的塔型、筒体材质以及初步设计与可研在塔型或材质上的一致性与变化情况如存在优化调整需说明调整内容及合理性。\n'
'4最后单独写“后评价认为……”总结性结论需明确判断主要设备的选型、材质与可研是否基本一致深化设计过程中的尺寸或结构优化是否合理以及是否满足装置长周期平稳运行需要。\n'
'\n'
'【写作约束】\n'
'正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”等标签;不得编造设备名称、材质、塔型、专利设备、运行效果或优化原因;证据不足处写“待补充”。',
'2.1.4': '本节必须按“厂址选择及外部条件评价”目的组织内容,对比最终选择厂址与前期各阶段是否一致;如有变化,必须分析变化原因。并结合项目实际运行情况,评价厂址及外部条件是否满足项目要求,判断推荐厂址方案的合理性。\n'
'\n'
'必须严格按以下格式与顺序输出,不得缺项、不得改名、不得调换顺序:\n'
'1固定输出小标题“1前期工作厂址方案对比”并在该标题下写一段或多段连续文字对比最终选择厂址与可研、前评估、初设及相关决策程序中的厂址方案是否一致可结合前期决策程序合规性、初步设计评价、前评估意见采纳落实、厂址结论等证据展开但不得跑题写成程序评价主节。若厂址方案发生变化必须明确写出变化内容、原因及合理性。\n'
'2固定输出小标题“2外部条件满足性评价”并在该标题下写一段或多段连续文字结合项目实际运行情况评价厂址及外部条件是否满足项目要求可按“厂址选择、总图与配套工程、环境保护设施、风险防控、港口、码头、铁路、公路、管道、供水、供电等方面”组织内容。实际有证据的写具体情况无证据的对应项可单独写“待补充”但不得在完整段落末尾附加“待补充”。\n'
'3最后直接单独写“后评价认为……”不得再输出“3后评价结论”或其他总结性小标题。结论必须明确判断厂址选择是否合理、前期工作与厂址方案是否一致、外部条件是否满足项目建设与运行需要对缺少运行数据支撑的外部条件可在结论中指出仍需补充完善。\n'
'\n'
'【写作约束】\n'
'除“1前期工作厂址方案对比”“2外部条件满足性评价”外不得输出其他自拟小标题正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”等标签可使用连续自然段表达不得编造厂址变化原因、交通运输条件、管道条件、供水供电条件或运行适应性结论证据不足处写“待补充”。',
'2.1.5': '本节必须按“总图及系统配套工程评价”目的组织内容,评价项目总图布置、公用工程、储运工程及辅助设施配置是否合理,并区分新建部分与依托部分进行对比分析。\n'
'\n'
'必须严格按以下格式与顺序输出,不得缺项、不得改名、不得调换顺序:\n'
'1固定输出“1总图布置”作为小标题写明可研报告中装置工艺部分、配套单元、储罐、变配电室、机柜室等占地情况说明装置总占地面积、布置位置、是否新征用地并评价总平面布置的合理性。\n'
'2对总图、储运、公用工程及辅助工程等主要建设内容与可行性研究报告进行对比说明是否有变化如有较大变化应说明原因并评价其变化的合理性。\n'
'3在“2系统工程配套”相关文字之后必须输出“表2-5 总图、储运、公用工程及辅助工程对比”,并紧跟表格,用于呈现新建部分对比。\n'
'4随后必须输出“表2-6 储运、公用工程及辅助工程依托对比”,并紧跟表格,用于呈现依托部分对比。\n'
'5最后单独写“后评价认为……”总结性结论明确判断总图布置是否合理、公用工程及辅助设施配置是否满足需要、利用现有设施是否节约投资并取得较好效果。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表不得改写成列表或段落。\n'
'2必须优先使用要素管理中对应“表2-5 总图、储运、公用工程及辅助工程对比”和“表2-6 '
'储运、公用工程及辅助工程依托对比”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表2-5字段含义必须覆盖序号、项目名称、单位、可研报告、初步设计、实际实施、备注。\n'
'4表2-6字段含义必须覆盖序号、依托项目名称、单位、可研报告、初步设计、实际实施、备注。\n'
'5两张表均不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表不得只写正文不写表。\n'
'6表后不输出任何模板性注释"注.表中内容可根据项目实际需要进行增减"等套话),仅保留要素管理中有实质内容的原始注释。\n'
'\n'
'【写作约束】\n'
'正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”等标签;不得编造占地面积、储罐数量、容积、公用工程能力、依托关系或投资效果;证据不足处写“待补充”。',
'2.1.6': '本节必须按“主要技术指标评价”目的组织内容,围绕可研报告、初步设计与实际运行的主要技术指标进行对比,评价初步设计相对可研的优化效果以及实际运行达成情况。\n'
'\n'
'必须严格按以下格式与顺序输出,不得缺项、不得改名、不得调换顺序:\n'
'1先写一段引导语明确“项目可研报告和初步设计主要设计指标对比见下表”并说明通过对比可观察到主要技术指标变化趋势。\n'
'2随后必须输出表题“表2-7 主要设计指标对比表”,并紧跟表格。\n'
'3表格后可写一段分析至少覆盖烷基化油RON、酸耗、能耗等关键指标的变化方向如证据显示初步设计优于可研应说明这是设计优化、细化和深化的结果。\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表不得改写成列表或段落。\n'
'2必须优先使用要素管理中对应“表2-7 主要设计指标对比表”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段必须覆盖序号、指标名称、可研报告、初步设计、实际运行、备注。\n'
'4行项目可包括但不限于原油加工量、综合商品率、全厂柴汽比、全厂新鲜水耗、全厂平均电耗、能耗、其它、常减压蒸馏装置能耗具体按要素管理表格直出可酌情增减。\n'
'5表后注释需保留要素管理中的原始注释若未提取到注释不输出任何模板性注释如“注根据项目的情况可酌情增减指标”等套话仅保留要素管理中有实质内容的原始注释。\n'
'6表2-7不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”等标签不得编造RON、酸耗、能耗及其他技术指标数据证据不足处写“待补充”。',
'2.1.7': '本节必须按“风险分析评价”目的组织内容,围绕可研报告对技术、设备、施工、社会、原料及产品、安全、环保、消防、职业卫生等风险的识别、分析及应对措施进行评价,并结合实际执行情况判断风险防控措施是否有效。\n'
'\n'
'必须严格按以下格式与顺序输出,不得缺项、不得改名、不得调换顺序:\n'
'1正文只允许两段不得再拆分为“1/2/3/4”等子标题、不得再加编号小节。\n'
'2第一段为风险分析主体段综合写可研报告对技术、设备、施工、社会、原料及产品、安全、环保、消防、职业卫生等风险的识别与应对说明工艺技术选择、设备选型、防腐与伴热等措施是否将风险降到可控范围。\n'
'3第二段必须以“后评价认为”开头给出总结性结论明确评价前期风险防控措施在后续设计、施工和生产运行中的贯彻执行情况以及对建设实施和生产运行安全的保障效果。\n'
'4除“后评价认为”外不得输出其他总结性标题如“后评价结论”“风险防控措施评价”“生产运行风险评价”等\n'
'\n'
'【写作约束】\n'
'正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”等标签;不得编造工艺技术来源、风险结论、防腐措施、环保安全执行效果或事故情况;证据不足处写“待补充”;有实质内容时不得在段尾附加“待补充”。',
'2.2': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节2.2.1、2.2.2、2.2.3、2.2.4。',
'2.2.1': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.2.1 编制单位资质及选择方式评价"\n'
'2标题后仅输出一段连续文字围绕“可研报告编制单位资质及选择方式评价”展开不得再拆分小标题或编号条目。\n'
'3该段至少应包含编制单位全称及简称如有、单位沿革或背景、资质等级与资质类别、区域/行业熟悉度、承担项目前期工作的能力评价。\n'
'4如有明确依据可补充选择该单位的原因如不可替代专有技术、区域经验、既有装置熟悉度等结尾需给出“具备承担项目前期工作的能力”或同义评价。\n'
'5证据不足处写“待补充”不得编造单位资质、历史沿革、能力结论或选择依据。',
'2.2.2': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.2.2 编制进度评价"\n'
'2标题后仅输出一段连续文字简要说明可行性研究报告编制历程至少包含关键时间节点如启动、提交、审查、批复等或阶段性进展信息。\n'
'3该段必须明确判断编制进度是否满足项目需要及建设单位要求并给出简要依据。\n'
'4不得拆分为条目或小标题证据不足处写“待补充”不得编造时间线、进度结论或满足性判断。',
'2.2.3': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.2.3 与专项评价的结合情况"\n'
'2标题后仅输出一段连续文字说明可行性研究编制与专项评价结论的结合情况。\n'
'3该段应至少体现专项评价结论在可研中的采纳、衔接或落实情况并给出是否结合充分、是否支撑项目决策的判断。\n'
'4不得拆分为条目或小标题证据不足处写“待补充”不得编造专项评价结论或采纳落实情况。',
'2.2.4': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.2.4 可行性研究报告的质量评价"\n'
'2标题后仅输出一段连续文字结合前期工作的成效评价可行性研究报告的质量。\n'
'3该段应至少体现可研报告在完整性、深度、可实施性、与批复/初设衔接性或风险识别等方面的质量判断,并说明其对后续建设实施的支撑作用。\n'
'4不得拆分为条目或小标题证据不足处写“待补充”不得编造质量结论或前期工作成效。',
'2.3': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.3 前评估工作评价"\n'
'2标题后按连续自然段输出不得拆分小标题或编号条目。\n'
'3第一段写前评估组织单位及资质情况明确其是否满足承担项目可研评估的资质要求。\n'
'4第二段写前评估会议或评估过程时间、组织形式及评审范围至少覆盖原料及产品方案、工艺技术、节能节水、厂址、公用工程、环保安全、投资经济性等方面。\n'
'5第三段写评估主要结论明确项目可行性判断依据。\n'
'6第四段写评估意见与建议数量及落实情况说明可研编制单位是否逐项答复、修改并采纳。\n'
'7最后单独写“后评价认为……”总结性结论评价前评估结论的客观性、公正性以及其对后续设计、建设实施和投产运行的支撑作用。\n'
'8证据不足处写“待补充”不得编造评估单位资质、评审时间、意见数量或落实结论。',
'2.4': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节2.4.1、2.4.2、2.4.3、2.4.4。',
'2.4.1': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.4.1 设计单位资质及选择方式评价"\n'
'2标题后仅输出一段连续文字说明初步设计承担单位及其分工关系如主体装置设计与配套公用工程设计分工\n'
'3该段至少应包含各设计单位全称及简称如有、主要业务范围或专业特长、资质等级或资质类型、与项目匹配性评价。\n'
'4结尾需明确给出资质与选择方式评价结论如“均具有与承担项目相适应的设计资质符合资质要求”或同义表述\n'
'5不得拆分为条目或小标题证据不足处写“待补充”不得编造设计单位资质、分工内容或结论。',
'2.4.2': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.4.2 初步设计进度评价"\n'
'2标题后仅输出一段连续文字按时间顺序写明初步设计关键节点至少包括开始时间、完成时间、审查完成时间、批复时间。\n'
'3该段结尾必须明确判断“初步设计进度满足合同和项目总体部署的工期要求”或同义评价。\n'
'4不得拆分为条目或小标题证据不足处写“待补充”不得编造时间节点、审查时间或进度评价结论。',
'2.4.3': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.4.3 初步设计质量评价"\n'
'2标题后仅输出一段连续文字结合前期工作成效、要素评价内容和采取的设计手段评价初步设计质量。\n'
'3该段至少应覆盖设计内容完整性、设计深度、技术水平、与相关规范/规定的符合性,并明确是否满足要求。\n'
'4可结合优化设计、细化深化、可实施性和运行验证等信息支撑评价结论。\n'
'5不得拆分为条目或小标题证据不足处写“待补充”不得编造设计质量结论、设计手段或规范符合性。',
'2.4.4': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.4.4 初步设计审查工作评价"(与 2.4.3 等同级小节相同:纯文本编号标题,不得使用“###”等 Markdown 标题语法)。\n'
'2标题须单独成行正文另起一行禁止标题与首段正文粘在同一行如「…评价2017年12月……」\n'
'3标题后先输出一段连续文字写明初步设计审查工作的时间、组织单位、审查专业分组、意见数量、设计单位整改落实情况以及未采纳意见数量与说明上报情况如有\n'
'4随后单独写“后评价认为……”总结性结论评价审查意见的客观性、公正性、指导作用并说明未采纳少量意见的合理性判断如有证据\n'
'5不得拆分为条目或小标题证据不足处写“待补充”不得编造审查时间、意见数量、采纳落实情况或结论。',
'2.5': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.5 前期决策程序评价"\n'
'2标题后仅输出一段连续文字从可行性研究、初步设计等环节说明项目是否严格按国家基本建设程序运作。\n'
'3该段应明确写出可研批复前专项评价/专项报告(如环境评价、职业病危害预评价、安全预评价等)的完成与批复情况,并给出“符合项目建设程序规定”或同义结论。\n'
'4不得拆分为条目或小标题证据不足处写“待补充”不得编造批复时间、专项名称或程序合规结论。',
'2.6': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"2.6 前期工作评价结论"\n'
'2标题后仅输出两段连续文字不得拆分为条目、小标题或“主要结论/主要问题/改进建议”三段结构。\n'
'3第一段应围绕前期工作总体过程与关键决策展开至少覆盖项目任务背景如国Ⅵ质量升级目标、可研研究结论与方案选择、工艺技术比选与确定、支持性报告与可研批复情况、初步设计阶段方案优化及其合理性。\n'
'4第二段应给出总体程序与合规性结论明确前期工作是否执行国家和集团基本建设制度、是否按基本建设程序运作、依据是否充分、决策程序是否合规。\n'
'5证据不足处写“待补充”不得编造工艺路线、审批流程、批复结论或合规性判断。',
'3': '按章节标题自然组织内容,围绕证据包先事实后结论,缺失项写“待补充”。',
'3.1': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.1 工程建设管理模式评价"\n'
'2随后固定输出小标题“3.1.1管理模式”,并在该小标题下用连续文字说明项目管理模式(如“业主+监理+EPC”、项目经理负责制、组织架构设置、职责分工及工程建设到投产试运行衔接效果。\n'
'3再固定输出小标题“3.1.2管理效果”并在该小标题下先写一段总体管理成效描述再按顺序固定输出“1加强设计、施工、采购管理确保工程质量”“2项目建设安全管理全面受控”“3进度控制存在一定不足”三项内容。\n'
'4上述三项中前两项重点写质量管理、安全/HSE管理、体系运行与控制成效第3项需如实写进度偏差、滞后原因及对目标进度的影响不得回避问题。\n'
'5不得输出与本节无关的小标题证据不足处写“待补充”不得编造管理模式、事故指标、验收合格率、进度偏差或原因。',
'3.10': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.10 工程竣工验收评价"\n'
'2随后必须按顺序固定输出以下六个小标题并分别展开\n'
' 1消防验收\n'
' 2环境保护验收\n'
' 3安全设施验收\n'
' 4职业病防护设施验收\n'
' 5档案验收\n'
' 6竣工决算审计\n'
'3每个小标题下应写明组织方式政府/企业自行组织)、时间节点、验收或审计结论(是否通过/同意投入使用)。\n'
'4六个小标题后必须再写一段总体情况说明概述专项验收完成情况、竣工验收是否已完成、未完成原因及计划安排如有\n'
'5最后单独写“后评价认为……”总结性结论明确对专项验收组织情况、竣工验收进度滞后问题及改进方向的评价。\n'
'6不得新增无关小标题或调整上述顺序证据不足处写“待补充”不得编造验收时间、验收结论、审计结论、未完成原因或计划节点。',
'3.11': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.11 建设实施评价结论"\n'
'2标题后先写一段总体结论概述建设管理模式、任务完成情况、质量/HSE/投资/目标达成情况。\n'
'3随后固定输出小标题“3.11.1经验和好的做法”,并按顺序固定输出三项:\n'
' 1选用适宜管理模式保证项目顺利实施\n'
' 2加强项目过程控制高质量完成项目实施\n'
' 3生产人员提前介入实现工程建设和投产的有效衔接\n'
'4再固定输出小标题“3.11.2存在问题”,并按顺序固定输出两项:\n'
' 1施工图设计不优化存在浪费和安全隐患\n'
' 2主体装置和配套单元建设不同步进度控制待加强\n'
'5每个分项下均应写连续文字既要有事实依据也要有评价判断不得空写标题。\n'
'6不得新增无关小标题或调整上述顺序证据不足处写“待补充”不得编造合格率、工期差值、问题数量、HSE结论或投资达成情况。',
'3.2': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.2 招投标评价"\n'
'2标题后先输出连续文字说明项目招投标总体执行情况至少覆盖EPC总承包、监理承包商、无损检测承包商、工程质量监督单位等确定方式招标/非招标/谈判)及合规性。\n'
'3对于非招标确定的单位应写明资质能力、选择理由及上报审批手续履行情况。\n'
'4随后必须输出表题“表3-1 项目承包单位情况”,并紧跟表格。\n'
'5表后单独写“后评价认为……”总结性结论明确招投标程序是否符合法律法规及公司管理制度并如实评价合同金额与批复概算关系等不足。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表3-1 项目承包单位情况”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段必须覆盖序号、单元名称、承包单位、合同金额(万元)、是/否招标、资质情况。\n'
'4表3-1不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得拆分为与本节无关的小标题;证据不足处写“待补充”;不得编造承包单位名称、合同金额、招标方式、资质情况或审批结论。',
'3.3': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节3.3.1、3.3.2、3.3.3、3.3.4。',
'3.3.1': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.3.1 与批复后初步设计符合性评价"\n'
'2标题后仅输出一段连续文字围绕施工图设计与批复后初步设计在范围、内容、规模方面的一致性进行评价。\n'
'3该段应至少包含施工图设计与初步设计一致性结论、建设单位技术人员在前期与施工图阶段的参与情况、设计联络/图纸审查/交底会审等过程控制对符合性的保障作用。\n'
'4结尾需明确给出符合性判断如“与初步设计一致符合性较好”或同义表述\n'
'5正文禁止出现任何形式的表格交叉引用不得写“详见表…”“参见表…”“见表…”“如表…所示”等亦不得出现表3-2、表3-3、表3-4、表2-7等表号相关进度与变更数据只在3.3.2、3.3.4以表呈现。\n'
'6不得拆分为条目或小标题证据不足处写“待补充”不得编造一致性结论、参与过程或审查工作。',
'3.3.2': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.3.2 设计进度评价"\n'
'2标题后先输出一段连续文字说明设计管理模式如以设计为龙头的EPC总承包、主体装置与公用工程设计分工、总承包单位对设计-采购-施工衔接的进度协调机制、分批次按节点出图情况以及总体进度满足性判断。\n'
'3该段中应体现施工图设计时间区间如有证据并以“施工图设计进度情况见表3-2”或同义句引出表格。\n'
'4随后必须输出表题“表3-2 施工图设计进度情况”,并紧跟表格。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表3-2 施工图设计进度情况”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段必须覆盖序号、项目、设计单位、合同期限、实际执行情况、备注。\n'
'4表3-2不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得拆分为与本节无关的小标题;证据不足处写“待补充”;不得编造设计分工、进度节点、合同期限或执行情况。',
'3.3.3': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.3.3 施工图设计水平及质量评价"\n'
'2标题后先写一段连续文字说明建设单位设计管理与技术人员参与情况前期方案比选、优化、总图与公用工程依托、设计联络、图纸审查、交底与会审等并评价设计单位对可研评估与基础设计审查意见的采纳情况及设计满足性。\n'
'3再写一段连续文字客观描述后评价现场发现的设计问题与不足如管廊/框架载荷考虑不足、设计保守、投资浪费、通行受阻或碰头等安全隐患),不得回避负面问题。\n'
'4最后单独写“后评价认为……”总结性结论明确设计水平和质量总体判断既要写成效也要写不足及其影响。\n'
'5本节为纯文字评价禁止输出任何表题行或表号清单不得以独立行/列表重复“表3-2…”“表3-3…”“表3-4…”“表2-7…”等禁止表格交叉引用。\n'
'6不得拆分为额外条目或无关小标题证据不足处写“待补充”不得编造现场问题、隐患结论或投资浪费判断。',
'3.3.4': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.3.4 施工图设计变更管理评价"\n'
'2标题后先输出一段连续文字说明设计变更管理机制与控制措施至少覆盖EPC统筹协调、初设审查意见采纳、施工图会审、三维配管设计应用、现场设计代表配合等对减少变更的作用。\n'
'3随后写一段连续文字明确设计变更总量、变更费用、费用占比并以“见表3-3~表3-5”或同义表达引出表格。\n'
'4随后必须依次输出\n'
' - 表3-3 施工图设计变更情况(全厂性项目)\n'
' - 表3-4 施工图设计变更情况(单装置项目)\n'
' - 表3-5 影响投资或工期重(较)大设计变更及原因分析\n'
' 每个表题后必须紧跟对应表格。\n'
'5最后单独写“后评价认为……”总结性结论明确施工图设计变更管理总体评价并对是否存在因设计原因导致的重大投资/工期影响作出判断。\n'
'\n'
'【表格强制要求】\n'
'1三张表必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表3-3”“表3-4”“表3-5”的结构化表若存在对应表格须直出其表头、行项目和单元格内容不得改列名、不得替换成其他表。\n'
'3表3-3字段必须覆盖序号、单元名称、设计变更份数、设计变更金额万元、备注含合计行\n'
'4表3-4字段必须覆盖序号、专业、设计变更份数、设计变更金额万元、备注含合计行\n'
'5表3-5字段必须覆盖序号、单元名称、变更内容、金额万元、原因、备注。\n'
'6三张表均不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得拆分为与本节无关的小标题;证据不足处写“待补充”;不得编造变更份数、费用金额、占比、重大变更结论或原因分析。',
'3.4': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节3.4.1、3.4.2。',
'3.4.1': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.4.1 施工准备评价"\n'
'2标题后先写一段总述说明项目开工前施工准备工作总体情况如招投标组织、承包商选择、项目部成立、人员配备、施工物资准备等\n'
'3随后必须按顺序固定输出以下六个小标题并分别展开\n'
' 1项目部成立、人员配备\n'
' 2完成总体部署并获得批复\n'
' 3开工报告批准\n'
' 4“四通一平”工作完成\n'
' 5EPC总承包管理组织成立\n'
' 6资金已准备到位\n'
'4每个小标题下均应写对应事实与完成情况涉及时间节点或批复信息时应按证据给出。\n'
'5末尾再写一段总结性文字明确项目是否满足工程施工准备基本条件。\n'
'6本节为施工准备文字评价禁止出现任何表格交叉引用与表题清单不得写“详见表…”“参见表…”“见表…”“如表…所示”不得单独成行输出“表3-2/表3-3/表3-4/表2-7”等表号或表题。\n'
'7不得新增无关小标题或调整上述顺序证据不足处写“待补充”不得编造批复时间、资金到位情况、组织机构或准备完成结论。',
'3.4.2': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.4.2 施工计划的执行情况"\n'
'2标题后先写一段连续文字简要说明工程建设进度控制目标与实际执行情况。\n'
'3随后必须输出表题“表3-6 施工进度情况”,并紧跟表格。\n'
'4表后再写一段简要评价如工程进度有较大变化必须分析原因如无明显偏差应明确说明总体执行情况。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表3-6 施工进度情况”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段必须覆盖序号、项目、施工单位、合同期限、实际执行情况、备注。\n'
'4表3-6不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得拆分为与本节无关的小标题;证据不足处写“待补充”;不得编造施工单位、合同期限、执行进度或偏差原因。',
'3.5': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.5 采购工作评价"\n'
'2随后固定输出小标题“1采购工作情况介绍”并在该小标题下写连续文字至少包括采购分工甲供/乙供或建设单位采购与EPC采购边界、采购合同数量与金额、供应商审查与选商把关、入厂检验与质量控制、采购进度控制措施及关键设备采购完成节点。\n'
'3在采购进度或关键设备采购描述后必须以“主要设备及大型机组的采购计划见表3-7”或同义句引出表格。\n'
'4随后必须输出表题“表3-7 采购工作情况”,并紧跟表格。\n'
'5表格后必须保留注释\n'
'1.采购工作评价指甲供主要材料、设备的评价;\n'
' 2.应招标数量指合同数量,应招标金额指合同金额。\n'
'6最后单独写“后评价认为……”总结性结论明确采购质量控制效果、交货进度与对总体建设部署的支撑情况。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表3-7 采购工作情况”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段必须覆盖序号、物资类别名称、采购方式、制造商、供货商、金额万元、未招标原因并包含金额分项单位、数量、单价、小计以及“应招标数量/招标数量率”“应招标金额/招标金额率”等统计行。\n'
'4表3-7不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得拆分为与本节无关的小标题;证据不足处写“待补充”;不得编造采购合同数量、金额、完成节点、检验合格率、招标率或未招标原因。',
'3.6': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.6 工程监理评价"\n'
'2标题后先输出一段连续文字说明监理单位名称、资质等级、承担能力、监理组织配置如总监、专业监理、资料员等及人员配备是否满足合同与现场需求。\n'
'3再输出一段连续文字评价监理单位在进度、质量、安全、投资控制等方面的措施与执行效果可结合监理通知单、暂停令、问题整改闭环、安全事故率等事实进行支撑。\n'
'4结尾需明确给出总体评价结论说明监理工作对建设目标实现的作用。\n'
'5不得拆分为条目或无关小标题证据不足处写“待补充”不得编造监理人数、通知单数量、暂停令数量、事故率或控制成效。',
'3.7': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.7 工程质量评价"\n'
'2随后固定输出小标题“1工程质量责任主体评价”并按顺序展开\n'
' 1明确责任主体\n'
' 2确保质量保证体系建立及有效运行\n'
' 3制定质量计划\n'
' 4质量保证措施\n'
' 5监理承包商、质量检测单位的质量管理\n'
'3再固定输出小标题“2工程质量管控过程评价”并按顺序展开设备安装、管道安装、建筑工程、钢结构/防腐保温/电气仪表、“三查四定和中间交接验收”等过程质量管控情况,需体现质监点、监督记录、通知书及整改闭环情况。\n'
'4再固定输出小标题“3工程质量验收结果评价”明确质量控制点、单位工程、分项工程合格率及投料试车结果等验收结论。\n'
'5最后单独写“后评价认为……”总结性结论明确工程质量责任落实、体系运行、过程整改与质量目标达成情况。\n'
'\n'
'【写作约束】\n'
'不得新增无关小标题或调整上述顺序;证据不足处写“待补充”;不得编造合格率、通知书数量、问题项数量、质监记录份数或质量事故结论。',
'3.8': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.8 HSE管理评价"\n'
'2随后固定输出小标题“1建立HSE管理组织强化责任落实”并在该小标题下结合证据写具体做法与执行效果。\n'
'3再固定输出小标题“2落实HSE管理制度强化日常安全监督检查”并在该小标题下结合证据写具体做法与执行效果。\n'
'4再固定输出小标题“3加强承包商管理提升安全管理意识”并在该小标题下结合证据写具体做法与执行效果。\n'
'5再固定输出小标题“4强化HSE过程管控确保全过程受控”并在该小标题下结合证据写具体做法与执行效果。\n'
'6各小标题下应结合证据写具体做法与执行效果可覆盖HSE组织设置、岗位责任、属地管理、制度修订、监督检查、教育培训、JSA活动、作业许可、隐患排查、违约处理等内容。\n'
'7结尾需单独写一段总体成效结论明确是否实现HSE目标如“零事故、零伤害、零污染”及安全工时等结果如有证据\n'
'\n'
'【写作约束】\n'
'四个固定小节标题的输出体例须与上一节“3.7 工程质量评价”中“1工程质量责任主体评价”“2工程质量管控过程评价”“3工程质量验收结果评价”等小节标题保持一致单独成行行首两个全角空格缩进纯文本编号体与正文同字号、不得整行加粗禁止使用“##”“###”以及成对的“**…**”等 Markdown 标题或强调语法把小节标题做成副标题样式。\n'
'不得新增无关小标题或调整上述顺序;证据不足处写“待补充”,不得编造培训次数/人数、安全工时、事故结论或HSE目标达成情况。',
'3.9': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"3.9 三查四定及中间交接"\n'
'2标题后仅输出一段连续文字说明“三查四定”和中间交接验收监督检查情况。\n'
'3该段应至少包含组织时间如有、牵头单位与参与专业、问题发现数量、承包商整改销项机制、监理与业主联合复核确认情况。\n'
'4不得拆分为条目或无关小标题证据不足处写“待补充”不得编造问题数量、整改闭环结论或参与单位。',
'4': '按章节标题自然组织内容,围绕证据包先事实后结论,缺失项写“待补充”。',
'4.1': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"4.1 生产准备评价"\n'
'2随后必须按顺序固定输出以下五个小标题并分别展开\n'
' 4.1.1投产管理组织机构成立、生产人员配备情况\n'
' 4.1.2生产人员培训\n'
' 4.1.3岗位规章制度、操作规程、事故应急预案情况\n'
' 4.1.4原料、燃料、动力的供给情况\n'
' 4.1.5环保、消防、安全及职业卫生等方面的批准文件\n'
'3在4.1.1中应写明生产准备组织设置、人员配置、职责分工及是否满足投产需求可体现“三查四定”、中交验收、开工准备、HSE管理、试生产审批等职责。\n'
'4在4.1.2中应按阶段写培训安排、外部学习/现场培训、考核与持证上岗情况,并给出能力达标结论。\n'
'5在4.1.3中应写明试车方案、操作规程、制度体系和应急预案的建立与执行情况,明确其对试车与运行的支撑作用。\n'
'6在4.1.4中应分“原料/辅助材料供给”和“公用工程供应”描述,明确供给来源、保障措施及满足性判断。\n'
'7在4.1.5中应列出环保、消防、安全、职业卫生等批准/审查文件及关键信息(如文号、时间),证据不足处写“待补充”。\n'
'8最后单独写“后评价认为……”总结性结论明确生产准备条件是否充分可靠、是否为投产试运行奠定基础。\n'
'9不得新增无关小标题或调整上述顺序不得编造人员数量、培训批次、文号时间、物料来源、公用工程供给或准备充分性结论。',
'4.2': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"4.2 联合试运与试生产情况评价"\n'
'2随后必须按顺序固定输出以下三个小标题并分别展开\n'
' 4.2.1总体试车安排\n'
' 4.2.2投料试车情况\n'
' 4.2.3出现的问题及解决措施\n'
'3在4.2.1中应写明试车组织机制、联动试车安排、投料组织逻辑、物料供给方式和产品质量控制要求。\n'
'4在4.2.2中应按阶段描述试车过程(如三查四定/单机与中交、联动试车、投料试车),并分别写烷基化装置与废酸再生单元的关键时间节点和结果。\n'
'5在4.2.3中应按问题-措施闭环方式描述问题,至少覆盖问题现象、原因或适配不足、整改措施与整改结果;不得只写问题不写处置。\n'
'6最后单独写“后评价认为……”总结性结论明确联合试运与试生产组织成效、一次投产情况、问题责任归属及改进判断。\n'
'7不得新增无关小标题或调整上述顺序证据不足处写“待补充”不得编造试车时间节点、产品合格结论、问题数量、责任归属或整改效果。',
'4.3': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节4.3.1、4.3.2、4.3.3、4.3.4、4.3.5、4.3.6。',
'4.3.1': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"4.3.1 原料供应评价"\n'
'2标题后先写一段连续文字说明装置原料来源如上游MTBE、加氢裂化、轻烃回收等及在上游正常生产情况下的供应稳定性与对装置平稳运行的支撑作用。\n'
'3再写一段连续文字分析原料组成与产量控制关系如碳四烯烃与异丁烷配比、异丁烷补充来源、关键控制因素并结合装置负荷率与未满负荷原因进行评价。\n'
'4不得拆分为条目或无关小标题证据不足处写“待补充”不得编造来源装置、负荷率、原料组成变化或原因判断。',
'4.3.2': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"4.3.2 生产运行总体情况评价"\n'
'2随后必须按顺序固定输出以下三个小标题并分别展开\n'
' 1运行负荷逐步提高\n'
' 2精细管理合理扩大原料来源\n'
' 3优化运行装置能耗逐步降低\n'
'3在“1运行负荷逐步提高”中应写明投产以来装置运行天数、产量、负荷率及关键产品质量指标达成情况如有证据\n'
'4在“2精细管理合理扩大原料来源”中应写明原料受限背景、工艺优化措施如引入替代原料、试用过程中的问题与调整、以及对产量和创效能力的影响。\n'
'5在“3优化运行装置能耗逐步降低”中应写明能耗优化措施如酸烃比、异丁烷循环量、分馏压力、压缩机运行优化等及优化前后能耗变化结果。\n'
'6随后必须输出“后评价认为……”总结性结论明确装置运行负荷、原料优化与能耗改善的总体评价。\n'
'7最后必须输出表题“表4-1 投产以来运行周期统计表”,并紧跟表格。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表4-1 投产以来运行周期统计表”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段必须覆盖序号、装置名称、本周期开工日期、本周期运行时间、本周期非计划停工次数、本周期非计划停工时数、原因简要分析。\n'
'4表4-1不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得新增无关小标题或调整上述顺序;证据不足处写“待补充”;不得编造负荷率、产量、原料加工量、增产量、能耗值或优化效果。',
'4.3.3': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"4.3.3 达标评价"\n'
'2随后必须按顺序固定输出以下三个小标题并分别展开\n'
' 1) 标定工作评价\n'
' 2) 主要装置达标评价\n'
' 3) 全厂达标评价\n'
'3在“1) 标定工作评价”中应写明标定组织方式、方案审定流程、参与部门、标定时间及是否按规范执行。\n'
'4在“2) 主要装置达标评价”中应基于设计值、标定值、实际值进行对比评价;如存在较大变化,必须分析原因。\n'
'5在“2) 主要装置达标评价”内容后必须输出表题“表4-2 烷基化装置运行分析表(考核时间:×年×月×日)”,并紧跟表格。\n'
'6在“3) 全厂达标评价”中应针对全厂性项目,使用全年实际运行数据与设计值对比,评价全厂达标情况;如差距较大,应分析原因。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表4-2 烷基化装置运行分析表(考核时间:×年×月×日)”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段必须覆盖序号、项目、单位、设计值、标定值、实际值、备注。\n'
'4表内行项目应覆盖生产能力、主要原材料、主要产品产量、公用工程消耗水/蒸汽/电/燃料气)、综合能耗、现金加工成本、单位毛利等(按要素管理表格直出,可增减)。\n'
'5表后不输出任何模板性注释如“注表中内容可根据项目不同进行增减”等套话仅保留要素管理中有实质内容的原始注释。\n'
'6本节要求的表4-2不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'7表4-2 在全节仅允许出现一次必须在「2) 主要装置达标评价」对应文字之后输出表题与表格不得在「3) 全厂达标评价」中再次输出表4-2也不得重复输出同一张烷基化装置运行分析数据表。\n'
'\n'
'【写作约束】\n'
'不得新增无关小标题或调整上述顺序;证据不足处写“待补充”;不得编造标定时间、达标结论、对比差异或原因分析。',
'4.3.4': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"4.3.4 生产工艺技术评价"\n'
'2随后必须按顺序固定输出两个小标题并分别展开\n'
' 1烷基化工艺技术\n'
' 2废酸再生工艺技术\n'
'3在“1烷基化工艺技术”中应写明工艺路线、关键设备/关键机理、运行特征(如选择性、酸耗、可靠性、操作灵活性)及后评价对技术适配性的判断;可包含与同类工艺的横向比较(有证据时)。\n'
'4在“2废酸再生工艺技术”中应写明工艺来源与核心指标如硫回收率、热回收、尾气排放、产品硫酸浓度等并如实评价其与常规工艺相比的优缺点如流程复杂度、运行复杂度、投资水平等\n'
'5两部分均需体现“技术优势 + 局限性/代价”的平衡评价,不得只写优点不写不足。\n'
'6不得新增无关小标题或调整上述顺序证据不足处写“待补充”不得编造工艺来源、技术参数、比较结论或投资高低判断。',
'4.3.5': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"4.3.5 设备运行评价"\n'
'2标题后先写一段总体设备运行情况至少包括设备总量及分类如反应器、塔类、容器、换热、压缩机、空冷器、机泵等和投产至后评价时点的总体运行结论。\n'
'3随后按顺序固定输出两个小标题并分别展开\n'
' 1静设备方面\n'
' 2动设备方面\n'
'4在“1静设备方面”中应基于运行参数与设计参数对比评价塔器/容器等静设备工况与长周期运行适应性。\n'
'5在“2动设备方面”中应写明压缩机、机泵、阀门等关键动设备运行情况以及开工初期问题、整改措施与整改结果如材质更换、新增泵、安装方式调整、阀杆修复等\n'
'6结尾应给出总体设备运行评价如运行平稳、调节正常、满足长周期/安全运行要求等),若存在未完全满足项应如实写明。\n'
'7不得新增无关小标题或调整上述顺序证据不足处写“待补充”不得编造设备数量、故障问题、整改措施或运行结论。',
'4.3.6': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"4.3.6 公用工程及辅助设施合理性评价"\n'
'2标题后先写一段连续文字结合实际生产及标定结果验证公用工程及辅助设施是否满足生产运行需要。\n'
'3若存在问题必须再写一段连续文字明确问题表现、影响及整改措施或优化建议若无明显问题应明确说明总体满足性结论。\n'
'4不得拆分为无关小标题或条目证据不足处写“待补充”不得编造标定结果、公用工程能力、设施问题或整改建议。',
'4.4': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"4.4 生产运行评价结论"\n'
'2标题后先写两段总体结论\n'
' - 第一段聚焦生产准备、试车投产、设备与公用工程保障、达标结果及预期目标实现情况;\n'
' - 第二段聚焦投产以来原料供应、负荷变化、能耗变化、优化运行与创效提升情况。\n'
'3随后固定输出小标题“1主要经验”并按顺序固定输出两项\n'
' 1生产运行精细管理、合理优化实现提质增效\n'
' 2结合生产实际情况优化运行有效降低装置能耗\n'
'4再固定输出小标题“2存在问题”并按顺序固定输出两项\n'
' 1考核标定不规范\n'
' 2装置运行酸耗偏高\n'
'5各分项下均应写连续文字明确事实、原因和影响不得只列标题。\n'
'6不得新增无关小标题或调整上述顺序证据不足处写“待补充”不得编造负荷率、能耗值、酸耗值、标定时间或同类对标结论。',
'5': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出章标题"5 投资与经济效益评价"仅此一行与第1章「1 项目概况」同一编号体例勿写「第5章」勿使用 Markdown「#」前缀)。\n'
'2标题后空一行再写一段连续文字24句概括本章将依次从主要经济指标实现程度、投资与执行情况、经济效益、不确定性分析到本章结论展开仅可使用证据包缺失处写“待补充”。\n'
'3不得输出 5.15.5 各小节正文或表格(各小节由后续章节单独生成)。\n'
'4不得新增其他小标题或条目。',
'5.1': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"5.1 主要经济指标实现程度评价"\n'
'2标题后先写一段连续文字明确后评价时点如2020年12月31日下项目效益测算结果与可研报告对比情况。\n'
'3该段应至少写出税后财务内部收益率后评价值与可研值及差值判断并给出“项目效益目标实现程度较好”或同义结论。\n'
'4段末需以“见表5-1”或同义表达引出表格。\n'
'5随后必须输出表题“表5-1 主要经济指标对比表”,并紧跟表格。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表5-1 主要经济指标对比表”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段必须覆盖序号、项目名称、单位、可研值、后评价值、差值、比例%)、备注,并保留“(1)/(2)/(2)-(1)”等分栏信息(如要素管理中存在)。\n'
'4行项目应覆盖项目报批总投资含建设投资、建设期利息、铺底流动资金、年均营业收入、年均总成本费用、年均流转税金及附加、年均利润总额、年均所得税金、年均税后利润、项目投资内部收益率、项目投资财务净现值、项目静态投资回收期。\n'
'5表5-1不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得新增无关小标题证据不足处写“待补充”不得编造IRR、净现值、回收期、投资金额、差值或比例。',
'5.2': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节5.2.1、5.2.2、5.2.3、5.2.4。',
'5.2.1': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"5.2.1 投资控制及变动原因分析"\n'
'2标题后先写一段连续文字说明可研批复投资、初设批复概算、竣工决算审计投资三者对比明确与可研/初设相比的增减额及比例,并给出投资控制评价判断。\n'
'3再写一段连续文字说明投资变动主要原因分析及调概推进情况如有证据\n'
'4随后必须依次输出\n'
' - 表5-2 投资变动情况表(单位:万元、万美元)\n'
' - 表5-3 工程费用变动情况表(万元、万美元)\n'
' 每个表题后必须紧跟对应表格。\n'
'5表后应补充工程费用及其他费用、建设期利息等变动原因分析有证据时并说明超支/结余的主要因素。\n'
'\n'
'【表格强制要求】\n'
'1两张表必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表5-2”“表5-3”的结构化表若存在对应表格须直出其表头、行项目和单元格内容不得改列名、不得替换成其他表。\n'
'3表5-2应保留投资估算/初设概算/竣工决算及“决算与估算比较”“决算与概算比较”的差额、比例分栏结构,并保留批准单位/批准文号等信息行(如有)。\n'
'4表5-3应保留工程费用分解结构设备购置费、安装工程费、建筑工程费等及比较分栏结构并保留“其中外汇”等信息行如有\n'
'5表5-2、表5-3均不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得新增无关小标题;证据不足处写“待补充”;不得编造批复文号、投资金额、增减比例、超支结余原因或调概进展。',
'5.2.2': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"5.2.2 投资水平分析"\n'
'2标题后仅输出一段连续文字围绕单位加工能力投资或单位投资开展对标分析。\n'
'3该段至少应包含本项目烷基化装置单位加工能力投资值、与可比装置如吉林石化、锦州石化、兰州石化等对比结果、偏高/偏低判断;如有废酸单元投资数据,也应给出单位投资值及高低判断。\n'
'4不得拆分为条目或小标题证据不足处写“待补充”不得编造单位投资数值、对标对象或高低结论。',
'5.2.3': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"5.2.3 资金来源及到位评价"\n'
'2标题后仅输出一段连续文字说明可研阶段资金来源结构如企业自筹与债务资金比例与实际建设期资金来源差异。\n'
'3该段应写明投资计划下达金额与时间范围、资金到位及时性、资金使用合规性并明确是否存在转移、侵占、挪用或损失浪费问题。\n'
'4不得拆分为条目或小标题证据不足处写“待补充”不得编造资金比例、计划金额、到位情况或合规性结论。',
'5.2.4': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"5.2.4 投资控制的经验和教训"\n'
'2标题后仅输出一段连续文字结合初步设计概算批复值与竣工决算审计值的差异归纳投资控制方面的经验和教训。\n'
'3该段应至少体现超概金额与比例、主要影响因素如漏项、单方造价偏低、主材费偏低、工程量增加、材料设备涨价、设计变更、现场签证等以及对前期工作和投资控制能力的反思。\n'
'4结尾需提出改进方向如梳理流程、强化费用控制、合理确定并有效控制投资\n'
'5不得拆分为条目或小标题证据不足处写“待补充”不得编造超概金额比例、影响因素或改进结论。',
'5.3': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节5.3.1、5.3.2。',
'5.3.1': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"5.3.1 项目投产以来生产经营及效益状况"\n'
'2标题后先写投产以来各年度如2019、2020的运行天数、加工量、主要产品产量、平均负荷率、产品率、税后利润等核心经营数据对比描述。\n'
'3随后必须输出表题“表5-4 生产经营及效益情况对比表”,并紧跟表格。\n'
'4表后必须按顺序固定输出三段分析\n'
' 1营业收入变动分析\n'
' 2总成本费用变动分析\n'
' 3税后利润变动分析\n'
'5三段分析中应体现与可研值的对比关系说明增减幅度及主要原因。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表5-4 生产经营及效益情况对比表”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段应覆盖项目、单位、分年度可研报告值/实际值/增减(%)对比结构。\n'
'4行项目应覆盖运行情况、主要原料价格、主要产品价格、主要产品年产量、主要产品年销售量、主要原料和公用工程消耗量、主要经济指标营业收入、成本费用、利润总额、税后利润按要素管理表格直出可增减\n'
'5表5-4不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得新增无关小标题或调整上述顺序;证据不足处写“待补充”;不得编造运行天数、负荷率、收入成本、利润或变动原因。',
'5.3.2': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"5.3.2 项目经济效益后评价"\n'
'2随后必须按顺序固定输出以下六个小标题并分别展开\n'
' 1计算范围及评价方法\n'
' 2主要生产经营指标\n'
' 3总成本费用\n'
' 4营业收入\n'
' 5销售税金及附加\n'
' 6盈利能力分析\n'
'3在“1计算范围及评价方法”中应写明与可研一致的评价口径、增量法、评价期、基准收益率、实绩与预测口径、价格体系分段假设等关键参数。\n'
'4在“2主要生产经营指标”中必须以“见表5-5”或同义表达引出并输出“表5-5 主要生产经营指标”。\n'
'5在“3总成本费用”中应按分项说明测算口径原料产品价格、工资福利、折旧、修理费、其他制造费、摊销、安全生产费、安保基金、财务费用、营业费用等\n'
'6在“4营业收入”中应说明收入计算逻辑、价格选取与贴水假设并给出评价期关键结果有证据时\n'
'7在“5销售税金及附加”中应说明消费税、增值税、城建税、教育费附加等计税依据与税率口径。\n'
'8在“6盈利能力分析”中应至少包含IRR、NPV、投资回收期与可研对比结论并以“填写表5-6”或同义表达引出并输出“表5-6 不同因素变化对项目内部收益率的影响”。\n'
'9在“6盈利能力分析”末尾需补一句后评价财务报表见附表3~附表8如有证据不得遗漏该句仅为文字引用本节不得输出附表3附表8任一张的 Markdown 表格(附表由全书「附表」区汇总)。\n'
'\n'
'【表格强制要求】\n'
'1表5-5、表5-6必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表5-5”“表5-6”的结构化表若存在对应表格须直出其表头、行项目和单元格内容不得改列名、不得替换成其他表。\n'
'3表5-5应保留“后评价时点前实际值/后评价时点后预测值”的分年列结构表5-6应保留“财务内部收益率、变化幅度、占比”列结构。\n'
'4表5-5、表5-6均不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得新增无关小标题或调整上述顺序证据不足处写“待补充”不得编造价格体系、税率、负荷率、IRR/NPV/回收期、附表引用或敏感性分析结论除表5-5、表5-6外不得输出任何「附表」类 Markdown 表尤其禁止附表8',
'5.4': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"5.4 不确定性分析"\n'
'2标题后先写一段连续文字说明后评价时影响项目财务效益的主要不确定因素至少包括生产负荷、产品价格、原材料价格等\n'
'3再写一段连续文字说明定量分析方法以项目投资财务内部收益率达到基准收益率为约束计算相关因素的临界点%)或临界值。\n'
'4随后必须输出表题“表5-7 内部收益率为基准收益率时不确定因素临界点或临界值”,并紧跟表格。\n'
'5表后再写一段连续文字开展定性分析判断不确定因素可能变化趋势、潜在风险及风险应对对策减少风险/规避风险)。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表5-7 内部收益率为基准收益率时不确定因素临界点或临界值”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段必须覆盖序号、项目、单位、数值、备注行项目至少覆盖生产负荷、产品价格、主要原材料价格、其它。\n'
'4表5-7不得省略如要素管理中未命中对应表格也必须按模板字段输出占位表。\n'
'\n'
'【写作约束】\n'
'不得新增无关小标题;证据不足处写“待补充”;不得编造临界点、临界值、变化趋势或风险对策。',
'5.5': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"5.5 投资与经济效益评价结论"\n'
'2标题后先写一段连续文字聚焦投资控制结论至少包含竣工决算与可研估算/初设概算的差异金额与比例、投资控制是否有效、以及与同类项目单位工程费投资水平对比判断。\n'
'3再写一段连续文字聚焦经济效益结论明确评价方法与可研一致并给出税后财务内部收益率与可研值对比、差值及是否实现预期效益目标。\n'
'4最后写一段综合结论说明在加工负荷、价格体系变化条件下盈利能力表现并分析主要原因如收率变化、原料与产品价格关系等\n'
'5不得拆分为条目或无关小标题证据不足处写“待补充”不得编造投资差异、IRR数值、同类对标结论或原因分析。',
'6': '按章节标题自然组织内容,围绕证据包先事实后结论,缺失项写“待补充”。',
'6.1': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节6.1.1、6.1.2、6.1.3、6.1.4、6.1.5。',
'6.1.1': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"6.1.1 环境影响评价"\n'
'2标题后先写1段总体评价内容聚焦是否落实国家/地方环保法律法规与环评批复要求、环保设施“三同时”落实情况、环保设施是否满足生产需要、污染物总量控制是否在地方指标以内。\n'
'3固定输出小节标题"6.1.1.1 环保措施"并在该小节下按顺序固定输出以下5个条目标题与正文\n'
' 1废水处理措施\n'
' 2废气处理措施\n'
' 3固体废物处理措施\n'
' 4噪声处理措施\n'
' 5环境风险防范措施\n'
' 每个条目均需结合项目实际设施、处理路径、依托系统或管理制度进行描述。\n'
'4固定输出小节标题"6.1.1.2 效果及影响"先写1句监测说明再按顺序固定输出以下3个条目标题与正文\n'
' 1废气监测结果\n'
' 2废水监测结果\n'
' 3噪声监测结果\n'
' 各条目需说明监测结论与执行标准符合性(如有标准名称可写明)。\n'
'5末尾必须以“后评价认为”起1段结论综合评价环保措施落实情况、废气废水噪声达标情况、固体废物处置与环境总体影响。\n'
'【写作约束】\n'
'不得新增无关小标题;不得改变上述标题与编号顺序;不得把固定条目合并或拆分;证据不足处写“待补充”,不得编造监测结果、达标结论或法规标准。',
'6.1.2': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"6.1.2 安全影响评价"\n'
'2标题后先写1段总体评价内容聚焦是否符合相关规划要求设计与施工是否执行国家法律法规、规章及技术标准配套安全设施是否落实“三同时”同时设计、同时施工、同时投入生产和使用\n'
'3固定输出小节标题"6.1.2.1 安全风险因素",写项目生产运行中的主要风险因素,至少覆盖:易燃易爆介质风险、火灾爆炸风险、腐蚀危害、噪声、机械伤害等。\n'
'4固定输出小节标题"6.1.2.2 防范措施"先写1句引导语“为将安全风险降到最低采取的主要防范措施再按顺序固定输出3个条目\n'
' 1设计、施工过程中依法合规执行要求\n'
' 2落实安全评价及安全设施设计专篇措施并落实“三同时”\n'
' 3报警、联锁、快速切断等检测控制仪表与联锁设施投用情况。\n'
'5固定输出小节标题"6.1.2.3 效果及影响",写建设期与投产后的安全运行效果,至少包含:是否发生安全事故/人身伤害事故、运行平稳性、对周边单位与居民影响。\n'
'6末尾必须以“后评价认为”起1段结论综合评价危险因素可控性、措施有效性及项目整体安全影响。\n'
'【写作约束】\n'
'不得新增无关小标题;不得改变上述标题与编号顺序;不得把固定条目合并或拆分;证据不足处写“待补充”,不得编造事故情况、联锁投用率或安全结论。',
'6.1.3': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"6.1.3 科技进步影响"\n'
'2标题后写第1段简要评价项目对科技进步的推动作用至少覆盖\n'
' 对技术开发、技术创新、技术改造、技术引进的作用;\n'
' 对高新技术产业化、商品化和国际化的作用;\n'
' 对国家和地方科技进步的推动作用。\n'
'3紧接着写第2段从项目应用的引进技术、国产技术、自有技术等方面总结项目对企业、公司、国家科技发展和技术推广的影响。\n'
'【写作约束】\n'
'全文仅保留标题+两段正文,不得新增小标题或条目;不得改写为多级结构;证据不足处写“待补充”,不得编造技术来源、推广范围或科技成效。',
'6.1.4': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"6.1.4 项目社会影响评价"\n'
'2标题后写第1段围绕项目社会效益进行评价至少覆盖\n'
' 项目与国家及地方环保优先、油品质量提升政策导向的符合性;\n'
' 对汽油品质改善、高标号汽油产量提升、区域机动车排放降低与空气质量改善的作用;\n'
' 对当地及国家经济发展、社会稳定的促进作用。\n'
'3紧接着写第2段围绕项目实施后的成效至少覆盖\n'
' 全厂汽油质量是否满足国VI B相关指标要求\n'
' 成品油质量升级任务完成情况;\n'
' 对城市机动车尾气污染改善的环境效益判断。\n'
'【写作约束】\n'
'全文仅保留标题+两段正文,不得新增小标题或条目;不得改写为多级结构;证据不足处写“待补充”,不得编造油品指标、政策符合性或社会环境效益结论。',
'6.1.5': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"6.1.5 项目影响评价结论"\n'
'2标题后写第1段聚焦风险敏感性与管理要求至少覆盖\n'
' 炼化项目高温高压、易燃易爆/有毒有害介质等风险特征;\n'
' 项目区位敏感性与事故/环保事件潜在社会关注度;\n'
' 企业责任意识、危机意识及落实法律法规、主管部门要求、“三同时”和“环保优先”理念的情况。\n'
'3紧接着写第2段聚焦综合影响结论至少覆盖\n'
' 项目选址与总体规划符合性、清洁生产符合性、污染物达标与总量控制、事故防范与环境风险可接受性;\n'
' 项目在选址、总图、工艺技术、设备设施方面对国家法规和技术标准的符合性;\n'
' 项目投产后对油品质量升级如国VI标准达成与区域清洁油品供应、机动车尾气污染改善的环境效益。\n'
'【写作约束】\n'
'全文仅保留标题+两段正文,不得新增小标题或条目;不得改写为多级结构;证据不足处写“待补充”,不得编造合规性、风险可接受性、油品标准达成或环境效益结论。',
'6.2': '本节必须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节6.2.1、6.2.2、6.2.3、6.2.4。',
'6.2.1': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"6.2.1 资源分析"\n'
'2标题后仅输出1段连续正文围绕资源保障与持续性进行分析至少覆盖\n'
' 原料来源构成(如醚后碳四、饱和液化气等);\n'
' 投产初期负荷与一次加工负荷关联影响;\n'
' 通过上游优化及原料补充(如醚后碳五)后的负荷提升情况;\n'
' 后评价时点运行负荷状态及后续原料稳定供应判断;\n'
' 项目发展持续性结论。\n'
'【写作约束】\n'
'全文仅保留标题+1段正文不得新增小标题、条目或表格不得改写为多级结构证据不足处写“待补充”不得编造原料来源、负荷数据或持续性结论。',
'6.2.2': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"6.2.2 产品分析"\n'
'2标题后写第1段说明清洁汽油发展趋势与政策标准背景至少覆盖低硫、低烯烃、低芳烃、低苯、低蒸汽压方向以及国VI A/国VI B实施时间与要求。\n'
'3紧接着写第2段分析烷基化油的产品属性与价值至少覆盖低芳烃/低苯/低烯烃/低硫/低蒸汽压等品质优势、对国VI '
'B汽油升级作用、高辛烷值带来的高标号汽油增产价值、以及液化气向高附加值产品转化对资源利用率和经济效益的提升。\n'
'4最后写第3段分析区域市场与持续性至少覆盖项目区位与市场需求特征、汽油消费占比、就近销售与成本优势、产品销路保障及项目持续性判断。\n'
'【写作约束】\n'
'全文仅保留标题+3段正文不得新增小标题、条目或表格不得改写为多级结构证据不足处写“待补充”不得编造实施时间、产品指标、市场占比或持续性结论。',
'6.2.3': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"6.2.3 主要技术及经济指标对比"\n'
'2标题后先写1段引导性分析文字说明需通过项目装置规模、能耗、投资及成本、工艺技术及设备等与国内外同类项目对比判断项目运行是否达到同类水平及是否具有竞争优势并明确写出“主要技术经济指标对比表见表6-1”。\n'
'3紧接着必须输出表题"表6-1 装置技术经济指标对比表"\n'
'4表6-1必须输出为Markdown表格优先使用要素管理同名表至少包含以下列项目名称、技术来源、规模(万吨/年)、物耗(Wt)%、能耗(kgEo/t)、产品质量、产品收率Wt%、排名。\n'
'5表后不输出任何模板性注释"注:可根据项目具体情况增减指标"等套话),仅保留要素管理中有实质内容的原始注释。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表6-1 装置技术经济指标对比表”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表头字段须覆盖合同正文所列列名若要素管理表结构略有差异以要素管理为准直出不得省略整张表。\n'
'4表6-1不得省略如要素管理中未命中对应表格也必须按上述列名输出占位表单元格可填“待补充”\n'
'\n'
'【写作约束】\n'
'不得新增无关小标题;不得省略表题、表格或表注;证据不足处写“待补充”;不得编造对标对象数据、排名结果或竞争优势结论。',
'6.2.4': '必须严格按以下格式输出,不得缺项、不得改名:\n'
'1首行固定输出标题"6.2.4 项目持续性评价结论"\n'
'2标题后写第1段聚焦市场与产销持续性至少覆盖\n'
' 项目投产后油品质量升级与国家成品油质量升级任务完成情况;\n'
' 原料来源(上游装置供给)保障情况;\n'
' 成品油销售组织与区域流向(如省内占比)及销路保障;\n'
' 对占领和巩固当地市场的优势判断。\n'
'3紧接着写第2段聚焦政策与运行持续性至少覆盖\n'
' 项目与国家绿色发展、可持续发展产业政策符合性;\n'
' 投产后通过优化措施在加工能力、产品质量、能耗、物耗等指标上的达成情况;\n'
' 项目发展持续性总体结论。\n'
'【写作约束】\n'
'全文仅保留标题+2段正文不得新增小标题、条目或表格不得改写为多级结构证据不足处写“待补充”不得编造省内销售占比、指标达成值或持续性结论。',
'7': '本章为综合评价结论必须基于【前序章节正文第16章】归纳提炼是对前六章内容的总结升华不得脱离前文另起论述。按章节标题组织先事实后结论结论与数据须与前文一致缺失项写“待补充”。',
'7.1': '本节是对第16章的归纳性评价必须基于【前序章节正文】撰写。须使用“事实依据—评价判断—问题与建议”三段式结构。正文不得出现“【事实依据】”“【评价判断】”“【问题与建议】”标题标签。并按顺序完整覆盖下级小节7.1.1、7.1.2。',
'7.1.1': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"7.1.1 总体评价结论"\n'
'2标题后先写1段总体结论聚焦项目定位与总体成效至少覆盖\n'
' 作为国VI汽油质量升级配套项目的作用\n'
' 以醚后碳四等液化气为原料生产高辛烷值调和组分;\n'
' 对解决汽油池质量问题、完成国家质量升级任务、提升高标号汽油产量与附加值、经济效益表现的综合判断。\n'
'3随后按顺序固定输出以下5个条目标题与正文\n'
' 1前期工作规范有效及时完成质量升级\n'
' 2建设实施管理规范满足总体部署要求\n'
' 3投产一次成功主要技术指标达到设计要求\n'
' 4竣工决算投资超批复投资回报实现预期目标\n'
' 5产品适应市场需求发展持续性较好\n'
'4上述5个条目内容至少分别覆盖\n'
' 前期报批与立项及投产节点;\n'
' 管理模式(业主+监理+EPC及建设/HSE/进度成效;\n'
' 生产准备、一次开车成功、长周期运行与关键技术指标达标;\n'
' 竣工决算与批复差异及投资收益率对可研预期比较;\n'
' 市场适应性、国VI需求匹配、运行安全与持续性判断。\n'
'【写作约束】\n'
'须优先依据【前序章节正文第16章】归纳各条目不得与前面章节结论矛盾不得新增无关小标题不得改变上述标题与编号顺序不得合并或拆分5个固定条目证据不足处写“待补充”不得编造时间节点、百分比数据或收益指标。',
'7.1.2': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"7.1.2 成功度评价"\n'
'2标题后先写1段方法说明说明采用要素成功度评价方法基于前文各章评价要点形成项目综合评价结论可写明要素评分与汇总见下表。\n'
'3紧接着输出成功度等级划分说明按以下顺序逐条给出4级标准\n'
' 优秀项目评分≥9分项目的各项指标都已全面实现或超过且在经济上取得了较大效益和影响。\n'
' 良好项目9分评分≥8分项目的大部分指标都已全面实现且在经济上取得了预期效益和影响。\n'
' 中等项目8分评分≥6分项目实现了原定的部分目标且在经济上无明显效益和影响。\n'
' 较差项目评分6分项目实现的目标非常有限且在经济上没有正效益和影响。\n'
'4在上述等级标准之后固定输出表题"表7-1 项目综合评价评分表",并紧跟 Markdown 表格(列名、行键与要素管理一致)。\n'
'5表格之后用12段自然段给出本项目成功度综合评价结论可与前文结论一致、作归纳证据不足处写“待补充”。\n'
'\n'
'【表格强制要求】\n'
'1表格必须直接使用“要素管理”中的表格element_tables/element_cells不得自行新造表不得用正文推断补表。\n'
'2必须优先使用要素管理中对应“表7-1 项目综合评价评分表”的结构化表;若存在对应表格,须直出其表头、行项目和单元格内容,不得改列名、不得替换成其他表。\n'
'3表7-1不得省略如要素管理中未命中对应表格也必须按细则列结构输出占位表单元格可填“待补充”\n'
'\n'
'【写作约束】\n'
'须优先依据【前序章节正文第16章】归纳成功度结论不得新增无关小标题不得省略四级标准、表题与表格表内数值以要素管理直出为准证据不足处写“待补充”不得在正文编造与表内数据相矛盾的评分、综合得分或等级结论。',
'7.2': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"7.2 主要经验"\n'
'2随后固定输出主条目"1生产运行精细化管理实现提质增效"并在其下按顺序固定输出2个分项\n'
' 1合理扩大原料来源提高装置运行负荷\n'
' 2优化生产运行有效降低装置能耗\n'
'3再固定输出主条目"2采用适宜的建设管理模式有效实现建设目标"并在其下按顺序固定输出4个分项\n'
' 1结合企业实际情况和项目特点采用“业主+监理+EPC”管理模式并完成建设任务\n'
' 2发挥EPC统筹管理作用合理交叉设计采购施工并保障工期目标\n'
' 3加强建设过程管理实现质量、安全、环保等事故控制目标\n'
' 4加强属地管理与“三查四定”问题整改保障建设与投产衔接\n'
'4各分项正文需结合项目已发生事实进行归纳允许使用关键数据如负荷、能耗、工期、问题项数支撑经验结论。\n'
'【写作约束】\n'
'须优先依据【前序章节正文第16章】提炼主要经验与前文已述事实一致不得新增无关小标题不得改变上述标题与分项顺序不得合并或拆分固定分项证据不足处写“待补充”不得编造负荷、能耗、工期或整改数量数据。',
'7.3': '必须严格按以下格式与顺序输出,不得缺项、不得改名:\n'
'1首行固定输出标题"7.3 问题与建议"\n'
'2固定输出小节标题"7.3.1主要问题"\n'
'3在“7.3.1主要问题”下按顺序固定输出以下3个主条目及分项\n'
' 1施工图设计不优化存在浪费和安全隐患\n'
' 2投资和进度控制存在不足建设实施管理待加强\n'
' 1主体装置和配套单元建设不同步进度控制存在不足\n'
' 2项目实际投资超批复未能实现项目投资控制预期目标\n'
' 3招标管理待进一步规范\n'
' 3考核标定不规范运行有待进一步优化\n'
' 1考核标定不及时部分指标未按设计值标定\n'
' 2装置运行酸耗偏高生产待进一步优化\n'
'4随后固定输出小节标题"7.3.2对策建议"\n'
'5在“7.3.2对策建议”下按顺序固定输出以下4个主条目及分项\n'
' 1总结推广先进经验提高集团公司同类项目运行水平\n'
' 2合理利用副产品开发新产品进一步实现提质增效\n'
' 3上下游装置一体优化运行实现降本增效\n'
' 1合理配置催化原料、提高负荷、优化运行\n'
' 2研究降低MTBE/轻汽油醚化装置二甲醚生成量\n'
' 3加强烷基化原料预处理降低酸耗与再生负荷\n'
' 4优先利用低温热水伴热\n'
' 4尽快完成竣工验收进入正式生产阶段\n'
'6各条目与分项正文必须结合项目事实与数据展开不得只写标题。\n'
'【写作约束】\n'
'须优先依据【前序章节正文第16章】归纳问题与建议与前文已述问题一致不得新增无关小标题不得改变上述标题、条目与分项顺序不得合并或拆分固定条目证据不足处写“待补充”不得编造时间节点、投资金额比例、酸耗或整改结论。'}
DEFAULT_SECTION_OUTPUT_CONTRACT = '按章节标题自然组织内容,围绕证据包先事实后结论,缺失项写“待补充”。'

View File

@ -0,0 +1,150 @@
"""Default section prompt and example variables used by report templates."""
DEFAULT_SECTION_PROMPT = "按《炼油化工建设项目后评价报告编制细则(修订)》对应章节要求撰写,缺失信息标注“待补充”,禁止编造。"
SECTION_PROMPT_RULES: list[tuple[str, str]] = [
(
"1.1 项目基本情况",
"严格按以下字段顺序输出:项目名称、建设单位、建设地点、建设类型、起止时间、建设内容、建设投资、占地面积。"
"所有事实均须来自证据材料,缺失写“待补充”,禁止编造,禁止复刻示例原文。",
),
(
"1.2 项目决策要点",
"按“项目背景123+ 预期目标(规模/质量/效益)”撰写。"
"证据依据用于内部校验,不在报告正文显示“【证据依据:...】”标记。"
"背景每条先写 24 句书面语再视需要附表;表格须用 Markdown禁止粘贴未对齐的原始文本表。"
"预期目标须从证据归纳:证据中已有产能、产量(万吨/年、辛烷值、国VI、收入、利润等时不得三条目标全写待补充。"
"改扩建项目应补充原装置问题与改造动因。",
),
(
"1.3 项目实施情况",
"说明实际建成内容及与批复方案的差异,给出关键里程碑时间线(立项、批复、开工、中交、投产等)。",
),
(
"1.4 项目运行情况",
"说明投产以来装置运行负荷、产量及主要财务运行情况(营业收入、利润等),并与预期目标对照。",
),
(
"2.1.1 资源与原料评价",
"必须与《模版.doc》中本节结构一致先简述可研原料来源与实际一致性"
"再给出「原料数量及组成对比表」(列含:序号、原料名称、规格、可研/初设/实际各自的「数量(万吨)」「占比(%)」、备注,须有合计行);"
"再给出运行负荷与加工量等对比叙述;"
"再给出「原料性质对比表(醚后碳四)」(列:序号、名称、可研报告、初步设计、实际生产、备注;行至少含密度、硫含量、氮含量等,可按项目增删);"
"最后给出组成/性质变化分析及后评价判断。"
"表格一律使用 Markdown 表头+分隔行,禁止粘贴未对齐的纯文本表。"
"本节第一张主表表题须固定为「表1 原料数量及组成对比表」(与章节输出合同及要素管理默认表题一致,与正文节内表序一致);"
"禁止用安评/专篇中「表2.6-1 原料选择加氢工艺技术对比」等同号异题表替代上述两张模版主表;"
"本节只保留模版主表,不输出附录与“非模版主表”字样。"
"数据来自证据包,缺失填待补充,禁止编造。",
),
(
"2.1.2.1 产品方案评价",
"按“事实依据—评价判断—问题与建议”组织内容,但不要在正文中显示"
"“【事实依据】”“【评价判断】”“【问题与建议】”这三个标题标签;"
"以自然段或编号表达即可。缺失信息写“待补充”,禁止编造。",
),
("2", "对照前期工作细则,评价可研、前评估、初设及决策程序的合规性、完整性与合理性。"),
("3", "对照建设实施细则评价建设管理、招投标、设计、采购、施工、监理、质量、HSE与竣工验收。"),
("4", "对照生产运行细则,评价生产准备、联合试运、达标情况、工艺技术、设备与辅助系统运行效果。"),
("5", "对照投资与经济效益细则,评价投资控制、资金到位、经营效益、后评价测算及不确定性分析。"),
("6", "对照影响与持续性细则,评价环境、安全、科技、社会影响及资源、产品、技术经济竞争力持续性。"),
("7", "给出综合评价结论、成功度评价、主要经验、问题与建议,结论须与前文证据保持一致。"),
]
SECTION_EXAMPLE_RULES: list[tuple[str, str]] = [
(
"1",
"项目名称宁夏石化分公司16万吨/年烷基化装置建设项目独立后评价。"
"建设内容包括16万吨/年烷基化单元、1.5万吨/年废酸再生单元及配套公用工程。"
"批复可研估算22812万元批复初设概算25079万元竣工决算32486万元。"
"烷基化单元2018年11月投产废酸再生单元2020年11月投运。\n"
"---EXAMPLE---\n"
"项目运行情况投产后烷基化油收率保持在86%以上高于设计值81.28%"
"受原油加工负荷影响,阶段性加工负荷与可研预期存在偏差;"
"通过全厂优化运行后烷基化装置加工负荷保持在90%以上,"
"满足国VI汽油质量升级需要。",
),
(
"2",
"前期决策评价项目可研由具备甲级资质单位编制前评估提出57条意见并完成落实。"
"原料来源为MTBE装置醚后碳四来源与实际生产一致"
"产品全部调入汽油系统,由中石油西北销售公司统一销售,市场风险可控。"
"工艺路线采用中石油自主硫酸法烷基化技术并配套P&P湿法废酸再生技术。\n"
"---EXAMPLE---\n"
"前期工作结论:项目在可研、前评估、初设及决策程序方面总体规范,"
"对国VI质量升级任务支撑明确"
"后续应针对专利技术工程化经验不足、概算约束偏紧等问题,"
"在施工图阶段加强工程量校核与投资风险预控。",
),
(
"3",
"建设实施评价:项目采用“业主+监理+E+PC”管理模式。"
"单位工程质量合格率100%HSE总体受控。"
"但施工图设计进度与采购协同不足,废酸再生单元受工艺包与设备整改影响,"
"中交与投产明显滞后。\n"
"---EXAMPLE---\n"
"招采与设计变更情况共发生设计变更118份变更费用约5033万元"
"对进度与投资控制产生不利影响。"
"经验上应优先采用以设计为龙头的EPC协同模式"
"并提前锁定关键设备选材和高温腐蚀工况边界条件。",
),
(
"4",
"生产运行评价烷基化单元2018年11月一次投产成功"
"废酸再生单元经四次整改后于2020年11月投运。"
"运行期主要问题集中在原料杂质波动、局部腐蚀及部分指标偏离设计值,"
"通过优化烷烯比、补酸策略及上游分离精度逐步改进。\n"
"---EXAMPLE---\n"
"达标评价结论:烷基化单元在标定中出现辛烷值、硫含量、酸耗偏差,"
"废酸再生单元处理能力达标但能耗偏高。"
"总体上项目工艺可用、装置可稳态运行,"
"需持续优化操作和设备防腐管理以提升长周期绩效。",
),
(
"5",
"主要经济指标对比示例(宁夏石化项目):\n\n"
"表5-1 主要经济指标对比表\n"
"| 指标 | 单位 | 可研值 | 后评价值 | 差值 |\n"
"| --- | --- | --- | --- | --- |\n"
"| 报批总投资 | 万元 | 22812 | 32486 | +9674 |\n"
"| 年均税后利润 | 万元 | 20652 | 13283 | -7369 |\n"
"| 税后内部收益率 | % | 85.12 | 35.46 | -49.66 |\n"
"| 静态投资回收期 | 年 | 2.29 | 4.38 | +2.09 |\n"
"\n"
"示例结论:项目收益率虽较可研明显下降,但仍高于基准收益率,"
"基本实现效益目标;投资控制偏弱是主要短板。\n"
"---EXAMPLE---\n"
"5.2.2 投资水平分析正文参考勿输出为表5-2表5-2 仅用于 5.2.1 投资变动情况表):\n\n"
"同类烷基化装置单位工程费对标(撰写段落时参考,非模版表号):\n"
"| 项目 | 规模(万吨/年) | 工程费(万元) | 单位造价(元/吨) |\n"
"| --- | --- | --- | --- |\n"
"| 宁夏石化 | 16 | 15159 | 947 |\n"
"| 乌石化 | 20 | 21286 | 1064 |\n"
"| 锦州石化 | 25 | 23401 | 936 |\n"
"| 兰州石化 | 20 | 14377 | 719 |",
),
(
"6",
"影响评价示例:项目落实环评及安全“三同时”,废气、废水、噪声监测达标,"
"投产以来未发生重大安全事故,环境与安全风险总体可控。\n"
"---EXAMPLE---\n"
"持续性评价示例(宁夏石化项目):\n\n"
"表6-1 装置技术经济指标对比表\n"
"| 项目名称 | 技术来源 | 规模(万吨/年) | 物耗(Wt)% | 能耗(kgEo/t) | 产品质量 | 产品收率Wt% | 排名 |\n"
"| --- | --- | --- | --- | --- | --- | --- | --- |\n"
"| 宁夏石化烷基化 | 自主硫酸法烷基化 | 16 | 待补充 | 待补充 | 国VI调和组分 | 86以上 | 待补充 |\n"
"| 同类装置A | … | … | … | … | … | … | … |",
),
(
"7",
"综合结论示例宁夏石化16万吨/年烷基化项目完成了国VI汽油质量升级目标"
"生产运行总体平稳,效益指标虽低于可研预测但仍高于基准收益率。"
"项目综合评分8.62分,评级为“良”。\n"
"---EXAMPLE---\n"
"建议示例:"
"1持续优化自主技术工程化能力重点治理腐蚀与高温环节选材问题"
"2加强设计-采购-施工一体化协同,减少大额变更;"
"3围绕原料品质与上游协同运行进一步提升装置长期经济性。",
),
]

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
fastapi>=0.115.6
uvicorn[standard]>=0.34.0
python-multipart>=0.0.20
pydantic>=2.11
pydantic-settings>=2.7.1
SQLAlchemy>=2.0.36
PyMySQL>=1.1.1
requests>=2.32.3

0
routers/__init__.py Normal file
View File

346
routers/template.py Normal file
View File

@ -0,0 +1,346 @@
"""
routers/template.py
报告模板管理上传文档 远程解析为 Markdown 抽取目录并为每个目录生成声明
创建模板目录 + 声明 按章节拆分正文并入库远程 MySQL
"""
from __future__ import annotations
import asyncio
import logging
import os
import re
import tempfile
import uuid
from datetime import datetime
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
from sqlalchemy import or_
from sqlalchemy.orm import Session
from database import get_db
from database.models import (
ReportSectionReference,
ReportTemplate,
ReportTemplateSection,
)
from schemas.template import (
SectionReferenceItem,
TemplateItem,
TemplateSectionItem,
UploadTemplateResult,
)
from services.template_prompt_mapper import resolve_uploaded_template_prompts
from services.desensitize_service import count_masked_numbers, desensitize_content
from services.file_parse_client import parse_file_to_markdown
from services.section_extractor import (
clamp_text_bytes,
extract_sections_from_text,
normalize_section_key,
parse_section_order,
split_markdown_into_sections,
)
from config import settings
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/templates", tags=["报告模板管理"])
ALLOWED_SUFFIXES = {".doc", ".docx", ".pdf", ".txt", ".md", ".html", ".htm", ".rtf"}
def _clamp_title(value: str | None) -> str:
return str(value or "").strip()[:255]
def _build_description(source_file: str | None) -> str:
sf = str(source_file or "").strip()
base = "通过文件上传导入"
return f"{base}\n来源文件:{sf}" if sf else base
def _find_duplicates(db: Session, filename: str) -> dict:
"""按来源文件名查找已导入的模板与章节范文,用于重复检查。"""
template_ids = [
t.id
for t in db.query(ReportTemplate.id)
.filter(ReportTemplate.description.like(f"%来源文件:{filename}%"))
.all()
]
ref_count = (
db.query(ReportSectionReference)
.filter(ReportSectionReference.source_file == filename)
.count()
)
return {"template_ids": template_ids, "ref_count": ref_count}
def _delete_by_source(db: Session, filename: str, template_ids: list[str]) -> None:
"""删除指定来源文件已导入的模板(含章节)与章节范文。"""
for tid in template_ids:
db.query(ReportTemplateSection).filter(
ReportTemplateSection.template_id == tid
).delete(synchronize_session=False)
db.query(ReportTemplate).filter(ReportTemplate.id == tid).delete(
synchronize_session=False
)
db.query(ReportSectionReference).filter(
ReportSectionReference.source_file == filename
).delete(synchronize_session=False)
db.commit()
def _extract_source_file(description: str | None) -> str | None:
"""从模板描述中解析来源文件名(上传时写入 '来源文件xxx')。"""
m = re.search(r"来源文件\s*[:]\s*(.+)$", str(description or ""))
if not m:
return None
return (m.group(1) or "").strip() or None
def _serialize_template(db: Session, template_id: str) -> TemplateItem:
t = db.query(ReportTemplate).filter(ReportTemplate.id == template_id).first()
if not t:
raise HTTPException(status_code=404, detail="模板不存在")
sections = (
db.query(ReportTemplateSection)
.filter(ReportTemplateSection.template_id == t.id)
.order_by(ReportTemplateSection.section_order.asc())
.all()
)
src = _extract_source_file(t.description)
return TemplateItem(
id=t.id,
name=t.name,
description=t.description,
sourceFile=src,
createdAt=t.created_at.strftime("%Y-%m-%d %H:%M:%S") if t.created_at else None,
updatedAt=t.updated_at.strftime("%Y-%m-%d %H:%M:%S") if t.updated_at else None,
isDefault=t.is_default,
isActive=t.is_active,
sections=[
TemplateSectionItem(
id=s.id,
sectionKey=s.section_key,
sectionTitle=s.section_title,
sectionPrompt=s.section_prompt,
sectionOutputContract=s.section_output_contract,
sectionOrder=s.section_order,
examples=s.examples,
)
for s in sections
],
)
@router.post("/upload", response_model=UploadTemplateResult, summary="上传文档并解析为模板(目录+声明)与章节内容")
async def upload_template_route(
file: UploadFile = File(...),
force: bool = Query(False, description="为 true 时覆盖同名来源文件的已有模板与章节后重新导入"),
db: Session = Depends(get_db),
):
filename = (file.filename or "").strip()
suffix = os.path.splitext(filename)[1].lower()
if suffix not in ALLOWED_SUFFIXES:
raise HTTPException(
status_code=400,
detail=f"不支持的文件格式({suffix or '未知'});支持:{', '.join(sorted(ALLOWED_SUFFIXES))}",
)
# 0) 重复检查同名来源文件已导入则拒绝force=true 则先删除旧数据再重导)
dup = _find_duplicates(db, filename)
if dup["template_ids"] or dup["ref_count"]:
if not force:
raise HTTPException(
status_code=409,
detail=(
f"文件「{filename}」已导入(模板 {len(dup['template_ids'])} 个、"
f"章节 {dup['ref_count']} 条),不可重复入库;"
f"如需覆盖请使用 force=true 重新上传。"
),
)
logger.info(
"重复导入覆盖 | file=%s | 删除旧模板=%s | 旧章节=%s",
filename, len(dup["template_ids"]), dup["ref_count"],
)
_delete_by_source(db, filename, dup["template_ids"])
# 1) 落盘临时文件
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
tmp_path = tmp.name
tmp.write(await file.read())
try:
# 2) 远程解析为 Markdown
try:
markdown = parse_file_to_markdown(tmp_path)
except Exception as e: # noqa: BLE001
raise HTTPException(status_code=400, detail=f"文档解析失败:{e}")
if not markdown or not markdown.strip():
raise HTTPException(status_code=400, detail="解析结果为空")
# 3) 抽取目录
sections = extract_sections_from_text(markdown)
if not sections:
raise HTTPException(status_code=400, detail="未从文档中识别到章节标题(目录)")
# 4) 按标题拆分正文,对每段正文脱敏(去精确数字等);跳过无正文的章节
raw_sections = split_markdown_into_sections(markdown)
max_bytes = int(getattr(settings, "SECTION_CONTENT_MAX_BYTES", 60000) or 60000)
ref_sections: list[dict] = []
masked_total = 0
skipped_empty = 0
truncated = 0
for s in raw_sections:
filtered = desensitize_content(s["content"])
if not filtered.strip():
skipped_empty += 1
continue
masked_total += max(count_masked_numbers(s["content"], filtered), 0)
clamped = clamp_text_bytes(filtered, max_bytes)
if clamped is not filtered and len(clamped) != len(filtered):
truncated += 1
ref_sections.append({**s, "content": clamped})
logger.info(
"解析结果 | md_len=%s | toc=%s | 入库章节=%s | 跳过空正文=%s | 截断超长=%s | 脱敏数字串=%s",
len(markdown), len(sections), len(ref_sections), skipped_empty, truncated, masked_total,
)
# 5) 复刻 eval_report将上传目录匹配默认模板得到每章节 提示词/输出合同/示例
# 放到工作线程执行:内部含并行 LLM 调用,避免阻塞事件循环(上传期间仍可并发处理其它请求)
resolved = await asyncio.to_thread(resolve_uploaded_template_prompts, sections)
logger.info(
"提示词匹配完成 | 章节=%s | 命中提示词=%s",
len(resolved), sum(1 for r in resolved if (r.get("sectionPrompt") or "").strip()),
)
now = datetime.now()
# 6) 创建模板(目录 + 提示词/输出合同/示例)
template = ReportTemplate(
id=uuid.uuid4().hex,
name=os.path.splitext(filename)[0] or "上传模板",
description=_build_description(filename),
is_default=False,
is_active=True,
created_at=now,
updated_at=now,
)
db.add(template)
db.flush()
for i, sec in enumerate(sections):
r = resolved[i] if i < len(resolved) else {}
db.add(
ReportTemplateSection(
id=uuid.uuid4().hex,
template_id=template.id,
section_key=normalize_section_key(sec["sectionKey"], sec["sectionTitle"]),
section_title=_clamp_title(sec["sectionTitle"]),
section_prompt=(r.get("sectionPrompt") or None),
section_output_contract=(r.get("sectionOutputContract") or None),
section_order=i,
examples="",
created_at=now,
updated_at=now,
)
)
# 7) 章节内容入库report_section_references 格式)
saved_refs: list[SectionReferenceItem] = []
for sec in ref_sections:
ref = ReportSectionReference(
id=uuid.uuid4().hex,
template_id=template.id,
source_file=filename,
section_key=sec["section_key"],
section_title=_clamp_title(sec["section_title"]),
section_order=parse_section_order(sec["section_key"]),
content=sec["content"],
created_at=now,
updated_at=now,
)
db.add(ref)
saved_refs.append(
SectionReferenceItem(
id=ref.id,
templateId=ref.template_id,
sourceFile=ref.source_file,
sectionKey=ref.section_key,
sectionTitle=ref.section_title,
sectionOrder=ref.section_order,
contentLength=len(ref.content or ""),
content=ref.content or "",
)
)
db.commit()
logger.info(
"模板上传完成 | file=%s | toc=%s | refs=%s",
filename, len(sections), len(saved_refs),
)
return UploadTemplateResult(
template=_serialize_template(db, template.id),
sourceFile=filename,
markdownLength=len(markdown),
totalSections=len(sections),
totalReferences=len(saved_refs),
references=saved_refs,
parseWarnings=[],
)
except HTTPException:
db.rollback()
raise
except Exception as e: # noqa: BLE001
db.rollback()
raise HTTPException(status_code=400, detail=f"模板创建失败:{e}")
finally:
try:
os.remove(tmp_path)
except OSError:
pass
@router.get("", response_model=list[TemplateItem], summary="获取模板列表")
def list_templates_route(db: Session = Depends(get_db)):
rows = (
db.query(ReportTemplate)
.order_by(ReportTemplate.created_at.desc())
.all()
)
return [_serialize_template(db, r.id) for r in rows]
@router.get("/{template_id}", response_model=TemplateItem, summary="获取模板详情")
def get_template_route(template_id: str, db: Session = Depends(get_db)):
return _serialize_template(db, template_id)
@router.delete("/{template_id}", status_code=204, summary="删除模板(含其来源文件的章节范文)")
def delete_template_route(template_id: str, db: Session = Depends(get_db)):
t = db.query(ReportTemplate).filter(ReportTemplate.id == template_id).first()
if not t:
raise HTTPException(status_code=404, detail="模板不存在")
source_file = _extract_source_file(t.description)
db.query(ReportTemplateSection).filter(
ReportTemplateSection.template_id == t.id
).delete(synchronize_session=False)
db.delete(t)
# 同步删除该模板在 report_section_references 中的章节内容:
# 优先按 template_id 精确删除;同时按 source_file 兜底清理历史数据template_id 为空的旧记录)
conditions = [ReportSectionReference.template_id == t.id]
if source_file:
conditions.append(ReportSectionReference.source_file == source_file)
ref_deleted = (
db.query(ReportSectionReference)
.filter(or_(*conditions))
.delete(synchronize_session=False)
)
db.commit()
logger.info(
"模板删除完成 | template_id=%s | source_file=%s | 删除章节范文=%s",
template_id, source_file, ref_deleted,
)

0
schemas/__init__.py Normal file
View File

51
schemas/template.py Normal file
View File

@ -0,0 +1,51 @@
from __future__ import annotations
from typing import List, Optional
from pydantic import BaseModel
class TemplateSectionItem(BaseModel):
id: str
sectionKey: str
sectionTitle: str
# 复刻 eval_report章节提示词 / 输出合同 / 示例
sectionPrompt: Optional[str] = None
sectionOutputContract: Optional[str] = None
sectionOrder: int = 0
examples: Optional[str] = None
class TemplateItem(BaseModel):
id: str
name: str
description: Optional[str] = None
sourceFile: Optional[str] = None
createdAt: Optional[str] = None
updatedAt: Optional[str] = None
isDefault: bool = False
isActive: bool = True
sections: List[TemplateSectionItem] = []
class SectionReferenceItem(BaseModel):
id: str
templateId: Optional[str] = None
sourceFile: str
sectionKey: str
sectionTitle: str
sectionOrder: int = 0
contentLength: int = 0
content: str = ""
class UploadTemplateResult(BaseModel):
"""上传解析结果:模板(目录 + 声明)+ 入库的章节内容。"""
template: TemplateItem
sourceFile: str
markdownLength: int
totalSections: int
totalReferences: int
references: List[SectionReferenceItem] = []
parseWarnings: List[str] = []

0
services/__init__.py Normal file
View File

View File

@ -0,0 +1,126 @@
"""
services/declaration_service.py
为每个目录章节生成一个"声明"
声明一段说明该章节应写什么结构与约束的撰写指引存入
report_template_sections.section_prompt
优先用 LLM结合章节标题 + 该章节正文生成未配置或失败时
回退到确定性模板保证流程稳定可用
"""
from __future__ import annotations
import logging
import re
from concurrent.futures import ThreadPoolExecutor, as_completed
from config import settings
from services.llm_client import chat_completions_json, llm_configured
logger = logging.getLogger(__name__)
_SYSTEM_PROMPT = (
"你是报告模板专家。任务:阅读给定章节的范文(参考正文),总结这一章应该怎么写,"
"作为后续报告撰写该章节的写作指引。需提炼:①内容要点(写哪些事项);"
"②组织结构(应有的小节/条目顺序);③数据与口径要求(需引用的对比/指标/表格等);"
"④写作约束(先事实后评价、缺失写「待补充」、不得编造)。"
"严格要求:不要输出任何思考过程或解释;只输出 JSON 对象 {\"guide\": \"...\"}"
"guide 为 300 字以内的写作指引纯文本(不含 markdown 标题);"
"范文缺失或过短时,按章节标题给出通用写作指引。"
)
def _strip_number_prefix(title: str) -> str:
t = str(title or "").strip()
t = re.sub(r"^(?:\d+(?:\.\d+)*|[一二三四五六七八九十]+[、.])\s*", "", t).strip()
return t
def _fallback_declaration(section_title: str) -> str:
label = _strip_number_prefix(section_title) or "本章节"
return (
f"本章节为「{label}」。撰写时应紧扣标题主题,先陈述事实与数据,再给出分析与评价;"
f"结构需与标题保持一致,条理清晰、用语规范;"
f"所有结论须有依据,缺失信息写「待补充」,禁止编造。"
)
def _build_user_prompt(section_title: str, content: str) -> str:
body = (content or "").strip()
if len(body) > 2500:
body = body[:2500]
body_block = f"\n\n该章节范文(参考正文,节选):\n```\n{body}\n```" if body else ""
return (
f"章节标题:{section_title}{body_block}\n\n"
f"请根据上述范文,总结该章节应该怎么写,并只返回 JSON{{\"guide\": \"300字以内的写作指引\"}}"
)
def generate_declaration(section_title: str, content: str = "") -> str:
"""根据范文为单个章节生成"怎么写"的写作指引JSON 取 guide自动剔除思考过程"""
use_llm = bool(getattr(settings, "DECLARATION_USE_LLM", True)) and llm_configured()
if not use_llm:
return _fallback_declaration(section_title)
try:
data = chat_completions_json(
system_prompt=_SYSTEM_PROMPT,
user_prompt=_build_user_prompt(section_title, content),
temperature=0.2,
max_tokens=2048,
)
guide = str((data or {}).get("guide") or "").strip()
if guide:
return guide
except Exception as e: # noqa: BLE001 - 兜底,保证主流程不被 LLM 影响
logger.warning("生成章节声明失败,使用兜底模板 | title=%s | err=%s", section_title, e)
return _fallback_declaration(section_title)
def _content_for_section(s: dict, content_by_key: dict[str, str]) -> str:
"""目录键可能是 canonical 形式,优先用标题中的编号前缀去匹配正文。"""
title = str(s.get("sectionTitle") or "")
m = re.match(r"^(\d+(?:\.\d+)*)", title.strip())
num = m.group(1) if m else ""
return content_by_key.get(num, "") or content_by_key.get(str(s.get("sectionKey") or ""), "")
def generate_declarations(sections: list[dict], content_by_key: dict[str, str] | None = None) -> list[str]:
"""
为目录中每个章节并发生成"怎么写"的写作指引基于范文
sections: [{sectionKey, sectionTitle}, ...]
content_by_key: 章节编号/ -> 范文正文用于为指引提供上下文可选
每章一次 LLM 调用多线程并发以打满 GPULLM 为网络 I/O线程下真正并行
"""
content_by_key = content_by_key or {}
tasks = [(str(s.get("sectionTitle") or ""), _content_for_section(s, content_by_key)) for s in sections]
if not tasks:
return []
use_llm = bool(getattr(settings, "DECLARATION_USE_LLM", True)) and llm_configured()
if not use_llm:
return [_fallback_declaration(title) for title, _ in tasks]
max_workers = max(int(getattr(settings, "TEMPLATE_UPLOAD_LLM_MAX_WORKERS", 8) or 8), 1)
results: list[str] = [""] * len(tasks)
if len(tasks) == 1:
results[0] = generate_declaration(*tasks[0])
return results
workers = min(max_workers, len(tasks))
with ThreadPoolExecutor(max_workers=workers) as executor:
future_to_idx = {
executor.submit(generate_declaration, title, content): i
for i, (title, content) in enumerate(tasks)
}
for fut in as_completed(future_to_idx):
idx = future_to_idx[fut]
try:
results[idx] = fut.result()
except Exception as e: # noqa: BLE001
logger.warning("章节声明并发生成失败,使用兜底 | idx=%s | err=%s", idx, e)
results[idx] = _fallback_declaration(tasks[idx][0])
logger.info("章节声明生成 | 章节=%s | 线程=%s", len(tasks), workers)
return results

View File

@ -0,0 +1,80 @@
"""
services/desensitize_service.py
章节内容脱敏把范文正文中的"精确数据"过滤掉得到可复用的模板化内容
规则默认
- 阿拉伯数字串含小数千分位全角数字 占位符默认 "X"
"总投资10.5亿元" "总投资X亿元""2020年3月" "X年X月""85.3%" "X%"
- 标题行 # 开头)整行保留,不动章节编号/标题。
- 行首的列表/枚举序号 "1" "1." "2"保留仅脱敏正文中的数字
- 单位与符号万元/亿元/%// 保留仅去掉其中的精确数值
可通过 config 调整占位符是否脱敏表格数字是否启用
中文数字一二三通常用于序数/层级默认保留
"""
from __future__ import annotations
import logging
import re
from config import settings
logger = logging.getLogger(__name__)
# 阿拉伯数字(含全角)串,允许小数点/千分位分隔
_NUMBER_RE = re.compile(r"[0-9-]+(?:[.,][0-9-]+)*")
# 行首枚举序号1 / 1. / 2 / 2、 等(这些是结构标记,保留)
_LEADING_ENUM_RE = re.compile(r"^(\s*(?:[(]\s*[0-9-]+\s*[)]|[0-9-]+\s*[).、.]))")
_HEADING_RE = re.compile(r"^\s*#{1,6}\s")
_TABLE_ROW_RE = re.compile(r"^\s*\|.*\|\s*$")
_TABLE_SEP_RE = re.compile(r"^\s*\|?[\s:\-|]+\|?\s*$")
def _mask_numbers(segment: str, placeholder: str) -> str:
return _NUMBER_RE.sub(placeholder, segment)
def _desensitize_line(line: str, placeholder: str, mask_table_numbers: bool) -> str:
# 标题行整行保留(不动章节编号/标题)
if _HEADING_RE.match(line):
return line
# 表格行
if _TABLE_ROW_RE.match(line):
if _TABLE_SEP_RE.match(line): # 分隔行 |---|---|
return line
if not mask_table_numbers:
return line
return _mask_numbers(line, placeholder)
# 普通正文:保留行首枚举序号,仅脱敏其余部分
m = _LEADING_ENUM_RE.match(line)
if m:
prefix = m.group(1)
rest = line[len(prefix):]
return prefix + _mask_numbers(rest, placeholder)
return _mask_numbers(line, placeholder)
def desensitize_content(text: str) -> str:
"""对单个章节正文脱敏。未启用时原样返回。"""
if not text:
return text
if not bool(getattr(settings, "DESENSITIZE_ENABLED", True)):
return text
placeholder = str(getattr(settings, "DESENSITIZE_PLACEHOLDER", "X") or "X")
mask_table = bool(getattr(settings, "DESENSITIZE_MASK_TABLE_NUMBERS", True))
lines = text.splitlines()
out = [_desensitize_line(ln, placeholder, mask_table) for ln in lines]
return "\n".join(out)
def count_masked_numbers(original: str, filtered: str) -> int:
"""粗略统计脱敏掉的数字串数量(用于日志)。"""
return len(_NUMBER_RE.findall(original or "")) - len(_NUMBER_RE.findall(filtered or ""))

View File

@ -0,0 +1,194 @@
"""
services/file_parse_client.py
调用远程解析服务默认 http://192.168.4.194:8000/convert
上传文件multipart文件字段默认 "file"并附带 engine=auto 表单字段 返回 Markdown
响应解析JSON 中按 results / md_content / mdcontent / markdown / content
逐层提取若响应非 JSON 则整体作为 Markdown 返回
"""
from __future__ import annotations
import json
import logging
import mimetypes
import time
import uuid
from pathlib import Path
from typing import Any
from urllib import error as urlerror
from urllib import request as urlrequest
from config import settings
logger = logging.getLogger(__name__)
MD_CONTENT_KEYS = ("md_content", "mdcontent", "markdown", "content")
class FileParseApiError(RuntimeError):
def __init__(self, message: str, *, status_code: int | None = None, api_url: str = "") -> None:
super().__init__(message)
self.status_code = status_code
self.api_url = api_url
def _build_multipart_body(
file_path: Path,
field_name: str,
extra_fields: dict[str, str] | None = None,
) -> tuple[bytes, str]:
boundary = uuid.uuid4().hex
mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
file_bytes = file_path.read_bytes()
parts: list[bytes] = []
# 普通表单字段(如 engine=auto
for key, value in (extra_fields or {}).items():
if value is None or str(value).strip() == "":
continue
parts.append(f"--{boundary}\r\n".encode("utf-8"))
parts.append(
f'Content-Disposition: form-data; name="{key}"\r\n\r\n'.encode("utf-8")
)
parts.append(f"{value}\r\n".encode("utf-8"))
# 文件字段
parts.append(f"--{boundary}\r\n".encode("utf-8"))
parts.append(
(
f'Content-Disposition: form-data; name="{field_name}"; filename="{file_path.name}"\r\n'
f"Content-Type: {mime_type}\r\n\r\n"
).encode("utf-8")
)
parts.append(file_bytes)
parts.append(f"\r\n--{boundary}--\r\n".encode("utf-8"))
return b"".join(parts), boundary
def _extract_md_contents(payload: Any) -> list[str]:
if isinstance(payload, str):
return [payload]
if isinstance(payload, list):
out: list[str] = []
for item in payload:
out.extend(_extract_md_contents(item))
return out
if not isinstance(payload, dict):
return []
for key in MD_CONTENT_KEYS:
value = payload.get(key)
if isinstance(value, str):
return [value]
results = payload.get("results")
if results is not None:
return _extract_md_contents(results)
out = []
for value in payload.values():
out.extend(_extract_md_contents(value))
return out
def _response_to_markdown(text: str) -> str:
try:
payload = json.loads(text)
except json.JSONDecodeError:
# 非 JSON 直接当作 Markdown 返回
return text
contents = _extract_md_contents(payload)
if not contents:
raise ValueError("解析服务响应中未找到 md_content/markdown/content 字段")
return "\n\n".join(c.strip() for c in contents if c and c.strip())
def _request_once(
api_url: str,
file_path: Path,
field_name: str,
*,
timeout_sec: int,
extra_fields: dict[str, str] | None = None,
) -> str:
body, boundary = _build_multipart_body(file_path, field_name, extra_fields)
req = urlrequest.Request(
api_url,
data=body,
method="POST",
headers={"content-type": f"multipart/form-data; boundary={boundary}"},
)
try:
with urlrequest.urlopen(req, timeout=timeout_sec) as resp:
raw = resp.read()
encoding = resp.headers.get_content_charset() or "utf-8"
return raw.decode(encoding, errors="replace")
except urlerror.HTTPError as exc:
body_text = ""
try:
body_text = (exc.read() or b"").decode("utf-8", errors="replace")[:1000]
except Exception:
pass
raise FileParseApiError(
f"解析服务 HTTP {exc.code}{api_url}{body_text or exc.reason}",
status_code=int(exc.code or 0),
api_url=api_url,
) from exc
except urlerror.URLError as exc:
raise FileParseApiError(
f"无法连接解析服务({api_url}{exc.reason}",
status_code=0,
api_url=api_url,
) from exc
def parse_file_to_markdown(file_path: str | Path) -> str:
"""
将上传文件通过远程 file_parse 服务转换为 Markdown
失败时对 5xx 做有限重试
"""
path = Path(file_path)
if not path.is_file():
raise FileNotFoundError(f"文件不存在: {path}")
api_url = str(settings.FILE_PARSE_API_URL or "").strip()
if not api_url:
raise ValueError("FILE_PARSE_API_URL 未配置")
field_name = str(settings.FILE_PARSE_FIELD_NAME or "file").strip() or "file"
timeout_sec = max(int(settings.FILE_PARSE_HTTP_TIMEOUT_SEC or 600), 30)
retry_count = max(int(settings.FILE_PARSE_RETRY_COUNT or 1), 1)
backoff_sec = max(float(settings.FILE_PARSE_RETRY_BACKOFF_SEC or 1.0), 1.0)
retryable_status = {500, 502, 503, 504}
extra_fields: dict[str, str] = {}
engine = str(getattr(settings, "FILE_PARSE_ENGINE", "") or "").strip()
if engine:
extra_fields["engine"] = engine
last_error: Exception | None = None
for attempt in range(1, retry_count + 1):
try:
raw = _request_once(
api_url, path, field_name, timeout_sec=timeout_sec, extra_fields=extra_fields
)
markdown = _response_to_markdown(raw)
if not markdown.strip():
raise ValueError("解析服务返回的 Markdown 为空")
return markdown
except FileParseApiError as exc:
last_error = exc
status = int(exc.status_code or 0)
if attempt >= retry_count or status not in retryable_status:
raise
wait = backoff_sec * attempt
logger.warning(
"file_parse 重试 %s/%s status=%s wait=%ss file=%s",
attempt, retry_count, status, wait, path.name,
)
time.sleep(wait)
if last_error:
raise last_error
raise RuntimeError("file_parse 请求失败")

118
services/llm_client.py Normal file
View File

@ -0,0 +1,118 @@
"""
services/llm_client.py
极简 OpenAI 兼容 Chat Completions 客户端仅用于生成章节声明可选
"""
from __future__ import annotations
import json
import logging
import re
import requests
from config import settings
logger = logging.getLogger(__name__)
def llm_configured() -> bool:
return bool(
str(settings.LLM_API_BASE or "").strip()
and str(settings.LLM_API_KEY or "").strip()
and str(settings.LLM_MODEL_NAME or "").strip()
)
_THINK_BLOCK_RE = re.compile(r"<think>.*?</think>", re.DOTALL | re.IGNORECASE)
def _strip_reasoning(text: str) -> str:
"""去掉思考模型的思维链:成对 <think>…</think>,以及截断/前导的残留标签。"""
s = text or ""
s = _THINK_BLOCK_RE.sub("", s)
# 仅剩结束标签时,说明前面是未配对的思考段,取最后一个 </think> 之后的正文
if "</think>" in s:
s = s.rsplit("</think>", 1)[-1]
s = re.sub(r"</?think>", "", s, flags=re.IGNORECASE)
return s.strip()
def chat_completion_text(
*,
system_prompt: str,
user_prompt: str,
temperature: float = 0.2,
max_tokens: int = 512,
timeout_sec: int | None = None,
) -> str:
"""调用 LLM 返回纯文本。失败抛出异常,由调用方决定是否兜底。"""
base = str(settings.LLM_API_BASE or "").strip().rstrip("/")
url = f"{base}/chat/completions"
headers = {
"Authorization": f"Bearer {settings.LLM_API_KEY}",
"Content-Type": "application/json",
}
payload = {
"model": settings.LLM_MODEL_NAME,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"temperature": temperature,
"max_tokens": max_tokens,
}
# 关闭思考模型的思维链vLLM/Qwen3 等支持该扩展字段;不支持的服务会忽略)
if bool(getattr(settings, "LLM_DISABLE_THINKING", False)):
payload["chat_template_kwargs"] = {"enable_thinking": False}
resp = requests.post(
url,
headers=headers,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
timeout=timeout_sec or int(settings.LLM_HTTP_TIMEOUT_SEC or 120),
)
resp.raise_for_status()
data = resp.json()
return _strip_reasoning((data["choices"][0]["message"]["content"] or "").strip())
def _extract_json(text: str) -> dict:
"""从模型输出中解析 JSON object容忍 ```json``` 代码块包裹)。"""
s = (text or "").strip()
if s.startswith("```"):
s = re.sub(r"^```[a-zA-Z]*\s*", "", s)
s = re.sub(r"\s*```$", "", s).strip()
try:
obj = json.loads(s)
except json.JSONDecodeError:
m = re.search(r"\{.*\}", s, flags=re.DOTALL)
if not m:
return {}
try:
obj = json.loads(m.group(0))
except json.JSONDecodeError:
return {}
return obj if isinstance(obj, dict) else {}
def chat_completions_json(
*,
system_prompt: str,
user_prompt: str,
temperature: float = 0.1,
max_tokens: int = 4096,
timeout_sec: int | None = None,
) -> dict:
"""调用 LLM 并将返回解析为 JSON objectdict。失败返回 {}"""
try:
text = chat_completion_text(
system_prompt=system_prompt,
user_prompt=user_prompt,
temperature=temperature,
max_tokens=max_tokens,
timeout_sec=timeout_sec,
)
except Exception as e: # noqa: BLE001
logger.warning("chat_completions_json 调用失败: %s", e)
return {}
return _extract_json(text)

View File

@ -0,0 +1,406 @@
"""
services/section_extractor.py
Markdown
1) 抽取目录章节标题层级-> 用于生成模板章节目录
2) 按标题拆分正文 -> 每个章节的内容用于入库 report_section_references
抽取/过滤逻辑参考 eval_report/routers/template.py routers/reference.py
"""
from __future__ import annotations
import hashlib
import re
_MAX_SECTION_TITLE_LEN = 200
# ────────────────────────────── 通用过滤/清洗 ──────────────────────────────
def _segment_looks_like_year(segment: str) -> bool:
if not segment.isdigit() or len(segment) != 4:
return False
year = int(segment)
return 1900 <= year <= 2099
def _is_valid_section_number(num: str) -> bool:
"""章节编号形如 1 / 1.1 / 2.3.4排除正文年份2017、2019 等)。"""
parts = [p for p in str(num or "").strip().split(".") if p]
if not parts or not all(p.isdigit() for p in parts):
return False
if any(_segment_looks_like_year(p) for p in parts):
return False
if len(parts) == 1:
return 1 <= int(parts[0]) <= 20
return all(1 <= int(p) <= 99 for p in parts)
def _heading_title_core(rest: str) -> str:
return re.sub(r"^\d+(?:\.\d+)*\s*", "", str(rest or "").strip()).strip()
def _rest_looks_like_body_text(rest: str) -> bool:
"""过滤日期句、长段落、数据说明句等被误识别为标题的正文。"""
t = _heading_title_core(rest) or str(rest or "").strip()
if not t:
return True
if re.match(r"^[月日]", t):
return True
if re.search(r"\d", t):
return True
if re.match(r"^\d{4}\s*年", t) or re.match(r"^\d{4}[、,]", t):
return True
if re.search(r"\d{4}\s*[-~—至]\s*\d{4}", t):
return True
if t.count("") >= 2 or t.count("") >= 2:
return True
if len(t) > 80 and re.search(r"[,。;:]", t):
return True
if len(t) > 45 and any(
k in t
for k in (
"运营数据", "预测数据", "实际运营", "根据公司",
"发展规划", "工况下", "万吨", "有项目", "无项目",
)
):
return True
if len(t) > 45 and not re.search(
r"(评价|分析|结论|概况|说明|措施|建议|对比|控制|实现|状况|情况|程序|模式|评价结论)$",
t.rstrip("。;,"),
):
return True
return False
def _looks_like_real_heading_title(title: str) -> bool:
if not str(title or "").strip():
return False
return not _rest_looks_like_body_text(title)
def _clean_heading_title(s: str) -> str:
t = str(s or "").strip()
t = re.sub(r"\s+", " ", t)
t = re.sub(r"\s+\d+$", "", t).strip() # 去掉目录行尾页码
m_note = re.search(r"[(]([^)]{20,})[)]", t)
if m_note and re.search(r"[,。;:]", m_note.group(1)):
t = re.sub(r"\s*[(][^)]{20,}[)]\s*$", "", t).strip()
return t
def _section_dict(section_key: str, section_title: str) -> dict:
return {"sectionKey": section_key, "sectionTitle": section_title}
def _canonical_to_section_key(canonical: str, order: int) -> str:
return (
re.sub(r"[^a-z0-9\u4e00-\u9fa5]+", "-", canonical).strip("-")
or f"section-{order}"
)
def normalize_section_key(raw_key: str | None, title: str | None) -> str:
"""生成稳定且可入库的 section_key<=64超长追加短哈希。"""
base = (raw_key or "").strip().lower()
if not base:
base = (title or "").strip().lower()
base = re.sub(r"[^a-z0-9\u4e00-\u9fa5]+", "-", base).strip("-")
if not base:
base = "section"
if len(base) <= 64:
return base
digest = hashlib.md5(base.encode("utf-8")).hexdigest()[:10]
prefix = base[:53].rstrip("-")
return f"{prefix}-{digest}"
# ────────────────────────────── 目录TOC抽取 ──────────────────────────────
def _walk_markdown_heading_sections(text: str) -> list[dict]:
"""
单次遍历 Markdown按标题# ~ ######)切分章节并捕获正文(不含本节标题行)。
标题层级自动编号## 项目概况 -> 1.1 项目概况),无显式编号也可处理。
被判定为"非真实标题" # 行视为正文内容,不另起章节。
正文范围
- 默认SECTION_CONTENT_INCLUDE_SUBSECTIONS=True聚合整棵子树
即本节标题之后直到下一个"层级 <= 本节"的标题之前的全部内容
含下级小节标题与正文保证父章节正文非空
- 关闭时仅取到下一个任意标题之前本节自身正文
返回每节{number, title, full_title, canonical, section_key(canonical), level, content}
目录抽取与正文拆分共用此函数确保目录与内容一一对应
"""
from config import settings
include_sub = bool(getattr(settings, "SECTION_CONTENT_INCLUDE_SUBSECTIONS", True))
lines = str(text or "").splitlines()
counters: list[int] = []
accepted: list[dict] = []
seen: set[str] = set()
for idx, raw in enumerate(lines):
m = re.match(r"^(#{1,6})\s+(.+)$", str(raw or "").strip())
if not m:
continue
level = len(m.group(1))
title = _clean_heading_title(m.group(2).strip())
is_valid = (
bool(title)
and len(title) <= _MAX_SECTION_TITLE_LEN
and _looks_like_real_heading_title(title)
)
if not is_valid:
continue
if len(counters) < level:
counters.extend([0] * (level - len(counters)))
else:
counters = counters[:level]
counters[level - 1] += 1
for i in range(level, len(counters)):
counters[i] = 0
num = ".".join(str(counters[i]) for i in range(level))
full_title = f"{num} {title}"
canonical = f"{num}|{title}".lower()
if canonical in seen:
continue
seen.add(canonical)
accepted.append(
{
"number": num,
"title": title,
"full_title": full_title,
"canonical": canonical,
"section_key": _canonical_to_section_key(canonical, len(accepted) + 1),
"level": level,
"start_idx": idx,
}
)
total = len(lines)
for i, sec in enumerate(accepted):
body_start = sec["start_idx"] + 1 # 排除本节标题行
end = total
for j in range(i + 1, len(accepted)):
nxt = accepted[j]
if include_sub:
if nxt["level"] <= sec["level"]:
end = nxt["start_idx"]
break
else:
end = nxt["start_idx"]
break
sec["content"] = "\n".join(lines[body_start:end]).strip()
sec.pop("start_idx", None)
return accepted
def _extract_sections_from_markdown_headings(text: str) -> list[dict]:
"""
Markdown 标题# / ## / ###)构建模板章节目录。
复刻 eval_report 报告模板管理模块 services/template_service.py 的同名逻辑
标题层级自动编号## 项目概况 -> 1.1 项目概况),并过滤非真实标题行。
"""
lines = str(text or "").splitlines()
counters: list[int] = []
out: list[dict] = []
seen: set[str] = set()
for raw in lines:
m = re.match(r"^(#{1,6})\s+(.+)$", str(raw or "").strip())
if not m:
continue
level = len(m.group(1))
title = _clean_heading_title(m.group(2).strip())
if not title or len(title) > _MAX_SECTION_TITLE_LEN:
continue
if not _looks_like_real_heading_title(title):
continue
if len(counters) < level:
counters.extend([0] * (level - len(counters)))
else:
counters = counters[:level]
counters[level - 1] += 1
for i in range(level, len(counters)):
counters[i] = 0
num = ".".join(str(counters[i]) for i in range(level))
full_title = f"{num} {title}"
canonical = f"{num}|{title}".lower()
if canonical in seen:
continue
seen.add(canonical)
out.append(
_section_dict(
_canonical_to_section_key(canonical, len(out) + 1),
full_title,
)
)
return out
def extract_sections_from_text(text: str) -> list[dict]:
"""抽取模板章节目录(入库 report_template_sections
复刻 eval_report 报告模板管理模块的逻辑优先按 Markdown 标题层级识别
命中数 >= 8 时直接采用否则回退到目录/编号行识别"""
md_sections = _extract_sections_from_markdown_headings(text)
if len(md_sections) >= 8:
return md_sections
lines = str(text or "").splitlines()
out: list[dict] = []
seen: set[str] = set()
candidates: list[dict] = []
for raw in lines:
line = str(raw or "").strip()
if not line:
continue
line = re.sub(r"^#{1,6}\s*", "", line).strip()
line = line.replace("\u3000", " ")
line = re.sub(r"\s+", " ", line).strip()
if re.match(r"^20\d{2}\s*年\s*\d{1,2}\s*月$", line):
continue
if line in {"目次", "目录"}:
continue
if re.match(r"^\d+\s*[\)]\s*.+$", line):
continue
has_page_no = bool(re.search(r"\s+\d+\s*$", line))
m = re.match(r"^((?:\d+(?:\.\d+){0,5}))\s*([^\s].*)$", line)
if m:
num = m.group(1).strip()
if not _is_valid_section_number(num):
continue
rest = _clean_heading_title(m.group(2).strip())
if not rest or rest.startswith("") or rest.startswith(")"):
continue
if _rest_looks_like_body_text(rest):
continue
if len(rest) > _MAX_SECTION_TITLE_LEN:
continue
full_title = f"{num} {rest}"[:_MAX_SECTION_TITLE_LEN].rstrip()
canonical = f"{num}|{rest}".lower()
else:
m2 = re.match(r"^([一二三四五六七八九十]+[、.])\s*([^\s].*)$", line)
if not m2:
continue
rest2 = _clean_heading_title(m2.group(2).strip())
if not rest2 or _rest_looks_like_body_text(rest2) or len(rest2) > _MAX_SECTION_TITLE_LEN:
continue
full_title = f"{m2.group(1)} {rest2}"[:_MAX_SECTION_TITLE_LEN].rstrip()
canonical = f"{m2.group(1)}|{rest2}".lower()
candidates.append({"canonical": canonical, "title": full_title, "has_page_no": has_page_no})
use_toc_only = False
toc_rows = [c for c in candidates if c["has_page_no"]]
toc_nums = set()
for c in toc_rows:
m_num = re.match(r"^(\d+)", c["title"])
if m_num:
toc_nums.add(m_num.group(1))
if len(toc_rows) >= 20 and {"1", "2", "3", "4", "5", "6", "7"}.issubset(toc_nums):
use_toc_only = True
picked = toc_rows if use_toc_only else candidates
for c in picked:
canonical = c["canonical"]
if canonical in seen:
continue
if not _looks_like_real_heading_title(c["title"]):
continue
seen.add(canonical)
out.append(_section_dict(_canonical_to_section_key(canonical, len(out) + 1), c["title"]))
return out
# ────────────────────────────── 正文按标题拆分 ──────────────────────────────
def split_markdown_into_sections(text: str) -> list[dict[str, str]]:
"""
Markdown 标题切分正文与目录抽取共用同一套标题识别与自动编号
保证每个目录章节都能拿到对应正文section_key 为自动编号 1.1
若文档不含 Markdown 标题则回退到"带编号标题"的拆分方式
返回 [{section_key, section_title, content}, ...]
"""
walk = _walk_markdown_heading_sections(text)
if walk:
return [
{
"section_key": s["number"],
"section_title": s["full_title"],
"content": s["content"],
}
for s in walk
]
return split_markdown_by_headings(text)
def split_markdown_by_headings(text: str) -> list[dict[str, str]]:
"""
Markdown 标题# ~ ####,且带章节编号,如 ## 1.1 标题)拆分正文。
返回 [{section_key, section_title, content}, ...]section_key 为编号 1.1
"""
lines = str(text or "").splitlines()
heading_pattern = re.compile(r"^#{1,4}\s+(\d+(?:\.\d+)*)\s+(.+)")
sections: list[dict[str, str]] = []
current_key: str | None = None
current_title: str | None = None
current_lines: list[str] = []
for line in lines:
m = heading_pattern.match(line)
if m:
if current_key and current_lines:
sections.append({
"section_key": current_key,
"section_title": current_title or "",
"content": "\n".join(current_lines).strip(),
})
current_key = m.group(1)
current_title = m.group(2).strip()
current_lines = [line]
else:
if current_key:
current_lines.append(line)
if current_key and current_lines:
sections.append({
"section_key": current_key,
"section_title": current_title or "",
"content": "\n".join(current_lines).strip(),
})
return sections
def parse_section_order(section_key: str) -> int:
"""'1.2.1' 转为整数 121 用于排序。"""
digits = str(section_key or "").replace(".", "")
return int(digits) if digits.isdigit() else 0
def clamp_text_bytes(text: str, max_bytes: int, *, suffix: str = "\n…(内容过长,已截断)") -> str:
"""
将文本按 UTF-8 字节数截断到 max_bytes 以内且不会截断到半个字符
用于适配 MySQL TEXT 最大 65535 字节
"""
if not text or max_bytes <= 0:
return text
data = text.encode("utf-8")
if len(data) <= max_bytes:
return text
suffix_bytes = len(suffix.encode("utf-8"))
budget = max(max_bytes - suffix_bytes, 0)
# errors="ignore" 会丢弃末尾被切断的不完整字符,保证是合法 UTF-8
truncated = data[:budget].decode("utf-8", errors="ignore").rstrip()
return truncated + suffix

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,524 @@
"""
services/template_service.py
复刻自 eval_reportreport_template_sections 数据的获取方式
- DEFAULT_TEMPLATE_SECTIONS系统默认后评价报告章节目录key, title
- default_section_prompt / default_section_output_contract / default_section_examples
按章节标题/编号取对应提示词输出合同示例
- build_default_template_catalog默认目录 + 提示词/合同供上传模版匹配
说明eval_report 会额外从编制细则模版Word 文档抽取更细的提示词/示例
本项目默认不含这两个 .doc 文件与 DocParser故相关函数在缺文件时优雅降级
回退到 SECTION_PROMPT_RULES / SECTION_EXAMPLE_RULES
"""
from __future__ import annotations
import re
import uuid
from datetime import datetime
from functools import lru_cache
from pathlib import Path
from sqlalchemy.orm import Session
from database.models import ReportTemplate, ReportTemplateSection
from prompts.report_generation.section_output_contracts import (
DEFAULT_SECTION_OUTPUT_CONTRACT,
SECTION_OUTPUT_CONTRACTS,
)
from prompts.report_generation.template_prompt_rules import (
DEFAULT_SECTION_PROMPT,
SECTION_EXAMPLE_RULES,
SECTION_PROMPT_RULES,
)
SYSTEM_DEFAULT_TEMPLATE_NAME = "后评价默认模板"
GUIDELINE_BASENAME = "炼油化工建设项目后评价报告编制细则(修订)"
PROJECT_EXAMPLE_BASENAME = "模版"
MAX_SECTION_EXAMPLE_CHARS = 12000
DEFAULT_TEMPLATE_SECTIONS: list[tuple[str, str]] = [
("1", "1 项目概况"),
("1-1", "1.1 项目基本情况"),
("1-2", "1.2 项目决策要点"),
("1-3", "1.3 项目实施情况"),
("1-4", "1.4 项目运行情况"),
("2", "2 前期工作评价"),
("2-1", "2.1 项目要素评价"),
("2-1-1", "2.1.1 资源与原料评价"),
("2-1-2", "2.1.2 产品方案及市场评价"),
("2-1-2-1", "2.1.2.1 产品方案评价"),
("2-1-2-2", "2.1.2.2 产品市场评价"),
("2-1-3", "2.1.3 工艺方案评价"),
("2-1-3-1", "2.1.3.1 总加工方案评价"),
("2-1-3-2", "2.1.3.2 建设规模及工艺技术方案评价"),
("2-1-3-3", "2.1.3.3 主要设备方案评价"),
("2-1-4", "2.1.4 厂址选择及外部条件评价"),
("2-1-5", "2.1.5 总图及系统配套工程评价"),
("2-1-6", "2.1.6 主要技术指标评价"),
("2-1-7", "2.1.7 风险分析评价"),
("2-2", "2.2 工作程序评价"),
("2-2-1", "2.2.1 编制单位资质及选择方式评价"),
("2-2-2", "2.2.2 编制进度评价"),
("2-2-3", "2.2.3 与专项评价的结合情况"),
("2-2-4", "2.2.4 可行性研究报告的质量评价"),
("2-3", "2.3 前评估工作评价"),
("2-4", "2.4 初步设计评价"),
("2-4-1", "2.4.1 设计单位资质及选择方式评价"),
("2-4-2", "2.4.2 初步设计进度评价"),
("2-4-3", "2.4.3 初步设计质量评价"),
("2-4-4", "2.4.4 初步设计审查工作评价"),
("2-5", "2.5 前期决策程序评价"),
("2-6", "2.6 前期工作评价结论"),
("3", "3 建设实施评价"),
("3-1", "3.1 工程建设管理模式评价"),
("3-2", "3.2 招投标评价"),
("3-3", "3.3 施工图设计评价"),
("3-3-1", "3.3.1 与批复后初步设计符合性评价"),
("3-3-2", "3.3.2 设计进度评价"),
("3-3-3", "3.3.3 施工图设计水平及质量评价"),
("3-3-4", "3.3.4 施工图设计变更管理评价"),
("3-4", "3.4 工程承包商或施工单位评价"),
("3-4-1", "3.4.1 施工准备评价"),
("3-4-2", "3.4.2 施工计划的执行情况"),
("3-5", "3.5 采购工作评价"),
("3-6", "3.6 工程监理评价"),
("3-7", "3.7 工程质量评价"),
("3-8", "3.8 HSE管理评价"),
("3-9", "3.9 三查四定及中间交接"),
("3-10", "3.10 工程竣工验收评价"),
("3-11", "3.11 建设实施评价结论"),
("4", "4 生产运行评价"),
("4-1", "4.1 生产准备评价"),
("4-2", "4.2 联合试运与试生产情况评价"),
("4-3", "4.3 生产运行评价"),
("4-3-1", "4.3.1 原料供应评价"),
("4-3-2", "4.3.2 生产运行总体情况评价"),
("4-3-3", "4.3.3 达标评价"),
("4-3-4", "4.3.4 生产工艺技术评价"),
("4-3-5", "4.3.5 设备运行评价"),
("4-3-6", "4.3.6 公用工程及辅助设施合理性评价"),
("4-4", "4.4 生产运行评价结论"),
("5", "5 投资与经济效益评价"),
("5-1", "5.1 主要经济指标实现程度评价"),
("5-2", "5.2 投资和执行情况评价"),
("5-2-1", "5.2.1 投资控制及变动原因分析"),
("5-2-2", "5.2.2 投资水平分析"),
("5-2-3", "5.2.3 资金来源及到位评价"),
("5-2-4", "5.2.4 投资控制的经验和教训"),
("5-3", "5.3 经济效益分析"),
("5-3-1", "5.3.1 项目投产以来生产经营及效益状况"),
("5-3-2", "5.3.2 项目经济效益后评价"),
("5-4", "5.4 不确定性分析"),
("5-5", "5.5 投资与经济效益评价结论"),
("6", "6 影响与持续性评价"),
("6-1", "6.1 影响评价"),
("6-1-1", "6.1.1 环境影响评价"),
("6-1-2", "6.1.2 安全影响评价"),
("6-1-3", "6.1.3 科技进步影响"),
("6-1-4", "6.1.4 项目社会影响评价"),
("6-1-5", "6.1.5 项目影响评价结论"),
("6-2", "6.2 持续性评价"),
("6-2-1", "6.2.1 资源分析"),
("6-2-2", "6.2.2 产品分析"),
("6-2-3", "6.2.3 主要技术及经济指标对比"),
("6-2-4", "6.2.4 项目持续性评价结论"),
("7", "7 综合评价结论"),
("7-1", "7.1 综合评价结论"),
("7-1-1", "7.1.1 总体评价结论"),
("7-1-2", "7.1.2 成功度评价"),
("7-2", "7.2 主要经验"),
("7-3", "7.3 问题与建议"),
]
def default_section_output_contract(section_title: str, section_key: str | None = None) -> str:
section_no = _extract_number_prefix(section_title) or _section_key_to_number(section_key)
if section_no and section_no in SECTION_OUTPUT_CONTRACTS:
return SECTION_OUTPUT_CONTRACTS[section_no]
return DEFAULT_SECTION_OUTPUT_CONTRACT
def default_section_prompt(section_title: str, section_key: str | None = None) -> str:
guideline_prompt = _guideline_prompt_for(section_title, section_key)
if guideline_prompt:
return guideline_prompt
title = _normalize_section_identity(section_title)
key = str(section_key or "").strip().lower()
for pattern, prompt in SECTION_PROMPT_RULES:
p = pattern.lower()
if title.startswith(p):
return prompt
if p.isdigit() and (title.startswith(f"{p} ") or key.startswith(f"{p}-") or key == p):
return prompt
return DEFAULT_SECTION_PROMPT
def build_default_template_catalog() -> list[dict[str, str]]:
"""系统默认模板章节目录及对应提示词、输出合同(供上传模版匹配)。"""
out: list[dict[str, str]] = []
for key, title in DEFAULT_TEMPLATE_SECTIONS:
out.append(
{
"sectionKey": key,
"sectionTitle": title,
"sectionNumber": _extract_number_prefix(title) or _section_key_to_number(key),
"sectionPrompt": default_section_prompt(title, key),
"sectionOutputContract": default_section_output_contract(title, key),
}
)
return out
def default_section_examples(section_title: str, section_key: str | None = None) -> str:
project_example = _project_example_for(section_title, section_key)
if project_example:
return project_example
title = _normalize_section_identity(section_title)
key = str(section_key or "").strip().lower()
num = _extract_number_prefix(section_title) or _section_key_to_number(section_key)
chapter_no = ""
if num:
chapter_no = num.split(".")[0]
elif key:
chapter_no = key.split("-")[0]
for prefix, examples in SECTION_EXAMPLE_RULES:
p = str(prefix).strip().lower()
if chapter_no == p:
return examples
if title.startswith(f"{p} "):
return examples
if key.startswith(f"{p}-") or key == p:
return examples
return ""
def _normalize_section_identity(value: str | None) -> str:
text = str(value or "").strip().lower()
text = text.replace("", ".").replace("", ".")
text = re.sub(r"\s+", " ", text)
return text
def _section_key_to_number(section_key: str | None) -> str:
key = str(section_key or "").strip()
if not key:
return ""
if re.fullmatch(r"\d+(?:-\d+)*", key):
return key.replace("-", ".")
return ""
def _extract_number_prefix(title: str) -> str:
m = re.match(r"^\s*(\d+(?:\.\d+)*)\s*", str(title or ""))
return m.group(1) if m else ""
def _normalize_heading_key(value: str) -> str:
s = str(value or "").strip().lower()
s = s.replace("", ".").replace("", ".")
s = re.sub(r"\s+", "", s)
return s
def _tuple_from_number(number_str: str) -> tuple[int, ...]:
if not number_str:
return tuple()
parts = []
for p in number_str.split("."):
if p.isdigit():
parts.append(int(p))
else:
return tuple()
return tuple(parts)
def _read_doc_text(path: str) -> str:
"""读取 .doc/.docx 文本。本项目无 DocParser 时返回空串(优雅降级)。"""
try:
from function.documents.doc_parser import DocParser # type: ignore
except Exception:
return ""
try:
return DocParser(path).read()
except Exception:
return ""
@lru_cache(maxsize=1)
def _guideline_section_prompt_map() -> dict[str, str]:
guideline_path = _resolve_guideline_path()
if not guideline_path:
return {}
raw_text = _read_doc_text(guideline_path)
if not raw_text:
return {}
return _build_guideline_prompt_map(raw_text)
def _resolve_guideline_path() -> str | None:
root = Path(__file__).resolve().parents[1]
candidates = [
root / f"{GUIDELINE_BASENAME}.doc",
root / f"{GUIDELINE_BASENAME}.docx",
]
for p in candidates:
if p.is_file():
return str(p)
return None
def _resolve_project_example_path() -> str | None:
root = Path(__file__).resolve().parents[1]
candidates = [
root / f"{PROJECT_EXAMPLE_BASENAME}.doc",
root / f"{PROJECT_EXAMPLE_BASENAME}.docx",
]
for p in candidates:
if p.is_file():
return str(p)
return None
@lru_cache(maxsize=1)
def _project_example_entries() -> list[tuple[str, str]]:
path = _resolve_project_example_path()
if not path:
return []
raw_text = _read_doc_text(path)
if not raw_text:
return []
return _build_project_example_entries(raw_text)
def _build_project_example_entries(text: str) -> list[tuple[str, str]]:
lines = str(text or "").splitlines()
headings: list[tuple[int, int, str]] = []
for idx, raw in enumerate(lines):
line = str(raw or "").strip()
m = re.match(r"^\s*(#{1,6})\s*(.+?)\s*$", line)
if not m:
continue
level = len(m.group(1))
heading_title = m.group(2).strip()
if not heading_title:
continue
headings.append((idx, level, heading_title))
out: list[tuple[str, str]] = []
for i, (start_idx, level, title) in enumerate(headings):
end_idx = len(lines)
for j in range(i + 1, len(headings)):
next_idx, next_level, _ = headings[j]
if next_level <= level:
end_idx = next_idx
break
body = "\n".join(lines[start_idx + 1 : end_idx]).strip()
body = re.sub(r"\n{3,}", "\n\n", body)
if not body:
continue
out.append((title, body))
return out
def _project_example_for(section_title: str, section_key: str | None = None) -> str:
entries = _project_example_entries()
if not entries:
return ""
target_title = _clean_section_title(section_title)
target_key = _section_key_to_number(section_key)
target_core = _core_title(target_title or target_key)
if not target_core:
return ""
best_title = ""
best_body = ""
best_score = -1
for heading, body in entries:
heading_clean = _clean_section_title(heading)
heading_core = _core_title(heading_clean)
score = _title_match_score(target_core, heading_core)
if score > best_score:
best_score = score
best_title = heading_clean
best_body = body
if best_score < 4 or not best_body:
return ""
text = f"### {best_title}\n\n{best_body}".strip()
if len(text) > MAX_SECTION_EXAMPLE_CHARS:
text = text[:MAX_SECTION_EXAMPLE_CHARS].rstrip() + "\n\n(示例过长,已截断)"
return text
def _clean_section_title(value: str | None) -> str:
s = str(value or "").strip()
s = re.sub(r"^\s*\d+(?:[.\-]\d+)*\s*", "", s)
return s.strip()
def _core_title(value: str | None) -> str:
s = str(value or "").strip()
s = s.replace("", "(").replace("", ")")
s = re.sub(r"\([^)]*\)", "", s)
s = re.sub(r"[、,。;::()\-\s]", "", s)
s = s.replace("项目", "")
s = s.replace("情况", "")
s = s.replace("工作", "")
return s.strip().lower()
def _title_match_score(target: str, candidate: str) -> int:
if not target or not candidate:
return 0
if target == candidate:
return 100
score = 0
if target in candidate or candidate in target:
score += 40
tks_t = re.findall(r"[\u4e00-\u9fa5]{2,8}|[a-z]{2,12}", target)
tks_c = re.findall(r"[\u4e00-\u9fa5]{2,8}|[a-z]{2,12}", candidate)
if tks_t and tks_c:
overlap = len(set(tks_t) & set(tks_c))
score += overlap * 8
ch_overlap = len(set(target) & set(candidate))
score += min(ch_overlap, 20)
return score
def _build_guideline_prompt_map(text: str) -> dict[str, str]:
lines = str(text or "").splitlines()
headings: list[tuple[int, str, str, tuple[int, ...]]] = []
for idx, raw in enumerate(lines):
line = str(raw or "").strip()
m = re.match(r"^\s*#{1,6}\s*(.+?)\s*$", line)
if not m:
continue
heading_title = m.group(1).strip()
number = _extract_number_prefix(heading_title)
number_tuple = _tuple_from_number(number)
if not number_tuple:
continue
headings.append((idx, heading_title, number, number_tuple))
prompt_map: dict[str, str] = {}
for i, (start_idx, heading_title, number, number_tuple) in enumerate(headings):
end_idx = len(lines)
for j in range(i + 1, len(headings)):
next_start, _, _, next_tuple = headings[j]
if len(next_tuple) < len(number_tuple) or next_tuple[: len(number_tuple)] != number_tuple:
end_idx = next_start
break
body = "\n".join(lines[start_idx + 1 : end_idx]).strip()
body = re.sub(r"\n{3,}", "\n\n", body)
if not body:
continue
key_title = _normalize_heading_key(heading_title)
key_number = _normalize_heading_key(number)
prompt_map[key_title] = body
prompt_map[key_number] = body
return prompt_map
def _guideline_prompt_for(section_title: str, section_key: str | None = None) -> str:
mapping = _guideline_section_prompt_map()
if not mapping:
return ""
title = str(section_title or "").strip()
number = _extract_number_prefix(title) or _section_key_to_number(section_key)
candidates = [
_normalize_heading_key(title),
_normalize_heading_key(number),
]
for key in candidates:
if key and key in mapping:
return mapping[key]
return ""
def list_templates(db: Session) -> list[ReportTemplate]:
return (
db.query(ReportTemplate)
.order_by(ReportTemplate.is_default.desc(), ReportTemplate.updated_at.desc())
.all()
)
def ensure_default_template(db: Session) -> None:
now = datetime.now()
system_default = (
db.query(ReportTemplate)
.filter(ReportTemplate.name == SYSTEM_DEFAULT_TEMPLATE_NAME)
.first()
)
if not system_default:
system_default = ReportTemplate(
id=uuid.uuid4().hex,
name=SYSTEM_DEFAULT_TEMPLATE_NAME,
description="系统预置模板(细则完整章节)",
is_default=True,
is_active=True,
created_at=now,
updated_at=now,
)
db.add(system_default)
db.flush()
current_rows = (
db.query(ReportTemplateSection)
.filter(ReportTemplateSection.template_id == system_default.id)
.order_by(ReportTemplateSection.section_order.asc())
.all()
)
current_pairs = [(r.section_key, r.section_title) for r in current_rows]
expected_pairs = list(DEFAULT_TEMPLATE_SECTIONS)
db.query(ReportTemplate).update({ReportTemplate.is_default: False})
system_default.is_default = True
system_default.is_active = True
system_default.updated_at = now
if current_pairs == expected_pairs:
has_changed = False
for row in current_rows:
current_examples = str(row.examples or "").strip()
new_examples = default_section_examples(row.section_title, row.section_key).strip()
if new_examples and current_examples != new_examples:
row.examples = new_examples
row.updated_at = now
has_changed = True
current_out = str(getattr(row, "section_output_contract", None) or "").strip()
new_out = default_section_output_contract(row.section_title, row.section_key).strip()
if not current_out and new_out:
row.section_output_contract = new_out
row.updated_at = now
has_changed = True
if has_changed:
system_default.updated_at = now
db.commit()
return
db.query(ReportTemplateSection).filter(
ReportTemplateSection.template_id == system_default.id
).delete()
for i, (key, title) in enumerate(DEFAULT_TEMPLATE_SECTIONS):
db.add(
ReportTemplateSection(
id=uuid.uuid4().hex,
template_id=system_default.id,
section_key=key,
section_title=title,
section_prompt="",
section_output_contract=default_section_output_contract(title, key),
section_order=i,
examples=default_section_examples(title, key),
created_at=now,
updated_at=now,
)
)
db.commit()