diff --git a/app/api/canvas-skill-tags/route.ts b/app/api/canvas-skill-tags/route.ts index 6820bba4e..11f68b249 100644 --- a/app/api/canvas-skill-tags/route.ts +++ b/app/api/canvas-skill-tags/route.ts @@ -4,6 +4,7 @@ import { auth } from "@/app/(auth)/auth"; import { db } from "@/lib/db"; import { systemSettings } from "@/lib/db/schema"; import { + CANVAS_SKILL_TAGS_CONFIG_VERSION, CANVAS_SKILL_TAGS_SETTING_KEY, normalizeCanvasSkillTagSettings, } from "@/lib/canvas-skill-tags"; @@ -16,7 +17,9 @@ export async function GET() { .where(eq(systemSettings.key, CANVAS_SKILL_TAGS_SETTING_KEY)) .limit(1); - const settings = normalizeCanvasSkillTagSettings(setting?.value); + const settings = normalizeCanvasSkillTagSettings(setting?.value, { + requireCurrentVersion: true, + }); return NextResponse.json(settings); } catch (error) { console.error("[CanvasSkillTags API] Get error:", error); @@ -36,7 +39,12 @@ export async function PUT(request: NextRequest) { const enabled = normalized.enabled; const title = normalized.title; const tags = normalized.tags; - const nextValue = { enabled, title, tags }; + const nextValue = { + enabled, + title, + tags, + version: CANVAS_SKILL_TAGS_CONFIG_VERSION, + }; const [existing] = await db .select() diff --git a/app/api/canvas/clawd-chat/route.ts b/app/api/canvas/clawd-chat/route.ts index ff281dd24..9df44252c 100644 --- a/app/api/canvas/clawd-chat/route.ts +++ b/app/api/canvas/clawd-chat/route.ts @@ -20,6 +20,8 @@ import { } from "@/lib/openclaw-chat/spreadsheet-markdown"; import { generateUUID } from "@/lib/utils"; +const CLAW_UPGRADE_RETRY_MESSAGE = "claw正在升级更新稍等5分钟再试"; + const requestSchema = z.object({ id: z.string().uuid(), message: z.string().trim().max(8000).optional().default(""), @@ -120,21 +122,6 @@ async function readOpenClawConfig() { return normalizeOpenClawChatConfig(setting?.value); } -function getOpenClawErrorMessage(payload: unknown, fallback: string) { - if (payload && typeof payload === "object") { - const raw = payload as Record; - const nestedError = - raw.error && typeof raw.error === "object" - ? (raw.error as Record) - : null; - const message = nestedError?.message || raw.message || raw.error; - if (typeof message === "string" && message.trim()) { - return message.trim(); - } - } - return fallback; -} - function getOpenClawErrorCode(payload: unknown) { if (!payload || typeof payload !== "object") return ""; const raw = payload as Record; @@ -163,12 +150,12 @@ export async function POST(request: Request) { const config = await readOpenClawConfig(); if (!config.enabled) { - return NextResponse.json({ error: "Claw 未启用" }, { status: 403 }); + return NextResponse.json({ error: CLAW_UPGRADE_RETRY_MESSAGE }, { status: 503 }); } if (!config.baseUrl || !config.token) { return NextResponse.json( - { error: "Claw 接口地址或 Token 未配置" }, - { status: 500 } + { error: CLAW_UPGRADE_RETRY_MESSAGE }, + { status: 503 } ); } @@ -272,12 +259,14 @@ export async function POST(request: Request) { if (!response.ok) { const errorCode = getOpenClawErrorCode(upstreamPayload); + console.error("[ClawdChat API] OpenClaw upstream error:", { + status: response.status, + code: errorCode || undefined, + payload: upstreamPayload, + }); return NextResponse.json( { - error: - response.status === 429 - ? "正在处理中,请稍后重试" - : getOpenClawErrorMessage(upstreamPayload, "大龙虾聊天接口调用失败"), + error: CLAW_UPGRADE_RETRY_MESSAGE, code: errorCode || undefined, }, { status: response.status } @@ -286,11 +275,10 @@ export async function POST(request: Request) { } catch (error) { const isAbortError = error instanceof Error && error.name === "AbortError"; + console.error("[ClawdChat API] OpenClaw request failed:", error); return NextResponse.json( { - error: isAbortError - ? "大龙虾聊天接口请求超时" - : "大龙虾聊天接口暂时不可用", + error: CLAW_UPGRADE_RETRY_MESSAGE, }, { status: isAbortError ? 504 : upstreamStatus } ); @@ -302,8 +290,9 @@ export async function POST(request: Request) { : ""; if (!answer) { + console.error("[ClawdChat API] OpenClaw empty answer:", upstreamPayload); return NextResponse.json( - { error: "大龙虾聊天接口未返回有效回答" }, + { error: CLAW_UPGRADE_RETRY_MESSAGE }, { status: 502 } ); } diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 32da74ee2..2dbae2d2d 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -317,6 +317,7 @@ interface CanvasChatPanelProps { type CanvasComposerMode = "agent" | "image" | "video" | "detail" | "clawd"; const CLAW_MODE_LABEL = "Claw"; +const CLAW_UPGRADE_RETRY_MESSAGE = "claw正在升级更新稍等5分钟再试"; function getCanvasComposerModeSessionLabel(mode: CanvasComposerMode) { if (mode === "clawd") return CLAW_MODE_LABEL; @@ -10081,7 +10082,11 @@ export function CanvasChatPanel({ }); const responseData = await response.json().catch(() => null); if (!response.ok) { - throw new Error(responseData?.error || `${CLAW_MODE_LABEL} 请求失败`); + console.error("[CanvasChat] Claw API returned error:", { + status: response.status, + responseData, + }); + throw new Error(CLAW_UPGRADE_RETRY_MESSAGE); } if (clawdSkillTagPrompt && clawdSkillTagForRequest) { @@ -10133,7 +10138,7 @@ export function CanvasChatPanel({ setMessages((previousMessages) => previousMessages.filter((message) => message.id !== userMessageId) ); - toast.error(error instanceof Error ? error.message : `${CLAW_MODE_LABEL} 请求失败`); + toast.error(CLAW_UPGRADE_RETRY_MESSAGE); setClawdTableFiles(tableFilesSnapshot); setAttachments(attachmentsSnapshot); if (clawdSkillTagForRequest && hasPendingClawdSkillTagPrompt) { @@ -11306,7 +11311,8 @@ export function CanvasChatPanel({ const composerPlaceholder = isWorkspacePresentation ? isClawdMode ? activeClawdSkillTag - ? `继续向 ${CLAW_MODE_LABEL} 提问,当前使用「${activeClawdSkillTag.name}」` + ? activeClawdSkillTag.placeholder?.trim() || + `继续向 ${CLAW_MODE_LABEL} 提问,当前使用「${activeClawdSkillTag.name}」` : `输入要交给 ${CLAW_MODE_LABEL} 的问题,可上传图片或表格` : isDetailGenerationMode ? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)" @@ -11319,7 +11325,8 @@ export function CanvasChatPanel({ : "可选补充本次生成要求,例如:更高级、更生活化、暖调灯光。" : isClawdMode ? activeClawdSkillTag - ? `继续向 ${CLAW_MODE_LABEL} 提问,当前使用「${activeClawdSkillTag.name}」` + ? activeClawdSkillTag.placeholder?.trim() || + `继续向 ${CLAW_MODE_LABEL} 提问,当前使用「${activeClawdSkillTag.name}」` : `输入要交给 ${CLAW_MODE_LABEL} 的问题,可上传图片或表格` : isDetailGenerationMode ? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)" diff --git a/components/canvas/CanvasFileListPanel.tsx b/components/canvas/CanvasFileListPanel.tsx index db12e488d..4b326ee11 100644 --- a/components/canvas/CanvasFileListPanel.tsx +++ b/components/canvas/CanvasFileListPanel.tsx @@ -4,13 +4,15 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { formatDistanceToNow } from "date-fns"; import { zhCN } from "date-fns/locale"; -import { Trash2, FileImage, Loader2, Plus, X } from "lucide-react"; +import { Check, Pencil, Trash2, FileImage, Loader2, Plus, X } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { canvasService } from "@/services/canvas"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import type { CanvasProjectListItem, CanvasWorkspaceType } from "@/types/canvas"; +import { useCanvasStore } from "@/store/canvas"; const PROJECT_LIST_PAGE_SIZE = 24; @@ -38,6 +40,10 @@ export function CanvasFileListPanel({ const [isLoadingMore, setIsLoadingMore] = useState(false); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(false); + const [editingProjectId, setEditingProjectId] = useState(null); + const [editingTitle, setEditingTitle] = useState(""); + const [renamingProjectId, setRenamingProjectId] = useState(null); + const setCurrentProjectTitle = useCanvasStore((state) => state.setProjectTitle); // 加载项目列表 const loadProjects = async (nextPage = 1, append = false) => { @@ -87,6 +93,7 @@ export function CanvasFileListPanel({ // 切换画布 const handleSwitch = (id: string) => { + if (editingProjectId === id) return; if (id === currentId) { onClose(); return; @@ -99,6 +106,73 @@ export function CanvasFileListPanel({ }, 100); }; + const handleStartRename = ( + e: React.MouseEvent, + project: CanvasProjectListItem + ) => { + e.stopPropagation(); + setEditingProjectId(project.id); + setEditingTitle(project.title || `未命名${workspaceLabel}`); + }; + + const handleCancelRename = (e?: React.MouseEvent) => { + e?.stopPropagation(); + setEditingProjectId(null); + setEditingTitle(""); + }; + + const handleRename = async (e?: React.MouseEvent | React.FormEvent) => { + e?.stopPropagation(); + e?.preventDefault(); + if (!editingProjectId || renamingProjectId) return; + + const nextTitle = editingTitle.trim(); + if (!nextTitle) { + toast.error(`${workspaceLabel}名称不能为空`); + return; + } + + const currentProject = projects.find((project) => project.id === editingProjectId); + if (currentProject && nextTitle === currentProject.title) { + handleCancelRename(); + return; + } + + setRenamingProjectId(editingProjectId); + const result = await canvasService.updateProject( + editingProjectId, + { title: nextTitle }, + workspaceType + ); + + if (result.success) { + const updatedAt = result.data?.updatedAt + ? new Date(result.data.updatedAt) + : new Date(); + setProjects((prev) => + prev + .map((project) => + project.id === editingProjectId + ? { ...project, title: nextTitle, updatedAt } + : project + ) + .sort( + (a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ) + ); + if (editingProjectId === currentId) { + setCurrentProjectTitle(nextTitle); + } + toast.success("重命名成功"); + setEditingProjectId(null); + setEditingTitle(""); + } else { + toast.error(result.error || "重命名失败"); + } + setRenamingProjectId(null); + }; + // 删除画布 const handleDelete = async (e: React.MouseEvent, id: string) => { e.stopPropagation(); @@ -204,34 +278,98 @@ export function CanvasFileListPanel({ {/* 信息 */}
-

- {project.title || `未命名${workspaceLabel}`} -

+ {editingProjectId === project.id ? ( +
event.stopPropagation()} + onSubmit={handleRename} + > + setEditingTitle(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Escape") { + event.preventDefault(); + handleCancelRename(); + } + }} + value={editingTitle} + /> + + +
+ ) : ( +

+ {project.title || `未命名${workspaceLabel}`} +

+ )}

- {formatDistanceToNow(new Date(project.updatedAt), { - addSuffix: true, - locale: zhCN + {formatDistanceToNow(new Date(project.updatedAt), { + addSuffix: true, + locale: zhCN, })}

- {/* 删除按钮 */} - + {/* 操作按钮 */} + {editingProjectId === project.id ? null : ( +
+ + +
+ )} ))} diff --git a/lib/canvas-skill-tags.ts b/lib/canvas-skill-tags.ts index cdd9b20a2..6a2a03810 100644 --- a/lib/canvas-skill-tags.ts +++ b/lib/canvas-skill-tags.ts @@ -1,10 +1,12 @@ export const CANVAS_SKILL_TAGS_SETTING_KEY = "canvas_skill_tags"; -export const DEFAULT_CANVAS_SKILL_TAGS_TITLE = "试试这些 Skills"; +export const CANVAS_SKILL_TAGS_CONFIG_VERSION = 4; +export const DEFAULT_CANVAS_SKILL_TAGS_TITLE = "Claw 快捷技能"; export type CanvasSkillTagSettings = { enabled: boolean; title: string; tags: CanvasSkillTag[]; + version?: number; }; export type CanvasSkillTag = { @@ -21,137 +23,153 @@ export const DEFAULT_CANVAS_SKILL_TAGS: CanvasSkillTag[] = [ id: "video-gateway", name: "视频理解", iconName: "clapperboard", - placeholder: "例如:分析这个短视频链接,总结分镜、口播脚本和可复用卖点", + placeholder: "粘贴视频链接:请总结这个视频的内容结构、分镜、口播脚本和可复用卖点", prompt: - "【技能路由:video-gateway】用途:视频链接/附件理解。请优先按视频理解任务处理,支持 mp4/mov/webm/m3u8 和抖音/小红书/B站等链接。主要触发关键词:视频、短视频、总结、分析、拆解、分镜、口播、脚本。", + "【技能路由:video-gateway】用途:理解视频链接、视频附件、短视频脚本和上一条视频上下文。触发关键词:视频、短视频、video、总结、分析、理解、拆解、提炼、复盘、脚本、分镜、口播、文案、复刻、卖点、爆点、标题、改写;支持 mp4/mov/webm/m3u8 和抖音/小红书/B站/快手/YouTube/腾讯视频等页面链接。请先确认是否拿到可访问的视频 URL 或可复用的视频上下文;有视频时输出核心结论、内容结构/镜头/脚本拆解、卖点爆点和可执行优化建议;没有视频 URL 且无上下文时,提示用户重新上传视频或提供公网视频链接。", }, { id: "image-search-api", name: "以图搜图", iconName: "image", - placeholder: "例如:用这张图片找市场同款,并识别是否为公司 SKU", + placeholder: "上传图片后输入:查市场ID/市场同款,返回相似商品、主图、链接和相似度", prompt: - "【技能路由:image-search-api】用途:以图搜图/公司 SKU 图片识别。请优先按图片搜索任务处理。主要触发关键词:以图搜图、图片搜索、找同款、相似款、市场同款、公司SKU、内部SKU、货号。需要图片 URL、附件或 base64。", + "【技能路由:image-search-api】用途:以图搜图,查市场相似款或公司内部 SKU。触发关键词:以图搜图、图搜图、图片搜索、图片找款、图片找货、相似款、同款、找同款、找相似、相似商品、市场同款、公司SKU、内部SKU、自有商品、货号、商品资料。必须先确认用户有图片 URL、图片附件或 base64;如果用户同时提到市场同款和公司 SKU,先让用户确认搜索口径。市场口径输出相似市场商品 ID、详情链接、主图、店铺、相似度;公司 SKU 口径输出 SKU/货号、商品名/规格、SPU、链接、相似度。没有图片时提示支持的图片输入方式,不要转成文本 SKU 查询。", }, { id: "product-asset-api", - name: "商品素材下载", + name: "商品素材", iconName: "shopping-bag", - placeholder: "例如:SKU: ABC123,下载主图、白底图、SKU 图和详情页素材", + placeholder: "输入 SKU: xxx,查详情页、主图、SKU图、白底图下载链接", prompt: - "【技能路由:product-asset-api】用途:商品详情页/主图/SKU图/白底图下载。请优先按商品素材查询下载任务处理。主要触发关键词:详情页、详情素材、任务详情、sku图、主图、白底图、白底。需要 SKU: 或 SPU:。", + "【技能路由:product-asset-api】用途:查询商品详情页、主图、SKU 图、白底图下载链接。触发关键词:详情页、详情页面、详情素材、详情整套、任务详情、SKU图、主图、白底图、白底、SKU:、SPU:、货号 + 详情/图片/白底需求。必须要求用户提供 SKU 或 SPU;提到白底/白底图时查白底图,其他详情/主图/SKU图查任务详情素材。输出下载链接,不默认展开图片预览;命中多组候选时默认展示最新/最相关 1 组,并提示可展开全部详情链接;未命中时说明 SKU/SPU 可能不正确或素材未入库。", }, { id: "install-media-db", - name: "安装资料查询", + name: "安装资料", iconName: "book-open", - placeholder: "例如:帮我查这个订单/货号的安装图纸、安装视频和说明书", + placeholder: "输入订单号或货号:查安装图纸、安装视频、说明书", prompt: - "【技能路由:install-media-db】用途:安装图纸/安装视频/说明书查询。请优先按安装资料查询任务处理。主要触发关键词:安装图纸、安装视频、安装说明书、安装资料、图纸地址、视频地址、订单号查安装。", + "【技能路由:install-media-db】用途:查询安装图纸、安装视频、安装说明书。触发关键词:安装图纸、安装视频、安装说明书、安装资料、安装资源、图纸地址、视频地址、订单号查图纸、订单号查安装。必须要求用户提供订单号或货号;提供订单号时先映射货号/SKU 再查安装资料,提供货号时直接查安装资料。输出结论、订单/货号映射、资料类型、打开链接、未命中原因;查询执行后附完整 Excel 表格下载链接。", }, { id: "product-region-logistics-db", - name: "仓配快递策略", + name: "仓配快递", iconName: "message-circle", - placeholder: "例如:货号 ABC123 发浙江省默认走什么快递、哪个仓库", + placeholder: "货号 ABC123 发浙江省杭州市,默认走什么仓库和快递", prompt: - "【技能路由:product-region-logistics-db】用途:货号 + 区域仓配/快递策略。请优先按仓配物流策略查询任务处理。主要触发关键词:发什么快递、什么快递、物流策略、仓库策略、仓配策略、默认快递、默认仓库。需要货号和省份。", + "【技能路由:product-region-logistics-db】用途:按货号和地区查询仓配/快递策略。触发关键词:发什么快递、什么快递、快递策略、物流策略、仓库策略、仓配策略、发哪个仓、哪个仓发、默认快递、默认仓库;需要同时包含货号/商品/产品/SKU/SPU 和省份/地区。必须要求用户提供货号和省份,城市/区县可选。输出推荐快递/仓库、查询口径、命中订单数、发货件数、快递 TOP、仓库 TOP、仓配组合;样本不足时提醒放宽地区或延长周期;查询执行后附完整 Excel 表格下载链接。", }, { id: "product-retention-db", - name: "退补留客决策", + name: "退补留客", iconName: "badge-percent", - placeholder: "例如:这个售后场景应该退货还是补偿,补偿区间多少", + placeholder: "货号 ABC123,广东省,这个售后场景退货还是补偿,补偿区间多少", prompt: - "【技能路由:product-retention-db】用途:退货/补偿/留客决策。请优先按退补留客决策任务处理。主要触发关键词:退货还是补偿、退回还是补偿、补偿区间、优惠留客、留客、挽留、放货、退补决策。", + "【技能路由:product-retention-db】用途:售后留客,判断退货、补偿、放货等方案。触发关键词:退货还是补偿、退回还是补偿、补偿区间、优惠留客、留客、挽留、放货、退补决策、退货决策。必须要求用户提供货号和地区。输出推荐方案、置信度、样本订单、退货件数、单均利润、退回预期损失、补偿区间、方案对比;样本不足时不要强行下结论;查询执行后附完整 Excel 表格下载链接。", }, { id: "company-aftersale-db", - name: "售后退款分析", + name: "售后退款", iconName: "message-circle", - placeholder: "例如:汇总 6 月售后分析,拆解退款原因和风险类型", + placeholder: "分析最近30天售后退款原因,按平台/店铺/商品ID输出风险和动作", prompt: - "【技能路由:company-aftersale-db】用途:公司售后/退款原因分析。请优先按售后退款原因分析任务处理。主要触发关键词:售后分析、退款原因、售后原因、退货退款风险、售后类型、售后结果。", + "【技能路由:company-aftersale-db】用途:公司售后/退款原因分析。触发关键词:售后分析、退款原因、售后原因、退货退款风险、售后类型、售后结果。只分析公司售后/退款只读数据,默认口径最近 30 天,排除配件单和特殊单。输出退款原因 TOP、平台差异、二级原因、店铺风险、类型结果矩阵、建议动作。如果用户提到 PDD/京东/天猫商品 ID 健康、运营健康、运营业绩,应让位给对应技能;查询执行后附完整 Excel 表格下载链接。", }, { id: "business-db", - name: "销售业绩汇总", + name: "整体业绩", iconName: "badge-percent", - placeholder: "例如:汇总 2026 年 6 月全平台整体销售业绩和 GMV", + placeholder: "查 2026年6月 全平台整体销售业绩、GMV、退款和平台结构", prompt: - "【技能路由:business-db】用途:整体销售业绩汇总。请优先按销售业绩汇总任务处理。触发条件:销售/业绩/GMV/成交/营业额 + 总/整体/全平台/汇总 + 日期。", + "【技能路由:business-db】用途:整体销售业绩汇总。触发条件:销售/业绩/GMV/成交/营业额 + 总/整体/全平台/汇总/多少/查一下/看一下 + 明确日期,例如 2026-06-01、6月1日、昨天、今天。按用户指定日期查询整体销售业绩。输出发货件数、订单数、付款金额、退款金额、平台结构、店铺结构、同步状态;指定日期无数据时回落到最近可用日期并明确说明;查询执行后附完整 Excel 表格下载链接。", }, { id: "market-db", name: "市场大盘分析", iconName: "layout-grid", - placeholder: "例如:分析书桌类目 2026 年 6 月市场大盘、价格带和品牌榜", + placeholder: "分析鞋柜 2026年6月市场大盘、趋势、价格带、品牌/店铺和热搜词", prompt: - "【技能路由:market-db】用途:市场大盘分析。请优先按市场大盘/行业类目分析任务处理。主要触发关键词:市场分析、市场大盘、行业分析、类目分析、市场趋势、价格带、品牌榜、店铺榜、热搜词等 + 类目词。", + "【技能路由:market-db】用途:市场大盘/行业/类目分析。触发关键词:市场分析、市场大盘、行业分析、类目分析、品类分析、市场趋势、品牌榜、店铺榜、热搜词、上升词、词根、价格带;必须包含具体类目/关键词,如电动升降桌、书架、鞋柜。只使用市场数据库做分析,输出市场规模、趋势、竞争环境、价格带、品牌/店铺、关键词机会、建议动作。不要处理商品详情页、SKU 图片、图片搜索、以图搜图、同款/相似款;这些交给市场 ID 详情或图片搜索技能。查询执行后附完整 Excel 表格下载链接。", }, { id: "market-insight-db", name: "市场洞察表", iconName: "layout-grid", - placeholder: "例如:生成鞋柜市场洞察表,并支持完整 Excel 导出", + placeholder: "做鞋柜市场洞察表,先选渠道,后续支持确认导出完整表", prompt: - "【技能路由:market-insight-db】用途:市场洞察表/完整 Excel 导出。请优先按市场洞察表任务处理。主要触发关键词:市场洞察、洞察表、洞察报告、洞察分析;后续支持按推荐继续、看SKU、看关键词、确认导出完整表。", + "【技能路由:market-insight-db】用途:市场洞察表,支持交互式第一关/第二关和完整 Excel 导出。触发关键词:市场洞察、洞察表、洞察报告、洞察分析、市场洞察表;后续追问支持淘宝、天猫、抖音、1688、京东、全渠道、看 SKU、看关键词、看价格带、看 TOP 商品、按推荐继续、确认导出完整表。先确认三级品类和渠道;第一关输出结论可靠性、大盘趋势、价格带机会、品牌/店铺竞争、代表商品 TOP、SKU 摘要、关键词机会、推荐下一步;用户回复确认导出完整表时生成完整 Excel 并返回下载链接。", + }, + { + id: "rank-board-opportunity-db", + name: "榜单机会", + iconName: "sparkles", + placeholder: "电动升降桌榜单机会评分,判断蓝海/红海、切入组合和竞争格局", + prompt: + "【技能路由:rank-board-opportunity-db】用途:榜单机会评分、红海判断、竞争格局和新品切入机会。触发关键词:榜单机会、机会评分、蓝海、红海、切入机会、优先切入、差异化切入、竞争格局、竞争环境、头部垄断、拥挤度、市场拥挤、CR3、CR5、HHI、应用场景+款式+价格带。先用榜单表确定商品池并按商品 ID 去重,基于渠道、价格带、款式、材质、店铺类型、应用场景等维度计算机会分和红海分。输出最值得切入组合、最红海组合、整体竞争格局、总览指标、优先切入/差异化切入/高风险红海组合,并附完整 Excel 表格下载链接。", }, { id: "rank-board-db", - name: "市场榜单分析", + name: "榜单数据分析", iconName: "layout-grid", - placeholder: "例如:分析目标商品 ID 的市场榜单结构、价格带和材质分布", + placeholder: "电动升降桌榜单数据分析,按同价带同品类看价格带、材质、店铺、商品ID明细和主图", prompt: - "【技能路由:rank-board-db】用途:市场榜单/商品ID榜单分析。请优先按榜单分析任务处理。主要触发关键词:榜单、排行榜、渠道榜、目标ID、市场ID、商品ID计算、ID明细、价格带、材质、店铺、榜单结构。", + "【技能路由:rank-board-db】用途:榜单数据分析、市场榜单快照分析、商品 ID 榜单分类结构与明细下钻。触发关键词:榜单数据分析、榜单分析、市场 ID 榜单、市场榜单、排名榜、排行榜、渠道榜、目标 ID、市场 ID、商品 ID 计算、ID 明细、ID 数据、分类数据、价格带、同价带、同品类、款式、材质、店铺、店铺类型、应用场景、竞争环境、榜单结构、主图、详情链接、SKU 图、详情图。分析企划库最新榜单快照,先读 planning_rank_board_aggregate_snapshot_di,再下钻 planning_rank_board_dimension_snapshot_di 和 planning_rank_board_item_snapshot_di;不要混用公司运营权限数据。商品 ID 明细必须限定在同价格带、同品类内,默认按销售额降序、买家估算降序、最佳排名升序排序;如果用户指定平台,要按淘宝/天猫/京东/1688/抖音过滤。输出命中商品 ID 数、销售额、平台/主题/价格带口径、品牌竞争、价格带/材质/款式/应用场景/店铺结构、商品 ID TOP、主图或详情链接、样本提示、建议动作;涉及“我的/我负责/运营/部门/名下”时需要校验身份权限;查询执行后附完整 Excel 表格下载链接。", }, { id: "market-id-detail-db", name: "市场ID详情", iconName: "shopping-bag", - placeholder: "例如:补全这些市场商品 ID 的主图、SKU 图、详情页、标题和价格", + placeholder: "市场ID 944734849380,补全标题、店铺、价格、主图、SKU图、详情页链接", prompt: - "【技能路由:market-id-detail-db】用途:市场商品 ID 详情补全。请优先按市场商品 ID 详情任务处理。主要触发关键词:市场ID详情、市场商品ID、市场商品详情、商品ID + 主图/sku图/详情页/标题/价格/链接。", + "【技能路由:market-id-detail-db】用途:市场商品 ID 详情补全。触发关键词:市场 ID 详情、市场商品 ID、市场商品详情、商品 ID + 主图/商品图片/SKU 图/规格图/详情图/详情页/标题/店铺/价格/链接/URL,以及 8-20 位数字商品 ID 并带市场/详情/图片等语义。必须要求用户提供市场商品 ID。输出商品标题、店铺、价格、详情链接、主图、SKU 图、详情图等,按用户要求展示;即使 0 命中,也说明已查询但未命中;查询执行后附完整 Excel 表格下载链接,0 行结果也给说明表。", }, { id: "pdd-goods-db", - name: "拼多多经营分析", + name: "PDD经营", iconName: "shopping-bag", - placeholder: "例如:分析这个拼多多商品 ID 的经营表现、排行、转化和价格", + placeholder: "拼多多 962162611585 最近7天经营分析:成交、访客、转化、推广、SKU", prompt: - "【技能路由:pdd-goods-db】用途:拼多多商品经营分析。请优先按拼多多商品经营分析任务处理。主要触发关键词:拼多多、PDD、datagoods + 商品ID或经营/表现/数据/排行/转化/推广/价格/SKU 等意图。", + "【技能路由:pdd-goods-db】用途:拼多多商品经营分析。触发关键词:拼多多、PDD、datagoods + 商品 ID,或经营、表现、数据、排行、排名、转化、推广、曝光、价格、SKU、关键词、策略等意图。分析 PDD 商品经营、流量、转化、推广、价格和 SKU 结构。输出成交金额、访客转化、推广消耗、商品/类目/关键词表现、问题诊断、优化动作;如果用户要市场大盘而不是 PDD 经营,交给市场分析;查询执行后附完整 Excel 表格下载链接。", }, { id: "channel-id-health-db", - name: "渠道ID健康诊断", + name: "渠道ID诊断", iconName: "badge-percent", - placeholder: "例如:诊断这个京东/天猫商品 ID 的费比、SKU 利润和优化动作", + placeholder: "京东/天猫 商品ID 12345678 最近7天健康诊断,费比、转化、SKU利润", prompt: - "【技能路由:channel-id-health-db】用途:京东/天猫商品ID健康诊断。请优先按渠道商品 ID 健康诊断任务处理。主要触发关键词:京东/JD/天猫/tmall + 健康诊断、ID健康、商品经营、费比、SKU利润、放大、控投、优化。", + "【技能路由:channel-id-health-db】用途:京东/天猫商品 ID 健康诊断。触发条件:京东/JD 或 天猫/tmall + 健康诊断、ID健康、商品ID健康、商品经营、商品表现、目标达成、费比、SKU利润、利润梯队、放大、控投、优化,或直接包含 8 位以上商品 ID。必须识别渠道:京东或天猫;涉及团队/名下/运营范围时必须校验身份权限。输出平台成交、访客、转化、DWS 真实发货业绩、真实费比、商品 ID 明细、SKU 利润梯队、建议动作;权限缺失时只返回权限提示,不返回表格;授权查询执行后附完整 Excel 表格下载链接。", }, { id: "operator-health-db", name: "运营健康诊断", iconName: "badge-percent", - placeholder: "例如:给我的团队做运营健康诊断,包含业绩、费比、SKU 和优化建议", + placeholder: "我的团队近7日运营健康诊断,哪些ID要放大、控投、优化", prompt: - "【技能路由:operator-health-db】用途:运营健康诊断。请优先按运营健康诊断任务处理。主要触发关键词:运营健康、健康诊断、健康分析、放大、控投、优化 + 我的/团队/运营/商品ID/SKU/费比/业绩。", + "【技能路由:operator-health-db】用途:运营健康诊断。触发关键词:运营健康、健康诊断、健康分析、放大、控投、优化、我的、我的团队、团队、运营、商品 ID、SKU、费比、业绩、最近 7 天、近 30 天、本月、上月等周期。必须校验用户身份和权限;支持本人运营健康、我的团队运营健康、指定团队运营健康。输出真实发货业绩、真实费比、售后风险、商品 ID/SKU 问题、推广诊断摘要、放大/控投建议、优先级动作;推广诊断摘要必须包含推广消耗、ROAS、CTR、CVR、推广优先处理 ID;权限缺失时只返回权限提示,不返回表格;授权查询执行后附完整 Excel 表格下载链接。", + }, + { + id: "promotion-diagnosis-db", + name: "推广诊断", + iconName: "megaphone", + placeholder: "我的拼多多近7日推广诊断,输出消耗、ROAS、CTR、CVR和优先处理ID", + prompt: + "【技能路由:promotion-diagnosis-db】用途:推广诊断、推广投产诊断、推广 ROI/ROAS 诊断。触发关键词:推广诊断、推广投产、投产诊断、推广 ROI、ROI诊断、ROAS、推广消耗、点击率、CTR、转化率、CVR、推广优先处理 ID、我的拼多多近7日推广诊断、我的天猫近30日推广投产诊断、我的推广ROI诊断。支持拼多多、天猫等平台和近 7 日、近 30 日、本月、上月等周期;必须按第三方传入的 user_name 校验运营本人/负责人权限,不放宽到全公司数据;权限缺失时只返回权限提示,不返回表格。输出推广消耗、ROAS/ROI、CTR、CVR、异常商品/计划/关键词、推广优先处理 ID、止损/放大/优化动作;授权查询执行后附完整 Excel 表格下载链接。", }, { id: "business-operator-dimension-db", - name: "运营维度排行", + name: "运营排行", iconName: "layout-grid", - placeholder: "例如:按运营维度汇总 6 月销售数据,输出排行和差异", + placeholder: "我的团队 2026年6月 按运营维度销售排行和费比", prompt: - "【技能路由:business-operator-dimension-db】用途:团队/运营维度销售排行。请优先按运营维度销售排行任务处理。主要触发关键词:运营维度、按运营、各运营、每个运营、运营排行、运营排名、运营汇总、运营销售数据。", + "【技能路由:business-operator-dimension-db】用途:团队/运营维度销售排行。触发关键词:运营维度、按运营、各运营、每个运营、所有运营、运营排行、运营排名、运营汇总、运营销售排行、运营销售排名、运营销售数据、运营业绩排行、运营业绩排名。必须校验用户身份和团队权限;支持“我的团队运营维度销售排行”或“某负责人团队运营销售排行”。输出团队总览、运营 TOP、目标业绩、达成率、目标费比、真实费比、商品 ID 矩阵、SKU 利润梯队、趋势图建议;权限缺失时只返回权限提示,不返回表格;授权查询执行后附完整 Excel 表格下载链接。", }, { id: "business-operator-db", - name: "单运营业绩费比", + name: "运营业绩", iconName: "badge-percent", - placeholder: "例如:张三 2026 年 6 月业绩和费比情况", + placeholder: "张三 2026年6月业绩和费比情况", prompt: - "【技能路由:business-operator-db】用途:单个运营业绩与费比。请优先按单个运营业绩费比任务处理。触发条件:人名 + 业绩/费比/销售/成交/GMV + 月份,例如“张三2026年6月业绩”。", + "【技能路由:business-operator-db】用途:单个运营业绩与费比分析。触发条件:人名 + 业绩/费比/销售/成交/营业额/GMV,并且必须包含月份:本月、上月、2026年6月、6月等,例如“张三2026年6月业绩”“子瑜本月费比”。必须校验用户身份和权限,只允许用户查询本人或自己有管理权限的运营。输出真实发货业绩、发货件数、真实费比、目标达成、同比/环比、店铺贡献、费用拆解、商品 ID 矩阵、SKU 利润梯队、每日趋势、建议动作;权限缺失时只返回权限提示,不返回表格;授权查询执行后附完整 Excel 表格下载链接。", }, ]; @@ -204,24 +222,56 @@ export function normalizeCanvasSkillTags(value: unknown): CanvasSkillTag[] { return normalized.slice(0, 30); } +function mergeCanvasSkillTagsWithDefaultUpdates(value: unknown): CanvasSkillTag[] { + const currentTags = normalizeCanvasSkillTags(value); + const currentById = new Map(currentTags.map((tag) => [tag.id, tag])); + const defaultIds = new Set(DEFAULT_CANVAS_SKILL_TAGS.map((tag) => tag.id)); + const merged: CanvasSkillTag[] = DEFAULT_CANVAS_SKILL_TAGS.map((defaultTag) => { + const currentTag = currentById.get(defaultTag.id); + return currentTag?.iconUrl ? { ...defaultTag, iconUrl: currentTag.iconUrl } : defaultTag; + }); + + currentTags.forEach((tag) => { + if (!defaultIds.has(tag.id)) { + merged.push(tag); + } + }); + + return normalizeCanvasSkillTags(merged); +} + export function normalizeCanvasSkillTagSettings( - value: unknown + value: unknown, + options: { requireCurrentVersion?: boolean } = {} ): CanvasSkillTagSettings { if (Array.isArray(value)) { return { enabled: true, title: DEFAULT_CANVAS_SKILL_TAGS_TITLE, tags: normalizeCanvasSkillTags(value), + version: CANVAS_SKILL_TAGS_CONFIG_VERSION, }; } if (value && typeof value === "object") { const row = value as Record; + const version = Number(row.version || 0); + if (options.requireCurrentVersion && version < CANVAS_SKILL_TAGS_CONFIG_VERSION) { + const title = normalizeString(row.title) || DEFAULT_CANVAS_SKILL_TAGS_TITLE; + return { + enabled: row.enabled !== false, + title, + tags: mergeCanvasSkillTagsWithDefaultUpdates(row.tags), + version: CANVAS_SKILL_TAGS_CONFIG_VERSION, + }; + } + const title = normalizeString(row.title) || DEFAULT_CANVAS_SKILL_TAGS_TITLE; return { enabled: row.enabled !== false, title, tags: normalizeCanvasSkillTags(row.tags), + version: CANVAS_SKILL_TAGS_CONFIG_VERSION, }; } @@ -229,5 +279,6 @@ export function normalizeCanvasSkillTagSettings( enabled: true, title: DEFAULT_CANVAS_SKILL_TAGS_TITLE, tags: normalizeCanvasSkillTags(null), + version: CANVAS_SKILL_TAGS_CONFIG_VERSION, }; }