Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji
This commit is contained in:
commit
f200972c00
8 files changed
+164
-200
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { PenLine } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { getBackendBaseURL } from '@/core/config';
|
||||
import { apiFetch } from '@/core/api';
|
||||
import { useModels } from '@/core/models/hooks';
|
||||
|
||||
import type { AIWritingRequest } from '../../types/ai-writing';
|
||||
import { useArticleTypes } from '../../hooks/useArticleTypes';
|
||||
@@ -23,11 +22,6 @@ interface AIWritingFormProps {
|
||||
hideAdvancedFields?: boolean;
|
||||
}
|
||||
|
||||
interface ModelOption {
|
||||
name: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const INPUT_CLASS =
|
||||
'flex h-12 w-full rounded-xl border border-[#e8dedd] bg-white px-4 py-2 text-sm shadow-sm ' +
|
||||
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 ' +
|
||||
@@ -52,10 +46,24 @@ export function AIWritingForm({
|
||||
const [wordCountInput, setWordCountInput] = useState('800');
|
||||
const [strictMode, setStrictMode] = useState(false);
|
||||
const [modelName, setModelName] = useState('');
|
||||
const [models, setModels] = useState<ModelOption[]>([]);
|
||||
const { models, isLoading: modelsLoading } = useModels();
|
||||
|
||||
const { articleTypes, loading: typesLoading, refresh: refreshTypes } = useArticleTypes();
|
||||
|
||||
// 首次 /api/models 返回后默认选第一项;配置变更导致当前值不在列表时也回退到首项。
|
||||
useEffect(() => {
|
||||
if (models.length === 0) return;
|
||||
const stillExists = models.some((m) => m.name === modelName);
|
||||
if (!modelName || !stillExists) {
|
||||
setModelName(models[0].name);
|
||||
}
|
||||
}, [models, modelName]);
|
||||
|
||||
const selectedModel = useMemo(
|
||||
() => models.find((m) => m.name === modelName) ?? models[0],
|
||||
[models, modelName],
|
||||
);
|
||||
|
||||
// 初始化时加载文章类型
|
||||
useEffect(() => {
|
||||
refreshTypes();
|
||||
@@ -81,25 +89,6 @@ export function AIWritingForm({
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch(`${getBackendBaseURL()}/api/models`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
const list: ModelOption[] = (data.models ?? []).map(
|
||||
(m: { name: string; display_name?: string }) => ({
|
||||
name: m.name,
|
||||
displayName: m.display_name || m.name,
|
||||
}),
|
||||
);
|
||||
setModels(list);
|
||||
if (list.length > 0 && !modelName) {
|
||||
setModelName(list[0].name);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
function handleWordCountBlur() {
|
||||
const num = parseInt(wordCountInput, 10);
|
||||
if (isNaN(num) || num < 100) {
|
||||
@@ -122,7 +111,7 @@ export function AIWritingForm({
|
||||
wordCountTarget: wordCount,
|
||||
keywordCount: DEFAULT_KEYWORD_COUNT,
|
||||
writingMode: hideAdvancedFields ? 'loose' : strictMode ? 'strict' : 'loose',
|
||||
modelName: modelName || undefined,
|
||||
modelName: selectedModel?.name || modelName || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -192,17 +181,25 @@ export function AIWritingForm({
|
||||
<div className="mt-4 space-y-2">
|
||||
<label className={FIELD_LABEL_CLASS}>写作模型</label>
|
||||
<Select
|
||||
value={modelName}
|
||||
value={selectedModel?.name ?? ''}
|
||||
onValueChange={setModelName}
|
||||
disabled={disabled || models.length === 0}
|
||||
disabled={disabled || modelsLoading || models.length === 0}
|
||||
>
|
||||
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
|
||||
<SelectValue placeholder={models.length > 0 ? '选择模型…' : '模型加载中…'} />
|
||||
<SelectValue
|
||||
placeholder={
|
||||
modelsLoading
|
||||
? '模型加载中…'
|
||||
: models.length > 0
|
||||
? '选择模型…'
|
||||
: '暂无可用模型'
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{models.map((m) => (
|
||||
<SelectItem key={m.name} value={m.name} className="text-sm">
|
||||
{m.displayName}
|
||||
{m.display_name || m.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface StreamRecommendResult {
|
||||
* `done` → picks parsed and validated, ready to populate the dialog as
|
||||
* default-checked selections.
|
||||
* `asking` → recommender failed to emit a usable [RECOMMEND_READY] block,
|
||||
* caller should apply its fallback list (typically the 6 roundtable-* seats).
|
||||
* caller should pick from the live candidate pool as fallback.
|
||||
* `error` → upstream / network failure.
|
||||
*/
|
||||
status: "done" | "asking" | "error";
|
||||
|
||||
@@ -62,20 +62,6 @@ export interface RecommendAgentsDialogProps {
|
||||
const MIN_SELECTED = 2;
|
||||
const MAX_SELECTED = 8;
|
||||
|
||||
/**
|
||||
* Default selection when the recommender fails or returns < 2 valid picks.
|
||||
* Intersected with the live candidate pool at runtime so missing built-ins
|
||||
* don't break the dialog.
|
||||
*/
|
||||
const FALLBACK_AGENT_IDS = [
|
||||
"roundtable-intelligence",
|
||||
"roundtable-environment",
|
||||
"roundtable-solution-design",
|
||||
"roundtable-risk-review",
|
||||
"roundtable-execution-plan",
|
||||
"roundtable-summary",
|
||||
];
|
||||
|
||||
type DialogPhase =
|
||||
| "idle"
|
||||
| "loading_agents"
|
||||
@@ -319,12 +305,17 @@ export function RecommendAgentsDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: status === "asking" or picks too few. Use the 6 built-in
|
||||
// roundtable seats, intersected against the live pool.
|
||||
const fallback = intersectInOrder(FALLBACK_AGENT_IDS, candidates);
|
||||
const seed = fallback.length >= MIN_SELECTED
|
||||
? fallback
|
||||
: candidates.slice(0, MIN_SELECTED).map((c) => c.agent_id);
|
||||
// Fallback: status === "asking" or picks too few — pick from live pool.
|
||||
const seed = candidates.slice(0, MIN_SELECTED).map((c) => c.agent_id);
|
||||
if (seed.length < MIN_SELECTED) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
phase: "error",
|
||||
streamPhase: null,
|
||||
errorMessage: `候选智能体不足 ${MIN_SELECTED} 个,无法进入圆桌研讨。请先发布更多智能体。`,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setSelectedIds(new Set(seed));
|
||||
const { rationale } = parseRecommendStreamContent(result.content || "");
|
||||
setState((prev) => ({
|
||||
@@ -334,7 +325,7 @@ export function RecommendAgentsDialog({
|
||||
recommendedIds: seed,
|
||||
reasons,
|
||||
streamPhase: null,
|
||||
warning: "智能推荐未生成有效结果,已为你勾选默认的圆桌席位,可手动调整。",
|
||||
warning: "智能推荐未生成有效结果,已为你勾选候选池中的前几位智能体,可手动调整。",
|
||||
}));
|
||||
} catch (err) {
|
||||
if (myGen !== generationRef.current) return;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*
|
||||
* 状态全部来自 `useStep2Orchestration` —— 本组件不调任何后端、不持久化。
|
||||
*/
|
||||
import { useRef } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Cpu,
|
||||
FileIcon,
|
||||
@@ -80,7 +80,6 @@ export interface Step2PanelProps {
|
||||
/** 主聊天里 `present_files` 会在气泡下方渲染卡片;Step 2 沿用同一交互。 */
|
||||
openArtifactPreview: (threadId: string, path: string) => void;
|
||||
// ─── Setters ──────────────────────────────────────────────────
|
||||
setIsRoundTableRunning: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setClarificationDraft: React.Dispatch<React.SetStateAction<string>>;
|
||||
setCustomAgentInput: React.Dispatch<React.SetStateAction<string>>;
|
||||
setAgentConfigs: React.Dispatch<
|
||||
@@ -88,8 +87,10 @@ export interface Step2PanelProps {
|
||||
>;
|
||||
setSelectedAgentId: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
// ─── Handlers ─────────────────────────────────────────────────
|
||||
onPauseRoundtable: () => void;
|
||||
onResumeRoundtable: () => void;
|
||||
onRecordInjectMessage: () => void;
|
||||
onSubmitClarification: (answer: string) => void;
|
||||
onInjectToRoundtable: () => void;
|
||||
onSavePersona: (id: number) => void;
|
||||
onProceedToStep3: () => void;
|
||||
onTriggerToast: (msg: string) => void;
|
||||
@@ -116,13 +117,14 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
seatStubMessages,
|
||||
openArtifactFromStep,
|
||||
openArtifactPreview,
|
||||
setIsRoundTableRunning,
|
||||
setClarificationDraft,
|
||||
setCustomAgentInput,
|
||||
setAgentConfigs,
|
||||
setSelectedAgentId,
|
||||
onPauseRoundtable,
|
||||
onResumeRoundtable,
|
||||
onRecordInjectMessage,
|
||||
onSubmitClarification,
|
||||
onInjectToRoundtable,
|
||||
onSavePersona,
|
||||
onProceedToStep3,
|
||||
onTriggerToast,
|
||||
@@ -132,6 +134,25 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
const { atBottom: step2AtBottom, scrollToBottom: scrollStep2ToBottom } =
|
||||
useAutoScroll(step2ScrollRef, [step2RoundtableDialogues]);
|
||||
|
||||
// 干预研审 → 暂停会商:与右上角按钮共用 onPauseRoundtable;乐观 UI 避免等父 state 刷新。
|
||||
const [pauseFlash, setPauseFlash] = useState(false);
|
||||
const [optimisticPaused, setOptimisticPaused] = useState(false);
|
||||
const showPausedUi = !isRoundTableRunning || optimisticPaused;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRoundTableRunning) {
|
||||
setOptimisticPaused(false);
|
||||
}
|
||||
}, [isRoundTableRunning]);
|
||||
|
||||
const handleInject = useCallback(() => {
|
||||
setOptimisticPaused(true);
|
||||
onPauseRoundtable();
|
||||
onRecordInjectMessage();
|
||||
setPauseFlash(true);
|
||||
window.setTimeout(() => setPauseFlash(false), 1600);
|
||||
}, [onPauseRoundtable, onRecordInjectMessage]);
|
||||
|
||||
const isSandboxStreaming =
|
||||
isOrchestrating ||
|
||||
step2RoundtableDialogues.some((dialogue) => dialogue.streaming === true);
|
||||
@@ -175,28 +196,28 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isRoundTableRunning ? (
|
||||
{showPausedUi ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRoundTableRunning(false);
|
||||
onTriggerToast("⏸️ 会商已暂停");
|
||||
}}
|
||||
type="button"
|
||||
onClick={onResumeRoundtable}
|
||||
className={`bg-emerald-100 hover:bg-emerald-200 text-emerald-800 text-[10px] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-emerald-200/40 shadow-sm ${
|
||||
pauseFlash
|
||||
? "ring-2 ring-emerald-400 ring-offset-1 animate-pulse"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<Play className="w-3.5 h-3.5 fill-emerald-800" />
|
||||
恢复会商
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPauseRoundtable}
|
||||
className="bg-amber-100 hover:bg-amber-200 text-amber-800 text-[10px] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-amber-200/40 shadow-sm"
|
||||
>
|
||||
<Pause className="w-3.5 h-3.5" />
|
||||
暂停会商
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsRoundTableRunning(true);
|
||||
onTriggerToast("▶️ 恢复大模型并行博弈与推理决策");
|
||||
}}
|
||||
className="bg-emerald-100 hover:bg-emerald-200 text-emerald-800 text-[10px] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-emerald-200/40 shadow-sm"
|
||||
>
|
||||
<Play className="w-3.5 h-3.5 fill-emerald-800" />
|
||||
恢复会商
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -324,14 +345,16 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
value={customAgentInput}
|
||||
onChange={(e) => setCustomAgentInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onInjectToRoundtable();
|
||||
if (e.key === "Enter") handleInject();
|
||||
}}
|
||||
placeholder="作为指挥官,您可以随时插话输入指令,对智能体博弈朝向实施人工干预..."
|
||||
className="flex-1 bg-white border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs focus:outline-none focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 font-medium"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={onInjectToRoundtable}
|
||||
type="button"
|
||||
onClick={handleInject}
|
||||
title="暂停会商并记录干预指令;点击右上角「恢复会商」继续执行"
|
||||
className="bg-primary hover:bg-[var(--brand-900)] text-white text-xs font-bold px-4 py-2.5 rounded-xl transition shrink-0 cursor-pointer active:scale-95 shadow-md shadow-primary/10"
|
||||
>
|
||||
干预研审
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
type ThreadIdMap,
|
||||
} from "@/roundtable-planning/api/multi-agent";
|
||||
import {
|
||||
DEFAULT_SELECTED_AGENTS,
|
||||
MAX_CYCLES,
|
||||
buildAgentNameToRole,
|
||||
buildRoleTemplates,
|
||||
@@ -99,18 +98,8 @@ export type Step2Snapshot = {
|
||||
seatStubMessages: Record<string, StubAiMessage[]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 6 个默认席位的初始 Persona 配置。model 留空 → 运行时 `agentConfigs[seat]?.model
|
||||
* || selectedModel || undefined`,即「席位覆盖优先,否则跟随全局模型」。
|
||||
*/
|
||||
const DEFAULT_AGENT_CONFIGS: Record<number, AgentConfig> = {
|
||||
2: { model: "", weight: 90, temp: 0.2, prompt: "你负责解析核心任务、资源极限和用户目标偏好,监督全局研商收敛。" },
|
||||
3: { model: "", weight: 85, temp: 0.3, prompt: "你负责搜集电力、通信系统、历史极端部署条件情报,支持备灾架构体系。" },
|
||||
4: { model: "", weight: 88, temp: 0.1, prompt: "你评估服务器机房热载荷、算力并发承载极限,预报运行瓶颈与风险时钟。" },
|
||||
5: { model: "", weight: 92, temp: 0.4, prompt: "你负责双全栈主备建模设计,高内聚低耦合,规避不可预知停机崩溃。" },
|
||||
6: { model: "", weight: 95, temp: 0.1, prompt: "你作为安全主管,严厉把控预算开销与合规通过率防线,识别计划漏洞。" },
|
||||
7: { model: "", weight: 80, temp: 0.4, prompt: "你负责对72小时里程碑执行路径规划、重排和资源调配演练序列。" },
|
||||
};
|
||||
/** 席位 Persona 配置;初始为空,用户在 Persona 抽屉保存后写入。 */
|
||||
const DEFAULT_AGENT_CONFIGS: Record<number, AgentConfig> = {};
|
||||
|
||||
export interface UseStep2OrchestrationOptions {
|
||||
/** 当前页面 currentStep,===2 触发自动 init。 */
|
||||
@@ -171,9 +160,15 @@ export interface UseStep2OrchestrationReturn {
|
||||
// ─── Handlers ───────────────────────────────────────────────────────
|
||||
/** 中断当前编排循环 + 所有 in-flight SSE。 */
|
||||
stopOrchestration: () => void;
|
||||
/** 与 Step2Panel「暂停会商」按钮相同 —— 挂起编排并中止 in-flight SSE。 */
|
||||
pauseRoundtable: () => void;
|
||||
/** 与 Step2Panel「恢复会商」按钮相同 —— 恢复自动研讨(含待处理的干预指令)。 */
|
||||
resumeRoundtable: () => void;
|
||||
/** 记录干预指令(不暂停);通常由 Step2Panel 在 pauseRoundtable 之后调用。 */
|
||||
recordInjectMessage: () => void;
|
||||
/** 中断所有 in-flight SSE(给新建任务 / 加载草稿用)。 */
|
||||
abortAllInFlight: () => void;
|
||||
/** 用户「干预研审」按钮 —— 追加一条人工指令气泡并触发一轮编排。 */
|
||||
/** @deprecated 请用 pauseRoundtable + recordInjectMessage;保留兼容组合调用。 */
|
||||
handleInjectToRoundtable: () => void;
|
||||
/** 总控澄清回复 —— 把回答拼到 prompt 里再起一轮编排。 */
|
||||
submitClarification: (rawAnswer: string) => void;
|
||||
@@ -247,9 +242,7 @@ export function useStep2Orchestration(
|
||||
Record<string, StubAiMessage[]>
|
||||
>({});
|
||||
const seatStubMessagesRef = useRef<Record<string, StubAiMessage[]>>({});
|
||||
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>(
|
||||
DEFAULT_SELECTED_AGENTS,
|
||||
);
|
||||
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>([]);
|
||||
const [threadIds, setThreadIds] = useState<ThreadIdMap | null>(null);
|
||||
const [coordinatorName, setCoordinatorName] = useState<string | null>(null);
|
||||
const [isInitializing, setIsInitializing] = useState<boolean>(false);
|
||||
@@ -296,9 +289,8 @@ export function useStep2Orchestration(
|
||||
const orchestrationCancelRef = useRef<boolean>(false);
|
||||
const step2InitRef = useRef<boolean>(false);
|
||||
const activeAbortersRef = useRef<Set<AbortController>>(new Set());
|
||||
/** 干预研审:暂停当前轮次后待执行的 leader prompt。 */
|
||||
/** 干预研审:恢复会商后待执行的 leader prompt。 */
|
||||
const pendingInjectPromptRef = useRef<string | null>(null);
|
||||
const [injectFlushToken, setInjectFlushToken] = useState(0);
|
||||
// selectedModel / agentConfigs / agentNameToRole / threadIds / coordinatorName
|
||||
// 等等被 runOrchestration 在循环里反复读取。把它们打到 ref 上,可以让
|
||||
// runOrchestration 的 useCallback deps 大幅收窄,避免每次模型切换、agentConfigs
|
||||
@@ -1112,17 +1104,11 @@ export function useStep2Orchestration(
|
||||
|
||||
// 挂起 / 恢复:与上面的 init effect 独立。`isRoundTableRunning` toggle 时:
|
||||
// - false → stopOrchestration
|
||||
// - true 且 idle → 触发一次「请继续」的 leader cycle
|
||||
// 注意 deps 故意只有 isRoundTableRunning,不再额外加 currentStep —— 回到 Step 2
|
||||
// 检视讨论时不应该自动 fire 新的 leader 调用。
|
||||
// - true 且 idle → 触发 leader cycle(若有干预指令则优先执行)
|
||||
useEffect(() => {
|
||||
if (currentStep !== 2) return;
|
||||
if (!isRoundTableRunning) {
|
||||
// 干预研审会先写入 pending 再置 false;此时 stop 已在 handler 里触发,
|
||||
// 避免 effect 再次 stop 误杀随后 flush 启动的 inject 编排。
|
||||
if (!pendingInjectPromptRef.current) {
|
||||
stopOrchestration();
|
||||
}
|
||||
stopOrchestration();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -1132,6 +1118,18 @@ export function useStep2Orchestration(
|
||||
!coordinatorNameRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
const pending = pendingInjectPromptRef.current;
|
||||
if (pending) {
|
||||
pendingInjectPromptRef.current = null;
|
||||
void runOrchestration(
|
||||
pending,
|
||||
threadIdsRef.current,
|
||||
coordinatorNameRef.current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
void runOrchestration(
|
||||
"用户已恢复研讨,请继续上一轮的协调工作。",
|
||||
threadIdsRef.current,
|
||||
@@ -1140,53 +1138,41 @@ export function useStep2Orchestration(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isRoundTableRunning]);
|
||||
|
||||
// 干预研审:当前轮次停止后 flush 待执行的 inject 编排(编排期间 UI 仍为「恢复会商」)。
|
||||
useEffect(() => {
|
||||
if (isOrchestrating) return;
|
||||
const pending = pendingInjectPromptRef.current;
|
||||
if (!pending) return;
|
||||
if (!threadIdsRef.current || !coordinatorNameRef.current) return;
|
||||
pendingInjectPromptRef.current = null;
|
||||
triggerToast("您的人工纠偏指令已同步至圆桌节点!");
|
||||
void runOrchestration(
|
||||
pending,
|
||||
threadIdsRef.current,
|
||||
coordinatorNameRef.current,
|
||||
);
|
||||
}, [isOrchestrating, injectFlushToken, runOrchestration, triggerToast]);
|
||||
const pauseRoundtable = useCallback(() => {
|
||||
setIsRoundTableRunning(false);
|
||||
orchestrationCancelRef.current = true;
|
||||
stopOrchestration();
|
||||
triggerToast("⏸️ 会商已暂停");
|
||||
}, [stopOrchestration, triggerToast]);
|
||||
|
||||
// ─── Public handlers ─────────────────────────────────────────────
|
||||
const handleInjectToRoundtable = useCallback(() => {
|
||||
if (!customAgentInput.trim()) return;
|
||||
const resumeRoundtable = useCallback(() => {
|
||||
setIsRoundTableRunning(true);
|
||||
triggerToast("▶️ 恢复大模型并行博弈与推理决策");
|
||||
}, [triggerToast]);
|
||||
|
||||
const recordInjectMessage = useCallback(() => {
|
||||
const userText = customAgentInput.trim();
|
||||
if (!userText) return;
|
||||
if (!threadIdsRef.current || !coordinatorNameRef.current) {
|
||||
triggerToast("⚠️ 圆桌尚未就绪,请稍候");
|
||||
triggerToast("⚠️ 圆桌尚未就绪,无法记录干预指令");
|
||||
return;
|
||||
}
|
||||
|
||||
const userText = customAgentInput.trim();
|
||||
setCustomAgentInput("");
|
||||
|
||||
const prompt = `用户人工干预新指令:${userText}\n\n请结合当前讨论状态,决定是否继续派活或直接给出更新后的结论。`;
|
||||
pendingInjectPromptRef.current = prompt;
|
||||
|
||||
// 与 Step2Panel 右上角「暂停会商 / 恢复会商」联动:立即进入挂起态。
|
||||
setIsRoundTableRunning(false);
|
||||
stopOrchestration();
|
||||
|
||||
appendDialogue({
|
||||
sender: "用户 (插话干预)",
|
||||
avatarType: "human",
|
||||
content: `💡 (人工指令介入) ${userText}`,
|
||||
confidence: "最高指令",
|
||||
});
|
||||
pendingInjectPromptRef.current = `用户人工干预新指令:${userText}\n\n请结合当前讨论状态,决定是否继续派活或直接给出更新后的结论。`;
|
||||
triggerToast("干预指令已记录,点击「恢复会商」后执行");
|
||||
}, [appendDialogue, customAgentInput, triggerToast]);
|
||||
|
||||
if (isOrchestratingRef.current) {
|
||||
triggerToast("⏸️ 会商已暂停,当前轮次结束后将处理您的干预指令");
|
||||
} else {
|
||||
triggerToast("⏸️ 会商已暂停,正在处理您的干预指令…");
|
||||
}
|
||||
setInjectFlushToken((n) => n + 1);
|
||||
}, [appendDialogue, customAgentInput, stopOrchestration, triggerToast]);
|
||||
const handleInjectToRoundtable = useCallback(() => {
|
||||
pauseRoundtable();
|
||||
recordInjectMessage();
|
||||
}, [pauseRoundtable, recordInjectMessage]);
|
||||
|
||||
const submitClarification = useCallback(
|
||||
(rawAnswer: string) => {
|
||||
@@ -1378,6 +1364,9 @@ export function useStep2Orchestration(
|
||||
setAgentConfigs,
|
||||
resetInitGate,
|
||||
stopOrchestration,
|
||||
pauseRoundtable,
|
||||
resumeRoundtable,
|
||||
recordInjectMessage,
|
||||
abortAllInFlight,
|
||||
handleInjectToRoundtable,
|
||||
submitClarification,
|
||||
|
||||
@@ -24,23 +24,7 @@ export type RoleEntry = {
|
||||
/** 用户在推荐弹窗选定的一个研讨席位。 */
|
||||
export type SelectedAgent = { agent_id: string; name: string };
|
||||
|
||||
/**
|
||||
* 6 个内置 roundtable 席位的展示元数据。用户保留默认席位时,
|
||||
* 头像 / 配色 / 中文显示名都按这里走;非内置(用户自建)走 FALLBACK_AVATAR_TYPES。
|
||||
*/
|
||||
export const BUILTIN_AGENT_META: Record<
|
||||
string,
|
||||
{ avatarType: string; displayName: string }
|
||||
> = {
|
||||
"roundtable-intelligence": { avatarType: "purple", displayName: "情报收集" },
|
||||
"roundtable-environment": { avatarType: "emerald", displayName: "环境评估" },
|
||||
"roundtable-solution-design": { avatarType: "human", displayName: "方案设计" },
|
||||
"roundtable-risk-review": { avatarType: "amber", displayName: "风险审查" },
|
||||
"roundtable-execution-plan": { avatarType: "pink", displayName: "执行规划" },
|
||||
"roundtable-summary": { avatarType: "blue", displayName: "总结陈述" },
|
||||
};
|
||||
|
||||
/** 非内置 agent 的头像配色轮换池。 */
|
||||
/** 研讨席位的头像配色轮换池。 */
|
||||
export const FALLBACK_AVATAR_TYPES = [
|
||||
"blue",
|
||||
"purple",
|
||||
@@ -49,28 +33,12 @@ export const FALLBACK_AVATAR_TYPES = [
|
||||
"pink",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 用户未确认推荐弹窗时,Step 2 默认使用的 6 个内置席位。
|
||||
* 保留默认时页面与原硬编码行为完全一致。
|
||||
*/
|
||||
export const DEFAULT_SELECTED_AGENTS: SelectedAgent[] = [
|
||||
{ agent_id: "roundtable-summary", name: "总结陈述" },
|
||||
{ agent_id: "roundtable-intelligence", name: "情报收集" },
|
||||
{ agent_id: "roundtable-environment", name: "环境评估" },
|
||||
{ agent_id: "roundtable-solution-design", name: "方案设计" },
|
||||
{ agent_id: "roundtable-risk-review", name: "风险审查" },
|
||||
{ agent_id: "roundtable-execution-plan", name: "执行规划" },
|
||||
];
|
||||
|
||||
export function getAvatarTypeFor(agentId: string, index: number): string {
|
||||
return (
|
||||
BUILTIN_AGENT_META[agentId]?.avatarType ??
|
||||
FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length]
|
||||
);
|
||||
export function getAvatarTypeFor(_agentId: string, index: number): string {
|
||||
return FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length];
|
||||
}
|
||||
|
||||
export function getDisplayNameFor(agent: SelectedAgent): string {
|
||||
return BUILTIN_AGENT_META[agent.agent_id]?.displayName ?? agent.name;
|
||||
return agent.name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,7 +62,6 @@ import {
|
||||
import { useStep2Orchestration } from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||
import { useDraftPersistence } from "@/roundtable-planning/hooks/useDraftPersistence";
|
||||
import {
|
||||
BUILTIN_AGENT_META,
|
||||
getAvatarTypeFor,
|
||||
getDisplayNameFor,
|
||||
makeRolesFromTemplates,
|
||||
@@ -260,8 +259,7 @@ export default function RoundtablePlanningPage() {
|
||||
(agents: Array<{ agent_id: string; name: string }>) => {
|
||||
const normalized: SelectedAgent[] = agents.map(({ agent_id, name }) => ({
|
||||
agent_id,
|
||||
// 内置 agent 走 BUILTIN_AGENT_META;非内置用候选池传过来的 name。
|
||||
name: BUILTIN_AGENT_META[agent_id]?.displayName ?? name ?? agent_id,
|
||||
name: name ?? agent_id,
|
||||
}));
|
||||
step2.setSelectedAgents(normalized);
|
||||
step2.resetInitGate();
|
||||
@@ -496,13 +494,14 @@ export default function RoundtablePlanningPage() {
|
||||
seatStubMessages={step2.seatStubMessages}
|
||||
openArtifactFromStep={step2.openArtifactFromStep}
|
||||
openArtifactPreview={step2.openArtifactPreview}
|
||||
setIsRoundTableRunning={step2.setIsRoundTableRunning}
|
||||
onPauseRoundtable={step2.pauseRoundtable}
|
||||
onResumeRoundtable={step2.resumeRoundtable}
|
||||
onRecordInjectMessage={step2.recordInjectMessage}
|
||||
setClarificationDraft={step2.setClarificationDraft}
|
||||
setCustomAgentInput={step2.setCustomAgentInput}
|
||||
setAgentConfigs={step2.setAgentConfigs}
|
||||
setSelectedAgentId={step2.setSelectedAgentId}
|
||||
onSubmitClarification={step2.submitClarification}
|
||||
onInjectToRoundtable={step2.handleInjectToRoundtable}
|
||||
onSavePersona={step2.handleSaveAgentAndRecalculate}
|
||||
onProceedToStep3={() => {
|
||||
if (!step2.hasConsensus) {
|
||||
@@ -894,9 +893,7 @@ function LeftSidebar({
|
||||
...selectedAgents.map((agent, i) => ({
|
||||
id: agent.agent_id,
|
||||
name: getDisplayNameFor(agent),
|
||||
role: BUILTIN_AGENT_META[agent.agent_id]
|
||||
? `${BUILTIN_AGENT_META[agent.agent_id].displayName} 席位`
|
||||
: "圆桌席位",
|
||||
role: "圆桌席位",
|
||||
icon: getAvatar(getAvatarTypeFor(agent.agent_id, i), "h-7 w-7"),
|
||||
})),
|
||||
].map((contrib, idx) => (
|
||||
@@ -956,8 +953,8 @@ function LeftSidebar({
|
||||
<IntentSectionIcon className="h-5 w-5" />
|
||||
</CollapsedSidebarSection>
|
||||
|
||||
<CollapsedSidebarSection title="参与角色" className="flex-1 min-h-[120px]">
|
||||
<div className="flex flex-col items-center gap-2 overflow-y-auto max-h-[320px] custom-scrollbar">
|
||||
<CollapsedSidebarSection title="参与角色">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{roles.map((role) => (
|
||||
<RoleAvatarBadge
|
||||
key={role.id}
|
||||
@@ -1245,7 +1242,7 @@ function CollapsedSidebarSection({
|
||||
<div
|
||||
title={title}
|
||||
className={cn(
|
||||
"w-full bg-white border border-slate-200/50 rounded-2xl p-2 shadow-[0_2px_8px_rgba(0,0,0,0.01)] flex flex-col items-center justify-center",
|
||||
"w-full shrink-0 bg-white border border-slate-200/50 rounded-2xl p-2 shadow-[0_2px_8px_rgba(0,0,0,0.01)] flex flex-col items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -406,7 +406,7 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
|
||||
|
||||
> 以下两个示例覆盖**不同任务类型**,目的是教你「思路要呼应任务本身」,**不要**把任何一个示例的具体职能链当模板。
|
||||
|
||||
### 示例 A:合规改造任务(职能型候选池)
|
||||
### 示例 A:合规改造任务(领域候选池)
|
||||
|
||||
**用户输入**:
|
||||
```
|
||||
@@ -415,12 +415,11 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
|
||||
关键假设:现有系统具备基础数字化能力;允许预定窗口停机
|
||||
|
||||
候选智能体池:
|
||||
- roundtable-intelligence (情报收集):检索同类历史案例、行业最佳实践与外部情报
|
||||
- roundtable-environment (环境评估):评估硬件容量与系统量化承载力
|
||||
- roundtable-solution-design (方案设计):产出主备方案与关键路径
|
||||
- roundtable-risk-review (风险审查):合规红线检查与收敛建议
|
||||
- roundtable-execution-plan (执行规划):里程碑与资源排布
|
||||
- roundtable-summary (总结陈述):归纳冲突标记与盖印
|
||||
- compliance-research (合规调研):检索同类历史案例、行业最佳实践与监管口径
|
||||
- infra-capacity (基础设施评估):评估硬件容量与系统量化承载力
|
||||
- solution-architect (方案架构):产出主备方案与关键路径
|
||||
- risk-auditor (风险审查):合规红线检查与收敛建议
|
||||
- execution-planner (执行规划):里程碑与资源排布
|
||||
- my-cost-calc (成本测算):基于预算和资源单价计算总成本
|
||||
```
|
||||
|
||||
@@ -431,10 +430,10 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
|
||||
```json
|
||||
{
|
||||
"picks": [
|
||||
{"agent_id": "roundtable-intelligence", "reason": "检索同类等保 2.0 改造案例与监管口径,直接支撑本次合规对标"},
|
||||
{"agent_id": "roundtable-solution-design", "reason": "在 6 个月工期约束下产出主备改造方案与关键路径"},
|
||||
{"agent_id": "roundtable-risk-review", "reason": "对照等保 2.0 合规红线审查方案,避免后期返工"},
|
||||
{"agent_id": "roundtable-execution-plan", "reason": "把改造拆成可在 6 个月内交付的里程碑与资源排布"},
|
||||
{"agent_id": "compliance-research", "reason": "检索同类等保 2.0 改造案例与监管口径,直接支撑本次合规对标"},
|
||||
{"agent_id": "solution-architect", "reason": "在 6 个月工期约束下产出主备改造方案与关键路径"},
|
||||
{"agent_id": "risk-auditor", "reason": "对照等保 2.0 合规红线审查方案,避免后期返工"},
|
||||
{"agent_id": "execution-planner", "reason": "把改造拆成可在 6 个月内交付的里程碑与资源排布"},
|
||||
{"agent_id": "my-cost-calc", "reason": "校验整体方案落在 500 万元预算硬约束内"}
|
||||
]
|
||||
}
|
||||
@@ -453,7 +452,7 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
|
||||
- channel-planner (渠道规划师):按预算和受众设计跨平台投放组合
|
||||
- audience-insight (受众洞察):画像、关键词、情绪点研究
|
||||
- creative-copy (创意文案):产出落地页与社媒文案变体
|
||||
- roundtable-risk-review (风险审查):合规红线检查与收敛建议
|
||||
- ad-compliance (广告合规):审查文案与素材合规风险
|
||||
```
|
||||
|
||||
**你的输出**:
|
||||
@@ -471,8 +470,8 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
|
||||
}
|
||||
```
|
||||
|
||||
> **关键观察**:示例 A 选了 `roundtable-intelligence / solution-design / execution-plan` 等"职能型"智能体,示例 B **完全没用任何 `roundtable-*` 内置**,而是从用户自建的领域智能体里挑。
|
||||
> 这是因为:**候选池里的智能体能不能匹配任务,只看 description 和任务的实际语义,不看 id 是不是 `roundtable-` 开头**。如果候选池里有"广告设计智能体"且任务是广告设计,**你必须挑它**,而不是套用示例 A 的职能链。
|
||||
> **关键观察**:两个示例都从候选池里按任务语义挑选,**不要**套用固定职能链(如"调研→设计→风险→执行")。
|
||||
> 候选池里的智能体能不能匹配任务,只看 description 和任务的实际语义。如果候选池里有"广告设计智能体"且任务是广告设计,**你必须挑它**,而不是机械复制示例 A 的链路。
|
||||
"""
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user