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";
import type { ProjectExecutionRouter } from "./services/project-execution-router.js";
import type { ProjectSkillRouterService } from "./services/project-skill-router.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 { AutomationTaskService } from "./services/automation-task-service.js";
import {
......@@ -1786,6 +1787,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
const isXhsProject = (value: string): boolean => /xiaohongshu|xhs|小红书/.test(value);
const isDouyinProject = (value: string): boolean => /douyin|抖音|tiktok/.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 normalizedProjectId = projectId.trim().toLowerCase();
......@@ -1799,6 +1803,15 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
if (isZhihuProject(normalizedProjectId)) {
return "知乎专家";
}
if (isWechatProject(normalizedProjectId)) {
return "微信公众号运营";
}
if (isContentPlannerProject(normalizedProjectId)) {
return "内容创作者";
}
if (isGeoProject(normalizedProjectId)) {
return "AI推荐引擎优化";
}
if (isXhsProject(projectBaseName)) {
return "小红书专家";
}
......@@ -1808,6 +1821,15 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
if (isZhihuProject(projectBaseName)) {
return "知乎专家";
}
if (isWechatProject(projectBaseName)) {
return "微信公众号运营";
}
if (isContentPlannerProject(projectBaseName)) {
return "内容创作者";
}
if (isGeoProject(projectBaseName)) {
return "AI推荐引擎优化";
}
return null;
};
......@@ -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 (
sessionId: string,
prompt: string,
......@@ -2030,8 +2011,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
projectConfig
});
const preferWorkspaceEntry = resolvedAttachments.length > 0 || declaredWorkspaceEntryDecision?.kind === "workspace-entry";
const runtimeStatus = await runtimeManager.status();
const canUseSkills = runtimeStatus.activeMode === "bundled-runtime";
const autoSkillRoute = requestedSkillId
|| preferWorkspaceEntry
|| !canUseSkills
? null
: await projectSkillRouter.resolve(target.sessionState.projectId, prompt);
const defaultEntryRoute = (!requestedSkillId && !autoSkillRoute && projectConfig?.defaultEntry?.type === "skill")
......@@ -2139,6 +2123,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
undefined,
"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, {
reason: "chat-send",
execute: () => gatewayClient.sendPrompt(executionSessionId, result.handoff.content)
......@@ -2151,13 +2140,19 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
fallbackExecutionPolicy.modelId,
executionSkillId
);
const handoffArtifacts = await collectWorkspaceExecutionArtifacts({
projectRoot: preparedExecution.sessionState.projectRoot,
beforeSnapshot: handoffBeforeSnapshot,
assistantSummary: fallbackResult.reply.content,
extraScanRoots: [handoffWorkspaceRoot]
}).catch(() => []);
await recordWorkspaceTaskPanelExecution({
sessionId: executionSessionId,
projectId: preparedExecution.sessionState.projectId,
projectRoot: preparedExecution.sessionState.projectRoot,
prompt,
runId: result.runId,
artifacts: result.artifacts
artifacts: [...(result.artifacts ?? []), ...handoffArtifacts]
});
return { ...fallbackResult, executionPolicy: fallbackExecutionPolicy };
}
......@@ -2178,6 +2173,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
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, {
reason: "chat-send",
execute: () => gatewayClient.sendPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt)
......@@ -2185,13 +2185,19 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await projectStore.appendSessionMessage(result.sessionId, result.reply);
await projectStore.updateSessionLastActive(result.sessionId).catch(() => undefined);
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({
sessionId: result.sessionId,
projectId: preparedExecution.sessionState.projectId,
projectRoot: preparedExecution.sessionState.projectRoot,
prompt,
runId: result.reply.id || randomUUID(),
artifacts: []
artifacts: gatewayArtifacts
});
return { ...result, executionPolicy: preparedExecution.executionPolicy };
} catch (error) {
......@@ -2230,7 +2236,9 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
projectConfig
});
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")
? ((await projectStore.getProjectSkillTarget(project.id, projectConfig.defaultEntry.id))
? {
......@@ -2278,6 +2286,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
});
if ("handoff" in result) {
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, {
reason: "automation-task",
execute: () => gatewayClient.sendPrompt(sessionId, withAutomationDomesticSourceConstraint(result.handoff.content))
......@@ -2288,7 +2301,13 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
fallbackExecutionPolicy.modelId,
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);
return {
......@@ -2693,6 +2712,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
stage: "chat-fallback",
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, {
reason: "chat-stream",
execute: () => gatewayClient.streamPrompt(executionSessionId, result.handoff.content, {
......@@ -2777,13 +2801,29 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await projectStore.updateSessionLastActive(nextSessionId).catch(() => undefined);
})().catch(() => undefined);
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({
sessionId: executionSessionId,
projectId: preparedExecution.sessionState.projectId,
projectRoot: preparedExecution.sessionState.projectRoot,
prompt,
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({
type: "completed",
......@@ -2905,6 +2945,11 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
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, {
reason: "chat-stream",
execute: () => gatewayClient.streamPrompt(executionSessionId, preparedExecution.gatewayPrompt ?? prompt, {
......@@ -2990,6 +3035,21 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
await projectStore.updateSessionLastActive(nextSessionId).catch(() => undefined);
})().catch(() => undefined);
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({
sessionId: nextSessionId,
projectId: preparedExecution.sessionState.projectId,
......@@ -2998,6 +3058,7 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
runId,
artifacts: []
});
});
queueOrSend({
type: "completed",
requestId,
......@@ -3140,21 +3201,6 @@ export function registerDesktopIpc(services: MainServices): RegisteredDesktopIpc
const sessionId = `automation:${projectId}:${task.id}:${run.id}`;
runtimeCloudSupervisor.noteSessions([sessionId]);
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 {
sessionId: result.sessionId,
......
......@@ -101,6 +101,7 @@ export interface CollectWorkspaceExecutionArtifactsInput {
projectRoot: string;
beforeSnapshot: WorkspaceArtifactSnapshot;
assistantSummary?: string;
extraScanRoots?: string[];
}
function toErrorMessage(error: unknown): string {
......@@ -198,6 +199,9 @@ function shouldSkipWorkspaceArtifactPath(relativePath: string): boolean {
if (segments[0] === "inputs") {
return true;
}
if (segments[0] === "session-messages") {
return true;
}
return false;
}
......@@ -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();
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;
}
export async function collectWorkspaceExecutionArtifacts(
input: CollectWorkspaceExecutionArtifactsInput
): Promise<TaskPanelArtifact[]> {
const afterSnapshot = await createWorkspaceArtifactSnapshot(input.projectRoot);
const afterSnapshot = await createWorkspaceArtifactSnapshot(input.projectRoot, input.extraScanRoots);
const changedEntries = [...afterSnapshot.values()]
.filter((entry) => {
const before = input.beforeSnapshot.get(entry.relativePath);
......@@ -466,7 +485,10 @@ export class ProjectWorkspaceExecutorService {
const paths = this.runtimeManager.resolveBundledPaths();
const automationCommand = await resolveProjectAutomationCommand(input.projectRoot, input, paths.pythonExecutable);
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 instrumentationDir = path.join(paths.runtimeDataDir, "workspace-runner", "instrumented-modules");
......@@ -531,7 +553,8 @@ export class ProjectWorkspaceExecutorService {
const collectArtifacts = (assistantSummary?: string) => collectWorkspaceExecutionArtifacts({
projectRoot: input.projectRoot,
beforeSnapshot: beforeArtifactSnapshot,
assistantSummary
assistantSummary,
extraScanRoots: [path.join(paths.runtimeDataDir, "workspace")]
}).catch(() => []);
child.stdout.setEncoding("utf8");
......
......@@ -23,7 +23,7 @@ function resolveTaskExpertIconKey(expertName: string): ExpertVisualKey {
if (/zhihu|知乎/.test(seed)) {
return "zhihu"
}
if (/content-account|planner|账号规划|内容账号规划/.test(seed)) {
if (/content-account|planner|账号规划|内容账号规划|内容创作|内容规划/.test(seed)) {
return "planner"
}
if (/precision-leads|线索|lead/.test(seed)) {
......@@ -38,7 +38,7 @@ function resolveTaskExpertIconKey(expertName: string): ExpertVisualKey {
if (/(^|[\s-])x($|[\s-])|twitter/.test(seed)) {
return "x"
}
if (/geo/.test(seed)) {
if (/geo|AI推荐|推荐引擎/.test(seed)) {
return "geo"
}
if (/browser|automation|chrome|playwright|web|浏览器|自动化/.test(seed)) {
......@@ -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[] }) {
const summary = useMemo(() => summarizeTaskPanelItems(items), [items])
const stats = [
......@@ -203,7 +194,7 @@ function TaskPanelOutputList({ outputs }: { outputs: TaskPanelOutputItem[] }) {
<div className="task-panel-output-header">
<div className="task-panel-output-heading">
<span className="task-panel-output-heading-icon" aria-hidden="true">
<TaskPanelPackageIcon />
📦
</span>
<h2>内容产出</h2>
</div>
......
......@@ -7,10 +7,28 @@ export interface TaskPanelSummary {
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) {
return /douyin|抖音/.test(expertName.toLowerCase())
function resolveExpertArtifactExtensions(expertName: string): Set<string> | null {
for (const rule of expertArtifactExtensionRules) {
if (rule.pattern.test(expertName)) {
return rule.extensions
}
}
return null
}
function getTaskPanelArtifactExtension(artifact: TaskPanelArtifact) {
......@@ -21,11 +39,12 @@ function getTaskPanelArtifactExtension(artifact: TaskPanelArtifact) {
}
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.filter((artifact) => douyinVisibleArtifactExtensions.has(getTaskPanelArtifactExtension(artifact)))
return task.artifacts.filter((artifact) => allowedExtensions.has(getTaskPanelArtifactExtension(artifact)))
}
function toDateInputValue(date: Date) {
......@@ -125,51 +144,42 @@ export const mockTaskPanelItems: TaskPanelItem[] = [
]
},
{
id: "mock-task-leads",
id: "mock-task-wechat-article",
date: getDefaultTaskPanelDate(),
expertName: "平台精准线索专家",
taskTitle: "筛选高意向线索名单",
status: "pending",
statusDetail: "等待线索表上传后开始处理",
creditsUsed: 0,
messageCount: 4,
updatedAt: "12:10",
artifacts: []
},
{
id: "mock-task-poster",
date: toDateInputValue(addDays(new Date(), -1)),
expertName: "海报专家",
taskTitle: "生成活动海报文案",
status: "failed",
statusDetail: "素材包缺少主视觉图片",
creditsUsed: 360,
messageCount: 12,
updatedAt: "18:22",
expertName: "微信公众号运营",
taskTitle: "撰写公众号推文与涨粉计划",
status: "completed",
statusDetail: "已完成推文草稿、排版建议和涨粉策略",
creditsUsed: 1050,
messageCount: 28,
updatedAt: "14:15",
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",
date: toDateInputValue(addDays(new Date(), -1)),
expertName: "平台精准线索专家",
taskTitle: "清洗昨日线索表",
id: "mock-task-geo-optimize",
date: getDefaultTaskPanelDate(),
expertName: "AI推荐引擎优化",
taskTitle: "优化品牌在AI搜索引擎中的可见性",
status: "completed",
statusDetail: "已完成重复线索剔除和等级标注",
creditsUsed: 520,
messageCount: 16,
updatedAt: "17:40",
statusDetail: "已完成关键词分析、内容优化建议和技术审计",
creditsUsed: 880,
messageCount: 22,
updatedAt: "15:30",
artifacts: [
{ id: "artifact-yesterday-leads", name: "昨日高意向线索清单.xlsx", kind: "表格", summary: "线索分级、跟进优先级和备注字段。", url: "/Users/edy/Documents/qianjiangclaw/tasks/leads/昨日高意向线索清单.xlsx" },
{ id: "artifact-yesterday-followup", name: "跟进话术建议.md", kind: "文档", summary: "按线索来源拆分的首轮沟通建议。", url: "/Users/edy/Documents/qianjiangclaw/tasks/leads/跟进话术建议.md" }
{ id: "artifact-geo-report", name: "AI搜索可见性优化报告.md", kind: "文档", summary: "品牌在主流AI搜索引擎中的现状分析与优化建议。", url: "/Users/edy/Documents/qianjiangclaw/tasks/geo/AI搜索可见性优化报告.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 {
const employeeCount = new Set(items.map((item) => item.expertName)).size
const summary = items.reduce<TaskPanelSummary>((nextSummary, item) => {
const standaloneItems = items.filter((item) => isStandaloneExpert(item.expertName))
const employeeCount = new Set(standaloneItems.map((item) => item.expertName)).size
const summary = standaloneItems.reduce<TaskPanelSummary>((nextSummary, item) => {
nextSummary.creditsUsed += item.creditsUsed ?? 0
nextSummary.messageCount += item.messageCount ?? 0
nextSummary.artifactCount += getVisibleTaskPanelArtifacts(item).length
......@@ -185,8 +195,11 @@ export function summarizeTaskPanelItems(items: TaskPanelItem[]): TaskPanelSummar
}
export async function loadTaskPanelItems(date: string): Promise<TaskPanelItem[]> {
let items: TaskPanelItem[]
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