From 38adaed6e6e76ef9c9029d2b954ad7c0780ef3c2 Mon Sep 17 00:00:00 2001 From: Yuuuu <1569752928@qq.com> Date: Tue, 7 Apr 2026 09:36:40 +0800 Subject: [PATCH] Harden PMFlow context binding and world model snapshots --- CLAUDE.md | 30 +- ...add_mounted_runtime_active_draft_fields.py | 116 ++ backend/app/config/ai_prompts.py | 32 + .../ai_prompts_catalog/part_scenes_01.py | 15 +- .../ai_prompts_catalog/part_scenes_05.py | 15 + backend/app/models/mounted_ai_runtime.py | 5 +- backend/app/schemas/mounted_ai_runtime.py | 20 + .../mixin_deep_guard_answer.py | 190 ++- .../ai_pmo_typed_executor_common.py | 1 + .../ai_pmo_kernel_mixins_ingress_render.py | 51 +- .../intent/ai_pmo_kernel_mixins_intent_llm.py | 21 - .../ai_pmo_kernel_mixins_intent_resolve.py | 75 +- ...i_pmo_kernel_mixins_world_model_session.py | 5 +- .../ai_pmo_kernel_ingress_flow/mixin_auto.py | 280 ++++ .../ai_pmo_kernel_ingress_flow/mixin_entry.py | 376 ++++- .../mixin_execute_query_project.py | 1076 ++++++++++++-- .../services/ai_pmo/draft_confirm_handler.py | 172 ++- .../ai_pmo/semantic_context/__init__.py | 25 + .../ai_pmo/semantic_context/assembler.py | 178 +++ .../semantic_context/decision_schema.py | 39 + .../ai_pmo/semantic_context/gatekeeper.py | 51 + .../semantic_context/mode_card_service.py | 57 + .../ai_pmo/semantic_context/packet_schema.py | 67 + .../semantic_decision_service.py | 39 + .../workflow_semantic_planner/service.py | 22 + .../context_cache_service/mixin_cache_core.py | 23 +- .../mixin_profile_route_parser.py | 4 +- .../services/context_cache_service/shared.py | 1 + .../mixin_reply_outbox.py | 78 +- .../mixin_engine_resolution_core.py | 1 - .../services/llm_service/shared_part_01.py | 23 + .../services/mounted_ai_runtime_service.py | 58 +- .../mounted_ai_runtime_write_service.py | 83 +- backend/static/工单全字段报表_2026-04-03.xlsx | Bin 0 -> 359645 bytes .../test_adaptive_rag_mainline_contract.py | 77 + backend/tests/test_ai_pmo_ingress_mainline.py | 1242 +++++++++++++++-- .../test_context_cache_profile_catalog.py | 94 ++ backend/tests/test_draft_confirm_handler.py | 130 ++ backend/tests/test_model_routing_service.py | 40 + .../test_mounted_ai_runtime_migration.py | 43 + .../tests/test_mounted_ai_runtime_models.py | 6 + .../test_mounted_ai_runtime_write_service.py | 48 + .../tests/test_semantic_context_assembler.py | 141 ++ .../test_semantic_context_runtime_api.py | 165 +++ .../tests/test_semantic_decision_service.py | 60 + .../consultant_dispatch_feishu_runs.jsonl | 1 + frontend/src/api/mountedAi.ts | 38 + .../scheduler-center/SchedulerCenterPage.tsx | 10 + .../MountedAISemanticDecisionPanel.tsx | 94 ++ .../MountedAISemanticPacketPanel.tsx | 175 +++ .../components/MountedAIShadowDiffPanel.tsx | 94 ++ frontend/tsconfig.tsbuildinfo | 2 +- 52 files changed, 5308 insertions(+), 381 deletions(-) create mode 100644 backend/alembic/versions/20260406_add_mounted_runtime_active_draft_fields.py create mode 100644 backend/app/services/ai_pmo/semantic_context/__init__.py create mode 100644 backend/app/services/ai_pmo/semantic_context/assembler.py create mode 100644 backend/app/services/ai_pmo/semantic_context/decision_schema.py create mode 100644 backend/app/services/ai_pmo/semantic_context/gatekeeper.py create mode 100644 backend/app/services/ai_pmo/semantic_context/mode_card_service.py create mode 100644 backend/app/services/ai_pmo/semantic_context/packet_schema.py create mode 100644 backend/app/services/ai_pmo/semantic_context/semantic_decision_service.py create mode 100644 backend/static/工单全字段报表_2026-04-03.xlsx create mode 100644 backend/tests/test_semantic_context_assembler.py create mode 100644 backend/tests/test_semantic_context_runtime_api.py create mode 100644 backend/tests/test_semantic_decision_service.py create mode 100644 frontend/src/pages/scheduler-center/components/MountedAISemanticDecisionPanel.tsx create mode 100644 frontend/src/pages/scheduler-center/components/MountedAISemanticPacketPanel.tsx create mode 100644 frontend/src/pages/scheduler-center/components/MountedAIShadowDiffPanel.tsx diff --git a/CLAUDE.md b/CLAUDE.md index 5dd5d7a4..4704340b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,7 +63,8 @@ backend/ │ ├── 20260324_add_project_create_idempotency_indexes.py # 项目创建幂等索引:活动项目标题大小写无关唯一 │ ├── 20260327_cleanup_model_routing_legacy_names.py # model_routing 旧函数名清理与重命名迁移 │ ├── 20260405_add_mounted_ai_runtime_tables.py # Mounted Runtime 建表迁移 -│ └── 20260405_add_mounted_runtime_plan_state_fields.py # Mounted Runtime plan_status / conversation_scope_key 字段 +│ ├── 20260405_add_mounted_runtime_plan_state_fields.py # Mounted Runtime plan_status / conversation_scope_key 字段 +│ └── 20260406_add_mounted_runtime_active_draft_fields.py # Mounted draft active work item / message anchor 字段 ├── scripts/ # 后端运维/数据修复脚本 │ ├── backfill_orphan_signal_links.py # 回填孤立 signal 的 project_signal_links │ ├── dev/ # 开发辅助脚本(check_users/seed_all) @@ -196,19 +197,21 @@ backend/ - 新增:`backend/app/utils/project_command_utils.py`(项目创建标题/描述提取与归一化辅助) - 新增:`backend/app/services/ai_pmo_intent_routing.py`(AI PMO 意图/路由常量与解析器收口,统一 `family.action` 路由映射) - 新增:`backend/app/services/ai_pmo/workflow_semantic_planner/`(统一业务语义规划层:唯一允许承载 workflow bundle 语义关键词/正则/兼容 helper 的目录) +- 新增:`backend/app/services/ai_pmo/semantic_context/`(OpenCow 风格语义上下文子模块:统一 `SemanticContextPacket/ModeCard/SemanticDecision/DecisionGateResult` 契约、`SemanticContextAssembler`、`SemanticDecisionGatekeeper` 与 `SemanticDecisionService`;当前已接入 ingress shadow 观测,并为 intent/query-scene 主链提供 direct metadata 替代语义入口) - 新增:`backend/app/models/reply_iteration.py`(回复质量迭代模型:`QualityAsset/QualityAssetPair/JudgeRun/OptimizerCandidate/PromptRelease`) - 新增:`backend/app/models/training_pipeline.py`(训练/微调管线模型:`TrainingDataset/TrainingJob/TrainingEvaluation/TrainedModelArtifact/TrainedModelDeployment`) -- 新增:`backend/app/models/mounted_ai_runtime.py`(Mounted Runtime 运行时模型:`MountedTopicWorkspace/MountedDraftWorkspace/MountedConversationRun/MountedAgentTrace/MountedSourceActivation/MountedToolCapabilityPolicy`;`MountedTopicWorkspace` 额外承载 `conversation_scope_key + plan_status`,用于同群同人 topic 隔离与 plan 状态机) +- 新增:`backend/app/models/mounted_ai_runtime.py`(Mounted Runtime 运行时模型:`MountedTopicWorkspace/MountedDraftWorkspace/MountedConversationRun/MountedAgentTrace/MountedSourceActivation/MountedToolCapabilityPolicy`;`MountedTopicWorkspace` 额外承载 `conversation_scope_key + plan_status`,用于同群同人 topic 隔离与 plan 状态机;`MountedDraftWorkspace` 额外承载 `active_work_item_id + latest_source_message_id + latest_reply_message_id`,用于当前 active draft 绑定与 reply anchor 锚定) - 新增:`backend/app/services/reply_iteration/service.py`(反馈中心提升为学习资产、裁判分桶、候选改动、prompt release trial 服务;支持 `backfill_mounted_training_assets_from_feedback`:从真实 `feedback_center_records + qa_records` 回填 golden/failure、preference pair 与 judge runs,供 mounted runtime 真实业务训练使用) - 新增:`backend/app/services/training_pipeline/service.py`(训练数据集、训练作业、评测、artifact promote、deployment 决策服务;支持 `bootstrap_mounted_training_wave`:自动从 mounted runtime 质量资产生成 `teacher_dev_set / teacher_eval_frozen_set / sft / preference`,并启动 low/high 训练波次;mounted wave 会立刻消费本次 queued jobs,产出 completed job/evaluation/acceptance gate;当 failure 样本带 `corrected_answer` 时会自动合成 synthetic golden / preference pair) - 新增:`backend/app/services/prompt_release/service.py`(只读 QA prompt release 版本管理与 serving 渲染优先级) -- 新增:`backend/app/services/mounted_ai_runtime_service.py`、`backend/app/services/mounted_ai_runtime_write_service.py`(Mounted Runtime 读模型与写路径:来源/主题/run 查询、source activation / tool policies / training ops / hard cases / replay-run 管理,以及 ingress topic/run/trace/source-activation 落库;当某来源存在 active activation 白名单时,仅命中的 scope 才进入 mounted tracking;run detail 会返回 rich trace:`input_summary/output_summary/tool_trace`;写路径会持久化 `conversation_scope_key` 与显式 `plan_status`) +- 新增:`backend/app/services/mounted_ai_runtime_service.py`、`backend/app/services/mounted_ai_runtime_write_service.py`(Mounted Runtime 读模型与写路径:来源/主题/run 查询、source activation / tool policies / training ops / hard cases / replay-run 管理,以及 ingress topic/run/trace/source-activation 落库;当某来源存在 active activation 白名单时,仅命中的 scope 才进入 mounted tracking;run detail 会返回 rich trace:`input_summary/output_summary/tool_trace`,并暴露 `topic/draft/semantic_packet_summary/semantic_decision/semantic_gate_result/semantic_diff`;写路径会持久化 `conversation_scope_key`、显式 `plan_status` 与 semantic shadow 观测结果) - 新增:`backend/app/api/ai_steward_api/routes_reply_iteration.py`(AI Steward 回复质量闭环接口:反馈提升、judge-runs、candidates、prompt-releases、trials,以及 `POST /ai-steward/reply-iteration/backfill-mounted-assets` 真实业务训练资产回填入口) - 新增:`backend/app/api/ai_steward_api/routes_training_pipeline.py`(AI Steward 训练管线接口:datasets、jobs、evaluations、artifacts、deployments,以及 `POST /ai-steward/training/mounted-wave` 一键训练波次入口、`POST /ai-steward/training/jobs/{id}/run` 单 job 执行入口) - 新增:`backend/app/api/ai_steward_api/routes_mounted_ai.py`、`backend/app/schemas/mounted_ai_runtime.py`(Mounted Runtime 控制台接口:sources/source-activations/tool-policies/topics/runs/run-detail/cutover-status/eval-suites/training-ops/hard-cases,以及 admin/source-activation、admin/tool-policy、admin/replay-run) - 新增:`backend/alembic/versions/20260327_add_reply_iteration_training_tables.py`(回复质量迭代/训练管线主迁移:学习资产表、prompt release、训练数据集、训练作业与 experiment 扩展字段) - 新增:`backend/alembic/versions/20260405_add_mounted_ai_runtime_tables.py`(Mounted Runtime 建表迁移:topic workspace、draft workspace、conversation run、agent trace、source activation、tool capability policy) - 新增:`backend/alembic/versions/20260405_add_mounted_runtime_plan_state_fields.py`(Mounted Runtime 计划态增强迁移:`mounted_topic_workspaces` 新增 `conversation_scope_key / plan_status`,支撑同群同人多 topic 隔离与 plan 状态机) +- 新增:`backend/alembic/versions/20260406_add_mounted_runtime_active_draft_fields.py`(Mounted Runtime active draft 锚点迁移:`mounted_draft_workspaces` 新增 `active_work_item_id / latest_source_message_id / latest_reply_message_id`,支撑 session-first 草稿确认与最新回包锚定) - 新增:`backend/tests/test_reply_iteration_service.py`、`test_training_pipeline_service.py`、`test_reply_iteration_api.py`、`test_reply_iteration_training_migration.py`(回复质量闭环与训练管线基础回归) - 新增:`backend/tests/test_mounted_ai_runtime_models.py`、`test_mounted_ai_runtime_migration.py`、`test_mounted_ai_runtime_api.py`、`test_mounted_ai_runtime_write_service.py`(Mounted Runtime 模型/迁移/API/写路径回归) - 新增:`backend/app/services/ai_pmo/ai_pmo_kernel/ai_pmo_workflow_runtime.py`(AI PMO `ContextAssembler` + `WorkflowRegistry/WorkflowHandler` 运行时骨架) @@ -228,10 +231,11 @@ backend/ - 更新:`backend/app/services/signal/signal_capture_service/mixin_capture_entry.py`(Feishu capture 在 source activation 存在 active 白名单规则时,未命中 scope 的 chat 直接返回 `mounted_runtime_scope_not_activated`,不再继续进入 kernel/queue) - 更新:`backend/app/services/signal/signal_capture_service/mixin_capture_entry.py`(kernel 未处理或 capture 主链异常时会回滚当前 session 并把真实失败原因写入 `direct_reply_text`,不再回退统一“系统处理中出现异常”伪成功文案) - 更新:`backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_auto.py`(写操作改为“先起草后确认”,状态进入 `draft_pending_confirm`) -- 更新:`backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_entry.py`(会话命中 `draft_pending_confirm` 时优先路由草稿确认处理,并把 ingress 主链同步写入 Mounted Runtime 的 topic/run/trace;除主脑 `ConversationOrchestrator` 外,还会额外记录 `PlanCoach/Clarifier/DraftSynthesizer/Executor` 等 specialist trace;`query.answer` + `business_review/assignee` 场景会在 runtime 中收口到 `plan/chat`,不再伪装成 `execute/Executor`;kernel 失败改为 `logger.exception` 保留完整 traceback) -- 更新:`backend/app/services/ai_pmo/ai_pmo_typed_executor_execute/mixin_execute_query_project.py`(query `failed_safe` 只在 SQLAlchemy session 已进入 failed/inactive 状态时回滚,避免把同链路的 ingest/work_item 一起回滚掉;需求收敛/范围建议/负责人推荐/项目存在性查询会优先走 deterministic 直答,不再默认落到 heavy RAG;“一个 AI 客服工具,这个项目有吗”这类问句会直接返回项目事实库命中结果) +- 更新:`backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_entry.py`(会话命中 `draft_pending_confirm` 时优先路由草稿确认处理,并把 ingress 主链同步写入 Mounted Runtime 的 topic/run/trace;除主脑 `ConversationOrchestrator` 外,还会额外记录 `PlanCoach/Clarifier/DraftSynthesizer/Executor` 等 specialist trace;`query.answer` + `business_review/assignee` 场景会在 runtime 中收口到 `plan/chat`,不再伪装成 `execute/Executor`;query-scene 强制改为优先读取 direct metadata,不再在主链回填 `semantic_bundle`;kernel 失败改为 `logger.exception` 保留完整 traceback) +- 更新:`backend/app/services/ai_pmo/ai_pmo_typed_executor_execute/mixin_execute_query_project.py`(query `failed_safe` 只在 SQLAlchemy session 已进入 failed/inactive 状态时回滚,避免把同链路的 ingest/work_item 一起回滚掉;需求收敛/范围建议/负责人推荐/项目存在性查询会优先走 deterministic 直答,不再默认落到 heavy RAG;query executor 对 `message_intent/consultant_mode_preference/advise_subtype/force_thinking_mode` 改为 direct metadata first,旧 `semantic_bundle.diagnostics` 仅保留兼容兜底;`requirement_plan` followup 在存在 active workspace 时优先按 mounted runtime 状态与字段增量续接,不再先依赖旧 planner 猜 continuation;“一个 AI 客服工具,这个项目有吗”这类问句会直接返回项目事实库命中结果) +- 更新:`backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_resolve.py`、`ai_pmo_kernel_mixins_intent_llm.py`、`backend/app/services/ai_pmo/ai_pmo_kernel/mixins/ingress/ai_pmo_kernel_mixins_ingress_render.py`(意图解析主链移除 `signal_source.onboard` 的 pre-LLM 规则锁;LLM metadata 不再注入 `semantic_bundle`;capture result 仅生成 `direct_metadata_compat` 形式的兼容语义包,不再为回包临时重跑 `workflow_semantic_planner`) - 更新:`backend/app/services/adaptive_rag_service/mixin_followup_anchor.py`、`mixin_fast_answer_project_main.py`(standalone 项目存在性问句不再误判成 referential followup;项目风险/阻塞问句在无任务拆解时返回“缺任务分解、缺里程碑、缺负责人推进节奏”的风险代理信号,不再给空泛兜底) -- 更新:`backend/app/services/ai_pmo/ai_pmo_typed_executor_discovery/mixin_discover.py`、`backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_resolve.py`(`signal_source.onboard` 显式接入请求会在 LLM 前锁定 intent,禁止再漂移成 `project_signal.discover`;discovery mixin 补齐 `_safe_int/_utc_now_naive` 导入,消除真实 discovery 执行时的 `NameError`) +- 更新:`backend/app/services/ai_pmo/ai_pmo_typed_executor_discovery/mixin_discover.py`、`backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_resolve.py`(discovery mixin 补齐 `_safe_int/_utc_now_naive` 导入,消除真实 discovery 执行时的 `NameError`;`signal_source.onboard` 不再走 pre-LLM 规则锁,主链保持 model-only,再由后续 route/gate 约束) - 更新:`backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_rules.py`(canonical intent builder 现在保留 `project.update/task.* / personnel.* / knowledge.* / notification.*` 等 mutation intent,避免 LLM 已识别的写操作被错误降级成 `ignore`) - 更新:`backend/app/models/work_item.py`(新增 `draft_content/draft_expires_at/draft_version/confirm_message_id`) - 新增:`backend/app/services/ai_pmo/ai_pmo_kernel/compositions/world_model.py`(Kernel world-model 组合入口) @@ -399,8 +403,8 @@ frontend/ - 更新:`frontend/src/pages/CharterDetailPage.tsx`、`ContractSyncBoardPage.tsx`、`DashboardPage.tsx`、`KnowledgeGraphContextBoardPage.tsx`、`LoginPage.tsx`、`ModelRoutingConfigPage.tsx`、`SchedulerCenterPage.tsx`、`SkillTasksPage.tsx`、`SkillsPage.tsx`、`TaskBriefCenterPage.tsx`、`TaskBriefRunsPage.tsx`、`TaskBriefTemplatePage.tsx`、`UserProfilePage.tsx`、`UserSkillsPage.tsx`、`WorkloadDetailPage.tsx`、`WorkloadsPage.tsx`(统一降级为轻量入口,转发到 `pages/*/` 子目录) - 新增:`frontend/src/pages/charter-detail/CharterDetailPage.tsx`、`contract-sync-board/ContractSyncBoardPage.tsx`、`dashboard/DashboardPage.tsx`、`knowledge-graph-context-board/KnowledgeGraphContextBoardPage.tsx`、`login/LoginPage.tsx`、`model-routing-config/ModelRoutingConfigPage.tsx`(剩余核心页面目录化收敛) - 新增:`frontend/src/pages/scheduler-center/SchedulerCenterPage.tsx`、`skill-tasks/SkillTasksPage.tsx`、`skills/SkillsPage.tsx`、`task-brief-center/TaskBriefCenterPage.tsx`、`task-brief-runs/TaskBriefRunsPage.tsx`、`task-brief-template/TaskBriefTemplatePage.tsx`(任务/调度/技能页面目录化收敛) -- 新增:`frontend/src/pages/scheduler-center/components/MountedAISourceStatusPanel.tsx`、`MountedAISourceActivationPanel.tsx`、`MountedAIToolGuardrailsPanel.tsx`、`MountedAITopicsPanel.tsx`、`MountedAIRunsPanel.tsx`、`MountedAIRunReplayPanel.tsx`、`MountedAIOrchestrationGraphPanel.tsx`、`MountedAIContextLayersPanel.tsx`、`MountedAITrainingOpsPanel.tsx`、`MountedAIEvalScoreboardPanel.tsx`、`MountedAIHardCasesPanel.tsx`、`MountedAIAcceptancePanel.tsx`(Mounted AI Console 来源状态、白名单 cutover、tool guardrails、topic workspace、run 列表、replay 详情、agent 编排图、context layers、training ops、eval score board、hard cases 与验收门槛面板;其中 source activation / tool guardrails 已支持前台编辑,run replay 支持直接触发 `admin/replay-run`,training ops 已支持直接发起 mounted training wave,并展示 recent datasets / recent jobs / acceptance gate,replay/orchestration 已展示 rich trace 的 `input/output/tool` 明细) -- 更新:`frontend/src/pages/scheduler-center/SchedulerCenterPage.tsx`(Scheduler Center 升级为 Mounted AI Console:接入 mounted runtime 查询与 admin actions,支持 source activation、tool policy、run replay、training wave 的前台操作,并支持通过 `mountedRunId` 查询参数直接定位 run) +- 新增:`frontend/src/pages/scheduler-center/components/MountedAISourceStatusPanel.tsx`、`MountedAISourceActivationPanel.tsx`、`MountedAIToolGuardrailsPanel.tsx`、`MountedAITopicsPanel.tsx`、`MountedAIRunsPanel.tsx`、`MountedAIRunReplayPanel.tsx`、`MountedAIOrchestrationGraphPanel.tsx`、`MountedAIContextLayersPanel.tsx`、`MountedAISemanticPacketPanel.tsx`、`MountedAISemanticDecisionPanel.tsx`、`MountedAIShadowDiffPanel.tsx`、`MountedAITrainingOpsPanel.tsx`、`MountedAIEvalScoreboardPanel.tsx`、`MountedAIHardCasesPanel.tsx`、`MountedAIAcceptancePanel.tsx`(Mounted AI Console 来源状态、白名单 cutover、tool guardrails、topic workspace、run 列表、replay 详情、agent 编排图、context layers、semantic packet / decision / shadow diff、training ops、eval score board、hard cases 与验收门槛面板;其中 source activation / tool guardrails 已支持前台编辑,run replay 支持直接触发 `admin/replay-run`,training ops 已支持直接发起 mounted training wave,并展示 recent datasets / recent jobs / acceptance gate) +- 更新:`frontend/src/pages/scheduler-center/SchedulerCenterPage.tsx`(Scheduler Center 升级为 Mounted AI Console:接入 mounted runtime 查询与 admin actions,支持 source activation、tool policy、run replay、training wave 的前台操作,并支持通过 `mountedRunId` 查询参数直接定位 run;run detail 区新增 semantic packet / decision / gate / shadow diff 观测区) - 新增:`frontend/src/pages/user-profile/UserProfilePage.tsx`、`user-skills/UserSkillsPage.tsx`、`workload-detail/WorkloadDetailPage.tsx`、`workloads/WorkloadsPage.tsx`(用户与负载页面目录化收敛) - 更新:`frontend/src/pages/InboxPage.tsx`(降级为轻量入口,转发到 `pages/inbox/InboxPage`) - 新增:`frontend/src/pages/inbox/helpers.ts`(Inbox 页面 tab 过滤、排序、project-scope 判断 helper) @@ -578,12 +582,16 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 - `workflow_semantic_planner` 也必须把通用需求收敛里的短槽位回答继续识别为 `message_intent=requirement_plan`,例如“先给内部 PM 和研发负责人用 / 飞书总结就行 / 每次会后半小时内整理出来”;这类碎片化回复不得因为没有新 `semantic_bundle` 就退回普通 query - `ExecuteQueryProjectMixin` 对 `message_intent=requirement_plan` 的查询必须优先走 mounted runtime 直答快路径:默认只输出自然短话术,每轮最多只保留 1 个关键问题;当 `scoring_basis/target_user/output_form/cadence/first_scope` 收集完成时,必须直接整理成需求草稿,禁止继续空转追问 - `requirement_plan` 的 Mounted Runtime 状态必须显式落库到 `MountedTopicWorkspace.plan_status`:至少区分 `idle / collecting / options_ready / plan_ready / awaiting_execute_confirm / suspended`,禁止只靠 `draft.state` 或 `metadata_json` 猜当前阶段 +- 新的 OpenCow 风格语义上下文链必须先组装 `SemanticContextPacket`,再由模型返回 `SemanticDecision`,最后由 gatekeeper 决定 `plan_status`;当前 shadow 结果已进入 Mounted Runtime 观测,同时 intent/query-scene 主链已切到 direct metadata,不得再把 `semantic_bundle` 当作主链真源或绕过统一执行门禁 - 正在收敛中的 `requirement_plan` 必须支持普通人碎片化回复:像“技术指标吧 / 给投顾看 / 飞书日报就行 / 每天收盘后发”这种单字段短句,即使缺少新的 `semantic_bundle`,只要同线程/同群同人存在 active plan workspace,也必须自动续接当前需求收敛,不得回落到通用 query 或高阶 RAG - `requirement_plan` 的追问节奏必须像自然聊天:上一轮只确认新增信息、下一轮只问一个缺口;初始描述中的“每日/自动”不能过早推断成已确认的更新频率,避免少问关键问题或提前出草稿 - `requirement_plan` 必须按 profile 区分场景:`scoring` 需求继续收集 `打分依据/目标用户/输出形式/更新频率/第一版范围`,`generic` 需求收集 `输入材料/目标用户/输出形式/响应时效/第一版范围`;像“会议纪要提待办”这类普通工具需求,禁止再复用“这个分数怎么打”的旧问题模板 +- `generic requirement_plan` 的 `A/B/C` 选项只能出现在 active requirement session 的 `collecting` 轮次里,并且只允许绑定当前缺口字段(如 `target_user / output_form / cadence`);普通闲聊、query.answer、非 requirement followup 必须继续保持自由文案,禁止全局套用选项模板 +- 用户显式说“聊个新需求 / 另一个需求 / 跟上一个无关”时,AI PMO 必须先清掉当前会话里继承的旧 `current_project_id`,避免后续短回复继续被旧项目上下文污染 +- 裸 `A/B/C/确认/你自行推荐` 这类低信息量回复,如果没有命中当前 active requirement session 或待确认草稿,必须直接提示“当前没有进行中的收敛轮次/草稿”,禁止再回落到 `query.answer` 检索图谱并生成旧项目结论 - 飞书私聊中的“答错了/不对/调用高级模型/用更强模型/重新回答”必须被识别为 `correction_escalation` 指令:优先复用上一轮上下文主题并强制深度回答,禁止把这类话术当成新的系统配置查询 - `POST /rag/ask` 在 `rag_request_context` 或统一语义规划层命中 `business_review/correction_escalation` 时,必须强制留在 `query.read_only` 主链并进入对应 QA Prompt;禁止因为 LLM 意图分类把“分析/MVP/重答”误送到 `signal.create -> draft_pending_confirm` -- Feishu 新消息即使被低阶 LLM 先判成 `project.create`,只要统一语义规划层从原句识别出 `requirement_plan/business_review/correction_escalation`,就必须在 ingress `_coerce_intent_for_query_scene` 阶段强制拉回 `query.answer`;Mounted Runtime 记录 `mode_after/agent` 时也必须优先读取 `semantic_bundle.diagnostics`,禁止因顶层 `route_metadata` 缺字段把真实 `plan` 记成 `chat` +- Feishu 新消息即使被低阶 LLM 先判成 `project.create`,只要统一语义规划层从原句识别出 `requirement_plan/business_review/correction_escalation`,就必须在 ingress `_coerce_intent_for_query_scene` 阶段强制拉回 `query.answer`;Mounted Runtime 记录 `mode_after/agent` 时必须优先读取顶层 direct metadata,`semantic_bundle.diagnostics` 只允许作为兼容回退,禁止因顶层 `route_metadata` 缺字段把真实 `plan` 记成 `chat` - 飞书入口的高精度同步判定不能只看 `primary_workflow`;只要 `WorkflowSemanticPlanner` 的 `diagnostics.message_intent=requirement_plan`,就必须优先返回 `semantic:requirement_plan` 并直接同步进入 AI PMO kernel,转发 `semantic_bundle`,禁止再先入异步队列发送 `已收到,正在处理` ACK - “高级模型是否启用/模型路由是否启用/当前是否高配”这类运行态问题,只有命中运行态配置、路由快照、引擎配置或真实调用记录时才允许下结论;若证据只有页面目录、代码图谱或功能清单,回复必须明确为“无法仅凭静态资料确认” - 通用行业趋势问答(如“宠物行业最新动态”)不得注入数据库、项目历史、用户历史或外部实时搜索结果;只有显式带项目上下文时才允许走 `external_trend`,对“不要提项目/别提项目”这类否定语句不能误判为项目上下文 @@ -612,7 +620,7 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 - `model-routing` 只暴露逻辑层字段:`function_name/tier_alias/source/rule_name/rule_priority/last_execution`;`PUT` 仅接受 `tier_alias=low|high` - 回复质量闭环接口:参考 `backend/app/api/ai_steward_api/routes_reply_iteration.py`(`POST /ai-steward/feedback-center/items/{id}/promote`、`POST /ai-steward/reply-iteration/backfill-mounted-assets`、`/reply-iteration/judge-runs/candidates/prompt-releases/trials`) - 训练/微调管线接口:参考 `backend/app/api/ai_steward_api/routes_training_pipeline.py`(`GET/POST /ai-steward/training/datasets/jobs`、`POST /training/jobs/{id}/run`、`GET /training/jobs/{id}/evaluations`、`POST /training/artifacts/{id}/promote`、`POST /training/deployments/{id}/decision`、`POST /training/mounted-wave`;mounted wave 会自动生成 `teacher_dev_set / teacher_eval_frozen_set / sft / preference` 并启动并执行 low/high 训练波次) -- Mounted Runtime 控制台接口:参考 `backend/app/api/ai_steward_api/routes_mounted_ai.py`(`GET /ai-steward/mounted-ai/sources|source-activations|tool-policies|topics|runs|runs/{id}|cutover-status|eval-suites|eval-suites/{id}|training-ops|hard-cases`,以及 `POST /ai-steward/mounted-ai/admin/source-activation|tool-policy|replay-run`,统一 `{code,message,data}` 包络);其中 `cutover-status` 必须返回模块级旧脑退役视图:`legacy_cutover_state / retired_item_count / active_item_count / legacy_cutover_items[]` +- Mounted Runtime 控制台接口:参考 `backend/app/api/ai_steward_api/routes_mounted_ai.py`(`GET /ai-steward/mounted-ai/sources|source-activations|tool-policies|topics|runs|runs/{id}|cutover-status|eval-suites|eval-suites/{id}|training-ops|hard-cases`,以及 `POST /ai-steward/mounted-ai/admin/source-activation|tool-policy|replay-run`,统一 `{code,message,data}` 包络);其中 `runs/{id}` 必须返回 `topic/draft/semantic_packet_summary/semantic_decision/semantic_gate_result/semantic_diff`,`cutover-status` 必须返回模块级旧脑退役视图:`legacy_cutover_state / retired_item_count / active_item_count / legacy_cutover_items[]` - 任务包中心接口:参考 `backend/app/api/task_briefs.py`(`/task-briefs/center/templates/runs/generate/contract-sync`) - KG 上下文闭环接口:参考 `backend/app/api/knowledge_graph.py`(`GET /ai-steward/knowledge-graph/capability-summary`、`POST /ai-steward/knowledge-graph/verify-context-consumption`、`POST /ai-steward/knowledge-graph/sync/work-items`;旧 `POST /ai-steward/knowledge-graph/sync/feishu` 仅保留兼容别名) - 需求点落信号源:参考 `backend/app/services/signal/signal_source_orchestration/mixin_registry_execute.py`(`materialize_requirement_sources` 负责归一、去重、`raw_context` 持久化,并为每条 registry 同步绑定/物化正式 `Signal`) @@ -690,6 +698,8 @@ uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 - World Model 静态层已接入 Redis 缓存(key: `wm:static:{tenant_id}:{project_id}`,TTL 600s) - 写操作后主动 invalidate,动态层不缓存 - AI PMO 意图识别已切换 `model-only`:`_resolve_intent` 仅接受 LLM 结构化结果,不再走规则/正则兜底链路 +- `semantic_context` 新链当前状态:ingress shadow 观测已落库,intent/query-scene/render 主链已切到 direct metadata,旧 `semantic_bundle/workflow_semantic_planner` 仍仅在部分 requirement-plan executor / followup 兼容路径中生效;后续删旧规则时应继续以 Mounted Runtime 观测页核对 shadow diff +- IT 五轮 requirement plan 现以 `MountedDraftWorkspace.active_work_item_id + latest_*_message_id` 锚定当前 active draft;历史未绑定这些字段的旧草稿不会自动认领,需先重新生成当前草稿后再确认/修改 - AI PMO / Feishu 执行门已取消置信度阈值拦截与高风险人工复核:`confidence` 仅保留在 `decision_trace` / processing log 中作观测,不再作为自动执行的 reject 条件;当前门禁仅保留 `force_manual` 与 `correction_action` - 模型默认值与 provider/tier 映射以 `backend/.openclaw/pmflow-routing-rules.json` 为唯一配置源;`.env` / `.env.example` 仅保留 API key 与运行开关,`OPENCLAW_ROUTING_RULES_PATH` 只用于定位这份文件 - 调用层与路由页面只使用 `low/high` 两层语义;`safety` 已废弃并从配置校验、运行时解析、前端选项中移除 diff --git a/backend/alembic/versions/20260406_add_mounted_runtime_active_draft_fields.py b/backend/alembic/versions/20260406_add_mounted_runtime_active_draft_fields.py new file mode 100644 index 00000000..09cb9f58 --- /dev/null +++ b/backend/alembic/versions/20260406_add_mounted_runtime_active_draft_fields.py @@ -0,0 +1,116 @@ +"""add mounted runtime active draft fields + +Revision ID: 20260406_add_mounted_runtime_active_draft_fields +Revises: 20260405_add_mounted_runtime_plan_state_fields +Create Date: 2026-04-06 +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +revision = "20260406_add_mounted_runtime_active_draft_fields" +down_revision = "20260405_add_mounted_runtime_plan_state_fields" +branch_labels = None +depends_on = None + + +def _column_exists(bind, table_name: str, column_name: str) -> bool: + inspector = sa.inspect(bind) + try: + columns = inspector.get_columns(table_name) + except Exception: + return False + return any(str(column.get("name")) == column_name for column in columns) + + +def _index_exists(bind, table_name: str, index_name: str) -> bool: + inspector = sa.inspect(bind) + try: + indexes = inspector.get_indexes(table_name) + except Exception: + return False + return any(str(index.get("name")) == index_name for index in indexes) + + +def upgrade() -> None: + bind = op.get_bind() + + if not _column_exists(bind, "mounted_draft_workspaces", "active_work_item_id"): + op.add_column( + "mounted_draft_workspaces", + sa.Column("active_work_item_id", sa.BigInteger(), nullable=True), + ) + if not _column_exists(bind, "mounted_draft_workspaces", "latest_source_message_id"): + op.add_column( + "mounted_draft_workspaces", + sa.Column("latest_source_message_id", sa.VARCHAR(length=128), nullable=True), + ) + if not _column_exists(bind, "mounted_draft_workspaces", "latest_reply_message_id"): + op.add_column( + "mounted_draft_workspaces", + sa.Column("latest_reply_message_id", sa.VARCHAR(length=128), nullable=True), + ) + + if not _index_exists(bind, "mounted_draft_workspaces", "ix_mounted_draft_workspaces_active_work_item_id"): + op.create_index( + "ix_mounted_draft_workspaces_active_work_item_id", + "mounted_draft_workspaces", + ["active_work_item_id"], + ) + if not _index_exists(bind, "mounted_draft_workspaces", "ix_mounted_draft_workspaces_latest_source_message_id"): + op.create_index( + "ix_mounted_draft_workspaces_latest_source_message_id", + "mounted_draft_workspaces", + ["latest_source_message_id"], + ) + if not _index_exists(bind, "mounted_draft_workspaces", "ix_mounted_draft_workspaces_latest_reply_message_id"): + op.create_index( + "ix_mounted_draft_workspaces_latest_reply_message_id", + "mounted_draft_workspaces", + ["latest_reply_message_id"], + ) + + op.execute( + sa.text( + """ + UPDATE mounted_draft_workspaces AS draft + SET latest_source_message_id = COALESCE( + NULLIF(draft.latest_source_message_id, ''), + ( + SELECT topic.last_message_id + FROM mounted_topic_workspaces AS topic + WHERE topic.id = draft.topic_workspace_id + ) + ) + WHERE COALESCE(NULLIF(draft.latest_source_message_id, ''), '') = '' + """ + ) + ) + + +def downgrade() -> None: + bind = op.get_bind() + if _index_exists(bind, "mounted_draft_workspaces", "ix_mounted_draft_workspaces_latest_reply_message_id"): + op.drop_index( + "ix_mounted_draft_workspaces_latest_reply_message_id", + table_name="mounted_draft_workspaces", + ) + if _index_exists(bind, "mounted_draft_workspaces", "ix_mounted_draft_workspaces_latest_source_message_id"): + op.drop_index( + "ix_mounted_draft_workspaces_latest_source_message_id", + table_name="mounted_draft_workspaces", + ) + if _index_exists(bind, "mounted_draft_workspaces", "ix_mounted_draft_workspaces_active_work_item_id"): + op.drop_index( + "ix_mounted_draft_workspaces_active_work_item_id", + table_name="mounted_draft_workspaces", + ) + if _column_exists(bind, "mounted_draft_workspaces", "latest_reply_message_id"): + op.drop_column("mounted_draft_workspaces", "latest_reply_message_id") + if _column_exists(bind, "mounted_draft_workspaces", "latest_source_message_id"): + op.drop_column("mounted_draft_workspaces", "latest_source_message_id") + if _column_exists(bind, "mounted_draft_workspaces", "active_work_item_id"): + op.drop_column("mounted_draft_workspaces", "active_work_item_id") diff --git a/backend/app/config/ai_prompts.py b/backend/app/config/ai_prompts.py index f94fb736..b74eb427 100644 --- a/backend/app/config/ai_prompts.py +++ b/backend/app/config/ai_prompts.py @@ -66,6 +66,37 @@ from .ai_prompt_registry import ( # Deprecated compatibility aliases kept outside the registry surface. PROMPT_FEISHU_REQUIREMENT_ANALYSIS = PROMPT_FEISHU_PROJECT_SIGNAL_CLASSIFICATION PROMPT_TASK_BREAKDOWN = PROMPT_DECOMPOSE_TASKS +PMFLOW_SEMANTIC_CONTEXT_DECISION_PROMPT = """ +你是 PMFlow 的统一语义规划层。 + +你的输入是系统整理好的 semantic context packet。你的职责不是执行写操作,而是: +1. 判断用户这句话是否继续当前 topic。 +2. 判断当前 workflow 是否继续停留在 plan。 +3. 给出 draft patch 建议。 +4. 给出要回复给用户的内容。 +5. 明确 requested_actions 与是否需要确认。 + +硬约束: +- 只能依据 packet 中的事实、草稿、recent turns、mode card 判断。 +- 不得把推断包装成事实。 +- 不得请求 mode card 明确禁止的动作。 +- 输出必须是一个 JSON 对象,不要输出 Markdown。 + +JSON schema: +{ + "topic_relation": "continue_current_topic | new_topic | uncertain", + "mode_decision": "stay_plan | ready_confirm | request_execute | answer_only", + "draft_patch": {}, + "assistant_reply": "string", + "requires_confirmation": true, + "requested_actions": ["reply.compose"], + "confidence": 0.0, + "uncertainties": [] +} + +semantic_context_packet: +{semantic_packet} +""".strip() def build_default_role_prompt_bundle() -> dict: @@ -198,6 +229,7 @@ __all__ = [ "PROMPT_AI_PMO_REQUIREMENT_PLAN_QA", "PROMPT_EXTERNAL_TREND_QA", "PROMPT_INDUSTRY_TREND_QA", + "PMFLOW_SEMANTIC_CONTEXT_DECISION_PROMPT", "PROMPT_REGISTRY", "PROMPT_REGISTRY_MAP", "get_prompt_registry", diff --git a/backend/app/config/ai_prompts_catalog/part_scenes_01.py b/backend/app/config/ai_prompts_catalog/part_scenes_01.py index fe745cdc..50c82617 100644 --- a/backend/app/config/ai_prompts_catalog/part_scenes_01.py +++ b/backend/app/config/ai_prompts_catalog/part_scenes_01.py @@ -10,7 +10,9 @@ FEISHU_QA_RESPONSE_STYLE = _compose_prompt( 2. 用正式、克制、面向协作的中文,像资深项目经理而不是聊天机器人。 3. 如果能用数字、项目名、负责人、时间点表达,请直接写明。 4. 若信息不足,必须明确说明缺少什么,最多列 3 项。 -5. 禁止自我介绍、寒暄、思考过程、数据库/模型/接口实现细节。 +5. 可使用 1-3 个轻量 emoji 做分段,但不要花哨,不要影响专业感。 +6. 飞书输出优先短段落和清晰换行;如果包含“结论/依据/下一步/信息不足”,要让这些段落一眼可扫读。 +7. 禁止自我介绍、寒暄、思考过程、数据库/模型/接口实现细节。 """.strip(), TEXT_OUTPUT_GUARDRAIL, ) @@ -21,7 +23,7 @@ PROMPT_FEISHU_DIRECT_QA = _compose_prompt( """ 回答要求: 1. 仅基于给定上下文回答,优先使用结构化数据。 -2. 如果问题涉及项目状态、进展、成员或任务,请优先给出可验证的事实和数字。 +2. 如果问题涉及项目状态、进展、成员或任务,请优先引用项目画像、检索证据和用户焦点里的可验证事实与数字。 3. 如果问题涉及选择、建议或判断,直接给出明确结论和 1-2 条理由。 4. 信息不足时,直接写“信息不足,缺少:...”并列出关键缺失项。 5. 控制在 220 字以内,适合飞书消息阅读。 @@ -30,10 +32,11 @@ PROMPT_FEISHU_DIRECT_QA = _compose_prompt( 当前项目ID:{project_id} 问题:{question} -System Snapshot:{system_snapshot} -User Snapshot:{user_snapshot} -最近会话:{conversation} -检索上下文:{context_blocks} +项目画像:{project_manifest} +系统摘要:{system_summary} +用户焦点:{user_focus} +最近会话摘要:{conversation} +检索证据:{context_blocks} """.strip(), ) diff --git a/backend/app/config/ai_prompts_catalog/part_scenes_05.py b/backend/app/config/ai_prompts_catalog/part_scenes_05.py index bb18d999..8fe9a5e6 100644 --- a/backend/app/config/ai_prompts_catalog/part_scenes_05.py +++ b/backend/app/config/ai_prompts_catalog/part_scenes_05.py @@ -52,6 +52,21 @@ PROMPT_AI_PMO_REQUIREMENT_PLAN_QA = _compose_prompt( 6. 不能把未发生的创建、入库、执行写成已完成;如果用户没明确要求,不要推进执行。 7. 如果上下文不够,只能说“我先帮你收敛需求”,并指出最关键缺口;不要编造系统已有模块或项目归属。 8. 回复总长度尽量控制在 2-4 句,优先短句、口语化、像产品经理在继续追问,不要写成长文。 +9. 如果识别为“宽泛 IT 项目概念”,默认按五轮收敛: + - 第 1 轮:项目性质 + - 第 2 轮:服务对象 + - 第 3 轮:技术路线 + - 第 4 轮:第一版范围 + - 第 5 轮:推进方式 +10. 宽泛 IT 项目收敛时,每轮必须给 A/B/C 三个选项;每个选项都要写: + - 适合什么场景 + - 推荐技术/能力 + - 推荐理由 + - 代价或限制 +11. 每轮 A/B/C 之后,必须补一句: + “当前我更推荐:X,原因:……” +12. 宽泛 IT 项目收敛优先用项目管理口径,不要只给抽象概念词,不要让用户像在填表。 +13. 飞书输出允许使用轻量 emoji 做分段,但整体仍要专业、克制、易扫读。 问题:{question} 当前项目:{project_id} diff --git a/backend/app/models/mounted_ai_runtime.py b/backend/app/models/mounted_ai_runtime.py index 5c06c90a..7ac53be9 100644 --- a/backend/app/models/mounted_ai_runtime.py +++ b/backend/app/models/mounted_ai_runtime.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any, Optional -from sqlalchemy import Boolean, Float, ForeignKey, Integer, Text, TIMESTAMP, UniqueConstraint, VARCHAR +from sqlalchemy import BigInteger, Boolean, Float, ForeignKey, Integer, Text, TIMESTAMP, UniqueConstraint, VARCHAR from sqlalchemy.orm import Mapped, mapped_column from .base import Base @@ -68,6 +68,9 @@ class MountedDraftWorkspace(Base): draft_payload: Mapped[Optional[Any]] = mapped_column(JSONB, nullable=True) required_confirmations: Mapped[Optional[Any]] = mapped_column(JSONB, nullable=True) last_ready_at: Mapped[Optional[TIMESTAMP]] = mapped_column(TIMESTAMP, nullable=True) + active_work_item_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True, index=True) + latest_source_message_id: Mapped[Optional[str]] = mapped_column(VARCHAR(128), nullable=True, index=True) + latest_reply_message_id: Mapped[Optional[str]] = mapped_column(VARCHAR(128), nullable=True, index=True) confirm_message_id: Mapped[Optional[str]] = mapped_column(VARCHAR(128), nullable=True, index=True) diff --git a/backend/app/schemas/mounted_ai_runtime.py b/backend/app/schemas/mounted_ai_runtime.py index a93c2adb..18a3813b 100644 --- a/backend/app/schemas/mounted_ai_runtime.py +++ b/backend/app/schemas/mounted_ai_runtime.py @@ -150,6 +150,20 @@ class MountedAIRunListPayload(BaseModel): total: int = 0 +class MountedAIDraftItem(BaseModel): + model_config = ConfigDict(extra="ignore") + + id: int + topic_workspace_id: int + state: str + draft_version: int + requirement_card: Optional[Any] = None + draft_payload: Optional[Any] = None + required_confirmations: Optional[Any] = None + last_ready_at: Optional[str] = None + confirm_message_id: Optional[str] = None + + class MountedAIAgentTraceItem(BaseModel): model_config = ConfigDict(extra="ignore") @@ -168,10 +182,16 @@ class MountedAIRunDetailPayload(BaseModel): model_config = ConfigDict(extra="ignore") item: MountedAIRunItem + topic: Optional[MountedAITopicItem] = None + draft: Optional[MountedAIDraftItem] = None agent_traces: list[MountedAIAgentTraceItem] = Field(default_factory=list) context_layers_hit: Optional[Any] = None tool_calls: Optional[Any] = None execution_outcome: Optional[Any] = None + semantic_packet_summary: Optional[Any] = None + semantic_decision: Optional[Any] = None + semantic_gate_result: Optional[Any] = None + semantic_diff: Optional[Any] = None class MountedAICutoverStatusPayload(BaseModel): diff --git a/backend/app/services/adaptive_rag_service/mixin_deep_guard_answer.py b/backend/app/services/adaptive_rag_service/mixin_deep_guard_answer.py index 9f68513c..5f302053 100644 --- a/backend/app/services/adaptive_rag_service/mixin_deep_guard_answer.py +++ b/backend/app/services/adaptive_rag_service/mixin_deep_guard_answer.py @@ -92,6 +92,178 @@ class AdaptiveRAGDeepGuardAnswerMixin: kept.append(text) return "\n".join(kept) if kept else "无" + @staticmethod + def _truncate_compact_text(text: Optional[str], *, limit: int, empty: str = "无") -> str: + raw = str(text or "").strip() + if not raw: + return empty + if len(raw) <= max(1, int(limit)): + return raw + clipped = raw[: max(1, int(limit) - 8)].rstrip() + return f"{clipped}...(截断)" + + @staticmethod + def _split_context_blocks(context_blocks_text: str) -> list[str]: + raw = str(context_blocks_text or "").strip() + if not raw or raw == "无": + return [] + return [ + str(block).strip() + for block in re.split(r"\n(?=\[(?:\d+|M)\])", raw) + if str(block).strip() + ] + + @classmethod + def _extract_manifest_block(cls, context_blocks_text: str) -> str: + for block in cls._split_context_blocks(context_blocks_text): + if not block.startswith("[M]"): + continue + lines = [str(line).strip() for line in block.splitlines()] + payload = "\n".join(line for line in lines[1:] if line).strip() + return cls._truncate_compact_text(payload, limit=260) + return "无" + + @classmethod + def _build_feishu_direct_project_manifest( + cls, + *, + project_id: Optional[int], + context_blocks_text: str, + user_snapshot: Optional[dict[str, Any]], + ) -> str: + manifest_text = cls._extract_manifest_block(context_blocks_text) + if manifest_text != "无": + return manifest_text + bits: list[str] = [] + if project_id is not None: + bits.append(f"当前项目ID {int(project_id)}") + projects = user_snapshot.get("projects") if isinstance(user_snapshot, dict) else None + if isinstance(projects, list) and projects: + project_names = [ + str(item.get("title") or item.get("name") or item.get("id") or "").strip() + for item in projects[:3] + if isinstance(item, dict) + ] + project_names = [name for name in project_names if name] + bits.append(f"关联项目 {len(projects)} 个") + if project_names: + bits.append(f"重点项目:{'、'.join(project_names)}") + return cls._truncate_compact_text(";".join(bits), limit=260) + + @classmethod + def _build_feishu_direct_system_summary(cls, system_snapshot: Optional[dict[str, Any]]) -> str: + system_data = system_snapshot if isinstance(system_snapshot, dict) else {} + assistant = system_data.get("assistant_profile") if isinstance(system_data.get("assistant_profile"), dict) else {} + platform = system_data.get("platform_profile") if isinstance(system_data.get("platform_profile"), dict) else {} + backend = platform.get("backend") if isinstance(platform.get("backend"), dict) else {} + module_counts = backend.get("module_counts") if isinstance(backend.get("module_counts"), dict) else {} + feature_catalog = platform.get("feature_catalog") if isinstance(platform.get("feature_catalog"), list) else [] + + bits: list[str] = [] + assistant_name = str(assistant.get("name") or "").strip() + assistant_identity = str(assistant.get("identity") or "").strip() + if assistant_name or assistant_identity: + identity_text = " / ".join(part for part in (assistant_name, assistant_identity) if part) + bits.append(f"助手:{identity_text}") + if module_counts: + top_modules = [ + f"{str(key)} {int(value)}" + for key, value in list(module_counts.items())[:4] + if value is not None + ] + if top_modules: + bits.append(f"模块统计:{'、'.join(top_modules)}") + if feature_catalog: + feature_names = [ + str(item.get("name") or item.get("route") or "").strip() + for item in feature_catalog[:4] + if isinstance(item, dict) + ] + feature_names = [name for name in feature_names if name] + bits.append(f"功能目录 {len(feature_catalog)} 个") + if feature_names: + bits.append(f"高频能力:{'、'.join(feature_names)}") + return cls._truncate_compact_text(";".join(bits), limit=320) + + @classmethod + def _build_feishu_direct_user_focus(cls, user_snapshot: Optional[dict[str, Any]]) -> str: + user_data = user_snapshot if isinstance(user_snapshot, dict) else {} + current_user = ( + user_data.get("current_user") + if isinstance(user_data.get("current_user"), dict) + else user_data.get("user_profile") + if isinstance(user_data.get("user_profile"), dict) + else {} + ) + projects = user_data.get("projects") if isinstance(user_data.get("projects"), list) else [] + tasks = user_data.get("tasks") if isinstance(user_data.get("tasks"), list) else [] + bits: list[str] = [] + current_user_name = str( + current_user.get("name") + or current_user.get("display_name") + or current_user.get("username") + or "" + ).strip() + if current_user_name: + bits.append(f"当前用户:{current_user_name}") + if projects: + project_names = [ + str(item.get("title") or item.get("name") or item.get("id") or "").strip() + for item in projects[:3] + if isinstance(item, dict) + ] + project_names = [name for name in project_names if name] + focus_text = f"关联项目 {len(projects)} 个" + if project_names: + focus_text = f"{focus_text}({', '.join(project_names)})" + bits.append(focus_text) + if tasks: + task_names = [ + str(item.get("title") or item.get("name") or item.get("id") or "").strip() + for item in tasks[:3] + if isinstance(item, dict) + ] + task_names = [name for name in task_names if name] + task_text = f"待关注任务 {len(tasks)} 个" + if task_names: + task_text = f"{task_text}({', '.join(task_names)})" + bits.append(task_text) + return cls._truncate_compact_text(";".join(bits), limit=320) + + @classmethod + def _compact_feishu_direct_conversation(cls, conversation_text: str) -> str: + raw = str(conversation_text or "").strip() + if not raw or raw == "无": + return "无" + blocks = [ + cls._truncate_compact_text(block, limit=180, empty="") + for block in re.split(r"\n(?=Q:)", raw) + if str(block).strip() + ] + compact = "\n".join(block for block in blocks[-3:] if block) + return cls._truncate_compact_text(compact, limit=520) + + @classmethod + def _compact_feishu_direct_context_blocks(cls, context_blocks_text: str) -> str: + noise_markers = ("项目列表", "历史项目详情", "assistant_profile", "platform_profile") + kept: list[str] = [] + fallback: Optional[str] = None + for block in cls._split_context_blocks(context_blocks_text): + if block.startswith("[M]"): + continue + compact_block = cls._truncate_compact_text(block, limit=220, empty="") + if not compact_block: + continue + fallback = fallback or compact_block + if any(marker in compact_block for marker in noise_markers): + continue + kept.append(compact_block) + if len(kept) >= 2: + break + if not kept and fallback: + kept.append(fallback) + return cls._truncate_compact_text("\n".join(kept), limit=520) + @staticmethod def _is_ai_pmo_process_entry(asked_via: Optional[str]) -> bool: channel = str(asked_via or "").strip().lower() @@ -444,6 +616,15 @@ class AdaptiveRAGDeepGuardAnswerMixin: normalized_asked_via = str(asked_via or "").strip().lower() if normalized_asked_via in {"feishu", "lark", "feishu_group", "feishu_dm"}: + project_manifest_text = self._build_feishu_direct_project_manifest( + project_id=project_id, + context_blocks_text=context_blocks_text, + user_snapshot=user_snapshot, + ) + system_summary_text = self._build_feishu_direct_system_summary(system_snapshot) + user_focus_text = self._build_feishu_direct_user_focus(user_snapshot) + compact_conversation_text = self._compact_feishu_direct_conversation(conversation_text) + compact_context_blocks_text = self._compact_feishu_direct_context_blocks(context_blocks_text) scene_key, prompt = ( "feishu_direct", await self._render_scene_prompt( @@ -451,10 +632,11 @@ class AdaptiveRAGDeepGuardAnswerMixin: fallback_template=PROMPT_FEISHU_DIRECT_QA, project_id=project_id if project_id is not None else "unknown", question=question, - system_snapshot=system_snapshot_text or "{}", - user_snapshot=user_snapshot_text or "{}", - conversation=conversation_text or "无", - context_blocks=context_blocks_text or "无", + project_manifest=project_manifest_text, + system_summary=system_summary_text, + user_focus=user_focus_text, + conversation=compact_conversation_text, + context_blocks=compact_context_blocks_text, ), ) return self._apply_consultant_mode_preference( diff --git a/backend/app/services/ai_pmo/ai_pmo_kernel/ai_pmo_typed_executor_common.py b/backend/app/services/ai_pmo/ai_pmo_kernel/ai_pmo_typed_executor_common.py index e8fa28e8..6536d4c3 100644 --- a/backend/app/services/ai_pmo/ai_pmo_kernel/ai_pmo_typed_executor_common.py +++ b/backend/app/services/ai_pmo/ai_pmo_kernel/ai_pmo_typed_executor_common.py @@ -70,6 +70,7 @@ from ..ai_pmo_intent import ( ROUTE_ACTION_ACK, ROUTE_ACTION_CLOSE, ROUTE_ACTION_DISCOVER, + ROUTE_PROJECT_CREATE, ROUTE_PROJECT_SIGNAL_CREATE, ROUTE_PROJECT_SIGNAL_DISCOVER, ROUTE_PROJECT_UPDATE, diff --git a/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/ingress/ai_pmo_kernel_mixins_ingress_render.py b/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/ingress/ai_pmo_kernel_mixins_ingress_render.py index 282b0f43..4772f6de 100644 --- a/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/ingress/ai_pmo_kernel_mixins_ingress_render.py +++ b/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/ingress/ai_pmo_kernel_mixins_ingress_render.py @@ -3,7 +3,6 @@ from __future__ import annotations from .... import ai_pmo_kernel_common_core as _common -from ....workflow_semantic_planner import WorkflowSemanticPlanner for _name, _value in vars(_common).items(): if not _name.startswith("__"): @@ -11,6 +10,42 @@ for _name, _value in vars(_common).items(): class KernelIngressRenderMixin: + @staticmethod + def _build_semantic_bundle_compat( + *, + intent: CanonicalIntent, + workflow_route: Optional[str], + source_system: Optional[str], + message_text: str, + ) -> dict[str, Any]: + metadata = intent.metadata if isinstance(intent.metadata, dict) else {} + diagnostics: dict[str, Any] = {} + for key in ( + "message_intent", + "review_framework", + "consultant_mode_preference", + "force_thinking_mode", + "advise_subtype", + "topic_switch", + ): + value = metadata.get(key) + if value not in (None, ""): + diagnostics[key] = value + if str(intent.name or "").strip(): + diagnostics["intent_name"] = str(intent.name or "").strip() + if str(workflow_route or "").strip(): + diagnostics["workflow_route"] = str(workflow_route or "").strip() + primary_workflow = "query_flow" if str(workflow_route or "").strip().startswith("query.") else "action_flow" + return { + "bundle_id": None, + "semantic_version": "compat-direct-metadata-v1", + "source_system": str(source_system or "unknown"), + "primary_workflow": primary_workflow, + "message_gate": "needs_confirmation" if bool(metadata.get("requires_confirmation")) else "auto_execute", + "diagnostics": diagnostics, + "raw_message_excerpt": re.sub(r"\s+", " ", str(message_text or "")).strip()[:240], + } + def _build_capture_result( self, *, @@ -94,18 +129,16 @@ class KernelIngressRenderMixin: else None ) if not isinstance(semantic_bundle, dict): - semantic_bundle = WorkflowSemanticPlanner.plan_from_intent( - intent_name=str(intent.name or ""), - message_text=str(getattr(work_item, "raw_message", "") or direct_reply_text or intent.signal_summary or ""), + semantic_bundle = self._build_semantic_bundle_compat( + intent=intent, + workflow_route=workflow_route, source_system=str(getattr(work_item, "source_system", "") or "unknown"), - workflow_route=str(workflow_route or ""), - project_id=_safe_int(work_item.project_id), - metadata=intent.metadata if isinstance(intent.metadata, dict) else {}, - ).to_dict() + message_text=str(getattr(work_item, "raw_message", "") or direct_reply_text or intent.signal_summary or ""), + ) result["semantic_bundle"] = semantic_bundle result["semantic_bundle_id"] = str(semantic_bundle.get("bundle_id") or "").strip() or None result["semantic_version"] = str(semantic_bundle.get("semantic_version") or "").strip() or None - result["semantic_source"] = "workflow_semantic_planner" + result["semantic_source"] = "direct_metadata_compat" if str(render.get("template_key") or "").strip().lower() == "draft_pending_confirm": payload = render.get("payload") if isinstance(render.get("payload"), dict) else {} result["draft_version"] = _safe_int(payload.get("draft_version")) or _safe_int(getattr(work_item, "draft_version", None)) diff --git a/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_llm.py b/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_llm.py index 1083bcbe..e022d1b5 100644 --- a/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_llm.py +++ b/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_llm.py @@ -3,7 +3,6 @@ from __future__ import annotations from .... import ai_pmo_kernel_common_core as _common -from ....workflow_semantic_planner import WorkflowSemanticPlanner for _name, _value in vars(_common).items(): if not _name.startswith("__"): @@ -199,26 +198,6 @@ class KernelIntentLLMMixin: if structured_route is not None else bool(payload.get("requires_confirmation")) ), - "semantic_bundle": WorkflowSemanticPlanner.plan_from_intent( - intent_name=intent_name, - message_text=message_text, - source_system="ai_pmo_ingress", - workflow_route=( - structured_route.key - if structured_route is not None - else LEGACY_INTENT_ROUTE_MAP.get(intent_name, ROUTE_QUERY_ANSWER) - ), - project_id=project_id, - metadata={ - **payload, - "confidence": confidence, - "requires_confirmation": ( - structured_route.requires_confirmation - if structured_route is not None - else bool(payload.get("requires_confirmation")) - ), - }, - ).to_dict(), }, ) diff --git a/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_resolve.py b/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_resolve.py index 9b99479e..79b0e491 100644 --- a/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_resolve.py +++ b/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/intent/ai_pmo_kernel_mixins_intent_resolve.py @@ -11,6 +11,26 @@ for _name, _value in vars(_common).items(): class KernelIntentResolveMixin: + _QUERY_FOLLOWUP_TOKENS = ( + "需求", + "mvp", + "方案", + "对应", + "这个", + "它", + "给我", + "补充", + "展开", + "细化", + "重答", + "重新回答", + "高级模型", + "更强模型", + "分析", + "评审", + "怎么做", + ) + @staticmethod def _within_recent_window(last_active_at: Optional[datetime], *, window_seconds: int) -> bool: if not isinstance(last_active_at, datetime): @@ -18,6 +38,13 @@ class KernelIntentResolveMixin: delta = _utc_now_naive() - last_active_at return 0 <= delta.total_seconds() <= float(window_seconds) + @classmethod + def _looks_like_query_scene_followup(cls, message_text: str) -> bool: + normalized = re.sub(r"\s+", "", str(message_text or "")).lower() + if not normalized: + return False + return any(token in normalized for token in cls._QUERY_FOLLOWUP_TOKENS) + @classmethod def _should_inherit_recent_feishu_session_context( cls, @@ -27,6 +54,8 @@ class KernelIntentResolveMixin: ) -> bool: if session is None: return False + if WorkflowSemanticPlanner.looks_like_explicit_new_requirement_topic(message_text): + return False sender = str(getattr(session, "sender", "") or "").strip().lower() if not sender.startswith("feishu:"): return False @@ -37,28 +66,7 @@ class KernelIntentResolveMixin: return False if not str(getattr(session, "last_intent", "") or "").strip(): return False - if WorkflowSemanticPlanner._derive_query_scene_metadata(message_text): - return True - normalized = re.sub(r"\s+", "", str(message_text or "")).lower() - if not normalized: - return False - followup_tokens = ( - "需求", - "mvp", - "方案", - "对应", - "这个", - "它", - "给我", - "补充", - "展开", - "细化", - "重答", - "重新回答", - "高级模型", - "更强模型", - ) - return any(token in normalized for token in followup_tokens) + return cls._looks_like_query_scene_followup(message_text) async def _resolve_intent( self, @@ -101,29 +109,6 @@ class KernelIntentResolveMixin: ) return inherited, "session_inherited" - if WorkflowSemanticPlanner.looks_like_signal_source_request(message_text): - project_id = _safe_int(world_model.get("resolved_project_id")) - if project_id is None and inherit_session_context and session is not None: - project_id = _safe_int(getattr(session, "current_project_id", None)) - return self._build_canonical_intent( - intent_name=INTENT_SIGNAL_SOURCE_ONBOARD, - message_text=message_text, - project_id=project_id, - confidence=0.92, - metadata={ - "context_source": "semantic_signal_source_locked", - "source_request": True, - "semantic_bundle": WorkflowSemanticPlanner.plan_from_intent( - intent_name=INTENT_SIGNAL_SOURCE_ONBOARD, - message_text=message_text, - source_system="ai_pmo_ingress", - workflow_route=ROUTE_SIGNAL_SOURCE_ONBOARD, - project_id=project_id, - metadata={"confidence": 0.92, "requires_confirmation": False}, - ).to_dict(), - }, - ), "rule_signal_source_onboard" - # 模型唯一入口:不再使用规则/正则意图分流。 if not self._intent_llm_enabled(): raise RuntimeError("intent_model_only_mode_requires_llm_enabled") diff --git a/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/world_model/ai_pmo_kernel_mixins_world_model_session.py b/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/world_model/ai_pmo_kernel_mixins_world_model_session.py index 16ce8e50..30cf355e 100644 --- a/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/world_model/ai_pmo_kernel_mixins_world_model_session.py +++ b/backend/app/services/ai_pmo/ai_pmo_kernel/mixins/world_model/ai_pmo_kernel_mixins_world_model_session.py @@ -110,6 +110,7 @@ class KernelWorldModelSessionMixin: source_message_id: Optional[str] = None, thread_context: Optional[dict[str, Any]] = None, current_project_id: Optional[int], + clear_current_project_id: bool = False, last_intent: Optional[str], last_message_text: Optional[str], ) -> None: @@ -122,7 +123,9 @@ class KernelWorldModelSessionMixin: source_message_id=source_message_id, thread_context=thread_context, ) - if current_project_id is not None: + if clear_current_project_id: + row.current_project_id = None + elif current_project_id is not None: row.current_project_id = int(current_project_id) row.last_intent = (str(last_intent or "").strip()[:64] or None) trimmed_message = str(last_message_text or "").strip() diff --git a/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_auto.py b/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_auto.py index 54a38e72..b96dc0f2 100644 --- a/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_auto.py +++ b/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_auto.py @@ -209,6 +209,30 @@ class KernelIngressFlowAutoMixin: channel_ref=channel_ref, ) log_perf_fn("TypedExecutor.execute", stage_started_at) + if self._should_promote_requirement_plan_draft( + intent=intent, + execution_result=execution_result, + ): + stage_started_at = time.perf_counter() + capture_result = await self._promote_requirement_plan_draft_confirmation( + work_item=work_item, + intent=intent, + execution_result=execution_result, + world_model=world_model, + decision_payload=decision_payload, + decision_trace=decision_trace, + decision_row=decision_row, + source_system=source_system, + source_message_id=source_message_id, + message_text=message_text, + sender=sender, + ingest_event=ingest_event, + started=started, + session=session, + resolved_project_for_session=resolved_project_for_session, + ) + log_perf_fn("_promote_requirement_plan_draft_confirmation", stage_started_at) + return capture_result verification = await self._verify_execution(intent=intent, execution_result=execution_result, world_model=world_model) if isinstance(execution_result, dict): execution_result.setdefault("decision_trace", decision_trace) @@ -405,6 +429,262 @@ class KernelIngressFlowAutoMixin: return str(getattr(workflow_route, "action", "") or "") == ROUTE_ACTION_CREATE return True + @staticmethod + def _should_promote_requirement_plan_draft( + *, + intent: CanonicalIntent, + execution_result: Any, + ) -> bool: + if intent.name != INTENT_QUERY or not isinstance(execution_result, dict): + return False + artifact = execution_result.get("draft_artifact") + if not isinstance(artifact, dict): + return False + return bool(artifact.get("promote_to_confirmation")) + + async def _promote_requirement_plan_draft_confirmation( + self, + *, + work_item: Any, + intent: CanonicalIntent, + execution_result: dict[str, Any], + world_model: dict[str, Any], + decision_payload: dict[str, Any], + decision_trace: dict[str, Any], + decision_row: Any, + source_system: str, + source_message_id: str, + message_text: str, + sender: str, + ingest_event: Any, + started: float, + session: ConversationSession, + resolved_project_for_session: Optional[int], + ) -> dict[str, Any]: + from ....models import MountedDraftWorkspace, MountedTopicWorkspace + + artifact = execution_result.get("draft_artifact") if isinstance(execution_result, dict) else {} + draft_workspace_id = _safe_int((artifact or {}).get("draft_workspace_id")) + if draft_workspace_id is None: + return { + "handled": False, + "status": "failed_safe", + "error": "requirement_plan_draft_missing_workspace", + } + + draft_row = ( + await self.db.execute( + select(MountedDraftWorkspace).where( + and_( + MountedDraftWorkspace.tenant_id == int(self.tenant_id), + MountedDraftWorkspace.id == int(draft_workspace_id), + ) + ) + ) + ).scalar_one_or_none() + if draft_row is None: + return { + "handled": False, + "status": "failed_safe", + "error": "requirement_plan_draft_not_found", + } + + topic_row = ( + await self.db.execute( + select(MountedTopicWorkspace).where( + and_( + MountedTopicWorkspace.tenant_id == int(self.tenant_id), + MountedTopicWorkspace.id == int(draft_row.topic_workspace_id), + ) + ) + ) + ).scalar_one_or_none() + if topic_row is None: + return { + "handled": False, + "status": "failed_safe", + "error": "requirement_plan_topic_not_found", + } + + draft_payload = draft_row.draft_payload if isinstance(draft_row.draft_payload, dict) else {} + if not draft_payload: + draft_payload = ( + artifact.get("draft_payload") + if isinstance(artifact.get("draft_payload"), dict) + else {} + ) + draft_text = str((artifact or {}).get("draft_text") or draft_payload.get("draft_text") or execution_result.get("answer") or "").strip() + draft_version = _safe_int((artifact or {}).get("draft_version")) or int(draft_row.draft_version or 0) or 1 + + work_item.intent = ROUTE_PROJECT_CREATE + work_item.state = STATE_DRAFT_PENDING_CONFIRM + work_item.draft_content = draft_payload + work_item.draft_version = int(draft_version) + work_item.draft_expires_at = None + work_item.confirm_message_id = None + work_item.execution_result = { + **(execution_result if isinstance(execution_result, dict) else {}), + "status": "draft_pending_confirm", + "workflow_route": ROUTE_PROJECT_CREATE, + "workflow_action": ROUTE_ACTION_CREATE, + "source_workflow_route": str((artifact or {}).get("source_workflow_route") or ROUTE_QUERY_ANSWER), + "draft": draft_payload, + "draft_version": int(draft_version), + "draft_artifact": { + **(artifact if isinstance(artifact, dict) else {}), + "active_work_item_id": int(work_item.id), + }, + } + work_item.reply_template_key = "draft_pending_confirm" + + draft_row.state = STATE_DRAFT_PENDING_CONFIRM + draft_row.draft_version = int(draft_version) + draft_row.draft_payload = draft_payload + draft_row.active_work_item_id = int(work_item.id) + draft_row.latest_source_message_id = str(source_message_id or "").strip()[:128] or None + draft_row.latest_reply_message_id = None + draft_row.confirm_message_id = None + draft_row.last_ready_at = _utc_now_naive() + + topic_row.mode = "plan" + topic_row.status = "active" + topic_row.plan_status = STATE_DRAFT_PENDING_CONFIRM + topic_row.pending_actions = ["confirm_requirement_draft"] + topic_row.open_questions = [] + topic_row.readiness_score = 1.0 + topic_row.last_message_id = str(source_message_id or "").strip()[:128] or topic_row.last_message_id + topic_row.metadata_json = { + **(topic_row.metadata_json if isinstance(topic_row.metadata_json, dict) else {}), + "plan_mode": True, + "plan_topic_action": "draft_pending_confirm", + "active_draft_work_item_id": int(work_item.id), + "active_draft_version": int(draft_version), + } + + verification = {"verified": False, "issues": [STATE_DRAFT_PENDING_CONFIRM]} + work_item.review_summary = await self.review_service.review_work_item( + work_item=work_item, + intent_name=INTENT_PROJECT_CREATE, + execution_result=work_item.execution_result, + world_model=world_model, + verification=verification, + simulation=decision_payload["simulation"], + ) + await self._maybe_emit_risk_cadence_snapshot( + project_id=_safe_int(work_item.project_id), + review_summary=work_item.review_summary, + ) + + decision_row.execution_result = work_item.execution_result + decision_row.final_outcome = STATE_DRAFT_PENDING_CONFIRM + operation_code_for_log = "project.create" + await self._upsert_operation_execution_ledger( + work_item_id=int(work_item.id), + operation_code=operation_code_for_log, + source_message_id=source_message_id, + project_id=_safe_int(work_item.project_id), + status=self._normalize_execution_ledger_status( + work_item_state=STATE_DRAFT_PENDING_CONFIRM, + execution_status=str(work_item.execution_result.get("status") or ""), + ), + payload={ + "work_item_id": int(work_item.id), + "workflow_route": ROUTE_PROJECT_CREATE, + "workflow_action": ROUTE_ACTION_CREATE, + "source_system": source_system, + "source_message_id": source_message_id, + "draft_artifact": artifact if isinstance(artifact, dict) else {}, + }, + execution_result={ + **work_item.execution_result, + "work_item_state": STATE_DRAFT_PENDING_CONFIRM, + }, + ) + + render = { + "receipt_phase": RECEIPT_PHASE_PENDING, + "template_key": "draft_pending_confirm", + "payload": { + "intent": INTENT_PROJECT_CREATE, + "draft_version": int(draft_version), + "draft_expires_at": None, + }, + "text": draft_text or "已生成草稿,请确认后入库。", + } + capture_intent = CanonicalIntent( + name=INTENT_PROJECT_CREATE, + confidence=max(float(intent.confidence or 0.0), 0.9), + project_id=_safe_int(work_item.project_id) or resolved_project_for_session, + signal_title="", + signal_summary=str(message_text or "")[:300], + metadata={ + **(intent.metadata if isinstance(intent.metadata, dict) else {}), + "workflow_route": ROUTE_PROJECT_CREATE, + "workflow_family": ROUTE_FAMILY_PROJECT_CREATE, + "workflow_action": ROUTE_ACTION_CREATE, + "requires_confirmation": True, + "resumed_from": ROUTE_QUERY_ANSWER, + }, + ) + + await self._project_draft_pending_confirm( + work_item=work_item, + message_text=message_text, + draft_text=str(render["text"]), + intent=capture_intent, + ) + + direct_reply_required = await self._claim_reply_receipt( + work_item_id=int(work_item.id), + source_system=source_system, + source_message_id=source_message_id, + receipt_phase=str(render["receipt_phase"]), + template_key=str(render["template_key"]), + payload=render["payload"], + ) + ingest_event.status = "done" + ingest_event.processed_at = _utc_now_naive() + await self._write_processing_log( + source_message_id=source_message_id, + work_item=work_item, + message_text=message_text, + route=f"ai_pmo.{STATE_DRAFT_PENDING_CONFIRM}", + operation_code=operation_code_for_log, + operation_result=work_item.execution_result, + processing_status="success", + duration_ms=int((time.perf_counter() - started) * 1000), + ) + final_project_id = _safe_int(work_item.project_id) or resolved_project_for_session + await self._update_session( + tenant_id=self.tenant_id, + sender=sender, + session=session, + current_project_id=final_project_id, + last_intent=STATE_DRAFT_PENDING_CONFIRM, + last_message_text=message_text[:200], + ) + try: + await self.db.commit() + except Exception as exc: + await self._persist_kernel_failure_with_fresh_session( + work_item_id=_safe_int(getattr(work_item, "id", None)), + ingest_event_id=_safe_int(getattr(ingest_event, "id", None)), + error_message=str(exc), + source_system=source_system, + source_message_id=source_message_id, + ) + raise + + return self._build_capture_result( + work_item=work_item, + intent=capture_intent, + execution_result=work_item.execution_result, + render=render, + direct_reply_required=direct_reply_required, + status="captured", + classification="command", + ) + @staticmethod def _build_capacity_warning_text( *, diff --git a/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_entry.py b/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_entry.py index ddf6a23f..9315d67a 100644 --- a/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_entry.py +++ b/backend/app/services/ai_pmo/ai_pmo_kernel_ingress_flow/mixin_entry.py @@ -2,6 +2,11 @@ from __future__ import annotations from .shared import * from app.utils.thread_context import build_feishu_conversation_scope_key +from app.services.ai_pmo.semantic_context import ( + SemanticContextAssembler, + SemanticDecisionGatekeeper, +) +from app.services.ai_pmo.semantic_context.semantic_decision_service import SemanticDecisionService from app.services.ai_pmo.workflow_semantic_planner import WorkflowSemanticPlanner from app.services.ai_pmo.workflow_semantic_planner.constants import ( QUERY_SCENE_BUSINESS_REVIEW, @@ -27,6 +32,47 @@ class KernelIngressFlowEntryMixin: QUERY_SCENE_CORRECTION_ESCALATION, QUERY_SCENE_REQUIREMENT_PLAN, }) + _QUERY_SCENE_TOKEN_RULES = ( + ( + QUERY_SCENE_CORRECTION_ESCALATION, + { + "consultant_mode_preference": "challenge", + "force_thinking_mode": "deep", + }, + ("不对", "错了", "答错", "纠正", "更正", "重新回答", "重答", "高级模型", "更强模型"), + ), + ( + QUERY_SCENE_BUSINESS_REVIEW, + { + "review_framework": "five_lenses", + "consultant_mode_preference": "clarify", + }, + ("分析", "拆解", "评审", "评估", "复盘", "诊断", "怎么做", "mvp", "需求"), + ), + ( + QUERY_SCENE_BUSINESS_REVIEW, + { + "consultant_mode_preference": "advise", + "advise_subtype": "assignee", + }, + ("谁来做", "谁负责", "安排谁", "推荐谁", "负责人"), + ), + ( + QUERY_SCENE_REQUIREMENT_PLAN, + { + "consultant_mode_preference": "clarify", + "topic_switch": "new", + }, + ("新需求", "新项目", "做个", "想做个", "准备做", "创建需求"), + ), + ( + QUERY_SCENE_REQUIREMENT_PLAN, + { + "consultant_mode_preference": "clarify", + }, + ("需求", "方案", "范围", "排期", "补充", "细化", "展开", "mvp"), + ), + ) @classmethod def _extract_route_scene_metadata(cls, route_metadata: Optional[dict[str, Any]]) -> dict[str, str]: @@ -36,16 +82,26 @@ class KernelIngressFlowEntryMixin: value = metadata.get(key) if value not in (None, ""): scene_metadata[key] = str(value).strip() - semantic_diagnostics = metadata.get("semantic_bundle") - if isinstance(semantic_diagnostics, dict): - semantic_diagnostics = semantic_diagnostics.get("diagnostics") - if isinstance(semantic_diagnostics, dict): - for key in cls._RAG_QUERY_SCENE_KEYS: - value = semantic_diagnostics.get(key) - if value not in (None, "") and key not in scene_metadata: - scene_metadata[key] = str(value).strip() return scene_metadata + @classmethod + def _derive_query_scene_metadata(cls, text: str) -> dict[str, Any]: + normalized = re.sub(r"\s+", "", str(text or "")).lower() + if not normalized: + return {} + if any(token in normalized for token in ("最近怎么样", "在吗", "你好", "哈哈", "hi", "hello")): + return { + "message_intent": "casual_chat", + "consultant_mode_preference": "brainstorm", + } + for message_intent, extra_fields, tokens in cls._QUERY_SCENE_TOKEN_RULES: + if any(token in normalized for token in tokens): + return { + "message_intent": message_intent, + **extra_fields, + } + return {} + @classmethod def _looks_like_mixed_candidate(cls, *, text: str) -> bool: normalized = re.sub(r"\s+", "", str(text or "")).lower() @@ -73,30 +129,12 @@ class KernelIngressFlowEntryMixin: semantic_bundle: Optional[dict[str, Any]], ) -> dict[str, Any]: context = rag_request_context if isinstance(rag_request_context, dict) else {} - semantic_diagnostics = ( - semantic_bundle.get("diagnostics") - if isinstance(semantic_bundle, dict) and isinstance(semantic_bundle.get("diagnostics"), dict) - else {} - ) - semantic_primary_workflow = ( - str(semantic_bundle.get("primary_workflow") or "").strip().lower() - if isinstance(semantic_bundle, dict) - else "" - ) - derived = WorkflowSemanticPlanner._derive_query_scene_metadata(message_text) + derived = cls._derive_query_scene_metadata(message_text) scene_metadata: dict[str, Any] = { key: value for key, value in derived.items() if key in cls._RAG_QUERY_SCENE_KEYS and value not in (None, "") } - if str(source_system or "").strip().lower() == "feishu" and semantic_primary_workflow == "query_flow": - for key in cls._RAG_QUERY_SCENE_KEYS: - value = semantic_diagnostics.get(key) - if value in (None, ""): - continue - text = str(value).strip() - if text: - scene_metadata[key] = text.lower() if key != "review_framework" else text for key in cls._RAG_QUERY_SCENE_KEYS: value = context.get(key) if value in (None, ""): @@ -115,20 +153,14 @@ class KernelIngressFlowEntryMixin: semantic_bundle: Optional[dict[str, Any]], scene_metadata: dict[str, Any], ) -> bool: + _ = semantic_bundle message_intent = str(scene_metadata.get("message_intent") or "").strip().lower() if message_intent not in cls._RAG_QUERY_SCENES: return False if isinstance(rag_request_context, dict): action_mode = str(rag_request_context.get("action_mode") or "auto").strip().lower() or "auto" return action_mode not in {"write", "read_write"} - if str(source_system or "").strip().lower() == "feishu": - primary_workflow = ( - str(semantic_bundle.get("primary_workflow") or "").strip().lower() - if isinstance(semantic_bundle, dict) - else "" - ) - return primary_workflow == "query_flow" or message_intent in cls._RAG_QUERY_SCENES - return False + return str(source_system or "").strip().lower() == "feishu" def _coerce_intent_for_query_scene( self, @@ -140,39 +172,27 @@ class KernelIngressFlowEntryMixin: semantic_bundle: Optional[dict[str, Any]], world_model: dict[str, Any], ) -> CanonicalIntent: - effective_semantic_bundle = ( - semantic_bundle - if isinstance(semantic_bundle, dict) and semantic_bundle - else (intent.metadata or {}).get("semantic_bundle") - if isinstance((intent.metadata or {}).get("semantic_bundle"), dict) - else None - ) scene_metadata = self._build_forced_query_scene_metadata( source_system=source_system, message_text=message_text, rag_request_context=rag_request_context, - semantic_bundle=effective_semantic_bundle, + semantic_bundle=semantic_bundle, ) + intent_scene_metadata = self._extract_route_scene_metadata(intent.metadata) + if intent_scene_metadata: + scene_metadata = { + **scene_metadata, + **intent_scene_metadata, + } if not self._should_force_query_scene( source_system=source_system, rag_request_context=rag_request_context, - semantic_bundle=effective_semantic_bundle, + semantic_bundle=semantic_bundle, scene_metadata=scene_metadata, ): return intent project_id = _safe_int(intent.project_id) or _safe_int(world_model.get("resolved_project_id")) - semantic_bundle = WorkflowSemanticPlanner.plan_from_intent( - intent_name=INTENT_QUERY, - message_text=message_text, - source_system="ai_pmo_ingress", - workflow_route=ROUTE_QUERY_ANSWER, - project_id=project_id, - metadata={ - "confidence": max(float(intent.confidence or 0.0), 0.86), - **scene_metadata, - }, - ).to_dict() metadata = { **(intent.metadata or {}), **scene_metadata, @@ -181,7 +201,6 @@ class KernelIngressFlowEntryMixin: "workflow_family": ROUTE_FAMILY_QUERY, "workflow_action": ROUTE_ACTION_ANSWER, "requires_confirmation": False, - "semantic_bundle": semantic_bundle, } pretty = re.sub(r"\s+", " ", str(message_text or "")).strip() return CanonicalIntent( @@ -466,6 +485,91 @@ class KernelIngressFlowEntryMixin: error=str(runtime_exc), ) + async def _safe_record_semantic_shadow( + *, + latest_user_message: str, + workflow_route: str, + route_metadata: Optional[dict[str, Any]], + operator_user_id: Optional[int], + ) -> None: + if mounted_tracking is None: + return + packet_summary: dict[str, Any] | None = None + try: + packet = await SemanticContextAssembler(self.db).assemble( + tenant_id=int(self.tenant_id), + operator_user_id=int(operator_user_id or 0), + topic_workspace_id=int(mounted_tracking.topic_workspace.id), + latest_user_message=latest_user_message, + ) + packet_summary = { + "packet_version": packet.packet_version, + "topic_key": packet.topic.topic_key, + "workflow_mode": packet.mode_card.workflow_mode, + "recent_turn_count": len(packet.thread.recent_turns), + "draft_version": packet.draft.draft_version if packet.draft is not None else None, + "thread_summary": packet.thread.thread_summary, + "recent_turns": [turn.model_dump(mode="json") for turn in packet.thread.recent_turns], + "open_questions": list(packet.thread.open_questions), + "mode_card": packet.mode_card.model_dump(mode="json"), + "topic": packet.topic.model_dump(mode="json"), + "draft": packet.draft.model_dump(mode="json") if packet.draft is not None else None, + } + decision = await SemanticDecisionService(llm_service).decide(packet=packet) + gate_result = SemanticDecisionGatekeeper.evaluate( + mode_card=packet.mode_card, + decision=decision, + ) + await mounted_runtime.record_semantic_shadow( + tracking=mounted_tracking, + source_message_preview=latest_user_message, + semantic_packet_summary=packet_summary, + semantic_decision=decision.model_dump(mode="json"), + semantic_gate_result=gate_result.model_dump(mode="json"), + semantic_diff={ + "changed": ( + str(decision.mode_decision or "").strip().lower() + != str(mounted_tracking.topic_workspace.plan_status or "").strip().lower() + ), + "legacy_route": str(workflow_route or "").strip() or None, + "legacy_message_intent": str((route_metadata or {}).get("message_intent") or "").strip() or None, + "shadow_topic_relation": decision.topic_relation, + "shadow_mode_decision": decision.mode_decision, + }, + ) + except Exception as runtime_exc: + logger.warning( + "mounted_ai_runtime_semantic_shadow_failed", + tenant_id=self.tenant_id, + source_system=source_system, + source_message_id=str(getattr(getattr(mounted_tracking, "conversation_run", None), "source_message_id", "") or ""), + error=str(runtime_exc), + ) + try: + await mounted_runtime.record_semantic_shadow( + tracking=mounted_tracking, + source_message_preview=latest_user_message, + semantic_packet_summary=packet_summary, + semantic_decision=None, + semantic_gate_result=None, + semantic_diff={ + "changed": False, + "legacy_route": str(workflow_route or "").strip() or None, + "legacy_message_intent": str((route_metadata or {}).get("message_intent") or "").strip() or None, + "shadow_error": str(runtime_exc), + }, + status="failed", + failure_reason=str(runtime_exc), + ) + except Exception as persist_exc: + logger.warning( + "mounted_ai_runtime_semantic_shadow_persist_failed", + tenant_id=self.tenant_id, + source_system=source_system, + source_message_id=str(getattr(getattr(mounted_tracking, "conversation_run", None), "source_message_id", "") or ""), + error=str(persist_exc), + ) + def _resolve_specialist_agent_name( *, workflow_route: str, @@ -541,8 +645,12 @@ class KernelIngressFlowEntryMixin: ingest_event=ingest_event, ingress_options=ingress_options, ) - if self._should_route_to_draft_confirmation( + if await self._should_route_to_draft_confirmation_session( session=session, + sender=sender, + source_system=source_system, + channel_ref=channel_ref, + source_sender_open_id=source_sender_open_id, message_text=clean_text, ingress_options=ingress_options, ): @@ -603,6 +711,26 @@ class KernelIngressFlowEntryMixin: ) await _safe_finalize_mounted_runtime(result=draft_capture) return draft_capture + await self._maybe_reset_session_project_context_for_new_requirement( + session=session, + sender=sender, + channel_ref=channel_ref, + source_sender_open_id=source_sender_open_id, + source_message_id=synthetic_message_id, + thread_context=thread_context, + message_text=clean_text, + ) + orphan_requirement_followup_result = await self._maybe_handle_orphan_requirement_followup( + source_system=source_system, + source_message_id=synthetic_message_id, + channel_ref=channel_ref, + source_sender_open_id=source_sender_open_id, + ingress_options=ingress_options, + message_text=clean_text, + ) + if orphan_requirement_followup_result is not None: + await _safe_finalize_mounted_runtime(result=orphan_requirement_followup_result) + return orphan_requirement_followup_result mixed_meta: dict[str, Any] = {} if not self._is_short_confirmation_message( message_text=clean_text, @@ -706,6 +834,12 @@ class KernelIngressFlowEntryMixin: workflow_route.metadata = route_metadata perf_intent_name = workflow_route.key _log_perf("_resolve_intent", stage_started_at) + await _safe_record_semantic_shadow( + latest_user_message=clean_text, + workflow_route=workflow_route.key, + route_metadata=route_metadata, + operator_user_id=_safe_int(getattr(operator, "id", None)), + ) if intent_context_source: world_model["context_source"] = intent_context_source work_item.context_snapshot = world_model @@ -1213,6 +1347,134 @@ class KernelIngressFlowEntryMixin: and WorkflowSemanticPlanner.looks_like_natural_draft_revision(message_text) ) + async def _should_route_to_draft_confirmation_session( + self, + *, + session: ConversationSession, + sender: str, + source_system: str, + channel_ref: Optional[str], + source_sender_open_id: Optional[str], + message_text: str, + ingress_options: Optional[dict[str, Any]], + ) -> bool: + if WorkflowSemanticPlanner.looks_like_explicit_new_requirement_topic(message_text): + return False + if self._should_route_to_draft_confirmation( + session=session, + message_text=message_text, + ingress_options=ingress_options, + ): + return True + + from ..draft_confirm_handler import DraftConfirmHandler + + action, _ = DraftConfirmHandler.parse_action_from_text(message_text=message_text) + if action == DraftConfirmHandler._ACTION_UNKNOWN and not WorkflowSemanticPlanner.looks_like_natural_draft_revision(message_text): + return False + + handler = DraftConfirmHandler(self) + return await handler.has_active_draft_session( + sender=sender, + source_system=source_system, + channel_ref=channel_ref, + source_sender_open_id=source_sender_open_id, + thread_context=self._extract_thread_context_from_ingress_options(ingress_options), + ) + + @staticmethod + def _build_orphan_requirement_followup_reply() -> str: + return "当前没有进行中的 A/B/C 收敛轮次,也没有待确认草稿。你先发一句新需求,我按新的 topic 接着收。" + + async def _maybe_reset_session_project_context_for_new_requirement( + self, + *, + session: Optional[ConversationSession], + sender: str, + channel_ref: Optional[str], + source_sender_open_id: Optional[str], + source_message_id: Optional[str], + thread_context: Optional[dict[str, Any]], + message_text: str, + ) -> bool: + if session is None: + return False + if not WorkflowSemanticPlanner.looks_like_explicit_new_requirement_topic(message_text): + return False + if _safe_int(getattr(session, "current_project_id", None)) is None: + return False + await self._update_session( + tenant_id=self.tenant_id, + sender=sender, + session=session, + source_chat_id=channel_ref, + source_sender_open_id=source_sender_open_id, + source_message_id=source_message_id, + thread_context=thread_context, + current_project_id=None, + clear_current_project_id=True, + last_intent=str(getattr(session, "last_intent", "") or "").strip() or None, + last_message_text=str(getattr(session, "last_message_text", "") or "").strip() or None, + ) + return True + + async def _maybe_handle_orphan_requirement_followup( + self, + *, + source_system: str, + source_message_id: str, + channel_ref: Optional[str], + source_sender_open_id: Optional[str], + ingress_options: Optional[dict[str, Any]], + message_text: str, + ) -> Optional[dict[str, Any]]: + if not WorkflowSemanticPlanner.looks_like_low_context_requirement_followup(message_text): + return None + checker = getattr(self, "_has_active_requirement_plan_workspace", None) + if not callable(checker): + return None + thread_context = self._extract_thread_context_from_ingress_options(ingress_options) + is_feishu_source = str(source_system or "").strip().lower() == "feishu" + conversation_key = ( + build_feishu_conversation_scope_key( + source_chat_id=channel_ref, + source_sender_open_id=source_sender_open_id, + sender=None, + ) + if is_feishu_source + else source_message_id + ) + has_active_requirement_plan_workspace = await checker( + source_system=source_system, + source_message_id=source_message_id, + source_chat_id=channel_ref, + sender=None, + source_sender_open_id=source_sender_open_id, + thread_context=thread_context, + conversation_key=conversation_key, + force_new_topic=False, + ) + if has_active_requirement_plan_workspace: + return None + return { + "handled": True, + "status": "answered", + "classification": "question", + "linked_project_ids": [], + "signal_id": None, + "created_project_id": None, + "auto_tasks": [], + "ai_trace_id": None, + "results": [], + "intent": INTENT_QUERY, + "direct_reply_required": True, + "direct_reply_text": self._build_orphan_requirement_followup_reply(), + "reply_template_key": "requirement_plan_anchor_required", + "work_item_id": None, + "decision_level": None, + "review_summary": None, + } + @classmethod def _merge_ingress_thread_context( cls, diff --git a/backend/app/services/ai_pmo/ai_pmo_typed_executor_execute/mixin_execute_query_project.py b/backend/app/services/ai_pmo/ai_pmo_typed_executor_execute/mixin_execute_query_project.py index 533d7d48..74124d3d 100644 --- a/backend/app/services/ai_pmo/ai_pmo_typed_executor_execute/mixin_execute_query_project.py +++ b/backend/app/services/ai_pmo/ai_pmo_typed_executor_execute/mixin_execute_query_project.py @@ -55,7 +55,9 @@ class ExecuteQueryProjectMixin: message_intent: Optional[str], ) -> bool: normalized_intent = str(message_intent or "").strip().lower() - return normalized_intent == "casual_chat" or WorkflowSemanticPlanner._looks_like_casual_chat_message(message_text) + if normalized_intent: + return normalized_intent == "casual_chat" + return WorkflowSemanticPlanner._looks_like_casual_chat_message(message_text) @classmethod def _should_use_requirement_clarify_answer( @@ -67,8 +69,8 @@ class ExecuteQueryProjectMixin: ) -> bool: normalized_intent = str(message_intent or "").strip().lower() normalized_mode = str(consultant_mode_preference or "").strip().lower() - if normalized_intent != "business_review" or normalized_mode != "clarify": - return False + if normalized_intent or normalized_mode: + return normalized_intent == "business_review" and normalized_mode == "clarify" return WorkflowSemanticPlanner._looks_like_requirement_followup_clarify(message_text) @classmethod @@ -78,9 +80,9 @@ class ExecuteQueryProjectMixin: message_text: str, message_intent: Optional[str], consultant_mode_preference: Optional[str], + advise_subtype: Optional[str], ) -> bool: normalized_intent = str(message_intent or "").strip().lower() - normalized_mode = str(consultant_mode_preference or "").strip().lower() if cls._should_use_requirement_clarify_answer( message_text=message_text, message_intent=message_intent, @@ -91,18 +93,20 @@ class ExecuteQueryProjectMixin: message_text=message_text, message_intent=message_intent, consultant_mode_preference=consultant_mode_preference, + advise_subtype=advise_subtype, ): return False if cls._should_use_assignee_recommendation_answer( message_text=message_text, message_intent=message_intent, consultant_mode_preference=consultant_mode_preference, + advise_subtype=advise_subtype, ): return False if normalized_intent == "requirement_plan": return True - if normalized_intent == "business_review" and normalized_mode == "clarify": - return WorkflowSemanticPlanner._should_keep_requirement_discussion_in_query(message_text) + if normalized_intent or str(consultant_mode_preference or "").strip(): + return False return WorkflowSemanticPlanner._should_keep_requirement_discussion_in_query(message_text) @classmethod @@ -112,11 +116,17 @@ class ExecuteQueryProjectMixin: message_text: str, message_intent: Optional[str], consultant_mode_preference: Optional[str], + advise_subtype: Optional[str], ) -> bool: normalized_intent = str(message_intent or "").strip().lower() normalized_mode = str(consultant_mode_preference or "").strip().lower() - if normalized_intent == "business_review" and normalized_mode == "advise": - return True + normalized_subtype = str(advise_subtype or "").strip().lower() + if normalized_intent or normalized_mode or normalized_subtype: + return ( + normalized_intent == "business_review" + and normalized_mode == "advise" + and normalized_subtype != "assignee" + ) return WorkflowSemanticPlanner._looks_like_requirement_scope_advise(message_text) @classmethod @@ -126,11 +136,17 @@ class ExecuteQueryProjectMixin: message_text: str, message_intent: Optional[str], consultant_mode_preference: Optional[str], + advise_subtype: Optional[str], ) -> bool: normalized_intent = str(message_intent or "").strip().lower() normalized_mode = str(consultant_mode_preference or "").strip().lower() - if normalized_intent == "business_review" and normalized_mode == "advise": - return WorkflowSemanticPlanner._looks_like_assignee_recommendation(message_text) + normalized_subtype = str(advise_subtype or "").strip().lower() + if normalized_intent or normalized_mode or normalized_subtype: + return ( + normalized_intent == "business_review" + and normalized_mode == "advise" + and normalized_subtype == "assignee" + ) return WorkflowSemanticPlanner._looks_like_assignee_recommendation(message_text) @staticmethod @@ -148,8 +164,11 @@ class ExecuteQueryProjectMixin: def _build_requirement_clarify_answer(*, message_text: str) -> str: _ = message_text return ( - "可以,我先按轻量版继续理解。你先确认这次最先收窄的是哪一块:目标对象、交付能力,还是覆盖范围;" - "你定一个优先级,我再继续往下收。" + "可以,我先按轻量版继续理解。你先选这次最先收窄的优先级,直接回 A/B/C 就行:\n" + "A. 目标对象:这套自动化最先服务谁\n" + "B. 交付能力:第一版必须先交付什么能力\n" + "C. 覆盖范围:第一版先覆盖哪些部署场景\n" + "你回一个字母,我就顺着这一项继续往下收。" ) @staticmethod @@ -163,7 +182,7 @@ class ExecuteQueryProjectMixin: @classmethod def _detect_requirement_plan_profile(cls, *, message_text: str, card: dict[str, Any]) -> str: existing = str(card.get("plan_profile") or "").strip().lower() - if existing in {"scoring", "generic"}: + if existing in {"scoring", "generic", "it_project"}: return existing normalized = WorkflowSemanticPlanner._normalize_text( " ".join( @@ -173,14 +192,185 @@ class ExecuteQueryProjectMixin: str(card.get("title") or ""), str(card.get("goal") or ""), str(card.get("source_material") or ""), + str(card.get("project_nature") or ""), + str(card.get("tech_route") or ""), + str(card.get("delivery_mode") or ""), ) if str(part or "").strip() ) ) if any(token in normalized for token in ("打分", "评分", "分数", "趋势分")): return "scoring" + if cls._looks_like_it_project_requirement(normalized): + return "it_project" return "generic" + @staticmethod + def _looks_like_it_project_requirement(normalized: str) -> bool: + text = str(normalized or "").strip().lower() + if not text: + return False + strong_tokens = ( + "it项目", + "技术项目", + "openclaw", + "自动化部署", + "批量部署", + "发布治理", + "部署治理", + "发布平台", + "部署平台", + "任务编排", + "环境接入", + "回滚", + "审批", + "平台运维", + "运维团队", + "fastapi", + "react", + "redis", + "celery", + "postgres", + "postgresql", + "k8s", + "kubernetes", + "helm", + "argocd", + "terraform", + "ansible", + "ci/cd", + "cicd", + "jenkins", + ) + if any(token in text for token in strong_tokens): + return True + has_delivery_shape = any(token in text for token in ("部署", "发布", "上线", "交付")) + has_platform_shape = any(token in text for token in ("自动化", "批量", "治理", "回滚", "审批", "环境")) + return has_delivery_shape and has_platform_shape + + @staticmethod + def _extract_requirement_plan_choice(message_text: str) -> Optional[str]: + match = re.match(r"^\s*([ABCabc])(?:[\.\,,。、::)\]】\s]|$)", str(message_text or "")) + if not match: + return None + return match.group(1).strip().upper() + + @classmethod + def _strip_requirement_plan_choice_prefix(cls, message_text: str) -> str: + choice = cls._extract_requirement_plan_choice(message_text) + if not choice: + return str(message_text or "").strip() + return re.sub(r"^\s*[ABCabc](?:[\.\,,。、::)\]】\s]+)?", "", str(message_text or "")).strip() + + @staticmethod + def _it_project_choice_value(*, field: str, choice: str) -> Optional[str]: + mapping = { + "project_nature": { + "A": "内部提效工具", + "B": "发布治理平台", + "C": "云原生交付底座", + }, + "target_user": { + "A": "开发团队", + "B": "平台运维团队", + "C": "项目交付团队", + }, + "tech_route": { + "A": "脚本增强版(FastAPI + Shell/Ansible + PostgreSQL)", + "B": "任务编排平台版(FastAPI + React + Redis + Celery + PostgreSQL)", + "C": "云原生交付版(Kubernetes + Helm + ArgoCD/Terraform + FastAPI)", + }, + "first_scope": { + "A": "第一版先做批量执行闭环", + "B": "第一版先做环境接入闭环", + "C": "第一版先做治理闭环", + }, + "delivery_mode": { + "A": "内部 PoC", + "B": "项目级试点(带基础审批和回滚)", + "C": "平台化上线", + }, + } + normalized_field = str(field or "").strip() + normalized_choice = str(choice or "").strip().upper() + return mapping.get(normalized_field, {}).get(normalized_choice) + + @staticmethod + def _generic_choice_value(*, field: str, choice: str) -> Optional[str]: + mapping = { + "source_material": { + "A": "用户当前需求描述", + "B": "会议纪要/文档", + "C": "录音/转写/回放", + }, + "target_user": { + "A": "核心执行人/内部 PM", + "B": "负责人/管理者", + "C": "整个团队/终端用户", + }, + "output_form": { + "A": "飞书总结", + "B": "飞书消息", + "C": "单独页面", + }, + "cadence": { + "A": "触发后半小时内", + "B": "当天内", + "C": "第二天", + }, + } + normalized_field = str(field or "").strip() + normalized_choice = str(choice or "").strip().upper() + return mapping.get(normalized_field, {}).get(normalized_choice) + + @staticmethod + def _compact_requirement_plan_label(value: Any) -> str: + text = str(value or "").strip() + if not text: + return "" + return text.split("(", 1)[0].strip() + + @classmethod + def _summarize_it_project_choice_label(cls, *, field: str, value: Any) -> str: + text = str(value or "").strip() + normalized = WorkflowSemanticPlanner._normalize_text(text) + if field == "project_nature": + if "内部提效工具" in normalized: + return "内部提效工具" + if any(token in normalized for token in ("发布治理平台", "部署治理平台", "治理平台")): + return "发布治理平台" + if any(token in normalized for token in ("云原生交付底座", "交付底座")): + return "云原生交付底座" + if field == "target_user": + if any(token in normalized for token in ("平台运维", "运维团队")): + return "平台运维团队" + if any(token in normalized for token in ("开发团队", "研发团队", "开发")): + return "开发团队" + if any(token in normalized for token in ("项目交付", "交付团队")): + return "项目交付团队" + if field == "tech_route": + if any(token in normalized for token in ("脚本增强", "shell", "ansible")): + return "脚本增强版" + if any(token in normalized for token in ("任务编排平台版", "celery", "redis")): + return "任务编排平台版" + if any(token in normalized for token in ("云原生交付版", "kubernetes", "argocd", "terraform", "helm")): + return "云原生交付版" + if field == "first_scope": + if any(token in normalized for token in ("批量执行闭环", "任务创建", "执行日志", "失败重试")): + return "批量执行闭环" + if any(token in normalized for token in ("环境接入闭环", "目标机器", "环境注册", "凭证管理")): + return "环境接入闭环" + if any(token in normalized for token in ("治理闭环", "审批", "回滚", "审计")): + return "治理闭环" + if field == "delivery_mode": + if any(token in normalized for token in ("内部poc", "内部验证", "poc")): + return "内部 PoC" + if "项目级试点" in normalized or "试点" in normalized: + return "项目级试点" + if any(token in normalized for token in ("平台化上线", "正式上线", "多团队接入")): + return "平台化上线" + return cls._compact_requirement_plan_label(text).rstrip("。.!!??") + @staticmethod def _sanitize_requirement_plan_text(message_text: str) -> str: cleaned = re.sub( @@ -354,6 +544,13 @@ class ExecuteQueryProjectMixin: return "会议纪要待办提取工具" if any(token in normalized for token in ("会议纪要", "会议记录", "纪要")): return "会议纪要整理工具" + if profile == "it_project": + if "openclaw" in normalized and any(token in normalized for token in ("部署", "发布")): + return "OpenClaw 批量自动化部署平台" + if any(token in normalized for token in ("自动化部署", "批量部署")): + return "批量自动化部署平台" + if any(token in normalized for token in ("发布治理", "部署治理")): + return "发布治理平台" if any(token in normalized for token in ("大a", "a股", "沪深")) and any( token in normalized for token in ("打分", "评分", "分数") ): @@ -378,6 +575,10 @@ class ExecuteQueryProjectMixin: ): return "会后自动整理待办,减少信息散落,让负责人能快速跟进。" return "先把第一版核心能力收清楚,再决定是否正式创建。" + if profile == "it_project": + if "openclaw" in normalized and any(token in normalized for token in ("部署", "发布")): + return "先把 OpenClaw 批量部署、执行追踪和回滚治理收成一条可复用流程,再决定是否正式创建。" + return "先把 IT 项目的第一版目标、技术路线和推进方式收清楚,再决定是否正式创建。" if any(token in normalized for token in ("大a", "a股", "沪深")) and any( token in normalized for token in ("打分", "评分", "分数") ): @@ -390,6 +591,11 @@ class ExecuteQueryProjectMixin: normalized = WorkflowSemanticPlanner._normalize_text(raw) updates: dict[str, Any] = {} profile = cls._detect_requirement_plan_profile(message_text=raw, card=card) + choice = cls._extract_requirement_plan_choice(raw) + cleaned_raw = cls._strip_requirement_plan_choice_prefix(raw) + current_card = dict(card or {}) + current_card["plan_profile"] = profile + missing_fields = cls._requirement_plan_missing_fields(current_card) updates["plan_profile"] = profile if not card.get("title"): updates["title"] = cls._derive_requirement_plan_title(message_text=raw, card=card) @@ -403,6 +609,17 @@ class ExecuteQueryProjectMixin: updates["source_material"] = "会议录音/转写" elif any(token in normalized for token in ("直播录屏", "回放")): updates["source_material"] = "直播录屏/回放" + elif WorkflowSemanticPlanner.looks_like_explicit_new_requirement_topic(raw): + topic_tail = "" + if "关于" in normalized: + topic_tail = normalized.partition("关于")[2] + topic_tail = re.sub(r"[@_a-z0-9]+", "", topic_tail) + if len(topic_tail) >= 4: + updates["source_material"] = "用户当前需求描述" + elif choice and missing_fields and missing_fields[0] == "source_material": + mapped = cls._generic_choice_value(field="source_material", choice=choice) + if mapped: + updates["source_material"] = mapped if profile == "scoring" and not card.get("scoring_basis"): if any(token in normalized for token in ("技术指标", "均线", "成交量", "涨跌比", "量价", "macd", "rsi")): @@ -412,7 +629,75 @@ class ExecuteQueryProjectMixin: elif any(token in normalized for token in ("规则", "已有逻辑", "现成逻辑", "自动化")): updates["scoring_basis"] = "沿用现有打分规则并自动化" - if not card.get("target_user"): + if profile == "it_project": + if not card.get("project_nature"): + if any(token in normalized for token in ("内部提效工具", "脚本工具", "提效工具")): + updates["project_nature"] = "内部提效工具" + elif any(token in normalized for token in ("发布治理平台", "部署治理平台", "治理平台")): + updates["project_nature"] = "发布治理平台" + elif any(token in normalized for token in ("云原生交付底座", "交付底座")): + updates["project_nature"] = "云原生交付底座" + elif choice and missing_fields and missing_fields[0] == "project_nature": + mapped = cls._it_project_choice_value(field="project_nature", choice=choice) + if mapped: + updates["project_nature"] = mapped + + if not card.get("target_user"): + target_match = re.search(r"(?:主要)?给([^,。;;]{1,24}?)(?:看|用)", cleaned_raw) + if target_match: + updates["target_user"] = target_match.group(1).strip() + elif any(token in cleaned_raw for token in ("平台运维", "运维团队")): + updates["target_user"] = "平台运维团队" + elif any(token in cleaned_raw for token in ("开发团队", "研发团队", "开发")): + updates["target_user"] = "开发团队" + elif any(token in cleaned_raw for token in ("项目交付", "交付团队")): + updates["target_user"] = "项目交付团队" + elif choice and missing_fields and missing_fields[0] == "target_user": + mapped = cls._it_project_choice_value(field="target_user", choice=choice) + if mapped: + updates["target_user"] = mapped + + if not card.get("tech_route"): + if all(token in normalized for token in ("fastapi", "react")) and any( + token in normalized for token in ("celery", "redis", "postgres", "postgresql") + ): + updates["tech_route"] = "任务编排平台版(FastAPI + React + Redis + Celery + PostgreSQL)" + elif any(token in normalized for token in ("shell", "ansible")) or "脚本增强" in normalized: + updates["tech_route"] = "脚本增强版(FastAPI + Shell/Ansible + PostgreSQL)" + elif any(token in normalized for token in ("k8s", "kubernetes", "helm", "argocd", "terraform")): + updates["tech_route"] = "云原生交付版(Kubernetes + Helm + ArgoCD/Terraform + FastAPI)" + elif "任务编排平台版" in cleaned_raw: + updates["tech_route"] = "任务编排平台版(FastAPI + React + Redis + Celery + PostgreSQL)" + elif choice and missing_fields and missing_fields[0] == "tech_route": + mapped = cls._it_project_choice_value(field="tech_route", choice=choice) + if mapped: + updates["tech_route"] = mapped + + if not card.get("first_scope"): + if any(token in cleaned_raw for token in ("批量执行闭环", "任务创建", "执行日志", "失败重试")): + updates["first_scope"] = cleaned_raw + elif any(token in cleaned_raw for token in ("环境接入闭环", "目标机器", "环境注册", "凭证管理")): + updates["first_scope"] = cleaned_raw + elif any(token in cleaned_raw for token in ("治理闭环", "审批", "回滚", "审计")): + updates["first_scope"] = cleaned_raw + elif choice and missing_fields and missing_fields[0] == "first_scope": + mapped = cls._it_project_choice_value(field="first_scope", choice=choice) + if mapped: + updates["first_scope"] = mapped + + if not card.get("delivery_mode"): + if any(token in normalized for token in ("poc", "验证版", "内部验证", "内部poc")): + updates["delivery_mode"] = "内部 PoC" + elif any(token in cleaned_raw for token in ("项目级试点", "试点")): + updates["delivery_mode"] = "项目级试点(带基础审批和回滚)" + elif any(token in cleaned_raw for token in ("平台化上线", "正式上线", "多团队接入")): + updates["delivery_mode"] = "平台化上线" + elif choice and missing_fields and missing_fields[0] == "delivery_mode": + mapped = cls._it_project_choice_value(field="delivery_mode", choice=choice) + if mapped: + updates["delivery_mode"] = mapped + + if profile != "it_project" and not card.get("target_user"): target_match = re.search(r"(?:主要)?给([^,。;;]{1,24}?)(?:看|用)", raw) if target_match: updates["target_user"] = target_match.group(1).strip() @@ -426,8 +711,12 @@ class ExecuteQueryProjectMixin: updates["target_user"] = raw.strip("。;;") elif "自己" in raw: updates["target_user"] = "自己" + elif profile == "generic" and choice and missing_fields and missing_fields[0] == "target_user": + mapped = cls._generic_choice_value(field="target_user", choice=choice) + if mapped: + updates["target_user"] = mapped - if not card.get("output_form"): + if profile != "it_project" and not card.get("output_form"): if "飞书总结" in raw: updates["output_form"] = "飞书总结" elif "飞书日报" in raw: @@ -438,8 +727,12 @@ class ExecuteQueryProjectMixin: updates["output_form"] = "页面看板" elif "简报" in raw: updates["output_form"] = "简报" + elif profile == "generic" and choice and missing_fields and missing_fields[0] == "output_form": + mapped = cls._generic_choice_value(field="output_form", choice=choice) + if mapped: + updates["output_form"] = mapped - if not card.get("cadence"): + if profile != "it_project" and not card.get("cadence"): if any(token in normalized for token in ("每次会后半小时内", "会后半小时内")): updates["cadence"] = "每次会后半小时内" elif any(token in normalized for token in ("每日收盘后", "每天收盘后", "收盘后")): @@ -454,8 +747,12 @@ class ExecuteQueryProjectMixin: token in normalized for token in ("更新", "发", "推送", "同步", "生成", "刷新", "跑") ): updates["cadence"] = "每日" + elif profile == "generic" and choice and missing_fields and missing_fields[0] == "cadence": + mapped = cls._generic_choice_value(field="cadence", choice=choice) + if mapped: + updates["cadence"] = mapped - if not card.get("first_scope") and any( + if profile != "it_project" and not card.get("first_scope") and any( token in normalized for token in ("先保留", "先只做", "别做太重", "范围够轻", "方向判断", "先抽", "先提", "不做复杂") ) and not any(token in normalized for token in ("先别创建", "别创建", "先收第一版", "先收一版")): @@ -473,6 +770,9 @@ class ExecuteQueryProjectMixin: def _requirement_plan_missing_fields(cls, card: dict[str, Any]) -> list[str]: profile = cls._detect_requirement_plan_profile(message_text="", card=card) ordered_fields = ( + ("project_nature", "target_user", "tech_route", "first_scope", "delivery_mode") + if profile == "it_project" + else ("scoring_basis", "target_user", "output_form", "cadence", "first_scope") if profile == "scoring" else ("source_material", "target_user", "output_form", "cadence", "first_scope") @@ -482,10 +782,26 @@ class ExecuteQueryProjectMixin: @classmethod def _build_requirement_plan_ack(cls, *, updates: dict[str, Any], profile: str) -> str: parts: list[str] = [] + if profile == "it_project": + if str(updates.get("project_nature") or "").strip(): + parts.append(f"项目先按{cls._summarize_it_project_choice_label(field='project_nature', value=updates['project_nature'])}来收") + if str(updates.get("target_user") or "").strip(): + parts.append(f"主要服务{cls._summarize_it_project_choice_label(field='target_user', value=updates['target_user'])}") + if str(updates.get("tech_route") or "").strip(): + parts.append(f"技术路线先按{cls._summarize_it_project_choice_label(field='tech_route', value=updates['tech_route'])}") + if str(updates.get("first_scope") or "").strip(): + parts.append(f"第一版先收在{cls._summarize_it_project_choice_label(field='first_scope', value=updates['first_scope'])}") + if str(updates.get("delivery_mode") or "").strip(): + parts.append(f"推进方式先按{cls._summarize_it_project_choice_label(field='delivery_mode', value=updates['delivery_mode'])}") + if not parts: + return "" + return f"好,我先按这个方向继续记:{';'.join(parts[:2])}。" if profile == "scoring" and str(updates.get("scoring_basis") or "").strip(): parts.append(f"第一版按{updates['scoring_basis']}来打分") - if profile == "generic" and str(updates.get("source_material") or "").strip(): - parts.append(f"输入先按{updates['source_material']}来") + if profile == "generic": + source_material = str(updates.get("source_material") or "").strip() + if source_material and source_material != "用户当前需求描述": + parts.append(f"输入先按{source_material}来") if str(updates.get("target_user") or "").strip(): parts.append(f"主要给{updates['target_user']}用") if str(updates.get("output_form") or "").strip(): @@ -496,8 +812,135 @@ class ExecuteQueryProjectMixin: return "" return f"好,我先记下:{','.join(parts[:2])}。" - @staticmethod - def _build_requirement_plan_next_question(*, missing_field: str, profile: str, message_text: str = "") -> str: + @classmethod + def _build_requirement_plan_next_question( + cls, + *, + missing_field: str, + profile: str, + message_text: str = "", + card: Optional[dict[str, Any]] = None, + ) -> str: + if profile == "it_project": + current_card = dict(card or {}) + current_project_nature = cls._summarize_it_project_choice_label(field="project_nature", value=current_card.get("project_nature") or "当前方向") + current_target_user = cls._summarize_it_project_choice_label(field="target_user", value=current_card.get("target_user") or "当前团队") + current_tech_route = cls._summarize_it_project_choice_label(field="tech_route", value=current_card.get("tech_route") or "当前技术路线") + current_first_scope = cls._summarize_it_project_choice_label(field="first_scope", value=current_card.get("first_scope") or "当前范围") + if missing_field == "project_nature": + return ( + "🧭 我先按“批量自动化部署 OpenClaw”这类 IT 项目来收。\n" + "现在最关键的,不是立刻展开做法,而是先定它更像工具、平台,还是长期底座。\n\n" + "A. 先做内部提效工具\n" + "更适合:先减少重复部署和手工操作\n" + "推荐技术/能力:FastAPI + React + PostgreSQL\n" + "我为什么会考虑这个:开发快,适合先把 MVP 跑通\n" + "代价:后续任务编排和治理能力要继续补\n\n" + "B. 先做发布治理平台\n" + "更适合:多人协作、多环境发布、需要审批和回滚\n" + "推荐技术/能力:FastAPI + React + PostgreSQL + Redis\n" + "我当前更偏这个:更适合承接状态流转、执行追踪和治理规则\n" + "代价:前期设计复杂度高于 A\n\n" + "C. 直接做云原生交付底座\n" + "更适合:目标是多环境、多实例、规模化标准交付\n" + "推荐技术/能力:Kubernetes + Helm + ArgoCD/Terraform\n" + "我为什么暂时不主推:长期上限最高,但基础设施门槛也最大\n" + "代价:基础设施门槛和早期投入最大\n\n" + "⭐ 当前我更推荐:B\n" + "原因:你提的是批量自动化部署,通常核心矛盾不只是提效,而是发布治理和流程标准化。\n\n" + "你回 A / B / C,我就顺着这条继续往下收。" + ) + if missing_field == "target_user": + return ( + f"🧭 我先按“{current_project_nature}”这个方向继续收。\n" + "现在要先把服务对象定住,不然后面做执行体验、治理能力还是项目复制,重心会飘。\n\n" + "A. 开发团队\n" + "更适合:开发自己发起部署、追执行结果\n" + "推荐技术/能力:一键执行、日志查看、失败重试\n" + "我为什么会考虑这个:反馈最快,最适合先验证使用价值\n" + "代价:治理能力会相对弱一些\n\n" + "B. 平台运维团队\n" + "更适合:统一管理环境、凭证、发布窗口和回滚\n" + "推荐技术/能力:批量执行、审批、审计、回滚\n" + "我当前更偏这个:最符合部署治理场景,风险控制也更强\n" + "代价:前期需要更清晰的权限和流程边界\n\n" + "C. 项目交付团队\n" + "更适合:按项目模板复制环境或服务\n" + "推荐技术/能力:项目模板、环境参数、标准交付流程\n" + "我为什么会考虑这个:适合多项目复制交付\n" + "代价:前提是流程已经比较稳定\n\n" + "⭐ 当前我更推荐:B\n" + "原因:先把平台运维场景收清楚,后面的技术路线、权限边界和治理要求会更稳。\n\n" + "你回 A / B / C,我就顺着这条继续往下收。" + ) + if missing_field == "tech_route": + return ( + f"🧭 我先按“{current_target_user}要长期使用”这个方向继续收。\n" + "现在最关键的,不是再补更多背景,而是先把技术路线定住,因为它会直接影响第一版工期和后续扩展方式。\n\n" + "A. 先做脚本增强版\n" + "更适合:先快速验证批量执行闭环\n" + "推荐技术/能力:FastAPI + Shell/Ansible + PostgreSQL\n" + "我为什么会考虑这个:最快能落地,适合先把执行链跑通\n" + "代价:任务编排和可观测性上限一般\n\n" + "B. 先做任务编排平台版\n" + "更适合:长期要做批量任务、状态机、失败重试和执行追踪\n" + "推荐技术/能力:FastAPI + React + Redis + Celery + PostgreSQL\n" + "我当前更偏这个:最平衡,既能落地又保留平台化空间\n" + "代价:系统复杂度高于 A\n\n" + "C. 直接做云原生交付版\n" + "更适合:目标是多环境、多实例和规模化标准交付\n" + "推荐技术/能力:Kubernetes + Helm + ArgoCD/Terraform + FastAPI\n" + "我为什么暂时不主推:环境一致性和规模化能力最强,但门槛也最高\n" + "代价:对基础设施成熟度要求最高\n\n" + "⭐ 当前我更推荐:B\n" + "原因:它最适合把批量自动化部署做成长期可用的平台,而不是一次性脚本工具。\n\n" + "你回 A / B / C,我就顺着这条继续往下收。" + ) + if missing_field == "first_scope": + return ( + f"🧭 我先按“{current_tech_route}”继续往下收。\n" + "这一步要定的,是第一版到底先证明什么价值,避免一上来摊太大。\n\n" + "A. 批量执行闭环\n" + "更适合:先证明平台真正能跑通部署任务\n" + "推荐技术/能力:任务创建、执行日志、失败重试\n" + "我当前更偏这个:最能体现平台核心价值\n" + "代价:环境接入和治理能力会稍后补\n\n" + "B. 环境接入闭环\n" + "更适合:先把服务器、集群、仓库和凭证接进来\n" + "推荐技术/能力:目标注册、凭证管理、环境选择\n" + "我为什么会考虑这个:能为后续自动化执行打基础\n" + "代价:短期可见价值不如 A 直接\n\n" + "C. 治理闭环\n" + "更适合:团队已经进入正式上线阶段\n" + "推荐技术/能力:审批、回滚、审计、权限\n" + "我为什么会考虑这个:能先控制上线风险和责任边界\n" + "代价:没有稳定执行闭环时,容易先做重了\n\n" + "⭐ 当前我更推荐:A\n" + "原因:MVP 最先要证明“能执行、能追踪、失败能恢复”,这样平台主线才会立住。\n\n" + "你回 A / B / C,我就顺着这条继续往下收。" + ) + return ( + f"🧭 我先按“{current_first_scope}”继续收尾。\n" + "现在要拍板的,是先用什么方式把第一版推起来:先轻量验证、先试点,还是直接平台化。\n\n" + "A. 内部 PoC\n" + "更适合:先验证 1-2 个典型部署任务\n" + "推荐技术/能力:轻量试用、快速反馈、低风险验证\n" + "我为什么会考虑这个:最适合快速试错\n" + "代价:对真实治理问题暴露不够充分\n\n" + "B. 项目级试点\n" + "更适合:选一个真实项目先落地\n" + "推荐技术/能力:试点接入、基础审批、基础回滚\n" + "我当前更偏这个:最能检验真实价值,也能尽早暴露流程问题\n" + "代价:需要更明确的项目配合和负责人\n\n" + "C. 平台化上线\n" + "更适合:一开始就计划多团队接入\n" + "推荐技术/能力:统一权限、统一规范、统一环境管理\n" + "我为什么暂时不主推:长期价值最大,但前期投入和协作复杂度也最高\n" + "代价:前期投入和协作复杂度最高\n\n" + "⭐ 当前我更推荐:B\n" + "原因:PoC 太轻,平台化太重,项目级试点最适合项目管理推进和真实验收。\n\n" + "你回 A / B / C,我就顺着这条继续往下收。" + ) if profile == "scoring": if missing_field == "scoring_basis": return "那我先确认一个最关键的问题:这个分数主要想根据什么来打,是技术指标、情绪资金,还是你已经有一套规则想自动化?" @@ -515,21 +958,243 @@ class ExecuteQueryProjectMixin: return "第一版你最想先解决复盘里的哪一步:切片、总结,还是可执行清单?" if any(token in normalized for token in ("雷达", "监控", "预警")): return "第一版你最想先盯哪类信号:政策、舆情,还是竞品动作?" - return "我先确认输入从哪来,是会议纪要、录音转写,还是别的材料?" + return ( + "我先把输入来源定一下,不然后面收范围时容易越聊越散。\n\n" + "A. 先按当前聊天里的需求描述来\n" + "更适合:你先把目标讲清,我边聊边收第一版\n\n" + "B. 先按会议纪要 / 文档来\n" + "更适合:已经有结构化材料,重点是提炼结果\n\n" + "C. 先按录音转写 / 回放来\n" + "更适合:信息还比较散,需要先转成可整理内容\n\n" + "你回 A / B / C,也可以直接说更准确的输入来源。" + ) if missing_field == "target_user": - return "我先确认一下,这个结果主要给谁用,是内部 PM、负责人,还是整个团队?" + return ( + "我先把结果主要给谁用定一下,这会直接影响后面的内容粒度和表达方式。\n\n" + "A. 核心执行人 / 内部 PM\n" + "更适合:先服务负责跟进、拆动作、推动落地的人\n\n" + "B. 负责人 / 管理者\n" + "更适合:先看结论、风险和待拍板项\n\n" + "C. 整个团队 / 终端用户\n" + "更适合:需要直接同步所有相关方,或结果就是给最终使用者看的\n\n" + "你回 A / B / C,也可以直接说更准确的对象。" + ) if missing_field == "output_form": - return "结果你更希望最后怎么给结果,是飞书总结、飞书消息,还是单独页面?" + return ( + "结果最后怎么给你,我先一起定掉,不然后面范围会偏。\n\n" + "A. 飞书总结\n" + "更适合:一次性看完整结论和重点\n\n" + "B. 飞书消息\n" + "更适合:更轻、更快,直接在会话里收结果\n\n" + "C. 单独页面\n" + "更适合:后面要长期查看、筛选或继续编辑\n\n" + "你回 A / B / C,也可以直接说你想要的交付方式。" + ) if missing_field == "cadence": - return "时效我也一起记一下:你希望多久整理出来,是每次会后半小时内、当天内,还是第二天?" + return ( + "时效我也一起记一下,先把你能接受的出结果节奏定住。\n\n" + "A. 触发后半小时内\n" + "更适合:希望尽快看到第一版结果\n\n" + "B. 当天内\n" + "更适合:接受汇总后统一产出\n\n" + "C. 第二天\n" + "更适合:允许补充整理和复核\n\n" + "你回 A / B / C,也可以直接说更具体的时效。" + ) return "第一版先保留哪些字段最有价值,比如待办、责任人、截止时间,哪些先不做?" + @classmethod + def _build_it_project_signal_sources(cls, *, card: dict[str, Any]) -> str: + normalized = WorkflowSemanticPlanner._normalize_text( + " ".join( + str(card.get(key) or "").strip() + for key in ("title", "goal", "project_nature", "tech_route", "first_scope", "delivery_mode") + if str(card.get(key) or "").strip() + ) + ) + sources = ["飞书需求消息"] + if any(token in normalized for token in ("部署", "发布", "openclaw", "回滚", "环境")): + sources.extend( + [ + "部署目标清单(服务/环境/实例)", + "代码仓库/镜像信息", + "环境参数与凭证", + "执行日志与失败日志", + ] + ) + else: + sources.extend(["目标对象清单", "代码与配置来源", "执行反馈日志"]) + deduped = list(dict.fromkeys(sources)) + return f"当前至少要接入:{';'.join(deduped)}" + + @classmethod + def _build_it_project_known_info(cls, *, card: dict[str, Any]) -> str: + facts: list[str] = [] + title = str(card.get("title") or "").strip() + project_nature = str(card.get("project_nature") or "").strip() + target_user = str(card.get("target_user") or "").strip() + tech_route = str(card.get("tech_route") or "").strip() + first_scope = str(card.get("first_scope") or "").strip() + delivery_mode = str(card.get("delivery_mode") or "").strip() + if title: + facts.append(f"当前要解决的是{title}") + if project_nature: + facts.append(f"当前按{project_nature}收敛") + if target_user: + facts.append(f"主要服务{target_user}") + if tech_route: + facts.append(f"技术路线先按{cls._compact_requirement_plan_label(tech_route)}") + if first_scope: + facts.append(f"第一版先按{cls._compact_requirement_plan_label(first_scope)}推进") + if delivery_mode: + facts.append(f"推进方式先按{cls._compact_requirement_plan_label(delivery_mode)}") + return f"目前已经明确:{';'.join(facts)}" if facts else "目前已知信息还比较少,建议先补部署对象、环境清单和现有流程。" + + @classmethod + def _build_it_project_one_line_judgement(cls, *, card: dict[str, Any]) -> str: + target_user = str(card.get("target_user") or "核心团队").strip() + delivery_mode = cls._compact_requirement_plan_label(str(card.get("delivery_mode") or "内部 PoC")) + if not delivery_mode: + delivery_mode = "内部 PoC" + return ( + f"先按面向{target_user}的{delivery_mode}推进," + "先把 OpenClaw 的批量部署、执行追踪和失败恢复跑成一条闭环。" + ) + + @staticmethod + def _trim_sentence_tail(value: Any) -> str: + return str(value or "").strip().rstrip("。.!!??") + + @staticmethod + def _build_it_project_core_flow(*, card: dict[str, Any]) -> str: + _ = card + return "提交部署需求 -> 生成部署任务 -> 选择目标环境 -> 执行部署 -> 回传结果/日志 -> 失败重试或回滚 -> 留痕审计" + + @classmethod + def _build_it_project_staffing(cls, *, card: dict[str, Any]) -> str: + delivery_mode = str(card.get("delivery_mode") or "").strip() + tech_route = str(card.get("tech_route") or "").strip() + if "平台化上线" in delivery_mode: + staffing = [ + "需求负责人/PM 1名", + "后端工程师 2名", + "前端工程师 1名", + "平台/运维工程师 1-2名", + "测试/验收 1名", + "业务/审批代表 1名", + ] + elif "项目级试点" in delivery_mode: + staffing = [ + "需求负责人/PM 1名", + "后端工程师 1名", + "前端工程师 1名", + "平台/运维工程师 1名", + "测试/试点验收 1名", + "试点负责人 1名", + ] + else: + staffing = [ + "需求负责人/PM 1名", + "后端工程师 1名", + "平台/运维工程师 1名", + "前端工程师 0.5-1名", + "试用/验收 1名", + ] + if any(token in tech_route for token in ("Kubernetes", "ArgoCD", "Terraform")): + staffing.append("云原生平台支持 1名") + return f"建议最小配置:{';'.join(staffing)}" + + @classmethod + def _build_it_project_risks_and_dependencies(cls, *, card: dict[str, Any]) -> str: + delivery_mode = str(card.get("delivery_mode") or "").strip() + tech_route = str(card.get("tech_route") or "").strip() + risks = [ + "部署目标清单是否稳定", + "环境访问权限与凭证归属", + "仓库/镜像/配置规范", + "凭证管理", + "失败重试与回滚策略", + ] + if "项目级试点" in delivery_mode: + risks.append("试点项目配合度") + if "平台化上线" in delivery_mode: + risks.append("统一权限与审批链路") + if any(token in tech_route for token in ("Kubernetes", "ArgoCD", "Terraform")): + risks.append("K8s/基础设施成熟度") + return "当前主要风险在" + "、".join(risks) + + @classmethod + def _build_it_project_pending_items(cls, *, card: dict[str, Any]) -> list[str]: + pending: list[str] = [] + if not str(card.get("success_signal") or "").strip(): + pending.append("成功判断标准还没完全定死") + pending.extend( + [ + "部署目标范围(服务/环境/实例)", + "仓库/镜像/配置来源", + "环境权限与凭证归属", + "试点负责人和投入人天", + ] + ) + tech_route = str(card.get("tech_route") or "").strip() + if any(token in tech_route for token in ("Kubernetes", "ArgoCD", "Terraform")): + pending.append("现有 K8s / GitOps 基础设施成熟度") + return list(dict.fromkeys(item for item in pending if str(item or "").strip())) + @classmethod def _build_requirement_plan_draft_text(cls, *, card: dict[str, Any]) -> str: profile = cls._detect_requirement_plan_profile(message_text="", card=card) pending = [] if not str(card.get("success_signal") or "").strip(): pending.append("成功判断标准还没完全定死") + if profile == "it_project": + pending = cls._build_it_project_pending_items(card=card) + lines = [ + "📋 我先按当前收敛结果,整理成一版更像正式 brief 的需求草稿:", + "", + f"🧾【需求名称】{str(card.get('title') or '待补充').strip()}", + "", + f"🧭【一句话判断】{cls._build_it_project_one_line_judgement(card=card)}", + "", + "🎯【需求目标】" + f"{cls._trim_sentence_tail(card.get('goal') or '待补充')}。" + " 这次先不追求把审批、审计、权限一次做满,而是先把主链路跑通。", + "", + f"🏗️【项目性质】这件事更像{str(card.get('project_nature') or '待补充').strip()},不像一次性脚本工具,后面需要承接状态流转和回滚治理。", + "", + f"👥【服务对象】第一版先服务{str(card.get('target_user') or '待补充').strip()},他们会最早发起部署、看执行结果、处理失败和判断是否回滚。", + "", + f"📡【信号源】{cls._build_it_project_signal_sources(card=card)}", + "", + f"📝【当前已知信息】{cls._build_it_project_known_info(card=card)}", + "", + "⚙️【技术路线】" + f"当前更适合走{str(card.get('tech_route') or '待补充').strip()}," + "因为它比脚本方案更稳,也没有一上来重到云原生底座。", + "", + "🧱【第一版范围】" + f"{cls._trim_sentence_tail(card.get('first_scope') or '先收一个最小可跑闭环')}。" + " 这版重点先放在任务创建、执行日志和失败重试。", + "", + f"🔄【建议核心流程】{cls._build_it_project_core_flow(card=card)}", + "", + f"👤【人员配置】{cls._build_it_project_staffing(card=card)}", + "", + "🚀【推进方式】" + f"先按{str(card.get('delivery_mode') or '待补充').strip()}推进," + "这样能尽快在真实项目里验证价值,也能更早暴露流程问题。", + "", + "✅【成功标准】" + f"{cls._trim_sentence_tail(card.get('success_signal') or '先保证能稳定创建部署任务、看到执行结果,并能识别失败原因')}," + "并且能在试点里跑完一次真实闭环。", + "", + f"⚠️【风险与依赖】{cls._build_it_project_risks_and_dependencies(card=card)}。", + "", + f"❓【待确认】还需要你拍板:{';'.join(pending) if pending else '暂无,已经可以继续整理成正式创建草稿'}", + "", + "如果你认可,我下一轮就按这版继续细化;如果要改,直接说改哪一项,我就在这版上改。", + ] + return "\n".join(lines).strip() if profile == "generic": lines = [ "📋 我先把当前收敛结果整理成一版需求草稿,你直接看这版就行:", @@ -564,6 +1229,115 @@ class ExecuteQueryProjectMixin: ] return "\n".join(lines).strip() + @classmethod + def _build_requirement_plan_requirement_items(cls, *, card: dict[str, Any]) -> list[dict[str, str]]: + profile = cls._detect_requirement_plan_profile(message_text="", card=card) + items: list[dict[str, str]] = [] + + def _append(section: str, title: str, value: Any) -> None: + text = str(value or "").strip() + if not text: + return + items.append( + { + "section": section, + "title": title, + "description": text, + } + ) + + _append("需求目标", "需求目标", card.get("goal")) + _append("目标用户", "目标用户", card.get("target_user")) + _append("第一版范围", "第一版范围", card.get("first_scope")) + _append("成功标准", "成功标准", card.get("success_signal")) + if profile == "it_project": + _append("项目性质", "项目性质", card.get("project_nature")) + _append("技术路线", "技术路线", card.get("tech_route")) + _append("推进方式", "推进方式", card.get("delivery_mode")) + elif profile == "scoring": + _append("打分依据", "打分依据", card.get("scoring_basis")) + _append("输出形式", "输出形式", card.get("output_form")) + _append("更新频率", "更新频率", card.get("cadence")) + else: + _append("输入材料", "输入材料", card.get("source_material")) + _append("输出形式", "输出形式", card.get("output_form")) + _append("响应时效", "响应时效", card.get("cadence")) + return items[:12] + + @classmethod + def _build_requirement_plan_missing_info(cls, *, card: dict[str, Any]) -> list[str]: + profile = cls._detect_requirement_plan_profile(message_text="", card=card) + if profile == "it_project": + return cls._build_it_project_pending_items(card=card) + + label_map = { + "source_material": "输入材料", + "target_user": "目标用户", + "output_form": "输出形式", + "cadence": "响应时效", + "scoring_basis": "打分依据", + "first_scope": "第一版范围", + "success_signal": "成功标准", + } + pending: list[str] = [] + for field in cls._requirement_plan_missing_fields(card): + label = label_map.get(str(field or "").strip()) + if label: + pending.append(label) + if not str(card.get("success_signal") or "").strip(): + pending.append("成功标准") + return list(dict.fromkeys(item for item in pending if str(item or "").strip())) + + @classmethod + def _build_requirement_plan_project_creation_draft( + cls, + *, + card: dict[str, Any], + draft_text: str, + ) -> dict[str, Any]: + profile = cls._detect_requirement_plan_profile(message_text="", card=card) + title = str(card.get("title") or "需求收敛草稿").strip() or "需求收敛草稿" + goal = str(card.get("goal") or "").strip() + first_scope = str(card.get("first_scope") or "").strip() + success_signal = str(card.get("success_signal") or "").strip() + target_user = str(card.get("target_user") or "").strip() + summary_parts = [text for text in (goal, first_scope) if text] + summary = ";".join(dict.fromkeys(summary_parts))[:300] or title + positioning = "" + if profile == "it_project": + positioning = cls._build_it_project_one_line_judgement(card=card) + if not positioning: + positioning = first_scope or goal or title + + draft_payload = { + "draft_source": "requirement_plan", + "plan_profile": profile, + "title": title, + "summary": summary, + "positioning_one_liner": positioning[:200], + "target_customers": target_user[:120] or None, + "original_text": str(draft_text or "").strip()[:4000] or summary, + "requirement_items": cls._build_requirement_plan_requirement_items(card=card), + "missing_info": cls._build_requirement_plan_missing_info(card=card), + "mvp_items": [first_scope] if first_scope else [], + "validation_metrics": ( + {"product": [success_signal]} + if success_signal + else {} + ), + "success_signal": success_signal or None, + "draft_text": str(draft_text or "").strip(), + "requirement_card": dict(card), + } + if profile == "it_project": + draft_payload["team_setup"] = [ + { + "role": "建议配置", + "notes": cls._build_it_project_staffing(card=card), + } + ] + return draft_payload + @classmethod def _resolve_requirement_plan_status( cls, @@ -579,20 +1353,76 @@ class ExecuteQueryProjectMixin: return "options_ready" return "collecting" + @classmethod + def _requirement_plan_material_updates(cls, *, message_text: str) -> dict[str, Any]: + return { + key: value + for key, value in cls._extract_requirement_plan_updates(message_text=message_text, card={}).items() + if key not in {"plan_profile", "title", "goal"} + } + + @classmethod + def _topic_expects_requirement_plan_choice_reply(cls, *, topic: Optional[MountedTopicWorkspace]) -> bool: + if topic is None: + return False + plan_status = str(getattr(topic, "plan_status", "") or "").strip().lower() + if plan_status in {"awaiting_execute_confirm", "plan_ready", "suspended"}: + return False + pending_actions = getattr(topic, "pending_actions", None) + if isinstance(pending_actions, (list, tuple, set)) and "confirm_requirement_draft" in { + str(item or "").strip() for item in pending_actions + }: + return False + metadata = getattr(topic, "metadata_json", None) + if not isinstance(metadata, dict): + return False + last_prompt = str(metadata.get("plan_last_prompt") or "").strip() + if not last_prompt: + return False + normalized_prompt = WorkflowSemanticPlanner._normalize_text(last_prompt) + if "你回a/b/c" in normalized_prompt or "直接回a/b/c" in normalized_prompt: + return True + return bool(re.search(r"(^|\n)\s*[ABC]\.\s*", last_prompt)) + + @classmethod + def _should_continue_requirement_plan_from_workspace( + cls, + *, + message_text: str, + topic: Optional[MountedTopicWorkspace] = None, + ) -> bool: + normalized = WorkflowSemanticPlanner._normalize_text(message_text) + if not normalized or WorkflowSemanticPlanner._looks_like_casual_chat_message(message_text): + return False + if WorkflowSemanticPlanner.looks_like_explicit_new_requirement_topic(message_text): + return False + # Active plan threads often continue with "A,xxx原因..." rather than a bare letter. + # Once a user is clearly responding to an A/B/C follow-up, keep the flow in plan mode. + if cls._extract_requirement_plan_choice(message_text): + return cls._topic_expects_requirement_plan_choice_reply(topic=topic) + if cls._requirement_plan_material_updates(message_text=message_text): + return True + return any(token in normalized for token in ("整理成草稿", "出草稿", "汇总一下", "按这版", "就这样先收")) + @classmethod def _looks_like_requirement_plan_seed(cls, *, message_text: str) -> bool: normalized = WorkflowSemanticPlanner._normalize_text(message_text) if not normalized: return False - raw_updates = cls._extract_requirement_plan_updates(message_text=message_text, card={}) - material_updates = { - key: value - for key, value in raw_updates.items() - if key not in {"plan_profile", "title", "goal"} - } + material_updates = cls._requirement_plan_material_updates(message_text=message_text) has_updates = bool(material_updates) if WorkflowSemanticPlanner.looks_like_explicit_new_requirement_topic(message_text): - return has_updates or WorkflowSemanticPlanner._looks_like_requirement_plan_chat(message_text) or WorkflowSemanticPlanner.looks_like_project_create_request(message_text) + topic_tail = "" + if "关于" in normalized: + topic_tail = normalized.partition("关于")[2] + topic_tail = re.sub(r"[@_a-z0-9]+", "", topic_tail) + has_topic_subject = len(topic_tail) >= 4 + return ( + has_updates + or WorkflowSemanticPlanner._looks_like_requirement_plan_chat(message_text) + or WorkflowSemanticPlanner.looks_like_project_create_request(message_text) + or has_topic_subject + ) if has_updates: return True return ( @@ -603,21 +1433,9 @@ class ExecuteQueryProjectMixin: @classmethod def _looks_like_requirement_plan_continuation(cls, *, message_text: str) -> bool: - normalized = WorkflowSemanticPlanner._normalize_text(message_text) - if not normalized or WorkflowSemanticPlanner._looks_like_casual_chat_message(message_text): - return False - if WorkflowSemanticPlanner.looks_like_explicit_new_requirement_topic(message_text): - return False - if WorkflowSemanticPlanner._looks_like_requirement_plan_slot_answer(message_text): + if cls._should_continue_requirement_plan_from_workspace(message_text=message_text): return True - material_updates = { - key: value - for key, value in cls._extract_requirement_plan_updates(message_text=message_text, card={}).items() - if key not in {"plan_profile", "title", "goal"} - } - if material_updates: - return True - return any(token in normalized for token in ("整理成草稿", "出草稿", "汇总一下", "按这版", "就这样先收")) + return WorkflowSemanticPlanner._looks_like_requirement_plan_slot_answer(message_text) @classmethod def _collect_requirement_topic_text_fragments(cls, payload: Any) -> list[str]: @@ -831,13 +1649,25 @@ class ExecuteQueryProjectMixin: .order_by(MountedTopicWorkspace.updated_at.desc(), MountedTopicWorkspace.id.desc()) ) ).scalars().all() - topic = None if force_new_topic else self._select_requirement_plan_topic( - candidates=list(topic_rows), - topic_key=topic_key, - root_message_id=root_message_id, - thread_id=thread_id, - conversation_scope_key=scope_key or conversation_key, - ) + topic: Optional[MountedTopicWorkspace] + if force_new_topic: + normalized_topic_key = str(topic_key or "").strip() + topic = next( + ( + row + for row in topic_rows + if str(getattr(row, "topic_key", "") or "").strip() == normalized_topic_key + ), + None, + ) + else: + topic = self._select_requirement_plan_topic( + candidates=list(topic_rows), + topic_key=topic_key, + root_message_id=root_message_id, + thread_id=thread_id, + conversation_scope_key=scope_key or conversation_key, + ) if topic is None: topic = MountedTopicWorkspace( tenant_id=int(self.tenant_id), @@ -871,6 +1701,13 @@ class ExecuteQueryProjectMixin: topic.thread_key = thread_id or topic.thread_key topic.root_message_id = root_message_id or topic.root_message_id or str(source_message_id or "").strip() or None topic.last_message_id = str(source_message_id or "").strip() or topic.last_message_id + if force_new_topic: + await self._suspend_other_requirement_plan_topics( + source_system=source_system, + conversation_scope_key=scope_key or conversation_key, + keep_topic_id=int(topic.id), + source_message_id=source_message_id, + ) draft = ( await self.db.execute( @@ -984,9 +1821,10 @@ class ExecuteQueryProjectMixin: missing_field=self._requirement_plan_missing_fields(card)[0] if self._requirement_plan_missing_fields(card) else "first_scope", profile=profile, message_text=message_text, + card=card, ) return self._build_direct_query_result( - answer=f"可以,我们先不创建项目,先只聊需求。{next_question}", + answer=f"可以,我们先不创建项目,先把这件事收成一版需求。\n\n{next_question}", scene_label="Mounted Runtime Plan", reason_code="requirement_planning_direct_fallback", query_type="plan", @@ -1055,40 +1893,50 @@ class ExecuteQueryProjectMixin: topic.readiness_score = 1.0 if should_emit_draft else readiness if should_emit_draft: answer = self._build_requirement_plan_draft_text(card=card) + project_creation_draft = self._build_requirement_plan_project_creation_draft( + card=card, + draft_text=answer, + ) draft.state = "ready" draft.draft_version = int(draft.draft_version or 0) + 1 draft.requirement_card = card - draft.draft_payload = { - "plan_profile": profile, - "title": card.get("title"), - "goal": card.get("goal"), - "source_material": card.get("source_material"), - "target_user": card.get("target_user"), - "scoring_basis": card.get("scoring_basis"), - "output_form": card.get("output_form"), - "cadence": card.get("cadence"), - "first_scope": card.get("first_scope"), - "success_signal": card.get("success_signal"), - "draft_text": answer, - } + draft.draft_payload = project_creation_draft + draft.active_work_item_id = None + draft.latest_source_message_id = str(source_message_id or "").strip()[:128] or None + draft.latest_reply_message_id = None + draft.last_ready_at = now_cn_naive() topic.pending_actions = ["confirm_requirement_draft"] topic.open_questions = [] topic.readiness_score = 1.0 await self.db.flush() - return self._build_direct_query_result( + result = self._build_direct_query_result( answer=answer, scene_label="Mounted Runtime Draft", reason_code="requirement_planning_draft_ready", query_type="draft", ) + result["reply_template_key"] = "draft_pending_confirm" + result["draft_version"] = int(draft.draft_version or 0) + result["draft_artifact"] = { + "type": "requirement_plan_draft", + "topic_workspace_id": int(topic.id), + "draft_workspace_id": int(draft.id), + "draft_version": int(draft.draft_version or 0), + "promote_to_confirmation": True, + "workflow_route": ROUTE_PROJECT_CREATE, + "draft_payload": project_creation_draft, + "draft_text": answer, + } + return result next_question = self._build_requirement_plan_next_question( missing_field=missing_fields[0] if missing_fields else "first_scope", profile=profile, message_text=message_text, + card=card, ) ack = self._build_requirement_plan_ack(updates=updates, profile=profile) - answer = f"{ack}{next_question}" if ack else f"可以,我们先不创建项目,先只聊需求。{next_question}" + answer = f"{ack}\n\n{next_question}" if ack else f"可以,我们先不创建项目,先把这件事收成一版需求。\n\n{next_question}" draft.state = "collecting" draft.requirement_card = card draft.draft_payload = None @@ -1303,6 +2151,7 @@ class ExecuteQueryProjectMixin: message_intent = str(rag_request_context.get("message_intent") or "").strip() or None mixed_signal_part = str(rag_request_context.get("mixed_signal_part") or "").strip() or None consultant_mode_preference = str(rag_request_context.get("consultant_mode_preference") or "").strip().lower() or None + advise_subtype = str(rag_request_context.get("advise_subtype") or "").strip().lower() or None topic_switch = str(rag_request_context.get("topic_switch") or "").strip().lower() or None else: thread_context = metadata.get("thread_context") if isinstance(metadata.get("thread_context"), dict) else None @@ -1327,17 +2176,18 @@ class ExecuteQueryProjectMixin: ) session_id = None force_new_session = False - thinking_mode = str(semantic_diagnostics.get("force_thinking_mode") or "").strip() or None + thinking_mode = str(metadata.get("force_thinking_mode") or semantic_diagnostics.get("force_thinking_mode") or "").strip() or None action_mode = "read" sync_requirement = False message_intent = str( - semantic_diagnostics.get("message_intent") or metadata.get("message_intent") or "" + metadata.get("message_intent") or semantic_diagnostics.get("message_intent") or "" ).strip() or None mixed_signal_part = None consultant_mode_preference = str( - semantic_diagnostics.get("consultant_mode_preference") or metadata.get("consultant_mode_preference") or "" + metadata.get("consultant_mode_preference") or semantic_diagnostics.get("consultant_mode_preference") or "" ).strip().lower() or None - topic_switch = str(semantic_diagnostics.get("topic_switch") or metadata.get("topic_switch") or "").strip().lower() or None + advise_subtype = str(metadata.get("advise_subtype") or semantic_diagnostics.get("advise_subtype") or "").strip().lower() or None + topic_switch = str(metadata.get("topic_switch") or semantic_diagnostics.get("topic_switch") or "").strip().lower() or None if isinstance(rag_request_context, dict): source_sender_open_id = str(rag_request_context.get("source_sender_open_id") or "").strip() or None thread_context = rag_request_context.get("thread_context") if isinstance(rag_request_context.get("thread_context"), dict) else None @@ -1350,26 +2200,65 @@ class ExecuteQueryProjectMixin: ).strip() or None if not message_intent: message_intent = str( - semantic_diagnostics.get("message_intent") or metadata.get("message_intent") or "" + metadata.get("message_intent") or semantic_diagnostics.get("message_intent") or "" ).strip() or None if not consultant_mode_preference: consultant_mode_preference = str( - semantic_diagnostics.get("consultant_mode_preference") or metadata.get("consultant_mode_preference") or "" + metadata.get("consultant_mode_preference") or semantic_diagnostics.get("consultant_mode_preference") or "" + ).strip().lower() or None + if not advise_subtype: + advise_subtype = str( + rag_request_context.get("advise_subtype") + or metadata.get("advise_subtype") + or semantic_diagnostics.get("advise_subtype") + or "" ).strip().lower() or None if not topic_switch: topic_switch = str( rag_request_context.get("topic_switch") - or semantic_diagnostics.get("topic_switch") or metadata.get("topic_switch") + or semantic_diagnostics.get("topic_switch") or "" ).strip().lower() or None if not thinking_mode: thinking_mode = str( rag_request_context.get("thinking_mode") + or metadata.get("force_thinking_mode") or semantic_diagnostics.get("force_thinking_mode") or "" ).strip() or None - if not topic_switch and WorkflowSemanticPlanner.looks_like_explicit_new_requirement_topic(message_text): + explicit_new_requirement_topic = WorkflowSemanticPlanner.looks_like_explicit_new_requirement_topic(message_text) + if explicit_new_requirement_topic: + # "新需求 / 另一个需求" is a stronger signal than any stale upstream classify label. + # Force the conversation into a fresh requirement-plan topic so follow-up A/B/C replies + # cannot accidentally continue an unrelated historical draft in the same chat. + topic_switch = "new" + message_intent = "requirement_plan" + consultant_mode_preference = "clarify" + advise_subtype = None + has_active_requirement_plan_workspace = False + active_requirement_plan_topic: Optional[MountedTopicWorkspace] = None + active_requirement_plan_draft: Optional[MountedDraftWorkspace] = None + if not message_intent: + active_requirement_plan_topic, active_requirement_plan_draft = await self._find_active_requirement_plan_workspace( + source_system=source_system, + source_message_id=source_message_id, + source_chat_id=source_chat_id, + sender=None, + source_sender_open_id=source_sender_open_id, + thread_context=thread_context, + conversation_key=conversation_key, + ) + has_active_requirement_plan_workspace = ( + active_requirement_plan_topic is not None and active_requirement_plan_draft is not None + ) + if has_active_requirement_plan_workspace and self._should_continue_requirement_plan_from_workspace( + message_text=message_text, + topic=active_requirement_plan_topic, + ): + message_intent = "requirement_plan" + consultant_mode_preference = consultant_mode_preference or "clarify" + if not topic_switch and explicit_new_requirement_topic: topic_switch = "new" message_intent = message_intent or "requirement_plan" consultant_mode_preference = consultant_mode_preference or "clarify" @@ -1386,17 +2275,25 @@ class ExecuteQueryProjectMixin: message_intent=message_intent, ) if not message_intent and self._looks_like_requirement_plan_continuation(message_text=message_text): - has_active_plan_workspace = await self._has_active_requirement_plan_workspace( - source_system=source_system, - source_message_id=source_message_id, - source_chat_id=source_chat_id, - sender=None, - source_sender_open_id=source_sender_open_id, - thread_context=thread_context, - conversation_key=conversation_key, - force_new_topic=str(topic_switch or "").strip().lower() == "new", - ) - if has_active_plan_workspace: + if not has_active_requirement_plan_workspace: + active_requirement_plan_topic, active_requirement_plan_draft = await self._find_active_requirement_plan_workspace( + source_system=source_system, + source_message_id=source_message_id, + source_chat_id=source_chat_id, + sender=None, + source_sender_open_id=source_sender_open_id, + thread_context=thread_context, + conversation_key=conversation_key, + ) + has_active_requirement_plan_workspace = ( + str(topic_switch or "").strip().lower() != "new" + and active_requirement_plan_topic is not None + and active_requirement_plan_draft is not None + ) + if has_active_requirement_plan_workspace and self._should_continue_requirement_plan_from_workspace( + message_text=message_text, + topic=active_requirement_plan_topic, + ): message_intent = "requirement_plan" consultant_mode_preference = consultant_mode_preference or "clarify" if self._should_use_lightweight_chat_answer( @@ -1428,6 +2325,7 @@ class ExecuteQueryProjectMixin: message_text=message_text, message_intent=message_intent, consultant_mode_preference=consultant_mode_preference, + advise_subtype=advise_subtype, ): direct_result = await self._build_requirement_planning_direct_result( message_text=message_text, @@ -1446,6 +2344,7 @@ class ExecuteQueryProjectMixin: message_text=message_text, message_intent=message_intent, consultant_mode_preference=consultant_mode_preference, + advise_subtype=advise_subtype, ): direct_result = self._build_direct_query_result( answer=await self._build_assignee_recommendation_answer( @@ -1462,6 +2361,7 @@ class ExecuteQueryProjectMixin: message_text=message_text, message_intent=message_intent, consultant_mode_preference=consultant_mode_preference, + advise_subtype=advise_subtype, ): direct_result = self._build_direct_query_result( answer=self._build_requirement_advise_answer(message_text=message_text), diff --git a/backend/app/services/ai_pmo/draft_confirm_handler.py b/backend/app/services/ai_pmo/draft_confirm_handler.py index a5faee93..1ef75ef7 100644 --- a/backend/app/services/ai_pmo/draft_confirm_handler.py +++ b/backend/app/services/ai_pmo/draft_confirm_handler.py @@ -12,8 +12,10 @@ for _name, _value in vars(_common).items(): globals()[_name] = _value from ..ai_steward_service import AIStewardService +from ..mounted_ai_runtime_write_service import MountedAIRuntimeWriteService from .workflow_semantic_planner import WorkflowSemanticPlanner from ..signal.draft_generator_service import DraftGeneratorService +from ...models import MountedDraftWorkspace, MountedTopicWorkspace from ...models.feishu_reply_outbox import FeishuReplyOutbox @@ -52,6 +54,7 @@ class DraftConfirmHandler: ) -> dict[str, Any]: action, revision_note = self.parse_action_from_text(message_text=message_text) explicit_work_item_id = self._extract_work_item_id(message_text) + looks_like_revision = WorkflowSemanticPlanner.looks_like_natural_draft_revision(message_text) target = await self._resolve_pending_draft( sender=sender, @@ -62,11 +65,11 @@ class DraftConfirmHandler: thread_context=thread_context, ) if target is None: - if explicit_work_item_id is not None: + if explicit_work_item_id is not None or action != self._ACTION_UNKNOWN or looks_like_revision: return { - "handled": False, - "status": "ignored", - "followup_reply": "", + "handled": True, + "status": "failed_safe", + "followup_reply": "当前没有可确认草稿。请先在当前会话重新生成最新草稿,再回复“确认”或“修改”。", "target_work_item_id": None, "target_state": None, "target_intent": None, @@ -646,6 +649,25 @@ class DraftConfirmHandler: explicit_work_item_id: Optional[int], thread_context: Optional[dict[str, Any]], ) -> Optional[tuple[WorkItem, IngestEvent]]: + if explicit_work_item_id is not None: + return await self._load_work_item_target_by_id(work_item_id=int(explicit_work_item_id)) + + active_mounted = await self._resolve_active_mounted_draft( + sender=sender, + source_system=source_system, + channel_ref=channel_ref, + source_sender_open_id=source_sender_open_id, + thread_context=thread_context, + ) + if active_mounted is not None: + mounted_draft, _ = active_mounted + active_work_item_id = _safe_int(getattr(mounted_draft, "active_work_item_id", None)) + if active_work_item_id is not None: + target = await self._load_work_item_target_by_id(work_item_id=int(active_work_item_id)) + if target is not None: + return target + return None + stmt = ( select(WorkItem, IngestEvent) .join(IngestEvent, IngestEvent.id == WorkItem.ingest_event_id) @@ -659,11 +681,6 @@ class DraftConfirmHandler: .limit(30) ) rows = (await self.db.execute(stmt)).all() - if explicit_work_item_id is not None: - for work_item, ingest in rows: - if int(work_item.id) == int(explicit_work_item_id): - return work_item, ingest - return None normalized_sender = str(sender or "").strip() normalized_channel = str(channel_ref or "").strip() @@ -738,6 +755,143 @@ class DraftConfirmHandler: return None return candidates[0] + async def has_active_draft_session( + self, + *, + sender: str, + source_system: str, + channel_ref: Optional[str], + source_sender_open_id: Optional[str], + thread_context: Optional[dict[str, Any]], + ) -> bool: + if not hasattr(self.db, "execute"): + return False + mounted = await self._resolve_active_mounted_draft( + sender=sender, + source_system=source_system, + channel_ref=channel_ref, + source_sender_open_id=source_sender_open_id, + thread_context=thread_context, + ) + return mounted is not None + + async def _load_work_item_target_by_id( + self, + *, + work_item_id: int, + ) -> Optional[tuple[WorkItem, IngestEvent]]: + row = ( + await self.db.execute( + select(WorkItem, IngestEvent) + .join(IngestEvent, IngestEvent.id == WorkItem.ingest_event_id) + .where( + and_( + WorkItem.tenant_id == int(self.tenant_id), + WorkItem.id == int(work_item_id), + ) + ) + .limit(1) + ) + ).first() + if row is None: + return None + return row + + async def _resolve_active_mounted_draft( + self, + *, + sender: str, + source_system: str, + channel_ref: Optional[str], + source_sender_open_id: Optional[str], + thread_context: Optional[dict[str, Any]], + ) -> Optional[tuple[MountedDraftWorkspace, MountedTopicWorkspace]]: + normalized_source = str(source_system or "").strip().lower() + if not normalized_source: + return None + conversation_scope_key = MountedAIRuntimeWriteService.build_conversation_scope_key( + source_system=normalized_source, + channel_ref=channel_ref, + sender=sender, + source_sender_open_id=source_sender_open_id, + ) + anchor_context = self._normalize_thread_context(thread_context) + + rows = ( + await self.db.execute( + select(MountedDraftWorkspace, MountedTopicWorkspace) + .join( + MountedTopicWorkspace, + MountedTopicWorkspace.id == MountedDraftWorkspace.topic_workspace_id, + ) + .where( + and_( + MountedDraftWorkspace.tenant_id == int(self.tenant_id), + MountedTopicWorkspace.tenant_id == int(self.tenant_id), + MountedTopicWorkspace.source_system == normalized_source, + MountedTopicWorkspace.mode == "plan", + MountedTopicWorkspace.status == "active", + MountedDraftWorkspace.state.in_(("ready", STATE_DRAFT_PENDING_CONFIRM)), + ) + ) + .order_by(MountedTopicWorkspace.updated_at.desc(), MountedDraftWorkspace.id.desc()) + ) + ).all() + if not rows: + return None + + normalized_channel = str(channel_ref or "").strip() + best: Optional[tuple[MountedDraftWorkspace, MountedTopicWorkspace]] = None + best_score = -1 + for draft_row, topic_row in rows: + if not hasattr(draft_row, "topic_workspace_id") or not hasattr(topic_row, "mode"): + continue + score = 0 + if normalized_channel and str(getattr(topic_row, "channel_ref", "") or "").strip() == normalized_channel: + score += 30 + if conversation_scope_key and str(getattr(topic_row, "conversation_scope_key", "") or "").strip() == str(conversation_scope_key).strip(): + score += 60 + if anchor_context and self._matches_mounted_draft_anchor( + draft=draft_row, + topic=topic_row, + anchor_context=anchor_context, + ): + score += 120 + if str(getattr(topic_row, "plan_status", "") or "").strip().lower() in { + STATE_DRAFT_PENDING_CONFIRM, + "awaiting_execute_confirm", + "draft_ready", + }: + score += 20 + if _safe_int(getattr(draft_row, "active_work_item_id", None)) is not None: + score += 10 + if score > best_score: + best = (draft_row, topic_row) + best_score = score + if best_score <= 0: + return None + return best + + @classmethod + def _matches_mounted_draft_anchor( + cls, + *, + draft: MountedDraftWorkspace, + topic: MountedTopicWorkspace, + anchor_context: dict[str, str], + ) -> bool: + if not anchor_context: + return False + candidate_values = { + str(getattr(draft, "latest_reply_message_id", "") or "").strip(), + str(getattr(draft, "latest_source_message_id", "") or "").strip(), + str(getattr(topic, "root_message_id", "") or "").strip(), + str(getattr(topic, "thread_key", "") or "").strip(), + str(getattr(topic, "last_message_id", "") or "").strip(), + } + candidate_values.discard("") + return any(str(value or "").strip() in candidate_values for value in anchor_context.values()) + @staticmethod def _normalize_thread_context( thread_context: Optional[dict[str, Any]], diff --git a/backend/app/services/ai_pmo/semantic_context/__init__.py b/backend/app/services/ai_pmo/semantic_context/__init__.py new file mode 100644 index 00000000..26172fcf --- /dev/null +++ b/backend/app/services/ai_pmo/semantic_context/__init__.py @@ -0,0 +1,25 @@ +from .assembler import SemanticContextAssembler +from .decision_schema import DecisionGateResult, ModeCard, SemanticDecision +from .gatekeeper import SemanticDecisionGatekeeper +from .mode_card_service import build_mode_card +from .packet_schema import ( + SemanticContextDraftSnapshot, + SemanticContextPacket, + SemanticContextThread, + SemanticContextTopic, + SemanticContextTurn, +) + +__all__ = [ + "DecisionGateResult", + "ModeCard", + "SemanticContextAssembler", + "SemanticContextDraftSnapshot", + "SemanticContextPacket", + "SemanticContextThread", + "SemanticContextTopic", + "SemanticContextTurn", + "SemanticDecision", + "SemanticDecisionGatekeeper", + "build_mode_card", +] diff --git a/backend/app/services/ai_pmo/semantic_context/assembler.py b/backend/app/services/ai_pmo/semantic_context/assembler.py new file mode 100644 index 00000000..cd3c251a --- /dev/null +++ b/backend/app/services/ai_pmo/semantic_context/assembler.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +from typing import Any, Optional + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from ....models import MountedConversationRun, MountedDraftWorkspace, MountedTopicWorkspace +from .mode_card_service import build_mode_card +from .packet_schema import ( + SemanticContextDraftSnapshot, + SemanticContextPacket, + SemanticContextThread, + SemanticContextTopic, + SemanticContextTurn, +) + + +class SemanticContextAssembler: + def __init__(self, db: AsyncSession, *, recent_turn_limit: int = 6): + self.db = db + self.recent_turn_limit = max(int(recent_turn_limit), 2) + + @staticmethod + def _normalize_list(value: Any) -> list[str]: + if isinstance(value, list): + return [str(item).strip() for item in value if str(item or "").strip()] + return [] + + @staticmethod + def _normalize_mapping(value: Any) -> Optional[Any]: + if isinstance(value, (dict, list)): + return value + return None + + @staticmethod + def _extract_source_preview(row: MountedConversationRun) -> Optional[str]: + execution_outcome = row.execution_outcome if isinstance(row.execution_outcome, dict) else {} + preview = str(execution_outcome.get("source_message_preview") or "").strip() + return preview or None + + async def _load_topic( + self, + *, + tenant_id: int, + topic_workspace_id: int, + ) -> MountedTopicWorkspace: + topic_row = ( + await self.db.execute( + select(MountedTopicWorkspace).where( + MountedTopicWorkspace.tenant_id == int(tenant_id), + MountedTopicWorkspace.id == int(topic_workspace_id), + ) + ) + ).scalar_one_or_none() + if topic_row is None: + raise ValueError("mounted topic workspace not found") + return topic_row + + async def _load_draft( + self, + *, + tenant_id: int, + topic_workspace_id: int, + ) -> MountedDraftWorkspace | None: + return ( + await self.db.execute( + select(MountedDraftWorkspace).where( + MountedDraftWorkspace.tenant_id == int(tenant_id), + MountedDraftWorkspace.topic_workspace_id == int(topic_workspace_id), + ) + ) + ).scalar_one_or_none() + + async def _load_recent_turns( + self, + *, + tenant_id: int, + topic_workspace_id: int, + ) -> list[SemanticContextTurn]: + run_rows = ( + await self.db.execute( + select(MountedConversationRun) + .where( + MountedConversationRun.tenant_id == int(tenant_id), + MountedConversationRun.topic_workspace_id == int(topic_workspace_id), + MountedConversationRun.status == "completed", + ) + .order_by(MountedConversationRun.updated_at.desc(), MountedConversationRun.id.desc()) + .limit(max(self.recent_turn_limit, 1)) + ) + ).scalars().all() + turns: list[SemanticContextTurn] = [] + for row in reversed(run_rows): + source_preview = self._extract_source_preview(row) + if source_preview: + turns.append(SemanticContextTurn(role="user", text=source_preview)) + final_reply = str(row.final_reply or "").strip() + if final_reply: + turns.append(SemanticContextTurn(role="assistant", text=final_reply)) + if len(turns) <= self.recent_turn_limit: + return turns + return turns[-self.recent_turn_limit :] + + async def assemble( + self, + *, + tenant_id: int, + operator_user_id: int, + topic_workspace_id: int, + latest_user_message: str, + ) -> SemanticContextPacket: + topic_row = await self._load_topic( + tenant_id=tenant_id, + topic_workspace_id=topic_workspace_id, + ) + draft_row = await self._load_draft( + tenant_id=tenant_id, + topic_workspace_id=topic_workspace_id, + ) + recent_turns = await self._load_recent_turns( + tenant_id=tenant_id, + topic_workspace_id=topic_workspace_id, + ) + + topic = SemanticContextTopic( + topic_workspace_id=int(topic_row.id), + topic_key=topic_row.topic_key, + source_system=topic_row.source_system, + channel_ref=topic_row.channel_ref, + conversation_scope_key=topic_row.conversation_scope_key, + thread_key=topic_row.thread_key, + mode=topic_row.mode, + status=topic_row.status, + plan_status=topic_row.plan_status, + facts=self._normalize_mapping(topic_row.facts_json), + goals=self._normalize_mapping(topic_row.goals_json), + constraints=self._normalize_mapping(topic_row.constraints_json), + metadata=self._normalize_mapping(topic_row.metadata_json), + ) + draft = None + if draft_row is not None: + draft = SemanticContextDraftSnapshot( + draft_workspace_id=int(draft_row.id), + state=draft_row.state, + draft_version=int(draft_row.draft_version or 0), + requirement_card=self._normalize_mapping(draft_row.requirement_card), + draft_payload=self._normalize_mapping(draft_row.draft_payload), + required_confirmations=self._normalize_list(draft_row.required_confirmations), + ) + + metadata = topic_row.metadata_json if isinstance(topic_row.metadata_json, dict) else {} + thread = SemanticContextThread( + thread_key=topic_row.thread_key, + conversation_scope_key=topic_row.conversation_scope_key, + thread_summary=str( + metadata.get("last_message_preview") + or (topic_row.facts_json or {}).get("title") + or "" + ).strip() + or None, + recent_turns=recent_turns, + open_questions=self._normalize_list(topic_row.open_questions), + ) + return SemanticContextPacket( + packet_version="v1", + tenant_id=int(tenant_id), + operator_user_id=int(operator_user_id), + topic_workspace_id=int(topic_row.id), + latest_user_message=str(latest_user_message or "").strip(), + topic=topic, + draft=draft, + thread=thread, + mode_card=build_mode_card( + topic_mode=topic_row.mode, + plan_status=topic_row.plan_status, + ), + ) diff --git a/backend/app/services/ai_pmo/semantic_context/decision_schema.py b/backend/app/services/ai_pmo/semantic_context/decision_schema.py new file mode 100644 index 00000000..8b4124e5 --- /dev/null +++ b/backend/app/services/ai_pmo/semantic_context/decision_schema.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict, Field + + +class ModeCard(BaseModel): + model_config = ConfigDict(extra="ignore") + + workflow_mode: Literal["CHAT", "PLAN", "EXECUTE"] = "CHAT" + mode_state: str = "IDLE" + allowed_actions: list[str] = Field(default_factory=list) + forbidden_actions: list[str] = Field(default_factory=list) + exit_conditions: list[str] = Field(default_factory=list) + + +class SemanticDecision(BaseModel): + model_config = ConfigDict(extra="ignore") + + topic_relation: Literal["continue_current_topic", "new_topic", "uncertain"] + mode_decision: Literal["stay_plan", "ready_confirm", "request_execute", "answer_only"] + draft_patch: dict[str, Any] = Field(default_factory=dict) + assistant_reply: str = "" + requires_confirmation: bool = False + requested_actions: list[str] = Field(default_factory=list) + confidence: float = 0.0 + uncertainties: list[str] = Field(default_factory=list) + + +class DecisionGateResult(BaseModel): + model_config = ConfigDict(extra="ignore") + + accepted: bool + blocked_reasons: list[str] = Field(default_factory=list) + allowed_actions: list[str] = Field(default_factory=list) + next_plan_status: str + needs_confirmation: bool = False + mode_state_after: str | None = None diff --git a/backend/app/services/ai_pmo/semantic_context/gatekeeper.py b/backend/app/services/ai_pmo/semantic_context/gatekeeper.py new file mode 100644 index 00000000..ea103d28 --- /dev/null +++ b/backend/app/services/ai_pmo/semantic_context/gatekeeper.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from .decision_schema import DecisionGateResult, ModeCard, SemanticDecision + + +class SemanticDecisionGatekeeper: + @staticmethod + def _next_plan_status(*, accepted: bool, decision: SemanticDecision) -> str: + if not accepted: + return "active" + if decision.mode_decision == "ready_confirm": + return "awaiting_execute_confirm" + if decision.mode_decision == "request_execute": + return "execution_pending" + if decision.mode_decision == "answer_only": + return "idle" + return "active" + + @classmethod + def evaluate( + cls, + *, + mode_card: ModeCard, + decision: SemanticDecision, + ) -> DecisionGateResult: + blocked_reasons: list[str] = [] + allowed_actions: list[str] = [] + allowed_set = {str(item).strip() for item in mode_card.allowed_actions if str(item or "").strip()} + forbidden_set = {str(item).strip() for item in mode_card.forbidden_actions if str(item or "").strip()} + + for action in decision.requested_actions: + normalized_action = str(action or "").strip() + if not normalized_action: + continue + if normalized_action in forbidden_set: + blocked_reasons.append(f"forbidden_action:{normalized_action}") + continue + if allowed_set and normalized_action not in allowed_set: + blocked_reasons.append(f"unapproved_action:{normalized_action}") + continue + allowed_actions.append(normalized_action) + + accepted = len(blocked_reasons) == 0 + return DecisionGateResult( + accepted=accepted, + blocked_reasons=blocked_reasons, + allowed_actions=allowed_actions, + next_plan_status=cls._next_plan_status(accepted=accepted, decision=decision), + needs_confirmation=bool(decision.requires_confirmation), + mode_state_after=decision.mode_decision, + ) diff --git a/backend/app/services/ai_pmo/semantic_context/mode_card_service.py b/backend/app/services/ai_pmo/semantic_context/mode_card_service.py new file mode 100644 index 00000000..48fa72ba --- /dev/null +++ b/backend/app/services/ai_pmo/semantic_context/mode_card_service.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from .decision_schema import ModeCard + + +def build_mode_card(*, topic_mode: str | None, plan_status: str | None) -> ModeCard: + normalized_mode = str(topic_mode or "chat").strip().lower() + normalized_plan_status = str(plan_status or "idle").strip().lower() + workflow_mode = { + "plan": "PLAN", + "execute": "EXECUTE", + }.get(normalized_mode, "CHAT") + + if workflow_mode == "PLAN": + return ModeCard( + workflow_mode="PLAN", + mode_state=normalized_plan_status.upper() or "ACTIVE", + allowed_actions=[ + "reply.compose", + "draft.patch", + "clarify.ask", + "topic.continue_or_split", + "plan.mark_ready_for_confirmation", + ], + forbidden_actions=[ + "project.create", + "task.create", + "signal_source.onboard", + "db.write", + ], + exit_conditions=[ + "用户明确要求开始执行", + "草稿完成且需要确认项已补齐", + ], + ) + + if workflow_mode == "EXECUTE": + return ModeCard( + workflow_mode="EXECUTE", + mode_state=normalized_plan_status.upper() or "ACTIVE", + allowed_actions=[ + "reply.compose", + "project.create", + "task.create", + "followup.execute", + ], + forbidden_actions=["db.write"], + exit_conditions=["执行完成或执行失败后返回结果"], + ) + + return ModeCard( + workflow_mode="CHAT", + mode_state=normalized_plan_status.upper() or "IDLE", + allowed_actions=["reply.compose", "clarify.ask"], + forbidden_actions=["project.create", "task.create", "db.write"], + exit_conditions=["用户提出新的规划或执行需求"], + ) diff --git a/backend/app/services/ai_pmo/semantic_context/packet_schema.py b/backend/app/services/ai_pmo/semantic_context/packet_schema.py new file mode 100644 index 00000000..e5b04691 --- /dev/null +++ b/backend/app/services/ai_pmo/semantic_context/packet_schema.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from typing import Any, Optional + +from pydantic import BaseModel, ConfigDict, Field + +from .decision_schema import ModeCard + + +class SemanticContextTurn(BaseModel): + model_config = ConfigDict(extra="ignore") + + role: str + text: str + + +class SemanticContextThread(BaseModel): + model_config = ConfigDict(extra="ignore") + + thread_key: Optional[str] = None + conversation_scope_key: Optional[str] = None + thread_summary: Optional[str] = None + recent_turns: list[SemanticContextTurn] = Field(default_factory=list) + open_questions: list[str] = Field(default_factory=list) + + +class SemanticContextTopic(BaseModel): + model_config = ConfigDict(extra="ignore") + + topic_workspace_id: int + topic_key: str + source_system: str + channel_ref: Optional[str] = None + conversation_scope_key: Optional[str] = None + thread_key: Optional[str] = None + mode: str + status: str + plan_status: Optional[str] = None + facts: Optional[Any] = None + goals: Optional[Any] = None + constraints: Optional[Any] = None + metadata: Optional[Any] = None + + +class SemanticContextDraftSnapshot(BaseModel): + model_config = ConfigDict(extra="ignore") + + draft_workspace_id: int + state: str + draft_version: int + requirement_card: Optional[Any] = None + draft_payload: Optional[Any] = None + required_confirmations: list[str] = Field(default_factory=list) + + +class SemanticContextPacket(BaseModel): + model_config = ConfigDict(extra="ignore") + + packet_version: str = "v1" + tenant_id: int + operator_user_id: int + topic_workspace_id: int + latest_user_message: str + topic: SemanticContextTopic + draft: SemanticContextDraftSnapshot | None = None + thread: SemanticContextThread + mode_card: ModeCard diff --git a/backend/app/services/ai_pmo/semantic_context/semantic_decision_service.py b/backend/app/services/ai_pmo/semantic_context/semantic_decision_service.py new file mode 100644 index 00000000..0b575939 --- /dev/null +++ b/backend/app/services/ai_pmo/semantic_context/semantic_decision_service.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import json +from typing import Any + +from ....config.ai_prompts import PMFLOW_SEMANTIC_CONTEXT_DECISION_PROMPT +from ....services.llm_service.service import LLMService +from .decision_schema import SemanticDecision +from .packet_schema import SemanticContextPacket + + +class SemanticDecisionService: + FUNCTION_NAME = "pmflow_semantic_context_decision" + + def __init__(self, llm_service: LLMService): + self.llm_service = llm_service + + @staticmethod + def build_prompt(packet: SemanticContextPacket) -> str: + return PMFLOW_SEMANTIC_CONTEXT_DECISION_PROMPT.format( + semantic_packet=json.dumps(packet.model_dump(mode="json"), ensure_ascii=False, indent=2) + ) + + async def decide(self, *, packet: SemanticContextPacket) -> SemanticDecision: + structured = await self.llm_service.generate_structured_object( + prompt=self.build_prompt(packet), + function_name=self.FUNCTION_NAME, + use_prompt_profile=False, + temperature=0.0, + response_mode="fast", + ) + payload: Any = getattr(structured, "payload", None) + if payload is None and hasattr(structured, "model_dump"): + payload = structured.model_dump() + if payload is None: + payload = structured + if not isinstance(payload, dict): + raise ValueError("semantic decision payload is not a dict") + return SemanticDecision.model_validate(payload) diff --git a/backend/app/services/ai_pmo/workflow_semantic_planner/service.py b/backend/app/services/ai_pmo/workflow_semantic_planner/service.py index 052c7604..c6a369fe 100644 --- a/backend/app/services/ai_pmo/workflow_semantic_planner/service.py +++ b/backend/app/services/ai_pmo/workflow_semantic_planner/service.py @@ -263,6 +263,28 @@ class WorkflowSemanticPlanner: return True return False + @classmethod + def looks_like_requirement_plan_choice_reply(cls, text: str) -> bool: + return bool(re.match(r"^\s*([ABCabc])(?:[\.\,,。、::)\]】\s]|$)", str(text or ""))) + + @classmethod + def looks_like_low_context_requirement_followup(cls, text: str) -> bool: + raw = str(text or "").strip() + if not raw: + return False + if cls._looks_like_casual_chat_message(raw): + return False + if cls.looks_like_explicit_new_requirement_topic(raw): + return False + if cls._has_question_structure(raw) or cls.is_obvious_question(raw): + return False + if cls.looks_like_requirement_plan_choice_reply(raw): + return True + normalized = cls._normalize_text(raw) + if normalized in DRAFT_CONFIRM_EXACT_HINTS: + return True + return normalized in {"你自行推荐", "你来推荐", "你来定", "你决定"} + @classmethod def _looks_like_requirement_plan_slot_answer(cls, text: str) -> bool: normalized = cls._normalize_text(text) diff --git a/backend/app/services/context_cache_service/mixin_cache_core.py b/backend/app/services/context_cache_service/mixin_cache_core.py index ab98e321..1ac29c61 100644 --- a/backend/app/services/context_cache_service/mixin_cache_core.py +++ b/backend/app/services/context_cache_service/mixin_cache_core.py @@ -75,10 +75,27 @@ class ContextCacheCoreMixin: pass @staticmethod - def _is_system_snapshot_complete(payload: Optional[dict[str, Any]]) -> bool: + def _expected_system_snapshot_source_scan() -> dict[str, str]: + backend_root = Path(__file__).resolve().parents[3] + workspace_root = backend_root.parent + app_root = backend_root / "app" + return { + "backend_root": str(backend_root), + "app_root": str(app_root), + "workspace_root": str(workspace_root), + } + + def _is_system_snapshot_complete(self, payload: Optional[dict[str, Any]]) -> bool: if not isinstance(payload, dict): return False - return isinstance(payload.get("assistant_profile"), dict) and isinstance(payload.get("platform_profile"), dict) + if str(payload.get("schema_version") or "").strip() != SYSTEM_SNAPSHOT_SCHEMA_VERSION: + return False + if not isinstance(payload.get("assistant_profile"), dict) or not isinstance(payload.get("platform_profile"), dict): + return False + platform_profile = payload.get("platform_profile") if isinstance(payload.get("platform_profile"), dict) else {} + source_scan = platform_profile.get("source_scan") if isinstance(platform_profile.get("source_scan"), dict) else {} + expected = self._expected_system_snapshot_source_scan() + return all(str(source_scan.get(key) or "").strip() == value for key, value in expected.items()) async def get_system_snapshot(self, *, db: AsyncSession, tenant_id: int) -> dict[str, Any]: tid = _safe_int(tenant_id, 1) @@ -123,6 +140,7 @@ class ContextCacheCoreMixin: platform_profile = self._build_platform_profile() return { "tenant_id": tenant_id, + "schema_version": SYSTEM_SNAPSHOT_SCHEMA_VERSION, "assistant_profile": assistant_profile, "platform_profile": platform_profile, "projects": projects.get("items", []), @@ -130,4 +148,3 @@ class ContextCacheCoreMixin: "pending_signals": pending, "snapshot_at": now_cn_aware().isoformat(), } - diff --git a/backend/app/services/context_cache_service/mixin_profile_route_parser.py b/backend/app/services/context_cache_service/mixin_profile_route_parser.py index 04193a50..f4e2ef77 100644 --- a/backend/app/services/context_cache_service/mixin_profile_route_parser.py +++ b/backend/app/services/context_cache_service/mixin_profile_route_parser.py @@ -190,7 +190,7 @@ class ContextCacheProfileRouteParserMixin: } def _build_platform_profile(self) -> dict[str, Any]: - backend_root = Path(__file__).resolve().parents[2] + backend_root = Path(__file__).resolve().parents[3] workspace_root = backend_root.parent app_root = backend_root / "app" api_dir = app_root / "api" @@ -246,6 +246,8 @@ class ContextCacheProfileRouteParserMixin: "feature_catalog": feature_catalog.get("items") if isinstance(feature_catalog.get("items"), list) else [], "source_scan": { "backend_root": str(backend_root), + "app_root": str(app_root), + "workspace_root": str(workspace_root), "app_python_files": app_python_files, "feature_catalog_source": str(feature_catalog.get("source") or "unknown"), "scanned_at": now_cn_aware().isoformat(), diff --git a/backend/app/services/context_cache_service/shared.py b/backend/app/services/context_cache_service/shared.py index 394c734a..5652dd84 100644 --- a/backend/app/services/context_cache_service/shared.py +++ b/backend/app/services/context_cache_service/shared.py @@ -27,6 +27,7 @@ except Exception: # pragma: no cover RedisSync = None +SYSTEM_SNAPSHOT_SCHEMA_VERSION = "2026-04-07.context-v2" SYSTEM_TTL_SECONDS = 30 * 60 USER_TTL_SECONDS = 5 * 60 SESSION_TTL_SECONDS = 60 * 60 diff --git a/backend/app/services/integrations/feishu_integration_service/mixin_reply_outbox.py b/backend/app/services/integrations/feishu_integration_service/mixin_reply_outbox.py index a3a3517c..891b1d5a 100644 --- a/backend/app/services/integrations/feishu_integration_service/mixin_reply_outbox.py +++ b/backend/app/services/integrations/feishu_integration_service/mixin_reply_outbox.py @@ -328,6 +328,22 @@ class FeishuReplyOutboxMixin: row.last_error = None if sent_message_id: row.sent_message_id = sent_message_id[:128] + if str(row.source_message_id or "").strip(): + from app.models import MountedDraftWorkspace + + mounted_rows = ( + await db_update.execute( + select(MountedDraftWorkspace).where( + and_( + MountedDraftWorkspace.tenant_id == int(row.tenant_id), + MountedDraftWorkspace.latest_source_message_id == str(row.source_message_id).strip()[:128], + MountedDraftWorkspace.state.in_(("ready", "draft_pending_confirm")), + ) + ) + ) + ).scalars().all() + for mounted_row in mounted_rows: + mounted_row.latest_reply_message_id = sent_message_id[:128] await db_update.commit() perf_t2 = time.perf_counter() perf_t0 = _OUTBOX_PERF_T0_BY_ID.pop(outbox_id, None) @@ -357,11 +373,67 @@ class FeishuReplyOutboxMixin: @classmethod def _normalize_outgoing_reply_text(cls, value: Optional[str]) -> str: - text = cls._clean_reply_text(value) - if (not text) or cls._looks_like_provider_error(text): - return cls._build_error_reply_text() + cleaner = getattr(cls, "_clean_reply_text", None) + if callable(cleaner): + text = cleaner(value) + else: + text = str(value or "").strip() + if text.startswith("```"): + text = text.replace("```", "").strip() + if text.lower().startswith("json"): + text = text[4:].strip() + looks_like_provider_error = getattr(cls, "_looks_like_provider_error", None) + has_provider_error = callable(looks_like_provider_error) and bool(looks_like_provider_error(text)) + if not text or has_provider_error: + error_builder = getattr(cls, "_build_error_reply_text", None) + if callable(error_builder): + return error_builder() + return "当前回复生成失败,请稍后重试。" + text = cls._beautify_outgoing_reply_text(text) return text[:800] + @staticmethod + def _beautify_outgoing_reply_text(value: Optional[str]) -> str: + raw = str(value or "").strip() + if not raw: + return "" + + label_map = { + "结论:": "🎯 结论:", + "依据:": "🔎 依据:", + "下一步:": "➡️ 下一步:", + "信息不足,缺少:": "🧩 信息不足,缺少:", + "当前我更推荐:": "⭐ 当前我更推荐:", + } + section_prefixes = tuple(label_map.values()) + ("🧭 第 ",) + choice_prefixes = ("A.", "B.", "C.") + tail_prefixes = ("你回 A / B / C",) + + beautified: list[str] = [] + for raw_line in raw.splitlines(): + stripped = str(raw_line).strip() + if not stripped: + if beautified and beautified[-1] != "": + beautified.append("") + continue + for plain, pretty in label_map.items(): + if stripped.startswith(plain): + stripped = f"{pretty}{stripped[len(plain):]}" + break + needs_leading_gap = ( + any(stripped.startswith(prefix) for prefix in section_prefixes) + or stripped in choice_prefixes + or any(stripped.startswith(prefix) for prefix in choice_prefixes) + or any(stripped.startswith(prefix) for prefix in tail_prefixes) + ) + if needs_leading_gap and beautified and beautified[-1] != "": + beautified.append("") + beautified.append(stripped) + + text = "\n".join(beautified).strip() + text = re.sub(r"\n{3,}", "\n\n", text) + return text + @staticmethod def _build_reply_hash(*, source_message_id: Optional[str], message_text: str) -> Optional[str]: source = str(source_message_id or "").strip() diff --git a/backend/app/services/llm_service/mixin_engine_resolution_core.py b/backend/app/services/llm_service/mixin_engine_resolution_core.py index 9ac36509..f7d1a40b 100644 --- a/backend/app/services/llm_service/mixin_engine_resolution_core.py +++ b/backend/app/services/llm_service/mixin_engine_resolution_core.py @@ -5,7 +5,6 @@ from .shared import * class LLMEngineResolutionCoreMixin: _PROTECTED_QUERY_HIGH_FUNCTIONS = frozenset({ "adaptive_rag_answer_deep", - "adaptive_rag_answer_normal", }) def _stub_response(self, prompt: str) -> str: diff --git a/backend/app/services/llm_service/shared_part_01.py b/backend/app/services/llm_service/shared_part_01.py index 5420c574..b5e401eb 100644 --- a/backend/app/services/llm_service/shared_part_01.py +++ b/backend/app/services/llm_service/shared_part_01.py @@ -265,6 +265,29 @@ STRUCTURED_OBJECT_SCHEMAS: dict[str, dict[str, Any]] = { "requires_confirmation": ("bool",), }, }, + "pmflow_semantic_context_decision": { + "required_keys": ( + "topic_relation", + "mode_decision", + "draft_patch", + "assistant_reply", + "requires_confirmation", + "requested_actions", + "confidence", + ), + "type_checks": { + "topic_relation": "str", + "mode_decision": "str", + "draft_patch": "dict", + "assistant_reply": "str", + "requires_confirmation": "bool", + "requested_actions": "list", + "confidence": "number", + }, + "optional_type_checks": { + "uncertainties": ("list",), + }, + }, "signal_planner_analyze": { "required_keys": ("summary", "priority", "execution_plan", "risks", "confidence"), "type_checks": { diff --git a/backend/app/services/mounted_ai_runtime_service.py b/backend/app/services/mounted_ai_runtime_service.py index ca3bcf86..7175bb04 100644 --- a/backend/app/services/mounted_ai_runtime_service.py +++ b/backend/app/services/mounted_ai_runtime_service.py @@ -129,6 +129,19 @@ class MountedAIRuntimeService: "updated_at": self._dt(row.updated_at), } + def _draft_snapshot(self, row: MountedDraftWorkspace) -> dict[str, Any]: + return { + "id": int(row.id), + "topic_workspace_id": int(row.topic_workspace_id), + "state": row.state, + "draft_version": int(row.draft_version or 0), + "requirement_card": row.requirement_card, + "draft_payload": row.draft_payload, + "required_confirmations": row.required_confirmations, + "last_ready_at": self._dt(row.last_ready_at), + "confirm_message_id": row.confirm_message_id, + } + @staticmethod def _source_activation_snapshot(row: MountedSourceActivation) -> dict[str, Any]: return { @@ -547,13 +560,56 @@ class MountedAIRuntimeService: .order_by(MountedAgentTrace.id.asc()) ) ).scalars().all() + topic_row: MountedTopicWorkspace | None = None + draft_row: MountedDraftWorkspace | None = None + if row.topic_workspace_id is not None: + topic_row = ( + await self.db.execute( + select(MountedTopicWorkspace).where( + MountedTopicWorkspace.tenant_id == int(tenant_id), + MountedTopicWorkspace.id == int(row.topic_workspace_id), + ) + ) + ).scalar_one_or_none() + if topic_row is not None: + draft_row = ( + await self.db.execute( + select(MountedDraftWorkspace).where( + MountedDraftWorkspace.tenant_id == int(tenant_id), + MountedDraftWorkspace.topic_workspace_id == int(topic_row.id), + ) + ) + ).scalar_one_or_none() + execution_outcome = row.execution_outcome if isinstance(row.execution_outcome, dict) else row.execution_outcome return { "item": self._run_snapshot(row, trace_count=len(trace_rows)), + "topic": self._topic_snapshot(topic_row, draft=draft_row) if topic_row is not None else None, + "draft": self._draft_snapshot(draft_row) if draft_row is not None else None, "agent_traces": [self._trace_snapshot(trace) for trace in trace_rows], "context_layers_hit": row.context_layers_hit, "tool_calls": row.tool_calls, - "execution_outcome": row.execution_outcome, + "execution_outcome": execution_outcome, + "semantic_packet_summary": ( + execution_outcome.get("semantic_packet_summary") + if isinstance(execution_outcome, dict) + else None + ), + "semantic_decision": ( + execution_outcome.get("semantic_decision") + if isinstance(execution_outcome, dict) + else None + ), + "semantic_gate_result": ( + execution_outcome.get("semantic_gate_result") + if isinstance(execution_outcome, dict) + else None + ), + "semantic_diff": ( + execution_outcome.get("semantic_diff") + if isinstance(execution_outcome, dict) + else None + ), } async def replay_run( diff --git a/backend/app/services/mounted_ai_runtime_write_service.py b/backend/app/services/mounted_ai_runtime_write_service.py index b86b3744..4965746c 100644 --- a/backend/app/services/mounted_ai_runtime_write_service.py +++ b/backend/app/services/mounted_ai_runtime_write_service.py @@ -31,6 +31,15 @@ class MountedAIRuntimeWriteService: return None return text[:max_len] + @staticmethod + def _merge_json_dict(existing: Any, updates: dict[str, Any]) -> dict[str, Any]: + base = dict(existing) if isinstance(existing, dict) else {} + for key, value in updates.items(): + if value is None: + continue + base[key] = value + return base + @classmethod def build_topic_key( cls, @@ -415,6 +424,63 @@ class MountedAIRuntimeWriteService: ) await self.db.flush() + async def record_semantic_shadow( + self, + *, + tracking: MountedIngressTracking, + source_message_preview: Optional[str], + semantic_packet_summary: Optional[dict[str, Any]], + semantic_decision: Optional[dict[str, Any]], + semantic_gate_result: Optional[dict[str, Any]], + semantic_diff: Optional[dict[str, Any]], + status: str = "completed", + failure_reason: Optional[str] = None, + ) -> None: + topic_row = tracking.topic_workspace + run_row = tracking.conversation_run + normalized_status = self._compact(status, max_len=24) or "completed" + run_row.execution_outcome = self._merge_json_dict( + run_row.execution_outcome, + { + "source_message_preview": self._compact(source_message_preview, max_len=280), + "semantic_packet_summary": semantic_packet_summary if isinstance(semantic_packet_summary, dict) else None, + "semantic_decision": semantic_decision if isinstance(semantic_decision, dict) else None, + "semantic_gate_result": semantic_gate_result if isinstance(semantic_gate_result, dict) else None, + "semantic_diff": semantic_diff if isinstance(semantic_diff, dict) else None, + }, + ) + next_plan_status = None + if isinstance(semantic_gate_result, dict): + next_plan_status = self._compact(semantic_gate_result.get("next_plan_status"), max_len=32) + if next_plan_status: + topic_row.plan_status = next_plan_status + self.db.add( + MountedAgentTrace( + tenant_id=int(topic_row.tenant_id), + conversation_run_id=int(run_row.id), + agent_name="SemanticShadowPlanner", + status=normalized_status, + input_summary={ + "topic_workspace_id": int(topic_row.id), + "mode_before": run_row.mode_before, + }, + output_summary={ + "semantic_packet_summary": semantic_packet_summary if isinstance(semantic_packet_summary, dict) else None, + "semantic_decision": semantic_decision if isinstance(semantic_decision, dict) else None, + "semantic_gate_result": semantic_gate_result if isinstance(semantic_gate_result, dict) else None, + "semantic_diff": semantic_diff if isinstance(semantic_diff, dict) else None, + }, + tool_trace=[ + { + "tool_name": "semantic.shadow", + "status": "completed", + } + ], + failure_reason=self._compact(failure_reason, max_len=500), + ) + ) + await self.db.flush() + async def finalize_run( self, *, @@ -435,13 +501,16 @@ class MountedAIRuntimeWriteService: run_row.status = "completed" run_row.active_agent = "ReplyComposer" run_row.final_reply = final_reply or None - run_row.execution_outcome = { - "handled": bool((result or {}).get("handled")) if isinstance(result, dict) else False, - "status": str((result or {}).get("status") or "").strip() if isinstance(result, dict) else "", - "classification": str((result or {}).get("classification") or "").strip() if isinstance(result, dict) else "", - "workflow_route": str((result or {}).get("workflow_route") or "").strip() if isinstance(result, dict) else "", - "work_item_id": (result or {}).get("work_item_id") if isinstance(result, dict) else None, - } + run_row.execution_outcome = self._merge_json_dict( + run_row.execution_outcome, + { + "handled": bool((result or {}).get("handled")) if isinstance(result, dict) else False, + "status": str((result or {}).get("status") or "").strip() if isinstance(result, dict) else "", + "classification": str((result or {}).get("classification") or "").strip() if isinstance(result, dict) else "", + "workflow_route": str((result or {}).get("workflow_route") or "").strip() if isinstance(result, dict) else "", + "work_item_id": (result or {}).get("work_item_id") if isinstance(result, dict) else None, + }, + ) run_row.completed_at = now_at self.db.add( MountedAgentTrace( diff --git a/backend/static/工单全字段报表_2026-04-03.xlsx b/backend/static/工单全字段报表_2026-04-03.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f13911ab3eac6755a8d81d2419e91d09c7bca2c7 GIT binary patch literal 359645 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%0ARp`@=xQMSfULG3Z#KQz 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 BdB6Yw literal 0 HcmV?d00001 diff --git a/backend/tests/test_adaptive_rag_mainline_contract.py b/backend/tests/test_adaptive_rag_mainline_contract.py index 86ca6c18..e51f7978 100644 --- a/backend/tests/test_adaptive_rag_mainline_contract.py +++ b/backend/tests/test_adaptive_rag_mainline_contract.py @@ -429,6 +429,83 @@ async def test_deep_guard_answer_prefers_member_workload_direct_answer() -> None assert "严重超载" in result +@pytest.mark.asyncio +async def test_deep_guard_answer_feishu_direct_prefers_manifest_and_compact_focus_context() -> None: + class _FakeLLM: + def __init__(self) -> None: + self.prompt = "" + + async def generate(self, prompt: str, **kwargs): + _ = kwargs + self.prompt = prompt + return "信息不足,缺少:目标对象" + + class _FakeService(AdaptiveRAGDeepGuardAnswerMixin): + def __init__(self) -> None: + self.db = object() + self.tenant_id = 1 + self.llm = _FakeLLM() + + def _normalize_thinking_mode(self, thinking_mode: str) -> str: + return "normal" + + async def get_project_manifest(self, project_id): + _ = project_id + return { + "manifest_text": "项目#7 Alpha(进行中);功能 18 个;KG 实体 12,关系 19。" + } + + async def _render_scene_prompt(self, *, prompt_key: str, fallback_template: str, **kwargs): + _ = prompt_key + return str(fallback_template).format(**kwargs).strip() + + def _enforce_feature_evidence_in_answer(self, *, question: str, answer_text: str, retrieved, system_snapshot): + _ = (question, retrieved, system_snapshot) + return answer_text + + service = _FakeService() + system_snapshot = { + "assistant_profile": {"name": "PMFlow", "identity": "项目管理顾问"}, + "platform_profile": { + "backend": {"module_counts": {"projects": 28, "tasks": 146, "signals": 41}}, + "feature_catalog": [ + {"name": "项目管理", "route": "/projects"}, + {"name": "信号接入", "route": "/signals"}, + {"name": "任务看板", "route": "/board"}, + {"name": "知识图谱", "route": "/kg"}, + {"name": "训练中心", "route": "/training"}, + ], + }, + } + user_snapshot = { + "current_user": {"name": "林宇航"}, + "projects": [{"id": idx, "title": f"项目{idx}"} for idx in range(1, 15)], + "tasks": [{"id": idx, "title": f"任务{idx}"} for idx in range(1, 10)], + } + + result = await service._generate_answer( + question="信息不全,继续提供", + retrieved=[], + conversation=[ + {"question": "上一次你缺什么", "answer": "缺目标对象和验收口径"}, + {"question": "现在呢", "answer": "还缺时间边界"}, + ], + project_id=7, + system_snapshot=system_snapshot, + user_snapshot=user_snapshot, + asked_via="feishu", + ) + + assert result == "信息不足,缺少:目标对象" + assert "项目画像:" in service.llm.prompt + assert "项目#7 Alpha(进行中)" in service.llm.prompt + assert "系统摘要:" in service.llm.prompt + assert "用户焦点:" in service.llm.prompt + assert "林宇航" in service.llm.prompt + assert '"projects"' not in service.llm.prompt + assert "项目14" not in service.llm.prompt + + @pytest.mark.asyncio async def test_select_scene_prompt_applies_consultant_mode_preference_wrapper() -> None: class _FakeService(AdaptiveRAGDeepGuardAnswerMixin): diff --git a/backend/tests/test_ai_pmo_ingress_mainline.py b/backend/tests/test_ai_pmo_ingress_mainline.py index 801e9685..42c2e201 100644 --- a/backend/tests/test_ai_pmo_ingress_mainline.py +++ b/backend/tests/test_ai_pmo_ingress_mainline.py @@ -29,6 +29,9 @@ from app.services.ai_pmo.ai_pmo_kernel_ingress_flow import mixin_entry from app.services.ai_pmo.ai_pmo_kernel.mixins.ingress import ai_pmo_kernel_mixins_ingress_render from app.services.ai_pmo.ai_pmo_kernel_ingress_flow import mixin_prepare_manual from app.services.ai_pmo.ai_pmo_kernel_world_model_core import mixin_project +from app.services.ai_pmo.ai_pmo_kernel.mixins.world_model import ( + ai_pmo_kernel_mixins_world_model_session, +) from app.services.ai_pmo.ai_pmo_typed_executor_execute import mixin_execute_query_project from app.services.ai_pmo.ai_pmo_typed_executor_execute import mixin_execute_mutations from app.services.ai_pmo.ai_pmo_intent import CanonicalIntent @@ -362,6 +365,214 @@ async def test_process_ingress_records_mounted_runtime_for_draft_confirmation(mo assert finalize_kwargs["result"]["direct_reply_text"] == "已确认草稿并继续执行。" +@pytest.mark.asyncio +async def test_process_ingress_records_semantic_shadow_before_legacy_finalize(monkeypatch) -> None: + from app.services.ai_pmo.semantic_context.decision_schema import ModeCard, SemanticDecision + from app.services.ai_pmo.semantic_context.packet_schema import ( + SemanticContextPacket, + SemanticContextThread, + SemanticContextTopic, + ) + + captured: dict[str, object] = {"calls": []} + + class FakeMountedAIRuntimeWriteService: + def __init__(self, db) -> None: + captured["db"] = db + + async def begin_ingress_run(self, **kwargs): + captured["calls"].append(("begin", kwargs)) + return SimpleNamespace( + topic_workspace=SimpleNamespace(id=191, tenant_id=1, plan_status="idle"), + conversation_run=SimpleNamespace(id=192, source_message_id="s-msg-semantic-shadow"), + context_layers_hit={"current_message": True, "thread": False, "topic": False}, + ) + + async def record_semantic_shadow(self, **kwargs): + captured["calls"].append(("semantic_shadow", kwargs)) + + async def record_orchestrator_decision(self, **kwargs): + captured["calls"].append(("record", kwargs)) + return "chat" + + async def record_agent_trace(self, **kwargs): + captured["calls"].append(("agent_trace", kwargs)) + + async def finalize_run(self, **kwargs): + captured["calls"].append(("finalize", kwargs)) + + class FakeSemanticContextAssembler: + def __init__(self, db) -> None: + captured["semantic_db"] = db + + async def assemble(self, **kwargs): + captured["semantic_packet_kwargs"] = kwargs + return SemanticContextPacket( + tenant_id=1, + operator_user_id=8, + topic_workspace_id=191, + latest_user_message=str(kwargs["latest_user_message"]), + topic=SemanticContextTopic( + topic_workspace_id=191, + topic_key="feishu:chat-shadow:root:om-root-shadow", + source_system="feishu", + channel_ref="chat-shadow", + conversation_scope_key="feishu:chat-shadow:ou-shadow", + thread_key="thread-shadow", + mode="plan", + status="active", + plan_status="collecting", + facts={"title": "语义 shadow"}, + goals=["统一语义入口"], + constraints={"must_not": ["db.write"]}, + metadata={"source": "test"}, + ), + thread=SemanticContextThread( + thread_key="thread-shadow", + conversation_scope_key="feishu:chat-shadow:ou-shadow", + thread_summary="继续整理语义上下文", + recent_turns=[], + open_questions=["是否继续当前 topic"], + ), + mode_card=ModeCard( + workflow_mode="PLAN", + mode_state="ACTIVE", + allowed_actions=["reply.compose", "draft.patch"], + forbidden_actions=["project.create", "db.write"], + exit_conditions=["用户明确要求开始执行"], + ), + ) + + class FakeSemanticDecisionService: + def __init__(self, _llm_service) -> None: + pass + + async def decide(self, *, packet): + captured["semantic_packet"] = packet.model_dump(mode="json") + return SemanticDecision( + topic_relation="continue_current_topic", + mode_decision="stay_plan", + draft_patch={"goal": "继续整理语义上下文"}, + assistant_reply="我先继续整理上下文。", + requires_confirmation=False, + requested_actions=["reply.compose", "draft.patch"], + confidence=0.88, + ) + + class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin): + def __init__(self) -> None: + self.db = object() + self.tenant_id = 1 + self.context_assembler = SimpleNamespace(assemble_world_model=self._assemble_world_model) + self.route_resolver = SimpleNamespace(from_canonical=self._route_from_canonical) + self.decision_engine = SimpleNamespace(decide=lambda **_: {"decision": "unused"}) + + async def _assemble_world_model(self, **kwargs): + _ = kwargs + return None, {"capacity": [], "skill_graph": [], "resolved_project_id": None} + + def _route_from_canonical(self, **kwargs): + _ = kwargs + return SimpleNamespace( + key="query.answer", + family="query", + action="answer", + requested_count=1, + requires_confirmation=False, + project_id=None, + metadata={}, + ) + + async def _prepare_ingress_envelope(self, **kwargs): + _ = kwargs + return { + "early_result": None, + "clean_text": "继续整理语义上下文,不直接执行。", + "synthetic_message_id": "s-msg-semantic-shadow", + "closure_payload": {}, + "ingest_event": SimpleNamespace(id=200), + "operator": SimpleNamespace(id=8), + "work_item": SimpleNamespace(id=55, state=None, context_snapshot=None, intent=None, project_id=None), + "session": SimpleNamespace(last_intent="query.answer"), + "session_key": "session-shadow", + } + + def _merge_ingress_thread_context(self, **kwargs): + _ = kwargs + return None + + def _should_route_to_draft_confirmation(self, **kwargs): + _ = kwargs + return False + + def _is_short_confirmation_message(self, **kwargs): + _ = kwargs + return False + + async def _prepare_mixed_sidecar_context(self, **kwargs): + return kwargs["message_text"], {} + + async def _resolve_intent(self, **kwargs): + _ = kwargs + return CanonicalIntent( + name=mixin_entry.INTENT_IGNORE, + confidence=0.91, + project_id=None, + signal_title="", + signal_summary="", + metadata={"message_intent": "requirement_plan"}, + ), "llm" + + def _coerce_intent_for_query_scene(self, **kwargs): + return kwargs["intent"] + + def _enrich_signal_source_intent_from_closure(self, **kwargs): + return kwargs["intent"] + + async def _finalize_ignored_ingress(self, **kwargs): + _ = kwargs + return { + "handled": True, + "status": "ignored", + "classification": "ignored", + "direct_reply_required": False, + "direct_reply_text": "这轮先按旧链路忽略处理。", + } + + def _schedule_portfolio_snapshot_persist(self, **kwargs): + _ = kwargs + return None + + monkeypatch.setattr(mixin_entry, "MountedAIRuntimeWriteService", FakeMountedAIRuntimeWriteService) + monkeypatch.setattr(mixin_entry, "SemanticContextAssembler", FakeSemanticContextAssembler) + monkeypatch.setattr(mixin_entry, "SemanticDecisionService", FakeSemanticDecisionService) + + kernel = FakeKernel() + result = await kernel.process_ingress( + source_system="feishu", + source_message_id="feishu-shadow-1", + message_text="继续整理语义上下文,不直接执行。", + sender="tester", + channel_ref="chat-shadow", + source_sender_open_id="ou-shadow", + ) + + assert result["status"] == "ignored" + assert [name for name, _ in captured["calls"]] == [ + "begin", + "semantic_shadow", + "record", + "agent_trace", + "finalize", + ] + semantic_kwargs = captured["calls"][1][1] + assert semantic_kwargs["semantic_packet_summary"]["packet_version"] == "v1" + assert semantic_kwargs["semantic_packet_summary"]["topic_key"] == "feishu:chat-shadow:root:om-root-shadow" + assert semantic_kwargs["semantic_decision"]["mode_decision"] == "stay_plan" + assert semantic_kwargs["semantic_gate_result"]["accepted"] is True + assert semantic_kwargs["semantic_diff"]["legacy_route"] == "query.answer" + + def test_should_route_to_draft_confirmation_requires_anchor_or_explicit_id() -> None: session = SimpleNamespace(last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM) @@ -471,6 +682,141 @@ def test_new_project_statement_does_not_route_to_existing_draft_without_anchor() ) is False +@pytest.mark.asyncio +async def test_should_route_to_draft_confirmation_session_does_not_hijack_explicit_new_requirement_topic( + monkeypatch, +) -> None: + called = False + + class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin): + def __init__(self) -> None: + self.db = object() + self.tenant_id = 1 + + async def _fake_has_active_draft_session(self, **kwargs): + nonlocal called + _ = (self, kwargs) + called = True + return True + + monkeypatch.setattr(DraftConfirmHandler, "has_active_draft_session", _fake_has_active_draft_session) + + kernel = FakeKernel() + should_route = await kernel._should_route_to_draft_confirmation_session( + session=SimpleNamespace(last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM), + sender="tester", + source_system="feishu", + channel_ref="chat-active", + source_sender_open_id="ou_active", + message_text="跟你聊个新需求,关于换季系统自动推荐搭配衣服", + ingress_options=None, + ) + + assert should_route is False + assert called is False + + +def test_should_inherit_recent_feishu_session_context_skips_explicit_new_requirement_topic() -> None: + session = SimpleNamespace( + sender="feishu:tester", + last_active_at=datetime.now(), + last_intent=mixin_entry.STATE_DRAFT_PENDING_CONFIRM, + ) + + should_inherit = KernelIntentResolveMixin._should_inherit_recent_feishu_session_context( + message_text="跟你聊个新需求,关于换季系统自动推荐搭配衣服", + session=session, + ) + + assert should_inherit is False + + +@pytest.mark.asyncio +async def test_explicit_new_requirement_clears_inherited_session_project_context() -> None: + class _FakeDB: + async def flush(self): + return None + + class FakeKernel( + mixin_entry.KernelIngressFlowEntryMixin, + ai_pmo_kernel_mixins_world_model_session.KernelWorldModelSessionMixin, + ): + def __init__(self) -> None: + self.db = _FakeDB() + self.tenant_id = 1 + + kernel = FakeKernel() + session = SimpleNamespace( + current_project_id=104, + last_intent="query.read_only", + last_message_text="A", + ) + + cleared = await kernel._maybe_reset_session_project_context_for_new_requirement( + session=session, + sender="tester", + channel_ref="chat-active", + source_sender_open_id="ou_active", + source_message_id="msg-new-topic", + thread_context=None, + message_text="跟你聊个新需求,关于换季系统自动推荐搭配衣服", + ) + + assert cleared is True + assert session.current_project_id is None + assert session.last_intent == "query.read_only" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("message_text", ["A", "确认", "你自行推荐"]) +async def test_orphan_requirement_followup_returns_direct_reprompt_when_no_active_session(message_text: str) -> None: + class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin): + def __init__(self) -> None: + self.tenant_id = 1 + + async def _has_active_requirement_plan_workspace(self, **kwargs): + _ = kwargs + return False + + kernel = FakeKernel() + result = await kernel._maybe_handle_orphan_requirement_followup( + source_system="feishu", + source_message_id="msg-orphan-reply", + channel_ref="chat-active", + source_sender_open_id="ou_active", + ingress_options=None, + message_text=message_text, + ) + + assert result is not None + assert result["handled"] is True + assert result["direct_reply_required"] is True + assert "没有进行中的 A/B/C 收敛轮次" in str(result["direct_reply_text"] or "") + + +@pytest.mark.asyncio +async def test_orphan_requirement_followup_skips_guard_when_active_requirement_session_exists() -> None: + class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin): + def __init__(self) -> None: + self.tenant_id = 1 + + async def _has_active_requirement_plan_workspace(self, **kwargs): + _ = kwargs + return True + + kernel = FakeKernel() + result = await kernel._maybe_handle_orphan_requirement_followup( + source_system="feishu", + source_message_id="msg-plan-reply", + channel_ref="chat-active", + source_sender_open_id="ou_active", + ingress_options=None, + message_text="A", + ) + + assert result is None + + def test_mounted_runtime_query_business_review_uses_plan_mode_and_non_executor_agent() -> None: mode = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_mode_override( workflow_route="query.answer", @@ -545,16 +891,12 @@ def test_mounted_runtime_query_requirement_plan_uses_plan_coach() -> None: assert agent == "PlanCoach" -def test_mounted_runtime_query_requirement_plan_uses_semantic_bundle_metadata_for_plan_mode() -> None: +def test_mounted_runtime_query_requirement_plan_uses_direct_metadata_for_plan_mode() -> None: mode = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_mode_override( workflow_route="query.answer", route_metadata={ - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - } - } + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, reply_template_key=None, ) @@ -764,12 +1106,10 @@ def test_coerce_rag_business_review_scene_keeps_rag_ask_on_query_chain() -> None assert coerced.metadata["message_intent"] == "business_review" assert coerced.metadata["consultant_mode_preference"] == "clarify" assert coerced.metadata["workflow_route"] == "query.answer" - semantic_bundle = coerced.metadata["semantic_bundle"] - assert semantic_bundle["diagnostics"]["message_intent"] == "business_review" - assert semantic_bundle["diagnostics"]["workflow_route"] == "query.answer" + assert "semantic_bundle" not in coerced.metadata -def test_coerce_feishu_semantic_business_review_bundle_keeps_query_chain() -> None: +def test_coerce_feishu_direct_business_review_metadata_keeps_query_chain() -> None: class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin): def __init__(self) -> None: self.tenant_id = 1 @@ -781,7 +1121,12 @@ def test_coerce_feishu_semantic_business_review_bundle_keeps_query_chain() -> No project_id=None, signal_title="大模型 API 聚合平台", signal_summary="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,", - metadata={"llm_function": "classify_intent_ai_pmo_kernel"}, + metadata={ + "llm_function": "classify_intent_ai_pmo_kernel", + "message_intent": "business_review", + "review_framework": "five_lenses", + "consultant_mode_preference": "clarify", + }, ) coerced = kernel._coerce_intent_for_query_scene( @@ -789,14 +1134,7 @@ def test_coerce_feishu_semantic_business_review_bundle_keeps_query_chain() -> No source_system="feishu", message_text="我现在要创建需求为一个大模型api聚合平台,你帮我分析下要怎么做,", rag_request_context=None, - semantic_bundle={ - "primary_workflow": "query_flow", - "diagnostics": { - "message_intent": "business_review", - "review_framework": "five_lenses", - "consultant_mode_preference": "clarify", - }, - }, + semantic_bundle=None, world_model={}, ) @@ -805,7 +1143,7 @@ def test_coerce_feishu_semantic_business_review_bundle_keeps_query_chain() -> No assert coerced.metadata["workflow_route"] == "query.answer" -def test_coerce_feishu_deferred_requirement_discussion_to_query_chain_from_llm_bundle() -> None: +def test_coerce_feishu_deferred_requirement_discussion_to_query_chain_from_direct_metadata() -> None: class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin): def __init__(self) -> None: self.tenant_id = 1 @@ -818,13 +1156,8 @@ def test_coerce_feishu_deferred_requirement_discussion_to_query_chain_from_llm_b signal_title="直播复盘工具", signal_summary="最近直播复盘太慢了,想做个给运营团队用的小工具,先不要直接创建,我再补充几轮,最后确认再入库。", metadata={ - "semantic_bundle": mixin_entry.WorkflowSemanticPlanner.plan_from_intent( - intent_name="project.create", - message_text="最近直播复盘太慢了,想做个给运营团队用的小工具,先不要直接创建,我再补充几轮,最后确认再入库。", - source_system="ai_pmo_ingress", - workflow_route="project_create.create", - metadata={"confidence": 0.94, "requires_confirmation": True}, - ).to_dict() + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, ) @@ -843,7 +1176,7 @@ def test_coerce_feishu_deferred_requirement_discussion_to_query_chain_from_llm_b assert coerced.metadata["workflow_route"] == "query.answer" -def test_coerce_feishu_requirement_plan_without_semantic_bundle_keeps_query_chain() -> None: +def test_coerce_feishu_requirement_plan_from_direct_metadata_keeps_query_chain() -> None: class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin): def __init__(self) -> None: self.tenant_id = 1 @@ -855,7 +1188,11 @@ def test_coerce_feishu_requirement_plan_without_semantic_bundle_keeps_query_chai project_id=None, signal_title="A股趋势日评打分工具", signal_summary="做一个自动给每日的大A趋势打分的小工具,先不创建,先收敛第一版。", - metadata={"llm_function": "classify_intent_ai_pmo_kernel"}, + metadata={ + "llm_function": "classify_intent_ai_pmo_kernel", + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", + }, ) coerced = kernel._coerce_intent_for_query_scene( @@ -873,7 +1210,7 @@ def test_coerce_feishu_requirement_plan_without_semantic_bundle_keeps_query_chai assert coerced.metadata["workflow_route"] == "query.answer" -def test_coerce_feishu_assignee_recommendation_to_query_chain_from_llm_bundle() -> None: +def test_coerce_feishu_assignee_recommendation_to_query_chain_from_direct_metadata() -> None: class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin): def __init__(self) -> None: self.tenant_id = 1 @@ -886,13 +1223,8 @@ def test_coerce_feishu_assignee_recommendation_to_query_chain_from_llm_bundle() signal_title="直播复盘工具", signal_summary="那你觉得分配给谁做比较合适?", metadata={ - "semantic_bundle": mixin_entry.WorkflowSemanticPlanner.plan_from_intent( - intent_name="project.create", - message_text="那你觉得分配给谁做比较合适?", - source_system="ai_pmo_ingress", - workflow_route="project_create.create", - metadata={"confidence": 0.88, "requires_confirmation": True}, - ).to_dict() + "message_intent": "business_review", + "consultant_mode_preference": "advise", }, ) @@ -959,10 +1291,11 @@ async def test_kernel_intent_resolve_inherits_recent_feishu_session_context_with @pytest.mark.asyncio -async def test_kernel_intent_resolve_locks_signal_source_request_before_llm() -> None: +async def test_kernel_intent_resolve_uses_llm_for_signal_source_request() -> None: class FakeResolver(KernelIntentResolveMixin, KernelIntentRulesMixin): def __init__(self) -> None: self.tenant_id = 1 + self.called = False def _is_short_confirmation_message(self, **kwargs): _ = kwargs @@ -974,7 +1307,15 @@ async def test_kernel_intent_resolve_locks_signal_source_request_before_llm() -> async def _resolve_intent_with_llm(self, **kwargs): _ = kwargs - raise AssertionError("explicit signal source request should not hit llm classifier") + self.called = True + return CanonicalIntent( + name="signal_source.onboard", + confidence=0.92, + project_id=2, + signal_title="", + signal_summary="", + metadata={"source_request": True}, + ) resolver = FakeResolver() @@ -985,7 +1326,8 @@ async def test_kernel_intent_resolve_locks_signal_source_request_before_llm() -> thread_context=None, ) - assert source == "rule_signal_source_onboard" + assert source == "llm_only" + assert resolver.called is True assert intent.name == "signal_source.onboard" assert intent.project_id == 2 assert intent.metadata["source_request"] is True @@ -1358,7 +1700,7 @@ async def test_execute_query_intent_feishu_fallback_uses_thread_key(monkeypatch) @pytest.mark.asyncio -async def test_execute_query_intent_uses_semantic_bundle_rag_context_when_no_explicit_rag_request(monkeypatch) -> None: +async def test_execute_query_intent_uses_direct_scene_metadata_when_no_explicit_rag_request(monkeypatch) -> None: captured: dict[str, object] = {} class FakeAdaptiveRAGService: @@ -1382,13 +1724,9 @@ async def test_execute_query_intent_uses_semantic_bundle_rag_context_when_no_exp metadata={ "source_chat_id": "chat-dm-9", "source_sender_open_id": "ou-feishu-9", - "semantic_bundle": { - "diagnostics": { - "message_intent": "correction_escalation", - "consultant_mode_preference": "challenge", - "force_thinking_mode": "deep", - } - }, + "message_intent": "correction_escalation", + "consultant_mode_preference": "challenge", + "force_thinking_mode": "deep", }, ) result = await executor._execute_query_intent( @@ -1542,7 +1880,11 @@ async def test_execute_query_intent_short_circuits_casual_chat_without_rag(monke ) assert result["status"] == "succeeded" - assert "我在线" in str(result["answer"] or "") + answer = str(result["answer"] or "") + assert "我在线" in answer + assert "A." not in answer + assert "B." not in answer + assert "C." not in answer assert str(result["rag_result"]["query_type"]) == "chat" @@ -1585,7 +1927,12 @@ async def test_execute_query_intent_short_circuits_requirement_followup_clarify_ ) assert result["status"] == "succeeded" - assert "轻量版继续理解" in str(result["answer"] or "") + answer = str(result["answer"] or "") + assert "轻量版继续理解" in answer + assert "A." in answer + assert "B." in answer + assert "C." in answer + assert "直接回 A/B/C" in answer assert str(result["rag_result"]["query_type"]) == "clarify" @@ -1644,12 +1991,8 @@ async def test_execute_query_intent_requirement_plan_scene_short_circuits_withou intent = SimpleNamespace( project_id=None, metadata={ - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - } - } + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, ) @@ -1695,12 +2038,8 @@ async def test_execute_query_intent_requirement_plan_followup_persists_progress_ metadata={ "source_chat_id": "chat-plan-1", "source_sender_open_id": "ou-plan-1", - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - } - }, + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, ) followup_intent = SimpleNamespace( @@ -1712,12 +2051,8 @@ async def test_execute_query_intent_requirement_plan_followup_persists_progress_ "root_message_id": "msg-plan-root", "reply_to_message_id": "msg-plan-root", }, - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - } - }, + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, ) @@ -1789,12 +2124,8 @@ async def test_execute_query_intent_requirement_plan_builds_feishu_friendly_draf metadata={ "source_chat_id": "chat-plan-2", "source_sender_open_id": "ou-plan-2", - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - } - }, + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, ) intent_followup = SimpleNamespace( @@ -1806,12 +2137,8 @@ async def test_execute_query_intent_requirement_plan_builds_feishu_friendly_draf "root_message_id": "msg-plan-root-2", "reply_to_message_id": "msg-plan-root-2", }, - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - } - }, + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, ) @@ -1852,6 +2179,8 @@ async def test_execute_query_intent_requirement_plan_builds_feishu_friendly_draf assert "【输出形式】" in answer assert "【更新频率】" in answer assert str(final_result["rag_result"]["query_type"]) == "draft" + assert final_result["reply_template_key"] == "draft_pending_confirm" + assert final_result["draft_artifact"]["promote_to_confirmation"] is True draft = ( await requirement_plan_db_session.execute( @@ -1865,6 +2194,9 @@ async def test_execute_query_intent_requirement_plan_builds_feishu_friendly_draf ).scalar_one() assert draft.state == "ready" assert "每日 A 股趋势打分工具" in str((draft.draft_payload or {}).get("title") or "") + assert draft.active_work_item_id is None + assert draft.latest_source_message_id == "msg-plan-reply-2c" + assert draft.latest_reply_message_id is None assert topic.plan_status == "awaiting_execute_confirm" assert topic.pending_actions == ["confirm_requirement_draft"] @@ -1895,12 +2227,8 @@ async def test_execute_query_intent_explicit_new_requirement_topic_suspends_prev metadata={ "source_chat_id": "chat-plan-switch", "source_sender_open_id": "ou-plan-switch", - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - } - }, + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, ) explicit_switch_intent = SimpleNamespace( @@ -1908,13 +2236,9 @@ async def test_execute_query_intent_explicit_new_requirement_topic_suspends_prev metadata={ "source_chat_id": "chat-plan-switch", "source_sender_open_id": "ou-plan-switch", - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - "topic_switch": "new", - } - }, + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", + "topic_switch": "new", }, ) @@ -1949,6 +2273,332 @@ async def test_execute_query_intent_explicit_new_requirement_topic_suspends_prev assert rows[1].plan_status == "collecting" +@pytest.mark.asyncio +async def test_execute_query_intent_explicit_new_it_requirement_overrides_stale_clarify_and_avoids_old_scoring_topic( + monkeypatch, + requirement_plan_db_session, +) -> None: + class FakeAdaptiveRAGService: + def __init__(self, db, tenant_id: int): + _ = (db, tenant_id) + + async def answer_question(self, **kwargs): + _ = kwargs + raise AssertionError("new IT requirement topic should stay in direct plan flow") + + class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin): + def __init__(self) -> None: + self.db = requirement_plan_db_session + self.tenant_id = 3 + + monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService) + executor = FakeExecutor() + + conversation_scope_key = build_feishu_conversation_scope_key( + source_chat_id="chat-plan-it-switch", + source_sender_open_id="ou-plan-it-switch", + sender=None, + ) + old_topic = MountedTopicWorkspace( + tenant_id=3, + topic_key="feishu:chat-plan-it-switch:message:msg-old-root", + source_system="feishu", + channel_ref="chat-plan-it-switch", + conversation_scope_key=conversation_scope_key, + root_message_id="msg-old-root", + mode="plan", + status="active", + plan_status="awaiting_execute_confirm", + metadata_json={"plan_mode": True}, + facts_json={"plan_profile": "scoring", "title": "每日 A 股趋势打分工具"}, + ) + requirement_plan_db_session.add(old_topic) + await requirement_plan_db_session.flush() + requirement_plan_db_session.add( + MountedDraftWorkspace( + tenant_id=3, + topic_workspace_id=int(old_topic.id), + state="ready", + draft_version=1, + requirement_card={ + "plan_profile": "scoring", + "title": "每日 A 股趋势打分工具", + "goal": "每天产出一个可解释的 A 股趋势分数", + "scoring_basis": "技术指标(均线、成交量、涨跌比等)", + "target_user": "投顾", + "output_form": "飞书日报", + "cadence": "每日", + "first_scope": "第一版先做 A 股趋势分数和一句解释", + }, + draft_payload={"title": "每日 A 股趋势打分工具"}, + required_confirmations=["需求草稿需要人工确认后才允许正式创建"], + ) + ) + await requirement_plan_db_session.flush() + + stale_clarify_intent = SimpleNamespace( + project_id=None, + metadata={ + "source_chat_id": "chat-plan-it-switch", + "source_sender_open_id": "ou-plan-it-switch", + "message_intent": "business_review", + "consultant_mode_preference": "clarify", + }, + ) + followup_intent = SimpleNamespace( + project_id=None, + metadata={ + "source_chat_id": "chat-plan-it-switch", + "source_sender_open_id": "ou-plan-it-switch", + "thread_context": { + "root_message_id": "msg-new-it-root", + "reply_to_message_id": "msg-new-it-root", + }, + }, + ) + + first = await executor._execute_query_intent( + intent=stale_clarify_intent, + message_text="想跟你聊个新需求关于批量自动化部署openclaw的,先别创建,先收第一版。", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-new-it-root", + ) + second = await executor._execute_query_intent( + intent=followup_intent, + message_text="C,先按云原生交付底座来收。", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-new-it-2", + ) + + first_answer = str(first["answer"] or "") + second_answer = str(second["answer"] or "") + + assert "🧭 我先按“批量自动化部署 OpenClaw”这类 IT 项目来收。" in first_answer + assert "云原生交付底座" in first_answer + assert "目标对象" not in first_answer + assert "A股趋势打分" not in first_answer + + assert "🧭 我先按“云原生交付底座”这个方向继续收。" in second_answer + assert "现在要先把服务对象定住" in second_answer + assert "A股趋势打分" not in second_answer + assert "飞书日报" not in second_answer + + rows = ( + await requirement_plan_db_session.execute( + select(MountedTopicWorkspace) + .where(MountedTopicWorkspace.tenant_id == 3) + .order_by(MountedTopicWorkspace.id.asc()) + ) + ).scalars().all() + + assert len(rows) == 2 + assert rows[0].status == "suspended" + assert rows[0].plan_status == "suspended" + assert rows[1].status == "active" + assert rows[1].plan_status == "collecting" + assert rows[1].root_message_id == "msg-new-it-root" + + +@pytest.mark.asyncio +async def test_execute_query_intent_explicit_new_requirement_topic_reuses_current_tracking_topic( + monkeypatch, + requirement_plan_db_session, +) -> None: + class FakeAdaptiveRAGService: + def __init__(self, db, tenant_id: int): + _ = (db, tenant_id) + + async def answer_question(self, **kwargs): + _ = kwargs + raise AssertionError("explicit new topic switching should not enter heavy RAG path") + + class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin): + def __init__(self) -> None: + self.db = requirement_plan_db_session + self.tenant_id = 3 + + monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService) + executor = FakeExecutor() + + conversation_scope_key = build_feishu_conversation_scope_key( + source_chat_id="chat-plan-switch-reuse", + source_sender_open_id="ou-plan-switch-reuse", + sender=None, + ) + existing_topic = MountedTopicWorkspace( + tenant_id=3, + topic_key="feishu:chat-plan-switch-reuse:message:msg-plan-switch-root-reuse", + source_system="feishu", + channel_ref="chat-plan-switch-reuse", + conversation_scope_key=conversation_scope_key, + root_message_id="msg-plan-switch-root-reuse", + mode="plan", + status="active", + plan_status="collecting", + metadata_json={"plan_mode": True}, + ) + requirement_plan_db_session.add(existing_topic) + await requirement_plan_db_session.flush() + requirement_plan_db_session.add( + MountedDraftWorkspace( + tenant_id=3, + topic_workspace_id=int(existing_topic.id), + state="collecting", + draft_version=0, + requirement_card={"title": "旧需求"}, + draft_payload=None, + required_confirmations=["需求草稿需要人工确认后才允许正式创建"], + ) + ) + tracking_topic = MountedTopicWorkspace( + tenant_id=3, + topic_key="feishu:chat-plan-switch-reuse:message:msg-plan-switch-new-reuse", + source_system="feishu", + channel_ref="chat-plan-switch-reuse", + conversation_scope_key=conversation_scope_key, + mode="chat", + status="active", + plan_status="idle", + metadata_json={"tracking": True}, + ) + requirement_plan_db_session.add(tracking_topic) + await requirement_plan_db_session.flush() + + explicit_switch_intent = SimpleNamespace( + project_id=None, + metadata={ + "source_chat_id": "chat-plan-switch-reuse", + "source_sender_open_id": "ou-plan-switch-reuse", + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", + "topic_switch": "new", + }, + ) + + switch_result = await executor._execute_query_intent( + intent=explicit_switch_intent, + message_text="想跟你聊个新需求,关于批量自动化部署openclaw,先不创建,先收第一版。", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-plan-switch-new-reuse", + ) + + rows = ( + await requirement_plan_db_session.execute( + select(MountedTopicWorkspace) + .where(MountedTopicWorkspace.tenant_id == 3) + .order_by(MountedTopicWorkspace.id.asc()) + ) + ).scalars().all() + + assert len(rows) == 2 + assert switch_result["status"] == "succeeded" + assert rows[0].status == "suspended" + assert rows[0].plan_status == "suspended" + assert rows[1].id == tracking_topic.id + assert rows[1].status == "active" + assert rows[1].mode == "plan" + assert rows[1].plan_status == "collecting" + + +@pytest.mark.asyncio +async def test_execute_query_intent_explicit_new_requirement_topic_with_subject_starts_collection_immediately( + monkeypatch, + requirement_plan_db_session, +) -> None: + class FakeAdaptiveRAGService: + def __init__(self, db, tenant_id: int): + _ = (db, tenant_id) + + async def answer_question(self, **kwargs): + _ = kwargs + raise AssertionError("explicit new topic seed should not enter heavy RAG path") + + class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin): + def __init__(self) -> None: + self.db = requirement_plan_db_session + self.tenant_id = 3 + + monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService) + executor = FakeExecutor() + + conversation_scope_key = build_feishu_conversation_scope_key( + source_chat_id="chat-plan-switch-subject", + source_sender_open_id="ou-plan-switch-subject", + sender=None, + ) + existing_topic = MountedTopicWorkspace( + tenant_id=3, + topic_key="feishu:chat-plan-switch-subject:message:msg-plan-switch-subject-root", + source_system="feishu", + channel_ref="chat-plan-switch-subject", + conversation_scope_key=conversation_scope_key, + root_message_id="msg-plan-switch-subject-root", + mode="plan", + status="active", + plan_status="draft_pending_confirm", + metadata_json={"plan_mode": True}, + ) + requirement_plan_db_session.add(existing_topic) + await requirement_plan_db_session.flush() + requirement_plan_db_session.add( + MountedDraftWorkspace( + tenant_id=3, + topic_workspace_id=int(existing_topic.id), + state="draft_pending_confirm", + draft_version=1, + requirement_card={"title": "旧需求"}, + draft_payload={"title": "旧需求"}, + required_confirmations=["需求草稿需要人工确认后才允许正式创建"], + ) + ) + tracking_topic = MountedTopicWorkspace( + tenant_id=3, + topic_key="feishu:chat-plan-switch-subject:message:msg-plan-switch-subject-new", + source_system="feishu", + channel_ref="chat-plan-switch-subject", + conversation_scope_key=conversation_scope_key, + mode="chat", + status="active", + plan_status="idle", + metadata_json={"tracking": True}, + ) + requirement_plan_db_session.add(tracking_topic) + await requirement_plan_db_session.flush() + + explicit_switch_intent = SimpleNamespace( + project_id=None, + metadata={ + "source_chat_id": "chat-plan-switch-subject", + "source_sender_open_id": "ou-plan-switch-subject", + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", + "topic_switch": "new", + }, + ) + + result = await executor._execute_query_intent( + intent=explicit_switch_intent, + message_text="跟你聊个新需求,关于换季系统自动推荐搭配衣服", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-plan-switch-subject-new", + ) + + answer = str(result["answer"] or "") + assert "你先发一句这次真正想解决什么问题" not in answer + assert "主要给谁用" in answer + assert "会议纪要" not in answer + assert "输入先按用户当前需求描述来" not in answer + assert "A." in answer + assert "B." in answer + assert "C." in answer + assert "你回 A / B / C" in answer + assert str(result["rag_result"]["query_type"]) == "plan" + + @pytest.mark.asyncio async def test_execute_query_intent_low_overlap_requirement_seed_switches_new_topic( monkeypatch, @@ -2024,12 +2674,8 @@ async def test_execute_query_intent_low_overlap_requirement_seed_switches_new_to metadata={ "source_chat_id": "chat-plan-overlap", "source_sender_open_id": "ou-plan-overlap", - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - } - }, + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, ) @@ -2083,12 +2729,8 @@ async def test_execute_query_intent_requirement_plan_accepts_fragmented_short_an metadata={ "source_chat_id": "chat-plan-3", "source_sender_open_id": "ou-plan-3", - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - } - }, + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, ) fragmented_followup_intent = SimpleNamespace( @@ -2201,12 +2843,8 @@ async def test_execute_query_intent_requirement_plan_generalizes_for_meeting_min metadata={ "source_chat_id": "chat-plan-meeting", "source_sender_open_id": "ou-plan-meeting", - "semantic_bundle": { - "diagnostics": { - "message_intent": "requirement_plan", - "consultant_mode_preference": "clarify", - } - }, + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", }, ) followup_intent = SimpleNamespace( @@ -2258,8 +2896,17 @@ async def test_execute_query_intent_requirement_plan_generalizes_for_meeting_min ) assert "主要给谁用" in str(first["answer"] or "") or "主要给谁看" in str(first["answer"] or "") - assert "怎么给结果" in str(second["answer"] or "") or "怎么接收" in str(second["answer"] or "") + assert "A." in str(first["answer"] or "") + assert "B." in str(first["answer"] or "") + assert "C." in str(first["answer"] or "") + assert "怎么给结果" in str(second["answer"] or "") or "怎么接收" in str(second["answer"] or "") or "最后怎么给你" in str(second["answer"] or "") + assert "A." in str(second["answer"] or "") + assert "B." in str(second["answer"] or "") + assert "C." in str(second["answer"] or "") assert "多久整理出来" in str(third["answer"] or "") or "时效" in str(third["answer"] or "") + assert "A." in str(third["answer"] or "") + assert "B." in str(third["answer"] or "") + assert "C." in str(third["answer"] or "") assert "先保留哪些字段" in str(fourth["answer"] or "") or "第一版先保留哪些字段" in str(fourth["answer"] or "") final_answer = str(final_result["answer"] or "") @@ -2285,6 +2932,259 @@ async def test_execute_query_intent_requirement_plan_generalizes_for_meeting_min assert "待办" in str(card["first_scope"] or "") +@pytest.mark.asyncio +async def test_execute_query_intent_requirement_plan_it_project_runs_five_round_pm_template( + monkeypatch, + requirement_plan_db_session, +) -> None: + class FakeAdaptiveRAGService: + def __init__(self, db, tenant_id: int): + _ = (db, tenant_id) + + async def answer_question(self, **kwargs): + _ = kwargs + raise AssertionError("it project requirement plan should not enter heavy RAG path") + + class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin): + def __init__(self) -> None: + self.db = requirement_plan_db_session + self.tenant_id = 3 + + monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService) + executor = FakeExecutor() + + root_intent = SimpleNamespace( + project_id=None, + metadata={ + "source_chat_id": "chat-plan-it", + "source_sender_open_id": "ou-plan-it", + "message_intent": "requirement_plan", + "consultant_mode_preference": "clarify", + }, + ) + followup_intent = SimpleNamespace( + project_id=None, + metadata={ + "source_chat_id": "chat-plan-it", + "source_sender_open_id": "ou-plan-it", + "thread_context": { + "root_message_id": "msg-plan-it-root", + "reply_to_message_id": "msg-plan-it-root", + }, + }, + ) + + first = await executor._execute_query_intent( + intent=root_intent, + message_text="想跟你聊个 IT 项目需求,做一个批量自动化部署 OpenClaw 的平台,先别创建,先收第一版。", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-plan-it-root", + ) + second = await executor._execute_query_intent( + intent=followup_intent, + message_text="B,先做发布治理平台。", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-plan-it-2", + ) + third = await executor._execute_query_intent( + intent=followup_intent, + message_text="B,主要给平台运维团队用。", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-plan-it-3", + ) + fourth = await executor._execute_query_intent( + intent=followup_intent, + message_text="B,走任务编排平台版,用 FastAPI + React + Redis + Celery + Postgres。", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-plan-it-4", + ) + fifth = await executor._execute_query_intent( + intent=followup_intent, + message_text="A,第一版先做批量执行闭环,包含任务创建、执行日志和失败重试。", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-plan-it-5", + ) + final_result = await executor._execute_query_intent( + intent=followup_intent, + message_text="B,先做项目级试点,带基础审批和回滚。", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-plan-it-6", + ) + + assert "🧭 我先按“批量自动化部署 OpenClaw”这类 IT 项目来收。" in str(first["answer"] or "") + assert "A. 先做内部提效工具" in str(first["answer"] or "") + assert "先定它更像工具、平台,还是长期底座" in str(first["answer"] or "") + assert "⭐ 当前我更推荐:B" in str(first["answer"] or "") + assert "你回 A / B / C" in str(first["answer"] or "") + + assert "🧭 我先按“发布治理平台”这个方向继续收。" in str(second["answer"] or "") + assert "平台运维团队" in str(second["answer"] or "") + assert "⭐ 当前我更推荐:B" in str(second["answer"] or "") + + assert "🧭 我先按“平台运维团队要长期使用”这个方向继续收。" in str(third["answer"] or "") + assert "现在最关键的,不是再补更多背景,而是先把技术路线定住" in str(third["answer"] or "") + assert "FastAPI + React + Redis + Celery + PostgreSQL" in str(third["answer"] or "") + assert "⭐ 当前我更推荐:B" in str(third["answer"] or "") + + assert "🧭 我先按“任务编排平台版”继续往下收。" in str(fourth["answer"] or "") + assert "批量执行闭环" in str(fourth["answer"] or "") + assert "⭐ 当前我更推荐:A" in str(fourth["answer"] or "") + + assert "🧭 我先按“批量执行闭环”继续收尾。" in str(fifth["answer"] or "") + assert "项目级试点" in str(fifth["answer"] or "") + assert "⭐ 当前我更推荐:B" in str(fifth["answer"] or "") + + final_answer = str(final_result["answer"] or "") + assert "📋 我先按当前收敛结果,整理成一版更像正式 brief 的需求草稿:" in final_answer + assert "🧭【一句话判断】" in final_answer + assert "🏗️【项目性质】这件事更像发布治理平台" in final_answer + assert "👥【服务对象】第一版先服务平台运维团队" in final_answer + assert "📡【信号源】当前至少要接入:" in final_answer + assert "飞书需求消息" in final_answer + assert "部署目标清单" in final_answer + assert "📝【当前已知信息】目前已经明确:" in final_answer + assert "⚙️【技术路线】当前更适合走任务编排平台版(FastAPI + React + Redis + Celery + PostgreSQL)" in final_answer + assert "🧱【第一版范围】第一版先做批量执行闭环,包含任务创建、执行日志和失败重试。" in final_answer + assert "🔄【建议核心流程】" in final_answer + assert "生成部署任务" in final_answer + assert "👤【人员配置】建议最小配置:" in final_answer + assert "平台/运维工程师" in final_answer + assert "🚀【推进方式】先按项目级试点(带基础审批和回滚)推进" in final_answer + assert "⚠️【风险与依赖】当前主要风险在" in final_answer + assert "凭证管理" in final_answer + assert "❓【待确认】还需要你拍板:" in final_answer + assert "部署目标范围" in final_answer + assert "试点负责人" in final_answer + assert str(final_result["rag_result"]["query_type"]) == "draft" + + draft = ( + await requirement_plan_db_session.execute( + select(MountedDraftWorkspace).where(MountedDraftWorkspace.tenant_id == 3) + ) + ).scalar_one() + card = draft.requirement_card or {} + assert card["plan_profile"] == "it_project" + assert card["project_nature"] == "发布治理平台" + assert card["target_user"] == "平台运维团队" + assert "FastAPI" in str(card["tech_route"] or "") + assert "批量执行闭环" in str(card["first_scope"] or "") + assert "项目级试点" in str(card["delivery_mode"] or "") + + +@pytest.mark.asyncio +async def test_execute_query_intent_single_choice_does_not_resume_awaiting_requirement_draft( + monkeypatch, + requirement_plan_db_session, +) -> None: + class FakeAdaptiveRAGService: + def __init__(self, db, tenant_id: int): + _ = (db, tenant_id) + + async def answer_question(self, **kwargs): + _ = kwargs + return { + "answer": "当前没有进行中的 A/B/C 收敛轮次。你先发一句新需求,我按新的 topic 接着收。", + "query_type": "status", + "confidence": 0.42, + "missing_fields": [], + } + + class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin): + def __init__(self) -> None: + self.db = requirement_plan_db_session + self.tenant_id = 3 + + monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService) + executor = FakeExecutor() + + conversation_scope_key = build_feishu_conversation_scope_key( + source_chat_id="chat-old-draft", + source_sender_open_id="ou-old-draft", + sender=None, + ) + old_topic = MountedTopicWorkspace( + tenant_id=3, + topic_key="feishu:chat-old-draft:message:sim-plan-1431-1", + source_system="feishu", + channel_ref="chat-old-draft", + conversation_scope_key=conversation_scope_key, + root_message_id="sim-plan-1431-1", + mode="plan", + status="active", + plan_status="awaiting_execute_confirm", + pending_actions=["confirm_requirement_draft"], + metadata_json={ + "plan_mode": True, + "plan_question_round": 17, + "plan_last_prompt": "下一个关键问题:你更想怎么接收这个结果,是飞书日报、页面看板,还是只推送一个分数加一句解释?", + }, + facts_json={ + "title": "每日 A 股趋势打分工具", + "plan_profile": "scoring", + "target_user": "投顾", + }, + ) + requirement_plan_db_session.add(old_topic) + await requirement_plan_db_session.flush() + requirement_plan_db_session.add( + MountedDraftWorkspace( + tenant_id=3, + topic_workspace_id=int(old_topic.id), + state="ready", + draft_version=1, + requirement_card={ + "plan_profile": "scoring", + "title": "每日 A 股趋势打分工具", + "goal": "每天产出一个可解释的 A 股趋势分数", + "target_user": "投顾", + "scoring_basis": "技术指标(均线、成交量、涨跌比等)", + "output_form": "飞书日报", + "cadence": "每日", + "first_scope": "第一版先做 A 股趋势分数和一句解释", + }, + draft_payload={"title": "每日 A 股趋势打分工具"}, + required_confirmations=["需求草稿需要人工确认后才允许正式创建"], + ) + ) + await requirement_plan_db_session.flush() + + followup_intent = SimpleNamespace( + project_id=None, + metadata={ + "source_chat_id": "chat-old-draft", + "source_sender_open_id": "ou-old-draft", + }, + ) + + result = await executor._execute_query_intent( + intent=followup_intent, + message_text="C", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-old-draft-c", + ) + + answer = str(result["answer"] or "") + assert "每日 A 股趋势打分工具" not in answer + assert "需求草稿" not in answer + assert "新需求" in answer + assert str(result["rag_result"]["query_type"]) == "status" + + topic = ( + await requirement_plan_db_session.execute( + select(MountedTopicWorkspace).where(MountedTopicWorkspace.id == int(old_topic.id)) + ) + ).scalar_one() + assert topic.plan_status == "awaiting_execute_confirm" + assert topic.pending_actions == ["confirm_requirement_draft"] + + @pytest.mark.asyncio async def test_execute_query_intent_short_circuits_requirement_advise_without_rag(monkeypatch) -> None: class FakeAdaptiveRAGService: @@ -2395,6 +3295,49 @@ async def test_execute_query_intent_short_circuits_assignee_recommendation_witho assert str(result["rag_result"]["query_type"]) == "recommendation" +@pytest.mark.asyncio +async def test_execute_query_intent_prefers_direct_advise_subtype_for_assignee(monkeypatch) -> None: + class FakeAdaptiveRAGService: + def __init__(self, db, tenant_id: int): + _ = (db, tenant_id) + + async def answer_question(self, **kwargs): + _ = kwargs + raise AssertionError("direct assignee subtype should bypass heavy RAG path") + + class FakeExecutor(mixin_execute_query_project.ExecuteQueryProjectMixin): + def __init__(self) -> None: + self.db = object() + self.tenant_id = 3 + + async def _build_assignee_recommendation_answer(self, *, message_text: str, project_id): + _ = (message_text, project_id) + return "如果先给一个 owner,我会优先推荐 AI工程师。" + + monkeypatch.setattr(mixin_execute_query_project, "AdaptiveRAGService", FakeAdaptiveRAGService) + executor = FakeExecutor() + intent = SimpleNamespace( + project_id=None, + metadata={ + "message_intent": "business_review", + "consultant_mode_preference": "advise", + "advise_subtype": "assignee", + }, + ) + + result = await executor._execute_query_intent( + intent=intent, + message_text="那你觉得分配给谁做比较合适?", + operator_user_id=999, + source_system="feishu", + source_message_id="msg-chat-3c-direct", + ) + + assert result["status"] == "succeeded" + assert "AI工程师" in str(result["answer"] or "") + assert str(result["rag_result"]["query_type"]) == "recommendation" + + @pytest.mark.asyncio async def test_execute_query_intent_short_circuits_project_existence_lookup_without_rag(monkeypatch) -> None: class FakeAdaptiveRAGService: @@ -2449,6 +3392,79 @@ async def test_draft_confirmation_route_does_not_hijack_explicit_signal_source_r assert should_route is False +@pytest.mark.asyncio +async def test_should_route_to_draft_confirmation_session_uses_active_mounted_draft_without_anchor( + monkeypatch, +) -> None: + captured: dict[str, object] = {} + + class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin): + def __init__(self) -> None: + self.db = object() + self.tenant_id = 1 + + async def _fake_has_active_draft_session(self, **kwargs): + _ = self + captured["kwargs"] = kwargs + return True + + monkeypatch.setattr(DraftConfirmHandler, "has_active_draft_session", _fake_has_active_draft_session) + + kernel = FakeKernel() + should_route = await kernel._should_route_to_draft_confirmation_session( + session=SimpleNamespace(last_intent="query.answer"), + sender="tester", + source_system="feishu", + channel_ref="chat-active", + source_sender_open_id="ou_active", + message_text="确认", + ingress_options=None, + ) + + assert should_route is True + assert captured["kwargs"] == { + "sender": "tester", + "source_system": "feishu", + "channel_ref": "chat-active", + "source_sender_open_id": "ou_active", + "thread_context": {}, + } + + +@pytest.mark.asyncio +async def test_should_route_to_draft_confirmation_session_does_not_hijack_smalltalk_even_if_active_draft_exists( + monkeypatch, +) -> None: + called = False + + class FakeKernel(mixin_entry.KernelIngressFlowEntryMixin): + def __init__(self) -> None: + self.db = object() + self.tenant_id = 1 + + async def _fake_has_active_draft_session(self, **kwargs): + nonlocal called + _ = (self, kwargs) + called = True + return True + + monkeypatch.setattr(DraftConfirmHandler, "has_active_draft_session", _fake_has_active_draft_session) + + kernel = FakeKernel() + should_route = await kernel._should_route_to_draft_confirmation_session( + session=SimpleNamespace(last_intent="query.answer"), + sender="tester", + source_system="feishu", + channel_ref="chat-active", + source_sender_open_id="ou_active", + message_text="收到,辛苦了", + ingress_options=None, + ) + + assert should_route is False + assert called is False + + def test_mounted_runtime_agent_name_uses_plan_coach_for_scope_advise() -> None: agent_name = mixin_entry.KernelIngressFlowEntryMixin._resolve_mounted_runtime_agent_name( workflow_route="query.answer", diff --git a/backend/tests/test_context_cache_profile_catalog.py b/backend/tests/test_context_cache_profile_catalog.py index 77d3af91..37c94946 100644 --- a/backend/tests/test_context_cache_profile_catalog.py +++ b/backend/tests/test_context_cache_profile_catalog.py @@ -1,7 +1,25 @@ +from pathlib import Path + +import app.services.context_cache_service.mixin_cache_core as cache_core_module +import app.services.context_cache_service.mixin_profile_route_parser as route_parser_module + +from app.services.context_cache_service.mixin_cache_core import ContextCacheCoreMixin from app.services.context_cache_service.mixin_profile_basics import ContextCacheProfileBasicsMixin from app.services.context_cache_service.mixin_profile_route_parser import ContextCacheProfileRouteParserMixin +class _FakeContextCacheProfileService( + ContextCacheProfileBasicsMixin, + ContextCacheProfileRouteParserMixin, +): + pass + + +class _FakeContextCacheCoreService(ContextCacheCoreMixin): + def __init__(self) -> None: + super().__init__(redis_url=None) + + def _find_route(items: list[dict], route: str) -> dict: for item in items: if str(item.get("route") or "") == route: @@ -21,3 +39,79 @@ def test_route_feature_summary_uses_canonical_rag_ask() -> None: _summary, apis, _aliases = ContextCacheProfileRouteParserMixin._route_feature_summary("/ai-steward") assert "/rag/ask" in apis assert "/ai-steward/rag/ask" not in apis + + +def test_build_platform_profile_scans_backend_app_root(tmp_path, monkeypatch) -> None: + backend_root = tmp_path / "backend" + app_root = backend_root / "app" + fake_module_file = app_root / "services" / "context_cache_service" / "mixin_profile_route_parser.py" + fake_module_file.parent.mkdir(parents=True, exist_ok=True) + fake_module_file.write_text("# fake module path anchor\n", encoding="utf-8") + + files = { + app_root / "api" / "demo.py": '@router.get("/demo")\nasync def demo():\n return {"ok": True}\n', + app_root / "services" / "service_a.py": "class ServiceA:\n pass\n", + app_root / "models" / "model_a.py": "class ModelA:\n pass\n", + app_root / "schemas" / "schema_a.py": "class SchemaA:\n pass\n", + app_root / "dependencies" / "dep_a.py": "def dep_a():\n return 1\n", + app_root / "core" / "core_a.py": "VALUE = 1\n", + app_root / "config" / "config_a.py": "SETTING = 'x'\n", + } + for path, content in files.items(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + monkeypatch.setattr(route_parser_module, "__file__", str(fake_module_file)) + + profile = _FakeContextCacheProfileService()._build_platform_profile() + + assert profile["backend"]["module_counts"] == { + "api": 1, + "services": 2, + "models": 1, + "schemas": 1, + "dependencies": 1, + "core": 1, + "config": 1, + } + assert profile["backend"]["api_route_count_estimate"] == 1 + assert profile["source_scan"]["backend_root"] == str(backend_root) + assert profile["source_scan"]["app_root"] == str(app_root) + assert profile["source_scan"]["feature_catalog_source"] == "fallback" + + +def test_is_system_snapshot_complete_rejects_stale_source_scan(tmp_path, monkeypatch) -> None: + backend_root = tmp_path / "backend" + app_root = backend_root / "app" + fake_module_file = app_root / "services" / "context_cache_service" / "mixin_cache_core.py" + fake_module_file.parent.mkdir(parents=True, exist_ok=True) + fake_module_file.write_text("# fake module path anchor\n", encoding="utf-8") + monkeypatch.setattr(cache_core_module, "__file__", str(fake_module_file)) + + service = _FakeContextCacheCoreService() + + stale_payload = { + "schema_version": "2026-04-01.context-v1", + "assistant_profile": {"name": "PMFlow"}, + "platform_profile": { + "source_scan": { + "backend_root": str(app_root), + "app_root": str(app_root / "app"), + "workspace_root": str(tmp_path), + } + }, + } + fresh_payload = { + "schema_version": cache_core_module.SYSTEM_SNAPSHOT_SCHEMA_VERSION, + "assistant_profile": {"name": "PMFlow"}, + "platform_profile": { + "source_scan": { + "backend_root": str(backend_root), + "app_root": str(app_root), + "workspace_root": str(tmp_path), + } + }, + } + + assert service._is_system_snapshot_complete(stale_payload) is False + assert service._is_system_snapshot_complete(fresh_payload) is True diff --git a/backend/tests/test_draft_confirm_handler.py b/backend/tests/test_draft_confirm_handler.py index 83832315..b09c01de 100644 --- a/backend/tests/test_draft_confirm_handler.py +++ b/backend/tests/test_draft_confirm_handler.py @@ -1,8 +1,10 @@ +import json from types import SimpleNamespace import pytest from app.config.ai_prompts import ( + PROMPT_AI_PMO_REQUIREMENT_PLAN_QA, PROMPT_EXTERNAL_TREND_QA, PROMPT_FEISHU_DIRECT_QA, PROMPT_FEISHU_MIXED_QA, @@ -107,6 +109,25 @@ def test_reply_outbox_extract_sent_message_id() -> None: assert FeishuReplyOutboxMixin._extract_sent_message_id({"message_id": "msg_456"}) == "msg_456" +def test_reply_outbox_normalize_beautifies_structured_pm_sections() -> None: + from app.services.integrations.feishu_integration_service.mixin_reply_outbox import FeishuReplyOutboxMixin + + raw = ( + "结论:项目 #104 当前还不能直接判断是否启动后端。\n" + "依据:当前只命中静态代码索引,缺少真实归属和负责人信息。\n" + "下一步:先确认后端代码归属。\n" + "信息不足,缺少:负责人、仓库路径" + ) + + normalized = FeishuReplyOutboxMixin._normalize_outgoing_reply_text(raw) + + assert "🎯 结论:" in normalized + assert "🔎 依据:" in normalized + assert "➡️ 下一步:" in normalized + assert "🧩 信息不足,缺少:" in normalized + assert "\n\n🔎 依据:" in normalized + + def test_feishu_reply_llm_core_strips_action_process_prefaces() -> None: reply = ( "好的,我来帮你创建项目和信号源。\n\n" @@ -950,6 +971,58 @@ async def test_draft_confirm_handler_ignores_non_draft_reply_without_target(monk assert result["followup_reply"] == "" +@pytest.mark.asyncio +async def test_draft_confirm_handler_confirm_without_target_returns_failed_safe(monkeypatch) -> None: + kernel = type("Kernel", (), {"db": object(), "tenant_id": 1})() + handler = DraftConfirmHandler(kernel) + + async def _fake_resolve_pending_draft(**kwargs): + _ = kwargs + return None + + monkeypatch.setattr(handler, "_resolve_pending_draft", _fake_resolve_pending_draft) + + result = await handler.handle_text_reply( + message_text="确认", + sender="林宇航", + source_system="feishu", + source_message_id="msg-1", + channel_ref="chat-1", + source_sender_open_id="ou_xxx", + thread_context={"reply_to_message_id": "reply-1"}, + ) + + assert result["handled"] is True + assert result["status"] == "failed_safe" + assert "当前没有可确认草稿" in result["followup_reply"] + + +@pytest.mark.asyncio +async def test_draft_confirm_handler_natural_revision_without_target_returns_failed_safe(monkeypatch) -> None: + kernel = type("Kernel", (), {"db": object(), "tenant_id": 1})() + handler = DraftConfirmHandler(kernel) + + async def _fake_resolve_pending_draft(**kwargs): + _ = kwargs + return None + + monkeypatch.setattr(handler, "_resolve_pending_draft", _fake_resolve_pending_draft) + + result = await handler.handle_text_reply( + message_text="把目标用户改成平台运维团队", + sender="林宇航", + source_system="feishu", + source_message_id="msg-1", + channel_ref="chat-1", + source_sender_open_id="ou_xxx", + thread_context={"reply_to_message_id": "reply-1"}, + ) + + assert result["handled"] is True + assert result["status"] == "failed_safe" + assert "当前没有可确认草稿" in result["followup_reply"] + + def test_kernel_ingress_mixin_auto_safe_int_regression() -> None: assert callable(mixin_auto._safe_int) assert mixin_auto._safe_int("123") == 123 @@ -1171,13 +1244,70 @@ async def test_adaptive_rag_select_scene_prompt_prefers_project_status_for_progr assert "当前阶段" in prompt +@pytest.mark.asyncio +async def test_adaptive_rag_select_scene_prompt_feishu_direct_compacts_snapshot_into_manifest_and_focus() -> None: + mixin = _make_adaptive_rag_mixin() + system_snapshot = { + "assistant_profile": {"name": "PMFlow", "identity": "项目管理顾问"}, + "platform_profile": { + "backend": {"module_counts": {"projects": 28, "tasks": 146, "signals": 41}}, + "feature_catalog": [ + {"name": "项目管理", "route": "/projects"}, + {"name": "信号接入", "route": "/signals"}, + {"name": "任务看板", "route": "/board"}, + {"name": "知识图谱", "route": "/kg"}, + {"name": "训练中心", "route": "/training"}, + ], + }, + } + user_snapshot = { + "current_user": {"name": "林宇航"}, + "projects": [{"id": idx, "title": f"项目{idx}"} for idx in range(1, 15)], + "tasks": [{"id": idx, "title": f"任务{idx}"} for idx in range(1, 10)], + } + scene_key, prompt = await mixin._select_scene_prompt( + question="信息不全,继续提供", + project_id=7, + asked_via="feishu", + message_intent=None, + mixed_signal_part=None, + conversation_text="Q: 上一轮你提到这个需求还缺目标对象\nA: 我建议先补齐范围和交付目标", + context_blocks_text=( + "[M] 项目画像\n项目#7 Alpha(进行中);功能 18 个;KG 实体 12,关系 19。\n" + "[1] 来源: task\n相关度: 0.882\n内容: 当前需求缺少目标对象、验收口径和时间边界。\n" + "[2] 来源: project\n相关度: 0.701\n内容: 这是一个很长的项目列表噪声块,不应该整段原样进入飞书直答。" + ), + system_snapshot=system_snapshot, + user_snapshot=user_snapshot, + system_snapshot_text=json.dumps(system_snapshot, ensure_ascii=False), + user_snapshot_text=json.dumps(user_snapshot, ensure_ascii=False), + relevance_max=0.0, + ) + assert scene_key == "feishu_direct" + assert "项目画像:" in prompt + assert "项目#7 Alpha(进行中)" in prompt + assert "系统摘要:" in prompt + assert "用户焦点:" in prompt + assert "林宇航" in prompt + assert '"projects"' not in prompt + assert "项目14" not in prompt + + def test_feishu_qa_prompts_prefer_professional_answer_style() -> None: assert "先给结论" in PROMPT_FEISHU_DIRECT_QA assert "信息不足,缺少" in PROMPT_FEISHU_DIRECT_QA assert "适合飞书消息阅读" in PROMPT_FEISHU_DIRECT_QA + assert "轻量 emoji" in PROMPT_FEISHU_DIRECT_QA + assert "项目画像" in PROMPT_FEISHU_DIRECT_QA + assert "系统摘要" in PROMPT_FEISHU_DIRECT_QA + assert "用户焦点" in PROMPT_FEISHU_DIRECT_QA assert "先回答问题部分" in PROMPT_FEISHU_MIXED_QA assert "[已收到需求]" in PROMPT_FEISHU_MIXED_QA assert "专业" in PROMPT_FEISHU_MIXED_QA + assert "五轮" in PROMPT_AI_PMO_REQUIREMENT_PLAN_QA + assert "A/B/C" in PROMPT_AI_PMO_REQUIREMENT_PLAN_QA + assert "推荐技术/能力" in PROMPT_AI_PMO_REQUIREMENT_PLAN_QA + assert "当前我更推荐" in PROMPT_AI_PMO_REQUIREMENT_PLAN_QA assert "XXXX年" not in PROMPT_EXTERNAL_TREND_QA assert "通用行业认知" in PROMPT_EXTERNAL_TREND_QA assert "项目状态分析助手" in PROMPT_PROJECT_STATUS_QA diff --git a/backend/tests/test_model_routing_service.py b/backend/tests/test_model_routing_service.py index 15637645..151417af 100644 --- a/backend/tests/test_model_routing_service.py +++ b/backend/tests/test_model_routing_service.py @@ -204,6 +204,46 @@ async def test_runtime_low_override_does_not_downgrade_protected_adaptive_rag_fu assert routed is None +@pytest.mark.asyncio +async def test_runtime_low_override_allows_adaptive_rag_answer_normal_to_use_low_tier(db_env) -> None: + db_session, _maker = db_env + + await ModelRoutingService.upsert_override( + db_session, + tenant_id=1, + function_name="adaptive_rag_answer_normal", + tier_alias="low", + updated_by=9, + ) + + low_engine = { + "id": "low-1", + "provider": "glm", + "model": "qwen-plus", + "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "api_key": "low-key", + } + high_engine = { + "id": "high-1", + "provider": "openai", + "model": "GLM-5", + "base_url": "https://api.openai.com/v1", + "api_key": "high-key", + } + + routed = await llm_service._resolve_function_override_engine( + db_session, + tenant_id=1, + function_name="adaptive_rag_answer_normal", + engines=[low_engine, high_engine], + active_engine=low_engine, + ) + + assert routed is not None + assert routed["provider"] == "glm" + assert routed["model"] == "qwen-plus" + + @pytest.mark.asyncio async def test_model_routing_list_includes_current_business_entry_points(db_env) -> None: db_session, _maker = db_env diff --git a/backend/tests/test_mounted_ai_runtime_migration.py b/backend/tests/test_mounted_ai_runtime_migration.py index 48462d70..2e11447c 100644 --- a/backend/tests/test_mounted_ai_runtime_migration.py +++ b/backend/tests/test_mounted_ai_runtime_migration.py @@ -38,6 +38,21 @@ def _load_plan_state_migration_module(): return module +def _load_active_draft_migration_module(): + path = ( + Path(__file__).resolve().parents[1] + / "alembic" + / "versions" + / "20260406_add_mounted_runtime_active_draft_fields.py" + ) + spec = importlib.util.spec_from_file_location("mounted_runtime_active_draft_migration", path) + module = importlib.util.module_from_spec(spec) + assert spec is not None + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + def test_mounted_ai_runtime_migration_adds_tables() -> None: migration = _load_migration_module() engine = sa.create_engine("sqlite:///:memory:") @@ -88,3 +103,31 @@ def test_mounted_ai_runtime_plan_state_migration_adds_topic_fields() -> None: assert "conversation_scope_key" in columns assert "plan_status" in columns + + +def test_mounted_ai_runtime_active_draft_migration_adds_draft_anchor_fields() -> None: + base_migration = _load_migration_module() + plan_state_migration = _load_plan_state_migration_module() + active_draft_migration = _load_active_draft_migration_module() + engine = sa.create_engine("sqlite:///:memory:") + + with engine.begin() as conn: + context = MigrationContext.configure(conn) + base_migration.op = Operations(context) + plan_state_migration.op = Operations(context) + active_draft_migration.op = Operations(context) + + base_migration.upgrade() + plan_state_migration.upgrade() + active_draft_migration.upgrade() + + inspector = sa.inspect(conn) + columns = {column["name"] for column in inspector.get_columns("mounted_draft_workspaces")} + indexes = {index["name"] for index in inspector.get_indexes("mounted_draft_workspaces")} + + assert "active_work_item_id" in columns + assert "latest_source_message_id" in columns + assert "latest_reply_message_id" in columns + assert "ix_mounted_draft_workspaces_active_work_item_id" in indexes + assert "ix_mounted_draft_workspaces_latest_source_message_id" in indexes + assert "ix_mounted_draft_workspaces_latest_reply_message_id" in indexes diff --git a/backend/tests/test_mounted_ai_runtime_models.py b/backend/tests/test_mounted_ai_runtime_models.py index f72a5ef5..24fb1698 100644 --- a/backend/tests/test_mounted_ai_runtime_models.py +++ b/backend/tests/test_mounted_ai_runtime_models.py @@ -96,6 +96,9 @@ def test_mounted_ai_runtime_models_roundtrip_and_defaults() -> None: draft_version=1, requirement_card={"goals": ["整理复盘需求"]}, required_confirmations=[{"type": "user_confirm"}], + active_work_item_id=501, + latest_source_message_id="om_reply_source_1", + latest_reply_message_id="om_reply_sent_1", ) run = MountedConversationRun( tenant_id=1, @@ -149,6 +152,9 @@ def test_mounted_ai_runtime_models_roundtrip_and_defaults() -> None: assert saved_topic.status == "active" assert saved_draft.state == "collecting" + assert saved_draft.active_work_item_id == 501 + assert saved_draft.latest_source_message_id == "om_reply_source_1" + assert saved_draft.latest_reply_message_id == "om_reply_sent_1" assert saved_run.status == "queued" assert saved_trace.tool_trace is None assert saved_activation.cutover_mode == "mounted_only" diff --git a/backend/tests/test_mounted_ai_runtime_write_service.py b/backend/tests/test_mounted_ai_runtime_write_service.py index dc731713..68e04f6a 100644 --- a/backend/tests/test_mounted_ai_runtime_write_service.py +++ b/backend/tests/test_mounted_ai_runtime_write_service.py @@ -270,3 +270,51 @@ async def test_update_topic_plan_state_persists_explicit_plan_status_and_pending assert saved_topic.plan_status == "awaiting_execute_confirm" assert saved_topic.pending_actions == ["confirm_requirement_draft"] assert saved_topic.metadata_json["plan_topic_action"] == "draft_ready" + + +@pytest.mark.asyncio +async def test_record_semantic_shadow_merges_with_finalize_run_execution_outcome(db_session) -> None: + service = MountedAIRuntimeWriteService(db_session) + + tracking = await service.begin_ingress_run( + tenant_id=1, + source_system="feishu", + source_message_id="om_message_6", + message_text="先继续整理语义上下文,不要直接执行。", + sender="林宇航", + channel_ref="chat_semantic_shadow", + thread_context={"root_message_id": "om_root_6"}, + source_sender_open_id="ou_semantic_shadow", + ) + + await service.record_semantic_shadow( + tracking=tracking, + source_message_preview="先继续整理语义上下文,不要直接执行。", + semantic_packet_summary={"packet_version": "v1", "recent_turn_count": 2}, + semantic_decision={"topic_relation": "continue_current_topic", "mode_decision": "stay_plan"}, + semantic_gate_result={"accepted": True, "next_plan_status": "collecting"}, + semantic_diff={"changed": True, "legacy_route": "query.answer"}, + ) + await service.finalize_run( + tracking=tracking, + result={ + "handled": True, + "status": "planned", + "classification": "query", + "work_item_id": 654, + "workflow_route": "query.answer", + "direct_reply_text": "我先把语义上下文整理成 shadow 观测结果。", + }, + ) + await db_session.commit() + + saved_topic = (await db_session.execute(select(MountedTopicWorkspace))).scalar_one() + saved_run = (await db_session.execute(select(MountedConversationRun))).scalar_one() + + assert saved_topic.plan_status == "collecting" + assert saved_run.execution_outcome["handled"] is True + assert saved_run.execution_outcome["semantic_packet_summary"]["packet_version"] == "v1" + assert saved_run.execution_outcome["semantic_decision"]["mode_decision"] == "stay_plan" + assert saved_run.execution_outcome["semantic_gate_result"]["next_plan_status"] == "collecting" + assert saved_run.execution_outcome["semantic_diff"]["legacy_route"] == "query.answer" + assert saved_run.execution_outcome["source_message_preview"] == "先继续整理语义上下文,不要直接执行。" diff --git a/backend/tests/test_semantic_context_assembler.py b/backend/tests/test_semantic_context_assembler.py new file mode 100644 index 00000000..ba4353e8 --- /dev/null +++ b/backend/tests/test_semantic_context_assembler.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +import os + +import pytest +import pytest_asyncio +from sqlalchemy import JSON +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + +os.environ["DATABASE_URL"] = "postgresql+asyncpg://tester:tester@localhost:5432/pmflow_test" + +from app.models.base import Base +from app.models.mounted_ai_runtime import ( + MountedConversationRun, + MountedDraftWorkspace, + MountedTopicWorkspace, +) +from app.services.ai_pmo.semantic_context.assembler import SemanticContextAssembler + + +def _use_sqlite_json_types() -> None: + for table in ( + MountedTopicWorkspace.__table__, + MountedDraftWorkspace.__table__, + MountedConversationRun.__table__, + ): + for column in table.columns: + if column.name in { + "facts_json", + "goals_json", + "constraints_json", + "options_json", + "open_questions", + "pending_actions", + "bound_entities", + "metadata_json", + "requirement_card", + "draft_payload", + "required_confirmations", + "context_layers_hit", + "tool_calls", + "execution_outcome", + }: + column.type = JSON() + + +@pytest_asyncio.fixture +async def db_session(): + engine = create_async_engine("sqlite+aiosqlite:///:memory:") + _use_sqlite_json_types() + async with engine.begin() as conn: + await conn.run_sync( + lambda sync_conn: Base.metadata.create_all( + sync_conn, + tables=[ + MountedTopicWorkspace.__table__, + MountedDraftWorkspace.__table__, + MountedConversationRun.__table__, + ], + ) + ) + maker = async_sessionmaker(engine, expire_on_commit=False) + async with maker() as session: + yield session + await engine.dispose() + + +@pytest.mark.asyncio +async def test_semantic_context_assembler_builds_packet_from_runtime_state(db_session) -> None: + topic = MountedTopicWorkspace( + tenant_id=7, + topic_key="feishu:chat_semantic:root:om_root_semantic", + source_system="feishu", + channel_ref="chat_semantic", + conversation_scope_key="feishu:chat_semantic:ou_semantic", + thread_key="thread_semantic", + root_message_id="om_root_semantic", + mode="plan", + status="active", + plan_status="collecting", + facts_json={"title": "OpenCow 风格语义上下文迁移"}, + goals_json=["统一语义入口"], + constraints_json={"must_not": ["db.write"]}, + open_questions=["当前是不是新需求 topic"], + pending_actions=["continue_requirement_plan"], + readiness_score=0.4, + metadata_json={"last_message_preview": "这次先把语义上下文统一掉"}, + ) + db_session.add(topic) + await db_session.flush() + + draft = MountedDraftWorkspace( + tenant_id=7, + topic_workspace_id=int(topic.id), + state="collecting", + draft_version=2, + requirement_card={"goal": "统一语义入口", "scope": "先做 shadow mode"}, + draft_payload={"draft_text": "当前先保留 mounted runtime 作为底座"}, + required_confirmations=["需求草稿需要人工确认后才允许正式创建"], + ) + db_session.add(draft) + db_session.add( + MountedConversationRun( + tenant_id=7, + trace_id="mounted:feishu:prev", + topic_workspace_id=int(topic.id), + source_system="feishu", + source_message_id="om_prev", + mode_before="plan", + mode_after="plan", + active_agent="PlanCoach", + status="completed", + trigger_reason="shadow", + context_layers_hit={"topic": True}, + execution_outcome={ + "source_message_preview": "先别执行,先整理需求上下文", + "semantic_decision": {"mode_decision": "stay_plan"}, + }, + final_reply="我先把上下文和草稿整理出来。", + ) + ) + await db_session.commit() + + packet = await SemanticContextAssembler(db_session).assemble( + tenant_id=7, + operator_user_id=99, + topic_workspace_id=int(topic.id), + latest_user_message="这轮继续按 OpenCow 风格整理语义上下文。", + ) + + assert packet.packet_version == "v1" + assert packet.topic.topic_key == "feishu:chat_semantic:root:om_root_semantic" + assert packet.topic.plan_status == "collecting" + assert packet.draft is not None + assert packet.draft.draft_version == 2 + assert packet.mode_card.workflow_mode == "PLAN" + assert "draft.patch" in packet.mode_card.allowed_actions + assert "project.create" in packet.mode_card.forbidden_actions + assert packet.thread.recent_turns[0].role == "user" + assert packet.thread.recent_turns[-1].role == "assistant" + assert packet.thread.open_questions == ["当前是不是新需求 topic"] diff --git a/backend/tests/test_semantic_context_runtime_api.py b/backend/tests/test_semantic_context_runtime_api.py new file mode 100644 index 00000000..236c8239 --- /dev/null +++ b/backend/tests/test_semantic_context_runtime_api.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +import os +from types import SimpleNamespace + +import pytest +import pytest_asyncio +from fastapi import FastAPI +from httpx import ASGITransport, AsyncClient +from sqlalchemy import JSON +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + +os.environ["DATABASE_URL"] = "postgresql+asyncpg://tester:tester@localhost:5432/pmflow_test" + +from app.api.ai_steward_api import routes_mounted_ai +from app.core.database import get_db +from app.dependencies.auth import get_current_user +from app.dependencies.tenant import get_tenant_id +from app.models.base import Base +from app.models.mounted_ai_runtime import ( + MountedAgentTrace, + MountedConversationRun, + MountedDraftWorkspace, + MountedTopicWorkspace, +) + + +def _use_sqlite_json_types() -> None: + for table in ( + MountedTopicWorkspace.__table__, + MountedDraftWorkspace.__table__, + MountedConversationRun.__table__, + MountedAgentTrace.__table__, + ): + for column in table.columns: + if column.name in { + "facts_json", + "goals_json", + "constraints_json", + "options_json", + "open_questions", + "pending_actions", + "bound_entities", + "metadata_json", + "requirement_card", + "draft_payload", + "required_confirmations", + "context_layers_hit", + "tool_calls", + "execution_outcome", + "input_summary", + "output_summary", + "tool_trace", + }: + column.type = JSON() + + +@pytest_asyncio.fixture +async def db_session(): + engine = create_async_engine("sqlite+aiosqlite:///:memory:") + _use_sqlite_json_types() + async with engine.begin() as conn: + await conn.run_sync( + lambda sync_conn: Base.metadata.create_all( + sync_conn, + tables=[ + MountedTopicWorkspace.__table__, + MountedDraftWorkspace.__table__, + MountedConversationRun.__table__, + MountedAgentTrace.__table__, + ], + ) + ) + maker = async_sessionmaker(engine, expire_on_commit=False) + async with maker() as session: + yield session + await engine.dispose() + + +def _build_app(db_session): + app = FastAPI() + app.include_router(routes_mounted_ai.router) + + async def _fake_get_db(): + yield db_session + + app.dependency_overrides[get_db] = _fake_get_db + app.dependency_overrides[get_tenant_id] = lambda: 1 + app.dependency_overrides[get_current_user] = lambda: SimpleNamespace( + id=9001, + tenant_id=1, + role="ADMIN", + name="管理员", + username="admin", + ) + return app + + +@pytest.mark.asyncio +async def test_mounted_runtime_run_detail_exposes_semantic_shadow_fields(db_session) -> None: + topic = MountedTopicWorkspace( + tenant_id=1, + topic_key="feishu:chat_1:root:om_root_1", + source_system="feishu", + channel_ref="chat_1", + thread_key="thread_1", + mode="plan", + status="active", + plan_status="collecting", + facts_json={"title": "语义上下文迁移"}, + open_questions=["是否切新 topic"], + metadata_json={"last_message_preview": "这次继续统一语义入口"}, + ) + db_session.add(topic) + await db_session.flush() + + draft = MountedDraftWorkspace( + tenant_id=1, + topic_workspace_id=int(topic.id), + state="collecting", + draft_version=3, + requirement_card={"goal": "统一语义入口"}, + draft_payload={"draft_text": "先做 shadow mode"}, + required_confirmations=["需求草稿需要人工确认后才允许正式创建"], + ) + db_session.add(draft) + await db_session.flush() + + run = MountedConversationRun( + tenant_id=1, + trace_id="mounted:feishu:semantic-detail", + topic_workspace_id=int(topic.id), + source_system="feishu", + source_message_id="om_message_1", + mode_before="plan", + mode_after="plan", + active_agent="ConversationOrchestrator", + status="completed", + trigger_reason="shadow", + context_layers_hit={"topic": True, "thread": True}, + execution_outcome={ + "source_message_preview": "这轮继续按 OpenCow 风格整理语义上下文", + "semantic_packet_summary": {"packet_version": "v1", "recent_turn_count": 2}, + "semantic_decision": {"topic_relation": "continue_current_topic", "mode_decision": "stay_plan"}, + "semantic_gate_result": {"accepted": True, "next_plan_status": "collecting"}, + "semantic_diff": {"changed": True, "legacy_route": "query.answer"}, + }, + final_reply="我先把上下文整理出来。", + ) + db_session.add(run) + await db_session.commit() + + app = _build_app(db_session) + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + resp = await client.get(f"/ai-steward/mounted-ai/runs/{run.id}") + + assert resp.status_code == 200 + payload = resp.json()["data"] + assert payload["semantic_packet_summary"]["packet_version"] == "v1" + assert payload["semantic_decision"]["mode_decision"] == "stay_plan" + assert payload["semantic_gate_result"]["accepted"] is True + assert payload["semantic_diff"]["legacy_route"] == "query.answer" + assert payload["topic"]["topic_key"] == "feishu:chat_1:root:om_root_1" + assert payload["draft"]["draft_version"] == 3 diff --git a/backend/tests/test_semantic_decision_service.py b/backend/tests/test_semantic_decision_service.py new file mode 100644 index 00000000..ecf4c7c3 --- /dev/null +++ b/backend/tests/test_semantic_decision_service.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from app.services.ai_pmo.semantic_context.decision_schema import ( + DecisionGateResult, + ModeCard, + SemanticDecision, +) +from app.services.ai_pmo.semantic_context.gatekeeper import SemanticDecisionGatekeeper + + +def test_semantic_decision_gatekeeper_accepts_allowed_plan_patch() -> None: + mode_card = ModeCard( + workflow_mode="PLAN", + mode_state="ACTIVE", + allowed_actions=["reply.compose", "draft.patch", "plan.mark_ready_for_confirmation"], + forbidden_actions=["project.create", "task.create", "db.write"], + exit_conditions=["用户明确要求开始执行"], + ) + decision = SemanticDecision( + topic_relation="continue_current_topic", + mode_decision="ready_confirm", + draft_patch={"goal": "先收一版需求草稿"}, + assistant_reply="我先整理成待确认草稿。", + requires_confirmation=True, + requested_actions=["draft.patch", "reply.compose"], + confidence=0.92, + ) + + result = SemanticDecisionGatekeeper.evaluate(mode_card=mode_card, decision=decision) + + assert isinstance(result, DecisionGateResult) + assert result.accepted is True + assert result.blocked_reasons == [] + assert result.allowed_actions == ["draft.patch", "reply.compose"] + assert result.next_plan_status == "awaiting_execute_confirm" + + +def test_semantic_decision_gatekeeper_blocks_forbidden_action() -> None: + mode_card = ModeCard( + workflow_mode="PLAN", + mode_state="ACTIVE", + allowed_actions=["reply.compose", "draft.patch"], + forbidden_actions=["project.create", "task.create", "db.write"], + exit_conditions=["用户明确要求开始执行"], + ) + decision = SemanticDecision( + topic_relation="continue_current_topic", + mode_decision="request_execute", + draft_patch={}, + assistant_reply="我准备直接创建项目。", + requires_confirmation=True, + requested_actions=["project.create"], + confidence=0.73, + ) + + result = SemanticDecisionGatekeeper.evaluate(mode_card=mode_card, decision=decision) + + assert result.accepted is False + assert "forbidden_action:project.create" in result.blocked_reasons + assert result.next_plan_status == "active" diff --git a/backend/tmp_acceptance/consultant_dispatch_feishu_runs.jsonl b/backend/tmp_acceptance/consultant_dispatch_feishu_runs.jsonl index 0a307911..7b7654ae 100644 --- a/backend/tmp_acceptance/consultant_dispatch_feishu_runs.jsonl +++ b/backend/tmp_acceptance/consultant_dispatch_feishu_runs.jsonl @@ -2,3 +2,4 @@ {"generated_at": "2026-03-28T08:51:37.907500", "case": "advise", "source_message_id": "codex-feishu-advise-1774659056", "question": "统一决策层值不值得先做", "run_status": "missing", "processing": {"status": "success", "route": "ai_pmo.query.answer", "intent": "query.answer", "reply_source": "action_succeeded", "error_message": ""}, "outbox": [{"id": 654, "status": "sent", "attempts": 1, "sent_message_id": "om_x100b53462eac6ca4c2ccea9c028e82c", "text": "不值得优先做。 \n依据:当前无项目明确关联“统一决策层”需求;系统中22个活跃项目均未在名称、信号或任务中体现该概念;核心功能模块(如/ai-steward、/projects)也未定义相关能力或路由;知识图谱与代码索引中无“decision layer”“unified decision”等关键词匹配。 \n下一步:请确认该需求对应的具体项目ID、业务场景及预期交付物。信息不足,缺少:①需求提出方;②关联项目ID;③验收标准。"}], "final_reply": "不值得优先做。 \n依据:当前无项目明确关联“统一决策层”需求;系统中22个活跃项目均未在名称、信号或任务中体现该概念;核心功能模块(如/ai-steward、/projects)也未定义相关能力或路由;知识图谱与代码索引中无“decision layer”“unified decision”等关键词匹配。 \n下一步:请确认该需求对应的具体项目ID、业务场景及预期交付物。信息不足,缺少:①需求提出方;②关联项目ID;③验收标准。"} {"generated_at": "2026-03-28T08:52:16.466761", "case": "challenge", "source_message_id": "codex-feishu-challenge-1774659109", "question": "我要把整个系统一次性全部重构掉,这么做靠谱吗", "run_status": "missing", "processing": {"status": "success", "route": "ai_pmo.query.answer", "intent": "query.answer", "reply_source": "action_succeeded", "error_message": ""}, "outbox": [{"id": 655, "status": "sent", "attempts": 1, "sent_message_id": "om_x100b53462dfeb8a4b4a22d394e5186c", "text": "不靠谱。 \n依据:当前系统后端模块统计为全零(api/services/models/schemas均为0),但热点模块集中在`backend/app/api/sstc.py`等5个文件,说明核心逻辑实际集中、未模块化;前端仅6个路由,功能边界清晰;156个待处理信号中P0级3个、P1级46个,系统处于高负载运行态。 \n下一步:立即暂停重构决策,优先完成`sstc.py`模块拆解图谱与接口契约定义,并确认杨宇(唯一跨角色开发者,workload_ratio=796.67)是否参与重构设计。"}], "final_reply": "不靠谱。 \n依据:当前系统后端模块统计为全零(api/services/models/schemas均为0),但热点模块集中在`backend/app/api/sstc.py`等5个文件,说明核心逻辑实际集中、未模块化;前端仅6个路由,功能边界清晰;156个待处理信号中P0级3个、P1级46个,系统处于高负载运行态。 \n下一步:立即暂停重构决策,优先完成`sstc.py`模块拆解图谱与接口契约定义,并确认杨宇(唯一跨角色开发者,workload_ratio=796.67)是否参与重构设计。"} {"generated_at": "2026-03-28T08:53:35.385412", "case": "brainstorm", "source_message_id": "codex-feishu-brainstorm-1774659145", "question": "我们一起想几个 AI 劝退助手的方向", "run_status": "missing", "processing": {"status": "success", "route": "ai_pmo.draft_pending_confirm", "intent": "project_signal.create", "reply_source": "draft_pending_confirm", "error_message": ""}, "outbox": [{"id": 656, "status": "sent", "attempts": 1, "sent_message_id": "om_x100b5346298338a4b2c4f90aaa6bc9f", "text": "📋 我先按产品经理视角起草一版评审建议,请确认:\n\n📌 需求草稿\n我们一起想几个 AI 劝退助手的方向\n\n🔎 同名 / 相似项目:\n· #51 AI劝退助手:现有项目已有一定描述,建议对比当前需求的目标、范围和交付边界。;若范围重叠,可能造成重复建设或维护分散。;名称或目标高度接近,优先确认是否复用或合并。\n\n🚀 MVP 建议:\n· 明确项目目标与边界\n· 完成同名/相似项目查重\n· 交付最小可验证闭环\n\n一、定位\n· 一句话描述:我们一起想几个 AI 劝退助手的方向\n· 目标客户:待补充\n\n二、核心痛点\n· 待补充核心痛点\n\n三、MVP 功能范围(8周交付)\nPhase 1:核心功能(Week 1-4)\n· 明确项目目标与边界\n· 完成同名/相似项目查重\n· 交付最小可验证闭环\nPhase 2:差异化功能(Week 5-8)\n· 待补充:确认差异化方向后再展开\n\n四、技术架构(精简版)\n· 待补充技术架构\n\n五、商业模式\n· 待补充商业模式\n\n六、验证指标\n验证指标:\n· 待补充\n\n七、快速启动建议\n快速启动建议:\n· 待补充\n\n八、团队配置\n· 待补充团队配置\n\n📈 前景判断:\n· 该项目具备落地价值,但建议先确认范围边界和现有项目复用情况,再决定是否推进\n\n🧩 需求清单:\n· 项目定位与目标\n· MVP 功能范围\n\n❓ 还缺的信息:\n· 暂无\n\n✅ 可行性:待讨论\n信息仍需补充,建议先确认边界。\n\n👤 推荐负责人:系统管理员 · 当前负载100%\n基于当前负载与任务匹配度给出的建议。\n\n📅 跟进计划:\n· 明天:确认触发时机与验收口径\n· 一周后:复核执行效果并决定是否扩展\n\n---\n回复 确认 → 入库\n回复 修改 [具体修改意见] → 重新起草\n回复 取消 → 放弃\n⏰ 24小时内未确认自动取消"}], "final_reply": "📋 我先按产品经理视角起草一版评审建议,请确认:\n\n📌 需求草稿\n我们一起想几个 AI 劝退助手的方向\n\n🔎 同名 / 相似项目:\n· #51 AI劝退助手:现有项目已有一定描述,建议对比当前需求的目标、范围和交付边界。;若范围重叠,可能造成重复建设或维护分散。;名称或目标高度接近,优先确认是否复用或合并。\n\n🚀 MVP 建议:\n· 明确项目目标与边界\n· 完成同名/相似项目查重\n· 交付最小可验证闭环\n\n一、定位\n· 一句话描述:我们一起想几个 AI 劝退助手的方向\n· 目标客户:待补充\n\n二、核心痛点\n· 待补充核心痛点\n\n三、MVP 功能范围(8周交付)\nPhase 1:核心功能(Week 1-4)\n· 明确项目目标与边界\n· 完成同名/相似项目查重\n· 交付最小可验证闭环\nPhase 2:差异化功能(Week 5-8)\n· 待补充:确认差异化方向后再展开\n\n四、技术架构(精简版)\n· 待补充技术架构\n\n五、商业模式\n· 待补充商业模式\n\n六、验证指标\n验证指标:\n· 待补充\n\n七、快速启动建议\n快速启动建议:\n· 待补充\n\n八、团队配置\n· 待补充团队配置\n\n📈 前景判断:\n· 该项目具备落地价值,但建议先确认范围边界和现有项目复用情况,再决定是否推进\n\n🧩 需求清单:\n· 项目定位与目标\n· MVP 功能范围\n\n❓ 还缺的信息:\n· 暂无\n\n✅ 可行性:待讨论\n信息仍需补充,建议先确认边界。\n\n👤 推荐负责人:系统管理员 · 当前负载100%\n基于当前负载与任务匹配度给出的建议。\n\n📅 跟进计划:\n· 明天:确认触发时机与验收口径\n· 一周后:复核执行效果并决定是否扩展\n\n---\n回复 确认 → 入库\n回复 修改 [具体修改意见] → 重新起草\n回复 取消 → 放弃\n⏰ 24小时内未确认自动取消"} +{"generated_at": "2026-04-07T00:07:35.291848", "idx": 9001, "source_message_id": "real-scene-p1-manual-9001-1775491344", "question": "跟你聊个新需求,关于换季系统自动推荐搭配衣服", "message": "跟你聊个新需求,关于换季系统自动推荐搭配衣服", "capture_status": "answered", "capture_direct_reply_text": "可以,我们先不创建项目,先把这件事收成一版需求。\n\n我先把结果主要给谁用定一下,这会直接影响后面的内容粒度和表达方式。\n\nA. 核心执行人 / 内部 PM\n更适合:先服务负责跟进、拆动作、推动落地的人\n\nB. 负责人 / 管理者\n更适合:先看结论、风险和待拍板项\n\nC. 整个团队 / 终端用户\n更适合:需要直接同步所有相关方,或结果就是给最终使用者看的\n\n你回 A / B / C,也可以直接说更准确的对象。", "consultant_dispatch": null, "run_status": "missing", "ack": "", "final_reply": "可以,我们先不创建项目,先把这件事收成一版需求。\n\n我先把结果主要给谁用定一下,这会直接影响后面的内容粒度和表达方式。\n\nA. 核心执行人 / 内部 PM\n更适合:先服务负责跟进、拆动作、推动落地的人\n\nB. 负责人 / 管理者\n更适合:先看结论、风险和待拍板项\n\nC. 整个团队 / 终端用户\n更适合:需要直接同步所有相关方,或结果就是给最终使用者看的\n\n你回 A / B / C,也可以直接说更准确的对象。", "prompt_scene": "unknown", "inferred_scene": "unknown", "actual_scene": "unknown", "thinking_leak": false, "has_capacity_warning": false, "processing": {"route": "ai_pmo.query.answer", "intent": "query.answer", "status": "success", "duration_ms": 9984, "error_message": "", "reply_source": "action_succeeded"}, "outbox": [{"id": 966, "status": "pending", "attempts": 0, "created_at": "2026-04-07 00:02:34.138267", "text": "可以,我们先不创建项目,先把这件事收成一版需求。\n\n我先把结果主要给谁用定一下,这会直接影响后面的内容粒度和表达方式。\n\nA. 核心执行人 / 内部 PM\n更适合:先服务负责跟进、拆动作、推动落地的人\n\nB. 负责人 / 管理者\n更适合:先看结论、风险和待拍板项\n\nC. 整个团队 / 终端用户\n更适合:需要直接同步所有相关方,或结果就是给最终使用者看的\n\n你回 A / B / C,也可以直接说更准确的对象。"}], "call_log_candidates": [], "pass": false, "pass_rule": "unknown"} diff --git a/frontend/src/api/mountedAi.ts b/frontend/src/api/mountedAi.ts index 1d4230fc..fcd65098 100644 --- a/frontend/src/api/mountedAi.ts +++ b/frontend/src/api/mountedAi.ts @@ -94,6 +94,7 @@ export interface MountedAITopicItem { thread_key?: string | null; mode: string; status: string; + plan_status?: string | null; readiness_score?: number | null; draft_state?: string | null; draft_version?: number | null; @@ -137,12 +138,49 @@ export interface MountedAIAgentTraceItem { created_at?: string | null; } +export interface MountedAIDraftItem { + id: number; + topic_workspace_id: number; + state: string; + draft_version: number; + requirement_card?: unknown; + draft_payload?: unknown; + required_confirmations?: unknown; + last_ready_at?: string | null; + confirm_message_id?: string | null; +} + +export interface MountedAISemanticTurn { + role: string; + text: string; +} + +export interface MountedAISemanticPacketSummary { + packet_version: string; + topic_key?: string | null; + workflow_mode?: string | null; + recent_turn_count?: number | null; + draft_version?: number | null; + thread_summary?: string | null; + recent_turns?: MountedAISemanticTurn[]; + open_questions?: string[]; + mode_card?: unknown; + topic?: unknown; + draft?: unknown; +} + export interface MountedAIRunDetailPayload { item: MountedAIRunItem; + topic?: MountedAITopicItem | null; + draft?: MountedAIDraftItem | null; agent_traces: MountedAIAgentTraceItem[]; context_layers_hit?: Record | null; tool_calls?: unknown; execution_outcome?: unknown; + semantic_packet_summary?: MountedAISemanticPacketSummary | null; + semantic_decision?: unknown; + semantic_gate_result?: unknown; + semantic_diff?: unknown; } export interface MountedAICutoverStatusPayload { diff --git a/frontend/src/pages/scheduler-center/SchedulerCenterPage.tsx b/frontend/src/pages/scheduler-center/SchedulerCenterPage.tsx index 0d6a024e..8acdc293 100644 --- a/frontend/src/pages/scheduler-center/SchedulerCenterPage.tsx +++ b/frontend/src/pages/scheduler-center/SchedulerCenterPage.tsx @@ -13,6 +13,9 @@ import { MountedAIHardCasesPanel } from "./components/MountedAIHardCasesPanel"; import { MountedAIOrchestrationGraphPanel } from "./components/MountedAIOrchestrationGraphPanel"; import { MountedAIRunReplayPanel } from "./components/MountedAIRunReplayPanel"; import { MountedAIRunsPanel } from "./components/MountedAIRunsPanel"; +import { MountedAISemanticDecisionPanel } from "./components/MountedAISemanticDecisionPanel"; +import { MountedAISemanticPacketPanel } from "./components/MountedAISemanticPacketPanel"; +import { MountedAIShadowDiffPanel } from "./components/MountedAIShadowDiffPanel"; import { MountedAISourceActivationPanel } from "./components/MountedAISourceActivationPanel"; import { MountedAISourceStatusPanel } from "./components/MountedAISourceStatusPanel"; import { MountedAIToolGuardrailsPanel } from "./components/MountedAIToolGuardrailsPanel"; @@ -290,6 +293,13 @@ export default function SchedulerCenterPage() { /> +

+ + +
diff --git a/frontend/src/pages/scheduler-center/components/MountedAISemanticDecisionPanel.tsx b/frontend/src/pages/scheduler-center/components/MountedAISemanticDecisionPanel.tsx new file mode 100644 index 00000000..65f4cbcd --- /dev/null +++ b/frontend/src/pages/scheduler-center/components/MountedAISemanticDecisionPanel.tsx @@ -0,0 +1,94 @@ +import type { MountedAIRunDetailPayload } from "@/api/mountedAi"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +interface MountedAISemanticDecisionPanelProps { + detail?: MountedAIRunDetailPayload; + loading: boolean; +} + +function prettyJson(value: unknown): string { + if (value == null) { + return "暂无"; + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +export function MountedAISemanticDecisionPanel({ detail, loading }: MountedAISemanticDecisionPanelProps) { + const decision = + detail?.semantic_decision && typeof detail.semantic_decision === "object" + ? (detail.semantic_decision as Record) + : null; + const gate = + detail?.semantic_gate_result && typeof detail.semantic_gate_result === "object" + ? (detail.semantic_gate_result as Record) + : null; + + return ( + + + Semantic Decision / Gate + 展示模型如何判定 topic、mode 与 draft patch,以及 gatekeeper 最终是否放行。 + + + {loading ? ( +
+ 正在加载 semantic decision... +
+ ) : null} + + {!loading && !detail ? ( +
+ 选择一条 run 后,这里会展示 semantic decision 与 gate result。 +
+ ) : null} + + {!loading && detail ? ( + <> +
+
+
Topic Relation
+
{String(decision?.topic_relation || "-")}
+
+
+
Mode Decision
+
{String(decision?.mode_decision || "-")}
+
+
+
Gate Accepted
+
+ + {gate?.accepted ? "accepted" : "blocked"} + +
+
+
+
Next Plan Status
+
{String(gate?.next_plan_status || "-")}
+
+
+ +
+
+
Semantic Decision
+
+                  {prettyJson(detail.semantic_decision)}
+                
+
+
+
Gate Result
+
+                  {prettyJson(detail.semantic_gate_result)}
+                
+
+
+ + ) : null} +
+
+ ); +} diff --git a/frontend/src/pages/scheduler-center/components/MountedAISemanticPacketPanel.tsx b/frontend/src/pages/scheduler-center/components/MountedAISemanticPacketPanel.tsx new file mode 100644 index 00000000..27240beb --- /dev/null +++ b/frontend/src/pages/scheduler-center/components/MountedAISemanticPacketPanel.tsx @@ -0,0 +1,175 @@ +import type { MountedAIRunDetailPayload } from "@/api/mountedAi"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +interface MountedAISemanticPacketPanelProps { + detail?: MountedAIRunDetailPayload; + loading: boolean; +} + +function prettyJson(value: unknown): string { + if (value == null) { + return "暂无"; + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +export function MountedAISemanticPacketPanel({ detail, loading }: MountedAISemanticPacketPanelProps) { + const packet = detail?.semantic_packet_summary; + const recentTurns = Array.isArray(packet?.recent_turns) ? packet.recent_turns : []; + const openQuestions = Array.isArray(packet?.open_questions) ? packet.open_questions : []; + const modeCard = + packet?.mode_card && typeof packet.mode_card === "object" + ? (packet.mode_card as Record) + : null; + + return ( + + + Semantic Context Packet + 展示当前 shadow 语义包里真正喂给模型的 topic / draft / recent turns / mode card 摘要。 + + + {loading ? ( +
+ 正在加载 semantic packet... +
+ ) : null} + + {!loading && !detail ? ( +
+ 选择一条 run 后,这里会展示 semantic packet。 +
+ ) : null} + + {!loading && detail ? ( + <> +
+
+
Packet Version
+
{packet?.packet_version || "-"}
+
+
+
Workflow Mode
+
{packet?.workflow_mode || detail.topic?.mode || "-"}
+
+
+
Draft Version
+
{packet?.draft_version ?? detail.draft?.draft_version ?? "-"}
+
+
+
Plan Status
+
{detail.topic?.plan_status || "-"}
+
+
+ +
+
+
+
+
Topic / Thread
+
+ 当前挂载 runtime topic 与 thread 边界。 +
+
+ {recentTurns.length} turns +
+
+
+ Topic Key: + {detail.topic?.topic_key || packet?.topic_key || "-"} +
+
+ Thread Key: + {detail.topic?.thread_key || "-"} +
+
+ Draft State: + {detail.draft?.state || detail.topic?.draft_state || "-"} +
+
+
Thread Summary
+
+ {packet?.thread_summary || "暂无"} +
+
+
+
+ +
+
Mode Card
+ {modeCard ? ( +
+
+ {String(modeCard.workflow_mode || "-")} + {String(modeCard.mode_state || "-")} +
+
+                      {prettyJson(modeCard)}
+                    
+
+ ) : ( +
+ 当前 run 尚未记录 mode card。 +
+ )} +
+
+ +
+
+
+
Recent Turns
+ {packet?.recent_turn_count ?? recentTurns.length} +
+
+ {recentTurns.length > 0 ? ( + recentTurns.map((turn, index) => ( +
+
+ {turn.role} +
+
{turn.text}
+
+ )) + ) : ( +
+ 当前 packet 没有 recent turns。 +
+ )} +
+
+ +
+
Open Questions
+
+ {openQuestions.length > 0 ? ( + openQuestions.map((item, index) => ( +
+ {item} +
+ )) + ) : ( +
+ 当前 packet 没有待确认问题。 +
+ )} +
+
+
+ + ) : null} +
+
+ ); +} diff --git a/frontend/src/pages/scheduler-center/components/MountedAIShadowDiffPanel.tsx b/frontend/src/pages/scheduler-center/components/MountedAIShadowDiffPanel.tsx new file mode 100644 index 00000000..68305501 --- /dev/null +++ b/frontend/src/pages/scheduler-center/components/MountedAIShadowDiffPanel.tsx @@ -0,0 +1,94 @@ +import type { MountedAIRunDetailPayload } from "@/api/mountedAi"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +interface MountedAIShadowDiffPanelProps { + detail?: MountedAIRunDetailPayload; + loading: boolean; +} + +function prettyJson(value: unknown): string { + if (value == null) { + return "暂无"; + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +export function MountedAIShadowDiffPanel({ detail, loading }: MountedAIShadowDiffPanelProps) { + const diff = + detail?.semantic_diff && typeof detail.semantic_diff === "object" + ? (detail.semantic_diff as Record) + : null; + const executionOutcome = + detail?.execution_outcome && typeof detail.execution_outcome === "object" + ? (detail.execution_outcome as Record) + : null; + + return ( + + + Shadow Diff + 对比旧链路的 legacy route 与新语义链路的 shadow 结论,用于切主链前验收。 + + + {loading ? ( +
+ 正在加载 shadow diff... +
+ ) : null} + + {!loading && !detail ? ( +
+ 选择一条 run 后,这里会展示 shadow diff。 +
+ ) : null} + + {!loading && detail ? ( + <> +
+
+
Legacy Route
+
{String(diff?.legacy_route || executionOutcome?.workflow_route || "-")}
+
+
+
Legacy Intent
+
{String(diff?.legacy_message_intent || "-")}
+
+
+
Shadow Changed
+
+ + {diff?.changed ? "changed" : "same"} + +
+
+
+
Shadow Error
+
{String(diff?.shadow_error || "-")}
+
+
+ +
+
+
Shadow Diff Payload
+
+                  {prettyJson(detail.semantic_diff)}
+                
+
+
+
Legacy Execution Outcome
+
+                  {prettyJson(detail.execution_outcome)}
+                
+
+
+ + ) : null} +
+
+ ); +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index e91bcd75..5d455ac2 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/skillradarchart.tsx","./src/components/themeinit.tsx","./src/components/ai-config/configsubpageheader.tsx","./src/components/ai-config/featureintroguide.tsx","./src/components/ai-steward/floatingqachat.tsx","./src/components/ai-steward/promptrolesettings.tsx","./src/components/ai-steward/qahistorypanel.tsx","./src/components/ai-steward/qamarkdowncontent.tsx","./src/components/budget/budgetcontrolpanel.tsx","./src/components/command/commanditems.ts","./src/components/inbox/inboxactionpanel.tsx","./src/components/inbox/inboxlistitem.tsx","./src/components/layout/layout.tsx","./src/components/layout/sidebar.tsx","./src/components/layout/topbar.tsx","./src/components/model-capability/costoptimizationpanel.tsx","./src/components/model-capability/experimentsyncpanel.tsx","./src/components/model-capability/modelcapabilitymatrix.tsx","./src/components/model-capability/modelupdateimpactpanel.tsx","./src/components/sstc/aivisibilitypanel.tsx","./src/components/sstc/feedbacksummarycard.tsx","./src/components/sstc/sstcchainview.tsx","./src/components/sstc/sstccharterstagepanel.tsx","./src/components/sstc/sstcexecutionboardpanel.tsx","./src/components/sstc/sstcglobalheader.tsx","./src/components/sstc/sstcreleasecreatedialog.tsx","./src/components/sstc/sstcreleasestagepanel.tsx","./src/components/sstc/sstcreviewcreatedialog.tsx","./src/components/sstc/sstcreviewstagepanel.tsx","./src/components/sstc/sstcriskfloatpanel.tsx","./src/components/sstc/sstcsignalstagepanel.tsx","./src/components/sstc/sstcstagenav.tsx","./src/components/sstc/sstctaskcreatedialog.tsx","./src/components/sstc/sstctaskstagepanel.tsx","./src/components/sstc/sstcworkspace.tsx","./src/components/sstc/signalcontenthovercard.tsx","./src/components/sstc/chartermodules.ts","./src/components/sstc/sstcutils.ts","./src/components/sstc/usesstcworkspaceorchestration.ts","./src/components/ui/rolebadge.tsx","./src/components/ui/skillbadge.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/users/userformdialog.tsx","./src/hooks/usetheme.ts","./src/lib/api.ts","./src/lib/apierror.ts","./src/lib/datetime.ts","./src/lib/display.ts","./src/lib/feishueventmode.ts","./src/lib/formatinboxtime.ts","./src/lib/inbox.ts","./src/lib/inboxtextmap.ts","./src/lib/rbac.ts","./src/lib/sessionguard.ts","./src/lib/title.ts","./src/lib/utils.ts","./src/lib/zhcn.ts","./src/pages/aiconfigpage.tsx","./src/pages/aistewardpage.tsx","./src/pages/charterdetailpage.tsx","./src/pages/contractsyncboardpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/feishuconfigpage.tsx","./src/pages/inboxpage.tsx","./src/pages/knowledgegraphcontextboardpage.tsx","./src/pages/loginpage.tsx","./src/pages/modelroutingconfigpage.tsx","./src/pages/projectboardpage.tsx","./src/pages/projectspage.tsx","./src/pages/recipespage.tsx","./src/pages/schedulercenterpage.tsx","./src/pages/skilltaskspage.tsx","./src/pages/skillspage.tsx","./src/pages/taskbriefcenterpage.tsx","./src/pages/taskbriefrunspage.tsx","./src/pages/taskbrieftemplatepage.tsx","./src/pages/userprofilepage.tsx","./src/pages/userskillspage.tsx","./src/pages/userspage.tsx","./src/pages/workloaddetailpage.tsx","./src/pages/workloadspage.tsx","./src/pages/ai-config/aiconfigpage.tsx","./src/pages/ai-config/constants.ts","./src/pages/ai-config/types.ts","./src/pages/ai-config/components/governancetoolscard.tsx","./src/pages/ai-config/components/prioritybadge.tsx","./src/pages/ai-config/components/statusbadge.tsx","./src/pages/ai-config/sections/advancedtab.tsx","./src/pages/ai-config/sections/configuretab.tsx","./src/pages/ai-config/sections/knowledgetab.tsx","./src/pages/ai-config/sections/overviewtab.tsx","./src/pages/ai-steward/aistewardpage.tsx","./src/pages/ai-steward/constants.ts","./src/pages/ai-steward/types.ts","./src/pages/ai-steward/components/briefingdetaildrawer.tsx","./src/pages/ai-steward/components/briefingprintdocument.tsx","./src/pages/ai-steward/components/briefingsectioncard.tsx","./src/pages/ai-steward/components/featureoverviewcards.tsx","./src/pages/ai-steward/components/feedback-center/feedbackdetail.tsx","./src/pages/ai-steward/components/feedback-center/feedbacklist.tsx","./src/pages/ai-steward/components/feedback-center/metricspanel.tsx","./src/pages/ai-steward/components/feedback-center/reasonpicker.tsx","./src/pages/ai-steward/components/feedback-center/trendpanel.tsx","./src/pages/ai-steward/sections/blockerstab.tsx","./src/pages/ai-steward/sections/briefingtab.tsx","./src/pages/ai-steward/sections/conflictstab.tsx","./src/pages/ai-steward/sections/createprojecttab.tsx","./src/pages/ai-steward/sections/enginetab.tsx","./src/pages/ai-steward/sections/feedbackcentertab.tsx","./src/pages/ai-steward/sections/overviewtab.tsx","./src/pages/ai-steward/sections/signalstab.tsx","./src/pages/charter-detail/charterdetailpage.tsx","./src/pages/contract-sync-board/contractsyncboardpage.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/feishu-config/feishuconfigpage.tsx","./src/pages/feishu-config/helpers.ts","./src/pages/feishu-config/components/setupguidecard.tsx","./src/pages/inbox/inboxpage.tsx","./src/pages/inbox/helpers.ts","./src/pages/inbox/components/inboxfilterspanel.tsx","./src/pages/knowledge-graph-context-board/knowledgegraphcontextboardpage.tsx","./src/pages/login/loginpage.tsx","./src/pages/model-routing-config/modelroutingconfigpage.tsx","./src/pages/project-board/projectboardpage.tsx","./src/pages/project-board/helpers.ts","./src/pages/project-board/components/createcharterdialog.tsx","./src/pages/project-board/components/projectsettingsdialog.tsx","./src/pages/projects/projectspage.tsx","./src/pages/projects/helpers.ts","./src/pages/projects/components/deleteprojectdialog.tsx","./src/pages/projects/components/projectquerydialog.tsx","./src/pages/recipes/recipespage.tsx","./src/pages/recipes/helpers.ts","./src/pages/recipes/components/analyzerecipedialog.tsx","./src/pages/recipes/components/createrecipedialog.tsx","./src/pages/scheduler-center/schedulercenterpage.tsx","./src/pages/scheduler-center/components/mountedaiacceptancepanel.tsx","./src/pages/scheduler-center/components/mountedaicontextlayerspanel.tsx","./src/pages/scheduler-center/components/mountedaievalscoreboardpanel.tsx","./src/pages/scheduler-center/components/mountedaihardcasespanel.tsx","./src/pages/scheduler-center/components/mountedaiorchestrationgraphpanel.tsx","./src/pages/scheduler-center/components/mountedairunreplaypanel.tsx","./src/pages/scheduler-center/components/mountedairunspanel.tsx","./src/pages/scheduler-center/components/mountedaisourceactivationpanel.tsx","./src/pages/scheduler-center/components/mountedaisourcestatuspanel.tsx","./src/pages/scheduler-center/components/mountedaitoolguardrailspanel.tsx","./src/pages/scheduler-center/components/mountedaitopicspanel.tsx","./src/pages/scheduler-center/components/mountedaitrainingopspanel.tsx","./src/pages/skill-tasks/skilltaskspage.tsx","./src/pages/skills/skillspage.tsx","./src/pages/task-brief-center/taskbriefcenterpage.tsx","./src/pages/task-brief-runs/taskbriefrunspage.tsx","./src/pages/task-brief-template/taskbrieftemplatepage.tsx","./src/pages/user-profile/userprofilepage.tsx","./src/pages/user-skills/userskillspage.tsx","./src/pages/users/userspage.tsx","./src/pages/users/helpers.ts","./src/pages/users/components/bulkassigndialog.tsx","./src/pages/users/components/deleteuserdialog.tsx","./src/pages/users/components/importusersdialog.tsx","./src/pages/users/components/resetpassworddialog.tsx","./src/pages/workload-detail/workloaddetailpage.tsx","./src/pages/workloads/workloadspage.tsx","./src/router/index.tsx","./src/stores/auth.ts","./src/types/inbox.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/skillradarchart.tsx","./src/components/themeinit.tsx","./src/components/ai-config/configsubpageheader.tsx","./src/components/ai-config/featureintroguide.tsx","./src/components/ai-steward/floatingqachat.tsx","./src/components/ai-steward/promptrolesettings.tsx","./src/components/ai-steward/qahistorypanel.tsx","./src/components/ai-steward/qamarkdowncontent.tsx","./src/components/budget/budgetcontrolpanel.tsx","./src/components/command/commanditems.ts","./src/components/inbox/inboxactionpanel.tsx","./src/components/inbox/inboxlistitem.tsx","./src/components/layout/layout.tsx","./src/components/layout/sidebar.tsx","./src/components/layout/topbar.tsx","./src/components/model-capability/costoptimizationpanel.tsx","./src/components/model-capability/experimentsyncpanel.tsx","./src/components/model-capability/modelcapabilitymatrix.tsx","./src/components/model-capability/modelupdateimpactpanel.tsx","./src/components/sstc/aivisibilitypanel.tsx","./src/components/sstc/feedbacksummarycard.tsx","./src/components/sstc/sstcchainview.tsx","./src/components/sstc/sstccharterstagepanel.tsx","./src/components/sstc/sstcexecutionboardpanel.tsx","./src/components/sstc/sstcglobalheader.tsx","./src/components/sstc/sstcreleasecreatedialog.tsx","./src/components/sstc/sstcreleasestagepanel.tsx","./src/components/sstc/sstcreviewcreatedialog.tsx","./src/components/sstc/sstcreviewstagepanel.tsx","./src/components/sstc/sstcriskfloatpanel.tsx","./src/components/sstc/sstcsignalstagepanel.tsx","./src/components/sstc/sstcstagenav.tsx","./src/components/sstc/sstctaskcreatedialog.tsx","./src/components/sstc/sstctaskstagepanel.tsx","./src/components/sstc/sstcworkspace.tsx","./src/components/sstc/signalcontenthovercard.tsx","./src/components/sstc/chartermodules.ts","./src/components/sstc/sstcutils.ts","./src/components/sstc/usesstcworkspaceorchestration.ts","./src/components/ui/rolebadge.tsx","./src/components/ui/skillbadge.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toast.tsx","./src/components/users/userformdialog.tsx","./src/hooks/usetheme.ts","./src/lib/api.ts","./src/lib/apierror.ts","./src/lib/datetime.ts","./src/lib/display.ts","./src/lib/feishueventmode.ts","./src/lib/formatinboxtime.ts","./src/lib/inbox.ts","./src/lib/inboxtextmap.ts","./src/lib/rbac.ts","./src/lib/sessionguard.ts","./src/lib/title.ts","./src/lib/utils.ts","./src/lib/zhcn.ts","./src/pages/aiconfigpage.tsx","./src/pages/aistewardpage.tsx","./src/pages/charterdetailpage.tsx","./src/pages/contractsyncboardpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/feishuconfigpage.tsx","./src/pages/inboxpage.tsx","./src/pages/knowledgegraphcontextboardpage.tsx","./src/pages/loginpage.tsx","./src/pages/modelroutingconfigpage.tsx","./src/pages/projectboardpage.tsx","./src/pages/projectspage.tsx","./src/pages/recipespage.tsx","./src/pages/schedulercenterpage.tsx","./src/pages/skilltaskspage.tsx","./src/pages/skillspage.tsx","./src/pages/taskbriefcenterpage.tsx","./src/pages/taskbriefrunspage.tsx","./src/pages/taskbrieftemplatepage.tsx","./src/pages/userprofilepage.tsx","./src/pages/userskillspage.tsx","./src/pages/userspage.tsx","./src/pages/workloaddetailpage.tsx","./src/pages/workloadspage.tsx","./src/pages/ai-config/aiconfigpage.tsx","./src/pages/ai-config/constants.ts","./src/pages/ai-config/types.ts","./src/pages/ai-config/components/governancetoolscard.tsx","./src/pages/ai-config/components/prioritybadge.tsx","./src/pages/ai-config/components/statusbadge.tsx","./src/pages/ai-config/sections/advancedtab.tsx","./src/pages/ai-config/sections/configuretab.tsx","./src/pages/ai-config/sections/knowledgetab.tsx","./src/pages/ai-config/sections/overviewtab.tsx","./src/pages/ai-steward/aistewardpage.tsx","./src/pages/ai-steward/constants.ts","./src/pages/ai-steward/types.ts","./src/pages/ai-steward/components/briefingdetaildrawer.tsx","./src/pages/ai-steward/components/briefingprintdocument.tsx","./src/pages/ai-steward/components/briefingsectioncard.tsx","./src/pages/ai-steward/components/featureoverviewcards.tsx","./src/pages/ai-steward/components/feedback-center/feedbackdetail.tsx","./src/pages/ai-steward/components/feedback-center/feedbacklist.tsx","./src/pages/ai-steward/components/feedback-center/metricspanel.tsx","./src/pages/ai-steward/components/feedback-center/reasonpicker.tsx","./src/pages/ai-steward/components/feedback-center/trendpanel.tsx","./src/pages/ai-steward/sections/blockerstab.tsx","./src/pages/ai-steward/sections/briefingtab.tsx","./src/pages/ai-steward/sections/conflictstab.tsx","./src/pages/ai-steward/sections/createprojecttab.tsx","./src/pages/ai-steward/sections/enginetab.tsx","./src/pages/ai-steward/sections/feedbackcentertab.tsx","./src/pages/ai-steward/sections/overviewtab.tsx","./src/pages/ai-steward/sections/signalstab.tsx","./src/pages/charter-detail/charterdetailpage.tsx","./src/pages/contract-sync-board/contractsyncboardpage.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/feishu-config/feishuconfigpage.tsx","./src/pages/feishu-config/helpers.ts","./src/pages/feishu-config/components/setupguidecard.tsx","./src/pages/inbox/inboxpage.tsx","./src/pages/inbox/helpers.ts","./src/pages/inbox/components/inboxfilterspanel.tsx","./src/pages/knowledge-graph-context-board/knowledgegraphcontextboardpage.tsx","./src/pages/login/loginpage.tsx","./src/pages/model-routing-config/modelroutingconfigpage.tsx","./src/pages/project-board/projectboardpage.tsx","./src/pages/project-board/helpers.ts","./src/pages/project-board/components/createcharterdialog.tsx","./src/pages/project-board/components/projectsettingsdialog.tsx","./src/pages/projects/projectspage.tsx","./src/pages/projects/helpers.ts","./src/pages/projects/components/deleteprojectdialog.tsx","./src/pages/projects/components/projectquerydialog.tsx","./src/pages/recipes/recipespage.tsx","./src/pages/recipes/helpers.ts","./src/pages/recipes/components/analyzerecipedialog.tsx","./src/pages/recipes/components/createrecipedialog.tsx","./src/pages/scheduler-center/schedulercenterpage.tsx","./src/pages/scheduler-center/components/mountedaiacceptancepanel.tsx","./src/pages/scheduler-center/components/mountedaicontextlayerspanel.tsx","./src/pages/scheduler-center/components/mountedaievalscoreboardpanel.tsx","./src/pages/scheduler-center/components/mountedaihardcasespanel.tsx","./src/pages/scheduler-center/components/mountedaiorchestrationgraphpanel.tsx","./src/pages/scheduler-center/components/mountedairunreplaypanel.tsx","./src/pages/scheduler-center/components/mountedairunspanel.tsx","./src/pages/scheduler-center/components/mountedaisemanticdecisionpanel.tsx","./src/pages/scheduler-center/components/mountedaisemanticpacketpanel.tsx","./src/pages/scheduler-center/components/mountedaishadowdiffpanel.tsx","./src/pages/scheduler-center/components/mountedaisourceactivationpanel.tsx","./src/pages/scheduler-center/components/mountedaisourcestatuspanel.tsx","./src/pages/scheduler-center/components/mountedaitoolguardrailspanel.tsx","./src/pages/scheduler-center/components/mountedaitopicspanel.tsx","./src/pages/scheduler-center/components/mountedaitrainingopspanel.tsx","./src/pages/skill-tasks/skilltaskspage.tsx","./src/pages/skills/skillspage.tsx","./src/pages/task-brief-center/taskbriefcenterpage.tsx","./src/pages/task-brief-runs/taskbriefrunspage.tsx","./src/pages/task-brief-template/taskbrieftemplatepage.tsx","./src/pages/user-profile/userprofilepage.tsx","./src/pages/user-skills/userskillspage.tsx","./src/pages/users/userspage.tsx","./src/pages/users/helpers.ts","./src/pages/users/components/bulkassigndialog.tsx","./src/pages/users/components/deleteuserdialog.tsx","./src/pages/users/components/importusersdialog.tsx","./src/pages/users/components/resetpassworddialog.tsx","./src/pages/workload-detail/workloaddetailpage.tsx","./src/pages/workloads/workloadspage.tsx","./src/router/index.tsx","./src/stores/auth.ts","./src/types/inbox.ts"],"version":"5.6.3"} \ No newline at end of file

EEsxSIe>gV-{Ko3 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 n+d%8C=0a^DB$T$O{ze0#$~TmLDlc^B_9+a5 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