代码修改
This commit is contained in:
1 parent
68ac49259c
commit
f3c8f2e1ab
12 files changed
+689
-172
No files matched your search
@@ -40,6 +40,7 @@ import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-di
|
|||||||
import { TagChipList } from "@/components/workspace/tags/tag-chip";
|
import { TagChipList } from "@/components/workspace/tags/tag-chip";
|
||||||
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
||||||
import {
|
import {
|
||||||
|
useAgent,
|
||||||
useDeleteAgent,
|
useDeleteAgent,
|
||||||
useSetAgentFeatured,
|
useSetAgentFeatured,
|
||||||
useUpdateAgent,
|
useUpdateAgent,
|
||||||
@@ -74,6 +75,13 @@ export function AgentCard({ agent }: AgentCardProps) {
|
|||||||
);
|
);
|
||||||
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
const [detailSkillName, setDetailSkillName] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// The /api/agents list response intentionally omits `soul` (perf optimization
|
||||||
|
// — SOUL.md is only read on demand). Fetch the detail row when the edit
|
||||||
|
// dialog opens so the SOUL textarea can be hydrated.
|
||||||
|
const { agent: agentDetail, isLoading: isAgentDetailLoading } = useAgent(
|
||||||
|
editOpen ? agent.id : null,
|
||||||
|
);
|
||||||
|
|
||||||
const storedAuth = useMemo(() => getStoredAuth(), []);
|
const storedAuth = useMemo(() => getStoredAuth(), []);
|
||||||
const currentUserId = storedAuth?.user_id ?? null;
|
const currentUserId = storedAuth?.user_id ?? null;
|
||||||
const isAdmin = storedAuth?.system_role === "admin";
|
const isAdmin = storedAuth?.system_role === "admin";
|
||||||
@@ -93,7 +101,6 @@ export function AgentCard({ agent }: AgentCardProps) {
|
|||||||
}
|
}
|
||||||
setEditName(agent.name);
|
setEditName(agent.name);
|
||||||
setEditDescription(agent.description ?? "");
|
setEditDescription(agent.description ?? "");
|
||||||
setEditSoul(agent.soul ?? "");
|
|
||||||
setEditPublished(agent.published);
|
setEditPublished(agent.published);
|
||||||
setSelectedSkills(agent.skills ?? []);
|
setSelectedSkills(agent.skills ?? []);
|
||||||
}, [
|
}, [
|
||||||
@@ -101,10 +108,23 @@ export function AgentCard({ agent }: AgentCardProps) {
|
|||||||
agent.name,
|
agent.name,
|
||||||
agent.published,
|
agent.published,
|
||||||
agent.skills,
|
agent.skills,
|
||||||
agent.soul,
|
|
||||||
editOpen,
|
editOpen,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SOUL.md only ships on the detail endpoint; fall back to whatever the list
|
||||||
|
// happened to carry so older deployments (or cached objects that still
|
||||||
|
// include `soul`) keep working.
|
||||||
|
if (agentDetail) {
|
||||||
|
setEditSoul(agentDetail.soul ?? "");
|
||||||
|
} else {
|
||||||
|
setEditSoul(agent.soul ?? "");
|
||||||
|
}
|
||||||
|
}, [agent.soul, agentDetail, editOpen]);
|
||||||
|
|
||||||
function handleChat() {
|
function handleChat() {
|
||||||
router.push(`/page/workspace/agents/${agent.id}/chats/new`);
|
router.push(`/page/workspace/agents/${agent.id}/chats/new`);
|
||||||
}
|
}
|
||||||
@@ -459,7 +479,11 @@ export function AgentCard({ agent }: AgentCardProps) {
|
|||||||
<Textarea
|
<Textarea
|
||||||
value={editSoul ?? ""}
|
value={editSoul ?? ""}
|
||||||
onChange={(e) => setEditSoul(e.target.value)}
|
onChange={(e) => setEditSoul(e.target.value)}
|
||||||
placeholder="输入 SOUL.md 内容"
|
placeholder={
|
||||||
|
isAgentDetailLoading && !agentDetail
|
||||||
|
? "正在加载 SOUL.md..."
|
||||||
|
: "输入 SOUL.md 内容"
|
||||||
|
}
|
||||||
className="min-h-40 font-mono text-xs"
|
className="min-h-40 font-mono text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,141 +27,162 @@ import TagsPage from "./TagsPage";
|
|||||||
import LightAppNewNotePage from "./LightAppNewNotePage";
|
import LightAppNewNotePage from "./LightAppNewNotePage";
|
||||||
import LightAppNoteListPage from "./LightAppNoteListPage";
|
import LightAppNoteListPage from "./LightAppNoteListPage";
|
||||||
import LightAppNoteQAPage from "./LightAppNoteQAPage";
|
import LightAppNoteQAPage from "./LightAppNoteQAPage";
|
||||||
|
import TaskXkFrom from "@/strategy-components/components/task-components/TaskXkFrom";
|
||||||
|
import TaskManagement from "@/strategy-components/components/task-components/TaskManagement";
|
||||||
|
|
||||||
export default function StrategyRoutes() {
|
export default function StrategyRoutes() {
|
||||||
return (
|
return (
|
||||||
<RouteBaseProvider value="/page/strategy">
|
<RouteBaseProvider value="/page/strategy">
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<div className="h-full w-full overflow-hidden">
|
<div className="h-full w-full overflow-hidden">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<Navigate to="qa/general" replace />} />
|
<Route index element={<Navigate to="qa/general" replace />} />
|
||||||
|
|
||||||
{/* 问答中心 */}
|
{/* 问答中心 */}
|
||||||
<Route path="qa/general" element={<GeneralQA />} />
|
<Route path="qa/general" element={<GeneralQA />} />
|
||||||
|
{/* 任务管理 */}
|
||||||
|
<Route path="task-management" element={<TaskManagement />}>
|
||||||
|
{/* XK任务 */}
|
||||||
|
<Route
|
||||||
|
path="xk-task-jc"
|
||||||
|
element={
|
||||||
|
<TaskXkFrom
|
||||||
|
gopath={'#/pageIndie/pageHome/pageSwrw'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="xk-task-dispatch"
|
||||||
|
element={
|
||||||
|
<TaskXkFrom
|
||||||
|
gopath={'#/pageIndie/pageJczl'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
{/* 轻应用 */}
|
||||||
|
<Route path="light-app/new-note" element={<LightAppNewNotePage />} />
|
||||||
|
<Route path="light-app/notes" element={<LightAppNoteListPage />} />
|
||||||
|
<Route path="light-app/note-qa" element={<LightAppNoteQAPage />} />
|
||||||
|
|
||||||
{/* 轻应用 */}
|
{/* 对话 */}
|
||||||
<Route path="light-app/new-note" element={<LightAppNewNotePage />} />
|
<Route path="chats" element={<ChatsPage />} />
|
||||||
<Route path="light-app/notes" element={<LightAppNoteListPage />} />
|
<Route
|
||||||
<Route path="light-app/note-qa" element={<LightAppNoteQAPage />} />
|
path="chats/:thread_id"
|
||||||
|
element={
|
||||||
|
<ChatRuntime>
|
||||||
|
<ChatPage />
|
||||||
|
</ChatRuntime>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 对话 */}
|
{/* 智能体管理 */}
|
||||||
<Route path="chats" element={<ChatsPage />} />
|
<Route path="agents" element={<AgentsPage />} />
|
||||||
<Route
|
<Route path="agents/new" element={<NewAgentPage />} />
|
||||||
path="chats/:thread_id"
|
<Route
|
||||||
element={
|
path="agents/:agent_id/chats"
|
||||||
<ChatRuntime>
|
element={<Navigate to="/page/strategy/agents" replace />}
|
||||||
<ChatPage />
|
/>
|
||||||
</ChatRuntime>
|
<Route
|
||||||
}
|
path="agents/:agent_id/chats/:thread_id"
|
||||||
/>
|
element={
|
||||||
|
<ChatRuntime>
|
||||||
|
<AgentChatPage />
|
||||||
|
</ChatRuntime>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 智能体管理 */}
|
{/* 技能 / 记忆 */}
|
||||||
<Route path="agents" element={<AgentsPage />} />
|
<Route path="skills" element={<SkillsPage />} />
|
||||||
<Route path="agents/new" element={<NewAgentPage />} />
|
<Route path="skills/evolution" element={<SkillEvolutionPage />} />
|
||||||
<Route
|
<Route path="memory" element={<MemoryPage />} />
|
||||||
path="agents/:agent_id/chats"
|
|
||||||
element={<Navigate to="/page/strategy/agents" replace />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="agents/:agent_id/chats/:thread_id"
|
|
||||||
element={
|
|
||||||
<ChatRuntime>
|
|
||||||
<AgentChatPage />
|
|
||||||
</ChatRuntime>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 技能 / 记忆 */}
|
{/* 定时任务 */}
|
||||||
<Route path="skills" element={<SkillsPage />} />
|
<Route
|
||||||
<Route path="skills/evolution" element={<SkillEvolutionPage />} />
|
path="scheduled"
|
||||||
<Route path="memory" element={<MemoryPage />} />
|
element={<Navigate to="/page/strategy/scheduled/tasks" replace />}
|
||||||
|
/>
|
||||||
|
<Route path="scheduled/tasks" element={<ScheduledTasksPage />} />
|
||||||
|
<Route
|
||||||
|
path="scheduled/runs/:run_id"
|
||||||
|
element={<ScheduledTaskRunDetailPage />}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 定时任务 */}
|
{/* 设置 / 管理 — 权限与 workspace 侧一致,非 admin 重定向到 chats/new */}
|
||||||
<Route
|
<Route
|
||||||
path="scheduled"
|
path="settings/recommended-questions"
|
||||||
element={<Navigate to="/page/strategy/scheduled/tasks" replace />}
|
element={
|
||||||
/>
|
getStoredAuth()?.system_role === "admin" ? (
|
||||||
<Route path="scheduled/tasks" element={<ScheduledTasksPage />} />
|
<RecommendedQuestionsPage />
|
||||||
<Route
|
) : (
|
||||||
path="scheduled/runs/:run_id"
|
<Navigate to="/page/workspace/chats/new" replace />
|
||||||
element={<ScheduledTaskRunDetailPage />}
|
)
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
{/* 设置 / 管理 — 权限与 workspace 侧一致,非 admin 重定向到 chats/new */}
|
<Route
|
||||||
<Route
|
path="settings/prompt-prefix"
|
||||||
path="settings/recommended-questions"
|
element={
|
||||||
element={
|
getStoredAuth()?.system_role === "admin" ? (
|
||||||
getStoredAuth()?.system_role === "admin" ? (
|
<PromptPrefixSettingsPage />
|
||||||
<RecommendedQuestionsPage />
|
) : (
|
||||||
) : (
|
<Navigate to="/page/workspace/chats/new" replace />
|
||||||
<Navigate to="/page/workspace/chats/new" replace />
|
)
|
||||||
)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="settings/llm-metrics"
|
||||||
path="settings/prompt-prefix"
|
element={
|
||||||
element={
|
getStoredAuth()?.system_role === "admin" ? (
|
||||||
getStoredAuth()?.system_role === "admin" ? (
|
<LlmMetricsPage />
|
||||||
<PromptPrefixSettingsPage />
|
) : (
|
||||||
) : (
|
<Navigate to="/page/workspace/chats/new" replace />
|
||||||
<Navigate to="/page/workspace/chats/new" replace />
|
)
|
||||||
)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="admin/users"
|
||||||
path="settings/llm-metrics"
|
element={
|
||||||
element={
|
getStoredAuth()?.system_role === "admin" ? (
|
||||||
getStoredAuth()?.system_role === "admin" ? (
|
<UsersPage />
|
||||||
<LlmMetricsPage />
|
) : (
|
||||||
) : (
|
<Navigate to="/page/workspace/chats/new" replace />
|
||||||
<Navigate to="/page/workspace/chats/new" replace />
|
)
|
||||||
)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="admin/scheduled-tasks"
|
||||||
path="admin/users"
|
element={
|
||||||
element={
|
getStoredAuth()?.system_role === "admin" ? (
|
||||||
getStoredAuth()?.system_role === "admin" ? (
|
<AdminScheduledTasksPage />
|
||||||
<UsersPage />
|
) : (
|
||||||
) : (
|
<Navigate to="/page/workspace/chats/new" replace />
|
||||||
<Navigate to="/page/workspace/chats/new" replace />
|
)
|
||||||
)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="admin/user-activity"
|
||||||
path="admin/scheduled-tasks"
|
element={
|
||||||
element={
|
getStoredAuth()?.system_role === "admin" ? (
|
||||||
getStoredAuth()?.system_role === "admin" ? (
|
<AdminUserActivityPage />
|
||||||
<AdminScheduledTasksPage />
|
) : (
|
||||||
) : (
|
<Navigate to="/page/workspace/chats/new" replace />
|
||||||
<Navigate to="/page/workspace/chats/new" replace />
|
)
|
||||||
)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="admin/tags"
|
||||||
path="admin/user-activity"
|
element={
|
||||||
element={
|
getStoredAuth()?.system_role === "admin" ? (
|
||||||
getStoredAuth()?.system_role === "admin" ? (
|
<TagsPage />
|
||||||
<AdminUserActivityPage />
|
) : (
|
||||||
) : (
|
<Navigate to="/page/workspace/chats/new" replace />
|
||||||
<Navigate to="/page/workspace/chats/new" replace />
|
)
|
||||||
)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Routes>
|
||||||
<Route
|
</div>
|
||||||
path="admin/tags"
|
</SidebarProvider>
|
||||||
element={
|
<Toaster position="top-center" />
|
||||||
getStoredAuth()?.system_role === "admin" ? (
|
|
||||||
<TagsPage />
|
|
||||||
) : (
|
|
||||||
<Navigate to="/page/workspace/chats/new" replace />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
</div>
|
|
||||||
</SidebarProvider>
|
|
||||||
<Toaster position="top-center" />
|
|
||||||
</RouteBaseProvider>
|
</RouteBaseProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -46,6 +46,8 @@ export interface MessageStepsCardProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
/** 点击 write_file / present_files 等产物相关步骤时打开沙箱。 */
|
/** 点击 write_file / present_files 等产物相关步骤时打开沙箱。 */
|
||||||
onArtifactStepClick?: (step: BubbleStep) => void;
|
onArtifactStepClick?: (step: BubbleStep) => void;
|
||||||
|
/** 把 agent_orchestration 派活的 agent id 解析成中文席位名(用于 chip 显示)。 */
|
||||||
|
resolveAgentName?: (idOrName: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 工具调用阶段的 step 才进入折叠卡;非工具步骤(connecting/streaming/tool_result)归到「思考」段。 */
|
/** 工具调用阶段的 step 才进入折叠卡;非工具步骤(connecting/streaming/tool_result)归到「思考」段。 */
|
||||||
@@ -58,6 +60,7 @@ export function MessageStepsCard({
|
|||||||
streaming,
|
streaming,
|
||||||
className,
|
className,
|
||||||
onArtifactStepClick,
|
onArtifactStepClick,
|
||||||
|
resolveAgentName,
|
||||||
}: MessageStepsCardProps) {
|
}: MessageStepsCardProps) {
|
||||||
// 把步骤拆成「工具调用」与「思考过程」两类 —— 与主聊天 MessageGroup 的 `lastToolCallStep` /
|
// 把步骤拆成「工具调用」与「思考过程」两类 —— 与主聊天 MessageGroup 的 `lastToolCallStep` /
|
||||||
// `lastReasoningStep` 划分对齐。
|
// `lastReasoningStep` 划分对齐。
|
||||||
@@ -131,12 +134,14 @@ export function MessageStepsCard({
|
|||||||
step={s}
|
step={s}
|
||||||
status="complete"
|
status="complete"
|
||||||
onArtifactStepClick={onArtifactStepClick}
|
onArtifactStepClick={onArtifactStepClick}
|
||||||
|
resolveAgentName={resolveAgentName}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<ToolStep
|
<ToolStep
|
||||||
step={lastToolStep}
|
step={lastToolStep}
|
||||||
status={streaming && trailingReasoning.length === 0 ? "active" : "complete"}
|
status={streaming && trailingReasoning.length === 0 ? "active" : "complete"}
|
||||||
onArtifactStepClick={onArtifactStepClick}
|
onArtifactStepClick={onArtifactStepClick}
|
||||||
|
resolveAgentName={resolveAgentName}
|
||||||
/>
|
/>
|
||||||
</ChainOfThoughtContent>
|
</ChainOfThoughtContent>
|
||||||
)}
|
)}
|
||||||
@@ -155,13 +160,15 @@ function ToolStep({
|
|||||||
step,
|
step,
|
||||||
status,
|
status,
|
||||||
onArtifactStepClick,
|
onArtifactStepClick,
|
||||||
|
resolveAgentName,
|
||||||
}: {
|
}: {
|
||||||
step: BubbleStep;
|
step: BubbleStep;
|
||||||
status: "complete" | "active";
|
status: "complete" | "active";
|
||||||
onArtifactStepClick?: (step: BubbleStep) => void;
|
onArtifactStepClick?: (step: BubbleStep) => void;
|
||||||
|
resolveAgentName?: (idOrName: string) => string;
|
||||||
}) {
|
}) {
|
||||||
const display = getStepDisplay(step.key, step.label, step.args);
|
const display = getStepDisplay(step.key, step.label, step.args);
|
||||||
const argNode = renderStepArgs(step.key, step.args);
|
const argNode = renderStepArgs(step.key, step.args, resolveAgentName);
|
||||||
const [, toolName] = step.key.split(":");
|
const [, toolName] = step.key.split(":");
|
||||||
const isArtifactStep =
|
const isArtifactStep =
|
||||||
toolName === "write_file" ||
|
toolName === "write_file" ||
|
||||||
|
|||||||
@@ -353,7 +353,13 @@ function Step1Message({
|
|||||||
<div className="text-xs text-slate-700 leading-relaxed font-medium break-words space-y-2 roundtable-md">
|
<div className="text-xs text-slate-700 leading-relaxed font-medium break-words space-y-2 roundtable-md">
|
||||||
{introText && <MarkdownContent>{introText}</MarkdownContent>}
|
{introText && <MarkdownContent>{introText}</MarkdownContent>}
|
||||||
{parts.summary && <IntentReadySummaryCard summary={parts.summary} />}
|
{parts.summary && <IntentReadySummaryCard summary={parts.summary} />}
|
||||||
{parts.after && <MarkdownContent>{parts.after}</MarkdownContent>}
|
{/*
|
||||||
|
parts.after 是模型违反 SOUL §0「硬规则 3」在 [INTENT_READY] 块后
|
||||||
|
继续输出的内容(典型:"那么我先帮你列一下大纲……")。后端 intent.py
|
||||||
|
的终态帧已经把 content 截断到 marker 结尾,但流式期间 LangGraph
|
||||||
|
messages-tuple 还是会把那段废话逐 token 推过来——故意在 UI 层
|
||||||
|
把它丢弃,让用户看到的就是"复述 + 摘要卡",干净结束。
|
||||||
|
*/}
|
||||||
{isStreaming && <StreamingCursor />}
|
{isStreaming && <StreamingCursor />}
|
||||||
{isPendingClarification && clarification && (
|
{isPendingClarification && clarification && (
|
||||||
<IntentClarificationCard
|
<IntentClarificationCard
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ export interface Step2PanelProps {
|
|||||||
agentConfigs: Record<number, AgentConfig>;
|
agentConfigs: Record<number, AgentConfig>;
|
||||||
selectedAgentId: number | null;
|
selectedAgentId: number | null;
|
||||||
roles: RoleEntry[];
|
roles: RoleEntry[];
|
||||||
|
/** agent id(小写)→ 席位元数据,用于把派活 chip 的 id 显示成中文席位名。 */
|
||||||
|
agentNameToRole: Record<
|
||||||
|
string,
|
||||||
|
{ speakerId: number; avatarType: string; displayName: string }
|
||||||
|
>;
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
availableModels: ModelOption[];
|
availableModels: ModelOption[];
|
||||||
modelsLoading: boolean;
|
modelsLoading: boolean;
|
||||||
@@ -109,6 +114,7 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
agentConfigs,
|
agentConfigs,
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
roles,
|
roles,
|
||||||
|
agentNameToRole,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
availableModels,
|
availableModels,
|
||||||
modelsLoading,
|
modelsLoading,
|
||||||
@@ -134,6 +140,12 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
const { atBottom: step2AtBottom, scrollToBottom: scrollStep2ToBottom } =
|
const { atBottom: step2AtBottom, scrollToBottom: scrollStep2ToBottom } =
|
||||||
useAutoScroll(step2ScrollRef, [step2RoundtableDialogues]);
|
useAutoScroll(step2ScrollRef, [step2RoundtableDialogues]);
|
||||||
|
|
||||||
|
const resolveAgentName = useCallback(
|
||||||
|
(idOrName: string) =>
|
||||||
|
agentNameToRole[idOrName.toLowerCase()]?.displayName ?? idOrName,
|
||||||
|
[agentNameToRole],
|
||||||
|
);
|
||||||
|
|
||||||
// 干预研审:记录指令并自动恢复会商,右上角按钮状态由 isRoundTableRunning 驱动
|
// 干预研审:记录指令并自动恢复会商,右上角按钮状态由 isRoundTableRunning 驱动
|
||||||
const [pauseFlash, setPauseFlash] = useState(false);
|
const [pauseFlash, setPauseFlash] = useState(false);
|
||||||
|
|
||||||
@@ -245,6 +257,7 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
<Step2Message
|
<Step2Message
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
msg={msg}
|
msg={msg}
|
||||||
|
resolveAgentName={resolveAgentName}
|
||||||
onArtifactStepClick={(step) => openArtifactFromStep(msg, step)}
|
onArtifactStepClick={(step) => openArtifactFromStep(msg, step)}
|
||||||
onFileCardClick={(path) => {
|
onFileCardClick={(path) => {
|
||||||
if (msg.agentThreadId) {
|
if (msg.agentThreadId) {
|
||||||
@@ -396,6 +409,8 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
*/
|
*/
|
||||||
interface Step2MessageProps {
|
interface Step2MessageProps {
|
||||||
msg: Step2Dialogue;
|
msg: Step2Dialogue;
|
||||||
|
/** 把派活 chip 的 agent id 解析成中文席位名。 */
|
||||||
|
resolveAgentName?: (idOrName: string) => string;
|
||||||
onArtifactStepClick?: (
|
onArtifactStepClick?: (
|
||||||
step: NonNullable<Step2Dialogue["steps"]>[number],
|
step: NonNullable<Step2Dialogue["steps"]>[number],
|
||||||
) => void;
|
) => void;
|
||||||
@@ -405,6 +420,7 @@ interface Step2MessageProps {
|
|||||||
|
|
||||||
function Step2Message({
|
function Step2Message({
|
||||||
msg,
|
msg,
|
||||||
|
resolveAgentName,
|
||||||
onArtifactStepClick,
|
onArtifactStepClick,
|
||||||
onFileCardClick,
|
onFileCardClick,
|
||||||
}: Step2MessageProps) {
|
}: Step2MessageProps) {
|
||||||
@@ -435,6 +451,7 @@ function Step2Message({
|
|||||||
steps={msg.steps}
|
steps={msg.steps}
|
||||||
streaming={isStreaming}
|
streaming={isStreaming}
|
||||||
onArtifactStepClick={onArtifactStepClick}
|
onArtifactStepClick={onArtifactStepClick}
|
||||||
|
resolveAgentName={resolveAgentName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{reasoning && (
|
{reasoning && (
|
||||||
|
|||||||
@@ -434,6 +434,51 @@ export function useStep2Orchestration(
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反查某个产出文件是由哪个席位 thread 写入的。
|
||||||
|
*
|
||||||
|
* 总控(coordinator)常用 `present_files` 呈现「子智能体写的文件」,但文件物理上落在
|
||||||
|
* 子席位 thread 的 outputs 里,coordinator thread 并没有 —— 直接拿总控气泡的
|
||||||
|
* `agentThreadId`(=coordinator thread)走 HTTP 拉会 404(`Thread ... not found`)。
|
||||||
|
* 流式期间子席位 write_file 时,内容已缓存进 seatStubMessages(key=真实写入 thread)。
|
||||||
|
* 这里按 path 反查那条 stub,让关闭沙箱后再点文件卡也能复用虚拟预览(内存内容,
|
||||||
|
* 与流式期间体验一致,且不依赖后端 thread 是否仍可访问)。
|
||||||
|
*/
|
||||||
|
const findSeatArtifactSource = useCallback(
|
||||||
|
(
|
||||||
|
rawPath: string,
|
||||||
|
): { seatThreadId: string; messageId: string; toolCallId: string } | null => {
|
||||||
|
const target = rawPath.trim();
|
||||||
|
if (!target) return null;
|
||||||
|
const targetBase = target.split("/").pop() ?? target;
|
||||||
|
let basenameMatch:
|
||||||
|
| { seatThreadId: string; messageId: string; toolCallId: string }
|
||||||
|
| null = null;
|
||||||
|
for (const [seatThreadId, msgs] of Object.entries(
|
||||||
|
seatStubMessagesRef.current,
|
||||||
|
)) {
|
||||||
|
const msg = msgs?.[0];
|
||||||
|
if (!msg) continue;
|
||||||
|
for (const tc of msg.tool_calls) {
|
||||||
|
if (tc.name !== "write_file" && tc.name !== "str_replace") continue;
|
||||||
|
const p =
|
||||||
|
typeof (tc.args as { path?: unknown })?.path === "string"
|
||||||
|
? ((tc.args as { path?: string }).path as string)
|
||||||
|
: "";
|
||||||
|
if (!p) continue;
|
||||||
|
if (p === target) {
|
||||||
|
return { seatThreadId, messageId: msg.id, toolCallId: tc.id };
|
||||||
|
}
|
||||||
|
if (!basenameMatch && (p.split("/").pop() ?? p) === targetBase) {
|
||||||
|
basenameMatch = { seatThreadId, messageId: msg.id, toolCallId: tc.id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return basenameMatch;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `openArtifactPreview` —— 走真实路径(present_files 等已落盘场景),走 HTTP API 加载。
|
* `openArtifactPreview` —— 走真实路径(present_files 等已落盘场景),走 HTTP API 加载。
|
||||||
* 沙箱 write_file 实时预览请用 `openSeatVirtualArtifact`。
|
* 沙箱 write_file 实时预览请用 `openSeatVirtualArtifact`。
|
||||||
@@ -441,7 +486,27 @@ export function useStep2Orchestration(
|
|||||||
const openArtifactPreview = useCallback(
|
const openArtifactPreview = useCallback(
|
||||||
(threadId: string, path: string, options?: { auto?: boolean }) => {
|
(threadId: string, path: string, options?: { auto?: boolean }) => {
|
||||||
const normalized = path.trim();
|
const normalized = path.trim();
|
||||||
if (!normalized || !threadId) return;
|
if (!normalized) return;
|
||||||
|
|
||||||
|
// present_files 呈现的文件多半是子智能体 write_file 写入的:真实内容/thread 在 stub 里。
|
||||||
|
// 先反查 stub 走虚拟预览(内存内容)—— 关闭沙箱后再点文件卡也能显示,且不受
|
||||||
|
// 总控 coordinator thread 在后端 artifacts 路由下 not found 的影响。
|
||||||
|
const seatSource = findSeatArtifactSource(normalized);
|
||||||
|
if (seatSource) {
|
||||||
|
const virtualUrl = buildWriteFileVirtualUrl(
|
||||||
|
normalized,
|
||||||
|
seatSource.messageId,
|
||||||
|
seatSource.toolCallId,
|
||||||
|
);
|
||||||
|
openSeatVirtualArtifact(
|
||||||
|
seatSource.seatThreadId,
|
||||||
|
virtualUrl,
|
||||||
|
options?.auto ?? false,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!threadId) return;
|
||||||
|
|
||||||
setActiveArtifactThreadId(threadId);
|
setActiveArtifactThreadId(threadId);
|
||||||
const prev = artifactPathsRef.current;
|
const prev = artifactPathsRef.current;
|
||||||
@@ -462,7 +527,14 @@ export function useStep2Orchestration(
|
|||||||
openPanel();
|
openPanel();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selectArtifact, setArtifacts, setArtifactsOpen],
|
[
|
||||||
|
findSeatArtifactSource,
|
||||||
|
buildWriteFileVirtualUrl,
|
||||||
|
openSeatVirtualArtifact,
|
||||||
|
selectArtifact,
|
||||||
|
setArtifacts,
|
||||||
|
setArtifactsOpen,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openArtifactFromStep = useCallback(
|
const openArtifactFromStep = useCallback(
|
||||||
|
|||||||
@@ -140,6 +140,75 @@ export function newCoordinatorName(): string {
|
|||||||
*/
|
*/
|
||||||
export const INTENT_READY_RE = /\[INTENT_READY\]\s*```json\s*(\{[\s\S]*?\})\s*```/;
|
export const INTENT_READY_RE = /\[INTENT_READY\]\s*```json\s*(\{[\s\S]*?\})\s*```/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 JSON 文本里**字符串值内部**的字面控制字符(换行 / 回车 / 制表符)转义成合法的
|
||||||
|
* `\n` / `\r` / `\t`。模型偶尔会在 summary 的字符串值里直接敲换行,产出的 JSON 不合法,
|
||||||
|
* `JSON.parse` 会抛 `Bad control character in string literal`。这里只在字符串内替换,
|
||||||
|
* 不动结构性空白,避免破坏本来就合法的 JSON。
|
||||||
|
*/
|
||||||
|
function sanitizeJsonControlChars(raw: string): string {
|
||||||
|
let out = "";
|
||||||
|
let inString = false;
|
||||||
|
let escaped = false;
|
||||||
|
for (const ch of raw) {
|
||||||
|
if (escaped) {
|
||||||
|
out += ch;
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === "\\") {
|
||||||
|
out += ch;
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '"') {
|
||||||
|
inString = !inString;
|
||||||
|
out += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inString) {
|
||||||
|
if (ch === "\n") {
|
||||||
|
out += "\\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === "\r") {
|
||||||
|
out += "\\r";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === "\t") {
|
||||||
|
out += "\\t";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out += ch;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIntentSummary(raw: string): IntentSummary | null {
|
||||||
|
const attempt = (s: string): IntentSummary | null => {
|
||||||
|
const parsed = JSON.parse(s) as unknown;
|
||||||
|
if (
|
||||||
|
parsed &&
|
||||||
|
typeof parsed === "object" &&
|
||||||
|
typeof (parsed as { objective?: unknown }).objective === "string"
|
||||||
|
) {
|
||||||
|
return parsed as IntentSummary;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
return attempt(raw);
|
||||||
|
} catch {
|
||||||
|
// 字符串值里有裸换行 / 制表符 → 转义后重试,避免整块摘要丢失导致 Step 2 无法解锁。
|
||||||
|
try {
|
||||||
|
return attempt(sanitizeJsonControlChars(raw));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把含 [INTENT_READY] 标记的气泡正文拆成前 / 摘要 / 后三段。
|
* 把含 [INTENT_READY] 标记的气泡正文拆成前 / 摘要 / 后三段。
|
||||||
* 摘要 JSON 格式异常时整体保留在 `before`,用户仍能看到原文。
|
* 摘要 JSON 格式异常时整体保留在 `before`,用户仍能看到原文。
|
||||||
@@ -152,15 +221,8 @@ export function splitIntentReadyBlock(text: string): {
|
|||||||
if (!text) return { before: text, summary: null, after: "" };
|
if (!text) return { before: text, summary: null, after: "" };
|
||||||
const m = INTENT_READY_RE.exec(text);
|
const m = INTENT_READY_RE.exec(text);
|
||||||
if (!m) return { before: text, summary: null, after: "" };
|
if (!m) return { before: text, summary: null, after: "" };
|
||||||
let summary: IntentSummary | null = null;
|
const summary = parseIntentSummary(m[1]);
|
||||||
try {
|
if (!summary) return { before: text, summary: null, after: "" };
|
||||||
const parsed = JSON.parse(m[1]);
|
|
||||||
if (parsed && typeof parsed === "object" && typeof parsed.objective === "string") {
|
|
||||||
summary = parsed as IntentSummary;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return { before: text, summary: null, after: "" };
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
before: text.slice(0, m.index).trimEnd(),
|
before: text.slice(0, m.index).trimEnd(),
|
||||||
summary,
|
summary,
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { ChainOfThoughtSearchResult } from "@/components/ai-elements/chain-of-thought";
|
import { ChainOfThoughtSearchResult } from "@/components/ai-elements/chain-of-thought";
|
||||||
|
import { CodeBlock } from "@/components/ai-elements/code-block";
|
||||||
|
import { MarkdownContent } from "@/roundtable-planning/components/MessageBubble";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据工具名 + args 渲染 ChainOfThoughtStep 的子节点(路径 chip / 命令代码块等)。
|
* 根据工具名 + args 渲染 ChainOfThoughtStep 的子节点(路径 chip / 命令代码块等)。
|
||||||
@@ -34,6 +36,7 @@ import { ChainOfThoughtSearchResult } from "@/components/ai-elements/chain-of-th
|
|||||||
export function renderStepArgs(
|
export function renderStepArgs(
|
||||||
key: string,
|
key: string,
|
||||||
args?: Record<string, unknown>,
|
args?: Record<string, unknown>,
|
||||||
|
resolveAgentName?: (idOrName: string) => string,
|
||||||
): React.ReactNode {
|
): React.ReactNode {
|
||||||
if (!args) return null;
|
if (!args) return null;
|
||||||
const [, toolName] = key.split(":");
|
const [, toolName] = key.split(":");
|
||||||
@@ -43,15 +46,18 @@ export function renderStepArgs(
|
|||||||
const agentName = typeof args.agent_name === "string" ? args.agent_name : "";
|
const agentName = typeof args.agent_name === "string" ? args.agent_name : "";
|
||||||
const task = typeof args.task === "string" ? args.task : "";
|
const task = typeof args.task === "string" ? args.task : "";
|
||||||
if (!agentName && !task) return null;
|
if (!agentName && !task) return null;
|
||||||
|
// 派活 chip 显示中文席位名而非 agent id(客户自建 agent 的 id 常是一串 hex)。
|
||||||
|
const displayName =
|
||||||
|
agentName && resolveAgentName ? resolveAgentName(agentName) : agentName;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{agentName && (
|
{displayName && (
|
||||||
<ChainOfThoughtSearchResult>{agentName}</ChainOfThoughtSearchResult>
|
<ChainOfThoughtSearchResult>{displayName}</ChainOfThoughtSearchResult>
|
||||||
)}
|
)}
|
||||||
{task && (
|
{task && (
|
||||||
<span className="block text-[11px] text-slate-600 line-clamp-3 mt-0.5">
|
<div className="mt-1 max-h-64 overflow-y-auto rounded-md border border-slate-200 bg-white px-2.5 py-1 text-[11px] text-slate-600 leading-relaxed">
|
||||||
{task}
|
<MarkdownContent>{task}</MarkdownContent>
|
||||||
</span>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -75,9 +81,14 @@ export function renderStepArgs(
|
|||||||
case "bash": {
|
case "bash": {
|
||||||
const command = typeof args.command === "string" ? args.command : "";
|
const command = typeof args.command === "string" ? args.command : "";
|
||||||
return command ? (
|
return command ? (
|
||||||
<code className="block bg-slate-100 rounded px-2 py-1 text-[11px] font-mono text-slate-800 mt-0.5 line-clamp-2 break-all">
|
<div className="mt-1 max-h-60 overflow-auto rounded-md border border-slate-200 [&_pre]:px-2.5! [&_pre]:py-1.5!">
|
||||||
{command}
|
<CodeBlock
|
||||||
</code>
|
className="rounded-none border-none"
|
||||||
|
showLineNumbers={false}
|
||||||
|
language="bash"
|
||||||
|
code={command}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
case "write_todos": {
|
case "write_todos": {
|
||||||
|
|||||||
@@ -488,6 +488,7 @@ export default function RoundtablePlanningPage() {
|
|||||||
agentConfigs={step2.agentConfigs}
|
agentConfigs={step2.agentConfigs}
|
||||||
selectedAgentId={step2.selectedAgentId}
|
selectedAgentId={step2.selectedAgentId}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
|
agentNameToRole={step2.agentNameToRole}
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel}
|
||||||
availableModels={availableModels}
|
availableModels={availableModels}
|
||||||
modelsLoading={modelsLoading}
|
modelsLoading={modelsLoading}
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
export interface SceneInfo {
|
||||||
|
applicableTask: string;
|
||||||
|
createTime: string;
|
||||||
|
deleted: number;
|
||||||
|
id: number;
|
||||||
|
relatedAgent: string;
|
||||||
|
sceneCode: string;
|
||||||
|
sceneIntro: string;
|
||||||
|
sceneName: string;
|
||||||
|
sceneType: string;
|
||||||
|
status: number;
|
||||||
|
updateTime: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskListItem {
|
||||||
|
createBy: number;
|
||||||
|
createTime: string;
|
||||||
|
deptId: null;
|
||||||
|
id: number;
|
||||||
|
isTop: number;
|
||||||
|
issuingDept: string;
|
||||||
|
keywords: null;
|
||||||
|
overview: string;
|
||||||
|
planTaskTop: number;
|
||||||
|
platform: null;
|
||||||
|
ptaskId: null;
|
||||||
|
recommendedBasis: null;
|
||||||
|
scene: string; // 或 SceneInfo,如果你已经解析
|
||||||
|
sourceType: number;
|
||||||
|
step: null;
|
||||||
|
taskContent: string;
|
||||||
|
taskDirection: string;
|
||||||
|
taskEndDate: string;
|
||||||
|
taskLevel: number;
|
||||||
|
taskName: string;
|
||||||
|
taskNo: string;
|
||||||
|
taskStartDate: string;
|
||||||
|
taskStatus: number;
|
||||||
|
taskSuperiorFlag: number;
|
||||||
|
taskTimeLimit: null;
|
||||||
|
taskType: number;
|
||||||
|
column: string;
|
||||||
|
updateBy: number;
|
||||||
|
updateTime: string;
|
||||||
|
endCount: number;
|
||||||
|
sumAll: number;
|
||||||
|
progress: number;
|
||||||
|
taskStatusData: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionListItem {
|
||||||
|
actionDay: number;
|
||||||
|
actionEndDate: string;
|
||||||
|
actionManager: number;
|
||||||
|
actionName: string;
|
||||||
|
actionNo: any;
|
||||||
|
actionStartDate: string;
|
||||||
|
actionStatus: number;
|
||||||
|
actionTerm: number;
|
||||||
|
actionTitle: any;
|
||||||
|
actorName: any;
|
||||||
|
audienceGroup: any
|
||||||
|
audienceGroupId: any;
|
||||||
|
audienceRecReason: any
|
||||||
|
behavior: any
|
||||||
|
createBy: number
|
||||||
|
createTime: string;
|
||||||
|
deleteFlag: number;
|
||||||
|
deptId: any;
|
||||||
|
deptName: string;
|
||||||
|
drivingFactor: any;
|
||||||
|
expectedBehaviorId: any;
|
||||||
|
id: number
|
||||||
|
keywords: string
|
||||||
|
level: number
|
||||||
|
scene: string
|
||||||
|
sourceType: number
|
||||||
|
step: null
|
||||||
|
taskId: number
|
||||||
|
updateBy: number
|
||||||
|
updateTime: number
|
||||||
|
useActionId: null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface actionColumnItem {
|
||||||
|
icon: string;
|
||||||
|
columnName: string;
|
||||||
|
actionList: ActionListItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const taskStatusList = {
|
||||||
|
'0': {
|
||||||
|
label: '待签收',
|
||||||
|
color: '#0062d9',
|
||||||
|
icon: '待签收.svg'
|
||||||
|
},
|
||||||
|
'10': {
|
||||||
|
label: '待分析',
|
||||||
|
color: '#2f9acc',
|
||||||
|
icon: '待CEH.svg'
|
||||||
|
},
|
||||||
|
'20': {
|
||||||
|
label: '分析中',
|
||||||
|
color: '#d97b00',
|
||||||
|
icon: '进行中.svg'
|
||||||
|
},
|
||||||
|
'25': {
|
||||||
|
label: '分析完成已提交',
|
||||||
|
color: '#1eb07f',
|
||||||
|
icon: '完成.svg'
|
||||||
|
},
|
||||||
|
'30': {
|
||||||
|
label: '已分析待审核',
|
||||||
|
color: '#1eb07f',
|
||||||
|
icon: '完成.svg'
|
||||||
|
},
|
||||||
|
'40': {
|
||||||
|
label: '分析审核通过',
|
||||||
|
color: '#1eb07f',
|
||||||
|
icon: '完成.svg'
|
||||||
|
},
|
||||||
|
'50': {
|
||||||
|
label: '分析审核未通过',
|
||||||
|
color: '#cf1322',
|
||||||
|
icon: '未通过.svg'
|
||||||
|
},
|
||||||
|
'60': {
|
||||||
|
label: '行动已下发待签收',
|
||||||
|
color: '#1eb07f',
|
||||||
|
icon: '完成.svg'
|
||||||
|
},
|
||||||
|
'70': {
|
||||||
|
label: '行动已签收待策划',
|
||||||
|
color: '#1eb07f',
|
||||||
|
icon: '完成.svg'
|
||||||
|
},
|
||||||
|
'80': {
|
||||||
|
label: '行动策划已提交',
|
||||||
|
color: '#1eb07f',
|
||||||
|
icon: '完成.svg'
|
||||||
|
},
|
||||||
|
'100': {
|
||||||
|
label: '作战方案待审核',
|
||||||
|
color: '#1eb07f',
|
||||||
|
icon: '完成.svg'
|
||||||
|
},
|
||||||
|
'110': {
|
||||||
|
label: '作战方案审核通过',
|
||||||
|
color: '#1eb07f',
|
||||||
|
icon: '完成.svg'
|
||||||
|
},
|
||||||
|
'120': {
|
||||||
|
label: '作战方案未通过',
|
||||||
|
color: '#cf1322',
|
||||||
|
icon: '未通过.svg'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default function TaskManagement() {
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Outlet, useNavigate } from 'react-router-dom';
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
const userInfo = localStorage.getItem('userInfo');
|
||||||
|
const xkLinkUrl = import.meta.env.VITE_AIPI_XKLINKTO_URL;
|
||||||
|
interface TaskPgFromProps {
|
||||||
|
gopath: string
|
||||||
|
}
|
||||||
|
const TaskXkFrom: React.FC<TaskPgFromProps> = ({gopath}) => {
|
||||||
|
const [xkUrl, setXkUrl] = useState(xkLinkUrl);
|
||||||
|
useEffect(() => {
|
||||||
|
if(userInfo) {
|
||||||
|
const userData: any = JSON.parse(userInfo);
|
||||||
|
setXkUrl(`${xkLinkUrl}${gopath}?accessToken=${userData.accessToken}`);
|
||||||
|
}
|
||||||
|
}, [gopath]);
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<iframe className="h-[calc(100vh-92px)]" src={xkUrl}></iframe>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default TaskXkFrom
|
||||||
@@ -133,6 +133,70 @@ def _normalize_options(raw: object) -> tuple[str, ...]:
|
|||||||
return tuple(sorted(o for o in out if o))
|
return tuple(sorted(o for o in out if o))
|
||||||
|
|
||||||
|
|
||||||
|
def _count_past_clarifications(messages: list[dict[str, Any]]) -> int:
|
||||||
|
"""统计当前 thread 内已经发出过的 ``ask_clarification`` 次数。
|
||||||
|
|
||||||
|
用来给 ``stream_intent`` 做"3 轮硬上限"兜底:
|
||||||
|
- 0-1 次:正常流程,模型自行决定继续问还是收敛
|
||||||
|
- 2 次(即将进入第 3 轮):prompt 追加系统提示,要求本轮必须 INTENT_READY
|
||||||
|
- ≥3 次:工具白名单移除 ``ask_clarification``,模型物理上无法再问
|
||||||
|
|
||||||
|
只数"已经成型"的 tool_call,不区分用户是否回答过——LangGraph 把 ask
|
||||||
|
工具的调用永久记录在 AIMessage.tool_calls 里,数它即可。
|
||||||
|
"""
|
||||||
|
if not isinstance(messages, list):
|
||||||
|
return 0
|
||||||
|
count = 0
|
||||||
|
for msg in messages:
|
||||||
|
if not isinstance(msg, dict):
|
||||||
|
continue
|
||||||
|
if (msg.get("type") or "").lower() not in ("ai", "aimessage", "aimessagechunk"):
|
||||||
|
continue
|
||||||
|
for tc in msg.get("tool_calls") or []:
|
||||||
|
if (tc.get("name") or "").lower() == "ask_clarification":
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_thread_messages(
|
||||||
|
client: httpx.AsyncClient,
|
||||||
|
base: str,
|
||||||
|
headers: dict[str, str],
|
||||||
|
thread_id: str,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""读取当前 thread checkpoint 的 messages 列表(best-effort)。
|
||||||
|
|
||||||
|
用 ASGITransport loopback 调 ``GET /api/threads/{tid}/state``,失败时返回
|
||||||
|
空列表——计数兜底是个"刹车",失败时退化成原有的纯 SOUL 自律行为,不阻断主流程。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp = await client.get(f"{base}/api/threads/{thread_id}/state", headers=headers)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("intent: get thread state failed (%s) — falling back to soul-only flow", exc)
|
||||||
|
return []
|
||||||
|
if resp.status_code != 200:
|
||||||
|
logger.warning(
|
||||||
|
"intent: get thread state %s returned %s — falling back to soul-only flow",
|
||||||
|
thread_id,
|
||||||
|
resp.status_code,
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
payload = resp.json()
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
values = payload.get("values") if isinstance(payload, dict) else None
|
||||||
|
if not isinstance(values, dict):
|
||||||
|
return []
|
||||||
|
messages = values.get("messages")
|
||||||
|
return messages if isinstance(messages, list) else []
|
||||||
|
|
||||||
|
|
||||||
|
# 硬上限:超过这个轮数,无论模型怎么想,都强制移除 ask_clarification 工具。
|
||||||
|
# 与 SOUL.md §0 「硬规则 2」对齐——"绝不存在第 4 轮"。
|
||||||
|
_MAX_CLARIFICATION_ROUNDS = 3
|
||||||
|
|
||||||
|
|
||||||
def _is_repeated_clarification(
|
def _is_repeated_clarification(
|
||||||
messages: list[dict[str, Any]],
|
messages: list[dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
@@ -279,33 +343,70 @@ async def stream_intent(request: Request, payload: dict = Body(...)) -> Streamin
|
|||||||
# 因此既无须 trust_env=False 防代理,也不存在 loopback 40s 超时这种坑。
|
# 因此既无须 trust_env=False 防代理,也不存在 loopback 40s 超时这种坑。
|
||||||
async with _make_loopback_client(request) as client:
|
async with _make_loopback_client(request) as client:
|
||||||
try:
|
try:
|
||||||
|
# —— 3 轮硬上限兜底 ——
|
||||||
|
# 在发起 run 之前先读一次 thread state,数当前 ask_clarification 已发出次数:
|
||||||
|
# past=0/1 → 正常,不动 message,不动工具白名单
|
||||||
|
# past=2(本轮即将变成第 3 轮)→ 给消息加前缀,强提示"必须 INTENT_READY"
|
||||||
|
# past≥3 → 直接把 ask_clarification 也加进 excluded_tools,工具物理消失
|
||||||
|
# 这一步是个"刹车":SOUL 让模型自律,这里让系统兜底。
|
||||||
|
past_clarifications = _count_past_clarifications(
|
||||||
|
await _fetch_thread_messages(client, base, headers, thread_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. ask_clarification 的工具白名单——超过硬上限就移除。
|
||||||
|
base_excluded_tools = [
|
||||||
|
"web_search",
|
||||||
|
"present_files",
|
||||||
|
"view_image",
|
||||||
|
"agent_orchestration",
|
||||||
|
"bash",
|
||||||
|
"ls",
|
||||||
|
"read_file",
|
||||||
|
"write_file",
|
||||||
|
"str_replace",
|
||||||
|
"memory",
|
||||||
|
"hindsight_recall",
|
||||||
|
"hindsight_reflect",
|
||||||
|
"hindsight_retain",
|
||||||
|
"task",
|
||||||
|
"write_todos",
|
||||||
|
]
|
||||||
|
if past_clarifications >= _MAX_CLARIFICATION_ROUNDS:
|
||||||
|
base_excluded_tools.append("ask_clarification")
|
||||||
|
logger.info(
|
||||||
|
"intent stream: past_clarifications=%d >= %d, force-disabling "
|
||||||
|
"ask_clarification to break the loop",
|
||||||
|
past_clarifications,
|
||||||
|
_MAX_CLARIFICATION_ROUNDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. message 前缀——给模型一个明确的"本轮必须收敛"信号。
|
||||||
|
effective_message = message
|
||||||
|
if past_clarifications >= _MAX_CLARIFICATION_ROUNDS:
|
||||||
|
effective_message = (
|
||||||
|
f"[系统强制] 已澄清 {past_clarifications} 轮,触发硬上限。"
|
||||||
|
f"本轮请直接输出 [INTENT_READY],缺的字段用 assumptions 兜底,"
|
||||||
|
f"绝对不要再调任何工具,也不要再询问任何问题。\n\n"
|
||||||
|
f"用户最新输入:{message}"
|
||||||
|
)
|
||||||
|
elif past_clarifications >= _MAX_CLARIFICATION_ROUNDS - 1:
|
||||||
|
effective_message = (
|
||||||
|
f"[系统提示] 已澄清 {past_clarifications} 轮,这是最后一轮。"
|
||||||
|
f"本轮请直接输出 [INTENT_READY],未明确的字段用 assumptions 兜底。\n\n"
|
||||||
|
f"用户最新输入:{message}"
|
||||||
|
)
|
||||||
|
|
||||||
body = _run_payload(
|
body = _run_payload(
|
||||||
agent_name=INTENT_AGENT_ID,
|
agent_name=INTENT_AGENT_ID,
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
thread_id=thread_id,
|
thread_id=thread_id,
|
||||||
new_message=message,
|
new_message=effective_message,
|
||||||
# Intent agent only ever needs ask_clarification. Block
|
# Intent agent only ever needs ask_clarification (and even that
|
||||||
|
# is yanked once past_clarifications hits the cap above). Block
|
||||||
# everything else so the model can't drift into solutioning
|
# everything else so the model can't drift into solutioning
|
||||||
# (e.g. saving a design doc with write_file) — the model
|
# (e.g. saving a design doc with write_file) — SOUL prose is
|
||||||
# tends to ignore SOUL prose, hard exclusion is reliable.
|
# not reliable on its own, hard exclusion is.
|
||||||
# 仅保留 ask_clarification;其余工具见模块 docstring「工具白名单」。
|
excluded_tools=base_excluded_tools,
|
||||||
excluded_tools=[
|
|
||||||
"web_search",
|
|
||||||
"present_files",
|
|
||||||
"view_image",
|
|
||||||
"agent_orchestration",
|
|
||||||
"bash",
|
|
||||||
"ls",
|
|
||||||
"read_file",
|
|
||||||
"write_file",
|
|
||||||
"str_replace",
|
|
||||||
"memory",
|
|
||||||
"hindsight_recall",
|
|
||||||
"hindsight_reflect",
|
|
||||||
"hindsight_retain",
|
|
||||||
"task",
|
|
||||||
"write_todos",
|
|
||||||
],
|
|
||||||
skill_stop_names=[],
|
skill_stop_names=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -338,7 +439,13 @@ async def stream_intent(request: Request, payload: dict = Body(...)) -> Streamin
|
|||||||
except json.JSONDecodeError as exc:
|
except json.JSONDecodeError as exc:
|
||||||
logger.warning("intent summary JSON parse failed: %s", exc)
|
logger.warning("intent summary JSON parse failed: %s", exc)
|
||||||
summary = None
|
summary = None
|
||||||
ready_text = msg_text
|
# SOUL §0 硬规则 3 要求 ```json``` 反引号闭合后不得再输出任何字符,
|
||||||
|
# 但模型仍偶尔会在后面接 "那么我帮你列一下大纲……" 之类的尾巴。
|
||||||
|
# 终态帧只回到 marker 块结尾 + 一个换行,把废话从 content 里裁掉——
|
||||||
|
# 草稿持久化 / 跨设备重放 / 前端 splitIntentReadyBlock 的 after
|
||||||
|
# 段都因此干净。流式期间用户仍会逐 token 看到那段废话,前端
|
||||||
|
# Step1Panel 已经把 `parts.after` 隐藏掉,体验闭环。
|
||||||
|
ready_text = msg_text[: m.end()].rstrip() + "\n"
|
||||||
break
|
break
|
||||||
if summary and ready_text:
|
if summary and ready_text:
|
||||||
final_frame = {
|
final_frame = {
|
||||||
|
|||||||
Reference in new issue
Block a user