buggggg
This commit is contained in:
1 parent
e54f42c0eb
commit
b8d3a549df
3 files changed
+74
-6
No files matched your search
@@ -256,6 +256,10 @@ export function AIWritingProvider({ children }: { children: ReactNode }) {
|
|||||||
const stream = streamRef.current;
|
const stream = streamRef.current;
|
||||||
if (!stream) return;
|
if (!stream) return;
|
||||||
try {
|
try {
|
||||||
|
// 所见即所得:用户在表单选了什么 materialSource,前端就原样传过去。
|
||||||
|
// 三个合法值:knowledge_base / skill / notebook,后端 state.py 接受任意 str。
|
||||||
|
// 与 material_source 独立的 notebook_search_space_id / notebook_source_ids
|
||||||
|
// 控制是否真的去笔记后端拉素材(仅当用户选了"我的笔记"模式时由表单填充)。
|
||||||
await stream.submit(
|
await stream.submit(
|
||||||
{
|
{
|
||||||
user_intent: request.userIntent,
|
user_intent: request.userIntent,
|
||||||
@@ -266,7 +270,9 @@ export function AIWritingProvider({ children }: { children: ReactNode }) {
|
|||||||
word_count_target: request.wordCountTarget,
|
word_count_target: request.wordCountTarget,
|
||||||
keyword_count: Math.max(1, Math.min(10, request.keywordCount)),
|
keyword_count: Math.max(1, Math.min(10, request.keywordCount)),
|
||||||
writing_mode: request.writingMode === 'strict' ? 'strict' : 'loose',
|
writing_mode: request.writingMode === 'strict' ? 'strict' : 'loose',
|
||||||
material_source: request.materialSource === 'skill' ? 'skill' : 'knowledge_base',
|
material_source: request.materialSource,
|
||||||
|
notebook_search_space_id: request.noteSearchSpaceId ?? null,
|
||||||
|
notebook_source_ids: request.noteSourceIds ?? null,
|
||||||
revision_count: 0,
|
revision_count: 0,
|
||||||
max_revisions: 3,
|
max_revisions: 3,
|
||||||
status: 'researching',
|
status: 'researching',
|
||||||
|
|||||||
@@ -291,6 +291,17 @@ export default function StudioNotebookDetailPage() {
|
|||||||
await send(text, selectedSourceIds);
|
await send(text, selectedSourceIds);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clarification 卡片的「发送」按钮回调。MessageList 内部把用户选中的选项 +
|
||||||
|
// 自由文本合并成一段 answer 传过来,这里走和普通发送同一条 send() 链路,
|
||||||
|
// 把当前选中的来源也一并带上,保持上下文一致。
|
||||||
|
const handleClarificationSubmit = useCallback(
|
||||||
|
async (answer: string) => {
|
||||||
|
if (isStreaming) return;
|
||||||
|
await send(answer, selectedSourceIds);
|
||||||
|
},
|
||||||
|
[send, selectedSourceIds, isStreaming],
|
||||||
|
);
|
||||||
|
|
||||||
const handleBackToList = () => {
|
const handleBackToList = () => {
|
||||||
setActiveThreadId(null);
|
setActiveThreadId(null);
|
||||||
setThreadId(undefined);
|
setThreadId(undefined);
|
||||||
@@ -655,6 +666,7 @@ export default function StudioNotebookDetailPage() {
|
|||||||
thread={thread}
|
thread={thread}
|
||||||
paddingBottom={120}
|
paddingBottom={120}
|
||||||
extraRehypePlugins={citationRehypePlugins}
|
extraRehypePlugins={citationRehypePlugins}
|
||||||
|
onClarificationSubmit={handleClarificationSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+55
-5
@@ -109,20 +109,42 @@ async def _search_notebook(
|
|||||||
"""从笔记 API 按关键词检索内容,返回 source_type='notebook' 的 Material 列表。"""
|
"""从笔记 API 按关键词检索内容,返回 source_type='notebook' 的 Material 列表。"""
|
||||||
import httpx # 懒导入:仅在笔记模式下才需要
|
import httpx # 懒导入:仅在笔记模式下才需要
|
||||||
|
|
||||||
|
# 入参全 dump(token 只打长度,不打明文,防止泄漏到日志)
|
||||||
|
logger.info(
|
||||||
|
"[notebook-search] called keywords=%s search_space_id=%s source_ids=%s token_len=%d",
|
||||||
|
keywords,
|
||||||
|
search_space_id,
|
||||||
|
source_ids,
|
||||||
|
len(token) if token else 0,
|
||||||
|
)
|
||||||
|
|
||||||
base = (os.getenv("DEER_FLOW_NOTEBOOK_API_BASE") or "").rstrip("/")
|
base = (os.getenv("DEER_FLOW_NOTEBOOK_API_BASE") or "").rstrip("/")
|
||||||
if not base:
|
if not base:
|
||||||
logger.warning("DEER_FLOW_NOTEBOOK_API_BASE not set, skipping notebook search")
|
# 改成 error 级别,warning 在很多部署里默认会被过滤掉看不到
|
||||||
|
logger.error(
|
||||||
|
"[notebook-search] DEER_FLOW_NOTEBOOK_API_BASE not set, skipping notebook search. "
|
||||||
|
"请在后端启动环境里设这个环境变量(指向笔记后端根地址,例如 http://localhost:8003)。"
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
full_url = f"{base}{_NOTEBOOK_SEARCH_PATH}"
|
||||||
|
logger.info("[notebook-search] base=%s full_url=%s", base, full_url)
|
||||||
|
|
||||||
results: list[Material] = []
|
results: list[Material] = []
|
||||||
mat_idx = 0
|
mat_idx = 0
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=20) as client:
|
async with httpx.AsyncClient(timeout=20) as client:
|
||||||
for kw in keywords[:_NOTEBOOK_MAX_KEYWORDS]:
|
for kw in keywords[:_NOTEBOOK_MAX_KEYWORDS]:
|
||||||
|
logger.info("[notebook-search] POST kw=%r body=%s", kw, {
|
||||||
|
"query": kw,
|
||||||
|
"source_ids": source_ids,
|
||||||
|
"search_space_id": search_space_id,
|
||||||
|
"top_k": _NOTEBOOK_TOP_K,
|
||||||
|
})
|
||||||
try:
|
try:
|
||||||
|
# /mentioned-documents/search 接口无需鉴权,不传 Authorization header
|
||||||
resp = await client.post(
|
resp = await client.post(
|
||||||
f"{base}{_NOTEBOOK_SEARCH_PATH}",
|
full_url,
|
||||||
headers={"Authorization": f"Bearer {token}"},
|
|
||||||
json={
|
json={
|
||||||
"query": kw,
|
"query": kw,
|
||||||
"source_ids": source_ids,
|
"source_ids": source_ids,
|
||||||
@@ -130,10 +152,29 @@ async def _search_notebook(
|
|||||||
"top_k": _NOTEBOOK_TOP_K,
|
"top_k": _NOTEBOOK_TOP_K,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
logger.info(
|
||||||
|
"[notebook-search] kw=%r http_status=%d body_len=%d",
|
||||||
|
kw,
|
||||||
|
resp.status_code,
|
||||||
|
len(resp.content or b""),
|
||||||
|
)
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
# 把错误响应体打出来,方便定位 401 / 422 等
|
||||||
|
logger.error(
|
||||||
|
"[notebook-search] kw=%r HTTP %d response: %s",
|
||||||
|
kw,
|
||||||
|
resp.status_code,
|
||||||
|
(resp.text or "")[:1000],
|
||||||
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
# 解析 documents 列表,每个 chunk 作为独立素材
|
# 解析 documents 列表,每个 chunk 作为独立素材
|
||||||
documents: list[dict] = data.get("documents") if isinstance(data, dict) else []
|
documents: list[dict] = data.get("documents") if isinstance(data, dict) else []
|
||||||
|
logger.info(
|
||||||
|
"[notebook-search] kw=%r parsed documents=%d",
|
||||||
|
kw,
|
||||||
|
len(documents or []),
|
||||||
|
)
|
||||||
for doc in documents or []:
|
for doc in documents or []:
|
||||||
doc_title: str = doc.get("title") or "笔记文档"
|
doc_title: str = doc.get("title") or "笔记文档"
|
||||||
doc_url: str = doc.get("url") or ""
|
doc_url: str = doc.get("url") or ""
|
||||||
@@ -154,10 +195,11 @@ async def _search_notebook(
|
|||||||
url=doc_url,
|
url=doc_url,
|
||||||
))
|
))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("notebook search kw=%r failed: %s", kw, e)
|
logger.warning("[notebook-search] kw=%r failed: %s", kw, e, exc_info=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("notebook search client error: %s", e)
|
logger.warning("[notebook-search] client error: %s", e, exc_info=True)
|
||||||
|
|
||||||
|
logger.info("[notebook-search] done — returning %d materials", len(results))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@@ -250,6 +292,7 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
# Step 1.5: 笔记检索(如果传入了 notebook_search_space_id)
|
# Step 1.5: 笔记检索(如果传入了 notebook_search_space_id)
|
||||||
notebook_search_space_id: int | None = state.get("notebook_search_space_id")
|
notebook_search_space_id: int | None = state.get("notebook_search_space_id")
|
||||||
notebook_materials: list[Material] = []
|
notebook_materials: list[Material] = []
|
||||||
|
|
||||||
if notebook_search_space_id:
|
if notebook_search_space_id:
|
||||||
notebook_source_ids: list[int] = state.get("notebook_source_ids") or []
|
notebook_source_ids: list[int] = state.get("notebook_source_ids") or []
|
||||||
notebook_token: str = cfg.get("notebook_token") or ""
|
notebook_token: str = cfg.get("notebook_token") or ""
|
||||||
@@ -369,6 +412,13 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
# 技能模式不再走下面的并发/合并逻辑,all_results_nested 留空、跳过 web 通用搜索
|
# 技能模式不再走下面的并发/合并逻辑,all_results_nested 留空、跳过 web 通用搜索
|
||||||
all_results_nested: list = []
|
all_results_nested: list = []
|
||||||
# 把已收集到的素材挂在 raw_materials_pre,下方合并阶段直接挪过去
|
# 把已收集到的素材挂在 raw_materials_pre,下方合并阶段直接挪过去
|
||||||
|
elif material_source == "notebook" and notebook_search_space_id:
|
||||||
|
# ── 纯笔记模式 ──
|
||||||
|
# 用户在表单选了「我的笔记」作为素材来源 → 跳过 web/intranet 检索,
|
||||||
|
# 仅使用 Step 1.5 已拉取的 notebook_materials。
|
||||||
|
# 与 UI 文案"从笔记本中检索素材"对齐。
|
||||||
|
_emit(f"仅使用笔记素材,跳过 web/intranet 检索({len(notebook_materials)} 条)")
|
||||||
|
all_results_nested = []
|
||||||
else:
|
else:
|
||||||
# ── 常规 web/intranet 模式 ──
|
# ── 常规 web/intranet 模式 ──
|
||||||
provider = get_search_provider(ai_writing_config)
|
provider = get_search_provider(ai_writing_config)
|
||||||
|
|||||||
Reference in new issue
Block a user