const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,PUT,POST,DELETE,OPTIONS", "Access-Control-Allow-Headers": "*" }; const BILI_API = "https://api.bilibili.com"; // 通用B站请求封装 async function biliProxy(url, sessdata) { const headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://www.bilibili.com/", "Cookie": `SESSDATA=${sessdata}` }; const resp = await fetch(url, { headers }); return await resp.json(); } // 获取视频完整字幕文本 async function getBiliSubtitle(bvid, sessdata) { const viewUrl = `${BILI_API}/x/web-interface/view?bvid=${bvid}`; const viewData = await biliProxy(viewUrl, sessdata); if (viewData.code !== 0) return null; const cid = viewData.data.cid; const subUrl = `${BILI_API}/x/player/v2?cid=${cid}&bvid=${bvid}`; const subData = await biliProxy(subUrl, sessdata); if (!subData.data?.subtitle?.subtitles?.length) return null; const subUrlRaw = subData.data.subtitle.subtitles[0].subtitle_url; const subResp = await fetch(subUrlRaw); const subJson = await subResp.json(); let fullText = ""; subJson.body.forEach(item => fullText += item.content + "\n"); return fullText; } // AI结构化解析字幕:食材调料 + 分步做法(强化固定格式,减少错乱) async function parseRecipeByAI(env, text) { const prompt = ` 你是美食菜谱提取助手,严格按照下方固定格式输出,禁止增加额外解释、禁止编造不存在的用料和步骤。 从字幕原文提取:食材、调料、用量、制作步骤。 格式模板(严格遵守): 【食材&调料清单】 - 条目1 - 条目2 【制作步骤】 第1步: 第2步: 第3步: 字幕原文: """${text}""" `; const res = await env.AI.run("@cf/meta/llama-3.1-8b-instruct", { messages: [{ role: "user", content: prompt }] }); return res.response; } async function handle(request, env) { const url = new URL(request.url); const path = url.pathname; // 基础菜谱CRUD接口 if (path === "/ping") { return new Response(JSON.stringify({ ok: true, msg: "pong" }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); } if (path === "/list") { const raw = await env.RECIPE_KV.get("recipe_data"); const list = raw ? JSON.parse(raw) : []; return new Response(JSON.stringify(list), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); } if (path === "/save" && request.method === "POST") { const item = await request.json(); const raw = await env.RECIPE_KV.get("recipe_data"); const all = raw ? JSON.parse(raw) : []; const idx = all.findIndex(x => x.id === item.id); idx >= 0 ? all[idx] = item : all.push(item); await env.RECIPE_KV.put("recipe_data", JSON.stringify(all)); return new Response(JSON.stringify({ ok: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); } if (path === "/delete" && request.method === "DELETE") { const id = url.searchParams.get("id"); const raw = await env.RECIPE_KV.get("recipe_data"); const all = raw ? JSON.parse(raw) : []; const filtered = all.filter(x => x.id !== id); await env.RECIPE_KV.put("recipe_data", JSON.stringify(filtered)); return new Response(JSON.stringify({ ok: true }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); } // B站收藏夹列表 if (path === "/bili/folders") { const mid = url.searchParams.get("mid"); const sessdata = url.searchParams.get("sessdata"); if (!mid || !sessdata) { return new Response(JSON.stringify({ code: -1, msg: "缺少mid或sessdata参数" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }); } const apiUrl = `${BILI_API}/x/v3/fav/folder/created/list-all?up_mid=${mid}`; const resData = await biliProxy(apiUrl, sessdata); return new Response(JSON.stringify(resData), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); } // 收藏夹视频分页接口 if (path === "/bili/resources") { const media_id = url.searchParams.get("media_id"); const sessdata = url.searchParams.get("sessdata"); const pn = url.searchParams.get("pn") || "1"; const ps = 20; if (!media_id || !sessdata) { return new Response(JSON.stringify({ code: -1, msg: "缺少media_id或sessdata参数" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }); } const apiUrl = `${BILI_API}/x/v3/fav/resource/list?media_id=${media_id}&pn=${pn}&ps=${ps}&order=mtime`; const resData = await biliProxy(apiUrl, sessdata); return new Response(JSON.stringify(resData), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); } // 单视频字幕抓取 + AI菜谱解析 if (path === "/bili/parse-recipe") { const bvid = url.searchParams.get("bvid"); const sessdata = url.searchParams.get("sessdata"); if (!bvid || !sessdata) { return new Response(JSON.stringify({ code: -1, msg: "缺少bvid或sessdata" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }); } try { const subText = await getBiliSubtitle(bvid, sessdata); if (!subText) { return new Response(JSON.stringify({ code: 0, hasSub: false, msg: "该视频无可用字幕,无法自动拆分菜谱" }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); } const aiResult = await parseRecipeByAI(env, subText); return new Response(JSON.stringify({ code: 0, hasSub: true, content: aiResult }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); } catch (err) { return new Response(JSON.stringify({ code: -2, msg: "AI解析失败:" + err.message }), { headers: { ...corsHeaders, "Content-Type": "application/json" } }); } } return new Response("Not found", { status: 404, headers: corsHeaders }); } export default { async fetch(request, env, ctx) { if (request.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); return await handle(request, env); } };