원클릭으로
api-module-auth
小豆子 FR 的 lib/api/ 模块规范 — 目录即后端、深目录轻文件、全局拦截器链
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
小豆子 FR 的 lib/api/ 模块规范 — 目录即后端、深目录轻文件、全局拦截器链
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
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)