首页/新闻资讯/正文详情

nestjs-trpc 模块配置完全指南:TRPCModule.forRoot 全参数解析与 Comp AI CRM 实践

发布时间:2026/9/24 15:47:55 来源:云帆数科 栏目:资讯中心
nestjs-trpc 模块配置完全指南:TRPCModule.forRoot 全参数解析与 Comp AI CRM 实践
后端前端CRM人工智能AI Agent【免费下载链接】crmComp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.项目地址https://gitcode.com/gh_mirrors/crm48/crm点击查看免费下载TRPCModule.forRoot()是 nestjs-trpc 在 NestJS 中接入 tRPC 的唯一入口其选项决定了路由挂载点、请求上下文、全局中间件、错误处理、数据序列化与流式传输等全部行为。本文基于 nestjs-trpc v2.13.0 发布的dist/interfaces/module-options.interface.d.ts逐项拆解TRPCModuleOptions的每个字段并结合 Comp AI CRM 仓库 apps/api/src/trpc/trpc.module.ts 中的真实生产配置给出可直接复制的配置写法与避坑要点。读完本文你将掌握如何正确装配 forRoot、如何用全局中间件与 onError 做可观测性、如何用 errorFormatter 塑造客户端看到的错误载荷以及 SSE / JSONL 流式场景下的超时与保活调优。TRPCModuleOptions 全景一份类型定义看懂全部选项TRPCModuleOptions的类型定义被完整转写自 v2.13.0 发布的dist/interfaces/module-options.interface.d.tsexport interface TRPCModuleOptions { basePath?: string; context?: ClassTRPCContext; errorFormatter?: TRPCErrorFormatterany, TRPCDefaultErrorShape; transformer?: DataTransformer | CombinedDataTransformer; logger?: LoggerService; onError?: ClassTRPCErrorHandler; globalMiddlewares?: ArrayClassTRPCMiddleware | ConstructorTRPCMiddleware; sse?: TRPCSSEOptions; jsonl?: TRPCJSONLOptions; }各选项的类型与默认值汇总如下OptionTypeDefaultbasePathstring/trpccontextClassTRPCContext—errorFormatterTRPCErrorFormatter—transformerDataTransformer \| CombinedDataTransformer—loggerLoggerServiceConsoleLoggeronErrorClassTRPCErrorHandler—globalMiddlewaresArrayClassTRPCMiddleware[]sseTRPCSSEOptions见下文说明jsonlTRPCJSONLOptions—两条贯穿全文的硬性规则请先记住TRPCModule.forRoot(options?)返回的是DynamicModule且没有forRootAsync。如果配置依赖运行时值如数据库连接、环境变量正确做法是把ConfigService注入到 context 类或中间件里而不是试图在模块定义阶段await任何东西。所有以类形式传入的选项context、onError、globalMiddlewares中的类必须同时出现在模块的providers数组里。forRoot只是记录“用哪个类”真正让 Nest 完成实例化与依赖注入的是providers。漏掉任何一个运行时就会得到未定义注入或“procedure 不存在”的 404。这一点在 Comp AI CRM 中体现得十分标准apps/api/src/trpc/trpc.module.ts 里forRoot传入的TrpcContext、TrpcErrorHandler、LoggingMiddleware、DomainErrorMiddleware四个类全部重复列入了providers。basePath路由挂载点与 404 的头号来源basePath决定 tRPC handler 挂载在哪个路径下。默认值是/trpc此时每个 procedure 的完整地址为/trpc/alias.procedureTRPCModule.forRoot({ basePath: /api/trpc })两个最容易踩的坑必须与客户端httpLink配置的url完全一致包括你用app.setGlobalPrefix()设置的全局前缀。两者是拼接关系basePath与全局前缀会组合成最终路径忘记这一点是“看起来像 procedure 不存在”的 404 最常见成因。如果客户端指向了错误的路径你会在网络面板看到一个 404而 tRPC 侧查不到任何 procedure 调用记录——先核对两端 URL 再排查逻辑。Comp AI CRM 的实践是basePath: /api/trpc见 apps/api/src/trpc/trpc.module.ts并在错误处理中保留了/trpc/${path}的路径还原逻辑见 apps/api/src/trpc/trpc-error.handler.ts方便遥测系统还原被全局前缀修饰前的原始 procedure 路径。context每个请求一次的自定义上下文context接收一个实现TRPCContext接口的类该类的实例每个请求创建一次是挂载请求对象、会话、按请求粒度的数据加载器的正确位置。接口约定与实现细节可进一步阅读 middlewares-and-context.md。Comp AI CRM 的实现展示了标准写法apps/api/src/trpc/trpc.context.tsimport { auth } from crm/auth; import { Injectable } from nestjs/common; import { fromNodeHeaders } from better-auth/node; import type { Request } from express; import type { ContextOptions, TRPCContext } from nestjs-trpc; import type { BaseTrpcContext } from ./context.types; export async function createBaseTrpcContext( req: Request | undefined, ): PromiseBaseTrpcContext { const session req ? await auth.api .getSession({ headers: fromNodeHeaders(req.headers) }) .catch(() null) : null; return { req, session }; } Injectable() export class TrpcContext implements TRPCContext { async create(opts: ContextOptions): PromiseBaseTrpcContext { const req req in opts ? opts.req : undefined; return createBaseTrpcContext(req); } }从源码结构可以看到几个值得借鉴的要点上下文类型在 context.types.ts 中单独定义BaseTrpcContext包含req与session并派生出带user的AuthedTrpcContext供认证后的 procedure 使用——类型在中间件链中逐层收窄而不是在上下文里塞一个大而全的对象。session 解析失败时用.catch(() null)兜底保证未登录请求也能正常进入后续流程由认证中间件决定是否拒绝。req in opts的存在性判断是为了兼容 Express 与 Fastify 两套ContextOptions结构保证上下文类在两个平台下都能工作。globalMiddlewares全局中间件与“不要放认证”的告诫globalMiddlewares中的中间件会应用到应用内每一个procedure且执行顺序在 router 级与 procedure 级中间件之前Module({ imports: [ TRPCModule.forRoot({ globalMiddlewares: [LoggedMiddleware, ErrorReportingMiddleware], }), ], providers: [LoggedMiddleware, ErrorReportingMiddleware], }) export class AppModule {}适合放进全局中间件的场景计时、日志、错误上报、限流。不适合的场景是认证——因为全局认证中间件必须为每一个公开 procedure 特判放行而这些例外很容易写错、漏掉导致公开接口意外被锁死或受保护接口意外暴露。更好的做法是在需要保护的 router 上显式标注UseMiddlewares(AuthMiddleware)让受保护面在代码里一目了然。Comp AI CRM 的全局中间件组合apps/api/src/trpc/trpc.module.ts恰好体现了“全局做可观测、局部做认证”的分工LoggingMiddlewareapps/api/src/trpc/middlewares/logging.middleware.ts在next()前后记录耗时输出type path ok/err durationMs的结构化日志。注意它用Date.now()而不是process.hrtime毫秒精度对日志足够关键点是始终returnopts.next()的结果否则中间件链会在这一层被吞掉。DomainErrorMiddlewareapps/api/src/trpc/middlewares/domain-error.middleware.ts把业务代码抛出的 NestJSHttpException翻译成 tRPC 的TRPCError通过 HTTP 状态码400/401/403/404/409/429映射到BAD_REQUEST/UNAUTHORIZED/FORBIDDEN/NOT_FOUND/CONFLICT/TOO_MANY_REQUESTS其余一律归为INTERNAL_SERVER_ERROR。这解决了一个关键问题默认情况下HttpException不会被翻译成 tRPC 错误形状会以不透明的 500 浮出表面。AuthMiddleware与SessionOnlyMiddleware并没有放进globalMiddlewares而是作为 providers 被模块exports由各个 router 按需引用——这正是原文档建议的“显式认证”模式。onError只观察、不改写的错误上报钩子onError是每次 procedure 抛错时被调用的可注入 handler典型用途是上报 Sentry 等监控系统。关键约束它只能观察不能改写响应。// app.error-handler.ts import { Inject, Injectable } from nestjs/common; import { OnErrorOptions, TRPCErrorHandler } from nestjs-trpc; Injectable() export class AppErrorHandler implements TRPCErrorHandler { constructor(Inject(LogService) private readonly logService: LogService) {} onError(opts: OnErrorOptions): void { this.logService.error([${opts.type}] ${opts.path}: ${opts.error.message}); } }OnErrorOptions与TRPCErrorHandler的完整接口如下export interface OnErrorOptions { error: TRPCError; type: TRPCProcedureType | unknown; path: string | undefined; input: unknown; ctx: Recordstring, unknown | undefined; req: unknown; } export interface TRPCErrorHandler { onError(opts: OnErrorOptions): void; }装配方式同样是“forRoot 注册 providers 提供实例”Module({ imports: [TRPCModule.forRoot({ onError: AppErrorHandler })], providers: [AppErrorHandler], }) export class AppModule {}三条使用纪律onError返回void且不会被 await——不要把慢操作如同步发送邮件、磁盘写入放在请求路径上。opts.input是用户的原始载荷opts.ctx可能携带会话数据两者都会随你的上报到达目标系统受数据合规约束时务必先脱敏再落日志。要区分真实 bug 与预期拒绝按错误码过滤——一个打错 id 的NOT_FOUND不是事故onError(opts: OnErrorOptions): void { if (opts.error.code INTERNAL_SERVER_ERROR) { this.sentry.captureException(opts.error.cause ?? opts.error, { extra: { path: opts.path } }); } }Comp AI CRM 的 trpc-error.handler.ts 完整实现了这套过滤INTERNAL_SERVER_ERROR时用logger.error记录堆栈error.stack ?? String(error.cause ?? error)并调用crm/telemetry的apiError上报携带还原后的/trpc/${path}与status: 500其他错误码则降级为logger.warn只记message/type/path/code避免噪音淹没真实故障。errorFormatter唯一能改写客户端载荷的选项与onError不同errorFormatter是挂在 options 对象上的普通函数不是可注入类它负责塑形客户端收到的错误载荷——既能附加结构化的校验错误又能保证内部消息不会在生产环境泄漏出去TRPCModule.forRoot({ errorFormatter({ shape, error }) { return { ...shape, data: { ...shape.data, zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, }, }; }, })tRPC 官方的错误格式化规范可参考 tRPC 文档的 error formatting 章节核心心智模型是onError看内部、errorFormatter看外部前者做可观测性后者做客户端契约。Comp AI CRM 的 error-formatter.ts 是一个值得精读的生产级范例它把 tRPC 字符串化后的整个ZodError从“满是code、minimum、inclusive、path的 JSON 数组”提炼成人类可读的完整句子如email must be a valid email。两个实现细节值得注意用duck-typing鸭子类型解析 cause 而非instanceof ZodError——错误跨包边界传递时workspace 里如果存在两份 zod 副本会导致instanceof静默失效把原始 JSON 又打回屏幕上。对以标点结尾的 message 直接原样返回项目自定义的校验文案本身是整句否则才拼接字段名前缀空句子与重复句子会被过滤去重。transformerJSON 表达不了的类型的救星数据 transformer 用于传输 JSON 无法直接表达的类型——Date、Map、Set、BigInt、undefinedimport superjson from superjson; TRPCModule.forRoot({ transformer: superjson })三条硬性约束客户端必须配置同一个 transformer否则每个请求都会反序列化失败。自 v2.5.0 起 CLI 在生成 schema 时会自动探测 transformer生成的类型会把 transformer 的影响考虑进去。新增或移除 transformer 是破坏性的线格式变更——必须两端服务端与所有客户端一起部署不能只改一端灰度。logger唯一不经过 DI 的选项logger接受任意实现了LoggerService的实现替换默认的ConsoleLoggerimport { Module } from nestjs/common; import { TRPCModule } from nestjs-trpc; import { MyLogger } from ./my-logger.service; Module({ imports: [TRPCModule.forRoot({ logger: new MyLogger() })], }) export class AppModule {}特别注意这里传的是实例而不是类——它是全部选项中唯一不参与 DI 解析的因此一个自身需要注入依赖的 logger 只能手动构造或从 app context 中获取。Pino 与 Winston 的适配器如nestjs-pino都能满足该接口。Comp AI CRM 传入了new ContextLogger()apps/api/src/trpc/trpc.module.ts这个自定义 logger 定义在 context-logger.ts与仓库自建的日志中间件、异常过滤器共用同一套上下文日志体系保证 tRPC 请求日志与 Nest 其他日志格式统一。sse订阅流的保活与超时调优TRPCSSEOptions完整类型v2.13.0 新增export interface TRPCSSEOptions { /** Enable SSE subscriptions. default true */ enabled?: boolean; ping?: { /** default false */ enabled: boolean; /** Interval in milliseconds. default 1000 */ intervalMs?: number; }; /** Max duration in ms before ending the stream. default undefined */ maxDurationMs?: number; /** End the request immediately after data is sent. default false */ emitAndEndImmediately?: boolean; client?: { /** Client reconnects after this much inactivity, in ms. default undefined */ reconnectAfterInactivityMs?: number; }; }一个生产环境的安全基线TRPCModule.forRoot({ sse: { ping: { enabled: true, intervalMs: 30000 }, client: { reconnectAfterInactivityMs: 60000 }, }, })四个关键认知Ping 默认关闭而intervalMs默认1000。如果只开 ping 不设间隔每个打开的流每秒会收到一次 ping——务必显式设置间隔。间隔要低于代理的空闲超时。30s 对 nginx、ALB、Cloudflare 常见的 60s 默认值是安全的。client.reconnectAfterInactivityMs应明显大于ping.intervalMs否则客户端会在健康的 ping 间隙里反复重连。emitAndEndImmediately是为无法保持流式响应的 serverless 运行时准备的在常规长驻 Node 服务器上请保持关闭——它会让订阅失去意义。maxDurationMs用来封顶流生命周期当上游负载均衡器反正会切断连接时主动收尾比被动掐断能让客户端重连得更干净。jsonl批量流式响应的保活心跳TRPCJSONLOptions只有一个字段export interface TRPCJSONLOptions { /** Interval in ms between keep-alive pings on streamed batch responses. default undefined */ pingMs?: number; }它作用于httpBatchStreamLink的批量流式响应不是订阅。适用场景你批量请求了多个 procedure其中某个慢 procedure 让整个响应挂着不返回超过了代理的空闲超时。此时给批量流加心跳TRPCModule.forRoot({ jsonl: { pingMs: 30000 } })已经消失的选项autoSchemaFile 与 schemaFileImportsautoSchemaFile和schemaFileImports是v1 时代的选项在 v2 的TRPCModuleOptions类型中不存在——传给forRoot会直接报 TypeScript 错误。但官方多篇文档context、client、integrations 页面至今仍展示autoSchemaFile: ./src/generated这是 v1/v2 混排造成的坑。v2 中输出位置是 CLI 的职责nestjs-trpc generate --output ./src/generated相关细节见 codegen-and-client.md。兼容性备注v2 CLI 在扫描你的forRoot调用时仍会识别字面量autoSchemaFile以帮助从 v1 迁移的项目——但不要依赖它它不属于类型化 API。Express 与 Fastify零配置双平台适配nestjs-trpc原生同时支持 Express 与 Fastify无需任何配置适配器会检查 Nest 的HttpAdapterHost并自动选择正确的 tRPC adapter。唯一的可见影响在ContextOptions的类型上——它是CreateExpressContextOptions | CreateFastifyContextOptions的联合类型。在伸手去取某个驱动特有的 request 属性之前先做类型收窄你的 context 类就能保持跨平台可移植。这正是 Comp AI CRM 的TrpcContext.create中req in opts ? opts.req : undefined判断的用意兼容两个平台的同时保持类型安全。实战对照一份完整的生产级 forRoot 配置把本文所有要点汇聚成 Comp AI CRM 的实际装配apps/api/src/trpc/trpc.module.tsModule({ imports: [ TRPCModule.forRoot({ basePath: /api/trpc, context: TrpcContext, logger: new ContextLogger(), errorFormatter: formatTrpcError, onError: TrpcErrorHandler, globalMiddlewares: [LoggingMiddleware, DomainErrorMiddleware], }), ], providers: [ TrpcContext, TrpcErrorHandler, LoggingMiddleware, DomainErrorMiddleware, AuthMiddleware, SessionOnlyMiddleware, ], exports: [AuthMiddleware, SessionOnlyMiddleware], }) export class TrpcModule {}这份配置体现了本文的全部原则basePath 显式声明避免默认/trpc与全局前缀组合产生歧义context 类实现了完整的会话解析并独立于模块定义时间全局中间件只做日志与异常翻译认证AuthMiddleware与会话限制SessionOnlyMiddleware通过exports交给各 router 显式UseMiddlewaresonError 按错误码分级500 级才上报遥测其余仅 warnerrorFormatter 把 Zod 校验错误翻译成可读句子不泄漏内部堆栈logger 传入项目自定义实例与全局日志体系打通。对照 v2.13.0 的类型定义逐项核验后可以发现这份配置恰好覆盖了TRPCModuleOptions中除transformer、sse、jsonl之外的全部字段——项目暂未引入自定义 transformer数据均为 JSON 可表达类型也暂未启用订阅与批量流式传输一旦启用可分别按本文的 SSE 保活基线与jsonl: { pingMs: 30000 }方案直接扩展。参考链接apps/api/src/trpc/trpc.module.ts — forRoot 生产装配示例apps/api/src/trpc/trpc.context.ts — context 类实现apps/api/src/trpc/error-formatter.ts — errorFormatter 生产实现apps/api/src/trpc/trpc-error.handler.ts — onError 分级上报实现apps/api/src/trpc/middlewares — 全局/认证中间件目录middlewares-and-context.md — context 类与中间件链的完整讨论codegen-and-client.md — CLI 生成与客户端消费api-reference.md — 从发布的 .d.ts 转写的完整导出符号与类型签名赞分享后端前端CRM人工智能AI Agent【免费下载链接】crmComp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.项目地址https://gitcode.com/gh_mirrors/crm48/crm点击查看免费下载相关推荐NestJS tRPC 端到端类型安全实战nestjs-trpc 适配器完全指南基于 Comp AI CRM 源码验证NestJS tRPC 端到端类型安全实战nestjs trpc 适配器完全指南基于 Comp AI CRM 源码验证 本文是一份以 nestjs t后端前端CRM人工智能AI AgentComp AI CRM 实战nestjs-trpc 中间件与上下文的完整指南Comp AI CRM 实战nestjs trpc 中间件与上下文的完整指南 导读 本指南以 nestjs trpc 官方技能文档为骨架结合 Comp AI后端前端CRM人工智能AI Agentnestjs-trpc 代码生成与客户端使用Comp AI CRM 的端到端类型安全实践nestjs trpc 代码生成与客户端使用Comp AI CRM 的端到端类型安全实践 本篇技术指南聚焦 nestjs trpc 的代码生成Codegen后端前端CRM人工智能AI Agent上一篇STM32 PID温控实战指南从0到1实现±0.5℃高精度控制下一篇终极指南如何使用RPG Maker Decrypter快速解密游戏资源创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

Edge AI工控主板I/O桥接:PCIe与USB 2.0转UART方案选型与实战
Edge AI工控主板I/O桥接:PCIe与USB 2.0转UART方案选型与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 15:47:55

Zynq-7000 Vivado设计流程全解析:从工程创建到比特流生成
Zynq-7000 Vivado设计流程全解析:从工程创建到比特流生成

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/24 15:47:48

PHPStan 纯度分析错误标识符 possiblyImpure.propertyAssign 深度解析
PHPStan 纯度分析错误标识符 possiblyImpure.propertyAssign 深度解析

开发工具代码质量静态分析 【免费下载链接】phpstan PHP Static Analysis Tool - discover bugs in your code without running it! 项目地址: https://gitcode.com/gh_mirrors/ph/phpstan 点击查看 免费下载 possiblyImpure.propertyAssign 是 PHPStan 纯度检查&a… · 2026/9/24 15:47:48

WinSW实战:把Java应用注册为Windows服务以稳定运行NeoJ Community
WinSW实战:把Java应用注册为Windows服务以稳定运行NeoJ Community

1. 先从为什么说起:neoj-community 为什么要做成 Windows 服务1.1 直接跑命令行的痛,老运维都懂先说结论:任何需要长期在后台跑的程序,都不应该裸跑在控制台窗口里。neoj-community 这种社区版服务,本地开发测试还好说… · 2026/9/24 18:43:06

基于东方通中间件的教学资源系统:SpringBoot+Vue从开发到TongWeb部署实战
基于东方通中间件的教学资源系统:SpringBoot+Vue从开发到TongWeb部署实战

从选题开始,这个项目就比大多数“图书馆管理系统”一类毕设有意思。我见过太多学生交上来的SpringBootVue项目,清一色跑在Tomcat上,一问到中间件、应用服务器、部署架构就答不上来。而你这个题目里带上了“东方通中间件”,天然就有… · 2026/9/24 18:43:06

数据产品运营分析实战:从指标体系到决策闭环
数据产品运营分析实战:从指标体系到决策闭环

数据产品经理这个岗位,这几年算是被行业反复讨论的热词之一。但说实话,我见过太多团队口口声声说要“数据驱动”,结果核心决策依然靠老板拍脑袋,或者产品上线一个多月连基本的埋点日志都没对齐。真正能把运营数据分析这件事做到能… · 2026/9/24 18:43:06

HTML5网页调用手机相机的可控实现方案
HTML5网页调用手机相机的可控实现方案

简介:本资源是一套面向Web前端开发者与移动端H5项目实践者的完整拍照上传解决方案,聚焦HTML5在手机浏览器中调用摄像头、预览图片并上传至PHP后台的核心流程。资源包含22个文件,涵盖1个主入口HTML页面、1个核心JS脚本、1个PHP服务端接收脚本&… · 2026/9/24 18:43:06

市场饱和度评估:基于 OpenClaw 采集区域与行业公开数据的量化分析方法
市场饱和度评估:基于 OpenClaw 采集区域与行业公开数据的量化分析方法

一、引言:为什么要做市场饱和度评估在企业进入新市场、调整区域经营策略或制定行业扩张计划时,决策者最常面对的问题之一,就是目标市场是否已经趋于饱和。市场饱和度反映的是一个区域或行业中,现有供给能力与潜在需求之间的匹配程… · 2026/9/24 18:43:06

GEO优化选型避坑指南:成本逻辑、路线对比、认知误区与行业趋势复盘
GEO优化选型避坑指南:成本逻辑、路线对比、认知误区与行业趋势复盘

1. 引言区别于传统SEO的固定排名逻辑,GEO优化依托大模型语义识别、内容采信、智能推荐机制,重构了品牌AI场景流量获取逻辑。赛道热度攀升的同时,行业乱象随之显现:市场报价从每月数千元至数万元跨度极大,服务标准不统一… · 2026/9/24 18:42:53

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码