1. 从“今晚看什么”到一条能跑通的推荐链路“今晚看什么”这个问题我和朋友每次都要纠结半小时。后来我想不如做个电影推荐系统输入几个候选片名转盘随机选一部选中后自动拉取这部电影的详情海报、评分、简介。听起来简单但真动手会碰到两个现实问题一是前端交互和转盘动画要写不少代码二是电影数据从哪来、推荐逻辑放哪。我的做法是把职责拆开Cursor 负责写 Express React 的工程代码Coze 负责推荐逻辑和数据获取。Coze 的可视化工作流可以挂载电影数据插件把“输入片名 → 查询电影信息 → 返回结构化结果”这条链路做成一个可发布的 APICursor 则帮我把 Express 路由骨架、React 页面组件、接口联调这些重复劳动快速生成出来。这套组合适合想快速验证想法、又不想从零手写全部代码的开发者尤其是对 React 和 Express 有基础但不熟练的初中级前端。整条链路的目标很明确本地起一个 Express 服务暴露/api/movie接口内部去调 Coze 的工作流 APIReact 前端负责输入片名、渲染转盘、转动后把选中的片名发给后端拿到电影详情展示在页面上。下面按“前置准备 → 可复制配置 → 验证请求 → 排错”的顺序走一遍你可以直接跟着敲。2. 前置准备Coze 工作流与 TaoToken 接入配置先说 Coze 侧。进入 Coze 后创建应用选空白应用填名称和描述进入后会看到流程图编辑界面。新建工作流填基本信息系统会生成“开始”和“结束”两个默认节点。在插件市场搜索电影数据相关插件并添加然后把“开始 → 电影信息节点 → 结束”连起来。参数配置上把开始节点的参数名改成name电影信息节点的输入选择开始节点的name结束节点参数类型设为 object 数组把电影节点的输出赋给它。试运行没问题后发布发布时选择 API如果没有个人令牌需要先创建一个并保存好。发布后官方会做一次快速审核审核通过进入 API 测试阶段系统会展示两个 IDworkflow_id 和 app_id这两个 ID 和令牌都要记下来后面 Express 里要用。这里有个容易忽略的点令牌和 ID 属于敏感信息绝对不能写进前端代码。前端一旦暴露令牌任何人都能拿你的额度去调接口。所以正确做法是让 Express 做一层安全隔离令牌只存在后端的环境变量里前端只跟自己的 Express 接口打交道。如果你在接入过程中需要统一管理模型调用和密钥可以用 TaoToken 的控制台来集中配置。模型对话入口在 https://taotoken.net/api API Keys 管理在 https://taotoken.net/api-keys 接入文档在 https://taotoken.net/doc 。把密钥放在后端环境变量、通过服务端转发请求是这套架构里必须坚持的原则。3. 可复制配置Express 路由骨架与 React 页面组件3.1 Express 后端环境变量与路由先在项目下建Server/目录初始化 Express并创建.env文件放环境信息# Server/.env COZE_TOKEN你的Coze个人令牌 COZE_WORKFLOW_ID你的workflow_id COZE_APP_ID你的app_id PORT3001安装依赖cd Server npm init -y npm install express cors dotenv axios下面是 Express 路由骨架核心是把 Coze 的返回结果解析出来只取第一条相关数据// Server/index.js require(dotenv).config(); const express require(express); const cors require(cors); const axios require(axios); const app express(); app.use(cors()); app.use(express.json()); app.post(/api/movie, async (req, res) { const { name } req.body; if (!name) { return res.status(400).json({ error: 缺少片名参数 name }); } try { const response await axios.post( https://api.coze.cn/v1/workflow/run, { parameters: { name }, workflow_id: process.env.COZE_WORKFLOW_ID, app_id: process.env.COZE_APP_ID, }, { headers: { Authorization: Bearer ${process.env.COZE_TOKEN}, Content-Type: application/json, }, timeout: 15000, } ); const raw response.data; if (raw.code ! 0) { return res.status(502).json({ error: Coze 返回异常, detail: raw.msg }); } // data 是字符串需要二次解析 const parsed JSON.parse(raw.data); const list parsed.output || []; const first list[0] || null; if (!first) { return res.status(404).json({ error: 未找到相关电影 }); } res.json({ name: first.name, desc: first.desc, picurl: first.picurl, rate: first.rate, star: first.star, subtxt: first.subtxt, }); } catch (err) { console.error(请求 Coze 失败:, err.message); res.status(500).json({ error: 服务端请求失败, detail: err.message }); } }); const port process.env.PORT || 3001; app.listen(port, () { console.log(Express 服务已启动: http://localhost:${port}); });注意raw.data是一个 JSON 字符串必须再JSON.parse一次才能拿到output数组这是很多人第一次接入时最容易卡住的地方。3.2 React 前端转盘与结果展示前端用 Cursor 生成 React 项目后核心是两个部分转盘组件和结果展示。下面是一个精简的页面组件包含输入、转盘转动和调用后端// src/App.jsx import { useState } from react; export default function App() { const [input, setInput] useState(); const [movies, setMovies] useState([]); const [selected, setSelected] useState(null); const [loading, setLoading] useState(false); const handleSpin async () { const list input.split(\n).map((s) s.trim()).filter(Boolean); if (list.length 0) return; setMovies(list); const pick list[Math.floor(Math.random() * list.length)]; setLoading(true); try { const res await fetch(http://localhost:3001/api/movie, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name: pick }), }); const data await res.json(); setSelected(data); } catch (e) { setSelected({ error: 请求失败请检查后端是否启动 }); } finally { setLoading(false); } }; return ( div style{{ maxWidth: 720, margin: 40px auto, fontFamily: sans-serif }} h2电影转盘/h2 textarea rows{6} style{{ width: 100% }} placeholder每行输入一个电影名 value{input} onChange{(e) setInput(e.target.value)} / button onClick{handleSpin} disabled{loading} style{{ marginTop: 12 }} {loading ? 转动中... : 开始转动} /button {selected !selected.error ( div style{{ marginTop: 24, display: flex, gap: 16 }} img src{selected.picurl} alt{selected.name} width{160} / div h3{selected.name}/h3 p评分{selected.rate ?? 暂无}/p p{selected.subtxt}/p p{selected.desc}/p /div /div )} {selected?.error p style{{ color: red }}{selected.error}/p} /div ); }转盘动画可以后续用 CSS transform 或 canvas 补上先把数据链路跑通更重要。4. 验证请求本地启动与接口联调先起后端cd Server node index.js # 输出Express 服务已启动: http://localhost:3001用 curl 直接验证接口确认 Coze 链路通了curl -X POST http://localhost:3001/api/movie \ -H Content-Type: application/json \ -d {name:千与千寻}正常会返回类似{ name: 千与千寻, desc: 千寻和爸爸妈妈一同驱车前往新家..., picurl: https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2557573348.jpg, rate: 9.4, star: 4.7, subtxt: 原名:千と千尋の神隠し / 宫崎骏 / 柊瑠美 / 2001 }再起前端npm run dev # 打开 http://localhost:5173在文本框里每行输入一个片名点“开始转动”页面下方应出现海报和详情。如果 curl 通了但前端报跨域检查 Express 是否加了cors()如果前端请求地址写错确认端口是 3001 而不是 5173。5. 本篇常见错排查报错一JSON.parse抛Unexpected token。原因通常是直接把response.data当对象用了。Coze 返回的data字段是字符串必须先JSON.parse(raw.data)再取output。如果解析后output为空数组说明插件没查到数据换一个更常见的片名再试。报错二401 或token校验失败。检查.env里的COZE_TOKEN是否有多余空格或换行Bearer后面要有一个空格。另外确认令牌没有过期重新生成后记得重启 Express因为dotenv只在启动时读取一次。报错三workflow_id或app_id填错。这两个 ID 在 Coze API 测试页面展示容易和空间 ID 混淆。如果返回code非 0先打印raw.msg看具体原因多数是 ID 不匹配或工作流未发布。报错四前端拿不到数据但后端日志正常。大概率是 CORS 或端口问题。确认app.use(cors())在路由之前注册确认前端 fetch 的地址是http://localhost:3001而不是相对路径。报错五Cursor 生成的代码改几次都不对。我试过把报错信息和相关代码片段整理清楚再交给 Cursor比反复说“还是不行”有效得多。如果某个逻辑始终绕不过去可以把代码贴到模型对话里让它先分析思路再回到 Cursor 迭代修正工具接力往往比死磕一个工具快。6. 把推荐链路接到你的日常开发里这套系统的价值不只是“选电影”而是验证了一种分工方式Coze 管逻辑和数据Cursor 管工程代码Express 做安全隔离层。同样的结构可以平移到选餐厅、选书、选周末活动只要把 Coze 工作流里的插件换掉、参数名改一下前端几乎不用大动。如果你打算长期做这类小工具建议把模型调用和密钥管理统一收口避免每个项目都散落一份配置。模型对话和 Coding Plan 的入口在 https://taotoken.net/api API Keys 在 https://taotoken.net/api-keys 接入文档在 https://taotoken.net/doc Claude Code 相关配置可以参考 https://taotoken.net/claude-code 。先把/api/movie这条链路跑通再往上加转盘动画、随机推荐、多平台数据聚合迭代起来会顺很多。