84 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
type KeyItem = { id: string; name: string; tokenPrefix: string; scopes: string[]; revokedAt: string | null; expiresAt: string | null };
export default function TVstarPluginPage() {
const [keys, setKeys] = useState<KeyItem[]>([]);
const [createdToken, setCreatedToken] = useState("");
const [busy, setBusy] = useState(false);
const [origin, setOrigin] = useState("https://ai.yydd.ai");
useEffect(() => { setOrigin(window.location.origin); }, []);
const loadKeys = useCallback(async () => {
const response = await fetch("/api/user/access-keys", { cache: "no-store" });
const data = await response.json();
if (!response.ok || !data.success) throw new Error(data.error || "读取密钥失败");
setKeys(Array.isArray(data.data) ? data.data : []);
}, []);
useEffect(() => { void loadKeys().catch(error => toast.error(error.message)); }, [loadKeys]);
const createKey = async () => {
setBusy(true);
try {
const response = await fetch("/api/user/access-keys", { method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "TVstar MCP", scopes: ["canvas:read", "canvas:write"], expiresInDays: 90 }) });
const data = await response.json();
if (!response.ok || !data.success || !data.data?.token) throw new Error(data.error || "创建密钥失败");
setCreatedToken(data.data.token);
await loadKeys();
toast.success("密钥已创建;请立即复制,关闭后不会再次显示");
} catch (error) { toast.error(error instanceof Error ? error.message : "创建密钥失败"); }
finally { setBusy(false); }
};
const revokeKey = async (id: string) => {
if (!window.confirm("撤销后使用此密钥的 Agent 将失去画布访问权限,确定继续吗?")) return;
setBusy(true);
try {
const response = await fetch(`/api/user/access-keys/${id}`, { method: "DELETE" });
const data = await response.json();
if (!response.ok || !data.success) throw new Error(data.error || "撤销失败");
setCreatedToken("");
await loadKeys();
toast.success("密钥已撤销");
} catch (error) { toast.error(error instanceof Error ? error.message : "撤销失败"); }
finally { setBusy(false); }
};
const example = JSON.stringify({ mcpServers: { tvstar: { command: "node", args: ["/替换为解压目录/tvstar-mcp/mcp-server.mjs"],
env: { YEDIR_BASE_URL: origin, YEDIR_ACCESS_TOKEN: "粘贴新创建的 Access Key" } } } }, null, 2);
return <main className="min-h-screen bg-[#151515] px-4 py-10 text-white">
<div className="mx-auto max-w-4xl">
<p className="text-sm text-cyan-300">TVstar Plugin</p>
<h1 className="mt-2 text-3xl font-semibold">让 Agent 操作视频画布</h1>
<p className="mt-3 text-sm leading-7 text-white/60">当前插件提供画布列表、读取和有版本保护的节点操作。视频生成与合成仍由 TVstar 画布内的任务系统执行,MCP 暂未提供直接发起生成的工具。</p>
<div className="mt-8 grid gap-4 md:grid-cols-2">
<section className="rounded-2xl border border-white/10 bg-[#222] p-5">
<h2 className="font-medium">1. 下载并安装</h2>
<p className="mt-2 text-sm text-white/50">需要 Node.js 20 或更新版本。解压后在 tvstar-mcp 目录运行:</p>
<a className="mt-4 inline-block rounded-lg bg-white px-4 py-2 text-sm font-medium text-black" href="/api/tvstar/plugin/download">下载 TVstar MCP 安装包</a>
<pre className="mt-4 overflow-x-auto rounded-lg bg-black/40 p-3 text-xs">cd tvstar-mcp{"\n"}npm ci</pre>
</section>
<section className="rounded-2xl border border-white/10 bg-[#222] p-5">
<h2 className="font-medium">2. 创建画布 Access Key</h2>
<p className="mt-2 text-sm text-white/50">读取和修改画布需要对应权限。密钥只在创建时显示一次。</p>
<button className="mt-4 rounded-lg bg-white px-4 py-2 text-sm font-medium text-black disabled:opacity-50" disabled={busy} onClick={() => void createKey()}>{busy ? "处理中…" : "创建 Access Key"}</button>
{createdToken ? <div className="mt-3 break-all rounded-lg border border-amber-300/30 bg-amber-300/5 p-3 text-xs text-amber-100"><p>{createdToken}</p><button className="mt-2 underline" onClick={() => void navigator.clipboard.writeText(createdToken).then(() => toast.success("已复制"))}>复制密钥</button></div> : null}
</section>
</div>
<section className="mt-4 rounded-2xl border border-white/10 bg-[#222] p-5">
<h2 className="font-medium">3. 配置支持 STDIO MCP 的 Agent</h2>
<p className="mt-2 text-sm text-white/50">将安装包的绝对路径和密钥填入客户端配置。不同 Agent 的配置文件位置和外层字段可能不同,请以所用客户端的说明为准。</p>
<pre className="mt-4 overflow-x-auto rounded-lg bg-black/40 p-4 text-xs leading-6 text-cyan-100">{example}</pre>
<button className="mt-3 text-sm text-cyan-300 underline" onClick={() => void navigator.clipboard.writeText(example).then(() => toast.success("配置示例已复制"))}>复制配置示例</button>
</section>
<section className="mt-4 rounded-2xl border border-white/10 bg-[#222] p-5">
<h2 className="font-medium">已创建的 Access Key</h2>
<div className="mt-3 space-y-2">{keys.length ? keys.map(key => <div className="flex items-center justify-between gap-4 rounded-lg bg-black/20 p-3 text-sm" key={key.id}><span className="min-w-0 truncate">{key.name} · {key.tokenPrefix} · {key.revokedAt ? "已撤销" : "有效"}</span>{!key.revokedAt ? <button className="shrink-0 text-rose-300 disabled:opacity-40" disabled={busy} onClick={() => void revokeKey(key.id)}>撤销</button> : null}</div>) : <p className="text-sm text-white/40">暂无密钥</p>}</div>
</section>
</div>
</main>;
}