# 插件开发
**本文引用的文件**
- [server/src/app.ts](file://server/src/app.ts)
- [server/src/config/index.ts](file://server/src/config/index.ts)
- [server/src/types/index.ts](file://server/src/types/index.ts)
- [server/src/modules/tts/tts.controller.ts](file://server/src/modules/tts/tts.controller.ts)
- [server/src/modules/tts/tts.service.ts](file://server/src/modules/tts/tts.service.ts)
- [server/src/modules/tts/aliyun.provider.ts](file://server/src/modules/tts/aliyun.provider.ts)
- [server/src/modules/tts/minimax.provider.ts](file://server/src/modules/tts/minimax.provider.ts)
- [server/src/modules/tts/mock.provider.ts](file://server/src/modules/tts/mock.provider.ts)
- [server/src/modules/tts/aliyun-realtime.provider.ts](file://server/src/modules/tts/aliyun-realtime.provider.ts)
- [server/src/modules/tts/audio-merger.ts](file://server/src/modules/tts/audio-merger.ts)
- [server/src/services/storage.service.ts](file://server/src/services/storage.service.ts)
- [server/src/services/oss.service.ts](file://server/src/services/oss.service.ts)
- [server/src/middleware/errorHandler.js](file://server/src/middleware/errorHandler.js)
## 目录
1. [简介](#简介)
2. [项目结构](#项目结构)
3. [核心组件](#核心组件)
4. [架构总览](#架构总览)
5. [详细组件分析](#详细组件分析)
6. [依赖关系分析](#依赖关系分析)
7. [性能考量](#性能考量)
8. [故障排查指南](#故障排查指南)
9. [结论](#结论)
10. [附录](#附录)
## 简介
本文件面向希望为AI有声书生成平台开发“插件”的工程师,系统讲解如何开发三类插件:
- TTS音色提供商插件:对接不同厂商的语音合成能力
- AI模型插件:扩展文本生成、理解等LLM能力
- 存储插件:统一管理本地与云存储(如OSS)
文档覆盖插件架构设计、接口规范、生命周期管理、注册机制、依赖注入与配置管理,并提供开发模板、最佳实践与调试技巧,以及插件间通信、错误处理与性能优化策略。
## 项目结构
平台采用模块化与分层架构:
- 应用入口负责中间件、静态资源、路由注册与服务初始化
- 配置中心集中管理模型与第三方服务配置
- TTS模块负责音色合成、文本分段、并发与合并、状态查询
- 存储模块统一抽象本地与OSS,屏蔽差异
- 中间件提供错误处理、安全与性能监控
```mermaid
graph TB
subgraph "应用层"
APP["应用入口
server/src/app.ts"]
ROUTER["路由注册
TTS/业务路由"]
end
subgraph "服务层"
TTS_CTRL["TTS控制器
tts.controller.ts"]
TTS_SVC["TTS服务
tts.service.ts"]
STORAGE["存储服务
storage.service.ts"]
OSS["OSS服务
oss.service.ts"]
end
subgraph "插件层"
ALIYUN["阿里云TTS插件
aliyun.provider.ts"]
MINIMAX["MiniMax TTS插件
minimax.provider.ts"]
MOCK["Mock TTS插件
mock.provider.ts"]
REALTIME["阿里云实时TTS插件
aliyun-realtime.provider.ts"]
MERGER["音频合并器
audio-merger.ts"]
end
subgraph "配置与类型"
CFG["配置中心
config/index.ts"]
TYPES["类型定义
types/index.ts"]
end
APP --> ROUTER
ROUTER --> TTS_CTRL
TTS_CTRL --> TTS_SVC
TTS_SVC --> ALIYUN
TTS_SVC --> MINIMAX
TTS_SVC --> MOCK
TTS_SVC --> REALTIME
TTS_SVC --> MERGER
TTS_SVC --> STORAGE
STORAGE --> OSS
APP --> CFG
APP --> TYPES
```
图表来源
- [server/src/app.ts:1-194](file://server/src/app.ts#L1-L194)
- [server/src/modules/tts/tts.controller.ts:1-274](file://server/src/modules/tts/tts.controller.ts#L1-L274)
- [server/src/modules/tts/tts.service.ts:1-715](file://server/src/modules/tts/tts.service.ts#L1-L715)
- [server/src/modules/tts/aliyun.provider.ts:1-152](file://server/src/modules/tts/aliyun.provider.ts#L1-L152)
- [server/src/modules/tts/minimax.provider.ts:1-280](file://server/src/modules/tts/minimax.provider.ts#L1-L280)
- [server/src/modules/tts/mock.provider.ts:1-61](file://server/src/modules/tts/mock.provider.ts#L1-L61)
- [server/src/modules/tts/aliyun-realtime.provider.ts:1-158](file://server/src/modules/tts/aliyun-realtime.provider.ts#L1-L158)
- [server/src/modules/tts/audio-merger.ts:1-86](file://server/src/modules/tts/audio-merger.ts#L1-L86)
- [server/src/services/storage.service.ts:1-278](file://server/src/services/storage.service.ts#L1-L278)
- [server/src/services/oss.service.ts:1-256](file://server/src/services/oss.service.ts#L1-L256)
- [server/src/config/index.ts:1-117](file://server/src/config/index.ts#L1-L117)
- [server/src/types/index.ts:1-124](file://server/src/types/index.ts#L1-L124)
章节来源
- [server/src/app.ts:1-194](file://server/src/app.ts#L1-L194)
- [server/src/config/index.ts:1-117](file://server/src/config/index.ts#L1-L117)
- [server/src/types/index.ts:1-124](file://server/src/types/index.ts#L1-L124)
## 核心组件
- 应用入口与中间件
- 负责全局中间件(CORS、日志、安全、限流、Sentry)、静态资源挂载、路由注册与优雅关闭
- 初始化数据库、Redis、存储、WebSocket、书籍生成队列
- 配置中心
- 统一加载.env与models.json,提供模型枚举、启用筛选、错误识别与自动切换辅助
- TTS控制器与服务
- 控制器负责参数校验、配额检查、路由转发
- 服务负责文本分段、并发合成、合并、上传、状态查询、回调与WebSocket推送
- 存储服务
- 抽象本地与OSS,提供上传/下载/删除/签名URL/目录清理等能力
- 插件(TTS提供商)
- 阿里云HTTP/TTS、MiniMax异步、Mock占位、阿里云实时WebSocket
- 音频合并器
- 支持本地与远程URL的合并与时长探测
章节来源
- [server/src/app.ts:1-194](file://server/src/app.ts#L1-L194)
- [server/src/config/index.ts:1-117](file://server/src/config/index.ts#L1-L117)
- [server/src/modules/tts/tts.controller.ts:1-274](file://server/src/modules/tts/tts.controller.ts#L1-L274)
- [server/src/modules/tts/tts.service.ts:1-715](file://server/src/modules/tts/tts.service.ts#L1-L715)
- [server/src/services/storage.service.ts:1-278](file://server/src/services/storage.service.ts#L1-L278)
- [server/src/modules/tts/aliyun.provider.ts:1-152](file://server/src/modules/tts/aliyun.provider.ts#L1-L152)
- [server/src/modules/tts/minimax.provider.ts:1-280](file://server/src/modules/tts/minimax.provider.ts#L1-L280)
- [server/src/modules/tts/mock.provider.ts:1-61](file://server/src/modules/tts/mock.provider.ts#L1-L61)
- [server/src/modules/tts/aliyun-realtime.provider.ts:1-158](file://server/src/modules/tts/aliyun-realtime.provider.ts#L1-L158)
- [server/src/modules/tts/audio-merger.ts:1-86](file://server/src/modules/tts/audio-merger.ts#L1-L86)
## 架构总览
TTS插件体系通过“服务工厂 + 插件实现 + 统一存储”的方式解耦。服务层根据配置与优先级选择具体插件,插件负责与外部API交互,最终由存储服务统一对外暴露URL。
```mermaid
sequenceDiagram
participant Client as "客户端"
participant Ctrl as "TTS控制器"
participant Svc as "TTS服务"
participant Prov as "TTS插件(阿里云/MiniMax/Mock)"
participant Merge as "音频合并器"
participant Store as "存储服务"
participant OSS as "OSS服务"
Client->>Ctrl : "POST /api/tts/generate"
Ctrl->>Svc : "generateAudio(userId,text,voiceId,params,options)"
Svc->>Svc : "选择插件(优先级/配置)"
Svc->>Prov : "synthesize(分段,参数,输出路径)"
Prov-->>Svc : "返回本地文件路径或云端URL"
Svc->>Merge : "合并多段音频"
Merge-->>Svc : "输出合并文件"
Svc->>Store : "uploadAudio(合并文件,audioId)"
Store->>OSS : "OSS上传(可选)"
Store-->>Svc : "返回访问URL"
Svc-->>Ctrl : "{audioId,audioUrl}"
Ctrl-->>Client : "任务已创建"
```
图表来源
- [server/src/modules/tts/tts.controller.ts:52-127](file://server/src/modules/tts/tts.controller.ts#L52-L127)
- [server/src/modules/tts/tts.service.ts:200-542](file://server/src/modules/tts/tts.service.ts#L200-L542)
- [server/src/modules/tts/aliyun.provider.ts:21-150](file://server/src/modules/tts/aliyun.provider.ts#L21-L150)
- [server/src/modules/tts/minimax.provider.ts:234-278](file://server/src/modules/tts/minimax.provider.ts#L234-L278)
- [server/src/modules/tts/mock.provider.ts:12-60](file://server/src/modules/tts/mock.provider.ts#L12-L60)
- [server/src/modules/tts/audio-merger.ts:9-36](file://server/src/modules/tts/audio-merger.ts#L9-L36)
- [server/src/services/storage.service.ts:43-49](file://server/src/services/storage.service.ts#L43-L49)
- [server/src/services/oss.service.ts:91-94](file://server/src/services/oss.service.ts#L91-L94)
## 详细组件分析
### TTS控制器(接口与生命周期)
- 职责
- 参数校验、配额检查、路由转发至服务层
- 生命周期:请求进入 -> 校验 -> 调用服务 -> 返回任务ID/状态
- 关键点
- 支持可选鉴权、字数限制、书籍归属与章节标题
- 生成成功后进行配额消耗
- 提供状态查询、预览生成、下载信息与批量下载
```mermaid
flowchart TD
Start(["请求进入 /api/tts/generate"]) --> Validate["参数校验
text/voiceId/可选bookId/chapterTitle"]
Validate --> Quota["配额检查(音频分钟)"]
Quota --> CallSvc["调用 TTS 服务 generateAudio"]
CallSvc --> Return["返回 {audioId,audioUrl}"]
Return --> End(["结束"])
```
图表来源
- [server/src/modules/tts/tts.controller.ts:52-127](file://server/src/modules/tts/tts.controller.ts#L52-L127)
章节来源
- [server/src/modules/tts/tts.controller.ts:1-274](file://server/src/modules/tts/tts.controller.ts#L1-L274)
### TTS服务(工厂、优先级与回退)
- 职责
- 文本分段、并发合成、合并、上传、状态查询、回调与WebSocket推送
- 插件优先级:MiniMax -> 阿里云HTTP -> Mock
- 额度限制错误自动回退到下一个插件
- 关键点
- MiniMax异步长文本无需分段;阿里云HTTP需分段
- 并发策略:MiniMax串行,其他并行
- 云端URL降级到本地文件再统一上传
```mermaid
flowchart TD
A["输入: text, voiceId, voiceParams, options"] --> B["构建优先级列表"]
B --> C{"逐个尝试插件"}
C --> |额度限制| D["记录最后错误, 继续下一个"]
C --> |真实失败| E["抛出错误, 停止"]
C --> |成功| F["合并音频 -> 上传存储 -> 更新记录 -> 回调/推送"]
D --> C
F --> G["返回 {audioId,audioUrl,bookId}"]
```
图表来源
- [server/src/modules/tts/tts.service.ts:304-542](file://server/src/modules/tts/tts.service.ts#L304-L542)
章节来源
- [server/src/modules/tts/tts.service.ts:1-715](file://server/src/modules/tts/tts.service.ts#L1-L715)
### TTS插件(接口规范与实现要点)
- 接口规范
- 统一方法:synthesize(text, voiceId, params, outputPath, retries?, modelOverride?)
- 返回值:本地文件路径或“cloud:”前缀的云端URL
- 错误处理:区分速率限制与服务错误,支持指数退避重试
- 实现要点
- 阿里云HTTP:支持指令模型参数拼装、云端URL回退
- MiniMax异步:创建任务 -> 轮询 -> tar提取MP3
- Mock:FFmpeg生成占位音频,失败时写入最小MP3头
- 阿里云实时:WebSocket流式合成,适合长文本
```mermaid
classDiagram
class TtsProvider {
+synthesize(text, voiceId, params, outputPath, retries, modelOverride) string
}
class AliyunTtsProvider {
-apiKey string
-model string
-voice string
}
class MiniMaxTtsProvider {
-apiKey string
}
class MockTtsProvider
class AliyunRealtimeTtsProvider {
-apiKey string
-model string
-voice string
}
TtsProvider <|.. AliyunTtsProvider
TtsProvider <|.. MiniMaxTtsProvider
TtsProvider <|.. MockTtsProvider
TtsProvider <|.. AliyunRealtimeTtsProvider
```
图表来源
- [server/src/modules/tts/aliyun.provider.ts:9-152](file://server/src/modules/tts/aliyun.provider.ts#L9-L152)
- [server/src/modules/tts/minimax.provider.ts:42-280](file://server/src/modules/tts/minimax.provider.ts#L42-L280)
- [server/src/modules/tts/mock.provider.ts:11-61](file://server/src/modules/tts/mock.provider.ts#L11-L61)
- [server/src/modules/tts/aliyun-realtime.provider.ts:11-158](file://server/src/modules/tts/aliyun-realtime.provider.ts#L11-L158)
章节来源
- [server/src/modules/tts/aliyun.provider.ts:1-152](file://server/src/modules/tts/aliyun.provider.ts#L1-L152)
- [server/src/modules/tts/minimax.provider.ts:1-280](file://server/src/modules/tts/minimax.provider.ts#L1-L280)
- [server/src/modules/tts/mock.provider.ts:1-61](file://server/src/modules/tts/mock.provider.ts#L1-L61)
- [server/src/modules/tts/aliyun-realtime.provider.ts:1-158](file://server/src/modules/tts/aliyun-realtime.provider.ts#L1-L158)
### 存储插件(统一抽象与OSS)
- 职责
- 本地与OSS无缝切换,提供上传/下载/删除/签名URL/目录清理
- 通过环境变量STORAGE_TYPE切换模式
- 关键点
- OSS服务封装OSS SDK,支持对象键规范化、CDN域名回源
- 存储服务在本地模式下直接写入uploads目录
```mermaid
classDiagram
class StorageService {
-storageType StorageType
+setStorageType(type)
+getStorageType() StorageType
+uploadAudio(localPath, audioId) string
+uploadVideo(localPath, videoId) string
+uploadCover(localPath, bookId) string
+uploadFile(localPath, category, id) string
+uploadBuffer(buffer, objectKey, contentType) string
+deleteFile(url)
+deleteDirectory(prefix, id)
+downloadFile(url) Buffer
+getSignedUrl(url, expires) string
+testConnection() boolean
}
class OSSService {
-client OSS
-bucket string
-cdnDomain string
+uploadFile(localPath, objectKey) string
+uploadBuffer(buffer, objectKey, contentType) string
+uploadAudio(localPath, audioId) string
+uploadVideo(localPath, videoId) string
+uploadCover(localPath, bookId) string
+deleteFile(objectKey)
+deleteDirectory(prefix)
+getSignedUrl(objectKey, expires) string
+downloadFile(objectKey) Buffer
+getFileUrl(objectKey) string
+testConnection() boolean
}
StorageService --> OSSService : "OSS模式委托"
```
图表来源
- [server/src/services/storage.service.ts:13-278](file://server/src/services/storage.service.ts#L13-L278)
- [server/src/services/oss.service.ts:13-256](file://server/src/services/oss.service.ts#L13-L256)
章节来源
- [server/src/services/storage.service.ts:1-278](file://server/src/services/storage.service.ts#L1-L278)
- [server/src/services/oss.service.ts:1-256](file://server/src/services/oss.service.ts#L1-L256)
### 音频合并器(跨域与本地处理)
- 职责
- 合并本地或多段远程URL音频,计算时长
- 远程URL通过FFmpegProcessor处理,本地使用FFmpeg concat
- 关键点
- 单文件直接复制,避免不必要的转码
- 输出格式自动适配
章节来源
- [server/src/modules/tts/audio-merger.ts:1-86](file://server/src/modules/tts/audio-merger.ts#L1-L86)
### 配置与类型(模型与参数)
- 配置中心
- 加载.env与models.json,提供模型枚举、启用筛选、错误识别与自动切换辅助
- TTS相关:DashScope API Key、模型列表、实时模型开关
- 类型定义
- VoiceParams、Voice、IUser、IAudio、ApiResponse等
章节来源
- [server/src/config/index.ts:1-117](file://server/src/config/index.ts#L1-L117)
- [server/src/types/index.ts:1-124](file://server/src/types/index.ts#L1-L124)
## 依赖关系分析
- 组件耦合
- 控制器仅依赖服务接口,低耦合
- 服务层通过工厂选择插件,便于替换与扩展
- 存储服务对上层透明,对下层可替换
- 外部依赖
- HTTP/WS调用第三方TTS
- FFmpeg/ffprobe用于合并与时长探测
- OSS SDK用于对象存储
- 循环依赖
- 未见循环导入;服务与插件通过函数调用解耦
```mermaid
graph LR
CTRL["tts.controller.ts"] --> SVC["tts.service.ts"]
SVC --> ALI["aliyun.provider.ts"]
SVC --> MINI["minimax.provider.ts"]
SVC --> MOCK["mock.provider.ts"]
SVC --> RT["aliyun-realtime.provider.ts"]
SVC --> MERGE["audio-merger.ts"]
SVC --> STORE["storage.service.ts"]
STORE --> OSS["oss.service.ts"]
APP["app.ts"] --> CTRL
APP --> CFG["config/index.ts"]
APP --> TYPES["types/index.ts"]
```
图表来源
- [server/src/app.ts:1-194](file://server/src/app.ts#L1-L194)
- [server/src/modules/tts/tts.controller.ts:1-274](file://server/src/modules/tts/tts.controller.ts#L1-L274)
- [server/src/modules/tts/tts.service.ts:1-715](file://server/src/modules/tts/tts.service.ts#L1-L715)
- [server/src/modules/tts/aliyun.provider.ts:1-152](file://server/src/modules/tts/aliyun.provider.ts#L1-L152)
- [server/src/modules/tts/minimax.provider.ts:1-280](file://server/src/modules/tts/minimax.provider.ts#L1-L280)
- [server/src/modules/tts/mock.provider.ts:1-61](file://server/src/modules/tts/mock.provider.ts#L1-L61)
- [server/src/modules/tts/aliyun-realtime.provider.ts:1-158](file://server/src/modules/tts/aliyun-realtime.provider.ts#L1-L158)
- [server/src/modules/tts/audio-merger.ts:1-86](file://server/src/modules/tts/audio-merger.ts#L1-L86)
- [server/src/services/storage.service.ts:1-278](file://server/src/services/storage.service.ts#L1-L278)
- [server/src/services/oss.service.ts:1-256](file://server/src/services/oss.service.ts#L1-L256)
- [server/src/config/index.ts:1-117](file://server/src/config/index.ts#L1-L117)
- [server/src/types/index.ts:1-124](file://server/src/types/index.ts#L1-L124)
## 性能考量
- 并发与批处理
- 非MiniMax场景采用并行分段合成,提升吞吐
- MiniMax异步轮询较长,串行避免超时
- 重试与退避
- 速率限制与服务错误采用指数退避重试
- 存储与网络
- 云端URL优先,失败时本地降级并统一上传
- OSS签名URL用于私有桶下载
- 工具链
- FFmpeg/ffprobe用于高效合并与时长探测
章节来源
- [server/src/modules/tts/tts.service.ts:348-383](file://server/src/modules/tts/tts.service.ts#L348-L383)
- [server/src/modules/tts/aliyun.provider.ts:125-141](file://server/src/modules/tts/aliyun.provider.ts#L125-L141)
- [server/src/services/storage.service.ts:184-193](file://server/src/services/storage.service.ts#L184-L193)
- [server/src/modules/tts/audio-merger.ts:69-85](file://server/src/modules/tts/audio-merger.ts#L69-L85)
## 故障排查指南
- 错误处理
- 控制器与服务层均抛出自定义错误,便于统一拦截
- 速率限制自动回退,非速率限制直接失败
- 日志与诊断
- 服务层写入本地日志文件,便于定位
- 应用启动时打印存储模式、上传目录、缓存状态
- 常见问题
- API Key缺失:插件构造时抛错,触发Mock回退
- 云端URL下载失败:自动降级到本地文件再上传
- WebSocket实时合成失败:检查权限与网络,考虑禁用实时模式
章节来源
- [server/src/middleware/errorHandler.js:1-41](file://server/src/middleware/errorHandler.js#L1-L41)
- [server/src/modules/tts/tts.service.ts:268-279](file://server/src/modules/tts/tts.service.ts#L268-L279)
- [server/src/modules/tts/aliyun.provider.ts:118-146](file://server/src/modules/tts/aliyun.provider.ts#L118-L146)
- [server/src/services/storage.service.ts:160-176](file://server/src/services/storage.service.ts#L160-L176)
- [server/src/app.ts:133-194](file://server/src/app.ts#L133-L194)
## 结论
平台通过“服务工厂 + 插件实现 + 统一存储”的架构,实现了TTS能力的可插拔扩展。开发者只需遵循统一接口与生命周期规范,即可快速接入新的TTS提供商、AI模型与存储后端。配合完善的错误处理、重试与性能优化策略,可在生产环境中稳定运行。
## 附录
### 插件开发模板(TTS提供商)
- 必备接口
- 构造函数:读取配置(API Key/模型/音色映射)
- synthesize(text, voiceId, params, outputPath, retries?, modelOverride?):返回本地路径或“cloud:”URL
- 建议实现
- 明确错误类型(速率限制/服务错误/参数错误)
- 支持重试与指数退避
- 提供最小可用的Mock实现以便联调
章节来源
- [server/src/modules/tts/aliyun.provider.ts:15-150](file://server/src/modules/tts/aliyun.provider.ts#L15-L150)
- [server/src/modules/tts/minimax.provider.ts:45-278](file://server/src/modules/tts/minimax.provider.ts#L45-L278)
- [server/src/modules/tts/mock.provider.ts:11-61](file://server/src/modules/tts/mock.provider.ts#L11-L61)
### 插件注册机制与依赖注入
- 注册机制
- 服务层通过工厂函数选择插件,无需硬编码依赖
- 配置中心提供模型与供应商列表,便于动态切换
- 依赖注入
- 插件通过构造函数注入配置
- 存储服务通过单例注入,对外透明
章节来源
- [server/src/modules/tts/tts.service.ts:163-190](file://server/src/modules/tts/tts.service.ts#L163-L190)
- [server/src/config/index.ts:69-117](file://server/src/config/index.ts#L69-L117)
- [server/src/services/storage.service.ts:13-28](file://server/src/services/storage.service.ts#L13-L28)
### 配置管理
- 环境变量
- STORAGE_TYPE:存储模式(oss/local)
- DashScope/OSS相关环境变量:API Key、区域、Bucket、Endpoint、CDN域名
- 模型配置
- models.json统一管理供应商与模型,支持启用/禁用与类型过滤
章节来源
- [server/src/services/storage.service.ts:16-28](file://server/src/services/storage.service.ts#L16-L28)
- [server/src/config/index.ts:83-117](file://server/src/config/index.ts#L83-L117)
### 插件间通信与事件
- WebSocket事件
- 生成完成后推送“音频生成完成”事件,携带bookId与chapterId
- 回调机制
- onComplete回调在服务层执行,确保异步流程可控
章节来源
- [server/src/modules/tts/tts.service.ts:510-515](file://server/src/modules/tts/tts.service.ts#L510-L515)
- [server/src/modules/tts/tts.service.ts:497-508](file://server/src/modules/tts/tts.service.ts#L497-L508)
### 最佳实践
- 插件实现
- 明确错误分类与重试策略
- 提供Mock实现,便于本地联调
- 服务层
- 优先级与回退策略清晰,避免单一故障点
- 统一上传与状态查询,保证一致性
- 存储层
- 云端URL优先,失败时本地降级
- OSS签名URL用于私有桶下载
章节来源
- [server/src/modules/tts/aliyun.provider.ts:118-146](file://server/src/modules/tts/aliyun.provider.ts#L118-L146)
- [server/src/modules/tts/minimax.provider.ts:258-275](file://server/src/modules/tts/minimax.provider.ts#L258-L275)
- [server/src/services/storage.service.ts:39-49](file://server/src/services/storage.service.ts#L39-L49)