PostHog Self-driving Inbox 新信号源接入指南:从 SignalSourceProduct 枚举到 Scout Emitter 的三端改造
本文为 PostHog「Self-driving Inbox(自动驾驶收件箱)」的源码级扩展指南:当你需要把一个新的数据仓库来源(Jira、GitLab、Sentry、Intercom 等)接入 PostHog 的信号体系时,需要同时改动后端的信号发射器(scout emitter)、Code 前端的开关与表单接线,以及(可选的)CLI 自助配置向导。读完本文,你能独立完成一个新 inbox 信号源从枚举、迁移、HogQL 取数到 UI 开关卡片的全链路落地,并理解每个环节的底层实现与常见陷阱。
一、Self-driving Inbox 的工作原理
Self-driving Inbox 连接的是 PostHog 的数据仓库源(data-warehouse source):它同步一张「可行动记录」表(issues / tickets / conversations 等),再由 posthog/posthog 后端的一个服务端 signals scout 监听新行,把 findings/reports 发射进 Code Inbox。
关键设计约束是:scout 并不泛化地处理任意表——它由一个以 (source_type, table) 为键的静态注册表驱动。因此新增一个来源必然触碰三个表面(surface):
| 表面 | 改动内容 |
|---|---|
posthog/posthog(本仓库) |
新的 scout emitter + 注册表条目 + SignalSourceProduct 枚举值(+ 迁移)+ 契约变体。前提:数据仓库源本身必须已经存在(所有 Tier-1 源均已存在) |
posthog/code(独立仓库) |
约 8 个 UI/接线文件:source-product 联合类型、开关卡片、设置表单、hook 映射、图标、过滤项(OAuth 源无需额外服务/路由) |
PostHog/context-mill(独立仓库,可选) |
self-driving 技能的 connected-tools 列表,使 npx @posthog/wizard self-driving 主动推荐该来源。可跳过——跳过后来源在其他地方照常工作,只是向导不会主动推荐 |
前两个表面是强制的(缺了它们来源既无法发射信号,也无法在应用里被开关);第三个表面只决定 CLI 向导是否在 onboarding 时「主动提供」该来源。
部署顺序(不可跳过)
后端 SignalSourceProduct 的选择项必须先于 Code UI 上线。Code 端开关调用 createSignalSourceConfig({ source_product }),而 Django 模型会拒绝不在 choices 中的 source_product,返回 400。因此:先合入 posthog/posthog 的 PR(至少先合入枚举迁移),再合入 posthog/code 的 PR。若两个 PR 同时提交,应在 Code PR 描述中注明依赖关系。
二、后端(posthog/posthog)改造清单
Scout 位于 products/signals/backend/。数据仓库 → 信号的通用管道(fetcher、gate、warehouse hook)都是可复用的,每个来源真正新增的只有 emitter 模块和一条注册表条目,共 7 步:
1. 枚举:SignalSourceProduct
在 enums.py 中添加 SignalSourceProduct 成员,并同步添加 SIGNAL_SOURCE_PRODUCT_LABELS 条目。该文件的设计意图在源码注释中写得很清楚:这是一个「Django-free 的纯 StrEnum」,为了让 contracts.py、模型层和前端 codegen 都能低成本地导入同一份分类法,StrEnum 成员与字符串值相等,可直接用于 == 比较与 ORM 过滤。
模型侧的 choices 由 signal_source_product_choices() 这个可调用函数生成——从 models.py 的 SignalSourceConfig 结构看,source_product 字段的 choices 直接绑定到该函数,因此新增一个标签项即自动成为模型合法值;源码注释也说明了这样做的目的:让新来源「成为单文件改动」,且 choices 的取值/顺序与 SIGNAL_SOURCE_PRODUCT_LABELS 保持一致,避免产生无意义的 AlterField 迁移噪音。
当前枚举已覆盖 Tier-1 客服/工单类(FRESHDESK、FRESHSERVICE、FRONT、GORGIAS、KUSTOMER、DIXA、PLAIN)、issue 跟踪类(GITLAB、GITEA、SHORTCUT)、错误跟踪类(SENTRY、ROLLBAR、BUGSNAG、HONEYBADGER、RAYGUN)、Tier-2 安全扫描器(SNYK、SONARQUBE、SEMGREP、RAPID7_INSIGHTVM)、Tier-3 反馈/评论类以及 OAuth 客服源(INTERCOM、HUBSPOT)等——可见注册表机制已在生产中被大规模复用。
2. SourceType(通常无需改动)
models.py 中 SignalSourceConfig.SourceType 只有在记录类型不是已有的 ISSUE / TICKET 时才需要新增成员(例如 scanner 类来源的 SCANNER_FINDING、反馈类的 FEEDBACK/REVIEW)。源码注释特别强调:emission 注册表能发射的每个 source_type 都必须出现在这里,否则启用该来源时会 400。
3. 迁移
执行 python manage.py makemigrations signals 生成 NNNN_alter_signalsourceconfig_source_product。一次接入多个来源时,把所有新枚举值集中进同一个迁移——多个并行 PR 各自往该模型加迁移会在合并队列中冲突。
4. Emitter:每来源一个新模块
新建 products/signals/backend/emission/<source>_<table>.py,导出一份 SignalSourceTableConfig。其字段定义见 registry.py:
source_product/source_type:必须与SignalSourceConfig的 choices 一致;emitter:emitter(row) -> SignalEmitterOutput(source_product, source_type, source_id, description, weight, extra)的发射函数;record_fetcher:取数函数,数据仓库源统一用data_warehouse_record_fetcher;partition_field:增量游标列;partition_field_is_datetime_string:来源以字符串存时间时置 True;fields:需要 SELECT 的列(只取 emitter 和 extra 元数据用到的);可选where_clause;- 可选的 LLM
actionability_prompt(判定记录是否可行动,prompt 必须包含{description}占位符,由 pydantic 校验器强制)、summarization_prompt+description_summarization_threshold_chars(长描述摘要,二者必须同时设置); - 其他防御性字段:
max_records(默认 1000)、first_sync_lookback_days(首次同步回看天数,默认 7)、unloggable_fields(日志脱敏列)。
最可靠的抄写模板是 github_issues.py,但必须先看目标表的真实列结构——阅读 products/warehouse_sources/backend/temporal/data_imports/sources/<name>/ 下的 settings.py / canonical_descriptions.py 确认精确列名。
⚠️ 并非所有来源都是扁平列。 GitHub/Linear 把
title/body/created_at暴露为顶层列,通用 fetcher 的SELECT {fields} FROM {table} WHERE {partition_field} > cursor直接可用。但 Jira 的issues表只有id、key、self、fields(嵌套 JSON blob)、expand——summary/description/status全部藏在fields里。对这类来源必须在 emitter 里 SELECTfields并用JSONExtractString(fields, '…')提取,partition_field也要写成 JSON 表达式。registry.py 中SignalSourceTableConfig的注释明确指出,partition_field会被直接插值进 HogQL 的WHERE,因此 JSON 表达式形式是被支持的——但文档仍强调:这是「最容易微妙出错的一点」,克隆配置后必须实际跑一次同步来验证,而不能只靠读代码。
以 jira_issues.py 为实证:该来源对状态/优先级/指派人用 JSONExtractString(JSONExtractRaw(fields, 'status'), 'name') 之类的嵌套提取(见第 57–69 行 FIELDS),where_clause 过滤掉 Done/Closed/Resolved 终态工单;由于 created 被 source 提升为顶层列但其存储类型随同步而异,partition_field 写作 "toString(created)" 并配合 partition_field_is_datetime_string=True(源码注释建议首次启用时对照真实同步验证)。另外 Jira 描述是 Atlassian Document Format 富文本 JSON,emitter 里专门实现了 _adf_to_text() 做尽力而为的纯文本提取。
5. 注册:registry
在 registry.py 的 _register_all_emitters() 中添加 import 并注册,当前仓库的实际 API 是:
register_signal_source(
ExternalDataSourceType.<YOURSOURCE>, # 来自 products/warehouse_sources/backend/facade/types.py
"<table>", # 如 "issues" / "tickets" / "conversations"
<CONFIG>,
)
注册键是 (source_type.value, table)。注意源码中一个特殊约定:GitHub 的 schema 行是带定界符的 owner/repo.issues,因此 _registry_key() 对 GITHUB 会先做 github_split_schema_name 拆出裸 endpoint 再查表(见 registry.py 第 114–119 行)。
6. 契约变体
在 contracts.py 中为该来源添加一个 Literal[SignalSourceProduct.X] 的 payload 变体——该文件里每个来源一个带字面量限定 source_product 的 pydantic 模型(Jira 的变体在第 209 行附近),用于约束信号 payload 的形状。
7. 通用管道零改动
通用 fetcher(data_warehouse.py)、gate(emission/gate.py)与 warehouse hook 接线都不需要任何修改。从 fetcher 源码可以看清增量语义:有 last_synced_at 时按 partition_expr > last_synced_at 连续同步;首次同步则回看 first_sync_lookback_days 天;若 partition_field_is_datetime_string 为真,分区表达式会被包进 parseDateTimeBestEffort(...)。查询通过 execute_hogql_query(..., query_type="EmitSignalsNewRecords", bypass_warehouse_access_control=True) 执行,属于内部数据导入信号通道(无用户上下文)。
验证
- 迁移能干净地 apply,之后
python manage.py makemigrations --check无输出; ExternalDataSourceType对应成员存在(registry 中从products/warehouse_sources/backend/facade/types.py导入,可对照注册表文件核对)。
三、Code 前端(posthog/code 仓库)改造清单
Product key 是小写的 source_product(如 "jira")。在 packages/ 下不区分大小写地 grep "zendesk"——每个与来源列表相关的命中点都要加上新 product。标准清单:
类型门槛(每个来源必改)
packages/shared/src/inbox-types.ts— 在SourceProduct联合类型中加入"jira";packages/api-client/src/posthog-client.ts— 加入SignalSourceConfig.source_product联合类型;只有当记录类型不是已有的issue/ticket时才新增source_type值。
UI 实路径(每个来源必改)
packages/ui/src/features/inbox/hooks/useSignalSourceToggles.ts—SetupSourceProduct、SOURCE_TYPE_MAP、SOURCE_LABELS、DATA_WAREHOUSE_SOURCES({ dwSourceType, requiredTable })、ALL_SOURCE_PRODUCTS与computeValues初始化对象;packages/ui/src/features/inbox/components/SignalSourceToggles.tsx—SignalSourceValues字段、toggleX/setupX回调,以及「External connections」列中的一张<SignalSourceToggleCard>(icon/label/description +requiresSetup/onSetup/loading/syncStatus);packages/ui/src/features/inbox/components/DataSourceSetup.tsx—DataSourceType、REQUIRED_SCHEMAS和 switch 分支。凭据类来源直接把分支路由到<DynamicSourceSetup sourceType="Jira" title="Connect Jira" schemas={schemasPayload("jira")} … />——不新建任何表单组件。只有 OAuth/资源选择类来源才需要定制XSetup;packages/ui/src/features/inbox/components/utils/source-product-icons.tsx—SOURCE_PRODUCT_META条目(Icon、color、label);没有合适的 Phosphor 图标时内联一个 SVG 图标文件(可抄PgAnalyzeIcon.tsx);packages/ui/src/features/inbox/filterOptions.tsx—INBOX_SOURCE_OPTIONS条目(来源过滤下拉)。
Core 镜像层(保持同步)
packages/core/src/inbox/signalSourceService.ts— 镜像SOURCE_TYPE_MAP、DATA_WAREHOUSE_SOURCES、ALL_SOURCE_PRODUCTS、computeSourceValues初始化,外加WarehouseSourceProduct/SignalSourceValues;packages/core/src/inbox/dataSourceService.ts—DataSourceType、REQUIRED_SCHEMAS与createXDataSource方法。
这两处今天不在 UI 实路径上,但 SignalSourceService/DataSourceService 镜像着相同映射,要保持一致。
OAuth 管线——已不再需要每来源定制
现在存在通用的、按 kind 参数化的集成流程:IntegrationService(packages/core/src/integrations/integration.ts)+ integration tRPC 路由(packages/host-router/src/routers/integration.router.ts)。DynamicSourceSetup 通过字段的 kind 走这条通用路由启动任意 OAuth 流程。所以新的 OAuth 来源不需要新的 per-kind 服务、symbol 或路由——不要按来源去克隆 linear.ts / linear-integration.router.ts(旧的 linear/slack/github 路由仍服务于其他调用方,保留即可)。
验证(Code 仓库)
- 动了
inbox-types.ts之后跑pnpm --filter @posthog/shared build(它是发布型类型包); pnpm typecheck(联合类型被跨包消费,需全仓类型检查);biome lint packages/core packages/ui— 零noRestrictedImports,导入有序。
四、设置表单:用动态渲染器,不要硬编码
不要手写每来源的设置表单。 PostHog Cloud 通过 HTTP 下发每个来源连接表单的字段 schema,应用用 DynamicSourceSetup(packages/ui/src/features/inbox/components/DynamicSourceSetup.tsx)通用渲染。新的凭据类来源零表单代码——只需把 DataSourceSetup 的 switch 分支路由到 DynamicSourceSetup,传入大写 sourceType 和要同步的 schemas。
- 端点:
GET /api/environments/{projectId}/external_data_sources/wizard/?source_type=<Type>→Record<string, SourceConfig>。客户端方法PostHogAPIClient.getExternalDataSourceConfigs,hook 为useSourceConfig(sourceType)。 SourceConfig.fields是联合类型:input(text/email/password/url/number…)、select、switch-group、oauth、oauth-account-select、ssh-tunnel、file-upload。DynamicSourceSetup对 input/select/switch-group 以及oauth/oauth-account-select通用渲染,并按字段的name构建createExternalDataSourcepayload。后端是字段名/标签/必填/secret 的唯一事实来源,表单永不漂移。- 字段
name就是payload键——你不再手工维护它们(Jira →subdomain、email、api_token,除 token 外全部secret:false)。
OAuth 来源同样无需定制表单。 DynamicSourceSetup 通用渲染 oauth 字段(一个「连接」按钮:按 kind 启动流程,轮询 getIntegrationsForProject 等待新集成出现,并把其 id 写入 payload[<name>])与 oauth-account-select 字段(对集成资源做服务端搜索的选择器,覆盖 OAuth 之后才需选资源的场景,如 GitHub 的 repo picker)。因此 OAuth 来源(Intercom、HubSpot、Salesforce…)与凭据来源一样只是一次注册表条目改动。支持的 OAuth kind 列表见 posthog/models/integration/oauth.py 中 OauthIntegration.supported_kinds(含 slack, salesforce, hubspot, google-ads, …, intercom, linear, clickup, jira, …, stripe,另加经 App install 的 github)。不在该列表中的来源必须走 API-key 表单——其仓库连接器直接吃凭据,与 OAuth 集成列表无关(注意:Jira 的仓库源用的也是 API token 而非 OAuth kind=jira)。
只有两种字段类型仍缺通用渲染器(此时禁用提交):ssh-tunnel 与 file-upload,需路由到定制表单。旧的 GitHubSetup/ZendeskSetup/PgAnalyzeSetup 硬编码表单仅为历史原因保留——可以留,也可以顺手迁到 DynamicSourceSetup,但不要再新增。
full_refresh 细节:issues 类来源(github/linear/jira)会在 useSignalSourceToggles.ts 的 ensureRequiredTableSyncing 中把 issues 强制为 full_refresh——因为 issue 会被编辑/关闭,增量 append 会漏掉更新。若你的新来源也同步 issues 表,需把它加入该条件;ticket/conversation 类来源只需强制 should_sync=true。
五、来源目录(Tier-1 参考表)
source_type 是传给 createExternalDataSource 的大写 DWH 类型串。实现时应对照 external_data_sources serializer / 该来源的 source.py 精确核对 source_type 与 payload 键名:
| Product | source_type |
Table | Auth | 设置模板 | payload 键 |
|---|---|---|---|---|---|
| Jira | Jira |
issues |
API token | DynamicSourceSetup |
subdomain, email, api_token |
| GitLab | GitLab |
issues |
API token | DynamicSourceSetup |
gitlab_host, personal_access_token, project |
| Sentry | Sentry |
issues |
API token | DynamicSourceSetup |
auth_token, organization_slug, api_base_url? |
| Freshdesk | Freshdesk |
tickets |
API key | DynamicSourceSetup |
subdomain, api_key |
| Front | Front |
conversations |
API token | DynamicSourceSetup |
api_token |
| Gorgias | Gorgias |
tickets |
API key | DynamicSourceSetup |
gorgias_domain, email, api_key |
| Intercom | Intercom |
conversations |
OAuth (kind=intercom) |
DynamicSourceSetup |
intercom_integration_id |
(Zendesk tickets、GitHub issues、Linear issues、pganalyze issues+servers、Jira issues 已经上线——照抄即可,不要重复添加。Jira 行作为 payload 键与 JSON-blob emitter 陷阱的规范范例保留。)
对照当前仓库的 registry.py,上表中的 GitLab issues、Sentry issues、Freshdesk tickets、Front conversations、Gorgias tickets、Intercom conversations 均已注册,说明该目录表与生产注册表一致。
六、self-driving 向导表面(PostHog/context-mill,可选)
前两个表面让来源在应用里工作,但不会让 CLI npx @posthog/wizard self-driving 的 onboarding 主动推荐它。该流程是由 PostHog/context-mill 仓库里一个技能驱动的 AI agent,其 connected-tool 列表是硬编码的——不动态读取向导端点。因此在把它加进去之前,新来源对 self-driving onboarding 不可见。
需要编辑 context/skills/self-driving/references/5-connected-tools.md(以及 4-sources.md、2-read-context.md、description.md 中涉及来源列表的段落):
- 把工具加入 step-5 的
wizard_ask多选选项({ label, value }); - 添加 responder 映射——与 posthog emitter 使用同一
source_product/source_type对(如 Jira →jira/issue); - 把其 DWH
source_type加入「已连接」检测列表; - 按运行方式分类:
- 凭据类来源(API key/token —— Jira、Zendesk、pganalyze):运行中无法收集凭据,所以永不把用户送去做 UI 流程。归入 Zendesk 组——arm 休眠的 responder 并记录 follow-up。无 connector 文件。
- 一键 OAuth 来源(Linear 风格,
kind=<source>集成):新增一个专用 connector 引用(5X-<source>.md,克隆5b-linear.md),移交授权链接并从集成 id 创建来源。
dist/ 是 gitignored 的(CI 里构建),但建议运行一次 node scripts/build.js(Node ≥20)确认技能能打包——构建会校验结构。
七、created_via 归因
ExternalDataSource.created_via 记录来源以何种方式创建,并且由服务端从请求传输层派生(event_usage.py 中的 get_event_source,按 user-agent 判定)——API 调用方不能直接设置它,这是刻意为之(否则任何客户端都能自我贴标签)。新来源自动继承该机制,无每来源接线。机器值 mcp 会按调用方升级:
| 调用方 | 传输层 | created_via |
|---|---|---|
| PostHog Desktop app inbox(posthog/code) | EventSource.POSTHOG_CODE |
self_driving |
npx @posthog/wizard(self-driving 程序) |
EventSource.WIZARD |
self_driving |
| 其他 MCP 客户端 | EventSource.MCP |
mcp |
升级逻辑位于 external_data_source.py 的 _create_external_data_source 中:源码显示 created_via 在创建时写入后不可再变更(PATCH 时被 pop 掉),且当机器注入的值为 mcp 时会用 transport_created_via 映射按 get_event_source(request) 覆写;WIZARD 传输经 is_wizard_self_driving_program(request) 判定后进一步升级为 self_driving。
八、Worktree、PR 规范与陷阱
Worktree 设置(后端工作)
# 从本地 posthog/posthog 克隆出发
git fetch origin
git worktree add ../worktrees/inbox-<source> -b <your-branch-prefix>/inbox-<source>-source origin/master
- 若 agent 沙箱未配置 GPG 签名密钥,对该次提交禁用签名(
git -c commit.gpgsign=false commit)以免因缺 key 失败。 - 不要反射性地加
--no-verify:pre-commit/pre-push 钩子会跑 lint、type-check 与ci:preflight,跳过它们会让破坏直达 CI。仅在某个钩子在 worktree 里确实无法运行时绕过它,且先手工跑完上文 Verify 步骤。 - PR 以 ready for review(非 draft)状态提交;语义化提交前缀用
feat(data-warehouse):(凡触碰 warehouse sources / signals)。 - 合并后清理 worktree:
git worktree remove ../worktrees/inbox-<source>。
PR 规范
- 每个来源、每个仓库各一个 PR。标题:
feat(data-warehouse): add <Source> as a self-driving inbox source。 - PR 正文基于仓库 PR 模板;后端 PR 先行(或共享枚举迁移先行),Code PR 引用后端 PR 并声明部署顺序依赖。
- 迁移卫生:若开多个后端 PR,把所有新
SignalSourceProduct值放进一个 prep PR 的迁移里,各来源 emitter PR 不携带迁移——否则冲突。 - 按
merging-prs技能经 Trunk 队列合入各 PR。
陷阱清单
source_type(DWH,大写,如"Jira")≠source_product(小写,如"jira")≠SourceType记录种类("issue")。三个不同的字段。DATA_WAREHOUSE_SOURCES通过source_type.toLowerCase()匹配已存在的外部来源——所以dwSourceType必须等于真实的 DWH 类型串。- 精确的
payload键才是真正的风险点,必须与该source_type的 posthog serializer 匹配。上线前对照source.py确认。 - 每个 product 都要覆盖
SOURCE_PRODUCT_META和INBOX_SOURCE_OPTIONS(inbox 的 CLAUDE.md 特别点名),否则 findings 渲染时缺图标/过滤项。 - 不要与
packages/core/src/onboarding/githubConnectService.ts混淆——那是仓库访问(clone/branch/PR),与 warehouse source 无关。
小结
PostHog Self-driving Inbox 的扩展模型是「通用管道 + 静态注册」:fetcher、gate、契约校验、动态表单渲染器全部复用,每新增一个来源只写一份 SignalSourceTableConfig 及其 emitter,再按三份清单(后端七步、前端九处、可选向导)完成接线。掌握 (source_type, table) 注册键、source_type/source_product/SourceType 三字段区分、以及 Jira 式 JSON-blob 表的 JSONExtractString 取数与 partition_field 表达式这三点,就能把任意新的 Tier-1 来源安全地接入 inbox 信号体系。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00