opencode-agentrouter-auth 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,9 +2,20 @@
2
2
 
3
3
  [使用说明](<README.md>) · [优化设计与兼容边界](<docs/OPTIMIZATION.md>)
4
4
 
5
- 本页记录源码行为。版本号不代表对应版本已经在 npm 发布;安装前应核对实际可用版本。
5
+ 本页记录源码行为,当前版本为 **0.1.6**。npm 可用版本以官方 registry 查询为准;文档和源码更新不代表远程网关升级或模型验证成功。
6
6
 
7
- ## 0.1.5 — 当前源码
7
+ ## 0.1.6 — 2026-11-06
8
+
9
+ - 登录顺序保持 API Key 在前、GitHub 在后。API Key 无插件问题,只用宿主原生单输入;保存不代表验证。当前宿主 API `authorize` 不接收原生 key,因此不添加保存前验证、第二个 key 字段、verified 标记或状态徽章。
10
+ - GitHub 仅预先询问系统访问令牌(遮蔽输入),随后由用户浏览器授权、回填完整回调 URL,再查找已有模型 key。管理令牌不是模型 key;不是免令牌或全自动 OAuth。
11
+ - 首次登录移除端点、站点、请求头、用户 ID、用户名密码及 Cookie 问题。新账号默认 `agentrouter.org`;升级保留旧账号 key、site 和其他既有设置,不做自动迁移。
12
+ - 已有 key 查找兼容已知 `data.items` / `data` 数组形状;候选须启用(`status=1`)、非空、未掩码、未过期,按 key 去重后必须唯一。多个候选、无候选、未知响应或分页截断不确定时要求手动 API Key 恢复,不自动创建、轮换、重试,也不以空 key 完成 OAuth。
13
+ - loader 对缺失 key 或保留占位 key 在网络前返回本地 `400 agentrouter_missing_api_key`。
14
+ - 文档移除旧首次登录问题清单,区分保存、模型测试、账号余额与签到;宿主“已登录”不代表模型已验证。高级 metadata 仍可通过既有手动配置维护;插件选项仅支持布尔开关,没有新增 Cookie 设置页。
15
+ - npm 安装说明固定到 0.1.6,要求先查询官方 registry 并核对宿主实际加载版本;继续原本地目录注册的用户不要重复添加 npm 插件。本地原生回归 278 项 Node.js、2 项 Python 通过;离线验证不代表真实授权或模型已验证。
16
+ - 不改变限流、Responses 兼容、过滤或签到运行逻辑;独立 [本地签到工具](<LOCAL-CHECKIN.md>) 保持不变,不自动启用。
17
+
18
+ ## 0.1.5 — 已发布
8
19
 
9
20
  - 新增内部共享有界读取器:请求保持 **4 MiB / 2 秒**,JSON 响应诊断限制为 **64 KiB / 2 秒**。检查副本,不消费原正文;无法可靠读取或解析时放弃改写。
10
21
  - 在明确错误封套内新增有限的数字状态、错误码与繁忙短语分类;每层验证错误叶子,只继承合法数字状态,深度预算耗尽时透传。认证、权限、余额、配额及内容策略等业务原因优先,不误包装成暂时性故障。
@@ -18,6 +29,8 @@
18
29
 
19
30
  ## 0.1.4
20
31
 
32
+ 以下登录问答为历史行为,已由 0.1.6 的简化流程替代;不是当前版本的操作步骤。
33
+
21
34
  - GitHub 登录先选择站点,系统访问令牌使用遮蔽输入;签到续期默认跳过,仅主动配置时显示 GitHub Cookie 获取说明与本地格式校验。格式通过不代表线上有效,普通登录无需配置签到续期。
22
35
  - 修复完整普通历史消息带空 `logprobs: []` 时未能参与内联重放的问题;保留工具关联、非空或未知概率数据、引用注释、推理/加密及其他不透明状态。
23
36
  - 明确历史兼容边界:仅对已有完整内联内容的适用条目去除资源本地 ID,不恢复仅存在于其他上游资源的历史或加密状态,不把这一补充描述为跨资源问题的通用解法。
package/README.md CHANGED
@@ -5,11 +5,21 @@
5
5
  ## 文档导航
6
6
 
7
7
  - [优化设计与升级指南](<docs/OPTIMIZATION.md>):问题背景、与参考项目的取舍、兼容边界、升级/回滚与本地验证。
8
- - [更新日志](<CHANGELOG.md>):0.1.5 与 0.1.4 的版本变化。
8
+ - [更新日志](<CHANGELOG.md>):0.1.6 登录简化与历史版本的变化。
9
9
  - [本地签到说明](<LOCAL-CHECKIN.md>):独立签到工具的使用与安全边界。
10
10
 
11
11
  > 仓库源码更新不等于 npm 发布或你的网关已升级。请按升级指南自行验证;不要把真实 Cookie、访问令牌、私有词表或抓包提交到仓库。
12
12
 
13
+ ### 0.1.6:登录收敛
14
+
15
+ - 登录顺序保持 **API Key 在前、GitHub 在后**。API Key 只使用宿主原生的一个密钥输入框,保存不代表验证通过。
16
+ - GitHub 只预先询问系统访问令牌(遮蔽输入),随后由用户在浏览器授权、手动回填完整回调 URL,再查找已有模型密钥;**不是免令牌或全自动 OAuth**。
17
+ - 新账号默认使用 `agentrouter.org`;升级不改写旧账号保存的 key、站点或其他设置。首次登录不再询问站点、请求头、用户 ID、账号密码或 Cookie。
18
+ - 仅在已有密钥列表中存在唯一可用候选时保存;缺失、歧义或分页不完整时转为手动 API Key 恢复。不自动创建、轮换密钥或重试,不以空 key 完成 OAuth。
19
+ - loader 遇到缺失 key 或保留占位 key,会在发出网络请求前返回本地 `400 agentrouter_missing_api_key`。
20
+
21
+ > **本页登录流程适用于 0.1.6。** 下方命令固定该版本,安装前先查询 npm 官方 registry;源码版本号不等于已发布或你的网关已升级。限流、Responses 兼容、过滤与签到运行逻辑不在本次变更范围。
22
+
13
23
  ### 0.1.5 变更
14
24
 
15
25
  - JSON 错误诊断增加 **64 KiB / 2 秒** 上限;与请求改写共用不消费原始正文的限量读取模块。请求原有 **4 MiB / 2 秒** 边界不变。
@@ -21,7 +31,7 @@
21
31
 
22
32
  ### 0.1.4 变更
23
33
 
24
- - GitHub 登录先选站点,系统令牌遮蔽输入;签到续期默认跳过,主动配置时才显示 GitHub Cookie 获取说明和本地格式校验。
34
+ - 历史版本改进了系统令牌遮蔽输入和可选签到配置;0.1.6 已用下文的简化登录替代旧交互。
25
35
  - 修复完整历史消息含空 `logprobs: []` 时漏掉内联重放的情况,保留工具关联、非空概率数据、引用注释和不透明状态。
26
36
  - 无法恢复仅存在于其他上游资源的历史或加密状态;这是范围受限的兼容补充,并非所有跨资源错误的通用解法。
27
37
 
@@ -36,24 +46,27 @@
36
46
  包名为 [`opencode-agentrouter-auth`](<https://www.npmjs.com/package/opencode-agentrouter-auth>)。先在 npm 官方 registry 核对目标版本,再交给 **magpie 的插件管理器**安装;仅运行 `npm install` 不会自动把插件注册到 magpie:
37
47
 
38
48
  ```bash
39
- # 在有 Node.js / npm 的终端查询;本仓库当前版本为 0.1.5
40
- npm view opencode-agentrouter-auth@0.1.5 version --registry=https://registry.npmjs.org
49
+ # 在有 Node.js / npm 的终端查询目标版本;确认可用后再安装
50
+ npm view opencode-agentrouter-auth@0.1.6 version --registry=https://registry.npmjs.org
41
51
 
42
52
  # Docker:查询成功后安装,容器名按你的实际部署修改
43
- docker exec magpie magpie plugin add npm:opencode-agentrouter-auth@0.1.5
53
+ docker exec magpie magpie plugin add npm:opencode-agentrouter-auth@0.1.6
44
54
 
45
55
  # 或者,本地 / 原生进程部署
46
- magpie plugin add npm:opencode-agentrouter-auth@0.1.5
56
+ magpie plugin add npm:opencode-agentrouter-auth@0.1.6
47
57
  ```
48
58
 
49
59
  - Docker 与原生进程命令二选一。安装后用 `magpie plugin list --json`(Docker 中在命令前加 `docker exec magpie`)核对插件,再在面板检查 AgentRouter 的插件选项和登录状态。
50
60
  - 如果版本查询返回 `E404`,该版本在所查 registry 尚不可用;不要把源码版本号当成 npm 已发布证明,可改用下方源码安装。若仅宿主安装失败,可运行 `magpie plugin registry` 检查它实际使用的 registry;查询命令不会替你修改宿主设置。
51
61
  - 已用本地目录安装的用户,继续更新原目录即可;不要直接再添加一份 npm 插件。先备份并确认宿主如何关联插件路径、认证及选项,再决定是否迁移,避免重复注册或丢失原配置。
52
- - 固定 `@0.1.5` 便于复现;省略版本会让宿主选择版本,但并不等同于更新所有插件。`magpie plugin update` 则可能更新其他插件,不作为本插件的定点升级步骤。
62
+ - 固定 `@0.1.6` 便于复现;省略版本会让宿主选择版本,但并不等同于更新所有插件。`magpie plugin update` 则可能更新其他插件,不作为本插件的定点升级步骤。
53
63
  - 已通过 npm 安装的用户,升级或降级前须确认当前宿主的**单包替换方式以及认证/选项保留行为**;未确认前暂停操作,不直接重复执行 `plugin add`。旧版本也须经 npm 查询确认存在,不能假设仓库旧版本都已发布。下方[升级/回滚指南](<docs/OPTIMIZATION.md#11-升级与回退>)针对本地包/目录备份,不是已验证的 npm 定点降级流程。
54
64
 
55
65
  #### Docker 源码安装
56
- 将插件克隆至 magpie 配置卷中的本地插件目录:
66
+
67
+ **源码安装与 npm 安装二选一,不要重复注册。** 克隆后核对提交及包版本为预期版本,更新已有目录前先备份;最终以宿主实际加载版本为准,不能仅凭本地文件已更新判断升级成功。
68
+
69
+ 将插件源码放入 magpie 配置卷中的本地插件目录:
57
70
  ```bash
58
71
  # 假设 magpie 配置卷挂在宿主机 $VOL(按你的实际部署路径修改)
59
72
  VOL=/path/to/magpie/config # 替换为你自己的配置卷路径
@@ -74,40 +87,38 @@ magpie plugin add /path/to/agentrouter-magpie-plugin
74
87
 
75
88
  ---
76
89
 
77
- ### 2. 登录并绑定账号
90
+ ### 2. 登录并绑定账号(0.1.6)
78
91
 
79
- 运行交互式登录命令:
92
+ 以下流程适用于已加载 0.1.6 的宿主;仍运行 0.1.5 的用户会看到旧交互。新账号运行交互式登录命令(升级既有账号不要求重新登录):
80
93
  ```bash
81
94
  docker exec -it magpie magpie plugin login agentrouter
82
95
  # 非 Docker 环境直接运行:magpie plugin login agentrouter
83
96
  ```
84
97
 
85
- 根据需求选择以下两种登录方式之一:
86
-
87
- #### 方案 A:GitHub 授权登录(推荐,支持读取额度)
88
- 适合已绑定 GitHub 的 AgentRouter 账号,可自动获取 key、用户 ID 并读取实时余额:
89
- 1. **获取系统访问令牌**:登录 [AgentRouter 官网](https://agentrouter.org/),点击右上角头像进入「个人设置」→「访问令牌」,复制**系统访问令牌**(注意:**不是** `sk-…` API 密钥,也**不是** GitHub Personal Access Token)。
90
- 2. 选择 `用 GitHub 登录 AgentRouter(推荐)`,先选站点,再在遮蔽输入框粘贴系统访问令牌。
91
- 3. **普通登录选“跳过”签到续期配置**,不需要复制 GitHub Cookie。只有主动选择配置时才显示 Cookie 输入与获取说明;不愿提供也可留空跳过。
92
- 4. 随后生成 GitHub 授权 URL;浏览器未自动打开时,手动打开宿主提供的授权链接。完成授权后,浏览器会跳转至 `agentrouter.org/oauth/github/magpie?...`。
93
- > ⚠️ **跳转后显示「页面未找到」是正常现象**(站点在此特定子路由不会提早自动兑换授权码)。请直接从地址栏**完整复制 URL**(包含 `code=…`),不要刷新该回调页面。
94
- 5. 粘贴回登录交互,插件完成凭据保存。获取 Cookie 的步骤见下文;登录成功不等于已验证每日签到。
95
-
96
- #### 方案 B:API 密钥快速登录(最简,仅模型对话)
97
- 如果仅需调用模型进行聊天/编程,无需读取余额与签到:
98
- 1. **获取 API 密钥**:登录 AgentRouter,在「令牌」页面新建或复制已有密钥(`sk-…`)。
99
- 2. 在终端登录交互中选择 `AgentRouter API 密钥`。
100
- 3. 按照提示输入参数:
101
- - **请求端点**:直接按**回车留空**(默认 `https://agentrouter.org`,**切勿在此处填入 `sk-…` 密钥**)。
102
- - **站点**:回车默认选择 `agentrouter.org`。
103
- - 仅需对话时跳过可选字段,签到续期选“跳过”。主动选择配置后才输入 `githubCookie`(GitHub 登录态),并填写本站用户 ID;不能用 AgentRouter Cookie 代替,也不需要站点密码。
104
- - **API 密钥**:最后单独提示时,粘贴你的 `sk-…` 密钥。
98
+ 按宿主显示顺序选择:
99
+
100
+ #### 方案 A:API Key(最简,先列出)
101
+ 1. 登录 [AgentRouter 官网](<https://agentrouter.org/>),在「令牌」页面复制你要使用的**模型 API Key**。若尚无密钥,请自行在站点管理,插件不会代建。
102
+ 2. 选择 `API Key(推荐)`,只在**宿主原生密钥输入框**粘贴一次并保存;插件没有额外问题或第二个 key 字段。
103
+ 3. 新账号默认连接 `agentrouter.org`。保存后按下一节运行模型测试;**已保存 / 宿主显示已登录 ≠ 模型已验证可用**。
104
+
105
+ 当前宿主的 API `authorize` 不会收到原生输入框中的 key,插件无法在保存前验证它;本版本不新增 verified 标记或“已验证”状态徽章。仅有模型 key 时,余额和签到可能不可用,但不影响独立测试模型调用。
106
+
107
+ #### 方案 B:GitHub 授权(需要管理令牌及手动回调)
108
+ 适合已绑定 GitHub 的 AgentRouter 账号,流程为:**系统访问令牌 → 用户浏览器授权 → 完整手动回调 → 查找已有模型 key**。
109
+ 1. 在 AgentRouter「个人设置」→「访问令牌」复制**系统访问令牌**。它是管理凭据,**不是模型 API Key,也不是 GitHub Personal Access Token**;不能当作模型 key 使用。
110
+ 2. 选择 GitHub 登录方式,在唯一的插件预置问题中以遮蔽输入粘贴系统访问令牌。无需填写站点、请求头、用户 ID、账号密码或 Cookie。
111
+ 3. 打开宿主提供的授权链接,在浏览器完成 GitHub 授权。此流程需要你参与,不是免令牌或全自动 OAuth。
112
+ 4. 跳转后,从地址栏**完整复制回调 URL**并粘贴回宿主(保留 `code`、`state` 等全部参数)。回调页面可能显示“页面未找到”;不要因此刷新、删参数或只复制授权码,也不要把回调 URL 发给他人。
113
+ 5. 插件读取已有密钥列表,只接受唯一可用候选。候选须为启用状态(`status=1`)、非空、未掩码、未过期,重复 key 先去重。支持已知的 `data.items` 与 `data` 数组形状;多个候选、无候选、未知响应或分页截断导致结果不确定时,不会擅自选 key。
114
+
115
+ **恢复方式:** 按提示改选 API Key,将你明确选定的模型 key 填入宿主原生输入框。插件不自动创建或轮换密钥、不自动重试,也不会以空 key 宣告 OAuth 成功。即使凭据已保存,仍需模型测试;管理令牌或余额读取成功同样不证明模型可用。
105
116
 
106
117
  ---
107
118
 
108
119
  ### 3. 测试与验证
109
120
 
110
- 绑定完成后,执行以下命令验证状态:
121
+ 保存凭据后分别验证模型与账号数据;宿主“已登录”只说明认证记录已保存,不是模型可用性证明:
111
122
 
112
123
  ```bash
113
124
  # 1. 测试模型对话是否正常响应
@@ -117,7 +128,7 @@ docker exec magpie magpie provider test agentrouter
117
128
  # 2. 查看已识别并激活的模型列表
118
129
  docker exec magpie magpie provider models agentrouter
119
130
 
120
- # 3. 检查账号额度与用量读取
131
+ # 3. 可选:已配置账号数据凭据时检查额度与用量(不是模型验证)
121
132
  docker exec magpie magpie quota agentrouter
122
133
  ```
123
134
 
@@ -139,17 +150,23 @@ docker exec magpie magpie quota agentrouter
139
150
 
140
151
  ---
141
152
 
142
- ## 登录方式与需要填写的内容
153
+ ## 登录与高级配置边界
143
154
 
144
- | 方式 | 需要什么 | 能读取什么 |
155
+ | 方式(显示顺序) | 首次需要填写 | 保存后的边界 |
145
156
  |---|---|---|
146
- | **用 GitHub 登录**(推荐) | 先填系统访问令牌,再浏览器授权并复制回调地址 | 保存手填令牌,自动获取 API 密钥、session Cookie 和用户 ID;余额读取仍可能被 WAF 拦截 |
147
- | API 密钥 | `sk-…` | 对话、模型列表、累计消耗;不能推算剩余额度 |
148
- | API 密钥 + Cookie 或访问令牌 | Cookie 只填 Cookie 的值(`session=…`,多个用 `; ` 连接),同时填用户 ID;或有效访问令牌 + 用户 ID | 尝试读取当前余额;被 WAF 拦截时降级到累计消耗 |
149
- | API 密钥 + GitHub 登录态 | `githubCookie` 与本站用户 ID | 重新 GitHub OAuth 登录签到;会话过期/授权确认需人工处理 |
150
- | 站点用户名密码(旧版兼容) | 仅适用于仍开放密码登录的站点 | GitHub-only AgentRouter 用户留空 |
157
+ | **API Key** | 宿主原生单个模型 key 输入框 | 保存不验证;可测试对话、模型列表,不能据此推算余额 |
158
+ | **GitHub** | 系统访问令牌(遮蔽输入),浏览器授权后完整手动回调 | 查找唯一已有模型 key;管理令牌和宿主登录状态不等于模型验证 |
159
+
160
+ 新账号默认使用 `agentrouter.org`;升级不改写旧账号保存的 key、站点、请求头或其他设置。首次登录不再询问端点、站点、请求头、用户 ID、用户名密码或 Cookie。
161
+
162
+ 余额与签到的既有能力继续保留。需要高级配置时,先备份,再按下文 [手动配置格式](<#手动配置文件格式plugin-authjson>) 维护既有账号 metadata;这些不是额外登录方式或必填问题:
163
+
164
+ - `cookie` 或 `accessToken` + `userId`:尝试读取账号余额;访问令牌失效时仍可尝试已有有效 Cookie,均不保证通过 WAF。
165
+ - `githubCookie` + 本站 `userId`:可选签到续期,GitHub 会话过期或需要授权确认时仍须人工处理。
166
+ - `username` / `password`:仅兼容仍支持密码登录的旧站点;GitHub-only 用户无需填写。
167
+ - `endpoint` / `site` / `headers`:保留既有端点、站点与请求头能力。
151
168
 
152
- API 密钥登录时,Cookie 和访问令牌任选其一即可。GitHub 登录需要先填写同一 AgentRouter 账号的系统访问令牌(「个人设置 → 访问令牌」),不能为空;不是 `sk-…` API 密钥,也不是 GitHub Token。Cookie 和用户 ID 由 GitHub 授权自动获取。访问令牌失效时可以继续尝试有效 Cookie;两者都不能保证通过滑动验证码。
169
+ **当前插件选项仅支持布尔开关,没有新增 Cookie 设置页或任意文本选项。** 不要把凭据粘贴到布尔选项;高级字段只能通过已有宿主能力或下文的手动配置路径维护。
153
170
 
154
171
  ## 签到:GitHub 重新登录
155
172
 
@@ -158,8 +175,7 @@ AgentRouter 的每日额度由**重新登录**触发,余额查询成功不等
158
175
 
159
176
  ### 配置与安全边界
160
177
 
161
- 登录时先选择是否配置签到续期(`githubCookieMode`),默认优先选择跳过;仅选择配置才显示 **GitHub 登录 Cookie** 字段
162
- (metadata 字段名为 `githubCookie`)。支持 Cookie 值、单个 `Cookie:` 前缀、DevTools 表格和 Cookie JSON 数组。
178
+ 0.1.6 首次登录不收集签到 Cookie,也不会自动启用签到续期。以下是既有高级配置说明:仅在你明确需要并接受风险时,手动维护既有 metadata 的 **GitHub 登录 Cookie**(`githubCookie`)。支持 Cookie 值、单个 `Cookie:` 前缀、DevTools 表格和 Cookie JSON 数组。
163
179
  它与 AgentRouter 的 `cookie` 字段不同;推理 API key、系统访问令牌、GitHub PAT/Actions token 均不能替代。
164
180
  这是高权限凭据:只保存在你信任的网关服务器,像密码一样保护,不要发到聊天、工单、截图或 Git。
165
181
  远程部署时不是“只存在本机”。如果不愿把 GitHub 会话交给网关,则不要配置此功能。
@@ -168,12 +184,10 @@ AgentRouter 的每日额度由**重新登录**触发,余额查询成功不等
168
184
 
169
185
  1. 在自己的浏览器打开 [github.com](https://github.com/),登录与 AgentRouter 绑定的同一 GitHub 账号。
170
186
  2. 打开开发者工具 → **Network / 网络**,刷新页面,选中发往 `github.com` 的页面请求。
171
- 3. 在 **Request Headers / 请求标头** 中找到 **Cookie**,只复制其值并粘贴回插件;不要复制 `Set-Cookie`、整份请求标头或 AgentRouter 的 Cookie。
187
+ 3. 在 **Request Headers / 请求标头** 中找到 **Cookie**,只复制其值并保存到既有账号 metadata 的 `githubCookie`;不要复制 `Set-Cookie`、整份请求标头或 AgentRouter 的 Cookie。
172
188
  4. 不需要运行控制台脚本;`document.cookie` 读不到完整的 HttpOnly 登录 Cookie。插件不能替你读取浏览器或剪贴板。
173
189
 
174
- 找不到或不愿提供时,返回选择跳过,或让可选 Cookie 字段留空。非空输入会在授权前进行本地格式检查:需要有效的 GitHub 登录 Cookie 名,最多 32 KiB(UTF-8)。格式通过**不代表**登录态未过期或账号匹配,不会为验证格式发送网络请求。
175
-
176
- 显式跳过会清除本次隐藏字段中的 Cookie 残留;旧调用未提供新选择字段时,仍兼容合法 `githubCookie`,已有账号无需迁移。若宿主不支持条件问题,请保持 Cookie 字段留空以跳过。
190
+ 找不到或不愿提供时,不配置此字段。不要因为包含某个 Cookie 名就认定登录态有效:完整会话仍可能过期、账号不匹配或被 WAF 拦截。旧 metadata 原样保留,升级不自动采集、更新或清除凭据。[本地签到说明](<LOCAL-CHECKIN.md>) 中的独立工具保持不变,也不会自动启用。
177
191
 
178
192
  先在浏览器用对应 GitHub 账号登录并授权 AgentRouter,再配置完整 GitHub Cookie。
179
193
  每个账号必须填写正确的本站用户 ID,插件会校验 OAuth 返回的 ID,防止把另一个账号的结果
@@ -239,9 +253,9 @@ AgentRouter 的网关只回应它认识的客户端 —— 白名单里是它**
239
253
  - **开关**(provider 编辑器里的 `Send the Codex client headers`):默认开。关掉后不再发默认的 Codex 标识,只发你自己给的头 —— 适合你已经有别的可用标识时。
240
254
  - **模型配置层**:宿主传入的 `provider.models[*].headers` 对象(provider 编辑器可在模型配置中保存这些头)
241
255
  - **provider 级**:宿主传入的 `provider.headers` 多行文本,覆盖模型配置层
242
- - **账号级**:登录时填的 `headers`(保存为 `metadata.headers`),覆盖前述配置层
256
+ - **账号级**:既有账号的 `metadata.headers`(手动配置),覆盖前述配置层;0.1.6 登录不再询问请求头
243
257
 
244
- 格式是「一行一个 `Name: value`」,空行和 `#` 开头跳过。这里**只适合放这种头**:不要整段粘贴浏览器 devtools 的请求头 —— `Cookie` 有专门的一问,`Authorization` 由插件按 key/令牌设置,而 `User-Agent` 应该填**网关认的客户端标识**,不是浏览器的那一长串。
258
+ 格式是「一行一个 `Name: value`」,空行和 `#` 开头跳过。这里**只适合放这种头**:不要整段粘贴浏览器 devtools 的请求头 —— Cookie 应放入用途对应的既有 metadata 字段,不在登录时询问,`Authorization` 由插件按 key/令牌设置,而 `User-Agent` 应该填**网关认的客户端标识**,不是浏览器的那一长串。
245
259
 
246
260
  > 踩坑记录:把 Mobile Safari 的完整 `User-Agent` 贴进 `headers` 后,`/v1` 一律 401
247
261
  > `unauthorized client detected`,而站点路由(余额、签到)却一切正常 —— 很容易以为是 key 或插件坏了。
@@ -254,7 +268,7 @@ AgentRouter 的网关只回应它认识的客户端 —— 白名单里是它**
254
268
  - `githubCookie`:GitHub 登录会话,自动重新 OAuth 签到;高权限凭据,需妥善保护。
255
269
  - 站点用户名密码:仅兼容旧站点;GitHub-only 用户留空。
256
270
 
257
- `sk-` key 不能代替账号访问令牌。`/api/user/token` 会重新生成访问令牌,使旧令牌失效;插件不调用此接口,GitHub 登录直接保存你填写的系统访问令牌。
271
+ `sk-` key 不能代替账号访问令牌。`/api/user/token` 会重新生成访问令牌,使旧令牌失效;插件不调用此接口,GitHub 流程用你填写的系统访问令牌查找已有模型 key 并随成功认证保存,不会把它当作模型 key。
258
272
 
259
273
  ## 协议:只有 Responses API
260
274
 
@@ -424,27 +438,14 @@ JSON 仅在 HTTP **200 / 400 / 429** 时检查副本,最多 **64 KiB / 2 秒**
424
438
 
425
439
  SSE 仍只观察首个有效内容或未知事件之前最多 **64 KiB / 15 秒** 的前缀。增量解析兼容混合换行、注释、多行 `data:` 与跨块 UTF-8;重复 `event:` 取最后一个值,只移除冒号后的一个可选空格。已知字段内出现未知数据形状时也立即结束诊断,透传原始字节并保留取消、背压及超时中的在途读取;不发送心跳、不缓存整段回答、不观察已经放行的后续事件。相关实现见 [response.mjs](<response.mjs>)。
426
440
 
427
- ## 登录问答(API 密钥方式)
428
-
429
- 面板或 CLI 的登录问答(**密钥最后单独问**):
430
-
431
- 1. 请求端点(对话与模型列表)—— 可留空,默认 `https://agentrouter.org`;**这一问只填地址**,填了 `sk-…` 会被当场拒绝并提示
432
- 2. 站点 —— `agentrouter.org` / `ps.air-outer.com`
433
- 3. 签到续期配置 —— 默认“跳过”;仅主动选择“配置”才显示 GitHub 登录 Cookie 输入(非本站 Cookie),仍可留空跳过
434
- 4. 旧版站点用户名/密码 —— GitHub-only 账号留空;保存在网关服务器
435
- 5. 浏览器 Cookie —— 可留空;填完整 `session=…`,不能保证通过 WAF
436
- 6. 访问令牌 —— 可留空
437
- 7. 用户 ID —— Cookie 或访问令牌读取账号数据时填写 `New-Api-User` 的值
438
- 8. 自定义请求头 —— 每行一个 `Name: value`,留空用默认
439
- 9. **API 密钥**(`sk-…`)
441
+ ## API Key 保存与故障恢复(0.1.6)
440
442
 
441
- > 第 1 问**只填地址**。旧版本的这一问容易让人把 `sk-…` 粘进去,那会让对话请求打到 `sk-…/v1`,
442
- > 表现是**每个模型都无响应、也不报错**(插件当时没有任何提示)。现在两头都堵上了:问答时校验,
443
- > 以及读取该值时不是 `http(s)://` 就忽略、退回默认端点 —— 已经存坏的账号不用重新登录也能恢复。
443
+ API Key 方式只有宿主原生的一个 key 输入框,插件不追加问题。当前宿主不会把原生 key 传给 API `authorize`,因此不做保存前联网验证,不读取不存在的 `inputs.key`,也不增加第二个 key 字段或 verified 状态。
444
444
 
445
- > 这里**没有** `authorize` 钩子,是有意的:宿主调用 `authorize(inputs)` 时密钥**不在 inputs 里**
446
- > (它是独立参数),在 `authorize` 里读 `inputs.key` 永远为空,会把正确密钥判成失败。
447
- > 去掉它,宿主就会自己保存 `{type:"api", key, metadata: inputs}`。
445
+ - **保存不等于验证**:保存后运行 `magpie provider test agentrouter`,再判断模型是否可用。宿主已登录、管理令牌有效和余额可读都不是模型验证结果。
446
+ - **本地 `400 agentrouter_missing_api_key`**:loader 遇到缺失 key 或保留占位 key,在任何网络请求前返回。改选 API Key,保存真实模型 key 后再测。
447
+ - **旧配置端点误填 key**:`endpoint` 应为 HTTP(S) 地址。既有端点校验回退能力保留;如需手动修正,先备份,不要把 key 或其他凭据写入 URL。
448
+ - **GitHub 查 key 不确定**:没有唯一可用候选或列表分页不完整时,手动指定模型 key;不自动创建、轮换、重试或保存空 key。
448
449
 
449
450
  ## GitHub 登录怎么走
450
451
 
@@ -452,11 +453,11 @@ SSE 仍只观察首个有效内容或未知事件之前最多 **64 KiB / 15 秒*
452
453
 
453
454
  1. `GET /api/oauth/state?mode=login` 拿站点签发的 state
454
455
  2. 从 `/api/status` 读站点的 `github_client_id` 和 `server_address`,拼出 GitHub 授权 URL
455
- 3. 你在打开的页面授权后,浏览器跳到 `agentrouter.org/oauth/github/magpie`(GitHub app 是站点的,回调也归站点)
456
- 4. 把**那一整条地址**粘回插件,插件从里面取出 `code`
456
+ 3. 你在打开的页面授权后,浏览器跳到站点回调,例如 `https://agentrouter.org/oauth/github/magpie`。以本次授权后的真实地址为准,保留实际受信任来源,不要照模板重建 URL(GitHub app 和回调均归站点)
457
+ 4. 把**那一整条地址**粘回插件,保留 `code`、`state` 等所有参数
457
458
  5. `GET /api/oauth/github?code=…&state=…` 兑换登录态,拿到用户记录(`id`、`quota`、`checked_in`)
458
459
  6. 从响应头提取 `Set-Cookie: session=…`,保存登录前填写的系统访问令牌,不重新生成令牌
459
- 7. 用填写的访问令牌及授权返回的用户 ID 调 `/api/token/` 取出密钥
460
+ 7. 用填写的管理访问令牌及授权返回的用户 ID 调 `/api/token/` 查找已有模型密钥;仅接受去重后的唯一启用、非空、未掩码、未过期候选。多个候选或分页不完整等不确定结果均要求手动 API Key 恢复,不自动补页重试、创建或轮换密钥
460
461
 
461
462
  开始授权前必须先填系统访问令牌;留空会提示补填,不会发起 GitHub 授权。令牌应属于本次 GitHub 登录对应的 AgentRouter 账号。`session` 同时保存到 `metadata.cookie`,后续账号查询会带上它。
462
463
 
@@ -487,7 +488,7 @@ SSE 仍只观察首个有效内容或未知事件之前最多 **64 KiB / 15 秒*
487
488
 
488
489
  ## 手动配置文件格式(plugin-auth.json)
489
490
 
490
- 除了上述 `magpie plugin login` 交互式登录外,也可以直接手动编辑配置文件。
491
+ 这是已支持的高级路径,不是首次登录表单,也不是新的设置页面。修改前备份宿主的认证配置,保护文件权限;示例中的占位值不可原样用于真实账号。保持既有账号 key、站点和不相关字段不变。
491
492
 
492
493
  `plugin-auth.json` 的形态:
493
494
 
@@ -516,7 +517,7 @@ SSE 仍只观察首个有效内容或未知事件之前最多 **64 KiB / 15 秒*
516
517
  ## 端点与站点是分开的
517
518
 
518
519
  - `endpoint`(默认 `https://agentrouter.org`):对话和模型列表走这里
519
- - `site`(`org` / `air`):余额和签到走这里
520
+ - `site`(`org` / `air`):余额和签到走这里;新账号默认 `org`,升级保留旧值
520
521
 
521
522
  所以你可以请求打 `agentrouter.org`、余额和签到走 `ps.air-outer.com`,两者互不影响。
522
523
 
@@ -578,7 +579,7 @@ npm run test:headers # 需要 Python 3;仅执行维护脚本的 Python 部
578
579
 
579
580
  | 现象 | 原因 | 怎么办 |
580
581
  |---|---|---|
581
- | 所有模型都无响应,面板 0/7,且**没有任何报错** | 旧版把 `sk-…` 存进了 `metadata.endpoint`,请求打到 `sk-…/v1` | 本版已修:端点问答会拒绝,非 URL 的已存值会被忽略并回退默认端点;无需重新登录 |
582
+ | 所有模型都无响应,面板 0/7,且**没有任何报错** | 旧版把 `sk-…` 存进了 `metadata.endpoint`,请求打到 `sk-…/v1` | 0.1.6 不再询问端点;既有非 URL 端点仍会被忽略并回退默认值,升级无需重新登录 |
582
583
  | `/v1` 一律 `401 unauthorized client detected` | 自定义头里放了浏览器 `User-Agent` 等网关不认的标识 | 清掉该头,或用默认(`codex_cli_rs/0.101.0`) |
583
584
  | 某模型 `503 … 无可用渠道` | 面板列的是 models.dev 目录里已下架的模型 | 见「模型列表从哪来」,用站点真实列表或关掉该模型 |
584
585
  | 模型列表比预期少 | provider 编辑器里只选中了部分模型 | `magpie provider models agentrouter` 看 `pickedFrom`,在编辑器里勾选 |
@@ -1,11 +1,13 @@
1
- # 0.1.5 优化设计与兼容边界
1
+ # 0.1.6 优化与兼容边界
2
2
 
3
3
  [使用说明](<../README.md>) · [版本记录](<../CHANGELOG.md>) · [MIT 许可](<../LICENSE>)
4
4
 
5
- 本文说明当前 0.1.5 源码的行为、取舍与维护方法。版本号表示源码版本,**不表示该版本已在 npm 发布**。示例均为虚构数据,不含真实请求、账号或凭据。
5
+ 本文说明 **0.1.6** 的登录简化以及从 0.1.5 延续的兼容能力。安装命令固定 0.1.6,先查询 npm 官方 registry 确认可用;源码、npm 包和宿主实际加载版本应分别核对。下文保留既有优化与安全边界,示例不含真实请求、账号或凭据。
6
6
 
7
7
  ## 1. 优化什么,不优化什么
8
8
 
9
+ 0.1.6 只收敛首次登录和密钥缺失保护:API Key 原生单输入;GitHub 管理令牌、浏览器授权及完整手动回调;唯一已有 key 才能保存。不改变限流、Responses 兼容、过滤或签到运行逻辑。下列响应处理与正文能力沿用 0.1.5。
10
+
9
11
  这里的“优化”是减少错误归类和兼容处理中的误判、无界等待与信息丢失,不是增加上游额度或提高模型能力:
10
12
 
11
13
  - **HTTP 200 不一定是成功回答。** 上游可能在 JSON 错误封套或 SSE 前导事件中报告失败。只识别明确、有限的错误依据,不从正常正文中搜索错误词。
@@ -41,10 +43,32 @@
41
43
  | [request.mjs](<../request.mjs>) | 判断请求范围、执行过滤及内联、维护改写后的请求头 |
42
44
  | [content-filter.mjs](<../content-filter.mjs>) | 加载私有字面量词表,保护代码、工具数据和不透明内容 |
43
45
  | [response.mjs](<../response.mjs>) | JSON 错误分类、SSE 前导观察、失败说明与原始流转交 |
44
- | [index.mjs](<../index.mjs>) | 对接宿主选项及 fetch;一次宿主 fetch 调用只发起一次上游 fetch |
46
+ | [index.mjs](<../index.mjs>) | 对接宿主选项及 fetch;一次宿主 fetch 调用至多发起一次上游 fetch;缺失 key 时本地拒绝 |
45
47
 
46
48
  共享读取器是内部模块,不新增根入口公开 API。0.1.5 没有新增依赖、环境变量、提供商选项或登录配置迁移。
47
49
 
50
+ ## 3A. 0.1.6 登录与凭据边界
51
+
52
+ ### API Key:先列出,原生单输入,只保存
53
+
54
+ 方法顺序保持 API Key 在前、GitHub 在后。API Key 不含插件问题,只在宿主原生输入框输入一次。当前宿主的 API `authorize` 不接收原生 key,无法在保存前验证;不添加第二个 key 字段、verified 标记或“已验证”徽章。**凭据已保存 / 宿主已登录 ≠ 模型已验证**,保存后须独立测试模型。
55
+
56
+ ### GitHub:管理令牌 → 用户浏览器 → 完整手动回调 → 已有 key
57
+
58
+ 唯一的插件预置问题是系统访问令牌(password 遮蔽输入)。这是 AgentRouter 管理凭据,**不是模型 API Key,也不是 GitHub PAT**。随后用户须打开授权链接,在浏览器完成授权,将包含 code/state 等全部参数的完整回调 URL 粘回宿主;不能承诺免令牌、自动收回调或全自动 OAuth。回调 URL 是敏感临时凭据,不应公开。
59
+
60
+ 查找已有 key 支持已知的 `data.items` / `data` 数组形状。候选须满足 `status=1`、非空、未掩码、未过期,并按实际 key 去重;只有唯一可用候选才能保存。只读取一页(`page_size=10`):总数大于返回条数,或未提供总数且已返回 10 条时,按列表不完整处理。多个候选、无候选、未知形状或分页截断导致结果不确定时,改用原生 API Key 输入手动指定。不自动创建、轮换、补页重试或以空 key 成功。管理 API 调用成功也不证明模型调用成功。
61
+
62
+ loader 对缺失 key 或保留占位 key 在网络前返回本地 `400 agentrouter_missing_api_key`;应手动保存真实模型 key 后再测,不把它误判为上游错误。
63
+
64
+ ### 旧配置与签到:保留能力,不增加问答
65
+
66
+ 新账号默认 `agentrouter.org`;升级不改写旧账号 key、site、headers 或其他设置。首次登录不再询问端点、站点、请求头、用户 ID、用户名密码或 Cookie。当前插件选项只有布尔开关,**没有新增 Cookie 设置页**。需要高级配置时先备份,再使用 [README 的既有手动配置格式](<../README.md#手动配置文件格式plugin-authjson>);不要把凭据写进布尔选项。
67
+
68
+ GitHub Cookie 是可选签到续期的高权限会话,不随升级自动采集或启用。远程网关会将凭据保存在远程服务器;格式正确不代表会话有效、账号匹配或可通过 WAF。不得公开 Cookie、令牌或回调,也不能绕过授权确认、2FA 或验证码。
69
+
70
+ 模型测试、余额读取与签到到账应分别确认;余额可能是历史缓存,宿主登录不保证签到成功。独立 [本地签到工具](<../LOCAL-CHECKIN.md>) 保持不变,不自动启用。
71
+
48
72
  ## 4. 选项组合:关闭内联不等于关闭过滤
49
73
 
50
74
  提供商编辑器保留原有选项和默认值:
@@ -191,16 +215,16 @@ Markdown 采取保守策略,而不是试图实现完整语法解析器:
191
215
  - 响应改写设为 JSON,并移除 `content-length`、`content-encoding`、`transfer-encoding`、`etag`、`content-md5`、`digest`。
192
216
  - 不改写的路径保留原始正文和对应响应头;默认客户端标识开关与用户配置头仍遵循插件既有规则。不要把 Cookie 或 Authorization 当作普通客户端标识示例传播。
193
217
 
194
- **一次宿主 fetch = 一次上游 fetch。** 插件不追加 POST,不切账号、不重放工具、不降级历史;宿主自身的重试、路由和故障转移是另一层策略,必须分别配置和核查。返回 429/503 或等待头不等于插件执行了重试,也不能保证宿主一定按同样策略等待。
218
+ **一次宿主 fetch 至多一次上游 fetch;缺失 key 时为零。** 插件不追加 POST,不切账号、不重放工具、不降级历史;宿主自身的重试、路由和故障转移是另一层策略,必须分别配置和核查。返回 429/503 或等待头不等于插件执行了重试,也不能保证宿主一定按同样策略等待。
195
219
 
196
220
  ## 11. 升级与回退
197
221
 
198
- 1. 阅读 [使用说明](<../README.md>) 和 [版本记录](<../CHANGELOG.md>),核对实际安装来源与可用版本。不要仅凭本文版本号执行未经核实的 npm 版本安装。
222
+ 1. 阅读 [使用说明](<../README.md>) 和 [版本记录](<../CHANGELOG.md>),核对来源:npm 安装固定 0.1.6 并先查询官方 registry;本地包/目录安装应核对提交、包版本和宿主实际运行版本。不能把源码版本号当成 npm 发布或远程网关升级证明。
199
223
  2. 在受保护的位置备份当前插件版本、私有词表及宿主管理的配置;不要将凭据备份加入公开仓库。
200
224
  3. 使用已核验的完整本地包或完整插件目录升级,不能只覆盖入口文件。新增 [body-inspection.mjs](<../body-inspection.mjs>) 是运行时必需模块,漏带会导致模块加载失败。
201
- 4. 无需迁移认证字段、选项或环境变量。重新加载插件,让运行中的 loader 使用新源码与规则快照。先确认四种开关组合符合自己的使用意图。
225
+ 4. 无需迁移认证字段、选项或环境变量;升级前后核对旧 key、site 和其他设置不变,不必重新登录。重新加载插件,让运行中的 loader 使用新源码与规则快照。先确认四种开关组合符合自己的使用意图。
202
226
  5. 如只需停用正文变换,可关闭两个正文选项;这不会停用响应诊断。需要完整回退时,恢复此前验证过的完整版本并重新加载,不混用新旧模块。
203
- 6. 回退到 0.1.4 会失去本页列出的 0.1.5 诊断、SSE 和独立过滤修复;回退本身也不能恢复已缺失的上游历史。
227
+ 6. 从 0.1.6 回退到 0.1.5 会恢复旧登录交互,并失去0.1.6 的密钥发现与缺失 key 保护。回退到 0.1.4 还会失去 0.1.5 诊断、SSE 和独立过滤修复;回退不能恢复已缺失的上游历史。
204
228
 
205
229
  本节不要求重新登录、提交真实 Cookie、执行自动签到或回放私有会话。涉及真实流量的验收应单独评估并由部署者授权。
206
230
 
@@ -215,7 +239,9 @@ npm run test:headers
215
239
 
216
240
  第二条需要 Python 3,仅执行维护脚本的 Python 测试部分,不调用 Docker。这些回归不需要真实账号或凭据;不要用真实登录或生产请求替代合成回归。
217
241
 
218
- 0.1.5 当前源码的本地原生回归记录为 **Node.js 255 项、Python 2 项通过**,并经独立范围复核接受。该结论只覆盖已验证的源码行为;后续代码改变后应重新运行,不能沿用旧计数作为通过凭据。
242
+ 0.1.6 的本地原生回归为 **Node.js 278 项、Python 2 项通过**;另有实际安装包的离线 mock smoke 与独立认证契约复验。它们不使用真实账号,不能证明线上授权或模型调用成功。0.1.5 历史记录为 255 项 Node.js、2 项 Python;后续源码改变必须重新验证,不能沿用旧计数。
243
+
244
+ 维护 0.1.6 时还应独立核对方法顺序、输入数量、管理令牌必填、完整回调、唯一 key 发现/歧义/分页不完整的手动恢复、无空 key OAuth 成功,以及 loader 在缺失或保留占位 key 时网络前本地拒绝。当前宿主不向 API authorize 传入原生 key,不能用合成桩虚构保存前验证能力。
219
245
 
220
246
  重点维护以下断言,而不是只追求测试数量:
221
247
 
@@ -232,4 +258,4 @@ npm run test:headers
232
258
  - 不保证跨资源历史或加密状态可恢复,不自动删除历史来掩盖失败。
233
259
  - 不保证词表能避免内容拒绝,也不提供通用 DLP、审核类别推断或自动词表学习。
234
260
  - 不保证延迟、吞吐或模型质量提升;有界检查仍可能等待到预算,且不是整体内存/超时限制。
235
- - 不改变登录、签到、WAF 或宿主重试策略,也不是对整个部署的安全认证。
261
+ - 除本页明确列出的登录与缺失 key 保护外,不改变签到、WAF 或宿主重试策略,也不是对整个部署的安全认证。
package/index.mjs CHANGED
@@ -324,52 +324,10 @@ async function jsonOrText(res) {
324
324
  // used here only has to survive the authorize step.
325
325
  const GITHUB_CALLBACK_PATH = "/oauth/github/magpie";
326
326
 
327
- // The form and check-in use the same local parser. A missing mode is a legacy
328
- // caller; explicit skip must erase a hidden value when Magpie merges metadata.
329
- function githubCookieForAuth(inputs) {
330
- const mode = inputs?.githubCookieMode;
331
- if (mode === "skip") return "";
332
- if (mode !== undefined && mode !== "configure") {
333
- throw new Error("请选择跳过或配置 GitHub 自动签到");
334
- }
335
- const parsed = parseGithubCookie(inputs?.githubCookie);
336
- if (parsed.error) throw new Error(parsed.error);
337
- return parsed.cookie;
338
- }
339
-
340
- function githubCookiePrompts() {
341
- return [
342
- {
343
- type: "select",
344
- key: "githubCookieMode",
345
- message: "是否配置 GitHub 签到续期?跳过不影响普通登录和 API 调用,可稍后配置",
346
- options: [
347
- { label: "跳过,暂不配置(推荐)", value: "skip" },
348
- { label: "配置 GitHub Cookie", value: "configure" },
349
- ],
350
- },
351
- {
352
- type: "password",
353
- key: "githubCookie",
354
- when: { key: "githubCookieMode", op: "eq", value: "configure" },
355
- message: "GitHub 登录 Cookie(可选,可留空放弃配置)\n" +
356
- "1. 在自己的浏览器打开 https://github.com,登录与 AgentRouter 绑定的同一 GitHub 账号。\n" +
357
- "2. 打开开发者工具 → Network/网络,刷新页面,选择发往 github.com 的页面请求。\n" +
358
- "3. 在 Request Headers/请求标头中找到 Cookie,只复制其值回到这里粘贴;不要复制 Set-Cookie、整份标头或 AgentRouter Cookie。\n" +
359
- "4. 无需运行控制台脚本;document.cookie 无法取得完整的 HttpOnly 登录 Cookie。\n" +
360
- "这是高权限凭证,会提交并保存到自己的网关,仅在可信网关使用。勿发到聊天、截图、日志或公共仓库。插件不能自动读取浏览器或剪贴板,也不能自动弹出 Cookie 获取窗口。\n" +
361
- "找不到、不愿提供或暂不配置可留空或选择跳过。格式合格不代表线上有效或账号正确;Cookie 失效或需要二次验证时仍须人工更新,不保证永久自动签到。",
362
- placeholder: "user_session=…; logged_in=yes(非 AgentRouter session)",
363
- validate: value => parseGithubCookie(value).error,
364
- },
365
- ];
366
- }
367
-
368
- // Full callback URLs must belong to this flow. A code alone remains supported.
327
+ // Login accepts only a full callback URL belonging to this flow.
369
328
  function githubCallbackCode(pasted, expectedURL, state) {
370
329
  const text = String(pasted ?? "").trim();
371
330
  if (!text || text.length > 8192) return "";
372
- if (/^[A-Za-z0-9_-]{8,2048}$/.test(text)) return text;
373
331
  try {
374
332
  const url = new URL(text);
375
333
  const expected = new URL(expectedURL);
@@ -400,8 +358,8 @@ async function oauthState(base) {
400
358
  signal: withTimeout(REQUEST_TIMEOUT),
401
359
  });
402
360
  const { json } = await jsonOrText(res);
403
- const st = String(json?.data ?? "").trim();
404
- if (!json?.success || !st) return { ok: false, why: "站点没有下发 OAuth state" };
361
+ const st = typeof json?.data === "string" ? json.data.trim() : "";
362
+ if (!res.ok || json?.success !== true || !st) return { ok: false, why: "站点没有下发 OAuth state" };
405
363
  return { ok: true, state: st };
406
364
  } catch (e) {
407
365
  return { ok: false, why: `拿不到 OAuth state:${e?.message ?? String(e)}` };
@@ -421,10 +379,16 @@ async function siteAuthConfig(base) {
421
379
  signal: withTimeout(REQUEST_TIMEOUT),
422
380
  });
423
381
  const { json } = await jsonOrText(res);
424
- const id = String(json?.data?.github_client_id ?? "").trim();
425
- if (!id) return { ok: false, why: "站点没有开启 GitHub 登录" };
426
- const reported = String(json?.data?.server_address ?? "").trim().replace(/\/+$/, "");
427
- return { ok: true, id, origin: reported || base.replace(/\/+$/, "") };
382
+ const id = typeof json?.data?.github_client_id === "string" ? json.data.github_client_id.trim() : "";
383
+ if (!res.ok || !id) return { ok: false, why: "站点没有开启 GitHub 登录" };
384
+ const reported = json.data.server_address;
385
+ if (reported != null && typeof reported !== "string") return { ok: false, why: "站点回调地址无效" };
386
+ const url = new URL(String(reported ?? "").trim() || base);
387
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash ||
388
+ !Object.values(SITES).some(site => site.site === url.origin)) {
389
+ return { ok: false, why: "站点回调地址不受信任" };
390
+ }
391
+ return { ok: true, id, origin: url.origin };
428
392
  } catch (e) {
429
393
  return { ok: false, why: `读不到站点配置:${e?.message ?? String(e)}` };
430
394
  }
@@ -478,72 +442,86 @@ function sessionOf(res) {
478
442
  // /api/user routes take. The account keeps this session alongside the
479
443
  // system access token explicitly entered before authorization.
480
444
  async function redeemGithub(base, code, state, inputs) {
481
- let json;
482
- let session = "";
483
- try {
484
- const res = await fetch(
485
- `${base}/api/oauth/github?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}&mode=login`,
486
- { headers: DEFAULT_HEADERS, signal: withTimeout(REQUEST_TIMEOUT) },
487
- );
488
- session = sessionOf(res);
489
- ({ json } = await jsonOrText(res));
490
- } catch {
491
- return { type: "failed", error: "兑换 GitHub 授权失败,请检查网络后重新发起登录;不要重复使用旧授权码" };
445
+ const token = String(inputs.accessToken ?? "").trim();
446
+ if (!token) return { type: "failed", error: "缺少系统访问令牌,请重新登录或改用 API Key 接入。" };
447
+ const u = new URL(base + "/api/oauth/github");
448
+ u.searchParams.set("code", code);
449
+ u.searchParams.set("state", state);
450
+ u.searchParams.set("mode", "login");
451
+ const res = await fetch(u, { headers: DEFAULT_HEADERS, redirect: "manual", signal: withTimeout(REQUEST_TIMEOUT) });
452
+ const session = sessionOf(res);
453
+ const { json } = await jsonOrText(res);
454
+ const d = json?.data;
455
+ if (!res.ok || json?.success !== true || !d?.id) {
456
+ return { type: "failed", error: "GitHub 授权兑换未完成,授权码可能已使用或过期;请重新授权或改用 API Key 接入。" };
492
457
  }
493
- if (!json?.success) {
494
- return { type: "failed", error: "GitHub 授权兑换失败,授权码可能已使用或过期;请重新发起登录并复制新的完整回调地址" };
458
+ const found = await keyOf(base, token, String(d.id));
459
+ if (found.kind !== "found") {
460
+ const messages = {
461
+ missing: "未找到启用且完整的模型 Key",
462
+ ambiguous: "存在多个不同的模型 Key,无法确定使用哪一个",
463
+ incomplete: "模型 Key 列表可能不完整,无法确定唯一候选",
464
+ rejected: "站点拒绝读取模型 Key,请检查系统访问令牌及权限",
465
+ blocked: "读取模型 Key 时遇到站点验证页",
466
+ unsupported: "站点返回的模型 Key 列表格式无法确认",
467
+ upstream: "站点暂时无法提供模型 Key 列表",
468
+ network: "读取模型 Key 的网络请求失败或超时",
469
+ };
470
+ return { type: "failed", error: messages[found.kind] + "。请改用 API Key 接入,或确认后主动重试。" };
495
471
  }
496
- const d = json.data ?? {};
497
- const id = d.id ?? "";
498
- // Keep the explicitly entered system token; regenerating it here would
499
- // invalidate the token the user just supplied.
500
- const token = String(inputs?.accessToken ?? "").trim();
501
- if (!token) return { type: "failed", error: "请填写系统访问令牌(AgentRouter 个人设置 → 访问令牌)后重新登录" };
502
-
503
- // The completions key is not in this answer either, so it is read with the
504
- // token: the account's own key list. A site that refuses leaves the key
505
- // empty, and the account then reads as signed in but unable to chat, which
506
- // its row says.
507
- const key = await keyOf(base, token, id);
508
472
  return {
509
473
  type: "success",
510
- // magpie saves this as an API sign-in: the key for completions, the
511
- // access token beside it for the balance and the check-in, and the
512
- // browser session the site itself set, which is what gets a /api/user
513
- // read past the WAF.
514
- key: key ?? "",
474
+ key: found.key,
515
475
  metadata: {
476
+ site: "org",
516
477
  accessToken: token,
517
- userId: String(id ?? ""),
518
- site: String(inputs?.site ?? "org"),
519
- ...(session ? { cookie: `session=${session}` } : {}),
520
- githubCookie: inputs?.githubCookie ?? "",
521
- ...(inputs?.githubCookieMode ? { githubCookieMode: inputs.githubCookieMode } : {}),
478
+ userId: String(d.id),
479
+ ...(session ? { cookie: "session=" + session } : {}),
522
480
  },
523
481
  };
524
482
  }
525
483
 
526
- // keyOf reads the account's key with the access token just obtained.
527
- // /api/token/ is a normal user route and answers JSON, so it works where
528
- // /api/user/self may be behind the WAF.
529
- //
530
- // The key is taken as the site stores it rather than required to start with
531
- // "sk-": New API keeps a key exactly as it was generated, and this site's own
532
- // list holds the bare 48 characters, which its /v1 routes accept on their own.
533
- // An empty entry is the only thing that means "no key".
484
+ function usableModelKey(value) {
485
+ if (typeof value !== "string") return false;
486
+ const key = value.trim();
487
+ return key !== "" && !["agentrouter", "your_api_key"].includes(key.toLowerCase()) && !/[*…<>]|\.\.\.|\s/.test(key);
488
+ }
489
+
490
+ function selectModelKey(json, pageSize) {
491
+ const data = json?.data;
492
+ const items = Array.isArray(data) ? data : data?.items;
493
+ if (!Array.isArray(items)) return { kind: "unsupported" };
494
+ const total = Array.isArray(data) ? json.total : data?.total;
495
+ if (total != null && (!Number.isSafeInteger(total) || total < items.length)) return { kind: "unsupported" };
496
+ if (total != null ? total > items.length : items.length >= pageSize) return { kind: "incomplete" };
497
+ const keys = new Set();
498
+ const now = Date.now() / 1000;
499
+ for (const item of items) {
500
+ if (item?.status !== 1 || !usableModelKey(item.key)) continue;
501
+ const expiry = item.expired_time;
502
+ if (expiry != null && (typeof expiry !== "number" || !Number.isFinite(expiry) || (expiry !== -1 && expiry <= now))) continue;
503
+ keys.add(item.key.trim());
504
+ }
505
+ if (keys.size === 0) return { kind: "missing" };
506
+ if (keys.size !== 1) return { kind: "ambiguous" };
507
+ return { kind: "found", key: keys.values().next().value };
508
+ }
509
+
534
510
  async function keyOf(base, token, id) {
511
+ const pageSize = 10;
535
512
  try {
536
- const res = await fetch(`${base}/api/token/?p=1&page_size=10`, {
537
- headers: { ...DEFAULT_HEADERS, Authorization: token, "New-Api-User": String(id ?? "") },
513
+ const res = await fetch(base + "/api/token/?p=1&page_size=" + pageSize, {
514
+ headers: { ...DEFAULT_HEADERS, Authorization: token, "New-Api-User": id },
538
515
  signal: withTimeout(REQUEST_TIMEOUT),
539
516
  });
540
- const { json } = await jsonOrText(res);
541
- const items = json?.data?.items;
542
- if (!Array.isArray(items)) return "";
543
- const live = items.find((x) => x && x.status === 1 && typeof x.key === "string") ?? items.find((x) => x && typeof x.key === "string");
544
- return String(live?.key ?? "").trim();
517
+ const { json, text } = await jsonOrText(res);
518
+ if (!json && /^\s*</.test(text)) return { kind: "blocked" };
519
+ if (res.status === 401 || res.status === 403) return { kind: "rejected" };
520
+ if (!res.ok) return { kind: "upstream" };
521
+ if (json?.success === false) return { kind: "rejected" };
522
+ return selectModelKey(json, pageSize);
545
523
  } catch {
546
- return "";
524
+ return { kind: "network" };
547
525
  }
548
526
  }
549
527
 
@@ -988,144 +966,64 @@ export const AgentRouterAuthPlugin = async (input) => {
988
966
  methods: [
989
967
  {
990
968
  type: "api",
991
- label: "AgentRouter API 密钥",
992
- placeholder: "sk-…",
993
- prompts: [
994
- {
995
- type: "text",
996
- key: "endpoint",
997
- message: "请求端点(对话与模型列表)",
998
- placeholder: ENDPOINT,
999
- // The key is asked for separately, last. This question is here
1000
- // because the editor's box is where the key used to go: a key
1001
- // pasted into it becomes apiBase's endpoint and every request
1002
- // then goes nowhere, silently. So the answer is checked here,
1003
- // where the user can still be told.
1004
- validate: (v) =>
1005
- !String(v ?? "").trim() || isURLish(v)
1006
- ? undefined
1007
- : "请填站点地址,例如 https://agentrouter.org(不要在这里填 API 密钥)",
1008
- },
1009
- {
1010
- type: "select",
1011
- key: "site",
1012
- message: "站点(签到与余额走这里)",
1013
- options: [
1014
- { label: "agentrouter.org", value: "org" },
1015
- { label: "ps.air-outer.com(备用域名)", value: "air" },
1016
- ],
1017
- },
1018
- ...githubCookiePrompts(),
1019
- {
1020
- type: "text",
1021
- key: "username",
1022
- message: "旧版兼容:站点用户名(GitHub-only 账号留空)",
1023
- placeholder: "AgentRouter GitHub 账号无需填写",
1024
- },
1025
- {
1026
- type: "text",
1027
- key: "password",
1028
- message: "旧版兼容:站点密码(仅支持密码登录的站点适用;保存在网关服务器)",
1029
- placeholder: "AgentRouter GitHub 账号留空,不需要也不能在这里设置密码",
1030
- },
1031
- {
1032
- type: "text",
1033
- key: "cookie",
1034
- message: "浏览器 Cookie(可选,登录站点后复制请求中的 Cookie;需同时填用户 ID,仍可能被验证码拦截)",
1035
- placeholder: "session=…",
1036
- },
1037
- {
1038
- type: "password",
1039
- key: "accessToken",
1040
- message: "系统访问令牌(可选,AgentRouter「个人设置 → 访问令牌」;不是 API Key 或 GitHub Token,本站 Cookie 有效时不需要)",
1041
- placeholder: "留空则用浏览器 Cookie 读取余额",
1042
- },
1043
- {
1044
- type: "text",
1045
- key: "userId",
1046
- message: "本站用户 ID(New-Api-User;API 登录配置 GitHub 自动签到或读取账号数据时需填写;GitHub 登录会自动获取)",
1047
- placeholder: "例如 1234",
1048
- },
1049
- {
1050
- type: "text",
1051
- key: "headers",
1052
- message: "自定义请求头:每行一个「名称: 值」,留空则用默认的 Codex 客户端标识",
1053
- placeholder: "User-Agent: codex_cli_rs/0.101.0",
1054
- },
1055
- ],
1056
- // Magpie falls back to the separately entered key when this result
1057
- // omits key. Only normalize metadata; other hosts keep native entry.
1058
- ...(input?.magpie ? { authorize: async (inputs) => ({
1059
- type: "success",
1060
- metadata: { ...inputs, githubCookie: githubCookieForAuth(inputs) },
1061
- }) } : {}),
969
+ label: "API Key(推荐)",
970
+ // The current host owns the one native Key input and saves it.
971
+ // Its authorize callback does not receive that Key for validation.
972
+ ...(input?.magpie ? {
973
+ authorize: async () => ({ type: "success", metadata: { site: "org" } }),
974
+ } : {}),
1062
975
  },
1063
976
  {
1064
- // Signing in the way the site's own "Login with GitHub" button does.
1065
- //
1066
- // Ask for the site's system access token before authorization.
1067
- // GitHub supplies the user ID and session; the entered token is kept
1068
- // for account reads and is never regenerated by this flow.
1069
- //
1070
- // The GitHub app is the site's, so its callback is the site's own
1071
- // page and the code lands in the browser's address bar rather than at
1072
- // a server this plugin runs. That is why the code is asked for: the
1073
- // user pastes the address the browser ended on, and the code is read
1074
- // out of it.
1075
977
  type: "oauth",
1076
- label: "用 GitHub 登录 AgentRouter(推荐)",
978
+ label: "GitHub(需系统访问令牌)",
1077
979
  authorize: async (inputs) => {
1078
- const githubCookie = githubCookieForAuth(inputs);
1079
- if (!String(inputs?.accessToken ?? "").trim()) {
1080
- throw new Error("请填写系统访问令牌(AgentRouter 个人设置 → 访问令牌,不是 API 密钥或 GitHub Token)");
980
+ const token = String(inputs?.accessToken ?? "").trim();
981
+ if (!token) return { type: "failed", error: "请输入系统访问令牌;如已有模型 Key,请改用 API Key 接入。" };
982
+ const selected = SITES.org;
983
+ let state, cfg;
984
+ try {
985
+ const st = await oauthState(selected.site);
986
+ if (!st.ok || typeof st.state !== "string" || !st.state.trim()) throw new Error("invalid OAuth state");
987
+ state = st.state;
988
+ cfg = await siteAuthConfig(selected.site);
989
+ if (!cfg.ok || !cfg.id) throw new Error("invalid OAuth configuration");
990
+ } catch {
991
+ return { type: "failed", error: "无法获取 GitHub 授权配置,请检查网络或站点验证后重试,也可改用 API Key 接入。" };
1081
992
  }
1082
- const site = siteOf(String(inputs?.site ?? "org"));
1083
- const st = await oauthState(site.site);
1084
- if (!st.ok) throw new Error(st.why);
1085
- const cid = await siteAuthConfig(site.site);
1086
- if (!cid.ok) throw new Error(cid.why);
1087
- const url =
1088
- "https://github.com/login/oauth/authorize" +
1089
- `?client_id=${encodeURIComponent(cid.id)}` +
1090
- `&state=${encodeURIComponent(st.state)}` +
1091
- "&scope=user:email" +
1092
- `&redirect_uri=${encodeURIComponent(callbackUrl(cid.origin))}`;
993
+ const u = new URL("https://github.com/login/oauth/authorize");
994
+ const redirectUri = callbackUrl(cfg.origin);
995
+ u.searchParams.set("client_id", cfg.id);
996
+ u.searchParams.set("redirect_uri", redirectUri);
997
+ u.searchParams.set("scope", "user:email");
998
+ u.searchParams.set("state", state);
1093
999
  return {
1094
- url,
1095
- instructions:
1096
- "若未自动打开浏览器,请手动打开上方链接。不要刷新授权后的页面,以免一次性 code 被消耗。\n" +
1097
- `在打开的页面用 GitHub 登录并授权。授权后浏览器会跳到 ${cid.origin} 下的一个地址,` +
1098
- "它显示的是「页面未找到」——这是正常的,那一页不会用掉你的授权码。" +
1099
- "请把地址栏里的完整地址(含 code=… 和 state=…)复制下来,回到这里粘贴;也可仅粘贴 code。\n" +
1100
- "这个链接只完成站点登录,不会自动读取或弹出 GitHub Cookie 获取窗口;Cookie 是以后自动签到的可选配置。\n" +
1101
- "授权码是一次性的;若已使用或过期失效,请重新发起登录,不要重复提交旧地址。若 GitHub 显示回调地址未注册/redirect_uri mismatch,请联系站点管理员;这不是 Cookie 或二次验证的问题。",
1000
+ url: u.href,
1102
1001
  method: "code",
1103
- // The code is read from whatever the user pastes: the whole
1104
- // callback URL, or just the code.
1105
- callback: async (pasted) => {
1106
- const code = githubCallbackCode(pasted, callbackUrl(cid.origin), st.state);
1107
- if (!code) return { type: "failed", error: "GitHub 回调地址、state 或授权码无效,或授权已取消;请重新登录并复制完整回调地址或仅 code" };
1108
- return redeemGithub(site.site, code, st.state, { ...inputs, githubCookie });
1002
+ instructions: "若未自动打开浏览器,请手动打开链接并完成 GitHub 授权。最终页面可能显示「页面未找到」,请勿刷新;复制地址栏的完整回调地址并粘贴回来,保留 code 和 state。授权码一次有效,失效后请重新登录。插件只查找已有模型 Key;失败时可改用 API Key 接入。请勿将回调地址发到聊天或日志。",
1003
+ callback: async (value) => {
1004
+ let cb;
1005
+ try {
1006
+ if (typeof value !== "string" || !value.trim().startsWith("https://")) throw new Error("full callback required");
1007
+ cb = githubCallbackCode(value, redirectUri, state);
1008
+ if (!cb) throw new Error("invalid callback");
1009
+ } catch {
1010
+ return { type: "failed", error: "回调地址无效或 state 不匹配。请重新登录获得新链接,再粘贴完整回调地址。" };
1011
+ }
1012
+ try {
1013
+ return await redeemGithub(selected.site, cb, state, { accessToken: token });
1014
+ } catch {
1015
+ return { type: "failed", error: "GitHub 授权兑换请求失败或超时,请重新授权重试或改用 API Key 接入。" };
1016
+ }
1109
1017
  },
1110
1018
  };
1111
1019
  },
1112
1020
  prompts: [
1113
- {
1114
- type: "select",
1115
- key: "site",
1116
- message: "站点",
1117
- options: [
1118
- { label: "agentrouter.org", value: "org" },
1119
- { label: "ps.air-outer.com(备用域名)", value: "air" },
1120
- ],
1121
- },
1122
1021
  {
1123
1022
  type: "password",
1124
1023
  key: "accessToken",
1125
- message: "系统访问令牌(必填,AgentRouter「个人设置 → 访问令牌」;不是 API 密钥或 GitHub Token)",
1126
- placeholder: "粘贴系统访问令牌,用于查询账号额度",
1024
+ message: "系统访问令牌(AgentRouter 个人设置 → 访问令牌;不是模型 API Key)",
1025
+ validate: value => String(value ?? "").trim() ? undefined : "请输入系统访问令牌;如已有模型 Key,请改用 API Key 入口",
1127
1026
  },
1128
- ...githubCookiePrompts(),
1129
1027
  ],
1130
1028
  },
1131
1029
  ],
@@ -1134,8 +1032,7 @@ export const AgentRouterAuthPlugin = async (input) => {
1134
1032
  // are present. Server-only references and unknown kinds stay untouched.
1135
1033
  // Response normalization preserves successful bytes and never retries.
1136
1034
  async loader(getAuth, provider) {
1137
- const auth = await getAuth();
1138
- if (!auth) return {};
1035
+ const auth = (await getAuth()) ?? {};
1139
1036
  const textFilter = textFilterEnabled() ? await loadTextFilter() : null;
1140
1037
  const base = apiBase(provider, auth);
1141
1038
  return {
@@ -1143,6 +1040,13 @@ export const AgentRouterAuthPlugin = async (input) => {
1143
1040
  apiKey: apiKey(auth) || "agentrouter",
1144
1041
  async fetch(input, init = {}) {
1145
1042
  const key = apiKey(auth);
1043
+ if (!usableModelKey(key)) {
1044
+ return Response.json({ error: {
1045
+ type: "invalid_request_error",
1046
+ code: "agentrouter_missing_api_key",
1047
+ message: "尚未配置模型 API Key,请使用 API Key 入口补充。系统访问令牌和 GitHub Cookie 不能用于模型调用。",
1048
+ } }, { status: 400 });
1049
+ }
1146
1050
  const h = new Headers(input instanceof Request ? input.headers : undefined);
1147
1051
  new Headers(init.headers ?? {}).forEach((v, k) => h.set(k, v));
1148
1052
  applyHeaders(h, headersOf(provider, auth));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-agentrouter-auth",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "AgentRouter (agentrouter.org) provider for magpie and OpenCode: GitHub or API-key sign-in, configurable request headers, balance and the daily check-in",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -37,7 +37,7 @@
37
37
  "maxConcurrency": 2
38
38
  },
39
39
  "scripts": {
40
- "test": "node --test body-inspection.test.mjs usage.test.mjs loader-stream.test.mjs response-bounds.test.mjs checkin.test.mjs local-checkin.test.mjs local-checkin.integration.test.mjs request-replay.test.mjs content-filter.test.mjs auth-guidance.test.mjs resource-compat.test.mjs auth-request-boundary.test.mjs response-preservation.test.mjs",
40
+ "test": "node --test body-inspection.test.mjs usage.test.mjs loader-stream.test.mjs response-bounds.test.mjs checkin.test.mjs local-checkin.test.mjs local-checkin.integration.test.mjs request-replay.test.mjs content-filter.test.mjs auth-guidance.test.mjs auth-flow.test.mjs resource-compat.test.mjs auth-request-boundary.test.mjs response-preservation.test.mjs",
41
41
  "test:headers": "python3 set-ar-headers.test.py"
42
42
  }
43
43
  }