feat: rwfx 任务深链支持目的树闸门 + 任务↔对话(1:N)绑定
后端 - config.yaml 新增 task_deeplink 段:完整 URL(无 base)+ test/delete_test 两个独立测试开关 + mock 假数据(目的树 / delete 结果,可随时编辑) - 新增 TaskDeeplinkConfig 模型挂到 AppConfig;新增公开只读接口 GET /api/public/config/task-deeplink 暴露给前端(/api/public 已在鉴权白名单) 前端 - 登录深链 goPath=rwfx 进入「任务工作区」:左侧按 metadata.taskId 过滤的对话列表 (新建对话 / 编辑 / 下一步 / 清除任务记录)+ 右侧问答 - 新建对话(含首次进入)顶部展示目的树闸门:有数据弹覆盖确认→调 delete,success 才开始问答; 恢复历史对话不跑闸门 - 新建 thread 打 metadata.taskId 标签实现「一个任务挂多个对话」(1:N) - 地址/开关/假数据全部走后端配置,拿不到时用内置默认值兜底;test/delete_test=true 走假数据 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5b610fd807
commit
a2d767e2db
15 files changed
+907
-36
No files matched your search
@@ -51,6 +51,7 @@ from app.gateway.routers import (
|
||||
parallel_agent_tasks,
|
||||
parallel_agents,
|
||||
positions,
|
||||
public_config,
|
||||
public_embed,
|
||||
public_leaderboard,
|
||||
public_scheduled_tasks,
|
||||
@@ -1368,6 +1369,7 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
|
||||
|
||||
# Public embed Q&A status polling (/api/public/embed/*)
|
||||
app.include_router(public_embed.router)
|
||||
app.include_router(public_config.router)
|
||||
app.include_router(html_page_favorites.router)
|
||||
|
||||
# Product knowledge base API is mounted at /api/knowledge
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""公开只读配置接口(前端运行时读取 config.yaml 派生值)。
|
||||
|
||||
目前只暴露 ``task_deeplink`` 段(任务深链 rwfx 的目的树闸门地址 + 测试开关 +
|
||||
测试假数据),供前端登录深链流程在运行时读取——无需把这些地址硬编码进前端,
|
||||
改 ``config.yaml`` 即时生效(Gateway mtime 热重载)。
|
||||
|
||||
**无鉴权**:路由挂在 ``/api/public/`` 前缀下,已被
|
||||
``auth_middleware._PUBLIC_PATH_PREFIXES`` 白名单放行。段内只有地址与开关,不含密钥。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from deerflow.config import get_app_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/public/config", tags=["public-config"])
|
||||
|
||||
|
||||
@router.get("/task-deeplink")
|
||||
async def get_task_deeplink_config() -> dict:
|
||||
"""返回 ``config.yaml`` 的 ``task_deeplink`` 段(含 test/delete_test/各 URL/mock)。"""
|
||||
config = get_app_config()
|
||||
return config.task_deeplink.model_dump()
|
||||
@@ -15,10 +15,10 @@ from deerflow.config.checkpointer_config import CheckpointerConfig, load_checkpo
|
||||
from deerflow.config.database_config import DatabaseConfig
|
||||
from deerflow.config.extensions_config import ExtensionsConfig
|
||||
from deerflow.config.gateway_config import GatewayConfig
|
||||
from deerflow.config.guardrails_config import GuardrailsConfig, load_guardrails_config_from_dict
|
||||
from deerflow.config.knowledge_config import KnowledgeConfig
|
||||
from deerflow.config.knowledge_ingest_config import KnowledgeIngestConfig
|
||||
from deerflow.config.log_system_config import LogSystemConfig
|
||||
from deerflow.config.guardrails_config import GuardrailsConfig, load_guardrails_config_from_dict
|
||||
from deerflow.config.knowledge_config import KnowledgeConfig
|
||||
from deerflow.config.knowledge_ingest_config import KnowledgeIngestConfig
|
||||
from deerflow.config.log_system_config import LogSystemConfig
|
||||
from deerflow.config.login_config import AuthLoginConfig
|
||||
from deerflow.config.memory_config import MemoryConfig, load_memory_config_from_dict
|
||||
from deerflow.config.model_config import ModelConfig
|
||||
@@ -33,6 +33,7 @@ from deerflow.config.skills_config import SkillsConfig
|
||||
from deerflow.config.stream_bridge_config import StreamBridgeConfig, load_stream_bridge_config_from_dict
|
||||
from deerflow.config.subagents_config import SubagentsAppConfig, load_subagents_config_from_dict
|
||||
from deerflow.config.summarization_config import SummarizationConfig, load_summarization_config_from_dict
|
||||
from deerflow.config.task_deeplink_config import TaskDeeplinkConfig
|
||||
from deerflow.config.title_config import TitleConfig, load_title_config_from_dict
|
||||
from deerflow.config.token_usage_config import TokenUsageConfig
|
||||
from deerflow.config.tool_config import ToolConfig, ToolGroupConfig
|
||||
@@ -136,9 +137,10 @@ class AppConfig(BaseModel):
|
||||
run_events: RunEventsConfig = Field(default_factory=RunEventsConfig, description="Run event storage configuration")
|
||||
checkpointer: CheckpointerConfig | None = Field(default=None, description="Checkpointer configuration")
|
||||
stream_bridge: StreamBridgeConfig | None = Field(default=None, description="Stream bridge configuration")
|
||||
browser_context: BrowserContextConfig = Field(default_factory=BrowserContextConfig, description="Optional browser page capture integration")
|
||||
knowledge: KnowledgeConfig = Field(default_factory=KnowledgeConfig, description="Product knowledge base (obsidian-wiki compatible vault) configuration")
|
||||
knowledge_ingest: KnowledgeIngestConfig = Field(default_factory=KnowledgeIngestConfig, description="External knowledge-ingest endpoint configuration")
|
||||
browser_context: BrowserContextConfig = Field(default_factory=BrowserContextConfig, description="Optional browser page capture integration")
|
||||
knowledge: KnowledgeConfig = Field(default_factory=KnowledgeConfig, description="Product knowledge base (obsidian-wiki compatible vault) configuration")
|
||||
knowledge_ingest: KnowledgeIngestConfig = Field(default_factory=KnowledgeIngestConfig, description="External knowledge-ingest endpoint configuration")
|
||||
task_deeplink: TaskDeeplinkConfig = Field(default_factory=TaskDeeplinkConfig, description="任务深链(rwfx)目的树闸门与跳转地址配置(前端经 /api/public/config/task-deeplink 读取)")
|
||||
|
||||
@classmethod
|
||||
def resolve_config_path(cls, config_path: str | None = None) -> Path:
|
||||
@@ -387,26 +389,26 @@ def _get_config_mtime(config_path: Path) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def _load_and_cache_app_config(config_path: str | None = None) -> AppConfig:
|
||||
"""Load config from disk and refresh cache metadata."""
|
||||
global _app_config, _app_config_path, _app_config_mtime, _app_config_skill_evolution_overrides_mtime, _app_config_is_custom
|
||||
|
||||
resolved_path = AppConfig.resolve_config_path(config_path)
|
||||
_app_config = AppConfig.from_file(str(resolved_path))
|
||||
_app_config_path = resolved_path
|
||||
_app_config_mtime = _get_config_mtime(resolved_path)
|
||||
_app_config_skill_evolution_overrides_mtime = _get_skill_evolution_overrides_mtime()
|
||||
_app_config_is_custom = False
|
||||
summarization = _app_config.summarization
|
||||
logger.info(
|
||||
"Loaded AppConfig from %s; summarization enabled=%s trigger=%s keep=%s trim_tokens_to_summarize=%s",
|
||||
resolved_path,
|
||||
summarization.enabled,
|
||||
summarization.trigger,
|
||||
summarization.keep,
|
||||
summarization.trim_tokens_to_summarize,
|
||||
)
|
||||
return _app_config
|
||||
def _load_and_cache_app_config(config_path: str | None = None) -> AppConfig:
|
||||
"""Load config from disk and refresh cache metadata."""
|
||||
global _app_config, _app_config_path, _app_config_mtime, _app_config_skill_evolution_overrides_mtime, _app_config_is_custom
|
||||
|
||||
resolved_path = AppConfig.resolve_config_path(config_path)
|
||||
_app_config = AppConfig.from_file(str(resolved_path))
|
||||
_app_config_path = resolved_path
|
||||
_app_config_mtime = _get_config_mtime(resolved_path)
|
||||
_app_config_skill_evolution_overrides_mtime = _get_skill_evolution_overrides_mtime()
|
||||
_app_config_is_custom = False
|
||||
summarization = _app_config.summarization
|
||||
logger.info(
|
||||
"Loaded AppConfig from %s; summarization enabled=%s trigger=%s keep=%s trim_tokens_to_summarize=%s",
|
||||
resolved_path,
|
||||
summarization.enabled,
|
||||
summarization.trigger,
|
||||
summarization.keep,
|
||||
summarization.trim_tokens_to_summarize,
|
||||
)
|
||||
return _app_config
|
||||
|
||||
|
||||
def get_app_config() -> AppConfig:
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
"""任务深链(``goPath=rwfx``)的「目的树闸门 + 跳转」配置。
|
||||
|
||||
前端(登录深链流程)通过公开只读接口 ``GET /api/public/config/task-deeplink``
|
||||
读取这一段:所有外部地址都是**完整 URL**(调用时仅追加 ``?taskId=`` / ``?id=``),
|
||||
随时改 ``config.yaml`` 即可(Gateway 有 mtime 热重载,无需重新构建前端)。
|
||||
|
||||
测试开关(线上接口未通时用假数据,互相独立):
|
||||
- ``test`` → 目的树查询(``purpose_detail_url``)走 ``mock.purpose_detail`` 假数据。
|
||||
- ``delete_test`` → 删除接口(``delete_url``)走 ``mock.delete`` 假数据(该接口尚未开发完时打开)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class TaskDeeplinkConfig(BaseModel):
|
||||
"""rwfx 任务深链的目的树查询 / 删除接口与跳转页地址。"""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
test: bool = Field(
|
||||
default=False,
|
||||
description="为真时前端目的树查询走 mock.purpose_detail 假数据(不打真实接口)",
|
||||
)
|
||||
delete_test: bool = Field(
|
||||
default=False,
|
||||
description="为真时前端删除接口走 mock.delete 假数据(删除接口未开发完时打开)",
|
||||
)
|
||||
purpose_detail_url: str = Field(
|
||||
default="",
|
||||
description="目的树查询完整 URL,调用时追加 ?taskId=",
|
||||
)
|
||||
delete_url: str = Field(
|
||||
default="",
|
||||
description="删除完整 URL,调用时追加 ?taskId=",
|
||||
)
|
||||
treemap_edit_url: str = Field(
|
||||
default="",
|
||||
description="B 系统树形完整编辑页 URL,跳转时追加 ?id=",
|
||||
)
|
||||
treemap_next_url: str = Field(
|
||||
default="",
|
||||
description="B 系统树形下一步页 URL,跳转时追加 ?id=",
|
||||
)
|
||||
mock: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="test/delete_test 为真时返回的假数据:{purpose_detail: {...}, delete: {message, success}}",
|
||||
)
|
||||
@@ -324,3 +324,44 @@ summarization:
|
||||
type: tokens
|
||||
value: 40000
|
||||
trim_tokens_to_summarize: 8000
|
||||
|
||||
# ───────────────────────────────────────────────────────────────────────────
|
||||
# 任务深链(goPath=rwfx):目的树闸门 + 问答后跳转。前端经公开只读接口
|
||||
# GET /api/public/config/task-deeplink 读取。地址都是完整 URL(调用时仅追加
|
||||
# ?taskId= / ?id=),随时改这里即可(Gateway mtime 热重载,前端无需重建)。
|
||||
task_deeplink:
|
||||
# 线上接口未通时用假数据;两个开关互相独立。
|
||||
test: true # 目的树查询走 mock.purpose_detail 假数据
|
||||
delete_test: true # 删除接口走 mock.delete 假数据(该接口尚未开发完)
|
||||
purpose_detail_url: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/getTaskPurposeDetail"
|
||||
delete_url: "https://ch1.b.uat.4.cn/consumer/taskAnalyseSearch/delete"
|
||||
treemap_edit_url: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-edit"
|
||||
treemap_next_url: "https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-mode-treemap-full-next"
|
||||
mock:
|
||||
# test=true 时原样返回给前端(结构同真实 getTaskPurposeDetail 返回)。
|
||||
purpose_detail:
|
||||
state: "200"
|
||||
msg: "操作成功!"
|
||||
data:
|
||||
- purposeDto: { id: 5837, zzPurpose: "目的01", selectedFlag: 1, taskId: 3324, recDetailId: 4271 }
|
||||
childrenList:
|
||||
- actorDto: { id: 9101, taskId: 3324, purposeId: 5837, actorName: "行为体01", actorOrgan: "行为体01", recReason: "行为体01理由", selectedFlag: 1 }
|
||||
childrenList:
|
||||
- expectedBehaviorDto: { id: 6910, taskId: 3324, purposeId: 5837, behavior: "预期01", recReason: "预期01理由", selectedFlag: "1" }
|
||||
childrenList:
|
||||
- drivingFactorDto: { id: 7964, taskId: 3324, purposeId: 5837, drivingFactor: "预期01因素", selectedFlag: 1 }
|
||||
childrenList:
|
||||
- narrationDto: { id: 6500, purposeId: 5837, taskId: 3324, narration: "叙事01", recReason: "叙事01理由", selectedFlag: 1 }
|
||||
childrenList: []
|
||||
- actorDto: { id: 9102, taskId: 3324, purposeId: 5837, actorName: "行为体02", actorOrgan: "行为体02", recReason: "行为体02理由", selectedFlag: 1 }
|
||||
childrenList:
|
||||
- expectedBehaviorDto: { id: 6911, taskId: 3324, purposeId: 5837, behavior: "预期02", recReason: "预期02理由", selectedFlag: "1" }
|
||||
childrenList:
|
||||
- drivingFactorDto: { id: 7965, taskId: 3324, purposeId: 5837, drivingFactor: "预期02因素", selectedFlag: 1 }
|
||||
childrenList:
|
||||
- narrationDto: { id: 6501, purposeId: 5837, taskId: 3324, narration: "叙事02", recReason: "叙事02理由", selectedFlag: 1 }
|
||||
childrenList: []
|
||||
# delete_test=true 时原样返回给前端。改 success 测「成功覆盖 / 不可覆盖」两条分支。
|
||||
delete:
|
||||
message: "行动已经开始策划,不可覆盖"
|
||||
success: true
|
||||
Reference in new issue
Block a user