服务层模式Skill:用 Result 类型与标签化错误,构建纯净可测的业务服务层
核心结论速览
- 它是什么:一份面向 Whispering 架构的「服务层实现规范」技能,指导你用
createTaggedError、命名空间导出与Result<T, E>类型搭建纯净、隔离、可测试的业务服务。 - 解决什么问题:服务层职责混乱、错误处理靠抛异常、UI 与业务逻辑耦合、平台差异难统一——这套模式给出统一答案。
- 核心机制:三层架构「服务 → 查询 → UI」+ 工厂函数 + Live 单例 + 标签化错误联合类型。
- 谁最适合用:使用 TypeScript 构建中大型前端/桌面应用、需要跨平台(桌面 Tauri 与网页)复用业务逻辑的开发者。
- 上手成本:只要熟悉 TypeScript 与函数式错误处理(Ok/Err),即可快速套用,模式本身非常轻量。
该技能叫什么?如何下载安装 AI 技能提升开发效率?
技能中文名:服务层模式Skill 技能英文名:services-layer 技能ID:11866 作者 / 版本:epicenter / 1.0(以官方说明为准)
这个技能要解决的问题非常具体:很多项目在写业务逻辑时,习惯把「取数据、做判断、弹提示、改 UI 状态」全揉在一个函数里,错误靠 throw 抛出,调用方靠 try/catch 兜底。结果是逻辑无法复用、单元测试难写、跨平台实现重复、错误信息五花八门。
服务层模式Skill 提供的是一套「约定优于配置」的架构范式:服务必须是纯的、隔离的、不依赖 UI 的,并且永远返回 Result<T, E> 类型而不抛出错误。它把错误建模成可识别、可分类、可带上下文的标签化对象,从而让错误处理从「意外」变成「类型系统的一部分」。
一键安装 AI 技能提升开发效率的思路
AI 技能(Skill)本质上是一份结构化、可被 AI 编程助手读取的「领域知识包」。安装它的思路不是装一个运行时依赖,而是让你的 AI 助手在需要时能自动加载这份规范,从而在写代码时直接产出符合约定的实现,避免你反复口头交代架构规则。
安装前提:
- 你的开发环境已接入支持技能(Skill)机制的 AI 编程助手或平台;
- 项目使用 TypeScript,并已(或将)引入
wellcrafted库(提供服务所需的createTaggedError、Result、tryAsync、trySync等能力); - 你对项目目录有写入权限,便于放置技能文件。
通用安装步骤:
- 在技能市场、技能库或团队内部技能仓库中,按技能英文名
services-layer或技能ID11866检索该技能; - 通过平台的「安装 / 添加 / 启用」入口一键安装(具体入口与命令以官方说明为准);
- 若平台支持本地技能目录,也可将技能目录(含
SKILL.md等描述文件)放入约定的技能存放路径; - 安装完成后,在对话中提及「服务层」「services-layer」「创建服务」等关键词,触发技能加载。
如何验证是否生效:
- 向 AI 助手提问「如何在本项目中创建一个新服务?」,若回答中出现
createTaggedError、Result<T, E>、工厂函数 + Live 实例等关键词,说明技能已被正确加载; - 让助手生成一段服务代码,检查它是否返回 Result 而非抛出异常、是否导出了工厂与 Live 两个出口、错误名是否遵循
{服务名称}ServiceError命名; - 若没有命中,可显式点名「请使用 services-layer 技能中的服务层模式」再试一次。
该技能的主要功能和特点
- 定义统一的三层架构:明确「服务 → 查询(RPC/查询层)→ UI」的调用链路,服务只做纯业务逻辑,查询层负责编排,UI 只负责展示与交互,职责边界一目了然。
- 标签化错误构造(createTaggedError):一次调用同时得到「错误构造函数」和「Err 助手」两个出口,
Err(MyServiceError({...}))与MyServiceErr({...})等价,后者更简洁。 - 强类型错误上下文(.withContext):可为错误附加结构化元数据,例如 HTTP 状态码
{ status: number },并由 TypeScript 强制约束形状,避免上下文随手乱塞。 - 错误联合类型组合:把
ConnectionError | ResponseError | ParseError组合为一个HttpServiceError,让调用方对一类错误统一处理。 - Result 优先的错误处理:所有服务方法签名统一为
Promise<Result<T, E>>,永不抛异常,错误必须被显式处理。 - trySync / tryAsync 包装风险操作:把可能失败的同步/异步调用安全地转为 Result,并通过
extractErrorMessage提取技术细节。 - 工厂 + Live 双出口:
createXxxService()工厂便于注入模拟参数做单元测试,XxxServiceLive作为生产单例直接使用。 - 命名空间导出体系:按
desktop/(仅桌面 Tauri)、isomorphic/(跨平台)分层,通过services.transcriptions.openai.transcribe(...)这类命名空间访问,结构清晰、可读性强。 - 平台特定服务变体:同一接口分别实现桌面版(Tauri API)与网页版(浏览器 API),在构建时通过
window.__TAURI_INTERNALS__检测自动选择,业务代码零改动。 - 错误消息编写规范:要求错误信息「用户友好 + 可操作 + 含技术细节」,例如「无法连接到所选麦克风。这可能是因为设备已被其他应用程序使用、已断开连接或缺少适当权限。」
和普通做法的差别在哪:普通项目靠「约定 + Code Review」维持服务层干净,一旦有人 import 了设置或 UI 提示就破功;这套模式把约束写进类型与导出结构里,AI 助手与团队成员都能照着落地,一致性不依赖人的自觉。
如何使用该技能?
第 1 步:定义域特定错误类型
import { createTaggedError } from 'wellcrafted/error';
export const { MyServiceError, MyServiceErr } = createTaggedError('MyServiceError');
type MyServiceError = ReturnType<typeof MyServiceError>;
若错误需要结构化上下文,链式追加 .withContext<T>():
export const { ResponseError, ResponseErr } =
createTaggedError('ResponseError').withContext<{ status: number }>();
第 2 步:用工厂函数编写服务实现
输入示例:一个接受 param1: string 与 param2: number 的服务方法;输出:Promise<Result<OutputType, MyServiceError>>。
export function createMyService() {
return {
async doSomething(options: { param1: string; param2: number }) {
if (!options.param1) {
return MyServiceErr({ message: 'param1是必需的' });
}
const { data, error } = await tryAsync({
try: () => riskyAsyncOperation(options),
catch: (e) => MyServiceErr({ message: `操作失败:${extractErrorMessage(e)}` }),
});
if (error) return Err(error);
return Ok(data);
},
};
}
第 3 步:导出 Live 生产实例
export type MyService = ReturnType<typeof createMyService>;
export const MyServiceLive = createMyService();
第 4 步:按此规范整理目录并用命名空间导出
// services/isomorphic/index.ts
import * as transcriptions from './transcription';
import * as completions from './completion';
export const services = {
db: DbServiceLive,
sound: PlaySoundServiceLive,
transcriptions,
completions,
} as const;
第 5 步:在查询层或调用方消费服务
import { services, desktopServices } from '$lib/services';
await services.transcriptions.openai.transcribe(blob, options);
await services.db.recordings.getAll();
await desktopServices.ffmpeg.compressAudioBlob(blob, options);
第 6 步:需要平台差异时,先定接口再分别实现
接口在 types.ts 中定义(如 TextService 含 readFromClipboard / copyToClipboard / writeToCursor),桌面版用 Tauri API、网页版用 navigator.clipboard,最后在 index.ts 中依据 window.__TAURI_INTERNALS__ 选择实现。
第 7 步:自查关键规则——服务不导入设置(配置走参数)、不导入 UI 代码(无 toast、无通知)、始终返回 Result、用 trySync/tryAsync 包装、导出工厂 + Live 实例、错误命名统一。
同类型竞品对比分析
| 方案名称 | 定位 | 核心功能 | 安装/使用门槛 | 适用场景 | 优势与不足 |
|---|---|---|---|---|---|
| 服务层模式Skill(services-layer) | 服务层架构规范型 AI 技能 | 三层架构约定、createTaggedError 标签化错误、Result 统一错误处理、命名空间导出、平台特定服务变体 | 低:安装技能 + 引入 wellcrafted 即可,无需重构全部代码 | TypeScript 中大型应用、跨桌面/网页复用业务逻辑、需要强错误语义的项目 | 优势:约束写进类型、AI 可直接产出合规代码、可测试性强;不足:依赖 wellcrafted 生态,团队需接受 Result 风格而非抛异常习惯 |
| 传统分层 + 全局异常捕获 | 常见工程实践 | 靠目录约定 + try/catch + 全局错误边界 | 极低:无需任何额外依赖 | 小型项目、快速原型 | 优势:零学习成本;不足:错误类型松散、调用方容易漏处理、跨平台实现易重复 |
| 领域驱动设计(DDD)分层框架 | 重型架构方法论 | 实体、值对象、聚合根、仓储、领域服务 | 高:概念多、样板代码多 | 复杂业务域、长期演进的企业系统 | 优势:业务建模严谨;不足:对中小项目过重,前端场景落地成本高 |
| tRPC / RPC 框架自带的服务层 | 端到端类型安全通信层 | 类型安全的接口定义与调用、输入输出校验 | 中:需搭建服务端与客户端约定 | 全栈 TypeScript、前后端同一仓库 | 优势:端到端类型推导强;不足:偏重通信链路,不解决「服务内部如何建模错误、如何隔离 UI」 |
可以看出,服务层模式Skill 的独特价值在于:它不与具体通信框架绑定,而是专注「服务内部怎么写才干净、错误怎么建模才可辨」,这与 RPC 框架、DDD 方法论是互补而非替代关系。
该技能的典型应用场景与实际体验情况
场景一:新增一个带域特定错误的服务
角色:业务开发工程师。痛点是新服务写完后,错误类型随手定义,调用方拿到的是字符串或裸 Error,无法分支处理。使用该技能后,先 createTaggedError('XxxServiceError') 定义错误,再写工厂函数返回 Result,调用方按错误名精确匹配。实际体验:上手难度低,模板化程度高,复制真实示例改改即可;注意不要遗漏「导出 Live 实例」这一步,否则查询层无法直接消费。
场景二:跨平台(桌面 Tauri 与网页)复用业务逻辑
角色:跨端开发者。痛点是桌面端和网页端的剪贴板、文件系统 API 完全不同,容易写出两套互不相干的代码。用该技能的做法:先在 types.ts 定义共享接口 TextService,再分别写 createTextServiceDesktop() 与 createTextServiceWeb(),最后按 window.__TAURI_INTERNALS__ 自动选择。实际体验:接口定义的收益非常明显,两个实现都被类型强制对齐,漏实现方法会直接编译报错;注意平台检测发生在构建/加载期,需确保运行环境判断准确。
场景三:给错误加上 HTTP 状态码这类强类型上下文
角色:API 封装层开发者。痛点是错误对象里塞了各种字段,形状不统一。使用 .withContext<{ status: number }>() 后,创建错误时若漏传或传错 context 形状,TypeScript 直接报错。实际体验:类型约束带来的安全感很强,尤其在多错误联合(HttpServiceError)场景下,调用方可按 status 做统一降级处理。
场景四:为现有服务补单元测试
角色:测试/质量工程师。痛点是有状态、依赖 UI 的服务极难测。因为服务是「纯的、接受显式参数、无隐藏依赖」,工厂函数 createMyService() 可直接在测试中构造实例并注入模拟参数。实际体验:测试编写速度明显提升,无需搭建复杂 mock 环境;注意按规范「服务不导入设置」,配置必须走参数,否则测试仍会被环境依赖拖累。
该技能能为用户带来的价值
- 错误处理从「靠运气」变成「靠类型」:所有服务统一返回
Result<T, E>,编译器会提醒你处理错误,漏处理的情况大幅减少,线上因未捕获异常导致的崩溃随之下降。 - 调试效率提升:标签化错误 +
extractErrorMessage保留技术细节,错误来源清晰可辨,排查链路从「翻日志猜」变为「看错误名定位」。 - 测试成本显著降低:工厂 + Live 双出口的设计,让单元测试无需构造整个应用上下文,服务可独立验证。
- 跨平台复用成为常态:接口先行的平台变体模式,让桌面与网页共享业务语义,只是替换底层 API,重复代码量大幅压缩。
- 团队协作与 AI 协作一致性提升:命名规则(
{服务名称}ServiceError)、导出结构(命名空间)、关键规则(6 条)都是显式约定,新成员和 AI 助手产出的代码风格统一,Review 成本下降。 - 长期架构价值:三层架构让 UI 与业务彻底解耦,未来替换 UI 框架、迁移平台时,服务层几乎无需改动,这是一笔可持续兑现的架构红利。
常见问题 FAQ
Q1:这个技能怎么安装?安装后不生效怎么办?
按技能英文名 services-layer 或技能ID 11866 在技能平台检索并一键安装即可,具体入口与命令以官方说明为准。验证方式是提问「如何创建新服务」,若回答包含 createTaggedError、Result<T, E>、工厂 + Live 等要素即已生效;若未命中,可在提示中显式点名该技能名。
Q2:使用这个技能收费吗?
技能本体由作者 epicenter 发布,版本 1.0,是否收费、是否有授权限制请以官方说明为准。它依赖的 wellcrafted 库属于开源生态依赖,可自行查阅其许可。技能本身不产生运行时费用。
Q3:它适合谁用?小项目有必要吗? 最适合使用 TypeScript、业务逻辑较复杂、且存在跨平台(桌面/网页)需求的中大型项目团队。小项目或一次性原型可以只借用其中一两条规则(如统一返回 Result、错误命名规范),不必全套照搬。
Q4:必须引入 wellcrafted 库吗?能不能用别的方案替代?
技能中的 createTaggedError、Result、tryAsync、trySync、extractErrorMessage 均来自 wellcrafted。若不想引入,可用其他 Result 风格库自行实现等价能力,但错误标签化与上下文类型约束的写法需相应调整,具体替代方案以各库官方文档为准。
Q5:它和 tRPC 这类 RPC 框架冲突吗?该怎么选? 不冲突。RPC 框架解决的是「前后端之间怎么通信、类型怎么贯通」,本技能解决的是「服务内部怎么组织、错误怎么建模、平台差异怎么隔离」。典型做法是:服务层用本技能规范实现,查询层/RPC 层再调用服务,两者组合使用效果最佳。