diff --git a/README.md b/README.md index 98288b1..c3491b9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,153 @@ -# pipeline-bidding +# pipeline-bidding — 投标产线模块 +## 模块定位 + +产线平台(pipeline-app / pipeline-core)的第二条产线能力包:**投标产线 `bidding_general`**。 +验证并落地「产线能力包住自己仓库、宿主不装则零接触」的分层原则——能力模块全部住在本仓库, +不进 `pipeline-service`;宿主只要不 `import pipeline_bidding`,一切都不会发生。 + +模块 = 数据(`bid_*` 表 + CRUD)+ 能力(`bid_*_capability`)+ 流转(`bid_flow` 状态对账器) ++ 主 agent 能力包(`bid_ability`)+ 角色技能(部署期随 pipeline-core skills_library 分发)。 + +**产线起点 = 上传招标文件**(`import_tender_file` 无 project_id 时自动立项)。阶段链: + +> 收到招标文件(自动立项)→ 解析(评分项/资质/投标文件要求/商务要素/章节骨架) +> → QC 契合度审核(四类逐类,10 分制,>9.5 放行)→ 资料准备 → 分角色并发编写 +> (business→bid_biz_writer / technical→bid_writer)→ 章节评审(含篇幅门禁)→ 合成 +> → 整书评分 → 交付确认。 + +核心机制: + +- **角色任务流转不用 RoleSpec.next_role**(全留空):投标是「一章一任务 + 两级回退循环」 + 扇出结构,由 `bid_flow` 状态对账器(每 15s 一轮)补派,对账器是角色任务的**唯一创建者**。 +- **阻塞门禁在创建源头**(对账器判 `pipeline_human_tasks.project_id + pending`), + 不写进引擎 poller SQL。`tenant_id=project_id` 只是角色任务约定,禁止写进引擎层。 +- **两级回退循环**:章节级(得分率 < `bid_chapter_pass_ratio` 或篇幅不达标 → rejected+意见 → + 自动重写,超 `bid_max_revise` 抛人工);整书级(< `bid_pass_ratio` 或否决项未响应 → + 按 `target_chapter_id` 打回 → 重写→重审→重合成→重评分,超 `bid_max_round` 抛人工)。 +- **QC 门禁**:四类解析产出逐类审核落 `bid_qc_reviews`;不通过 → 清空该类产出 → 对账器重派 + 解析(`params.qc_redo=1` + 改进意见)→ 重审;轮次超 `bid_qc_max_round` → `qc_escalation` + 阻塞人工(逃逸阀:人工修产出后在记录页把 passed 改 1 即生效)。 +- **门限参数全部读 appbase `params` 表 + DEFAULT_PARAMS 兜底**,不硬编码: + `bid_chapter_pass_ratio`(0.8) / `bid_pass_ratio`(0.85) / `bid_max_revise`(3) / + `bid_max_round`(3) / `bid_qc_pass_score`(9.5) / `bid_qc_max_round`(3) / + `bid_chapter_min_words`(800) / `bid_chapter_min_words_biz`(500) / `bid_write_concurrency`(4)。 + +## 表清单(models/*.json,建表产物 mysql.ddl.sql) + +| 表名 | 说明 | 关键字段 | +|---|---|---| +| `bid_tenders` | 招标商务要素 | project_id, source, title, purchaser, tender_no, industry | +| `bid_tender_files` | 招标文件 | project_id, tender_id, file_name, file_path, file_type, content_text | +| `bid_scoring_items` | 评分项与得分规则 | project_id, section, item_no, item_name, max_score, scoring_rule, is_veto | +| `bid_qualifications` | 招标所需资质 | project_id, qual_name, requirement, is_mandatory, owner, kb_doc_id, match_status | +| `bid_doc_requirements` | 投标文件要求 | project_id, req_type, chapter_no, chapter_title, requirement, page_limit, source_ref | +| `bid_chapters` | 投标文件章节 | project_id, chapter_no, parent_no, title, section, outline, status, content, word_count, review_score | +| `bid_reviews` | 标书评审记录 | project_id, scope, chapter_id, document_id, round, reviewer, review_type | +| `bid_scores` | 标书评分明细 | project_id, review_id, scoring_item_id, item_name, score, max_score, reason | +| `bid_documents` | 合成标书 | project_id, version, doc_name, file_path, page_count, chapter_count, total_score | +| `bid_qc_reviews` | 解析产出 QC 审核记录 | project_id, qc_type, round, reviewer, review_type, fit_score, pass_score | +| `bid_kb_docs` | 公司投标知识库 | org_id, doc_type(sample_bid 等), doc_name, doc_no, tags, file_path, content_text | +| `bid_members` | 投标项目参与人员 | project_id, user_id, user_name, member_role, duty, status | + +种子数据(`init/data.json`,build.sh 幂等写入):`pipelines` 产线主记录 `bidding_general` + +`appcodes` 12 组码表(bid_qc_type / bid_tender_source / bid_member_role / bid_file_type / +bid_scoring_section / bid_qual_match_status / bid_qual_owner / bid_req_type / +bid_chapter_status / bid_review_scope / bid_kb_doc_type / bid_doc_status)。 + +## 对外 API / dspy 端点(wwwroot/) + +部署后挂载在宿主 `wwwroot/pipeline-bidding/`(软链): + +- **api/bid_probe.dspy** — 服务内探针(诊断)。`?action=status` 返回模块装载状态 + + poller 心跳 + 活跃投标项目数;`?action=reconcile` 在服务进程内跑一轮全量对账返回动作明细。 +- **api/bid_task_tree.dspy** — 任务树懒加载(根 → role:阶段 → 任务 → step:/del: 叶子)。 + 项目解析三级兜底:session_settings(default_bidding_general) → agent_settings → + 最近 in_progress 项目。 +- **api/bid_task_io.dspy** — 任务树右侧详情面板:输入 = 任务描述 + QC 改进意见, + 执行步骤,输出 = 交付件(复用 `pipeline_service/task_io_render.py` 统一渲染规范)。 +- **bid_task_tree_popup.ui** — ResourceBrowser 复合 widget 弹窗(左树右详情联动)。 +- **agent/index.ui** — 驾驶舱入口页(顶栏含任务树按钮)。 +- **12 个 CRUD 目录**(bid_tenders / bid_tender_files / bid_scoring_items / + bid_qualifications / bid_doc_requirements / bid_chapters / bid_reviews / bid_scores / + bid_documents / bid_qc_reviews / bid_kb_docs / bid_members)— 每个含 + index.ui + get/add/update/delete_*.dspy 标准五件套。 + +⚠️ RBAC 铁律:`/api/` 前缀不覆盖具体 dspy,**新 API 必须逐条显式注册**(scripts/load_path.py), +否则 403;注册后 redis db0 FLUSHDB 即生效。 + +## load 注册函数 + +宿主入口(pipeline_app.py)两步,缺一不可: + +```python +from pipeline_bidding.init import load_pipeline_bidding +load_pipeline_bidding() # import 后必须再显式调用,漏了无任何日志(静默大坑) +``` + +`load_pipeline_bidding()`(幂等)做三件事: + +1. `role_tool_schemas.register_role_tools()` — 把约 55 个投标角色工具 schema + (read_tender_file / extract_* / add_* / patch_chapters / write_chapter / + review_chapter / start_qc / finish_qc / compose_bid / start_bid_score / + finalize_bid_score / dispatch_analysis_dim 等)注册进引擎 + `pipeline_service.capability_tools.TOOL_SCHEMAS`(引擎 `exec_capability_tool` 的 + `module` 支持全路径,裸名仍走 `pipeline_service.*`,SDLC 零变化)。 +2. `import bid_ability` — 主 agent 能力包 + slash 命令注册(import 即注册)。 +3. `bid_flow.start_poller()` — 状态对账器启动(`PIPELINE_MODE=web` 时不注册, + 与引擎 poller 同款模式开关)。 + +角色工具装配链:角色技能 frontmatter `capability+tools` → `_capability_to_tools_map` +→ `TOOL_SCHEMAS` → **两者交集才是可见工具**(技能声明了但 schema 没注册 = 工具静默缺失)。 +独立脚本验证必须先显式调 `load_pipeline_bidding()`,仅 import 包不触发注册。 + +码表导入自持:`scripts/import_init_bidding.py`(幂等 + 废弃清理 + 产线描述), +不依赖宿主 `import_init.py` 的 INIT_MODULES。 + +## 宿主集成(部署) + +在宿主应用(pipeline-app)根目录或本目录执行一键脚本: + +```bash +./build.sh +``` + +build.sh 职责(运行期禁止 schema 变更,建表只在部署期): + +1. **建表**:读 `mysql.ddl.sql`(json2ddl 产物),把 `drop table if exists` 剔除、 + `CREATE TABLE` 改为 `CREATE TABLE IF NOT EXISTS` 幂等化,用宿主 config 的 + `pipeline` 库连接执行。 +2. **种子数据**:`init/data.json` 幂等写入 pipelines 产线记录 + appcodes/appcodes_kv 码表。 +3. **wwwroot 软链**:`ln -sf 本仓库/wwwroot → $APP_ROOT/wwwroot/pipeline-bidding`。 +4. **pip install**:`$APP_ROOT/py3/bin/pip install 本目录`(非 editable 安装)。 + +宿主负责的后续:app 入口 import + 调用 `load_pipeline_bidding()`; +RBAC 用 `scripts/load_path.py`(其中 set_role_perm.py 必须用宿主根**绝对路径**); +i18n 合并用 merge_i18n.py。 + +## 部署注意(实测踩坑) + +1. **非 editable 安装**:测试机只 `git pull` 仓库不会更新 site-packages,改码后必须 + `./py3/bin/pip install pkgs/pipeline-bidding` 再重启(可对比 site-packages 与仓库文件 + md5 确认加载版本)。 +2. **运行时技能树独立仓库**:在 `/d/pipeline/pipeline-app/skills/`(git: + yumoqing/pipeline-sdlc.git),不是 pipeline_core 的 skills_library;改了 bid-workflow + 等技能必须同步 SKILL.md 到 `pipelines/bidding_general/common/<技能名>/` 再重启。 +3. **宿主入口漏调用 `load_pipeline_bidding()` 是静默大坑**:无任何日志。诊断走 + `api/bid_probe.dspy?action=status` 心跳探针。 +4. **RBAC 缓存**:新路径注册后 redis db0 FLUSHDB 即生效(旧说须重启已过时); + 精确 path 匹配,父路径不覆盖子路径。 +5. **角色任务 failed 必须是对账器阻塞态**:否则每 15s 重复造任务(机构未配 LLM 时必现); + 测试环境无 LLM 模型时任务 failed 属预期。 +6. **自动立项的项目不继承模型**:不带 default_model,解析链兜底全局默认名可能不存在 → 401; + 建后必查/补 `sd_projects.default_model`。 +7. **bid_chapters 键归一**:`resolve_project_id` 先按 id 校验、按 name 反查归一, + 名字歧义拒绝(防「项目名当 project_id」写库产生孤儿骨架)。 +8. 对账器结构铁律:维度过滤(等上游 QC 的不算掉链)必须在 `_open_orch_tasks` 判定前 + 无条件执行,编排任务创建/掉链计数留在「无编排任务在办」的 else 分支内, + 否则每 15s 重复建编排任务堆积。 +9. 共享工具 `create_task` 有产线角色校验守卫(get_role_spec 查不到即拒绝), + `list_roles` 查看本产线可用角色。 + +详细设计见 `docs/DESIGN.md`;技能库见 pipeline-core +`skills_library/pipelines/bidding_general/`(common 7 技能 + roles 9 角色含 bid_biz_writer)。