feat(task-deeplink): 3qfx/xdfx 新建对话改手动「开始回答」+ xdfx 行动详情卡片 + 独立问答智能体列表栏

- AgentChatPage: 纯绑定深链(3qfx/xdfx)不再自动发开场信息,改由右上角「开始回答」按钮手动触发,从根上消除新建对话竞态导致的重复发送;移除 autoSentArmedRef/streamThreadId 相关逻辑
- task-deeplink.ts: 抽出 fetchActionDetail 返回整包 ActionDetail,fetchTaskIdFromAction 复用之
- TaskFxWorkspace: xdfx 左栏「当前任务」下新增「行动详情」卡片(行动名称/叙事)
- StrategyRoutes/WorkspaceRoutes: 独立智能体问答整页新增最左「智能体列表」栏(showAgentList);xdfx 右侧宿主改用 TaskBindChat(精简版)
- 新增 agent-chat-sidebar.tsx

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-18 20:55:41 +08:00
1 parent 179a3f2b2f
commit 6aaa4c841e
6 files changed
+298 -49

No files matched your search

+31 -33
View File
@@ -7,6 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { Button } from "@/components/ui/button";
import { AgentChatSidebar } from "@/components/workspace/agents/agent-chat-sidebar";
import { AgentDetailPanel } from "@/components/workspace/agents/agent-detail-panel";
import { AgentQuestionChips } from "@/components/workspace/agents/agent-question-chips";
import { AgentWelcome } from "@/components/workspace/agent-welcome";
@@ -39,6 +40,7 @@ export default function AgentChatPage({
taskGate,
taskBind,
autoStartWhenPurposeEmpty,
showAgentList,
}: {
/** 当作为「通用问答右侧嵌入」渲染时由父组件传入的 agent id(替代路由参数)。
* 传入即视为嵌入态:路由仍停留在 /chats/new,仅右侧问答区换成该智能体的对话。 */
@@ -56,6 +58,9 @@ export default function AgentChatPage({
taskBind?: boolean;
/** rwfx:查询任务目的为空数据时,自动开始第一次问答(跳过手动点「开始问答」)。 */
autoStartWhenPurposeEmpty?: boolean;
/** 是否在最左侧展示「智能体列表」栏(顶部下拉切换 我的/广场,点击进入该智能体对话)。
* 仅独立的智能体问答整页开启;任务工作区 / 模块页 / 嵌入态不传(各有自己的左栏)。 */
showAgentList?: boolean;
} = {}) {
const routeBase = useRouteBase();
const { t } = useI18n();
@@ -80,16 +85,13 @@ export default function AgentChatPage({
taskCtxOn && taskContext && taskContext.agentId === agent_id
? taskContext.taskId
: "";
// 纯绑定(taskBind 且无闸门):新建对话自动发出的任务开场信息。rwfx 走闸门发出,不在此列。
const taskAutoSendText =
// 纯绑定(taskBind,无闸门,3qfx/xdfx):新建对话进问答页后,右上角「开始回答」按钮点击时
// 发出的任务开场信息。不再自动发(避免新建对话竞态重复发送),改由用户手动点按钮触发。
// rwfx 走目的树闸门「开始问答」,不在此列。
const taskStartText =
taskBind && !taskGate && taskContext && taskContext.agentId === agent_id
? (taskContext.openingText?.trim() ?? "")
: "";
// 纯绑定每次进入「新建对话」自动发一次开场信息。用「武装」标志去重,而不是绑定到 threadId——
// 新建态首帧 useThreadChat 会把 threadId 从 useState 初值换成新 uuid(抖动),若用 threadId 做
// 去重,会被误判成「又一个新会话」而重复发送(表现为「发了两次、开始回答后又变回一个」)。
// 「武装」只在会话真正创建完成(isNewThread 落回 false)后才重置,故对 threadId 抖动免疫。
const autoSentArmedRef = useRef(true);
// If ChatPage stashed an opening message for this agent, skip the landing
// screen on first render so the user doesn't see a flash of the welcome UI
@@ -106,7 +108,7 @@ export default function AgentChatPage({
}
};
const [hasStartedConversation, setHasStartedConversation] = useState(
() => hasPendingForThisAgent() || !!taskAutoSendText,
() => hasPendingForThisAgent(),
);
const { agent, isLoading: isAgentLoading } = useAgent(agent_id);
@@ -125,19 +127,13 @@ export default function AgentChatPage({
useEffect(() => {
if (isNewThread) {
setHasStartedConversation(hasPendingForThisAgent() || !!taskAutoSendText);
setHasStartedConversation(hasPendingForThisAgent());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isNewThread, threadId, agent_id]);
// 会话真正创建完成(isNewThread 落回 false)后重新「武装」,让下一次「新建对话」能再自动发一次。
// 不依赖 threadId,故新建态首帧的 threadId 抖动不会误触发重复发送。
useEffect(() => {
if (!isNewThread) autoSentArmedRef.current = true;
}, [isNewThread]);
const { showNotification } = useNotification();
const [thread, sendMessage, , streamThreadId] = useThreadStream({
const [thread, sendMessage] = useThreadStream({
threadId: isNewThread ? undefined : threadId,
assistantId: agent_id,
context: { ...settings.context, agent_id },
@@ -234,22 +230,11 @@ export default function AgentChatPage({
handleSubmit({ text: pending.text, files: [] });
}, [agent_id, threadId, isNewThread, handleSubmit]);
// 纯绑定任务深链(3qfx/xdfx,无闸门):进入新建对话即自动发出任务开场信息(每个新建会话恰好一次)。
//
// 两道守卫缺一不可:
// 1) `streamThreadId == null`:等底层 stream 真正切到全新线程再发。从已有会话点「新建对话」时
// isNewThread 先翻 true,但 onStreamThreadId 慢一帧才归零;若那一帧就 submit,hook 仍绑定上一个
// 会话(submit 的 threadId 选项仅在 hook 线程为空时生效)→ 开场信息发进旧对话。
// 2) `autoSentArmedRef`:本轮新建会话是否还「待发」。只在会话创建完成(isNewThread→false)后重置,
// 不受 threadId 抖动影响,避免同一个新建会话里重复发出。
useEffect(() => {
if (!taskAutoSendText) return;
if (!isNewThread) return;
if (streamThreadId != null) return;
if (!autoSentArmedRef.current) return;
autoSentArmedRef.current = false;
handleSubmit({ text: taskAutoSendText, files: [] });
}, [taskAutoSendText, isNewThread, streamThreadId, handleSubmit]);
// 纯绑定任务深链(3qfx/xdfx):新建对话进问答页 landing 后,由 header 右上角「开始回答」按钮
// 手动触发发出开场信息(见下方 header)。不再自动发,从根上避免新建对话竞态导致的重复发送。
const handleTaskStart = useCallback(() => {
if (taskStartText) handleSubmit({ text: taskStartText, files: [] });
}, [handleSubmit, taskStartText]);
const handleClarificationSubmit = useCallback(
(answer: string) => {
@@ -265,7 +250,7 @@ export default function AgentChatPage({
const isLandingState = isNewThread && !hasStartedConversation;
// 任务闸门面板:仅 rwfx(taskGate)在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。
// 3qfx/xdfx(taskBind)不展示闸门,靠 taskAutoSendText 直接发出开场信息。
// 3qfx/xdfx(taskBind)不展示闸门,新建对话进问答页后由右上角「开始回答」按钮手动发出开场信息。
const showTaskGate = !!taskGate && !!taskId && isNewThread && !hasStartedConversation;
const isDemoMode = env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true";
@@ -283,6 +268,8 @@ export default function AgentChatPage({
always opens between the chat and the detail panel — the detail
panel stays rightmost. */}
<div className="flex size-full min-h-0 overflow-hidden workspace-chat-bg">
{/* 最左:智能体列表栏(仅独立整页问答开启)。顶部下拉切换 我的/广场。 */}
{showAgentList && <AgentChatSidebar activeAgentId={agent_id} />}
<div className="min-h-0 min-w-0 flex-1">
<ChatBox threadId={threadId}>
{/* Main conversation column. `relative` hosts the floating
@@ -305,6 +292,17 @@ export default function AgentChatPage({
</div>
<div className="flex items-center">
{/* 3qfx/xdfx 任务深链:新建对话进问答页后,点此发出任务开场信息开始问答(不自动发)。 */}
{taskStartText && isLandingState && (
<Button
size="sm"
className="mr-1 bg-[var(--color-main)] text-white shadow-sm hover:bg-[var(--brand-950)]"
onClick={handleTaskStart}
disabled={isDemoMode}
>
开始回答
</Button>
)}
<Tooltip content={t.agents.newChat}>
<Button
size="sm"
+4 -3
View File
@@ -224,7 +224,8 @@ export default function StrategyRoutes() {
</ChatRuntime>
}
/>
{/* 行动分析:任务工作区(左对话列表 1:N 精简版;右侧带目的树闸门,与 rwfx 同一查询/显示)。
{/* 行动分析:任务工作区(左对话列表 1:N 精简版 + 左上行动详情卡片;直接问答,
右上角「开始回答」按钮手动发出开场信息,不走目的树闸门/覆盖确认)。
注意:xdfx 深链传入的是「行动id」,登录时已先经 resolveDeeplinkTaskId 换成任务id。 */}
<Route
path="plan-task/plan-task-action/agent/:agent_id/chats"
@@ -241,7 +242,7 @@ export default function StrategyRoutes() {
<Route
path=":thread_id"
element={
<TaskFxChat taskBase="/page/strategy/task-management/plan-task/plan-task-action" />
<TaskBindChat taskBase="/page/strategy/task-management/plan-task/plan-task-action" />
}
/>
</Route>
@@ -290,7 +291,7 @@ export default function StrategyRoutes() {
path="agents/:agent_id/chats/:thread_id"
element={
<ChatRuntime>
<AgentChatPage />
<AgentChatPage showAgentList />
</ChatRuntime>
}
/>
+1 -1
View File
@@ -288,7 +288,7 @@ export default function WorkspaceRoutes({
element={
<WorkspaceLayout {...layoutProps}>
<ChatRuntime>
<AgentChatPage />
<AgentChatPage showAgentList />
</ChatRuntime>
</WorkspaceLayout>
}