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 type { ElementType } from "react";
|
||||||
import { useNavigate, useLocation } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
BarChart3,
|
|
||||||
Bell,
|
Bell,
|
||||||
Bot,
|
Bot,
|
||||||
Brain,
|
Brain,
|
||||||
@@ -11,7 +10,6 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
Compass,
|
|
||||||
Database,
|
Database,
|
||||||
FileBox,
|
FileBox,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -27,15 +25,11 @@ import {
|
|||||||
ListTodo,
|
ListTodo,
|
||||||
MessageSquareCode,
|
MessageSquareCode,
|
||||||
MessageSquarePlus,
|
MessageSquarePlus,
|
||||||
MoreHorizontal,
|
|
||||||
Network,
|
Network,
|
||||||
Plus,
|
Plus,
|
||||||
Scale,
|
|
||||||
Settings,
|
Settings,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Tags,
|
|
||||||
Target,
|
Target,
|
||||||
TrendingUp,
|
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
Wrench,
|
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",
|
id: "resource-management",
|
||||||
label: "资源管理",
|
label: "资源管理",
|
||||||
icon: Database,
|
icon: Database,
|
||||||
children: [
|
children: [
|
||||||
{
|
{ id: "resource-network-xwt", label: "网络xwt资源管理", icon: Network },
|
||||||
id: "resource-tags",
|
{ id: "resource-knowledge-base", label: "知识库资源管理", icon: Library },
|
||||||
label: "标签管理",
|
{ id: "resource-zz", label: "ZZ资源管理", icon: FileBox },
|
||||||
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 },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -299,8 +210,6 @@ const defaultExpandedIds = new Set([
|
|||||||
"qa-center",
|
"qa-center",
|
||||||
"task-management",
|
"task-management",
|
||||||
"xk-center-task",
|
"xk-center-task",
|
||||||
"result-management",
|
|
||||||
"result-planning",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] {
|
function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] {
|
||||||
@@ -490,15 +399,8 @@ export function PageSidebar({
|
|||||||
isDark ? "text-white" : "text-gray-900"
|
isDark ? "text-white" : "text-gray-900"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
DeerFlow
|
CM助手
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
|
||||||
className={`text-xs theme-transition ${
|
|
||||||
isDark ? "text-blue-400/60" : "text-red-400/80"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Page Navigation
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,24 @@ import { useCanvasGraphContext } from '../../contexts/CanvasGraphContext';
|
|||||||
import { useAIWriting } from '../../hooks/useAIWritingStream';
|
import { useAIWriting } from '../../hooks/useAIWritingStream';
|
||||||
import { useAIWritingSessions } from '../../hooks/useAIWritingSessions';
|
import { useAIWritingSessions } from '../../hooks/useAIWritingSessions';
|
||||||
import { adminCleanupAIWritingSessions } from '../../api/ai-writing-sessions';
|
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 { AIWritingForm } from './AIWritingForm';
|
||||||
import { AIWritingTimeline } from './AIWritingTimeline';
|
import { AIWritingTimeline } from './AIWritingTimeline';
|
||||||
import { InterventionCard } from './InterventionCard';
|
import { InterventionCard } from './InterventionCard';
|
||||||
@@ -269,7 +287,9 @@ export function AIWritingPanel() {
|
|||||||
新建
|
新建
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isRunning && !state.isPaused && (
|
{/* ⏸️/▶️ "停止 / 继续" 由 ENABLE_PAUSE_RESUME_UI 控制(默认 false 隐藏)。
|
||||||
|
后期把 flag 翻成 true 即可恢复,UI / hook 全套代码都已就位。 */}
|
||||||
|
{ENABLE_PAUSE_RESUME_UI && isRunning && !state.isPaused && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -280,7 +300,7 @@ export function AIWritingPanel() {
|
|||||||
停止
|
停止
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{state.isPaused && (
|
{ENABLE_PAUSE_RESUME_UI && state.isPaused && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -415,7 +435,7 @@ export function AIWritingPanel() {
|
|||||||
isError ? 'text-destructive' : 'text-muted-foreground',
|
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" />
|
<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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 素材总量上限边界:config.yaml 的 ai_writing.max_materials,非法值回退默认 20
|
# 素材总量上限边界:config.yaml 的 ai_writing.max_materials,非法值回退默认 50
|
||||||
_DEFAULT_MAX_MATERIALS = 20
|
_DEFAULT_MAX_MATERIALS = 50
|
||||||
_MIN_MAX_MATERIALS = 1
|
_MIN_MAX_MATERIALS = 1
|
||||||
_MAX_MAX_MATERIALS = 100
|
_MAX_MAX_MATERIALS = 100
|
||||||
|
|
||||||
@@ -93,12 +93,18 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
article_type = state.get("article_type", "analysis")
|
article_type = state.get("article_type", "analysis")
|
||||||
target_audience = state.get("target_audience", "普通读者")
|
target_audience = state.get("target_audience", "普通读者")
|
||||||
|
|
||||||
# 检查是否是补充搜索(PAUSE-1 re_search action)
|
# 检查干预模式(PAUSE-1 素材确认 / 素材不足求助)
|
||||||
intervention = state.get("last_user_intervention") or {}
|
intervention = state.get("last_user_intervention") or {}
|
||||||
extra_keywords: list[str] = intervention.get("_extra_keywords") or intervention.get("extra_keywords") 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),决定生成多少个检索词、检索多少个方向
|
# 用户指定的检索关键词数量(1~10),决定生成多少个检索词、检索多少个方向
|
||||||
keyword_count = _resolve_keyword_count(state.get("keyword_count"))
|
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})
|
writer({"type": "research_progress", "message": message})
|
||||||
|
|
||||||
# Step 1: 提取关键词
|
# Step 1: 提取关键词
|
||||||
_emit("正在生成检索词…")
|
kw_result: dict = {} # 走追加分支时跳过 LLM 调用,但下方 notice 拼接需要兜底值
|
||||||
kw_result = await llm_json(
|
if is_append_only:
|
||||||
EXTRACT_KEYWORDS_SYSTEM,
|
# 追加模式:直接用用户填的关键词,不再调 LLM
|
||||||
EXTRACT_KEYWORDS_USER.format(
|
keywords: list[str] = _clean_keywords(extra_keywords, intent)
|
||||||
user_intent=intent,
|
_emit(f"追加检索 {len(keywords)} 个关键词…")
|
||||||
article_type=article_type,
|
else:
|
||||||
target_audience=target_audience,
|
_emit("正在生成检索词…")
|
||||||
keyword_count=keyword_count,
|
kw_result = await llm_json(
|
||||||
),
|
EXTRACT_KEYWORDS_SYSTEM,
|
||||||
fallback={"keywords": [intent]},
|
EXTRACT_KEYWORDS_USER.format(
|
||||||
config=config,
|
user_intent=intent,
|
||||||
model_name=keyword_model,
|
article_type=article_type,
|
||||||
)
|
target_audience=target_audience,
|
||||||
# 清洗关键词:剔除占位符 / 空值,全空时回退写作意图
|
keyword_count=keyword_count,
|
||||||
keywords: list[str] = _clean_keywords(kw_result.get("keywords"), intent)
|
),
|
||||||
if extra_keywords:
|
fallback={"keywords": [intent]},
|
||||||
keywords = list(dict.fromkeys(keywords + extra_keywords))
|
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: 并发搜索
|
# Step 2: 并发搜索
|
||||||
provider = get_search_provider(ai_writing_config)
|
provider = get_search_provider(ai_writing_config)
|
||||||
|
|
||||||
# 按用户指定的关键词数量截断;补充搜索时不截断用户额外添加的关键词
|
# 按用户指定的关键词数量截断。
|
||||||
search_limit = keyword_count
|
# 追加模式:keywords 本身就是用户填的,全部检索不截断(用户填 N 个就搜 N 个)。
|
||||||
if extra_keywords:
|
# 其他模式:受 keyword_count 限制,但 extra_keywords 在内时上限放宽到 keywords 总数。
|
||||||
search_limit = max(search_limit, len(keywords))
|
if is_append_only:
|
||||||
search_keywords = keywords[:search_limit]
|
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)
|
total = len(search_keywords)
|
||||||
# 每个检索词分到的检索条数 = 总量上限 / 检索词数(向上取整)
|
# 每个检索词分到的检索条数 = 总量上限 / 检索词数(向上取整)
|
||||||
per_keyword = _per_keyword_limit(max_materials, total)
|
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)
|
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()
|
seen_urls: set[str] = set()
|
||||||
raw_materials: list[Material] = []
|
raw_materials: list[Material] = []
|
||||||
existing_pkg = state.get("material_package")
|
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", []):
|
for m in existing_pkg.get("materials", []):
|
||||||
seen_urls.add(m.get("url", ""))
|
seen_urls.add(m.get("url", ""))
|
||||||
raw_materials.append(m)
|
raw_materials.append(m)
|
||||||
@@ -235,7 +256,13 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
if not materials:
|
if not materials:
|
||||||
summary = "未检索到相关素材,可重新搜索或在无素材的情况下继续写作"
|
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 提示用户,不打断写作流程
|
# 容错告警:检索接口 / 模型调用出错时以非致命 notice 提示用户,不打断写作流程
|
||||||
notices: list[dict] = []
|
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]),
|
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 {
|
return {
|
||||||
"material_package": pkg,
|
"material_package": pkg,
|
||||||
# 补充检索:直接回写作(沿用原大纲);常规检索:进入素材确认暂停点
|
# 补充检索:直接回写作(沿用原大纲);常规检索:进入素材确认暂停点
|
||||||
"status": "writing_draft" if is_supplement else "awaiting_material_confirm",
|
"status": "writing_draft" if is_supplement else "awaiting_material_confirm",
|
||||||
"progress_events": [
|
"progress_events": [
|
||||||
{"type": "step_start", "agent_name": "素材收集专家", "message": f"搜索关键词:{', '.join(keywords[:3])}"},
|
{"type": "step_start", "agent_name": "素材收集专家", "message": step_start_msg},
|
||||||
*notices,
|
*notices,
|
||||||
{"type": "materials_ready", "agent_name": "素材收集专家", "message": summary},
|
{"type": "materials_ready", "agent_name": "素材收集专家", "message": summary},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in new issue
Block a user