Refly refly/utils 工具包实战Vitest 测试工作流与 aggregateTokenUsage 聚合逻辑源码解析【免费下载链接】reflyThe first open-source agent skills builder. Define skills by vibe workflow, run on Claude Code, Cursor, Codex more. Build Clawdbot · APIs for Lovable · Bots for Slack Lark/Feishu · Skills are infrastructure, not prompts.项目地址: https://gitcode.com/GitHub_Trending/re/refly本篇技术指南围绕 Refly 开源仓库中的refly/utils共享工具包展开先讲清它的包结构与导出方式、基于 Vitest 的测试工作流如何配置和运行再深入aggregateTokenUsage这个核心函数的源码实现与测试验证说明它如何在 Token 用量统计、步骤合并等真实链路中被复用。读完后你将掌握如何在这个 monorepo 中定位、调用并扩展共享工具函数并理解其测试规范与边界行为。包定位与导出结构refly/utils是 Refly 项目monorepo中的核心工具包官方描述为 Refly Code Utils - A collection of utility functions for the Refly project见 packages/utils/README.md。它的源码位于packages/utils/src/下覆盖 ID 生成、模型与 Token 相关逻辑、文件 ID 处理、解析、校验等多个子模块。从 packages/utils/package.json 可以看到几个关键设计包的main与types直接指向src/index.ts即该包以源码形式被 workspace 内其他包消费而非经过打包产物exports中额外暴露了./token子路径./src/token.ts允许按需引入 Token 计数相关能力依赖项中包含kitokenToken 分词器、markdown-it、cheerio、yjs、prosemirror-model等说明该工具包同时服务于前端的富文本/协作编辑场景和后端的内容处理场景通过workspace:*协议依赖refly/common-types与refly/openapi-schema两个内部包类型定义如TokenUsageItem来源于 OpenAPI schema 生成包。一个值得注意的导出细节在 packages/utils/src/index.ts 末尾// Note: token.ts uses tiktoken (WASM) which is not compatible with frontend bundlers // Backend should import directly: import { countToken } from refly/utils/src/token也就是说token.ts基于 kitoken 的 Token 计数模块被刻意排除在src/index.ts的主导出之外因为其中涉及的 WASM 分词器与前端打包器不兼容后端如需使用应从refly/utils/token子路径直接导入。这个约束在使用该包时必须遵守否则会引入前端构建问题。测试工作流Vitest 配置与运行方式README 明确说明该包使用 Vitest 做单元测试测试文件与被测源码放在一起约定位置为src/**/*.test.ts。测试命令README 给出的原始命令如下# Run tests in watch mode pnpm test # Run tests once pnpm test:run # Run tests with coverage (requires vitest/coverage-v8) pnpm test:coverage对照 packages/utils/package.json 中当前的scripts实际映射需要做一个准确性校准scripts: { clean: rimraf dist rimraf tsconfig.tsbuildinfo, test: vitest run, test:run: vitest run, test:coverage: vitest run --coverage }可以看到当前版本中pnpm test与pnpm test:run都映射到vitest run一次性运行而非 README 描述的 watch 模式pnpm test:coverage则附带--coverage参数生成覆盖率报告对应 devDependencies 中的vitest/coverage-v8。如果你需要 watch 模式可以直接运行pnpm vitest。以 package.json 的实际配置为准即可。配置文件测试行为由 packages/utils/vitest.config.ts 定义非常精简import { defineConfig } from vitest/config; export default defineConfig({ test: { globals: true, // describe/it/expect 全局可用 environment: node, // Node 环境运行 include: [**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}], exclude: [node_modules, dist, .turbo], }, });要点globals: true让测试文件可以直接使用describe/it/expect尽管仓库中部分测试文件仍显式import { describe, it, expect } from vitestinclude模式同时匹配*.test.ts与*.spec.ts两种命名。现有测试文件从源码目录可以确认目前存在以下与源码同目录放置的测试文件印证了 tests alongside source code 的约定packages/utils/src/models.test.ts ——aggregateTokenUsagepackages/utils/src/file-id.test.ts —— 文件 ID 校验/提取/替换packages/utils/src/step.test.ts —— 步骤排序与合并packages/utils/src/time.test.ts —— 时间处理packages/utils/src/validator.test.ts —— 参数校验packages/utils/src/query-processor.test.ts —— 查询处理aggregateTokenUsageToken 用量聚合核心函数README 中重点文档化的函数是aggregateTokenUsage它的作用是把分散的 Token 用量记录按模型名聚合对每个唯一模型求和输入/输出 Token。这是理解该包技术深度的最佳入口因为它同时涉及类型设计、空值防御与跨包复用。函数签名与用法README 给出的原始示例完整保留如下import { aggregateTokenUsage } from refly/utils; const usage [ { modelName: gpt-4, modelProvider: openai, inputTokens: 100, outputTokens: 50 }, { modelName: gpt-4, modelProvider: openai, inputTokens: 200, outputTokens: 75 } ]; const aggregated aggregateTokenUsage(usage); // Result: [{ modelName: gpt-4, modelProvider: openai, inputTokens: 300, outputTokens: 125 }]参数为usageItems: TokenUsageItem[]该类型来自refly/openapi-schema返回聚合后的TokenUsageItem[]。源码实现细节实际实现位于 packages/utils/src/models.ts比 README 描述更丰富的行为有四点分组键仅为modelName聚合以item.modelName为 key即使两条记录标注了不同的modelProvider只要模型名相同就会合并元数据取首次出现值创建聚合条目时用pick(item, [modelProvider, modelName, modelLabel, providerItemId])只固化第一次出现的元数据后续同名记录不会覆盖累加四个 Token 字段而非两个除inputTokens/outputTokens外还累加cacheReadTokens与cacheWriteTokens缓存读写 Token用于 prompt cache 计费场景且对缺失的缓存字段做?? 0兜底空值防御循环开头if (!item) continue;会静默跳过数组中的null/undefined项。核心循环可概括为export const aggregateTokenUsage (usageItems: TokenUsageItem[]): TokenUsageItem[] { const aggregatedUsage: Recordstring, TokenUsageItem {}; for (const item of usageItems) { if (!item) continue; const key item.modelName; if (!aggregatedUsage[key]) { aggregatedUsage[key] { ...pick(item, [modelProvider, modelName, modelLabel, providerItemId]), inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, }; } aggregatedUsage[key].inputTokens item.inputTokens; aggregatedUsage[key].outputTokens item.outputTokens; aggregatedUsage[key].cacheReadTokens item.cacheReadTokens ?? 0; aggregatedUsage[key].cacheWriteTokens item.cacheWriteTokens ?? 0; } // ... 按首次出现顺序转回数组 };输出顺序由Object.entries保持插入序即模型按首次出现排序而非字母序。测试用例验证的行为边界packages/utils/src/models.test.ts 中的用例对上述实现行为做了系统性验证覆盖了这些边界场景场景验证的期望行为空数组 / 含null、undefined项返回空数组无效项被跳过单条记录原样输出字段完整同名模型多条记录累加正确如 100200150450不同模型混合按modelName分组各组独立累加零 Token 记录正常参与累加不影响结果同名但不同 providermodelProvider保留首次出现值大数值 / 小数 / 负数直接按数值运算累加如 100.5200.75301.25空modelName也被视为一个合法分组键参与聚合输出顺序按模型名首次出现顺序排列这些测试用描述性命名should preserve model provider from first occurrence精确表达期望行为正是 README 开发规范中 Use descriptive test names that explain the expected behavior 的落地示例。仓库内的真实复用链路aggregateTokenUsage不是孤立的工具函数它在多条业务链路中被复用步骤合并packages/utils/src/step.ts 中的mergeStepsByName在合并新旧两组ActionStep时对tokenUsage字段执行aggregateTokenUsage([...lhs, ...rhs])而不是直接覆盖——注释明确写道 aggregate tokenUsage rather than overwriting避免 SSE 流式更新时用陈旧的小数据覆盖累计的大数据画布动作结果更新packages/ai-workspace-common/src/hooks/canvas/use-invoke-action.ts在每次动作执行后将新产生的用量追加到已有tokenUsage上并重新聚合use-update-action-result.ts则对全部步骤的用量做一次扁平化聚合API 结果层apps/api/src/utils/result.ts 在处理执行结果时同样调用它归并用量记录。从源码结构看这种多处消费、统一收敛到共享包的模式正是该 monorepo 对工具函数治理方式的体现任何 Token 用量归并逻辑都集中在一处实现和测试。开发规范如何向该包添加新函数README 的 Development 章节给出了标准流程结合仓库现状补充如下在src/下的合适文件中创建函数按主题归类例如模型相关放models.ts文件 ID 相关放file-id.ts从src/index.ts导出——注意src/index.ts是按模块export * from ./xxx组织的一行一个模块新模块需按此风格追加。同时注意token.ts这类前端不兼容模块应刻意不加入主导出并保留注释说明参考 packages/utils/src/index.ts 末尾的既有做法在同级目录添加.test.ts单元测试遵循现有测试模式更新 packages/utils/README.md 补充文档。一个值得参考的文档化范例是 packages/utils/src/file-id.ts它在文件头集中列出了所有消费方packages/agent-tools/src/builtin/index.ts、apps/api/src/modules/tool/utils/schema-utils.ts等及各自调用的函数名并明确新增文件 ID 处理逻辑时优先复用这些共享工具这为跨包共享工具函数树立了清晰的契约文档风格。测试规范README 的 Testing Guidelines 要求覆盖所有边界情况与错误条件空输入、非法输入、混合有效/无效项等使用描述性测试名说明期望行为同时测试有效与非法输入追求 100% 函数覆盖率配合pnpm test:coverage验证;遵循既有测试文件的模式与结构。对照models.test.ts中 15 个用例的组织方式每个it聚焦一个边界这些规范在包内已有严格一致的执行样板。小结refly/utils以源码直出 按需子路径导出的方式服务于前后端两端主入口src/index.ts覆盖通用能力token子路径隔离了与前端打包器不兼容的 WASM 分词器。其测试工作流由精简的 Vitest 配置驱动globals: true、node 环境、**/*.test.ts与源码同目录当前pnpm test实际执行一次性运行。核心函数aggregateTokenUsage则以按模型名分组、元数据取首次出现、四 Token 字段累加、空值防御的确定行为支撑起步骤合并、画布动作结果更新与 API 结果处理等多条业务链路并通过 15 个边界用例锁定了其语义。理解这个包的最佳路径就是从它文档化的每个函数出发顺着src/index.ts的导出表与.test.ts用例回到源码逐一印证。【免费下载链接】reflyThe first open-source agent skills builder. Define skills by vibe workflow, run on Claude Code, Cursor, Codex more. Build Clawdbot · APIs for Lovable · Bots for Slack Lark/Feishu · Skills are infrastructure, not prompts.项目地址: https://gitcode.com/GitHub_Trending/re/refly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
