Merge branch 'dhk-1' into 'master'

Dhk 1

See merge request knowledge-base-bj/deerflow-server!34
This commit is contained in:
董宏凯 committed 2026-05-21 09:23:14 +00:00
commit 03e0d8248d
3 files changed
+89 -134

No files matched your search

+4 -102
View File
@@ -2,7 +2,6 @@ import { useEffect, useState } from "react";
import type { ElementType } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import {
BarChart3,
Bell,
Bot,
Brain,
@@ -11,7 +10,6 @@ import {
ChevronRight,
ChevronsUpDown,
CircleHelp,
Compass,
Database,
FileBox,
FileText,
@@ -27,15 +25,11 @@ import {
ListTodo,
MessageSquareCode,
MessageSquarePlus,
MoreHorizontal,
Network,
Plus,
Scale,
Settings,
Sparkles,
Tags,
Target,
TrendingUp,
User,
Users,
Wrench,
@@ -200,97 +194,14 @@ const menuItems: MenuItem[] = [
},
],
},
{
id: "result-management",
label: "结果管理",
icon: BarChart3,
children: [
{
id: "result-planning",
label: "规划结果",
icon: Target,
children: [
{ id: "result-planning-all", label: "全部", icon: BarChart3 },
{ id: "result-planning-goal", label: "目标分析", icon: Target },
{ id: "result-planning-brand", label: "品牌策略", icon: Target },
{ id: "result-planning-content", label: "内容规划", icon: Target },
{ id: "result-planning-channel", label: "渠道规划", icon: Target },
{ id: "result-planning-narrative", label: "叙事策略", icon: Target },
],
},
{
id: "result-mnty",
label: "MNTY结果",
icon: Brain,
children: [
{ id: "result-mnty-all", label: "全部", icon: BarChart3 },
{ id: "result-mnty-mechanism", label: "机制研究", icon: Brain },
{ id: "result-mnty-plan", label: "规划设计", icon: Brain },
{ id: "result-mnty-zsty", label: "ZSTY分析", icon: Brain },
],
},
{
id: "result-effect-pg",
label: "效果PG结果",
icon: TrendingUp,
children: [
{ id: "result-effect-pg-all", label: "全部", icon: BarChart3 },
{ id: "result-effect-pg-behavior", label: "行为PG", icon: TrendingUp },
{ id: "result-effect-pg-evaluation", label: "效果复盘", icon: TrendingUp },
{ id: "result-effect-pg-info", label: "信息PG", icon: TrendingUp },
{ id: "result-effect-pg-account", label: "账号PG", icon: TrendingUp },
],
},
{
id: "result-xk",
label: "XK结果",
icon: Compass,
children: [
{ id: "result-xk-all", label: "全部", icon: BarChart3 },
{ id: "result-xk-jc", label: "任务JC", icon: Compass },
{ id: "result-xk-dispatch", label: "任务调度", icon: Compass },
],
},
{
id: "result-dq",
label: "DQ结果",
icon: Database,
children: [
{ id: "result-dq-all", label: "全部", icon: BarChart3 },
{ id: "result-dq-audience", label: "受众分析", icon: Database },
{ id: "result-dq-kb", label: "知识研究", icon: Database },
{ id: "result-dq-trend", label: "趋势研究", icon: Database },
],
},
{
id: "result-other",
label: "其他结果",
icon: MoreHorizontal,
children: [
{ id: "result-other-all", label: "全部", icon: BarChart3 },
{ id: "result-other-general", label: "通用分析", icon: MoreHorizontal },
{ id: "result-other-temp", label: "临时任务", icon: MoreHorizontal },
{ id: "result-other-other", label: "其他", icon: MoreHorizontal },
],
},
],
},
{
id: "resource-management",
label: "资源管理",
icon: Database,
children: [
{
id: "resource-tags",
label: "标签管理",
icon: Tags,
path: "/page/strategy/admin/tags",
adminOnly: true,
},
{ id: "resource-zz", label: "ZZ资源", icon: FileBox },
{ id: "resource-network-actor", label: "网络达人", icon: Network },
{ id: "resource-knowledge-base", label: "知识库", icon: Library },
{ id: "resource-evaluation-index", label: "评估指标", icon: Scale },
{ id: "resource-network-xwt", label: "网络xwt资源管理", icon: Network },
{ id: "resource-knowledge-base", label: "知识库资源管理", icon: Library },
{ id: "resource-zz", label: "ZZ资源管理", icon: FileBox },
],
},
];
@@ -299,8 +210,6 @@ const defaultExpandedIds = new Set([
"qa-center",
"task-management",
"xk-center-task",
"result-management",
"result-planning",
]);
function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] {
@@ -490,15 +399,8 @@ export function PageSidebar({
isDark ? "text-white" : "text-gray-900"
}`}
>
DeerFlow
CM助手
</h1>
<p
className={`text-xs theme-transition ${
isDark ? "text-blue-400/60" : "text-red-400/80"
}`}
>
Page Navigation
</p>
</div>
)}
</div>
@@ -17,6 +17,24 @@ import { useCanvasGraphContext } from '../../contexts/CanvasGraphContext';
import { useAIWriting } from '../../hooks/useAIWritingStream';
import { useAIWritingSessions } from '../../hooks/useAIWritingSessions';
import { adminCleanupAIWritingSessions } from '../../api/ai-writing-sessions';
/**
* 暂停 / 继续功能开关(默认 false = 关)。
*
* 历史背景:legacy 路径下,为了让用户点"停止"后还能"继续",引入了 per-worker
* 内存 dict(``_event_queues`` / ``_session_pending_pause``)。多 worker 部署下
* 这些 dict 互相看不见 → SSE 重连失败 / "会话丢失" → 触发了整个 ai-writing-优化.md
* 的迁移计划。
*
* 当前 LangGraph Server 路径下架构已无 per-worker 状态,但 ``stream.stop()`` 仅
* 取消客户端 SSE 订阅,**不会真正暂停后端 run**(LLM 继续烧 token);``resume``
* 也无法重新订阅同一个 run。这是个产品语义没想清楚的功能,先关掉。
*
* 后期想清楚怎么做"真·暂停"(``client.runs.cancel(run_id, action='interrupt')``
* 保留 checkpoint)+ "真·继续"(``client.runs.joinStream`` 续上原 run)时,
* 把这个 flag 翻成 true 即可,UI / hook 全套代码都已就位。
*/
const ENABLE_PAUSE_RESUME_UI = false;
import { AIWritingForm } from './AIWritingForm';
import { AIWritingTimeline } from './AIWritingTimeline';
import { InterventionCard } from './InterventionCard';
@@ -269,7 +287,9 @@ export function AIWritingPanel() {
新建
</Button>
)}
{isRunning && !state.isPaused && (
{/* ⏸️/▶️ "停止 / 继续" 由 ENABLE_PAUSE_RESUME_UI 控制(默认 false 隐藏)。
后期把 flag 翻成 true 即可恢复,UI / hook 全套代码都已就位。 */}
{ENABLE_PAUSE_RESUME_UI && isRunning && !state.isPaused && (
<Button
variant="ghost"
size="sm"
@@ -280,7 +300,7 @@ export function AIWritingPanel() {
停止
</Button>
)}
{state.isPaused && (
{ENABLE_PAUSE_RESUME_UI && state.isPaused && (
<>
<Button
variant="ghost"
@@ -415,7 +435,7 @@ export function AIWritingPanel() {
isError ? 'text-destructive' : 'text-muted-foreground',
)}
>
{state.isPaused ? (
{ENABLE_PAUSE_RESUME_UI && state.isPaused ? (
<>
<span className="inline-block size-1.5 rounded-full bg-amber-400" />
已暂停
@@ -19,8 +19,8 @@ from ._utils import llm_json, safe_get_stream_writer
logger = logging.getLogger(__name__)
# 素材总量上限边界:config.yaml 的 ai_writing.max_materials,非法值回退默认 20
_DEFAULT_MAX_MATERIALS = 20
# 素材总量上限边界:config.yaml 的 ai_writing.max_materials,非法值回退默认 50
_DEFAULT_MAX_MATERIALS = 50
_MIN_MAX_MATERIALS = 1
_MAX_MAX_MATERIALS = 100
@@ -93,12 +93,18 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
article_type = state.get("article_type", "analysis")
target_audience = state.get("target_audience", "普通读者")
# 检查是否是补充搜索(PAUSE-1 re_search action)
# 检查干预模式(PAUSE-1 素材确认 / 素材不足求助)
intervention = state.get("last_user_intervention") or {}
extra_keywords: list[str] = intervention.get("_extra_keywords") or intervention.get("extra_keywords") or []
action = intervention.get("action")
# 素材不足求助点触发的「补充检索」:保留原大纲,检索完直接回写作、不走素材确认/大纲
is_supplement = intervention.get("action") == "supplement_search"
is_supplement = action == "supplement_search"
# PAUSE-1 素材确认卡用户点「追加检索」并填了新关键词:
# - 跳过 LLM 重新生成检索词(节省一次调用 + 尊重用户意图)
# - 只检索 extra_keywords 这几个新词,不重检索之前 LLM 出过的词
# - 保留 existing material_package,按 URL 去重合并
is_append_only = action == "re_search" and bool(extra_keywords)
# 用户指定的检索关键词数量(1~10),决定生成多少个检索词、检索多少个方向
keyword_count = _resolve_keyword_count(state.get("keyword_count"))
@@ -119,32 +125,43 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
writer({"type": "research_progress", "message": message})
# Step 1: 提取关键词
_emit("正在生成检索词…")
kw_result = await llm_json(
EXTRACT_KEYWORDS_SYSTEM,
EXTRACT_KEYWORDS_USER.format(
user_intent=intent,
article_type=article_type,
target_audience=target_audience,
keyword_count=keyword_count,
),
fallback={"keywords": [intent]},
config=config,
model_name=keyword_model,
)
# 清洗关键词:剔除占位符 / 空值,全空时回退写作意图
keywords: list[str] = _clean_keywords(kw_result.get("keywords"), intent)
if extra_keywords:
keywords = list(dict.fromkeys(keywords + extra_keywords))
kw_result: dict = {} # 走追加分支时跳过 LLM 调用,但下方 notice 拼接需要兜底值
if is_append_only:
# 追加模式:直接用用户填的关键词,不再调 LLM
keywords: list[str] = _clean_keywords(extra_keywords, intent)
_emit(f"追加检索 {len(keywords)} 个关键词…")
else:
_emit("正在生成检索词…")
kw_result = await llm_json(
EXTRACT_KEYWORDS_SYSTEM,
EXTRACT_KEYWORDS_USER.format(
user_intent=intent,
article_type=article_type,
target_audience=target_audience,
keyword_count=keyword_count,
),
fallback={"keywords": [intent]},
config=config,
model_name=keyword_model,
)
# 清洗关键词:剔除占位符 / 空值,全空时回退写作意图
keywords = _clean_keywords(kw_result.get("keywords"), intent)
if extra_keywords:
keywords = list(dict.fromkeys(keywords + extra_keywords))
# Step 2: 并发搜索
provider = get_search_provider(ai_writing_config)
# 按用户指定的关键词数量截断;补充搜索时不截断用户额外添加的关键词
search_limit = keyword_count
if extra_keywords:
search_limit = max(search_limit, len(keywords))
search_keywords = keywords[:search_limit]
# 按用户指定的关键词数量截断。
# 追加模式:keywords 本身就是用户填的,全部检索不截断(用户填 N 个就搜 N 个)。
# 其他模式:受 keyword_count 限制,但 extra_keywords 在内时上限放宽到 keywords 总数。
if is_append_only:
search_keywords = keywords
else:
search_limit = keyword_count
if extra_keywords:
search_limit = max(search_limit, len(keywords))
search_keywords = keywords[:search_limit]
total = len(search_keywords)
# 每个检索词分到的检索条数 = 总量上限 / 检索词数(向上取整)
per_keyword = _per_keyword_limit(max_materials, total)
@@ -168,10 +185,14 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
all_results_nested: list[list[SearchResult]] = await asyncio.gather(*search_tasks, return_exceptions=True)
# 合并去重
# - 首次检索 / 普通 re_search(不带 extra_keywords,用户只想 LLM 重出):旧素材丢弃
# - 素材不足求助 supplement_search:保留旧素材 + 追加新检索结果
# - PAUSE-1 追加检索 is_append_only:保留旧素材 + 仅检索新关键词
seen_urls: set[str] = set()
raw_materials: list[Material] = []
existing_pkg = state.get("material_package")
if existing_pkg and intervention.get("action") != "re_search":
preserve_existing = existing_pkg and (is_supplement or is_append_only)
if preserve_existing:
for m in existing_pkg.get("materials", []):
seen_urls.add(m.get("url", ""))
raw_materials.append(m)
@@ -235,7 +256,13 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
if not materials:
summary = "未检索到相关素材,可重新搜索或在无素材的情况下继续写作"
pkg = MaterialPackage(keywords=keywords, materials=materials, summary=summary)
# 追加模式下,pkg.keywords 体现到目前为止累积的检索词(旧 + 新追加),
# 让前端列表 / 历史回显能看到完整的搜索路径。
pkg_keywords = keywords
if is_append_only and existing_pkg:
old_kws = list(existing_pkg.get("keywords") or [])
pkg_keywords = list(dict.fromkeys(old_kws + keywords))
pkg = MaterialPackage(keywords=pkg_keywords, materials=materials, summary=summary)
# 容错告警:检索接口 / 模型调用出错时以非致命 notice 提示用户,不打断写作流程
notices: list[dict] = []
@@ -253,12 +280,18 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
raw="\n\n----------\n\n".join(raw_dumps[:3]),
))
# 进度事件文案:追加模式下让用户看到只搜了哪几个新词
step_start_msg = (
f"追加检索关键词:{', '.join(keywords[:3])}"
if is_append_only
else f"搜索关键词:{', '.join(keywords[:3])}"
)
return {
"material_package": pkg,
# 补充检索:直接回写作(沿用原大纲);常规检索:进入素材确认暂停点
"status": "writing_draft" if is_supplement else "awaiting_material_confirm",
"progress_events": [
{"type": "step_start", "agent_name": "素材收集专家", "message": f"搜索关键词:{', '.join(keywords[:3])}"},
{"type": "step_start", "agent_name": "素材收集专家", "message": step_start_msg},
*notices,
{"type": "materials_ready", "agent_name": "素材收集专家", "message": summary},
],