# 多注册流程来源与驱动注册设计

本文解决的是另一个会在第二个 flow 接入时立刻爆炸的问题：当前项目虽然有 `tab-runtime`，但“来源 source 是谁、该注入什么脚本、URL family 怎么判定、localhost callback 应该清谁”仍然带着明显的 OpenAI 单流程假设。

## 1. 当前源码里的真实耦合

### 1.1 `signup-page` 其实不是抽象 source，而是 OpenAI Auth 标签页别名

目前这些地方都把 OpenAI Auth / Consent / Add-phone 页面统称为 `signup-page`：

- `content/utils.js` 的 `detectScriptSource()`
- `background/navigation-utils.js` 的 `matchesSourceUrlFamily()`
- `background/tab-runtime.js` 的注册表、冲突清理、callback 清理

这说明当前的 source 体系不是“可扩展的来源注册表”，而是“OpenAI 时代遗留的 source 命名”。

### 1.2 `content/utils.js` 的默认回退不安全

当前 `detectScriptSource()` 在无法识别页面时会回退成 `vps-panel`。这对单流程时期问题不大，但多 flow 下会出现两个风险：

1. 新 flow 页面被误识别成 `vps-panel`
2. `PING / READY / COMPLETE` 日志和标签注册打到错误 source

### 1.3 `tab-runtime` 的 localhost cleanup 直接写死 `registry['signup-page']`

`closeLocalhostCallbackTabs()` 现在默认把匹配 callback 的残留 tab 从 `signup-page` 名下清掉。将来只要第二个 flow 也有 callback，或者 callback 不是来自 OpenAI Auth 页，这里就会清错或漏清。

### 1.4 manifest 的静态注入列表目前基本围绕 OpenAI

`manifest.json` 里的静态内容脚本匹配域名，目前只有 OpenAI Auth 页是明确的 flow 页面入口。第二个 flow 如果沿用这套做法，后续只会不断在 manifest 里继续加分散规则，缺少统一 source 定义中心。

## 2. 设计目标

后续必须拆成两层注册表：

1. `sourceRegistry`：管理“页面来源与标签页生命周期”
2. `driverRegistry`：管理“这个来源页面由哪个 content driver 负责，支持哪些命令”

这两层不要再混成一个 `signup-page` 字符串。

## 3. `sourceRegistry` 设计

建议 source 用稳定 ID，而不是模糊业务词。

```js
sourceRegistry = {
  'openai-auth': {
    flowId: 'openai',
    kind: 'flow-page',
    label: 'OpenAI 认证页',
    hostPatterns: [
      'https://auth.openai.com/*',
      'https://auth0.openai.com/*',
      'https://accounts.openai.com/*',
    ],
    familyMatcher: 'openai-auth-family',
    injectFiles: [
      'content/activation-utils.js',
      'content/utils.js',
      'content/operation-delay.js',
      'content/auth-page-recovery.js',
      'content/phone-country-utils.js',
      'content/phone-auth.js',
      'content/signup-page.js',
    ],
    cleanupScopes: ['oauth-localhost-callback'],
  },

  'mail-gmail': {
    flowId: null,
    kind: 'mail-provider',
    label: 'Gmail',
    hostPatterns: ['https://mail.google.com/*'],
    familyMatcher: 'gmail-family',
    injectFiles: ['content/activation-utils.js', 'content/utils.js', 'content/gmail-mail.js'],
  },

  'panel-sub2api': {
    flowId: 'openai',
    kind: 'panel-page',
    label: 'SUB2API 后台',
    dynamicOnly: true,
    familyMatcher: 'sub2api-panel-family',
    injectFiles: ['content/utils.js', 'content/sub2api-panel.js'],
  },
};
```

### 3.1 source 里至少要有这些字段

- `id`
- `flowId`
- `kind`
- `label`
- `hostPatterns`
- `familyMatcher`
- `injectFiles`
- `readyPolicy`
- `cleanupScopes`
- `tabReusePolicy`

### 3.2 source 不等于 flow

一个 flow 会有多个 source：

- `openai-entry`
- `openai-auth`
- `openai-plus-checkout`
- `openai-paypal`
- `openai-gopay`
- `panel-sub2api`

同样，一个 source 也可能不属于具体 flow，例如：

- `mail-gmail`
- `mail-163`
- `mail-2925`
- `mail-inbucket`

## 4. `driverRegistry` 设计

`driverRegistry` 解决的是“页面上能做什么”，不是“标签页如何复用”。

```js
driverRegistry = {
  'openai-auth': {
    sourceId: 'openai-auth',
    driverId: 'content/signup-page',
    commands: [
      'OPEN_SIGNUP',
      'SUBMIT_SIGNUP_IDENTIFIER',
      'SUBMIT_PASSWORD',
      'SUBMIT_PROFILE',
      'SUBMIT_LOGIN_CODE',
      'SUBMIT_PHONE_CODE',
      'DETECT_AUTH_STATE',
    ],
  },

  'mail-gmail': {
    sourceId: 'mail-gmail',
    driverId: 'content/gmail-mail',
    commands: ['POLL_MAILBOX', 'OPEN_MESSAGE', 'DELETE_MESSAGE'],
  },
};
```

这样做的好处是：

- `tab-runtime` 只关心 source
- workflow / mail service 只关心 driver capability
- 后续换内容脚本实现时，不需要改 tab 生命周期代码

## 5. `signup-page` 的迁移策略

`signup-page` 不能直接继续作为未来通用 source 名字。

建议迁移方式：

1. 新增正式 source：`openai-auth`
2. 在 `sourceRegistry.aliases` 里临时保留：
   - `signup-page -> openai-auth`
3. `getSourceLabel()`、`matchesSourceUrlFamily()`、`ensureContentScriptReadyOnTab()` 全部优先读注册表
4. 等 OpenAI 全链路迁完后，再删除 `signup-page` 别名

## 6. localhost callback cleanup 怎么改

当前 callback 清理逻辑最大的问题是“只知道 callback URL，不知道 callback 属于哪个 flow source”。

建议改成：

```js
callbackRegistry = {
  'oauth-localhost-callback': {
    ownerSourceId: 'openai-auth',
    matcher: isLocalhostOAuthCallbackUrl,
    clearOwnerTabOnClose: true,
  },
};
```

`closeLocalhostCallbackTabs()` 的输入要升级为：

- `cleanupScope`
- `callbackUrl`
- `ownerSourceId`

而不是默认拿 `signup-page`。

## 7. `content/utils.js` 的改法

### 7.1 禁止默认回退成 `vps-panel`

新的规则应该是：

- 有 `window.__MULTIPAGE_SOURCE` 时，以注入 source 为准
- 没有显式 source 时，只在静态 host map 里做白名单匹配
- 仍无法识别时返回 `unknown-source`

`unknown-source` 不参与 tab 注册，不自动 `reportReady()`，只记录诊断日志。

### 7.2 child frame ready 规则也要转到注册表

当前 `shouldReportReadyForFrame()` 里写死了多个 source 名称。后续应改成 source metadata：

- `readyPolicy: 'top-frame-only'`
- `readyPolicy: 'allow-child-frame'`

## 8. manifest / 动态注入策略

原则上以后由 `sourceRegistry` 作为单一事实来源，manifest 只是实现载体。

建议约定：

- 稳定公共页面可继续走静态 `content_scripts`
- 变动大、域名多、只在运行期打开的页面改走 `chrome.scripting.executeScript`
- 每个 source 明确自己的 `hostPatterns` 和 `injectFiles`
- 新增 flow 时，不允许直接在业务代码里散写 `injectFiles = [...]`

## 9. 与网络策略的关系

`sourceRegistry` 只负责页面来源，不负责代理策略；但每个 flow 应同时提供自己的 `networkProfile`，至少包含：

- `guardDomains`
- `probeEndpoints`
- `failCloseDomains`

否则 `ip-proxy-core` 仍会继续写死 `chatgpt.com / openai.com`。

## 10. 第一阶段迁移顺序

1. 引入 `sourceRegistry / driverRegistry` 与旧 source alias。
2. 把 `getSourceLabel()`、`matchesSourceUrlFamily()`、`ensureContentScriptReadyOnTab()` 改成读注册表。
3. 去掉 `detectScriptSource()` 的 `vps-panel` 默认回退。
4. 改 `closeLocalhostCallbackTabs()`，不再写死 `signup-page`。
5. 第二个 flow 接入时，严格要求先注册 source 和 driver，再写步骤逻辑。

## 11. 本文对应解决的缺口

本文主要补齐以下缺口：

- `signup-page` 实际是 OpenAI source，不是通用注册页
- source URL family / content ready / localhost callback cleanup 没有统一注册表
- manifest 与动态注入没有统一入口
- `content/utils.js` 的默认 source 回退会污染未来 flow

