设计:完成UI和框架基础设计

This commit is contained in:
ivan 2026-04-07 17:29:08 +08:00
parent 4309a64acb
commit 8c5e14e00d
15 changed files with 7598 additions and 13 deletions

View File

@ -14,6 +14,13 @@
<meta-data <meta-data
android:name="com.facebook.sdk.ClientToken" android:name="com.facebook.sdk.ClientToken"
android:value="@string/facebook_client_token" /> android:value="@string/facebook_client_token" />
<!-- 关闭 Facebook SDK 自动应用事件与广告主标识采集(仍可通过代码显式打点) -->
<meta-data
android:name="com.facebook.sdk.AutoLogAppEventsEnabled"
android:value="false" />
<meta-data
android:name="com.facebook.sdk.AdvertiserIDCollectionEnabled"
android:value="false" />
<application <application
android:label="FunyMee AI" android:label="FunyMee AI"

View File

@ -73,6 +73,50 @@
"price_4999": "ojlx1r", "price_4999": "ojlx1r",
"price_9999": "hue6dt" "price_9999": "hue6dt"
}, },
"extConfig": {
"keys": {
"showVideoMenu": ["go_run", "need_wait"],
"allowScreenshot": ["screen"],
"blockScreenshot": ["safe_area"],
"allowThirdPartyPayment": ["san_fang", "lucky"],
"privacyUrl": ["privacy"],
"agreementUrl": ["agreement"],
"items": ["items"]
},
"itemKeys": {
"image": ["image"],
"imgNeed": ["img_need"],
"cost": ["cost"],
"title": ["title"],
"params": ["params"],
"detail": ["detail"]
},
"defaults": {
"go_run": false,
"screen": false,
"san_fang": false,
"privacy": "https://www.baidu.com",
"agreement": "https://www.baidu.com",
"items": [
{
"image": "https://cdn.petsheroai.xyz/cdn/temp/20260313/2032427067902025728.png",
"image_fix": "https://cdn.petsheroai.xyz/cdn/temp/20260313/2032427067902025728.png",
"img_need": 2,
"cost": 1,
"title": "BananaTask",
"params": "animal_expression"
},
{
"image": "https://cdn.petsheroai.xyz/cdn/temp/20260313/2032427067956551680.png",
"image_fix": "https://cdn.petsheroai.xyz/cdn/temp/20260313/2032427067956551680.png",
"img_need": 2,
"cost": 1,
"title": "BananaTask",
"params": "animal_expression"
}
]
}
},
"fieldMapping": { "fieldMapping": {
"User_token": "comedy", "User_token": "comedy",
"accountId": "meme", "accountId": "meme",

Binary file not shown.

BIN
desgin/Credit Record.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
desgin/credit_tag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

6812
desgin/funymee_home.pen Normal file

File diff suppressed because it is too large Load Diff

BIN
desgin/xiabiao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,479 @@
# FunyMee 应用开发手册(完整版)
本文档是 **FunyMee 客户端** 的主开发手册:页面范围、启动与认证、`skin_config`、**`client_proxy_framework` 已封装 API**、分屏实施要点、合规与发布清单。
实现代理/V2 报文细节、字段别名全表时,需对照 [FunyMeeAI_client_guide.md](./FunyMeeAI_client_guide.md)从零搭工程、Adjust/Facebook 原生步骤可对照 [skin_app_development_guide.md](./skin_app_development_guide.md)。**按本手册顺序施工 + 上述两份文档作规格备查,即可闭环完整 App**(不含后端实现与商店审核商务材料)。
---
## 目录
1. [资料闭环与本手册怎么用](#1-资料闭环与本手册怎么用)
2. [页面与画板映射](#2-页面与画板映射)
3. [目标工程结构(建议)](#3-目标工程结构建议)
4. [依赖与原生能力](#4-依赖与原生能力)
5. [启动、配置与认证](#5-启动配置与认证)
6. [请求与字段映射(框架行为)](#6-请求与字段映射框架行为)
7. [API 与数据实体总表](#7-api-与数据实体总表)
8. [分屏实施规格](#8-分屏实施规格)
9. [生图状态机与轮询](#9-生图状态机与轮询)
10. [历史 24 小时与倒计时](#10-历史-24-小时与倒计时)
11. [积分流水与支付记录](#11-积分流水与支付记录)
12. [内购(积分购买)](#12-内购积分购买)
13. [举报(反馈)](#13-举报反馈)
14. [注销账号](#14-注销账号)
15. [主题、字体与资源](#15-主题字体与资源)
16. [权限与合规](#16-权限与合规)
17. [调试、环境与联调](#17-调试环境与联调)
18. [测试与上线检查清单](#18-测试与上线检查清单)
19. [导航关系](#19-导航关系)
20. [外部文档索引](#20-外部文档索引)
21. [修订记录](#21-修订记录)
---
## 1. 资料闭环与本手册怎么用
| 层级 | 内容 | 文档/代码 |
|------|------|-----------|
| **产品/UI** | 视觉与交互 | `desgin/funymee_home.pen`、同目录 PNG 切图 |
| **运行时配置** | BaseUrl、AES、代理键、归因、字段映射 | `assets/skin_config.json` |
| **框架 API** | 已封装的 REST 调用与实体 | 本地依赖 `../client_proxy_framework`,见下文 API 表 |
| **协议深度** | V2 包装、密文键名、错误码 | [FunyMeeAI_client_guide.md](./FunyMeeAI_client_guide.md) |
| **工程/Android/iOS/SDK** | 换皮脚手架、清单、Gradle | [skin_app_development_guide.md](./skin_app_development_guide.md)、`client_proxy_framework/docs/sdk_integration_guide.md` |
| **配置项语义** | 各 JSON 键说明 | [new_app_config_template.md](./new_app_config_template.md) |
**推荐阅读顺序**:本手册 §19 → 开始写壳与首页 → 对照 client_guide 联调接口 → skin_guide 处理 SDK/权限遗漏。
---
## 2. 页面与画板映射
来源:`desgin/funymee_home.pen` 根级 Frame`children` 顶层)。
| 顺序 | 画板名称 | Pencil 根 id | 说明 |
|------|----------|----------------|------|
| 1 | FunyMee Home | `bi8Au` | 首页 |
| 2 | 通用背景 · 黄→白渐变 | `suXxr` | 可复用渐变,非独立路由 |
| 3 | My History 页面 | `WBRp4` | 历史24h 提示与卡片时间见设计稿 |
| 4 | Credit Record 页面 | `QR6Gq` | 积分流水 Tab |
| 5 | Purchase Point 页面 | `ETbdo` | 购买积分 |
| 6 | 生成图片 页面 | `EYsUi` | 选图/发起任务前 |
| 7 | 生成图片 · 生成中 | `YoZaK` | 进行中 |
| 8 | 生成图片 · 已完成 | `c7R7z` | 完成态 |
| 9 | 生成图片 · 可下载 | `2SyyL` | 可下载 |
| 10 | 生成图片 · 单图 | `0iJjl` | 单图结果变体 |
| 11 | 个人中心 | `5J8Po` | 设置、积分入口、注销入口等 |
| 12 | 注销账户 · 步骤1 | `SYt0O` | |
| 13 | 注销账户 · 步骤2 | `yxwpg` | 二次确认 |
| 14 | 通用 · 加载中 | `8rpyo` | 全屏 Loading 组件 |
| 15 | 举报 | `Y9WlO` | **仅从生图完成后的结果页进入** |
补充切图(如 `desgin/订阅.png`)若单独成页,在本表追加一行并分配路由名。
---
## 3. 目标工程结构(建议)
在现有 `lib/` 上扩展(与 [skin_app_development_guide.md](./skin_app_development_guide.md) 一致):
```
lib/
├── main.dart
├── app.dart # MaterialApp + 路由入口
├── core/
│ ├── auth/auth_service.dart # 已有
│ ├── user/user_state.dart # 已有;可改为 ChangeNotifier
│ ├── theme/ # 色板、字体、圆角
│ ├── routing/ # 路由表、RoutePath 常量
│ └── widgets/ # 全屏 Loading、通用渐变背景等
└── features/
├── home/
├── generate/ # 选图 → 中 → 完成 → 下载 / 单图
├── history/ # My History + Credit RecordTab
├── purchase/
├── profile/ # 个人中心 + 注销两步
└── report/ # 举报(从结果页 push
```
验收:**每个画板至少对应一个 `Widget` 文件与一个路由路径常量**。
---
## 4. 依赖与原生能力
当前 `pubspec.yaml` 已含:`client_proxy_framework``http``encrypt``crypto``logger``shared_preferences``android_id``device_info_plus`
**按功能待增(在实现到对应章节时加入)**
| 能力 | 典型 package | 使用场景 |
|------|----------------|----------|
| 路由 | `go_router`(可选) | 深链、声明式路由 |
| 选图/拍照 | `image_picker` | 生成页上传 |
| 网络图 | `cached_network_image` | 历史缩略图、结果图 |
| 保存相册 | `gal``image_gallery_saver``photo_manager` | 下载落盘 |
| 时间/国际化 | `intl` | 创建时间格式化 |
| UUID/路径 | `path``mime` | 上传文件名 |
框架侧已依赖 `in_app_purchase`(经 `client_proxy_framework` 传递);**确保 Android/iOS 内购能力与商店元数据在商店后台配置完成**。
---
## 5. 启动、配置与认证
### 5.1 `main.dart` 顺序(与现码一致)
1. `WidgetsFlutterBinding.ensureInitialized()`
2. `ClientBootstrap.initFromAsset('assets/skin_config.json')`
- 内部:`SkinConfig.fromJson``ApiClient.init(skin)`**此后方能发代理请求**
3. `ClientBootstrap.initAnalytics()`Adjust / Facebook 配置来自 `skin_config.analytics`
4. `AnalyticsService.initAttribution()`(若需与归因桥接)
5. `runApp(App(...))`
6. `AuthService.init()``FrameworkAuthService.init` + `start()`(设备 ID、`fast_login``common_info`
### 5.2 `skin_config.json` 必用字段(顶层)
| JSON 路径 | 用途 |
|-----------|------|
| `app.name` / `app.id` / `app.packageName` | 展示名、业务 appId、包名请求映射 |
| `backend.iosAppType` / `backend.androidAppType` | 接口 query `app``HIOS` / `HAndroid` 等 |
| `api.preBaseUrl` / `api.prodBaseUrl` / `api.proxyPath` / `api.aesKey` | 环境与加解密 |
| `api.alwaysUsePreBaseUrl` / `api.debugBaseUrlOverride` | 开发期切环境 |
| `proxyKeys.*` / `v2.*` | 代理外框字段名与 V2 噪声 |
| `fieldMapping` | 业务 JSON 键 ↔ V2 伪装键(巨大见文件;**查具体接口时打开 client_guide** |
| `analytics.adjust` / `analytics.facebook` | SDK 初始化 |
| `adjustEvents.*` | 埋点 token`AnalyticsService.track*` 对齐 |
### 5.3 业务侧 `app` 参数
`UserApi` / `ImageApi` 等需要 `queryParams['app']` 处:
- iOS`ClientBootstrap.skin.backendAppTypeIOS`(或 `ApiClient.instance.config` 同等字段)
- Android`backendAppTypeAndroid`
`defaultTargetPlatform` 对齐,**不要用 appId 字符串顶替**。
### 5.4 登录与鉴权
- **匿名设备登录**`UserApi.fastLogin`(框架在 `FrameworkAuthService` 内调用;宿主实现 `AuthServiceCallbacks` 提供 `getDeviceId``computeSign`)。
- **Token**:成功后写入 `ApiClient`/代理层;之后 `ProxyClient` 自动带 `pkg` + `User_token`(除 fast_login
- **`common_info`**:拉取 `CommonInfoResponse`(积分、用户信息、`extConfig``t2IConfig``payCenterUrl``userToken` 等),在 `AppAuthCallbacks.onCommonInfoLoaded` 同步到 `UserState`
- **业务请求前**`await AuthService.loginComplete`(或框架提供的 `FrameworkAuthService.loginComplete`),避免空 token。
### 5.5 `extConfig``skin_config` + common_info框架内解析
1. **`skin_config.json``extConfig`**(可选):配置 **wire 键名列表**`keys` / `itemKeys`)及本地 **`defaults`**(与线上下发同键;`common_info` 成功后与服务器 **浅合并**,服务器覆盖同名顶层键)。见 `ExtConfigKeySchema``SkinExtConfigSection`,导出见 `package:client_proxy_framework/client_proxy_framework.dart`
2. **`common_info.extConfig`**:线上 JSON 字符串;若与 `defaults` 合并后仍为空对象,则得到空的 `ExtConfigData`
`AppConfig` 提供 `extConfigKeySchema``extConfigDefaults`;手写 `AppConfig` 时可覆盖;**换皮仅用 JSON 时在 `SkinConfig` 中已注入**。
| 导出 | 用途 |
|------|------|
| `ExtConfigRuntime.data` | `ValueNotifier<ExtConfigData?>`,监听后刷新首页 Tab / Grid |
| `ExtConfigRuntime.commonInfoSucceeded` | `true` / `false` / `null`:是否成功拉到 common_info**建议仅在为 `true` 时展示核心业务 UI** |
| `ExtConfigData` | `showVideoMenu``allowScreenshot``allowThirdPartyPayment``privacyUrl``agreementUrl``items` |
| `ExtConfigItem` | 单项:`image``image_fix``img_need``cost``title``params` / `detail``taskExt``params ?? detail` |
| `kExtConfigItemsCategoryId` | 固定 `-1`,作「静态 items Tab」分类 id |
| `mergeHomeTabsWithExtConfigItems<T>()` | `showVideoMenu == true` 时在 API Tab 列表 **末尾** 追加静态 Tab |
| `ClientBootstrap.skin.extConfigKeySchema` 等 | `SkinConfig` 从 JSON `extConfig.keys` 注入;宿主可只改配置不换代码 |
**默认 wire 键(可在 `skin_config.extConfig.keys` 中整体改写):**
| 语义 | 默认候选键(首个存在则生效) |
|------|------------------------------|
| 展示顶部 Video Tab 栏 + items 固定最后一格 | `go_run``need_wait` |
| 允许截屏 | `screen`;若无则看 `safe_area``true` ⇒ 不允许截屏) |
| 允许第三方支付 | `san_fang``lucky` |
| 隐私 / 协议 URL | `privacy``agreement` |
| items 数组 | `items` |
**`itemKeys` 默认(可省略 `imageFix`,仍解析 `image_fix`** `image``image_fix``img_need``cost``title``params``detail`
示例(与当前后端约定一致时可原样使用):
```json
{
"go_run": false,
"screen": false,
"san_fang": false,
"privacy": "https://example.com/privacy",
"agreement": "https://example.com/terms",
"items": [
{
"image": "https://cdn.example.com/a.png",
"image_fix": "https://cdn.example.com/a.png",
"img_need": 2,
"cost": 1,
"title": "BananaTask",
"params": "animal_expression"
}
]
}
```
首页逻辑建议(对齐 `app_client``await FrameworkAuthService.loginComplete` 后判断 `ExtConfigRuntime.commonInfoSucceeded.value == true` 再进入主页;`showVideoMenu == true` 时展示顶部 Tab分类列表用 `mergeHomeTabsWithExtConfigItems` 把静态 Tab 放在最后,**该 Tab 的 Grid 数据源为 `ExtConfigRuntime.data.value?.items`**。第三方支付入口用 `allowThirdPartyPayment`;截屏策略用 `shouldPreventCapture` 或自行根据 `allowScreenshot` 调用宿主侧防护(框架不强制依赖 `screen_secure`)。
---
## 6. 请求与字段映射(框架行为)
- 宿主**只使用** `UserApi``ImageApi``PaymentApi``FeedbackApi` 等,**Body/Query 使用业务原始字段名**(如 `deviceId``taskId`)。
- `ProxyClient` 负责:按 `skin_config.fieldMapping` 做键名映射、V2 包装、AES、噪音字段等。
- 查阅某一字段的密文键名:在 [FunyMeeAI_client_guide.md](./FunyMeeAI_client_guide.md) 或 `skin_config.json``fieldMapping` 中搜索原始键。
---
## 7. API 与数据实体总表
以下均定义于 `client_proxy_framework/lib/src/services/*.dart`,实体在 `entities/`
### 7.1 用户
| 方法 | Path | 说明 |
|------|------|------|
| `UserApi.fastLogin` | `POST /v1/user/fast_login` | 设备登录 |
| `UserApi.referrer` | `POST /v1/user/referrer` | 归因上报 |
| `UserApi.getCommonInfo` | `GET /v1/user/common_info` | 通用信息(首页配置、积分、扩展 JSON |
| `UserApi.getAppLanguage` | `GET /v1/config/app-language` | 语言配置 |
| `UserApi.getAccount` | `GET /v1/user/account` | 账户信息 |
| `UserApi.getCreditsPage` | `GET /v1/user/credits-page` | **积分分页流水**`page/size/type` |
| `UserApi.getUserPayments` | `GET /v1/user/payments` | **支付/订单记录列表** |
| `UserApi.getUnreadMessageCount` | `GET /v1/user/unread-message-count` | 未读消息 |
| `UserApi.deleteAccount` | `GET /v1/user/delete` | **注销** |
核心实体:`FastLoginResponse``CommonInfoResponse``AccountResponse``CreditsPageInfoResponse`(含 `CreditRecordItem`)、`UserPaymentsListResponse`
### 7.2 图片 / 任务
| 方法 | Path | 说明 |
|------|------|------|
| `ImageApi.getCategoryList` | `GET /v1/image/img2video/categories` | 分类 |
| `ImageApi.getImg2VideoTasks` | `GET /v1/image/img2video/tasks` | 图生视频任务模板列表 |
| `ImageApi.getPromptRecommends` | `GET /v1/image/prompt/recomends` | 推荐提示词 |
| `ImageApi.createTxt2Img` | `POST /v1/image/txt2img_create` | 文生图 |
| `ImageApi.createImg2VideoPose` | `POST /v1/image/img2video_pose_task` | 图生视频姿态任务 |
| `ImageApi.getProgress` | `GET /v1/image/progress` | **任务进度** |
| `ImageApi.getImg2VideoPoseTemplates` | `GET /v1/image/img2Video_pose_template` | 姿态模板 |
| `ImageApi.getUploadPresignedUrl` | `POST /v1/image/upload-presigned-url` | **用户图预签名上传** |
| `ImageApi.createTask` | `POST /v1/image/create-task` | **主创建任务**(多参数,与后端协定) |
| `ImageApi.getMyTasks` | `GET /v1/image/my-tasks` | **我的任务列表** |
核心实体:`CreateTaskResponse``ProgressResponse``status``progress``resultUrl`)、`MyTasksResponse` / `MyTaskItem``taskId``status``createTime``resultUrl` 等)、`UploadPresignedUrlResponse`
**与 app_client 对齐(框架内、无 UI**
- **上传图 ↔ 任务 id 本地缓存**:创建任务成功后调用 `TaskUploadCoverStore.saveAfterCreateTaskResponse(response:, source:)`(或 `saveAfterCreateTaskBody` / `saveForTask`)。文件在应用 support 目录下 `gallery_upload_covers/`,默认 25h 过期清理,文件名与数字 `taskId` 一致(如 `123.jpg`)。
- **我的任务列表**:仍用 `ImageApi.getMyTasks``MyTasksResponse` 同时兼容列表键 `tasks` / `intensify``hasNext` / `manifest``MyTaskItem` / `CreateTaskResponse` 中任务 id 兼容 `taskId` / `tree` / `id`(解密后的 business 字段)。
- **与 app_client Gallery 相同的行模型**`GalleryTaskItem` / `GalleryMediaItem``listingDisplayFromApi` 等见 `gallery_task_models.dart`;原始列表 Map 可用 `ImageTaskHistory.parseGalleryTasksFromData` 解析,本机封面路径用 `ImageTaskHistory.localCoverPathsForGalleryTasks`(或与 `MyTaskItem` 对应的 `localCoverPathsForMyTaskItems`)在刷新列表后合并。
**FunyMee 推荐直接调用的封装(框架已导出)**
- **`compressImageForUpload` / `CompressImageForUploadOptions`**:上传前压图。
- **`ImagePresignedUploadCreateTaskFlow.run`**:压图(可关)→ `getUploadPresignedUrl` → HTTP PUT → `createTask` → 可选 `TaskUploadCoverStore``UploadPresignedUrlResponse` 支持 `putHeaders` 等与 PUT 合并;若后端只要 `imgUrl` 可设 `createTaskUseImgUrlOnly: true`
- **`UserAccountRefresh.fetchAndNotify`**`getAccount` + 回调,无 UI 状态。
- **`AdjustService.obtainReferrerForUpload`**:返回 `ReferrerForUpload``digest` + `source`),供 `UserApi.referrer` 等与参考产品一致。
- **`ensureDeviceMemoryProfileInitialized`**(默认通道 `client_proxy_framework/device_memory`,插件已接 Android `ActivityManager.totalMem`)、`deviceGridMaxConcurrentVideos` 等。
- **`VideoThumbnailCache.instance`**:远程视频缩略图 / 海报帧缓存。
### 7.3 支付
| 方法 | Path | 说明 |
|------|------|------|
| `PaymentApi.getGooglePayActivities` | `GET /v1/payment/getGooglePayActivities` | Android 商品活动 |
| `PaymentApi.getApplePayActivities` | `GET /v1/payment/getApplePayActivities` | iOS 商品活动 |
| `PaymentApi.getPaymentMethods` | `POST /v1/payment/get-payment-methods` | 某活动支付方式 |
| `PaymentApi.createPayment` | `POST /v1/payment/createPayment` | 创建订单 |
| `PaymentApi.getPaymentDetailList` / `getOrderDetail` 等 | 见 `payment_api.dart` | 订单详情、列表 |
内购验证与 `PurchaseDetails` 流封装见 `PaymentService``client_proxy_framework`)。
### 7.4 反馈(举报)
| 方法 | Path | 说明 |
|------|------|------|
| `FeedbackApi.getUploadPresignedUrl` | `POST /v1/feedback/upload-presigned-url` | 截图/凭证上传 |
| `FeedbackApi.submit` | `POST /v1/feedback/submit` | 提交(`fileUrls``contentType``content` |
实体:`SubmitFeedbackResponse``FeedbackUploadPresignedUrlResponse`
---
## 8. 分屏实施规格
| 页面 | 主要 API / 数据源 | 关键实体与注意事项 |
|------|-------------------|-------------------|
| **FunyMee Home** | `getCommonInfo`框架内已拉Tab/静态列表用 **`ExtConfigRuntime` / `ExtConfigData`**;其它首页结构可解析 `t2IConfig` | 见 **§5.5**;静态 Tab id 使用 `kExtConfigItemsCategoryId` |
| **生成图片** | `getUploadPresignedUrl` + HTTP PUT 到 `uploadUrl``createTask``createTxt2Img` / 业务指定路径 | 传 `userId``app`;参数以后台为准 |
| **生成中** | `getProgress` 轮询 | `taskId` 来自创建响应;`status` / `progress` 与 client_guide 状态枚举对齐 |
| **已完成 / 可下载 / 单图** | 同一任务不同 UI 态;数据来自 `ProgressResponse``MyTaskItem` | 下载前校验 `resultUrl`**举报入口放此组页面** |
| **My History** | `getMyTasks`(分页 `page` / `pageSize` / `cursor` 按后端) | 展示 `createTime``resultUrl`、状态;无远程封面时可合并 `TaskUploadCoverStore`;过期逻辑见 §10 |
| **Credit Record** | `getCreditsPage` | `CreditRecordItem.createTime` 为整型时间戳(秒或毫秒需与后端确认) |
| **Purchase Point** | `getGooglePayActivities` / `getApplePayActivities``getPaymentMethods``createPayment` + 平台 IAB | 对齐 `PaymentService` 与订单恢复 |
| **个人中心** | `getAccount`;展示 `common_info` 冗余字段 | 跳转购买、注销;**无举报** |
| **注销** | 两步 UI + `deleteAccount` | 成功后清除本地 token、回首页或登录流 |
| **举报** | `FeedbackApi` 预签名上传 + `submit` | `contentType` 与后台枚举一致(见 client_guide |
| **全屏 Loading** | 无 API | 覆盖路由栈或全局 `Overlay` |
---
## 9. 生图状态机与轮询
推荐流程:
1. **创建**`createTask`(或文生图等)→ 取 `taskId`;若有用户上传的本地待传文件,成功后调用 `TaskUploadCoverStore.saveAfterCreateTaskResponse` 与任务 id 关联(见 §7.2)。
2. **轮询**:循环 `ImageApi.getProgress(app:, taskId:, userId:)`,间隔 13s带** backoff** 与**页面 dispose 取消**。
3. **状态映射**(字符串以**后端文档为准**,此处为占位):
- `pending` / `processing` → 生成中页
- `success` / `completed``resultUrl` 有效 → 可下载 / 单图
- `failed` → 错误态 UI + 重试/客服
4. **离开 App 再进入**:用 `getMyTasks` 或持久化 `taskId` 恢复轮询。
详细状态码表:在 **client_guide** 搜索 `progress` / `task` / `status`
---
## 10. 历史 24 小时与倒计时
- 列表项展示 **创建时间**:优先 `MyTaskItem.createTime`格式由后端决定ISO8601 或时间戳用 `intl`/自建解析)。
- **「剩余可下载时间」**:若接口**未**直接返回过期时间戳,则采用产品规则:
`deadline = createdAt + 24h`,剩余 = `deadline - DateTime.now()`;展示与 **My History** 画板一致(天/时/分)。
- 过期后:隐藏或置灰 Download可选调用后台是否仍返回 `resultUrl` 以决定客户端行为。
---
## 11. 积分流水与支付记录
- **积分流水Credit Record 页)**`UserApi.getCreditsPage(page:, size:, type:)`
**`type` 含义必须与后台确认**(过滤消费/充值等);分页用 `total/current/pages`
- **支付记录(若单独 Tab**`UserApi.getUserPayments(app:, userId:)`,实体 `UserPaymentsListResponse`
---
## 12. 内购(积分购买)
### 12.1 编排层(推荐,`package:client_proxy_framework/...` 已导出 `payment_flow.dart`
| 类型 | 作用 |
|------|------|
| `PaymentSettlementSink` | 宿主实现 `onPaymentSettled`**仅此一处**拉 `common_info` / 更新 `UserState` / 埋点 |
| `PaymentSettlement` / `PaymentFlowOutcomeType` | 成功 / 失败 / 取消 / 超时 等统一结果 |
| `PaymentFlowCatalog.loadStoreActivities` | 按平台调 `getGooglePayActivities` / `getApplePayActivities` |
| `PaymentApi.getPaymentMethods` | 仍直接调用;选完后走三方或内购 |
| `ThirdPartyCheckoutCoordinator.createOrder` | 封装 `createPayment`,得到 `orderId` + `payUrl` |
| `ThirdPartyCheckoutCoordinator.openPayUrlIfPresent` | 需传入宿主 `PaymentCheckoutUrlLauncher`WebView / `launchUrl` |
| `ThirdPartyPaymentWatch` | 宿主在打开收银台后调用 `start(orderId:)``stop()` 停止;终态只触发一次 `sink` |
| `NativeIapCoordinator.purchaseGooglePlay` | **仅 Android**`createPayment` → 拉起购买 → `googlepay` → consume`federation` 映射) |
**宿主策略**:根据 `ExtConfigRuntime.data``allowThirdPartyPayment`(或自研规则)选择调用 `ThirdPartyCheckoutCoordinator` + `ThirdPartyPaymentWatch`,或 `NativeIapCoordinator.purchaseGooglePlay`**不要在框架里写死分支**。
### 12.2 底层 API仍可直接用
1. 拉商品活动:`PaymentApi.getGooglePayActivities` / `getApplePayActivities`
2. 用户选档位 → `PaymentApi.getPaymentMethods(activityId: int)`
3. `PaymentApi.createPayment``PaymentApi.googlepay``PaymentService` 补单等:**编排类内部已组合**,单独对接时仍以 client_guide 为准。
---
## 13. 举报(反馈)
1. **入口**:仅从 **生图完成后的结果页**(已完成 / 可下载 / 单图)进入 `Y9WlO` 对应界面。
2. 可选图片:`FeedbackApi.getUploadPresignedUrl` → PUT 文件 → 得到 `fileUrl` 列表。
3. `FeedbackApi.submit(fileUrls:, content:, contentType:)``content` 可含任务 id、原因枚举等约定字段。
---
## 14. 注销账号
1. UI`SYt0O``yxwpg`
2. 调用 `UserApi.deleteAccount(app:, userId:)`GET**确认是否需额外 body/query**。
3. 清理:`ApiClient` 侧 token、本地 `SharedPreferences`、再 `runApp` 或导航到首页。
---
## 15. 主题、字体与资源
- 颜色/圆角:对齐 Pencil主强调色与 Tab 下划线可参考 `#c99304`
- 字体:`Inter` + `Bonheur Royale`(见历史卡片 Download 标签);在 `pubspec.yaml` 注册 `fonts:` 并放入 `assets/fonts/`
- 首页背景:设计使用 `首页.png`,可复制为 `assets/images/` 或完全用 `LinearGradient` 复刻 `suXxr`
---
## 16. 权限与合规
- **相册/存储**保存生成图、选图上传前申请权限iOS `NSPhotoLibraryUsageDescription` 等)。
- **网络**ATS / 明文规则按环境配置。
- **Adjust / Facebook**:按 skin_guide 填 `AndroidManifest.xml``Info.plist`
- **隐私政策 URL**:若 `common_info` / `h5UrlConfig` 下发,在个人中心打开。
---
## 17. 调试、环境与联调
- 切换预发布:`skin_config.api.alwaysUsePreBaseUrl: true``debugBaseUrlOverride`
- 抓包:注意请求体为代理包装后的 JSON**解密验证用 client_guide + 后端工具**。
- `Logger` / `AnalyticsService` 调试开关见 `skin_config.analytics.debugLogs`
---
## 18. 测试与上线检查清单
- [ ] 冷启动:无崩溃,`loginComplete` 后首屏可请求接口
- [ ] 生图全链路:创建 → 进度 → 结果 → 下载(或保存相册)
- [ ] 历史分页与 24h 展示正确
- [ ] 积分流水 `type`、分页正确
- [ ] 内购沙盒成功、积分刷新、Adjust 事件(若配置)
- [ ] 举报:仅结果页可见、提交成功
- [ ] 注销:账号不可用、本地状态清空
- [ ] 弱网 / 后台恢复 / 任务恢复
---
## 19. 导航关系
```mermaid
flowchart TB
Home[FunyMee Home]
GenFlow[生成图片→生成中]
GenResult[生图完成: 已完成/可下载/单图]
Hist[My History]
Credit[Credit Record]
Buy[Purchase Point]
Profile[个人中心]
Report[举报]
Del1[注销 步骤1]
Del2[注销 步骤2]
Home --> GenFlow
GenFlow --> GenResult
GenResult --> Report
Home --> Hist
Home --> Profile
Hist --- Credit
Profile --> Buy
Profile --> Del1 --> Del2
```
---
## 20. 外部文档索引
| 文档 | 用途 |
|------|------|
| [skin_app_development_guide.md](./skin_app_development_guide.md) | 换皮、Android/iOS、SDK、`AppAuthCallbacks` 模板 |
| [FunyMeeAI_client_guide.md](./FunyMeeAI_client_guide.md) | 代理/V2/接口细节、错误码 |
| [new_app_config_template.md](./new_app_config_template.md) | 配置模板说明 |
| `assets/skin_config.json` | 本应用实际配置 |
| `../client_proxy_framework/lib/src/services/*.dart` | API 源码与注释 |
---
## 21. 修订记录
| 日期 | 说明 |
|------|------|
| 2026-04-07 | 初版:页面清单与启动流程 |
| 2026-04-07 | 举报入口:生图完成页 |
| 2026-04-07 | **完整版**API 总表、分屏规格、状态机、支付/举报/注销、`skin_config`、工程结构、测试清单;明确与 client_guide / skin_guide 的闭环关系 |
| 2026-04-07 | 框架增加 `ExtConfigData` / `ExtConfigRuntime`:解析 `go_run`/`screen`/`san_fang` 与旧键名common_info 成功后更新;手册 §5.5 |
| 2026-04-07 | `skin_config.extConfig``keys`/`itemKeys`/`defaults``ExtConfigKeySchema`;与 common_info 浅合并 |
| 2026-04-07 | 支付编排:`PaymentSettlementSink``ThirdPartyCheckoutCoordinator``ThirdPartyPaymentWatch``NativeIapCoordinator``CreatePaymentResponse.federation` |

View File

@ -58,6 +58,29 @@
*** ***
## 五点五、extConfig`skin_config.json`,换皮即配)
**`skin_config.json` 根级**增加 `extConfig` 后,无需改代码即可指定:
1. **逻辑字段** ↔ 线上下发 JSON **键名** 的候选列表(兼容多版本后端键名)。
2. **`defaults`**:本地默认对象(与 `common_info.extConfig` **同一套 wire 键**`common_info` 成功后会与服务器 JSON **浅合并****服务器顶层键覆盖同名键**。
典型结构见仓库内 `assets/skin_config.json``extConfig` 示例;框架内类型为 `ExtConfigKeySchema` / `SkinExtConfigSection``client_proxy_framework`)。
| 子节 | 说明 |
|------|------|
| `keys.showVideoMenu` | 字符串数组,如 `["go_run","need_wait"]`,依次为布尔字段候选键 |
| `keys.allowScreenshot` | 直接表示「允许截屏」的键,如 `["screen"]` |
| `keys.blockScreenshot` | 为 `true` 时表示**禁止**截屏的键,如 `["safe_area"]` |
| `keys.allowThirdPartyPayment` | 如 `["san_fang","lucky"]` |
| `keys.privacyUrl` / `agreementUrl` / `items` | 隐私、协议 URL、items 数组所在键名 |
| `itemKeys` | **固定列表项字段**(不含业务上可省略的兜底图时,可不写 `imageFix`,框架默认 `image_fix``image``imgNeed``cost``title``params``detail``imageFix` 可选 |
| `defaults` | 与线上下发相同键名的对象,可放静态 `items` 列表供无网或后台未下发时使用 |
省略整个 `extConfig` 时,解析行为与内置默认键名一致(见 FunyMee 开发手册 §5.5)。
***
## 六、Adjust SDK 配置 ## 六、Adjust SDK 配置
| 配置项 | 值 | 说明 | | 配置项 | 值 | 说明 |

View File

@ -198,6 +198,7 @@ dependencies:
crypto: ^3.0.3 crypto: ^3.0.3
logger: ^2.0.2 logger: ^2.0.2
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
android_id: ^0.5.1
device_info_plus: ^11.1.0 device_info_plus: ^11.1.0
``` ```
@ -472,13 +473,30 @@ class MainActivity : FlutterActivity() {
```dart ```dart
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'package:android_id/android_id.dart';
import 'package:client_proxy_framework/client_proxy_framework.dart'; import 'package:client_proxy_framework/client_proxy_framework.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../user/user_state.dart'; import '../user/user_state.dart';
const _prefsKeyFallbackDeviceId = 'persisted_device_id';
Future<String> _persistedFallbackDeviceId() async {
final prefs = await SharedPreferences.getInstance();
var id = prefs.getString(_prefsKeyFallbackDeviceId);
if (id != null && id.isNotEmpty) return id;
final random = Random.secure();
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
id = base64UrlEncode(bytes).replaceAll('=', '');
await prefs.setString(_prefsKeyFallbackDeviceId, id);
return id;
}
/// 归因回调实现 /// 归因回调实现
class AppAttributionCallbacks implements AttributionCallbacks { class AppAttributionCallbacks implements AttributionCallbacks {
@override @override
@ -523,18 +541,25 @@ class AppAttributionCallbacks implements AttributionCallbacks {
/// 认证回调实现 /// 认证回调实现
class AppAuthCallbacks implements AuthServiceCallbacks { class AppAuthCallbacks implements AuthServiceCallbacks {
/// 与 app_client 一致Android 用 Settings.Secure.ANDROID_ID`android_id` 包);
/// `device_info_plus``AndroidDeviceInfo.id` 是 ROM 构建号,不能作为设备 ID。
/// iOS 用 `identifierForVendor`;读失败或其它平台用 SharedPreferences 持久化随机 id。
@override @override
Future<String> getDeviceId() async { Future<String> getDeviceId() async {
final deviceInfo = DeviceInfoPlugin();
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.android: case TargetPlatform.android:
final android = await deviceInfo.androidInfo; final androidId = await const AndroidId().getId();
return android.id; if (androidId != null && androidId.isNotEmpty) {
return androidId;
}
return _persistedFallbackDeviceId();
case TargetPlatform.iOS: case TargetPlatform.iOS:
final ios = await deviceInfo.iosInfo; final ios = await DeviceInfoPlugin().iosInfo;
return ios.identifierForVendor ?? 'ios-unknown'; final idfv = ios.identifierForVendor;
if (idfv != null && idfv.isNotEmpty) return idfv;
return _persistedFallbackDeviceId();
default: default:
return 'device-${DateTime.now().millisecondsSinceEpoch}'; return _persistedFallbackDeviceId();
} }
} }

View File

@ -76,5 +76,9 @@
<string>TODO: Add Facebook Client Token</string> <string>TODO: Add Facebook Client Token</string>
<key>FacebookDisplayName</key> <key>FacebookDisplayName</key>
<string>FunyMee AI</string> <string>FunyMee AI</string>
<key>FacebookAutoLogAppEventsEnabled</key>
<false/>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<false/>
</dict> </dict>
</plist> </plist>

View File

@ -1,25 +1,47 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'package:android_id/android_id.dart';
import 'package:client_proxy_framework/client_proxy_framework.dart'; import 'package:client_proxy_framework/client_proxy_framework.dart';
import 'package:crypto/crypto.dart' show md5; import 'package:crypto/crypto.dart' show md5;
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../user/user_state.dart'; import '../user/user_state.dart';
const _prefsKeyFallbackDeviceId = 'persisted_device_id';
Future<String> _persistedFallbackDeviceId() async {
final prefs = await SharedPreferences.getInstance();
var id = prefs.getString(_prefsKeyFallbackDeviceId);
if (id != null && id.isNotEmpty) return id;
final random = Random.secure();
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
id = base64UrlEncode(bytes).replaceAll('=', '');
await prefs.setString(_prefsKeyFallbackDeviceId, id);
return id;
}
class AppAuthCallbacks implements AuthServiceCallbacks { class AppAuthCallbacks implements AuthServiceCallbacks {
/// app_client Android Settings.Secure.ANDROID_IDandroid_id Build.ID
/// iOS identifierForVendor SharedPreferences id
@override @override
Future<String> getDeviceId() async { Future<String> getDeviceId() async {
final deviceInfo = DeviceInfoPlugin();
switch (defaultTargetPlatform) { switch (defaultTargetPlatform) {
case TargetPlatform.android: case TargetPlatform.android:
final android = await deviceInfo.androidInfo; final androidId = await const AndroidId().getId();
return android.id; if (androidId != null && androidId.isNotEmpty) {
return androidId;
}
return _persistedFallbackDeviceId();
case TargetPlatform.iOS: case TargetPlatform.iOS:
final ios = await deviceInfo.iosInfo; final ios = await DeviceInfoPlugin().iosInfo;
return ios.identifierForVendor ?? 'ios-unknown'; final idfv = ios.identifierForVendor;
if (idfv != null && idfv.isNotEmpty) return idfv;
return _persistedFallbackDeviceId();
default: default:
return 'device-${DateTime.now().millisecondsSinceEpoch}'; return _persistedFallbackDeviceId();
} }
} }

View File

@ -9,6 +9,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.5.1" version: "5.5.1"
android_id:
dependency: "direct main"
description:
name: android_id
sha256: "50d62501d623a7e7358b3ceffd1bdd9b420292eba66cec8347d33ed10791f28e"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -64,6 +80,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@ -175,6 +199,22 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
hooks:
dependency: transitive
description:
name: hooks
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
url: "https://pub.dev"
source: hosted
version: "1.0.2"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@ -191,6 +231,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev"
source: hosted
version: "4.8.0"
in_app_purchase: in_app_purchase:
dependency: transitive dependency: transitive
description: description:
@ -223,6 +271,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.8+1" version: "0.4.8+1"
jni:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
url: "https://pub.dev"
source: hosted
version: "1.0.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -279,6 +343,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.0" version: "2.7.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -303,6 +375,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.17.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
url: "https://pub.dev"
source: hosted
version: "0.17.6"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -311,6 +407,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "914a07484c4380e572998d30486e77e0d9cd2faec72fee268086d07bf7f302c9"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@ -335,6 +455,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -367,6 +495,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.1" version: "3.9.1"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -492,6 +636,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
video_thumbnail:
dependency: transitive
description:
name: video_thumbnail
sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b"
url: "https://pub.dev"
source: hosted
version: "0.5.6"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -532,6 +684,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks: sdks:
dart: ">=3.11.1 <4.0.0" dart: ">=3.11.1 <4.0.0"
flutter: ">=3.35.0" flutter: ">=3.38.4"

View File

@ -18,6 +18,7 @@ dependencies:
crypto: ^3.0.3 crypto: ^3.0.3
logger: ^2.0.2 logger: ^2.0.2
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
android_id: ^0.5.1
device_info_plus: ^11.1.0 device_info_plus: ^11.1.0
dev_dependencies: dev_dependencies: