ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

做 Siri AI 跨设备对话,TaoToken 放在服务端入口

做 Siri AI 跨设备对话,TaoToken 放在服务端入口 1. 跨端 Siri 对话服务端模拟先解决 401 invalid_api_key 与 Base URL 拼接Apple 把新一代 Apple Intelligence 与重构后的 Siri AI 推到英文测试版跨设备对话成为后端工程师很容易被问到的一类场景手机发起、平板接力、车机继续服务端要维护同一 session 的上下文还要把模型调用收敛到一个可控入口。我这边在本地用 Node Express 模拟时第一轮就撞上401 invalid_api_key日志里同时夹着404 path_not_found。根因不是业务代码而是模型入口没有统一Key 从旧脚本里复制、Base URL 又被手写成了带斜杠的旧地址。后来把服务端入口改成从 TaoToken 官网获取 Key并固定配置Base URL https://taotoken.net/api跨设备请求才稳定下来。官网入口可以先从这里进去https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentsiri_server_entry 。这篇文章按跨端后端工程师的视角给出三段东西第一服务端入口如何拿 Key、如何配置 Base URL第二设备注册、会话路由、模型调用的可运行代码第三跨设备请求样例与响应日志以及 Claude Code、Codex、CC Switch 的本地调试配置。不会把 Key 放到客户端也不会让 iPhone、iPad 直接持有模型凭证。先说结论跨设备 Siri 对话的服务端模拟本质上是一个带会话状态的模型网关。它至少要处理四件事设备注册deviceId绑定到sessionId知道哪些设备属于同一个对话。会话路由同一sessionId下任何设备发起的请求都能拿到共享上下文。模型调用统一走 TaoToken 的 Base URLKey 只存在服务端。日志追踪每次请求有traceId能串起手机、平板、车机之间的切换。最容易踩的坑不是业务逻辑而是配置。下面从 Key 和 Base URL 开始。2. 在 TaoToken 官网创建 Key服务端入口只保留一个模型出口第一步打开 TaoToken 官网并进入控制台https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentsiri_create_key 。登录后进入 API Keys 页面创建 Key。文中统一用YOUR_API_KEY占位不要把自己的真实 Key 提交到仓库也不要把 Key 下发给 iOS、Android、车机客户端。服务端环境变量建议只保留三个核心项export TAOTOKEN_API_KEYYOUR_API_KEY export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_MODEL按控制台可用模型填写 export PORT8787其中TAOTOKEN_API_KEY从 TaoToken 控制台创建服务端专用。TAOTOKEN_BASE_URL固定为https://taotoken.net/api不要手写完整请求路径。TAOTOKEN_MODEL按控制台当前可用模型填写不要硬编码到业务代码里。为什么强调“服务端入口只保留一个模型出口”因为跨设备对话里设备类型多、网络环境杂、系统版本不一致。如果每个端各自持有 Key、各自拼接 Base URL后面会出现非常难查的问题iPhone 上正常iPad 上 401车机上 404最后发现是某个端把 Base URL 写成了带/v1的旧地址。统一走服务端网关后客户端只拿短期 session token模型 Key 只在网关层出现。建议服务端配置读取写成这样const config { apiKey: process.env.TAOTOKEN_API_KEY ?? YOUR_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL ?? https://taotoken.net/api, model: process.env.TAOTOKEN_MODEL ?? 按控制台可用模型填写, };如果你用 OpenAI 兼容 SDK也可以直接让 SDK 使用这个baseURL。关键是不要让业务代码里出现多个模型出口。一个服务端入口、一个 Base URL、一个 Key 来源排障成本会下降很多。3. 可运行的服务端入口设备注册、会话路由、模型调用三段式代码下面给一个最小可运行示例。技术栈是 TypeScript Express OpenAI 兼容 SDK模型调用统一走 TaoToken 的 Base URL。先安装依赖mkdir siri-gateway cd siri-gateway npm init -y npm i express openai npm i -D typescript tsx types/express types/node然后创建server.tsimport express from express; import OpenAI from openai; const app express(); app.use(express.json({ limit: 1mb })); const config { apiKey: process.env.TAOTOKEN_API_KEY ?? YOUR_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL ?? https://taotoken.net/api, model: process.env.TAOTOKEN_MODEL ?? 按控制台可用模型填写, }; const client new OpenAI({ apiKey: config.apiKey, baseURL: config.baseURL, }); type DeviceSession { sessionId: string; devices: Setstring; locale: string; contextSummary: string; updatedAt: number; }; const sessions new Mapstring, DeviceSession(); function getSession(sessionId: string): DeviceSession | undefined { const session sessions.get(sessionId); if (!session) return undefined; const ttlMs 30 * 60 * 1000; if (Date.now() - session.updatedAt ttlMs) { sessions.delete(sessionId); return undefined; } return session; } app.post(/v1/siri/register, (req, res) { const { sessionId, deviceId, locale } req.body ?? {}; if (!sessionId || !deviceId) { return res.status(400).json({ error: sessionId/deviceId required }); } const session getSession(sessionId) ?? { sessionId, devices: new Setstring(), locale: locale ?? en-US, contextSummary: , updatedAt: Date.now(), }; session.devices.add(deviceId); if (locale) session.locale locale; session.updatedAt Date.now(); sessions.set(sessionId, session); res.json({ ok: true, sessionId, devices: [...session.devices], locale: session.locale, }); }); app.post(/v1/siri/chat, async (req, res) { const { sessionId, deviceId, input, locale } req.body ?? {}; const session getSession(sessionId); if (!session) { return res.status(404).json({ error: session not found or expired }); } if (!session.devices.has(deviceId)) { return res.status(403).json({ error: device not bound to this session }); } const startedAt Date.now(); const traceId trace_${Date.now()}_${Math.random().toString(16).slice(2)}; try { const completion await client.chat.completions.create({ model: config.model, messages: [ { role: system, content: 你是跨设备对话服务端模拟器。当前 locale${locale ?? session.locale}。已有上下文摘要${session.contextSummary}, }, { role: user, content: input ?? }, ], temperature: 0.3, stream: false, }); const reply completion.choices[0]?.message?.content ?? ; session.contextSummary ${session.contextSummary}\n用户(${deviceId}): ${input}\n助手: ${reply}.slice(-4000); session.updatedAt Date.now(); res.json({ traceId, sessionId, deviceId, reply, latencyMs: Date.now() - startedAt, model: completion.model, usage: completion.usage, }); } catch (err: any) { res.status(502).json({ traceId, error: model_gateway_error, message: err?.message ?? unknown, hint: 检查 TAOTOKEN_API_KEY 与 TAOTOKEN_BASE_URLhttps://taotoken.net/api, }); } }); app.post(/v1/siri/chat/stream, async (req, res) { const { sessionId, deviceId, input, locale } req.body ?? {}; const session getSession(sessionId); if (!session) { return res.status(404).json({ error: session not found or expired }); } if (!session.devices.has(deviceId)) { return res.status(403).json({ error: device not bound to this session }); } const traceId trace_${Date.now()}_${Math.random().toString(16).slice(2)}; res.setHeader(Content-Type, text/event-stream; charsetutf-8); res.setHeader(Cache-Control, no-cache, no-transform); res.setHeader(Connection, keep-alive); res.setHeader(X-Accel-Buffering, no); const heartbeat setInterval(() { res.write(: heartbeat ${Date.now()}\n\n); }, 15000); try { const stream await client.chat.completions.create({ model: config.model, messages: [ { role: system, content: 跨设备对话服务端模拟。locale${locale ?? session.locale}。上下文摘要${session.contextSummary}, }, { role: user, content: input ?? }, ], stream: true, temperature: 0.3, }); let full ; for await (const chunk of stream) { const delta chunk.choices[0]?.delta?.content ?? ; if (!delta) continue; full delta; res.write(data: ${JSON.stringify({ traceId, sessionId, deviceId, delta })}\n\n); } session.contextSummary ${session.contextSummary}\n用户(${deviceId}): ${input}\n助手: ${full}.slice(-4000); session.updatedAt Date.now(); res.write(data: ${JSON.stringify({ traceId, done: true })}\n\n); res.write(data: [DONE]\n\n); } catch (err: any) { res.write(data: ${JSON.stringify({ traceId, error: model_gateway_error, message: err?.message })}\n\n); res.write(data: [DONE]\n\n); } finally { clearInterval(heartbeat); res.end(); } }); app.listen(Number(process.env.PORT ?? 8787), () { console.log(siri gateway listening on :${process.env.PORT ?? 8787}); });这段代码有三个关键点/v1/siri/register负责把设备绑定到同一个sessionId并记录默认locale。/v1/siri/chat是非流式对话返回完整回复、traceId、耗时和 token 使用量。/v1/siri/chat/stream是 SSE 流式对话适合手机、平板这类需要逐字显示的场景。注意模型调用只发生在服务端。客户端只传sessionId、deviceId、input、locale不接触TAOTOKEN_API_KEY。4. 跨设备请求样例与响应日志从 iPhone 到 iPad 的 trace_id 追踪启动服务npx tsx server.ts先注册 iPhonecurl -s -X POST http://localhost:8787/v1/siri/register \ -H Content-Type: application/json \ -d {sessionId:sess_10086,deviceId:iphone-15,locale:en-US}再注册 iPad 到同一个 sessioncurl -s -X POST http://localhost:8787/v1/siri/register \ -H Content-Type: application/json \ -d {sessionId:sess_10086,deviceId:ipad-pro,locale:zh-CN}用 iPad 发起跨设备对话curl -s -X POST http://localhost:8787/v1/siri/chat \ -H Content-Type: application/json \ -d {sessionId:sess_10086,deviceId:ipad-pro,input:把刚才的会议纪要同步到手机,locale:zh-CN}典型响应日志{ traceId: trace_1710000000000_a1b2c3, sessionId: sess_10086, deviceId: ipad-pro, reply: 已根据当前会话上下文整理会议纪要并准备同步到同一 session 下的 iphone-15。, latencyMs: 842, model: 按控制台可用模型填写, usage: { prompt_tokens: 128, completion_tokens: 64, total_tokens: 192 } }流式请求样例curl -N -X POST http://localhost:8787/v1/siri/chat/stream \ -H Content-Type: application/json \ -d {sessionId:sess_10086,deviceId:iphone-15,input:继续刚才的日程安排,locale:zh-CN}流式响应片段data: {traceId:trace_1710000001000_d4e5f6,sessionId:sess_10086,deviceId:iphone-15,delta:已} data: {traceId:trace_1710000001000_d4e5f6,sessionId:sess_10086,deviceId:iphone-15,delta:继续} data: {traceId:trace_1710000001000_d4e5f6,sessionId:sess_10086,deviceId:iphone-15,delta:安排} data: {traceId:trace_1710000001000_d4e5f6,done:true} data: [DONE]排障时建议每个请求打印一行结构化日志{ traceId: trace_1710000001000_d4e5f6, sessionId: sess_10086, deviceId: iphone-15, locale: zh-CN, model: 按控制台可用模型填写, firstTokenMs: 310, totalMs: 1180, status: 200 }跨设备问题通常不是模型本身而是sessionId没传、deviceId没绑定、locale被最后一个设备覆盖。把traceId贯穿日志后你可以很快看出请求是从哪台设备发起、命中哪个 session、最终用哪个模型返回。5. Claude Code / Codex / CC Switch 三件套本地调试跨端入口的配置如果你在本地用 Claude Code 查看服务端日志、生成测试请求Claude Code 侧配置放在settings.json使用ANTHROPIC_*变量{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: YOUR_API_KEY } }注意Claude Code 用ANTHROPIC_*不要把这组变量写到 Codex 的配置里。两者配置体系不同混用会出现读不到 Key 或 Base URL 不生效的问题。Codex 用config.tomlmodel 按控制台可用模型填写 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEYCodex 侧读的是TAOTOKEN_API_KEY不是ANTHROPIC_API_KEY。本地可以先导出export TAOTOKEN_API_KEYYOUR_API_KEYCC Switch 三件套按下面填API Key: YOUR_API_KEY Base URL: https://taotoken.net/api 模型名: 按控制台可用模型填写如果你同时开 Claude Code、Codex 和服务端网关建议把配置文件分开Claude Codesettings.jsonCodexconfig.toml服务端.env或系统环境变量不要让一个变量名串到另一个工具。官网入口可以在这里再次打开核对https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentsiri_debug_config 。6. 排障与上生产401、404、429、流式中断、locale 漂移下面这张表可以直接当检查清单现象常见根因处理401 invalid_api_keyKey 错、漏 Bearer、环境变量未加载重新在 TaoToken 控制台创建 Key确认服务端读到TAOTOKEN_API_KEY404 path_not_foundBase URL 多写或漏写版本前缀路径拼接重复固定 Base URL 为https://taotoken.net/api不要手写完整 URL429 rate_limit多台设备共用一个 Key 且无服务端限流按deviceId sessionId做令牌桶失败重试带抖动流式响应卡住反向代理缓冲、防火墙切连接设置X-Accel-Buffering: no每 15s 发 SSE 心跳locale 漂移最后一个注册设备覆盖session.locale每轮请求优先用body.localesession.locale只做默认值session 串线sessionId复用或过期时间太长设备绑定校验 TTL建议 30 分钟无活动过期上下文无限增长每轮都把全文塞进 messages保留最近 N 轮 摘要摘要长度设上限先用一个小脚本确认 Key 和 Base URL 是否可用import OpenAI from openai; const client new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY ?? YOUR_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL ?? https://taotoken.net/api, }); const r await client.chat.completions.create({ model: process.env.TAOTOKEN_MODEL ?? 按控制台可用模型填写, messages: [{ role: user, content: ping }], }); console.log(r.choices[0]?.message?.content);如果这个脚本能通再排查服务端网关。如果这个脚本报 401优先检查 Key 是否复制完整、环境变量是否在当前 shell 生效。如果报 404优先检查 Base URL 是否被手写成了别的地址。上生产前再做四件事Key 放密钥管理服务不写进镜像、不写进前端。日志脱敏Authorization、TAOTOKEN_API_KEY不落盘。为/v1/siri/chat和/v1/siri/chat/stream加超时、重试、限流。把traceId、sessionId、deviceId、locale、model、firstTokenMs、totalMs做成统一日志字段。跨设备对话的难点不在“能不能调通模型”而在“多端切换后还能不能定位问题”。服务端入口统一后模型侧只暴露一个 Base URL 和一个 Key 来源排障范围会小很多。7. 下一步模型对话 → Coding Plan → 创建 Key → Claude Code 文档如果你准备把上面的服务端入口跑起来可以按这个顺序操作先试模型对话确认返回格式和模型可用性https://taotoken.net/models/detail/chat?utm_sourcetaotoken_aicg_blog_endutm_contentsiri_cross_device_chat如果要把这套入口接到编码工作流看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentsiri_cross_device_coding_plan然后创建自己的 Key替换文中的YOUR_API_KEYhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentsiri_cross_device_api_keysClaude Code 侧配置参考文档https://taotoken.net/doc/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentsiri_cross_device_claude_code最后再强调一次配置基线服务端入口使用TAOTOKEN_API_KEY和Base URL https://taotoken.net/apiClaude Code 使用ANTHROPIC_*Codex 使用config.tomlCC Switch 三件套分别填 API Key、Base URL、模型名。把这些固定下来跨设备 Siri 对话的服务端模拟就能从 401 排到可观测的traceId。
返回列表