feat: update canvas skills and project list rename

This commit is contained in:
134 2026-06-29 15:59:01 +08:00
parent 16a6134c77
commit 12c93e1154
5 changed files with 296 additions and 103 deletions

View File

@ -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()

View File

@ -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<string, unknown>;
const nestedError =
raw.error && typeof raw.error === "object"
? (raw.error as Record<string, unknown>)
: 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<string, unknown>;
@ -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 }
);
}

View File

@ -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张)"

View File

@ -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<string | null>(null);
const [editingTitle, setEditingTitle] = useState("");
const [renamingProjectId, setRenamingProjectId] = useState<string | null>(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({
{/* 信息 */}
<div className="flex-1 min-w-0">
<h4 className={cn(
"font-medium text-sm truncate",
project.id === currentId && "text-primary"
)}>
{project.title || `未命名${workspaceLabel}`}
</h4>
{editingProjectId === project.id ? (
<form
className="flex min-w-0 items-center gap-2"
onClick={(event) => event.stopPropagation()}
onSubmit={handleRename}
>
<Input
autoFocus
className="h-8 min-w-0 rounded-md text-sm"
disabled={renamingProjectId === project.id}
maxLength={80}
onChange={(event) => setEditingTitle(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Escape") {
event.preventDefault();
handleCancelRename();
}
}}
value={editingTitle}
/>
<Button
className="h-8 w-8 shrink-0"
disabled={renamingProjectId === project.id}
size="icon"
type="submit"
variant="ghost"
>
{renamingProjectId === project.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Check className="h-4 w-4" />
)}
</Button>
<Button
className="h-8 w-8 shrink-0"
disabled={renamingProjectId === project.id}
onClick={handleCancelRename}
size="icon"
type="button"
variant="ghost"
>
<X className="h-4 w-4" />
</Button>
</form>
) : (
<h4 className={cn(
"font-medium text-sm truncate",
project.id === currentId && "text-primary"
)}>
{project.title || `未命名${workspaceLabel}`}
</h4>
)}
<p className="text-xs text-muted-foreground mt-0.5">
{formatDistanceToNow(new Date(project.updatedAt), {
addSuffix: true,
locale: zhCN
{formatDistanceToNow(new Date(project.updatedAt), {
addSuffix: true,
locale: zhCN,
})}
</p>
</div>
{/* 删除按钮 */}
<Button
variant="ghost"
size="icon"
className={cn(
"h-8 w-8 text-muted-foreground hover:text-destructive transition-opacity",
isVideoWorkspace
? "opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
: "opacity-0 group-hover:opacity-100"
)}
onClick={(e) => handleDelete(e, project.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
{/* 操作按钮 */}
{editingProjectId === project.id ? null : (
<div
className={cn(
"flex shrink-0 items-center gap-1 transition-opacity",
isVideoWorkspace
? "opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
: "opacity-0 group-hover:opacity-100"
)}
>
<Button
aria-label={`重命名${project.title || `未命名${workspaceLabel}`}`}
className="h-8 w-8 text-muted-foreground hover:text-foreground"
onClick={(event) => handleStartRename(event, project)}
size="icon"
title="重命名"
variant="ghost"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
aria-label={`删除${project.title || `未命名${workspaceLabel}`}`}
className="h-8 w-8 text-muted-foreground hover:text-destructive"
onClick={(e) => handleDelete(e, project.id)}
size="icon"
title="删除"
variant="ghost"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
))}

View File

@ -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<string, unknown>;
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,
};
}