技能使用指南

服务层模式Skill 用法详解

服务层模式Skillservices-layer
9 个章节 约 18 分钟阅读 AI 智能生成

服务层模式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 等能力);
  • 你对项目目录有写入权限,便于放置技能文件。

通用安装步骤:

  1. 在技能市场、技能库或团队内部技能仓库中,按技能英文名 services-layer 或技能ID 11866 检索该技能;
  2. 通过平台的「安装 / 添加 / 启用」入口一键安装(具体入口与命令以官方说明为准);
  3. 若平台支持本地技能目录,也可将技能目录(含 SKILL.md 等描述文件)放入约定的技能存放路径;
  4. 安装完成后,在对话中提及「服务层」「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 层再调用服务,两者组合使用效果最佳。