代码修改
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 { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
||||
import {
|
||||
useAgent,
|
||||
useDeleteAgent,
|
||||
useSetAgentFeatured,
|
||||
useUpdateAgent,
|
||||
@@ -74,6 +75,13 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
);
|
||||
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 currentUserId = storedAuth?.user_id ?? null;
|
||||
const isAdmin = storedAuth?.system_role === "admin";
|
||||
@@ -93,7 +101,6 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
}
|
||||
setEditName(agent.name);
|
||||
setEditDescription(agent.description ?? "");
|
||||
setEditSoul(agent.soul ?? "");
|
||||
setEditPublished(agent.published);
|
||||
setSelectedSkills(agent.skills ?? []);
|
||||
}, [
|
||||
@@ -101,10 +108,23 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
agent.name,
|
||||
agent.published,
|
||||
agent.skills,
|
||||
agent.soul,
|
||||
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() {
|
||||
router.push(`/page/workspace/agents/${agent.id}/chats/new`);
|
||||
}
|
||||
@@ -459,7 +479,11 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
<Textarea
|
||||
value={editSoul ?? ""}
|
||||
onChange={(e) => setEditSoul(e.target.value)}
|
||||
placeholder="输入 SOUL.md 内容"
|
||||
placeholder={
|
||||
isAgentDetailLoading && !agentDetail
|
||||
? "正在加载 SOUL.md..."
|
||||
: "输入 SOUL.md 内容"
|
||||
}
|
||||
className="min-h-40 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -27,141 +27,162 @@ import TagsPage from "./TagsPage";
|
||||
import LightAppNewNotePage from "./LightAppNewNotePage";
|
||||
import LightAppNoteListPage from "./LightAppNoteListPage";
|
||||
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() {
|
||||
return (
|
||||
<RouteBaseProvider value="/page/strategy">
|
||||
<SidebarProvider>
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<Routes>
|
||||
<Route index element={<Navigate to="qa/general" replace />} />
|
||||
<SidebarProvider>
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<Routes>
|
||||
<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="light-app/notes" element={<LightAppNoteListPage />} />
|
||||
<Route path="light-app/note-qa" element={<LightAppNoteQAPage />} />
|
||||
{/* 对话 */}
|
||||
<Route path="chats" element={<ChatsPage />} />
|
||||
<Route
|
||||
path="chats/:thread_id"
|
||||
element={
|
||||
<ChatRuntime>
|
||||
<ChatPage />
|
||||
</ChatRuntime>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 对话 */}
|
||||
<Route path="chats" element={<ChatsPage />} />
|
||||
<Route
|
||||
path="chats/:thread_id"
|
||||
element={
|
||||
<ChatRuntime>
|
||||
<ChatPage />
|
||||
</ChatRuntime>
|
||||
}
|
||||
/>
|
||||
{/* 智能体管理 */}
|
||||
<Route path="agents" element={<AgentsPage />} />
|
||||
<Route path="agents/new" element={<NewAgentPage />} />
|
||||
<Route
|
||||
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="agents" element={<AgentsPage />} />
|
||||
<Route path="agents/new" element={<NewAgentPage />} />
|
||||
<Route
|
||||
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 path="skills/evolution" element={<SkillEvolutionPage />} />
|
||||
<Route path="memory" element={<MemoryPage />} />
|
||||
|
||||
{/* 技能 / 记忆 */}
|
||||
<Route path="skills" element={<SkillsPage />} />
|
||||
<Route path="skills/evolution" element={<SkillEvolutionPage />} />
|
||||
<Route path="memory" element={<MemoryPage />} />
|
||||
{/* 定时任务 */}
|
||||
<Route
|
||||
path="scheduled"
|
||||
element={<Navigate to="/page/strategy/scheduled/tasks" replace />}
|
||||
/>
|
||||
<Route path="scheduled/tasks" element={<ScheduledTasksPage />} />
|
||||
<Route
|
||||
path="scheduled/runs/:run_id"
|
||||
element={<ScheduledTaskRunDetailPage />}
|
||||
/>
|
||||
|
||||
{/* 定时任务 */}
|
||||
<Route
|
||||
path="scheduled"
|
||||
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
|
||||
path="settings/recommended-questions"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<RecommendedQuestionsPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/prompt-prefix"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<PromptPrefixSettingsPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/llm-metrics"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<LlmMetricsPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/users"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<UsersPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/scheduled-tasks"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<AdminScheduledTasksPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/user-activity"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<AdminUserActivityPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/tags"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<TagsPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
<Toaster position="top-center" />
|
||||
{/* 设置 / 管理 — 权限与 workspace 侧一致,非 admin 重定向到 chats/new */}
|
||||
<Route
|
||||
path="settings/recommended-questions"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<RecommendedQuestionsPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/prompt-prefix"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<PromptPrefixSettingsPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/llm-metrics"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<LlmMetricsPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/users"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<UsersPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/scheduled-tasks"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<AdminScheduledTasksPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/user-activity"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<AdminUserActivityPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/tags"
|
||||
element={
|
||||
getStoredAuth()?.system_role === "admin" ? (
|
||||
<TagsPage />
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
<Toaster position="top-center" />
|
||||
</RouteBaseProvider>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,8 @@ export interface MessageStepsCardProps {
|
||||
className?: string;
|
||||
/** 点击 write_file / present_files 等产物相关步骤时打开沙箱。 */
|
||||
onArtifactStepClick?: (step: BubbleStep) => void;
|
||||
/** 把 agent_orchestration 派活的 agent id 解析成中文席位名(用于 chip 显示)。 */
|
||||
resolveAgentName?: (idOrName: string) => string;
|
||||
}
|
||||
|
||||
/** 工具调用阶段的 step 才进入折叠卡;非工具步骤(connecting/streaming/tool_result)归到「思考」段。 */
|
||||
@@ -58,6 +60,7 @@ export function MessageStepsCard({
|
||||
streaming,
|
||||
className,
|
||||
onArtifactStepClick,
|
||||
resolveAgentName,
|
||||
}: MessageStepsCardProps) {
|
||||
// 把步骤拆成「工具调用」与「思考过程」两类 —— 与主聊天 MessageGroup 的 `lastToolCallStep` /
|
||||
// `lastReasoningStep` 划分对齐。
|
||||
@@ -131,12 +134,14 @@ export function MessageStepsCard({
|
||||
step={s}
|
||||
status="complete"
|
||||
onArtifactStepClick={onArtifactStepClick}
|
||||
resolveAgentName={resolveAgentName}
|
||||
/>
|
||||
))}
|
||||
<ToolStep
|
||||
step={lastToolStep}
|
||||
status={streaming && trailingReasoning.length === 0 ? "active" : "complete"}
|
||||
onArtifactStepClick={onArtifactStepClick}
|
||||
resolveAgentName={resolveAgentName}
|
||||
/>
|
||||
</ChainOfThoughtContent>
|
||||
)}
|
||||
@@ -155,13 +160,15 @@ function ToolStep({
|
||||
step,
|
||||
status,
|
||||
onArtifactStepClick,
|
||||
resolveAgentName,
|
||||
}: {
|
||||
step: BubbleStep;
|
||||
status: "complete" | "active";
|
||||
onArtifactStepClick?: (step: BubbleStep) => void;
|
||||
resolveAgentName?: (idOrName: string) => string;
|
||||
}) {
|
||||
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 isArtifactStep =
|
||||
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">
|
||||
{introText && <MarkdownContent>{introText}</MarkdownContent>}
|
||||
{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 />}
|
||||
{isPendingClarification && clarification && (
|
||||
<IntentClarificationCard
|
||||
|
||||
@@ -67,6 +67,11 @@ export interface Step2PanelProps {
|
||||
agentConfigs: Record<number, AgentConfig>;
|
||||
selectedAgentId: number | null;
|
||||
roles: RoleEntry[];
|
||||
/** agent id(小写)→ 席位元数据,用于把派活 chip 的 id 显示成中文席位名。 */
|
||||
agentNameToRole: Record<
|
||||
string,
|
||||
{ speakerId: number; avatarType: string; displayName: string }
|
||||
>;
|
||||
selectedModel: string;
|
||||
availableModels: ModelOption[];
|
||||
modelsLoading: boolean;
|
||||
@@ -109,6 +114,7 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
agentConfigs,
|
||||
selectedAgentId,
|
||||
roles,
|
||||
agentNameToRole,
|
||||
selectedModel,
|
||||
availableModels,
|
||||
modelsLoading,
|
||||
@@ -134,6 +140,12 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
const { atBottom: step2AtBottom, scrollToBottom: scrollStep2ToBottom } =
|
||||
useAutoScroll(step2ScrollRef, [step2RoundtableDialogues]);
|
||||
|
||||
const resolveAgentName = useCallback(
|
||||
(idOrName: string) =>
|
||||
agentNameToRole[idOrName.toLowerCase()]?.displayName ?? idOrName,
|
||||
[agentNameToRole],
|
||||
);
|
||||
|
||||
// 干预研审:记录指令并自动恢复会商,右上角按钮状态由 isRoundTableRunning 驱动
|
||||
const [pauseFlash, setPauseFlash] = useState(false);
|
||||
|
||||
@@ -245,6 +257,7 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
<Step2Message
|
||||
key={msg.id}
|
||||
msg={msg}
|
||||
resolveAgentName={resolveAgentName}
|
||||
onArtifactStepClick={(step) => openArtifactFromStep(msg, step)}
|
||||
onFileCardClick={(path) => {
|
||||
if (msg.agentThreadId) {
|
||||
@@ -396,6 +409,8 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
*/
|
||||
interface Step2MessageProps {
|
||||
msg: Step2Dialogue;
|
||||
/** 把派活 chip 的 agent id 解析成中文席位名。 */
|
||||
resolveAgentName?: (idOrName: string) => string;
|
||||
onArtifactStepClick?: (
|
||||
step: NonNullable<Step2Dialogue["steps"]>[number],
|
||||
) => void;
|
||||
@@ -405,6 +420,7 @@ interface Step2MessageProps {
|
||||
|
||||
function Step2Message({
|
||||
msg,
|
||||
resolveAgentName,
|
||||
onArtifactStepClick,
|
||||
onFileCardClick,
|
||||
}: Step2MessageProps) {
|
||||
@@ -435,6 +451,7 @@ function Step2Message({
|
||||
steps={msg.steps}
|
||||
streaming={isStreaming}
|
||||
onArtifactStepClick={onArtifactStepClick}
|
||||
resolveAgentName={resolveAgentName}
|
||||
/>
|
||||
)}
|
||||
{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 加载。
|
||||
* 沙箱 write_file 实时预览请用 `openSeatVirtualArtifact`。
|
||||
@@ -441,7 +486,27 @@ export function useStep2Orchestration(
|
||||
const openArtifactPreview = useCallback(
|
||||
(threadId: string, path: string, options?: { auto?: boolean }) => {
|
||||
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);
|
||||
const prev = artifactPathsRef.current;
|
||||
@@ -462,7 +527,14 @@ export function useStep2Orchestration(
|
||||
openPanel();
|
||||
}
|
||||
},
|
||||
[selectArtifact, setArtifacts, setArtifactsOpen],
|
||||
[
|
||||
findSeatArtifactSource,
|
||||
buildWriteFileVirtualUrl,
|
||||
openSeatVirtualArtifact,
|
||||
selectArtifact,
|
||||
setArtifacts,
|
||||
setArtifactsOpen,
|
||||
],
|
||||
);
|
||||
|
||||
const openArtifactFromStep = useCallback(
|
||||
|
||||
@@ -140,6 +140,75 @@ export function newCoordinatorName(): string {
|
||||
*/
|
||||
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] 标记的气泡正文拆成前 / 摘要 / 后三段。
|
||||
* 摘要 JSON 格式异常时整体保留在 `before`,用户仍能看到原文。
|
||||
@@ -152,15 +221,8 @@ export function splitIntentReadyBlock(text: string): {
|
||||
if (!text) return { before: text, summary: null, after: "" };
|
||||
const m = INTENT_READY_RE.exec(text);
|
||||
if (!m) return { before: text, summary: null, after: "" };
|
||||
let summary: IntentSummary | null = null;
|
||||
try {
|
||||
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: "" };
|
||||
}
|
||||
const summary = parseIntentSummary(m[1]);
|
||||
if (!summary) return { before: text, summary: null, after: "" };
|
||||
return {
|
||||
before: text.slice(0, m.index).trimEnd(),
|
||||
summary,
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
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 / 命令代码块等)。
|
||||
@@ -34,6 +36,7 @@ import { ChainOfThoughtSearchResult } from "@/components/ai-elements/chain-of-th
|
||||
export function renderStepArgs(
|
||||
key: string,
|
||||
args?: Record<string, unknown>,
|
||||
resolveAgentName?: (idOrName: string) => string,
|
||||
): React.ReactNode {
|
||||
if (!args) return null;
|
||||
const [, toolName] = key.split(":");
|
||||
@@ -43,15 +46,18 @@ export function renderStepArgs(
|
||||
const agentName = typeof args.agent_name === "string" ? args.agent_name : "";
|
||||
const task = typeof args.task === "string" ? args.task : "";
|
||||
if (!agentName && !task) return null;
|
||||
// 派活 chip 显示中文席位名而非 agent id(客户自建 agent 的 id 常是一串 hex)。
|
||||
const displayName =
|
||||
agentName && resolveAgentName ? resolveAgentName(agentName) : agentName;
|
||||
return (
|
||||
<>
|
||||
{agentName && (
|
||||
<ChainOfThoughtSearchResult>{agentName}</ChainOfThoughtSearchResult>
|
||||
{displayName && (
|
||||
<ChainOfThoughtSearchResult>{displayName}</ChainOfThoughtSearchResult>
|
||||
)}
|
||||
{task && (
|
||||
<span className="block text-[11px] text-slate-600 line-clamp-3 mt-0.5">
|
||||
{task}
|
||||
</span>
|
||||
<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">
|
||||
<MarkdownContent>{task}</MarkdownContent>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -75,9 +81,14 @@ export function renderStepArgs(
|
||||
case "bash": {
|
||||
const command = typeof args.command === "string" ? args.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">
|
||||
{command}
|
||||
</code>
|
||||
<div className="mt-1 max-h-60 overflow-auto rounded-md border border-slate-200 [&_pre]:px-2.5! [&_pre]:py-1.5!">
|
||||
<CodeBlock
|
||||
className="rounded-none border-none"
|
||||
showLineNumbers={false}
|
||||
language="bash"
|
||||
code={command}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
case "write_todos": {
|
||||
|
||||
@@ -488,6 +488,7 @@ export default function RoundtablePlanningPage() {
|
||||
agentConfigs={step2.agentConfigs}
|
||||
selectedAgentId={step2.selectedAgentId}
|
||||
roles={roles}
|
||||
agentNameToRole={step2.agentNameToRole}
|
||||
selectedModel={selectedModel}
|
||||
availableModels={availableModels}
|
||||
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))
|
||||
|
||||
|
||||
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(
|
||||
messages: list[dict[str, Any]],
|
||||
*,
|
||||
@@ -279,33 +343,70 @@ async def stream_intent(request: Request, payload: dict = Body(...)) -> Streamin
|
||||
# 因此既无须 trust_env=False 防代理,也不存在 loopback 40s 超时这种坑。
|
||||
async with _make_loopback_client(request) as client:
|
||||
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(
|
||||
agent_name=INTENT_AGENT_ID,
|
||||
model_name=model_name,
|
||||
thread_id=thread_id,
|
||||
new_message=message,
|
||||
# Intent agent only ever needs ask_clarification. Block
|
||||
new_message=effective_message,
|
||||
# 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
|
||||
# (e.g. saving a design doc with write_file) — the model
|
||||
# tends to ignore SOUL prose, hard exclusion is reliable.
|
||||
# 仅保留 ask_clarification;其余工具见模块 docstring「工具白名单」。
|
||||
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",
|
||||
],
|
||||
# (e.g. saving a design doc with write_file) — SOUL prose is
|
||||
# not reliable on its own, hard exclusion is.
|
||||
excluded_tools=base_excluded_tools,
|
||||
skill_stop_names=[],
|
||||
)
|
||||
|
||||
@@ -338,7 +439,13 @@ async def stream_intent(request: Request, payload: dict = Body(...)) -> Streamin
|
||||
except json.JSONDecodeError as exc:
|
||||
logger.warning("intent summary JSON parse failed: %s", exc)
|
||||
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
|
||||
if summary and ready_text:
|
||||
final_frame = {
|
||||
|
||||
Reference in new issue
Block a user