Compare commits

..

No commits in common. "dda131c5ea339a7f17caab4ac5a7decceb166336" and "10cf4f0b00fec1e0b6ddd161f86afd0ad8008045" have entirely different histories.

26 changed files with 321 additions and 1963 deletions

View File

@ -132,7 +132,6 @@ stop:
@-pkill -f "next start" 2>/dev/null || true @-pkill -f "next start" 2>/dev/null || true
@-pkill -f "next-server" 2>/dev/null || true @-pkill -f "next-server" 2>/dev/null || true
@-pkill -f "next-server" 2>/dev/null || true @-pkill -f "next-server" 2>/dev/null || true
@-pkill -f "frontend/.next/standalone/server.js" 2>/dev/null || true
@-nginx -c $(PWD)/docker/nginx/nginx.local.conf -p $(PWD) -s quit 2>/dev/null || true @-nginx -c $(PWD)/docker/nginx/nginx.local.conf -p $(PWD) -s quit 2>/dev/null || true
@sleep 1 @sleep 1
@-pkill -9 nginx 2>/dev/null || true @-pkill -9 nginx 2>/dev/null || true

View File

@ -75,8 +75,6 @@
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"next": "^16.1.7", "next": "^16.1.7",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nextra": "^4.6.1",
"nextra-theme-docs": "^4.6.1",
"nuxt-og-image": "^5.1.13", "nuxt-og-image": "^5.1.13",
"ogl": "^1.0.11", "ogl": "^1.0.11",
"react": "^19.0.0", "react": "^19.0.0",

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
import { importPage } from "nextra/pages"; import { generateStaticParamsFor, importPage } from "nextra/pages";
import { useMDXComponents as getMDXComponents } from "../../../../mdx-components"; import { useMDXComponents as getMDXComponents } from "../../../../mdx-components";
export const generateStaticParams = generateStaticParamsFor("mdxPath");
export async function generateMetadata(props) { export async function generateMetadata(props) {
const params = await props.params; const params = await props.params;
const { metadata } = await importPage(params.mdxPath, params.lang); const { metadata } = await importPage(params.mdxPath, params.lang);

View File

@ -3,6 +3,7 @@ import { getPageMap } from "nextra/page-map";
import { Footer, Layout } from "nextra-theme-docs"; import { Footer, Layout } from "nextra-theme-docs";
import { Header } from "@/components/landing/header"; import { Header } from "@/components/landing/header";
import { getLocaleByLang } from "@/core/i18n/locale";
import "nextra-theme-docs/style.css"; import "nextra-theme-docs/style.css";
const footer = <Footer>MIT {new Date().getFullYear()} © Nextra.</Footer>; const footer = <Footer>MIT {new Date().getFullYear()} © Nextra.</Footer>;
@ -26,11 +27,18 @@ function formatPageRoute(base: string, items: PageMapItem[]): PageMapItem[] {
export default async function DocLayout({ children, params }) { export default async function DocLayout({ children, params }) {
const { lang } = await params; const { lang } = await params;
const locale = getLocaleByLang(lang);
const pages = await getPageMap(`/${lang}`); const pages = await getPageMap(`/${lang}`);
return ( return (
<Layout <Layout
navbar={<Header />} navbar={
<Header
className="relative max-w-full px-10"
homeURL="/"
locale={locale}
/>
}
pageMap={formatPageRoute(`/${lang}/docs`, pages)} pageMap={formatPageRoute(`/${lang}/docs`, pages)}
docsRepositoryBase="https://github.com/bytedance/deerflow/tree/main/frontend/src/app/content" docsRepositoryBase="https://github.com/bytedance/deerflow/tree/main/frontend/src/app/content"
footer={footer} footer={footer}

View File

@ -2,7 +2,7 @@
import { BotIcon, PlusSquare } from "lucide-react"; import { BotIcon, PlusSquare } from "lucide-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useCallback } from "react"; import { useCallback, useState } from "react";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input"; import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -11,7 +11,11 @@ import { ArtifactTrigger } from "@/components/workspace/artifacts";
import { ChatBox, useThreadChat } from "@/components/workspace/chats"; import { ChatBox, useThreadChat } from "@/components/workspace/chats";
import { ExportTrigger } from "@/components/workspace/export-trigger"; import { ExportTrigger } from "@/components/workspace/export-trigger";
import { InputBox } from "@/components/workspace/input-box"; import { InputBox } from "@/components/workspace/input-box";
import { MessageList } from "@/components/workspace/messages"; import {
MessageList,
MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM,
} from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context"; import { ThreadContext } from "@/components/workspace/messages/context";
import { ThreadTitle } from "@/components/workspace/thread-title"; import { ThreadTitle } from "@/components/workspace/thread-title";
import { TodoList } from "@/components/workspace/todo-list"; import { TodoList } from "@/components/workspace/todo-list";
@ -20,17 +24,15 @@ import { Tooltip } from "@/components/workspace/tooltip";
import { useAgent } from "@/core/agents"; import { useAgent } from "@/core/agents";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { useNotification } from "@/core/notification/hooks"; import { useNotification } from "@/core/notification/hooks";
import { useLocalSettings } from "@/core/settings"; import { useThreadSettings } from "@/core/settings";
import { useThreadStream } from "@/core/threads/hooks"; import { useThreadStream } from "@/core/threads/hooks";
import { textOfMessage } from "@/core/threads/utils"; import { textOfMessage } from "@/core/threads/utils";
import { env } from "@/env"; import { env } from "@/env";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const MESSAGE_LIST_DEFAULT_PADDING_BOTTOM = 160;
const MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM = 120;
export default function AgentChatPage() { export default function AgentChatPage() {
const { t } = useI18n(); const { t } = useI18n();
const [showFollowups, setShowFollowups] = useState(false);
const router = useRouter(); const router = useRouter();
const { agent_name } = useParams<{ const { agent_name } = useParams<{
@ -40,7 +42,7 @@ export default function AgentChatPage() {
const { agent } = useAgent(agent_name); const { agent } = useAgent(agent_name);
const { threadId, isNewThread, setIsNewThread } = useThreadChat(); const { threadId, isNewThread, setIsNewThread } = useThreadChat();
const [settings, setSettings] = useLocalSettings(); const [settings, setSettings] = useThreadSettings(threadId);
const { showNotification } = useNotification(); const { showNotification } = useNotification();
const [thread, sendMessage] = useThreadStream({ const [thread, sendMessage] = useThreadStream({
@ -84,12 +86,13 @@ export default function AgentChatPage() {
await thread.stop(); await thread.stop();
}, [thread]); }, [thread]);
const messageListPaddingBottom = const messageListPaddingBottom = showFollowups
MESSAGE_LIST_DEFAULT_PADDING_BOTTOM + ? MESSAGE_LIST_DEFAULT_PADDING_BOTTOM +
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM; MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM
: undefined;
return ( return (
<ThreadContext.Provider value={{ thread, threadId }}> <ThreadContext.Provider value={{ thread }}>
<ChatBox threadId={threadId}> <ChatBox threadId={threadId}>
<div className="relative flex size-full min-h-0 justify-between"> <div className="relative flex size-full min-h-0 justify-between">
<header <header
@ -163,10 +166,9 @@ export default function AgentChatPage() {
<InputBox <InputBox
className={cn("bg-background/5 w-full -translate-y-4")} className={cn("bg-background/5 w-full -translate-y-4")}
isNewThread={isNewThread}
threadId={threadId} threadId={threadId}
autoFocus={isNewThread} autoFocus={isNewThread}
showWelcomeStyle={isNewThread}
hasSubmitted={!isNewThread}
status={ status={
thread.error thread.error
? "error" ? "error"
@ -182,6 +184,7 @@ export default function AgentChatPage() {
} }
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"} disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
onContextChange={(context) => setSettings("context", context)} onContextChange={(context) => setSettings("context", context)}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onStop={handleStop} onStop={handleStop}
/> />

View File

@ -186,7 +186,7 @@ export default function NewAgentPage() {
// ── Step 2: chat ─────────────────────────────────────────────────────────── // ── Step 2: chat ───────────────────────────────────────────────────────────
return ( return (
<ThreadContext.Provider value={{ thread, threadId }}> <ThreadContext.Provider value={{ thread }}>
<ArtifactsProvider> <ArtifactsProvider>
<div className="flex size-full flex-col"> <div className="flex size-full flex-col">
{header} {header}

View File

@ -67,19 +67,10 @@ export default function ChatPage() {
// 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/is_chatting 参数。 // 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/is_chatting 参数。
const shouldRenderHistory = !showWelcomeStyle; const shouldRenderHistory = !showWelcomeStyle;
const createNewSession = useMemo(() => isNewThread, [isNewThread]); const createNewSession = useMemo(() => isNewThread, [isNewThread]);
const safeThreadId = useMemo(() => {
if (!threadId || threadId === "new") {
return undefined;
}
return threadId;
}, [threadId]);
const streamThreadId = useMemo(() => { const streamThreadId = useMemo(() => {
if (isNewThread && createNewSession) { return isNewThread && createNewSession ? undefined : threadId;
return undefined; }, [createNewSession, isNewThread, threadId]);
}
return safeThreadId;
}, [createNewSession, isNewThread, safeThreadId]);
const { showNotification } = useNotification(); const { showNotification } = useNotification();
@ -88,7 +79,7 @@ export default function ChatPage() {
skillError: selectedSkillError, skillError: selectedSkillError,
clearSkillError: clearSelectedSkillError, clearSkillError: clearSelectedSkillError,
isBootstrapping: isSelectedSkillBootstrapping, isBootstrapping: isSelectedSkillBootstrapping,
} = useSelectedSkillListener({ threadId: safeThreadId ?? null }); } = useSelectedSkillListener({ threadId });
// 对话行为控制器 // 对话行为控制器
const [thread, sendMessage, isUploading] = useThreadStream({ const [thread, sendMessage, isUploading] = useThreadStream({
threadId: streamThreadId, threadId: streamThreadId,
@ -100,7 +91,7 @@ export default function ChatPage() {
setIsNewThread(false); setIsNewThread(false);
// if (!shouldStayOnNewRoute) { // if (!shouldStayOnNewRoute) {
// Keep /new in history so router.back() can return to it. // Keep /new in history so router.back() can return to it.
router.replace(`/workspace/chats/${currentThreadId}?is_chatting=true`); router.replace(`/workspace/chats/${currentThreadId}`);
// } // }
// history.pushState(null, "", pathOfThread(currentThreadId)); // history.pushState(null, "", pathOfThread(currentThreadId));
}, },
@ -504,7 +495,7 @@ export default function ChatPage() {
if (threadId && threadId !== "new") { if (threadId && threadId !== "new") {
nextQuery.set("thread_id", threadId); nextQuery.set("thread_id", threadId);
} }
router.replace(`/workspace/chats/${threadId}?is_chatting=false`); router.replace(`/workspace/chats/new?${nextQuery.toString()}`);
}} }}
> >

View File

@ -133,7 +133,7 @@ const ChatBox: React.FC<{
artifactPanelOpen ? "translate-x-0" : "translate-x-full", artifactPanelOpen ? "translate-x-0" : "translate-x-full",
)} )}
> >
{selectedArtifact && threadId ? ( {selectedArtifact ? (
<ArtifactFileDetail <ArtifactFileDetail
// className="size-full" // className="size-full"
filepath={selectedArtifact} filepath={selectedArtifact}
@ -167,7 +167,7 @@ const ChatBox: React.FC<{
<ArtifactFileList <ArtifactFileList
className="max-w-(--container-width-sm) p-4 pt-12" className="max-w-(--container-width-sm) p-4 pt-12"
files={thread.values.artifacts ?? []} files={thread.values.artifacts ?? []}
threadId={threadId ?? ""} threadId={threadId}
/> />
</main> </main>
</div> </div>

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { useParams, usePathname, useSearchParams } from "next/navigation"; import { useParams, usePathname, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
export function useThreadChat() { export function useThreadChat() {
@ -26,13 +26,53 @@ export function useThreadChat() {
const rawPathThreadId = params?.thread_id ?? threadIdFromPathname; const rawPathThreadId = params?.thread_id ?? threadIdFromPathname;
const isNewRoute = rawPathThreadId === "new"; const isNewRoute = rawPathThreadId === "new";
const threadIdFromPathOrParams = isNewRoute const threadIdFromPathOrParams:string = isNewRoute
? normalizeThreadId(threadIdFromSearchParams) ? threadIdFromSearchParams?? params.thread_id
: normalizeThreadId(rawPathThreadId); : params.thread_id;
// console.log("[useThreadChat] pathname", pathname); // console.log("[useThreadChat] pathname", pathname);
// console.log("[useThreadChat] params.thread_id", params?.thread_id); // console.log("[useThreadChat] params.thread_id", params?.thread_id);
// console.log("[useThreadChat] threadIdFromPathname", threadIdFromPathname); // console.log("[useThreadChat] threadIdFromPathname", threadIdFromPathname);
// console.log("[useThreadChat] threadIdFromPath", threadIdFromPath); // console.log("[useThreadChat] threadIdFromPath", threadIdFromPath);
// 持久化兜底:用于处理首屏水合或 params 时序问题。
const readStoredThreadId = () => {
if (typeof window === "undefined") {
return undefined;
}
const stored = window.sessionStorage.getItem("workspace.thread_id");
return isValidThreadId(stored) ? stored : undefined;
};
// 读取 query 的 thread_id先用 hook必要时用 window 兜底)。
const readQueryThreadId = () => {
const fromHook = threadIdFromSearchParams;
if (isValidThreadId(fromHook)) {
return fromHook;
}
if (typeof window === "undefined") {
return undefined;
}
const fromLocation = new URLSearchParams(window.location.search).get(
"thread_id",
);
if (isValidThreadId(fromLocation)) {
return fromLocation.trim();
}
return undefined;
};
const queryThreadIdFromParams = readQueryThreadId();
// console.log("[useThreadChat] query.thread_id", queryThreadIdFromParams);
// 归一化:当值为 "new" 时,替换为 query 中的 thread_id如果存在
const normalizeThreadId = useCallback(
(value?: string | null) => {
if (!value) {
return undefined;
}
return value === "new" ? queryThreadIdFromParams : value;
},
[queryThreadIdFromParams],
);
// New session is only controlled by `/workspace/chats/new`. // New session is only controlled by `/workspace/chats/new`.
const [isNewThread, setIsNewThread] = useState(() => isNewRoute); const [isNewThread, setIsNewThread] = useState(() => isNewRoute);
@ -42,7 +82,7 @@ export function useThreadChat() {
// console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath); // console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath);
const [threadId, setThreadId] = useState<string>(() => { const [threadId, setThreadId] = useState<string>(() => {
return threadIdFromPathOrParams ?? ""; return threadIdFromPathOrParams;
}); });
@ -53,10 +93,11 @@ export function useThreadChat() {
} }
setIsNewThread(isNewRoute); setIsNewThread(isNewRoute);
// Prefer path thread id, fall back to query thread_id when path is /new. // Prefer path thread id, fall back to query thread_id when path is /new.
setThreadId(threadIdFromPathOrParams ?? ""); setThreadId(threadIdFromPathOrParams);
setShowWelcomeStyle(isNewRoute || !isChattingFromQuery); setShowWelcomeStyle(isNewRoute || !isChattingFromQuery);
}, [ }, [
isNewRoute, isNewRoute,
normalizeThreadId,
pathname, pathname,
searchParams, searchParams,
isChattingFromQuery, isChattingFromQuery,
@ -73,11 +114,6 @@ export function useThreadChat() {
}; };
} }
function normalizeThreadId(value?: string | null): string | undefined {
if (!value) return undefined;
return isValidThreadId(value) ? value.trim() : undefined;
}
function isValidThreadId(value?: string | null): value is string { function isValidThreadId(value?: string | null): value is string {
if (!value) return false; if (!value) return false;
const normalized = value.trim().toLowerCase(); const normalized = value.trim().toLowerCase();

View File

@ -59,10 +59,10 @@ import {
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import type { import type {
SelectedSkillPayloadItem, SelectedSkillPayloadItem,
SuggestionSkillChildren,
} from "@/core/i18n/locales/types"; } from "@/core/i18n/locales/types";
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages"; import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
import { useModels } from "@/core/models/hooks"; import { useModels } from "@/core/models/hooks";
import { bootstrapRemoteSkill } from "@/core/skills/api";
import type { AgentThreadContext } from "@/core/threads"; import type { AgentThreadContext } from "@/core/threads";
import { useIframeSkill } from "@/hooks/use-iframe-skill"; import { useIframeSkill } from "@/hooks/use-iframe-skill";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -429,7 +429,6 @@ export function InputBox({
{showWelcomeStyle && !hasSubmitted && searchParams.get("mode") !== "skill" && ( {showWelcomeStyle && !hasSubmitted && searchParams.get("mode") !== "skill" && (
<SuggestionListContainer <SuggestionListContainer
threadId={threadId}
sendSelectSkill={iframeSkill.sendSelectSkill} sendSelectSkill={iframeSkill.sendSelectSkill}
/> />
)} )}
@ -494,31 +493,26 @@ export function InputBox({
// SuggestionList 容器 // SuggestionList 容器
function SuggestionListContainer({ function SuggestionListContainer({
threadId,
sendSelectSkill, sendSelectSkill,
}: { }: {
threadId: string;
sendSelectSkill: (selectedSkills: SelectedSkillPayloadItem[]) => void; sendSelectSkill: (selectedSkills: SelectedSkillPayloadItem[]) => void;
}) { }) {
return ( return (
<div className="absolute right-0 bottom-0 left-0 z-0 flex translate-y-full items-center justify-center pt-4"> <div className="absolute right-0 bottom-0 left-0 z-0 flex translate-y-full items-center justify-center pt-4">
<SuggestionList threadId={threadId} sendSelectSkill={sendSelectSkill} /> <SuggestionList sendSelectSkill={sendSelectSkill} />
</div> </div>
); );
} }
// 快速选择skillbutton // 快速选择skillbutton
function SuggestionList({ function SuggestionList({
threadId,
sendSelectSkill, sendSelectSkill,
}: { }: {
threadId: string;
sendSelectSkill: (selectedSkills: SelectedSkillPayloadItem[]) => void; sendSelectSkill: (selectedSkills: SelectedSkillPayloadItem[]) => void;
}) { }) {
const { t } = useI18n(); const { t } = useI18n();
const searchParams = useSearchParams();
const { textInput } = usePromptInputController(); const { textInput } = usePromptInputController();
const suggestions = t.inputBox.suggestions; const suggestions =t.inputBox.suggestions
const promptSuggestions = suggestions.filter( const promptSuggestions = suggestions.filter(
( (
suggestion, suggestion,
@ -531,35 +525,14 @@ function SuggestionList({
suggestion: { suggestion: {
prompt: string; prompt: string;
skill_id?: string[]; skill_id?: string[];
children?: SelectedSkillPayloadItem[]; children?: SuggestionSkillChildren[];
suggestion: string; suggestion: string;
}, },
) => { ) => {
const languageTypeRaw =
searchParams.get("languageType")?.trim() ??
searchParams.get("language_type")?.trim();
const languageType = languageTypeRaw ? Number(languageTypeRaw) : 0;
const bootstrapByIds = (ids: string[]) => {
const content_ids = Array.from(
new Set(
ids
.map((id) => Number(id))
.filter((id) => Number.isFinite(id) && id > 0),
),
);
if (!threadId || content_ids.length === 0) return;
void bootstrapRemoteSkill({
thread_id: threadId,
content_ids,
language_type: languageType,
target_dir: "/mnt/user-data/uploads/skill",
clear_target: true,
});
};
// 优先从 children 中提取 skill_id 数组,转换为 selectedSkills 发送给宿主页 // 优先从 children 中提取 skill_id 数组,转换为 selectedSkills 发送给宿主页
const childSkillIds = (suggestion.children ?? []) const childSkillIds = (suggestion.children ?? [])
.map((item) => String(item.id).trim()) .flatMap((item) => item.skill_id)
.map((item) => item.trim())
.filter((id): id is string => Boolean(id)); .filter((id): id is string => Boolean(id));
if (childSkillIds.length > 0) { if (childSkillIds.length > 0) {
sendSelectSkill( sendSelectSkill(
@ -568,7 +541,6 @@ function SuggestionList({
name: suggestion.suggestion, name: suggestion.suggestion,
})), })),
); );
bootstrapByIds(childSkillIds);
return; return;
} }
if (suggestion.skill_id && suggestion.skill_id.length > 0) { if (suggestion.skill_id && suggestion.skill_id.length > 0) {
@ -578,7 +550,6 @@ function SuggestionList({
name: suggestion.suggestion, name: suggestion.suggestion,
})), })),
); );
bootstrapByIds(suggestion.skill_id);
return; return;
} }
// 原有逻辑 // 原有逻辑
@ -598,7 +569,7 @@ function SuggestionList({
} }
}, 500); }, 500);
}, },
[textInput, sendSelectSkill, threadId, searchParams], [textInput, sendSelectSkill],
); );
return ( return (
<Suggestions className="min-h-16 w-fit items-start" data-testid="welcome-suggestions"> <Suggestions className="min-h-16 w-fit items-start" data-testid="welcome-suggestions">

View File

@ -58,7 +58,7 @@ export const zhCN: Translations = {
// Welcome // Welcome
welcome: { welcome: {
// TODO: 测试环境标识 // TODO: 测试环境标识
greeting: "轻办公 · XClaw Tagv3.2.0 --- Skill功能施工中 --- refactor(frontend): 将 SELECT_SKILL 重命名为 SELECT_SKILLS.", greeting: "轻办公 · XClaw Tagv3.1.0 fix适配md图片的更多情况",
description: description:
"欢迎使用 🦌 DeerFlow一个完全开源的超级智能体。通过内置和自定义的 Skills\nDeerFlow 可以帮你搜索网络、分析数据,还能为你生成幻灯片、\n图片、视频、播客及网页等几乎可以做任何事情。", "欢迎使用 🦌 DeerFlow一个完全开源的超级智能体。通过内置和自定义的 Skills\nDeerFlow 可以帮你搜索网络、分析数据,还能为你生成幻灯片、\n图片、视频、播客及网页等几乎可以做任何事情。",
@ -117,31 +117,31 @@ export const zhCN: Translations = {
prompt: prompt:
"为[主题/产品]撰写吸引人的自媒体文案,包括标题、正文和话题标签。", "为[主题/产品]撰写吸引人的自媒体文案,包括标题、正文和话题标签。",
icon: PenLineIcon, icon: PenLineIcon,
children: [{ id: "1245", name: "微信文章撰写" }], children: [{ id: "1245", name: "自媒体文案" }],
}, },
{ {
suggestion: "需求文档", suggestion: "需求文档",
prompt: "编写[项目/功能]的需求文档,包含功能描述、用户故事和验收标准。", prompt: "编写[项目/功能]的需求文档,包含功能描述、用户故事和验收标准。",
icon: CompassIcon, icon: CompassIcon,
children: [{ id: "520", name: "分解功能产品需求文档" }], children: [{ id: "520", name: "需求文档" }],
}, },
{ {
suggestion: "使用指南", suggestion: "使用指南",
prompt: "编写[产品/功能]的使用指南,包含操作步骤、注意事项和常见问题。", prompt: "编写[产品/功能]的使用指南,包含操作步骤、注意事项和常见问题。",
icon: GraduationCapIcon, icon: GraduationCapIcon,
children: [{ id: "409", name: "指南编写" }], children: [{ id: "409", name: "使用指南" }],
}, },
{ {
suggestion: "Excel数据分析", suggestion: "Excel数据分析",
prompt: "对[Excel文件/数据]进行分析,生成数据洞察和可视化建议。", prompt: "对[Excel文件/数据]进行分析,生成数据洞察和可视化建议。",
icon: MicroscopeIcon, icon: MicroscopeIcon,
children: [{ id: "5", name: "数据分析" }], children: [{ id: "5", name: "Excel数据分析" }],
}, },
{ {
suggestion: "市场调研", suggestion: "市场调研",
prompt: "针对[行业/产品]进行市场调研,分析市场规模、竞品和趋势。", prompt: "针对[行业/产品]进行市场调研,分析市场规模、竞品和趋势。",
icon: ShapesIcon, icon: ShapesIcon,
children: [{ id: "1216", name: "市场究报告" }], children: [{ id: "1216", name: "市场研" }],
}, },
], ],
suggestionsCreate: [ suggestionsCreate: [

View File

@ -12,7 +12,7 @@ export const POST_MESSAGE_TYPES = {
// 会话是否处于聊天态 // 会话是否处于聊天态
IS_CHATTING: "isChatting", IS_CHATTING: "isChatting",
// 选择预定义 skill // 选择预定义 skill
SELECT_SKILLS: "selectedSkills", SELECT_SKILL: "selectedSkills",
// 打开 skill 选择对话框 // 打开 skill 选择对话框
OPEN_SKILL_DIALOG: "openSkillDialog", OPEN_SKILL_DIALOG: "openSkillDialog",
} as const; } as const;
@ -41,7 +41,7 @@ export interface IsChattingMessage {
} }
export interface SelectSkillMessage { export interface SelectSkillMessage {
type: typeof POST_MESSAGE_TYPES.SELECT_SKILLS; type: typeof POST_MESSAGE_TYPES.SELECT_SKILL;
selectedSkills: SelectedSkillPayloadItem[]; selectedSkills: SelectedSkillPayloadItem[];
} }

View File

@ -193,17 +193,14 @@ export function useThreadStream({
handleStreamStart(meta.thread_id); handleStreamStart(meta.thread_id);
setOnStreamThreadId(meta.thread_id); setOnStreamThreadId(meta.thread_id);
}, },
onLangChainEvent: onLangChainEvent(event) {
onToolEnd == null if (event.event === "on_tool_end") {
? undefined listeners.current.onToolEnd?.({
: (event) => { name: event.name,
if (event.event === "on_tool_end") { data: event.data,
listeners.current.onToolEnd?.({ });
name: event.name, }
data: event.data, },
});
}
},
onUpdateEvent(data) { onUpdateEvent(data) {
const updates: Array<Partial<AgentThreadState> | null> = Object.values( const updates: Array<Partial<AgentThreadState> | null> = Object.values(
data || {}, data || {},
@ -328,20 +325,15 @@ export function useThreadStream({
} }
setOptimisticMessages(newOptimistic); setOptimisticMessages(newOptimistic);
// For "new chat with prefilled thread_id" flows, calling onStart before if (resolvedThreadId) {
// submit can trigger route switch too early, which causes the new page to
// fetch history before the thread/run is actually created.
// Let useStream.onCreated -> handleStreamStart drive onStart instead.
if (resolvedThreadId && !createNewSession) {
_handleOnStart(resolvedThreadId); _handleOnStart(resolvedThreadId);
} }
let uploadedFileInfo: UploadedFileInfo[] = []; let uploadedFileInfo: UploadedFileInfo[] = [];
try { try {
// 新会话模式下,仅在本地已有历史消息时才重置旧线程。 // 新会话模式下,删除旧线程并创建同名新线程
// 对于全新 thread_id避免多发一次 DELETE /threads/{id}(通常会 404 if (createNewSession && resolvedThreadId) {
if (createNewSession && resolvedThreadId && thread.messages.length > 0) {
await apiClient.threads.delete(resolvedThreadId).catch(() => undefined); await apiClient.threads.delete(resolvedThreadId).catch(() => undefined);
} }

View File

@ -70,7 +70,7 @@ export function useIframeSkill(): UseIframeSkillReturn {
// 发送选择预定义 skill // 发送选择预定义 skill
const sendSelectSkill = useCallback((selectedSkills: SelectedSkillPayloadItem[]) => { const sendSelectSkill = useCallback((selectedSkills: SelectedSkillPayloadItem[]) => {
const message = { type: POST_MESSAGE_TYPES.SELECT_SKILLS, selectedSkills }; const message = { type: POST_MESSAGE_TYPES.SELECT_SKILL, selectedSkills };
console.log("[useIframeSkill] sendSelectSkill:", message); console.log("[useIframeSkill] sendSelectSkill:", message);
sendToParent(message); sendToParent(message);
}, []); }, []);
@ -89,7 +89,7 @@ export function useIframeSkill(): UseIframeSkillReturn {
const clearSkill = useCallback(() => { const clearSkill = useCallback(() => {
setSelectedSkill(null); setSelectedSkill(null);
// 发送空数组给主页,通知取消选择 // 发送空数组给主页,通知取消选择
const message = { type: POST_MESSAGE_TYPES.SELECT_SKILLS, selectedSkills: [] }; const message = { type: POST_MESSAGE_TYPES.SELECT_SKILL, selectedSkills: [] };
console.log("[useIframeSkill] clearSkill, sending selectedSkills=[]:", message); console.log("[useIframeSkill] clearSkill, sending selectedSkills=[]:", message);
sendToParent(message); sendToParent(message);
}, []); }, []);

View File

@ -1,72 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
# Default deploy target (can be overridden by positional args)
DEFAULT_REMOTE="root@111.228.39.147"
DEFAULT_REMOTE_APP_DIR="/root/deerflow2"
usage() {
cat <<'EOF'
Usage:
./scripts/deploy-frontend-standalone.sh [remote] [remote_app_dir]
Arguments:
remote SSH target, optional. Default: root@111.228.39.147
remote_app_dir Remote deerflow2 root dir, optional. Default: /root/deerflow2
Example:
./scripts/deploy-frontend-standalone.sh
./scripts/deploy-frontend-standalone.sh ubuntu@1.2.3.4 /opt/deerflow2
Notes:
- Run this script on your local machine (build machine), not on the server.
- Requires: pnpm, rsync, ssh
- Script will run:
sudo systemctl daemon-reload
sudo systemctl restart deerflow.service
EOF
}
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
usage
exit 0
fi
if [ "$#" -gt 2 ]; then
usage
exit 1
fi
REMOTE="${1:-$DEFAULT_REMOTE}"
REMOTE_APP_DIR="${2:-$DEFAULT_REMOTE_APP_DIR}"
echo "==> Deploy target:"
echo " remote: $REMOTE"
echo " app dir: $REMOTE_APP_DIR"
echo "==> Building frontend (standalone)..."
pnpm -C frontend build
echo "==> Uploading standalone server..."
rsync -azP --delete --info=progress2 \
frontend/.next/standalone/ \
"$REMOTE:$REMOTE_APP_DIR/frontend/.next/standalone/"
echo "==> Uploading static assets..."
rsync -azP --info=progress2 \
frontend/.next/static/ \
"$REMOTE:$REMOTE_APP_DIR/frontend/.next/standalone/.next/static/"
echo "==> Uploading public assets..."
rsync -azP --info=progress2 \
frontend/public/ \
"$REMOTE:$REMOTE_APP_DIR/frontend/.next/standalone/public/"
echo "==> Reloading systemd and restarting deerflow.service on remote host..."
ssh "$REMOTE" "sudo systemctl daemon-reload && sudo systemctl restart deerflow.service"
echo "==> Done."

View File

@ -38,43 +38,7 @@ else
echo "Python is required to generate BETTER_AUTH_SECRET, but neither python3 nor python was found." echo "Python is required to generate BETTER_AUTH_SECRET, but neither python3 nor python was found."
exit 1 exit 1
fi fi
FRONTEND_STANDALONE_DIR="$REPO_ROOT/frontend/.next/standalone" FRONTEND_CMD="env BETTER_AUTH_SECRET=$($PYTHON_BIN -c 'import secrets; print(secrets.token_hex(16))') pnpm run preview"
FRONTEND_STANDALONE_SERVER="$FRONTEND_STANDALONE_DIR/server.js"
FRONTEND_STANDALONE_STATIC_DIR="$FRONTEND_STANDALONE_DIR/.next/static"
FRONTEND_SOURCE_STATIC_DIR="$REPO_ROOT/frontend/.next/static"
FRONTEND_STANDALONE_PUBLIC_DIR="$FRONTEND_STANDALONE_DIR/public"
FRONTEND_SOURCE_PUBLIC_DIR="$REPO_ROOT/frontend/public"
if [ ! -f "$FRONTEND_STANDALONE_SERVER" ]; then
echo "✗ Frontend standalone server not found: $FRONTEND_STANDALONE_SERVER"
echo " make start (prod) now uses prebuilt standalone assets and will not run local build."
echo " Please prepare frontend artifacts first (example):"
echo " pnpm -C frontend build"
exit 1
fi
# Align local runtime layout with deploy-frontend-standalone.sh:
# standalone + .next/static + public under frontend/.next/standalone/
if [ ! -d "$FRONTEND_STANDALONE_STATIC_DIR" ] && [ -d "$FRONTEND_SOURCE_STATIC_DIR" ]; then
mkdir -p "$FRONTEND_STANDALONE_DIR/.next"
cp -R "$FRONTEND_SOURCE_STATIC_DIR" "$FRONTEND_STANDALONE_STATIC_DIR"
fi
if [ ! -d "$FRONTEND_STANDALONE_PUBLIC_DIR" ] && [ -d "$FRONTEND_SOURCE_PUBLIC_DIR" ]; then
cp -R "$FRONTEND_SOURCE_PUBLIC_DIR" "$FRONTEND_STANDALONE_PUBLIC_DIR"
fi
if [ ! -d "$FRONTEND_STANDALONE_STATIC_DIR" ]; then
echo "✗ Missing standalone static assets: $FRONTEND_STANDALONE_STATIC_DIR"
echo " Please ensure .next/static is available for standalone runtime."
exit 1
fi
if [ ! -d "$FRONTEND_STANDALONE_PUBLIC_DIR" ]; then
echo "✗ Missing standalone public assets: $FRONTEND_STANDALONE_PUBLIC_DIR"
echo " Please ensure public/ is available for standalone runtime."
exit 1
fi
FRONTEND_CMD="env BETTER_AUTH_SECRET=$($PYTHON_BIN -c 'import secrets; print(secrets.token_hex(16))') HOSTNAME=0.0.0.0 PORT=3000 node server.js"
fi fi
# ── Stop existing services ──────────────────────────────────────────────────── # ── Stop existing services ────────────────────────────────────────────────────
@ -84,7 +48,6 @@ pkill -f "langgraph dev" 2>/dev/null || true
pkill -f "uvicorn app.gateway.app:app" 2>/dev/null || true pkill -f "uvicorn app.gateway.app:app" 2>/dev/null || true
pkill -f "next dev" 2>/dev/null || true pkill -f "next dev" 2>/dev/null || true
pkill -f "next-server" 2>/dev/null || true pkill -f "next-server" 2>/dev/null || true
pkill -f "frontend/.next/standalone/server.js" 2>/dev/null || true
nginx -c "$REPO_ROOT/docker/nginx/nginx.local.conf" -p "$REPO_ROOT" -s quit 2>/dev/null || true nginx -c "$REPO_ROOT/docker/nginx/nginx.local.conf" -p "$REPO_ROOT" -s quit 2>/dev/null || true
sleep 1 sleep 1
pkill -9 nginx 2>/dev/null || true pkill -9 nginx 2>/dev/null || true
@ -147,7 +110,6 @@ cleanup() {
pkill -f "next dev" 2>/dev/null || true pkill -f "next dev" 2>/dev/null || true
pkill -f "next start" 2>/dev/null || true pkill -f "next start" 2>/dev/null || true
pkill -f "next-server" 2>/dev/null || true pkill -f "next-server" 2>/dev/null || true
pkill -f "frontend/.next/standalone/server.js" 2>/dev/null || true
# Kill nginx using the captured PID first (most reliable), # Kill nginx using the captured PID first (most reliable),
# then fall back to pkill/killall for any stray nginx workers. # then fall back to pkill/killall for any stray nginx workers.
if [ -n "${NGINX_PID:-}" ] && kill -0 "$NGINX_PID" 2>/dev/null; then if [ -n "${NGINX_PID:-}" ] && kill -0 "$NGINX_PID" 2>/dev/null; then
@ -219,11 +181,7 @@ echo "Starting Gateway API..."
echo "✓ Gateway API started on localhost:8001" echo "✓ Gateway API started on localhost:8001"
echo "Starting Frontend..." echo "Starting Frontend..."
if $DEV_MODE; then (cd frontend && $FRONTEND_CMD > ../logs/frontend.log 2>&1) &
(cd frontend && $FRONTEND_CMD > ../logs/frontend.log 2>&1) &
else
(cd frontend/.next/standalone && $FRONTEND_CMD > ../../../logs/frontend.log 2>&1) &
fi
./scripts/wait-for-port.sh 3000 120 "Frontend" || { ./scripts/wait-for-port.sh 3000 120 "Frontend" || {
echo " See logs/frontend.log for details" echo " See logs/frontend.log for details"
tail -20 logs/frontend.log tail -20 logs/frontend.log

View File

@ -1,6 +0,0 @@
# RunningHub API Configuration
# Copy this file to .env and fill in your actual API key
# RunningHub API Key for image generation
# Get your API key from: https://www.runninghub.cn
RUNNINGHUB_API_KEY=your_api_key_here

View File

@ -1,31 +0,0 @@
# Environment variables
.env
.env.local
.env.*.local
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
env/
ENV/
# Output files
*.jpg
*.jpeg
*.png
*.webp
outputs/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db

103
skills/public/image-generation/SKILL.md Executable file → Normal file
View File

@ -7,47 +7,14 @@ description: Use this skill when the user requests to generate, create, imagine,
## Overview ## Overview
This skill generates high-quality images using RunningHub API with structured prompts and a Python script. The workflow includes creating JSON-formatted prompts and executing image generation through asynchronous task submission. This skill generates high-quality images using structured prompts and a Python script. The workflow includes creating JSON-formatted prompts and executing image generation with optional reference images.
## Core Capabilities ## Core Capabilities
- Create structured JSON prompts for AIGC image generation - Create structured JSON prompts for AIGC image generation
- Generate images through RunningHub's Z-Image Turbo LoRA API - Support multiple reference images for style/composition guidance
- Support asynchronous task submission and status polling - Generate images through automated Python script execution
- Handle various image generation scenarios (character design, scenes, products, etc.) - Handle various image generation scenarios (character design, scenes, products, etc.)
- Support multiple aspect ratios and output formats (PNG, JPEG, WEBP)
## Configuration
### API Key Setup
This skill uses RunningHub API for image generation. You need to configure your API key before using the skill.
**Option 1: Environment Variable (Recommended)**
```bash
# Set the RUNNINGHUB_API_KEY environment variable
export RUNNINGHUB_API_KEY=your_api_key_here
# Or on Windows:
set RUNNINGHUB_API_KEY=your_api_key_here
```
**Option 2: .env File**
1. Copy `.env.example` to `.env`:
```bash
cp .env.example .env
```
2. Edit `.env` and add your API key:
```
RUNNINGHUB_API_KEY=your_api_key_here
```
3. The `.env` file is automatically excluded from version control via `.gitignore`
**Security Notes:**
- Never commit `.env` files to version control
- Never hardcode API keys in source code
- Rotate your API keys if they are accidentally exposed
- Get your API key from: https://www.runninghub.cn
## Workflow ## Workflow
@ -71,20 +38,20 @@ Call the Python script:
```bash ```bash
python /mnt/skills/public/image-generation/scripts/generate.py \ python /mnt/skills/public/image-generation/scripts/generate.py \
--prompt-file /mnt/user-data/workspace/prompt-file.json \ --prompt-file /mnt/user-data/workspace/prompt-file.json \
--output-file /mnt/user-data/outputs/generated-image.jpg \ --reference-images /path/to/ref1.jpg /path/to/ref2.png \
--output-file /mnt/user-data/outputs/generated-image.jpg
--aspect-ratio 16:9 --aspect-ratio 16:9
``` ```
Parameters: Parameters:
- `--prompt-file`: Absolute path to JSON prompt file (required) - `--prompt-file`: Absolute path to JSON prompt file (required)
- `--reference-images`: Absolute paths to reference images (optional, space-separated)
- `--output-file`: Absolute path to output image file (required) - `--output-file`: Absolute path to output image file (required)
- `--aspect-ratio`: Aspect ratio of the generated image (optional, default: 16:9) - `--aspect-ratio`: Aspect ratio of the generated image (optional, default: 16:9)
[!NOTE] [!NOTE]
- The script uses RunningHub API which requires `RUNNINGHUB_API_KEY` environment variable to be set Do NOT read the python file, just call it with the parameters.
- Do NOT read the python file, just call it with the parameters
- The script automatically handles task submission, status polling, and image download
## Character Generation Example ## Character Generation Example
@ -119,6 +86,40 @@ python /mnt/skills/public/image-generation/scripts/generate.py \
--aspect-ratio 2:3 --aspect-ratio 2:3
``` ```
With reference images:
```json
{
"characters": [{
"gender": "based on [Image 1]",
"age": "based on [Image 1]",
"ethnicity": "human from [Image 1] adapted to Star Wars universe",
"body_type": "based on [Image 1]",
"facial_features": "matching [Image 1] with slight weathered look from space travel",
"clothing": "Star Wars style outfit - worn leather jacket with utility vest, cargo pants with tactical pouches, scuffed boots, belt with holster",
"accessories": "blaster pistol on hip, comlink device on wrist, goggles pushed up on forehead, satchel with supplies, personal vehicle based on [Image 2]",
"era": "Star Wars universe, post-Empire era"
}],
"prompt": "Character inspired by [Image 1] standing next to a vehicle inspired by [Image 2] on a bustling alien planet street in Star Wars universe aesthetic. Character wearing worn leather jacket with utility vest, cargo pants with tactical pouches, scuffed boots, belt with blaster holster. The vehicle adapted to Star Wars aesthetic with weathered metal panels, repulsor engines, desert dust covering, parked on the street. Exotic alien marketplace street with multi-level architecture, weathered metal structures, hanging market stalls with colorful awnings, alien species walking by as background characters. Twin suns casting warm golden light, atmospheric dust particles in air, moisture vaporators visible in distance. Gritty lived-in Star Wars aesthetic, practical effects look, film grain texture, cinematic composition.",
"negative_prompt": "clean futuristic look, sterile environment, overly CGI appearance, fantasy medieval elements, Earth architecture, modern city",
"style": "Star Wars original trilogy aesthetic, lived-in universe, practical effects inspired, cinematic film look, slightly desaturated with warm tones",
"composition": "medium wide shot, character in foreground with alien street extending into background, environmental storytelling, rule of thirds",
"lighting": "warm golden hour lighting from twin suns, rim lighting on character, atmospheric haze, practical light sources from market stalls",
"color_palette": "warm sandy tones, ochre and sienna, dusty blues, weathered metals, muted earth colors with pops of alien market colors",
"technical": {
"aspect_ratio": "9:16",
"quality": "high",
"detail_level": "highly detailed with film-like texture"
}
}
```
```bash
python /mnt/skills/public/image-generation/scripts/generate.py \
--prompt-file /mnt/user-data/workspace/star-wars-scene.json \
--reference-images /mnt/user-data/uploads/character-ref.jpg /mnt/user-data/uploads/vehicle-ref.jpg \
--output-file /mnt/user-data/outputs/star-wars-scene-01.jpg \
--aspect-ratio 16:9
```
## Common Scenarios ## Common Scenarios
Use different JSON schemas for different scenarios. Use different JSON schemas for different scenarios.
@ -157,10 +158,30 @@ After generation:
- Provide brief description of the generation result - Provide brief description of the generation result
- Offer to iterate if adjustments needed - Offer to iterate if adjustments needed
## Tips: Enhancing Generation with Reference Images
For scenarios where visual accuracy is critical, **use the `image_search` tool first** to find reference images before generation.
**Recommended scenarios for using image_search tool:**
- **Character/Portrait Generation**: Search for similar poses, expressions, or styles to guide facial features and body proportions
- **Specific Objects or Products**: Find reference images of real objects to ensure accurate representation
- **Architectural or Environmental Scenes**: Search for location references to capture authentic details
- **Fashion and Clothing**: Find style references to ensure accurate garment details and styling
**Example workflow:**
1. Call the `image_search` tool to find suitable reference images:
```
image_search(query="Japanese woman street photography 1990s", size="Large")
```
2. Download the returned image URLs to local files
3. Use the downloaded images as `--reference-images` parameter in the generation script
This approach significantly improves generation quality by providing the model with concrete visual guidance rather than relying solely on text descriptions.
## Notes ## Notes
- Always use English for prompts regardless of user's language - Always use English for prompts regardless of user's language
- JSON format ensures structured, parsable prompts - JSON format ensures structured, parsable prompts
- Reference images enhance generation quality significantly
- Iterative refinement is normal for optimal results - Iterative refinement is normal for optimal results
- For character generation, include the detailed character object plus a consolidated prompt field - For character generation, include the detailed character object plus a consolidated prompt field
- The script automatically polls task status and downloads the generated image

210
skills/public/image-generation/scripts/generate.py Executable file → Normal file
View File

@ -1,14 +1,8 @@
import base64 import base64
import json
import os import os
import time
from typing import List
import requests import requests
from PIL import Image from PIL import Image
from dotenv import load_dotenv
load_dotenv()
def validate_image(image_path: str) -> bool: def validate_image(image_path: str) -> bool:
@ -23,171 +17,77 @@ def validate_image(image_path: str) -> bool:
""" """
try: try:
with Image.open(image_path) as img: with Image.open(image_path) as img:
img.verify() img.verify() # Verify that it's a valid image
# Re-open to check if it can be fully loaded (verify() may not catch all issues)
with Image.open(image_path) as img: with Image.open(image_path) as img:
img.load() img.load() # Force load the image data
return True return True
except Exception as e: except Exception as e:
print(f"Warning: Image '{image_path}' is invalid or corrupted: {e}") print(f"Warning: Image '{image_path}' is invalid or corrupted: {e}")
return False return False
def submit_generation_task(prompt: str, aspect_ratio: str = "16:9", output_format: str = "png") -> str:
"""
Submit image generation task to RunningHub API.
Args:
prompt: Text prompt for image generation
aspect_ratio: Aspect ratio of the generated image
output_format: Output image format (png, jpeg, webp)
Returns:
Task ID for tracking the generation
"""
api_key = os.getenv("RUNNINGHUB_API_KEY")
if not api_key:
raise Exception("RUNNINGHUB_API_KEY environment variable is not set")
url = "https://www.runninghub.cn/openapi/v2/rhart-image/z-image/turbo-lora"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"prompt": prompt,
"aspectRatio": aspect_ratio,
"lora_name": "Z-Image _ 清纯高颜值_脸模版V1.0.safetensors",
"lora_strength": 1,
"outputFormat": output_format
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
result = response.json()
if result.get("status") not in ["QUEUED", "RUNNING", "SUCCESS"]:
raise Exception(f"Task submission failed: {result.get('errorMessage', 'Unknown error')}")
return result.get("taskId")
def query_task_status(task_id: str) -> dict:
"""
Query the status of a generation task.
Args:
task_id: Task ID to query
Returns:
Task status information
"""
api_key = os.getenv("RUNNINGHUB_API_KEY")
if not api_key:
raise Exception("RUNNINGHUB_API_KEY environment variable is not set")
url = "https://www.runninghub.cn/openapi/v2/query"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"taskId": task_id
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
def download_image(url: str, output_path: str) -> None:
"""
Download image from URL and save to file.
Args:
url: Image URL to download
output_path: Local path to save the image
"""
response = requests.get(url, stream=True)
response.raise_for_status()
with open(output_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
def generate_image( def generate_image(
prompt_file: str, prompt_file: str,
reference_images: List[str], reference_images: list[str],
output_file: str, output_file: str,
aspect_ratio: str = "16:9", aspect_ratio: str = "16:9",
) -> str: ) -> str:
"""
Generate image using RunningHub API.
Args:
prompt_file: Path to JSON prompt file
reference_images: List of reference image paths (currently not supported by RunningHub API)
output_file: Output path for generated image
aspect_ratio: Aspect ratio of the generated image
Returns:
Success message with output file path
"""
with open(prompt_file, "r", encoding="utf-8") as f: with open(prompt_file, "r", encoding="utf-8") as f:
prompt_data = json.load(f) prompt = f.read()
parts = []
i = 0
if reference_images: # Filter out invalid reference images
print("Note: RunningHub API does not support reference images in this version. Reference images will be ignored.") valid_reference_images = []
for ref_img in reference_images:
prompt_text = prompt_data.get("prompt", "") if validate_image(ref_img):
if not prompt_text: valid_reference_images.append(ref_img)
prompt_text = json.dumps(prompt_data, ensure_ascii=False)
output_format = "png"
if output_file.lower().endswith(".jpg") or output_file.lower().endswith(".jpeg"):
output_format = "jpeg"
elif output_file.lower().endswith(".webp"):
output_format = "webp"
print(f"Submitting generation task...")
task_id = submit_generation_task(prompt_text, aspect_ratio, output_format)
print(f"Task submitted successfully. Task ID: {task_id}")
max_retries = 60
retry_interval = 2
for attempt in range(max_retries):
print(f"Checking task status... (Attempt {attempt + 1}/{max_retries})")
status_result = query_task_status(task_id)
status = status_result.get("status")
if status == "SUCCESS":
print("Task completed successfully!")
results = status_result.get("results", [])
if results and len(results) > 0:
image_url = results[0].get("url")
if image_url:
print(f"Downloading image from: {image_url}")
download_image(image_url, output_file)
return f"Successfully generated image to {output_file}"
else:
raise Exception("No image URL found in task results")
else:
raise Exception("No results found in task response")
elif status == "FAILED":
error_msg = status_result.get("errorMessage", "Unknown error")
raise Exception(f"Task failed: {error_msg}")
elif status in ["QUEUED", "RUNNING"]:
print(f"Task status: {status}. Waiting...")
time.sleep(retry_interval)
else: else:
raise Exception(f"Unknown task status: {status}") print(f"Skipping invalid reference image: {ref_img}")
raise Exception(f"Task did not complete within {max_retries * retry_interval} seconds") if len(valid_reference_images) < len(reference_images):
print(f"Note: {len(reference_images) - len(valid_reference_images)} reference image(s) were skipped due to validation failure.")
for reference_image in valid_reference_images:
i += 1
with open(reference_image, "rb") as f:
image_b64 = base64.b64encode(f.read()).decode("utf-8")
parts.append(
{
"inlineData": {
"mimeType": "image/jpeg",
"data": image_b64,
}
}
)
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
return "GEMINI_API_KEY is not set"
response = requests.post(
"https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent",
headers={
"x-goog-api-key": api_key,
"Content-Type": "application/json",
},
json={
"generationConfig": {"imageConfig": {"aspectRatio": aspect_ratio}},
"contents": [{"parts": [*parts, {"text": prompt}]}],
},
)
response.raise_for_status()
json = response.json()
parts: list[dict] = json["candidates"][0]["content"]["parts"]
image_parts = [part for part in parts if part.get("inlineData", False)]
if len(image_parts) == 1:
base64_image = image_parts[0]["inlineData"]["data"]
# Save the image to a file
with open(output_file, "wb") as f:
f.write(base64.b64decode(base64_image))
return f"Successfully generated image to {output_file}"
else:
raise Exception("Failed to generate image")
if __name__ == "__main__": if __name__ == "__main__":

0
skills/public/image-generation/templates/doraemon.md Executable file → Normal file
View File

View File

@ -1,16 +0,0 @@
.env
*.mp4
*.avi
*.mov
*.mkv
outputs/
workspace/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg-info/
dist/
build/

View File

@ -1,93 +0,0 @@
# Video Generation Skill - Setup Guide
## Quick Start
1. **Install Dependencies**
```bash
pip install requests python-dotenv
```
2. **Configure API Key**
Create a `.env` file in the project root directory:
```env
RUNNINGHUB_API_KEY=your_api_key_here
```
Or set it as an environment variable:
```bash
# Windows PowerShell
$env:RUNNINGHUB_API_KEY="your_api_key_here"
# Linux/Mac
export RUNNINGHUB_API_KEY="your_api_key_here"
```
3. **Generate a Video**
```bash
python scripts/generate.py --prompt-file workspace/your-prompt.json --output-file outputs/video.mp4 --duration 5
```
## Parameters
- `--prompt-file`: Path to JSON prompt file (required)
- `--output-file`: Output video file path (required)
- `--aspect-ratio`: Video aspect ratio (optional, default: 16:9)
- `--duration`: Video duration in seconds (optional, default: 5, range: 1-16)
## Getting API Key
To use this skill, you need a RunningHub API key:
1. Visit [RunningHub](https://www.runninghub.cn/)
2. Sign up for an account
3. Get your API key from the dashboard
4. Add it to your `.env` file
## Example Prompt
Create a JSON file with your video description:
```json
{
"title": "Your Video Title",
"description": "Description of what you want to generate",
"visual": {
"scene": "Scene description",
"elements": ["element1", "element2"],
"colors": "Color palette",
"lighting": "Lighting description"
},
"camera": {
"movement": "Camera movement",
"focus": "Focus description"
},
"audio": {
"background": "Background music description",
"effects": "Sound effects description"
}
}
```
## Notes
- The `.env` file is already in `.gitignore` and won't be committed to version control
- Never share your API key or commit it to public repositories
- The script automatically loads environment variables from `.env` file
- Video generation may take several minutes depending on the complexity
## Troubleshooting
**Error: RUNNINGHUB_API_KEY is not set**
- Make sure you've created the `.env` file with your API key
- Or set the environment variable before running the script
**Error: Failed to submit task**
- Check that your API key is valid
- Ensure you have sufficient credits in your RunningHub account
- Verify your internet connection
**Video generation takes too long**
- This is normal for AI video generation
- The script will automatically poll for status until completion
- You can check the RunningHub dashboard for task progress

56
skills/public/video-generation/SKILL.md Executable file → Normal file
View File

@ -7,14 +7,13 @@ description: Use this skill when the user requests to generate, create, or imagi
## Overview ## Overview
This skill generates high-quality videos using structured prompts and a Python script. The workflow includes creating JSON-formatted prompts and executing video generation through RunningHub API. This skill generates high-quality videos using structured prompts and a Python script. The workflow includes creating JSON-formatted prompts and executing video generation with optional reference image.
## Core Capabilities ## Core Capabilities
- Create structured JSON prompts for AIGC video generation - Create structured JSON prompts for AIGC video generation
- Generate videos through RunningHub Vidu model (text-to-video-q3-turbo) - Support reference image as guidance or the first/last frame of the video
- Support up to 16 seconds video generation with audio - Generate videos through automated Python script execution
- Automatic camera switching and dialogue generation
## Workflow ## Workflow
@ -22,16 +21,21 @@ This skill generates high-quality videos using structured prompts and a Python s
When a user requests video generation, identify: When a user requests video generation, identify:
- Subject/content: What should be in the video - Subject/content: What should be in the image
- Style preferences: Art style, mood, color palette - Style preferences: Art style, mood, color palette
- Technical specs: Aspect ratio, resolution, duration - Technical specs: Aspect ratio, composition, lighting
- Audio requirements: Background music, dialogue, sound effects - Reference image: Any image to guide generation
- You don't need to check the folder under `/mnt/user-data`
### Step 2: Create Structured Prompt ### Step 2: Create Structured Prompt
Generate a structured JSON file in `/mnt/user-data/workspace/` with naming pattern: `{descriptive-name}.json` Generate a structured JSON file in `/mnt/user-data/workspace/` with naming pattern: `{descriptive-name}.json`
The prompt should include visual descriptions, camera movements, and audio specifications in a natural language format. ### Step 3: Create Reference Image (Optional when image-generation skill is available)
Generate reference image for the video generation.
- If only 1 image is provided, use it as the guided frame of the video
### Step 3: Execute Generation ### Step 3: Execute Generation
@ -39,6 +43,7 @@ Call the Python script:
```bash ```bash
python /mnt/skills/public/video-generation/scripts/generate.py \ python /mnt/skills/public/video-generation/scripts/generate.py \
--prompt-file /mnt/user-data/workspace/prompt-file.json \ --prompt-file /mnt/user-data/workspace/prompt-file.json \
--reference-images /path/to/ref1.jpg \
--output-file /mnt/user-data/outputs/generated-video.mp4 \ --output-file /mnt/user-data/outputs/generated-video.mp4 \
--aspect-ratio 16:9 --aspect-ratio 16:9
``` ```
@ -46,28 +51,20 @@ python /mnt/skills/public/video-generation/scripts/generate.py \
Parameters: Parameters:
- `--prompt-file`: Absolute path to JSON prompt file (required) - `--prompt-file`: Absolute path to JSON prompt file (required)
- `--output-file`: Absolute path to output video file (required) - `--reference-images`: Absolute paths to reference image (optional)
- `--aspect-ratio`: Aspect ratio of the generated video (optional, default: 16:9) - `--output-file`: Absolute path to output image file (required)
- `--aspect-ratio`: Aspect ratio of the generated image (optional, default: 16:9)
[!NOTE] [!NOTE]
Do NOT read the python file, instead just call it with the parameters. Do NOT read the python file, instead just call it with the parameters.
## Environment Variables
Set the following environment variable before running the script:
- `RUNNINGHUB_API_KEY`: Your RunningHub API key
Example:
```bash
export RUNNINGHUB_API_KEY=a73d0e93afb4432c978e5bff30b7517e
```
## Video Generation Example ## Video Generation Example
User request: "Generate a short video clip depicting the opening scene from "The Chronicles of Narnia: The Lion, the Witch and the Wardrobe" User request: "Generate a short video clip depicting the opening scene from "The Chronicles of Narnia: The Lion, the Witch and the Wardrobe"
Step 1: Create a JSON prompt file with the following content: Step 1: Search for the opening scene of "The Chronicles of Narnia: The Lion, the Witch and the Wardrobe" online
Step 2: Create a JSON prompt file with the following content:
```json ```json
{ {
@ -111,11 +108,16 @@ Step 1: Create a JSON prompt file with the following content:
} }
``` ```
Step 2: Use the generate.py script to generate the video Step 3: Use the image-generation skill to generate the reference image
Load the image-generation skill and generate a single reference image `narnia-farewell-scene-01.jpg` according to the skill.
Step 4: Use the generate.py script to generate the video
```bash ```bash
python /mnt/skills/public/video-generation/scripts/generate.py \ python /mnt/skills/public/video-generation/scripts/generate.py \
--prompt-file /mnt/user-data/workspace/narnia-farewell-scene.json \ --prompt-file /mnt/user-data/workspace/narnia-farewell-scene.json \
--output-file /mnt/user-data/outputs/narnia-farewell-scene.mp4 \ --reference-images /mnt/user-data/outputs/narnia-farewell-scene-01.jpg \
--output-file /mnt/user-data/outputs/narnia-farewell-scene-01.mp4 \
--aspect-ratio 16:9 --aspect-ratio 16:9
``` ```
> Do NOT read the python file, just call it with the parameters. > Do NOT read the python file, just call it with the parameters.
@ -125,7 +127,7 @@ python /mnt/skills/public/video-generation/scripts/generate.py \
After generation: After generation:
- Videos are typically saved in `/mnt/user-data/outputs/` - Videos are typically saved in `/mnt/user-data/outputs/`
- Share generated videos with user using `present_files` tool - Share generated videos (come first) with user as well as generated image if applicable, using `present_files` tool
- Provide brief description of the generation result - Provide brief description of the generation result
- Offer to iterate if adjustments needed - Offer to iterate if adjustments needed
@ -133,7 +135,5 @@ After generation:
- Always use English for prompts regardless of user's language - Always use English for prompts regardless of user's language
- JSON format ensures structured, parsable prompts - JSON format ensures structured, parsable prompts
- RunningHub Vidu model supports up to 16 seconds video generation - Reference image enhance generation quality significantly
- Audio is automatically generated including dialogue and sound effects
- The model has "director thinking" capability for automatic camera switching
- Iterative refinement is normal for optimal results - Iterative refinement is normal for optimal results

117
skills/public/video-generation/scripts/generate.py Executable file → Normal file
View File

@ -1,83 +1,75 @@
import base64
import os import os
import time import time
from typing import List
import requests import requests
from dotenv import load_dotenv
load_dotenv()
def generate_video( def generate_video(
prompt_file: str, prompt_file: str,
reference_images: List[str], reference_images: list[str],
output_file: str, output_file: str,
aspect_ratio: str = "16:9", aspect_ratio: str = "16:9",
duration: str = "5",
) -> str: ) -> str:
with open(prompt_file, "r", encoding="utf-8") as f: with open(prompt_file, "r", encoding="utf-8") as f:
prompt = f.read() prompt = f.read()
referenceImages = []
api_key = os.getenv("RUNNINGHUB_API_KEY") i = 0
if not api_key: json = {
return "RUNNINGHUB_API_KEY is not set" "instances": [{"prompt": prompt}],
json_data = {
"prompt": prompt,
"style": "general",
"aspectRatio": aspect_ratio,
"resolution": "720p",
"duration": duration,
"audio": True
} }
for reference_image in reference_images:
i += 1
with open(reference_image, "rb") as f:
image_b64 = base64.b64encode(f.read()).decode("utf-8")
referenceImages.append(
{
"image": {"mimeType": "image/jpeg", "bytesBase64Encoded": image_b64},
"referenceType": "asset",
}
)
if i > 0:
json["instances"][0]["referenceImages"] = referenceImages
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
return "GEMINI_API_KEY is not set"
response = requests.post( response = requests.post(
"https://www.runninghub.cn/openapi/v2/vidu/text-to-video-q3-turbo", "https://generativelanguage.googleapis.com/v1beta/models/veo-3.1-generate-preview:predictLongRunning",
headers={ headers={
"Authorization": f"Bearer {api_key}", "x-goog-api-key": api_key,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
json=json_data, json=json,
) )
json = response.json()
response_json = response.json() operation_name = json["name"]
if "taskId" not in response_json:
return f"Failed to submit task: {response_json}"
task_id = response_json["taskId"]
while True: while True:
response = requests.post( response = requests.get(
"https://www.runninghub.cn/openapi/v2/query", f"https://generativelanguage.googleapis.com/v1beta/{operation_name}",
headers={ headers={
"Authorization": f"Bearer {api_key}", "x-goog-api-key": api_key,
"Content-Type": "application/json",
}, },
json={"taskId": task_id},
) )
json = response.json()
response_json = response.json() if json.get("done", False):
status = response_json.get("status") sample = json["response"]["generateVideoResponse"]["generatedSamples"][0]
url = sample["video"]["uri"]
if status == "SUCCESS": download(url, output_file)
results = response_json.get("results", []) break
if results and len(results) > 0:
url = results[0].get("url")
if url:
download(url, output_file)
break
elif status == "FAILED":
error_message = response_json.get("errorMessage", "Unknown error")
return f"Video generation failed: {error_message}"
time.sleep(3) time.sleep(3)
return f"The video has been generated successfully to {output_file}" return f"The video has been generated successfully to {output_file}"
def download(url: str, output_file: str): def download(url: str, output_file: str):
response = requests.get(url) api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
return "GEMINI_API_KEY is not set"
response = requests.get(
url,
headers={
"x-goog-api-key": api_key,
},
)
with open(output_file, "wb") as f: with open(output_file, "wb") as f:
f.write(response.content) f.write(response.content)
@ -85,28 +77,28 @@ def download(url: str, output_file: str):
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
parser = argparse.ArgumentParser(description="Generate videos using RunningHub API") parser = argparse.ArgumentParser(description="Generate videos using Gemini API")
parser.add_argument( parser.add_argument(
"--prompt-file", "--prompt-file",
required=True, required=True,
help="Absolute path to JSON prompt file", help="Absolute path to JSON prompt file",
) )
parser.add_argument(
"--reference-images",
nargs="*",
default=[],
help="Absolute paths to reference images (space-separated)",
)
parser.add_argument( parser.add_argument(
"--output-file", "--output-file",
required=True, required=True,
help="Output path for generated video", help="Output path for generated image",
) )
parser.add_argument( parser.add_argument(
"--aspect-ratio", "--aspect-ratio",
required=False, required=False,
default="16:9", default="16:9",
help="Aspect ratio of the generated video", help="Aspect ratio of the generated image",
)
parser.add_argument(
"--duration",
required=False,
default="5",
help="Duration of the generated video in seconds (1-16)",
) )
args = parser.parse_args() args = parser.parse_args()
@ -115,10 +107,9 @@ if __name__ == "__main__":
print( print(
generate_video( generate_video(
args.prompt_file, args.prompt_file,
[], args.reference_images,
args.output_file, args.output_file,
args.aspect_ratio, args.aspect_ratio,
args.duration,
) )
) )
except Exception as e: except Exception as e: