with one click
api-module-auth
小豆子 FR 的 lib/api/ 模块规范 — 目录即后端、深目录轻文件、全局拦截器链
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
小豆子 FR 的 lib/api/ 模块规范 — 目录即后端、深目录轻文件、全局拦截器链
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Relay-v3 Lua 状态机接入指南。新增一个互联网房间业务(聊天、卡牌、白板、投票、协作…)时使用。描述后端元函数契约、前端闭环套路、Lua 脚本模板、state 转换规范、错误案例。识别"我要实现一个新业务""写一个 Lua 房间脚本""加一个 action 类型"时触发。
当用户提及 Relay/LAN/快照/action 流/net_p2p/net_engine 协议、讨论消息传输层(事件 vs 快照)、新建/重构 P2P 协议、排查"晚加入者错过事件""两端不同步""房间状态丢失""广播丢失"等问题时触发。包含 v1 action/事件驱动(已落地,遗留用法)与 v2 快照驱动(推荐新功能)。
Flutter 项目中"样式"相关工程的渐进式披露指南。当用户要做 UI 样式选型、视觉对齐复刻、画布/HTML mockup 与 Flutter 实现的双向对照、或在 Material 3 体系下选某一类样式(顶部 App Bar / Card / Button / NavigationBar / Modal 等)落地时触发。本 skill 是样式大类的总入口,所有方案的最终形态都登记在分类索引表里,按需加载对应方案文件。同时承载小豆子 FR 项目的 UI 设计原则与实战 bug 沉淀(border-emphasis 边框强调式、嵌套 sheet race condition、多风格 lottery 投票挑选、纯色按钮减负、左重右轻、装饰性 vs 功能性颜色决策)。
flutter的开发操作流程,在dart-flutter任何问题都需要优先加载这个SKILL
Flutter 通过 home_widget 把 1Hz 实时值(如倒计时)推到 Android 桌面 AppWidget 的端到端架构。当用户提到 home_widget 不同步、桌面小组件不刷新、appwidget 实时值、widget 显示卡死、widget 进程被杀场景、AppWidgetProvider 找不到时触发。
Comprehensive Rive animation platform skill covering scripting (Luau), runtime integration (React/Next.js), state machines, data binding, and the complete API. Use this skill when users need to create interactive animations with Rive, integrate Rive into React/Next.js applications, write Rive scripts (Node, Layout, Converter, PathEffect protocols), control animations via state machines, implement scroll-based animations, or work with Rive's drawing API (Path, Paint, Renderer). Triggers on: "rive", "rive animation", "rive script", "luau", "@rive-app/react-canvas", "state machine animation", "interactive animation", "scroll animation with rive". 本项目(xiaodouzi/fr,Flutter + rive ^0.14.5)特化:DataBind / ViewModel 双向数据绑定、lab demo 添加流程见 references/flutter-databind-0.14.md 与 references/flutter-project-workflow.md。
| name | api-module-auth |
| description | 小豆子 FR 的 lib/api/ 模块规范 — 目录即后端、深目录轻文件、全局拦截器链 |
lib/api/ 33 files / 1424 lines
├── api_module.dart (24行) ← barrel export
├── api_config.dart (30行) ← baseUrl + timeout
├── api_client.dart (197行) ← 拦截器链 + HTTP 核心
├── api_response.dart (38行) ← ApiResponse / ApiException
│
├── token/ ← Token 生命周期
│ ├── token_storage.dart (66行) ← SharedPreferences 实现
│ └── token_manager.dart (63行) ← 获取/缓存/refresh
│
├── interceptors/ ← 拦截器链
│ ├── auth_interceptor.dart (37行) ← Bearer 注入 + 401 refresh
│ ├── logging_interceptor.dart (30行) ← 请求/响应日志
│ └── retry_interceptor.dart (39行) ← 5xx 指数退避重试
│
├── goframe/ ── 小豆子 GoFrame 后端
│ ├── goframe.dart (10行) ← barrel
│ ├── goframe_config.dart (6行) ← baseUrl 常量
│ ├── kv/
│ │ ├── kv.dart (1行) ← barrel
│ │ └── kv_endpoint.dart (67行) ← get/set/delete/list
│ ├── file/
│ │ ├── file.dart (1行) ← barrel
│ │ └── file_endpoint.dart (103行) ← upload/metadata/delete
│ ├── download/
│ │ ├── download.dart (2行) ← barrel
│ │ ├── download_controller.dart (16行) ← cancel/pause/resume
│ │ └── apk_endpoint.dart (131行) ← 流式下载 + 断点续传
│ └── article/
│ ├── article.dart (1行) ← barrel
│ └── article_endpoint.dart (51行) ← AI 文章编辑
│
├── github/ ── GitHub REST API
│ ├── github.dart (13行) ← barrel
│ ├── github_config.dart (8行) ← baseUrl / version 常量
│ ├── github_exception.dart (12行) ← GithubApiException
│ ├── actions/
│ │ ├── actions.dart (2行) ← barrel
│ │ ├── actions_endpoint.dart (87行) ← listRuns / listJobs
│ │ └── actions_models.dart (86行) ← WorkflowRunModel / WorkflowJobModel
│ └── issues/
│ ├── issues.dart (2行) ← barrel
│ ├── issues_endpoint.dart (101行)← CRUD + close/reopen
│ └── issues_models.dart (63行) ← IssueModel / CreateIssueRequest
│
├── minimax/ ── MiniMax TTS
│ ├── minimax.dart (8行) ← barrel
│ ├── minimax_config.dart (13行) ← WS URL / 默认音色
│ └── models/
│ └── tts_models.dart (67行) ← SynthesisParams / TaskState
│
├── notion/ ── Notion REST API (3 步上传图床)
│ ├── notion.dart (10行) ← barrel
│ ├── notion_config.dart (15行) ← baseUrl + version + 5MB 上限
│ ├── notion_exception.dart (14行) ← NotionApiException
│ ├── database_endpoint.dart (~85行) ← getDatabase / queryLatestPage / listDatabases
│ ├── page_endpoint.dart (~85行) ← createPageWithTimestamp
│ └── file_endpoint.dart (~160行) ← 3 步上传(createFileUpload → sendFileContent → appendImageBlock)
│
└── providers/
└── api_providers.dart (49行) ← Riverpod 注入全部 endpoint
| 文件 | 导入 | 行数 | 角色 |
|---|---|---|---|
api_client.dart | api_config, api_response, interceptors/*, token/token_manager | 197 | 核心 — 拦截器链编排 |
api_config.dart | — | 30 | 配置常量 |
api_response.dart | — | 38 | 响应模型 |
token/token_manager.dart | token_storage | 63 | Token 生命周期 |
token/token_storage.dart | shared_preferences | 66 | 持久化 |
interceptors/* | api_client, api_response, token_manager | 30-39 | 拦截器,每个 1 职责 |
goframe/*/kv_endpoint.dart | api_client, api_response | 67 | 端点 |
goframe/*/file_endpoint.dart | api_client, api_response | 103 | 端点 |
goframe/*/apk_endpoint.dart | api_config, download_controller, http, path_provider, io | 131 | 端点(流式,直连 http) |
goframe/*/article_endpoint.dart | api_client, api_response | 51 | 端点 |
github/*/actions_endpoint.dart | github_config, github_exception, actions_models, http | 87 | 端点(独立 baseUrl,自持 http) |
github/*/issues_endpoint.dart | github_config, github_exception, issues_models, http | 101 | 端点 |
minimax/models/* | — | 67 | 纯模型 |
providers/api_providers.dart | 全部 goframe endpoint + api_client + token | 49 | DI 组装 |
| 模块 | 职责 | 禁止混入 |
|---|---|---|
api_client.dart | 拦截器链 + HTTP 核心 + ApiResponse 解析 | 业务 URL、业务模型 |
api_config.dart | baseUrl、timeout 配置 | 任何运行时代码 |
api_response.dart | ApiResponse<T>、ApiException 定义 | 业务字段 |
interceptors/ | 横切关注点:auth / log / retry | 业务逻辑、状态管理 |
token/ | Token 生命周期:缓存、持久化、refresh | HTTP 细节 |
goframe/ | 小豆子 GoFrame 后端 (47.110.80.47:8988) | 其他后端逻辑 |
github/ | GitHub REST API (api.github.com) | GoFrame/Minimax 逻辑 |
minimax/ | MiniMax TTS 配置 + 模型定义 | HTTP 请求细节 |
notion/ | Notion REST API:Database / Page / File Upload(图床 3 步上传) | 其他后端逻辑 |
providers/ | Riverpod DI 组装 | 任何业务逻辑 |
lib/api/// ❌ 禁止:在业务代码中直接 import http 发请求
import 'package:http/http.dart' as http;
class FooService {
Future<X> fetch() async {
final res = await http.get(Uri.parse('https://...')); // ❌ 散落
}
}
// ✅ 必须:在 lib/api/ 下加 endpoint,业务代码只 import api 层
import '../api/goframe/kv/kv_endpoint.dart';
class FooService {
final KvEndpoint _kv;
Future<X> fetch() => _kv.get('key');
}
lib/api/goframe/ ← 自己后端的 API
lib/api/github/ ← GitHub 的 API
lib/api/minimax/ ← MiniMax 的 API
不看代码就知道依赖了哪些远端服务。新增后端直接加同级目录。
每个端点(kv、file、download、article)各自是一个子目录,文件控制在 200 行以内。
goframe/download/
├── download.dart (2行) ← barrel
├── download_controller.dart (16行) ← 工具类
└── apk_endpoint.dart (131行) ← 业务端点
barrel 文件只做 export,不含业务逻辑。
| 后端 | baseUrl | HTTP 方式 |
|---|---|---|
| goframe/ | 走 api_client.dart(统一拦截器链) | ApiClient.request() |
| github/ | 自持 http.Client(不同 baseUrl/header 规范) | _client.get/post/patch |
| minimax/ | 纯模型 + 配置(WS 会话在 service 层管理) | 不发起 HTTP |
不同 baseUrl 的后端不强行套同一拦截器链,避免拦截器污染。
// ✅ 优先:通过 Riverpod Provider 注入
class SomeNotifier extends StateNotifier<X> {
SomeNotifier(this._kv) : super(...);
final KvEndpoint _kv;
}
// providers/api_providers.dart 已有全部 endpoint 的 Provider
final someProvider = StateNotifierProvider<SomeNotifier, X>((ref) {
return SomeNotifier(ref.watch(kvEndpointProvider));
});
// ❌ 避免:业务代码中手动构造 ApiClient(除非 Lab Demo 等一次性场景)
// 会导致 token 状态无法共享、拦截器链重复创建
final client = ApiClient(config: ..., tokenManager: ...); // ❌
// ❌ core/ + endpoints/ 两个抽象层,分不清哪个属于哪个后端
lib/api/
├── core/ // "core" 是什么后端?
├── endpoints/ // "endpoints" 是什么维度?
└── minimax/ // 突然冒出个具体后端名
// ✅ 每个目录代表一个后端,一视同仁同级排列
lib/api/
├── goframe/ // 小豆子后端
├── github/ // GitHub API
└── minimax/ // MiniMax TTS
// ❌ 旧 services/api_client.dart (439行)
// 同时处理:HTTP 客户端 + KV 调用 + 文件上传 + 流式下载 + 平台判断
class ApiService {
static const String baseUrl = 'http://47.110.80.47:8988'; // 配置
static Future<bool> setKv(...) { ... } // KV 业务
static Future<String?> downloadApkToLocal(...) { ... } // 流式下载
// ... 440 行
}
// ✅ goframe/kv/kv_endpoint.dart (67行) — 只做 KV 请求
class KvEndpoint {
final ApiClient _client;
KvEndpoint(this._client);
Future<ApiResponse<KvItem?>> get(String key) => _client.request<KvItem>(
method: 'GET',
path: '/api/v1/kv/$key',
fromJson: (json) => KvItem.fromJson(json),
);
// set() / delete() / list() ...
}
// ✅ goframe/download/download_controller.dart (16行) — 只做下载控制
class DownloadController { ... }
// ✅ goframe/download/apk_endpoint.dart (131行) — 只做 APK 流式下载
class ApkDownloadEndpoint { ... }
// ❌ GitHub 端点走了 GoFrame 的拦截器链
final response = await _apiClient.request(
method: 'GET',
path: 'https://api.github.com/repos/owner/repo/actions/runs',
// 拦截器会注入 'Authorization: Bearer <goframe-token>',但 GitHub 需要自己的 token
);
// ✅ goframe: 走统一拦截器链(共享 auth/log/retry)
class KvEndpoint {
final ApiClient _client; // 拦截器链处理 token + log + retry
}
// ✅ github: 自持 http.Client(不同 baseUrl + 不同 auth)
class GithubActionsEndpoint {
final String token; // GitHub PAT
final http.Client _client; // 自持 client
Map<String, String> get _headers => {
'Authorization': 'Bearer $token',
'Accept': 'application/vnd.github+json',
};
}
// 1. lib/api/goframe/xxx/xxx.dart ← barrel
export 'xxx_endpoint.dart';
// 2. lib/api/goframe/xxx/xxx_endpoint.dart ← endpoint
class XxxEndpoint {
final ApiClient _client;
XxxEndpoint(this._client);
Future<ApiResponse<XxxResult>> doSomething() =>
_client.request<XxxResult>(method: 'GET', path: '/api/v1/xxx', ...);
}
// 3. lib/api/goframe/goframe.dart ← 注册 barrel
export 'xxx/xxx.dart';
// 4. lib/api/providers/api_providers.dart ← 注入
final xxxEndpointProvider = Provider<XxxEndpoint>((ref) {
return XxxEndpoint(ref.watch(apiClientProvider));
});
# 1. 创建目录(同级排列)
mkdir -p lib/api/newbackend/
# 2. 写 barrel + endpoint(参考 goframe/kv/ 的模式)
# 3. 注册顶层 barrel
echo "export 'newbackend/newbackend.dart';" >> lib/api/api_module.dart
goframe/kv/ 模式(走 ApiClient 拦截器链)github/issues/ 模式notion/file_endpoint.dart 模式(3 步上传 + 手工 multipart)// ✅ StateNotifier + Provider 注入(推荐)
class FooNotifier extends StateNotifier<FooState> {
final KvEndpoint _kv;
FooNotifier(this._kv) : super(FooState.initial());
Future<void> load() async {
final res = await _kv.get('foo');
// ...
}
}
final fooProvider = StateNotifierProvider<FooNotifier, FooState>((ref) {
return FooNotifier(ref.watch(kvEndpointProvider));
});
// ✅ ConsumerWidget 中直接读取
class FooPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final foo = ref.watch(fooProvider);
// ...
}
}
以下命令应定期运行(或在 CI 中检查),防止 API 调用散落到业务代码中:
# 检测 1:业务代码中是否出现了 http.get/post 等裸调用
# 业务代码不应直接使用 http 包
grep -rn "http\.\(get\|post\|put\|delete\|patch\)" lib/ \
--include="*.dart" \
--exclude-dir=api --exclude-dir=generated \
--exclude-dir=services
# ↑ 如果在 lib/core/、lib/providers/、lib/screens/ 等目录发现 http 调用,就是违规
# 检测 2:业务代码中是否直接 import http 包
grep -rn "package:http/http.dart" lib/ \
--include="*.dart" \
--exclude-dir=api --exclude-dir=generated
# ↑ 只在 lib/api/ 下允许 import http
# 检测 3:lib/api/ 中是否有文件超 200 行
find lib/api -name "*.dart" -exec wc -l {} + | sort -rn | awk '$1 > 200'
# 检测 4:barrel 文件是否混入了业务逻辑
grep -rn "class \|Future\|import 'package:" lib/api --include="*dart" \
| grep -E "/(goframe|github|minimax)/[a-z]+\.dart:" \
| grep -v "_endpoint\|_models\|_config\|_exception\|_controller\|_test"
# 检测 5:业务代码中是否硬编码了 API URL
grep -rn "https\?://" lib/ \
--include="*.dart" \
--exclude-dir=api --exclude-dir=generated
# ↑ URL 只能出现在 lib/api/*/xxx_config.dart 中
2026-07 加进来。用 Notion 数据库当图床:拍照后 append 到最新 page 末尾。
lib/api/notion/ ← Notion REST API(自持 http.Client)
├── notion.dart (10行) ← barrel
├── notion_config.dart (15行) ← baseUrl + version + 5MB 上限
├── notion_exception.dart (14行) ← NotionApiException (statusCode/code/message)
├── database_endpoint.dart (~85行) ← getDatabase / queryLatestPage / listDatabases
├── page_endpoint.dart (~85行) ← createPageWithTimestamp (mention.date 模板)
└── file_endpoint.dart (~160行) ← createFileUpload / sendFileContent / appendImageBlock
test/api/notion/ ← 链路测试(本地跑,不入库)
├── notion_test_helpers.dart ← token/db_id 环境变量 + 517 字节测试图
├── database_endpoint_test.dart ← getDatabase / queryLatestPage / listDatabases
├── page_endpoint_test.dart ← createPageWithTimestamp + 错误处理
└── file_endpoint_test.dart ← createFileUpload + 3 步链路 + 无效 token
Notion 单图上传必须分 3 步,1 个 HTTP 请求搞不定:
| 步骤 | API | 作用 |
|---|---|---|
| 1 | POST /v1/file_uploads | 创建 file_upload 对象,拿到 upload_id(设置 content_length 必须准确) |
| 2 | POST /v1/file_uploads/{id}/send | multipart/form-data 上传字节(form 字段名固定为 file) |
| 3 | PATCH /v1/blocks/{page_id}/children | 把 image block(type: file_upload)追加到 page |
参考源码:.claude/repo/notion-cli/cmd/file.go 的 uploadFromSource 函数(行 323-377)。
| 决策 | 选择 | 理由 |
|---|---|---|
| API 调用方式 | 自持 http.Client(github 模式) | 不混用 GoFrame 拦截器链;Notion 错误语义不同 |
| 多步上传 vs 单步 | 多步 | Notion API 不支持单步;file_upload 必须分两步:声明 → 传字节 |
| multipart 库 | 手工构造 body | http.MultipartFile.headers 是私有字段;http_parser.MediaType 是传递依赖没显式 export — 手工拼 boundary 最稳 |
| 中文属性名 | 直接用 "名称"(curl 验证后) | Notion API 完全支持 UTF-8;先用 curl 验证再写 Dart |
| 标题模板 | mention.date | 与现有 me 数据库 page 风格一致(date mention + 空格) |
| 测试图 | 517 字节 100x100 PNG(Python 手工生成) | 避免依赖大资源;fixture 放 test/fixtures/test_image.png |
| Token 来源 | String.fromEnvironment + 显式 requireTestToken() | CI 友好;本地手动 NOTION_TOKEN=... 注入;绝不放硬编码默认 token 进 git |
// ✅ 手工构造 multipart body,绕开 http_parser.MediaType 依赖
Future<void> sendFileContent({
required String uploadId,
required String filename,
required String contentType,
required List<int> bytes,
}) async {
final url = Uri.parse('${NotionConfig.baseUrl}/v1/file_uploads/$uploadId/send');
final boundary = '----notionUpload${DateTime.now().microsecondsSinceEpoch}';
final filenameEscaped = filename.replaceAll('"', '\\"');
final bodyBytes = <int>[];
bodyBytes.addAll(utf8.encode('--$boundary\r\n'));
bodyBytes.addAll(utf8.encode(
'Content-Disposition: form-data; name="file"; filename="$filenameEscaped"\r\n'));
bodyBytes.addAll(utf8.encode('Content-Type: $contentType\r\n\r\n'));
bodyBytes.addAll(bytes);
bodyBytes.addAll(utf8.encode('\r\n--$boundary--\r\n'));
final resp = await _client.post(url, headers: {
'Authorization': 'Bearer $token',
'Notion-Version': NotionConfig.version,
'Content-Type': 'multipart/form-data; boundary=$boundary',
}, body: bodyBytes);
_checkError(resp);
}
// ✅ 标题用 mention.date,匹配 me 数据库现有 page 风格
Future<Map<String, dynamic>> createPageWithTimestamp({
required String databaseId,
String? titlePropertyName, // 默认 "名称"
}) async {
final propertyName = titlePropertyName ?? '名称';
final nowIso = DateTime.now().toIso8601String();
final body = jsonEncode({
'parent': {'database_id': databaseId},
'properties': {
propertyName: {
'title': [
{'type': 'mention', 'mention': {'type': 'date', 'date': {'start': nowIso}}},
{'type': 'text', 'text': {'content': ' '}},
],
},
},
});
final resp = await _client.post(
Uri.parse('${NotionConfig.baseUrl}/v1/pages'),
headers: _headers,
body: body,
);
_checkError(resp);
return jsonDecode(resp.body);
}
// ✅ test/api/notion/notion_test_helpers.dart
const String testToken = String.fromEnvironment('NOTION_TOKEN', defaultValue: '');
// 默认空 — 必须显式传环境变量;绝不放硬编码默认 token
String requireTestToken() {
if (testToken.isEmpty) {
throw StateError('testToken 为空。请用环境变量传入:\n'
' NOTION_TOKEN=ntn_xxx flutter test test/api/notion/');
}
return testToken;
}
| 错误操作 | 实际后果 | 正确做法 |
|---|---|---|
改 .gitignore 把 test/api/ 加白名单让测试入库 | GitHub secret scanning 拦截 push,token 差点泄漏 | .gitignore 第 42-46 行已有 test/* 排除规则(仅 test/core/localnet/ 例外);绝不轻易改这条规则。token 测试永远本地跑,不入库 |
用 http.MultipartFile.headers 覆盖 content-type | flutter analyze 报 undefined_getter(私有字段) | 用 http.MultipartRequest 走标准 API,或手工构造 multipart body(最终选这条路) |
测试期望 contentLength=99999 时 Notion 拒绝 | Notion 实际接受(content_length 是 hint 非 hard limit),测试失败 | 改测试无效 token 抛 401 — 更可靠 |
| 直接写 Dart 不知道 Notion API 中文 property 是否支持 | 浪费时间调试 | 先用 curl 验证一次:bash curl 把中文 property name 和 response 都过一遍再写 Dart |
| 试图让 demo 通过 Provider 直接读 SharedPreferences | Provider 是同步的,SharedPreferences 是异步的 | demo 层 initState() 里 await SharedPreferences 然后 ref.read(provider.notifier).state = ... |
const String testToken = String.fromEnvironment('NOTION_TOKEN', defaultValue: '');
.gitignore 除非用户明确说 — 项目已有 test/* 排除 + test/core/localnet/ 例外,不要扩展例外.claude/skills/、memory/ 是项目级资产,commit 时确认不包含密钥git diff --staged | grep -E "ntn_|secret_|token.*=.*[a-zA-Z0-9]{20,}"
按 Notion 模式(github + notion)扩展时,按此清单:
lib/api/<backend>/ 子目录 — 自持 http.Clientnotion_config.dart(或同形 config)— baseUrl、version、常量限制notion_exception.dart — 解析 4xx/5xx 的 code + message 字段api_providers.dart 用 StateProvider 暴露 token + endpointtest/api/<backend>/,本地跑不入库(遵守 .gitignore)flutter analyze 无 errorcrash_log_demo.dart)