Merge branch 'master' into dhk-1

Integrate task deeplink/FX workspace updates from master while keeping dhk-1 menu-overrides and sensitive-words documentation in CLAUDE.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-06-17 19:39:14 +08:00
commit e6a41dbc97
26 files changed
+1302 -84

No files matched your search

+49 -1
View File
@@ -26,14 +26,17 @@ import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks";
import { useNotification } from "@/core/notification/hooks";
import { saveLastAgentId, useThreadSettings } from "@/core/settings";
import { useThreadStream } from "@/core/threads/hooks";
import { stopThreadRun, useThreadStream } from "@/core/threads/hooks";
import { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env";
import { readTaskContext } from "@/core/auth/task-purpose";
import { TaskPurposePanel } from "@/strategy-components/components/task-components/TaskPurposePanel";
import { desensitize } from "@/lib/desensitize";
export default function AgentChatPage({
embeddedAgentId,
chatBasePath,
taskGate,
}: {
/** 当作为「通用问答右侧嵌入」渲染时由父组件传入的 agent id(替代路由参数)。
* 传入即视为嵌入态:路由仍停留在 /chats/new,仅右侧问答区换成该智能体的对话。 */
@@ -43,6 +46,9 @@ export default function AgentChatPage({
* 传入如 `/page/strategy/task-management/plan-task-sentiment/agent/${id}/chats`,
* 使开新会话 / 首条消息后的 URL 改写都停留在任务管理上下文(侧边栏保持对应业务高亮)。 */
chatBasePath?: string;
/** rwfx 任务深链闸门:新建对话时顶部展示目的树面板(覆盖确认后才开始问答),
* 并给新建 thread 打 `metadata.taskId` 标签(任务↔对话 1:N)。见 task-purpose。 */
taskGate?: boolean;
} = {}) {
const routeBase = useRouteBase();
const { t } = useI18n();
@@ -57,6 +63,16 @@ export default function AgentChatPage({
// 对话路由前缀:调用方可覆盖(任务深链嵌入态),否则用当前 routeBase 下的独立智能体对话路径。
const agentChatsBase = chatBasePath ?? `${routeBase}/agents/${agent_id}/chats`;
// rwfx 任务闸门:读取登录深链/「新建对话」暂存的任务上下文(taskId + 开场信息)。
const taskContext = useMemo(
() => (taskGate ? readTaskContext() : null),
[taskGate],
);
const taskId =
taskGate && taskContext && taskContext.agentId === agent_id
? taskContext.taskId
: "";
// 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
// before the auto-sent message takes over.
@@ -101,6 +117,8 @@ export default function AgentChatPage({
threadId: isNewThread ? undefined : threadId,
assistantId: agent_id,
context: { ...settings.context, agent_id },
// 任务闸门:新建 thread 打 metadata.taskId 标签,供任务对话列表(1:N)按任务过滤。
threadMetadata: taskId ? { taskId } : undefined,
onStart: (createdThreadId) => {
setThreadId(createdThreadId);
setIsNewThread(false);
@@ -143,6 +161,30 @@ export default function AgentChatPage({
[sendMessage, threadId, agent_id],
);
// 任务流程:切换 / 新建会话时,停掉「上一会话服务端在跑」的 run(老对话要停止)。
// 仅 taskGate(rwfx 任务工作区)启用——普通问答页保留后台续跑行为不变。
const prevTaskThreadRef = useRef<string | null>(null);
useEffect(() => {
if (!taskGate) return;
const prev = prevTaskThreadRef.current;
if (prev && prev !== threadId) void stopThreadRun(prev);
prevTaskThreadRef.current = threadId ?? null;
}, [taskGate, threadId]);
useEffect(() => {
// 离开任务问答页(卸载)时也停掉当前会话的服务端 run。
return () => {
if (taskGate) void stopThreadRun(prevTaskThreadRef.current);
};
}, [taskGate]);
// 任务闸门通过(无目的数据 / 覆盖成功):发出开场信息开始问答;无开场信息则
// 仅切到对话态(隐藏面板),让用户手动输入。
const handleGateStart = useCallback(() => {
const text = taskContext?.openingText?.trim();
if (text) handleSubmit({ text, files: [] });
else setHasStartedConversation(true);
}, [handleSubmit, taskContext]);
// ChatPage stashes the opening message in sessionStorage when the user picks
// an agent on the landing screen, then redirects here. Replay it once so the
// user doesn't have to re-type the question on the agent chat page.
@@ -181,6 +223,8 @@ export default function AgentChatPage({
}, [thread]);
const isLandingState = isNewThread && !hasStartedConversation;
// 任务闸门面板:仅在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。
const showTaskGate = !!taskId && isNewThread && !hasStartedConversation;
const isDemoMode = env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true";
const status = thread.error
@@ -245,6 +289,10 @@ export default function AgentChatPage({
</div>
</header>
{showTaskGate && (
<TaskPurposePanel taskId={taskId} onStart={handleGateStart} />
)}
{isLandingState ? (
/* Home: centered hero, suggested questions, and input.
Everything lives inside the scrollable main column. No fixed,
+12
View File
@@ -27,6 +27,7 @@ import { QuestionNavigator } from "@/components/workspace/question-navigator";
import { ThreadTitle } from "@/components/workspace/thread-title";
import { TodoList } from "@/components/workspace/todo-list";
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
import { UserIdentityBadge } from "@/components/workspace/user-identity-badge";
import { Welcome } from "@/components/workspace/welcome";
import { CompactWelcome } from "@/components/workspace/compact-welcome";
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
@@ -481,6 +482,17 @@ export default function ChatPage() {
<ExportTrigger threadId={threadId} />
<KnowledgeSaveTrigger threadId={threadId} />
<ArtifactTrigger />
{/* 不带全局顶栏(紧凑模式)时,名称+岗位回落到聊天页顶栏右侧,
与全量模式 Header 右上角保持一致。全量模式已有顶栏则不重复展示。 */}
{!compactChrome && (
<>
<div
className="bg-border/70 mx-1 h-6 w-px shrink-0"
aria-hidden
/>
<UserIdentityBadge />
</>
)}
</div>
</header>
<main className="flex min-h-0 max-w-full grow flex-col">
+33 -33
View File
@@ -3,7 +3,6 @@
import {
BookMarkedIcon,
BrainCircuitIcon,
CheckIcon,
FileTextIcon,
GraduationCapIcon,
} from "lucide-react";
@@ -23,7 +22,9 @@ import {
} from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context";
import { TodoList } from "@/components/workspace/todo-list";
import { Tooltip } from "@/components/workspace/tooltip";
import { Button } from "@/components/ui/button";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useAgentSelector } from "@/core/agents";
import { getAccessToken } from "@/core/auth";
import { useI18n } from "@/core/i18n/hooks";
@@ -369,7 +370,7 @@ export default function IframeChatPage() {
}, [handleInterrupt, handleSubmit]);
const isLandingState = isNewThread && !hasStartedConversation;
const bottomBarHeightClass = "pb-28";
const bottomBarHeightClass = "pb-24";
const modeButtons = [
{
key: "writing",
@@ -377,8 +378,7 @@ export default function IframeChatPage() {
icon: FileTextIcon,
active: !!settings.context.writing_mode,
onClick: toggleWritingMode,
activeClassName:
"border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100",
activeClassName: "text-amber-600 hover:text-amber-700",
},
{
key: "memory",
@@ -386,8 +386,7 @@ export default function IframeChatPage() {
icon: BrainCircuitIcon,
active: settings.context.memory_injection_enabled ?? true,
onClick: toggleMemoryInjection,
activeClassName:
"border-blue-300 bg-blue-50 text-blue-700 hover:bg-blue-100",
activeClassName: "text-blue-600 hover:text-blue-700",
},
{
key: "references",
@@ -396,8 +395,7 @@ export default function IframeChatPage() {
active: ragModeEnabled,
onClick: toggleRagMode,
disabled: ragModeLocked,
activeClassName:
"border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100",
activeClassName: "text-emerald-600 hover:text-emerald-700",
},
{
key: "pro",
@@ -405,8 +403,7 @@ export default function IframeChatPage() {
icon: GraduationCapIcon,
active: proModeEnabled,
onClick: toggleProMode,
activeClassName:
"border-violet-300 bg-violet-50 text-violet-700 hover:bg-violet-100",
activeClassName: "text-violet-600 hover:text-violet-700",
},
] as const;
@@ -496,29 +493,32 @@ export default function IframeChatPage() {
</div>
</div>
<div className="absolute right-0 bottom-0 left-0 z-40 border-t bg-background/95 backdrop-blur">
<div className="mx-auto flex max-w-(--container-width-lg) flex-wrap items-center justify-center gap-2 px-4 py-4">
{modeButtons.map((button) => {
const Icon = button.icon;
return (
<Button
key={button.key}
type="button"
variant="outline"
disabled={button.disabled || isUploading}
onClick={button.onClick}
className={cn(
"gap-1.5 rounded-full border border-transparent bg-muted/40 text-muted-foreground shadow-none hover:bg-muted hover:text-foreground",
button.active && button.activeClassName,
button.disabled && "cursor-not-allowed opacity-80",
)}
>
{button.active && <CheckIcon className="size-3.5" />}
<Icon className="size-3.5" />
<span className="text-xs font-normal">{button.label}</span>
</Button>
);
})}
</div>
<TooltipProvider delayDuration={250}>
<div className="mx-auto flex max-w-(--container-width-lg) flex-wrap items-center justify-center gap-0 px-0 py-1">
{modeButtons.map((button) => {
const Icon = button.icon;
return (
<Tooltip key={button.key} content={button.label}>
<Button
type="button"
variant="ghost"
size="icon"
aria-label={button.label}
disabled={button.disabled || isUploading}
onClick={button.onClick}
className={cn(
"size-9 rounded-full text-muted-foreground shadow-none hover:bg-muted/60 hover:text-foreground",
button.active && button.activeClassName,
button.disabled && "cursor-not-allowed opacity-80",
)}
>
<Icon className="size-4" />
</Button>
</Tooltip>
);
})}
</div>
</TooltipProvider>
</div>
</main>
</div>
+1 -1
View File
@@ -164,7 +164,7 @@ export default function LoginPage() {
if (cancelled) return;
if (detail) {
persistCopTaskDetail(detail);
const target = await resolveTaskDeeplinkTarget(goPath, detail);
const target = await resolveTaskDeeplinkTarget(goPath, detail, taskId);
if (cancelled) return;
if (target) {
navigate(target.path, { replace: true, state: target.state });
+22 -3
View File
@@ -11,6 +11,11 @@ import TaskPgFrom from "@/strategy-components/components/task-components/TaskPgF
import TaskTyFrom from "@/strategy-components/components/task-components/TaskTyFrom";
import TaskXkFrom from "@/strategy-components/components/task-components/TaskXkFrom";
import TaskAnalyzes from "@/strategy-components/components/task-components/TaskAnalyzes";
import {
TaskFxChat,
TaskFxIndexResolver,
TaskFxWorkspace,
} from "@/strategy-components/components/task-components/TaskFxWorkspace";
import { ActionAnalyzes } from "@/strategy-components/components/task-components/ActionAnalyzes";
import KnowledgeBaseResourceManagement from "@/strategy-components/components/resource-management/KnowledgeBaseResourceManagement";
import NetworkActorResourceManagement from "@/strategy-components/components/resource-management/NetworkActorResourceManagement";
@@ -164,12 +169,26 @@ export default function StrategyRoutes() {
</ChatRuntime>
}
/>
{/* 任务FX:任务工作区(左对话列表 1:N + 右问答;新建对话顶部目的树闸门)。 */}
<Route
path="plan-task/plan-task-fx/agent/:agent_id/chats/:thread_id"
path="plan-task/plan-task-fx/agent/:agent_id/chats"
element={
<TaskEmbeddedAgentChat taskBase="/page/strategy/task-management/plan-task/plan-task-fx" />
<TaskFxWorkspace taskBase="/page/strategy/task-management/plan-task/plan-task-fx" />
}
/>
>
<Route
index
element={
<TaskFxIndexResolver taskBase="/page/strategy/task-management/plan-task/plan-task-fx" />
}
/>
<Route
path=":thread_id"
element={
<TaskFxChat taskBase="/page/strategy/task-management/plan-task/plan-task-fx" />
}
/>
</Route>
<Route
path="plan-task/plan-task-action"
element={