职责概述

解决的问题:CC 的一些核心能力需要高性能——语法高亮要快、文件搜索要快、终端布局计算要快。以前这些用 Rust/C++ 原生模块实现,但原生二进制导致跨平台分发困难。这一层把所有原生模块用纯 TypeScript 重写,消除了原生依赖,同时保持性能可接受。

应用场景:① 代码差异的彩色渲染(新增行绿色、删除行红色)② 输入文件路径时的模糊搜索补全 ③ 终端界面的 Flexbox 布局计算 ④ 容器环境中的 MITM 代理设置 ⑤ 远程开发时本地 IDE 驱动容器内的 CC。

一句话理解:就像 Electron 之于桌面 App——用 Web 技术替代原生开发,牺牲一点极致性能,换来跨平台和可维护性。

架构设计

UI 渲染层
yoga-layout — Flexbox 布局引擎
color-diff — 语法高亮与 diff 渲染
搜索引擎层
file-index — 模糊文件搜索
网络基础设施层
upstreamproxy — MITM 代理中继
bridge — 远程会话桥接 API

Native 模块架构图

yoga-layout
~2580 行纯 TS
Flexbox 引擎
flex-direction / grow-shrink
justify / align / wrap
多级布局缓存
color-diff
~500+ 行纯 TS
差异渲染
highlight.js 语法着色
word-diff 算法
ANSI TrueColor 输出
file-index
~300+ 行纯 TS
模糊搜索
nucleo 风格评分
边界/驼峰/连续加分
异步分块构建
upstreamproxy
relay + config
网络代理
CONNECT→WebSocket
CA 证书链拼接
NO_PROXY 白名单
bridge/
20+ 文件
远程会话
REST API 客户端 | JWT 认证 | 权限回调 | 远程 Bridge 核心 | 会话运行器 | 可信设备

核心数据流

1. Yoga Layout 计算流程

calculateLayout()
根节点入口,重置计数器,递增 generation
缓存检查
单条目 (_hasL) → 多条目 (_cIn) → measure 缓存 (_hasM) 三级缓存查找
computeFlexBasis()
计算每个子节点的 flex-basis,受 flex-grow/shrink/min/max 约束
resolveFlexibleLengths()
多轮迭代分配剩余空间,冻结违反 min/max 的子节点
定位子节点
justify-content + align-items + auto margin + 相对定位

2. Upstream Proxy 初始化流程

环境检测
CLAUDE_CODE_REMOTE=1 && CCR_UPSTREAM_PROXY_ENABLED=1
读取 Session Token
/run/ccr/session_token 读取,设置 prctl(PR_SET_DUMPABLE, 0)
CA 证书拼接
下载 upstreamproxy CA,与系统 bundle 合并,设置 SSL_CERT_FILE
启动 CONNECT 中继
startUpstreamProxyRelay() — 本地 CONNECT→WebSocket 代理
暴露环境变量
HTTPS_PROXY + NO_PROXY(排除 Anthropic API、GitHub、npm 等域名)

关键类型与接口

FileIndex API (native-ts/file-index/index.ts)

// native-ts/file-index/index.ts — 核心接口
export type SearchResult = {
  path: string
  score: number  // 越低越好,最佳匹配 = 0.0
}

export class FileIndex {
  loadFromFileList(fileList: string[]): void
  loadFromFileListAsync(fileList: string[]): {
    queryable: Promise<void>  // 首个 chunk 就绪即可搜索
    done: Promise<void>       // 全部索引构建完成
  }
  search(query: string, limit: number): SearchResult[]
}

Yoga Node 核心 (native-ts/yoga-layout/index.ts)

// native-ts/yoga-layout/index.ts:403-466 — Node 类(核心字段)
export class Node {
  style: Style
  layout: Layout
  children: Node[]
  measureFunc: MeasureFunction | null
  isDirty_: boolean
  // 多级布局缓存
  _hasL: boolean         // 单条目 layout 缓存
  _cIn: Float64Array     // 多条目缓存输入(4 slot × 8 float)
  _cOut: Float64Array    // 多条目缓存输出(4 slot × 2 float)
  _fbGen: number         // flexBasis 缓存的 generation
  // 快速跳过标志
  _hasAutoMargin: boolean
  _hasPosition: boolean
  _hasPadding: boolean
  _hasBorder: boolean
  _hasMargin: boolean

  calculateLayout(ownerWidth?: number, ownerHeight?: number): void
}

Color Diff 接口 (native-ts/color-diff/index.ts)

// native-ts/color-diff/index.ts:52-69
export type Hunk = {
  oldStart: number; oldLines: number
  newStart: number; newLines: number
  lines: string[]
}
export type NativeModule = {
  ColorDiff: typeof ColorDiff
  ColorFile: typeof ColorFile
  getSyntaxTheme: (themeName: string) => SyntaxTheme
}

Bridge API (bridge/bridgeApi.ts)

// bridge/bridgeApi.ts:68 — 工厂函数签名
export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient
// deps 包含: baseUrl, getAccessToken, runnerVersion,
//   onAuth401 (OAuth 刷新), getTrustedDeviceToken

设计模式与亮点

1. 多级布局缓存(Yoga)

Yoga 实现了三层缓存策略:单条目 _hasL(layout pass 结果)、四条目 _cIn/_cOut(覆盖 scroll 场景下同一节点看到多组不同输入的情况)、flexBasis _fbGen(基于 generation 的跨计算缓存)。CPU Profile 显示,1000 节点基准测试中 ~67% 的 resolveEdges4Into 调用操作全零边数组——快速路径通过 _hasPadding/_hasBorder/_hasMargin 标志单分支跳过。

2. 时间分块异步索引(FileIndex)

loadFromFileListAsync() 不按数量分块而是按时间分块(CHUNK_MS = 4ms):慢速机器自动获得更小的分块,保持 UI 响应性。首个 chunk 就绪即返回 queryable promise——270K 文件索引中约 5-10ms 即可开始搜索部分结果。

3. 失败开放设计(UpstreamProxy)

initUpstreamProxy() 的每一步都失败开放:token 文件缺失、CA 下载失败、relay 启动失败——任何错误仅记录日志并返回 { enabled: false }。一个破损的代理配置绝不会中断正常工作会话。Token 文件在 relay 确认启动后才 unlink,确保 supervisor 重启可重试。

4. Generation-based 缓存新鲜度

Yoga 和 FileIndex 都使用 generation 标记缓存有效性。Yoga 的 _generation 在每次 calculateLayout() 时递增,允许同一 generation 内的缓存命中(即使节点仍标记 dirty——virtual scroll 的新挂载项)。跨 generation 的 dirty 节点缓存被视为陈旧。这种设计将 1593 节点新鲜挂载的 105K 次 layoutNode 访问降至约 10K 次。

5. Highlight.js 惰性加载(Color-Diff)

Color-diff 通过闭包惰性加载 highlight.js(190+ 语言语法,~50MB),避免模块评估时即加载。这是解决 Windows CI 上 beforeEach/afterEach 超时问题的关键优化——测试文件通过间接依赖引入 color-diff 时不再付出启动成本。

开发者实践指南

如何扩展 Yoga 布局能力

  1. native-ts/yoga-layout/enums.ts 中添加新的枚举值
  2. defaultStyle() 中添加默认值
  3. Node 类中添加对应的 setter/getter
  4. layoutNode() 的对应 STEP 中添加处理逻辑
  5. 更新缓存比较逻辑——确保新维度参与缓存键匹配
Yoga 缓存调试: 布局错误通常源于缓存陈旧。可通过 getYogaCounters() 检查 visited/measured/cacheHits/live 计数。如果 cacheHits 远低于预期,检查 _hasL/_cN 是否在 dirty 时被正确清除。

如何添加新的 FileIndex 评分规则

  1. 修改 native-ts/file-index/index.ts 中的评分常量(SCORE_MATCH, BONUS_BOUNDARY 等)
  2. 在搜索函数中添加新的 bonus/penalty 计算
  3. 注意 posBufInt32Array(MAX_QUERY_LEN)——查询长度上限为 64 字符

如何调试 UpstreamProxy

  1. 设置 CLAUDE_CODE_REMOTE=1 + CCR_UPSTREAM_PROXY_ENABLED=1
  2. 检查 /run/ccr/session_token 是否存在且可读
  3. 查看 relay 日志——所有步骤都有 logForDebugging() 输出
  4. 验证 NO_PROXY 列表是否覆盖了不应代理的目标(Anthropic API、GitHub 等)

架构师决策指南

纯 TS 重写 vs 原生模块的决策

三个 native-ts 模块都经历了从 Rust/C++ NAPI 到纯 TypeScript 的迁移。这一决策的核心权衡:

Bridge 模块的职责边界

bridge/ 包含 20+ 文件,覆盖了 REST API 客户端、JWT 工具、权限回调、远程会话运行、调试工具等。关键的设计决策是将 bridge 定位为 API 层而非业务逻辑层——它不包含任何 Claude Code 的核心逻辑,仅负责将本地操作转发到远程执行环境。这使得 bridge 可以被 IDE 插件、CCR 容器、Desktop App 等不同场景复用。

NO_PROXY 的维护负担

upstreamproxy.ts 中的 NO_PROXY_LIST 是硬编码的域名列表。每次添加新的第三方依赖(如新的包管理器仓库),都需要考虑是否加入排除列表。更健壮的方案是通过配置文件管理,但这与 CCR 容器的无状态设计冲突——当前硬编码是合理的折衷。

可视化处理拓扑图

Phase 1 — Color-Diff 差异渲染管线

从终端颜色检测到语法高亮再到 ANSI 输出的完整渲染管线,支持 truecolor/256色/ANSI 三种终端模式。

ColorDiff.render()color-diff/index.ts:860
终端颜色能力检测detectColorMode() → truecolor / 256色 / ANSIcolor-diff/index.ts:95-99
TrueColor38;2;r;g;b
256 色38;5;index
ANSI3/4 位码
语法主题构建buildTheme() — 前景/背景/装饰色完整映射color-diff/index.ts:282-362
语言检测detectLanguage() — 扩展名 + 首行内容推断color-diff/index.ts:422-451
语法高亮 + word-diff 叠加highlightLine() → hljs 语法着色 → wordDiffStrings() 字符级差异color-diff/index.ts:504-636
差异范围渲染applyBackground() — 差异背景叠加到语法高亮之上color-diff/index.ts:768-817
ANSI 转义序列输出colorToEscape() → intoLines() — 行号+标记+背景+语法color-diff/index.ts:129-162

Phase 2 — File-Index 模糊搜索与 Yoga 布局计算

FileIndexfile-index/index.ts
异步分 chunk 构建CHUNK_SIZE=8192, CHUNK_MS=4msindex.ts:95-133
位图预计算indexPath() — 26位 a-z 字母位图index.ts:156-167
位图快速过滤AND 运算 O(1) 拒绝 ~89% 路径
模糊匹配评分边界加分 + camelCase 加分index.ts:173-290
SearchResult[]score 越低越好
calculateLayout()yoga-layout/index.ts:927
三级缓存查找_hasL → _cIn → _hasM
缓存命中直接返回
缓存未命中重新计算
computeFlexBasis()flex-grow/shrink/min/max 约束index.ts:2059-2172
resolveFlexibleLengths()多轮迭代冻结违反 min/max 子节点index.ts:2182-2282
定位子节点justify + align + auto margin
cacheWrite() 写入缓存style快照 + 父约束哈希

Phase 3 — UpstreamProxy 代理中继

CCR 容器环境下的透明代理建立流程,每步失败开放。

initUpstreamProxy()upstreamproxy.ts:79-153
三前置条件检查CLAUDE_CODE_REMOTE → CCR_UPSTREAM_PROXY_ENABLED → session token 文件
不满足→ { enabled: false }
全部通过继续初始化
CA 证书拼接下载 upstreamproxy CA + 系统 bundle → SSL_CERT_FILE
PR_SET_DUMPABLE=0libc FFI 阻止 ptrace — 安全加固upstreamproxy.ts:112
CONNECT→WebSocket 中继startUpstreamProxyRelay() — 本地 TCP 监听relay.ts:155-174
帧协议双向转发encodeChunk / decodeChunk — 轻量帧封装relay.ts:66-103
环境变量注入HTTPS_PROXY + NO_PROXY + NODE_EXTRA_CA_CERTSupstreamproxy.ts:160-199
拓扑总结:Color-Diff 构建了从终端能力检测到 ANSI 输出的完整渲染管线;File-Index 通过位图预过滤将搜索空间降低一个数量级;Yoga 用三级缓存将布局计算从 O(全树) 降至 O(脏路径);UpstreamProxy 以失败开放设计确保透明代理不中断工作流。

核心处理流程详解

Native 能力层的五个模块各自独立,但共享一个核心设计理念:从原生二进制依赖迁移到纯 TypeScript 实现,消除跨平台编译和 NAPI 绑定的复杂性。以下以 color-diff 差异渲染file-index 模糊搜索yoga-layout 布局计算upstreamproxy 代理中继 四条核心流程展开。

1. Color-Diff: 语法检测与主题构建
ColorDiff.render()(L860-932)首先调用 detectColorMode()(L95-99)检测终端颜色能力(truecolor/256色/ANSI),然后调用 buildTheme()(L282-362)根据主题名构建包含前景/背景/装饰色的完整主题映射。最后 detectLanguage()(L422-451)根据文件扩展名和首行内容推断语法语言 — color-diff/index.ts:860-868
2. Color-Diff: 语法高亮 + word-diff 叠加
每个 hunk 行经 highlightLine()(L504-538)处理:先通过 highlight.js 语法高亮(使用 hljs 10.x/11.x 兼容的 scope/kind 字段),然后对修改行执行 wordDiffStrings()(L604-636)的字符级差异计算。差异范围通过 applyBackground()(L768-817)叠加到语法高亮之上 — color-diff/index.ts:504-636
3. Color-Diff: ANSI 转义序列输出
最终通过 intoLines()(L819-826)将 Block 数组转换为 ANSI 转义序列字符串。colorToEscape()(L129-145)根据颜色模式(truecolor 用 38;2;r;g;b,256 色用 38;5;index,ANSI 用 3/4 位码)生成终端转义序列。每行包含行号 + 标记(+/-/ )+ 背景 + 语法高亮的完整信息 — color-diff/index.ts:129-162
4. File-Index: 异步分 chunk 构建
FileIndexbuildAsync()(L95-133)将文件列表按 ~8-12k 条分 chunk,每处理一个 chunk 后通过 yieldToEventLoop()(L325-327)让出事件循环,避免阻塞 UI。indexPath()(L156-167)为每个路径预计算:小写形式 + a-z 字母位图 + 路径长度。位图提供了 O(1) 的快速拒绝能力 — file-index/index.ts:95-167
5. File-Index: 模糊搜索与评分
search()(L173-290)先通过字母位图快速过滤(O(1) 拒绝不含查询字符的路径),然后对幸存路径执行模糊匹配。智能大小写:全小写查询不区分大小写,含大写则区分。匹配评分考虑路径边界加分(scoreBonusAt L297-303)和 camelCase 加分 — file-index/index.ts:173-303
6. Yoga-Layout: Flexbox 布局计算
calculateLayout()(L927-963)是布局入口。它调用 layoutNode()(L1058-1902)递归计算每个节点的尺寸和位置。热路径上的多级缓存:cacheWrite()(L969-1012)比较当前 style + 约束的哈希,命中时直接返回缓存布局。computeFlexBasis()(L2059-2172)和 resolveFlexibleLengths()(L2182-2282)处理弹性布局核心算法 — yoga-layout/index.ts:927-963, 1058-1902
7. UpstreamProxy: CCR 环境检测
initUpstreamProxy()(L79-153)首先检查三个前置条件:CLAUDE_CODE_REMOTE(必须在 CCR 容器中)→ CCR_UPSTREAM_PROXY_ENABLED(服务端通过 StartupContext 注入)→ session token 文件存在。通过后下载 CA 证书包并设置 PR_SET_DUMPABLE=0(L112, 通过 libc FFI 阻止 ptrace)— upstreamproxy.ts:79-153
8. UpstreamProxy: CONNECT→WebSocket 中继
startUpstreamProxyRelay()(relay.ts:155-174)启动本地 TCP 监听,每个连接先累积 HTTP CONNECT 请求(handleData L295-342),解析目标地址后通过 openTunnel()(relay.ts:344-428)建立到 Anthropic 的 WebSocket 通道。后续数据通过 encodeChunk/decodeChunk(relay.ts:66-103)的轻量帧协议双向转发 — relay.ts:155-428
这四个模块共同展示了纯 TypeScript 重写原生模块的设计哲学:color-diff 用 highlight.js 替代 syntect + bat,file-index 用位图预计算替代 nucleo 的 FFI,yoga-layout 用 ~2600 行纯 TS 实现了 Facebook Yoga 的完整 Flexbox 算法。核心收益是消除了跨平台二进制分发(N-API、WASM)的复杂性,代价是 CPU 密集场景(大文件 diff、万级文件搜索)的性能可能略低于原生实现。

设计精华

1. 位图快速拒绝——O(1) 路径过滤

FileIndex.indexPath()(L156-167)为每个文件路径预计算一个 26 位整数位图,每一位对应 a-z 中的一个字母。搜索时先对查询字符串做位图 AND 操作,如果结果的位图是查询位图的子集,才进入昂贵的模糊匹配。这一步可以拒绝约 89% 的路径(对于常见查询如"test"),将实际需要评分的路径数量降低一个数量级。

a-z 字母位图
26 位 Int32 位图,每位表示路径中是否包含对应字母。O(1) 的 AND 运算替代 O(n) 的字符串扫描
// file-index/index.ts:156-167
private indexPath(i: number): void {
  const lp = this.paths[i]!.toLowerCase()
  this.lowerPaths[i] = lp
  this.pathLens[i] = len
  let bits = 0
  for (let j = 0; j < len; j++) {
    const c = lp.charCodeAt(j)
    if (c >= 97 && c <= 122) bits |= 1 << (c - 97)  // a-z 映射到 bit 0-25
  }
  this.charBits[i] = bits
}
// 搜索时:if ((charBits[i] & needleBits) !== needleBits) → 跳过
位图预过滤是信息检索中 Bloom filter 思想的简化应用。它没有 false negative(被拒绝的路径确实不含所需字符),但有 false positive(通过过滤的路径不一定能模糊匹配)。在这个场景下 false positive 的代价只是一次评分计算,而 true negative 带来的节省是巨大的——特别是对于 10 万级文件的 monorepo。

2. Yoga 多级布局缓存——热路径优化

yoga-layout/index.ts 实现了多级缓存策略:calculateLayout() 在 L932-934 重置访问计数器,layoutNode() 通过 cacheWrite()(L969-1012)比较当前 style 快照 + 父约束是否匹配缓存。命中时跳过整个布局递归,直接返回缓存的 Layout 对象。sameFloat()(L113-115)使用 a === b || (isNaN(a) && isNaN(b)) 做 NaN 安全的浮点比较。

多级布局缓存
比较 style + 约束的哈希决定是否命中缓存,避免每次渲染都递归整棵布局树
// yoga-layout/index.ts:969-1012
function cacheWrite(node: Node, performLayout: boolean, ...): boolean {
  // 比较 style 快照 + 父约束
  if (sameFloat(cached.ownerWidth, ownerWidth) &&
      sameFloat(cached.ownerHeight, ownerHeight) &&
      // ... 所有 style 字段一致
     ) {
    commitCacheOutputs(node, performLayout)  // 命中:直接返回
    return true
  }
  // 未命中:更新缓存并重新计算
}
终端 UI 的布局计算有一个特殊性质:大部分情况下只有少量节点的 style 或约束发生变化(如用户输入导致文本宽度变化)。多级缓存利用了这一特性——未变化的子树直接返回缓存结果,只重新计算脏路径。这使得每次按键的布局计算从 O(全树) 降低到 O(脏路径)。

3. 异步分 chunk 构建——不阻塞事件循环

FileIndex.buildAsync()(L95-133)将文件列表索引化过程拆分为多个 chunk,每处理约 8-12k 条路径后调用 yieldToEventLoop() 让出控制权。这使得即使有 10 万级文件,索引构建也不会阻塞 UI 渲染和用户输入。

事件循环让步模式
每处理一个 chunk 后 await 一个微任务,确保 UI 渲染和用户输入可以穿插执行
// file-index/index.ts:95-133
private async buildAsync(
  fileList: string[], markQueryable: () => void,
): Promise {
  const CHUNK_SIZE = 8192
  for (let i = 0; i < fileList.length; i += CHUNK_SIZE) {
    const end = Math.min(i + CHUNK_SIZE, fileList.length)
    const chunk = fileList.slice(i, end)
    this.buildIndex(chunk)
    if (i === 0) markQueryable()  // 首个 chunk 后即可查询
    await yieldToEventLoop()       // 让出事件循环
  }
}
这种模式在 Node.js 单线程环境中至关重要。yieldToEventLoop() 的实现通常是一个 Promise.resolve().then() 的微任务——它比 setTimeout(fn, 0) 更轻量,但仍然允许渲染和 I/O 回调在 chunk 之间执行。首个 chunk 后调用 markQueryable() 实现了"渐进可用"——用户不需要等待全部文件索引完成就能开始搜索。

4. CONNECT→WebSocket 中继——代理透明化

upstreamproxy 在 CCR 容器中建立透明代理:本地工具(Bash/MCP/LSP)通过 HTTP_PROXY=http://127.0.0.1:{port} 发出请求,proxy 拦截 CONNECT 请求,通过 WebSocket 隧道转发到 Anthropic 的 egress 网关。工具代码无需知道代理的存在——getUpstreamProxyEnv()(upstreamproxy.ts:160-199)将代理环境变量注入到每个子进程。

透明代理注入
通过环境变量将代理配置注入子进程,工具代码零修改即可在 CCR 容器中工作
// upstreamproxy.ts:160-199
export function getUpstreamProxyEnv(): Record {
  if (!state.enabled) return {}
  return {
    HTTP_PROXY:  `http://127.0.0.1:${state.port}`,
    HTTPS_PROXY: `http://127.0.0.1:${state.port}`,
    NO_PROXY: NO_PROXY_LIST.join(','),  // 排除 Anthropic 内部域名
    NODE_EXTRA_CA_CERTS: state.caBundlePath,
  }
}
透明代理的设计精髓在于"环境变量即配置"——Node.js 的 HTTP_PROXY / HTTPS_PROXY / NODE_EXTRA_CA_CERTS 是事实标准环境变量,几乎所有 HTTP 库都自动尊重。这意味着 Bash/MCP/LSP/hooks 等子进程无需任何代码修改就能通过代理通信。NO_PROXY_LIST 硬编码排除 Anthropic 内部域名,避免循环代理。

Agent 实践借鉴 — 客服 Agent 平台能力设计

一、场景映射:客服 agent 的跨平台部署挑战

客服 agent 可能部署在三种截然不同的环境:云端 Docker 容器(有完整的语音识别 API、大模型服务、弹性扩缩容)、本地服务器(GPU 有限、网络可能受限、但有局域网 CRM 系统直连)、门店智能终端(有摄像头可扫条码、有本地数据库、但没有云端语音 API)。CC 用纯 TS 重写消除原生依赖以实现跨平台一致性,客服 agent 也面临同样的问题——同一个 agent 在不同环境下必须有统一的表现。

二、借鉴 CC + 客服改造

实践 1:平台能力检测 + 优雅降级(借鉴 CC 的 UpstreamProxy 环境检测)

CC 的 initUpstreamProxy() 检测 CCR 环境变量,每一步都失败开放。客服 agent 启动时也需要检测所在环境的能力——没有语音 API 就纯文字模式,没有摄像头就禁用扫码功能,没有本地数据库就用远程 API。

// 借鉴 upstreamproxy.ts:79-153 的环境检测 + 失败开放模式
interface PlatformCapabilities {
  canUseVoice: boolean       // 语音识别/合成
  canUseCamera: boolean      // 摄像头扫条码
  canUseLocalDB: boolean     // 本地数据库(门店场景)
  canUseGPU: boolean         // 本地推理能力
  canUseInternet: boolean    // 外网访问(门店可能断网)
}

class PlatformDetector {
  private capabilities: PlatformCapabilities

  constructor() {
    this.capabilities = {
      // 逐项检测,每项失败不影响其他项(失败开放)
      canUseVoice: this.checkVoiceAPI(),
      canUseCamera: this.checkCameraAccess(),
      canUseLocalDB: this.checkLocalDB(),
      canUseGPU: this.checkGPU(),
      canUseInternet: this.checkInternet(),
    }
    console.log('平台能力检测结果:', this.capabilities)
  }

  private checkVoiceAPI(): boolean {
    try {
      // 云端:检测 ASR 服务是否可达
      if (process.env.ASR_ENDPOINT) return true
      // 本地:检测 Whisper 模型是否可用
      return fs.existsSync('/usr/local/models/whisper')
    } catch { return false }  // 失败开放:不可用就不启用
  }

  private checkCamera(): boolean {
    try {
      // 门店终端:检测 /dev/video* 设备
      const devices = fs.readdirSync('/dev').filter(f => f.startsWith('video'))
      return devices.length > 0
    } catch { return false }
  }

  // 获取能力报告,供上层判断
  getCapabilities(): Readonly<PlatformCapabilities> {
    return this.capabilities
  }
}

// 使用:agent 启动时检测一次,运行时按能力路由
const platform = new PlatformDetector()
const caps = platform.getCapabilities()

// 没有语音 API 就关闭语音入口
if (!caps.canUseVoice) {
  channelRegistry.disable('phone')
  console.warn('语音 API 不可用,已关闭电话渠道')
}

实践 2:统一平台接口(借鉴 CC 的 Shell Provider 抽象)

CC 用 ShellProvider 接口统一了 Bash 和 PowerShell。客服 agent 的平台差异更大——语音、图像、本地存储都需要统一接口,让上层业务代码不关心底层是云端 API 还是本地模块。

// 借鉴 shellProvider.ts 的 Provider 抽象模式
interface SpeechService {
  recognize(audioBuffer: Buffer): Promise<string>    // 语音转文本
  synthesize(text: string): Promise<Buffer>           // 文本转语音
}

interface VisionService {
  scanBarcode(imageBuffer: Buffer): Promise<string>   // 扫条码
  ocr(imageBuffer: Buffer): Promise<string>           // 图片文字识别
}

interface StorageService {
  queryProduct(barcode: string): Promise<Product | null>
  saveInteraction(record: InteractionRecord): Promise<void>
}

// 云端实现:调用远程 API
class CloudSpeechService implements SpeechService {
  async recognize(audioBuffer: Buffer): Promise<string> {
    const resp = await fetch(`${process.env.ASR_ENDPOINT}/recognize`, {
      method: 'POST', body: audioBuffer,
    })
    return resp.json().then(r => r.text)
  }
}

// 本地实现:调用本地 Whisper 模型
class LocalSpeechService implements SpeechService {
  async recognize(audioBuffer: Buffer): Promise<string> {
    const whisper = require('whisper.cpp')  // 本地 NAPI 模块
    return whisper.transcribe(audioBuffer)
  }
}

// 工厂函数:根据平台能力自动选择实现
function createSpeechService(caps: PlatformCapabilities): SpeechService {
  if (caps.canUseInternet) return new CloudSpeechService()    // 云端优先
  if (caps.canUseGPU) return new LocalSpeechService()         // 本地降级
  return new TextOnlyFallback()                                // 纯文字兜底
}

实践 3:知识库搜索的预过滤(借鉴 CC 的位图快速拒绝思路)

CC 的 FileIndex 用 26 位字母位图预过滤 89% 的路径。客服场景的知识库搜索同样需要预过滤——10 万条 FAQ/商品条目中,先用轻量条件排除不可能匹配的,再做昂贵的语义搜索。

// 借鉴 file-index/index.ts:156-167 的位图预过滤思路
// (不需要抄位图算法本身,用标准搜索引擎的预过滤即可)

interface KnowledgeEntry {
  id: string
  category: string           // 'product' | 'faq' | 'policy' | 'troubleshoot'
  title: string
  keywords: string[]
  tags: string[]
}

class KnowledgeSearchEngine {
  private entries: KnowledgeEntry[] = []
  // 预过滤索引:按类别和标签建立倒排索引
  private categoryIndex = new Map<string, number[]>()
  private tagIndex = new Map<string, number[]>()

  // 步骤 1:轻量预过滤(类似位图快速拒绝)
  preFilter(query: SearchQuery): KnowledgeEntry[] {
    let candidateIndices: number[] | null = null

    // 按类别过滤
    if (query.category) {
      const indices = this.categoryIndex.get(query.category) ?? []
      candidateIndices = candidateIndices
        ? intersect(candidateIndices, indices)
        : indices
    }
    // 按标签过滤
    if (query.tags && query.tags.length > 0) {
      for (const tag of query.tags) {
        const indices = this.tagIndex.get(tag) ?? []
        candidateIndices = candidateIndices
          ? intersect(candidateIndices, indices)
          : indices
      }
    }
    // 如果没有过滤条件,返回全部
    const result = candidateIndices ?? this.entries.map((_, i) => i)
    return result.map(i => this.entries[i])
  }

  // 步骤 2:对幸存条目做昂贵的语义搜索
  async search(query: SearchQuery): Promise<KnowledgeEntry[]> {
    const candidates = this.preFilter(query)
    // 只对候选条目做向量相似度搜索(昂贵的操作)
    return this.semanticSearch(query.text, candidates, query.limit)
  }
}

// 使用示例:搜索退货相关的 FAQ
const results = await knowledgeBase.search({
  text: '买的东西坏了怎么退',
  category: 'faq',
  tags: ['refund', 'after_sales'],
  limit: 5,
})

三、落地清单

必须抄的
  • 平台能力检测 + 优雅降级——启动时检测环境能力,运行时按能力路由,每项检测独立失败开放
  • 减少原生依赖——客服 agent 尽量用纯 JS/HTTP 实现核心功能,避免 NAPI/Rust 依赖导致跨环境部署困难
  • 统一平台接口——语音/图像/存储用 Provider 接口抽象,云端和本地各自实现,上层不感知差异
不需要抄的
  • 位图算法的具体实现——太底层,客服场景用标准搜索引擎(Elasticsearch/Meilisearch)的预过滤即可
  • Yoga 布局引擎——客服面板用 Web CSS 渲染,不需要终端布局引擎
  • UpstreamProxy 的 CONNECT→WebSocket 中继——客服场景不需要 MITM 代理

四、常见坑

  1. 平台检测只在启动时做一次——门店终端的网络可能时断时续,启动时检测到有外网但运行中断网了。必须加定时重检或请求失败时重检逻辑。
  2. 降级模式的功能缺失导致客户困惑——语音 API 不可用时降级为纯文字,但电话渠道的客户根本无法打字。降级策略必须考虑渠道特性:电话断网应该转人工,而不是降级为文字。
  3. 知识库搜索跳过预过滤直接全量语义搜索——10 万条 FAQ 全量做向量相似度计算,单次搜索耗时可能超过 5 秒。先用类别/标签预过滤将候选集缩小到千级,再做语义搜索。
  4. 本地模型和云端模型的输出格式不一致——同一个 SpeechService 接口的两个实现返回格式不同(云端返回 JSON,本地返回纯文本)。必须统一接口契约并做集成测试。

代码索引

文件行数说明
native-ts/yoga-layout/index.ts~2580完整 Flexbox 布局引擎,含多级缓存、wrap、baseline
native-ts/yoga-layout/enums.ts~100FlexDirection/Align/Justify/Wrap/Edge 等枚举定义
native-ts/color-diff/index.ts~500+语法高亮 + word-diff + ANSI TrueColor 渲染
native-ts/file-index/index.ts~300+nucleo 风格模糊搜索,异步分 chunk 构建
upstreamproxy/upstreamproxy.ts~200+CCR upstream proxy 初始化:token/CA/relay/NO_PROXY
upstreamproxy/relay.ts~200CONNECT→WebSocket 本地代理中继
bridge/bridgeApi.ts~200+Bridge REST API 客户端工厂函数
bridge/bridgeConfig.ts~100Bridge 配置管理
bridge/sessionRunner.ts~200远程会话运行器
bridge/remoteBridgeCore.ts~200远程 Bridge 核心逻辑
bridge/jwtUtils.ts~100JWT 令牌工具
bridge/trustedDevice.ts~100可信设备令牌管理
bridge/types.ts~150Bridge 类型定义(BridgeConfig, PermissionResponse 等)
bridge/debugUtils.ts~100Bridge 调试工具
bridge/replBridgeHandle.ts~100REPL Bridge 句柄管理
bridge/replBridgeTransport.ts~100REPL Bridge 传输层