84 lines
5.9 KiB
TypeScript
84 lines
5.9 KiB
TypeScript
"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>;
|
||
}
|