Merge branch 'dhk-1' into 'master'
Dhk 1 See merge request knowledge-base-bj/deerflow-server!34
This commit is contained in:
commit
03e0d8248d
3 files changed
+89
-134
No files matched your search
@@ -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" />
|
||||
已暂停
|
||||
|
||||
+62
-29
@@ -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},
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user