从 Fetcher 包迁移
本页回答:使用 @ahoo-wang/fetcher-wow、@ahoo-wang/fetcher-generator 或 @ahoo-wang/fetcher-react 中 Wow Hook 的应用需要改什么?
Wow 的 TypeScript 包已从 Fetcher 仓库迁入 Wow 仓库,这样 Kotlin 契约、TypeScript 客户端和生成器可以在一个 PR 里改完、一起发布。变化的是包名、命令名、peer 依赖范围和版本线;已弃用的 Condition API 挪到了 @ahoo-wang/wow-client/legacy 子路径;wow-client 的首个版本还改动了一组 API——错误、命令头、取消参数、聚合构造器——列在首个版本的 API 变化中。
变化一览
| 原来 | 现在 | 说明 |
|---|---|---|
@ahoo-wang/fetcher-wow | @ahoo-wang/wow-client | 见API 变化;另外已弃用的 Condition API(Condition 构造器、Operator、基于 Condition 的查询类型与工厂函数)和 en_US / zh_CN 操作符文案挪到了 @ahoo-wang/wow-client/legacy;/query/locale/en_US 和 /query/locale/zh_CN 子路径已不存在 |
@ahoo-wang/fetcher-generator | @ahoo-wang/wow-generator | 命令改名为 wow-generator;fetcher-generator 作为别名保留到 v10 |
@ahoo-wang/fetcher-react 中的 Wow Hook | @ahoo-wang/wow-react | useSingleQuery、useListQuery、usePagedQuery、useCountQuery、useListStreamQuery 及对应的 useFetcher* 版本 |
| Fetcher 5.x 版本线 | Wow 版本线 | wow-client 9.x.y 与 Wow 9.x.y 一起发布 |
@ahoo-wang/fetcher-view-engine(从未发布) | @ahoo-wang/wow-view-engine | 随 Wow 9.2.0 发布,见视图引擎 |
Fetcher 的核心包保留原名,文档仍在 fetcher.ahoo.me:fetcher、fetcher-decorator、fetcher-eventstream、fetcher-openapi、fetcher-react 和 fetcher-cosec。@ahoo-wang/fetcher-viewer 与 fetcher-react 的 dataMonitor Hook 没有迁移,留在 Fetcher 5.x。
步骤
1. 替换依赖
先升级 peer 依赖:wow-react 需要 React 19.0 或更高版本(react 为 ^19.0.0),不支持 React 18。它不再需要 @ahoo-wang/fetcher-react:只有还要用它的其他 Hook 时才保留,并且版本不低于 5.1.5,这样它对 fetcher-wow 的 peer 依赖是可选的。然后替换迁走的包:
pnpm remove @ahoo-wang/fetcher-wow @ahoo-wang/fetcher-generator
pnpm add @ahoo-wang/wow-client
pnpm add -D @ahoo-wang/wow-generator
# 仅当应用使用 Wow 查询 Hook 时
pnpm add react react-dom @ahoo-wang/wow-react| 包 | peer 依赖 | 范围 |
|---|---|---|
wow-client | fetcher、fetcher-decorator、fetcher-eventstream | ^5.1.5 |
wow-generator | fetcher、fetcher-decorator、fetcher-eventstream | ^5.1.5 |
wow-generator、wow-react | wow-client | ~x.y.z,即同一个小版本 |
wow-react | fetcher | ^5.1.5 |
wow-react | react | ^19.0.0,不支持 React 18 |
fetcher-generator 把 fetcher-openapi 作为 peer,wow-generator 不再需要它:除非应用自己导入 @ahoo-wang/fetcher-openapi,否则可以移除。fetcher-eventstream 仍要保留,wow-client 需要它。
应用仍在使用 fetcher-react 时,5.1.3 起它对 fetcher-wow 的 peer 依赖是可选的,所以移除 fetcher-wow 后依赖图里只剩一份 Wow 类型。只有其他依赖仍然需要 fetcher-wow 时才保留它,并且不要在同一个应用里同时从两个包导入 Wow 类型:两套类型不能互换。
2. 改写导入
先替换模块名,再按下文的API 变化修改调用。已弃用的 Condition API 和操作符文案从 @ahoo-wang/wow-client/legacy 导入,其余一切从根入口导入。
// 迁移前
import { CommandClient, filter, pagedQuery } from '@ahoo-wang/fetcher-wow';
import { zh_CN } from '@ahoo-wang/fetcher-wow/query/locale/zh_CN';
import { usePagedQuery, useFetcher } from '@ahoo-wang/fetcher-react';
// 迁移后
import { CommandClient, filter, pagedQuery } from '@ahoo-wang/wow-client';
import { zh_CN } from '@ahoo-wang/wow-client/legacy';
import { usePagedQuery } from '@ahoo-wang/wow-react';
import { useFetcher } from '@ahoo-wang/fetcher-react';只有五个 Wow 查询 Hook 及其 useFetcher* 版本迁到了 wow-react。useFetcher、useQuery、useFetcherQuery 等其余 Hook 仍在 @ahoo-wang/fetcher-react。搜索 fetcher-wow 和这十个 Hook 名,就能找到所有要改的行。
两个列表流 Hook 的形态变了。useListStreamQuery 与 useFetcherListStreamQuery 不再把 ReadableStream 放进 result 交给组件读取,而是自己读取,以 items 返回已收到的行,流结束后 done 为 true;新查询、abort()、reset() 和组件卸载时都会取消流。删掉调用 getReader() 的 effect,直接渲染 items。useFetcherListStreamQuery 现在会发送 Accept: text/event-stream(缺少它时 Wow 服务端返回 JSON),并且不再接受 resultExtractor;流中出现错误事件时,error 是一个 WowError。
这些 Hook 的选项与返回类型现在由 wow-react 自己声明,不再扩展 fetcher-react 的 UseQueryOptions、UseQueryReturn 和 UseFetcherQueryOptions。下表各项都能由类型检查找出。
@ahoo-wang/fetcher-react 中的 Wow Hook | @ahoo-wang/wow-react |
|---|---|
status 是 fetcher-react 的 PromiseStatus 枚举,用 PromiseStatus.SUCCESS 比较 | status 是 QueryStatus,即字符串联合 'idle' | 'loading' | 'success' | 'error':直接用字符串字面量比较和赋值 |
E 默认为 FetcherError,列表流为 FetcherError | WowError | E 默认为 Error,因为自定义 execute 可能以任何错误拒绝。读取 error.exchange 的代码显式传入 FetcherError 作为 E,或用 instanceof FetcherError 收窄 |
选项 initialStatus、propagateError、onAbort,以及 useFetcher* 请求型 Hook 的 resultExtractor | 已删除。失败从 error 或 onError 读取;需要别的提取方式时,改用 use*Query Hook 并自己提供 execute |
attributes 为 Record<string, any> | Map<string, any> | Record<string, unknown>,与 wow-client 查询方法接受的类型一致 |
为 props、测试或 story 标类型而从 @ahoo-wang/fetcher-react 导入的选项与返回类型 | 从 @ahoo-wang/wow-react 导入 QueryHookOptions、QueryHookReturn、QueryStatus 和 QueryExecutor |
这些 Hook 也改为运行在 wow-react 自己的请求状态机上,不再用 fetcher-react 的。下面这些变化类型检查找不出来,依赖旧行为的代码需要逐处核对。
@ahoo-wang/fetcher-react 中的 Wow Hook | @ahoo-wang/wow-react |
|---|---|
请求失败和 abort() 都会清空 result | 两者都保留上一次结果,刷新失败不会让界面变空白;只有 reset() 清空。为此自己保存「上一次成功结果」的组件可以删掉那份副本 |
reset() 不中止进行中的请求,迟到的响应仍会写回 result 或 error,并调用 onSuccess 或 onError | reset() 中止该请求,它的任何结果都不会再到达 |
useFetcher* Hook 的 url 变化要等下一次执行才生效 | url 变化会重新执行查询 |
请求型 Hook 每遇到新的 fetcher 实例就重跑,所以在渲染里写 new Fetcher() 会无限请求;流型 Hook 要等下一次执行 | 所有 useFetcher* Hook 在 Fetcher 的名字(没有名字时是 baseURL)变化时重跑;在渲染里写 new Fetcher({ baseURL }) 只发一次请求 |
fetcher 是未注册的名字时,请求型 Hook 在渲染时抛错 | 该次请求失败,error 给出原因 |
挂载即执行的 Hook,首帧(服务端也一样)是 idle | 首帧是 loading,服务端与客户端一致,骨架屏从第一帧就能显示 |
Condition API 挪到 /legacy 之后,根入口的 singleQuery、listQuery、pagedQuery 构造的内容也变了:它们接收 filter 而不是 condition,filter 默认为 filter.matchAll()。根入口的 listQuery 也没有默认的 limit 了:@ahoo-wang/fetcher-wow 的 listQuery 和 /legacy 的一样,不传时发送 limit: 10;根入口什么也不发,交给服务端决定。Wow 9.1.5 及以后此时最多返回默认列表大小(100)行;Wow 8.11~9.1.3 拒绝这个查询。依赖旧默认值的调用请显式传 limit: 10;见不带 limit 的列表查询。传了 condition 的调用,要改用 /legacy 里的同名工厂函数,或者用 filter.* 改写。
首个版本的 API 变化
@ahoo-wang/wow-client 的首个版本也借机修正了 fetcher-wow 无法修改的 API。下表除最后一行的流行为外,都能由类型检查找出所有调用点。
@ahoo-wang/fetcher-wow | @ahoo-wang/wow-client |
|---|---|
ErrorCodes.isSucceeded(code) / ErrorCodes.isError(code) | 已删除:改为比较 code === ErrorCodes.SUCCEEDED。ErrorCodes 由类改为冻结的 as const 对象,并补充了查询 schema 与批处理错误码;SUCCEEDED_MESSAGE、NOT_FOUND_MESSAGE 已删除。ErrorInfo.errorCode 的类型是 ErrorCode(Wow 的错误码或任意其他字符串)。 |
DEFAULT_PROJECTION、defaultProjection() | 已删除:projection() 不传参数,或干脆不传 projection,都返回全部字段。 |
EMPTY_PAGED_LIST | 已删除:pagedList() 每次返回一个新的空页({ total: 0, list: [] }),调用方不会改到一个共享的对象。 |
LogicalField | QueryField:同一个类型的现行名字。 |
ReadableDomainEventStream | ReadableStream<DomainEventStream>,它原本就是这个类型。 |
QueryEventStreamResultExtractor / CommandResultEventStreamResultExtractor | 不再公开;改用端点预设,它把提取器和 Accept: text/event-stream 头放在一起:用 @post(path, QUERY_STREAM_ENDPOINT) / @post(path, COMMAND_STREAM_ENDPOINT)(整个类用 @api('…', COMMAND_STREAM_ENDPOINT))代替 @post(path, { headers: { Accept: 'text/event-stream' }, resultExtractor: QueryEventStreamResultExtractor })。只需要提取器的代码读 QUERY_STREAM_ENDPOINT.resultExtractor / COMMAND_STREAM_ENDPOINT.resultExtractor。 |
| 手工读取失败请求的错误响应体 | await toWowError(error) 返回 WowError(errorCode、errorMsg、bindingErrors、status),Wow 没有应答时返回 undefined;isErrorInfo(value) 是类型守卫。参见错误。 |
CommandHeaders / WowHeaders 类 | 冻结的 as const 对象;CommandHeaders.WAIT_STAGE 等成员写法不变,但每个值现在是字面量类型。 |
SnapshotMetadataFields / DomainEventStreamMetadataFields 类 | 冻结的 as const 对象;SnapshotMetadataFields.STATE 等成员写法不变,每个值是字面量类型('body.name',而不是 string)。不能再 new,也不能用 instanceof。 |
默认类型里的 any:DynamicDocument 是 Record<string, any>,CommandResult.result 是 Record<string, any>,DomainEventStream、StateEvent、EventStreamQueryClient、QueryClientFactory 的事件体(和状态)类型默认为 any | 改为 unknown:读值前先收窄,或写明类型(DomainEventStream<CartEvent>)。aggregate<Row>、aggregateStream<Row> 的 Row 可以是任意对象类型,接口也行;自己写的辅助函数若约束 Row extends DynamicDocument,改为 Row extends object。 |
所有头都必填且为 string 的 CommandRequestHeaders | 所有头可选并带类型:等待阶段是 CommandStage 名称,Command-Aggregate-Version 与 Command-Wait-Timeout 是整数字符串,Command-Local-First 是 'true' 或 'false'。用 commandHeaders({ … }) 与 waitStrategy({ … }) 构造,后者也支持等待链(tail)。 |
new CommandClient<C>(metadata) | new CommandClient(metadata);命令体类型移到调用处:send<C>(request)、sendAndWaitStream<C>(request)。 |
查询方法最后一个参数 abortController?: AbortController | 所有查询与加载方法改为 abort?: AbortController | AbortSignal,可传入 AbortSignal.timeout(ms) 或数据请求库的 signal。传控制器的调用仍能编译;实现 QueryApi 或 SnapshotQueryApi 的类需要更新签名。attributes 的类型为 Record<string, unknown>。 |
terms(field, alias, missingKey?) | terms(field, alias, { missingKey }) |
histogram(field, { interval, alias }) | histogram(field, alias, { interval }) |
dateHistogram(field, { unit, alias, timeZone?, dense? }) | dateHistogram(field, alias, { unit, timeZone?, dense? }) |
count(alias, predicate?)、any(field, alias, predicate?)、sum/avg/min/max/stddev/variance/distinctCount(expression, alias, predicate?) | 指标过滤移入选项对象:count(alias, { filter })、sum(expression, alias, { filter })…… |
percentile(expression, percentile, alias, predicate?) | percentile(expression, alias, { percentile, filter? }) |
QueryClientFactory#createOwnerLoadStateAggregateClient | createLoadOwnerStateAggregateClient |
createQueryApiMetadata、SnapshotQueryEndpointPaths、EventStreamQueryEndpointPaths、LoadStateAggregateEndpointPaths、LoadOwnerStateAggregateEndpointPaths | 不再公开:通过 QueryClientFactory 创建客户端并调用其方法。 |
getPropertyValue、requireElementScopedFilter、effectiveSort、DEFAULT_OWNER_ID | 已删除。读取嵌套值请用自己的工具函数或工具库;空所有者 ID 直接用 ''。 |
带 contextName、aggregateName 的 MediumMaterializedSnapshot / SmallMaterializedSnapshot | 这两个字段已删除,服务端从未发送过它们。 |
| 服务端中途失败时,错误作为又一个数据事件出现在流中 | listStream、listStateStream、aggregateStream、sendAndWaitStream 以 WowError 使流出错,for await 会抛出。原先检查 event.event 是否为错误名的代码应改为捕获异常。 |
listStream、listStateStream、aggregateStream、loadStream、sendAndWaitStream 返回 ReadableStream<JsonServerSentEvent<T>>,代码读 event.data | 改为返回行本身的 ReadableStream<T>(sendAndWaitStream 为 ReadableStream<CommandResult>,名字仍是 CommandResultEventStream)。写 for await (const row of stream),去掉 .data;命令结果的阶段看它自己的 stage。event、id、retry 不再提供:Wow 没有赋予它们含义。wow-react 的 ListStreamExecutor 随之改变。为自定义 text/event-stream 端点生成的客户端仍返回 fetcher 的 JsonServerSentEventStream。 |
新增且不破坏兼容:@ahoo-wang/wow-client/dsl 入口(不含 HTTP 代码的查询 DSL)、按版本范围读取的 EventStreamQueryClient.load / loadStream、WowMetadataClient,以及 aggregation.query() 与 AGGREGATION_LIMITS。
Wow 8.10 服务端
Wow 8.11 及以后的服务端支持 FilterExpression。Wow 8.10 服务端只认 Condition 模型,所以连接它的应用用 @ahoo-wang/wow-client/legacy 构造查询,其余一切从根入口导入;查询客户端两种查询都接受:
import type { SnapshotQueryClient } from '@ahoo-wang/wow-client';
import { and, eq, listQuery, ownerId } from '@ahoo-wang/wow-client/legacy';
declare const snapshots: SnapshotQueryClient<unknown>;
const carts = await snapshots.listState(
listQuery({ condition: and(ownerId('u-42'), eq('state.status', 'ACTIVE')) }),
);SnapshotQueryClient.getById 和 getStateById 发送的是 FilterExpression,需要 Wow 8.11 或更高版本;连接 8.10 时,改用 single 或 singleState,传入用 /legacy 的 aggregateId(id) 构造的查询。
3. 脚本改用新命令
{
"scripts": {
"generate": "wow-generator generate -i ./openapi.json -o ./src/generated -t ./tsconfig.json"
}
}v10 之前 fetcher-generator 命令仍作为 wow-generator 的别名可用,所以完成第 1 步后,没改的脚本照样能跑。已有的命令选项不变。
把可选配置文件从 fetcher-generator.config.json 改名为 wow-generator.config.json。v10 之前,新文件名不存在时仍会读取旧名,并给出弃用警告。所有权清单改名为 .wow-generator.json:第一次重新生成会读取已有的 .fetcher-generator.json,照常清理过时文件,然后用新清单替换它;请提交新清单。
配置读不到、解析不了或选项形状不对时,现在会以退出码 3 失败,而不是被忽略;http(s) 输入返回非 2xx 状态时以退出码 2 失败。检查退出码的脚本能看到这些失败,见 CLI 退出码。
4. 重新生成客户端
生成的代码改为从 @ahoo-wang/wow-client 而不是 @ahoo-wang/fetcher-wow 导入 Wow 类型。因此移除 fetcher-wow 后,由 fetcher-generator 生成的代码无法通过编译。用同一份 OpenAPI 文档重新生成:
pnpm exec wow-generator generate -i ./openapi.json -o ./src/generated -t ./tsconfig.json检查 diff。除导入模块名外,9.x 生成器还会带来下列预期内的变化:
- 查询类型:带
filter属性的ListQuery、PagedQueryschema(Wow 8.11 及以后)现在映射为@ahoo-wang/wow-client的FilterListQuery、FilterPagedQuery;Condition、ConditionOptions、Operatorschema 以及 Wow 8.10 的ListQuery、PagedQueryschema 映射为从@ahoo-wang/wow-client/legacy导入的类型。 - 每个文件以
// Code generated by wow-generator. DO NOT EDIT.开头,使用单引号,相对导入带.js扩展名。 - 方法以 operationId 的最后一段命名(
example.cart.add_cart_item→addCartItem),不再取尚未被占用的最短后缀。方法名因此改变时,可以在配置的apiClients[tag].methodNames中保留旧名。 - 命令客户端把构造器收到的
apiMetadata合并到默认值之上,所以new CartCommandClient({ fetcher })保留限界上下文的基础路径。原先不带这个前缀直接访问服务的代码,现在需要传basePath: ''。 - 查询客户端工厂的
aggregateName是聚合的路由段,字段类型为`${CartAggregatedFields}`。标注为ListQuery或FilterListQuery的查询需要写明字段类型:ListQuery<`${CartAggregatedFields}`>。 - 不是来自 Wow 的文档,其 API 客户端保留
tenantId和ownerId路径参数;只有 Wow 文档默认把它们交给拦截器。 - API 客户端方法把 query、header 参数和请求体作为带类型的位置参数,放在
httpRequest之前,必填的在前。原先通过httpRequest(urlParams.query、headers、body)传入它们的调用,现在改为按参数传入;httpRequest仍用于其他内容。 - 流式命令客户端(
CartStreamCommandClient)改为@api('', COMMAND_STREAM_ENDPOINT),从@ahoo-wang/wow-client导入,不再逐项写出JsonEventStreamResultExtractor。类型不变;但服务端发来错误事件时(例如命令校验失败),流现在以WowError报错,不再把ErrorInfo当成又一条命令结果交出来:for await会抛错。原先检查event.event是否为错误名的代码,改为捕获异常。 - 类型名保留 schema 名里的缩写。以大写字母开头、不含分隔符的段按原样保留:
MCPListTools不再变成McplistTools,OpenAIFile不再变成OpenAifile,RealtimeSessionCreateRequestGA不再变成RealtimeSessionCreateRequestGa。含分隔符或以小写开头的段仍转成 PascalCase(order_item→OrderItem)。来自 Wow 服务端的文档不受影响;其他文档按编译器报出的位置改导入即可。方法名、枚举成员、端点常量不变。 - 生成的
index.ts只导出本次生成的文件。以前,-t传入的 tsconfig 的include覆盖输出目录时,输出目录里手写的.ts文件也会被再导出;现在不论include覆盖哪些目录都不会。这类文件请从它自己的模块导入,最好放到输出目录之外。 - 生成的命令类型名不再重复
Command。请求体本身已命名为…Command的命令不再声明别名:MountedCommandCommand、MockVariableCommandCommand不再生成,方法参数为CommandRequest<CommandBody<MountedCommand>>。原先引用旧别名的代码改用请求体模型MountedCommand,或CommandBody<MountedCommand>。其他命令的别名不变(AddCartItemCommand)。 - API 客户端文件名改为 camelCase,与其他生成文件一致:
CartApiClient.ts→cartApiClient.ts。类名不变,从生成的index.ts导入的代码不受影响;直接导入该文件的(./generated/example/CartApiClient.js)要改路径。重新生成会删除旧文件,在不区分大小写的文件系统上也一样;在 macOS、Windows 上请以改名提交(git mv),因为那里的 git 可能察觉不到只改了大小写。 - 有多个路径变量的方法,调用处要调整实参顺序。 路径变量作为位置参数,按它们在路径中的顺序排列,不再按文档列出的顺序(可能是字母序,Wow 示例服务的文档就是这样):
/cart/{id}/{customerId}/{mockEnum}的mockVariableCommand(customerId, id, mockEnum, …)变为mockVariableCommand(id, customerId, mockEnum, …)。类型相同的路径变量按旧顺序传仍能编译,请求却会发到错误的资源,编译器发现不了这些调用:请搜索路由含两个及以上变量的方法的每一处调用。只有一个路径变量的方法不受影响。
其他差异都属于生成器的变化,要像审查契约变更一样审查。请重新生成,不要手工改生成文件里的导入:这些文件归生成器所有,见生成输出与重新生成。
5. 类型检查与测试
pnpm exec tsc --noEmit
pnpm test移除包之后,残留的 @ahoo-wang/fetcher-wow 导入会让类型检查失败。还要对真实的 Wow 服务端跑一遍应用的集成测试:类型检查不能证明路由和命令阶段的行为与之前一致。
此后的版本规则
- Wow 的 TypeScript 包跟随 Wow 发版。选择与 Wow 服务端一致的版本,并同时升级
wow-client、wow-generator和wow-react;它们之间以~x.y.z互相声明。 - 破坏性改动只在
x.Y.0版本发布,发布说明逐条列出并写明迁移方法。 - Wow 包在 npm 上的首个版本是 9.2.0(2026-10-03);暂时不能迁移的应用继续使用 5.x 的
fetcher-wow和fetcher-generator,它们在 Fetcher 的 5.x 分支上继续收到修复。 - 在 Wow 9.x 期间,客户端和生成器仍能连接 Wow 8.x 服务端(8.11 及以后用
FilterExpression,8.10 通过@ahoo-wang/wow-client/legacy;CI 对各版本验证了什么,见兼容性矩阵),fetcher-generator别名和fetcher-generator.config.json回退读取可用,已弃用的ConditionAPI 也可以从/legacy导入。它们都在 v10 移除;在此之前请改用FilterExpression和filter.*构造器,见过滤器。 - 新功能只进 Wow 的包。Fetcher 保留 5.x 分支只做修复,计划在 Fetcher 6.0 发布时对
fetcher-wow和fetcher-generator执行 npm deprecate。
检查清单
| 检查项 | 完成标准 |
|---|---|
| 依赖 | package.json 中已没有 fetcher-wow 和 fetcher-generator;应用若仍为其他 Hook 使用 fetcher-react,版本不低于 5.1.5 |
| 导入 | 没有源文件导入 @ahoo-wang/fetcher-wow,Condition API 和操作符文案从 @ahoo-wang/wow-client/legacy 导入,Wow 查询 Hook 从 @ahoo-wang/wow-react 导入 |
| 变化的 API | 不再调用 ErrorCodes.isSucceeded/isError、getPropertyValue、createQueryApiMetadata、*EndpointPaths 常量或 createOwnerLoadStateAggregateClient;聚合构造器按 (目标, 别名, 选项) 调用;命令头用 commandHeaders()/waitStrategy() 构造;失败调用用 toWowError 读取,流消费者捕获 WowError;Wow Hook 的 status 用字符串字面量比较,选项里不再传 initialStatus、propagateError、onAbort 或 resultExtractor |
| 生成代码 | 已用 wow-generator 重新生成,生成文件导入的是 @ahoo-wang/wow-client |
| 版本 | wow-client、wow-generator、wow-react 处于同一个小版本,并与 Wow 服务端一致 |
| 验证 | 类型检查以及针对真实 Wow 服务端的集成测试通过 |