openbot
§ · Nested detail page

Connectors & Worker 深度解析

这一页只看 OpenBot 的一个局部模块,用真实源码行号解释它承担的契约、失败路径和与主架构的关系。

§ 00 · IMPORT CHAIN

导入链与契约

▸小 runner,强契约

worker connector runner 很小,但契约明确。ConnectorPersistence 类型从 worker/src/connector-runner.ts:6 定义,runConnector 入口是 worker/src/connector-runner.ts:17,discover 调用在 worker/src/connector-runner.ts:23;batch 路径由 worker/src/connector-runner.ts:27 选择,能力检查是 worker/src/connector-runner.ts:30,逐条 apply 位于 worker/src/connector-runner.ts:35,cursor 提交和成功记录分别是 worker/src/connector-runner.ts:36、worker/src/connector-runner.ts:40 显示 adapter.discover、persistence.cursor、batch/individual writes 和 recordRun 是它的全部接口。它不是业务 extractor,而是所有 knowledge source 同步的事务外壳。

src/worker/src/connector-runner.ts · L1-45TypeScript
1import type {
2  ConnectorAdapter,
3  ConnectorChange,
4} from "../../server/src/connectors/contract";
5
6export type ConnectorPersistence = {
7  cursor: () => Promise<string | null>;
8  apply?: (change: ConnectorChange) => Promise<void>;
9  commitCursor?: (cursor: string) => Promise<void>;
10  persistBatch?: (
11    changes: ConnectorChange[],
12    cursor: string | null,
13  ) => Promise<void>;
14  recordRun?: (status: "succeeded" | "failed") => Promise<void>;
15};
16
17export async function runConnector(
18  adapter: ConnectorAdapter,
19  persistence: ConnectorPersistence,
20  mode: "sync" | "reconcile" = "sync",
21) {
22  try {
23    const discovered = await adapter.discover({
24      cursor: await persistence.cursor(),
25      mode,
26    });
27    if (persistence.persistBatch) {
28      await persistence.persistBatch(discovered.changes, discovered.nextCursor);
29    } else {
30      if (!persistence.apply || !persistence.commitCursor) {
31        throw new Error(
32          "Connector persistence must support batch or individual writes.",
33        );
34      }
35      for (const change of discovered.changes) await persistence.apply(change);
36      if (discovered.nextCursor !== null) {
37        await persistence.commitCursor(discovered.nextCursor);
38      }
39    }
40    await persistence.recordRun?.("succeeded");
41  } catch (error) {
42    await persistence.recordRun?.("failed");
43    throw error;
44  }
45}
§ 01 · CORE IMPL

核心实现,逐段拆

▸batch 优先,逐条 fallback

worker/src/connector-runner.ts:27 先用 persistBatch,worker/src/connector-runner.ts:30 检查 individual write 能力,worker/src/connector-runner.ts:35 逐条 apply,worker/src/connector-runner.ts:36 到 worker/src/connector-runner.ts:38 最后提交 cursor。

src/worker/src/connector-runner.ts · L17-45TypeScript
17export async function runConnector(
18  adapter: ConnectorAdapter,
19  persistence: ConnectorPersistence,
20  mode: "sync" | "reconcile" = "sync",
21) {
22  try {
23    const discovered = await adapter.discover({
24      cursor: await persistence.cursor(),
25      mode,
26    });
27    if (persistence.persistBatch) {
28      await persistence.persistBatch(discovered.changes, discovered.nextCursor);
29    } else {
30      if (!persistence.apply || !persistence.commitCursor) {
31        throw new Error(
32          "Connector persistence must support batch or individual writes.",
33        );
34      }
35      for (const change of discovered.changes) await persistence.apply(change);
36      if (discovered.nextCursor !== null) {
37        await persistence.commitCursor(discovered.nextCursor);
38      }
39    }
40    await persistence.recordRun?.("succeeded");
41  } catch (error) {
42    await persistence.recordRun?.("failed");
43    throw error;
44  }
45}

▸channel start 的 UI 顺序也有事务感

useStartChannel 从 app/src/lib/channels/start.ts:13 导出,mutation 创建在 app/src/lib/channels/start.ts:16,start 函数是 app/src/lib/channels/start.ts:20;createChannel 调用在 app/src/lib/channels/start.ts:21,cache seed 是 app/src/lib/channels/start.ts:22,首消息暂存是 app/src/lib/channels/start.ts:23,navigate 从 app/src/lib/channels/start.ts:24 到 app/src/lib/channels/start.ts:27 完成 展示 UI 开新 channel 的顺序:create mutation、seed query cache、stash first message、navigate。app/src/lib/channels/start.ts:10 到 app/src/lib/channels/start.ts:11 的注释说明这是为了让第一条消息在 thread join 前可见。

src/app/src/lib/channels/start.ts · L13-31TypeScript
13export function useStartChannel() {
14  const queryClient = useQueryClient();
15  const navigate = useNavigate();
16  const createChannel = useMutation(createChannelMutationOptions(queryClient));
17
18  return {
19    pending: createChannel.isPending,
20    start: async (agentId: string, text: string) => {
21      const channel = await createChannel.mutateAsync([agentId]);
22      queryClient.setQueryData(channelKeys.detail(channel.id), channel);
23      stashFirstMessage(channel.id, text);
24      await navigate({
25        params: { channelId: channel.id },
26        replace: true,
27        to: "/channel/$channelId",
28      });
29    },
30  };
31}
§ 02 · KEY FLOW

关键流程

▸同步游标只代表已持久化位置

cursor() → adapter.discover(cursor, mode) → persistBatch(changes,nextCursor) OR apply(change)* then commitCursor(nextCursor) → recordRun(succeeded) ↘ catch → recordRun(failed) → throw

这个流程的要点是 cursor 代表“已经安全持久化到哪里”,所以 worker/tests/connector-runner.test.ts:27 的失败测试很重要:如果 apply 抛错,worker/tests/connector-runner.test.ts:48 断言 cursors 仍为空。

§ 03 · EDGE CASES

边界 / 失败模式

▸失败不推进游标

  • worker/src/connector-runner.ts:31 在缺少 batch 和 individual write 能力时直接抛错,避免静默丢数据。
  • worker/src/connector-runner.ts:41 到 worker/src/connector-runner.ts:43 失败会记录 run failed 并继续抛出,调度层能重试。
  • worker/tests/connector-runner.test.ts:51 验证 reconcile mode 被传给 adapter,说明同步模式不是 runner 私自决定。
Generated · 2026-08-21 Source · CopilotKit/OpenBot ← 返回 Trending 索引