feat: update automation workspace and release pipeline

This commit is contained in:
coso
2026-03-25 23:13:17 +08:00
parent b8c10ad8ea
commit 8b9f91bd31
26 changed files with 2476 additions and 813 deletions
@@ -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("[服务型技能] 每日趋势摘要"),
}),
);
+187 -39
View File
@@ -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}
+203 -227
View File
@@ -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,
+5 -8
View File
@@ -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>
+3
View File
@@ -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 {
+19 -10
View File
@@ -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) {