代码提交
This commit is contained in:
1 parent
ace30af202
commit
64afa9a1ed
5 files changed
+409
-172
No files matched your search
@@ -1,5 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/** Dev demo 调试页:填后端地址 / token / 智能体 id,把面板塞进一个弹窗,验证嵌入与流式。 */
|
||||
/**
|
||||
* Dev demo 调试页:填后端地址 / token / 智能体 id,把面板塞进一个弹窗,验证嵌入与流式。
|
||||
*
|
||||
* 同时作为「新契约」的参考实现:任务名 / 简介 / 模型由宿主持有并通过 props 传入,
|
||||
* 「重新分析 / 保存 / 停止」等按钮由宿主渲染,通过模板 ref 调用面板 expose 出的方法。
|
||||
*/
|
||||
import { reactive, ref } from "vue";
|
||||
import ParallelAgentsPanel from "./components/ParallelAgentsPanel.vue";
|
||||
import type { AgentInput, AgentResult } from "./types";
|
||||
@@ -16,13 +21,24 @@ const cfg = reactive({
|
||||
const open = ref(false);
|
||||
const agents = ref<AgentInput[]>([]);
|
||||
|
||||
// 宿主持有:任务名 / 简介 / 模型(面板不再渲染这些输入)。
|
||||
const taskName = ref(localStorage.getItem("pa.taskName") || "");
|
||||
const brief = ref(localStorage.getItem("pa.brief") || "");
|
||||
const model = ref(localStorage.getItem("pa.model") || "");
|
||||
|
||||
// 模板 ref:拿到面板暴露的方法 / 状态。
|
||||
const panel = ref<InstanceType<typeof ParallelAgentsPanel> | null>(null);
|
||||
|
||||
// 面板保存 / 生成 id 时回填,方便复制下来下次填进去测「按 taskId 回显」。
|
||||
function onTaskId(id: string) {
|
||||
cfg.taskId = id;
|
||||
localStorage.setItem("pa.taskId", id);
|
||||
}
|
||||
function onAgentCompleted(result: AgentResult) {
|
||||
console.log("[demo] agent-completed → 某智能体分析完:", result.name, result.status, result.content);
|
||||
}
|
||||
function onCompleted(results: AgentResult[]) {
|
||||
console.log("[demo] completed → 各智能体返回内容:", results);
|
||||
console.log("[demo] completed → 全部分析完,各智能体返回内容:", results);
|
||||
}
|
||||
|
||||
function parseAgents(): AgentInput[] {
|
||||
@@ -68,21 +84,74 @@ function launch() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 模拟宿主弹窗:面板自适应容器 -->
|
||||
<!-- 模拟宿主弹窗:宿主自己渲染「任务名 / 简介 / 模型 + 按钮」,面板只展示会话 -->
|
||||
<div
|
||||
v-if="open"
|
||||
style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 50"
|
||||
@click.self="open = false"
|
||||
>
|
||||
<div style="width: 92vw; max-width: 1080px; height: 86vh; border-radius: 12px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.5)">
|
||||
<ParallelAgentsPanel
|
||||
:base-url="cfg.baseUrl"
|
||||
:username="cfg.username || undefined"
|
||||
:task-id="cfg.taskId || undefined"
|
||||
:agents="agents"
|
||||
@update:taskId="onTaskId"
|
||||
@completed="onCompleted"
|
||||
/>
|
||||
<div style="width: 92vw; max-width: 1080px; height: 86vh; border-radius: 12px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.5); display: flex; flex-direction: column; background: #0d1526">
|
||||
<!-- 宿主工具条:这部分原来在面板内,现在由宿主自己掌控 -->
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; border-bottom: 1px solid #1e2d4d">
|
||||
<input
|
||||
v-model="taskName"
|
||||
placeholder="任务名称"
|
||||
style="padding: 6px 10px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px"
|
||||
/>
|
||||
<textarea
|
||||
v-model="brief"
|
||||
rows="2"
|
||||
placeholder="任务简介:把背景、目标、约束写清楚"
|
||||
style="padding: 6px 10px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px; resize: vertical"
|
||||
/>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 8px">
|
||||
<!-- expose 出的 ref 在组件实例上会自动解包,直接读,无需 .value -->
|
||||
<button
|
||||
v-if="!panel?.running"
|
||||
:disabled="!panel?.canStart"
|
||||
@click="panel?.reanalyze()"
|
||||
style="padding: 6px 14px; background: #1677ff; color: #fff; border: 0; border-radius: 6px; cursor: pointer"
|
||||
>
|
||||
{{ panel?.hasResults ? "重新分析" : "开始分析" }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click="panel?.cancel()"
|
||||
style="padding: 6px 14px; background: #2d4473; color: #e5edf7; border: 0; border-radius: 6px; cursor: pointer"
|
||||
>
|
||||
停止
|
||||
</button>
|
||||
<button
|
||||
:disabled="panel?.saving || !panel?.hasResults"
|
||||
@click="panel?.save()"
|
||||
style="padding: 6px 14px; background: #2d4473; color: #e5edf7; border: 0; border-radius: 6px; cursor: pointer"
|
||||
>
|
||||
{{ panel?.saving ? "保存中…" : cfg.taskId ? "更新保存" : "保存" }}
|
||||
</button>
|
||||
<input
|
||||
v-model="model"
|
||||
placeholder="模型(留空=默认)"
|
||||
style="margin-left: auto; width: 200px; padding: 6px 10px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 面板:自适应容器,纯展示会话 -->
|
||||
<div style="flex: 1; min-height: 0">
|
||||
<ParallelAgentsPanel
|
||||
ref="panel"
|
||||
:base-url="cfg.baseUrl"
|
||||
:username="cfg.username || undefined"
|
||||
:task-id="cfg.taskId || undefined"
|
||||
v-model:task-name="taskName"
|
||||
v-model:brief="brief"
|
||||
:model="model"
|
||||
:agents="agents"
|
||||
@update:taskId="onTaskId"
|
||||
@agent-completed="onAgentCompleted"
|
||||
@completed="onCompleted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 并行多智能体面板(主组件,可嵌入)。
|
||||
* 并行多智能体面板(主组件,可嵌入)—— 纯展示 + 暴露方法版本。
|
||||
*
|
||||
* 输入「任务名称 + 简介」,并行调用宿主传入的多个智能体(后端固定禁写文件),消息列表
|
||||
* 照搬圆桌 Step2 并行多席位的深色样式与结构(步骤条 / 并行双列 / 席位状态条),支持保存
|
||||
* 聊天记录 + 回显 + 重新分析。
|
||||
* 「任务名称 / 简介 / 模型」全部由宿主通过 props 传入;本组件不再渲染顶部表单与按钮,
|
||||
* 只展示「席位状态条 + 并行会话」。宿主自行渲染「重新分析 / 保存」等按钮,并通过模板 ref
|
||||
* 调用本组件暴露的 `reanalyze()` / `save()` / `cancel()` 等方法。
|
||||
*
|
||||
* 并行调用宿主传入的多个智能体(后端固定禁写文件),消息列表照搬圆桌 Step2 并行多席位的
|
||||
* 深色样式与结构(步骤条 / 并行双列 / 席位状态条),支持保存聊天记录 + 回显 + 重新分析。
|
||||
*
|
||||
* 自适应容器:根元素 `h-full w-full flex flex-col min-h-0`,宿主直接塞进抽屉 / 弹窗即可。
|
||||
*/
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import type { AgentInput, AgentResult, Dialogue, SavedTask, SeatView } from "@/types";
|
||||
import { useParallelRun } from "@/composables/useParallelRun";
|
||||
import {
|
||||
createTask,
|
||||
ensureTask,
|
||||
listModels,
|
||||
listTasks,
|
||||
updateTask,
|
||||
type ApiConfig,
|
||||
type ModelItem,
|
||||
} from "@/api";
|
||||
import { DEFAULT_BASE_URL, DEFAULT_MODEL } from "@/config";
|
||||
import { DEFAULT_BASE_URL } from "@/config";
|
||||
import ParallelSeatStrip from "./ParallelSeatStrip.vue";
|
||||
import MessageBubble from "./MessageBubble.vue";
|
||||
|
||||
@@ -37,15 +38,26 @@ const props = withDefaults(
|
||||
loginPassword?: string;
|
||||
agents: AgentInput[];
|
||||
taskId?: string;
|
||||
/** 任务名称:由宿主传入(可配合 update:taskName 做 v-model)。 */
|
||||
taskName?: string;
|
||||
/** 任务简介:由宿主传入(可配合 update:brief 做 v-model)。 */
|
||||
brief?: string;
|
||||
/** 本次并行分析使用的模型;空 = 后端回退默认。 */
|
||||
model?: string;
|
||||
dark?: boolean;
|
||||
}>(),
|
||||
{ dark: true },
|
||||
{ dark: true, taskName: "", brief: "", model: "" },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "saved", task: SavedTask): void;
|
||||
(e: "update:taskId", id: string): void;
|
||||
/** 回显已保存任务时回传标题,宿主可据此同步自己的输入框。 */
|
||||
(e: "update:taskName", name: string): void;
|
||||
/** 回显已保存任务时回传简介。 */
|
||||
(e: "update:brief", brief: string): void;
|
||||
/** 单个智能体一分析完就回传它的结果(每个席位完成时各触发一次)。 */
|
||||
(e: "agent-completed", result: AgentResult): void;
|
||||
/** 一轮并行分析结束时回传各智能体的返回内容。 */
|
||||
(e: "completed", results: AgentResult[]): void;
|
||||
}>();
|
||||
@@ -58,21 +70,26 @@ const cfg = computed<ApiConfig>(() => ({
|
||||
loginPassword: props.loginPassword,
|
||||
}));
|
||||
|
||||
const run = useParallelRun();
|
||||
// 单个智能体完成即回传它的结果(一分析完就给宿主)。
|
||||
const run = useParallelRun({
|
||||
onAgentSettled: (agentId) => {
|
||||
const r = getResult(agentId);
|
||||
if (r) emit("agent-completed", r);
|
||||
},
|
||||
});
|
||||
const { dialogues, seats, running } = run;
|
||||
|
||||
const form = reactive({ taskName: "", brief: "" });
|
||||
// 任务名 / 简介 / 模型全部由宿主 props 提供(本组件不再持有可编辑表单)。
|
||||
const taskName = computed(() => props.taskName ?? "");
|
||||
const brief = computed(() => props.brief ?? "");
|
||||
const model = computed(() => (props.model || "").trim() || undefined);
|
||||
|
||||
const currentTaskId = ref<string | null>(props.taskId ?? null);
|
||||
const selected = ref<string[]>([]);
|
||||
const saving = ref(false);
|
||||
const history = ref<SavedTask[]>([]);
|
||||
const showHistory = ref(false);
|
||||
const errorMsg = ref<string | null>(null);
|
||||
|
||||
// 模型切换:拉后端模型列表,空字符串 = 用默认(prop/config/后端回退)。
|
||||
const models = ref<ModelItem[]>([]);
|
||||
const selectedModel = ref<string>(props.model ?? DEFAULT_MODEL ?? "");
|
||||
|
||||
const dialogueById = computed(() => {
|
||||
const m = new Map<number, Dialogue>();
|
||||
for (const d of dialogues.value) m.set(d.id, d);
|
||||
@@ -81,7 +98,7 @@ const dialogueById = computed(() => {
|
||||
|
||||
const hasResults = computed(() => dialogues.value.length > 0);
|
||||
const canStart = computed(
|
||||
() => !running.value && props.agents.length > 0 && (form.taskName.trim() || form.brief.trim()),
|
||||
() => !running.value && props.agents.length > 0 && !!(taskName.value.trim() || brief.value.trim()),
|
||||
);
|
||||
|
||||
// 单选:主区每次只显示**一个**席位的消息列表,点头像切换。
|
||||
@@ -137,21 +154,43 @@ function getResults(): AgentResult[] {
|
||||
function getResult(agentId: string): AgentResult | null {
|
||||
return getResults().find((r) => r.agent_id === agentId) ?? null;
|
||||
}
|
||||
// 模板 ref 用:`<ParallelAgentsPanel ref="panelRef" />` → panelRef.value.getResults()
|
||||
defineExpose({ getResults, getResult, start: () => onStart(), cancel: () => run.cancel() });
|
||||
// 模板 ref 用:`<ParallelAgentsPanel ref="panelRef" />`
|
||||
// 宿主自行渲染按钮,调用下面这些方法 / 读取状态:
|
||||
// panelRef.value.reanalyze() // 开始 / 重新分析
|
||||
// panelRef.value.save() // 保存 / 更新保存
|
||||
// panelRef.value.cancel() // 停止
|
||||
// panelRef.value.loadTask(id) // 回显某条历史
|
||||
// panelRef.value.running / saving / hasResults / canStart // 按钮态(ref)
|
||||
defineExpose({
|
||||
getResults,
|
||||
getResult,
|
||||
start: () => onStart(),
|
||||
reanalyze: () => onStart(),
|
||||
save: () => onSave(),
|
||||
cancel: () => run.cancel(),
|
||||
loadTask: (id: string) => loadTask(id),
|
||||
refreshHistory: () => refreshHistory(),
|
||||
history,
|
||||
running,
|
||||
saving,
|
||||
hasResults,
|
||||
canStart,
|
||||
errorMsg,
|
||||
});
|
||||
|
||||
/** 用户在某智能体的协作卡片里提交了回答 → 带上回答重跑该智能体。 */
|
||||
async function onClarify(agentId: string, answer: string) {
|
||||
errorMsg.value = null;
|
||||
const brief = `${form.brief}\n\n【用户补充】${answer}`.trim();
|
||||
const merged = `${brief.value}\n\n【用户补充】${answer}`.trim();
|
||||
try {
|
||||
await run.rerunAgent(cfg.value, {
|
||||
taskName: form.taskName,
|
||||
brief,
|
||||
taskName: taskName.value,
|
||||
brief: merged,
|
||||
agentId,
|
||||
model: selectedModel.value || undefined,
|
||||
model: model.value,
|
||||
});
|
||||
emit("completed", getResults());
|
||||
void autoSave(); // 重跑完也自动存一次
|
||||
} catch (err) {
|
||||
errorMsg.value = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
@@ -162,26 +201,37 @@ async function onStart() {
|
||||
errorMsg.value = null;
|
||||
try {
|
||||
await run.start(cfg.value, {
|
||||
taskName: form.taskName,
|
||||
brief: form.brief,
|
||||
taskName: taskName.value,
|
||||
brief: brief.value,
|
||||
agents: props.agents,
|
||||
model: selectedModel.value || undefined,
|
||||
model: model.value,
|
||||
});
|
||||
// 一轮结束:回传各智能体返回内容(宿主也可用 ref 调 getResults())。
|
||||
emit("completed", getResults());
|
||||
void autoSave(); // 分析完自动保存一次
|
||||
} catch (err) {
|
||||
errorMsg.value = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
}
|
||||
|
||||
/** 一轮分析结束后自动保存一次(有结果才存;失败静默,不打断主流程)。 */
|
||||
async function autoSave() {
|
||||
if (!hasResults.value || saving.value) return;
|
||||
try {
|
||||
await onSave();
|
||||
} catch {
|
||||
/* 自动保存失败不打扰用户,错误已在 onSave 内记录 */
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (saving.value || !hasResults.value) return;
|
||||
saving.value = true;
|
||||
errorMsg.value = null;
|
||||
try {
|
||||
const title = form.taskName.trim() || "未命名任务";
|
||||
const title = taskName.value.trim() || "未命名任务";
|
||||
const agentsSnapshot: AgentInput[] = props.agents.map((a) => ({ ...a }));
|
||||
const payload = { title, brief: form.brief, agents: agentsSnapshot, messages: dialogues.value };
|
||||
const payload = { title, brief: brief.value, agents: agentsSnapshot, messages: dialogues.value };
|
||||
// 按宿主传入的 taskId **upsert**:有 id 先更新,不存在(404)再用该 id 创建;无 id 则新建。
|
||||
let saved: SavedTask | null;
|
||||
const id = currentTaskId.value;
|
||||
@@ -206,14 +256,17 @@ async function onSave() {
|
||||
|
||||
async function refreshHistory() {
|
||||
try {
|
||||
history.value = await listTasks(cfg.value);
|
||||
const list = await listTasks(cfg.value);
|
||||
// 按更新时间倒序,最新的在最前(供「进入默认显示最新一条」用)。
|
||||
history.value = [...list].sort((a, b) =>
|
||||
(b.updated_at || b.created_at || "").localeCompare(a.updated_at || a.created_at || ""),
|
||||
);
|
||||
} catch {
|
||||
/* 历史拉取失败不阻断主流程 */
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTask(id: string) {
|
||||
showHistory.value = false;
|
||||
errorMsg.value = null;
|
||||
try {
|
||||
// get-or-create:新 id 直接初始化空记录(不 404);已存在则回显其聊天记录。
|
||||
@@ -224,11 +277,12 @@ async function loadTask(id: string) {
|
||||
currentTaskId.value = task.id;
|
||||
selected.value = [];
|
||||
if (task.messages && task.messages.length) {
|
||||
form.taskName = task.title ?? "";
|
||||
form.brief = task.brief ?? "";
|
||||
// 标题 / 简介由宿主持有:回传让宿主同步自己的输入框(v-model)。
|
||||
emit("update:taskName", task.title ?? "");
|
||||
emit("update:brief", task.brief ?? "");
|
||||
run.hydrate(task.messages);
|
||||
} else {
|
||||
// 空记录(新初始化):清空当前会话,等用户填任务 / 开始分析。
|
||||
// 空记录(新初始化):清空当前会话,等宿主填任务 / 触发分析。
|
||||
run.reset();
|
||||
}
|
||||
emit("update:taskId", task.id);
|
||||
@@ -237,19 +291,14 @@ async function loadTask(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshModels() {
|
||||
const list = await listModels(cfg.value);
|
||||
models.value = list;
|
||||
// 当前选择不在列表里(且非空)时,回退为默认(空 = 后端回退)。
|
||||
if (selectedModel.value && !list.some((m) => m.name === selectedModel.value)) {
|
||||
selectedModel.value = "";
|
||||
onMounted(async () => {
|
||||
await refreshHistory();
|
||||
if (props.taskId) {
|
||||
void loadTask(props.taskId);
|
||||
} else if (history.value.length) {
|
||||
// 没指定 taskId:默认回显最新一条聊天记录。
|
||||
void loadTask(history.value[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void refreshHistory();
|
||||
void refreshModels();
|
||||
if (props.taskId) void loadTask(props.taskId);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -258,98 +307,17 @@ onMounted(() => {
|
||||
class="parallel-agents pa-bg flex h-full w-full min-h-0 flex-col"
|
||||
:class="dark ? 'parallel-agents--dark' : ''"
|
||||
>
|
||||
<!-- 顶部:任务名 + 操作 -->
|
||||
<div class="pa-border flex shrink-0 flex-col gap-2 border-b px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="form.taskName"
|
||||
class="pa-input min-w-0 flex-1 px-3 py-1.5 text-sm"
|
||||
placeholder="任务名称"
|
||||
/>
|
||||
<div class="relative shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class="pa-btn-ghost px-2.5 py-1.5 text-xs"
|
||||
@click="showHistory = !showHistory"
|
||||
>
|
||||
历史 ▾
|
||||
</button>
|
||||
<div
|
||||
v-if="showHistory"
|
||||
class="pa-card pa-border absolute right-0 z-20 mt-1 max-h-72 w-72 overflow-auto rounded-lg border p-1 shadow-lg"
|
||||
>
|
||||
<div v-if="!history.length" class="px-3 py-4 text-center text-xs pa-text-muted">
|
||||
暂无保存的任务
|
||||
</div>
|
||||
<button
|
||||
v-for="t in history"
|
||||
:key="t.id"
|
||||
type="button"
|
||||
class="pa-btn-ghost block w-full truncate px-2.5 py-1.5 text-left text-xs"
|
||||
style="border: 0"
|
||||
@click="loadTask(t.id)"
|
||||
>
|
||||
<span class="font-semibold">{{ t.title || "未命名任务" }}</span>
|
||||
<span class="pa-text-muted"> · {{ (t.updated_at || "").slice(0, 16).replace("T", " ") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="form.brief"
|
||||
rows="2"
|
||||
class="pa-input w-full resize-y px-3 py-1.5 text-sm"
|
||||
placeholder="任务简介:把背景、目标、约束写清楚,会并行下发给各智能体"
|
||||
/>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
v-if="!running"
|
||||
type="button"
|
||||
class="pa-btn-primary px-3.5 py-1.5 text-sm font-semibold"
|
||||
:disabled="!canStart"
|
||||
@click="onStart"
|
||||
>
|
||||
{{ hasResults ? "重新分析" : "开始分析" }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="pa-btn-ghost px-3.5 py-1.5 text-sm font-semibold"
|
||||
@click="run.cancel()"
|
||||
>
|
||||
停止
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pa-btn-ghost px-3.5 py-1.5 text-sm font-semibold"
|
||||
:disabled="saving || !hasResults"
|
||||
@click="onSave"
|
||||
>
|
||||
{{ saving ? "保存中…" : currentTaskId ? "更新保存" : "保存" }}
|
||||
</button>
|
||||
<!-- 顶部表单与按钮已移除:任务名 / 简介 / 模型由宿主 props 传入,
|
||||
「重新分析 / 保存 / 停止」由宿主调用本组件 expose 出的方法。 -->
|
||||
|
||||
<!-- 模型切换:作用于下一次「开始分析 / 重新分析」 -->
|
||||
<label class="ml-auto flex items-center gap-1.5 text-xs pa-text-muted">
|
||||
模型
|
||||
<select
|
||||
v-model="selectedModel"
|
||||
:disabled="running"
|
||||
class="pa-input px-2 py-1 text-xs"
|
||||
title="选择本次并行分析使用的模型"
|
||||
>
|
||||
<option value="">默认</option>
|
||||
<option v-for="m in models" :key="m.name" :value="m.name">
|
||||
{{ m.display_name || m.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<span v-if="props.agents.length === 0" class="text-xs pa-text-muted">
|
||||
⚠️ 未传入任何智能体(agents 为空)
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="errorMsg" class="text-xs" style="color: var(--pa-rose)">{{ errorMsg }}</p>
|
||||
</div>
|
||||
<!-- 错误提示(保留,便于宿主无需额外处理也能看到失败原因) -->
|
||||
<p
|
||||
v-if="errorMsg"
|
||||
class="pa-border shrink-0 border-b px-4 py-2 text-xs"
|
||||
style="color: var(--pa-rose)"
|
||||
>
|
||||
{{ errorMsg }}
|
||||
</p>
|
||||
|
||||
<!-- 席位状态条 -->
|
||||
<div v-if="seats.length" class="pa-border shrink-0 border-b px-4">
|
||||
@@ -383,7 +351,7 @@ onMounted(() => {
|
||||
v-else
|
||||
class="flex h-full flex-col items-center justify-center gap-2 text-center"
|
||||
>
|
||||
<p class="text-sm pa-text-muted">填写任务名称与简介,点「开始分析」并行调用各智能体。</p>
|
||||
<p class="text-sm pa-text-muted">等待宿主触发「开始 / 重新分析」,并行调用各智能体。</p>
|
||||
<p class="text-xs pa-text-muted">并行运行期间禁止写文件,产出直接呈现在正文。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,12 +18,25 @@ function nextId(): number {
|
||||
return _seq++;
|
||||
}
|
||||
|
||||
export function useParallelRun() {
|
||||
export interface ParallelRunOptions {
|
||||
/** 单个智能体进入终态(完成 / 待确认 / 出错)时回调一次,用于「一分析完就回传」。 */
|
||||
onAgentSettled?: (agentId: string, status: SeatView["status"]) => void;
|
||||
}
|
||||
|
||||
export function useParallelRun(opts: ParallelRunOptions = {}) {
|
||||
const dialogues = ref<Dialogue[]>([]);
|
||||
const seats = ref<SeatView[]>([]);
|
||||
const running = ref(false);
|
||||
const abortRef = shallowRef<AbortController | null>(null);
|
||||
|
||||
// 本轮已「结算」(回调过 onAgentSettled)的 agentId,去重,避免重复回传。
|
||||
const settled = new Set<string>();
|
||||
function settleAgent(agentId: string, status: SeatView["status"]) {
|
||||
if (settled.has(agentId)) return;
|
||||
settled.add(agentId);
|
||||
opts.onAgentSettled?.(agentId, status);
|
||||
}
|
||||
|
||||
// 增量节流:累积到 pending,rAF 合并刷新一次,避免逐 token reflow。
|
||||
const pending = new Map<string, string>(); // agentId -> 待追加文本
|
||||
let rafScheduled = false;
|
||||
@@ -93,6 +106,7 @@ export function useParallelRun() {
|
||||
}
|
||||
setSeatStatus(ev.agent_id, "done");
|
||||
dialogues.value = [...dialogues.value];
|
||||
settleAgent(ev.agent_id, "done");
|
||||
break;
|
||||
}
|
||||
case "agent_clarification": {
|
||||
@@ -113,6 +127,7 @@ export function useParallelRun() {
|
||||
}
|
||||
setSeatStatus(ev.agent_id, "done");
|
||||
dialogues.value = [...dialogues.value];
|
||||
settleAgent(ev.agent_id, "done");
|
||||
break;
|
||||
}
|
||||
case "agent_error": {
|
||||
@@ -125,6 +140,7 @@ export function useParallelRun() {
|
||||
}
|
||||
setSeatStatus(ev.agent_id, "error");
|
||||
dialogues.value = [...dialogues.value];
|
||||
settleAgent(ev.agent_id, "error");
|
||||
break;
|
||||
}
|
||||
case "all_done":
|
||||
@@ -188,6 +204,8 @@ export function useParallelRun() {
|
||||
}
|
||||
}
|
||||
dialogues.value = [...dialogues.value];
|
||||
// 兜底:没收到终态事件就断流的席位也回调一次(按当前席位状态)。
|
||||
for (const s of seats.value) settleAgent(s.agentId, s.status === "error" ? "error" : "done");
|
||||
running.value = false;
|
||||
abortRef.value = null;
|
||||
}
|
||||
@@ -218,6 +236,7 @@ export function useParallelRun() {
|
||||
setSeatStatus(args.agentId, "running");
|
||||
dialogues.value = [...dialogues.value];
|
||||
|
||||
settled.delete(args.agentId); // 该席位重跑,允许再次回传
|
||||
running.value = true;
|
||||
const ac = new AbortController();
|
||||
abortRef.value = ac;
|
||||
@@ -235,6 +254,8 @@ export function useParallelRun() {
|
||||
if (dd.confidence === "并行执行中") dd.confidence = "已完成";
|
||||
}
|
||||
dialogues.value = [...dialogues.value];
|
||||
const st = seats.value.find((x) => x.agentId === args.agentId);
|
||||
settleAgent(args.agentId, st?.status === "error" ? "error" : "done");
|
||||
running.value = false;
|
||||
abortRef.value = null;
|
||||
}
|
||||
@@ -266,6 +287,7 @@ export function useParallelRun() {
|
||||
cancel();
|
||||
pending.clear();
|
||||
bubbleByAgent.clear();
|
||||
settled.clear();
|
||||
dialogues.value = [];
|
||||
seats.value = [];
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user