mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat: update automation workspace and release pipeline
This commit is contained in:
@@ -10,6 +10,7 @@ import { SettingsTabs } from "@/types/settings";
|
||||
const {
|
||||
mockBuildClawAgentParams,
|
||||
mockCreateAutomationJob,
|
||||
mockCreateContent,
|
||||
mockHomeShellExecutionStrategy,
|
||||
mockHomeShellModel,
|
||||
mockHomeShellProviderType,
|
||||
@@ -82,6 +83,7 @@ const {
|
||||
runnerType: "instant",
|
||||
defaultExecutorBinding: "agent_turn",
|
||||
executionLocation: "client_default",
|
||||
defaultArtifactKind: "brief",
|
||||
themeTarget: "video",
|
||||
version: "seed-v1",
|
||||
slotSchema: [
|
||||
@@ -112,6 +114,7 @@ const {
|
||||
runnerType: "scheduled",
|
||||
defaultExecutorBinding: "automation_job",
|
||||
executionLocation: "client_default",
|
||||
defaultArtifactKind: "analysis",
|
||||
themeTarget: "social-media",
|
||||
version: "seed-v1",
|
||||
slotSchema: [
|
||||
@@ -180,6 +183,22 @@ const {
|
||||
...request,
|
||||
}),
|
||||
),
|
||||
mockCreateContent: vi.fn(async (request: Record<string, unknown>) => ({
|
||||
id: "content-service-skill-1",
|
||||
project_id: request.project_id,
|
||||
title: request.title,
|
||||
content_type: request.content_type,
|
||||
status: "draft",
|
||||
order: 0,
|
||||
word_count: 0,
|
||||
body: typeof request.body === "string" ? request.body : "",
|
||||
metadata:
|
||||
request.metadata && typeof request.metadata === "object"
|
||||
? request.metadata
|
||||
: undefined,
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
})),
|
||||
mockHomeShellProviderType: { current: "mock-provider" },
|
||||
mockHomeShellModel: { current: "mock-model" },
|
||||
mockHomeShellExecutionStrategy: { current: "react" },
|
||||
@@ -366,6 +385,17 @@ vi.mock("@/lib/api/serviceSkillRuns", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api/project", () => ({
|
||||
createContent: mockCreateContent,
|
||||
getDefaultContentTypeForProject: vi.fn((projectType: string) => {
|
||||
switch (projectType) {
|
||||
case "video":
|
||||
return "episode";
|
||||
case "social-media":
|
||||
return "post";
|
||||
default:
|
||||
return "document";
|
||||
}
|
||||
}),
|
||||
listProjects: mockListProjects,
|
||||
}));
|
||||
|
||||
@@ -1138,11 +1168,27 @@ describe("AgentChatHomeShell", () => {
|
||||
|
||||
await flushEffects();
|
||||
|
||||
expect(mockCreateContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
project_id: "project-1",
|
||||
title: "复制短视频脚本",
|
||||
content_type: "episode",
|
||||
body: "",
|
||||
}),
|
||||
);
|
||||
expect(onEnterWorkspace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
projectId: "project-1",
|
||||
contentId: "content-service-skill-1",
|
||||
theme: "video",
|
||||
initialCreationMode: "guided",
|
||||
initialRequestMetadata: {
|
||||
artifact: {
|
||||
artifact_mode: "draft",
|
||||
artifact_kind: "brief",
|
||||
workbench_surface: "right_panel",
|
||||
},
|
||||
},
|
||||
initialUserPrompt:
|
||||
expect.stringContaining("[服务型技能] 复制短视频脚本"),
|
||||
}),
|
||||
@@ -1302,14 +1348,46 @@ describe("AgentChatHomeShell", () => {
|
||||
payload: expect.objectContaining({
|
||||
kind: "agent_turn",
|
||||
prompt: expect.stringContaining("[服务型技能] 每日趋势摘要"),
|
||||
content_id: "content-service-skill-1",
|
||||
request_metadata: expect.objectContaining({
|
||||
artifact: expect.objectContaining({
|
||||
artifact_mode: "draft",
|
||||
artifact_kind: "analysis",
|
||||
}),
|
||||
harness: expect.objectContaining({
|
||||
theme: "social-media",
|
||||
session_mode: "theme_workbench",
|
||||
content_id: "content-service-skill-1",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(mockCreateContent).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateContent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
project_id: "project-1",
|
||||
title: "每日趋势摘要",
|
||||
content_type: "post",
|
||||
body: "",
|
||||
}),
|
||||
);
|
||||
expect(mockCreateContent.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mockCreateAutomationJob.mock.invocationCallOrder[0],
|
||||
);
|
||||
expect(onEnterWorkspace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
projectId: "project-1",
|
||||
contentId: "content-service-skill-1",
|
||||
theme: "social-media",
|
||||
initialCreationMode: "guided",
|
||||
initialRequestMetadata: {
|
||||
artifact: {
|
||||
artifact_mode: "draft",
|
||||
artifact_kind: "analysis",
|
||||
workbench_surface: "right_panel",
|
||||
},
|
||||
},
|
||||
initialUserPrompt: expect.stringContaining("[服务型技能] 每日趋势摘要"),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
isTerminalServiceSkillRunStatus,
|
||||
type ServiceSkillRun,
|
||||
} from "@/lib/api/serviceSkillRuns";
|
||||
import { listProjects, type Project } from "@/lib/api/project";
|
||||
import { createContent, listProjects, type Project } from "@/lib/api/project";
|
||||
import {
|
||||
AutomationJobDialog,
|
||||
type AutomationJobDialogInitialValues,
|
||||
@@ -19,9 +19,7 @@ import type { Page, PageParams } from "@/types/page";
|
||||
import { SettingsTabs } from "@/types/settings";
|
||||
import { EmptyState } from "./components/EmptyState";
|
||||
import type { CreationMode } from "./components/types";
|
||||
import {
|
||||
saveChatToolPreferences,
|
||||
} from "./utils/chatToolPreferences";
|
||||
import { saveChatToolPreferences } from "./utils/chatToolPreferences";
|
||||
import { isTeamRuntimeRecommendation } from "./utils/contextualRecommendations";
|
||||
import { resolveClawWorkspaceProviderSelection } from "./utils/clawWorkspaceProviderSelection";
|
||||
import { normalizeProjectId } from "./utils/topicProjectResolution";
|
||||
@@ -53,6 +51,7 @@ import { ServiceSkillHomePanel } from "./service-skills/ServiceSkillHomePanel";
|
||||
import { ServiceSkillLaunchDialog } from "./service-skills/ServiceSkillLaunchDialog";
|
||||
import { composeServiceSkillPrompt } from "./service-skills/promptComposer";
|
||||
import {
|
||||
buildServiceSkillAutomationAgentTurnPayloadContext,
|
||||
buildServiceSkillAutomationInitialValues,
|
||||
supportsServiceSkillLocalAutomation,
|
||||
} from "./service-skills/automationDraft";
|
||||
@@ -61,6 +60,7 @@ import type {
|
||||
ServiceSkillHomeItem,
|
||||
ServiceSkillSlotValues,
|
||||
} from "./service-skills/types";
|
||||
import { buildServiceSkillWorkspaceSeed } from "./service-skills/workspaceLaunch";
|
||||
|
||||
const PageContainer = styled.div<{ $compact?: boolean }>`
|
||||
display: flex;
|
||||
@@ -213,10 +213,14 @@ function prioritizeAutomationWorkspaces(
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
const matched = workspaces.find((workspace) => workspace.id === normalizedProjectId);
|
||||
const matched = workspaces.find(
|
||||
(workspace) => workspace.id === normalizedProjectId,
|
||||
);
|
||||
const fallbackWorkspace =
|
||||
matched ?? buildFallbackAutomationWorkspace(normalizedProjectId, theme);
|
||||
const remaining = workspaces.filter((workspace) => workspace.id !== normalizedProjectId);
|
||||
const remaining = workspaces.filter(
|
||||
(workspace) => workspace.id !== normalizedProjectId,
|
||||
);
|
||||
|
||||
return [fallbackWorkspace, ...remaining];
|
||||
}
|
||||
@@ -253,7 +257,8 @@ function buildServiceSkillRunSuccessMessage(
|
||||
}
|
||||
|
||||
interface PendingServiceSkillAutomationLaunch {
|
||||
enterWorkspacePayload: HomeShellEnterWorkspacePayload;
|
||||
skill: ServiceSkillHomeItem;
|
||||
prompt: string;
|
||||
usage: {
|
||||
skillId: string;
|
||||
runnerType: ServiceSkillHomeItem["runnerType"];
|
||||
@@ -327,7 +332,9 @@ export function AgentChatHomeShell({
|
||||
const [automationDialogOpen, setAutomationDialogOpen] = useState(false);
|
||||
const [automationDialogInitialValues, setAutomationDialogInitialValues] =
|
||||
useState<AutomationJobDialogInitialValues | null>(null);
|
||||
const [automationWorkspaces, setAutomationWorkspaces] = useState<Project[]>([]);
|
||||
const [automationWorkspaces, setAutomationWorkspaces] = useState<Project[]>(
|
||||
[],
|
||||
);
|
||||
const [automationJobSaving, setAutomationJobSaving] = useState(false);
|
||||
const [pendingServiceSkillAutomation, setPendingServiceSkillAutomation] =
|
||||
useState<PendingServiceSkillAutomationLaunch | null>(null);
|
||||
@@ -449,11 +456,13 @@ export function AgentChatHomeShell({
|
||||
launch.enterWorkspacePayload.themeOverride ?? activeTheme;
|
||||
|
||||
try {
|
||||
const providerSelection = await resolveClawWorkspaceProviderSelection({
|
||||
currentProviderType: providerType,
|
||||
currentModel: model,
|
||||
theme: targetTheme,
|
||||
});
|
||||
const providerSelection = await resolveClawWorkspaceProviderSelection(
|
||||
{
|
||||
currentProviderType: providerType,
|
||||
currentModel: model,
|
||||
theme: targetTheme,
|
||||
},
|
||||
);
|
||||
|
||||
if (providerSelection) {
|
||||
if (providerSelection.providerType !== providerType) {
|
||||
@@ -509,10 +518,13 @@ export function AgentChatHomeShell({
|
||||
],
|
||||
);
|
||||
|
||||
const handleServiceSkillSelect = useCallback((skill: ServiceSkillHomeItem) => {
|
||||
setSelectedServiceSkill(skill);
|
||||
setServiceSkillDialogOpen(true);
|
||||
}, []);
|
||||
const handleServiceSkillSelect = useCallback(
|
||||
(skill: ServiceSkillHomeItem) => {
|
||||
setSelectedServiceSkill(skill);
|
||||
setServiceSkillDialogOpen(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleOpenServiceSkillAutomationJob = useCallback(
|
||||
(skill: ServiceSkillHomeItem) => {
|
||||
@@ -529,11 +541,91 @@ export function AgentChatHomeShell({
|
||||
[onNavigate],
|
||||
);
|
||||
|
||||
const handleServiceSkillLaunch = useCallback(
|
||||
const createServiceSkillSeededContent = useCallback(
|
||||
async (skill: ServiceSkillHomeItem, projectId?: string | null) => {
|
||||
const normalizedProjectId = normalizeProjectId(
|
||||
projectId ?? currentProjectId,
|
||||
);
|
||||
const seed = buildServiceSkillWorkspaceSeed(
|
||||
skill,
|
||||
skill.themeTarget ?? activeTheme,
|
||||
);
|
||||
|
||||
if (!normalizedProjectId || !seed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createContent({
|
||||
project_id: normalizedProjectId,
|
||||
title: seed.title,
|
||||
content_type: seed.contentType,
|
||||
body: "",
|
||||
metadata: seed.metadata,
|
||||
});
|
||||
},
|
||||
[activeTheme, currentProjectId],
|
||||
);
|
||||
|
||||
const prepareServiceSkillWorkspacePayload = useCallback(
|
||||
async (
|
||||
skill: ServiceSkillHomeItem,
|
||||
slotValues: ServiceSkillSlotValues,
|
||||
) => {
|
||||
prompt: string,
|
||||
options?: {
|
||||
contentId?: string | null;
|
||||
projectId?: string | null;
|
||||
},
|
||||
): Promise<HomeShellEnterWorkspacePayload> => {
|
||||
const normalizedProjectId = normalizeProjectId(
|
||||
options?.projectId ?? currentProjectId,
|
||||
);
|
||||
const existingContentId = options?.contentId?.trim();
|
||||
const seed = buildServiceSkillWorkspaceSeed(
|
||||
skill,
|
||||
skill.themeTarget ?? activeTheme,
|
||||
);
|
||||
|
||||
if (existingContentId) {
|
||||
return {
|
||||
prompt,
|
||||
contentId: existingContentId,
|
||||
themeOverride: skill.themeTarget,
|
||||
initialRequestMetadata: seed?.requestMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
if (!normalizedProjectId || !seed) {
|
||||
return {
|
||||
prompt,
|
||||
themeOverride: skill.themeTarget,
|
||||
initialRequestMetadata: seed?.requestMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
const created = await createServiceSkillSeededContent(
|
||||
skill,
|
||||
normalizedProjectId,
|
||||
);
|
||||
|
||||
if (!created) {
|
||||
return {
|
||||
prompt,
|
||||
themeOverride: skill.themeTarget,
|
||||
initialRequestMetadata: seed.requestMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
prompt,
|
||||
contentId: created.id,
|
||||
themeOverride: skill.themeTarget,
|
||||
initialRequestMetadata: seed.requestMetadata,
|
||||
};
|
||||
},
|
||||
[activeTheme, createServiceSkillSeededContent, currentProjectId],
|
||||
);
|
||||
|
||||
const handleServiceSkillLaunch = useCallback(
|
||||
async (skill: ServiceSkillHomeItem, slotValues: ServiceSkillSlotValues) => {
|
||||
const prompt = composeServiceSkillPrompt({
|
||||
skill,
|
||||
slotValues,
|
||||
@@ -599,13 +691,23 @@ export function AgentChatHomeShell({
|
||||
}
|
||||
|
||||
if (skill.runnerType !== "instant") {
|
||||
toast.info("当前先进入工作区生成首版方案,下一阶段再接本地自动化任务。");
|
||||
toast.info(
|
||||
"当前先进入工作区生成首版方案,下一阶段再接本地自动化任务。",
|
||||
);
|
||||
}
|
||||
|
||||
const entered = handleEnterWorkspace({
|
||||
prompt,
|
||||
themeOverride: skill.themeTarget,
|
||||
});
|
||||
let workspacePayload: HomeShellEnterWorkspacePayload;
|
||||
try {
|
||||
workspacePayload = await prepareServiceSkillWorkspacePayload(
|
||||
skill,
|
||||
prompt,
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(`准备服务型技能工作区失败:${getErrorMessage(error)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entered = handleEnterWorkspace(workspacePayload);
|
||||
|
||||
if (!entered) {
|
||||
return;
|
||||
@@ -618,14 +720,16 @@ export function AgentChatHomeShell({
|
||||
setServiceSkillDialogOpen(false);
|
||||
setSelectedServiceSkill(null);
|
||||
},
|
||||
[handleEnterWorkspace, input, recordServiceSkillUsage],
|
||||
[
|
||||
handleEnterWorkspace,
|
||||
input,
|
||||
prepareServiceSkillWorkspacePayload,
|
||||
recordServiceSkillUsage,
|
||||
],
|
||||
);
|
||||
|
||||
const handleServiceSkillAutomationSetup = useCallback(
|
||||
async (
|
||||
skill: ServiceSkillHomeItem,
|
||||
slotValues: ServiceSkillSlotValues,
|
||||
) => {
|
||||
async (skill: ServiceSkillHomeItem, slotValues: ServiceSkillSlotValues) => {
|
||||
if (!supportsServiceSkillLocalAutomation(skill)) {
|
||||
await handleServiceSkillLaunch(skill, slotValues);
|
||||
return;
|
||||
@@ -670,10 +774,8 @@ export function AgentChatHomeShell({
|
||||
}),
|
||||
);
|
||||
setPendingServiceSkillAutomation({
|
||||
enterWorkspacePayload: {
|
||||
prompt,
|
||||
themeOverride: skill.themeTarget,
|
||||
},
|
||||
skill,
|
||||
prompt,
|
||||
usage: {
|
||||
skillId: skill.id,
|
||||
runnerType: skill.runnerType,
|
||||
@@ -705,10 +807,31 @@ export function AgentChatHomeShell({
|
||||
|
||||
setAutomationJobSaving(true);
|
||||
try {
|
||||
const createdJob = await createAutomationJob(payload.request);
|
||||
const pendingLaunch = pendingServiceSkillAutomation;
|
||||
let request = payload.request;
|
||||
let automationContentId: string | null = null;
|
||||
|
||||
if (pendingLaunch && request.payload.kind === "agent_turn") {
|
||||
const createdContent = await createServiceSkillSeededContent(
|
||||
pendingLaunch.skill,
|
||||
request.workspace_id,
|
||||
);
|
||||
automationContentId = createdContent?.id ?? null;
|
||||
request = {
|
||||
...request,
|
||||
payload: {
|
||||
...request.payload,
|
||||
...buildServiceSkillAutomationAgentTurnPayloadContext({
|
||||
skill: pendingLaunch.skill,
|
||||
contentId: automationContentId,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const createdJob = await createAutomationJob(request);
|
||||
toast.success(`本地自动化任务已创建:${createdJob.name}`);
|
||||
|
||||
const pendingLaunch = pendingServiceSkillAutomation;
|
||||
setAutomationDialogOpen(false);
|
||||
setAutomationDialogInitialValues(null);
|
||||
setPendingServiceSkillAutomation(null);
|
||||
@@ -723,7 +846,23 @@ export function AgentChatHomeShell({
|
||||
jobName: createdJob.name,
|
||||
});
|
||||
recordServiceSkillUsage(pendingLaunch.usage);
|
||||
const entered = handleEnterWorkspace(pendingLaunch.enterWorkspacePayload);
|
||||
let workspacePayload: HomeShellEnterWorkspacePayload;
|
||||
try {
|
||||
workspacePayload = await prepareServiceSkillWorkspacePayload(
|
||||
pendingLaunch.skill,
|
||||
pendingLaunch.prompt,
|
||||
{
|
||||
contentId: automationContentId,
|
||||
projectId: request.workspace_id,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`自动化任务已创建,但准备工作区失败:${getErrorMessage(error)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const entered = handleEnterWorkspace(workspacePayload);
|
||||
if (!entered) {
|
||||
toast.error("自动化任务已创建,但进入工作区失败,请稍后手动打开。");
|
||||
}
|
||||
@@ -734,7 +873,14 @@ export function AgentChatHomeShell({
|
||||
setAutomationJobSaving(false);
|
||||
}
|
||||
},
|
||||
[handleEnterWorkspace, pendingServiceSkillAutomation, recordServiceSkillUsage],
|
||||
[
|
||||
createServiceSkillSeededContent,
|
||||
currentProjectId,
|
||||
handleEnterWorkspace,
|
||||
pendingServiceSkillAutomation,
|
||||
prepareServiceSkillWorkspacePayload,
|
||||
recordServiceSkillUsage,
|
||||
],
|
||||
);
|
||||
|
||||
const handleRecommendationClick = useCallback(
|
||||
@@ -850,7 +996,9 @@ export function AgentChatHomeShell({
|
||||
catalogMeta={serviceSkillCatalogMeta}
|
||||
loading={serviceSkillsLoading}
|
||||
onSelect={handleServiceSkillSelect}
|
||||
onOpenAutomationJob={handleOpenServiceSkillAutomationJob}
|
||||
onOpenAutomationJob={
|
||||
handleOpenServiceSkillAutomationJob
|
||||
}
|
||||
/>
|
||||
<ClawHomeSolutionsPanel
|
||||
solutions={clawSolutions}
|
||||
|
||||
@@ -15,13 +15,8 @@ import {
|
||||
useRef,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
useAgentChatUnified,
|
||||
useCompatSubagentRuntime,
|
||||
} from "./hooks";
|
||||
import {
|
||||
type TaskStatusReason,
|
||||
} from "./hooks/agentChatShared";
|
||||
import { useAgentChatUnified, useCompatSubagentRuntime } from "./hooks";
|
||||
import { type TaskStatusReason } from "./hooks/agentChatShared";
|
||||
import {
|
||||
settleLiveArtifactAfterStreamStops,
|
||||
useArtifactDisplayState,
|
||||
@@ -72,9 +67,7 @@ import { logAgentDebug } from "@/lib/agentDebug";
|
||||
import { SettingsTabs } from "@/types/settings";
|
||||
import { setActiveContentTarget } from "@/lib/activeContentTarget";
|
||||
import { recordWorkspaceRepair } from "@/lib/workspaceHealthTelemetry";
|
||||
import {
|
||||
useImageGen,
|
||||
} from "@/components/image-gen/useImageGen";
|
||||
import { useImageGen } from "@/components/image-gen/useImageGen";
|
||||
import { resolveMediaGenerationPreference } from "@/lib/mediaGeneration";
|
||||
|
||||
import type {
|
||||
@@ -82,22 +75,15 @@ import type {
|
||||
SiteSavedContentTarget,
|
||||
WriteArtifactContext,
|
||||
} from "./types";
|
||||
import type {
|
||||
ThemeType,
|
||||
LayoutMode,
|
||||
} from "@/components/content-creator/types";
|
||||
import type { ThemeType, LayoutMode } from "@/components/content-creator/types";
|
||||
import { normalizeProjectId } from "./utils/topicProjectResolution";
|
||||
import {
|
||||
buildHarnessRequestMetadata,
|
||||
} from "./utils/harnessRequestMetadata";
|
||||
import { buildHarnessRequestMetadata } from "./utils/harnessRequestMetadata";
|
||||
import { deriveHarnessSessionState } from "./utils/harnessState";
|
||||
import {
|
||||
mergeArtifacts,
|
||||
resolveDefaultArtifactViewMode,
|
||||
} from "./utils/messageArtifacts";
|
||||
import {
|
||||
createChatToolPreferencesFromExecutionRuntime,
|
||||
} from "./utils/sessionExecutionRuntime";
|
||||
import { createChatToolPreferencesFromExecutionRuntime } from "./utils/sessionExecutionRuntime";
|
||||
import {
|
||||
buildRealSubagentTimelineItems,
|
||||
buildSyntheticSubagentTimelineItems,
|
||||
@@ -112,15 +98,11 @@ import { useThemeScopedChatToolPreferences } from "./hooks/useThemeScopedChatToo
|
||||
import { useLimeSkills } from "./hooks/useLimeSkills";
|
||||
import { useWorkspaceProjectSelection } from "./hooks/useWorkspaceProjectSelection";
|
||||
import { useBootstrapDispatchPreview } from "./hooks/useBootstrapDispatchPreview";
|
||||
import {
|
||||
useRuntimeTeamFormation,
|
||||
} from "./hooks/useRuntimeTeamFormation";
|
||||
import { useRuntimeTeamFormation } from "./hooks/useRuntimeTeamFormation";
|
||||
import { useThemeWorkbenchEntryPrompt } from "./hooks/useThemeWorkbenchEntryPrompt";
|
||||
import { useThemeWorkbenchEntryPromptActions } from "./hooks/useThemeWorkbenchEntryPromptActions";
|
||||
import { useThemeWorkbenchSendBoundary } from "./hooks/useThemeWorkbenchSendBoundary";
|
||||
import type {
|
||||
BrowserTaskPreflight,
|
||||
} from "./hooks/handleSendTypes";
|
||||
import type { BrowserTaskPreflight } from "./hooks/handleSendTypes";
|
||||
import { mergeThreadItems } from "./utils/threadTimelineView";
|
||||
import {
|
||||
DEFAULT_STYLE_PROFILE,
|
||||
@@ -191,9 +173,7 @@ import {
|
||||
normalizeInitialTheme,
|
||||
projectTypeToTheme,
|
||||
} from "./agentChatWorkspaceShared";
|
||||
import type {
|
||||
AgentChatWorkspaceProps,
|
||||
} from "./agentChatWorkspaceContract";
|
||||
import type { AgentChatWorkspaceProps } from "./agentChatWorkspaceContract";
|
||||
|
||||
const GENERAL_BROWSER_ASSIST_PROFILE_KEY = "general_browser_assist";
|
||||
|
||||
@@ -257,8 +237,7 @@ export function AgentChatWorkspace({
|
||||
chatToolPreferences,
|
||||
setChatToolPreferences,
|
||||
syncChatToolPreferencesSource,
|
||||
} =
|
||||
useThemeScopedChatToolPreferences(activeTheme);
|
||||
} = useThemeScopedChatToolPreferences(activeTheme);
|
||||
const {
|
||||
projectId,
|
||||
shouldDisableSessionRestore,
|
||||
@@ -1122,11 +1101,7 @@ export function AgentChatWorkspace({
|
||||
|
||||
useEffect(() => {
|
||||
syncChatToolPreferencesSource(activeTheme, runtimeChatToolPreferences);
|
||||
}, [
|
||||
activeTheme,
|
||||
runtimeChatToolPreferences,
|
||||
syncChatToolPreferencesSource,
|
||||
]);
|
||||
}, [activeTheme, runtimeChatToolPreferences, syncChatToolPreferencesSource]);
|
||||
|
||||
const hasRealTeamGraph =
|
||||
childSubagentSessions.length > 0 || Boolean(subagentParentContext);
|
||||
@@ -1442,18 +1417,19 @@ export function AgentChatWorkspace({
|
||||
harnessAttentionLevel,
|
||||
navbarHarnessPanelVisible,
|
||||
} = contextHarnessRuntime;
|
||||
const themeWorkbenchScaffoldRuntime = useWorkspaceThemeWorkbenchScaffoldRuntime({
|
||||
isThemeWorkbench,
|
||||
mappedTheme,
|
||||
sessionId,
|
||||
projectId,
|
||||
canvasState,
|
||||
documentVersionStatusMap,
|
||||
setDocumentVersionStatusMap,
|
||||
clearThemeSkillsRailState,
|
||||
setCanvasState,
|
||||
setLayoutMode,
|
||||
});
|
||||
const themeWorkbenchScaffoldRuntime =
|
||||
useWorkspaceThemeWorkbenchScaffoldRuntime({
|
||||
isThemeWorkbench,
|
||||
mappedTheme,
|
||||
sessionId,
|
||||
projectId,
|
||||
canvasState,
|
||||
documentVersionStatusMap,
|
||||
setDocumentVersionStatusMap,
|
||||
clearThemeSkillsRailState,
|
||||
setCanvasState,
|
||||
setLayoutMode,
|
||||
});
|
||||
const {
|
||||
shouldUseCompactThemeWorkbench,
|
||||
shouldSkipThemeWorkbenchAutoGuideWithoutPrompt,
|
||||
@@ -1492,15 +1468,17 @@ export function AgentChatWorkspace({
|
||||
pendingActionRequest,
|
||||
});
|
||||
|
||||
const themeWorkbenchSidebarRuntime = useWorkspaceThemeWorkbenchSidebarRuntime({
|
||||
isThemeWorkbench,
|
||||
sessionId,
|
||||
messages,
|
||||
isSending,
|
||||
themeWorkbenchBackendRunState,
|
||||
contextActivityLogs: contextWorkspace.activityLogs,
|
||||
historyPageSize: THEME_WORKBENCH_HISTORY_PAGE_SIZE,
|
||||
});
|
||||
const themeWorkbenchSidebarRuntime = useWorkspaceThemeWorkbenchSidebarRuntime(
|
||||
{
|
||||
isThemeWorkbench,
|
||||
sessionId,
|
||||
messages,
|
||||
isSending,
|
||||
themeWorkbenchBackendRunState,
|
||||
contextActivityLogs: contextWorkspace.activityLogs,
|
||||
historyPageSize: THEME_WORKBENCH_HISTORY_PAGE_SIZE,
|
||||
},
|
||||
);
|
||||
|
||||
const { handleViewContextDetail } = useWorkspaceContextDetailActions({
|
||||
contextWorkspace,
|
||||
@@ -1709,45 +1687,42 @@ export function AgentChatWorkspace({
|
||||
setCreationMode,
|
||||
setTaskFiles,
|
||||
});
|
||||
const {
|
||||
handleClearMessages,
|
||||
handleBackHome,
|
||||
resetTopicLocalState,
|
||||
} = useWorkspaceResetRuntime({
|
||||
clearMessages,
|
||||
clearRuntimeTeamState,
|
||||
clearProjectSelectionRuntime,
|
||||
resetRestoredSessionState,
|
||||
resetProjectSelection,
|
||||
resetGuideState,
|
||||
hasHandledNewChatRequest,
|
||||
markNewChatRequestHandled,
|
||||
createFreshSession,
|
||||
defaultTopicSidebarVisible,
|
||||
normalizedInitialTheme: normalizedEntryTheme,
|
||||
initialCreationMode,
|
||||
newChatAt,
|
||||
initialSessionName,
|
||||
projectId,
|
||||
externalProjectId,
|
||||
onNavigate: _onNavigate,
|
||||
autoCollapsedTopicSidebarRef,
|
||||
processedMessageIdsRef: processedMessageIds,
|
||||
setInput,
|
||||
setSelectedText,
|
||||
setLayoutMode,
|
||||
setShowSidebar,
|
||||
setCanvasState,
|
||||
setGeneralCanvasState,
|
||||
setTaskFiles,
|
||||
setSelectedFileId,
|
||||
setBrowserTaskPreflight,
|
||||
setMentionedCharacters,
|
||||
setProject,
|
||||
setProjectMemory,
|
||||
setActiveTheme,
|
||||
setCreationMode,
|
||||
});
|
||||
const { handleClearMessages, handleBackHome, resetTopicLocalState } =
|
||||
useWorkspaceResetRuntime({
|
||||
clearMessages,
|
||||
clearRuntimeTeamState,
|
||||
clearProjectSelectionRuntime,
|
||||
resetRestoredSessionState,
|
||||
resetProjectSelection,
|
||||
resetGuideState,
|
||||
hasHandledNewChatRequest,
|
||||
markNewChatRequestHandled,
|
||||
createFreshSession,
|
||||
defaultTopicSidebarVisible,
|
||||
normalizedInitialTheme: normalizedEntryTheme,
|
||||
initialCreationMode,
|
||||
newChatAt,
|
||||
initialSessionName,
|
||||
projectId,
|
||||
externalProjectId,
|
||||
onNavigate: _onNavigate,
|
||||
autoCollapsedTopicSidebarRef,
|
||||
processedMessageIdsRef: processedMessageIds,
|
||||
setInput,
|
||||
setSelectedText,
|
||||
setLayoutMode,
|
||||
setShowSidebar,
|
||||
setCanvasState,
|
||||
setGeneralCanvasState,
|
||||
setTaskFiles,
|
||||
setSelectedFileId,
|
||||
setBrowserTaskPreflight,
|
||||
setMentionedCharacters,
|
||||
setProject,
|
||||
setProjectMemory,
|
||||
setActiveTheme,
|
||||
setCreationMode,
|
||||
});
|
||||
|
||||
const { switchTopic } = useWorkspaceTopicSwitch({
|
||||
projectId,
|
||||
@@ -2184,7 +2159,9 @@ export function AgentChatWorkspace({
|
||||
return;
|
||||
}
|
||||
|
||||
setLayoutMode((current) => (current === "canvas" ? "chat-canvas" : current));
|
||||
setLayoutMode((current) =>
|
||||
current === "canvas" ? "chat-canvas" : current,
|
||||
);
|
||||
setFocusedTimelineItemId(normalizedItemId);
|
||||
setTimelineFocusRequestKey((current) => current + 1);
|
||||
}, []);
|
||||
@@ -2433,136 +2410,135 @@ export function AgentChatWorkspace({
|
||||
teamDispatchPreviewState,
|
||||
});
|
||||
|
||||
const workspaceShellSceneRuntime =
|
||||
useWorkspaceConversationShellSceneRuntime({
|
||||
navigationActions,
|
||||
inputbarScene,
|
||||
canvasScene,
|
||||
shellChromeRuntime,
|
||||
themeWorkbenchShellRuntime,
|
||||
contextHarnessRuntime,
|
||||
teamSessionRuntime,
|
||||
currentImageWorkbenchState,
|
||||
project,
|
||||
projectId,
|
||||
projectMemory,
|
||||
handleSend,
|
||||
mappedTheme,
|
||||
runtimeStyleSelection,
|
||||
setRuntimeStyleSelection,
|
||||
generalCanvasState,
|
||||
runtimeStylePrompt,
|
||||
showSidebar,
|
||||
topics,
|
||||
switchTopic,
|
||||
handleResumeSidebarTask,
|
||||
deleteTopic,
|
||||
renameTopic,
|
||||
childSubagentSessions,
|
||||
subagentParentContext,
|
||||
handleReturnToParentSession,
|
||||
entryBannerVisible,
|
||||
entryBannerMessage,
|
||||
contextWorkspaceEnabled: contextWorkspace.enabled,
|
||||
input,
|
||||
setInput,
|
||||
providerType,
|
||||
setProviderType,
|
||||
model,
|
||||
setModel,
|
||||
executionStrategy,
|
||||
setExecutionStrategy,
|
||||
chatToolPreferences,
|
||||
setChatToolPreferences,
|
||||
selectedTeam,
|
||||
handleSelectTeam,
|
||||
handleEnableSuggestedTeam,
|
||||
creationMode,
|
||||
setCreationMode,
|
||||
activeTheme,
|
||||
setActiveTheme,
|
||||
lockTheme,
|
||||
artifacts,
|
||||
resolvedCanvasState,
|
||||
contentId,
|
||||
selectedText,
|
||||
handleRecommendationClick,
|
||||
skills,
|
||||
skillsLoading,
|
||||
handleNavigateToSkillSettings,
|
||||
handleRefreshSkills,
|
||||
handleOpenBrowserAssistInCanvas,
|
||||
browserAssistLaunching,
|
||||
hideHistoryToggle,
|
||||
showChatPanel,
|
||||
topBarChrome,
|
||||
onBackToProjectManagement,
|
||||
fromResources,
|
||||
handleBackHome,
|
||||
handleToggleSidebar,
|
||||
chatMode,
|
||||
isBrowserAssistCanvasVisible,
|
||||
browserAssistAttentionLevel,
|
||||
browserAssistEntryLabel,
|
||||
showHarnessToggle,
|
||||
navbarHarnessPanelVisible,
|
||||
harnessPendingCount,
|
||||
harnessAttentionLevel,
|
||||
sessionId,
|
||||
syncStatus,
|
||||
pendingA2UIForm,
|
||||
handleToggleCanvas,
|
||||
hideInlineStepProgress,
|
||||
isContentCreationMode,
|
||||
hasMessages,
|
||||
steps,
|
||||
currentStepIndex,
|
||||
goToStep,
|
||||
displayMessages,
|
||||
turns,
|
||||
effectiveThreadItems,
|
||||
currentTurnId,
|
||||
threadRead,
|
||||
pendingActions,
|
||||
submittedActionsInFlight,
|
||||
queuedTurns,
|
||||
isSending,
|
||||
stopSending,
|
||||
resumeThread,
|
||||
replayPendingAction,
|
||||
promoteQueuedTurn,
|
||||
deleteMessage,
|
||||
editMessage,
|
||||
handleA2UISubmit,
|
||||
handleWriteFile,
|
||||
handleFileClick: handleWorkspaceFileClick,
|
||||
handleOpenArtifactFromTimeline,
|
||||
handleOpenSavedSiteContent,
|
||||
handleArtifactClick: handleWorkspaceArtifactClick,
|
||||
handleOpenSubagentSession,
|
||||
handlePermissionResponseWithBrowserPreflight,
|
||||
pendingPromotedA2UIActionRequest,
|
||||
shouldCollapseCodeBlocks,
|
||||
shouldCollapseCodeBlockInChat,
|
||||
handleCodeBlockClick,
|
||||
layoutMode,
|
||||
handleActivateTeamWorkbench,
|
||||
isThemeWorkbench,
|
||||
showNovelNavbarControls,
|
||||
novelChapterListCollapsed,
|
||||
handleToggleNovelChapterList,
|
||||
handleAddNovelChapter,
|
||||
handleCloseCanvas,
|
||||
settledWorkbenchArtifacts,
|
||||
taskFiles,
|
||||
selectedFileId,
|
||||
handleHarnessLoadFilePreview,
|
||||
setCanvasWorkbenchLayoutMode,
|
||||
workspacePathMissing: Boolean(workspacePathMissing),
|
||||
workspaceHealthError,
|
||||
focusedTimelineItemId,
|
||||
timelineFocusRequestKey,
|
||||
});
|
||||
const workspaceShellSceneRuntime = useWorkspaceConversationShellSceneRuntime({
|
||||
navigationActions,
|
||||
inputbarScene,
|
||||
canvasScene,
|
||||
shellChromeRuntime,
|
||||
themeWorkbenchShellRuntime,
|
||||
contextHarnessRuntime,
|
||||
teamSessionRuntime,
|
||||
currentImageWorkbenchState,
|
||||
project,
|
||||
projectId,
|
||||
projectMemory,
|
||||
handleSend,
|
||||
mappedTheme,
|
||||
runtimeStyleSelection,
|
||||
setRuntimeStyleSelection,
|
||||
generalCanvasState,
|
||||
runtimeStylePrompt,
|
||||
showSidebar,
|
||||
topics,
|
||||
switchTopic,
|
||||
handleResumeSidebarTask,
|
||||
deleteTopic,
|
||||
renameTopic,
|
||||
childSubagentSessions,
|
||||
subagentParentContext,
|
||||
handleReturnToParentSession,
|
||||
entryBannerVisible,
|
||||
entryBannerMessage,
|
||||
contextWorkspaceEnabled: contextWorkspace.enabled,
|
||||
input,
|
||||
setInput,
|
||||
providerType,
|
||||
setProviderType,
|
||||
model,
|
||||
setModel,
|
||||
executionStrategy,
|
||||
setExecutionStrategy,
|
||||
chatToolPreferences,
|
||||
setChatToolPreferences,
|
||||
selectedTeam,
|
||||
handleSelectTeam,
|
||||
handleEnableSuggestedTeam,
|
||||
creationMode,
|
||||
setCreationMode,
|
||||
activeTheme,
|
||||
setActiveTheme,
|
||||
lockTheme,
|
||||
artifacts,
|
||||
resolvedCanvasState,
|
||||
contentId,
|
||||
selectedText,
|
||||
handleRecommendationClick,
|
||||
skills,
|
||||
skillsLoading,
|
||||
handleNavigateToSkillSettings,
|
||||
handleRefreshSkills,
|
||||
handleOpenBrowserAssistInCanvas,
|
||||
browserAssistLaunching,
|
||||
hideHistoryToggle,
|
||||
showChatPanel,
|
||||
topBarChrome,
|
||||
onBackToProjectManagement,
|
||||
fromResources,
|
||||
handleBackHome,
|
||||
handleToggleSidebar,
|
||||
chatMode,
|
||||
isBrowserAssistCanvasVisible,
|
||||
browserAssistAttentionLevel,
|
||||
browserAssistEntryLabel,
|
||||
showHarnessToggle,
|
||||
navbarHarnessPanelVisible,
|
||||
harnessPendingCount,
|
||||
harnessAttentionLevel,
|
||||
sessionId,
|
||||
syncStatus,
|
||||
pendingA2UIForm,
|
||||
handleToggleCanvas,
|
||||
hideInlineStepProgress,
|
||||
isContentCreationMode,
|
||||
hasMessages,
|
||||
steps,
|
||||
currentStepIndex,
|
||||
goToStep,
|
||||
displayMessages,
|
||||
turns,
|
||||
effectiveThreadItems,
|
||||
currentTurnId,
|
||||
threadRead,
|
||||
pendingActions,
|
||||
submittedActionsInFlight,
|
||||
queuedTurns,
|
||||
isSending,
|
||||
stopSending,
|
||||
resumeThread,
|
||||
replayPendingAction,
|
||||
promoteQueuedTurn,
|
||||
deleteMessage,
|
||||
editMessage,
|
||||
handleA2UISubmit,
|
||||
handleWriteFile,
|
||||
handleFileClick: handleWorkspaceFileClick,
|
||||
handleOpenArtifactFromTimeline,
|
||||
handleOpenSavedSiteContent,
|
||||
handleArtifactClick: handleWorkspaceArtifactClick,
|
||||
handleOpenSubagentSession,
|
||||
handlePermissionResponseWithBrowserPreflight,
|
||||
pendingPromotedA2UIActionRequest,
|
||||
shouldCollapseCodeBlocks,
|
||||
shouldCollapseCodeBlockInChat,
|
||||
handleCodeBlockClick,
|
||||
layoutMode,
|
||||
handleActivateTeamWorkbench,
|
||||
isThemeWorkbench,
|
||||
showNovelNavbarControls,
|
||||
novelChapterListCollapsed,
|
||||
handleToggleNovelChapterList,
|
||||
handleAddNovelChapter,
|
||||
handleCloseCanvas,
|
||||
settledWorkbenchArtifacts,
|
||||
taskFiles,
|
||||
selectedFileId,
|
||||
handleHarnessLoadFilePreview,
|
||||
setCanvasWorkbenchLayoutMode,
|
||||
workspacePathMissing: Boolean(workspacePathMissing),
|
||||
workspaceHealthError,
|
||||
focusedTimelineItemId,
|
||||
timelineFocusRequestKey,
|
||||
});
|
||||
|
||||
return workspaceShellSceneRuntime.shellSceneNode;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function TeamWorkbenchSummaryPanel({
|
||||
selectedTeamRoles = [],
|
||||
teamDispatchPreviewState = null,
|
||||
}: TeamWorkbenchSummaryPanelProps) {
|
||||
const runtimeTeamState = teamDispatchPreviewState;
|
||||
const dispatchPreviewState = teamDispatchPreviewState;
|
||||
const executionSummary = summarizeTeamWorkspaceExecution({
|
||||
currentSessionId,
|
||||
currentSessionRuntimeStatus,
|
||||
@@ -89,17 +89,17 @@ export function TeamWorkbenchSummaryPanel({
|
||||
});
|
||||
const hasRealTeamGraph =
|
||||
childSubagentSessions.length > 0 || Boolean(subagentParentContext);
|
||||
const runtimeFormationMeta = runtimeTeamState
|
||||
? resolveRuntimeFormationStatusMeta(runtimeTeamState.status)
|
||||
const runtimeFormationMeta = dispatchPreviewState
|
||||
? resolveRuntimeFormationStatusMeta(dispatchPreviewState.status)
|
||||
: null;
|
||||
const runtimeTeamLabel =
|
||||
runtimeTeamState?.label?.trim() ||
|
||||
runtimeTeamState?.blueprint?.label?.trim() ||
|
||||
dispatchPreviewState?.label?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.label?.trim() ||
|
||||
selectedTeamLabel?.trim() ||
|
||||
null;
|
||||
const runtimeSummaryText =
|
||||
runtimeTeamState?.summary?.trim() ||
|
||||
runtimeTeamState?.blueprint?.summary?.trim() ||
|
||||
dispatchPreviewState?.summary?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.summary?.trim() ||
|
||||
selectedTeamSummary ||
|
||||
null;
|
||||
const operationSummary = buildOperationSummary({
|
||||
@@ -111,10 +111,10 @@ export function TeamWorkbenchSummaryPanel({
|
||||
.slice(0, 4);
|
||||
const displayRoleCount = hasRealTeamGraph
|
||||
? executionSummary.totalSessionCount
|
||||
: runtimeTeamState?.members.length ?? 0;
|
||||
: dispatchPreviewState?.members.length ?? 0;
|
||||
const roleCards =
|
||||
runtimeTeamState?.members.length
|
||||
? runtimeTeamState.members.map((member) => ({
|
||||
dispatchPreviewState?.members.length
|
||||
? dispatchPreviewState.members.map((member) => ({
|
||||
id: member.id,
|
||||
label: member.label,
|
||||
roleKey: member.roleKey,
|
||||
@@ -151,7 +151,7 @@ export function TeamWorkbenchSummaryPanel({
|
||||
{
|
||||
label: hasRealTeamGraph
|
||||
? "总会话"
|
||||
: runtimeTeamState
|
||||
: dispatchPreviewState
|
||||
? "当前成员"
|
||||
: "总会话",
|
||||
value: String(
|
||||
@@ -159,7 +159,7 @@ export function TeamWorkbenchSummaryPanel({
|
||||
),
|
||||
hint: hasRealTeamGraph
|
||||
? "已进入团队图谱"
|
||||
: runtimeTeamState
|
||||
: dispatchPreviewState
|
||||
? "当前编队"
|
||||
: "等待创建",
|
||||
},
|
||||
@@ -193,13 +193,13 @@ export function TeamWorkbenchSummaryPanel({
|
||||
"团队已就绪,等待主代理开始编排"}
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-slate-500">
|
||||
{runtimeTeamState?.status === "failed"
|
||||
? runtimeTeamState.errorMessage?.trim() ||
|
||||
{dispatchPreviewState?.status === "failed"
|
||||
? dispatchPreviewState.errorMessage?.trim() ||
|
||||
"这次 Team 准备失败,可继续在当前对话中推进。"
|
||||
: runtimeSummaryText ||
|
||||
"这里展示团队总览与运行密度;主对话只保留调度记录,角色执行正文在左侧 Team 画布查看。"}
|
||||
</p>
|
||||
{!hasRealTeamGraph && runtimeTeamState ? (
|
||||
{!hasRealTeamGraph && dispatchPreviewState ? (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3 rounded-2xl border px-3 py-2 text-xs leading-5",
|
||||
@@ -207,15 +207,15 @@ export function TeamWorkbenchSummaryPanel({
|
||||
"border border-slate-200 bg-slate-50 text-slate-700",
|
||||
)}
|
||||
>
|
||||
{runtimeTeamState.status === "forming"
|
||||
{dispatchPreviewState.status === "forming"
|
||||
? "模型正在依据当前任务准备 Team,真实成员加入后会自动切换到实时协作轨道。"
|
||||
: runtimeTeamState.status === "formed"
|
||||
? `已准备 ${runtimeTeamState.members.length} 个成员,当前等待系统分派真实成员进入协作。`
|
||||
: runtimeTeamState.errorMessage?.trim() ||
|
||||
: dispatchPreviewState.status === "formed"
|
||||
? `已准备 ${dispatchPreviewState.members.length} 个成员,当前等待系统分派真实成员进入协作。`
|
||||
: dispatchPreviewState.errorMessage?.trim() ||
|
||||
"暂未成功准备这次 Team。"}
|
||||
</div>
|
||||
) : null}
|
||||
{!hasRealTeamGraph && !runtimeTeamState ? (
|
||||
{!hasRealTeamGraph && !dispatchPreviewState ? (
|
||||
<div className="mt-3 rounded-2xl border border-sky-100 bg-sky-50 px-3 py-2 text-xs leading-5 text-sky-700">
|
||||
尚未出现真实团队成员。开始分派成员后,左侧 Team 画布会自动切换为实时协作视图。
|
||||
</div>
|
||||
@@ -243,7 +243,7 @@ export function TeamWorkbenchSummaryPanel({
|
||||
<section className="rounded-[24px] border border-slate-200/80 bg-white p-4 shadow-sm shadow-slate-950/5">
|
||||
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-500">
|
||||
<Bot className="h-3.5 w-3.5" />
|
||||
<span>{runtimeTeamState ? "当前成员" : "角色分工"}</span>
|
||||
<span>{dispatchPreviewState ? "当前成员" : "角色分工"}</span>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{roleCards.map((role) => (
|
||||
@@ -294,9 +294,9 @@ export function TeamWorkbenchSummaryPanel({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{runtimeTeamState?.blueprint?.label ? (
|
||||
{dispatchPreviewState?.blueprint?.label ? (
|
||||
<div className="mt-3 rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs leading-5 text-slate-500">
|
||||
参考蓝图 Team:{runtimeTeamState.blueprint.label}
|
||||
参考蓝图 Team:{dispatchPreviewState.blueprint.label}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
@@ -1394,12 +1394,12 @@ export function TeamWorkspaceBoard({
|
||||
selectedTeamRoles = [],
|
||||
teamDispatchPreviewState = null,
|
||||
}: TeamWorkspaceBoardProps) {
|
||||
const runtimeTeamState = teamDispatchPreviewState;
|
||||
const dispatchPreviewState = teamDispatchPreviewState;
|
||||
const isChildSession = Boolean(subagentParentContext);
|
||||
const canvasStorageScopeId =
|
||||
currentSessionId?.trim() ||
|
||||
subagentParentContext?.parent_session_id?.trim() ||
|
||||
runtimeTeamState?.requestId?.trim() ||
|
||||
dispatchPreviewState?.requestId?.trim() ||
|
||||
"team-workspace";
|
||||
const [shellExpanded, setShellExpanded] = useState(defaultShellExpanded);
|
||||
const detailExpanded = !embedded;
|
||||
@@ -1623,26 +1623,26 @@ export function TeamWorkspaceBoard({
|
||||
const normalizedSelectedTeamRoles = (selectedTeamRoles ?? []).filter((role) =>
|
||||
role.label.trim(),
|
||||
);
|
||||
const runtimeFormationMeta = runtimeTeamState
|
||||
? resolveRuntimeFormationStatusMeta(runtimeTeamState.status)
|
||||
const runtimeFormationMeta = dispatchPreviewState
|
||||
? resolveRuntimeFormationStatusMeta(dispatchPreviewState.status)
|
||||
: null;
|
||||
const runtimeFormationLabel =
|
||||
runtimeTeamState?.label?.trim() ||
|
||||
runtimeTeamState?.blueprint?.label?.trim() ||
|
||||
dispatchPreviewState?.label?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.label?.trim() ||
|
||||
normalizedSelectedTeamLabel;
|
||||
const runtimeFormationSummary =
|
||||
runtimeTeamState?.summary?.trim() ||
|
||||
runtimeTeamState?.blueprint?.summary?.trim() ||
|
||||
dispatchPreviewState?.summary?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.summary?.trim() ||
|
||||
normalizedSelectedTeamSummary;
|
||||
const runtimeMembers = useMemo(
|
||||
() => runtimeTeamState?.members ?? [],
|
||||
[runtimeTeamState?.members],
|
||||
() => dispatchPreviewState?.members ?? [],
|
||||
[dispatchPreviewState?.members],
|
||||
);
|
||||
const runtimeBlueprintRoles = useMemo(
|
||||
() => runtimeTeamState?.blueprint?.roles ?? [],
|
||||
[runtimeTeamState?.blueprint?.roles],
|
||||
() => dispatchPreviewState?.blueprint?.roles ?? [],
|
||||
[dispatchPreviewState?.blueprint?.roles],
|
||||
);
|
||||
const hasRuntimeFormation = Boolean(runtimeTeamState);
|
||||
const hasRuntimeFormation = Boolean(dispatchPreviewState);
|
||||
const hasSelectedTeamPlan =
|
||||
Boolean(normalizedSelectedTeamLabel) ||
|
||||
Boolean(normalizedSelectedTeamSummary) ||
|
||||
@@ -1740,9 +1740,9 @@ export function TeamWorkspaceBoard({
|
||||
{runtimeMembers.length} 位当前成员
|
||||
</span>
|
||||
) : null}
|
||||
{runtimeTeamState?.blueprint?.label ? (
|
||||
{dispatchPreviewState?.blueprint?.label ? (
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2.5 py-1">
|
||||
参考方案 · {runtimeTeamState.blueprint.label}
|
||||
参考方案 · {dispatchPreviewState.blueprint.label}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1750,7 +1750,7 @@ export function TeamWorkspaceBoard({
|
||||
};
|
||||
|
||||
const renderRuntimeFormationPanel = () => {
|
||||
if (!runtimeTeamState || !runtimeFormationMeta) {
|
||||
if (!dispatchPreviewState || !runtimeFormationMeta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1780,15 +1780,15 @@ export function TeamWorkspaceBoard({
|
||||
{runtimeFormationMeta.title}
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||
{runtimeTeamState.status === "failed"
|
||||
? runtimeTeamState.errorMessage?.trim() ||
|
||||
{dispatchPreviewState.status === "failed"
|
||||
? dispatchPreviewState.errorMessage?.trim() ||
|
||||
"当前协作准备失败,暂时无法展示更多内容。"
|
||||
: runtimeFormationSummary ||
|
||||
"这里会先展示当前协作方案,成员加入后再切换成实时进展。"}
|
||||
</p>
|
||||
{runtimeTeamState.blueprint?.label ? (
|
||||
{dispatchPreviewState.blueprint?.label ? (
|
||||
<div className="mt-3 rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs leading-5 text-slate-500">
|
||||
参考方案:{runtimeTeamState.blueprint.label}
|
||||
参考方案:{dispatchPreviewState.blueprint.label}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -3212,11 +3212,11 @@ export function TeamWorkspaceBoard({
|
||||
? isChildSession
|
||||
? "当前协作成员会在各自面板里持续更新进展和结果,主对话只保留必要摘要。"
|
||||
: `${visibleSessions.length} 位协作成员已加入,每位成员都会在自己的面板里持续更新进展和结果。`
|
||||
: runtimeTeamState?.status === "forming"
|
||||
: dispatchPreviewState?.status === "forming"
|
||||
? "正在准备当前协作分工,成员接入后会在这里独立更新进展。"
|
||||
: runtimeTeamState?.status === "formed"
|
||||
: dispatchPreviewState?.status === "formed"
|
||||
? "当前协作分工已经就绪,成员接入后会在各自面板里开始处理。"
|
||||
: runtimeTeamState?.status === "failed"
|
||||
: dispatchPreviewState?.status === "failed"
|
||||
? "这次协作准备失败,暂时无法生成成员面板。"
|
||||
: "成员加入后,这里会展开为独立的协作进展面板。";
|
||||
|
||||
@@ -3258,7 +3258,7 @@ export function TeamWorkspaceBoard({
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-500">
|
||||
{hasRuntimeFormation
|
||||
? buildRuntimeFormationHint(runtimeTeamState)
|
||||
? buildRuntimeFormationHint(dispatchPreviewState)
|
||||
: "这里先保持简洁,避免遮挡消息区;只有真正需要协作分工时才会展开完整面板。"}
|
||||
</p>
|
||||
{hasRuntimeFormation
|
||||
@@ -3295,7 +3295,7 @@ export function TeamWorkspaceBoard({
|
||||
});
|
||||
const boardHint =
|
||||
!hasRealTeamGraph && hasRuntimeFormation
|
||||
? buildRuntimeFormationHint(runtimeTeamState)
|
||||
? buildRuntimeFormationHint(dispatchPreviewState)
|
||||
: buildBoardHint({
|
||||
hasRealTeamGraph,
|
||||
isChildSession,
|
||||
@@ -4558,14 +4558,14 @@ export function TeamWorkspaceBoard({
|
||||
renderSelectedTeamPlanPanel()
|
||||
)}
|
||||
<div className="mt-4 rounded-[20px] border border-dashed border-slate-200 bg-slate-50 p-5 text-sm leading-6 text-slate-500">
|
||||
{runtimeTeamState?.status === "forming" ? (
|
||||
{dispatchPreviewState?.status === "forming" ? (
|
||||
"系统正在准备当前协作分工,完成后会先展示成员卡片,后续再切换为独立的实时进展面板。"
|
||||
) : runtimeTeamState?.status === "formed" ? (
|
||||
) : dispatchPreviewState?.status === "formed" ? (
|
||||
<>
|
||||
当前协作方案已就绪。系统开始分工后,这里会从方案视图过渡到实时协作画布。
|
||||
</>
|
||||
) : runtimeTeamState?.status === "failed" ? (
|
||||
runtimeTeamState.errorMessage?.trim() ||
|
||||
) : dispatchPreviewState?.status === "failed" ? (
|
||||
dispatchPreviewState.errorMessage?.trim() ||
|
||||
"当前协作准备失败,暂时还没有协作成员加入。"
|
||||
) : (
|
||||
<>
|
||||
@@ -4589,7 +4589,7 @@ export function TeamWorkspaceBoard({
|
||||
{runtimeFormationMeta?.title || "等待协作成员加入"}
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-600">
|
||||
{buildRuntimeFormationEmptyDetail(runtimeTeamState)}
|
||||
{buildRuntimeFormationEmptyDetail(dispatchPreviewState)}
|
||||
</p>
|
||||
{hasRuntimeFormation ? (
|
||||
<div className="mt-4 space-y-4">
|
||||
|
||||
@@ -510,22 +510,22 @@ export function TeamWorkspaceDock({
|
||||
selectedTeamRoles = [],
|
||||
teamDispatchPreviewState = null,
|
||||
}: TeamWorkspaceDockProps) {
|
||||
const runtimeTeamState = teamDispatchPreviewState;
|
||||
const dispatchPreviewState = teamDispatchPreviewState;
|
||||
const launcherOnly = typeof onActivateWorkbench === "function";
|
||||
const hasRealTeamGraph =
|
||||
childSubagentSessions.length > 0 || Boolean(subagentParentContext);
|
||||
const hasRuntimeFormation = Boolean(runtimeTeamState);
|
||||
const runtimeFormationMeta = runtimeTeamState
|
||||
? resolveRuntimeFormationStatusMeta(runtimeTeamState.status)
|
||||
const hasRuntimeFormation = Boolean(dispatchPreviewState);
|
||||
const runtimeFormationMeta = dispatchPreviewState
|
||||
? resolveRuntimeFormationStatusMeta(dispatchPreviewState.status)
|
||||
: null;
|
||||
const runtimeTeamLabel =
|
||||
runtimeTeamState?.label?.trim() ||
|
||||
runtimeTeamState?.blueprint?.label?.trim() ||
|
||||
dispatchPreviewState?.label?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.label?.trim() ||
|
||||
selectedTeamLabel?.trim() ||
|
||||
null;
|
||||
const runtimeTeamSummary =
|
||||
runtimeTeamState?.summary?.trim() ||
|
||||
runtimeTeamState?.blueprint?.summary?.trim() ||
|
||||
dispatchPreviewState?.summary?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.summary?.trim() ||
|
||||
selectedTeamSummary?.trim() ||
|
||||
null;
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
@@ -540,9 +540,9 @@ export function TeamWorkspaceDock({
|
||||
const showAttentionCue =
|
||||
!expanded &&
|
||||
(hasRealTeamGraph ||
|
||||
runtimeTeamState?.status === "forming" ||
|
||||
runtimeTeamState?.status === "formed" ||
|
||||
runtimeTeamState?.status === "failed");
|
||||
dispatchPreviewState?.status === "forming" ||
|
||||
dispatchPreviewState?.status === "formed" ||
|
||||
dispatchPreviewState?.status === "failed");
|
||||
const dockCount = subagentParentContext
|
||||
? (subagentParentContext.sibling_subagent_sessions?.length ?? 0) + 1
|
||||
: childSubagentSessions.length;
|
||||
@@ -550,9 +550,9 @@ export function TeamWorkspaceDock({
|
||||
const [teamDetailExpanded, setTeamDetailExpanded] = useState(false);
|
||||
const hasSelectedTeamDetails =
|
||||
Boolean(runtimeTeamSummary) ||
|
||||
(runtimeTeamState?.members.length ?? 0) > 0 ||
|
||||
Boolean(runtimeTeamState?.blueprint?.summary?.trim()) ||
|
||||
(runtimeTeamState?.blueprint?.roles.length ?? 0) > 0 ||
|
||||
(dispatchPreviewState?.members.length ?? 0) > 0 ||
|
||||
Boolean(dispatchPreviewState?.blueprint?.summary?.trim()) ||
|
||||
(dispatchPreviewState?.blueprint?.roles.length ?? 0) > 0 ||
|
||||
Boolean(selectedTeamSummary?.trim()) ||
|
||||
(selectedTeamRoles?.length ?? 0) > 0;
|
||||
const activeRuntimeSessionCount = childSubagentSessions.filter(
|
||||
@@ -592,11 +592,11 @@ export function TeamWorkspaceDock({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (runtimeTeamState?.status === "forming") {
|
||||
if (dispatchPreviewState?.status === "forming") {
|
||||
return { label: "准备中", tone: "active" };
|
||||
}
|
||||
|
||||
if (runtimeTeamState?.status === "failed") {
|
||||
if (dispatchPreviewState?.status === "failed") {
|
||||
return { label: "失败", tone: "error" };
|
||||
}
|
||||
|
||||
@@ -616,9 +616,12 @@ export function TeamWorkspaceDock({
|
||||
};
|
||||
}
|
||||
|
||||
if (runtimeTeamState?.status === "formed") {
|
||||
return runtimeTeamState.members.length > 0
|
||||
? { label: `${runtimeTeamState.members.length} 名成员`, tone: "success" }
|
||||
if (dispatchPreviewState?.status === "formed") {
|
||||
return dispatchPreviewState.members.length > 0
|
||||
? {
|
||||
label: `${dispatchPreviewState.members.length} 名成员`,
|
||||
tone: "success",
|
||||
}
|
||||
: { label: "已就绪", tone: "success" };
|
||||
}
|
||||
|
||||
@@ -634,7 +637,7 @@ export function TeamWorkspaceDock({
|
||||
launcherOnly,
|
||||
teamConcurrencyBadgeText,
|
||||
runtimeTeamLabel,
|
||||
runtimeTeamState,
|
||||
dispatchPreviewState,
|
||||
]);
|
||||
const toggleLabel = useMemo(() => {
|
||||
if (expanded) {
|
||||
@@ -652,13 +655,13 @@ export function TeamWorkspaceDock({
|
||||
}
|
||||
return `查看任务进展 · ${dockCount}`;
|
||||
}
|
||||
if (runtimeTeamState?.status === "forming") {
|
||||
if (dispatchPreviewState?.status === "forming") {
|
||||
return "查看任务进展 · 准备中";
|
||||
}
|
||||
if (runtimeTeamState?.status === "formed") {
|
||||
return `查看任务进展 · ${runtimeTeamState.members.length}`;
|
||||
if (dispatchPreviewState?.status === "formed") {
|
||||
return `查看任务进展 · ${dispatchPreviewState.members.length}`;
|
||||
}
|
||||
if (runtimeTeamState?.status === "failed") {
|
||||
if (dispatchPreviewState?.status === "failed") {
|
||||
return "查看任务进展 · 失败";
|
||||
}
|
||||
return TEAM_WORKSPACE_SURFACE_TITLE;
|
||||
@@ -666,7 +669,7 @@ export function TeamWorkspaceDock({
|
||||
dockCount,
|
||||
expanded,
|
||||
hasRealTeamGraph,
|
||||
runtimeTeamState,
|
||||
dispatchPreviewState,
|
||||
teamConcurrencyBadgeText,
|
||||
teamQueueBadgeText,
|
||||
]);
|
||||
@@ -787,7 +790,7 @@ export function TeamWorkspaceDock({
|
||||
selectedTeamLabel={selectedTeamLabel}
|
||||
selectedTeamSummary={selectedTeamSummary}
|
||||
selectedTeamRoles={selectedTeamRoles}
|
||||
teamDispatchPreviewState={runtimeTeamState}
|
||||
teamDispatchPreviewState={dispatchPreviewState}
|
||||
/>
|
||||
) : (
|
||||
<EmptyStateCard data-testid="team-workspace-empty-card" role="status">
|
||||
@@ -808,12 +811,12 @@ export function TeamWorkspaceDock({
|
||||
{runtimeFormationMeta?.title || "等待协作成员加入"}
|
||||
</EmptyStateTitle>
|
||||
<EmptyStateDescription>
|
||||
{runtimeTeamState?.status === "forming"
|
||||
{dispatchPreviewState?.status === "forming"
|
||||
? "系统正在按当前任务准备协作分工,成员接入后会自动展开完整协作面板。"
|
||||
: runtimeTeamState?.status === "formed"
|
||||
? `已准备 ${runtimeTeamState.members.length} 位协作成员,后续会自动接入并继续处理。`
|
||||
: runtimeTeamState?.status === "failed"
|
||||
? runtimeTeamState.errorMessage?.trim() ||
|
||||
: dispatchPreviewState?.status === "formed"
|
||||
? `已准备 ${dispatchPreviewState.members.length} 位协作成员,后续会自动接入并继续处理。`
|
||||
: dispatchPreviewState?.status === "failed"
|
||||
? dispatchPreviewState.errorMessage?.trim() ||
|
||||
"这次协作准备失败,你仍然可以继续在当前对话中处理。"
|
||||
: (
|
||||
<>
|
||||
@@ -835,7 +838,7 @@ export function TeamWorkspaceDock({
|
||||
<div>
|
||||
<EmptyStateDetailTitle>{runtimeTeamLabel}</EmptyStateDetailTitle>
|
||||
<EmptyStateDetailHint>
|
||||
{runtimeTeamState
|
||||
{dispatchPreviewState
|
||||
? "查看当前协作分工与参考方案"
|
||||
: "查看当前协作方案与分工"}
|
||||
</EmptyStateDetailHint>
|
||||
@@ -851,12 +854,12 @@ export function TeamWorkspaceDock({
|
||||
{runtimeTeamSummary ? (
|
||||
<EmptyStateRoleItem>
|
||||
<EmptyStateRoleName>
|
||||
{runtimeTeamState ? "当前摘要" : "方案摘要"}
|
||||
{dispatchPreviewState ? "当前摘要" : "方案摘要"}
|
||||
</EmptyStateRoleName>
|
||||
<EmptyStateRoleSummary>{runtimeTeamSummary}</EmptyStateRoleSummary>
|
||||
</EmptyStateRoleItem>
|
||||
) : null}
|
||||
{runtimeTeamState?.members.map((member) => {
|
||||
{dispatchPreviewState?.members.map((member) => {
|
||||
const memberStatusMeta = resolveRuntimeMemberStatusMeta(
|
||||
member.status,
|
||||
);
|
||||
@@ -896,7 +899,7 @@ export function TeamWorkspaceDock({
|
||||
</EmptyStateRoleItem>
|
||||
);
|
||||
})}
|
||||
{!runtimeTeamState
|
||||
{!dispatchPreviewState
|
||||
? selectedTeamRoles?.map((role) => (
|
||||
<EmptyStateRoleItem key={`dock-role-${role.id}`}>
|
||||
<EmptyStateRoleName>{role.label}</EmptyStateRoleName>
|
||||
@@ -904,13 +907,13 @@ export function TeamWorkspaceDock({
|
||||
</EmptyStateRoleItem>
|
||||
))
|
||||
: null}
|
||||
{runtimeTeamState?.blueprint?.label ? (
|
||||
{dispatchPreviewState?.blueprint?.label ? (
|
||||
<EmptyStateRoleItem>
|
||||
<EmptyStateRoleName>
|
||||
参考方案 · {runtimeTeamState.blueprint.label}
|
||||
参考方案 · {dispatchPreviewState.blueprint.label}
|
||||
</EmptyStateRoleName>
|
||||
<EmptyStateRoleSummary>
|
||||
{runtimeTeamState.blueprint.summary ||
|
||||
{dispatchPreviewState.blueprint.summary ||
|
||||
"当前协作参考了当前方案里的分工偏好。"}
|
||||
</EmptyStateRoleSummary>
|
||||
</EmptyStateRoleItem>
|
||||
@@ -928,8 +931,8 @@ export function TeamWorkspaceDock({
|
||||
{runtimeTeamSummary ? (
|
||||
<EmptyStateBadge>{runtimeTeamSummary}</EmptyStateBadge>
|
||||
) : null}
|
||||
{runtimeTeamState?.members.length ? (
|
||||
<EmptyStateBadge>{runtimeTeamState.members.length} 个成员</EmptyStateBadge>
|
||||
{dispatchPreviewState?.members.length ? (
|
||||
<EmptyStateBadge>{dispatchPreviewState.members.length} 个成员</EmptyStateBadge>
|
||||
) : null}
|
||||
<EmptyStateBadge>不遮挡画布</EmptyStateBadge>
|
||||
</EmptyStateFooter>
|
||||
|
||||
@@ -53,6 +53,12 @@ describe("homeShellEntry", () => {
|
||||
defaultToolPreferences,
|
||||
payload: {
|
||||
prompt: "请起草一版首稿",
|
||||
contentId: "content-1",
|
||||
initialRequestMetadata: {
|
||||
artifact: {
|
||||
artifact_mode: "draft",
|
||||
},
|
||||
},
|
||||
themeOverride: "social-media",
|
||||
},
|
||||
now: () => 456,
|
||||
@@ -66,18 +72,30 @@ describe("homeShellEntry", () => {
|
||||
agentEntry: "claw",
|
||||
immersiveHome: false,
|
||||
projectId: "project-1",
|
||||
contentId: "content-1",
|
||||
theme: "social-media",
|
||||
initialCreationMode: "guided",
|
||||
initialUserPrompt: "请起草一版首稿",
|
||||
initialUserImages: undefined,
|
||||
initialRequestMetadata: {
|
||||
artifact: {
|
||||
artifact_mode: "draft",
|
||||
},
|
||||
},
|
||||
openBrowserAssistOnMount: undefined,
|
||||
newChatAt: 456,
|
||||
lockTheme: false,
|
||||
},
|
||||
workspaceBootstrap: {
|
||||
projectId: "project-1",
|
||||
contentId: "content-1",
|
||||
initialUserPrompt: "请起草一版首稿",
|
||||
initialUserImages: undefined,
|
||||
initialRequestMetadata: {
|
||||
artifact: {
|
||||
artifact_mode: "draft",
|
||||
},
|
||||
},
|
||||
theme: "social-media",
|
||||
initialCreationMode: "guided",
|
||||
openBrowserAssistOnMount: undefined,
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Suspense,
|
||||
lazy,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Suspense, lazy, useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
AgentChatHomeShell,
|
||||
type AgentChatWorkspaceBootstrap,
|
||||
@@ -37,7 +31,10 @@ function scheduleWorkspacePrefetch(task: () => void): () => void {
|
||||
};
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(task, WORKSPACE_PREFETCH_FALLBACK_DELAY_MS);
|
||||
const timeoutId = window.setTimeout(
|
||||
task,
|
||||
WORKSPACE_PREFETCH_FALLBACK_DELAY_MS,
|
||||
);
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildServiceSkillAutomationAgentTurnPayloadContext,
|
||||
buildServiceSkillAutomationInitialValues,
|
||||
supportsServiceSkillLocalAutomation,
|
||||
} from "./automationDraft";
|
||||
@@ -15,6 +16,8 @@ const SCHEDULED_SKILL: ServiceSkillItem = {
|
||||
runnerType: "scheduled",
|
||||
defaultExecutorBinding: "automation_job",
|
||||
executionLocation: "client_default",
|
||||
defaultArtifactKind: "analysis",
|
||||
themeTarget: "social-media",
|
||||
version: "seed-v1",
|
||||
slotSchema: [
|
||||
{
|
||||
@@ -75,5 +78,42 @@ describe("service skill automation draft", () => {
|
||||
expect(initialValues.cron_expr).toBe("00 09 * * *");
|
||||
expect(initialValues.prompt).toContain("[服务型技能] 每日趋势摘要");
|
||||
expect(initialValues.prompt).toContain("[自动化执行要求]");
|
||||
expect(initialValues.agent_request_metadata).toEqual(
|
||||
expect.objectContaining({
|
||||
artifact: expect.objectContaining({
|
||||
artifact_mode: "draft",
|
||||
artifact_kind: "analysis",
|
||||
}),
|
||||
harness: expect.objectContaining({
|
||||
theme: "social-media",
|
||||
session_mode: "theme_workbench",
|
||||
run_title: "每日趋势摘要",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("应为自动化 agent_turn 生成可复用的 artifact/content payload 上下文", () => {
|
||||
const payloadContext = buildServiceSkillAutomationAgentTurnPayloadContext({
|
||||
skill: SCHEDULED_SKILL,
|
||||
contentId: "content-1",
|
||||
});
|
||||
|
||||
expect(payloadContext).toEqual(
|
||||
expect.objectContaining({
|
||||
content_id: "content-1",
|
||||
request_metadata: expect.objectContaining({
|
||||
artifact: expect.objectContaining({
|
||||
artifact_mode: "draft",
|
||||
artifact_kind: "analysis",
|
||||
}),
|
||||
harness: expect.objectContaining({
|
||||
theme: "social-media",
|
||||
session_mode: "theme_workbench",
|
||||
content_id: "content-1",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { AutomationJobDialogInitialValues } from "@/components/settings-v2/system/automation/AutomationJobDialog";
|
||||
import { buildHarnessRequestMetadata } from "../utils/harnessRequestMetadata";
|
||||
import { composeServiceSkillAutomationPrompt } from "./promptComposer";
|
||||
import type {
|
||||
ServiceSkillItem,
|
||||
ServiceSkillSlotDefinition,
|
||||
ServiceSkillSlotValues,
|
||||
} from "./types";
|
||||
import { buildServiceSkillWorkspaceSeed } from "./workspaceLaunch";
|
||||
|
||||
const DEFAULT_AUTOMATION_INTERVAL_SECS = 86_400;
|
||||
const WEEKDAY_TO_CRON_DAY: Record<string, string> = {
|
||||
@@ -25,6 +27,11 @@ interface BuildServiceSkillAutomationInitialValuesInput {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
interface BuildServiceSkillAutomationAgentTurnPayloadContextInput {
|
||||
skill: ServiceSkillItem;
|
||||
contentId?: string | null;
|
||||
}
|
||||
|
||||
function resolveSlotValue(
|
||||
slot: ServiceSkillSlotDefinition,
|
||||
slotValues: ServiceSkillSlotValues,
|
||||
@@ -53,7 +60,9 @@ function resolveScheduleSlotValue(
|
||||
skill: ServiceSkillItem,
|
||||
slotValues: ServiceSkillSlotValues,
|
||||
): string {
|
||||
const scheduleSlot = skill.slotSchema.find((slot) => slot.type === "schedule_time");
|
||||
const scheduleSlot = skill.slotSchema.find(
|
||||
(slot) => slot.type === "schedule_time",
|
||||
);
|
||||
if (!scheduleSlot) {
|
||||
return "";
|
||||
}
|
||||
@@ -70,7 +79,9 @@ function buildDefaultSchedulePrefill(): Pick<
|
||||
};
|
||||
}
|
||||
|
||||
function buildCronPrefill(expr: string): Pick<
|
||||
function buildCronPrefill(
|
||||
expr: string,
|
||||
): Pick<
|
||||
AutomationJobDialogInitialValues,
|
||||
"schedule_kind" | "cron_expr" | "cron_tz"
|
||||
> {
|
||||
@@ -115,7 +126,9 @@ function parseScheduleTextToPrefill(
|
||||
return buildCronPrefill(`${dailyMatch[2]} ${dailyMatch[1]} * * *`);
|
||||
}
|
||||
|
||||
const weekdayMatch = value.match(/^每周([一二三四五六日天])\s*(\d{1,2}):(\d{2})$/);
|
||||
const weekdayMatch = value.match(
|
||||
/^每周([一二三四五六日天])\s*(\d{1,2}):(\d{2})$/,
|
||||
);
|
||||
if (weekdayMatch) {
|
||||
const cronDay = WEEKDAY_TO_CRON_DAY[weekdayMatch[1]];
|
||||
if (cronDay) {
|
||||
@@ -125,7 +138,9 @@ function parseScheduleTextToPrefill(
|
||||
}
|
||||
}
|
||||
|
||||
const weekdayDailyMatch = value.match(/^(?:工作日|每个工作日)\s*(\d{1,2}):(\d{2})$/);
|
||||
const weekdayDailyMatch = value.match(
|
||||
/^(?:工作日|每个工作日)\s*(\d{1,2}):(\d{2})$/,
|
||||
);
|
||||
if (weekdayDailyMatch) {
|
||||
return buildCronPrefill(
|
||||
`${weekdayDailyMatch[2]} ${weekdayDailyMatch[1]} * * 1-5`,
|
||||
@@ -156,6 +171,34 @@ function buildServiceSkillAutomationDescription(
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildServiceSkillAutomationRequestMetadata(
|
||||
skill: ServiceSkillItem,
|
||||
contentId?: string | null,
|
||||
): Record<string, unknown> | undefined {
|
||||
const targetTheme = skill.themeTarget?.trim();
|
||||
const workspaceSeed = buildServiceSkillWorkspaceSeed(skill, targetTheme);
|
||||
|
||||
if (!targetTheme && !workspaceSeed?.requestMetadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...(workspaceSeed?.requestMetadata ?? {}),
|
||||
harness: buildHarnessRequestMetadata({
|
||||
theme: targetTheme || "general",
|
||||
preferences: {
|
||||
webSearch: false,
|
||||
thinking: false,
|
||||
task: false,
|
||||
subagent: false,
|
||||
},
|
||||
sessionMode: workspaceSeed ? "theme_workbench" : "default",
|
||||
runTitle: skill.title,
|
||||
contentId: contentId || undefined,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function supportsServiceSkillLocalAutomation(
|
||||
skill: ServiceSkillItem,
|
||||
): boolean {
|
||||
@@ -165,6 +208,25 @@ export function supportsServiceSkillLocalAutomation(
|
||||
);
|
||||
}
|
||||
|
||||
export function buildServiceSkillAutomationAgentTurnPayloadContext({
|
||||
skill,
|
||||
contentId,
|
||||
}: BuildServiceSkillAutomationAgentTurnPayloadContextInput): {
|
||||
content_id?: string | null;
|
||||
request_metadata?: Record<string, unknown> | null;
|
||||
} {
|
||||
const normalizedContentId = contentId?.trim() || null;
|
||||
const requestMetadata = buildServiceSkillAutomationRequestMetadata(
|
||||
skill,
|
||||
normalizedContentId,
|
||||
);
|
||||
|
||||
return {
|
||||
content_id: normalizedContentId,
|
||||
request_metadata: requestMetadata ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildServiceSkillAutomationInitialValues({
|
||||
skill,
|
||||
slotValues,
|
||||
@@ -188,6 +250,10 @@ export function buildServiceSkillAutomationInitialValues({
|
||||
}),
|
||||
system_prompt: "",
|
||||
web_search: false,
|
||||
agent_content_id: "",
|
||||
agent_request_metadata: buildServiceSkillAutomationAgentTurnPayloadContext({
|
||||
skill,
|
||||
}).request_metadata,
|
||||
max_retries: "2",
|
||||
delivery_mode: "none",
|
||||
...schedulePrefill,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { ContentType, ProjectType } from "@/lib/api/project";
|
||||
import { getDefaultContentTypeForProject } from "@/lib/api/project";
|
||||
import type { ServiceSkillArtifactKind, ServiceSkillItem } from "@/lib/api/serviceSkills";
|
||||
import type {
|
||||
ServiceSkillArtifactKind,
|
||||
ServiceSkillItem,
|
||||
} from "@/lib/api/serviceSkills";
|
||||
import { isContentCreationTheme } from "@/components/content-creator/utils/systemPrompt";
|
||||
|
||||
export interface ServiceSkillWorkspaceSeed {
|
||||
|
||||
@@ -42,16 +42,16 @@ function resolveTeamWorkbenchTriggerState(params: {
|
||||
teamControlSummary?: TeamWorkspaceControlSummary | null;
|
||||
executionSummary: ReturnType<typeof summarizeTeamWorkspaceExecution>;
|
||||
}): CanvasWorkbenchTeamView["triggerState"] {
|
||||
const runtimeTeamState = params.teamDispatchPreviewState;
|
||||
const dispatchPreviewState = params.teamDispatchPreviewState;
|
||||
if (!params.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (runtimeTeamState?.status === "failed") {
|
||||
if (dispatchPreviewState?.status === "failed") {
|
||||
return { tone: "error", label: "失败" };
|
||||
}
|
||||
|
||||
if (runtimeTeamState?.status === "forming") {
|
||||
if (dispatchPreviewState?.status === "forming") {
|
||||
return { tone: "active", label: "组建中" };
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ function resolveTeamWorkbenchTriggerState(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (runtimeTeamState?.status === "formed" && !params.hasRealTeamGraph) {
|
||||
if (dispatchPreviewState?.status === "formed" && !params.hasRealTeamGraph) {
|
||||
return { tone: "active", label: "已就绪" };
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export function useTeamWorkbenchPresentation({
|
||||
teamWaitSummary = null,
|
||||
teamControlSummary = null,
|
||||
}: UseTeamWorkbenchPresentationParams) {
|
||||
const runtimeTeamState = teamDispatchPreviewState;
|
||||
const dispatchPreviewState = teamDispatchPreviewState;
|
||||
const boardProps = useMemo<ComponentProps<typeof TeamWorkspaceBoard>>(
|
||||
() => ({
|
||||
...surfaceProps,
|
||||
@@ -194,8 +194,8 @@ export function useTeamWorkbenchPresentation({
|
||||
return {
|
||||
enabled: true,
|
||||
title:
|
||||
runtimeTeamState?.label?.trim() ||
|
||||
runtimeTeamState?.blueprint?.label?.trim() ||
|
||||
dispatchPreviewState?.label?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.label?.trim() ||
|
||||
surfaceProps.selectedTeamLabel ||
|
||||
"团队工作台",
|
||||
subtitle: "主对话保留调度记录,画布按角色分别展示执行过程与结果。",
|
||||
@@ -210,7 +210,7 @@ export function useTeamWorkbenchPresentation({
|
||||
autoFocusToken,
|
||||
enabled,
|
||||
renderTeamWorkbenchPreview,
|
||||
runtimeTeamState,
|
||||
dispatchPreviewState,
|
||||
surfaceProps.selectedTeamLabel,
|
||||
teamWorkbenchSummaryPanel,
|
||||
triggerState,
|
||||
|
||||
@@ -18,9 +18,9 @@ interface HookHarness {
|
||||
}
|
||||
|
||||
const mockSendMessage = vi.fn<HookProps["sendMessage"]>(async () => undefined);
|
||||
const mockPrepareRuntimeTeamBeforeSend = vi.fn<HookProps["prepareRuntimeTeamBeforeSend"]>(
|
||||
async () => null,
|
||||
);
|
||||
const mockPrepareRuntimeTeamBeforeSend = vi.fn<
|
||||
HookProps["prepareRuntimeTeamBeforeSend"]
|
||||
>(async () => null);
|
||||
const mockFinalizeAfterSendSuccess = vi.fn();
|
||||
const mockRollbackAfterSendFailure = vi.fn();
|
||||
const mockSetInput = vi.fn();
|
||||
@@ -98,6 +98,7 @@ function mountHook(initialProps?: Partial<HookProps>): HookHarness {
|
||||
currentGateKey: "default_gate",
|
||||
themeWorkbenchActiveQueueTitle: undefined,
|
||||
contentId: null,
|
||||
workspaceRequestMetadataBase: undefined,
|
||||
messagesCount: 0,
|
||||
sendMessage: mockSendMessage,
|
||||
resolveSendBoundary: (({ sourceText }) => ({
|
||||
@@ -106,9 +107,10 @@ function mountHook(initialProps?: Partial<HookProps>): HookHarness {
|
||||
shouldConsumePendingThemeWorkbenchInitialPrompt: false,
|
||||
shouldDismissThemeWorkbenchEntryPrompt: false,
|
||||
})) as HookProps["resolveSendBoundary"],
|
||||
isBlockedByBrowserPreflight: (() => false) as HookProps["isBlockedByBrowserPreflight"],
|
||||
maybeStartBrowserTaskPreflight:
|
||||
(() => false) as HookProps["maybeStartBrowserTaskPreflight"],
|
||||
isBlockedByBrowserPreflight: (() =>
|
||||
false) as HookProps["isBlockedByBrowserPreflight"],
|
||||
maybeStartBrowserTaskPreflight: (() =>
|
||||
false) as HookProps["maybeStartBrowserTaskPreflight"],
|
||||
finalizeAfterSendSuccess:
|
||||
mockFinalizeAfterSendSuccess as HookProps["finalizeAfterSendSuccess"],
|
||||
rollbackAfterSendFailure:
|
||||
@@ -171,20 +173,23 @@ describe("useWorkspaceSendActions", () => {
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
const started = await harness.getValue().handleSend(
|
||||
[],
|
||||
false,
|
||||
false,
|
||||
"继续处理当前话题",
|
||||
"react",
|
||||
autoContinue,
|
||||
);
|
||||
const started = await harness
|
||||
.getValue()
|
||||
.handleSend(
|
||||
[],
|
||||
false,
|
||||
false,
|
||||
"继续处理当前话题",
|
||||
"react",
|
||||
autoContinue,
|
||||
);
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockSendMessage).toHaveBeenCalledTimes(1);
|
||||
const args =
|
||||
mockSendMessage.mock.calls[0] as Parameters<HookProps["sendMessage"]>;
|
||||
const args = mockSendMessage.mock.calls[0] as Parameters<
|
||||
HookProps["sendMessage"]
|
||||
>;
|
||||
expect(args?.[6]).toBeUndefined();
|
||||
expect(args?.[7]).toEqual(autoContinue);
|
||||
expect(args?.[8]).toMatchObject({
|
||||
@@ -221,7 +226,10 @@ describe("useWorkspaceSendActions", () => {
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockSetRuntimeTeamDispatchPreview).toHaveBeenNthCalledWith(1, null);
|
||||
expect(mockSetRuntimeTeamDispatchPreview).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
null,
|
||||
);
|
||||
expect(mockSetRuntimeTeamDispatchPreview).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
@@ -239,4 +247,45 @@ describe("useWorkspaceSendActions", () => {
|
||||
harness.unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("应保留 workspace 级 request metadata,并与 harness 元数据合并", async () => {
|
||||
const harness = mountHook({
|
||||
isThemeWorkbench: true,
|
||||
mappedTheme: "social-media",
|
||||
contentId: "content-service-skill-1",
|
||||
workspaceRequestMetadataBase: {
|
||||
artifact: {
|
||||
artifact_mode: "draft",
|
||||
artifact_kind: "analysis",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await act(async () => {
|
||||
const started = await harness.getValue().handleSend();
|
||||
expect(started).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockSendMessage).toHaveBeenCalledTimes(1);
|
||||
const args = mockSendMessage.mock.calls[0] as Parameters<
|
||||
HookProps["sendMessage"]
|
||||
>;
|
||||
expect(args?.[8]).toMatchObject({
|
||||
requestMetadata: {
|
||||
artifact: {
|
||||
artifact_mode: "draft",
|
||||
artifact_kind: "analysis",
|
||||
},
|
||||
harness: expect.objectContaining({
|
||||
theme: "social-media",
|
||||
session_mode: "theme_workbench",
|
||||
content_id: "content-service-skill-1",
|
||||
}),
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
harness.unmount();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
extractExistingHarnessMetadata,
|
||||
} from "../utils/harnessRequestMetadata";
|
||||
import { isTeamRuntimeRecommendation } from "../utils/contextualRecommendations";
|
||||
import { saveChatToolPreferences, type ChatToolPreferences } from "../utils/chatToolPreferences";
|
||||
import {
|
||||
saveChatToolPreferences,
|
||||
type ChatToolPreferences,
|
||||
} from "../utils/chatToolPreferences";
|
||||
import type { HandleSendOptions } from "../hooks/handleSendTypes";
|
||||
import type { ThemeWorkbenchSendBoundaryState } from "../hooks/useThemeWorkbenchSendBoundary";
|
||||
import type { UseRuntimeTeamFormationResult } from "../hooks/useRuntimeTeamFormation";
|
||||
@@ -90,7 +93,10 @@ interface UseWorkspaceSendActionsParams {
|
||||
}) => Promise<boolean>;
|
||||
}
|
||||
|
||||
function applyActiveContextPrompt(text: string, activeContextPrompt: string): string {
|
||||
function applyActiveContextPrompt(
|
||||
text: string,
|
||||
activeContextPrompt: string,
|
||||
): string {
|
||||
if (!activeContextPrompt.trim()) {
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,11 @@ import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AutomationJobDialog } from "./AutomationJobDialog";
|
||||
|
||||
const {
|
||||
mockListBrowserProfiles,
|
||||
mockListBrowserEnvironmentPresets,
|
||||
} = vi.hoisted(() => ({
|
||||
mockListBrowserProfiles: vi.fn(),
|
||||
mockListBrowserEnvironmentPresets: vi.fn(),
|
||||
}));
|
||||
const { mockListBrowserProfiles, mockListBrowserEnvironmentPresets } =
|
||||
vi.hoisted(() => ({
|
||||
mockListBrowserProfiles: vi.fn(),
|
||||
mockListBrowserEnvironmentPresets: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/browser-runtime/api", () => ({
|
||||
browserRuntimeApi: {
|
||||
@@ -162,6 +160,67 @@ async function renderDialog(props: {
|
||||
}
|
||||
|
||||
describe("AutomationJobDialog", () => {
|
||||
it("编辑 agent_turn 任务时应保留 content_id 与 request_metadata", async () => {
|
||||
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
||||
await renderDialog({
|
||||
onSubmit,
|
||||
jobOverride: {
|
||||
payload: {
|
||||
kind: "agent_turn",
|
||||
prompt: "生成趋势摘要",
|
||||
system_prompt: "请保持简洁",
|
||||
web_search: false,
|
||||
content_id: "content-1",
|
||||
request_metadata: {
|
||||
artifact: {
|
||||
artifact_mode: "draft",
|
||||
artifact_kind: "analysis",
|
||||
},
|
||||
harness: {
|
||||
theme: "social-media",
|
||||
session_mode: "theme_workbench",
|
||||
content_id: "content-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const submitButton = Array.from(document.querySelectorAll("button")).find(
|
||||
(button) => button.textContent?.includes("保存修改"),
|
||||
) as HTMLButtonElement | undefined;
|
||||
|
||||
await act(async () => {
|
||||
submitButton?.click();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
mode: "edit",
|
||||
id: "job-1",
|
||||
request: expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
kind: "agent_turn",
|
||||
prompt: "生成趋势摘要",
|
||||
system_prompt: "请保持简洁",
|
||||
web_search: false,
|
||||
content_id: "content-1",
|
||||
request_metadata: expect.objectContaining({
|
||||
artifact: expect.objectContaining({
|
||||
artifact_mode: "draft",
|
||||
artifact_kind: "analysis",
|
||||
}),
|
||||
harness: expect.objectContaining({
|
||||
theme: "social-media",
|
||||
session_mode: "theme_workbench",
|
||||
content_id: "content-1",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}, 10_000);
|
||||
|
||||
it("编辑浏览器任务时应保持 browser_session payload 提交", async () => {
|
||||
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
||||
await renderDialog({ onSubmit });
|
||||
@@ -265,7 +324,9 @@ describe("AutomationJobDialog", () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
document.querySelector("[data-testid='automation-job-dialog-scroll-area']"),
|
||||
document.querySelector(
|
||||
"[data-testid='automation-job-dialog-scroll-area']",
|
||||
),
|
||||
).not.toBeNull();
|
||||
|
||||
const submitButton = Array.from(document.querySelectorAll("button")).find(
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -53,6 +60,8 @@ type AutomationJobFormState = {
|
||||
prompt: string;
|
||||
system_prompt: string;
|
||||
web_search: boolean;
|
||||
agent_content_id: string;
|
||||
agent_request_metadata: Record<string, unknown> | null;
|
||||
browser_profile_id: string;
|
||||
browser_profile_key: string;
|
||||
browser_url: string;
|
||||
@@ -103,6 +112,8 @@ function createDefaultForm(workspaces: Project[]): AutomationJobFormState {
|
||||
prompt: "",
|
||||
system_prompt: "",
|
||||
web_search: false,
|
||||
agent_content_id: "",
|
||||
agent_request_metadata: null,
|
||||
browser_profile_id: "",
|
||||
browser_profile_key: "",
|
||||
browser_url: "",
|
||||
@@ -210,6 +221,8 @@ function createFormFromJob(
|
||||
form.prompt = job.payload.prompt;
|
||||
form.system_prompt = job.payload.system_prompt ?? "";
|
||||
form.web_search = job.payload.web_search;
|
||||
form.agent_content_id = job.payload.content_id ?? "";
|
||||
form.agent_request_metadata = job.payload.request_metadata ?? null;
|
||||
} else {
|
||||
form.browser_profile_id = job.payload.profile_id;
|
||||
form.browser_profile_key = job.payload.profile_key ?? "";
|
||||
@@ -228,9 +241,9 @@ function createFormFromJob(
|
||||
? "telegram"
|
||||
: job.delivery.channel === "google_sheets"
|
||||
? "google_sheets"
|
||||
: job.delivery.channel === "local_file"
|
||||
? "local_file"
|
||||
: "webhook";
|
||||
: job.delivery.channel === "local_file"
|
||||
? "local_file"
|
||||
: "webhook";
|
||||
form.delivery_target = job.delivery.target ?? "";
|
||||
const deliveryOutputContract = normalizeDeliveryOutputContract(
|
||||
form.delivery_channel,
|
||||
@@ -311,7 +324,9 @@ function scheduleHint(form: AutomationJobFormState): string {
|
||||
if (form.schedule_kind === "cron") {
|
||||
return "使用 Cron 表达式驱动执行。";
|
||||
}
|
||||
return form.at_local ? "一次性任务,到点后自动停用。" : "选择一次性触发时间。";
|
||||
return form.at_local
|
||||
? "一次性任务,到点后自动停用。"
|
||||
: "选择一次性触发时间。";
|
||||
}
|
||||
|
||||
export function AutomationJobDialog({
|
||||
@@ -370,7 +385,9 @@ export function AutomationJobDialog({
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setProfiles(nextProfiles.filter((profile) => profile.archived_at === null));
|
||||
setProfiles(
|
||||
nextProfiles.filter((profile) => profile.archived_at === null),
|
||||
);
|
||||
setPresets(nextPresets.filter((preset) => preset.archived_at === null));
|
||||
} catch (loadError) {
|
||||
if (!cancelled) {
|
||||
@@ -421,16 +438,19 @@ export function AutomationJobDialog({
|
||||
const scheduleSummary = useMemo(() => scheduleHint(form), [form]);
|
||||
const selectedBrowserProfile = useMemo(
|
||||
() =>
|
||||
profiles.find((profile) => profile.id === form.browser_profile_id) ?? null,
|
||||
profiles.find((profile) => profile.id === form.browser_profile_id) ??
|
||||
null,
|
||||
[form.browser_profile_id, profiles],
|
||||
);
|
||||
const selectedEnvironmentPreset = useMemo(
|
||||
() =>
|
||||
presets.find((preset) => preset.id === form.browser_environment_preset_id) ??
|
||||
null,
|
||||
presets.find(
|
||||
(preset) => preset.id === form.browser_environment_preset_id,
|
||||
) ?? null,
|
||||
[form.browser_environment_preset_id, presets],
|
||||
);
|
||||
const isTextOnlyDelivery = form.delivery_channel === TEXT_ONLY_DELIVERY_CHANNEL;
|
||||
const isTextOnlyDelivery =
|
||||
form.delivery_channel === TEXT_ONLY_DELIVERY_CHANNEL;
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
@@ -454,13 +474,16 @@ export function AutomationJobDialog({
|
||||
prompt: form.prompt.trim(),
|
||||
system_prompt: form.system_prompt.trim() || null,
|
||||
web_search: form.web_search,
|
||||
content_id: form.agent_content_id.trim() || null,
|
||||
request_metadata: form.agent_request_metadata ?? null,
|
||||
};
|
||||
} else {
|
||||
if (!form.browser_profile_id.trim()) {
|
||||
throw new Error("请选择浏览器资料");
|
||||
}
|
||||
const profileKey =
|
||||
selectedBrowserProfile?.profile_key ?? form.browser_profile_key.trim();
|
||||
selectedBrowserProfile?.profile_key ??
|
||||
form.browser_profile_key.trim();
|
||||
if (!profileKey) {
|
||||
throw new Error("浏览器资料不可用,请重新选择");
|
||||
}
|
||||
@@ -472,7 +495,10 @@ export function AutomationJobDialog({
|
||||
} catch {
|
||||
throw new Error("浏览器启动地址格式无效");
|
||||
}
|
||||
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
||||
if (
|
||||
parsedUrl.protocol !== "http:" &&
|
||||
parsedUrl.protocol !== "https:"
|
||||
) {
|
||||
throw new Error("浏览器启动地址仅支持 http/https");
|
||||
}
|
||||
}
|
||||
@@ -543,7 +569,9 @@ export function AutomationJobDialog({
|
||||
});
|
||||
}
|
||||
} catch (submitError) {
|
||||
setError(submitError instanceof Error ? submitError.message : "保存任务失败");
|
||||
setError(
|
||||
submitError instanceof Error ? submitError.message : "保存任务失败",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,7 +587,8 @@ export function AutomationJobDialog({
|
||||
{mode === "create" ? "新建自动化任务" : "编辑自动化任务"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
用结构化 job 承载 Agent 与浏览器任务,调度、工作区、运行历史都在单一模型里维护。
|
||||
用结构化 job 承载 Agent
|
||||
与浏览器任务,调度、工作区、运行历史都在单一模型里维护。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -568,37 +597,40 @@ export function AutomationJobDialog({
|
||||
className="min-h-0 flex-1 overflow-y-auto px-4 pb-4 pt-4 sm:px-6 sm:pb-6 sm:pt-5"
|
||||
>
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="automation-job-name">任务名称</Label>
|
||||
<Input
|
||||
id="automation-job-name"
|
||||
value={form.name}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({ ...current, name: event.target.value }))
|
||||
}
|
||||
placeholder="例如:每日品牌线索巡检"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>工作区</Label>
|
||||
<Select
|
||||
value={form.workspace_id}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => ({ ...current, workspace_id: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择工作区" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{workspaces.map((workspace) => (
|
||||
<SelectItem key={workspace.id} value={workspace.id}>
|
||||
{workspace.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="automation-job-name">任务名称</Label>
|
||||
<Input
|
||||
id="automation-job-name"
|
||||
value={form.name}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
name: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="例如:每日品牌线索巡检"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>工作区</Label>
|
||||
<Select
|
||||
value={form.workspace_id}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => ({ ...current, workspace_id: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择工作区" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{workspaces.map((workspace) => (
|
||||
<SelectItem key={workspace.id} value={workspace.id}>
|
||||
{workspace.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-2">
|
||||
@@ -634,7 +666,9 @@ export function AutomationJobDialog({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="agent_turn">Agent 对话任务</SelectItem>
|
||||
<SelectItem value="browser_session">浏览器会话任务</SelectItem>
|
||||
<SelectItem value="browser_session">
|
||||
浏览器会话任务
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -714,72 +748,74 @@ export function AutomationJobDialog({
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{form.schedule_kind === "every" ? (
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label>间隔秒数</Label>
|
||||
<Input
|
||||
value={form.every_secs}
|
||||
type="number"
|
||||
min={60}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
every_secs: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{form.schedule_kind === "cron" ? (
|
||||
<>
|
||||
{form.schedule_kind === "every" ? (
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label>Cron 表达式</Label>
|
||||
<Label>间隔秒数</Label>
|
||||
<Input
|
||||
value={form.cron_expr}
|
||||
value={form.every_secs}
|
||||
type="number"
|
||||
min={60}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
cron_expr: event.target.value,
|
||||
every_secs: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="0 9 * * *"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>时区</Label>
|
||||
) : null}
|
||||
|
||||
{form.schedule_kind === "cron" ? (
|
||||
<>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label>Cron 表达式</Label>
|
||||
<Input
|
||||
value={form.cron_expr}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
cron_expr: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="0 9 * * *"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>时区</Label>
|
||||
<Input
|
||||
value={form.cron_tz}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
cron_tz: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Asia/Shanghai"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{form.schedule_kind === "at" ? (
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label>触发时间</Label>
|
||||
<Input
|
||||
value={form.cron_tz}
|
||||
value={form.at_local}
|
||||
type="datetime-local"
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
cron_tz: event.target.value,
|
||||
at_local: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Asia/Shanghai"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{form.schedule_kind === "at" ? (
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label>触发时间</Label>
|
||||
<Input
|
||||
value={form.at_local}
|
||||
type="datetime-local"
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
at_local: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-xs text-slate-500">{scheduleSummary}</div>
|
||||
<div className="mt-3 text-xs text-slate-500">
|
||||
{scheduleSummary}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{form.payload_kind === "agent_turn" ? (
|
||||
@@ -801,7 +837,9 @@ export function AutomationJobDialog({
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-2">
|
||||
<Label htmlFor="automation-job-system-prompt">附加系统指令</Label>
|
||||
<Label htmlFor="automation-job-system-prompt">
|
||||
附加系统指令
|
||||
</Label>
|
||||
<Textarea
|
||||
id="automation-job-system-prompt"
|
||||
value={form.system_prompt}
|
||||
@@ -819,133 +857,137 @@ export function AutomationJobDialog({
|
||||
) : (
|
||||
<div className="mt-5 rounded-[24px] border border-slate-200/80 bg-white/80 p-4">
|
||||
<div className="grid gap-5 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>浏览器资料</Label>
|
||||
<Select
|
||||
value={form.browser_profile_id || undefined}
|
||||
onValueChange={(value) => {
|
||||
const profile =
|
||||
profiles.find((item) => item.id === value) ?? null;
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
browser_profile_id: value,
|
||||
browser_profile_key:
|
||||
profile?.profile_key ?? current.browser_profile_key,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择浏览器资料" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{form.browser_profile_id && !selectedBrowserProfile ? (
|
||||
<SelectItem value={form.browser_profile_id}>
|
||||
{form.browser_profile_key
|
||||
? `${form.browser_profile_key}(当前绑定,已不可用)`
|
||||
: "当前绑定资料(已不可用)"}
|
||||
<div className="space-y-2">
|
||||
<Label>浏览器资料</Label>
|
||||
<Select
|
||||
value={form.browser_profile_id || undefined}
|
||||
onValueChange={(value) => {
|
||||
const profile =
|
||||
profiles.find((item) => item.id === value) ?? null;
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
browser_profile_id: value,
|
||||
browser_profile_key:
|
||||
profile?.profile_key ?? current.browser_profile_key,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择浏览器资料" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{form.browser_profile_id && !selectedBrowserProfile ? (
|
||||
<SelectItem value={form.browser_profile_id}>
|
||||
{form.browser_profile_key
|
||||
? `${form.browser_profile_key}(当前绑定,已不可用)`
|
||||
: "当前绑定资料(已不可用)"}
|
||||
</SelectItem>
|
||||
) : null}
|
||||
{profiles.map((profile) => (
|
||||
<SelectItem key={profile.id} value={profile.id}>
|
||||
{profile.name} · {profile.profile_key}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="text-xs text-slate-500">
|
||||
调度执行时会复用统一 `launch_browser_session` 启动边界。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>环境预设</Label>
|
||||
<Select
|
||||
value={
|
||||
form.browser_environment_preset_id || NO_PRESET_VALUE
|
||||
}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
browser_environment_preset_id:
|
||||
value === NO_PRESET_VALUE ? "" : value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="不附加环境预设" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NO_PRESET_VALUE}>
|
||||
不附加环境预设
|
||||
</SelectItem>
|
||||
) : null}
|
||||
{profiles.map((profile) => (
|
||||
<SelectItem key={profile.id} value={profile.id}>
|
||||
{profile.name} · {profile.profile_key}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="text-xs text-slate-500">
|
||||
调度执行时会复用统一 `launch_browser_session` 启动边界。
|
||||
{form.browser_environment_preset_id &&
|
||||
!selectedEnvironmentPreset ? (
|
||||
<SelectItem
|
||||
value={form.browser_environment_preset_id}
|
||||
>
|
||||
当前绑定预设(已不可用)
|
||||
</SelectItem>
|
||||
) : null}
|
||||
{presets.map((preset) => (
|
||||
<SelectItem key={preset.id} value={preset.id}>
|
||||
{preset.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="automation-job-browser-url">启动地址</Label>
|
||||
<Input
|
||||
id="automation-job-browser-url"
|
||||
value={form.browser_url}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
browser_url: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="留空则使用资料默认启动地址"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="automation-job-browser-target-id">
|
||||
Target ID
|
||||
</Label>
|
||||
<Input
|
||||
id="automation-job-browser-target-id"
|
||||
value={form.browser_target_id}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
browser_target_id: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="可选,指定固定 target"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>流模式</Label>
|
||||
<Select
|
||||
value={form.browser_stream_mode}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
browser_stream_mode: value as BrowserStreamMode,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="events">仅事件</SelectItem>
|
||||
<SelectItem value="frames">仅画面</SelectItem>
|
||||
<SelectItem value="both">事件 + 画面</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>环境预设</Label>
|
||||
<Select
|
||||
value={form.browser_environment_preset_id || NO_PRESET_VALUE}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
browser_environment_preset_id:
|
||||
value === NO_PRESET_VALUE ? "" : value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="不附加环境预设" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={NO_PRESET_VALUE}>
|
||||
不附加环境预设
|
||||
</SelectItem>
|
||||
{form.browser_environment_preset_id &&
|
||||
!selectedEnvironmentPreset ? (
|
||||
<SelectItem value={form.browser_environment_preset_id}>
|
||||
当前绑定预设(已不可用)
|
||||
</SelectItem>
|
||||
) : null}
|
||||
{presets.map((preset) => (
|
||||
<SelectItem key={preset.id} value={preset.id}>
|
||||
{preset.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="automation-job-browser-url">启动地址</Label>
|
||||
<Input
|
||||
id="automation-job-browser-url"
|
||||
value={form.browser_url}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
browser_url: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="留空则使用资料默认启动地址"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="automation-job-browser-target-id">
|
||||
Target ID
|
||||
</Label>
|
||||
<Input
|
||||
id="automation-job-browser-target-id"
|
||||
value={form.browser_target_id}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
browser_target_id: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="可选,指定固定 target"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>流模式</Label>
|
||||
<Select
|
||||
value={form.browser_stream_mode}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
browser_stream_mode: value as BrowserStreamMode,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="events">仅事件</SelectItem>
|
||||
<SelectItem value="frames">仅画面</SelectItem>
|
||||
<SelectItem value="both">事件 + 画面</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{resourcesLoading ? (
|
||||
<div className="mt-3 text-xs text-slate-500">
|
||||
正在加载浏览器资料与环境预设...
|
||||
@@ -962,45 +1004,49 @@ export function AutomationJobDialog({
|
||||
|
||||
<div className="mt-5 grid gap-4 rounded-[24px] border border-slate-200/80 bg-white/80 p-4 md:grid-cols-2">
|
||||
<div className="flex items-center justify-between rounded-[18px] border border-slate-200/80 bg-slate-50/70 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-slate-900">启用任务</div>
|
||||
<div className="text-xs text-slate-500">关闭后 job 不参与轮询</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-slate-900">
|
||||
启用任务
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
关闭后 job 不参与轮询
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={form.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm((current) => ({ ...current, enabled: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
checked={form.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm((current) => ({ ...current, enabled: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-[18px] border border-slate-200/80 bg-slate-50/70 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-slate-900">
|
||||
{form.payload_kind === "agent_turn"
|
||||
? "允许 Web 搜索"
|
||||
: "自动打开调试窗口"}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-slate-900">
|
||||
{form.payload_kind === "agent_turn"
|
||||
? "允许 Web 搜索"
|
||||
: "自动打开调试窗口"}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{form.payload_kind === "agent_turn"
|
||||
? "为这个 job 单独开启搜索能力"
|
||||
: "适合需要人工观察或接管的浏览器任务"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{form.payload_kind === "agent_turn"
|
||||
? "为这个 job 单独开启搜索能力"
|
||||
: "适合需要人工观察或接管的浏览器任务"}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={
|
||||
form.payload_kind === "agent_turn"
|
||||
? form.web_search
|
||||
: form.browser_open_window
|
||||
}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
...(current.payload_kind === "agent_turn"
|
||||
? { web_search: checked }
|
||||
: { browser_open_window: checked }),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Switch
|
||||
checked={
|
||||
form.payload_kind === "agent_turn"
|
||||
? form.web_search
|
||||
: form.browser_open_window
|
||||
}
|
||||
onCheckedChange={(checked) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
...(current.payload_kind === "agent_turn"
|
||||
? { web_search: checked }
|
||||
: { browser_open_window: checked }),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1027,92 +1073,96 @@ export function AutomationJobDialog({
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{form.delivery_mode === "announce" ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>输出目标</Label>
|
||||
<Select
|
||||
value={form.delivery_channel}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => {
|
||||
const deliveryChannel = value as
|
||||
| "webhook"
|
||||
| "telegram"
|
||||
| "local_file"
|
||||
| "google_sheets";
|
||||
const contract = normalizeDeliveryOutputContract(
|
||||
deliveryChannel,
|
||||
current.delivery_output_schema,
|
||||
current.delivery_output_format,
|
||||
);
|
||||
return {
|
||||
{form.delivery_mode === "announce" ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>输出目标</Label>
|
||||
<Select
|
||||
value={form.delivery_channel}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => {
|
||||
const deliveryChannel = value as
|
||||
| "webhook"
|
||||
| "telegram"
|
||||
| "local_file"
|
||||
| "google_sheets";
|
||||
const contract = normalizeDeliveryOutputContract(
|
||||
deliveryChannel,
|
||||
current.delivery_output_schema,
|
||||
current.delivery_output_format,
|
||||
);
|
||||
return {
|
||||
...current,
|
||||
delivery_channel: deliveryChannel,
|
||||
delivery_output_schema: contract.outputSchema,
|
||||
delivery_output_format: contract.outputFormat,
|
||||
};
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="webhook">Webhook</SelectItem>
|
||||
<SelectItem value="google_sheets">
|
||||
Google Sheets
|
||||
</SelectItem>
|
||||
<SelectItem value="local_file">本地文件</SelectItem>
|
||||
<SelectItem value="telegram">Telegram</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>输出契约</Label>
|
||||
<Select
|
||||
disabled={isTextOnlyDelivery}
|
||||
value={form.delivery_output_schema}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
delivery_channel: deliveryChannel,
|
||||
delivery_output_schema: contract.outputSchema,
|
||||
delivery_output_format: contract.outputFormat,
|
||||
};
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="webhook">Webhook</SelectItem>
|
||||
<SelectItem value="google_sheets">Google Sheets</SelectItem>
|
||||
<SelectItem value="local_file">本地文件</SelectItem>
|
||||
<SelectItem value="telegram">Telegram</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>输出契约</Label>
|
||||
<Select
|
||||
disabled={isTextOnlyDelivery}
|
||||
value={form.delivery_output_schema}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
delivery_output_schema: value as AutomationOutputSchema,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">文本摘要</SelectItem>
|
||||
<SelectItem value="json">JSON 对象</SelectItem>
|
||||
<SelectItem value="table">表格</SelectItem>
|
||||
<SelectItem value="csv">CSV</SelectItem>
|
||||
<SelectItem value="links">链接列表</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>输出格式</Label>
|
||||
<Select
|
||||
disabled={isTextOnlyDelivery}
|
||||
value={form.delivery_output_format}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
delivery_output_format: value as AutomationOutputFormat,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">文本摘要</SelectItem>
|
||||
<SelectItem value="json">结构化 JSON</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
delivery_output_schema:
|
||||
value as AutomationOutputSchema,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">文本摘要</SelectItem>
|
||||
<SelectItem value="json">JSON 对象</SelectItem>
|
||||
<SelectItem value="table">表格</SelectItem>
|
||||
<SelectItem value="csv">CSV</SelectItem>
|
||||
<SelectItem value="links">链接列表</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>输出格式</Label>
|
||||
<Select
|
||||
disabled={isTextOnlyDelivery}
|
||||
value={form.delivery_output_format}
|
||||
onValueChange={(value) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
delivery_output_format:
|
||||
value as AutomationOutputFormat,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">文本摘要</SelectItem>
|
||||
<SelectItem value="json">结构化 JSON</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{form.delivery_mode === "announce" ? (
|
||||
<>
|
||||
@@ -1190,7 +1240,11 @@ export function AutomationJobDialog({
|
||||
onClick={() => void handleSubmit()}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? "保存中..." : mode === "create" ? "创建任务" : "保存修改"}
|
||||
{saving
|
||||
? "保存中..."
|
||||
: mode === "create"
|
||||
? "创建任务"
|
||||
: "保存修改"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ export type AutomationOutputSchema =
|
||||
| "table"
|
||||
| "csv"
|
||||
| "links";
|
||||
export type AutomationRequestMetadata = Record<string, unknown>;
|
||||
|
||||
export interface DeliveryConfig {
|
||||
mode: "none" | "announce";
|
||||
@@ -69,6 +70,8 @@ export interface AgentTurnAutomationPayload {
|
||||
prompt: string;
|
||||
system_prompt?: string | null;
|
||||
web_search: boolean;
|
||||
content_id?: string | null;
|
||||
request_metadata?: AutomationRequestMetadata | null;
|
||||
}
|
||||
|
||||
export interface BrowserSessionAutomationPayload {
|
||||
|
||||
@@ -7,9 +7,7 @@ export type ServiceSkillSource = "cloud_catalog" | "local_custom";
|
||||
|
||||
export type ServiceSkillRunnerType = "instant" | "scheduled" | "managed";
|
||||
|
||||
export type ServiceSkillExecutionLocation =
|
||||
| "client_default"
|
||||
| "cloud_required";
|
||||
export type ServiceSkillExecutionLocation = "client_default" | "cloud_required";
|
||||
|
||||
export type ServiceSkillArtifactKind =
|
||||
| "report"
|
||||
@@ -119,7 +117,8 @@ const SEEDED_SERVICE_SKILL_CATALOG: ServiceSkillCatalog = {
|
||||
id: "carousel-post-replication",
|
||||
skillKey: "carousel-post-replication",
|
||||
title: "复制轮播帖",
|
||||
summary: "拆解参考轮播帖的结构、文风和卖点,再输出一版可继续改写的轮播内容。",
|
||||
summary:
|
||||
"拆解参考轮播帖的结构、文风和卖点,再输出一版可继续改写的轮播内容。",
|
||||
category: "社媒内容",
|
||||
outputHint: "轮播结构 + 文案初稿",
|
||||
source: "cloud_catalog",
|
||||
@@ -325,7 +324,8 @@ const SEEDED_SERVICE_SKILL_CATALOG: ServiceSkillCatalog = {
|
||||
id: "daily-trend-briefing",
|
||||
skillKey: "daily-trend-briefing",
|
||||
title: "每日趋势摘要",
|
||||
summary: "围绕指定平台、行业和地区,先产出一版趋势摘要和后续本地定时任务建议。",
|
||||
summary:
|
||||
"围绕指定平台、行业和地区,先产出一版趋势摘要和后续本地定时任务建议。",
|
||||
category: "社媒运营",
|
||||
outputHint: "趋势摘要 + 调度建议",
|
||||
source: "cloud_catalog",
|
||||
@@ -386,7 +386,8 @@ const SEEDED_SERVICE_SKILL_CATALOG: ServiceSkillCatalog = {
|
||||
id: "account-performance-tracking",
|
||||
skillKey: "account-performance-tracking",
|
||||
title: "跟踪账号表现",
|
||||
summary: "围绕指定账号先做一版表现分析,并整理后续本地跟踪任务需要的指标和告警条件。",
|
||||
summary:
|
||||
"围绕指定账号先做一版表现分析,并整理后续本地跟踪任务需要的指标和告警条件。",
|
||||
category: "社媒运营",
|
||||
outputHint: "账号分析 + 跟踪指标",
|
||||
source: "cloud_catalog",
|
||||
@@ -464,7 +465,8 @@ function isServiceSkillSlotDefinition(
|
||||
typeof slot.type === "string" &&
|
||||
typeof slot.required === "boolean" &&
|
||||
typeof slot.placeholder === "string" &&
|
||||
(slot.defaultValue === undefined || typeof slot.defaultValue === "string") &&
|
||||
(slot.defaultValue === undefined ||
|
||||
typeof slot.defaultValue === "string") &&
|
||||
(slot.helpText === undefined || typeof slot.helpText === "string") &&
|
||||
(slot.options === undefined || isSlotOptionArray(slot.options))
|
||||
);
|
||||
@@ -521,7 +523,9 @@ function isServiceSkillCatalog(value: unknown): value is ServiceSkillCatalog {
|
||||
);
|
||||
}
|
||||
|
||||
function cloneServiceSkillCatalog(catalog: ServiceSkillCatalog): ServiceSkillCatalog {
|
||||
function cloneServiceSkillCatalog(
|
||||
catalog: ServiceSkillCatalog,
|
||||
): ServiceSkillCatalog {
|
||||
return {
|
||||
...catalog,
|
||||
items: catalog.items.map((item) => ({
|
||||
@@ -637,7 +641,9 @@ async function requestRemoteServiceSkillCatalog(): Promise<ServiceSkillCatalog>
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.message?.trim() || `请求失败 (${response.status})`);
|
||||
throw new Error(
|
||||
payload?.message?.trim() || `请求失败 (${response.status})`,
|
||||
);
|
||||
}
|
||||
|
||||
const catalog = parseServiceSkillCatalog(payload?.data);
|
||||
@@ -663,7 +669,10 @@ export function isSeededServiceSkillCatalog(
|
||||
|
||||
export function saveServiceSkillCatalog(
|
||||
catalog: ServiceSkillCatalog,
|
||||
source: Exclude<ServiceSkillCatalogChangeSource, "seeded_fallback" | "cache_clear"> = "manual_override",
|
||||
source: Exclude<
|
||||
ServiceSkillCatalogChangeSource,
|
||||
"seeded_fallback" | "cache_clear"
|
||||
> = "manual_override",
|
||||
): ServiceSkillCatalog {
|
||||
const normalized = parseServiceSkillCatalog(catalog);
|
||||
if (!normalized) {
|
||||
|
||||
Reference in New Issue
Block a user