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
This diff is collapsed.
...@@ -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 isDouyinTaskPanelExpert(expertName: string) { function isStandaloneExpert(expertName: string): boolean {
return /douyin|抖音/.test(expertName.toLowerCase()) return standaloneExpertPatterns.some((pattern) => pattern.test(expertName))
}
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) { 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