# xxsmore
**Repository Path**: opencode-plugin/xxsmore
## Basic Information
- **Project Name**: xxsmore
- **Description**: xxs-支持多key轮询 (小学生公益站)
- **Primary Language**: TypeScript
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-17
- **Last Updated**: 2026-10-07
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# XXS More OpenCode Provider Plugin
**提供商 ID**: `xxsmore` | **显示名称**: XXS More | **Base URL**: `https://xxs.l.cd/v1`
OpenCode 插件,集成 小学生公益站(xxs.l.cd)网关,支持 API Key 多密钥轮询以缓解速率限制。
## 功能
- **多 Key 轮询** — 输入多个 API Key(逗号分隔),自动循环切换,减缓 `429` 速率限制
- **错误容错** — 单个 Key 连续错误 3 次自动跳过,全部不可用时重置计数
- **模型支持** — 内置 6 个模型(DeepSeek V4 系列 + GLM 5.3),全部为思考模型
- **思考模型适配** — 对声明 `interleaved` 的模型自动注入 `thinking`,并兜底回传 `reasoning_content`(规避 tools 请求 400)
- **think 标签兼容** — 上游若把推理混入 `content`(`思考回答` / `思考回答`),自动剥离并改投 reasoning 通道
- **模型别名映射** — OpenCode 上送简写别名,插件解析为上游真实模型 id 后请求
- **请求头注入** — 自动设置 `User-Agent` 和 `Accept` 头
- **未授权兜底** — 未配置 Key 时直接返回 401 并提示授权,不触发无意义重试
- **首 chunk 超时保护** — SSE 流 15 秒无首块时返回 429,交由 OpenCode 重试(防上游挂起)
- **调试日志** — `~/.xxsmore/debug.log`,由环境变量控制开关
- **凭证存储** — API Key 持久化到 `~/.xxsmore/auth.json`
## 安装
### 前提
- Node.js 22+
- OpenCode
### 步骤
```bash
# 1. 进入项目
cd xxsmore
# 2. 安装依赖
npm install
# 3. 构建
npm run build
```
## 配置
### 1. 配置 opencode.json
```json
// opencode.json
{
"plugins": [
"/path/to/xxsmore"
]
}
```
### 2. 添加凭据
在 OpenCode 中执行 `/connect` 命令,选择 **XXS More**:
```
┌ 创建 API Key:https://xxs.l.cd/keys
│
│ 邀请链接(注册可额外获得额度):
│ https://xxs.l.cd/sign-up?aff=gheO
│ https://xxs.l.cd/sign-up?aff=SEKX
│ https://xxs.l.cd/sign-up?aff=NE8s
│ https://xxs.l.cd/sign-up?aff=ufBU
│
│ 输入 API Keys(多key用逗号分隔)
│ ────────────────────────────────
│ sk-key1,sk-key2,sk-key3
└ enter
```
> 多个 Key 用逗号分隔,插件会自动轮询使用。未授权时模型请求会返回
> `未授权:请通过 /connect 命令点击 XXS More 进行授权`。
### 3. 选择模型
执行 `/models` 命令,选择可用模型:
| Model ID | 上游模型 | 描述 | 上下文 | 最大输出 | 输入 | 价格(输入/输出,$/1M) |
|----------|----------|------|--------|----------|------|--------------------------|
| `deepseek-v4-flash` | `deepseek-v4-flash` | DeepSeek V4 Flash | 1.0M | 384K | text | 0.08 / 0.16 |
| `deepseek-v4-flash-0731` | `deepseek-v4-flash-0731` | DeepSeek V4 Flash 0731 | 1.0M | 384K | text | 0.06 / 0.12 |
| `deepseek-v4-pro` | `deepseek-v4-pro` | DeepSeek V4 Pro | 1.0M | 384K | text | 1.60 / 3.20 |
| `deepseek-v4-pro-0813` | `deepseek-v4-pro-0813` | DeepSeek V4 Pro 0813 | 1.0M | 384K | text | 0.58 / 1.74 |
| `deepseek-v4.1-flash` | `deepseek-v4.1-flash` | DeepSeek V4.1 Flash(多模态) | 1.0M | 384K | text, image | 0.15 / 0.60 |
| `glm-5-3` | `glm-5-3` | GLM 5.3 | 1.0M | 128K | text | 1.40 / 4.40 |
> **模型别名映射**:OpenCode 上送的是表中 `Model ID` 列(别名),插件通过它匹配
> `constants.ts` 中的模型定义,再用「上游模型」列的真实 id 请求上游。
> 本网关 `/v1/models` 返回的 id 即为可直接请求的模型名,因此当前别名与真实 id 一致。
## 思考内容处理
插件对思考内容做两件事:**注入思考开关** 与 **think 标签剥离**。
### 1. 思考开关注入
对声明了 `interleaved` 的模型,在请求体注入 `thinking: { type: "enabled" }`,
使上游把推理输出到独立的 `reasoning_content` 字段。`reasoning_effort` 由 OpenCode
按用户选择的档位自动传入(SDK 在 `transformRequestBody` 之前写入),插件仅透传、不写死。
上游实测接受的档位:`low` / `medium` / `high` / `xhigh` / `none`。
> DeepSeek 官方支持的档位更宽(`minimal` / `low` / `medium` / `high` / `xhigh` / `max` / `ultra`,
> 其中 `max` 映射为最大思考强度),详见
> [思考模式文档](https://api-docs.deepseek.com/zh-cn/guides/thinking_mode)。
> 本网关目前未开放 `max` / `ultra` / `minimal`,传入会返回
> `400 field ReasoningEffort invalid`。
>
> 注意:OpenCode 会为 `id` 含 `deepseek-v4` 的模型**自动生成 `max` 变体**
> (`provider/transform.ts`)。插件不覆盖该行为 —— `max` 是官方合法档位,
> 若后续网关开放即可直接使用。在当前网关下请选择 default 或其他档位。
### 2. think 标签剥离(DeepSeek 系通病兼容)
部分上游不返回 `reasoning_content`,而是把推理内容混在 `delta.content` 中:
| 形态 | 样例 | 说明 |
|------|------|------|
| A | `思考内容实际回答` | 单个 `` 结尾,其之前是推理 |
| B | `思考回答` | 成对标签包裹,标签内是推理 |
插件在 SSE 流上做增量剥离(`src/utils/think-tag.ts`),把标记内的文本改投
`reasoning_content` 通道,OpenCode 便会渲染为独立的思考区而非正文。
**状态机**:
```
undecided ──(遇 )──▶ reasoning ──(遇 )──▶ content
│ ▲
└──(遇 ,形态 A:此前文本全部归推理)────────────┘
```
- `undecided` 期间文本被缓冲;一旦上游提供了 `reasoning_content`(推理已分离),
或缓冲超过 64 字符,立即判定为正文并实时透传 —— 正常链路无额外延迟
- 标记被切断到相邻 chunk(如 ``)时按真前缀保留,跨 chunk 正确拼接
- 改写后 `finish_reason` / `usage` 等字段原样保留
> 边界说明:该机制需假定「`` 之前的文本属于推理」。若正文中真的出现
> 字面量 ``(例如讨论 think 标签本身),该段会被误判为推理。
## 多 Key 轮询机制
```
请求 → chat.headers → KeyManager.getNextKey()
↓
┌──────────────┐
│ key1 ← 首次 │
│ key2 ← 下次 │
│ key3 ← 再下次 │
└──────────────┘
↓
写入 Authorization → 发起 API 请求
```
- 循环轮询:`key1 → key2 → key3 → key1 → ...`
- 错误追踪:每个 Key 连续失败 3 次后跳过
- 自动恢复:所有 Key 都被跳过时重置计数
- 成功自愈:请求成功后清零该 Key 的错误计数
## 错误处理
自定义 `fetch` 统一将上游错误转换为 OpenCode 可解析的标准 JSON 错误流:
| 场景 | 行为 |
|------|------|
| 401 / 403 | 立即报错(不重试),保留上游原始错误消息 |
| 408 / 409 / 429 / 5xx | 触发 OpenCode 重试(自动轮换 Key) |
| 网络异常 | 返回 502 标准错误流,不向 UI 抛异常 |
| 未授权(无 Key) | 返回 401 并提示 `/connect` 授权,不发真实请求 |
| 首 chunk 15s 超时 | 返回 429 触发 OpenCode 重试(防上游挂起) |
## 调试日志
通过环境变量开启调试日志:
```bash
# 方式一:全局 DEBUG
DEBUG=1 opencode
# 方式二:仅 XXS More
OPENCODE_XXSMORE_DEBUG=1 opencode
```
日志位置:`%USERPROFILE%\.xxsmore\debug.log`
## 上游模型列表(原始记录)
网关 `/v1/models` 接口返回的完整模型列表,供后续新增模型时参考:
```bash
curl https://xxs.l.cd/v1/models -H "Authorization: Bearer "
```
```json
{
"data": [
{
"id": "deepseek-v4-flash",
"object": "model",
"created": 1626777600,
"owned_by": "newapi",
"supported_endpoint_types": ["openai", "openai-response", "openai-response-compact", "anthropic", "gemini", "openai-alpha-search"]
},
{
"id": "deepseek-v4-flash-0731",
"object": "model",
"created": 1626777600,
"owned_by": "newapi",
"supported_endpoint_types": ["openai", "openai-response", "openai-response-compact", "anthropic", "gemini", "openai-alpha-search"]
},
{
"id": "deepseek-v4-pro",
"object": "model",
"created": 1626777600,
"owned_by": "newapi",
"supported_endpoint_types": ["openai", "openai-response", "openai-response-compact", "anthropic", "gemini", "openai-alpha-search"]
},
{
"id": "deepseek-v4-pro-0813",
"object": "model",
"created": 1626777600,
"owned_by": "newapi",
"supported_endpoint_types": ["openai", "openai-response", "openai-response-compact", "anthropic", "gemini", "openai-alpha-search"]
},
{
"id": "deepseek-v4.1-flash",
"object": "model",
"created": 1626777600,
"owned_by": "openai",
"supported_endpoint_types": ["openai"]
},
{
"id": "glm-5-3",
"object": "model",
"created": 1626777600,
"owned_by": "openai",
"supported_endpoint_types": ["openai"]
},
{
"id": "小学生AI助手-PROMAX",
"object": "model",
"created": 1626777600,
"owned_by": "openai",
"supported_endpoint_types": ["openai"]
}
],
"object": "list",
"success": true
}
```
> 说明:`小学生AI助手-PROMAX` 为站点自有模型,未纳入插件注册列表。
> 其余模型已全部注册(见上方模型表)。新增模型时,在 `src/constants.ts` 的 `models`
> 中添加条目(`key` 与 `name` 需全小写且与上游 id 一致),并在 README 模型表中同步。
## 项目结构
```
xxsmore/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # 主入口:auth hook + config hook + chat.headers + 共享 fetch
│ ├── constants.ts # 提供商配置、模型定义
│ ├── plugin/
│ │ ├── auth.ts # 凭证读写 ~/.xxsmore/auth.json
│ │ └── key-manager.ts # 多 Key 轮询算法
│ └── utils/
│ ├── think-tag.ts # think 标签剥离状态机(SSE 增量改写)
│ └── debug-logger.ts # 文件日志(无 console.error)
├── scripts/ # 诊断与验证脚本
└── dist/ # 构建产物
```
## 诊断脚本
| 脚本 | 用途 |
|------|------|
| `scripts/check-think-strip.mjs` | think 剥离层合成样本验证(形态 A/B、跨 chunk、快路径、边界),无需上游 |
| `scripts/check-plugin-e2e.mjs` | 端到端:真实上游回归 + 注入合成 SSE 验证剥离生效 |
| `scripts/check-thinking.mjs` | 思考分离 / tools 契约 / 兜底对比(S1–S6) |
| `scripts/check-model-matrix.mjs` | 6 模型 × 有无 thinking 注入 矩阵 + effort 透传 |
| `scripts/check-effort.mjs` | `reasoning_effort` 各档位兼容性枚举 |
| `scripts/probe-think-body.mjs` | 非流式快速探测 think 标记泄漏 |
| `scripts/repro-think-leak.mjs` | 流式多轮复现 think 标记泄漏 |
```bash
node scripts/check-think-strip.mjs # 剥离逻辑单测(离线)
node scripts/check-plugin-e2e.mjs # 端到端(需要可用 API Key)
```
> 脚本默认使用内置测试 Key,可通过环境变量 `XXSMORE_API_KEY` 覆盖。
> 上游限制并发 3 个在途请求,脚本默认低并发运行。
## 开发
```bash
npm run typecheck # 类型检查
npm run build # 构建到 dist/
```
## Key 黑名单(blacklist.json)
KeyManager 会为每个 Key 维护错误计数,持久性错误(`401/403/400` 等)触发 `reportError`(计数 +1),
当 `errorCount >= 3` 时该 Key 被临时拉黑,轮询时自动跳过;请求成功一次即触发 `reportSuccess` 复位计数。
黑名单状态通过快照文件跨重启保留,重启后自动恢复各 Key 的错误计数。
快照文件格式(与插件写盘格式一致):
```json
{
"updatedAt": 1759800000000,
"keys": [
{ "key": "sk-xxx1", "errorCount": 3, "blocked": true },
{ "key": "sk-xxx2", "errorCount": 0, "blocked": false }
]
}
```
目标路径:`%USERPROFILE%\.xxsmore\blacklist.json`
复制方法(可选):将项目 `jsonc/blacklist.jsonc` 复制到 `%USERPROFILE%\.xxsmore\` 并重命名为
`blacklist.json`。不复制也不影响运行——插件仅在内存中计数,不跨重启保留;快照读写失败仅记录
debug 警告,绝不中断主流程。
## 配置文件
插件运行所需的配置文件位于本仓库 ./jsonc/ 目录下(provider.jsonc 与 models.jsonc)。使用前将这两个文件复制到用户目录 %USERPROFILE%\.xxsmore/ 下(缺失时插件启动会 fail fast 报错)。复制后可按需编辑模型列表与提供商信息(支持注释),修改后重启插件生效。