Commit e7115c62 authored by edy's avatar edy

fix(desktop): add artifact scanning for all 6 standalone experts in task panel

- Gateway paths (zhihu/wechat/content-planner) now do snapshot diff instead
  of hardcoded artifacts: [] — scan projectRoot + runtime/workspace/
- Handoff paths (geo workspace executor → gateway) now capture artifacts
  after the gateway call, not before when files haven't been generated yet
- Add collectWorkspaceExecutionArtifacts extraScanRoots parameter for
  scanning additional directories beyond projectRoot
- Exclude session-messages/ from artifact scanning to avoid false positives
- Add isWechatProject/isContentPlannerProject/isGeoProject helpers and
  extend resolveTaskPanelExpertName for 3 new expert name mappings
- Guard projectSkillRouter.resolve() behind bundled-runtime mode check
  to prevent "skills require bundled runtime" error in external-gateway mode
- Remove automation task panel recording (定时任务 excluded from workbench)
- UI: replace single douyin filter with per-expert artifact extension rules
  (expertArtifactExtensionRules), filter home-chat experts from workbench,
  restore 📦 emoji icon, update mock data for all 6 experts
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 22fdc81e
Pipeline #18508 failed
...@@ -64,6 +64,7 @@ import type { ProjectContextService } from "./services/project-context.js"; ...@@ -64,6 +64,7 @@ import type { ProjectContextService } from "./services/project-context.js";
import type { ProjectExecutionRouter } from "./services/project-execution-router.js"; import type { ProjectExecutionRouter } from "./services/project-execution-router.js";
import type { ProjectSkillRouterService } from "./services/project-skill-router.js"; import type { ProjectSkillRouterService } from "./services/project-skill-router.js";
import type { ProjectWorkspaceExecutorService } from "./services/project-workspace-executor.js"; import type { ProjectWorkspaceExecutorService } from "./services/project-workspace-executor.js";
import { createWorkspaceArtifactSnapshot, collectWorkspaceExecutionArtifacts } from "./services/project-workspace-executor.js";
import type { TaskPanelService } from "./services/task-panel-service.js"; import type { TaskPanelService } from "./services/task-panel-service.js";
import type { AutomationTaskService } from "./services/automation-task-service.js"; import type { AutomationTaskService } from "./services/automation-task-service.js";
import { import {
...@@ -1786,6 +1787,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1786,6 +1787,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
const isXhsProject = (value: string): boolean => /xiaohongshu|xhs|小红书/.test(value); const isXhsProject = (value: string): boolean => /xiaohongshu|xhs|小红书/.test(value);
const isDouyinProject = (value: string): boolean => /douyin|抖音|tiktok/.test(value); const isDouyinProject = (value: string): boolean => /douyin|抖音|tiktok/.test(value);
const isZhihuProject = (value: string): boolean => /zhihu|知乎/.test(value); const isZhihuProject = (value: string): boolean => /zhihu|知乎/.test(value);
const isWechatProject = (value: string): boolean => /wechat|weixin|公众号|微信/.test(value);
const isContentPlannerProject = (value: string): boolean => /content-account|planner|内容账号|内容创作|内容规划/.test(value);
const isGeoProject = (value: string): boolean => /geo/.test(value);
const resolveTaskPanelExpertName = (projectId: string, projectRoot: string): string | null => { const resolveTaskPanelExpertName = (projectId: string, projectRoot: string): string | null => {
const normalizedProjectId = projectId.trim().toLowerCase(); const normalizedProjectId = projectId.trim().toLowerCase();
...@@ -1799,6 +1803,15 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1799,6 +1803,15 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
if (isZhihuProject(normalizedProjectId)) { if (isZhihuProject(normalizedProjectId)) {
return "知乎专家"; return "知乎专家";
} }
if (isWechatProject(normalizedProjectId)) {
return "微信公众号运营";
}
if (isContentPlannerProject(normalizedProjectId)) {
return "内容创作者";
}
if (isGeoProject(normalizedProjectId)) {
return "AI推荐引擎优化";
}
if (isXhsProject(projectBaseName)) { if (isXhsProject(projectBaseName)) {
return "小红书专家"; return "小红书专家";
} }
...@@ -1808,6 +1821,15 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1808,6 +1821,15 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
if (isZhihuProject(projectBaseName)) { if (isZhihuProject(projectBaseName)) {
return "知乎专家"; return "知乎专家";
} }
if (isWechatProject(projectBaseName)) {
return "微信公众号运营";
}
if (isContentPlannerProject(projectBaseName)) {
return "内容创作者";
}
if (isGeoProject(projectBaseName)) {
return "AI推荐引擎优化";
}
return null; return null;
}; };
...@@ -1849,47 +1871,6 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -1849,47 +1871,6 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
} }
}; };
const summarizeAutomationReply = (replyText: string): string => {
const normalized = replyText.replace(/\s+/g, " ").trim();
return normalized.length > 160 ? normalized.slice(0, 160) + "..." : normalized;
};
const toAutomationTaskPanelArtifacts = (
artifacts: TaskPanelArtifact[] | undefined,
replyText: string,
runId: string
): TaskPanelArtifact[] => {
if (artifacts?.length) {
return artifacts;
}
const summary = summarizeAutomationReply(replyText);
if (!summary) {
return [];
}
return [
{
id: `automation-reply:${runId}`,
name: "自动化执行结果",
kind: "回复",
summary
}
];
};
const resolveAutomationTaskPanelExpertName = async (task: AutomationTask, projectId: string): Promise<string> => {
const taskExpertName = task.expertName?.trim();
if (taskExpertName) {
return taskExpertName;
}
const project = await projectStore.getProjectSummary(projectId).catch(() => null);
return project?.displayName?.trim()
|| project?.name?.trim()
|| (projectId === BUILTIN_HOME_PROJECT_ID ? "通用助手" : projectId);
};
const sendHomeImagePrompt = async ( const sendHomeImagePrompt = async (
sessionId: string, sessionId: string,
prompt: string, prompt: string,
...@@ -2030,8 +2011,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2030,8 +2011,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
projectConfig projectConfig
}); });
const preferWorkspaceEntry = resolvedAttachments.length > 0 || declaredWorkspaceEntryDecision?.kind === "workspace-entry"; const preferWorkspaceEntry = resolvedAttachments.length > 0 || declaredWorkspaceEntryDecision?.kind === "workspace-entry";
const runtimeStatus = await runtimeManager.status();
const canUseSkills = runtimeStatus.activeMode === "bundled-runtime";
const autoSkillRoute = requestedSkillId const autoSkillRoute = requestedSkillId
|| preferWorkspaceEntry || preferWorkspaceEntry
|| !canUseSkills
? null ? null
: await projectSkillRouter.resolve(target.sessionState.projectId, prompt); : await projectSkillRouter.resolve(target.sessionState.projectId, prompt);
const defaultEntryRoute = (!requestedSkillId && !autoSkillRoute && projectConfig?.defaultEntry?.type === "skill") const defaultEntryRoute = (!requestedSkillId && !autoSkillRoute && projectConfig?.defaultEntry?.type === "skill")
...@@ -2139,6 +2123,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2139,6 +2123,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
undefined, undefined,
"chat-fallback" "chat-fallback"
); );
const handoffWorkspaceRoot = path.join(runtimeManager.resolveBundledPaths().runtimeDataDir, "workspace");
const handoffBeforeSnapshot = await createWorkspaceArtifactSnapshot(
preparedExecution.sessionState.projectRoot,
[handoffWorkspaceRoot]
).catch(() => new Map());
const fallbackResult = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, { const fallbackResult = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "chat-send", reason: "chat-send",
execute: () => gatewayClient.sendPrompt(executionSessionId, result.handoff.content) execute: () => gatewayClient.sendPrompt(executionSessionId, result.handoff.content)
...@@ -2151,13 +2140,19 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2151,13 +2140,19 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
fallbackExecutionPolicy.modelId, fallbackExecutionPolicy.modelId,
executionSkillId executionSkillId
); );
const handoffArtifacts = await collectWorkspaceExecutionArtifacts({
projectRoot: preparedExecution.sessionState.projectRoot,
beforeSnapshot: handoffBeforeSnapshot,
assistantSummary: fallbackResult.reply.content,
extraScanRoots: [handoffWorkspaceRoot]
}).catch(() => []);
await recordWorkspaceTaskPanelExecution({ await recordWorkspaceTaskPanelExecution({
sessionId: executionSessionId, sessionId: executionSessionId,
projectId: preparedExecution.sessionState.projectId, projectId: preparedExecution.sessionState.projectId,
projectRoot: preparedExecution.sessionState.projectRoot, projectRoot: preparedExecution.sessionState.projectRoot,
prompt, prompt,
runId: result.runId, runId: result.runId,
artifacts: result.artifacts artifacts: [...(result.artifacts ?? []), ...handoffArtifacts]
}); });
return { ...fallbackResult, executionPolicy: fallbackExecutionPolicy }; return { ...fallbackResult, executionPolicy: fallbackExecutionPolicy };
} }
...@@ -2178,6 +2173,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2178,6 +2173,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
executionPolicy: preparedExecution.executionPolicy executionPolicy: preparedExecution.executionPolicy
}; };
} }
const gatewayWorkspaceRoot = path.join(runtimeManager.resolveBundledPaths().runtimeDataDir, "workspace");
const gatewayBeforeSnapshot = await createWorkspaceArtifactSnapshot(
preparedExecution.sessionState.projectRoot,
[gatewayWorkspaceRoot]
).catch(() => new Map());
const result = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, { const result = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "chat-send", reason: "chat-send",
execute: () => gatewayClient.sendPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt) execute: () => gatewayClient.sendPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt)
...@@ -2185,13 +2185,19 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2185,13 +2185,19 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await projectStore.appendSessionMessage(result.sessionId, result.reply); await projectStore.appendSessionMessage(result.sessionId, result.reply);
await projectStore.updateSessionLastActive(result.sessionId).catch(() => undefined); await projectStore.updateSessionLastActive(result.sessionId).catch(() => undefined);
runtimeCloudSupervisor.noteMessageSent(result.sessionId, result.reply.content, preparedExecution.executionPolicy.modelId, executionSkillId); runtimeCloudSupervisor.noteMessageSent(result.sessionId, result.reply.content, preparedExecution.executionPolicy.modelId, executionSkillId);
const gatewayArtifacts = await collectWorkspaceExecutionArtifacts({
projectRoot: preparedExecution.sessionState.projectRoot,
beforeSnapshot: gatewayBeforeSnapshot,
assistantSummary: result.reply.content,
extraScanRoots: [gatewayWorkspaceRoot]
}).catch(() => []);
await recordWorkspaceTaskPanelExecution({ await recordWorkspaceTaskPanelExecution({
sessionId: result.sessionId, sessionId: result.sessionId,
projectId: preparedExecution.sessionState.projectId, projectId: preparedExecution.sessionState.projectId,
projectRoot: preparedExecution.sessionState.projectRoot, projectRoot: preparedExecution.sessionState.projectRoot,
prompt, prompt,
runId: result.reply.id || randomUUID(), runId: result.reply.id || randomUUID(),
artifacts: [] artifacts: gatewayArtifacts
}); });
return { ...result, executionPolicy: preparedExecution.executionPolicy }; return { ...result, executionPolicy: preparedExecution.executionPolicy };
} catch (error) { } catch (error) {
...@@ -2230,7 +2236,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2230,7 +2236,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
projectConfig projectConfig
}); });
const preferWorkspaceEntry = declaredWorkspaceEntryDecision.kind === "workspace-entry"; const preferWorkspaceEntry = declaredWorkspaceEntryDecision.kind === "workspace-entry";
const autoSkillRoute = preferWorkspaceEntry ? null : await projectSkillRouter.resolve(project.id, prompt); const autoRuntimeStatus = await runtimeManager.status();
const autoCanUseSkills = autoRuntimeStatus.activeMode === "bundled-runtime";
const autoSkillRoute = (preferWorkspaceEntry || !autoCanUseSkills) ? null : await projectSkillRouter.resolve(project.id, prompt);
const defaultEntryRoute = (!autoSkillRoute && projectConfig?.defaultEntry?.type === "skill") const defaultEntryRoute = (!autoSkillRoute && projectConfig?.defaultEntry?.type === "skill")
? ((await projectStore.getProjectSkillTarget(project.id, projectConfig.defaultEntry.id)) ? ((await projectStore.getProjectSkillTarget(project.id, projectConfig.defaultEntry.id))
? { ? {
...@@ -2278,6 +2286,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2278,6 +2286,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
}); });
if ("handoff" in result) { if ("handoff" in result) {
const fallbackExecutionPolicy = await resolveExecutionPolicy(project.id, undefined, "chat-fallback"); const fallbackExecutionPolicy = await resolveExecutionPolicy(project.id, undefined, "chat-fallback");
const autoHandoffWorkspaceRoot = path.join(runtimeManager.resolveBundledPaths().runtimeDataDir, "workspace");
const autoHandoffBeforeSnapshot = await createWorkspaceArtifactSnapshot(
projectRoot,
[autoHandoffWorkspaceRoot]
).catch(() => new Map());
const fallbackResult = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, { const fallbackResult = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "automation-task", reason: "automation-task",
execute: () => gatewayClient.sendPrompt(sessionId, withAutomationDomesticSourceConstraint(result.handoff.content)) execute: () => gatewayClient.sendPrompt(sessionId, withAutomationDomesticSourceConstraint(result.handoff.content))
...@@ -2288,7 +2301,13 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2288,7 +2301,13 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
fallbackExecutionPolicy.modelId, fallbackExecutionPolicy.modelId,
executionSkillId executionSkillId
); );
return { ...fallbackResult, executionPolicy: fallbackExecutionPolicy, artifacts: result.artifacts }; const autoHandoffArtifacts = await collectWorkspaceExecutionArtifacts({
projectRoot,
beforeSnapshot: autoHandoffBeforeSnapshot,
assistantSummary: fallbackResult.reply.content,
extraScanRoots: [autoHandoffWorkspaceRoot]
}).catch(() => []);
return { ...fallbackResult, executionPolicy: fallbackExecutionPolicy, artifacts: [...(result.artifacts ?? []), ...autoHandoffArtifacts] };
} }
runtimeCloudSupervisor.noteMessageSent(sessionId, result.reply.content, executionPolicy.modelId, executionSkillId); runtimeCloudSupervisor.noteMessageSent(sessionId, result.reply.content, executionPolicy.modelId, executionSkillId);
return { return {
...@@ -2693,6 +2712,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2693,6 +2712,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
stage: "chat-fallback", stage: "chat-fallback",
label: "Routing to chat model" label: "Routing to chat model"
}); });
const streamHandoffWorkspaceRoot = path.join(runtimeManager.resolveBundledPaths().runtimeDataDir, "workspace");
const streamHandoffBeforeSnapshot = await createWorkspaceArtifactSnapshot(
preparedExecution.sessionState.projectRoot,
[streamHandoffWorkspaceRoot]
).catch(() => new Map());
await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, { await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "chat-stream", reason: "chat-stream",
execute: () => gatewayClient.streamPrompt(executionSessionId, result.handoff.content, { execute: () => gatewayClient.streamPrompt(executionSessionId, result.handoff.content, {
...@@ -2777,13 +2801,29 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2777,13 +2801,29 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await projectStore.updateSessionLastActive(nextSessionId).catch(() => undefined); await projectStore.updateSessionLastActive(nextSessionId).catch(() => undefined);
})().catch(() => undefined); })().catch(() => undefined);
runtimeCloudSupervisor.noteMessageSent(nextSessionId, reply.content, executionPolicy?.modelId, executionSkillId); runtimeCloudSupervisor.noteMessageSent(nextSessionId, reply.content, executionPolicy?.modelId, executionSkillId);
void collectWorkspaceExecutionArtifacts({
projectRoot: preparedExecution.sessionState.projectRoot,
beforeSnapshot: streamHandoffBeforeSnapshot,
assistantSummary: reply.content,
extraScanRoots: [streamHandoffWorkspaceRoot]
}).then((gatewayArtifacts) => {
void recordWorkspaceTaskPanelExecution({ void recordWorkspaceTaskPanelExecution({
sessionId: executionSessionId, sessionId: executionSessionId,
projectId: preparedExecution.sessionState.projectId, projectId: preparedExecution.sessionState.projectId,
projectRoot: preparedExecution.sessionState.projectRoot, projectRoot: preparedExecution.sessionState.projectRoot,
prompt, prompt,
runId: result.runId, runId: result.runId,
artifacts: result.artifacts artifacts: [...(result.artifacts ?? []), ...gatewayArtifacts]
});
}).catch(() => {
void recordWorkspaceTaskPanelExecution({
sessionId: executionSessionId,
projectId: preparedExecution.sessionState.projectId,
projectRoot: preparedExecution.sessionState.projectRoot,
prompt,
runId: result.runId,
artifacts: result.artifacts ?? []
});
}); });
queueOrSend({ queueOrSend({
type: "completed", type: "completed",
...@@ -2905,6 +2945,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2905,6 +2945,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
executionPolicy: executionPolicy ?? undefined executionPolicy: executionPolicy ?? undefined
}; };
} }
const gatewayStreamWorkspaceRoot = path.join(runtimeManager.resolveBundledPaths().runtimeDataDir, "workspace");
const gatewayStreamBeforeSnapshot = await createWorkspaceArtifactSnapshot(
preparedExecution.sessionState.projectRoot,
[gatewayStreamWorkspaceRoot]
).catch(() => new Map());
const stream = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, { const stream = await runGatewayChatRequestWithRecovery(chatGatewayRecoveryCoordinator, {
reason: "chat-stream", reason: "chat-stream",
execute: () => gatewayClient.streamPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt, { execute: () => gatewayClient.streamPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt, {
...@@ -2990,6 +3035,21 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2990,6 +3035,21 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await projectStore.updateSessionLastActive(nextSessionId).catch(() => undefined); await projectStore.updateSessionLastActive(nextSessionId).catch(() => undefined);
})().catch(() => undefined); })().catch(() => undefined);
runtimeCloudSupervisor.noteMessageSent(nextSessionId, reply.content, executionPolicy?.modelId, executionSkillId); runtimeCloudSupervisor.noteMessageSent(nextSessionId, reply.content, executionPolicy?.modelId, executionSkillId);
void collectWorkspaceExecutionArtifacts({
projectRoot: preparedExecution.sessionState.projectRoot,
beforeSnapshot: gatewayStreamBeforeSnapshot,
assistantSummary: reply.content,
extraScanRoots: [gatewayStreamWorkspaceRoot]
}).then((gatewayArtifacts) => {
void recordWorkspaceTaskPanelExecution({
sessionId: nextSessionId,
projectId: preparedExecution.sessionState.projectId,
projectRoot: preparedExecution.sessionState.projectRoot,
prompt,
runId,
artifacts: gatewayArtifacts
});
}).catch(() => {
void recordWorkspaceTaskPanelExecution({ void recordWorkspaceTaskPanelExecution({
sessionId: nextSessionId, sessionId: nextSessionId,
projectId: preparedExecution.sessionState.projectId, projectId: preparedExecution.sessionState.projectId,
...@@ -2998,6 +3058,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -2998,6 +3058,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
runId, runId,
artifacts: [] artifacts: []
}); });
});
queueOrSend({ queueOrSend({
type: "completed", type: "completed",
requestId, requestId,
...@@ -3140,21 +3201,6 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc ...@@ -3140,21 +3201,6 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
const sessionId = `automation:${projectId}:${task.id}:${run.id}`; const sessionId = `automation:${projectId}:${task.id}:${run.id}`;
runtimeCloudSupervisor.noteSessions([sessionId]); runtimeCloudSupervisor.noteSessions([sessionId]);
const result = await executeAutomationPrompt(sessionId, projectId, task.prompt); const result = await executeAutomationPrompt(sessionId, projectId, task.prompt);
const expertName = await resolveAutomationTaskPanelExpertName(task, projectId);
await taskPanelService.recordWorkspaceExecution({
sessionId: result.sessionId,
runId: run.id,
expertName,
taskTitle: task.title,
completedAt: new Date().toISOString(),
messageCount: 2,
artifacts: toAutomationTaskPanelArtifacts(result.artifacts, result.reply.content, run.id)
}).catch((error) => startupLogger.warn("diagnostics", "automation-task.archive-failed", "Failed to archive automation task output.", {
taskId: task.id,
runId: run.id,
error: error instanceof Error ? error.message : String(error)
}));
return { return {
sessionId: result.sessionId, sessionId: result.sessionId,
......
...@@ -101,6 +101,7 @@ export interface CollectWorkspaceExecutionArtifactsInput { ...@@ -101,6 +101,7 @@ export interface CollectWorkspaceExecutionArtifactsInput {
projectRoot: string; projectRoot: string;
beforeSnapshot: WorkspaceArtifactSnapshot; beforeSnapshot: WorkspaceArtifactSnapshot;
assistantSummary?: string; assistantSummary?: string;
extraScanRoots?: string[];
} }
function toErrorMessage(error: unknown): string { function toErrorMessage(error: unknown): string {
...@@ -198,6 +199,9 @@ function shouldSkipWorkspaceArtifactPath(relativePath: string): boolean { ...@@ -198,6 +199,9 @@ function shouldSkipWorkspaceArtifactPath(relativePath: string): boolean {
if (segments[0] === "inputs") { if (segments[0] === "inputs") {
return true; return true;
} }
if (segments[0] === "session-messages") {
return true;
}
return false; return false;
} }
...@@ -269,16 +273,31 @@ async function scanWorkspaceArtifacts( ...@@ -269,16 +273,31 @@ async function scanWorkspaceArtifacts(
} }
} }
export async function createWorkspaceArtifactSnapshot(projectRoot: string): Promise<WorkspaceArtifactSnapshot> { export async function createWorkspaceArtifactSnapshot(
projectRoot: string,
extraScanRoots?: string[]
): Promise<WorkspaceArtifactSnapshot> {
const snapshot: WorkspaceArtifactSnapshot = new Map(); const snapshot: WorkspaceArtifactSnapshot = new Map();
await scanWorkspaceArtifacts(projectRoot, projectRoot, snapshot); await scanWorkspaceArtifacts(projectRoot, projectRoot, snapshot);
if (extraScanRoots) {
for (const extraRoot of extraScanRoots) {
try {
const extraStat = await stat(extraRoot);
if (extraStat.isDirectory()) {
await scanWorkspaceArtifacts(extraRoot, extraRoot, snapshot);
}
} catch {
// extra root not accessible — skip
}
}
}
return snapshot; return snapshot;
} }
export async function collectWorkspaceExecutionArtifacts( export async function collectWorkspaceExecutionArtifacts(
input: CollectWorkspaceExecutionArtifactsInput input: CollectWorkspaceExecutionArtifactsInput
): Promise<TaskPanelArtifact[]> { ): Promise<TaskPanelArtifact[]> {
const afterSnapshot = await createWorkspaceArtifactSnapshot(input.projectRoot); const afterSnapshot = await createWorkspaceArtifactSnapshot(input.projectRoot, input.extraScanRoots);
const changedEntries = [...afterSnapshot.values()] const changedEntries = [...afterSnapshot.values()]
.filter((entry) => { .filter((entry) => {
const before = input.beforeSnapshot.get(entry.relativePath); const before = input.beforeSnapshot.get(entry.relativePath);
...@@ -466,7 +485,10 @@ export class ProjectWorkspaceExecutorService { ...@@ -466,7 +485,10 @@ export class ProjectWorkspaceExecutorService {
const paths = this.runtimeManager.resolveBundledPaths(); const paths = this.runtimeManager.resolveBundledPaths();
const automationCommand = await resolveProjectAutomationCommand(input.projectRoot, input, paths.pythonExecutable); const automationCommand = await resolveProjectAutomationCommand(input.projectRoot, input, paths.pythonExecutable);
const runnerScriptPath = automationCommand ? null : await resolveRunnerScriptPath(); const runnerScriptPath = automationCommand ? null : await resolveRunnerScriptPath();
const beforeArtifactSnapshot = await createWorkspaceArtifactSnapshot(input.projectRoot); const beforeArtifactSnapshot = await createWorkspaceArtifactSnapshot(
input.projectRoot,
[path.join(paths.runtimeDataDir, "workspace")]
);
const vendorPackageDir = path.join(paths.runtimeDir, "openclaw", "package"); const vendorPackageDir = path.join(paths.runtimeDir, "openclaw", "package");
const instrumentationDir = path.join(paths.runtimeDataDir, "workspace-runner", "instrumented-modules"); const instrumentationDir = path.join(paths.runtimeDataDir, "workspace-runner", "instrumented-modules");
...@@ -531,7 +553,8 @@ export class ProjectWorkspaceExecutorService { ...@@ -531,7 +553,8 @@ export class ProjectWorkspaceExecutorService {
const collectArtifacts = (assistantSummary?: string) => collectWorkspaceExecutionArtifacts({ const collectArtifacts = (assistantSummary?: string) => collectWorkspaceExecutionArtifacts({
projectRoot: input.projectRoot, projectRoot: input.projectRoot,
beforeSnapshot: beforeArtifactSnapshot, beforeSnapshot: beforeArtifactSnapshot,
assistantSummary assistantSummary,
extraScanRoots: [path.join(paths.runtimeDataDir, "workspace")]
}).catch(() => []); }).catch(() => []);
child.stdout.setEncoding("utf8"); child.stdout.setEncoding("utf8");
......
...@@ -23,7 +23,7 @@ function resolveTaskExpertIconKey(expertName: string): ExpertVisualKey { ...@@ -23,7 +23,7 @@ function resolveTaskExpertIconKey(expertName: string): ExpertVisualKey {
if (/zhihu|知乎/.test(seed)) { if (/zhihu|知乎/.test(seed)) {
return "zhihu" return "zhihu"
} }
if (/content-account|planner|账号规划|内容账号规划/.test(seed)) { if (/content-account|planner|账号规划|内容账号规划|内容创作|内容规划/.test(seed)) {
return "planner" return "planner"
} }
if (/precision-leads|线索|lead/.test(seed)) { if (/precision-leads|线索|lead/.test(seed)) {
...@@ -38,7 +38,7 @@ function resolveTaskExpertIconKey(expertName: string): ExpertVisualKey { ...@@ -38,7 +38,7 @@ function resolveTaskExpertIconKey(expertName: string): ExpertVisualKey {
if (/(^|[\s-])x($|[\s-])|twitter/.test(seed)) { if (/(^|[\s-])x($|[\s-])|twitter/.test(seed)) {
return "x" return "x"
} }
if (/geo/.test(seed)) { if (/geo|AI推荐|推荐引擎/.test(seed)) {
return "geo" return "geo"
} }
if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) { if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) {
...@@ -110,15 +110,6 @@ function TaskPanelOutputIcon({ artifact }: { artifact: TaskPanelArtifact }) { ...@@ -110,15 +110,6 @@ function TaskPanelOutputIcon({ artifact }: { artifact: TaskPanelArtifact }) {
) )
} }
function TaskPanelPackageIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M4.75 8.15 12 4.2l7.25 3.95v7.7L12 19.8l-7.25-3.95v-7.7Z" fill="none" stroke="currentColor" strokeLinejoin="round" strokeWidth="1.7" />
<path d="m4.95 8.35 7.05 3.9 7.05-3.9M12 12.25v7.25M8.35 6.2l7.25 4" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.55" />
</svg>
)
}
function TaskPanelStatCards({ items }: { items: TaskPanelItem[] }) { function TaskPanelStatCards({ items }: { items: TaskPanelItem[] }) {
const summary = useMemo(() => summarizeTaskPanelItems(items), [items]) const summary = useMemo(() => summarizeTaskPanelItems(items), [items])
const stats = [ const stats = [
...@@ -203,7 +194,7 @@ function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) { ...@@ -203,7 +194,7 @@ function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) {
<div className="task-panel-output-header"> <div className="task-panel-output-header">
<div className="task-panel-output-heading"> <div className="task-panel-output-heading">
<span className="task-panel-output-heading-icon" aria-hidden="true"> <span className="task-panel-output-heading-icon" aria-hidden="true">
<TaskPanelPackageIcon /> 📦
</span> </span>
<h2>内容产出</h2> <h2>内容产出</h2>
</div> </div>
......
...@@ -7,10 +7,28 @@ export interface TaskPanelSummary { ...@@ -7,10 +7,28 @@ export interface TaskPanelSummary {
employeeCount: number employeeCount: number
} }
const douyinVisibleArtifactExtensions = new Set([".docx", ".mp4"]) const expertArtifactExtensionRules: { pattern: RegExp; extensions: Set<string> }[] = [
{ pattern: /小红书|xiaohongshu|xhs|rednote/i, extensions: new Set([".md", ".docx", ".xlsx", ".png", ".jpg"]) },
{ pattern: /抖音|douyin/i, extensions: new Set([".docx", ".mp4"]) },
{ pattern: /微信|公众号|wechat|weixin/i, extensions: new Set([".md", ".docx", ".xlsx"]) },
{ pattern: /知乎|zhihu/i, extensions: new Set([".md", ".docx", ".xlsx"]) },
{ pattern: /内容创作|内容规划|内容账号|content-account|planner/i, extensions: new Set([".md", ".docx", ".xlsx", ".csv", ".pdf"]) },
{ pattern: /AI推荐|推荐引擎|geo/i, extensions: new Set([".md", ".xlsx", ".csv"]) }
]
const standaloneExpertPatterns: RegExp[] = expertArtifactExtensionRules.map((rule) => rule.pattern)
function isStandaloneExpert(expertName: string): boolean {
return standaloneExpertPatterns.some((pattern) => pattern.test(expertName))
}
function isDouyinTaskPanelExpert(expertName: string) { function resolveExpertArtifactExtensions(expertName: string): Set<string> | null {
return /douyin|抖音/.test(expertName.toLowerCase()) for (const rule of expertArtifactExtensionRules) {
if (rule.pattern.test(expertName)) {
return rule.extensions
}
}
return null
} }
function getTaskPanelArtifactExtension(artifact: TaskPanelArtifact) { function getTaskPanelArtifactExtension(artifact: TaskPanelArtifact) {
...@@ -21,11 +39,12 @@ function getTaskPanelArtifactExtension(artifact: TaskPanelArtifact) { ...@@ -21,11 +39,12 @@ function getTaskPanelArtifactExtension(artifact: TaskPanelArtifact) {
} }
export function getVisibleTaskPanelArtifacts(task: Pick<TaskPanelItem, "expertName" | "artifacts">) { export function getVisibleTaskPanelArtifacts(task: Pick<TaskPanelItem, "expertName" | "artifacts">) {
if (!isDouyinTaskPanelExpert(task.expertName)) { const allowedExtensions = resolveExpertArtifactExtensions(task.expertName)
if (!allowedExtensions) {
return task.artifacts return task.artifacts
} }
return task.artifacts.filter((artifact) => douyinVisibleArtifactExtensions.has(getTaskPanelArtifactExtension(artifact))) return task.artifacts.filter((artifact) => allowedExtensions.has(getTaskPanelArtifactExtension(artifact)))
} }
function toDateInputValue(date: Date) { function toDateInputValue(date: Date) {
...@@ -125,51 +144,42 @@ export const mockTaskPanelItems: TaskPanelItem[] = [ ...@@ -125,51 +144,42 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
] ]
}, },
{ {
id: "mock-task-leads", id: "mock-task-wechat-article",
date: getDefaultTaskPanelDate(), date: getDefaultTaskPanelDate(),
expertName: "平台精准线索专家", expertName: "微信公众号运营",
taskTitle: "筛选高意向线索名单", taskTitle: "撰写公众号推文与涨粉计划",
status: "pending", status: "completed",
statusDetail: "等待线索表上传后开始处理", statusDetail: "已完成推文草稿、排版建议和涨粉策略",
creditsUsed: 0, creditsUsed: 1050,
messageCount: 4, messageCount: 28,
updatedAt: "12:10", updatedAt: "14:15",
artifacts: []
},
{
id: "mock-task-poster",
date: toDateInputValue(addDays(new Date(), -1)),
expertName: "海报专家",
taskTitle: "生成活动海报文案",
status: "failed",
statusDetail: "素材包缺少主视觉图片",
creditsUsed: 360,
messageCount: 12,
updatedAt: "18:22",
artifacts: [ artifacts: [
{ id: "artifact-poster-brief", name: "活动海报文案草稿.txt", kind: "文档", summary: "活动主题、主标题和利益点文案草稿。", url: "/Users/edy/Documents/qianjiangclaw/tasks/poster/活动海报文案草稿.txt" } { id: "artifact-wechat-draft", name: "公众号推文草稿.md", kind: "文档", summary: "推文正文、标题方案和封面图建议。", url: "/Users/edy/Documents/qianjiangclaw/tasks/wechat/公众号推文草稿.md" },
{ id: "artifact-wechat-plan", name: "涨粉运营计划.docx", kind: "文档", summary: "月度涨粉目标、活动策划和转化路径。", url: "/Users/edy/Documents/qianjiangclaw/tasks/wechat/涨粉运营计划.docx" },
{ id: "artifact-wechat-calendar", name: "推文排期表.xlsx", kind: "表格", summary: "按日期拆分的选题、撰稿人和发布状态。", url: "/Users/edy/Documents/qianjiangclaw/tasks/wechat/推文排期表.xlsx" }
] ]
}, },
{ {
id: "mock-task-yesterday-leads", id: "mock-task-geo-optimize",
date: toDateInputValue(addDays(new Date(), -1)), date: getDefaultTaskPanelDate(),
expertName: "平台精准线索专家", expertName: "AI推荐引擎优化",
taskTitle: "清洗昨日线索表", taskTitle: "优化品牌在AI搜索引擎中的可见性",
status: "completed", status: "completed",
statusDetail: "已完成重复线索剔除和等级标注", statusDetail: "已完成关键词分析、内容优化建议和技术审计",
creditsUsed: 520, creditsUsed: 880,
messageCount: 16, messageCount: 22,
updatedAt: "17:40", updatedAt: "15:30",
artifacts: [ artifacts: [
{ id: "artifact-yesterday-leads", name: "昨日高意向线索清单.xlsx", kind: "表格", summary: "线索分级、跟进优先级和备注字段。", url: "/Users/edy/Documents/qianjiangclaw/tasks/leads/昨日高意向线索清单.xlsx" }, { id: "artifact-geo-report", name: "AI搜索可见性优化报告.md", kind: "文档", summary: "品牌在主流AI搜索引擎中的现状分析与优化建议。", url: "/Users/edy/Documents/qianjiangclaw/tasks/geo/AI搜索可见性优化报告.md" },
{ id: "artifact-yesterday-followup", name: "跟进话术建议.md", kind: "文档", summary: "按线索来源拆分的首轮沟通建议。", url: "/Users/edy/Documents/qianjiangclaw/tasks/leads/跟进话术建议.md" } { id: "artifact-geo-keywords", name: "GEO关键词矩阵.xlsx", kind: "表格", summary: "按搜索意图分类的关键词覆盖率和竞争度。", url: "/Users/edy/Documents/qianjiangclaw/tasks/geo/GEO关键词矩阵.xlsx" }
] ]
} }
] ]
export function summarizeTaskPanelItems(items: TaskPanelItem[]): TaskPanelSummary { export function summarizeTaskPanelItems(items: TaskPanelItem[]): TaskPanelSummary {
const employeeCount = new Set(items.map((item) => item.expertName)).size const standaloneItems = items.filter((item) => isStandaloneExpert(item.expertName))
const summary = items.reduce<TaskPanelSummary>((nextSummary, item) => { const employeeCount = new Set(standaloneItems.map((item) => item.expertName)).size
const summary = standaloneItems.reduce<TaskPanelSummary>((nextSummary, item) => {
nextSummary.creditsUsed += item.creditsUsed ?? 0 nextSummary.creditsUsed += item.creditsUsed ?? 0
nextSummary.messageCount += item.messageCount ?? 0 nextSummary.messageCount += item.messageCount ?? 0
nextSummary.artifactCount += getVisibleTaskPanelArtifacts(item).length nextSummary.artifactCount += getVisibleTaskPanelArtifacts(item).length
...@@ -185,8 +195,11 @@ export function summarizeTaskPanelItems(items: TaskPanelItem[]): TaskPanelSummar ...@@ -185,8 +195,11 @@ export function summarizeTaskPanelItems(items: TaskPanelItem[]): TaskPanelSummar
} }
export async function loadTaskPanelItems(date: string): Promise<TaskPanelItem[]> { export async function loadTaskPanelItems(date: string): Promise<TaskPanelItem[]> {
let items: TaskPanelItem[]
if (typeof window !== "undefined" && window.qjcDesktop) { if (typeof window !== "undefined" && window.qjcDesktop) {
return window.qjcDesktop.tasks.listByDate(date) items = await window.qjcDesktop.tasks.listByDate(date)
} else {
items = mockTaskPanelItems.filter((item) => item.date === date)
} }
return mockTaskPanelItems.filter((item) => item.date === date) return items.filter((item) => isStandaloneExpert(item.expertName))
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment