DragonAPI in Magpie
把 DragonAPI 接进 magpie:模型广场、余额、用量,外加一个能按分组建钥匙的本地界面。
当前版本 0.3.0。只做供应商插件 + 只读看板 + 密钥管理,不做「页面里点充值」。
它是什么
一个独立的 magpie 插件包,同时是两样东西:
| 身份 | 作用 |
|---|---|
| 供应商插件 | 把站点 /api/pricing 的目录折成 magpie 的模型表;sk- 钥匙直接读余额与逐条用量;视频模型走 magpie 的 /v1/videos 三件套 |
| 页面插件 + 本地服务 | magpie 的插件页是 connect-src 'none' 的沙箱,页面自己发不出请求。所以插件起一个只监听 127.0.0.1 的 HTTP 服务,页面导航过去,界面由服务提供 |
安装
包已发布到 npm:@fish-under-sea/magpie-dragonapi(npm · GitHub)。
两种装法,任选其一:
| 方式 | 怎么做 |
|---|---|
| Discover(推荐) | magpie 窗口 → Discover → Unofficial。这个仓库打了 magpie-plugin topic,package.json 里也有已提交的 magpie.page.file,所以会按 magpie.name / magpie.summary 列出卡片,点它安装 |
| 命令行 | magpie plugin add @fish-under-sea/magpie-dragonapi |
npm 拉不到时(内网、镜像抽风):到 Releases 下
fish-under-sea-magpie-dragonapi-0.3.0.tgz,解包成目录,再 magpie plugin add <那个目录> ——
plugin add 认 path,和从文件夹加插件是同一条路。
装好后:Settings → Plugins 里出现 DragonAPI 行(带一张页面),供应商列表里出现 DragonAPI。
只装不配也能看到模型广场——目录走站点公开的
/api/pricing,不需要任何凭据。 要看余额、读用量、建钥匙,才需要下面的钥匙与 PAT。
为什么不照抄 NearToken
NearToken 那套「页面里点充值、点连接」依赖两个本站没有的东西:
- 设备授权(
/api/dsh/device/*)——那是 neartoken.cc 的私有补丁,DragonAPI 没有。本站登录要过人机校验,PAT 只能在浏览器会话里创建,插件代不了。 - 请求头切分组(
x-neartoken-group)——DragonAPI 不读这个头。它的分组是钥匙的属性:TokenAuth里userGroup = tokenGroup,一把钥匙绑死一个分组。
所以这里的做法是:
- 分组 = 账号。一个分组一把钥匙,magpie 里就是多个账号;每个账号的模型清单只列它那一组的模型,宿主自己会把请求送到对的账号(上游 magpie 的
AccountServes保证)。 - 登录 = 贴钥匙,或用 PAT 让插件替你建钥匙。
三步接入
1. 在站点建一把钥匙
到 控制台 → API 密钥 新建,选好分组(分组决定能调哪些模型、按什么倍率计费),复制 sk- 开头的钥匙。
2. 在 magpie 里添加账号
打开供应商列表里的 DragonAPI,点「添加账号」,两种方式任选:
| 方式 | 做什么 | 适合 |
|---|---|---|
| DragonAPI API Key | 把 sk-… 粘进去 | 最常见;一把钥匙一个分组 |
| 用访问令牌按分组自动建钥匙 | 插件调站点 API 建一把新钥匙并直接绑成账号 | 想少跑一趟控制台 |
第二种要先在插件页面里填访问令牌(PAT),见下一节。它在 magpie 里表现为第二条登录方式,选完分组就自动建好并绑上。
3. 需要多个分组就重复第 2 步
每个分组加一个账号。magpie 的供应商编辑器里会列出多个账号,每个账号只带它那一组的模型。
访问令牌(PAT)
建钥匙、看钥匙列表、看账户资料都要 PAT。它只能在浏览器里创建:
- 登录控制台 → 头像 → 安全 → 访问令牌 → 新建;
- 勾选权限:
profile:read—— 读账户与可用分组(建钥匙必填)api_key:read—— 读钥匙列表api_key:write—— 建钥匙 / 删钥匙api_key:reveal—— 看钥匙完整明文usage:read—— 读用量流水wallet:read—— 读订阅、套餐与邀请(只读展示)
- 复制
nap-开头的令牌,粘到插件页面的「访问令牌」页。
旧版访问令牌已停用(站点返回
ACCESS_TOKEN_LEGACY_RETIRED),必须用新的nap-令牌。
PAT 存在本地:<magpie 配置目录>/dragonapi-state.json,权限 0600。它必须明文存——每次建钥匙都要用。文件里不存任何 sk- 钥匙明文,那些由 magpie 自己的 plugin-auth.json 保管。
保存 PAT 时插件会并行探四个只读端点(账户资料 / 钥匙列表 / 用量 / 订阅),每次最多等待 12 秒:
| 情况 | 行为 |
|---|---|
| 四个端点都说令牌无效(401/403) | 拒收,并说清是权限不够还是令牌失效 |
| 能用一个、缺另一个 | 照样存下,提示缺哪一格、哪些功能会失败(例如只缺 profile:read 时建钥匙会失败,但读用量照常) |
| 站点连不上 / 返回验证页 | 照样存下,标成「未验证」,稍后刷新即可再确认 |
| 写不进状态文件 | 直接报错并给出文件路径——不会出现「提示已保存、重启后消失」 |
界面里有什么
四个标签页:账户概览 / 模型广场 / 使用日志 / 访问令牌。整页限高——页头、标签、页脚固定不动, 超长内容在面板或卡片里滚动;账户页统一面板滚动,确保订阅、推荐和密钥始终可达。
| 页 | 内容 |
|---|---|
| 模型广场 | 左侧筛选(14 个分组带倍率与数量、11 家厂商带品牌图标)+ 右侧搜索、类型分段、分组横幅。一张卡 = 一个模型:站点把同一个模型在多个分组里各卖一份(189 行 / 125 个模型),卡片把它的全部分组都列出来(不省略、不折叠成「+N」),价格给跨分组的区间(如 $0.12–0.32/M)而不是只报最低价。卡内含供应商图标、名称/厂商、说明、计费方式、输入·输出·缓存价、分组、端点。纯展示,没有启用/停用开关——本站做不到 NearToken 那种按分组启停,所以这里不给假开关,也不伪造健康度数据 |
| 模型详情 | 点卡片上的「详情」打开(居中面板):价签 → 概览 → 计费方式(把 p/c/cr 系数、service_tier 倍率翻成中文)→ 每一档的实际单价 → 按用量单价 → 按分组定价(同型号横向比价,当前分组高亮)。站点原文的计费表达式收在折叠块里 |
| 账户概览 | 账户卡(余额、已用、绑定情况)+ 数据概览(日/周/月切换、消耗柱状图、按模型的调用与费用)+ 推荐计划(待确认 / 总收入 / 邀请,链接可复制、累计奖励可转入余额)+ API 密钥表格(名称、状态、密钥、额度、分组、绑定、时间、过期、操作) |
| 新建密钥 | 从「+ 创建 API 密钥」打开的居中弹窗:基本信息(名称、分组)与额度设置(无限配额开关、额度),样式与模型详情面板一致 |
| 使用日志 | 对标 NearToken 的详细程度:日/周/月 + 模型名 / 分组 / 类型 / 令牌名 / 请求 ID 筛选,用量·请求·Token 统计条(带隐私遮罩),明细 9 列(时间、分组、模型、费用、请求体大小、流式吞吐、Token/缓存、首字·耗时、详情),分页与「载入更多」 |
| 访问令牌 | 填入/清除 PAT,说明需要哪些权限,并显示实际验到的 scope;健康与诊断记录 |
预览时可以加 ?demo 用虚拟数据把四个页面都填满(订阅示例包括不限量、取消、到期与超额)。
仅设置 PAT、没有绑定 sk- 也能读取账户钱包与账户级用量。profile:read 用于账户资料与钱包余额,usage:read 用于账户日志;有 PAT 时默认只统计账户日志,不再叠加绑定钥匙日志。钱包余额与钥匙额度分别展示,不求和。
账户概览内置只读「订阅套餐」:我的订阅、到期、下次重置、额度与进度,以及站点返回的套餐列表。需要 wallet:read,不购买、不支付、不修改扣费偏好。套餐标价保留原币,quota 按站点参数换算。日志只取最近一批,页面会明确标注不完整,不能当作全量账单。
价格怎么算
站点有两种计费,插件分开处理,算不准就明说「价格未知」,不编数字:
| 计费方式 | 怎么折 | 可信度 |
|---|---|---|
| 倍率计费 | 模型倍率 × 分组倍率 × 2 美元/百万 token | 准 |
| 表达式计费(对话) | 从 billing_expr 取 tier(...) 里的 p / c / cr / cc 系数 | 准(多档时标注是哪一档) |
| 表达式计费(视频) | 取 u("seconds") / u("resolution") 的单价,给区间 | 区间价,实际以控制台为准 |
| 解析不出来 | 显示「价格未知」+ 原因 | —— |
auto 分组不算单价——它的费率取决于命中的通道,站点自己也这么说。
已知限制
| 限制 | 原因 | 影响 |
|---|---|---|
| 页面里不能直接绑账号 | 页面跑在沙箱里,写不了 magpie 的账号表 | 绑定只能走 magpie 的「添加账号」;插件用第二条登录方式把「建钥匙 + 绑定」合成一步 |
| 没有设备授权 | 站点没有这个端点 | 每次都要贴钥匙,或先用 PAT 让插件建 |
| 一把钥匙一个分组 | 分组是钥匙的属性 | 多分组 = 多账号,账号列表会变长 |
| 视频按秒计费只能给区间 | 价格随分辨率/规格变 | 实际扣费以控制台为准 |
priceRate 未验证 | 站点前端还有一个「充值价系数」,拿不到登录态确认不了 | 单价可能整体差一个系数;流水里的实际费用不受影响 |
| 独立服务拿不到绑定钥匙明文 | 钥匙只在插件进程内存里 | 钥匙额度显示「—」;已设置 PAT 的账户钱包与用量仍可读取 |
独立启动(插件宿主起不来时)
node serve.mjs
会在 127.0.0.1:3517 起同一个服务,界面可用(绑定钥匙额度除外;PAT 账户钱包和日志可读取)。如果插件已经在跑,它会提示并退出,不会抢端口。
开发
node --test "tests/*.test.mjs"
测试用站点 2026-11-10 的真实快照(证据-站点定价快照-2026-11-10.json)当夹具,站点改了写法就会被发现。
本地实时预览(改界面时用这个)
node dev.mjs
node dev.mjs --sandbox
node dev.mjs --upstream http://127.0.0.1:3517
改 ui/ 里任何文件(html / css / js)都会自动整页刷新,不需要装 magpie,也不用重装插件。
它起两个进程内的服务:内部服务(3672)是真的 lib/service.mjs,带真实站点目录与 /api/*;
开发服务(3671)只负责发静态文件、注入热刷新脚本、把 /api/* 反代过去。所以界面走的
还是生产那条请求路径,只是文件每次从磁盘现读。
两种模式都要看一遍,尤其是改请求、存储、导航的时候:
| 模式 | 地址 | 什么时候用 |
|---|---|---|
| 顶层直开 | /?theme=dark | 日常改样式最快,devtools 干净 |
| 沙箱 iframe | /__sandbox?theme=dark | 和 magpie 里一致:sandbox="allow-scripts",安全源不透明(localStorage 会抛 SecurityError) |
这个差异造成过真实事故:顶层一切正常,装进 magpie 后所有接口都「连不上本地服务」——
不透明源里相对路径的 fetch 会被判 Failed to fetch。沙箱模式就是为了提前看见这类问题。
默认访问 http://127.0.0.1:3671/?theme=dark。默认独立模式写 .dev-state/(含 PAT,已进 .gitignore),与插件实际状态分离。只有 --upstream 模式代理到指定插件服务,才操作那个服务实际使用的状态;该模式不启动内部服务。
Node 22 若不支持 npm 脚本的测试隔离参数,可直接运行:
node --test "tests/*.test.mjs"
目录结构:
index.mjs 供应商插件入口:config / auth / provider.models + 起本地服务
lib/site.mjs 站点客户端(sk- 与 PAT 两条通道分开,错误带 status/kind)
lib/pricing.mjs 两种计费规则的解析与降级;详情页用的 prices / multipliers / usageRows
lib/catalog.mjs /api/pricing → magpie 模型表(分组×模型)
lib/catalog-source.mjs 目录的唯一所有者:缓存、条件请求、刷新、健康记录
lib/status-source.mjs 站点参数的唯一所有者:缓存与降级
lib/usage.mjs 日志解析、汇总、窗口
lib/store.mjs 本地状态(PAT、账号元数据),原子写 0600,写失败返回结构化结果
lib/service.mjs 本地 HTTP 服务 + 全部 API(只管呈现,不缓存)
page/ 带路页(把自己导航到本地服务)
ui/ 真正的界面
ui/model-agg.js 跨分组聚合(一张卡 = 一个模型):纯函数、可单测
ui/vendor-icons/ 厂商品牌图标(来自同期 magpie-neartoken 仓库,MIT)
serve.mjs 独立启动
dev.mjs 本地实时预览(不打包进 npm)
两个「源」为什么单独存在
目录和站点参数各有一个 *-source.mjs,是因为它们曾经各被缓存了两份(插件一份、服务一份),
结果页面点「刷新」刷的是另一份缓存,按钮点了没反应。现在只有一处持有缓存,
lib/service.mjs 通过注入拿到它们,tests/refresh.test.mjs 钉住这条链路。
许可
MIT