news 2026/9/11 12:32:11

Huly 服务端客户端库 `@hcengineering/server-client` 深入解析:从版本演进到源码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Huly 服务端客户端库 `@hcengineering/server-client` 深入解析:从版本演进到源码实现

Huly 服务端客户端库@hcengineering/server-client深入解析:从版本演进到源码实现

【免费下载链接】platformHuly — All-in-One Project Management Platform (alternative to Linear, Jira, Slack, Notion, Motion)项目地址: https://gitcode.com/GitHub_Trending/platform80/platform

@hcengineering/server-client是 Huly 全栈平台(platform 为主线,结合其源码,完整梳理这个包从 0.7.0 首版发布到 0.7.17 的演进脉络,并逐一解析其四大核心能力——服务端 WebSocket 客户端工厂、transactor 端点发现、认证 Token 提取与 Blob 流式读写,让读者既能看懂版本历史,也能直接将其 API 用于自己的服务端集成。

一、包定位:服务端进程与平台之间的通信层

在 Huly 的架构中,浏览器端使用@hcengineering/client与 transactor 通信,而服务端进程(如 collaborator、workspace-service、tool、account-service 等)则需要一套针对 Node.js 环境优化的客户端设施。@hcengineering/server-client正是为此而生。

从 package.json 可以看到它的依赖关系,清晰地揭示了它的职责边界:

"dependencies": { "@hcengineering/platform": "workspace:^0.7.20", "@hcengineering/core": "workspace:^0.7.26", "@hcengineering/client-resources": "workspace:^0.7.19", "@hcengineering/client": "workspace:^0.7.19", "@hcengineering/account-client": "workspace:^0.7.25", "@hcengineering/server-core": "workspace:^0.7.19", "@hcengineering/server-token": "workspace:^0.7.18", "ws": "^8.18.2" }

可以看到它复用了平台层的clientclient-resources建立基础连接,通过account-client与账号服务交互,通过server-token完成 JWT 解析,并直接依赖ws包——这正是它与浏览器端客户端的本质区别:浏览器客户端使用浏览器原生 WebSocket,而服务端客户端使用 Node.js 的ws

包的入口 src/index.ts 导出四个模块:

export * from './account' export * from './blob' export * from './client' export * from './token'

下面逐一深入。

二、版本演进:从 0.7.0 首版到 0.7.17

CHANGELOG.md 记录了完整的发布历史。该日志由工具自动生成(This log was last generated on Wed, 26 Nov 2025 16:04:43 GMT and should not be manually modified.),因此每个条目的描述非常精简,但结合 CHANGELOG.json 与 package.json 中的当前版本0.7.18,可以还原出完整的演进脉络:

版本发布时间变更说明解读
0.7.02025-10-09Initial release包首次发布
0.7.12025-10-10New deps依赖更新,随 platform-rig 工具链演进
0.7.22025-10-11Use latest platform-rig升级到最新的@hcengineering/platform-rig构建工具链
0.7.32025-10-13update deps常规依赖更新
0.7.42025-10-14deps常规依赖更新
0.7.52025-10-14Version update only仅版本号同步
0.7.62025-10-15Version update only仅版本号同步
0.7.72025-10-17Update deps依赖更新
0.7.82025-10-21bump core跟随@hcengineering/core升级
0.7.92025-10-27update deps依赖更新
0.7.152025-10-27Use updated deps依赖更新
0.7.162025-11-06bump core再次跟随@hcengineering/core升级
0.7.172025-11-26Bump常规版本推进

从这份日志可以读出三个规律:第一,该包的绝大多数变更都是"跟随核心包升级",尤其是bump core(跟随@hcengineering/core)与Use latest platform-rig(跟随构建工具链),说明它作为底层基础设施,稳定性优先;第二,10 月 14 日一天内连续发布了 0.7.4 / 0.7.5 / 0.7.6 三个版本,其中两个标注Version update only,说明存在密集的依赖同步流程;第三,从 0.7.0 到 0.7.17 均为Patches级别的补丁版本,没有破坏性变更,进一步印证了其 API 的稳定性。当前仓库中 package.json 的版本已是0.7.18,略微领先于 CHANGELOG 记录的最后一条。

三、核心能力一:createClient—— 服务端 WebSocket 客户端工厂

src/client.ts 是包的"心脏"。它导出一个异步工厂函数:

export async function createClient ( transactorUrl: string, token: string, model?: Tx[], connectTimeout: number = 0 ): Promise<Client>

它的工作流程分为四步:

1. 用ws替换浏览器 WebSocket 工厂。这是服务端与浏览器端最关键的分水岭:

const WebSocket = require('ws') setMetadata(client.metadata.ClientSocketFactory, (url) => { const socket = new WebSocket(url, { headers: { 'User-Agent': getMetadata(plugin.metadata.UserAgent) ?? 'Anticrm Client' } }) return socket })

服务端进程通过ws库建立真实的 WebSocket 连接,并可以自定义User-Agent请求头——这个值通过 src/plugin.ts 中声明的插件元数据plugin.metadata.UserAgent注入。

2. 强制启用二进制协议与压缩。服务端客户端默认使用二进制协议和协议压缩,这与浏览器端"尽量兼容"的策略不同,服务端场景更看重传输效率:

setMetadata(client.metadata.UseBinaryProtocol, true) setMetadata(client.metadata.UseProtocolCompression, true)

3. 设置连接超时。connectTimeout参数(毫秒,默认0表示不超时)会被写入client.metadata.ConnectionTimeout,注释明确指出其语义:"If connectTimeout is set, connect will try to connect only specified amount of time, and will return failure if failed."即一旦超时立即失败,而不是无限等待。

4. 可选注入预置模型。当传入model?: Tx[]时,代码会基于 sha1 对事务列表做链式哈希(每个事务的哈希由前一个哈希拼接其 JSON 序列化结果后计算得出),并覆写平台的持久化存储,使客户端直接使用传入的模型而无需从服务端加载:

if (model !== undefined) { let prev = '' const hashes = model.map((it) => { const h = crypto.createHash('sha1') h.update(prev) h.update(JSON.stringify(it)) prev = h.digest('hex') return prev }) setMetadata(client.metadata.OverridePersistenceStore, { load: async () => ({ hash: hashes[hashes.length - 1], transactions: model, full: true }), store: async (model: LoadModelResponse) => {} }) }

最后通过getResource(client.function.GetClient)获取真正的客户端工厂实现(来自client-resources),并以tokentransactorUrl完成连接。这意味着调用方只需要关心"连到哪里、用什么凭证",而把协议细节完全交给平台层。

四、核心能力二:getTransactorEndpoint—— 端点发现与重试策略

服务端进程通常只知道账号服务(account service)的地址,而不知道具体应该连接哪个 transactor 端点。src/account.ts 中的getTransactorEndpoint正是解决这个问题的:

export async function getTransactorEndpoint ( token: string, kind: 'internal' | 'external' | 'byregion' = 'byregion', timeout: number = -1 ): Promise<string>

它的工作方式是:通过账号客户端调用selectWorkspace('', kind, externalRegions)查询当前 workspace 对应的端点。三个参数的含义:

  • kind:端点类型,'internal'(内部端点)、'external'(外部端点)或'byregion'(按区域选择,默认值);
  • timeout:总超时毫秒数,默认-1表示不超时;
  • externalRegions:来自环境变量process.env.EXTERNAL_REGIONS?.split(';'),即用分号分隔的外部区域列表。

值得注意的细节是连接错误重试逻辑。代码定义了connectionErrorCodes = ['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND'],在循环中:

  • 若超时(timeout > 0 && st + timeout < Date.now()),直接抛出原错误;
  • 若错误原因码属于连接类错误(如ECONNREFUSED),等待 1 秒后重试
  • 其他错误直接抛出。

这种"只对连接类错误无限重试、其他错误快速失败"的策略,恰好契合服务端进程启动阶段账号服务尚未就绪的场景。

同文件还提供了三个可复用的重试工具函数:

  • withRetry(f, shouldFail, intervalMs = 1000):通用的重试包装器,只要shouldFail(err, attempt)返回false就持续重试;
  • withRetryConnUntilTimeout(f, timeoutMs = 5000):在指定时间窗内(默认 5 秒)对连接类错误重试;
  • withRetryConnUntilSuccess(f):对连接类错误无限重试,直到成功;对其他错误打印console.error后抛出。

此外,getAccountClient(token?, retryTimeoutMs?)是对@hcengineering/account-clientgetClient的薄封装,账号服务地址来自plugin.metadata.Endpoint元数据。

五、核心能力三:readToken/extractToken—— 认证 Token 提取

服务端进程(尤其是 HTTP 服务)需要从请求头中解析出平台 Token。src/token.ts 提供了两个函数:

  • readToken(headers: IncomingHttpHeaders): string | undefined:返回原始 Token 字符串;
  • extractToken(headers): Token | undefined:返回解码后的Token对象。

Token 的提取优先级为:Authorization头优先,Cookie兜底。具体来说:

  1. Authorization:按Bearer <token>格式解析,取空格后的第二部分:

    const encodedToken = authorization.split(' ')[1]
  2. Cookie:遍历所有 cookie,寻找名称包含token的项(大小写不敏感),取出其值并调用decodeToken(来自@hcengineering/server-token)验证:

    const cookies = cookie.split(';') for (const cookie of cookies) { if (cookie.toLocaleLowerCase().includes('token')) { const encodedToken = cookie.split('=')[1] const token = decodeToken(encodedToken) if (token.workspace != null) { return encodedToken // 带 workspace 的 token 立即返回 } authToken = encodedToken // 否则暂存 } }

    注意这里的细节:如果 cookie 中的 token 解码后带有 workspace 信息,立即返回;否则暂存为后备。这一设计在 Huly 的多工作区模型下很有意义——服务端往往需要的是与具体 workspace 绑定的请求上下文。

extractToken则在readToken的基础上多走一步decodeToken,把字符串转为结构化对象(包含账号与工作区信息),任何一步失败都返回undefined,保证调用方的健壮性。

六、核心能力四:BlobClient—— 大对象流式读写

src/blob.ts 中的BlobClient封装了对平台存储适配器(StorageAdapter,来自@hcengineering/server-core)的大对象(Blob)访问:

export class BlobClient { constructor ( readonly storageAdapter: StorageAdapter, readonly workspace: WorkspaceIds ) { ... } }

它对外暴露三个方法:

  • checkFile(ctx, name): Promise<boolean>:通过storageAdapter.stat判断对象是否存在;
  • upload(ctx, name, size, contentType, buffer):将内存中的Buffer直接写入存储;
  • writeTo(ctx, name, size, writable)将存储对象以流式方式写出,这是最有技术含量的一个。

writeTo的实现值得展开。它按50MB(chunkSize = 50 * 1024 * 1024为单位,通过storageAdapter.partial分段读取并写入目标流,内置了多层容错

  1. 单块重试:每个 50MB 块最多尝试 5 次,失败后按100 * (i + 1)毫秒退避;
  2. 空块保护:连续收到 3 次(maxEmptyChunkRetries = 3)空块即中止并抛出错误,防止无限循环,同时每次空块重试间隔递增(100 * emptyChunkRetries毫秒);
  3. 键不存在容错:若底层存储返回NoSuchKey/NotFound等错误码,视为"对象不存在",直接结束写入而非报错;
  4. 写入错误传播writable.write的回调若收到错误会reject,从而中断整个流程。

整个设计体现了一个面向大文件(视频、文档等)传输的服务端客户端应有的品质:分块、限流、退避重试、防死循环

七、仓库中的实际使用

在仓库中,该包的 API 被多个服务端组件消费,印证了其作为基础设施的定位。例如在 server/account-service/src/index.ts、server/collaborator/src/platform.ts、server/tool/src/utils.ts、server/workspace-service/src/service.ts 等文件中,都通过getTransactorEndpointextractToken完成端点发现与请求认证。这些服务端进程的典型接入模式是:

  1. 读取配置得到账号服务地址,设置plugin.metadata.Endpoint
  2. 用某个账号 Token 调用getTransactorEndpoint(token, kind, timeout)拿到 transactor 地址;
  3. 调用createClient(transactorUrl, token, model, connectTimeout)建立连接;
  4. 在收到 HTTP 请求时,用extractToken(headers)解析请求方身份。

八、构建与测试

作为一个标准 Rush monorepo 包,它的 package.json 中提供了完善的开发脚本:

"scripts": { "build": "compile", "build:watch": "compile", "format": "format src", "test": "jest --passWithNoTests --silent --forceExit", "_phase:build": "compile transpile src", "_phase:test": "jest --passWithNoTests --silent --forceExit", "_phase:format": "format src", "_phase:validate": "compile validate" }

构建与校验通过@hcengineering/platform-rigcompile/validate指令)完成,测试使用 Jest(当前仓库中无独立测试文件,因此--passWithNoTests保证空跑通过)。包发布时只包含lib/**/*types/**/*src/**/*,并显式排除了所有测试文件(见files字段),符合 npm 包发布的最佳实践。由于该包不含浏览器端代码,发布时无需 Svelte 相关处理,但保留了"svelte": "src/index.ts"字段以便工具链统一识别入口。

结语

回顾整个 CHANGELOG.md,@hcengineering/server-client用十余个"平凡"的补丁版本维系了服务端客户端的稳定,而这背后是四个精悍模块的支撑:client.tscreateClientws工厂切入平台协议栈并默认启用二进制压缩,account.tsgetTransactorEndpoint以"连接错误无限重试"的策略完成端点发现,token.ts以"Authorization 优先、Cookie 兜底"的规则解析请求身份,blob.tsBlobClient以 50MB 分块加多层容错实现大对象流式传输。对于任何希望在 Node.js 进程中与 Huly 平台集成的开发者,理解这四个模块就等于掌握了服务端接入的全部关键路径。

【免费下载链接】platformHuly — All-in-One Project Management Platform (alternative to Linear, Jira, Slack, Notion, Motion)项目地址: https://gitcode.com/GitHub_Trending/platform80/platform

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 12:31:24

CMSIS-NN源码深度解析:嵌入式AI推理加速原理与实战边界

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

作者头像 李华
网站建设 2026/9/11 12:28:45

支持信源检测API对接的平台哪家好?2026年度选型测评报告

引言信源检测&#xff0c;正在从“投后看收录”的单一动作&#xff0c;升级成一套要接进企业系统、能持续自动化监测的 API 能力。企业做完一次发稿、铺了一批内容之后&#xff0c;真正的疑问不是“我发了几篇”&#xff0c;而是“这些内容有没有成为被 AI 采信的信源、在回答里…

作者头像 李华
网站建设 2026/9/11 12:28:38

把 M16 帧率拉满:G-Helper Turbo 模式只调 3 个参数

把 M16 帧率拉满&#xff1a;G-Helper Turbo 模式只调 3 个参数 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, Vivobook, Zenbook, Expe…

作者头像 李华
网站建设 2026/9/11 12:26:38

孩子为何总想逃离AI玩伴?陪伴机器人亟需一场“去教育化”变革

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

作者头像 李华
网站建设 2026/9/11 12:25:58

EMD-KPCA-LSTM组合模型:MATLAB下多维时间序列预测实战

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

作者头像 李华